From 9013e241c3de90532103732ed5cd6130cdb69d86 Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Tue, 5 May 2026 00:09:57 -0500 Subject: [PATCH] feat: extract native El test suite from feat/native-testing - Add tests/native/test_{core,text,string,math,state,time,json,env,fs}.el - test_codegen_js.el renamed to test_core.el per dev convention - Add native test CI steps to ci-dev.yaml (compile-link-run pattern) - No lexer.el/parser.el/codegen.el changes taken from this branch --- .gitea/workflows/ci-dev.yaml | 91 ++++++++++++ tests/native/test_core.el | 80 +++++++++++ tests/native/test_env.el | 82 +++++++++++ tests/native/test_fs.el | 96 +++++++++++++ tests/native/test_json.el | 132 +++++++++++++++++ tests/native/test_math.el | 153 ++++++++++++++++++++ tests/native/test_state.el | 98 +++++++++++++ tests/native/test_string.el | 272 +++++++++++++++++++++++++++++++++++ tests/native/test_text.el | 84 +++++++++++ tests/native/test_time.el | 155 ++++++++++++++++++++ 10 files changed, 1243 insertions(+) create mode 100644 tests/native/test_core.el create mode 100644 tests/native/test_env.el create mode 100644 tests/native/test_fs.el create mode 100644 tests/native/test_json.el create mode 100644 tests/native/test_math.el create mode 100644 tests/native/test_state.el create mode 100644 tests/native/test_string.el create mode 100644 tests/native/test_text.el create mode 100644 tests/native/test_time.el diff --git a/.gitea/workflows/ci-dev.yaml b/.gitea/workflows/ci-dev.yaml index c4e57c0..d415c5f 100644 --- a/.gitea/workflows/ci-dev.yaml +++ b/.gitea/workflows/ci-dev.yaml @@ -73,6 +73,97 @@ jobs: EL_HOME="$(pwd)" \ bash tests/html_sanitizer/run.sh + # Native El test suites (elc --test, compile-link-run) + - name: Run tests — native (core) + run: | + set -euo pipefail + ELC="$(pwd)/dist/platform/elc" + RUNTIME="$(pwd)/el-compiler/runtime" + "$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c + gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c "$RUNTIME/el_runtime.c" \ + -lcurl -lpthread -lm -o /tmp/el_native_core + /tmp/el_native_core + + - name: Run tests — native (text) + run: | + set -euo pipefail + ELC="$(pwd)/dist/platform/elc" + RUNTIME="$(pwd)/el-compiler/runtime" + "$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c + gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c "$RUNTIME/el_runtime.c" \ + -lcurl -lpthread -lm -o /tmp/el_native_text + /tmp/el_native_text + + - name: Run tests — native (string) + run: | + set -euo pipefail + ELC="$(pwd)/dist/platform/elc" + RUNTIME="$(pwd)/el-compiler/runtime" + "$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c + gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c "$RUNTIME/el_runtime.c" \ + -lcurl -lpthread -lm -o /tmp/el_native_string + /tmp/el_native_string + + - name: Run tests — native (math) + run: | + set -euo pipefail + ELC="$(pwd)/dist/platform/elc" + RUNTIME="$(pwd)/el-compiler/runtime" + "$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c + gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c "$RUNTIME/el_runtime.c" \ + -lcurl -lpthread -lm -o /tmp/el_native_math + /tmp/el_native_math + + - name: Run tests — native (state) + run: | + set -euo pipefail + ELC="$(pwd)/dist/platform/elc" + RUNTIME="$(pwd)/el-compiler/runtime" + "$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c + gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c "$RUNTIME/el_runtime.c" \ + -lcurl -lpthread -lm -o /tmp/el_native_state + /tmp/el_native_state + + - name: Run tests — native (time) + run: | + set -euo pipefail + ELC="$(pwd)/dist/platform/elc" + RUNTIME="$(pwd)/el-compiler/runtime" + "$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c + gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c "$RUNTIME/el_runtime.c" \ + -lcurl -lpthread -lm -o /tmp/el_native_time + /tmp/el_native_time + + - name: Run tests — native (json) + run: | + set -euo pipefail + ELC="$(pwd)/dist/platform/elc" + RUNTIME="$(pwd)/el-compiler/runtime" + "$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c + gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c "$RUNTIME/el_runtime.c" \ + -lcurl -lpthread -lm -o /tmp/el_native_json + /tmp/el_native_json + + - name: Run tests — native (env) + run: | + set -euo pipefail + ELC="$(pwd)/dist/platform/elc" + RUNTIME="$(pwd)/el-compiler/runtime" + "$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c + gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c "$RUNTIME/el_runtime.c" \ + -lcurl -lpthread -lm -o /tmp/el_native_env + /tmp/el_native_env + + - name: Run tests — native (fs) + run: | + set -euo pipefail + ELC="$(pwd)/dist/platform/elc" + RUNTIME="$(pwd)/el-compiler/runtime" + "$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c + gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c "$RUNTIME/el_runtime.c" \ + -lcurl -lpthread -lm -o /tmp/el_native_fs + /tmp/el_native_fs + - name: Publish El SDK to Artifact Registry (dev) env: GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }} diff --git a/tests/native/test_core.el b/tests/native/test_core.el new file mode 100644 index 0000000..f4a32c7 --- /dev/null +++ b/tests/native/test_core.el @@ -0,0 +1,80 @@ +// test_codegen_js.el - basic tests for JS codegen features. +// +// These tests verify that core El language features produce correct values +// when compiled and executed via the C backend. They serve as a +// regression baseline for the codegen pipeline. + +test "arithmetic" { + let x: Int = 2 + 3 + assert x == 5, "addition" + let y: Int = 10 - 4 + assert y == 6, "subtraction" + let z: Int = 3 * 4 + assert z == 12, "multiplication" + let w: Int = 15 / 3 + assert w == 5, "division" +} + +test "string-concat" { + let a: String = "hello" + let b: String = " world" + let c: String = a + b + assert c == "hello world", "string concatenation" +} + +test "str-len" { + let n: Int = str_len("hello") + assert n == 5, "str_len hello" + let m: Int = str_len("") + assert m == 0, "str_len empty" +} + +test "bool-logic" { + let t = true + let f = false + assert t, "true is truthy" + assert !f, "false negated is truthy" + assert t && !f, "true and not false" + assert t || f, "true or false" +} + +test "list-operations" { + let lst: [Int] = native_list_empty() + let lst = native_list_append(lst, 10) + let lst = native_list_append(lst, 20) + let lst = native_list_append(lst, 30) + let n: Int = native_list_len(lst) + assert n == 3, "list length 3" + let v0: Int = native_list_get(lst, 0) + let v1: Int = native_list_get(lst, 1) + let v2: Int = native_list_get(lst, 2) + assert v0 == 10, "first element" + assert v1 == 20, "second element" + assert v2 == 30, "third element" +} + +test "str-slice" { + let s: String = str_slice("hello world", 6, 11) + assert s == "world", "slice from 6 to 11" +} + +test "str-contains" { + assert str_contains("hello world", "world"), "contains world" + assert !str_contains("hello world", "xyz"), "does not contain xyz" +} + +test "int-to-str" { + let s: String = int_to_str(42) + assert s == "42", "int to string" +} + +test "str-to-int" { + let n: Int = str_to_int("123") + assert n == 123, "string to int" +} + +test "str-starts-ends" { + assert str_starts_with("hello world", "hello"), "starts with hello" + assert str_ends_with("hello world", "world"), "ends with world" + assert !str_starts_with("hello world", "world"), "does not start with world" +} diff --git a/tests/native/test_env.el b/tests/native/test_env.el new file mode 100644 index 0000000..4b33e08 --- /dev/null +++ b/tests/native/test_env.el @@ -0,0 +1,82 @@ +// test_env.el - native test suite for runtime/env.el +// +// Covers: env() for reading environment variables, args() returning a list, +// state_set/get/del/keys via the env module re-exports, uuid_new/uuid_v4 +// format validation. + +test "env-missing-returns-empty" { + let v: String = env("__EL_NO_SUCH_VAR_XYZ__") + assert v == "", "missing env var returns empty string" +} + +test "env-path-is-set" { + // PATH is expected to be set in virtually any UNIX environment. + let v: String = env("PATH") + assert str_len(v) > 0, "PATH env var is non-empty" +} + +test "env-home-is-set" { + // HOME is present on macOS/Linux test environments. + let v: String = env("HOME") + assert str_len(v) > 0, "HOME env var is non-empty" +} + +test "args-returns-list" { + let a: [String] = args() + // Even with no arguments, the list should be non-null (at minimum the + // program name is argv[0]). + let n: Int = native_list_len(a) + assert n >= 0, "args returns a list (may be empty if runtime strips argv)" +} + +test "uuid-new-format" { + let id: String = uuid_new() + // UUID v4: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx (36 chars) + let n: Int = str_len(id) + assert n == 36, "uuid_new returns 36-character string" + // Check the dashes at correct positions + let d1: String = str_char_at(id, 8) + let d2: String = str_char_at(id, 13) + let d3: String = str_char_at(id, 18) + let d4: String = str_char_at(id, 23) + assert d1 == "-", "dash at position 8" + assert d2 == "-", "dash at position 13" + assert d3 == "-", "dash at position 18" + assert d4 == "-", "dash at position 23" +} + +test "uuid-v4-format" { + let id: String = uuid_v4() + let n: Int = str_len(id) + assert n == 36, "uuid_v4 returns 36-character string" + // The version nibble must be '4' + let version_char: String = str_char_at(id, 14) + assert version_char == "4", "uuid_v4 version nibble is '4'" +} + +test "uuid-uniqueness" { + let id1: String = uuid_new() + let id2: String = uuid_new() + assert !str_eq(id1, id2), "two uuid_new calls produce different UUIDs" +} + +test "env-state-set-get-via-env-module" { + // runtime/env.el re-exports state_set / state_get. + state_set("env_test_key", "env_test_val") + let v: String = state_get("env_test_key") + assert v == "env_test_val", "state_set/get work via env module" + state_del("env_test_key") + let after: String = state_get("env_test_key") + assert after == "", "state_del removes key" +} + +test "env-state-keys-json-via-env-module" { + state_set("esk_a", "1") + state_set("esk_b", "2") + let ks: String = state_keys() + assert str_starts_with(ks, "["), "state_keys returns JSON array" + assert str_contains(ks, "esk_a"), "esk_a in keys" + assert str_contains(ks, "esk_b"), "esk_b in keys" + state_del("esk_a") + state_del("esk_b") +} diff --git a/tests/native/test_fs.el b/tests/native/test_fs.el new file mode 100644 index 0000000..045610f --- /dev/null +++ b/tests/native/test_fs.el @@ -0,0 +1,96 @@ +// test_fs.el - native test suite for runtime/fs.el +// +// Covers: fs_write/read round-trip, fs_exists, fs_mkdir, fs_list, +// fs_list_json, and edge cases (empty file, overwrite, non-existent path). + +test "fs-write-and-read" { + let path: String = "/tmp/el_test_fs_basic.txt" + let content: String = "hello from El" + let ok: Bool = fs_write(path, content) + assert ok, "fs_write returns true on success" + let got: String = fs_read(path) + assert got == content, "fs_read returns what was written" +} + +test "fs-exists" { + let path: String = "/tmp/el_test_fs_exists.txt" + fs_write(path, "exists check") + let exists: Bool = fs_exists(path) + assert exists, "fs_exists returns true for created file" + let missing: Bool = fs_exists("/tmp/__el_no_such_file_xyz__.txt") + assert !missing, "fs_exists returns false for non-existent file" +} + +test "fs-read-nonexistent" { + let got: String = fs_read("/tmp/__el_no_such_file_abc__.txt") + assert got == "", "reading nonexistent file returns empty string" +} + +test "fs-write-overwrite" { + let path: String = "/tmp/el_test_fs_overwrite.txt" + fs_write(path, "original content") + fs_write(path, "new content") + let got: String = fs_read(path) + assert got == "new content", "second write overwrites first" +} + +test "fs-write-empty-file" { + let path: String = "/tmp/el_test_fs_empty.txt" + let ok: Bool = fs_write(path, "") + assert ok, "writing empty file succeeds" + let got: String = fs_read(path) + assert got == "", "reading empty file returns empty string" + let exists: Bool = fs_exists(path) + assert exists, "empty file still exists" +} + +test "fs-write-multiline" { + let path: String = "/tmp/el_test_fs_multiline.txt" + let content: String = "line one\nline two\nline three" + fs_write(path, content) + let got: String = fs_read(path) + assert got == content, "multiline content round-trips" + let lines: [String] = str_split_lines(got) + let n: Int = native_list_len(lines) + assert n == 3, "three lines after read" +} + +test "fs-mkdir" { + let dir: String = "/tmp/el_test_fs_mkdir_dir" + let ok: Bool = fs_mkdir(dir) + assert ok, "fs_mkdir returns true" + let exists: Bool = fs_exists(dir) + assert exists, "created directory exists" +} + +test "fs-list" { + let dir: String = "/tmp/el_test_fs_list_dir" + fs_mkdir(dir) + fs_write(dir + "/a.txt", "a") + fs_write(dir + "/b.txt", "b") + let files: [String] = fs_list(dir) + // Filter out empty strings from trailing newline + let n: Int = native_list_len(files) + // We expect at least 2 entries (a.txt and b.txt) + assert n >= 2, "fs_list returns at least 2 entries" +} + +test "fs-list-json" { + let dir: String = "/tmp/el_test_fs_listjson_dir" + fs_mkdir(dir) + fs_write(dir + "/x.txt", "x") + fs_write(dir + "/y.txt", "y") + let result: String = fs_list_json(dir) + assert str_starts_with(result, "["), "fs_list_json returns JSON array" + assert str_ends_with(result, "]"), "fs_list_json JSON array is closed" + // Should contain at least one filename + assert str_len(result) > 2, "fs_list_json result is non-empty array" +} + +test "fs-write-and-read-special-chars" { + let path: String = "/tmp/el_test_fs_special.txt" + let content: String = "tabs:\there\nnewlines: ok\n\"quoted\"" + fs_write(path, content) + let got: String = fs_read(path) + assert got == content, "special chars in content round-trip" +} diff --git a/tests/native/test_json.el b/tests/native/test_json.el new file mode 100644 index 0000000..30da7e3 --- /dev/null +++ b/tests/native/test_json.el @@ -0,0 +1,132 @@ +// test_json.el - native test suite for runtime/json.el +// +// Covers: json_get (dot-path), typed extractors (int, bool, float), +// json_array_len/get, json_set, json_build_object, json_build_array, +// and json_escape_string. + +test "json-get-string" { + let obj: String = "{\"name\":\"alice\",\"role\":\"admin\"}" + let name: String = json_get(obj, "name") + assert name == "alice", "get string field name" + let role: String = json_get(obj, "role") + assert role == "admin", "get string field role" + let missing: String = json_get(obj, "xyz") + assert missing == "", "missing key returns empty string" +} + +test "json-get-int" { + let obj: String = "{\"count\":42,\"offset\":0}" + let count: Int = json_get_int(obj, "count") + assert count == 42, "get int field count" + let offset: Int = json_get_int(obj, "offset") + assert offset == 0, "get int field offset = 0" +} + +test "json-get-bool" { + let obj: String = "{\"active\":true,\"deleted\":false}" + let active: Bool = json_get_bool(obj, "active") + assert active, "get bool true" + let deleted: Bool = json_get_bool(obj, "deleted") + assert !deleted, "get bool false" +} + +test "json-dot-path" { + let obj: String = "{\"user\":{\"name\":\"bob\",\"age\":30}}" + let name: String = json_get(obj, "user.name") + assert name == "bob", "dot-path traversal user.name" + let age: String = json_get(obj, "user.age") + assert age == "30", "dot-path traversal user.age" +} + +test "json-array-len" { + let arr: String = "[\"a\",\"b\",\"c\"]" + let n: Int = json_array_len(arr) + assert n == 3, "array length 3" + let empty_arr: String = "[]" + let m: Int = json_array_len(empty_arr) + assert m == 0, "empty array length 0" +} + +test "json-array-get" { + let arr: String = "[\"foo\",\"bar\",\"baz\"]" + let first: String = json_array_get_string(arr, 0) + assert first == "foo", "first element" + let second: String = json_array_get_string(arr, 1) + assert second == "bar", "second element" + let third: String = json_array_get_string(arr, 2) + assert third == "baz", "third element" +} + +test "json-get-raw" { + let obj: String = "{\"items\":[1,2,3],\"meta\":{\"page\":1}}" + let raw_arr: String = json_get_raw(obj, "items") + let arr_len: Int = json_array_len(raw_arr) + assert arr_len == 3, "raw array has 3 elements" + let raw_meta: String = json_get_raw(obj, "meta") + let page: String = json_get(raw_meta, "page") + assert page == "1", "page from nested raw object" +} + +test "json-set" { + let obj: String = "{\"name\":\"alice\"}" + let updated: String = json_set(obj, "name", "\"bob\"") + let name: String = json_get(updated, "name") + assert name == "bob", "set replaces existing key" +} + +test "json-build-object" { + let kvs: [String] = native_list_empty() + let kvs = native_list_append(kvs, "name") + let kvs = native_list_append(kvs, "alice") + let kvs = native_list_append(kvs, "role") + let kvs = native_list_append(kvs, "admin") + let obj: String = json_build_object(kvs) + let name: String = json_get(obj, "name") + let role: String = json_get(obj, "role") + assert name == "alice", "built object name field" + assert role == "admin", "built object role field" +} + +test "json-build-array" { + let items: [String] = native_list_empty() + let items = native_list_append(items, "\"alpha\"") + let items = native_list_append(items, "\"beta\"") + let items = native_list_append(items, "\"gamma\"") + let arr: String = json_build_array(items) + let n: Int = json_array_len(arr) + assert n == 3, "built array has 3 elements" + let first: String = json_array_get_string(arr, 0) + assert first == "alpha", "first built element" + let third: String = json_array_get_string(arr, 2) + assert third == "gamma", "third built element" +} + +test "json-escape-string" { + let escaped: String = json_escape_string("say \"hello\"") + assert str_contains(escaped, "\\\""), "double quotes are escaped" + let tab_esc: String = json_escape_string("a\tb") + assert str_contains(tab_esc, "\\t"), "tabs are escaped" + let plain: String = json_escape_string("no special chars") + assert plain == "no special chars", "plain string unchanged" +} + +test "json-get-float" { + let obj: String = "{\"price\":9.99,\"tax\":0.0}" + let price: Float = json_get_float(obj, "price") + let diff: Float = price - 9.99 + assert diff > -0.001, "price close to 9.99 low" + assert diff < 0.001, "price close to 9.99 high" + let tax: Float = json_get_float(obj, "tax") + assert tax == 0.0, "tax is zero" +} + +test "json-nested-array-index" { + let obj: String = "{\"tags\":[\"go\",\"el\",\"rust\"]}" + let tags_raw: String = json_get_raw(obj, "tags") + let count: Int = json_array_len(tags_raw) + assert count == 3, "tags array has 3 elements" + let first_tag: String = json_array_get_string(tags_raw, 0) + assert first_tag == "go", "first tag is go" + let last_tag: String = json_array_get_string(tags_raw, 2) + assert last_tag == "rust", "last tag is rust" +} diff --git a/tests/native/test_math.el b/tests/native/test_math.el new file mode 100644 index 0000000..4b3e22f --- /dev/null +++ b/tests/native/test_math.el @@ -0,0 +1,153 @@ +// test_math.el - native test suite for runtime/math.el +// +// Covers: integer math (abs, max, min), float math (sqrt, log, sin, cos, pi), +// float conversions, format_float, and decimal_round. + +test "el-abs" { + let pos: Int = el_abs(5) + assert pos == 5, "abs of positive" + let neg: Int = el_abs(-5) + assert neg == 5, "abs of negative" + let zero: Int = el_abs(0) + assert zero == 0, "abs of zero" + let large: Int = el_abs(-1000000) + assert large == 1000000, "abs of large negative" +} + +test "el-max" { + let m: Int = el_max(3, 7) + assert m == 7, "max of 3 and 7" + let m2: Int = el_max(7, 3) + assert m2 == 7, "max is commutative" + let same: Int = el_max(5, 5) + assert same == 5, "max of equal values" + let neg: Int = el_max(-3, -7) + assert neg == -3, "max of two negatives" +} + +test "el-min" { + let m: Int = el_min(3, 7) + assert m == 3, "min of 3 and 7" + let m2: Int = el_min(7, 3) + assert m2 == 3, "min is commutative" + let same: Int = el_min(5, 5) + assert same == 5, "min of equal values" + let neg: Int = el_min(-3, -7) + assert neg == -7, "min of two negatives" +} + +test "math-pi" { + let pi: Float = math_pi() + // pi ~ 3.14159265358979 + // Check it's between 3.141 and 3.142 + let too_low: Float = pi - 3.141 + let too_high: Float = 3.142 - pi + assert too_low > 0.0, "pi > 3.141" + assert too_high > 0.0, "pi < 3.142" +} + +test "math-sqrt" { + let r4: Float = math_sqrt(4.0) + let diff4: Float = r4 - 2.0 + assert diff4 == 0.0, "sqrt(4) == 2.0" + let r9: Float = math_sqrt(9.0) + let diff9: Float = r9 - 3.0 + assert diff9 == 0.0, "sqrt(9) == 3.0" + let r1: Float = math_sqrt(1.0) + let diff1: Float = r1 - 1.0 + assert diff1 == 0.0, "sqrt(1) == 1.0" + let r0: Float = math_sqrt(0.0) + assert r0 == 0.0, "sqrt(0) == 0.0" +} + +test "math-sin-cos" { + // sin(0) == 0, cos(0) == 1 + let s0: Float = math_sin(0.0) + assert s0 == 0.0, "sin(0) == 0.0" + let c0: Float = math_cos(0.0) + assert c0 == 1.0, "cos(0) == 1.0" + // sin(pi/2) ~ 1.0, cos(pi/2) ~ 0.0 + let half_pi: Float = math_pi() / 2.0 + let s_half: Float = math_sin(half_pi) + // Check within 0.000001 of 1.0 + let diff_s: Float = s_half - 1.0 + assert diff_s > -0.000001, "sin(pi/2) close to 1.0 low" + assert diff_s < 0.000001, "sin(pi/2) close to 1.0 high" +} + +test "int-to-float-and-back" { + let f: Float = int_to_float(42) + let back: Int = float_to_int(f) + assert back == 42, "42 round-trips int->float->int" + let neg: Float = int_to_float(-7) + let neg_back: Int = float_to_int(neg) + assert neg_back == -7, "-7 round-trips" + let zero: Float = int_to_float(0) + let zero_back: Int = float_to_int(zero) + assert zero_back == 0, "0 round-trips" +} + +test "float-to-int-truncates" { + let t1: Int = float_to_int(3.9) + assert t1 == 3, "3.9 truncates to 3" + let t2: Int = float_to_int(3.1) + assert t2 == 3, "3.1 truncates to 3" + let t3: Int = float_to_int(-3.7) + assert t3 == -3, "-3.7 truncates toward zero to -3" +} + +test "str-to-float-and-back" { + let f: Float = str_to_float("3.14") + // Check it's between 3.13 and 3.15 + let lo: Float = f - 3.13 + let hi: Float = 3.15 - f + assert lo > 0.0, "3.14 parsed > 3.13" + assert hi > 0.0, "3.14 parsed < 3.15" + let zero: Float = str_to_float("0.0") + assert zero == 0.0, "parse 0.0" +} + +test "format-float" { + let s0: String = format_float(3.14159, 2) + assert s0 == "3.14", "format to 2 decimals" + let s1: String = format_float(1.0, 0) + assert s1 == "1", "format to 0 decimals" + let s2: String = format_float(0.0, 3) + assert s2 == "0.000", "format zero to 3 decimals" + let s3: String = format_float(-2.5, 1) + assert s3 == "-2.5", "format negative to 1 decimal" +} + +test "decimal-round" { + let r0: Float = decimal_round(2.5, 0) + assert r0 == 3.0, "round 2.5 to 0 places" + let r1: Float = decimal_round(2.45, 1) + assert r1 == 2.5, "round 2.45 to 1 place" + let neg: Float = decimal_round(-2.5, 0) + assert neg == -3.0, "round -2.5 to 0 places (half-away-from-zero)" + let exact: Float = decimal_round(1.0, 2) + assert exact == 1.0, "rounding exact value unchanged" +} + +test "math-log" { + // log10(100) == 2 + let l100: Float = math_log(100.0) + let diff: Float = l100 - 2.0 + assert diff > -0.000001, "log10(100) close to 2 low" + assert diff < 0.000001, "log10(100) close to 2 high" + // log10(1) == 0 + let l1: Float = math_log(1.0) + assert l1 == 0.0, "log10(1) == 0" +} + +test "math-ln" { + // ln(1) == 0 + let l1: Float = math_ln(1.0) + assert l1 == 0.0, "ln(1) == 0" + // ln(e) ~ 1.0 — e ~ 2.71828 + let e: Float = 2.718281828 + let le: Float = math_ln(e) + let diff: Float = le - 1.0 + assert diff > -0.000001, "ln(e) close to 1 low" + assert diff < 0.000001, "ln(e) close to 1 high" +} diff --git a/tests/native/test_state.el b/tests/native/test_state.el new file mode 100644 index 0000000..39f8438 --- /dev/null +++ b/tests/native/test_state.el @@ -0,0 +1,98 @@ +// test_state.el - native test suite for runtime/state.el +// +// Covers: state_set/get/del, state_has, state_get_or, state_keys, +// and edge cases such as empty values, overwrite, and multiple keys. + +test "state-set-get-basic" { + state_set("test_key", "hello") + let v: String = state_get("test_key") + assert v == "hello", "get returns set value" +} + +test "state-get-missing" { + let v: String = state_get("__nonexistent_key_xyz__") + assert v == "", "missing key returns empty string" +} + +test "state-overwrite" { + state_set("ow_key", "first") + state_set("ow_key", "second") + let v: String = state_get("ow_key") + assert v == "second", "second write overwrites first" +} + +test "state-del" { + state_set("del_key", "to be deleted") + state_del("del_key") + let v: String = state_get("del_key") + assert v == "", "deleted key returns empty string" +} + +test "state-del-nonexistent" { + // Should not panic or error on deleting a non-existent key. + state_del("__never_set_key__") + let v: String = state_get("__never_set_key__") + assert v == "", "del of nonexistent key is a no-op" +} + +test "state-has" { + state_set("has_key", "value") + assert state_has("has_key"), "has returns true for set key" + assert !state_has("__no_has_key__"), "has returns false for absent key" + state_del("has_key") + assert !state_has("has_key"), "has returns false after del" +} + +test "state-get-or" { + state_set("gor_key", "actual") + let v1: String = state_get_or("gor_key", "default") + assert v1 == "actual", "get_or returns value when key set" + let v2: String = state_get_or("__absent_gor_key__", "fallback") + assert v2 == "fallback", "get_or returns default when key absent" +} + +test "state-multiple-keys" { + state_set("mk_a", "alpha") + state_set("mk_b", "beta") + state_set("mk_c", "gamma") + let a: String = state_get("mk_a") + let b: String = state_get("mk_b") + let c: String = state_get("mk_c") + assert a == "alpha", "key a correct" + assert b == "beta", "key b correct" + assert c == "gamma", "key c correct" + state_del("mk_a") + state_del("mk_b") + state_del("mk_c") +} + +test "state-keys-returns-json-array" { + state_set("keys_test_1", "v1") + state_set("keys_test_2", "v2") + let ks: String = state_keys() + // The result is a JSON array string like ["keys_test_1","keys_test_2",...] + assert str_starts_with(ks, "["), "state_keys returns JSON array" + assert str_ends_with(ks, "]"), "state_keys JSON array is closed" + assert str_contains(ks, "keys_test_1"), "keys array contains keys_test_1" + assert str_contains(ks, "keys_test_2"), "keys array contains keys_test_2" + state_del("keys_test_1") + state_del("keys_test_2") +} + +test "state-numeric-value-as-string" { + state_set("num_key", "42") + let v: String = state_get("num_key") + let n: Int = str_to_int(v) + assert n == 42, "stored numeric string round-trips to int" + state_del("num_key") +} + +test "state-long-value" { + let long_val: String = str_repeat("abcdefghij", 100) + state_set("long_val_key", long_val) + let got: String = state_get("long_val_key") + assert got == long_val, "long value round-trips correctly" + let got_len: Int = str_len(got) + assert got_len == 1000, "long value is 1000 bytes" + state_del("long_val_key") +} diff --git a/tests/native/test_string.el b/tests/native/test_string.el new file mode 100644 index 0000000..e9f1192 --- /dev/null +++ b/tests/native/test_string.el @@ -0,0 +1,272 @@ +// test_string.el - native test suite for runtime/string.el +// +// Covers: type conversions, core primitives, comparison and search, +// case conversion, whitespace trimming, replacement, repetition, +// reversal, prefix/suffix stripping, padding, counting, character +// classification, splitting, and joining. + +test "int-to-str" { + let s: String = int_to_str(42) + assert s == "42", "int 42 to string" + let neg: String = int_to_str(-7) + assert neg == "-7", "negative int to string" + let zero: String = int_to_str(0) + assert zero == "0", "zero to string" +} + +test "str-to-int" { + let n: Int = str_to_int("123") + assert n == 123, "parse 123" + let neg: Int = str_to_int("-5") + assert neg == -5, "parse negative" + let zero: Int = str_to_int("0") + assert zero == 0, "parse zero" +} + +test "bool-to-str" { + let t: String = bool_to_str(true) + assert t == "true", "true to string" + let f: String = bool_to_str(false) + assert f == "false", "false to string" +} + +test "str-len" { + let n: Int = str_len("hello") + assert n == 5, "length of hello" + let e: Int = str_len("") + assert e == 0, "length of empty string" + let space: Int = str_len("a b") + assert space == 3, "length with spaces" +} + +test "str-eq" { + assert str_eq("abc", "abc"), "identical strings are equal" + assert !str_eq("abc", "ABC"), "case-sensitive comparison" + assert str_eq("", ""), "empty strings are equal" + assert !str_eq("a", "b"), "different single chars" +} + +test "str-slice" { + let s: String = str_slice("hello world", 6, 11) + assert s == "world", "slice end of string" + let start: String = str_slice("hello world", 0, 5) + assert start == "hello", "slice start of string" + let empty: String = str_slice("hello", 2, 2) + assert empty == "", "zero-length slice" +} + +test "str-starts-ends-with" { + assert str_starts_with("hello world", "hello"), "starts with hello" + assert str_ends_with("hello world", "world"), "ends with world" + assert !str_starts_with("hello world", "world"), "does not start with world" + assert !str_ends_with("hello world", "hello"), "does not end with hello" + assert str_starts_with("abc", ""), "empty prefix always matches" + assert str_ends_with("abc", ""), "empty suffix always matches" +} + +test "str-contains" { + assert str_contains("hello world", "world"), "contains world" + assert str_contains("hello world", "lo wo"), "contains interior substring" + assert !str_contains("hello world", "xyz"), "does not contain xyz" + assert str_contains("abc", ""), "empty sub is always contained" + assert !str_contains("", "x"), "empty string does not contain nonempty sub" +} + +test "str-index-of" { + let i: Int = str_index_of("hello world", "world") + assert i == 6, "index of world" + let j: Int = str_index_of("hello world", "xyz") + assert j == -1, "not found returns -1" + let k: Int = str_index_of("aabbcc", "bb") + assert k == 2, "index of bb in aabbcc" +} + +test "str-last-index-of" { + let i: Int = str_last_index_of("abcabc", "bc") + assert i == 4, "last occurrence of bc" + let j: Int = str_last_index_of("hello", "xyz") + assert j == -1, "not found returns -1" + let k: Int = str_last_index_of("aaa", "a") + assert k == 2, "last single-char match" +} + +test "str-to-upper-lower" { + let up: String = str_to_upper("hello") + assert up == "HELLO", "to upper" + let lo: String = str_to_lower("WORLD") + assert lo == "world", "to lower" + let mixed: String = str_to_upper("Hello World") + assert mixed == "HELLO WORLD", "mixed to upper" + let empty: String = str_to_lower("") + assert empty == "", "empty stays empty" +} + +test "str-trim" { + let s: String = str_trim(" hello ") + assert s == "hello", "trim both ends" + let lonly: String = str_trim(" hello") + assert lonly == "hello", "trim left only" + let ronly: String = str_trim("hello ") + assert ronly == "hello", "trim right only" + let tabs: String = str_trim("\thello\n") + assert tabs == "hello", "trim tabs and newlines" + let empty: String = str_trim(" ") + assert empty == "", "all whitespace trims to empty" +} + +test "str-replace" { + let s: String = str_replace("hello world", "world", "there") + assert s == "hello there", "replace word" + let none: String = str_replace("hello", "xyz", "abc") + assert none == "hello", "no match leaves string unchanged" + let multi: String = str_replace("aaa", "a", "b") + assert multi == "bbb", "replace all occurrences" + let empty_from: String = str_replace("hello", "", "x") + assert empty_from == "hello", "empty from returns original" +} + +test "str-repeat" { + let s: String = str_repeat("ab", 3) + assert s == "ababab", "repeat 3 times" + let once: String = str_repeat("x", 1) + assert once == "x", "repeat once" + let zero: String = str_repeat("abc", 0) + assert zero == "", "repeat zero times" + let neg: String = str_repeat("abc", -1) + assert neg == "", "negative repeat is empty" +} + +test "str-reverse" { + let s: String = str_reverse("hello") + assert s == "olleh", "reverse hello" + let single: String = str_reverse("a") + assert single == "a", "reverse single char" + let empty: String = str_reverse("") + assert empty == "", "reverse empty string" + let palindrome: String = str_reverse("racecar") + assert palindrome == "racecar", "reverse palindrome" +} + +test "str-strip-prefix-suffix" { + let p: String = str_strip_prefix("foobar", "foo") + assert p == "bar", "strip prefix foo" + let no_prefix: String = str_strip_prefix("foobar", "baz") + assert no_prefix == "foobar", "no match leaves string unchanged" + let s: String = str_strip_suffix("hello.md", ".md") + assert s == "hello", "strip suffix .md" + let no_suffix: String = str_strip_suffix("hello.md", ".txt") + assert no_suffix == "hello.md", "non-matching suffix unchanged" +} + +test "str-strip-chars" { + let s: String = str_strip_chars(" \thello \n", " \t\n") + assert s == "hello", "strip whitespace chars" + let dots: String = str_strip_chars("...hello...", ".") + assert dots == "hello", "strip dot chars" + let all: String = str_strip_chars("aaa", "a") + assert all == "", "strip all chars leaves empty" +} + +test "str-pad-left-right" { + let l: String = str_pad_left("42", 5, "0") + assert l == "00042", "zero-pad left to width 5" + let r: String = str_pad_right("hi", 5, "-") + assert r == "hi---", "dash-pad right to width 5" + let no_pad: String = str_pad_left("hello", 3, "x") + assert no_pad == "hello", "no pad when string already wide enough" +} + +test "str-count" { + let n: Int = str_count("abc abc abc", "abc") + assert n == 3, "count three occurrences" + let overlap: Int = str_count("aaa", "aa") + assert overlap == 1, "non-overlapping count" + let zero: Int = str_count("hello", "xyz") + assert zero == 0, "not found gives 0" + let empty_sub: Int = str_count("hello", "") + assert empty_sub == 0, "empty sub gives 0" +} + +test "str-count-lines-words-letters" { + let s: String = "hello world\nfoo bar" + let lines: Int = str_count_lines(s) + let words: Int = str_count_words(s) + let letters: Int = str_count_letters(s) + assert lines == 2, "line count" + assert words == 4, "word count" + assert letters == 16, "letter count" +} + +test "str-count-chars-and-digits" { + let digits: Int = str_count_digits("abc123def456") + assert digits == 6, "six digits" + let none: Int = str_count_digits("hello") + assert none == 0, "no digits" + let chars: Int = str_count_chars("hello") + assert chars == 5, "five ASCII chars" +} + +test "char-classes" { + assert is_letter("A"), "A is a letter" + assert is_digit("7"), "7 is a digit" + assert is_whitespace(" "), "space is whitespace" + assert !is_letter("3"), "3 is not a letter" + assert !is_digit("X"), "X is not a digit" + assert is_alphanumeric("abc123"), "abc123 is alphanumeric" + assert !is_alphanumeric("abc!"), "abc! is not alphanumeric" + assert is_uppercase("ABC"), "ABC is uppercase" + assert is_lowercase("abc"), "abc is lowercase" + assert !is_uppercase("Abc"), "mixed is not uppercase" +} + +test "str-split" { + let parts: [String] = str_split("a,b,c", ",") + let n: Int = native_list_len(parts) + assert n == 3, "split into 3 parts" + let p0: String = native_list_get(parts, 0) + let p1: String = native_list_get(parts, 1) + let p2: String = native_list_get(parts, 2) + assert p0 == "a", "first part" + assert p1 == "b", "second part" + assert p2 == "c", "third part" +} + +test "str-split-lines" { + let lines: [String] = str_split_lines("alpha\nbeta\r\ngamma\n") + let n: Int = native_list_len(lines) + assert n == 3, "split into 3 lines" + let l0: String = native_list_get(lines, 0) + let l1: String = native_list_get(lines, 1) + let l2: String = native_list_get(lines, 2) + assert l0 == "alpha", "first line" + assert l1 == "beta", "second line strips CR" + assert l2 == "gamma", "third line" +} + +test "str-join" { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, "alpha") + let parts = native_list_append(parts, "beta") + let parts = native_list_append(parts, "gamma") + let result: String = str_join(parts, ", ") + assert result == "alpha, beta, gamma", "join with separator" + let empty_parts: [String] = native_list_empty() + let empty_result: String = str_join(empty_parts, ",") + assert empty_result == "", "join empty list gives empty string" +} + +test "str-char-at" { + let c: String = str_char_at("hello", 1) + assert c == "e", "char at index 1" + let first: String = str_char_at("abc", 0) + assert first == "a", "first char" + let oob: String = str_char_at("abc", 10) + assert oob == "", "out of bounds returns empty" +} + +test "url-encode-decode" { + let encoded: String = url_encode("hello world") + assert !str_contains(encoded, " "), "space is encoded" + let decoded: String = url_decode(encoded) + assert decoded == "hello world", "round-trip decode" +} diff --git a/tests/native/test_text.el b/tests/native/test_text.el new file mode 100644 index 0000000..869505d --- /dev/null +++ b/tests/native/test_text.el @@ -0,0 +1,84 @@ +// test_text.el - native test suite for text primitives. +// +// Mirrors the acceptance corpus in tests/text/examples/ using the +// native test/assert system instead of run.sh + expected output files. + +test "count-substring" { + let x: Int = str_count("abc abc abc", "abc") + assert x == 3 +} + +test "count-overlap-skip" { + let x: Int = str_count("aaa", "aa") + assert x == 1, "non-overlapping count of aa in aaa" +} + +test "count-lines-words-letters" { + let s: String = "hello world\nfoo bar" + let lines: Int = str_count_lines(s) + let words: Int = str_count_words(s) + let letters: Int = str_count_letters(s) + assert lines == 2, "line count" + assert words == 4, "word count" + assert letters == 16, "letter count" +} + +test "index-of-all" { + let positions: [Int] = str_index_of_all("abXcdXefX", "X") + let n: Int = native_list_len(positions) + assert n == 3, "should find 3 occurrences" + let p0: Int = native_list_get(positions, 0) + let p1: Int = native_list_get(positions, 1) + let p2: Int = native_list_get(positions, 2) + assert p0 == 2, "first X at index 2" + assert p1 == 5, "second X at index 5" + assert p2 == 8, "third X at index 8" +} + +test "str-repeat" { + let s: String = str_repeat("ab", 3) + assert s == "ababab", "repeat 3 times" +} + +test "str-reverse" { + let s: String = str_reverse("hello") + assert s == "olleh", "reverse hello" +} + +test "str-strip-prefix" { + let s: String = str_strip_prefix("foobar", "foo") + assert s == "bar", "strip prefix foo" +} + +test "str-strip-suffix" { + let s: String = str_strip_suffix("hello.md", ".md") + assert s == "hello", "strip suffix .md" +} + +test "str-strip-chars" { + let s: String = str_strip_chars(" \thello \n", " \t\n") + assert s == "hello", "strip whitespace chars" +} + +test "split-lines" { + let lines: [String] = str_split_lines("alpha\nbeta\r\ngamma\n") + let n: Int = native_list_len(lines) + assert n == 3, "split into 3 lines" +} + +test "str-join" { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, "alpha") + let parts = native_list_append(parts, "beta") + let parts = native_list_append(parts, "gamma") + let result: String = str_join(parts, ", ") + assert result == "alpha, beta, gamma", "join with separator" +} + +test "char-classes" { + assert is_letter("A"), "A is a letter" + assert is_digit("7"), "7 is a digit" + assert is_whitespace(" "), "space is whitespace" + assert !is_letter("3"), "3 is not a letter" + assert !is_digit("X"), "X is not a digit" +} diff --git a/tests/native/test_time.el b/tests/native/test_time.el new file mode 100644 index 0000000..c4d8173 --- /dev/null +++ b/tests/native/test_time.el @@ -0,0 +1,155 @@ +// test_time.el - native test suite for runtime/time.el +// +// Covers: time_now (positive timestamp), time_to_parts (UTC decomposition), +// time_format (ISO 8601 and strftime tokens), time_add/diff, unix_timestamp, +// duration helpers, and instant conversions. + +test "time-now-positive" { + let ts: Int = time_now() + // time_now() returns milliseconds since epoch. + // Any value greater than 1_700_000_000_000 (Nov 2023) is valid in 2025+. + assert ts > 1700000000000, "time_now returns a reasonable recent timestamp" +} + +test "unix-timestamp-positive" { + let s: Int = unix_timestamp() + // Should be greater than 1_700_000_000 (seconds, Nov 2023) + assert s > 1700000000, "unix_timestamp returns seconds > 1700000000" +} + +test "now-ns-positive" { + let ns: Int = now_ns() + // Should be a large nanosecond value + assert ns > 0, "now_ns returns positive value" +} + +test "time-to-parts-epoch" { + // Unix epoch: 0 ms = 1970-01-01T00:00:00.000Z + let parts: String = time_to_parts(0) + let year: String = json_get(parts, "year") + let month: String = json_get(parts, "month") + let day: String = json_get(parts, "day") + let hour: String = json_get(parts, "hour") + assert year == "1970", "epoch year is 1970" + assert month == "1", "epoch month is 1" + assert day == "1", "epoch day is 1" + assert hour == "0", "epoch hour is 0" +} + +test "time-to-parts-known-date" { + // 2024-03-15T12:30:45.000Z + // seconds = 2024-03-15 12:30:45 UTC + // epoch ms: use a known value + // 2024-03-15 00:00:00 UTC = 1710460800 seconds + // + 12*3600 + 30*60 + 45 = 43200 + 1800 + 45 = 45045 seconds + // total: 1710505845 seconds = 1710505845000 ms + let ts: Int = 1710505845000 + let parts: String = time_to_parts(ts) + let year: String = json_get(parts, "year") + let month: String = json_get(parts, "month") + let day: String = json_get(parts, "day") + let hour: String = json_get(parts, "hour") + let minute: String = json_get(parts, "minute") + let second: String = json_get(parts, "second") + assert year == "2024", "year is 2024" + assert month == "3", "month is 3 (March)" + assert day == "15", "day is 15" + assert hour == "12", "hour is 12" + assert minute == "30", "minute is 30" + assert second == "45", "second is 45" +} + +test "time-format-iso" { + // Epoch = 1970-01-01T00:00:00.000Z + let formatted: String = time_format(0, "ISO") + assert formatted == "1970-01-01T00:00:00.000Z", "epoch formats to ISO correctly" +} + +test "time-format-iso-empty-fmt" { + // Empty format string should also produce ISO 8601 + let formatted: String = time_format(0, "") + assert formatted == "1970-01-01T00:00:00.000Z", "empty fmt produces ISO" +} + +test "time-format-strftime" { + // 2024-03-15T12:30:45.000Z + let ts: Int = 1710505845000 + let fmt: String = time_format(ts, "%Y-%m-%d") + assert fmt == "2024-03-15", "strftime %Y-%m-%d" + let hms: String = time_format(ts, "%H:%M:%S") + assert hms == "12:30:45", "strftime %H:%M:%S" +} + +test "time-add-milliseconds" { + let base: Int = 1000000 + let result: Int = time_add(base, 500, "ms") + assert result == 1000500, "add 500 ms" +} + +test "time-add-seconds" { + let base: Int = 0 + let result: Int = time_add(base, 60, "sec") + assert result == 60000, "add 60 seconds = 60000 ms" +} + +test "time-add-minutes" { + let base: Int = 0 + let result: Int = time_add(base, 2, "min") + assert result == 120000, "add 2 minutes = 120000 ms" +} + +test "time-add-hours" { + let base: Int = 0 + let result: Int = time_add(base, 1, "hour") + assert result == 3600000, "add 1 hour = 3600000 ms" +} + +test "time-add-days" { + let base: Int = 0 + let result: Int = time_add(base, 1, "day") + assert result == 86400000, "add 1 day = 86400000 ms" +} + +test "time-diff-seconds" { + let t1: Int = 0 + let t2: Int = 90000 + let d: Int = time_diff(t1, t2, "sec") + assert d == 90, "diff 90000 ms = 90 seconds" +} + +test "time-diff-negative" { + let t1: Int = 5000 + let t2: Int = 2000 + let d: Int = time_diff(t1, t2, "sec") + assert d == -3, "negative diff when t2 < t1" +} + +test "duration-helpers" { + let d_secs: Int = duration_seconds(5) + assert d_secs == 5000000000, "5 seconds in nanoseconds" + let d_ms: Int = duration_millis(100) + assert d_ms == 100000000, "100 ms in nanoseconds" + let d_ns: Int = duration_nanos(42) + assert d_ns == 42, "42 nanos is identity" + let back_secs: Int = duration_to_seconds(d_secs) + assert back_secs == 5, "convert back to seconds" + let back_ms: Int = duration_to_millis(d_ms) + assert back_ms == 100, "convert back to milliseconds" +} + +test "instant-conversions" { + let inst: Int = unix_millis(1000) + assert inst == 1000000000, "unix_millis(1000) in nanoseconds" + let inst_secs: Int = unix_seconds(2) + assert inst_secs == 2000000000, "unix_seconds(2) in nanoseconds" + let back_ms: Int = instant_to_unix_millis(inst) + assert back_ms == 1000, "instant to unix millis" + let back_secs: Int = instant_to_unix_seconds(inst_secs) + assert back_secs == 2, "instant to unix seconds" +} + +test "time-from-parts" { + // time_from_parts(secs, ns, tz) -> secs*1000 + ns/1_000_000 + let ts: Int = time_from_parts(1000, 500000000, "UTC") + assert ts == 1000500, "time_from_parts: 1000 secs + 500ms" +}