Add FreeRouting JAR oracle harness for behaviour-level validation
FreeRouting ships no unit tests for its geometry/router, so there is no value-level oracle to port against. This adds a dev/test-only harness that runs the reference JAR to route a DSN, letting freeroute's output be diffed against the reference implementation — the router phase will assert connectivity parity (same nets routed) via routed_net_set(). - tests/oracle.py: locate Java 21+ and a freerouting JAR (env overrides: FREEROUTE_ORACLE_JAVA, FREEROUTING_JAR), route a DSN, and extract routed connectivity from the SES. requires_oracle skips when no JVM/JAR is present, so the suite stays Java-free. - tests/test_oracle.py: routes a routable board end-to-end and checks the connectivity extraction. - tests/dsn/fixtures/kicad_routable.dsn: a real KiCad pcbnew-exported DSN (Arduino_Mega template, path sanitized) that actually routes — the smd_demo fixture leaves its nets unrouted. - pyproject: pythonpath=["tests"] so the harness imports as `oracle`.
This commit is contained in:
parent
c905032a39
commit
18f3afc8a5
@ -56,4 +56,5 @@ force-sort-within-sections = true
|
||||
[tool.pytest.ini_options]
|
||||
minversion = "7.0"
|
||||
testpaths = ["tests"]
|
||||
pythonpath = ["tests"]
|
||||
addopts = ["-ra", "--strict-markers"]
|
||||
|
||||
447
tests/dsn/fixtures/kicad_routable.dsn
Normal file
447
tests/dsn/fixtures/kicad_routable.dsn
Normal file
@ -0,0 +1,447 @@
|
||||
(pcb kicad_routable
|
||||
(parser
|
||||
(string_quote ")
|
||||
(space_in_quoted_tokens on)
|
||||
(host_cad "KiCad's Pcbnew")
|
||||
(host_version "10.0.4")
|
||||
)
|
||||
(resolution um 10)
|
||||
(unit um)
|
||||
(structure
|
||||
(layer F.Cu
|
||||
(type signal)
|
||||
(property
|
||||
(index 0)
|
||||
)
|
||||
)
|
||||
(layer B.Cu
|
||||
(type signal)
|
||||
(property
|
||||
(index 1)
|
||||
)
|
||||
)
|
||||
(boundary
|
||||
(path pcb 0 199060 -49200 199060 -59360 201600 -61900 201600 -96190
|
||||
199060 -98730 199060 -99238 199058 -99323.6 199020 -99490.5
|
||||
198945 -99644.7 198839 -99778.5 198705 -99885.2 198550 -99959.5
|
||||
198384 -99997.6 198298 -100000 100762 -100000 100676 -99997.6
|
||||
100510 -99959.5 100355 -99885.2 100221 -99778.5 100115 -99644.7
|
||||
100040 -99490.5 100002 -99323.6 100000 -99238 100000 -47422
|
||||
100002 -47336.4 100040 -47169.5 100115 -47015.3 100221 -46881.5
|
||||
100355 -46774.8 100510 -46700.5 100676 -46662.4 100762 -46660
|
||||
196520 -46660 199060 -49200)
|
||||
)
|
||||
(via "Via[0-1]_600:300_um")
|
||||
(rule
|
||||
(width 200)
|
||||
(clearance 200)
|
||||
(clearance 50 (type smd_smd))
|
||||
)
|
||||
)
|
||||
(placement
|
||||
(component Connector_PinSocket_2.54mm:PinSocket_2x18_P2.54mm_Vertical
|
||||
(place J7 193980 -92380 front 180 (PN Digital))
|
||||
)
|
||||
(component Connector_PinSocket_2.54mm:PinSocket_1x08_P2.54mm_Vertical
|
||||
(place J1 127940 -97460 front 90 (PN Power))
|
||||
(place J3 150800 -97460 front 90 (PN Analog))
|
||||
(place J5 173660 -97460 front 90 (PN Analog))
|
||||
(place J4 145720 -49200 front 90 (PN PWM))
|
||||
(place J6 168580 -49200 front 90 (PN Communication))
|
||||
)
|
||||
(component Connector_PinSocket_2.54mm:PinSocket_1x10_P2.54mm_Vertical
|
||||
(place J2 118796 -49200 front 90 (PN PWM))
|
||||
)
|
||||
(component Arduino_MountingHole:MountingHole_3.2mm
|
||||
(place MH6 196520 -97460 front 0 (PN MountingHole_3.2mm))
|
||||
(place MH1 115240 -49200 front 0 (PN MountingHole_3.2mm))
|
||||
(place MH2 113970 -97460 front 0 (PN MountingHole_3.2mm))
|
||||
(place MH3 166040 -64440 front 0 (PN MountingHole_3.2mm))
|
||||
(place MH4 166040 -92380 front 0 (PN MountingHole_3.2mm))
|
||||
(place MH5 190170 -49200 front 0 (PN MountingHole_3.2mm))
|
||||
)
|
||||
)
|
||||
(library
|
||||
(image Connector_PinSocket_2.54mm:PinSocket_2x18_P2.54mm_Vertical
|
||||
(outline (polygon signal 0 -4335 1795 1755 1795 1755 -44945 -4335 -44945))
|
||||
(outline (path signal 120 1330 -1270 1330 -44510))
|
||||
(outline (path signal 120 1330 1330 1330 0))
|
||||
(outline (path signal 120 0 1330 1330 1330))
|
||||
(outline (path signal 120 -1270 -1270 1330 -1270))
|
||||
(outline (path signal 120 -1270 1330 -1270 -1270))
|
||||
(outline (path signal 120 -3870 -44510 1330 -44510))
|
||||
(outline (path signal 120 -3870 1330 -1270 1330))
|
||||
(outline (path signal 120 -3870 1330 -3870 -44510))
|
||||
(outline (path signal 100 1270 -44450 -3810 -44450))
|
||||
(outline (path signal 100 1270 270 1270 -44450))
|
||||
(outline (path signal 100 270 1270 1270 270))
|
||||
(outline (path signal 100 -3810 -44450 -3810 1270))
|
||||
(outline (path signal 100 -3810 1270 270 1270))
|
||||
(pin Rect[A]Pad_1700.000000x1700.000000_um 1 0 0)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 2 -2540 0)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 3 0 -2540)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 4 -2540 -2540)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 5 0 -5080)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 6 -2540 -5080)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 7 0 -7620)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 8 -2540 -7620)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 9 0 -10160)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 10 -2540 -10160)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 11 0 -12700)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 12 -2540 -12700)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 13 0 -15240)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 14 -2540 -15240)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 15 0 -17780)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 16 -2540 -17780)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 17 0 -20320)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 18 -2540 -20320)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 19 0 -22860)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 20 -2540 -22860)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 21 0 -25400)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 22 -2540 -25400)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 23 0 -27940)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 24 -2540 -27940)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 25 0 -30480)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 26 -2540 -30480)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 27 0 -33020)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 28 -2540 -33020)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 29 0 -35560)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 30 -2540 -35560)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 31 0 -38100)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 32 -2540 -38100)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 33 0 -40640)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 34 -2540 -40640)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 35 0 -43180)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 36 -2540 -43180)
|
||||
)
|
||||
(image Connector_PinSocket_2.54mm:PinSocket_1x08_P2.54mm_Vertical
|
||||
(outline (polygon signal 0 -1795 -19545 -1795 1795 1745 1795 1745 -19545))
|
||||
(outline (path signal 120 1330 1330 1330 0))
|
||||
(outline (path signal 120 0 1330 1330 1330))
|
||||
(outline (path signal 120 1330 -1270 1330 -19110))
|
||||
(outline (path signal 120 -1330 -1270 1330 -1270))
|
||||
(outline (path signal 120 -1330 -1270 -1330 -19110))
|
||||
(outline (path signal 120 -1330 -19110 1330 -19110))
|
||||
(outline (path signal 100 635 1270 1270 635))
|
||||
(outline (path signal 100 -1270 1270 635 1270))
|
||||
(outline (path signal 100 1270 635 1270 -19050))
|
||||
(outline (path signal 100 1270 -19050 -1270 -19050))
|
||||
(outline (path signal 100 -1270 -19050 -1270 1270))
|
||||
(pin Rect[A]Pad_1700.000000x1700.000000_um 1 0 0)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 2 0 -2540)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 3 0 -5080)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 4 0 -7620)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 5 0 -10160)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 6 0 -12700)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 7 0 -15240)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 8 0 -17780)
|
||||
)
|
||||
(image Connector_PinSocket_2.54mm:PinSocket_1x10_P2.54mm_Vertical
|
||||
(outline (polygon signal 0 -1795 -24595 -1795 1795 1745 1795 1745 -24595))
|
||||
(outline (path signal 120 1330 1330 1330 0))
|
||||
(outline (path signal 120 0 1330 1330 1330))
|
||||
(outline (path signal 120 1330 -1270 1330 -24190))
|
||||
(outline (path signal 120 -1330 -1270 1330 -1270))
|
||||
(outline (path signal 120 -1330 -1270 -1330 -24190))
|
||||
(outline (path signal 120 -1330 -24190 1330 -24190))
|
||||
(outline (path signal 100 635 1270 1270 635))
|
||||
(outline (path signal 100 -1270 1270 635 1270))
|
||||
(outline (path signal 100 1270 635 1270 -24130))
|
||||
(outline (path signal 100 1270 -24130 -1270 -24130))
|
||||
(outline (path signal 100 -1270 -24130 -1270 1270))
|
||||
(pin Rect[A]Pad_1700.000000x1700.000000_um 1 0 0)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 2 0 -2540)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 3 0 -5080)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 4 0 -7620)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 5 0 -10160)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 6 0 -12700)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 7 0 -15240)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 8 0 -17780)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 9 0 -20320)
|
||||
(pin Oval[A]Pad_1700.000000x1700.000000_um 10 0 -22860)
|
||||
)
|
||||
(image Arduino_MountingHole:MountingHole_3.2mm
|
||||
(outline (polygon signal 0 194.668 1686.29 449.803 1636.81 694.386 1548.96 922.704 1424.81
|
||||
1129.38 1267.27 1309.59 1080.02 1459.11 867.447 1574.42 634.549
|
||||
1652.84 386.775 1692.51 129.897 1695 0 1692.51 -129.896 1652.84 -386.774
|
||||
1574.42 -634.548 1459.11 -867.446 1309.59 -1080.01 1129.38 -1267.27
|
||||
922.704 -1424.81 694.386 -1548.96 449.803 -1636.81 194.668 -1686.29
|
||||
-65.017 -1696.24 -323.188 -1666.44 -573.776 -1597.57 -810.924 -1491.26
|
||||
-1029.06 -1350 -1223.08 -1177.09 -1388.42 -976.599 -1521.23 -753.21
|
||||
-1618.38 -512.161 -1677.59 -259.121 -1697.49 0 -1677.59 259.122
|
||||
-1618.38 512.162 -1521.23 753.211 -1388.42 976.6 -1223.08 1177.09
|
||||
-1029.06 1350 -810.924 1491.26 -573.776 1597.57 -323.188 1666.44
|
||||
-65.017 1696.24))
|
||||
(keepout "" (circle F.Cu 3700))
|
||||
(keepout "" (circle B.Cu 3700))
|
||||
)
|
||||
(padstack Oval[A]Pad_1700.000000x1700.000000_um
|
||||
(shape (path F.Cu 1700 0 0 0 0))
|
||||
(shape (path B.Cu 1700 0 0 0 0))
|
||||
(attach off)
|
||||
)
|
||||
(padstack Rect[A]Pad_1700.000000x1700.000000_um
|
||||
(shape (rect F.Cu -850 -850 850 850))
|
||||
(shape (rect B.Cu -850 -850 850 850))
|
||||
(attach off)
|
||||
)
|
||||
(padstack "Via[0-1]_600:300_um"
|
||||
(shape (circle F.Cu 600))
|
||||
(shape (circle B.Cu 600))
|
||||
(attach off)
|
||||
)
|
||||
)
|
||||
(network
|
||||
(net GND
|
||||
(pins J7-1 J7-2 J1-6 J1-7 J2-4)
|
||||
)
|
||||
(net /*52
|
||||
(pins J7-3)
|
||||
)
|
||||
(net /53
|
||||
(pins J7-4)
|
||||
)
|
||||
(net /50
|
||||
(pins J7-5)
|
||||
)
|
||||
(net /51
|
||||
(pins J7-6)
|
||||
)
|
||||
(net /48
|
||||
(pins J7-7)
|
||||
)
|
||||
(net /49
|
||||
(pins J7-8)
|
||||
)
|
||||
(net /*46
|
||||
(pins J7-9)
|
||||
)
|
||||
(net /47
|
||||
(pins J7-10)
|
||||
)
|
||||
(net /*44
|
||||
(pins J7-11)
|
||||
)
|
||||
(net /*45
|
||||
(pins J7-12)
|
||||
)
|
||||
(net /42
|
||||
(pins J7-13)
|
||||
)
|
||||
(net /43
|
||||
(pins J7-14)
|
||||
)
|
||||
(net /40
|
||||
(pins J7-15)
|
||||
)
|
||||
(net /41
|
||||
(pins J7-16)
|
||||
)
|
||||
(net /38
|
||||
(pins J7-17)
|
||||
)
|
||||
(net /39
|
||||
(pins J7-18)
|
||||
)
|
||||
(net /36
|
||||
(pins J7-19)
|
||||
)
|
||||
(net /37
|
||||
(pins J7-20)
|
||||
)
|
||||
(net /34
|
||||
(pins J7-21)
|
||||
)
|
||||
(net /35
|
||||
(pins J7-22)
|
||||
)
|
||||
(net /32
|
||||
(pins J7-23)
|
||||
)
|
||||
(net /33
|
||||
(pins J7-24)
|
||||
)
|
||||
(net /30
|
||||
(pins J7-25)
|
||||
)
|
||||
(net /31
|
||||
(pins J7-26)
|
||||
)
|
||||
(net /28
|
||||
(pins J7-27)
|
||||
)
|
||||
(net /29
|
||||
(pins J7-28)
|
||||
)
|
||||
(net /26
|
||||
(pins J7-29)
|
||||
)
|
||||
(net /27
|
||||
(pins J7-30)
|
||||
)
|
||||
(net /24
|
||||
(pins J7-31)
|
||||
)
|
||||
(net /25
|
||||
(pins J7-32)
|
||||
)
|
||||
(net /22
|
||||
(pins J7-33)
|
||||
)
|
||||
(net /23
|
||||
(pins J7-34)
|
||||
)
|
||||
(net +5V
|
||||
(pins J7-35 J7-36 J1-5)
|
||||
)
|
||||
(net /IOREF
|
||||
(pins J1-2)
|
||||
)
|
||||
(net /A0
|
||||
(pins J3-1)
|
||||
)
|
||||
(net /A1
|
||||
(pins J3-2)
|
||||
)
|
||||
(net /A2
|
||||
(pins J3-3)
|
||||
)
|
||||
(net /A3
|
||||
(pins J3-4)
|
||||
)
|
||||
(net /A4
|
||||
(pins J3-5)
|
||||
)
|
||||
(net /A5
|
||||
(pins J3-6)
|
||||
)
|
||||
(net /A6
|
||||
(pins J3-7)
|
||||
)
|
||||
(net /A7
|
||||
(pins J3-8)
|
||||
)
|
||||
(net /A8
|
||||
(pins J5-1)
|
||||
)
|
||||
(net /A9
|
||||
(pins J5-2)
|
||||
)
|
||||
(net /A10
|
||||
(pins J5-3)
|
||||
)
|
||||
(net /A11
|
||||
(pins J5-4)
|
||||
)
|
||||
(net /A12
|
||||
(pins J5-5)
|
||||
)
|
||||
(net /A13
|
||||
(pins J5-6)
|
||||
)
|
||||
(net /A14
|
||||
(pins J5-7)
|
||||
)
|
||||
(net /A15
|
||||
(pins J5-8)
|
||||
)
|
||||
(net /AREF
|
||||
(pins J2-3)
|
||||
)
|
||||
(net /*13
|
||||
(pins J2-5)
|
||||
)
|
||||
(net /*12
|
||||
(pins J2-6)
|
||||
)
|
||||
(net /*11
|
||||
(pins J2-7)
|
||||
)
|
||||
(net /*10
|
||||
(pins J2-8)
|
||||
)
|
||||
(net /*9
|
||||
(pins J2-9)
|
||||
)
|
||||
(net /*8
|
||||
(pins J2-10)
|
||||
)
|
||||
(net /*7
|
||||
(pins J4-1)
|
||||
)
|
||||
(net /*6
|
||||
(pins J4-2)
|
||||
)
|
||||
(net /*5
|
||||
(pins J4-3)
|
||||
)
|
||||
(net /*4
|
||||
(pins J4-4)
|
||||
)
|
||||
(net /*3
|
||||
(pins J4-5)
|
||||
)
|
||||
(net /*2
|
||||
(pins J4-6)
|
||||
)
|
||||
(net "/TX0{slash}1"
|
||||
(pins J4-7)
|
||||
)
|
||||
(net "/RX0{slash}0"
|
||||
(pins J4-8)
|
||||
)
|
||||
(net +3V3
|
||||
(pins J1-4)
|
||||
)
|
||||
(net "/TX3{slash}14"
|
||||
(pins J6-1)
|
||||
)
|
||||
(net "/RX3{slash}15"
|
||||
(pins J6-2)
|
||||
)
|
||||
(net "/TX2{slash}16"
|
||||
(pins J6-3)
|
||||
)
|
||||
(net "/RX2{slash}17"
|
||||
(pins J6-4)
|
||||
)
|
||||
(net "/TX1{slash}18"
|
||||
(pins J6-5)
|
||||
)
|
||||
(net "/RX1{slash}19"
|
||||
(pins J6-6)
|
||||
)
|
||||
(net "/SDA{slash}20"
|
||||
(pins J2-2 J6-7)
|
||||
)
|
||||
(net "/SCL{slash}21"
|
||||
(pins J2-1 J6-8)
|
||||
)
|
||||
(net VCC
|
||||
(pins J1-8)
|
||||
)
|
||||
(net "/~{RESET}"
|
||||
(pins J1-3)
|
||||
)
|
||||
(net "unconnected-(J1-Pin_1-Pad1)"
|
||||
(pins J1-1)
|
||||
)
|
||||
(class kicad_default +3V3 +5V /*10 /*11 /*12 /*13 /*2 /*3 /*4 /*44 /*45
|
||||
/*46 /*5 /*52 /*6 /*7 /*8 /*9 /22 /23 /24 /25 /26 /27 /28 /29 /30 /31
|
||||
/32 /33 /34 /35 /36 /37 /38 /39 /40 /41 /42 /43 /47 /48 /49 /50 /51
|
||||
/53 /A0 /A1 /A10 /A11 /A12 /A13 /A14 /A15 /A2 /A3 /A4 /A5 /A6 /A7 /A8
|
||||
/A9 /AREF /IOREF "/RX0{slash}0" "/RX1{slash}19" "/RX2{slash}17" "/RX3{slash}15"
|
||||
"/SCL{slash}21" "/SDA{slash}20" "/TX0{slash}1" "/TX1{slash}18" "/TX2{slash}16"
|
||||
"/TX3{slash}14" "/~{RESET}" GND VCC "unconnected-(J1-Pin_1-Pad1)"
|
||||
(circuit
|
||||
(use_via "Via[0-1]_600:300_um")
|
||||
)
|
||||
(rule
|
||||
(width 200)
|
||||
(clearance 200)
|
||||
)
|
||||
)
|
||||
)
|
||||
(wiring
|
||||
)
|
||||
)
|
||||
160
tests/oracle.py
Normal file
160
tests/oracle.py
Normal file
@ -0,0 +1,160 @@
|
||||
"""FreeRouting JAR oracle for behaviour-level validation.
|
||||
|
||||
**Dev/test only — not part of the shipped package** (freeroute is Java-free by
|
||||
design). This runs the reference FreeRouting JAR to route a Specctra DSN so that
|
||||
freeroute's own output can be diffed against the reference implementation. There
|
||||
are no unit-test oracles for FreeRouting's geometry/router, so the oracle is how
|
||||
we validate at the behaviour level: same DSN in, compare routed connectivity.
|
||||
|
||||
Tests that use this skip automatically when a Java 21+ runtime or the JAR is not
|
||||
available (e.g. CI without a JVM), so the normal suite stays Java-free.
|
||||
|
||||
Overrides:
|
||||
- ``FREEROUTE_ORACLE_JAVA`` — explicit path to a ``java`` binary.
|
||||
- ``FREEROUTING_JAR`` — explicit path to a freerouting JAR.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import glob
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
from freeroute.dsn.sexp import parse
|
||||
|
||||
_MIN_JAVA_MAJOR = 21
|
||||
_REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
# JARs bundled in the (gitignored) reference clone, newest first.
|
||||
_JAR_CANDIDATES = (
|
||||
"reference/freerouting/scripts/benchmark/binaries/freerouting-current.jar",
|
||||
"reference/freerouting/scripts/benchmark/binaries/freerouting-2.2.4.jar",
|
||||
"reference/freerouting/integrations/KiCad/kicad-freerouting/plugins/jar/freerouting-2.2.4.jar",
|
||||
"reference/freerouting/scripts/benchmark/binaries/freerouting-1.9.0.jar",
|
||||
)
|
||||
|
||||
|
||||
def _java_major(java: str) -> int:
|
||||
"""Parse the major version from ``java -version`` (handles the old 1.8 form)."""
|
||||
try:
|
||||
out = subprocess.run(
|
||||
[java, "-version"], capture_output=True, text=True, timeout=10
|
||||
).stderr
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return 0
|
||||
m = re.search(r'version "(\d+)(?:\.(\d+))?', out)
|
||||
if not m:
|
||||
return 0
|
||||
major = int(m.group(1))
|
||||
# "1.8.0" -> 8; "26.0.1" -> 26
|
||||
return int(m.group(2)) if major == 1 and m.group(2) else major
|
||||
|
||||
|
||||
def find_java() -> str | None:
|
||||
"""Return a path to a Java >= 21 runtime, or None."""
|
||||
candidates: list[str] = []
|
||||
override = os.environ.get("FREEROUTE_ORACLE_JAVA")
|
||||
if override:
|
||||
candidates.append(override)
|
||||
java_home = os.environ.get("JAVA_HOME")
|
||||
if java_home:
|
||||
candidates.append(os.path.join(java_home, "bin", "java"))
|
||||
# Arch-style versioned JVMs, newest major first.
|
||||
candidates += sorted(
|
||||
glob.glob("/usr/lib/jvm/java-*-openjdk/bin/java"), reverse=True
|
||||
)
|
||||
on_path = shutil.which("java")
|
||||
if on_path:
|
||||
candidates.append(on_path)
|
||||
|
||||
for java in candidates:
|
||||
if java and os.path.isfile(java) and _java_major(java) >= _MIN_JAVA_MAJOR:
|
||||
return java
|
||||
return None
|
||||
|
||||
|
||||
def find_freerouting_jar() -> str | None:
|
||||
"""Return a path to a runnable freerouting JAR, or None."""
|
||||
override = os.environ.get("FREEROUTING_JAR")
|
||||
if override and os.path.isfile(override):
|
||||
return override
|
||||
for rel in _JAR_CANDIDATES:
|
||||
p = _REPO_ROOT / rel
|
||||
if p.is_file():
|
||||
return str(p)
|
||||
return None
|
||||
|
||||
|
||||
JAVA = find_java()
|
||||
JAR = find_freerouting_jar()
|
||||
HAS_ORACLE = JAVA is not None and JAR is not None
|
||||
|
||||
requires_oracle = pytest.mark.skipif(
|
||||
not HAS_ORACLE, reason="FreeRouting oracle unavailable (needs Java 21+ and a freerouting JAR)"
|
||||
)
|
||||
|
||||
|
||||
def route_dsn(dsn_path: str | Path, *, max_passes: int = 6, timeout: int = 180) -> str:
|
||||
"""Route ``dsn_path`` with the FreeRouting JAR and return the SES text.
|
||||
|
||||
Raises RuntimeError if the oracle is unavailable or produced no SES.
|
||||
"""
|
||||
if not HAS_ORACLE:
|
||||
raise RuntimeError("FreeRouting oracle unavailable")
|
||||
with tempfile.NamedTemporaryFile(suffix=".ses", delete=False) as tmp:
|
||||
out_path = tmp.name
|
||||
try:
|
||||
subprocess.run(
|
||||
[JAVA, "-jar", JAR, "-de", str(dsn_path), "-do", out_path, "-mp", str(max_passes)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
check=False,
|
||||
)
|
||||
text = Path(out_path).read_text()
|
||||
finally:
|
||||
with contextlib.suppress(OSError):
|
||||
os.unlink(out_path)
|
||||
if not text.strip():
|
||||
raise RuntimeError("FreeRouting produced an empty SES")
|
||||
return text
|
||||
|
||||
|
||||
def routed_nets(ses_text: str) -> dict[str, dict[str, int]]:
|
||||
"""Map each net in an SES ``network_out`` to its wire/via counts.
|
||||
|
||||
This is the diff primitive the router phase compares against: two SES files
|
||||
route the "same" board when they cover the same set of nets with wires/vias.
|
||||
"""
|
||||
root = parse(ses_text)
|
||||
routes = root.child("routes")
|
||||
if routes is None:
|
||||
return {}
|
||||
network_out = routes.child("network_out")
|
||||
if network_out is None:
|
||||
return {}
|
||||
result: dict[str, dict[str, int]] = {}
|
||||
for net in network_out.children("net"):
|
||||
vals = net.values()
|
||||
name = vals[0].text if vals else "?"
|
||||
result[name] = {
|
||||
"wires": len(net.children("wire")),
|
||||
"vias": len(net.children("via")),
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def routed_net_set(ses_text: str) -> set[str]:
|
||||
"""Return the set of nets that actually carry routing (>=1 wire or via)."""
|
||||
return {
|
||||
name
|
||||
for name, counts in routed_nets(ses_text).items()
|
||||
if counts["wires"] or counts["vias"]
|
||||
}
|
||||
41
tests/test_oracle.py
Normal file
41
tests/test_oracle.py
Normal file
@ -0,0 +1,41 @@
|
||||
"""Tests for the FreeRouting oracle harness (tests/oracle.py).
|
||||
|
||||
The routing test is gated on a live JVM + JAR and skips otherwise, so the suite
|
||||
stays green without Java. When the maze router lands, its output SES will be
|
||||
diffed against ``routed_net_set`` of the oracle SES here.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from oracle import HAS_ORACLE, requires_oracle, route_dsn, routed_net_set, routed_nets
|
||||
|
||||
FIXTURES = Path(__file__).parent / "dsn" / "fixtures"
|
||||
ROUTABLE = FIXTURES / "kicad_routable.dsn"
|
||||
|
||||
|
||||
def test_oracle_harness_imports():
|
||||
# The harness must import and report availability even with no Java/JAR,
|
||||
# so gated tests can skip cleanly instead of erroring at collection.
|
||||
assert isinstance(HAS_ORACLE, bool)
|
||||
|
||||
|
||||
@requires_oracle
|
||||
def test_oracle_routes_and_reports_connectivity():
|
||||
"""Reference FreeRouting routes a routable KiCad board and we can read it back.
|
||||
|
||||
Exercises the whole oracle path — JAR execution, SES production, and the
|
||||
``routed_nets`` extraction that the router phase will diff against.
|
||||
"""
|
||||
ses = route_dsn(ROUTABLE, max_passes=5)
|
||||
|
||||
nets = routed_nets(ses)
|
||||
assert nets, "SES network_out had no nets"
|
||||
for counts in nets.values():
|
||||
assert set(counts) == {"wires", "vias"}
|
||||
|
||||
routed = routed_net_set(ses)
|
||||
assert routed, "oracle produced no routed nets on a routable board"
|
||||
# Every routed net is a subset of all nets in the SES.
|
||||
assert routed <= set(nets)
|
||||
Loading…
x
Reference in New Issue
Block a user