> Canonical HTML: [https://docs.schematic.rs](https://docs.schematic.rs)

# Welcome to Pup

Pup is Schematic’s command-line tool for checking behaviors your code must always preserve. **Supertests** express those behaviors as precise claims over every possible input in scope, not just selected examples. Pup looks for problems ordinary tests can miss. When it finds one, it explains what went wrong.

## Write a supertest

A supertest looks much like an ordinary test. It calls your code and uses assertions to state what should happen.

<p data-language-example="true" data-code-language="rust">In Rust, the <code>#[supertest]</code> attribute turns a test function into a supertest.</p>

<p data-language-example="true" data-code-language="python">In Python, the <code>@supertest</code> decorator turns a test function into a supertest. If you use <code>import schematic</code>, write <code>@schematic.supertest</code> instead.</p>

<p data-language-example="true" data-code-language="ruby">In Ruby, the <code>Schematic.supertest</code> DSL defines a test block as a supertest.</p>

A supertest takes inputs as parameters. Pup checks the claim across all possible values in scope. If one breaks the claim, Pup returns it as a concrete counterexample.

Consider `collapse_spaces`, a function that should replace every run of consecutive spaces with one space. Once text has been normalized, running the function again should not change it. We can express that claim as a supertest:

**supertests/normalize_spaces.rs**

```rust
use schematic::supertest;

#[supertest]
fn normalizing_twice_changes_nothing(text: String) {
    let once = collapse_spaces(&text);
    let twice = collapse_spaces(&once);

    assert_eq!(twice, once);
}
```

**supertests/normalize_spaces.py**

```python
from schematic import *

@supertest
def normalizing_twice_changes_nothing(text: str) -> None:
    once = collapse_spaces(text)
    twice = collapse_spaces(once)

    assert twice == once
```

**supertests/normalize_spaces.rb**

```ruby
require "schematic"

Schematic.supertest(
  "normalizing_twice_changes_nothing",
  text: String
) do |text|
  once = collapse_spaces(text)
  twice = collapse_spaces(once)

  assert twice == once
end
```

The input `text` covers every possible string, not just a few examples. That includes easy-to-miss cases such as empty strings, strings without spaces, and long runs of spaces.

Your language’s test runner does not execute the supertest. Pup does not run it over those strings one by one either. Instead, Pup reasons about the supertest as one claim that must hold for every possible input.

## Check the supertest with Pup

Start a check by passing the supertest file to Pup:

**Terminal**

```console
$ pup check supertests/normalize_spaces.rs
text-tools · a81d7c2 · supertests/normalize_spaces.rs

● normalizing_twice_changes_nothing · checking · ref 43
```

**Terminal**

```console
$ pup check supertests/normalize_spaces.py
text-tools · a81d7c2 · supertests/normalize_spaces.py

● normalizing_twice_changes_nothing · checking · ref 43
```

**Terminal**

```console
$ pup check supertests/normalize_spaces.rb
text-tools · a81d7c2 · supertests/normalize_spaces.rb

● normalizing_twice_changes_nothing · checking · ref 43
```

The row identifies one check for this supertest at commit `a81d7c2`. Checks are asynchronous. You can leave the terminal and inspect it later, or pass `--wait` to keep the command open. Once checking completes, Pup returns the result and, if the claim is false, the exact input that breaks it:

**Terminal**

```console
$ pup check status
text-tools · supertests/normalize_spaces.rs::normalizing_twice_changes_nothing

    COMMIT  · STATUS · WHEN   · REF
› × a81d7c2 · failed · 1m ago ·  43

Problem P1
  Normalization is not idempotent
  collapse_spaces replaces only one pair of spaces per call.

Confirmed counterexample
  text   = "hello   world"
  once   = "hello  world"
  twice  = "hello world"
```

**Terminal**

```console
$ pup check status
text-tools · supertests/normalize_spaces.py::normalizing_twice_changes_nothing

    COMMIT  · STATUS · WHEN   · REF
› × a81d7c2 · failed · 1m ago ·  43

Problem P1
  Normalization is not idempotent
  collapse_spaces replaces only one pair of spaces per call.

Confirmed counterexample
  text   = "hello   world"
  once   = "hello  world"
  twice  = "hello world"
```

**Terminal**

```console
$ pup check status
text-tools · supertests/normalize_spaces.rb::normalizing_twice_changes_nothing

    COMMIT  · STATUS · WHEN   · REF
› × a81d7c2 · failed · 1m ago ·  43

Problem P1
  Normalization is not idempotent
  collapse_spaces replaces only one pair of spaces per call.

Confirmed counterexample
  text   = "hello   world"
  once   = "hello  world"
  twice  = "hello world"
```

This is a concrete counterexample, not a prediction. Running the implementation with that input reproduces the failure.

## Fix the implementation and check again

The counterexample reveals the bug: the implementation replaces only one pair of spaces per call. Repeating the replacement until no consecutive spaces remain fixes it:

**src/text.rs**

```rust
fn collapse_spaces(text: &str) -> String {
    text.replace("  ", " ") // Before
    let mut normalized = text.to_owned(); // After
    while normalized.contains("  ") { // After
        normalized = normalized.replace("  ", " "); // After
    } // After
    normalized // After
}
```

**src/text.py**

```python
def collapse_spaces(text: str) -> str:
    return text.replace("  ", " ") # Before
    normalized = text # After
    while "  " in normalized: # After
        normalized = normalized.replace("  ", " ") # After
    return normalized # After
```

**lib/text.rb**

```ruby
def collapse_spaces(text)
  text.gsub("  ", " ") # Before
  normalized = text.dup # After
  while normalized.include?("  ") # After
    normalized = normalized.gsub("  ", " ") # After
  end # After
  normalized # After
end
```

Once the fix is committed, running the same command checks the updated source:

**Terminal**

```console
$ pup check supertests/normalize_spaces.rs
text-tools · c24f901 · supertests/normalize_spaces.rs

● normalizing_twice_changes_nothing · checking · ref 44

$ pup check status
text-tools · supertests/normalize_spaces.rs::normalizing_twice_changes_nothing

    COMMIT  · STATUS     · WHEN    · REF
› ✓ c24f901 · 0 problems · 12s ago ·  44
  × a81d7c2 · failed     · 4m ago  ·  43
```

**Terminal**

```console
$ pup check supertests/normalize_spaces.py
text-tools · c24f901 · supertests/normalize_spaces.py

● normalizing_twice_changes_nothing · checking · ref 44

$ pup check status
text-tools · supertests/normalize_spaces.py::normalizing_twice_changes_nothing

    COMMIT  · STATUS     · WHEN    · REF
› ✓ c24f901 · 0 problems · 12s ago ·  44
  × a81d7c2 · failed     · 4m ago  ·  43
```

**Terminal**

```console
$ pup check supertests/normalize_spaces.rb
text-tools · c24f901 · supertests/normalize_spaces.rb

● normalizing_twice_changes_nothing · checking · ref 44

$ pup check status
text-tools · supertests/normalize_spaces.rb::normalizing_twice_changes_nothing

    COMMIT  · STATUS     · WHEN    · REF
› ✓ c24f901 · 0 problems · 12s ago ·  44
  × a81d7c2 · failed     · 4m ago  ·  43
```

## Understand check results

`0 problems` means the standard check completed without detecting any actionable issues. In Pup, a Problem is actionable analysis to inspect, while `failed` means Pup confirmed a counterexample.

For stronger assurance, pass `--prove`. This may take longer while Pup works to mathematically prove that the claim always holds. A `verified` result means Pup proved the claim for the checked commit. If Pup cannot prove the claim or confirm a failure, the result is `inconclusive`.

## Next steps

[Get started](/get-started/) covers installation, browser login, linking your first repository, and your first check. To go deeper, learn more about [writing supertests](/supertests/) or see the [`pup check` command reference](/commands/checks/).
