From 72d2d9c8061b8259301c3e2c2732333cc8faf1eb Mon Sep 17 00:00:00 2001 From: Brummel Date: Tue, 2 Jun 2026 16:59:43 +0200 Subject: [PATCH] test: prune duplicate tests, re-sight two blind coverage guards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to a fan-out audit of the whole test suite (742 tests, 21 read-only assessors + synthesis). Two clusters of the audit were actioned here; the #66 hashing-removal cohort it surfaced is left for the #66 scope, and the RED-first doc-rot sweep is deferred. §2 — true duplicates removed (each kept test is a strict superset or identical fixture+assertions of the deleted one): - ailang-check param_in_reject_message_names_allowed_set (kept the _renders_allowed_set_in_ailang_syntax superset) - ailang-check cross_module_pat_ctor_typedriven_resolves (kept ct2_; shared cross_module_ws helper retained, 2 other callers) - ailang-core mq1_qualified_instancedef_class_accepted (kept ct1_) - ail e2e bool_to_str_emits_true_branch (kept the rc-stats superset) - ail loop_recur_str adt-leg guard (the standalone heap pin owns it; dropped the dangling header reference too) - ailang-check over_strict_mode_silent_when_param_is_borrow (its assertion is subsumed by explicit_fn_with_no_uses_is_clean; its own doc claimed a "borrowing body" the Lit body never provided) §4 — meta-guards that had gone partially blind re-sighted: - spec_drift::spec_mentions_every_def_kind now pushes Class/Instance exemplars and asserts their `(class `/`(instance` anchors; the "wait for 22b.4" exclusion was stale (anchors shipped in e809f45). - schema_coverage now tracks Term::New (VariantTag + EXPECTED + visitor insert); the new_*/raw_buf_* fixtures already exercise it, so the "no fixture emits new" exclusion was stale. - prose doc_wrap_widow_control_skips_when_combined_too_long was tautological: its 32/32/42-char words gave combined=75<=80, so widow control actually fired. Rebuilt the fixture (short head + two 40-char words → combined=81>80) so it hits the real skip branch, and added the missing "last line stays 1 word" assertion. The obvious 2-word fixture does NOT work — it skips via the prev_words<2 branch, not the combined-over-budget branch. ailang-check 124->121, ailang-core 55->54, e2e 102->101, loop_recur_str 4->3; spec_drift 8 green, schema_coverage green, prose lib 63 green. --- crates/ail/tests/e2e.rs | 10 --- .../loop_recur_str_binder_no_leak_pin.rs | 26 ------ crates/ailang-check/src/lib.rs | 87 ------------------- crates/ailang-check/src/linearity.rs | 24 ----- crates/ailang-core/src/workspace.rs | 34 -------- crates/ailang-core/tests/schema_coverage.rs | 12 ++- crates/ailang-core/tests/spec_drift.rs | 26 ++++-- crates/ailang-prose/src/lib.rs | 27 ++++-- 8 files changed, 43 insertions(+), 203 deletions(-) diff --git a/crates/ail/tests/e2e.rs b/crates/ail/tests/e2e.rs index 9b80c3c..8ab55bc 100644 --- a/crates/ail/tests/e2e.rs +++ b/crates/ail/tests/e2e.rs @@ -3039,16 +3039,6 @@ fn bool_to_str_drop_balances_rc_stats() { assert_eq!(live, 0, "no leaked heap-Str slabs allowed; live={live}"); } -/// stdout-smoke for the true branch — same fixture as -/// `bool_to_str_drop_balances_rc_stats` but checked here without -/// the RC-stats overhead. Pins the byte content of `ailang_bool_to_str`'s -/// "true" slab. -#[test] -fn bool_to_str_emits_true_branch() { - let out = build_and_run("bool_to_str_drop_rc.ail"); - assert_eq!(out, "true\n"); -} - /// stdout-smoke for the false branch. Pins the byte /// content of `ailang_bool_to_str`'s "false" slab. #[test] diff --git a/crates/ail/tests/loop_recur_str_binder_no_leak_pin.rs b/crates/ail/tests/loop_recur_str_binder_no_leak_pin.rs index 7f4fa9a..1b725e7 100644 --- a/crates/ail/tests/loop_recur_str_binder_no_leak_pin.rs +++ b/crates/ail/tests/loop_recur_str_binder_no_leak_pin.rs @@ -31,10 +31,6 @@ //! `ailang_rc_stats: allocs=4 frees=1 live=3` — the three superseded Str //! slabs (seed "x" plus two intermediate concat results) leak while //! stdout is already correct (`xyyy`). -//! -//! This file also re-pins the boxed-ADT leg (`alloc_rc_adt_*`) as a -//! GREEN guard: the f488d31 fix must stay green while the Str leg is -//! closed — fixing Str must not regress the ADT case. use std::path::Path; use std::process::Command; @@ -131,28 +127,6 @@ fn alloc_rc_str_loop_binder_replaced_by_recur_does_not_leak() { ); } -/// GREEN guard: the boxed-ADT leg fixed in f488d31 must stay leak-clean -/// while the Str leg is closed. Fixing Str must not regress this. -#[test] -fn alloc_rc_adt_loop_binder_replaced_by_recur_stays_leak_clean() { - let (allocs, frees, live, stdout) = - build_run_stats("loop_recur_heap_binder_no_leak_pin.ail"); - assert_eq!( - stdout.trim_end(), - "2", - "ADT loop result mis-built (allocs={allocs} frees={frees} live={live})" - ); - assert_eq!( - live, 0, - "the f488d31 boxed-ADT recur-dec regressed: live={live} \ - (allocs={allocs} frees={frees})" - ); - assert_eq!( - allocs, frees, - "ADT leg alloc/free mismatch (allocs={allocs} frees={frees} live={live})" - ); -} - /// mir.4 soundness, recur-arg leg: a Str loop binder whose `recur` /// rebinds it to a fresh static LITERAL each iteration. The seed-only /// promotion would miss this — the recur literal "reset" must also be diff --git a/crates/ailang-check/src/lib.rs b/crates/ailang-check/src/lib.rs index fa9be8e..9535eb8 100644 --- a/crates/ailang-check/src/lib.rs +++ b/crates/ailang-check/src/lib.rs @@ -6366,48 +6366,6 @@ mod tests { assert!(diags.is_empty(), "expected green; got {diags:?}"); } - /// a bare `Pattern::Ctor` (e.g. `MkBox x`) - /// against a scrutinee of type `lib.Box` resolves through - /// the type-driven lookup keyed on `expected.name`. Previously - /// this exercised an imports-fallback; post-canonical-type-form the lookup is - /// anchored to the scrutinee's qualified TypeDef directly. - #[test] - fn cross_module_pat_ctor_typedriven_resolves() { - let consumer = Module { - schema: SCHEMA.into(), - name: "use_lib".into(), - kernel: false, - imports: vec![Import { module: "lib".into(), alias: None }], - defs: vec![fn_def( - "open", - Type::Fn { - params: vec![Type::Con { - name: "lib.Box".into(), - args: vec![Type::int()], - }], - ret: Box::new(Type::int()), - effects: vec![], - param_modes: vec![], - ret_mode: ParamMode::Own, - }, - vec!["b"], - Term::Match { - scrutinee: Box::new(Term::Var { name: "b".into() }), - arms: vec![Arm { - pat: Pattern::Ctor { - ctor: "MkBox".into(), - fields: vec![Pattern::Var { name: "x".into() }], - }, - body: Term::Var { name: "x".into() }, - }], - }, - )], - }; - let ws = cross_module_ws(consumer); - let diags = check_workspace(&ws); - assert!(diags.is_empty(), "expected green; got {diags:?}"); - } - /// a recursive cross-module ADT (`std_list.List a` with a /// `Cons a (List a)` ctor) round-trips through both `Term::Ctor` synth /// and pattern-ctor binding without unqualified-field-name unification @@ -8527,51 +8485,6 @@ mod tests { ); } - /// fieldtest B3 (raw-buf, #7): the rendered `param-not-in-restricted-set` - /// message must name the *allowed set*, not only the offending type. - /// Property per design/models/0007-kernel-extensions.md §4 - /// ("ParamNotInRestrictedSet names the offending type AND the allowed - /// set"): a downstream author rejected for `(con StubT (con Str))` - /// under `param-in = {"a": {"Int", "Float"}}` must be told `Int` and - /// `Float` are the permitted choices, so the message is actionable. - #[test] - fn param_in_reject_message_names_allowed_set() { - let m: Module = serde_json::from_value(serde_json::json!({ - "schema": SCHEMA, - "name": "k", - "defs": [ - {"kind": "type", "name": "StubT", "vars": ["a"], - "ctors": [{"name": "Stub", "fields": [{"k": "var", "name": "a"}]}], - "param-in": {"a": ["Int", "Float"]}}, - {"kind": "fn", "name": "bad", - "type": {"k": "fn", "params": [], "param_modes": [], - "ret": {"k": "con", "name": "StubT", - "args": [{"k": "con", "name": "Str"}]}, "ret_mode": "own", - "effects": []}, - "params": [], - "body": {"t": "lit", "lit": {"kind": "unit"}}} - ] - })).unwrap(); - let mut modules = BTreeMap::new(); - modules.insert("k".to_string(), m); - let ws = Workspace { - entry: "k".into(), - modules, - root_dir: std::path::PathBuf::from("."), - registry: ailang_core::workspace::Registry::default(), - }; - let diags = check_workspace(&ws); - let d = diags - .iter() - .find(|d| d.code == "param-not-in-restricted-set") - .unwrap_or_else(|| panic!("expected param-not-in-restricted-set, got {diags:?}")); - assert!( - d.message.contains("Int") && d.message.contains("Float"), - "message must name the allowed set {{Int, Float}}, got: {:?}", - d.message - ); - } - /// #52: the allowed set in `param-not-in-restricted-set` must render in /// AILang type syntax — a brace-wrapped, comma-separated set of bare /// type names (`{Float, Int}`) — not a Rust debug list diff --git a/crates/ailang-check/src/linearity.rs b/crates/ailang-check/src/linearity.rs index e642637..b5f3948 100644 --- a/crates/ailang-check/src/linearity.rs +++ b/crates/ailang-check/src/linearity.rs @@ -2104,30 +2104,6 @@ mod tests { ); } - /// a fn with `(borrow T)` and a borrowing - /// body never fires `over-strict-mode` (the lint is gated on - /// `Own`). - #[test] - fn over_strict_mode_silent_when_param_is_borrow() { - // Body shape: literal — no use of the param at all. - let m = Module { - schema: ailang_core::SCHEMA.into(), - name: "t".into(), - kernel: false, - imports: vec![], - defs: vec![fn_with_modes( - "f", - vec![ParamMode::Borrow], - Term::Lit { lit: Literal::Int { value: 0 } }, - )], - }; - let diags = check_module(&m); - assert!( - !diags.iter().any(|d| d.code == "over-strict-mode"), - "no over-strict-mode expected for borrow-mode param; got {diags:?}" - ); - } - /// a fn with `(own T)` whose body is /// `(match p0 ...)` and never consumes `p0` directly nor any of /// `p0`'s sub-binders DOES fire `over-strict-mode` under the diff --git a/crates/ailang-core/src/workspace.rs b/crates/ailang-core/src/workspace.rs index b121ad9..d1f128b 100644 --- a/crates/ailang-core/src/workspace.rs +++ b/crates/ailang-core/src/workspace.rs @@ -2638,40 +2638,6 @@ mod tests { } } - /// a qualified `InstanceDef.class` referencing a class in - /// an imported module is the canonical form post-canonical-class-form and is - /// accepted by `validate_canonical_type_names`. Symmetric to the canonical-form rule's - /// "qualified Type::Con resolves" positive path. - #[test] - fn mq1_qualified_instancedef_class_accepted() { - let a: Module = serde_json::from_value(serde_json::json!({ - "schema": crate::SCHEMA, - "name": "A", - "imports": [], - "defs": [{ - "kind": "class", - "name": "Show", - "param": "a", - "methods": [] - }] - })).unwrap(); - let b: Module = serde_json::from_value(serde_json::json!({ - "schema": crate::SCHEMA, - "name": "B", - "imports": [{ "module": "A" }], - "defs": [{ - "kind": "instance", - "class": "A.Show", - "type": { "k": "con", "name": "Int" }, - "methods": [] - }] - })).unwrap(); - let mut modules = BTreeMap::new(); - modules.insert("A".to_string(), a); - modules.insert("B".to_string(), b); - validate_canonical_type_names(&modules, &["prelude"]).expect("qualified A.Show must be accepted"); - } - /// pd.1 Task 1: `load_modules_with` returns modules without injecting /// prelude. The prelude inject is the caller's responsibility (the /// shim `load_workspace_with` does it in pd.1; surface owns it from diff --git a/crates/ailang-core/tests/schema_coverage.rs b/crates/ailang-core/tests/schema_coverage.rs index 92ccd05..584705d 100644 --- a/crates/ailang-core/tests/schema_coverage.rs +++ b/crates/ailang-core/tests/schema_coverage.rs @@ -50,6 +50,7 @@ enum VariantTag { TermReuseAs, TermLoop, TermRecur, + TermNew, TermIntrinsic, // Pattern PatternWild, @@ -97,6 +98,7 @@ const EXPECTED_VARIANTS: &[VariantTag] = &[ VariantTag::TermReuseAs, VariantTag::TermLoop, VariantTag::TermRecur, + VariantTag::TermNew, VariantTag::TermIntrinsic, VariantTag::PatternWild, VariantTag::PatternVar, @@ -248,13 +250,9 @@ fn visit_term(t: &Term, observed: &mut HashSet) { } } Term::New { args, .. } => { - // prep.2 (kernel-extension-mechanics): functional construction. - // The variant is intentionally NOT added to `VariantTag` / - // `EXPECTED_VARIANTS` in prep.2 — no .ail fixture in the - // current corpus emits `"t": "new"` (per the iter's - // out-of-scope note), so an EXPECTED entry would fail the - // coverage check. A future milestone that ships a Term::New - // fixture extends both the enum and the expected list. + // Functional construction. Exercised by the `new_*` / + // `raw_buf_*` fixtures under examples/. + observed.insert(VariantTag::TermNew); for arg in args { match arg { NewArg::Type(t) => visit_type(t, observed), diff --git a/crates/ailang-core/tests/spec_drift.rs b/crates/ailang-core/tests/spec_drift.rs index fb700ab..1ab2392 100644 --- a/crates/ailang-core/tests/spec_drift.rs +++ b/crates/ailang-core/tests/spec_drift.rs @@ -14,7 +14,8 @@ use std::collections::BTreeMap; use ailang_core::ast::{ - ConstDef, Ctor, Def, FnDef, Literal, NewArg, Pattern, Suppress, Term, Type, TypeDef, + ClassDef, ConstDef, Ctor, Def, FnDef, InstanceDef, Literal, NewArg, Pattern, Suppress, Term, + Type, TypeDef, }; use ailang_core::FORM_A_SPEC; @@ -293,10 +294,27 @@ fn spec_mentions_every_def_kind() { drop_iterative: false, param_in: BTreeMap::new(), }; + let class_def = ClassDef { + name: "C".into(), + param: "a".into(), + superclass: None, + methods: vec![], + doc: None, + }; + let instance_def = InstanceDef { + class: "C".into(), + type_: Type::int(), + methods: vec![], + doc: None, + }; let exemplars: Vec<(&str, Def)> = vec![ ("(fn ", Def::Fn(fn_def)), ("(const ", Def::Const(const_def)), ("(data ", Def::Type(type_def)), + // class/instance gained prose projection in e809f45 (form-a.tidy): + // FORM_A_SPEC now carries `(class ` / `(instance ` anchors. + ("(class ", Def::Class(class_def)), + ("(instance", Def::Instance(instance_def)), ]; for (anchor, def) in exemplars { @@ -304,12 +322,6 @@ fn spec_mentions_every_def_kind() { Def::Fn(_) => "fn", Def::Const(_) => "const", Def::Type(_) => "type", - // class/instance Def variants exist but are - // not yet anchored in the prose-spec block. Once 22b.4 - // adds prose projection for them, the FORM_A_SPEC text - // gains `(class ` / `(instance ` anchors and this match - // will be exercised. For 22b.1 the exemplars list above - // does not produce these variants. Def::Class(_) => "class", Def::Instance(_) => "instance", }; diff --git a/crates/ailang-prose/src/lib.rs b/crates/ailang-prose/src/lib.rs index 64715f4..11397c1 100644 --- a/crates/ailang-prose/src/lib.rs +++ b/crates/ailang-prose/src/lib.rs @@ -2238,20 +2238,31 @@ mod tests { #[test] fn doc_wrap_widow_control_skips_when_combined_too_long() { - // If the combined length of (pulled + " " + orphan) would - // exceed budget, widow control must NOT fire — preserving - // the wrap budget invariant takes priority over cosmetics. - // Construct a case: prev-tail = a long word, orphan = a long - // word — combined > 80. - let pieces = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb \ - cccccccccccccccccccccccccccccccccccccccccc"; - let lines = wrap_words(pieces, 80); + // Widow control pulls the penultimate line's tail word down to + // join a 1-word orphan — but only when (pulled + " " + orphan) + // still fits the budget. When it does NOT fit, the budget + // invariant wins and the orphan stays a 1-word last line. + // + // Construct that case at budget 80: a short head word forces the + // penultimate line to hold two words ("tail" + 40×'b'), and the + // orphan is a 40×'c' word. Greedy wrap yields + // ["tail bbb…"(45), "ccc…"(40)]; the merge candidate is + // pulled(40) + 1 + orphan(40) = 81 > 80, so widow control must + // skip and the last line keeps its single word. + let pieces = format!("tail {} {}", "b".repeat(40), "c".repeat(40)); + let lines = wrap_words(&pieces, 80); // Sanity: the wrap produced multiple lines. assert!(lines.len() >= 2); // No line exceeds budget. for l in &lines { assert!(l.len() <= 80, "line over budget: {l:?}"); } + // Widow control did NOT fire: the orphan stays a 1-word last line. + assert_eq!( + lines.last().unwrap().split_whitespace().count(), + 1, + "widow control should have skipped (merge over budget); got {lines:?}" + ); } // ---- Polish 5: nested match across lines ----