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
96 lines
3.7 KiB
Rust
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:?}",
|
|
);
|
|
}
|