mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-06 19:35:13 +02:00
* 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
162 lines
5.9 KiB
Rust
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.
|
|
}
|