started work on testing library; very minimal implementation
authorBrendan Hansen <brendan.f.hansen@gmail.com>
Mon, 26 Sep 2022 16:40:27 +0000 (11:40 -0500)
committerBrendan Hansen <brendan.f.hansen@gmail.com>
Mon, 26 Sep 2022 16:40:27 +0000 (11:40 -0500)
core/math.onyx
core/std.onyx
core/string.onyx
core/test/testing.onyx [new file with mode: 0644]
scripts/core_tests.onyx [new file with mode: 0644]

index a0e46fd7a3bda5e4ce6b61c9146455586f31e99a..4460b5739382652889a2b00cf27705951a3cc8f1 100644 (file)
@@ -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");
+}
+
+}
index dbab9d7e705ba3dbe54e0fd4db7a55bf3199a03e..51eceef095da4f1fd6097de794fd3eb4b4cd7f08 100644 (file)
@@ -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"
index 0db20e7d7bf26b2a54bda37f0896f8f530de7706..578f32fa0267189b7a883c97d3e4a737ba3c1ee1 100644 (file)
@@ -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 (file)
index 0000000..6ee3e2b
--- /dev/null
@@ -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 (file)
index 0000000..5a09646
--- /dev/null
@@ -0,0 +1,12 @@
+
+
+#load "core/std"
+
+use core
+
+#inject
+core.Running_Tests :: true 
+
+main :: () {
+    test.run_tests();
+}