cheatah
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-processing
4// pipeline that exercises EVERY public function in stdlib/string/string.hpp in a
5// single program (not isolated per-function prints — that is what the compile-run
6// 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 go
23// through io.str (print as True/False). Fully DETERMINISTIC — stdout is asserted
24// byte-for-byte. Note: purrc treats a newline as a statement terminator, so each
25// 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"
29TEST(StdlibE2E, String) {
30 e2e::expect_e2e("string_sys", R"PURR(import io
31import string
33let raw = " RaW inVENTORY \nsku001, sku002 , Cat-Food\nQTY: 42 units\n"
35# ---------------- normalize ----------------
36io.print(string.center(" REPORT ", 36, "="))
38# splitlines breaks the raw note into logical lines
39let lines = string.splitlines(raw)
40io.print(string.join(" / ", lines))
42# strip / lstrip / rstrip clean the first line's edges
43let header = lines[0]
44io.print("strip :" + string.strip(header))
45io.print("lstrip :" + string.lstrip(header))
46io.print("rstrip :" + string.rstrip(header) + "|")
48# ---------------- case ops ----------------
49let clean = string.strip(header)
50io.print("upper :" + string.upper(clean))
51io.print("lower :" + string.lower(clean))
52io.print("cap :" + string.capitalize(clean))
53io.print("title :" + string.title(clean))
54io.print("swap :" + string.swapcase(clean))
55io.print("capwords:" + string.capwords(raw))
57# ---------------- search / test ----------------
58let line2 = string.strip(lines[1])
59io.print("starts :" + io.str(string.startswith(line2, "sku")))
60io.print("ends :" + io.str(string.endswith(line2, "Food")))
61io.print("has :" + io.str(string.contains(line2, "Cat")))
62io.print("find :" + io.str(string.find(line2, "sku")))
63io.print("rfind :" + io.str(string.rfind(line2, "sku")))
64io.print("count :" + io.str(string.count(line2, "sku")))
66# ---------------- transform: split on comma ----------------
67let parts = string.split(line2, ",")
68io.print("nparts :" + io.str(len(parts)))
69io.print(string.ljust("", 36, "-"))
71# clean each comma-part, then split each on whitespace (ws split)
72let i = 0
73while 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 token
79 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 + 1
95io.print(string.ljust("", 36, "-"))
97# ---------------- numeric field: line 3 "QTY: 42 units" ----------------
98let line3 = lines[2]
99let qparts = string.split(line3)
100let qty = qparts[1]
101io.print("qty.isdigit :" + io.str(string.isdigit(qty)))
102io.print("qty.zfill :" + string.zfill(qty, 6))
103io.print("qty.center :" + string.center(qty, 8, "."))
105# ---------------- classifier sweep ----------------
106io.print("upper? ABC :" + io.str(string.isupper("ABC")))
107io.print("lower? abc :" + io.str(string.islower("abc")))
108io.print("space? :" + io.str(string.isspace(" \t")))
110io.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");