uce/site/tests/cli_runner.uce

462 lines
19 KiB
Plaintext

struct CliHttpResponse
{
u64 status = 0;
String raw;
String headers;
String body;
};
u64 cli_tests_passed = 0;
u64 cli_tests_failed = 0;
u64 cli_tests_skipped = 0;
StringList cli_tests_failures;
bool cli_contains(String haystack, String needle)
{
return(haystack.find(needle) != String::npos);
}
String cli_truncate(String value, u64 max_len = 240)
{
if(value.length() <= max_len)
return(value);
return(value.substr(0, max_len) + "...");
}
CliHttpResponse cli_http_request(String host, u16 port, String path, String extra_headers = "")
{
CliHttpResponse res;
u64 fd = socket_connect(host, port);
if(fd == 0)
{
res.raw = "socket_connect failed";
return(res);
}
String host_header = host;
if(host == "127.0.0.1" && port == 80)
host_header = "uce.openfu.com";
else if(host == "127.0.0.1" && port == 8080)
host_header = "localhost";
String request = "GET " + path + " HTTP/1.0\r\nHost: " + host_header + "\r\nConnection: close\r\n" + extra_headers + "\r\n";
socket_write(fd, request);
for(u64 i = 0; i < 16; i++)
{
String chunk = socket_read(fd, 65536, 30);
if(chunk == "")
break;
res.raw += chunk;
}
socket_close(fd);
u64 line_end = res.raw.find("\r\n");
if(line_end != String::npos)
{
StringList parts = split(res.raw.substr(0, line_end), " ");
if(parts.size() >= 2)
res.status = int_val(parts[1]);
}
u64 split_at = res.raw.find("\r\n\r\n");
if(split_at != String::npos)
{
res.headers = res.raw.substr(0, split_at + 4);
res.body = res.raw.substr(split_at + 4);
}
else
{
res.headers = res.raw;
res.body = "";
}
return(res);
}
CliHttpResponse cli_frontend(String path, String extra_headers = "")
{
return(cli_http_request("127.0.0.1", 80, path, extra_headers));
}
CliHttpResponse cli_direct_http(String path, String extra_headers = "")
{
return(cli_http_request("127.0.0.1", 8080, path, extra_headers));
}
void cli_test_case(String name, bool ok, String summary)
{
if(ok)
{
cli_tests_passed++;
print("[PASS] ", name, " - ", summary, "\n");
}
else
{
cli_tests_failed++;
cli_tests_failures.push_back(name + ": " + summary);
print("[FAIL] ", name, " - ", summary, "\n");
}
}
void cli_test_skip(String name, String summary)
{
cli_tests_skipped++;
print("[SKIP] ", name, " - ", summary, "\n");
}
bool cli_expect_http_once(CliHttpResponse& res, String path, u64 expected_status, String marker, StringList error_markers, String& summary)
{
bool ok = res.status == expected_status;
summary = "HTTP " + std::to_string(res.status) + " for " + path;
if(marker != "" && !cli_contains(res.body, marker))
{
ok = false;
summary += "; missing marker " + marker;
}
String lower = to_lower(res.body);
for(String marker_text : error_markers)
{
if(cli_contains(lower, marker_text))
{
ok = false;
summary += "; body contained " + marker_text;
}
}
return(ok);
}
bool cli_expect_http(String name, String path, u64 expected_status, String marker, StringList error_markers = {})
{
CliHttpResponse res = cli_frontend(path);
String summary;
bool ok = cli_expect_http_once(res, path, expected_status, marker, error_markers, summary);
if(!ok && expected_status != 500)
{
usleep(500000);
res = cli_frontend(path);
ok = cli_expect_http_once(res, path, expected_status, marker, error_markers, summary);
}
cli_test_case(name, ok, ok ? summary : summary + "; body=" + cli_truncate(res.body));
return(ok);
}
StringList cli_demo_error_markers()
{
StringList markers;
markers.push_back(String("uce compile ") + "error");
markers.push_back(String("fatal signal during ") + "request");
markers.push_back(String("uncaught exception during ") + "request");
return(markers);
}
void cli_run_demo_smoke()
{
StringList files = split("call_file.uce\ncall_file_funcs.uce\ncollections.uce\ncomponents.uce\ncookie.uce\ndvalue.uce\nempty.uce\nerror-reporting.uce\nfile_append.uce\nfileio.uce\nheader.uce\nhello.uce\nindex.uce\njson.uce\nmarkdown.uce\nmemcached.uce\nmysql.uce\nonce-init.uce\nparse_time.uce\npost-multipart.uce\npost.uce\npreprocessor-comments.uce\nrandom.uce\nregex.uce\nscript.uce\nsession.uce\nsharedunit.uce\nshell.uce\nsqlite.uce\nstr_replace.uce\nstring.uce\ntask-status.uce\ntask.uce\ntask_repeat.uce\nunit-browser.uce\nunit-info.uce\nuri.uce\nutf8.uce\nwebsockets.ws.uce\nworking-dir.uce\nxml.uce\nyaml.uce\nzip.uce", "\n");
for(String file : files)
cli_expect_http("uce_demo_smoke:demo page " + file, "/demo/" + file, 200, "", file == "unit-browser.uce" ? StringList() : cli_demo_error_markers());
}
bool cli_doc_source_has_example(String source)
{
for(String line : split(source, "\n"))
if(trim(line) == ":example")
return(true);
return(false);
}
// A real example must exercise the documented API and print a derived result.
// These placeholder shapes (auto-generated stubs) are forbidden by the gate.
bool cli_doc_example_is_placeholder(String source)
{
bool in_example = false;
StringList body;
for(String line : split(source, "\n"))
{
String t = trim(line);
if(!in_example)
{
if(t == ":example")
in_example = true;
continue;
}
if(t.length() > 0 && t.substr(0, 1) == ":")
break;
body.push_back(line);
}
String joined = join(body, "\n");
if(cli_contains(joined, " example\\n\");"))
return(true);
if(cli_contains(joined, "is resource-bound; configure"))
return(true);
if(cli_contains(joined, "documents a UCE concept"))
return(true);
return(false);
}
void cli_run_doc_pages_gate()
{
StringList error_markers;
error_markers.push_back("doc example error");
error_markers.push_back(String("uce compile ") + "error");
error_markers.push_back(String("wasm runtime error during ") + "request");
error_markers.push_back(String("fatal signal during ") + "request");
error_markers.push_back(String("uncaught exception during ") + "request");
error_markers.push_back("timed out acquiring compile lock");
for(String file_name : ls("../doc/pages/"))
{
String source = file_get_contents("../doc/pages/" + file_name);
String page = nibble(file_name, ".");
if(page == "")
continue;
bool has_example = cli_doc_source_has_example(source);
String path = "/doc/index.uce?p=" + uri_encode(page);
CliHttpResponse res = cli_frontend(path);
String summary;
bool ok = cli_expect_http_once(res, path, 200, "doc-detail", error_markers, summary);
String lower = to_lower(res.body);
if(has_example)
{
if(!cli_contains(res.body, "class=\"example-output\"") || !cli_contains(res.body, "example-output-label\">Output"))
{
ok = false;
summary += "; missing example Output block";
}
if(cli_contains(lower, "doc example error"))
{
ok = false;
summary += "; example reported an error";
}
if(cli_doc_example_is_placeholder(source))
{
ok = false;
summary += "; placeholder example (must exercise the API)";
}
}
cli_test_case("uce_doc_gate:doc page " + page, ok, ok ? summary : summary + "; body=" + cli_truncate(res.body));
}
}
void cli_run_http_smoke()
{
cli_expect_http("uce_http_smoke:doc index", "/doc/index.uce", 200, "<html>");
// Regression guard: the index enumerates pages via ls("pages/"). When that
// returned empty (wasm ls() was a stub), the page still rendered <html> but
// listed nothing — so assert the function grid is actually populated.
cli_expect_http("uce_http_smoke:doc index lists functions", "/doc/index.uce", 200, "func-item");
cli_expect_http("uce_http_smoke:doc singlepage", "/doc/singlepage.uce", 200, "<html>");
cli_expect_http("uce_http_smoke:doc component page", "/doc/index.uce?p=component", 200, "component()");
cli_expect_http("uce_http_smoke:doc regex page", "/doc/index.uce?p=regex_search", 200, "regex_search");
cli_expect_http("uce_http_smoke:doc xml page", "/doc/index.uce?p=xml_encode", 200, "xml_encode");
cli_expect_http("uce_http_smoke:doc yaml page", "/doc/index.uce?p=yaml_encode", 200, "yaml_encode");
cli_expect_http("uce_http_smoke:doc relative time page", "/doc/index.uce?p=time_format_relative", 200, "time_format_relative");
cli_expect_http("uce_http_smoke:starter home", "/examples/uce-starter/", 200, "Stunning Apps");
cli_expect_http("uce_http_smoke:starter dashboard", "/examples/uce-starter/?dashboard", 200, "Dashboard");
cli_expect_http("uce_http_smoke:starter dashboard ONCE assets reach head", "/examples/uce-starter/?dashboard", 200, "views/dashboard.css");
cli_expect_http("uce_http_smoke:starter workspace nested route", "/examples/uce-starter/?workspace/projects", 200, "Workspace");
cli_expect_http("uce_http_smoke:starter workspace ONCE assets reach head", "/examples/uce-starter/?workspace/projects", 200, "css/workspace.css");
cli_expect_http("uce_http_smoke:starter ajax section", "/examples/uce-starter/?page2-section1", 200, "UCE starter AJAX fragment response");
cli_expect_http("uce_http_smoke:starter route traversal blocked", "/examples/uce-starter/?../../../demo/index", 404, "The requested page does not exist.");
// Configurable error page (page_runtime_error): a runtime fault must render the
// developer-defined error-page unit with the real error info marshalled across
// the membrane, at the error status. Asserts HTTP 500 AND that error["request_uri"]
// reached the page (the "mode=trap" query is only shown if error_info crossed).
cli_expect_http("uce_http_smoke:configurable error page 500 + error info", "/demo/error-reporting.uce?mode=trap", 500, "mode=trap");
}
bool cli_site_body_reports_failure(String lower)
{
if(cli_contains(lower, "status-badge status-error\">fail</span>"))
return(true);
for(String digit : split("1 2 3 4 5 6 7 8 9", " "))
if(cli_contains(lower, ">failed " + digit))
return(true);
return(false);
}
void cli_run_site_suite()
{
DValue manifest;
for(String line : split(file_get_contents("manifest.txt"), "\n"))
{
line = trim(line);
if(line == "" || line[0] == '#')
continue;
StringList parts = split(line, "|");
if(parts.size() < 7 || trim(parts[5]) != "1")
continue;
String file = trim(parts[0]);
String title = trim(parts[1]);
String expected = first(trim(parts[4]), title);
CliHttpResponse res;
String lower;
bool ok = false;
for(u64 attempt = 0; attempt < 3; attempt++)
{
if(attempt > 0)
usleep(1000000);
res = cli_frontend("/tests/" + file);
lower = to_lower(res.body);
ok = res.status == 200 && cli_contains(res.body, expected);
for(String marker : { "compile error", "runtime error", "timed out acquiring compile lock", "near line" })
if(cli_contains(lower, marker))
ok = false;
if(cli_contains(lower, "<div class=\"tests-summary\">") && cli_site_body_reports_failure(lower))
ok = false;
if(ok)
break;
}
String name = "uce_site_suite:site tests " + (file == "index.uce" ? "index" : file == "io.uce" ? "filesystem" : file == "websockets.ws.uce" ? "websockets page" : replace(replace(replace(file, ".uce", ""), ".ws", ""), "_", " "));
String fail_detail = "; status=" + std::to_string(res.status) + "; expected_marker=" + (cli_contains(res.body, expected) ? "yes" : "no") + "; summary=" + (cli_contains(lower, "<div class=\"tests-summary\">") ? "yes" : "no") + "; reports_failure=" + (cli_site_body_reports_failure(lower) ? "yes" : "no") + "; body=" + cli_truncate(res.body);
cli_test_case(name, ok, (ok ? "HTTP 200 with suite page marker and no failed cases for /tests/" : "site suite failed for /tests/") + file + (ok ? "" : fail_detail));
}
}
void cli_run_security_smoke()
{
CliHttpResponse dotdot = cli_direct_http("/../site/demo/hello.uce");
cli_test_case("uce_security_smoke:direct HTTP rejects dot-dot script traversal", dotdot.status != 200 && !cli_contains(dotdot.body, "hello world"), "direct HTTP dot-dot traversal returned HTTP " + std::to_string(dotdot.status));
CliHttpResponse spoof = cli_direct_http("/no-such-script.uce", "Script-Filename: /Code/uce.openfu.com/uce/site/demo/hello.uce\r\n");
cli_test_case("uce_security_smoke:direct HTTP ignores Script-Filename header", spoof.status != 200 && !cli_contains(spoof.body, "hello world"), "direct HTTP Script-Filename override returned HTTP " + std::to_string(spoof.status));
CliHttpResponse sanitizer = cli_frontend("/tests/security_headers.uce");
String headers_lower = to_lower(sanitizer.headers);
bool injected = false;
for(String header : { "x-uce-injected", "x-uce-injected-name", "x-uce-cookie-injected", "x-uce-redirect-injected", "x-uce-status-injected" })
if(cli_contains(headers_lower, "\r\n" + to_lower(header) + ":"))
injected = true;
CliHttpResponse direct_sanitizer = cli_direct_http("/site/tests/security_headers.uce");
bool direct_ok = direct_sanitizer.status < 500 || cli_contains(direct_sanitizer.body, "security header sanitizer test");
cli_test_case("uce_security_smoke:response headers sanitize CRLF", !injected && direct_ok, "CRLF response header injection sanitized");
String attacker = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
CliHttpResponse session = cli_frontend("/tests/http.uce", "Cookie: uce-site-tests=" + attacker + "\r\n");
String session_headers = session.headers;
bool adopted = cli_contains(session_headers, "Set-Cookie: uce-site-tests=" + attacker);
bool hardened = cli_contains(session_headers, "Set-Cookie: uce-site-tests=") && cli_contains(session_headers, "HttpOnly") && cli_contains(session_headers, "SameSite=Lax");
cli_test_case("uce_security_smoke:unknown session ids are not adopted", !adopted && hardened, "unknown caller-supplied session id was replaced");
}
void cli_run_starter_parity()
{
StringList markers = {
"landing\t\tHome | UCE Starter",
"dashboard\t?dashboard\tDashboard | UCE Starter",
"gauges\t?gauges\tGauges | UCE Starter",
"features\t?features\tFeatures | UCE Starter",
"components\t?page1\tComponents | UCE Starter",
"workspace\t?workspace\tWorkspace | UCE Starter"
};
for(String row : markers)
{
StringList parts = split(row, "\t");
String name = parts[0];
String query = parts[1];
String title = parts[2];
cli_expect_http("uce_starter_parity:starter view " + name, "/examples/uce-starter/index.uce" + query, 200, "<title>" + title + "</title>", cli_demo_error_markers());
}
cli_expect_http("uce_starter_parity:starter unknown route 404", "/examples/uce-starter/index.uce?does-not-exist", 404, "<title>404 Not Found | UCE Starter</title>", cli_demo_error_markers());
}
void cli_run_task_lifetime_smoke()
{
String late_file = "/tmp/uce-site-tests-late-task.txt";
file_unlink(late_file);
CliHttpResponse res = cli_frontend("/tests/tasks.uce?mode=late");
usleep(3000000);
String observed = file_exists(late_file) ? file_get_contents(late_file) : "";
cli_test_case("uce_task_lifetime:delayed callback after request return", res.status == 200 && cli_contains(res.body, "late task scheduled") && cli_contains(observed, "late callback ran"), "status=" + std::to_string(res.status) + " observed=" + cli_truncate(observed));
}
void cli_run_tcp_smoke()
{
u64 fd80 = socket_connect("127.0.0.1", 80);
cli_test_case("uce_tcp_smoke:frontend port 80", fd80 != 0, "TCP connect " + String(fd80 != 0 ? "succeeded" : "failed") + " on port 80");
if(fd80 != 0)
socket_close(fd80);
u64 fd8080 = socket_connect("127.0.0.1", 8080);
cli_test_case("uce_tcp_smoke:http websocket port 8080", fd8080 != 0, "TCP connect " + String(fd8080 != 0 ? "succeeded" : "failed") + " on port 8080");
if(fd8080 != 0)
socket_close(fd8080);
}
void cli_run_wasm_kill(bool include_kill)
{
if(!include_kill)
return;
for(String row : { "wasm kill trap|/tests/wasm-kill/oob.uce|unreachable", "wasm kill loop|/tests/wasm-kill/loop.uce|interrupt", "wasm kill recurse|/tests/wasm-kill/recurse.uce|wasm_kill_recurse" })
{
StringList parts = split(row, "|");
CliHttpResponse res = cli_frontend(parts[1]);
CliHttpResponse health = cli_frontend("/demo/hello.uce");
bool ok = res.status == 500 && cli_contains(res.body, "wasm runtime error during request") && cli_contains(res.body, parts[2]) && health.status == 200 && cli_contains(health.body, "hello world");
cli_test_case("uce_wasm_kill:" + parts[0], ok, ok ? "clean wasm trap page and post-trap health check for " + parts[1] : "kill check failed; status=" + std::to_string(res.status) + " health=" + std::to_string(health.status));
}
}
void cli_print_list(bool include_kill)
{
print("UCE CLI test groups:\n");
print(" demo, http, site, security, starter, tcp");
if(include_kill)
print(", wasm-kill");
print("\n");
}
CLI(Request& context)
{
context.header["Content-Type"] = "text/plain; charset=utf-8";
DValue input = cli_input(context);
String action = first(input["action"].to_string(), "run");
bool include_kill = input["include_kill"].to_bool() || input["include_wasm_kill"].to_bool();
if(action == "list")
{
cli_print_list(include_kill);
return;
}
print("Running UCE CLI tests\n");
f64 group_start = time_precise();
cli_run_demo_smoke();
print("[GROUP] demo ", std::to_string((u64)((time_precise() - group_start) * 1000)), " ms\n");
group_start = time_precise();
cli_run_http_smoke();
print("[GROUP] http ", std::to_string((u64)((time_precise() - group_start) * 1000)), " ms\n");
group_start = time_precise();
cli_run_site_suite();
print("[GROUP] site ", std::to_string((u64)((time_precise() - group_start) * 1000)), " ms\n");
group_start = time_precise();
cli_run_doc_pages_gate();
print("[GROUP] doc-gate ", std::to_string((u64)((time_precise() - group_start) * 1000)), " ms\n");
group_start = time_precise();
cli_run_security_smoke();
print("[GROUP] security ", std::to_string((u64)((time_precise() - group_start) * 1000)), " ms\n");
group_start = time_precise();
cli_run_task_lifetime_smoke();
print("[GROUP] task-lifetime ", std::to_string((u64)((time_precise() - group_start) * 1000)), " ms\n");
group_start = time_precise();
cli_run_starter_parity();
print("[GROUP] starter ", std::to_string((u64)((time_precise() - group_start) * 1000)), " ms\n");
group_start = time_precise();
cli_run_tcp_smoke();
String repo_root = process_start_directory();
String cli_arg_out = shell_exec("cd " + shell_escape(repo_root) + " && bash scripts/uce-cli /tests/cli.uce action=arg message=hello-cli 2>&1");
cli_test_case("uce_cli_fixture:cli_arg", contains(cli_arg_out, "hello-cli") && contains(cli_arg_out, "fallback"), cli_truncate(cli_arg_out));
print("[GROUP] tcp ", std::to_string((u64)((time_precise() - group_start) * 1000)), " ms\n");
group_start = time_precise();
cli_run_wasm_kill(include_kill);
print("[GROUP] wasm-kill ", std::to_string((u64)((time_precise() - group_start) * 1000)), " ms\n");
print("\nSummary: ", std::to_string(cli_tests_passed), " passed, ", std::to_string(cli_tests_failed), " failed, ", std::to_string(cli_tests_skipped), " skipped\n");
if(cli_tests_failed != 0)
{
context.set_status(500, "CLI Tests Failed");
print("Failures:\n");
for(String failure : cli_tests_failures)
print("- ", failure, "\n");
}
}
RENDER(Request& context)
{
context.set_status(404, "Not Found");
context.header["Content-Type"] = "text/plain; charset=utf-8";
print("Invoke this test runner through the UCE CLI socket.\n");
}