Skip to content

Commit

Permalink
release 0.4.0
Browse files Browse the repository at this point in the history
- made `(^Context) pass` and `(^Context) fail` return
  a boolean so they can be passed to `(^Context) endCustom`
- added new assertion function `(^Context) assert.sameType`
- library now exits with a fatal error if:
	- a custom assertion function does not end
	  all of its branches with `T.endCustom`
	- a previous assertion function failed and
	  its enclosing function did not return

implementation details:
- rearranged functions around the file into a much
  more sensible format to be read
  • Loading branch information
thacuber2a03 committed Sep 21, 2024
1 parent 69eb0dd commit 26dce9d
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 81 deletions.
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ the [Umka](https://github.com/vtereshkov/umka-lang) programming language.

- Pretty simple; less than 300 sloc, easy to read and modify/extend
- Carries timing information and keeps track of test results
- Can print testing information in a slick and straightforward format, or a verbose but insightful one
<!-- might erase this line -->
- Can print testing information in a slick and straightforward format, or a verbose but in-depth one

## Installation

Expand All @@ -20,7 +21,7 @@ Then, import it in your test file.
> This will install the `master` branch version of the box, which might not be stable.
Run `umbox install toast`.
You can also run `umbox init toast` to make a new box with Umka and toast preinstalled.
You can also run `umbox init -p toast` to make a new box with Umka and toast preinstalled.

## Usage

Expand Down Expand Up @@ -81,16 +82,15 @@ As of now, the result must be checked for a `false` value and the test returned
You can also make custom assertion functions by, either mixing and matching assertions,
or encoding your own logic with the help of the `fail` and `pass` functions.

For example, here's how an `assertEqualTypes` function could be written
using custom logic for some specific interface `T`:
For example, here's how `assert.sameType` function would be written
if it was a custom assertion function:

```go
fn assertEqualTypes(T: ^toast::Context, a, b: T): bool {
fn assertSameType(T: ^toast::Context, a, b: any): bool {
T.startCustom()

if !selftypeeq(a, b) {
T.fail("expected a and b to have the same type")
return T.endCustom(false)
return T.endCustom(T.fail("expected a and b to have the same type"))
}

return T.endCustom(true)
Expand All @@ -103,9 +103,9 @@ It can then be used as any other test function:
// ...

// this will pass
if !assertEqualTypes(T, 1, 2) { return }
if !assertSameType(T, 1, 2) { return }
// this will fail
if !assertEqualTypes(T, 1, "aeiou") { return }
if !assertSameType(T, 1, "aeiou") { return }

// ...
```
Expand Down
16 changes: 9 additions & 7 deletions test.um
Original file line number Diff line number Diff line change
Expand Up @@ -28,26 +28,28 @@ fn passOnExistentFile(T: ^toast::Context) {

fn assertEqual(T: ^toast::Context, a, b: int): bool {
T.startCustom()
return T.endCustom(T.assert.isTrue(a == b, "expected a and b to be equal"))
return T.endCustom(T.assert.isTrue(
a == b, "expected a and b to be equal"
))
}

fn customAssert(T: ^toast::Context) {
if !assertEqual(T, 1, 1) { return }
}

fn assertTypeEqual(T: ^toast::Context, a, b: any): bool {
fn assertSquare(T: ^toast::Context, n, s: int): bool {
T.startCustom()

if !selftypeeq(a, b) {
T.fail("expected a and b to have the same type")
return T.endCustom(false)
if n * n != s {
return T.endCustom(T.fail(sprintf("%v squared does not equal %v", n, s)))
}

return T.endCustom(true)
}

fn customLogic(T: ^toast::Context) {
if !assertTypeEqual(T, 1, 2) { return }
if !assertSquare(T, 3, 9 ) { return }
if !assertSquare(T, 3, 10) { return }
}

fn testToast(T: ^toast::Context) {
Expand All @@ -57,7 +59,7 @@ fn testToast(T: ^toast::Context) {
"fake file": false,
"real file": true,
"custom assertion": true,
"custom logic": true
"custom logic": false
}

U := toast::newContext()
Expand Down
181 changes: 116 additions & 65 deletions toast.um
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ TestFn* = fn (ctx: ^Context)
//~~type TestInfo
// Information about a test.
TestInfo* = struct {
name: str
func: TestFn
result: std::Err
time: real
name: str // Name of the test.
func: TestFn // The function attached to this test.
result: std::Err // The final result of this test.
time: real // The time it took for this test to pass *or* to fail.
// other unexported fields
//~~

funcDepth: uint
}
//~~

Assertions = struct { ctx: weak ^Context }

Expand All @@ -46,11 +48,12 @@ Context* = struct {
// Whether to use the compact or the verbose output.
compactOutput: bool

// The amount of custom assertions called so far.
customAsserts: uint
}
// other unexported fields...
//~~

customAsserts: []std::Err
}

)

fn eprintln(text: str = "") { fprintf(std::stderr(), "%s\n", text) }
Expand All @@ -77,24 +80,9 @@ fn newContext*(): ^Context {
return c
}

//~~fn (^Context) fail
// Marks this test as failed. You should immediately return once calling this.
fn (c: ^Context) fail*(msg: str, code: int = -1) {
//~~
t := c.current
std::assert(t != null, "attempt to call fail() outside of a test case")

s := c.bold("X")
if !c.compactOutput { s = sprintf("test '%s': %s", t.name, msg) }
eprint(c.red(s))

t.funcDepth++
t.result = std::error(code, msg)
}

//~~fn (^Context) pass
// Marks this test as passing. You should immediately return once calling this.
fn (c: ^Context) pass*(msg: str = "") {
fn (c: ^Context) pass*(msg: str = ""): bool {
//~~
t := c.current
std::assert(t != null, "attempt to call pass() outside of a test case")
Expand All @@ -105,79 +93,134 @@ fn (c: ^Context) pass*(msg: str = "") {

t.funcDepth++
t.result = std::error(0, msg)
return true
}

//~~ fn (^Context) assert.isTrue
// Asserts that `cond` is true. If the resulting `bool` is false, the caller should return immediately.
// If `msg` is not `""`, prints an extra reason alongside the error.
fn (a: ^Assertions) isTrue*(cond: bool, msg: str = ""): bool {
//~~fn (^Context) fail
// Marks this test as failed. You should immediately return once calling this.
fn (c: ^Context) fail*(msg: str, code: int = -1): bool {
//~~
c := a.ctx
t := c.current
std::assert(t != null, "attempt to call an assertion outside of a test case")
std::assert(t != null, "attempt to call fail() outside of a test case")

if t.result.code == 0 && !cond {
t.funcDepth++
s := "assertion failed!"
if msg != "" { s += sprintf("\nreason: '%s'", msg) }
c.fail(s)
return false
}
s := c.bold("X")
if !c.compactOutput { s = sprintf("test '%s': %s", t.name, msg) }
eprint(c.red(s))

return true
t.funcDepth++
t.result = std::error(code, msg)
return false
}

//~~ fn (^Context) assert.isFalse
// Asserts that `cond` is false. Everything else from `assert.isTrue` applies.
// (Currently, this is literally just a call to `assert.isTrue` with the condition inverted.)
fn (a: ^Assertions) isFalse*(cond: bool, msg: str = ""): bool {
//~~
return a.isTrue(!cond, msg)
fn (c: ^Context) checkInvariants() {
t := c.current
std::assert(t != null, "attempt to call an assertion outside of a test case")
std::assert(t.result.code == 0, "a previous assertion failed, but its enclosing function did not return")
}

//~~fn (^Context) startCustom
// Marks the start of a custom assertion.
// This must be called at the beginning of all custom assertions.
fn (c: ^Context) startCustom*() {
//~~
std::assert(c.current != null, "attempt to call startCustom outside of a test suite")
c.customAsserts++
c.current.funcDepth += 2 // one more for this very function
c.checkInvariants()
t := c.current
c.customAsserts = append(c.customAsserts, std::error(-1))
t.funcDepth += 2 // one more for this very function
}

//~~fn (^Context) endCustom
// Marks the end of the custom assertion.
// The intended return boolean must be passed through this
// function and that must be returned instead:
// The intended return boolean must be passed through this function
// and that value must be returned instead:
// `return T.endCustom(returnVal)`.
fn (c: ^Context) endCustom*(res: bool): bool {
//~~
std::assert(
c.current != null && c.customAsserts != 0,
c.current != null && len(c.customAsserts) != 0,
"attempt to call endCustom outside a custom assertion"
)

c.customAsserts--
c.customAsserts = delete(c.customAsserts, len(c.customAsserts)-1)
c.current.funcDepth--
// returning from this function decrements once more
return res
}

fn (a: ^Assertions) startAssertion(): (weak ^Context, weak ^TestInfo) {
c := a.ctx
c.checkInvariants()
return c, c.current
}

fn (a: ^Assertions) failAssertion(err: str): bool {
c := a.ctx
t := c.current

t.funcDepth += 2
c.fail(err)
return false
}

//~~ fn (^Context) assert.isTrue
// Asserts that `cond` is true.
// If the resulting `bool` is false, the caller should return immediately.
// If `msg` is not `""`, prints an extra reason alongside the error.
fn (a: ^Assertions) isTrue*(cond: bool, msg: str = ""): bool {
//~~
a.startAssertion()

if !cond {
s := "assertion failed!"
if msg != "" { s += sprintf("\nreason: '%s'", msg) }
return a.failAssertion(s)
}

return true
}

//~~ fn (^Context) assert.isFalse
// Asserts that `cond` is false. Everything else from `assert.isTrue` applies.
// (Currently, this is literally just a call to `assert.isTrue` with the condition inverted.)
fn (a: ^Assertions) isFalse*(cond: bool, msg: str = ""): bool {
//~~
a.ctx.current.funcDepth++
return a.isTrue(!cond, msg)
}

//~~fn (^Context) assert.isOk
// Asserts that `e`'s code is 0. If the resulting `bool` is false, the caller should return immediately.
// If `msg` is not `""`, prints an extra reason alongside the error. If it is, it defaults to `e`'s error message.
// Asserts that `e`'s code is 0.
// If the resulting `bool` is false, the caller should return immediately.
//
// If `msg` is not `""`, prints an extra reason alongside the error.
// If it is, it defaults to `e`'s error message.
fn (a: ^Assertions) isOk*(e: std::Err, msg: str = ""): bool {
//~~
c := a.ctx
t := c.current
std::assert(t != null, "attempt to call an assertion outside of a test case")
a.startAssertion()

if t.result.code == 0 && e.code != 0 {
t.funcDepth++
if e.code != 0 {
s := sprintf("error code is not std::StdErr.ok (%i)", e.code)
s += sprintf("\nreason: '%s'", msg != "" ? msg : e.msg)
c.fail(s, e.code)
return false
m := msg
if m == "" { m = e.msg }
if m != "" { s += sprintf("\nreason: '%s'", m) }
return a.failAssertion(s)
}

return true
}

//~~ fn (^Context) assert.sameType
// Asserts that `a` and `b` have the same type.
// If the resulting `bool` is false, the caller should return immediately.
// If `msg` is not `""`, prints an extra reason alongside the error.
fn (a: ^Assertions) sameType(va, vb: any, msg: str = ""): bool {
//~~
a.startAssertion()

if !selftypeeq(va, vb) {
s := sprintf("expected %v and %v to be compatible", va, vb)
if msg != "" { s += sprintf("\nreason: '%s'", msg)}
return a.failAssertion(s)
}

return true
Expand Down Expand Up @@ -211,9 +254,11 @@ fn (c: ^Context) registerTests*(tests: []TestInfo) {
}

//~~fn (^Context) run
// Runs each of the tests registered to this context, and returns whether any single one of them had an error.
// Runs each of the tests registered to this context,
// and returns whether any single one of them had an error.
//
// `quitIfErr` exits the application after every test is run, if any single one of them has any errors.
// `quitIfErr` exits the application after every test is run,
// if any single one of them threw an error.
fn (c: ^Context) run*(quitIfErr: bool = true): bool {
//~~
didFail := false
Expand All @@ -228,11 +273,17 @@ fn (c: ^Context) run*(quitIfErr: bool = true): bool {
t.func(c)
t.time = std::clock() - time

if len(c.customAsserts) != 0 {
loc := c.customAsserts[len(c.customAsserts)-1].trace[1]
eprintln(c.red("\n\n[FATAL] you missed a call to T.endCustom"))
eprint(sprintf("in function %s, line %i\nin file %s", loc.func, loc.line, loc.file))
}

if t.result.code != 0 {
didFail = true
if !c.compactOutput {
pos := t.result.trace[t.funcDepth]
eprint(sprintf("\nat file %s\nline %i", pos.file, pos.line))
eprint(sprintf("\nin file %s\nline %i", pos.file, pos.line))
}
} else {
if t.result.msg == "" { c.pass("passed") }
Expand All @@ -259,7 +310,7 @@ fn (c: ^Context) run*(quitIfErr: bool = true): bool {
pos := t.result.trace[t.funcDepth]

eprintln(sprintf(
"%s\nat file %s\nline %i, took %fms\n",
"%s\nin file %s\nline %i, took %fms\n",
c.red(msg), pos.file, pos.line, t.time * 1000
))
}
Expand Down

0 comments on commit 26dce9d

Please sign in to comment.