// 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") ); }