mirror of
https://github.com/diodeinc/pcb.git
synced 2026-09-14 20:17:19 +08:00
1944 lines
55 KiB
Rust
1944 lines
55 KiB
Rust
use crate::common;
|
|
|
|
use pcb_zen_core::DiagnosticsPass;
|
|
use pcb_zen_core::SortPass;
|
|
use pcb_zen_core::lang::component::FrozenComponentValue;
|
|
use pcb_zen_core::lang::error::CategorizedDiagnostic;
|
|
use pcb_zen_core::lang::net::FrozenNetValue;
|
|
use starlark::errors::EvalSeverity;
|
|
use starlark::values::{FrozenValue, ValueLike};
|
|
|
|
fn eval_single_root_component(source: &str) -> FrozenComponentValue {
|
|
eval_single_root_component_with_files(vec![("test.zen", source)])
|
|
}
|
|
|
|
fn eval_single_root_component_with_files(files: Vec<(&str, &str)>) -> FrozenComponentValue {
|
|
let result = common::eval_zen(
|
|
files
|
|
.into_iter()
|
|
.map(|(path, content)| (path.to_string(), content.to_string()))
|
|
.collect(),
|
|
);
|
|
assert!(result.is_success(), "eval failed: {:?}", result.diagnostics);
|
|
|
|
let output = result.output.expect("expected eval output");
|
|
let module_tree = output.module_tree();
|
|
let root_module = module_tree
|
|
.values()
|
|
.find(|module| module.path().is_root())
|
|
.expect("expected root module");
|
|
|
|
let components: Vec<_> = root_module.components().cloned().collect();
|
|
assert_eq!(
|
|
components.len(),
|
|
1,
|
|
"expected exactly one root component, got {}",
|
|
components.len()
|
|
);
|
|
|
|
components
|
|
.into_iter()
|
|
.next()
|
|
.expect("expected one component")
|
|
}
|
|
|
|
fn frozen_net_id(value: FrozenValue) -> u64 {
|
|
value
|
|
.downcast_ref::<FrozenNetValue>()
|
|
.expect("expected frozen net")
|
|
.net_id()
|
|
}
|
|
|
|
const DESCRIBED_SYMBOL: &str = r#"(kicad_symbol_lib
|
|
(version 20231120)
|
|
(generator "test")
|
|
(symbol "Described"
|
|
(property "Reference" "U")
|
|
(property "Value" "Described")
|
|
(property "Description" "Symbol description")
|
|
(property "Manufacturer_Name" "Acme")
|
|
(property "Manufacturer_Part_Number" "ACME-1")
|
|
(symbol "Described_1_1"
|
|
(pin passive line (at 0 0 0) (length 2.54) (name "P") (number "1"))
|
|
)
|
|
)
|
|
)"#;
|
|
|
|
#[test]
|
|
fn untyped_component_prefers_symbol_description_over_value() {
|
|
let component = eval_single_root_component_with_files(vec![
|
|
("described.kicad_sym", DESCRIBED_SYMBOL),
|
|
(
|
|
"test.zen",
|
|
r#"
|
|
Component(
|
|
name = "U1",
|
|
footprint = File("@kicad-footprints/Resistor_SMD.pretty/R_0603_1608Metric.kicad_mod"),
|
|
symbol = Symbol(library = "described.kicad_sym"),
|
|
pins = {"P": Net("P")},
|
|
properties = {"value": "32MHz"},
|
|
)
|
|
"#,
|
|
),
|
|
]);
|
|
|
|
assert_eq!(component.description(), Some("Symbol description"));
|
|
}
|
|
|
|
#[test]
|
|
fn typed_component_prefers_value_over_symbol_description() {
|
|
let component = eval_single_root_component_with_files(vec![
|
|
("described.kicad_sym", DESCRIBED_SYMBOL),
|
|
(
|
|
"test.zen",
|
|
r#"
|
|
Component(
|
|
name = "R1",
|
|
type = "resistor",
|
|
footprint = File("@kicad-footprints/Resistor_SMD.pretty/R_0603_1608Metric.kicad_mod"),
|
|
symbol = Symbol(library = "described.kicad_sym"),
|
|
pins = {"P": Net("P")},
|
|
properties = {"value": "1k"},
|
|
)
|
|
"#,
|
|
),
|
|
]);
|
|
|
|
assert_eq!(component.description(), Some("1k"));
|
|
}
|
|
|
|
#[test]
|
|
fn explicit_description_overrides_typed_component_value() {
|
|
let component = eval_single_root_component_with_files(vec![
|
|
("described.kicad_sym", DESCRIBED_SYMBOL),
|
|
(
|
|
"test.zen",
|
|
r#"
|
|
Component(
|
|
name = "R1",
|
|
type = "resistor",
|
|
description = "Explicit description",
|
|
footprint = File("@kicad-footprints/Resistor_SMD.pretty/R_0603_1608Metric.kicad_mod"),
|
|
symbol = Symbol(library = "described.kicad_sym"),
|
|
pins = {"P": Net("P")},
|
|
properties = {"value": "1k"},
|
|
)
|
|
"#,
|
|
),
|
|
]);
|
|
|
|
assert_eq!(component.description(), Some("Explicit description"));
|
|
}
|
|
|
|
#[test]
|
|
fn typed_component_uses_symbol_description_without_value() {
|
|
let component = eval_single_root_component_with_files(vec![
|
|
("described.kicad_sym", DESCRIBED_SYMBOL),
|
|
(
|
|
"test.zen",
|
|
r#"
|
|
Component(
|
|
name = "R1",
|
|
type = "resistor",
|
|
footprint = File("@kicad-footprints/Resistor_SMD.pretty/R_0603_1608Metric.kicad_mod"),
|
|
symbol = Symbol(library = "described.kicad_sym"),
|
|
pins = {"P": Net("P")},
|
|
)
|
|
"#,
|
|
),
|
|
]);
|
|
|
|
assert_eq!(component.description(), Some("Symbol description"));
|
|
}
|
|
|
|
const EXPLICIT_JUMPER_SYMBOL: &str = r#"(kicad_symbol_lib
|
|
(version 20251024)
|
|
(generator "test")
|
|
(symbol "ExplicitJumper"
|
|
(property "Reference" "JP")
|
|
(property "Footprint" File("@kicad-footprints/Jumper.pretty/SolderJumper-2_P1.3mm_Open_Pad1.0x1.5mm.kicad_mod"))
|
|
(jumper_pin_groups ("1" "3"))
|
|
(symbol "ExplicitJumper_1_1"
|
|
(pin passive line (at 0 0 0) (length 2.54) (name "A") (number "1"))
|
|
(pin passive line (at 0 0 0) (length 2.54) (name "B") (number "3"))
|
|
)
|
|
)
|
|
)"#;
|
|
|
|
#[test]
|
|
fn component_prefers_part_datasheet_over_component_datasheet() {
|
|
let component = eval_single_root_component(
|
|
r#"
|
|
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},
|
|
datasheet = "component.pdf",
|
|
part = builtin.Part(
|
|
mpn = "PART-1",
|
|
manufacturer = "MFR",
|
|
datasheet = "https://example.com/part-1.pdf",
|
|
),
|
|
)
|
|
"#,
|
|
);
|
|
|
|
assert_eq!(
|
|
component.datasheet(),
|
|
Some("https://example.com/part-1.pdf")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn component_modifier_can_set_part_datasheet() {
|
|
let component = eval_single_root_component(
|
|
r#"
|
|
P1 = Net()
|
|
P2 = Net()
|
|
|
|
def match_part(component):
|
|
component.part = builtin.Part(
|
|
mpn = "HOUSE-1",
|
|
manufacturer = "MFR",
|
|
datasheet = "https://example.com/house-1.pdf",
|
|
)
|
|
|
|
builtin.add_component_modifier(match_part)
|
|
|
|
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},
|
|
)
|
|
"#,
|
|
);
|
|
|
|
assert_eq!(
|
|
component.datasheet(),
|
|
Some("https://example.com/house-1.pdf")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn file_backed_footprint_validation_reports_embedded_file_errors() {
|
|
let result = common::eval_zen(vec![
|
|
(
|
|
"bad.kicad_mod".to_string(),
|
|
r#"
|
|
(footprint "Bad"
|
|
(embedded_files
|
|
(file
|
|
(name model.step)
|
|
(type model)
|
|
(data |not base64|)
|
|
(checksum "00")
|
|
)
|
|
)
|
|
)
|
|
"#
|
|
.to_string(),
|
|
),
|
|
(
|
|
"test.zen".to_string(),
|
|
r#"
|
|
P1 = Net()
|
|
P2 = Net()
|
|
P3 = Net()
|
|
P4 = Net()
|
|
|
|
Component(
|
|
name = "R1",
|
|
footprint = "bad.kicad_mod",
|
|
pin_defs = {"1": "1", "2": "2"},
|
|
pins = {"1": P1, "2": P2},
|
|
)
|
|
|
|
Component(
|
|
name = "R2",
|
|
footprint = "bad.kicad_mod",
|
|
pin_defs = {"1": "1", "2": "2"},
|
|
pins = {"1": P3, "2": P4},
|
|
)
|
|
"#
|
|
.to_string(),
|
|
),
|
|
]);
|
|
assert!(result.is_success(), "eval failed: {:?}", result.diagnostics);
|
|
|
|
let diagnostics = result
|
|
.output
|
|
.expect("expected eval output")
|
|
.validate_footprints();
|
|
|
|
assert!(diagnostics.iter().any(|d| d.is_error()));
|
|
let footprint_diags = diagnostics
|
|
.iter()
|
|
.filter(|diagnostic| diagnostic.body.contains("uses invalid footprint"))
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(footprint_diags.len(), 2);
|
|
let footprint_diag = footprint_diags[0];
|
|
assert!(footprint_diag.span.is_some());
|
|
let child = footprint_diag
|
|
.child
|
|
.as_ref()
|
|
.expect("expected inner footprint-file diagnostic");
|
|
assert!(child.body.contains("Invalid KiCad footprint"), "{child:?}");
|
|
assert!(child.body.contains("invalid base64 data"), "{child:?}");
|
|
assert!(child.path.ends_with("bad.kicad_mod"), "{child:?}");
|
|
assert!(child.span.is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn component_modifier_part_without_datasheet_clears_stale_part_datasheet() {
|
|
let component = eval_single_root_component(
|
|
r#"
|
|
P1 = Net()
|
|
P2 = Net()
|
|
|
|
def match_part(component):
|
|
component.part = builtin.Part(
|
|
mpn = "HOUSE-1",
|
|
manufacturer = "MFR",
|
|
)
|
|
|
|
builtin.add_component_modifier(match_part)
|
|
|
|
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 = builtin.Part(
|
|
mpn = "PART-1",
|
|
manufacturer = "MFR",
|
|
datasheet = "https://example.com/part-1.pdf",
|
|
),
|
|
)
|
|
"#,
|
|
);
|
|
|
|
assert_eq!(component.datasheet(), None);
|
|
}
|
|
|
|
snapshot_eval!(component_properties, {
|
|
"C146731.kicad_sym" => include_str!("resources/C146731.kicad_sym"),
|
|
"test_props.zen" => r#"
|
|
Component(
|
|
name = "U1",
|
|
pins = {
|
|
"ICLK": Net("ICLK"),
|
|
"Q1": Net("Q1"),
|
|
"Q2": Net("Q2"),
|
|
"Q3": Net("Q3"),
|
|
"Q4": Net("Q4"),
|
|
"GND": Net("GND"),
|
|
"VDD": Net("VDD"),
|
|
"OE": Net("OE"),
|
|
},
|
|
symbol = Symbol(library = "C146731.kicad_sym", name = "NB3N551DG"),
|
|
footprint = File("@kicad-footprints/Capacitor_SMD.pretty/C_0805_2012Metric.kicad_mod"),
|
|
properties = {"CustomProp": "Value123"},
|
|
)
|
|
"#
|
|
});
|
|
|
|
snapshot_eval!(interface_net_incompatible, {
|
|
"Module.zen" => r#"
|
|
SingleNet = interface(signal = Net)
|
|
signal_if = SingleNet(name="sig")
|
|
|
|
Component(
|
|
name = "test_comp",
|
|
footprint = "test_footprint",
|
|
pin_defs = {"in": "1", "out": "2"},
|
|
pins = {
|
|
"in": signal_if, # This should fail - interfaces not accepted for pins
|
|
"out": Net()
|
|
}
|
|
)
|
|
"#
|
|
});
|
|
|
|
snapshot_eval!(component_with_symbol, {
|
|
"test.zen" => r#"
|
|
# Create a symbol
|
|
i2c_symbol = Symbol(
|
|
name="I2C",
|
|
definition=[
|
|
("SCL", ["1"]),
|
|
("SDA", ["2"]),
|
|
("VDD", ["3"]),
|
|
("GND", ["4"])
|
|
]
|
|
)
|
|
|
|
# Create a component using the symbol
|
|
Component(
|
|
name = "I2C_Device",
|
|
footprint = "SOIC-8",
|
|
symbol = i2c_symbol, # Use Symbol instead of pin_defs
|
|
pins = {
|
|
"SCL": Net("SCL"),
|
|
"SDA": Net("SDA"),
|
|
"VDD": Net("VDD"),
|
|
"GND": Net("GND"),
|
|
}
|
|
)
|
|
"#
|
|
});
|
|
|
|
#[test]
|
|
fn component_explicit_jumper_group_auto_fills_missing_peer() {
|
|
let component = eval_single_root_component_with_files(vec![
|
|
("explicit_jumper.kicad_sym", EXPLICIT_JUMPER_SYMBOL),
|
|
(
|
|
"test.zen",
|
|
r#"
|
|
shared = Net("SHARED")
|
|
|
|
Component(
|
|
name = "JP1",
|
|
footprint = File("@kicad-footprints/Jumper.pretty/SolderJumper-2_P1.3mm_Open_Pad1.0x1.5mm.kicad_mod"),
|
|
symbol = Symbol(library = "explicit_jumper.kicad_sym", name = "ExplicitJumper"),
|
|
pins = {"A": shared},
|
|
)
|
|
"#,
|
|
),
|
|
]);
|
|
|
|
assert!(component.connections().contains_key("A"));
|
|
assert!(component.connections().contains_key("B"));
|
|
assert_eq!(
|
|
frozen_net_id(*component.connections().get("A").unwrap()),
|
|
frozen_net_id(*component.connections().get("B").unwrap())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn component_explicit_jumper_group_conflicting_nets_error() {
|
|
let result = common::eval_zen(vec![
|
|
(
|
|
"explicit_jumper.kicad_sym".to_string(),
|
|
EXPLICIT_JUMPER_SYMBOL.to_string(),
|
|
),
|
|
(
|
|
"test.zen".to_string(),
|
|
r#"
|
|
Component(
|
|
name = "JP1",
|
|
footprint = File("@kicad-footprints/Jumper.pretty/SolderJumper-2_P1.3mm_Open_Pad1.0x1.5mm.kicad_mod"),
|
|
symbol = Symbol(library = "explicit_jumper.kicad_sym", name = "ExplicitJumper"),
|
|
pins = {"A": Net("LEFT"), "B": Net("RIGHT")},
|
|
)
|
|
"#
|
|
.to_string(),
|
|
),
|
|
]);
|
|
|
|
assert!(!result.is_success(), "expected eval failure");
|
|
assert!(
|
|
result
|
|
.diagnostics
|
|
.iter()
|
|
.any(|diagnostic| diagnostic.to_string().contains("Jumpered pins A, B")),
|
|
"expected jumper conflict diagnostic, got {:?}",
|
|
result.diagnostics
|
|
);
|
|
}
|
|
|
|
snapshot_eval!(component_infers_spice_model_from_symbol, {
|
|
"my_model.lib" => r#"
|
|
.SUBCKT my_resistor p n PARAMS: RVAL=1k
|
|
R1 p n {RVAL}
|
|
.ENDS my_resistor
|
|
"#,
|
|
"test_sim_symbol.kicad_sym" => r#"(kicad_symbol_lib (version 20211014) (generator kicad_symbol_editor)
|
|
(symbol "TestSim" (pin_names (offset 1.016)) (in_bom yes) (on_board yes)
|
|
(property "Reference" "R" (id 0) (at 0 0 0))
|
|
(property "Sim.Library" "my_model.lib" (id 1) (at 0 0 0))
|
|
(property "Sim.Name" "my_resistor" (id 2) (at 0 0 0))
|
|
(property "Sim.Device" "SUBCKT" (id 3) (at 0 0 0))
|
|
(property "Sim.Pins" "2=p 1=n" (id 4) (at 0 0 0))
|
|
(property "Sim.Params" "RVAL=2200" (id 5) (at 0 0 0))
|
|
(symbol "TestSim_0_1"
|
|
(rectangle (start -10.16 10.16) (end 10.16 -10.16))
|
|
)
|
|
(symbol "TestSim_1_1"
|
|
(pin passive line (at -12.7 2.54 0) (length 2.54)
|
|
(name "P1" (effects (font (size 1.27 1.27))))
|
|
(number "1" (effects (font (size 1.27 1.27))))
|
|
)
|
|
(pin passive line (at -12.7 -2.54 0) (length 2.54)
|
|
(name "P2" (effects (font (size 1.27 1.27))))
|
|
(number "2" (effects (font (size 1.27 1.27))))
|
|
)
|
|
)
|
|
)
|
|
)"#,
|
|
"test.zen" => r#"
|
|
Component(
|
|
name = "R1",
|
|
footprint = "0603",
|
|
symbol = Symbol(library = "./test_sim_symbol.kicad_sym"),
|
|
pins = {
|
|
"P1": Net("A"),
|
|
"P2": Net("B"),
|
|
},
|
|
)
|
|
"#
|
|
});
|
|
|
|
snapshot_eval!(component_ignores_non_subckt_symbol_spice_model, {
|
|
"my_model.lib" => r#"
|
|
.SUBCKT my_resistor p n
|
|
R1 p n 1k
|
|
.ENDS my_resistor
|
|
"#,
|
|
"test_invalid_sim_symbol.kicad_sym" => r#"(kicad_symbol_lib (version 20211014) (generator kicad_symbol_editor)
|
|
(symbol "TestSim" (pin_names (offset 1.016)) (in_bom yes) (on_board yes)
|
|
(property "Reference" "R" (id 0) (at 0 0 0))
|
|
(property "Sim.Library" "my_model.lib" (id 1) (at 0 0 0))
|
|
(property "Sim.Name" "my_resistor" (id 2) (at 0 0 0))
|
|
(property "Sim.Device" "R" (id 3) (at 0 0 0))
|
|
(property "Sim.Pins" "1=p 2=n" (id 4) (at 0 0 0))
|
|
(symbol "TestSim_0_1"
|
|
(rectangle (start -10.16 10.16) (end 10.16 -10.16))
|
|
)
|
|
(symbol "TestSim_1_1"
|
|
(pin passive line (at -12.7 2.54 0) (length 2.54)
|
|
(name "P1" (effects (font (size 1.27 1.27))))
|
|
(number "1" (effects (font (size 1.27 1.27))))
|
|
)
|
|
(pin passive line (at -12.7 -2.54 0) (length 2.54)
|
|
(name "P2" (effects (font (size 1.27 1.27))))
|
|
(number "2" (effects (font (size 1.27 1.27))))
|
|
)
|
|
)
|
|
)
|
|
)"#,
|
|
"test.zen" => r#"
|
|
Component(
|
|
name = "R1",
|
|
footprint = "0603",
|
|
symbol = Symbol(library = "./test_invalid_sim_symbol.kicad_sym"),
|
|
pins = {
|
|
"P1": Net("A"),
|
|
"P2": Net("B"),
|
|
},
|
|
)
|
|
"#
|
|
});
|
|
|
|
snapshot_eval!(component_ignores_incomplete_subckt_symbol_spice_model, {
|
|
"test_incomplete_sim_symbol.kicad_sym" => r#"(kicad_symbol_lib (version 20211014) (generator kicad_symbol_editor)
|
|
(symbol "TestSim" (pin_names (offset 1.016)) (in_bom yes) (on_board yes)
|
|
(property "Reference" "R" (id 0) (at 0 0 0))
|
|
(property "Sim.Name" "my_resistor" (id 1) (at 0 0 0))
|
|
(property "Sim.Device" "SUBCKT" (id 2) (at 0 0 0))
|
|
(property "Sim.Pins" "1=p 2=n" (id 3) (at 0 0 0))
|
|
(symbol "TestSim_0_1"
|
|
(rectangle (start -10.16 10.16) (end 10.16 -10.16))
|
|
)
|
|
(symbol "TestSim_1_1"
|
|
(pin passive line (at -12.7 2.54 0) (length 2.54)
|
|
(name "P1" (effects (font (size 1.27 1.27))))
|
|
(number "1" (effects (font (size 1.27 1.27))))
|
|
)
|
|
(pin passive line (at -12.7 -2.54 0) (length 2.54)
|
|
(name "P2" (effects (font (size 1.27 1.27))))
|
|
(number "2" (effects (font (size 1.27 1.27))))
|
|
)
|
|
)
|
|
)
|
|
)"#,
|
|
"test.zen" => r#"
|
|
Component(
|
|
name = "R1",
|
|
footprint = "0603",
|
|
symbol = Symbol(library = "./test_incomplete_sim_symbol.kicad_sym"),
|
|
pins = {
|
|
"P1": Net("A"),
|
|
"P2": Net("B"),
|
|
},
|
|
)
|
|
"#
|
|
});
|
|
|
|
snapshot_eval!(component_duplicate_pin_names, {
|
|
"duplicate_pins_symbol.kicad_sym" => r#"(kicad_symbol_lib (version 20211014) (generator kicad_symbol_editor)
|
|
(symbol "TestSymbol" (pin_names (offset 1.016)) (in_bom yes) (on_board yes)
|
|
(property "Reference" "U" (id 0) (at 0 0 0))
|
|
(symbol "TestSymbol_0_1"
|
|
(rectangle (start -10.16 10.16) (end 10.16 -10.16))
|
|
)
|
|
(symbol "TestSymbol_1_1"
|
|
(pin input line (at -12.7 2.54 0) (length 2.54)
|
|
(name "in" (effects (font (size 1.27 1.27))))
|
|
(number "1" (effects (font (size 1.27 1.27))))
|
|
)
|
|
(pin output line (at 12.7 0 180) (length 2.54)
|
|
(name "out" (effects (font (size 1.27 1.27))))
|
|
(number "2" (effects (font (size 1.27 1.27))))
|
|
)
|
|
(pin input line (at -12.7 -2.54 0) (length 2.54)
|
|
(name "in" (effects (font (size 1.27 1.27))))
|
|
(number "3" (effects (font (size 1.27 1.27))))
|
|
)
|
|
)
|
|
)
|
|
)"#,
|
|
"test.zen" => r#"
|
|
Component(
|
|
name = "test_comp",
|
|
footprint = "test_footprint",
|
|
symbol = Symbol(library = "./duplicate_pins_symbol.kicad_sym"),
|
|
pins = {
|
|
"in": Net("in"),
|
|
"out": Net("out"),
|
|
}
|
|
)
|
|
"#
|
|
});
|
|
|
|
snapshot_eval!(component_manufacturer_from_symbol, {
|
|
"test_symbol.kicad_sym" => r#"(kicad_symbol_lib (version 20211014) (generator kicad_symbol_editor)
|
|
(symbol "TestSymbol" (pin_names (offset 1.016)) (in_bom yes) (on_board yes)
|
|
(property "Reference" "U" (id 0) (at 0 0 0))
|
|
(property "Manufacturer_Name" "ACME Corp" (id 1) (at 0 0 0))
|
|
(symbol "TestSymbol_0_1"
|
|
(rectangle (start -10.16 10.16) (end 10.16 -10.16))
|
|
)
|
|
(symbol "TestSymbol_1_1"
|
|
(pin input line (at -12.7 2.54 0) (length 2.54)
|
|
(name "VCC" (effects (font (size 1.27 1.27))))
|
|
(number "1" (effects (font (size 1.27 1.27))))
|
|
)
|
|
(pin output line (at 12.7 0 180) (length 2.54)
|
|
(name "GND" (effects (font (size 1.27 1.27))))
|
|
(number "2" (effects (font (size 1.27 1.27))))
|
|
)
|
|
)
|
|
)
|
|
)"#,
|
|
"test.zen" => r#"
|
|
Component(
|
|
name = "test_comp",
|
|
footprint = "test_footprint",
|
|
symbol = Symbol(library = "./test_symbol.kicad_sym"),
|
|
pins = {
|
|
"VCC": Net("VCC"),
|
|
"GND": Net("GND"),
|
|
}
|
|
)
|
|
"#
|
|
});
|
|
|
|
snapshot_eval!(component_with_dnp_kwarg, {
|
|
"test.zen" => r#"
|
|
Component(
|
|
name = "test_comp_dnp",
|
|
footprint = "test_footprint",
|
|
dnp = True,
|
|
pin_defs = {
|
|
"in": "1",
|
|
"out": "2",
|
|
},
|
|
pins = {
|
|
"in": Net("in"),
|
|
"out": Net("out"),
|
|
},
|
|
)
|
|
"#
|
|
});
|
|
|
|
snapshot_eval!(module_dnp_propagates_to_children, {
|
|
"SubModule.zen" => r#"
|
|
# Child module with multiple components
|
|
vcc = io(Net)
|
|
gnd = io(Net)
|
|
|
|
Component(
|
|
name = "R1",
|
|
footprint = "0402",
|
|
pin_defs = {"1": "1", "2": "2"},
|
|
pins = {"1": vcc, "2": gnd},
|
|
properties = {"resistance": "10k"}
|
|
)
|
|
|
|
Component(
|
|
name = "C1",
|
|
footprint = "0402",
|
|
pin_defs = {"1": "1", "2": "2"},
|
|
pins = {"1": vcc, "2": gnd},
|
|
properties = {"capacitance": "100nF"}
|
|
)
|
|
"#,
|
|
"test.zen" => r#"
|
|
# Load and instantiate module with dnp=True
|
|
SubMod = Module("SubModule.zen")
|
|
|
|
vcc = Net("VCC")
|
|
gnd = Net("GND")
|
|
|
|
# This module and all its child components should be DNP
|
|
SubMod(
|
|
name = "sub_dnp",
|
|
vcc = vcc,
|
|
gnd = gnd,
|
|
dnp = True
|
|
)
|
|
|
|
# This module should NOT be DNP
|
|
SubMod(
|
|
name = "sub_normal",
|
|
vcc = vcc,
|
|
gnd = gnd
|
|
)
|
|
"#
|
|
});
|
|
|
|
snapshot_eval!(component_inherits_reference_prefix, {
|
|
"ic_symbol.kicad_sym" => r#"(kicad_symbol_lib
|
|
(symbol "MyIC"
|
|
(property "Reference" "IC" (at 0 0 0))
|
|
(symbol "MyIC_0_1"
|
|
(pin input line (at 0 0 0) (length 2.54)
|
|
(name "IN" (effects (font (size 1.27 1.27))))
|
|
(number "1" (effects (font (size 1.27 1.27))))
|
|
)
|
|
(pin output line (at 0 0 0) (length 2.54)
|
|
(name "OUT" (effects (font (size 1.27 1.27))))
|
|
(number "2" (effects (font (size 1.27 1.27))))
|
|
)
|
|
)
|
|
)
|
|
)"#,
|
|
"test.zen" => r#"
|
|
# Test that component inherits reference prefix "IC" from symbol
|
|
# when no explicit prefix is provided
|
|
Component(
|
|
name = "MyComponent1",
|
|
footprint = "SOIC-8",
|
|
symbol = Symbol(library = "ic_symbol.kicad_sym"),
|
|
pins = {
|
|
"IN": Net("in_signal"),
|
|
"OUT": Net("out_signal"),
|
|
}
|
|
)
|
|
|
|
# Test that explicit prefix still overrides symbol reference
|
|
Component(
|
|
name = "MyComponent2",
|
|
footprint = "SOIC-8",
|
|
symbol = Symbol(library = "ic_symbol.kicad_sym"),
|
|
prefix = "U", # Explicit prefix should override
|
|
pins = {
|
|
"IN": Net("in2"),
|
|
"OUT": Net("out2"),
|
|
}
|
|
)
|
|
|
|
# The component prefix will be visible in the module snapshot
|
|
"#
|
|
});
|
|
|
|
// ============================================================================
|
|
// Component Mutation Tests
|
|
// ============================================================================
|
|
// Note: Component() now returns None (like Module()), so direct mutation
|
|
// outside of modifiers is not allowed. Mutation must happen in modifier functions.
|
|
|
|
#[test]
|
|
fn simulation_leaves_generic_part_unassigned() {
|
|
let component = eval_single_root_component(
|
|
r#"
|
|
load("@stdlib/properties.zen", "Simulation")
|
|
|
|
Component(
|
|
name = "R1",
|
|
prefix = "R",
|
|
footprint = "0603",
|
|
pin_defs = {"1": "1", "2": "2"},
|
|
pins = {"1": Net("A"), "2": Net("B")},
|
|
properties = {"package": "0603", "resistance": "10k"},
|
|
type = "resistor",
|
|
)
|
|
|
|
Simulation(
|
|
name = "sim",
|
|
setup = "* noop",
|
|
)
|
|
"#,
|
|
);
|
|
|
|
assert_eq!(component.mpn(), None);
|
|
assert_eq!(component.manufacturer(), None);
|
|
}
|
|
|
|
#[test]
|
|
fn simulation_applies_component_modifiers() {
|
|
let component = eval_single_root_component(
|
|
r#"
|
|
load("@stdlib/properties.zen", "Simulation")
|
|
|
|
def assign_custom_part(component):
|
|
if hasattr(component, "resistance"):
|
|
component.part = builtin.Part(mpn = "CUSTOM_MPN", manufacturer = "ACME")
|
|
|
|
Component(
|
|
name = "R1",
|
|
prefix = "R",
|
|
footprint = "0603",
|
|
pin_defs = {"1": "1", "2": "2"},
|
|
pins = {"1": Net("A"), "2": Net("B")},
|
|
properties = {"package": "0603", "resistance": "10k"},
|
|
type = "resistor",
|
|
)
|
|
|
|
Simulation(
|
|
name = "sim",
|
|
setup = "* noop",
|
|
modifiers = [assign_custom_part],
|
|
)
|
|
"#,
|
|
);
|
|
|
|
assert_eq!(component.mpn(), Some("CUSTOM_MPN"));
|
|
assert_eq!(component.manufacturer(), Some("ACME"));
|
|
}
|
|
|
|
snapshot_eval!(component_modifier_basic, {
|
|
"test.zen" => r#"
|
|
# Test component modifier
|
|
def assign_part(component):
|
|
if hasattr(component, "resistance"):
|
|
component.part = builtin.Part(mpn="ASSIGNED_MPN", manufacturer="ACME")
|
|
|
|
builtin.add_component_modifier(assign_part)
|
|
|
|
Component(
|
|
name = "R1",
|
|
footprint = "0603",
|
|
pin_defs = {"1": "1", "2": "2"},
|
|
pins = {"1": Net("A"), "2": Net("B")},
|
|
properties = {"resistance": "10k"},
|
|
)
|
|
|
|
# Component will have mpn and manufacturer set by modifier
|
|
# This is verified by the module snapshot
|
|
"#
|
|
});
|
|
|
|
snapshot_eval!(component_modifier_conditional, {
|
|
"test.zen" => r#"
|
|
# Test conditional component modifier
|
|
def assign_preferred_resistor(component):
|
|
if hasattr(component, "resistance"):
|
|
resistance = str(component.resistance)
|
|
if resistance == "10k":
|
|
component.part = builtin.Part(mpn="10K_PART", manufacturer="Vendor_10K")
|
|
elif resistance == "100k":
|
|
component.part = builtin.Part(mpn="100K_PART", manufacturer="Vendor_100K")
|
|
|
|
builtin.add_component_modifier(assign_preferred_resistor)
|
|
|
|
Component(
|
|
name = "R1",
|
|
footprint = "0603",
|
|
pin_defs = {"1": "1", "2": "2"},
|
|
pins = {"1": Net("A"), "2": Net("B")},
|
|
properties = {"resistance": "10k"},
|
|
)
|
|
|
|
Component(
|
|
name = "R2",
|
|
footprint = "0603",
|
|
pin_defs = {"1": "1", "2": "2"},
|
|
pins = {"1": Net("C"), "2": Net("D")},
|
|
properties = {"resistance": "100k"},
|
|
)
|
|
|
|
Component(
|
|
name = "R3",
|
|
footprint = "0603",
|
|
pin_defs = {"1": "1", "2": "2"},
|
|
pins = {"1": Net("E"), "2": Net("F")},
|
|
properties = {"resistance": "1k"}, # No modifier for this
|
|
)
|
|
|
|
# R1 should have 10K_PART, R2 should have 100K_PART, R3 should have no MPN
|
|
# This is verified by the module snapshot
|
|
"#
|
|
});
|
|
|
|
snapshot_eval!(component_modifier_dnp, {
|
|
"test.zen" => r#"
|
|
# Test component modifier setting DNP
|
|
def mark_dnp_for_test_points(component):
|
|
if hasattr(component, "type"):
|
|
if str(component.type) == "test_point":
|
|
component.dnp = True
|
|
|
|
builtin.add_component_modifier(mark_dnp_for_test_points)
|
|
|
|
Component(
|
|
name = "TP1",
|
|
footprint = "test_point",
|
|
type = "test_point",
|
|
pin_defs = {"1": "1"},
|
|
pins = {"1": Net("SIGNAL")},
|
|
)
|
|
|
|
Component(
|
|
name = "R1",
|
|
footprint = "0603",
|
|
type = "resistor",
|
|
pin_defs = {"1": "1", "2": "2"},
|
|
pins = {"1": Net("A"), "2": Net("B")},
|
|
)
|
|
|
|
# Test point should have dnp=True, resistor should not
|
|
# This is verified by the module snapshot
|
|
"#
|
|
});
|
|
|
|
snapshot_eval!(component_modifier_spice_model, {
|
|
"r.lib" => r#"
|
|
.SUBCKT my_resistor p n PARAMS: RVAL=1k
|
|
R1 p n {RVAL}
|
|
.ENDS my_resistor
|
|
"#,
|
|
"test.zen" => r#"
|
|
def assign_spice_model(component):
|
|
if hasattr(component, "resistance"):
|
|
pins = component.pins
|
|
component.spice_model = SpiceModel(
|
|
"r.lib",
|
|
"my_resistor",
|
|
nets=[pins["1"], pins["2"]],
|
|
args={"RVAL": "1000"},
|
|
)
|
|
|
|
builtin.add_component_modifier(assign_spice_model)
|
|
|
|
Component(
|
|
name = "R1",
|
|
footprint = "0603",
|
|
pin_defs = {"1": "1", "2": "2"},
|
|
pins = {"1": Net("A"), "2": Net("B")},
|
|
properties = {"resistance": "10k"},
|
|
)
|
|
"#
|
|
});
|
|
|
|
snapshot_eval!(component_modifier_multiple, {
|
|
"test.zen" => r#"
|
|
# Test multiple component modifiers in sequence
|
|
def modifier1(component):
|
|
if hasattr(component, "resistance"):
|
|
component.mod1_applied = "yes"
|
|
|
|
def modifier2(component):
|
|
if hasattr(component, "resistance"):
|
|
component.mod2_applied = "yes"
|
|
component.part = builtin.Part(mpn="FINAL_MPN", manufacturer="ACME")
|
|
|
|
builtin.add_component_modifier(modifier1)
|
|
builtin.add_component_modifier(modifier2)
|
|
|
|
Component(
|
|
name = "R1",
|
|
footprint = "0603",
|
|
pin_defs = {"1": "1", "2": "2"},
|
|
pins = {"1": Net("A"), "2": Net("B")},
|
|
properties = {"resistance": "10k"},
|
|
)
|
|
|
|
# Both modifiers should have run, setting mod1_applied, mod2_applied, and mpn
|
|
# This is verified by the module snapshot
|
|
"#
|
|
});
|
|
|
|
snapshot_eval!(component_modifier_parent, {
|
|
"Child.zen" => r#"
|
|
# Child module creates a component
|
|
Component(
|
|
name = "R1",
|
|
footprint = "0603",
|
|
pin_defs = {"1": "1", "2": "2"},
|
|
pins = {"1": Net("A"), "2": Net("B")},
|
|
properties = {"resistance": "10k"},
|
|
)
|
|
|
|
# Component should have parent_modified and manufacturer set by parent modifier
|
|
# This is verified by the module snapshot
|
|
"#,
|
|
"test.zen" => r#"
|
|
# Parent module registers a modifier
|
|
def parent_modifier(component):
|
|
if hasattr(component, "resistance"):
|
|
component.parent_modified = "yes"
|
|
component.part = builtin.Part(mpn="PARENT_MPN", manufacturer="ParentVendor")
|
|
|
|
builtin.add_component_modifier(parent_modifier)
|
|
|
|
# Instantiate child - components in child should get parent modifier
|
|
Child = Module("Child.zen")
|
|
Child(name = "ChildInstance")
|
|
"#
|
|
});
|
|
|
|
snapshot_eval!(component_modifier_child_overrides_parent, {
|
|
"Child.zen" => r#"
|
|
# Child modifier runs first and sets manufacturer
|
|
def child_modifier(component):
|
|
if hasattr(component, "resistance"):
|
|
component.part = builtin.Part(mpn="CHILD_MPN", manufacturer="ChildVendor")
|
|
|
|
builtin.add_component_modifier(child_modifier)
|
|
|
|
Component(
|
|
name = "R1",
|
|
footprint = "0603",
|
|
pin_defs = {"1": "1", "2": "2"},
|
|
pins = {"1": Net("A"), "2": Net("B")},
|
|
properties = {"resistance": "10k"},
|
|
)
|
|
|
|
# Parent modifier ran AFTER child modifier
|
|
# So final value should be ParentVendor (parent overwrites child)
|
|
# This is verified by the module snapshot
|
|
"#,
|
|
"test.zen" => r#"
|
|
# Parent modifier sets manufacturer
|
|
def parent_modifier(component):
|
|
if hasattr(component, "resistance"):
|
|
component.part = builtin.Part(mpn="PARENT_MPN", manufacturer="ParentVendor")
|
|
|
|
builtin.add_component_modifier(parent_modifier)
|
|
|
|
Child = Module("Child.zen")
|
|
Child(name = "ChildInstance")
|
|
"#
|
|
});
|
|
|
|
snapshot_eval!(component_modifier_grandparent, {
|
|
"Child.zen" => r#"
|
|
def child_modifier(component):
|
|
if hasattr(component, "resistance"):
|
|
component.child_modified = "yes"
|
|
|
|
builtin.add_component_modifier(child_modifier)
|
|
|
|
Component(
|
|
name = "R1",
|
|
footprint = "0603",
|
|
pin_defs = {"1": "1", "2": "2"},
|
|
pins = {"1": Net("A"), "2": Net("B")},
|
|
properties = {"resistance": "10k"},
|
|
)
|
|
|
|
# All modifiers should have run (bottom-up: child, parent, grandparent)
|
|
# This is verified by the module snapshot
|
|
"#,
|
|
"Parent.zen" => r#"
|
|
def parent_modifier(component):
|
|
if hasattr(component, "resistance"):
|
|
component.parent_modified = "yes"
|
|
|
|
builtin.add_component_modifier(parent_modifier)
|
|
|
|
Child = Module("Child.zen")
|
|
Child(name = "ChildInstance")
|
|
"#,
|
|
"test.zen" => r#"
|
|
def grandparent_modifier(component):
|
|
if hasattr(component, "resistance"):
|
|
component.gp_modified = "yes"
|
|
|
|
builtin.add_component_modifier(grandparent_modifier)
|
|
|
|
# Create hierarchy: Grandparent -> Parent -> Child
|
|
Parent = Module("Parent.zen")
|
|
Parent(name = "ParentInstance")
|
|
"#
|
|
});
|
|
|
|
snapshot_eval!(component_modifier_execution_order, {
|
|
"Child.zen" => r#"
|
|
def child_modifier(component):
|
|
if hasattr(component, "order"):
|
|
component.order = component.order + " -> child"
|
|
else:
|
|
component.order = "child"
|
|
|
|
builtin.add_component_modifier(child_modifier)
|
|
|
|
Component(
|
|
name = "R1",
|
|
footprint = "0603",
|
|
pin_defs = {"1": "1", "2": "2"},
|
|
pins = {"1": Net("A"), "2": Net("B")},
|
|
properties = {"resistance": "10k"},
|
|
)
|
|
|
|
# Execution order should be: child first, then parent
|
|
# Component should have order = "child -> parent"
|
|
# This is verified by the module snapshot
|
|
"#,
|
|
"test.zen" => r#"
|
|
def parent_modifier(component):
|
|
if hasattr(component, "order"):
|
|
component.order = component.order + " -> parent"
|
|
else:
|
|
component.order = "parent"
|
|
|
|
builtin.add_component_modifier(parent_modifier)
|
|
|
|
Child = Module("Child.zen")
|
|
Child(name = "ChildInstance")
|
|
"#
|
|
});
|
|
|
|
snapshot_eval!(current_module_path_root, {
|
|
"test.zen" => r#"
|
|
path = builtin.current_module_path()
|
|
print("Root module path:", path)
|
|
print("Root path length:", len(path))
|
|
print("Is root:", len(path) == 0)
|
|
"#
|
|
});
|
|
|
|
snapshot_eval!(current_module_path_visible, {
|
|
"Child.zen" => r#"
|
|
# Store the module path in component properties so it's visible in snapshot
|
|
path = builtin.current_module_path()
|
|
|
|
Component(
|
|
name = "R1",
|
|
footprint = "0603",
|
|
pin_defs = {"1": "1", "2": "2"},
|
|
pins = {"1": Net("A"), "2": Net("B")},
|
|
properties = {
|
|
"module_path": str(path),
|
|
"module_depth": len(path),
|
|
"is_root": len(path) == 0,
|
|
},
|
|
)
|
|
"#,
|
|
"test.zen" => r#"
|
|
path = builtin.current_module_path()
|
|
print("Root module path:", path)
|
|
print("Root is_root:", len(path) == 0)
|
|
|
|
Child = Module("Child.zen")
|
|
Child(name = "ChildInstance")
|
|
|
|
# Create component in root too
|
|
Component(
|
|
name = "R2",
|
|
footprint = "0603",
|
|
pin_defs = {"1": "1", "2": "2"},
|
|
pins = {"1": Net("A"), "2": Net("B")},
|
|
properties = {
|
|
"module_path": str(path),
|
|
"module_depth": len(path),
|
|
"is_root": len(path) == 0,
|
|
},
|
|
)
|
|
"#
|
|
});
|
|
|
|
snapshot_eval!(current_module_path_nested_visible, {
|
|
"GrandChild.zen" => r#"
|
|
path = builtin.current_module_path()
|
|
|
|
Component(
|
|
name = "R1",
|
|
footprint = "0603",
|
|
pin_defs = {"1": "1", "2": "2"},
|
|
pins = {"1": Net("A"), "2": Net("B")},
|
|
properties = {
|
|
"module_path": str(path),
|
|
"module_depth": len(path),
|
|
},
|
|
)
|
|
"#,
|
|
"Child.zen" => r#"
|
|
path = builtin.current_module_path()
|
|
|
|
Component(
|
|
name = "R2",
|
|
footprint = "0603",
|
|
pin_defs = {"1": "1", "2": "2"},
|
|
pins = {"1": Net("A"), "2": Net("B")},
|
|
properties = {
|
|
"module_path": str(path),
|
|
"module_depth": len(path),
|
|
},
|
|
)
|
|
|
|
GrandChild = Module("GrandChild.zen")
|
|
GrandChild(name = "GrandChildInstance")
|
|
"#,
|
|
"test.zen" => r#"
|
|
path = builtin.current_module_path()
|
|
print("Root module depth:", len(path))
|
|
|
|
Child = Module("Child.zen")
|
|
Child(name = "ChildInstance")
|
|
"#
|
|
});
|
|
|
|
snapshot_eval!(current_module_path_conditional_modifier, {
|
|
"Child.zen" => r#"
|
|
def child_modifier(component):
|
|
component.modified_in_child = True
|
|
|
|
# This should NOT run in child (not root)
|
|
if len(builtin.current_module_path()) == 0:
|
|
builtin.add_component_modifier(child_modifier)
|
|
|
|
Component(
|
|
name = "R1",
|
|
footprint = "0603",
|
|
pin_defs = {"1": "1", "2": "2"},
|
|
pins = {"1": Net("A"), "2": Net("B")},
|
|
)
|
|
|
|
# Child component should NOT have modified_in_child
|
|
# (because modifier is only added at root)
|
|
"#,
|
|
"test.zen" => r#"
|
|
def root_modifier(component):
|
|
component.modified_in_root = True
|
|
|
|
# This SHOULD run in root
|
|
if len(builtin.current_module_path()) == 0:
|
|
builtin.add_component_modifier(root_modifier)
|
|
|
|
Child = Module("Child.zen")
|
|
Child(name = "ChildInstance")
|
|
|
|
# Create a component in root to verify modifier runs
|
|
Component(
|
|
name = "R2",
|
|
footprint = "0603",
|
|
pin_defs = {"1": "1", "2": "2"},
|
|
pins = {"1": Net("A"), "2": Net("B")},
|
|
)
|
|
|
|
# Root component should have modified_in_root=True
|
|
# This is verified by the module snapshot
|
|
"#
|
|
});
|
|
|
|
snapshot_eval!(component_modifier_order_independent, {
|
|
"test.zen" => r#"
|
|
# Test that modifiers apply to all components regardless of registration order
|
|
# Components created BEFORE modifier registration should also be modified
|
|
|
|
Component(
|
|
name = "R1",
|
|
footprint = "0603",
|
|
pin_defs = {"1": "1", "2": "2"},
|
|
pins = {"1": Net("A"), "2": Net("B")},
|
|
properties = {"resistance": "10k"},
|
|
)
|
|
|
|
Component(
|
|
name = "R2",
|
|
footprint = "0603",
|
|
pin_defs = {"1": "1", "2": "2"},
|
|
pins = {"1": Net("C"), "2": Net("D")},
|
|
properties = {"resistance": "100k"},
|
|
)
|
|
|
|
# Register modifier AFTER components are created
|
|
def assign_manufacturer(component):
|
|
if hasattr(component, "resistance"):
|
|
component.part = builtin.Part(mpn="POST_MPN", manufacturer="PostRegistrationVendor")
|
|
component.modified = "yes"
|
|
|
|
builtin.add_component_modifier(assign_manufacturer)
|
|
|
|
# Create more components AFTER modifier registration
|
|
Component(
|
|
name = "R3",
|
|
footprint = "0603",
|
|
pin_defs = {"1": "1", "2": "2"},
|
|
pins = {"1": Net("E"), "2": Net("F")},
|
|
properties = {"resistance": "1k"},
|
|
)
|
|
|
|
# ALL three components should have manufacturer and modified set
|
|
# R1 and R2 were created before modifier registration
|
|
# R3 was created after modifier registration
|
|
# All should be modified because modifiers now apply at end of evaluation
|
|
# This is verified by the module snapshot
|
|
"#
|
|
});
|
|
|
|
fn warning_kinds(diagnostics: &pcb_zen_core::Diagnostics) -> std::collections::HashSet<String> {
|
|
diagnostics
|
|
.warnings()
|
|
.into_iter()
|
|
.filter_map(|diag| {
|
|
diag.innermost()
|
|
.downcast_error_ref::<CategorizedDiagnostic>()
|
|
.map(|err| err.kind.clone())
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn eval_component_diagnostics(files: Vec<(String, String)>) -> pcb_zen_core::Diagnostics {
|
|
let result = common::eval_zen(files);
|
|
assert!(result.is_success(), "eval failed: {:?}", result.diagnostics);
|
|
let mut diagnostics = result.diagnostics;
|
|
SortPass.apply(&mut diagnostics);
|
|
diagnostics
|
|
}
|
|
|
|
#[test]
|
|
fn warns_for_no_connect_pin() {
|
|
let diagnostics = eval_component_diagnostics(vec![
|
|
(
|
|
"nc_pin.kicad_sym".to_string(),
|
|
r#"(kicad_symbol_lib
|
|
(version 20211014)
|
|
(generator "test")
|
|
(symbol "NcPin"
|
|
(property "Reference" "U")
|
|
(symbol "NcPin_0_1"
|
|
(pin no_connect line
|
|
(at 0 0 0)
|
|
(length 2.54)
|
|
(name "NC")
|
|
(number "1")
|
|
)
|
|
)
|
|
)
|
|
)"#
|
|
.to_string(),
|
|
),
|
|
(
|
|
"test.zen".to_string(),
|
|
r#"
|
|
symbol = Symbol(library = "nc_pin.kicad_sym")
|
|
|
|
Component(
|
|
name = "U1",
|
|
footprint = File("@kicad-footprints/Resistor_SMD.pretty/R_0402_1005Metric.kicad_mod"),
|
|
symbol = symbol,
|
|
pins = {
|
|
"NC": Net("SIG"),
|
|
},
|
|
)
|
|
"#
|
|
.to_string(),
|
|
),
|
|
]);
|
|
let warnings = diagnostics.warnings();
|
|
let kinds = warning_kinds(&diagnostics);
|
|
|
|
assert!(kinds.contains("pin.no_connect"));
|
|
assert!(
|
|
warnings
|
|
.iter()
|
|
.any(|diag| diag.body.contains("marked no_connect")
|
|
&& diag.body.contains("omit it from `pins`")),
|
|
"expected no_connect warning, got: {:?}",
|
|
warnings
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn warns_for_explicit_not_connected_pin() {
|
|
let diagnostics = eval_component_diagnostics(vec![
|
|
(
|
|
"nc_pin.kicad_sym".to_string(),
|
|
r#"(kicad_symbol_lib
|
|
(version 20211014)
|
|
(generator "test")
|
|
(symbol "NcPin"
|
|
(property "Reference" "U")
|
|
(symbol "NcPin_0_1"
|
|
(pin no_connect line
|
|
(at 0 0 0)
|
|
(length 2.54)
|
|
(name "NC")
|
|
(number "1")
|
|
)
|
|
)
|
|
)
|
|
)"#
|
|
.to_string(),
|
|
),
|
|
(
|
|
"test.zen".to_string(),
|
|
r#"
|
|
NotConnected = builtin.not_connected
|
|
symbol = Symbol(library = "nc_pin.kicad_sym")
|
|
|
|
Component(
|
|
name = "U1",
|
|
footprint = File("@kicad-footprints/Resistor_SMD.pretty/R_0402_1005Metric.kicad_mod"),
|
|
symbol = symbol,
|
|
pins = {
|
|
"NC": NotConnected(),
|
|
},
|
|
)
|
|
"#
|
|
.to_string(),
|
|
),
|
|
]);
|
|
|
|
let warnings = diagnostics.warnings();
|
|
let kinds = warning_kinds(&diagnostics);
|
|
|
|
assert!(kinds.contains("pin.no_connect"));
|
|
assert!(
|
|
warnings
|
|
.iter()
|
|
.any(|diag| diag.body.contains("explicitly connected to NotConnected")),
|
|
"expected explicit NotConnected warning, got: {:?}",
|
|
warnings
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn omitting_no_connect_pin_is_allowed() {
|
|
let diagnostics = eval_component_diagnostics(vec![
|
|
(
|
|
"nc_pin.kicad_sym".to_string(),
|
|
r#"(kicad_symbol_lib
|
|
(version 20211014)
|
|
(generator "test")
|
|
(symbol "NcPin"
|
|
(property "Reference" "U")
|
|
(symbol "NcPin_0_1"
|
|
(pin no_connect line
|
|
(at 0 0 0)
|
|
(length 2.54)
|
|
(name "NC")
|
|
(number "1")
|
|
)
|
|
)
|
|
)
|
|
)"#
|
|
.to_string(),
|
|
),
|
|
(
|
|
"test.zen".to_string(),
|
|
r#"
|
|
symbol = Symbol(library = "nc_pin.kicad_sym")
|
|
|
|
Component(
|
|
name = "U1",
|
|
footprint = File("@kicad-footprints/Resistor_SMD.pretty/R_0402_1005Metric.kicad_mod"),
|
|
symbol = symbol,
|
|
pins = {},
|
|
)
|
|
"#
|
|
.to_string(),
|
|
),
|
|
]);
|
|
|
|
assert!(
|
|
!warning_kinds(&diagnostics).contains("pin.no_connect"),
|
|
"did not expect pin.no_connect warning, got: {:?}",
|
|
diagnostics.warnings()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn warns_for_power_pin_on_plain_net() {
|
|
let diagnostics = eval_component_diagnostics(vec![
|
|
(
|
|
"power_pin.kicad_sym".to_string(),
|
|
r#"(kicad_symbol_lib
|
|
(version 20211014)
|
|
(generator "test")
|
|
(symbol "PowerPin"
|
|
(property "Reference" "U")
|
|
(symbol "PowerPin_0_1"
|
|
(pin power_in line
|
|
(at 0 0 0)
|
|
(length 2.54)
|
|
(name "VCC")
|
|
(number "1")
|
|
)
|
|
)
|
|
)
|
|
)"#
|
|
.to_string(),
|
|
),
|
|
(
|
|
"test.zen".to_string(),
|
|
r#"
|
|
symbol = Symbol(library = "power_pin.kicad_sym")
|
|
|
|
Component(
|
|
name = "U1",
|
|
footprint = File("@kicad-footprints/Resistor_SMD.pretty/R_0402_1005Metric.kicad_mod"),
|
|
symbol = symbol,
|
|
pins = {
|
|
"VCC": Net("VCC"),
|
|
},
|
|
)
|
|
"#
|
|
.to_string(),
|
|
),
|
|
]);
|
|
let warnings = diagnostics.warnings();
|
|
let kinds = warning_kinds(&diagnostics);
|
|
|
|
assert!(kinds.contains("pin.power_net"));
|
|
assert!(
|
|
warnings.iter().any(
|
|
|diag| diag.body.contains("power pin") && diag.body.contains("Power() or Ground()")
|
|
),
|
|
"expected power pin warning, got: {:?}",
|
|
warnings
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn alternate_pin_suppresses_warning() {
|
|
let diagnostics = eval_component_diagnostics(vec![
|
|
(
|
|
"alt_pin.kicad_sym".to_string(),
|
|
r#"(kicad_symbol_lib
|
|
(version 20211014)
|
|
(generator "test")
|
|
(symbol "AltPin"
|
|
(property "Reference" "U")
|
|
(symbol "AltPin_0_1"
|
|
(pin power_in line
|
|
(at 0 0 0)
|
|
(length 2.54)
|
|
(name "PIO1")
|
|
(number "1")
|
|
(alternate "GPIO1" input line)
|
|
)
|
|
)
|
|
)
|
|
)"#
|
|
.to_string(),
|
|
),
|
|
(
|
|
"test.zen".to_string(),
|
|
r#"
|
|
symbol = Symbol(library = "alt_pin.kicad_sym")
|
|
|
|
Component(
|
|
name = "U1",
|
|
footprint = File("@kicad-footprints/Resistor_SMD.pretty/R_0402_1005Metric.kicad_mod"),
|
|
symbol = symbol,
|
|
pins = {
|
|
"PIO1": Net("SIG"),
|
|
},
|
|
)
|
|
"#
|
|
.to_string(),
|
|
),
|
|
]);
|
|
let kinds = warning_kinds(&diagnostics);
|
|
|
|
assert!(!kinds.contains("pin.power_net"));
|
|
assert!(
|
|
diagnostics
|
|
.warnings()
|
|
.iter()
|
|
.all(|diag| !diag.body.contains("power pin")),
|
|
"did not expect power pin warning, got: {:?}",
|
|
diagnostics.warnings()
|
|
);
|
|
}
|
|
|
|
snapshot_eval!(module_schematic_collapse, {
|
|
"SubModule.zen" => r#"
|
|
vcc = io(Net)
|
|
gnd = io(Net)
|
|
|
|
Component(
|
|
name = "R1",
|
|
footprint = "0402",
|
|
pin_defs = {"1": "1", "2": "2"},
|
|
pins = {"1": vcc, "2": gnd},
|
|
)
|
|
"#,
|
|
"test.zen" => r#"
|
|
SubMod = Module("SubModule.zen")
|
|
|
|
vcc = Net("VCC")
|
|
gnd = Net("GND")
|
|
|
|
SubMod(
|
|
name = "collapsed_module",
|
|
vcc = vcc,
|
|
gnd = gnd,
|
|
schematic = "collapse"
|
|
)
|
|
"#
|
|
});
|
|
|
|
snapshot_eval!(module_schematic_embed, {
|
|
"SubModule.zen" => r#"
|
|
vcc = io(Net)
|
|
gnd = io(Net)
|
|
|
|
Component(
|
|
name = "R1",
|
|
footprint = "0402",
|
|
pin_defs = {"1": "1", "2": "2"},
|
|
pins = {"1": vcc, "2": gnd},
|
|
)
|
|
"#,
|
|
"test.zen" => r#"
|
|
SubMod = Module("SubModule.zen")
|
|
|
|
vcc = Net("VCC")
|
|
gnd = Net("GND")
|
|
|
|
SubMod(
|
|
name = "embedded_module",
|
|
vcc = vcc,
|
|
gnd = gnd,
|
|
schematic = "embed"
|
|
)
|
|
"#
|
|
});
|
|
|
|
snapshot_eval!(module_schematic_invalid, {
|
|
"SubModule.zen" => r#"
|
|
vcc = io(Net)
|
|
"#,
|
|
"test.zen" => r#"
|
|
SubMod = Module("SubModule.zen")
|
|
|
|
SubMod(
|
|
name = "invalid_module",
|
|
vcc = Net("VCC"),
|
|
schematic = "invalid_value"
|
|
)
|
|
"#
|
|
});
|
|
|
|
fn legacy_property_diagnostics(
|
|
diagnostics: &pcb_zen_core::Diagnostics,
|
|
severity: EvalSeverity,
|
|
) -> Vec<String> {
|
|
diagnostics
|
|
.diagnostics
|
|
.iter()
|
|
.filter(|diag| {
|
|
matches!(
|
|
(&diag.severity, &severity),
|
|
(EvalSeverity::Warning, EvalSeverity::Warning)
|
|
| (EvalSeverity::Advice, EvalSeverity::Advice)
|
|
| (EvalSeverity::Error, EvalSeverity::Error)
|
|
)
|
|
})
|
|
.filter_map(|diag| {
|
|
let kind = diag
|
|
.innermost()
|
|
.downcast_error_ref::<CategorizedDiagnostic>()?
|
|
.kind
|
|
.clone();
|
|
if kind == "deprecated.component_property" {
|
|
Some(diag.body.clone())
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
#[test]
|
|
fn errors_for_each_legacy_component_property_key() {
|
|
let result = common::eval_zen(vec![(
|
|
"test.zen".to_string(),
|
|
r#"
|
|
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},
|
|
properties = {
|
|
"do_not_populate": True,
|
|
"Do_not_populate": True,
|
|
"DNP": True,
|
|
"dnp": True,
|
|
"Exclude_from_bom": True,
|
|
"exclude_from_bom": True,
|
|
"Exclude_from_pos_files": True,
|
|
"exclude_from_pos_files": True,
|
|
"type": "resistor",
|
|
"Type": "resistor",
|
|
"mpn": "X",
|
|
"Mpn": "X",
|
|
"manufacturer": "M",
|
|
"Manufacturer": "M",
|
|
"datasheet": "ds.pdf",
|
|
"description": "desc",
|
|
"Description": "desc",
|
|
},
|
|
)
|
|
"#
|
|
.to_string(),
|
|
)]);
|
|
let mut diagnostics = result.diagnostics;
|
|
SortPass.apply(&mut diagnostics);
|
|
|
|
let error_bodies = legacy_property_diagnostics(&diagnostics, EvalSeverity::Error);
|
|
|
|
for (legacy_key, typed_kwarg) in [
|
|
("do_not_populate", "dnp"),
|
|
("Do_not_populate", "dnp"),
|
|
("DNP", "dnp"),
|
|
("dnp", "dnp"),
|
|
("Exclude_from_bom", "skip_bom"),
|
|
("exclude_from_bom", "skip_bom"),
|
|
("Exclude_from_pos_files", "skip_pos"),
|
|
("exclude_from_pos_files", "skip_pos"),
|
|
("type", "type"),
|
|
("Type", "type"),
|
|
("datasheet", "datasheet"),
|
|
("description", "description"),
|
|
("Description", "description"),
|
|
] {
|
|
let expected = format!(
|
|
"Component 'R1': `properties[\"{legacy_key}\"]` is no longer supported; pass `{typed_kwarg}=...` to Component() instead"
|
|
);
|
|
assert!(
|
|
error_bodies.iter().any(|b| b == &expected),
|
|
"expected legacy-property error for `{legacy_key}`, got: {:?}",
|
|
error_bodies
|
|
);
|
|
}
|
|
|
|
for sourcing_key in ["mpn", "Mpn", "manufacturer", "Manufacturer"] {
|
|
let expected = format!(
|
|
"Component 'R1': `properties[\"{sourcing_key}\"]` is no longer supported; pass `part=Part(mpn=..., manufacturer=...)` to Component() instead"
|
|
);
|
|
assert!(
|
|
error_bodies.iter().any(|b| b == &expected),
|
|
"expected sourcing-key error for `{sourcing_key}`, got: {:?}",
|
|
error_bodies
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn no_legacy_warning_when_typed_kwargs_are_used() {
|
|
let diagnostics = eval_component_diagnostics(vec![(
|
|
"test.zen".to_string(),
|
|
r#"
|
|
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 = builtin.Part(mpn = "RC0603FR-071KL", manufacturer = "Yageo"),
|
|
type = "resistor",
|
|
dnp = True,
|
|
skip_bom = True,
|
|
skip_pos = True,
|
|
datasheet = "ds.pdf",
|
|
description = "desc",
|
|
properties = {"resistance": "1k"},
|
|
)
|
|
"#
|
|
.to_string(),
|
|
)]);
|
|
|
|
let warning_bodies = legacy_property_diagnostics(&diagnostics, EvalSeverity::Warning);
|
|
let advice_bodies = legacy_property_diagnostics(&diagnostics, EvalSeverity::Advice);
|
|
assert!(
|
|
warning_bodies.is_empty(),
|
|
"expected no legacy-property warnings, got: {:?}",
|
|
warning_bodies
|
|
);
|
|
assert!(
|
|
advice_bodies.is_empty(),
|
|
"expected no legacy-property advice, got: {:?}",
|
|
advice_bodies
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn advises_for_mpn_and_manufacturer_kwargs() {
|
|
let diagnostics = eval_component_diagnostics(vec![(
|
|
"test.zen".to_string(),
|
|
r#"
|
|
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},
|
|
mpn = "RC0603FR-071KL",
|
|
manufacturer = "Yageo",
|
|
)
|
|
"#
|
|
.to_string(),
|
|
)]);
|
|
|
|
let warning_bodies = legacy_property_diagnostics(&diagnostics, EvalSeverity::Warning);
|
|
let advice_bodies = legacy_property_diagnostics(&diagnostics, EvalSeverity::Advice);
|
|
|
|
assert!(
|
|
warning_bodies.is_empty(),
|
|
"expected sourcing kwargs to avoid warnings, got: {:?}",
|
|
warning_bodies
|
|
);
|
|
|
|
for kwarg in ["mpn", "manufacturer"] {
|
|
let expected = format!(
|
|
"Component 'R1': `{kwarg}=...` is deprecated; pass `part=Part(mpn=..., manufacturer=...)` to Component() instead"
|
|
);
|
|
assert!(
|
|
advice_bodies.iter().any(|b| b == &expected),
|
|
"expected sourcing-kwarg advice for `{kwarg}`, got: {:?}",
|
|
advice_bodies
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn empty_legacy_mpn_kwarg_counts_as_missing_part_info() {
|
|
let eval_result = common::eval_zen(vec![(
|
|
"test.zen".to_string(),
|
|
r#"
|
|
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},
|
|
mpn = "",
|
|
manufacturer = "Yageo",
|
|
)
|
|
"#
|
|
.to_string(),
|
|
)]);
|
|
|
|
assert!(
|
|
eval_result.is_success(),
|
|
"eval failed: {:?}",
|
|
eval_result.diagnostics
|
|
);
|
|
|
|
let eval_output = eval_result.output.expect("expected eval output");
|
|
let mut diagnostics = eval_output.to_schematic_with_diagnostics().diagnostics;
|
|
SortPass.apply(&mut diagnostics);
|
|
|
|
assert!(
|
|
diagnostics.iter().any(|diag| {
|
|
matches!(diag.severity, EvalSeverity::Error)
|
|
&& diag
|
|
.downcast_error_ref::<CategorizedDiagnostic>()
|
|
.is_some_and(|c| c.kind == "bom.unspecified")
|
|
}),
|
|
"expected empty legacy mpn to leave part info unspecified, got: {:?}",
|
|
diagnostics
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn part_kwarg_does_not_warn() {
|
|
let diagnostics = eval_component_diagnostics(vec![(
|
|
"test.zen".to_string(),
|
|
r#"
|
|
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 = builtin.Part(mpn = "RC0603FR-071KL", manufacturer = "Yageo"),
|
|
)
|
|
"#
|
|
.to_string(),
|
|
)]);
|
|
|
|
let warning_bodies = legacy_property_diagnostics(&diagnostics, EvalSeverity::Warning);
|
|
let advice_bodies = legacy_property_diagnostics(&diagnostics, EvalSeverity::Advice);
|
|
assert!(
|
|
warning_bodies.is_empty(),
|
|
"expected no warnings when using part=Part(...), got: {:?}",
|
|
warning_bodies
|
|
);
|
|
assert!(
|
|
advice_bodies.is_empty(),
|
|
"expected no advice when using part=Part(...), got: {:?}",
|
|
advice_bodies
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn legacy_dnp_property_still_takes_effect() {
|
|
let result = common::eval_zen(vec![(
|
|
"test.zen".to_string(),
|
|
r#"
|
|
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},
|
|
properties = {"do_not_populate": True},
|
|
)
|
|
"#
|
|
.to_string(),
|
|
)]);
|
|
let mut diagnostics = result.diagnostics;
|
|
SortPass.apply(&mut diagnostics);
|
|
|
|
assert!(
|
|
legacy_property_diagnostics(&diagnostics, EvalSeverity::Error)
|
|
.iter()
|
|
.any(|body| body.contains("`properties[\"do_not_populate\"]` is no longer supported")),
|
|
"expected legacy-property error, got: {:?}",
|
|
diagnostics
|
|
);
|
|
|
|
let output = result
|
|
.output
|
|
.expect("expected eval output despite diagnostics");
|
|
let module_tree = output.module_tree();
|
|
let root_module = module_tree
|
|
.values()
|
|
.find(|module| module.path().is_root())
|
|
.expect("expected root module");
|
|
let component = root_module
|
|
.components()
|
|
.next()
|
|
.expect("expected root component");
|
|
|
|
assert!(
|
|
component.dnp(),
|
|
"legacy do_not_populate should still set dnp"
|
|
);
|
|
}
|