Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
target/
**/*.rs.bk
__pycache__/
.coverage

# Local spelling-policy caches and helper tool state
.uv-cache/
Expand Down
67 changes: 64 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,4 @@ rstest = "0.26.1"
rstest-bdd = "0.5.0"
rstest-bdd-macros = "0.5.0"
serde_json = "1.0.150"
trybuild = "1.0.120"
25 changes: 25 additions & 0 deletions docs/developers-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,31 @@ The test suite covers the same behaviour from several angles:
output.
- End-to-end tests in `tests/e2e.rs` build and run the compiled binary with
accelerated logical seconds.
- Executable UI tests in `tests/ui/` compile against the public crate boundary
and pin the user-facing `Display` output of public error types.

### Public error UI tests

The `tests/ui.rs` harness uses `trybuild` to compile and execute every
`tests/ui/*_display.rs` fixture as an external crate. These fixtures use
`trybuild`'s pass mode because Rust evaluates `Display` implementations at
runtime. A compile-fail fixture can snapshot compiler diagnostics, but it
cannot observe an error value's formatted output.

Treat each expected string literal in a display fixture as a UI snapshot. When
adding a public error type or variant, add an assertion with representative
field values to the corresponding fixture, or add a new `*_display.rs` file.
Run the focused harness with:

```sh
cargo test --test ui
```

`make test` also discovers the harness and is the required pre-commit and CI
entrypoint. If an intentional wording change alters a message, update the
expected literal in the same commit and review the string diff deliberately.
These executable fixtures do not use adjacent `.stderr` files, so
`TRYBUILD=overwrite` is not part of this snapshot workflow.

## Spelling gate

Expand Down
7 changes: 6 additions & 1 deletion docs/repository-layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ omits build output such as `target/`.
│ ├── e2e.rs
│ ├── features/
│ │ └── sleep_cli.feature
│ └── snapshots.rs
│ ├── snapshots.rs
│ ├── ui.rs
│ └── ui/
│ └── ...
├── AGENTS.md
├── Cargo.toml
├── LICENSE
Expand Down Expand Up @@ -96,6 +99,8 @@ omits build output such as `target/`.
scenarios.
- `tests/snapshots.rs`: Pins representative locale-aware remaining-time
output.
- `tests/ui.rs`: Compiles and executes public error display UI fixtures.
- `tests/ui/`: Holds external-crate fixtures that pin user-facing error text.
- `AGENTS.md`: Provides repository-specific working instructions for agents and
contributors.
- `Cargo.toml`: Defines package metadata, dependencies, lint policy, and Cargo
Expand Down
7 changes: 7 additions & 0 deletions tests/ui.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//! Executable UI checks for the public error types.

#[test]
fn public_error_display_output() {
let cases = trybuild::TestCases::new();
cases.pass("tests/ui/*_display.rs");
}
31 changes: 31 additions & 0 deletions tests/ui/cli_error_display.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//! Pins the user-facing display output of `CliError`.

use catnap::{CliError, DurationParseError};

fn assert_display(error: CliError, expected: &str) {
assert_eq!(error.to_string(), expected);
}

fn main() {
assert_display(CliError::NonUnicodeArgument, "invalid non-Unicode argument");
assert_display(
CliError::MissingOptionValue { option: "--colour" },
"option '--colour' requires an argument",
);
assert_display(
CliError::InvalidLogicalSecond {
value: "0".to_owned(),
},
"invalid logical second duration '0'",
);
assert_display(
CliError::UnknownOption {
option: "--unknown".to_owned(),
},
"unrecognized option '--unknown'",
);
assert_display(
CliError::Duration(DurationParseError::MissingOperand),
"missing operand",
);
}
10 changes: 10 additions & 0 deletions tests/ui/clock_config_error_display.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//! Pins the user-facing display output of `ClockConfigError`.

use catnap::ClockConfigError;

fn main() {
assert_eq!(
ClockConfigError::ZeroLogicalSecond.to_string(),
"logical second duration must be greater than zero",
);
}
41 changes: 41 additions & 0 deletions tests/ui/duration_parse_error_display.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//! Pins the user-facing display output of `DurationParseError`.

use catnap::DurationParseError;

fn assert_display(error: DurationParseError, expected: &str) {
assert_eq!(error.to_string(), expected);
}

fn main() {
assert_display(DurationParseError::MissingOperand, "missing operand");
assert_display(
DurationParseError::EmptyOperand {
operand: String::new(),
},
"invalid time interval ''",
);
assert_display(
DurationParseError::InvalidSuffix {
operand: "2fortnights".to_owned(),
},
"invalid time suffix in '2fortnights'",
);
assert_display(
DurationParseError::InvalidNumber {
operand: "soon".to_owned(),
},
"invalid time interval 'soon'",
);
assert_display(
DurationParseError::TooPrecise {
operand: "0.0000000001s".to_owned(),
},
"time interval '0.0000000001s' has more than nanosecond precision",
);
assert_display(
DurationParseError::Overflow {
operand: "340282366920938463464s".to_owned(),
},
"time interval '340282366920938463464s' is too large",
);
}
Loading