diff --git a/core/testing/runner.odin b/core/testing/runner.odin new file mode 100644 index 000000000..35f7afde1 --- /dev/null +++ b/core/testing/runner.odin @@ -0,0 +1,60 @@ +//+private +package testing + +import "core:io" +import "core:os" +import "core:strings" + +reset_t :: proc(t: ^T) { + clear(&t.cleanups); + t.error_count = 0; +} +end_t :: proc(t: ^T) { + for i := len(t.cleanups)-1; i >= 0; i -= 1 { + c := t.cleanups[i]; + c.procedure(c.user_data); + } +} + +runner :: proc(internal_tests: []Internal_Test) -> bool { + stream := os.stream_from_handle(os.stdout); + w, _ := io.to_writer(stream); + + t := &T{}; + t.w = w; + reserve(&t.cleanups, 1024); + defer delete(t.cleanups); + + total_success_count := 0; + total_test_count := len(internal_tests); + + for it in internal_tests { + if it.p == nil { + total_test_count -= 1; + continue; + } + + free_all(context.temp_allocator); + reset_t(t); + defer end_t(t); + + name := strings.trim_prefix(it.name, "test_"); + + logf(t, "[Test: %q]", name); + + // TODO(bill): Catch panics + { + it.p(t); + } + + if t.error_count != 0 { + logf(t, "[%q : FAILURE]", name); + } else { + logf(t, "[%q : SUCCESS]", name); + total_success_count += 1; + } + } + logf(t, "----------------------------------------"); + logf(t, "%d/%d SUCCESSFUL", total_success_count, total_test_count); + return total_success_count == total_test_count; +} diff --git a/core/testing/testing.odin b/core/testing/testing.odin new file mode 100644 index 000000000..d9d4a53a3 --- /dev/null +++ b/core/testing/testing.odin @@ -0,0 +1,68 @@ +package testing + +import "core:fmt" +import "core:io" + +Test_Signature :: proc(^T); + +Internal_Test :: struct { + name: string, + p: Test_Signature, +} + + +Internal_Cleanup :: struct { + procedure: proc(rawptr), + user_data: rawptr, +} + +T :: struct { + error_count: int, + + w: io.Writer, + + cleanups: [dynamic]Internal_Cleanup, +} + + +error :: proc(t: ^T, args: ..any, loc := #caller_location) { + log(t=t, args=args, loc=loc); + t.error_count += 1; +} + +errorf :: proc(t: ^T, format: string, args: ..any, loc := #caller_location) { + logf(t=t, format=format, args=args, loc=loc); + t.error_count += 1; +} + +fail :: proc(t: ^T) { + error(t, "FAIL"); + t.error_count += 1; +} + +failed :: proc(t: ^T) -> bool { + return t.error_count != 0; +} + +log :: proc(t: ^T, args: ..any, loc := #caller_location) { + fmt.wprintln(t.w, ..args); +} + +logf :: proc(t: ^T, format: string, args: ..any, loc := #caller_location) { + fmt.wprintf(t.w, format, ..args); + fmt.wprintln(t.w); +} + + +// cleanup registers a procedure and user_data, which will be called when the test, and all its subtests, complete +// cleanup proceduers will be called in LIFO (last added, first called) order. +cleanup :: proc(t: ^T, procedure: proc(rawptr), user_data: rawptr) { + append(&t.cleanups, Internal_Cleanup{procedure, user_data}); +} + +expect :: proc(t: ^T, ok: bool, msg: string = "", loc := #caller_location) -> bool { + if !ok { + error(t=t, args={msg}, loc=loc); + } + return ok; +} diff --git a/src/build_settings.cpp b/src/build_settings.cpp index 8ffc4955a..e1192eea5 100644 --- a/src/build_settings.cpp +++ b/src/build_settings.cpp @@ -365,8 +365,8 @@ bool is_excluded_target_filename(String name) { return true; } - String test_suffix = str_lit("_test"); if (build_context.command_kind != Command_test) { + String test_suffix = str_lit("_test"); if (string_ends_with(name, test_suffix) && name != test_suffix) { // Ignore *_test.odin files return true; diff --git a/src/checker.cpp b/src/checker.cpp index d5d6bf5c3..fdf9cefea 100644 --- a/src/checker.cpp +++ b/src/checker.cpp @@ -1865,6 +1865,20 @@ void generate_minimum_dependency_set(Checker *c, Entity *start) { } if (build_context.command_kind == Command_test) { + AstPackage *testing_package = get_core_package(&c->info, str_lit("testing")); + Scope *testing_scope = testing_package->scope; + + // Add all of testing library as a dependency + for_array(i, testing_scope->elements.entries) { + Entity *e = testing_scope->elements.entries[i].value; + if (e != nullptr) { + e->flags |= EntityFlag_Used; + add_dependency_to_set(c, e); + } + } + + Entity *test_signature = scope_lookup_current(testing_scope, str_lit("Test_Signature")); + AstPackage *pkg = c->info.init_package; Scope *s = pkg->scope; for_array(i, s->elements.entries) { @@ -1884,6 +1898,7 @@ void generate_minimum_dependency_set(Checker *c, Entity *start) { continue; } + bool is_tester = false; if (name != prefix) { is_tester = true; @@ -1893,11 +1908,11 @@ void generate_minimum_dependency_set(Checker *c, Entity *start) { Type *t = base_type(e->type); GB_ASSERT(t->kind == Type_Proc); - if (t->Proc.param_count == 0 && t->Proc.result_count == 0) { + if (are_types_identical(t, base_type(test_signature->type))) { // Good } else { gbString str = type_to_string(t); - error(e->token, "Testing procedures must have a signature type of proc(), got %s", str); + error(e->token, "Testing procedures must have a signature type of proc(^testing.T), got %s", str); gb_string_free(str); is_tester = false; } @@ -2103,6 +2118,28 @@ Type *find_core_type(Checker *c, String name) { return e->type; } + +Entity *find_entity_in_pkg(CheckerInfo *info, String const &pkg, String const &name) { + AstPackage *package = get_core_package(info, pkg); + Entity *e = scope_lookup_current(package->scope, name); + if (e == nullptr) { + compiler_error("Could not find type declaration for '%.*s.%.*s'\n", LIT(pkg), LIT(name)); + // NOTE(bill): This will exit the program as it's cannot continue without it! + } + return e; +} + +Type *find_type_in_pkg(CheckerInfo *info, String const &pkg, String const &name) { + AstPackage *package = get_core_package(info, pkg); + Entity *e = scope_lookup_current(package->scope, name); + if (e == nullptr) { + compiler_error("Could not find type declaration for '%.*s.%.*s'\n", LIT(pkg), LIT(name)); + // NOTE(bill): This will exit the program as it's cannot continue without it! + } + GB_ASSERT(e->type != nullptr); + return e->type; +} + CheckerTypePath *new_checker_type_path() { gbAllocator a = heap_allocator(); auto *tp = gb_alloc_item(a, CheckerTypePath); diff --git a/src/ir.cpp b/src/ir.cpp index 0f235b914..3022fd869 100644 --- a/src/ir.cpp +++ b/src/ir.cpp @@ -12930,12 +12930,39 @@ void ir_gen_tree(irGen *s) { ir_emit(proc, ir_alloc_instr(proc, irInstr_StartupRuntime)); Array empty_args = {}; if (build_context.command_kind == Command_test) { + Type *t_Internal_Test = find_type_in_pkg(m->info, str_lit("testing"), str_lit("Internal_Test")); + Type *array_type = alloc_type_array(t_Internal_Test, m->info->testing_procedures.count); + Type *slice_type = alloc_type_slice(t_Internal_Test); + irValue *all_tests_array = ir_add_global_generated(proc->module, array_type, nullptr); + for_array(i, m->info->testing_procedures) { - Entity *e = m->info->testing_procedures[i]; - irValue **found = map_get(&proc->module->values, hash_entity(e)); + Entity *testing_proc = m->info->testing_procedures[i]; + String name = testing_proc->token.string; + irValue **found = map_get(&m->values, hash_entity(testing_proc)); GB_ASSERT(found != nullptr); - ir_emit_call(proc, *found, empty_args); + + irValue *v_name = ir_find_or_add_entity_string(m, name); + irValue *v_p = *found; + + + irValue *elem_ptr = ir_emit_array_epi(proc, all_tests_array, cast(i32)i); + irValue *name_ptr = ir_emit_struct_ep(proc, elem_ptr, 0); + irValue *p_ptr = ir_emit_struct_ep(proc, elem_ptr, 1); + ir_emit_store(proc, name_ptr, v_name); + ir_emit_store(proc, p_ptr, v_p); } + + irValue *all_tests_slice = ir_add_local_generated(proc, slice_type, true); + ir_fill_slice(proc, all_tests_slice, + ir_array_elem(proc, all_tests_array), + ir_const_int(m->info->testing_procedures.count)); + + + irValue *runner = ir_get_package_value(m, str_lit("testing"), str_lit("runner")); + + auto args = array_make(temporary_allocator(), 1); + args[0] = ir_emit_load(proc, all_tests_slice); + ir_emit_call(proc, runner, args); } else { irValue **found = map_get(&proc->module->values, hash_entity(entry_point)); if (found != nullptr) { diff --git a/src/llvm_backend.cpp b/src/llvm_backend.cpp index 09c4ef2fd..ef08edaa4 100644 --- a/src/llvm_backend.cpp +++ b/src/llvm_backend.cpp @@ -11496,6 +11496,13 @@ lbValue lb_find_runtime_value(lbModule *m, String const &name) { lbValue value = *found; return value; } +lbValue lb_find_package_value(lbModule *m, String const &pkg, String const &name) { + Entity *e = find_entity_in_pkg(m->info, pkg, name); + lbValue *found = map_get(&m->values, hash_entity(e)); + GB_ASSERT_MSG(found != nullptr, "Unable to find value '%.*s.%.*s'", LIT(pkg), LIT(name)); + lbValue value = *found; + return value; +} lbValue lb_get_type_info_ptr(lbModule *m, Type *type) { i32 index = cast(i32)lb_type_info_index(m->info, type); @@ -12885,12 +12892,51 @@ void lb_generate_code(lbGenerator *gen) { LLVMBuildCall2(p->builder, LLVMGetElementType(lb_type(m, startup_runtime->type)), startup_runtime->value, nullptr, 0, ""); if (build_context.command_kind == Command_test) { + Type *t_Internal_Test = find_type_in_pkg(m->info, str_lit("testing"), str_lit("Internal_Test")); + Type *array_type = alloc_type_array(t_Internal_Test, m->info->testing_procedures.count); + Type *slice_type = alloc_type_slice(t_Internal_Test); + lbAddr all_tests_array_addr = lb_add_global_generated(p->module, array_type, {}); + lbValue all_tests_array = lb_addr_get_ptr(p, all_tests_array_addr); + + LLVMTypeRef lbt_Internal_Test = lb_type(m, t_Internal_Test); + + LLVMValueRef indices[2] = {}; + indices[0] = LLVMConstInt(lb_type(m, t_i32), 0, false); + for_array(i, m->info->testing_procedures) { - Entity *e = m->info->testing_procedures[i]; - lbValue *found = map_get(&m->values, hash_entity(e)); + Entity *testing_proc = m->info->testing_procedures[i]; + String name = testing_proc->token.string; + lbValue *found = map_get(&m->values, hash_entity(testing_proc)); GB_ASSERT(found != nullptr); - lb_emit_call(p, *found, {}); + + lbValue v_name = lb_find_or_add_entity_string(m, name); + lbValue v_proc = *found; + + indices[1] = LLVMConstInt(lb_type(m, t_int), i, false); + + LLVMValueRef vals[2] = {}; + vals[0] = v_name.value; + vals[1] = v_proc.value; + GB_ASSERT(LLVMIsConstant(vals[0])); + GB_ASSERT(LLVMIsConstant(vals[1])); + + LLVMValueRef dst = LLVMConstInBoundsGEP(all_tests_array.value, indices, gb_count_of(indices)); + LLVMValueRef src = LLVMConstNamedStruct(lbt_Internal_Test, vals, gb_count_of(vals)); + + LLVMBuildStore(p->builder, src, dst); } + + lbAddr all_tests_slice = lb_add_local_generated(p, slice_type, true); + lb_fill_slice(p, all_tests_slice, + lb_array_elem(p, all_tests_array), + lb_const_int(m, t_int, m->info->testing_procedures.count)); + + + lbValue runner = lb_find_package_value(m, str_lit("testing"), str_lit("runner")); + + auto args = array_make(heap_allocator(), 1); + args[0] = lb_addr_load(p, all_tests_slice); + lb_emit_call(p, runner, args); } else { lbValue *found = map_get(&m->values, hash_entity(entry_point)); GB_ASSERT(found != nullptr); diff --git a/src/parser.cpp b/src/parser.cpp index c1cda84c2..1b3a37c3b 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -5287,7 +5287,6 @@ ParseFileError process_imported_file(Parser *p, ImportedFile const &imported_fil AstFile *file = gb_alloc_item(heap_allocator(), AstFile); file->pkg = pkg; file->id = cast(i32)(imported_file.index+1); - TokenPos err_pos = {0}; ParseFileError err = init_ast_file(file, fi->fullpath, &err_pos); err_pos.file_id = file->id; @@ -5328,6 +5327,16 @@ ParseFileError process_imported_file(Parser *p, ImportedFile const &imported_fil } } + if (build_context.command_kind == Command_test) { + String name = file->fullpath; + name = remove_extension_from_path(name); + + String test_suffix = str_lit("_test"); + if (string_ends_with(name, test_suffix) && name != test_suffix) { + file->is_test = true; + } + } + if (parse_file(p, file)) { gb_mutex_lock(&p->file_add_mutex); defer (gb_mutex_unlock(&p->file_add_mutex)); @@ -5373,6 +5382,11 @@ ParseFileError parse_packages(Parser *p, String init_filename) { try_add_import_path(p, init_fullpath, init_fullpath, init_pos, Package_Init); p->init_fullpath = init_fullpath; + if (build_context.command_kind == Command_test) { + String s = get_fullpath_core(heap_allocator(), str_lit("testing")); + try_add_import_path(p, s, s, init_pos, Package_Normal); + } + for_array(i, build_context.extra_packages) { String path = build_context.extra_packages[i]; String fullpath = path_to_full_path(heap_allocator(), path); // LEAK?