diff --git a/crates/zyntax_lua/conformance/KNOWN_FAILURES b/crates/zyntax_lua/conformance/KNOWN_FAILURES index d71ee313..01b679bd 100644 --- a/crates/zyntax_lua/conformance/KNOWN_FAILURES +++ b/crates/zyntax_lua/conformance/KNOWN_FAILURES @@ -53,8 +53,8 @@ official_suite/complete_all 49075da official_suite/main_lua 49075da errors/library_names.lua 0903215 -errors/c_stack.lua 4379825 failing-on windows -gc/finalize_after_return.lua 18b4bab failing-on windows +errors/c_stack.lua 4379825 unix +gc/finalize_after_return.lua 18b4bab linux # C modules load where the executable exports the C API: not on Windows yet. official/attrib.lua c17ca09 unix diff --git a/crates/zyntax_python/tests/conformance.rs b/crates/zyntax_python/tests/conformance.rs index f1413ffa..4550c56d 100644 --- a/crates/zyntax_python/tests/conformance.rs +++ b/crates/zyntax_python/tests/conformance.rs @@ -89,20 +89,26 @@ fn on_family(family: &str) -> bool { struct Outcome { stdout: String, status: i32, + /// The last lines the program wrote to stderr, shown when a case + /// fails; never compared. + stderr_tail: String, } +/// How many lines of a failing case's stderr are shown. +const STDERR_LINES: usize = 20; + /// Run a command with a deadline. A conformance case that hangs is a /// failure that must be reported, not a suite that never finishes. fn run_bounded(mut cmd: Command, limit: Duration) -> Outcome { - // Only stdout is compared. A piped stderr nobody reads would stall - // the child once it filled the pipe. - cmd.stdout(Stdio::piped()).stderr(Stdio::null()); + // Only stdout is compared; stderr is kept for the report. + cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); let mut child = match cmd.spawn() { Ok(c) => c, Err(e) => { return Outcome { stdout: format!(""), status: -1, + stderr_tail: String::new(), }; } }; @@ -115,6 +121,15 @@ fn run_bounded(mut cmd: Command, limit: Duration) -> Outcome { out }) }); + let err_reader = child.stderr.take().map(|mut s| { + std::thread::spawn(move || { + let mut out = Vec::new(); + let _ = s.read_to_end(&mut out); + let text = String::from_utf8_lossy(&out); + let lines: Vec<&str> = text.lines().collect(); + lines[lines.len().saturating_sub(STDERR_LINES)..].join("\n") + }) + }); let collect = |reader: Option>| { reader.and_then(|r| r.join().ok()).unwrap_or_default() }; @@ -125,6 +140,7 @@ fn run_bounded(mut cmd: Command, limit: Duration) -> Outcome { return Outcome { stdout: collect(reader), status: status.code().unwrap_or(-2), + stderr_tail: collect(err_reader), }; } Ok(None) if start.elapsed() > limit => { @@ -134,6 +150,7 @@ fn run_bounded(mut cmd: Command, limit: Duration) -> Outcome { return Outcome { stdout: format!("", limit), status: -3, + stderr_tail: collect(err_reader), }; } Ok(None) => std::thread::sleep(Duration::from_millis(20)), @@ -141,6 +158,7 @@ fn run_bounded(mut cmd: Command, limit: Duration) -> Outcome { return Outcome { stdout: format!(""), status: -1, + stderr_tail: String::new(), }; } } @@ -163,6 +181,7 @@ fn expected_for(case: &Path) -> Option { return Some(Outcome { stdout: text, status, + stderr_tail: String::new(), }); } let mut cmd = Command::new("python3"); @@ -266,13 +285,20 @@ fn category(name: &str, warm_up: WarmUp) { (true, Some(issue)) => fixed.push((key, issue.clone())), (false, Some(issue)) => known_failed.push((key, issue.clone())), (false, None) => { - regressions.push(format!( + let mut report = format!( "{key}\n expected (CPython, exit {}):\n{}\n got (zypy, exit {}):\n{}", expected.status, indent(&expected.stdout), got.status, indent(&got.stdout) - )); + ); + if !got.stderr_tail.is_empty() { + report.push_str(&format!( + "\n zypy's stderr (last {STDERR_LINES} lines):\n{}", + indent(&got.stderr_tail) + )); + } + regressions.push(report); } } } diff --git a/crates/zyntax_python/tests/memory_pressure.rs b/crates/zyntax_python/tests/memory_pressure.rs index c8d26599..656b3c57 100644 --- a/crates/zyntax_python/tests/memory_pressure.rs +++ b/crates/zyntax_python/tests/memory_pressure.rs @@ -7,7 +7,10 @@ //! run twice, at a small count and at one many times larger; the //! difference in peak resident memory between the two is what the run //! failed to release. A bounded program grows by a slab or two; a leak -//! grows by the count. +//! grows by the count. A program that grows past the allowance runs a +//! third time, longer again: the growth a warm-up leaves stops by the +//! second run, and only a program that grows past the allowance again +//! leaks. //! //! `pressure/KNOWN_LEAKS` lists the programs known to grow, each with //! the git-bug issue that tracks why. A known leak that stops growing @@ -38,6 +41,8 @@ const LARGE: u64 = 400_000; /// makes has landed in both. const SMALL_LLVM: u64 = 100_000; const LARGE_LLVM: u64 = 1_000_000; +/// How many times the long run's steps the confirming run takes. +const CONFIRM_FACTOR: u64 = 4; /// Growth a bounded program is allowed between the two: the slabs its /// allocator takes, the code the long run compiles that the short one /// interprets, and the bodies kept for later tiers, all of which stop @@ -150,9 +155,29 @@ fn peak_memory_does_not_grow_with_the_step_count() { )); continue; } - let growth = large.saturating_sub(small); - let leaks = growth > ALLOWED_GROWTH; - let per_step = growth / (large_steps - small_steps); + let mut growth = large.saturating_sub(small); + let mut per_step = growth / (large_steps - small_steps); + let mut leaks = growth > ALLOWED_GROWTH; + if leaks { + let confirm_steps = large_steps * CONFIRM_FACTOR; + let (confirm_status, confirm) = run(program, confirm_steps); + if confirm_status != 0 { + failures.push(format!( + "{name}: exited {confirm_status} at {confirm_steps} steps" + )); + continue; + } + let further = confirm.saturating_sub(large); + eprintln!( + " {name}: {} MB to {} MB, then {} MB at {confirm_steps} steps", + small >> 20, + large >> 20, + confirm >> 20 + ); + leaks = further > ALLOWED_GROWTH; + growth = further; + per_step = further / (confirm_steps - large_steps); + } match (leaks, known.get(&name)) { (true, Some(issue)) => { known_count += 1;