Files
AILang/crates/ail/tests/embed_rc_accounting_tsan.rs
T
Brummel 7ae92d3e60 refactor(test): hoist duplicated test helpers into ailang-test-support
closes #60

The fixture-corpus filter (NON_PARSEABLE_FIXTURES + list_ail_fixtures)
was copy-pasted across three test crates and drifted out of sync as
new reject fixtures landed; the examples_dir / workspace_root path
walk was reimplemented ~30 more times across the suite, under two
spellings (workspace_root / ws_root).

Introduce crates/ailang-test-support — a zero-dependency, dev-only
leaf crate — as the single home for these helpers:
- NON_PARSEABLE_FIXTURES, list_ail_fixtures
- workspace_root, examples_dir
- canonical_workspace_root (symlink-resolved variant, for the tsan/
  race tests that feed the root into native build steps)

All integration-test copies across ailang-core, ailang-surface,
ailang-prose, ailang-check, and ail now import from the shared crate;
the local definitions and their now-orphaned path imports are removed.
Path helpers resolve via the support crate's own CARGO_MANIFEST_DIR,
so they return the correct absolute paths from any caller.

ail_bin helpers are intentionally left in place: they depend on
env!("CARGO_BIN_EXE_ail"), which Cargo only defines when compiling the
ail crate's own integration tests, so they cannot move to a shared
crate.

Behaviour-identical: same paths, same fixture lists; full workspace
test suite green. Net -177 LOC.
2026-06-02 01:54:37 +02:00

75 lines
3.6 KiB
Rust

//! M2 item 1: direct rc-accounting de-globalisation proof. Compiles
//! runtime/rc.c + runtime/str.c into libailang_rt.a, links the
//! rc_accounting_tsan.c harness against it under -fsanitize=thread.
//! Per-thread-ctx build: tsan-clean + exit 0. RED until rc.c gains
//! ailang_ctx_* + __ail_tls_ctx (link error: undefined symbols).
use std::process::Command;
fn cc(args: &[&str], ctx: &str) {
let st = Command::new("clang").args(args).status().expect("spawn clang");
assert!(st.success(), "clang failed: {ctx}");
}
#[test]
fn rc_accounting_per_ctx_is_tsan_clean() {
let root = ailang_test_support::canonical_workspace_root();
let tmp = std::env::temp_dir().join("ail_m2_rcacct");
std::fs::create_dir_all(&tmp).unwrap();
let rc_o = tmp.join("rc.o");
let str_o = tmp.join("str.o");
let rt_a = tmp.join("libailang_rt.a");
cc(&["-O1", "-g", "-fsanitize=thread", "-c", "-o", rc_o.to_str().unwrap(),
root.join("runtime/rc.c").to_str().unwrap()], "rc.c");
cc(&["-O1", "-g", "-fsanitize=thread", "-c", "-o", str_o.to_str().unwrap(),
root.join("runtime/str.c").to_str().unwrap()], "str.c");
let st = Command::new("ar").args(["rcs", rt_a.to_str().unwrap(),
rc_o.to_str().unwrap(), str_o.to_str().unwrap()]).status().unwrap();
assert!(st.success(), "ar failed");
let bin = tmp.join("rcacct");
cc(&["-O1", "-g", "-fsanitize=thread", "-pthread",
"-o", bin.to_str().unwrap(),
root.join("crates/ail/tests/embed/rc_accounting_tsan.c").to_str().unwrap(),
rt_a.to_str().unwrap()], "link rc_accounting_tsan");
let out = Command::new(&bin).env("TSAN_OPTIONS", "halt_on_error=1")
.output().expect("run harness");
assert!(out.status.success(),
"per-ctx rc-accounting harness must exit 0 tsan-clean; stderr:\n{}",
String::from_utf8_lossy(&out.stderr));
}
#[test]
fn rc_accounting_shared_ctx_is_flagged_by_tsan() {
// Negative control: all N threads share one ailang_ctx_t* while
// calling ailang_rc_alloc/ailang_rc_dec → concurrent
// ctx->alloc_count++ is a genuine data race tsan MUST report.
// Proves the de-globalisation property is not vacuous.
let root = ailang_test_support::canonical_workspace_root();
let tmp = std::env::temp_dir().join("ail_m2_rcacct_neg");
std::fs::create_dir_all(&tmp).unwrap();
let rc_o = tmp.join("rc.o");
let str_o = tmp.join("str.o");
let rt_a = tmp.join("libailang_rt.a");
cc(&["-O1", "-g", "-fsanitize=thread", "-c", "-o", rc_o.to_str().unwrap(),
root.join("runtime/rc.c").to_str().unwrap()], "rc.c");
cc(&["-O1", "-g", "-fsanitize=thread", "-c", "-o", str_o.to_str().unwrap(),
root.join("runtime/str.c").to_str().unwrap()], "str.c");
let st = Command::new("ar").args(["rcs", rt_a.to_str().unwrap(),
rc_o.to_str().unwrap(), str_o.to_str().unwrap()]).status().unwrap();
assert!(st.success(), "ar failed");
let bin = tmp.join("rcacct_neg");
cc(&["-O1", "-g", "-fsanitize=thread", "-pthread", "-DSHARED_CTX",
"-o", bin.to_str().unwrap(),
root.join("crates/ail/tests/embed/rc_accounting_tsan.c").to_str().unwrap(),
rt_a.to_str().unwrap()], "link rc_accounting_tsan -DSHARED_CTX");
let out = Command::new(&bin).env("TSAN_OPTIONS", "halt_on_error=1")
.output().expect("run rcacct neg");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!out.status.success() || stderr.contains("WARNING: ThreadSanitizer"),
"shared-ctx rc-accounting negative control MUST be tsan-flagged \
(the de-globalisation teeth); exit={:?} stderr:\n{stderr}",
out.status.code()
);
}