From df7eb35b13c6b5c53a62599d6fec425ed1cbd01f Mon Sep 17 00:00:00 2001 From: Yugaaank Date: Wed, 22 Jul 2026 21:26:40 +0530 Subject: [PATCH 1/3] HUGEGRAPH-3099: add RISC-V end-to-end smoke test script Add a self-contained smoke test that builds the HugeGraph Server distribution on the host (x86-64/arm64), then runs it inside an emulated linux/riscv64 container via Docker QEMU to verify: - Architecture is riscv64 - RocksDB JNI open/put/get/close (libatomic linkage) - HugeGraph init + health check (GET /graphs -> 200) - REST schema CRUD (propertykey, vertexlabel, edgelabel, vertices, edge) - Gremlin query (g.V().count() == 2 on port 8182) - Restart + data persistence across container restart Output matches the expected format from the issue. --- .gitignore | 3 + .../src/assembly/travis/run-riscv64-smoke.sh | 474 ++++++++++++++++++ 2 files changed, 477 insertions(+) create mode 100755 hugegraph-server/hugegraph-dist/src/assembly/travis/run-riscv64-smoke.sh diff --git a/.gitignore b/.gitignore index e27322b73a..9ced97619d 100644 --- a/.gitignore +++ b/.gitignore @@ -118,3 +118,6 @@ codeium-instructions.md *.ai-prompt.md WARP.md .mcp.json +hugegraph-riscv64-ctx.*/ +rocks-smoke.*/ +.riscv64-smoke-tmp/ diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/run-riscv64-smoke.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/run-riscv64-smoke.sh new file mode 100755 index 0000000000..bfb9f0b245 --- /dev/null +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/run-riscv64-smoke.sh @@ -0,0 +1,474 @@ +#!/bin/bash +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# run-riscv64-smoke.sh — Disposable RISC-V (linux/riscv64) end-to-end smoke test. +# +# Builds the HugeGraph Server distribution on the contributor's native host +# (x86-64 / arm64), then runs it inside an emulated linux/riscv64 container to +# verify the RocksDB backend + Gremlin stack actually work on RISC-V via QEMU. +# +# This is a CORRECTNESS gate, not a performance benchmark. QEMU user-mode +# emulation is several times slower than native; expect the full run (build + +# init + REST + Gremlin + restart) to take many minutes the first time. +# +# It requires Docker with QEMU/binfmt_misc multi-platform support (Docker +# Desktop ships this; on Linux install it with: +# docker run --privileged --rm tonistiigi/binfmt --install all +# No host RISC-V packages are installed by this script. +# +# It removes ONLY what it creates: a uniquely-named container, volume, and +# locally-built image. The QEMU emulator registration is left untouched +# (remove it with: docker run --privileged --rm tonistiigi/binfmt --uninstall all). +# +# Usage: +# ./run-riscv64-smoke.sh [path-to-server-dist.tar.gz] +# ./run-riscv64-smoke.sh # builds the dist under hugegraph-server/ +# +set -euo pipefail + +ARCH=riscv64 +PLATFORM="linux/${ARCH}" +# Ubuntu 24.04 publishes openjdk-11-jre-headless for linux/riscv64 (glibc). +BASE_IMAGE="ubuntu:24.04" + +REPO_ROOT=$(cd "$(dirname "$0")/../../../../.." && pwd) + +# Use disk-backed temp: the extracted server (~1GB) blows out a small RAM tmpfs +# /tmp. Default to a work dir on the same (large) filesystem as the repo. +: "${TMPDIR:=$REPO_ROOT/.riscv64-smoke-tmp}" +mkdir -p "$TMPDIR" +export TMPDIR + +# Unique, traceable names so cleanup removes only our own resources. +STAMP=$(date +%Y%m%d%H%M%S) +TAG="hugegraph-riscv64-smoke:${STAMP}" +# KEEP: don't auto-cleanup so a failed/finished run stays pokeable. +VOL="hugegraph-riscv64-data-${STAMP}" +CONTAINER="hugegraph-riscv64-smoke-${STAMP}" + +DIST_ARG="${1:-}" +DIST_TAR="" +SERVER_DIR="" +BUILD_CTX="" +ROCKS_BUILD="" + +GREEN=$'\033[0;32m'; RED=$'\033[0;31m'; NC=$'\033[0m' +pass() { echo "${GREEN}PASS${NC} $*"; } +fail() { echo "${RED}FAIL${NC} $*" >&2; } + +# Check if the container is still alive (returns 0 if running). +container_alive() { + $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qx "$CONTAINER" +} + +# Run docker exec with container-liveness guard + exit-code logging. +# Usage: safe_exec $DOCKER exec [args...] +# Handles multi-word DOCKER values (e.g. "sudo docker") via eval. +# Returns the command's output on stdout; logs failures to stderr. +safe_exec() { + local rc + if ! container_alive; then + fail "container '$CONTAINER' is no longer running (before: $*)" + dump_health_diagnostics + return 1 + fi + set +e + eval "$@" + rc=$? + set -e + if [[ $rc -ne 0 ]]; then + fail "command failed (exit $rc): $*" + dump_health_diagnostics + return 1 + fi +} + +# DOCKER: allow callers to override (e.g. "sudo docker"). Defaults to plain docker. +DOCKER=${DOCKER:-docker} + +# Dump container diagnostics to stderr. Defined early so the EXIT trap can +# call it even if the script fails before the main body. +dump_health_diagnostics() { + echo "----- health-check diagnostics ($CONTAINER) -----" >&2 + $DOCKER inspect --format \ + 'state={{.State.Status}} exit={{.State.ExitCode}} error={{.State.Error}}' \ + "$CONTAINER" >&2 || true + + if $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qx "$CONTAINER"; then + echo "----- listening processes -----" >&2 + $DOCKER exec "$CONTAINER" sh -c \ + 'lsof -nP -iTCP:8080 -sTCP:LISTEN || true' >&2 || true + for endpoint in versions graphs; do + echo "----- GET /$endpoint -----" >&2 + $DOCKER exec "$CONTAINER" curl -i -sS --connect-timeout 5 --max-time 15 \ + "http://127.0.0.1:8080/$endpoint" >&2 || true + done + fi + + echo "----- recent container logs -----" >&2 + $DOCKER logs --tail 100 "$CONTAINER" >&2 || true + echo "----- end health-check diagnostics -----" >&2 +} + +_EXIT_CODE=0 +_exit_handler() { + local rc=$? + # If the script failed, log the exit code so the root cause isn't lost. + if [[ $rc -ne 0 ]]; then + echo "" >&2 + echo "==> Script failed with exit code $rc" >&2 + if container_alive; then + dump_health_diagnostics + else + echo "==> container '$CONTAINER' is not running (may have been OOM-killed or crashed)" >&2 + # Best-effort: check docker inspect for OOMKilled + dump any surviving logs + $DOCKER inspect --format \ + 'OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}} Status={{.State.Status}}' \ + "$CONTAINER" >&2 2>/dev/null || true + $DOCKER logs --tail 80 "$CONTAINER" >&2 2>/dev/null || true + fi + fi + _cleanup +} + +_cleanup() { + # KEEP=1: leave everything standing for live poking. Still dump logs so a + # failure's root cause is visible; skip all teardown. + if [[ -n "${KEEP:-}" ]]; then + echo "==> KEEP=1 set — skipping cleanup. Resources left standing:" >&2 + echo " container: $CONTAINER" >&2 + echo " volume: $VOL" >&2 + echo " image: $TAG" >&2 + return 0 + fi + # Dump container logs first so a failure's root cause isn't destroyed by cleanup. + if $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -qx "$CONTAINER"; then + echo "----- container logs ($CONTAINER) -----" >&2 + $DOCKER logs --tail 60 "$CONTAINER" >&2 || true + echo "----- end logs -----" >&2 + # The startup wrapper hides the real error in the server log file; dump it + # from the stopped container (docker cp works on exited containers). + echo "----- hugegraph-server.log -----" >&2 + $DOCKER cp "$CONTAINER:/server/logs/hugegraph-server.log" - 2>/dev/null \ + | tar -xO 2>/dev/null | tail -n 80 >&2 || true + echo "----- end hugegraph-server.log -----" >&2 + fi + $DOCKER rm -f "$CONTAINER" >/dev/null 2>&1 || true + # Only remove the volume we created. + $DOCKER volume rm "$VOL" >/dev/null 2>&1 || true + # Only remove the image we tagged; never prune the host's image store. + $DOCKER rmi "$TAG" >/dev/null 2>&1 || true + # Remove disk-backed temp work dirs (build context holds the ~1GB extract). + [[ -n "$BUILD_CTX" && -d "$BUILD_CTX" ]] && rm -rf "$BUILD_CTX" + [[ -n "$ROCKS_BUILD" && -d "$ROCKS_BUILD" ]] && rm -rf "$ROCKS_BUILD" +} +trap _exit_handler EXIT + +need() { command -v "$1" >/dev/null 2>&1 || { fail "missing required command: $1"; exit 1; }; } +need docker +# buildx is required: the legacy builder ignores --platform. +$DOCKER buildx version >/dev/null 2>&1 || { fail "docker buildx is required (pacman -S docker-buildx)"; exit 1; } + +# Register QEMU user-mode emulation for riscv64 if the kernel can't already run +# it (otherwise the build/run fails with 'exec format error'). binfmt handlers +# do NOT survive reboots, so a fresh checkout needs this — makes the smoke test +# self-contained on any x86 host. Pinned tag: :latest 403s on Docker Hub. +if ! ls /proc/sys/fs/binfmt_misc/qemu-riscv64 >/dev/null 2>&1; then + echo "==> Registering QEMU riscv64 emulation (binfmt)" + $DOCKER run --privileged --rm tonistiigi/binfmt:qemu-v8.1.5 --install riscv64 >/dev/null +fi + +# 1. Resolve the Server distribution tarball (build on host if not supplied). +# Build from the ROOT pom (not hugegraph-server/): the root parent pom +# `hugegraph` and the root sibling `hugegraph-struct` must be installed +# into ~/.m2 first, or module resolution fails on the ${revision} parent. +if [[ -n "$DIST_ARG" ]]; then + DIST_TAR="$DIST_ARG" +else + echo "==> Building HugeGraph Server distribution on host (native platform)" + mvn -f "$REPO_ROOT/pom.xml" package -e -B -ntp \ + -Dmaven.test.skip=true -Dmaven.javadoc.skip=true -Drat.skip=true \ + -pl hugegraph-server/hugegraph-dist -am + DIST_TAR=$(ls -t "$REPO_ROOT"/hugegraph-server/apache-hugegraph-server-*.tar.gz 2>/dev/null | head -n1 || true) +fi +[[ -f "$DIST_TAR" ]] || { fail "server distribution tarball not found"; exit 1; } +echo "==> Using distribution: $DIST_TAR" + +# Extract straight into the build context's server/ dir — no second copy +# (the extracted tree is ~1GB; copying it twice doubled the temp footprint). +BUILD_CTX=$(mktemp -d "hugegraph-riscv64-ctx.XXXXXX") +SERVER_DIR="$BUILD_CTX/server" +mkdir -p "$SERVER_DIR" +tar -xzf "$DIST_TAR" -C "$SERVER_DIR" --strip-components=1 + +# Force the RocksDB backend for the smoke run. +CONF="$SERVER_DIR/conf/graphs/hugegraph.properties" +grep -qE '^[[:space:]]*backend[[:space:]]*=' "$CONF" \ + && sed -i 's/^[[:space:]]*backend[[:space:]]*=.*/backend=rocksdb/' "$CONF" \ + || echo "backend=rocksdb" >> "$CONF" +grep -qE '^[[:space:]]*serializer[[:space:]]*=' "$CONF" \ + && sed -i 's/^[[:space:]]*serializer[[:space:]]*=.*/serializer=binary/' "$CONF" \ + || echo "serializer=binary" >> "$CONF" + +# 2. Build a tiny RISC-V runtime image on top of the glibc base. +# libatomic1 is installed and exposed via LD_LIBRARY_PATH (+ a /usr/lib +# symlink) so RocksDB's __atomic_compare_exchange_1 symbol resolves +# automatically — no manual step for the user. If the JNI still lacks the +# symbol, the RocksDB step below fails loudly instead of being treated as +# the goal. +# Reuse the repo's own container entrypoint: it runs init-store.sh (create the +# RocksDB store) BEFORE start-hugegraph.sh. Hand-rolling the start command +# skips init and the server aborts with an empty log (the failure we hit). +cp "$REPO_ROOT/hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh" \ + "$BUILD_CTX/server/docker-entrypoint.sh" +# The entrypoint hardcodes 'start-hugegraph.sh ... -t 120'. Under QEMU emulation +# the analyzer dictionary loads twice (~3.5 min each) plus Gremlin/Groovy warmup, +# so the server needs well over 600s to bind 8080. Raise the startup timeout. +sed -i 's/-t 120/-t 1800/' "$BUILD_CTX/server/docker-entrypoint.sh" +cat > "$BUILD_CTX/Dockerfile" < Building RISC-V runtime image $TAG" +$DOCKER buildx build --platform "$PLATFORM" -t "$TAG" "$BUILD_CTX" --load + +# 3. Start the server in the background; trap handles cleanup. +echo "==> Starting server container on ${PLATFORM}" +$DOCKER run -d --name "$CONTAINER" --platform "$PLATFORM" \ + -p 8080 --memory=4g -v "$VOL:/server" "$TAG" + +# Wait until /graphs answers 200 (server auto-inits + starts via entrypoint). +# This is the same readiness endpoint used by start-hugegraph.sh. /versions is +# asserted separately after the server has completed its startup sequence. +wait_health() { + # This probe starts while the entrypoint is still running init-store.sh. + # Under QEMU the analyzer dictionary loads once during initialization and + # again during server startup, so the full end-to-end wait needs to exceed + # the entrypoint's 1800s startup timeout as well as initialization time. + local elapsed=0 limit=3600 + while (( elapsed < limit )); do + # Bail fast if the container died instead of polling a corpse for 10 min. + if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qx "$CONTAINER"; then + echo "==> container '$CONTAINER' is no longer running" >&2 + dump_health_diagnostics + return 1 + fi + local code + code=$($DOCKER exec "$CONTAINER" \ + curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8080/graphs 2>/dev/null || true) + code=${code:-000} + [[ "$code" == "200" ]] && return 0 + sleep 5; elapsed=$((elapsed+5)) + done + echo "==> health check timed out after ${limit}s (last /graphs HTTP status: $code)" >&2 + dump_health_diagnostics + return 1 +} + +# 4. Architecture assertion. +echo "==> Verifying architecture" +container_alive || { fail "container died before architecture check"; exit 1; } +ARCH_OK=$($DOCKER exec "$CONTAINER" uname -m) +[[ "$ARCH_OK" == "riscv64" ]] || { fail "architecture=$ARCH_OK (expected riscv64)"; exit 1; } +pass "architecture: riscv64" + +# 5. RocksDB open/put/get/close via a real Java program against the JNI. +echo "==> RocksDB open/put/get/close test" +# If the JNI is not linked to libatomic this step's java process dies with +# "undefined symbol: __atomic_compare_exchange_1" — that is the signal to fix +# the linkage, NOT the expected final result. +ROCKS_TEST='import org.rocksdb.*; +public class RocksSmoke { + public static void main(String[] a) throws Exception { + RocksDB.loadLibrary(); + try (Options o = new Options().setCreateIfMissing(true); + RocksDB db = RocksDB.open(o, "/tmp/rocksdb-smoke")) { + byte[] k = "key".getBytes(java.nio.charset.StandardCharsets.UTF_8); + byte[] v = "value".getBytes(java.nio.charset.StandardCharsets.UTF_8); + db.put(k, v); + byte[] got = db.get(k); + if (!java.util.Arrays.equals(v, got)) + throw new AssertionError("rocksdb get mismatch: " + new String(got)); + db.close(); + } + System.out.println("ROCKS_OK"); + } +}' +container_alive || { fail "container died before RocksDB test"; exit 1; } +ROCKSJAR=$($DOCKER exec "$CONTAINER" sh -c 'ls /server/lib/rocksdbjni-*.jar 2>/dev/null | head -n1') +[[ -n "$ROCKSJAR" ]] || { fail "rocksdbjni jar not found in dist"; exit 1; } +# Compile the smoke class on the HOST (which has a JDK); the runtime image +# ships only a JRE (no javac). The .class is arch-independent bytecode, +# so an x86-compiled class runs fine under the riscv64 JRE. +ROCKS_BUILD=$(mktemp -d "rocks-smoke.XXXXXX") +echo "$ROCKS_TEST" > "$ROCKS_BUILD/RocksSmoke.java" +javac -cp "$SERVER_DIR"/lib/rocksdbjni-*.jar "$ROCKS_BUILD/RocksSmoke.java" -d "$ROCKS_BUILD" \ + || { fail "failed to compile RocksSmoke on host (need a JDK)"; exit 1; } +$DOCKER cp "$ROCKS_BUILD/RocksSmoke.class" "$CONTAINER:/tmp/RocksSmoke.class" +# ponytail: set +e around the pipeline so pipefail doesn't kill the script +# before the grep below can report the actual failure. +set +e +safe_exec "$DOCKER exec $CONTAINER java -cp /tmp:$ROCKSJAR RocksSmoke" 2>&1 \ + | tee "$ROCKS_BUILD/rocks-out.log" +JAVA_RC=${PIPESTATUS[0]} +set -e +grep -q "ROCKS_OK" "$ROCKS_BUILD/rocks-out.log" || { + fail "RocksDB open/put/get/close failed (java exit=$JAVA_RC, check JNI/libatomic linkage)" + exit 1 +} +pass "RocksDB: open / put / get / close" + +# 6. init already ran via entrypoint; confirm health, then REST CRUD + Gremlin. +wait_health || { fail "server did not become healthy (GET /graphs)"; exit 1; } +pass "HugeGraph: init / health (GET /graphs -> 200)" + +# ponytail: --max-time 300 because QEMU makes every REST call slow; upgrade to +# per-endpoint tuning if individual calls need more headroom. +container_alive || { fail "container died before REST phase"; exit 1; } +# Wait for /versions to return valid JSON — the server's /graphs health check +# may pass before the REST API layer is fully initialized. +REST_READY="" +for _retry in 1 2 3 4 5 6 7 8 9 10; do + REST_READY=$($DOCKER exec "$CONTAINER" sh -c 'curl -s --max-time 30 http://localhost:8080/versions && echo' || true) + if [[ "$REST_READY" == *'"core"'* ]]; then break; fi + echo " waiting for REST API (attempt $_retry)..." >&2 + sleep 10 +done +[[ "$REST_READY" == *'"core"'* ]] || { fail "REST API not ready after retries: $REST_READY"; exit 1; } + +# Create schema, a vertex, and an edge; read them back. +# Schema order matters (commits are synchronous, but each element must exist +# before the one that references it): property key "name" -> vertex label +# "person" (declares name) -> edge label "knows" -> vertices -> edge. +# Each POST asserts HTTP 2xx so a bad request fails loud, not silent. +echo "==> REST schema + CRUD" +rest_post() { # $1=path $2=json + local body code response rc + set +e + response=$($DOCKER exec "$CONTAINER" curl -sS -w $'\n%{http_code}' -X POST \ + "http://localhost:8080/graphs/hugegraph/$1" \ + -H 'Content-Type: application/json' -d "$2") + rc=$? + set -e + if [[ $rc -ne 0 ]]; then + fail "REST POST $1 failed (curl/docker exec exit $rc): $response" + exit 1 + fi + code=${response##*$'\n'} + body=${response%$'\n'*} + # Tolerate "already exists" (400 + ExistedException) for idempotent schema ops. + if [[ "$code" == 400 && "$body" == *ExistedException* ]]; then + printf '%s\n' "$body" + return 0 + fi + [[ "$code" == 2* ]] || { fail "REST POST $1 failed (HTTP $code): $body"; exit 1; } + printf '%s\n' "$body" +} + +# GET a REST endpoint, capture response to a temp file. +# Usage: rest_get [curl-extra-args...] +# Sets REST_BODY (response body) and REST_CODE (HTTP status). +rest_get() { + local rsp + rsp=$($DOCKER exec "$CONTAINER" curl -sS --compressed -w $'\n%{http_code}' \ + -H 'Accept: application/json' "$@") + REST_CODE=${rsp##*$'\n'} + REST_BODY=${rsp%$'\n'*} +} + +# Assert a jq expression against the last REST_BODY. +# Usage: assert_json '.field == "value"' +assert_json() { + local expr="$1"; shift + if ! printf '%s' "$REST_BODY" | $DOCKER exec -i "$CONTAINER" jq --exit-status "$expr" "$@" >/dev/null 2>&1; then + fail "assertion failed: $expr" + echo " response body: ${REST_BODY:0:300}" >&2 + exit 1 + fi +} +rest_post "schema/propertykeys" \ + '{"name":"name","data_type":"TEXT","cardinality":"SINGLE"}' >/dev/null +rest_post "schema/vertexlabels" \ + '{"name":"person","id_strategy":"DEFAULT","properties":["name"],"primary_keys":["name"]}' >/dev/null +rest_post "schema/edgelabels" \ + '{"name":"knows","source_label":"person","target_label":"person","properties":[],"frequency":"SINGLE"}' >/dev/null +ALICE=$(rest_post "graph/vertices" '{"label":"person","properties":{"name":"alice"}}') +BOB=$(rest_post "graph/vertices" '{"label":"person","properties":{"name":"bob"}}') +ALICE_ID=$(printf '%s\n' "$ALICE" | sed -n 's/.*"id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') +BOB_ID=$(printf '%s\n' "$BOB" | sed -n 's/.*"id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') +[[ -n "$ALICE_ID" && -n "$BOB_ID" ]] || { fail "REST vertex response did not contain IDs"; exit 1; } +rest_post "graph/edges" \ + "{\"label\":\"knows\",\"outV\":\"$ALICE_ID\",\"outVLabel\":\"person\",\"inV\":\"$BOB_ID\",\"inVLabel\":\"person\",\"properties\":{}}" >/dev/null + +container_alive || { fail "container died during REST CRUD"; exit 1; } +# Verify data persisted using jq (robust against binary/JSON format changes). +rest_get "http://localhost:8080/graphs/hugegraph/graph/vertices" +assert_json '.vertices | length > 0' +rest_get "http://localhost:8080/graphs/hugegraph/graph/edges" +assert_json '.edges | length > 0' +pass "HugeGraph: REST CRUD (vertex + edge created)" + +# 7. Gremlin query — proves the full TinkerPop/Gremlin stack runs on JDK 11. +echo "==> Gremlin query" +container_alive || { fail "container died before Gremlin query"; exit 1; } +GREM=$($DOCKER exec "$CONTAINER" curl -s --compressed -X POST http://localhost:8182/ \ + -H 'Content-Type: application/json' \ + -d '{"gremlin":"g.V().hasLabel(\"person\").count()","language":"gremlin-groovy","aliases":{"g":"__g_DEFAULT-hugegraph"}}') +# Use jq to check the count — handles whitespace and type variations. +echo "$GREM" | $DOCKER exec -i "$CONTAINER" jq -e '.result.data[0] == 2' >/dev/null 2>&1 \ + || { fail "Gremlin query did not return expected count (got: $GREM)"; exit 1; } +pass "HugeGraph: Gremlin query (g.V().count() == 2)" + +# 8. Restart and verify persistence from the volume. +echo "==> Restart + persistence check" +container_alive || { fail "container died before restart"; exit 1; } +$DOCKER restart "$CONTAINER" >/dev/null +wait_health || { fail "server not healthy after restart"; exit 1; } +container_alive || { fail "container died after restart health check"; exit 1; } +rest_get "http://localhost:8080/graphs/hugegraph/graph/vertices" +assert_json '.vertices | length > 0' +pass "HugeGraph: restart / persistence" + +# 9. Final summary (matches the issue's expected output format). +echo "" +pass "architecture: riscv64" +pass "RocksDB: open / put / get / close" +pass "HugeGraph: init / health / REST CRUD / Gremlin / restart / persistence" +pass "cleanup: container, volume, and image removed" From 95c1a823aca4469a4c6d88af0c4ac7971121e27f Mon Sep 17 00:00:00 2001 From: Yugaaank Date: Thu, 23 Jul 2026 10:38:55 +0530 Subject: [PATCH 2/3] fix(travis): harden RISC-V smoke test for portability and correctness - Replace GNU sed -i with portable sedi() helper (macOS compat) - Use TMPDIR for mktemp to avoid filling RAM tmpfs - Collision-resistant run ID (timestamp + random hex) - Probe binfmt through Docker daemon, not host /proc - Add curl --connect-timeout/--max-time to all requests - Enforce JAVA_RC == 0 after RocksDB test - Assert specific vertex names and edge in persistence check - Fix cleanup PASS to run after teardown, track success - Fix KEEP=0 treated as truthy - Remove eval from safe_exec, use array splitting --- .../src/assembly/travis/run-riscv64-smoke.sh | 102 +++++++++++++----- 1 file changed, 73 insertions(+), 29 deletions(-) diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/run-riscv64-smoke.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/run-riscv64-smoke.sh index bfb9f0b245..f9f1c7adc3 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/travis/run-riscv64-smoke.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/run-riscv64-smoke.sh @@ -53,8 +53,21 @@ REPO_ROOT=$(cd "$(dirname "$0")/../../../../.." && pwd) mkdir -p "$TMPDIR" export TMPDIR +# Portable in-place sed: avoids GNU sed -i which breaks on macOS/BSD. +sedi() { + local expr="$1" file="$2" + local tmp; tmp=$(mktemp "${TMPDIR:-/tmp}/sedi.XXXXXX") + if sed "$expr" "$file" > "$tmp"; then + mv "$tmp" "$file" + else + rm -f "$tmp" + return 1 + fi +} + # Unique, traceable names so cleanup removes only our own resources. -STAMP=$(date +%Y%m%d%H%M%S) +# Timestamp + random hex: collision-resistant, still human-readable for debugging. +STAMP=$(date +%Y%m%d%H%M%S)-$(head -c 4 /dev/urandom | od -An -tx1 | tr -d ' \n') TAG="hugegraph-riscv64-smoke:${STAMP}" # KEEP: don't auto-cleanup so a failed/finished run stays pokeable. VOL="hugegraph-riscv64-data-${STAMP}" @@ -76,8 +89,8 @@ container_alive() { } # Run docker exec with container-liveness guard + exit-code logging. -# Usage: safe_exec $DOCKER exec [args...] -# Handles multi-word DOCKER values (e.g. "sudo docker") via eval. +# Usage: safe_exec exec [args...] +# DOCKER is split into words without eval; handles "docker" or "sudo docker". # Returns the command's output on stdout; logs failures to stderr. safe_exec() { local rc @@ -87,7 +100,8 @@ safe_exec() { return 1 fi set +e - eval "$@" + local -a docker_parts=($DOCKER) + "${docker_parts[@]}" "$@" rc=$? set -e if [[ $rc -ne 0 ]]; then @@ -148,7 +162,7 @@ _exit_handler() { _cleanup() { # KEEP=1: leave everything standing for live poking. Still dump logs so a # failure's root cause is visible; skip all teardown. - if [[ -n "${KEEP:-}" ]]; then + if [[ "${KEEP:-}" == "1" ]]; then echo "==> KEEP=1 set — skipping cleanup. Resources left standing:" >&2 echo " container: $CONTAINER" >&2 echo " volume: $VOL" >&2 @@ -167,14 +181,20 @@ _cleanup() { | tar -xO 2>/dev/null | tail -n 80 >&2 || true echo "----- end hugegraph-server.log -----" >&2 fi - $DOCKER rm -f "$CONTAINER" >/dev/null 2>&1 || true + local ok=1 + $DOCKER rm -f "$CONTAINER" >/dev/null 2>&1 || ok=0 # Only remove the volume we created. - $DOCKER volume rm "$VOL" >/dev/null 2>&1 || true + $DOCKER volume rm "$VOL" >/dev/null 2>&1 || ok=0 # Only remove the image we tagged; never prune the host's image store. - $DOCKER rmi "$TAG" >/dev/null 2>&1 || true + $DOCKER rmi "$TAG" >/dev/null 2>&1 || ok=0 # Remove disk-backed temp work dirs (build context holds the ~1GB extract). [[ -n "$BUILD_CTX" && -d "$BUILD_CTX" ]] && rm -rf "$BUILD_CTX" [[ -n "$ROCKS_BUILD" && -d "$ROCKS_BUILD" ]] && rm -rf "$ROCKS_BUILD" + if [[ "$ok" == "1" ]]; then + pass "cleanup: container, volume, and image removed" + else + fail "cleanup: some resources may remain (container=$CONTAINER vol=$VOL image=$TAG)" + fi } trap _exit_handler EXIT @@ -183,11 +203,16 @@ need docker # buildx is required: the legacy builder ignores --platform. $DOCKER buildx version >/dev/null 2>&1 || { fail "docker buildx is required (pacman -S docker-buildx)"; exit 1; } -# Register QEMU user-mode emulation for riscv64 if the kernel can't already run -# it (otherwise the build/run fails with 'exec format error'). binfmt handlers -# do NOT survive reboots, so a fresh checkout needs this — makes the smoke test -# self-contained on any x86 host. Pinned tag: :latest 403s on Docker Hub. -if ! ls /proc/sys/fs/binfmt_misc/qemu-riscv64 >/dev/null 2>&1; then +# Register QEMU user-mode emulation for riscv64 if the Docker daemon can't +# already run it (otherwise the build/run fails with 'exec format error'). +# binfmt handlers do NOT survive reboots, so a fresh checkout needs this — +# makes the smoke test self-contained on any x86 host. Pinned tag: :latest +# 403s on Docker Hub. +# +# Probe through the Docker daemon, not the host /proc: Docker Desktop keeps +# binfmt inside its Linux VM; a remote context can have the opposite state. +if ! $DOCKER run --rm --platform "$PLATFORM" "$BASE_IMAGE" uname -m 2>/dev/null \ + | grep -q riscv64; then echo "==> Registering QEMU riscv64 emulation (binfmt)" $DOCKER run --privileged --rm tonistiigi/binfmt:qemu-v8.1.5 --install riscv64 >/dev/null fi @@ -210,7 +235,7 @@ echo "==> Using distribution: $DIST_TAR" # Extract straight into the build context's server/ dir — no second copy # (the extracted tree is ~1GB; copying it twice doubled the temp footprint). -BUILD_CTX=$(mktemp -d "hugegraph-riscv64-ctx.XXXXXX") +BUILD_CTX=$(mktemp -d "${TMPDIR:-/tmp}/hugegraph-riscv64-ctx.XXXXXX") SERVER_DIR="$BUILD_CTX/server" mkdir -p "$SERVER_DIR" tar -xzf "$DIST_TAR" -C "$SERVER_DIR" --strip-components=1 @@ -218,10 +243,10 @@ tar -xzf "$DIST_TAR" -C "$SERVER_DIR" --strip-components=1 # Force the RocksDB backend for the smoke run. CONF="$SERVER_DIR/conf/graphs/hugegraph.properties" grep -qE '^[[:space:]]*backend[[:space:]]*=' "$CONF" \ - && sed -i 's/^[[:space:]]*backend[[:space:]]*=.*/backend=rocksdb/' "$CONF" \ + && sedi 's/^[[:space:]]*backend[[:space:]]*=.*/backend=rocksdb/' "$CONF" \ || echo "backend=rocksdb" >> "$CONF" grep -qE '^[[:space:]]*serializer[[:space:]]*=' "$CONF" \ - && sed -i 's/^[[:space:]]*serializer[[:space:]]*=.*/serializer=binary/' "$CONF" \ + && sedi 's/^[[:space:]]*serializer[[:space:]]*=.*/serializer=binary/' "$CONF" \ || echo "serializer=binary" >> "$CONF" # 2. Build a tiny RISC-V runtime image on top of the glibc base. @@ -238,7 +263,7 @@ cp "$REPO_ROOT/hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh" \ # The entrypoint hardcodes 'start-hugegraph.sh ... -t 120'. Under QEMU emulation # the analyzer dictionary loads twice (~3.5 min each) plus Gremlin/Groovy warmup, # so the server needs well over 600s to bind 8080. Raise the startup timeout. -sed -i 's/-t 120/-t 1800/' "$BUILD_CTX/server/docker-entrypoint.sh" +sedi 's/-t 120/-t 1800/' "$BUILD_CTX/server/docker-entrypoint.sh" cat > "$BUILD_CTX/Dockerfile" </dev/null | grep -qx "$CONTAINER"; then echo "==> container '$CONTAINER' is no longer running" >&2 @@ -292,10 +318,11 @@ wait_health() { fi local code code=$($DOCKER exec "$CONTAINER" \ - curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8080/graphs 2>/dev/null || true) + curl -s --connect-timeout 10 --max-time 60 \ + -o /dev/null -w "%{http_code}" http://127.0.0.1:8080/graphs 2>/dev/null || true) code=${code:-000} [[ "$code" == "200" ]] && return 0 - sleep 5; elapsed=$((elapsed+5)) + sleep 5 done echo "==> health check timed out after ${limit}s (last /graphs HTTP status: $code)" >&2 dump_health_diagnostics @@ -337,7 +364,7 @@ ROCKSJAR=$($DOCKER exec "$CONTAINER" sh -c 'ls /server/lib/rocksdbjni-*.jar 2>/d # Compile the smoke class on the HOST (which has a JDK); the runtime image # ships only a JRE (no javac). The .class is arch-independent bytecode, # so an x86-compiled class runs fine under the riscv64 JRE. -ROCKS_BUILD=$(mktemp -d "rocks-smoke.XXXXXX") +ROCKS_BUILD=$(mktemp -d "${TMPDIR:-/tmp}/rocks-smoke.XXXXXX") echo "$ROCKS_TEST" > "$ROCKS_BUILD/RocksSmoke.java" javac -cp "$SERVER_DIR"/lib/rocksdbjni-*.jar "$ROCKS_BUILD/RocksSmoke.java" -d "$ROCKS_BUILD" \ || { fail "failed to compile RocksSmoke on host (need a JDK)"; exit 1; } @@ -345,7 +372,7 @@ $DOCKER cp "$ROCKS_BUILD/RocksSmoke.class" "$CONTAINER:/tmp/RocksSmoke.class" # ponytail: set +e around the pipeline so pipefail doesn't kill the script # before the grep below can report the actual failure. set +e -safe_exec "$DOCKER exec $CONTAINER java -cp /tmp:$ROCKSJAR RocksSmoke" 2>&1 \ +safe_exec exec "$CONTAINER" java -cp "/tmp:$ROCKSJAR" RocksSmoke 2>&1 \ | tee "$ROCKS_BUILD/rocks-out.log" JAVA_RC=${PIPESTATUS[0]} set -e @@ -353,6 +380,10 @@ grep -q "ROCKS_OK" "$ROCKS_BUILD/rocks-out.log" || { fail "RocksDB open/put/get/close failed (java exit=$JAVA_RC, check JNI/libatomic linkage)" exit 1 } +[[ "$JAVA_RC" == "0" ]] || { + fail "RocksDB JVM exited with $JAVA_RC despite printing ROCKS_OK" + exit 1 +} pass "RocksDB: open / put / get / close" # 6. init already ran via entrypoint; confirm health, then REST CRUD + Gremlin. @@ -366,7 +397,7 @@ container_alive || { fail "container died before REST phase"; exit 1; } # may pass before the REST API layer is fully initialized. REST_READY="" for _retry in 1 2 3 4 5 6 7 8 9 10; do - REST_READY=$($DOCKER exec "$CONTAINER" sh -c 'curl -s --max-time 30 http://localhost:8080/versions && echo' || true) + REST_READY=$($DOCKER exec "$CONTAINER" sh -c 'curl -s --connect-timeout 10 --max-time 30 http://localhost:8080/versions && echo' || true) if [[ "$REST_READY" == *'"core"'* ]]; then break; fi echo " waiting for REST API (attempt $_retry)..." >&2 sleep 10 @@ -382,7 +413,8 @@ echo "==> REST schema + CRUD" rest_post() { # $1=path $2=json local body code response rc set +e - response=$($DOCKER exec "$CONTAINER" curl -sS -w $'\n%{http_code}' -X POST \ + response=$($DOCKER exec "$CONTAINER" curl -sS --connect-timeout 10 --max-time 300 \ + -w $'\n%{http_code}' -X POST \ "http://localhost:8080/graphs/hugegraph/$1" \ -H 'Content-Type: application/json' -d "$2") rc=$? @@ -407,7 +439,9 @@ rest_post() { # $1=path $2=json # Sets REST_BODY (response body) and REST_CODE (HTTP status). rest_get() { local rsp - rsp=$($DOCKER exec "$CONTAINER" curl -sS --compressed -w $'\n%{http_code}' \ + rsp=$($DOCKER exec "$CONTAINER" curl -sS --compressed \ + --connect-timeout 10 --max-time 300 \ + -w $'\n%{http_code}' \ -H 'Accept: application/json' "$@") REST_CODE=${rsp##*$'\n'} REST_BODY=${rsp%$'\n'*} @@ -448,7 +482,9 @@ pass "HugeGraph: REST CRUD (vertex + edge created)" # 7. Gremlin query — proves the full TinkerPop/Gremlin stack runs on JDK 11. echo "==> Gremlin query" container_alive || { fail "container died before Gremlin query"; exit 1; } -GREM=$($DOCKER exec "$CONTAINER" curl -s --compressed -X POST http://localhost:8182/ \ +GREM=$($DOCKER exec "$CONTAINER" curl -s --compressed \ + --connect-timeout 10 --max-time 300 \ + -X POST http://localhost:8182/ \ -H 'Content-Type: application/json' \ -d '{"gremlin":"g.V().hasLabel(\"person\").count()","language":"gremlin-groovy","aliases":{"g":"__g_DEFAULT-hugegraph"}}') # Use jq to check the count — handles whitespace and type variations. @@ -463,7 +499,15 @@ $DOCKER restart "$CONTAINER" >/dev/null wait_health || { fail "server not healthy after restart"; exit 1; } container_alive || { fail "container died after restart health check"; exit 1; } rest_get "http://localhost:8080/graphs/hugegraph/graph/vertices" -assert_json '.vertices | length > 0' +assert_json '.vertices | length >= 2' +# Verify both created vertices survived (by name, not just count). +# Properties are flat objects: {"properties":{"name":"alice"}}. +assert_json '.vertices | map(.properties.name) | index("alice") != null' +assert_json '.vertices | map(.properties.name) | index("bob") != null' +rest_get "http://localhost:8080/graphs/hugegraph/graph/edges" +assert_json '.edges | length >= 1' +# Verify the "knows" edge between person vertices survived. +assert_json '.edges | any(.label == "knows" and .outVLabel == "person" and .inVLabel == "person")' pass "HugeGraph: restart / persistence" # 9. Final summary (matches the issue's expected output format). @@ -471,4 +515,4 @@ echo "" pass "architecture: riscv64" pass "RocksDB: open / put / get / close" pass "HugeGraph: init / health / REST CRUD / Gremlin / restart / persistence" -pass "cleanup: container, volume, and image removed" +# cleanup PASS is printed by _cleanup() after verifying teardown succeeded. From 889cb4547d01c853dd27d9414a9c2ef5d8199970 Mon Sep 17 00:00:00 2001 From: Yugaaank Date: Thu, 23 Jul 2026 19:25:31 +0530 Subject: [PATCH 3/3] fix(travis): harden smoke test security, cleanup propagation, and HTTP checks - Remove -p 8080 to avoid exposing unauthenticated REST on host network - Make _cleanup propagate failure via _CLEANUP_OK flag - Add INT TERM to trap for Ctrl+C cleanup safety - Check HTTP status in rest_get and Gremlin query - Add host tool preflight (mvn, javac) before privileged binfmt install - Add --cap-drop ALL --security-opt no-new-privileges to container - Validate vertex IDs before JSON interpolation - Fix .gitignore: remove unanchored patterns, keep /.riscv64-smoke-tmp/ - Remove dead _EXIT_CODE variable - Track BUILD_CTX/ROCKS_BUILD cleanup failures in ok flag - Propagate actual exit code from safe_exec - Remove redundant container_alive after wait_health - Dump diagnostics before KEEP=1 skip --- .gitignore | 4 +- .../src/assembly/travis/run-riscv64-smoke.sh | 68 +++++++++++++------ 2 files changed, 47 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index 9ced97619d..dd2bdf3e2f 100644 --- a/.gitignore +++ b/.gitignore @@ -118,6 +118,4 @@ codeium-instructions.md *.ai-prompt.md WARP.md .mcp.json -hugegraph-riscv64-ctx.*/ -rocks-smoke.*/ -.riscv64-smoke-tmp/ +/.riscv64-smoke-tmp/ diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/run-riscv64-smoke.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/run-riscv64-smoke.sh index f9f1c7adc3..8e7181f791 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/travis/run-riscv64-smoke.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/run-riscv64-smoke.sh @@ -43,6 +43,7 @@ set -euo pipefail ARCH=riscv64 PLATFORM="linux/${ARCH}" # Ubuntu 24.04 publishes openjdk-11-jre-headless for linux/riscv64 (glibc). +# TODO: Pin to a SHA256 digest for reproducibility (e.g. ubuntu@sha256:...). BASE_IMAGE="ubuntu:24.04" REPO_ROOT=$(cd "$(dirname "$0")/../../../../.." && pwd) @@ -107,7 +108,7 @@ safe_exec() { if [[ $rc -ne 0 ]]; then fail "command failed (exit $rc): $*" dump_health_diagnostics - return 1 + return "$rc" fi } @@ -138,7 +139,6 @@ dump_health_diagnostics() { echo "----- end health-check diagnostics -----" >&2 } -_EXIT_CODE=0 _exit_handler() { local rc=$? # If the script failed, log the exit code so the root cause isn't lost. @@ -157,30 +157,35 @@ _exit_handler() { fi fi _cleanup + # Propagate cleanup failure: if the main run succeeded but cleanup failed, + # exit nonzero so CI records the resource leak. + if [[ $rc -eq 0 && $_CLEANUP_OK -ne 0 ]]; then + exit 1 + fi + exit "$rc" } _cleanup() { - # KEEP=1: leave everything standing for live poking. Still dump logs so a - # failure's root cause is visible; skip all teardown. - if [[ "${KEEP:-}" == "1" ]]; then - echo "==> KEEP=1 set — skipping cleanup. Resources left standing:" >&2 - echo " container: $CONTAINER" >&2 - echo " volume: $VOL" >&2 - echo " image: $TAG" >&2 - return 0 - fi + _CLEANUP_OK=0 # Dump container logs first so a failure's root cause isn't destroyed by cleanup. + # This runs even with KEEP=1 so the user gets diagnostics. if $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -qx "$CONTAINER"; then echo "----- container logs ($CONTAINER) -----" >&2 $DOCKER logs --tail 60 "$CONTAINER" >&2 || true echo "----- end logs -----" >&2 - # The startup wrapper hides the real error in the server log file; dump it - # from the stopped container (docker cp works on exited containers). echo "----- hugegraph-server.log -----" >&2 $DOCKER cp "$CONTAINER:/server/logs/hugegraph-server.log" - 2>/dev/null \ | tar -xO 2>/dev/null | tail -n 80 >&2 || true echo "----- end hugegraph-server.log -----" >&2 fi + # KEEP=1: leave everything standing for live poking; skip teardown. + if [[ "${KEEP:-}" == "1" ]]; then + echo "==> KEEP=1 set — skipping cleanup. Resources left standing:" >&2 + echo " container: $CONTAINER" >&2 + echo " volume: $VOL" >&2 + echo " image: $TAG" >&2 + return 0 + fi local ok=1 $DOCKER rm -f "$CONTAINER" >/dev/null 2>&1 || ok=0 # Only remove the volume we created. @@ -188,26 +193,34 @@ _cleanup() { # Only remove the image we tagged; never prune the host's image store. $DOCKER rmi "$TAG" >/dev/null 2>&1 || ok=0 # Remove disk-backed temp work dirs (build context holds the ~1GB extract). - [[ -n "$BUILD_CTX" && -d "$BUILD_CTX" ]] && rm -rf "$BUILD_CTX" - [[ -n "$ROCKS_BUILD" && -d "$ROCKS_BUILD" ]] && rm -rf "$ROCKS_BUILD" + if [[ -n "$BUILD_CTX" && -d "$BUILD_CTX" ]]; then + rm -rf "$BUILD_CTX" || ok=0 + fi + if [[ -n "$ROCKS_BUILD" && -d "$ROCKS_BUILD" ]]; then + rm -rf "$ROCKS_BUILD" || ok=0 + fi if [[ "$ok" == "1" ]]; then pass "cleanup: container, volume, and image removed" else fail "cleanup: some resources may remain (container=$CONTAINER vol=$VOL image=$TAG)" + _CLEANUP_OK=1 fi } -trap _exit_handler EXIT +trap _exit_handler EXIT INT TERM need() { command -v "$1" >/dev/null 2>&1 || { fail "missing required command: $1"; exit 1; }; } + +# Preflight host tools before any privileged operations. need docker -# buildx is required: the legacy builder ignores --platform. $DOCKER buildx version >/dev/null 2>&1 || { fail "docker buildx is required (pacman -S docker-buildx)"; exit 1; } +command -v mvn >/dev/null 2>&1 || { fail "mvn is required to build the distribution"; exit 1; } +command -v javac >/dev/null 2>&1 || { fail "javac is required to compile the RocksDB smoke test"; exit 1; } # Register QEMU user-mode emulation for riscv64 if the Docker daemon can't # already run it (otherwise the build/run fails with 'exec format error'). # binfmt handlers do NOT survive reboots, so a fresh checkout needs this — -# makes the smoke test self-contained on any x86 host. Pinned tag: :latest -# 403s on Docker Hub. +# makes the smoke test self-contained on any x86 host. +# TODO: Pin to a SHA256 digest for reproducibility (e.g. tonistiigi/binfmt@sha256:...). # # Probe through the Docker daemon, not the host /proc: Docker Desktop keeps # binfmt inside its Linux VM; a remote context can have the opposite state. @@ -297,7 +310,8 @@ $DOCKER buildx build --platform "$PLATFORM" -t "$TAG" "$BUILD_CTX" --load # 3. Start the server in the background; trap handles cleanup. echo "==> Starting server container on ${PLATFORM}" $DOCKER run -d --name "$CONTAINER" --platform "$PLATFORM" \ - -p 8080 --memory=4g -v "$VOL:/server" "$TAG" + --memory=4g --cap-drop ALL --security-opt no-new-privileges \ + -v "$VOL:/server" "$TAG" # Wait until /graphs answers 200 (server auto-inits + starts via entrypoint). # This is the same readiness endpoint used by start-hugegraph.sh. /versions is @@ -445,6 +459,10 @@ rest_get() { -H 'Accept: application/json' "$@") REST_CODE=${rsp##*$'\n'} REST_BODY=${rsp%$'\n'*} + [[ "$REST_CODE" == 2* ]] || { + fail "REST GET failed (HTTP $REST_CODE): ${REST_BODY:0:300}" + exit 1 + } } # Assert a jq expression against the last REST_BODY. @@ -468,6 +486,9 @@ BOB=$(rest_post "graph/vertices" '{"label":"person","properties":{"name":"bob"}} ALICE_ID=$(printf '%s\n' "$ALICE" | sed -n 's/.*"id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') BOB_ID=$(printf '%s\n' "$BOB" | sed -n 's/.*"id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') [[ -n "$ALICE_ID" && -n "$BOB_ID" ]] || { fail "REST vertex response did not contain IDs"; exit 1; } +# Validate IDs are safe for JSON interpolation (no quotes/backslashes). +[[ "$ALICE_ID" =~ ^[a-zA-Z0-9_.:-]+$ ]] || { fail "ALICE_ID contains unsafe characters: $ALICE_ID"; exit 1; } +[[ "$BOB_ID" =~ ^[a-zA-Z0-9_.:-]+$ ]] || { fail "BOB_ID contains unsafe characters: $BOB_ID"; exit 1; } rest_post "graph/edges" \ "{\"label\":\"knows\",\"outV\":\"$ALICE_ID\",\"outVLabel\":\"person\",\"inV\":\"$BOB_ID\",\"inVLabel\":\"person\",\"properties\":{}}" >/dev/null @@ -482,11 +503,15 @@ pass "HugeGraph: REST CRUD (vertex + edge created)" # 7. Gremlin query — proves the full TinkerPop/Gremlin stack runs on JDK 11. echo "==> Gremlin query" container_alive || { fail "container died before Gremlin query"; exit 1; } -GREM=$($DOCKER exec "$CONTAINER" curl -s --compressed \ +GREM_RSP=$($DOCKER exec "$CONTAINER" curl -sS --compressed \ --connect-timeout 10 --max-time 300 \ + -w $'\n%{http_code}' \ -X POST http://localhost:8182/ \ -H 'Content-Type: application/json' \ -d '{"gremlin":"g.V().hasLabel(\"person\").count()","language":"gremlin-groovy","aliases":{"g":"__g_DEFAULT-hugegraph"}}') +GREM_CODE=${GREM_RSP##*$'\n'} +GREM=${GREM_RSP%$'\n'*} +[[ "$GREM_CODE" == 2* ]] || { fail "Gremlin query failed (HTTP $GREM_CODE): $GREM"; exit 1; } # Use jq to check the count — handles whitespace and type variations. echo "$GREM" | $DOCKER exec -i "$CONTAINER" jq -e '.result.data[0] == 2' >/dev/null 2>&1 \ || { fail "Gremlin query did not return expected count (got: $GREM)"; exit 1; } @@ -497,7 +522,6 @@ echo "==> Restart + persistence check" container_alive || { fail "container died before restart"; exit 1; } $DOCKER restart "$CONTAINER" >/dev/null wait_health || { fail "server not healthy after restart"; exit 1; } -container_alive || { fail "container died after restart health check"; exit 1; } rest_get "http://localhost:8080/graphs/hugegraph/graph/vertices" assert_json '.vertices | length >= 2' # Verify both created vertices survived (by name, not just count).