Skip to content

Commit

Permalink
feat: Testing std lib (#129)
Browse files Browse the repository at this point in the history
  • Loading branch information
giann committed Oct 10, 2023
1 parent ac681fa commit d813297
Show file tree
Hide file tree
Showing 7 changed files with 371 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
279 changes: 279 additions & 0 deletions src/lib/test.buzz
Original file line number Diff line number Diff line change
@@ -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>],
[bool] asserts = [<bool>],
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 = [<bool>];
this.asserts = [<bool>];
this.elapsed = 0;
}

fun failedAsserts() > int {
return this.asserts.reduce::<int>(
fun (int _, bool success, int accumulator)
-> accumulator + if (success) 0 else 1,
initial: 0,
);
}

fun failedTests() > int {
return this.tests.reduce::<int>(
fun (int _, bool success, int accumulator)
-> accumulator + if (success) 0 else 1,
initial: 0,
);
}

fun succeededTests() > int {
return this.tests.reduce::<int>(
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>(T actual, T expected, str? message) > void {
this.assert(
actual == expected,
error: "expected `{expected}` got `{actual}`",
message: message
);
}

fun assertNotEqual::<T>(T actual, T expected, str? message) > void {
this.assert(
actual != expected,
error: "expected `{expected}` got `{actual}`",
message: message
);
}

fun assertAreEqual::<T>([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>([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::<T>(any value, str? message) > void {
this.assert(
!(value is T),
error: "`{value}` type is `{typeof value}`",
message: message
);
}

fun assertThrows::<T>(Function() > void !> T fn, str? message) > void {
try {
fn();
} catch (any error) {
this.assertOfType::<T>(error, message: message);
return;
}

this.assert(false, error: "Did not throw", message: message);
}

fun assertDoesNotThrow::<T>(Function() > void fn, str? message) > void {
try {
fn();
} catch (any error) {
if (error is T) {
this.assert(false, error: "Did throw", message: message);
return;
}
}
}
}
4 changes: 4 additions & 0 deletions src/obj.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 5 additions & 1 deletion src/value.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
Expand Down
12 changes: 12 additions & 0 deletions tests/046-try-catch.buzz
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Loading

0 comments on commit d813297

Please sign in to comment.