mirror of
https://github.com/diodeinc/pcb.git
synced 2026-09-14 20:17:19 +08:00
433 lines
13 KiB
Rust
433 lines
13 KiB
Rust
#![cfg(not(target_os = "windows"))]
|
|
|
|
use pcb_test_utils::assert_snapshot;
|
|
use pcb_test_utils::sandbox::Sandbox;
|
|
|
|
/// Helper to run netlist command and extract just position data for focused snapshot testing.
|
|
/// This avoids the noise of the full netlist JSON and focuses on position data verification.
|
|
fn snapshot_netlist_positions(sandbox: &mut Sandbox, program: &str, args: &[&str]) -> String {
|
|
// Run the normal netlist command
|
|
let full_output = sandbox.snapshot_run(program, args);
|
|
|
|
// Parse JSON and extract position data if the build succeeded
|
|
if full_output.contains("Exit Code: 0")
|
|
&& full_output.contains("--- STDOUT ---")
|
|
&& let Some(json_start) = full_output.find("{")
|
|
&& let Some(json_end) = full_output.rfind("}")
|
|
{
|
|
let json_str = &full_output[json_start..=json_end];
|
|
if let Ok(netlist) = serde_json::from_str::<serde_json::Value>(json_str) {
|
|
return extract_position_data(sandbox, &netlist);
|
|
}
|
|
}
|
|
|
|
// If parsing failed or build failed, return the original output
|
|
full_output
|
|
}
|
|
|
|
fn extract_position_data(sandbox: &Sandbox, netlist: &serde_json::Value) -> String {
|
|
use serde_json::json;
|
|
|
|
let mut position_data = json!({});
|
|
|
|
if let Some(instances) = netlist.get("instances").and_then(|i| i.as_object()) {
|
|
for (instance_path, instance) in instances {
|
|
if let Some(instance_obj) = instance.as_object() {
|
|
let mut instance_positions = json!({});
|
|
|
|
// Extract symbol_positions if present
|
|
if let Some(symbol_pos) = instance_obj.get("symbol_positions")
|
|
&& let Some(symbol_pos_obj) = symbol_pos.as_object()
|
|
&& !symbol_pos_obj.is_empty()
|
|
{
|
|
instance_positions["symbol_positions"] = symbol_pos.clone();
|
|
}
|
|
|
|
// Only include instances that have position data
|
|
if !instance_positions.as_object().unwrap().is_empty() {
|
|
let sanitized_path = sandbox.sanitize_output(instance_path);
|
|
position_data[sanitized_path] = instance_positions;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if position_data.as_object().unwrap().is_empty() {
|
|
"No position data found in netlist".to_string()
|
|
} else {
|
|
serde_json::to_string_pretty(&position_data)
|
|
.unwrap_or_else(|_| "Failed to serialize position data".to_string())
|
|
}
|
|
}
|
|
|
|
const SIMPLE_BOARD_WITH_POSITIONS_ZEN: &str = r#"
|
|
# ```pcb
|
|
# [workspace]
|
|
# pcb-version = "0.4"
|
|
# ```
|
|
|
|
Resistor = Module("@stdlib/generics/Resistor.zen")
|
|
Led = Module("@stdlib/generics/Led.zen")
|
|
|
|
vcc = Power("VCC_3V3")
|
|
gnd = Ground("GND")
|
|
led_anode = Net("LED_ANODE")
|
|
|
|
Resistor(name="R1", value="330Ohm", package="0603", P1=vcc, P2=led_anode)
|
|
Led(name="D1", color="red", package="0603", A=led_anode, K=gnd)
|
|
|
|
# Position comments that should be parsed and included in netlist
|
|
# pcb:sch R1 x=100.0000 y=200.0000 rot=0
|
|
# pcb:sch D1 x=150.0000 y=200.0000 rot=90
|
|
# pcb:sch VCC_3V3.1 x=80.0000 y=180.0000 rot=0
|
|
# pcb:sch VCC_3V3.2 x=120.0000 y=180.0000 rot=0
|
|
# pcb:sch GND.1 x=80.0000 y=220.0000 rot=0
|
|
# pcb:sch GND.2 x=170.0000 y=220.0000 rot=0
|
|
# pcb:sch LED_ANODE x=125.0000 y=200.0000 rot=0
|
|
"#;
|
|
|
|
const HIERARCHICAL_BOARD_WITH_POSITIONS_ZEN: &str = r#"
|
|
load("@stdlib/interfaces.zen", "Gpio")
|
|
|
|
LedModule = Module("../modules/LedModule.zen")
|
|
|
|
vcc_3v3 = Power("VCC_3V3")
|
|
gnd = Ground("GND")
|
|
|
|
LedModule(name="LED1", led_color="green", VCC=vcc_3v3, GND=gnd, CTRL=Gpio("LED_CTRL"))
|
|
LedModule(name="LED2", led_color="red", VCC=vcc_3v3, GND=gnd, CTRL=Gpio("LED_CTRL2"))
|
|
|
|
# Position comments for hierarchical design
|
|
# pcb:sch LED1.R1 x=100.0000 y=100.0000 rot=0
|
|
# pcb:sch LED1.D1 x=150.0000 y=100.0000 rot=90
|
|
# pcb:sch LED2.R1 x=100.0000 y=200.0000 rot=0
|
|
# pcb:sch LED2.D1 x=150.0000 y=200.0000 rot=90
|
|
# pcb:sch VCC_3V3.1 x=50.0000 y=150.0000 rot=0
|
|
# pcb:sch VCC_3V3.2 x=200.0000 y=150.0000 rot=0
|
|
# pcb:sch GND.1 x=50.0000 y=250.0000 rot=0
|
|
# pcb:sch GND.2 x=200.0000 y=250.0000 rot=0
|
|
# pcb:sch LED_CTRL_LED_CTRL x=80.0000 y=120.0000 rot=0
|
|
# pcb:sch LED_CTRL2_LED_CTRL2 x=80.0000 y=220.0000 rot=0
|
|
"#;
|
|
|
|
const WORKSPACE_PCB_TOML: &str = r#"
|
|
[workspace]
|
|
pcb-version = "0.4"
|
|
"#;
|
|
|
|
const SIMPLE_BOARD_WITH_MIRROR_POSITIONS_ZEN: &str = r#"
|
|
# ```pcb
|
|
# [workspace]
|
|
# pcb-version = "0.4"
|
|
# ```
|
|
|
|
Resistor = Module("@stdlib/generics/Resistor.zen")
|
|
Led = Module("@stdlib/generics/Led.zen")
|
|
|
|
vcc = Power("VCC_3V3")
|
|
gnd = Ground("GND")
|
|
led_anode = Net("LED_ANODE")
|
|
|
|
Resistor(name="R1", value="330Ohm", package="0603", P1=vcc, P2=led_anode)
|
|
Led(name="D1", color="red", package="0603", A=led_anode, K=gnd)
|
|
|
|
# Position comments with optional mirror
|
|
# pcb:sch R1 x=100.0000 y=200.0000 rot=0 mirror=x
|
|
# pcb:sch D1 x=150.0000 y=200.0000 rot=90
|
|
# pcb:sch VCC_3V3.1 x=80.0000 y=180.0000 rot=0 mirror=y
|
|
# pcb:sch VCC_3V3.2 x=120.0000 y=180.0000 rot=0
|
|
# pcb:sch GND.1 x=80.0000 y=220.0000 rot=0
|
|
# pcb:sch GND.2 x=170.0000 y=220.0000 rot=0
|
|
# pcb:sch LED_ANODE x=125.0000 y=200.0000 rot=0
|
|
"#;
|
|
|
|
const LED_MODULE_ZEN: &str = r#"
|
|
load("@stdlib/interfaces.zen", "Gpio")
|
|
|
|
Resistor = Module("@stdlib/generics/Resistor.zen")
|
|
Led = Module("@stdlib/generics/Led.zen")
|
|
|
|
led_color = config(str, default="red")
|
|
r_value = config(str, default="330Ohm")
|
|
package = config(str, default="0603")
|
|
|
|
VCC = io(Power)
|
|
GND = io(Ground)
|
|
CTRL = io(Gpio)
|
|
|
|
led_anode = Net("LED_ANODE")
|
|
|
|
Resistor(name="R1", value=r_value, package=package, P1=VCC, P2=led_anode)
|
|
Led(name="D1", color=led_color, package=package, A=led_anode, K=CTRL)
|
|
"#;
|
|
|
|
#[test]
|
|
fn test_netlist_simple_board_with_positions() {
|
|
let mut sandbox = Sandbox::new();
|
|
sandbox.write("boards/SimpleBoard.zen", SIMPLE_BOARD_WITH_POSITIONS_ZEN);
|
|
let output = snapshot_netlist_positions(
|
|
&mut sandbox,
|
|
"pcbc",
|
|
&["build", "boards/SimpleBoard.zen", "--netlist"],
|
|
);
|
|
assert_snapshot!("netlist_simple_board_with_positions", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_netlist_hierarchical_board_with_positions() {
|
|
let mut sandbox = Sandbox::new();
|
|
sandbox
|
|
.write("pcb.toml", WORKSPACE_PCB_TOML)
|
|
.write("modules/LedModule.zen", LED_MODULE_ZEN)
|
|
.write(
|
|
"boards/HierarchicalBoard.zen",
|
|
HIERARCHICAL_BOARD_WITH_POSITIONS_ZEN,
|
|
)
|
|
.sync();
|
|
let output = snapshot_netlist_positions(
|
|
&mut sandbox,
|
|
"pcbc",
|
|
&["build", "boards/HierarchicalBoard.zen", "--netlist"],
|
|
);
|
|
assert_snapshot!("netlist_hierarchical_board_with_positions", output);
|
|
}
|
|
|
|
const I2C_MODULE_ZEN: &str = r#"
|
|
load("@stdlib/interfaces.zen", "I2c")
|
|
|
|
Resistor = Module("@stdlib/generics/Resistor.zen")
|
|
|
|
VDD = io(Power)
|
|
GND = io(Ground)
|
|
I2C = io(I2c)
|
|
|
|
Resistor(name="R_SCL", value="10kohm", package="0402", P1=VDD, P2=I2C.SCL)
|
|
Resistor(name="R_SDA", value="10kohm", package="0402", P1=VDD, P2=I2C.SDA)
|
|
|
|
# Hierarchical labels on multi-field interface bus (I2c) - SCL and SDA fields.
|
|
# pcb:sch I2C_SCL.0 x=10.0000 y=20.0000 rot=0
|
|
# pcb:sch I2C_SDA.0 x=10.0000 y=30.0000 rot=0
|
|
"#;
|
|
|
|
const I2C_HIERARCHICAL_BOARD_ZEN: &str = r#"
|
|
load("@stdlib/interfaces.zen", "I2c")
|
|
|
|
I2cPullups = Module("../modules/I2cPullups.zen")
|
|
|
|
vdd = Power("VDD_3V3")
|
|
gnd = Ground("GND")
|
|
bus = I2c("I2C_BUS")
|
|
|
|
I2cPullups(name="PU1", VDD=vdd, GND=gnd, I2C=bus)
|
|
"#;
|
|
|
|
#[test]
|
|
fn test_netlist_interface_field_positions() {
|
|
let mut sandbox = Sandbox::new();
|
|
sandbox
|
|
.write("pcb.toml", WORKSPACE_PCB_TOML)
|
|
.write("modules/I2cPullups.zen", I2C_MODULE_ZEN)
|
|
.write("boards/I2cBoard.zen", I2C_HIERARCHICAL_BOARD_ZEN)
|
|
.sync();
|
|
let output = snapshot_netlist_positions(
|
|
&mut sandbox,
|
|
"pcbc",
|
|
&["build", "boards/I2cBoard.zen", "--netlist"],
|
|
);
|
|
assert_snapshot!("netlist_interface_field_positions", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_netlist_no_positions() {
|
|
let board_zen = r#"
|
|
# ```pcb
|
|
# [workspace]
|
|
# pcb-version = "0.4"
|
|
# ```
|
|
|
|
Resistor = Module("@stdlib/generics/Resistor.zen")
|
|
|
|
vcc = Power("VCC")
|
|
gnd = Ground("GND")
|
|
|
|
Resistor(name="R1", value="1kOhm", package="0603", P1=vcc, P2=gnd)
|
|
"#;
|
|
|
|
let mut sandbox = Sandbox::new();
|
|
sandbox.write("boards/NoPositions.zen", board_zen);
|
|
let output = snapshot_netlist_positions(
|
|
&mut sandbox,
|
|
"pcbc",
|
|
&["build", "boards/NoPositions.zen", "--netlist"],
|
|
);
|
|
assert_snapshot!("netlist_no_positions", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_netlist_mixed_position_formats() {
|
|
let board_zen = r#"
|
|
# ```pcb
|
|
# [workspace]
|
|
# pcb-version = "0.4"
|
|
# ```
|
|
|
|
Resistor = Module("@stdlib/generics/Resistor.zen")
|
|
Led = Module("@stdlib/generics/Led.zen")
|
|
|
|
vcc = Power("VCC")
|
|
gnd = Ground("GND")
|
|
sig = Net("SIGNAL")
|
|
|
|
Resistor(name="R1", value="1kOhm", package="0603", P1=vcc, P2=sig)
|
|
Led(name="D1", color="red", package="0603", A=sig, K=gnd)
|
|
|
|
# pcb:sch R1 x=100.0000 y=100.0000 rot=0
|
|
# pcb:sch VCC x=80.0000 y=80.0000 rot=0
|
|
# pcb:sch SIGNAL.1 x=125.0000 y=100.0000 rot=0
|
|
# pcb:sch SIGNAL.2 x=125.0000 y=150.0000 rot=0
|
|
"#;
|
|
|
|
let mut sandbox = Sandbox::new();
|
|
sandbox.write("boards/MixedPositions.zen", board_zen);
|
|
let output = snapshot_netlist_positions(
|
|
&mut sandbox,
|
|
"pcbc",
|
|
&["build", "boards/MixedPositions.zen", "--netlist"],
|
|
);
|
|
assert_snapshot!("netlist_mixed_position_formats", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_netlist_positions_with_mirror() {
|
|
let mut sandbox = Sandbox::new();
|
|
sandbox.write(
|
|
"boards/SimpleBoardWithMirror.zen",
|
|
SIMPLE_BOARD_WITH_MIRROR_POSITIONS_ZEN,
|
|
);
|
|
let output = snapshot_netlist_positions(
|
|
&mut sandbox,
|
|
"pcbc",
|
|
&["build", "boards/SimpleBoardWithMirror.zen", "--netlist"],
|
|
);
|
|
assert_snapshot!("netlist_positions_with_mirror", output);
|
|
}
|
|
|
|
/// Helper to run netlist command and extract just the nets section for focused snapshot testing.
|
|
/// This shows net names, kinds, and other metadata without the noise of instances.
|
|
fn snapshot_netlist_nets(sandbox: &mut Sandbox, program: &str, args: &[&str]) -> String {
|
|
let full_output = sandbox.snapshot_run(program, args);
|
|
|
|
if full_output.contains("Exit Code: 0")
|
|
&& full_output.contains("--- STDOUT ---")
|
|
&& let Some(json_start) = full_output.find('{')
|
|
&& let Some(json_end) = full_output.rfind('}')
|
|
{
|
|
let json_str = &full_output[json_start..=json_end];
|
|
if let Ok(netlist) = serde_json::from_str::<serde_json::Value>(json_str)
|
|
&& let Some(nets) = netlist.get("nets")
|
|
{
|
|
return serde_json::to_string_pretty(nets)
|
|
.unwrap_or_else(|_| "Failed to serialize nets".to_string());
|
|
}
|
|
}
|
|
|
|
full_output
|
|
}
|
|
|
|
const NOT_CONNECTED_MODULE_ZEN: &str = r#"
|
|
vcc = io(Power)
|
|
|
|
Component(
|
|
name = "R1",
|
|
footprint = File("@kicad-footprints/Resistor_SMD.pretty/R_0402_1005Metric.kicad_mod"),
|
|
pin_defs = {"1": "1"},
|
|
skip_bom = True,
|
|
pins = {"1": vcc},
|
|
)
|
|
"#;
|
|
|
|
const NOT_CONNECTED_BOARD_ZEN: &str = r#"
|
|
# ```pcb
|
|
# [workspace]
|
|
# pcb-version = "0.4"
|
|
# ```
|
|
|
|
PowerConsumer = Module("PowerConsumer.zen")
|
|
|
|
# NotConnected satisfies the Power IO but remains an open net
|
|
nc = NotConnected()
|
|
|
|
PowerConsumer(name = "U1", vcc = nc)
|
|
"#;
|
|
|
|
const IO_RENAMED_CHILD_ZEN: &str = r#"
|
|
IN_GD = io("IN_GD", Net)
|
|
GND = io("GND", Net)
|
|
|
|
Component(
|
|
name = "R1",
|
|
footprint = File("@kicad-footprints/Resistor_SMD.pretty/R_0402_1005Metric.kicad_mod"),
|
|
pin_defs = {"1": "1", "2": "2"},
|
|
skip_bom = True,
|
|
pins = {"1": IN_GD, "2": GND},
|
|
)
|
|
|
|
# pcb:sch IN_GD.0 x=-351.5200 y=-140.7000 rot=0
|
|
# pcb:sch GND.1 x=-100.0000 y=-50.0000 rot=0
|
|
"#;
|
|
|
|
const IO_RENAMED_PARENT_ZEN: &str = r#"
|
|
Child = Module("Child.zen")
|
|
|
|
VBUS_RAW = Net("VBUS_RAW")
|
|
GND = Net("GND")
|
|
|
|
Child(name = "U1", IN_GD = VBUS_RAW, GND = GND)
|
|
|
|
# pcb:sch U1.VBUS_RAW.0 x=-1498.6000 y=-25.4000 rot=0
|
|
# pcb:sch U1.GND.1 x=-1562.1000 y=12.7000 rot=0
|
|
"#;
|
|
|
|
/// Descendant net-symbol overrides (`<child>.<NET>.<idx>` comments in the parent)
|
|
/// must survive conversion even when the parent renames the net across the module
|
|
/// boundary (here `IN_GD=VBUS_RAW`); the override key uses the global net name.
|
|
#[test]
|
|
fn test_netlist_descendant_net_symbol_override_with_renamed_io() {
|
|
let mut sandbox = Sandbox::new().with_workspace();
|
|
sandbox
|
|
.write("boards/Child.zen", IO_RENAMED_CHILD_ZEN)
|
|
.write("boards/Parent.zen", IO_RENAMED_PARENT_ZEN);
|
|
let output = sandbox.snapshot_run("pcbc", &["build", "boards/Parent.zen", "--netlist"]);
|
|
|
|
let json_start = output.find('{').expect("netlist JSON in output");
|
|
let json_end = output.rfind('}').expect("netlist JSON in output");
|
|
let netlist: serde_json::Value =
|
|
serde_json::from_str(&output[json_start..=json_end]).expect("netlist parses as JSON");
|
|
|
|
let root_positions = netlist["instances"]
|
|
.as_object()
|
|
.unwrap()
|
|
.iter()
|
|
.find_map(|(path, inst)| path.ends_with(":<root>").then(|| &inst["symbol_positions"]))
|
|
.expect("root instance present");
|
|
|
|
// Renamed io: parent override uses the global net name, not the child's local name.
|
|
assert_eq!(root_positions["sym:U1.VBUS_RAW#0"]["x"], -1498.6);
|
|
// Same-named net keeps working.
|
|
assert_eq!(root_positions["sym:U1.GND#1"]["x"], -1562.1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_netlist_not_connected_open_intent() {
|
|
let mut sandbox = Sandbox::new();
|
|
sandbox
|
|
.write("boards/PowerConsumer.zen", NOT_CONNECTED_MODULE_ZEN)
|
|
.write("boards/NCBoard.zen", NOT_CONNECTED_BOARD_ZEN);
|
|
let output = snapshot_netlist_nets(
|
|
&mut sandbox,
|
|
"pcbc",
|
|
&["build", "boards/NCBoard.zen", "--netlist"],
|
|
);
|
|
assert_snapshot!("netlist_not_connected_open_intent", output);
|
|
}
|