Expand description
Contract tests for Rust: a test that names the promise it proves, checked as it compiles.
A Contract is the list of promises a piece of code makes, written down in
contract/<primitive>/README.md as numbered obligations. A contract test
names the obligations it proves with #[impish::contract]:
#[impish::contract("slugify/A1")]
fn lowercases_letters_and_digits() {
assert_eq!(slug::slugify("Rust 2024"), "rust-2024");
}The attribute reads contract/slugify/README.md while the test compiles. If
A1 isn’t declared there, or is retired, the test doesn’t compile, so a
typo or a broken promise can’t slip into a green build.
This crate is the attribute only. The cargo-impish command checks that
every obligation has a test (cargo impish check) and that the tests catch
real bugs (cargo impish mutate). The README explains the method.
§Getting started
Add the crate as a dev-dependency, and the command once per machine:
cargo add --dev impish
cargo install cargo-impishWrite the Contract for a primitive, here slugify, in
contract/slugify/README.md:
# slugify
Turns a title into a URL slug.
## Obligations
### A1 — Letters and digits are kept, lowercased
- **must:** keep ASCII letters and digits, lowercased
- **kind:** functional
- **status:** active
```covers
src/lib.rs::slugify
```Prove it in tests/contract/slugify.rs, and list the primitive in
tests/contract/main.rs (mod slugify;), which Cargo builds as the
contract test target:
#[impish::contract("slugify/A1")]
fn lowercases_letters_and_digits() {
assert_eq!(slug::slugify("Rust 2024"), "rust-2024");
}Then cargo test runs it, and cargo impish check confirms every
obligation is proved:
The Contract holds: 1 obligations across 1 primitives.§Existing tests
The attribute doesn’t replace a test attribute that is already there. Add
it on the line below (or above) #[test], #[tokio::test] or any
attribute whose path ends in test, and the test still runs once:
#[tokio::test]
#[impish::contract("fetch/A2")]
async fn retries_after_a_timeout() {
// ...
}On its own, the attribute makes the function a #[test].
§What is checked as the test compiles
For each tag, the attribute reports a compile error on that tag when:
| Error | Meaning and fix |
|---|---|
name the obligations this test proves: #[impish::contract("<primitive>/A<n>")] | The attribute has no tags. Name at least one. Tags are string literals. |
`x` isn't <primitive>/A<n> | A tag is not a primitive and an obligation number, such as "slugify/A1". |
`x`: the primitive must be letters, digits, `-` and `_` | A tag tried to name a path outside contract/. Nothing is read. |
`x`: contract/p/README.md can't be read | The primitive has no Contract yet. Create its README, or fix the name. |
…isn't a valid Contract; `cargo impish check` names its problems | The README doesn’t parse. Run cargo impish check for the line. |
`x`: contract/p/README.md doesn't declare A9 | No such obligation. Add it to the Contract first, or fix the number. |
`x`: contract/p/README.md declares A2 retired | The promise was withdrawn. Delete the test, or name the obligation that replaced it. |
The README is part of the test’s build, so editing it checks the tags again
on the next cargo test.
§Where it belongs
Use the attribute only in tests/contract/<primitive>.rs, the tests of
that primitive’s Contract; cargo impish check reports it anywhere else.
Tags are resolved against contract/ in the crate being tested
(CARGO_MANIFEST_DIR). Several tags on one test are fine:
#[impish::contract("cli/A1", "cli/A2")].
The Contract format, every check and mutate message, and how to adopt
impish in an existing crate are in the README.
Attribute Macros§
- contract
- Marks a contract test with the obligations it proves.