195 lines
9.5 KiB
Bash
Executable File
195 lines
9.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Compile normally; execute unit tests away from real wallets, service buses,
|
|
# container storage, processes and networking. Never silently fall back to host.
|
|
set -euo pipefail
|
|
SCRIPT_REPO=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
REPO=${ARCHY_TEST_REPO:-$SCRIPT_REPO}
|
|
REPO=$(cd "$REPO" && pwd)
|
|
if [[ ${ARCHY_TEST_PACKAGE:-archipelago} == gashboard ]]; then
|
|
[[ $# == 0 ]] || { echo 'Gashboard isolation accepts no command arguments' >&2; exit 2; }
|
|
[[ -z ${ARCHY_TEST_TERMINAL_FIXTURE:-}${ARCHY_TEST_CLOUD_FIXTURE_CONTAINER:-} &&
|
|
${ARCHY_TEST_C01_STARTUP:-0} == 0 && ${ARCHY_TEST_CLOUD_COMPILE_ONLY:-0} == 0 &&
|
|
${ARCHY_TEST_RADIO_OWNER:-0} == 0 ]] || {
|
|
echo 'Gashboard isolation cannot be combined with another fixture mode' >&2; exit 2;
|
|
}
|
|
exec bash "$SCRIPT_REPO/scripts/test-gashboard-isolated.sh" "$REPO"
|
|
fi
|
|
if [[ ${ARCHY_TEST_C01_STARTUP:-0} == 1 && ${ARCHY_TEST_ISOLATOR:-systemd} != systemd ]]; then
|
|
echo 'C01 startup qualification requires the systemd fixture namespace' >&2; exit 2
|
|
fi
|
|
if [[ -n ${ARCHY_TEST_TERMINAL_FIXTURE:-} ]]; then
|
|
exec bash "$SCRIPT_REPO/scripts/test-terminal-backend-isolated.sh" "$@"
|
|
fi
|
|
if [[ -n ${ARCHY_TEST_CLOUD_FIXTURE_CONTAINER:-} || ${ARCHY_TEST_CLOUD_COMPILE_ONLY:-0} == 1 ]]; then
|
|
[[ ${ARCHY_TEST_ISOLATOR:-systemd} == podman ]] || {
|
|
echo 'Cloud fixture tests require Podman-isolated compilation; host compilation is forbidden' >&2; exit 2;
|
|
}
|
|
exec bash "$SCRIPT_REPO/scripts/test-cloud-backend-isolated.sh" "$@"
|
|
fi
|
|
case "${ARCHY_TEST_PACKAGE:-archipelago}" in
|
|
archipelago) test_target=(-p archipelago --bin archipelago) ;;
|
|
archipelago-publishing-tests) test_target=(-p archipelago-publishing-tests --lib) ;;
|
|
archipelago-container) test_target=(-p archipelago-container --lib) ;;
|
|
archipelago-security) test_target=(-p archipelago-security --lib) ;;
|
|
*) echo 'Unsupported isolated test package' >&2; exit 2 ;;
|
|
esac
|
|
|
|
if [[ ${ARCHY_TEST_ISOLATOR:-systemd} == podman ]]; then
|
|
command -v podman >/dev/null
|
|
image=${ARCHY_TEST_IMAGE:?ARCHY_TEST_IMAGE is required for podman isolation}
|
|
cargo_home=${ARCHY_TEST_CARGO_HOME:?ARCHY_TEST_CARGO_HOME is required for podman isolation}
|
|
cargo_home=$(mkdir -p "$cargo_home" && cd "$cargo_home" && pwd)
|
|
artifacts=$(mktemp -d)
|
|
trap 'rm -rf -- "$artifacts"' EXIT
|
|
install -d "$artifacts/runtime/archipelago" "$artifacts/runtime/containers" \
|
|
"$artifacts/runtime/tmp"
|
|
|
|
podman run --rm \
|
|
--cpus="${ARCHY_TEST_CPUS:-4}" --memory="${ARCHY_TEST_MEMORY:-4g}" --pids-limit=2048 \
|
|
--cap-drop=all --security-opt=no-new-privileges --read-only \
|
|
--tmpfs /tmp:rw,size=512m --tmpfs /root:rw,size=512m \
|
|
--network=pasta --env CARGO_HOME=/cargo-home \
|
|
--volume "$cargo_home:/cargo-home:rw,Z" \
|
|
--volume "$REPO:/workspace:rw,Z" --volume "$artifacts:/artifacts:rw,Z" \
|
|
--workdir /workspace \
|
|
"$image" \
|
|
cargo test --manifest-path core/Cargo.toml "${test_target[@]}" \
|
|
--locked --no-run --message-format=json \
|
|
--config 'profile.test.package.archipelago.opt-level=0' \
|
|
--config 'profile.test.package.archipelago.debug=0' \
|
|
>"$artifacts/metadata"
|
|
|
|
executable=$(python3 - "$artifacts/metadata" <<'PY'
|
|
import json,sys
|
|
found=[]
|
|
for line in open(sys.argv[1]):
|
|
try: item=json.loads(line)
|
|
except json.JSONDecodeError: continue
|
|
if item.get('reason')=='compiler-artifact' and item.get('profile',{}).get('test') and item.get('executable'):
|
|
found.append(item['executable'])
|
|
assert len(found)==1, f'Expected one unit test executable, got {len(found)}'
|
|
print(found[0])
|
|
PY
|
|
)
|
|
case "$executable" in
|
|
/workspace/*) ;;
|
|
*) echo 'Compiled test executable escaped the workspace' >&2; exit 1 ;;
|
|
esac
|
|
|
|
podman run --rm \
|
|
--cpus="${ARCHY_TEST_CPUS:-4}" --memory="${ARCHY_TEST_MEMORY:-4g}" --pids-limit=1024 \
|
|
--cap-drop=all --cap-add=chown --cap-add=fowner --cap-add=setuid --cap-add=setgid \
|
|
--security-opt=no-new-privileges --read-only \
|
|
--tmpfs /run:rw,size=64m --tmpfs /root:rw,size=64m \
|
|
--tmpfs /var/lib/tor:rw,size=64m --tmpfs /tmp:rw,size=2g --network=none \
|
|
--tmpfs /var/lib/archipelago-public-updates:rw --tmpfs /var/lib/archipelago-public-transactions:rw \
|
|
--volume "$artifacts/runtime/archipelago:/var/lib/archipelago:rw,Z" \
|
|
--volume "$artifacts/runtime/containers:/var/lib/containers:rw,Z" \
|
|
--volume "$REPO:/workspace:ro,Z" --workdir /workspace/core \
|
|
--env ARCHY_TEST_ISOLATED=1 \
|
|
--env ARCHY_TOR_OWNER_FIXTURE=1 \
|
|
"$image" "$executable" --test-threads="${ARCHY_TEST_THREADS:-4}" "$@"
|
|
exit
|
|
fi
|
|
|
|
[[ ${ARCHY_TEST_ISOLATOR:-systemd} == systemd ]] || {
|
|
echo 'ARCHY_TEST_ISOLATOR must be systemd or podman' >&2
|
|
exit 2
|
|
}
|
|
# Fresh tmpfs mounts isolate both data and free-space accounting. PrivateTmp=yes
|
|
# would overlay these with private directories on the potentially full host /tmp.
|
|
test_filesystems='/run:rw /var/lib/archipelago:rw /var/lib/containers:rw /root:rw /var/lib/tor:rw,size=64m /tmp:rw,size=2G,mode=1777 /var/tmp:rw,size=256M,mode=1777'
|
|
test_filesystems+=' /var/lib/archipelago-public-updates:rw /var/lib/archipelago-public-transactions:rw'
|
|
startup_environment=()
|
|
if [[ ${ARCHY_TEST_LOGIN_PASSWORD:-0} == 1 ]]; then
|
|
# Password owner qualification gets empty account files, never host shadow.
|
|
test_filesystems+=' /etc:rw,size=16M,mode=755'
|
|
startup_environment+=(--setenv=ARCHY_TEST_LOGIN_PASSWORD=1)
|
|
fi
|
|
if [[ ${ARCHY_TEST_C01_STARTUP:-0} == 1 ]]; then
|
|
[[ -n ${ARCHY_TEST_C01_BINARY:-} && -x $ARCHY_TEST_C01_BINARY ]] || {
|
|
echo 'C01 startup qualification requires a freshly compiled non-test binary' >&2; exit 2;
|
|
}
|
|
startup_binary=$(realpath -- "$ARCHY_TEST_C01_BINARY")
|
|
# Hide installed configuration/payloads as well as the ordinary test data.
|
|
# These are disposable fixture mounts, never C01 installation authority.
|
|
test_filesystems+=' /opt/archipelago:rw /etc/archipelago:rw'
|
|
startup_environment+=(--setenv=ARCHY_TEST_C01_STARTUP=1 "--setenv=ARCHY_TEST_C01_BINARY=$startup_binary")
|
|
fi
|
|
command -v systemd-run >/dev/null
|
|
command -v unshare >/dev/null
|
|
command -v setpriv >/dev/null
|
|
sudo -n true || { echo 'Isolated backend tests require noninteractive sudo for systemd namespaces.' >&2; exit 1; }
|
|
metadata=$(mktemp)
|
|
trap 'rm -f "$metadata"' EXIT
|
|
if ! cargo test --manifest-path "$REPO/core/Cargo.toml" "${test_target[@]}" \
|
|
--locked --no-run --message-format=json --config 'profile.test.package.archipelago.opt-level=0' --config 'profile.test.package.archipelago.debug=0' > "$metadata"; then
|
|
python3 - "$metadata" <<'PYDIAG'
|
|
import json,sys
|
|
for line in open(sys.argv[1]):
|
|
try: item=json.loads(line)
|
|
except json.JSONDecodeError: continue
|
|
rendered=item.get('message',{}).get('rendered') if item.get('reason')=='compiler-message' else None
|
|
if rendered: print(rendered,file=sys.stderr,end='')
|
|
PYDIAG
|
|
exit 1
|
|
fi
|
|
executable=$(python3 - "$metadata" <<'PY'
|
|
import json,sys
|
|
found=[]
|
|
for line in open(sys.argv[1]):
|
|
try: item=json.loads(line)
|
|
except json.JSONDecodeError: continue
|
|
if item.get('reason')=='compiler-artifact' and item.get('profile',{}).get('test') and item.get('executable'):
|
|
found.append(item['executable'])
|
|
assert len(found)==1, f'Expected one unit test executable, got {len(found)}'
|
|
print(found[0])
|
|
PY
|
|
)
|
|
[[ -x "$executable" ]]
|
|
radio_fixture=()
|
|
test_command=("$executable" --test-threads="${ARCHY_TEST_THREADS:-4}" "$@")
|
|
if [[ ${ARCHY_TEST_RADIO_OWNER:-0} == 1 ]]; then
|
|
test_filesystems+=' /var/lib/archipelago-radio-work:rw,size=256M,mode=700'
|
|
radio_fixture=(--property=ReadWritePaths=/sys/fs/cgroup
|
|
"--property=BindReadOnlyPaths=$executable:/opt/radio-test-client"
|
|
--setenv=ARCHY_RADIO_FIXTURE=1 --setenv=ARCHY_RADIO_CLIENT=/opt/radio-test-client)
|
|
test_command=(/usr/bin/python3 -B "$REPO/tests/regression/radio-owner.py")
|
|
fi
|
|
if [[ ${ARCHY_TEST_PACKAGE:-archipelago} == archipelago && ${ARCHY_TEST_RADIO_OWNER:-0} != 1 ]]; then
|
|
# The compiled binary lives below a private Reports checkout. Several root
|
|
# fixture tests drop to an unprivileged UID and re-exec current_exe; that UID
|
|
# must be able to traverse the binary path. Copy once into the unit's fresh
|
|
# isolated /tmp, never expose the source checkout or host /tmp to that UID.
|
|
test_command=(/usr/bin/bash -euc '
|
|
source=$1; shift
|
|
directory=$(mktemp -d /tmp/archy-isolated-test.XXXXXXXX)
|
|
chmod 0755 "$directory"
|
|
install -m 0555 -- "$source" "$directory/archipelago-tests"
|
|
cmp -s -- "$source" "$directory/archipelago-tests"
|
|
sha256sum -- "$source" "$directory/archipelago-tests"
|
|
exec "$directory/archipelago-tests" "$@"
|
|
' archy-isolated-test "$executable" --test-threads="${ARCHY_TEST_THREADS:-4}" "$@")
|
|
fi
|
|
unit="archy-isolated-tests-$(date +%s)-$$"
|
|
execution_profile=()
|
|
if [[ ${ARCHY_TEST_PACKAGE:-archipelago} == archipelago ]]; then
|
|
execution_profile=(python3 -B "$SCRIPT_REPO/scripts/tests/with_exec_profile.py" "$REPO" --)
|
|
fi
|
|
"${execution_profile[@]}" sudo -n systemd-run --unit="$unit" --wait --pipe --collect \
|
|
--property=CPUQuota=200% --property=MemoryMax=8G --property=MemorySwapMax=0 \
|
|
--property="WorkingDirectory=$REPO/core" \
|
|
--property="BindReadOnlyPaths=$REPO" \
|
|
--property=PrivateNetwork=yes --property=PrivateTmp=no --property=PrivateDevices=yes \
|
|
--property=ProtectSystem=strict --property=ProtectHome=read-only \
|
|
--property=NoNewPrivileges=yes \
|
|
--property="TemporaryFileSystem=$test_filesystems" \
|
|
--setenv=ARCHY_TEST_ISOLATED=1 \
|
|
--setenv=ARCHY_TOR_OWNER_FIXTURE=1 \
|
|
"--setenv=RUST_MIN_STACK=${RUST_MIN_STACK:-16777216}" \
|
|
"${startup_environment[@]}" \
|
|
"${radio_fixture[@]}" \
|
|
/usr/bin/unshare --pid --fork --mount-proc --kill-child \
|
|
/usr/bin/setpriv --bounding-set=-all,+chown,+dac_override,+fowner,+setuid,+setgid,+kill \
|
|
"${test_command[@]}"
|