blob: e988320f8944d435c2d7a2fd419fe662af983beb [file] [log] [blame]
// Copyright (c) 2013-2014 Sandstorm Development Group, Inc. and contributors
// Licensed under the MIT License:
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include "test.h"
#include "main.h"
#include "io.h"
#include "miniposix.h"
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#include "time.h"
#ifndef _WIN32
#include <sys/mman.h>
#endif
namespace kj {
namespace {
TestCase* testCasesHead = nullptr;
TestCase** testCasesTail = &testCasesHead;
} // namespace
TestCase::TestCase(const char* file, uint line, const char* description)
: file(file), line(line), description(description), next(nullptr), prev(testCasesTail),
matchedFilter(false) {
*prev = this;
testCasesTail = &next;
}
TestCase::~TestCase() {
*prev = next;
if (next == nullptr) {
testCasesTail = prev;
} else {
next->prev = prev;
}
}
// =======================================================================================
namespace _ { // private
GlobFilter::GlobFilter(const char* pattern): pattern(heapString(pattern)) {}
GlobFilter::GlobFilter(ArrayPtr<const char> pattern): pattern(heapString(pattern)) {}
bool GlobFilter::matches(StringPtr name) {
// Get out your computer science books. We're implementing a non-deterministic finite automaton.
//
// Our NDFA has one "state" corresponding to each character in the pattern.
//
// As you may recall, an NDFA can be transformed into a DFA where every state in the DFA
// represents some combination of states in the NDFA. Therefore, we actually have to store a
// list of states here. (Actually, what we really want is a set of states, but because our
// patterns are mostly non-cyclic a list of states should work fine and be a bit more efficient.)
// Our state list starts out pointing only at the start of the pattern.
states.resize(0);
states.add(0);
Vector<uint> scratch;
// Iterate through each character in the name.
for (char c: name) {
// Pull the current set of states off to the side, so that we can populate `states` with the
// new set of states.
Vector<uint> oldStates = kj::mv(states);
states = kj::mv(scratch);
states.resize(0);
// The pattern can omit a leading path. So if we're at a '/' then enter the state machine at
// the beginning on the next char.
if (c == '/' || c == '\\') {
states.add(0);
}
// Process each state.
for (uint state: oldStates) {
applyState(c, state);
}
// Store the previous state vector for reuse.
scratch = kj::mv(oldStates);
}
// If any one state is at the end of the pattern (or at a wildcard just before the end of the
// pattern), we have a match.
for (uint state: states) {
while (state < pattern.size() && pattern[state] == '*') {
++state;
}
if (state == pattern.size()) {
return true;
}
}
return false;
}
void GlobFilter::applyState(char c, int state) {
if (state < pattern.size()) {
switch (pattern[state]) {
case '*':
// At a '*', we both re-add the current state and attempt to match the *next* state.
if (c != '/' && c != '\\') { // '*' doesn't match '/'.
states.add(state);
}
applyState(c, state + 1);
break;
case '?':
// A '?' matches one character (never a '/').
if (c != '/' && c != '\\') {
states.add(state + 1);
}
break;
default:
// Any other character matches only itself.
if (c == pattern[state]) {
states.add(state + 1);
}
break;
}
}
}
} // namespace _ (private)
// =======================================================================================
namespace {
class TestExceptionCallback: public ExceptionCallback {
public:
TestExceptionCallback(ProcessContext& context): context(context) {}
bool failed() { return sawError; }
void logMessage(LogSeverity severity, const char* file, int line, int contextDepth,
String&& text) override {
void* traceSpace[32];
auto trace = getStackTrace(traceSpace, 2);
if (text.size() == 0) {
text = kj::heapString("expectation failed");
}
text = kj::str(kj::repeat('_', contextDepth), file, ':', line, ": ", kj::mv(text));
if (severity == LogSeverity::ERROR || severity == LogSeverity::FATAL) {
sawError = true;
context.error(kj::str(text, "\nstack: ", strArray(trace, " "), stringifyStackTrace(trace)));
} else {
context.warning(text);
}
}
private:
ProcessContext& context;
bool sawError = false;
};
TimePoint readClock() {
return systemPreciseMonotonicClock().now();
}
} // namespace
class TestRunner {
public:
explicit TestRunner(ProcessContext& context)
: context(context), useColor(isatty(STDOUT_FILENO)) {}
MainFunc getMain() {
return MainBuilder(context, "KJ Test Runner (version not applicable)",
"Run all tests that have been linked into the binary with this test runner.")
.addOptionWithArg({'f', "filter"}, KJ_BIND_METHOD(*this, setFilter), "<file>[:<line>]",
"Run only the specified test case(s). You may use a '*' wildcard in <file>. You may "
"also omit any prefix of <file>'s path; test from all matching files will run. "
"You may specify multiple filters; any test matching at least one filter will run. "
"<line> may be a range, e.g. \"100-500\".")
.addOption({'l', "list"}, KJ_BIND_METHOD(*this, setList),
"List all test cases that would run, but don't run them. If --filter is specified "
"then only the match tests will be listed.")
.callAfterParsing(KJ_BIND_METHOD(*this, run))
.build();
}
MainBuilder::Validity setFilter(StringPtr pattern) {
hasFilter = true;
ArrayPtr<const char> filePattern = pattern;
uint minLine = kj::minValue;
uint maxLine = kj::maxValue;
KJ_IF_MAYBE(colonPos, pattern.findLast(':')) {
char* end;
StringPtr lineStr = pattern.slice(*colonPos + 1);
bool parsedRange = false;
minLine = strtoul(lineStr.cStr(), &end, 0);
if (end != lineStr.begin()) {
if (*end == '-') {
// A range.
const char* part2 = end + 1;
maxLine = strtoul(part2, &end, 0);
if (end > part2 && *end == '\0') {
parsedRange = true;
}
} else if (*end == '\0') {
parsedRange = true;
maxLine = minLine;
}
}
if (parsedRange) {
// We have an exact line number.
filePattern = pattern.slice(0, *colonPos);
} else {
// Can't parse as a number. Maybe the colon is part of a Windows path name or something.
// Let's just keep it as part of the file pattern.
minLine = kj::minValue;
maxLine = kj::maxValue;
}
}
_::GlobFilter filter(filePattern);
for (TestCase* testCase = testCasesHead; testCase != nullptr; testCase = testCase->next) {
if (!testCase->matchedFilter && filter.matches(testCase->file) &&
testCase->line >= minLine && testCase->line <= maxLine) {
testCase->matchedFilter = true;
}
}
return true;
}
MainBuilder::Validity setList() {
listOnly = true;
return true;
}
MainBuilder::Validity run() {
if (testCasesHead == nullptr) {
return "no tests were declared";
}
// Find the common path prefix of all filenames, so we can strip it off.
ArrayPtr<const char> commonPrefix = StringPtr(testCasesHead->file);
for (TestCase* testCase = testCasesHead; testCase != nullptr; testCase = testCase->next) {
for (size_t i: kj::indices(commonPrefix)) {
if (testCase->file[i] != commonPrefix[i]) {
commonPrefix = commonPrefix.slice(0, i);
break;
}
}
}
// Back off the prefix to the last '/'.
while (commonPrefix.size() > 0 && commonPrefix.back() != '/' && commonPrefix.back() != '\\') {
commonPrefix = commonPrefix.slice(0, commonPrefix.size() - 1);
}
// Run the testts.
uint passCount = 0;
uint failCount = 0;
for (TestCase* testCase = testCasesHead; testCase != nullptr; testCase = testCase->next) {
if (!hasFilter || testCase->matchedFilter) {
auto name = kj::str(testCase->file + commonPrefix.size(), ':', testCase->line,
": ", testCase->description);
write(BLUE, "[ TEST ]", name);
if (!listOnly) {
bool currentFailed = true;
auto start = readClock();
KJ_IF_MAYBE(exception, runCatchingExceptions([&]() {
TestExceptionCallback exceptionCallback(context);
testCase->run();
currentFailed = exceptionCallback.failed();
})) {
context.error(kj::str(*exception));
}
auto end = readClock();
auto message = kj::str(name, " (", (end - start) / kj::MICROSECONDS, " μs)");
if (currentFailed) {
write(RED, "[ FAIL ]", message);
++failCount;
} else {
write(GREEN, "[ PASS ]", message);
++passCount;
}
}
}
}
if (passCount > 0) write(GREEN, kj::str(passCount, " test(s) passed"), "");
if (failCount > 0) write(RED, kj::str(failCount, " test(s) failed"), "");
context.exit();
KJ_UNREACHABLE;
}
private:
ProcessContext& context;
bool useColor;
bool hasFilter = false;
bool listOnly = false;
enum Color {
RED,
GREEN,
BLUE
};
void write(StringPtr text) {
FdOutputStream(STDOUT_FILENO).write(text.begin(), text.size());
}
void write(Color color, StringPtr prefix, StringPtr message) {
StringPtr startColor, endColor;
if (useColor) {
switch (color) {
case RED: startColor = "\033[0;1;31m"; break;
case GREEN: startColor = "\033[0;1;32m"; break;
case BLUE: startColor = "\033[0;1;34m"; break;
}
endColor = "\033[0m";
}
String text = kj::str(startColor, prefix, endColor, ' ', message, '\n');
write(text);
}
};
} // namespace kj
KJ_MAIN(kj::TestRunner);