shipwright-cli 1.10.0 → 2.0.0
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 +114 -36
- package/completions/_shipwright +212 -32
- package/completions/shipwright.bash +97 -25
- package/docs/strategy/01-market-research.md +619 -0
- package/docs/strategy/02-mission-and-brand.md +587 -0
- package/docs/strategy/03-gtm-and-roadmap.md +759 -0
- package/docs/strategy/QUICK-START.txt +289 -0
- package/docs/strategy/README.md +172 -0
- package/package.json +4 -2
- package/scripts/sw +208 -1
- package/scripts/sw-activity.sh +500 -0
- package/scripts/sw-adaptive.sh +925 -0
- package/scripts/sw-adversarial.sh +1 -1
- package/scripts/sw-architecture-enforcer.sh +1 -1
- package/scripts/sw-auth.sh +613 -0
- package/scripts/sw-autonomous.sh +664 -0
- package/scripts/sw-changelog.sh +704 -0
- package/scripts/sw-checkpoint.sh +1 -1
- package/scripts/sw-ci.sh +602 -0
- package/scripts/sw-cleanup.sh +1 -1
- package/scripts/sw-code-review.sh +637 -0
- package/scripts/sw-connect.sh +1 -1
- package/scripts/sw-context.sh +605 -0
- package/scripts/sw-cost.sh +1 -1
- package/scripts/sw-daemon.sh +432 -130
- package/scripts/sw-dashboard.sh +1 -1
- package/scripts/sw-db.sh +540 -0
- package/scripts/sw-decompose.sh +539 -0
- package/scripts/sw-deps.sh +551 -0
- package/scripts/sw-developer-simulation.sh +1 -1
- package/scripts/sw-discovery.sh +412 -0
- package/scripts/sw-docs-agent.sh +539 -0
- package/scripts/sw-docs.sh +1 -1
- package/scripts/sw-doctor.sh +59 -1
- package/scripts/sw-dora.sh +615 -0
- package/scripts/sw-durable.sh +710 -0
- package/scripts/sw-e2e-orchestrator.sh +535 -0
- package/scripts/sw-eventbus.sh +393 -0
- package/scripts/sw-feedback.sh +471 -0
- package/scripts/sw-fix.sh +1 -1
- package/scripts/sw-fleet-discover.sh +567 -0
- package/scripts/sw-fleet-viz.sh +404 -0
- package/scripts/sw-fleet.sh +8 -1
- package/scripts/sw-github-app.sh +596 -0
- package/scripts/sw-github-checks.sh +1 -1
- package/scripts/sw-github-deploy.sh +1 -1
- package/scripts/sw-github-graphql.sh +1 -1
- package/scripts/sw-guild.sh +569 -0
- package/scripts/sw-heartbeat.sh +1 -1
- package/scripts/sw-hygiene.sh +559 -0
- package/scripts/sw-incident.sh +617 -0
- package/scripts/sw-init.sh +88 -1
- package/scripts/sw-instrument.sh +699 -0
- package/scripts/sw-intelligence.sh +1 -1
- package/scripts/sw-jira.sh +1 -1
- package/scripts/sw-launchd.sh +363 -28
- package/scripts/sw-linear.sh +1 -1
- package/scripts/sw-logs.sh +1 -1
- package/scripts/sw-loop.sh +64 -3
- package/scripts/sw-memory.sh +1 -1
- package/scripts/sw-mission-control.sh +487 -0
- package/scripts/sw-model-router.sh +545 -0
- package/scripts/sw-otel.sh +596 -0
- package/scripts/sw-oversight.sh +689 -0
- package/scripts/sw-pipeline-composer.sh +1 -1
- package/scripts/sw-pipeline-vitals.sh +1 -1
- package/scripts/sw-pipeline.sh +687 -24
- package/scripts/sw-pm.sh +693 -0
- package/scripts/sw-pr-lifecycle.sh +522 -0
- package/scripts/sw-predictive.sh +1 -1
- package/scripts/sw-prep.sh +1 -1
- package/scripts/sw-ps.sh +1 -1
- package/scripts/sw-public-dashboard.sh +798 -0
- package/scripts/sw-quality.sh +595 -0
- package/scripts/sw-reaper.sh +1 -1
- package/scripts/sw-recruit.sh +573 -0
- package/scripts/sw-regression.sh +642 -0
- package/scripts/sw-release-manager.sh +736 -0
- package/scripts/sw-release.sh +706 -0
- package/scripts/sw-remote.sh +1 -1
- package/scripts/sw-replay.sh +520 -0
- package/scripts/sw-retro.sh +691 -0
- package/scripts/sw-scale.sh +444 -0
- package/scripts/sw-security-audit.sh +505 -0
- package/scripts/sw-self-optimize.sh +1 -1
- package/scripts/sw-session.sh +1 -1
- package/scripts/sw-setup.sh +1 -1
- package/scripts/sw-standup.sh +712 -0
- package/scripts/sw-status.sh +1 -1
- package/scripts/sw-strategic.sh +658 -0
- package/scripts/sw-stream.sh +450 -0
- package/scripts/sw-swarm.sh +583 -0
- package/scripts/sw-team-stages.sh +511 -0
- package/scripts/sw-templates.sh +1 -1
- package/scripts/sw-testgen.sh +515 -0
- package/scripts/sw-tmux-pipeline.sh +554 -0
- package/scripts/sw-tmux.sh +1 -1
- package/scripts/sw-trace.sh +485 -0
- package/scripts/sw-tracker-github.sh +188 -0
- package/scripts/sw-tracker-jira.sh +172 -0
- package/scripts/sw-tracker-linear.sh +251 -0
- package/scripts/sw-tracker.sh +117 -2
- package/scripts/sw-triage.sh +603 -0
- package/scripts/sw-upgrade.sh +1 -1
- package/scripts/sw-ux.sh +677 -0
- package/scripts/sw-webhook.sh +627 -0
- package/scripts/sw-widgets.sh +530 -0
- package/scripts/sw-worktree.sh +1 -1
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
3
|
+
# ║ shipwright testgen — Autonomous test generation and coverage maintenance ║
|
|
4
|
+
# ║ Analyze coverage · Generate tests · Maintain thresholds · Score quality ║
|
|
5
|
+
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
|
|
8
|
+
|
|
9
|
+
VERSION="1.13.0"
|
|
10
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
|
+
|
|
12
|
+
# ─── Handle subcommands ───────────────────────────────────────────────────────
|
|
13
|
+
if [[ "${1:-}" == "test" ]]; then
|
|
14
|
+
shift
|
|
15
|
+
exec "$SCRIPT_DIR/sw-testgen-test.sh" "$@"
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
# ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
|
|
19
|
+
CYAN='\033[38;2;0;212;255m' # #00d4ff — primary accent
|
|
20
|
+
PURPLE='\033[38;2;124;58;237m' # #7c3aed — secondary
|
|
21
|
+
BLUE='\033[38;2;0;102;255m' # #0066ff — tertiary
|
|
22
|
+
GREEN='\033[38;2;74;222;128m' # success
|
|
23
|
+
YELLOW='\033[38;2;250;204;21m' # warning
|
|
24
|
+
RED='\033[38;2;248;113;113m' # error
|
|
25
|
+
DIM='\033[2m'
|
|
26
|
+
BOLD='\033[1m'
|
|
27
|
+
RESET='\033[0m'
|
|
28
|
+
|
|
29
|
+
# ─── Cross-platform compatibility ──────────────────────────────────────────
|
|
30
|
+
# shellcheck source=lib/compat.sh
|
|
31
|
+
[[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
|
|
32
|
+
|
|
33
|
+
# ─── Output Helpers ─────────────────────────────────────────────────────────
|
|
34
|
+
info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
|
|
35
|
+
success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
|
|
36
|
+
warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
|
|
37
|
+
error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
|
|
38
|
+
|
|
39
|
+
emit_event() {
|
|
40
|
+
local type="$1"
|
|
41
|
+
shift
|
|
42
|
+
local json_data="{\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"type\":\"$type\""
|
|
43
|
+
for pair in "$@"; do
|
|
44
|
+
json_data="$json_data,\"$pair\""
|
|
45
|
+
done
|
|
46
|
+
json_data="$json_data}"
|
|
47
|
+
echo "$json_data" >> "${EVENTS_FILE:-.shipwright-events.jsonl}"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
|
|
51
|
+
|
|
52
|
+
# ─── Configuration ───────────────────────────────────────────────────────────
|
|
53
|
+
PROJECT_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
54
|
+
SCRIPTS_DIR="$PROJECT_ROOT/scripts"
|
|
55
|
+
TEST_SUITES_DIR="$PROJECT_ROOT/scripts"
|
|
56
|
+
COVERAGE_THRESHOLD=70
|
|
57
|
+
TESTGEN_DIR="${TESTGEN_DIR:-.claude/testgen}"
|
|
58
|
+
COVERAGE_DB="$TESTGEN_DIR/coverage.json"
|
|
59
|
+
QUALITY_DB="$TESTGEN_DIR/quality.json"
|
|
60
|
+
REGRESSION_DB="$TESTGEN_DIR/regressions.json"
|
|
61
|
+
|
|
62
|
+
# ─── Help ───────────────────────────────────────────────────────────────────
|
|
63
|
+
show_help() {
|
|
64
|
+
echo -e "${CYAN}${BOLD}shipwright testgen${RESET} ${DIM}v${VERSION}${RESET} — Test generation and coverage maintenance"
|
|
65
|
+
echo ""
|
|
66
|
+
echo -e "${BOLD}USAGE${RESET}"
|
|
67
|
+
echo -e " ${CYAN}shipwright testgen${RESET} <command> [options]"
|
|
68
|
+
echo ""
|
|
69
|
+
echo -e "${BOLD}COMMANDS${RESET}"
|
|
70
|
+
echo -e " ${CYAN}coverage${RESET} Show test coverage analysis"
|
|
71
|
+
echo -e " ${CYAN}generate${RESET} Generate tests for uncovered functions"
|
|
72
|
+
echo -e " ${CYAN}gaps${RESET} Show specific untested code paths"
|
|
73
|
+
echo -e " ${CYAN}quality${RESET} Score existing test quality"
|
|
74
|
+
echo -e " ${CYAN}maintain${RESET} Check if tests need updating after code changes"
|
|
75
|
+
echo -e " ${CYAN}threshold${RESET} Set/check coverage threshold"
|
|
76
|
+
echo -e " ${CYAN}regression${RESET} Compare test results across runs"
|
|
77
|
+
echo -e " ${CYAN}help${RESET} Show this help message"
|
|
78
|
+
echo ""
|
|
79
|
+
echo -e "${BOLD}OPTIONS${RESET}"
|
|
80
|
+
echo -e " ${CYAN}--target${RESET} <script> Target script for analysis"
|
|
81
|
+
echo -e " ${CYAN}--threshold${RESET} <num> Set minimum coverage % (default: 70)"
|
|
82
|
+
echo -e " ${CYAN}--json${RESET} Output JSON format"
|
|
83
|
+
echo -e " ${CYAN}--verbose${RESET} Detailed output"
|
|
84
|
+
echo ""
|
|
85
|
+
echo -e "${BOLD}EXAMPLES${RESET}"
|
|
86
|
+
echo -e " ${DIM}shipwright testgen coverage${RESET} # Overall coverage"
|
|
87
|
+
echo -e " ${DIM}shipwright testgen coverage --target sw-daemon.sh${RESET} # Target script"
|
|
88
|
+
echo -e " ${DIM}shipwright testgen generate --threshold 75${RESET} # Generate with threshold"
|
|
89
|
+
echo -e " ${DIM}shipwright testgen quality sw-pipeline-test.sh${RESET} # Score test quality"
|
|
90
|
+
echo ""
|
|
91
|
+
echo -e "${DIM}Docs: https://sethdford.github.io/shipwright${RESET}"
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
95
|
+
# COVERAGE ANALYSIS
|
|
96
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
97
|
+
|
|
98
|
+
analyze_coverage() {
|
|
99
|
+
local target_script="${1:-.}"
|
|
100
|
+
local output_format="${2:-text}"
|
|
101
|
+
|
|
102
|
+
mkdir -p "$TESTGEN_DIR"
|
|
103
|
+
|
|
104
|
+
# Extract all function definitions from target
|
|
105
|
+
local total_functions=0
|
|
106
|
+
local tested_functions=0
|
|
107
|
+
local function_names=""
|
|
108
|
+
|
|
109
|
+
if [[ -f "$target_script" ]]; then
|
|
110
|
+
# Parse function definitions
|
|
111
|
+
function_names=$(grep -E '^[a-zA-Z_][a-zA-Z0-9_]*\(\)' "$target_script" | sed 's/().*//' || echo "")
|
|
112
|
+
total_functions=$(echo "$function_names" | grep -c . || echo "0")
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
# Find existing tests that call these functions
|
|
116
|
+
if [[ -n "$function_names" ]]; then
|
|
117
|
+
local test_file
|
|
118
|
+
while IFS= read -r func; do
|
|
119
|
+
[[ -z "$func" ]] && continue
|
|
120
|
+
for test_file in "$TEST_SUITES_DIR"/*-test.sh; do
|
|
121
|
+
[[ -f "$test_file" ]] || continue
|
|
122
|
+
if grep -q "$func" "$test_file" 2>/dev/null; then
|
|
123
|
+
tested_functions=$((tested_functions + 1))
|
|
124
|
+
break
|
|
125
|
+
fi
|
|
126
|
+
done
|
|
127
|
+
done << EOF
|
|
128
|
+
$function_names
|
|
129
|
+
EOF
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
local coverage_pct=0
|
|
133
|
+
if [[ $total_functions -gt 0 ]]; then
|
|
134
|
+
coverage_pct=$((tested_functions * 100 / total_functions))
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
if [[ "$output_format" == "json" ]]; then
|
|
138
|
+
jq -n \
|
|
139
|
+
--arg target "$target_script" \
|
|
140
|
+
--argjson total "$total_functions" \
|
|
141
|
+
--argjson tested "$tested_functions" \
|
|
142
|
+
--argjson pct "$coverage_pct" \
|
|
143
|
+
'{target: $target, total_functions: $total, tested_functions: $tested, coverage_percent: $pct}'
|
|
144
|
+
else
|
|
145
|
+
info "Coverage Analysis"
|
|
146
|
+
echo ""
|
|
147
|
+
echo -e " ${CYAN}Target:${RESET} $target_script"
|
|
148
|
+
echo -e " ${CYAN}Functions:${RESET} $total_functions total"
|
|
149
|
+
echo -e " ${CYAN}Tested:${RESET} $tested_functions"
|
|
150
|
+
echo -e " ${CYAN}Coverage:${RESET} ${coverage_pct}%"
|
|
151
|
+
echo ""
|
|
152
|
+
|
|
153
|
+
if [[ $coverage_pct -lt $COVERAGE_THRESHOLD ]]; then
|
|
154
|
+
warn "Coverage below threshold ($COVERAGE_THRESHOLD%)"
|
|
155
|
+
else
|
|
156
|
+
success "Coverage meets threshold"
|
|
157
|
+
fi
|
|
158
|
+
fi
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
162
|
+
# TEST GENERATION
|
|
163
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
164
|
+
|
|
165
|
+
generate_tests() {
|
|
166
|
+
local target_script="${1:-.}"
|
|
167
|
+
local threshold="${2:-$COVERAGE_THRESHOLD}"
|
|
168
|
+
|
|
169
|
+
mkdir -p "$TESTGEN_DIR"
|
|
170
|
+
|
|
171
|
+
info "Generating tests for $target_script"
|
|
172
|
+
|
|
173
|
+
# Extract untested functions
|
|
174
|
+
local all_functions=""
|
|
175
|
+
all_functions=$(grep -E '^[a-zA-Z_][a-zA-Z0-9_]*\(\)' "$target_script" | sed 's/().*//' || echo "")
|
|
176
|
+
|
|
177
|
+
local untested_functions=""
|
|
178
|
+
if [[ -n "$all_functions" ]]; then
|
|
179
|
+
while IFS= read -r func_name; do
|
|
180
|
+
[[ -z "$func_name" ]] && continue
|
|
181
|
+
local found=false
|
|
182
|
+
for test_file in "$TEST_SUITES_DIR"/*-test.sh; do
|
|
183
|
+
[[ -f "$test_file" ]] || continue
|
|
184
|
+
if grep -q "$func_name" "$test_file" 2>/dev/null; then
|
|
185
|
+
found=true
|
|
186
|
+
break
|
|
187
|
+
fi
|
|
188
|
+
done
|
|
189
|
+
[[ "$found" == "false" ]] && untested_functions="${untested_functions}${func_name}"$'\n'
|
|
190
|
+
done << EOF
|
|
191
|
+
$all_functions
|
|
192
|
+
EOF
|
|
193
|
+
fi
|
|
194
|
+
|
|
195
|
+
local untested_count=0
|
|
196
|
+
[[ -n "$untested_functions" ]] && untested_count=$(echo "$untested_functions" | grep -c . || echo "0")
|
|
197
|
+
|
|
198
|
+
if [[ $untested_count -eq 0 ]]; then
|
|
199
|
+
success "All functions have tests"
|
|
200
|
+
return 0
|
|
201
|
+
fi
|
|
202
|
+
|
|
203
|
+
echo ""
|
|
204
|
+
info "Untested functions: $untested_count"
|
|
205
|
+
echo "$untested_functions" | while IFS= read -r func; do
|
|
206
|
+
[[ -z "$func" ]] && continue
|
|
207
|
+
echo -e " ${YELLOW}•${RESET} $func"
|
|
208
|
+
done
|
|
209
|
+
echo ""
|
|
210
|
+
|
|
211
|
+
# Generate test template
|
|
212
|
+
local test_template_file="$TESTGEN_DIR/generated-tests.sh"
|
|
213
|
+
{
|
|
214
|
+
echo "#!/usr/bin/env bash"
|
|
215
|
+
echo "# Generated tests for $target_script"
|
|
216
|
+
echo "set -euo pipefail"
|
|
217
|
+
echo "trap 'echo \"ERROR: \$BASH_SOURCE:\$LINENO exited with status \$?\" >&2' ERR"
|
|
218
|
+
echo ""
|
|
219
|
+
echo "SCRIPT_DIR=\"\$(cd \"\$(dirname \"\${BASH_SOURCE[0]}\")\" && pwd)\""
|
|
220
|
+
echo ""
|
|
221
|
+
echo "# Test counters"
|
|
222
|
+
echo "PASS=0"
|
|
223
|
+
echo "FAIL=0"
|
|
224
|
+
echo ""
|
|
225
|
+
|
|
226
|
+
echo "$untested_functions" | while IFS= read -r func; do
|
|
227
|
+
[[ -z "$func" ]] && continue
|
|
228
|
+
echo "test_${func}() {"
|
|
229
|
+
echo " # TODO: Implement test for $func"
|
|
230
|
+
echo " # - Test happy path"
|
|
231
|
+
echo " # - Test error cases"
|
|
232
|
+
echo " # - Test edge cases"
|
|
233
|
+
echo " ((PASS++))"
|
|
234
|
+
echo "}"
|
|
235
|
+
echo ""
|
|
236
|
+
done
|
|
237
|
+
|
|
238
|
+
echo "# Run all tests"
|
|
239
|
+
echo "$untested_functions" | while IFS= read -r func; do
|
|
240
|
+
[[ -z "$func" ]] && continue
|
|
241
|
+
echo "test_${func}"
|
|
242
|
+
done
|
|
243
|
+
echo ""
|
|
244
|
+
echo "echo \"Results: \$PASS passed, \$FAIL failed\""
|
|
245
|
+
} > "$test_template_file"
|
|
246
|
+
|
|
247
|
+
chmod +x "$test_template_file"
|
|
248
|
+
success "Generated test template: $test_template_file"
|
|
249
|
+
info "Review and implement test logic, then move to test suite"
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
253
|
+
# GAP DETECTION
|
|
254
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
255
|
+
|
|
256
|
+
show_gaps() {
|
|
257
|
+
local target_script="${1:-.}"
|
|
258
|
+
|
|
259
|
+
info "Finding test gaps in $target_script"
|
|
260
|
+
|
|
261
|
+
# Extract functions and their line numbers
|
|
262
|
+
local gap_found=0
|
|
263
|
+
while IFS=: read -r line_num func_name; do
|
|
264
|
+
[[ -z "$func_name" ]] && continue
|
|
265
|
+
# Check if tested
|
|
266
|
+
local tested=false
|
|
267
|
+
for test_file in "$TEST_SUITES_DIR"/*-test.sh; do
|
|
268
|
+
[[ -f "$test_file" ]] || continue
|
|
269
|
+
if grep -q "$func_name" "$test_file" 2>/dev/null; then
|
|
270
|
+
tested=true
|
|
271
|
+
break
|
|
272
|
+
fi
|
|
273
|
+
done
|
|
274
|
+
|
|
275
|
+
if [[ "$tested" == "false" ]]; then
|
|
276
|
+
gap_found=1
|
|
277
|
+
echo -e " ${YELLOW}Gap at line $line_num:${RESET} $func_name()"
|
|
278
|
+
fi
|
|
279
|
+
done < <(grep -En '^[a-zA-Z_][a-zA-Z0-9_]*\(\)' "$target_script" | sed 's/:.*\([a-zA-Z_][a-zA-Z0-9_]*\)().*/:\1/' || true)
|
|
280
|
+
|
|
281
|
+
[[ $gap_found -eq 0 ]] && success "No test gaps found"
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
285
|
+
# TEST QUALITY SCORING
|
|
286
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
287
|
+
|
|
288
|
+
score_quality() {
|
|
289
|
+
local test_file="$1"
|
|
290
|
+
|
|
291
|
+
[[ -f "$test_file" ]] || {
|
|
292
|
+
error "Test file not found: $test_file"
|
|
293
|
+
return 1
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
info "Scoring test quality: $(basename "$test_file")"
|
|
297
|
+
|
|
298
|
+
# Count assertions
|
|
299
|
+
local assertion_count=0
|
|
300
|
+
assertion_count=$(grep -c -E '(assert_|test_|expect_)' "$test_file" || echo "0")
|
|
301
|
+
|
|
302
|
+
# Count edge case patterns
|
|
303
|
+
local edge_case_count=0
|
|
304
|
+
edge_case_count=$(grep -c -E '(empty|null|nil|missing|invalid|error|fail)' "$test_file" || echo "0")
|
|
305
|
+
|
|
306
|
+
# Count error path tests
|
|
307
|
+
local error_path_count=0
|
|
308
|
+
error_path_count=$(grep -c -E '(exit|return 1|error|ERROR)' "$test_file" || echo "0")
|
|
309
|
+
|
|
310
|
+
# Calculate score (0-100)
|
|
311
|
+
local quality_score=0
|
|
312
|
+
quality_score=$((assertion_count * 10 + edge_case_count * 5 + error_path_count * 5))
|
|
313
|
+
[[ $quality_score -gt 100 ]] && quality_score=100
|
|
314
|
+
|
|
315
|
+
echo ""
|
|
316
|
+
echo -e " ${CYAN}Assertions:${RESET} $assertion_count"
|
|
317
|
+
echo -e " ${CYAN}Edge cases:${RESET} $edge_case_count"
|
|
318
|
+
echo -e " ${CYAN}Error paths:${RESET} $error_path_count"
|
|
319
|
+
echo -e " ${CYAN}Quality score:${RESET} $quality_score/100"
|
|
320
|
+
echo ""
|
|
321
|
+
|
|
322
|
+
if [[ $quality_score -ge 80 ]]; then
|
|
323
|
+
success "Excellent test quality"
|
|
324
|
+
elif [[ $quality_score -ge 60 ]]; then
|
|
325
|
+
warn "Good test quality, could improve"
|
|
326
|
+
else
|
|
327
|
+
warn "Low test quality, needs improvement"
|
|
328
|
+
fi
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
332
|
+
# TEST MAINTENANCE
|
|
333
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
334
|
+
|
|
335
|
+
maintain_tests() {
|
|
336
|
+
local source_file="${1:-.}"
|
|
337
|
+
|
|
338
|
+
info "Checking test maintenance for $source_file"
|
|
339
|
+
|
|
340
|
+
# Check for modified functions
|
|
341
|
+
local modified_count=0
|
|
342
|
+
local test_files=("$TEST_SUITES_DIR"/*-test.sh)
|
|
343
|
+
|
|
344
|
+
for test_file in "${test_files[@]}"; do
|
|
345
|
+
[[ -f "$test_file" ]] || continue
|
|
346
|
+
|
|
347
|
+
# Extract functions tested by this test
|
|
348
|
+
while IFS= read -r func_name; do
|
|
349
|
+
[[ -z "$func_name" ]] && continue
|
|
350
|
+
|
|
351
|
+
# Check if function signature changed
|
|
352
|
+
if git diff --no-index "$source_file" "$source_file" 2>/dev/null | grep -q "$func_name"; then
|
|
353
|
+
modified_count=$((modified_count + 1))
|
|
354
|
+
warn "Function $func_name may need test updates"
|
|
355
|
+
fi
|
|
356
|
+
done < <(grep -E "$func_name\(" "$test_file" | sed 's/.*\([a-zA-Z_][a-zA-Z0-9_]*\)(.*/\1/' | sort -u || true)
|
|
357
|
+
done
|
|
358
|
+
|
|
359
|
+
if [[ $modified_count -eq 0 ]]; then
|
|
360
|
+
success "All tests up to date with source"
|
|
361
|
+
fi
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
365
|
+
# THRESHOLD MANAGEMENT
|
|
366
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
367
|
+
|
|
368
|
+
manage_threshold() {
|
|
369
|
+
local action="${1:-show}"
|
|
370
|
+
local value="${2:-}"
|
|
371
|
+
|
|
372
|
+
case "$action" in
|
|
373
|
+
set)
|
|
374
|
+
[[ -z "$value" ]] && {
|
|
375
|
+
error "Threshold value required"
|
|
376
|
+
return 1
|
|
377
|
+
}
|
|
378
|
+
COVERAGE_THRESHOLD="$value"
|
|
379
|
+
success "Coverage threshold set to $value%"
|
|
380
|
+
;;
|
|
381
|
+
show)
|
|
382
|
+
info "Current coverage threshold: $COVERAGE_THRESHOLD%"
|
|
383
|
+
;;
|
|
384
|
+
check)
|
|
385
|
+
# Compare current coverage against threshold
|
|
386
|
+
local coverage_pct
|
|
387
|
+
coverage_pct=$(analyze_coverage "${value:-.}" json 2>/dev/null | jq -r '.coverage_percent // 0')
|
|
388
|
+
|
|
389
|
+
if [[ $coverage_pct -lt $COVERAGE_THRESHOLD ]]; then
|
|
390
|
+
warn "Coverage ${coverage_pct}% below threshold ($COVERAGE_THRESHOLD%)"
|
|
391
|
+
return 1
|
|
392
|
+
else
|
|
393
|
+
success "Coverage ${coverage_pct}% meets threshold ($COVERAGE_THRESHOLD%)"
|
|
394
|
+
fi
|
|
395
|
+
;;
|
|
396
|
+
*)
|
|
397
|
+
error "Unknown threshold action: $action"
|
|
398
|
+
return 1
|
|
399
|
+
;;
|
|
400
|
+
esac
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
404
|
+
# REGRESSION DETECTION
|
|
405
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
406
|
+
|
|
407
|
+
detect_regressions() {
|
|
408
|
+
mkdir -p "$TESTGEN_DIR"
|
|
409
|
+
|
|
410
|
+
info "Scanning for test regressions"
|
|
411
|
+
|
|
412
|
+
# Run all test suites and capture results
|
|
413
|
+
local current_results=()
|
|
414
|
+
local test_file
|
|
415
|
+
for test_file in "$TEST_SUITES_DIR"/*-test.sh; do
|
|
416
|
+
[[ -f "$test_file" ]] || continue
|
|
417
|
+
[[ "$(basename "$test_file")" == "sw-testgen-test.sh" ]] && continue
|
|
418
|
+
|
|
419
|
+
local result
|
|
420
|
+
if bash "$test_file" &>/dev/null; then
|
|
421
|
+
result="PASS"
|
|
422
|
+
else
|
|
423
|
+
result="FAIL"
|
|
424
|
+
fi
|
|
425
|
+
current_results+=("$(basename "$test_file"):$result")
|
|
426
|
+
done
|
|
427
|
+
|
|
428
|
+
# Compare with previous results
|
|
429
|
+
if [[ -f "$REGRESSION_DB" ]]; then
|
|
430
|
+
info "Comparing with previous run..."
|
|
431
|
+
|
|
432
|
+
local regression_found=0
|
|
433
|
+
for entry in "${current_results[@]}"; do
|
|
434
|
+
local test_name="${entry%:*}"
|
|
435
|
+
local current_status="${entry##*:}"
|
|
436
|
+
|
|
437
|
+
local previous_status
|
|
438
|
+
previous_status=$(jq -r ".\"$test_name\" // \"UNKNOWN\"" "$REGRESSION_DB" 2>/dev/null || echo "UNKNOWN")
|
|
439
|
+
|
|
440
|
+
if [[ "$previous_status" == "PASS" && "$current_status" == "FAIL" ]]; then
|
|
441
|
+
warn "Regression detected: $test_name (was PASS, now FAIL)"
|
|
442
|
+
regression_found=$((regression_found + 1))
|
|
443
|
+
fi
|
|
444
|
+
done
|
|
445
|
+
|
|
446
|
+
if [[ $regression_found -eq 0 ]]; then
|
|
447
|
+
success "No regressions detected"
|
|
448
|
+
fi
|
|
449
|
+
fi
|
|
450
|
+
|
|
451
|
+
# Save current results for future comparison
|
|
452
|
+
{
|
|
453
|
+
jq -n '.' > "$REGRESSION_DB"
|
|
454
|
+
for entry in "${current_results[@]}"; do
|
|
455
|
+
local test_name="${entry%:*}"
|
|
456
|
+
local status="${entry##*:}"
|
|
457
|
+
jq --arg name "$test_name" --arg status "$status" '.[$name] = $status' "$REGRESSION_DB" > "$REGRESSION_DB.tmp"
|
|
458
|
+
mv "$REGRESSION_DB.tmp" "$REGRESSION_DB"
|
|
459
|
+
done
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
success "Regression detection saved to $REGRESSION_DB"
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
466
|
+
# MAIN COMMAND ROUTER
|
|
467
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
468
|
+
|
|
469
|
+
main() {
|
|
470
|
+
local cmd="${1:-help}"
|
|
471
|
+
|
|
472
|
+
case "$cmd" in
|
|
473
|
+
coverage)
|
|
474
|
+
shift || true
|
|
475
|
+
analyze_coverage "$@"
|
|
476
|
+
;;
|
|
477
|
+
generate)
|
|
478
|
+
shift || true
|
|
479
|
+
generate_tests "$@"
|
|
480
|
+
;;
|
|
481
|
+
gaps)
|
|
482
|
+
shift || true
|
|
483
|
+
show_gaps "$@"
|
|
484
|
+
;;
|
|
485
|
+
quality)
|
|
486
|
+
shift || true
|
|
487
|
+
score_quality "$@"
|
|
488
|
+
;;
|
|
489
|
+
maintain)
|
|
490
|
+
shift || true
|
|
491
|
+
maintain_tests "$@"
|
|
492
|
+
;;
|
|
493
|
+
threshold)
|
|
494
|
+
shift || true
|
|
495
|
+
manage_threshold "$@"
|
|
496
|
+
;;
|
|
497
|
+
regression)
|
|
498
|
+
shift || true
|
|
499
|
+
detect_regressions "$@"
|
|
500
|
+
;;
|
|
501
|
+
help|--help|-h)
|
|
502
|
+
show_help
|
|
503
|
+
;;
|
|
504
|
+
*)
|
|
505
|
+
error "Unknown command: $cmd"
|
|
506
|
+
echo ""
|
|
507
|
+
show_help
|
|
508
|
+
exit 1
|
|
509
|
+
;;
|
|
510
|
+
esac
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
|
514
|
+
main "$@"
|
|
515
|
+
fi
|