From 3a5c37e9a4c41980e4bc920d81bdcb6a99d0de88 Mon Sep 17 00:00:00 2001 From: Brendan Hansen Date: Mon, 26 Sep 2022 11:40:27 -0500 Subject: [PATCH] started work on testing library; very minimal implementation --- core/math.onyx | 20 ++++++ core/std.onyx | 2 + core/string.onyx | 2 +- core/test/testing.onyx | 143 ++++++++++++++++++++++++++++++++++++++++ scripts/core_tests.onyx | 12 ++++ 5 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 core/test/testing.onyx create mode 100644 scripts/core_tests.onyx diff --git a/core/math.onyx b/core/math.onyx index a0e46fd7..4460b573 100644 --- a/core/math.onyx +++ b/core/math.onyx @@ -339,3 +339,23 @@ gcd :: (a: $T, b: T) -> T { lcm :: (a: $T, b: T) -> T { return (a * b) / gcd(a, b); } + + + +// +// Tests +// + +#if #defined(core.Running_Tests) { + +use core {test} + +@test.test.{"GCD works"} +(t: ^test.T) { + t->assert(gcd(35, 18) == 1, "gcd(35, 18) == 1"); + t->assert(gcd(35, 15) == 5, "gcd(35, 15) == 5"); + t->assert(gcd(35, 21) == 7, "gcd(35, 21) == 7"); + t->assert(gcd(35, 70) == 35, "gcd(35, 70) == 35"); +} + +} diff --git a/core/std.onyx b/core/std.onyx index dbab9d7e..51eceef0 100644 --- a/core/std.onyx +++ b/core/std.onyx @@ -37,6 +37,8 @@ package core #load "./runtime/default_link_options" #load "./arg_parse" +#load "./test/testing" + #local runtime :: package runtime #if runtime.runtime == .Wasi || runtime.runtime == .Onyx { #load "./os/file" diff --git a/core/string.onyx b/core/string.onyx index 0db20e7d..578f32fa 100644 --- a/core/string.onyx +++ b/core/string.onyx @@ -197,7 +197,7 @@ ends_with :: (s: str, suffix: str) -> bool { return true; } -is_empty :: macro (s: str) => s.count == 0 || s.data == null; +is_empty :: (s: str) => s.count == 0 || s.data == null; index_of :: (s: str, c: u8) -> i32 { for s.count { diff --git a/core/test/testing.onyx b/core/test/testing.onyx new file mode 100644 index 00000000..6ee3e2b6 --- /dev/null +++ b/core/test/testing.onyx @@ -0,0 +1,143 @@ +package core.test + +use core {array, string, printf} + +#doc """ + Test tag. Use this to mark a function as a test. + + You can either use just the type name: + + @core.test.test + (t: ^core.test.T) { + } + + Or you can specify a name using the full struct literal: + + @core.test.test.{"Important test name"} + (t: ^core.test.T) { + } +""" +test :: struct { + name: str; +} + + +#doc "Testing context" +T :: struct { + current_test_case: ^Test_Case; +} + +#inject +T.assert :: (t: ^T, cond: bool, name := "", site := #callsite) { + t.current_test_case.assertions << .{ + name, cond, site + }; + + if !cond { + t.current_test_case.passed = false; + } +} + + + +#doc """ + Runs all test cases in the provide packages. + If no packages are provided, ALL package tests are run. +""" +run_tests :: (packages: [] package_id = .[], log := true) -> bool { + ctx: T; + + cases := gather_test_cases(packages); + defer delete(^cases); + + failed_cases := make([..] ^Test_Case); + + if log do printf("Running {} test cases...\n", cases.count); + for ^ cases { + ctx.current_test_case = it; + + // Assume the test case passed until it didn't. + it.passed = true; + + it.runner(^ctx); + + if !it.passed { + failed_cases << it; + } + } + + if log { + printf("Results: {} / {} test cases passed.\n", + cases.count - failed_cases.length, cases.count); + + if failed_cases.count > 0 do printf("Failing test cases:\n"); + + for failed_cases { + printf("[{}]\n", it.name if !string.is_empty(it.name) else "Unnamed test"); + + for ^ it.assertions { + if it.passed do continue; + + printf(" {} at {}:{}\n", + it.name if !string.is_empty(it.name) else "Unnamed assertion", + it.loc.file, it.loc.line); + } + } + } + + return failed_cases.count == 0; +} + + + + +// +// Private interface +// + +#local +runner_proc :: #type (^T) -> void; + +#local +Test_Case :: struct { + name: str; + runner: (^T) -> void; + + passed := false; + assertions: [..] Assertion = make([..] Assertion); + + Assertion :: struct { + name: str; + passed: bool; + loc: CallSite; + } +} + +#local +gather_test_cases :: (packages: [] package_id) -> [] Test_Case { + result := make([..] Test_Case); + + procs1 := runtime.info.get_procedures_with_tag(test); + defer delete(^procs1); + + for procs1 { + if packages.count == 0 || array.contains(packages, it.pack) { + result << .{it.tag.name, *cast(^runner_proc) ^it.func}; + } + } + + + procs2 := runtime.info.get_procedures_with_tag(type_expr); + defer delete(^procs2); + + for procs2 { + if packages.count != 0 && !array.contains(packages, it.pack) do continue; + + if *it.tag == test { + tag := cast(^test) it.tag; + result << .{"", *cast(^runner_proc) ^it.func}; + } + } + + return result; +} diff --git a/scripts/core_tests.onyx b/scripts/core_tests.onyx new file mode 100644 index 00000000..5a096469 --- /dev/null +++ b/scripts/core_tests.onyx @@ -0,0 +1,12 @@ + + +#load "core/std" + +use core + +#inject +core.Running_Tests :: true + +main :: () { + test.run_tests(); +} -- 2.25.1