mirror of
https://github.com/diodeinc/pcb.git
synced 2026-09-14 20:17:19 +08:00
1103 lines
33 KiB
Rust
1103 lines
33 KiB
Rust
#![cfg(not(target_os = "windows"))]
|
|
|
|
use pcb_test_utils::assert_snapshot;
|
|
use pcb_test_utils::sandbox::Sandbox;
|
|
|
|
const SIMPLE_RESISTOR_ZEN: &str = r#"
|
|
value = config(str, default = "10kOhm")
|
|
|
|
P1 = io(Net)
|
|
P2 = io(Net)
|
|
|
|
Resistance = "foobar"
|
|
|
|
Component(
|
|
name = "R",
|
|
prefix = "R",
|
|
footprint = File("test.kicad_mod"),
|
|
pin_defs = {"P1": "1", "P2": "2"},
|
|
pins = {"P1": P1, "P2": P2},
|
|
type = "resistor",
|
|
properties = {"value": value},
|
|
)
|
|
"#;
|
|
|
|
const WARNING_AND_ERROR_ZEN: &str = r#"# ```pcb
|
|
# [workspace]
|
|
# pcb-version = "0.4"
|
|
# [dependencies]
|
|
# "github.com/mycompany/components" = "1.0.0"
|
|
# ```
|
|
|
|
SimpleResistor = Module("github.com/mycompany/components/SimpleResistor.zen")
|
|
|
|
vcc = Net("VCC")
|
|
gnd = Net("GND")
|
|
# This will cause an error - missing required parameter
|
|
SimpleResistor(name = "R1", P1 = vcc)
|
|
"#;
|
|
|
|
const TEST_KICAD_MOD: &str = r#"(footprint "test"
|
|
(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 TEST_NO_CONNECT_SYMBOL: &str = 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")
|
|
)
|
|
)
|
|
)
|
|
)
|
|
"#;
|
|
|
|
const DIODES_ZEN: &str = r#"
|
|
Rectifier = Module("@stdlib/generics/Rectifier.zen")
|
|
Zener = Module("@stdlib/generics/Zener.zen")
|
|
|
|
vin = Power("VIN")
|
|
gnd = Ground("GND")
|
|
protected = Net("PROTECTED")
|
|
|
|
Rectifier(
|
|
name = "D1",
|
|
package = "DO-214AC",
|
|
reverse_voltage = "40V",
|
|
forward_current = "1A",
|
|
A = vin,
|
|
K = protected,
|
|
)
|
|
|
|
Zener(
|
|
name = "D2",
|
|
package = "SOD-123",
|
|
zener_voltage = "5.1V",
|
|
power = "500mW",
|
|
A = gnd,
|
|
K = protected,
|
|
)
|
|
"#;
|
|
|
|
const SUPPRESSED_WARNINGS_ZEN: &str = r#"
|
|
warn("Regular warning")
|
|
warn("Suppressed warning 1", suppress=True)
|
|
warn("Suppressed warning 2", suppress=True)
|
|
"#;
|
|
|
|
const SUPPRESSED_ERRORS_ZEN: &str = r#"
|
|
error("Suppressed error 1", suppress=True)
|
|
error("Suppressed error 2", suppress=True)
|
|
"#;
|
|
|
|
const MIXED_SUPPRESSED_ZEN: &str = r#"
|
|
warn("Regular warning 1")
|
|
warn("Suppressed warning 1", suppress=True)
|
|
warn("Suppressed warning 2", suppress=True)
|
|
error("Suppressed error 1", suppress=True)
|
|
warn("Regular warning 2")
|
|
"#;
|
|
|
|
const CATEGORIZED_DIAGNOSTICS_ZEN: &str = r#"
|
|
warn("Voltage mismatch detected", kind="electrical.voltage_mismatch")
|
|
warn("Spacing violation", kind="layout.spacing")
|
|
warn("BOM missing part", kind="bom.missing_part")
|
|
warn("Regular warning without kind")
|
|
"#;
|
|
|
|
const MULTIPLE_ELECTRICAL_WARNINGS_ZEN: &str = r#"
|
|
warn("Overvoltage detected", kind="electrical.voltage.overvoltage")
|
|
warn("Undervoltage detected", kind="electrical.voltage.undervoltage")
|
|
warn("Current too high", kind="electrical.current.overcurrent")
|
|
warn("Layout issue", kind="layout.spacing")
|
|
"#;
|
|
|
|
const MIXED_CATEGORIZED_ZEN: &str = r#"
|
|
warn("Regular warning")
|
|
warn("Voltage issue", kind="electrical.voltage")
|
|
warn("Another regular warning")
|
|
error("Layout error", suppress=True, kind="layout.error")
|
|
warn("BOM warning", kind="bom.missing")
|
|
"#;
|
|
|
|
// Tests for inline comment suppression
|
|
const INLINE_SUPPRESS_BASIC_ZEN: &str = r#"
|
|
warn("This should be suppressed", kind="test.match") # suppress: test.match
|
|
warn("This should not be suppressed", kind="test.match")
|
|
"#;
|
|
|
|
const INLINE_SUPPRESS_HIERARCHICAL_ZEN: &str = r#"
|
|
warn("Voltage warning", kind="electrical.voltage.overvoltage") # suppress: electrical
|
|
warn("Current warning", kind="electrical.current.overcurrent") # suppress: electrical
|
|
warn("Layout warning", kind="layout.spacing")
|
|
"#;
|
|
|
|
const INLINE_SUPPRESS_SEVERITY_ZEN: &str = r#"
|
|
warn("Warning 1") # suppress: warnings
|
|
warn("Warning 2")
|
|
error("Error 1", suppress=True) # suppress: errors
|
|
"#;
|
|
|
|
const INLINE_SUPPRESS_MULTIPLE_ZEN: &str = r#"
|
|
warn("Should be suppressed", kind="test.match") # suppress: test.match, electrical
|
|
warn("Should not be suppressed", kind="layout.spacing")
|
|
"#;
|
|
|
|
const INLINE_SUPPRESS_ALL_ZEN: &str = r#"
|
|
warn("Suppressed by all", kind="test.match") # suppress: all
|
|
error("Also suppressed", suppress=True, kind="electrical.voltage") # suppress: all
|
|
warn("Not suppressed", kind="layout.spacing")
|
|
"#;
|
|
|
|
const INLINE_SUPPRESS_CASE_INSENSITIVE_ZEN: &str = r#"
|
|
warn("Suppressed", kind="test.match") # SUPPRESS: test.match
|
|
warn("Also suppressed") # suppress: WARNINGS
|
|
"#;
|
|
|
|
const INLINE_SUPPRESS_NO_SPACE_ZEN: &str = r#"
|
|
warn("Suppressed without space", kind="test.match") #suppress: test.match
|
|
warn("Suppressed with space", kind="electrical.voltage") # suppress: electrical
|
|
"#;
|
|
|
|
// Tests for previous-line suppression
|
|
const PREVIOUS_LINE_SUPPRESS_BASIC_ZEN: &str = r#"
|
|
# suppress: test.match
|
|
warn("This should be suppressed", kind="test.match")
|
|
warn("This should not be suppressed", kind="test.match")
|
|
"#;
|
|
|
|
const PREVIOUS_LINE_SUPPRESS_HIERARCHICAL_ZEN: &str = r#"
|
|
# suppress: electrical
|
|
warn("Voltage warning", kind="electrical.voltage.overvoltage")
|
|
# suppress: electrical
|
|
warn("Current warning", kind="electrical.current.overcurrent")
|
|
warn("Layout warning not suppressed", kind="layout.spacing")
|
|
"#;
|
|
|
|
const PREVIOUS_LINE_SUPPRESS_MULTIPLE_ZEN: &str = r#"
|
|
# suppress: test.match, electrical.voltage
|
|
warn("Should be suppressed by first pattern", kind="test.match")
|
|
# suppress: layout, warnings
|
|
warn("Should be suppressed by warnings pattern")
|
|
"#;
|
|
|
|
const PREVIOUS_LINE_MIXED_WITH_INLINE_ZEN: &str = r#"
|
|
# suppress: test.match
|
|
warn("Suppressed by previous line", kind="test.match")
|
|
warn("Suppressed by inline", kind="electrical.voltage") # suppress: electrical
|
|
warn("Not suppressed", kind="layout.spacing")
|
|
"#;
|
|
|
|
const PREVIOUS_LINE_WITH_COMMENT_ZEN: &str = r#"
|
|
# This is a regular comment explaining the code
|
|
# suppress: test.match
|
|
warn("Should be suppressed", kind="test.match")
|
|
"#;
|
|
|
|
const PREVIOUS_LINE_MULTILINE_STATEMENT_ZEN: &str = r#"
|
|
# suppress: test.match
|
|
warn(
|
|
"Should be suppressed",
|
|
kind="test.match"
|
|
)
|
|
"#;
|
|
|
|
const INLINE_NO_CROSS_LINE_CONTAMINATION_ZEN: &str = r#"
|
|
warn("Warning 1") # suppress: warnings
|
|
warn("Warning 2 should NOT be suppressed")
|
|
warn("Warning 3") # suppress: warnings
|
|
warn("Warning 4 should NOT be suppressed")
|
|
"#;
|
|
|
|
const INVALID_INHERITED_SYMBOL_DATASHEET_COMPONENT_ZEN: &str = r#"
|
|
P1 = io(Net)
|
|
P2 = io(Net)
|
|
|
|
Component(
|
|
name = "U",
|
|
symbol = Symbol(library = "Part.kicad_sym"),
|
|
pins = {"P1": P1, "P2": P2},
|
|
part = Part(mpn = "TEST", manufacturer = "TEST"),
|
|
)
|
|
"#;
|
|
|
|
const INVALID_INHERITED_SYMBOL_DATASHEET_BOARD_ZEN: &str = r#"
|
|
Part = Module("components/TestPart/Part.zen")
|
|
|
|
Part(name = "U1", P1 = Net("A"), P2 = Net("B"))
|
|
"#;
|
|
|
|
const CONFIGURABLE_BUILD_ZEN: &str = r#"
|
|
Resistor = Module("@stdlib/generics/Resistor.zen")
|
|
Mode = enum("ONE", "TWO")
|
|
|
|
enable_extra = config(bool, default=False)
|
|
count = config(int, default=1)
|
|
mode = config(Mode, default=Mode("ONE"))
|
|
package = config(str, default="0603")
|
|
|
|
vcc = Power("VCC")
|
|
gnd = Ground("GND")
|
|
|
|
for i in range(count):
|
|
Resistor(
|
|
name = "R{}".format(i + 1),
|
|
value = "1kohm",
|
|
package = package,
|
|
P1 = vcc,
|
|
P2 = gnd,
|
|
)
|
|
|
|
if enable_extra:
|
|
Resistor(name = "R_EXTRA", value = "2kohm", package = package, P1 = vcc, P2 = gnd)
|
|
|
|
if mode == Mode("TWO"):
|
|
Resistor(name = "R_MODE", value = "3kohm", package = package, P1 = vcc, P2 = gnd)
|
|
"#;
|
|
|
|
const PIN_NO_CONNECT_REPORTS_AT_NET_ZEN: &str = r#"
|
|
sig = Net("SIG")
|
|
|
|
Component(
|
|
name = "U1",
|
|
footprint = File("test.kicad_mod"),
|
|
symbol = Symbol(library = "nc_pin.kicad_sym"),
|
|
part = Part(mpn = "TEST", manufacturer = "TEST"),
|
|
pins = {
|
|
"NC": sig,
|
|
},
|
|
)
|
|
"#;
|
|
|
|
const PIN_NO_CONNECT_SUPPRESSES_AT_NET_ZEN: &str = r#"
|
|
sig = Net("SIG") # suppress: pin.no_connect
|
|
|
|
Component(
|
|
name = "U1",
|
|
footprint = File("test.kicad_mod"),
|
|
symbol = Symbol(library = "nc_pin.kicad_sym"),
|
|
part = Part(mpn = "TEST", manufacturer = "TEST"),
|
|
pins = {
|
|
"NC": sig,
|
|
},
|
|
)
|
|
"#;
|
|
|
|
const PIN_NO_CONNECT_NESTED_MODULE_DEDUPS_ZEN: &str = r#"
|
|
Child = Module("child.zen")
|
|
|
|
Child(name = "X1")
|
|
"#;
|
|
|
|
const PIN_NO_CONNECT_NESTED_CHILD_ZEN: &str = r#"
|
|
sig = Net("SIG")
|
|
|
|
Component(
|
|
name = "U1",
|
|
footprint = File("test.kicad_mod"),
|
|
symbol = Symbol(library = "nc_pin.kicad_sym"),
|
|
part = Part(mpn = "TEST", manufacturer = "TEST"),
|
|
pins = {
|
|
"NC": sig,
|
|
},
|
|
)
|
|
"#;
|
|
|
|
#[test]
|
|
fn test_warning_and_error_mixed() {
|
|
let mut sandbox = Sandbox::new();
|
|
|
|
// Create a fake git repository with a simple component
|
|
sandbox
|
|
.git_fixture("https://github.com/mycompany/components.git")
|
|
.write("pcb.toml", "[dependencies]")
|
|
.write("SimpleResistor.zen", SIMPLE_RESISTOR_ZEN)
|
|
.write("test.kicad_mod", TEST_KICAD_MOD)
|
|
.commit("Add simple resistor component")
|
|
.tag("v1.0.0", false)
|
|
.push_mirror();
|
|
|
|
// Create a board that has both a warning (unstable ref) and an error (missing param)
|
|
let output = sandbox
|
|
.write("board.zen", WARNING_AND_ERROR_ZEN)
|
|
.snapshot_run("pcbc", ["build", "board.zen"]);
|
|
assert_snapshot!("warning_and_error_mixed", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_pin_no_connect_reports_at_net_site() {
|
|
let mut sandbox = Sandbox::new().with_workspace();
|
|
let output = sandbox
|
|
.write("board.zen", PIN_NO_CONNECT_REPORTS_AT_NET_ZEN)
|
|
.write("test.kicad_mod", TEST_KICAD_MOD)
|
|
.write("nc_pin.kicad_sym", TEST_NO_CONNECT_SYMBOL)
|
|
.snapshot_run("pcbc", ["build", "board.zen"]);
|
|
assert_snapshot!("pin_no_connect_reports_at_net_site", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_pin_no_connect_suppresses_at_net_site() {
|
|
let mut sandbox = Sandbox::new().with_workspace();
|
|
let output = sandbox
|
|
.write("board.zen", PIN_NO_CONNECT_SUPPRESSES_AT_NET_ZEN)
|
|
.write("test.kicad_mod", TEST_KICAD_MOD)
|
|
.write("nc_pin.kicad_sym", TEST_NO_CONNECT_SYMBOL)
|
|
.snapshot_run("pcbc", ["build", "board.zen"]);
|
|
assert_snapshot!("pin_no_connect_suppresses_at_net_site", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_pin_no_connect_dedups_in_nested_modules() {
|
|
let mut sandbox = Sandbox::new().with_workspace();
|
|
let output = sandbox
|
|
.write("board.zen", PIN_NO_CONNECT_NESTED_MODULE_DEDUPS_ZEN)
|
|
.write("child.zen", PIN_NO_CONNECT_NESTED_CHILD_ZEN)
|
|
.write("test.kicad_mod", TEST_KICAD_MOD)
|
|
.write("nc_pin.kicad_sym", TEST_NO_CONNECT_SYMBOL)
|
|
.snapshot_run("pcbc", ["build", "board.zen"]);
|
|
|
|
assert_eq!(output.matches("Warning:").count(), 1, "{output}");
|
|
assert!(output.contains("net 'SIG'"), "{output}");
|
|
}
|
|
|
|
#[test]
|
|
fn test_build_with_config_overrides() {
|
|
let output = Sandbox::new()
|
|
.with_workspace()
|
|
.write("board.zen", CONFIGURABLE_BUILD_ZEN)
|
|
.snapshot_run(
|
|
"pcbc",
|
|
[
|
|
"build",
|
|
"--config",
|
|
"enable_extra=true",
|
|
"--config",
|
|
"count=2",
|
|
"--config",
|
|
"mode=TWO",
|
|
"--config",
|
|
"package=0402",
|
|
"board.zen",
|
|
],
|
|
);
|
|
|
|
assert!(output.contains("Exit Code: 0"), "{output}");
|
|
assert!(output.contains("(4 components)"), "{output}");
|
|
}
|
|
|
|
#[test]
|
|
fn test_build_rejects_unknown_config_override() {
|
|
let output = Sandbox::new()
|
|
.with_workspace()
|
|
.write("board.zen", CONFIGURABLE_BUILD_ZEN)
|
|
.snapshot_run(
|
|
"pcbc",
|
|
["build", "--config", "frequency=100MHz", "board.zen"],
|
|
);
|
|
|
|
assert!(output.contains("Exit Code: 1"), "{output}");
|
|
assert!(
|
|
output.contains("Unknown root input(s): frequency"),
|
|
"{output}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_diodes_build() {
|
|
let output = Sandbox::new()
|
|
.with_workspace()
|
|
.write("diodes.zen", DIODES_ZEN)
|
|
.snapshot_run("pcbc", ["build", "diodes.zen"]);
|
|
|
|
assert!(output.contains("Exit Code: 0"), "{output}");
|
|
}
|
|
|
|
#[test]
|
|
fn test_invalid_inherited_symbol_datasheet_is_silent() {
|
|
let output = Sandbox::new().with_workspace()
|
|
.write(
|
|
"components/TestPart/Part.kicad_sym",
|
|
r#"(kicad_symbol_lib
|
|
(version 20241209)
|
|
(symbol "Part"
|
|
(property "Reference" "U" (at 0 0 0) (effects (font (size 1.27 1.27))))
|
|
(property "Value" "Part" (at 0 -2.54 0) (effects (font (size 1.27 1.27))))
|
|
(property "Footprint" "Part" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))
|
|
(property "Datasheet" "missing-datasheet.pdf" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))
|
|
(symbol "Part_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)))))
|
|
)
|
|
)
|
|
)"#,
|
|
)
|
|
.write("components/TestPart/Part.kicad_mod", TEST_KICAD_MOD)
|
|
.write(
|
|
"components/TestPart/Part.zen",
|
|
INVALID_INHERITED_SYMBOL_DATASHEET_COMPONENT_ZEN,
|
|
)
|
|
.write("board.zen", INVALID_INHERITED_SYMBOL_DATASHEET_BOARD_ZEN)
|
|
.snapshot_run("pcbc", ["build", "board.zen"]);
|
|
|
|
assert!(output.contains("Exit Code: 0"), "{output}");
|
|
assert!(!output.contains("Warning:"), "{output}");
|
|
assert!(!output.contains("Error:"), "{output}");
|
|
}
|
|
|
|
#[test]
|
|
fn test_suppressed_warnings() {
|
|
let output = Sandbox::new()
|
|
.with_workspace()
|
|
.write("test.zen", SUPPRESSED_WARNINGS_ZEN)
|
|
.snapshot_run("pcbc", ["build", "test.zen"]);
|
|
assert_snapshot!("suppressed_warnings", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_suppressed_errors() {
|
|
let output = Sandbox::new()
|
|
.with_workspace()
|
|
.write("test.zen", SUPPRESSED_ERRORS_ZEN)
|
|
.snapshot_run("pcbc", ["build", "test.zen"]);
|
|
assert_snapshot!("suppressed_errors", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_mixed_suppressed_diagnostics() {
|
|
let output = Sandbox::new()
|
|
.with_workspace()
|
|
.write("test.zen", MIXED_SUPPRESSED_ZEN)
|
|
.snapshot_run("pcbc", ["build", "test.zen"]);
|
|
assert_snapshot!("mixed_suppressed_diagnostics", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_suppressed_warnings_with_deny_flag() {
|
|
// Suppressed warnings should not cause build failure even with -Dwarnings
|
|
let output = Sandbox::new()
|
|
.with_workspace()
|
|
.write("test.zen", SUPPRESSED_ERRORS_ZEN)
|
|
.snapshot_run("pcbc", ["build", "test.zen", "-Dwarnings"]);
|
|
assert_snapshot!("suppressed_with_deny_flag", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_mixed_suppressed_with_deny_flag() {
|
|
// Regular warnings should still fail with -Dwarnings, but suppressed should not
|
|
let output = Sandbox::new()
|
|
.with_workspace()
|
|
.write("test.zen", MIXED_SUPPRESSED_ZEN)
|
|
.snapshot_run("pcbc", ["build", "test.zen", "-Dwarnings"]);
|
|
assert_snapshot!("mixed_suppressed_with_deny_flag", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_aggregated_warnings() {
|
|
let mut sandbox = Sandbox::new();
|
|
|
|
// Create a fake git repository with components
|
|
sandbox
|
|
.git_fixture("https://github.com/mycompany/components.git")
|
|
.write(
|
|
"SimpleResistor/pcb.toml",
|
|
r#"
|
|
[dependencies]
|
|
"#,
|
|
)
|
|
.write("SimpleResistor/SimpleResistor.zen", SIMPLE_RESISTOR_ZEN)
|
|
.write("SimpleResistor/test.kicad_mod", TEST_KICAD_MOD)
|
|
.commit("Add simple resistor component")
|
|
.tag("SimpleResistor/v1.0.0", false)
|
|
.push_mirror();
|
|
|
|
// Create pcb.toml with a package alias.
|
|
let pcb_toml_content = r#"
|
|
[workspace]
|
|
pcb-version = "0.4"
|
|
|
|
[dependencies]
|
|
"github.com/mycompany/components/SimpleResistor" = "1.0.0"
|
|
"#;
|
|
|
|
// Create a board that uses the alias multiple times - should aggregate warnings
|
|
// because all warnings will trace back to the same PCB.toml line
|
|
let board_zen_content = r#"
|
|
SimpleResistor1 = Module("github.com/mycompany/components/SimpleResistor/SimpleResistor.zen")
|
|
SimpleResistor2 = Module("github.com/mycompany/components/SimpleResistor/SimpleResistor.zen")
|
|
SimpleResistor3 = Module("github.com/mycompany/components/SimpleResistor/SimpleResistor.zen")
|
|
|
|
vcc = Net("VCC")
|
|
gnd = Net("GND")
|
|
SimpleResistor1(name = "R1", value = "1kOhm", P1 = vcc, P2 = gnd)
|
|
SimpleResistor2(name = "R2", value = "2kOhm", P1 = vcc, P2 = gnd)
|
|
SimpleResistor3(name = "R3", value = "3kOhm", P1 = vcc, P2 = gnd)
|
|
"#;
|
|
|
|
let output = sandbox
|
|
.write("pcb.toml", pcb_toml_content)
|
|
.write("board.zen", board_zen_content)
|
|
.snapshot_run("pcbc", ["build", "board.zen"]);
|
|
assert_snapshot!("aggregated_warnings", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_mixed_aggregated_and_unique_warnings() {
|
|
let mut sandbox = Sandbox::new();
|
|
|
|
// Create multiple fake git repositories
|
|
sandbox
|
|
.git_fixture("https://github.com/company1/components.git")
|
|
.write(
|
|
"Component1/pcb.toml",
|
|
r#"
|
|
[dependencies]
|
|
"#,
|
|
)
|
|
.write("Component1/Component1.zen", SIMPLE_RESISTOR_ZEN)
|
|
.write("Component1/test.kicad_mod", TEST_KICAD_MOD)
|
|
.commit("Add component1")
|
|
.tag("Component1/v1.0.0", false)
|
|
.push_mirror();
|
|
|
|
sandbox
|
|
.git_fixture("https://github.com/company2/components.git")
|
|
.write(
|
|
"Component2/pcb.toml",
|
|
r#"
|
|
[dependencies]
|
|
"#,
|
|
)
|
|
.write("Component2/Component2.zen", SIMPLE_RESISTOR_ZEN)
|
|
.write("Component2/test.kicad_mod", TEST_KICAD_MOD)
|
|
.commit("Add component2")
|
|
.tag("Component2/v1.0.0", false)
|
|
.push_mirror();
|
|
|
|
// Create pcb.toml with dependencies for both deps.
|
|
// The first dep is referenced twice and should aggregate.
|
|
let pcb_toml_content = r#"
|
|
[workspace]
|
|
pcb-version = "0.4"
|
|
|
|
[dependencies]
|
|
"github.com/company1/components/Component1" = "1.0.0"
|
|
"github.com/company2/components/Component2" = "1.0.0"
|
|
"#;
|
|
|
|
// Create a board with both aggregated and unique warnings
|
|
let board_zen_content = r#"
|
|
# These should aggregate (same dependency line used multiple times -> same PCB.toml span)
|
|
Comp1a = Module("github.com/company1/components/Component1/Component1.zen")
|
|
Comp1b = Module("github.com/company1/components/Component1/Component1.zen")
|
|
# This should be unique (separate dependency line)
|
|
Comp2 = Module("github.com/company2/components/Component2/Component2.zen")
|
|
|
|
vcc = Net("VCC")
|
|
gnd = Net("GND")
|
|
Comp1a(name = "R1", value = "1kOhm", P1 = vcc, P2 = gnd)
|
|
Comp1b(name = "R2", value = "2kOhm", P1 = vcc, P2 = gnd)
|
|
Comp2(name = "R3", value = "3kOhm", P1 = vcc, P2 = gnd)
|
|
"#;
|
|
|
|
let output = sandbox
|
|
.write("pcb.toml", pcb_toml_content)
|
|
.write("board.zen", board_zen_content)
|
|
.snapshot_run("pcbc", ["build", "board.zen"]);
|
|
assert_snapshot!("mixed_aggregated_and_unique_warnings", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_commit_stable_ref() {
|
|
let mut sandbox = Sandbox::new();
|
|
|
|
let short_hash = &sandbox
|
|
.git_fixture("https://github.com/mycompany/components.git")
|
|
.branch("foo")
|
|
.write(
|
|
"pcb.toml",
|
|
r#"
|
|
[dependencies]
|
|
"#,
|
|
)
|
|
.write("SimpleResistor.zen", SIMPLE_RESISTOR_ZEN)
|
|
.write("test.kicad_mod", TEST_KICAD_MOD)
|
|
.commit("Add simple resistor component")
|
|
.push_mirror()
|
|
.rev_parse_head()[0..7];
|
|
|
|
// Read-only build rejects non-exact refs; `pcb sync` owns dependency hydration.
|
|
let unstable_default_zen = format!(
|
|
r#"# ```pcb
|
|
# [workspace]
|
|
# pcb-version = "0.4"
|
|
#
|
|
# [dependencies]
|
|
# "github.com/mycompany/components" = {{ rev = "{}" }}
|
|
# ```
|
|
|
|
SimpleResistor = Module("github.com/mycompany/components/SimpleResistor.zen")
|
|
"#,
|
|
short_hash
|
|
);
|
|
|
|
let output = sandbox
|
|
.write("board.zen", unstable_default_zen)
|
|
.snapshot_run("pcbc", ["build", "board.zen"]);
|
|
assert!(
|
|
!output.contains("Exit Code: 0"),
|
|
"expected build to reject non-exact dependency ref:\n{output}"
|
|
);
|
|
assert!(
|
|
output.contains("must specify an exact version"),
|
|
"expected exact-version rejection:\n{output}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_inline_manifest() {
|
|
// Standalone .zen file with inline pcb.toml
|
|
// Uses minimal code that doesn't require dependencies
|
|
let inline_manifest_zen = r#"# ```pcb
|
|
# [workspace]
|
|
# pcb-version = "0.4"
|
|
# ```
|
|
|
|
# Simple standalone script - no dependencies needed
|
|
x = 1 + 2
|
|
"#;
|
|
|
|
let output = Sandbox::new()
|
|
.write("standalone.zen", inline_manifest_zen)
|
|
.snapshot_run("pcbc", ["build", "standalone.zen"]);
|
|
assert_snapshot!("inline_manifest", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_inline_manifest_dependency() {
|
|
let mut sandbox = Sandbox::new();
|
|
sandbox
|
|
.git_fixture("https://github.com/mycompany/components.git")
|
|
.write("SimpleResistor/pcb.toml", "[dependencies]\n")
|
|
.write("SimpleResistor/SimpleResistor.zen", SIMPLE_RESISTOR_ZEN)
|
|
.write("SimpleResistor/test.kicad_mod", TEST_KICAD_MOD)
|
|
.commit("Add SimpleResistor package")
|
|
.tag("SimpleResistor/v1.0.0", false)
|
|
.push_mirror();
|
|
|
|
let inline_manifest_zen = r#"# ```pcb
|
|
# [workspace]
|
|
# pcb-version = "0.4"
|
|
#
|
|
# [dependencies]
|
|
# "github.com/mycompany/components/SimpleResistor" = "1.0.0"
|
|
# ```
|
|
|
|
SimpleResistor = Module("github.com/mycompany/components/SimpleResistor/SimpleResistor.zen")
|
|
|
|
vcc = Net("VCC")
|
|
gnd = Net("GND")
|
|
|
|
SimpleResistor(name = "R1", P1 = vcc, P2 = gnd)
|
|
"#;
|
|
|
|
let output = sandbox
|
|
.write("standalone.zen", inline_manifest_zen)
|
|
.snapshot_run("pcbc", ["build", "standalone.zen"]);
|
|
assert_snapshot!("inline_manifest_dependency", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_inline_manifest_unnamed_net_error() {
|
|
let inline_manifest_zen = r#"# ```pcb
|
|
# [workspace]
|
|
# pcb-version = "0.4"
|
|
# ```
|
|
|
|
Component(
|
|
name = "U1",
|
|
footprint = File("@kicad-footprints/Resistor_SMD.pretty/R_0402_1005Metric.kicad_mod"),
|
|
pin_defs = {"P1": "1"},
|
|
pins = {"P1": Net()},
|
|
part = Part(mpn = "TEST", manufacturer = "TEST"),
|
|
)
|
|
"#;
|
|
|
|
let output = Sandbox::new()
|
|
.write("standalone.zen", inline_manifest_zen)
|
|
.snapshot_run("pcbc", ["build", "standalone.zen"]);
|
|
assert_snapshot!("inline_manifest_unnamed_net_error", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_unused_module_io_warning() {
|
|
let leaf_module = r#"
|
|
VIN = io(Net)
|
|
|
|
Component(
|
|
name = "LOAD",
|
|
footprint = File("test.kicad_mod"),
|
|
pin_defs = {"P": "1"},
|
|
pins = {"P": VIN},
|
|
part = Part(mpn = "TEST", manufacturer = "TEST"),
|
|
)
|
|
"#;
|
|
|
|
let wrapper_module = r#"
|
|
Leaf = Module("Leaf.zen")
|
|
|
|
VIN = io(Net)
|
|
SPARE = io(Net)
|
|
|
|
Leaf(name = "LEAF", VIN = VIN)
|
|
"#;
|
|
|
|
let board = r#"
|
|
Wrapper = Module("Wrapper.zen")
|
|
|
|
Wrapper(
|
|
name = "WRAP",
|
|
VIN = Net("VIN"),
|
|
SPARE = Net("SPARE"),
|
|
)
|
|
"#;
|
|
|
|
let output = Sandbox::new()
|
|
.with_workspace()
|
|
.write("Leaf.zen", leaf_module)
|
|
.write("Wrapper.zen", wrapper_module)
|
|
.write("board.zen", board)
|
|
.write("test.kicad_mod", TEST_KICAD_MOD)
|
|
.snapshot_run("pcbc", ["build", "board.zen"]);
|
|
assert_snapshot!("unused_module_io_warning", output);
|
|
}
|
|
|
|
// Tests for -S flag with kind-based suppression
|
|
|
|
#[test]
|
|
fn test_suppress_by_exact_kind() {
|
|
// Suppress only electrical.voltage_mismatch
|
|
let output = Sandbox::new()
|
|
.with_workspace()
|
|
.write("test.zen", CATEGORIZED_DIAGNOSTICS_ZEN)
|
|
.snapshot_run(
|
|
"pcbc",
|
|
["build", "test.zen", "-S", "electrical.voltage_mismatch"],
|
|
);
|
|
assert_snapshot!("suppress_by_exact_kind", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_build_writes_diagnostics_json() {
|
|
let mut sandbox = Sandbox::new().with_workspace();
|
|
sandbox.write("test.zen", CATEGORIZED_DIAGNOSTICS_ZEN);
|
|
|
|
sandbox
|
|
.run(
|
|
"pcbc",
|
|
["build", "test.zen", "--diagnostics", "diagnostics.json"],
|
|
)
|
|
.run()
|
|
.expect("build should succeed");
|
|
|
|
let report_path = sandbox.root_path().join("diagnostics.json");
|
|
let report: serde_json::Value = serde_json::from_str(
|
|
&std::fs::read_to_string(report_path).expect("diagnostics report should be written"),
|
|
)
|
|
.expect("diagnostics report should be valid JSON");
|
|
|
|
let diagnostics = report
|
|
.get("test.zen")
|
|
.and_then(|value| value.as_array())
|
|
.expect("report should include diagnostics for the evaluated root file");
|
|
assert_eq!(diagnostics.len(), 4);
|
|
assert_eq!(diagnostics[0]["severity"], "warning");
|
|
assert_eq!(diagnostics[0]["kind"], "electrical.voltage_mismatch");
|
|
assert_eq!(diagnostics[0]["body"], "Voltage mismatch detected");
|
|
assert_eq!(diagnostics[0]["suppressed"], false);
|
|
assert_eq!(diagnostics[3]["kind"], serde_json::Value::Null);
|
|
}
|
|
|
|
#[test]
|
|
fn test_build_writes_diagnostics_json_on_failure() {
|
|
let mut sandbox = Sandbox::new().with_workspace();
|
|
sandbox.write("test.zen", r#"error("Build failed")"#);
|
|
|
|
let output = sandbox
|
|
.run(
|
|
"pcbc",
|
|
["build", "test.zen", "--diagnostics", "diagnostics.json"],
|
|
)
|
|
.unchecked()
|
|
.run()
|
|
.expect("build command should run");
|
|
assert!(!output.status.success());
|
|
|
|
let report_path = sandbox.root_path().join("diagnostics.json");
|
|
let report: serde_json::Value = serde_json::from_str(
|
|
&std::fs::read_to_string(report_path).expect("diagnostics report should be written"),
|
|
)
|
|
.expect("diagnostics report should be valid JSON");
|
|
|
|
let diagnostics = report
|
|
.get("test.zen")
|
|
.and_then(|value| value.as_array())
|
|
.expect("report should include diagnostics for the evaluated root file");
|
|
assert_eq!(diagnostics.len(), 1);
|
|
assert_eq!(diagnostics[0]["severity"], "error");
|
|
assert_eq!(diagnostics[0]["body"], "Build failed");
|
|
}
|
|
|
|
#[test]
|
|
fn test_suppress_by_hierarchical_kind() {
|
|
// -S electrical should suppress all electrical.* warnings
|
|
let output = Sandbox::new()
|
|
.with_workspace()
|
|
.write("test.zen", MULTIPLE_ELECTRICAL_WARNINGS_ZEN)
|
|
.snapshot_run("pcbc", ["build", "test.zen", "-S", "electrical"]);
|
|
assert_snapshot!("suppress_by_hierarchical_kind", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_suppress_by_partial_hierarchy() {
|
|
// -S electrical.voltage should suppress electrical.voltage.* but not electrical.current.*
|
|
let output = Sandbox::new()
|
|
.with_workspace()
|
|
.write("test.zen", MULTIPLE_ELECTRICAL_WARNINGS_ZEN)
|
|
.snapshot_run("pcbc", ["build", "test.zen", "-S", "electrical.voltage"]);
|
|
assert_snapshot!("suppress_by_partial_hierarchy", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_suppress_multiple_kinds() {
|
|
// Suppress multiple different kinds
|
|
let output = Sandbox::new()
|
|
.with_workspace()
|
|
.write("test.zen", CATEGORIZED_DIAGNOSTICS_ZEN)
|
|
.snapshot_run(
|
|
"pcbc",
|
|
[
|
|
"build",
|
|
"test.zen",
|
|
"-S",
|
|
"electrical.voltage_mismatch",
|
|
"-S",
|
|
"layout.spacing",
|
|
],
|
|
);
|
|
assert_snapshot!("suppress_multiple_kinds", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_suppress_all_warnings_by_severity() {
|
|
// -S warnings should suppress all warnings regardless of kind
|
|
let output = Sandbox::new()
|
|
.with_workspace()
|
|
.write("test.zen", CATEGORIZED_DIAGNOSTICS_ZEN)
|
|
.snapshot_run("pcbc", ["build", "test.zen", "-S", "warnings"]);
|
|
assert_snapshot!("suppress_all_warnings_by_severity", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_suppress_all_errors_by_severity() {
|
|
let errors_zen = r#"
|
|
error("Error 1", suppress=True, kind="validation.error1")
|
|
error("Error 2", suppress=True, kind="validation.error2")
|
|
"#;
|
|
|
|
// -S errors should suppress all errors
|
|
let output = Sandbox::new()
|
|
.with_workspace()
|
|
.write("test.zen", errors_zen)
|
|
.snapshot_run("pcbc", ["build", "test.zen", "-S", "errors"]);
|
|
assert_snapshot!("suppress_all_errors_by_severity", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_suppress_kind_with_deny_warnings() {
|
|
// Suppressed warnings should not cause build failure even with -Dwarnings
|
|
let output = Sandbox::new()
|
|
.with_workspace()
|
|
.write("test.zen", CATEGORIZED_DIAGNOSTICS_ZEN)
|
|
.snapshot_run(
|
|
"pcbc",
|
|
[
|
|
"build",
|
|
"test.zen",
|
|
"-S",
|
|
"electrical.voltage_mismatch",
|
|
"-S",
|
|
"layout.spacing",
|
|
"-S",
|
|
"bom.missing_part",
|
|
"-Dwarnings",
|
|
],
|
|
);
|
|
assert_snapshot!("suppress_kind_with_deny_warnings", output);
|
|
}
|
|
|
|
// Tests for inline comment suppression
|
|
|
|
#[test]
|
|
fn test_inline_suppress_basic() {
|
|
let output = Sandbox::new()
|
|
.with_workspace()
|
|
.write("test.zen", INLINE_SUPPRESS_BASIC_ZEN)
|
|
.snapshot_run("pcbc", ["build", "test.zen"]);
|
|
assert_snapshot!("inline_suppress_basic", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_inline_suppress_hierarchical() {
|
|
let output = Sandbox::new()
|
|
.with_workspace()
|
|
.write("test.zen", INLINE_SUPPRESS_HIERARCHICAL_ZEN)
|
|
.snapshot_run("pcbc", ["build", "test.zen"]);
|
|
assert_snapshot!("inline_suppress_hierarchical", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_inline_suppress_severity() {
|
|
let output = Sandbox::new()
|
|
.with_workspace()
|
|
.write("test.zen", INLINE_SUPPRESS_SEVERITY_ZEN)
|
|
.snapshot_run("pcbc", ["build", "test.zen"]);
|
|
assert_snapshot!("inline_suppress_severity", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_inline_suppress_multiple_patterns() {
|
|
let output = Sandbox::new()
|
|
.with_workspace()
|
|
.write("test.zen", INLINE_SUPPRESS_MULTIPLE_ZEN)
|
|
.snapshot_run("pcbc", ["build", "test.zen"]);
|
|
assert_snapshot!("inline_suppress_multiple_patterns", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_inline_suppress_all() {
|
|
let output = Sandbox::new()
|
|
.with_workspace()
|
|
.write("test.zen", INLINE_SUPPRESS_ALL_ZEN)
|
|
.snapshot_run("pcbc", ["build", "test.zen"]);
|
|
assert_snapshot!("inline_suppress_all", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_inline_suppress_case_insensitive() {
|
|
let output = Sandbox::new()
|
|
.with_workspace()
|
|
.write("test.zen", INLINE_SUPPRESS_CASE_INSENSITIVE_ZEN)
|
|
.snapshot_run("pcbc", ["build", "test.zen"]);
|
|
assert_snapshot!("inline_suppress_case_insensitive", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_inline_suppress_no_space_after_hash() {
|
|
let output = Sandbox::new()
|
|
.with_workspace()
|
|
.write("test.zen", INLINE_SUPPRESS_NO_SPACE_ZEN)
|
|
.snapshot_run("pcbc", ["build", "test.zen"]);
|
|
assert_snapshot!("inline_suppress_no_space_after_hash", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_inline_suppress_combined_with_cli() {
|
|
// Both inline and CLI suppression should work together
|
|
let combined_zen = r#"
|
|
warn("Suppressed by inline", kind="test.match") # suppress: test.match
|
|
warn("Suppressed by CLI", kind="electrical.voltage")
|
|
warn("Not suppressed", kind="layout.spacing")
|
|
"#;
|
|
|
|
let output = Sandbox::new()
|
|
.with_workspace()
|
|
.write("test.zen", combined_zen)
|
|
.snapshot_run("pcbc", ["build", "test.zen", "-S", "electrical"]);
|
|
assert_snapshot!("inline_suppress_combined_with_cli", output);
|
|
}
|
|
|
|
// Tests for previous-line suppression
|
|
|
|
#[test]
|
|
fn test_previous_line_suppress_basic() {
|
|
let output = Sandbox::new()
|
|
.with_workspace()
|
|
.write("test.zen", PREVIOUS_LINE_SUPPRESS_BASIC_ZEN)
|
|
.snapshot_run("pcbc", ["build", "test.zen"]);
|
|
assert_snapshot!("previous_line_suppress_basic", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_previous_line_suppress_hierarchical() {
|
|
let output = Sandbox::new()
|
|
.with_workspace()
|
|
.write("test.zen", PREVIOUS_LINE_SUPPRESS_HIERARCHICAL_ZEN)
|
|
.snapshot_run("pcbc", ["build", "test.zen"]);
|
|
assert_snapshot!("previous_line_suppress_hierarchical", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_previous_line_suppress_multiple_patterns() {
|
|
let output = Sandbox::new()
|
|
.with_workspace()
|
|
.write("test.zen", PREVIOUS_LINE_SUPPRESS_MULTIPLE_ZEN)
|
|
.snapshot_run("pcbc", ["build", "test.zen"]);
|
|
assert_snapshot!("previous_line_suppress_multiple_patterns", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_previous_line_mixed_with_inline() {
|
|
let output = Sandbox::new()
|
|
.with_workspace()
|
|
.write("test.zen", PREVIOUS_LINE_MIXED_WITH_INLINE_ZEN)
|
|
.snapshot_run("pcbc", ["build", "test.zen"]);
|
|
assert_snapshot!("previous_line_mixed_with_inline", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_previous_line_with_regular_comment() {
|
|
let output = Sandbox::new()
|
|
.with_workspace()
|
|
.write("test.zen", PREVIOUS_LINE_WITH_COMMENT_ZEN)
|
|
.snapshot_run("pcbc", ["build", "test.zen"]);
|
|
assert_snapshot!("previous_line_with_regular_comment", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_previous_line_multiline_statement() {
|
|
let output = Sandbox::new()
|
|
.with_workspace()
|
|
.write("test.zen", PREVIOUS_LINE_MULTILINE_STATEMENT_ZEN)
|
|
.snapshot_run("pcbc", ["build", "test.zen"]);
|
|
assert_snapshot!("previous_line_multiline_statement", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_inline_no_cross_line_contamination() {
|
|
// End-of-line comments should NOT affect the next line
|
|
let output = Sandbox::new()
|
|
.with_workspace()
|
|
.write("test.zen", INLINE_NO_CROSS_LINE_CONTAMINATION_ZEN)
|
|
.snapshot_run("pcbc", ["build", "test.zen"]);
|
|
assert_snapshot!("inline_no_cross_line_contamination", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_mixed_suppress_and_regular_diagnostics() {
|
|
// Mix of suppressed (by -S) and regular warnings
|
|
let output = Sandbox::new()
|
|
.with_workspace()
|
|
.write("test.zen", MIXED_CATEGORIZED_ZEN)
|
|
.snapshot_run("pcbc", ["build", "test.zen", "-S", "electrical"]);
|
|
assert_snapshot!("mixed_suppress_and_regular", output);
|
|
}
|