#!/usr/bin/env bash # OpsxDB 驱动验收:一次只起一个测试库(见 README.md)。 # ./verify-crud.sh # 按 sqlite → a → b → c → f → d → e 顺序,每步只留一个容器 # ./verify-crud.sh a # 只验 MySQL # ./verify-crud.sh f # 只验 Redis set -uo pipefail API="${OPSXDB_API:-http://127.0.0.1:8788}" ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$(dirname "$0")" REPORT="$ROOT/tests/accept-result.txt" : >"$REPORT" n_ok=0; n_fail=0 ok() { echo " OK $*"; echo " OK $*" >>"$REPORT"; n_ok=$((n_ok+1)); } bad() { echo " FAIL $*"; echo " FAIL $*" >>"$REPORT"; n_fail=$((n_fail+1)); } log() { echo "$*"; echo "$*" >>"$REPORT"; } post() { curl -sS -m 45 -H 'Content-Type: application/json' -d "$1" "$API$2" } json_ok() { python3 -c 'import json,sys d=json.load(sys.stdin) if d.get("needConfirm"): sys.exit(2) err=d.get("error") if err: sys.exit(1) if "ok" in d and d.get("ok") is False: sys.exit(1) sys.exit(0)' } wait_health() { local name="$1" tries="${2:-60}" for i in $(seq 1 "$tries"); do local st st=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$name" 2>/dev/null || echo missing) if [[ "$st" == "healthy" ]]; then return 0; fi if [[ "$st" == "running" && "$tries" -lt 5 ]]; then return 0; fi sleep 2 done echo " (health=$st after ${tries} tries)" [[ "$st" == "running" || "$st" == "healthy" ]] } assert_single_container() { local expect="$1" local running running=$(docker ps --filter name=opsxdb- --format '{{.Names}}' | grep -v '^$' || true) local n n=$(echo "$running" | grep -c . || true) if [[ -z "$expect" ]]; then [[ "$n" -eq 0 ]] && ok "无测试库容器" || bad "仍有容器: $running" return fi if [[ "$n" -eq 1 ]] && echo "$running" | grep -qx "$expect"; then ok "仅 $expect 在跑" else bad "期望仅 ${expect},实际: ${running:-none}" fi } up_one() { local alias="$1" container="$2" tries="${3:-40}" ./dbctl.sh up "$alias" wait_health "$container" "$tries" || true assert_single_container "$container" } exec_sql() { local base="$1" sql="$2" python3 -c 'import json,sys d=json.loads(sys.argv[1]); d["sql"]=sys.argv[2]; print(json.dumps(d))' "$base" "$sql" } crud_sql() { local driver="$1" host="$2" port="$3" user="$4" password="$5" db="$6" dialect="$7" local base base=$(printf '{"driver":"%s","host":"%s","port":%s,"user":"%s","password":"%s","database":"%s"}' \ "$driver" "$host" "$port" "$user" "$password" "$db") log "== $driver ==" local tc tc=$(post "$base" /api/db/test-connection) echo "$tc" | json_ok && ok "test-connection" || { bad "test-connection $tc"; return; } local dbs dbs=$(post "$base" /api/db/databases) echo "$dbs" | json_ok && ok "databases" || bad "databases $dbs" local tables tables=$(post "$base" /api/db/tables) echo "$tables" | json_ok && ok "tables" || bad "tables $tables" local sel ins upd del case "$dialect" in mysql|postgres|sqlite) sel='SELECT id,name FROM users ORDER BY id LIMIT 5' ins="INSERT INTO users (name, email, status) VALUES ('grace','grace@demo.local',1)" upd="UPDATE users SET status=0 WHERE name='grace'" del="DELETE FROM users WHERE name='grace'" ;; mssql) sel='SELECT TOP 5 id,name FROM dbo.users ORDER BY id' ins="INSERT INTO dbo.users (name, email, status) VALUES (N'grace',N'grace@demo.local',1)" upd="UPDATE dbo.users SET status=0 WHERE name=N'grace'" del="DELETE FROM dbo.users WHERE name=N'grace'" ;; oracle) sel='SELECT id,name FROM users ORDER BY id FETCH FIRST 5 ROWS ONLY' ins="INSERT INTO users (name, email, status) VALUES ('grace','grace@demo.local',1)" upd="UPDATE users SET status=0 WHERE name='grace'" del="DELETE FROM users WHERE name='grace'" ;; redis) sel='PING' ins='SET opsxdb:demo:grace hello' upd='GET opsxdb:demo:grace' del='DEL opsxdb:demo:grace' ;; *) bad "unknown dialect $dialect"; return ;; esac local body r body=$(exec_sql "$base" "$sel") r=$(post "$body" /api/db/exec) echo "$r" | json_ok && ok "SELECT/PING" || bad "SELECT/PING $r" body=$(exec_sql "$base" "$ins") r=$(post "$body" /api/db/exec) echo "$r" | json_ok && ok "INSERT/SET" || bad "INSERT/SET $r" body=$(exec_sql "$base" "$upd") r=$(post "$body" /api/db/exec) echo "$r" | json_ok && ok "UPDATE/GET" || bad "UPDATE/GET $r" body=$(exec_sql "$base" "$del") r=$(post "$body" /api/db/exec) echo "$r" | json_ok && ok "DELETE/DEL" || bad "DELETE/DEL $r" if [[ "$dialect" == "redis" ]]; then local keys kv scan keys=$(post "$base" /api/db/redis/keys) echo "$keys" | json_ok && ok "redis/keys" || bad "redis/keys $keys" body=$(python3 -c 'import json,sys; d=json.loads(sys.argv[1]); d["sql"]="SET CEX:FOLLOW:k1 v"; print(json.dumps(d))' "$base") post "$body" /api/db/exec >/dev/null kv=$(python3 -c 'import json,sys; d=json.loads(sys.argv[1]); d["key"]="CEX:FOLLOW:k1"; print(json.dumps(d))' "$base") kv=$(post "$kv" /api/db/redis/key) echo "$kv" | json_ok && ok "redis/key" || bad "redis/key $kv" body=$(exec_sql "$base" "SCAN 0 MATCH CEX:* COUNT 50") scan=$(post "$body" /api/db/exec) echo "$scan" | json_ok && ok "SCAN MATCH" || bad "SCAN MATCH $scan" fi } run_sqlite() { log log "---- sqlite(无 Docker)----" local SQLITE_DB="$ROOT/tests/tmp/demo.sqlite" mkdir -p "$ROOT/tests/tmp" rm -f "$SQLITE_DB" if ! command -v sqlite3 >/dev/null 2>&1; then bad "本机无 sqlite3" return fi sqlite3 "$SQLITE_DB" < init/sqlite/01_demo.sql crud_sql sqlite "" 0 "" "" "$SQLITE_DB" sqlite } run_mysql() { log log "---- a MySQL :3306 ----" up_one a opsxdb-mysql 40 docker exec -i opsxdb-mysql mysql -uroot -p'Demo@123' < init/mysql/01_demo.sql 2>/dev/null crud_sql mysql 127.0.0.1 3306 root 'Demo@123' demo mysql } run_mariadb() { log log "---- b MariaDB :3307 ----" up_one b opsxdb-mariadb 40 docker exec -i opsxdb-mariadb mariadb -uroot -p'Demo@123' < init/mariadb/01_demo.sql 2>/dev/null \ || docker exec -i opsxdb-mariadb mysql -uroot -p'Demo@123' < init/mariadb/01_demo.sql 2>/dev/null crud_sql mariadb 127.0.0.1 3307 root 'Demo@123' demo mysql } run_postgres() { log log "---- c PostgreSQL :5432 ----" up_one c opsxdb-postgres 40 docker exec -i opsxdb-postgres psql -U opsx -d demo < init/postgres/01_demo.sql >/dev/null crud_sql postgresql 127.0.0.1 5432 opsx 'Demo@123' demo postgres } run_redis() { log log "---- f Redis :6379 ----" up_one f opsxdb-redis 30 docker exec opsxdb-redis redis-cli SET LANG:zh-CN hello >/dev/null docker exec opsxdb-redis redis-cli HSET CEX:FOLLOW:FOLLOWERS:1 f1 '{"a":1}' >/dev/null crud_sql redis 127.0.0.1 6379 "" "" 0 redis } run_mssql() { log log "---- d SQL Server :1433 ----" up_one d opsxdb-mssql 120 local ready=0 for i in $(seq 1 60); do if docker exec opsxdb-mssql /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P 'Demo@12345' -C -Q 'SELECT 1' >/dev/null 2>&1 \ || docker exec opsxdb-mssql /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'Demo@12345' -Q 'SELECT 1' >/dev/null 2>&1; then ready=1 break fi sleep 3 done if [[ "$ready" -ne 1 ]]; then bad "sqlcmd 未就绪" return fi ok "sqlcmd ready" docker cp init/mssql/01_demo.sql opsxdb-mssql:/tmp/01_demo.sql docker exec opsxdb-mssql /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P 'Demo@12345' -C -i /tmp/01_demo.sql >/dev/null 2>&1 \ || docker exec opsxdb-mssql /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'Demo@12345' -i /tmp/01_demo.sql >/dev/null 2>&1 \ || true local base_master='{"driver":"sqlserver","host":"127.0.0.1","port":1433,"user":"sa","password":"Demo@12345","database":"master"}' local r r=$(post "$base_master" /api/db/test-connection) echo "$r" | json_ok && ok "test-connection master" || bad "test-connection master $r" crud_sql sqlserver 127.0.0.1 1433 sa 'Demo@12345' demo mssql } run_oracle() { log log "---- e Oracle :1521(Apple Silicon 常 ORA-00442)----" up_one e opsxdb-oracle 180 docker cp init/oracle/01_demo.sql opsxdb-oracle:/tmp/01_demo.sql 2>/dev/null || true docker exec opsxdb-oracle bash -lc "echo exit | sqlplus -s opsx/Demo@123@//localhost/XEPDB1 @/tmp/01_demo.sql" >/dev/null 2>&1 \ || docker exec opsxdb-oracle bash -lc "echo exit | sqlplus -s system/Demo@123@//localhost/XEPDB1 @/tmp/01_demo.sql" >/dev/null 2>&1 \ || true crud_sql oracle 127.0.0.1 1521 opsx 'Demo@123' XEPDB1 oracle } run_grey() { local alias="$1" name="$2" container="$3" driver="$4" port="$5" log log "---- $alias $name(驱动未接入,只验测连失败/灰态)----" up_one "$alias" "$container" 40 local base base=$(printf '{"driver":"%s","host":"127.0.0.1","port":%s}' "$driver" "$port") local r r=$(post "$base" /api/db/test-connection) if echo "$r" | grep -qiE '未接入|未就绪|not ready|unknown driver|unsupported'; then ok "灰态测连被拒绝" else echo "$r" | json_ok && bad "未接入驱动却测连成功 $r" || ok "测连失败(驱动未接入): $r" fi } usage() { cat </dev/null; then log "API down — 请先在仓库根目录 ./run.sh web" exit 1 fi ok "API health" target="${1:-all}" case "$target" in -h|--help|help) usage; exit 0 ;; sqlite) run_sqlite ;; a|mysql) run_mysql ;; b|mariadb|maria) run_mariadb ;; c|postgres|pg|postgresql) run_postgres ;; d|mssql|sqlserver) run_mssql ;; e|oracle) run_oracle ;; f|redis) run_redis ;; g|zk|zookeeper) run_grey g ZooKeeper opsxdb-zookeeper zookeeper 2181 ;; h|es|elasticsearch) run_grey h Elasticsearch opsxdb-elasticsearch elasticsearch 9200 ;; i|influx|influxdb) run_grey i InfluxDB opsxdb-influxdb influxdb 8086 ;; all|"") run_sqlite run_mysql run_mariadb run_postgres run_redis run_mssql run_oracle ;; *) usage; exit 1 ;; esac log log "==== SUMMARY ok=$n_ok fail=$n_fail ====" [[ "$n_fail" -eq 0 ]]