diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml index 78b37e92..4600167f 100644 --- a/.github/workflows/compatibility.yml +++ b/.github/workflows/compatibility.yml @@ -1,25 +1,23 @@ --- -# This is a basic workflow to help you get started with Actions - name: CI -# Controls when the workflow will run +# Full compatibility suite: the e2e matrix over every supported image +# (docs/compatibility.md -> Makefile E2E_FULL_IMAGES), both backends. Runs on main +# pushes and CI-labelled PRs, only when distrobox files changed. The per-PR gating +# subset is e2e.yml; both share the matrix logic in e2e-matrix.yml. on: - # Triggers the workflow on push or pull request events but only for the master branch push: branches: [main] pull_request: branches: [main] types: [opened, synchronize, ready_for_review, edited, labeled] - - # Allows you to run this workflow manually from the Actions tab workflow_dispatch: inputs: run_always: description: 'Run even if no files are changed' required: true type: boolean -# Check if we indeed modified distrobox stuff + jobs: check_changes: runs-on: ubuntu-latest @@ -29,201 +27,23 @@ jobs: outputs: distrobox_changed: ${{ steps.check_file_changed.outputs.distrobox_changed }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: # Checkout as many commits as needed for the diff fetch-depth: 2 - - # Fetch from compatibility table all the distros supported - id: check_file_changed run: | - if git diff --name-only HEAD^ HEAD | grep -E '\.go$|go\.mod|go\.sum|Makefile|docs/compatibility\.md|internal/inside-distrobox/assets/'; then - echo "::set-output name=distrobox_changed::True" + if git diff --name-only HEAD^ HEAD | grep -E '\.go$|go\.mod|go\.sum|Makefile|docs/compatibility\.md|internal/inside-distrobox/assets/|hack/ci/'; then + echo "distrobox_changed=True" >> "$GITHUB_OUTPUT" else - echo "::set-output name=distrobox_changed::False" + echo "distrobox_changed=False" >> "$GITHUB_OUTPUT" fi - # Prepare distros matrix - setup: - runs-on: ubuntu-latest + e2e: needs: check_changes - outputs: - targets: ${{ steps.set-matrix.outputs.targets }} if: >- needs.check_changes.outputs.distrobox_changed == 'True' || github.event.inputs.run_always == 'True' - steps: - - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - # Fetch from compatibility table all the distros supported - - id: set-matrix - run: | - skip_list="bazzite|chimera|slackware|stream8|ublue|neon|steamos" - - echo "::set-output name=targets::$(sed -n -e '/| Alma/,/| Void/ p' docs/compatibility.md | - cut -d'|' -f 4 | - sed 's/
/\n/g' | - tr -d ' ' | - sed '/^[[:space:]]*$/d' | - sort -u | grep -Ev "${skip_list}" | - jq -R -s -c 'split("\n")[:-1]')" - - run: - runs-on: ubuntu-latest - needs: setup - timeout-minutes: 30 - strategy: - fail-fast: false - matrix: - distribution: ${{fromJSON(needs.setup.outputs.targets)}} - container_manager: ["podman"] #, "docker"] - env: - XDG_CACHE_HOME: "/tmp/" - DBX_CONTAINER_MANAGER: ${{ matrix.container_manager }} - - steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Setup Go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 - with: - go-version-file: go.mod - - - name: Build distrobox binary - run: | - make build - cp ./bin/distrobox ./distrobox - - # Ensure distrobox create works: - - name: Distrobox create - shell: 'script -q -e -c "bash {0}"' - run: | - image=${{ matrix.distribution }} - container_name="$(basename "${image}" | sed -E 's/[:.]/-/g')" - ${DBX_CONTAINER_MANAGER} pull "${image}" - # ./distrobox create --yes --absolutely-disable-root-password-i-am-really-positively-sure -i "${image}" --name "${container_name}" - case "${container_name}" in - *init*) - echo "SYSTEMD DETECTED: creating container with --init..." - ./distrobox create --yes -i "${image}" --hostname "${container_name}" --name "${container_name}" --init --unshare-all - ;; - *) - ./distrobox create --yes -i "${image}" --hostname "${container_name}" --name "${container_name}" - ;; - esac - - # Ensure distrobox enter and init works: - - name: Distrobox enter - init - shell: 'script -q -e -c "bash {0}"' - run: | - image=${{ matrix.distribution }} - container_name="$(basename "${image}" | sed -E 's/[:.]/-/g')" - case "${container_name}" in - *init*) - echo "SYSTEMD DETECTED: performing systemctl check..." - ./distrobox enter --name "${container_name}" -- systemctl is-system-running | grep -E "degraded|running|starting" - ;; - *) - ./distrobox enter --name "${container_name}" -- whoami - ;; - esac - - # Ensure distrobox enter and init works: - - name: Distrobox enter - user - shell: 'script -q -e -c "bash {0}"' - run: | - image=${{ matrix.distribution }} - container_name="$(basename "${image}" | sed -E 's/[:.]/-/g')" - # Assert that distrobox exported binary indeed works - set -x - command_output="$(./distrobox enter --name "${container_name}" -- whoami | tr -d '\r' | tr -d '^@')" - expected_output="$(whoami)" - if [ "$command_output" != "$expected_output" ]; then - exit 1 - fi - - # Ensure distrobox enter and init works: - - name: Distrobox enter - command - shell: 'script -q -e -c "bash {0}"' - run: | - image=${{ matrix.distribution }} - container_name="$(basename "${image}" | sed -E 's/[:.]/-/g')" - # Assert that distrobox exported binary indeed works - set -x - command_output="$(./distrobox enter --name "${container_name}" -- uname -n | tr -d '\r' | tr -d '^@')" - expected_output="${container_name}" - if [ "$command_output" != "$expected_output" ]; then - exit 1 - fi - - # Ensure distrobox export works: - - name: Distrobox export - shell: 'script -q -e -c "bash {0}"' - run: | - image=${{ matrix.distribution }} - container_name="$(basename "${image}" | sed -E 's/[:.]/-/g')" - ./distrobox enter "${container_name}" -- distrobox-export --bin /bin/uname --export-path ${HOME}/ - # Assert that distrobox exported binary indeed works - set -x - command_output="$(${HOME}/uname -n | tr -d '\r' | tr -d '^@')" - expected_output="${container_name}" - if [ "$command_output" != "$expected_output" ]; then - exit 1 - fi - - # Ensure distrobox export works: - - name: Distrobox export - sudo - if: matrix.container_manager != 'docker' - shell: 'script -q -e -c "bash {0}"' - run: | - image=${{ matrix.distribution }} - container_name="$(basename "${image}" | sed -E 's/[:.]/-/g')" - ./distrobox enter "${container_name}" -- distrobox-export --sudo --bin /bin/uname --export-path ${HOME}/ - # Assert that distrobox exported binary indeed works - set -x - command_output="$(${HOME}/uname -n | tr -d '\r' | tr -d '^@')" - expected_output="${container_name}" - if [ "$command_output" != "$expected_output" ]; then - exit 1 - fi - - # Ensure distrobox upgrade works: - - name: Distrobox upgrade - run: | - image=${{ matrix.distribution }} - container_name="$(basename "${image}" | sed -E 's/[:.]/-/g')" - ./distrobox upgrade "${container_name}" - - # Ensure distrobox list works: - - name: Distrobox list - run: | - image=${{ matrix.distribution }} - container_name="$(basename "${image}" | sed -E 's/[:.]/-/g')" - ./distrobox list | grep "${container_name}" | grep "${image}" | grep -E "Up|running" - - # Ensure distrobox stop works: - - name: Distrobox stop - run: | - image=${{ matrix.distribution }} - container_name="$(basename "${image}" | sed -E 's/[:.]/-/g')" - ./distrobox stop --yes "${container_name}" - - # Ensure distrobox rm works: - - name: Distrobox logs - if: ${{ always() }} - run: | - image=${{ matrix.distribution }} - container_name="$(basename "${image}" | sed -E 's/[:.]/-/g')" - $DBX_CONTAINER_MANAGER logs "${container_name}" - - # Ensure distrobox rm works: - - name: Distrobox rm - if: ${{ always() }} - run: | - image=${{ matrix.distribution }} - container_name="$(basename "${image}" | sed -E 's/[:.]/-/g')" - ./distrobox rm --force "${container_name}" - $DBX_CONTAINER_MANAGER rmi -f "${image}" + uses: ./.github/workflows/e2e-matrix.yml + with: + images_var: E2E_FULL_IMAGES diff --git a/.github/workflows/e2e-matrix.yml b/.github/workflows/e2e-matrix.yml new file mode 100644 index 00000000..a6444e14 --- /dev/null +++ b/.github/workflows/e2e-matrix.yml @@ -0,0 +1,44 @@ +--- +name: e2e-matrix + +# Reusable: run the per-distro e2e matrix (image list x both backends) via +# `make e2e-one`. The image list is read from the Makefile variable named by +# `images_var`, so the list lives only there. +on: + workflow_call: + inputs: + images_var: + description: Makefile variable holding the image list (e.g. E2E_IMAGES). + required: true + type: string + +jobs: + setup: + runs-on: ubuntu-latest + outputs: + images: ${{ steps.set.outputs.images }} + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - id: set + env: + VAR: ${{ inputs.images_var }} + run: echo "images=$(make -s print-"$VAR" | jq -R -c 'split(" ") | map(select(length > 0))')" >> "$GITHUB_OUTPUT" + + e2e: + name: ${{ matrix.image }} (${{ matrix.backend }}) + needs: setup + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + image: ${{ fromJSON(needs.setup.outputs.images) }} + backend: [podman, docker] + env: + XDG_CACHE_HOME: "/tmp/" + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + with: + go-version-file: go.mod + - run: make e2e-one IMAGE="${{ matrix.image }}" CM="${{ matrix.backend }}" diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 00000000..c3c375c3 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,39 @@ +--- +name: E2E + +# Gating suite on EVERY pull request: the e2e matrix over the gating image subset +# (Makefile E2E_IMAGES) plus the distro-agnostic commands. The full matrix runs in +# compatibility.yml (main / CI-labelled); both share e2e-matrix.yml. +on: + pull_request: + branches: [main] + types: [opened, synchronize, reopened, ready_for_review] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +jobs: + e2e: + uses: ./.github/workflows/e2e-matrix.yml + with: + images_var: E2E_IMAGES + + # Distro-agnostic subcommands, once per backend on a fast image. + commands: + name: commands (${{ matrix.backend }}) + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + backend: [podman, docker] + env: + XDG_CACHE_HOME: "/tmp/" + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + with: + go-version-file: go.mod + - run: make e2e-commands CM="${{ matrix.backend }}" diff --git a/Makefile b/Makefile index 5e78a7ad..9e5fd7b3 100644 --- a/Makefile +++ b/Makefile @@ -79,3 +79,61 @@ lint: .PHONY: lint-fix lint-fix: $(GO_BUILD_ENV) golangci-lint run --fix + +# --------------------------------------------------------------------------- +# Local e2e via the hack/ci/*.sh scripts, on the freshly-built binary. Pulled +# images are kept so repeated runs do not re-download. Targets: +# e2e mirrors the per-PR gate (.github/workflows/e2e.yml): the gating +# image subset + commands.sh, on both backends (podman, docker). +# e2e-full the whole compatibility matrix (docs/compatibility.md) + commands. +# e2e-one one e2e.sh run for a single image + backend; override IMAGE=/CM=. +# e2e-commands just commands.sh on one image/backend; override IMAGE=/CM=. +# --------------------------------------------------------------------------- +IMAGE ?= docker.io/library/alpine:latest +CM ?= podman +E2E_ENV := DBX="$(CURDIR)/bin/distrobox" DBX_E2E_KEEP_IMAGE=1 + +E2E_IMAGES := \ + docker.io/library/alpine:latest \ + docker.io/library/debian:stable \ + docker.io/library/ubuntu:24.04 \ + docker.io/library/fedora:latest \ + registry.opensuse.org/opensuse/distrobox:latest \ + docker.io/library/archlinux:latest \ + registry.access.redhat.com/ubi9/ubi-init + +# Full compatibility matrix, derived from docs/compatibility.md like compatibility.yml +# (no drift). Lazy '=' so the pipeline runs only when e2e-full is invoked. +E2E_FULL_SKIP := bazzite|chimera|slackware|stream8|ublue|neon|steamos +E2E_FULL_IMAGES = $(shell sed -n -e '/| Alma/,/| Void/ p' docs/compatibility.md | cut -d'|' -f 4 | sed 's/
/\n/g' | tr -d ' ' | sed '/^[[:space:]]*$$/d' | sort -u | grep -Ev '$(E2E_FULL_SKIP)') + +# print- — echo a make variable; the CI matrices build from these lists. +.PHONY: print-% +print-%: + @echo '$($*)' + +.PHONY: e2e-one +e2e-one: build + echo ">>> e2e: $(IMAGE) / $(CM)"; \ + $(E2E_ENV) hack/ci/e2e.sh "$(IMAGE)" "$(CM)" + +.PHONY: e2e-commands +e2e-commands: build + echo ">>> commands: $(IMAGE) / $(CM)"; \ + $(E2E_ENV) hack/ci/commands.sh "$(IMAGE)" "$(CM)" + +.PHONY: e2e +e2e: build + @for img in $(E2E_IMAGES); do \ + for cm in podman docker; do \ + $(MAKE) --no-print-directory e2e-one IMAGE="$$img" CM="$$cm" || exit 1; \ + done; \ + done + @for cm in podman docker; do \ + $(MAKE) --no-print-directory e2e-commands CM="$$cm" || exit 1; \ + done + +# Full compatibility sweep (100+ images x 2 backends); local twin of compatibility.yml. +.PHONY: e2e-full +e2e-full: + $(MAKE) --no-print-directory e2e E2E_IMAGES="$(E2E_FULL_IMAGES)" diff --git a/hack/ci/commands.sh b/hack/ci/commands.sh new file mode 100755 index 00000000..196c86a9 --- /dev/null +++ b/hack/ci/commands.sh @@ -0,0 +1,245 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: GPL-3.0-only +# +# This file is part of the distrobox project: +# https://github.com/89luca89/distrobox +# +# Copyright (C) 2021 distrobox contributors +# +# distrobox is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 3 +# as published by the Free Software Foundation. +# +# distrobox is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with distrobox; if not, see . +# +# usage: commands.sh + +set -u + +IMAGE="${1:?usage: commands.sh }" +MODE="${2:?backend required: podman|docker}" + +case "${MODE}" in + podman) CM=podman ;; + docker) CM=docker ;; + *) + printf 'unknown backend: %s (use podman|docker)\n' "${MODE}" >&2 + exit 2 + ;; +esac +export DBX_CONTAINER_MANAGER="${CM}" +DBX="${DBX:-distrobox}" + +dbx() +{ + "${DBX}" "$@" +} +engine() +{ + "${CM}" "$@" +} + +fails=0 +pass() +{ + printf ' \033[1;32mPASS\033[0m %s\n' "${1}" +} +fail() +{ + printf ' \033[1;31mFAIL\033[0m %s\n' "${1}" + fails=$((fails + 1)) + if [ -n "${2:-}" ]; then + printf '%s\n' "${2}" | tr '\r' '\n' | sed 's/^/ | /' + fi +} +ok() +{ + desc="${1}" + shift + if out="$("$@" < /dev/null 2>&1)"; then pass "${desc}"; else fail "${desc}" "${out}"; fi +} +count() +{ + engine ps -a -q 2> /dev/null | wc -l | tr -d ' ' +} +warmup() +{ + dbx enter --name "${1}" -- true < /dev/null > /dev/null 2>&1 || true +} +eq() +{ + if [ "${2}" = "${3}" ]; then pass "${1}"; else fail "${1}" "got '${2}', want '${3}'"; fi +} +contains() +{ + if printf '%s' "${2}" | grep -qE "${3}"; then pass "${1}"; else fail "${1}" "want /${3}/ in: ${2}"; fi +} +enter_out() +{ + b="${1}" + shift + dbx enter --name "${b}" -- "$@" < /dev/null 2> /dev/null | tr -d '\r\000\n' +} + +printf '\n== commands: image=%s backend=%s ==\n' "${IMAGE}" "${MODE}" +engine pull "${IMAGE}" > /dev/null 2>&1 || true + +# Captured while pristine, to gate the destructive --all tests (which act on EVERY +# distrobox) so a developer's own boxes are never touched. CI runners are clean. +pre_boxes="$(dbx list 2> /dev/null | tail -n +2 | grep -c .)" + +# ---- ephemeral: a temporary box, auto-removed on exit ---- +before="$(count)" +ok "ephemeral runs a command" dbx ephemeral --yes --image "${IMAGE}" -- true +after="$(count)" +if [ "${before}" = "${after}" ]; then + pass "ephemeral leaves no leftover container" +else + fail "ephemeral leaves no leftover container" "before=${before} after=${after}" +fi + +# ---- flag checks that spin no container (fast) ---- +ok "--version prints" dbx --version +contains "create --compatibility lists images" "$(dbx create --compatibility < /dev/null 2>&1)" 'alpine|ubuntu|fedora|debian' +contains "create --dry-run prints the engine command" "$(dbx create --dry-run --image "${IMAGE}" --name dbx-cmd-dry < /dev/null 2>&1)" "${CM}.*create" +if dbx list 2> /dev/null | grep -q dbx-cmd-dry; then fail "create --dry-run creates nothing" "dbx-cmd-dry present"; else pass "create --dry-run creates nothing"; fi +nc="$(dbx list --no-color < /dev/null 2>&1)" +if printf '%s' "${nc}" | grep -q "$(printf '\033')"; then fail "list --no-color emits no ANSI" "escape found"; else pass "list --no-color emits no ANSI"; fi +ok "ls alias resolves to list" dbx ls + +# ---- base box -> generate-entry + clone (guarded on create success) ---- +gbox="dbx-cmd-genentry" +cbox="${gbox}-clone" +if cout="$(dbx create --yes --no-entry --image "${IMAGE}" --name "${gbox}" < /dev/null 2>&1)"; then + pass "create (base box)" + # First enter triggers setup; sudo writes a marker into the container rootfs + # (not a host-shared path) so a clone must carry it. + dbx enter --name "${gbox}" -- sudo -n touch /dbx-clone-marker < /dev/null > /dev/null 2>&1 || true + + desktop="${XDG_DATA_HOME:-${HOME}/.local/share}/applications/${gbox}.desktop" + geout="$(dbx generate-entry "${gbox}" < /dev/null 2>&1)" + if [ -f "${desktop}" ]; then pass "generate-entry writes a .desktop"; else fail "generate-entry writes a .desktop" "expected ${desktop}; generate-entry said: ${geout}"; fi + dbx generate-entry --delete "${gbox}" < /dev/null > /dev/null 2>&1 || true + if [ ! -f "${desktop}" ]; then pass "generate-entry --delete removes it"; else fail "generate-entry --delete removes it" "${desktop} still present"; fi + + dbx stop --yes "${gbox}" < /dev/null > /dev/null 2>&1 || true + if clout="$(dbx create --yes --clone "${gbox}" --name "${cbox}" < /dev/null 2>&1)"; then + pass "clone a stopped box" + warmup "${cbox}" + marker="$(dbx enter --name "${cbox}" -- sh -c 'test -f /dbx-clone-marker && printf yes' < /dev/null 2>&1 | tr -d '\r\000\n')" + if [ "${marker}" = "yes" ]; then pass "clone carries the rootfs marker"; else fail "clone carries the rootfs marker" "marker /dbx-clone-marker missing; enter returned: '${marker}'"; fi + else + fail "clone a stopped box" "${clout}" + fail "clone carries the rootfs marker" "no clone created" + fi +else + fail "create (base box)" "${cout}" +fi + +# ---- create/enter flags + engine-level mechanics, on one box ---- +fbox="dbx-cmd-flags" +vdir="$(mktemp -d)" +: > "${vdir}/vfile" +if fout="$(dbx create --yes --no-entry --image "${IMAGE}" --name "${fbox}" \ + --hostname dbxflagshost \ + --volume "${vdir}:/mnt/vol" \ + --init-hooks 'touch /dbx-hook-ran' \ + --additional-flags '--env CREATEVAR=cval' < /dev/null 2>&1)"; then + pass "create with a flag bundle" + warmup "${fbox}" + eq "--hostname sets the hostname" "$(enter_out "${fbox}" uname -n)" "dbxflagshost" + ok "--volume mounts a host dir" dbx enter --name "${fbox}" -- test -f /mnt/vol/vfile + ok "--init-hooks runs at setup" dbx enter --name "${fbox}" -- test -f /dbx-hook-ran + eq "create --additional-flags forwards --env" "$(enter_out "${fbox}" printenv CREATEVAR)" "cval" + eq "enter --additional-flags forwards --env" \ + "$(dbx enter --name "${fbox}" --additional-flags '--env ENTERVAR=eval' -- printenv ENTERVAR < /dev/null 2> /dev/null | tr -d '\r\000\n')" "eval" + hm="${HOME}/.dbx-cmd-home-$$" + : > "${hm}" + ok 'host $HOME is shared into the box' dbx enter --name "${fbox}" -- test -f "${hm}" + rm -f "${hm}" + eq "host env var forwarded" \ + "$(MY_DBX_VAR=fwd dbx enter --name "${fbox}" -- printenv MY_DBX_VAR < /dev/null 2> /dev/null | tr -d '\r\000\n')" "fwd" + badv="$(DBX_CMD_BAD='has$dollar' dbx enter --name "${fbox}" -- printenv DBX_CMD_BAD < /dev/null 2> /dev/null | tr -d '\r\000\n')" + if [ -z "${badv}" ]; then pass "env var with special chars is filtered"; else fail "env var with special chars is filtered" "leaked '${badv}'"; fi + pn="$(enter_out "${fbox}" printenv PATH)" + pc="$(dbx enter --name "${fbox}" --clean-path -- printenv PATH < /dev/null 2> /dev/null | tr -d '\r\000\n')" + if [ -n "${pc}" ] && [ "${pc}" != "${pn}" ] && printf '%s' ":${pc}:" | grep -q ':/usr/bin:'; then pass "enter --clean-path resets PATH to FHS"; else fail "enter --clean-path resets PATH to FHS" "normal='${pn}' clean='${pc}'"; fi + eq 'enter --no-workdir starts in $HOME' \ + "$(dbx enter --name "${fbox}" --no-workdir -- pwd < /dev/null 2> /dev/null | tr -d '\r\000\n')" "${HOME}" + contains "enter --dry-run prints the engine command" "$(dbx enter --name "${fbox}" --dry-run -- true < /dev/null 2>&1)" "${CM}.*exec" +else + fail "create with a flag bundle" "${fout}" +fi +dbx rm --force "${fbox}" < /dev/null > /dev/null 2>&1 || true +rm -rf "${vdir}" + +# ---- --home (custom HOME) + rm --rm-home ---- +chome="$(mktemp -d)/box-home" +if dbx create --yes --no-entry --image "${IMAGE}" --name dbx-cmd-home --home "${chome}" < /dev/null > /dev/null 2>&1; then + warmup dbx-cmd-home + eq "--home sets a custom HOME" "$(enter_out dbx-cmd-home printenv HOME)" "${chome}" + dbx rm --rm-home --force dbx-cmd-home < /dev/null > /dev/null 2>&1 || true + if [ -d "${chome}" ]; then fail "rm --rm-home removes the custom home" "${chome} still present"; else pass "rm --rm-home removes the custom home"; fi +else + fail "--home sets a custom HOME" "create --home failed" + fail "rm --rm-home removes the custom home" "no box created" +fi +rm -rf "${chome%/*}" + +# ---- assemble: create/rm boxes from a manifest ---- +mani="$(mktemp)" +printf '[dbx-cmd-assemble]\nimage=%s\n' "${IMAGE}" > "${mani}" +ok "assemble create" dbx assemble create --file "${mani}" +if dbx list | grep -q dbx-cmd-assemble; then pass "assemble created the box"; else fail "assemble created the box" "dbx-cmd-assemble not in distrobox list"; fi +ok "assemble rm" dbx assemble rm --file "${mani}" +if dbx list | grep -q dbx-cmd-assemble; then fail "assemble rm removed the box" "dbx-cmd-assemble still in distrobox list"; else pass "assemble rm removed the box"; fi +rm -f "${mani}" + +# ---- assemble: --dry-run, --name (single entry), --replace ---- +mani2="$(mktemp)" +printf '[dbx-cmd-asm1]\nimage=%s\n\n[dbx-cmd-asm2]\nimage=%s\n' "${IMAGE}" "${IMAGE}" > "${mani2}" +if dbx assemble create --file "${mani2}" --dry-run < /dev/null > /dev/null 2>&1 && ! dbx list 2> /dev/null | grep -q dbx-cmd-asm; then pass "assemble --dry-run creates nothing"; else fail "assemble --dry-run creates nothing" "$(dbx list 2> /dev/null | grep dbx-cmd-asm || true)"; fi +ok "assemble create --name (single entry)" dbx assemble create --file "${mani2}" --name dbx-cmd-asm1 +if dbx list 2> /dev/null | grep -q dbx-cmd-asm1 && ! dbx list 2> /dev/null | grep -q dbx-cmd-asm2; then pass "assemble --name builds only the named entry"; else fail "assemble --name builds only the named entry" "$(dbx list 2> /dev/null | grep dbx-cmd-asm || true)"; fi +ok "assemble create --replace" dbx assemble create --file "${mani2}" --name dbx-cmd-asm1 --replace +dbx assemble rm --file "${mani2}" < /dev/null > /dev/null 2>&1 || true +rm -f "${mani2}" + +# ---- multi-box --all flags (act on EVERY distrobox; guarded for safety) ---- +# Runs only from a clean start so a dev's own boxes are never touched (CI is clean). +if [ "${pre_boxes}" = "0" ]; then + dbx create --yes --no-entry --image "${IMAGE}" --name dbx-cmd-all1 < /dev/null > /dev/null 2>&1 || true + dbx create --yes --no-entry --image "${IMAGE}" --name dbx-cmd-all2 < /dev/null > /dev/null 2>&1 || true + warmup dbx-cmd-all1 + warmup dbx-cmd-all2 + appdir="${XDG_DATA_HOME:-${HOME}/.local/share}/applications" + icon="${HOME}/.dbx-cmd-icon.png" + : > "${icon}" + dbx generate-entry --all < /dev/null > /dev/null 2>&1 || true + if [ -f "${appdir}/dbx-cmd-all1.desktop" ] && [ -f "${appdir}/dbx-cmd-all2.desktop" ]; then pass "generate-entry --all covers every box"; else fail "generate-entry --all covers every box" "missing .desktop in ${appdir}"; fi + dbx generate-entry dbx-cmd-all1 --icon "${icon}" < /dev/null > /dev/null 2>&1 || true + contains "generate-entry --icon sets a custom icon" "$(cat "${appdir}/dbx-cmd-all1.desktop" 2> /dev/null)" "Icon=${icon}" + dbx generate-entry --all --delete < /dev/null > /dev/null 2>&1 || true + rm -f "${icon}" + ok "stop --all stops every box" dbx stop --all --yes + ok "upgrade --all upgrades every box" dbx upgrade --all + dbx rm --all --force < /dev/null > /dev/null 2>&1 || true + if [ "$(dbx list 2> /dev/null | tail -n +2 | grep -c .)" = "0" ]; then pass "rm --all removes every box"; else fail "rm --all removes every box" "boxes remain"; fi +else + printf ' \033[1;33mSKIP\033[0m --all flag tests (%s pre-existing distrobox(es); would be destructive)\n' "${pre_boxes}" +fi + +# ---- cleanup ---- +dbx rm --force "${gbox}" "${cbox}" "${fbox}" < /dev/null > /dev/null 2>&1 || true +if [ -z "${DBX_E2E_KEEP_IMAGE:-}" ]; then + engine rmi -f "${IMAGE}" > /dev/null 2>&1 || true +fi + +printf '== commands result: %d failed ==\n' "${fails}" +[ "${fails}" -eq 0 ] diff --git a/hack/ci/e2e.sh b/hack/ci/e2e.sh new file mode 100755 index 00000000..706b65ed --- /dev/null +++ b/hack/ci/e2e.sh @@ -0,0 +1,160 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: GPL-3.0-only +# +# This file is part of the distrobox project: +# https://github.com/89luca89/distrobox +# +# Copyright (C) 2021 distrobox contributors +# +# distrobox is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 3 +# as published by the Free Software Foundation. +# +# distrobox is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with distrobox; if not, see . +# +# usage: e2e.sh + +set -u + +IMAGE="${1:?usage: e2e.sh }" +MODE="${2:?backend required: podman|docker}" + +case "${MODE}" in + podman) CM=podman ;; + docker) CM=docker ;; + *) + printf 'unknown backend: %s (use podman|docker)\n' "${MODE}" >&2 + exit 2 + ;; +esac +export DBX_CONTAINER_MANAGER="${CM}" +DBX="${DBX:-distrobox}" + +dbx() +{ + "${DBX}" "$@" +} +engine() +{ + "${CM}" "$@" +} + +name="$(basename "${IMAGE}" | sed -E 's/[:.]/-/g')" +fails=0 + +pass() +{ + printf ' \033[1;32mPASS\033[0m %s\n' "${1}" +} +fail() +{ + printf ' \033[1;31mFAIL\033[0m %s\n' "${1}" + fails=$((fails + 1)) + if [ -n "${2:-}" ]; then + # \r -> \n so distrobox progress output (carriage returns) is readable + # line-by-line instead of one smeared line. + printf '%s\n' "${2}" | tr '\r' '\n' | sed 's/^/ | /' + fi +} +eq() +{ + if [ "${2}" = "${3}" ]; then pass "${1}"; else fail "${1}" "got '${2}', want '${3}'"; fi +} +ok() +{ + desc="${1}" + shift + if out="$("$@" < /dev/null 2>&1)"; then + pass "${desc}" + else + fail "${desc}" "${out}" + fi +} +enter_out() +{ + dbx enter --name "${name}" -- "$@" < /dev/null 2> /dev/null | tr -d '\r\000\n' +} + +# dump the box's logs (distrobox-init setup output). Only meaningful when the +# container itself won't come up, so it's called only on create/restart failure. +dump_logs() +{ + printf ' --- %s logs for %s (tail) ---\n' "${MODE}" "${name}" + engine logs "${name}" 2>&1 | tail -40 | sed 's/^/ | /' || true +} + +printf '\n== e2e: image=%s backend=%s name=%s ==\n' "${IMAGE}" "${MODE}" "${name}" + +case "${name}" in + *init*) create_flags="--pull --yes --image ${IMAGE} --name ${name} --init --unshare-all" ;; + *) create_flags="--pull --yes --image ${IMAGE} --name ${name} --additional-packages nano" ;; +esac +# shellcheck disable=SC2086 # create_flags is intentionally word-split +if create_out="$(dbx create ${create_flags} < /dev/null 2>&1)"; then + pass "create" +else + fail "create" "${create_out}" + dump_logs + printf '== result: %d failed ==\n' "${fails}" + exit 1 +fi + +# Warm up: the first enter STARTS the container, and `podman start` echoes the +# box name to stdout (plus first-boot setup streams). Do it once, discarded, so +# the checks below capture only their own command's output. +ok "enter" dbx enter --name "${name}" -- true + +# --- systemd boots for --init images --- +case "${name}" in + *init*) + sysstate="$(enter_out systemctl is-system-running)" + case "${sysstate}" in + running | degraded | starting) pass "systemd is running (--init): ${sysstate}" ;; + *) fail "systemd is running (--init)" "got '${sysstate}'" ;; + esac + ;; + *) ;; +esac + +# --- core promises (hard) --- +eq "enter runs as the host user" "$(enter_out whoami)" "$(whoami)" +sudo_out="$(enter_out sudo -n whoami)" +case "${sudo_out}" in + *root*) pass "passwordless sudo inside the box" ;; + *) fail "passwordless sudo inside the box" "want 'root' in output, got '${sudo_out}'" ;; +esac + +# --- additional-packages: nano (plain images only; init skips it, see create) --- +case "${name}" in + *init*) ;; + *) ok "additional-packages installed nano" dbx enter --name "${name}" -- sh -c 'command -v nano' ;; +esac + +# --- lifecycle (hard) --- +ok "upgrade" dbx upgrade "${name}" +ok "stop" dbx stop --yes "${name}" +# restart: entering a stopped box must bring it back up. timeout(1) guards a +# stuck setup, but it needs a real executable (dbx is a shell function), so call +# the binary directly. +restart_out="$(timeout 180 "${DBX}" enter --name "${name}" -- true < /dev/null 2>&1)" +rc=$? +if [ "${rc}" -eq 0 ]; then + pass "enter restarts a stopped box" +else + fail "enter restarts a stopped box" "${restart_out}" + dump_logs +fi + +ok "rm" dbx rm --force "${name}" +if [ -z "${DBX_E2E_KEEP_IMAGE:-}" ]; then + engine rmi -f "${IMAGE}" > /dev/null 2>&1 || true +fi + +printf '== result: %d failed ==\n' "${fails}" +[ "${fails}" -eq 0 ]