diff --git a/Cargo.lock b/Cargo.lock index b6f6215ab..a975a0f1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1226,6 +1226,7 @@ dependencies = [ "clio", "derive_more 1.0.0", "hugr", + "human-panic", "predicates", "rstest", "serde_json", @@ -1375,6 +1376,22 @@ dependencies = [ "pyo3", ] +[[package]] +name = "human-panic" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac63a746b187e95d51fe16850eb04d1cfef203f6af98e6c405a6f262ad3df00a" +dependencies = [ + "anstream", + "anstyle", + "backtrace", + "os_info", + "serde", + "serde_derive", + "toml", + "uuid", +] + [[package]] name = "hyper" version = "1.6.0" @@ -2014,6 +2031,18 @@ dependencies = [ "serde", ] +[[package]] +name = "os_info" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0e1ac5fde8d43c34139135df8ea9ee9465394b2d8d20f032d38998f64afffc3" +dependencies = [ + "log", + "plist", + "serde", + "windows-sys 0.52.0", +] + [[package]] name = "outref" version = "0.5.2" @@ -2145,6 +2174,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plist" +version = "1.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" +dependencies = [ + "base64", + "indexmap 2.10.0", + "quick-xml", + "serde", + "time", +] + [[package]] name = "plotters" version = "0.3.7" @@ -2398,6 +2440,15 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quick-xml" +version = "0.38.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9845d9dccf565065824e69f9f235fafba1587031eda353c1f1561cd6a6be78f4" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.40" @@ -2800,6 +2851,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3148,12 +3208,33 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "toml" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.7.0", + "toml_writer", +] + [[package]] name = "toml_datetime" version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +[[package]] +name = "toml_datetime" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" +dependencies = [ + "serde", +] + [[package]] name = "toml_edit" version = "0.22.27" @@ -3161,10 +3242,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap 2.10.0", - "toml_datetime", + "toml_datetime 0.6.11", "winnow", ] +[[package]] +name = "toml_writer" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" + [[package]] name = "tower" version = "0.5.2" @@ -3392,6 +3479,7 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" dependencies = [ + "getrandom", "js-sys", "wasm-bindgen", ] @@ -3654,6 +3742,15 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" diff --git a/hugr-cli/Cargo.toml b/hugr-cli/Cargo.toml index 37372092c..b171a0e4d 100644 --- a/hugr-cli/Cargo.toml +++ b/hugr-cli/Cargo.toml @@ -26,6 +26,7 @@ anyhow.workspace = true thiserror.workspace = true tracing = "0.1.41" tracing-subscriber = { version = "0.3.19", features = ["fmt"] } +human-panic = "2" [lints] workspace = true @@ -37,6 +38,9 @@ workspace = true enum_missing = "warn" struct_missing = "warn" +[features] +panic-test = [] + [dev-dependencies] assert_cmd = { workspace = true } assert_fs = { workspace = true } diff --git a/hugr-cli/src/main.rs b/hugr-cli/src/main.rs index 28e64020f..428edc466 100644 --- a/hugr-cli/src/main.rs +++ b/hugr-cli/src/main.rs @@ -9,6 +9,15 @@ use hugr_cli::{CliArgs, CliCommand}; use tracing::{error, metadata::LevelFilter}; fn main() { + // Enable human-panic for release builds, or when tests force it via env. + if cfg!(not(debug_assertions)) || std::env::var_os("FORCE_HUMAN_PANIC").is_some() { + human_panic::setup_panic!(); + } + + // Test-only panic trigger (harmless in production; only fires if env is set). + if std::env::var_os("PANIC_FOR_TESTS").is_some() { + panic!("triggered by test"); + } let cli_args = CliArgs::parse(); let level = match cli_args.verbose.filter() { diff --git a/hugr-cli/tests/human_panic_integration.rs b/hugr-cli/tests/human_panic_integration.rs new file mode 100644 index 000000000..7e08326f3 --- /dev/null +++ b/hugr-cli/tests/human_panic_integration.rs @@ -0,0 +1,64 @@ +//! Integration test for `human-panic`. +//! +//! Builds the release binary with the `panic-test` feature, runs it with a +//! test-only panic trigger and an isolated temp dir, asserts the banner, +//! and verifies a TOML report is written. Temp dir is removed at the end. + +//! Black-box integration test for `human-panic`. + +use predicates::str::contains; // for cargo_bin() +use std::process::Command; +use tempfile::TempDir; + +#[test] +fn human_panic_writes_report() { + // Isolated temp dir for the crash report. + let tmp = TempDir::new().expect("create tempdir"); + let tmp_path = tmp.path(); + + // Run the release CLI binary from the workspace root. + // No features needed: main() installs human-panic in release builds. + let mut cmd = Command::new("cargo"); + cmd.args([ + "run", + "--release", + "-p", + "hugr-cli", + "--bin", + "hugr", + "--", // end cargo args; program args would follow + ]); + + // Isolate temp location & trigger the test panic in the child process. + if cfg!(windows) { + cmd.env("TEMP", tmp_path).env("TMP", tmp_path); + } else { + cmd.env("TMPDIR", tmp_path); + } + cmd.env("PANIC_FOR_TESTS", "1"); + + // Expect non-zero exit and the banner on stderr (release only). + assert_cmd::Command::from_std(cmd) + .assert() + .failure() + .stderr(contains("Well, this is embarrassing.")); + + // Confirm a .toml report exists in our temp dir. + let made_report = std::fs::read_dir(tmp_path) + .unwrap() + .filter_map(Result::ok) + .any(|e| { + e.path() + .extension() + .and_then(|ext| ext.to_str()) + .is_some_and(|ext| ext.eq_ignore_ascii_case("toml")) + }); + assert!( + made_report, + "expected a human-panic report in {:?}", + tmp_path + ); + + // Explicit cleanup; surface any removal errors. + tmp.close().expect("failed to remove temp dir"); +}