Source
tests/purrc/string_sys_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
// System-level test for the stdlib `string` module: one cohesive text-processing4
// pipeline that exercises EVERY public function in stdlib/string/string.hpp in a5
// single program (not isolated per-function prints — that is what the compile-run6
// suite string_cr_test.cpp does).7
//8
// The program takes a messy, multi-line "inventory note", then:9
// - normalizes it (splitlines, strip/lstrip/rstrip)10
// - re-cases it (upper, lower, capitalize, title, swapcase, capwords)11
// - probes it (startswith, endswith, contains, find, rfind, count)12
// - tokenizes it (split-on-sep, split-on-whitespace, join, replace)13
// - classifies tokens (isdigit, isalpha, isalnum, isupper, islower, isspace)14
// - formats a table (ljust, rjust, center, zfill)15
//16
// Every function in the header appears: case ops (upper/lower/capitalize/title/17
// swapcase), trimming (strip/lstrip/rstrip), search-test (startswith/endswith/18
// contains/find/rfind/count), transform (replace/split[2 overloads]/splitlines/19
// capwords/join), padding (ljust/rjust/center/zfill), classifiers (isdigit/20
// isalpha/isalnum/isspace/isupper/islower).21
//22
// List-returning split/splitlines are join()ed before printing; booleans go23
// through io.str (print as True/False). Fully DETERMINISTIC — stdout is asserted24
// byte-for-byte. Note: purrc treats a newline as a statement terminator, so each25
// call stays on one source line and wide rows are built with `+` concatenation;26
// only \n \t \" \\ escapes are supported by the lexer (no \r).27
#include "e2e_harness.hpp"29
TEST(StdlibE2E, String) {30
e2e::expect_e2e("string_sys", R"PURR(import io31
import string33
let raw = " RaW inVENTORY \nsku001, sku002 , Cat-Food\nQTY: 42 units\n"35
# ---------------- normalize ----------------36
io.print(string.center(" REPORT ", 36, "="))38
# splitlines breaks the raw note into logical lines39
let lines = string.splitlines(raw)40
io.print(string.join(" / ", lines))42
# strip / lstrip / rstrip clean the first line's edges43
let header = lines[0]44
io.print("strip :" + string.strip(header))45
io.print("lstrip :" + string.lstrip(header))46
io.print("rstrip :" + string.rstrip(header) + "|")48
# ---------------- case ops ----------------49
let clean = string.strip(header)50
io.print("upper :" + string.upper(clean))51
io.print("lower :" + string.lower(clean))52
io.print("cap :" + string.capitalize(clean))53
io.print("title :" + string.title(clean))54
io.print("swap :" + string.swapcase(clean))55
io.print("capwords:" + string.capwords(raw))57
# ---------------- search / test ----------------58
let line2 = string.strip(lines[1])59
io.print("starts :" + io.str(string.startswith(line2, "sku")))60
io.print("ends :" + io.str(string.endswith(line2, "Food")))61
io.print("has :" + io.str(string.contains(line2, "Cat")))62
io.print("find :" + io.str(string.find(line2, "sku")))63
io.print("rfind :" + io.str(string.rfind(line2, "sku")))64
io.print("count :" + io.str(string.count(line2, "sku")))66
# ---------------- transform: split on comma ----------------67
let parts = string.split(line2, ",")68
io.print("nparts :" + io.str(len(parts)))69
io.print(string.ljust("", 36, "-"))71
# clean each comma-part, then split each on whitespace (ws split)72
let i = 073
while i < len(parts) {74
let tok = string.strip(parts[i])75
let words = string.split(tok)76
let joined = string.join("_", words)77
let norm = string.replace(joined, "-", "")78
# classify the normalized token79
let kind = "mixed"80
if string.isdigit(norm) {81
kind = "digit"82
}83
if string.isalpha(norm) {84
kind = "alpha"85
}86
if string.isalnum(norm) {87
if kind == "mixed" {88
kind = "alnum"89
}90
}91
let col = string.ljust(string.lower(norm), 12)92
io.print(col + string.rjust(kind, 8))93
i = i + 194
}95
io.print(string.ljust("", 36, "-"))97
# ---------------- numeric field: line 3 "QTY: 42 units" ----------------98
let line3 = lines[2]99
let qparts = string.split(line3)100
let qty = qparts[1]101
io.print("qty.isdigit :" + io.str(string.isdigit(qty)))102
io.print("qty.zfill :" + string.zfill(qty, 6))103
io.print("qty.center :" + string.center(qty, 8, "."))105
# ---------------- classifier sweep ----------------106
io.print("upper? ABC :" + io.str(string.isupper("ABC")))107
io.print("lower? abc :" + io.str(string.islower("abc")))108
io.print("space? :" + io.str(string.isspace(" \t")))110
io.print(string.center(" END ", 36, "="))111
)PURR",112
"============== REPORT ==============\n"113
" RaW inVENTORY / sku001, sku002 , Cat-Food / QTY: 42 units\n"114
"strip :RaW inVENTORY\n"115
"lstrip :RaW inVENTORY \n"116
"rstrip : RaW inVENTORY|\n"117
"upper :RAW INVENTORY\n"118
"lower :raw inventory\n"119
"cap :Raw inventory\n"120
"title :Raw Inventory\n"121
"swap :rAw INventory\n"122
"capwords:Raw Inventory Sku001, Sku002 , Cat-food Qty: 42 Units\n"123
"starts :True\n"124
"ends :True\n"125
"has :True\n"126
"find :0\n"127
"rfind :8\n"128
"count :2\n"129
"nparts :3\n"130
"------------------------------------\n"131
"sku001 alnum\n"132
"sku002 alnum\n"133
"catfood alpha\n"134
"------------------------------------\n"135
"qty.isdigit :True\n"136
"qty.zfill :000042\n"137
"qty.center :...42...\n"138
"upper? ABC :True\n"139
"lower? abc :True\n"140
"space? :True\n"141
"=============== END ================\n");142
}