-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathgit-pre-commit
More file actions
executable file
·281 lines (254 loc) · 8.14 KB
/
git-pre-commit
File metadata and controls
executable file
·281 lines (254 loc) · 8.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
#!/bin/bash
#
# MemCP Pre-commit Hook
# Runs SQL tests before allowing commits using a single MemCP instance
#
echo "🔍 Running MemCP pre-commit tests..."
# Global state for reliable cleanup
did_cleanup=0
active_pids=()
# Build memcp to ensure latest changes are included
echo "🔨 Building memcp..."
build_flags=""
if [ "${MEMCP_COVERAGE:-}" = "1" ]; then
build_flags="-cover"
export GOCOVERDIR="${MEMCP_COVERDIR:-/tmp/memcp-coverage}"
rm -rf "$GOCOVERDIR"
mkdir -p "$GOCOVERDIR"
echo "📊 Coverage mode enabled (GOCOVERDIR=$GOCOVERDIR)"
fi
if ! go build $build_flags -o memcp; then
echo "❌ Build failed, commit aborted"
exit 1
fi
# Find all numbered test files (01_*.yaml, 02_*.yaml, etc.)
test_files=$(find tests/ -name "[0-9][0-9]_*.yaml" | sort)
if [ -z "$test_files" ]; then
echo "❌ No test files found in tests/ directory"
exit 1
fi
echo "Found test files:"
echo "$test_files"
echo ""
test_port=4480
run_memcp_forever() {
trap 'kill $memcp_pid 2>/dev/null; exit 0' TERM INT
while true; do
echo "🚀 (re)Starting MemCP on port $test_port..."
pkill -f "memcp.*--api-port=$test_port" || true
sleep 1
memcp_log="${MEMCP_LOG:-/tmp/memcp-test.log}"
./memcp -data "/tmp/memcp-pre-commit-tests" --api-port=$test_port --mysql-port=$((test_port + 1000)) --disable-mysql lib/main.scm < "$fifo_path" >> "$memcp_log" 2>&1 &
memcp_pid=$!
# wait for process to exit; test loop will poll readiness
wait $memcp_pid
rc=$?
if [ $rc -ne 0 ]; then
echo "⚠️ MemCP exited unexpectedly (code=$rc). Restarting..."
fi
sleep 0.5
done
}
wait_for_sql_ready() {
local retries="${1:-60}"
local delay="${2:-1}"
for ((i=1; i<=retries; i++)); do
if curl -s "http://localhost:$test_port" >/dev/null 2>&1; then
http_code=$(curl -s -o /dev/null -w "%{http_code}" -u root:admin "http://localhost:$test_port/sql/memcp-tests" -d "SELECT 1")
if [ "$http_code" = "200" ]; then
return 0
fi
fi
sleep "$delay"
done
return 1
}
# Prepare data dir and logfile once outside the loop
rm -rf /tmp/memcp-pre-commit-tests || true
echo -n > /tmp/memcp-test.log
# Create FIFO to keep stdin open across restarts
fifo_path="/tmp/memcp-stdin-$test_port"
rm -f "$fifo_path" 2>/dev/null || true
mkfifo "$fifo_path"
# Keep writer end open for the lifetime of this script
exec 3<> "$fifo_path"
# Start supervisor loop
run_memcp_forever &
supervisor_pid=$!
# Wait until ready once
echo "⏳ Waiting for MemCP to be ready..."
if wait_for_sql_ready 60 1; then
echo "✅ MemCP is ready!"
else
echo "❌ MemCP did not become SQL-ready"
exit 1
fi
cleanup() {
# Make cleanup idempotent
if [ "$did_cleanup" -eq 1 ]; then return; fi
did_cleanup=1
echo ""; echo "🛑 Stopping MemCP..."
# Stop any active test jobs we spawned
if [ ${#active_pids[@]} -gt 0 ]; then
kill "${active_pids[@]}" 2>/dev/null || true
fi
# Also stop any other background jobs (best-effort)
for pid in $(jobs -p 2>/dev/null); do
kill "$pid" 2>/dev/null || true
done
kill $supervisor_pid 2>/dev/null || true
# Also kill an active memcp if still running
pkill -f "memcp.*--api-port=$test_port" 2>/dev/null || true
# Close and remove FIFO
exec 3>&-
rm -f "$fifo_path" 2>/dev/null || true
# Remove temp directory if created
if [ -n "$tmpdir" ] && [ -d "$tmpdir" ]; then
rm -rf "$tmpdir" 2>/dev/null || true
fi
echo "✅ MemCP stopped"
}
trap cleanup EXIT
# Handle Ctrl+C and termination signals
trap 'echo; echo "✋ Interrupted"; cleanup; exit 130' INT
trap 'echo; echo "✋ Terminated"; cleanup; exit 143' TERM
trap 'cleanup; exit 129' HUP
echo "🧪 Running SQL tests using single MemCP instance (parallel where safe)..."
all_passed=true
failed_files=""
# Split tests: disruptive tests run sequentially; others in parallel.
# Rebuild-heavy suites and suites that run internal parallel groups can deadlock
# when executed concurrently against one shared server instance.
parallel_tests=""
sequential_tests=""
for tf in $test_files; do
if grep -q "SHUTDOWN" "$tf" || \
grep -q "(rebuild)" "$tf" || \
grep -qi "repartition" "$tf" || \
grep -q "parallel:" "$tf" || \
grep -Eqi "CREATE USER|ALTER USER|DROP USER|SET PASSWORD|GRANT |REVOKE |system\\.user|system\\.access" "$tf"; then
sequential_tests="$sequential_tests $tf"
else
parallel_tests="$parallel_tests $tf"
fi
done
# Keep test suites isolated by default; allow opt-in parallelism via MEMCP_TEST_JOBS.
max_jobs="${MEMCP_TEST_JOBS:-1}"
if ! [[ "$max_jobs" =~ ^[0-9]+$ ]] || [ "$max_jobs" -lt 1 ]; then
max_jobs=1
fi
if [ "$max_jobs" -gt 8 ]; then max_jobs=8; fi
tmpdir=$(mktemp -d)
lockfile="/tmp/memcp-print.lock"
run_one() {
local tf="$1"
local out="$2"
local statusf="$3"
local no_db_cleanup="${4:-0}"
# Wait for readiness before starting
if ! wait_for_sql_ready 60 1; then
echo "FAIL" > "$statusf"
{
flock -x 9
echo "❌ $tf: MemCP did not become SQL-ready before running this suite"
echo ""
} 9>>"$lockfile"
return
fi
if [ "$no_db_cleanup" = "1" ]; then
python3 run_sql_tests.py "$tf" $test_port --connect-only --no-db-cleanup > "$out" 2>&1
else
python3 run_sql_tests.py "$tf" $test_port --connect-only > "$out" 2>&1
fi
rc=$?
# Determine status by exit code (0 => OK, nonzero => FAIL)
if [ $rc -eq 0 ]; then echo OK > "$statusf"; else echo FAIL > "$statusf"; fi
# Critical section: print output of this test
{
flock -x 9
cat "$out"
echo ""
} 9>>"$lockfile"
}
# Run parallel-safe tests (track only test PIDs)
for tf in $parallel_tests; do
out="$tmpdir/$(basename "$tf").out"
statusf="$tmpdir/$(basename "$tf").status"
run_one "$tf" "$out" "$statusf" 1 &
active_pids+=("$!")
# Concurrency gate: wait for the oldest test PID when at limit
while [ ${#active_pids[@]} -ge $max_jobs ]; do
wait "${active_pids[0]}"
active_pids=("${active_pids[@]:1}")
done
done
# Wait for remaining test jobs (ignore supervisor)
for pid in "${active_pids[@]}"; do
wait "$pid"
done
# Run disruptive tests sequentially
for tf in $sequential_tests; do
out="$tmpdir/$(basename "$tf").out"
statusf="$tmpdir/$(basename "$tf").status"
run_one "$tf" "$out" "$statusf" 0
done
# Stop supervisor and memcp now that all tests are done
kill $supervisor_pid 2>/dev/null || true
pkill -f "memcp.*--api-port=$test_port" 2>/dev/null || true
# Collect results in test file order
for tf in $test_files; do
statusf="$tmpdir/$(basename "$tf").status"
if [ -f "$statusf" ] && grep -q OK "$statusf"; then :; else
all_passed=false
failed_files="$failed_files $tf"
fi
done
# Print detailed logs for failed test files again at the end
if [ "$all_passed" != true ]; then
echo ""
echo "🔎 Detailed failure logs:"
for tf in $test_files; do
statusf="$tmpdir/$(basename "$tf").status"
if [ -f "$statusf" ] && grep -q OK "$statusf"; then
continue
fi
echo ""
echo "===== $(basename "$tf") ====="
out="$tmpdir/$(basename "$tf").out"
if [ -f "$out" ]; then
cat "$out"
else
echo "(no output captured)"
fi
echo "===== end $(basename "$tf") ====="
done
echo ""
fi
rm -rf "$tmpdir"
# Generate coverage report if coverage mode was enabled
if [ "${MEMCP_COVERAGE:-}" = "1" ] && [ -d "$GOCOVERDIR" ]; then
echo ""
echo "📊 Generating Go coverage report..."
if ls "$GOCOVERDIR"/*.* >/dev/null 2>&1; then
go tool covdata textfmt -i="$GOCOVERDIR" -o="$GOCOVERDIR/coverage.out" 2>/dev/null
if [ -f "$GOCOVERDIR/coverage.out" ]; then
go tool cover -func="$GOCOVERDIR/coverage.out" | tail -1
echo " Full report: go tool cover -html=$GOCOVERDIR/coverage.out"
fi
else
echo " ⚠️ No coverage data collected"
fi
fi
if [ "$all_passed" = true ]; then
echo "🎉 All tests passed, commit allowed"
exit 0
else
echo "❌ Some tests failed, commit aborted"
echo ""
echo "Failed test files:$failed_files"
echo ""
echo "You can run individual test files manually with:"
echo " python3 run_sql_tests.py <test_file.yaml> $test_port --connect-only"
echo " (Make sure to start MemCP first: ./memcp --api-port=$test_port --disable-mysql)"
exit 1
fi