OpenTTD Source 20250521-master-g82876c25e0
test_script_admin.cpp
1/*
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/>.
6 */
7
10#include "../stdafx.h"
11
12#include "../3rdparty/catch2/catch.hpp"
13
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"
19
20#include "../3rdparty/fmt/format.h"
21#include "../3rdparty/nlohmann/json.hpp"
22
23#include <squirrel.h>
24
25#include "../safeguards.h"
26
38public:
39 GameInstance game{};
40 ScriptObject::ActiveInstance active{game};
41
42 Squirrel engine{"test"};
43 ScriptAllocatorScope scope{&engine};
44};
45
46extern bool ScriptAdminMakeJSON(nlohmann::json &json, HSQUIRRELVM vm, SQInteger index, int depth = 0);
47
52static std::optional<std::string> TestScriptAdminMakeJSON(std::string_view squirrel)
53{
54 auto vm = sq_open(1024);
55 /* sq_compile creates a closure with our snipper, which is a table.
56 * Add "return " to get the table on the stack. */
57 std::string buffer = fmt::format("return {}", squirrel);
58
59 /* Insert an (empty) class for testing. */
60 sq_pushroottable(vm);
61 sq_pushstring(vm, "DummyClass");
62 sq_newclass(vm, SQFalse);
63 sq_newslot(vm, -3, SQFalse);
64 sq_pop(vm, 1);
65
66 /* Compile the snippet. */
67 REQUIRE(sq_compilebuffer(vm, buffer, "test", SQTrue) == SQ_OK);
68 /* Execute the snippet, capturing the return value. */
69 sq_pushroottable(vm);
70 REQUIRE(sq_call(vm, 1, SQTrue, SQTrue) == SQ_OK);
71 /* Ensure the snippet pushed a table on the stack. */
72 REQUIRE(sq_gettype(vm, -1) == OT_TABLE);
73
74 /* Feed the snippet into the MakeJSON function. */
75 nlohmann::json json;
76 if (!ScriptAdminMakeJSON(json, vm, -1)) {
77 sq_close(vm);
78 return std::nullopt;
79 }
80
81 sq_close(vm);
82 return json.dump();
83}
84
102static std::optional<std::string> TestScriptEventAdminPort(const std::string &json)
103{
104 auto vm = sq_open(1024);
105
106 /* Run the conversion JSON -> Squirrel (this will now be on top of the stack). */
107 ScriptEventAdminPort(json).GetObject(vm);
108 if (sq_gettype(vm, -1) == OT_NULL) {
109 sq_close(vm);
110 return std::nullopt;
111 }
112 REQUIRE(sq_gettype(vm, -1) == OT_TABLE);
113
114 nlohmann::json squirrel_json;
115 REQUIRE(ScriptAdminMakeJSON(squirrel_json, vm, -1) == true);
116
117 sq_close(vm);
118 return squirrel_json.dump();
119}
120
121TEST_CASE("Squirrel -> JSON conversion")
122{
123 TestScriptController controller;
124
125 CHECK(TestScriptAdminMakeJSON(R"sq({ test = null })sq") == R"json({"test":null})json");
126 CHECK(TestScriptAdminMakeJSON(R"sq({ test = 1 })sq") == R"json({"test":1})json");
127 CHECK(TestScriptAdminMakeJSON(R"sq({ test = -1 })sq") == R"json({"test":-1})json");
128 CHECK(TestScriptAdminMakeJSON(R"sq({ test = true })sq") == R"json({"test":true})json");
129 CHECK(TestScriptAdminMakeJSON(R"sq({ test = "a" })sq") == R"json({"test":"a"})json");
130 CHECK(TestScriptAdminMakeJSON(R"sq({ test = [ ] })sq") == R"json({"test":[]})json");
131 CHECK(TestScriptAdminMakeJSON(R"sq({ test = [ 1 ] })sq") == R"json({"test":[1]})json");
132 CHECK(TestScriptAdminMakeJSON(R"sq({ test = [ 1, "a", true, { test = 1 }, [], null ] })sq") == R"json({"test":[1,"a",true,{"test":1},[],null]})json");
133 CHECK(TestScriptAdminMakeJSON(R"sq({ test = { } })sq") == R"json({"test":{}})json");
134 CHECK(TestScriptAdminMakeJSON(R"sq({ test = { test = 1 } })sq") == R"json({"test":{"test":1}})json");
135 CHECK(TestScriptAdminMakeJSON(R"sq({ test = { test = 1, test = 2 } })sq") == R"json({"test":{"test":2}})json");
136 CHECK(TestScriptAdminMakeJSON(R"sq({ test = { test = 1, test2 = [ 2 ] } })sq") == R"json({"test":{"test":1,"test2":[2]}})json");
137
138 /* Cases that should fail, as we cannot convert a class to JSON. */
139 CHECK(TestScriptAdminMakeJSON(R"sq({ test = DummyClass })sq") == std::nullopt);
140 CHECK(TestScriptAdminMakeJSON(R"sq({ test = [ 1, DummyClass ] })sq") == std::nullopt);
141 CHECK(TestScriptAdminMakeJSON(R"sq({ test = { test = 1, test2 = DummyClass } })sq") == std::nullopt);
142}
143
144TEST_CASE("JSON -> Squirrel conversion")
145{
146 TestScriptController controller;
147
148 CHECK(TestScriptEventAdminPort(R"json({ "test": null })json") == R"json({"test":null})json");
149 CHECK(TestScriptEventAdminPort(R"json({ "test": 1 })json") == R"json({"test":1})json");
150 CHECK(TestScriptEventAdminPort(R"json({ "test": -1 })json") == R"json({"test":-1})json");
151 CHECK(TestScriptEventAdminPort(R"json({ "test": true })json") == R"json({"test":true})json");
152 CHECK(TestScriptEventAdminPort(R"json({ "test": "a" })json") == R"json({"test":"a"})json");
153 CHECK(TestScriptEventAdminPort(R"json({ "test": [] })json") == R"json({"test":[]})json");
154 CHECK(TestScriptEventAdminPort(R"json({ "test": [ 1 ] })json") == R"json({"test":[1]})json");
155 CHECK(TestScriptEventAdminPort(R"json({ "test": [ 1, "a", true, { "test": 1 }, [], null ] })json") == R"json({"test":[1,"a",true,{"test":1},[],null]})json");
156 CHECK(TestScriptEventAdminPort(R"json({ "test": {} })json") == R"json({"test":{}})json");
157 CHECK(TestScriptEventAdminPort(R"json({ "test": { "test": 1 } })json") == R"json({"test":{"test":1}})json");
158 CHECK(TestScriptEventAdminPort(R"json({ "test": { "test": 2 } })json") == R"json({"test":{"test":2}})json");
159 CHECK(TestScriptEventAdminPort(R"json({ "test": { "test": 1, "test2": [ 2 ] } })json") == R"json({"test":{"test":1,"test2":[2]}})json");
160
161 /* Check if spaces are properly ignored. */
162 CHECK(TestScriptEventAdminPort(R"json({"test":1})json") == R"json({"test":1})json");
163 CHECK(TestScriptEventAdminPort(R"json({"test": 1})json") == R"json({"test":1})json");
164
165 /* Valid JSON but invalid Squirrel (read: floats). */
166 CHECK(TestScriptEventAdminPort(R"json({ "test": 1.1 })json") == std::nullopt);
167 CHECK(TestScriptEventAdminPort(R"json({ "test": [ 1, 3, 1.1 ] })json") == std::nullopt);
168
169 /* Root element has to be an object. */
170 CHECK(TestScriptEventAdminPort(R"json( 1 )json") == std::nullopt);
171 CHECK(TestScriptEventAdminPort(R"json( "a" )json") == std::nullopt);
172 CHECK(TestScriptEventAdminPort(R"json( [ 1 ] )json") == std::nullopt);
173 CHECK(TestScriptEventAdminPort(R"json( null )json") == std::nullopt);
174 CHECK(TestScriptEventAdminPort(R"json( true )json") == std::nullopt);
175
176 /* Cases that should fail, as it is invalid JSON. */
177 CHECK(TestScriptEventAdminPort(R"json({"test":test})json") == std::nullopt);
178 CHECK(TestScriptEventAdminPort(R"json({ "test": 1 )json") == std::nullopt); // Missing closing }
179 CHECK(TestScriptEventAdminPort(R"json( "test": 1})json") == std::nullopt); // Missing opening {
180 CHECK(TestScriptEventAdminPort(R"json({ "test" = 1})json") == std::nullopt);
181 CHECK(TestScriptEventAdminPort(R"json({ "test": [ 1 })json") == std::nullopt); // Missing closing ]
182 CHECK(TestScriptEventAdminPort(R"json({ "test": 1 ] })json") == std::nullopt); // Missing opening [
183}
Runtime information about a game script like a pointer to the squirrel vm and the current state.
A controller to start enough so we can use Squirrel for testing.