@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,672 @@
1
+ #!/bin/bash
2
+ #
3
+ # Mutation Testing Report Generator
4
+ # ==================================
5
+ # Runs PIT mutation tests and generates a comprehensive HTML report.
6
+ #
7
+ # Usage:
8
+ # ./mutation-report.sh [OPTIONS]
9
+ #
10
+ # Options:
11
+ # -r, --run Force re-run mutation tests (even if results exist)
12
+ # -o, --output Output directory (default: postman/reports)
13
+ # -h, --help Show this help message
14
+ #
15
+ # Examples:
16
+ # ./mutation-report.sh # Generate report (runs tests if needed)
17
+ # ./mutation-report.sh --run # Force re-run tests and generate report
18
+ # ./mutation-report.sh -o ./reports # Custom output directory
19
+ #
20
+ # Prerequisites:
21
+ # - Maven installed and configured
22
+ # - Python 3.x installed
23
+ #
24
+ # Report includes:
25
+ # - Overall mutation score and line coverage
26
+ # - Test strength metrics
27
+ # - Mutator breakdown with kill rates
28
+ # - Package-level analysis
29
+ # - Class-level details
30
+ # - Survived mutations for review
31
+
32
+ set -e
33
+
34
+ # Colors for output
35
+ RED='\033[0;31m'
36
+ GREEN='\033[0;32m'
37
+ YELLOW='\033[1;33m'
38
+ BLUE='\033[0;34m'
39
+ NC='\033[0m' # No Color
40
+
41
+ # Helper functions
42
+ log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
43
+ log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
44
+ log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
45
+ log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
46
+
47
+ # Configuration
48
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
49
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
50
+ INSTALLATION_DIR="$PROJECT_ROOT/installation"
51
+ PIT_REPORTS_DIR="$INSTALLATION_DIR/target/pit-reports"
52
+ MUTATIONS_XML="$PIT_REPORTS_DIR/mutations.xml"
53
+ OUTPUT_DIR="$PROJECT_ROOT/postman/reports"
54
+
55
+ # Parse arguments
56
+ RUN_TESTS=false
57
+ while [[ $# -gt 0 ]]; do
58
+ case $1 in
59
+ -h|--help)
60
+ head -30 "$0" | tail -27
61
+ exit 0
62
+ ;;
63
+ -r|--run)
64
+ RUN_TESTS=true
65
+ shift
66
+ ;;
67
+ -o|--output)
68
+ OUTPUT_DIR="$2"
69
+ shift 2
70
+ ;;
71
+ *)
72
+ echo -e "${RED}Unknown option: $1${NC}"
73
+ exit 1
74
+ ;;
75
+ esac
76
+ done
77
+
78
+ echo -e "${BLUE}=== Mutation Testing Report Generator ===${NC}"
79
+
80
+ # Check if we should run mutation tests
81
+ if [[ "$RUN_TESTS" == false ]] && [[ ! -f "$MUTATIONS_XML" ]]; then
82
+ echo ""
83
+ log_warn "mutations.xml not found. Running mutation tests..."
84
+ RUN_TESTS=true
85
+ fi
86
+
87
+ if [[ "$RUN_TESTS" == true ]]; then
88
+ echo ""
89
+ log_info "Running mutation tests (this may take 2-3 minutes)..."
90
+ echo ""
91
+
92
+ cd "$INSTALLATION_DIR"
93
+ mvn org.pitest:pitest-maven:mutationCoverage 2>&1 | tee /tmp/pit-output.txt | grep -E "(INFO|Generated|Killed|Coverage|BUILD|>>)" || true
94
+
95
+ # Check if build succeeded
96
+ if grep -q "BUILD SUCCESS" /tmp/pit-output.txt; then
97
+ echo ""
98
+ log_success "Mutation tests completed!"
99
+ echo ""
100
+ elif grep -q "BUILD FAILURE" /tmp/pit-output.txt; then
101
+ echo ""
102
+ log_error "Mutation tests failed. Check /tmp/pit-output.txt for details."
103
+ exit 1
104
+ fi
105
+ fi
106
+
107
+ # Verify mutations.xml exists after run
108
+ if [[ ! -f "$MUTATIONS_XML" ]]; then
109
+ echo -e "${RED}Error: mutations.xml not found at $MUTATIONS_XML${NC}"
110
+ echo -e "${RED}Mutation tests may have failed. Check the output above.${NC}"
111
+ exit 1
112
+ fi
113
+
114
+ # Create output directory
115
+ mkdir -p "$OUTPUT_DIR"
116
+
117
+ TIMESTAMP=$(date +"%Y%m%d-%H%M%S")
118
+ HTML_OUTPUT="$OUTPUT_DIR/mutation-report-$TIMESTAMP.html"
119
+
120
+ echo -e "${GREEN}Parsing mutations.xml...${NC}"
121
+
122
+ # Use unified report generator
123
+ python3 "$SCRIPT_DIR/../lib/report_generator.py" mutation "$MUTATIONS_XML" "$HTML_OUTPUT"
124
+
125
+ # Skip inline Python script - now handled by separate module
126
+ : << 'SKIP_INLINE_PYTHON'
127
+ # Python script to parse mutations.xml and generate report
128
+ python3 << 'PYTHON_SCRIPT' - "$MUTATIONS_XML" "$HTML_OUTPUT"
129
+ import sys
130
+ import xml.etree.ElementTree as ET
131
+ from collections import defaultdict
132
+ from datetime import datetime
133
+
134
+ mutations_xml = sys.argv[1]
135
+ html_output = sys.argv[2]
136
+
137
+ # Parse XML
138
+ tree = ET.parse(mutations_xml)
139
+ root = tree.getroot()
140
+
141
+ # Collect statistics
142
+ total = 0
143
+ killed = 0
144
+ survived = 0
145
+ no_coverage = 0
146
+ timed_out = 0
147
+ tests_run = 0
148
+
149
+ # By mutator
150
+ mutator_stats = defaultdict(lambda: {'killed': 0, 'survived': 0, 'no_coverage': 0, 'total': 0})
151
+
152
+ # By class
153
+ class_stats = defaultdict(lambda: {'killed': 0, 'survived': 0, 'no_coverage': 0, 'total': 0})
154
+
155
+ # By package
156
+ package_stats = defaultdict(lambda: {'killed': 0, 'survived': 0, 'no_coverage': 0, 'total': 0})
157
+
158
+ # Survived mutations for review
159
+ survived_mutations = []
160
+
161
+ for mutation in root.findall('mutation'):
162
+ total += 1
163
+ status = mutation.get('status')
164
+ mutator = mutation.find('mutator').text.split('.')[-1]
165
+ mutated_class = mutation.find('mutatedClass').text
166
+ source_file = mutation.find('sourceFile').text
167
+ method = mutation.find('mutatedMethod').text
168
+ line = mutation.find('lineNumber').text
169
+ description = mutation.find('description').text
170
+ tests = int(mutation.get('numberOfTestsRun', 0))
171
+ tests_run += tests
172
+
173
+ # Package
174
+ package = '.'.join(mutated_class.split('.')[:-1])
175
+
176
+ # Update stats
177
+ mutator_stats[mutator]['total'] += 1
178
+ class_stats[mutated_class]['total'] += 1
179
+ package_stats[package]['total'] += 1
180
+
181
+ if status == 'KILLED':
182
+ killed += 1
183
+ mutator_stats[mutator]['killed'] += 1
184
+ class_stats[mutated_class]['killed'] += 1
185
+ package_stats[package]['killed'] += 1
186
+ elif status == 'SURVIVED':
187
+ survived += 1
188
+ mutator_stats[mutator]['survived'] += 1
189
+ class_stats[mutated_class]['survived'] += 1
190
+ package_stats[package]['survived'] += 1
191
+ survived_mutations.append({
192
+ 'class': mutated_class.split('.')[-1],
193
+ 'method': method,
194
+ 'line': line,
195
+ 'mutator': mutator,
196
+ 'description': description
197
+ })
198
+ elif status == 'NO_COVERAGE':
199
+ no_coverage += 1
200
+ mutator_stats[mutator]['no_coverage'] += 1
201
+ class_stats[mutated_class]['no_coverage'] += 1
202
+ package_stats[package]['no_coverage'] += 1
203
+ elif status == 'TIMED_OUT':
204
+ timed_out += 1
205
+
206
+ # Calculate percentages
207
+ mutation_score = (killed / total * 100) if total > 0 else 0
208
+ covered = total - no_coverage
209
+ test_strength = (killed / covered * 100) if covered > 0 else 0
210
+ tests_per_mutation = tests_run / total if total > 0 else 0
211
+
212
+ # Sort mutators by kill rate
213
+ mutator_sorted = sorted(
214
+ mutator_stats.items(),
215
+ key=lambda x: (x[1]['killed'] / x[1]['total'] if x[1]['total'] > 0 else 0),
216
+ reverse=True
217
+ )
218
+
219
+ # Sort packages by mutation score
220
+ package_sorted = sorted(
221
+ package_stats.items(),
222
+ key=lambda x: (x[1]['killed'] / x[1]['total'] if x[1]['total'] > 0 else 0),
223
+ reverse=True
224
+ )
225
+
226
+ # Sort classes by number of survived mutations (most problematic first)
227
+ class_sorted = sorted(
228
+ class_stats.items(),
229
+ key=lambda x: x[1]['survived'],
230
+ reverse=True
231
+ )[:15] # Top 15 classes with most survived
232
+
233
+ def get_status_class(percentage):
234
+ if percentage >= 80:
235
+ return 'excellent'
236
+ elif percentage >= 70:
237
+ return 'good'
238
+ elif percentage >= 60:
239
+ return 'moderate'
240
+ else:
241
+ return 'poor'
242
+
243
+ def get_status_icon(percentage):
244
+ if percentage >= 80:
245
+ return '✅'
246
+ elif percentage >= 70:
247
+ return '🟢'
248
+ elif percentage >= 60:
249
+ return '🟡'
250
+ else:
251
+ return '🔴'
252
+
253
+ # Generate HTML - matching API coverage matrix style
254
+ html = f'''<!DOCTYPE html>
255
+ <html lang="en">
256
+ <head>
257
+ <meta charset="UTF-8">
258
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
259
+ <title>Mutation Testing Report</title>
260
+ <style>
261
+ * {{ margin: 0; padding: 0; box-sizing: border-box; }}
262
+
263
+ body {{
264
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
265
+ background: #f8fafc;
266
+ color: #1e293b;
267
+ line-height: 1.6;
268
+ padding: 2rem;
269
+ }}
270
+
271
+ .container {{ max-width: 1200px; margin: 0 auto; }}
272
+
273
+ h1 {{
274
+ font-size: 1.75rem;
275
+ font-weight: 700;
276
+ color: #0f172a;
277
+ margin-bottom: 0.5rem;
278
+ }}
279
+
280
+ .timestamp {{ color: #64748b; font-size: 0.875rem; margin-bottom: 2rem; }}
281
+
282
+ h2 {{
283
+ font-size: 1.1rem;
284
+ font-weight: 600;
285
+ color: #0f172a;
286
+ margin: 2rem 0 1rem;
287
+ padding-bottom: 0.5rem;
288
+ border-bottom: 2px solid #e2e8f0;
289
+ }}
290
+
291
+ h3 {{
292
+ font-size: 1rem;
293
+ font-weight: 600;
294
+ color: #64748b;
295
+ margin: 1.5rem 0 1rem;
296
+ }}
297
+
298
+ .summary-grid {{
299
+ display: grid;
300
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
301
+ gap: 1rem;
302
+ margin-bottom: 2rem;
303
+ }}
304
+
305
+ .summary-card {{
306
+ background: white;
307
+ border: 1px solid #e2e8f0;
308
+ border-radius: 10px;
309
+ padding: 1.25rem;
310
+ text-align: center;
311
+ }}
312
+
313
+ .summary-card .value {{ font-size: 2rem; font-weight: 700; color: #3b82f6; }}
314
+ .summary-card .label {{ color: #64748b; font-size: 0.8rem; margin-top: 0.25rem; }}
315
+ .summary-card .detail {{ color: #94a3b8; font-size: 0.75rem; margin-top: 0.25rem; }}
316
+ .summary-card.success .value {{ color: #10b981; }}
317
+ .summary-card.warning .value {{ color: #f59e0b; }}
318
+ .summary-card.error .value {{ color: #ef4444; }}
319
+
320
+ table {{
321
+ width: 100%;
322
+ border-collapse: collapse;
323
+ background: white;
324
+ border: 1px solid #e2e8f0;
325
+ border-radius: 10px;
326
+ overflow: hidden;
327
+ margin-bottom: 2rem;
328
+ font-size: 0.8rem;
329
+ }}
330
+
331
+ th, td {{
332
+ padding: 0.6rem 0.5rem;
333
+ text-align: center;
334
+ border-bottom: 1px solid #e2e8f0;
335
+ vertical-align: middle;
336
+ }}
337
+ th {{
338
+ background: #f1f5f9;
339
+ font-weight: 600;
340
+ font-size: 0.65rem;
341
+ text-transform: uppercase;
342
+ color: #64748b;
343
+ }}
344
+
345
+ td.left {{ text-align: left; }}
346
+
347
+ tr:hover {{ background: #f8fafc; }}
348
+
349
+ .progress-bar {{
350
+ height: 8px;
351
+ background: #e2e8f0;
352
+ border-radius: 4px;
353
+ overflow: hidden;
354
+ min-width: 100px;
355
+ }}
356
+
357
+ .progress-fill {{
358
+ height: 100%;
359
+ border-radius: 4px;
360
+ }}
361
+
362
+ .progress-fill.success {{ background: #10b981; }}
363
+ .progress-fill.good {{ background: #22c55e; }}
364
+ .progress-fill.warning {{ background: #f59e0b; }}
365
+ .progress-fill.error {{ background: #ef4444; }}
366
+
367
+ .two-columns {{
368
+ display: grid;
369
+ grid-template-columns: 1fr 1fr;
370
+ gap: 1.5rem;
371
+ align-items: start;
372
+ }}
373
+
374
+ @media (max-width: 900px) {{
375
+ .two-columns {{ grid-template-columns: 1fr; }}
376
+ }}
377
+
378
+ .two-columns > div {{
379
+ display: flex;
380
+ flex-direction: column;
381
+ }}
382
+
383
+ .two-columns .card {{
384
+ flex: 1;
385
+ min-height: 400px;
386
+ max-height: 400px;
387
+ overflow-y: auto;
388
+ }}
389
+
390
+ .card {{
391
+ background: white;
392
+ border: 1px solid #e2e8f0;
393
+ border-radius: 10px;
394
+ padding: 1rem;
395
+ margin-bottom: 1rem;
396
+ }}
397
+
398
+ .survived-list {{
399
+ max-height: none;
400
+ overflow-y: auto;
401
+ }}
402
+
403
+ .survived-item {{
404
+ padding: 0.75rem;
405
+ border-bottom: 1px solid #e2e8f0;
406
+ font-size: 0.85rem;
407
+ }}
408
+
409
+ .survived-item:last-child {{ border-bottom: none; }}
410
+
411
+ .survived-class {{ font-weight: 600; color: #0f172a; }}
412
+ .survived-method {{ color: #3b82f6; }}
413
+ .survived-description {{ color: #64748b; font-style: italic; margin-top: 0.25rem; font-size: 0.8rem; }}
414
+
415
+ .definitions {{
416
+ background: white;
417
+ border: 1px solid #e2e8f0;
418
+ border-radius: 10px;
419
+ padding: 1.25rem;
420
+ margin-top: 2rem;
421
+ }}
422
+
423
+ .definitions h3 {{ margin-top: 0; color: #0f172a; }}
424
+ .definitions dl {{ margin: 0; }}
425
+ .definitions dt {{ font-weight: 600; margin-top: 0.75rem; color: #0f172a; }}
426
+ .definitions dd {{ color: #64748b; margin-left: 1rem; font-size: 0.9rem; }}
427
+
428
+ footer {{ margin-top: 2rem; text-align: center; color: #94a3b8; font-size: 0.8rem; }}
429
+ </style>
430
+ </head>
431
+ <body>
432
+ <div class="container">
433
+ <h1>🧬 Mutation Testing Report</h1>
434
+ <p class="timestamp">Generated: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</p>
435
+
436
+ <div class="summary-grid">
437
+ <div class="summary-card {'success' if mutation_score >= 80 else 'warning' if mutation_score >= 60 else 'error'}">
438
+ <div class="value">{mutation_score:.1f}%</div>
439
+ <div class="label">Mutation Score</div>
440
+ <div class="detail">{killed:,} / {total:,} killed</div>
441
+ </div>
442
+ <div class="summary-card {'success' if test_strength >= 80 else 'warning' if test_strength >= 60 else 'error'}">
443
+ <div class="value">{test_strength:.1f}%</div>
444
+ <div class="label">Test Strength</div>
445
+ <div class="detail">{killed:,} / {covered:,} covered</div>
446
+ </div>
447
+ <div class="summary-card {'error' if survived > 100 else 'warning' if survived > 50 else ''}">
448
+ <div class="value">{survived}</div>
449
+ <div class="label">Survived</div>
450
+ <div class="detail">Mutations not killed</div>
451
+ </div>
452
+ <div class="summary-card {'error' if no_coverage > 100 else 'warning' if no_coverage > 50 else ''}">
453
+ <div class="value">{no_coverage}</div>
454
+ <div class="label">No Coverage</div>
455
+ <div class="detail">Need more tests</div>
456
+ </div>
457
+ <div class="summary-card">
458
+ <div class="value">{tests_per_mutation:.1f}</div>
459
+ <div class="label">Tests/Mutation</div>
460
+ <div class="detail">{tests_run:,} tests run</div>
461
+ </div>
462
+ </div>
463
+
464
+ <h2>Mutator Breakdown</h2>
465
+ <table>
466
+ <thead>
467
+ <tr>
468
+ <th style="text-align: left;">Mutator</th>
469
+ <th>Kill Rate</th>
470
+ <th style="width: 150px;">Progress</th>
471
+ <th>Killed</th>
472
+ <th>Survived</th>
473
+ <th>No Coverage</th>
474
+ <th>Total</th>
475
+ <th>Status</th>
476
+ </tr>
477
+ </thead>
478
+ <tbody>
479
+ '''
480
+
481
+ # Friendly names for mutators
482
+ mutator_friendly_names = {
483
+ 'NegateConditionals': 'Flip Conditions (if → if not)',
484
+ 'VoidMethodCall': 'Remove Method Calls',
485
+ 'NullReturns': 'Return Null Instead',
486
+ 'EmptyObjectReturns': 'Return Empty Object',
487
+ 'BooleanTrueReturnVals': 'Return True Instead',
488
+ 'BooleanFalseReturnVals': 'Return False Instead',
489
+ 'PrimitiveReturns': 'Return 0/Default Value',
490
+ 'ConditionalsBoundary': 'Change < to ≤, > to ≥',
491
+ 'Math': 'Change Math Operators (+−×÷)',
492
+ 'Increments': 'Change ++ to −−',
493
+ 'InvertNegs': 'Remove Negative Signs',
494
+ }
495
+
496
+ for mutator, stats in mutator_sorted:
497
+ rate = (stats['killed'] / stats['total'] * 100) if stats['total'] > 0 else 0
498
+ status_class = 'success' if rate >= 80 else 'good' if rate >= 70 else 'warning' if rate >= 50 else 'error'
499
+ # Get friendly name or clean up the mutator name
500
+ short_name = mutator.replace('Mutator', '')
501
+ friendly_name = mutator_friendly_names.get(short_name, short_name)
502
+ html += f''' <tr>
503
+ <td class="left">{friendly_name}</td>
504
+ <td><strong>{rate:.1f}%</strong></td>
505
+ <td>
506
+ <div class="progress-bar">
507
+ <div class="progress-fill {status_class}" style="width: {rate}%"></div>
508
+ </div>
509
+ </td>
510
+ <td>{stats['killed']}</td>
511
+ <td>{stats['survived']}</td>
512
+ <td>{stats['no_coverage']}</td>
513
+ <td>{stats['total']}</td>
514
+ <td>{get_status_icon(rate)}</td>
515
+ </tr>
516
+ '''
517
+
518
+ html += ''' </tbody>
519
+ </table>
520
+
521
+ <h2>Package Breakdown</h2>
522
+ <table>
523
+ <thead>
524
+ <tr>
525
+ <th style="text-align: left;">Package</th>
526
+ <th>Kill Rate</th>
527
+ <th style="width: 150px;">Progress</th>
528
+ <th>Killed</th>
529
+ <th>Survived</th>
530
+ <th>No Coverage</th>
531
+ <th>Total</th>
532
+ </tr>
533
+ </thead>
534
+ <tbody>
535
+ '''
536
+
537
+ for package, stats in package_sorted:
538
+ rate = (stats['killed'] / stats['total'] * 100) if stats['total'] > 0 else 0
539
+ status_class = 'success' if rate >= 80 else 'good' if rate >= 70 else 'warning' if rate >= 50 else 'error'
540
+ short_pkg = package.replace('com.freshworks.marketplace.installation.', '')
541
+ html += f''' <tr>
542
+ <td class="left">{short_pkg}</td>
543
+ <td><strong>{rate:.1f}%</strong></td>
544
+ <td>
545
+ <div class="progress-bar">
546
+ <div class="progress-fill {status_class}" style="width: {rate}%"></div>
547
+ </div>
548
+ </td>
549
+ <td>{stats['killed']}</td>
550
+ <td>{stats['survived']}</td>
551
+ <td>{stats['no_coverage']}</td>
552
+ <td>{stats['total']}</td>
553
+ </tr>
554
+ '''
555
+
556
+ html += ''' </tbody>
557
+ </table>
558
+
559
+ <div class="two-columns">
560
+ <div>
561
+ <h2>Classes Needing Attention</h2>
562
+ <div class="card">
563
+ <p style="color: #64748b; margin-bottom: 1rem; font-size: 0.85rem;">
564
+ Classes with most survived mutations
565
+ </p>
566
+ <table>
567
+ <thead>
568
+ <tr>
569
+ <th style="text-align: left;">Class</th>
570
+ <th>Survived</th>
571
+ <th>Total</th>
572
+ <th>Kill Rate</th>
573
+ </tr>
574
+ </thead>
575
+ <tbody>
576
+ '''
577
+
578
+ for class_name, stats in class_sorted:
579
+ if stats['survived'] == 0:
580
+ continue
581
+ rate = (stats['killed'] / stats['total'] * 100) if stats['total'] > 0 else 0
582
+ short_class = class_name.split('.')[-1]
583
+ html += f''' <tr>
584
+ <td class="left">{short_class}</td>
585
+ <td style="color: #ef4444; font-weight: 600;">{stats['survived']}</td>
586
+ <td>{stats['total']}</td>
587
+ <td>{rate:.1f}%</td>
588
+ </tr>
589
+ '''
590
+
591
+ html += ''' </tbody>
592
+ </table>
593
+ </div>
594
+ </div>
595
+
596
+ <div>
597
+ <h2>Sample Survived Mutations</h2>
598
+ <div class="card survived-list">
599
+ '''
600
+
601
+ for mut in survived_mutations[:20]:
602
+ html += f''' <div class="survived-item">
603
+ <span class="survived-class">{mut['class']}</span>.<span class="survived-method">{mut['method']}</span> (line {mut['line']})
604
+ <div class="survived-description">{mut['description']}</div>
605
+ </div>
606
+ '''
607
+
608
+ html += ''' </div>
609
+ </div>
610
+ </div>
611
+
612
+ <div class="definitions">
613
+ <h3>Definitions</h3>
614
+ <dl>
615
+ <dt>Mutation Score</dt>
616
+ <dd>Percentage of mutations killed by tests. Target: ≥80%</dd>
617
+
618
+ <dt>Test Strength</dt>
619
+ <dd>Kill rate among covered mutations (excludes NO_COVERAGE). Shows test quality.</dd>
620
+
621
+ <dt>Killed</dt>
622
+ <dd>Mutations detected by at least one test - good!</dd>
623
+
624
+ <dt>Survived</dt>
625
+ <dd>Mutations not detected - tests need improvement.</dd>
626
+
627
+ <dt>No Coverage</dt>
628
+ <dd>Code not covered by any test - need more tests.</dd>
629
+
630
+ <dt>Common Mutators</dt>
631
+ <dd>
632
+ <strong>NegateConditionals:</strong> Changes if(x) to if(!x)<br>
633
+ <strong>VoidMethodCall:</strong> Removes void method calls<br>
634
+ <strong>NullReturns:</strong> Returns null instead of actual value<br>
635
+ <strong>BooleanReturns:</strong> Swaps true/false return values<br>
636
+ <strong>ConditionalsBoundary:</strong> Changes &lt; to &lt;=, etc.
637
+ </dd>
638
+ </dl>
639
+ </div>
640
+
641
+ <footer>
642
+ Generated by Mutation Report Generator | Data from PIT Mutation Testing
643
+ </footer>
644
+ </div>
645
+ </body>
646
+ </html>
647
+ '''
648
+
649
+ # Write HTML
650
+ with open(html_output, 'w') as f:
651
+ f.write(html)
652
+
653
+ print(f"Total mutations: {total}")
654
+ print(f"Killed: {killed} ({mutation_score:.1f}%)")
655
+ print(f"Survived: {survived}")
656
+ print(f"No Coverage: {no_coverage}")
657
+ print(f"Test Strength: {test_strength:.1f}%")
658
+ print(f"Tests per Mutation: {tests_per_mutation:.1f}")
659
+ PYTHON_SCRIPT
660
+ SKIP_INLINE_PYTHON
661
+
662
+ echo ""
663
+ echo -e "${GREEN}✅ Report generated: $HTML_OUTPUT${NC}"
664
+
665
+ # Cleanup old reports (keep only last one)
666
+ cd "$OUTPUT_DIR"
667
+ ls -t mutation-report-*.html 2>/dev/null | tail -n +2 | xargs -I {} rm -f {} 2>/dev/null || true
668
+ echo -e "${BLUE}Cleaned up old reports (kept only latest)${NC}"
669
+
670
+ echo ""
671
+ echo -e "${YELLOW}To view the report:${NC}"
672
+ echo " open $HTML_OUTPUT"