Writing & Running Tests
SuperJ has two test mechanisms. This guide covers both, but most SDK and application testing uses the first.
Unit tests (sj.test) | Golden-output tests | |
|---|---|---|
| Lives in | test/ | examples/ |
| You write | @Test methods with Asserts.* | a program that prints, plus expected_<name>.txt |
| Run with | superj test | superj golden |
| A pass means | every assertion held (exit code 0) | stdout matched the expected file byte-for-byte |
| Use it for | library/logic checks — maps, parsers, math, pools | conformance — the printed output IS the spec |
Rule of thumb: if you'd reach for assertEquals, write a unit test; if you'd println and eyeball the result, write a golden test.
Unit tests with sj.test
A first test
Create a file under test/ ending in Test.sj, mark it // UNIT, and write @Test static void methods. No main, no registration — the compiler wires it up:
// UNIT
import sj.util.IntArrayList;
import sj.test.Asserts;
class IntArrayListTest {
@Test
static void startsEmpty() {
IntArrayList list = new IntArrayList();
Asserts.assertTrue("new list is empty", list.isEmpty());
Asserts.assertEquals("size", 0, list.size());
}
@Test
static void addAndGet() {
IntArrayList list = new IntArrayList();
list.add(10);
list.add(20);
Asserts.assertEquals("size", 2, list.size());
Asserts.assertEquals("first", 10, list.get(0));
}
}
Run it:
superj test # build compiler + SDK, then run every test/ suite
Output:
test IntArrayListTest.startsEmpty ... ok
test IntArrayListTest.addAndGet ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 filtered out
Rules
- File: under
test/, with// UNITin the first five lines (the markersuperj testlooks for). One class per file; name it<Thing>Test.sj. @Testmarks astatic voidno-arg method. The compiler synthesizes the runnermainthat registers every@Testas"ClassName.methodName"and exits with the failure count. (Methods without@Test— helpers,capture()targets — are never run as tests.) It must bestaticbecause, with no reflection (SuperJ removed it), the runner can't instantiate a class or invoke an instance method at runtime — so registration is a compile-time method reference (ClassName::method, as in the explicitmainbelow), and a no-arg method reference with no receiver has no instance to bind to.- A test passes by returning and fails by throwing. An
Assertsfailure throwsAssertionFailure; any otherThrowable(e.g. an unexpected NPE) also counts as a failure, not a crash of the whole run. - If you need a custom entry point you may still write an explicit
main(build aTestRunnerand callt.add("name", Class::method)by hand) — an explicitmainsuppresses the synthesized one.@Testis the common path.
Assertions — sj.test.Asserts
All are static, message-first, and throw AssertionFailure on failure (the message is printed under the FAILED line):
| Method | Notes |
|---|---|
assertTrue(msg, cond) / assertTrue(cond) | |
assertFalse(msg, cond) / assertFalse(cond) | |
assertEquals(msg, expected, actual) | overloads for int, long, boolean, double, String, Object (via .equals) |
assertEquals(msg, expected, actual, epsilon) | double within tolerance — use for computed floats |
assertNotEquals(msg, unexpected, actual) | int, long |
assertNull(msg, value) / assertNotNull(msg, value) | |
fail(msg) | unconditional failure |
capture(TestCase body) → Throwable | runs body, returns what it threw (or null) — for exception tests |
Testing that something throws
No reflection: capture() runs a body and hands you whatever it threw, then you check it with instanceof. The body is a method reference to a non-@Test helper.
class PoolTest {
@Test
static void rejectsNull() {
Throwable t = Asserts.capture(PoolTest::offerNull);
Asserts.assertTrue("throws IAE", t instanceof IllegalArgumentException);
}
static void offerNull() { // helper — no @Test
new ObjectPool<Box>(2, null).offer(null);
}
}
Skipping a test — @Ignore
Add @Ignore (with @Test) to park a known-failing or flaky test. It is reported ... ignored, never run, and never counts as a failure:
@Test
@Ignore
static void flakyUntilFixed() {
Asserts.assertEquals("known bad", 1, 2); // skipped — suite stays green
}
test SomeTest.flakyUntilFixed ... ignored
test result: ok. 4 passed; 0 failed; 1 ignored; 0 filtered out
@Ignore without @Test is a compile error.
Reading SDK internals from a test — @VisibleForTesting
A test lives in a different package from the SDK class it exercises, so a private field or method of an SDK class is normally unreachable. Making it public pollutes the API; leaving it private makes the internal untestable.
@VisibleForTesting relaxes the private-access check only when the compiler is in test mode (i.e. superj test). Mark the member private and annotate it; a @Test in another package can then read/call it, but every non-test compile still enforces the private boundary:
// in an SDK class (sj.crypto.TlsEngineBase)
@VisibleForTesting
private static byte[] LABEL_BYTES_DERIVED_SECRET = encodeTls13Label("derived");
// in a test (default package) — test mode relaxes the private check
@Test
static void derivedSecretLabelAccessibleAcrossPackage() {
byte[] label = TlsEngineBase.LABEL_BYTES_DERIVED_SECRET;
Asserts.assertNotNull("field accessible via @VisibleForTesting", label);
Asserts.assertEquals("label length", 13, label.length);
}
A regular superj compile (non-test) that tries the same access is still rejected with field '...' has private access in '...' — the relaxation only applies under superj test. The annotation works on private fields and private methods.
A/B differential tests
There's no special feature for this — compute a reference (oracle) result and the optimized/real result in one @Test, then assertEquals them over many inputs. Example: an insertion-sort oracle vs the SDK's Arrays.sort over seeded-random arrays (see test/SortDifferentialTest.sj):
@Test
static void arraysSortMatchesReference() {
Random rng = new Random(12345L);
for (int trial = 0; trial < 500; trial = trial + 1) {
int n = rng.nextInt(64);
int[] data = new int[n];
for (int i = 0; i < n; i = i + 1) data[i] = rng.nextInt(1000) - 500;
int[] expected = referenceSort(data); // A: trivially-correct oracle
int[] actual = new int[n]; // B: the path under test
System.arraycopy(data, 0, actual, 0, n);
Arrays.sort(actual);
for (int i = 0; i < n; i = i + 1)
Asserts.assertEquals("trial " + trial + " idx " + i, expected[i], actual[i]);
}
}
Running tests
superj test # the standard gate: build + run all of test/
superj test # same, if compiler + SDK are already built (dir defaults to test/)
superj test test --filter pool # only cases whose name contains "pool"
superj test:
- discovers every
// UNITfile under the directory (defaulttest/), - compiles each suite against the SDK (archive
--sdk-pathfast path first, falling back to whole-program--sdk-sourceif no archive is present) and links it, - runs it, and aggregates pass/fail by exit code — a non-zero suite makes
superj testexit non-zero (CI-ready).
Options: --filter <substr> (forwarded to each suite; runs only matching cases), --sdk-path <dir> (default $SJ_HOME/sdk), --sdk-source <dir> (default $SJ_HOME/sdk/sj — VIP install only), --clang-path <path>.
Project mode: caching, suite selection, parallel builds
Inside a project/workspace (superj test / superj test -p <member>), the runner additionally:
- Caches suite binaries. Each suite binary under
target/test/is stamped with the hash of [library sources + SDK variant + compiler version + flags + the suite file itself] — the same machinery assuperj build's up-to-date check. An unchanged suite skips compile+link entirely (its tests still run); editing one test file rebuilds exactly one binary, editing asrc/helper rebuilds all. Each result line shows provenance and timing:PASS test/MemPoolTest.sj (312ms, cached).superj cleanclears the cache. --suite <substr>selects suites by file path before the build loop — the single-suite edit-compile-run cycle no longer pays for the other suites. (--filterremains the runtime case filter within a suite.)--jobs <n>builds out-of-date suites in parallel (default: online cores, capped by the number of suites to build; the links are independent and dominate wall time). Compile failures point at a per-suitetarget/test/<suite>.buildlog.--debugis forwarded to each suite compile (DWARF line info, -O0 link). When a suite binary dies on a signal (exit code > 128, e.g. a segfault from a null deref or stack overflow), the runner automatically re-runs it underlldb(macOS) /gdb(Linux) and prints the faulting.sjsource line, so a crashing test reportsfile:lineinstead of a bare "Segmentation fault". Crashes inside native C leaf functions (which lack frame pointers) print the signal/stop reason but may not resolve a.sjframe.
The summary line reports cache effectiveness: test: 45 passed, 0 failed (1 built, 44 cached, jobs=10).
Under an enterprise SDK (precompiled module, no archive), suite builds fast-link against a cached native compile of the SDK instead of re-optimizing the merged whole-program IR per suite — a changed suite rebuilds in ~0.3 s instead of ~8 s. The prelink object is built once (serially, before any --jobs fan-out) and cached beside the SDK module.
Gotchas
- Unit tests are not run by the golden suite — they're a separate step. A change to SDK logic should run both
superj goldenandsuperj test. - Rebuild the SDK after a compiler change before running either suite, or stale-archive link errors look like test failures.
- Suites compile against the SDK (archive fast path,
--sdk-sourcefallback), so cross-classstatic final intconstants read from another class can surface as0in some situations; prefer a method or a same-class constant if you hit it.
Golden-output tests
For conformance and compiler behavior, where the printed output is the assertion. Put a program in examples/<name>.sj and its expected stdout in examples/expected_<name>.txt; superj golden compiles, runs, and diffs.
// examples/test_my_feature.sj
public class test_my_feature {
public static void main(String[] args) {
System.out.println("answer=" + (6 * 7));
}
}
# examples/expected_test_my_feature.txt
answer=42
superj golden # runs all categories: positive, negative,
# smoke, SDK positive/smoke/compile
Other golden conventions (first five lines of the file): // ERROR: <substr> marks a negative test that must fail to compile with that message; // WARNING: <substr> a compile that must warn. Don't put unit-logic assertions in a golden test — every new check forces regenerating the golden file, and the first mismatch hides the rest. Use a sj.test unit test instead.