@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,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 < to <=, 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"
|