← Commit history

Merge PR #443: 1.0.105: a warning type at KiCad's DRC cap no longer blocks live routing (closes #140)

Colby Knox ·a3bb03b16e ·13h ago ·parent 36b98a4
3 files changed +71−8
rust/Cargo.lock+3−3
@@ -319,7 +319,7 @@ dependencies = [  [[package]] name = "kicad-bridge"-version = "1.0.104"+version = "1.0.105" dependencies = [  "adom-eda-checks",  "kicad-core",@@ -331,7 +331,7 @@ dependencies = [  [[package]] name = "kicad-core"-version = "1.0.104"+version = "1.0.105" dependencies = [  "kicad-ipc-rs",  "kicad-platform",@@ -358,7 +358,7 @@ dependencies = [  [[package]] name = "kicad-platform"-version = "1.0.104"+version = "1.0.105" dependencies = [  "serde",  "serde_json",
rust/Cargo.toml+1−1
@@ -3,7 +3,7 @@ resolver = "2" members = ["crates/*"]  [workspace.package]-version = "1.0.104"+version = "1.0.105" edition = "2021" license = "MIT" publish = false
rust/crates/kicad-core/src/ipc.rs+67−4
@@ -241,13 +241,43 @@ fn fingerprint(v: &Value) -> String {     json!([v.get("type"), v.get("severity"), v.get("description"), positions]).to_string() } +/// The capped violation types in a report that hold errors (wiki #140). KiCad caps each type on+/// its own and grades every row of a type alike, so a warning type at the cap can hide neither+/// a new error of another type nor one of its own. The grade is read from the rows here, not+/// when the limits were computed, so a caller that re-grades rows (the silkscreen check grades+/// every finding an error) keeps those types. A report that says it is truncated without naming+/// the types counts as truncated.+pub fn capped_error_types(r: &Value) -> Vec<String> {+    if !r.get("violationReportMayBeTruncated").and_then(Value::as_bool).unwrap_or(false) {+        return Vec::new();+    }+    let Some(types) = r.get("possiblyTruncatedViolationTypes").and_then(Value::as_array) else {+        return vec!["(unnamed)".into()];+    };+    let rows = r.get("violations").and_then(Value::as_array).map(Vec::as_slice).unwrap_or(&[]);+    let mut out: Vec<String> = types+        .iter()+        .filter(|t| rows.iter().any(|v| v.get("type") == Some(*t) && v.get("severity").and_then(Value::as_str) == Some("error")))+        .map(|t| t.as_str().map(str::to_string).unwrap_or_else(|| t.to_string()))+        .collect();+    out.sort();+    out.dedup();+    out+}+ /// `_new_errors`: the error-severity violations in `after` that `before` does not /// account for, one to one (an existing error never masks a second identical one).-/// Refuses to certify a delta when either list may be capped (`drc_incomplete`).+/// Refuses to certify a delta when either list may be capped in a type that holds errors+/// (`drc_incomplete`); warning types at the cap are left to violationReportMayBeTruncated. pub fn new_errors(before: &Value, after: &Value) -> RResult<Vec<Value>> {-    let truncated = |r: &Value| r.get("violationReportMayBeTruncated").and_then(Value::as_bool).unwrap_or(false);-    if truncated(before) || truncated(after) {-        return Err(RoutingError::new("drc_incomplete", "DRC violation lists may be capped; cannot certify the error delta. No live copper was changed").with("mutated", json!(false)));+    let mut capped = capped_error_types(before);+    capped.extend(capped_error_types(after));+    capped.sort();+    capped.dedup();+    if !capped.is_empty() {+        return Err(RoutingError::new("drc_incomplete", format!("DRC lists of error types may be capped ({}); cannot certify the error delta. No live copper was changed", capped.join(", ")))+            .with("mutated", json!(false))+            .with("cappedErrorTypes", json!(capped)));     }     let errors = |r: &Value| -> Vec<Value> { r.get("violations").and_then(Value::as_array).map(|a| a.iter().filter(|v| v.get("severity").and_then(Value::as_str) == Some("error")).cloned().collect()).unwrap_or_default() };     let mut existing: Vec<(String, usize)> = Vec::new();@@ -2054,6 +2084,39 @@ mod tests {         }     } +    /// Wiki #140: 199 silk_overlap and 199 silk_over_copper warnings on a dense LED board blocked+    /// every live route although the board had no errors.+    #[test]+    fn warning_types_at_the_cap_do_not_block_the_error_delta() {+        let warn = |kind: &str| json!({"type": kind, "severity": "warning", "description": "silk", "items": []});+        let mut rows: Vec<Value> = vec![warn("silk_overlap"); 199];+        rows.extend(vec![warn("silk_over_copper"); 199]);+        let mut report = json!({"violations": rows});+        let limits = report_limits(report["violations"].as_array().unwrap(), 499);+        assert_eq!(limits["violationReportMayBeTruncated"], true, "still reported for information");+        assert_eq!(limits["possiblyTruncatedViolationTypes"], json!(["silk_over_copper", "silk_overlap"]));+        merge(&mut report, &limits);+        assert!(capped_error_types(&report).is_empty());+        assert_eq!(new_errors(&report, &report).unwrap(), Vec::<Value>::new());+        // a new error of another type is still seen+        let mut after = report.clone();+        let short = json!({"type": "shorting_items", "severity": "error", "description": "short", "items": []});+        after["violations"].as_array_mut().unwrap().push(short.clone());+        assert_eq!(new_errors(&report, &after).unwrap(), vec![short]);+        // re-graded as errors (the silkscreen check does this), the same capped types block again+        let regraded = |mut v: Value| {+            for r in v["violations"].as_array_mut().unwrap() {+                r["severity"] = json!("error");+            }+            v+        };+        let e = new_errors(&regraded(report.clone()), &regraded(report.clone())).unwrap_err();+        assert_eq!(e.code, "drc_incomplete");+        assert_eq!(e.detail["cappedErrorTypes"], json!(["silk_over_copper", "silk_overlap"]));+        // a report that is truncated without naming its types stays refused+        assert!(new_errors(&json!({"violations": [], "violationReportMayBeTruncated": true}), &json!({"violations": []})).is_err());+    }+     #[test]     fn existing_error_does_not_mask_additional_same_error() {         let v = json!({"type": "clearance", "severity": "error", "description": "collision", "items": [{"pos": {"x": 1, "y": 2}}]});