Skip to content

Instantly share code, notes, and snippets.

@laytan
Last active December 19, 2023 00:00
Show Gist options
  • Save laytan/c6e6b462403884441befb6953273d5ec to your computer and use it in GitHub Desktop.
Save laytan/c6e6b462403884441befb6953273d5ec to your computer and use it in GitHub Desktop.
Odin test runner
package generated_tests
import test_gen "core:testing/generator"
import "core:os"
import "core:testing"
import test_core_crypto "../../../tests/core/crypto"
main :: proc() {
tests := []testing.Internal_Test{
{ "test_core_crypto", "test_md5", test_core_crypto.test_md5 },
{ "test_core_crypto", "test_sha1", test_core_crypto.test_sha1 },
{ "test_core_crypto", "test_sha224", test_core_crypto.test_sha224 },
{ "test_core_crypto", "test_sha256", test_core_crypto.test_sha256 },
{ "test_core_crypto", "test_sha384", test_core_crypto.test_sha384 },
{ "test_core_crypto", "test_sha512", test_core_crypto.test_sha512 },
{ "test_core_crypto", "test_sha512_256", test_core_crypto.test_sha512_256 },
{ "test_core_crypto", "test_sha3_224", test_core_crypto.test_sha3_224 },
{ "test_core_crypto", "test_sha3_256", test_core_crypto.test_sha3_256 },
{ "test_core_crypto", "test_sha3_384", test_core_crypto.test_sha3_384 },
{ "test_core_crypto", "test_sha3_512", test_core_crypto.test_sha3_512 },
{ "test_core_crypto", "test_shake_128", test_core_crypto.test_shake_128 },
{ "test_core_crypto", "test_shake_256", test_core_crypto.test_shake_256 },
{ "test_core_crypto", "test_keccak_224", test_core_crypto.test_keccak_224 },
{ "test_core_crypto", "test_keccak_256", test_core_crypto.test_keccak_256 },
{ "test_core_crypto", "test_keccak_384", test_core_crypto.test_keccak_384 },
{ "test_core_crypto", "test_keccak_512", test_core_crypto.test_keccak_512 },
{ "test_core_crypto", "test_blake2b", test_core_crypto.test_blake2b },
{ "test_core_crypto", "test_blake2s", test_core_crypto.test_blake2s },
{ "test_core_crypto", "test_sm3", test_core_crypto.test_sm3 },
{ "test_core_crypto", "test_siphash_2_4", test_core_crypto.test_siphash_2_4 },
{ "test_core_crypto", "test_chacha20", test_core_crypto.test_chacha20 },
{ "test_core_crypto", "test_poly1305", test_core_crypto.test_poly1305 },
{ "test_core_crypto", "test_chacha20poly1305", test_core_crypto.test_chacha20poly1305 },
{ "test_core_crypto", "test_x25519", test_core_crypto.test_x25519 },
{ "test_core_crypto", "test_rand_bytes", test_core_crypto.test_rand_bytes },
{ "test_core_crypto", "bench_modern", test_core_crypto.bench_modern },
}
if testing.runner(tests) {
os.exit(int(test_gen.Exit_Codes.Success))
} else {
os.exit(int(test_gen.Exit_Codes.Failure))
}
}
package test_generator
import "core:c/libc"
import "core:fmt"
import "core:odin/ast"
import "core:odin/parser"
import "core:os"
import "core:path/filepath"
import "core:strings"
Exit_Codes :: enum {
Success,
Failure,
Generation_Error,
Spawn_Error,
Run_Error,
Run_Unknown_Exit,
}
Test_Proc :: struct {
pkg: string,
name: string,
}
Test_Visitor :: struct {
pkg: ^ast.Package,
procs: [dynamic]Test_Proc,
pkgs_seen: map[string]string,
}
when ODIN_OS == .Darwin {
// #define _W_INT(w) (*(int *)&(w)) /* convert union wait to int */
// #define _WSTATUS(x) (_W_INT(x) & 0177)
// #define WIFEXITED(x) (_WSTATUS(x) == 0)
// #define WEXITSTATUS(x) ((_W_INT(x) >> 8) & 0x000000ff)
_WSTATUS :: proc(x: i32) -> i32 { return x & 0177 }
WIFEXITED :: proc(x: i32) -> bool { return _WSTATUS(x) == 0 }
WEXITSTATUS :: proc(x: i32) -> i32 { return (x >> 8) & 0x000000ff }
} else {
WIFEXITED :: proc(x: i32) -> bool{ return true }
@(warning="WEXITSTATUS implementation missing for platform, test exit code will not reflect actual status.")
WEXITSTATUS :: proc(x: i32) -> i32 { return i32(Exit_Codes.Run_Unknown_Exit) }
}
main :: proc() {
if len(os.args) < 2 {
error("`odin test` takes a package as its first argument.")
}
package_path := os.args[1]
OUT_PATH :: ODIN_ROOT + "/core/testing/generated/generated.odin"
OUT_DIR :: ODIN_ROOT + "/core/testing/generated"
RUN :: "odin run " + ODIN_ROOT + "/core/testing/generated"
tv: Test_Visitor
collect_tests(&tv, package_path)
ws :: strings.write_string
buf := strings.builder_make()
ws(&buf, "package generated_tests\n\n")
ws(&buf, "import test_gen \"core:testing/generator\"\n")
ws(&buf, "import \"core:os\"\n")
ws(&buf, "import \"core:testing\"\n\n")
for fp, pkg_name in tv.pkgs_seen {
rp, err := filepath.rel(OUT_DIR, fp)
if err != nil {
error("Could not create relative path between %q and %q: %v.", OUT_DIR, fp, err)
}
ws(&buf, "import ")
ws(&buf, pkg_name)
ws(&buf, " \"")
ws(&buf, rp)
ws(&buf, "\"\n")
}
ws(&buf, "\n")
ws(&buf, "main :: proc() {\n")
ws(&buf, "\ttests := []testing.Internal_Test{\n")
for p in tv.procs {
ws(&buf, "\t\t{ \"")
ws(&buf, p.pkg)
ws(&buf, "\", \"")
ws(&buf, p.name)
ws(&buf, "\", ")
ws(&buf, p.pkg)
ws(&buf, ".")
ws(&buf, p.name)
ws(&buf, " },\n")
}
ws(&buf, "\t}\n\n")
ws(&buf, "\tif testing.runner(tests) {\n")
ws(&buf, "\t\tos.exit(int(test_gen.Exit_Codes.Success))\n")
ws(&buf, "\t} else {\n")
ws(&buf, "\t\tos.exit(int(test_gen.Exit_Codes.Failure))\n")
ws(&buf, "\t}\n")
ws(&buf, "}\n")
if errno := os.make_directory(OUT_DIR); errno != 0 && errno != os.EEXIST {
error("Could not make generated tests directory at %q, error code: %v.", OUT_DIR, errno)
}
if !os.write_entire_file(OUT_PATH, buf.buf[:]) {
error("Could not write tests to be ran to %q.", OUT_PATH)
}
res := libc.system(RUN)
switch {
case res == -1:
error("Spawning child process failed, error code: %v.", os.get_last_error(), exit_code=.Spawn_Error)
case WIFEXITED(res):
os.exit(int(WEXITSTATUS(res)))
case:
error("Unknown error during testing child process.", exit_code=.Run_Error)
}
}
collect_tests :: proc(tv: ^Test_Visitor, start_pkg: string) {
parse_ok: bool
tv.pkg, parse_ok = parser.parse_package_from_path(start_pkg)
if !parse_ok {
error("Could not parse package %q.", start_pkg)
}
tv.pkgs_seen[tv.pkg.fullpath] = tv.pkg.name
v := ast.Visitor{
visit = test_visitor,
data = tv,
}
ast.walk(&v, tv.pkg)
}
test_visitor :: proc(v: ^ast.Visitor, node: ^ast.Node) -> ^ast.Visitor {
if node == nil {
return nil
}
tv := (^Test_Visitor)(v.data)
#partial switch n in node.derived {
case ^ast.Package, ^ast.File: return v
case ^ast.Import_Decl:
import_path := n.fullpath[1:len(n.fullpath)-1]
// TODO: support collections.
if strings.contains(import_path, ":") {
break
}
import_path = filepath.join({tv.pkg.fullpath, import_path})
if import_path in tv.pkgs_seen {
break
}
tv.pkgs_seen[import_path] = tv.pkg.name
prev := tv.pkg
defer tv.pkg = prev
parse_ok: bool
tv.pkg, parse_ok = parser.parse_package_from_path(import_path)
if !parse_ok {
error("parsing package %s for tests failed.", n.fullpath)
}
ast.walk(v, tv.pkg)
case ^ast.Value_Decl:
is_test: bool
attributes_loop: for attr in n.attributes {
for elem in attr.elems {
ident, is_ident := elem.derived.(^ast.Ident)
if !is_ident {
continue
}
if ident.name == "test" {
is_test = true
break attributes_loop
}
}
}
if !is_test {
break
}
for name in n.names {
ident, is_ident := name.derived.(^ast.Ident)
if !is_ident {
continue
}
append(&tv.procs, Test_Proc{
pkg = tv.pkg.name,
name = ident.name,
})
}
}
return nil
}
error :: proc(msg: string, args: ..any, exit_code := Exit_Codes.Generation_Error) -> ! {
fmt.eprint("ERROR: ")
fmt.eprintf(msg, ..args)
fmt.eprintln()
os.exit(int(exit_code))
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment