From 00f13282a9075f93f968e0042012c75c81a2d6b0 Mon Sep 17 00:00:00 2001 From: Patric Stout Date: Fri, 8 Sep 2023 19:03:10 +0200 Subject: [PATCH] Codechange: keep how we convert string <-> JSON private (#11269) --- src/script/api/script_admin.cpp | 21 ++- src/script/api/script_admin.hpp | 12 -- src/script/api/script_event_types.cpp | 64 ++++---- src/script/api/script_event_types.hpp | 9 -- src/tests/test_script_admin.cpp | 212 +++++++++++++------------- 5 files changed, 156 insertions(+), 162 deletions(-) diff --git a/src/script/api/script_admin.cpp b/src/script/api/script_admin.cpp index 2c30434792..97a25bbf47 100644 --- a/src/script/api/script_admin.cpp +++ b/src/script/api/script_admin.cpp @@ -14,9 +14,22 @@ #include "../script_instance.hpp" #include "../../string_func.h" +#include + #include "../../safeguards.h" -/* static */ bool ScriptAdmin::MakeJSON(nlohmann::json &json, HSQUIRRELVM vm, SQInteger index, int depth) +/** + * Convert a Squirrel structure into a JSON object. + * + * This function is not "static", so it can be tested in unittests. + * + * @param json The resulting JSON object. + * @param vm The VM to operate on. + * @param index The index we are currently working for. + * @param depth The current depth in the squirrel struct. + * @return True iff the conversion was successful. + */ +bool ScriptAdminMakeJSON(nlohmann::json &json, HSQUIRRELVM vm, SQInteger index, int depth = 0) { if (depth == SQUIRREL_MAX_DEPTH) { ScriptLog::Error("Send parameters can only be nested to 25 deep. No data sent."); // SQUIRREL_MAX_DEPTH = 25 @@ -47,7 +60,7 @@ while (SQ_SUCCEEDED(sq_next(vm, index - 1))) { nlohmann::json tmp; - bool res = MakeJSON(tmp, vm, -1, depth + 1); + bool res = ScriptAdminMakeJSON(tmp, vm, -1, depth + 1); sq_pop(vm, 2); if (!res) { sq_pop(vm, 1); @@ -72,7 +85,7 @@ std::string key = std::string(buf); nlohmann::json value; - bool res = MakeJSON(value, vm, -1, depth + 1); + bool res = ScriptAdminMakeJSON(value, vm, -1, depth + 1); sq_pop(vm, 2); if (!res) { sq_pop(vm, 1); @@ -113,7 +126,7 @@ } nlohmann::json json; - if (!ScriptAdmin::MakeJSON(json, vm, -1)) { + if (!ScriptAdminMakeJSON(json, vm, -1)) { sq_pushinteger(vm, 0); return 1; } diff --git a/src/script/api/script_admin.hpp b/src/script/api/script_admin.hpp index 5dea0b0510..877506e98d 100644 --- a/src/script/api/script_admin.hpp +++ b/src/script/api/script_admin.hpp @@ -11,7 +11,6 @@ #define SCRIPT_ADMIN_HPP #include "script_object.hpp" -#include /** * Class that handles communication with the AdminPort. @@ -36,17 +35,6 @@ public: */ static bool Send(void *table); #endif /* DOXYGEN_API */ - -protected: - /** - * Convert a Squirrel structure into a JSON object. - * @param json The resulting JSON object. - * @param vm The VM to operate on. - * @param index The index we are currently working for. - * @param depth The current depth in the squirrel struct. - * @return True iff the conversion was successful. - */ - static bool MakeJSON(nlohmann::json &data, HSQUIRRELVM vm, SQInteger index, int depth = 0); }; #endif /* SCRIPT_ADMIN_HPP */ diff --git a/src/script/api/script_event_types.cpp b/src/script/api/script_event_types.cpp index 7b7ebcb9b7..7c4409b6bd 100644 --- a/src/script/api/script_event_types.cpp +++ b/src/script/api/script_event_types.cpp @@ -20,6 +20,8 @@ #include "../../engine_cmd.h" #include "table/strings.h" +#include + #include "../../safeguards.h" bool ScriptEventEnginePreview::IsEngineValid() const @@ -127,33 +129,12 @@ ScriptEventAdminPort::ScriptEventAdminPort(const std::string &json) : json(json) { } - -SQInteger ScriptEventAdminPort::GetObject(HSQUIRRELVM vm) -{ - auto json = nlohmann::json::parse(this->json, nullptr, false); - - if (!json.is_object()) { - ScriptLog::Error("The root element in the JSON data from AdminPort has to be an object."); - - sq_pushnull(vm); - return 1; - } - - auto top = sq_gettop(vm); - if (!this->ReadValue(vm, json)) { - /* Rewind the stack, removing anything that might be left on top. */ - sq_settop(vm, top); - - ScriptLog::Error("Received invalid JSON data from AdminPort."); - - sq_pushnull(vm); - return 1; - } - - return 1; -} - -bool ScriptEventAdminPort::ReadValue(HSQUIRRELVM vm, nlohmann::json &json) +/** + * Convert a JSON part fo Squirrel. + * @param vm The VM used. + * @param json The JSON part to convert to Squirrel. + */ +static bool ScriptEventAdminPortReadValue(HSQUIRRELVM vm, nlohmann::json &json) { switch (json.type()) { case nlohmann::json::value_t::null: @@ -181,7 +162,7 @@ bool ScriptEventAdminPort::ReadValue(HSQUIRRELVM vm, nlohmann::json &json) for (auto &[key, value] : json.items()) { sq_pushstring(vm, key.data(), key.size()); - if (!this->ReadValue(vm, value)) { + if (!ScriptEventAdminPortReadValue(vm, value)) { return false; } @@ -193,7 +174,7 @@ bool ScriptEventAdminPort::ReadValue(HSQUIRRELVM vm, nlohmann::json &json) sq_newarray(vm, 0); for (auto &value : json) { - if (!this->ReadValue(vm, value)) { + if (!ScriptEventAdminPortReadValue(vm, value)) { return false; } @@ -209,3 +190,28 @@ bool ScriptEventAdminPort::ReadValue(HSQUIRRELVM vm, nlohmann::json &json) return true; } + +SQInteger ScriptEventAdminPort::GetObject(HSQUIRRELVM vm) +{ + auto json = nlohmann::json::parse(this->json, nullptr, false); + + if (!json.is_object()) { + ScriptLog::Error("The root element in the JSON data from AdminPort has to be an object."); + + sq_pushnull(vm); + return 1; + } + + auto top = sq_gettop(vm); + if (!ScriptEventAdminPortReadValue(vm, json)) { + /* Rewind the stack, removing anything that might be left on top. */ + sq_settop(vm, top); + + ScriptLog::Error("Received invalid JSON data from AdminPort."); + + sq_pushnull(vm); + return 1; + } + + return 1; +} diff --git a/src/script/api/script_event_types.hpp b/src/script/api/script_event_types.hpp index 8ff883ac28..4496b3a310 100644 --- a/src/script/api/script_event_types.hpp +++ b/src/script/api/script_event_types.hpp @@ -14,8 +14,6 @@ #include "script_goal.hpp" #include "script_window.hpp" -#include - /** * Event Vehicle Crash, indicating a vehicle of yours is crashed. * It contains the crash site, the crashed vehicle and the reason for the crash. @@ -912,13 +910,6 @@ public: private: std::string json; ///< The JSON string. - - /** - * Convert a JSON part fo Squirrel. - * @param vm The VM used. - * @param json The JSON part to convert to Squirrel. - */ - bool ReadValue(HSQUIRRELVM vm, nlohmann::json &json); }; /** diff --git a/src/tests/test_script_admin.cpp b/src/tests/test_script_admin.cpp index 95e00a3c47..120dfc1d7d 100644 --- a/src/tests/test_script_admin.cpp +++ b/src/tests/test_script_admin.cpp @@ -20,6 +20,7 @@ #include "../3rdparty/fmt/format.h" #include +#include /** * A controller to start enough so we can use Squirrel for testing. @@ -40,146 +41,141 @@ public: ScriptAllocatorScope scope{&engine}; }; +extern bool ScriptAdminMakeJSON(nlohmann::json &json, HSQUIRRELVM vm, SQInteger index, int depth = 0); + /** - * Small wrapper around ScriptAdmin. - * - * MakeJSON is protected; so for tests, we make a public function with - * which we call into the protected one. This prevents accidental use - * by the rest of the code, while still being able to test it. + * Small wrapper around ScriptAdmin's MakeJSON that prepares the Squirrel + * engine if it was called from actual scripting.. */ -class TestScriptAdmin : public ScriptAdmin { -public: - static std::optional MakeJSON(std::string_view squirrel) - { - auto vm = sq_open(1024); - /* sq_compile creates a closure with our snipper, which is a table. - * Add "return " to get the table on the stack. */ - std::string buffer = fmt::format("return {}", squirrel); +static std::optional TestScriptAdminMakeJSON(std::string_view squirrel) +{ + auto vm = sq_open(1024); + /* sq_compile creates a closure with our snipper, which is a table. + * Add "return " to get the table on the stack. */ + std::string buffer = fmt::format("return {}", squirrel); - /* Insert an (empty) class for testing. */ - sq_pushroottable(vm); - sq_pushstring(vm, "DummyClass", -1); - sq_newclass(vm, SQFalse); - sq_newslot(vm, -3, SQFalse); - sq_pop(vm, 1); + /* Insert an (empty) class for testing. */ + sq_pushroottable(vm); + sq_pushstring(vm, "DummyClass", -1); + sq_newclass(vm, SQFalse); + sq_newslot(vm, -3, SQFalse); + sq_pop(vm, 1); - /* Compile the snippet. */ - REQUIRE(sq_compilebuffer(vm, buffer.c_str(), buffer.size(), "test", SQTrue) == SQ_OK); - /* Execute the snippet, capturing the return value. */ - sq_pushroottable(vm); - REQUIRE(sq_call(vm, 1, SQTrue, SQTrue) == SQ_OK); - /* Ensure the snippet pushed a table on the stack. */ - REQUIRE(sq_gettype(vm, -1) == OT_TABLE); - - /* Feed the snippet into the MakeJSON function. */ - nlohmann::json json; - if (!ScriptAdmin::MakeJSON(json, vm, -1)) { - sq_close(vm); - return std::nullopt; - } + /* Compile the snippet. */ + REQUIRE(sq_compilebuffer(vm, buffer.c_str(), buffer.size(), "test", SQTrue) == SQ_OK); + /* Execute the snippet, capturing the return value. */ + sq_pushroottable(vm); + REQUIRE(sq_call(vm, 1, SQTrue, SQTrue) == SQ_OK); + /* Ensure the snippet pushed a table on the stack. */ + REQUIRE(sq_gettype(vm, -1) == OT_TABLE); + /* Feed the snippet into the MakeJSON function. */ + nlohmann::json json; + if (!ScriptAdminMakeJSON(json, vm, -1)) { sq_close(vm); - return json.dump(); + return std::nullopt; } - /** - * Validate ScriptEventAdminPort can convert JSON to Squirrel. - * - * This function is not actually part of ScriptAdmin, but we will use MakeJSON, - * and as such need to be inside this class. - * - * The easiest way to do validate, is to first use ScriptEventAdminPort (the function - * we are testing) to convert the JSON to a Squirrel table. Then to use MakeJSON - * to convert it back to JSON. - * - * Sadly, Squirrel has no way to easily compare if two tables are identical, so we - * use the JSON -> Squirrel -> JSON method to validate the conversion. But mind you, - * a failure in the final JSON might also mean a bug in MakeJSON. - * - * @param json The JSON-string to convert to Squirrel - * @return The Squirrel table converted to a JSON-string. - */ - static std::optional TestScriptEventAdminPort(const std::string &json) - { - auto vm = sq_open(1024); + sq_close(vm); + return json.dump(); +} - /* Run the conversion JSON -> Squirrel (this will now be on top of the stack). */ - ScriptEventAdminPort(json).GetObject(vm); - if (sq_gettype(vm, -1) == OT_NULL) { - sq_close(vm); - return std::nullopt; - } - REQUIRE(sq_gettype(vm, -1) == OT_TABLE); - - nlohmann::json squirrel_json; - REQUIRE(ScriptAdmin::MakeJSON(squirrel_json, vm, -1) == true); +/** + * Validate ScriptEventAdminPort can convert JSON to Squirrel. + * + * This function is not actually part of ScriptAdmin, but we will use MakeJSON, + * and as such need to be inside this class. + * + * The easiest way to do validate, is to first use ScriptEventAdminPort (the function + * we are testing) to convert the JSON to a Squirrel table. Then to use MakeJSON + * to convert it back to JSON. + * + * Sadly, Squirrel has no way to easily compare if two tables are identical, so we + * use the JSON -> Squirrel -> JSON method to validate the conversion. But mind you, + * a failure in the final JSON might also mean a bug in MakeJSON. + * + * @param json The JSON-string to convert to Squirrel + * @return The Squirrel table converted to a JSON-string. + */ +static std::optional TestScriptEventAdminPort(const std::string &json) +{ + auto vm = sq_open(1024); + /* Run the conversion JSON -> Squirrel (this will now be on top of the stack). */ + ScriptEventAdminPort(json).GetObject(vm); + if (sq_gettype(vm, -1) == OT_NULL) { sq_close(vm); - return squirrel_json.dump(); + return std::nullopt; } + REQUIRE(sq_gettype(vm, -1) == OT_TABLE); -}; + nlohmann::json squirrel_json; + REQUIRE(ScriptAdminMakeJSON(squirrel_json, vm, -1) == true); + + sq_close(vm); + return squirrel_json.dump(); +} TEST_CASE("Squirrel -> JSON conversion") { TestScriptController controller; - CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = null })sq") == R"json({"test":null})json"); - CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = 1 })sq") == R"json({"test":1})json"); - CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = -1 })sq") == R"json({"test":-1})json"); - CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = true })sq") == R"json({"test":true})json"); - CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = "a" })sq") == R"json({"test":"a"})json"); - CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = [ ] })sq") == R"json({"test":[]})json"); - CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = [ 1 ] })sq") == R"json({"test":[1]})json"); - CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = [ 1, "a", true, { test = 1 }, [], null ] })sq") == R"json({"test":[1,"a",true,{"test":1},[],null]})json"); - CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = { } })sq") == R"json({"test":{}})json"); - CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = { test = 1 } })sq") == R"json({"test":{"test":1}})json"); - CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = { test = 1, test = 2 } })sq") == R"json({"test":{"test":2}})json"); - CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = { test = 1, test2 = [ 2 ] } })sq") == R"json({"test":{"test":1,"test2":[2]}})json"); + CHECK(TestScriptAdminMakeJSON(R"sq({ test = null })sq") == R"json({"test":null})json"); + CHECK(TestScriptAdminMakeJSON(R"sq({ test = 1 })sq") == R"json({"test":1})json"); + CHECK(TestScriptAdminMakeJSON(R"sq({ test = -1 })sq") == R"json({"test":-1})json"); + CHECK(TestScriptAdminMakeJSON(R"sq({ test = true })sq") == R"json({"test":true})json"); + CHECK(TestScriptAdminMakeJSON(R"sq({ test = "a" })sq") == R"json({"test":"a"})json"); + CHECK(TestScriptAdminMakeJSON(R"sq({ test = [ ] })sq") == R"json({"test":[]})json"); + CHECK(TestScriptAdminMakeJSON(R"sq({ test = [ 1 ] })sq") == R"json({"test":[1]})json"); + CHECK(TestScriptAdminMakeJSON(R"sq({ test = [ 1, "a", true, { test = 1 }, [], null ] })sq") == R"json({"test":[1,"a",true,{"test":1},[],null]})json"); + CHECK(TestScriptAdminMakeJSON(R"sq({ test = { } })sq") == R"json({"test":{}})json"); + CHECK(TestScriptAdminMakeJSON(R"sq({ test = { test = 1 } })sq") == R"json({"test":{"test":1}})json"); + CHECK(TestScriptAdminMakeJSON(R"sq({ test = { test = 1, test = 2 } })sq") == R"json({"test":{"test":2}})json"); + CHECK(TestScriptAdminMakeJSON(R"sq({ test = { test = 1, test2 = [ 2 ] } })sq") == R"json({"test":{"test":1,"test2":[2]}})json"); /* Cases that should fail, as we cannot convert a class to JSON. */ - CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = DummyClass })sq") == std::nullopt); - CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = [ 1, DummyClass ] })sq") == std::nullopt); - CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = { test = 1, test2 = DummyClass } })sq") == std::nullopt); + CHECK(TestScriptAdminMakeJSON(R"sq({ test = DummyClass })sq") == std::nullopt); + CHECK(TestScriptAdminMakeJSON(R"sq({ test = [ 1, DummyClass ] })sq") == std::nullopt); + CHECK(TestScriptAdminMakeJSON(R"sq({ test = { test = 1, test2 = DummyClass } })sq") == std::nullopt); } TEST_CASE("JSON -> Squirrel conversion") { TestScriptController controller; - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": null })json") == R"json({"test":null})json"); - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": 1 })json") == R"json({"test":1})json"); - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": -1 })json") == R"json({"test":-1})json"); - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": true })json") == R"json({"test":true})json"); - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": "a" })json") == R"json({"test":"a"})json"); - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": [] })json") == R"json({"test":[]})json"); - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": [ 1 ] })json") == R"json({"test":[1]})json"); - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": [ 1, "a", true, { "test": 1 }, [], null ] })json") == R"json({"test":[1,"a",true,{"test":1},[],null]})json"); - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": {} })json") == R"json({"test":{}})json"); - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": { "test": 1 } })json") == R"json({"test":{"test":1}})json"); - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": { "test": 2 } })json") == R"json({"test":{"test":2}})json"); - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": { "test": 1, "test2": [ 2 ] } })json") == R"json({"test":{"test":1,"test2":[2]}})json"); + CHECK(TestScriptEventAdminPort(R"json({ "test": null })json") == R"json({"test":null})json"); + CHECK(TestScriptEventAdminPort(R"json({ "test": 1 })json") == R"json({"test":1})json"); + CHECK(TestScriptEventAdminPort(R"json({ "test": -1 })json") == R"json({"test":-1})json"); + CHECK(TestScriptEventAdminPort(R"json({ "test": true })json") == R"json({"test":true})json"); + CHECK(TestScriptEventAdminPort(R"json({ "test": "a" })json") == R"json({"test":"a"})json"); + CHECK(TestScriptEventAdminPort(R"json({ "test": [] })json") == R"json({"test":[]})json"); + CHECK(TestScriptEventAdminPort(R"json({ "test": [ 1 ] })json") == R"json({"test":[1]})json"); + CHECK(TestScriptEventAdminPort(R"json({ "test": [ 1, "a", true, { "test": 1 }, [], null ] })json") == R"json({"test":[1,"a",true,{"test":1},[],null]})json"); + CHECK(TestScriptEventAdminPort(R"json({ "test": {} })json") == R"json({"test":{}})json"); + CHECK(TestScriptEventAdminPort(R"json({ "test": { "test": 1 } })json") == R"json({"test":{"test":1}})json"); + CHECK(TestScriptEventAdminPort(R"json({ "test": { "test": 2 } })json") == R"json({"test":{"test":2}})json"); + CHECK(TestScriptEventAdminPort(R"json({ "test": { "test": 1, "test2": [ 2 ] } })json") == R"json({"test":{"test":1,"test2":[2]}})json"); /* Check if spaces are properly ignored. */ - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({"test":1})json") == R"json({"test":1})json"); - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({"test": 1})json") == R"json({"test":1})json"); + CHECK(TestScriptEventAdminPort(R"json({"test":1})json") == R"json({"test":1})json"); + CHECK(TestScriptEventAdminPort(R"json({"test": 1})json") == R"json({"test":1})json"); /* Valid JSON but invalid Squirrel (read: floats). */ - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": 1.1 })json") == std::nullopt); - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": [ 1, 3, 1.1 ] })json") == std::nullopt); + CHECK(TestScriptEventAdminPort(R"json({ "test": 1.1 })json") == std::nullopt); + CHECK(TestScriptEventAdminPort(R"json({ "test": [ 1, 3, 1.1 ] })json") == std::nullopt); /* Root element has to be an object. */ - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json( 1 )json") == std::nullopt); - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json( "a" )json") == std::nullopt); - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json( [ 1 ] )json") == std::nullopt); - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json( null )json") == std::nullopt); - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json( true )json") == std::nullopt); + CHECK(TestScriptEventAdminPort(R"json( 1 )json") == std::nullopt); + CHECK(TestScriptEventAdminPort(R"json( "a" )json") == std::nullopt); + CHECK(TestScriptEventAdminPort(R"json( [ 1 ] )json") == std::nullopt); + CHECK(TestScriptEventAdminPort(R"json( null )json") == std::nullopt); + CHECK(TestScriptEventAdminPort(R"json( true )json") == std::nullopt); /* Cases that should fail, as it is invalid JSON. */ - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({"test":test})json") == std::nullopt); - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": 1 )json") == std::nullopt); // Missing closing } - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json( "test": 1})json") == std::nullopt); // Missing opening { - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test" = 1})json") == std::nullopt); - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": [ 1 })json") == std::nullopt); // Missing closing ] - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": 1 ] })json") == std::nullopt); // Missing opening [ + CHECK(TestScriptEventAdminPort(R"json({"test":test})json") == std::nullopt); + CHECK(TestScriptEventAdminPort(R"json({ "test": 1 )json") == std::nullopt); // Missing closing } + CHECK(TestScriptEventAdminPort(R"json( "test": 1})json") == std::nullopt); // Missing opening { + CHECK(TestScriptEventAdminPort(R"json({ "test" = 1})json") == std::nullopt); + CHECK(TestScriptEventAdminPort(R"json({ "test": [ 1 })json") == std::nullopt); // Missing closing ] + CHECK(TestScriptEventAdminPort(R"json({ "test": 1 ] })json") == std::nullopt); // Missing opening [ }