FlightGear next
NasalUnitTesting.cxx
Go to the documentation of this file.
1/*
2 * SPDX-FileName: NasalUnittesting.cxx
3 * SPDX-FileComment: Unit-test API for nasal
4 * SPDX-FileCopyrightText: Copyright (C) 2020 James Turner
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 */
7
8// There are two versions of this module, and we load one or the other
9// depending on if we're running the test_suite (using CppUnit) or
10// the normal simulator. The logic is that aircraft-developers and
11// people hacking Nasal likely don't have a way to run the test-suite,
12// whereas core-developers and Jenkins want a way to run all tests
13// through the standard CppUnit mechanism. So we have a consistent
14// Nasal API, but different implement in fgfs_test_suite vs
15// normal fgfs executable.
16
17#include "config.h"
18
20
21#include <Main/globals.hxx>
22#include <Main/util.hxx>
25
26#include <Main/fg_commands.hxx>
27
28#include <simgear/nasal/cppbind/from_nasal.hxx>
29#include <simgear/nasal/cppbind/to_nasal.hxx>
30#include <simgear/nasal/cppbind/NasalHash.hxx>
31#include <simgear/nasal/cppbind/Ghost.hxx>
32
33#include <simgear/structure/commands.hxx>
34#include <simgear/io/iostreams/sgstream.hxx>
35#include <simgear/misc/sg_dir.hxx>
36
38{
39 bool failure = false;
40 std::string failureMessage;
41 std::string failureFileName;
43};
44
45static std::unique_ptr<ActiveTest> static_activeTest;
46
47static naRef f_assert(const nasal::CallContext& ctx )
48{
49 bool pass = ctx.requireArg<bool>(0);
50 auto msg = ctx.getArg<std::string>(1);
51
52 if (!pass) {
53 if (!static_activeTest) {
54 ctx.runtimeError("No active test in progress");
55 }
56
57 if (static_activeTest->failure) {
58 ctx.runtimeError("Active test already failed");
59 }
60
61 static_activeTest->failure = true;
62 static_activeTest->failureMessage = msg;
63 static_activeTest->failureFileName = ctx.from_nasal<std::string>(naGetSourceFile(ctx.c_ctx(), 0));
64 static_activeTest->failLineNumber = naGetLine(ctx.c_ctx(), 0);
65
66 ctx.runtimeError("Test assert failed");
67 }
68
69 return naNil();
70}
71
72static naRef f_fail(const nasal::CallContext& ctx )
73{
74 auto msg = ctx.getArg<std::string>(0);
75
76 if (!static_activeTest) {
77 ctx.runtimeError("No active test in progress");
78 }
79
80 if (static_activeTest->failure) {
81 ctx.runtimeError("Active test already failed");
82 }
83
84 static_activeTest->failure = true;
85 static_activeTest->failureMessage = msg;
86 static_activeTest->failureFileName = ctx.from_nasal<std::string>(naGetSourceFile(ctx.c_ctx(), 0));
87 static_activeTest->failLineNumber = naGetLine(ctx.c_ctx(), 0);
88
89 ctx.runtimeError("Test failed");
90
91 return naNil();
92}
93
94static naRef f_assert_equal(const nasal::CallContext& ctx )
95{
96 naRef argA = ctx.requireArg<naRef>(0);
97 naRef argB = ctx.requireArg<naRef>(1);
98 auto msg = ctx.getArg<std::string>(2, "assert_equal failed");
99
100 bool same = nasalStructEqual(ctx.c_ctx(), argA, argB);
101 if (!same) {
102 std::string aStr = ctx.from_nasal<std::string>(argA);
103 std::string bStr = ctx.from_nasal<std::string>(argB);
104 msg += "; expected:" + aStr + ", actual:" + bStr;
105 static_activeTest->failure = true;
106 static_activeTest->failureMessage = msg;
107 static_activeTest->failureFileName = ctx.from_nasal<std::string>(naGetSourceFile(ctx.c_ctx(), 0));
108 static_activeTest->failLineNumber = naGetLine(ctx.c_ctx(), 0);
109 ctx.runtimeError(msg.c_str());
110 }
111
112 return naNil();
113}
114
115static naRef f_assert_doubles_equal(const nasal::CallContext& ctx )
116{
117 double argA = ctx.requireArg<double>(0);
118 double argB = ctx.requireArg<double>(1);
119 double tolerance = ctx.requireArg<double>(2);
120
121 auto msg = ctx.getArg<std::string>(3, "assert_doubles_equal failed");
122
123 const bool same = fabs(argA - argB) < tolerance;
124 if (!same) {
125 msg += "; expected:" + std::to_string(argA) + ", actual:" + std::to_string(argB);
126 static_activeTest->failure = true;
127 static_activeTest->failureMessage = msg;
128 static_activeTest->failureFileName = ctx.from_nasal<std::string>(naGetSourceFile(ctx.c_ctx(), 0));
129 static_activeTest->failLineNumber = naGetLine(ctx.c_ctx(), 0);
130 ctx.runtimeError(msg.c_str());
131 }
132
133 return naNil();
134}
135
136static naRef f_equal(const nasal::CallContext& ctx)
137{
138 naRef argA = ctx.requireArg<naRef>(0);
139 naRef argB = ctx.requireArg<naRef>(1);
140
141 bool same = nasalStructEqual(ctx.c_ctx(), argA, argB);
142 return naNum(same);
143}
144
145//------------------------------------------------------------------------------
146// commands
147
148bool command_executeNasalTest(const SGPropertyNode *arg, SGPropertyNode * root)
149{
150 SGPath p = SGPath::fromUtf8(arg->getStringValue("path"));
151 if (p.isRelative()) {
152 for (auto dp : globals->get_data_paths("Nasal")) {
153 SGPath absPath = dp / p.utf8Str();
154 if (absPath.exists()) {
155 p = absPath;
156 break;
157 }
158 }
159 }
160 if (!p.exists() || !p.isFile() || (p.lower_extension() != "nut")) {
161 SG_LOG(SG_NASAL, SG_DEV_ALERT, "not a Nasal test file:" << p);
162 return false;
163 }
164
165 return executeNasalTest(p);
166}
167
168bool command_executeNasalTestDir(const SGPropertyNode *arg, SGPropertyNode * root)
169{
170 SGPath p = SGPath::fromUtf8(arg->getStringValue("path"));
171 if (!p.exists() || !p.isDir()) {
172 SG_LOG(SG_NASAL, SG_DEV_ALERT, "no such directory:" << p);
173 return false;
174 }
175
177 return true;
178}
179
180//------------------------------------------------------------------------------
181naRef initNasalUnitTestInSim(naRef nasalGlobals, naContext c)
182{
183 nasal::Hash globals_module(nasalGlobals, c),
184 unitTest = globals_module.createHash("unitTest");
185
186 unitTest.set("assert", f_assert);
187 unitTest.set("fail", f_fail);
188 unitTest.set("assert_equal", f_assert_equal);
189 unitTest.set("assert_doubles_equal", f_assert_doubles_equal);
190 unitTest.set("equal", f_equal);
191
192 globals->get_commands()->addCommand("nasal-test", &command_executeNasalTest);
193 globals->get_commands()->addCommand("nasal-test-dir", &command_executeNasalTestDir);
194
195 return naNil();
196}
197
198void executeNasalTestsInDir(const SGPath& path)
199{
200 simgear::Dir d(path);
201
202 for (const auto& testFile : d.children(simgear::Dir::TYPE_FILE, "*.nut")) {
203 SG_LOG(SG_NASAL, SG_INFO, "Processing test file " << testFile);
204
205 } // of test files iteration
206}
207
208// variant on FGNasalSys parse,
209static naRef parseTestFile(naContext ctx, const char* filename,
210 const char* buf, int len,
211 std::string& errors)
212{
213 int errLine = -1;
214 naRef srcfile = naNewString(ctx);
215 naStr_fromdata(srcfile, (char*)filename, strlen(filename));
216 naRef code = naParseCode(ctx, srcfile, 1, (char*)buf, len, &errLine);
217 if(naIsNil(code)) {
218 std::ostringstream errorMessageStream;
219 errorMessageStream << "Nasal Test parse error: " << naGetError(ctx) <<
220 " in "<< filename <<", line " << errLine;
221 errors = errorMessageStream.str();
222 SG_LOG(SG_NASAL, SG_DEV_ALERT, errors);
223 return naNil();
224 }
225
226 const auto nasalSys = globals->get_subsystem<FGNasalSys>();
227 return naBindFunction(ctx, code, nasalSys->nasalGlobals());
228}
229
230
231
232bool executeNasalTest(const SGPath& path)
233{
234 naContext ctx = naNewContext();
235 const auto nasalSys = globals->get_subsystem<FGNasalSys>();
236 sg_ifstream file_in(path);
237 const auto source = file_in.read_all();
238
239 std::string errors;
240 std::string fileName = path.utf8Str();
241 naRef code = parseTestFile(ctx, fileName.c_str(),
242 source.c_str(),
243 source.size(), errors);
244 if(naIsNil(code)) {
245 naFreeContext(ctx);
246 return false;
247 }
248
249 // create test context
250
251 auto localNS = nasalSys->getGlobals().createHash("_test_" + path.utf8Str());
252 nasalSys->callWithContext(ctx, code, 0, 0, localNS.get_naRef());
253
254
255 auto setUpFunc = localNS.get("setUp");
256 auto tearDown = localNS.get("tearDown");
257
258 for (const auto value : localNS) {
259 if (value.getKey().find("test_") == 0) {
260 static_activeTest.reset(new ActiveTest);
261
262 if (naIsFunc(setUpFunc)) {
263 nasalSys->callWithContext(ctx, setUpFunc, 0, nullptr ,localNS.get_naRef());
264 }
265
266 const auto testName = value.getKey();
267 auto testFunc = value.getValue<naRef>();
268 if (!naIsFunc(testFunc)) {
269 SG_LOG(SG_NAVAID, SG_DEV_WARN, "Skipping non-function test member:" << testName);
270 continue;
271 }
272
273 nasalSys->callWithContext(ctx, testFunc, 0, nullptr, localNS.get_naRef());
274 if (static_activeTest->failure) {
275 SG_LOG(SG_NASAL, SG_ALERT, testName << ": Test failure:" << static_activeTest->failureMessage << "\n\tat: " << static_activeTest->failureFileName << ": " << static_activeTest->failLineNumber);
276 } else {
277 SG_LOG(SG_NASAL, SG_ALERT, testName << ": Test passed");
278 }
279
280 if (naIsFunc(tearDown)) {
281 nasalSys->callWithContext(ctx, tearDown, 0, nullptr ,localNS.get_naRef());
282 }
283
284 static_activeTest.reset();
285 }
286 }
287
288 // remvoe test hash/namespace
289
290 naFreeContext(ctx);
291 return true;
292}
293
294
296{
297 globals->get_commands()->removeCommand("nasal-test");
298 globals->get_commands()->removeCommand("nasal-test-dir");
299}
#define p(x)
static FGNasalSys * nasalSys
Definition NasalSys.cxx:82
int nasalStructEqual(naContext ctx, naRef a, naRef b)
@breif wrapper for naEqual which recursively checks vec/hash equality Probably not very performant.
static naRef f_fail(const nasal::CallContext &ctx)
bool command_executeNasalTestDir(const SGPropertyNode *arg, SGPropertyNode *root)
static naRef f_equal(const nasal::CallContext &ctx)
void shutdownNasalUnitTestInSim()
static std::unique_ptr< ActiveTest > static_activeTest
static naRef f_assert(const nasal::CallContext &ctx)
static naRef parseTestFile(naContext ctx, const char *filename, const char *buf, int len, std::string &errors)
static naRef f_assert_doubles_equal(const nasal::CallContext &ctx)
static naRef f_assert_equal(const nasal::CallContext &ctx)
naRef initNasalUnitTestInSim(naRef nasalGlobals, naContext c)
bool executeNasalTest(const SGPath &path)
void executeNasalTestsInDir(const SGPath &path)
bool command_executeNasalTest(const SGPropertyNode *arg, SGPropertyNode *root)
FGGlobals * globals
Definition globals.cxx:142
std::string failureMessage
std::string failureFileName