mirror of
https://github.com/89luca89/distrobox.git
synced 2026-08-17 16:34:42 -05:00
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:
@@ -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
|
||||
|
||||
@@ -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 }}"
|
||||
@@ -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 }}"
|
||||
@@ -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)"
|
||||
|
||||
Executable
+245
@@ -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
@@ -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 ]
|
||||
Reference in New Issue
Block a user