diff --git a/runtime/test.el b/runtime/test.el new file mode 100644 index 0000000..3ee7853 --- /dev/null +++ b/runtime/test.el @@ -0,0 +1,320 @@ +// runtime/test.el — El test framework: assertions, registration, and runner. +// +// Provides a minimal but complete test harness for El programs. No external +// dependencies. Written entirely in El using existing runtime primitives. +// +// ── Quick-start ────────────────────────────────────────────────────────────── +// +// 1. Write a test function with the standard test signature: +// +// fn test_str_eq_works(_: String) -> String { +// assert_true(str_eq("a", "a"), "same strings are equal") +// assert_false(str_eq("a", "b"), "different strings are not equal") +// return "" +// } +// +// 2. Register it and run: +// +// fn main() -> Void { +// test_case("str_eq works", "test_str_eq_works") +// test_run_all() +// } +// +// Test functions must have the signature (String) -> String. The argument is +// a dummy passed by the threading mechanism (see runtime/thread.el) and should +// be ignored. The return value is likewise ignored — results flow through the +// state-based assertion primitives. +// +// ── State keys ─────────────────────────────────────────────────────────────── +// +// _test_cases JSON array of {"name":"...","fn":"..."} +// _test_pass_count Int as string — total passing assertions +// _test_fail_count Int as string — total failing assertions +// _test_failures JSON array of failure message strings +// _test_current Name of the test case currently executing +// +// ── Dependencies ───────────────────────────────────────────────────────────── +// +// runtime/string.el — str_eq, str_concat, str_contains, int_to_str, ... +// runtime/state.el — state_set, state_get +// runtime/json.el — json_array_len, json_array_get_string, json_get, +// json_escape_string +// runtime/thread.el — spawn, join (for dynamic dispatch via dlsym) + +// ── Internal: JSON array helpers ───────────────────────────────────────────── +// +// _test_json_append — append a quoted, escaped string element to a JSON array. +// +// Given an existing JSON array string (e.g. '["a","b"]') and a plain string +// value, returns a new array with the value appended (e.g. '["a","b","c"]'). +// +// The array must be non-empty — always init with "[]" before calling. +fn _test_json_append(arr: String, val: String) -> String { + let escaped: String = json_escape_string(val) + let inner: String = str_slice(arr, 1, str_len(arr) - 1) + if str_eq(inner, "") { + return "[\"" + escaped + "\"]" + } + return "[" + inner + ",\"" + escaped + "\"]" +} + +// _test_json_obj_append — append a raw JSON object string to a JSON array. +// +// Used to build the _test_cases list where each element is already a +// JSON object (not a plain string). +fn _test_json_obj_append(arr: String, obj: String) -> String { + let inner: String = str_slice(arr, 1, str_len(arr) - 1) + if str_eq(inner, "") { + return "[" + obj + "]" + } + return "[" + inner + "," + obj + "]" +} + +// ── Test registration ───────────────────────────────────────────────────────── + +// test_case — register a named test case. +// +// name: Human-readable test case name (shown in output). +// fn_name: Name of a top-level El function with signature +// (String) -> String. The function should call assertion +// primitives from this module. The String arg it receives is "" +// and its return value is ignored. +// +// Test cases are stored in the _test_cases state key and executed in +// registration order by test_run_all(). +fn test_case(name: String, fn_name: String) { + let arr: String = state_get("_test_cases") + if str_eq(arr, "") { arr = "[]" } + let escaped_name: String = json_escape_string(name) + let escaped_fn: String = json_escape_string(fn_name) + let obj: String = "{\"name\":\"" + escaped_name + "\",\"fn\":\"" + escaped_fn + "\"}" + let arr = _test_json_obj_append(arr, obj) + state_set("_test_cases", arr) +} + +// ── Test state helpers ──────────────────────────────────────────────────────── + +// _test_init — reset all test counters and failure lists. +// +// Called at the start of test_run_all(). Safe to call multiple times. +fn _test_init() { + state_set("_test_pass_count", "0") + state_set("_test_fail_count", "0") + state_set("_test_failures", "[]") + state_set("_test_current", "") +} + +// _test_inc_pass — increment the global pass counter by 1. +fn _test_inc_pass() { + let n: Int = str_to_int(state_get("_test_pass_count")) + state_set("_test_pass_count", int_to_str(n + 1)) +} + +// _test_inc_fail — increment the global fail counter by 1. +fn _test_inc_fail() { + let n: Int = str_to_int(state_get("_test_fail_count")) + state_set("_test_fail_count", int_to_str(n + 1)) +} + +// test_pass — record a passing assertion for the current test case. +// +// Increments the pass counter. Called internally by assertions. +fn test_pass(name: String) { + _test_inc_pass() +} + +// test_fail — record a failing assertion for the current test case. +// +// name: assertion label or description (usually the msg parameter) +// msg: detailed failure message including expected/got values +// +// Increments the fail counter and appends the message to _test_failures. +// Also prints the failure immediately for visibility. +fn test_fail(name: String, msg: String) { + _test_inc_fail() + let failures: String = state_get("_test_failures") + if str_eq(failures, "") { failures = "[]" } + let entry: String = " " + msg + let failures = _test_json_append(failures, entry) + state_set("_test_failures", failures) + println(" FAIL: " + msg) +} + +// ── Assertions ──────────────────────────────────────────────────────────────── + +// assert_true — assert that condition is true. +// +// condition: the boolean value to test +// msg: description shown on failure +fn assert_true(condition: Bool, msg: String) { + if condition { + test_pass(msg) + return + } + let test_name: String = state_get("_test_current") + test_fail(msg, "[" + test_name + "] " + msg + ": expected true, got false") +} + +// assert_false — assert that condition is false. +// +// condition: the boolean value to test +// msg: description shown on failure +fn assert_false(condition: Bool, msg: String) { + if !condition { + test_pass(msg) + return + } + let test_name: String = state_get("_test_current") + test_fail(msg, "[" + test_name + "] " + msg + ": expected false, got true") +} + +// assert_eq — assert that two strings are equal. +// +// a, b: strings to compare +// msg: description shown on failure (quoted values appended automatically) +fn assert_eq(a: String, b: String, msg: String) { + if str_eq(a, b) { + test_pass(msg) + return + } + let test_name: String = state_get("_test_current") + test_fail(msg, "[" + test_name + "] " + msg + ": expected \"" + b + "\", got \"" + a + "\"") +} + +// assert_int_eq — assert that two integers are equal. +// +// a, b: integers to compare +// msg: description shown on failure +fn assert_int_eq(a: Int, b: Int, msg: String) { + if a == b { + test_pass(msg) + return + } + let test_name: String = state_get("_test_current") + test_fail(msg, "[" + test_name + "] " + msg + ": expected " + int_to_str(b) + ", got " + int_to_str(a)) +} + +// assert_neq — assert that two strings are NOT equal. +// +// a, b: strings to compare +// msg: description shown on failure +fn assert_neq(a: String, b: String, msg: String) { + if !str_eq(a, b) { + test_pass(msg) + return + } + let test_name: String = state_get("_test_current") + test_fail(msg, "[" + test_name + "] " + msg + ": expected values to differ, but both are \"" + a + "\"") +} + +// assert_contains — assert that string s contains substring sub. +// +// s: haystack string +// sub: needle substring +// msg: description shown on failure +fn assert_contains(s: String, sub: String, msg: String) { + if str_contains(s, sub) { + test_pass(msg) + return + } + let test_name: String = state_get("_test_current") + test_fail(msg, "[" + test_name + "] " + msg + ": \"" + s + "\" does not contain \"" + sub + "\"") +} + +// assert_starts_with — assert that string s starts with prefix. +// +// s: string to inspect +// prefix: expected prefix +// msg: description shown on failure +fn assert_starts_with(s: String, prefix: String, msg: String) { + if str_starts_with(s, prefix) { + test_pass(msg) + return + } + let test_name: String = state_get("_test_current") + test_fail(msg, "[" + test_name + "] " + msg + ": \"" + s + "\" does not start with \"" + prefix + "\"") +} + +// assert_ends_with — assert that string s ends with suffix. +// +// s: string to inspect +// suffix: expected suffix +// msg: description shown on failure +fn assert_ends_with(s: String, suffix: String, msg: String) { + if str_ends_with(s, suffix) { + test_pass(msg) + return + } + let test_name: String = state_get("_test_current") + test_fail(msg, "[" + test_name + "] " + msg + ": \"" + s + "\" does not end with \"" + suffix + "\"") +} + +// fail — unconditional test failure. +// +// msg: failure message shown in output +// +// Use when a code path that must not be reached is reached, or when an +// expected exception did not occur. +fn fail(msg: String) { + let test_name: String = state_get("_test_current") + test_fail(msg, "[" + test_name + "] " + msg) +} + +// ── Runner ──────────────────────────────────────────────────────────────────── + +// _test_run_one — execute a single registered test case by name. +// +// name: the human-readable test case name (set as _test_current) +// fn_name: the El function to invoke via the thread mechanism +// +// Sets _test_current so that assertions inside the test function know which +// test they belong to. Spawns and immediately joins the test function in a +// child thread (same dlsym mechanism as parallel_map) so dynamic dispatch +// works without needing closures. +fn _test_run_one(name: String, fn_name: String) { + state_set("_test_current", name) + let before_fail: Int = str_to_int(state_get("_test_fail_count")) + let tid: Int = __thread_create(fn_name, "") + __thread_join(tid) + let after_fail: Int = str_to_int(state_get("_test_fail_count")) + if after_fail == before_fail { + println("[test] " + name + " ... PASS") + } else { + println("[test] " + name + " ... FAIL") + } +} + +// test_run_all — execute all registered test cases and print a summary. +// +// Iterates through every test case registered via test_case(), runs each one, +// prints per-test PASS/FAIL status, then prints a summary line. +// +// Returns the total number of failing assertions. Exit with this value to +// signal CI failure: +// +// fn main() -> Int { +// test_case("str_eq", "test_str_eq") +// return test_run_all() +// } +fn test_run_all() -> Int { + _test_init() + + let cases: String = state_get("_test_cases") + if str_eq(cases, "") { cases = "[]" } + + let n: Int = json_array_len(cases) + let i: Int = 0 + while i < n { + let entry: String = json_array_get(cases, i) + let name: String = json_get(entry, "name") + let fn_name: String = json_get(entry, "fn") + _test_run_one(name, fn_name) + i = i + 1 + } + + let pass_count: Int = str_to_int(state_get("_test_pass_count")) + let fail_count: Int = str_to_int(state_get("_test_fail_count")) + println("[test] Summary: " + int_to_str(pass_count) + " passed, " + int_to_str(fail_count) + " failed") + + return fail_count +} diff --git a/tests/runtime/string_test.el b/tests/runtime/string_test.el new file mode 100644 index 0000000..9578e10 --- /dev/null +++ b/tests/runtime/string_test.el @@ -0,0 +1,341 @@ +// tests/runtime/string_test.el — Test suite for runtime/string.el +// +// Exercises every public function exported by runtime/string.el using the +// runtime/test.el framework. Each test function covers one string primitive +// or a tight family of related functions. +// +// ── Build and run ──────────────────────────────────────────────────────────── +// +// # Combine runtime modules + test framework + this file, then compile: +// cat runtime/string.el runtime/math.el runtime/state.el runtime/env.el \ +// runtime/fs.el runtime/exec.el runtime/time.el runtime/json.el \ +// runtime/http.el runtime/engram.el runtime/thread.el \ +// runtime/test.el \ +// tests/runtime/string_test.el > /tmp/string_test_combined.el +// +// ./dist/platform/elc /tmp/string_test_combined.el > /tmp/string_test.c +// cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \ +// -o /tmp/string_test /tmp/string_test.c el-compiler/runtime/el_seed.c +// /tmp/string_test; echo "exit: $?" +// +// Exit code equals the number of failing assertions (0 = all pass). +// +// ── Coverage ───────────────────────────────────────────────────────────────── +// +// str_eq str_neq (via assert_neq) +// str_len str_concat +// str_starts_with str_ends_with +// str_contains str_index_of +// str_slice str_replace +// str_to_upper str_to_lower +// str_trim str_lstrip / str_rstrip +// str_split str_join +// int_to_str str_to_int +// str_repeat str_reverse +// str_count + +// ── str_eq ──────────────────────────────────────────────────────────────────── + +fn test_str_eq(_: String) -> String { + assert_true(str_eq("hello", "hello"), "identical strings are equal") + assert_false(str_eq("hello", "world"), "different strings are not equal") + assert_true(str_eq("", ""), "empty strings are equal") + assert_false(str_eq("a", ""), "non-empty vs empty is not equal") + assert_false(str_eq("", "a"), "empty vs non-empty is not equal") + assert_false(str_eq("Hello", "hello"), "case-sensitive comparison") + return "" +} + +// ── str_len ─────────────────────────────────────────────────────────────────── + +fn test_str_len(_: String) -> String { + assert_int_eq(str_len(""), 0, "empty string has length 0") + assert_int_eq(str_len("a"), 1, "single char has length 1") + assert_int_eq(str_len("hello"), 5, "hello has length 5") + assert_int_eq(str_len("hello world"), 11, "space included in length") + return "" +} + +// ── str_concat ──────────────────────────────────────────────────────────────── + +fn test_str_concat(_: String) -> String { + assert_eq(str_concat("hello", " world"), "hello world", "basic concat") + assert_eq(str_concat("", "world"), "world", "empty prefix") + assert_eq(str_concat("hello", ""), "hello", "empty suffix") + assert_eq(str_concat("", ""), "", "both empty") + return "" +} + +// ── str_starts_with ─────────────────────────────────────────────────────────── + +fn test_str_starts_with(_: String) -> String { + assert_true(str_starts_with("hello world", "hello"), "prefix present") + assert_false(str_starts_with("hello world", "world"), "not a prefix") + assert_true(str_starts_with("hello", "hello"), "string is its own prefix") + assert_true(str_starts_with("hello", ""), "empty prefix always true") + assert_false(str_starts_with("", "a"), "empty string has no prefix") + assert_false(str_starts_with("hi", "hello"), "prefix longer than string") + return "" +} + +// ── str_ends_with ───────────────────────────────────────────────────────────── + +fn test_str_ends_with(_: String) -> String { + assert_true(str_ends_with("hello world", "world"), "suffix present") + assert_false(str_ends_with("hello world", "hello"), "not a suffix") + assert_true(str_ends_with("hello", "hello"), "string is its own suffix") + assert_true(str_ends_with("hello", ""), "empty suffix always true") + assert_false(str_ends_with("", "a"), "empty string has no suffix") + assert_false(str_ends_with("hi", "world"), "suffix longer than string") + return "" +} + +// ── str_contains ───────────────────────────────────────────────────────────── + +fn test_str_contains(_: String) -> String { + assert_true(str_contains("hello world", "world"), "contains at end") + assert_true(str_contains("hello world", "hello"), "contains at start") + assert_true(str_contains("hello world", "lo wo"), "contains in middle") + assert_false(str_contains("hello world", "xyz"), "not contained") + assert_true(str_contains("hello", ""), "empty sub always contained") + assert_false(str_contains("", "a"), "empty string contains nothing") + assert_true(str_contains("hello", "hello"), "string contains itself") + return "" +} + +// ── str_index_of ───────────────────────────────────────────────────────────── + +fn test_str_index_of(_: String) -> String { + assert_int_eq(str_index_of("hello world", "world"), 6, "index of suffix") + assert_int_eq(str_index_of("hello world", "hello"), 0, "index of prefix") + assert_int_eq(str_index_of("hello world", "o"), 4, "index of first occurrence") + assert_int_eq(str_index_of("hello world", "xyz"), -1, "not found returns -1") + assert_int_eq(str_index_of("hello", ""), 0, "empty sub returns 0") + assert_int_eq(str_index_of("", "a"), -1, "search in empty string") + assert_int_eq(str_index_of("aababc", "ab"), 1, "finds first occurrence") + return "" +} + +// ── str_replace ─────────────────────────────────────────────────────────────── + +fn test_str_replace(_: String) -> String { + assert_eq(str_replace("hello world", "world", "there"), "hello there", "basic replace") + assert_eq(str_replace("aaa", "a", "b"), "bbb", "replace all occurrences") + assert_eq(str_replace("hello", "xyz", "abc"), "hello", "no match is identity") + assert_eq(str_replace("", "a", "b"), "", "empty string unchanged") + assert_eq(str_replace("hello", "", "x"), "hello", "empty from is identity") + assert_eq(str_replace("hello hello", "hello", "bye"), "bye bye", "replace multiple") + assert_eq(str_replace("aXbXc", "X", "-"), "a-b-c", "single-char delimiter") + return "" +} + +// ── str_slice ───────────────────────────────────────────────────────────────── + +fn test_str_slice(_: String) -> String { + assert_eq(str_slice("hello world", 0, 5), "hello", "slice from start") + assert_eq(str_slice("hello world", 6, 11), "world", "slice from middle") + assert_eq(str_slice("hello world", 0, 0), "", "zero-length slice") + assert_eq(str_slice("hello", 0, 100), "hello", "end beyond length clamped") + assert_eq(str_slice("hello", 3, 3), "", "start == end is empty") + assert_eq(str_slice("hello world", 2, 7), "llo w", "interior slice") + return "" +} + +// ── str_to_upper / str_to_lower ────────────────────────────────────────────── + +fn test_str_to_upper(_: String) -> String { + assert_eq(str_to_upper("hello"), "HELLO", "lowercase to uppercase") + assert_eq(str_to_upper("HELLO"), "HELLO", "already uppercase unchanged") + assert_eq(str_to_upper("Hello World"), "HELLO WORLD", "mixed case") + assert_eq(str_to_upper(""), "", "empty string unchanged") + assert_eq(str_to_upper("hello123"), "HELLO123", "digits unchanged") + return "" +} + +fn test_str_to_lower(_: String) -> String { + assert_eq(str_to_lower("HELLO"), "hello", "uppercase to lowercase") + assert_eq(str_to_lower("hello"), "hello", "already lowercase unchanged") + assert_eq(str_to_lower("Hello World"), "hello world", "mixed case") + assert_eq(str_to_lower(""), "", "empty string unchanged") + assert_eq(str_to_lower("HELLO123"), "hello123", "digits unchanged") + return "" +} + +// ── str_trim ───────────────────────────────────────────────────────────────── + +fn test_str_trim(_: String) -> String { + assert_eq(str_trim(" hello "), "hello", "trims spaces both sides") + assert_eq(str_trim("hello"), "hello", "no whitespace unchanged") + assert_eq(str_trim(" "), "", "all-space string becomes empty") + assert_eq(str_trim(""), "", "empty string unchanged") + assert_eq(str_trim("\t hello \n"), "hello", "trims tabs and newlines") + assert_eq(str_trim(" hello world "), "hello world", "internal spaces preserved") + return "" +} + +// ── str_split ───────────────────────────────────────────────────────────────── + +fn test_str_split(_: String) -> String { + let parts: [String] = str_split("a,b,c", ",") + assert_int_eq(el_list_len(parts), 3, "split yields 3 parts") + assert_eq(el_list_get(parts, 0), "a", "first part is a") + assert_eq(el_list_get(parts, 1), "b", "second part is b") + assert_eq(el_list_get(parts, 2), "c", "third part is c") + + let single: [String] = str_split("hello", ",") + assert_int_eq(el_list_len(single), 1, "no sep found yields 1 part") + assert_eq(el_list_get(single, 0), "hello", "single part is the full string") + + let trailing: [String] = str_split("a,b,", ",") + assert_int_eq(el_list_len(trailing), 3, "trailing sep yields empty last element") + assert_eq(el_list_get(trailing, 2), "", "last element is empty") + + let empty_str: [String] = str_split("", ",") + assert_int_eq(el_list_len(empty_str), 1, "splitting empty string yields one empty element") + + let multi: [String] = str_split("one::two::three", "::") + assert_int_eq(el_list_len(multi), 3, "multi-char separator works") + assert_eq(el_list_get(multi, 0), "one", "multi-sep first part") + assert_eq(el_list_get(multi, 2), "three", "multi-sep last part") + return "" +} + +// ── str_join ───────────────────────────────────────────────────────────────── + +fn test_str_join(_: String) -> String { + let parts: [String] = el_list_empty() + let parts = el_list_append(parts, "a") + let parts = el_list_append(parts, "b") + let parts = el_list_append(parts, "c") + assert_eq(str_join(parts, ","), "a,b,c", "basic join with comma") + assert_eq(str_join(parts, ""), "abc", "join with empty separator") + assert_eq(str_join(parts, " | "), "a | b | c", "join with multi-char sep") + + let empty_list: [String] = el_list_empty() + assert_eq(str_join(empty_list, ","), "", "joining empty list yields empty string") + + let one: [String] = el_list_empty() + let one = el_list_append(one, "solo") + assert_eq(str_join(one, ","), "solo", "joining single element yields that element") + return "" +} + +// ── int_to_str / str_to_int ────────────────────────────────────────────────── + +fn test_int_to_str(_: String) -> String { + assert_eq(int_to_str(0), "0", "zero") + assert_eq(int_to_str(42), "42", "positive integer") + assert_eq(int_to_str(-1), "-1", "negative integer") + assert_eq(int_to_str(1000000), "1000000", "large integer") + return "" +} + +fn test_str_to_int(_: String) -> String { + assert_int_eq(str_to_int("0"), 0, "zero") + assert_int_eq(str_to_int("42"), 42, "positive integer") + assert_int_eq(str_to_int("-1"), -1, "negative integer") + assert_int_eq(str_to_int("1000000"), 1000000, "large integer") + return "" +} + +// ── str_repeat ──────────────────────────────────────────────────────────────── + +fn test_str_repeat(_: String) -> String { + assert_eq(str_repeat("ab", 3), "ababab", "repeat 3 times") + assert_eq(str_repeat("x", 1), "x", "repeat once") + assert_eq(str_repeat("x", 0), "", "repeat zero times yields empty") + assert_eq(str_repeat("", 5), "", "repeating empty string yields empty") + assert_eq(str_repeat("-", 4), "----", "single char repeat") + return "" +} + +// ── str_reverse ─────────────────────────────────────────────────────────────── + +fn test_str_reverse(_: String) -> String { + assert_eq(str_reverse("hello"), "olleh", "basic reverse") + assert_eq(str_reverse("a"), "a", "single char is its own reverse") + assert_eq(str_reverse(""), "", "empty string reverses to empty") + assert_eq(str_reverse("abcd"), "dcba", "even-length reverse") + assert_eq(str_reverse("racecar"), "racecar", "palindrome is unchanged") + return "" +} + +// ── str_count ───────────────────────────────────────────────────────────────── + +fn test_str_count(_: String) -> String { + assert_int_eq(str_count("hello world hello", "hello"), 2, "two occurrences") + assert_int_eq(str_count("aaa", "a"), 3, "adjacent single chars") + assert_int_eq(str_count("aaa", "aa"), 1, "non-overlapping: one match") + assert_int_eq(str_count("hello", "xyz"), 0, "no match") + assert_int_eq(str_count("", "a"), 0, "empty string has no occurrences") + assert_int_eq(str_count("hello", ""), 0, "empty sub returns 0") + return "" +} + +// ── str_strip_prefix / str_strip_suffix ─────────────────────────────────────── + +fn test_str_strip_prefix(_: String) -> String { + assert_eq(str_strip_prefix("foobar", "foo"), "bar", "strips matching prefix") + assert_eq(str_strip_prefix("foobar", "baz"), "foobar", "no-match is identity") + assert_eq(str_strip_prefix("hello", ""), "hello", "empty prefix is identity") + assert_eq(str_strip_prefix("hello", "hello"), "", "full match yields empty") + return "" +} + +fn test_str_strip_suffix(_: String) -> String { + assert_eq(str_strip_suffix("hello.md", ".md"), "hello", "strips matching suffix") + assert_eq(str_strip_suffix("hello.md", ".txt"), "hello.md", "no-match is identity") + assert_eq(str_strip_suffix("hello", ""), "hello", "empty suffix is identity") + assert_eq(str_strip_suffix("hello", "hello"), "", "full match yields empty") + return "" +} + +// ── str_find_chars ──────────────────────────────────────────────────────────── + +fn test_str_find_chars(_: String) -> String { + assert_int_eq(str_find_chars("hello world", " "), 5, "finds space at index 5") + assert_int_eq(str_find_chars("hello", "xyz"), -1, "not found returns -1") + assert_int_eq(str_find_chars("hello", ""), -1, "empty charset returns -1") + assert_int_eq(str_find_chars("hello", "aeiou"), 1, "finds first vowel at index 1") + assert_int_eq(str_find_chars("", "a"), -1, "search in empty string returns -1") + return "" +} + +// ── str_last_index_of ───────────────────────────────────────────────────────── + +fn test_str_last_index_of(_: String) -> String { + assert_int_eq(str_last_index_of("hello hello", "hello"), 6, "last occurrence") + assert_int_eq(str_last_index_of("hello", "hello"), 0, "single occurrence") + assert_int_eq(str_last_index_of("hello", "xyz"), -1, "not found returns -1") + assert_int_eq(str_last_index_of("aababc", "ab"), 3, "last ab in aababc") + return "" +} + +// ── Entry point ─────────────────────────────────────────────────────────────── + +fn main() -> Int { + test_case("str_eq", "test_str_eq") + test_case("str_len", "test_str_len") + test_case("str_concat", "test_str_concat") + test_case("str_starts_with", "test_str_starts_with") + test_case("str_ends_with", "test_str_ends_with") + test_case("str_contains", "test_str_contains") + test_case("str_index_of", "test_str_index_of") + test_case("str_replace", "test_str_replace") + test_case("str_slice", "test_str_slice") + test_case("str_to_upper", "test_str_to_upper") + test_case("str_to_lower", "test_str_to_lower") + test_case("str_trim", "test_str_trim") + test_case("str_split", "test_str_split") + test_case("str_join", "test_str_join") + test_case("int_to_str", "test_int_to_str") + test_case("str_to_int", "test_str_to_int") + test_case("str_repeat", "test_str_repeat") + test_case("str_reverse", "test_str_reverse") + test_case("str_count", "test_str_count") + test_case("str_strip_prefix", "test_str_strip_prefix") + test_case("str_strip_suffix", "test_str_strip_suffix") + test_case("str_find_chars", "test_str_find_chars") + test_case("str_last_index_of", "test_str_last_index_of") + return test_run_all() +}