Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parallelize --test ui #1318

Merged
merged 2 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading