diff --git a/docs/traceability.md b/docs/traceability.md index 68cb37e..155ea81 100644 --- a/docs/traceability.md +++ b/docs/traceability.md @@ -9,8 +9,8 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n | Metric | Value | |---|---| -| Source files scanned | 254 | -| TRACES tags found | 732 | +| Source files scanned | 256 | +| TRACES tags found | 734 | | Requirements defined | 177 | | Requirements covered | 98 | | **Coverage** | **55.4%** (98/177) | @@ -58,7 +58,7 @@ _None._ | FR-CULL-9 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:384`](../core/dr-catalog/src/schema.rs#L384), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1) | | FR-DEV-2 | [`core/dr-pipeline/src/operation.rs:352`](../core/dr-pipeline/src/operation.rs#L352) | | FR-DEV-3 | [`core/dr-gpu/src/adjust.rs:2165`](../core/dr-gpu/src/adjust.rs#L2165), [`core/dr-gpu/src/adjust.rs:651`](../core/dr-gpu/src/adjust.rs#L651), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/adjust.rs:84`](../core/dr-gpu/src/adjust.rs#L84), [`core/dr-gpu/tests/tone_curve.rs:1`](../core/dr-gpu/tests/tone_curve.rs#L1), [`core/dr-pipeline/src/detail.rs:387`](../core/dr-pipeline/src/detail.rs#L387), [`core/dr-pipeline/src/detail.rs:465`](../core/dr-pipeline/src/detail.rs#L465), [`core/dr-pipeline/src/framing.rs:188`](../core/dr-pipeline/src/framing.rs#L188), [`core/dr-pipeline/src/framing.rs:362`](../core/dr-pipeline/src/framing.rs#L362), [`core/dr-pipeline/src/framing.rs:617`](../core/dr-pipeline/src/framing.rs#L617), [`core/dr-pipeline/src/graph.rs:169`](../core/dr-pipeline/src/graph.rs#L169), [`core/dr-pipeline/src/graph.rs:573`](../core/dr-pipeline/src/graph.rs#L573), [`core/dr-pipeline/src/mask.rs:120`](../core/dr-pipeline/src/mask.rs#L120), [`core/dr-pipeline/src/operation.rs:299`](../core/dr-pipeline/src/operation.rs#L299), [`core/dr-pipeline/src/operation.rs:479`](../core/dr-pipeline/src/operation.rs#L479), [`core/dr-pipeline/src/ops/capture_sharpen.rs:1`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L1), [`core/dr-pipeline/src/ops/capture_sharpen.rs:207`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L207), [`core/dr-pipeline/src/ops/curve.rs:1`](../core/dr-pipeline/src/ops/curve.rs#L1), [`core/dr-pipeline/src/ops/curve.rs:218`](../core/dr-pipeline/src/ops/curve.rs#L218), [`core/dr-pipeline/src/ops/curve.rs:631`](../core/dr-pipeline/src/ops/curve.rs#L631), [`core/dr-pipeline/src/ops/curve.rs:99`](../core/dr-pipeline/src/ops/curve.rs#L99), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:1`](../core/dr-pipeline/src/ops/noise_reduction.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:270`](../core/dr-pipeline/src/ops/noise_reduction.rs#L270), [`core/dr-pipeline/src/sidecar.rs:156`](../core/dr-pipeline/src/sidecar.rs#L156), [`core/dr-pipeline/src/sidecar.rs:1627`](../core/dr-pipeline/src/sidecar.rs#L1627), [`core/dr-pipeline/src/sidecar.rs:1687`](../core/dr-pipeline/src/sidecar.rs#L1687), [`core/dr-pipeline/tests/tone_curve.rs:1`](../core/dr-pipeline/tests/tone_curve.rs#L1), [`ui/dr-ui/src/develop.rs:101`](../ui/dr-ui/src/develop.rs#L101), [`ui/dr-ui/src/develop.rs:1280`](../ui/dr-ui/src/develop.rs#L1280), [`ui/dr-ui/src/develop.rs:163`](../ui/dr-ui/src/develop.rs#L163), [`ui/dr-ui/src/develop.rs:1758`](../ui/dr-ui/src/develop.rs#L1758), [`ui/dr-ui/src/develop.rs:1776`](../ui/dr-ui/src/develop.rs#L1776), [`ui/dr-ui/src/develop.rs:1790`](../ui/dr-ui/src/develop.rs#L1790), [`ui/dr-ui/src/develop.rs:1812`](../ui/dr-ui/src/develop.rs#L1812), [`ui/dr-ui/src/develop.rs:1958`](../ui/dr-ui/src/develop.rs#L1958), [`ui/dr-ui/src/develop.rs:2056`](../ui/dr-ui/src/develop.rs#L2056), [`ui/dr-ui/src/develop.rs:326`](../ui/dr-ui/src/develop.rs#L326), [`ui/dr-ui/src/develop.rs:3287`](../ui/dr-ui/src/develop.rs#L3287), [`ui/dr-ui/src/develop.rs:363`](../ui/dr-ui/src/develop.rs#L363), [`ui/dr-ui/src/develop.rs:3851`](../ui/dr-ui/src/develop.rs#L3851), [`ui/dr-ui/src/develop.rs:3905`](../ui/dr-ui/src/develop.rs#L3905), [`ui/dr-ui/src/develop.rs:3949`](../ui/dr-ui/src/develop.rs#L3949), [`ui/dr-ui/src/develop.rs:3999`](../ui/dr-ui/src/develop.rs#L3999), [`ui/dr-ui/src/develop.rs:628`](../ui/dr-ui/src/develop.rs#L628), [`ui/dr-ui/src/develop.rs:675`](../ui/dr-ui/src/develop.rs#L675), [`ui/dr-ui/src/lib.rs:1422`](../ui/dr-ui/src/lib.rs#L1422), [`ui/dr-ui/src/lib.rs:2124`](../ui/dr-ui/src/lib.rs#L2124), [`ui/dr-ui/src/lib.rs:301`](../ui/dr-ui/src/lib.rs#L301), [`ui/dr-ui/src/library.rs:411`](../ui/dr-ui/src/library.rs#L411), [`ui/dr-ui/src/masks_ui.rs:218`](../ui/dr-ui/src/masks_ui.rs#L218), [`ui/dr-ui/src/masks_ui.rs:41`](../ui/dr-ui/src/masks_ui.rs#L41), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/src/masks_ui.rs:930`](../ui/dr-ui/src/masks_ui.rs#L930), [`ui/dr-ui/src/segmentation.rs:219`](../ui/dr-ui/src/segmentation.rs#L219), [`ui/dr-ui/src/segmentation.rs:322`](../ui/dr-ui/src/segmentation.rs#L322), [`ui/dr-ui/src/segmentation.rs:350`](../ui/dr-ui/src/segmentation.rs#L350), [`ui/dr-ui/ui/app.slint:2105`](../ui/dr-ui/ui/app.slint#L2105), [`ui/dr-ui/ui/app.slint:984`](../ui/dr-ui/ui/app.slint#L984) | -| FR-DEV-3a | [`core/dr-pipeline/build.rs:1807`](../core/dr-pipeline/build.rs#L1807), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/descriptor.rs:117`](../core/dr-pipeline/src/descriptor.rs#L117), [`core/dr-pipeline/src/descriptor.rs:157`](../core/dr-pipeline/src/descriptor.rs#L157), [`core/dr-pipeline/src/descriptor.rs:177`](../core/dr-pipeline/src/descriptor.rs#L177), [`core/dr-pipeline/src/descriptor.rs:232`](../core/dr-pipeline/src/descriptor.rs#L232), [`core/dr-pipeline/src/framing.rs:259`](../core/dr-pipeline/src/framing.rs#L259), [`core/dr-pipeline/src/graph.rs:23`](../core/dr-pipeline/src/graph.rs#L23), [`core/dr-pipeline/src/graph.rs:250`](../core/dr-pipeline/src/graph.rs#L250), [`core/dr-pipeline/src/graph.rs:45`](../core/dr-pipeline/src/graph.rs#L45), [`core/dr-pipeline/src/graph.rs:58`](../core/dr-pipeline/src/graph.rs#L58), [`core/dr-pipeline/src/mask.rs:954`](../core/dr-pipeline/src/mask.rs#L954), [`core/dr-pipeline/src/operation.rs:328`](../core/dr-pipeline/src/operation.rs#L328), [`core/dr-pipeline/src/ops/curve.rs:318`](../core/dr-pipeline/src/ops/curve.rs#L318), [`ui/dr-ui/src/develop.rs:1177`](../ui/dr-ui/src/develop.rs#L1177), [`ui/dr-ui/src/lib.rs:595`](../ui/dr-ui/src/lib.rs#L595) | +| FR-DEV-3a | [`core/dr-pipeline/build.rs:1807`](../core/dr-pipeline/build.rs#L1807), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/descriptor.rs:117`](../core/dr-pipeline/src/descriptor.rs#L117), [`core/dr-pipeline/src/descriptor.rs:157`](../core/dr-pipeline/src/descriptor.rs#L157), [`core/dr-pipeline/src/descriptor.rs:177`](../core/dr-pipeline/src/descriptor.rs#L177), [`core/dr-pipeline/src/descriptor.rs:232`](../core/dr-pipeline/src/descriptor.rs#L232), [`core/dr-pipeline/src/framing.rs:259`](../core/dr-pipeline/src/framing.rs#L259), [`core/dr-pipeline/src/graph.rs:23`](../core/dr-pipeline/src/graph.rs#L23), [`core/dr-pipeline/src/graph.rs:250`](../core/dr-pipeline/src/graph.rs#L250), [`core/dr-pipeline/src/graph.rs:45`](../core/dr-pipeline/src/graph.rs#L45), [`core/dr-pipeline/src/graph.rs:58`](../core/dr-pipeline/src/graph.rs#L58), [`core/dr-pipeline/src/mask.rs:954`](../core/dr-pipeline/src/mask.rs#L954), [`core/dr-pipeline/src/operation.rs:328`](../core/dr-pipeline/src/operation.rs#L328), [`core/dr-pipeline/src/ops/curve.rs:318`](../core/dr-pipeline/src/ops/curve.rs#L318), [`ui/dr-ui/src/develop.rs:1177`](../ui/dr-ui/src/develop.rs#L1177), [`ui/dr-ui/src/lib.rs:595`](../ui/dr-ui/src/lib.rs#L595), [`ui/dr-ui/tests/ui_names_no_operation.rs:1`](../ui/dr-ui/tests/ui_names_no_operation.rs#L1) | | FR-DEV-3b | [`core/dr-pipeline/src/descriptor.rs:177`](../core/dr-pipeline/src/descriptor.rs#L177), [`core/dr-pipeline/src/framing.rs:259`](../core/dr-pipeline/src/framing.rs#L259), [`core/dr-pipeline/src/graph.rs:58`](../core/dr-pipeline/src/graph.rs#L58), [`core/dr-pipeline/src/operation.rs:328`](../core/dr-pipeline/src/operation.rs#L328) | | FR-DEV-3c | [`core/dr-pipeline/build.rs:1807`](../core/dr-pipeline/build.rs#L1807), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/graph.rs:250`](../core/dr-pipeline/src/graph.rs#L250), [`core/dr-pipeline/src/graph.rs:45`](../core/dr-pipeline/src/graph.rs#L45), [`core/dr-pipeline/src/mask.rs:954`](../core/dr-pipeline/src/mask.rs#L954), [`ui/dr-ui/src/develop.rs:4578`](../ui/dr-ui/src/develop.rs#L4578) | | FR-DEV-3d | [`core/dr-gpu/src/adjust.rs:1041`](../core/dr-gpu/src/adjust.rs#L1041), [`core/dr-gpu/src/adjust.rs:104`](../core/dr-gpu/src/adjust.rs#L104), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/adjust.rs:84`](../core/dr-gpu/src/adjust.rs#L84), [`core/dr-gpu/src/adjust.rs:986`](../core/dr-gpu/src/adjust.rs#L986), [`core/dr-gpu/tests/capture_sharpen.rs:434`](../core/dr-gpu/tests/capture_sharpen.rs#L434), [`core/dr-gpu/tests/detail_stage.rs:242`](../core/dr-gpu/tests/detail_stage.rs#L242), [`core/dr-gpu/tests/local_contrast.rs:476`](../core/dr-gpu/tests/local_contrast.rs#L476), [`core/dr-gpu/tests/noise_reduction.rs:556`](../core/dr-gpu/tests/noise_reduction.rs#L556), [`core/dr-pipeline/src/framing.rs:188`](../core/dr-pipeline/src/framing.rs#L188), [`core/dr-pipeline/src/graph.rs:612`](../core/dr-pipeline/src/graph.rs#L612), [`core/dr-pipeline/src/operation.rs:31`](../core/dr-pipeline/src/operation.rs#L31), [`core/dr-pipeline/src/operation.rs:352`](../core/dr-pipeline/src/operation.rs#L352), [`core/dr-pipeline/src/operation.rs:52`](../core/dr-pipeline/src/operation.rs#L52), [`core/dr-pipeline/src/operation.rs:70`](../core/dr-pipeline/src/operation.rs#L70) | @@ -84,7 +84,7 @@ _None._ | FR-EXP-9 | [`core/dr-decode/src/lib.rs:506`](../core/dr-decode/src/lib.rs#L506), [`core/dr-export/src/lib.rs:128`](../core/dr-export/src/lib.rs#L128), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-gpu/src/adjust.rs:1068`](../core/dr-gpu/src/adjust.rs#L1068), [`ui/dr-ui/src/develop.rs:2771`](../ui/dr-ui/src/develop.rs#L2771), [`ui/dr-ui/src/lib.rs:344`](../ui/dr-ui/src/lib.rs#L344) | | FR-NC-1 | [`core/dr-sync-nextcloud/src/auth.rs:132`](../core/dr-sync-nextcloud/src/auth.rs#L132), [`core/dr-sync-nextcloud/src/auth.rs:44`](../core/dr-sync-nextcloud/src/auth.rs#L44), [`core/dr-sync-nextcloud/src/session.rs:128`](../core/dr-sync-nextcloud/src/session.rs#L128), [`ui/dr-ui/src/launch.rs:256`](../ui/dr-ui/src/launch.rs#L256), [`ui/dr-ui/src/launch.rs:49`](../ui/dr-ui/src/launch.rs#L49), [`ui/dr-ui/src/launch_ui.rs:344`](../ui/dr-ui/src/launch_ui.rs#L344) | | FR-NC-10 | [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:406`](../ui/dr-ui/src/lib.rs#L406), [`ui/dr-ui/src/library.rs:1588`](../ui/dr-ui/src/library.rs#L1588), [`ui/dr-ui/src/library.rs:448`](../ui/dr-ui/src/library.rs#L448), [`ui/dr-ui/src/library.rs:750`](../ui/dr-ui/src/library.rs#L750), [`ui/dr-ui/src/library.rs:953`](../ui/dr-ui/src/library.rs#L953), [`ui/dr-ui/src/library_ui.rs:1606`](../ui/dr-ui/src/library_ui.rs#L1606), [`ui/dr-ui/src/library_ui.rs:3362`](../ui/dr-ui/src/library_ui.rs#L3362), [`ui/dr-ui/src/library_ui.rs:498`](../ui/dr-ui/src/library_ui.rs#L498), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) | -| FR-NC-12 | [`core/dr-sync-nextcloud/src/lib.rs:34`](../core/dr-sync-nextcloud/src/lib.rs#L34), [`core/dr-sync-nextcloud/src/lib.rs:892`](../core/dr-sync-nextcloud/src/lib.rs#L892), [`core/dr-sync/src/lib.rs:157`](../core/dr-sync/src/lib.rs#L157), [`core/dr-sync/src/lib.rs:40`](../core/dr-sync/src/lib.rs#L40), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1) | +| FR-NC-12 | [`core/dr-sync-nextcloud/src/lib.rs:34`](../core/dr-sync-nextcloud/src/lib.rs#L34), [`core/dr-sync-nextcloud/src/lib.rs:892`](../core/dr-sync-nextcloud/src/lib.rs#L892), [`core/dr-sync/src/lib.rs:157`](../core/dr-sync/src/lib.rs#L157), [`core/dr-sync/src/lib.rs:40`](../core/dr-sync/src/lib.rs#L40), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`ui/dr-ui/src/remote.rs:1`](../ui/dr-ui/src/remote.rs#L1) | | FR-NC-2 | [`core/dr-sync-nextcloud/src/session.rs:128`](../core/dr-sync-nextcloud/src/session.rs#L128), [`core/dr-sync-nextcloud/src/session.rs:34`](../core/dr-sync-nextcloud/src/session.rs#L34) | | FR-NC-3 | [`core/dr-decode/src/locate.rs:1`](../core/dr-decode/src/locate.rs#L1), [`core/dr-decode/src/preview.rs:148`](../core/dr-decode/src/preview.rs#L148), [`core/dr-sync/src/capability.rs:41`](../core/dr-sync/src/capability.rs#L41), [`core/dr-thumbs/src/lib.rs:1`](../core/dr-thumbs/src/lib.rs#L1), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library.rs:2604`](../ui/dr-ui/src/library.rs#L2604), [`ui/dr-ui/src/library.rs:2630`](../ui/dr-ui/src/library.rs#L2630), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1), [`ui/dr-ui/src/library_ui.rs:3624`](../ui/dr-ui/src/library_ui.rs#L3624), [`ui/dr-ui/src/library_ui.rs:4590`](../ui/dr-ui/src/library_ui.rs#L4590), [`ui/dr-ui/ui/app.slint:540`](../ui/dr-ui/ui/app.slint#L540), [`ui/dr-ui/ui/settings.slint:353`](../ui/dr-ui/ui/settings.slint#L353), [`ui/dr-ui/ui/settings.slint:72`](../ui/dr-ui/ui/settings.slint#L72) | | FR-NC-4 | [`core/dr-sync-nextcloud/src/propfind.rs:100`](../core/dr-sync-nextcloud/src/propfind.rs#L100), [`core/dr-sync-nextcloud/src/propfind.rs:51`](../core/dr-sync-nextcloud/src/propfind.rs#L51), [`core/dr-sync/src/capability.rs:6`](../core/dr-sync/src/capability.rs#L6), [`core/dr-sync/src/lib.rs:157`](../core/dr-sync/src/lib.rs#L157), [`core/dr-sync/src/scan.rs:93`](../core/dr-sync/src/scan.rs#L93), [`ui/dr-ui/src/launch.rs:49`](../ui/dr-ui/src/launch.rs#L49) | diff --git a/ui/dr-ui/tests/ui_names_no_operation.rs b/ui/dr-ui/tests/ui_names_no_operation.rs new file mode 100644 index 0000000..c7a20a1 --- /dev/null +++ b/ui/dr-ui/tests/ui_names_no_operation.rs @@ -0,0 +1,271 @@ +// TRACES: FR-DEV-3a +//! The interface must not name an operation. +//! +//! This is the property the whole declarative pipeline rests on. A node in +//! `core/dr-pipeline/ops/` is one file precisely because nothing in `ui/` has +//! to learn about it: the panel builds controls from the parameter kinds the +//! core publishes and never asks which operation it is drawing (ARCH §4.3a, +//! FR-DEV-3a). +//! +//! Until now that was maintained by discipline alone, and the failure mode is +//! silent and cumulative. Special-casing one operation to fix a layout problem +//! is defensible on its own; by the fifth the panel names half the chain and +//! "a new operation is one file" has quietly stopped being true, without any +//! single commit having broken it. +//! +//! ## What counts as naming +//! +//! An operation id appearing as a **string literal** — `"exposure"`, in Rust +//! or in Slint. That is the form a special case takes, because the id is how +//! the panel would have to recognise the operation it wanted to treat +//! differently. +//! +//! Bare words are deliberately not matched. Three ids — `texture`, `contrast` +//! and `clarity` — are also ordinary graphics and English terms, and `texture` +//! in particular appears throughout `dr-ui` meaning a GPU texture. Matching +//! those would produce a test that fails constantly for reasons unrelated to +//! the invariant, which is a test people delete. +//! +//! ## What is exempt +//! +//! Code under `#[cfg(test)]`. The invariant is about the interface that ships; +//! a unit test asserting that `labels::resolve("clarity")` yields `"Clarity"` +//! is testing the localisation table, not special-casing an operation. There is +//! exactly one such case today, in `labels.rs`. + +use std::fs; +use std::path::{Path, PathBuf}; + +/// Below this, assume the scan broke rather than that the code is clean. +/// +/// The dangerous failure of a test like this is not a false alarm, which +/// someone investigates — it is silently examining nothing and reporting +/// success. An early version cut each file at the first *textual* match of +/// `#[cfg(test)]`, which in `develop.rs` is a doc comment discussing the +/// attribute: it read 18% of the most important file in the scan and passed. +/// +/// `dr-ui` is currently 75% shipping code by line. The floor is set well under +/// that so ordinary movement does not trip it, and well over the number a +/// parsing bug produces. +const MIN_SHIPPING_FRACTION: f64 = 0.60; + +/// Every operation id the pipeline declares. +/// +/// Read from the declarations rather than listed here, so a node added +/// tomorrow is covered without anyone remembering to extend this file — the +/// same reason `traceability` parses its denominators from `requirements.md` +/// at run time. +fn operation_ids(ops_dir: &Path) -> Vec { + let mut ids = Vec::new(); + + let entries = + fs::read_dir(ops_dir).unwrap_or_else(|e| panic!("cannot read {}: {e}", ops_dir.display())); + + for entry in entries { + let path = entry.expect("read dir entry").path(); + + if path.extension().and_then(|e| e.to_str()) != Some("yaml") { + continue; + } + // `_helpers.yaml` declares the shared WGSL library, not a node, and has + // no `id:`. The leading underscore is the documented marker. + if path + .file_name() + .and_then(|n| n.to_str()) + .is_some_and(|n| n.starts_with('_')) + { + continue; + } + + let text = fs::read_to_string(&path) + .unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display())); + + let id = text + .lines() + .find_map(|l| l.strip_prefix("id:")) + .map(str::trim) + .unwrap_or_else(|| panic!("{} declares no id:", path.display())); + + ids.push(id.to_string()); + } + + assert!( + !ids.is_empty(), + "found no operation declarations in {} — the path is wrong, and a scan \ + over nothing passes for the wrong reason", + ops_dir.display() + ); + + ids +} + +/// Every `.rs` and `.slint` file under a directory. +fn sources(dir: &Path, out: &mut Vec) { + let entries = + fs::read_dir(dir).unwrap_or_else(|e| panic!("cannot read {}: {e}", dir.display())); + + for entry in entries { + let path = entry.expect("read dir entry").path(); + if path.is_dir() { + sources(&path, out); + } else if matches!( + path.extension().and_then(|e| e.to_str()), + Some("rs") | Some("slint") + ) { + out.push(path); + } + } +} + +/// Blank out string literals and line comments, so a brace inside either does +/// not move the depth count. +/// +/// Raw strings and block comments are not handled. Neither appears around a +/// `#[cfg(test)]` item in this crate, and [`MIN_SHIPPING_FRACTION`] is the +/// backstop if that ever stops being true. +fn strip_literals_and_comments(line: &str) -> String { + let mut out = String::with_capacity(line.len()); + let mut chars = line.chars().peekable(); + let mut in_string = false; + + while let Some(c) = chars.next() { + if in_string { + match c { + '\\' => { + chars.next(); + } + '"' => in_string = false, + _ => {} + } + continue; + } + match c { + '"' => in_string = true, + '/' if chars.peek() == Some(&'/') => break, + _ => out.push(c), + } + } + + out +} + +/// The lines of a file that ship, each with its 1-based number. +/// +/// For Rust, every `#[cfg(test)]` item is dropped. The attribute must be the +/// whole line — a doc comment *mentioning* it is prose, not an attribute, and +/// treating the two alike is the bug [`MIN_SHIPPING_FRACTION`] describes. +fn shipping_lines(path: &Path, text: &str) -> Vec<(usize, String)> { + let lines: Vec<&str> = text.lines().collect(); + + if path.extension().and_then(|e| e.to_str()) != Some("rs") { + return lines + .iter() + .enumerate() + .map(|(i, l)| (i + 1, (*l).to_string())) + .collect(); + } + + let mut out = Vec::new(); + let mut i = 0; + + while i < lines.len() { + if lines[i].trim() != "#[cfg(test)]" { + out.push((i + 1, lines[i].to_string())); + i += 1; + continue; + } + + // Skip the item the attribute applies to: through its brace block, or + // to the `;` if it has none — `#[cfg(test)] use ...;` is legal. + let mut j = i + 1; + let mut depth: i32 = 0; + let mut opened = false; + + while j < lines.len() { + let stripped = strip_literals_and_comments(lines[j]); + depth += stripped.matches('{').count() as i32; + depth -= stripped.matches('}').count() as i32; + + if stripped.contains('{') { + opened = true; + } + if opened && depth <= 0 { + break; + } + if !opened && stripped.contains(';') { + break; + } + j += 1; + } + + i = j + 1; + } + + out +} + +#[test] +fn the_interface_names_no_operation() { + let crate_root = Path::new(env!("CARGO_MANIFEST_DIR")); + let repo = crate_root + .parent() + .and_then(Path::parent) + .expect("ui/dr-ui has a grandparent"); + + let ids = operation_ids(&repo.join("core/dr-pipeline/ops")); + + let mut files = Vec::new(); + sources(&crate_root.join("src"), &mut files); + sources(&crate_root.join("ui"), &mut files); + + let mut offences = Vec::new(); + let mut total = 0usize; + let mut scanned = 0usize; + + for path in &files { + let text = fs::read_to_string(path) + .unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display())); + + total += text.lines().count(); + let shipping = shipping_lines(path, &text); + scanned += shipping.len(); + + for (n, line) in shipping { + for id in &ids { + if line.contains(&format!("\"{id}\"")) { + offences.push(format!( + " {}:{n} names \"{id}\"\n {}", + path.strip_prefix(repo).unwrap_or(path).display(), + line.trim() + )); + } + } + } + } + + let fraction = scanned as f64 / total as f64; + assert!( + fraction >= MIN_SHIPPING_FRACTION, + "only {scanned} of {total} lines ({:.0}%) were scanned, below the {:.0}% \ + floor — `shipping_lines` is dropping code it should be reading, and \ + this test is passing without having looked. Fix the scan before \ + trusting the result.", + fraction * 100.0, + MIN_SHIPPING_FRACTION * 100.0 + ); + + assert!( + offences.is_empty(), + "\n\nFR-DEV-3a: the interface must not name an operation.\n\n{}\n\n\ + The core declares capabilities and the frontend composes them, which is \ + why adding a develop node is one file in core/dr-pipeline/ops/ and \ + touches no UI. A special case here takes that away from every node \ + added afterwards.\n\n\ + If a node needs presentation the panel cannot currently give it, the \ + answer is a `presentation:` hint in its declaration and a `WidgetKind` \ + the panel implements for every node that asks — see \ + core/dr-pipeline/ops/README.md and `develop::supported`.\n\n\ + Test code is exempt; this scans only what ships.\n", + offences.join("\n") + ); +}