diff options
author | Benoit Germain <benoit.germain@ubisoft.com> | 2024-12-13 17:22:17 +0100 |
---|---|---|
committer | Benoit Germain <benoit.germain@ubisoft.com> | 2024-12-13 17:22:17 +0100 |
commit | dc7a2bc3a9c8316e17902493b832ca117805d29f (patch) | |
tree | 3c1a03edf586869119ef0de03631f6f603650720 /unit_tests/shared.cpp | |
parent | 7500a80fc06c5311c46df8f1761f25ae67277fc4 (diff) | |
download | lanes-dc7a2bc3a9c8316e17902493b832ca117805d29f.tar.gz lanes-dc7a2bc3a9c8316e17902493b832ca117805d29f.tar.bz2 lanes-dc7a2bc3a9c8316e17902493b832ca117805d29f.zip |
Append all unit tests to depot
Diffstat (limited to 'unit_tests/shared.cpp')
-rw-r--r-- | unit_tests/shared.cpp | 360 |
1 files changed, 360 insertions, 0 deletions
diff --git a/unit_tests/shared.cpp b/unit_tests/shared.cpp new file mode 100644 index 0000000..14b2b13 --- /dev/null +++ b/unit_tests/shared.cpp | |||
@@ -0,0 +1,360 @@ | |||
1 | #include "_pch.hpp" | ||
2 | |||
3 | #include "shared.h" | ||
4 | |||
5 | // ################################################################################################# | ||
6 | // ################################################################################################# | ||
7 | // internal fixture module | ||
8 | // ################################################################################################# | ||
9 | // ################################################################################################# | ||
10 | |||
11 | LANES_API int luaopen_deep_test(lua_State* L_); | ||
12 | |||
13 | namespace | ||
14 | { | ||
15 | extern int luaopen_fixture(lua_State*); | ||
16 | namespace local { | ||
17 | void PreloadModule(lua_State* const L_, std::string_view const& name_, lua_CFunction const openf_) | ||
18 | { | ||
19 | STACK_CHECK_START_REL(L_, 0); | ||
20 | lua_getglobal(L_, "package"); // L_: package | ||
21 | luaG_getfield(L_, kIdxTop, "preload"); // L_: package package.preload | ||
22 | lua_pushcfunction(L_, openf_); // L_: package package.preload openf_ | ||
23 | luaG_setfield(L_, StackIndex{ -2 }, name_); // L_: package package.preload | ||
24 | lua_pop(L_, 2); | ||
25 | STACK_CHECK(L_, 0); | ||
26 | } | ||
27 | |||
28 | |||
29 | static std::map<lua_State*, std::atomic_flag> sFinalizerHits; | ||
30 | static std::mutex sCallCountsLock; | ||
31 | |||
32 | // a finalizer that we can detect even after closing the state | ||
33 | lua_CFunction sThrowingFinalizer = +[](lua_State* L_) { | ||
34 | std::lock_guard _guard{ sCallCountsLock }; | ||
35 | sFinalizerHits[L_].test_and_set(); | ||
36 | luaG_pushstring(L_, "throw"); | ||
37 | return 1; | ||
38 | }; | ||
39 | |||
40 | // a finalizer that we can detect even after closing the state | ||
41 | lua_CFunction sYieldingFinalizer = +[](lua_State* L_) { | ||
42 | std::lock_guard _guard{ sCallCountsLock }; | ||
43 | sFinalizerHits[L_].test_and_set(); | ||
44 | return 0; | ||
45 | }; | ||
46 | |||
47 | // a function that runs forever | ||
48 | lua_CFunction sForever = +[](lua_State* L_) { | ||
49 | while (true) { | ||
50 | std::this_thread::yield(); | ||
51 | } | ||
52 | return 0; | ||
53 | }; | ||
54 | |||
55 | lua_CFunction sNewLightUserData = +[](lua_State* const L_) { | ||
56 | lua_pushlightuserdata(L_, std::bit_cast<void*>(static_cast<uintptr_t>(42))); | ||
57 | return 1; | ||
58 | }; | ||
59 | |||
60 | lua_CFunction sNewUserData = +[](lua_State* const L_) { | ||
61 | std::ignore = luaG_newuserdatauv<int>(L_, UserValueCount{ 0 }); | ||
62 | return 1; | ||
63 | }; | ||
64 | |||
65 | // a function that enables any lane to require "fixture" | ||
66 | lua_CFunction sOnStateCreate = +[](lua_State* const L_) { | ||
67 | PreloadModule(L_, "fixture", luaopen_fixture); | ||
68 | PreloadModule(L_, "deep_test", luaopen_deep_test); | ||
69 | return 0; | ||
70 | }; | ||
71 | |||
72 | static luaL_Reg const sFixture[] = { | ||
73 | { "forever", sForever }, | ||
74 | { "newlightuserdata", sNewLightUserData }, | ||
75 | { "newuserdata", sNewUserData }, | ||
76 | { "on_state_create", sOnStateCreate }, | ||
77 | { "throwing_finalizer", sThrowingFinalizer }, | ||
78 | { "yielding_finalizer", sYieldingFinalizer }, | ||
79 | { nullptr, nullptr } | ||
80 | }; | ||
81 | } // namespace local | ||
82 | |||
83 | // ############################################################################################ | ||
84 | |||
85 | int luaopen_fixture(lua_State* L_) | ||
86 | { | ||
87 | STACK_CHECK_START_REL(L_, 0); | ||
88 | luaG_newlib<std::size(local::sFixture)>(L_, local::sFixture); // M | ||
89 | STACK_CHECK(L_, 1); | ||
90 | return 1; | ||
91 | } | ||
92 | |||
93 | } // namespace | ||
94 | |||
95 | // ################################################################################################# | ||
96 | // ################################################################################################# | ||
97 | // Internals | ||
98 | // ################################################################################################# | ||
99 | // ################################################################################################# | ||
100 | |||
101 | TEST(Internals, StackChecker) | ||
102 | { | ||
103 | LuaState _L{ LuaState::WithBaseLibs{ true }, LuaState::WithFixture{ false } }; | ||
104 | StackChecker::CallsCassert = false; | ||
105 | |||
106 | auto _doStackCheckerTest = [&_L](lua_CFunction const _f, LuaError const _expected) { | ||
107 | lua_pushcfunction(_L, _f); | ||
108 | ASSERT_EQ(ToLuaError(lua_pcall(_L, 0, 0, 0)), _expected); | ||
109 | }; | ||
110 | |||
111 | // function where the StackChecker detects something wrong with the stack | ||
112 | lua_CFunction _unbalancedStack1 = +[](lua_State* const _L) { | ||
113 | // record current position | ||
114 | STACK_CHECK_START_REL(_L, 0); | ||
115 | // push something | ||
116 | lua_newtable(_L); | ||
117 | // check if we are at the same position as before (no) | ||
118 | STACK_CHECK(_L, 0); | ||
119 | return 1; | ||
120 | }; | ||
121 | |||
122 | // function where the StackChecker detects no issue | ||
123 | lua_CFunction _balancedStack1 = +[](lua_State* const _L) { | ||
124 | // record current position | ||
125 | STACK_CHECK_START_REL(_L, 0); | ||
126 | // check if we are at the same position as before (yes) | ||
127 | STACK_CHECK(_L, 0); | ||
128 | return 0; | ||
129 | }; | ||
130 | |||
131 | lua_CFunction _goodStart = +[](lua_State* const _L) { | ||
132 | // check that the stack ends at the specified position, and record that as our reference point | ||
133 | STACK_CHECK_START_ABS(_L, 0); | ||
134 | // check if we are at the same position as before (yes) | ||
135 | STACK_CHECK(_L, 0); | ||
136 | return 0; | ||
137 | }; | ||
138 | |||
139 | lua_CFunction _badStart = +[](lua_State* const _L) { | ||
140 | // check that the stack ends at the specified position (no), and record that as our reference point | ||
141 | STACK_CHECK_START_ABS(_L, 1); | ||
142 | // check if we are at the same position as before (yes) | ||
143 | STACK_CHECK(_L, 0); | ||
144 | return 0; | ||
145 | }; | ||
146 | |||
147 | _doStackCheckerTest(_unbalancedStack1, LuaError::ERRRUN); | ||
148 | _doStackCheckerTest(_balancedStack1, LuaError::OK); | ||
149 | _doStackCheckerTest(_goodStart, LuaError::OK); | ||
150 | _doStackCheckerTest(_badStart, LuaError::ERRRUN); | ||
151 | } | ||
152 | |||
153 | // ################################################################################################# | ||
154 | // ################################################################################################# | ||
155 | // LuaState | ||
156 | // ################################################################################################# | ||
157 | // ################################################################################################# | ||
158 | |||
159 | LuaState::LuaState(WithBaseLibs const withBaseLibs_, WithFixture const withFixture_) | ||
160 | { | ||
161 | STACK_CHECK_START_REL(L, 0); | ||
162 | if (withBaseLibs_) { | ||
163 | luaL_openlibs(L); | ||
164 | } | ||
165 | if (withFixture_) { | ||
166 | // make require "fixture" call luaopen_fixture | ||
167 | local::PreloadModule(L, "fixture", luaopen_fixture); | ||
168 | local::PreloadModule(L, "deep_test", luaopen_deep_test); | ||
169 | } | ||
170 | STACK_CHECK(L, 0); | ||
171 | } | ||
172 | |||
173 | // ################################################################################################# | ||
174 | |||
175 | void LuaState::close() | ||
176 | { | ||
177 | if (L) { | ||
178 | lua_close(L); | ||
179 | |||
180 | { | ||
181 | std::lock_guard _guard{ local::sCallCountsLock }; | ||
182 | finalizerWasCalled = local::sFinalizerHits[L].test(); | ||
183 | local::sFinalizerHits.erase(L); | ||
184 | } | ||
185 | |||
186 | L = nullptr; | ||
187 | } | ||
188 | } | ||
189 | |||
190 | // ################################################################################################# | ||
191 | |||
192 | LuaError LuaState::doString(std::string_view const& str_) const | ||
193 | { | ||
194 | lua_settop(L, 0); | ||
195 | if (str_.empty()) { | ||
196 | lua_pushnil(L); | ||
197 | return LuaError::OK; | ||
198 | } | ||
199 | STACK_CHECK_START_REL(L, 0); | ||
200 | LuaError const _loadErr{ luaL_loadstring(L, str_.data()) }; // L: chunk() | ||
201 | if (_loadErr != LuaError::OK) { | ||
202 | STACK_CHECK(L, 1); // the error message is on the stack | ||
203 | return _loadErr; | ||
204 | } | ||
205 | LuaError const _callErr{ lua_pcall(L, 0, 1, 0) }; // L: "<msg>"? | ||
206 | [[maybe_unused]] std::string_view const _out{ luaG_tostring(L, kIdxTop) }; | ||
207 | STACK_CHECK(L, 1); | ||
208 | return _callErr; | ||
209 | } | ||
210 | |||
211 | // ################################################################################################# | ||
212 | |||
213 | std::string_view LuaState::doStringAndRet(std::string_view const& str_) const | ||
214 | { | ||
215 | lua_settop(L, 0); | ||
216 | if (str_.empty()) { | ||
217 | luaG_pushstring(L, ""); | ||
218 | return luaG_tostring(L, kIdxTop); | ||
219 | } | ||
220 | STACK_CHECK_START_REL(L, 0); | ||
221 | LuaError const _loadErr{ luaL_loadstring(L, str_.data()) }; // L: chunk() | ||
222 | if (_loadErr != LuaError::OK) { | ||
223 | STACK_CHECK(L, 1); // the error message is on the stack | ||
224 | return ""; | ||
225 | } | ||
226 | LuaError const _callErr{ lua_pcall(L, 0, 1, 0) }; // L: "<msg>"?|retstring | ||
227 | STACK_CHECK(L, 1); | ||
228 | return luaG_tostring(L, kIdxTop); | ||
229 | } | ||
230 | |||
231 | // ################################################################################################# | ||
232 | |||
233 | LuaError LuaState::doFile(std::filesystem::path const& root_, std::string_view const& str_) const | ||
234 | { | ||
235 | lua_settop(L, 0); | ||
236 | if (str_.empty()) { | ||
237 | lua_pushnil(L); | ||
238 | return LuaError::OK; | ||
239 | } | ||
240 | STACK_CHECK_START_REL(L, 0); | ||
241 | std::filesystem::path _combined{ root_ }; | ||
242 | _combined.append(str_); | ||
243 | _combined.replace_extension(".lua"); | ||
244 | LuaError const _loadErr{ luaL_loadfile(L, _combined.generic_string().c_str()) }; // L: chunk() | ||
245 | if (_loadErr != LuaError::OK) { | ||
246 | STACK_CHECK(L, 1); | ||
247 | return _loadErr; | ||
248 | } | ||
249 | LuaError const _callErr{ lua_pcall(L, 0, 1, 0) }; // L: "<msg>"? | ||
250 | STACK_CHECK(L, 1); // either nil, a return value, or an error string | ||
251 | return _callErr; | ||
252 | } | ||
253 | |||
254 | // ################################################################################################# | ||
255 | |||
256 | LuaError LuaState::loadString(std::string_view const& str_) const | ||
257 | { | ||
258 | lua_settop(L, 0); | ||
259 | if (str_.empty()) { | ||
260 | // this particular test is disabled: just create a dummy function that will run without error | ||
261 | lua_pushcfunction(L, +[](lua_State*){return 0;}); | ||
262 | return LuaError::OK; | ||
263 | } | ||
264 | STACK_CHECK_START_REL(L, 0); | ||
265 | LuaError const _loadErr{ luaL_loadstring(L, str_.data()) }; // L: chunk() | ||
266 | STACK_CHECK(L, 1); // function on success, error string on failure | ||
267 | return _loadErr; | ||
268 | } | ||
269 | |||
270 | // ################################################################################################# | ||
271 | |||
272 | LuaError LuaState::loadFile(std::filesystem::path const& root_, std::string_view const& str_) const | ||
273 | { | ||
274 | lua_settop(L, 0); | ||
275 | STACK_CHECK_START_REL(L, 0); | ||
276 | if (str_.empty()) { | ||
277 | // this particular test is disabled: just create a dummy function that will run without error | ||
278 | lua_pushcfunction(L, +[](lua_State*){return 0;}); | ||
279 | return LuaError::OK; | ||
280 | } | ||
281 | |||
282 | std::filesystem::path _combined{ root_ }; | ||
283 | _combined.append(str_); | ||
284 | _combined.replace_extension(".lua"); | ||
285 | LuaError const _loadErr{ luaL_loadfile(L, _combined.generic_string().c_str()) }; // L: chunk() | ||
286 | STACK_CHECK(L, 1); // function on success, error string on failure | ||
287 | return _loadErr; | ||
288 | } | ||
289 | |||
290 | // ################################################################################################# | ||
291 | |||
292 | LuaError LuaState::runChunk() const | ||
293 | { | ||
294 | STACK_CHECK_START_ABS(L, 1); // we must start with the chunk on the stack (or an error string if it failed to load) | ||
295 | LuaError const _callErr{ lua_pcall(L, 0, 1, 0) }; // L: "<msg>"? | ||
296 | STACK_CHECK(L, 1); | ||
297 | return _callErr; | ||
298 | } | ||
299 | |||
300 | // ################################################################################################# | ||
301 | // ################################################################################################# | ||
302 | |||
303 | TEST(LuaState, DoString) | ||
304 | { | ||
305 | LuaState _L{ LuaState::WithBaseLibs{ true }, LuaState::WithFixture{ false } }; | ||
306 | // if the script fails to load, we should find the error message at the top of the stack | ||
307 | ASSERT_TRUE([&L = _L]() { std::ignore = L.doString("function end"); return lua_gettop(L) == 1 && luaG_type(L, StackIndex{1}) == LuaType::STRING; }()); | ||
308 | |||
309 | // if the script runs, the stack should contain its return value | ||
310 | ASSERT_TRUE([&L = _L]() { std::ignore = L.doString("return true"); return lua_gettop(L) == 1 && luaG_type(L, StackIndex{1}) == LuaType::BOOLEAN; }()); | ||
311 | ASSERT_TRUE([&L = _L]() { std::ignore = L.doString("return 'hello'"); return lua_gettop(L) == 1 && luaG_tostring(L, StackIndex{1}) == "hello"; }()); | ||
312 | // or nil if it didn't return anything | ||
313 | ASSERT_TRUE([&L = _L]() { std::ignore = L.doString("return"); return lua_gettop(L) == 1 && luaG_type(L, StackIndex{1}) == LuaType::NIL; }()); | ||
314 | |||
315 | // on failure, doStringAndRet returns "", and the error message is on the stack | ||
316 | ASSERT_TRUE([&L = _L]() { return L.doStringAndRet("function end") == "" && lua_gettop(L) == 1 && luaG_type(L, StackIndex{1}) == LuaType::STRING && luaG_tostring(L, StackIndex{1}) != ""; }()); | ||
317 | // on success doStringAndRet returns the string returned by the script, that is also at the top of the stack | ||
318 | ASSERT_TRUE([&L = _L]() { return L.doStringAndRet("return 'hello'") == "hello" && lua_gettop(L) == 1 && luaG_type(L, StackIndex{1}) == LuaType::STRING && luaG_tostring(L, StackIndex{1}) == "hello"; }()); | ||
319 | // if the returned value is not (convertible to) a string, we should get an empty string out of doStringAndRet | ||
320 | ASSERT_TRUE([&L = _L]() { return L.doStringAndRet("return function() end") == "" && lua_gettop(L) == 1 && luaG_type(L, StackIndex{1}) == LuaType::FUNCTION && luaG_tostring(L, StackIndex{1}) == ""; }()); | ||
321 | } | ||
322 | |||
323 | // ################################################################################################# | ||
324 | // ################################################################################################# | ||
325 | // UnitTestRunner | ||
326 | // ################################################################################################# | ||
327 | // ################################################################################################# | ||
328 | |||
329 | UnitTestRunner::UnitTestRunner() | ||
330 | { | ||
331 | [[maybe_unused]] std::filesystem::path const _current{ std::filesystem::current_path() }; | ||
332 | std::filesystem::path _assertPath{ R"(.\lanes\unit_tests\scripts)" }; | ||
333 | // I need to append that path to the list of locations where modules can be required | ||
334 | // so that the scripts can require "_assert" and find _assert.lua (same with "_utils.lua") | ||
335 | std::string _script{ "package.path = package.path.." }; | ||
336 | _script += "';"; | ||
337 | _script += std::filesystem::canonical(_assertPath).generic_string(); | ||
338 | _script += "/?.lua'"; | ||
339 | std::ignore = L.doString(_script.c_str()); | ||
340 | |||
341 | root = std::filesystem::canonical(R"(.\lanes\unit_tests\scripts)").generic_string(); | ||
342 | } | ||
343 | |||
344 | // ################################################################################################# | ||
345 | |||
346 | TEST_P(UnitTestRunner, ScriptedTest) | ||
347 | { | ||
348 | FileRunnerParam const& _param = GetParam(); | ||
349 | switch (_param.test) { | ||
350 | case TestType::AssertNoLuaError: | ||
351 | ASSERT_EQ(L.doFile(root, _param.script), LuaError::OK) << L; | ||
352 | break; | ||
353 | case TestType::AssertNoThrow: | ||
354 | ASSERT_NO_THROW((std::ignore = L.doFile(root, _param.script), L.close())) << L; | ||
355 | break; | ||
356 | case TestType::AssertThrows: | ||
357 | ASSERT_THROW((std::ignore = L.doFile(root, _param.script), L.close()), std::logic_error) << L; | ||
358 | break; | ||
359 | } | ||
360 | } | ||