c1783e01bc
Five tests in crates/ail/tests/ct1_check_cli.rs, each pinning one named property: - check_json_emits_bare_cross_module_type_ref: --json mode surfaces `bare-cross-module-type-ref` with non-zero exit on the test_ct1_bare_xmod_rejected fixture. - check_json_emits_bad_cross_module_type_ref: same for `bad-cross-module-type-ref`. - check_json_emits_qualified_class_name: same for `qualified-class-name`. - check_human_mode_emits_actionable_message_to_stderr: human (non-JSON) mode prints both the offending type name and the migration-command hint to stderr — this path goes through the anyhow/thiserror Display impl, not workspace_error_to_diagnostic, so it needs its own pin. - check_ordering_match_post_migration_is_clean: post-migration ordering_match.ail.json typechecks via the CLI with zero diagnostics, guarding against revert of the ct.1.5 migration or a regression in Registry::normalize_type_for_lookup that would reject the canonical form.
181 lines
7.9 KiB
Rust
181 lines
7.9 KiB
Rust
//! ct.1: E2E coverage for the CLI surface of the canonical-type-names
|
|
//! validator. The unit tests in `workspace.rs` already prove the
|
|
//! validator fires; these tests prove the diagnostic survives the
|
|
//! `WorkspaceLoadError -> Diagnostic` translation in
|
|
//! `crates/ail/src/main.rs::workspace_error_to_diagnostic` AND is
|
|
//! observable on the CLI in both `--json` and human modes (exit code
|
|
//! + diagnostic code in the output stream).
|
|
//!
|
|
//! Companion to the happy-path test below: post-migration
|
|
//! `examples/ordering_match.ail.json` must `ail check` cleanly. If
|
|
//! someone reverts the migration (or breaks the canonical-form
|
|
//! acceptance path in the registry), that test goes red.
|
|
|
|
use std::path::PathBuf;
|
|
use std::process::Command;
|
|
|
|
fn ail_bin() -> PathBuf {
|
|
// CARGO_BIN_EXE_<name> is set by cargo when building integration tests.
|
|
PathBuf::from(env!("CARGO_BIN_EXE_ail"))
|
|
}
|
|
|
|
fn examples_dir() -> PathBuf {
|
|
// CARGO_MANIFEST_DIR points at `crates/ail`; examples/ sits at the
|
|
// workspace root, two levels up.
|
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
.join("..")
|
|
.join("..")
|
|
.join("examples")
|
|
}
|
|
|
|
/// Property: a workspace whose entry module contains a bare
|
|
/// cross-module Type::Con / Term::Ctor reference — here `Ordering`,
|
|
/// satisfiable only via the auto-injected `prelude` — is rejected by
|
|
/// `ail check --json` with diagnostic code `bare-cross-module-type-ref`
|
|
/// and non-zero exit. Guards against `workspace_error_to_diagnostic`
|
|
/// losing the `BareCrossModuleTypeRef` arm or the validator no longer
|
|
/// firing through the CLI loader path.
|
|
#[test]
|
|
fn check_json_emits_bare_cross_module_type_ref() {
|
|
let fixture = examples_dir().join("test_ct1_bare_xmod_rejected.ail.json");
|
|
let output = Command::new(ail_bin())
|
|
.args(["check", "--json", fixture.to_str().unwrap()])
|
|
.output()
|
|
.expect("ail binary must launch");
|
|
assert!(
|
|
!output.status.success(),
|
|
"ail check must fail on bare cross-module ref; stdout={} stderr={}",
|
|
String::from_utf8_lossy(&output.stdout),
|
|
String::from_utf8_lossy(&output.stderr),
|
|
);
|
|
let stdout = String::from_utf8(output.stdout).expect("stdout is utf-8");
|
|
let diags: serde_json::Value =
|
|
serde_json::from_str(&stdout).expect("--json mode emits a JSON array on stdout");
|
|
let arr = diags.as_array().expect("diagnostics is a JSON array");
|
|
assert!(
|
|
arr.iter().any(|d| d["code"] == "bare-cross-module-type-ref"),
|
|
"expected `bare-cross-module-type-ref` in diagnostics array; got {stdout}"
|
|
);
|
|
}
|
|
|
|
/// Property: a qualified `<owner>.<name>` Type::Con where `<owner>` is
|
|
/// not a known module is rejected by `ail check --json` with
|
|
/// diagnostic code `bad-cross-module-type-ref` and non-zero exit.
|
|
/// Guards against `workspace_error_to_diagnostic` losing the
|
|
/// `BadCrossModuleTypeRef` arm.
|
|
#[test]
|
|
fn check_json_emits_bad_cross_module_type_ref() {
|
|
let fixture = examples_dir().join("test_ct1_bad_qualifier.ail.json");
|
|
let output = Command::new(ail_bin())
|
|
.args(["check", "--json", fixture.to_str().unwrap()])
|
|
.output()
|
|
.expect("ail binary must launch");
|
|
assert!(
|
|
!output.status.success(),
|
|
"ail check must fail on unknown qualifier; stdout={} stderr={}",
|
|
String::from_utf8_lossy(&output.stdout),
|
|
String::from_utf8_lossy(&output.stderr),
|
|
);
|
|
let stdout = String::from_utf8(output.stdout).expect("stdout is utf-8");
|
|
let diags: serde_json::Value =
|
|
serde_json::from_str(&stdout).expect("--json mode emits a JSON array on stdout");
|
|
let arr = diags.as_array().expect("diagnostics is a JSON array");
|
|
assert!(
|
|
arr.iter().any(|d| d["code"] == "bad-cross-module-type-ref"),
|
|
"expected `bad-cross-module-type-ref` in diagnostics array; got {stdout}"
|
|
);
|
|
}
|
|
|
|
/// Property: a qualified class name in an `InstanceDef.class` field
|
|
/// (here `prelude.Eq`) is rejected by `ail check --json` with
|
|
/// diagnostic code `qualified-class-name` and non-zero exit. ct.1
|
|
/// reserves module qualification for type names; classes stay bare in
|
|
/// this milestone. Guards against `workspace_error_to_diagnostic`
|
|
/// losing the `QualifiedClassName` arm.
|
|
#[test]
|
|
fn check_json_emits_qualified_class_name() {
|
|
let fixture = examples_dir().join("test_ct1_qualified_class_rejected.ail.json");
|
|
let output = Command::new(ail_bin())
|
|
.args(["check", "--json", fixture.to_str().unwrap()])
|
|
.output()
|
|
.expect("ail binary must launch");
|
|
assert!(
|
|
!output.status.success(),
|
|
"ail check must fail on qualified class name; stdout={} stderr={}",
|
|
String::from_utf8_lossy(&output.stdout),
|
|
String::from_utf8_lossy(&output.stderr),
|
|
);
|
|
let stdout = String::from_utf8(output.stdout).expect("stdout is utf-8");
|
|
let diags: serde_json::Value =
|
|
serde_json::from_str(&stdout).expect("--json mode emits a JSON array on stdout");
|
|
let arr = diags.as_array().expect("diagnostics is a JSON array");
|
|
assert!(
|
|
arr.iter().any(|d| d["code"] == "qualified-class-name"),
|
|
"expected `qualified-class-name` in diagnostics array; got {stdout}"
|
|
);
|
|
}
|
|
|
|
/// Property: in non-JSON (human) mode `ail check` exits non-zero on a
|
|
/// ct.1 validator failure and writes an actionable error message to
|
|
/// stderr — naming both the offending type and the migration command
|
|
/// the author should run. Pinning the human-mode path separately
|
|
/// because in this mode the loader error short-circuits via `anyhow`
|
|
/// (no diagnostic-code prefix) and is formatted by the
|
|
/// `WorkspaceLoadError`'s `thiserror` Display impl rather than by
|
|
/// `workspace_error_to_diagnostic`. A regression that left `--json`
|
|
/// working but stripped the actionable hint from the human path
|
|
/// would otherwise ship unnoticed. The bare-cross-module fixture is
|
|
/// the representative case; the property — actionable hint in the
|
|
/// Display impl — is named per-variant.
|
|
#[test]
|
|
fn check_human_mode_emits_actionable_message_to_stderr() {
|
|
let fixture = examples_dir().join("test_ct1_bare_xmod_rejected.ail.json");
|
|
let output = Command::new(ail_bin())
|
|
.args(["check", fixture.to_str().unwrap()])
|
|
.output()
|
|
.expect("ail binary must launch");
|
|
assert!(
|
|
!output.status.success(),
|
|
"ail check (human mode) must fail on bare cross-module ref"
|
|
);
|
|
let stderr = String::from_utf8(output.stderr).expect("stderr is utf-8");
|
|
assert!(
|
|
stderr.contains("Ordering"),
|
|
"expected the offending type name in stderr; got {stderr}"
|
|
);
|
|
assert!(
|
|
stderr.contains("ail migrate-canonical-types"),
|
|
"expected the migration-command hint in stderr; got {stderr}"
|
|
);
|
|
}
|
|
|
|
/// Property: the post-migration `examples/ordering_match.ail.json`
|
|
/// (Term::Ctor `Ordering` -> `prelude.Ordering`) typechecks cleanly
|
|
/// through the CLI — `ail check` exits 0 with no diagnostics.
|
|
/// Guards against (a) a revert of the ct.1.5 migration, (b) a
|
|
/// regression in `Registry::normalize_type_for_lookup` that would make
|
|
/// the canonical (qualified) form fail to dispatch where the bare
|
|
/// form used to succeed.
|
|
#[test]
|
|
fn check_ordering_match_post_migration_is_clean() {
|
|
let fixture = examples_dir().join("ordering_match.ail.json");
|
|
let output = Command::new(ail_bin())
|
|
.args(["check", "--json", fixture.to_str().unwrap()])
|
|
.output()
|
|
.expect("ail binary must launch");
|
|
assert!(
|
|
output.status.success(),
|
|
"ail check must succeed on migrated ordering_match; stdout={} stderr={}",
|
|
String::from_utf8_lossy(&output.stdout),
|
|
String::from_utf8_lossy(&output.stderr),
|
|
);
|
|
let stdout = String::from_utf8(output.stdout).expect("stdout is utf-8");
|
|
let diags: serde_json::Value =
|
|
serde_json::from_str(&stdout).expect("--json mode emits a JSON array on stdout");
|
|
let arr = diags.as_array().expect("diagnostics is a JSON array");
|
|
assert!(
|
|
arr.is_empty(),
|
|
"expected zero diagnostics on migrated fixture; got {stdout}"
|
|
);
|
|
}
|