import args compile load progress strand term
fs:
- Path
glob:
- Glob
time:
- timeout
let err_style = term.style bold: true fg: :BRIGHT_RED:
let info_style = term.style fg: :YELLOW:
let pass_style = term.style fg: :GREEN:
let name_style = term.style dim: true
let note_style = term.style bold: true
let collect_key = strand.Key()
def parse_jobs value
let jobs = try
int $value
catch std.RuntimeError: _
throw args.Error "--jobs expects an integer"
if (jobs < 1)
throw args.Error "--jobs must be at least 1"
jobs
def parse_timeout value
let secs = try
float $value
catch std.RuntimeError: _
throw args.Error "--timeout expects a number"
if !(secs >= 0)
throw args.Error "--timeout must be non-negative"
secs
class ModuleCompileError: std.RuntimeError
pub field path output
def (init) self path output
self.path = path
self.output = output
def module_name path
let name = str $path.without_ext()
name.replace "\\" "/"
class Harness
field module_jobs jobs filter timeout
field errors = []
field success total = 0
def (init) self :jobs :module_jobs :filter :timeout
self.#module_jobs = strand.Resource $module_jobs
self.#jobs = strand.Resource $jobs
self.#timeout = timeout
self.#filter = try
Glob $filter
catch std.RuntimeError: _
throw args.Error "invalid --filter glob: $filter"
def matches_filter self path test
self.#filter.matches "$(module_name path)/$test"
def record_test self path name
self.#total = (self.#total + 1)
if !term.have_terminal
echo "run: $(module_name path)/$name"
def record_success self
self.#success = (self.#success + 1)
def with_timeout self func
if (self.#timeout != nil)
timeout $self.#timeout $func
else
func()
def record_error self err bt path test = nil
self.#errors.push [err, bt, path, test]
def run_test self path name func
self.#record_test $path $name
try
self.#with_timeout $func
self.#record_success()
catch std.RuntimeError: e
self.#record_error $e $(strand.error_backtrace()) $path $name
def run_module self path tests
progress.show message: (module_name path) total: $tests.len do |w|
strand.fork
for name func = tests
do self.#jobs.with do
progress.show message: $name do |_|
self.#run_test $path $name $func
w.delta()
def run_file self path
let source = path.read b
let compiled = compile.compile $path $source
let warnings = compiled.diagnostics.iter().any do |d| (d.severity == :WARNING:)
if (!compiled.ok || warnings)
throw ModuleCompileError $path $compiled
collect_key.value = []
self.#with_timeout do load.run $compiled.bytecode
let tests = collect_key.value
collect_key.value = nil
tests = [...tests.iter().filter(do |test| self.#matches_filter path test[0])]
self.#run_module $path $tests
def report self
let rc = 0
let skipped = 0
for e bt path test = self.#errors
let module = module_name $path
if (type e Skip)
if test
print (info_style "skipped") ": " (name_style "$module/$test")
if e.message
print " (" (note_style e.message) ")"
echo()
skipped = (skipped + 1)
else
print (info_style "skipped") ": " (name_style "$module")
if e.message
print " (" (note_style e.message) ")"
echo()
else if (type e ModuleCompileError)
rc = 1
print (err_style "failed") ": " (name_style "$module")
" (" (note_style "compilation failed") ")\n"
for diag = e.output.diagnostics
echo $diag.render().indent(2)
else
rc = 1
if test
print (err_style "failed") ": " (name_style "$module/$test") "\n"
else
print (err_style "failed") ": " (name_style "$module") "\n"
echo $ term.render_error(e, backtrace: bt).indent(2)
print (pass_style "passed") ": $(self.#success)/$(self.#total - skipped)"
if skipped
print " ($skipped " (info_style "skipped") ")"
echo()
rc
pub def run self files
progress.with do
progress.show message: modules total: $files.len do |w|
strand.fork
for path = files
do self.#module_jobs.with do
try
self.#run_file $path
catch std.RuntimeError: e
self.#record_error $e $(strand.error_backtrace()) $path
w.delta()
self.#report()
class Fixture
field wrap func
def (init) self wrap func
self.#wrap = wrap
self.#func = func
def (call) self
self.#wrap $self.#func
def (str) self
str $self.#func
# Defines a test fixture. `wrap` should be a function which accepts a block
# and invokes it with surrounding setup and teardown logic.
#
# # Returns
#
# A function suitable for use as a decorator on a test function.
#
# # Example
#
# ```
# #[fixture]
# def example_fixture block
# setup()
# try
# block()
# finally
# teardown()
#
# #[test]
# #[example_fixture]
# def example_test()
# do_something()
# ```
pub def fixture wrap
do |func| Fixture $wrap $func
# Register a test case that runs `func` with no arguments. `name`, if unspecified,
# defaults to the `str` coercion of `func`, which for `def`s is simply the function
# name. This function is suitable for use as a decorator:
#
# ```
# #[test]
# def the_test()
# do_something()
#
# #[test name: "alternate_name"]
# def overriden_name()
# do_something_else()
# ```
#
# # Returns
# If `func` is not specified, returns a function that takes the test case function instead
# (for use as a decorator). Otherwise, returns `func`
pub def test
:name = nil
func = nil
do
if (func == nil)
return do |func| test :name $func
collect_key.value.push [name || str(func), func]
func
pub class Assert: std.RuntimeError
pub field message = nil
def (init) self message
self.message = message
def (str) self
if (self.message != nil)
"assertion failed: $(self.message)"
else
"assertion failed"
# Exception thrown to skip a test case.
#
# Throw this (or call `skip()`) from a test function to mark it as skipped
# rather than failed.
pub class Skip: std.RuntimeError
# Skip message, or `nil` is none specified
pub field message = nil
def (init) self message = nil
self.message = message
# Skip the current test with an optional message.
pub def skip msg = nil
throw Skip $msg
# Assert that `cond` is truthy, throwing an error with optional `msg` on failure.
pub def assert cond msg = nil
if !cond
throw Assert $msg
# Assert that `cond` is falsy, throwing an error with optional `msg` on failure.
pub def assert_not cond msg = nil
if !cond
throw Assert $msg
# Assert that `left == right`, throwing an error with optional `msg` on failure.
pub def assert_eq left right msg = nil
if (left != right)
if msg
throw Assert "$(dbg left) == $(dbg right): $msg"
else
throw Assert "$(dbg left) == $(dbg right)"
# Assert that `left != right`, throwing an error with optional `msg` on failure.
pub def assert_ne left right msg = nil
if (left == right)
if msg
throw Assert "$(dbg left) != $(dbg right): $msg"
else
throw Assert "$(dbg left) != $(dbg right)"
# Assert that `block` throws an error of `type`, throwing an error with optional `msg`
# otherwise. The caught error is returned for further inspection if desired.
pub def assert_throws type :msg = nil block
try
block()
catch type: e
return e
if msg
throw Assert "$msg ($type thrown)"
else
throw Assert "$type thrown"
# Assert that `value` is of type `expected` (including a subtype), throwing an
# error with optional `msg` otherwise.
pub def assert_type expected value msg = nil
if !(type value expected)
if msg
throw Assert "$msg ($value @ $(type value) <: $expected)"
else
throw Assert "$value @ $(type value) <: $expected"
pub def main()
args.with
help: Run Do unit test modules.
- opt: jobs
short: j
type: $parse_jobs
default: 8
meta: N
help: Max concurrent tests (among all modules)
- opt: module-jobs
type: $parse_jobs
default: 4
meta: N
help: Max concurrent modules
- opt: filter
default: **
help: Glob filter for test names in `stem/test` form
- opt: timeout
type: $parse_timeout
default: 600
meta: SECS
help: Max seconds for each test and module load.
- arg: paths
type: $Path
collect: true
help: Test directories or files to run.
do |p|
let :filter :jobs :module_jobs :timeout :paths ... = p
let harness = Harness :jobs :module_jobs :filter :timeout
let files = []
for path = paths
if (path.metadata().type == :DIR:)
files.push ...path.glob("**/*.dol")
else
files.push $path
exit $ harness.run $files