Oak

test.oak

← Standard library See on GitHub ↗

// libtest is a unit testing library for Oak

{
	default: default
	map: map
	each: each
	every: every
	filter: filter
} := import('std')
{
	split: split
	join: join
} := import('str')
{
	printf: printf
} := import('fmt')
debug := import('debug')

// new constructs a new test suite, named `title`
//
// Methods:
//
// fn eq(name, result, expect)      asserts that a test named `name` returned the
//                                  result `result`, and should expect `expect`.
// fn skip(name, result, expect)    ignores the result of this test for reporting
// fn reportFailed()                only reports to the console any failed tests
// fn report()                      reports the result of all tests to the console
// fn exit()                        exits the program with a non-zero exit code
//                                  if not all tests passed.
fn new(title) {
	Tests := []
	Skipped := []

	fn red(s) '' + s + ''
	fn green(s) '' + s + ''

	fn reportTests(tests) {
		tests |> each(fn(test) {
			{
				name: name
				passed?: p?
				result: result
				expect: expect
			} := test

			printf(
				'  {{ 0 }} {{ 1 }}'
				if p? {
					true -> green('✔')
					_ -> red('✘')
				}
				name
			)

			fn printIndentedDebug(x, indent) {
				debug.inspect(x) |>
					split('\n') |>
					map(fn(line, i) if i { 0 -> line, _ -> indent + line }) |>
					join('\n')
			}

			if !p? -> {
				printf('\texpected: {{ 0 }}', printIndentedDebug(expect, '\t          '))
				printf('\t  result: {{ 0 }}', printIndentedDebug(result, '\t          '))
			}
		})
	}

	fn reportAggregate {
		failedTests := Tests |> filter(fn(t) !t.passed?)
		if len(failedTests) {
			0 -> printf('All {{ 0 }} tests passed.', len(Tests))
			_ -> printf('{{ 0 }} / {{ 1 }} tests passed.', len(Tests) - len(failedTests), len(Tests))
		}
		if skipped := len(Skipped) {
			0 -> ?
			_ -> printf('{{ 0 }} tests skipped.', skipped)
		}
	}

	self := {
		eq: fn(name, result, expect) Tests << {
			name: name
			passed?: result = expect
			result: result
			expect: expect
		}
		approx: fn(name, result, expect, epsilon) {
			epsilon := epsilon |> default(0.00000001)
			fn similar?(a, b) if type(a) {
				:list -> a |> every(fn(_, i) similar?(a.(i), b.(i)))
				:object -> a |> keys() |> every(fn(k) similar?(a.(k), b.(k)))
				_ -> a > b - epsilon & a < b + epsilon
			}
			Tests << {
				name: name
				passed?: similar?(result, expect)
				result: result
				expect: expect
			}
		}
		assert: fn(name, result) self.eq(name, result, true)
		skip: fn(name, result, expect) Skipped << { name: name }
		reportFailed: fn {
			if failedTests := Tests |> filter(fn(t) !t.passed?) {
				[] -> ?
				_ -> {
					printf('Failed {{ 0 }} tests:', title)
					failedTests |> reportTests()
				}
			}
			reportAggregate()
		}
		report: fn {
			printf('{{ 0 }} tests:', title)
			Tests |> reportTests()
			reportAggregate()
		}
		exit: fn {
			exit(if Tests |> filter(fn(t) !t.passed?) {
				[] -> 0
				_ -> 1
			})
		}
	}
}