Skip to content

Commit

Permalink
[lib] Add unit-testing framework
Browse files Browse the repository at this point in the history
  • Loading branch information
titzer committed Dec 29, 2023
1 parent d0e3d28 commit a2af291
Show file tree
Hide file tree
Showing 4 changed files with 327 additions and 0 deletions.
2 changes: 2 additions & 0 deletions apps/UnitTest/DEPS
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
../../lib/util/*.v3
../../lib/test/*.v3
49 changes: 49 additions & 0 deletions apps/UnitTest/UnitTest.v3
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright 2023 Virgil authors. All rights reserved.
// See LICENSE for details of Apache 2.0 license.

// Register tests.
def T = UnitTests.register;
def X = [
T("single", test_single),
T("ok", test_ok),
T("fail0", test_fail0),
T("fail1", test_fail1),
T("fail2", test_fail2),
UnitTests.registerRenderer(FooBar.render),
()
];

// Tests
//========================================================================
def test_single(t: Tester) {
}

def test_ok(t: Tester) {
t.asserti(11, 11);
t.assertz(true, true);
t.assert(6 == 6, "sanity test");
}

def test_fail0(t: Tester) {
t.fail("fail on purpose");
}

def test_fail1(t: Tester) {
t.asserti(7, 8);
}

def test_fail2(t: Tester) {
t.assert_eq(FooBar.new(7, 8), FooBar.new(8, 9));
}

class FooBar(x: int, y: int) {
def render(buf: StringBuilder) -> StringBuilder {
return buf.put2("FooBar(%d, %d)", x, y);
}
}

// Main
//========================================================================
def main(args: Array<string>) -> int {
return UnitTests.run(args);
}
107 changes: 107 additions & 0 deletions lib/test/Tester.v3
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright 2023 Virgil authors. All rights reserved.
// See LICENSE for details of Apache 2.0 license.

// A {Tester} is instantiated for each execution of a unit test. It contains
// methods that help with assertions and matching. It outputs failures in the
// standard format (for the "progress" utility) directly to the stdout.
class Tester(name: string) {
var ok = true;
var msg: string;

def fail(msg: string) {
if (!ok) return;
this.ok = false;
this.msg = msg;
if (UnitTests.fatal) System.error("UnitTestError", msg);
}
def assert(cond: bool, msg: string) -> this {
if (!cond) fail(msg);
}
def asserti(expected: int, got: int) -> this {
if (expected != got) fail2("expected %d, got %d", expected, got);
}
def assertz(expected: bool, got: bool) -> this {
if (expected != got) fail2("expected %z, got %z", expected, got);
}
def assertl(expected: long, got: long) -> this {
if (expected != got) fail2("expected %d, got %d", expected, got);
}
def assert_eq<T>(a: T, b: T) -> this {
if (a != b && !UnitTests.equal(a, b)) {
var buf = StringBuilder.new().puts("expected ");
render(buf, a);
buf.puts(" == ");
render(buf, b);
fail(buf.toString());
}
}
def assert_ne<T>(a: T, b: T) -> this {
if (a == b || UnitTests.equal(a, b)) {
var buf = StringBuilder.new().puts("expected ");
render(buf, a);
buf.puts(" != ");
render(buf, b);
fail(buf.toString());
}
}
def assert_type<F, T>(v: F) -> this {
if (!T.?(v)) fail("expected type");
}
def assertb(expected: Array<byte>, got: Array<byte>) -> this {
asserta("data", expected, got, StringBuilder.putx<byte>);
}
def assert_string(expected: string, got: string) -> this {
if (!Strings.equal(expected, got)) {
var msg = StringBuilder.new().puts("expected ");
if (expected == null) msg.puts("null");
else msg.putsq(expected);
msg.puts(", got ");
if (got == null) msg.puts("null");
else msg.putsq(got);
fail(msg.extract());
}
}
def asserta<T>(thing: string, expected: Array<T>, got: Array<T>, render: (StringBuilder, T) -> StringBuilder) {
if (expected.length != got.length) {
return fail3("expected %s.length == %d, got %d", thing, expected.length, got.length);
}
for (i < expected.length) {
var e = expected[i], g = got[i];
if (e != g && !UnitTests.equal(e, g)) {
var buf = StringBuilder.new()
.put2("expected %s[%d] == ", thing, i);
render(buf, e);
buf.puts(", got ");
render(buf, g);
return fail(buf.extract());
}
}
}
def assertar<T>(thing: string, expected: Array<T>, got: Array<T>, render: (T, StringBuilder) -> StringBuilder) {
return asserta(thing, expected, got, commute(render, _));
}
def commute<A, B, R>(f: (A, B) -> R, t: (B, A)) -> R { // XXX: factor out to good location
return f(t.1, t.0);
}
private def render<T>(buf: StringBuilder, v: T) -> StringBuilder {
match (v) {
x: u32 => buf.putd(x);
x: int => buf.putd(x);
x: u64 => buf.putd(x);
x: long => buf.putd(x);
x: bool => buf.putz(x);
x: string => buf.putsq(x);
_ => UnitTests.render(v, buf);
}
return buf;
}
def fail1<T>(msg: string, a: T) {
fail(Strings.format1(msg, a));
}
def fail2<T, U>(msg: string, a: T, b: U) {
fail(Strings.format2(msg, a, b));
}
def fail3<T, U, V>(msg: string, a: T, b: U, c: V) {
fail(Strings.format3(msg, a, b, c));
}
}
169 changes: 169 additions & 0 deletions lib/test/UnitTests.v3
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// Copyright 2020 Ben L. Titzer. All rights reserved.
// See LICENSE for details of Apache 2.0 license.

// Global unittest registry.
component UnitTests {
private var buf = StringBuilder.new();
private def expected = Strings.newMap<bool>(); // contains expected failures
private var list: List<UnitTest>;
private var renderers: Renderer; // list of custom renderers
private var comparators: Comparator; // list of custom comparators

var fatal: bool = false;
var trace: bool = false;

// Registration methods.
def register(name: string, fun: Tester -> ()) {
list = List.new(UnitTest(name, fun), list);
}
def registerT<T>(prefix: string, name: string, n: Tester -> T, f: T -> void) {
if (prefix != null) name = buf.reset().puts(prefix).puts(name).extract();
register(name, runNew<T>(_, n, f));
}
private def runNew<T>(t: Tester, n: Tester -> T, f: T -> ()) {
return f(n(t));
}

// Run method, e.g. from command-line. Parses {args}.
def run(args: Array<string>) -> int {
var matchers = Vector<GlobMatcher>.new();
// Parse options first
for (i < args.length) {
var arg = args[i];
if (arg == null) continue;
if (arg.length > 0 && arg[0] == '-') {
if (Strings.equal(arg, "-fatal")) {
fatal = true;
} else if (Strings.startsWith(arg, "-expected=")) {
loadExpectedFile(expected, Arrays.range(arg, "-expected=".length, arg.length));
} else {
System.puts("Unknown option: ");
System.puts(arg);
System.ln();
return 1;
}
} else {
matchers.put(GlobMatcher.new(arg));
}

}
// Filter the registered tests with matchers
var count = 0, r: List<UnitTest>;
for (l = UnitTests.list; l != null; l = l.tail) { // count and reverse list
var t = l.head;
if (matchers.length > 0) {
var skip = true;
for (i < matchers.length) {
if (skip) skip = !matchers[i].matches(t.name);
}
if (skip) continue;
}
r = List.new(l.head, r);
count++;
}
// Run tests
System.puts("##>");
System.puti(count);
System.puts(" unit tests\n");
var fail = false;
for (l = r; l != null; l = l.tail) {
var u = l.head;
var t = Tester.new(u.name);
System.puts("##+");
System.puts(u.name);
System.ln();
var before = if(trace, System.ticksUs());
u.fun(t);
if (trace) {
var diff = System.ticksUs() - before;
System.puts("##@");
System.puts(u.name);
System.puts(" : ");
System.puti(diff);
System.puts(" us\n");
}
if (t.ok) {
System.puts("##-ok\n");
} else if (expected[u.name]) {
System.puts("##-ok (ignored failure: ");
System.puts(t.msg);
System.puts(")\n");
} else {
fail = true;
System.puts("##-fail (");
System.puts(t.msg);
System.puts(")\n");
}
}
return if(fail, 1, 0);
}
// Register a custom rendering routine for the type {T}.
def registerRenderer<T>(func: (T, StringBuilder) -> StringBuilder) {
renderers = RendererOf<T>.new(func, renderers);
}
def render<T>(t: T, buf: StringBuilder) -> StringBuilder {
for (l = renderers; l != null; l = l.next) match (l) {
x: RendererOf<T> => return x.func(t, buf);
}
return buf.puts("?");
}
// Register a custom comparator for the type {T}.
def registerComparator<T>(func: (T, T) -> bool) {
comparators = ComparatorOf<T>.new(func, comparators);
}
def equal<T>(a: T, b: T) -> bool {
if (a == b) return true;
for (l = comparators; l != null; l = l.next) match (l) {
x: ComparatorOf<T> => return x.func(a, b);
}
return false;
}
}

// An individual unit test.
private type UnitTest(name: string, fun: Tester -> ()) #unboxed;

// Custom renderers to make using assertions extensible.
private class Renderer(next: Renderer) {
}
private class RendererOf<T> extends Renderer {
def func: (T, StringBuilder) -> StringBuilder;
new(func, next: Renderer) super(next) { }
}
// Custom comparators to make using assertions extensible.
private class Comparator(next: Comparator) {
}
private class ComparatorOf<T> extends Comparator {
def func: (T, T) -> bool;
new(func, next: Comparator) super(next) { }
}

// Load a file that contains expected failures, one on each line
def loadExpectedFile(expected: Map<string, bool>, fileName: string) {
var data = System.fileLoad(fileName);
if (data == null) return;
var line = 0, pos = 0;
while (pos < data.length) {
if (data[pos] == '\n') {
var test = Arrays.range(data, line, pos);
if (UnitTests.trace) {
System.puts("ignore: ");
System.puts(test);
System.ln();
}
if (pos > line) expected[test] = true;
line = pos + 1;
}
pos++;
}
if (pos > line) {
var test = Arrays.range(data, line, pos);
if (UnitTests.trace) {
System.puts("ignore: ");
System.puts(test);
System.ln();
}
expected[test] = true;
line = pos + 1;
}
}

0 comments on commit a2af291

Please sign in to comment.