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
57
Cargo.lock
generated
57
Cargo.lock
generated
|
|
@ -453,6 +453,19 @@ version = "1.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.15.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb"
|
||||
dependencies = [
|
||||
"encode_unicode",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"unicode-width",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.9.6"
|
||||
|
|
@ -609,6 +622,12 @@ version = "1.13.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
||||
|
||||
[[package]]
|
||||
name = "enum-as-inner"
|
||||
version = "0.6.1"
|
||||
|
|
@ -1159,6 +1178,28 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indicatif"
|
||||
version = "0.17.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3"
|
||||
dependencies = [
|
||||
"console",
|
||||
"instant",
|
||||
"number_prefix",
|
||||
"portable-atomic",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipconfig"
|
||||
version = "0.3.2"
|
||||
|
|
@ -1558,6 +1599,12 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "number_prefix"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.36.4"
|
||||
|
|
@ -1831,6 +1878,12 @@ dependencies = [
|
|||
"miniz_oxide 0.7.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0"
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
|
|
@ -3812,7 +3865,11 @@ name = "xtask"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"indicatif",
|
||||
"miette",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"xshell",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ http-body-util = "0.1.2"
|
|||
hyper = "1.4.1"
|
||||
hyper-util = { version = "0.1.8", features = ["client", "client-legacy", "service"] }
|
||||
image = { version = "0.25.2", default-features = false, features = ["jpeg", "png", "gif"] }
|
||||
indicatif = "0.17.8"
|
||||
jsonwebtoken = "9.3.0"
|
||||
lru-cache = "0.1.2"
|
||||
miette = { version = "7.2.0", features = ["fancy"] }
|
||||
|
|
@ -120,7 +121,7 @@ ruma = { git = "https://github.com/ruma/ruma", branch = "main", features = ["com
|
|||
rusqlite = { version = "0.32.1", features = ["bundled"] }
|
||||
rustls = { version = "0.23.13", default-features = false, features = ["ring", "log", "logging", "std", "tls12"] }
|
||||
sd-notify = { version = "0.4.2" }
|
||||
serde = { version = "1.0.210", features = ["rc"] }
|
||||
serde = { version = "1.0.210", features = ["rc", "derive"] }
|
||||
serde_html_form = "0.2.6"
|
||||
serde_json = { version = "1.0.128", features = ["raw_value"] }
|
||||
serde_yaml = "0.9.34"
|
||||
|
|
|
|||
|
|
@ -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