schematron 0.3.1

Schematron: pure Rust implementation of ISO/IEC 19757-3 rule-based XML validation, with its own XML parser, XPath 1.0 engine, XPath 2.0 subset, schema linting, and SVRL reporting. By SixArm.com.
Documentation
# Testing

How to prove a change works. The reference for what exists is
[`spec/testing.md`](../spec/testing.md); this file is about *how to work*.

## The layers, and which one to reach for

| Layer | Location | Reach for it when |
|---|---|---|
| Unit | `#[cfg(test)]` in each module | One function's behaviour, especially an edge case |
| Integration | `tests/validation.rs`, `tests/schema.rs` | Behaviour through the public API |
| Corpus | `tests/corpus/<case>/` | **Any Schematron semantics** — prefer this |
| CLI | `tests/cli.rs` | Arguments, exit codes, output selection |
| Docs | `tests/docs.rs` | A documented claim that could rot |
| Doc examples | rustdoc | Anything a caller reaches for |
| Fuzz | `fuzz/fuzz_targets/` | Panic freedom on hostile input |
| Bench | `benches/` | Anything with a complexity claim |

**Prefer a corpus case** for validation semantics. It is a directory, not
Rust, so it stays readable, and it doubles as documentation of what the
implementation is supposed to do.

## Adding a corpus case

```
tests/corpus/<name>/
  schema.sch      the schema
  input.xml       the document
  expected.txt    expected findings, one per line
  phase           optional: the phase to run
```

`expected.txt` lines are `KIND | location | text`, where `KIND` is `assert`
for a failed assertion or `report` for a successful report. Whitespace in
`text` is normalised, so wrap freely. Blank lines and `#` comments are
ignored — **use them**, and explain what the case is demonstrating:

```
# The broad rule claims the node, so the narrower rule after it is dead code.
assert | /*:a[1] | claimed by the broad rule
```

No Rust changes are needed. `tests/corpus.rs` discovers directories, and a
second test checks each case is complete.

An empty `expected.txt` (comments only) asserts that a document is clean —
`tests/corpus/no-findings/` does this.

## Running things

```sh
cargo test                              # everything
cargo test --lib                        # unit tests only, fastest loop
cargo test --test corpus                # the conformance suite
cargo test --test docs                  # the documentation audit
cargo test --lib xpath                  # one module
```

`tests/cli.rs` spawns the built binary, so it is the slow one (~40 s). It is
worth its cost — exit codes are what a build pipeline depends on — but do not
put logic tests there that belong in the library.

## Benchmarks

```sh
cargo bench
cargo bench -- --save-baseline main     # then compare after a change
cargo bench --bench bench_validate -- validate
```

For a fast signal while iterating:

```sh
cargo bench --bench bench_validate -- --warm-up-time 1 --measurement-time 2 --sample-size 10
```

**Benchmarks here have caught real defects, not just tracked numbers.** The
`validate` group at 1 000 and 10 000 elements exposed a quadratic in SVRL
location generation; the fix was a 14× speedup. If you change anything that
runs per node, check that group's scaling ratio.

## Fuzzing

Requires nightly and `cargo-fuzz`.

```sh
cargo +nightly fuzz build
cargo +nightly fuzz run fuzz_xpath -- -max_total_time=60 -rss_limit_mb=4096
```

Four targets: `fuzz_xml`, `fuzz_xpath`, `fuzz_schema`, `fuzz_validate`. Each
asserts more than "did not crash" — tree self-consistency, total conversions,
SVRL reparsing. Seeds are copied from `tests/corpus/`.

A crash writes `fuzz/artifacts/<target>/crash-<hash>`. Reproduce with:

```sh
cargo +nightly fuzz run fuzz_xpath fuzz/artifacts/fuzz_xpath/crash-<hash>
```

`fuzz_xpath` has already found a genuine panic this way. When one fires, fix
the defect **and** add a unit test naming the fuzz target that found it, so
the regression is covered by `cargo test` and not only by a nightly run.

## The MSRV boundary

```sh
cargo +1.94 test --all-features
```

Must run on the boundary toolchain itself. A newer compiler accepts strictly
more and proves nothing. See
[`spec/rust-msrv-n-minus-3.md`](../spec/rust-msrv-n-minus-3.md).

## What "done" means

All four gates in [`AGENTS.md`](../AGENTS.md) pass, plus:

- New behaviour has a test that **fails without the change**. Verify that by
  reverting the change, or by breaking the value the test asserts.
- New public items have rustdoc with an example.
- Any documented claim you added is machine-checked if it can be.
- `spec/` reflects the change, in the same edit.