Coverage for tools/sel_tools/code_evaluation/jobs/cpp.py: 98%
120 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-02 05:55 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-02 05:55 +0000
1"""Cpp code evaluation jobs."""
3import re
4from pathlib import Path
5from typing import ClassVar
7import git
9from sel_tools.code_evaluation.jobs.common import (
10 EvaluationJob,
11 run_shell_command,
12 run_shell_command_with_output,
13)
14from sel_tools.config import CMAKE_MODULE_PATH, HW_BUILD_FOLDER
15from sel_tools.file_export.copy_item import copy_item
16from sel_tools.utils.config import CMAKELISTS_FILE_NAME
17from sel_tools.utils.files import FileTree, FileVisitor
20class CMakeBuildJob(EvaluationJob):
21 """Job for compiling the project."""
23 name = "CMake Build"
25 def __init__(self, weight: int = 1, cmake_options: str = "") -> None:
26 super().__init__(weight)
27 self.__cmake_options = cmake_options
29 def _run(self, repo_path: Path) -> int:
30 build_folder = repo_path / HW_BUILD_FOLDER
31 build_folder.mkdir(parents=True, exist_ok=True)
32 if run_shell_command(f"cmake {self.__cmake_options} ..", build_folder) == 0:
33 self._comment = f"CMake step failed with option {self.__cmake_options}: Make sure cmake .. passes."
34 return 0
35 if run_shell_command("make", build_folder) == 0:
36 self._comment = "Make step failed: Make sure you build passes when calling make."
37 return 0
38 return 1
41class MakeTestJob(EvaluationJob):
42 """Job for running make test."""
44 name = "Make Test"
45 dependencies: ClassVar[list[EvaluationJob]] = [CMakeBuildJob()]
47 def _run(self, repo_path: Path) -> int:
48 build_folder = repo_path / HW_BUILD_FOLDER
49 score, output = run_shell_command_with_output("make test", build_folder)
50 if score != 0 and not output:
51 self._comment = "No tests registered: Make sure you have tests registered in CMakeLists.txt."
52 return 0
53 if score != 0 and "No tests were found" in output:
54 self._comment = "No tests were found: Make sure you have tests registered in CMakeLists.txt."
55 return 0
56 return score
59class ClangFormatTestJob(EvaluationJob):
60 """Job for checking the code format."""
62 name = "Clang Format Check"
64 def _run(self, repo_path: Path) -> int:
65 return run_shell_command(
66 rf"find . -type f -regex '.*\.\(cpp\|hpp\|cu\|c\|cc\|h\)' -not -path '*/{HW_BUILD_FOLDER}/*' "
67 "| xargs clang-format --style=file -i --dry-run --Werror",
68 repo_path,
69 )
72class CodeCoverageTestJob(EvaluationJob):
73 """Job for checking the code coverage."""
75 name = "Code Coverage"
76 dependencies: ClassVar[list[EvaluationJob]] = [CMakeBuildJob(cmake_options="-DCMAKE_BUILD_TYPE=Debug")]
78 def __init__(self, weight: int = 1, min_coverage: int = 75) -> None:
79 super().__init__(weight)
80 self.__min_coverage = min_coverage
82 @staticmethod
83 def parse_total_coverage(coverage_file: Path) -> int:
84 coverage_file_pattern = r"TOTAL.*\s(\d*)%"
85 text = coverage_file.read_text()
86 coverage = re.search(coverage_file_pattern, text, re.DOTALL)
87 return int(coverage.group(1)) if coverage else 0
89 def _run(self, repo_path: Path) -> int:
90 coverage_file = repo_path.resolve() / HW_BUILD_FOLDER / "report.txt"
91 if (score := run_shell_command(f"gcovr -o {coverage_file}", repo_path)) == 0:
92 self._comment = "Coverage failed report generation failed."
93 return score
94 coverage = self.parse_total_coverage(coverage_file)
95 self._comment = f"Code coverage: {coverage}%. We require at least {self.__min_coverage}%."
96 return int(coverage > self.__min_coverage)
99class ClangTidyTestJob(EvaluationJob):
100 """Job for checking with clang tidy."""
102 name = "Clang Tidy Check"
104 def _run(self, repo_path: Path) -> int:
105 hw_cmake_module_path = repo_path / "hw_cmake"
106 hw_cmake_module_path.mkdir(parents=True, exist_ok=True)
107 copy_item(CMAKE_MODULE_PATH / "ClangTidy.cmake", hw_cmake_module_path / "ClangTidy.cmake")
108 cmake_lists = repo_path / CMAKELISTS_FILE_NAME
110 if not cmake_lists.exists():
111 self._comment = (
112 f"{CMAKELISTS_FILE_NAME} not found: Make sure you project has a {CMAKELISTS_FILE_NAME} file."
113 )
114 return 0
116 content = cmake_lists.read_text()
117 content += "\n"
118 content += f"list(APPEND CMAKE_MODULE_PATH ${{PROJECT_SOURCE_DIR}}/{hw_cmake_module_path.stem})\n"
119 content += "include(ClangTidy)\n"
120 cmake_lists.write_text(content)
121 score = CMakeBuildJob().run(repo_path)[-1].score
122 git.Repo(repo_path).git.restore(".") # Undo all changes
123 return score
126class CleanRepoJob(EvaluationJob):
127 """Job for checking if build files were committed."""
129 name = "Clean Repo Check"
131 class CleanRepoVisitor(FileVisitor):
132 """Check if build files are committed."""
134 def __init__(self) -> None:
135 self.__is_clean: bool = True
136 self.__build_folder = f"/{HW_BUILD_FOLDER}/"
137 self.__dirty_suffixes = [".make", ".includecache"]
138 self.__dirty_file_names = ["CMakeCache.txt", "cmake_install.cmake"]
139 self.__dirty_directories = ["/CMakeFiles/", ".dir/"]
141 @property
142 def is_clean(self) -> bool:
143 return self.__is_clean
145 def visit_file(self, file: Path) -> None:
146 if self.__build_folder in str(file):
147 return
148 self.__is_clean = self.__is_clean and (
149 file.name not in self.__dirty_file_names
150 and file.suffix not in self.__dirty_suffixes
151 and all(directory not in str(file) for directory in self.__dirty_directories)
152 )
154 class SourceFilesCountVisitor(FileVisitor):
155 """Count the number of source files."""
157 def __init__(self, max_source_file_count: int) -> None:
158 self.__max_source_file_count = max_source_file_count
159 self.__source_file_count = 0
160 self.__build_folder = f"/{HW_BUILD_FOLDER}/"
162 @property
163 def is_below_max_source_file_count(self) -> bool:
164 return self.__source_file_count < self.__max_source_file_count
166 def visit_file(self, file: Path) -> None:
167 if self.__build_folder in str(file):
168 return
169 self.__source_file_count += 1
171 def _run(self, repo_path: Path) -> int:
172 clean_repo_visitor = CleanRepoJob.CleanRepoVisitor()
173 source_file_count_visitor = CleanRepoJob.SourceFilesCountVisitor(100)
174 file_tree = FileTree(repo_path)
175 file_tree.accept(clean_repo_visitor)
176 file_tree.accept(source_file_count_visitor)
177 if not clean_repo_visitor.is_clean:
178 self._comment = (
179 "We found build files committed to the repository. "
180 "Make sure that any files produced by the build system are not committed. "
181 "If you already have, make sure you remove them."
182 )
183 return 0
184 if not source_file_count_visitor.is_below_max_source_file_count:
185 self._comment = (
186 "We found too many third party source files committed to the repository. "
187 "Check if you really need them or if there is another way to include them."
188 )
189 return 0
190 return 1