// cell test - Run tests for packages var shop = use('internal/shop') var pkg = use('package') var fd = use('fd') var time = use('time') var json = use('json') var blob = use('blob') var dbg = use('js') var testlib = use('internal/testlib') // run gc with dbg.gc() var _args = args == null ? [] : args var target_pkg = null // null = current package var target_test = null // null = all tests, otherwise specific test file var all_pkgs = false var gc_after_each_test = false var verify_ir = false var diff_mode = false var os_ref = use('internal/os') var analyze = os_ref.analyze var run_ast_fn = os_ref.run_ast_fn var run_ast_noopt_fn = os_ref.run_ast_noopt_fn // Actor test support def ACTOR_TEST_TIMEOUT = 30000 // 30 seconds timeout for actor tests var pending_actor_tests = [] var actor_test_results = [] var is_valid_package = testlib.is_valid_package var get_current_package_name = testlib.get_current_package_name // Parse arguments // Usage: // cell test - run all tests for current package // cell test tests/suite - run specific test file in current package // cell test all - run all tests for current package // cell test package - run all tests for named package // cell test package - run specific test in named package // cell test package all - run all tests from all packages // // Flags: // -g - run GC after each test // --verify - enable IR verification (validates mcode IR after each optimizer pass) // --diff - enable differential testing (run each test optimized and unoptimized, compare results) function parse_args() { var cleaned_args = [] var i = 0 var name = null var lock = null var resolved = null var test_path = null for (i = 0; i < length(_args); i++) { if (_args[i] == '-g') { gc_after_each_test = true } else if (_args[i] == '--verify') { verify_ir = true } else if (_args[i] == '--diff') { diff_mode = true } else { push(cleaned_args, _args[i]) } } _args = cleaned_args if (length(_args) == 0) { // cell test - run all tests for current package if (!is_valid_package('.')) { log.console('No cell.toml found in current directory') return false } target_pkg = null return true } if (_args[0] == 'all') { // cell test all - run all tests for current package if (!is_valid_package('.')) { log.console('No cell.toml found in current directory') return false } target_pkg = null return true } if (_args[0] == 'package') { if (length(_args) < 2) { log.console('Usage: cell test package [test]') log.console(' cell test package all') return false } if (_args[1] == 'all') { // cell test package all - run tests from all packages all_pkgs = true log.console('Testing all packages...') return true } // cell test package [test] name = _args[1] // Check if package exists in lock or is a local path lock = shop.load_lock() if (lock[name]) { target_pkg = name } else if (starts_with(name, '/') && is_valid_package(name)) { target_pkg = name } else { // Try to resolve as dependency alias from current package resolved = null if (is_valid_package('.')) { resolved = pkg.alias_to_package(null, name) if (resolved) { target_pkg = resolved } else { log.console(`Package not found: ${name}`) return false } } else { log.console(`Package not found: ${name}`) return false } } if (length(_args) >= 3) { // cell test package target_test = _args[2] } log.console(`Testing package: ${target_pkg}`) return true } // cell test tests/suite or cell test - specific test file test_path = _args[0] // Normalize path - add tests/ prefix if not present and doesn't start with / if (!starts_with(test_path, 'tests/') && !starts_with(test_path, '/')) { // Check if file exists as-is first if (!fd.is_file(test_path + '.cm') && !fd.is_file(test_path)) { // Try with tests/ prefix if (fd.is_file('tests/' + test_path + '.cm') || fd.is_file('tests/' + test_path)) { test_path = 'tests/' + test_path } } } target_test = test_path target_pkg = null if (!is_valid_package('.')) { log.console('No cell.toml found in current directory') return false } return true } if (!parse_args()) { $stop() return } // Enable IR verification if requested if (verify_ir) { os_ref._verify_ir = true log.console('IR verification enabled') } if (diff_mode && !run_ast_noopt_fn) { log.console('error: --diff requires run_ast_noopt_fn (rebuild bootstrap)') $stop() return } var values_equal = testlib.values_equal var describe = testlib.describe // Diff mode: run a test function through noopt and compare var diff_mismatches = 0 function diff_check(test_name, file_path, opt_fn, noopt_fn) { if (!diff_mode) return var opt_result = null var noopt_result = null var opt_err = null var noopt_err = null var _opt = function() { opt_result = opt_fn() } disruption { opt_err = "disrupted" } _opt() var _noopt = function() { noopt_result = noopt_fn() } disruption { noopt_err = "disrupted" } _noopt() if (opt_err != noopt_err) { log.console(` DIFF ${test_name}: disruption mismatch opt=${opt_err != null ? opt_err : "ok"} noopt=${noopt_err != null ? noopt_err : "ok"}`) diff_mismatches = diff_mismatches + 1 } else if (!values_equal(opt_result, noopt_result)) { log.console(` DIFF ${test_name}: result mismatch opt=${describe(opt_result)} noopt=${describe(noopt_result)}`) diff_mismatches = diff_mismatches + 1 } } var ensure_dir = fd.ensure_dir var get_pkg_dir = testlib.get_pkg_dir // Collect .ce actor tests from a package function collect_actor_tests(package_name, specific_test) { var prefix = get_pkg_dir(package_name) var tests_dir = prefix + '/tests' if (!fd.is_dir(tests_dir)) return [] var files = pkg.list_files(package_name) var actor_tests = [] var i = 0 var f = null var test_name = null var match_name = null var test_base = null var match_base = null for (i = 0; i < length(files); i++) { f = files[i] // Check if file is in tests/ folder and is a .ce actor if (starts_with(f, "tests/") && ends_with(f, ".ce")) { // If specific test requested, filter if (specific_test) { test_name = text(f, 0, -3) // remove .ce match_name = specific_test if (!starts_with(match_name, 'tests/')) match_name = 'tests/' + match_name if (!ends_with(match_name, '.ce')) match_name = match_name // Match without extension test_base = test_name match_base = ends_with(match_name, '.ce') ? text(match_name, 0, -3) : match_name if (test_base != match_base) continue } push(actor_tests, { package: package_name || "local", file: f, path: prefix + '/' + f }) } } return actor_tests } // Spawn an actor test and track it function spawn_actor_test(test_info) { var test_name = text(test_info.file, 6, -3) 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 } var _spawn = function() { var actor_path = text(test_info.path, 0, -3) $start(function(event) { var end_time = time.number() var duration_ns = round((end_time - entry.start_time) * 1000000000) if (event.type == 'greet') { entry.actor = event.actor return } var idx = find(pending_actor_tests, e => e == entry) if (idx != null) { pending_actor_tests = array( array(pending_actor_tests, 0, idx), array(pending_actor_tests, idx + 1) ) } entry.duration_ns = duration_ns if (event.type == 'stop') { entry.status = "passed" log.console(` PASS ${test_name}`) } else { entry.status = "failed" entry.error = { message: event.reason || "Actor disrupted" } log.console(` FAIL ${test_name}: ${entry.error.message}`) } push(actor_test_results, entry) if (gc_after_each_test) dbg.gc() check_completion() }, actor_path) push(pending_actor_tests, entry) } disruption { entry.status = "failed" entry.error = { message: "Failed to spawn actor" } entry.duration_ns = 0 push(actor_test_results, entry) log.console(` FAIL ${test_name}: Failed to spawn`) } _spawn() } function run_tests(package_name, specific_test) { var prefix = get_pkg_dir(package_name) var tests_dir = prefix + '/tests' var pkg_result = { package: package_name || "local", files: [], total: 0, passed: 0, failed: 0 } if (!fd.is_dir(tests_dir)) return pkg_result var files = pkg.list_files(package_name) var test_files = [] var i = 0 var f = null var test_name = null var match_name = null var match_base = null var mod_path = null var file_result = null for (i = 0; i < length(files); i++) { f = files[i] // Check if file is in tests/ folder and is a .cm module (not .ce - those are actor tests) if (starts_with(f, "tests/") && ends_with(f, ".cm")) { // If specific test requested, filter if (specific_test) { test_name = text(f, 0, -3) // remove .cm match_name = specific_test if (!starts_with(match_name, 'tests/')) match_name = 'tests/' + match_name // Match without extension match_base = ends_with(match_name, '.cm') ? text(match_name, 0, -3) : match_name if (test_name != match_base) continue } push(test_files, f) } } if (length(test_files) > 0) { if (package_name) log.console(`Running tests for ${package_name}`) else log.console(`Running tests for local package`) } var _load_file = null var load_error = false var err_entry = null for (i = 0; i < length(test_files); i++) { f = test_files[i] mod_path = text(f, 0, -3) // remove .cm load_error = false file_result = { name: f, tests: [], passed: 0, failed: 0 } _load_file = function() { var test_mod = null var test_mod_noopt = null var use_pkg = package_name ? package_name : fd.realpath('.') var _load_noopt = null test_mod = shop.use(mod_path, use_pkg) // Load noopt version for diff mode if (diff_mode) { _load_noopt = function() { var src_path = prefix + '/' + f var src = text(fd.slurp(src_path)) var ast = analyze(src, src_path) test_mod_noopt = run_ast_noopt_fn(mod_path + '_noopt', ast, stone({ use: function(path) { return shop.use(path, use_pkg) } })) } disruption { log.console(` DIFF: failed to load noopt module for ${f}`) } _load_noopt() } var tests = [] var j = 0 var t = null var test_entry = null var start_time = null var _test_error = null var end_time = null var _run_one = null var all_keys = null var fn_count = 0 var null_count = 0 var other_count = 0 var first_null_key = null var first_other_key = null if (is_function(test_mod)) { push(tests, {name: 'main', fn: test_mod}) } else if (is_object(test_mod)) { all_keys = array(test_mod) log.console(` Found ${length(all_keys)} test entries`) arrfor(all_keys, function(k) { if (is_function(test_mod[k])) { fn_count = fn_count + 1 push(tests, {name: k, fn: test_mod[k]}) } else if (is_null(test_mod[k])) { null_count = null_count + 1 if (!first_null_key) first_null_key = k } else { other_count = other_count + 1 if (!first_other_key) first_other_key = k } }) log.console(` functions=${fn_count} nulls=${null_count} other=${other_count}`) if (first_other_key) { log.console(` first other key: ${first_other_key}`) log.console(` is_number=${is_number(test_mod[first_other_key])} is_text=${is_text(test_mod[first_other_key])} is_logical=${is_logical(test_mod[first_other_key])} is_object=${is_object(test_mod[first_other_key])}`) } } if (length(tests) > 0) { log.console(` ${f}`) for (j = 0; j < length(tests); j++) { t = tests[j] test_entry = { package: pkg_result.package, test: t.name, status: "pending", duration_ns: 0 } start_time = time.number() _test_error = null _run_one = function() { var ret = t.fn() if (is_text(ret)) { _test_error = ret disrupt } else if (ret && is_text(ret.message)) { _test_error = ret.message disrupt } test_entry.status = "passed" log.console(` PASS ${t.name}`) } disruption { var e = _test_error test_entry.status = "failed" test_entry.error = { message: e, stack: (e && e.stack) ? e.stack : "" } if (e && e.name) test_entry.error.name = e.name if (is_object(e) && e.message) { test_entry.error.message = e.message } log.console(` FAIL ${t.name} ${test_entry.error.message}`) if (test_entry.error.stack) { log.console(` ${text(array(test_entry.error.stack, '\n'), '\n ')}`) } } _run_one() // Differential check: compare opt vs noopt if (diff_mode && test_mod_noopt && is_object(test_mod_noopt) && is_function(test_mod_noopt[t.name])) { diff_check(t.name, f, t.fn, test_mod_noopt[t.name]) } end_time = time.number() test_entry.duration_ns = round((end_time - start_time) * 1000000000) // Update counters at _load_file level (not inside _run_one) if (test_entry.status == "passed") { pkg_result.passed = pkg_result.passed + 1 file_result.passed = file_result.passed + 1 } else { pkg_result.failed = pkg_result.failed + 1 file_result.failed = file_result.failed + 1 } push(file_result.tests, test_entry) pkg_result.total = pkg_result.total + 1 if (gc_after_each_test) { dbg.gc() } } } } disruption { load_error = true } _load_file() if (load_error) { log.console(" Error loading " + f) pkg_result.failed = pkg_result.failed + 1 file_result.failed = file_result.failed + 1 pkg_result.total = pkg_result.total + 1 } push(pkg_result.files, file_result) } return pkg_result } var all_results = [] var all_actor_tests = [] var packages = null var i = 0 if (all_pkgs) { // Run local first if we're in a valid package if (is_valid_package('.')) { push(all_results, run_tests(null, null)) all_actor_tests = array(all_actor_tests, collect_actor_tests(null, null)) } // Then all packages in lock packages = shop.list_packages() for (i = 0; i < length(packages); i++) { push(all_results, run_tests(packages[i], null)) all_actor_tests = array(all_actor_tests, collect_actor_tests(packages[i], null)) } } else { push(all_results, run_tests(target_pkg, target_test)) all_actor_tests = array(all_actor_tests, collect_actor_tests(target_pkg, target_test)) } // Spawn actor tests if any if (length(all_actor_tests) > 0) { log.console(`Running ${length(all_actor_tests)} actor test(s)...`) for (i = 0; i < length(all_actor_tests); i++) { spawn_actor_test(all_actor_tests[i]) } } // Check for timed out actor tests function check_timeouts() { var now = time.number() var timed_out = [] var i = 0 var entry = null var elapsed_ms = null var idx = null for (i = length(pending_actor_tests) - 1; i >= 0; i--) { entry = pending_actor_tests[i] elapsed_ms = (now - entry.start_time) * 1000 if (elapsed_ms > ACTOR_TEST_TIMEOUT) { push(timed_out, i) } } for (i = 0; i < length(timed_out); i++) { idx = timed_out[i] entry = pending_actor_tests[idx] pending_actor_tests = array(array(pending_actor_tests, 0, idx), array(pending_actor_tests, idx + 1)) entry.status = "failed" entry.error = { message: "Test timed out" } entry.duration_ns = ACTOR_TEST_TIMEOUT * 1000000 push(actor_test_results, entry) log.console(` TIMEOUT ${entry.test}`) } if (length(pending_actor_tests) > 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 (length(pending_actor_tests) > 0) return finalized = true finalize_results() } function finalize_results() { var i = 0 var j = 0 var r = null var pkg_result = null var file_result = null // Add actor test results to all_results for (i = 0; i < length(actor_test_results); i++) { r = actor_test_results[i] pkg_result = null for (j = 0; j < length(all_results); 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 } push(all_results, pkg_result) } file_result = null for (j = 0; j < length(pkg_result.files); 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 } push(pkg_result.files, file_result) } push(file_result.tests, r) pkg_result.total = pkg_result.total + 1 if (r.status == "passed") { pkg_result.passed = pkg_result.passed + 1 file_result.passed = file_result.passed + 1 } else { pkg_result.failed = pkg_result.failed + 1 file_result.failed = file_result.failed + 1 } } // Calculate totals var totals = { total: 0, passed: 0, failed: 0 } for (i = 0; i < length(all_results); i++) { totals.total = totals.total + all_results[i].total totals.passed = totals.passed + all_results[i].passed totals.failed = totals.failed + all_results[i].failed } log.console(`----------------------------------------`) log.console(`Tests: ${totals.passed} passed, ${totals.failed} failed, ${totals.total} total`) if (diff_mode) { log.console(`Diff mismatches: ${text(diff_mismatches)}`) } generate_reports(totals) $stop() } // If no actor tests, finalize immediately var totals = null if (length(all_actor_tests) == 0) { totals = { total: 0, passed: 0, failed: 0 } for (i = 0; i < length(all_results); i++) { totals.total = totals.total + all_results[i].total totals.passed = totals.passed + all_results[i].passed totals.failed = totals.failed + all_results[i].failed } log.console(`----------------------------------------`) log.console(`Tests: ${totals.passed} passed, ${totals.failed} failed, ${totals.total} total`) if (diff_mode) { log.console(`Diff mismatches: ${text(diff_mismatches)}`) } } else { $delay(check_timeouts, 1000) } // Generate Reports function function generate_reports(totals) { var timestamp = text(floor(time.number())) var report_dir = shop.get_reports_dir() + '/test_' + timestamp ensure_dir(report_dir) var i = 0 var j = 0 var k = 0 var pkg_res = null var f = null var status = null var t = null var dur = null var pkg_tests = null var json_path = null var txt_report = `TEST REPORT Date: ${time.text(time.number())} Total: ${totals.total}, Passed: ${totals.passed}, Failed: ${totals.failed} === SUMMARY === ` for (i = 0; i < length(all_results); i++) { pkg_res = all_results[i] if (pkg_res.total == 0) continue txt_report = txt_report + `Package: ${pkg_res.package}\n` for (j = 0; j < length(pkg_res.files); j++) { f = pkg_res.files[j] status = f.failed == 0 ? "PASS" : "FAIL" txt_report = txt_report + ` [${status}] ${f.name} (${f.passed}/${length(f.tests)})\n` } } txt_report = txt_report + `\n=== FAILURES ===\n` var has_failures = false for (i = 0; i < length(all_results); i++) { pkg_res = all_results[i] for (j = 0; j < length(pkg_res.files); j++) { f = pkg_res.files[j] for (k = 0; k < length(f.tests); k++) { t = f.tests[k] if (t.status == "failed") { has_failures = true txt_report = txt_report + `FAIL: ${pkg_res.package} :: ${f.name} :: ${t.test}\n` if (t.error) { txt_report = txt_report + ` Message: ${t.error.message}\n` if (t.error.stack) { txt_report = txt_report + ` Stack:\n${text(array(array(t.error.stack, '\n'), l => ` ${l}`), '\n')}\n` } } txt_report = txt_report + `\n` } } } } if (!has_failures) txt_report = txt_report + `None\n` txt_report = txt_report + `\n=== DETAILED RESULTS ===\n` for (i = 0; i < length(all_results); i++) { pkg_res = all_results[i] if (pkg_res.total == 0) continue for (j = 0; j < length(pkg_res.files); j++) { f = pkg_res.files[j] for (k = 0; k < length(f.tests); k++) { t = f.tests[k] dur = `${t.duration_ns || 0}ns` status = t.status == "passed" ? "PASS" : "FAIL" txt_report = txt_report + `[${status}] ${pkg_res.package} ${t.test} (${dur})\n` } } } ensure_dir(report_dir) fd.slurpwrite(`${report_dir}/test.txt`, stone(blob(txt_report))) log.console(`Report written to ${report_dir}/test.txt`) // Generate JSON per package for (i = 0; i < length(all_results); i++) { pkg_res = all_results[i] if (pkg_res.total == 0) continue pkg_tests = [] for (j = 0; j < length(pkg_res.files); j++) { f = pkg_res.files[j] for (k = 0; k < length(f.tests); k++) { push(pkg_tests, f.tests[k]) } } json_path = `${report_dir}/${replace(pkg_res.package, /\//, '_')}.json` fd.slurpwrite(json_path, stone(blob(json.encode(pkg_tests)))) } } // If no actor tests, generate reports and stop immediately if (length(all_actor_tests) == 0) { generate_reports(totals) $stop() }