string + number throws

This commit is contained in:
2025-12-28 14:25:57 -06:00
parent a035e28100
commit 3b42426e6f
4 changed files with 232 additions and 214 deletions

308
test.ce
View File

@@ -1,4 +1,5 @@
var shop = use('internal/shop')
var pkg = use('package')
var fd = use('fd')
var time = use('time')
var json = use('json')
@@ -6,7 +7,8 @@ var utf8 = use('utf8')
if (!args) args = []
var target_pkg = null // null = current, otherwise canonical path
var target_pkg = null // null = current package
var target_test = null // null = all tests, otherwise specific test file
var all_pkgs = false
// Actor test support
@@ -14,41 +16,139 @@ def ACTOR_TEST_TIMEOUT = 30000 // 30 seconds timeout for actor tests
var pending_actor_tests = []
var actor_test_results = []
if (args.length > 0) {
// Check if current directory is a valid cell package
function is_valid_package(dir) {
if (!dir) dir = '.'
return fd.is_file(dir + '/cell.toml')
}
// Get current package name from cell.toml or null
function get_current_package_name() {
if (!is_valid_package('.')) return null
try {
var config = pkg.load_config(null)
return config.package || 'local'
} catch (e) {
return 'local'
}
}
// 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 <name> - run all tests for named package
// cell test package <name> <test> - run specific test in named package
// cell test package all - run all tests from all packages
function parse_args() {
if (args.length == 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 (args.length < 2) {
log.console(`Usage: cell test package <name>`)
$stop()
return
log.console('Usage: cell test package <name> [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 <name> [test]
var name = args[1]
var resolved = shop.resolve_alias(name, null)
if (resolved) {
target_pkg = resolved.pkg
log.console(`Testing package: ${resolved.alias} (${resolved.pkg})`)
// Check if package exists in lock or is a local path
var lock = shop.load_lock()
if (lock[name]) {
target_pkg = name
} else if (name.startsWith('/') && is_valid_package(name)) {
target_pkg = name
} else {
log.console(`Package not found: ${name}`)
$stop()
return
// Try to resolve as dependency alias from current package
if (is_valid_package('.')) {
var 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
}
}
} else if (args[0] == 'all') {
all_pkgs = true
log.console(`Testing all packages...`)
} else {
log.console(`Usage: cell test [package <name> | all]`)
$stop()
return
if (args.length >= 3) {
// cell test package <name> <test>
target_test = args[2]
}
log.console(`Testing package: ${target_pkg}`)
return true
}
// cell test tests/suite or cell test <path> - specific test file
var test_path = args[0]
// Normalize path - add tests/ prefix if not present and doesn't start with /
if (!test_path.startsWith('tests/') && !test_path.startsWith('/')) {
// 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
}
function ensure_dir(path) {
if (fd.is_dir(path)) return true
var parts = path.split('/')
var current = ''
var current = path.startsWith('/') ? '/' : ''
for (var i = 0; i < parts.length; i++) {
if (parts[i] == '') continue
current += `${parts[i]}/`
current += parts[i] + '/'
if (!fd.is_dir(current)) {
fd.mkdir(current)
}
@@ -56,23 +156,46 @@ function ensure_dir(path) {
return true
}
// Get the directory for a package
function get_pkg_dir(package_name) {
if (!package_name) {
return fd.realpath('.')
}
if (package_name.startsWith('/')) {
return package_name
}
return shop.get_package_dir(package_name)
}
// Collect .ce actor tests from a package
function collect_actor_tests(pkg) {
var prefix = pkg ? shop.get_shop_path() + '/modules/' + pkg : (shop.get_current_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 = shop.list_files(pkg)
var files = pkg.list_files(package_name)
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")) {
// If specific test requested, filter
if (specific_test) {
var test_name = f.substring(0, f.length - 3) // remove .ce
var match_name = specific_test
if (!match_name.startsWith('tests/')) match_name = 'tests/' + match_name
if (!match_name.endsWith('.ce')) match_name = match_name
// Match without extension
var test_base = test_name
var match_base = match_name.endsWith('.ce') ? match_name.substring(0, match_name.length - 3) : match_name
if (test_base != match_base) continue
}
actor_tests.push({
package: pkg || "local",
package: package_name || "local",
file: f,
path: pkg ? `${prefix}/${f}` : f
path: prefix + '/' + f
})
}
}
@@ -83,7 +206,7 @@ function collect_actor_tests(pkg) {
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,
@@ -92,10 +215,9 @@ function spawn_actor_test(test_info) {
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 = $start(actor_path)
pending_actor_tests.push(entry)
@@ -109,12 +231,12 @@ function spawn_actor_test(test_info) {
}
}
function run_tests(pkg) {
var prefix = pkg ? shop.get_shop_path() + '/modules/' + pkg : (shop.get_current_package() || '.')
function run_tests(package_name, specific_test) {
var prefix = get_pkg_dir(package_name)
var tests_dir = prefix + '/tests'
var pkg_result = {
package: pkg || "local",
package: package_name || "local",
files: [],
total: 0,
passed: 0,
@@ -123,25 +245,34 @@ function run_tests(pkg) {
if (!fd.is_dir(tests_dir)) return pkg_result
var files = shop.list_files(pkg)
var files = pkg.list_files(package_name)
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 (not .ce - those are actor tests)
if (f.startsWith("tests/") && f.endsWith(".cm")) {
// If specific test requested, filter
if (specific_test) {
var test_name = f.substring(0, f.length - 3) // remove .cm
var match_name = specific_test
if (!match_name.startsWith('tests/')) match_name = 'tests/' + match_name
// Match without extension
var match_base = match_name.endsWith('.cm') ? match_name.substring(0, match_name.length - 3) : match_name
if (test_name != match_base) continue
}
test_files.push(f)
}
}
if (test_files.length > 0) {
if (pkg) log.console(`Running tests for ${pkg}`)
if (package_name) log.console(`Running tests for ${package_name}`)
else log.console(`Running tests for local package`)
}
for (var i = 0; i < test_files.length; i++) {
var f = test_files[i]
var mod_path = f.substring(0, f.length - 3) // remove .cm
var file_result = {
name: f,
tests: [],
@@ -151,12 +282,10 @@ function run_tests(pkg) {
try {
var test_mod
if (pkg) {
test_mod = shop.use(mod_path, pkg)
} else {
test_mod = globalThis.use(mod_path)
}
// For local packages (null), use the current directory as package context
var use_pkg = package_name ? package_name : fd.realpath('.')
test_mod = shop.use(mod_path, use_pkg)
var tests = []
if (typeof test_mod == 'function') {
tests.push({name: 'main', fn: test_mod})
@@ -167,7 +296,7 @@ function run_tests(pkg) {
}
}
}
if (tests.length > 0) {
log.console(` ${f}`)
for (var j = 0; j < tests.length; j++) {
@@ -182,7 +311,7 @@ function run_tests(pkg) {
var start_time = time.number()
try {
var ret = t.fn()
if (typeof ret == 'string') {
throw new Error(ret)
} else if (ret && (typeof ret.message == 'string' || ret instanceof Error)) {
@@ -200,8 +329,7 @@ function run_tests(pkg) {
stack: e.stack || ""
}
if (e.name) test_entry.error.name = e.name
// If it's an object but not a native Error, try to extract more info
if (typeof e == 'object' && e.message) {
test_entry.error.message = e.message
}
@@ -216,7 +344,7 @@ function run_tests(pkg) {
}
var end_time = time.number()
test_entry.duration_ns = number.round((end_time - start_time) * 1000000000)
file_result.tests.push(test_entry)
pkg_result.total++
}
@@ -224,8 +352,6 @@ function run_tests(pkg) {
} catch (e) {
log.console(` Error loading ${f}: ${e}`)
// Treat load error as a file failure?
// Or maybe add a dummy test entry for "load"
var test_entry = {
package: pkg_result.package,
test: "load_module",
@@ -247,19 +373,21 @@ 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]))
// Run local first if we're in a valid package
if (is_valid_package('.')) {
all_results.push(run_tests(null, null))
all_actor_tests = all_actor_tests.concat(collect_actor_tests(null, null))
}
// Then all packages in lock
var packages = shop.list_packages()
for (var i = 0; i < packages.length; i++) {
all_results.push(run_tests(packages[i], null))
all_actor_tests = all_actor_tests.concat(collect_actor_tests(packages[i], null))
}
} else {
all_results.push(run_tests(target_pkg))
all_actor_tests = all_actor_tests.concat(collect_actor_tests(target_pkg))
all_results.push(run_tests(target_pkg, target_test))
all_actor_tests = all_actor_tests.concat(collect_actor_tests(target_pkg, target_test))
}
// Spawn actor tests if any
@@ -271,10 +399,7 @@ if (all_actor_tests.length > 0) {
}
// Handle messages from actor tests
// Actor tests should send either a single result `{ type: "test_result", ... }`
// or an array / object containing multiple results (e.g. {results:[...]})
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++) {
@@ -283,16 +408,15 @@ function handle_actor_message(msg) {
break
}
}
if (found_idx == -1) return // Unknown sender
if (found_idx == -1) return
var base_entry = pending_actor_tests[found_idx]
pending_actor_tests.splice(found_idx, 1)
var end_time = time.number()
var duration_ns = number.round((end_time - base_entry.start_time) * 1000000000)
// Normalize to an array of result objects
var results = []
if (isa(msg, array)) {
results = msg
@@ -301,7 +425,7 @@ function handle_actor_message(msg) {
} else {
results = [msg]
}
for (var i = 0; i < results.length; i++) {
var res = results[i] || {}
var entry = {
@@ -311,7 +435,7 @@ function handle_actor_message(msg) {
status: "failed",
duration_ns: duration_ns
}
if (res.type && res.type != "test_result") {
entry.error = { message: `Unexpected message type: ${res.type}` }
log.console(` FAIL ${entry.test}: unexpected message`)
@@ -323,10 +447,10 @@ function handle_actor_message(msg) {
if (res.stack) entry.error.stack = res.stack
log.console(` FAIL ${entry.test}: ${entry.error.message}`)
}
actor_test_results.push(entry)
}
check_completion()
}
@@ -334,7 +458,7 @@ function handle_actor_message(msg) {
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
@@ -342,19 +466,19 @@ function check_timeouts() {
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)
}
@@ -366,7 +490,7 @@ var finalized = false
function check_completion() {
if (finalized) return
if (pending_actor_tests.length > 0) return
finalized = true
finalize_results()
}
@@ -375,7 +499,6 @@ 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) {
@@ -387,8 +510,7 @@ function finalize_results() {
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) {
@@ -400,7 +522,7 @@ function finalize_results() {
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") {
@@ -411,7 +533,7 @@ function finalize_results() {
file_result.failed++
}
}
// Calculate totals
var totals = { total: 0, passed: 0, failed: 0 }
for (var i = 0; i < all_results.length; i++) {
@@ -422,14 +544,13 @@ function finalize_results() {
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
@@ -440,7 +561,6 @@ if (all_actor_tests.length == 0) {
log.console(`----------------------------------------`)
log.console(`Tests: ${totals.passed} passed, ${totals.failed} failed, ${totals.total} total`)
} else {
// Start timeout checker
$delay(check_timeouts, 1000)
}
@@ -451,7 +571,6 @@ function generate_reports(totals) {
var report_dir = shop.get_reports_dir() + '/test_' + timestamp
ensure_dir(report_dir)
// Generate test.txt
var txt_report = `TEST REPORT
Date: ${time.text(time.number())}
Total: ${totals.total}, Passed: ${totals.passed}, Failed: ${totals.failed}
@@ -497,7 +616,7 @@ Total: ${totals.total}, Passed: ${totals.passed}, Failed: ${totals.failed}
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++) {
@@ -508,7 +627,7 @@ Total: ${totals.total}, Passed: ${totals.passed}, Failed: ${totals.failed}
}
}
}
ensure_dir(report_dir)
fd.slurpwrite(`${report_dir}/test.txt`, utf8.encode(txt_report))
log.console(`Report written to ${report_dir}/test.txt`)
@@ -516,7 +635,7 @@ Total: ${totals.total}, Passed: ${totals.passed}, Failed: ${totals.failed}
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]
@@ -524,7 +643,7 @@ Total: ${totals.total}, Passed: ${totals.passed}, Failed: ${totals.failed}
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)))
}
@@ -535,7 +654,6 @@ 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)
})