OpenTTD Source  14.0-beta1
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 
36 public:
37  GameInstance game{};
38  ScriptObject::ActiveInstance active{&game};
39 
40  Squirrel engine{"test"};
41  ScriptAllocatorScope scope{&engine};
42 };
43 
44 extern bool ScriptAdminMakeJSON(nlohmann::json &json, HSQUIRRELVM vm, SQInteger index, int depth = 0);
45 
50 static std::optional<std::string> TestScriptAdminMakeJSON(std::string_view squirrel)
51 {
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);
56 
57  /* Insert an (empty) class for testing. */
58  sq_pushroottable(vm);
59  sq_pushstring(vm, "DummyClass", -1);
60  sq_newclass(vm, SQFalse);
61  sq_newslot(vm, -3, SQFalse);
62  sq_pop(vm, 1);
63 
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. */
67  sq_pushroottable(vm);
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);
71 
72  /* Feed the snippet into the MakeJSON function. */
73  nlohmann::json json;
74  if (!ScriptAdminMakeJSON(json, vm, -1)) {
75  sq_close(vm);
76  return std::nullopt;
77  }
78 
79  sq_close(vm);
80  return json.dump();
81 }
82 
100 static std::optional<std::string> TestScriptEventAdminPort(const std::string &json)
101 {
102  auto vm = sq_open(1024);
103 
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) {
107  sq_close(vm);
108  return std::nullopt;
109  }
110  REQUIRE(sq_gettype(vm, -1) == OT_TABLE);
111 
112  nlohmann::json squirrel_json;
113  REQUIRE(ScriptAdminMakeJSON(squirrel_json, vm, -1) == true);
114 
115  sq_close(vm);
116  return squirrel_json.dump();
117 }
118 
119 TEST_CASE("Squirrel -> JSON conversion")
120 {
121  TestScriptController controller;
122 
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");
135 
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);
140 }
141 
142 TEST_CASE("JSON -> Squirrel conversion")
143 {
144  TestScriptController controller;
145 
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");
158 
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");
162 
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);
166 
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);
173 
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 [
181 }
Squirrel
Definition: squirrel.hpp:23
ScriptAllocatorScope
Definition: squirrel.hpp:288
GameInstance
Runtime information about a game script like a pointer to the squirrel vm and the current state.
Definition: game_instance.hpp:16
TestScriptController
A controller to start enough so we can use Squirrel for testing.
Definition: test_script_admin.cpp:35