@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,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Mutation testing report generator (PIT / mutations.xml).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
from report_utils import get_html_template
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def generate_mutation_report(args):
|
|
11
|
+
import xml.etree.ElementTree as ET
|
|
12
|
+
|
|
13
|
+
tree = ET.parse(args.mutations_xml)
|
|
14
|
+
root = tree.getroot()
|
|
15
|
+
|
|
16
|
+
total = killed = survived = no_coverage = tests_run = 0
|
|
17
|
+
mutator_stats = defaultdict(lambda: {'killed': 0, 'survived': 0, 'no_coverage': 0, 'total': 0})
|
|
18
|
+
package_stats = defaultdict(lambda: {'killed': 0, 'survived': 0, 'total': 0})
|
|
19
|
+
survived_mutations = []
|
|
20
|
+
|
|
21
|
+
for mutation in root.findall('mutation'):
|
|
22
|
+
total += 1
|
|
23
|
+
status = mutation.get('status')
|
|
24
|
+
mutator = mutation.find('mutator').text.split('.')[-1]
|
|
25
|
+
mutated_class = mutation.find('mutatedClass').text
|
|
26
|
+
package = '.'.join(mutated_class.split('.')[:-1])
|
|
27
|
+
tests_run += int(mutation.get('numberOfTestsRun', 0))
|
|
28
|
+
|
|
29
|
+
mutator_stats[mutator]['total'] += 1
|
|
30
|
+
package_stats[package]['total'] += 1
|
|
31
|
+
|
|
32
|
+
if status == 'KILLED':
|
|
33
|
+
killed += 1
|
|
34
|
+
mutator_stats[mutator]['killed'] += 1
|
|
35
|
+
package_stats[package]['killed'] += 1
|
|
36
|
+
elif status == 'SURVIVED':
|
|
37
|
+
survived += 1
|
|
38
|
+
mutator_stats[mutator]['survived'] += 1
|
|
39
|
+
package_stats[package]['survived'] += 1
|
|
40
|
+
if len(survived_mutations) < 20:
|
|
41
|
+
survived_mutations.append({
|
|
42
|
+
'class': mutated_class.split('.')[-1],
|
|
43
|
+
'method': mutation.find('mutatedMethod').text,
|
|
44
|
+
'line': mutation.find('lineNumber').text,
|
|
45
|
+
'description': mutation.find('description').text
|
|
46
|
+
})
|
|
47
|
+
elif status == 'NO_COVERAGE':
|
|
48
|
+
no_coverage += 1
|
|
49
|
+
mutator_stats[mutator]['no_coverage'] += 1
|
|
50
|
+
|
|
51
|
+
mutation_score = (killed / total * 100) if total > 0 else 0
|
|
52
|
+
covered = total - no_coverage
|
|
53
|
+
test_strength = (killed / covered * 100) if covered > 0 else 0
|
|
54
|
+
|
|
55
|
+
if mutation_score >= 80:
|
|
56
|
+
health = {'class': 'excellent', 'icon': '🟢', 'label': f'{mutation_score:.1f}% Score'}
|
|
57
|
+
elif mutation_score >= 70:
|
|
58
|
+
health = {'class': 'good', 'icon': '🟡', 'label': f'{mutation_score:.1f}% Score'}
|
|
59
|
+
else:
|
|
60
|
+
health = {'class': 'poor', 'icon': '🔴', 'label': f'{mutation_score:.1f}% Score'}
|
|
61
|
+
|
|
62
|
+
content = f'''
|
|
63
|
+
<div class="summary-grid">
|
|
64
|
+
<div class="summary-card {'success' if mutation_score >= 80 else 'warning' if mutation_score >= 60 else 'error'}">
|
|
65
|
+
<div class="value">{mutation_score:.1f}%</div><div class="label">Mutation Score</div><div class="detail">{killed:,} / {total:,} killed</div>
|
|
66
|
+
</div>
|
|
67
|
+
<div class="summary-card {'success' if test_strength >= 80 else 'warning' if test_strength >= 60 else 'error'}">
|
|
68
|
+
<div class="value">{test_strength:.1f}%</div><div class="label">Test Strength</div><div class="detail">{killed:,} / {covered:,} covered</div>
|
|
69
|
+
</div>
|
|
70
|
+
<div class="summary-card {'error' if survived > 100 else 'warning' if survived > 50 else ''}">
|
|
71
|
+
<div class="value">{survived}</div><div class="label">Survived</div>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="summary-card {'error' if no_coverage > 100 else 'warning' if no_coverage > 50 else ''}">
|
|
74
|
+
<div class="value">{no_coverage}</div><div class="label">No Coverage</div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div class="section">
|
|
79
|
+
<div class="section-title">Mutator Breakdown</div>
|
|
80
|
+
<table>
|
|
81
|
+
<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>
|
|
82
|
+
<tbody>
|
|
83
|
+
'''
|
|
84
|
+
|
|
85
|
+
for mutator, stats in sorted(mutator_stats.items(), key=lambda x: x[1]['killed']/x[1]['total'] if x[1]['total'] > 0 else 0, reverse=True):
|
|
86
|
+
rate = (stats['killed'] / stats['total'] * 100) if stats['total'] > 0 else 0
|
|
87
|
+
bar_class = 'success' if rate >= 80 else 'warning' if rate >= 60 else 'error'
|
|
88
|
+
content += f'''<tr>
|
|
89
|
+
<td style="text-align:left;">{mutator.replace('Mutator', '')}</td>
|
|
90
|
+
<td><strong>{rate:.1f}%</strong></td>
|
|
91
|
+
<td><div class="progress-bar small"><div class="progress-fill {bar_class}" style="width:{rate}%;"></div></div></td>
|
|
92
|
+
<td>{stats['killed']}</td><td>{stats['survived']}</td><td>{stats['total']}</td>
|
|
93
|
+
</tr>'''
|
|
94
|
+
|
|
95
|
+
content += '</tbody></table></div>'
|
|
96
|
+
|
|
97
|
+
logo = getattr(args, 'logo', 'Quality Reports')
|
|
98
|
+
html = get_html_template(
|
|
99
|
+
title="Mutation Testing Report",
|
|
100
|
+
logo=logo,
|
|
101
|
+
content=content,
|
|
102
|
+
health_badge=health,
|
|
103
|
+
footer_text="Generated by PIT Mutation Testing"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
with open(args.output_file, 'w') as f:
|
|
107
|
+
f.write(html)
|
|
108
|
+
|
|
109
|
+
print(f"Total: {total}, Killed: {killed} ({mutation_score:.1f}%), Survived: {survived}")
|
|
110
|
+
print(f"Report generated: {args.output_file}")
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit test report generator — combines Surefire, JaCoCo, and PIT mutation data.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import glob
|
|
6
|
+
import os
|
|
7
|
+
from collections import defaultdict
|
|
8
|
+
from report_utils import (
|
|
9
|
+
get_html_template, build_css_tabs, report_detail_links_row,
|
|
10
|
+
resolve_jacoco_href, resolve_pit_href, format_mutator_name,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def parse_surefire_reports(surefire_dir):
|
|
15
|
+
import xml.etree.ElementTree as ET
|
|
16
|
+
|
|
17
|
+
results = {
|
|
18
|
+
'tests': 0, 'failures': 0, 'errors': 0, 'skipped': 0,
|
|
19
|
+
'time': 0.0, 'test_classes': []
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
xml_files = glob.glob(os.path.join(surefire_dir, 'TEST-*.xml'))
|
|
23
|
+
for xml_file in xml_files:
|
|
24
|
+
try:
|
|
25
|
+
tree = ET.parse(xml_file)
|
|
26
|
+
root = tree.getroot()
|
|
27
|
+
tests = int(root.get('tests', 0))
|
|
28
|
+
failures = int(root.get('failures', 0))
|
|
29
|
+
errors = int(root.get('errors', 0))
|
|
30
|
+
skipped = int(root.get('skipped', 0))
|
|
31
|
+
time = float(root.get('time', 0))
|
|
32
|
+
name = root.get('name', '').split('.')[-1]
|
|
33
|
+
|
|
34
|
+
results['tests'] += tests
|
|
35
|
+
results['failures'] += failures
|
|
36
|
+
results['errors'] += errors
|
|
37
|
+
results['skipped'] += skipped
|
|
38
|
+
results['time'] += time
|
|
39
|
+
|
|
40
|
+
if tests > 0:
|
|
41
|
+
results['test_classes'].append({
|
|
42
|
+
'name': name,
|
|
43
|
+
'tests': tests,
|
|
44
|
+
'failures': failures,
|
|
45
|
+
'errors': errors,
|
|
46
|
+
'skipped': skipped,
|
|
47
|
+
'time': time,
|
|
48
|
+
'passed': tests - failures - errors - skipped
|
|
49
|
+
})
|
|
50
|
+
except Exception as e:
|
|
51
|
+
print(f"Warning: Could not parse {xml_file}: {e}")
|
|
52
|
+
|
|
53
|
+
results['passed'] = results['tests'] - results['failures'] - results['errors'] - results['skipped']
|
|
54
|
+
results['test_classes'].sort(key=lambda x: x['tests'], reverse=True)
|
|
55
|
+
return results
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def parse_jacoco_report(jacoco_xml):
|
|
59
|
+
import xml.etree.ElementTree as ET
|
|
60
|
+
|
|
61
|
+
results = {
|
|
62
|
+
'line_covered': 0, 'line_missed': 0,
|
|
63
|
+
'branch_covered': 0, 'branch_missed': 0,
|
|
64
|
+
'method_covered': 0, 'method_missed': 0,
|
|
65
|
+
'class_covered': 0, 'class_missed': 0,
|
|
66
|
+
'packages': []
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
tree = ET.parse(jacoco_xml)
|
|
71
|
+
root = tree.getroot()
|
|
72
|
+
|
|
73
|
+
for counter in root.findall('counter'):
|
|
74
|
+
ctype = counter.get('type')
|
|
75
|
+
covered = int(counter.get('covered', 0))
|
|
76
|
+
missed = int(counter.get('missed', 0))
|
|
77
|
+
if ctype == 'LINE':
|
|
78
|
+
results['line_covered'] = covered
|
|
79
|
+
results['line_missed'] = missed
|
|
80
|
+
elif ctype == 'BRANCH':
|
|
81
|
+
results['branch_covered'] = covered
|
|
82
|
+
results['branch_missed'] = missed
|
|
83
|
+
elif ctype == 'METHOD':
|
|
84
|
+
results['method_covered'] = covered
|
|
85
|
+
results['method_missed'] = missed
|
|
86
|
+
elif ctype == 'CLASS':
|
|
87
|
+
results['class_covered'] = covered
|
|
88
|
+
results['class_missed'] = missed
|
|
89
|
+
|
|
90
|
+
for pkg in root.findall('package'):
|
|
91
|
+
pkg_name = pkg.get('name', '').replace('/', '.')
|
|
92
|
+
pkg_data = {'name': pkg_name, 'line_covered': 0, 'line_missed': 0}
|
|
93
|
+
for counter in pkg.findall('counter'):
|
|
94
|
+
if counter.get('type') == 'LINE':
|
|
95
|
+
pkg_data['line_covered'] = int(counter.get('covered', 0))
|
|
96
|
+
pkg_data['line_missed'] = int(counter.get('missed', 0))
|
|
97
|
+
total = pkg_data['line_covered'] + pkg_data['line_missed']
|
|
98
|
+
if total > 0:
|
|
99
|
+
pkg_data['coverage'] = (pkg_data['line_covered'] / total) * 100
|
|
100
|
+
results['packages'].append(pkg_data)
|
|
101
|
+
|
|
102
|
+
results['packages'].sort(key=lambda x: x.get('coverage', 0))
|
|
103
|
+
except Exception as e:
|
|
104
|
+
print(f"Warning: Could not parse JaCoCo XML: {e}")
|
|
105
|
+
|
|
106
|
+
return results
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def parse_mutation_data(mutations_xml):
|
|
110
|
+
import xml.etree.ElementTree as ET
|
|
111
|
+
|
|
112
|
+
results = {
|
|
113
|
+
'total': 0, 'killed': 0, 'survived': 0, 'no_coverage': 0,
|
|
114
|
+
'mutator_stats': defaultdict(lambda: {'killed': 0, 'survived': 0, 'total': 0}),
|
|
115
|
+
'survived_mutations': []
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
tree = ET.parse(mutations_xml)
|
|
120
|
+
root = tree.getroot()
|
|
121
|
+
|
|
122
|
+
for mutation in root.findall('mutation'):
|
|
123
|
+
results['total'] += 1
|
|
124
|
+
status = mutation.get('status')
|
|
125
|
+
mutator = mutation.find('mutator').text.split('.')[-1]
|
|
126
|
+
mutated_class = mutation.find('mutatedClass').text
|
|
127
|
+
|
|
128
|
+
results['mutator_stats'][mutator]['total'] += 1
|
|
129
|
+
|
|
130
|
+
if status == 'KILLED':
|
|
131
|
+
results['killed'] += 1
|
|
132
|
+
results['mutator_stats'][mutator]['killed'] += 1
|
|
133
|
+
elif status == 'SURVIVED':
|
|
134
|
+
results['survived'] += 1
|
|
135
|
+
results['mutator_stats'][mutator]['survived'] += 1
|
|
136
|
+
if len(results['survived_mutations']) < 15:
|
|
137
|
+
results['survived_mutations'].append({
|
|
138
|
+
'class': mutated_class.split('.')[-1],
|
|
139
|
+
'method': mutation.find('mutatedMethod').text,
|
|
140
|
+
'line': mutation.find('lineNumber').text,
|
|
141
|
+
'description': mutation.find('description').text
|
|
142
|
+
})
|
|
143
|
+
elif status == 'NO_COVERAGE':
|
|
144
|
+
results['no_coverage'] += 1
|
|
145
|
+
except Exception as e:
|
|
146
|
+
print(f"Warning: Could not parse mutations.xml: {e}")
|
|
147
|
+
|
|
148
|
+
return results
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def generate_unit_test_report(args):
|
|
152
|
+
surefire = parse_surefire_reports(args.surefire_dir) if args.surefire_dir and os.path.isdir(args.surefire_dir) else None
|
|
153
|
+
jacoco = parse_jacoco_report(args.jacoco_xml) if args.jacoco_xml and os.path.isfile(args.jacoco_xml) else None
|
|
154
|
+
mutation = parse_mutation_data(args.mutations_xml) if args.mutations_xml and os.path.isfile(args.mutations_xml) else None
|
|
155
|
+
|
|
156
|
+
test_pass_rate = 0
|
|
157
|
+
if surefire and surefire['tests'] > 0:
|
|
158
|
+
test_pass_rate = (surefire['passed'] / surefire['tests']) * 100
|
|
159
|
+
|
|
160
|
+
line_coverage = 0
|
|
161
|
+
if jacoco:
|
|
162
|
+
total_lines = jacoco['line_covered'] + jacoco['line_missed']
|
|
163
|
+
if total_lines > 0:
|
|
164
|
+
line_coverage = (jacoco['line_covered'] / total_lines) * 100
|
|
165
|
+
|
|
166
|
+
branch_coverage = 0
|
|
167
|
+
if jacoco:
|
|
168
|
+
total_branches = jacoco['branch_covered'] + jacoco['branch_missed']
|
|
169
|
+
if total_branches > 0:
|
|
170
|
+
branch_coverage = (jacoco['branch_covered'] / total_branches) * 100
|
|
171
|
+
|
|
172
|
+
mutation_score = 0
|
|
173
|
+
if mutation and mutation['total'] > 0:
|
|
174
|
+
mutation_score = (mutation['killed'] / mutation['total']) * 100
|
|
175
|
+
|
|
176
|
+
all_tests_pass = surefire and surefire['failures'] == 0 and surefire['errors'] == 0
|
|
177
|
+
good_coverage = line_coverage >= 70
|
|
178
|
+
good_mutation = mutation_score >= 70
|
|
179
|
+
|
|
180
|
+
if all_tests_pass and good_coverage and good_mutation:
|
|
181
|
+
health = {'class': 'excellent', 'icon': '🟢', 'label': 'Healthy'}
|
|
182
|
+
elif all_tests_pass and (good_coverage or good_mutation):
|
|
183
|
+
health = {'class': 'good', 'icon': '🟡', 'label': 'Good'}
|
|
184
|
+
else:
|
|
185
|
+
health = {'class': 'poor', 'icon': '🔴', 'label': 'Needs Attention'}
|
|
186
|
+
|
|
187
|
+
content = '<div class="summary-grid">'
|
|
188
|
+
|
|
189
|
+
if surefire:
|
|
190
|
+
card_class = 'success' if surefire['failures'] == 0 and surefire['errors'] == 0 else 'error'
|
|
191
|
+
content += f'''
|
|
192
|
+
<div class="summary-card {card_class}">
|
|
193
|
+
<div class="value">{surefire['passed']}/{surefire['tests']}</div>
|
|
194
|
+
<div class="label">Tests Passed</div>
|
|
195
|
+
<div class="detail">{surefire['time']:.1f}s | {surefire['skipped']} skipped</div>
|
|
196
|
+
</div>'''
|
|
197
|
+
|
|
198
|
+
if jacoco:
|
|
199
|
+
card_class = 'success' if line_coverage >= 80 else 'warning' if line_coverage >= 60 else 'error'
|
|
200
|
+
content += f'''
|
|
201
|
+
<div class="summary-card {card_class}">
|
|
202
|
+
<div class="value">{line_coverage:.1f}%</div>
|
|
203
|
+
<div class="label">Line Coverage</div>
|
|
204
|
+
<div class="detail">{jacoco['line_covered']:,} / {jacoco['line_covered'] + jacoco['line_missed']:,} lines</div>
|
|
205
|
+
</div>'''
|
|
206
|
+
|
|
207
|
+
if jacoco:
|
|
208
|
+
card_class = 'success' if branch_coverage >= 80 else 'warning' if branch_coverage >= 60 else 'error'
|
|
209
|
+
content += f'''
|
|
210
|
+
<div class="summary-card {card_class}">
|
|
211
|
+
<div class="value">{branch_coverage:.1f}%</div>
|
|
212
|
+
<div class="label">Branch Coverage</div>
|
|
213
|
+
<div class="detail">{jacoco['branch_covered']:,} / {jacoco['branch_covered'] + jacoco['branch_missed']:,} branches</div>
|
|
214
|
+
</div>'''
|
|
215
|
+
|
|
216
|
+
if mutation:
|
|
217
|
+
card_class = 'success' if mutation_score >= 80 else 'warning' if mutation_score >= 60 else 'error'
|
|
218
|
+
content += f'''
|
|
219
|
+
<div class="summary-card {card_class}">
|
|
220
|
+
<div class="value">{mutation_score:.1f}%</div>
|
|
221
|
+
<div class="label">Mutation Score</div>
|
|
222
|
+
<div class="detail">{mutation['killed']:,} / {mutation['total']:,} killed</div>
|
|
223
|
+
</div>'''
|
|
224
|
+
|
|
225
|
+
content += '</div>'
|
|
226
|
+
|
|
227
|
+
output_html = args.output_file
|
|
228
|
+
jacoco_href = resolve_jacoco_href(output_html, args.jacoco_xml) if jacoco else None
|
|
229
|
+
pit_href = resolve_pit_href(output_html, args.mutations_xml) if mutation else None
|
|
230
|
+
content += report_detail_links_row([
|
|
231
|
+
('Detailed Coverage Analysis', 'JaCoCo Coverage Report', jacoco_href),
|
|
232
|
+
('Detailed Mutation Analysis', 'PIT Mutation Report', pit_href),
|
|
233
|
+
])
|
|
234
|
+
|
|
235
|
+
test_results_html = _build_test_results_html(surefire)
|
|
236
|
+
coverage_html = _build_coverage_html(jacoco)
|
|
237
|
+
mutations_html = _build_mutations_html(mutation)
|
|
238
|
+
survived_html = _build_survived_html(mutation)
|
|
239
|
+
|
|
240
|
+
content += '<div class="section">'
|
|
241
|
+
content += build_css_tabs('unit', [
|
|
242
|
+
('results', 'Test Results', test_results_html),
|
|
243
|
+
('coverage', 'Coverage Analysis', coverage_html),
|
|
244
|
+
('mutations', 'Mutation Testing', mutations_html),
|
|
245
|
+
('survived', 'Survived Mutations', survived_html),
|
|
246
|
+
])
|
|
247
|
+
content += '</div>'
|
|
248
|
+
|
|
249
|
+
logo = getattr(args, 'logo', 'Quality Reports')
|
|
250
|
+
html = get_html_template(
|
|
251
|
+
title="Unit Test Quality Report",
|
|
252
|
+
logo=logo,
|
|
253
|
+
content=content,
|
|
254
|
+
health_badge=health,
|
|
255
|
+
footer_text="Test Results + Code Coverage + Mutation Testing"
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
with open(args.output_file, 'w') as f:
|
|
259
|
+
f.write(html)
|
|
260
|
+
|
|
261
|
+
print(f"Unit Test Report generated: {args.output_file}")
|
|
262
|
+
if surefire:
|
|
263
|
+
print(f" Tests: {surefire['passed']}/{surefire['tests']} passed ({test_pass_rate:.1f}%)")
|
|
264
|
+
if jacoco:
|
|
265
|
+
print(f" Line Coverage: {line_coverage:.1f}%")
|
|
266
|
+
if mutation:
|
|
267
|
+
print(f" Mutation Score: {mutation_score:.1f}%")
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _build_test_results_html(surefire):
|
|
271
|
+
if surefire and surefire['test_classes']:
|
|
272
|
+
html = '''
|
|
273
|
+
<div class="section-title">Test Results by Class</div>
|
|
274
|
+
<table>
|
|
275
|
+
<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>
|
|
276
|
+
<tbody>'''
|
|
277
|
+
for tc in surefire['test_classes'][:20]:
|
|
278
|
+
row_class = 'style="background:#fef2f2;"' if tc['failures'] > 0 or tc['errors'] > 0 else ''
|
|
279
|
+
html += f'''<tr {row_class}>
|
|
280
|
+
<td style="text-align:left;">{tc['name']}</td>
|
|
281
|
+
<td>{tc['tests']}</td>
|
|
282
|
+
<td style="color:#22c55e;">{tc['passed']}</td>
|
|
283
|
+
<td style="color:#ef4444;">{tc['failures'] + tc['errors']}</td>
|
|
284
|
+
<td style="color:#f59e0b;">{tc['skipped']}</td>
|
|
285
|
+
<td>{tc['time']:.2f}s</td>
|
|
286
|
+
</tr>'''
|
|
287
|
+
return html + '</tbody></table>'
|
|
288
|
+
return '<p style="color: var(--color-muted); text-align: center; padding: 2rem;">No test results available</p>'
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _build_coverage_html(jacoco):
|
|
292
|
+
if jacoco and jacoco['packages']:
|
|
293
|
+
html = '''
|
|
294
|
+
<div class="section-title">Coverage by Package (Lowest First)</div>
|
|
295
|
+
<table>
|
|
296
|
+
<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>
|
|
297
|
+
<tbody>'''
|
|
298
|
+
for pkg in jacoco['packages'][:15]:
|
|
299
|
+
cov = pkg.get('coverage', 0)
|
|
300
|
+
bar_class = 'success' if cov >= 80 else 'warning' if cov >= 60 else 'error'
|
|
301
|
+
short_name = pkg['name'].split('.')[-2] + '.' + pkg['name'].split('.')[-1] if '.' in pkg['name'] else pkg['name']
|
|
302
|
+
html += f'''<tr>
|
|
303
|
+
<td style="text-align:left;" title="{pkg['name']}">{short_name}</td>
|
|
304
|
+
<td><strong>{cov:.1f}%</strong></td>
|
|
305
|
+
<td><div class="progress-bar small"><div class="progress-fill {bar_class}" style="width:{cov}%;"></div></div></td>
|
|
306
|
+
<td>{pkg['line_covered']}</td>
|
|
307
|
+
<td>{pkg['line_missed']}</td>
|
|
308
|
+
</tr>'''
|
|
309
|
+
return html + '</tbody></table>'
|
|
310
|
+
return '<p style="color: var(--color-muted); text-align: center; padding: 2rem;">No coverage data available</p>'
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _build_mutations_html(mutation):
|
|
314
|
+
if mutation and mutation['mutator_stats']:
|
|
315
|
+
html = '''
|
|
316
|
+
<div class="section-title">Mutation Analysis by Mutator</div>
|
|
317
|
+
<table>
|
|
318
|
+
<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>
|
|
319
|
+
<tbody>'''
|
|
320
|
+
sorted_mutators = sorted(mutation['mutator_stats'].items(),
|
|
321
|
+
key=lambda x: x[1]['killed']/x[1]['total'] if x[1]['total'] > 0 else 0,
|
|
322
|
+
reverse=True)
|
|
323
|
+
for mutator, stats in sorted_mutators:
|
|
324
|
+
rate = (stats['killed'] / stats['total'] * 100) if stats['total'] > 0 else 0
|
|
325
|
+
bar_class = 'success' if rate >= 80 else 'warning' if rate >= 60 else 'error'
|
|
326
|
+
readable_name = format_mutator_name(mutator)
|
|
327
|
+
html += f'''<tr>
|
|
328
|
+
<td style="text-align:left;" title="{mutator}">{readable_name}</td>
|
|
329
|
+
<td><strong>{rate:.1f}%</strong></td>
|
|
330
|
+
<td><div class="progress-bar small"><div class="progress-fill {bar_class}" style="width:{rate}%;"></div></div></td>
|
|
331
|
+
<td>{stats['killed']}</td><td>{stats['survived']}</td><td>{stats['total']}</td>
|
|
332
|
+
</tr>'''
|
|
333
|
+
return html + '</tbody></table>'
|
|
334
|
+
return '<p style="color: var(--color-muted); text-align: center; padding: 2rem;">No mutation data available</p>'
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _build_survived_html(mutation):
|
|
338
|
+
if mutation and mutation['survived_mutations']:
|
|
339
|
+
html = '''
|
|
340
|
+
<div class="section-title">Survived Mutations (Test Gaps)</div>
|
|
341
|
+
<table>
|
|
342
|
+
<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>
|
|
343
|
+
<tbody>'''
|
|
344
|
+
for sm in mutation['survived_mutations']:
|
|
345
|
+
desc = sm['description'][:80] + '...' if len(sm['description']) > 80 else sm['description']
|
|
346
|
+
html += f'''<tr>
|
|
347
|
+
<td style="text-align:left;">{sm['class']}</td>
|
|
348
|
+
<td style="text-align:left;"><code>{sm['method']}</code></td>
|
|
349
|
+
<td>{sm['line']}</td>
|
|
350
|
+
<td style="text-align:left;font-size:0.8em;">{desc}</td>
|
|
351
|
+
</tr>'''
|
|
352
|
+
return html + '</tbody></table>'
|
|
353
|
+
return '<p style="color: var(--color-muted); text-align: center; padding: 2rem;">No survived mutations. All mutations were killed by tests.</p>'
|