diff --git a/src/codechecker.bzl b/src/codechecker.bzl index 505532d0..bb428ff3 100644 --- a/src/codechecker.bzl +++ b/src/codechecker.bzl @@ -341,6 +341,7 @@ def codechecker_test( name = name, targets = targets, options = analyze, + skip = skip, config = config, tags = tags, **kwargs diff --git a/src/per_file.bzl b/src/per_file.bzl index 27e87654..57d55e56 100644 --- a/src/per_file.bzl +++ b/src/per_file.bzl @@ -50,12 +50,30 @@ def _run_code_checker( clangsa_plist = ctx.actions.declare_file(clangsa_plist_file_name) codechecker_log = ctx.actions.declare_file(codechecker_log_file_name) + # Create skipfile + config = ctx.actions.declare_file( + "{}/{}_skipfile".format(*file_name_params), + ) + ctx.actions.write( + output = config, + content = "\n".join(ctx.attr.skip), + ) + if "--ctu" in options: - inputs = [compile_commands_json, config_file] + sources_and_headers + inputs = [ + compile_commands_json, + config_file, + config, + ] + sources_and_headers else: # NOTE: we collect only headers, so CTU may not work! headers = depset(transitive = target[SourceFilesInfo].headers.to_list()) - inputs = depset([compile_commands_json, config_file, src], transitive = [headers]) + inputs = depset([ + compile_commands_json, + config_file, + src, + config, + ], transitive = [headers]) outputs = [clang_tidy_plist, clangsa_plist, codechecker_log] @@ -71,6 +89,7 @@ def _run_code_checker( data_dir, src.path, codechecker_log.path, + config.path, analyzer_output_paths, ], mnemonic = "CodeChecker", @@ -217,6 +236,11 @@ per_file_test = rule( default = [], doc = "List of CodeChecker options, e.g.: --ctu", ), + "skip": attr.string_list( + default = [], + doc = "List of skip/ignore file rules. " + + "See https://codechecker.readthedocs.io/en/latest/analyzer/user_guide/#skip-file", + ), "targets": attr.label_list( aspects = [ compile_commands_aspect, diff --git a/src/per_file_script.py b/src/per_file_script.py index d64b7a6c..ab9901dc 100644 --- a/src/per_file_script.py +++ b/src/per_file_script.py @@ -23,30 +23,42 @@ import shutil import subprocess import sys -from typing import Optional -# The output directory for CodeChecker -DATA_DIR: Optional[str] = None -# The file to be analyzed -FILE_PATH: Optional[str] = None -# List of pairs of analyzers and their plist files -ANALYZER_PLIST_PATHS: Optional[list[list[str]]] = None -LOG_FILE: Optional[str] = None COMPILE_COMMANDS_JSON: str = "{compile_commands_json}" COMPILE_COMMANDS_ABSOLUTE: str = f"{COMPILE_COMMANDS_JSON}.abs" CODECHECKER_ARGS: str = "{codechecker_args}" CONFIG_FILE: str = "{config_file}" +SKIP_FILE: str = sys.argv[4] +# The output directory for CodeChecker DATA_DIR = sys.argv[1] +# The file to be analyzed FILE_PATH = sys.argv[2] LOG_FILE = sys.argv[3] -ANALYZER_PLIST_PATHS = [item.split(",") for item in sys.argv[4].split(";")] +# List of pairs of analyzers and their plist files +ANALYZER_PLIST_PATHS = [item.split(",") for item in sys.argv[5].split(";")] + +EMPTY_PLIST = """ + + + + metadata + + generated_by + + name + CodeChecker + + + + +""" def log(msg: str) -> None: """ Append message to the log file """ - with open(LOG_FILE, "a", encoding="utf-8") as log_file: # type: ignore + with open(LOG_FILE, "a", encoding="utf-8") as log_file: log_file.write(msg) @@ -76,8 +88,9 @@ def _run_codechecker() -> None: codechecker_cmd: list[str] = ( ["CodeChecker", "analyze"] + CODECHECKER_ARGS.split() - + ["--output=" + DATA_DIR] # type: ignore - + ["--file=*/" + FILE_PATH] # type: ignore + + ["--output=" + DATA_DIR] + + ["--file=*/" + FILE_PATH] + + ["--skip", SKIP_FILE] + ["--config", CONFIG_FILE] + [COMPILE_COMMANDS_ABSOLUTE] ) @@ -97,7 +110,7 @@ def _run_codechecker() -> None: log(result.stdout) try: - with open(LOG_FILE, "a", encoding="utf-8") as log_file: # type: ignore + with open(LOG_FILE, "a", encoding="utf-8") as log_file: subprocess.run( codechecker_cmd, env=os.environ, @@ -118,7 +131,7 @@ def _display_error(ret_code: int) -> None: # Log and exit on error print("===-----------------------------------------------------===") print(f"[ERROR]: CodeChecker returned with {ret_code}!") - with open(LOG_FILE, "r", encoding="utf-8") as log_file: # type: ignore + with open(LOG_FILE, "r", encoding="utf-8") as log_file: print(log_file.read()) sys.exit(1) @@ -126,28 +139,41 @@ def _display_error(ret_code: int) -> None: def _move_plist_files(): """ Move the plist files from the temporary directory to their final destination + If the files doesn't exists, write an empty plist file to the target. + This can happen when an analysis was skipped + because of a CodeChecker skipfile. + For each analysis action we must have an output file, even if its skipped, + so we substitute it with an empty one. """ # NOTE: the following we do to get rid of md5 hash in plist file names # Copy the plist files to the specified destinations - for file in os.listdir(DATA_DIR): - for analyzer_info in ANALYZER_PLIST_PATHS: # type: ignore - if re.search( - rf"_{analyzer_info[0]}_.*\.plist$", file - ) and os.path.isfile( - os.path.join(DATA_DIR, file) # type: ignore - - ): + destination_and_source_pattern_pairs = [ + (analyzer[1], re.compile(rf"_{analyzer[0]}_.*\.plist$")) + for analyzer in ANALYZER_PLIST_PATHS + ] + + for ( + destination_plist_path, + source_plist_search_pattern, + ) in destination_and_source_pattern_pairs: + for file_path in os.listdir(DATA_DIR): + if not os.path.isfile(os.path.join(DATA_DIR, file_path)): + continue + if source_plist_search_pattern.search(file_path): shutil.move( - os.path.join(DATA_DIR, file), # type: ignore - analyzer_info[1], - ) + os.path.join(DATA_DIR, file_path), destination_plist_path + ) + break + else: + with open(destination_plist_path, "w", encoding="utf-8") as file: + file.write(EMPTY_PLIST) def main(): """ Main function of CodeChecker wrapper """ - if len(sys.argv) != 5: + if len(sys.argv) != 6: print("Wrong amount of arguments") sys.exit(1) _create_compile_commands_json_with_absolute_paths() diff --git a/test/unit/skip/BUILD b/test/unit/skip/BUILD index 5d9e0737..b45ec426 100644 --- a/test/unit/skip/BUILD +++ b/test/unit/skip/BUILD @@ -76,6 +76,18 @@ codechecker_test( skip = [ "-*test/unit/skip/skip*", ], + targets = [ + "simple_target", + ], +) + +codechecker_test( + name = "per_file_skipfile_both_files_except_one", + per_file = True, + skip = [ + "+*test/unit/skip/skip.cc", + "-*test/unit/skip/skip*", + ], tags = ["manual"], targets = [ "simple_target", diff --git a/test/unit/skip/test_skip.py b/test/unit/skip/test_skip.py index 807230e0..66a49899 100644 --- a/test/unit/skip/test_skip.py +++ b/test/unit/skip/test_skip.py @@ -41,7 +41,7 @@ def test_codechecker_skipfile(self): ) self.assertEqual(ret, 0, stderr) - def test_per_file_skipfile_full_path(self): + def test_per_file_skipfile_exact_file_path(self): """ Test: bazel test //test/unit/skip:per_file_skipfile_exact_file_path """ @@ -53,8 +53,7 @@ def test_per_file_skipfile_full_path(self): f"{self.BAZEL_TESTLOGS_DIR}/" "per_file_skipfile_exact_file_path/test.log" ) - # FIXME: change to assertFalse, this file should be skipped - self.assertTrue( + self.assertFalse( self.contains_regex_in_file(log_file, r"defect\(s\) in skip.cc") ) self.assertTrue( @@ -73,8 +72,7 @@ def test_per_file_skipfile_folder_skip_path(self): f"{self.BAZEL_TESTLOGS_DIR}/" "per_file_skipfile_folder_skip_path/test.log" ) - # FIXME: change to assertFalse, this file should be skipped - self.assertTrue( + self.assertFalse( self.contains_regex_in_file(log_file, r"defect\(s\) in skip.cc") ) # This is correct. @@ -89,17 +87,35 @@ def test_per_file_skipfile_both_files(self): ret, _, stderr = self.run_command( "bazel test //test/unit/skip:per_file_skipfile_both_files" ) - # FIXME: The return code here should be 0, both files should be skipped - self.assertEqual(ret, 3, stderr) + self.assertEqual(ret, 0, stderr) log_file = ( f"{self.BAZEL_TESTLOGS_DIR}/per_file_skipfile_both_files/test.log" ) - # FIXME: Change to assertFalse after fix, should have been skipped. - self.assertTrue( + self.assertFalse( self.contains_regex_in_file(log_file, r"defect\(s\) in skip.cc") ) - # FIXME: Change to assertFalse after fix, should have been skipped. + self.assertFalse( + self.contains_regex_in_file(log_file, r"defect\(s\) in skip2.cc") + ) + + def test_per_file_skipfile_both_files_except_one(self): + """ + Test: bazel test + //test/unit/skip:per_file_skipfile_both_files_except_one + """ + ret, _, stderr = self.run_command( + "bazel test //test/unit/skip:" + "per_file_skipfile_both_files_except_one" + ) + self.assertEqual(ret, 3, stderr) + log_file = ( + f"{self.BAZEL_TESTLOGS_DIR}/" + "per_file_skipfile_both_files_except_one/test.log" + ) self.assertTrue( + self.contains_regex_in_file(log_file, r"defect\(s\) in skip.cc") + ) + self.assertFalse( self.contains_regex_in_file(log_file, r"defect\(s\) in skip2.cc") )