1 // Copyright (c) 2013-2014 Sandstorm Development Group, Inc. and contributors
2 // Licensed under the MIT License:
3 //
4 // Permission is hereby granted, free of charge, to any person obtaining a copy
5 // of this software and associated documentation files (the "Software"), to deal
6 // in the Software without restriction, including without limitation the rights
7 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 // copies of the Software, and to permit persons to whom the Software is
9 // furnished to do so, subject to the following conditions:
10 //
11 // The above copyright notice and this permission notice shall be included in
12 // all copies or substantial portions of the Software.
13 //
14 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 // THE SOFTWARE.
21
22 #ifndef _GNU_SOURCE
23 #define _GNU_SOURCE
24 #endif
25
26 #include "test.h"
27 #include "main.h"
28 #include "io.h"
29 #include "miniposix.h"
30 #include <stdlib.h>
31 #include <signal.h>
32 #include <string.h>
33 #include "time.h"
34 #ifndef _WIN32
35 #include <sys/mman.h>
36 #endif
37
38 namespace kj {
39
40 namespace {
41
42 TestCase* testCasesHead = nullptr;
43 TestCase** testCasesTail = &testCasesHead;
44
45 } // namespace
46
TestCase(const char * file,uint line,const char * description)47 TestCase::TestCase(const char* file, uint line, const char* description)
48 : file(file), line(line), description(description), next(nullptr), prev(testCasesTail),
49 matchedFilter(false) {
50 *prev = this;
51 testCasesTail = &next;
52 }
53
~TestCase()54 TestCase::~TestCase() {
55 *prev = next;
56 if (next == nullptr) {
57 testCasesTail = prev;
58 } else {
59 next->prev = prev;
60 }
61 }
62
63 // =======================================================================================
64
65 namespace _ { // private
66
GlobFilter(const char * pattern)67 GlobFilter::GlobFilter(const char* pattern): pattern(heapString(pattern)) {}
GlobFilter(ArrayPtr<const char> pattern)68 GlobFilter::GlobFilter(ArrayPtr<const char> pattern): pattern(heapString(pattern)) {}
69
matches(StringPtr name)70 bool GlobFilter::matches(StringPtr name) {
71 // Get out your computer science books. We're implementing a non-deterministic finite automaton.
72 //
73 // Our NDFA has one "state" corresponding to each character in the pattern.
74 //
75 // As you may recall, an NDFA can be transformed into a DFA where every state in the DFA
76 // represents some combination of states in the NDFA. Therefore, we actually have to store a
77 // list of states here. (Actually, what we really want is a set of states, but because our
78 // patterns are mostly non-cyclic a list of states should work fine and be a bit more efficient.)
79
80 // Our state list starts out pointing only at the start of the pattern.
81 states.resize(0);
82 states.add(0);
83
84 Vector<uint> scratch;
85
86 // Iterate through each character in the name.
87 for (char c: name) {
88 // Pull the current set of states off to the side, so that we can populate `states` with the
89 // new set of states.
90 Vector<uint> oldStates = kj::mv(states);
91 states = kj::mv(scratch);
92 states.resize(0);
93
94 // The pattern can omit a leading path. So if we're at a '/' then enter the state machine at
95 // the beginning on the next char.
96 if (c == '/' || c == '\\') {
97 states.add(0);
98 }
99
100 // Process each state.
101 for (uint state: oldStates) {
102 applyState(c, state);
103 }
104
105 // Store the previous state vector for reuse.
106 scratch = kj::mv(oldStates);
107 }
108
109 // If any one state is at the end of the pattern (or at a wildcard just before the end of the
110 // pattern), we have a match.
111 for (uint state: states) {
112 while (state < pattern.size() && pattern[state] == '*') {
113 ++state;
114 }
115 if (state == pattern.size()) {
116 return true;
117 }
118 }
119 return false;
120 }
121
applyState(char c,int state)122 void GlobFilter::applyState(char c, int state) {
123 if (state < pattern.size()) {
124 switch (pattern[state]) {
125 case '*':
126 // At a '*', we both re-add the current state and attempt to match the *next* state.
127 if (c != '/' && c != '\\') { // '*' doesn't match '/'.
128 states.add(state);
129 }
130 applyState(c, state + 1);
131 break;
132
133 case '?':
134 // A '?' matches one character (never a '/').
135 if (c != '/' && c != '\\') {
136 states.add(state + 1);
137 }
138 break;
139
140 default:
141 // Any other character matches only itself.
142 if (c == pattern[state]) {
143 states.add(state + 1);
144 }
145 break;
146 }
147 }
148 }
149
150 } // namespace _ (private)
151
152 // =======================================================================================
153
154 namespace {
155
156 class TestExceptionCallback: public ExceptionCallback {
157 public:
TestExceptionCallback(ProcessContext & context)158 TestExceptionCallback(ProcessContext& context): context(context) {}
159
failed()160 bool failed() { return sawError; }
161
logMessage(LogSeverity severity,const char * file,int line,int contextDepth,String && text)162 void logMessage(LogSeverity severity, const char* file, int line, int contextDepth,
163 String&& text) override {
164 void* traceSpace[32];
165 auto trace = getStackTrace(traceSpace, 2);
166
167 if (text.size() == 0) {
168 text = kj::heapString("expectation failed");
169 }
170
171 text = kj::str(kj::repeat('_', contextDepth), file, ':', line, ": ", kj::mv(text));
172
173 if (severity == LogSeverity::ERROR || severity == LogSeverity::FATAL) {
174 sawError = true;
175 context.error(kj::str(text, "\nstack: ", strArray(trace, " "), stringifyStackTrace(trace)));
176 } else {
177 context.warning(text);
178 }
179 }
180
181 private:
182 ProcessContext& context;
183 bool sawError = false;
184 };
185
readClock()186 TimePoint readClock() {
187 return systemPreciseMonotonicClock().now();
188 }
189
190 } // namespace
191
192 class TestRunner {
193 public:
TestRunner(ProcessContext & context)194 explicit TestRunner(ProcessContext& context)
195 : context(context), useColor(isatty(STDOUT_FILENO)) {}
196
getMain()197 MainFunc getMain() {
198 return MainBuilder(context, "KJ Test Runner (version not applicable)",
199 "Run all tests that have been linked into the binary with this test runner.")
200 .addOptionWithArg({'f', "filter"}, KJ_BIND_METHOD(*this, setFilter), "<file>[:<line>]",
201 "Run only the specified test case(s). You may use a '*' wildcard in <file>. You may "
202 "also omit any prefix of <file>'s path; test from all matching files will run. "
203 "You may specify multiple filters; any test matching at least one filter will run. "
204 "<line> may be a range, e.g. \"100-500\".")
205 .addOption({'l', "list"}, KJ_BIND_METHOD(*this, setList),
206 "List all test cases that would run, but don't run them. If --filter is specified "
207 "then only the match tests will be listed.")
208 .callAfterParsing(KJ_BIND_METHOD(*this, run))
209 .build();
210 }
211
setFilter(StringPtr pattern)212 MainBuilder::Validity setFilter(StringPtr pattern) {
213 hasFilter = true;
214 ArrayPtr<const char> filePattern = pattern;
215 uint minLine = kj::minValue;
216 uint maxLine = kj::maxValue;
217
218 KJ_IF_MAYBE(colonPos, pattern.findLast(':')) {
219 char* end;
220 StringPtr lineStr = pattern.slice(*colonPos + 1);
221
222 bool parsedRange = false;
223 minLine = strtoul(lineStr.cStr(), &end, 0);
224 if (end != lineStr.begin()) {
225 if (*end == '-') {
226 // A range.
227 const char* part2 = end + 1;
228 maxLine = strtoul(part2, &end, 0);
229 if (end > part2 && *end == '\0') {
230 parsedRange = true;
231 }
232 } else if (*end == '\0') {
233 parsedRange = true;
234 maxLine = minLine;
235 }
236 }
237
238 if (parsedRange) {
239 // We have an exact line number.
240 filePattern = pattern.slice(0, *colonPos);
241 } else {
242 // Can't parse as a number. Maybe the colon is part of a Windows path name or something.
243 // Let's just keep it as part of the file pattern.
244 minLine = kj::minValue;
245 maxLine = kj::maxValue;
246 }
247 }
248
249 _::GlobFilter filter(filePattern);
250
251 for (TestCase* testCase = testCasesHead; testCase != nullptr; testCase = testCase->next) {
252 if (!testCase->matchedFilter && filter.matches(testCase->file) &&
253 testCase->line >= minLine && testCase->line <= maxLine) {
254 testCase->matchedFilter = true;
255 }
256 }
257
258 return true;
259 }
260
setList()261 MainBuilder::Validity setList() {
262 listOnly = true;
263 return true;
264 }
265
run()266 MainBuilder::Validity run() {
267 if (testCasesHead == nullptr) {
268 return "no tests were declared";
269 }
270
271 // Find the common path prefix of all filenames, so we can strip it off.
272 ArrayPtr<const char> commonPrefix = StringPtr(testCasesHead->file);
273 for (TestCase* testCase = testCasesHead; testCase != nullptr; testCase = testCase->next) {
274 for (size_t i: kj::indices(commonPrefix)) {
275 if (testCase->file[i] != commonPrefix[i]) {
276 commonPrefix = commonPrefix.slice(0, i);
277 break;
278 }
279 }
280 }
281
282 // Back off the prefix to the last '/'.
283 while (commonPrefix.size() > 0 && commonPrefix.back() != '/' && commonPrefix.back() != '\\') {
284 commonPrefix = commonPrefix.slice(0, commonPrefix.size() - 1);
285 }
286
287 // Run the testts.
288 uint passCount = 0;
289 uint failCount = 0;
290 for (TestCase* testCase = testCasesHead; testCase != nullptr; testCase = testCase->next) {
291 if (!hasFilter || testCase->matchedFilter) {
292 auto name = kj::str(testCase->file + commonPrefix.size(), ':', testCase->line,
293 ": ", testCase->description);
294
295 write(BLUE, "[ TEST ]", name);
296
297 if (!listOnly) {
298 bool currentFailed = true;
299 auto start = readClock();
300 KJ_IF_MAYBE(exception, runCatchingExceptions([&]() {
301 TestExceptionCallback exceptionCallback(context);
302 testCase->run();
303 currentFailed = exceptionCallback.failed();
304 })) {
305 context.error(kj::str(*exception));
306 }
307 auto end = readClock();
308
309 auto message = kj::str(name, " (", (end - start) / kj::MICROSECONDS, " μs)");
310
311 if (currentFailed) {
312 write(RED, "[ FAIL ]", message);
313 ++failCount;
314 } else {
315 write(GREEN, "[ PASS ]", message);
316 ++passCount;
317 }
318 }
319 }
320 }
321
322 if (passCount > 0) write(GREEN, kj::str(passCount, " test(s) passed"), "");
323 if (failCount > 0) write(RED, kj::str(failCount, " test(s) failed"), "");
324 context.exit();
325
326 KJ_UNREACHABLE;
327 }
328
329 private:
330 ProcessContext& context;
331 bool useColor;
332 bool hasFilter = false;
333 bool listOnly = false;
334
335 enum Color {
336 RED,
337 GREEN,
338 BLUE
339 };
340
write(StringPtr text)341 void write(StringPtr text) {
342 FdOutputStream(STDOUT_FILENO).write(text.begin(), text.size());
343 }
344
write(Color color,StringPtr prefix,StringPtr message)345 void write(Color color, StringPtr prefix, StringPtr message) {
346 StringPtr startColor, endColor;
347 if (useColor) {
348 switch (color) {
349 case RED: startColor = "\033[0;1;31m"; break;
350 case GREEN: startColor = "\033[0;1;32m"; break;
351 case BLUE: startColor = "\033[0;1;34m"; break;
352 }
353 endColor = "\033[0m";
354 }
355
356 String text = kj::str(startColor, prefix, endColor, ' ', message, '\n');
357 write(text);
358 }
359 };
360
361 } // namespace kj
362
363 KJ_MAIN(kj::TestRunner);
364