mirror of
https://gitlab.computer.surgery/matrix/grapevine.git
synced 2025-12-18 16:21:24 +01:00
write complement test result summary to a tsv file
This commit is contained in:
parent
e4e224f5dc
commit
b3b1d43d8e
3 changed files with 151 additions and 18 deletions
|
|
@ -1,9 +1,13 @@
|
||||||
use std::path::PathBuf;
|
use std::{
|
||||||
|
fs::{self},
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
use miette::{IntoDiagnostic, Result, WrapErr};
|
use miette::{miette, IntoDiagnostic, Result, WrapErr};
|
||||||
use xshell::{cmd, Shell};
|
use xshell::{cmd, Shell};
|
||||||
|
|
||||||
mod docker;
|
mod docker;
|
||||||
|
mod summary;
|
||||||
mod test2json;
|
mod test2json;
|
||||||
|
|
||||||
use self::{
|
use self::{
|
||||||
|
|
@ -12,23 +16,59 @@ use self::{
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(clap::Args)]
|
#[derive(clap::Args)]
|
||||||
pub(crate) struct Args;
|
pub(crate) struct Args {
|
||||||
|
/// Directory to write test results
|
||||||
|
///
|
||||||
|
/// This directory will be created automatically, but it must be empty.
|
||||||
|
/// If it exists and is not empty, an error will be returned.
|
||||||
|
#[clap(short, long)]
|
||||||
|
out: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
pub(crate) fn main(_args: Args) -> Result<()> {
|
pub(crate) fn main(args: Args) -> Result<()> {
|
||||||
let sh = Shell::new().unwrap();
|
let sh = Shell::new().unwrap();
|
||||||
let toplevel = get_toplevel_path(&sh)
|
let toplevel = get_toplevel_path(&sh)
|
||||||
.wrap_err("failed to determine repository root directory")?;
|
.wrap_err("failed to determine repository root directory")?;
|
||||||
|
create_out_dir(&args.out).wrap_err_with(|| {
|
||||||
|
format!("error initializing output directory {:?}", args.out)
|
||||||
|
})?;
|
||||||
let docker_image = load_docker_image(&sh, &toplevel).wrap_err(
|
let docker_image = load_docker_image(&sh, &toplevel).wrap_err(
|
||||||
"failed to build and load complement-grapevine docker image",
|
"failed to build and load complement-grapevine docker image",
|
||||||
)?;
|
)?;
|
||||||
let test_count = count_complement_tests(&sh, &docker_image)
|
let test_count = count_complement_tests(&sh, &docker_image)
|
||||||
.wrap_err("failed to determine total complement test count")?;
|
.wrap_err("failed to determine total complement test count")?;
|
||||||
run_complement(&sh, &docker_image, test_count)
|
run_complement(&sh, &args.out, &docker_image, test_count)
|
||||||
.wrap_err("failed to run complement tests")?;
|
.wrap_err("failed to run complement tests")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ensures that output directory exists and is empty
|
||||||
|
///
|
||||||
|
/// If the directory does not exist, it will be created. If it is not empty, an
|
||||||
|
/// error will be returned.
|
||||||
|
///
|
||||||
|
/// We have no protection against concurrent programs modifying the contents of
|
||||||
|
/// the directory while the complement wrapper tool is running.
|
||||||
|
fn create_out_dir(out: &Path) -> Result<()> {
|
||||||
|
fs::create_dir_all(out)
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err("error creating output directory")?;
|
||||||
|
let mut entries = fs::read_dir(out)
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err("error checking current contents of output directory")?;
|
||||||
|
if entries.next().is_some() {
|
||||||
|
return Err(miette!(
|
||||||
|
"output directory is not empty. Refusing to run, instead of \
|
||||||
|
possibly overwriting existing files."
|
||||||
|
));
|
||||||
|
}
|
||||||
|
fs::create_dir(out.join("logs"))
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err("error creating logs subdirectory in output directory")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the path to the repository root
|
/// Returns the path to the repository root
|
||||||
fn get_toplevel_path(sh: &Shell) -> Result<PathBuf> {
|
fn get_toplevel_path(sh: &Shell) -> Result<PathBuf> {
|
||||||
let path =
|
let path =
|
||||||
|
|
|
||||||
50
xtask/src/complement/summary.rs
Normal file
50
xtask/src/complement/summary.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
//! Functions for working with the `summary.tsv` files emitted by the complement
|
||||||
|
//! wrapper.
|
||||||
|
//!
|
||||||
|
//! This file is a TSV containing test names and results for each test in a
|
||||||
|
//! complement run.
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
collections::BTreeMap,
|
||||||
|
io::{BufWriter, Write},
|
||||||
|
};
|
||||||
|
|
||||||
|
use miette::{IntoDiagnostic, Result};
|
||||||
|
|
||||||
|
use super::test2json::TestResult;
|
||||||
|
|
||||||
|
pub(crate) type TestResults = BTreeMap<String, TestResult>;
|
||||||
|
|
||||||
|
/// Escape a string value for use in a TSV file.
|
||||||
|
///
|
||||||
|
/// According to the [tsv spec][1], the only characters that need to be escaped
|
||||||
|
/// are `\n`, `\t`, `\r`, and `\`.
|
||||||
|
///
|
||||||
|
/// [1]: https://www.loc.gov/preservation/digital/formats/fdd/fdd000533.shtml
|
||||||
|
fn escape_tsv_value(value: &str) -> String {
|
||||||
|
value
|
||||||
|
.replace('\\', "\\\\")
|
||||||
|
.replace('\n', "\\n")
|
||||||
|
.replace('\t', "\\t")
|
||||||
|
.replace('\r', "\\r")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write a test result summary to a writer.
|
||||||
|
pub(crate) fn write_summary<W: Write>(
|
||||||
|
w: &mut BufWriter<W>,
|
||||||
|
summary: &TestResults,
|
||||||
|
) -> Result<()> {
|
||||||
|
// Write header line
|
||||||
|
writeln!(w, "test\tresult").into_diagnostic()?;
|
||||||
|
// Write rows
|
||||||
|
for (test, result) in summary {
|
||||||
|
writeln!(
|
||||||
|
w,
|
||||||
|
"{}\t{}",
|
||||||
|
escape_tsv_value(test),
|
||||||
|
escape_tsv_value(&result.to_string())
|
||||||
|
)
|
||||||
|
.into_diagnostic()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,10 @@
|
||||||
//! [test2json]: https://pkg.go.dev/cmd/test2json@go1.22.4
|
//! [test2json]: https://pkg.go.dev/cmd/test2json@go1.22.4
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
io::{BufRead, BufReader},
|
collections::BTreeMap,
|
||||||
|
fs::File,
|
||||||
|
io::{BufRead, BufReader, BufWriter, Seek, SeekFrom, Write},
|
||||||
|
path::Path,
|
||||||
process::{Command, Stdio},
|
process::{Command, Stdio},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
@ -14,6 +17,8 @@ use serde::Deserialize;
|
||||||
use strum::Display;
|
use strum::Display;
|
||||||
use xshell::{cmd, Shell};
|
use xshell::{cmd, Shell};
|
||||||
|
|
||||||
|
use super::summary::{write_summary, TestResults};
|
||||||
|
|
||||||
/// Returns the total number of complement tests that will be run
|
/// Returns the total number of complement tests that will be run
|
||||||
///
|
///
|
||||||
/// This is only able to count toplevel tests, and will not included subtests
|
/// This is only able to count toplevel tests, and will not included subtests
|
||||||
|
|
@ -35,6 +40,7 @@ pub(crate) fn count_complement_tests(
|
||||||
/// Runs complement test suite
|
/// Runs complement test suite
|
||||||
pub(crate) fn run_complement(
|
pub(crate) fn run_complement(
|
||||||
sh: &Shell,
|
sh: &Shell,
|
||||||
|
out: &Path,
|
||||||
docker_image: &str,
|
docker_image: &str,
|
||||||
test_count: u64,
|
test_count: u64,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
|
@ -56,12 +62,12 @@ pub(crate) fn run_complement(
|
||||||
.expect("child process spawned with piped stdout should have stdout");
|
.expect("child process spawned with piped stdout should have stdout");
|
||||||
let lines = BufReader::new(stdout).lines();
|
let lines = BufReader::new(stdout).lines();
|
||||||
|
|
||||||
let mut ctx = TestContext::new(test_count);
|
let mut ctx = TestContext::new(out, test_count)?;
|
||||||
for line in lines {
|
for line in lines {
|
||||||
let line = line
|
let line = line
|
||||||
.into_diagnostic()
|
.into_diagnostic()
|
||||||
.wrap_err("error reading output from complement process")?;
|
.wrap_err("error reading output from complement process")?;
|
||||||
ctx.handle_line(&line);
|
ctx.handle_line(&line)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -95,7 +101,7 @@ enum GoTestEvent {
|
||||||
|
|
||||||
#[derive(Copy, Clone, Display, Debug)]
|
#[derive(Copy, Clone, Display, Debug)]
|
||||||
#[strum(serialize_all = "UPPERCASE")]
|
#[strum(serialize_all = "UPPERCASE")]
|
||||||
enum TestResult {
|
pub(crate) enum TestResult {
|
||||||
Pass,
|
Pass,
|
||||||
Fail,
|
Fail,
|
||||||
Skip,
|
Skip,
|
||||||
|
|
@ -106,6 +112,13 @@ struct TestContext {
|
||||||
pass_count: u64,
|
pass_count: u64,
|
||||||
fail_count: u64,
|
fail_count: u64,
|
||||||
skip_count: u64,
|
skip_count: u64,
|
||||||
|
// We do not need a specific method to flush this before dropping
|
||||||
|
// `TestContext`, because the file is only written from the
|
||||||
|
// `update_summary_file` method. This method always calls flush on
|
||||||
|
// a non-error path, and the file is left in an inconsistent state on an
|
||||||
|
// error anyway.
|
||||||
|
summary_file: BufWriter<File>,
|
||||||
|
results: TestResults,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a string to use for displaying a test name
|
/// Returns a string to use for displaying a test name
|
||||||
|
|
@ -132,7 +145,7 @@ fn test_is_toplevel(test: &str) -> bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TestContext {
|
impl TestContext {
|
||||||
fn new(test_count: u64) -> TestContext {
|
fn new(out: &Path, test_count: u64) -> Result<TestContext> {
|
||||||
// TODO: figure out how to display ETA without it fluctuating wildly.
|
// TODO: figure out how to display ETA without it fluctuating wildly.
|
||||||
let style = ProgressStyle::with_template(
|
let style = ProgressStyle::with_template(
|
||||||
"({msg}) {pos}/{len} [{elapsed}] {wide_bar}",
|
"({msg}) {pos}/{len} [{elapsed}] {wide_bar}",
|
||||||
|
|
@ -141,14 +154,23 @@ impl TestContext {
|
||||||
.progress_chars("##-");
|
.progress_chars("##-");
|
||||||
let pb = ProgressBar::new(test_count).with_style(style);
|
let pb = ProgressBar::new(test_count).with_style(style);
|
||||||
pb.enable_steady_tick(Duration::from_secs(1));
|
pb.enable_steady_tick(Duration::from_secs(1));
|
||||||
|
|
||||||
|
let summary_file = File::create(out.join("summary.tsv"))
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err("failed to create summary file in output dir")?;
|
||||||
|
let summary_file = BufWriter::new(summary_file);
|
||||||
|
|
||||||
let ctx = TestContext {
|
let ctx = TestContext {
|
||||||
pb,
|
pb,
|
||||||
pass_count: 0,
|
pass_count: 0,
|
||||||
fail_count: 0,
|
fail_count: 0,
|
||||||
skip_count: 0,
|
skip_count: 0,
|
||||||
|
summary_file,
|
||||||
|
results: BTreeMap::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
ctx.update_progress();
|
ctx.update_progress();
|
||||||
ctx
|
Ok(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_progress(&self) {
|
fn update_progress(&self) {
|
||||||
|
|
@ -160,8 +182,25 @@ impl TestContext {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_test_result(&mut self, test: &str, result: TestResult) {
|
fn update_summary_file(&mut self) -> Result<()> {
|
||||||
|
// Truncate the file to clear existing contents
|
||||||
|
self.summary_file
|
||||||
|
.get_mut()
|
||||||
|
.seek(SeekFrom::Start(0))
|
||||||
|
.into_diagnostic()?;
|
||||||
|
self.summary_file.get_mut().set_len(0).into_diagnostic()?;
|
||||||
|
write_summary(&mut self.summary_file, &self.results)?;
|
||||||
|
self.summary_file.flush().into_diagnostic()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_test_result(
|
||||||
|
&mut self,
|
||||||
|
test: &str,
|
||||||
|
result: TestResult,
|
||||||
|
) -> Result<()> {
|
||||||
self.pb.println(format!("=== {result}\t{test}"));
|
self.pb.println(format!("=== {result}\t{test}"));
|
||||||
|
self.results.insert(test.to_owned(), result);
|
||||||
// 'complement.test -test.list' is only able to count toplevel tests
|
// 'complement.test -test.list' is only able to count toplevel tests
|
||||||
// ahead-of-time, so we don't include subtests in the pass/fail/skip
|
// ahead-of-time, so we don't include subtests in the pass/fail/skip
|
||||||
// counts.
|
// counts.
|
||||||
|
|
@ -173,9 +212,11 @@ impl TestContext {
|
||||||
}
|
}
|
||||||
self.update_progress();
|
self.update_progress();
|
||||||
}
|
}
|
||||||
|
self.update_summary_file().wrap_err("error writing summary file")?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_event(&mut self, event: GoTestEvent) {
|
fn handle_event(&mut self, event: GoTestEvent) -> Result<()> {
|
||||||
match event {
|
match event {
|
||||||
GoTestEvent::OtherAction => (),
|
GoTestEvent::OtherAction => (),
|
||||||
GoTestEvent::Run {
|
GoTestEvent::Run {
|
||||||
|
|
@ -186,25 +227,26 @@ impl TestContext {
|
||||||
GoTestEvent::Pass {
|
GoTestEvent::Pass {
|
||||||
test,
|
test,
|
||||||
} => {
|
} => {
|
||||||
self.handle_test_result(test_str(&test), TestResult::Pass);
|
self.handle_test_result(test_str(&test), TestResult::Pass)?;
|
||||||
}
|
}
|
||||||
GoTestEvent::Fail {
|
GoTestEvent::Fail {
|
||||||
test,
|
test,
|
||||||
} => {
|
} => {
|
||||||
self.handle_test_result(test_str(&test), TestResult::Fail);
|
self.handle_test_result(test_str(&test), TestResult::Fail)?;
|
||||||
}
|
}
|
||||||
GoTestEvent::Skip {
|
GoTestEvent::Skip {
|
||||||
test,
|
test,
|
||||||
} => {
|
} => {
|
||||||
self.handle_test_result(test_str(&test), TestResult::Skip);
|
self.handle_test_result(test_str(&test), TestResult::Skip)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Processes a line of output from `test2json`
|
/// Processes a line of output from `test2json`
|
||||||
fn handle_line(&mut self, line: &str) {
|
fn handle_line(&mut self, line: &str) -> Result<()> {
|
||||||
match serde_json::from_str(line) {
|
match serde_json::from_str(line) {
|
||||||
Ok(event) => self.handle_event(event),
|
Ok(event) => self.handle_event(event)?,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let label =
|
let label =
|
||||||
LabeledSpan::at_offset(e.column() - 1, "error here");
|
LabeledSpan::at_offset(e.column() - 1, "error here");
|
||||||
|
|
@ -217,5 +259,6 @@ impl TestContext {
|
||||||
eprintln!("{report:?}");
|
eprintln!("{report:?}");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue