SCUA

How-to

Test your code

SCUA has a built-in test runner. You write test functions in *_test.scua files, assert what you expect, and run scua test. It reports pass/fail and exits non-zero on failure, so it fits straight into CI.

#A first test

Put your tests in a file whose name ends in _test.scua, and write each test as a function named test_*:

-- inventory_test.scua
fn add_item(bag, item)
  bag.push(item)
  return bag
end

fn test_add_one()
  let bag = add_item([], "sword")
  assert_eq(len(bag), 1)
end

fn test_add_two()
  let bag = add_item(add_item([], "sword"), "shield")
  assert_eq(bag[1], "shield")
end

Run the tests from the directory that holds them:

$ scua test
PASS  ./inventory_test.scua  test_add_one
PASS  ./inventory_test.scua  test_add_two

2 passed, 0 failed (1 file)

scua test finds every *_test.scua file under the current directory (or a directory you name: scua test tests/, or a single file: scua test one_test.scua). You can name several — files, directories, or a mix — and all of them run: scua test tests/ extra_test.scua. Each test_* function runs on its own, so one failing test doesn't stop the others.

If your project has dependencies — a scua.toml and a vendored deps/ — your tests resolve them exactly as running the program does; there is nothing to configure. For a tree with no lock, --mod-path DIR adds a module search directory. Those are the only options scua test takes, and it will tell you so rather than ignore one it does not understand.

#Assertions

The runner doesn't need a special assertion library; the assert family is built in.

  • assert(cond, msg?) faults with msg unless cond is truthy.
  • assert_eq(got, want) faults unless the two values are equal, and the message shows both.
  • assert_ne(a, b) faults if the two values are equal.
  • assert_near(got, want, eps) faults unless got and want are within eps of each other. Use it for floats, which aren't exact.

A failing comparison tells you what it saw:

fn test_math()
  assert_eq(2 + 2, 5)
end
$ scua test
FAIL  ./math_test.scua  test_math assert_eq failed: expected 5, got 4

0 passed, 1 failed (1 file)

#How a test fails

A test fails when it raises a fault that nothing catches: a failed assert, a runtime fault like an out-of-range index, or an explicit error(...). Anything that would stop a normal program stops the test and marks it failed, with the located message. A test that runs to the end without faulting passes.

Because each test_* runs in isolation, a fault in one is reported and the rest keep going. If a file has a syntax, type, or compile error, the whole file is reported as an error and its tests count as failures.

#Files without test functions

If a *_test.scua file has no test_* functions, the whole file is treated as a single test: it passes if it runs without an uncaught fault. This is handy for a quick check or a script-style test.

-- smoke_test.scua
assert(1 < 2, "basic sanity")
assert_near(0.1 + 0.2, 0.3, 0.001)

#Testing code that needs a capability

scua test cannot be handed a capability grant. A file that says import net (or http, or fs) is therefore unreachable from tests — including the functions in it that only shuffle bytes and touch nothing. Splitting the file in two gets the pure half back, but then the language has chosen your architecture for you.

The way out is to take the capability as a parameter instead of importing it. The code under test never names the real thing, so it needs no grant, and a test hands it a stand-in you wrote by hand:

-- transport_test.scua
import str

-- The network is a parameter, not an import: `send` is fn(url) -> Ok(body) | Error(e).
-- Nothing in this file imports `http`, so `scua test` needs no grant to reach it.
fn shout_page(send, url)
  let body = send(url)?
  return Ok(str.upper(body))
end

fn test_body_is_shouted()
  let fake = fn(u) return Ok(`hello from {u}`) end
  match shout_page(fake, "example.com")
    Ok(b)    -> assert_eq(b, "HELLO FROM EXAMPLE.COM")
    Error(e) -> assert(false, `unexpected error: {e}`)
  end
end

fn test_a_refused_connection_propagates()
  let broken = fn(u) return Error("connection refused") end
  match shout_page(broken, "example.com")
    Ok(b)    -> assert(false, `expected an error, got {b}`)
    Error(e) -> assert_eq(e, "connection refused")
  end
end
$ scua test transport_test.scua
PASS  transport_test.scua  test_body_is_shouted
PASS  transport_test.scua  test_a_refused_connection_propagates

2 passed, 0 failed (1 file)

No grant on that command line, and the error path is as easy to test as the happy one — a refused connection is a function that returns Error, not a network you have to arrange.

The real capability is bound in an adapter: the one small place that names it.

import http

-- The only file that names the capability. Pure delegation, and the only part
-- a hand-written stand-in cannot replace.
fn http_transport()
  return fn(url)
    let resp = http.get(url)?
    return Ok(resp.body)
  end
end

print(shout_page(http_transport(), "http://example.com"))

Be honest about what this does and doesn't buy you. The adapter itself stays untested — it needs the grant, so scua test cannot reach it. That is a real gap, but it is a handful of lines of delegation with no branching, rather than your whole library.

The pattern pays for itself a second time, which is the reason to reach for it even where testing isn't the motive: a host embedding SCUA can supply its own transport too. The same parameter that takes your stand-in in a test takes the host's HTTP client in production. A library written this way works under a host that has no http capability to grant, because it never asked for one.

#A note on actors

The runner shines for synchronous logic: pure functions, data transforms, validation, anything you can call and assert on directly. Testing a partition's message handling is harder, because a partition processes its messages after your test function has already returned, so you can't tell an actor and assert on the result in the same test. For now, test the plain functions your handlers call (keep the logic out of the handler where you can), and check end-to-end actor behaviour by running a small program and looking at its output.