`cargo fmt --check` is a required step and had drifted across 45 files. Most of it arrived this week: several operations were written in parallel worktrees and merged by hand, and a hand-merge resolves conflicts without ever running the formatter over the result. No behaviour changes — this is `cargo fmt --all` and nothing else, kept as its own commit so the next reader can skip it wholesale rather than search it for one that matters. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
124 lines
4.1 KiB
Rust
124 lines
4.1 KiB
Rust
//! What operations say they are about (ARCH §4.3a).
|
|
//!
|
|
//! The point of an attribute is that a panel can group operations without
|
|
//! knowing what any of them is. These assert the properties that makes
|
|
//! possible, over the real chain rather than over fixtures — including the
|
|
//! hand-written operations, which `build.rs` never sees and so cannot check.
|
|
|
|
use dr_pipeline::{Attribute, EditGraph};
|
|
|
|
/// The invariant the whole scheme rests on.
|
|
///
|
|
/// An operation with no attribute is invisible to a panel that groups by them.
|
|
/// `build.rs` refuses to *generate* one; this covers the ones it does not
|
|
/// generate.
|
|
#[test]
|
|
fn every_operation_says_what_it_is_about() {
|
|
let graph = EditGraph::default_chain();
|
|
for cap in graph.capabilities() {
|
|
assert!(
|
|
!cap.attributes.is_empty(),
|
|
"{} declares no attribute, so no panel grouping by attribute \
|
|
would ever show it",
|
|
cap.id.0
|
|
);
|
|
}
|
|
}
|
|
|
|
/// A panel can build its groups from the chain alone, with no table mapping
|
|
/// operations to groups — which is what lets `ui/` name no operation
|
|
/// (FR-DEV-3a).
|
|
#[test]
|
|
fn the_groups_are_derivable_from_the_chain() {
|
|
let graph = EditGraph::default_chain();
|
|
let caps = graph.capabilities();
|
|
|
|
let mut present: Vec<Attribute> = caps
|
|
.iter()
|
|
.flat_map(|c| c.attributes.iter().copied())
|
|
.collect();
|
|
present.sort();
|
|
present.dedup();
|
|
|
|
assert!(
|
|
present.contains(&Attribute::Tone),
|
|
"the chain has tonal work"
|
|
);
|
|
assert!(present.contains(&Attribute::Colour));
|
|
assert!(
|
|
present.contains(&Attribute::Geometry),
|
|
"framing is in the capability list and is geometry"
|
|
);
|
|
|
|
// And nothing derived is empty: a group with no operations in it would be
|
|
// a tab that opens onto nothing.
|
|
for a in &present {
|
|
assert!(
|
|
caps.iter().any(|c| c.attributes.contains(a)),
|
|
"{a:?} appeared with nothing in it"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// The case the plural exists for.
|
|
#[test]
|
|
fn an_operation_may_be_about_two_things() {
|
|
let graph = EditGraph::default_chain();
|
|
let curve = graph
|
|
.capabilities()
|
|
.into_iter()
|
|
.find(|c| c.id.0 == "tone_curve")
|
|
.expect("the chain has a tone curve");
|
|
|
|
assert!(curve.attributes.contains(&Attribute::Tone));
|
|
assert!(
|
|
curve.attributes.contains(&Attribute::Colour),
|
|
"the per-channel curves are chromatic; filing it under tone alone \
|
|
would hide it from half the people looking for it"
|
|
);
|
|
}
|
|
|
|
/// Attributes describe the operation, never the screen — so a frontend that
|
|
/// groups by them can still reach every parameter, and one that ignores them
|
|
/// loses nothing.
|
|
#[test]
|
|
fn grouping_reaches_every_parameter() {
|
|
let graph = EditGraph::default_chain();
|
|
let caps = graph.capabilities();
|
|
|
|
let ungrouped: usize = caps.iter().map(|c| c.params.len()).sum();
|
|
let reachable: usize = caps
|
|
.iter()
|
|
.filter(|c| Attribute::ALL.iter().any(|a| c.attributes.contains(a)))
|
|
.map(|c| c.params.len())
|
|
.sum();
|
|
|
|
assert_eq!(
|
|
reachable, ungrouped,
|
|
"a panel showing every attribute must show every parameter"
|
|
);
|
|
}
|
|
|
|
/// The vocabulary is closed, so a typo cannot invent a category holding one
|
|
/// operation — which is indistinguishable from a deliberate new one until
|
|
/// somebody notices the tab with a single control in it.
|
|
#[test]
|
|
fn the_vocabulary_is_closed() {
|
|
assert_eq!(Attribute::from_name("tone"), Some(Attribute::Tone));
|
|
assert_eq!(Attribute::from_name("colour"), Some(Attribute::Colour));
|
|
assert_eq!(Attribute::from_name("Tone"), None, "names are lower case");
|
|
assert_eq!(Attribute::from_name("color"), None, "and are spelled once");
|
|
assert_eq!(Attribute::from_name("tonal"), None);
|
|
}
|
|
|
|
/// Every attribute names a concept the localiser can resolve, the way an
|
|
/// operation's own label does.
|
|
#[test]
|
|
fn every_attribute_is_nameable() {
|
|
for a in Attribute::ALL {
|
|
let key = a.label().0;
|
|
assert!(key.starts_with("attr."), "{a:?} has key {key:?}");
|
|
assert!(key.len() > "attr.".len());
|
|
}
|
|
}
|