//! End-to-end scenario tests for `cstat extract` against the §13 fixture //! and the unparseable-file contract fixture. //! //! These observe the JSON envelope only — they should survive any refactor //! that preserves the spec §7 contract. use std::collections::BTreeSet; use std::path::{Path, PathBuf}; use std::process::Command; use std::sync::OnceLock; use cstat_schema::{LevelStatus, OracleOutput}; fn workspace_root() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("..") .join("..") .canonicalize() .expect("workspace root resolves") } fn run_extract(project: &Path) -> OracleOutput { run_extract_with(project, &[]) } fn run_extract_with(project: &Path, extra_args: &[&str]) -> OracleOutput { let bin = env!("CARGO_BIN_EXE_cstat"); let mut cmd = Command::new(bin); cmd.args(["extract", "--path"]).arg(project); cmd.args(extra_args); let output = cmd.output().expect("invoke cstat binary"); assert!( output.status.success(), "cstat extract on {project:?} failed: status={:?} stderr={}", output.status, String::from_utf8_lossy(&output.stderr) ); serde_json::from_slice(&output.stdout).expect("stdout parses as OracleOutput envelope") } /// Default (codegen-disabled) extract on the §13 fixture. Cached so the /// many scenario tests that share this baseline don't each pay the /// cost of the external-tool stages (clippy, rustdoc, cargo test). fn run_extract_on_fixture() -> OracleOutput { static CACHE: OnceLock = OnceLock::new(); CACHE .get_or_init(|| { let fixture = workspace_root().join("examples").join("single-file-lib"); assert!(fixture.is_dir(), "fixture missing: {fixture:?}"); run_extract(&fixture) }) .clone() } /// Shared codegen-enabled extract. Cached so the tests don't trigger /// repeated cargo builds (each ~3s) or contend on the fixture's /// `target/cstat-*` lock when run in parallel. fn run_extract_on_fixture_with_codegen() -> &'static OracleOutput { static CACHE: OnceLock = OnceLock::new(); CACHE.get_or_init(|| { let fixture = workspace_root().join("examples").join("single-file-lib"); assert!(fixture.is_dir(), "fixture missing: {fixture:?}"); run_extract_with(&fixture, &["--enable-codegen"]) }) } fn run_extract_on_broken_fixture() -> OracleOutput { let fixture = workspace_root().join("examples").join("broken-file"); assert!(fixture.is_dir(), "fixture missing: {fixture:?}"); run_extract(&fixture) } /// Shared extract on the `with-impls` fixture (lib with traits, an /// inherent impl, two trait-impls, and a `Display` impl exercising the /// orphan-rule quadrants). Cached so we don't shell out repeatedly. fn run_extract_on_with_impls_fixture() -> &'static OracleOutput { static CACHE: OnceLock = OnceLock::new(); CACHE.get_or_init(|| { let fixture = workspace_root().join("examples").join("with-impls"); assert!(fixture.is_dir(), "fixture missing: {fixture:?}"); run_extract(&fixture) }) } /// Shared extract on the `with-graphs` fixture (lib with three modules /// and a small intra-crate call graph), used by the Stage 9 graph tests. /// Cached so we don't shell out repeatedly. fn run_extract_on_with_graphs_fixture() -> &'static OracleOutput { static CACHE: OnceLock = OnceLock::new(); CACHE.get_or_init(|| { let fixture = workspace_root().join("examples").join("with-graphs"); assert!(fixture.is_dir(), "fixture missing: {fixture:?}"); run_extract(&fixture) }) } /// Stage-3 levels (spec §2.1 1–6 plus §2.9 #47) — always `ok` on the fixture. const STAGE3_OK_LEVELS: &[&str] = &[ "fs.layout", "fs.bytes", "fs.source", "tok.stream", "tok.comments", "fmt.whitespace", "rustfmt", ]; /// Stage-4 levels that always report `ok` on a well-formed project. /// Level 9 (`ast.expanded`) is conditional on cargo-expand and exercised /// separately. const STAGE4_OK_LEVELS: &[&str] = &["ast.cst", "ast.items"]; /// Stage-5 levels — spec §2.8 41–44. `cargo.lock` is conditional on a /// generated `Cargo.lock` (which the fixture commits, so it's `ok` there). const STAGE5_OK_LEVELS: &[&str] = &[ "cargo.workspace", "cargo.lock", "cargo.cfg", "cargo.toolchain", ]; #[test] fn every_required_level_is_present_with_a_documented_status() { let envelope = run_extract_on_fixture(); let required_ids: Vec<&str> = cstat_orchestrate::REQUIRED_LEVELS .iter() .map(|d| d.id) .collect(); assert_eq!( envelope.levels.len(), required_ids.len(), "envelope levels count differs from required-levels table" ); for id in required_ids { let report = envelope .levels .get(id) .unwrap_or_else(|| panic!("level {id} missing from envelope")); match report.status { LevelStatus::Ok => {} LevelStatus::Skipped | LevelStatus::Failed => { assert!( report.reason.as_ref().is_some_and(|r| !r.is_empty()), "non-ok level {id} has empty reason" ); } } } } #[test] fn envelope_carries_a_populated_toolchain_stamp_and_schema_version() { let envelope = run_extract_on_fixture(); assert!(!envelope.schema_version.is_empty()); assert!(!envelope.cstat_version.is_empty()); assert!(!envelope.toolchain.version.is_empty(), "toolchain version"); assert!(!envelope.toolchain.channel.is_empty(), "toolchain channel"); } #[test] fn meta_levels_missing_count_matches_non_ok_level_count() { let envelope = run_extract_on_fixture(); let observed_non_ok = envelope .levels .values() .filter(|r| r.status != LevelStatus::Ok) .count(); let missing = envelope .vector .get("meta.levels_missing.count") .expect("meta.levels_missing.count present"); assert_eq!( missing as usize, observed_non_ok, "meta counter must agree with the levels map" ); } #[test] fn stage_three_levels_report_ok_on_the_fixture() { let envelope = run_extract_on_fixture(); for id in STAGE3_OK_LEVELS { let report = envelope .levels .get(*id) .unwrap_or_else(|| panic!("level {id} missing")); assert_eq!( report.status, LevelStatus::Ok, "stage-3 level {id} expected ok but got {:?} (reason: {:?})", report.status, report.reason ); } } #[test] fn fixture_vector_contains_the_completion_gate_metrics() { let envelope = run_extract_on_fixture(); assert_eq!( envelope.vector.get("fs.global.rust_file_count"), Some(1.0), "fixture has one .rs file" ); let total_bytes = envelope .vector .get("fs.global.total_bytes") .expect("fs.global.total_bytes populated"); assert!(total_bytes > 0.0, "total_bytes must reflect the .rs payload"); for key in [ "fs.layout.file_size_bytes.max", "fs.layout.file_size_bytes.mean", ] { let v = envelope .vector .get(key) .unwrap_or_else(|| panic!("{key} missing")); assert!(v >= 0.0, "{key} must be non-negative"); } let blank_ratio = envelope .vector .get("fs.source.blank_line_ratio.mean") .expect("blank_line_ratio.mean populated"); assert!( (0.0..=1.0).contains(&blank_ratio), "blank_line_ratio must be a fraction, got {blank_ratio}" ); let unique_idents = envelope .vector .get("tok.global.unique_identifiers") .expect("tok.global.unique_identifiers populated"); assert!( unique_idents >= 5.0, "fixture should have several distinct identifiers, got {unique_idents}" ); assert_eq!(envelope.vector.get("tok.comments.todo_count"), Some(0.0)); assert_eq!(envelope.vector.get("tok.comments.fixme_count"), Some(0.0)); let conformant = envelope .vector .get("rustfmt.conformant") .expect("rustfmt.conformant populated"); assert!( conformant == 0.0 || conformant == 1.0, "rustfmt.conformant is a 0/1 flag, got {conformant}" ); } #[test] fn stage_four_ast_levels_report_ok_on_the_fixture() { let envelope = run_extract_on_fixture(); for id in STAGE4_OK_LEVELS { let report = envelope .levels .get(*id) .unwrap_or_else(|| panic!("level {id} missing")); assert_eq!( report.status, LevelStatus::Ok, "stage-4 level {id} expected ok but got {:?} (reason: {:?})", report.status, report.reason ); } } #[test] fn ast_expanded_is_ok_or_skipped_with_a_reason() { let envelope = run_extract_on_fixture(); let report = envelope.levels.get("ast.expanded").expect("ast.expanded present"); match report.status { LevelStatus::Ok => { assert!( envelope.vector.contains_key("ast.expanded.token_count"), "ok ast.expanded must populate its vector keys" ); } LevelStatus::Skipped => { assert!( report.reason.as_ref().is_some_and(|r| !r.is_empty()), "skipped ast.expanded must explain itself" ); assert!( !envelope.vector.contains_key("ast.expanded.token_count"), "skipped levels must omit their vector keys (spec §8)" ); } LevelStatus::Failed => panic!( "ast.expanded failed on a well-formed fixture (reason: {:?})", report.reason ), } } #[test] fn fixture_ast_metrics_reflect_the_two_pub_functions() { let envelope = run_extract_on_fixture(); // §13: two top-level `pub fn`s. The exact value is the §13 anchor. assert_eq!( envelope.vector.get("ast.global.func_count"), Some(2.0), "fixture has exactly two functions" ); let parsed = envelope .vector .get("ast.cst.parsed_file_count") .expect("ast.cst.parsed_file_count populated"); assert_eq!(parsed, 1.0, "single-file fixture parses one file"); let pub_ratio = envelope .vector .get("ast.items.pub_ratio") .expect("ast.items.pub_ratio populated"); assert!( (0.0..=1.0).contains(&pub_ratio) && pub_ratio > 0.0, "fixture's items are all `pub`, ratio must be > 0" ); // The crate-root module must show up under entities.ast.module with the // function count it owns. let modules = envelope .entities .get("ast.module") .expect("entities.ast.module populated"); assert!( modules .iter() .any(|r| r.metrics.get("func_count").copied() == Some(2.0)), "expected a module with two functions; got {modules:?}" ); } #[test] fn meta_files_unparseable_count_is_zero_on_the_fixture() { let envelope = run_extract_on_fixture(); assert_eq!( envelope.vector.get("meta.files_unparseable.count"), Some(0.0), "well-formed fixture has no parse errors" ); assert!( envelope.entities.get("ast.file_errors").map_or(true, |v| v.is_empty()), "well-formed fixture must not produce file_errors entries" ); } #[test] fn unparseable_file_degrades_gracefully_per_spec_section_11() { // Anchor test 6 (§11 row 4): one well-formed file + one broken file. let envelope = run_extract_on_broken_fixture(); // The process exits 0 — already asserted by run_extract on success. // AST levels still report ok despite one file failing to parse. for id in ["ast.cst", "ast.items"] { let report = envelope.levels.get(id).expect("ast level present"); assert_eq!( report.status, LevelStatus::Ok, "{id} must stay ok in the face of one bad file (reason: {:?})", report.reason ); } // The broken file shows up under entities.ast.file_errors. let errors = envelope .entities .get("ast.file_errors") .expect("entities.ast.file_errors populated"); assert_eq!( errors.len(), 1, "exactly one unparseable file expected, got {}", errors.len() ); let bad = &errors[0]; let file_id = bad .ids .file .as_ref() .expect("file_errors entry carries a FileId"); assert!( file_id.as_str().ends_with("bad.rs"), "file_errors should identify bad.rs, got {file_id:?}" ); // meta counter agrees with the entity record count. assert_eq!( envelope.vector.get("meta.files_unparseable.count"), Some(1.0), "meta.files_unparseable.count must reflect entities.ast.file_errors" ); // The good file's metrics still land: the surviving `pub fn good` shows up. assert_eq!( envelope.vector.get("ast.global.func_count"), Some(1.0), "good.rs's single function must still be counted" ); assert_eq!( envelope.vector.get("ast.cst.parsed_file_count"), Some(1.0), "exactly one .rs file parsed successfully" ); assert_eq!( envelope.vector.get("ast.cst.unparseable_file_count"), Some(1.0), "exactly one .rs file failed to parse" ); } #[test] fn stage_five_cargo_metadata_levels_report_ok_on_the_fixture() { let envelope = run_extract_on_fixture(); for id in STAGE5_OK_LEVELS { let report = envelope .levels .get(*id) .unwrap_or_else(|| panic!("level {id} missing")); assert_eq!( report.status, LevelStatus::Ok, "stage-5 level {id} expected ok but got {:?} (reason: {:?})", report.status, report.reason ); } } #[test] fn fixture_cargo_metadata_reflects_a_single_crate_with_no_deps() { let envelope = run_extract_on_fixture(); // §13 fixture is a single package with no dependencies. assert_eq!( envelope.vector.get("cargo.workspace.member_count"), Some(1.0), "fixture is a single crate" ); assert_eq!( envelope.vector.get("cargo.workspace.direct_deps"), Some(0.0), "fixture declares no dependencies" ); assert_eq!( envelope.vector.get("cargo.workspace.dev_deps"), Some(0.0), "fixture declares no dev-dependencies" ); assert_eq!( envelope.vector.get("cargo.workspace.edition"), Some(2021.0), "fixture is edition 2021" ); assert_eq!( envelope.vector.get("cargo.workspace.is_virtual"), Some(0.0), "fixture is a real package, not a virtual workspace" ); // Cargo.lock has one entry: the package itself, with no resolved deps. assert_eq!( envelope.vector.get("cargo.lock.package_count"), Some(1.0), "fixture lockfile has one package entry" ); let edge_count = envelope .vector .get("cargo.lock.dep_edge_count") .expect("cargo.lock.dep_edge_count populated"); assert_eq!(edge_count, 0.0, "fixture has no resolved deps"); let lockfile_version = envelope .vector .get("cargo.lock.lockfile_version") .expect("cargo.lock.lockfile_version populated"); assert!(lockfile_version >= 3.0, "modern cargo writes v3+ lockfiles"); // rustc --print=cfg should yield at least a handful of entries on any // supported host. Exact contents vary, so we only assert "non-empty". let cfg_count = envelope .vector .get("cargo.cfg.entry_count") .expect("cargo.cfg.entry_count populated"); assert!( cfg_count > 0.0, "rustc --print=cfg returned no entries (got {cfg_count})" ); // Toolchain pin: fixture has no rust-toolchain.toml. assert_eq!( envelope.vector.get("cargo.toolchain.pinned"), Some(0.0), "fixture does not pin a toolchain" ); } #[test] fn envelope_toolchain_stamp_classifies_the_active_channel() { let envelope = run_extract_on_fixture(); let channel = envelope.toolchain.channel.as_str(); assert!( matches!(channel, "stable" | "beta" | "nightly" | "dev"), "channel {channel:?} is not one of the documented values" ); assert!(!envelope.toolchain.version.is_empty()); assert!(!envelope.toolchain.host.is_empty()); } #[test] fn provenance_attributes_each_vector_key_to_exactly_one_level() { let envelope = run_extract_on_fixture(); let level_ids: BTreeSet<&str> = cstat_orchestrate::REQUIRED_LEVELS .iter() .map(|d| d.id) .chain(std::iter::once("meta")) .collect(); for (key, _) in envelope.vector.iter() { let owners = envelope .provenance .0 .get(key) .unwrap_or_else(|| panic!("no provenance entry for vector key {key}")); assert_eq!( owners.len(), 1, "vector key {key} has multiple owners {owners:?}; namespace cross-check failed" ); let owner = owners[0].as_str(); assert!( level_ids.contains(owner), "vector key {key} attributed to unknown level {owner}" ); } } // ───── Stage 6: codegen extractors ────────────────────────────────────── /// The five codegen-level ids covered by Stage 6 (spec §2.5 20–24). const STAGE6_LEVELS: &[&str] = &["llvm.preopt", "llvm.opt", "asm", "obj", "bin"]; #[test] fn stage_six_codegen_levels_skip_cleanly_without_the_flag() { // Default `cstat extract` (no --enable-codegen) must not drive a // cargo build. All five Stage-6 levels report Skipped with a reason // that explains how to turn them on. Spec §8 omission contract: no // codegen vector keys leak into the envelope. let envelope = run_extract_on_fixture(); for id in STAGE6_LEVELS { let report = envelope.levels.get(*id).expect("level present"); assert_eq!( report.status, LevelStatus::Skipped, "{id} should skip when codegen is disabled; got {:?}", report.status ); let reason = report.reason.as_deref().unwrap_or(""); assert!( !reason.is_empty(), "{id} skipped without a reason — user has no way to know how to enable it" ); } for key in envelope.vector.0.keys() { for prefix in ["llvm.preopt.", "llvm.opt.", "asm.", "obj.", "bin."] { assert!( !key.starts_with(prefix), "skipped codegen level emitted {key} (spec §8 says skipped levels omit their keys)" ); } } } #[test] fn stage_six_codegen_levels_report_ok_when_enabled_on_the_fixture() { let envelope = run_extract_on_fixture_with_codegen(); // The fixture is lib-only, so bin legitimately skips. Every other // Stage-6 level must report ok. for id in ["llvm.preopt", "llvm.opt", "asm", "obj"] { let report = envelope.levels.get(id).expect("level present"); assert_eq!( report.status, LevelStatus::Ok, "{id} expected ok with --enable-codegen; got {:?} (reason: {:?})", report.status, report.reason ); } let bin_report = envelope.levels.get("bin").expect("bin level present"); assert_eq!( bin_report.status, LevelStatus::Skipped, "bin should skip on the lib-only fixture; got {:?} (reason: {:?})", bin_report.status, bin_report.reason ); let bin_reason = bin_report.reason.as_deref().unwrap_or(""); assert!( bin_reason.contains("bin") || bin_reason.contains("link"), "bin skip reason should explain why; got {bin_reason:?}" ); } #[test] fn fixture_codegen_per_function_records_show_add_and_classify() { let envelope = run_extract_on_fixture_with_codegen(); // The fixture defines exactly two functions. Both should appear in // the per-function LLVM IR records, pre and post optimization. for kind in ["llvm.preopt_func", "llvm.opt_func"] { let records = envelope .entities .get(kind) .unwrap_or_else(|| panic!("entities.{kind} missing")); assert_eq!( records.len(), 2, "fixture defines two functions but {kind} has {} records", records.len() ); for r in records { assert!( r.metrics.values().all(|v| v.is_finite() && *v >= 0.0), "all per-function metrics must be finite non-negative; got {:?}", r.metrics ); } } // Summary scalars must agree with the per-function records. let summary_count = envelope .vector .get("llvm.preopt.function_count") .expect("llvm.preopt.function_count populated"); assert_eq!(summary_count, 2.0); let bb_sum = envelope .vector .get("llvm.preopt.basic_block_count.sum") .expect("llvm.preopt.basic_block_count.sum populated"); let per_func_bb_sum: f64 = envelope .entities .get("llvm.preopt_func") .unwrap() .iter() .map(|r| r.metrics.get("preopt_basic_blocks").copied().unwrap_or(0.0)) .sum(); assert_eq!( bb_sum, per_func_bb_sum, "vector sum must equal sum-over-entities" ); // Assembly: the fixture has two functions, so the .s file should // declare at least two function symbols. let asm_funcs = envelope .vector .get("asm.global.function_count") .expect("asm.global.function_count populated"); assert!( asm_funcs >= 2.0, "assembly should declare ≥ 2 functions, got {asm_funcs}" ); } #[test] fn fixture_codegen_object_section_artifacts_are_observed() { let envelope = run_extract_on_fixture_with_codegen(); // The fixture is lib-only, so its build artifact is an .rlib. The // obj level should observe at least one rlib with non-trivial size. let rlib_count = envelope .vector .get("obj.global.rlib_file_count") .expect("obj.global.rlib_file_count populated"); assert!(rlib_count >= 1.0, "expected at least one rlib, got {rlib_count}"); let rlib_bytes = envelope .vector .get("obj.global.rlib_total_bytes") .expect("obj.global.rlib_total_bytes populated"); assert!(rlib_bytes > 0.0, "rlib should not be empty"); } // ───── Stage 7: binary-artifact extractors + AST↔Symbol join ──────────── /// The six binary-artifact level ids covered by Stage 7 (spec §2.6 25–30). const STAGE7_LEVELS: &[&str] = &[ "sym.table", "sym.demangled", "sym.sections", "sym.relocs", "dwarf", "link.map", ]; #[test] fn stage_seven_binary_levels_skip_cleanly_without_codegen() { // Without --enable-codegen there are no object artifacts to read, // so every Stage 7 level must skip with a reason and emit no keys. let envelope = run_extract_on_fixture(); for id in STAGE7_LEVELS { let report = envelope.levels.get(*id).expect("level present"); assert_eq!( report.status, LevelStatus::Skipped, "{id} should skip without codegen; got {:?}", report.status ); let reason = report.reason.as_deref().unwrap_or(""); assert!(!reason.is_empty(), "{id} skipped without a reason"); } for key in envelope.vector.0.keys() { for prefix in ["sym.", "dwarf.", "link.map."] { assert!( !key.starts_with(prefix), "skipped binary level emitted {key} (spec §8 says skipped levels omit their keys)" ); } } } #[test] fn stage_seven_binary_levels_report_ok_with_codegen_on_the_lib_fixture() { let envelope = run_extract_on_fixture_with_codegen(); // sym.table, sym.demangled, sym.sections, sym.relocs, dwarf must // all be ok on the fixture (which has a real .rlib with DWARF). // link.map legitimately skips: no -Wl,-Map flag was passed. for id in ["sym.table", "sym.demangled", "sym.sections", "sym.relocs", "dwarf"] { let report = envelope.levels.get(id).expect("level present"); assert_eq!( report.status, LevelStatus::Ok, "{id} expected ok with codegen; got {:?} (reason: {:?})", report.status, report.reason ); } let link = envelope.levels.get("link.map").expect("link.map present"); assert_eq!( link.status, LevelStatus::Skipped, "link.map should skip on the lib-only fixture; got {:?}", link.status ); let reason = link.reason.as_deref().unwrap_or(""); assert!( reason.contains("link") || reason.contains("Map"), "link.map skip reason should mention linking; got {reason:?}" ); } #[test] fn fixture_symbol_table_reports_at_least_the_two_pub_functions() { let envelope = run_extract_on_fixture_with_codegen(); let func_count = envelope .vector .get("sym.global.function_count") .expect("sym.global.function_count populated"); assert!( func_count >= 2.0, "fixture's two pub fns must produce ≥ 2 function symbols, got {func_count}" ); // Per-function size distribution: max ≥ 0, mean ≥ 0, sum ≥ 0. for key in [ "sym.func.size_bytes.max", "sym.func.size_bytes.mean", "sym.func.size_bytes.sum", ] { let v = envelope.vector.get(key).unwrap_or_else(|| panic!("{key} missing")); assert!(v.is_finite() && v >= 0.0, "{key} must be a finite non-negative; got {v}"); } // Per-symbol entity records: one per function symbol with a SymbolId. let records = envelope .entities .get("sym.func") .expect("entities.sym.func populated"); assert!( records.len() >= 2, "expected ≥ 2 function-symbol records, got {}", records.len() ); for r in records { assert!( r.ids.symbol.is_some(), "every sym.func record must carry a SymbolId; got {r:?}" ); } } #[test] fn fixture_section_layout_observes_text_and_debug_bytes() { let envelope = run_extract_on_fixture_with_codegen(); let text = envelope .vector .get("sym.sections.text_bytes") .expect("sym.sections.text_bytes populated"); assert!(text > 0.0, "fixture's two fns must produce some .text"); let debug = envelope .vector .get("sym.sections.debug_bytes") .expect("sym.sections.debug_bytes populated"); assert!(debug > 0.0, "dev-profile build must carry DWARF debug sections"); let text_ratio = envelope .vector .get("sym.sections.text_ratio") .expect("text_ratio populated"); assert!( (0.0..=1.0).contains(&text_ratio), "text_ratio must be a fraction; got {text_ratio}" ); } #[test] fn fixture_dwarf_subprograms_match_the_two_pub_functions() { let envelope = run_extract_on_fixture_with_codegen(); let n = envelope .vector .get("dwarf.global.subprogram_count") .expect("dwarf.global.subprogram_count populated"); assert!( n >= 2.0, "fixture's two pub fns must show up as DWARF subprograms; got {n}" ); let with_link = envelope .vector .get("dwarf.global.subprograms_with_linkage_count") .expect("subprograms_with_linkage_count populated"); assert!(with_link >= 2.0, "both fns must have DW_AT_linkage_name"); } /// §13 worked-example anchor: the `classify` entity has both an /// `AstFuncId` and a `SymbolId`. The DWARF level is what joins them /// (via DW_AT_name → DW_AT_linkage_name). #[test] fn fixture_ast_symbol_join_populates_both_ids_on_classify() { let envelope = run_extract_on_fixture_with_codegen(); let records = envelope .entities .get("ast.func") .expect("entities.ast.func populated by the DWARF level"); let classify = records .iter() .find(|r| { r.ids .ast_func .as_ref() .is_some_and(|id| id.fq_path.ends_with("::classify")) }) .expect("classify entity present in entities.ast.func"); // Spec §6 / §13: both IDs populated. assert!( classify.ids.ast_func.is_some(), "classify must carry AstFuncId" ); let symbol = classify .ids .symbol .as_ref() .expect("classify must carry SymbolId after the DWARF join"); assert!( symbol.as_str().contains("classify"), "joined SymbolId must mention the source name; got {symbol:?}" ); // The join's success rate scalar should reflect that both fns // joined. let success = envelope .vector .get("dwarf.global.join_success_count") .expect("join_success_count populated"); let attempts = envelope .vector .get("dwarf.global.join_attempt_count") .expect("join_attempt_count populated"); assert!( attempts >= 2.0, "two AST fns means at least two join attempts; got {attempts}" ); assert!( success >= 2.0, "both fixture fns have unambiguous DWARF names; got success={success}" ); } #[test] fn fixture_source_to_symbol_ratio_is_one_with_no_generics() { // Spec §13: source_to_symbol_ratio = 1.0 when no generics are // monomorphized. The fixture has no generics. let envelope = run_extract_on_fixture_with_codegen(); let ratio = envelope .vector .get("sym.global.source_to_symbol_ratio") .expect("sym.global.source_to_symbol_ratio populated"); assert!( (ratio - 1.0).abs() < 1e-9, "expected source-to-symbol ratio = 1.0 (no generics); got {ratio}" ); let generics = envelope .vector .get("sym.demangled.generic_instantiation_count") .expect("generic_instantiation_count populated"); assert_eq!(generics, 0.0, "fixture has no generics"); } // ───── Stage 8: type-system extractors (stable side) ──────────────────── /// All three Stage 8 level ids — `type.usage` (AST approximation), /// `type.trait_impl`, and `type.coherence` (rustdoc-JSON-derived). const STAGE8_LEVELS: &[&str] = &["type.usage", "type.trait_impl", "type.coherence"]; #[test] fn stage_eight_levels_appear_with_a_documented_status() { let envelope = run_extract_on_fixture(); for id in STAGE8_LEVELS { let report = envelope .levels .get(*id) .unwrap_or_else(|| panic!("level {id} missing")); match report.status { LevelStatus::Ok => {} LevelStatus::Skipped | LevelStatus::Failed => { assert!( report.reason.as_ref().is_some_and(|r| !r.is_empty()), "non-ok stage-8 level {id} has empty reason" ); } } } } #[test] fn fixture_type_usage_observes_the_two_pub_functions() { // type.usage is AST-derived, so it must be `ok` on every well-formed // fixture — no nightly prerequisite. let envelope = run_extract_on_fixture(); let report = envelope.levels.get("type.usage").expect("type.usage present"); assert_eq!( report.status, LevelStatus::Ok, "type.usage is AST-derived; should be ok (reason: {:?})", report.reason ); assert_eq!( envelope.vector.get("type.usage.function_count"), Some(2.0), "fixture has exactly two functions" ); // Both functions reference `i32`; `classify` also uses `str`. Distinct // types overall is therefore at least 2. let overall = envelope .vector .get("type.usage.distinct_types_overall") .expect("type.usage.distinct_types_overall populated"); assert!(overall >= 2.0, "expected ≥ 2 distinct types overall, got {overall}"); let primitives = envelope .vector .get("type.usage.primitive_types_overall") .expect("primitive_types_overall populated"); assert!(primitives >= 1.0, "expected i32 to register as a primitive"); // Per-entity records: one per function, each with an AstFuncId. let records = envelope .entities .get("type.usage_func") .expect("entities.type.usage_func populated"); assert_eq!(records.len(), 2, "two functions, two records"); for r in records { assert!( r.ids.ast_func.is_some(), "type.usage_func record must carry an AstFuncId; got {r:?}" ); let distinct = r .metrics .get("distinct_types") .copied() .expect("distinct_types metric"); assert!(distinct >= 1.0, "every fn touches at least one type"); } } #[test] fn rustdoc_levels_are_ok_or_skipped_with_a_reason() { let envelope = run_extract_on_fixture(); for id in ["type.trait_impl", "type.coherence"] { let report = envelope.levels.get(id).expect("level present"); match report.status { LevelStatus::Ok => { // When ok, the level emits at least its impl_count / // quadrant scalars (zero is fine; "populated" is the // contract, "non-zero" is fixture-dependent). let prefix = format!("{id}."); let any = envelope.vector.0.keys().any(|k| k.starts_with(&prefix)); assert!(any, "ok {id} must emit at least one vector key"); } LevelStatus::Skipped => { assert!( report.reason.as_ref().is_some_and(|r| !r.is_empty()), "skipped {id} must explain itself" ); let prefix = format!("{id}."); let leaked = envelope.vector.0.keys().find(|k| k.starts_with(&prefix)); assert!( leaked.is_none(), "skipped level {id} leaked key {leaked:?} (spec §8)" ); } LevelStatus::Failed => panic!( "{id} failed on a well-formed fixture (reason: {:?})", report.reason ), } } } #[test] fn with_impls_fixture_classifies_orphan_rule_quadrants() { // The `with-impls` fixture is the load-bearing scenario for level 19: // - 2 local-trait + local-type impls (Describe / Reverse for Counter) // - 1 foreign-trait + local-type impl (Display for Counter) // - 0 local-trait + foreign-type impls // - 0 foreign-trait + foreign-type impls (no blankets that count) let envelope = run_extract_on_with_impls_fixture(); let trait_impl = envelope.levels.get("type.trait_impl").expect("level present"); let coherence = envelope.levels.get("type.coherence").expect("level present"); // If nightly is unavailable both levels skip — only run the // substantive checks when rustdoc actually ran. Either outcome is // acceptable to spec §11. if !matches!(trait_impl.status, LevelStatus::Ok) { return; } assert_eq!(coherence.status, LevelStatus::Ok); // Level 17: at least the 2 local traits + 1 inherent + 3 user impls // are visible; rustdoc additionally synthesizes auto-trait + blanket // impls, so the totals will be larger than the source-visible count. let trait_count = envelope .vector .get("type.trait_impl.trait_count") .expect("trait_count populated"); assert!( trait_count >= 2.0, "fixture defines two local traits; got {trait_count}" ); let inherent = envelope .vector .get("type.trait_impl.inherent_impl_count") .expect("inherent_impl_count populated"); assert!( inherent >= 1.0, "fixture defines an inherent impl on Counter; got {inherent}" ); // Level 19: the user-written orphan-rule quadrants. We exclude // synthetic and blanket impls so these counts reflect the source. let llt = envelope .vector .get("type.coherence.local_trait_local_type") .expect("local_trait_local_type populated"); let flt = envelope .vector .get("type.coherence.foreign_trait_local_type") .expect("foreign_trait_local_type populated"); let lft = envelope .vector .get("type.coherence.local_trait_foreign_type") .expect("local_trait_foreign_type populated"); let fft = envelope .vector .get("type.coherence.foreign_trait_foreign_type") .expect("foreign_trait_foreign_type populated"); assert_eq!(llt, 2.0, "Describe + Reverse for Counter"); assert_eq!(flt, 1.0, "Display for Counter"); assert_eq!(lft, 0.0, "no local-trait + foreign-type impls in fixture"); assert_eq!(fft, 0.0, "no foreign-trait + foreign-type impls in fixture"); } // ───── Stage 9: graph-derived extractors ──────────────────────────────── /// The eight graph-derived level ids covered by Stage 9 (spec §2.7 /// 31, 32, 34, 36–40). Levels 33 (mono call graph) and 35 (data-flow) /// land in Stage 14. const STAGE9_LEVELS: &[&str] = &[ "graph.mod", "graph.call_static", "graph.cfg", "graph.type_usage", "graph.sccs", "graph.communities", "graph.power_law", "graph.self_sim", ]; #[test] fn stage_nine_graph_levels_report_ok_on_a_well_formed_fixture() { let envelope = run_extract_on_fixture(); for id in STAGE9_LEVELS { let report = envelope .levels .get(*id) .unwrap_or_else(|| panic!("level {id} missing")); assert_eq!( report.status, LevelStatus::Ok, "stage-9 level {id} expected ok; got {:?} (reason: {:?})", report.status, report.reason ); // Each ok level must contribute at least one vector key. let prefix = format!("{id}."); let any = envelope.vector.0.keys().any(|k| k.starts_with(&prefix)); assert!(any, "ok level {id} emitted no vector keys"); } } #[test] fn fixture_call_and_mod_graphs_are_empty_with_no_uses_or_calls() { // The §13 fixture has a single file with two top-level fns that // never call each other. Spec §13 anchor: graph.call.edge_count = 0. let envelope = run_extract_on_fixture(); assert_eq!( envelope.vector.get("graph.call_static.edge_count"), Some(0.0), "fixture's two fns don't call each other" ); assert_eq!( envelope.vector.get("graph.call_static.node_count"), Some(2.0), "fixture has exactly two function nodes" ); // Single-file crate → one module node, no edges. assert_eq!( envelope.vector.get("graph.mod.edge_count"), Some(0.0), "single-file crate has no use-clause edges" ); } #[test] fn with_graphs_fixture_observes_three_modules_and_call_edges() { let envelope = run_extract_on_with_graphs_fixture(); // Module graph: three modules (`crate`, `crate::helpers`, // `crate::compute`) with two use-driven edges (helpers→compute and // compute→helpers). let mod_nodes = envelope .vector .get("graph.mod.node_count") .expect("graph.mod.node_count populated"); assert_eq!(mod_nodes, 3.0, "fixture has three modules; got {mod_nodes}"); let mod_edges = envelope .vector .get("graph.mod.edge_count") .expect("graph.mod.edge_count populated"); assert!( mod_edges >= 2.0, "fixture's reciprocal `use` clauses must produce ≥ 2 edges; got {mod_edges}" ); // Call graph: 5 functions, multiple calls. Edge count must be > 0 // and node count must be 5. let call_nodes = envelope .vector .get("graph.call_static.node_count") .expect("graph.call_static.node_count populated"); assert_eq!(call_nodes, 5.0, "fixture has 5 functions; got {call_nodes}"); let call_edges = envelope .vector .get("graph.call_static.edge_count") .expect("graph.call_static.edge_count populated"); assert!(call_edges >= 4.0, "fixture has multiple calls; got {call_edges}"); } #[test] fn with_graphs_fixture_per_function_cfg_records_carry_ast_func_ids() { let envelope = run_extract_on_with_graphs_fixture(); let records = envelope .entities .get("graph.cfg_func") .expect("entities.graph.cfg_func populated"); // Five functions: run, clamp, boosted, scale, quadratic. assert_eq!(records.len(), 5, "five fns → five CFG records"); for r in records { assert!( r.ids.ast_func.is_some(), "every CFG record must carry an AstFuncId; got {r:?}" ); let nodes = r .metrics .get("nodes") .copied() .expect("CFG record has node count"); assert!(nodes >= 1.0, "every fn has at least one basic block; got {nodes}"); } // `clamp` has two `if`/`else if` branches → branch_count ≥ 2. let clamp = records .iter() .find(|r| { r.ids .ast_func .as_ref() .is_some_and(|id| id.fq_path.ends_with("::clamp")) }) .expect("clamp fn present"); let branches = clamp .metrics .get("branches") .copied() .expect("clamp.branches"); assert!( branches >= 2.0, "clamp has if/else-if/else; expected ≥ 2 branches, got {branches}" ); } #[test] fn fixture_type_usage_graph_populates_function_and_type_counts() { let envelope = run_extract_on_fixture(); let funcs = envelope .vector .get("graph.type_usage.function_count") .expect("graph.type_usage.function_count populated"); let types = envelope .vector .get("graph.type_usage.type_count") .expect("graph.type_usage.type_count populated"); let edges = envelope .vector .get("graph.type_usage.edge_count") .expect("graph.type_usage.edge_count populated"); assert_eq!(funcs, 2.0, "fixture has two functions"); assert!( types >= 2.0, "fixture references at least i32 and str; got {types}" ); assert!( edges >= 2.0, "fixture has at least 2 fn→type edges; got {edges}" ); } #[test] fn fixture_scc_and_community_metrics_are_well_formed() { let envelope = run_extract_on_with_graphs_fixture(); // Every fn-node is its own SCC (call graph has cycles only via // recursion, which the fixture doesn't have). So SCC count == node count. let n = envelope .vector .get("graph.call_static.node_count") .expect("call_static node_count populated"); let scc_count = envelope .vector .get("graph.sccs.call_static.count") .expect("graph.sccs.call_static.count populated"); assert_eq!(scc_count, n, "no recursion → one SCC per fn"); // No non-trivial SCCs (no cycles). assert_eq!( envelope .vector .get("graph.sccs.call_static.nontrivial_count"), Some(0.0), "fixture has no recursive calls" ); // Community detection emits a modularity Q in [-0.5, 1.0]; check the // value is finite and in-range. let q = envelope .vector .get("graph.communities.call_static.modularity_q") .expect("modularity_q populated"); assert!(q.is_finite(), "modularity Q must be finite; got {q}"); assert!( (-0.5..=1.0).contains(&q), "modularity Q must lie in [-0.5, 1.0]; got {q}" ); } #[test] fn fixture_power_law_and_self_sim_emit_finite_scalars() { let envelope = run_extract_on_with_graphs_fixture(); let alpha = envelope .vector .get("graph.power_law.call_static.degree_exponent") .expect("degree_exponent populated"); assert!(alpha.is_finite(), "α must be finite; got {alpha}"); assert!(alpha >= 0.0, "α must be non-negative; got {alpha}"); let n = envelope .vector .get("graph.power_law.call_static.sample_count") .expect("sample_count populated"); assert!(n >= 0.0, "sample_count must be non-negative; got {n}"); for key in [ "graph.self_sim.branch.func_vs_file.ks_distance", "graph.self_sim.branch.func_vs_module.ks_distance", "graph.self_sim.branch.file_vs_module.ks_distance", ] { let d = envelope.vector.get(key).expect("ks distance populated"); assert!(d.is_finite(), "{key} must be finite; got {d}"); assert!( (0.0..=1.0).contains(&d), "{key} must be a KS distance in [0,1]; got {d}" ); } } #[test] fn graph_level_vector_keys_use_their_level_namespace() { // Spec §5: vector keys are namespaced by their level's id. Anchor // test 5 (no two levels claim the same key) is already exercised // generally by the provenance test; this one specifically asserts // that every Stage-9 emission lives under its level's prefix. let envelope = run_extract_on_with_graphs_fixture(); for (key, _) in envelope.vector.iter() { for id in STAGE9_LEVELS { // If a key is claimed by `id`, it must start with `id.`. let owner = envelope .provenance .0 .get(key) .and_then(|v| v.first()) .map(|s| s.as_str()) .unwrap_or(""); if owner == *id { let prefix = format!("{id}."); assert!( key.starts_with(&prefix), "key {key:?} owned by {id} should live under {prefix:?}" ); } } } } // ───── Stage 10: external-tool extractors ────────────────────────────── /// Spec §2.9 levels covered by Stage 10. Each may be `ok` or /// `skipped`; on a well-formed fixture none should be `failed`. const STAGE10_LEVELS: &[&str] = &["clippy", "rustdoc", "tests", "doctests"]; #[test] fn stage_ten_levels_appear_with_a_documented_status() { let envelope = run_extract_on_fixture(); for id in STAGE10_LEVELS { let report = envelope .levels .get(*id) .unwrap_or_else(|| panic!("level {id} missing from envelope")); match report.status { LevelStatus::Ok | LevelStatus::Skipped => { if report.status == LevelStatus::Skipped { assert!( report.reason.as_ref().is_some_and(|r| !r.is_empty()), "skipped {id} must explain itself" ); } } LevelStatus::Failed => panic!( "stage-10 level {id} should not fail on a well-formed fixture (reason: {:?})", report.reason ), } } } #[test] fn stage_ten_levels_obey_the_omission_contract() { // Spec §8: skipped/failed levels must not leak vector keys under // their level-id prefix. Ok levels must emit at least one key. let envelope = run_extract_on_fixture(); for id in STAGE10_LEVELS { let report = envelope.levels.get(*id).expect("level present"); let prefix = format!("{id}."); let any_key = envelope.vector.0.keys().any(|k| k.starts_with(&prefix)); match report.status { LevelStatus::Ok => assert!(any_key, "ok level {id} emitted no keys"), LevelStatus::Skipped => { let leaked = envelope.vector.0.keys().find(|k| k.starts_with(&prefix)); assert!( leaked.is_none(), "skipped level {id} leaked key {leaked:?} (spec §8)" ); } LevelStatus::Failed => unreachable!("ruled out by previous test"), } } } #[test] fn fixture_tests_and_doctests_inventories_show_zero_on_the_lib_fixture() { // The §13 fixture has no `#[test]` functions and no doctests. Spec // §8: ok levels with zero matter still must populate their summary // keys (so the consumer can distinguish "no tests" from "level // didn't run"). If the level is skipped on this host that's also // ok — bail early. let envelope = run_extract_on_fixture(); let tests_report = envelope.levels.get("tests").expect("tests level present"); if matches!(tests_report.status, LevelStatus::Ok) { let count = envelope .vector .get("tests.test_count") .expect("tests.test_count populated"); assert_eq!(count, 0.0, "fixture defines no tests; got {count}"); let binaries = envelope .vector .get("tests.binary_count") .expect("tests.binary_count populated"); assert!( binaries >= 1.0, "even an empty lib produces one test binary; got {binaries}" ); } let dt_report = envelope .levels .get("doctests") .expect("doctests level present"); if matches!(dt_report.status, LevelStatus::Ok) { let count = envelope .vector .get("doctests.test_count") .expect("doctests.test_count populated"); assert_eq!(count, 0.0, "fixture has no doctests; got {count}"); } } #[test] fn fixture_clippy_level_yields_finite_lint_counts() { // The §13 fixture is trivially clean — clippy may produce zero or a // handful of pedantic lints depending on the host's clippy version. // The contract: when ok, the summary scalars exist and are finite. let envelope = run_extract_on_fixture(); let report = envelope.levels.get("clippy").expect("clippy level present"); if !matches!(report.status, LevelStatus::Ok) { return; } for key in [ "clippy.message_count", "clippy.lint_count", "clippy.warning_count", "clippy.error_count", "clippy.distinct_lint_names", ] { let v = envelope .vector .get(key) .unwrap_or_else(|| panic!("{key} missing")); assert!(v.is_finite() && v >= 0.0, "{key} must be a finite count; got {v}"); } // The fixture compiles, so no errors. assert_eq!( envelope.vector.get("clippy.error_count"), Some(0.0), "fixture must compile cleanly under clippy" ); } #[test] fn fixture_rustdoc_inventory_reports_two_local_functions_when_available() { // Level 46 needs nightly to produce rustdoc JSON. When ok we expect // the two pub fns (add + classify) to show up as `function` kind // entries; when skipped the level explains itself. let envelope = run_extract_on_fixture(); let report = envelope.levels.get("rustdoc").expect("rustdoc level present"); match report.status { LevelStatus::Ok => { let fns = envelope .vector .get("rustdoc.kind.function") .expect("rustdoc.kind.function populated when level is ok"); assert!( fns >= 2.0, "fixture has two pub functions; rustdoc should list at least that many, got {fns}" ); let local = envelope .vector .get("rustdoc.local_item_count") .expect("rustdoc.local_item_count populated"); assert!(local >= 2.0, "local_item_count must include the two fns"); } LevelStatus::Skipped => { assert!( report.reason.as_ref().is_some_and(|r| !r.is_empty()), "skipped rustdoc level must explain itself" ); } LevelStatus::Failed => panic!("rustdoc failed on a clean fixture: {:?}", report.reason), } } // ───── Stage 13: rustc-derived Level impls (nightly subprocess) ──────── /// The seven nightly-IR level ids covered by Stage 13 (spec §2.2 #10, /// §2.3 #11–15, §2.4 #18). const STAGE13_LEVELS: &[&str] = &[ "ast.resolved", "hir", "thir", "mir.preopt", "mir.opt", "borrowck", "type.mono", ]; #[test] fn stage_thirteen_levels_skip_cleanly_without_the_flag() { // Without --enable-rustc-internal the seven IR levels must all // report Skipped with a reason that tells the user how to enable // them. Spec §8 omission contract: no IR vector keys leak. let envelope = run_extract_on_fixture(); for id in STAGE13_LEVELS { let report = envelope.levels.get(*id).expect("level present"); assert_eq!( report.status, LevelStatus::Skipped, "{id} should skip when rustc-internal extraction is disabled; got {:?}", report.status ); let reason = report.reason.as_deref().unwrap_or(""); assert!( !reason.is_empty(), "{id} skipped without a reason — user has no way to know how to enable it" ); } for key in envelope.vector.0.keys() { for prefix in [ "ast.resolved.", "hir.", "thir.", "mir.preopt.", "mir.opt.", "borrowck.", "type.mono.", ] { assert!( !key.starts_with(prefix), "skipped rustc-internal level emitted {key} (spec §8 says skipped levels omit their keys)" ); } } } /// Shared --enable-rustc-internal extract on the fixture. Cached so /// the several scenario tests below don't each pay the cost of /// building and running the nightly sub-binary. fn run_extract_on_fixture_with_rustc_internal() -> &'static OracleOutput { static CACHE: OnceLock = OnceLock::new(); CACHE.get_or_init(|| { let fixture = workspace_root().join("examples").join("single-file-lib"); assert!(fixture.is_dir(), "fixture missing: {fixture:?}"); run_extract_with(&fixture, &["--enable-rustc-internal"]) }) } #[test] fn stage_thirteen_levels_appear_with_a_documented_status_when_enabled() { // With --enable-rustc-internal each level is either ok (nightly // present and the extractor ran) or skipped (nightly absent, or // the sub-binary couldn't be located/built on this host). Failed // is acceptable too — the spec §11 contract is "documented // status with non-empty reason", not "guaranteed ok". let envelope = run_extract_on_fixture_with_rustc_internal(); for id in STAGE13_LEVELS { let report = envelope.levels.get(*id).expect("level present"); match report.status { LevelStatus::Ok => { let prefix = format!("{id}."); let any = envelope.vector.0.keys().any(|k| k.starts_with(&prefix)); assert!(any, "ok {id} must emit at least one vector key"); } LevelStatus::Skipped | LevelStatus::Failed => { assert!( report.reason.as_ref().is_some_and(|r| !r.is_empty()), "{id} non-ok must explain itself" ); let prefix = format!("{id}."); let leaked = envelope.vector.0.keys().find(|k| k.starts_with(&prefix)); assert!( leaked.is_none(), "{id} {:?} leaked key {leaked:?} (spec §8)", report.status ); } } } } #[test] fn stage_thirteen_levels_share_one_reason_when_nightly_missing() { // If nightly isn't available, all seven IR levels must report the // same reason — the single subprocess invocation skipped once and // every level routed the same diagnostic back. This stops the // user from seeing seven different complaints when there is only // one underlying cause. let envelope = run_extract_on_fixture_with_rustc_internal(); let reasons: BTreeSet = STAGE13_LEVELS .iter() .map(|id| { envelope .levels .get(*id) .and_then(|r| r.reason.clone()) .unwrap_or_default() }) .collect(); let any_ok = STAGE13_LEVELS.iter().any(|id| { envelope .levels .get(*id) .map(|r| r.status == LevelStatus::Ok) .unwrap_or(false) }); if !any_ok { assert_eq!( reasons.len(), 1, "every IR level should share one reason when the cache fails once; got {reasons:?}" ); } } #[test] fn stage_thirteen_fixture_reflects_two_functions_when_ok() { // When nightly is available and the sub-binary runs, the fixture // (two pub fns: add + classify) must produce observable signals: // ≥ 2 HIR fns, ≥ 2 THIR bodies, ≥ 2 MIR bodies (pre and opt), and // ≥ 2 mono items. Exact values are rustc-version-sensitive; we // lower-bound them. let envelope = run_extract_on_fixture_with_rustc_internal(); let hir_status = &envelope.levels.get("hir").expect("hir present").status; if !matches!(hir_status, LevelStatus::Ok) { // Nightly unavailable or extractor couldn't run; the previous // tests already cover that case. return; } let hir_fns = envelope .vector .get("hir.fn_count") .expect("hir.fn_count populated when ok"); assert!( hir_fns >= 2.0, "fixture has two pub fns; hir.fn_count must be ≥ 2, got {hir_fns}" ); let thir_bodies = envelope .vector .get("thir.body_count") .expect("thir.body_count populated when ok"); assert!( thir_bodies >= 2.0, "thir.body_count must reflect both fixture fns, got {thir_bodies}" ); for level_id in ["mir.preopt", "mir.opt"] { let key = format!("{level_id}.body_count"); let n = envelope .vector .get(&key) .unwrap_or_else(|| panic!("{key} populated when ok")); assert!(n >= 2.0, "{key} must be ≥ 2, got {n}"); // classify has two if-expressions → ≥ 4 basic blocks in MIR. let bb_max_key = format!("{level_id}.basic_block_count.max"); let bb_max = envelope .vector .get(&bb_max_key) .unwrap_or_else(|| panic!("{bb_max_key} populated when ok")); assert!( bb_max >= 4.0, "{bb_max_key} must capture classify's branching (≥ 4); got {bb_max}" ); } let mono_items = envelope .vector .get("type.mono.item_count") .expect("type.mono.item_count populated when ok"); assert!( mono_items >= 2.0, "fixture's two non-generic fns must produce ≥ 2 mono items, got {mono_items}" ); // Non-generic fns yield exactly one instantiation each → no group // has more than one instance. let multi = envelope .vector .get("type.mono.multi_instance_group_count") .expect("type.mono.multi_instance_group_count populated when ok"); assert_eq!( multi, 0.0, "fixture has no generics; multi-instance groups must be 0" ); // HIR expr-kind histogram must include "if" with ≥ 2 (classify has // two if-expressions). let if_kinds = envelope .vector .get("hir.expr_kind.if") .unwrap_or(0.0); assert!( if_kinds >= 2.0, "hir.expr_kind.if should reflect classify's two ifs, got {if_kinds}" ); // borrowck and ast.resolved levels emit at least their core scalars. let region_sum = envelope .vector .get("borrowck.region_count.sum") .expect("borrowck.region_count.sum populated when ok"); assert!( region_sum.is_finite() && region_sum >= 0.0, "borrowck.region_count.sum must be finite non-negative, got {region_sum}" ); let imports = envelope .vector .get("ast.resolved.import_count") .expect("ast.resolved.import_count populated when ok"); assert!( imports.is_finite() && imports >= 0.0, "ast.resolved.import_count must be finite non-negative, got {imports}" ); } // ───── Stage 14: derived cross-level extractors (33, 35) ──────────────── /// The two Stage-14 derived levels (spec §2.7 #33 + #35). const STAGE14_LEVELS: &[&str] = &["graph.call_mono", "graph.dataflow"]; #[test] fn stage_fourteen_levels_skip_cleanly_without_codegen() { // The mono-call-graph and per-function dataflow levels both compose // codegen-side artifacts. Without `--enable-codegen` they must skip // with a documented reason and contribute no vector keys (spec §8). let envelope = run_extract_on_fixture(); for id in STAGE14_LEVELS { let report = envelope.levels.get(*id).expect("level present"); assert_eq!( report.status, LevelStatus::Skipped, "{id} should skip without codegen; got {:?}", report.status ); let reason = report.reason.as_deref().unwrap_or(""); assert!(!reason.is_empty(), "{id} skipped without a reason"); } for key in envelope.vector.0.keys() { assert!( !key.starts_with("graph.call_mono.") && !key.starts_with("graph.dataflow."), "skipped Stage-14 level emitted {key} (spec §8 says skipped levels omit their keys)" ); } } #[test] fn stage_fourteen_levels_report_ok_with_codegen_on_the_fixture() { let envelope = run_extract_on_fixture_with_codegen(); for id in STAGE14_LEVELS { let report = envelope.levels.get(*id).expect("level present"); assert_eq!( report.status, LevelStatus::Ok, "{id} expected ok with codegen; got {:?} (reason: {:?})", report.status, report.reason ); } } #[test] fn fixture_call_mono_graph_observes_at_least_the_two_pub_functions() { // Spec §7 Stage 14 done-when: mono-call-graph edges and per-function // dataflow density scalars appear in the vector for the fixture. // The fixture has two top-level pub fns; the codegen build emits // each as its own LLVM function, so the mono node count is ≥ 2 and // every node carries both a SymbolId and a MonoId. let envelope = run_extract_on_fixture_with_codegen(); let node_count = envelope .vector .get("graph.call_mono.node_count") .expect("graph.call_mono.node_count populated"); assert!( node_count >= 2.0, "two pub fns must produce ≥ 2 mono nodes, got {node_count}" ); // The fixture has no cross-fn calls, so the in-graph edge count is // 0 and there are exactly as many leaves as nodes (every fn is a // leaf in its own call graph). External calls (to panic helpers, // intrinsics, etc.) may still register on the external side. let edge_count = envelope .vector .get("graph.call_mono.edge_count") .expect("graph.call_mono.edge_count populated"); assert!( edge_count.is_finite() && edge_count >= 0.0, "edge_count must be finite ≥ 0, got {edge_count}" ); let leaf_count = envelope .vector .get("graph.call_mono.leaf_count") .expect("graph.call_mono.leaf_count populated"); assert!(leaf_count >= 2.0, "≥ 2 leaves expected, got {leaf_count}"); let recursive = envelope .vector .get("graph.call_mono.recursive_node_count") .expect("graph.call_mono.recursive_node_count populated"); assert_eq!(recursive, 0.0, "fixture has no recursion; got {recursive}"); // Degree summaries must be finite even on graphs with zero edges. for key in [ "graph.call_mono.in_degree.max", "graph.call_mono.in_degree.mean", "graph.call_mono.out_degree.max", "graph.call_mono.out_degree.mean", "graph.call_mono.density", ] { let v = envelope .vector .get(key) .unwrap_or_else(|| panic!("{key} missing")); assert!(v.is_finite() && v >= 0.0, "{key} must be finite ≥ 0; got {v}"); } // Per-node entities: each carries SymbolId + MonoId per spec §6. let records = envelope .entities .get("graph.call_mono_node") .expect("entities.graph.call_mono_node populated when ok"); assert!( records.len() >= 2, "expected ≥ 2 mono-node records, got {}", records.len() ); for r in records { assert!( r.ids.symbol.is_some(), "every mono-node record must carry a SymbolId; got {r:?}" ); assert!( r.ids.mono.is_some(), "every mono-node record must carry a MonoId; got {r:?}" ); } } #[test] fn fixture_dataflow_density_is_a_finite_per_function_scalar() { let envelope = run_extract_on_fixture_with_codegen(); let n = envelope .vector .get("graph.dataflow.function_count") .expect("graph.dataflow.function_count populated"); assert!(n >= 2.0, "at least two functions expected, got {n}"); // Aggregate scalars must be finite and well-typed. density.mean is // dimensionless (edges per instruction) but, in pathological cases // with heavy reuse, can exceed 1 — only the finite+non-negative // contract is load-bearing. for key in [ "graph.dataflow.def_count.sum", "graph.dataflow.def_count.mean", "graph.dataflow.def_count.max", "graph.dataflow.use_count.sum", "graph.dataflow.use_count.mean", "graph.dataflow.use_count.max", "graph.dataflow.edge_count.sum", "graph.dataflow.edge_count.mean", "graph.dataflow.edge_count.max", "graph.dataflow.density.mean", "graph.dataflow.density.max", ] { let v = envelope .vector .get(key) .unwrap_or_else(|| panic!("{key} missing")); assert!(v.is_finite() && v >= 0.0, "{key} must be finite ≥ 0; got {v}"); } // At least one function in this fixture (classify) is non-trivial // and must have a non-zero def-use edge count. let edge_max = envelope .vector .get("graph.dataflow.edge_count.max") .unwrap(); assert!( edge_max > 0.0, "fixture's classify body must have some def-use chain, got max={edge_max}" ); // Per-function entity records: density present, finite, ≥ 0. let records = envelope .entities .get("graph.dataflow_func") .expect("entities.graph.dataflow_func populated when ok"); assert!( records.len() >= 2, "expected ≥ 2 dataflow records, got {}", records.len() ); for r in records { let density = r .metrics .get("density") .copied() .unwrap_or_else(|| panic!("per-fn density missing: {r:?}")); assert!( density.is_finite() && density >= 0.0, "per-fn density must be finite ≥ 0; got {density}" ); assert!( r.ids.symbol.is_some(), "every dataflow record must carry a SymbolId; got {r:?}" ); } } #[test] fn stage_fourteen_keys_obey_their_level_namespace() { // Spec §4 + §7: each level owns its key prefix; provenance must // attribute each Stage-14 key to exactly its owning level. let envelope = run_extract_on_fixture_with_codegen(); for (key, owners) in &envelope.provenance.0 { if key.starts_with("graph.call_mono.") { assert_eq!( owners, &vec!["graph.call_mono".to_string()], "{key} should be owned only by graph.call_mono; got {owners:?}" ); } if key.starts_with("graph.dataflow.") { assert_eq!( owners, &vec!["graph.dataflow".to_string()], "{key} should be owned only by graph.dataflow; got {owners:?}" ); } } }