From d813297a8610deb8c555730823fb7c43aed18d98 Mon Sep 17 00:00:00 2001 From: Benoit Giannangeli Date: Sun, 8 Oct 2023 01:02:30 +0200 Subject: [PATCH] feat: Testing std lib (#129) --- CHANGELOG.md | 1 + build.zig | 1 + src/lib/test.buzz | 279 +++++++++++++++++++++++++++++++++++++++ src/obj.zig | 4 + src/value.zig | 6 +- tests/046-try-catch.buzz | 12 ++ tests/068-testing.buzz | 69 ++++++++++ 7 files changed, 371 insertions(+), 1 deletion(-) create mode 100644 src/lib/test.buzz create mode 100644 tests/068-testing.buzz diff --git a/CHANGELOG.md b/CHANGELOG.md index 32d5d00a..23c7c30c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ - Number literals can embed `_`: `1_000_000.300_245` (https://github.com/buzz-language/buzz/issues/163) - Type can be inferred when declaring a variable/constant with the `var` or `const` keyword: `var something = "hello"` (https://github.com/buzz-language/buzz/issues/194) - Objects can have generic types (https://github.com/buzz-language/buzz/issues/82) +- Draft of the testing std lib (https://github.com/buzz-language/buzz/issues/129) ## Changed diff --git a/build.zig b/build.zig index 399e2353..05d7272e 100644 --- a/build.zig +++ b/build.zig @@ -398,6 +398,7 @@ pub fn build(b: *Build) !void { "errors", "ffi", "serialize", + "test", }; // TODO: this section is slow. Modifying Buzz parser shouldn't trigger recompile of all buzz dynamic libraries diff --git a/src/lib/test.buzz b/src/lib/test.buzz new file mode 100644 index 00000000..66bbed5f --- /dev/null +++ b/src/lib/test.buzz @@ -0,0 +1,279 @@ +import "std"; +import "os"; +import "io"; + +export enum(int) Color { + | attributes + reset = 0, + bright = 1, + dim = 2, + underscore = 4, + blink = 5, + reverse = 7, + hidden = 8, + + | foreground + black = 30, + red = 31, + green = 32, + yellow = 33, + blue = 34, + magenta = 35, + cyan = 36, + white = 37, + + | background + onblack = 40, + onred = 41, + ongreen = 42, + onyellow = 43, + onblue = 44, + onmagenta = 45, + oncyan = 46, + onwhite = 47, +} + +export fun color(str text, Color color, bool reset = true) > str { + return "\27[{color.value}m{text}{if (reset) "\27[0m" else ""}"; +} + +export fun bright(str text) -> color(text, color: Color.bright); +export fun dim(str text) -> color(text, color: Color.dim); +export fun underscore(str text) -> color(text, color: Color.underscore); +export fun blink(str text) -> color(text, color: Color.blink); +export fun reverse(str text) -> color(text, color: Color.reverse); +export fun hidden(str text) -> color(text, color: Color.hidden); +export fun black(str text) -> color(text, color: Color.black); +export fun red(str text) -> color(text, color: Color.red); +export fun green(str text) -> color(text, color: Color.green); +export fun yellow(str text) -> color(text, color: Color.yellow); +export fun blue(str text) -> color(text, color: Color.blue); +export fun magenta(str text) -> color(text, color: Color.magenta); +export fun cyan(str text) -> color(text, color: Color.cyan); +export fun white(str text) -> color(text, color: Color.white); +export fun onblack(str text) -> color(text, color: Color.onblack); +export fun onred(str text) -> color(text, color: Color.onred); +export fun ongreen(str text) -> color(text, color: Color.ongreen); +export fun onyellow(str text) -> color(text, color: Color.onyellow); +export fun onblue(str text) -> color(text, color: Color.onblue); +export fun onmagenta(str text) -> color(text, color: Color.onmagenta); +export fun oncyan(str text) -> color(text, color: Color.oncyan); +export fun onwhite(str text) -> color(text, color: Color.onwhite); + +export object Tester { + [bool] tests = [], + [bool] asserts = [], + float elapsed = 0.0, + Function(Tester t) > void? beforeAll, + Function(Tester t) > void? beforeEach, + Function(Tester t) > void? afterAll, + Function(Tester t) > void? afterEach, + + static fun init( + Function(Tester t) > void? beforeAll, + Function(Tester t) > void? beforeEach, + Function(Tester t) > void? afterAll, + Function(Tester t) > void? afterEach + ) > Tester { + var t = Tester{ + beforeAll = beforeAll, + beforeEach = beforeEach, + afterAll = afterAll, + afterEach = afterEach, + }; + + if (t.beforeAll -> beforeAll) { + beforeAll(t); + } + + return t; + } + + fun reset() > void { + this.tests = []; + this.asserts = []; + this.elapsed = 0; + } + + fun failedAsserts() > int { + return this.asserts.reduce::( + fun (int _, bool success, int accumulator) + -> accumulator + if (success) 0 else 1, + initial: 0, + ); + } + + fun failedTests() > int { + return this.tests.reduce::( + fun (int _, bool success, int accumulator) + -> accumulator + if (success) 0 else 1, + initial: 0, + ); + } + + fun succeededTests() > int { + return this.tests.reduce::( + fun (int _, bool success, int accumulator) + -> accumulator + if (success) 1 else 0, + initial: 0, + ); + } + + fun it(str message, Function() > void fn) > void { + float startTime = time(); + + stdout.write(yellow("▶ Test: {message}\n")) catch void; + + if (this.beforeEach -> beforeEach) { + beforeEach(this); + } + + int previousFailCount = this.failedAsserts(); + fn(); + + if (this.afterEach -> afterEach) { + afterEach(this); + } + + this.tests.append(previousFailCount == this.failedAsserts()); + + this.elapsed = this.elapsed + (time() - startTime); + } + + fun summary() > void { + if (this.afterAll -> afterAll) { + afterAll(this); + } + + const failed = this.failedTests(); + + stdout.write("\n") catch void; + + foreach (bool testStatus in this.tests) { + if (testStatus) { + stdout.write(green("●")) catch void; + } else { + stdout.write(yellow("●")) catch void; + } + } + + stdout.write( + green("\n{this.succeededTests()}") + + dim(" successes, ") + + yellow("{failed}") + + dim(" failures in ") + + "{this.elapsed / 1000.0}" + + dim(" seconds\n") + ) catch void; + + if (failed > 0) { + exit(1); + } + } + + fun report(str? error, str? message) > void { + stderr.write(red(" Assert failed: {message ?? ""}") + dim("\n {error}\n")) catch void; + } + + fun assert(bool condition, str? error, str? message) > void { + if (!condition) { + this.report(error, message: message); + + this.asserts.append(false); + } else { + this.asserts.append(true); + } + } + + fun assertEqual::(T actual, T expected, str? message) > void { + this.assert( + actual == expected, + error: "expected `{expected}` got `{actual}`", + message: message + ); + } + + fun assertNotEqual::(T actual, T expected, str? message) > void { + this.assert( + actual != expected, + error: "expected `{expected}` got `{actual}`", + message: message + ); + } + + fun assertAreEqual::([T] values, str? message) > void { + if (values.len() < 2) { + return; + } + + bool equal = true; + T previous = values[0]; + foreach (T value in values) { + if (value != previous) { + equal = false; + break; + } + + previous = value; + } + + this.assert( + equal, + error: "one element is not equal", + message: message + ); + } + + fun assertAreNotEqual::([T] values, str? message) > void { + if (values.len() < 2) { + return; + } + + bool equal = true; + T previous = values[0]; + foreach (int i, T value in values) { + if (i > 0 and value == previous) { + equal = false; + break; + } + + previous = value; + } + + this.assert( + equal, + error: "one element is equal", + message: message + ); + } + + fun assertOfType::(any value, str? message) > void { + this.assert( + !(value is T), + error: "`{value}` type is `{typeof value}`", + message: message + ); + } + + fun assertThrows::(Function() > void !> T fn, str? message) > void { + try { + fn(); + } catch (any error) { + this.assertOfType::(error, message: message); + return; + } + + this.assert(false, error: "Did not throw", message: message); + } + + fun assertDoesNotThrow::(Function() > void fn, str? message) > void { + try { + fn(); + } catch (any error) { + if (error is T) { + this.assert(false, error: "Did throw", message: message); + return; + } + } + } +} \ No newline at end of file diff --git a/src/obj.zig b/src/obj.zig index 73c37531..c10a1679 100644 --- a/src/obj.zig +++ b/src/obj.zig @@ -338,6 +338,10 @@ pub const Obj = struct { } pub fn is(self: *Self, type_def: *ObjTypeDef) bool { + if (type_def.def_type == .Any) { + return true; + } + return switch (self.obj_type) { .String => type_def.def_type == .String, .Pattern => type_def.def_type == .Pattern, diff --git a/src/value.zig b/src/value.zig index a442de25..ede2bb82 100644 --- a/src/value.zig +++ b/src/value.zig @@ -80,7 +80,7 @@ pub const Value = packed struct { } pub inline fn isFloat(self: Value) bool { - return self.val & TaggedValueMask != TaggedValueMask; + return !self.isBool() and !self.isError() and !self.isInteger() and !self.isNull() and !self.isObj() and !self.isVoid(); } pub inline fn isNumber(self: Value) bool { @@ -255,6 +255,10 @@ pub fn valueEql(a: Value, b: Value) bool { pub fn valueIs(type_def_val: Value, value: Value) bool { const type_def: *ObjTypeDef = ObjTypeDef.cast(type_def_val.obj()).?; + if (type_def.def_type == .Any) { + return true; + } + if (value.isObj()) { return value.obj().is(type_def); } diff --git a/tests/046-try-catch.buzz b/tests/046-try-catch.buzz index 1c6e0519..750a362e 100644 --- a/tests/046-try-catch.buzz +++ b/tests/046-try-catch.buzz @@ -56,4 +56,16 @@ test "Try catch" { assert(returnFromCatch() == 21, message: "return from catch clause works"); assert(afterLocal == "bye", message: "catch closed its scope"); +} + +test "catch any catches everything" { + var caught = false; + try { + willFail(); + } catch (any error) { + caught = true; + assert(error is str, message: "Could cath any error"); + } + + assert(caught, message: "Could catch any error"); } \ No newline at end of file diff --git a/tests/068-testing.buzz b/tests/068-testing.buzz new file mode 100644 index 00000000..e5b4e0e7 --- /dev/null +++ b/tests/068-testing.buzz @@ -0,0 +1,69 @@ +import "std"; +import "test"; +import "os"; + +test "Test std lib" { + Tester t = Tester.init( + beforeAll: fun (Tester t) { + t.assert(true); + }, + afterAll: fun (Tester t) { + t.assert(true); + }, + beforeEach: fun (Tester t) { + t.assert(true); + }, + afterEach: fun (Tester t) { + t.assert(true); + } + ); + + t.it( + "Should be an integer equal to 12", + fn: fun () { + var value = 12; + + t.assertOfType::(value); + + t.assertEqual::(value, expected: 12, message: "Yeah!"); + } + ); + + t.it( + "Should compare list elements", + fn: fun () { + t.assertAreNotEqual::([1, 2, 3], message: "Testing failure"); + } + ); + + t.it( + "Should compare list elements", + fn: fun () { + t.assertAreEqual::([1, 1, 1], message: "List of 1s"); + } + ); + + t.it( + "Should throw", + fn: fun () { + t.assertThrows::( + fun () !> str { + throw "Failing!"; + }, + message: "Should fail with str error", + ); + }, + ); + + t.it( + "Should not throw", + fn: fun () { + t.assertDoesNotThrow::( + fun () {}, + message: "Should fail with str error", + ); + }, + ); + + t.summary(); +} \ No newline at end of file