2 * This file is part of OpenTTD.
3 * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
4 * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
5 * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
8 /** @file script_admin_json.cpp Tests for the Squirrel -> JSON conversion in ScriptAdmin. */
10 #include "../stdafx.h"
12 #include "../3rdparty/catch2/catch.hpp"
14 #include "../game/game_instance.hpp"
15 #include "../script/api/script_admin.hpp"
16 #include "../script/api/script_event_types.hpp"
17 #include "../script/script_instance.hpp"
18 #include "../script/squirrel.hpp"
20 #include "../3rdparty/fmt/format.h"
21 #include "../3rdparty/nlohmann/json.hpp"
26 * A controller to start enough so we can use Squirrel for testing.
28 * To run Squirrel, we need an Allocator, so malloc/free works.
29 * For functions that log, we need an ActiveInstance, so the logger knows where
30 * to send the logs to.
32 * By instantiating this class, both are set correctly. After that you can
33 * use Squirrel without issues.
35 class TestScriptController
{
38 ScriptObject::ActiveInstance active
{&game
};
40 Squirrel engine
{"test"};
41 ScriptAllocatorScope scope
{&engine
};
44 extern bool ScriptAdminMakeJSON(nlohmann::json
&json
, HSQUIRRELVM vm
, SQInteger index
, int depth
= 0);
47 * Small wrapper around ScriptAdmin's MakeJSON that prepares the Squirrel
48 * engine if it was called from actual scripting..
50 static std::optional
<std::string
> TestScriptAdminMakeJSON(std::string_view squirrel
)
52 auto vm
= sq_open(1024);
53 /* sq_compile creates a closure with our snipper, which is a table.
54 * Add "return " to get the table on the stack. */
55 std::string buffer
= fmt::format("return {}", squirrel
);
57 /* Insert an (empty) class for testing. */
59 sq_pushstring(vm
, "DummyClass", -1);
60 sq_newclass(vm
, SQFalse
);
61 sq_newslot(vm
, -3, SQFalse
);
64 /* Compile the snippet. */
65 REQUIRE(sq_compilebuffer(vm
, buffer
.c_str(), buffer
.size(), "test", SQTrue
) == SQ_OK
);
66 /* Execute the snippet, capturing the return value. */
68 REQUIRE(sq_call(vm
, 1, SQTrue
, SQTrue
) == SQ_OK
);
69 /* Ensure the snippet pushed a table on the stack. */
70 REQUIRE(sq_gettype(vm
, -1) == OT_TABLE
);
72 /* Feed the snippet into the MakeJSON function. */
74 if (!ScriptAdminMakeJSON(json
, vm
, -1)) {
84 * Validate ScriptEventAdminPort can convert JSON to Squirrel.
86 * This function is not actually part of ScriptAdmin, but we will use MakeJSON,
87 * and as such need to be inside this class.
89 * The easiest way to do validate, is to first use ScriptEventAdminPort (the function
90 * we are testing) to convert the JSON to a Squirrel table. Then to use MakeJSON
91 * to convert it back to JSON.
93 * Sadly, Squirrel has no way to easily compare if two tables are identical, so we
94 * use the JSON -> Squirrel -> JSON method to validate the conversion. But mind you,
95 * a failure in the final JSON might also mean a bug in MakeJSON.
97 * @param json The JSON-string to convert to Squirrel
98 * @return The Squirrel table converted to a JSON-string.
100 static std::optional
<std::string
> TestScriptEventAdminPort(const std::string
&json
)
102 auto vm
= sq_open(1024);
104 /* Run the conversion JSON -> Squirrel (this will now be on top of the stack). */
105 ScriptEventAdminPort(json
).GetObject(vm
);
106 if (sq_gettype(vm
, -1) == OT_NULL
) {
110 REQUIRE(sq_gettype(vm
, -1) == OT_TABLE
);
112 nlohmann::json squirrel_json
;
113 REQUIRE(ScriptAdminMakeJSON(squirrel_json
, vm
, -1) == true);
116 return squirrel_json
.dump();
119 TEST_CASE("Squirrel -> JSON conversion")
121 TestScriptController controller
;
123 CHECK(TestScriptAdminMakeJSON(R
"sq({ test = null })sq") == R
"json({"test
":null})json");
124 CHECK(TestScriptAdminMakeJSON(R
"sq({ test = 1 })sq") == R
"json({"test
":1})json");
125 CHECK(TestScriptAdminMakeJSON(R
"sq({ test = -1 })sq") == R
"json({"test
":-1})json");
126 CHECK(TestScriptAdminMakeJSON(R
"sq({ test = true })sq") == R
"json({"test
":true})json");
127 CHECK(TestScriptAdminMakeJSON(R
"sq({ test = "a
" })sq") == R
"json({"test
":"a
"})json");
128 CHECK(TestScriptAdminMakeJSON(R
"sq({ test = [ ] })sq") == R
"json({"test
":[]})json");
129 CHECK(TestScriptAdminMakeJSON(R
"sq({ test = [ 1 ] })sq") == R
"json({"test
":[1]})json");
130 CHECK(TestScriptAdminMakeJSON(R
"sq({ test = [ 1, "a
", true, { test = 1 }, [], null ] })sq") == R
"json({"test
":[1,"a
",true,{"test
":1},[],null]})json");
131 CHECK(TestScriptAdminMakeJSON(R
"sq({ test = { } })sq") == R
"json({"test
":{}})json");
132 CHECK(TestScriptAdminMakeJSON(R
"sq({ test = { test = 1 } })sq") == R
"json({"test
":{"test
":1}})json");
133 CHECK(TestScriptAdminMakeJSON(R
"sq({ test = { test = 1, test = 2 } })sq") == R
"json({"test
":{"test
":2}})json");
134 CHECK(TestScriptAdminMakeJSON(R
"sq({ test = { test = 1, test2 = [ 2 ] } })sq") == R
"json({"test
":{"test
":1,"test2
":[2]}})json");
136 /* Cases that should fail, as we cannot convert a class to JSON. */
137 CHECK(TestScriptAdminMakeJSON(R
"sq({ test = DummyClass })sq") == std::nullopt
);
138 CHECK(TestScriptAdminMakeJSON(R
"sq({ test = [ 1, DummyClass ] })sq") == std::nullopt
);
139 CHECK(TestScriptAdminMakeJSON(R
"sq({ test = { test = 1, test2 = DummyClass } })sq") == std::nullopt
);
142 TEST_CASE("JSON -> Squirrel conversion")
144 TestScriptController controller
;
146 CHECK(TestScriptEventAdminPort(R
"json({ "test
": null })json") == R
"json({"test
":null})json");
147 CHECK(TestScriptEventAdminPort(R
"json({ "test
": 1 })json") == R
"json({"test
":1})json");
148 CHECK(TestScriptEventAdminPort(R
"json({ "test
": -1 })json") == R
"json({"test
":-1})json");
149 CHECK(TestScriptEventAdminPort(R
"json({ "test
": true })json") == R
"json({"test
":true})json");
150 CHECK(TestScriptEventAdminPort(R
"json({ "test
": "a
" })json") == R
"json({"test
":"a
"})json");
151 CHECK(TestScriptEventAdminPort(R
"json({ "test
": [] })json") == R
"json({"test
":[]})json");
152 CHECK(TestScriptEventAdminPort(R
"json({ "test
": [ 1 ] })json") == R
"json({"test
":[1]})json");
153 CHECK(TestScriptEventAdminPort(R
"json({ "test
": [ 1, "a
", true, { "test
": 1 }, [], null ] })json") == R
"json({"test
":[1,"a
",true,{"test
":1},[],null]})json");
154 CHECK(TestScriptEventAdminPort(R
"json({ "test
": {} })json") == R
"json({"test
":{}})json");
155 CHECK(TestScriptEventAdminPort(R
"json({ "test
": { "test
": 1 } })json") == R
"json({"test
":{"test
":1}})json");
156 CHECK(TestScriptEventAdminPort(R
"json({ "test
": { "test
": 2 } })json") == R
"json({"test
":{"test
":2}})json");
157 CHECK(TestScriptEventAdminPort(R
"json({ "test
": { "test
": 1, "test2
": [ 2 ] } })json") == R
"json({"test
":{"test
":1,"test2
":[2]}})json");
159 /* Check if spaces are properly ignored. */
160 CHECK(TestScriptEventAdminPort(R
"json({"test
":1})json") == R
"json({"test
":1})json");
161 CHECK(TestScriptEventAdminPort(R
"json({"test
": 1})json") == R
"json({"test
":1})json");
163 /* Valid JSON but invalid Squirrel (read: floats). */
164 CHECK(TestScriptEventAdminPort(R
"json({ "test
": 1.1 })json") == std::nullopt
);
165 CHECK(TestScriptEventAdminPort(R
"json({ "test
": [ 1, 3, 1.1 ] })json") == std::nullopt
);
167 /* Root element has to be an object. */
168 CHECK(TestScriptEventAdminPort(R
"json( 1 )json") == std::nullopt
);
169 CHECK(TestScriptEventAdminPort(R
"json( "a
" )json") == std::nullopt
);
170 CHECK(TestScriptEventAdminPort(R
"json( [ 1 ] )json") == std::nullopt
);
171 CHECK(TestScriptEventAdminPort(R
"json( null )json") == std::nullopt
);
172 CHECK(TestScriptEventAdminPort(R
"json( true )json") == std::nullopt
);
174 /* Cases that should fail, as it is invalid JSON. */
175 CHECK(TestScriptEventAdminPort(R
"json({"test
":test})json") == std::nullopt
);
176 CHECK(TestScriptEventAdminPort(R
"json({ "test
": 1 )json") == std::nullopt
); // Missing closing }
177 CHECK(TestScriptEventAdminPort(R
"json( "test
": 1})json") == std::nullopt
); // Missing opening {
178 CHECK(TestScriptEventAdminPort(R
"json({ "test
" = 1})json") == std::nullopt
);
179 CHECK(TestScriptEventAdminPort(R
"json({ "test
": [ 1 })json") == std::nullopt
); // Missing closing ]
180 CHECK(TestScriptEventAdminPort(R
"json({ "test
": 1 ] })json") == std::nullopt
); // Missing opening [