Skip to content

Commit c6a8862

Browse files
feat: unified test runner script for all test types
scripts/test-all.sh with modes: --unit Rust workspace tests --integration MySQL/Postgres/DynamoDB via Docker testcontainers --bindings Python, Node.js, Ruby, Go, Java, .NET binding tests --interop Cross-language interop tests --fuzz libfuzzer targets with configurable duration --sanitizers Miri + AddressSanitizer + Valgrind --lint cargo fmt + clippy --e2e Published package E2E tests --all unit + integration + bindings + interop + lint Options: --binding=NAME for single binding, --fuzz-time=N for duration. Summary with PASS/FAIL/SKIP counts at the end.
1 parent b5ad562 commit c6a8862

1 file changed

Lines changed: 354 additions & 0 deletions

File tree

scripts/test.sh

Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
#!/usr/bin/env bash
2+
set -uo pipefail
3+
4+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)"
5+
cd "$ROOT_DIR"
6+
7+
########################################################################
8+
# Help / Usage
9+
########################################################################
10+
11+
show_help() {
12+
cat >&2 <<EOF
13+
Usage: $(basename "$0") <mode> [options]
14+
15+
Modes:
16+
--unit Rust unit tests (cargo test --workspace)
17+
--integration Integration tests with MySQL, Postgres, DynamoDB (Docker required)
18+
--bindings All language binding tests (Python, Node, Ruby, Go, Java, .NET)
19+
--interop Cross-language interop tests
20+
--fuzz Fuzz tests (requires cargo-fuzz + nightly)
21+
--sanitizers Miri + AddressSanitizer + Valgrind
22+
--lint Format check + clippy
23+
--e2e E2E tests against published packages
24+
--all Run everything (unit + integration + bindings + interop + lint)
25+
26+
Options:
27+
--binding=NAME Run only a specific binding test (python, node, ruby, go, java, dotnet)
28+
--fuzz-time=N Fuzz duration per target in seconds (default: 30)
29+
30+
Examples:
31+
$(basename "$0") --unit
32+
$(basename "$0") --bindings --binding=python
33+
$(basename "$0") --all
34+
$(basename "$0") --fuzz --fuzz-time=60
35+
EOF
36+
exit 1
37+
}
38+
39+
########################################################################
40+
# Utilities
41+
########################################################################
42+
43+
PASS=0
44+
FAIL=0
45+
SKIP=0
46+
RESULTS=()
47+
48+
log() { echo ">>> $1" >&2; }
49+
pass() { PASS=$((PASS + 1)); RESULTS+=("PASS: $1"); log "PASS: $1"; }
50+
fail() { FAIL=$((FAIL + 1)); RESULTS+=("FAIL: $1"); log "FAIL: $1"; }
51+
skip() { SKIP=$((SKIP + 1)); RESULTS+=("SKIP: $1"); log "SKIP: $1"; }
52+
53+
run_test() {
54+
local name="$1"
55+
shift
56+
log "Running: $name"
57+
if "$@"; then
58+
pass "$name"
59+
else
60+
fail "$name"
61+
fi
62+
}
63+
64+
summary() {
65+
echo ""
66+
echo "========================================"
67+
echo " Test Summary"
68+
echo "========================================"
69+
for r in "${RESULTS[@]}"; do
70+
echo " $r"
71+
done
72+
echo "----------------------------------------"
73+
echo " PASS: $PASS FAIL: $FAIL SKIP: $SKIP"
74+
echo "========================================"
75+
if [ "$FAIL" -gt 0 ]; then
76+
exit 1
77+
fi
78+
}
79+
80+
########################################################################
81+
# Test functions
82+
########################################################################
83+
84+
do_lint() {
85+
log "=== Lint ==="
86+
run_test "cargo fmt" cargo fmt --check
87+
run_test "cargo clippy" cargo clippy --workspace --all-targets -- -D warnings
88+
}
89+
90+
do_unit() {
91+
log "=== Unit Tests ==="
92+
run_test "cargo test (workspace)" cargo test --workspace --exclude asherah-node
93+
}
94+
95+
do_integration() {
96+
log "=== Integration Tests (Docker required) ==="
97+
if ! docker info >/dev/null 2>&1; then
98+
skip "integration tests (Docker not running)"
99+
return
100+
fi
101+
run_test "integration (MySQL, Postgres, DynamoDB)" \
102+
cargo test -p asherah --features mysql,postgres,dynamodb \
103+
--test integration_containers -- --test-threads=1
104+
}
105+
106+
do_bindings() {
107+
local binding="${BINDING_FILTER:-all}"
108+
log "=== Binding Tests (${binding}) ==="
109+
110+
# Build FFI libs
111+
log "Building Rust FFI libraries..."
112+
cargo build --release -p asherah-ffi -p asherah-java -p asherah-cobhan 2>&1 | tail -1
113+
export ASHERAH_DOTNET_NATIVE="$ROOT_DIR/target/release"
114+
export ASHERAH_RUBY_NATIVE="$ROOT_DIR/target/release"
115+
export ASHERAH_GO_NATIVE="$ROOT_DIR/target/release"
116+
export STATIC_MASTER_KEY_HEX="746869734973415374617469634d61737465724b6579466f7254657374696e67"
117+
118+
# Python
119+
if [ "$binding" = "all" ] || [ "$binding" = "python" ]; then
120+
if command -v python3 >/dev/null 2>&1; then
121+
if ! python3 -c "import asherah" 2>/dev/null; then
122+
log "Installing Python binding (maturin develop)..."
123+
if command -v maturin >/dev/null 2>&1; then
124+
maturin develop --release --manifest-path asherah-py/Cargo.toml 2>&1 | tail -1
125+
else
126+
pip3 install maturin 2>&1 | tail -1
127+
maturin develop --release --manifest-path asherah-py/Cargo.toml 2>&1 | tail -1
128+
fi
129+
fi
130+
run_test "Python (pytest)" python3 -m pytest asherah-py/tests -vv
131+
else
132+
skip "Python tests (python3 not installed)"
133+
fi
134+
fi
135+
136+
# Node.js
137+
if [ "$binding" = "all" ] || [ "$binding" = "node" ]; then
138+
if command -v node >/dev/null 2>&1; then
139+
if [ ! -f asherah-node/index.node ]; then
140+
log "Building Node.js addon..."
141+
(cd asherah-node && npm install 2>&1 | tail -1 && npx @napi-rs/cli build --release 2>&1 | tail -1)
142+
# Copy to platform dir
143+
local plat_dir=""
144+
case "$(uname -s)-$(uname -m)" in
145+
Darwin-arm64) plat_dir="asherah-node/npm/darwin-arm64" ;;
146+
Darwin-x86_64) plat_dir="asherah-node/npm/darwin-x64" ;;
147+
Linux-x86_64) plat_dir="asherah-node/npm/linux-x64-gnu" ;;
148+
Linux-aarch64) plat_dir="asherah-node/npm/linux-arm64-gnu" ;;
149+
esac
150+
if [ -n "$plat_dir" ] && [ -f asherah-node/index.node ]; then
151+
mkdir -p "$plat_dir"
152+
local existing
153+
existing=$(ls "$plat_dir"/*.node 2>/dev/null | head -1)
154+
if [ -n "$existing" ]; then
155+
cp asherah-node/index.node "$existing"
156+
else
157+
cp asherah-node/index.node "$plat_dir/index.node"
158+
fi
159+
fi
160+
fi
161+
run_test "Node.js" bash -c "cd asherah-node && npm test"
162+
else
163+
skip "Node.js tests (node not installed)"
164+
fi
165+
fi
166+
167+
# Ruby
168+
if [ "$binding" = "all" ] || [ "$binding" = "ruby" ]; then
169+
RUBY_CMD="ruby"
170+
if [ -x "/opt/homebrew/opt/ruby/bin/ruby" ]; then
171+
RUBY_CMD="/opt/homebrew/opt/ruby/bin/ruby"
172+
export PATH="/opt/homebrew/opt/ruby/bin:/opt/homebrew/lib/ruby/gems/4.0.0/bin:$PATH"
173+
fi
174+
if ! $RUBY_CMD -e 'require "ffi"' 2>/dev/null; then
175+
log "Installing Ruby ffi gem..."
176+
gem install ffi --no-document 2>&1 | tail -1
177+
fi
178+
run_test "Ruby" $RUBY_CMD -I asherah-ruby/lib -I asherah-ruby/test asherah-ruby/test/round_trip_test.rb
179+
fi
180+
181+
# Go
182+
if [ "$binding" = "all" ] || [ "$binding" = "go" ]; then
183+
if command -v go >/dev/null 2>&1; then
184+
(cd asherah-go && go mod tidy 2>&1) || true
185+
run_test "Go" bash -c "cd asherah-go && CGO_ENABLED=0 go test ./..."
186+
else
187+
skip "Go tests (go not installed)"
188+
fi
189+
fi
190+
191+
# Java
192+
if [ "$binding" = "all" ] || [ "$binding" = "java" ]; then
193+
if command -v mvn >/dev/null 2>&1 && command -v java >/dev/null 2>&1; then
194+
log "Building Java JAR..."
195+
mvn -B -f asherah-java/java/pom.xml -Dnative.build.skip=true -DskipTests package -q 2>&1 | tail -1
196+
run_test "Java (JUnit)" mvn -B -f asherah-java/java/pom.xml -Dnative.build.skip=true test
197+
else
198+
skip "Java tests (maven/java not installed)"
199+
fi
200+
fi
201+
202+
# .NET
203+
if [ "$binding" = "all" ] || [ "$binding" = "dotnet" ]; then
204+
if command -v dotnet >/dev/null 2>&1; then
205+
run_test ".NET (xUnit)" dotnet test asherah-dotnet/AsherahDotNet.slnx --nologo
206+
else
207+
skip ".NET tests (dotnet not installed)"
208+
fi
209+
fi
210+
}
211+
212+
do_interop() {
213+
log "=== Interop Tests ==="
214+
if command -v python3 >/dev/null 2>&1 && [ -d interop/tests ]; then
215+
run_test "Cross-language interop (Python/Node/Rust/Ruby)" \
216+
python3 -m pytest interop/tests -vv
217+
else
218+
skip "Interop tests (pytest or interop/ directory not available)"
219+
fi
220+
}
221+
222+
do_fuzz() {
223+
local fuzz_time="${FUZZ_TIME:-30}"
224+
log "=== Fuzz Tests (${fuzz_time}s per target) ==="
225+
if ! command -v cargo-fuzz >/dev/null 2>&1; then
226+
skip "Fuzz tests (cargo-fuzz not installed — run: cargo install cargo-fuzz)"
227+
return
228+
fi
229+
if ! rustup run nightly rustc --version >/dev/null 2>&1; then
230+
skip "Fuzz tests (nightly toolchain not installed — run: rustup install nightly)"
231+
return
232+
fi
233+
234+
local targets
235+
targets=$(cd fuzz && cargo +nightly fuzz list 2>/dev/null)
236+
if [ -z "$targets" ]; then
237+
skip "Fuzz tests (no fuzz targets found)"
238+
return
239+
fi
240+
241+
for target in $targets; do
242+
run_test "fuzz: $target (${fuzz_time}s)" \
243+
bash -c "cd fuzz && cargo +nightly fuzz run $target -- -max_total_time=$fuzz_time"
244+
done
245+
}
246+
247+
do_sanitizers() {
248+
log "=== Sanitizer Tests ==="
249+
250+
# Miri
251+
if rustup run nightly miri --version >/dev/null 2>&1; then
252+
run_test "Miri (undefined behavior)" \
253+
cargo +nightly miri test -p asherah-ffi --lib
254+
else
255+
skip "Miri (not installed — run: rustup +nightly component add miri)"
256+
fi
257+
258+
# AddressSanitizer (Linux only)
259+
if [ "$(uname)" = "Linux" ]; then
260+
run_test "AddressSanitizer" bash -c \
261+
'RUSTFLAGS="-Zsanitizer=address" ASAN_OPTIONS="detect_leaks=1" cargo +nightly test -p asherah-ffi --target x86_64-unknown-linux-gnu -- --test-threads=1'
262+
else
263+
skip "AddressSanitizer (Linux only)"
264+
fi
265+
266+
# Valgrind (Linux only)
267+
if command -v valgrind >/dev/null 2>&1; then
268+
run_test "Valgrind" valgrind --error-exitcode=1 \
269+
cargo test -p asherah-ffi --lib -- --test-threads=1
270+
else
271+
skip "Valgrind (not installed)"
272+
fi
273+
}
274+
275+
do_e2e() {
276+
log "=== E2E Tests ==="
277+
278+
# npm package
279+
if [ -d e2e-npm-test ] && command -v node >/dev/null 2>&1; then
280+
run_test "E2E npm" bash -c "cd e2e-npm-test && npm install && node test.js"
281+
else
282+
skip "E2E npm (directory or node not available)"
283+
fi
284+
285+
# PyPI package
286+
if [ -d e2e-pypi-test ] && command -v python3 >/dev/null 2>&1; then
287+
run_test "E2E PyPI" bash -c "cd e2e-pypi-test && python3 -m pytest -vv"
288+
else
289+
skip "E2E PyPI (directory or python3 not available)"
290+
fi
291+
}
292+
293+
do_all() {
294+
do_lint
295+
do_unit
296+
do_integration
297+
do_bindings
298+
do_interop
299+
}
300+
301+
########################################################################
302+
# Argument parsing
303+
########################################################################
304+
305+
if [ $# -eq 0 ]; then
306+
show_help
307+
fi
308+
309+
MODE=""
310+
BINDING_FILTER="all"
311+
FUZZ_TIME=30
312+
313+
while [ $# -gt 0 ]; do
314+
case "$1" in
315+
--unit) MODE="unit" ;;
316+
--integration) MODE="integration" ;;
317+
--bindings) MODE="bindings" ;;
318+
--interop) MODE="interop" ;;
319+
--fuzz) MODE="fuzz" ;;
320+
--sanitizers) MODE="sanitizers" ;;
321+
--lint) MODE="lint" ;;
322+
--e2e) MODE="e2e" ;;
323+
--all) MODE="all" ;;
324+
--binding=*) BINDING_FILTER="${1#--binding=}" ;;
325+
--fuzz-time=*) FUZZ_TIME="${1#--fuzz-time=}" ;;
326+
--help|-h) show_help ;;
327+
*)
328+
echo "Unknown option: $1" >&2
329+
show_help
330+
;;
331+
esac
332+
shift
333+
done
334+
335+
if [ -z "$MODE" ]; then
336+
show_help
337+
fi
338+
339+
export BINDING_FILTER
340+
export FUZZ_TIME
341+
342+
case "$MODE" in
343+
unit) do_unit ;;
344+
integration) do_integration ;;
345+
bindings) do_bindings ;;
346+
interop) do_interop ;;
347+
fuzz) do_fuzz ;;
348+
sanitizers) do_sanitizers ;;
349+
lint) do_lint ;;
350+
e2e) do_e2e ;;
351+
all) do_all ;;
352+
esac
353+
354+
summary

0 commit comments

Comments
 (0)