Testing
A Wirescript program can check itself. The pattern below compiles to a circuit that runs its own assertions when the grid loads, says nothing when everything passes, and prints what it needs to diagnose the failure when something does not.
Type checking cannot do this job. It proves a program is well formed, not that a gate computes what you expected, and most unexpected behavior comes from the gates.
Contents
- The shape
- Compare two paths, not one path against a constant
- Include a control
- What an in-game test cannot prove
The shape
var pass: int = 0
var total: int = 0
mod assert<T: int | float | string>(want: T, got: T, label: string) {
total = total + 1
let ok = want == got
pass = pass + ok
if !ok {
BroadcastChatMessage("FAIL: ${label} want ${want} got ${got}")
}
}
on Loaded() {
var xs: int[]
xs.push(10)
xs.push(20)
assert(2, xs.length(), "length after two pushes")
assert(20, xs[1], "second element")
BroadcastChatMessage("array checks: ${pass}/${total}")
}
The file is examples/assert.ws, verified against
just check. The later examples reuse this assert, pass, and total without
repeating them. Five parts of the example matter.
on Loaded() as the trigger. The Load Event fires once when the chip is
placed, and again whenever the world loads with it, so the test runs by
pasting it in.
Module-level counters. pass and total live at module scope, so assert
updates them in place and each call is a bare assert(want, got, label). The
summary line at the end is the whole result. pass = pass + ok relies on bool
coercing to int, so there is no if ok then 1 else 0.
One generic assert. <T: int | float | string> lets the same mod check
ints, floats, and strings. The compiler monomorphizes each call to its argument
type. == compares any variant, but the failure line stringifies the values
with ${...}. Only those three types support that, so the bound names them.
Widen it to plain <T> for a check that never prints the value.
The failure line prints the values. A wrong value and a value that never arrived look
identical when all you print is a label. want 1 got 0 tells them apart, and
assert gives you that on every check without writing the interpolation by hand.
Silence on success. Only failures print. A run that says
array checks: 12/12 and nothing else is a pass you can read at a glance.
Twelve lines of OK: ... would have to be scanned. The checks print with
BroadcastChatMessage rather than PrintToConsole, so the output lands in chat
where you are already looking.
Compare two paths, not one path against a constant
A check against a hardcoded expected value only proves the program agrees with what you typed. The stronger form computes the same answer two ways and compares those, so the test fails when the two disagree even if you were wrong about both:
mod byAddition(n: int) -> int { return n + n }
on Loaded() {
assert(byAddition(21), 21 * 2, "addition agrees with multiplication")
BroadcastChatMessage("math checks: ${pass}/${total}")
}
This is how to test anything with two implementations: an optimized path against an obvious one, a compile-time answer against the runtime gates, a lookup table against the formula that generated it.
Include a control
A test that reads 0 when a value fails to arrive will pass any check whose
expected answer is also 0. Give at least one check a value nothing else in the
program produces, so a whole channel going dead cannot slip through:
on Loaded() {
var xs: int[]
xs.push(12345)
assert(12345, xs[0], "control literal survives the round trip")
BroadcastChatMessage("checks: ${pass}/${total}")
}
12345 cannot be confused with a default, an empty read, or an off-by-one.
What an in-game test cannot prove
Running the circuit proves the values are right. It cannot prove how they were produced, which is what a test of a compiler feature needs to show. A compile-time evaluation that fell back to emitting gates without any diagnostic still computes the correct answer and still passes every check above.
So the in-game program is one half. The other half is a compiler-side test that asserts the structure: that the gate count is what it should be, that a particular gate class is absent, that a value was baked into a component rather than wired. Together they cover both questions, and neither covers both alone.