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:
parent
6487eb1757
commit
d4d347b616
|
@ -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
|
||||
|
|
|
@ -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 }}'
|
||||
|
|
|
@ -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
|
|
@ -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)
|
|
@ -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}
|
|
@ -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} -- "$@"
|
|
@ -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
|
|
@ -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())
|
Loading…
Reference in New Issue