// See License Information at the bottom of this file #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" { RUN_CONDITION :: "if [[ \" ${ARGS[*]} \" =~ \" % \" ]] || [[ \" ${ARGS[*]} \" =~ \" all \" ]]; then\n"; print_to_builder(*run_all_tests_builder, RUN_CONDITION, category.category_name); } category_tests_string := builder_to_string(category_builder); if category_tests_string.count > 0 { print_to_builder(*run_all_tests_builder, "%\n", category_tests_string); } else { print_to_builder( *run_all_tests_builder, "echo No tests in this category: %\n", category.category_name ); } 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); } /* zlib License (C) Copyright 2023 Peter Slattery This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. */