test: prune duplicate tests, re-sight two blind coverage guards
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.
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Int>` 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<VariantTag>) {
|
||||
}
|
||||
}
|
||||
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),
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
@@ -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 ----
|
||||
|
||||
Reference in New Issue
Block a user