Onset: version control where nobody waits and nothing gets lost. The on command, its core and onset-server.
| 1 | #!/usr/bin/env bash |
| 2 | # End-to-end checks of hosted projects: two people, a reader, two machines, |
| 3 | # takeovers, waiting changes, offline work and 32 agents landing at once. |
| 4 | # Against the self-hosted vcs-server by default; with VCS_TARGET=worker, |
| 5 | # against the hosted service (apps/vcs) under `wrangler dev`, in its test |
| 6 | # sign-in mode. |
| 7 | # Usage: [VCS_TARGET=worker] tests/hosted.sh [path/to/target/debug] |
| 8 | set -uo pipefail |
| 9 |
| 10 | ROOT=$(cd "$(dirname "$0")/.." && pwd) |
| 11 | TESTS=$ROOT/tests |
| 12 | BIN=${1:-$ROOT/target/debug} |
| 13 | VCS=$BIN/vcs |
| 14 | SERVER=$BIN/vcs-server |
| 15 | TARGET=${VCS_TARGET:-server} |
| 16 | WORK=$(mktemp -d) |
| 17 | export VCS_SAVER=0 |
| 18 | PORT=$((20000 + RANDOM % 20000)) |
| 19 | HOST="127.0.0.1:$PORT" |
| 20 | SERVER_PID= |
| 21 | stop_server() { |
| 22 | [ -z "$SERVER_PID" ] && return |
| 23 | if [ "$TARGET" = worker ]; then kill -- "-$SERVER_PID" 2>/dev/null; else kill "$SERVER_PID" 2>/dev/null; fi |
| 24 | wait "$SERVER_PID" 2>/dev/null |
| 25 | SERVER_PID= |
| 26 | } |
| 27 | cleanup() { |
| 28 | stop_server |
| 29 | rm -rf "$WORK" |
| 30 | } |
| 31 | trap cleanup EXIT |
| 32 | cd "$WORK" |
| 33 |
| 34 | if [ "$TARGET" = worker ]; then |
| 35 | APP=$ROOT/../apps/vcs |
| 36 | ALICE=vcs_test_alice_0123456789 |
| 37 | BOB=vcs_test_bob_0123456789 |
| 38 | CAROL=vcs_test_carol_0123456789 |
| 39 | USERS="{\"$ALICE\":{\"handle\":\"alice\",\"name\":\"Alice\",\"email\":\"alice@acme.test\"},\"$BOB\":{\"handle\":\"bob\",\"name\":\"Bob\",\"email\":\"bob@acme.test\"},\"$CAROL\":{\"handle\":\"carol\",\"name\":\"Carol\",\"email\":\"carol@acme.test\"}}" |
| 40 | start_server() { |
| 41 | # Its own process group, so stopping it stops workerd too, and its own |
| 42 | # name, so a dev process left from another run never stands in for it. |
| 43 | WRANGLER_SEND_METRICS=false setsid "$APP/node_modules/.bin/wrangler" dev --cwd "$APP" --name "vcs-test-$PORT" --port "$PORT" --ip 127.0.0.1 --inspector-port "$((PORT + 1))" \ |
| 44 | --persist-to "$WORK/wrangler" --var AUTH_MODE:test --var "TEST_USERS:$USERS" --var COMPACT_AT:4096 >"$WORK/server.log" 2>&1 & |
| 45 | SERVER_PID=$! |
| 46 | for _ in $(seq 300); do curl -s "http://$HOST/.well-known/vcs.json" 2>/dev/null | grep -q api && return; sleep 0.2; done |
| 47 | cat "$WORK/server.log"; echo "worker didn't start"; exit 1 |
| 48 | } |
| 49 | add_member() { # project, handle, role |
| 50 | curl -sf -X POST -H "authorization: Bearer $ALICE" -H 'content-type: application/json' -d "{\"handle\":\"$2\",\"role\":\"$3\"}" "http://$HOST/v1/projects/$1/members" >/dev/null |
| 51 | } |
| 52 | else |
| 53 | start_server() { |
| 54 | "$SERVER" serve --data "$WORK/server" --listen "$HOST" --threads 32 2>"$WORK/server.log" & |
| 55 | SERVER_PID=$! |
| 56 | for _ in $(seq 50); do curl -s "http://$HOST/.well-known/vcs.json" >/dev/null && return; sleep 0.1; done |
| 57 | echo "server didn't start"; exit 1 |
| 58 | } |
| 59 | add_member() { |
| 60 | "$SERVER" member add "$1" "$2" --role "$3" --data "$WORK/server" |
| 61 | } |
| 62 | ALICE=$("$SERVER" user add alice --name Alice --email alice@acme.test --data "$WORK/server") |
| 63 | BOB=$("$SERVER" user add bob --name Bob --email bob@acme.test --data "$WORK/server") |
| 64 | CAROL=$("$SERVER" user add carol --name Carol --email carol@acme.test --data "$WORK/server") |
| 65 | fi |
| 66 |
| 67 | pass=0 |
| 68 | fail=0 |
| 69 | check() { |
| 70 | local name=$1 |
| 71 | shift |
| 72 | if "$@" >/dev/null 2>&1; then pass=$((pass + 1)); else fail=$((fail + 1)); echo "FAIL: $name"; fi |
| 73 | } |
| 74 | code() { |
| 75 | local want=$1 |
| 76 | shift |
| 77 | "$@" >/dev/null 2>&1 |
| 78 | [ "$?" = "$want" ] |
| 79 | } |
| 80 | jq_() { python3 -c "import json,sys; d=json.load(sys.stdin); print($1)"; } |
| 81 |
| 82 | # People and machines: each has a VCS_HOME. |
| 83 | as() { # who, command... |
| 84 | local who=$1 |
| 85 | shift |
| 86 | case $who in |
| 87 | laptop) VCS_HOME="$WORK/laptop" VCS_NAME="Alice" VCS_EMAIL="alice@acme.test" "$@" ;; |
| 88 | cloud) VCS_HOME="$WORK/cloud" VCS_NAME="Alice" VCS_EMAIL="alice@acme.test" "$@" ;; |
| 89 | bob) VCS_HOME="$WORK/bob" VCS_NAME="Bob" VCS_EMAIL="bob@acme.test" "$@" ;; |
| 90 | carol) VCS_HOME="$WORK/carol" VCS_NAME="Carol" VCS_EMAIL="carol@acme.test" "$@" ;; |
| 91 | esac |
| 92 | } |
| 93 |
| 94 | start_server |
| 95 |
| 96 | check "laptop login" as laptop "$VCS" login "$HOST" --token "$ALICE" |
| 97 | check "cloud login" as cloud "$VCS" login "$HOST" --token "$ALICE" |
| 98 | check "bob login" as bob "$VCS" login "$HOST" --token "$BOB" |
| 99 | check "carol login" as carol "$VCS" login "$HOST" --token "$CAROL" |
| 100 |
| 101 | # Signing in through the browser (a stand-in sign-in page approves it). |
| 102 | LPORT=$((PORT + 3)) |
| 103 | python3 "$TESTS/login-server.py" "$LPORT" "$ALICE" & |
| 104 | LOGIN_PID=$! |
| 105 | sleep 0.5 |
| 106 | check "browser login" sh -c "VCS_HOME=$WORK/browser VCS_LOGIN=http://127.0.0.1:$LPORT/login '$VCS' login '$HOST' 2>/dev/null | grep -q 'as alice'" |
| 107 | kill "$LOGIN_PID" 2>/dev/null |
| 108 | check "no browser login without a sign-in page" sh -c "VCS_HOME=$WORK/browser2 '$VCS' login '$HOST' 2>&1 | grep -q -- '--token'" |
| 109 |
| 110 | # Alice starts locally and publishes. |
| 111 | mkdir shop && cd shop |
| 112 | printf 'line 1\nline 2\nline 3\n' > shared.txt |
| 113 | printf 'SECRET=alice\n' > .env |
| 114 | check "init" as laptop "$VCS" init |
| 115 | check "first version" as laptop "$VCS" land -m "First version" |
| 116 | printf 'line 1\nline 2\nline 3\nline 4\n' > shared.txt |
| 117 | check "second version" as laptop "$VCS" land -m "Second version" |
| 118 | check "publish" as laptop "$VCS" publish "$HOST/acme/shop" |
| 119 | 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 |
| 120 | add_member acme/shop bob member |
| 121 | add_member acme/shop carol reader |
| 122 |
| 123 | # Bob gets it. |
| 124 | cd "$WORK" |
| 125 | check "bob gets" as bob "$VCS" get "$HOST/acme/shop" bobshop |
| 126 | check "bob has the files" test "$(tail -1 bobshop/shared.txt)" = "line 4" |
| 127 | check "bob doesn't get alice's .env" test ! -e bobshop/.env |
| 128 |
| 129 | # Parallel landings from two people. |
| 130 | printf 'alice\n' > shop/alice.txt |
| 131 | printf 'bob\n' > bobshop/bob.txt |
| 132 | check "alice lands" as laptop "$VCS" -C shop land -m "Alice's file" |
| 133 | check "bob lands without syncing" as bob "$VCS" -C bobshop land -m "Bob's file" |
| 134 | check "main is 4" test "$(as bob "$VCS" --json -C bobshop log | jq_ 'd["versions"][0]["n"]')" = 4 |
| 135 |
| 136 | # A conflict between them. |
| 137 | as laptop "$VCS" -C shop sync >/dev/null 2>&1 |
| 138 | as bob "$VCS" -C bobshop sync >/dev/null 2>&1 |
| 139 | sed -i 's/^line 2$/line two (alice)/' shop/shared.txt |
| 140 | sed -i 's/^line 2$/line two (bob)/' bobshop/shared.txt |
| 141 | check "alice lands line 2" as laptop "$VCS" -C shop land -m "Alice's line 2" |
| 142 | check "bob conflicts (exit 3)" code 3 as bob "$VCS" -C bobshop land -m "Bob's line 2" |
| 143 | check "bob syncs (exit 3)" code 3 as bob "$VCS" -C bobshop sync |
| 144 | check "markers" grep -q '<<<<<<< main' bobshop/shared.txt |
| 145 | sed -i '/^<<<<<<< /d; /^=======$/d; /^>>>>>>> /d; /line two (alice)/d' bobshop/shared.txt |
| 146 | check "bob lands after fixing" as bob "$VCS" -C bobshop land |
| 147 |
| 148 | # A reader's change waits for someone who can land it. |
| 149 | check "carol gets" as carol "$VCS" get "$HOST/acme/shop" carolshop |
| 150 | printf 'fix from carol\n' > carolshop/carol.txt |
| 151 | check "carol's land waits (exit 5)" code 5 as carol "$VCS" -C carolshop land -m "Carol's fix" |
| 152 | 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]') |
| 153 | check "alice sees the waiting change" sh -c "VCS_HOME=$WORK/laptop $VCS -C $WORK/shop log --changes | grep -q waiting" |
| 154 | as laptop "$VCS" -C shop land "$CAROL_CHANGE" >"$WORK/carol-land.out" 2>&1; cat "$WORK/carol-land.out" >&2 |
| 155 | check "alice lands carol's change" grep -q landed "$WORK/carol-land.out" |
| 156 | check "carol is the author" test "$(as laptop "$VCS" --json -C shop log -n 1 | jq_ 'd["versions"][0]["author"].split(" <")[0]')" = Carol |
| 157 |
| 158 | # Alice moves from her laptop to a cloud machine mid-change. |
| 159 | as laptop "$VCS" -C shop sync >/dev/null 2>&1 |
| 160 | printf 'started on the laptop\n' > shop/feature.txt |
| 161 | check "describe on laptop" as laptop "$VCS" -C shop describe "Feature" |
| 162 | FEATURE=$(as laptop "$VCS" --json -C shop status | jq_ 'd["change"]["id"]') |
| 163 | check "cloud picks it up" as cloud "$VCS" get "$HOST/acme/shop" cloudshop --change "$FEATURE" |
| 164 | check "the edit came along" test "$(cat cloudshop/feature.txt)" = "started on the laptop" |
| 165 | check ".env came along" test "$(cat cloudshop/.env)" = "SECRET=alice" |
| 166 | printf 'continued in the cloud\n' >> cloudshop/feature.txt |
| 167 | check "cloud saves" as cloud "$VCS" -C cloudshop status |
| 168 | printf 'late laptop edit\n' > shop/late.txt |
| 169 | OUT=$(as laptop "$VCS" --json -C shop status 2>/dev/null) |
| 170 | check "laptop's late edit is rescued into a new change" sh -c "echo '$OUT' | grep -q 'Rescued from'" |
| 171 | check "cloud lands the feature" as cloud "$VCS" -C cloudshop land |
| 172 | check "feature has both edits" test "$(as bob "$VCS" -C bobshop sync >/dev/null 2>&1; cat bobshop/feature.txt | wc -l)" = 2 |
| 173 |
| 174 | # Sharing a change. |
| 175 | printf 'draft\n' > bobshop/draft.txt |
| 176 | check "bob describes" as bob "$VCS" -C bobshop describe "Draft for Alice" |
| 177 | DRAFT=$(as bob "$VCS" --json -C bobshop status | jq_ 'd["change"]["id"]') |
| 178 | check "bob shares it" as bob "$VCS" -C bobshop access "$DRAFT" @alice |
| 179 | check "alice sees it" sh -c "VCS_HOME=$WORK/laptop $VCS -C $WORK/shop log --changes | grep -q 'Draft for Alice'" |
| 180 |
| 181 | # Private paths: only Alice sees billing/. (First drop the rescued change, |
| 182 | # which still has the takeover's conflict in it.) |
| 183 | as laptop "$VCS" -C shop drop >/dev/null 2>&1 |
| 184 | as laptop "$VCS" -C shop sync >/dev/null 2>&1 |
| 185 | mkdir -p shop/billing && printf 'rate 10\n' > shop/billing/rates.txt |
| 186 | check "alice lands billing" as laptop "$VCS" -C shop land -m "Billing rates" |
| 187 | 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" |
| 188 | check "alice makes billing/ hers" as laptop "$VCS" -C shop access billing @alice |
| 189 | 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" |
| 190 | printf 'bob was here\n' >> bobshop/README.md 2>/dev/null || printf 'bob was here\n' > bobshop/README.md |
| 191 | check "bob lands outside billing" as bob "$VCS" -C bobshop land -m "Bob's readme" |
| 192 | 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" |
| 193 | mkdir -p bobshop/billing && printf 'sneaky\n' > bobshop/billing/new.txt |
| 194 | check "bob can't write into billing (exit 7)" code 7 as bob "$VCS" -C bobshop land -m "Sneaky" |
| 195 | rm -rf bobshop/billing |
| 196 | as bob "$VCS" -C bobshop drop >/dev/null 2>&1 |
| 197 | printf 'rate 12\n' > shop/billing/rates.txt |
| 198 | check "alice changes billing" as laptop "$VCS" -C shop land -m "New billing rates" |
| 199 | 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'" |
| 200 | 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" |
| 201 | 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" |
| 202 | # Naming the full tree in a change of his own doesn't get bob in either. |
| 203 | FULL=$(curl -s -H "authorization: Bearer $ALICE" "http://$HOST/v1/projects/acme/shop" | jq_ 'd["main"]["tree"]') |
| 204 | CLAIM="{\"change\":{\"id\":\"bcdfghjkmnpq\",\"base_tree\":\"$FULL\",\"head_tree\":\"$FULL\"},\"workspace\":\"w1\"}" |
| 205 | check "bob can't claim the full tree" sh -c "curl -s -o /dev/null -w '%{http_code}' -X PUT -H 'authorization: Bearer $BOB' -H 'content-type: application/json' -d '$CLAIM' http://$HOST/v1/projects/acme/shop/changes/bcdfghjkmnpq | grep -q 403" |
| 206 | check "and still can't read it" sh -c "! curl -sf -H 'authorization: Bearer $BOB' http://$HOST/v1/projects/acme/shop/trees/$FULL >/dev/null" |
| 207 |
| 208 | # Undo a hosted land. |
| 209 | as cloud "$VCS" -C cloudshop sync >/dev/null 2>&1 |
| 210 | printf 'oops\n' > cloudshop/oops.txt |
| 211 | check "cloud lands oops" as cloud "$VCS" -C cloudshop land -m "Oops" |
| 212 | check "cloud undoes it" as cloud "$VCS" -C cloudshop undo |
| 213 | check "main took it back" sh -c "VCS_HOME=$WORK/bob $VCS -C $WORK/bobshop log -n 1 | grep -q 'Take back'" |
| 214 |
| 215 | # Offline: saves stay local, land says offline, later it all goes up. |
| 216 | stop_server |
| 217 | printf 'written offline\n' > cloudshop/offline.txt |
| 218 | check "status works offline" as cloud "$VCS" -C cloudshop status |
| 219 | check "land says offline (exit 6)" code 6 as cloud "$VCS" -C cloudshop land -m "Offline work" |
| 220 | start_server |
| 221 | check "land works once back" as cloud "$VCS" -C cloudshop land -m "Offline work" |
| 222 |
| 223 | # 32 agents land at once on one main, none refused. |
| 224 | BEFORE=$(as bob "$VCS" --json -C bobshop log -n 1 | jq_ 'd["versions"][0]["n"]') |
| 225 | for i in $(seq 1 32); do |
| 226 | as bob "$VCS" get "$HOST/acme/shop" "agent$i" >/dev/null 2>&1 |
| 227 | printf 'agent %s\n' "$i" > "agent$i/agent-$i.txt" |
| 228 | done |
| 229 | pids=() |
| 230 | for i in $(seq 1 32); do |
| 231 | (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") & |
| 232 | pids+=($!) |
| 233 | done |
| 234 | wait "${pids[@]}" |
| 235 | OK=$(grep -l '^0$' "$WORK"/land*.code | wc -l) |
| 236 | check "all 32 landed" test "$OK" = 32 |
| 237 | for i in $(seq 1 32); do |
| 238 | [ "$(cat "$WORK/land$i.code")" = 0 ] || { echo "agent $i:"; cat "$WORK/land$i.out"; } |
| 239 | done |
| 240 | AFTER=$(as bob "$VCS" --json -C bobshop log -n 1 | jq_ 'd["versions"][0]["n"]') |
| 241 | check "main moved by 32" test "$((AFTER - BEFORE))" = 32 |
| 242 | # An agent that landed in the middle still sees every version, in order. |
| 243 | check "log has no gaps" sh -c "VCS_HOME=$WORK/bob '$VCS' --json -C agent1 log -n 40 | python3 -c 'import json,sys; n=[v[\"n\"] for v in json.load(sys.stdin)[\"versions\"]]; assert n == list(range(n[0], n[0]-len(n), -1)), n'" |
| 244 |
| 245 | [ "$fail" != 0 ] && [ -f "$WORK/server.log" ] && { echo "server log:"; tail -n 60 "$WORK/server.log"; } |
| 246 | echo "hosted: $pass passed, $fail failed" |
| 247 | [ "$fail" = 0 ] |
| 248 |