workflows: Run unit tests in our tasks container

This reduces our tools like `ruff` to a single source of truth (as all
our other projects already run their unit tests and linting in the tasks
container). It also removes a lot of moving parts only relevant for CI.
In practice, us developers run the unit tests in toolbox or our own dev
machines anyway.

Move building the guide in the release workflow to the tasks container
as well.

Cherry-picked from main commit f16f1fc14b
This commit is contained in:
Martin Pitt 2024-02-13 11:51:44 +01:00 committed by Martin Pitt
parent 6487eb1757
commit d4d347b616
8 changed files with 16 additions and 404 deletions

View File

@ -10,7 +10,7 @@ jobs:
source:
runs-on: ubuntu-latest
container:
image: ghcr.io/cockpit-project/unit-tests
image: quay.io/cockpit/tasks:latest
options: --user root
permissions:
# create GitHub release

View File

@ -4,36 +4,32 @@ jobs:
unit-tests:
runs-on: ubuntu-22.04
permissions: {}
container:
image: quay.io/cockpit/tasks:latest
options: --user 1001
strategy:
matrix:
startarg:
- { make: 'check-memory', tag: 'latest' }
- { make: 'distcheck', tag: 'latest' }
target:
- check-memory
- distcheck
# this runs static code checks, unlike distcheck
- { make: 'check', tag: 'latest' }
- check
fail-fast: false
timeout-minutes: 60
env:
FORCE_COLOR: 1
TEST_BROWSER: firefox
CFLAGS: '-O2'
steps:
- name: Clone repository
uses: actions/checkout@v4
with:
# need this to also fetch tags
fetch-depth: 0
submodules: true
- name: Build unit test container if it changed
run: |
changes=$(git diff --name-only origin/${{ github.event.pull_request.base.ref }}..HEAD -- containers/unit-tests/)
if [ -n "${changes}" ]; then
containers/unit-tests/build
fi
- name: Run unit-tests container
- name: Run unit test
timeout-minutes: 30
run: |
containers/unit-tests/start \
--verbose \
--env=FORCE_COLOR=1 \
--env=CFLAGS='-O2' \
--env=EXTRA_DISTCHECK_CONFIGURE_FLAGS='${{ matrix.startarg.distcheck_flags }}' \
--image-tag='${{ matrix.startarg.tag }}' \
--make '${{ matrix.startarg.make }}'
./autogen.sh
make -j$(nproc) '${{ matrix.target }}'

View File

@ -1,26 +0,0 @@
ARG debian_arch=amd64
FROM docker.io/${debian_arch}/debian:testing
ARG personality=linux64
ENV personality=${personality}
COPY setup.sh /
RUN ${personality} /setup.sh ${personality} && rm -rf /setup.sh
# 'builder' user created in setup.sh
USER builder
WORKDIR /home/builder
ENV LANG=C.UTF-8
# HACK: unbreak distcheck on Debian: https://bugs.debian.org/1035546
ENV DEB_PYTHON_INSTALL_LAYOUT=deb
VOLUME /source
COPY entrypoint /
ENTRYPOINT ["/entrypoint"]
CMD ["/bin/bash"]
# for filtering from our 'exec' script
LABEL org.cockpit-project.container=unit-tests

View File

@ -1,71 +0,0 @@
# Cockpit unit test container
This container has all build dependencies and toolchains (GCC and clang) that we
want to exercise Cockpit with, mostly for `make distcheck` and `make check-memory`.
This container runs on [GitHub](.github/workflows/unit-tests.yml), but can be easily
run locally too.
It assumes that the Cockpit source git checkout is available in `/source`. It
will not modify that directory or take uncommitted changes into account, but it
will re-use an already existing `node_modules/` directory.
The scripts can use either podman (preferred) or docker. If you use docker, you
need to run all commands as root. With podman the containers work as either user
or root.
## Building
The `build` script will build the `cockpit/unit-tests` container.
## Running tests
You need to disable SELinux with `sudo setenforce 0` for this. There is no
other way for the container to access the files in your build tree (do *not*
use the `--volume` `:Z` option, as that will destroy the file labels on the
host).
Tests in that container get started with the `start` script. By default, this
script runs the unit tests on amd64. The script accepts a number of arguments
to modify its behaviour:
- `--env CC=othercc` to set the `CC` environment variable inside the container (ie:
to build with a different compiler)
- `--image-tag` to specify a different tag to use for the `cockpit/unit-tests` image
Additionally, a testing scenario can be provided with specifying a `make` target.
Supported scenarios are:
- `check-memory`: runs 'make check-memory' (ie: run the unit tests under valgrind)
- `distcheck`: runs 'make distcheck' and some related checks
- `pycheck`: runs browser unit tests against the Python bridge
Some examples:
$ ./start --make check-memory # run the valgrind tests on amd64
$ ./start --env=CC=clang --make check-memory # run the valgrind tests, compiled with clang
## Debugging tests
For interactive debugging, run a shell in the container:
$ ./start
You will find the cockpit source tree (from the host) mounted at `/source` in
the container. Run
$ /source/autogen.sh
to create a build tree, then you can run any make or other debugging command
interactively.
You can also attach to another container using the provided `exec` script. For example:
$ ./exec uname -a # run a command as the "builder" user
$ ./exec --root # start a shell as root
## More Info
* [Cockpit Project](https://cockpit-project.org)
* [Cockpit Development](https://github.com/cockpit-project/cockpit)

View File

@ -1,6 +0,0 @@
#!/bin/sh
set -eu
dir=$(dirname "$0")
podman build --build-arg debian_arch=amd64 --build-arg personality=linux64 -t ghcr.io/cockpit-project/unit-tests ${dir}

View File

@ -1,12 +0,0 @@
#!/bin/sh -e
export TEST_BROWSER=firefox
printf "Host: " && uname -srvm
. /usr/lib/os-release
printf "Container: \${NAME} \${VERSION} / " && ${personality} uname -nrvm
echo
set -ex
exec ${personality} -- "$@"

View File

@ -1,79 +0,0 @@
#!/bin/sh -ex
dependencies="\
appstream-util \
autoconf \
automake \
build-essential \
clang \
curl \
dbus \
firefox-esr \
flake8 \
gcc-multilib \
gdb \
git \
glib-networking \
glib-networking-dbgsym\
gtk-doc-tools \
gettext \
libc6-dbg \
libfontconfig1 \
libglib2.0-0-dbgsym \
libglib2.0-dev \
libgnutls28-dev \
libjavascript-minifier-xs-perl \
libjson-glib-dev \
libjson-perl \
libkrb5-dev \
libpam0g-dev \
libpcp-import1-dev \
libpcp-pmda3-dev \
libpcp3-dev \
libpolkit-agent-1-dev \
libpolkit-gobject-1-dev \
libssh-4-dbgsym \
libssh-dev \
libsystemd-dev \
mypy \
npm \
nodejs \
pkg-config \
python3 \
python3-mypy \
python3-pip \
python3-pytest-asyncio \
python3-pytest-cov \
python3-pytest-timeout \
ssh \
strace \
valgrind \
vulture \
xmlto \
xsltproc \
"
echo "deb http://deb.debian.org/debian-debug/ testing-debug main" > /etc/apt/sources.list.d/ddebs.list
echo "deb http://deb.debian.org/debian-debug/ testing-proposed-updates-debug main" >> /etc/apt/sources.list.d/ddebs.list
apt-get update
apt-get install -y --no-install-recommends eatmydata
DEBIAN_FRONTEND=noninteractive eatmydata apt-get install -y --no-install-recommends ${dependencies}
# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1057968
echo "deb http://deb.debian.org/debian unstable main" > /etc/apt/sources.list.d/unstable.list
apt-get update
apt-get install -y --no-install-recommends python3-flake8
rm /etc/apt/sources.list.d/unstable.list
adduser --gecos "Builder" builder
if [ "$(uname -m)" = "x86_64" ] ; then
# See https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1030835
pip install --break-system-packages ruff
fi
# minimize image
# useful command: dpkg-query --show -f '${package} ${installed-size}\n' | sort -k2n
dpkg -P --force-depends libgl1-mesa-dri libglx-mesa0 perl
rm -rf /var/cache/apt /var/lib/apt /var/log/* /usr/share/doc/ /usr/share/man/ /usr/share/help /usr/share/info

View File

@ -1,190 +0,0 @@
#!/usr/bin/python3
# This file is part of Cockpit.
#
# Copyright (C) 2022 Red Hat, Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
import argparse
import os
import shlex
import sys
import tempfile
from subprocess import run
def logged(func):
def wrapper(args, **kwargs):
print('+', shlex.join(args))
return func(args, **kwargs)
return wrapper
def git(*args):
run(['git', *args], check=True)
def git_output(*args):
return run(['git', *args], check=True, capture_output=True, text=True).stdout.strip()
def podman(*args, check=True):
if os.path.exists('/run/.toolboxenv'):
cmd = ['flatpak-spawn', '--host', 'podman', *args]
else:
cmd = ['podman', *args]
return run(cmd, check=check)
class PodmanTemporaryDirectory(tempfile.TemporaryDirectory):
"""TemporaryDirectory subclass capable of removing files owned by subuids"""
@classmethod
def _rmtree(cls, name, ignore_errors=False): # noqa: FBT002
del ignore_errors # can't remove or rename this kwarg
podman('unshare', 'rm', '-r', name)
def __enter__(self):
# Override the TemporaryDirectory behaviour of returning its name here
return self
class SourceDirectory(PodmanTemporaryDirectory):
def __init__(self):
super().__init__(prefix='cockpit-source.')
def prepare(self, args):
if args.branch:
opts = ['-c', 'advice.detachedHead=false', '-b', args.branch]
else:
opts = []
git('clone', '--recurse-submodule=vendor/*', *opts, '.', self.name)
if not args.head and not args.branch:
if stash := git_output('stash', 'create'):
git('-C', self.name, 'fetch', '--quiet', '--no-write-fetch-head', 'origin', stash)
git('-C', self.name, 'stash', 'apply', stash)
if not args.no_node_modules:
run([f'{self.name}/tools/node-modules', 'checkout'], check=True)
class ResultsDirectory(PodmanTemporaryDirectory):
def __init__(self):
super().__init__(prefix='cockpit-results.')
def copy_out(self, destination):
podman('unshare', 'cp', '-rT', self.name, destination)
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--verbose', '-v', action='store_true', help='Show commands when running them')
parser.add_argument('--results', metavar='DIRECTORY', help="Copy container /results to the given host directory")
group = parser.add_argument_group(title='Container options')
group.add_argument('--image', default='ghcr.io/cockpit-project/unit-tests', help='Container image to use')
group.add_argument('--image-tag', default='latest', help='Container image tag to use')
group.add_argument('--env', metavar='NAME=VAL', action='append', default=[],
help='Set an environment variable in the container')
group.add_argument('--network', action="store_true",
help="Enable network in the container (default: disabled)")
group.add_argument('--interactive', '-i', action="store_true",
help="Interactive mode (implied by no command or script)")
group.add_argument('--tty', '-t', action="store_true",
help="Allocate a pseudoterminal (implied by no command or script)")
group.add_argument('--user', help="Pass through the --user flag to podman")
group.add_argument('--entrypoint', metavar='CMD', help="Provide the --entrypoint flag to podman")
group.add_argument('--workdir', help="Provide the --workdir flag to podman")
group = parser.add_argument_group(title='What to build').add_mutually_exclusive_group()
group.add_argument('--head', action='store_true', help='Build the HEAD commit')
group.add_argument('-b', dest='branch', metavar='NAME', help='Build the named branch or tag')
group.add_argument('--work-tree', action='store_true',
help='Build the HEAD commit, plus changes on the filesystem (default)')
group = parser.add_argument_group(title='Preparation').add_mutually_exclusive_group()
group.add_argument('--no-node-modules', action='store_true',
help='Disable checking out node_modules/ during preparation')
group = parser.add_argument_group(title='Command to run').add_mutually_exclusive_group()
group.add_argument('-c', metavar='SCRIPT', dest='script', help="Run the provided shell script")
group.add_argument('--make-dist', action='store_true', help='Run `make dist`. Requires --results.')
group.add_argument('--make', metavar='TARGET', help='Run `make` on the given target')
# re: default=[]: https://github.com/python/cpython/issues/86020
group.add_argument('command', metavar='CMD', nargs='*', default=[], help="Run a normal command, with arguments")
args = parser.parse_args()
if args.results and os.path.exists(args.results):
parser.error(f'--results directory `{args.results}` already exists')
if args.make_dist and not args.results:
parser.error('--make-dist requires --results directory')
if args.verbose:
global run
run = logged(run)
with SourceDirectory() as source_dir, ResultsDirectory() as results_dir:
options = {
'--rm',
'--log-driver=none',
f'--volume={source_dir.name}:/source:Z,U',
}
if args.results:
options.add(f'--volume={results_dir.name}:/results:Z,U')
if not args.network:
options.add('--network=none')
if args.user:
options.add(f'--user={args.user}')
if args.entrypoint:
options.add(f'--entrypoint={args.entrypoint}')
if args.workdir:
options.add(f'--workdir={args.workdir}')
if args.interactive:
options.add('--interactive')
if args.tty:
options.add('--tty')
for keyval in args.env:
options.add(f'--env={keyval}')
command = []
if args.command:
command = args.command
elif args.script:
command = ['sh', '-c', args.script]
elif args.make:
command = ['sh', '-c', '/source/autogen.sh; exec make -j$(nproc) ' + shlex.quote(args.make)]
elif args.make_dist:
command = ['sh', '-c', 'cp -t /results $(/source/tools/make-dist)']
else:
options.update(['--tty', '--interactive'])
source_dir.prepare(args)
result = podman('run', *options, f'{args.image}:{args.image_tag}', *command)
if result.returncode == 0 and args.results:
results_dir.copy_out(args.results)
return result.returncode
if __name__ == '__main__':
sys.exit(main())