this repo has no description
1#! /usr/bin/env python3
2"""
3Audit automated tests by traversing our key subprojects and produces the following statistics:
4
5 number of source files
6 number of test suite files
7 number of test support files (i.e. non-test files like mocks, fakes, abstract base test suites, etc)
8 lines of text in each of the above categories (easier than source lines of code, and good enough for comparison)
9 the ratio of test lines to source lines
10
11Usage:
12
13From the repo root, call with
14
15 python direct-file/scripts/audit-tests.py
16
17"""
18import sys
19from abc import ABC, abstractmethod
20from dataclasses import dataclass
21from functools import cache
22from pathlib import Path
23from typing import TextIO
24
25
26@dataclass(frozen=True)
27class Report:
28 """A `Report` presents audit results."""
29 name: str
30 sources: list[Path]
31 source_lines: int
32 tests: list[Path]
33 test_lines: int
34 test_support: list[Path]
35 test_support_lines: int
36 all_test_lines_to_source_lines_percent: int
37
38 def print(self, output: TextIO):
39 output.write(self.name + ':\n')
40
41 output.write(f' {self.source_lines:7,} lines in {len(self.sources):5,} source files\n')
42 output.write(f' {self.test_lines:7,} lines in {len(self.tests):5,} test files\n')
43 output.write(f' {self.test_support_lines:7,} lines in {len(self.test_support):5,} test support files\n')
44 output.write(f' test to source code ratio: {self.all_test_lines_to_source_lines_percent:2}%\n')
45
46 output.write('\n')
47
48
49@dataclass(frozen=True)
50class Audit:
51 """An `Audit` gathers statistics from a `Project`."""
52 parent_dir: Path
53 project: 'Project'
54
55 def run(self) -> Report:
56 sources = self.project.find_sources(self.parent_dir)
57 source_lines = sum(
58 [count_lines(file) for file in sources]
59 )
60 tests = self.project.find_tests(self.parent_dir)
61 test_lines = sum(
62 [count_lines(file) for file in tests]
63 )
64 test_support = self.project.find_test_support_files(self.parent_dir)
65 test_support_lines = sum(
66 [count_lines(file) for file in test_support]
67 )
68
69 all_test_lines = test_lines + test_support_lines
70 if source_lines:
71 all_test_lines_to_source_lines_percent = int(
72 all_test_lines / source_lines * 100
73 )
74 else:
75 all_test_lines_to_source_lines_percent = 0
76
77 return Report(
78 self.project.child_dir.name,
79 sources,
80 source_lines,
81 tests,
82 test_lines,
83 test_support,
84 test_support_lines,
85 all_test_lines_to_source_lines_percent,
86 )
87
88
89@dataclass(frozen=True)
90class Project(ABC):
91 """A `Project` finds project source and test files."""
92 child_dir: Path
93
94 @abstractmethod
95 def find_sources(self, parent_dir: Path) -> list[Path]:
96 """Find source files."""
97 pass
98
99 @abstractmethod
100 def find_tests(self, parent_dir: Path) -> list[Path]:
101 """Find actual test files and test suites."""
102 pass
103
104 @abstractmethod
105 def find_test_support_files(self, parent_dir: Path) -> list[Path]:
106 """Find test support code (e.g. fakes, mocks, abstract tests, etc)."""
107 pass
108
109
110@dataclass(frozen=True)
111class MavenProject(Project):
112 @cache
113 def find_all_test_files(self, parent_dir: Path) -> list[Path]:
114 """Find all tests and test support files."""
115 project_root = parent_dir / self.child_dir
116 return find_files([project_root / 'src/test/java'], '**/*.java')
117
118 def find_sources(self, parent_dir: Path) -> list[Path]:
119 project_root = parent_dir / self.child_dir
120 return find_files([project_root / 'src/main/java'], '**/*.java')
121
122 def find_tests(self, parent_dir: Path) -> list[Path]:
123 all_test_files = self.find_all_test_files(parent_dir)
124 return [file for file in all_test_files if is_java_test(file)]
125
126 def find_test_support_files(self, parent_dir: Path) -> list[Path]:
127 all_test_files = self.find_all_test_files(parent_dir)
128 return [file for file in all_test_files if not is_java_test(file)]
129
130
131@dataclass(frozen=True)
132class NPMProject(Project):
133 @cache
134 def find_all_files(self, parent_dir: Path) -> list[Path]:
135 """Find all source and test files."""
136 project_root = parent_dir / self.child_dir
137 return find_files([project_root / 'src'], '**/*.ts*')
138
139 def find_sources(self, parent_dir: Path) -> list[Path]:
140 all_files = self.find_all_files(parent_dir)
141 return [file for file in all_files if not is_typescript_test(file)]
142
143 def find_tests(self, parent_dir: Path) -> list[Path]:
144 all_files = self.find_all_files(parent_dir)
145 return [file for file in all_files if is_typescript_test(file)]
146
147 def find_test_support_files(self, parent_dir: Path) -> list[Path]:
148 all_files = self.find_all_files(parent_dir)
149 return [file for file in all_files if is_typescript_test_support(file)]
150
151
152@dataclass(frozen=True)
153class SBTProject(Project):
154 @cache
155 def find_all_test_files(self, parent_dir: Path) -> list[Path]:
156 """Find all tests and test support files."""
157 project_root = parent_dir / self.child_dir
158 return find_files([project_root / 'shared/src/test/scala'], '**/*.scala')
159
160 def find_sources(self, parent_dir: Path) -> list[Path]:
161 project_root = parent_dir / self.child_dir
162 return find_files([project_root / 'shared/src/main/scala'], '**/*.scala')
163
164 def find_tests(self, parent_dir: Path) -> list[Path]:
165 all_test_files = self.find_all_test_files(parent_dir)
166 return [file for file in all_test_files if is_scala_test(file)]
167
168 def find_test_support_files(self, parent_dir: Path) -> list[Path]:
169 all_test_files = self.find_all_test_files(parent_dir)
170 return [file for file in all_test_files if not is_scala_test(file)]
171
172
173PROJECTS = [
174 MavenProject(Path('backend')),
175 NPMProject(Path('df-client/df-client-app')),
176 # TODO: projects in df-client/
177 MavenProject(Path('email-service')),
178 SBTProject(Path('fact-graph-scala')),
179 # TODO: libs/
180 # TODO: state-api/
181 MavenProject(Path('status')),
182 MavenProject(Path('submit')),
183 # TODO: projects in utils/
184]
185
186
187def count_lines(file: Path) -> int:
188 count = 0
189 with file.open() as f:
190 for _ in f:
191 count += 1
192 return count
193
194
195def find_files(roots: list[Path], glob: str) -> list[Path]:
196 files = []
197 for directory in roots:
198 files += [
199 file for file in directory.glob(glob) if file.is_file()
200 ]
201 files.sort()
202 return files
203
204
205def find_parent_dir() -> Path:
206 script = Path(__file__)
207 if (not script.parts[-2] == 'scripts'
208 or not script.parts[-3] == 'direct-file'):
209 sys.exit(
210 f"Expected '{script.name}' to live in 'direct-file/scripts/' but it's located in '{script}'\n"
211 )
212 return script.parent.parent.resolve()
213
214
215def is_java_test(path: Path) -> bool:
216 return (
217 path.name.endswith('Test.java')
218 and not path.name.startswith('Base')
219 )
220
221
222def is_scala_test(path: Path) -> bool:
223 return path.name.endswith('Spec.scala')
224
225
226def is_typescript_test(path: Path) -> bool:
227 return (
228 path.name.endswith('.test.ts')
229 or path.name.endswith('.test.tsx')
230 )
231
232
233TYPESCRIPT_TEST_SUPPORT = {
234 'factgraphTestHelpers.tsx',
235 'test-utils.tsx',
236}
237
238
239def is_typescript_test_support(path: Path) -> bool:
240 if path.name in TYPESCRIPT_TEST_SUPPORT:
241 return True
242 else:
243 return 'test' in path.parts and not is_typescript_test(path)
244
245
246def main():
247 output = sys.stdout
248 parent_dir = find_parent_dir()
249 for project in PROJECTS:
250 audit = Audit(parent_dir, project)
251 report = audit.run()
252 report.print(output)
253
254
255if __name__ == '__main__':
256 main()