← Commit history

Update 2 file(s)

Colby Knox ·52f08e1f85 ·9d ago ·parent 62866a8
2 files changed +435
tests/fixtures/sch_drill.lbradded+264
@@ -0,0 +1,264 @@+<?xml version="1.0" encoding="utf-8"?>+<eagle version="9.6.2">+<drawing>+<settings>+<setting alwaysvectorfont="no" />+</settings>+<grid distance="0.1" unitdist="inch" unit="inch" style="lines" multiple="1" display="no" altdistance="0.01" altunitdist="inch" altunit="inch" />+<layers>+<layer number="1" name="Top" color="4" fill="1" visible="yes" active="yes" />+<layer number="16" name="Bottom" color="1" fill="1" visible="yes" active="yes" />+<layer number="17" name="Pads" color="2" fill="1" visible="yes" active="yes" />+<layer number="18" name="Vias" color="2" fill="1" visible="yes" active="yes" />+<layer number="19" name="Unrouted" color="6" fill="1" visible="yes" active="yes" />+<layer number="20" name="Dimension" color="15" fill="1" visible="yes" active="yes" />+<layer number="21" name="tPlace" color="7" fill="1" visible="yes" active="yes" />+<layer number="22" name="bPlace" color="7" fill="1" visible="yes" active="yes" />+<layer number="23" name="tOrigins" color="15" fill="1" visible="yes" active="yes" />+<layer number="24" name="bOrigins" color="15" fill="1" visible="yes" active="yes" />+<layer number="25" name="tNames" color="7" fill="1" visible="yes" active="yes" />+<layer number="26" name="bNames" color="7" fill="1" visible="yes" active="yes" />+<layer number="27" name="tValues" color="7" fill="1" visible="yes" active="yes" />+<layer number="28" name="bValues" color="7" fill="1" visible="yes" active="yes" />+<layer number="29" name="tStop" color="7" fill="3" visible="no" active="yes" />+<layer number="30" name="bStop" color="7" fill="6" visible="no" active="yes" />+<layer number="31" name="tCream" color="7" fill="4" visible="no" active="yes" />+<layer number="32" name="bCream" color="7" fill="5" visible="no" active="yes" />+<layer number="33" name="tFinish" color="6" fill="3" visible="no" active="yes" />+<layer number="34" name="bFinish" color="6" fill="6" visible="no" active="yes" />+<layer number="35" name="tGlue" color="7" fill="4" visible="no" active="yes" />+<layer number="36" name="bGlue" color="7" fill="5" visible="no" active="yes" />+<layer number="37" name="tTest" color="7" fill="1" visible="no" active="yes" />+<layer number="38" name="bTest" color="7" fill="1" visible="no" active="yes" />+<layer number="39" name="tKeepout" color="4" fill="11" visible="no" active="yes" />+<layer number="40" name="bKeepout" color="1" fill="11" visible="no" active="yes" />+<layer number="41" name="tRestrict" color="4" fill="10" visible="no" active="yes" />+<layer number="42" name="bRestrict" color="1" fill="10" visible="no" active="yes" />+<layer number="43" name="vRestrict" color="2" fill="10" visible="no" active="yes" />+<layer number="44" name="Drills" color="7" fill="1" visible="no" active="yes" />+<layer number="45" name="Holes" color="7" fill="1" visible="no" active="yes" />+<layer number="46" name="Milling" color="3" fill="1" visible="no" active="yes" />+<layer number="47" name="Measures" color="7" fill="1" visible="no" active="yes" />+<layer number="48" name="Document" color="7" fill="1" visible="yes" active="yes" />+<layer number="49" name="Reference" color="7" fill="1" visible="yes" active="yes" />+<layer number="51" name="tDocu" color="7" fill="1" visible="yes" active="yes" />+<layer number="52" name="bDocu" color="7" fill="1" visible="yes" active="yes" />+<layer number="91" name="Nets" color="2" fill="1" visible="yes" active="yes" />+<layer number="92" name="Busses" color="1" fill="1" visible="yes" active="yes" />+<layer number="93" name="Pins" color="2" fill="1" visible="no" active="yes" />+<layer number="94" name="Symbols" color="4" fill="1" visible="yes" active="yes" />+<layer number="95" name="Names" color="7" fill="1" visible="yes" active="yes" />+<layer number="96" name="Values" color="7" fill="1" visible="yes" active="yes" />+<layer number="97" name="Info" color="7" fill="1" visible="yes" active="yes" />+<layer number="98" name="Guide" color="6" fill="1" visible="yes" active="yes" />+</layers>+<library>+<description>made by fusion-bridge</description>+<packages>+<package name="R0603">+<smd name="1" x="-0.762" y="0" dx="0.6096" dy="0.762" layer="1" />+<smd name="2" x="0.762" y="0" dx="0.6096" dy="0.762" layer="1" />+<wire x1="-1.3168" y1="-0.631" x2="1.3168" y2="-0.631" width="0.1" layer="51" />+<wire x1="1.3168" y1="-0.631" x2="1.3168" y2="0.631" width="0.1" layer="51" />+<wire x1="1.3168" y1="0.631" x2="-1.3168" y2="0.631" width="0.1" layer="51" />+<wire x1="-1.3168" y1="0.631" x2="-1.3168" y2="-0.631" width="0.1" layer="51" />+<text x="-1.3168" y="0.931" size="1" layer="25">&gt;NAME</text>+<text x="-1.3168" y="-1.931" size="1" layer="27">&gt;VALUE</text>+</package>+<package name="C0603">+<smd name="1" x="-0.762" y="0" dx="0.6096" dy="0.762" layer="1" />+<smd name="2" x="0.762" y="0" dx="0.6096" dy="0.762" layer="1" />+<wire x1="-1.3168" y1="-0.631" x2="1.3168" y2="-0.631" width="0.1" layer="51" />+<wire x1="1.3168" y1="-0.631" x2="1.3168" y2="0.631" width="0.1" layer="51" />+<wire x1="1.3168" y1="0.631" x2="-1.3168" y2="0.631" width="0.1" layer="51" />+<wire x1="-1.3168" y1="0.631" x2="-1.3168" y2="-0.631" width="0.1" layer="51" />+<text x="-1.3168" y="0.931" size="1" layer="25">&gt;NAME</text>+<text x="-1.3168" y="-1.931" size="1" layer="27">&gt;VALUE</text>+</package>+<package name="SOT23">+<smd name="1" x="-0.9398" y="-1.016" dx="0.6096" dy="0.7112" layer="1" />+<smd name="2" x="0.9398" y="-1.016" dx="0.6096" dy="0.7112" layer="1" />+<smd name="3" x="0" y="1.016" dx="0.6096" dy="0.7112" layer="1" />+<wire x1="-1.4946" y1="-1.6216" x2="1.4946" y2="-1.6216" width="0.1" layer="51" />+<wire x1="1.4946" y1="-1.6216" x2="1.4946" y2="1.6216" width="0.1" layer="51" />+<wire x1="1.4946" y1="1.6216" x2="-1.4946" y2="1.6216" width="0.1" layer="51" />+<wire x1="-1.4946" y1="1.6216" x2="-1.4946" y2="-1.6216" width="0.1" layer="51" />+<text x="-1.4946" y="1.9216" size="1" layer="25">&gt;NAME</text>+<text x="-1.4946" y="-2.9216" size="1" layer="27">&gt;VALUE</text>+</package>+<package name="QUAD">+<smd name="1" x="-1" y="-1" dx="0.6" dy="0.7" layer="1"/>+<smd name="2" x="1" y="-1" dx="0.6" dy="0.7" layer="1"/>+<smd name="3" x="1" y="1" dx="0.6" dy="0.7" layer="1"/>+<smd name="4" x="-1" y="1" dx="0.6" dy="0.7" layer="1"/>+<text x="-2" y="2" size="1" layer="25">&gt;NAME</text>+</package>+</packages>+<symbols>+<symbol name="RES_PWR">+<wire x1="-5.08" y1="-3.81" x2="5.08" y2="-3.81" width="0.254" layer="94" />+<wire x1="5.08" y1="-3.81" x2="5.08" y2="3.81" width="0.254" layer="94" />+<wire x1="5.08" y1="3.81" x2="-5.08" y2="3.81" width="0.254" layer="94" />+<wire x1="-5.08" y1="3.81" x2="-5.08" y2="-3.81" width="0.254" layer="94" />+<text x="-5.08" y="4.61" size="1.778" layer="95">&gt;NAME</text>+<text x="-5.08" y="-6.31" size="1.778" layer="96">&gt;VALUE</text>+<pin name="A" x="-10.16" y="1.27" length="middle" direction="pas" />+<pin name="B" x="10.16" y="1.27" length="middle" direction="pas" rot="R180" />+</symbol>+<symbol name="CAP_PWR">+<wire x1="-5.08" y1="-3.81" x2="5.08" y2="-3.81" width="0.254" layer="94" />+<wire x1="5.08" y1="-3.81" x2="5.08" y2="3.81" width="0.254" layer="94" />+<wire x1="5.08" y1="3.81" x2="-5.08" y2="3.81" width="0.254" layer="94" />+<wire x1="-5.08" y1="3.81" x2="-5.08" y2="-3.81" width="0.254" layer="94" />+<text x="-5.08" y="4.61" size="1.778" layer="95">&gt;NAME</text>+<text x="-5.08" y="-6.31" size="1.778" layer="96">&gt;VALUE</text>+<pin name="A" x="-10.16" y="1.27" length="middle" direction="pas" />+<pin name="B" x="10.16" y="1.27" length="middle" direction="pas" rot="R180" />+</symbol>+<symbol name="NPN_PWR">+<wire x1="-5.08" y1="-3.81" x2="5.08" y2="-3.81" width="0.254" layer="94" />+<wire x1="5.08" y1="-3.81" x2="5.08" y2="3.81" width="0.254" layer="94" />+<wire x1="5.08" y1="3.81" x2="-5.08" y2="3.81" width="0.254" layer="94" />+<wire x1="-5.08" y1="3.81" x2="-5.08" y2="-3.81" width="0.254" layer="94" />+<text x="-5.08" y="4.61" size="1.778" layer="95">&gt;NAME</text>+<text x="-5.08" y="-6.31" size="1.778" layer="96">&gt;VALUE</text>+<pin name="B" x="-10.16" y="1.27" length="middle" direction="pas" />+<pin name="E" x="10.16" y="1.27" length="middle" direction="pas" rot="R180" />+<pin name="C" x="10.16" y="-1.27" length="middle" direction="pas" rot="R180" />+</symbol>+<symbol name="RES_MOVED">+<wire x1="-5.08" y1="-3.81" x2="5.08" y2="-3.81" width="0.254" layer="94" />+<wire x1="5.08" y1="-3.81" x2="5.08" y2="3.81" width="0.254" layer="94" />+<wire x1="5.08" y1="3.81" x2="-5.08" y2="3.81" width="0.254" layer="94" />+<wire x1="-5.08" y1="3.81" x2="-5.08" y2="-3.81" width="0.254" layer="94" />+<text x="-5.08" y="4.61" size="1.778" layer="95">&gt;NAME</text>+<text x="-5.08" y="-6.31" size="1.778" layer="96">&gt;VALUE</text>+<pin name="A" x="-15.24" y="1.27" length="middle" direction="pas" />+<pin name="B" x="15.24" y="1.27" length="middle" direction="pas" rot="R180" />+</symbol>+<symbol name="RES_Y">+<wire x1="-5.08" y1="-3.81" x2="5.08" y2="-3.81" width="0.254" layer="94" />+<wire x1="5.08" y1="-3.81" x2="5.08" y2="3.81" width="0.254" layer="94" />+<wire x1="5.08" y1="3.81" x2="-5.08" y2="3.81" width="0.254" layer="94" />+<wire x1="-5.08" y1="3.81" x2="-5.08" y2="-3.81" width="0.254" layer="94" />+<text x="-5.08" y="4.61" size="1.778" layer="95">&gt;NAME</text>+<text x="-5.08" y="-6.31" size="1.778" layer="96">&gt;VALUE</text>+<pin name="A" x="-10.16" y="6.35" length="middle" direction="pas" />+<pin name="B" x="10.16" y="1.27" length="middle" direction="pas" rot="R180" />+</symbol>+</symbols>+<devicesets>+<deviceset name="RES" prefix="R">+<gates>+<gate name="P" symbol="RES_PWR" x="0" y="0" />+</gates>+<devices>+<device name="" package="R0603">+<connects>+<connect gate="P" pin="A" pad="1" />+<connect gate="P" pin="B" pad="2" />+</connects>+<technologies>+<technology name="">+<attribute name="ADOM_PINMAP" value="1:P.A;2:P.B" constant="no" />+</technology>+</technologies>+</device>+</devices>+</deviceset>+<deviceset name="CAP" prefix="C">+<gates>+<gate name="P" symbol="CAP_PWR" x="0" y="0" />+</gates>+<devices>+<device name="" package="C0603">+<connects>+<connect gate="P" pin="A" pad="1" />+<connect gate="P" pin="B" pad="2" />+</connects>+<technologies>+<technology name="">+<attribute name="ADOM_PINMAP" value="1:P.A;2:P.B" constant="no" />+</technology>+</technologies>+</device>+</devices>+</deviceset>+<deviceset name="NPN" prefix="Q">+<gates>+<gate name="P" symbol="NPN_PWR" x="0" y="0" />+</gates>+<devices>+<device name="" package="SOT23">+<connects>+<connect gate="P" pin="B" pad="1" />+<connect gate="P" pin="C" pad="3" />+<connect gate="P" pin="E" pad="2" />+</connects>+<technologies>+<technology name="">+<attribute name="ADOM_PINMAP" value="1:P.B;2:P.E;3:P.C" constant="no" />+</technology>+</technologies>+</device>+</devices>+</deviceset>+<deviceset name="RES2" prefix="R">+<gates>+<gate name="P" symbol="RES_MOVED" x="0" y="0" />+</gates>+<devices>+<device name="" package="R0603">+<connects>+<connect gate="P" pin="A" pad="1" />+<connect gate="P" pin="B" pad="2" />+</connects>+<technologies>+<technology name="">+<attribute name="ADOM_PINMAP" value="1:P.A;2:P.B" constant="no" />+</technology>+</technologies>+</device>+</devices>+</deviceset>+<deviceset name="DUAL" prefix="U" uservalue="yes">+<gates>+<gate name="A" symbol="RES_PWR" x="0" y="0"/>+<gate name="B" symbol="RES_PWR" x="0" y="12.7"/>+</gates>+<devices>+<device name="" package="QUAD">+<connects>+<connect gate="A" pin="A" pad="1"/>+<connect gate="A" pin="B" pad="2"/>+<connect gate="B" pin="A" pad="3"/>+<connect gate="B" pin="B" pad="4"/>+</connects>+<technologies>+<technology name=""/>+</technologies>+</device>+</devices>+</deviceset>+<deviceset name="RESY" prefix="R">+<gates>+<gate name="P" symbol="RES_Y" x="0" y="0" />+</gates>+<devices>+<device name="" package="R0603">+<connects>+<connect gate="P" pin="A" pad="1" />+<connect gate="P" pin="B" pad="2" />+</connects>+<technologies>+<technology name="">+<attribute name="ADOM_PINMAP" value="1:P.A;2:P.B" constant="no" />+</technology>+</technologies>+</device>+</devices>+</deviceset>+</devicesets>+</library>+</drawing>+</eagle>
tests/sch_drill.pyadded+171
@@ -0,0 +1,171 @@+#!/usr/bin/env python3+"""Live drill of the schematic edit verbs: each one runs for real on a throwaway seeded design, and each+result is checked with a separate read. The daily battery skips these (they edit a design), which is how+fusion_sch_swap_part stayed broken from 1.13.8 to 1.13.53 with only its dry run ever exercised.++    python3 tests/sch_drill.py --target DESKTOP-BAR6S0F [--dir C:/Users/<you>/Documents/fusion-lane-tests/drill]++It opens a new design and closes it again, and touches nothing else. Run it when no other lane has this+Fusion in front. Exit 1 when any step fails."""+import argparse+import json+import os+import subprocess+import sys+import time++ap = argparse.ArgumentParser()+ap.add_argument('--target', required=True)+ap.add_argument('--dir', default='C:/Users/olive/Documents/fusion-lane-tests/drill')+ap.add_argument('--keep', action='store_true', help='leave the design open')+opts = ap.parse_args()+A = ['adom-bridge', '--target', opts.target, '--ai-thread', 'fusion-lane']+D = opts.dir+LBR = D + '/sch_drill.lbr'+FIXTURE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'fixtures', 'sch_drill.lbr')+results = []+++def call(verb, args=None):+    out = subprocess.run(A + [verb, json.dumps(args or {})], capture_output=True, text=True).stdout+    try:+        return json.loads(out)+    except Exception:+        return {'success': False, 'error': out[:300]}+++def step(name, verb, args, check=None):+    """Run a verb for real, then the separate read. check returns None when good, else what is wrong."""+    r = call(verb, args)+    why = None+    if r.get('success') is not True:+        why = '%s: %s' % (r.get('errorCode'), str(r.get('error'))[:170])+    elif check:+        try:+            why = check(r)+        except Exception as e:+            why = 'check raised %r' % (e,)+    results.append((name, why))+    print('%-34s %s' % (name, 'PASS' if why is None else 'FAIL  ' + why), flush=True)+    return r+++def pins(*refs):+    rows = call('fusion_sch_pins', {'designators': list(refs)}).get('pins') or []+    return {(p['ref'], p['pin']): p for p in rows}+++def net(ref, pin):+    return pins(ref).get((ref, pin), {}).get('net')+++def parts():+    return {c.get('designator'): c for c in call('fusion_schematic_data', {'pins': False}).get('components') or []}+++def items(*kinds):+    return call('fusion_sch_items', {'kinds': list(kinds)})+++def expect(cond, why):+    return None if cond else why+++# ── the design ─────────────────────────────────────────────────────────────────────────────────────+print(json.dumps(call('send_files', {'filePaths': [FIXTURE], 'dest': D, 'destIsFolder': True, 'reason': 'sch_drill: its test library'}).get('success')), 'library sent')+fixed = [{'ref': 'R1', 'deviceset': 'RES', 'value': '10k', 'x': 10, 'y': 10, 'nets': {'A': 'NA', 'B': 'NB'}},+         {'ref': 'R2', 'deviceset': 'RES', 'value': '22k', 'x': 20, 'y': 10, 'nets': {'A': 'NC', 'B': 'ND'}}]+r = call('fusion_board_seed', {'libraryPath': LBR, 'outDir': D, 'name': 'Drill', 'title': 'drill', 'outlineMm': [80, 60], 'fixed': fixed})+if r.get('success') is not True:+    sys.exit('seed failed: %s' % r.get('error'))+run = call('fusion_new_electronics_from_eagle', {'schPath': D + '/Drill.sch', 'brdPath': D + '/Drill.brd'}).get('runId')+for _ in range(40):+    time.sleep(6)+    st = call('fusion_eagle_import_status', {'runId': run})+    if st.get('stage') in ('done', 'failed'):+        break+if st.get('stage') != 'done':+    sys.exit('import failed: %s' % json.dumps(st.get('result'))[:300])+call('fusion_show_schematic', {'expectDocument': 'Untitled'})++# ── placing ────────────────────────────────────────────────────────────────────────────────────────+for ref, sym, x, y in (('R4', 'RES', 3000, 3000), ('R5', 'RES', 3000, 4000), ('R6', 'RES', 4500, 4000), ('R7', 'RES', 3000, 5000), ('R8', 'RES', 4500, 5000)):+    step('sch_place_part ' + ref, 'fusion_sch_place_part', {'library': LBR, 'symbol': sym, 'designator': ref, 'x': x, 'y': y},+         lambda _r, ref=ref: expect(ref in parts(), ref + ' not on the sheet'))+step('sch_place_part partId 2', 'fusion_sch_place_part', {'library': LBR, 'symbol': 'DUAL', 'designator': 'U2', 'partId': 2, 'x': 6000, 'y': 3000},+     lambda _r: expect([g.get('gate') for g in parts()['U2'].get('gates') or []] == ['B'], 'gate is not B'))+r = call('fusion_sch_place_part', {'library': LBR, 'symbol': 'RES', 'designator': 'R9', 'x': 100000, 'y': 1000})+results.append(('off the sheet refused', expect(r.get('errorCode') == 'off_the_sheet', str(r.get('errorCode')))))+print('%-34s %s' % (results[-1][0], 'PASS' if results[-1][1] is None else 'FAIL  ' + str(results[-1][1])))++# ── wiring ─────────────────────────────────────────────────────────────────────────────────────────+step('sch_connect label', 'fusion_sch_connect', {'connections': [{'designator': 'R4', 'pin': 'A', 'net': 'N1'}]},+     lambda _r: expect(net('R4', 'A') == 'N1', 'R4.A is on %s' % net('R4', 'A')))+step('sch_connect wire', 'fusion_sch_connect', {'connections': [{'designator': 'R4', 'pin': 'B', 'net': 'N2', 'kind': 'wire'}]},+     lambda _r: expect(net('R4', 'B') == 'N2', 'R4.B is on %s' % net('R4', 'B')))+step('sch_connect_pins', 'fusion_sch_connect_pins', {'pairs': [{'from': 'R5.1', 'to': 'R6.1'}]},+     lambda _r: expect(net('R5', 'A') and net('R5', 'A') == net('R6', 'A'), 'R5.A %s, R6.A %s' % (net('R5', 'A'), net('R6', 'A'))))+step('sch_edit_label', 'fusion_sch_edit_label', {'edits': [{'net': 'N1', 'by': [0, 2.54]}]})+step('sch_text', 'fusion_sch_text', {'texts': [{'text': 'DRILL NOTE', 'at': [20, 40]}]},+     lambda _r: expect('DRILL NOTE' in json.dumps(items('texts')), 'text not on the sheet'))+w = (pins('R4').get(('R4', 'B')) or {}).get('at')+wires = [x for x in items('wires').get('wires') or []]+stub = next((x for x in wires if w and ('%g,%g' % tuple(w)) in str(x.get('id'))), wires[0] if wires else {})+step('sch_split_wire', 'fusion_sch_split_wire', {'id': stub.get('id')})+junctions = items('junctions').get('junctions') or []+step('sch_split_wire left a junction', 'fusion_sch_items', {'kinds': ['junctions']}, lambda _r: expect(junctions, 'no junction on the sheet'))+step('sch_junction remove', 'fusion_sch_junction', {'remove': [(junctions or [{}])[0].get('id')]},+     lambda _r: expect(len(items('junctions').get('junctions') or []) == len(junctions) - 1, 'junction still there'))+wires = items('wires').get('wires') or []+step('sch_delete_wire', 'fusion_sch_delete_wire', {'ids': [(wires or [{}])[-1].get('id')]},+     lambda _r: expect(len(items('wires').get('wires') or []) < len(wires), 'wire still there'))++# ── moving and editing ─────────────────────────────────────────────────────────────────────────────+a0 = pins('R6')[('R6', 'A')]['at']+step('sch_move_region', 'fusion_sch_move_region', {'box': [a0[0] - 15, a0[1] - 6, a0[0] + 40, a0[1] + 6], 'by': [10.16, 0]},+     lambda _r: expect(abs(pins('R6')[('R6', 'A')]['at'][0] - a0[0] - 10.16) < 0.01, 'R6 did not move'))+step('sch_edit rotate', 'fusion_sch_edit', {'edits': [{'designator': 'R7', 'action': 'rotate', 'degrees': 90}]},+     lambda _r: expect((parts()['R7'].get('gates') or [{}])[0].get('rotation') in (90, 90.0), 'R7 not turned'))+step('sch_reset_fields', 'fusion_sch_reset_fields', {'references': ['R7']})+step('sch_fix_connectivity dry', 'fusion_sch_fix_connectivity', {})++# ── buses ──────────────────────────────────────────────────────────────────────────────────────────+# a long bus under R8, whose pins are on a row above it, so an entry from each pin lands on the bus+r8 = pins('R8')+bus_y = r8[('R8', 'A')]['at'][1] - 20.32+step('sch_bus', 'fusion_sch_bus', {'label': 'D[0..1]', 'points': [[60, bus_y], [190, bus_y]]},+     lambda _r: expect(items('buses').get('buses'), 'no bus on the sheet'))+step('sch_connect_to_bus', 'fusion_sch_connect_to_bus', {'pins': ['R8.1', 'R8.2'], 'prefix': 'D', 'entry': 'down'},+     lambda _r: expect(net('R8', 'A') == 'D0' and net('R8', 'B') == 'D1', 'R8 on %s, %s' % (net('R8', 'A'), net('R8', 'B'))))+step('sch_bus_entry', 'fusion_sch_bus_entry', {'entries': [{'at': [70, bus_y + 2.54], 'direction': 'down-right', 'member': 'D0'}]},+     lambda _r: expect(items('busEntries').get('busEntries'), 'no bus entry on the sheet'))++# ── swapping ───────────────────────────────────────────────────────────────────────────────────────+n1 = (net('R1', 'A'), net('R1', 'B'))+step('sch_swap_part moved pins', 'fusion_sch_swap_part', {'library': LBR, 'symbol': 'RES2', 'designator': 'R1'},+     lambda r: expect(parts()['R1'].get('libReference') == 'RES2' and (net('R1', 'A'), net('R1', 'B')) == n1 and len(r.get('pinsJoined') or []) == 2, 'R1 %s %s' % (parts()['R1'].get('libReference'), r.get('pinsJoined'))))+step('sch_swap_part pin off its wire', 'fusion_sch_swap_part', {'library': LBR, 'symbol': 'RESY', 'designator': 'R1'},+     lambda r: expect((net('R1', 'A'), net('R1', 'B')) == n1, 'nets now %s' % ((net('R1', 'A'), net('R1', 'B')),)))+step('sch_swap_part prefix, keepComment', 'fusion_sch_swap_part', {'library': LBR, 'symbol': 'CAP', 'designator': 'R2', 'keepComment': True},+     lambda _r: expect(parts().get('R2', {}).get('libReference') == 'CAP' and parts()['R2'].get('comment') == '22k', 'R2 %s' % (parts().get('R2'),)))+step('board kept in step', 'fusion_cross_check', {},+     lambda r: expect(not [x for x in r.get('onSheetNotOnBoard') or [] if x != 'U2'] and not r.get('onBoardNotOnSheet'), json.dumps({k: r.get(k) for k in ('onSheetNotOnBoard', 'onBoardNotOnSheet')})))++# ── the rest ───────────────────────────────────────────────────────────────────────────────────────+step('sch_batch', 'fusion_sch_batch', {'ops': [{'op': 'sch_text', 'texts': [{'at': [25, 45], 'text': 'BATCH NOTE'}]}]},+     lambda _r: expect('BATCH NOTE' in json.dumps(items('texts')), 'batch text not on the sheet'))+def sheet_count():+    return len((call('fusion_sch_hierarchy', {}).get('tree') or {}).get('sheets') or [])+++n = sheet_count()+step('sch_create', 'fusion_sch_create', {}, lambda _r: expect(sheet_count() == n + 1, 'sheets %d, was %d' % (sheet_count(), n)))++dialogs = call('fusion_check_dialogs', {}).get('count')+results.append(('no dialog left', expect(not dialogs, '%s dialog(s) open' % dialogs)))+if not opts.keep:+    for _ in range(4):+        call('fusion_close_document', {'name': 'Untitled'})+bad = [n for n, why in results if why]+print('\n%d PASS, %d FAIL%s' % (len(results) - len(bad), len(bad), ': ' + ', '.join(bad) if bad else ''))+sys.exit(1 if bad else 0)