ci(e2e): verify the v2 binary end-to-end across distros and engines

Unit tests can't prove the rewritten binary actually works on each supported
distro and on both podman and docker; this exercises the real thing.

Move the tests in a set of simple scripts to run them.
Make a smaller e2e test for CLI and Images that runs on every PR
Keep the full compatibility suite in a separate job for main merge and
manual runs.

Signed-off-by: Luca Di Maio <luca.dimaio1@gmail.com>
This commit is contained in:
Luca Di Maio
2026-08-07 14:46:19 +02:00
parent 1f3fa8109e
commit f01efac527
6 changed files with 558 additions and 192 deletions
+12 -192
View File
@@ -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/<br>/\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
+44
View File
@@ -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 }}"
+39
View File
@@ -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 }}"
+58
View File
@@ -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/<br>/\n/g' | tr -d ' ' | sed '/^[[:space:]]*$$/d' | sort -u | grep -Ev '$(E2E_FULL_SKIP)')
# print-<VAR> — 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)"
+245
View File
@@ -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 <http://www.gnu.org/licenses/>.
#
# usage: commands.sh <image> <podman|docker>
set -u
IMAGE="${1:?usage: commands.sh <image> <podman|docker>}"
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 ]
Executable
+160
View File
@@ -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 <http://www.gnu.org/licenses/>.
#
# usage: e2e.sh <image> <podman|docker>
set -u
IMAGE="${1:?usage: e2e.sh <image> <podman|docker>}"
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 ]