Skip to content

Commit 2fd53de

Browse files
committed
[feature] Трекинг-файл системы модульного тестирования
Добавлен трекинг-файл TestCoverageTracker.md с перечнем файлов тестовой системы, требующих доработки (Раздел A) и создания (Раздел B) модульных тестов. Состав: - Раздел A: 89 существующих тестовых файлов, отсортированных по числу тест-методов (приоритет доработки для файлов с малым покрытием) - Раздел B: 485 логических модулей с публичным API без тестов - Общая статистика покрытия по проекту Tests/MathCore.Tests (MSTest) - Скрипт перегенерации .scripts/analyze_test_coverage.py Учтены нестандартные тестовые файлы ([TestClass] без суффикса Tests), частичные классы (Complex, Matrix, StringPtr, Polynom) и исключены маркерные типы.
1 parent 2743b50 commit 2fd53de

2 files changed

Lines changed: 750 additions & 0 deletions

File tree

‎.scripts/analyze_test_coverage.py‎

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
"""Построение трекинг-файла TestCoverageTracker.md (система модульного тестирования MathCore)."""
2+
import re
3+
from pathlib import Path
4+
5+
ROOT = Path(r"D:\src\lib\MathCore\MathCore")
6+
SRC_DIR = ROOT / "MathCore"
7+
TEST_DIR = ROOT / "Tests" / "MathCore.Tests"
8+
OUT = ROOT / "TestCoverageTracker.md"
9+
10+
# Класс с атрибутом [TestClass]: атрибут на предыдущих строках, объявление class/record-класса
11+
TEST_CLASS_RE = re.compile(
12+
r'\[TestClass\]\s*'
13+
r'(?:(?:static|sealed|abstract|partial|public|internal|private|protected)\s+)*'
14+
r'(?:class|record\s+class|record\s+struct)\s+([A-Za-z_][A-Za-z0-9_]*)',
15+
re.MULTILINE)
16+
METHOD_RE = re.compile(r'\[(?:TestMethod|DataTestMethod|DataRow|DynamicData)\b')
17+
18+
# ================= 1. Тестовые файлы и классы =================
19+
test_files = [] # (rel, n_methods, is_strict_tests)
20+
test_classes = set()
21+
for path in TEST_DIR.rglob("*.cs"):
22+
if "\\bin\\" in str(path) or "\\obj\\" in str(path):
23+
continue
24+
rel = path.relative_to(ROOT).as_posix()
25+
text = path.read_text(encoding="utf-8", errors="ignore")
26+
n_methods = len(METHOD_RE.findall(text))
27+
is_strict = path.name.endswith("Tests.cs")
28+
has_test_class = bool(TEST_CLASS_RE.search(text))
29+
for m in TEST_CLASS_RE.finditer(text):
30+
test_classes.add(m.group(1))
31+
# считаем файл тестовым, если он строгий тест ИЛИ содержит test-класс с методами
32+
if n_methods > 0 and (is_strict or has_test_class):
33+
test_files.append((rel, n_methods))
34+
35+
# Покрываемые имена типов из тестовых классов
36+
covered_type_names = set()
37+
for tc in test_classes:
38+
covered_type_names.add(re.sub(r'Tests$', '', tc))
39+
covered_type_names.add(tc)
40+
41+
MANUAL_COVERED = {
42+
'Complex', 'Matrix', 'MatrixArray', 'MatrixComplex', 'MatrixDecimal', 'MatrixFloat',
43+
'MatrixInt', 'MatrixLong', 'MatrixN', 'Polynom', 'StringPtr', 'Randomizer',
44+
'RandomNormal', 'RungeKutta', 'RungeKuttaVector2', 'RungeKuttaVector3',
45+
'RungeKuttaMatrix', 'RungeKuttaComplex', 'Euler', 'EulerModified',
46+
'Levenshtein', 'NamedLock', 'Messenger', 'Maybe', 'FList', 'EnumExtensions',
47+
}
48+
covered_type_names |= MANUAL_COVERED
49+
50+
# ================= 2. Исходные модули =================
51+
PUBLIC_CLASS_RE = re.compile(
52+
r'^\s*public\s+(?:static\s+|sealed\s+|abstract\s+|readonly\s+|partial\s+)*'
53+
r'(?:class|struct|interface|enum)\s+([A-Za-z_][A-Za-z0-9_]*)', re.MULTILINE)
54+
RECORD_RE = re.compile(
55+
r'^\s*public\s+(?:sealed\s+|abstract\s+|partial\s+)*record\s+(?:class\s+|struct\s+)?'
56+
r'([A-Za-z_][A-Za-z0-9_]*)', re.MULTILINE)
57+
TRIVIAL = re.compile(r'(Attribute|Annotations|\.tt\.cs)$|^(Service/|Properties/)', re.I)
58+
59+
src_modules = {} # logical -> (rel, types)
60+
for path in SRC_DIR.rglob("*.cs"):
61+
if "\\bin\\" in str(path) or "\\obj\\" in str(path):
62+
continue
63+
rel = path.relative_to(ROOT).as_posix()
64+
if TRIVIAL.search(rel):
65+
continue
66+
text = path.read_text(encoding="utf-8", errors="ignore")
67+
types = [m.group(1) for m in PUBLIC_CLASS_RE.finditer(text)] + [m.group(1) for m in RECORD_RE.finditer(text)]
68+
if not types:
69+
continue
70+
if all('Attribute' in t for t in types):
71+
continue
72+
logical = path.stem.split('.')[0]
73+
src_modules[logical] = (rel, types)
74+
75+
def covered(logical, types):
76+
if logical in covered_type_names:
77+
return True
78+
for t in types:
79+
base = t.split('<')[0]
80+
if base in covered_type_names:
81+
return True
82+
if (base + 'Tests') in test_classes:
83+
return True
84+
return False
85+
86+
covered_mods = []
87+
uncovered_mods = []
88+
for logical, (rel, types) in src_modules.items():
89+
(covered_mods if covered(logical, types) else uncovered_mods).append((logical, rel, types))
90+
91+
# ================= 3. Генерация =================
92+
total_methods = sum(x[1] for x in test_files)
93+
test_files_sorted = sorted(test_files, key=lambda x: (x[1], x[0].lower()))
94+
for i, (rel, n) in enumerate(test_files_sorted, 1):
95+
pass
96+
97+
sb = []
98+
sb.append('# Трекинг-файл: система модульного тестирования MathCore')
99+
sb.append('')
100+
sb.append('> Перечень файлов тестовой системы проекта **MathCore**, требующих **добавления** или **доработки**.')
101+
sb.append('>')
102+
sb.append('> Сводка построена автоматически по проекту `Tests/MathCore.Tests` (MSTest). Учтены файлы `*Tests.cs` и файлы с тест-классами `[TestClass]`.')
103+
sb.append('')
104+
sb.append('## Общая статистика')
105+
sb.append('')
106+
sb.append(f'- **Тестовых файлов:** {len(test_files)}')
107+
sb.append(f'- **Всего тест-методов / атрибутов тестов:** {total_methods}')
108+
sb.append(f'- **Логических модулей с публичным API:** {len(src_modules)}')
109+
sb.append(f' - покрыто тестами: {len(covered_mods)}')
110+
sb.append(f' - **без тестов:** {len(uncovered_mods)}')
111+
sb.append('')
112+
sb.append('## Условные обозначения статуса')
113+
sb.append('')
114+
sb.append('- **🟡 Нужно доработать** — тестовый файл существует, но покрытие недостаточно (мало тест-методов)')
115+
sb.append('- **⬜ Нужно создать** — для логического модуля тесты отсутствуют')
116+
sb.append('- **✅ Готово** — тесты покрывают модуль (заполняется вручную)')
117+
sb.append('')
118+
119+
sb.append('## Раздел A. Существующие тестовые файлы, требующие доработки')
120+
sb.append('')
121+
sb.append('> По возрастанию числа тест-методов: чем меньше, тем выше приоритет доработки.')
122+
sb.append('')
123+
sb.append('| # | Тестовый файл | Тест-методов | Статус |')
124+
sb.append('|---|---------------|:---:|--------|')
125+
for i, (rel, n) in enumerate(test_files_sorted, 1):
126+
disp = rel.replace('Tests/MathCore.Tests/', '')
127+
sb.append(f'| {i} | `{disp}` | {n} | 🟡 Нужно доработать |')
128+
129+
sb.append('## Раздел B. Логические модули без тестов (нужно создать)')
130+
sb.append('')
131+
sb.append(f'> {len(uncovered_mods)} логических модулей с публичным API без тестов в `Tests/MathCore.Tests`.')
132+
sb.append('')
133+
sb.append('| # | Логический модуль | Исходный файл (пример) | Статус |')
134+
sb.append('|---|--------------------|------------------------|--------|')
135+
for i, (logical, rel, types) in enumerate(sorted(uncovered_mods, key=lambda x: x[0].lower()), 1):
136+
disp_rel = rel.replace('MathCore/', '')
137+
sb.append(f'| {i} | `{logical}` | `{disp_rel}` | ⬜ Нужно создать |')
138+
sb.append('')
139+
140+
OUT.write_text('\n'.join(sb), encoding='utf-8')
141+
print('OK')
142+
print('Тестовых файлов:', len(test_files))
143+
print('Тестовых классов:', len(test_classes))
144+
print('Модулей покрыто:', len(covered_mods), '| без тестов:', len(uncovered_mods))

0 commit comments

Comments
 (0)