code-review-forge 2.0.0a1__py3-none-any.whl
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.
- code_forge/__init__.py +14 -0
- code_forge/__main__.py +8 -0
- code_forge/autofix.py +78 -0
- code_forge/baseline.py +216 -0
- code_forge/cli.py +983 -0
- code_forge/delta.py +65 -0
- code_forge/diagnose.py +109 -0
- code_forge/diff.py +82 -0
- code_forge/disposition.py +32 -0
- code_forge/e2e_check.py +641 -0
- code_forge/env_resolver.py +91 -0
- code_forge/errors.py +34 -0
- code_forge/exit_codes.py +37 -0
- code_forge/factories.py +191 -0
- code_forge/falsify.py +85 -0
- code_forge/gate_check.py +466 -0
- code_forge/git.py +351 -0
- code_forge/hold.py +126 -0
- code_forge/install_hooks.py +331 -0
- code_forge/lock.py +162 -0
- code_forge/machine.py +792 -0
- code_forge/mode_resolver.py +60 -0
- code_forge/mutation.py +380 -0
- code_forge/parsers/__init__.py +56 -0
- code_forge/parsers/_sarif.py +77 -0
- code_forge/parsers/base.py +65 -0
- code_forge/parsers/checkpatch.py +66 -0
- code_forge/parsers/clippy.py +85 -0
- code_forge/parsers/non_ascii.py +47 -0
- code_forge/parsers/ruff.py +18 -0
- code_forge/parsers/semgrep.py +18 -0
- code_forge/parsers/shellcheck.py +56 -0
- code_forge/registry.py +153 -0
- code_forge/reporter.py +133 -0
- code_forge/runner.py +205 -0
- code_forge/sarif.py +226 -0
- code_forge/skills/adversarial-qe/SKILL.md +272 -0
- code_forge/skills/code-forge/SKILL.md +1193 -0
- code_forge/skills/code-review-expert/SKILL.md +162 -0
- code_forge/skills/code-review-expert/references/code-quality-checklist.md +130 -0
- code_forge/skills/code-review-expert/references/removal-plan.md +52 -0
- code_forge/skills/code-review-expert/references/security-checklist.md +118 -0
- code_forge/skills/code-review-expert/references/solid-checklist.md +65 -0
- code_forge/skills/kernel-fp-verify/SKILL.md +101 -0
- code_forge/skills/qodo-review/SKILL.md +135 -0
- code_forge/skills/smoke-test/SKILL.md +253 -0
- code_forge/skills/smoke-test/references/boundary-cases.md +114 -0
- code_forge/skills/smoke-test/references/concurrency-patterns.md +306 -0
- code_forge/skills/smoke-test/references/injection-payloads.md +124 -0
- code_forge/skills/smoke-test/test-library/shell/README.md +271 -0
- code_forge/skills/smoke-test/test-library/shell/primitives.sh +352 -0
- code_forge/skills/smoke-test/test-library/shell/primitives_test.sh +324 -0
- code_forge/snapshot.py +196 -0
- code_forge/source.py +64 -0
- code_forge/state.py +246 -0
- code_forge/verdict.py +43 -0
- code_review_forge-2.0.0a1.dist-info/METADATA +237 -0
- code_review_forge-2.0.0a1.dist-info/RECORD +62 -0
- code_review_forge-2.0.0a1.dist-info/WHEEL +5 -0
- code_review_forge-2.0.0a1.dist-info/entry_points.txt +2 -0
- code_review_forge-2.0.0a1.dist-info/licenses/LICENSE +179 -0
- code_review_forge-2.0.0a1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# smoke-test primitives -- reusable assertion functions for shell scripts
|
|
3
|
+
# Source this file: source ~/.claude/skills/smoke-test/test-library/shell/primitives.sh
|
|
4
|
+
|
|
5
|
+
# ============================================================
|
|
6
|
+
# Helper: run_and_capture -- execute command, capture stdout/stderr
|
|
7
|
+
# ============================================================
|
|
8
|
+
run_and_capture() {
|
|
9
|
+
SMOKE_LAST_STDOUT=$(mktemp /tmp/smoke-fw-stdout.XXXXXX)
|
|
10
|
+
export SMOKE_LAST_STDOUT
|
|
11
|
+
SMOKE_LAST_STDERR=$(mktemp /tmp/smoke-fw-stderr.XXXXXX)
|
|
12
|
+
export SMOKE_LAST_STDERR
|
|
13
|
+
"$@" > "$SMOKE_LAST_STDOUT" 2> "$SMOKE_LAST_STDERR"
|
|
14
|
+
SMOKE_LAST_STATUS=$?
|
|
15
|
+
export SMOKE_LAST_STATUS
|
|
16
|
+
return $SMOKE_LAST_STATUS
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# ============================================================
|
|
20
|
+
# Helper: run_concurrent -- launch N background instances
|
|
21
|
+
# ============================================================
|
|
22
|
+
run_concurrent() {
|
|
23
|
+
CONCURRENT_PIDS=()
|
|
24
|
+
CONCURRENT_RESULT_DIR=$(mktemp -d /tmp/smoke-fw-concurrent.XXXXXX)
|
|
25
|
+
export CONCURRENT_RESULT_DIR
|
|
26
|
+
local n=$1; shift
|
|
27
|
+
for ((i=0; i<n; i++)); do
|
|
28
|
+
(
|
|
29
|
+
run_and_capture "$@"
|
|
30
|
+
cp "$SMOKE_LAST_STDOUT" "$CONCURRENT_RESULT_DIR/$i.out"
|
|
31
|
+
cp "$SMOKE_LAST_STDERR" "$CONCURRENT_RESULT_DIR/$i.err"
|
|
32
|
+
echo "$SMOKE_LAST_STATUS" > "$CONCURRENT_RESULT_DIR/$i.status"
|
|
33
|
+
) &
|
|
34
|
+
CONCURRENT_PIDS+=($!)
|
|
35
|
+
done
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# ============================================================
|
|
39
|
+
# Helper: concurrent_wait -- reap all background instances
|
|
40
|
+
# ============================================================
|
|
41
|
+
concurrent_wait() {
|
|
42
|
+
local failed=0
|
|
43
|
+
for pid in "${CONCURRENT_PIDS[@]}"; do
|
|
44
|
+
wait "$pid" || failed=1
|
|
45
|
+
done
|
|
46
|
+
return $failed
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# ============================================================
|
|
50
|
+
# Shared: dump SMOKE_LAST_* debug context on FAIL
|
|
51
|
+
# ============================================================
|
|
52
|
+
_smoke_dump_context() {
|
|
53
|
+
if [[ -n "$SMOKE_LAST_STDOUT" && -f "$SMOKE_LAST_STDOUT" ]]; then
|
|
54
|
+
echo " stdout: $(head -5 "$SMOKE_LAST_STDOUT" | sed 's/sk-[A-Za-z0-9]\{20,\}/\*\*\*/g')"
|
|
55
|
+
fi
|
|
56
|
+
if [[ -n "$SMOKE_LAST_STDERR" && -f "$SMOKE_LAST_STDERR" ]]; then
|
|
57
|
+
echo " stderr: $(head -5 "$SMOKE_LAST_STDERR" | sed 's/sk-[A-Za-z0-9]\{20,\}/\*\*\*/g')"
|
|
58
|
+
fi
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# ============================================================
|
|
62
|
+
# Execution primitives
|
|
63
|
+
# ============================================================
|
|
64
|
+
|
|
65
|
+
assert_success() {
|
|
66
|
+
local status=$1
|
|
67
|
+
local description=${2:-"<command>"}
|
|
68
|
+
if [[ $status -eq 0 ]]; then
|
|
69
|
+
echo "PASS: assert_success $description"
|
|
70
|
+
return 0
|
|
71
|
+
else
|
|
72
|
+
echo "FAIL: assert_success $description -- expected 0, got $status"
|
|
73
|
+
_smoke_dump_context
|
|
74
|
+
return 1
|
|
75
|
+
fi
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
assert_failure() {
|
|
79
|
+
local status=$1
|
|
80
|
+
local description=${2:-"<command>"}
|
|
81
|
+
if [[ $status -ne 0 ]]; then
|
|
82
|
+
echo "PASS: assert_failure $description"
|
|
83
|
+
return 0
|
|
84
|
+
else
|
|
85
|
+
echo "FAIL: assert_failure $description -- expected non-zero, got 0"
|
|
86
|
+
_smoke_dump_context
|
|
87
|
+
return 1
|
|
88
|
+
fi
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
assert_exit_code() {
|
|
92
|
+
local expected=$1
|
|
93
|
+
local actual=$2
|
|
94
|
+
local description=${3:-"<command>"}
|
|
95
|
+
if [[ $actual -eq $expected ]]; then
|
|
96
|
+
echo "PASS: assert_exit_code $expected $description"
|
|
97
|
+
return 0
|
|
98
|
+
else
|
|
99
|
+
echo "FAIL: assert_exit_code $expected $description -- expected $expected, got $actual"
|
|
100
|
+
_smoke_dump_context
|
|
101
|
+
return 1
|
|
102
|
+
fi
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
# ============================================================
|
|
106
|
+
# Output primitives
|
|
107
|
+
# ============================================================
|
|
108
|
+
|
|
109
|
+
assert_output_contains() {
|
|
110
|
+
local output=$1
|
|
111
|
+
local pattern=$2
|
|
112
|
+
local description=${3:-"output"}
|
|
113
|
+
if echo "$output" | grep -qF "$pattern" 2>/dev/null; then
|
|
114
|
+
echo "PASS: assert_output_contains '$pattern' $description"
|
|
115
|
+
return 0
|
|
116
|
+
else
|
|
117
|
+
echo "FAIL: assert_output_contains '$pattern' $description -- pattern not found"
|
|
118
|
+
echo " actual output: $(echo "$output" | head -3)"
|
|
119
|
+
return 1
|
|
120
|
+
fi
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
assert_output_not_contains() {
|
|
124
|
+
local output=$1
|
|
125
|
+
local pattern=$2
|
|
126
|
+
local description=${3:-"output"}
|
|
127
|
+
if echo "$output" | grep -qF "$pattern" 2>/dev/null; then
|
|
128
|
+
echo "FAIL: assert_output_not_contains '$pattern' $description -- pattern found"
|
|
129
|
+
echo " matched line: $(echo "$output" | grep -F "$pattern" | head -1)"
|
|
130
|
+
return 1
|
|
131
|
+
else
|
|
132
|
+
echo "PASS: assert_output_not_contains '$pattern' $description"
|
|
133
|
+
return 0
|
|
134
|
+
fi
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
assert_stderr_empty() {
|
|
138
|
+
local description=${1:-"stderr"}
|
|
139
|
+
if [[ -n "$SMOKE_LAST_STDERR" && -f "$SMOKE_LAST_STDERR" && -s "$SMOKE_LAST_STDERR" ]]; then
|
|
140
|
+
echo "FAIL: assert_stderr_empty $description -- stderr not empty"
|
|
141
|
+
echo " stderr: $(head -3 "$SMOKE_LAST_STDERR")"
|
|
142
|
+
return 1
|
|
143
|
+
else
|
|
144
|
+
echo "PASS: assert_stderr_empty $description"
|
|
145
|
+
return 0
|
|
146
|
+
fi
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
assert_stderr_contains() {
|
|
150
|
+
local stderr_output=$1
|
|
151
|
+
local pattern=$2
|
|
152
|
+
local description=${3:-"stderr"}
|
|
153
|
+
if echo "$stderr_output" | grep -qF "$pattern" 2>/dev/null; then
|
|
154
|
+
echo "PASS: assert_stderr_contains '$pattern' $description"
|
|
155
|
+
return 0
|
|
156
|
+
else
|
|
157
|
+
echo "FAIL: assert_stderr_contains '$pattern' $description -- pattern not found in stderr"
|
|
158
|
+
echo " actual stderr: $(echo "$stderr_output" | head -3)"
|
|
159
|
+
return 1
|
|
160
|
+
fi
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# ============================================================
|
|
164
|
+
# File/State primitives
|
|
165
|
+
# ============================================================
|
|
166
|
+
|
|
167
|
+
assert_file_exists() {
|
|
168
|
+
local file=$1
|
|
169
|
+
local description=${2:-"$file"}
|
|
170
|
+
if [[ -f "$file" ]]; then
|
|
171
|
+
echo "PASS: assert_file_exists $description"
|
|
172
|
+
return 0
|
|
173
|
+
else
|
|
174
|
+
echo "FAIL: assert_file_exists $description -- file not found"
|
|
175
|
+
return 1
|
|
176
|
+
fi
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
assert_file_not_exists() {
|
|
180
|
+
local file=$1
|
|
181
|
+
local description=${2:-"$file"}
|
|
182
|
+
if [[ ! -f "$file" ]]; then
|
|
183
|
+
echo "PASS: assert_file_not_exists $description"
|
|
184
|
+
return 0
|
|
185
|
+
else
|
|
186
|
+
echo "FAIL: assert_file_not_exists $description -- file exists"
|
|
187
|
+
return 1
|
|
188
|
+
fi
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
assert_file_contains() {
|
|
192
|
+
local file=$1
|
|
193
|
+
local pattern=$2
|
|
194
|
+
local description=${3:-"$file"}
|
|
195
|
+
if [[ -f "$file" ]] && grep -qF "$pattern" "$file" 2>/dev/null; then
|
|
196
|
+
echo "PASS: assert_file_contains '$pattern' $description"
|
|
197
|
+
return 0
|
|
198
|
+
else
|
|
199
|
+
echo "FAIL: assert_file_contains '$pattern' $description -- pattern not found in file"
|
|
200
|
+
return 1
|
|
201
|
+
fi
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
# ============================================================
|
|
205
|
+
# Security primitives
|
|
206
|
+
# ============================================================
|
|
207
|
+
|
|
208
|
+
assert_no_command_exec() {
|
|
209
|
+
local input=$1
|
|
210
|
+
local context=${2:-"input"}
|
|
211
|
+
local failed=0
|
|
212
|
+
# shellcheck disable=SC2016 # $(), backticks, <() are literal patterns
|
|
213
|
+
if echo "$input" | grep -qE '\$\(.*\)|`[^`]+`|<\(' 2>/dev/null; then
|
|
214
|
+
echo "FAIL: assert_no_command_exec $context -- command execution/substitution detected in: '$input'"
|
|
215
|
+
failed=1
|
|
216
|
+
fi
|
|
217
|
+
if echo "$input" | grep -qE '[;&|]' 2>/dev/null; then
|
|
218
|
+
echo "FAIL: assert_no_command_exec $context -- command chaining/pipe detected in: '$input'"
|
|
219
|
+
failed=1
|
|
220
|
+
fi
|
|
221
|
+
[[ $failed -eq 0 ]] && echo "PASS: assert_no_command_exec $context"
|
|
222
|
+
return $failed
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
# JSON/URL-safe variant: only checks command execution/substitution, skips ;&|
|
|
226
|
+
# Use when input legitimately contains & (query strings) or | (regex)
|
|
227
|
+
assert_no_command_exec_json() {
|
|
228
|
+
local input=$1
|
|
229
|
+
local context=${2:-"input"}
|
|
230
|
+
# shellcheck disable=SC2016 # $(), backticks, <() are literal patterns
|
|
231
|
+
if echo "$input" | grep -qE '\$\(.*\)|`[^`]+`|<\(' 2>/dev/null; then
|
|
232
|
+
echo "FAIL: assert_no_command_exec_json $context -- command execution/substitution detected in: '$input'"
|
|
233
|
+
return 1
|
|
234
|
+
fi
|
|
235
|
+
echo "PASS: assert_no_command_exec_json $context"
|
|
236
|
+
return 0
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
assert_no_path_traversal() {
|
|
240
|
+
local input=$1
|
|
241
|
+
local context=${2:-"path"}
|
|
242
|
+
local failed=0
|
|
243
|
+
if echo "$input" | grep -qE '\.\.[/\\]' 2>/dev/null; then
|
|
244
|
+
echo "FAIL: assert_no_path_traversal $context -- path traversal detected in: '$input'"
|
|
245
|
+
failed=1
|
|
246
|
+
fi
|
|
247
|
+
if echo "$input" | grep -qiE '%2e%2e[%/]' 2>/dev/null; then
|
|
248
|
+
echo "FAIL: assert_no_path_traversal $context -- encoded path traversal in: '$input'"
|
|
249
|
+
failed=1
|
|
250
|
+
fi
|
|
251
|
+
[[ $failed -eq 0 ]] && echo "PASS: assert_no_path_traversal $context"
|
|
252
|
+
return $failed
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
# ============================================================
|
|
256
|
+
# Process primitives
|
|
257
|
+
# ============================================================
|
|
258
|
+
|
|
259
|
+
assert_no_zombie() {
|
|
260
|
+
local root_pid=${1:-$$}
|
|
261
|
+
local description=${2:-"process tree"}
|
|
262
|
+
local max_depth=${3:-100}
|
|
263
|
+
local zombies=0
|
|
264
|
+
_smoke_scan_children() {
|
|
265
|
+
local ppid=$1
|
|
266
|
+
local depth=${2:-0}
|
|
267
|
+
if (( depth > max_depth )); then
|
|
268
|
+
echo " WARNING: max depth $max_depth reached at PID=$ppid"
|
|
269
|
+
return 0
|
|
270
|
+
fi
|
|
271
|
+
local children_file="/proc/$ppid/task/$ppid/children"
|
|
272
|
+
[[ -f "$children_file" ]] || return 0
|
|
273
|
+
local child
|
|
274
|
+
# shellcheck disable=SC2013 # children file is space-separated PIDs
|
|
275
|
+
for child in $(cat "$children_file" 2>/dev/null); do
|
|
276
|
+
local stat="/proc/$child/stat"
|
|
277
|
+
local raw
|
|
278
|
+
raw=$(cat "$stat" 2>/dev/null) || continue
|
|
279
|
+
local after_comm="${raw#*) }"
|
|
280
|
+
local state="${after_comm%% *}"
|
|
281
|
+
if [[ "$state" == "Z" ]]; then
|
|
282
|
+
((zombies++))
|
|
283
|
+
echo " ZOMBIE: PID=$child"
|
|
284
|
+
fi
|
|
285
|
+
_smoke_scan_children "$child" $((depth + 1))
|
|
286
|
+
done
|
|
287
|
+
}
|
|
288
|
+
_smoke_scan_children "$root_pid" 0
|
|
289
|
+
if [[ $zombies -eq 0 ]]; then
|
|
290
|
+
echo "PASS: assert_no_zombie $description"
|
|
291
|
+
return 0
|
|
292
|
+
else
|
|
293
|
+
echo "FAIL: assert_no_zombie $description -- $zombies zombie process(es) in tree"
|
|
294
|
+
return 1
|
|
295
|
+
fi
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
assert_temp_clean() {
|
|
299
|
+
local pattern=${1:-"/tmp/smoke-test-*"}
|
|
300
|
+
local description=${2:-"temp files"}
|
|
301
|
+
local leftovers
|
|
302
|
+
leftovers=$(find /tmp -name "$(basename "$pattern")" \
|
|
303
|
+
-newer /proc/$$/cmdline 2>/dev/null | wc -l)
|
|
304
|
+
if [[ $leftovers -eq 0 ]]; then
|
|
305
|
+
echo "PASS: assert_temp_clean $description"
|
|
306
|
+
return 0
|
|
307
|
+
else
|
|
308
|
+
echo "FAIL: assert_temp_clean $description -- $leftovers leftover file(s)"
|
|
309
|
+
find /tmp -name "$(basename "$pattern")" \
|
|
310
|
+
-newer /proc/$$/cmdline 2>/dev/null | head -5
|
|
311
|
+
return 1
|
|
312
|
+
fi
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
# ============================================================
|
|
316
|
+
# Data primitives
|
|
317
|
+
# ============================================================
|
|
318
|
+
|
|
319
|
+
assert_json_valid() {
|
|
320
|
+
local file_or_str=$1
|
|
321
|
+
local description=${2:-"json"}
|
|
322
|
+
local schema=${3:-}
|
|
323
|
+
if ! command -v jq &>/dev/null; then
|
|
324
|
+
echo "SKIP: assert_json_valid $description -- jq not installed"
|
|
325
|
+
return 0
|
|
326
|
+
fi
|
|
327
|
+
if [[ -f "$file_or_str" ]]; then
|
|
328
|
+
jq empty "$file_or_str" 2>/dev/null || {
|
|
329
|
+
echo "FAIL: assert_json_valid $description -- invalid JSON syntax"
|
|
330
|
+
return 1
|
|
331
|
+
}
|
|
332
|
+
if [[ -n "$schema" ]]; then
|
|
333
|
+
jq -e "$schema" "$file_or_str" >/dev/null 2>&1 || {
|
|
334
|
+
echo "FAIL: assert_json_valid $description -- schema mismatch: $schema"
|
|
335
|
+
return 1
|
|
336
|
+
}
|
|
337
|
+
fi
|
|
338
|
+
else
|
|
339
|
+
echo "$file_or_str" | jq empty 2>/dev/null || {
|
|
340
|
+
echo "FAIL: assert_json_valid $description -- invalid JSON syntax"
|
|
341
|
+
return 1
|
|
342
|
+
}
|
|
343
|
+
if [[ -n "$schema" ]]; then
|
|
344
|
+
echo "$file_or_str" | jq -e "$schema" >/dev/null 2>&1 || {
|
|
345
|
+
echo "FAIL: assert_json_valid $description -- schema mismatch: $schema"
|
|
346
|
+
return 1
|
|
347
|
+
}
|
|
348
|
+
fi
|
|
349
|
+
fi
|
|
350
|
+
echo "PASS: assert_json_valid $description"
|
|
351
|
+
return 0
|
|
352
|
+
}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Self-tests for smoke-test primitives
|
|
3
|
+
# Each primitive gets one positive and one negative test case
|
|
4
|
+
|
|
5
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
+
source "$SCRIPT_DIR/primitives.sh"
|
|
7
|
+
PASS_COUNT=0; FAIL_COUNT=0
|
|
8
|
+
|
|
9
|
+
# ============================================================
|
|
10
|
+
# Execution primitives
|
|
11
|
+
# ============================================================
|
|
12
|
+
|
|
13
|
+
test_assert_success() {
|
|
14
|
+
local result
|
|
15
|
+
result=$(assert_success 0 "positive")
|
|
16
|
+
[[ "$result" == PASS:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_success positive"; return 1; }
|
|
17
|
+
((PASS_COUNT++))
|
|
18
|
+
result=$(assert_success 1 "negative")
|
|
19
|
+
[[ "$result" == FAIL:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_success negative"; return 1; }
|
|
20
|
+
((PASS_COUNT++))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test_assert_failure() {
|
|
24
|
+
local result
|
|
25
|
+
result=$(assert_failure 1 "positive")
|
|
26
|
+
[[ "$result" == PASS:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_failure positive"; return 1; }
|
|
27
|
+
((PASS_COUNT++))
|
|
28
|
+
result=$(assert_failure 0 "negative")
|
|
29
|
+
[[ "$result" == FAIL:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_failure negative"; return 1; }
|
|
30
|
+
((PASS_COUNT++))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
test_assert_exit_code() {
|
|
34
|
+
local result
|
|
35
|
+
result=$(assert_exit_code 2 2 "positive")
|
|
36
|
+
[[ "$result" == PASS:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_exit_code positive"; return 1; }
|
|
37
|
+
((PASS_COUNT++))
|
|
38
|
+
result=$(assert_exit_code 2 0 "negative")
|
|
39
|
+
[[ "$result" == FAIL:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_exit_code negative"; return 1; }
|
|
40
|
+
((PASS_COUNT++))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# ============================================================
|
|
44
|
+
# Output primitives
|
|
45
|
+
# ============================================================
|
|
46
|
+
|
|
47
|
+
test_assert_output_contains() {
|
|
48
|
+
local result
|
|
49
|
+
result=$(assert_output_contains "hello world" "world" "positive")
|
|
50
|
+
[[ "$result" == PASS:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_output_contains positive"; return 1; }
|
|
51
|
+
((PASS_COUNT++))
|
|
52
|
+
result=$(assert_output_contains "hello world" "xyz" "negative")
|
|
53
|
+
[[ "$result" == FAIL:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_output_contains negative"; return 1; }
|
|
54
|
+
((PASS_COUNT++))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
test_assert_output_not_contains() {
|
|
58
|
+
local result
|
|
59
|
+
result=$(assert_output_not_contains "hello world" "xyz" "positive")
|
|
60
|
+
[[ "$result" == PASS:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_output_not_contains positive"; return 1; }
|
|
61
|
+
((PASS_COUNT++))
|
|
62
|
+
result=$(assert_output_not_contains "hello world" "world" "negative")
|
|
63
|
+
[[ "$result" == FAIL:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_output_not_contains negative"; return 1; }
|
|
64
|
+
((PASS_COUNT++))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
test_assert_stderr_empty() {
|
|
68
|
+
local result
|
|
69
|
+
# Simulate empty stderr
|
|
70
|
+
SMOKE_LAST_STDERR=$(mktemp /tmp/smoke-test-stderr-empty.XXXXXX)
|
|
71
|
+
result=$(assert_stderr_empty "positive")
|
|
72
|
+
rm -f "$SMOKE_LAST_STDERR"
|
|
73
|
+
[[ "$result" == PASS:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_stderr_empty positive"; return 1; }
|
|
74
|
+
((PASS_COUNT++))
|
|
75
|
+
# Simulate non-empty stderr
|
|
76
|
+
SMOKE_LAST_STDERR=$(mktemp /tmp/smoke-test-stderr-nonempty.XXXXXX)
|
|
77
|
+
echo "some error" > "$SMOKE_LAST_STDERR"
|
|
78
|
+
result=$(assert_stderr_empty "negative")
|
|
79
|
+
rm -f "$SMOKE_LAST_STDERR"
|
|
80
|
+
[[ "$result" == FAIL:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_stderr_empty negative"; return 1; }
|
|
81
|
+
((PASS_COUNT++))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
test_assert_stderr_contains() {
|
|
85
|
+
local result
|
|
86
|
+
result=$(assert_stderr_contains "error: invalid" "error" "positive")
|
|
87
|
+
[[ "$result" == PASS:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_stderr_contains positive"; return 1; }
|
|
88
|
+
((PASS_COUNT++))
|
|
89
|
+
result=$(assert_stderr_contains "error: invalid" "success" "negative")
|
|
90
|
+
[[ "$result" == FAIL:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_stderr_contains negative"; return 1; }
|
|
91
|
+
((PASS_COUNT++))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# ============================================================
|
|
95
|
+
# File/State primitives
|
|
96
|
+
# ============================================================
|
|
97
|
+
|
|
98
|
+
test_assert_file_exists() {
|
|
99
|
+
local result
|
|
100
|
+
local tmpf=$(mktemp /tmp/smoke-test-file-exists.XXXXXX)
|
|
101
|
+
result=$(assert_file_exists "$tmpf" "positive")
|
|
102
|
+
rm -f "$tmpf"
|
|
103
|
+
[[ "$result" == PASS:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_file_exists positive"; return 1; }
|
|
104
|
+
((PASS_COUNT++))
|
|
105
|
+
result=$(assert_file_exists "/tmp/__nonexistent_file__test__" "negative")
|
|
106
|
+
[[ "$result" == FAIL:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_file_exists negative"; return 1; }
|
|
107
|
+
((PASS_COUNT++))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
test_assert_file_not_exists() {
|
|
111
|
+
local result
|
|
112
|
+
result=$(assert_file_not_exists "/tmp/__nonexistent_file__test__" "positive")
|
|
113
|
+
[[ "$result" == PASS:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_file_not_exists positive"; return 1; }
|
|
114
|
+
((PASS_COUNT++))
|
|
115
|
+
local tmpf=$(mktemp /tmp/smoke-test-file-notexists.XXXXXX)
|
|
116
|
+
result=$(assert_file_not_exists "$tmpf" "negative")
|
|
117
|
+
rm -f "$tmpf"
|
|
118
|
+
[[ "$result" == FAIL:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_file_not_exists negative"; return 1; }
|
|
119
|
+
((PASS_COUNT++))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
test_assert_file_contains() {
|
|
123
|
+
local result
|
|
124
|
+
local tmpf=$(mktemp /tmp/smoke-test-file-contains.XXXXXX)
|
|
125
|
+
echo "hello world" > "$tmpf"
|
|
126
|
+
result=$(assert_file_contains "$tmpf" "hello" "positive")
|
|
127
|
+
rm -f "$tmpf"
|
|
128
|
+
[[ "$result" == PASS:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_file_contains positive"; return 1; }
|
|
129
|
+
((PASS_COUNT++))
|
|
130
|
+
tmpf=$(mktemp /tmp/smoke-test-file-contains.XXXXXX)
|
|
131
|
+
echo "hello world" > "$tmpf"
|
|
132
|
+
result=$(assert_file_contains "$tmpf" "xyz" "negative")
|
|
133
|
+
rm -f "$tmpf"
|
|
134
|
+
[[ "$result" == FAIL:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_file_contains negative"; return 1; }
|
|
135
|
+
((PASS_COUNT++))
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# ============================================================
|
|
139
|
+
# Security primitives
|
|
140
|
+
# ============================================================
|
|
141
|
+
|
|
142
|
+
test_assert_no_command_exec() {
|
|
143
|
+
local result
|
|
144
|
+
result=$(assert_no_command_exec "normal input" "positive")
|
|
145
|
+
[[ "$result" == PASS:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_no_command_exec positive"; return 1; }
|
|
146
|
+
((PASS_COUNT++))
|
|
147
|
+
result=$(assert_no_command_exec '$(id)' "negative")
|
|
148
|
+
[[ "$result" == FAIL:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_no_command_exec negative(sub)"; return 1; }
|
|
149
|
+
((PASS_COUNT++))
|
|
150
|
+
result=$(assert_no_command_exec '; rm -rf /' "negative")
|
|
151
|
+
[[ "$result" == FAIL:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_no_command_exec negative(chain)"; return 1; }
|
|
152
|
+
((PASS_COUNT++))
|
|
153
|
+
result=$(assert_no_command_exec '<(cat /etc/passwd)' "negative-procsub")
|
|
154
|
+
[[ "$result" == FAIL:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_no_command_exec negative(procsub)"; return 1; }
|
|
155
|
+
((PASS_COUNT++))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
test_assert_no_command_exec_json() {
|
|
159
|
+
local result
|
|
160
|
+
# Positive: safe inputs (including URL query strings and regex patterns)
|
|
161
|
+
result=$(assert_no_command_exec_json "normal input" "positive")
|
|
162
|
+
[[ "$result" == PASS:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_no_command_exec_json positive"; return 1; }
|
|
163
|
+
((PASS_COUNT++))
|
|
164
|
+
result=$(assert_no_command_exec_json "key=value&foo=bar" "positive-url")
|
|
165
|
+
[[ "$result" == PASS:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_no_command_exec_json positive(url)"; return 1; }
|
|
166
|
+
((PASS_COUNT++))
|
|
167
|
+
result=$(assert_no_command_exec_json "error|warn|info" "positive-regex")
|
|
168
|
+
[[ "$result" == PASS:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_no_command_exec_json positive(regex)"; return 1; }
|
|
169
|
+
((PASS_COUNT++))
|
|
170
|
+
# Negative: command substitution still detected
|
|
171
|
+
result=$(assert_no_command_exec_json '$(id)' "negative-sub")
|
|
172
|
+
[[ "$result" == FAIL:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_no_command_exec_json negative(sub)"; return 1; }
|
|
173
|
+
((PASS_COUNT++))
|
|
174
|
+
result=$(assert_no_command_exec_json '`whoami`' "negative-backtick")
|
|
175
|
+
[[ "$result" == FAIL:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_no_command_exec_json negative(backtick)"; return 1; }
|
|
176
|
+
((PASS_COUNT++))
|
|
177
|
+
# Negative: ; and | are allowed -- should PASS (not FAIL)
|
|
178
|
+
result=$(assert_no_command_exec_json '; rm -rf /' "negative-chain-allowed")
|
|
179
|
+
[[ "$result" == PASS:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_no_command_exec_json negative(chain should pass)"; return 1; }
|
|
180
|
+
((PASS_COUNT++))
|
|
181
|
+
# Negative: process substitution detected
|
|
182
|
+
result=$(assert_no_command_exec_json '<(id)' "negative-procsub")
|
|
183
|
+
[[ "$result" == FAIL:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_no_command_exec_json negative(procsub)"; return 1; }
|
|
184
|
+
((PASS_COUNT++))
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
test_assert_no_path_traversal() {
|
|
188
|
+
local result
|
|
189
|
+
result=$(assert_no_path_traversal "config.json" "positive")
|
|
190
|
+
[[ "$result" == PASS:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_no_path_traversal positive"; return 1; }
|
|
191
|
+
((PASS_COUNT++))
|
|
192
|
+
result=$(assert_no_path_traversal "../../etc/passwd" "negative")
|
|
193
|
+
[[ "$result" == FAIL:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_no_path_traversal negative"; return 1; }
|
|
194
|
+
((PASS_COUNT++))
|
|
195
|
+
result=$(assert_no_path_traversal '%2e%2e%2fetc%2fpasswd' "negative-encoded")
|
|
196
|
+
[[ "$result" == FAIL:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_no_path_traversal negative(encoded)"; return 1; }
|
|
197
|
+
((PASS_COUNT++))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
# ============================================================
|
|
201
|
+
# Process primitives
|
|
202
|
+
# ============================================================
|
|
203
|
+
|
|
204
|
+
test_assert_no_zombie() {
|
|
205
|
+
local result
|
|
206
|
+
# Positive: normal child exit + wait -> no zombie
|
|
207
|
+
( true ) & wait $! 2>/dev/null
|
|
208
|
+
result=$(assert_no_zombie $$ "clean tree")
|
|
209
|
+
[[ "$result" == PASS:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_no_zombie positive"; return 1; }
|
|
210
|
+
((PASS_COUNT++))
|
|
211
|
+
# Negative: double-fork to create a grandchild zombie (depth 2)
|
|
212
|
+
(
|
|
213
|
+
( sleep 0.05; exit 0 ) &
|
|
214
|
+
sleep 0.3
|
|
215
|
+
) & local child=$!
|
|
216
|
+
sleep 0.15
|
|
217
|
+
result=$(assert_no_zombie $$ "zombie grandchild")
|
|
218
|
+
kill -0 "$child" 2>/dev/null && wait "$child" 2>/dev/null
|
|
219
|
+
if [[ "$result" == FAIL:* ]]; then
|
|
220
|
+
((PASS_COUNT++))
|
|
221
|
+
else
|
|
222
|
+
echo "SKIP: assert_no_zombie negative inconclusive (zombie may have been reaped)"
|
|
223
|
+
((PASS_COUNT++))
|
|
224
|
+
fi
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
test_assert_no_zombie_depth() {
|
|
228
|
+
local result
|
|
229
|
+
# Depth limit param accepted: scan with depth=1 on clean tree
|
|
230
|
+
( true ) & wait $! 2>/dev/null
|
|
231
|
+
result=$(assert_no_zombie $$ "depth limit clean" 1)
|
|
232
|
+
[[ "$result" == PASS:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_no_zombie depth positive"; return 1; }
|
|
233
|
+
((PASS_COUNT++))
|
|
234
|
+
# Depth limit param accepted with larger value
|
|
235
|
+
result=$(assert_no_zombie $$ "depth limit 50" 50)
|
|
236
|
+
[[ "$result" == PASS:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_no_zombie depth 50"; return 1; }
|
|
237
|
+
((PASS_COUNT++))
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
test_assert_temp_clean() {
|
|
241
|
+
local result
|
|
242
|
+
# Positive: no test temp files created by this session
|
|
243
|
+
result=$(assert_temp_clean "/tmp/smoke-test-*" "clean")
|
|
244
|
+
[[ "$result" == PASS:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_temp_clean positive"; return 1; }
|
|
245
|
+
((PASS_COUNT++))
|
|
246
|
+
# Negative: create a test temp file, then check
|
|
247
|
+
local tmpf=$(mktemp /tmp/smoke-test-leak.XXXXXX)
|
|
248
|
+
echo "leaked" > "$tmpf"
|
|
249
|
+
result=$(assert_temp_clean "/tmp/smoke-test-*" "leaked files")
|
|
250
|
+
rm -f "$tmpf"
|
|
251
|
+
[[ "$result" == FAIL:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_temp_clean negative"; return 1; }
|
|
252
|
+
((PASS_COUNT++))
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
# ============================================================
|
|
256
|
+
# Data primitives
|
|
257
|
+
# ============================================================
|
|
258
|
+
|
|
259
|
+
test_assert_json_valid() {
|
|
260
|
+
local result
|
|
261
|
+
result=$(assert_json_valid '{"a":1}' "positive")
|
|
262
|
+
if [[ "$result" == SKIP:* ]]; then
|
|
263
|
+
echo "SKIP: assert_json_valid -- jq not installed"
|
|
264
|
+
return 0
|
|
265
|
+
fi
|
|
266
|
+
[[ "$result" == PASS:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_json_valid positive"; return 1; }
|
|
267
|
+
((PASS_COUNT++))
|
|
268
|
+
result=$(assert_json_valid '{bad' "negative")
|
|
269
|
+
[[ "$result" == FAIL:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_json_valid negative"; return 1; }
|
|
270
|
+
((PASS_COUNT++))
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
test_assert_json_valid_schema() {
|
|
274
|
+
local result
|
|
275
|
+
result=$(assert_json_valid '{"a":1}' "schema check")
|
|
276
|
+
if [[ "$result" == SKIP:* ]]; then
|
|
277
|
+
echo "SKIP: assert_json_valid_schema -- jq not installed"
|
|
278
|
+
return 0
|
|
279
|
+
fi
|
|
280
|
+
# Schema match: object with numeric "a"
|
|
281
|
+
result=$(assert_json_valid '{"a":1}' "schema match" '.a == 1')
|
|
282
|
+
[[ "$result" == PASS:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_json_valid schema positive"; return 1; }
|
|
283
|
+
((PASS_COUNT++))
|
|
284
|
+
# Schema mismatch: object with numeric "a" but value wrong
|
|
285
|
+
result=$(assert_json_valid '{"a":2}' "schema mismatch" '.a == 1')
|
|
286
|
+
[[ "$result" == FAIL:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_json_valid schema negative"; return 1; }
|
|
287
|
+
((PASS_COUNT++))
|
|
288
|
+
# Valid JSON but schema returns false -> FAIL
|
|
289
|
+
result=$(assert_json_valid '{"a":false}' "schema false" '.a')
|
|
290
|
+
[[ "$result" == FAIL:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_json_valid schema falsy"; return 1; }
|
|
291
|
+
((PASS_COUNT++))
|
|
292
|
+
# Valid JSON with truthy schema -> PASS
|
|
293
|
+
result=$(assert_json_valid '{"a":1}' "schema truthy" '.a')
|
|
294
|
+
[[ "$result" == PASS:* ]] || { ((FAIL_COUNT++)); echo "FAIL: assert_json_valid schema truthy"; return 1; }
|
|
295
|
+
((PASS_COUNT++))
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
# ============================================================
|
|
299
|
+
# Run all tests
|
|
300
|
+
# ============================================================
|
|
301
|
+
|
|
302
|
+
main() {
|
|
303
|
+
test_assert_success
|
|
304
|
+
test_assert_failure
|
|
305
|
+
test_assert_exit_code
|
|
306
|
+
test_assert_output_contains
|
|
307
|
+
test_assert_output_not_contains
|
|
308
|
+
test_assert_stderr_empty
|
|
309
|
+
test_assert_stderr_contains
|
|
310
|
+
test_assert_file_exists
|
|
311
|
+
test_assert_file_not_exists
|
|
312
|
+
test_assert_file_contains
|
|
313
|
+
test_assert_no_command_exec
|
|
314
|
+
test_assert_no_command_exec_json
|
|
315
|
+
test_assert_no_path_traversal
|
|
316
|
+
test_assert_no_zombie
|
|
317
|
+
test_assert_no_zombie_depth
|
|
318
|
+
test_assert_temp_clean
|
|
319
|
+
test_assert_json_valid
|
|
320
|
+
test_assert_json_valid_schema
|
|
321
|
+
echo "=== $PASS_COUNT PASS, $FAIL_COUNT FAIL ==="
|
|
322
|
+
return $(( FAIL_COUNT > 0 ))
|
|
323
|
+
}
|
|
324
|
+
main "$@"
|