diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d839d30..6a911de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,8 +67,8 @@ jobs: - name: Adjust snapshots on Windows if: ${{ startsWith(matrix.os, 'windows') }} run: | - perl -i -pe 's/\//\\/g if /at exn/' exn/tests/snapshots/*.snap - perl -i -pe 's/(? Result<(), MainError> { app::run().or_raise(|| MainError)?; @@ -84,10 +84,3 @@ mod http { } impl std::error::Error for HttpError {} } - -// Output when running `cargo run --example antipattern`. -// Notice "failed to send request" appears twice with no new information. -// -// Error: fatal error occurred in application, at examples/src/antipattern.rs:35:16 -// |-- failed to send request, at examples/src/antipattern.rs:59:34 -// `-- failed to send request to server: 127.0.0.1, at examples/src/antipattern.rs:75:9 diff --git a/examples/src/basic.rs b/examples/src/basic.rs index e1d5e43..83a81fd 100644 --- a/examples/src/basic.rs +++ b/examples/src/basic.rs @@ -25,10 +25,10 @@ //! 3. **Keep Errors Simple** - Use `struct Error(String)` by default. Only add complexity (enums, //! fields) when needed for programmatic handling. -use derive_more::Display; use exn::Result; use exn::ResultExt; use exn::bail; +use parse_display::Display; fn main() -> Result<(), MainError> { app::run().or_raise(|| MainError)?; @@ -68,9 +68,3 @@ mod http { pub struct HttpError(String); impl std::error::Error for HttpError {} } - -// Output when running `cargo run --example basic`: -// -// Error: fatal error occurred in application, at examples/src/basic.rs:34:16 -// |-- failed to run app, at examples/src/basic.rs:49:14 -// `-- failed to send request to server: https://example.com, at examples/src/basic.rs:62:9 diff --git a/examples/src/custom-layout.rs b/examples/src/custom-layout.rs index 4311066..d8e89c8 100644 --- a/examples/src/custom-layout.rs +++ b/examples/src/custom-layout.rs @@ -109,9 +109,3 @@ mod http { impl Error for HttpError {} } - -// Output when running `cargo run --example custom_layout`: -// -// Error: fatal error occurred in application: -// 0: failed to run app, at examples/src/custom-layout.rs:74:30 -// 1: failed to send request to server: https://example.com, at examples/src/custom-layout.rs:94:9 diff --git a/examples/src/downcast.rs b/examples/src/downcast.rs index 37212c4..159004d 100644 --- a/examples/src/downcast.rs +++ b/examples/src/downcast.rs @@ -23,12 +23,12 @@ use std::error::Error; -use derive_more::Display; use exn::Exn; use exn::Frame; use exn::Result; use exn::ResultExt; use exn::bail; +use parse_display::Display; use crate::http::HttpError; @@ -106,19 +106,3 @@ mod http { } impl Error for HttpError {} } - -// Output when running `cargo run --example downcast`: -// -// HTTP error with status code: 503 -// Retryable error, attempting retry #1 -// -// HTTP error with status code: 503 -// Retryable error, attempting retry #2 -// -// HTTP error with status code: 503 -// Retryable error, attempting retry #3 -// -// HTTP error with status code: 503 -// Error: fatal error occurred in application, at examples/src/downcast.rs:54:24 -// |-- failed to run app, at examples/src/downcast.rs:82:35 -// `-- HTTP 503: service unavailable, at examples/src/downcast.rs:95:9 diff --git a/examples/src/from-anyhow.rs b/examples/src/from-anyhow.rs index 43dc7a7..cb00404 100644 --- a/examples/src/from-anyhow.rs +++ b/examples/src/from-anyhow.rs @@ -18,10 +18,10 @@ //! - Legacy code returns `anyhow::Result`. //! - At the boundary, convert into `exn::Result`. -use derive_more::Display; use exn::Result; use exn::ResultExt; use exn_anyhow::from_anyhow; +use parse_display::Display; fn main() -> Result<(), MainError> { app::run().or_raise(|| MainError)?; @@ -62,10 +62,3 @@ mod legacy { Ok(port) } } - -// Output when running `cargo run -p examples --example from-anyhow`: -// -// Error: fatal error occurred in application, at examples/src/from-anyhow.rs:27:16 -// |-- failed to run app, at examples/src/from-anyhow.rs:42:14 -// |-- PORT must be a number; got "not-a-number", at exn-anyhow/src/lib.rs:51:19 -// `-- invalid digit found in string, at exn-anyhow/src/lib.rs:48:19 diff --git a/examples/src/into-anyhow.rs b/examples/src/into-anyhow.rs index 4ee9693..f301792 100644 --- a/examples/src/into-anyhow.rs +++ b/examples/src/into-anyhow.rs @@ -20,10 +20,10 @@ use std::error::Error; -use derive_more::Display; use exn::Result; use exn::ResultExt; use exn_anyhow::into_anyhow; +use parse_display::Display; fn main() -> anyhow::Result<()> { app::run().map_err(into_anyhow)?; @@ -62,11 +62,3 @@ mod config { pub struct ConfigError(String); impl Error for ConfigError {} } - -// Output when running `cargo run -p examples --example into-anyhow`: -// -// Error: failed to start app -// -// Caused by: -// 0: PORT must be a number; got "not-a-number" -// 1: invalid digit found in string diff --git a/examples/src/into-std-error.rs b/examples/src/into-std-error.rs index 6a8a512..4540760 100644 --- a/examples/src/into-std-error.rs +++ b/examples/src/into-std-error.rs @@ -20,9 +20,9 @@ use std::error::Error; -use derive_more::Display; use exn::Result; use exn::ResultExt; +use parse_display::Display; fn main() -> std::result::Result<(), Box> { app::run()?; @@ -61,9 +61,3 @@ mod config { pub struct ConfigError(String); impl Error for ConfigError {} } - -// Output when running `cargo run -p examples --example into-std-error`: -// -// Error: failed to start app, at examples/src/into-std-error.rs:36:40 -// |-- PORT must be a number; got "not-a-number", at examples/src/into-std-error.rs:55:14 -// `-- invalid digit found in string, at examples/src/into-std-error.rs:55:14 diff --git a/examples/src/library-boundary.rs b/examples/src/library-boundary.rs index 0a96ff4..7f64e11 100644 --- a/examples/src/library-boundary.rs +++ b/examples/src/library-boundary.rs @@ -22,12 +22,12 @@ use std::error::Error; -use derive_more::Display; use exn::Exn; use exn::Frame; use exn::Result; use exn::ResultExt; use exn::bail; +use parse_display::Display; fn main() { demo(429); @@ -176,7 +176,6 @@ mod library { } #[derive(Debug, Display)] - #[display("{_0}")] pub struct ServiceError(String); impl Error for ServiceError {} } @@ -237,26 +236,3 @@ mod library { impl Error for HttpError {} } } - -// Output when running `cargo run -p examples --example library-boundary`: -// -// Start demo for user: 429 -// RateLimited: rate limited by upstream -// Retryable error, attempting retry #1 -// -// RateLimited: rate limited by upstream -// Retryable error, attempting retry #2 -// -// RateLimited: rate limited by upstream -// Retryable error, attempting retry #3 -// -// Action: Retried too many times, aborting -// Error: RateLimited: rate limited by upstream, at examples/src/library-boundary.rs:149:13 -// |-- failed to fetch profile for user 429, at examples/src/library-boundary.rs:170:55 -// `-- HTTP 429: too many requests, at examples/src/library-boundary.rs:218:24 -// -// Start demo for user: 404 -// Action: Return 404 -// Error: NotFound: user 404 not found, at examples/src/library-boundary.rs:149:13 -// |-- failed to fetch profile for user 404, at examples/src/library-boundary.rs:169:47 -// `-- no row for user_id 404, at examples/src/library-boundary.rs:189:24 diff --git a/examples/tests/snapshots/tests__antipattern.snap b/examples/tests/snapshots/tests__antipattern.snap new file mode 100644 index 0000000..001feb1 --- /dev/null +++ b/examples/tests/snapshots/tests__antipattern.snap @@ -0,0 +1,7 @@ +--- +source: examples/tests/tests.rs +expression: "capture_output(\"antipattern\")" +--- +Error: fatal error occurred in application, at examples/src/antipattern.rs:35:16 +|-- failed to send request, at examples/src/antipattern.rs:59:34 +`-- failed to send request to server: 127.0.0.1, at examples/src/antipattern.rs:75:9 diff --git a/examples/tests/snapshots/tests__basic.snap b/examples/tests/snapshots/tests__basic.snap new file mode 100644 index 0000000..0d5333c --- /dev/null +++ b/examples/tests/snapshots/tests__basic.snap @@ -0,0 +1,7 @@ +--- +source: examples/tests/tests.rs +expression: "capture_output(\"basic\")" +--- +Error: fatal error occurred in application, at examples/src/basic.rs:34:16 +|-- failed to run app, at examples/src/basic.rs:49:14 +`-- failed to send request to server: https://example.com, at examples/src/basic.rs:62:9 diff --git a/examples/tests/snapshots/tests__custom-layout.snap b/examples/tests/snapshots/tests__custom-layout.snap new file mode 100644 index 0000000..37c7e5e --- /dev/null +++ b/examples/tests/snapshots/tests__custom-layout.snap @@ -0,0 +1,7 @@ +--- +source: examples/tests/tests.rs +expression: "capture_output(\"custom-layout\")" +--- +Error: fatal error occurred in application: +0: failed to run app, at examples/src/custom-layout.rs:74:30 +1: failed to send request to server: https://example.com, at examples/src/custom-layout.rs:94:9 diff --git a/examples/tests/snapshots/tests__downcast.snap b/examples/tests/snapshots/tests__downcast.snap new file mode 100644 index 0000000..60df7a2 --- /dev/null +++ b/examples/tests/snapshots/tests__downcast.snap @@ -0,0 +1,17 @@ +--- +source: examples/tests/tests.rs +expression: "capture_output(\"downcast\")" +--- +HTTP error with status code: 503 +Retryable error, attempting retry #1 + +HTTP error with status code: 503 +Retryable error, attempting retry #2 + +HTTP error with status code: 503 +Retryable error, attempting retry #3 + +HTTP error with status code: 503 +Error: fatal error occurred in application, at examples/src/downcast.rs:54:24 +|-- failed to run app, at examples/src/downcast.rs:82:35 +`-- HTTP 503: service unavailable, at examples/src/downcast.rs:95:9 diff --git a/examples/tests/snapshots/tests__from-anyhow.snap b/examples/tests/snapshots/tests__from-anyhow.snap new file mode 100644 index 0000000..37ac637 --- /dev/null +++ b/examples/tests/snapshots/tests__from-anyhow.snap @@ -0,0 +1,8 @@ +--- +source: examples/tests/tests.rs +expression: "capture_output(\"from-anyhow\")" +--- +Error: fatal error occurred in application, at examples/src/from-anyhow.rs:27:16 +|-- failed to run app, at examples/src/from-anyhow.rs:42:14 +|-- PORT must be a number; got "not-a-number", at exn-anyhow/src/lib.rs:51:19 +`-- invalid digit found in string, at exn-anyhow/src/lib.rs:48:19 diff --git a/examples/tests/snapshots/tests__into-anyhow.snap b/examples/tests/snapshots/tests__into-anyhow.snap new file mode 100644 index 0000000..ac4f5eb --- /dev/null +++ b/examples/tests/snapshots/tests__into-anyhow.snap @@ -0,0 +1,9 @@ +--- +source: examples/tests/tests.rs +expression: "capture_output(\"into-anyhow\")" +--- +Error: failed to start app + +Caused by: + 0: PORT must be a number; got "not-a-number" + 1: invalid digit found in string diff --git a/examples/tests/snapshots/tests__into-std-error.snap b/examples/tests/snapshots/tests__into-std-error.snap new file mode 100644 index 0000000..65ec088 --- /dev/null +++ b/examples/tests/snapshots/tests__into-std-error.snap @@ -0,0 +1,7 @@ +--- +source: examples/tests/tests.rs +expression: "capture_output(\"into-std-error\")" +--- +Error: failed to start app, at examples/src/into-std-error.rs:36:40 +|-- PORT must be a number; got "not-a-number", at examples/src/into-std-error.rs:55:14 +`-- invalid digit found in string, at examples/src/into-std-error.rs:55:14 diff --git a/examples/tests/snapshots/tests__library-boundary.snap b/examples/tests/snapshots/tests__library-boundary.snap new file mode 100644 index 0000000..5dc2690 --- /dev/null +++ b/examples/tests/snapshots/tests__library-boundary.snap @@ -0,0 +1,24 @@ +--- +source: examples/tests/tests.rs +expression: "capture_output(\"library-boundary\")" +--- +Start demo for user: 429 +RateLimited: rate limited by upstream +Retryable error, attempting retry #1 + +RateLimited: rate limited by upstream +Retryable error, attempting retry #2 + +RateLimited: rate limited by upstream +Retryable error, attempting retry #3 + +Action: Retried too many times, aborting +Error: RateLimited: rate limited by upstream, at examples/src/library-boundary.rs:149:13 +|-- failed to fetch profile for user 429, at examples/src/library-boundary.rs:170:55 +`-- HTTP 429: too many requests, at examples/src/library-boundary.rs:217:24 + +Start demo for user: 404 +Action: Return 404 +Error: NotFound: user 404 not found, at examples/src/library-boundary.rs:149:13 +|-- failed to fetch profile for user 404, at examples/src/library-boundary.rs:169:47 +`-- no row for user_id 404, at examples/src/library-boundary.rs:188:24 diff --git a/examples/tests/tests.rs b/examples/tests/tests.rs new file mode 100644 index 0000000..227f48f --- /dev/null +++ b/examples/tests/tests.rs @@ -0,0 +1,65 @@ +// Copyright 2025 FastLabs Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::fs; +use std::path::PathBuf; +use std::process::Command; + +use insta::assert_snapshot; + +#[test] +fn snapshots() { + fn capture_output(name: &str) -> String { + let mut cargo = Command::new("cargo"); + cargo.current_dir(env!("CARGO_MANIFEST_DIR")); + cargo.args(["run", "--example", name, "--quiet"]); + + let output = cargo.output().unwrap(); + String::from_utf8_lossy(&output.stderr).to_string() + } + + assert_snapshot!("antipattern", capture_output("antipattern")); + assert_snapshot!("basic", capture_output("basic")); + assert_snapshot!("custom-layout", capture_output("custom-layout")); + assert_snapshot!("downcast", capture_output("downcast")); + assert_snapshot!("from-anyhow", capture_output("from-anyhow")); + assert_snapshot!("into-anyhow", capture_output("into-anyhow")); + assert_snapshot!("into-std-error", capture_output("into-std-error")); + assert_snapshot!("library-boundary", capture_output("library-boundary")); +} + +#[test] +fn coverage() { + let examples_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src"); + let snapshots_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/snapshots"); + + let entries = fs::read_dir(&examples_dir).unwrap_or_else(|err| { + panic!("failed to read examples directory at {examples_dir:?}: {err:?}") + }); + + for entry in entries { + let entry = entry.unwrap(); + let path = entry.path(); + if path.extension().and_then(|s| s.to_str()) != Some("rs") { + unreachable!("all examples files should be rs files"); + } + + let example_name = path.file_stem().unwrap().to_str().unwrap(); + let snapshot_path = snapshots_dir.join(format!("tests__{example_name}.snap")); + assert!( + snapshot_path.exists(), + "snapshot for example {example_name} does not exist" + ); + } +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs index ff47c44..4da110f 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::fs; -use std::path::PathBuf; use std::process::Command as StdCommand; use clap::Parser; @@ -66,7 +64,6 @@ struct CommandTest { impl CommandTest { fn run(self) { run_command(make_test_cmd(self.no_capture, true, &[])); - run_example_tests(); } } @@ -87,67 +84,6 @@ impl CommandLint { } } -fn run_example_tests() { - let examples_dir = PathBuf::from(env!("CARGO_WORKSPACE_DIR")) - .join("examples") - .join("src"); - - let entries = fs::read_dir(&examples_dir).unwrap_or_else(|err| { - panic!("failed to read examples directory at {examples_dir:?}: {err:?}") - }); - - let mut total = 0; - let mut failed = vec![]; - for entry in entries { - let entry = entry.unwrap(); - let path = entry.path(); - - if path.extension().and_then(|s| s.to_str()) != Some("rs") { - continue; - } - - let example_name = path.file_stem().unwrap().to_str().unwrap(); - - let mut cmd = find_command("cargo"); - cmd.args(["--quiet", "run", "--example", example_name]); - - let output = cmd.output().unwrap(); - let stderr = String::from_utf8_lossy(&output.stderr); - - let content = fs::read_to_string(&path).unwrap(); - let content = content.lines().collect::>().join("\n"); - - let actual = stderr - .lines() - .map(|line| { - if line.is_empty() { - "//".to_string() - } else { - format!("// {}", line) - } - }) - .collect::>() - .join("\n"); - - if !content.contains(&actual) { - failed.push((path, actual)); - } - - total += 1; - } - - if !failed.is_empty() { - eprintln!("{}/{} example tests failed:", failed.len(), total); - for (path, actual) in failed { - eprintln!("\nexample: {}", path.display()); - eprintln!("actual stderr:\n{}", actual); - } - std::process::exit(1); - } else { - println!("all {} example tests passed", total); - } -} - fn find_command(cmd: &str) -> StdCommand { match which::which(cmd) { Ok(exe) => {