mirror of
https://gitlab.computer.surgery/matrix/grapevine.git
synced 2025-12-18 16:21:24 +01:00
add live progress display to complement wrapper
Added the `derive` feature to the workspace serde dependency here. Previously, the dependency was only used in the main package, which ended up enabling the `derive` feature through transitive serde dependencies. This is not the case for xtask, so we need to enable it explicitly.
This commit is contained in:
parent
ef6eb27b9b
commit
e4e224f5dc
5 changed files with 277 additions and 8 deletions
|
|
@ -8,6 +8,10 @@ rust-version.workspace = true
|
|||
[dependencies]
|
||||
clap.workspace = true
|
||||
miette.workspace = true
|
||||
indicatif.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
strum.workspace = true
|
||||
xshell.workspace = true
|
||||
|
||||
[lints]
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ use xshell::{cmd, Shell};
|
|||
mod docker;
|
||||
mod test2json;
|
||||
|
||||
use self::{docker::load_docker_image, test2json::run_complement};
|
||||
use self::{
|
||||
docker::load_docker_image,
|
||||
test2json::{count_complement_tests, run_complement},
|
||||
};
|
||||
|
||||
#[derive(clap::Args)]
|
||||
pub(crate) struct Args;
|
||||
|
|
@ -19,7 +22,9 @@ pub(crate) fn main(_args: Args) -> Result<()> {
|
|||
let docker_image = load_docker_image(&sh, &toplevel).wrap_err(
|
||||
"failed to build and load complement-grapevine docker image",
|
||||
)?;
|
||||
run_complement(&sh, &docker_image)
|
||||
let test_count = count_complement_tests(&sh, &docker_image)
|
||||
.wrap_err("failed to determine total complement test count")?;
|
||||
run_complement(&sh, &docker_image, test_count)
|
||||
.wrap_err("failed to run complement tests")?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,18 +2,220 @@
|
|||
//!
|
||||
//! [test2json]: https://pkg.go.dev/cmd/test2json@go1.22.4
|
||||
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
use std::{
|
||||
io::{BufRead, BufReader},
|
||||
process::{Command, Stdio},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use miette::{miette, IntoDiagnostic, LabeledSpan, Result, WrapErr};
|
||||
use serde::Deserialize;
|
||||
use strum::Display;
|
||||
use xshell::{cmd, Shell};
|
||||
|
||||
/// Returns the total number of complement tests that will be run
|
||||
///
|
||||
/// This is only able to count toplevel tests, and will not included subtests
|
||||
/// (`A/B`)
|
||||
pub(crate) fn count_complement_tests(
|
||||
sh: &Shell,
|
||||
docker_image: &str,
|
||||
) -> Result<u64> {
|
||||
let test_list = cmd!(sh, "go tool test2json complement.test -test.list .*")
|
||||
.env("COMPLEMENT_BASE_IMAGE", docker_image)
|
||||
.read()
|
||||
.into_diagnostic()?;
|
||||
let test_count = u64::try_from(test_list.lines().count())
|
||||
.into_diagnostic()
|
||||
.wrap_err("test count overflowed u64")?;
|
||||
Ok(test_count)
|
||||
}
|
||||
|
||||
/// Runs complement test suite
|
||||
pub(crate) fn run_complement(sh: &Shell, docker_image: &str) -> Result<()> {
|
||||
pub(crate) fn run_complement(
|
||||
sh: &Shell,
|
||||
docker_image: &str,
|
||||
test_count: u64,
|
||||
) -> Result<()> {
|
||||
// TODO: handle SIG{INT,TERM}
|
||||
// TODO: XTASK_PATH variable, so that we don't need to pollute devshell with
|
||||
// go
|
||||
cmd!(sh, "go tool test2json complement.test -test.v=test2json")
|
||||
let cmd = cmd!(sh, "go tool test2json complement.test -test.v=test2json")
|
||||
.env("COMPLEMENT_BASE_IMAGE", docker_image)
|
||||
.env("COMPLEMENT_SPAWN_HS_TIMEOUT", "5")
|
||||
.env("COMPLEMENT_ALWAYS_PRINT_SERVER_LOGS", "1")
|
||||
.run()
|
||||
.env("COMPLEMENT_ALWAYS_PRINT_SERVER_LOGS", "1");
|
||||
eprintln!("$ {cmd}");
|
||||
let child = Command::from(cmd)
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
.into_diagnostic()
|
||||
.wrap_err("error spawning complement process")?;
|
||||
let stdout = child
|
||||
.stdout
|
||||
.expect("child process spawned with piped stdout should have stdout");
|
||||
let lines = BufReader::new(stdout).lines();
|
||||
|
||||
let mut ctx = TestContext::new(test_count);
|
||||
for line in lines {
|
||||
let line = line
|
||||
.into_diagnostic()
|
||||
.wrap_err("error reading output from complement process")?;
|
||||
ctx.handle_line(&line);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Schema from <https://pkg.go.dev/cmd/test2json#hdr-Output_Format>
|
||||
///
|
||||
/// Only the fields that we need are included here.
|
||||
#[derive(Deserialize)]
|
||||
#[serde(
|
||||
rename_all = "snake_case",
|
||||
rename_all_fields = "PascalCase",
|
||||
tag = "Action"
|
||||
)]
|
||||
enum GoTestEvent {
|
||||
Run {
|
||||
test: Option<String>,
|
||||
},
|
||||
Pass {
|
||||
test: Option<String>,
|
||||
},
|
||||
Fail {
|
||||
test: Option<String>,
|
||||
},
|
||||
Skip {
|
||||
test: Option<String>,
|
||||
},
|
||||
#[serde(other)]
|
||||
OtherAction,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Display, Debug)]
|
||||
#[strum(serialize_all = "UPPERCASE")]
|
||||
enum TestResult {
|
||||
Pass,
|
||||
Fail,
|
||||
Skip,
|
||||
}
|
||||
|
||||
struct TestContext {
|
||||
pb: ProgressBar,
|
||||
pass_count: u64,
|
||||
fail_count: u64,
|
||||
skip_count: u64,
|
||||
}
|
||||
|
||||
/// Returns a string to use for displaying a test name
|
||||
///
|
||||
/// From the test2json docs:
|
||||
///
|
||||
/// > The Test field, if present, specifies the test, example, or benchmark
|
||||
/// > function that caused the event. Events for the overall package test do not
|
||||
/// > set Test.
|
||||
///
|
||||
/// For events that do not have a `Test` field, we display their test name as
|
||||
/// `"GLOBAL"` instead.
|
||||
fn test_str(test: &Option<String>) -> &str {
|
||||
if let Some(test) = test {
|
||||
test
|
||||
} else {
|
||||
"GLOBAL"
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether a test name is a toplevel test (as opposed to a subtest)
|
||||
fn test_is_toplevel(test: &str) -> bool {
|
||||
!test.contains('/')
|
||||
}
|
||||
|
||||
impl TestContext {
|
||||
fn new(test_count: u64) -> TestContext {
|
||||
// TODO: figure out how to display ETA without it fluctuating wildly.
|
||||
let style = ProgressStyle::with_template(
|
||||
"({msg}) {pos}/{len} [{elapsed}] {wide_bar}",
|
||||
)
|
||||
.expect("static progress bar template should be valid")
|
||||
.progress_chars("##-");
|
||||
let pb = ProgressBar::new(test_count).with_style(style);
|
||||
pb.enable_steady_tick(Duration::from_secs(1));
|
||||
let ctx = TestContext {
|
||||
pb,
|
||||
pass_count: 0,
|
||||
fail_count: 0,
|
||||
skip_count: 0,
|
||||
};
|
||||
ctx.update_progress();
|
||||
ctx
|
||||
}
|
||||
|
||||
fn update_progress(&self) {
|
||||
self.pb
|
||||
.set_position(self.pass_count + self.fail_count + self.skip_count);
|
||||
self.pb.set_message(format!(
|
||||
"PASS {}, FAIL {}, SKIP {}",
|
||||
self.pass_count, self.fail_count, self.skip_count
|
||||
));
|
||||
}
|
||||
|
||||
fn handle_test_result(&mut self, test: &str, result: TestResult) {
|
||||
self.pb.println(format!("=== {result}\t{test}"));
|
||||
// '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
|
||||
// counts.
|
||||
if test_is_toplevel(test) {
|
||||
match result {
|
||||
TestResult::Pass => self.pass_count += 1,
|
||||
TestResult::Fail => self.fail_count += 1,
|
||||
TestResult::Skip => self.skip_count += 1,
|
||||
}
|
||||
self.update_progress();
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_event(&mut self, event: GoTestEvent) {
|
||||
match event {
|
||||
GoTestEvent::OtherAction => (),
|
||||
GoTestEvent::Run {
|
||||
test,
|
||||
} => {
|
||||
self.pb.println(format!("=== RUN \t{}", test_str(&test)));
|
||||
}
|
||||
GoTestEvent::Pass {
|
||||
test,
|
||||
} => {
|
||||
self.handle_test_result(test_str(&test), TestResult::Pass);
|
||||
}
|
||||
GoTestEvent::Fail {
|
||||
test,
|
||||
} => {
|
||||
self.handle_test_result(test_str(&test), TestResult::Fail);
|
||||
}
|
||||
GoTestEvent::Skip {
|
||||
test,
|
||||
} => {
|
||||
self.handle_test_result(test_str(&test), TestResult::Skip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes a line of output from `test2json`
|
||||
fn handle_line(&mut self, line: &str) {
|
||||
match serde_json::from_str(line) {
|
||||
Ok(event) => self.handle_event(event),
|
||||
Err(e) => {
|
||||
let label =
|
||||
LabeledSpan::at_offset(e.column() - 1, "error here");
|
||||
let report = miette!(labels = vec![label], "{e}",)
|
||||
.with_source_code(line.to_owned())
|
||||
.wrap_err(
|
||||
"failed to parse go test2json event from complement \
|
||||
tests. Ignoring this event.",
|
||||
);
|
||||
eprintln!("{report:?}");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue