Files
Akhil Velagapudi 4e01cb2635 Bundle kicad lib assets into lib/std (#886)
* docs: simplify README

* Add third-party notices

* Bundle stdlib KiCad assets

* Remove implicit KiCad library resolution

* Remove stale KiCad alias fixtures

* Remove layout-time model embedding

* Remove global KiCad footprint lookup

* Add KiCad stdlib path aliases

* Clean up KiCad stdlib aliases

* Derive WorkspaceConfig default

* Clean up KiCad STEP generator typing

* Use bundled KiCad footprints in tests

* Simplify interface template tests

* Ignore bundled KiCad library dependencies

* Bundle KiCad connector compatibility assets

* Resolve KiCad FPIDs to bundled stdlib footprints

* Normalize legacy KiCad library dependencies

* Bundle Governor KiCad compatibility assets

* Restore changelog entries
2026-06-17 11:56:42 -04:00

650 lines
19 KiB
Rust

#![cfg(not(target_os = "windows"))]
use pcb_test_utils::sandbox::Sandbox;
use serde_json::{Map, Value};
fn parse_netlist_json(output: &str) -> Value {
assert!(
output.contains("Exit Code: 0"),
"build failed, output was:\n{}",
output
);
let json_start = output
.find('{')
.expect("expected JSON object start in snapshot output");
let json_end = output
.rfind('}')
.expect("expected JSON object end in snapshot output");
let json_str = &output[json_start..=json_end];
serde_json::from_str::<Value>(json_str).expect("expected valid netlist JSON")
}
fn component_attrs(netlist: &Value) -> &Map<String, Value> {
netlist
.get("instances")
.and_then(Value::as_object)
.and_then(|instances| {
instances.values().find_map(|inst| {
if inst.get("kind").and_then(Value::as_str) == Some("Component") {
inst.get("attributes").and_then(Value::as_object)
} else {
None
}
})
})
.expect("expected component instance with attributes")
}
fn module_signature_params(netlist: &Value) -> &[Value] {
netlist
.get("instances")
.and_then(Value::as_object)
.and_then(|instances| {
instances.values().find_map(|inst| {
if inst.get("kind").and_then(Value::as_str) == Some("Module") {
inst.get("attributes")
.and_then(Value::as_object)
.and_then(|attrs| attrs.get("__signature"))
.and_then(|signature| signature.get("Json"))
.and_then(|json| json.get("parameters"))
.and_then(Value::as_array)
} else {
None
}
})
})
.expect("expected module instance with __signature parameters")
}
#[test]
fn netlist_includes_part_and_alternatives_json() {
let board = r#"
# ```pcb
# [workspace]
# pcb-version = "0.4"
# ```
P1 = Net()
P2 = Net()
Component(
name = "R1",
footprint = File("@kicad-footprints/Resistor_SMD.pretty/R_0603_1608Metric.kicad_mod"),
pin_defs = {"1": "1", "2": "2"},
pins = {"1": P1, "2": P2},
part = Part(
mpn = "PART-123",
manufacturer = "ACME",
qualifications = ["Q1"],
),
properties = {
"alternatives": [Part(mpn = "ALT-1", manufacturer = "ALT-MFR-1")],
},
)
"#;
let output = Sandbox::new()
.write("boards/PartBoard.zen", board)
.snapshot_run("pcbc", ["build", "boards/PartBoard.zen", "--netlist"]);
let netlist = parse_netlist_json(&output);
let attrs = component_attrs(&netlist);
assert_eq!(
attrs["mpn"]["String"].as_str(),
Some("PART-123"),
"expected scalar mpn"
);
assert_eq!(
attrs["manufacturer"]["String"].as_str(),
Some("ACME"),
"expected scalar manufacturer"
);
assert_eq!(
attrs["part"]["Json"]["mpn"].as_str(),
Some("PART-123"),
"expected part.mpn JSON payload"
);
assert_eq!(
attrs["part"]["Json"]["manufacturer"].as_str(),
Some("ACME"),
"expected part.manufacturer JSON payload"
);
assert_eq!(
attrs["part"]["Json"]["qualifications"],
serde_json::json!(["Q1"]),
"expected part.qualifications JSON payload"
);
let alternatives = attrs["alternatives"]["Array"]
.as_array()
.expect("expected alternatives array");
assert_eq!(alternatives.len(), 1);
assert_eq!(
alternatives[0]["Json"]["mpn"].as_str(),
Some("ALT-1"),
"expected alternatives[0].mpn JSON payload"
);
}
#[test]
fn netlist_reflects_modifier_mutations_for_part_and_alternatives() {
let board = r#"
# ```pcb
# [workspace]
# pcb-version = "0.4"
# ```
P1 = Net()
P2 = Net()
def mutate(component):
if component.name == "R1":
component.part = Part(
mpn = "PART-MOD",
manufacturer = "MFR-MOD",
qualifications = ["Preferred"],
)
component.alternatives = [Part(mpn = "ALT-1", manufacturer = "ALT-MFR-1")]
component.alternatives.append(
Part(mpn = "ALT-2", manufacturer = "ALT-MFR-2")
)
builtin.add_component_modifier(mutate)
Component(
name = "R1",
footprint = File("@kicad-footprints/Resistor_SMD.pretty/R_0603_1608Metric.kicad_mod"),
pin_defs = {"1": "1", "2": "2"},
pins = {"1": P1, "2": P2},
)
"#;
let output = Sandbox::new()
.write("boards/PartModifierBoard.zen", board)
.snapshot_run(
"pcbc",
["build", "boards/PartModifierBoard.zen", "--netlist"],
);
let netlist = parse_netlist_json(&output);
let attrs = component_attrs(&netlist);
assert_eq!(attrs["mpn"]["String"].as_str(), Some("PART-MOD"));
assert_eq!(attrs["manufacturer"]["String"].as_str(), Some("MFR-MOD"));
assert_eq!(attrs["part"]["Json"]["mpn"].as_str(), Some("PART-MOD"));
assert_eq!(
attrs["part"]["Json"]["qualifications"],
serde_json::json!(["Preferred"])
);
let alternatives = attrs["alternatives"]["Array"]
.as_array()
.expect("expected alternatives array");
assert_eq!(alternatives.len(), 2);
assert_eq!(alternatives[0]["Json"]["mpn"].as_str(), Some("ALT-1"));
assert_eq!(alternatives[1]["Json"]["mpn"].as_str(), Some("ALT-2"));
}
#[test]
fn netlist_signature_includes_io_direction_metadata() {
let board = r#"
# ```pcb
# [workspace]
# pcb-version = "0.4"
# ```
Child = Module("Child.zen")
Child(name = "U1", VIN = Net("VIN"), VOUT = Net("VOUT"), BIDIR = Net("IO"))
"#;
let child = r#"
VIN = io(Net, direction = "input")
VOUT = io(Net, direction = "output")
BIDIR = io(Net)
Component(
name = "U",
footprint = File("@kicad-footprints/Resistor_SMD.pretty/R_0402_1005Metric.kicad_mod"),
pin_defs = {"IN": "1", "OUT": "2", "IO": "3"},
pins = {"IN": VIN, "OUT": VOUT, "IO": BIDIR},
skip_bom = True,
)
"#;
let output = Sandbox::new()
.write("board.zen", board)
.write("Child.zen", child)
.snapshot_run("pcbc", ["build", "board.zen", "--netlist"]);
let netlist = parse_netlist_json(&output);
let params = module_signature_params(&netlist);
let vin = params
.iter()
.find(|param| param["name"].as_str() == Some("VIN"))
.expect("expected VIN parameter");
assert_eq!(vin["direction"].as_str(), Some("input"));
let vout = params
.iter()
.find(|param| param["name"].as_str() == Some("VOUT"))
.expect("expected VOUT parameter");
assert_eq!(vout["direction"].as_str(), Some("output"));
let bidir = params
.iter()
.find(|param| param["name"].as_str() == Some("BIDIR"))
.expect("expected BIDIR parameter");
assert!(
bidir.get("direction").is_none(),
"expected no direction for BIDIR, got {bidir:?}"
);
}
// --- Manifest-inherited parts (symbol_parts) tests ---
const MINIMAL_KICAD_SYM: &str = r#"(kicad_symbol_lib
(version 20241209)
(symbol "TestPart"
(property "Reference" "U" (at 0 0 0) (effects (font (size 1.27 1.27))))
(property "Value" "TestPart" (at 0 -2.54 0) (effects (font (size 1.27 1.27))))
(property "Footprint" "TestPart" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))
(symbol "TestPart_0_1"
(pin input line (at -5.08 0 0) (length 2.54) (name "P1" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27)))))
(pin input line (at 5.08 0 180) (length 2.54) (name "P2" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27)))))
)
)
)"#;
const TEST_KICAD_MOD: &str = r#"(footprint "TestPart"
(layer "F.Cu")
(pad "1" smd rect (at -1 0) (size 1 1) (layers "F.Cu"))
(pad "2" smd rect (at 1 0) (size 1 1) (layers "F.Cu"))
)"#;
const DATASHEET_TEST_COMPONENT_ZEN: &str = r#"
P1 = io(Net)
P2 = io(Net)
Component(
name = "U",
symbol = Symbol(library = "TestPart.kicad_sym"),
part = Part(mpn = "TestPart", manufacturer = "Test"),
pins = {"P1": P1, "P2": P2},
)
"#;
const DATASHEET_TEST_BOARD_ZEN: &str = r#"
TestPart = Module("components/TestPart/TestPart.zen")
TestPart(name = "U1", P1 = Net("A"), P2 = Net("B"))
"#;
fn datasheet_test_symbol(datasheet: &str) -> String {
format!(
r#"(kicad_symbol_lib
(version 20241209)
(symbol "TestPart"
(property "Reference" "U" (at 0 0 0) (effects (font (size 1.27 1.27))))
(property "Value" "TestPart" (at 0 -2.54 0) (effects (font (size 1.27 1.27))))
(property "Footprint" "TestPart" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))
(property "Datasheet" "{datasheet}" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))
(symbol "TestPart_0_1"
(pin input line (at -5.08 0 0) (length 2.54) (name "P1" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27)))))
(pin input line (at 5.08 0 180) (length 2.54) (name "P2" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27)))))
)
)
)"#
)
}
fn build_datasheet_test_output(datasheet: &str, extra_files: &[(&str, &str)]) -> String {
let mut sandbox = Sandbox::new().with_workspace();
sandbox
.write(
"components/TestPart/TestPart.kicad_sym",
datasheet_test_symbol(datasheet),
)
.write("components/TestPart/TestPart.kicad_mod", TEST_KICAD_MOD)
.write(
"components/TestPart/TestPart.zen",
DATASHEET_TEST_COMPONENT_ZEN,
)
.write("board.zen", DATASHEET_TEST_BOARD_ZEN);
for (path, contents) in extra_files {
sandbox.write(path, *contents);
}
sandbox.snapshot_run("pcbc", ["build", "board.zen", "--netlist"])
}
fn manifest_component_attrs(parts_toml: &str, component_args: &str) -> Map<String, Value> {
let mut sb = Sandbox::new();
sb.git_fixture("https://github.com/testorg/components.git")
.write("MyPart/pcb.toml", parts_toml)
.write("MyPart/TestPart.kicad_sym", MINIMAL_KICAD_SYM)
.write("MyPart/TestPart.kicad_mod", TEST_KICAD_MOD)
.write(
"MyPart/MyPart.zen",
format!(
r#"
P1 = io(Net)
P2 = io(Net)
Component(
name = "U",
symbol = Symbol(library = "TestPart.kicad_sym"),
pins = {{"P1": P1, "P2": P2}},
{component_args}
)
"#
),
)
.commit("Add test component")
.tag("MyPart/v1.0.0", false)
.push_mirror();
let output = sb
.write(
"pcb.toml",
r#"
[workspace]
pcb-version = "0.4"
[dependencies]
"github.com/testorg/components/MyPart" = "1.0.0"
"#,
)
.write(
"board.zen",
r#"
MyPart = Module("github.com/testorg/components/MyPart/MyPart.zen")
MyPart(name = "U1", P1 = Net("A"), P2 = Net("B"))
"#,
)
.sync()
.snapshot_run("pcbc", ["build", "board.zen", "--netlist"]);
let netlist = parse_netlist_json(&output);
component_attrs(&netlist).clone()
}
#[test]
fn manifest_parts_single_part_inherited() {
let attrs = manifest_component_attrs(
r#"
parts = [
{ mpn = "TEST-MPN-001", symbol = "TestPart.kicad_sym", manufacturer = "TestCorp" },
]
"#,
"",
);
assert_eq!(
attrs["mpn"]["String"].as_str(),
Some("TEST-MPN-001"),
"part.mpn should be inherited from manifest"
);
assert_eq!(
attrs["manufacturer"]["String"].as_str(),
Some("TestCorp"),
"part.manufacturer should be inherited from manifest"
);
assert_eq!(
attrs["part"]["Json"]["mpn"].as_str(),
Some("TEST-MPN-001"),
"part JSON payload should be present"
);
assert!(
attrs.get("alternatives").is_none(),
"no alternatives when manifest has a single part"
);
}
#[test]
fn manifest_parts_multiple_parts_with_alternatives() {
let attrs = manifest_component_attrs(
r#"
parts = [
{ mpn = "PRIMARY-001", symbol = "TestPart.kicad_sym", manufacturer = "PrimaryCorp", qualifications = ["Preferred"] },
{ mpn = "ALT-001", symbol = "TestPart.kicad_sym", manufacturer = "AltCorp1", qualifications = ["Approved"] },
{ mpn = "ALT-002", symbol = "TestPart.kicad_sym", manufacturer = "AltCorp2" },
]
"#,
"",
);
assert_eq!(attrs["mpn"]["String"].as_str(), Some("PRIMARY-001"));
assert_eq!(
attrs["manufacturer"]["String"].as_str(),
Some("PrimaryCorp")
);
assert_eq!(attrs["part"]["Json"]["mpn"].as_str(), Some("PRIMARY-001"));
assert_eq!(
attrs["part"]["Json"]["qualifications"],
serde_json::json!(["Preferred"])
);
let alternatives = attrs["alternatives"]["Array"]
.as_array()
.expect("expected alternatives array from manifest");
assert_eq!(alternatives.len(), 2);
assert_eq!(alternatives[0]["Json"]["mpn"].as_str(), Some("ALT-001"));
assert_eq!(
alternatives[0]["Json"]["manufacturer"].as_str(),
Some("AltCorp1")
);
assert_eq!(
alternatives[0]["Json"]["qualifications"],
serde_json::json!(["Approved"])
);
assert_eq!(alternatives[1]["Json"]["mpn"].as_str(), Some("ALT-002"));
assert_eq!(
alternatives[1]["Json"]["manufacturer"].as_str(),
Some("AltCorp2")
);
assert_eq!(
alternatives[1]["Json"]["qualifications"],
serde_json::json!([])
);
}
#[test]
fn manifest_parts_explicit_part_overrides_manifest() {
let attrs = manifest_component_attrs(
r#"
parts = [
{ mpn = "MANIFEST-001", symbol = "TestPart.kicad_sym", manufacturer = "ManifestCorp" },
]
"#,
r#" part = Part(
mpn = "EXPLICIT-999",
manufacturer = "ExplicitCorp",
),"#,
);
assert_eq!(
attrs["mpn"]["String"].as_str(),
Some("EXPLICIT-999"),
"explicit part should override manifest"
);
assert_eq!(
attrs["manufacturer"]["String"].as_str(),
Some("ExplicitCorp"),
"explicit manufacturer should override manifest"
);
}
#[test]
fn manifest_parts_append_to_existing_alternatives_when_part_is_explicit() {
let attrs = manifest_component_attrs(
r#"
parts = [
{ mpn = "MANIFEST-001", symbol = "TestPart.kicad_sym", manufacturer = "ManifestCorp", qualifications = ["Preferred"] },
{ mpn = "MANIFEST-002", symbol = "TestPart.kicad_sym", manufacturer = "AltCorp" },
]
"#,
r#" part = Part(
mpn = "EXPLICIT-999",
manufacturer = "ExplicitCorp",
),
properties = {
"alternatives": [Part(mpn = "USER-ALT-1", manufacturer = "UserCorp")],
},"#,
);
assert_eq!(attrs["mpn"]["String"].as_str(), Some("EXPLICIT-999"));
assert_eq!(
attrs["manufacturer"]["String"].as_str(),
Some("ExplicitCorp")
);
assert_eq!(attrs["part"]["Json"]["mpn"].as_str(), Some("EXPLICIT-999"));
let alternatives = attrs["alternatives"]["Array"]
.as_array()
.expect("expected alternatives array");
assert_eq!(alternatives.len(), 3);
assert_eq!(alternatives[0]["Json"]["mpn"].as_str(), Some("USER-ALT-1"));
assert_eq!(
alternatives[1]["Json"]["mpn"].as_str(),
Some("MANIFEST-001")
);
assert_eq!(
alternatives[2]["Json"]["mpn"].as_str(),
Some("MANIFEST-002")
);
assert_eq!(
alternatives[1]["Json"]["qualifications"],
serde_json::json!(["Preferred"])
);
}
#[test]
fn component_inherits_local_symbol_datasheet() {
let output = build_datasheet_test_output(
"docs/Part.pdf",
&[("components/TestPart/docs/Part.pdf", "%PDF-1.4\n%")],
);
let netlist = parse_netlist_json(&output);
let attrs = component_attrs(&netlist);
assert_eq!(
attrs["datasheet"]["String"].as_str(),
Some("package://workspace/components/TestPart/docs/Part.pdf")
);
}
#[test]
fn component_drops_invalid_inherited_symbol_datasheet() {
let output = build_datasheet_test_output("missing/Part.pdf", &[]);
let netlist = parse_netlist_json(&output);
let attrs = component_attrs(&netlist);
assert!(
!attrs.contains_key("datasheet"),
"expected invalid inherited datasheet to be dropped, attrs were: {attrs:#?}"
);
}
#[test]
fn component_inherits_skip_bom_from_symbol_in_bom() {
let sym_not_in_bom = r#"(kicad_symbol_lib
(version 20241209)
(symbol "TestPart" (in_bom no) (on_board yes)
(property "Reference" "U" (at 0 0 0) (effects (font (size 1.27 1.27))))
(property "Value" "TestPart" (at 0 -2.54 0) (effects (font (size 1.27 1.27))))
(property "Footprint" "TestPart" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))
(symbol "TestPart_0_1"
(pin input line (at -5.08 0 0) (length 2.54) (name "P1" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27)))))
(pin input line (at 5.08 0 180) (length 2.54) (name "P2" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27)))))
)
)
)"#;
// Symbol has in_bom=no → component should inherit skip_bom=true
let output = Sandbox::new()
.write("components/TestPart/TestPart.kicad_sym", sym_not_in_bom)
.write("components/TestPart/TestPart.kicad_mod", TEST_KICAD_MOD)
.write(
"components/TestPart/TestPart.zen",
r#"
P1 = io(Net)
P2 = io(Net)
Component(
name = "U",
symbol = Symbol(library = "TestPart.kicad_sym"),
part = Part(mpn = "TestPart", manufacturer = "Test"),
pins = {"P1": P1, "P2": P2},
)
"#,
)
.write(
"board.zen",
r#"
# ```pcb
# [workspace]
# pcb-version = "0.4"
# ```
TestPart = Module("components/TestPart/TestPart.zen")
TestPart(name = "U1", P1 = Net("A"), P2 = Net("B"))
"#,
)
.snapshot_run("pcbc", ["build", "board.zen", "--netlist"]);
let netlist = parse_netlist_json(&output);
let attrs = component_attrs(&netlist);
assert_eq!(
attrs["skip_bom"]["Boolean"].as_bool(),
Some(true),
"symbol with in_bom=no should set skip_bom=true"
);
// Symbol has in_bom=yes → component should have skip_bom=false (default)
let output2 = Sandbox::new()
.write("components/TestPart/TestPart.kicad_sym", MINIMAL_KICAD_SYM)
.write("components/TestPart/TestPart.kicad_mod", TEST_KICAD_MOD)
.write(
"components/TestPart/TestPart.zen",
r#"
P1 = io(Net)
P2 = io(Net)
Component(
name = "U",
symbol = Symbol(library = "TestPart.kicad_sym"),
part = Part(mpn = "TestPart", manufacturer = "Test"),
pins = {"P1": P1, "P2": P2},
)
"#,
)
.write(
"board.zen",
r#"
# ```pcb
# [workspace]
# pcb-version = "0.4"
# ```
TestPart = Module("components/TestPart/TestPart.zen")
TestPart(name = "U1", P1 = Net("A"), P2 = Net("B"))
"#,
)
.snapshot_run("pcbc", ["build", "board.zen", "--netlist"]);
let netlist2 = parse_netlist_json(&output2);
let attrs2 = component_attrs(&netlist2);
assert!(
attrs2.get("skip_bom").is_none() || attrs2["skip_bom"]["Boolean"].as_bool() == Some(false),
"symbol with in_bom=yes should not set skip_bom"
);
}