feat(runtime)!: sys.startApp replaces routing, history and app identity
The runtime kept a back stack, a launcher fallback, an app id and a title because a teardown destroys the Lua that would otherwise hold them. Only the first of those is true: everything about where an app came from can ride in the arguments, and the arguments are the one value that has to outlive the VM. So the runtime now does four things -- close the state, load a path, hand the next state its arguments, defer the swap to a batch boundary -- and sys.startApp(path, args) is the whole of navigation. Routing, history, titles and data directories move to the Lua file a firmware boots, where they can differ per product without a flag on Runtime. Arguments cross as JSON, encoded while the sending state still holds the table, so a function or a cycle raises at the call rather than stranding a launch. start(args) receives the decoded table, or nil at boot, which is how the entry file knows to open its own launcher. Removes launch, replace, back, canGoBack, getAppID, getAppTitle, setAppTitle and getAppDataPath, along with the home and data fields. LANDSCAPE.md goes with them: it recorded a divergence from firmwares that have since migrated.
This commit is contained in:
@@ -93,21 +93,23 @@ int main() {
|
||||
bench.http.headers[0].name == "Accept");
|
||||
expectError(state, "http.get('https://example.test', {maxBytes = 999999})");
|
||||
|
||||
// cjson is required rather than global, because the library provides it and no
|
||||
// firmware implements it. Decoding is lua-cjson's; what is asserted here is
|
||||
// the wiring and the depth limit the panel's C stack needs.
|
||||
run(state, "local cjson = require 'cjson'\n"
|
||||
"assert(rawget(_G, 'cjson') == nil)\n"
|
||||
"local value = cjson.decode('{\"a\":[1,2.5,true],\"b\":\"x\\\\u00e9\"}')\n"
|
||||
"assert(math.type(value.a[1]) == 'integer' and value.a[2] == 2.5)\n"
|
||||
"assert(value.a[3] == true and value.b == 'x\\u{e9}')\n"
|
||||
"assert(cjson.decode('null') == cjson.null)\n"
|
||||
"assert(cjson.encode({1, 2, 3}) == '[1,2,3]')\n"
|
||||
"assert(not pcall(cjson.decode, '{'))\n"
|
||||
"assert(not pcall(cjson.encode, print))\n"
|
||||
"local deep = {}; for _ = 1, 40 do deep = {deep} end\n"
|
||||
"assert(not pcall(cjson.encode, deep))\n"
|
||||
"assert(not pcall(cjson.decode, string.rep('[', 40)))");
|
||||
// cjson is required rather than global, because the library provides it and
|
||||
// no firmware implements it. Decoding is lua-cjson's; what is asserted here
|
||||
// is the wiring and the depth limit the panel's C stack needs.
|
||||
run(state,
|
||||
"local cjson = require 'cjson'\n"
|
||||
"assert(rawget(_G, 'cjson') == nil)\n"
|
||||
"local value = "
|
||||
"cjson.decode('{\"a\":[1,2.5,true],\"b\":\"x\\\\u00e9\"}')\n"
|
||||
"assert(math.type(value.a[1]) == 'integer' and value.a[2] == 2.5)\n"
|
||||
"assert(value.a[3] == true and value.b == 'x\\u{e9}')\n"
|
||||
"assert(cjson.decode('null') == cjson.null)\n"
|
||||
"assert(cjson.encode({1, 2, 3}) == '[1,2,3]')\n"
|
||||
"assert(not pcall(cjson.decode, '{'))\n"
|
||||
"assert(not pcall(cjson.encode, print))\n"
|
||||
"local deep = {}; for _ = 1, 40 do deep = {deep} end\n"
|
||||
"assert(not pcall(cjson.encode, deep))\n"
|
||||
"assert(not pcall(cjson.decode, string.rep('[', 40)))");
|
||||
|
||||
run(state, "assert(wifi.scan()[1].ssid == 'home')\n"
|
||||
"assert(wifi.isConnected())\n"
|
||||
@@ -212,8 +214,8 @@ int main() {
|
||||
"local function note(name) return function(a) events[#events + 1] = "
|
||||
"name .. ':' .. tostring(a) end end\n"
|
||||
"return {\n"
|
||||
" start = function(route, arg) events[#events + 1] = 'start:' .. "
|
||||
"route .. ':' .. arg end,\n"
|
||||
" start = function(args) events[#events + 1] = 'start:' .. "
|
||||
"args.app .. ':' .. args.arg end,\n"
|
||||
" draw = function(delta) if delta < 0 then error('kaboom') end\n"
|
||||
" events[#events + 1] = 'draw:' .. delta end,\n"
|
||||
" onTouchDown = note('down'),\n"
|
||||
@@ -223,7 +225,8 @@ int main() {
|
||||
" onButton = note('btap'),\n"
|
||||
"}\n";
|
||||
esp32lua::Runtime hosted(chrome.providers());
|
||||
assert(hosted.startApp("Reader", "book.epub"));
|
||||
assert(hosted.startApp(esp32lua::MAIN_PATH,
|
||||
"{\"app\":\"Reader\",\"arg\":\"book.epub\"}"));
|
||||
chrome.gui.commits = 0;
|
||||
hosted.callDraw(33);
|
||||
hosted.callTouch(esp32lua::TouchPhase::Down, 5, 6);
|
||||
@@ -246,13 +249,19 @@ int main() {
|
||||
|
||||
chrome.fs.files["/.lua/main.lua"] =
|
||||
"return { start = function() error('boom') end }";
|
||||
assert(!hosted.startApp("Reader"));
|
||||
assert(!hosted.startApp(esp32lua::MAIN_PATH));
|
||||
assert(chrome.log.message.find("start: ") == 0);
|
||||
assert(!hosted.hasApp());
|
||||
|
||||
// Arguments that are not JSON leave no app running rather than a state with
|
||||
// no start() behind it.
|
||||
chrome.fs.files["/.lua/main.lua"] = "return { start = function() end }";
|
||||
assert(!hosted.startApp(esp32lua::MAIN_PATH, "{not json"));
|
||||
assert(!hosted.hasApp());
|
||||
|
||||
chrome.fs.files["/.lua/main.lua"] = "return 7";
|
||||
assert(!hosted.startApp("Reader"));
|
||||
assert(chrome.log.message == "main.lua returned no table");
|
||||
assert(!hosted.startApp(esp32lua::MAIN_PATH));
|
||||
assert(chrome.log.message == "/.lua/main.lua returned no table");
|
||||
}
|
||||
|
||||
// A feature callback without its provider is a wiring bug, not a silent
|
||||
@@ -289,16 +298,18 @@ int main() {
|
||||
{
|
||||
fake::Bench host;
|
||||
// The tree is main.lua's, so the test states it the way a card would.
|
||||
// Routing, history and the launcher are all this file's, built out of the
|
||||
// arguments it is handed; the runtime knows only the path it loads.
|
||||
host.fs.files["/.lua/main.lua"] =
|
||||
"package.path = '/.lua/lib/?.lua'\n"
|
||||
"return {\n"
|
||||
" home = 'Home',\n"
|
||||
" data = '/.lua/data/?',\n"
|
||||
" start = function(route, arg)\n"
|
||||
" start = function(args)\n"
|
||||
" args = args or {app = 'Home'}\n"
|
||||
" route, history = args.app, args.history or {}\n"
|
||||
" local dir = '/.lua/apps/' .. route\n"
|
||||
" package.path = dir .. '/?.lua;/.lua/lib/?.lua'\n"
|
||||
" app = assert(loadfile(dir .. '/main.lua'))()\n"
|
||||
" app.init(arg)\n"
|
||||
" app.init(args.arg)\n"
|
||||
" end,\n"
|
||||
"}\n";
|
||||
host.fs.files["/.lua/lib/greet.lua"] =
|
||||
@@ -315,44 +326,40 @@ int main() {
|
||||
"return {init = function() end}";
|
||||
|
||||
esp32lua::Runtime app(host.providers());
|
||||
assert(app.startApp("Home"));
|
||||
assert(app.appId() == "Home" && app.appTitle() == "Home");
|
||||
assert(app.appDataPath() == "/.lua/data/Home");
|
||||
run(app.state(), "assert(started == 'hi:')");
|
||||
assert(app.startApp(esp32lua::MAIN_PATH));
|
||||
assert(app.appPath() == esp32lua::MAIN_PATH);
|
||||
run(app.state(), "assert(started == 'hi:nil' and route == 'Home')");
|
||||
|
||||
// A subapp shares the app ID, so both routes share one data directory.
|
||||
run(app.state(), "sys.launch('Reader', 'book.epub')");
|
||||
// The arguments cross the teardown as JSON, so a nested table survives and
|
||||
// the history is whatever main.lua chose to put in it.
|
||||
run(app.state(),
|
||||
"sys.startApp('/.lua/main.lua', "
|
||||
"{app = 'Reader', arg = 'book.epub', history = {'Home'}})");
|
||||
assert(app.hasPendingNavigation());
|
||||
run(app.state(), "assert(started == 'hi:')"); // the current app keeps
|
||||
// running until applied
|
||||
run(app.state(), "assert(started == 'hi:nil')"); // still running until
|
||||
// applied
|
||||
assert(app.applyPendingNavigation());
|
||||
run(app.state(), "assert(started == 'page:book.epub')");
|
||||
run(app.state(), "sys.launch('Reader/Notes')");
|
||||
run(app.state(), "assert(started == 'page:book.epub')\n"
|
||||
"assert(route == 'Reader' and history[1] == 'Home')");
|
||||
|
||||
// Going back is the same call with the stack main.lua kept, popped by
|
||||
// main.lua: nothing in C++ remembers where the app came from.
|
||||
run(app.state(), "sys.startApp('/.lua/main.lua', {app = history[1]})");
|
||||
assert(app.applyPendingNavigation());
|
||||
assert(app.appPath() == "Reader/Notes" && app.appId() == "Reader");
|
||||
assert(app.appDataPath() == "/.lua/data/Reader");
|
||||
run(app.state(), "assert(route == 'Home' and #history == 0)");
|
||||
|
||||
// Back unwinds history, then lands on the launcher.
|
||||
run(app.state(), "sys.back()");
|
||||
assert(app.applyPendingNavigation() && app.appPath() == "Reader");
|
||||
run(app.state(), "sys.back()");
|
||||
assert(app.applyPendingNavigation() && app.appPath() == "Home");
|
||||
run(app.state(), "sys.back()");
|
||||
assert(app.applyPendingNavigation() && app.appPath() == "Home");
|
||||
// Arguments JSON cannot carry raise at the call, leaving the app running.
|
||||
expectError(app.state(), "sys.startApp('/.lua/main.lua', {f = print})");
|
||||
assert(!app.hasPendingNavigation());
|
||||
run(app.state(), "assert(route == 'Home')");
|
||||
expectError(app.state(), "sys.startApp('/.lua/main.lua', 'not a table')");
|
||||
|
||||
// sys.replace does not grow history, so back from it still reaches the
|
||||
// launcher.
|
||||
run(app.state(), "sys.replace('Reader', 'other.epub')");
|
||||
assert(app.applyPendingNavigation() && app.appPath() == "Reader");
|
||||
run(app.state(), "sys.back()");
|
||||
assert(app.applyPendingNavigation() && app.appPath() == "Home");
|
||||
|
||||
// A missing app, a broken app, and a traversal all leave nothing running.
|
||||
assert(!app.startApp("Absent"));
|
||||
// A missing file, a broken app, and a traversal all leave nothing running.
|
||||
assert(!app.startApp("/.lua/absent.lua"));
|
||||
assert(!app.hasApp() && app.state() == nullptr);
|
||||
host.fs.files["/.lua/apps/Broken/main.lua"] =
|
||||
"return {init = function() error('nope') end}";
|
||||
assert(!app.startApp("Broken"));
|
||||
assert(!app.startApp(esp32lua::MAIN_PATH, "{\"app\":\"Broken\"}"));
|
||||
assert(!app.hasApp());
|
||||
assert(!app.startApp("../secrets"));
|
||||
assert(host.log.message.find("refusing to start") == 0);
|
||||
|
||||
Reference in New Issue
Block a user