this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 256 lines 7.9 kB view raw
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()