Initial implementation
This commit is contained in:
parent
0ed93d6fbd
commit
d3aa58655a
|
@ -0,0 +1,294 @@
|
|||
#import "Basic";
|
||||
#import "String";
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// BEGIN INTERFACE
|
||||
|
||||
// Register a function to run before each test
|
||||
Before_Each :: (proc: Test_Proc, loc := #caller_location) #expand
|
||||
{
|
||||
h := harness();
|
||||
if h.before_each
|
||||
{
|
||||
print_error("You cannot register more than one Before_Each handler", loc);
|
||||
exit(1);
|
||||
}
|
||||
h.before_each = proc;
|
||||
}
|
||||
|
||||
// Register a function to run after each test
|
||||
After_Each :: (proc: Test_Proc, loc := #caller_location) #expand
|
||||
{
|
||||
h := harness();
|
||||
if h.after_each
|
||||
{
|
||||
print_error("You cannot register more than one After_Each handler", loc);
|
||||
exit(1);
|
||||
}
|
||||
h.after_each = proc;
|
||||
}
|
||||
|
||||
// Wrapper for registering a test procedure you want to have run
|
||||
Test :: (name: string, proc: Test_Proc, loc := #caller_location, only := false) #expand
|
||||
{
|
||||
h := harness();
|
||||
test_index := h.tests.count;
|
||||
array_add(*h.tests, .{
|
||||
name = name,
|
||||
proc = proc
|
||||
});
|
||||
|
||||
if only
|
||||
{
|
||||
if h.only_index < 0
|
||||
{
|
||||
h.only_index = test_index;
|
||||
}
|
||||
else
|
||||
{
|
||||
print_error("You cannot register more than one test to be the only one to run.", loc);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Call this at the beginning of your main function
|
||||
Init_Test_Harness :: (prefix: string = "", loc := #caller_location) #expand
|
||||
{
|
||||
h := harness();
|
||||
<< h = Test_Harness.{
|
||||
test_prefix = prefix,
|
||||
filename = tprint("%", #file),
|
||||
};
|
||||
init_string_builder(*h.message_builder);
|
||||
|
||||
apply_command_line_arguments(h);
|
||||
|
||||
print("%\n", loc.fully_pathed_filename);
|
||||
}
|
||||
|
||||
// Call this at the end of your main function
|
||||
Run_Test_Harness :: (loc:= #caller_location) #expand
|
||||
{
|
||||
h := harness();
|
||||
|
||||
if h.only_index >= 0 && h.only_index < h.tests.count
|
||||
{
|
||||
run_single_test(h, h.tests[h.only_index]);
|
||||
}
|
||||
else
|
||||
{
|
||||
for h.tests
|
||||
{
|
||||
run_single_test(h, it);
|
||||
}
|
||||
}
|
||||
|
||||
return_code := finish_all_tests();
|
||||
exit(return_code);
|
||||
}
|
||||
|
||||
EXPECT_MACRO :: (input: $T, expected: T, loc: Source_Code_Location, info: string, condition: Code) #expand {
|
||||
test_begin();
|
||||
passed := #insert condition;
|
||||
if passed {
|
||||
report_test_passed();
|
||||
} else {
|
||||
report_test_failed(input, expected, loc, info);
|
||||
}
|
||||
}
|
||||
|
||||
expect :: (input: $T, expected: T, loc := #caller_location, info := "") {
|
||||
EXPECT_MACRO(input, expected, loc, info, #code input == expected);
|
||||
}
|
||||
expect_strings_equal :: (input: string, expected: string, loc:= #caller_location, info := "") {
|
||||
EXPECT_MACRO(input, expected, loc, info, #code equal(input, expected));
|
||||
}
|
||||
expect_true :: (input: bool, loc:= #caller_location, info := "") {
|
||||
EXPECT_MACRO(input, true, loc, info, #code input == true);
|
||||
}
|
||||
expect_false :: (input: bool, loc:= #caller_location, info := "") {
|
||||
EXPECT_MACRO(input, false, loc, info, #code input == false);
|
||||
}
|
||||
|
||||
// END INTERFACE
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
|
||||
test_begin :: () {
|
||||
TESTS_ATTEMPTED += 1;
|
||||
}
|
||||
|
||||
report_test_passed :: () {
|
||||
TESTS_PASSED += 1;
|
||||
}
|
||||
|
||||
report_test_failed :: (input: $T, expected: T, loc: Source_Code_Location, info: string) {
|
||||
h := harness();
|
||||
ANY_TESTS_FAILED = true;
|
||||
print_to_builder(
|
||||
*h.message_builder,
|
||||
" %Test Failed:%\n",
|
||||
h.colors.red,
|
||||
h.colors.normal
|
||||
);
|
||||
print_to_builder(*h.message_builder,
|
||||
" %:%:%\n", loc.fully_pathed_filename, loc.line_number, loc.character_number
|
||||
);
|
||||
print_to_builder(*h.message_builder,
|
||||
" Expected: %\n", expected
|
||||
);
|
||||
print_to_builder(*h.message_builder,
|
||||
" Received: %\n", input
|
||||
);
|
||||
if info.count > 0 {
|
||||
print_to_builder(*h.message_builder, " Info: %\n", info);
|
||||
}
|
||||
}
|
||||
|
||||
#scope_file
|
||||
|
||||
ANY_TESTS_FAILED := false;
|
||||
TESTS_ATTEMPTED := 0;
|
||||
TESTS_PASSED := 0;
|
||||
TOTAL_TESTS_ATTEMPTED := 0;
|
||||
TOTAL_TESTS_PASSED := 0;
|
||||
|
||||
Terminal_Color_Codes :: struct {
|
||||
normal: string;
|
||||
red: string;
|
||||
green: string;
|
||||
blue: string;
|
||||
};
|
||||
|
||||
colors :: Terminal_Color_Codes.{
|
||||
normal = "\e[m",
|
||||
red = "\e[1;31m",
|
||||
green = "\e[1;32m",
|
||||
blue = "\e[1;34m",
|
||||
};
|
||||
no_colors :: Terminal_Color_Codes.{};
|
||||
|
||||
Test_Proc :: #type () -> ();
|
||||
|
||||
Test_Desc :: struct {
|
||||
name: string;
|
||||
proc: Test_Proc;
|
||||
}
|
||||
|
||||
Test_Harness :: struct {
|
||||
filename: string;
|
||||
tests: [..]Test_Desc;
|
||||
|
||||
// if set, then tests[only_index] will be the only one to run.
|
||||
// it is an error to set multiple tests to only.
|
||||
only_index := -1;
|
||||
|
||||
before_each: Test_Proc;
|
||||
after_each: Test_Proc;
|
||||
test_prefix: string = "";
|
||||
|
||||
verbose: bool = false;
|
||||
colors := no_colors;
|
||||
|
||||
message_builder: String_Builder;
|
||||
}
|
||||
|
||||
#add_context global_test_harness: Test_Harness;
|
||||
harness :: () -> *Test_Harness { return *context.global_test_harness; }
|
||||
|
||||
apply_command_line_arguments :: (harness: *Test_Harness)
|
||||
{
|
||||
args := get_command_line_arguments();
|
||||
for arg: args {
|
||||
if arg[0] != #char "-" continue;
|
||||
if arg == {
|
||||
case "-colors"; harness.colors = colors;
|
||||
case "-verbose"; harness.verbose = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test_suite_end :: (name: string)
|
||||
{
|
||||
h := harness();
|
||||
print_test_name :: (h: Test_Harness, name: string, prefix: string)
|
||||
{
|
||||
print("%", h.colors.blue);
|
||||
if (prefix.count) print("% :: %\n", prefix, name);
|
||||
else print("%\n", name);
|
||||
print("%", h.colors.normal);
|
||||
}
|
||||
|
||||
if TESTS_ATTEMPTED == TESTS_PASSED
|
||||
{
|
||||
if TESTS_ATTEMPTED > 0
|
||||
{
|
||||
if (h.verbose)
|
||||
{
|
||||
print(" %PASS ", h.colors.green);
|
||||
print_test_name(h, name, h.test_prefix);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
print(" %NO TESTS RUN ", h.colors.red);
|
||||
print_test_name(h, name, h.test_prefix);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
print(" %FAIL ", h.colors.red);
|
||||
print_test_name(h, name, h.test_prefix);
|
||||
}
|
||||
|
||||
messages := builder_to_string(*h.message_builder, do_reset = true);
|
||||
print(messages);
|
||||
|
||||
TOTAL_TESTS_ATTEMPTED += TESTS_ATTEMPTED;
|
||||
TOTAL_TESTS_PASSED += TESTS_PASSED;
|
||||
TESTS_ATTEMPTED = 0;
|
||||
TESTS_PASSED = 0;
|
||||
}
|
||||
|
||||
run_single_test :: (h: *Test_Harness, test: Test_Desc)
|
||||
{
|
||||
if h.before_each
|
||||
{
|
||||
h.before_each();
|
||||
}
|
||||
|
||||
test.proc();
|
||||
test_suite_end(test.name);
|
||||
|
||||
if h.after_each
|
||||
{
|
||||
h.after_each();
|
||||
}
|
||||
}
|
||||
|
||||
finish_all_tests :: () -> s32
|
||||
{
|
||||
h := harness();
|
||||
return_code: s32 = 0;
|
||||
|
||||
if !ANY_TESTS_FAILED
|
||||
{
|
||||
print("Tests: % / %\n", TOTAL_TESTS_PASSED, TOTAL_TESTS_ATTEMPTED);
|
||||
print(" SUITE STATUS: %PASS%\n\n", h.colors.green, h.colors.normal);
|
||||
}
|
||||
else
|
||||
{
|
||||
return_code = 1;
|
||||
print("Tests: % / %\n", TOTAL_TESTS_PASSED, TOTAL_TESTS_ATTEMPTED);
|
||||
print(" SUITE_STATUS: %FAIL%\n\n", h.colors.red, h.colors.normal);
|
||||
}
|
||||
return return_code;
|
||||
}
|
||||
|
||||
print_error :: (msg: string, loc: Source_Code_Location)
|
||||
{
|
||||
h := harness();
|
||||
print("%", h.colors.red);
|
||||
print("%:% - %\n", loc.fully_pathed_filename, loc.line_number, msg);
|
||||
print("%", h.colors.normal);
|
||||
}
|
|
@ -0,0 +1,243 @@
|
|||
#import "Basic";
|
||||
#import "Compiler";
|
||||
#import "File";
|
||||
#import "File_Utilities";
|
||||
#import "String";
|
||||
|
||||
Test_Category :: struct
|
||||
{
|
||||
file_extension: string = ".test.jai";
|
||||
// The extension all test files will end in.
|
||||
// You can override this, but it is recommended that you never set it to .jai as
|
||||
// that will match all your normal jai files as well.
|
||||
|
||||
exe_extension: string = "";
|
||||
// The extension of the output test executable.
|
||||
// If not set, this will be overridden
|
||||
// with .test.exe on windows and .test everywhere else.
|
||||
|
||||
category_name: string = "default";
|
||||
// If set to anything other than defualt, will put these tests inside an if block inside
|
||||
// run_all_tests that checks for this name in the arguments to run_all_tests.sh
|
||||
// ie. if category_name is "unit", then calling "run_all_tests.sh unit" will run just the
|
||||
// tests in this category
|
||||
|
||||
exclude_from_run_all_tests: bool = false;
|
||||
// This is useful if you have a category of tests that need to be run manually, or don't
|
||||
// make sense to be run in CI
|
||||
}
|
||||
|
||||
Config :: struct
|
||||
{
|
||||
root_paths: []string;
|
||||
// REQUIRED: The root directory in which tests are found
|
||||
|
||||
test_categories: []Test_Category;
|
||||
// REQUIRED: You must supply at least one test category. However, it is enough to simply
|
||||
// do:
|
||||
// config.test_categories = .[.{}];
|
||||
|
||||
tests_output_dir: string = "";
|
||||
// Where test executables will be output.
|
||||
// If not set, this will be set to the value of {build_options.root_path}/tests_out
|
||||
}
|
||||
|
||||
build_all_tests :: (config: Config, build_options: Build_Options, loc := #caller_location)
|
||||
{
|
||||
set_optimization(*build_options, .DEBUG);
|
||||
configure(config, build_options, loc);
|
||||
output();
|
||||
}
|
||||
|
||||
configure :: (config: Config, build_options: Build_Options, loc := #caller_location)
|
||||
{
|
||||
stored_config = config;
|
||||
|
||||
if stored_config.test_categories.count == 0
|
||||
{
|
||||
print("gs_test: Error - No test_categories provided, so no tests will be build. Aborting.\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
for * stored_config.test_categories
|
||||
{
|
||||
if (it.file_extension == "") it.file_extension = ".test.jai";
|
||||
if (it.exe_extension == "")
|
||||
{
|
||||
it.exe_extension = ".test";
|
||||
}
|
||||
}
|
||||
|
||||
if (stored_config.root_paths.count == 0)
|
||||
{
|
||||
compiler_report(loc.fully_pathed_filename, loc.line_number, loc.character_number, "You must supply a root_path in gs_test's config object");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (stored_config.tests_output_dir == "")
|
||||
{
|
||||
path := parse_path(stored_config.root_paths[0]);
|
||||
array_add(*path.words, "tests_out");
|
||||
stored_config.tests_output_dir = path_to_string(path);
|
||||
print("Fallback tests_output_dir: %\n", stored_config.tests_output_dir);
|
||||
}
|
||||
else
|
||||
{
|
||||
for i: 0..stored_config.tests_output_dir.count - 1
|
||||
{
|
||||
if stored_config.tests_output_dir[i] == #char "\\"
|
||||
{
|
||||
stored_config.tests_output_dir[i] = #char "/";
|
||||
}
|
||||
}
|
||||
path := parse_path(stored_config.tests_output_dir);
|
||||
stored_config.tests_output_dir = path_to_string(path);
|
||||
}
|
||||
|
||||
stored_build_options = build_options;
|
||||
stored_build_options.output_path = stored_config.tests_output_dir;
|
||||
|
||||
make_directory_if_it_does_not_exist(stored_build_options.output_path, true);
|
||||
|
||||
for root_path: config.root_paths
|
||||
{
|
||||
visit_files(root_path, true, *test_files, record_test_file);
|
||||
}
|
||||
}
|
||||
|
||||
output :: ()
|
||||
{
|
||||
run_all_tests_builder: String_Builder;
|
||||
init_string_builder(*run_all_tests_builder);
|
||||
append(*run_all_tests_builder, "#!/bin/bash\n");
|
||||
append(*run_all_tests_builder, "SUCCESS=0\n");
|
||||
append(*run_all_tests_builder, "ARGS=$@\n");
|
||||
|
||||
category_builders: [..]String_Builder;
|
||||
for stored_config.test_categories
|
||||
{
|
||||
b := array_add(*category_builders);
|
||||
init_string_builder(b);
|
||||
}
|
||||
|
||||
for test_files
|
||||
{
|
||||
category := stored_config.test_categories[it.category];
|
||||
exe_path := build_test_file(it.file_path, category);
|
||||
if (exe_path != "" && !category.exclude_from_run_all_tests)
|
||||
{
|
||||
print_to_builder(*category_builders[it.category], "% $@\n%", exe_path, UPDATE_SUCCESS_CODE);
|
||||
}
|
||||
}
|
||||
|
||||
for category, i: stored_config.test_categories
|
||||
{
|
||||
if category.exclude_from_run_all_tests continue;
|
||||
|
||||
category_builder := *category_builders[i];
|
||||
print_to_builder(*run_all_tests_builder, "\n# Category: %\n", category.category_name);
|
||||
if category.category_name != "default"
|
||||
{
|
||||
print_to_builder(*run_all_tests_builder, "if [[ \" ${ARGS[*]} \" =~ \" % \" ]]; then\n", category.category_name);
|
||||
}
|
||||
print_to_builder(*run_all_tests_builder, "%\n", builder_to_string(category_builder));
|
||||
if category.category_name != "default"
|
||||
{
|
||||
print_to_builder(*run_all_tests_builder, "fi\n");
|
||||
}
|
||||
}
|
||||
|
||||
append(*run_all_tests_builder, "exit $SUCCESS");
|
||||
run_all_tests_sh := builder_to_string(*run_all_tests_builder);
|
||||
write_entire_file(string_append(stored_config.tests_output_dir, "/run_all_tests.sh"), run_all_tests_sh);
|
||||
}
|
||||
|
||||
#scope_file
|
||||
|
||||
stored_config: Config;
|
||||
stored_build_options: Build_Options;
|
||||
|
||||
Test_File :: struct
|
||||
{
|
||||
file_path: string;
|
||||
category: int;
|
||||
}
|
||||
|
||||
test_files: [..]Test_File;
|
||||
|
||||
UPDATE_SUCCESS_CODE :: #string DONE
|
||||
if [ $? != 0 ];
|
||||
then
|
||||
SUCCESS=1
|
||||
fi
|
||||
DONE
|
||||
|
||||
string_append :: (args: .. string) -> string
|
||||
{
|
||||
sb: String_Builder;
|
||||
init_string_builder(*sb);
|
||||
for args {
|
||||
append(*sb, it);
|
||||
}
|
||||
return builder_to_string(*sb);
|
||||
}
|
||||
|
||||
record_test_file :: (info: *File_Visit_Info, test_files: *[..]Test_File)
|
||||
{
|
||||
if (info.is_directory) return;
|
||||
|
||||
longest_match := 0;
|
||||
longest_match_index := -1;
|
||||
for category, category_i: stored_config.test_categories
|
||||
{
|
||||
if (ends_with_nocase(info.full_name, category.file_extension))
|
||||
{
|
||||
if longest_match < category.file_extension.count
|
||||
{
|
||||
longest_match = category.file_extension.count;
|
||||
longest_match_index = category_i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if longest_match_index >= 0
|
||||
{
|
||||
array_add(test_files, .{
|
||||
file_path = copy_string(info.full_name),
|
||||
category = longest_match_index
|
||||
});
|
||||
print(" Test File: % - Category: %\n", info.full_name, stored_config.test_categories[longest_match_index].category_name);
|
||||
}
|
||||
}
|
||||
|
||||
build_test_file :: (path_string: string, category: Test_Category) -> string
|
||||
{
|
||||
workspace := compiler_create_workspace(tprint("Test: %", path_string));
|
||||
if !workspace
|
||||
{
|
||||
compiler_report(path_string, 0, 0, "Unable to create workspace for test file");
|
||||
return "";
|
||||
}
|
||||
|
||||
path := parse_path(path_string);
|
||||
filename := path.words[path.words.count - 1];
|
||||
filename_without_ext := slice(filename, 0, filename.count - category.file_extension.count);
|
||||
exe_output_path := string_append(
|
||||
filename_without_ext,
|
||||
category.exe_extension
|
||||
);
|
||||
|
||||
build_options := stored_build_options;
|
||||
build_options.output_executable_name = exe_output_path;
|
||||
|
||||
set_build_options(build_options, workspace);
|
||||
add_build_file(path_string, workspace);
|
||||
add_build_string(tprint("#load \"%harness.jai\";\n", #filepath), workspace);
|
||||
|
||||
output_path := parse_path(stored_build_options.output_path);
|
||||
array_add(*output_path.words, exe_output_path);
|
||||
output_path.trailing_slash = false;
|
||||
|
||||
return path_to_string(output_path);
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
#import "Basic";
|
||||
#import "Compiler";
|
||||
#import "String";
|
||||
|
||||
gs_test :: #import "gs_test";
|
||||
|
||||
#run build();
|
||||
build :: ()
|
||||
{
|
||||
target_options := get_build_options();
|
||||
|
||||
target_options.output_executable_name = "basic_test";
|
||||
target_options.output_path = tprint("%/output", #filepath);
|
||||
|
||||
gs_test.build_all_tests(.{
|
||||
root_paths = .[#filepath],
|
||||
test_categories = .[
|
||||
.{
|
||||
file_extension = ".gs_test.jai",
|
||||
}
|
||||
]
|
||||
}, target_options);
|
||||
|
||||
set_build_options_dc(.{ do_output = false }); // exclude this file from build
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
#load "./example_math.jai";
|
||||
|
||||
main :: () {
|
||||
Init_Test_Harness();
|
||||
|
||||
Test("basic test", () {
|
||||
expect(times2(1), 2);
|
||||
expect(times2(2), 4);
|
||||
});
|
||||
|
||||
Run_Test_Harness();
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
befores_called := 0;
|
||||
afters_called := 0;
|
||||
|
||||
main :: () {
|
||||
Init_Test_Harness();
|
||||
|
||||
Before_Each(() {
|
||||
befores_called += 1;
|
||||
});
|
||||
|
||||
After_Each(() {
|
||||
afters_called += 1;
|
||||
});
|
||||
|
||||
Test("before / after hooks #1", () {
|
||||
expect(befores_called, 1);
|
||||
expect(afters_called, 0);
|
||||
});
|
||||
|
||||
Test("before / after hooks #2", () {
|
||||
expect(befores_called, 2);
|
||||
expect(afters_called, 1);
|
||||
});
|
||||
|
||||
Run_Test_Harness();
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
#!/bin/bash
|
||||
|
||||
ROOT_DIR=$(dirname BASH_SOURCE[0])/../
|
||||
IMPORT_DIR=$ROOT_DIR/../
|
||||
|
||||
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
JAI=jai-linux
|
||||
elif [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
JAI=jai-macos
|
||||
else
|
||||
JAI=jai
|
||||
fi
|
||||
|
||||
$JAI -import_dir "${IMPORT_DIR}" ./basic_build.jai
|
||||
COMPILED=$?
|
||||
|
||||
if [ $COMPILED -ne 0 ]; then
|
||||
echo "Building Tests Failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
chmod +x ./tests_out/run_all_tests.sh
|
|
@ -0,0 +1,8 @@
|
|||
|
||||
times2 :: (a: int) -> int {
|
||||
return a + a;
|
||||
}
|
||||
|
||||
times3 :: (a: int) -> int {
|
||||
return a + a + a;
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
#!/bin/bash
|
||||
SUCCESS=0
|
||||
ARGS=$@
|
||||
|
||||
TEST_DIR=$(dirname ${BASH_SOURCE[0]})/tests_out
|
||||
|
||||
# Category: solo
|
||||
if [[ " ${ARGS[*]} " =~ " solo " ]]; then
|
||||
$TEST_DIR/basic_first.test $@
|
||||
if [ $? != 0 ];
|
||||
then
|
||||
SUCCESS=1
|
||||
fi
|
||||
$TEST_DIR/basic_first2.test $@
|
||||
if [ $? != 0 ];
|
||||
then
|
||||
SUCCESS=1
|
||||
fi
|
||||
|
||||
fi
|
||||
exit $SUCCESS
|
Loading…
Reference in New Issue