Skip to content

Commit

Permalink
Parallelize --test ui (#1318)
Browse files Browse the repository at this point in the history
  • Loading branch information
jhjourdan authored Jan 17, 2025
2 parents 9645e8a + 5e132fa commit d4faf14
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 84 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions creusot/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ arraydeque = "0.4"
creusot-contracts = { path = "../creusot-contracts", features = ["typechecker"] }
escargot = { version = "0.5" }
creusot-setup = { path = "../creusot-setup" }
libc = "0.2"

[[test]]
name = "ui"
Expand Down
3 changes: 0 additions & 3 deletions creusot/tests/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ pub fn differ(
let output = err.as_output().unwrap();

write!(buf, "{}", from_utf8(&output.stderr)?)?;
// let success = compare_str(&mut buf, from_utf8(&output.stderr)?, from_utf8(expect_err)?);
Ok((false, buf))
}
Err(err) => {
Expand Down Expand Up @@ -75,7 +74,6 @@ fn compare_str(buf: &mut Buffer, got: &str, expect: &str) -> bool {
.algorithm(Algorithm::Patience)
.diff_lines(&expect, &got);

// let result = TextDiff::from_lines(expect, got);
if result.ratio() == 1.0 {
buf.set_color(ColorSpec::new().set_fg(Some(Color::Yellow))).unwrap();
write!(buf, " <Differences in spans and line ending only.>").unwrap();
Expand Down Expand Up @@ -125,7 +123,6 @@ fn normalize_cargo_paths(input: &str) -> String {
}

fn print_diff<'a, W: WriteColor>(mut buf: W, diff: TextDiff<'a, 'a, 'a, str>) {
// let mut last_lines: ArrayDeque<[_; 3], Wrapping> = ArrayDeque::new();
let mut multiple_diffs = false;

for ops in diff.grouped_ops(3) {
Expand Down
228 changes: 147 additions & 81 deletions creusot/tests/ui.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,32 @@
use clap::Parser;
use libc::{c_ushort, ioctl, STDOUT_FILENO, TIOCGWINSZ};
use std::{
env,
fs::File,
io::{BufRead, BufReader, IsTerminal, Write},
path::{Path, PathBuf},
process::Command,
sync::{
atomic::{self, AtomicUsize},
Mutex,
},
thread,
};
use termcolor::*;

mod diff;
use diff::{differ, normalize_file_path};

/// Used to query the size of the terminal
#[derive(Default)]
#[repr(C)]
struct TermSize {
row: c_ushort,
col: c_ushort,
x: c_ushort,
y: c_ushort,
}

#[derive(Debug, Parser)]
struct Args {
/// Suppress all output other than failing test cases
Expand Down Expand Up @@ -153,14 +169,14 @@ fn run_creusot(

fn should_succeed<B>(s: &str, args: &Args, b: B)
where
B: Fn(&Path) -> Option<std::process::Command>,
B: Fn(&Path) -> Option<std::process::Command> + Send + Sync,
{
glob_runner(s, args, b, true);
}

fn should_fail<B>(s: &str, args: &Args, b: B)
where
B: Fn(&Path) -> Option<std::process::Command>,
B: Fn(&Path) -> Option<std::process::Command> + Send + Sync,
{
glob_runner(s, args, b, false);
}
Expand All @@ -180,106 +196,156 @@ fn erase_global_paths(s: &mut Vec<u8>) {

fn glob_runner<B>(s: &str, args: &Args, command_builder: B, should_succeed: bool)
where
B: Fn(&Path) -> Option<std::process::Command>,
B: Fn(&Path) -> Option<std::process::Command> + Send + Sync,
{
let is_tty = std::io::stdout().is_terminal();
let mut out = StandardStream::stdout(if args.force_color || is_tty {
let out = StandardStream::stdout(if args.force_color || is_tty {
ColorChoice::Always
} else {
ColorChoice::Never
});

let mut test_count = 0;
let mut test_failures = 0;

for entry in glob::glob(s).expect("Failed to read glob pattern") {
test_count += 1;
let entry = entry.unwrap();

if let Some(ref filter) = args.filter {
if !entry.to_str().map(|entry| entry.contains(filter)).unwrap_or(false) {
continue;
let test_count = AtomicUsize::new(0);
let test_failures = AtomicUsize::new(0);

let entries = Mutex::new(glob::glob(s).expect("Failed to read glob pattern"));
let nb_threads = thread::available_parallelism().map(|n| n.into()).unwrap_or(1usize);
let out = Mutex::new((Vec::new(), out));

// Print all test currently running
let write_in_flight = |in_flight: &Vec<String>, out: &mut StandardStream| {
// get terminal width
let mut size: TermSize = TermSize::default();
unsafe { ioctl(STDOUT_FILENO, TIOCGWINSZ.into(), &mut size as *mut _) };
let width = size.col as usize;
// Save cursor position
write!(out, "\x1b7").unwrap();
let mut wrote = 0;
write!(out, "Testing: ").unwrap();
wrote += "Testing: ".len();
for (i, name) in (&*in_flight).iter().enumerate() {
if i != 0 {
write!(out, ", ").unwrap();
wrote += ", ".len();
}
}
let output = match command_builder(&entry) {
None => continue,
Some(mut c) => {
let mut o = c.output().unwrap();
// Replace global paths in stderr with (a simulacrum of) local paths
erase_global_paths(&mut o.stderr);
o
if wrote + name.len() + 5 > width {
// Do not overflow the line, or output breaks!
write!(out, "...").unwrap();
break;
}
};

let stderr = entry.with_extension("stderr");
let stdout = entry.with_extension("coma");

// Default (not `quiet`): print "Testing tests/current/test ... " and flush before running the test
// if `quiet` enabled: postpone printing, store the message in `current`, only print it if the test case fails
let mut current: &str = &format!("Testing {} ... ", entry.display());
if !args.quiet {
write!(out, "{}", current).unwrap();
current = "";
out.flush().unwrap();
write!(out, "{name}").unwrap();
wrote += name.len();
}
// restore cursor position (put it back at the beginning of the line)
write!(out, "\x1b8").unwrap();
out.flush().unwrap();
};
// erase after the cursor to the end of the screen
let erase_in_flight = |out: &mut StandardStream| write!(out, "\x1b[J").unwrap();

thread::scope(|s| {
let worker = || {
// invariant: the cursor is always at the start of the line where we should write `Testing ...`
loop {
let Some(entry) = entries.lock().unwrap().next() else {
return;
};
test_count.fetch_add(1, atomic::Ordering::SeqCst);
let entry = entry.unwrap();

if let Some(ref filter) = args.filter {
if !entry.to_str().map(|entry| entry.contains(filter)).unwrap_or(false) {
continue;
}
}

if args.bless {
let (success, _) =
differ(output.clone(), &stdout, Some(&stderr), should_succeed, is_tty).unwrap();
let entry_name = entry.file_stem().unwrap().to_str().unwrap();

let output = match command_builder(&entry) {
None => continue,
Some(mut c) => {
if !args.quiet {
let mut out = out.lock().unwrap();
let (ref mut in_flight, ref mut out) = &mut *out;
in_flight.push(entry_name.to_string());
erase_in_flight(out);
write_in_flight(in_flight, out);
}

let mut o = c.output().unwrap();
// Replace global paths in stderr with (a simulacrum of) local paths
erase_global_paths(&mut o.stderr);
o
}
};

if success {
if is_tty {
// Move to beginning of line and clear line.
write!(out, "\x1b[G\x1b[2K").unwrap();
} else {
out.set_color(ColorSpec::new().set_fg(Some(Color::Green))).unwrap();
writeln!(out, "ok").unwrap();
}
} else {
write!(out, "{current}").unwrap();
out.set_color(ColorSpec::new().set_fg(Some(Color::Blue))).unwrap();
writeln!(&mut out, "blessed").unwrap();
out.reset().unwrap();
}
let stderr = entry.with_extension("stderr");
let stdout = entry.with_extension("coma");

if output.stdout.is_empty() {
let _ = std::fs::remove_file(stdout);
} else {
std::fs::write(stdout, &output.stdout).unwrap();
}
let (success, buf) =
differ(output.clone(), &stdout, Some(&stderr), should_succeed, is_tty).unwrap();

if output.stderr.is_empty() {
let _ = std::fs::remove_file(stderr);
} else {
std::fs::write(stderr, &output.stderr).unwrap();
}
} else {
let (success, buf) =
differ(output.clone(), &stdout, Some(&stderr), should_succeed, is_tty).unwrap();
let mut out = out.lock().unwrap();
let (ref mut in_flight, ref mut out) = &mut *out;

if success {
if !args.quiet {
if is_tty {
// Move to beginning of line and clear line.
write!(out, "\x1b[G\x1b[2K").unwrap();
} else {
out.set_color(ColorSpec::new().set_fg(Some(Color::Green))).unwrap();
writeln!(out, "ok").unwrap();
if let Some(i) = in_flight.iter().position(|n| n == entry_name) {
in_flight.remove(i);
}
}
} else {
write!(out, "{current}").unwrap();
out.set_color(ColorSpec::new().set_fg(Some(Color::Red))).unwrap();
writeln!(&mut out, "failure").unwrap();

test_failures += 1;
};
out.reset().unwrap();
if args.bless {
if !success {
erase_in_flight(out);
write!(out, "{}: ", entry.display()).unwrap();
out.set_color(ColorSpec::new().set_fg(Some(Color::Blue))).unwrap();
writeln!(out, "blessed").unwrap();
out.reset().unwrap();
}

let wrt = BufferWriter::stdout(ColorChoice::Always);
wrt.print(&buf).unwrap();
if output.stdout.is_empty() {
let _ = std::fs::remove_file(stdout);
} else {
std::fs::write(stdout, &output.stdout).unwrap();
}

if output.stderr.is_empty() {
let _ = std::fs::remove_file(stderr);
} else {
std::fs::write(stderr, &output.stderr).unwrap();
}
} else {
if !success {
erase_in_flight(out);
write!(out, "{}: ", entry.display()).unwrap();
out.set_color(ColorSpec::new().set_fg(Some(Color::Red))).unwrap();
writeln!(out, "failure").unwrap();

test_failures.fetch_add(1, atomic::Ordering::SeqCst);
};
out.reset().unwrap();
out.flush().unwrap();

let wrt = BufferWriter::stdout(ColorChoice::Always);
wrt.print(&buf).unwrap();
}
if !args.quiet {
erase_in_flight(out);
if !in_flight.is_empty() {
write_in_flight(in_flight, out);
}
}
}
};
let mut handles = Vec::new();
for _ in 0..nb_threads {
handles.push(s.spawn(worker));
}
}
});

let test_count = test_count.load(atomic::Ordering::SeqCst);
let test_failures = test_failures.load(atomic::Ordering::SeqCst);
let (_, mut out) = out.into_inner().unwrap();

if test_failures > 0 {
out.set_color(ColorSpec::new().set_fg(Some(Color::Red))).unwrap();
Expand Down

0 comments on commit d4faf14

Please sign in to comment.