Source
tests/purrc/sys_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 `sys` module (suite SysCompileRun) PLUS its4
// per-module system-level test (StdlibE2E.Sys — both live here because sys's5
// whole surface is `sys.argv`). Each test writes a tiny .purr, compiles it with6
// purrc, runs it under the cheatah runtime, and asserts the exact stdout.7
// Complements the in-process unit tests (stdlib/tests/sys_test.cpp).8
//9
// The runtime invocation is what is under test: `cheatah <module> [args…]`10
// forwards `<module>` + the user args through the cheatah_set_argv hook, so11
// sys.argv[0] is the module path and sys.argv[1:] the user arguments (Python's12
// convention). The module path is a build-dir temp path — NON-deterministic —13
// so programs never print argv[0] raw; they assert deterministic PROPERTIES of14
// it (non-empty, ends with ".so") and print only the user arguments, which the15
// tests fully control.16
#include "e2e_harness.hpp"18
namespace {20
// Like e2e::expect_e2e, but appends user arguments to the cheatah invocation —21
// `cheatah <module> <args…>` — so a program can observe sys.argv[1:]. `args` is22
// spliced into the shell command verbatim; callers pass pre-quoted words23
// (e.g. "alpha 'two words' ''").24
void expect_e2e_with_args(const std::string& name, const std::string& src,25
const std::string& args, const std::string& expected) {26
const std::string tmp = PURR_TEST_TMP;27
const std::string purr = tmp + "/" + name + "_e2e.purr";28
const std::string mod = tmp + "/" + name + "_e2e.so";29
{ std::ofstream f(purr); f << src; }31
const std::string compile =32
std::string(PURRC_PATH) + " \"" + purr + "\" -o \"" + mod + "\"";33
ASSERT_EQ(std::system(compile.c_str()), 0) << name << ": purrc failed to compile the program";35
int rc = -1;36
const std::string out = e2e::run_capture(37
"\"" + std::string(CHEATAH_RUNTIME_PATH) + "\" \"" + mod + "\" " + args, rc);38
EXPECT_EQ(rc, 0) << name << ": program exited non-zero";39
EXPECT_EQ(out, expected) << name << ": stdout mismatch";40
}42
} // namespace44
// With no user arguments, sys.argv is exactly [module]: length 1, and argv[0]45
// is the module path the runtime was given (a real path to the loaded .so).46
TEST(SysCompileRun, ArgvNoUserArguments) {47
e2e::expect_e2e("sys_argv_noargs", R"PURR(import io48
import string49
import sys50
io.print(len(sys.argv))51
io.print(len(sys.argv[0]) > 0, string.endswith(sys.argv[0], ".so"))52
)PURR", "1\nTrue True\n");53
}55
// User arguments land in sys.argv[1:] in order, with argv[0] still the module.56
TEST(SysCompileRun, ArgvForwardsUserArguments) {57
expect_e2e_with_args("sys_argv_forward", R"PURR(import io58
import sys59
io.print(len(sys.argv))60
io.print(sys.argv[1], sys.argv[2], sys.argv[3])61
)PURR", "alpha beta gamma", "4\nalpha beta gamma\n");62
}64
// Arguments are forwarded VERBATIM: an argument with an embedded space stays one65
// argument, and an empty argument stays present (and empty) rather than dropped.66
TEST(SysCompileRun, ArgvPreservesSpacedAndEmptyArguments) {67
expect_e2e_with_args("sys_argv_verbatim", R"PURR(import io68
import sys69
io.print(len(sys.argv))70
io.print("[" + sys.argv[1] + "]")71
io.print("[" + sys.argv[2] + "]")72
)PURR", "'two words' ''", "3\n[two words]\n[]\n");73
}75
// sys.argv[1:] is the user-argument list — sliceable and joinable like any list.76
TEST(SysCompileRun, ArgvSliceOfUserArguments) {77
expect_e2e_with_args("sys_argv_slice", R"PURR(import io78
import string79
import sys80
io.print(string.join("|", sys.argv[1:]))81
)PURR", "a b c", "a|b|c\n");82
}84
// sys.argv is iterable (`for a in sys.argv { … }`) — the loop sees every entry.85
TEST(SysCompileRun, ArgvIsIterable) {86
expect_e2e_with_args("sys_argv_iter", R"PURR(import io87
import sys88
let n = 089
for a in sys.argv {90
n = n + 191
}92
io.print(n, len(sys.argv))93
io.print(n == len(sys.argv))94
)PURR", "one two", "3 3\nTrue\n");95
}97
// System-level test: one cohesive program exercising the whole sys surface —98
// length, the argv[0] module-path properties, indexing, slicing, and iteration —99
// printing only deterministic values (the user args + boolean properties).100
TEST(StdlibE2E, Sys) {101
expect_e2e_with_args("sys_sys", R"PURR(import io102
import string103
import sys105
# The runtime forwarded [module, x, y, z].106
io.print(len(sys.argv))108
# argv[0] is the module path: never printed raw (build-dir path), only its properties.109
io.print(len(sys.argv[0]) > 0, string.endswith(sys.argv[0], ".so"))111
# Indexing and slicing follow Python: argv[1:] is exactly the user arguments.112
io.print(sys.argv[1], sys.argv[2], sys.argv[3])113
io.print(string.join(",", sys.argv[1:]))115
# Iteration visits every entry once, in order (skip the path at index 0).116
let seen = 0117
let joined = ""118
for a in sys.argv {119
if seen > 0 {120
joined = joined + "/" + a121
}122
seen = seen + 1123
}124
io.print(seen, joined)125
)PURR", "x y z",126
"4\n"127
"True True\n"128
"x y z\n"129
"x,y,z\n"130
"4 /x/y/z\n");131
}