#!/usr/bin/env bash # Modules integration test runner. # The tests (defined in ./tests.nix) use the NixOS testing framework and are executed in a VM. # # Usage: # Run all tests # ./run-tests.sh # # Test specific scenario # ./run-tests.sh --scenario|-s # # - When contains a space, is treated as nix code defining # a scenario. It is evaluated in the same context as other scenarios in ./tests.nix # # Example: # ./run-tests.sh -s "{ nix-bitcoin.nodeinfo.enable = true; }" container --run c nodeinfo # # - When does not name a scenario, the test is run with an adhoc scenario # where services. is enabled. # # Example: # ./run-tests.sh -s electrs # # Run test(s) and link results to avoid garbage collection # ./run-tests.sh [--scenario ] --out-link-prefix /tmp/nix-bitcoin-test # # Pass extra args to nix-build # ./run-tests.sh build --builders 'ssh://mybuildhost - - 15' # # Run interactive test debugging # ./run-tests.sh [--scenario ] debug # # This starts the testing VM and drops you into a Python REPL where you can # manually execute the tests from ./tests.py # # Run a test scenario in a container # sudo ./run-tests.sh [--scenario ] container # # This is useful for quick experiments; containers start much faster than VMs. # Running the Python test suite in containers is not yet supported. # For now, creating NixOS containers requires root permissions. # See ./lib/make-container.sh for a complete documentation. # # Run a test scenario in a regular NixOS VM. # No tests are executed, the machine's serial console is attached to your terminal. # ./run-tests.sh [--scenario ] vm # # This is useful for directly exploring a test configuration without the # intermediate Python REPL layer. # Run command 'q' inside the machine for instant poweroff. # # Run tests from a snapshot copy of the source files # ./run-tests.sh --copy-src|-c ... # # This allows you to continue editing the nix-bitcoin sources while tests are running # and reading source files. # Files are copied to /tmp, a caching scheme helps minimizing copies. # # To add custom scenarios, set the environment variable `scenarioOverridesFile`. set -eo pipefail scriptDir=$(cd "${BASH_SOURCE[0]%/*}" && pwd) args=("$@") scenario= outLinkPrefix= ciBuild= while :; do case $1 in --scenario|-s) if [[ $2 ]]; then scenario=$2 shift shift else >&2 echo "Error: $1 requires an argument." exit 1 fi ;; --out-link-prefix|-o) if [[ $2 ]]; then outLinkPrefix=$2 shift shift else >&2 echo "Error: $1 requires an argument." exit 1 fi ;; --ci) shift ciBuild=1 ;; --copy-src|-c) shift if [[ ! $_nixBitcoinInCopiedSrc ]]; then . "$scriptDir/lib/copy-src.sh" exit fi ;; *) break esac done numCPUs=${numCPUs:-$(nproc)} # Min. 800 MiB needed to avoid 'out of memory' errors memoryMiB=${memoryMiB:-2048} NIX_PATH=nixpkgs=$(nix eval --raw -f "$scriptDir/../pkgs/nixpkgs-pinned.nix" nixpkgs):nix-bitcoin=$(realpath "$scriptDir/..") export NIX_PATH runAtExit= trap 'eval "$runAtExit"' EXIT # Support explicit scenario definitions if [[ $scenario = *' '* ]]; then scenarioOverridesFile=$(mktemp "${XDG_RUNTIME_DIR:-/tmp}/nb-scenario.XXX") export scenarioOverridesFile # shellcheck disable=SC2016 runAtExit+='rm -f "$scenarioOverridesFile";' echo "{ scenarios, pkgs, lib }: with lib; { tmp = $scenario; }" > "$scenarioOverridesFile" scenario=tmp fi # Run the test. No temporary files are left on the host system. run() { # TMPDIR is also used by the test driver for VM tmp files TMPDIR=$(mktemp -d /tmp/nix-bitcoin-test.XXX) export TMPDIR runAtExit+="rm -rf ${TMPDIR};" nix-build --out-link "$TMPDIR/driver" -E "((import \"$scriptDir/tests.nix\" {}).getTest \"$scenario\").vm" -A driver # Variable 'tests' contains the Python code that is executed by the driver on startup if [[ $1 == --interactive ]]; then echo "Running interactive testing environment" tests=$( echo 'is_interactive = True' echo 'exec(open(os.environ["testScript"]).read())' # Start VM echo 'if "machine" in vars(): machine.start()' # Start REPL. # Use `code.interact` for the REPL instead of the builtin test driver REPL # because it supports low featured terminals like Emacs' shell-mode. echo 'import code' echo 'code.interact(local=globals())' ) else tests='exec(open(os.environ["testScript"]).read())' fi echo "VM stats: CPUs: $numCPUs, memory: $memoryMiB MiB" [[ $NB_TEST_ENABLE_NETWORK ]] || QEMU_NET_OPTS='restrict=on' cd "$TMPDIR" # The VM creates a VDE control socket in $PWD env -i \ NIX_PATH="$NIX_PATH" \ TMPDIR="$TMPDIR" \ USE_TMPDIR=1 \ QEMU_OPTS="-smp $numCPUs -m $memoryMiB -nographic $QEMU_OPTS" \ QEMU_NET_OPTS="$QEMU_NET_OPTS" \ "$TMPDIR/driver/bin/nixos-test-driver" <(echo "$tests") } debug() { run --interactive } evalTest() { nix-instantiate --eval -E "($(vmTestNixExpr)).outPath" } instantiate() { nix-instantiate -E "$(vmTestNixExpr)" "$@" } container() { export scriptDir scenario "$scriptDir/lib/make-container.sh" "$@" } # Run a regular NixOS VM vm() { TMPDIR=$(mktemp -d /tmp/nix-bitcoin-vm.XXX) export TMPDIR runAtExit+="rm -rf $TMPDIR;" nix-build --out-link "$TMPDIR/vm" -E "((import \"$scriptDir/tests.nix\" {}).getTest \"$scenario\").vmWithoutTests" echo "VM stats: CPUs: $numCPUs, memory: $memoryMiB MiB" [[ $NB_TEST_ENABLE_NETWORK ]] || export QEMU_NET_OPTS="restrict=on,$QEMU_NET_OPTS" # shellcheck disable=SC2211 USE_TMPDIR=1 \ NIX_DISK_IMAGE=$TMPDIR/img.qcow2 \ QEMU_OPTS="-smp $numCPUs -m $memoryMiB -nographic $QEMU_OPTS" \ "$TMPDIR"/vm/bin/run-*-vm } doBuild() { name=$1 shift if [[ $ciBuild ]]; then "$scriptDir/ci/build-to-cachix.sh" "$@" else if [[ $outLinkPrefix ]]; then outLink="--out-link $outLinkPrefix-$name" else outLink=--no-out-link fi nix-build $outLink "$@" fi } # Run the test by building the test derivation buildTest() { vmTestNixExpr | doBuild $scenario "$@" - } vmTestNixExpr() { extraQEMUOpts= if [[ $ciBuild ]]; then # On continuous integration nodes there are few other processes running alongside the # test, so use more memory here for maximum performance. memoryMiB=4096 memTotalKiB=$(awk '/MemTotal/ { print $2 }' /proc/meminfo) memAvailableKiB=$(awk '/MemAvailable/ { print $2 }' /proc/meminfo) # Round down to nearest multiple of 50 MiB for improved test build caching # shellcheck disable=SC2017 ((memAvailableMiB = memAvailableKiB / (1024 * 50) * 50)) ((memAvailableMiB < memoryMiB)) && memoryMiB=$memAvailableMiB >&2 echo "VM stats: CPUs: $numCPUs, memory: $memoryMiB MiB" >&2 echo "Host memory total: $((memTotalKiB / 1024)) MiB, available: $memAvailableMiB MiB" # VMX is usually not available on CI nodes due to recursive virtualisation. # Explicitly disable VMX, otherwise QEMU 4.20 fails with message # "error: failed to set MSR 0x48b to 0x159ff00000000" extraQEMUOpts="-cpu host,-vmx" fi cat <&1) == *"requires a sub-command"* ]]; then hasFlakes=1 else hasFlakes= fi fi if [[ ! $hasFlakes ]]; then echo "Skipping test '$testName'. Nix flake support is not enabled." return 1 fi } flake() { if ! checkFlakeSupport "flake"; then return; fi nix flake check "$scriptDir/.." } # Test generating module documentation for search.nixos.org nixosSearch() { if ! checkFlakeSupport "nixosSearch"; then return; fi if [[ $_nixBitcoinInCopiedSrc ]]; then # flake-info requires that its target flake is under version control . "$scriptDir/lib/create-git-repo.sh" fi if [[ $outLinkPrefix ]]; then # Add gcroots for flake-info nix build "$scriptDir/nixos-search#flake-info" -o "$outLinkPrefix-flake-info" fi echo "Running flake-info (nixos-search)" nix run "$scriptDir/nixos-search#flake-info" -- flake "$scriptDir/.." } # A basic subset of tests to keep the total runtime within # manageable bounds (<4 min on desktop systems). # These are also run on the CI server. basic() { scenario=default buildTest "$@" scenario=netns buildTest "$@" scenario=netnsRegtest buildTest "$@" } # All tests that only consist of building a nix derivation. # Their output is cached in /nix/store. buildable() { basic "$@" scenario=full buildTest "$@" scenario=regtest buildTest "$@" scenario=hardened buildTest "$@" scenario=clightningReplication buildTest "$@" } examples() { script=" set -e ./deploy-container.sh ./deploy-container-minimal.sh ./deploy-qemu-vm.sh ./deploy-krops.sh " (cd "$scriptDir/../examples" && nix-shell --run "$script") } shellcheck() { if ! checkFlakeSupport "shellcheck"; then return; fi "$scriptDir/shellcheck.sh" } all() { buildable "$@" shellcheck examples flake nixosSearch } # An alias for buildTest build() { buildTest "$@" } if [[ $# -gt 0 && $1 != -* ]]; then # An explicit command was provided command=$1 shift if [[ $command == eval ]]; then command=evalTest fi : ${scenario:=default} elif [[ $scenario ]]; then command=buildTest else command=basic fi $command "$@"