From 0ead7b073086ae77f1b928e07286749d7954b2aa Mon Sep 17 00:00:00 2001 From: Jan Dubois Date: Fri, 5 Sep 2025 21:30:35 -0700 Subject: [PATCH] Add BATS for integration tests Signed-off-by: Jan Dubois --- .github/workflows/test.yml | 8 ++ .gitmodules | 16 ++++ Makefile | 4 + hack/bats/helpers/load.bash | 48 ++++++++++ hack/bats/lib/bats-assert | 1 + hack/bats/lib/bats-core | 1 + hack/bats/lib/bats-file | 1 + hack/bats/lib/bats-support | 1 + hack/bats/tests/preserve-env.bats | 153 ++++++++++++++++++++++++++++++ hack/bats/tests/protect.bats | 69 ++++++++++++++ 10 files changed, 302 insertions(+) create mode 100644 .gitmodules create mode 100644 hack/bats/helpers/load.bash create mode 160000 hack/bats/lib/bats-assert create mode 160000 hack/bats/lib/bats-core create mode 160000 hack/bats/lib/bats-file create mode 160000 hack/bats/lib/bats-support create mode 100644 hack/bats/tests/preserve-env.bats create mode 100644 hack/bats/tests/protect.bats diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6611b084509..4c45aaa35a9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -301,6 +301,11 @@ jobs: - ../hack/test-templates/test-misc.yaml # TODO: merge net-user-v2 into test-misc steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + # BATS tests don't expect lima version warnings like: + # msg="treating lima version \"ea336ae\" from \"/Users/runner/.lima-bats/dummy/lima-version\" as very latest release" + fetch-depth: 0 + submodules: true - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version: 1.25.x @@ -319,6 +324,9 @@ jobs: sudo modprobe kvm # `sudo usermod -aG kvm $(whoami)` does not take an effect on GHA sudo chown $(whoami) /dev/kvm + - name: "Run BATS integration tests" + run: make bats + if: matrix.template == '../hack/test-templates/test-misc.yaml' - name: Install ansible-playbook run: | sudo apt-get install -y --no-install-recommends ansible diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000000..78b8c0083ba --- /dev/null +++ b/.gitmodules @@ -0,0 +1,16 @@ +[submodule "hack/bats/lib/bats-core"] + path = hack/bats/lib/bats-core + url = https://github.com/lima-vm/bats-core.git + branch = master +[submodule "hack/bats/lib/bats-file"] + path = hack/bats/lib/bats-file + url = https://github.com/lima-vm/bats-file.git + branch = master +[submodule "hack/bats/lib/bats-assert"] + path = hack/bats/lib/bats-assert + url = https://github.com/lima-vm/bats-assert.git + branch = master +[submodule "hack/bats/lib/bats-support"] + path = hack/bats/lib/bats-support + url = https://github.com/lima-vm/bats-support.git + branch = master diff --git a/Makefile b/Makefile index 3b718ecddbc..b85fbb57e39 100644 --- a/Makefile +++ b/Makefile @@ -505,6 +505,10 @@ check-generated: ((git diff $$(find . -name '*.pb.desc') | cat) && \ (echo "Please run 'make generate' when making changes to proto files and check-in the generated file changes" && false)) +.PHONY: bats +bats: native + PATH=$$PWD/_output/bin:$$PATH ./hack/bats/lib/bats-core/bin/bats --timing ./hack/bats/tests + .PHONY: lint lint: check-generated golangci-lint run ./... diff --git a/hack/bats/helpers/load.bash b/hack/bats/helpers/load.bash new file mode 100644 index 00000000000..6ae7b49cf2b --- /dev/null +++ b/hack/bats/helpers/load.bash @@ -0,0 +1,48 @@ +set -o errexit -o nounset -o pipefail + +# Don't run the tests in ~/.lima because they may destroy _config, _templates etc. +export LIMA_HOME=${LIMA_BATS_LIMA_HOME:-$HOME/.lima-bats} + +absolute_path() { + ( + cd "$1" + pwd + ) +} + +PATH_BATS_HELPERS=$(absolute_path "$(dirname "${BASH_SOURCE[0]}")") +PATH_BATS_ROOT=$(absolute_path "$PATH_BATS_HELPERS/..") + +source "$PATH_BATS_ROOT/lib/bats-support/load.bash" +source "$PATH_BATS_ROOT/lib/bats-assert/load.bash" +source "$PATH_BATS_ROOT/lib/bats-file/load.bash" + +bats_require_minimum_version 1.5.0 + +# If called from foo() this function will call local_foo() if it exist. +call_local_function() { + local func + func="local_${FUNCNAME[1]}" + if [ "$(type -t "$func")" = "function" ]; then + "$func" + fi +} + +setup_file() { + if [[ ${CI:-} == true ]]; then + # Without a terminal the output is using TAP formatting, which does not include the filename + local TEST_FILENAME=${BATS_TEST_FILENAME#"$PATH_BATS_ROOT/tests/"} + TEST_FILENAME=${TEST_FILENAME%.bats} + echo "# ===== ${TEST_FILENAME} =====" >&3 + fi + call_local_function +} +teardown_file() { + call_local_function +} +setup() { + call_local_function +} +teardown() { + call_local_function +} \ No newline at end of file diff --git a/hack/bats/lib/bats-assert b/hack/bats/lib/bats-assert new file mode 160000 index 00000000000..3be0fb78567 --- /dev/null +++ b/hack/bats/lib/bats-assert @@ -0,0 +1 @@ +Subproject commit 3be0fb7856791b4a64aef7a1336e965f5252e45f diff --git a/hack/bats/lib/bats-core b/hack/bats/lib/bats-core new file mode 160000 index 00000000000..397c4a5661e --- /dev/null +++ b/hack/bats/lib/bats-core @@ -0,0 +1 @@ +Subproject commit 397c4a5661e12b518487086db9eec9b306d31828 diff --git a/hack/bats/lib/bats-file b/hack/bats/lib/bats-file new file mode 160000 index 00000000000..0f24d004706 --- /dev/null +++ b/hack/bats/lib/bats-file @@ -0,0 +1 @@ +Subproject commit 0f24d004706e7d3747c3d7d0f14a47f05a31748e diff --git a/hack/bats/lib/bats-support b/hack/bats/lib/bats-support new file mode 160000 index 00000000000..0954abb9925 --- /dev/null +++ b/hack/bats/lib/bats-support @@ -0,0 +1 @@ +Subproject commit 0954abb9925cad550424cebca2b99255d4eabe96 diff --git a/hack/bats/tests/preserve-env.bats b/hack/bats/tests/preserve-env.bats new file mode 100644 index 00000000000..5aa8c38bd4e --- /dev/null +++ b/hack/bats/tests/preserve-env.bats @@ -0,0 +1,153 @@ +load "../helpers/load" + +NAME=bats + +local_setup_file() { + unset LIMA_SHELLENV_ALLOW + unset LIMA_SHELLENV_BLOCK + + if [[ -n "${LIMA_BATS_REUSE_INSTANCE:-}" ]]; then + run limactl list --format '{{.Status}}' "$NAME" + [[ $status == 0 ]] && [[ $output == "Running" ]] && return + fi + limactl unprotect "$NAME" || : + limactl delete --force "$NAME" || : + # Make sure that the host agent doesn't inherit file handles 3 or 4. + # Otherwise bats will not finish until the host agent exits. + limactl start --yes --name "$NAME" template://default 3>&- 4>&- +} + +local_teardown_file() { + if [[ -z "${LIMA_BATS_REUSE_INSTANCE:-}" ]]; then + limactl delete --force "$NAME" + fi +} + +local_setup() { + # make sure changes from previous tests are removed + limactl shell "$NAME" sh -c '[ ! -f ~/.bash_profile ] || sed -i -E "/^export (FOO|BAR|SSH_)/d" ~/.bash_profile' +} + +@test 'there are no FOO*, BAR*, or SSH_FOO* variables defined in the VM' { + # just to confirm because the other tests depend on these being unused + run -0 limactl shell "$NAME" printenv + refute_line --regexp '^FOO' + refute_line --regexp '^BAR' + refute_line --regexp '^SSH_FOO' +} + +@test 'environment is not preserved by default' { + export FOO=foo + run -0 limactl shell "$NAME" printenv + refute_line --regexp '^FOO=' +} + +@test 'environment is preserved with --preserve-env' { + export FOO=foo + run -0 limactl shell --preserve-env "$NAME" printenv + assert_line FOO=foo +} + +@test 'profile settings inside the VM take precedence over preserved variables' { + limactl shell "$NAME" sh -c 'echo "export FOO=bar" >>~/.bash_profile' + export FOO=foo + run -0 limactl shell --preserve-env "$NAME" printenv + assert_line FOO=bar +} + +@test 'builtin block list is used when LIMA_SHELLENV_BLOCK is not set' { + # default block list includes SSH_* + export SSH_FOO=ssh_foo + run -0 limactl shell --preserve-env "$NAME" printenv + refute_line --regexp '^SSH_FOO=' +} + +@test 'custom block list replaces builtin block list' { + export LIMA_SHELLENV_BLOCK=FOO + export FOO=foo + export SSH_FOO=foo + run -0 limactl shell --preserve-env "$NAME" printenv + refute_line --regexp '^FOO=' + assert_line SSH_FOO=foo +} + +@test 'custom block list starting with + appends to builtin block list' { + export LIMA_SHELLENV_BLOCK=+FOO + export FOO=foo + export SSH_FOO=foo + run -0 limactl shell --preserve-env "$NAME" printenv + refute_line --regexp '^FOO=' + refute_line --regexp '^SSH_FOO=' +} + +@test 'block list entries can use * wildcard at the end' { + export LIMA_SHELLENV_BLOCK="FOO*" + export FOO=foo + export FOOBAR=foobar + export BAR=bar + run -0 limactl shell --preserve-env "$NAME" printenv + refute_line --regexp '^FOO' + assert_line BAR=bar +} + +@test 'wildcard does only work at the end of the pattern' { + export LIMA_SHELLENV_BLOCK="*FOO" + export FOO=foo + export BARFOO=barfoo + run -0 limactl shell --preserve-env "$NAME" printenv + assert_line FOO=foo + assert_line BARFOO=barfoo +} + +@test 'block list can use a , separated list with whitespace ignored' { + export LIMA_SHELLENV_BLOCK="FOO*, , BAR" + export FOO=foo + export FOOBAR=foobar + export BAR=bar + export BARBAZ=barbaz + run -0 limactl shell --preserve-env "$NAME" printenv + refute_line --regexp '^FOO' + refute_line --regexp '^BAR=' + assert_line BARBAZ=barbaz +} + +@test 'allow list overrides block list but blocks everything else' { + export LIMA_SHELLENV_ALLOW=SSH_FOO + export SSH_FOO=ssh_foo + export SSH_BAR=ssh_bar + export BAR=bar + run -0 limactl shell --preserve-env "$NAME" printenv + assert_line SSH_FOO=ssh_foo + refute_line --regexp '^SSH_BAR=' + refute_line --regexp '^BAR=' +} + +@test 'allow list can use a , separated list with whitespace ignored' { + export LIMA_SHELLENV_ALLOW="FOO*, , BAR" + export FOO=foo + export FOOBAR=foobar + export BAR=bar + export BARBAZ=barbaz + run -0 limactl shell --preserve-env "$NAME" printenv + assert_line FOO=foo + assert_line FOOBAR=foobar + assert_line BAR=bar + refute_line --regexp '^BARBAZ=' +} + +@test 'setting both allow list and block list generates a warning' { + export LIMA_SHELLENV_ALLOW=FOO + export LIMA_SHELLENV_BLOCK=BAR + export FOO=foo + run -0 --separate-stderr limactl shell --preserve-env "$NAME" printenv FOO + assert_output foo + assert_stderr --regexp 'level=warning msg="Both LIMA_SHELLENV_BLOCK and LIMA_SHELLENV_ALLOW are set' +} + +@test 'limactl info includes the default block list' { + run -0 limactl info + run -0 limactl yq '.shellEnvBlock[]' <<<"$output" + assert_line PATH + assert_line "SSH_*" + assert_line USER +} diff --git a/hack/bats/tests/protect.bats b/hack/bats/tests/protect.bats new file mode 100644 index 00000000000..dcf464cc82b --- /dev/null +++ b/hack/bats/tests/protect.bats @@ -0,0 +1,69 @@ +load "../helpers/load" + +NAME=dummy +CLONE=clone +NOTEXIST=notexist + +local_setup_file() { + for INSTANCE in "$NAME" "$CLONE" "$NOTEXIST"; do + limactl unprotect "$INSTANCE" || : + limactl delete --force "$INSTANCE" || : + done +} + +@test 'create dummy instance' { + # needs an image that clonefile() can process; /dev/null doesn't work + limactl create --name "$NAME" - <<<"{images: [location: /etc/profile], disk: 1M}" +} + +@test 'protecting a non-existing instance fails' { + run -1 limactl protect "${NOTEXIST}" + assert_output --partial 'failed to inspect instance' +} + +@test 'protecting the dummy instance succeeds' { + run -0 limactl protect "$NAME" + assert_output --regexp "Protected \\\\\"$NAME\\\\\"" + assert_file_exists "$LIMA_HOME/$NAME/protected" +} + +@test 'protecting it again shows a warning, but succeeds' { + run -0 limactl protect "$NAME" + assert_output --partial 'already protected. Skipping' + assert_file_exists "$LIMA_HOME/$NAME/protected" +} + +@test 'cloning a protected instance creates an unprotected clone' { + run -0 limactl clone --yes "$NAME" "$CLONE" + # TODO there is currently no output from the clone command, which feels wrong + refute_output + assert_file_not_exists "$LIMA_HOME/$CLONE/protected" +} + +@test 'deleting the unprotected clone instance succeeds' { + run -0 limactl delete --force "$CLONE" + assert_output --regexp "Deleted \\\\\"$CLONE\\\\\"" +} + +@test 'deleting protected dummy instance fails' { + run -1 limactl delete --force "$NAME" + assert_output --partial 'instance is protected' + assert_file_exists "$LIMA_HOME/$NAME/protected" +} + +@test 'unprotecting the dummy instance succeeds' { + run -0 limactl unprotect "$NAME" + assert_output --regexp "Unprotected \\\\\"$NAME\\\\\"" + assert_file_not_exists "$LIMA_HOME/$NAME/protected" +} + +@test 'unprotecting it again shows a warning, but succeeds' { + run -0 limactl unprotect "$NAME" + assert_output --partial "isn't protected. Skipping" + assert_file_not_exists "$LIMA_HOME/$NAME/protected" +} + +@test 'deleting unprotected dummy instance succeeds' { + run -0 limactl delete --force "$NAME" + assert_output --regexp "Deleted \\\\\"$NAME\\\\\"" +}