Browse Source

`odin test` to work with the new `core:testing` package

gingerBill 4 years ago
parent
commit
2aa588209e
7 changed files with 262 additions and 10 deletions
  1. 60 0
      core/testing/runner.odin
  2. 68 0
      core/testing/testing.odin
  3. 1 1
      src/build_settings.cpp
  4. 39 2
      src/checker.cpp
  5. 30 3
      src/ir.cpp
  6. 49 3
      src/llvm_backend.cpp
  7. 15 1
      src/parser.cpp

+ 60 - 0
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;
+}

+ 68 - 0
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;
+}

+ 1 - 1
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;

+ 39 - 2
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);

+ 30 - 3
src/ir.cpp

@@ -12930,12 +12930,39 @@ void ir_gen_tree(irGen *s) {
 		ir_emit(proc, ir_alloc_instr(proc, irInstr_StartupRuntime));
 		Array<irValue *> 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<irValue *>(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) {

+ 49 - 3
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<lbValue>(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);

+ 15 - 1
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?