cheatah
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 point
4// (spawn, Thread.join, Thread.joinable). Each writes a tiny .purr, compiles it with purrc, runs
5// 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), never
7// raced against the main thread's. Complements the in-process unit tests
8// (stdlib/tests/thread_test.cpp) and the per-module system test (StdlibE2E.Thread).
9#include "e2e_harness.hpp"
11TEST(ThreadCompileRun, SpawnJoin) {
12 e2e::expect_e2e("thread_spawn_join", R"PURR(import io
13import thread
15fn worker(n : int, tag : str) {
16 io.print(tag, n * 2)
19let t = thread.spawn(worker, 21, "spawned:")
20t.join()
21io.print("joined")
22)PURR",
23 "spawned: 42\njoined\n");
26TEST(ThreadCompileRun, Joinable) {
27 // Both joinable() readings are SAMPLED around the join but PRINTED after it, so the
28 // worker's line always lands first: printing the pre-join reading directly would race
29 // the worker's own output (this test did, and flaked under load).
30 e2e::expect_e2e("thread_joinable", R"PURR(import io
31import thread
33fn worker() {
34 io.print("ran")
37let t = thread.spawn(worker)
38let before = t.joinable()
39t.join()
40let after = t.joinable()
41io.print(before)
42io.print(after)
43)PURR",
44 "ran\nTrue\nFalse\n");
47TEST(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 io
50import thread
52fn fails() {
53 raise "boom from worker"
56let t = thread.spawn(fails)
57try {
58 t.join()
59} except e {
60 io.print("caught:", e)
62io.print("alive")
63)PURR",
64 "caught: boom from worker\nalive\n");
67TEST(ThreadCompileRun, WithGuardJoinsAtScopeExit) {
68 // The RAII story: a Thread held by `with` joins on scope exit, so the worker's line is
69 // guaranteed to precede anything printed after the block.
70 e2e::expect_e2e("thread_with_guard", R"PURR(import io
71import thread
73fn worker(tag : str) {
74 io.print("worker", tag)
77with thread.spawn(worker, "A") {
79io.print("after")
80)PURR",
81 "worker A\nafter\n");
84TEST(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 io
88import thread
90fn collect(venue : str, product : str, depth : int, funding : bool) {
91 io.print(venue, product, depth, funding)
94let a = thread.spawn(collect, "alpha", "X-Y", 10, false)
95a.join()
96let b = thread.spawn(collect, "beta", "Y-Z", 20, true)
97b.join()
98)PURR",
99 "alpha X-Y 10 False\nbeta Y-Z 20 True\n");