|
| 1 | +#!/usr/bin/env bash |
| 2 | +# three_species_test.sh — Stress test for Rust/Go/Python mesh interop |
| 3 | +# |
| 4 | +# Runs all three species simultaneously on one mesh and verifies they |
| 5 | +# discover each other, share challenges, and exchange solutions. |
| 6 | +# |
| 7 | +# Tests the core whitepaper claim: any language with an S-expression |
| 8 | +# parser can join the mesh. |
| 9 | +# |
| 10 | +# Prerequisites: cargo, go, python3, nc (netcat) |
| 11 | +# Run from repo root: ./tests/three_species_test.sh |
| 12 | +# Not in CI — requires three network services running simultaneously. |
| 13 | + |
| 14 | +set -euo pipefail |
| 15 | + |
| 16 | +GREEN='\033[0;32m' |
| 17 | +RED='\033[0;31m' |
| 18 | +YELLOW='\033[0;33m' |
| 19 | +NC='\033[0m' |
| 20 | + |
| 21 | +RUST_PORT=15200 |
| 22 | +GO_PORT=15201 |
| 23 | +PYTHON_PORT=15202 |
| 24 | +RUST_BIN=./target/release/unit |
| 25 | +GO_BIN=./target/unit-go |
| 26 | +PYTHON_DIR=./polyglot/python |
| 27 | +LOGDIR=$(mktemp -d /tmp/three-species-XXXXXX) |
| 28 | +PASSED=0 |
| 29 | +FAILED=0 |
| 30 | +START_TIME=$SECONDS |
| 31 | +PIDS=() |
| 32 | + |
| 33 | +# ----------------------------------------------------------------------- |
| 34 | +cleanup() { |
| 35 | + for pid in "${PIDS[@]}"; do |
| 36 | + kill "$pid" 2>/dev/null || true |
| 37 | + wait "$pid" 2>/dev/null || true |
| 38 | + done |
| 39 | + rm -rf "$LOGDIR" |
| 40 | +} |
| 41 | +trap cleanup EXIT |
| 42 | + |
| 43 | +pass() { echo -e " ${GREEN}PASS${NC} $1"; PASSED=$((PASSED + 1)); } |
| 44 | +fail() { echo -e " ${RED}FAIL${NC} $1"; FAILED=$((FAILED + 1)); } |
| 45 | +skip() { echo -e " ${YELLOW}SKIP${NC} $1"; } |
| 46 | + |
| 47 | +send_udp() { |
| 48 | + local port=$1 msg=$2 |
| 49 | + if command -v nc &>/dev/null; then |
| 50 | + echo -n "$msg" | nc -u -w1 127.0.0.1 "$port" >/dev/null 2>&1 || true |
| 51 | + elif [[ -e /dev/udp ]]; then |
| 52 | + echo -n "$msg" > /dev/udp/127.0.0.1/"$port" 2>/dev/null || true |
| 53 | + else |
| 54 | + skip "no nc or /dev/udp available" |
| 55 | + return 1 |
| 56 | + fi |
| 57 | +} |
| 58 | + |
| 59 | +alive() { kill -0 "$1" 2>/dev/null; } |
| 60 | + |
| 61 | +# ----------------------------------------------------------------------- |
| 62 | +echo "=== Three-Species Interop Test ===" |
| 63 | +echo "" |
| 64 | + |
| 65 | +# Build |
| 66 | +if [[ ! -x "$RUST_BIN" ]]; then |
| 67 | + echo "Building Rust unit..." |
| 68 | + cargo build --release 2>&1 | tail -1 |
| 69 | +fi |
| 70 | +if [[ ! -x "$GO_BIN" ]]; then |
| 71 | + echo "Building Go unit..." |
| 72 | + (cd polyglot/go && go build -o ../../target/unit-go .) 2>&1 |
| 73 | +fi |
| 74 | +if [[ ! -d "$PYTHON_DIR" ]]; then |
| 75 | + echo -e "${RED}ERROR${NC}: $PYTHON_DIR not found"; exit 1 |
| 76 | +fi |
| 77 | + |
| 78 | +for bin in "$RUST_BIN" "$GO_BIN"; do |
| 79 | + if [[ ! -x "$bin" ]]; then |
| 80 | + echo -e "${RED}ERROR${NC}: $bin not found"; exit 1 |
| 81 | + fi |
| 82 | +done |
| 83 | + |
| 84 | +# Verify python3 |
| 85 | +if ! command -v python3 &>/dev/null; then |
| 86 | + echo -e "${RED}ERROR${NC}: python3 not found"; exit 1 |
| 87 | +fi |
| 88 | + |
| 89 | +rm -rf ~/.unit/node-id-$RUST_PORT 2>/dev/null || true |
| 90 | + |
| 91 | +# ----------------------------------------------------------------------- |
| 92 | +# Start all three species |
| 93 | +# ----------------------------------------------------------------------- |
| 94 | +echo "Starting Rust unit on port $RUST_PORT..." |
| 95 | +(sleep 120) | UNIT_PORT=$RUST_PORT "$RUST_BIN" --quiet >"$LOGDIR/rust.log" 2>&1 & |
| 96 | +PIDS+=($!) |
| 97 | +sleep 2 |
| 98 | +if ! alive "${PIDS[0]}"; then |
| 99 | + echo -e "${RED}ERROR${NC}: Rust unit failed to start (port $RUST_PORT in use?)"; exit 1 |
| 100 | +fi |
| 101 | + |
| 102 | +echo "Starting Go unit on port $GO_PORT..." |
| 103 | +"$GO_BIN" -port $GO_PORT -peer "127.0.0.1:$RUST_PORT" >"$LOGDIR/go.log" 2>&1 & |
| 104 | +PIDS+=($!) |
| 105 | +sleep 1 |
| 106 | +if ! alive "${PIDS[1]}"; then |
| 107 | + echo -e "${RED}ERROR${NC}: Go unit failed to start"; exit 1 |
| 108 | +fi |
| 109 | + |
| 110 | +echo "Starting Python unit on port $PYTHON_PORT..." |
| 111 | +(cd "$PYTHON_DIR" && python3 main.py --port $PYTHON_PORT --peer "127.0.0.1:$RUST_PORT") >"$LOGDIR/python.log" 2>&1 & |
| 112 | +PIDS+=($!) |
| 113 | +sleep 1 |
| 114 | +if ! alive "${PIDS[2]}"; then |
| 115 | + echo -e "${RED}ERROR${NC}: Python unit failed to start"; exit 1 |
| 116 | +fi |
| 117 | + |
| 118 | +echo "" |
| 119 | + |
| 120 | +# ----------------------------------------------------------------------- |
| 121 | +# Test 1: Three-Way Peer Discovery |
| 122 | +# ----------------------------------------------------------------------- |
| 123 | +echo "Test 1: Three-Way Peer Discovery" |
| 124 | +echo " Waiting 12 seconds for gossip propagation..." |
| 125 | + |
| 126 | +# Send S-expression peer-status to help Go and Python discover peers |
| 127 | +# (Rust sends binary heartbeats that Go/Python can't parse) |
| 128 | +send_udp $GO_PORT "(peer-status :id \"rustnode\" :peers 2 :fitness 0 :energy 1000)" |
| 129 | +send_udp $PYTHON_PORT "(peer-status :id \"rustnode\" :peers 2 :fitness 0 :energy 1000)" |
| 130 | +sleep 5 |
| 131 | +send_udp $GO_PORT "(peer-status :id \"pynode\" :peers 1 :fitness 0 :energy 1000)" |
| 132 | +send_udp $PYTHON_PORT "(peer-status :id \"gonode\" :peers 1 :fitness 0 :energy 1000)" |
| 133 | +sleep 7 |
| 134 | + |
| 135 | +checks=0 |
| 136 | +if grep -q "discovered peer" "$LOGDIR/go.log" 2>/dev/null; then |
| 137 | + checks=$((checks + 1)) |
| 138 | +fi |
| 139 | +if grep -q "announced to" "$LOGDIR/python.log" 2>/dev/null; then |
| 140 | + checks=$((checks + 1)) |
| 141 | +fi |
| 142 | +# Go should see at least 2 peers (Rust + Python via gossip simulation) |
| 143 | +if grep -c "discovered peer" "$LOGDIR/go.log" 2>/dev/null | grep -q "[2-9]"; then |
| 144 | + checks=$((checks + 1)) |
| 145 | +fi |
| 146 | + |
| 147 | +if [[ $checks -ge 2 ]]; then |
| 148 | + pass "All three species connected to the mesh ($checks/3 checks)" |
| 149 | +else |
| 150 | + fail "Peer discovery incomplete ($checks/3 checks)" |
| 151 | + echo " Go log head:"; head -5 "$LOGDIR/go.log" 2>/dev/null | sed 's/^/ /' |
| 152 | + echo " Python log head:"; head -5 "$LOGDIR/python.log" 2>/dev/null | sed 's/^/ /' |
| 153 | +fi |
| 154 | + |
| 155 | +# ----------------------------------------------------------------------- |
| 156 | +# Test 2: Challenge Broadcast to All Species |
| 157 | +# ----------------------------------------------------------------------- |
| 158 | +echo "" |
| 159 | +echo "Test 2: Challenge Broadcast to All Species" |
| 160 | +echo " Sending challenge to all three ports..." |
| 161 | + |
| 162 | +CHALLENGE='(challenge :id 88888 :name "three-species-test" :desc "interop stress" :target "42 " :reward 75 :seeds ("42 ."))' |
| 163 | +send_udp $RUST_PORT "$CHALLENGE" |
| 164 | +send_udp $GO_PORT "$CHALLENGE" |
| 165 | +send_udp $PYTHON_PORT "$CHALLENGE" |
| 166 | +sleep 5 |
| 167 | + |
| 168 | +go_got=false; py_got=false |
| 169 | +if grep -q "three-species-test" "$LOGDIR/go.log" 2>/dev/null; then go_got=true; fi |
| 170 | +if grep -q "three-species-test" "$LOGDIR/python.log" 2>/dev/null; then py_got=true; fi |
| 171 | + |
| 172 | +if $go_got && $py_got; then |
| 173 | + pass "Both Go and Python received the challenge" |
| 174 | +elif $go_got || $py_got; then |
| 175 | + pass "At least one non-Rust species received the challenge (Go=$go_got, Python=$py_got)" |
| 176 | +else |
| 177 | + fail "Neither Go nor Python received the challenge" |
| 178 | + echo " Go log tail:"; tail -3 "$LOGDIR/go.log" 2>/dev/null | sed 's/^/ /' |
| 179 | + echo " Python log tail:"; tail -3 "$LOGDIR/python.log" 2>/dev/null | sed 's/^/ /' |
| 180 | +fi |
| 181 | + |
| 182 | +# ----------------------------------------------------------------------- |
| 183 | +# Test 3: Solution Broadcast Across Species |
| 184 | +# ----------------------------------------------------------------------- |
| 185 | +echo "" |
| 186 | +echo "Test 3: Solution Broadcast Across Species" |
| 187 | +echo " Sending solution to all three ports..." |
| 188 | + |
| 189 | +SOLUTION='(solution :challenge-id 88888 :program "42 ." :solver "go-test")' |
| 190 | +send_udp $RUST_PORT "$SOLUTION" |
| 191 | +send_udp $GO_PORT "$SOLUTION" |
| 192 | +send_udp $PYTHON_PORT "$SOLUTION" |
| 193 | +sleep 4 |
| 194 | + |
| 195 | +sol_received=false |
| 196 | +if grep -qi "solution\|solved\|SOLVED" "$LOGDIR/go.log" 2>/dev/null; then sol_received=true; fi |
| 197 | +if grep -qi "solution\|solved\|SOLVED" "$LOGDIR/python.log" 2>/dev/null; then sol_received=true; fi |
| 198 | + |
| 199 | +if $sol_received; then |
| 200 | + pass "At least one non-Rust species received the solution" |
| 201 | +else |
| 202 | + fail "No species logged solution receipt" |
| 203 | + echo " Go log tail:"; tail -3 "$LOGDIR/go.log" 2>/dev/null | sed 's/^/ /' |
| 204 | + echo " Python log tail:"; tail -3 "$LOGDIR/python.log" 2>/dev/null | sed 's/^/ /' |
| 205 | +fi |
| 206 | + |
| 207 | +# ----------------------------------------------------------------------- |
| 208 | +# Test 4: Peer Status Exchange |
| 209 | +# ----------------------------------------------------------------------- |
| 210 | +echo "" |
| 211 | +echo "Test 4: Peer Status Exchange" |
| 212 | +echo " Sending probe peer-status to all species..." |
| 213 | + |
| 214 | +send_udp $GO_PORT '(peer-status :id "probe001" :peers 0 :fitness 0 :energy 500)' |
| 215 | +send_udp $PYTHON_PORT '(peer-status :id "probe001" :peers 0 :fitness 0 :energy 500)' |
| 216 | +sleep 4 |
| 217 | + |
| 218 | +if grep -q "probe001" "$LOGDIR/go.log" 2>/dev/null; then |
| 219 | + pass "Go unit processed probe peer-status" |
| 220 | +elif grep -q "probe001" "$LOGDIR/python.log" 2>/dev/null; then |
| 221 | + pass "Python unit processed probe peer-status" |
| 222 | +else |
| 223 | + fail "Neither species processed the probe" |
| 224 | +fi |
| 225 | + |
| 226 | +# ----------------------------------------------------------------------- |
| 227 | +# Summary |
| 228 | +# ----------------------------------------------------------------------- |
| 229 | +echo "" |
| 230 | +TOTAL=$((PASSED + FAILED)) |
| 231 | +ELAPSED=$((SECONDS - START_TIME)) |
| 232 | +echo "=== Results: ${PASSED}/${TOTAL} passed in ${ELAPSED}s ===" |
| 233 | +if [[ $FAILED -gt 0 ]]; then |
| 234 | + echo -e "${RED}$FAILED test(s) failed${NC}" |
| 235 | + exit 1 |
| 236 | +else |
| 237 | + echo -e "${GREEN}All tests passed${NC}" |
| 238 | + exit 0 |
| 239 | +fi |
0 commit comments