@freshworks/shiftleft-tools 1.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +351 -0
- package/bin/shiftleft.js +95 -0
- package/package.json +57 -0
- package/src/commands/doctor.js +208 -0
- package/src/commands/init-postman.js +298 -0
- package/src/commands/init-rules.js +78 -0
- package/src/commands/link.js +172 -0
- package/src/commands/protect.js +61 -0
- package/src/commands/run-tests.js +182 -0
- package/src/commands/setup-pipeline.js +209 -0
- package/src/commands/update.js +203 -0
- package/src/index.js +4 -0
- package/src/utils/copy-tree.js +98 -0
- package/src/utils/gitignore.js +26 -0
- package/src/utils/logger.js +9 -0
- package/src/utils/manifest.js +145 -0
- package/src/utils/stack.js +80 -0
- package/src/utils/template.js +135 -0
- package/templates/AGENTS.md +109 -0
- package/templates/CLAUDE.md +3 -0
- package/templates/jenkins/Jenkinsfile-java.groovy +432 -0
- package/templates/jenkins/Jenkinsfile-node.groovy +450 -0
- package/templates/postman/.husky/pre-commit +19 -0
- package/templates/postman/.prettierrc.json +5 -0
- package/templates/postman/README.md.ejs +147 -0
- package/templates/postman/collections/01-core.json.ejs +91 -0
- package/templates/postman/config/local.json.ejs +12 -0
- package/templates/postman/config/staging.json.ejs +26 -0
- package/templates/postman/environments/local.postman_environment.json.ejs +31 -0
- package/templates/postman/environments/staging.postman_environment.json.ejs +31 -0
- package/templates/postman/gitignore +16 -0
- package/templates/postman/npmrc +31 -0
- package/templates/postman/package.json.ejs +66 -0
- package/templates/postman/run-all-shim.sh +16 -0
- package/templates/postman/scripts/auth/generate-jwt.sh +113 -0
- package/templates/postman/scripts/auth/get-issuer-secret.sh +140 -0
- package/templates/postman/scripts/infra/start-mocks.sh +138 -0
- package/templates/postman/scripts/infra/stop-mocks.sh +43 -0
- package/templates/postman/scripts/lib/api_coverage.py +1122 -0
- package/templates/postman/scripts/lib/cleanup-reports.sh +101 -0
- package/templates/postman/scripts/lib/cleanup-stryker.sh +44 -0
- package/templates/postman/scripts/lib/report_combined.py +527 -0
- package/templates/postman/scripts/lib/report_consolidated.py +363 -0
- package/templates/postman/scripts/lib/report_generator.py +121 -0
- package/templates/postman/scripts/lib/report_migration.py +156 -0
- package/templates/postman/scripts/lib/report_mutation.py +110 -0
- package/templates/postman/scripts/lib/report_unit.py +353 -0
- package/templates/postman/scripts/lib/report_utils.py +973 -0
- package/templates/postman/scripts/report-generators/generate-consolidated-report.sh +445 -0
- package/templates/postman/scripts/report-generators/java-api-coverage-matrix.sh +257 -0
- package/templates/postman/scripts/report-generators/mutation-report.sh +672 -0
- package/templates/postman/scripts/report-generators/node-api-coverage-matrix.sh +167 -0
- package/templates/postman/scripts/report-generators/stage-report-artifacts.sh +27 -0
- package/templates/postman/scripts/run-all.sh +452 -0
- package/templates/postman/scripts/runners/run-mutation-tests.sh +113 -0
- package/templates/postman/scripts/runners/run-tests-local.sh +936 -0
- package/templates/postman/scripts/runners/run-tests-staging.sh +741 -0
- package/templates/postman-node/README.md.ejs +26 -0
- package/templates/postman-node/collections/crud/01-bootstrap.json.ejs +34 -0
- package/templates/postman-node/config/local.json.ejs +46 -0
- package/templates/postman-node/config/staging.json.ejs +31 -0
- package/templates/postman-node/local.test.env.ejs +3 -0
- package/templates/postman-node/mocks/external.js +14 -0
- package/templates/postman-node/package.json.ejs +39 -0
- package/templates/postman-node/requirements.txt +1 -0
- package/templates/postman-node/scripts/database/cleanup-mysql.sh +12 -0
- package/templates/postman-node/scripts/database/run-migrations.js +29 -0
- package/templates/postman-node/scripts/database/start-mysql.sh +34 -0
- package/templates/postman-node/scripts/database/wait-for-mysql.sh +36 -0
- package/templates/postman-node/scripts/lib/api_coverage_node.py +1137 -0
- package/templates/postman-node/scripts/lib/fetch-jwt.sh +86 -0
- package/templates/postman-node/scripts/lib/run-newman.sh +104 -0
- package/templates/postman-node/scripts/lib/setup-database.sh +55 -0
- package/templates/postman-node/scripts/lib/start-app.sh +48 -0
- package/templates/postman-node/scripts/lib/utils.sh +114 -0
- package/templates/postman-node/scripts/report-generators/stage-report-artifacts.sh +26 -0
- package/templates/postman-node/scripts/run-all.sh +303 -0
- package/templates/postman-node/scripts/runners/run-tests.sh +123 -0
- package/templates/postman-node/scripts/setup-mocks.js.ejs +29 -0
- package/templates/postman-node/stryker.config.js.ejs +51 -0
- package/templates/rules/local-test-setup.mdc +420 -0
- package/templates/rules/testing-node.mdc +66 -0
- package/templates/rules/testing.mdc +248 -0
- package/templates/skills/_shared/postman-standards.md +380 -0
- package/templates/skills/enhance-test-pipeline/SKILL-java.md +483 -0
- package/templates/skills/enhance-test-pipeline/SKILL-node.md +431 -0
- package/templates/skills/enhance-test-pipeline/SKILL.md +9 -0
- package/templates/skills/review-test-suite/SKILL-java.md +137 -0
- package/templates/skills/review-test-suite/SKILL-node.md +78 -0
- package/templates/skills/review-test-suite/SKILL.md +9 -0
- package/templates/skills/run-test-suite/SKILL-java.md +186 -0
- package/templates/skills/run-test-suite/SKILL-node.md +191 -0
- package/templates/skills/run-test-suite/SKILL.md +9 -0
- package/templates/skills/setup-api-tests/SKILL-java.md +1094 -0
- package/templates/skills/setup-api-tests/SKILL-node.md +141 -0
- package/templates/skills/setup-api-tests/SKILL.md +9 -0
- package/templates/skills/setup-mutation-tests/SKILL-java.md +303 -0
- package/templates/skills/setup-mutation-tests/SKILL-node.md +408 -0
- package/templates/skills/setup-mutation-tests/SKILL.md +9 -0
- package/templates/skills/setup-test-pipeline/SKILL-java.md +454 -0
- package/templates/skills/setup-test-pipeline/SKILL-node.md +318 -0
- package/templates/skills/setup-test-pipeline/SKILL.md +9 -0
- package/templates/skills/write-api-tests/SKILL-java.md +115 -0
- package/templates/skills/write-api-tests/SKILL-node.md +83 -0
- package/templates/skills/write-api-tests/SKILL.md +9 -0
- package/templates/stryker.config.js +50 -0
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Combined quality report generator — Unit Tests + API Tests in top-level tabs.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import glob
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from report_utils import (
|
|
9
|
+
get_html_template, build_css_tabs, report_detail_links_row,
|
|
10
|
+
resolve_jacoco_href, resolve_pit_href, resolve_sibling_report_href,
|
|
11
|
+
format_mutator_name,
|
|
12
|
+
)
|
|
13
|
+
from report_unit import parse_surefire_reports, parse_jacoco_report, parse_mutation_data
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def generate_combined_report(args):
|
|
17
|
+
output_html = args.output_file
|
|
18
|
+
|
|
19
|
+
surefire = parse_surefire_reports(args.surefire_dir) if args.surefire_dir and os.path.isdir(args.surefire_dir) else None
|
|
20
|
+
jacoco = parse_jacoco_report(args.jacoco_xml) if args.jacoco_xml and os.path.isfile(args.jacoco_xml) else None
|
|
21
|
+
mutation = parse_mutation_data(args.mutations_xml) if args.mutations_xml and os.path.isfile(args.mutations_xml) else None
|
|
22
|
+
|
|
23
|
+
api_coverage = None
|
|
24
|
+
if args.api_coverage_file and os.path.isfile(args.api_coverage_file):
|
|
25
|
+
with open(args.api_coverage_file, 'r') as f:
|
|
26
|
+
api_coverage = json.load(f)
|
|
27
|
+
|
|
28
|
+
postman_total = getattr(args, 'postman_assertions_total', 0) or 0
|
|
29
|
+
postman_passed = getattr(args, 'postman_assertions_passed', 0) or 0
|
|
30
|
+
postman_failed = getattr(args, 'postman_assertions_failed', 0) or 0
|
|
31
|
+
postman_requests = getattr(args, 'postman_requests', 0) or 0
|
|
32
|
+
postman_duration = getattr(args, 'postman_duration', '0') or '0'
|
|
33
|
+
postman_col_passed = getattr(args, 'postman_collections_passed', 0) or 0
|
|
34
|
+
postman_col_failed = getattr(args, 'postman_collections_failed', 0) or 0
|
|
35
|
+
|
|
36
|
+
test_pass_rate = 0
|
|
37
|
+
if surefire and surefire['tests'] > 0:
|
|
38
|
+
test_pass_rate = (surefire['passed'] / surefire['tests']) * 100
|
|
39
|
+
|
|
40
|
+
line_coverage = 0
|
|
41
|
+
if jacoco:
|
|
42
|
+
total_lines = jacoco['line_covered'] + jacoco['line_missed']
|
|
43
|
+
if total_lines > 0:
|
|
44
|
+
line_coverage = (jacoco['line_covered'] / total_lines) * 100
|
|
45
|
+
|
|
46
|
+
branch_coverage = 0
|
|
47
|
+
if jacoco:
|
|
48
|
+
total_branches = jacoco['branch_covered'] + jacoco['branch_missed']
|
|
49
|
+
if total_branches > 0:
|
|
50
|
+
branch_coverage = (jacoco['branch_covered'] / total_branches) * 100
|
|
51
|
+
|
|
52
|
+
mutation_score = 0
|
|
53
|
+
if mutation and mutation['total'] > 0:
|
|
54
|
+
mutation_score = (mutation['killed'] / mutation['total']) * 100
|
|
55
|
+
|
|
56
|
+
api_pass_rate = (postman_passed / postman_total * 100) if postman_total > 0 else 0
|
|
57
|
+
api_coverage_pct = api_coverage.get('coverage_percent', 0) if api_coverage else 0
|
|
58
|
+
|
|
59
|
+
all_tests_pass = surefire and surefire['failures'] == 0 and surefire['errors'] == 0
|
|
60
|
+
good_coverage = line_coverage >= 70
|
|
61
|
+
good_mutation = mutation_score >= 70
|
|
62
|
+
api_tests_pass = postman_failed == 0
|
|
63
|
+
|
|
64
|
+
if all_tests_pass and good_coverage and good_mutation and api_tests_pass:
|
|
65
|
+
health = {'class': 'excellent', 'icon': '🟢', 'label': 'Healthy'}
|
|
66
|
+
elif all_tests_pass and api_tests_pass and (good_coverage or good_mutation):
|
|
67
|
+
health = {'class': 'good', 'icon': '🟡', 'label': 'Good'}
|
|
68
|
+
else:
|
|
69
|
+
health = {'class': 'poor', 'icon': '🔴', 'label': 'Needs Attention'}
|
|
70
|
+
|
|
71
|
+
content = '<div class="summary-grid">'
|
|
72
|
+
|
|
73
|
+
if surefire:
|
|
74
|
+
card_class = 'success' if all_tests_pass else 'error'
|
|
75
|
+
content += f'''
|
|
76
|
+
<div class="summary-card {card_class}">
|
|
77
|
+
<div class="value">{surefire['passed']}/{surefire['tests']}</div>
|
|
78
|
+
<div class="label">Unit Tests</div>
|
|
79
|
+
<div class="detail">{surefire['time']:.1f}s | {surefire['skipped']} skipped</div>
|
|
80
|
+
</div>'''
|
|
81
|
+
else:
|
|
82
|
+
content += '''
|
|
83
|
+
<div class="summary-card">
|
|
84
|
+
<div class="value" style="font-size:1.25rem;color:var(--color-muted);">N/A</div>
|
|
85
|
+
<div class="label">Unit Tests</div>
|
|
86
|
+
<div class="detail">No data available</div>
|
|
87
|
+
</div>'''
|
|
88
|
+
|
|
89
|
+
if jacoco:
|
|
90
|
+
card_class = 'success' if line_coverage >= 80 else 'warning' if line_coverage >= 60 else 'error'
|
|
91
|
+
content += f'''
|
|
92
|
+
<div class="summary-card {card_class}">
|
|
93
|
+
<div class="value">{line_coverage:.1f}%</div>
|
|
94
|
+
<div class="label">Line Coverage</div>
|
|
95
|
+
<div class="detail">{jacoco['line_covered']:,} / {jacoco['line_covered'] + jacoco['line_missed']:,} lines</div>
|
|
96
|
+
</div>'''
|
|
97
|
+
else:
|
|
98
|
+
content += '''
|
|
99
|
+
<div class="summary-card">
|
|
100
|
+
<div class="value" style="font-size:1.25rem;color:var(--color-muted);">N/A</div>
|
|
101
|
+
<div class="label">Line Coverage</div>
|
|
102
|
+
<div class="detail">No data available</div>
|
|
103
|
+
</div>'''
|
|
104
|
+
|
|
105
|
+
if mutation:
|
|
106
|
+
card_class = 'success' if mutation_score >= 80 else 'warning' if mutation_score >= 60 else 'error'
|
|
107
|
+
content += f'''
|
|
108
|
+
<div class="summary-card {card_class}">
|
|
109
|
+
<div class="value">{mutation_score:.1f}%</div>
|
|
110
|
+
<div class="label">Mutation Score</div>
|
|
111
|
+
<div class="detail">{mutation['killed']:,} / {mutation['total']:,} killed</div>
|
|
112
|
+
</div>'''
|
|
113
|
+
else:
|
|
114
|
+
content += '''
|
|
115
|
+
<div class="summary-card">
|
|
116
|
+
<div class="value" style="font-size:1.25rem;color:var(--color-muted);">N/A</div>
|
|
117
|
+
<div class="label">Mutation Score</div>
|
|
118
|
+
<div class="detail">No data available</div>
|
|
119
|
+
</div>'''
|
|
120
|
+
|
|
121
|
+
if postman_total > 0:
|
|
122
|
+
card_class = 'success' if postman_failed == 0 else 'error'
|
|
123
|
+
content += f'''
|
|
124
|
+
<div class="summary-card {card_class}">
|
|
125
|
+
<div class="value">{postman_passed}/{postman_total}</div>
|
|
126
|
+
<div class="label">API Assertions</div>
|
|
127
|
+
<div class="detail">{postman_col_passed + postman_col_failed} collections | {postman_requests} requests</div>
|
|
128
|
+
</div>'''
|
|
129
|
+
else:
|
|
130
|
+
content += '''
|
|
131
|
+
<div class="summary-card">
|
|
132
|
+
<div class="value" style="font-size:1.25rem;color:var(--color-muted);">N/A</div>
|
|
133
|
+
<div class="label">API Assertions</div>
|
|
134
|
+
<div class="detail">No data available</div>
|
|
135
|
+
</div>'''
|
|
136
|
+
|
|
137
|
+
if api_coverage:
|
|
138
|
+
card_class = 'success' if api_coverage_pct >= 80 else 'warning' if api_coverage_pct >= 60 else 'error'
|
|
139
|
+
content += f'''
|
|
140
|
+
<div class="summary-card {card_class}">
|
|
141
|
+
<div class="value">{api_coverage_pct:.0f}%</div>
|
|
142
|
+
<div class="label">API Coverage</div>
|
|
143
|
+
<div class="detail">{api_coverage.get('tested', 0)}/{api_coverage.get('total', 0)} endpoints with 2xx</div>
|
|
144
|
+
</div>'''
|
|
145
|
+
else:
|
|
146
|
+
content += '''
|
|
147
|
+
<div class="summary-card">
|
|
148
|
+
<div class="value" style="font-size:1.25rem;color:var(--color-muted);">N/A</div>
|
|
149
|
+
<div class="label">API Coverage</div>
|
|
150
|
+
<div class="detail">No data available</div>
|
|
151
|
+
</div>'''
|
|
152
|
+
|
|
153
|
+
content += '</div>'
|
|
154
|
+
|
|
155
|
+
jacoco_href = resolve_jacoco_href(output_html, args.jacoco_xml) if jacoco else None
|
|
156
|
+
pit_href = resolve_pit_href(output_html, args.mutations_xml) if mutation else None
|
|
157
|
+
|
|
158
|
+
unit_content = report_detail_links_row([
|
|
159
|
+
('Detailed Coverage Analysis', 'JaCoCo Coverage Report', jacoco_href),
|
|
160
|
+
('Detailed Mutation Analysis', 'PIT Mutation Report', pit_href),
|
|
161
|
+
])
|
|
162
|
+
|
|
163
|
+
unit_content += build_css_tabs('unit', [
|
|
164
|
+
('results', 'Test Results', _build_unit_test_results(surefire)),
|
|
165
|
+
('coverage', 'Coverage Analysis', _build_unit_coverage(jacoco)),
|
|
166
|
+
('mutations', 'Mutation Testing', _build_unit_mutations(mutation)),
|
|
167
|
+
('survived', 'Survived Mutations', _build_unit_survived(mutation)),
|
|
168
|
+
])
|
|
169
|
+
|
|
170
|
+
api_coverage_html_path = getattr(args, 'api_coverage_html', None)
|
|
171
|
+
postman_consolidated_html_path = getattr(args, 'postman_consolidated_html', None)
|
|
172
|
+
postman_href = resolve_sibling_report_href(output_html, postman_consolidated_html_path)
|
|
173
|
+
api_coverage_href = resolve_sibling_report_href(output_html, api_coverage_html_path)
|
|
174
|
+
|
|
175
|
+
api_content = report_detail_links_row([
|
|
176
|
+
('Detailed API Test Results', 'Postman Consolidated Report', postman_href),
|
|
177
|
+
('Detailed API Coverage Analysis', 'API Coverage Matrix Report', api_coverage_href),
|
|
178
|
+
])
|
|
179
|
+
|
|
180
|
+
postman_collections = _load_postman_collections(args)
|
|
181
|
+
products_present = sorted(set(c['product'] for c in postman_collections))
|
|
182
|
+
real_products = [p for p in products_present if p and p != 'default']
|
|
183
|
+
is_multi_product = len(real_products) > 1
|
|
184
|
+
|
|
185
|
+
api_results_html = _build_api_results_html(
|
|
186
|
+
postman_collections, real_products, is_multi_product,
|
|
187
|
+
postman_total, postman_passed, postman_failed,
|
|
188
|
+
postman_requests, postman_duration, postman_col_passed, postman_col_failed
|
|
189
|
+
)
|
|
190
|
+
api_coverage_detail_html = _build_api_coverage_detail(api_coverage)
|
|
191
|
+
api_gaps_html = _build_api_gaps(api_coverage)
|
|
192
|
+
|
|
193
|
+
api_content += build_css_tabs('api', [
|
|
194
|
+
('results', 'API Results', api_results_html),
|
|
195
|
+
('coverage', 'Coverage Summary', api_coverage_detail_html),
|
|
196
|
+
('gaps', 'Gaps Analysis', api_gaps_html),
|
|
197
|
+
])
|
|
198
|
+
|
|
199
|
+
content += '<div class="section">'
|
|
200
|
+
content += build_css_tabs('top', [
|
|
201
|
+
('unit', 'Unit Tests', unit_content),
|
|
202
|
+
('api', 'API Tests', api_content),
|
|
203
|
+
])
|
|
204
|
+
content += '</div>'
|
|
205
|
+
|
|
206
|
+
logo = getattr(args, 'logo', 'Quality Reports')
|
|
207
|
+
env = getattr(args, 'env', None)
|
|
208
|
+
timestamp = getattr(args, 'timestamp', None)
|
|
209
|
+
subtitle = f"{env} environment • {timestamp}" if env and timestamp else None
|
|
210
|
+
html = get_html_template(
|
|
211
|
+
title="Quality Report",
|
|
212
|
+
logo=logo,
|
|
213
|
+
content=content,
|
|
214
|
+
health_badge=health,
|
|
215
|
+
subtitle=subtitle,
|
|
216
|
+
footer_text="Unit Tests + Code Coverage + Mutation Testing + API Integration Tests"
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
with open(args.output_file, 'w') as f:
|
|
220
|
+
f.write(html)
|
|
221
|
+
|
|
222
|
+
print(f"Combined Quality Report generated: {args.output_file}")
|
|
223
|
+
if surefire:
|
|
224
|
+
print(f" Unit Tests: {surefire['passed']}/{surefire['tests']} passed ({test_pass_rate:.1f}%)")
|
|
225
|
+
if jacoco:
|
|
226
|
+
print(f" Line Coverage: {line_coverage:.1f}%")
|
|
227
|
+
if mutation:
|
|
228
|
+
print(f" Mutation Score: {mutation_score:.1f}%")
|
|
229
|
+
if postman_total > 0:
|
|
230
|
+
print(f" API Assertions: {postman_passed}/{postman_total} passed ({api_pass_rate:.1f}%)")
|
|
231
|
+
if api_coverage:
|
|
232
|
+
print(f" API Coverage: {api_coverage_pct:.1f}%")
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _load_postman_collections(args):
|
|
236
|
+
def _product_slug(name):
|
|
237
|
+
return name.lower().replace(' ', '-').replace('_', '-')
|
|
238
|
+
|
|
239
|
+
def _parse_collection(jf, product_slug):
|
|
240
|
+
with open(jf, 'r') as f:
|
|
241
|
+
data = json.load(f)
|
|
242
|
+
run = data.get('run', {})
|
|
243
|
+
stats = run.get('stats', {})
|
|
244
|
+
timings = run.get('timings', {})
|
|
245
|
+
col_name = data.get('collection', {}).get('info', {}).get('name', os.path.basename(jf))
|
|
246
|
+
|
|
247
|
+
if not product_slug:
|
|
248
|
+
env_name = ''
|
|
249
|
+
env = data.get('environment')
|
|
250
|
+
if isinstance(env, dict):
|
|
251
|
+
env_name = env.get('name', '')
|
|
252
|
+
if ' - ' in env_name:
|
|
253
|
+
product_slug = _product_slug(env_name.split(' - ', 1)[-1])
|
|
254
|
+
else:
|
|
255
|
+
product_slug = 'default'
|
|
256
|
+
|
|
257
|
+
assertions = stats.get('assertions', {})
|
|
258
|
+
requests = stats.get('requests', {})
|
|
259
|
+
duration_ms = (timings.get('completed', 0) - timings.get('started', 0))
|
|
260
|
+
total_a = assertions.get('total', 0)
|
|
261
|
+
failed_a = assertions.get('failed', 0)
|
|
262
|
+
|
|
263
|
+
basename_no_ext = os.path.splitext(os.path.basename(jf))[0]
|
|
264
|
+
if product_slug and product_slug != 'default':
|
|
265
|
+
html_href = f"{product_slug}/{basename_no_ext}.html"
|
|
266
|
+
else:
|
|
267
|
+
html_href = f"{basename_no_ext}.html"
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
'name': col_name,
|
|
271
|
+
'product': product_slug,
|
|
272
|
+
'assertions_total': total_a,
|
|
273
|
+
'assertions_passed': total_a - failed_a,
|
|
274
|
+
'assertions_failed': failed_a,
|
|
275
|
+
'requests_total': requests.get('total', 0),
|
|
276
|
+
'requests_failed': requests.get('failed', 0),
|
|
277
|
+
'duration': f"{duration_ms / 1000:.1f}s",
|
|
278
|
+
'status': 'passed' if failed_a == 0 else 'failed',
|
|
279
|
+
'html_href': html_href,
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
postman_collections = []
|
|
283
|
+
postman_json_dir = getattr(args, 'postman_json_dir', None)
|
|
284
|
+
if not postman_json_dir or not os.path.isdir(postman_json_dir):
|
|
285
|
+
return postman_collections
|
|
286
|
+
|
|
287
|
+
subdirs = sorted([
|
|
288
|
+
d for d in os.listdir(postman_json_dir)
|
|
289
|
+
if os.path.isdir(os.path.join(postman_json_dir, d))
|
|
290
|
+
])
|
|
291
|
+
if subdirs:
|
|
292
|
+
for subdir in subdirs:
|
|
293
|
+
slug = _product_slug(subdir)
|
|
294
|
+
for jf in sorted(glob.glob(os.path.join(postman_json_dir, subdir, '*.json'))):
|
|
295
|
+
try:
|
|
296
|
+
postman_collections.append(_parse_collection(jf, slug))
|
|
297
|
+
except Exception as e:
|
|
298
|
+
print(f"Warning: failed to parse collection {jf} (slug={slug}): {e}")
|
|
299
|
+
continue
|
|
300
|
+
# Also include root-level JSON files as default/legacy collections
|
|
301
|
+
for jf in sorted(glob.glob(os.path.join(postman_json_dir, '*.json'))):
|
|
302
|
+
try:
|
|
303
|
+
postman_collections.append(_parse_collection(jf, 'default'))
|
|
304
|
+
except Exception as e:
|
|
305
|
+
print(f"Warning: failed to parse collection {jf} (slug=default): {e}")
|
|
306
|
+
continue
|
|
307
|
+
else:
|
|
308
|
+
for jf in sorted(glob.glob(os.path.join(postman_json_dir, '*.json'))):
|
|
309
|
+
try:
|
|
310
|
+
postman_collections.append(_parse_collection(jf, 'default'))
|
|
311
|
+
except Exception as e:
|
|
312
|
+
print(f"Warning: failed to parse collection {jf} (slug=default): {e}")
|
|
313
|
+
continue
|
|
314
|
+
|
|
315
|
+
return postman_collections
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _display_name_from_slug(slug):
|
|
319
|
+
_ACRONYMS = {'crm', 'api', 'sdk'}
|
|
320
|
+
return ' '.join(
|
|
321
|
+
w.upper() if w.lower() in _ACRONYMS else w.capitalize()
|
|
322
|
+
for w in slug.replace('-', ' ').split()
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _build_api_results_html(postman_collections, real_products, is_multi_product,
|
|
327
|
+
postman_total, postman_passed, postman_failed,
|
|
328
|
+
postman_requests, postman_duration, postman_col_passed, postman_col_failed):
|
|
329
|
+
if postman_collections:
|
|
330
|
+
products_to_show = real_products if is_multi_product else sorted(set(c['product'] for c in postman_collections))
|
|
331
|
+
html = ''
|
|
332
|
+
if is_multi_product:
|
|
333
|
+
html += '<div class="summary-grid" style="margin-bottom:1.5rem;">'
|
|
334
|
+
for slug in products_to_show:
|
|
335
|
+
p_cols = [c for c in postman_collections if c['product'] == slug]
|
|
336
|
+
p_total = sum(c['assertions_total'] for c in p_cols)
|
|
337
|
+
p_fail = sum(c['assertions_failed'] for c in p_cols)
|
|
338
|
+
p_pass = p_total - p_fail
|
|
339
|
+
n_fail_cols = sum(1 for c in p_cols if c['status'] == 'failed')
|
|
340
|
+
card_class = 'success' if p_fail == 0 else 'error'
|
|
341
|
+
status_icon = '✓' if p_fail == 0 else '✗'
|
|
342
|
+
fail_detail = f' • <span style="color:var(--color-error);">{n_fail_cols} failed</span>' if n_fail_cols else ''
|
|
343
|
+
html += f'''<div class="summary-card {card_class}">
|
|
344
|
+
<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:0.5rem;">
|
|
345
|
+
<div class="label" style="margin:0;text-align:left;">{_display_name_from_slug(slug)}</div>
|
|
346
|
+
<span style="font-size:1rem;font-weight:700;">{status_icon}</span>
|
|
347
|
+
</div>
|
|
348
|
+
<div class="value">{p_pass}<span style="font-size:1rem;font-weight:400;color:var(--color-muted);">/{p_total}</span></div>
|
|
349
|
+
<div class="detail">{len(p_cols)} collections{fail_detail}</div>
|
|
350
|
+
</div>'''
|
|
351
|
+
html += '</div>'
|
|
352
|
+
|
|
353
|
+
total_a = sum(c['assertions_total'] for c in postman_collections)
|
|
354
|
+
total_p = sum(c['assertions_passed'] for c in postman_collections)
|
|
355
|
+
total_f = sum(c['assertions_failed'] for c in postman_collections)
|
|
356
|
+
total_r = sum(c['requests_total'] for c in postman_collections)
|
|
357
|
+
html += f'''
|
|
358
|
+
<table>
|
|
359
|
+
<thead><tr><th style="text-align:left;">Metric</th><th>Value</th></tr></thead>
|
|
360
|
+
<tbody>
|
|
361
|
+
<tr><td style="text-align:left;">Assertions</td>
|
|
362
|
+
<td><strong style="color:var(--color-success);">{total_p}</strong> / {total_a}
|
|
363
|
+
{"" if total_f == 0 else f" <span style='color:var(--color-error);'>({total_f} failed)</span>"}
|
|
364
|
+
</td></tr>
|
|
365
|
+
<tr><td style="text-align:left;">Requests</td><td><strong>{total_r}</strong></td></tr>
|
|
366
|
+
<tr><td style="text-align:left;">Duration</td><td>{postman_duration}s</td></tr>
|
|
367
|
+
<tr><td style="text-align:left;">Collections</td>
|
|
368
|
+
<td>{len(postman_collections)} total
|
|
369
|
+
{f" • <span style='color:var(--color-error);'>{sum(1 for c in postman_collections if c['status'] == 'failed')} failed</span>" if any(c['status'] == 'failed' for c in postman_collections) else " • <span style='color:var(--color-success);'>all passed</span>"}
|
|
370
|
+
</td></tr>
|
|
371
|
+
</tbody>
|
|
372
|
+
</table>'''
|
|
373
|
+
return html
|
|
374
|
+
elif postman_total > 0:
|
|
375
|
+
return f'''
|
|
376
|
+
<table>
|
|
377
|
+
<thead><tr><th style="text-align:left;">Metric</th><th>Value</th></tr></thead>
|
|
378
|
+
<tbody>
|
|
379
|
+
<tr><td style="text-align:left;">Collections</td><td><strong>{postman_col_passed + postman_col_failed}</strong> ({postman_col_passed} passed, {postman_col_failed} failed)</td></tr>
|
|
380
|
+
<tr><td style="text-align:left;">Assertions</td><td><strong style="color: var(--color-success);">{postman_passed}</strong> / {postman_total}</td></tr>
|
|
381
|
+
<tr><td style="text-align:left;">Requests</td><td><strong>{postman_requests}</strong></td></tr>
|
|
382
|
+
<tr><td style="text-align:left;">Duration</td><td>{postman_duration}s</td></tr>
|
|
383
|
+
</tbody>
|
|
384
|
+
</table>'''
|
|
385
|
+
return '<p style="color: var(--color-muted); text-align: center; padding: 2rem;">No API test results available</p>'
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _build_api_coverage_detail(api_coverage):
|
|
389
|
+
if not api_coverage:
|
|
390
|
+
return '<p style="color: var(--color-muted); text-align: center; padding: 2rem;">No API coverage data available</p>'
|
|
391
|
+
|
|
392
|
+
by_method = api_coverage.get('by_method', {})
|
|
393
|
+
html = '''
|
|
394
|
+
<div class="section-title">Coverage by HTTP Method</div>
|
|
395
|
+
<table>
|
|
396
|
+
<thead><tr><th style="text-align:left;">Method</th><th>Tested</th><th>Total</th><th>Coverage</th><th style="width:200px;">Progress</th></tr></thead>
|
|
397
|
+
<tbody>'''
|
|
398
|
+
for method in sorted(by_method.keys()):
|
|
399
|
+
stats = by_method[method]
|
|
400
|
+
tested = stats.get('tested', 0)
|
|
401
|
+
total = stats.get('total', 0)
|
|
402
|
+
pct = (tested / total * 100) if total > 0 else 0
|
|
403
|
+
bar_class = 'success' if pct >= 80 else 'warning' if pct >= 60 else 'error'
|
|
404
|
+
html += f'''<tr>
|
|
405
|
+
<td style="text-align:left;"><span class="method method-{method}">{method}</span></td>
|
|
406
|
+
<td>{tested}</td>
|
|
407
|
+
<td>{total}</td>
|
|
408
|
+
<td><strong>{pct:.0f}%</strong></td>
|
|
409
|
+
<td><div class="progress-bar small"><div class="progress-fill {bar_class}" style="width:{pct}%;"></div></div></td>
|
|
410
|
+
</tr>'''
|
|
411
|
+
html += '</tbody></table>'
|
|
412
|
+
|
|
413
|
+
html += f'''
|
|
414
|
+
<div class="section-title" style="margin-top: 2rem;">Overall Metrics</div>
|
|
415
|
+
<table>
|
|
416
|
+
<thead><tr><th style="text-align:left;">Metric</th><th>Value</th></tr></thead>
|
|
417
|
+
<tbody>
|
|
418
|
+
<tr><td style="text-align:left;">Success Path (2xx) Coverage</td><td><strong>{api_coverage.get('coverage_percent', 0):.1f}%</strong></td></tr>
|
|
419
|
+
<tr><td style="text-align:left;">Well Tested (3+ tests)</td><td><strong>{api_coverage.get('well_tested_percent', 0):.1f}%</strong></td></tr>
|
|
420
|
+
<tr><td style="text-align:left;">Query Params Coverage</td><td><strong>{api_coverage.get('query_params_percent', 0):.1f}%</strong></td></tr>
|
|
421
|
+
</tbody>
|
|
422
|
+
</table>'''
|
|
423
|
+
return html
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _build_api_gaps(api_coverage):
|
|
427
|
+
if not api_coverage or not api_coverage.get('gaps'):
|
|
428
|
+
return '<p style="color: var(--color-muted); text-align: center; padding: 2rem;">No gaps found. All endpoints have test coverage.</p>'
|
|
429
|
+
|
|
430
|
+
html = '''
|
|
431
|
+
<div class="section-title">Untested Endpoints</div>
|
|
432
|
+
<table>
|
|
433
|
+
<thead><tr><th style="width:70px;">Method</th><th style="text-align:left;">Endpoint</th><th style="text-align:left;">Controller</th></tr></thead>
|
|
434
|
+
<tbody>'''
|
|
435
|
+
for gap in api_coverage['gaps']:
|
|
436
|
+
html += f'''<tr>
|
|
437
|
+
<td><span class="method method-{gap['method']}">{gap['method']}</span></td>
|
|
438
|
+
<td style="text-align:left;"><span class="path">{gap['path']}</span></td>
|
|
439
|
+
<td style="text-align:left;">{gap.get('controller', '')}</td>
|
|
440
|
+
</tr>'''
|
|
441
|
+
return html + '</tbody></table>'
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _build_unit_test_results(surefire):
|
|
445
|
+
if surefire and surefire['test_classes']:
|
|
446
|
+
html = '''
|
|
447
|
+
<div class="section-title">Test Results by Class</div>
|
|
448
|
+
<table>
|
|
449
|
+
<thead><tr><th style="text-align:left;">Test Class</th><th>Tests</th><th>Passed</th><th>Failed</th><th>Skipped</th><th>Time</th></tr></thead>
|
|
450
|
+
<tbody>'''
|
|
451
|
+
for tc in surefire['test_classes'][:20]:
|
|
452
|
+
row_class = 'style="background:#fef2f2;"' if tc['failures'] > 0 or tc['errors'] > 0 else ''
|
|
453
|
+
html += f'''<tr {row_class}>
|
|
454
|
+
<td style="text-align:left;">{tc['name']}</td>
|
|
455
|
+
<td>{tc['tests']}</td>
|
|
456
|
+
<td style="color:#22c55e;">{tc['passed']}</td>
|
|
457
|
+
<td style="color:#ef4444;">{tc['failures'] + tc['errors']}</td>
|
|
458
|
+
<td style="color:#f59e0b;">{tc['skipped']}</td>
|
|
459
|
+
<td>{tc['time']:.2f}s</td>
|
|
460
|
+
</tr>'''
|
|
461
|
+
return html + '</tbody></table>'
|
|
462
|
+
return '<p style="color: var(--color-muted); text-align: center; padding: 2rem;">No test results available</p>'
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _build_unit_coverage(jacoco):
|
|
466
|
+
if jacoco and jacoco['packages']:
|
|
467
|
+
html = '''
|
|
468
|
+
<div class="section-title">Coverage by Package (Lowest First)</div>
|
|
469
|
+
<table>
|
|
470
|
+
<thead><tr><th style="text-align:left;">Package</th><th>Coverage</th><th style="width:200px;">Progress</th><th>Covered</th><th>Missed</th></tr></thead>
|
|
471
|
+
<tbody>'''
|
|
472
|
+
for pkg in jacoco['packages'][:15]:
|
|
473
|
+
cov = pkg.get('coverage', 0)
|
|
474
|
+
bar_class = 'success' if cov >= 80 else 'warning' if cov >= 60 else 'error'
|
|
475
|
+
short_name = pkg['name'].split('.')[-2] + '.' + pkg['name'].split('.')[-1] if '.' in pkg['name'] else pkg['name']
|
|
476
|
+
html += f'''<tr>
|
|
477
|
+
<td style="text-align:left;" title="{pkg['name']}">{short_name}</td>
|
|
478
|
+
<td><strong>{cov:.1f}%</strong></td>
|
|
479
|
+
<td><div class="progress-bar small"><div class="progress-fill {bar_class}" style="width:{cov}%;"></div></div></td>
|
|
480
|
+
<td>{pkg['line_covered']}</td>
|
|
481
|
+
<td>{pkg['line_missed']}</td>
|
|
482
|
+
</tr>'''
|
|
483
|
+
return html + '</tbody></table>'
|
|
484
|
+
return '<p style="color: var(--color-muted); text-align: center; padding: 2rem;">No coverage data available</p>'
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _build_unit_mutations(mutation):
|
|
488
|
+
if mutation and mutation['mutator_stats']:
|
|
489
|
+
html = '''
|
|
490
|
+
<div class="section-title">Mutation Analysis by Mutator</div>
|
|
491
|
+
<table>
|
|
492
|
+
<thead><tr><th style="text-align:left;">Mutator</th><th>Kill Rate</th><th style="width:150px;">Progress</th><th>Killed</th><th>Survived</th><th>Total</th></tr></thead>
|
|
493
|
+
<tbody>'''
|
|
494
|
+
sorted_mutators = sorted(mutation['mutator_stats'].items(),
|
|
495
|
+
key=lambda x: x[1]['killed']/x[1]['total'] if x[1]['total'] > 0 else 0,
|
|
496
|
+
reverse=True)
|
|
497
|
+
for mutator, stats in sorted_mutators:
|
|
498
|
+
rate = (stats['killed'] / stats['total'] * 100) if stats['total'] > 0 else 0
|
|
499
|
+
bar_class = 'success' if rate >= 80 else 'warning' if rate >= 60 else 'error'
|
|
500
|
+
readable_name = format_mutator_name(mutator)
|
|
501
|
+
html += f'''<tr>
|
|
502
|
+
<td style="text-align:left;" title="{mutator}">{readable_name}</td>
|
|
503
|
+
<td><strong>{rate:.1f}%</strong></td>
|
|
504
|
+
<td><div class="progress-bar small"><div class="progress-fill {bar_class}" style="width:{rate}%;"></div></div></td>
|
|
505
|
+
<td>{stats['killed']}</td><td>{stats['survived']}</td><td>{stats['total']}</td>
|
|
506
|
+
</tr>'''
|
|
507
|
+
return html + '</tbody></table>'
|
|
508
|
+
return '<p style="color: var(--color-muted); text-align: center; padding: 2rem;">No mutation data available</p>'
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _build_unit_survived(mutation):
|
|
512
|
+
if mutation and mutation['survived_mutations']:
|
|
513
|
+
html = '''
|
|
514
|
+
<div class="section-title">Survived Mutations (Test Gaps)</div>
|
|
515
|
+
<table>
|
|
516
|
+
<thead><tr><th style="text-align:left;">Class</th><th style="text-align:left;">Method</th><th>Line</th><th style="text-align:left;">Description</th></tr></thead>
|
|
517
|
+
<tbody>'''
|
|
518
|
+
for sm in mutation['survived_mutations']:
|
|
519
|
+
desc = sm['description'][:80] + '...' if len(sm['description']) > 80 else sm['description']
|
|
520
|
+
html += f'''<tr>
|
|
521
|
+
<td style="text-align:left;">{sm['class']}</td>
|
|
522
|
+
<td style="text-align:left;"><code>{sm['method']}</code></td>
|
|
523
|
+
<td>{sm['line']}</td>
|
|
524
|
+
<td style="text-align:left;font-size:0.8em;">{desc}</td>
|
|
525
|
+
</tr>'''
|
|
526
|
+
return html + '</tbody></table>'
|
|
527
|
+
return '<p style="color: var(--color-muted); text-align: center; padding: 2rem;">No survived mutations. All mutations were killed by tests.</p>'
|