diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index 305c21b9f..94caff92c 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -170,6 +170,7 @@ if(BUILD_TESTING) message(STATUS "Building test-enginelib executable.") add_executable(test-enginelib src/lua_stack_test.cpp + src/lua_setup_test.cpp ) set_target_properties(test-enginelib PROPERTIES CXX_STANDARD 17 diff --git a/engine/lua/cloe-engine/init.lua b/engine/lua/cloe-engine/init.lua index ce16073b5..2ed7c28e0 100644 --- a/engine/lua/cloe-engine/init.lua +++ b/engine/lua/cloe-engine/init.lua @@ -44,9 +44,6 @@ local engine = { --- Contains engine state for a simulation. state = { - --- @type StackConf The current active stack configuration (volatile). - config = {}, - --- @type table A table of feature flags. features = { ["cloe-0.18.0"] = true, @@ -74,6 +71,9 @@ local engine = { --- @type Stack Reference to simulation stack type. stack = nil, + --- @type boolean True if simulation has started. + is_running = false, + --- @type string|nil Path to currently executing Lua script file. current_script_file = nil, @@ -144,6 +144,13 @@ function engine.is_available() return false end +--- Return whether the simulation has started. +--- +--- @return boolean +function engine.is_simulation_running() + return engine.state.is_running +end + --- Return path to Lua file that the engine is currently merging, --- or nil if no file is being loaded. --- @@ -167,6 +174,14 @@ function engine.get_stack() return unavailable("get_stack") end +--- Return the current simulation configuration. +--- +--- This is essential a dump of the stack. +--- @return StackConf +function engine.get_config() + return unavailable("get_config") +end + --- Return the simulation scheduler (aka. Coordinator) global instance. --- --- @return Coordinator diff --git a/engine/lua/cloe/engine.lua b/engine/lua/cloe/engine.lua index 6e1d954ea..f20a95cd8 100644 --- a/engine/lua/cloe/engine.lua +++ b/engine/lua/cloe/engine.lua @@ -39,7 +39,7 @@ end --- @nodiscard function engine.has_feature(id) validate("cloe.has_feature(string)", id) - return api.state.features[id] and true or false + return api.get_features()[id] and true or false end --- Throw an exception if Cloe does not have feature as defined by string. @@ -60,7 +60,7 @@ end --- --- @return StackConf function engine.config() - return api.state.config + return api.get_config() end --- Try to load (merge) stackfile. @@ -69,12 +69,12 @@ end --- @return Stack function engine.load_stackfile(file) validate("cloe.load_stackfile(string)", file) - local cwd = api.state.current_script_dir or "." + local cwd = api.get_script_dir() or "." if fs.is_relative(file) then file = cwd .. "/" .. file end - api.state.stack:merge_stackfile(file) - return api.state.stack + api.get_stack():merge_stackfile(file) + return api.get_stack() end --- Read JSON file into Lua types (most likely as Lua table). @@ -99,11 +99,11 @@ end --- @return nil function engine.apply_stack(stack) validate("cloe.apply_stack(string|table)", stack) - local file = api.state.current_script_file or "" + local file = api.get_script_file() or "" if type(stack) == "table" then - api.state.stack:merge_stacktable(stack --[[ @as table ]], file) + api.get_stack():merge_stacktable(stack --[[ @as table ]], file) else - api.state.stack:merge_stackjson(stack --[[ @as string ]], file) + api.get_stack():merge_stackjson(stack --[[ @as string ]], file) end end @@ -126,10 +126,18 @@ end --- Alias a set of signals in the Cloe data broker. --- +--- TODO: Does this mean that the signals are also required? +--- { +--- ["^regular expression$"] = +--- } +--- --- @param list table # regular expression to alias key --- @return table # current signal aliases table function engine.alias_signals(list) - -- TODO: Throw an error if simulation already started. + if api.is_simulation_running() then + error("can only alias signals before simulation start") + end + api.initial_input.signal_aliases = luax.tbl_extend("force", api.initial_input.signal_aliases, list) return api.initial_input.signal_aliases end @@ -139,7 +147,10 @@ end --- @param list string[] signals to merge into main list of required signals --- @return string[] # merged list of signals function engine.require_signals(list) - -- TODO: Throw an error if simulation already started. + if api.is_simulation_running() then + error("can only require signals before simulation start") + end + api.initial_input.signal_requires = luax.tbl_extend("force", api.initial_input.signal_requires, list) return api.initial_input.signal_requires end @@ -151,33 +162,50 @@ end --- --- ---@enum Sig --- local Sig = { ---- DriverDoorLatch = "vehicle::framework::chassis::.*driver_door::latch", ---- VehicleMps = "vehicle::sensors::chassis::velocity", +--- DriverDoorLatch = "^vehicle::framework::chassis::.*driver_door::latch$", +--- VehicleMps = "^vehicle::sensors::chassis::velocity$", --- } ---- cloe.require_signals_enum(Sig, true) +--- cloe.require_signals_enum(Sig) --- --- Later, you can use the enum with `cloe.signal()`: --- --- cloe.signal(Sig.DriverDoorLatch) +--- cloe.signal("...") --- --- @param enum table input mappging from enum name to signal name ---- @param alias boolean whether to treat signal names as alias regular expressions --- @return nil -function engine.require_signals_enum(enum, alias) - -- TODO: Throw an error if simulation already started. - local signals = {} - if alias then - local aliases = {} - for key, sigregex in pairs(enum) do - table.insert(aliases, { sigregex, key }) - table.insert(signals, key) +function engine.require_signals_enum(enum) + if api.is_simulation_running() then + error("can only require/alias signals before simulation start") + end + + -- Return true if signal should be treated as a regular expression. + local function is_regex(s) + return string.match(s, "^%^.*%$$") ~= nil + end + + -- We have to handle the following three variants: + -- + -- { + -- A = "A", -- do not alias + -- B = "B.b", -- alias B + -- C = "^C$", -- alias C and ^C$ + -- ["^D$"] = "^D$", -- alias ^D$ + -- } + local signals, aliases = {}, {} + for key, signal in pairs(enum) do + -- Case A + table.insert(signals, signal) + if signal ~= key then + -- Case B and C + table.insert(aliases, { signal, key }) end - engine.alias_signals(aliases) - else - for _, signame in pairs(enum) do - table.insert(signals, signame) + if is_regex(signal) then + -- Case C + table.insert(aliases, { signal, signal }) end end + engine.alias_signals(aliases) engine.require_signals(signals) end @@ -215,23 +243,73 @@ function engine.set_signal(name, value) api.signals[name] = value end ---- Record the given list of signals into the report. +--- Record the given list of signals each cycle and write the results into the +--- report. --- ---- This can be called multiple times, but if the signal is already ---- being recorded, then an error will be raised. +--- This (currently) works by scheduling a Lua function to run every +--- cycle and write the current signal value. If a signal changes +--- value multiple times during a cycle, it's currently *undefined* +--- which of these values will be recorded. --- ---- This should be called before simulation starts, ---- so not from a scheduled callback. +--- This setup function can be called multiple times, but if the output signal +--- name is already being recorded, then an error will be raised. +--- +--- This should be called before simulation starts, so not from a scheduled +--- callback. --- --- You can pass it a list of signals to record, or a mapping ---- from name to +--- from output name or signal name or function to produce value. +--- +--- When just specifying signal names to be recorded without a function +--- defining how they are to be recorded, a default implementation of: +--- +--- function() return cloe.signal(signal_name) end +--- +--- is used. This means that the signal needs to be made available through a +--- call to `cloe.require_signals()` or equivalent. +--- +--- Example 1: plain signal list +--- +--- local want = {"A", "B-signal", "C"} +--- cloe.require_signals(want) +--- cloe.record_signals(want) +--- +--- Example 2: mapping from recorded value to +--- +--- math.randomseed(os.time()) +--- cloe.record_signals({ +--- ["random_number"] = math.random, +--- ["A_subvalue"] = function() return cloe.signal("A").subvalue end, +--- }) +--- +--- Example 3: +--- +--- cloe.record_signals({ +--- "A", +--- ["B"] = "B-signal", +--- ["C"] = function() return cloe.signal("C").subvalue end, +--- }) +--- +--- Example 4: +--- +--- local Sig = { +--- Brake = "^vehicles.default.controllerA.brake_position$", +--- Accel = "^vehicles.default.controllerA.accel_position$", +--- } +--- cloe.require_signal_enum(Sig) +--- cloe.record_signals(Sig) --- --- @param mapping table mapping from signal names --- @return nil function engine.record_signals(mapping) validate("cloe.record_signals(table)", mapping) - api.state.report.signals = api.state.report.signals or {} - local signals = api.state.report.signals + if api.is_simulation_running() then + error("cloe.record_signals() cannot be called after simulation start") + end + + local report = api.get_report() + report.signals = report.signals or {} + local signals = report.signals signals.time = signals.time or {} for sig, getter in pairs(mapping) do if type(sig) == "number" then @@ -259,6 +337,7 @@ function engine.record_signals(mapping) for name, getter in pairs(mapping) do local value if type(name) == "number" then + assert(type(getter) == "string") name = getter end if type(getter) == "string" then @@ -267,8 +346,7 @@ function engine.record_signals(mapping) value = getter() end if value == nil then - -- TODO: Improve error message! - error("nil value received as signal value") + error(string.format("cannot record nil as value for signal %s at %d ms", name, cur_time)) end table.insert(signals[name], value) end @@ -291,8 +369,8 @@ function engine.insert_trigger(trigger) -- events are put in a queue and picked up by the engine at simulation -- start. After this, cloe.state.scheduler exists and we can use its -- methods. - if api.state.scheduler then - api.state.scheduler:insert_trigger(trigger) + if api.get_scheduler() then + api.get_scheduler():insert_trigger(trigger) else table.insert(api.initial_input.triggers, trigger) end @@ -307,8 +385,8 @@ end --- @return nil function engine.execute_action(action) validate("cloe.execute_action(string|table)", action) - if api.state.scheduler then - api.state.scheduler:execute_action(action) + if api.get_scheduler() then + api.get_scheduler():execute_action(action) else error("can only execute actions within scheduled events") end @@ -330,7 +408,7 @@ end local Task do local types = require("tableshape").types - Task = types.shape { + Task = types.shape({ on = types.string + types.table + types.func, run = types.string + types.table + types.func, desc = types.string:is_optional(), @@ -339,7 +417,7 @@ do pin = types.boolean:is_optional(), priority = types.integer:is_optional(), source = types.string:is_optional(), - } + }) end --- @class PartialTask @@ -373,12 +451,9 @@ end local Tasks do local types = require("tableshape").types - Tasks = types.shape( - PartialTaskSpec, - { - extra_fields = types.array_of(PartialTask) - } - ) + Tasks = types.shape(PartialTaskSpec, { + extra_fields = types.array_of(PartialTask), + }) end --- Expand a list of partial tasks to a list of complete tasks. @@ -501,7 +576,7 @@ end local Test do local types = require("tableshape").types - Test = types.shape { + Test = types.shape({ id = types.string, on = types.string + types.table + types.func, run = types.string + types.table + types.func, @@ -509,7 +584,7 @@ do info = types.table:is_optional(), enable = types.boolean:is_optional(), terminate = types.boolean:is_optional(), - } + }) end --- Schedule a test as a coroutine that can yield to Cloe. diff --git a/engine/lua/cloe/events.lua b/engine/lua/cloe/events.lua index baf94a8fa..668ed0630 100644 --- a/engine/lua/cloe/events.lua +++ b/engine/lua/cloe/events.lua @@ -53,12 +53,12 @@ function events.after_tests(...) if #names == 1 then local name = names[1] return function() - return api.state.report.tests[name].complete + return api.get_report().tests[name].complete end else return function() for _, k in ipairs(names) do - if not api.state.report.tests[k].complete then + if not api.get_report().tests[k].complete then return false end end @@ -78,7 +78,7 @@ function events.every(duration) if type(duration) == "string" then duration = types.Duration.new(duration) end - if duration:ns() % api.state.config.simulation.model_step_width ~= 0 then + if duration:ns() % api.get_config().simulation.model_step_width ~= 0 then error("interval duration is not a multiple of nominal step width") end return function(sync) diff --git a/engine/lua/cloe/init.lua b/engine/lua/cloe/init.lua index 96ca5968d..0b52ec41e 100644 --- a/engine/lua/cloe/init.lua +++ b/engine/lua/cloe/init.lua @@ -81,12 +81,12 @@ end --- Require a module, prioritizing modules relative to the script --- launched by cloe-engine. --- ---- If api.state.current_script_dir is nil, this is equivalent to require(). +--- If api.get_script_dir() is nil, this is equivalent to require(). --- --- @param module string module identifier, such as "project" function cloe.require(module) cloe.validate("cloe.require(string)", module) - local script_dir = api.state.current_script_dir + local script_dir = api.get_script_dir() if script_dir then local old_package_path = package.path package.path = string.format("%s/?.lua;%s/?/init.lua;%s", script_dir, script_dir, package.path) @@ -106,7 +106,7 @@ end function cloe.init_report(header) cloe.validate("cloe.init_report(?table)", header) local system = require("cloe.system") - local report = api.state.report + local report = api.get_report() report.metadata = { hostname = system.get_hostname(), username = system.get_username(), diff --git a/engine/lua/cloe/testing.lua b/engine/lua/cloe/testing.lua index 5b61c08da..49660a879 100644 --- a/engine/lua/cloe/testing.lua +++ b/engine/lua/cloe/testing.lua @@ -75,7 +75,7 @@ function TestFixture.new(test, scheduler) local debinfo = debug.getinfo(test.run) local source = string.format("%s:%s-%s", debinfo.short_src, debinfo.linedefined, debinfo.lastlinedefined) - local report = api.state.report + local report = api.get_report() if report["tests"] == nil then report["tests"] = {} end @@ -217,7 +217,7 @@ end --- @private function TestFixture:_terminate() - local report = api.state.report + local report = api.get_report() local tests = 0 local tests_failed = 0 for _, test in pairs(report["tests"]) do diff --git a/engine/src/config.hpp b/engine/src/config.hpp index 93e56cabc..07ac6cc77 100644 --- a/engine/src/config.hpp +++ b/engine/src/config.hpp @@ -49,8 +49,20 @@ #define CLOE_SIMULATION_UUID_VAR "CLOE_SIMULATION_UUID" #endif +#ifndef CLOE_LUA_DEBUGGER_PORT +#define CLOE_LUA_DEBUGGER_PORT 21110 +#endif + // The environment variable from which additional plugins should // be loaded. Takes the same format as PATH. #ifndef CLOE_PLUGIN_PATH #define CLOE_PLUGIN_PATH "CLOE_PLUGIN_PATH" #endif + +#ifndef CLOE_TRIGGER_PATH_DELIMITER +#define CLOE_TRIGGER_PATH_DELIMITER "/" +#endif + +#ifndef CLOE_SIGNAL_PATH_DELIMITER +#define CLOE_SIGNAL_PATH_DELIMITER "/" +#endif diff --git a/engine/src/error_handler.hpp b/engine/src/error_handler.hpp index acbcd023e..7434c1843 100644 --- a/engine/src/error_handler.hpp +++ b/engine/src/error_handler.hpp @@ -18,7 +18,8 @@ #pragma once -#include +#include // for ostream +#include // for stringstream #include // for Error #include // for ConfError, SchemaError @@ -26,6 +27,12 @@ namespace cloe { +/** + * Format various kinds of error so that they are easy to read. + * + * \param exception error to format + * \return formatted string, ready for printing + */ inline std::string format_error(const std::exception& exception) { std::stringstream buf; if (const auto* err = dynamic_cast(&exception); err) { @@ -44,6 +51,21 @@ inline std::string format_error(const std::exception& exception) { return buf.str(); } +/** + * Run a function and print any exception nicely to the ostream provided. + * + * This essentially replaces: + * + * try { ... } + * catch (cloe::ConcludedError&) { ... } + * catch (std::exception&) { ... } + * + * with a single line. + * + * \param out stream to write error message to (e.g. std::cerr) + * \param f function to run + * \return return value of f + */ template auto conclude_error(std::ostream& out, Func f) -> decltype(f()) { try { diff --git a/engine/src/lua_api.cpp b/engine/src/lua_api.cpp index 6043341ab..4a8131b22 100644 --- a/engine/src/lua_api.cpp +++ b/engine/src/lua_api.cpp @@ -25,7 +25,7 @@ namespace cloe { -sol::protected_function_result lua_safe_script_file(sol::state_view& lua, +sol::protected_function_result lua_safe_script_file(sol::state_view lua, const std::filesystem::path& filepath) { auto file = std::filesystem::path(filepath); auto dir = file.parent_path().generic_string(); @@ -33,16 +33,17 @@ sol::protected_function_result lua_safe_script_file(sol::state_view& lua, dir = "."; } - auto state = luat_cloe_engine_state(lua); - auto old_file = state["current_script_file"]; - auto old_dir = state["current_script_dir"]; - state["scripts_loaded"].get().add(file.generic_string()); - state["current_script_file"] = file.generic_string(); - state["current_script_dir"] = dir; + sol::object old_file = luat_cloe_engine_state(lua)["current_script_file"]; + sol::object old_dir = luat_cloe_engine_state(lua)["current_script_dir"]; + sol::table scripts_loaded = luat_cloe_engine_state(lua)["scripts_loaded"]; + scripts_loaded[scripts_loaded.size() + 1] = file.generic_string(); + luat_cloe_engine_state(lua)["scripts_loaded"] = scripts_loaded; + luat_cloe_engine_state(lua)["current_script_file"] = file.generic_string(); + luat_cloe_engine_state(lua)["current_script_dir"] = dir; logger::get("cloe")->info("Loading {}", file.generic_string()); auto result = lua.safe_script_file(file.generic_string(), sol::script_pass_on_error); - state["current_script_file"] = old_file; - state["current_script_dir"] = old_dir; + luat_cloe_engine_state(lua)["current_script_file"] = old_file; + luat_cloe_engine_state(lua)["current_script_dir"] = old_dir; return result; } diff --git a/engine/src/lua_api.hpp b/engine/src/lua_api.hpp index 1af36e676..83763f012 100644 --- a/engine/src/lua_api.hpp +++ b/engine/src/lua_api.hpp @@ -36,7 +36,7 @@ namespace cloe { * Safely load and run a user Lua script. */ [[nodiscard]] sol::protected_function_result lua_safe_script_file( - sol::state_view& lua, const std::filesystem::path& filepath); + sol::state_view lua, const std::filesystem::path& filepath); /** * Return the cloe-engine table as it is exported into Lua. @@ -46,28 +46,28 @@ namespace cloe { * engine/lua/cloe-engine/init.lua * */ -[[nodiscard]] inline auto luat_cloe_engine(sol::state_view& lua) { - return lua["package"]["loaded"]["cloe-engine"]; +[[nodiscard]] inline auto luat_cloe_engine(sol::state_view lua) { + return lua.globals()["package"]["loaded"]["cloe-engine"]; } -[[nodiscard]] inline auto luat_cloe_engine_fs(sol::state_view& lua) { - return lua["package"]["loaded"]["cloe-engine.fs"]; +[[nodiscard]] inline auto luat_cloe_engine_fs(sol::state_view lua) { + return lua.globals()["package"]["loaded"]["cloe-engine.fs"]; } -[[nodiscard]] inline auto luat_cloe_engine_types(sol::state_view& lua) { - return lua["package"]["loaded"]["cloe-engine.types"]; +[[nodiscard]] inline auto luat_cloe_engine_types(sol::state_view lua) { + return lua.globals()["package"]["loaded"]["cloe-engine.types"]; } -[[nodiscard]] inline auto luat_cloe_engine_initial_input(sol::state_view& lua) { - return lua["package"]["loaded"]["cloe-engine"]["initial_input"]; +[[nodiscard]] inline auto luat_cloe_engine_initial_input(sol::state_view lua) { + return lua.globals()["package"]["loaded"]["cloe-engine"]["initial_input"]; } -[[nodiscard]] inline auto luat_cloe_engine_state(sol::state_view& lua) { - return lua["package"]["loaded"]["cloe-engine"]["state"]; +[[nodiscard]] inline auto luat_cloe_engine_state(sol::state_view lua) { + return lua.globals()["package"]["loaded"]["cloe-engine"]["state"]; } -[[nodiscard]] inline auto luat_cloe_engine_plugins(sol::state_view& lua) { - return lua["package"]["loaded"]["cloe-engine"]["plugins"]; +[[nodiscard]] inline auto luat_cloe_engine_plugins(sol::state_view lua) { + return lua.globals()["package"]["loaded"]["cloe-engine"]["plugins"]; } } // namespace cloe diff --git a/engine/src/lua_debugger.cpp b/engine/src/lua_debugger.cpp index 8f68f8fe2..13f4e6f6c 100644 --- a/engine/src/lua_debugger.cpp +++ b/engine/src/lua_debugger.cpp @@ -26,7 +26,7 @@ namespace cloe { -void start_lua_debugger(sol::state& lua, int listen_port) { +void start_lua_debugger(sol::state_view lua, int listen_port) { static lrdb::server debug_server(listen_port); debug_server.reset(lua.lua_state()); } diff --git a/engine/src/lua_setup.cpp b/engine/src/lua_setup.cpp index b6c21a434..e1cdf1146 100644 --- a/engine/src/lua_setup.cpp +++ b/engine/src/lua_setup.cpp @@ -22,6 +22,7 @@ #include "lua_setup.hpp" #include // for path +#include #include // for state_view @@ -107,7 +108,7 @@ int lua_exception_handler(lua_State* L, sol::optional may * * \see lua_setup_builtin.cpp */ -void configure_package_path(sol::state_view& lua, const std::vector& paths) { +void configure_package_path(sol::state_view lua, const std::vector& paths) { std::string package_path = lua["package"]["path"]; for (const std::string& p : paths) { package_path += ";" + p + "/?.lua"; @@ -119,7 +120,7 @@ void configure_package_path(sol::state_view& lua, const std::vector /** * Add Lua package paths so that bundled Lua libaries can be found. */ -void register_package_path(sol::state_view& lua, const LuaOptions& opt) { +void register_package_path(sol::state_view lua, const LuaOptions& opt) { // Setup lua path: std::vector lua_path{}; if (!opt.no_system_lua) { @@ -157,7 +158,7 @@ void register_package_path(sol::state_view& lua, const LuaOptions& opt) { * * engine/lua/cloe-engine/init.lua */ -void register_cloe_engine(sol::state_view& lua, Stack& stack) { +void register_cloe_engine(sol::state_view lua, Stack& stack) { sol::table tbl = lua.create_table(); // Initial input will be processed at simulation start. @@ -175,10 +176,10 @@ void register_cloe_engine(sol::state_view& lua, Stack& stack) { tbl["state"] = lua.create_table(); tbl["state"]["report"] = lua.create_table(); tbl["state"]["stack"] = std::ref(stack); - tbl["state"]["config"] = fable::into_sol_object(lua, stack.active_config()); tbl["state"]["scheduler"] = sol::lua_nil; tbl["state"]["current_script_file"] = sol::lua_nil; tbl["state"]["current_script_dir"] = sol::lua_nil; + tbl["state"]["is_running"] = false; tbl["state"]["scripts_loaded"] = lua.create_table(); tbl["state"]["features"] = lua.create_table_with( // Version compatibility: @@ -188,8 +189,16 @@ void register_cloe_engine(sol::state_view& lua, Stack& stack) { "cloe-0.19", true, "cloe-0.20.0", true, "cloe-0.20", true, - "cloe-0.21.0", true, // nightly - "cloe-0.21", true, // nightly + "cloe-0.21.0", true, + "cloe-0.21", true, + "cloe-0.22.0", true, + "cloe-0.22", true, + "cloe-0.23.0", true, + "cloe-0.23", true, + "cloe-0.24.0", true, + "cloe-0.24", true, + "cloe-0.25.0", true, + "cloe-0.25", true, // Stackfile versions support: "cloe-stackfile", true, @@ -203,8 +212,11 @@ void register_cloe_engine(sol::state_view& lua, Stack& stack) { ); // clang-format on -#if 0 tbl.set_function("is_available", []() { return true; }); + + tbl.set_function("is_simulation_running", + [](sol::this_state lua) { return luat_cloe_engine_state(lua)["is_running"]; }); + tbl.set_function("get_script_file", [](sol::this_state lua) { return luat_cloe_engine_state(lua)["current_script_file"]; }); @@ -217,16 +229,18 @@ void register_cloe_engine(sol::state_view& lua, Stack& stack) { [](sol::this_state lua) { return luat_cloe_engine_state(lua)["scheduler"]; }); tbl.set_function("get_features", [](sol::this_state lua) { return luat_cloe_engine_state(lua)["features"]; }); - tbl.set_function("get_stack", - [](sol::this_state lua) { return luat_cloe_engine_state(lua)["stack"]; }); -#endif + tbl.set_function("get_stack", [&stack]() { return std::ref(stack); }); + tbl.set_function("get_config", [&stack](sol::this_state lua) { + return fable::into_sol_object(lua, stack.active_config()); + }); + tbl.set_function("log", cloe_api_log); tbl.set_function("exec", cloe_api_exec); luat_cloe_engine(lua) = tbl; } -void register_enum_loglevel(sol::state_view& lua, sol::table& tbl) { +void register_enum_loglevel(sol::state_view lua, sol::table& tbl) { // clang-format off tbl["LogLevel"] = lua.create_table_with( "TRACE", "trace", @@ -250,7 +264,7 @@ void register_enum_loglevel(sol::state_view& lua, sol::table& tbl) { * * engine/lua/cloe-engine/types.lua */ -void register_cloe_engine_types(sol::state_view& lua) { +void register_cloe_engine_types(sol::state_view lua) { sol::table tbl = lua.create_table(); register_usertype_duration(tbl); register_usertype_sync(tbl); @@ -270,7 +284,7 @@ void register_cloe_engine_types(sol::state_view& lua) { * * engine/lua/cloe-engine/fs.lua */ -void register_cloe_engine_fs(sol::state_view& lua) { +void register_cloe_engine_fs(sol::state_view lua) { sol::table tbl = lua.create_table(); register_lib_fs(tbl); luat_cloe_engine_fs(lua) = tbl; @@ -282,7 +296,7 @@ void register_cloe_engine_fs(sol::state_view& lua) { * You can just use `cloe`, and it will auto-require the cloe module. * If you don't use it, then it won't be loaded. */ -void register_cloe(sol::state_view& lua) { +void register_cloe(sol::state_view lua) { // This takes advantage of the `__index` function for metatables, which is called // when a key can't be found in the original table, here an empty table // assigned to cloe. It then loads the cloe module, and returns the key @@ -305,9 +319,8 @@ void register_cloe(sol::state_view& lua) { } // anonymous namespace -sol::state new_lua(const LuaOptions& opt, Stack& stack) { +void setup_lua(sol::state_view lua, const LuaOptions& opt, Stack& stack) { // clang-format off - sol::state lua; lua.open_libraries( sol::lib::base, sol::lib::coroutine, @@ -329,10 +342,9 @@ sol::state new_lua(const LuaOptions& opt, Stack& stack) { if (opt.auto_require_cloe) { register_cloe(lua); } - return lua; } -void merge_lua(sol::state_view& lua, const std::string& filepath) { +void merge_lua(sol::state_view lua, const std::string& filepath) { logger::get("cloe")->debug("Load script {}", filepath); auto result = lua_safe_script_file(lua, std::filesystem::path(filepath)); if (!result.valid()) { diff --git a/engine/src/lua_setup.hpp b/engine/src/lua_setup.hpp index 00e473675..fc183b45c 100644 --- a/engine/src/lua_setup.hpp +++ b/engine/src/lua_setup.hpp @@ -56,7 +56,7 @@ struct LuaOptions { * \see stack_factory.hpp * \see lua_setup.cpp */ -sol::state new_lua(const LuaOptions& opt, Stack& s); +void setup_lua(sol::state_view lua, const LuaOptions& opt, Stack& s); #if CLOE_ENGINE_WITH_LRDB /** @@ -65,7 +65,7 @@ sol::state new_lua(const LuaOptions& opt, Stack& s); * \param lua * \param listen_port */ -void start_lua_debugger(sol::state& lua, int listen_port); +void start_lua_debugger(sol::state_view lua, int listen_port); #endif /** @@ -73,7 +73,7 @@ void start_lua_debugger(sol::state& lua, int listen_port); * * \see lua_setup.cpp */ -void merge_lua(sol::state_view& lua, const std::string& filepath); +void merge_lua(sol::state_view lua, const std::string& filepath); /** * Define the filesystem library functions in the given table. diff --git a/engine/src/lua_setup_test.cpp b/engine/src/lua_setup_test.cpp new file mode 100644 index 000000000..0e6966316 --- /dev/null +++ b/engine/src/lua_setup_test.cpp @@ -0,0 +1,125 @@ +/* + * Copyright 2024 Robert Bosch GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include // for tmpnam +#include +#include +#include + +#include + +#include "lua_api.hpp" // for lua_safe_script_file +#include "lua_setup.hpp" // for setup_lua +#include "stack.hpp" // for Stack +using namespace cloe; // NOLINT(build/namespaces) + +class cloe_lua_setup : public testing::Test { + protected: + sol::state lua_state; + sol::state_view lua; + LuaOptions opt; + Stack stack; + + std::vector defer_deletion_; + + cloe_lua_setup() : lua(lua_state.lua_state()) { + opt.environment = std::make_unique(); + } + + std::filesystem::path WriteTempLuaFile(std::string_view content) { + // NOTE: Regarding the danger of std::tmpname. + // It is required to have a real file in the filesystem that can be + // then laoded by lua.safe_script_file. Because tests are run in parallel, + // this filename needs to be reasonably random. Since this program is not + // running at high privileges, the potential attack vector should not result + // in an escalation of privileges. + auto temp_file = std::filesystem::path(std::string(std::tmpnam(nullptr)) + ".lua"); // NOLINT + std::ofstream ofs(temp_file); + if (!ofs.is_open()) { + throw std::ios_base::failure("Failed to create temporary file"); + } + ofs << content; + ofs.close(); + + defer_deletion_.push_back(temp_file); + return temp_file; + } + + void TearDown() override { + for (const auto& f : defer_deletion_) { + std::filesystem::remove(f); + } + } +}; + +TEST_F(cloe_lua_setup, cloe_engine_is_available) { + setup_lua(lua, opt, stack); + lua.script(R"( + local api = require("cloe-engine") + assert(api.is_available()) + assert(not api.is_simulation_running()) + )"); +} + +TEST_F(cloe_lua_setup, describe_cloe) { + setup_lua(lua, opt, stack); + ASSERT_EQ(lua.script("local cloe = require('cloe'); return cloe.inspect(cloe.LogLevel.CRITICAL)").get(), + std::string("\"critical\"")); +} + +TEST_F(cloe_lua_setup, describe_cloe_without_require) { + opt.auto_require_cloe = true; + setup_lua(lua, opt, stack); + ASSERT_EQ(lua.script("return cloe.inspect(cloe.LogLevel.CRITICAL)").get(), + std::string("\"critical\"")); +} + +TEST_F(cloe_lua_setup, read_engine_state) { + setup_lua(lua, opt, stack); + ASSERT_TRUE(lua.script("return require('cloe-engine').is_available()").get()); + ASSERT_FALSE(luat_cloe_engine_state(lua)["is_running"].get()); +} + +TEST_F(cloe_lua_setup, write_engine_state) { + setup_lua(lua, opt, stack); + luat_cloe_engine_state(lua)["extra"] = "hello world!"; + ASSERT_EQ(lua.script("return require('cloe-engine').state.extra").get(), + std::string("hello world!")); +} + +TEST_F(cloe_lua_setup, write_engine_state_table) { + setup_lua(lua, opt, stack); + sol::table state = luat_cloe_engine_state(lua); + sol::table scripts_loaded = state["scripts_loaded"]; + ASSERT_TRUE(scripts_loaded.valid()); + scripts_loaded[scripts_loaded.size() + 1] = "hello_world.lua"; + ASSERT_EQ(lua.script("return require('cloe-engine').state.scripts_loaded[1]").get(), + std::string("hello_world.lua")); +} + +TEST_F(cloe_lua_setup, lua_safe_script_file) { + auto file = WriteTempLuaFile(R"( + local cloe = require("cloe") + local api = require("cloe-engine") + return api.get_script_file() + )"); + setup_lua(lua, opt, stack); + ASSERT_EQ(lua_safe_script_file(lua, file).get(), file.generic_string()); +} diff --git a/engine/src/lua_stack_test.cpp b/engine/src/lua_stack_test.cpp index 9022fcf8a..7b96d08ce 100644 --- a/engine/src/lua_stack_test.cpp +++ b/engine/src/lua_stack_test.cpp @@ -1,3 +1,20 @@ +/* + * Copyright 2024 Robert Bosch GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ #include #include diff --git a/engine/src/main.cpp b/engine/src/main.cpp index 081972759..dbc18e84e 100644 --- a/engine/src/main.cpp +++ b/engine/src/main.cpp @@ -63,6 +63,12 @@ int main(int argc, char** argv) { check->add_option("-J,--json-indent", check_options.json_indent, "JSON indentation level"); check->add_option("files", check_files, "Files to check"); + engine::ProbeOptions probe_options{}; + std::vector probe_files{}; + auto* probe = app.add_subcommand("probe", "Probe a simulation with (merged) stack files."); + probe->add_option("-J,--json-indent", probe_options.json_indent, "JSON indentation level"); + probe->add_option("files", probe_files, "Files to merge into a single stackfile")->required(); + // Run Command: engine::RunOptions run_options{}; std::vector run_files{}; diff --git a/engine/src/main_commands.hpp b/engine/src/main_commands.hpp index 848337a20..6716f9846 100644 --- a/engine/src/main_commands.hpp +++ b/engine/src/main_commands.hpp @@ -24,6 +24,7 @@ #include #include +#include "config.hpp" #include "lua_setup.hpp" #include "stack_factory.hpp" @@ -58,6 +59,19 @@ struct DumpOptions { int dump(const DumpOptions& opt, const std::vector& filepaths); +struct ProbeOptions { + cloe::StackOptions stack_options; + cloe::LuaOptions lua_options; + + std::ostream* output = &std::cout; + std::ostream* error = &std::cerr; + + // Flags: + int json_indent = 2; +}; + +int probe(const ProbeOptions& opt, const std::vector& filepaths); + struct RunOptions { cloe::StackOptions stack_options; cloe::LuaOptions lua_options; @@ -75,9 +89,10 @@ struct RunOptions { bool write_output = true; bool require_success = false; bool report_progress = true; + bool probe_simulation = false; bool debug_lua = false; - int debug_lua_port = 21110; + int debug_lua_port = CLOE_LUA_DEBUGGER_PORT; }; int run(const RunOptions& opt, const std::vector& filepaths); diff --git a/engine/src/main_probe.cpp b/engine/src/main_probe.cpp new file mode 100644 index 000000000..8d1afb4f1 --- /dev/null +++ b/engine/src/main_probe.cpp @@ -0,0 +1,42 @@ +/* + * Copyright 2024 Robert Bosch GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "main_commands.hpp" + +namespace engine { + +int probe(const ProbeOptions& opt, const std::vector& filepaths) { + RunOptions ropt; + + // Use settings from ProbeOptions: + ropt.stack_options = opt.stack_options; + ropt.lua_options = opt.lua_options; + ropt.output = opt.output; + ropt.error = opt.error; + ropt.json_indent = opt.json_indent; + + // Update defaults: + ropt.allow_empty = true; + ropt.write_output = false; + ropt.report_progress = false; + ropt.probe_simulation = true; + + return run(ropt, filepaths); +} + +} // namespace engine diff --git a/engine/src/main_run.cpp b/engine/src/main_run.cpp index ce7068e3d..8ad425a6e 100644 --- a/engine/src/main_run.cpp +++ b/engine/src/main_run.cpp @@ -62,12 +62,14 @@ int run(const RunOptions& opt, const std::vector& filepaths) { opt.stack_options.environment->set(CLOE_SIMULATION_UUID_VAR, uuid); // Load the stack file: + sol::state lua_state; + sol::state_view lua_view(lua_state.lua_state()); cloe::Stack stack = cloe::new_stack(opt.stack_options); - sol::state lua = cloe::new_lua(opt.lua_options, stack); + cloe::setup_lua(lua_view, opt.lua_options, stack); #if CLOE_ENGINE_WITH_LRDB if (opt.debug_lua) { log->info("Lua debugger listening at port: {}", opt.debug_lua_port); - cloe::start_lua_debugger(lua, opt.debug_lua_port); + cloe::start_lua_debugger(lua_view, opt.debug_lua_port); } #else if (opt.debug_lua) { @@ -78,7 +80,7 @@ int run(const RunOptions& opt, const std::vector& filepaths) { cloe::conclude_error(*opt.stack_options.error, [&]() { for (const auto& file : filepaths) { if (boost::algorithm::ends_with(file, ".lua")) { - cloe::merge_lua(lua, file); + cloe::merge_lua(lua_view, file); } else { cloe::merge_stack(opt.stack_options, stack, file); } @@ -96,12 +98,13 @@ int run(const RunOptions& opt, const std::vector& filepaths) { } // Create simulation: - Simulation sim(std::move(stack), std::move(lua), uuid); + Simulation sim(std::move(stack), lua_view, uuid); GLOBAL_SIMULATION_INSTANCE = ∼ std::ignore = std::signal(SIGINT, handle_signal); // Set options: sim.set_report_progress(opt.report_progress); + sim.set_probe_simulation(opt.probe_simulation); // Run simulation: auto result = cloe::conclude_error(*opt.stack_options.error, [&]() { return sim.run(); }); @@ -115,7 +118,11 @@ int run(const RunOptions& opt, const std::vector& filepaths) { if (opt.write_output) { sim.write_output(result); } - *opt.output << cloe::Json(result).dump(opt.json_indent) << std::endl; + if (opt.probe_simulation) { + *opt.output << cloe::Json(result.signals) << "\n" << std::flush; + } else { + *opt.output << cloe::Json(result).dump(opt.json_indent) << "\n" << std::flush; + } switch (result.outcome) { case SimulationOutcome::Success: @@ -128,6 +135,8 @@ int run(const RunOptions& opt, const std::vector& filepaths) { return EXIT_OUTCOME_NOSTART; case SimulationOutcome::Failure: return EXIT_OUTCOME_FAILURE; + case SimulationOutcome::Probing: + return EXIT_OUTCOME_SUCCESS; default: return EXIT_OUTCOME_UNKNOWN; } @@ -157,7 +166,7 @@ void handle_signal(int sig) { break; case SIGINT: default: - std::cerr << std::endl; // print newline so that ^C is on its own line + std::cerr << "\n" << std::flush; // print newline so that ^C is on its own line if (++interrupts == 3) { std::ignore = std::signal(sig, SIG_DFL); // third time goes to the default handler } diff --git a/engine/src/main_shell.cpp b/engine/src/main_shell.cpp index 732f2cbe4..09776d397 100644 --- a/engine/src/main_shell.cpp +++ b/engine/src/main_shell.cpp @@ -39,7 +39,7 @@ void print_error(std::ostream& os, const S& chunk) { os << sol::to_string(chunk.status()) << " error: " << err.what() << std::endl; } -bool evaluate(sol::state& lua, std::ostream& os, const char* buf) { +bool evaluate(sol::state_view lua, std::ostream& os, const char* buf) { try { auto result = lua.safe_script(buf, sol::script_pass_on_error); if (!result.valid()) { @@ -53,7 +53,7 @@ bool evaluate(sol::state& lua, std::ostream& os, const char* buf) { return true; } -int noninteractive_shell(sol::state& lua, std::ostream& os, const std::vector& actions, +int noninteractive_shell(sol::state_view lua, std::ostream& os, const std::vector& actions, bool ignore_errors) { int errors = 0; for (const auto& action : actions) { @@ -68,7 +68,7 @@ int noninteractive_shell(sol::state& lua, std::ostream& os, const std::vector& actions, +void interactive_shell(sol::state_view lua, std::ostream& os, const std::vector& actions, bool ignore_errors) { constexpr auto PROMPT = "> "; constexpr auto PROMPT_CONTINUE = ">> "; @@ -165,7 +165,10 @@ int shell(const ShellOptions& opt, const std::vector& filepaths) { cloe::Stack stack = cloe::new_stack(stack_opt); auto lopt = opt.lua_options; lopt.auto_require_cloe = true; - sol::state lua = cloe::new_lua(lopt, stack); + + sol::state lua_state; + sol::state_view lua_view(lua_state.lua_state()); + cloe::setup_lua(lua_view, lopt, stack); // Collect input files and strings to execute std::vector actions{}; @@ -178,12 +181,12 @@ int shell(const ShellOptions& opt, const std::vector& filepaths) { // Determine whether we should be interactive or not bool interactive = opt.interactive ? *opt.interactive : opt.commands.empty() && filepaths.empty(); if (!interactive) { - auto errors = noninteractive_shell(lua, *opt.error, actions, opt.ignore_errors); + auto errors = noninteractive_shell(lua_view, *opt.error, actions, opt.ignore_errors); if (errors != 0) { return EXIT_FAILURE; } } else { - interactive_shell(lua, *opt.output, actions, opt.ignore_errors); + interactive_shell(lua_view, *opt.output, actions, opt.ignore_errors); } return EXIT_SUCCESS; } diff --git a/engine/src/registrar.hpp b/engine/src/registrar.hpp index cc88edfc8..aa9b5c9b4 100644 --- a/engine/src/registrar.hpp +++ b/engine/src/registrar.hpp @@ -21,13 +21,15 @@ #pragma once -#include // for unique_ptr<>, shared_ptr<> -#include // for string +#include // for unique_ptr<>, shared_ptr<> +#include // for string +#include // for string_view #include // for cloe::Registrar #include "coordinator.hpp" // for Coordinator #include "server.hpp" // for Server, ServerRegistrar +#include "config.hpp" // for CLOE_TRIGGER_PATH_DELIMITER, ... namespace engine { @@ -59,55 +61,67 @@ class Registrar : public cloe::Registrar { server_registrar_->register_api_handler(endpoint, t, h); } - std::unique_ptr clone() const { + [[nodiscard]] std::unique_ptr clone() const { return std::make_unique(*this, "", "", ""); } std::unique_ptr with_static_prefix(const std::string& prefix) const override { - assert(prefix.size() > 0); + assert(!prefix.empty()); return std::make_unique(*this, "", prefix, ""); } std::unique_ptr with_api_prefix(const std::string& prefix) const override { - assert(prefix.size() > 0); + assert(!prefix.empty()); return std::make_unique(*this, "", "", prefix); } std::unique_ptr with_trigger_prefix(const std::string& prefix) const override { - assert(prefix.size() > 0 && prefix[0] != '_'); + assert(!prefix.empty() && prefix[0] != '_'); return std::make_unique(*this, prefix, "", ""); } - std::string trigger_key(const std::string& name) { - assert(name.size() != 0); + [[nodiscard]] std::string make_prefix(std::string_view name, std::string_view delim) const { + assert(!name.empty()); - if (trigger_prefix_.size() == 0) { + if (trigger_prefix_.empty()) { // This only works for Cloe internal triggers. - return name; + return std::string(name); } + + std::string prefix = trigger_prefix_; if (name == "_") { // Special case: "_" means we can actually use just trigger_prefix_. // This might cause a problem if we name a plugin the same as one // of the internal Cloe triggers... - return trigger_prefix_; + return prefix; } - return trigger_prefix_ + "/" + name; + prefix += delim; + prefix += name; + return prefix; + } + + [[nodiscard]] std::string make_trigger_name(std::string_view name) const { + return make_prefix(name, CLOE_TRIGGER_PATH_DELIMITER); + } + + [[nodiscard]] std::string make_signal_name(std::string_view name) const override { + return make_prefix(name, CLOE_SIGNAL_PATH_DELIMITER); } void register_action(cloe::ActionFactoryPtr&& af) override { - coordinator_->register_action(trigger_key(af->name()), std::move(af)); + coordinator_->register_action(make_trigger_name(af->name()), std::move(af)); } void register_event( cloe::EventFactoryPtr&& ef, std::shared_ptr storage) override { - coordinator_->register_event(trigger_key(ef->name()), std::move(ef), storage); + coordinator_->register_event(make_trigger_name(ef->name()), std::move(ef), storage); } sol::table register_lua_table() override { return coordinator_->register_lua_table(trigger_prefix_); } - cloe::DataBroker& data_broker() const override { + [[nodiscard]] cloe::DataBroker& data_broker() const override { assert(data_broker_ != nullptr); return *data_broker_; } diff --git a/engine/src/simulation.cpp b/engine/src/simulation.cpp index 4ad51082b..ac42d1a2f 100644 --- a/engine/src/simulation.cpp +++ b/engine/src/simulation.cpp @@ -726,51 +726,7 @@ StateId SimulationMachine::Connect::impl(SimulationContext& ctx) { cloe::handler::StaticJson(ctx.controller_ids())); } - ctx.progress.init_end(); - ctx.server->refresh_buffer_start_stream(); - logger()->info("Simulation initialization complete."); - return START; -} - -// START --------------------------------------------------------------------------------------- // - -size_t insert_triggers_from_config(SimulationContext& ctx) { - auto r = ctx.coordinator->trigger_registrar(cloe::Source::FILESYSTEM); - size_t count = 0; - for (const auto& c : ctx.config.triggers) { - if (!ctx.config.engine.triggers_ignore_source && source_is_transient(c.source)) { - continue; - } - try { - r->insert_trigger(c.conf()); - count++; - } catch (cloe::SchemaError& e) { - ctx.logger()->error("Error inserting trigger: {}", e.what()); - std::stringstream s; - fable::pretty_print(e, s); - ctx.logger()->error("> Message:\n {}", s.str()); - throw cloe::ConcludedError(e); - } catch (cloe::TriggerError& e) { - ctx.logger()->error("Error inserting trigger ({}): {}", e.what(), c.to_json().dump()); - throw cloe::ConcludedError(e); - } - } - return count; -} - -/** - * Pseudo-class which hosts the Cloe-Signals as properties inside of the Lua-VM - */ -class LuaCloeSignal {}; - -StateId SimulationMachine::Start::impl(SimulationContext& ctx) { - logger()->info("Starting simulation..."); - - // Begin execution progress - ctx.progress.exec_begin(); - - { - // Bind lua state_view to databroker + { // 8. Initialize Databroker & Lua auto* dbPtr = ctx.coordinator->data_broker(); if (!dbPtr) { throw std::logic_error("Coordinator did not provide a DataBroker instance"); @@ -959,6 +915,53 @@ StateId SimulationMachine::Start::impl(SimulationContext& ctx) { } } } + ctx.progress.init_end(); + ctx.server->refresh_buffer_start_stream(); + logger()->info("Simulation initialization complete."); + if (ctx.probe_simulation) { + ctx.outcome = SimulationOutcome::Probing; + return DISCONNECT; + } + return START; +} + +// START --------------------------------------------------------------------------------------- // + +size_t insert_triggers_from_config(SimulationContext& ctx) { + auto r = ctx.coordinator->trigger_registrar(cloe::Source::FILESYSTEM); + size_t count = 0; + for (const auto& c : ctx.config.triggers) { + if (!ctx.config.engine.triggers_ignore_source && source_is_transient(c.source)) { + continue; + } + try { + r->insert_trigger(c.conf()); + count++; + } catch (cloe::SchemaError& e) { + ctx.logger()->error("Error inserting trigger: {}", e.what()); + std::stringstream s; + fable::pretty_print(e, s); + ctx.logger()->error("> Message:\n {}", s.str()); + throw cloe::ConcludedError(e); + } catch (cloe::TriggerError& e) { + ctx.logger()->error("Error inserting trigger ({}): {}", e.what(), c.to_json().dump()); + throw cloe::ConcludedError(e); + } + } + return count; +} + +/** + * Pseudo-class which hosts the Cloe-Signals as properties inside of the Lua-VM + */ +class LuaCloeSignal {}; + +StateId SimulationMachine::Start::impl(SimulationContext& ctx) { + logger()->info("Starting simulation..."); + + // Begin execution progress + ctx.progress.exec_begin(); + // Process initial trigger list insert_triggers_from_config(ctx); @@ -1401,7 +1404,7 @@ StateId SimulationMachine::Abort::impl(SimulationContext& ctx) { // --------------------------------------------------------------------------------------------- // -Simulation::Simulation(cloe::Stack&& config, sol::state&& lua, const std::string& uuid) +Simulation::Simulation(cloe::Stack&& config, sol::state_view lua, const std::string& uuid) : config_(std::move(config)) , lua_(std::move(lua)) , logger_(cloe::logger::get("cloe")) @@ -1550,6 +1553,7 @@ SimulationResult Simulation::run() { ctx.commander->set_enabled(config_.engine.security_enable_commands); // Run the simulation + cloe::luat_cloe_engine_state(lua_)["is_running"] = true; machine.run(ctx); } catch (cloe::ConcludedError& e) { r.errors.emplace_back(e.what()); @@ -1572,6 +1576,7 @@ SimulationResult Simulation::run() { // (We could provide an option to time-out; this would involve using wait_for // instead of wait.) ctx.commander->wait_all(); + abort_fn_ = nullptr; // TODO(ben): Preserve NoStart outcome. if (ctx.outcome) { @@ -1579,20 +1584,20 @@ SimulationResult Simulation::run() { } else { r.outcome = SimulationOutcome::Aborted; } + r.sync = ctx.sync; r.statistics = ctx.statistics; r.elapsed = ctx.progress.elapsed(); r.triggers = ctx.coordinator->history(); r.report = sol::object(cloe::luat_cloe_engine_state(ctx.lua)["report"]); // Don't create output file data unless the output files are being written - if (ctx.config.engine.output_file_signals) { + if (ctx.config.engine.output_file_signals || ctx.probe_simulation) { r.signals = dump_signals(*ctx.db); } if (ctx.config.engine.output_file_signals_autocompletion) { r.signals_autocompletion = dump_signals_autocompletion(*ctx.db); } - abort_fn_ = nullptr; return r; } @@ -1636,7 +1641,7 @@ bool Simulation::write_output_file(const std::filesystem::path& filepath, return false; } logger()->debug("Writing file: {}", native); - ofs << j.dump(2) << std::endl; + ofs << j.dump(2) << "\n"; return true; } diff --git a/engine/src/simulation.hpp b/engine/src/simulation.hpp index c83abcada..bc7dcf917 100644 --- a/engine/src/simulation.hpp +++ b/engine/src/simulation.hpp @@ -112,7 +112,7 @@ struct SimulationResult { class Simulation { public: - Simulation(cloe::Stack&& config, sol::state&& lua, const std::string& uuid); + Simulation(cloe::Stack&& config, sol::state_view lua, const std::string& uuid); ~Simulation() = default; /** @@ -148,6 +148,11 @@ class Simulation { */ void set_report_progress(bool value) { report_progress_ = value; } + /** + * Set whether simulation should be probed for information only. + */ + void set_probe_simulation(bool value) { probe_simulation_ = value; } + /** * Abort the simulation from a separate thread. * @@ -157,13 +162,14 @@ class Simulation { private: cloe::Stack config_; - sol::state lua_; + sol::state_view lua_; cloe::Logger logger_; std::string uuid_; std::function abort_fn_; // Options: bool report_progress_{false}; + bool probe_simulation_{false}; }; } // namespace engine diff --git a/engine/src/simulation_context.hpp b/engine/src/simulation_context.hpp index cb2fd73b2..7f9bcd335 100644 --- a/engine/src/simulation_context.hpp +++ b/engine/src/simulation_context.hpp @@ -22,15 +22,15 @@ #pragma once -#include // for uint64_t -#include // for function<> -#include // for map<> -#include // for unique_ptr<>, shared_ptr<> -#include // for optional<> -#include // for string -#include // for vector<> +#include // for uint64_t +#include // for function<> +#include // for map<> +#include // for unique_ptr<>, shared_ptr<> +#include // for optional<> +#include // for string +#include // for vector<> -#include // for state_view +#include // for state_view #include // for Simulator, Controller, Registrar, Vehicle, Duration #include // for DataBroker @@ -39,13 +39,13 @@ #include // for Accumulator #include // for DurationTimer -#include "coordinator.hpp" // for Coordinator -#include "registrar.hpp" // for Registrar -#include "server.hpp" // for Server -#include "simulation_progress.hpp" // for SimulationProgress -#include "stack.hpp" // for Stack -#include "utility/command.hpp" // for CommandExecuter -#include "utility/time_event.hpp" // for TimeCallback +#include "coordinator.hpp" // for Coordinator +#include "registrar.hpp" // for Registrar +#include "server.hpp" // for Server +#include "simulation_progress.hpp" // for SimulationProgress +#include "stack.hpp" // for Stack +#include "utility/command.hpp" // for CommandExecuter +#include "utility/time_event.hpp" // for TimeCallback namespace engine { @@ -153,6 +153,7 @@ enum class SimulationOutcome { Stopped, ///< Simulation concluded, but without valuation. Failure, ///< Simulation explicitly concluded with failure. Success, ///< Simulation explicitly concluded with success. + Probing, ///< Simulation started briefly to gather specific information. }; // If possible, the following exit codes should not be used as they are used @@ -173,6 +174,7 @@ ENUM_SERIALIZATION(SimulationOutcome, ({ {SimulationOutcome::Failure, "failure"}, {SimulationOutcome::Success, "success"}, {SimulationOutcome::Stopped, "stopped"}, + {SimulationOutcome::Probing, "probing"}, })) // clang-format on @@ -197,7 +199,7 @@ DEFINE_NIL_EVENT(Loop, "loop", "begin of inner simulation loop each cycle") * performed in the simulation states in the `simulation.cpp` file. */ struct SimulationContext { - SimulationContext(sol::state_view&& l) : lua(l) {} + SimulationContext(sol::state_view l) : lua(std::move(l)) {} sol::state_view lua; @@ -209,9 +211,10 @@ struct SimulationContext { std::unique_ptr commander; // Configuration - cloe::Stack config; - std::string uuid{}; - bool report_progress{false}; + cloe::Stack config; ///< Input configuration. + std::string uuid{}; ///< UUID to use for simulation. + bool report_progress{false}; ///< Report simulation progress to console. + bool probe_simulation{false}; ///< Probe for information from enroll methods. // State SimulationSync sync; diff --git a/plugins/basic/src/basic.cpp b/plugins/basic/src/basic.cpp index d208218a9..b1fe41504 100644 --- a/plugins/basic/src/basic.cpp +++ b/plugins/basic/src/basic.cpp @@ -397,45 +397,36 @@ class BasicController : public Controller { } void enroll(Registrar& r) override { - auto& db = r.data_broker(); if (this->veh_) { - auto& vehicle = this->veh_->name(); - { - std::string name1 = fmt::format("vehicles.{}.{}.acc", vehicle, name()); - auto acc_signal = db.declare(name1); - acc_signal->set_getter( - [this]() -> const cloe::controller::basic::AccConfiguration& { - return this->acc_.config; - }); - acc_signal->set_setter( - [this](const cloe::controller::basic::AccConfiguration& value) { - this->acc_.config = value; - }); - } - { - std::string name1 = fmt::format("vehicles.{}.{}.aeb", vehicle, name()); - auto aeb_signal = db.declare(name1); - aeb_signal->set_getter( - [this]() -> const cloe::controller::basic::AebConfiguration& { - return this->aeb_.config; - }); - aeb_signal->set_setter( - [this](const cloe::controller::basic::AebConfiguration& value) { - this->aeb_.config = value; - }); - } - { - std::string name1 = fmt::format("vehicles.{}.{}.lka", vehicle, name()); - auto lka_signal = db.declare(name1); - lka_signal->set_getter( - [this]() -> const cloe::controller::basic::LkaConfiguration& { - return this->lka_.config; - }); - lka_signal->set_setter( - [this](const cloe::controller::basic::LkaConfiguration& value) { - this->lka_.config = value; - }); - } + auto acc_signal = r.declare_signal("acc"); + acc_signal->set_getter( + [this]() -> const cloe::controller::basic::AccConfiguration& { + return this->acc_.config; + }); + acc_signal->set_setter( + [this](const cloe::controller::basic::AccConfiguration& value) { + this->acc_.config = value; + }); + + auto aeb_signal = r.declare_signal("aeb"); + aeb_signal->set_getter( + [this]() -> const cloe::controller::basic::AebConfiguration& { + return this->aeb_.config; + }); + aeb_signal->set_setter( + [this](const cloe::controller::basic::AebConfiguration& value) { + this->aeb_.config = value; + }); + + auto lka_signal = r.declare_signal("lka"); + lka_signal->set_getter( + [this]() -> const cloe::controller::basic::LkaConfiguration& { + return this->lka_.config; + }); + lka_signal->set_setter( + [this](const cloe::controller::basic::LkaConfiguration& value) { + this->lka_.config = value; + }); } auto lua = r.register_lua_table(); diff --git a/plugins/speedometer/src/speedometer.cpp b/plugins/speedometer/src/speedometer.cpp index 1159fbb50..6dc7457f5 100644 --- a/plugins/speedometer/src/speedometer.cpp +++ b/plugins/speedometer/src/speedometer.cpp @@ -38,22 +38,19 @@ struct SpeedometerConf : public fable::Confable { class Speedometer : public cloe::Component { public: - Speedometer(const std::string& name, const SpeedometerConf&, std::shared_ptr ego) + Speedometer(const std::string& name, const SpeedometerConf& /*conf*/, + std::shared_ptr ego) : Component(name, "provides an event trigger to evaluate speed in km/h"), sensor_(ego) {} - virtual ~Speedometer() noexcept = default; + ~Speedometer() noexcept override = default; void enroll(cloe::Registrar& r) override { callback_kmph_ = r.register_event("kmph", "vehicle speed in km/h"); - auto& db = r.data_broker(); - { - std::string signal_name = fmt::format("components.{}.kmph", name()); - auto signal = db.declare(signal_name); - signal->set_getter( - [this]() -> double { return cloe::utility::EgoSensorCanon(sensor_).velocity_as_kmph(); }); - } + auto kmph_signal = r.declare_signal("kmph"); + kmph_signal->set_getter( + [this]() -> double { return cloe::utility::EgoSensorCanon(sensor_).velocity_as_kmph(); }); } cloe::Duration process(const cloe::Sync& sync) override { @@ -63,7 +60,7 @@ class Speedometer : public cloe::Component { } fable::Json active_state() const override { - return fable::Json{{"kmph", utility::EgoSensorCanon(sensor_).velocity_as_kmph()}}; + return fable::Json{{"kmph", cloe::utility::EgoSensorCanon(sensor_).velocity_as_kmph()}}; } private: @@ -75,7 +72,7 @@ class Speedometer : public cloe::Component { DEFINE_COMPONENT_FACTORY(SpeedometerFactory, SpeedometerConf, "speedometer", "provide an event trigger to evaluate speed in km/h") -DEFINE_COMPONENT_FACTORY_MAKE(SpeedometerFactory, Speedometer, EgoSensor) +DEFINE_COMPONENT_FACTORY_MAKE(SpeedometerFactory, Speedometer, cloe::EgoSensor) // Register factory as plugin entrypoint EXPORT_CLOE_PLUGIN(SpeedometerFactory) diff --git a/runtime/include/cloe/registrar.hpp b/runtime/include/cloe/registrar.hpp index a357d9f05..4aba20177 100644 --- a/runtime/include/cloe/registrar.hpp +++ b/runtime/include/cloe/registrar.hpp @@ -26,11 +26,11 @@ #include // for string #include // for move +#include // for DataBroker +#include // for Handler +#include // for ActionFactory, EventFactory, Callback, ... +#include // for Json #include -#include // for DataBroker -#include // for Handler -#include // for ActionFactory, EventFactory, Callback, ... -#include // for Json namespace cloe { @@ -159,19 +159,26 @@ class Registrar { virtual std::unique_ptr with_static_prefix(const std::string& prefix) const = 0; /** - * Return a new Registrar with the given trigger prefix. + * Return a new Registrar with the given trigger and data broker prefix. * * The returned object should remain valid even if the object creating it * is destroyed. */ virtual std::unique_ptr with_trigger_prefix(const std::string& prefix) const = 0; + [[nodiscard]] virtual std::string make_signal_name(std::string_view name) const = 0; + /** * Register an ActionFactory. */ virtual void register_action(std::unique_ptr&&) = 0; - virtual DataBroker& data_broker() const = 0; + template + SignalPtr declare_signal(std::string_view name) { + return data_broker().declare(make_signal_name(name)); + } + + [[nodiscard]] virtual DataBroker& data_broker() const = 0; /** * Construct and register an ActionFactory. diff --git a/tests/project.lua b/tests/project.lua index bdd01f352..ce930e833 100644 --- a/tests/project.lua +++ b/tests/project.lua @@ -29,7 +29,7 @@ local m = {} --- @return nil function m.init_report(...) local results = {} - local file = api.state.current_script_file + local file = api.get_script_file() if file then results["source"] = cloe.fs.realpath(file) end @@ -38,7 +38,7 @@ function m.init_report(...) results = luax.tbl_extend("force", results, tbl) end - api.state.report.metadata = results + api.get_report().metadata = results end --- Apply a stackfile, setting version to "4". diff --git a/tests/test_lua04_schedule_test.lua b/tests/test_lua04_schedule_test.lua index f7302de6f..d75339d1d 100644 --- a/tests/test_lua04_schedule_test.lua +++ b/tests/test_lua04_schedule_test.lua @@ -18,15 +18,17 @@ cloe.schedule({ end, }) -local signals = { "vehicles.default.basic.acc" } -cloe.require_signals(signals) -cloe.record_signals(signals) +local Sig = { + VehAcc = "vehicles.default.basic.acc" +} +cloe.require_signals_enum(Sig) +cloe.record_signals(Sig) cloe.record_signals( { ["acc_config.limit_acceleration"] = function() - return cloe.signal("vehicles.default.basic.acc").limit_acceleration + return cloe.signal(Sig.VehAcc).limit_acceleration end, ["acc_config.limit_deceleration"] = function() - return cloe.signal("vehicles.default.basic.acc").limit_deceleration + return cloe.signal(Sig.VehAcc).limit_deceleration end, }) diff --git a/tests/test_lua14_speedometer_signals.lua b/tests/test_lua14_speedometer_signals.lua new file mode 100644 index 000000000..e3348ab1d --- /dev/null +++ b/tests/test_lua14_speedometer_signals.lua @@ -0,0 +1,7 @@ +local cloe = require("cloe") + +cloe.load_stackfile("config_minimator_multi_agent_infinite.json") + +local Sig = { + Speedometer +} diff --git a/tests/test_lua15_acc_record.lua b/tests/test_lua15_acc_record.lua new file mode 100644 index 000000000..a7452f5b1 --- /dev/null +++ b/tests/test_lua15_acc_record.lua @@ -0,0 +1,42 @@ +local cloe = require("cloe") +local events, actions = cloe.events, cloe.actions + +cloe.load_stackfile("config_nop_smoketest.json") + +local Sig = { + VehAcc = "vehicles.default.basic.acc" +} +cloe.require_signals_enum(Sig) +cloe.record_signals(Sig) +cloe.record_signals( { + ["acc_config.limit_acceleration"] = function() + return cloe.signal(Sig.VehAcc).limit_acceleration + end, + ["acc_config.limit_deceleration"] = function() + return cloe.signal(Sig.VehAcc).limit_deceleration + end, +}) + +-- Run a simple test. +cloe.schedule_test({ + id = "20b741ee-ef82-4638-bd61-87a3fb4221d2", + on = events.start(), + terminate = false, + run = function(z, sync) + z:wait_duration("1s") + z:stop() + end, +}) + +-- Check recording. +cloe.schedule_test { + id = "a0065f68-2e1f-436c-8b17-fa19a630509c", + on = events.stop(), + run = function(z, sync) + -- Inspect the recording in the report: + local api = require("cloe-engine") + local report = api.get_report() + + z:assert(report.signals ~= nil, "report.signals should not be nil") + end +}