From 107c4a5dceda06f2bde1668e63ff5eeeec629a7f Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Sun, 7 Dec 2025 15:35:45 -0600 Subject: [PATCH] tests can now start actor based tests --- scripts/shop.cm | 6 +- scripts/test.ce | 372 ++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 299 insertions(+), 79 deletions(-) diff --git a/scripts/shop.cm b/scripts/shop.cm index cae1fbbc..a4482285 100644 --- a/scripts/shop.cm +++ b/scripts/shop.cm @@ -1184,11 +1184,7 @@ Shop.remove = function(alias) { // Remove directory if (fd.is_dir(target_dir)) { log.console("Removing " + target_dir) - try { - fd.rmdir(target_dir) - } catch (e) { - log.error("Failed to remove directory: " + e) - } + fd.rmdir(target_dir) } log.console("Removed " + alias) diff --git a/scripts/test.ce b/scripts/test.ce index 90fe7a53..505fa760 100644 --- a/scripts/test.ce +++ b/scripts/test.ce @@ -9,6 +9,11 @@ if (!args) args = [] var target_pkg = null // null = current, otherwise canonical path var all_pkgs = false +// Actor test support +def ACTOR_TEST_TIMEOUT = 30000 // 30 seconds timeout for actor tests +var pending_actor_tests = [] +var actor_test_results = [] + if (args.length > 0) { if (args[0] == 'package') { if (args.length < 2) { @@ -51,6 +56,58 @@ function ensure_dir(path) { return true } +// Collect .ce actor tests from a package +function collect_actor_tests(pkg) { + var prefix = pkg ? `.cell/modules/${pkg}` : "." + var tests_dir = `${prefix}/tests` + + if (!fd.is_dir(tests_dir)) return [] + + var files = shop.list_files(pkg) + var actor_tests = [] + for (var i = 0; i < files.length; i++) { + var f = files[i] + // Check if file is in tests/ folder and is a .ce actor + if (f.startsWith("tests/") && f.endsWith(".ce")) { + actor_tests.push({ + package: pkg || "local", + file: f, + path: pkg ? `${prefix}/${f}` : f + }) + } + } + return actor_tests +} + +// Spawn an actor test and track it +function spawn_actor_test(test_info) { + var test_name = test_info.file.substring(6, test_info.file.length - 3) // remove "tests/" and ".ce" + log.console(` [ACTOR] ${test_info.file}`) + + var entry = { + package: test_info.package, + file: test_info.file, + test: test_name, + status: "running", + start_time: time.number(), + actor: null + } + + try { + // Spawn the actor test - it should send back results + // The actor receives $_.parent which it can use to send results + var actor_path = test_info.path.substring(0, test_info.path.length - 3) // remove .ce + entry.actor = $_.spawn(actor_path) + pending_actor_tests.push(entry) + } catch (e) { + entry.status = "failed" + entry.error = { message: `Failed to spawn actor: ${e}` } + entry.duration_ns = 0 + actor_test_results.push(entry) + log.console(` FAIL ${test_name}: ${e}`) + } +} + function run_tests(pkg) { var prefix = pkg ? `.cell/modules/${pkg}` : "." var tests_dir = `${prefix}/tests` @@ -69,7 +126,7 @@ function run_tests(pkg) { var test_files = [] for (var i = 0; i < files.length; i++) { var f = files[i] - // Check if file is in tests/ folder and is a .cm module + // Check if file is in tests/ folder and is a .cm module (not .ce - those are actor tests) if (f.startsWith("tests/") && f.endsWith(".cm")) { test_files.push(f) } @@ -186,113 +243,280 @@ function run_tests(pkg) { } var all_results = [] +var all_actor_tests = [] if (all_pkgs) { // Run local first all_results.push(run_tests(null)) + all_actor_tests = all_actor_tests.concat(collect_actor_tests(null)) // Then all dependencies var deps = shop.list_packages(null) for (var i = 0; i < deps.length; i++) { all_results.push(run_tests(deps[i])) + all_actor_tests = all_actor_tests.concat(collect_actor_tests(deps[i])) } } else { all_results.push(run_tests(target_pkg)) + all_actor_tests = all_actor_tests.concat(collect_actor_tests(target_pkg)) } -// Calculate totals -var totals = { total: 0, passed: 0, failed: 0 } -for (var i = 0; i < all_results.length; i++) { - totals.total += all_results[i].total - totals.passed += all_results[i].passed - totals.failed += all_results[i].failed +// Spawn actor tests if any +if (all_actor_tests.length > 0) { + log.console(`Running ${all_actor_tests.length} actor test(s)...`) + for (var i = 0; i < all_actor_tests.length; i++) { + spawn_actor_test(all_actor_tests[i]) + } } -log.console(`----------------------------------------`) -log.console(`Tests: ${totals.passed} passed, ${totals.failed} failed, ${totals.total} total`) +// Handle messages from actor tests +// Actor tests should send: { type: "test_result", passed: bool, error: string|null } +function handle_actor_message(msg) { + // Find the pending test from this sender + var sender = msg.$sender + var found_idx = -1 + for (var i = 0; i < pending_actor_tests.length; i++) { + if (pending_actor_tests[i].actor == sender) { + found_idx = i + break + } + } + + if (found_idx == -1) return // Unknown sender + + var entry = pending_actor_tests[found_idx] + pending_actor_tests.splice(found_idx, 1) + + var end_time = time.number() + entry.duration_ns = Math.round((end_time - entry.start_time) * 1000000000) + + if (msg.type == "test_result") { + if (msg.passed) { + entry.status = "passed" + log.console(` PASS ${entry.test}`) + } else { + entry.status = "failed" + entry.error = { message: msg.error || "Test failed" } + if (msg.stack) entry.error.stack = msg.stack + log.console(` FAIL ${entry.test}: ${entry.error.message}`) + } + } else { + entry.status = "failed" + entry.error = { message: `Unexpected message type: ${msg.type}` } + log.console(` FAIL ${entry.test}: unexpected message`) + } + + actor_test_results.push(entry) + check_completion() +} + +// Check for timed out actor tests +function check_timeouts() { + var now = time.number() + var timed_out = [] + + for (var i = pending_actor_tests.length - 1; i >= 0; i--) { + var entry = pending_actor_tests[i] + var elapsed_ms = (now - entry.start_time) * 1000 + if (elapsed_ms > ACTOR_TEST_TIMEOUT) { + timed_out.push(i) + } + } + + for (var i = 0; i < timed_out.length; i++) { + var idx = timed_out[i] + var entry = pending_actor_tests[idx] + pending_actor_tests.splice(idx, 1) + + entry.status = "failed" + entry.error = { message: "Test timed out" } + entry.duration_ns = ACTOR_TEST_TIMEOUT * 1000000 + actor_test_results.push(entry) + log.console(` TIMEOUT ${entry.test}`) + } + + if (pending_actor_tests.length > 0) { + $_.delay(check_timeouts, 1000) + } + check_completion() +} + +// Check if all tests are complete and finalize +var finalized = false +function check_completion() { + if (finalized) return + if (pending_actor_tests.length > 0) return + + finalized = true + finalize_results() +} + +function finalize_results() { + // Add actor test results to all_results + for (var i = 0; i < actor_test_results.length; i++) { + var r = actor_test_results[i] + // Find or create package result + var pkg_result = null + for (var j = 0; j < all_results.length; j++) { + if (all_results[j].package == r.package) { + pkg_result = all_results[j] + break + } + } + if (!pkg_result) { + pkg_result = { package: r.package, files: [], total: 0, passed: 0, failed: 0 } + all_results.push(pkg_result) + } + + // Find or create file result + var file_result = null + for (var j = 0; j < pkg_result.files.length; j++) { + if (pkg_result.files[j].name == r.file) { + file_result = pkg_result.files[j] + break + } + } + if (!file_result) { + file_result = { name: r.file, tests: [], passed: 0, failed: 0 } + pkg_result.files.push(file_result) + } + + file_result.tests.push(r) + pkg_result.total++ + if (r.status == "passed") { + pkg_result.passed++ + file_result.passed++ + } else { + pkg_result.failed++ + file_result.failed++ + } + } + + // Calculate totals + var totals = { total: 0, passed: 0, failed: 0 } + for (var i = 0; i < all_results.length; i++) { + totals.total += all_results[i].total + totals.passed += all_results[i].passed + totals.failed += all_results[i].failed + } + + log.console(`----------------------------------------`) + log.console(`Tests: ${totals.passed} passed, ${totals.failed} failed, ${totals.total} total`) + + generate_reports(totals) + $_.stop() +} + +// If no actor tests, finalize immediately +if (all_actor_tests.length == 0) { + // Calculate totals + var totals = { total: 0, passed: 0, failed: 0 } + for (var i = 0; i < all_results.length; i++) { + totals.total += all_results[i].total + totals.passed += all_results[i].passed + totals.failed += all_results[i].failed + } + + log.console(`----------------------------------------`) + log.console(`Tests: ${totals.passed} passed, ${totals.failed} failed, ${totals.total} total`) +} else { + // Start timeout checker + $_.delay(check_timeouts, 1000) +} -// Generate Reports -var timestamp = Math.floor(time.number()).toString() -var report_dir = `.cell/reports/test_${timestamp}` -ensure_dir(report_dir) +// Generate Reports function +function generate_reports(totals) { + var timestamp = Math.floor(time.number()).toString() + var report_dir = `.cell/reports/test_${timestamp}` + ensure_dir(report_dir) -// Generate test.txt -var txt_report = `TEST REPORT + // Generate test.txt + var txt_report = `TEST REPORT Date: ${time.text(time.number())} Total: ${totals.total}, Passed: ${totals.passed}, Failed: ${totals.failed} === SUMMARY === ` -for (var i = 0; i < all_results.length; i++) { - var pkg_res = all_results[i] - if (pkg_res.total == 0) continue - txt_report += `Package: ${pkg_res.package}\n` - for (var j = 0; j < pkg_res.files.length; j++) { - var f = pkg_res.files[j] - var status = f.failed == 0 ? "PASS" : "FAIL" - txt_report += ` [${status}] ${f.name} (${f.passed}/${f.tests.length})\n` + for (var i = 0; i < all_results.length; i++) { + var pkg_res = all_results[i] + if (pkg_res.total == 0) continue + txt_report += `Package: ${pkg_res.package}\n` + for (var j = 0; j < pkg_res.files.length; j++) { + var f = pkg_res.files[j] + var status = f.failed == 0 ? "PASS" : "FAIL" + txt_report += ` [${status}] ${f.name} (${f.passed}/${f.tests.length})\n` + } } -} -txt_report += `\n=== FAILURES ===\n` -var has_failures = false -for (var i = 0; i < all_results.length; i++) { - var pkg_res = all_results[i] - for (var j = 0; j < pkg_res.files.length; j++) { - var f = pkg_res.files[j] - for (var k = 0; k < f.tests.length; k++) { - var t = f.tests[k] - if (t.status == "failed") { - has_failures = true - txt_report += `FAIL: ${pkg_res.package} :: ${f.name} :: ${t.test}\n` - if (t.error) { - txt_report += ` Message: ${t.error.message}\n` - if (t.error.stack) { - txt_report += ` Stack:\n${t.error.stack.split('\n').map(function(l){return ` ${l}`}).join('\n')}\n` + txt_report += `\n=== FAILURES ===\n` + var has_failures = false + for (var i = 0; i < all_results.length; i++) { + var pkg_res = all_results[i] + for (var j = 0; j < pkg_res.files.length; j++) { + var f = pkg_res.files[j] + for (var k = 0; k < f.tests.length; k++) { + var t = f.tests[k] + if (t.status == "failed") { + has_failures = true + txt_report += `FAIL: ${pkg_res.package} :: ${f.name} :: ${t.test}\n` + if (t.error) { + txt_report += ` Message: ${t.error.message}\n` + if (t.error.stack) { + txt_report += ` Stack:\n${t.error.stack.split('\n').map(function(l){return ` ${l}`}).join('\n')}\n` + } } + txt_report += `\n` } - txt_report += `\n` } } } -} -if (!has_failures) txt_report += `None\n` + if (!has_failures) txt_report += `None\n` -txt_report += `\n=== DETAILED RESULTS ===\n` -for (var i = 0; i < all_results.length; i++) { - var pkg_res = all_results[i] - if (pkg_res.total == 0) continue - - for (var j = 0; j < pkg_res.files.length; j++) { - var f = pkg_res.files[j] - for (var k = 0; k < f.tests.length; k++) { - var t = f.tests[k] - var dur = `${t.duration_ns}ns` - var status = t.status == "passed" ? "PASS" : "FAIL" - txt_report += `[${status}] ${pkg_res.package} ${t.test} (${dur})\n` + txt_report += `\n=== DETAILED RESULTS ===\n` + for (var i = 0; i < all_results.length; i++) { + var pkg_res = all_results[i] + if (pkg_res.total == 0) continue + + for (var j = 0; j < pkg_res.files.length; j++) { + var f = pkg_res.files[j] + for (var k = 0; k < f.tests.length; k++) { + var t = f.tests[k] + var dur = `${t.duration_ns || 0}ns` + var status = t.status == "passed" ? "PASS" : "FAIL" + txt_report += `[${status}] ${pkg_res.package} ${t.test} (${dur})\n` + } } } + + fd.slurpwrite(`${report_dir}/test.txt`, utf8.encode(txt_report)) + log.console(`Report written to ${report_dir}/test.txt`) + + // Generate JSON per package + for (var i = 0; i < all_results.length; i++) { + var pkg_res = all_results[i] + if (pkg_res.total == 0) continue + + var pkg_tests = [] + for (var j = 0; j < pkg_res.files.length; j++) { + var f = pkg_res.files[j] + for (var k = 0; k < f.tests.length; k++) { + pkg_tests.push(f.tests[k]) + } + } + + var json_path = `${report_dir}/${pkg_res.package.replace(/\//g, '_')}.json` + fd.slurpwrite(json_path, utf8.encode(json.encode(pkg_tests))) + } } -fd.slurpwrite(`${report_dir}/test.txt`, utf8.encode(txt_report)) -log.console(`Report written to ${report_dir}/test.txt`) - -// Generate JSON per package -for (var i = 0; i < all_results.length; i++) { - var pkg_res = all_results[i] - if (pkg_res.total == 0) continue - - var pkg_tests = [] - for (var j = 0; j < pkg_res.files.length; j++) { - var f = pkg_res.files[j] - for (var k = 0; k < f.tests.length; k++) { - pkg_tests.push(f.tests[k]) - } - } - - var json_path = `${report_dir}/${pkg_res.package.replace(/\//g, '_')}.json` - fd.slurpwrite(json_path, utf8.encode(json.encode(pkg_tests))) +// If no actor tests, generate reports and stop immediately +if (all_actor_tests.length == 0) { + generate_reports(totals) + $_.stop() +} else { + // Set up portal to receive messages from actor tests + $_.portal(function(msg) { + handle_actor_message(msg) + }) } - -$_.stop()