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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
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]]
|
[[package]]
|
||||||
name = "const-oid"
|
name = "const-oid"
|
||||||
version = "0.9.6"
|
version = "0.9.6"
|
||||||
|
|
@ -609,6 +622,12 @@ version = "1.13.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
|
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encode_unicode"
|
||||||
|
version = "0.3.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "enum-as-inner"
|
name = "enum-as-inner"
|
||||||
version = "0.6.1"
|
version = "0.6.1"
|
||||||
|
|
@ -1159,6 +1178,28 @@ dependencies = [
|
||||||
"serde",
|
"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]]
|
[[package]]
|
||||||
name = "ipconfig"
|
name = "ipconfig"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
|
|
@ -1558,6 +1599,12 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "number_prefix"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "object"
|
name = "object"
|
||||||
version = "0.36.4"
|
version = "0.36.4"
|
||||||
|
|
@ -1831,6 +1878,12 @@ dependencies = [
|
||||||
"miniz_oxide 0.7.4",
|
"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]]
|
[[package]]
|
||||||
name = "powerfmt"
|
name = "powerfmt"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
@ -3812,7 +3865,11 @@ name = "xtask"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
|
"indicatif",
|
||||||
"miette",
|
"miette",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"strum",
|
||||||
"xshell",
|
"xshell",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,7 @@ http-body-util = "0.1.2"
|
||||||
hyper = "1.4.1"
|
hyper = "1.4.1"
|
||||||
hyper-util = { version = "0.1.8", features = ["client", "client-legacy", "service"] }
|
hyper-util = { version = "0.1.8", features = ["client", "client-legacy", "service"] }
|
||||||
image = { version = "0.25.2", default-features = false, features = ["jpeg", "png", "gif"] }
|
image = { version = "0.25.2", default-features = false, features = ["jpeg", "png", "gif"] }
|
||||||
|
indicatif = "0.17.8"
|
||||||
jsonwebtoken = "9.3.0"
|
jsonwebtoken = "9.3.0"
|
||||||
lru-cache = "0.1.2"
|
lru-cache = "0.1.2"
|
||||||
miette = { version = "7.2.0", features = ["fancy"] }
|
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"] }
|
rusqlite = { version = "0.32.1", features = ["bundled"] }
|
||||||
rustls = { version = "0.23.13", default-features = false, features = ["ring", "log", "logging", "std", "tls12"] }
|
rustls = { version = "0.23.13", default-features = false, features = ["ring", "log", "logging", "std", "tls12"] }
|
||||||
sd-notify = { version = "0.4.2" }
|
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_html_form = "0.2.6"
|
||||||
serde_json = { version = "1.0.128", features = ["raw_value"] }
|
serde_json = { version = "1.0.128", features = ["raw_value"] }
|
||||||
serde_yaml = "0.9.34"
|
serde_yaml = "0.9.34"
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,10 @@ rust-version.workspace = true
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
miette.workspace = true
|
miette.workspace = true
|
||||||
|
indicatif.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
strum.workspace = true
|
||||||
xshell.workspace = true
|
xshell.workspace = true
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,10 @@ use xshell::{cmd, Shell};
|
||||||
mod docker;
|
mod docker;
|
||||||
mod test2json;
|
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)]
|
#[derive(clap::Args)]
|
||||||
pub(crate) struct 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(
|
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",
|
||||||
)?;
|
)?;
|
||||||
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")?;
|
.wrap_err("failed to run complement tests")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,220 @@
|
||||||
//!
|
//!
|
||||||
//! [test2json]: https://pkg.go.dev/cmd/test2json@go1.22.4
|
//! [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};
|
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
|
/// 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: handle SIG{INT,TERM}
|
||||||
// TODO: XTASK_PATH variable, so that we don't need to pollute devshell with
|
// TODO: XTASK_PATH variable, so that we don't need to pollute devshell with
|
||||||
// go
|
// 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_BASE_IMAGE", docker_image)
|
||||||
.env("COMPLEMENT_SPAWN_HS_TIMEOUT", "5")
|
.env("COMPLEMENT_SPAWN_HS_TIMEOUT", "5")
|
||||||
.env("COMPLEMENT_ALWAYS_PRINT_SERVER_LOGS", "1")
|
.env("COMPLEMENT_ALWAYS_PRINT_SERVER_LOGS", "1");
|
||||||
.run()
|
eprintln!("$ {cmd}");
|
||||||
|
let child = Command::from(cmd)
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.spawn()
|
||||||
.into_diagnostic()
|
.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