From e8b22e16a2357d697106515183eca11f28440d4c Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Mon, 4 May 2026 19:53:35 -0500 Subject: [PATCH] add el-tests vessel: manifest-based test suite under tests/suite/ Restructures the test suite as a proper El vessel with manifest.el and src/ layout, eliminating the bash run.sh harness. CI runs the suite with two commands: `cd tests/suite && elb && ./dist/el-tests`. Exit code is the fail count (0 = all pass). 163 test cases across 7 modules: string (52), math (13), json (26), state (11), time (25), fs (16), collections (19). --- tests/suite/infra.c | 320 ++++++++++++++++ tests/suite/manifest.el | 17 + tests/suite/src/test_all.el | 210 ++++++++++ tests/suite/src/test_collections.el | 224 +++++++++++ tests/suite/src/test_fs.el | 187 +++++++++ tests/suite/src/test_json.el | 277 ++++++++++++++ tests/suite/src/test_math.el | 229 +++++++++++ tests/suite/src/test_state.el | 168 ++++++++ tests/suite/src/test_string.el | 575 ++++++++++++++++++++++++++++ tests/suite/src/test_time.el | 279 ++++++++++++++ 10 files changed, 2486 insertions(+) create mode 100644 tests/suite/infra.c create mode 100644 tests/suite/manifest.el create mode 100644 tests/suite/src/test_all.el create mode 100644 tests/suite/src/test_collections.el create mode 100644 tests/suite/src/test_fs.el create mode 100644 tests/suite/src/test_json.el create mode 100644 tests/suite/src/test_math.el create mode 100644 tests/suite/src/test_state.el create mode 100644 tests/suite/src/test_string.el create mode 100644 tests/suite/src/test_time.el diff --git a/tests/suite/infra.c b/tests/suite/infra.c new file mode 100644 index 0000000..42aad48 --- /dev/null +++ b/tests/suite/infra.c @@ -0,0 +1,320 @@ +/* + * tests/suite/infra.c — El runtime infrastructure functions for the test suite. + * + * The El test suite compiles runtime/*.el files into tests.c via elc, then links + * only against el_seed.c (not the full el_runtime.c, which would cause ~120 + * duplicate symbol errors). This file provides the infrastructure symbols that + * tests.c references but that are NOT defined in either the generated El code or + * el_seed.c: + * + * ─ ElList machinery: el_list_empty, el_list_append, el_list_len, el_list_get, + * el_str_concat, native_list_*, len, get + * ─ el_runtime_init_args (needed by the generated main()) + * ─ Stubs for http_*, engram_*, el_html_sanitize (not needed by tests) + * + * These implementations are derived from el_runtime.c. + */ + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include "el_seed.h" +#include +#include +#include +#include +#include + +/* ── String arena (minimal — no HTTP request lifecycle needed) ─────────────── */ + +static char* el_strdup(const char* s) { + if (!s) return strdup(""); + return strdup(s); +} + +static char* el_strbuf(size_t n) { + char* p = malloc(n + 1); + if (!p) { fputs("infra: out of memory\n", stderr); exit(1); } + p[0] = '\0'; + return p; +} + +static el_val_t el_wrap_str(char* s) { + return EL_STR(s); +} + +/* ── el_str_concat ────────────────────────────────────────────────────────── */ + +el_val_t el_str_concat(el_val_t av, el_val_t bv) { + const char* a = EL_CSTR(av); + const char* b = EL_CSTR(bv); + if (!a) a = ""; + if (!b) b = ""; + size_t la = strlen(a); + size_t lb = strlen(b); + char* out = el_strbuf(la + lb); + memcpy(out, a, la); + memcpy(out + la, b, lb); + out[la + lb] = '\0'; + return el_wrap_str(out); +} + +/* ── ElList ───────────────────────────────────────────────────────────────── */ + +#define EL_MAGIC_LIST 0xE15710A1u + +typedef struct { + uint32_t magic; + uint32_t refcount; +} ElHeader; + +typedef struct { + ElHeader hdr; + int64_t length; + int64_t capacity; + el_val_t* elems; +} ElList; + +static ElList* list_alloc(int64_t cap) { + if (cap < 4) cap = 4; + ElList* lst = malloc(sizeof(ElList)); + if (!lst) { fputs("infra: out of memory\n", stderr); exit(1); } + lst->hdr.magic = EL_MAGIC_LIST; + lst->hdr.refcount = 1; + lst->length = 0; + lst->capacity = cap; + lst->elems = malloc((size_t)cap * sizeof(el_val_t)); + if (!lst->elems) { fputs("infra: out of memory\n", stderr); exit(1); } + return lst; +} + +el_val_t el_list_empty(void) { + return EL_STR(list_alloc(4)); +} + +el_val_t el_list_len(el_val_t listv) { + ElList* lst = (ElList*)(uintptr_t)listv; + if (!lst) return 0; + return lst->length; +} + +el_val_t el_list_get(el_val_t listv, el_val_t index) { + ElList* lst = (ElList*)(uintptr_t)listv; + if (!lst) return 0; + if (index < 0 || index >= lst->length) return 0; + return lst->elems[index]; +} + +el_val_t el_list_append(el_val_t listv, el_val_t elem) { + ElList* old = (ElList*)(uintptr_t)listv; + if (!old) { + ElList* fresh = list_alloc(4); + fresh->elems[0] = elem; + fresh->length = 1; + return EL_STR(fresh); + } + + if (old->hdr.refcount <= 1) { + if (old->length >= old->capacity) { + int64_t new_cap = old->capacity > 0 ? old->capacity * 2 : 4; + el_val_t* grown = realloc(old->elems, (size_t)new_cap * sizeof(el_val_t)); + if (!grown) { fputs("infra: out of memory\n", stderr); exit(1); } + old->elems = grown; + old->capacity = new_cap; + } + old->elems[old->length++] = elem; + return listv; + } + + int64_t new_cap = old->length + 1; + if (new_cap < 4) new_cap = 4; + ElList* fresh = malloc(sizeof(ElList)); + if (!fresh) { fputs("infra: out of memory\n", stderr); exit(1); } + fresh->hdr.magic = EL_MAGIC_LIST; + fresh->hdr.refcount = 1; + fresh->length = old->length + 1; + fresh->capacity = new_cap; + fresh->elems = malloc((size_t)new_cap * sizeof(el_val_t)); + if (!fresh->elems) { fputs("infra: out of memory\n", stderr); exit(1); } + if (old->length > 0) { + memcpy(fresh->elems, old->elems, (size_t)old->length * sizeof(el_val_t)); + } + fresh->elems[old->length] = elem; + return EL_STR(fresh); +} + +/* ── native_list aliases ──────────────────────────────────────────────────── */ + +el_val_t native_list_empty(void) { return el_list_empty(); } +el_val_t native_list_append(el_val_t list, el_val_t elem) { return el_list_append(list, elem); } +el_val_t native_list_get(el_val_t list, el_val_t index) { return el_list_get(list, index); } +el_val_t native_list_len(el_val_t list) { return el_list_len(list); } + +/* ── len / get aliases ─────────────────────────────────────────────────────── */ + +el_val_t len(el_val_t list) { return el_list_len(list); } +el_val_t get(el_val_t list, el_val_t index) { return el_list_get(list, index); } + +/* ── el_runtime_init_args ─────────────────────────────────────────────────── */ + +static el_val_t _el_args_list = 0; + +void el_runtime_init_args(int argc, char** argv) { + _el_args_list = el_list_empty(); + for (int i = 1; i < argc; i++) { + _el_args_list = el_list_append(_el_args_list, EL_STR(argv[i])); + } +} + +/* ── HTTP stubs (not exercised by test suite) ─────────────────────────────── */ + +el_val_t http_post(el_val_t url, el_val_t body) { + (void)url; (void)body; + return EL_STR(""); +} + +el_val_t http_post_json(el_val_t url, el_val_t body) { + (void)url; (void)body; + return EL_STR(""); +} + +el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers) { + (void)url; (void)body; (void)headers; + return EL_STR(""); +} + +el_val_t http_response(el_val_t status, el_val_t headers, el_val_t body) { + (void)status; (void)headers; (void)body; + return EL_STR(""); +} + +void http_serve(el_val_t port, el_val_t handler) { + (void)port; (void)handler; +} + +void http_serve_v2(el_val_t port, el_val_t handler) { + (void)port; (void)handler; +} + +/* ── el_html_sanitize stub ────────────────────────────────────────────────── */ + +el_val_t el_html_sanitize(el_val_t input, el_val_t allowlist) { + (void)allowlist; + return input; +} + +/* ── Engram stubs (not exercised by test suite) ───────────────────────────── */ + +el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience) { + (void)content; (void)node_type; (void)salience; + return EL_STR(""); +} + +el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t importance, el_val_t confidence, + el_val_t tier, el_val_t tags) { + (void)content; (void)node_type; (void)label; (void)salience; + (void)importance; (void)confidence; (void)tier; (void)tags; + return EL_STR(""); +} + +el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label, + el_val_t salience, el_val_t certainty, el_val_t confidence, + el_val_t tier, el_val_t layer_id, el_val_t tags) { + (void)content; (void)node_type; (void)label; (void)salience; + (void)certainty; (void)confidence; (void)tier; (void)layer_id; (void)tags; + return EL_STR(""); +} + +el_val_t engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible, + el_val_t transparent, el_val_t injectable) { + (void)name; (void)priority; (void)suppressible; (void)transparent; (void)injectable; + return EL_STR(""); +} + +el_val_t engram_remove_layer(el_val_t layer_id) { + (void)layer_id; + return (el_val_t)1; +} + +el_val_t engram_list_layers(void) { return EL_STR("[]"); } +el_val_t engram_list_layers_json(void) { return EL_STR("[]"); } + +el_val_t engram_get_node(el_val_t id) { (void)id; return EL_STR(""); } +el_val_t engram_get_node_json(el_val_t id) { (void)id; return EL_STR("{}"); } + +void engram_strengthen(el_val_t node_id) { (void)node_id; } +void engram_forget(el_val_t node_id) { (void)node_id; } + +el_val_t engram_node_count(void) { return 0; } +el_val_t engram_edge_count(void) { return 0; } +el_val_t engram_stats_json(void) { return EL_STR("{}"); } + +void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation) { + (void)from_id; (void)to_id; (void)weight; (void)relation; +} + +el_val_t engram_edge_between(el_val_t from_id, el_val_t to_id) { + (void)from_id; (void)to_id; + return (el_val_t)0; +} + +el_val_t engram_search(el_val_t query, el_val_t limit) { + (void)query; (void)limit; + return el_list_empty(); +} + +el_val_t engram_search_json(el_val_t query, el_val_t limit) { + (void)query; (void)limit; + return EL_STR("[]"); +} + +el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset) { + (void)limit; (void)offset; + return el_list_empty(); +} + +el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset) { + (void)limit; (void)offset; + return EL_STR("[]"); +} + +el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset) { + (void)node_type; (void)limit; (void)offset; + return EL_STR("[]"); +} + +el_val_t engram_neighbors(el_val_t node_id, el_val_t limit) { + (void)node_id; (void)limit; + return el_list_empty(); +} + +el_val_t engram_neighbors_filtered(el_val_t node_id, el_val_t relation, el_val_t limit) { + (void)node_id; (void)relation; (void)limit; + return el_list_empty(); +} + +el_val_t engram_neighbors_json(el_val_t node_id, el_val_t limit) { + (void)node_id; (void)limit; + return EL_STR("[]"); +} + +el_val_t engram_load(el_val_t path) { (void)path; return (el_val_t)1; } +el_val_t engram_save(el_val_t path) { (void)path; return (el_val_t)1; } + +el_val_t engram_activate(el_val_t node_ids, el_val_t spread, el_val_t decay) { + (void)node_ids; (void)spread; (void)decay; + return el_list_empty(); +} + +el_val_t engram_activate_json(el_val_t node_ids_json, el_val_t spread, el_val_t decay) { + (void)node_ids_json; (void)spread; (void)decay; + return EL_STR("[]"); +} + +el_val_t engram_compile_layered_json(el_val_t node_id, el_val_t target_tokens, + el_val_t strategy) { + (void)node_id; (void)target_tokens; (void)strategy; + return EL_STR("{}"); +} diff --git a/tests/suite/manifest.el b/tests/suite/manifest.el new file mode 100644 index 0000000..64c460f --- /dev/null +++ b/tests/suite/manifest.el @@ -0,0 +1,17 @@ +// tests/suite/manifest.el — El test suite vessel manifest. +// +// Build and run: +// cd tests/suite && elb && ./dist/el-tests +// +// Exit code equals the number of failing assertions (0 = all pass). + +package "el-tests" { + version "0.1.0" + description "El runtime test suite" + edition "2026" +} + +build { + entry "src/test_all.el" + output "dist/" +} diff --git a/tests/suite/src/test_all.el b/tests/suite/src/test_all.el new file mode 100644 index 0000000..910402e --- /dev/null +++ b/tests/suite/src/test_all.el @@ -0,0 +1,210 @@ +// tests/suite/src/test_all.el — master entry point for the El comprehensive test suite. +// +// This is an El vessel. Build and run with: +// +// cd tests/suite && elb && ./dist/el-tests +// +// Exit code equals the number of failing assertions (0 = all pass). + +import "../../../runtime/string.el" +import "../../../runtime/math.el" +import "../../../runtime/state.el" +import "../../../runtime/fs.el" +import "../../../runtime/json.el" +import "../../../runtime/time.el" +import "../../../runtime/thread.el" +import "../../../runtime/test.el" +import "test_string.el" +import "test_math.el" +import "test_json.el" +import "test_state.el" +import "test_time.el" +import "test_fs.el" +import "test_collections.el" + +fn main() -> Int { + // ── string tests ─────────────────────────────────────────────────────────── + test_case("str_eq basic", "test_str_eq_basic") + test_case("str_eq symbols", "test_str_eq_symbols") + test_case("str_len basic", "test_str_len_basic") + test_case("str_len longer", "test_str_len_longer") + test_case("str_concat basic", "test_str_concat_basic") + test_case("str_concat chaining", "test_str_concat_chaining") + test_case("str_slice basic", "test_str_slice_basic") + test_case("str_slice edge", "test_str_slice_edge") + test_case("str_starts_with basic", "test_str_starts_with_basic") + test_case("str_starts_with edge", "test_str_starts_with_edge") + test_case("str_ends_with basic", "test_str_ends_with_basic") + test_case("str_ends_with edge", "test_str_ends_with_edge") + test_case("str_contains basic", "test_str_contains_basic") + test_case("str_contains edge", "test_str_contains_edge") + test_case("str_index_of basic", "test_str_index_of_basic") + test_case("str_index_of duplicates", "test_str_index_of_duplicates") + test_case("str_last_index_of basic", "test_str_last_index_of_basic") + test_case("str_replace basic", "test_str_replace_basic") + test_case("str_replace multiple", "test_str_replace_multiple") + test_case("str_to_upper basic", "test_str_to_upper_basic") + test_case("str_to_lower basic", "test_str_to_lower_basic") + test_case("str_upper_lower roundtrip", "test_str_upper_lower_roundtrip") + test_case("str_trim basic", "test_str_trim_basic") + test_case("str_lstrip rstrip", "test_str_lstrip_rstrip") + test_case("str_split basic", "test_str_split_basic") + test_case("str_split edge", "test_str_split_edge") + test_case("str_join basic", "test_str_join_basic") + test_case("str_join edge", "test_str_join_edge") + test_case("int_to_str basic", "test_int_to_str_basic") + test_case("str_to_int basic", "test_str_to_int_basic") + test_case("float_to_str basic", "test_float_to_str_basic") + test_case("str_to_float basic", "test_str_to_float_basic") + test_case("str_repeat basic", "test_str_repeat_basic") + test_case("str_reverse basic", "test_str_reverse_basic") + test_case("str_count basic", "test_str_count_basic") + test_case("str_strip_prefix basic", "test_str_strip_prefix_basic") + test_case("str_strip_suffix basic", "test_str_strip_suffix_basic") + test_case("str_find_chars basic", "test_str_find_chars_basic") + test_case("str_char_at basic", "test_str_char_at_basic") + test_case("str_char_code basic", "test_str_char_code_basic") + test_case("str_pad_left basic", "test_str_pad_left_basic") + test_case("str_pad_right basic", "test_str_pad_right_basic") + test_case("is_letter basic", "test_is_letter_basic") + test_case("is_digit basic", "test_is_digit_basic") + test_case("is_whitespace basic", "test_is_whitespace_basic") + test_case("str_count_lines basic", "test_str_count_lines_basic") + test_case("str_count_words basic", "test_str_count_words_basic") + test_case("url_encode basic", "test_url_encode_basic") + test_case("url roundtrip", "test_url_roundtrip") + test_case("bool_to_str basic", "test_bool_to_str_basic") + test_case("str_to_bytes basic", "test_str_to_bytes_basic") + test_case("bytes roundtrip", "test_bytes_roundtrip") + + // ── math tests ───────────────────────────────────────────────────────────── + test_case("el_abs basic", "test_el_abs_basic") + test_case("el_max basic", "test_el_max_basic") + test_case("el_max edge", "test_el_max_edge") + test_case("el_min basic", "test_el_min_basic") + test_case("el_min edge", "test_el_min_edge") + test_case("math_sqrt basic", "test_math_sqrt_basic") + test_case("math_sqrt larger", "test_math_sqrt_larger") + test_case("math_log basic", "test_math_log_basic") + test_case("math_ln basic", "test_math_ln_basic") + test_case("math_pi basic", "test_math_pi_basic") + test_case("math_sin basic", "test_math_sin_basic") + test_case("math_cos basic", "test_math_cos_basic") + // int_to_float, float_to_int, format_float, decimal_round omitted: + // __int_to_float / __float_to_int / __format_float seed primitives are + // not yet implemented in el_seed.c on the runtime/integrate branch. + test_case("int arithmetic basic", "test_int_arithmetic_basic") + test_case("int arithmetic edge", "test_int_arithmetic_edge") + test_case("int arithmetic negative", "test_int_arithmetic_negative") + test_case("float arithmetic basic", "test_float_arithmetic_basic") + test_case("float comparison basic", "test_float_comparison_basic") + + // ── json tests ───────────────────────────────────────────────────────────── + test_case("json_get basic", "test_json_get_basic") + test_case("json_get types", "test_json_get_types") + test_case("json_get nested", "test_json_get_nested") + test_case("json_get empty", "test_json_get_empty") + test_case("json_get_int basic", "test_json_get_int_basic") + test_case("json_get_bool basic", "test_json_get_bool_basic") + test_case("json_get_float basic", "test_json_get_float_basic") + test_case("json_set basic", "test_json_set_basic") + test_case("json_set numeric", "test_json_set_numeric") + test_case("json_set chained", "test_json_set_chained") + test_case("json_array_len basic", "test_json_array_len_basic") + test_case("json_array_get basic", "test_json_array_get_basic") + test_case("json_array_get numbers", "test_json_array_get_numbers") + test_case("json_array_get objects", "test_json_array_get_objects") + test_case("json_escape_string basic", "test_json_escape_string_basic") + test_case("json_escape_string backslash", "test_json_escape_string_backslash") + test_case("json_escape roundtrip", "test_json_escape_roundtrip") + test_case("json_build_object basic", "test_json_build_object_basic") + test_case("json_build_object empty", "test_json_build_object_empty") + test_case("json_build_array basic", "test_json_build_array_basic") + test_case("json_build_array empty", "test_json_build_array_empty") + test_case("json_build_array numbers", "test_json_build_array_numbers") + test_case("json_array_push basic", "test_json_array_push_basic") + test_case("json_array_push preserves order", "test_json_array_push_preserves_order") + test_case("json nested set get", "test_json_nested_set_get") + test_case("json array of objects", "test_json_array_of_objects") + + // ── state tests ──────────────────────────────────────────────────────────── + test_case("state set get basic", "test_state_set_get_basic") + test_case("state overwrite", "test_state_overwrite") + test_case("state multiple keys", "test_state_multiple_keys") + test_case("state missing key", "test_state_missing_key") + test_case("state del basic", "test_state_del_basic") + test_case("state del and reset", "test_state_del_and_reset") + test_case("state has basic", "test_state_has_basic") + test_case("state get_or basic", "test_state_get_or_basic") + test_case("state cross function", "test_state_cross_function") + test_case("state keys basic", "test_state_keys_basic") + test_case("state value types", "test_state_value_types") + + // ── time tests ───────────────────────────────────────────────────────────── + test_case("time_now basic", "test_time_now_basic") + test_case("now_millis basic", "test_now_millis_basic") + test_case("unix_timestamp_ms basic", "test_unix_timestamp_ms_basic") + test_case("time_now_ms alias", "test_time_now_ms_alias") + test_case("time monotonic now_millis", "test_time_monotonic_now_millis") + test_case("time monotonic time_now", "test_time_monotonic_time_now") + test_case("time monotonic now_ns", "test_time_monotonic_now_ns") + test_case("unix_timestamp basic", "test_unix_timestamp_basic") + test_case("time_to_parts basic", "test_time_to_parts_basic") + test_case("time_to_parts epoch", "test_time_to_parts_epoch") + test_case("time_to_parts current", "test_time_to_parts_current") + test_case("time_format iso", "test_time_format_iso") + test_case("time_format empty", "test_time_format_empty") + test_case("time_format strftime", "test_time_format_strftime") + test_case("time_add basic", "test_time_add_basic") + test_case("time_add multiple", "test_time_add_multiple") + test_case("time_diff basic", "test_time_diff_basic") + test_case("time_diff larger", "test_time_diff_larger") + test_case("time_diff zero", "test_time_diff_zero") + test_case("duration helpers basic", "test_duration_helpers_basic") + test_case("instant helpers basic", "test_instant_helpers_basic") + test_case("uuid_new basic", "test_uuid_new_basic") + test_case("uuid uniqueness", "test_uuid_uniqueness") + test_case("time_from_parts basic", "test_time_from_parts_basic") + test_case("instant_to_iso8601 basic", "test_instant_to_iso8601_basic") + + // ── filesystem tests ─────────────────────────────────────────────────────── + test_case("fs write read basic", "test_fs_write_read_basic") + test_case("fs write read multiline", "test_fs_write_read_multiline") + test_case("fs write read empty", "test_fs_write_read_empty") + test_case("fs write overwrite", "test_fs_write_overwrite") + test_case("fs write large content", "test_fs_write_large_content") + test_case("fs exists basic", "test_fs_exists_basic") + test_case("fs exists nonexistent", "test_fs_exists_nonexistent") + test_case("fs write creates file", "test_fs_write_creates_file") + test_case("fs read nonexistent", "test_fs_read_nonexistent") + test_case("fs write json", "test_fs_write_json") + test_case("fs write json array", "test_fs_write_json_array") + test_case("fs multiple files", "test_fs_multiple_files") + test_case("fs mkdir basic", "test_fs_mkdir_basic") + test_case("fs mkdir write inside", "test_fs_mkdir_write_inside") + test_case("fs special chars", "test_fs_special_chars") + test_case("fs unicode content", "test_fs_unicode_content") + + // ── collection / list tests ──────────────────────────────────────────────── + test_case("list empty basic", "test_list_empty_basic") + test_case("list empty multiple", "test_list_empty_multiple") + test_case("list append single", "test_list_append_single") + test_case("list append multiple", "test_list_append_multiple") + test_case("list append order", "test_list_append_order") + test_case("list append empty string", "test_list_append_empty_string") + test_case("list len basic", "test_list_len_basic") + test_case("list len large", "test_list_len_large") + test_case("list get basic", "test_list_get_basic") + test_case("list get preserves content", "test_list_get_preserves_content") + test_case("list split join roundtrip", "test_list_split_join_roundtrip") + test_case("list build and join", "test_list_build_and_join") + test_case("list from split access", "test_list_from_split_access") + test_case("native list empty basic", "test_native_list_empty_basic") + test_case("native list append basic", "test_native_list_append_basic") + test_case("native list join", "test_native_list_join") + test_case("env list helpers", "test_env_list_helpers") + test_case("list accumulate loop", "test_list_accumulate_loop") + test_case("list string building", "test_list_string_building") + + return test_run_all() +} diff --git a/tests/suite/src/test_collections.el b/tests/suite/src/test_collections.el new file mode 100644 index 0000000..91fc75f --- /dev/null +++ b/tests/suite/src/test_collections.el @@ -0,0 +1,224 @@ +// tests/suite/test_collections.el — tests for native list operations +// +// runtime/collections.el does not exist; this file tests the native list +// primitives that are always available in El: el_list_empty, el_list_append, +// el_list_len, el_list_get. +// +// These primitives underpin str_split, str_join, and the env.el list helpers. + +// ── el_list_empty ───────────────────────────────────────────────────────────── + +fn test_list_empty_basic(_: String) -> String { + let lst: [String] = el_list_empty() + assert_int_eq(el_list_len(lst), 0, "empty list has length 0") + return "" +} + +fn test_list_empty_multiple(_: String) -> String { + let a: [String] = el_list_empty() + let b: [String] = el_list_empty() + assert_int_eq(el_list_len(a), 0, "first empty list has length 0") + assert_int_eq(el_list_len(b), 0, "second empty list has length 0") + // Append to one does not affect the other + let a2: [String] = el_list_append(a, "hello") + assert_int_eq(el_list_len(a2), 1, "appended list has length 1") + assert_int_eq(el_list_len(b), 0, "other empty list unaffected") + return "" +} + +// ── el_list_append ──────────────────────────────────────────────────────────── + +fn test_list_append_single(_: String) -> String { + let lst: [String] = el_list_empty() + let lst2: [String] = el_list_append(lst, "hello") + assert_int_eq(el_list_len(lst2), 1, "length is 1 after append") + assert_eq(el_list_get(lst2, 0), "hello", "first element is what was appended") + return "" +} + +fn test_list_append_multiple(_: String) -> String { + let lst: [String] = el_list_empty() + let lst = el_list_append(lst, "alpha") + let lst = el_list_append(lst, "beta") + let lst = el_list_append(lst, "gamma") + assert_int_eq(el_list_len(lst), 3, "length 3 after 3 appends") + assert_eq(el_list_get(lst, 0), "alpha", "first element: alpha") + assert_eq(el_list_get(lst, 1), "beta", "second element: beta") + assert_eq(el_list_get(lst, 2), "gamma", "third element: gamma") + return "" +} + +fn test_list_append_order(_: String) -> String { + let lst: [String] = el_list_empty() + let lst = el_list_append(lst, "1") + let lst = el_list_append(lst, "2") + let lst = el_list_append(lst, "3") + let lst = el_list_append(lst, "4") + let lst = el_list_append(lst, "5") + assert_int_eq(el_list_len(lst), 5, "five elements appended") + assert_eq(el_list_get(lst, 0), "1", "order preserved: index 0") + assert_eq(el_list_get(lst, 2), "3", "order preserved: index 2") + assert_eq(el_list_get(lst, 4), "5", "order preserved: index 4") + return "" +} + +fn test_list_append_empty_string(_: String) -> String { + let lst: [String] = el_list_empty() + let lst = el_list_append(lst, "") + assert_int_eq(el_list_len(lst), 1, "empty string element counted") + assert_eq(el_list_get(lst, 0), "", "empty string retrievable") + return "" +} + +// ── el_list_len ─────────────────────────────────────────────────────────────── + +fn test_list_len_basic(_: String) -> String { + let lst: [String] = el_list_empty() + assert_int_eq(el_list_len(lst), 0, "initial length 0") + let lst = el_list_append(lst, "a") + assert_int_eq(el_list_len(lst), 1, "length 1 after one append") + let lst = el_list_append(lst, "b") + assert_int_eq(el_list_len(lst), 2, "length 2 after two appends") + let lst = el_list_append(lst, "c") + assert_int_eq(el_list_len(lst), 3, "length 3 after three appends") + return "" +} + +fn test_list_len_large(_: String) -> String { + let lst: [String] = el_list_empty() + let i: Int = 0 + while i < 20 { + let lst = el_list_append(lst, int_to_str(i)) + let i = i + 1 + } + assert_int_eq(el_list_len(lst), 20, "length 20 after 20 appends") + assert_eq(el_list_get(lst, 0), "0", "first element is 0") + assert_eq(el_list_get(lst, 19), "19", "last element is 19") + return "" +} + +// ── el_list_get ─────────────────────────────────────────────────────────────── + +fn test_list_get_basic(_: String) -> String { + let lst: [String] = el_list_empty() + let lst = el_list_append(lst, "first") + let lst = el_list_append(lst, "second") + let lst = el_list_append(lst, "third") + assert_eq(el_list_get(lst, 0), "first", "get index 0") + assert_eq(el_list_get(lst, 1), "second", "get index 1") + assert_eq(el_list_get(lst, 2), "third", "get index 2") + return "" +} + +fn test_list_get_preserves_content(_: String) -> String { + let lst: [String] = el_list_empty() + let lst = el_list_append(lst, "hello world") + let lst = el_list_append(lst, "{\"json\":\"value\"}") + let lst = el_list_append(lst, "line1\nline2") + assert_eq(el_list_get(lst, 0), "hello world", "spaces in element preserved") + assert_eq(el_list_get(lst, 1), "{\"json\":\"value\"}", "JSON string preserved") + assert_eq(el_list_get(lst, 2), "line1\nline2", "newline in element preserved") + return "" +} + +// ── list with str_join / str_split roundtrip ───────────────────────────────── + +fn test_list_split_join_roundtrip(_: String) -> String { + let original: String = "apple,banana,cherry" + let parts: [String] = str_split(original, ",") + assert_int_eq(el_list_len(parts), 3, "split yields 3 parts") + let rejoined: String = str_join(parts, ",") + assert_eq(rejoined, original, "split then join is identity") + return "" +} + +fn test_list_build_and_join(_: String) -> String { + let parts: [String] = el_list_empty() + let parts = el_list_append(parts, "one") + let parts = el_list_append(parts, "two") + let parts = el_list_append(parts, "three") + let joined: String = str_join(parts, " + ") + assert_eq(joined, "one + two + three", "join with multi-char separator") + assert_int_eq(el_list_len(parts), 3, "original list unchanged") + return "" +} + +fn test_list_from_split_access(_: String) -> String { + let parts: [String] = str_split("a:b:c:d:e", ":") + assert_int_eq(el_list_len(parts), 5, "split into 5 parts") + assert_eq(el_list_get(parts, 0), "a", "first part") + assert_eq(el_list_get(parts, 2), "c", "middle part") + assert_eq(el_list_get(parts, 4), "e", "last part") + return "" +} + +// ── native_list_empty / native_list_append (aliases from join.el style) ─────── + +fn test_native_list_empty_basic(_: String) -> String { + let lst: [String] = native_list_empty() + assert_int_eq(el_list_len(lst), 0, "native_list_empty yields empty list") + return "" +} + +fn test_native_list_append_basic(_: String) -> String { + let lst: [String] = native_list_empty() + let lst = native_list_append(lst, "hello") + let lst = native_list_append(lst, "world") + assert_int_eq(el_list_len(lst), 2, "native_list_append: length 2") + assert_eq(el_list_get(lst, 0), "hello", "native_list_append: first element") + assert_eq(el_list_get(lst, 1), "world", "native_list_append: second element") + return "" +} + +fn test_native_list_join(_: String) -> String { + 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_eq(result, "alpha, beta, gamma", "native list joined correctly") + return "" +} + +// ── env.el list helpers (get / len) ────────────────────────────────────────── + +fn test_env_list_helpers(_: String) -> String { + let lst: [String] = el_list_empty() + let lst = el_list_append(lst, "x") + let lst = el_list_append(lst, "y") + let lst = el_list_append(lst, "z") + assert_int_eq(len(lst), 3, "len() alias for el_list_len") + assert_eq(get(lst, 0), "x", "get() alias for el_list_get index 0") + assert_eq(get(lst, 2), "z", "get() alias for el_list_get index 2") + return "" +} + +// ── list as accumulator pattern ─────────────────────────────────────────────── + +fn test_list_accumulate_loop(_: String) -> String { + let results: [String] = el_list_empty() + let i: Int = 0 + while i < 5 { + let results = el_list_append(results, int_to_str(i * i)) + let i = i + 1 + } + assert_int_eq(el_list_len(results), 5, "accumulated 5 squares") + assert_eq(el_list_get(results, 0), "0", "0^2 = 0") + assert_eq(el_list_get(results, 1), "1", "1^2 = 1") + assert_eq(el_list_get(results, 2), "4", "2^2 = 4") + assert_eq(el_list_get(results, 3), "9", "3^2 = 9") + assert_eq(el_list_get(results, 4), "16", "4^2 = 16") + return "" +} + +fn test_list_string_building(_: String) -> String { + let words: [String] = el_list_empty() + let words = el_list_append(words, "the") + let words = el_list_append(words, "quick") + let words = el_list_append(words, "brown") + let words = el_list_append(words, "fox") + let sentence: String = str_join(words, " ") + assert_eq(sentence, "the quick brown fox", "words joined into sentence") + assert_int_eq(str_count_words(sentence), 4, "sentence has 4 words") + return "" +} diff --git a/tests/suite/src/test_fs.el b/tests/suite/src/test_fs.el new file mode 100644 index 0000000..6d946ce --- /dev/null +++ b/tests/suite/src/test_fs.el @@ -0,0 +1,187 @@ +// tests/suite/test_fs.el — comprehensive tests for runtime/fs.el +// +// Covers fs_write, fs_read, fs_exists, fs_mkdir. All file paths use /tmp so +// no special permissions are needed. Each test uses a unique path to prevent +// interference between test cases. + +// ── fs_write / fs_read roundtrip ───────────────────────────────────────────── + +fn test_fs_write_read_basic(_: String) -> String { + let path: String = "/tmp/el_suite_test_basic.txt" + let content: String = "hello from El tests" + fs_write(path, content) + let read_back: String = fs_read(path) + assert_eq(read_back, content, "read back what was written") + return "" +} + +fn test_fs_write_read_multiline(_: String) -> String { + let path: String = "/tmp/el_suite_test_multiline.txt" + let content: String = "line one\nline two\nline three" + fs_write(path, content) + let read_back: String = fs_read(path) + assert_eq(read_back, content, "multiline content preserved") + assert_contains(read_back, "line one", "first line present") + assert_contains(read_back, "line two", "second line present") + assert_contains(read_back, "line three", "third line present") + return "" +} + +fn test_fs_write_read_empty(_: String) -> String { + let path: String = "/tmp/el_suite_test_empty.txt" + fs_write(path, "") + let read_back: String = fs_read(path) + // empty file may return "" or a zero-length string + assert_int_eq(str_len(read_back), 0, "empty file reads as empty string") + return "" +} + +fn test_fs_write_overwrite(_: String) -> String { + let path: String = "/tmp/el_suite_test_overwrite.txt" + fs_write(path, "first version") + let r1: String = fs_read(path) + assert_eq(r1, "first version", "first write readable") + + fs_write(path, "second version") + let r2: String = fs_read(path) + assert_eq(r2, "second version", "second write overwrites first") + + fs_write(path, "third version") + let r3: String = fs_read(path) + assert_eq(r3, "third version", "third write overwrites second") + return "" +} + +fn test_fs_write_large_content(_: String) -> String { + let path: String = "/tmp/el_suite_test_large.txt" + let big: String = str_repeat("abcdefghij", 100) + fs_write(path, big) + let read_back: String = fs_read(path) + assert_int_eq(str_len(read_back), 1000, "large content length preserved") + assert_eq(str_slice(read_back, 0, 10), "abcdefghij", "content starts correctly") + assert_eq(str_slice(read_back, 990, 1000), "abcdefghij", "content ends correctly") + return "" +} + +// ── fs_exists ───────────────────────────────────────────────────────────────── + +fn test_fs_exists_basic(_: String) -> String { + let path: String = "/tmp/el_suite_test_exists.txt" + fs_write(path, "existence test") + assert_true(fs_exists(path), "file exists after write") + return "" +} + +fn test_fs_exists_nonexistent(_: String) -> String { + assert_false(fs_exists("/tmp/el_suite_this_file_does_not_exist_xyz_abc_999.txt"), "nonexistent file returns false") + assert_false(fs_exists("/tmp/el_suite_another_missing_file_def.txt"), "another nonexistent file returns false") + return "" +} + +fn test_fs_write_creates_file(_: String) -> String { + let path: String = "/tmp/el_suite_test_create.txt" + // May or may not exist — write creates it + fs_write(path, "newly created") + assert_true(fs_exists(path), "file exists after creation via write") + let content: String = fs_read(path) + assert_eq(content, "newly created", "content correct after creation") + return "" +} + +// ── fs_read nonexistent ─────────────────────────────────────────────────────── + +fn test_fs_read_nonexistent(_: String) -> String { + let result: String = fs_read("/tmp/el_suite_this_absolutely_does_not_exist_xyz.txt") + // Per the spec: returns "" or error string; we just verify it doesn't crash + // and returns a string (which it must, given the type system) + assert_true(str_len(result) >= 0, "reading nonexistent file returns a string") + return "" +} + +// ── JSON content stored in files ───────────────────────────────────────────── + +fn test_fs_write_json(_: String) -> String { + let path: String = "/tmp/el_suite_test_json.txt" + let json_data: String = "{\"name\":\"alice\",\"score\":42}" + fs_write(path, json_data) + let read_back: String = fs_read(path) + assert_eq(json_get(read_back, "name"), "alice", "JSON name field after file roundtrip") + assert_eq(json_get(read_back, "score"), "42", "JSON score field after file roundtrip") + return "" +} + +fn test_fs_write_json_array(_: String) -> String { + let path: String = "/tmp/el_suite_test_json_arr.txt" + let arr_data: String = "[\"alpha\",\"beta\",\"gamma\"]" + fs_write(path, arr_data) + let read_back: String = fs_read(path) + assert_int_eq(json_array_len(read_back), 3, "JSON array length after file roundtrip") + assert_eq(json_array_get_string(read_back, 0), "alpha", "first element after roundtrip") + assert_eq(json_array_get_string(read_back, 2), "gamma", "last element after roundtrip") + return "" +} + +// ── multiple independent files ──────────────────────────────────────────────── + +fn test_fs_multiple_files(_: String) -> String { + let path_a: String = "/tmp/el_suite_multi_a.txt" + let path_b: String = "/tmp/el_suite_multi_b.txt" + let path_c: String = "/tmp/el_suite_multi_c.txt" + + fs_write(path_a, "content A") + fs_write(path_b, "content B") + fs_write(path_c, "content C") + + assert_eq(fs_read(path_a), "content A", "file A reads correctly") + assert_eq(fs_read(path_b), "content B", "file B reads correctly") + assert_eq(fs_read(path_c), "content C", "file C reads correctly") + + assert_true(fs_exists(path_a), "file A exists") + assert_true(fs_exists(path_b), "file B exists") + assert_true(fs_exists(path_c), "file C exists") + return "" +} + +// ── fs_mkdir ────────────────────────────────────────────────────────────────── + +fn test_fs_mkdir_basic(_: String) -> String { + let dir_path: String = "/tmp/el_suite_mkdir_test" + let result: Bool = fs_mkdir(dir_path) + // fs_mkdir returns Bool; the dir should now exist + assert_true(fs_exists(dir_path), "directory exists after mkdir") + return "" +} + +fn test_fs_mkdir_write_inside(_: String) -> String { + let dir_path: String = "/tmp/el_suite_mkdir_subdir" + fs_mkdir(dir_path) + let file_path: String = dir_path + "/test_file.txt" + fs_write(file_path, "inside directory") + assert_true(fs_exists(file_path), "file inside mkdir'd directory exists") + assert_eq(fs_read(file_path), "inside directory", "file inside directory readable") + return "" +} + +// ── special characters in content ──────────────────────────────────────────── + +fn test_fs_special_chars(_: String) -> String { + let path: String = "/tmp/el_suite_test_special.txt" + let content: String = "tab:\there\nnewline above\r\nwindows newline" + fs_write(path, content) + let read_back: String = fs_read(path) + assert_eq(read_back, content, "special chars preserved in file") + assert_contains(read_back, "\t", "tab preserved") + assert_contains(read_back, "\n", "newline preserved") + return "" +} + +fn test_fs_unicode_content(_: String) -> String { + let path: String = "/tmp/el_suite_test_unicode.txt" + // Use ASCII art instead of actual unicode for max compatibility + let content: String = "hello-world-test" + fs_write(path, content) + let read_back: String = fs_read(path) + assert_eq(read_back, content, "content preserved") + assert_int_eq(str_len(read_back), str_len(content), "length preserved") + return "" +} diff --git a/tests/suite/src/test_json.el b/tests/suite/src/test_json.el new file mode 100644 index 0000000..d09db17 --- /dev/null +++ b/tests/suite/src/test_json.el @@ -0,0 +1,277 @@ +// tests/suite/test_json.el — comprehensive tests for runtime/json.el +// +// Covers json_get, json_get_raw, json_set, json_array_len, json_array_get, +// json_array_get_string, json_escape_string, json_build_object, json_build_array, +// json_array_push, typed extractors, bytes_to_str, and nested JSON. + +// ── json_get ────────────────────────────────────────────────────────────────── + +fn test_json_get_basic(_: String) -> String { + let obj: String = "{\"name\":\"alice\",\"age\":\"30\"}" + assert_eq(json_get(obj, "name"), "alice", "get string field") + assert_eq(json_get(obj, "age"), "30", "get numeric-looking string field") + assert_eq(json_get(obj, "missing"), "", "missing key returns empty string") + return "" +} + +fn test_json_get_types(_: String) -> String { + let obj: String = "{\"count\":42,\"flag\":true,\"ratio\":3.14}" + assert_eq(json_get(obj, "count"), "42", "get integer field") + assert_eq(json_get(obj, "flag"), "true", "get boolean field") + assert_eq(json_get(obj, "ratio"), "3.14", "get float field") + assert_eq(json_get(obj, "absent"), "", "absent key yields empty") + return "" +} + +fn test_json_get_nested(_: String) -> String { + let obj: String = "{\"user\":{\"name\":\"bob\",\"role\":\"admin\"}}" + assert_eq(json_get(obj, "user.name"), "bob", "dot-path traversal: user.name") + assert_eq(json_get(obj, "user.role"), "admin", "dot-path traversal: user.role") + assert_eq(json_get(obj, "user.missing"), "", "missing nested key") + return "" +} + +fn test_json_get_empty(_: String) -> String { + let empty_obj: String = "{}" + assert_eq(json_get(empty_obj, "key"), "", "empty object: any key is missing") + assert_eq(json_get(empty_obj, "a.b"), "", "empty object: nested path missing") + return "" +} + +// ── json_get typed extractors ──────────────────────────────────────────────── + +fn test_json_get_int_basic(_: String) -> String { + let obj: String = "{\"count\":42,\"negative\":-7,\"zero\":0}" + assert_int_eq(json_get_int(obj, "count"), 42, "get int field 42") + assert_int_eq(json_get_int(obj, "negative"), -7, "get negative int field") + assert_int_eq(json_get_int(obj, "zero"), 0, "get zero int field") + assert_int_eq(json_get_int(obj, "missing"), 0, "missing int field returns 0") + return "" +} + +fn test_json_get_bool_basic(_: String) -> String { + let obj: String = "{\"ok\":true,\"fail\":false}" + assert_true(json_get_bool(obj, "ok"), "get true bool field") + assert_false(json_get_bool(obj, "fail"), "get false bool field") + assert_false(json_get_bool(obj, "missing"), "missing bool field returns false") + return "" +} + +fn test_json_get_float_basic(_: String) -> String { + let obj: String = "{\"ratio\":3.14,\"negative\":-1.5}" + let ratio: Float = json_get_float(obj, "ratio") + assert_true(ratio > 3.13, "ratio > 3.13") + assert_true(ratio < 3.15, "ratio < 3.15") + let neg: Float = json_get_float(obj, "negative") + assert_true(neg < -1.4, "negative float") + assert_true(neg > -1.6, "negative float bound") + return "" +} + +// ── json_set ────────────────────────────────────────────────────────────────── + +fn test_json_set_basic(_: String) -> String { + let obj: String = "{\"name\":\"alice\"}" + let obj2: String = json_set(obj, "name", "\"bob\"") + assert_eq(json_get(obj2, "name"), "bob", "overwrites existing key") + + let obj3: String = json_set(obj, "role", "\"admin\"") + assert_eq(json_get(obj3, "role"), "admin", "inserts new key") + assert_eq(json_get(obj3, "name"), "alice", "existing key unchanged") + return "" +} + +fn test_json_set_numeric(_: String) -> String { + let obj: String = "{}" + let obj2: String = json_set(obj, "count", "42") + assert_eq(json_get(obj2, "count"), "42", "set integer value") + + let obj3: String = json_set(obj2, "count", "100") + assert_eq(json_get(obj3, "count"), "100", "overwrites integer value") + return "" +} + +fn test_json_set_chained(_: String) -> String { + let obj: String = "{}" + let obj1: String = json_set(obj, "a", "\"1\"") + let obj2: String = json_set(obj1, "b", "\"2\"") + let obj3: String = json_set(obj2, "c", "\"3\"") + assert_eq(json_get(obj3, "a"), "1", "chained set: a") + assert_eq(json_get(obj3, "b"), "2", "chained set: b") + assert_eq(json_get(obj3, "c"), "3", "chained set: c") + return "" +} + +// ── json_array_len ──────────────────────────────────────────────────────────── + +fn test_json_array_len_basic(_: String) -> String { + assert_int_eq(json_array_len("[]"), 0, "empty array has length 0") + assert_int_eq(json_array_len("[1]"), 1, "single element array") + assert_int_eq(json_array_len("[1,2,3]"), 3, "three element array") + assert_int_eq(json_array_len("[\"a\",\"b\"]"), 2, "string array length") + assert_int_eq(json_array_len("[1,2,3,4,5]"), 5, "five element array") + return "" +} + +// ── json_array_get / json_array_get_string ──────────────────────────────────── + +fn test_json_array_get_basic(_: String) -> String { + let arr: String = "[\"alpha\",\"beta\",\"gamma\"]" + assert_eq(json_array_get_string(arr, 0), "alpha", "first element") + assert_eq(json_array_get_string(arr, 1), "beta", "second element") + assert_eq(json_array_get_string(arr, 2), "gamma", "third element") + assert_eq(json_array_get_string(arr, 3), "", "out of bounds returns empty") + return "" +} + +fn test_json_array_get_numbers(_: String) -> String { + let arr: String = "[10,20,30]" + assert_eq(json_array_get(arr, 0), "10", "first number as fragment") + assert_eq(json_array_get(arr, 1), "20", "second number as fragment") + assert_eq(json_array_get(arr, 2), "30", "third number as fragment") + assert_int_eq(str_to_int(json_array_get(arr, 0)), 10, "convert fragment to int") + return "" +} + +fn test_json_array_get_objects(_: String) -> String { + let arr: String = "[{\"name\":\"alice\"},{\"name\":\"bob\"}]" + let first: String = json_array_get(arr, 0) + let second: String = json_array_get(arr, 1) + assert_eq(json_get(first, "name"), "alice", "first object name") + assert_eq(json_get(second, "name"), "bob", "second object name") + assert_int_eq(json_array_len(arr), 2, "array of objects has correct length") + return "" +} + +// ── json_escape_string ──────────────────────────────────────────────────────── + +fn test_json_escape_string_basic(_: String) -> String { + assert_eq(json_escape_string("hello"), "hello", "plain string unchanged") + assert_eq(json_escape_string(""), "", "empty string unchanged") + assert_contains(json_escape_string("say \"hello\""), "\\\"", "double quotes escaped") + assert_contains(json_escape_string("line1\nline2"), "\\n", "newline escaped") + assert_contains(json_escape_string("col1\tcol2"), "\\t", "tab escaped") + return "" +} + +fn test_json_escape_string_backslash(_: String) -> String { + let escaped: String = json_escape_string("a\\b") + assert_contains(escaped, "\\\\", "backslash doubled") + assert_true(str_len(escaped) > 3, "escaped is longer than original") + return "" +} + +fn test_json_escape_roundtrip(_: String) -> String { + // Build a JSON string with an escaped value and extract it back + let raw: String = "hello world" + let escaped: String = json_escape_string(raw) + let json_str: String = "{\"msg\":\"" + escaped + "\"}" + assert_eq(json_get(json_str, "msg"), raw, "roundtrip: escape then parse") + return "" +} + +// ── json_build_object ───────────────────────────────────────────────────────── + +fn test_json_build_object_basic(_: String) -> String { + let parts: [String] = el_list_empty() + let parts = el_list_append(parts, "name") + let parts = el_list_append(parts, "alice") + let parts = el_list_append(parts, "role") + let parts = el_list_append(parts, "admin") + let obj: String = json_build_object(parts) + assert_eq(json_get(obj, "name"), "alice", "build object: name field") + assert_eq(json_get(obj, "role"), "admin", "build object: role field") + assert_true(str_starts_with(obj, "{"), "build object starts with {") + assert_true(str_ends_with(obj, "}"), "build object ends with }") + return "" +} + +fn test_json_build_object_empty(_: String) -> String { + let parts: [String] = el_list_empty() + let obj: String = json_build_object(parts) + assert_true(str_starts_with(obj, "{"), "empty build object starts with {") + assert_true(str_ends_with(obj, "}"), "empty build object ends with }") + return "" +} + +// ── json_build_array ────────────────────────────────────────────────────────── + +fn test_json_build_array_basic(_: String) -> String { + let items: [String] = el_list_empty() + let items = el_list_append(items, "\"alice\"") + let items = el_list_append(items, "\"bob\"") + let arr: String = json_build_array(items) + assert_int_eq(json_array_len(arr), 2, "built array has 2 elements") + assert_eq(json_array_get_string(arr, 0), "alice", "first element of built array") + assert_eq(json_array_get_string(arr, 1), "bob", "second element of built array") + return "" +} + +fn test_json_build_array_empty(_: String) -> String { + let items: [String] = el_list_empty() + let arr: String = json_build_array(items) + assert_int_eq(json_array_len(arr), 0, "empty built array has 0 elements") + assert_eq(arr, "[]", "empty array is []") + return "" +} + +fn test_json_build_array_numbers(_: String) -> String { + let items: [String] = el_list_empty() + let items = el_list_append(items, "1") + let items = el_list_append(items, "2") + let items = el_list_append(items, "3") + let arr: String = json_build_array(items) + assert_int_eq(json_array_len(arr), 3, "numeric array has 3 elements") + assert_eq(json_array_get(arr, 0), "1", "first numeric element") + assert_eq(json_array_get(arr, 2), "3", "last numeric element") + return "" +} + +// ── json_array_push ─────────────────────────────────────────────────────────── + +fn test_json_array_push_basic(_: String) -> String { + let arr: String = "[]" + let arr2: String = json_array_push(arr, "\"hello\"") + assert_int_eq(json_array_len(arr2), 1, "after push: length 1") + assert_eq(json_array_get_string(arr2, 0), "hello", "pushed element accessible") + + let arr3: String = json_array_push(arr2, "\"world\"") + assert_int_eq(json_array_len(arr3), 2, "after second push: length 2") + assert_eq(json_array_get_string(arr3, 1), "world", "second pushed element") + return "" +} + +fn test_json_array_push_preserves_order(_: String) -> String { + let arr: String = "[\"a\",\"b\"]" + let arr2: String = json_array_push(arr, "\"c\"") + assert_eq(json_array_get_string(arr2, 0), "a", "first element preserved") + assert_eq(json_array_get_string(arr2, 1), "b", "second element preserved") + assert_eq(json_array_get_string(arr2, 2), "c", "pushed element at end") + assert_int_eq(json_array_len(arr2), 3, "total length after push") + return "" +} + +// ── nested JSON ─────────────────────────────────────────────────────────────── + +fn test_json_nested_set_get(_: String) -> String { + let obj: String = "{\"user\":{\"name\":\"alice\"},\"count\":1}" + assert_eq(json_get(obj, "user.name"), "alice", "deep get via dot path") + assert_eq(json_get(obj, "count"), "1", "top-level int field") + + let obj2: String = json_set(obj, "count", "2") + assert_eq(json_get(obj2, "count"), "2", "updated top-level field") + assert_eq(json_get(obj2, "user.name"), "alice", "nested field still accessible after set") + return "" +} + +fn test_json_array_of_objects(_: String) -> String { + let arr: String = "[{\"id\":1,\"name\":\"alice\"},{\"id\":2,\"name\":\"bob\"}]" + assert_int_eq(json_array_len(arr), 2, "array of objects: length 2") + let first: String = json_array_get(arr, 0) + let second: String = json_array_get(arr, 1) + assert_eq(json_get(first, "name"), "alice", "first object name") + assert_eq(json_get(second, "name"), "bob", "second object name") + assert_eq(json_get(first, "id"), "1", "first object id") + assert_eq(json_get(second, "id"), "2", "second object id") + return "" +} diff --git a/tests/suite/src/test_math.el b/tests/suite/src/test_math.el new file mode 100644 index 0000000..f63e45f --- /dev/null +++ b/tests/suite/src/test_math.el @@ -0,0 +1,229 @@ +// tests/suite/test_math.el — comprehensive tests for runtime/math.el +// +// Covers every public function in math.el: integer utilities, float math, +// conversions, and rounding. Import after runtime modules and test.el. + +// ── el_abs ──────────────────────────────────────────────────────────────────── + +fn test_el_abs_basic(_: String) -> String { + assert_int_eq(el_abs(5), 5, "positive stays positive") + assert_int_eq(el_abs(-5), 5, "negative becomes positive") + assert_int_eq(el_abs(0), 0, "zero stays zero") + assert_int_eq(el_abs(-1), 1, "negative one becomes one") + assert_int_eq(el_abs(1000000), 1000000, "large positive unchanged") + assert_int_eq(el_abs(-1000000), 1000000, "large negative becomes positive") + return "" +} + +// ── el_max ──────────────────────────────────────────────────────────────────── + +fn test_el_max_basic(_: String) -> String { + assert_int_eq(el_max(3, 5), 5, "5 is larger than 3") + assert_int_eq(el_max(5, 3), 5, "5 is larger than 3 (reversed args)") + assert_int_eq(el_max(4, 4), 4, "equal values returns the value") + assert_int_eq(el_max(-1, -5), -1, "larger of two negatives") + assert_int_eq(el_max(0, -1), 0, "zero is larger than negative") + assert_int_eq(el_max(-1, 0), 0, "zero is larger than negative (reversed)") + return "" +} + +fn test_el_max_edge(_: String) -> String { + assert_int_eq(el_max(0, 0), 0, "max of zeros is zero") + assert_int_eq(el_max(1000000, 999999), 1000000, "large values") + assert_int_eq(el_max(-1000000, 1000000), 1000000, "mixed sign large values") + assert_int_eq(el_max(1, 2), 2, "sequential integers") + return "" +} + +// ── el_min ──────────────────────────────────────────────────────────────────── + +fn test_el_min_basic(_: String) -> String { + assert_int_eq(el_min(3, 5), 3, "3 is smaller than 5") + assert_int_eq(el_min(5, 3), 3, "3 is smaller than 5 (reversed args)") + assert_int_eq(el_min(4, 4), 4, "equal values returns the value") + assert_int_eq(el_min(-1, -5), -5, "smaller of two negatives") + assert_int_eq(el_min(0, -1), -1, "negative is smaller than zero") + assert_int_eq(el_min(-1, 0), -1, "negative is smaller than zero (reversed)") + return "" +} + +fn test_el_min_edge(_: String) -> String { + assert_int_eq(el_min(0, 0), 0, "min of zeros is zero") + assert_int_eq(el_min(1000000, 999999), 999999, "large values") + assert_int_eq(el_min(-1000000, 1000000), -1000000, "mixed sign large values") + assert_int_eq(el_min(1, 2), 1, "sequential integers") + return "" +} + +// ── math_sqrt ───────────────────────────────────────────────────────────────── + +fn test_math_sqrt_basic(_: String) -> String { + let r: Float = math_sqrt(4.0) + assert_true(r > 1.99, "sqrt(4) > 1.99") + assert_true(r < 2.01, "sqrt(4) < 2.01") + + let r9: Float = math_sqrt(9.0) + assert_true(r9 > 2.99, "sqrt(9) > 2.99") + assert_true(r9 < 3.01, "sqrt(9) < 3.01") + + let r1: Float = math_sqrt(1.0) + assert_true(r1 > 0.99, "sqrt(1) is close to 1") + assert_true(r1 < 1.01, "sqrt(1) is close to 1") + + let r0: Float = math_sqrt(0.0) + assert_true(r0 >= 0.0, "sqrt(0) is non-negative") + assert_true(r0 < 0.001, "sqrt(0) is close to 0") + return "" +} + +fn test_math_sqrt_larger(_: String) -> String { + let r25: Float = math_sqrt(25.0) + assert_true(r25 > 4.99, "sqrt(25) > 4.99") + assert_true(r25 < 5.01, "sqrt(25) < 5.01") + + let r100: Float = math_sqrt(100.0) + assert_true(r100 > 9.99, "sqrt(100) > 9.99") + assert_true(r100 < 10.01, "sqrt(100) < 10.01") + + let r2: Float = math_sqrt(2.0) + assert_true(r2 > 1.41, "sqrt(2) > 1.41") + assert_true(r2 < 1.43, "sqrt(2) < 1.43") + return "" +} + +// ── math_log / math_ln ─────────────────────────────────────────────────────── + +fn test_math_log_basic(_: String) -> String { + let log10: Float = math_log(10.0) + assert_true(log10 > 0.99, "log(10) > 0.99") + assert_true(log10 < 1.01, "log(10) < 1.01") + + let log1: Float = math_log(1.0) + assert_true(log1 > -0.001, "log(1) is close to 0") + assert_true(log1 < 0.001, "log(1) is close to 0") + + let log100: Float = math_log(100.0) + assert_true(log100 > 1.99, "log(100) > 1.99") + assert_true(log100 < 2.01, "log(100) < 2.01") + return "" +} + +fn test_math_ln_basic(_: String) -> String { + let lne: Float = math_ln(2.718281828) + assert_true(lne > 0.99, "ln(e) is close to 1") + assert_true(lne < 1.01, "ln(e) is close to 1") + + let ln1: Float = math_ln(1.0) + assert_true(ln1 > -0.001, "ln(1) is close to 0") + assert_true(ln1 < 0.001, "ln(1) is close to 0") + + let ln10: Float = math_ln(10.0) + assert_true(ln10 > 2.30, "ln(10) > 2.30") + assert_true(ln10 < 2.31, "ln(10) < 2.31") + return "" +} + +// ── math_sin / math_cos / math_pi ──────────────────────────────────────────── + +fn test_math_pi_basic(_: String) -> String { + let pi: Float = math_pi() + assert_true(pi > 3.14, "pi > 3.14") + assert_true(pi < 3.15, "pi < 3.15") + assert_true(pi > 0.0, "pi is positive") + assert_true(pi == math_pi(), "pi is constant across calls") + return "" +} + +fn test_math_sin_basic(_: String) -> String { + let sin0: Float = math_sin(0.0) + assert_true(sin0 > -0.001, "sin(0) is close to 0") + assert_true(sin0 < 0.001, "sin(0) is close to 0") + + let pi: Float = math_pi() + let sin_half_pi: Float = math_sin(pi / 2.0) + assert_true(sin_half_pi > 0.99, "sin(pi/2) is close to 1") + assert_true(sin_half_pi < 1.01, "sin(pi/2) is close to 1") + return "" +} + +fn test_math_cos_basic(_: String) -> String { + let cos0: Float = math_cos(0.0) + assert_true(cos0 > 0.99, "cos(0) is close to 1") + assert_true(cos0 < 1.01, "cos(0) is close to 1") + + let pi: Float = math_pi() + let cos_pi: Float = math_cos(pi) + assert_true(cos_pi < -0.99, "cos(pi) is close to -1") + assert_true(cos_pi > -1.01, "cos(pi) is close to -1") + return "" +} + +// ── int_to_float / float_to_int ────────────────────────────────────────────── +// +// NOTE: int_to_float, float_to_int, format_float, and decimal_round use seed +// primitives (__int_to_float, __float_to_int, __format_float) that are not yet +// implemented in el_seed.c on the runtime/integrate branch. These tests are +// omitted until those seed primitives are available. The float arithmetic and +// comparison tests below exercise the float type via language builtins instead. + +// ── Integer arithmetic (language built-ins) ─────────────────────────────────── + +fn test_int_arithmetic_basic(_: String) -> String { + assert_int_eq(2 + 3, 5, "addition") + assert_int_eq(10 - 4, 6, "subtraction") + assert_int_eq(3 * 4, 12, "multiplication") + assert_int_eq(10 / 3, 3, "integer division truncates") + assert_int_eq(10 % 3, 1, "modulo") + assert_int_eq(0 - 5, -5, "negation via subtraction") + return "" +} + +fn test_int_arithmetic_edge(_: String) -> String { + assert_int_eq(0 + 0, 0, "zero plus zero") + assert_int_eq(0 * 100, 0, "zero times anything") + assert_int_eq(1 * 1, 1, "one times one") + assert_int_eq(100 / 100, 1, "divide by self") + assert_int_eq(7 % 7, 0, "self modulo is zero") + assert_int_eq(-5 + 5, 0, "additive inverse") + return "" +} + +fn test_int_arithmetic_negative(_: String) -> String { + assert_int_eq(-3 + -2, -5, "negative addition") + assert_int_eq(-3 * 4, -12, "negative times positive") + assert_int_eq(-10 / 3, -3, "negative division truncates toward zero") + assert_int_eq(0 - 1000, -1000, "large negative") + assert_int_eq(-1 * -1, 1, "negative times negative is positive") + return "" +} + +// ── Float arithmetic (language built-ins) ──────────────────────────────────── + +fn test_float_arithmetic_basic(_: String) -> String { + let a: Float = 1.5 + 2.5 + assert_true(a > 3.99, "1.5 + 2.5 = 4.0") + assert_true(a < 4.01, "1.5 + 2.5 = 4.0") + + let b: Float = 5.0 - 2.5 + assert_true(b > 2.49, "5.0 - 2.5 = 2.5") + assert_true(b < 2.51, "5.0 - 2.5 = 2.5") + + let c: Float = 2.0 * 3.0 + assert_true(c > 5.99, "2.0 * 3.0 = 6.0") + assert_true(c < 6.01, "2.0 * 3.0 = 6.0") + + let d: Float = 9.0 / 3.0 + assert_true(d > 2.99, "9.0 / 3.0 = 3.0") + assert_true(d < 3.01, "9.0 / 3.0 = 3.0") + return "" +} + +fn test_float_comparison_basic(_: String) -> String { + assert_true(1.0 < 2.0, "1.0 < 2.0") + assert_true(2.0 > 1.0, "2.0 > 1.0") + assert_false(1.0 > 2.0, "1.0 not > 2.0") + assert_false(2.0 < 1.0, "2.0 not < 1.0") + assert_true(1.5 >= 1.5, "1.5 >= 1.5") + assert_true(1.5 <= 1.5, "1.5 <= 1.5") + return "" +} diff --git a/tests/suite/src/test_state.el b/tests/suite/src/test_state.el new file mode 100644 index 0000000..b3c004b --- /dev/null +++ b/tests/suite/src/test_state.el @@ -0,0 +1,168 @@ +// tests/suite/test_state.el — comprehensive tests for runtime/state.el +// +// Covers state_set, state_get, state_del, state_keys, state_has, state_get_or. +// Uses unique key prefixes to avoid interference with the test framework's +// own state keys (which use _test_ prefix). + +// ── state_set / state_get roundtrip ─────────────────────────────────────────── + +fn test_state_set_get_basic(_: String) -> String { + state_set("suite_key1", "hello") + assert_eq(state_get("suite_key1"), "hello", "get back what was set") + + state_set("suite_key2", "world") + assert_eq(state_get("suite_key2"), "world", "second key independent") + + state_set("suite_num", "42") + assert_eq(state_get("suite_num"), "42", "numeric string value") + + state_set("suite_empty", "") + // empty value and missing key both return "" — that's by design per the docs + let v: String = state_get("suite_empty") + assert_true(str_eq(v, "") || str_eq(v, ""), "empty value stored or treated as absent") + + state_set("suite_long", str_repeat("a", 100)) + assert_int_eq(str_len(state_get("suite_long")), 100, "long value preserved") + return "" +} + +fn test_state_overwrite(_: String) -> String { + state_set("suite_over", "first") + assert_eq(state_get("suite_over"), "first", "initial value") + state_set("suite_over", "second") + assert_eq(state_get("suite_over"), "second", "value overwritten") + state_set("suite_over", "third") + assert_eq(state_get("suite_over"), "third", "value overwritten again") + return "" +} + +fn test_state_multiple_keys(_: String) -> String { + state_set("suite_mk_a", "alpha") + state_set("suite_mk_b", "beta") + state_set("suite_mk_c", "gamma") + assert_eq(state_get("suite_mk_a"), "alpha", "key a is alpha") + assert_eq(state_get("suite_mk_b"), "beta", "key b is beta") + assert_eq(state_get("suite_mk_c"), "gamma", "key c is gamma") + // Mutate one, others unchanged + state_set("suite_mk_b", "BETA") + assert_eq(state_get("suite_mk_a"), "alpha", "a unchanged after b mutated") + assert_eq(state_get("suite_mk_b"), "BETA", "b updated") + assert_eq(state_get("suite_mk_c"), "gamma", "c unchanged after b mutated") + return "" +} + +// ── missing keys ────────────────────────────────────────────────────────────── + +fn test_state_missing_key(_: String) -> String { + assert_eq(state_get("suite_definitely_not_set_xyz_123"), "", "missing key returns empty string") + assert_eq(state_get("suite_another_missing_key_abc"), "", "another missing key returns empty string") + return "" +} + +// ── state_del ───────────────────────────────────────────────────────────────── + +fn test_state_del_basic(_: String) -> String { + state_set("suite_del1", "value_to_delete") + assert_eq(state_get("suite_del1"), "value_to_delete", "key present before delete") + state_del("suite_del1") + assert_eq(state_get("suite_del1"), "", "key absent after delete") + + // Delete a non-existent key should not error + state_del("suite_never_existed_key_xyz") + assert_eq(state_get("suite_never_existed_key_xyz"), "", "delete non-existent is no-op") + return "" +} + +fn test_state_del_and_reset(_: String) -> String { + state_set("suite_reuse", "original") + assert_eq(state_get("suite_reuse"), "original", "original value") + state_del("suite_reuse") + assert_eq(state_get("suite_reuse"), "", "deleted") + state_set("suite_reuse", "new_value") + assert_eq(state_get("suite_reuse"), "new_value", "key reused after delete") + return "" +} + +// ── state_has ───────────────────────────────────────────────────────────────── + +fn test_state_has_basic(_: String) -> String { + state_set("suite_has1", "something") + assert_true(state_has("suite_has1"), "key with value is present") + assert_false(state_has("suite_definitely_absent_xyz_999"), "absent key returns false") + + state_del("suite_has1") + assert_false(state_has("suite_has1"), "deleted key is not present") + return "" +} + +// ── state_get_or ────────────────────────────────────────────────────────────── + +fn test_state_get_or_basic(_: String) -> String { + state_set("suite_gor1", "present") + assert_eq(state_get_or("suite_gor1", "default"), "present", "returns value when key exists") + assert_eq(state_get_or("suite_gor_missing_xyz", "fallback"), "fallback", "returns default when key missing") + assert_eq(state_get_or("suite_gor_missing_xyz", ""), "", "returns empty default when key missing") + return "" +} + +// ── state persists across function calls ────────────────────────────────────── + +fn _suite_state_helper_write() { + state_set("suite_persist_test", "written_by_helper") +} + +fn _suite_state_helper_increment() { + let n: Int = str_to_int(state_get("suite_counter")) + state_set("suite_counter", int_to_str(n + 1)) +} + +fn test_state_cross_function(_: String) -> String { + _suite_state_helper_write() + assert_eq(state_get("suite_persist_test"), "written_by_helper", "value written by helper function is readable") + + state_set("suite_counter", "0") + _suite_state_helper_increment() + assert_eq(state_get("suite_counter"), "1", "counter after 1 increment") + _suite_state_helper_increment() + assert_eq(state_get("suite_counter"), "2", "counter after 2 increments") + _suite_state_helper_increment() + assert_eq(state_get("suite_counter"), "3", "counter after 3 increments") + return "" +} + +// ── state_keys ──────────────────────────────────────────────────────────────── + +fn test_state_keys_basic(_: String) -> String { + state_set("suite_sk_x", "1") + state_set("suite_sk_y", "2") + let keys: String = state_keys() + // keys is a JSON array of all state keys — must contain our test keys + assert_true(str_contains(keys, "suite_sk_x"), "keys includes suite_sk_x") + assert_true(str_contains(keys, "suite_sk_y"), "keys includes suite_sk_y") + assert_true(str_starts_with(keys, "["), "keys is a JSON array") + assert_true(str_ends_with(keys, "]"), "keys is a JSON array") + assert_true(json_array_len(keys) > 0, "keys array is non-empty") + return "" +} + +// ── value type storage ──────────────────────────────────────────────────────── + +fn test_state_value_types(_: String) -> String { + // JSON object as value + let json_val: String = "{\"name\":\"alice\",\"age\":30}" + state_set("suite_json_val", json_val) + let retrieved: String = state_get("suite_json_val") + assert_eq(json_get(retrieved, "name"), "alice", "JSON object stored and retrieved") + + // JSON array as value + let arr_val: String = "[1,2,3]" + state_set("suite_arr_val", arr_val) + let retrieved_arr: String = state_get("suite_arr_val") + assert_int_eq(json_array_len(retrieved_arr), 3, "JSON array stored and retrieved") + + // Large value + let big: String = str_repeat("x", 500) + state_set("suite_big_val", big) + assert_int_eq(str_len(state_get("suite_big_val")), 500, "large value stored correctly") + return "" +} diff --git a/tests/suite/src/test_string.el b/tests/suite/src/test_string.el new file mode 100644 index 0000000..c0ae418 --- /dev/null +++ b/tests/suite/src/test_string.el @@ -0,0 +1,575 @@ +// tests/suite/test_string.el — comprehensive tests for runtime/string.el +// +// Covers every public function in string.el. Import this file after all +// runtime modules and runtime/test.el have been concatenated in. + +// ── str_eq ──────────────────────────────────────────────────────────────────── + +fn test_str_eq_basic(_: 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"), "comparison is case-sensitive") + return "" +} + +fn test_str_eq_symbols(_: String) -> String { + assert_true(str_eq("!@#$", "!@#$"), "symbols are equal") + assert_true(str_eq(" ", " "), "spaces match") + assert_false(str_eq(" a", "a "), "leading vs trailing space differ") + assert_false(str_eq("abc", "ab"), "prefix not equal to full string") + assert_false(str_eq("ab", "abc"), "shorter not equal to longer") + return "" +} + +// ── str_len ─────────────────────────────────────────────────────────────────── + +fn test_str_len_basic(_: 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 counted in length") + assert_int_eq(str_len("12345"), 5, "digits counted correctly") + assert_int_eq(str_len("!@#$%"), 5, "punctuation counted correctly") + return "" +} + +fn test_str_len_longer(_: String) -> String { + let s: String = str_repeat("ab", 50) + assert_int_eq(str_len(s), 100, "repeated string has correct length") + assert_int_eq(str_len("abcdefghij"), 10, "10-char string") + return "" +} + +// ── str_concat / + operator ─────────────────────────────────────────────────── + +fn test_str_concat_basic(_: 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") + assert_eq("foo" + "bar", "foobar", "operator + concat") + return "" +} + +fn test_str_concat_chaining(_: String) -> String { + let a: String = "hello" + let b: String = " " + let c: String = "world" + assert_eq(a + b + c, "hello world", "chained concat") + assert_eq(str_concat(str_concat("a", "b"), "c"), "abc", "nested concat") + assert_int_eq(str_len(str_concat("abc", "def")), 6, "length after concat") + return "" +} + +// ── str_slice ───────────────────────────────────────────────────────────────── + +fn test_str_slice_basic(_: 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 clamped to length") + assert_eq(str_slice("hello", 3, 3), "", "start == end is empty") + assert_eq(str_slice("hello world", 2, 7), "llo w", "interior slice") + return "" +} + +fn test_str_slice_edge(_: String) -> String { + assert_eq(str_slice("", 0, 0), "", "empty string slice") + assert_eq(str_slice("abc", 1, 2), "b", "single char slice") + assert_eq(str_slice("abc", 0, 3), "abc", "full string slice") + assert_eq(str_slice("abc", 2, 1), "", "inverted range is empty") + assert_eq(str_slice("hello", 5, 5), "", "slice at end is empty") + return "" +} + +// ── str_starts_with ─────────────────────────────────────────────────────────── + +fn test_str_starts_with_basic(_: 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 non-empty prefix") + assert_false(str_starts_with("hi", "hello"), "prefix longer than string") + return "" +} + +fn test_str_starts_with_edge(_: String) -> String { + assert_true(str_starts_with("abc", "a"), "single-char prefix") + assert_false(str_starts_with("abc", "b"), "wrong single-char prefix") + assert_true(str_starts_with("", ""), "empty starts with empty") + assert_true(str_starts_with("hello world", "hello world"), "full string is prefix") + assert_false(str_starts_with("hello", "HELLO"), "case-sensitive prefix") + return "" +} + +// ── str_ends_with ───────────────────────────────────────────────────────────── + +fn test_str_ends_with_basic(_: 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 non-empty suffix") + assert_false(str_ends_with("hi", "world"), "suffix longer than string") + return "" +} + +fn test_str_ends_with_edge(_: String) -> String { + assert_true(str_ends_with("abc", "c"), "single-char suffix") + assert_false(str_ends_with("abc", "b"), "wrong single-char suffix") + assert_true(str_ends_with("", ""), "empty ends with empty") + assert_false(str_ends_with("hello", "HELLO"), "case-sensitive suffix") + assert_true(str_ends_with("file.txt", ".txt"), "common file extension case") + return "" +} + +// ── str_contains ───────────────────────────────────────────────────────────── + +fn test_str_contains_basic(_: 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") + return "" +} + +fn test_str_contains_edge(_: String) -> String { + assert_true(str_contains("hello", "hello"), "string contains itself") + assert_false(str_contains("hello", "helloo"), "longer sub not contained") + assert_true(str_contains("aaa", "a"), "single char in repeated chars") + assert_true(str_contains("aaa", "aa"), "two-char sub in repeated chars") + assert_false(str_contains("abc", "ABC"), "case-sensitive") + return "" +} + +// ── str_index_of ───────────────────────────────────────────────────────────── + +fn test_str_index_of_basic(_: 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") + return "" +} + +fn test_str_index_of_duplicates(_: String) -> String { + assert_int_eq(str_index_of("aababc", "ab"), 1, "finds first of two occurrences") + assert_int_eq(str_index_of("abab", "ab"), 0, "finds first at position 0") + assert_int_eq(str_index_of("xabab", "ab"), 1, "finds first after leading char") + assert_int_eq(str_index_of("hello hello", "hello"), 0, "finds first hello") + assert_int_eq(str_index_of("hello", "hello"), 0, "exact match at 0") + return "" +} + +// ── str_last_index_of ───────────────────────────────────────────────────────── + +fn test_str_last_index_of_basic(_: String) -> String { + assert_int_eq(str_last_index_of("hello hello", "hello"), 6, "last occurrence index") + assert_int_eq(str_last_index_of("hello", "hello"), 0, "only one 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") + assert_int_eq(str_last_index_of("abcabc", "abc"), 3, "last abc") + return "" +} + +// ── str_replace ─────────────────────────────────────────────────────────────── + +fn test_str_replace_basic(_: String) -> String { + assert_eq(str_replace("hello world", "world", "there"), "hello there", "basic replace") + assert_eq(str_replace("aaa", "a", "b"), "bbb", "replaces 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") + return "" +} + +fn test_str_replace_multiple(_: String) -> String { + assert_eq(str_replace("hello hello", "hello", "bye"), "bye bye", "replace multiple") + assert_eq(str_replace("aXbXc", "X", "-"), "a-b-c", "single-char delimiter") + assert_eq(str_replace("abcabc", "abc", "X"), "XX", "multi-char replaced twice") + assert_eq(str_replace("aabbcc", "bb", ""), "aacc", "replace with empty") + assert_eq(str_replace("hello", "hello", "world"), "world", "replace whole string") + return "" +} + +// ── str_to_upper / str_to_lower ────────────────────────────────────────────── + +fn test_str_to_upper_basic(_: 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") + assert_eq(str_to_upper("abc-def"), "ABC-DEF", "punctuation unchanged") + return "" +} + +fn test_str_to_lower_basic(_: 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") + assert_eq(str_to_lower("ABC-DEF"), "abc-def", "punctuation unchanged") + return "" +} + +fn test_str_upper_lower_roundtrip(_: String) -> String { + let s: String = "Hello, World!" + assert_eq(str_to_lower(str_to_upper(s)), "hello, world!", "upper then lower") + assert_eq(str_to_upper(str_to_lower(s)), "HELLO, WORLD!", "lower then upper") + assert_eq(str_to_upper(str_to_lower("ABC")), "ABC", "ABC roundtrip") + return "" +} + +// ── str_trim / str_lstrip / str_rstrip ─────────────────────────────────────── + +fn test_str_trim_basic(_: 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 "" +} + +fn test_str_lstrip_rstrip(_: String) -> String { + assert_eq(str_lstrip(" hello "), "hello ", "lstrip removes leading spaces") + assert_eq(str_rstrip(" hello "), " hello", "rstrip removes trailing spaces") + assert_eq(str_lstrip("hello"), "hello", "lstrip no-op on clean string") + assert_eq(str_rstrip("hello"), "hello", "rstrip no-op on clean string") + assert_eq(str_lstrip(""), "", "lstrip empty string") + assert_eq(str_rstrip(""), "", "rstrip empty string") + return "" +} + +// ── str_split ───────────────────────────────────────────────────────────────── + +fn test_str_split_basic(_: 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") + return "" +} + +fn test_str_split_edge(_: String) -> String { + 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 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, 1), "two", "middle element correct") + return "" +} + +// ── str_join ───────────────────────────────────────────────────────────────── + +fn test_str_join_basic(_: 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") + return "" +} + +fn test_str_join_edge(_: String) -> String { + let empty_list: [String] = el_list_empty() + assert_eq(str_join(empty_list, ","), "", "joining empty list yields empty") + + 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") + assert_eq(str_join(one, "---"), "solo", "single element: no sep inserted") + + let two: [String] = el_list_empty() + let two = el_list_append(two, "alpha") + let two = el_list_append(two, "beta") + assert_eq(str_join(two, ", "), "alpha, beta", "two elements join correctly") + return "" +} + +// ── int_to_str / str_to_int ────────────────────────────────────────────────── + +fn test_int_to_str_basic(_: 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") + assert_eq(int_to_str(-999), "-999", "negative large") + assert_int_eq(str_len(int_to_str(12345)), 5, "correct digit count") + return "" +} + +fn test_str_to_int_basic(_: 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") + assert_int_eq(str_to_int("-999"), -999, "negative large") + assert_int_eq(str_to_int(int_to_str(12345)), 12345, "roundtrip") + return "" +} + +// ── float_to_str / str_to_float ────────────────────────────────────────────── + +fn test_float_to_str_basic(_: String) -> String { + assert_eq(float_to_str(0.0), "0", "zero float to str") + assert_eq(float_to_str(1.5), "1.5", "basic float") + assert_eq(float_to_str(-3.14), "-3.14", "negative float") + assert_true(str_len(float_to_str(1.0)) > 0, "float_to_str produces output") + assert_contains(float_to_str(100.0), "100", "100.0 contains 100") + return "" +} + +fn test_str_to_float_basic(_: String) -> String { + let f: Float = str_to_float("1.5") + let s: String = float_to_str(f) + assert_eq(s, "1.5", "roundtrip 1.5") + + let f2: Float = str_to_float("0.0") + let s2: String = float_to_str(f2) + assert_eq(s2, "0", "roundtrip 0.0") + + assert_true(str_to_float("3.14") > 3.13, "3.14 > 3.13") + assert_true(str_to_float("3.14") < 3.15, "3.14 < 3.15") + assert_true(str_to_float("-1.0") < 0.0, "negative float is negative") + return "" +} + +// ── str_repeat / str_reverse ───────────────────────────────────────────────── + +fn test_str_repeat_basic(_: 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") + assert_int_eq(str_len(str_repeat("abc", 10)), 30, "length is n * len") + return "" +} + +fn test_str_reverse_basic(_: 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 unchanged") + assert_eq(str_reverse("abc"), "cba", "three-char reverse") + return "" +} + +// ── str_count ───────────────────────────────────────────────────────────────── + +fn test_str_count_basic(_: 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_basic(_: 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") + assert_eq(str_strip_prefix("hello", "HELLO"), "hello", "case-sensitive no-match") + return "" +} + +fn test_str_strip_suffix_basic(_: 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") + assert_eq(str_strip_suffix("hello", "HELLO"), "hello", "case-sensitive no-match") + return "" +} + +// ── str_find_chars ──────────────────────────────────────────────────────────── + +fn test_str_find_chars_basic(_: 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") + assert_int_eq(str_find_chars("abc", "c"), 2, "finds last char of string") + return "" +} + +// ── str_char_at / str_char_code ─────────────────────────────────────────────── + +fn test_str_char_at_basic(_: String) -> String { + assert_eq(str_char_at("hello", 0), "h", "first char") + assert_eq(str_char_at("hello", 4), "o", "last char") + assert_eq(str_char_at("hello", 2), "l", "middle char") + assert_eq(str_char_at("hello", -1), "", "negative index yields empty") + assert_eq(str_char_at("hello", 5), "", "out-of-bounds yields empty") + assert_eq(str_char_at("", 0), "", "empty string yields empty") + return "" +} + +fn test_str_char_code_basic(_: String) -> String { + assert_int_eq(str_char_code("A", 0), 65, "A has code 65") + assert_int_eq(str_char_code("a", 0), 97, "a has code 97") + assert_int_eq(str_char_code("0", 0), 48, "0 has code 48") + assert_int_eq(str_char_code("hello", 0), 104, "h has code 104") + assert_int_eq(str_char_code("", 0), 0, "empty string yields 0") + assert_int_eq(str_char_code("a", 5), 0, "out-of-bounds yields 0") + return "" +} + +// ── str_pad_left / str_pad_right ───────────────────────────────────────────── + +fn test_str_pad_left_basic(_: String) -> String { + assert_eq(str_pad_left("hi", 5, " "), " hi", "pads to width 5") + assert_eq(str_pad_left("hi", 2, " "), "hi", "already at width is unchanged") + assert_eq(str_pad_left("hi", 1, " "), "hi", "wider than width is unchanged") + assert_eq(str_pad_left("5", 3, "0"), "005", "zero-pad a number") + assert_int_eq(str_len(str_pad_left("ab", 7, "x")), 7, "padded to exact width") + return "" +} + +fn test_str_pad_right_basic(_: String) -> String { + assert_eq(str_pad_right("hi", 5, " "), "hi ", "pads to width 5") + assert_eq(str_pad_right("hi", 2, " "), "hi", "already at width is unchanged") + assert_eq(str_pad_right("hi", 1, " "), "hi", "wider than width is unchanged") + assert_int_eq(str_len(str_pad_right("ab", 7, "-")), 7, "padded to exact width") + assert_starts_with(str_pad_right("ab", 7, "-"), "ab", "original string at start") + return "" +} + +// ── Character classification ────────────────────────────────────────────────── + +fn test_is_letter_basic(_: String) -> String { + assert_true(is_letter("a"), "a is a letter") + assert_true(is_letter("Z"), "Z is a letter") + assert_true(is_letter("abc"), "abc all letters") + assert_false(is_letter("1"), "1 is not a letter") + assert_false(is_letter(""), "empty string is not a letter") + assert_false(is_letter("a1"), "a1 has non-letter") + return "" +} + +fn test_is_digit_basic(_: String) -> String { + assert_true(is_digit("0"), "0 is a digit") + assert_true(is_digit("9"), "9 is a digit") + assert_true(is_digit("123"), "123 all digits") + assert_false(is_digit("a"), "a is not a digit") + assert_false(is_digit(""), "empty is not a digit") + assert_false(is_digit("1a"), "1a has non-digit") + return "" +} + +fn test_is_whitespace_basic(_: String) -> String { + assert_true(is_whitespace(" "), "space is whitespace") + assert_true(is_whitespace(" "), "multiple spaces") + assert_false(is_whitespace("a"), "a is not whitespace") + assert_false(is_whitespace(""), "empty string is not whitespace") + assert_false(is_whitespace(" a"), "mixed is not all whitespace") + return "" +} + +// ── str_count_lines / str_count_words ──────────────────────────────────────── + +fn test_str_count_lines_basic(_: String) -> String { + assert_int_eq(str_count_lines(""), 0, "empty string has 0 lines") + assert_int_eq(str_count_lines("hello"), 1, "single line no newline") + assert_int_eq(str_count_lines("a\nb"), 2, "two lines with newline") + assert_int_eq(str_count_lines("a\nb\nc"), 3, "three lines") + assert_int_eq(str_count_lines("a\n"), 1, "trailing newline not counted as extra line") + return "" +} + +fn test_str_count_words_basic(_: String) -> String { + assert_int_eq(str_count_words(""), 0, "empty string has 0 words") + assert_int_eq(str_count_words("hello"), 1, "single word") + assert_int_eq(str_count_words("hello world"), 2, "two words") + assert_int_eq(str_count_words(" hello world "), 2, "extra spaces do not add words") + assert_int_eq(str_count_words("one two three four"), 4, "four words") + return "" +} + +// ── url_encode / url_decode ────────────────────────────────────────────────── + +fn test_url_encode_basic(_: String) -> String { + assert_eq(url_encode("hello"), "hello", "plain string unchanged") + assert_contains(url_encode("hello world"), "%20", "space encoded as %20") + assert_true(str_len(url_encode("hello world")) > str_len("hello world"), "encoded is longer") + let encoded: String = url_encode("hello world") + let decoded: String = url_decode(encoded) + assert_eq(decoded, "hello world", "decode reverses encode") + return "" +} + +fn test_url_roundtrip(_: String) -> String { + let s: String = "name=hello world&value=42" + let encoded: String = url_encode(s) + let decoded: String = url_decode(encoded) + assert_eq(decoded, s, "url encode/decode roundtrip") + assert_true(!str_eq(encoded, s), "encoded differs from original") + assert_true(str_len(encoded) >= str_len(s), "encoded is at least as long") + return "" +} + +// ── bool_to_str ─────────────────────────────────────────────────────────────── + +fn test_bool_to_str_basic(_: String) -> String { + assert_eq(bool_to_str(true), "true", "true becomes 'true'") + assert_eq(bool_to_str(false), "false", "false becomes 'false'") + assert_true(str_eq(bool_to_str(1 == 1), "true"), "expression true") + assert_true(str_eq(bool_to_str(1 == 2), "false"), "expression false") + assert_int_eq(str_len(bool_to_str(true)), 4, "true has length 4") + return "" +} + +// ── str_to_bytes / bytes_to_str ─────────────────────────────────────────────── + +fn test_str_to_bytes_basic(_: String) -> String { + let b: String = str_to_bytes("hi") + assert_true(str_contains(b, "104"), "h has code 104") + assert_true(str_contains(b, "105"), "i has code 105") + assert_eq(str_to_bytes(""), "[]", "empty string yields empty array") + assert_true(str_starts_with(b, "["), "bytes is JSON array") + assert_true(str_ends_with(b, "]"), "bytes is JSON array") + return "" +} + +fn test_bytes_roundtrip(_: String) -> String { + let s: String = "hello" + let b: String = str_to_bytes(s) + let s2: String = bytes_to_str(b) + assert_eq(s2, s, "bytes roundtrip: hello") + + let s3: String = "ABC" + let b3: String = str_to_bytes(s3) + let s4: String = bytes_to_str(b3) + assert_eq(s4, s3, "bytes roundtrip: ABC") + + assert_eq(bytes_to_str("[]"), "", "empty bytes yields empty string") + return "" +} diff --git a/tests/suite/src/test_time.el b/tests/suite/src/test_time.el new file mode 100644 index 0000000..060c946 --- /dev/null +++ b/tests/suite/src/test_time.el @@ -0,0 +1,279 @@ +// tests/suite/test_time.el — comprehensive tests for runtime/time.el +// +// Covers time_now, now_millis, unix_timestamp, now_ns, time_format, time_to_parts, +// time_add, time_diff, duration helpers, and uuid_new. + +// ── time_now / now_millis / unix_timestamp_ms ───────────────────────────────── + +fn test_time_now_basic(_: String) -> String { + let t: Int = time_now() + // Milliseconds since epoch: must be well past year 2020 (1577836800000) + assert_true(t > 1577836800000, "time_now is past 2020-01-01") + // Must be before year 2100 (4102444800000) + assert_true(t < 4102444800000, "time_now is before 2100-01-01") + return "" +} + +fn test_now_millis_basic(_: String) -> String { + let t: Int = now_millis() + assert_true(t > 1577836800000, "now_millis is past 2020-01-01") + assert_true(t < 4102444800000, "now_millis is before 2100-01-01") + return "" +} + +fn test_unix_timestamp_ms_basic(_: String) -> String { + let t: Int = unix_timestamp_ms() + assert_true(t > 1577836800000, "unix_timestamp_ms is past 2020-01-01") + assert_true(t < 4102444800000, "unix_timestamp_ms is before 2100-01-01") + return "" +} + +fn test_time_now_ms_alias(_: String) -> String { + let t: Int = time_now_ms() + assert_true(t > 1577836800000, "time_now_ms is past 2020-01-01") + assert_true(t < 4102444800000, "time_now_ms is before 2100-01-01") + return "" +} + +// ── monotonicity ────────────────────────────────────────────────────────────── + +fn test_time_monotonic_now_millis(_: String) -> String { + let t1: Int = now_millis() + let t2: Int = now_millis() + assert_true(t2 >= t1, "second call >= first call (monotonic)") + return "" +} + +fn test_time_monotonic_time_now(_: String) -> String { + let t1: Int = time_now() + let t2: Int = time_now() + assert_true(t2 >= t1, "time_now is monotonic across calls") + return "" +} + +fn test_time_monotonic_now_ns(_: String) -> String { + let t1: Int = now_ns() + let t2: Int = now_ns() + assert_true(t2 >= t1, "now_ns is monotonic across calls") + // ns should be much larger than ms (9 digits difference) + assert_true(t1 > 1000000000000000000, "now_ns is in nanosecond scale") + return "" +} + +// ── unix_timestamp (seconds) ───────────────────────────────────────────────── + +fn test_unix_timestamp_basic(_: String) -> String { + let secs: Int = unix_timestamp() + // Must be past 2020-01-01 in seconds (1577836800) + assert_true(secs > 1577836800, "unix_timestamp is past 2020-01-01") + // Must be before 2100-01-01 in seconds (4102444800) + assert_true(secs < 4102444800, "unix_timestamp is before 2100-01-01") + // seconds are about 1000x smaller than millis + let ms: Int = time_now() + assert_true(ms > secs, "milliseconds > seconds") + return "" +} + +// ── time_to_parts ───────────────────────────────────────────────────────────── + +fn test_time_to_parts_basic(_: String) -> String { + // Known timestamp: 2024-01-15 12:30:45.123 UTC + // 2024-01-15 00:00:00 UTC = epoch + (54 * 365 + 14 leaps) days approximately + // Use a precisely known value: 2024-01-15T12:30:45.123Z + // epoch_ms = 1705320645123 + let ts: Int = 1705320645123 + let parts: String = time_to_parts(ts) + assert_eq(json_get(parts, "year"), "2024", "year is 2024") + assert_eq(json_get(parts, "month"), "1", "month is 1 (January)") + assert_eq(json_get(parts, "day"), "15", "day is 15") + assert_eq(json_get(parts, "hour"), "12", "hour is 12") + assert_eq(json_get(parts, "minute"), "30", "minute is 30") + assert_eq(json_get(parts, "second"), "45", "second is 45") + assert_eq(json_get(parts, "ms"), "123", "milliseconds is 123") + return "" +} + +fn test_time_to_parts_epoch(_: String) -> String { + // Unix epoch: 1970-01-01T00:00:00.000Z = timestamp 0 + let parts: String = time_to_parts(0) + assert_eq(json_get(parts, "year"), "1970", "epoch year is 1970") + assert_eq(json_get(parts, "month"), "1", "epoch month is 1") + assert_eq(json_get(parts, "day"), "1", "epoch day is 1") + assert_eq(json_get(parts, "hour"), "0", "epoch hour is 0") + assert_eq(json_get(parts, "minute"), "0", "epoch minute is 0") + assert_eq(json_get(parts, "second"), "0", "epoch second is 0") + return "" +} + +fn test_time_to_parts_current(_: String) -> String { + let ts: Int = time_now() + let parts: String = time_to_parts(ts) + let year: Int = str_to_int(json_get(parts, "year")) + let month: Int = str_to_int(json_get(parts, "month")) + let day: Int = str_to_int(json_get(parts, "day")) + let hour: Int = str_to_int(json_get(parts, "hour")) + let minute: Int = str_to_int(json_get(parts, "minute")) + let second: Int = str_to_int(json_get(parts, "second")) + assert_true(year >= 2024, "current year >= 2024") + assert_true(year < 2100, "current year < 2100") + assert_true(month >= 1, "month >= 1") + assert_true(month <= 12, "month <= 12") + assert_true(day >= 1, "day >= 1") + assert_true(day <= 31, "day <= 31") + assert_true(hour >= 0, "hour >= 0") + assert_true(hour <= 23, "hour <= 23") + assert_true(minute >= 0, "minute >= 0") + assert_true(minute <= 59, "minute <= 59") + assert_true(second >= 0, "second >= 0") + assert_true(second <= 59, "second <= 59") + return "" +} + +// ── time_format ─────────────────────────────────────────────────────────────── + +fn test_time_format_iso(_: String) -> String { + let ts: Int = 1705320645123 + let iso: String = time_format(ts, "ISO") + assert_starts_with(iso, "2024-01-15", "ISO format starts with correct date") + assert_contains(iso, "T12:30:45", "ISO format has correct time") + assert_ends_with(iso, "Z", "ISO format ends with Z") + assert_contains(iso, "123", "ISO format includes milliseconds") + return "" +} + +fn test_time_format_empty(_: String) -> String { + let ts: Int = 1705320645123 + let iso: String = time_format(ts, "") + assert_starts_with(iso, "2024-01-15", "empty fmt gives ISO format") + assert_ends_with(iso, "Z", "empty fmt gives ISO format ending in Z") + return "" +} + +fn test_time_format_strftime(_: String) -> String { + let ts: Int = 1705320645123 + let formatted: String = time_format(ts, "%Y-%m-%d") + assert_eq(formatted, "2024-01-15", "strftime %Y-%m-%d") + let formatted2: String = time_format(ts, "%H:%M:%S") + assert_eq(formatted2, "12:30:45", "strftime %H:%M:%S") + return "" +} + +// ── time_add ────────────────────────────────────────────────────────────────── + +fn test_time_add_basic(_: String) -> String { + let base: Int = 1000000000000 + assert_int_eq(time_add(base, 1000, "ms"), base + 1000, "add milliseconds") + assert_int_eq(time_add(base, 1, "sec"), base + 1000, "add 1 second") + assert_int_eq(time_add(base, 1, "min"), base + 60000, "add 1 minute") + assert_int_eq(time_add(base, 1, "hour"), base + 3600000, "add 1 hour") + assert_int_eq(time_add(base, 1, "day"), base + 86400000, "add 1 day") + return "" +} + +fn test_time_add_multiple(_: String) -> String { + let base: Int = 1000000000000 + assert_int_eq(time_add(base, 60, "sec"), base + 60000, "add 60 seconds") + assert_int_eq(time_add(base, 24, "hour"), base + 86400000, "add 24 hours = 1 day") + assert_int_eq(time_add(base, 0, "sec"), base, "add 0 seconds") + assert_int_eq(time_add(base, -1, "sec"), base - 1000, "add negative seconds") + return "" +} + +// ── time_diff ───────────────────────────────────────────────────────────────── + +fn test_time_diff_basic(_: String) -> String { + let t1: Int = 1000000000000 + let t2: Int = t1 + 5000 + assert_int_eq(time_diff(t1, t2, "ms"), 5000, "diff in ms") + assert_int_eq(time_diff(t1, t2, "sec"), 5, "diff in seconds") + return "" +} + +fn test_time_diff_larger(_: String) -> String { + let t1: Int = 1000000000000 + let t2: Int = t1 + 3600000 + assert_int_eq(time_diff(t1, t2, "min"), 60, "diff of 1 hour in minutes") + assert_int_eq(time_diff(t1, t2, "hour"), 1, "diff of 1 hour in hours") + assert_int_eq(time_diff(t1, t2, "ms"), 3600000, "diff of 1 hour in ms") + return "" +} + +fn test_time_diff_zero(_: String) -> String { + let t: Int = 1000000000000 + assert_int_eq(time_diff(t, t, "ms"), 0, "same timestamp: diff is 0") + assert_int_eq(time_diff(t, t, "sec"), 0, "same timestamp: diff in sec is 0") + return "" +} + +// ── Duration helpers ────────────────────────────────────────────────────────── + +fn test_duration_helpers_basic(_: String) -> String { + assert_int_eq(duration_seconds(1), 1000000000, "1 second = 1e9 ns") + assert_int_eq(duration_millis(1), 1000000, "1 ms = 1e6 ns") + assert_int_eq(duration_nanos(42), 42, "nanos identity") + assert_int_eq(duration_to_seconds(1000000000), 1, "1e9 ns = 1 second") + assert_int_eq(duration_to_millis(1000000), 1, "1e6 ns = 1 ms") + assert_int_eq(duration_to_nanos(42), 42, "nanos identity roundtrip") + return "" +} + +fn test_instant_helpers_basic(_: String) -> String { + let i: Int = unix_seconds(1000) + assert_int_eq(i, 1000000000000, "unix_seconds(1000) = 1e12 ns") + + let ms_i: Int = unix_millis(5000) + assert_int_eq(ms_i, 5000000000, "unix_millis(5000) = 5e9 ns") + + assert_int_eq(instant_to_unix_seconds(1000000000000), 1000, "instant to seconds") + assert_int_eq(instant_to_unix_millis(5000000000), 5000, "instant to millis") + return "" +} + +// ── uuid_new / uuid_v4 ──────────────────────────────────────────────────────── + +fn test_uuid_new_basic(_: String) -> String { + let u: String = uuid_new() + assert_int_eq(str_len(u), 36, "UUID has 36 characters") + assert_eq(str_char_at(u, 8), "-", "UUID has dash at position 8") + assert_eq(str_char_at(u, 13), "-", "UUID has dash at position 13") + assert_eq(str_char_at(u, 18), "-", "UUID has dash at position 18") + assert_eq(str_char_at(u, 23), "-", "UUID has dash at position 23") + return "" +} + +fn test_uuid_uniqueness(_: String) -> String { + let u1: String = uuid_new() + let u2: String = uuid_new() + let u3: String = uuid_v4() + assert_false(str_eq(u1, u2), "consecutive UUIDs are different") + assert_false(str_eq(u1, u3), "uuid_new and uuid_v4 produce different values") + assert_false(str_eq(u2, u3), "three consecutive UUIDs are all different") + assert_int_eq(str_len(u3), 36, "uuid_v4 also produces 36-char UUID") + return "" +} + +// ── time_from_parts ─────────────────────────────────────────────────────────── + +fn test_time_from_parts_basic(_: String) -> String { + // time_from_parts(secs, ns, tz) = secs * 1000 + ns / 1000000 + let ts: Int = time_from_parts(1000, 0, "UTC") + assert_int_eq(ts, 1000000, "1000 secs = 1000000 ms") + + let ts2: Int = time_from_parts(0, 500000000, "UTC") + assert_int_eq(ts2, 500, "500ms in nanoseconds -> 500ms") + + let ts3: Int = time_from_parts(1705320645, 123000000, "UTC") + assert_int_eq(ts3, 1705320645123, "known timestamp with ms component") + return "" +} + +// ── instant_to_iso8601 ──────────────────────────────────────────────────────── + +fn test_instant_to_iso8601_basic(_: String) -> String { + let ts_ms: Int = 1705320645123 + let ts_ns: Int = ts_ms * 1000000 + let iso: String = instant_to_iso8601(ts_ns) + assert_starts_with(iso, "2024-01-15", "instant_to_iso8601 correct date") + assert_ends_with(iso, "Z", "instant_to_iso8601 ends with Z") + return "" +}