Monorepo for Tangled tangled.org
9

Configure Feed

Select the types of activity you want to include in your feed.

1#!/usr/bin/env bash 2set -euo pipefail 3# note: needs `sudo modprobe vhost_vsock`! 4 5log() { 6 printf "\n\033[1;36m>>> %s\033[0m\n" "$*" 7} 8 9strip_ansi() { 10 local esc 11 esc=$(printf '\033') 12 sed -E "s/${esc}\[[0-9;]*[a-zA-Z]//g; s/${esc}\([a-zA-Z]//g" "$@" 13} 14 15declare -a TEST_NAMES=() 16declare -a TEST_STATUSES=() 17declare -a TEST_TIMES=() 18SKIP_TEST_RC=200 19 20get_time_ms() { 21 local t="${EPOCHREALTIME:-}" 22 if [[ "$t" == *.* ]]; then 23 local secs="${t%.*}" 24 local subs="${t#*.}" 25 subs="${subs:0:3}" 26 while [ "${#subs}" -lt 3 ]; do 27 subs="${subs}0" 28 done 29 echo "${secs}${subs}" 30 else 31 echo "$(date +%s)000" 32 fi 33} 34 35format_duration() { 36 local ms=$1 37 local secs=$((ms / 1000)) 38 local rem=$((ms % 1000)) 39 printf "%d.%03ds" "$secs" "$rem" 40} 41 42print_summary() { 43 if [ "${#TEST_NAMES[@]}" -eq 0 ]; then 44 return 45 fi 46 printf "\n" 47 log "test summary" 48 echo "=========================================" 49 local passed_count=0 50 local skipped_count=0 51 local failed_count=0 52 local total_time=0 53 for i in "${!TEST_NAMES[@]}"; do 54 local name="${TEST_NAMES[$i]}" 55 local status="${TEST_STATUSES[$i]}" 56 local duration_ms="${TEST_TIMES[$i]}" 57 local duration_str 58 duration_str=$(format_duration "$duration_ms") 59 60 local status_color="\033[0;32m" 61 if [ "$status" = "Failed" ]; then 62 status_color="\033[0;31m" 63 failed_count=$((failed_count + 1)) 64 elif [ "$status" = "Skipped" ]; then 65 status_color="\033[0;33m" 66 skipped_count=$((skipped_count + 1)) 67 else 68 passed_count=$((passed_count + 1)) 69 fi 70 total_time=$((total_time + duration_ms)) 71 72 printf " %-30s %b%-8b\033[0m %s\n" "$name" "$status_color" "$status" "$duration_str" 73 done 74 echo "-----------------------------------------" 75 local total_tests="${#TEST_NAMES[@]}" 76 local total_time_str 77 total_time_str=$(format_duration "$total_time") 78 printf " total: %d tests, %d passed, %d skipped, %d failed\n" "$total_tests" "$passed_count" "$skipped_count" "$failed_count" 79 printf " total execution time: %s\n" "$total_time_str" 80 echo "=========================================" 81} 82 83skip_test() { 84 echo "skipped: $*" 85 return "$SKIP_TEST_RC" 86} 87 88host_is_nixos() { 89 [ -e /etc/NIXOS ] && return 0 90 [ -r /etc/os-release ] && grep -q '^ID=nixos$' /etc/os-release 91} 92 93JOBS="${JOBS:-4}" 94while [[ $# -gt 0 ]]; do 95 case "$1" in 96 -j | --jobs) 97 JOBS="$2" 98 shift 2 99 ;; 100 --jobs=*) 101 JOBS="${1#*=}" 102 shift 103 ;; 104 --only) 105 TEST_ONLY="$2" 106 shift 2 107 ;; 108 --only=*) 109 TEST_ONLY="${1#*=}" 110 shift 111 ;; 112 *) 113 echo "unknown argument: $1" >&2 114 echo "usage: $0 [-j N|--jobs N] [--only TEST]" >&2 115 exit 1 116 ;; 117 esac 118done 119if ! [[ "$JOBS" =~ ^[0-9]+$ ]] || [ "$JOBS" -lt 1 ]; then 120 echo "error: --jobs must be a positive integer (got '$JOBS')" >&2 121 exit 1 122fi 123 124pick_free_port() { 125 local port 126 for _ in $(seq 1 50); do 127 port=$(((RANDOM % 16384) + 20000)) 128 if ! (exec 3<>"/dev/tcp/127.0.0.1/$port") 2>/dev/null; then 129 echo "$port" 130 return 0 131 fi 132 done 133 echo "error: could not find a free port for the cache" >&2 134 return 1 135} 136 137SUCCESS=0 138rm -rf /tmp/test-spindle-microvm-logs 139 140log "setup local cache & temp environment" 141TEMP_DIR=$(mktemp -d -t test-spindle-microvm-XXXXXX) 142 143if [ ! -e /dev/vsock ]; then 144 echo "error: /dev/vsock is missing; run sudo modprobe vhost_vsock" >&2 145 exit 1 146fi 147 148log "build spindle & microvm image tarball" 149nix develop --command go build -o spindle/spindle-microvm-run ./cmd/spindle-microvm-run 150TARBALL_PATH=$(nix build .#spindle-nixos-image-tarball --no-link --print-out-paths) 151mkdir -p "$TEMP_DIR/image" 152tar -C "$TEMP_DIR/image" -xzf "$TARBALL_PATH" 153IMAGE_SPEC_JSON="$TEMP_DIR/image/spec.json" 154 155log "build alpine microvm image tarball" 156ALPINE_TARBALL_PATH=$(nix build .#spindle-alpine-image-tarball --no-link --print-out-paths) 157mkdir -p "$TEMP_DIR/alpine-image" 158tar -C "$TEMP_DIR/alpine-image" -xzf "$ALPINE_TARBALL_PATH" 159ALPINE_IMAGE_SPEC_JSON="$TEMP_DIR/alpine-image/spec.json" 160 161kill_temp_dir_procs() { 162 if [ -f "$TEMP_DIR/ncps.pid" ]; then 163 kill "$(cat "$TEMP_DIR/ncps.pid")" 2>/dev/null || true 164 fi 165 pkill -TERM -f "$TEMP_DIR" 2>/dev/null || true 166 local i 167 for i in $(seq 1 20); do 168 pgrep -f "$TEMP_DIR" >/dev/null 2>&1 || break 169 sleep 0.25 170 done 171 pkill -KILL -f "$TEMP_DIR" 2>/dev/null || true 172} 173 174collect_logs() { 175 echo "test failed. copying logs to /tmp/test-spindle-microvm-logs" 176 mkdir -p /tmp/test-spindle-microvm-logs 177 local f 178 for f in "$TEMP_DIR"/*; do 179 [ -f "$f" ] && cp "$f" /tmp/test-spindle-microvm-logs/ 180 done 181 local work logf 182 for work in "$TEMP_DIR"/work-*; do 183 [ -d "$work" ] || continue 184 for logf in "$work"/*.log; do 185 [ -f "$logf" ] || continue 186 strip_ansi "$logf" > "/tmp/test-spindle-microvm-logs/$(basename "$work")-$(basename "$logf")" 187 done 188 done 189} 190 191CLEANED=0 192cleanup() { 193 [ "$CLEANED" -eq 1 ] && return 194 CLEANED=1 195 196 print_summary 197 log "cleaning up..." 198 199 local jobs_pids 200 jobs_pids=$(jobs -p) 201 [ -n "$jobs_pids" ] && kill $jobs_pids 2>/dev/null || true 202 203 kill_temp_dir_procs 204 205 [ "$SUCCESS" -ne 1 ] && collect_logs 206 207 chmod -R +w "$TEMP_DIR" 2>/dev/null || true 208 rm -rf "$TEMP_DIR" 209 echo "done" 210} 211trap cleanup EXIT 212# route signals through the EXIT trap so an interrupt still tears down VMs. 213trap 'exit 130' INT 214trap 'exit 143' TERM 215 216CACHE_PORT=$(pick_free_port) 217./spindle/engines/microvm/start-test-cache.sh "$TEMP_DIR" "$CACHE_PORT" 218source "$TEMP_DIR/env.sh" 219 220run_vm() { 221 local name="" 222 local timeout="60s" 223 local upload=0 224 local upload_url="" 225 local activate="" 226 local no_cache=0 227 local db="" 228 local spec="$IMAGE_SPEC_JSON" 229 230 while [[ $# -gt 0 ]]; do 231 case "$1" in 232 --spec) 233 spec="$2" 234 shift 2 235 ;; 236 --name) 237 name="$2" 238 shift 2 239 ;; 240 --timeout) 241 timeout="$2" 242 shift 2 243 ;; 244 --upload) 245 upload=1 246 shift 247 ;; 248 --upload-url) 249 upload=1 250 upload_url="$2" 251 shift 2 252 ;; 253 --activate) 254 activate="$2" 255 shift 2 256 ;; 257 --no-cache) 258 no_cache=1 259 shift 260 ;; 261 --db) 262 db="$2" 263 shift 2 264 ;; 265 --) 266 shift 267 break 268 ;; 269 *) 270 echo "unknown argument: $1" >&2 271 exit 1 272 ;; 273 esac 274 done 275 276 local work_dir="$TEMP_DIR/work-${name}" 277 mkdir -p "$work_dir" 278 279 local args=( 280 --image-spec "$spec" 281 --work-dir "$work_dir" 282 --exec-timeout "$timeout" 283 --port "${SPINDLE_TEST_VSOCK_PORT:-10240}" 284 --memory-mib 2049 285 ) 286 287 if [ "$no_cache" -eq 0 ]; then 288 args+=( 289 --cache-read-url "$CACHE_URL" 290 --cache-trusted-public-key "$CACHE_PUBKEY" 291 ) 292 fi 293 294 if [ "$upload" -eq 1 ]; then 295 if [ -z "$upload_url" ]; then 296 upload_url="$CACHE_UPLOAD_URL?secret-key=$CACHE_SECRET_KEY_PATH" 297 fi 298 args+=( 299 --cache-upload-url "$upload_url" 300 ) 301 fi 302 303 if [ -n "$activate" ]; then 304 args+=( 305 --activate-config "$activate" 306 ) 307 fi 308 309 if [ -n "$db" ]; then 310 args+=( 311 --db "$db" 312 ) 313 fi 314 315 local out 316 if ! out=$(spindle/spindle-microvm-run "${args[@]}" -- "$@" 2>&1); then 317 echo "$out" | strip_ansi >&2 318 strip_ansi "$work_dir/serial.log" >&2 319 strip_ansi "$work_dir/qemu.log" >&2 320 return 1 321 fi 322 echo "$out" 323} 324 325run_test_job() { 326 local name="$1" 327 local func="$2" 328 local port="$3" 329 export SPINDLE_TEST_VSOCK_PORT="$port" 330 331 local logfile="$TEMP_DIR/test-${name}.log" 332 local start 333 start=$(get_time_ms) 334 log "[$name] start (vsock port $port)" 335 336 local status="Passed" 337 if "$func" > "$logfile" 2>&1; then 338 status="Passed" 339 else 340 local rc=$? 341 if [ "$rc" -eq "$SKIP_TEST_RC" ]; then 342 status="Skipped" 343 else 344 status="Failed" 345 fi 346 fi 347 348 local duration_ms=$(($(get_time_ms) - start)) 349 printf '%s\t%s\n' "$status" "$duration_ms" > "$TEMP_DIR/test-${name}.status" 350 351 local duration_str 352 duration_str=$(format_duration "$duration_ms") 353 if [ "$status" = "Failed" ]; then 354 printf "\n\033[0;31m>>> [%s] FAILED (%s)\033[0m\n" "$name" "$duration_str" 355 strip_ansi "$logfile" || true 356 elif [ "$status" = "Skipped" ]; then 357 printf "\n\033[0;33m>>> [%s] skipped (%s)\033[0m\n" "$name" "$duration_str" 358 strip_ansi "$logfile" || true 359 else 360 printf "\n\033[0;32m>>> [%s] passed (%s)\033[0m\n" "$name" "$duration_str" 361 fi 362} 363 364# schedules every selected test across at most $JOBS concurrent VMs (each on its 365# own vsock port), then aggregates the per-test status files into the summary 366# arrays. returns 1 if any test failed, 0 otherwise. 367run_tests() { 368 local base_port=10240 369 local idx=0 370 local running=0 371 372 for func in "${TESTS[@]}"; do 373 local name="${func#test_}" 374 name="${name//_/-}" 375 if [ -n "${TEST_ONLY:-}" ] && [ "${TEST_ONLY}" != "$name" ]; then 376 continue 377 fi 378 379 run_test_job "$name" "$func" "$((base_port + idx))" & 380 idx=$((idx + 1)) 381 running=$((running + 1)) 382 383 if [ "$running" -ge "$JOBS" ]; then 384 wait -n || true 385 running=$((running - 1)) 386 fi 387 done 388 wait 389 390 local failed=0 391 for func in "${TESTS[@]}"; do 392 local name="${func#test_}" 393 name="${name//_/-}" 394 if [ -n "${TEST_ONLY:-}" ] && [ "${TEST_ONLY}" != "$name" ]; then 395 continue 396 fi 397 398 local statusfile="$TEMP_DIR/test-${name}.status" 399 if [ ! -f "$statusfile" ]; then 400 TEST_NAMES+=("$name") 401 TEST_STATUSES+=("Failed") 402 TEST_TIMES+=(0) 403 failed=1 404 continue 405 fi 406 407 local status duration_ms 408 IFS=$'\t' read -r status duration_ms < "$statusfile" 409 TEST_NAMES+=("$name") 410 TEST_STATUSES+=("$status") 411 TEST_TIMES+=("$duration_ms") 412 if [ "$status" = "Failed" ]; then 413 failed=1 414 fi 415 done 416 417 return "$failed" 418} 419 420test_realize() { 421 local test_store_path 422 test_store_path=$(nix-build -E 'with import <nixpkgs> {}; writeText "test-file" "hello from cache"' --no-out-link) 423 nix copy --to "$CACHE_UPLOAD_URL?secret-key=$CACHE_SECRET_KEY_PATH" "$test_store_path" 424 425 local out 426 out=$(run_vm --name "realize" --timeout "60s" -- /run/current-system/sw/bin/bash -lc ' 427set -euo pipefail 428store_path=$1 429cache_url=$(sed -n "s/^extra-substituters = //p" /run/spindle/nix.conf) 430cache_url=${cache_url%% *} 431if [ -z "$cache_url" ]; then 432 echo "error: cache URL not found in /run/spindle/nix.conf" >&2 433 exit 1 434fi 435 436http_version=$(/run/current-system/sw/bin/curl --http2-prior-knowledge -fsS -o /dev/null -w "%{http_version}" "$cache_url/nix-cache-info") 437echo "http_version=$http_version" 438case "$http_version" in 439 2|2.0) ;; 440 *) 441 echo "error: cache proxy did not negotiate HTTP/2 (got $http_version)" >&2 442 exit 1 443 ;; 444esac 445 446/run/current-system/sw/bin/nix-store --realise "$store_path" >/dev/null 447' bash "$test_store_path") || return 1 448 449 if ! echo "$out" | strip_ansi | grep -q -E "^http_version=2(\\.0)?$"; then 450 echo "error: cache proxy did not report HTTP/2" >&2 451 echo "$out" | strip_ansi >&2 452 return 1 453 fi 454 echo "success: store path realized from cache and cache proxy accepted cleartext HTTP/2" 455} 456 457test_build_upload() { 458 local nix_expr='with import <nixpkgs> {}; writeText "uploaded-test-file" "hello from vm upload"' 459 local out 460 out=$(run_vm --name "build-upload" --timeout "120s" --upload -- /run/current-system/sw/bin/bash -l -c "nix-build -E '$nix_expr' --no-out-link") || return 1 461 462 local built_path 463 built_path=$(echo "$out" | strip_ansi | grep -v '\.drv' | grep -o '/nix/store/[a-z0-9]*-uploaded-test-file' | head -n 1 || true) 464 if [ -z "$built_path" ]; then 465 echo "error: could not find built store path in vm output" >&2 466 return 1 467 fi 468 echo "extracted path: $built_path" 469 470 local hash 471 hash=$(basename "$built_path" | cut -d'-' -f1) 472 if ! curl -s -f "$CACHE_URL/${hash}.narinfo" > /dev/null; then 473 echo "error: built store path was not uploaded to the binary cache" >&2 474 return 1 475 fi 476 echo "success: store path uploaded to cache" 477} 478 479test_ssh_store_upload() { 480 if ! host_is_nixos; then 481 skip_test "ssh store upload smoke only runs on nixos hosts" 482 fi 483 484 run_ssh_store_upload_case() { 485 local target="$1" 486 local label="$2" 487 local name="uploaded-test-file-${label}" 488 local content="hello from vm upload via ${label}" 489 local out 490 out=$(run_vm --name "$label" --timeout "180s" --upload-url "$target" -- /run/current-system/sw/bin/bash -l -c "nix-build -E 'with import <nixpkgs> {}; writeText \"$name\" \"$content\"' --no-out-link") || return 1 491 492 local built_path 493 built_path=$(echo "$out" | strip_ansi | grep -v '\.drv' | grep -o "/nix/store/[a-z0-9]*-${name}" | head -n 1 || true) 494 if [ -z "$built_path" ]; then 495 echo "error: could not find built store path in vm output for $label" >&2 496 echo "$out" | strip_ansi >&2 497 return 1 498 fi 499 if [ ! -e "$built_path" ]; then 500 echo "error: uploaded path missing from host store for $label: $built_path" >&2 501 return 1 502 fi 503 if [ "$(cat "$built_path")" != "$content" ]; then 504 echo "error: uploaded path content mismatch for $label" >&2 505 echo "path=$built_path" >&2 506 return 1 507 fi 508 echo "success: store path uploaded to $target via spindle" 509 } 510 511 run_ssh_store_upload_case "ssh-ng://localhost" "ssh-ng-upload" 512 run_ssh_store_upload_case "ssh://localhost" "ssh-upload" 513} 514 515test_networking() { 516 local hello_path 517 hello_path=$(nix-build -E 'with import <nixpkgs> {}; hello' --no-out-link) 518 519 local out 520 out=$(run_vm --name "networking" --timeout "120s" --no-cache -- /run/current-system/sw/bin/bash -c "/run/current-system/sw/bin/curl -I --connect-timeout 1 -m 1 http://10.0.2.2:$CACHE_PORT; /run/current-system/sw/bin/nix-store --realise $hello_path") || return 1 521 522 if echo "$out" | grep -qi -E "unreachable|timeout|failed to connect|timed out" || echo "$out" | grep -q "exited with code"; then 523 echo "success: host network access blocked" 524 else 525 echo "error: guest vm accessed host network or returned unexpected output" >&2 526 echo "$out" | strip_ansi >&2 527 return 1 528 fi 529 echo "success: guest vm reached the internet and substituted hello" 530} 531 532test_substitution_and_no_upload() { 533 local hello_path 534 hello_path=$(nix-build -E 'with import <nixpkgs> {}; hello' --no-out-link) 535 536 local out 537 out=$(run_vm --name "nixpkgs-hello" --timeout "180s" --upload -- /run/current-system/sw/bin/nix-store --realise "$hello_path") || return 1 538 539 # Check that it was substituted from our proxy 540 if ! echo "$out" | strip_ansi | grep -q -E "copying path.*hello"; then 541 echo "error: hello package was not substituted (or output mismatch)" >&2 542 echo "$out" | strip_ansi >&2 543 return 1 544 fi 545 546 # Check that nothing was uploaded to the cache 547 if ! echo "$out" | strip_ansi | grep -q "cache uploaded: 0"; then 548 echo "error: hello package substitution triggered cache upload" >&2 549 echo "$out" | strip_ansi >&2 550 return 1 551 fi 552 553 echo "success: hello package substituted from upstream cache and was not uploaded" 554} 555 556# a pinned registry, reused by the dependency and registry-pin tests. 557ACTIVATION_REGISTRY='"registry": { 558 "nixpkgs": "github:nixos/nixpkgs/nixos-unstable", 559 "my-nixpkgs": "nixpkgs" 560 }' 561 562test_activation_services() { 563 local config='{ 564 "services": { 565 "openssh": { 566 "enable": true, 567 "authorizedKeysFiles": ["/etc/ssh/authorized_keys"] 568 } 569 } 570 }' 571 local out 572 out=$(run_vm --name "activation-services" --timeout "300s" --activate "$config" -- /run/current-system/sw/bin/systemctl is-active sshd) || return 1 573 if ! echo "$out" | strip_ansi | grep -q "^active$"; then 574 echo "error: sshd not active after activation" >&2 575 echo "$out" | strip_ansi >&2 576 return 1 577 fi 578 echo "success: openssh service active after activation" 579} 580 581test_activation_dependencies() { 582 # cowsay as a bare dependency (resolved via the pinned nixpkgs registry); 583 # hello via the github flakeref and (separately) the my-nixpkgs alias. 584 local config='{ 585 '"$ACTIVATION_REGISTRY"', 586 "dependencies": [ 587 "cowsay", 588 "github:nixos/nixpkgs#hello", 589 "my-nixpkgs#hello" 590 ] 591 }' 592 local out 593 out=$(run_vm --name "activation-dependencies" --timeout "600s" --activate "$config" -- /run/current-system/sw/bin/bash -l -c ' 594set -euo pipefail 595cowsay "registry pin ok" >/dev/null && echo "cowsay=ran" 596echo "hello=$(hello)" 597') || return 1 598 599 local clean 600 clean=$(echo "$out" | strip_ansi) 601 if ! echo "$clean" | grep -qF "cowsay=ran"; then 602 echo "error: bare dependency 'cowsay' (resolved via the pinned nixpkgs registry) did not run" >&2 603 echo "$clean" >&2 604 return 1 605 fi 606 if ! echo "$clean" | grep -qF "hello=Hello, world!"; then 607 echo "error: hello dependency (github flakeref + my-nixpkgs alias) did not run" >&2 608 echo "$clean" >&2 609 return 1 610 fi 611 echo "success: bare, flakeref, and aliased dependencies all resolved and ran" 612} 613 614test_activation_registry_pin() { 615 # the nixpkgs the image itself was built from; the registry override must NOT 616 # resolve to this. deterministic (locked in the repo flake), so safe to compare. 617 local base_nixpkgs 618 base_nixpkgs=$(nix eval --raw --impure --expr '(builtins.getFlake (toString ./.)).inputs.nixpkgs.outPath') 619 620 local config='{ 621 '"$ACTIVATION_REGISTRY"' 622 }' 623 # the pinned nixpkgs must reach the system nix config: resolving it via the 624 # flakes CLI must not error "is not locked", and it must win the <nixpkgs> nixPath. 625 local out 626 out=$(run_vm --name "activation-registry-pin" --timeout "300s" --activate "$config" -- /run/current-system/sw/bin/bash -l -c ' 627set -euo pipefail 628echo "lib_version=$(nix eval --raw nixpkgs#lib.version)" 629echo "nix_path=$(nix eval --raw --impure --expr "toString <nixpkgs>")" 630') || return 1 631 632 local clean 633 clean=$(echo "$out" | strip_ansi) 634 if ! echo "$clean" | grep -qE "lib_version=[0-9]"; then 635 echo "error: guest could not resolve nixpkgs#lib.version from the user registry (locked-ref failure?)" >&2 636 echo "$clean" >&2 637 return 1 638 fi 639 local guest_nixpath 640 guest_nixpath=$(echo "$clean" | sed -n 's/^nix_path=//p' | head -n1) 641 if [ -z "$guest_nixpath" ] || [ "$guest_nixpath" = "$base_nixpkgs" ]; then 642 echo "error: guest <nixpkgs> nixPath did not resolve to the registry override (got '$guest_nixpath', base '$base_nixpkgs')" >&2 643 return 1 644 fi 645 echo "success: pinned nixpkgs registry reached the guest nix config (flakes CLI + <nixpkgs> nixPath)" 646} 647 648test_activation_cache_substitution() { 649 # a unique path that only exists in the configured (workflow) cache; the host 650 # seeds it so the guest can prove it substitutes through the read proxy. 651 local test_store_path 652 test_store_path=$(nix-build -E 'with import <nixpkgs> {}; writeText "activation-cache-test" "hello from the workflow cache"' --no-out-link) 653 nix copy --to "$CACHE_UPLOAD_URL?secret-key=$CACHE_SECRET_KEY_PATH" "$test_store_path" 654 655 # a trivial config: this test only cares that the read proxy serves the 656 # workflow-cache path during an activated run. 657 local out 658 out=$(run_vm --name "activation-cache-substitution" --timeout "300s" --activate '{}' -- /run/current-system/sw/bin/bash -l -c ' 659set -euo pipefail 660store_path=$1 661# the unique path only exists in the configured cache, so realising it proves it 662# was substituted through the read proxy and not built or found elsewhere. 663nix-store --realise "$store_path" >/dev/null 664echo "substituted=$(cat "$store_path")" 665' bash "$test_store_path") || return 1 666 667 if ! echo "$out" | strip_ansi | grep -qF "substituted=hello from the workflow cache"; then 668 echo "error: unique path was not substituted from the configured cache" >&2 669 echo "$out" | strip_ansi >&2 670 return 1 671 fi 672 echo "success: unique path substituted from the workflow cache through the read proxy" 673} 674 675test_activation_docker() { 676 local config='{ 677 "virtualisation": { 678 "docker": { "enable": true } 679 } 680 }' 681 # docker.service is up, but the daemon socket can lag a beat behind activation; 682 # wait for it to answer, then pull+run a real image. this drives the slimmed 683 # kernel modules: overlay.ko storage plus bridge/iptables networking out of the 684 # pruned tree, with outbound DNS/network over the guest slirp link. 685 local out 686 out=$(run_vm --name "activation-docker" --timeout "600s" --activate "$config" -- /run/current-system/sw/bin/bash -l -c ' 687set -euo pipefail 688echo "docker_unit=$(systemctl is-active docker)" 689for i in $(seq 1 60); do docker info >/dev/null 2>&1 && break; sleep 1; done 690docker info >/dev/null 691echo "storage_driver=$(docker info --format "{{.Driver}}")" 692docker run --rm alpine cat /etc/alpine-release | sed "s/^/alpine_release=/" 693docker run --rm alpine echo container-ran-ok 694') || return 1 695 696 local clean 697 clean=$(echo "$out" | strip_ansi) 698 if ! echo "$clean" | grep -q "^docker_unit=active$"; then 699 echo "error: docker service not active after activation" >&2 700 echo "$clean" >&2 701 return 1 702 fi 703 if ! echo "$clean" | grep -qE "storage_driver=overlay(2|fs)"; then 704 echo "error: docker is not using an overlay storage driver (overlay.ko missing?)" >&2 705 return 1 706 fi 707 if ! echo "$clean" | grep -qE "alpine_release=[0-9]+\."; then 708 echo "error: failed to pull and read the alpine image" >&2 709 return 1 710 fi 711 if ! echo "$clean" | grep -q "container-ran-ok"; then 712 echo "error: command did not run inside the alpine container" >&2 713 return 1 714 fi 715 echo "success: docker service active, pulled and ran an alpine container on the overlay storage driver" 716} 717 718test_activation_cached_realize() { 719 local config='{ 720 "services": { 721 "openssh": { 722 "enable": true, 723 "authorizedKeysFiles": ["/etc/ssh/authorized_keys"] 724 } 725 } 726 }' 727 local db_path="$TEMP_DIR/activation-cached.db" 728 729 # first run: build the config, upload its closure, and record the toplevel in 730 # the db. nothing cached yet, so this builds from scratch. 731 local out 732 out=$(run_vm --name "activation-cached-first" --timeout "600s" --activate "$config" --db "$db_path" --upload -- /run/current-system/sw/bin/systemctl is-active sshd) || return 1 733 if ! echo "$out" | strip_ansi | grep -q "^active$"; then 734 echo "error: sshd not active after first activation" >&2 735 echo "$out" | strip_ansi >&2 736 return 1 737 fi 738 739 # second run: same config + db, no upload. must realize the recorded toplevel 740 # from the cache instead of rebuilding, and the cached system must come up. 741 out=$(run_vm --name "activation-cached-second" --timeout "300s" --activate "$config" --db "$db_path" -- /run/current-system/sw/bin/systemctl is-active sshd) || return 1 742 743 local clean 744 clean=$(echo "$out" | strip_ansi) 745 if ! echo "$clean" | grep -q "realizing cached NixOS config"; then 746 echo "error: second run did not realize cached configuration" >&2 747 echo "$clean" >&2 748 return 1 749 fi 750 if ! echo "$clean" | grep -q "^active$"; then 751 echo "error: sshd not active after cached config activation" >&2 752 echo "$clean" >&2 753 return 1 754 fi 755 echo "success: second run realized the cached NixOS config from the cache and sshd came up" 756} 757 758test_alpine() { 759 local hello_path 760 hello_path=$(nix-build -E 'with import <nixpkgs> {}; hello' --no-out-link) 761 762 local out 763 out=$(run_vm --spec "$ALPINE_IMAGE_SPEC_JSON" --name "alpine" --timeout "180s" --no-cache -- /bin/sh -lc ' 764set -eu 765export HOME=/workspace 766hello_path=$1 767echo "release=$(cat /etc/alpine-release)" 768echo "user=$(id -un)" 769git version 770bash -c "echo bash=\$BASH_VERSION" 771touch /workspace/write-test 772echo "workspace writable" 773git ls-remote https://tangled.org/@tangled.org/core HEAD >/dev/null 774echo "git over https ok" 775apk add make 776echo "apk ok" 777# substitute a real package from cache.nixos.org over HTTPS and run it 778nix-store --realise "$hello_path" >/dev/null 779echo "ran=$("$hello_path/bin/hello")" 780' sh "$hello_path") || return 1 781 782 echo "$out" | strip_ansi >&2 783 for needle in "release=" "user=spindle-workflow" "git version" "bash=" "workspace writable" "git over https ok" "apk ok" "ran=Hello, world!"; do 784 if ! echo "$out" | strip_ansi | grep -q "$needle"; then 785 echo "error: alpine guest output missing $needle" >&2 786 return 1 787 fi 788 done 789 echo "success: alpine guest booted, ran as workflow user, wrote workspace, cloned + installed over the network, and substituted+ran a package from cache.nixos.org over HTTPS" 790} 791 792# asserts a store path's narinfo shows up in the local cache, retrying briefly 793# since the post-build-hook enqueues uploads asynchronously. 794cache_has_path() { 795 local path="$1" 796 local hash 797 hash=$(basename "$path" | cut -d'-' -f1) 798 local i 799 for i in $(seq 1 20); do 800 if curl -s -f "$CACHE_URL/${hash}.narinfo" > /dev/null; then 801 return 0 802 fi 803 sleep 0.5 804 done 805 return 1 806} 807 808test_alpine_nix() { 809 local test_store_path 810 test_store_path=$(nix-build -E 'with import <nixpkgs> {}; writeText "alpine-nix-test" "hello from cache to alpine"' --no-out-link) 811 nix copy --to "$CACHE_UPLOAD_URL?secret-key=$CACHE_SECRET_KEY_PATH" "$test_store_path" 812 813 # exercise the full local-cache path: daemon connectivity, substitution, 814 # store-db queries, and a build via *both* the classic (nix-build) and the 815 # new flakes/nix-command (nix build) frontends. the two build derivations 816 # use distinct names so we can confirm each got uploaded back to the cache. 817 local out 818 out=$(run_vm --spec "$ALPINE_IMAGE_SPEC_JSON" --name "alpine-nix" --timeout "180s" --upload -- /bin/sh -lc ' 819set -eu 820export HOME=/workspace 821store_path=$1 822 823echo "nix_version=$(nix --version | head -n1)" 824{ nix store info >/dev/null 2>&1 || nix store ping >/dev/null 2>&1; } && echo "daemon=ok" 825 826# substitute a path from the cache and query the store db about it 827nix-store --realise "$store_path" >/dev/null 828echo "substituted=$(cat "$store_path")" 829echo "requisites=$(nix-store --query --requisites "$store_path" | wc -l | tr -d " ")" 830nix path-info --json "$store_path" >/dev/null && echo "path_info=ok" 831 832# build via the new CLI; the substituted path is declared as a real input 833# (builtins.storePath) so nix must realise it into the build sandbox first. 834# heredoc is unquoted (the outer guest script is single-quoted, so a quoted 835# delimiter would close it), hence \$ escapes what nix/the builder must expand. 836export DEP="$store_path" 837cat > /workspace/new.nix <<NIXEOF 838let dep = builtins.storePath (builtins.getEnv "DEP"); in 839derivation { 840 name = "alpine-nix-build-new"; 841 system = "x86_64-linux"; 842 builder = "/bin/sh"; 843 # the sandbox only provides the sh builtin shell (no coreutils in PATH), so 844 # stick to builtins: the build only succeeds if nix realised the declared 845 # storePath dependency into the sandbox, where [ -r ] can see it. 846 args = [ "-c" "[ -r \${dep} ] && echo via-nix-build-with-dep > \$out" ]; 847} 848NIXEOF 849new_path=$(nix build --impure --file /workspace/new.nix --no-link --print-out-paths) 850echo "new_path=$new_path" 851echo "new_content=$(tr "\n" "|" < "$new_path")" 852 853# build via the classic CLI 854cat > /workspace/old.nix <<NIXEOF 855derivation { 856 name = "alpine-nix-build-old"; 857 system = "x86_64-linux"; 858 builder = "/bin/sh"; 859 args = [ "-c" "echo built-on-alpine > \$out" ]; 860} 861NIXEOF 862old_path=$(nix-build /workspace/old.nix --no-out-link) 863echo "old_path=$old_path" 864' sh "$test_store_path") || return 1 865 866 echo "$out" | strip_ansi >&2 867 local clean 868 clean=$(echo "$out" | strip_ansi) 869 870 local needle 871 for needle in "daemon=ok" "substituted=hello from cache to alpine" "path_info=ok"; do 872 if ! echo "$clean" | grep -q "$needle"; then 873 echo "error: alpine nix output missing '$needle'" >&2 874 return 1 875 fi 876 done 877 if ! echo "$clean" | grep -qE "requisites=[1-9][0-9]*"; then 878 echo "error: store db query returned no requisites for the substituted path" >&2 879 return 1 880 fi 881 if ! echo "$clean" | grep -q "new_content=via-nix-build-with-dep"; then 882 echo "error: 'nix build' (new CLI) did not realise its substituted dependency and build" >&2 883 return 1 884 fi 885 886 local new_path old_path 887 new_path=$(echo "$clean" | grep -o 'new_path=/nix/store/[a-z0-9]*-alpine-nix-build-new' | cut -d= -f2) 888 old_path=$(echo "$clean" | grep -o 'old_path=/nix/store/[a-z0-9]*-alpine-nix-build-old' | cut -d= -f2) 889 if [ -z "$new_path" ] || [ -z "$old_path" ]; then 890 echo "error: could not extract both built store paths from alpine guest output" >&2 891 return 1 892 fi 893 if ! cache_has_path "$new_path"; then 894 echo "error: nix-build (new CLI) output was not uploaded to the cache" >&2 895 return 1 896 fi 897 if ! cache_has_path "$old_path"; then 898 echo "error: nix-build (classic CLI) output was not uploaded to the cache" >&2 899 return 1 900 fi 901 echo "success: alpine guest substituted, queried the store db, built via both CLIs, and uploaded both outputs" 902} 903 904TESTS=( 905 test_alpine 906 test_alpine_nix 907 test_realize 908 test_build_upload 909 test_ssh_store_upload 910 test_networking 911 test_substitution_and_no_upload 912 test_activation_services 913 test_activation_dependencies 914 test_activation_registry_pin 915 test_activation_cache_substitution 916 test_activation_docker 917 test_activation_cached_realize 918) 919 920log "running ${#TESTS[@]} tests" 921if ! run_tests; then 922 exit 1 923fi 924 925SUCCESS=1 926log "passed!!"