Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

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.