new syntax for internals

This commit is contained in:
2026-02-10 11:03:01 -06:00
parent 6df3b741cf
commit 4deb0e2577
9 changed files with 483 additions and 292 deletions

View File

@@ -865,6 +865,7 @@ $_.clock(_ => {
var pkg = file_info ? file_info.package : null var pkg = file_info ? file_info.package : null
env.use = function(path) { return shop.use(path, pkg) } env.use = function(path) { return shop.use(path, pkg) }
env.args = _cell.args.arg env.args = _cell.args.arg
env.log = log
var script = text(fd.slurp(prog_path)) var script = text(fd.slurp(prog_path))
var ast = analyze(script, prog_path) var ast = analyze(script, prog_path)

Binary file not shown.

View File

@@ -35,7 +35,8 @@ function ensure_dir(path) {
if (fd.stat(path).isDirectory) return if (fd.stat(path).isDirectory) return
var parts = array(path, '/') var parts = array(path, '/')
var current = starts_with(path, '/') ? '/' : '' var current = starts_with(path, '/') ? '/' : ''
for (var i = 0; i < length(parts); i++) { var i = 0
for (i = 0; i < length(parts); i++) {
if (parts[i] == '') continue if (parts[i] == '') continue
current = current + parts[i] + '/' current = current + parts[i] + '/'
if (!fd.stat(current).isDirectory) { if (!fd.stat(current).isDirectory) {
@@ -119,12 +120,16 @@ function split_explicit_package_import(path)
if (!looks_explicit) return null if (!looks_explicit) return null
// Find the longest prefix that is an installed package // Find the longest prefix that is an installed package
for (var i = length(parts) - 1; i >= 1; i--) { var i = 0
var pkg_candidate = text(array(parts, 0, i), '/') var pkg_candidate = null
var mod_path = text(array(parts, i), '/') var mod_path = null
var candidate_dir = null
for (i = length(parts) - 1; i >= 1; i--) {
pkg_candidate = text(array(parts, 0, i), '/')
mod_path = text(array(parts, i), '/')
if (!mod_path || length(mod_path) == 0) continue if (!mod_path || length(mod_path) == 0) continue
var candidate_dir = get_packages_dir() + '/' + safe_package_path(pkg_candidate) candidate_dir = get_packages_dir() + '/' + safe_package_path(pkg_candidate)
if (fd.is_file(candidate_dir + '/cell.toml')) if (fd.is_file(candidate_dir + '/cell.toml'))
return {package: pkg_candidate, path: mod_path} return {package: pkg_candidate, path: mod_path}
@@ -158,8 +163,9 @@ function abs_path_to_package(package_dir)
return 'core' return 'core'
} }
// Also check if core_dir is a symlink pointing to package_dir // Also check if core_dir is a symlink pointing to package_dir
var core_target = null
if (fd.is_link(core_dir)) { if (fd.is_link(core_dir)) {
var core_target = fd.readlink(core_dir) core_target = fd.readlink(core_dir)
if (core_target == package_dir || fd.realpath(core_dir) == package_dir) { if (core_target == package_dir || fd.realpath(core_dir) == package_dir) {
return 'core' return 'core'
} }
@@ -181,9 +187,11 @@ function abs_path_to_package(package_dir)
// For local directories (e.g., linked targets), read the package name from cell.toml // For local directories (e.g., linked targets), read the package name from cell.toml
var _toml_path = package_dir + '/cell.toml' var _toml_path = package_dir + '/cell.toml'
var content = null
var cfg = null
if (fd.is_file(_toml_path)) { if (fd.is_file(_toml_path)) {
var content = text(fd.slurp(_toml_path)) content = text(fd.slurp(_toml_path))
var cfg = toml.decode(content) cfg = toml.decode(content)
if (cfg.package) if (cfg.package)
return cfg.package return cfg.package
} }
@@ -316,12 +324,16 @@ Shop.verify_package_name = function(pkg) {
// Convert module package to download URL // Convert module package to download URL
Shop.get_download_url = function(pkg, commit_hash) { Shop.get_download_url = function(pkg, commit_hash) {
var info = Shop.resolve_package_info(pkg) var info = Shop.resolve_package_info(pkg)
var parts = null
var host = null
var user = null
var repo = null
if (info == 'gitea') { if (info == 'gitea') {
var parts = array(pkg, '/') parts = array(pkg, '/')
var host = parts[0] host = parts[0]
var user = parts[1] user = parts[1]
var repo = parts[2] repo = parts[2]
return 'https://' + host + '/' + user + '/' + repo + '/archive/' + commit_hash + '.zip' return 'https://' + host + '/' + user + '/' + repo + '/archive/' + commit_hash + '.zip'
} }
@@ -332,12 +344,16 @@ Shop.get_download_url = function(pkg, commit_hash) {
// Get the API URL for checking remote git commits // Get the API URL for checking remote git commits
Shop.get_api_url = function(pkg) { Shop.get_api_url = function(pkg) {
var info = Shop.resolve_package_info(pkg) var info = Shop.resolve_package_info(pkg)
var parts = null
var host = null
var user = null
var repo = null
if (info == 'gitea') { if (info == 'gitea') {
var parts = array(pkg, '/') parts = array(pkg, '/')
var host = parts[0] host = parts[0]
var user = parts[1] user = parts[1]
var repo = parts[2] repo = parts[2]
return 'https://' + host + '/api/v1/repos/' + user + '/' + repo + '/branches/' return 'https://' + host + '/api/v1/repos/' + user + '/' + repo + '/branches/'
} }
@@ -393,9 +409,12 @@ function inject_env(inject) {
} }
// Add capability injections // Add capability injections
for (var i = 0; i < length(inject); i++) { var i = 0
var inj = inject[i] var inj = null
var key = trim(inj, '$') var key = null
for (i = 0; i < length(inject); i++) {
inj = inject[i]
key = trim(inj, '$')
if (key == 'fd') env[key] = fd if (key == 'fd') env[key] = fd
else env[key] = my$_[key] else env[key] = my$_[key]
} }
@@ -409,15 +428,19 @@ function inject_bindings_code(inject) {
var runtime_fns = ['logical', 'some', 'every', 'starts_with', 'ends_with', var runtime_fns = ['logical', 'some', 'every', 'starts_with', 'ends_with',
'actor', 'is_actor', 'log', 'send', 'actor', 'is_actor', 'log', 'send',
'fallback', 'parallel', 'race', 'sequence'] 'fallback', 'parallel', 'race', 'sequence']
for (var i = 0; i < length(runtime_fns); i++) { var i = 0
var fn = runtime_fns[i] var fn = null
var inj = null
var key = null
for (i = 0; i < length(runtime_fns); i++) {
fn = runtime_fns[i]
push(lines, `var ${fn} = env["${fn}"];`) push(lines, `var ${fn} = env["${fn}"];`)
} }
// Capability bindings ($delay, $start, etc.) // Capability bindings ($delay, $start, etc.)
for (var i = 0; i < length(inject); i++) { for (i = 0; i < length(inject); i++) {
var inj = inject[i] inj = inject[i]
var key = trim(inj, '$') key = trim(inj, '$')
push(lines, `var $${key} = env["${key}"];`) push(lines, `var $${key} = env["${key}"];`)
} }
return text(lines, '\n') return text(lines, '\n')
@@ -474,26 +497,34 @@ function resolve_mod_fn(path, pkg) {
// given a path and a package context // given a path and a package context
// return module info about where it was found // return module info about where it was found
function resolve_locator(path, ctx) function resolve_locator(path, ctx)
{ {
var explicit = split_explicit_package_import(path) var explicit = split_explicit_package_import(path)
var explicit_path = null
var fn = null
var core_dir = null
var core_file_path = null
var is_core = null
var scope = null
var alias_path = null
if (explicit) { if (explicit) {
if (is_internal_path(explicit.path) && ctx && explicit.package != ctx) if (is_internal_path(explicit.path) && ctx && explicit.package != ctx)
explicit = null explicit = null
} }
if (explicit) { if (explicit) {
var explicit_path = get_packages_dir() + '/' + safe_package_path(explicit.package) + '/' + explicit.path explicit_path = get_packages_dir() + '/' + safe_package_path(explicit.package) + '/' + explicit.path
if (fd.is_file(explicit_path)) { if (fd.is_file(explicit_path)) {
var fn = resolve_mod_fn(explicit_path, explicit.package) fn = resolve_mod_fn(explicit_path, explicit.package)
return {path: explicit_path, scope: SCOPE_PACKAGE, symbol: fn} return {path: explicit_path, scope: SCOPE_PACKAGE, symbol: fn}
} }
} }
// 1. If no context, resolve from core only // 1. If no context, resolve from core only
if (!ctx) { if (!ctx) {
var core_dir = Shop.get_core_dir() core_dir = Shop.get_core_dir()
var core_file_path = core_dir + '/' + path core_file_path = core_dir + '/' + path
if (fd.is_file(core_file_path)) { if (fd.is_file(core_file_path)) {
var fn = resolve_mod_fn(core_file_path, 'core') fn = resolve_mod_fn(core_file_path, 'core')
return {path: core_file_path, scope: SCOPE_CORE, symbol: fn} return {path: core_file_path, scope: SCOPE_CORE, symbol: fn}
} }
return null return null
@@ -502,7 +533,7 @@ function resolve_locator(path, ctx)
// check in ctx package // check in ctx package
// If ctx is an absolute path (starts with /), use it directly // If ctx is an absolute path (starts with /), use it directly
// Otherwise, look it up in the packages directory // Otherwise, look it up in the packages directory
var ctx_dir var ctx_dir = null
if (starts_with(ctx, '/')) { if (starts_with(ctx, '/')) {
ctx_dir = ctx ctx_dir = ctx
} else { } else {
@@ -511,10 +542,10 @@ function resolve_locator(path, ctx)
var ctx_path = ctx_dir + '/' + path var ctx_path = ctx_dir + '/' + path
if (fd.is_file(ctx_path)) { if (fd.is_file(ctx_path)) {
var fn = resolve_mod_fn(ctx_path, ctx) fn = resolve_mod_fn(ctx_path, ctx)
// Check if ctx is the core package (either by name or by path) // Check if ctx is the core package (either by name or by path)
var is_core = (ctx == 'core') || (ctx_dir == Shop.get_core_dir()) is_core = (ctx == 'core') || (ctx_dir == Shop.get_core_dir())
var scope = is_core ? SCOPE_CORE : SCOPE_LOCAL scope = is_core ? SCOPE_CORE : SCOPE_LOCAL
return {path: ctx_path, scope: scope, symbol: fn} return {path: ctx_path, scope: scope, symbol: fn}
} }
@@ -524,24 +555,24 @@ function resolve_locator(path, ctx)
// check for aliased dependency // check for aliased dependency
var alias = pkg_tools.split_alias(ctx, path) var alias = pkg_tools.split_alias(ctx, path)
if (alias) { if (alias) {
var alias_path = get_packages_dir() + '/' + safe_package_path(alias.package) + '/' + alias.path alias_path = get_packages_dir() + '/' + safe_package_path(alias.package) + '/' + alias.path
if (fd.is_file(alias_path)) { if (fd.is_file(alias_path)) {
var fn = resolve_mod_fn(alias_path, ctx) fn = resolve_mod_fn(alias_path, ctx)
return {path: alias_path, scope:SCOPE_PACKAGE, symbol:fn} return {path: alias_path, scope:SCOPE_PACKAGE, symbol:fn}
} }
} }
var package_path = get_packages_dir() + '/' + safe_package_path(path) var package_path = get_packages_dir() + '/' + safe_package_path(path)
if (fd.is_file(package_path)) { if (fd.is_file(package_path)) {
var fn = resolve_mod_fn(package_path, ctx) fn = resolve_mod_fn(package_path, ctx)
return {path: package_path, scope: SCOPE_PACKAGE, symbol: fn} return {path: package_path, scope: SCOPE_PACKAGE, symbol: fn}
} }
// 4. Check core as fallback // 4. Check core as fallback
var core_dir = Shop.get_core_dir() core_dir = Shop.get_core_dir()
var core_file_path = core_dir + '/' + path core_file_path = core_dir + '/' + path
if (fd.is_file(core_file_path)) { if (fd.is_file(core_file_path)) {
var fn = resolve_mod_fn(core_file_path, 'core') fn = resolve_mod_fn(core_file_path, 'core')
return {path: core_file_path, scope: SCOPE_CORE, symbol: fn} return {path: core_file_path, scope: SCOPE_CORE, symbol: fn}
} }
@@ -573,7 +604,7 @@ Shop.open_package_dylib = function(pkg) {
var link_target = link.get_target(pkg) var link_target = link.get_target(pkg)
var resolved_pkg = link_target ? link_target : pkg var resolved_pkg = link_target ? link_target : pkg
var pkg_dir; var pkg_dir = null
if (starts_with(resolved_pkg, '/')) { if (starts_with(resolved_pkg, '/')) {
pkg_dir = resolved_pkg pkg_dir = resolved_pkg
} else { } else {
@@ -581,9 +612,11 @@ Shop.open_package_dylib = function(pkg) {
} }
var toml_path = pkg_dir + '/cell.toml' var toml_path = pkg_dir + '/cell.toml'
var content = null
var cfg = null
if (fd.is_file(toml_path)) { if (fd.is_file(toml_path)) {
var content = text(fd.slurp(toml_path)) content = text(fd.slurp(toml_path))
var cfg = toml.decode(content) cfg = toml.decode(content)
if (cfg.dependencies) { if (cfg.dependencies) {
arrfor(array(cfg.dependencies), function(alias, i) { arrfor(array(cfg.dependencies), function(alias, i) {
var dep_pkg = cfg.dependencies[alias] var dep_pkg = cfg.dependencies[alias]
@@ -606,12 +639,19 @@ Shop.open_package_dylib = function(pkg) {
// Core is never loaded as a dynamic library via dlopen // Core is never loaded as a dynamic library via dlopen
function resolve_c_symbol(path, package_context) { function resolve_c_symbol(path, package_context) {
var explicit = split_explicit_package_import(path) var explicit = split_explicit_package_import(path)
var sym = null
var dl_path = null
var _path = null
var core_sym = null
var canon_pkg = null
var mod_name = null
if (explicit) { if (explicit) {
if (is_internal_path(explicit.path) && package_context && explicit.package != package_context) if (is_internal_path(explicit.path) && package_context && explicit.package != package_context)
explicit = null explicit = null
} }
if (explicit) { if (explicit) {
var sym = make_c_symbol(explicit.package, explicit.path) sym = make_c_symbol(explicit.package, explicit.path)
if (os.internal_exists(sym)) { if (os.internal_exists(sym)) {
return { return {
symbol: function() { return os.load_internal(sym) }, symbol: function() { return os.load_internal(sym) },
@@ -622,7 +662,7 @@ function resolve_c_symbol(path, package_context) {
} }
Shop.open_package_dylib(explicit.package) Shop.open_package_dylib(explicit.package)
var dl_path = get_lib_path(explicit.package) dl_path = get_lib_path(explicit.package)
if (open_dls[dl_path] && os.dylib_has_symbol(open_dls[dl_path], sym)) { if (open_dls[dl_path] && os.dylib_has_symbol(open_dls[dl_path], sym)) {
return { return {
symbol: function() { return os.dylib_symbol(open_dls[dl_path], sym) }, symbol: function() { return os.dylib_symbol(open_dls[dl_path], sym) },
@@ -635,8 +675,8 @@ function resolve_c_symbol(path, package_context) {
// If no package context, only check core internal symbols // If no package context, only check core internal symbols
if (!package_context || package_context == 'core') { if (!package_context || package_context == 'core') {
var _path = replace(path, '/', '_') _path = replace(path, '/', '_')
var core_sym = `js_${_path}_use` core_sym = `js_${_path}_use`
if (os.internal_exists(core_sym)) { if (os.internal_exists(core_sym)) {
return { return {
symbol: function() { return os.load_internal(core_sym) }, symbol: function() { return os.load_internal(core_sym) },
@@ -648,7 +688,7 @@ function resolve_c_symbol(path, package_context) {
} }
// 1. Check own package first (internal, then dylib) // 1. Check own package first (internal, then dylib)
var sym = make_c_symbol(package_context, path) sym = make_c_symbol(package_context, path)
if (os.internal_exists(sym)) { if (os.internal_exists(sym)) {
return { return {
symbol: function() { return os.load_internal(sym) }, symbol: function() { return os.load_internal(sym) },
@@ -658,7 +698,7 @@ function resolve_c_symbol(path, package_context) {
} }
Shop.open_package_dylib(package_context) Shop.open_package_dylib(package_context)
var dl_path = get_lib_path(package_context) dl_path = get_lib_path(package_context)
if (open_dls[dl_path] && os.dylib_has_symbol(open_dls[dl_path], sym)) { if (open_dls[dl_path] && os.dylib_has_symbol(open_dls[dl_path], sym)) {
return { return {
@@ -674,10 +714,10 @@ function resolve_c_symbol(path, package_context) {
// 2. Check aliased package imports (e.g. 'prosperon/sprite') // 2. Check aliased package imports (e.g. 'prosperon/sprite')
var pkg_alias = get_import_package(path) var pkg_alias = get_import_package(path)
if (pkg_alias) { if (pkg_alias) {
var canon_pkg = get_aliased_package(path, package_context) canon_pkg = get_aliased_package(path, package_context)
if (canon_pkg) { if (canon_pkg) {
var mod_name = get_import_name(path) mod_name = get_import_name(path)
var sym = make_c_symbol(canon_pkg, mod_name) sym = make_c_symbol(canon_pkg, mod_name)
// Check internal first // Check internal first
if (os.internal_exists(sym)) { if (os.internal_exists(sym)) {
@@ -691,7 +731,7 @@ function resolve_c_symbol(path, package_context) {
// Then check dylib // Then check dylib
Shop.open_package_dylib(canon_pkg) Shop.open_package_dylib(canon_pkg)
var dl_path = get_lib_path(canon_pkg) dl_path = get_lib_path(canon_pkg)
if (open_dls[dl_path] && os.dylib_has_symbol(open_dls[dl_path], sym)) { if (open_dls[dl_path] && os.dylib_has_symbol(open_dls[dl_path], sym)) {
return { return {
symbol: function() { return os.dylib_symbol(open_dls[dl_path], sym) }, symbol: function() { return os.dylib_symbol(open_dls[dl_path], sym) },
@@ -704,7 +744,7 @@ function resolve_c_symbol(path, package_context) {
} }
// 3. Check core internal symbols (core is never a dynamic library) // 3. Check core internal symbols (core is never a dynamic library)
var core_sym = `js_${replace(path, '/', '_')}_use` core_sym = `js_${replace(path, '/', '_')}_use`
if (os.internal_exists(core_sym)) { if (os.internal_exists(core_sym)) {
return { return {
symbol: function() { return os.load_internal(core_sym) }, symbol: function() { return os.load_internal(core_sym) },
@@ -732,31 +772,37 @@ function resolve_module_info(path, package_context) {
if (min_scope == 999) if (min_scope == 999)
return null return null
var cache_key var cache_key = null
var real_path = null
var real_info = null
var pkg_alias = null
var canon_pkg = null
var mod_name = null
if (mod_resolve.scope == SCOPE_CORE) { if (mod_resolve.scope == SCOPE_CORE) {
cache_key = 'core/' + path cache_key = 'core/' + path
} else if (mod_resolve.scope < 900 && mod_resolve.path) { } else if (mod_resolve.scope < 900 && mod_resolve.path) {
var real_path = fd.realpath(mod_resolve.path) real_path = fd.realpath(mod_resolve.path)
if (real_path) { if (real_path) {
var real_info = Shop.file_info(real_path) real_info = Shop.file_info(real_path)
if (real_info.package && real_info.name) if (real_info.package && real_info.name)
cache_key = real_info.package + '/' + real_info.name cache_key = real_info.package + '/' + real_info.name
else else
cache_key = real_path cache_key = real_path
} }
} }
if (!cache_key) { if (!cache_key) {
if (min_scope == SCOPE_CORE) if (min_scope == SCOPE_CORE)
cache_key = 'core/' + path cache_key = 'core/' + path
else if (min_scope == SCOPE_LOCAL && package_context) else if (min_scope == SCOPE_LOCAL && package_context)
cache_key = package_context + '/' + path cache_key = package_context + '/' + path
else if (min_scope == SCOPE_PACKAGE) { else if (min_scope == SCOPE_PACKAGE) {
var pkg_alias = get_import_package(path) pkg_alias = get_import_package(path)
if (pkg_alias) { if (pkg_alias) {
var canon_pkg = get_canonical_package(pkg_alias, package_context) canon_pkg = get_canonical_package(pkg_alias, package_context)
if (canon_pkg) { if (canon_pkg) {
var mod_name = get_import_name(path) mod_name = get_import_name(path)
cache_key = canon_pkg + '/' + mod_name cache_key = canon_pkg + '/' + mod_name
} else } else
cache_key = path cache_key = path
@@ -807,20 +853,26 @@ function execute_module(info)
var c_resolve = info.c_resolve var c_resolve = info.c_resolve
var mod_resolve = info.mod_resolve var mod_resolve = info.mod_resolve
var used var used = null
var context = null
var file_info = null
var inject = null
var env = null
var pkg = null
var use_fn = null
if (mod_resolve.scope < 900) { if (mod_resolve.scope < 900) {
var context = null context = null
if (c_resolve.scope < 900) { if (c_resolve.scope < 900) {
context = call_c_module(c_resolve) context = call_c_module(c_resolve)
} }
// Get file info to determine inject list // Get file info to determine inject list
var file_info = Shop.file_info(mod_resolve.path) file_info = Shop.file_info(mod_resolve.path)
var inject = Shop.script_inject_for(file_info) inject = Shop.script_inject_for(file_info)
var env = inject_env(inject) env = inject_env(inject)
var pkg = file_info.package pkg = file_info.package
var use_fn = make_use_fn(pkg) use_fn = make_use_fn(pkg)
// Call with signature: setup_module(args, use, env) // Call with signature: setup_module(args, use, env)
// args is null for module loading // args is null for module loading
@@ -942,17 +994,18 @@ Shop.fetch = function(pkg) {
// Check if we have the zip cached // Check if we have the zip cached
var zip_blob = get_cached_zip(pkg, commit) var zip_blob = get_cached_zip(pkg, commit)
var actual_hash = null
if (zip_blob) { if (zip_blob) {
// If we have a hash on record, verify it // If we have a hash on record, verify it
if (expected_hash) { if (expected_hash) {
var actual_hash = text(crypto.blake2(zip_blob), 'h') actual_hash = text(crypto.blake2(zip_blob), 'h')
if (actual_hash == expected_hash) { if (actual_hash == expected_hash) {
return { status: 'cached' } return { status: 'cached' }
} }
log.console("Zip hash mismatch for " + pkg + ", re-fetching...") log.console("Zip hash mismatch for " + pkg + ", re-fetching...")
} else { } else {
// No hash stored yet - compute and store it // No hash stored yet - compute and store it
var actual_hash = text(crypto.blake2(zip_blob), 'h') actual_hash = text(crypto.blake2(zip_blob), 'h')
lock_entry.zip_hash = actual_hash lock_entry.zip_hash = actual_hash
Shop.save_lock(lock) Shop.save_lock(lock)
return { status: 'cached' } return { status: 'cached' }
@@ -1004,10 +1057,12 @@ Shop.extract = function(pkg) {
// Check if already extracted at correct commit // Check if already extracted at correct commit
var lock = Shop.load_lock() var lock = Shop.load_lock()
var lock_entry = lock[pkg] var lock_entry = lock[pkg]
var extracted_commit_file = null
var extracted_commit = null
if (lock_entry && lock_entry.commit) { if (lock_entry && lock_entry.commit) {
var extracted_commit_file = target_dir + '/.cell_commit' extracted_commit_file = target_dir + '/.cell_commit'
if (fd.is_file(extracted_commit_file)) { if (fd.is_file(extracted_commit_file)) {
var extracted_commit = trim(text(fd.slurp(extracted_commit_file))) extracted_commit = trim(text(fd.slurp(extracted_commit_file)))
if (extracted_commit == lock_entry.commit) { if (extracted_commit == lock_entry.commit) {
// Already extracted at this commit, skip // Already extracted at this commit, skip
return true return true
@@ -1059,6 +1114,7 @@ Shop.update = function(pkg) {
log.console(`checking ${pkg}`) log.console(`checking ${pkg}`)
var new_entry = null
if (info == 'local') { if (info == 'local') {
// Check if local path exists // Check if local path exists
if (!fd.is_dir(pkg)) { if (!fd.is_dir(pkg)) {
@@ -1066,7 +1122,7 @@ Shop.update = function(pkg) {
return null return null
} }
// Local packages always get a lock entry // Local packages always get a lock entry
var new_entry = { new_entry = {
type: 'local', type: 'local',
updated: time.number() updated: time.number()
} }
@@ -1089,7 +1145,7 @@ Shop.update = function(pkg) {
if (local_commit == remote_commit) if (local_commit == remote_commit)
return null return null
var new_entry = { new_entry = {
type: info, type: info,
commit: remote_commit, commit: remote_commit,
updated: time.number() updated: time.number()
@@ -1114,21 +1170,28 @@ function install_zip(zip_blob, target_dir) {
var count = zip.count() var count = zip.count()
var created_dirs = {} var created_dirs = {}
for (var i = 0; i < count; i++) { var i = 0
var filename = null
var slash_pos = null
var rel_path = null
var full_path = null
var dir_path = null
var file_data = null
for (i = 0; i < count; i++) {
if (zip.is_directory(i)) continue if (zip.is_directory(i)) continue
var filename = zip.get_filename(i) filename = zip.get_filename(i)
var slash_pos = search(filename, '/') slash_pos = search(filename, '/')
if (slash_pos == null) continue if (slash_pos == null) continue
if (slash_pos + 1 >= length(filename)) continue if (slash_pos + 1 >= length(filename)) continue
var rel_path = text(filename, slash_pos + 1) rel_path = text(filename, slash_pos + 1)
var full_path = target_dir + '/' + rel_path full_path = target_dir + '/' + rel_path
var dir_path = fd.dirname(full_path) dir_path = fd.dirname(full_path)
if (!created_dirs[dir_path]) { if (!created_dirs[dir_path]) {
ensure_dir(dir_path) ensure_dir(dir_path)
created_dirs[dir_path] = true created_dirs[dir_path] = true
} }
var file_data = zip.slurp(filename) file_data = zip.slurp(filename)
stone(file_data) stone(file_data)
@@ -1151,14 +1214,16 @@ Shop.remove = function(pkg) {
Shop.get = function(pkg) { Shop.get = function(pkg) {
var lock = Shop.load_lock() var lock = Shop.load_lock()
var info = null
var commit = null
if (!lock[pkg]) { if (!lock[pkg]) {
var info = Shop.resolve_package_info(pkg) info = Shop.resolve_package_info(pkg)
if (!info) { if (!info) {
print("Invalid package: " + pkg); disrupt print("Invalid package: " + pkg); disrupt
} }
var commit = null commit = null
if (info != 'local') { if (info != 'local') {
commit = fetch_remote_hash(pkg) commit = fetch_remote_hash(pkg)
if (!commit) { if (!commit) {
@@ -1216,9 +1281,11 @@ function get_package_scripts(package)
{ {
var files = pkg_tools.list_files(package) var files = pkg_tools.list_files(package)
var scripts = [] var scripts = []
for (var i = 0; i < length(files); i++) { var i = 0
var file = files[i] var file = null
for (i = 0; i < length(files); i++) {
file = files[i]
if (ends_with(file, '.cm') || ends_with(file, '.ce')) { if (ends_with(file, '.cm') || ends_with(file, '.ce')) {
push(scripts, file) push(scripts, file)
} }

View File

@@ -4,19 +4,21 @@ var pkg = use('package')
// Check if current directory is a valid cell package // Check if current directory is a valid cell package
function is_valid_package(dir) { function is_valid_package(dir) {
if (!dir) dir = '.' var _dir = dir == null ? '.' : dir
return fd.is_file(dir + '/cell.toml') if (!_dir) _dir = '.'
return fd.is_file(_dir + '/cell.toml')
} }
// Get current package name from cell.toml or null // Get current package name from cell.toml or null
function get_current_package_name() { function get_current_package_name() {
if (!is_valid_package('.')) return null if (!is_valid_package('.')) return null
try { var _load = function() {
var config = pkg.load_config(null) var config = pkg.load_config(null)
return config.package || 'local' return config.package || 'local'
} catch (e) { } disruption {
return 'local' return 'local'
} }
return _load()
} }
// Get the directory for a package // Get the directory for a package
@@ -37,9 +39,10 @@ function ensure_dir(path) {
var parts = array(path, '/') var parts = array(path, '/')
var current = starts_with(path, '/') ? '/' : '' var current = starts_with(path, '/') ? '/' : ''
for (var i = 0; i < length(parts); i++) { var i = 0
for (i = 0; i < length(parts); i++) {
if (parts[i] == '') continue if (parts[i] == '') continue
current += parts[i] + '/' current = current + parts[i] + '/'
if (!fd.is_dir(current)) { if (!fd.is_dir(current)) {
fd.mkdir(current) fd.mkdir(current)
} }

View File

@@ -34,7 +34,8 @@ function ensure_dir(path) {
if (fd.stat(path).isDirectory) return if (fd.stat(path).isDirectory) return
var parts = array(path, '/') var parts = array(path, '/')
var current = starts_with(path, '/') ? '/' : '' var current = starts_with(path, '/') ? '/' : ''
for (var i = 0; i < length(parts); i++) { var i = 0
for (i = 0; i < length(parts); i++) {
if (parts[i] == '') continue if (parts[i] == '') continue
current = current + parts[i] + '/' current = current + parts[i] + '/'
if (!fd.stat(current).isDirectory) { if (!fd.stat(current).isDirectory) {
@@ -119,8 +120,9 @@ Link.add = function(canonical, target, shop) {
// Read the target's cell.toml to find its dependencies // Read the target's cell.toml to find its dependencies
var target_path = starts_with(target, '/') ? target : get_package_abs_dir(target) var target_path = starts_with(target, '/') ? target : get_package_abs_dir(target)
var toml_path = target_path + '/cell.toml' var toml_path = target_path + '/cell.toml'
var _install_deps = null
if (fd.is_file(toml_path)) { if (fd.is_file(toml_path)) {
var _install_deps = function() { _install_deps = function() {
var content = text(fd.slurp(toml_path)) var content = text(fd.slurp(toml_path))
var cfg = toml.decode(content) var cfg = toml.decode(content)
if (cfg && cfg.dependencies) { if (cfg && cfg.dependencies) {

View File

@@ -101,11 +101,12 @@ package.alias_to_package = function(name, alias)
} }
// alias is optional // alias is optional
package.add_dependency = function(name, locator, alias = locator) package.add_dependency = function(name, locator, alias)
{ {
var _alias = alias == null ? locator : alias
var config = package.load_config(name) var config = package.load_config(name)
if (!config.dependencies) config.dependencies = {} if (!config.dependencies) config.dependencies = {}
config.dependencies[alias] = locator config.dependencies[_alias] = locator
package.save_config(name, config) package.save_config(name, config)
} }
@@ -115,10 +116,11 @@ package.remove_dependency = function(name, locator)
var config = package.load_config(name) var config = package.load_config(name)
if (!config.dependencies) return if (!config.dependencies) return
var alias = null
if (config.dependencies[locator]) if (config.dependencies[locator])
delete config.dependencies[locator] delete config.dependencies[locator]
else { else {
var alias = package.find_alias(name, locator) alias = package.find_alias(name, locator)
if (alias) if (alias)
delete config.dependencies[alias] delete config.dependencies[alias]
} }
@@ -133,8 +135,9 @@ package.find_package_dir = function(file)
if (fd.is_file(dir)) if (fd.is_file(dir))
dir = fd.dirname(dir) dir = fd.dirname(dir)
var toml_path = null
while (dir && length(dir) > 0) { while (dir && length(dir) > 0) {
var toml_path = dir + '/cell.toml' toml_path = dir + '/cell.toml'
if (fd.is_file(toml_path)) { if (fd.is_file(toml_path)) {
return dir return dir
} }
@@ -163,9 +166,11 @@ package.split_alias = function(name, path)
if (!config) return null if (!config) return null
var deps = config.dependencies var deps = config.dependencies
var dep_locator = null
var remaining_path = null
if (deps && deps[first_part]) { if (deps && deps[first_part]) {
var dep_locator = deps[first_part] dep_locator = deps[first_part]
var remaining_path = text(array(parts, 1), '/') remaining_path = text(array(parts, 1), '/')
return { package: dep_locator, path: remaining_path } return { package: dep_locator, path: remaining_path }
} }
return null return null
@@ -208,18 +213,23 @@ package.list_files = function(pkg) {
var walk = function(current_dir, current_prefix) { var walk = function(current_dir, current_prefix) {
var list = fd.readdir(current_dir) var list = fd.readdir(current_dir)
if (!list) return if (!list) return
for (var i = 0; i < length(list); i++) { var i = 0
var item = list[i] var item = null
var full_path = null
var rel_path = null
var st = null
for (i = 0; i < length(list); i++) {
item = list[i]
if (item == '.' || item == '..') continue if (item == '.' || item == '..') continue
if (starts_with(item, '.')) continue if (starts_with(item, '.')) continue
// Skip build directories in root // Skip build directories in root
var full_path = current_dir + "/" + item full_path = current_dir + "/" + item
var rel_path = current_prefix ? current_prefix + "/" + item : item rel_path = current_prefix ? current_prefix + "/" + item : item
var st = fd.stat(full_path) st = fd.stat(full_path)
if (st.isDirectory) { if (st.isDirectory) {
walk(full_path, rel_path) walk(full_path, rel_path)
} else { } else {
@@ -237,7 +247,8 @@ package.list_files = function(pkg) {
package.list_modules = function(name) { package.list_modules = function(name) {
var files = package.list_files(name) var files = package.list_files(name)
var modules = [] var modules = []
for (var i = 0; i < length(files); i++) { var i = 0
for (i = 0; i < length(files); i++) {
if (ends_with(files[i], '.cm')) { if (ends_with(files[i], '.cm')) {
push(modules, text(files[i], 0, -3)) push(modules, text(files[i], 0, -3))
} }
@@ -248,7 +259,8 @@ package.list_modules = function(name) {
package.list_programs = function(name) { package.list_programs = function(name) {
var files = package.list_files(name) var files = package.list_files(name)
var programs = [] var programs = []
for (var i = 0; i < length(files); i++) { var i = 0
for (i = 0; i < length(files); i++) {
if (ends_with(files[i], '.ce')) { if (ends_with(files[i], '.ce')) {
push(programs, text(files[i], 0, -3)) push(programs, text(files[i], 0, -3))
} }
@@ -265,14 +277,16 @@ package.get_flags = function(name, flag_type, target) {
var flags = [] var flags = []
// Base flags // Base flags
var base = null
var target_flags = null
if (config.compilation && config.compilation[flag_type]) { if (config.compilation && config.compilation[flag_type]) {
var base = config.compilation[flag_type] base = config.compilation[flag_type]
flags = array(flags, filter(array(base, /\s+/), function(f) { return length(f) > 0 })) flags = array(flags, filter(array(base, /\s+/), function(f) { return length(f) > 0 }))
} }
// Target-specific flags // Target-specific flags
if (target && config.compilation && config.compilation[target] && config.compilation[target][flag_type]) { if (target && config.compilation && config.compilation[target] && config.compilation[target][flag_type]) {
var target_flags = config.compilation[target][flag_type] target_flags = config.compilation[target][flag_type]
flags = array(flags, filter(array(target_flags, /\s+/), function(f) { return length(f) > 0 })) flags = array(flags, filter(array(target_flags, /\s+/), function(f) { return length(f) > 0 }))
} }
@@ -290,23 +304,36 @@ package.get_c_files = function(name, target, exclude_main) {
// Group files by their base name (without target suffix) // Group files by their base name (without target suffix)
var groups = {} // base_key -> { generic: file, variants: { target: file } } var groups = {} // base_key -> { generic: file, variants: { target: file } }
for (var i = 0; i < length(files); i++) { var i = 0
var file = files[i] var file = null
var ext = null
var base = null
var name_part = null
var dir_part = null
var dir = null
var is_variant = null
var variant_target = null
var generic_name = null
var t = 0
var suffix = null
var group_key = null
for (i = 0; i < length(files); i++) {
file = files[i]
if (!ends_with(file, '.c') && !ends_with(file, '.cpp')) continue if (!ends_with(file, '.c') && !ends_with(file, '.cpp')) continue
var ext = ends_with(file, '.cpp') ? '.cpp' : '.c' ext = ends_with(file, '.cpp') ? '.cpp' : '.c'
var base = text(file, 0, -length(ext)) base = text(file, 0, -length(ext))
var name_part = fd.basename(base) name_part = fd.basename(base)
var dir_part = fd.dirname(base) dir_part = fd.dirname(base)
var dir = (dir_part && dir_part != '.') ? dir_part + '/' : '' dir = (dir_part && dir_part != '.') ? dir_part + '/' : ''
// Check for target suffix // Check for target suffix
var is_variant = false is_variant = false
var variant_target = null variant_target = null
var generic_name = name_part generic_name = name_part
for (var t = 0; t < length(known_targets); t++) { for (t = 0; t < length(known_targets); t++) {
var suffix = '_' + known_targets[t] suffix = '_' + known_targets[t]
if (ends_with(name_part, suffix)) { if (ends_with(name_part, suffix)) {
is_variant = true is_variant = true
variant_target = known_targets[t] variant_target = known_targets[t]
@@ -315,7 +342,7 @@ package.get_c_files = function(name, target, exclude_main) {
} }
} }
var group_key = dir + generic_name + ext group_key = dir + generic_name + ext
if (!groups[group_key]) { if (!groups[group_key]) {
groups[group_key] = { generic: null, variants: {} } groups[group_key] = { generic: null, variants: {} }
} }
@@ -332,18 +359,19 @@ package.get_c_files = function(name, target, exclude_main) {
arrfor(array(groups), function(key) { arrfor(array(groups), function(key) {
var group = groups[key] var group = groups[key]
var selected = null var selected = null
var basename = null
// Prefer target-specific variant if available // Prefer target-specific variant if available
if (target && group.variants[target]) { if (target && group.variants[target]) {
selected = group.variants[target] selected = group.variants[target]
} else if (group.generic) { } else if (group.generic) {
selected = group.generic selected = group.generic
} }
if (selected) { if (selected) {
// Skip main.c if requested // Skip main.c if requested
if (exclude_main) { if (exclude_main) {
var basename = fd.basename(selected) basename = fd.basename(selected)
if (basename == 'main.c' || starts_with(basename, 'main_')) return if (basename == 'main.c' || starts_with(basename, 'main_')) return
} }
push(result, selected) push(result, selected)

301
test.ce
View File

@@ -8,7 +8,7 @@ var dbg = use('js')
// run gc with dbg.gc() // run gc with dbg.gc()
if (!args) args = [] var _args = args == null ? [] : args
var target_pkg = null // null = current package var target_pkg = null // null = current package
var target_test = null // null = all tests, otherwise specific test file var target_test = null // null = all tests, otherwise specific test file
@@ -22,19 +22,20 @@ var actor_test_results = []
// Check if current directory is a valid cell package // Check if current directory is a valid cell package
function is_valid_package(dir) { function is_valid_package(dir) {
if (!dir) dir = '.' var _dir = dir == null ? '.' : dir
return fd.is_file(dir + '/cell.toml') return fd.is_file(_dir + '/cell.toml')
} }
// Get current package name from cell.toml or null // Get current package name from cell.toml or null
function get_current_package_name() { function get_current_package_name() {
if (!is_valid_package('.')) return null if (!is_valid_package('.')) return null
try { var _load = function() {
var config = pkg.load_config(null) var config = pkg.load_config(null)
return config.package || 'local' return config.package || 'local'
} catch (e) { } disruption {
return 'local' return 'local'
} }
return _load()
} }
// Parse arguments // Parse arguments
@@ -48,16 +49,21 @@ function get_current_package_name() {
function parse_args() { function parse_args() {
var cleaned_args = [] var cleaned_args = []
for (var i = 0; i < length(args); i++) { var i = 0
if (args[i] == '-g') { 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 gc_after_each_test = true
} else { } else {
push(cleaned_args, args[i]) push(cleaned_args, _args[i])
} }
} }
args = cleaned_args _args = cleaned_args
if (length(args) == 0) { if (length(_args) == 0) {
// cell test - run all tests for current package // cell test - run all tests for current package
if (!is_valid_package('.')) { if (!is_valid_package('.')) {
log.console('No cell.toml found in current directory') log.console('No cell.toml found in current directory')
@@ -67,7 +73,7 @@ function parse_args() {
return true return true
} }
if (args[0] == 'all') { if (_args[0] == 'all') {
// cell test all - run all tests for current package // cell test all - run all tests for current package
if (!is_valid_package('.')) { if (!is_valid_package('.')) {
log.console('No cell.toml found in current directory') log.console('No cell.toml found in current directory')
@@ -77,14 +83,14 @@ function parse_args() {
return true return true
} }
if (args[0] == 'package') { if (_args[0] == 'package') {
if (length(args) < 2) { if (length(_args) < 2) {
log.console('Usage: cell test package <name> [test]') log.console('Usage: cell test package <name> [test]')
log.console(' cell test package all') log.console(' cell test package all')
return false return false
} }
if (args[1] == 'all') { if (_args[1] == 'all') {
// cell test package all - run tests from all packages // cell test package all - run tests from all packages
all_pkgs = true all_pkgs = true
log.console('Testing all packages...') log.console('Testing all packages...')
@@ -92,18 +98,19 @@ function parse_args() {
} }
// cell test package <name> [test] // cell test package <name> [test]
var name = args[1] name = _args[1]
// Check if package exists in lock or is a local path // Check if package exists in lock or is a local path
var lock = shop.load_lock() lock = shop.load_lock()
if (lock[name]) { if (lock[name]) {
target_pkg = name target_pkg = name
} else if (starts_with(name, '/') && is_valid_package(name)) { } else if (starts_with(name, '/') && is_valid_package(name)) {
target_pkg = name target_pkg = name
} else { } else {
// Try to resolve as dependency alias from current package // Try to resolve as dependency alias from current package
resolved = null
if (is_valid_package('.')) { if (is_valid_package('.')) {
var resolved = pkg.alias_to_package(null, name) resolved = pkg.alias_to_package(null, name)
if (resolved) { if (resolved) {
target_pkg = resolved target_pkg = resolved
} else { } else {
@@ -116,9 +123,9 @@ function parse_args() {
} }
} }
if (length(args) >= 3) { if (length(_args) >= 3) {
// cell test package <name> <test> // cell test package <name> <test>
target_test = args[2] target_test = _args[2]
} }
log.console(`Testing package: ${target_pkg}`) log.console(`Testing package: ${target_pkg}`)
@@ -126,7 +133,7 @@ function parse_args() {
} }
// cell test tests/suite or cell test <path> - specific test file // cell test tests/suite or cell test <path> - specific test file
var test_path = args[0] test_path = _args[0]
// Normalize path - add tests/ prefix if not present and doesn't start with / // Normalize path - add tests/ prefix if not present and doesn't start with /
if (!starts_with(test_path, 'tests/') && !starts_with(test_path, '/')) { if (!starts_with(test_path, 'tests/') && !starts_with(test_path, '/')) {
@@ -160,9 +167,10 @@ function ensure_dir(path) {
var parts = array(path, '/') var parts = array(path, '/')
var current = starts_with(path, '/') ? '/' : '' var current = starts_with(path, '/') ? '/' : ''
for (var i = 0; i < length(parts); i++) { var i = 0
for (i = 0; i < length(parts); i++) {
if (parts[i] == '') continue if (parts[i] == '') continue
current += parts[i] + '/' current = current + parts[i] + '/'
if (!fd.is_dir(current)) if (!fd.is_dir(current))
fd.mkdir(current) fd.mkdir(current)
} }
@@ -189,23 +197,29 @@ function collect_actor_tests(package_name, specific_test) {
var files = pkg.list_files(package_name) var files = pkg.list_files(package_name)
var actor_tests = [] var actor_tests = []
for (var i = 0; i < length(files); i++) { var i = 0
var f = files[i] 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 // Check if file is in tests/ folder and is a .ce actor
if (starts_with(f, "tests/") && ends_with(f, ".ce")) { if (starts_with(f, "tests/") && ends_with(f, ".ce")) {
// If specific test requested, filter // If specific test requested, filter
if (specific_test) { if (specific_test) {
var test_name = text(f, 0, -3) // remove .ce test_name = text(f, 0, -3) // remove .ce
var match_name = specific_test match_name = specific_test
if (!starts_with(match_name, 'tests/')) match_name = 'tests/' + match_name if (!starts_with(match_name, 'tests/')) match_name = 'tests/' + match_name
if (!ends_with(match_name, '.ce')) match_name = match_name if (!ends_with(match_name, '.ce')) match_name = match_name
// Match without extension // Match without extension
var test_base = test_name test_base = test_name
var match_base = ends_with(match_name, '.ce') ? text(match_name, 0, -3) : match_name match_base = ends_with(match_name, '.ce') ? text(match_name, 0, -3) : match_name
if (test_base != match_base) continue if (test_base != match_base) continue
} }
push(actor_tests,{ push(actor_tests, {
package: package_name || "local", package: package_name || "local",
file: f, file: f,
path: prefix + '/' + f path: prefix + '/' + f
@@ -229,19 +243,19 @@ function spawn_actor_test(test_info) {
actor: null actor: null
} }
try { var _spawn = function() {
// Spawn the actor test - it should send back results
var actor_path = text(test_info.path, 0, -3) // remove .ce var actor_path = text(test_info.path, 0, -3) // remove .ce
entry.actor = $start(actor_path) entry.actor = $start(actor_path)
push(pending_actor_tests, entry) push(pending_actor_tests, entry)
} catch (e) { } disruption {
entry.status = "failed" entry.status = "failed"
entry.error = { message: `Failed to spawn actor: ${e}` } entry.error = { message: `Failed to spawn actor` }
entry.duration_ns = 0 entry.duration_ns = 0
push(actor_test_results, entry) push(actor_test_results, entry)
log.console(` FAIL ${test_name}: `) log.console(` FAIL ${test_name}: `)
log.error(e) log.error()
} }
_spawn()
} }
function run_tests(package_name, specific_test) { function run_tests(package_name, specific_test) {
@@ -260,17 +274,24 @@ function run_tests(package_name, specific_test) {
var files = pkg.list_files(package_name) var files = pkg.list_files(package_name)
var test_files = [] var test_files = []
for (var i = 0; i < length(files); i++) { var i = 0
var f = files[i] 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) // 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 (starts_with(f, "tests/") && ends_with(f, ".cm")) {
// If specific test requested, filter // If specific test requested, filter
if (specific_test) { if (specific_test) {
var test_name = text(f, 0, -3) // remove .cm test_name = text(f, 0, -3) // remove .cm
var match_name = specific_test match_name = specific_test
if (!starts_with(match_name, 'tests/')) match_name = 'tests/' + match_name if (!starts_with(match_name, 'tests/')) match_name = 'tests/' + match_name
// Match without extension // Match without extension
var match_base = ends_with(match_name, '.cm') ? text(match_name, 0, -3) : match_name match_base = ends_with(match_name, '.cm') ? text(match_name, 0, -3) : match_name
if (test_name != match_base) continue if (test_name != match_base) continue
} }
push(test_files, f) push(test_files, f)
@@ -282,24 +303,31 @@ function run_tests(package_name, specific_test) {
else log.console(`Running tests for local package`) else log.console(`Running tests for local package`)
} }
for (var i = 0; i < length(test_files); i++) { var _load_file = null
var f = test_files[i] for (i = 0; i < length(test_files); i++) {
var mod_path = text(f, 0, -3) // remove .cm f = test_files[i]
mod_path = text(f, 0, -3) // remove .cm
var file_result = { file_result = {
name: f, name: f,
tests: [], tests: [],
passed: 0, passed: 0,
failed: 0 failed: 0
} }
try { _load_file = function() {
var test_mod var test_mod = null
// For local packages (null), use the current directory as package context
var use_pkg = package_name ? package_name : fd.realpath('.') var use_pkg = package_name ? package_name : fd.realpath('.')
test_mod = shop.use(mod_path, use_pkg) test_mod = shop.use(mod_path, use_pkg)
var tests = [] 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
if (is_function(test_mod)) { if (is_function(test_mod)) {
push(tests, {name: 'main', fn: test_mod}) push(tests, {name: 'main', fn: test_mod})
} else if (is_object(test_mod)) { } else if (is_object(test_mod)) {
@@ -312,50 +340,55 @@ function run_tests(package_name, specific_test) {
if (length(tests) > 0) { if (length(tests) > 0) {
log.console(` ${f}`) log.console(` ${f}`)
for (var j = 0; j < length(tests); j++) { for (j = 0; j < length(tests); j++) {
var t = tests[j] t = tests[j]
var test_entry = { test_entry = {
package: pkg_result.package, package: pkg_result.package,
test: t.name, test: t.name,
status: "pending", status: "pending",
duration_ns: 0 duration_ns: 0
} }
var start_time = time.number() start_time = time.number()
try { _test_error = null
_run_one = function() {
var ret = t.fn() var ret = t.fn()
if (is_text(ret)) { if (is_text(ret)) {
throw Error(ret) _test_error = Error(ret)
disrupt
} else if (ret && (is_text(ret.message) || is_proto(ret, Error))) { } else if (ret && (is_text(ret.message) || is_proto(ret, Error))) {
throw ret _test_error = ret
disrupt
} }
test_entry.status = "passed" test_entry.status = "passed"
log.console(` PASS ${t.name}`) log.console(` PASS ${t.name}`)
pkg_result.passed++ pkg_result.passed++
file_result.passed++ file_result.passed++
} catch (e) { } disruption {
var e = _test_error
test_entry.status = "failed" test_entry.status = "failed"
test_entry.error = { test_entry.error = {
message: e, message: e,
stack: e.stack || "" stack: (e && e.stack) ? e.stack : ""
} }
if (e.name) test_entry.error.name = e.name if (e && e.name) test_entry.error.name = e.name
if (is_object(e) && e.message) { if (is_object(e) && e.message) {
test_entry.error.message = e.message test_entry.error.message = e.message
} }
log.console(` FAIL ${t.name} ${test_entry.error.message}`) log.console(` FAIL ${t.name} ${test_entry.error.message}`)
if (test_entry.error.stack) { if (test_entry.error.stack) {
log.console(` ${text(array(test_entry.error.stack, '\n'), '\n ')}`) log.console(` ${text(array(test_entry.error.stack, '\n'), '\n ')}`)
} }
pkg_result.failed++ pkg_result.failed++
file_result.failed++ file_result.failed++
} }
var end_time = time.number() _run_one()
end_time = time.number()
test_entry.duration_ns = round((end_time - start_time) * 1000000000) test_entry.duration_ns = round((end_time - start_time) * 1000000000)
push(file_result.tests, test_entry) push(file_result.tests, test_entry)
@@ -366,15 +399,15 @@ function run_tests(package_name, specific_test) {
} }
} }
} catch (e) { } disruption {
log.console(` Error loading ${f}: ${e}`) var test_entry = {
var test_entry = {
package: pkg_result.package, package: pkg_result.package,
test: "load_module", test: "load_module",
status: "failed", status: "failed",
duration_ns: 0, duration_ns: 0,
error: { message: `Error loading module: ${e}` } error: { message: `Error loading module` }
} }
log.console(` Error loading ${f}`)
push(file_result.tests, test_entry) push(file_result.tests, test_entry)
pkg_result.failed++ pkg_result.failed++
file_result.failed++ file_result.failed++
@@ -383,6 +416,7 @@ function run_tests(package_name, specific_test) {
dbg.gc() dbg.gc()
} }
} }
_load_file()
push(pkg_result.files, file_result) push(pkg_result.files, file_result)
} }
return pkg_result return pkg_result
@@ -390,6 +424,8 @@ function run_tests(package_name, specific_test) {
var all_results = [] var all_results = []
var all_actor_tests = [] var all_actor_tests = []
var packages = null
var i = 0
if (all_pkgs) { if (all_pkgs) {
// Run local first if we're in a valid package // Run local first if we're in a valid package
@@ -399,8 +435,8 @@ if (all_pkgs) {
} }
// Then all packages in lock // Then all packages in lock
var packages = shop.list_packages() packages = shop.list_packages()
for (var i = 0; i < length(packages); i++) { for (i = 0; i < length(packages); i++) {
push(all_results, run_tests(packages[i], null)) push(all_results, run_tests(packages[i], null))
all_actor_tests = array(all_actor_tests, collect_actor_tests(packages[i], null)) all_actor_tests = array(all_actor_tests, collect_actor_tests(packages[i], null))
} }
@@ -412,7 +448,7 @@ if (all_pkgs) {
// Spawn actor tests if any // Spawn actor tests if any
if (length(all_actor_tests) > 0) { if (length(all_actor_tests) > 0) {
log.console(`Running ${length(all_actor_tests)} actor test(s)...`) log.console(`Running ${length(all_actor_tests)} actor test(s)...`)
for (var i = 0; i < length(all_actor_tests); i++) { for (i = 0; i < length(all_actor_tests); i++) {
spawn_actor_test(all_actor_tests[i]) spawn_actor_test(all_actor_tests[i])
} }
} }
@@ -421,7 +457,10 @@ if (length(all_actor_tests) > 0) {
function handle_actor_message(msg) { function handle_actor_message(msg) {
var sender = msg.$sender var sender = msg.$sender
var found_idx = -1 var found_idx = -1
for (var i = 0; i < length(pending_actor_tests); i++) { var i = 0
var res = null
var entry = null
for (i = 0; i < length(pending_actor_tests); i++) {
if (pending_actor_tests[i].actor == sender) { if (pending_actor_tests[i].actor == sender) {
found_idx = i found_idx = i
break break
@@ -445,9 +484,9 @@ function handle_actor_message(msg) {
results = [msg] results = [msg]
} }
for (var i = 0; i < length(results); i++) { for (i = 0; i < length(results); i++) {
var res = results[i] || {} res = results[i] || {}
var entry = { entry = {
package: base_entry.package, package: base_entry.package,
file: base_entry.file, file: base_entry.file,
test: res.test || base_entry.test + (length(results) > 1 ? `#${i+1}` : ""), test: res.test || base_entry.test + (length(results) > 1 ? `#${i+1}` : ""),
@@ -481,18 +520,22 @@ function handle_actor_message(msg) {
function check_timeouts() { function check_timeouts() {
var now = time.number() var now = time.number()
var timed_out = [] var timed_out = []
var i = 0
var entry = null
var elapsed_ms = null
var idx = null
for (var i = length(pending_actor_tests) - 1; i >= 0; i--) { for (i = length(pending_actor_tests) - 1; i >= 0; i--) {
var entry = pending_actor_tests[i] entry = pending_actor_tests[i]
var elapsed_ms = (now - entry.start_time) * 1000 elapsed_ms = (now - entry.start_time) * 1000
if (elapsed_ms > ACTOR_TEST_TIMEOUT) { if (elapsed_ms > ACTOR_TEST_TIMEOUT) {
push(timed_out, i) push(timed_out, i)
} }
} }
for (var i = 0; i < length(timed_out); i++) { for (i = 0; i < length(timed_out); i++) {
var idx = timed_out[i] idx = timed_out[i]
var entry = pending_actor_tests[idx] entry = pending_actor_tests[idx]
pending_actor_tests = array(array(pending_actor_tests, 0, idx), array(pending_actor_tests, idx + 1)) pending_actor_tests = array(array(pending_actor_tests, 0, idx), array(pending_actor_tests, idx + 1))
entry.status = "failed" entry.status = "failed"
@@ -519,11 +562,17 @@ function check_completion() {
} }
function 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 // Add actor test results to all_results
for (var i = 0; i < length(actor_test_results); i++) { for (i = 0; i < length(actor_test_results); i++) {
var r = actor_test_results[i] r = actor_test_results[i]
var pkg_result = null pkg_result = null
for (var j = 0; j < length(all_results); j++) { for (j = 0; j < length(all_results); j++) {
if (all_results[j].package == r.package) { if (all_results[j].package == r.package) {
pkg_result = all_results[j] pkg_result = all_results[j]
break break
@@ -534,8 +583,8 @@ function finalize_results() {
push(all_results, pkg_result) push(all_results, pkg_result)
} }
var file_result = null file_result = null
for (var j = 0; j < length(pkg_result.files); j++) { for (j = 0; j < length(pkg_result.files); j++) {
if (pkg_result.files[j].name == r.file) { if (pkg_result.files[j].name == r.file) {
file_result = pkg_result.files[j] file_result = pkg_result.files[j]
break break
@@ -559,7 +608,7 @@ function finalize_results() {
// Calculate totals // Calculate totals
var totals = { total: 0, passed: 0, failed: 0 } var totals = { total: 0, passed: 0, failed: 0 }
for (var i = 0; i < length(all_results); i++) { for (i = 0; i < length(all_results); i++) {
totals.total += all_results[i].total totals.total += all_results[i].total
totals.passed += all_results[i].passed totals.passed += all_results[i].passed
totals.failed += all_results[i].failed totals.failed += all_results[i].failed
@@ -573,10 +622,10 @@ function finalize_results() {
} }
// If no actor tests, finalize immediately // If no actor tests, finalize immediately
var totals var totals = null
if (length(all_actor_tests) == 0) { if (length(all_actor_tests) == 0) {
totals = { total: 0, passed: 0, failed: 0 } totals = { total: 0, passed: 0, failed: 0 }
for (var i = 0; i < length(all_results); i++) { for (i = 0; i < length(all_results); i++) {
totals.total += all_results[i].total totals.total += all_results[i].total
totals.passed += all_results[i].passed totals.passed += all_results[i].passed
totals.failed += all_results[i].failed totals.failed += all_results[i].failed
@@ -593,6 +642,16 @@ function generate_reports(totals) {
var timestamp = text(floor(time.number())) var timestamp = text(floor(time.number()))
var report_dir = shop.get_reports_dir() + '/test_' + timestamp var report_dir = shop.get_reports_dir() + '/test_' + timestamp
ensure_dir(report_dir) 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 var txt_report = `TEST REPORT
Date: ${time.text(time.number())} Date: ${time.text(time.number())}
@@ -600,53 +659,53 @@ Total: ${totals.total}, Passed: ${totals.passed}, Failed: ${totals.failed}
=== SUMMARY === === SUMMARY ===
` `
for (var i = 0; i < length(all_results); i++) { for (i = 0; i < length(all_results); i++) {
var pkg_res = all_results[i] pkg_res = all_results[i]
if (pkg_res.total == 0) continue if (pkg_res.total == 0) continue
txt_report += `Package: ${pkg_res.package}\n` txt_report = txt_report + `Package: ${pkg_res.package}\n`
for (var j = 0; j < length(pkg_res.files); j++) { for (j = 0; j < length(pkg_res.files); j++) {
var f = pkg_res.files[j] f = pkg_res.files[j]
var status = f.failed == 0 ? "PASS" : "FAIL" status = f.failed == 0 ? "PASS" : "FAIL"
txt_report += ` [${status}] ${f.name} (${f.passed}/${length(f.tests)})\n` txt_report = txt_report + ` [${status}] ${f.name} (${f.passed}/${length(f.tests)})\n`
} }
} }
txt_report += `\n=== FAILURES ===\n` txt_report = txt_report + `\n=== FAILURES ===\n`
var has_failures = false var has_failures = false
for (var i = 0; i < length(all_results); i++) { for (i = 0; i < length(all_results); i++) {
var pkg_res = all_results[i] pkg_res = all_results[i]
for (var j = 0; j < length(pkg_res.files); j++) { for (j = 0; j < length(pkg_res.files); j++) {
var f = pkg_res.files[j] f = pkg_res.files[j]
for (var k = 0; k < length(f.tests); k++) { for (k = 0; k < length(f.tests); k++) {
var t = f.tests[k] t = f.tests[k]
if (t.status == "failed") { if (t.status == "failed") {
has_failures = true has_failures = true
txt_report += `FAIL: ${pkg_res.package} :: ${f.name} :: ${t.test}\n` txt_report = txt_report + `FAIL: ${pkg_res.package} :: ${f.name} :: ${t.test}\n`
if (t.error) { if (t.error) {
txt_report += ` Message: ${t.error.message}\n` txt_report = txt_report + ` Message: ${t.error.message}\n`
if (t.error.stack) { if (t.error.stack) {
txt_report += ` Stack:\n${text(array(array(t.error.stack, '\n'), l => ` ${l}`), '\n')}\n` txt_report = txt_report + ` Stack:\n${text(array(array(t.error.stack, '\n'), l => ` ${l}`), '\n')}\n`
} }
} }
txt_report += `\n` txt_report = txt_report + `\n`
} }
} }
} }
} }
if (!has_failures) txt_report += `None\n` if (!has_failures) txt_report = txt_report + `None\n`
txt_report += `\n=== DETAILED RESULTS ===\n` txt_report = txt_report + `\n=== DETAILED RESULTS ===\n`
for (var i = 0; i < length(all_results); i++) { for (i = 0; i < length(all_results); i++) {
var pkg_res = all_results[i] pkg_res = all_results[i]
if (pkg_res.total == 0) continue if (pkg_res.total == 0) continue
for (var j = 0; j < length(pkg_res.files); j++) { for (j = 0; j < length(pkg_res.files); j++) {
var f = pkg_res.files[j] f = pkg_res.files[j]
for (var k = 0; k < length(f.tests); k++) { for (k = 0; k < length(f.tests); k++) {
var t = f.tests[k] t = f.tests[k]
var dur = `${t.duration_ns || 0}ns` dur = `${t.duration_ns || 0}ns`
var status = t.status == "passed" ? "PASS" : "FAIL" status = t.status == "passed" ? "PASS" : "FAIL"
txt_report += `[${status}] ${pkg_res.package} ${t.test} (${dur})\n` txt_report = txt_report + `[${status}] ${pkg_res.package} ${t.test} (${dur})\n`
} }
} }
} }
@@ -655,19 +714,19 @@ Total: ${totals.total}, Passed: ${totals.passed}, Failed: ${totals.failed}
log.console(`Report written to ${report_dir}/test.txt`) log.console(`Report written to ${report_dir}/test.txt`)
// Generate JSON per package // Generate JSON per package
for (var i = 0; i < length(all_results); i++) { for (i = 0; i < length(all_results); i++) {
var pkg_res = all_results[i] pkg_res = all_results[i]
if (pkg_res.total == 0) continue if (pkg_res.total == 0) continue
var pkg_tests = [] pkg_tests = []
for (var j = 0; j < length(pkg_res.files); j++) { for (j = 0; j < length(pkg_res.files); j++) {
var f = pkg_res.files[j] f = pkg_res.files[j]
for (var k = 0; k < length(f.tests); k++) { for (k = 0; k < length(f.tests); k++) {
push(pkg_tests, f.tests[k]) push(pkg_tests, f.tests[k])
} }
} }
var json_path = `${report_dir}/${replace(pkg_res.package, /\//, '_')}.json` json_path = `${report_dir}/${replace(pkg_res.package, /\//, '_')}.json`
fd.slurpwrite(json_path, stone(blob(json.encode(pkg_tests)))) fd.slurpwrite(json_path, stone(blob(json.encode(pkg_tests))))
} }
} }

View File

@@ -124,14 +124,15 @@ function time_number(_rec) {
var dst = r.dst ? 1 : 0 var dst = r.dst ? 1 : 0
var yday = r.yday || 0 var yday = r.yday || 0
var i = 0
if (year > time.epoch) { if (year > time.epoch) {
var i = time.epoch i = time.epoch
while (i < year) { while (i < year) {
c = c + time.day * time.yearsize(i) c = c + time.day * time.yearsize(i)
i = i + 1 i = i + 1
} }
} else if (year < time.epoch) { } else if (year < time.epoch) {
var i = time.epoch - 1 i = time.epoch - 1
while (i > year) { while (i > year) {
c = c + time.day * time.yearsize(i) c = c + time.day * time.yearsize(i)
i = i - 1 i = i - 1

96
toml.cm
View File

@@ -26,23 +26,33 @@ function parse_toml(toml_text) {
var current_section = result var current_section = result
var current_section_name = '' var current_section_name = ''
for (var i = 0; i < length(lines); i++) { var i = 0
var line = trim(lines[i]) var line = null
var inner = null
var section_path = null
var j = 0
var key = null
var eq_index = null
var key_part = null
var value = null
var unquoted = null
for (i = 0; i < length(lines); i++) {
line = trim(lines[i])
if (line == null) line = lines[i] if (line == null) line = lines[i]
// Skip empty lines and comments // Skip empty lines and comments
if (!line || starts_with(line, '#')) continue if (!line || starts_with(line, '#')) continue
// Section header // Section header
if (starts_with(line, '[') && ends_with(line, ']')) { if (starts_with(line, '[') && ends_with(line, ']')) {
var inner = text(line, 1, -1) inner = text(line, 1, -1)
var section_path = parse_key_path(inner) section_path = parse_key_path(inner)
if (section_path == null) return null if (section_path == null) return null
current_section = result current_section = result
current_section_name = text(section_path, '.') current_section_name = text(section_path, '.')
for (var j = 0; j < length(section_path); j++) { for (j = 0; j < length(section_path); j++) {
var key = section_path[j] key = section_path[j]
// Only treat null as "missing"; do not clobber false/0/"" // Only treat null as "missing"; do not clobber false/0/""
if (current_section[key] == null) { if (current_section[key] == null) {
@@ -58,18 +68,18 @@ function parse_toml(toml_text) {
} }
// Key-value pair // Key-value pair
var eq_index = search(line, '=') eq_index = search(line, '=')
if (eq_index != null && eq_index > 0) { if (eq_index != null && eq_index > 0) {
var key_part = trim(text(line, 0, eq_index)) key_part = trim(text(line, 0, eq_index))
var value = trim(text(line, eq_index + 1)) value = trim(text(line, eq_index + 1))
if (key_part == null) key_part = trim(text(line, 0, eq_index)) if (key_part == null) key_part = trim(text(line, 0, eq_index))
if (value == null) value = trim(text(line, eq_index + 1)) if (value == null) value = trim(text(line, eq_index + 1))
var key = parse_key(key_part) key = parse_key(key_part)
if (key == null) return null if (key == null) return null
if (starts_with(value, '"') && ends_with(value, '"')) { if (starts_with(value, '"') && ends_with(value, '"')) {
var unquoted = text(value, 1, -1) unquoted = text(value, 1, -1)
current_section[key] = toml_unescape(unquoted) current_section[key] = toml_unescape(unquoted)
if (current_section[key] == null) return null if (current_section[key] == null) return null
} else if (starts_with(value, '[') && ends_with(value, ']')) { } else if (starts_with(value, '[') && ends_with(value, ']')) {
@@ -91,9 +101,9 @@ function parse_toml(toml_text) {
function parse_key(str) { function parse_key(str) {
if (!is_text(str)) return null if (!is_text(str)) return null
var inner = null
if (starts_with(str, '"') && ends_with(str, '"')) { if (starts_with(str, '"') && ends_with(str, '"')) {
var inner = text(str, 1, -1) inner = text(str, 1, -1)
return toml_unescape(inner) return toml_unescape(inner)
} }
return str return str
@@ -107,12 +117,15 @@ function parse_key_path(str) {
var current = '' var current = ''
var in_quote = false var in_quote = false
for (var i = 0; i < length(str); i++) { var i = 0
var c = str[i] var c = null
var piece = null
for (i = 0; i < length(str); i++) {
c = str[i]
if (c == '"' && (i == 0 || str[i - 1] != '\\')) { if (c == '"' && (i == 0 || str[i - 1] != '\\')) {
in_quote = !in_quote in_quote = !in_quote
} else if (c == '.' && !in_quote) { } else if (c == '.' && !in_quote) {
var piece = trim(current) piece = trim(current)
if (piece == null) piece = trim(current) if (piece == null) piece = trim(current)
push(parts, parse_key(piece)) push(parts, parse_key(piece))
current = '' current = ''
@@ -140,14 +153,17 @@ function parse_array(str) {
var current = '' var current = ''
var in_quotes = false var in_quotes = false
for (var i = 0; i < length(s); i++) { var i = 0
var ch = s[i] var ch = null
var piece = null
for (i = 0; i < length(s); i++) {
ch = s[i]
if (ch == '"' && (i == 0 || s[i - 1] != '\\')) { if (ch == '"' && (i == 0 || s[i - 1] != '\\')) {
in_quotes = !in_quotes in_quotes = !in_quotes
current = current + ch current = current + ch
} else if (ch == ',' && !in_quotes) { } else if (ch == ',' && !in_quotes) {
var piece = trim(current) piece = trim(current)
if (piece == null) piece = trim(current) if (piece == null) piece = trim(current)
push(items, parse_value(piece)) push(items, parse_value(piece))
current = '' current = ''
@@ -181,12 +197,14 @@ function encode_toml(obj) {
var result = [] var result = []
function encode_value(value) { function encode_value(value) {
var items = null
var i = 0
if (is_text(value)) return '"' + toml_escape(value) + '"' if (is_text(value)) return '"' + toml_escape(value) + '"'
if (is_logical(value)) return value ? 'true' : 'false' if (is_logical(value)) return value ? 'true' : 'false'
if (is_number(value)) return text(value) if (is_number(value)) return text(value)
if (is_array(value)) { if (is_array(value)) {
var items = [] items = []
for (var i = 0; i < length(value); i++) push(items, encode_value(value[i])) for (i = 0; i < length(value); i++) push(items, encode_value(value[i]))
return '[' + text(items, ', ') + ']' return '[' + text(items, ', ') + ']'
} }
return text(value) return text(value)
@@ -201,29 +219,41 @@ function encode_toml(obj) {
// First pass: encode top-level simple values // First pass: encode top-level simple values
var keys = array(obj) var keys = array(obj)
for (var i = 0; i < length(keys); i++) { var i = 0
var key = keys[i] var key = null
var value = obj[key] var value = null
for (i = 0; i < length(keys); i++) {
key = keys[i]
value = obj[key]
if (!is_object(value)) push(result, quote_key(key) + ' = ' + encode_value(value)) if (!is_object(value)) push(result, quote_key(key) + ' = ' + encode_value(value))
} }
// Second pass: encode nested objects // Second pass: encode nested objects
function encode_section(o, path) { function encode_section(o, path) {
var keys = array(o) var keys = array(o)
for (var i = 0; i < length(keys); i++) { var i = 0
var key = keys[i] var key = null
var value = o[key] var value = null
var quoted = null
var section_path = null
var section_keys = null
var j = 0
var sk = null
var sv = null
for (i = 0; i < length(keys); i++) {
key = keys[i]
value = o[key]
if (is_object(value)) { if (is_object(value)) {
var quoted = quote_key(key) quoted = quote_key(key)
var section_path = path ? path + '.' + quoted : quoted section_path = path ? path + '.' + quoted : quoted
push(result, '[' + section_path + ']') push(result, '[' + section_path + ']')
// Direct properties // Direct properties
var section_keys = array(value) section_keys = array(value)
for (var j = 0; j < length(section_keys); j++) { for (j = 0; j < length(section_keys); j++) {
var sk = section_keys[j] sk = section_keys[j]
var sv = value[sk] sv = value[sk]
if (!is_object(sv)) push(result, quote_key(sk) + ' = ' + encode_value(sv)) if (!is_object(sv)) push(result, quote_key(sk) + ' = ' + encode_value(sv))
} }