assert rejects: unit-testing that a constraint refuses bad data
Area: Testing Teaches: the negative-enforcement half of the
testatom. Argon is a constraint language, so a model’s most important property is which writes its invariants turn away —assert rejects { … }runs a write block against an isolated world and PASSes when the write is refused. An optional(Pkg::Code)pins the exact rejecting code. Prerequisites: refinement (whereinvariants); check constraints (delta guards). Run:ox test examples/assert_rejects_v0
The positive test forms — assert <expr> and assert [not] derivable — say what a model accepts or derives. They cannot say what it refuses. assert rejects { <writes> } is the missing half: it runs its block against an isolated copy of the test world, and a write-path guard rejection is a PASS (the rejected transaction commits nothing). An accepted write FAILs; a non-guard error — a typo, an unbound name — ERRORs loudly and never masquerades as a passing rejection.
What to read
The constraints under test in model.ar: a where-invariant concept (membership asserted, balance >= 0 enforced at the write with OE0668) and a check delta-guard (a new over-100t truck refused with Fleet::E001):
pub type Solvent <: Account where { self.balance >= 0 };
pub check OverweightTruck(v: Truck) :-
v.weight > 100
=> Diagnostic { severity: Severity::Error, code: "Fleet::E001", /* … */ };
The tests in tests/mod.ar exercise each rejection mode. A where-invariant violation, a code-pinned check rejection, the same rejection without a pin, and a legal write that must NOT satisfy a rejection assertion:
test "a negative balance is rejected by the where-invariant" {
let a = insert Solvent { balance: 100 };
assert rejects { update a: Account set { balance = -50 } };
}
test "an overweight truck is rejected by its check, by code" {
assert rejects(Fleet::E001) { let t = insert Truck { weight: 200 }; };
}
test "a within-limit balance is accepted" {
let a = insert Solvent { balance: 100 };
update a: Account set { balance = 50 };
assert a.balance == 50; // the positive form tests an accepted write
}
Running it
ox test discovers every test "name" { … } block, runs each against a fresh store, and reports pass/fail/error (it exits non-zero if any fails or errors — CI-usable):
$ ox test examples/assert_rejects_v0
PASS a negative balance is rejected by the where-invariant
PASS an overweight truck is rejected by its check, by code
PASS an overweight truck is rejected by its check
PASS a within-limit balance is accepted
4 passed, 0 failed, 0 errored, 0 inconclusive
The decisive contrast is the last test against the first three: setting a Solvent account to -50 is refused by the where-invariant (so assert rejects passes), but setting it to 50 is accepted — and an accepted write is tested with the positive assert a.balance == 50, never with assert rejects. The code-pinned variant passes only on its exact code: assert rejects(Fleet::E001) would FAIL, not pass, if the write were turned away by some other guard.
Honest caveats (what runs today)
assert rejectsis satisfied by a write-path guard rejection (awhere-invariantOE0668or acheckdelta-guard). A non-guard error inside the block (an unbound name, a type error) is an ERROR, not a PASS — the form does not let a broken test launder itself into a green rejection.- Each block runs against an isolated copy of the test world, so a rejected transaction leaves no residue for the next test.
This example is compiled and run in CI; a CLI test (oxc-driver/tests/cli_pipeline.rs) runs ox test examples/assert_rejects_v0 and asserts it stays green, and a companion test pins that the test runner classifies every outcome (pass / fail / error), so the negative-enforcement surface can’t drift from the language.
Source
The package’s real Argon source, transcluded from the files this corpus compiles — what you read here is exactly what CI builds.
root.ar
//! `assert rejects { … }` — unit-testing that a constraint REFUSES bad data
//! (§17.14). Argon is a constraint language: a model's most important property
//! is which writes its `where`-invariants and `check` delta-guards turn away.
//! The positive assertion forms (`assert <expr>`, `assert [not] derivable`)
//! cannot express that; `assert rejects` is the negative-enforcement half.
//!
//! Each `assert rejects { <writes> }` runs its block against an ISOLATED copy
//! of the test world. A write-path guard rejection PASSes (the rejected
//! transaction commits nothing); an optional `(Pkg::Code)` pin PASSes only on
//! that exact code; an accepted write FAILs; and a non-guard error (a typo, an
//! unbound name) ERRORs loudly — it never masquerades as a passing rejection.
//!
//! All concepts use the neutral `pub type` introducer — no ontological
//! commitment lives in the language.
mod model;
mod tests;
model.ar
//! The constraints under test.
// A primitive (`where`) concept: membership is asserted, and `balance >= 0` is
// a necessary invariant checked at the write — a violating write is refused
// with OE0668.
pub type Account {
mut balance: Int,
}
pub type Solvent <: Account where { self.balance >= 0 };
// A `pub check` delta-guard: a write that creates a new `Truck` over the 100t
// limit is refused atomically with the coded `Fleet::E001`.
pub type Truck {
mut weight: Int,
}
pub check OverweightTruck(v: Truck) :-
v.weight > 100 => Diagnostic {
severity: Severity::Error,
code: "Fleet::E001",
message: format!("truck exceeds the 100t road limit: {}", v.weight)
};
tests/mod.ar
use super::model::{ Account, Solvent, Truck };
// ANCHOR: rejects_tests
// A `where`-invariant rejection: setting a Solvent account below zero violates
// the necessary invariant `balance >= 0`, so the write is refused (OE0668).
test "a negative balance is rejected by the where-invariant" {
let a = insert Solvent { balance: 100 };
assert rejects {
update a: Account set { balance = - 50 }
};
}
// A `check` delta-guard rejection, with the rejecting code pinned: a 200t
// truck creates an OverweightTruck violation, refused with `Fleet::E001`.
test "an overweight truck is rejected by its check, by code" {
assert rejects(Fleet::E001) {
let t = insert Truck { weight: 200 };
};
}
// ANCHOR_END: rejects_tests
// The same rejection without a code pin — any guard rejection satisfies it.
test "an overweight truck is rejected by its check" {
assert rejects {
let t = insert Truck { weight: 200 };
};
}
// A legal write within the invariant is ACCEPTED, so it does not satisfy a
// rejection assertion. The positive form (`assert a.balance == 50` after the
// update) is how you test an accepted write — not `assert rejects`.
test "a within-limit balance is accepted" {
let a = insert Solvent { balance: 100 };
update a: Account set { balance = 50 };
assert a.balance == 50;
}