Source
tests/purrc/thread_cr_test.cpp
1
// Copyright (c) 2026 BigBrain LLC. MIT-licensed (see LICENSE).2
// Original work; see ACKNOWLEDGMENTS.md for the open-source ideas we build upon.3
// Compile-run unit tests for the `thread` module: one test per purr-callable entry point4
// (spawn, Thread.join, Thread.joinable). Each writes a tiny .purr, compiles it with purrc, runs5
// it under the cheatah runtime, and asserts the EXACT stdout — so every program is deterministic:6
// a worker's output is only observed after its join (or the `with` guard's scope exit), never7
// raced against the main thread's. Complements the in-process unit tests8
// (stdlib/tests/thread_test.cpp) and the per-module system test (StdlibE2E.Thread).9
#include "e2e_harness.hpp"11
TEST(ThreadCompileRun, SpawnJoin) {12
e2e::expect_e2e("thread_spawn_join", R"PURR(import io13
import thread15
fn worker(n : int, tag : str) {16
io.print(tag, n * 2)17
}19
let t = thread.spawn(worker, 21, "spawned:")20
t.join()21
io.print("joined")22
)PURR",23
"spawned: 42\njoined\n");24
}26
TEST(ThreadCompileRun, Joinable) {27
// Both joinable() readings are SAMPLED around the join but PRINTED after it, so the28
// worker's line always lands first: printing the pre-join reading directly would race29
// the worker's own output (this test did, and flaked under load).30
e2e::expect_e2e("thread_joinable", R"PURR(import io31
import thread33
fn worker() {34
io.print("ran")35
}37
let t = thread.spawn(worker)38
let before = t.joinable()39
t.join()40
let after = t.joinable()41
io.print(before)42
io.print(after)43
)PURR",44
"ran\nTrue\nFalse\n");45
}47
TEST(ThreadCompileRun, JoinCatchesWorkerRaise) {48
// A worker's `raise` does not kill the program: it re-surfaces at join(), catchable in-language.49
e2e::expect_e2e("thread_join_raise", R"PURR(import io50
import thread52
fn fails() {53
raise "boom from worker"54
}56
let t = thread.spawn(fails)57
try {58
t.join()59
} except e {60
io.print("caught:", e)61
}62
io.print("alive")63
)PURR",64
"caught: boom from worker\nalive\n");65
}67
TEST(ThreadCompileRun, WithGuardJoinsAtScopeExit) {68
// The RAII story: a Thread held by `with` joins on scope exit, so the worker's line is69
// guaranteed to precede anything printed after the block.70
e2e::expect_e2e("thread_with_guard", R"PURR(import io71
import thread73
fn worker(tag : str) {74
io.print("worker", tag)75
}77
with thread.spawn(worker, "A") {78
}79
io.print("after")80
)PURR",81
"worker A\nafter\n");82
}84
TEST(ThreadCompileRun, SpawnForwardsTheCollectorShapedSignature) {85
// The signature shape the module exists for: several plain-value args (str/int/float/bool),86
// each COPIED into its thread; threads joined one at a time so stdout stays deterministic.87
e2e::expect_e2e("thread_collector_shape", R"PURR(import io88
import thread90
fn collect(venue : str, product : str, depth : int, funding : bool) {91
io.print(venue, product, depth, funding)92
}94
let a = thread.spawn(collect, "alpha", "X-Y", 10, false)95
a.join()96
let b = thread.spawn(collect, "beta", "Y-Z", 20, true)97
b.join()98
)PURR",99
"alpha X-Y 10 False\nbeta Y-Z 20 True\n");100
}