#!/usr/bin/env bash # End-to-end checks of hosted projects against a real vcs-server: two # people, a reader, two machines, takeovers, waiting changes, offline work # and 32 agents landing at once. # Usage: tests/hosted.sh [path/to/target/debug] set -uo pipefail BIN=${1:-$(cd "$(dirname "$0")/.." && pwd)/target/debug} VCS=$BIN/vcs SERVER=$BIN/vcs-server WORK=$(mktemp -d) export VCS_SAVER=0 PORT=$((20000 + RANDOM % 20000)) HOST="127.0.0.1:$PORT" SERVER_PID= cleanup() { [ -n "$SERVER_PID" ] && kill "$SERVER_PID" 2>/dev/null rm -rf "$WORK" } trap cleanup EXIT cd "$WORK" start_server() { "$SERVER" serve --data "$WORK/server" --listen "$HOST" --threads 32 2>"$WORK/server.log" & SERVER_PID=$! for _ in $(seq 50); do curl -s "http://$HOST/.well-known/vcs.json" >/dev/null && return; sleep 0.1; done echo "server didn't start"; exit 1 } pass=0 fail=0 check() { local name=$1 shift if "$@" >/dev/null 2>&1; then pass=$((pass + 1)); else fail=$((fail + 1)); echo "FAIL: $name"; fi } code() { local want=$1 shift "$@" >/dev/null 2>&1 [ "$?" = "$want" ] } jq_() { python3 -c "import json,sys; d=json.load(sys.stdin); print($1)"; } # People and machines: each has a VCS_HOME. as() { # who, command... local who=$1 shift case $who in laptop) VCS_HOME="$WORK/laptop" VCS_NAME="Alice" VCS_EMAIL="alice@acme.test" "$@" ;; cloud) VCS_HOME="$WORK/cloud" VCS_NAME="Alice" VCS_EMAIL="alice@acme.test" "$@" ;; bob) VCS_HOME="$WORK/bob" VCS_NAME="Bob" VCS_EMAIL="bob@acme.test" "$@" ;; carol) VCS_HOME="$WORK/carol" VCS_NAME="Carol" VCS_EMAIL="carol@acme.test" "$@" ;; esac } ALICE=$("$SERVER" user add alice --name Alice --email alice@acme.test --data "$WORK/server") BOB=$("$SERVER" user add bob --name Bob --email bob@acme.test --data "$WORK/server") CAROL=$("$SERVER" user add carol --name Carol --email carol@acme.test --data "$WORK/server") start_server check "laptop login" as laptop "$VCS" login "$HOST" --token "$ALICE" check "cloud login" as cloud "$VCS" login "$HOST" --token "$ALICE" check "bob login" as bob "$VCS" login "$HOST" --token "$BOB" check "carol login" as carol "$VCS" login "$HOST" --token "$CAROL" # Alice starts locally and publishes. mkdir shop && cd shop printf 'line 1\nline 2\nline 3\n' > shared.txt printf 'SECRET=alice\n' > .env check "init" as laptop "$VCS" init check "first version" as laptop "$VCS" land -m "First version" printf 'line 1\nline 2\nline 3\nline 4\n' > shared.txt check "second version" as laptop "$VCS" land -m "Second version" check "publish" as laptop "$VCS" publish "$HOST/acme/shop" check "main is 2 on the host" test "$(curl -s -H "authorization: Bearer $ALICE" "http://$HOST/v1/projects/acme/shop" | jq_ 'd["main"]["n"]')" = 2 "$SERVER" member add acme/shop bob --role member --data "$WORK/server" "$SERVER" member add acme/shop carol --role reader --data "$WORK/server" # Bob gets it. cd "$WORK" check "bob gets" as bob "$VCS" get "$HOST/acme/shop" bobshop check "bob has the files" test "$(tail -1 bobshop/shared.txt)" = "line 4" check "bob doesn't get alice's .env" test ! -e bobshop/.env # Parallel landings from two people. printf 'alice\n' > shop/alice.txt printf 'bob\n' > bobshop/bob.txt check "alice lands" as laptop "$VCS" -C shop land -m "Alice's file" check "bob lands without syncing" as bob "$VCS" -C bobshop land -m "Bob's file" check "main is 4" test "$(as bob "$VCS" --json -C bobshop log | jq_ 'd["versions"][0]["n"]')" = 4 # A conflict between them. as laptop "$VCS" -C shop sync >/dev/null 2>&1 as bob "$VCS" -C bobshop sync >/dev/null 2>&1 sed -i 's/^line 2$/line two (alice)/' shop/shared.txt sed -i 's/^line 2$/line two (bob)/' bobshop/shared.txt check "alice lands line 2" as laptop "$VCS" -C shop land -m "Alice's line 2" check "bob conflicts (exit 3)" code 3 as bob "$VCS" -C bobshop land -m "Bob's line 2" check "bob syncs (exit 3)" code 3 as bob "$VCS" -C bobshop sync check "markers" grep -q '<<<<<<< main' bobshop/shared.txt sed -i '/^<<<<<<< /d; /^=======$/d; /^>>>>>>> /d; /line two (alice)/d' bobshop/shared.txt check "bob lands after fixing" as bob "$VCS" -C bobshop land # A reader's change waits for someone who can land it. check "carol gets" as carol "$VCS" get "$HOST/acme/shop" carolshop printf 'fix from carol\n' > carolshop/carol.txt check "carol's land waits (exit 5)" code 5 as carol "$VCS" -C carolshop land -m "Carol's fix" CAROL_CHANGE=$(as carol "$VCS" --json -C carolshop log --changes | jq_ '[c["change"]["id"] for c in d["changes"] if c["change"]["state"]=="waiting"][0]') check "alice sees the waiting change" sh -c "VCS_HOME=$WORK/laptop $VCS -C $WORK/shop log --changes | grep -q waiting" as laptop "$VCS" -C shop land "$CAROL_CHANGE" >"$WORK/carol-land.out" 2>&1; cat "$WORK/carol-land.out" >&2 check "alice lands carol's change" grep -q landed "$WORK/carol-land.out" check "carol is the author" test "$(as laptop "$VCS" --json -C shop log -n 1 | jq_ 'd["versions"][0]["author"].split(" <")[0]')" = Carol # Alice moves from her laptop to a cloud machine mid-change. as laptop "$VCS" -C shop sync >/dev/null 2>&1 printf 'started on the laptop\n' > shop/feature.txt check "describe on laptop" as laptop "$VCS" -C shop describe "Feature" FEATURE=$(as laptop "$VCS" --json -C shop status | jq_ 'd["change"]["id"]') check "cloud picks it up" as cloud "$VCS" get "$HOST/acme/shop" cloudshop --change "$FEATURE" check "the edit came along" test "$(cat cloudshop/feature.txt)" = "started on the laptop" check ".env came along" test "$(cat cloudshop/.env)" = "SECRET=alice" printf 'continued in the cloud\n' >> cloudshop/feature.txt check "cloud saves" as cloud "$VCS" -C cloudshop status printf 'late laptop edit\n' > shop/late.txt OUT=$(as laptop "$VCS" --json -C shop status 2>/dev/null) check "laptop's late edit is rescued into a new change" sh -c "echo '$OUT' | grep -q 'Rescued from'" check "cloud lands the feature" as cloud "$VCS" -C cloudshop land check "feature has both edits" test "$(as bob "$VCS" -C bobshop sync >/dev/null 2>&1; cat bobshop/feature.txt | wc -l)" = 2 # Sharing a change. printf 'draft\n' > bobshop/draft.txt check "bob describes" as bob "$VCS" -C bobshop describe "Draft for Alice" DRAFT=$(as bob "$VCS" --json -C bobshop status | jq_ 'd["change"]["id"]') check "bob shares it" as bob "$VCS" -C bobshop access "$DRAFT" @alice check "alice sees it" sh -c "VCS_HOME=$WORK/laptop $VCS -C $WORK/shop log --changes | grep -q 'Draft for Alice'" # Private paths: only Alice sees billing/. (First drop the rescued change, # which still has the takeover's conflict in it.) as laptop "$VCS" -C shop drop >/dev/null 2>&1 as laptop "$VCS" -C shop sync >/dev/null 2>&1 mkdir -p shop/billing && printf 'rate 10\n' > shop/billing/rates.txt check "alice lands billing" as laptop "$VCS" -C shop land -m "Billing rates" check "bob sees billing before the rule" sh -c "VCS_HOME=$WORK/bob $VCS -C $WORK/bobshop sync >/dev/null 2>&1; test -e $WORK/bobshop/billing/rates.txt" check "alice makes billing/ hers" as laptop "$VCS" -C shop access billing @alice check "bob's sync takes billing away" sh -c "VCS_HOME=$WORK/bob $VCS -C $WORK/bobshop sync >/dev/null 2>&1; test ! -e $WORK/bobshop/billing" printf 'bob was here\n' >> bobshop/README.md 2>/dev/null || printf 'bob was here\n' > bobshop/README.md check "bob lands outside billing" as bob "$VCS" -C bobshop land -m "Bob's readme" check "alice still has billing" sh -c "VCS_HOME=$WORK/laptop $VCS -C $WORK/shop sync >/dev/null 2>&1; test -e $WORK/shop/billing/rates.txt" mkdir -p bobshop/billing && printf 'sneaky\n' > bobshop/billing/new.txt check "bob can't write into billing (exit 7)" code 7 as bob "$VCS" -C bobshop land -m "Sneaky" rm -rf bobshop/billing as bob "$VCS" -C bobshop drop >/dev/null 2>&1 printf 'rate 12\n' > shop/billing/rates.txt check "alice changes billing" as laptop "$VCS" -C shop land -m "New billing rates" check "bob's log shows a private change" sh -c "VCS_HOME=$WORK/bob $VCS -C $WORK/bobshop log -n 1 | grep -q 'a private change'" check "carol never gets billing" sh -c "VCS_HOME=$WORK/carol $VCS get $HOST/acme/shop $WORK/carol2 >/dev/null 2>&1; test -e $WORK/carol2/README.md -o -e $WORK/carol2/shared.txt; test ! -e $WORK/carol2/billing" check "bob can't read billing over the API" sh -c "! curl -sf -H 'authorization: Bearer $BOB' http://$HOST/v1/projects/acme/shop/trees/\$(curl -s -H 'authorization: Bearer $ALICE' http://$HOST/v1/projects/acme/shop | python3 -c 'import json,sys; print(json.load(sys.stdin)[\"main\"][\"tree\"])') >/dev/null" # Undo a hosted land. as cloud "$VCS" -C cloudshop sync >/dev/null 2>&1 printf 'oops\n' > cloudshop/oops.txt check "cloud lands oops" as cloud "$VCS" -C cloudshop land -m "Oops" check "cloud undoes it" as cloud "$VCS" -C cloudshop undo check "main took it back" sh -c "VCS_HOME=$WORK/bob $VCS -C $WORK/bobshop log -n 1 | grep -q 'Take back'" # Offline: saves stay local, land says offline, later it all goes up. kill "$SERVER_PID"; wait "$SERVER_PID" 2>/dev/null; SERVER_PID= printf 'written offline\n' > cloudshop/offline.txt check "status works offline" as cloud "$VCS" -C cloudshop status check "land says offline (exit 6)" code 6 as cloud "$VCS" -C cloudshop land -m "Offline work" start_server check "land works once back" as cloud "$VCS" -C cloudshop land -m "Offline work" # 32 agents land at once on one main, none refused. BEFORE=$(as bob "$VCS" --json -C bobshop log -n 1 | jq_ 'd["versions"][0]["n"]') for i in $(seq 1 32); do as bob "$VCS" get "$HOST/acme/shop" "agent$i" >/dev/null 2>&1 printf 'agent %s\n' "$i" > "agent$i/agent-$i.txt" done pids=() for i in $(seq 1 32); do (VCS_HOME="$WORK/bob" VCS_NAME="Bob" VCS_AGENT="agent-$i" "$VCS" -C "agent$i" land -m "Agent $i" >"$WORK/land$i.out" 2>&1; echo $? >"$WORK/land$i.code") & pids+=($!) done wait "${pids[@]}" OK=$(grep -l '^0$' "$WORK"/land*.code | wc -l) check "all 32 landed" test "$OK" = 32 AFTER=$(as bob "$VCS" --json -C bobshop log -n 1 | jq_ 'd["versions"][0]["n"]') check "main moved by 32" test "$((AFTER - BEFORE))" = 32 echo "hosted: $pass passed, $fail failed" [ "$fail" = 0 ]