nyx/tests/parse_timeout_test.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

96 lines
3.7 KiB
Rust

//! Parse-timeout integration test (isolated in its own binary so the
//! installed analysis-options runtime cannot race with other tests).
//!
//! Tree-sitter parsing is normally fast, but adversarial inputs can drive
//! it into much slower parses. The scanner enforces a per-file timeout via
//! a progress callback; this test verifies the wiring end-to-end by setting
//! the timeout to 1 ms and confirming that a moderately-sized file is
//! *skipped* rather than parsed.
//!
//! The timeout is configured via `analysis.engine.parse_timeout_ms` (or
//! `--parse-timeout-ms` on the CLI); this test drives it by installing a
//! custom `AnalysisOptions` at process start.
use nyx_scanner::ast::run_rules_on_bytes;
use nyx_scanner::utils::AnalysisOptions;
use nyx_scanner::utils::analysis_options;
use nyx_scanner::utils::config::{AnalysisMode, Config};
use std::path::Path;
use std::time::{Duration, Instant};
fn hostile_cfg() -> Config {
let mut cfg = Config::default();
cfg.scanner.mode = AnalysisMode::Full;
cfg.scanner.read_vcsignore = false;
cfg.scanner.require_git_to_read_vcsignore = false;
cfg.performance.worker_threads = Some(1);
cfg.performance.batch_size = 8;
cfg.performance.channel_multiplier = 1;
cfg
}
/// Generate a large but valid JS source: many short statements on one line.
/// O(n) in size so the test harness itself is not a bottleneck.
fn build_bulk_source(bytes_target: usize) -> String {
let mut s = String::with_capacity(bytes_target + 64);
let mut i: usize = 0;
while s.len() < bytes_target {
use std::fmt::Write;
let _ = write!(s, "var a{i}=1;");
i += 1;
}
s.push('\n');
s
}
#[test]
fn parse_timeout_config_short_circuits_parse() {
// ~1 MiB of valid JS, plenty of real parser work to observe the
// timeout. Still well under MAX_PARSE_BYTES.
let source = build_bulk_source(1_000_000);
// Install a 1 ms timeout via the analysis-options runtime. This test
// runs in its own integration-test binary (separate process), so the
// `OnceLock` install cannot collide with other tests.
analysis_options::install(AnalysisOptions {
parse_timeout_ms: 1,
..AnalysisOptions::default()
});
let path = Path::new("slow.js");
let cfg = hostile_cfg();
let start = Instant::now();
let diags = run_rules_on_bytes(source.as_bytes(), path, &cfg, None, None)
.expect("timeout should yield Ok(empty), not error");
let elapsed = start.elapsed();
// A timed-out parse surfaces a synthetic informational diag
// carrying an `EngineNote::ParseTimeout` so downstream tooling can
// tell "we found nothing" from "we stopped looking". Any other
// finding would imply the parser actually produced a tree, i.e.
// the timeout did not short-circuit.
assert!(
diags.iter().all(|d| d.id == "engine.parse_timeout"),
"timed-out parse should only produce the engine.parse_timeout \
synthetic diag, got {diags:?}",
);
assert!(
diags.iter().any(|d| d.id == "engine.parse_timeout"
&& d.evidence
.as_ref()
.is_some_and(|ev| ev.engine_notes.iter().any(|n| matches!(
n,
nyx_scanner::engine_notes::EngineNote::ParseTimeout { .. }
)))),
"timed-out parse must emit the synthetic ParseTimeout note; \
got {diags:?}",
);
// With a 1 ms cap, tree-sitter should be cancelled and the file
// skipped long before a cold full-analysis run would finish. A
// 2 s budget is a generous bound even for slow debug builds.
assert!(
elapsed < Duration::from_secs(2),
"parse-timeout cancellation did not short-circuit; call took {elapsed:?}",
);
}