@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.
Files changed (106) hide show
  1. package/README.md +351 -0
  2. package/bin/shiftleft.js +95 -0
  3. package/package.json +57 -0
  4. package/src/commands/doctor.js +208 -0
  5. package/src/commands/init-postman.js +298 -0
  6. package/src/commands/init-rules.js +78 -0
  7. package/src/commands/link.js +172 -0
  8. package/src/commands/protect.js +61 -0
  9. package/src/commands/run-tests.js +182 -0
  10. package/src/commands/setup-pipeline.js +209 -0
  11. package/src/commands/update.js +203 -0
  12. package/src/index.js +4 -0
  13. package/src/utils/copy-tree.js +98 -0
  14. package/src/utils/gitignore.js +26 -0
  15. package/src/utils/logger.js +9 -0
  16. package/src/utils/manifest.js +145 -0
  17. package/src/utils/stack.js +80 -0
  18. package/src/utils/template.js +135 -0
  19. package/templates/AGENTS.md +109 -0
  20. package/templates/CLAUDE.md +3 -0
  21. package/templates/jenkins/Jenkinsfile-java.groovy +432 -0
  22. package/templates/jenkins/Jenkinsfile-node.groovy +450 -0
  23. package/templates/postman/.husky/pre-commit +19 -0
  24. package/templates/postman/.prettierrc.json +5 -0
  25. package/templates/postman/README.md.ejs +147 -0
  26. package/templates/postman/collections/01-core.json.ejs +91 -0
  27. package/templates/postman/config/local.json.ejs +12 -0
  28. package/templates/postman/config/staging.json.ejs +26 -0
  29. package/templates/postman/environments/local.postman_environment.json.ejs +31 -0
  30. package/templates/postman/environments/staging.postman_environment.json.ejs +31 -0
  31. package/templates/postman/gitignore +16 -0
  32. package/templates/postman/npmrc +31 -0
  33. package/templates/postman/package.json.ejs +66 -0
  34. package/templates/postman/run-all-shim.sh +16 -0
  35. package/templates/postman/scripts/auth/generate-jwt.sh +113 -0
  36. package/templates/postman/scripts/auth/get-issuer-secret.sh +140 -0
  37. package/templates/postman/scripts/infra/start-mocks.sh +138 -0
  38. package/templates/postman/scripts/infra/stop-mocks.sh +43 -0
  39. package/templates/postman/scripts/lib/api_coverage.py +1122 -0
  40. package/templates/postman/scripts/lib/cleanup-reports.sh +101 -0
  41. package/templates/postman/scripts/lib/cleanup-stryker.sh +44 -0
  42. package/templates/postman/scripts/lib/report_combined.py +527 -0
  43. package/templates/postman/scripts/lib/report_consolidated.py +363 -0
  44. package/templates/postman/scripts/lib/report_generator.py +121 -0
  45. package/templates/postman/scripts/lib/report_migration.py +156 -0
  46. package/templates/postman/scripts/lib/report_mutation.py +110 -0
  47. package/templates/postman/scripts/lib/report_unit.py +353 -0
  48. package/templates/postman/scripts/lib/report_utils.py +973 -0
  49. package/templates/postman/scripts/report-generators/generate-consolidated-report.sh +445 -0
  50. package/templates/postman/scripts/report-generators/java-api-coverage-matrix.sh +257 -0
  51. package/templates/postman/scripts/report-generators/mutation-report.sh +672 -0
  52. package/templates/postman/scripts/report-generators/node-api-coverage-matrix.sh +167 -0
  53. package/templates/postman/scripts/report-generators/stage-report-artifacts.sh +27 -0
  54. package/templates/postman/scripts/run-all.sh +452 -0
  55. package/templates/postman/scripts/runners/run-mutation-tests.sh +113 -0
  56. package/templates/postman/scripts/runners/run-tests-local.sh +936 -0
  57. package/templates/postman/scripts/runners/run-tests-staging.sh +741 -0
  58. package/templates/postman-node/README.md.ejs +26 -0
  59. package/templates/postman-node/collections/crud/01-bootstrap.json.ejs +34 -0
  60. package/templates/postman-node/config/local.json.ejs +46 -0
  61. package/templates/postman-node/config/staging.json.ejs +31 -0
  62. package/templates/postman-node/local.test.env.ejs +3 -0
  63. package/templates/postman-node/mocks/external.js +14 -0
  64. package/templates/postman-node/package.json.ejs +39 -0
  65. package/templates/postman-node/requirements.txt +1 -0
  66. package/templates/postman-node/scripts/database/cleanup-mysql.sh +12 -0
  67. package/templates/postman-node/scripts/database/run-migrations.js +29 -0
  68. package/templates/postman-node/scripts/database/start-mysql.sh +34 -0
  69. package/templates/postman-node/scripts/database/wait-for-mysql.sh +36 -0
  70. package/templates/postman-node/scripts/lib/api_coverage_node.py +1137 -0
  71. package/templates/postman-node/scripts/lib/fetch-jwt.sh +86 -0
  72. package/templates/postman-node/scripts/lib/run-newman.sh +104 -0
  73. package/templates/postman-node/scripts/lib/setup-database.sh +55 -0
  74. package/templates/postman-node/scripts/lib/start-app.sh +48 -0
  75. package/templates/postman-node/scripts/lib/utils.sh +114 -0
  76. package/templates/postman-node/scripts/report-generators/stage-report-artifacts.sh +26 -0
  77. package/templates/postman-node/scripts/run-all.sh +303 -0
  78. package/templates/postman-node/scripts/runners/run-tests.sh +123 -0
  79. package/templates/postman-node/scripts/setup-mocks.js.ejs +29 -0
  80. package/templates/postman-node/stryker.config.js.ejs +51 -0
  81. package/templates/rules/local-test-setup.mdc +420 -0
  82. package/templates/rules/testing-node.mdc +66 -0
  83. package/templates/rules/testing.mdc +248 -0
  84. package/templates/skills/_shared/postman-standards.md +380 -0
  85. package/templates/skills/enhance-test-pipeline/SKILL-java.md +483 -0
  86. package/templates/skills/enhance-test-pipeline/SKILL-node.md +431 -0
  87. package/templates/skills/enhance-test-pipeline/SKILL.md +9 -0
  88. package/templates/skills/review-test-suite/SKILL-java.md +137 -0
  89. package/templates/skills/review-test-suite/SKILL-node.md +78 -0
  90. package/templates/skills/review-test-suite/SKILL.md +9 -0
  91. package/templates/skills/run-test-suite/SKILL-java.md +186 -0
  92. package/templates/skills/run-test-suite/SKILL-node.md +191 -0
  93. package/templates/skills/run-test-suite/SKILL.md +9 -0
  94. package/templates/skills/setup-api-tests/SKILL-java.md +1094 -0
  95. package/templates/skills/setup-api-tests/SKILL-node.md +141 -0
  96. package/templates/skills/setup-api-tests/SKILL.md +9 -0
  97. package/templates/skills/setup-mutation-tests/SKILL-java.md +303 -0
  98. package/templates/skills/setup-mutation-tests/SKILL-node.md +408 -0
  99. package/templates/skills/setup-mutation-tests/SKILL.md +9 -0
  100. package/templates/skills/setup-test-pipeline/SKILL-java.md +454 -0
  101. package/templates/skills/setup-test-pipeline/SKILL-node.md +318 -0
  102. package/templates/skills/setup-test-pipeline/SKILL.md +9 -0
  103. package/templates/skills/write-api-tests/SKILL-java.md +115 -0
  104. package/templates/skills/write-api-tests/SKILL-node.md +83 -0
  105. package/templates/skills/write-api-tests/SKILL.md +9 -0
  106. 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>'