Skip to main content

Crate impish

Crate impish 

Source
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-impish

Write 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:

ErrorMeaning 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 readThe primitive has no Contract yet. Create its README, or fix the name.
…isn't a valid Contract; `cargo impish check` names its problemsThe README doesn’t parse. Run cargo impish check for the line.
`x`: contract/p/README.md doesn't declare A9No such obligation. Add it to the Contract first, or fix the number.
`x`: contract/p/README.md declares A2 retiredThe 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.