nyx/tests/gauss_seidel_tests.rs
Eli Peter a438886217
Python fp and docs updtes (#58)
* refactor: Update comments for clarity and add expectations.json files for performance metrics

* feat: Implement FP guard for JS/TS local-collection receivers to suppress missing ownership checks

* feat: Enhance Rust parameter handling to classify local collections and prevent false ownership checks

* refactor: Simplify code formatting for better readability in multiple files

* refactor: Improve UTF-8 sequence length handling and enhance clarity in loop iteration

* feat: Update Java and Python patterns to include new security rules

* refactor: Improve comment clarity and consistency across multiple Rust files

* refactor: Simplify code formatting for improved readability in integration tests and module files

* refactor: Improve comment formatting and enhance clarity in assertions across multiple files
2026-04-29 19:53:34 -04:00

162 lines
5.9 KiB
Rust

//! Phase-C regression tests for the Gauss-Seidel variant of JS/TS
//! pass-2 convergence.
//!
//! Default mode is Jacobi (order-independent, reproducible).
//! Gauss-Seidel is opt-in via `NYX_JS_GAUSS_SEIDEL=1` (or the
//! test-only override). The two variants must produce **equal
//! findings** on every fixture, this is the core correctness
//! invariant for shipping G-S behind a flag.
//!
//! If this test ever fails, Gauss-Seidel has a precision leak and
//! must NOT be enabled by default.
mod common;
use common::{scan_fixture_dir, validate_expectations};
use nyx_scanner::taint::{
last_js_ts_pass2_iterations, set_js_ts_gauss_seidel_override, set_js_ts_pass2_cap_override,
};
use nyx_scanner::utils::config::AnalysisMode;
use std::path::Path;
use std::sync::Mutex;
fn fixture_path(name: &str) -> std::path::PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures")
.join(name)
}
/// Serialize tests that mutate the Gauss-Seidel and pass-2 cap
/// overrides. Both are process-global `AtomicUsize`s and `cargo
/// test` runs in parallel by default.
static GS_TEST_GUARD: Mutex<()> = Mutex::new(());
/// Sort findings into a deterministic order that ignores
/// non-semantic fields so we can compare Jacobi vs. Gauss-Seidel
/// runs. Comparing raw `Diag` equality would be too strict ,
/// evidence ordering, span-derived IDs, and rank scores can differ
/// harmlessly between variants. We assert on the tuple
/// `(path, line, col, id, severity, suppressed)` which is the
/// finding's identity.
fn finding_identities(
diags: &[nyx_scanner::commands::scan::Diag],
) -> Vec<(
String,
usize,
usize,
String,
nyx_scanner::patterns::Severity,
bool,
)> {
let mut v: Vec<_> = diags
.iter()
.map(|d| {
(
d.path.clone(),
d.line,
d.col,
d.id.clone(),
d.severity,
d.suppressed,
)
})
.collect();
v.sort();
v
}
/// Phase-C correctness invariant: Jacobi and Gauss-Seidel produce
/// **equal findings** on the deep-chain fixture.
///
/// Gauss-Seidel may converge in fewer iterations, that is the whole
/// point of the optimisation, but the set of findings and their
/// primary locations must be identical. A divergence here would
/// mean G-S is cutting off a real flow or introducing a spurious
/// one; ship-blocking either way.
#[test]
fn gauss_seidel_matches_jacobi_on_deep_chain() {
let _guard = GS_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
set_js_ts_pass2_cap_override(0);
let dir = fixture_path("js_ts_pass2_deep_chain");
// Run 1: force Jacobi.
set_js_ts_gauss_seidel_override(1);
let diags_jacobi = scan_fixture_dir(&dir, AnalysisMode::Full);
let jacobi_iters = last_js_ts_pass2_iterations();
validate_expectations(&diags_jacobi, &dir);
// Run 2: force Gauss-Seidel.
set_js_ts_gauss_seidel_override(2);
let diags_gs = scan_fixture_dir(&dir, AnalysisMode::Full);
let gs_iters = last_js_ts_pass2_iterations();
validate_expectations(&diags_gs, &dir);
// Restore default mode so other tests see a clean override.
set_js_ts_gauss_seidel_override(0);
// Invariant 1: finding identity equality.
let jacobi_ids = finding_identities(&diags_jacobi);
let gs_ids = finding_identities(&diags_gs);
assert_eq!(
jacobi_ids, gs_ids,
"Jacobi and Gauss-Seidel must produce identical findings. \
Jacobi: {jacobi_ids:#?}\nGauss-Seidel: {gs_ids:#?}"
);
// Invariant 2: Gauss-Seidel never takes MORE iterations than
// Jacobi. On chain-shaped fixtures G-S should match or
// strictly improve.
assert!(
gs_iters <= jacobi_iters,
"Gauss-Seidel must not increase iteration count. \
Jacobi={jacobi_iters}, Gauss-Seidel={gs_iters}"
);
}
/// Determinism invariant: the same Gauss-Seidel run on the same
/// fixture produces byte-equal findings across invocations. Tests
/// that the pinned traversal order (`containment_order`) is
/// deterministic.
#[test]
fn gauss_seidel_is_deterministic_across_runs() {
let _guard = GS_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
set_js_ts_pass2_cap_override(0);
set_js_ts_gauss_seidel_override(2);
let dir = fixture_path("js_ts_pass2_deep_chain");
let diags1 = scan_fixture_dir(&dir, AnalysisMode::Full);
let diags2 = scan_fixture_dir(&dir, AnalysisMode::Full);
let diags3 = scan_fixture_dir(&dir, AnalysisMode::Full);
set_js_ts_gauss_seidel_override(0);
let ids1 = finding_identities(&diags1);
let ids2 = finding_identities(&diags2);
let ids3 = finding_identities(&diags3);
assert_eq!(ids1, ids2, "Gauss-Seidel findings must be deterministic");
assert_eq!(
ids2, ids3,
"Gauss-Seidel findings must be deterministic across 3 runs"
);
}
/// Default behaviour: no override, no env var → Jacobi. Guards
/// against a future refactor accidentally flipping the default.
#[test]
fn default_mode_is_jacobi_when_env_unset() {
let _guard = GS_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
set_js_ts_gauss_seidel_override(0); // clear any test override
// We can't fully prove this without process isolation
// (js_ts_gauss_seidel_enabled caches via OnceLock on first call),
// but we can assert that when the explicit test override is 0,
// the public accessor returns a bool. The cap on this test is
// that it runs alongside others in the same process; the
// important guarantee is covered by
// `gauss_seidel_matches_jacobi_on_deep_chain` above.
let _enabled = nyx_scanner::taint::js_ts_gauss_seidel_enabled();
// If the OnceLock got initialized to "enabled" by an earlier
// test that set the env var, we can't do much about it here.
// The determinism and equivalence tests above are the
// load-bearing guarantees.
}