local t = require('spec.zync_test')
local sandbox = require('zync.api.configurator.sandbox')
local assert = t.assert
local function get_upvalue(fn, name)
local i = 1
while true do
local up_name, up_value = debug.getupvalue(fn, i)
if not up_name then
return nil
end
if up_name == name then
return up_value
end
i = i + 1
end
end
describe('Configurator sandbox', function()
before_each(function()
t.helpers.setup()
end)
after_each(function()
t.helpers.teardown()
end)
it('loads a valid zconf table', function()
local path = t.tmp .. '/zconf.lua'
t.helpers.write_file(path, "return { board = 'nucleo_f767zi', runner = 'openocd' }")
local cfg, err = sandbox.load_zconf(path, false)
assert.is_nil(err)
t.helpers.assert_tables_equal(cfg, {
board = 'nucleo_f767zi',
runner = 'openocd',
})
end)
it('rejects missing required fields', function()
local path = t.tmp .. '/zconf.lua'
t.helpers.write_file(path, "return { runner = 'openocd' }")
local cfg, err = sandbox.load_zconf(path, false)
assert.is_nil(cfg)
assert.is_truthy(err:match("missing required field 'board'"))
end)
it('allows profiles without top-level board', function()
local path = t.tmp .. '/zconf.lua'
t.helpers.write_file(path, "return { profiles = { prod = { board = 'nucleo_f767zi' } } }")
local cfg, err = sandbox.load_zconf(path, false)
assert.is_nil(err)
t.helpers.assert_tables_equal(cfg, {
profiles = {
prod = {
board = 'nucleo_f767zi',
},
},
})
end)
it('blocks forbidden globals in the sandbox', function()
local path = t.tmp .. '/zconf.lua'
t.helpers.write_file(path, "return { board = os.getenv('ZEPHYR_BASE') }")
local cfg, err = sandbox.load_zconf(path, false)
assert.is_nil(cfg)
assert.is_truthy(err:match("access to global 'os' is not allowed"))
end)
it('validates schema types', function()
local path = t.tmp .. '/zconf.lua'
t.helpers.write_file(path, "return { board = 123 }")
local cfg, err = sandbox.load_zconf(path, true)
assert.is_nil(cfg)
assert.is_truthy(err:match("field 'board' has to be string"))
t.helpers.write_file(path, "return { profiles = 'bad' }")
cfg, err = sandbox.load_zconf(path, false)
assert.is_nil(cfg)
assert.is_truthy(err:match("field 'profiles' has to be table"))
end)
it('rejects non-table config returns', function()
local path = t.tmp .. '/zconf.lua'
t.helpers.write_file(path, "return 'nope'")
local cfg, err = sandbox.load_zconf(path, false)
assert.is_nil(cfg)
assert.is_truthy(err:match('table expected') or err:match('plain table'))
end)
it('rejects metatables in returned config', function()
local path = t.tmp .. '/zconf.lua'
t.helpers.write_file(path, "return setmetatable({ board = 'nucleo_f767zi' }, {})")
local cfg, err = sandbox.load_zconf(path, true)
assert.is_nil(cfg)
assert.is_truthy(err:match('must return a plain table'))
end)
it('rejects nested metatables', function()
local path = t.tmp .. '/zconf.lua'
t.helpers.write_file(path, [[
local nested = setmetatable({ value = 1 }, {})
return { board = 'nucleo_f767zi', nested = nested }
]])
local cfg, err = sandbox.load_zconf(path, true)
assert.is_nil(cfg)
assert.is_truthy(err:match('metatables are not allowed in config tables'))
end)
it('warns on unknown fields', function()
local path = t.tmp .. '/zconf.lua'
t.helpers.write_file(path, "return { board = 'nucleo_f767zi', extra = 1 }")
local print_stub = t.stub(_G, 'print')
local cfg, err = sandbox.load_zconf(path, false)
assert.is_nil(err)
assert.is_truthy(cfg)
assert.stub(print_stub).was.called_with("zconf: unknown field 'extra'")
print_stub:revert()
end)
it('enforces config depth limits', function()
local path = t.tmp .. '/zconf.lua'
t.helpers.write_file(path, [[
return {
board = 'nucleo_f767zi',
nested = { a = { b = { c = { d = { e = { f = { g = { h = {} } } } } } } } },
}
]])
local cfg, err = sandbox.load_zconf(path, false)
assert.is_nil(cfg)
assert.is_truthy(err:match('config table too deep'))
end)
it('errors on empty path', function()
local ok, cfg_or_err = pcall(function()
return sandbox.load_zconf('', false)
end)
assert.is_false(ok)
assert.is_truthy(tostring(cfg_or_err):match('Failed to load zync config'))
end)
it('propagates loadfile errors for invalid syntax', function()
local path = t.tmp .. '/zconf.lua'
t.helpers.write_file(path, "return { board = ")
local cfg, err = sandbox.load_zconf(path, false)
assert.is_nil(cfg)
assert.is_truthy(err:match('unexpected') or err:match('syntax'))
end)
it('propagates load errors after reading source', function()
local path = t.tmp .. '/zconf.lua'
t.helpers.write_file(path, "return { board = 'nucleo_f767zi' }")
local load_stub = t.stub(_G, 'load', function()
return nil, 'forced load failure'
end)
local cfg, err = sandbox.load_zconf(path, false)
load_stub:revert()
assert.is_nil(cfg)
assert.is_truthy(err:match('forced load failure'))
end)
it('enforces runtime instruction limits', function()
local path = t.tmp .. '/zconf.lua'
t.helpers.write_file(path, [[
local i = 0
while true do
i = i + 1
end
return { board = 'nucleo_f767zi' }
]])
local cfg, err = sandbox.load_zconf(path, false)
assert.is_nil(cfg)
assert.is_truthy(err:match('instruction limit exceeded') or err:match('time limit exceeded'))
end)
it('enforces runtime time limits', function()
local path = t.tmp .. '/zconf.lua'
t.helpers.write_file(path, [[
local i = 0
while true do
i = i + 1
end
return { board = 'nucleo_f767zi' }
]])
local calls = 0
local clock_stub = t.stub(os, 'clock', function()
calls = calls + 1
if calls == 1 then
return 0
end
return 999
end)
local cfg, err = sandbox.load_zconf(path, false)
clock_stub:revert()
assert.is_nil(cfg)
assert.is_truthy(err:match('time limit exceeded'))
end)
it('enforces config size limits', function()
local path = t.tmp .. '/zconf.lua'
local entries = {}
for i = 1, 2100 do
table.insert(entries, string.format("k%d = %d", i, i))
end
t.helpers.write_file(path, "return { board = 'nucleo_f767zi', " .. table.concat(entries, ', ') .. " }")
local cfg, err = sandbox.load_zconf(path, false)
assert.is_nil(cfg)
assert.is_truthy(err:match('config too large'))
end)
it('blocks defining new globals in sandbox', function()
local path = t.tmp .. '/zconf.lua'
t.helpers.write_file(path, [[
foo = 1
return { board = 'nucleo_f767zi' }
]])
local cfg, err = sandbox.load_zconf(path, false)
assert.is_nil(cfg)
assert.is_truthy(err:match('defining global'))
end)
it('exposes sandbox env access guards', function()
local sandbox_fn = get_upvalue(sandbox.load_zconf, 'sandbox')
assert.is_truthy(sandbox_fn)
local env = sandbox_fn({})
local ok_access, err_access = pcall(function()
return env.nope
end)
assert.is_false(ok_access)
assert.is_truthy(tostring(err_access):match('access to global'))
local ok_define, err_define = pcall(function()
env.nope = 1
end)
assert.is_false(ok_define)
assert.is_truthy(tostring(err_define):match('defining global'))
end)
it('rejects non-table schema inputs', function()
local validate_schema = get_upvalue(sandbox.load_zconf, 'validate_schema')
local zconf_schema = get_upvalue(sandbox.load_zconf, 'zconf_schema')
assert.is_truthy(validate_schema)
assert.is_truthy(zconf_schema)
local cfg, err = validate_schema('nope', zconf_schema)
assert.is_nil(cfg)
assert.is_truthy(err:match('plain table'))
end)
it('tracks instruction limit hook execution', function()
local with_limits = get_upvalue(sandbox.load_zconf, 'with_limits')
assert.is_truthy(with_limits)
local hook_err
local hook_stub = t.stub(debug, 'sethook', function(_, hook)
if type(hook) == 'function' then
local ok, err = pcall(hook)
if not ok then
hook_err = err
end
end
end)
local cfg, err = with_limits(function()
return { board = 'nucleo_f767zi' }
end, {
max_insn = 0,
hook_every = 1,
max_sec = 100,
})
hook_stub:revert()
assert.is_truthy(cfg)
assert.is_nil(err)
assert.is_truthy(tostring(hook_err):match('instruction limit exceeded'))
end)
it('tracks time limit hook execution', function()
local with_limits = get_upvalue(sandbox.load_zconf, 'with_limits')
assert.is_truthy(with_limits)
local calls = 0
local clock_stub = t.stub(os, 'clock', function()
calls = calls + 1
if calls == 1 then
return 0
end
return 999
end)
local hook_err
local hook_stub = t.stub(debug, 'sethook', function(_, hook)
if type(hook) == 'function' then
local ok, err = pcall(hook)
if not ok then
hook_err = err
end
end
end)
local cfg, err = with_limits(function()
return { board = 'nucleo_f767zi' }
end, {
max_insn = 1000000,
hook_every = 1,
max_sec = 0,
})
hook_stub:revert()
clock_stub:revert()
assert.is_truthy(cfg)
assert.is_nil(err)
assert.is_truthy(tostring(hook_err):match('time limit exceeded'))
end)
end)