Coverage for sel_tools/code_evaluation/jobs/cpp.py: 98%
134 statements
« prev ^ index » next coverage.py v7.6.4, created at 2024-11-04 21:22 +0000
« prev ^ index » next coverage.py v7.6.4, created at 2024-11-04 21:22 +0000
1"""Cpp code evaluation jobs."""
3import re
4from pathlib import Path
5from typing import ClassVar
7from sel_tools.code_evaluation.jobs.common import (
8 EvaluationJob,
9 run_shell_command,
10 run_shell_command_with_output,
11)
12from sel_tools.config import CMAKE_MODULE_PATH, HW_BUILD_FOLDER
13from sel_tools.file_export.copy_item import copy_item
14from sel_tools.utils.config import CMAKELISTS_FILE_NAME
15from sel_tools.utils.files import FileTree, FileVisitor, is_cpp
18class CMakeBuildJob(EvaluationJob):
19 """Job for compiling the project."""
21 name = "CMake Build"
23 def __init__(self, weight: int = 1, cmake_options: str = "") -> None:
24 super().__init__(weight)
25 self.__cmake_options = cmake_options
27 def _run(self, repo_path: Path) -> int:
28 build_folder = repo_path / HW_BUILD_FOLDER
29 build_folder.mkdir(parents=True, exist_ok=True)
30 if run_shell_command(f"cmake {self.__cmake_options} ..", build_folder) == 0:
31 self._comment = f"CMake step failed with option {self.__cmake_options}"
32 return 0
33 if run_shell_command("make", build_folder) == 0:
34 self._comment = "Make step failed"
35 return 0
36 return 1
39class MakeTestJob(EvaluationJob):
40 """Job for running make test."""
42 name = "Make Test"
43 dependencies: ClassVar[list[EvaluationJob]] = [CMakeBuildJob()]
45 def _run(self, repo_path: Path) -> int:
46 build_folder = repo_path / HW_BUILD_FOLDER
47 score, output = run_shell_command_with_output("make test", build_folder)
48 if score != 0 and not output:
49 self._comment = "No tests registered"
50 return 0
51 if score != 0 and "No tests were found" in output:
52 self._comment = "No tests were found"
53 return 0
54 return score
57class ClangFormatTestJob(EvaluationJob):
58 """Job for checking the code format."""
60 name = "Clang Format Check"
62 def _run(self, repo_path: Path) -> int:
63 return run_shell_command(
64 rf"find . -type f -regex '.*\.\(cpp\|hpp\|cu\|c\|cc\|h\)' -not -path '*/{HW_BUILD_FOLDER}/*' "
65 "| xargs clang-format --style=file -i --dry-run --Werror",
66 repo_path,
67 )
70class CodeCoverageTestJob(EvaluationJob):
71 """Job for checking the code coverage."""
73 name = "Code Coverage"
74 dependencies: ClassVar[list[EvaluationJob]] = [CMakeBuildJob(cmake_options="-DCMAKE_BUILD_TYPE=Debug")]
76 def __init__(self, weight: int = 1, min_coverage: int = 75) -> None:
77 super().__init__(weight)
78 self.__min_coverage = min_coverage
80 @staticmethod
81 def parse_total_coverage(coverage_file: Path) -> int:
82 coverage_file_pattern = r"TOTAL.*\s(\d*)%"
83 text = coverage_file.read_text()
84 coverage = re.search(coverage_file_pattern, text, re.DOTALL)
85 return int(coverage.group(1)) if coverage else 0
87 def _run(self, repo_path: Path) -> int:
88 coverage_file = repo_path.resolve() / HW_BUILD_FOLDER / "report.txt"
89 if (score := run_shell_command(f"gcovr -o {coverage_file}", repo_path)) == 0:
90 self._comment = "Coverage failed"
91 return score
92 coverage = self.parse_total_coverage(coverage_file)
93 self._comment = f"Code coverage: {coverage}%"
94 return int(coverage > self.__min_coverage)
97class ClangTidyTestJob(EvaluationJob):
98 """Job for checking with clang tidy."""
100 name = "Clang Tidy Check"
102 def _run(self, repo_path: Path) -> int:
103 copy_item(CMAKE_MODULE_PATH, repo_path)
104 cmake_lists = repo_path / CMAKELISTS_FILE_NAME
106 if not cmake_lists.exists():
107 self._comment = f"{CMAKELISTS_FILE_NAME} not found"
108 return 0
110 content = cmake_lists.read_text()
111 content += "\n"
112 content += f"list(APPEND CMAKE_MODULE_PATH ${ PROJECT_SOURCE_DIR} /{CMAKE_MODULE_PATH.stem})\n"
113 content += "include(ClangTidy)\n"
114 cmake_lists.write_text(content)
115 return CMakeBuildJob().run(repo_path)[-1].score
118class OOPTestJob(EvaluationJob):
119 """Job for checking if all OOP is used."""
121 name = "OOP Check"
123 class OOPVisitor(FileVisitor):
124 """Check if given cpp files are OOP."""
126 def __init__(self) -> None:
127 self.__struct_usages: int = 0
129 @property
130 def is_oop(self) -> bool:
131 return self.__struct_usages == 0
133 def visit_file(self, file: Path) -> None:
134 if is_cpp(file):
135 cpp_content = file.read_text()
136 self.__struct_usages += self.find_struct_usages(cpp_content)
138 @staticmethod
139 def find_struct_usages(file_content: str) -> int:
140 # TODO this is probably a very weak check
141 # but we can improve in the future with more checks
142 return len(re.findall(r"\sstruct\s", file_content, re.DOTALL))
144 def _run(self, repo_path: Path) -> int:
145 oop_visitor = OOPTestJob.OOPVisitor()
146 FileTree(repo_path).accept(oop_visitor)
147 return int(oop_visitor.is_oop)
150class CleanRepoJob(EvaluationJob):
151 """Job for checking if build files were committed."""
153 name = "Clean Repo Check"
155 class CleanRepoVisitor(FileVisitor):
156 """Check if build files are committed."""
158 def __init__(self) -> None:
159 self.__is_clean: bool = True
160 self.__build_folder = f"/{HW_BUILD_FOLDER}/"
161 self.__dirty_suffixes = [".make", ".includecache"]
162 self.__dirty_file_names = ["CMakeCache.txt", "cmake_install.cmake"]
163 self.__dirty_directories = ["/CMakeFiles/", ".dir/"]
165 @property
166 def is_clean(self) -> bool:
167 return self.__is_clean
169 def visit_file(self, file: Path) -> None:
170 if self.__build_folder in str(file):
171 return
172 self.__is_clean = self.__is_clean and (
173 file.name not in self.__dirty_file_names
174 and file.suffix not in self.__dirty_suffixes
175 and all(directory not in str(file) for directory in self.__dirty_directories)
176 )
178 class SourceFilesCountVisitor(FileVisitor):
179 """Count the number of source files."""
181 def __init__(self, max_source_file_count: int) -> None:
182 self.__max_source_file_count = max_source_file_count
183 self.__source_file_count = 0
184 self.__build_folder = f"/{HW_BUILD_FOLDER}/"
186 @property
187 def is_below_max_source_file_count(self) -> bool:
188 return self.__source_file_count < self.__max_source_file_count
190 def visit_file(self, file: Path) -> None:
191 if self.__build_folder in str(file):
192 return
193 self.__source_file_count += 1
195 def _run(self, repo_path: Path) -> int:
196 clean_repo_visitor = CleanRepoJob.CleanRepoVisitor()
197 source_file_count_visitor = CleanRepoJob.SourceFilesCountVisitor(100)
198 file_tree = FileTree(repo_path)
199 file_tree.accept(clean_repo_visitor)
200 file_tree.accept(source_file_count_visitor)
201 if not clean_repo_visitor.is_clean:
202 self._message = "Build files committed"
203 return 0
204 if not source_file_count_visitor.is_below_max_source_file_count:
205 self._message = "Committed to many third party source file"
206 return 0
207 return 1