uce/site/tests/core.uce
2026-06-12 19:55:35 +00:00

258 lines
18 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "testlib.h"
RENDER(Request& context)
{
u64 passed = 0;
u64 failed = 0;
u64 skipped = 0;
auto check = [&](String name, bool ok, String detail)
{
site_tests_case(name, ok ? "pass" : "fail", detail);
if(ok)
passed++;
else
failed++;
};
site_tests_page_start("Core APIs", "Pure helper coverage for strings, UTF-8 splitting, DValue, and JSON encoding/decoding.");
auto comma_parts = split("alpha,beta,gamma", ",");
auto empty_delim_parts = split("alpha", "");
check("split()", comma_parts.size() == 3 && comma_parts[1] == "beta" && empty_delim_parts.size() == 1 && empty_delim_parts[0] == "alpha", join(comma_parts, " | "));
auto spaced_parts = split_space(" one two three ");
check("split_space()", spaced_parts.size() == 3 && spaced_parts[2] == "three", join(spaced_parts, " / "));
StringMap kv = split_kv("\n#comment\nalpha = one\nempty=\n", '=', true, false);
StringMap http_headers = split_http_headers("GET /demo.uce?x=1 HTTP/1.1\r\nHost: example.test\r\nX-Empty:\r\n");
StringMap leading_crlf_headers = split_http_headers("\r\nGET /lead.uce HTTP/1.1\r\nHost: lead.example\r\n");
StringMap header_only = split_http_headers("Host: example.test\nX-Token: abc\n");
StringMap colon_uri_headers = split_http_headers("GET /clock.uce?t=12:30 HTTP/1.1\r\nHost: colon.example\r\n");
check("split_http_headers() request line with colon in URI", colon_uri_headers["REQUEST_METHOD"] == "GET" && colon_uri_headers["DOCUMENT_URI"] == "/clock.uce" && colon_uri_headers["QUERY_STRING"] == "t=12:30" && colon_uri_headers["HTTP_HOST"] == "colon.example", var_dump(colon_uri_headers));
check("trim() / split_kv() / split_http_headers()", trim(" padded value ") == "padded value" && kv["alpha"] == "one" && kv["empty"] == "" && http_headers["REQUEST_METHOD"] == "GET" && http_headers["DOCUMENT_URI"] == "/demo.uce" && http_headers["QUERY_STRING"] == "x=1" && http_headers["HTTP_X_EMPTY"] == "" && leading_crlf_headers["REQUEST_METHOD"] == "GET" && leading_crlf_headers["DOCUMENT_URI"] == "/lead.uce" && leading_crlf_headers["HTTP_HOST"] == "lead.example" && header_only["REQUEST_METHOD"] == "" && header_only["HTTP_HOST"] == "example.test" && header_only["HTTP_X_TOKEN"] == "abc", trim(" padded value ") + " / " + var_dump(kv) + " / " + var_dump(http_headers) + " / " + var_dump(leading_crlf_headers) + " / " + var_dump(header_only));
check("replace()", replace("alpha-beta-beta", "beta", "done") == "alpha-done-done", replace("alpha-beta-beta", "beta", "done"));
check("html_escape() attribute-safe quotes", html_escape("<&>\"Don't") == "&lt;&amp;&gt;&quot;Don&#39;t", html_escape("<&>\"Don't"));
check("regex_match()", regex_match("[A-Z][a-z]+", "Alice") && !regex_match("[A-Z][a-z]+", "Alice!"), "full-string validation");
DValue regex_email = regex_search("(?<user>[A-Za-z0-9._%+-]+)@(?<host>[A-Za-z0-9.-]+)", "Contact ops@example.test");
check("regex_search()", regex_email["matched"].to_bool() && regex_email["named"]["user"].to_string() == "ops" && regex_email["named"]["host"].to_string() == "example.test", json_encode(regex_email));
DValue regex_tags = regex_search_all("#(?<tag>[A-Za-z0-9_]+)", "#uce #docs");
check("regex_search_all()", regex_tags["count"].to_s64() == 2 && regex_tags["matches"]["1"]["named"]["tag"].to_string() == "docs", json_encode(regex_tags));
check("regex_replace()", regex_replace("#([A-Za-z0-9_]+)", "<tag>$1</tag>", "#uce") == "<tag>uce</tag>", regex_replace("#([A-Za-z0-9_]+)", "<tag>$1</tag>", "#uce"));
auto regex_parts = regex_split("\\s*,\\s*", "uce, components, markdown");
check("regex_split()", regex_parts.size() == 3 && regex_parts[1] == "components", join(regex_parts, " | "));
check("substr() + strpos()", strpos("component suite", "suite") == 10 && substr("component suite", 10) == "suite", "strpos=10 substr='" + substr("component suite", 10) + "'");
check("str_starts_with()", str_starts_with("websocket-suite", "websocket"), "websocket-suite starts with websocket");
check("str_ends_with()", str_ends_with("component.uce", ".uce"), "component.uce ends with .uce");
check("to_lower() / to_upper()", to_lower("MiXeD") == "mixed" && to_upper("MiXeD") == "MIXED", to_lower("MiXeD") + " / " + to_upper("MiXeD"));
check("request context params", context.params["SCRIPT_URL"] != "" && context.params["BASE_URL"] != "" && context.params["ROUTE_PATH"] != "" && context.params["ROUTE_PAGE"] != "" && context.params["ROUTE_VALID"] == "1", "script=" + context.params["SCRIPT_URL"] + " base=" + context.params["BASE_URL"] + " route=" + context.params["ROUTE_PATH"] + " page=" + context.params["ROUTE_PAGE"] + " valid=" + context.params["ROUTE_VALID"]);
String saved_query_string = context.params["QUERY_STRING"];
context.params["QUERY_STRING"] = "workspace/projects&theme=dark";
check("request_query_path() delegates to request_query_route()", request_query_path(context) == request_query_route(context)["l_path"].to_string() && request_query_path(context) == "workspace/projects", request_query_path(context) + " / " + json_encode(request_query_route(context)));
context.params["QUERY_STRING"] = saved_query_string;
check("route path sanitizers", route_path_normalize("/workspace/projects/") == "workspace/projects" && route_path_is_safe("workspace/projects") && !route_path_is_safe("../demo") && !route_path_is_safe("workspace/../demo") && !route_path_is_safe("workspace/file.uce") && route_path_sanitize("../demo") == "" && route_path_sanitize("") == "index", route_path_sanitize("/workspace/projects/"));
StringList route_parts = {"dashboard", "index", "dashboard", "themes"};
auto unique_routes = list_unique(route_parts);
auto sorted_routes = list_sort(unique_routes);
auto upper_routes = map(sorted_routes, [](String item) { return(to_upper(item)); });
auto dashboard_routes = filter(route_parts, [](String item) { return(item == "dashboard"); });
check("map/filter/list_unique/sort/find/some/every", unique_routes.size() == 3 && sorted_routes[0] == "dashboard" && upper_routes[2] == "THEMES" && dashboard_routes.size() == 2 && list_find(route_parts, [](String item) { return(str_starts_with(item, "them")); }, "missing") == "themes" && list_some(route_parts, [](String item) { return(item == "index"); }) && list_every(unique_routes, [](String item) { return(item != ""); }), join(upper_routes, ","));
DValue nav;
DValue nav_home;
nav_home["title"] = "Home";
nav_home["section"] = "main";
nav.push(nav_home);
DValue nav_dash;
nav_dash["title"] = "Dashboard";
nav_dash["section"] = "app";
nav.push(nav_dash);
DValue nav_themes;
nav_themes["title"] = "Themes";
nav_themes["section"] = "app";
nav.push(nav_themes);
DValue empty_tree;
DValue empty_pop = empty_tree.pop();
DValue app_nav = dv_filter(nav, [](DValue item, String key) { return(item["section"].to_string() == "app"); });
DValue nav_titles = dv_map(app_nav, [](DValue item, String key) { DValue title; title = item["title"].to_string(); return(title); });
DValue grouped_nav = dv_group_by(nav, [](DValue item, String key) { return(item["section"].to_string()); });
DValue nav_dash_summary = dv_pick(nav_dash, {"title"});
DValue nav_dash_public = dv_omit(nav_dash, {"section"});
check("DValue collection helpers", empty_pop.to_string() == "" && app_nav.is_list() && nav_titles["0"].to_string() == "Dashboard" && grouped_nav["app"]["1"]["title"].to_string() == "Themes" && join(dv_keys(nav_dash_summary), ",") == "title" && dv_values(nav_dash_public)["0"].to_string() == "Dashboard", json_encode(grouped_nav));
DValue conv;
conv["name"] = "ada";
conv["count"] = "12";
conv["junk"] = "not-a-number";
conv["flag"] = "off";
check("DValue to_* defaults", conv.get_by_path("missing/key").to_string("fallback") == "fallback" && conv["name"].to_string("fallback") == "ada" && conv["junk"].to_s64(-7) == -7 && conv["count"].to_s64(-7) == 12 && conv["junk"].to_f64(2.5) == 2.5 && conv["junk"].to_u64(9) == 9 && conv.get_by_path("nope").to_bool(true) && !conv["flag"].to_bool(true), json_encode(conv));
const DValue& conv_read = conv;
String const_each_keys = "";
conv_read.each([&](const DValue& item, String key) { const_each_keys += key + ":" + item.to_string("-") + " "; });
check("DValue const read accessors", conv_read.has("name") && conv_read.get_by_path("name").to_string() == "ada" && conv_read.is_array() && !conv_read.is_list() && conv_read.to_stringmap()["count"] == "12" && conv_read.get_type_name() == "array" && contains(const_each_keys, "count:12"), const_each_keys);
DValue ordered;
for(u32 i = 0; i < 12; i++)
{
DValue ordered_entry;
ordered_entry = "v" + std::to_string(i);
ordered.push(ordered_entry);
}
String ordered_keys = "";
ordered.each([&](const DValue& item, String key) { ordered_keys += key + ","; });
check("DValue list iteration is numeric", ordered_keys == "0,1,2,3,4,5,6,7,8,9,10,11," && dv_values(ordered)["10"].to_string() == "v10" && dv_map(ordered, [](const DValue& item, String key) { return(item); })["11"].to_string() == "v11", ordered_keys);
DValue uceb_source;
uceb_source["name"] = "phase1";
uceb_source["nested"]["answer"] = "42";
uceb_source["float"] = (f64)0.0000001;
uceb_source["bool"].set_bool(true);
uceb_source["empty_list"].set_array();
DValue uceb_item;
uceb_item = "first";
uceb_source["items"].push(uceb_item);
uceb_item = "second";
uceb_source["items"].push(uceb_item);
String uceb_binary = "bin";
uceb_binary.push_back((char)0x00);
uceb_binary += "ary";
uceb_source["binary"] = uceb_binary;
uceb_source["pointer"] = (void*)0x1234;
String uceb_encoded = ucb_encode(uceb_source);
String uceb_error = "";
DValue uceb_decoded;
bool uceb_ok = ucb_decode(uceb_encoded, uceb_decoded, &uceb_error);
check("UCEB1 DValue codec round-trip", uceb_ok && uceb_decoded["name"].to_string() == "phase1" && uceb_decoded["nested"]["answer"].to_string() == "42" && uceb_decoded["float"].to_f64() > 0.00000009 && uceb_decoded["float"].to_f64() < 0.00000011 && uceb_decoded["bool"].to_bool() && uceb_decoded["items"].is_list() && uceb_decoded["items"]["1"].to_string() == "second" && uceb_decoded["empty_list"].is_list() && uceb_decoded["binary"].to_string().size() == uceb_binary.size() && uceb_decoded["binary"].to_string() == uceb_binary && uceb_decoded["pointer"].to_string() == "", "bytes=" + std::to_string((u64)uceb_encoded.size()) + " error=" + uceb_error + " float=" + uceb_decoded["float"].to_string());
DValue uceb_invalid;
String uceb_bad_magic = "NOPE";
uceb_bad_magic.push_back((char)1);
String uceb_wrong_version = "UCEB";
uceb_wrong_version.push_back((char)2);
String uceb_truncated = "UCEB";
uceb_truncated.push_back((char)1);
uceb_truncated.push_back((char)0);
String uceb_depth_bomb = "UCEB";
uceb_depth_bomb.push_back((char)1);
for(u32 i = 0; i < 1030; i++)
{
uceb_depth_bomb.push_back((char)0); // flags
uceb_depth_bomb.push_back((char)0); // scalar length
uceb_depth_bomb.push_back((char)1); // child count
uceb_depth_bomb.push_back((char)1); // key length
uceb_depth_bomb += "x";
}
uceb_depth_bomb.push_back((char)0);
uceb_depth_bomb.push_back((char)0);
uceb_depth_bomb.push_back((char)0);
String uceb_negative_error = "";
bool uceb_bad_magic_ok = ucb_decode(uceb_bad_magic, uceb_invalid, &uceb_negative_error);
String uceb_magic_error = uceb_negative_error;
bool uceb_wrong_version_ok = ucb_decode(uceb_wrong_version, uceb_invalid, &uceb_negative_error);
String uceb_version_error = uceb_negative_error;
bool uceb_truncated_ok = ucb_decode(uceb_truncated, uceb_invalid, &uceb_negative_error);
String uceb_truncated_error = uceb_negative_error;
String uceb_trailing = uceb_encoded + "x";
bool uceb_trailing_ok = ucb_decode(uceb_trailing, uceb_invalid, &uceb_negative_error);
String uceb_trailing_error = uceb_negative_error;
bool uceb_depth_ok = ucb_decode(uceb_depth_bomb, uceb_invalid, &uceb_negative_error);
check("UCEB1 rejects invalid input", !uceb_bad_magic_ok && contains(uceb_magic_error, "magic") && !uceb_wrong_version_ok && contains(uceb_version_error, "version") && !uceb_truncated_ok && contains(uceb_truncated_error, "length") && !uceb_trailing_ok && contains(uceb_trailing_error, "trailing") && !uceb_depth_ok && contains(uceb_negative_error, "nesting"), uceb_magic_error + " / " + uceb_version_error + " / " + uceb_truncated_error + " / " + uceb_trailing_error + " / " + uceb_negative_error);
size_t abi_len = 0;
uce_dvalue* abi_root = reinterpret_cast<uce_dvalue*>(&uceb_source);
uce_dvalue* abi_nested = uce_dv_find(abi_root, "nested", 6);
uce_dvalue* abi_answer = uce_dv_get(abi_nested, "answer", 6);
const char* abi_answer_value = uce_dv_value(abi_answer, &abi_len);
uce_dv_iter abi_iter = uce_dv_iter_begin(uce_dv_find(abi_root, "items", 5));
const char* abi_key = 0;
size_t abi_key_len = 0;
uce_dvalue* abi_child = 0;
bool abi_iter_first = uce_dv_iter_next(uce_dv_find(abi_root, "items", 5), &abi_iter, &abi_key, &abi_key_len, &abi_child) == 1;
size_t abi_encoded_len = uce_dv_encode(abi_root, 0, 0);
String abi_encoded;
abi_encoded.resize(abi_encoded_len);
uce_dv_encode(abi_root, &abi_encoded[0], abi_encoded.size());
uce_dvalue* abi_decoded = uce_dv_decode(abi_encoded.data(), abi_encoded.size());
check("DValue C ABI accessors", abi_answer_value != 0 && String(abi_answer_value, abi_len) == "42" && uce_dv_count(abi_nested) == 1 && uce_dv_is_list(uce_dv_find(abi_root, "items", 5)) == 1 && abi_iter_first && String(abi_key, abi_key_len) == "0" && uce_dv_value(abi_child, &abi_len) != 0 && uce_dv_count(abi_root) == 8 && abi_decoded != 0 && uce_dv_find(abi_decoded, "nested", 6) != 0, "encoded=" + std::to_string((u64)abi_encoded_len) + " last_error=" + String(uce_dv_last_error()));
String binary_payload = "core";
binary_payload.push_back((char)0x00);
binary_payload.push_back((char)0xff);
binary_payload += "payload";
bool base64_ok = false;
String encoded_base64 = base64_encode(binary_payload);
String decoded_base64 = base64_decode(encoded_base64, base64_ok);
bool invalid_base64_ok = true;
base64_decode("AA=A", invalid_base64_ok);
check("base64_encode() / base64_decode() binary-safe", base64_ok && decoded_base64 == binary_payload && decoded_base64.size() == binary_payload.size() && !invalid_base64_ok, encoded_base64 + " bytes=" + std::to_string((u64)decoded_base64.size()));
String utf8_sample = "A\xC3\xA9";
auto utf8_parts = split_utf8(utf8_sample);
check("split_utf8()", utf8_parts.size() == 2, "count=" + std::to_string(utf8_parts.size()));
DValue payload;
payload["name"] = "uce";
payload["count"] = (f64)3;
payload["kind"] = "core";
String payload_json = json_encode(payload);
DValue decoded = json_decode(payload_json);
check("json_encode() / json_decode()", decoded["name"].to_string() == "uce" && int_val(decoded["count"].to_string()) == 3, payload_json);
DValue xml_doc;
xml_doc["name"] = "book";
xml_doc["attrs"]["id"] = "b1";
DValue xml_title;
xml_title["name"] = "title";
xml_title["text"] = "UCE & XML";
xml_doc["children"].push(xml_title);
String encoded_xml = xml_encode(xml_doc);
DValue decoded_xml = xml_decode(encoded_xml);
check("xml_encode() / xml_decode()", contains(encoded_xml, "id=\"b1\"") && contains(encoded_xml, "UCE &amp; XML") && decoded_xml["children"]["0"]["text"].to_string() == "UCE & XML", encoded_xml);
DValue xml_payload;
xml_payload["title"] = "Hello";
xml_payload["count"] = "3";
check("xml_encode() simple map", xml_encode(xml_payload, "payload") == "<payload><count>3</count><title>Hello</title></payload>", xml_encode(xml_payload, "payload"));
check("xml_decode() numeric entities", xml_decode("<x>&#x41;&#66;</x>")["text"].to_string() == "AB", xml_decode("<x>&#x41;&#66;</x>")["text"].to_string());
String yaml_config = "app:\n name: UCE Starter\n debug: true\n port: 8080\n paths:\n - site\n - cache\nmessage: |\n hello\n world\n";
DValue decoded_yaml = yaml_decode(yaml_config);
check("yaml_decode() config", decoded_yaml["app"]["name"].to_string() == "UCE Starter" && decoded_yaml["app"]["debug"].to_bool() && decoded_yaml["app"]["port"].to_s64() == 8080 && decoded_yaml["app"]["paths"]["1"].to_string() == "cache" && decoded_yaml["message"].to_string() == "hello\nworld", json_encode(decoded_yaml));
String encoded_yaml = yaml_encode(decoded_yaml);
DValue yaml_roundtrip = yaml_decode(encoded_yaml);
check("yaml_encode() / yaml_decode()", contains(encoded_yaml, "app:") && contains(encoded_yaml, "paths:") && yaml_roundtrip["app"]["debug"].to_bool() && yaml_roundtrip["message"].to_string() == "hello\nworld", encoded_yaml);
DValue tree;
tree["suite"] = "core";
tree["nested"]["api"] = "dvalue";
tree["nested"]["ok"].set_bool(true);
check("DValue map access", tree["suite"].to_string() == "core" && tree["nested"]["api"].to_string() == "dvalue", json_encode(tree));
// wasm_trace.h (WASM-PROPOSAL Phase 4): canned Wasmtime-format messages —
// the live trap path is gated by the phase-4 spike runner
String trace_msg = "error while executing at wasm backtrace:\n 0: 0x9f - units!_Z6renderR7Request\n";
for(int trace_i = 1; trace_i <= 40; trace_i++)
trace_msg += " " + std::to_string(trace_i) + ": 0x20 - units!_Z9recursionv\n";
trace_msg += "\nCaused by:\n wasm trap: call stack exhausted\n";
WasmTraceSummary trace_summary = wasm_trace_summarize(trace_msg);
String trace_text = wasm_trace_format(trace_summary);
check("wasm_trace_summarize() collapse + demangle", trace_summary.parsed && trace_summary.total_frames == 41 && trace_summary.cause == "wasm trap: call stack exhausted" && trace_summary.frames.size() == 2 && contains(trace_text, "×40") && contains(trace_text, "recursion()"), trace_text);
String fault_msg = "error while executing at wasm backtrace:\n 0: 0x2a - <unknown>!<wasm function 0>\n\nCaused by:\n 0: memory fault at wasm address 0x20000 in linear memory of size 0x10000\n 1: wasm trap: out of bounds memory access\n";
WasmTraceSummary fault_summary = wasm_trace_summarize(fault_msg);
check("wasm_trace_summarize() cause/detail split", fault_summary.cause == "wasm trap: out of bounds memory access" && contains(fault_summary.detail, "memory fault at wasm address 0x20000") && fault_summary.total_frames == 1, wasm_trace_format(fault_summary));
check("wasm_trace_collapse() raw passthrough", wasm_trace_collapse("plain host error") == "plain host error", wasm_trace_collapse("plain host error"));
site_tests_summary(passed, failed, skipped, "These assertions intentionally stay pure and side-effect free so they remain safe on the public site.");
site_tests_page_end();
}