Compare commits

..

1 commit

Author SHA1 Message Date
Chris Kuehl
b2203fd5b1 WIP fix race 2018-08-01 14:23:29 -07:00
31 changed files with 247 additions and 377 deletions

View file

@ -1,60 +0,0 @@
name: CI
on: push
jobs:
build-and-test:
runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
manylinux_arch: x86_64
docker_image: debian:buster
- arch: arm64
manylinux_arch: aarch64
docker_image: arm64v8/debian:buster
- arch: ppc64le
manylinux_arch: ppc64le
docker_image: ppc64le/debian:buster
- arch: s390x
manylinux_arch: s390x
docker_image: s390x/debian:buster
env:
BASE_IMAGE: ${{ matrix.docker_image }}
steps:
- uses: actions/checkout@v2
- name: Set up QEMU
id: qemu
uses: docker/setup-qemu-action@v1
if: ${{ matrix.arch != 'amd64' }}
with:
image: tonistiigi/binfmt:latest
- name: Build Docker image
run: make docker-image
- name: Run python tests
run: docker run --rm -v $(pwd):/mnt:rw dumb-init-build /mnt/ci/docker-python-test
- name: Build Debian package
run: docker run --init --rm -v $(pwd):/mnt:rw dumb-init-build make -C /mnt builddeb
- name: Test built Debian package
# XXX: This uses the clean base image (not the build one) to make
# sure it installs in a clean image without any hidden dependencies.
run: docker run --rm -v $(pwd):/mnt:rw ${{ matrix.docker_image }} /mnt/ci/docker-deb-test
- name: Build wheels
run: sudo make python-dists-${{ matrix.manylinux_arch }}
- name: Upload build artifacts
uses: actions/upload-artifact@v2
with:
name: ${{ matrix.arch }}
path: dist

View file

@ -1,7 +1,8 @@
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0 rev: v1.2.3
hooks: hooks:
- id: autopep8-wrapper
- id: check-added-large-files - id: check-added-large-files
- id: check-docstring-first - id: check-docstring-first
- id: check-executables-have-shebangs - id: check-executables-have-shebangs
@ -11,33 +12,15 @@ repos:
- id: detect-private-key - id: detect-private-key
- id: double-quote-string-fixer - id: double-quote-string-fixer
- id: end-of-file-fixer - id: end-of-file-fixer
- id: flake8
- id: name-tests-test - id: name-tests-test
- id: requirements-txt-fixer - id: requirements-txt-fixer
- id: trailing-whitespace - id: trailing-whitespace
- repo: https://github.com/pre-commit/mirrors-autopep8
rev: v2.0.0
hooks:
- id: autopep8
- repo: https://github.com/pycqa/flake8
rev: 6.0.0
hooks:
- id: flake8
- repo: https://github.com/asottile/reorder_python_imports - repo: https://github.com/asottile/reorder_python_imports
rev: v3.9.0 rev: v1.0.1
hooks: hooks:
- id: reorder-python-imports - id: reorder-python-imports
args: ['--py3-plus']
- repo: https://github.com/Lucas-C/pre-commit-hooks - repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.3.1 rev: v1.1.5
hooks: hooks:
- id: remove-tabs - id: remove-tabs
- repo: https://github.com/asottile/pyupgrade
rev: v3.3.0
hooks:
- id: pyupgrade
args: ['--py3-plus']
- repo: https://github.com/asottile/add-trailing-comma
rev: v2.3.0
hooks:
- id: add-trailing-comma
args: ['--py36-plus']

19
.travis.yml Normal file
View file

@ -0,0 +1,19 @@
language: c
services:
- docker
matrix:
include:
- env: ITEST_TARGET=itest_trusty
- env: ITEST_TARGET=itest_xenial
- env: ITEST_TARGET=itest_bionic
- env: ITEST_TARGET=itest_stretch
- env: ITEST_TARGET=itest_tox
- os: linux-ppc64le
env: ITEST_TARGET=itest_stretch
script:
- make "$ITEST_TARGET"
after_script:
- ci/artifact-upload

View file

@ -29,11 +29,11 @@ The process to release a new version is:
4. Commit the changes and tag the commit like `v1.0.0`. 4. Commit the changes and tag the commit like `v1.0.0`.
5. `git push --tags origin master` 5. `git push --tags origin master`
6. Wait for Travis to run, then find and download the binary and Debian 6. Wait for Travis to run, then find and download the binary and Debian
packages for all architectures; there will be links printed at the packages for both amd64 and ppc64el; there will be links printed at the end
end of the Travis output. Put these into your `dist` directory. of the Travis output. Put these into your `dist` directory.
7. Run `make release` 7. Run `make release`
8. Run `twine upload --skip-existing dist/*.tar.gz dist/*.whl` to upload the 8. Run `twine upload --skip-existing dist/*.tar.gz dist/*.whl` to upload the
new version to PyPI new version to PyPI
9. Upload the resulting Debian packages, binaries, and sha256sums file (all 9. Upload the resulting Debian package, binary (inside the `dist` directory),
inside the `dist` directory) to a new [GitHub and sha256sums file to a new [GitHub
release](https://github.com/Yelp/dumb-init/releases) release](https://github.com/Yelp/dumb-init/releases)

View file

@ -1,28 +1,24 @@
ARG BASE_IMAGE=debian:buster FROM debian:stretch
FROM $BASE_IMAGE
LABEL maintainer="Chris Kuehl <ckuehl@yelp.com>" LABEL maintainer="Chris Kuehl <ckuehl@yelp.com>"
# The default mirrors are too flaky to run reliably in CI.
RUN sed -E \
'/security\.debian/! s@http://[^/]+/@http://mirrors.kernel.org/@' \
-i /etc/apt/sources.list
# Install the bare minimum dependencies necessary for working with Debian # Install the bare minimum dependencies necessary for working with Debian
# packages. Build dependencies should be added under "Build-Depends" inside # packages. Build dependencies should be added under "Build-Depends" inside
# debian/control instead. # debian/control instead.
RUN : \ RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
&& apt-get update \ apt-get install -y --no-install-recommends \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
build-essential \ build-essential \
devscripts \ devscripts \
equivs \ equivs \
lintian \ lintian \
python3-distutils \ && rm -rf /var/lib/apt/lists/* && apt-get clean
python3-setuptools \ WORKDIR /mnt
python3-pip \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /tmp/mnt
COPY debian/control /control ENTRYPOINT apt-get update && \
RUN : \ mk-build-deps -i --tool 'apt-get --no-install-recommends -y' && \
&& apt-get update \ make builddeb
&& mk-build-deps --install --tool 'apt-get -y --no-install-recommends' /control \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

View file

@ -26,21 +26,18 @@ clean-tox:
.PHONY: release .PHONY: release
release: python-dists release: python-dists
cd dist && \ cd dist && \
sha256sum --binary dumb-init_$(VERSION)_amd64.deb dumb-init_$(VERSION)_x86_64 dumb-init_$(VERSION)_ppc64el.deb dumb-init_$(VERSION)_ppc64le dumb-init_$(VERSION)_s390x.deb dumb-init_$(VERSION)_s390x dumb-init_$(VERSION)_arm64.deb dumb-init_$(VERSION)_aarch64 \ sha256sum --binary dumb-init_$(VERSION)_amd64.deb dumb-init_$(VERSION)_amd64 dumb-init_$(VERSION)_ppc64el.deb dumb-init_$(VERSION)_ppc64el \
> sha256sums > sha256sums
.PHONY: python-dists .PHONY: python-dists
python-dists: python-dists-x86_64 python-dists-aarch64 python-dists-ppc64le python-dists-s390x python-dists: VERSION.h
.PHONY: python-dists-%
python-dists-%: VERSION.h
python setup.py sdist python setup.py sdist
docker run \ docker run \
--user $$(id -u):$$(id -g) \ --user $$(id -u):$$(id -g) \
-v `pwd`/dist:/dist:rw \ -v $(PWD)/dist:/dist:rw \
quay.io/pypa/manylinux2014_$*:latest \ quay.io/pypa/manylinux1_x86_64:latest \
bash -exc ' \ bash -exc ' \
/opt/python/cp38-cp38/bin/pip wheel --wheel-dir /tmp /dist/*.tar.gz && \ /opt/python/cp35-cp35m/bin/pip wheel --wheel-dir /tmp /dist/*.tar.gz && \
auditwheel repair --wheel-dir /dist /tmp/*.whl --wheel-dir /dist \ auditwheel repair --wheel-dir /dist /tmp/*.whl --wheel-dir /dist \
' '
@ -52,16 +49,16 @@ builddeb:
# Extract the built binary from the Debian package # Extract the built binary from the Debian package
dpkg-deb --fsys-tarfile dist/dumb-init_$(VERSION)_$(shell dpkg --print-architecture).deb | \ dpkg-deb --fsys-tarfile dist/dumb-init_$(VERSION)_$(shell dpkg --print-architecture).deb | \
tar -C dist --strip=3 -xvf - ./usr/bin/dumb-init tar -C dist --strip=3 -xvf - ./usr/bin/dumb-init
mv dist/dumb-init dist/dumb-init_$(VERSION)_$(shell uname -m) mv dist/dumb-init dist/dumb-init_$(VERSION)_$(shell dpkg --print-architecture)
.PHONY: builddeb-docker .PHONY: builddeb-docker
builddeb-docker: docker-image builddeb-docker: docker-image
mkdir -p dist mkdir -p dist
docker run --init --user $$(id -u):$$(id -g) -v $(PWD):/tmp/mnt dumb-init-build make builddeb docker run -v $(PWD):/mnt dumb-init-build
.PHONY: docker-image .PHONY: docker-image
docker-image: docker-image:
docker build $(if $(BASE_IMAGE),--build-arg BASE_IMAGE=$(BASE_IMAGE)) -t dumb-init-build . docker build -t dumb-init-build .
.PHONY: test .PHONY: test
test: test:
@ -71,3 +68,25 @@ test:
.PHONY: install-hooks .PHONY: install-hooks
install-hooks: install-hooks:
tox -e pre-commit -- install -f --install-hooks tox -e pre-commit -- install -f --install-hooks
ITEST_TARGETS = itest_trusty itest_xenial itest_bionic itest_stretch
.PHONY: itest $(ITEST_TARGETS)
itest: $(ITEST_TARGETS)
itest_trusty: _itest-ubuntu-trusty
itest_xenial: _itest-ubuntu-xenial
itest_bionic: _itest-ubuntu-bionic
itest_stretch: _itest-debian-stretch
itest_tox:
$(DOCKER_RUN_TEST) ubuntu:bionic /mnt/ci/docker-tox-test
_itest-%: _itest_deb-% _itest_python-%
@true
_itest_python-%:
$(DOCKER_RUN_TEST) $(shell sed 's/-/:/' <<< "$*") /mnt/ci/docker-python-test
_itest_deb-%: builddeb-docker
$(DOCKER_RUN_TEST) $(shell sed 's/-/:/' <<< "$*") /mnt/ci/docker-deb-test

View file

@ -1,6 +1,7 @@
dumb-init dumb-init
======== ========
[![Travis CI](https://travis-ci.org/Yelp/dumb-init.svg?branch=master)](https://travis-ci.org/Yelp/dumb-init/)
[![PyPI version](https://badge.fury.io/py/dumb-init.svg)](https://pypi.python.org/pypi/dumb-init) [![PyPI version](https://badge.fury.io/py/dumb-init.svg)](https://pypi.python.org/pypi/dumb-init)
@ -173,7 +174,7 @@ If you don't have an internal apt server, you can use `dpkg -i` to install the
One possibility is with the following commands in your Dockerfile: One possibility is with the following commands in your Dockerfile:
```Dockerfile ```Dockerfile
RUN wget https://github.com/Yelp/dumb-init/releases/download/v1.2.5/dumb-init_1.2.5_amd64.deb RUN wget https://github.com/Yelp/dumb-init/releases/download/v1.2.1/dumb-init_1.2.1_amd64.deb
RUN dpkg -i dumb-init_*.deb RUN dpkg -i dumb-init_*.deb
``` ```
@ -184,7 +185,7 @@ Since dumb-init is released as a statically-linked binary, you can usually just
plop it into your images. Here's an example of doing that in a Dockerfile: plop it into your images. Here's an example of doing that in a Dockerfile:
```Dockerfile ```Dockerfile
RUN wget -O /usr/local/bin/dumb-init https://github.com/Yelp/dumb-init/releases/download/v1.2.5/dumb-init_1.2.5_x86_64 RUN wget -O /usr/local/bin/dumb-init https://github.com/Yelp/dumb-init/releases/download/v1.2.1/dumb-init_1.2.1_amd64
RUN chmod +x /usr/local/bin/dumb-init RUN chmod +x /usr/local/bin/dumb-init
``` ```

View file

@ -1 +1 @@
1.2.5 1.2.1

View file

@ -1,6 +1,6 @@
// THIS FILE IS AUTOMATICALLY GENERATED // THIS FILE IS AUTOMATICALLY GENERATED
// Run `make VERSION.h` to update it after modifying VERSION. // Run `make VERSION.h` to update it after modifying VERSION.
unsigned char VERSION[] = { unsigned char VERSION[] = {
0x31, 0x2e, 0x32, 0x2e, 0x35, 0x0a 0x31, 0x2e, 0x32, 0x2e, 0x31, 0x0a
}; };
unsigned int VERSION_len = 6; unsigned int VERSION_len = 6;

18
ci/docker Normal file
View file

@ -0,0 +1,18 @@
# The default mirrors are too flaky to run reliably in CI.
sed -E \
'/security\.debian/! s@http://[^/]+/@http://mirrors.kernel.org/@' \
-i /etc/apt/sources.list
apt-get update
apt-get install -y --no-install-recommends \
build-essential \
procps \
python \
python-dev \
python-pip \
python-setuptools
cp -r /mnt/ /test
cd /test
# vim: ft=sh

View file

@ -1,12 +1,10 @@
#!/bin/bash -eux #!/bin/bash -eux
set -o pipefail set -o pipefail
apt-get update . /mnt/ci/docker
apt-get -y --no-install-recommends install python3-pip procps
cd /mnt
dpkg -i dist/*.deb dpkg -i dist/*.deb
pip3 install -r requirements-dev.txt pip install -r requirements-dev.txt
pytest tests/ py.test tests/
exec dumb-init /mnt/tests/test-zombies exec dumb-init /mnt/tests/test-zombies

View file

@ -1,12 +1,12 @@
#!/bin/bash -eux #!/bin/bash -eux
set -euo pipefail set -o pipefail
cd /mnt . /mnt/ci/docker
python3 setup.py clean python setup.py clean
python3 setup.py sdist python setup.py sdist
pip3 install -vv dist/*.tar.gz pip install -vv dist/*.tar.gz
pip3 install -r requirements-dev.txt pip install -r requirements-dev.txt
pytest-3 -vv tests/ py.test tests/
exec dumb-init /mnt/tests/test-zombies exec dumb-init /mnt/tests/test-zombies \

13
ci/docker-tox-test Executable file
View file

@ -0,0 +1,13 @@
#!/bin/bash -eux
set -o pipefail
. /mnt/ci/docker
apt-get update
apt-get install -y --no-install-recommends \
git \
python2.7-dev \
python3.6-dev \
tox
tox

43
debian/changelog vendored
View file

@ -1,46 +1,3 @@
dumb-init (1.2.5) unstable; urgency=medium
* Change the working directory in the parent process to "/" after forking.
https://github.com/Yelp/dumb-init/pull/210
Thanks to @Villemoes for the patch!
-- Chris Kuehl <ckuehl@yelp.com> Thu, 10 Dec 2020 10:54:47 -0800
dumb-init (1.2.4) unstable; urgency=medium
* Actually fix the bug that can cause `--help` or `--version` to crash in
some scenarios.
https://github.com/Yelp/dumb-init/pull/215
Thanks to @suve for the patch!
-- Chris Kuehl <ckuehl@yelp.com> Mon, 07 Dec 2020 11:58:06 -0800
dumb-init (1.2.3) unstable; urgency=medium
* Fix a bug that can cause `--help` or `--version` to crash in some
scenarios.
https://github.com/Yelp/dumb-init/pull/213
Thanks to @suve for the patch!
-- Chris Kuehl <ckuehl@yelp.com> Wed, 02 Dec 2020 10:43:02 -0800
dumb-init (1.2.2) unstable; urgency=medium
* Fix a race condition which can cause the child to receive SIGHUP and
SIGCONT very shortly after start (#174).
In general this was very rare, but some environments (especially some
container and virtualization environments) appear to encounter it at a
much higher rate, possibly due to scheduler quirks.
-- Chris Kuehl <ckuehl@yelp.com> Wed, 01 Aug 2018 16:36:22 -0700
dumb-init (1.2.1) unstable; urgency=medium dumb-init (1.2.1) unstable; urgency=medium
* Fix verbose debug logging for ignored signals. * Fix verbose debug logging for ignored signals.

6
debian/control vendored
View file

@ -2,14 +2,16 @@ Source: dumb-init
Section: utils Section: utils
Priority: extra Priority: extra
Maintainer: Chris Kuehl <ckuehl@yelp.com> Maintainer: Chris Kuehl <ckuehl@yelp.com>
Uploaders: Kent Wills <rkwills@yelp.com>
Build-Depends: Build-Depends:
debhelper (>= 9), debhelper (>= 9),
help2man, help2man,
musl-tools, musl-tools,
## Tests: ## Tests:
procps, procps,
python3, python,
python3-pytest, python-mock,
python-pytest,
Standards-Version: 3.9.7 Standards-Version: 3.9.7
Homepage: https://github.com/Yelp/dumb-init Homepage: https://github.com/Yelp/dumb-init
Vcs-Browser: https://github.com/Yelp/dumb-init Vcs-Browser: https://github.com/Yelp/dumb-init

2
debian/rules vendored
View file

@ -31,4 +31,4 @@ override_dh_builddeb:
override_dh_auto_test: override_dh_auto_test:
find . -name '*.pyc' -delete find . -name '*.pyc' -delete
find . -name '__pycache__' -delete find . -name '__pycache__' -delete
PATH=.:$$PATH timeout --signal=KILL 60 pytest-3 -vv tests/ PATH=.:$$PATH timeout --signal=KILL 60 py.test -vv tests/

View file

@ -94,7 +94,7 @@ void handle_signal(int signum) {
DEBUG("Received signal %d.\n", signum); DEBUG("Received signal %d.\n", signum);
if (signal_temporary_ignores[signum] == 1) { if (signal_temporary_ignores[signum] == 1) {
DEBUG("Ignoring tty hand-off signal %d.\n", signum); DEBUG("Ignoring signal %d during its first receive.\n", signum);
signal_temporary_ignores[signum] = 0; signal_temporary_ignores[signum] = 0;
} else if (signum == SIGCHLD) { } else if (signum == SIGCHLD) {
int status, exit_status; int status, exit_status;
@ -126,7 +126,7 @@ void handle_signal(int signum) {
void print_help(char *argv[]) { void print_help(char *argv[]) {
fprintf(stderr, fprintf(stderr,
"dumb-init v%.*s" "dumb-init v%s"
"Usage: %s [option] command [[arg] ...]\n" "Usage: %s [option] command [[arg] ...]\n"
"\n" "\n"
"dumb-init is a simple process supervisor that forwards signals to children.\n" "dumb-init is a simple process supervisor that forwards signals to children.\n"
@ -144,7 +144,7 @@ void print_help(char *argv[]) {
" -V, --version Print the current version and exit.\n" " -V, --version Print the current version and exit.\n"
"\n" "\n"
"Full help is available online at https://github.com/Yelp/dumb-init\n", "Full help is available online at https://github.com/Yelp/dumb-init\n",
VERSION_len, VERSION, VERSION,
argv[0] argv[0]
); );
} }
@ -175,10 +175,9 @@ void parse_rewrite_signum(char *arg) {
} }
void set_rewrite_to_sigstop_if_not_defined(int signum) { void set_rewrite_to_sigstop_if_not_defined(int signum) {
if (signal_rewrite[signum] == -1) { if (signal_rewrite[signum] == -1)
signal_rewrite[signum] = SIGSTOP; signal_rewrite[signum] = SIGSTOP;
} }
}
char **parse_command(int argc, char *argv[]) { char **parse_command(int argc, char *argv[]) {
int opt; int opt;
@ -199,7 +198,7 @@ char **parse_command(int argc, char *argv[]) {
debug = 1; debug = 1;
break; break;
case 'V': case 'V':
fprintf(stderr, "dumb-init v%.*s", VERSION_len, VERSION); fprintf(stderr, "dumb-init v%s", VERSION);
exit(0); exit(0);
case 'c': case 'c':
use_setsid = 0; use_setsid = 0;
@ -256,9 +255,8 @@ int main(int argc, char *argv[]) {
sigprocmask(SIG_BLOCK, &all_signals, NULL); sigprocmask(SIG_BLOCK, &all_signals, NULL);
int i = 0; int i = 0;
for (i = 1; i <= MAXSIG; i++) { for (i = 1; i <= MAXSIG; i++)
signal(i, dummy); signal(i, dummy);
}
/* /*
* Detach dumb-init from controlling tty, so that the child's session can * Detach dumb-init from controlling tty, so that the child's session can
@ -326,11 +324,6 @@ int main(int argc, char *argv[]) {
} else { } else {
/* parent */ /* parent */
DEBUG("Child spawned with PID %d.\n", child_pid); DEBUG("Child spawned with PID %d.\n", child_pid);
if (chdir("/") == -1) {
DEBUG("Unable to chdir(\"/\") (errno=%d %s)\n",
errno,
strerror(errno));
}
for (;;) { for (;;) {
int signum; int signum;
sigwait(&all_signals, &signum); sigwait(&all_signals, &signum);

View file

@ -1,2 +1,2 @@
[pytest] [pytest]
timeout = 20 timeout = 5

View file

@ -1,6 +1,4 @@
mock
pre-commit>=0.5.0 pre-commit>=0.5.0
pytest pytest
# TODO: This pin is to work around an issue where the system pytest is too old. pytest-timeout
# We should fix this by not depending on the system pytest/python packages at
# some point.
pytest-timeout<2.0.0

View file

@ -1,9 +1,11 @@
from __future__ import print_function
import os.path import os.path
import subprocess import subprocess
import tempfile import tempfile
from distutils.command.build import build as orig_build from distutils.command.build import build as orig_build
from distutils.core import Command from distutils.core import Command
from setuptools import Distribution from setuptools import Distribution
from setuptools import Extension from setuptools import Extension
from setuptools import setup from setuptools import setup
@ -56,8 +58,7 @@ class install_cexe(Command):
# this initializes attributes based on other commands' attributes # this initializes attributes based on other commands' attributes
self.set_undefined_options('build', ('build_scripts', 'build_dir')) self.set_undefined_options('build', ('build_scripts', 'build_dir'))
self.set_undefined_options( self.set_undefined_options(
'install', ('install_scripts', 'install_dir'), 'install', ('install_scripts', 'install_dir'))
)
def run(self): def run(self):
@ -123,7 +124,6 @@ setup(
author='Yelp', author='Yelp',
url='https://github.com/Yelp/dumb-init/', url='https://github.com/Yelp/dumb-init/',
platforms='linux', platforms='linux',
packages=[],
c_executables=[Extension('dumb-init', ['dumb-init.c'])], c_executables=[Extension('dumb-init', ['dumb-init.c'])],
cmdclass={ cmdclass={
'bdist_wheel': bdist_wheel, 'bdist_wheel': bdist_wheel,

View file

@ -8,6 +8,9 @@ from contextlib import contextmanager
from subprocess import PIPE from subprocess import PIPE
from subprocess import Popen from subprocess import Popen
from py._path.local import LocalPath
# these signals cause dumb-init to suspend itself # these signals cause dumb-init to suspend itself
SUSPEND_SIGNALS = frozenset([ SUSPEND_SIGNALS = frozenset([
signal.SIGTSTP, signal.SIGTSTP,
@ -17,8 +20,8 @@ SUSPEND_SIGNALS = frozenset([
NORMAL_SIGNALS = frozenset( NORMAL_SIGNALS = frozenset(
set(range(1, 32)) - set(range(1, 32)) -
{signal.SIGKILL, signal.SIGSTOP, signal.SIGCHLD} - set([signal.SIGKILL, signal.SIGSTOP, signal.SIGCHLD]) -
SUSPEND_SIGNALS, SUSPEND_SIGNALS
) )
@ -34,7 +37,7 @@ def print_signals(args=()):
stdout=PIPE, stdout=PIPE,
) )
line = proc.stdout.readline() line = proc.stdout.readline()
m = re.match(b'^ready \\(pid: ([0-9]+)\\)\n$', line) m = re.match(b'^ready \(pid: ([0-9]+)\)\n$', line)
assert m, line assert m, line
yield proc, m.group(1).decode('ascii') yield proc, m.group(1).decode('ascii')
@ -46,25 +49,15 @@ def print_signals(args=()):
def child_pids(pid): def child_pids(pid):
"""Return a list of direct child PIDs for the given PID.""" """Return a list of direct child PIDs for the given PID."""
children = set() children = set()
for p in os.listdir('/proc'): for p in LocalPath('/proc').listdir():
try: try:
with open(os.path.join('/proc', p, 'stat')) as f: stat = open(p.join('stat').strpath).read()
stat = f.read() m = re.match('^\d+ \(.+?\) [a-zA-Z] (\d+) ', stat)
m = re.match(
r'^\d+ \(.+?\) '
# This field, state, is normally a single letter, but can be
# "0" if there are some unusual security settings that prevent
# reading the process state (happens under GitHub Actions with
# QEMU for some reason).
'[0a-zA-Z] '
r'(\d+) ',
stat,
)
assert m, stat assert m, stat
ppid = int(m.group(1)) ppid = int(m.group(1))
if ppid == pid: if ppid == pid:
children.add(int(p)) children.add(int(p.basename))
except OSError: except IOError:
# Happens when the process exits after listing it, or between # Happens when the process exits after listing it, or between
# opening stat and reading it. # opening stat and reading it.
pass pass
@ -74,23 +67,22 @@ def child_pids(pid):
def pid_tree(pid): def pid_tree(pid):
"""Return a list of all descendant PIDs for the given PID.""" """Return a list of all descendant PIDs for the given PID."""
children = child_pids(pid) children = child_pids(pid)
return { return set(
pid pid
for child in children for child in children
for pid in pid_tree(child) for pid in pid_tree(child)
} | children ) | children
def is_alive(pid): def is_alive(pid):
"""Return whether a process is running with the given PID.""" """Return whether a process is running with the given PID."""
return os.path.isdir(os.path.join('/proc', str(pid))) return LocalPath('/proc').join(str(pid)).isdir()
def process_state(pid): def process_state(pid):
"""Return a process' state, such as "stopped" or "running".""" """Return a process' state, such as "stopped" or "running"."""
with open(os.path.join('/proc', str(pid), 'status')) as f: status = LocalPath('/proc').join(str(pid), 'status').read()
status = f.read() m = re.search('^State:\s+[A-Z] \(([a-z]+)\)$', status, re.MULTILINE)
m = re.search(r'^State:\s+[A-Z] \(([a-z]+)\)$', status, re.MULTILINE)
return m.group(1) return m.group(1)

View file

@ -4,6 +4,8 @@
Since all signals are printed and otherwise ignored, you'll need to send Since all signals are printed and otherwise ignored, you'll need to send
SIGKILL (kill -9) to this process to actually end it. SIGKILL (kill -9) to this process to actually end it.
""" """
from __future__ import print_function
import os import os
import signal import signal
import sys import sys
@ -11,7 +13,7 @@ import time
CATCHABLE_SIGNALS = frozenset( CATCHABLE_SIGNALS = frozenset(
set(range(1, 32)) - {signal.SIGKILL, signal.SIGSTOP, signal.SIGCHLD}, set(range(1, 32)) - set([signal.SIGKILL, signal.SIGSTOP, signal.SIGCHLD])
) )
@ -20,7 +22,7 @@ last_signal = None
def unbuffered_print(line): def unbuffered_print(line):
sys.stdout.write('{}\n'.format(line)) sys.stdout.write('{0}\n'.format(line))
sys.stdout.flush() sys.stdout.flush()
@ -32,7 +34,7 @@ if __name__ == '__main__':
for signum in CATCHABLE_SIGNALS: for signum in CATCHABLE_SIGNALS:
signal.signal(signum, print_signal) signal.signal(signum, print_signal)
unbuffered_print('ready (pid: {})'.format(os.getpid())) unbuffered_print('ready (pid: {0})'.format(os.getpid()))
# loop forever just printing signals # loop forever just printing signals
while True: while True:

View file

@ -17,7 +17,7 @@ def spawn_and_kill_pipeline():
proc = Popen(( proc = Popen((
'dumb-init', 'dumb-init',
'sh', '-c', 'sh', '-c',
"yes 'oh, hi' | tail & yes error | tail >&2", "yes 'oh, hi' | tail & yes error | tail >&2"
)) ))
def assert_living_pids(): def assert_living_pids():
@ -32,7 +32,7 @@ def spawn_and_kill_pipeline():
def living_pids(pids): def living_pids(pids):
return {pid for pid in pids if is_alive(pid)} return set(pid for pid in pids if is_alive(pid))
@pytest.mark.usefixtures('both_debug_modes', 'setsid_enabled') @pytest.mark.usefixtures('both_debug_modes', 'setsid_enabled')
@ -80,7 +80,7 @@ def spawn_process_which_dies_with_children():
# we need to sleep before the shell exits, or dumb-init might send # we need to sleep before the shell exits, or dumb-init might send
# TERM to print_signals before it has had time to register custom # TERM to print_signals before it has had time to register custom
# signal handlers # signal handlers
'{python} -m testing.print_signals & sleep 1'.format( '{python} -m testing.print_signals & sleep 0.1'.format(
python=sys.executable, python=sys.executable,
), ),
), ),
@ -91,7 +91,7 @@ def spawn_process_which_dies_with_children():
# read a line from print_signals, figure out its pid # read a line from print_signals, figure out its pid
line = proc.stdout.readline() line = proc.stdout.readline()
match = re.match(b'ready \\(pid: ([0-9]+)\\)\n', line) match = re.match(b'ready \(pid: ([0-9]+)\)\n', line)
assert match, line assert match, line
child_pid = int(match.group(1)) child_pid = int(match.group(1))
@ -129,14 +129,12 @@ def test_processes_dont_receive_term_on_exit_if_no_setsid():
os.kill(child_pid, signal.SIGKILL) os.kill(child_pid, signal.SIGKILL)
@pytest.mark.parametrize( @pytest.mark.parametrize('args', [
'args', [
('/doesnotexist',), ('/doesnotexist',),
('--', '/doesnotexist'), ('--', '/doesnotexist'),
('-c', '/doesnotexist'), ('-c', '/doesnotexist'),
('--single-child', '--', '/doesnotexist'), ('--single-child', '--', '/doesnotexist'),
], ])
)
@pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes') @pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes')
def test_fails_nonzero_with_bad_exec(args): def test_fails_nonzero_with_bad_exec(args):
"""If dumb-init can't exec as requested, it should exit nonzero.""" """If dumb-init can't exec as requested, it should exit nonzero."""

View file

@ -12,34 +12,12 @@ def current_version():
return open('VERSION').read().strip() return open('VERSION').read().strip()
def normalize_stderr(stderr):
# dumb-init prints out argv[0] in its usage message. This should always be
# just "dumb-init" under regular test scenarios here since that is how we
# call it, but in CI the use of QEMU causes the argv[0] to be replaced with
# the full path.
#
# This is supposed to be avoidable by:
# 1) Setting the "P" flag in the binfmt register:
# https://en.wikipedia.org/wiki/Binfmt_misc#Registration
# This can be done by setting the QEMU_PRESERVE_PARENT env var when
# calling binfmt.
#
# 2) Setting the "QEMU_ARGV0" env var to empty string for *all*
# processes:
# https://bugs.launchpad.net/qemu/+bug/1835839
#
# I can get it working properly in CI outside of Docker, but for some
# reason not during Docker builds. This doesn't affect the built executable
# so I decided to just punt on it.
return re.sub(rb'(^|(?<=\s))[a-z/.]+/dumb-init', b'dumb-init', stderr)
@pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes') @pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes')
def test_no_arguments_prints_usage(): def test_no_arguments_prints_usage():
proc = Popen(('dumb-init'), stderr=PIPE) proc = Popen(('dumb-init'), stderr=PIPE)
_, stderr = proc.communicate() _, stderr = proc.communicate()
assert proc.returncode != 0 assert proc.returncode != 0
assert normalize_stderr(stderr) == ( assert stderr == (
b'Usage: dumb-init [option] program [args]\n' b'Usage: dumb-init [option] program [args]\n'
b'Try dumb-init --help for full usage.\n' b'Try dumb-init --help for full usage.\n'
) )
@ -50,7 +28,7 @@ def test_exits_invalid_with_invalid_args():
proc = Popen(('dumb-init', '--yolo', '/bin/true'), stderr=PIPE) proc = Popen(('dumb-init', '--yolo', '/bin/true'), stderr=PIPE)
_, stderr = proc.communicate() _, stderr = proc.communicate()
assert proc.returncode == 1 assert proc.returncode == 1
assert normalize_stderr(stderr) in ( assert stderr in (
b"dumb-init: unrecognized option '--yolo'\n", # glibc b"dumb-init: unrecognized option '--yolo'\n", # glibc
b'dumb-init: unrecognized option: yolo\n', # musl b'dumb-init: unrecognized option: yolo\n', # musl
) )
@ -65,7 +43,7 @@ def test_help_message(flag, current_version):
proc = Popen(('dumb-init', flag), stderr=PIPE) proc = Popen(('dumb-init', flag), stderr=PIPE)
_, stderr = proc.communicate() _, stderr = proc.communicate()
assert proc.returncode == 0 assert proc.returncode == 0
assert normalize_stderr(stderr) == ( assert stderr == (
b'dumb-init v' + current_version.encode('ascii') + b'\n' b'dumb-init v' + current_version.encode('ascii') + b'\n'
b'Usage: dumb-init [option] command [[arg] ...]\n' b'Usage: dumb-init [option] command [[arg] ...]\n'
b'\n' b'\n'
@ -107,15 +85,15 @@ def test_verbose(flag):
assert stdout == b'oh, hi\n' assert stdout == b'oh, hi\n'
# child/parent race to print output after the fork(), can't guarantee exact order # child/parent race to print output after the fork(), can't guarantee exact order
assert re.search(b'(^|\n)\\[dumb-init\\] setsid complete\\.\n', stderr), stderr # child assert re.search(b'(^|\n)\[dumb-init\] setsid complete\.\n', stderr), stderr # child
assert re.search( # parent assert re.search( # parent
( (
'(^|\n)\\[dumb-init\\] Child spawned with PID [0-9]+\\.\n' '(^|\n)\[dumb-init\] Child spawned with PID [0-9]+\.\n'
'.*' # child might print here '.*' # child might print here
'\\[dumb-init\\] Received signal {signal.SIGCHLD}\\.\n' '\[dumb-init\] Received signal {signal.SIGCHLD}\.\n'
'\\[dumb-init\\] A child with PID [0-9]+ exited with exit status 0.\n' '\[dumb-init\] A child with PID [0-9]+ exited with exit status 0.\n'
'\\[dumb-init\\] Forwarded signal 15 to children\\.\n' '\[dumb-init\] Forwarded signal 15 to children\.\n'
'\\[dumb-init\\] Child exited with status 0\\. Goodbye\\.\n$' '\[dumb-init\] Child exited with status 0\. Goodbye\.\n$'
).format(signal=signal).encode('utf8'), ).format(signal=signal).encode('utf8'),
stderr, stderr,
re.DOTALL, re.DOTALL,
@ -132,18 +110,17 @@ def test_verbose_and_single_child(flag1, flag2):
assert stdout == b'oh, hi\n' assert stdout == b'oh, hi\n'
assert re.match( assert re.match(
( (
'^\\[dumb-init\\] Child spawned with PID [0-9]+\\.\n' '^\[dumb-init\] Child spawned with PID [0-9]+\.\n'
'\\[dumb-init\\] Received signal {signal.SIGCHLD}\\.\n' '\[dumb-init\] Received signal {signal.SIGCHLD}\.\n'
'\\[dumb-init\\] A child with PID [0-9]+ exited with exit status 0.\n' '\[dumb-init\] A child with PID [0-9]+ exited with exit status 0.\n'
'\\[dumb-init\\] Forwarded signal 15 to children\\.\n' '\[dumb-init\] Forwarded signal 15 to children\.\n'
'\\[dumb-init\\] Child exited with status 0\\. Goodbye\\.\n$' '\[dumb-init\] Child exited with status 0\. Goodbye\.\n$'
).format(signal=signal).encode('utf8'), ).format(signal=signal).encode('utf8'),
stderr, stderr,
) )
@pytest.mark.parametrize( @pytest.mark.parametrize('extra_args', [
'extra_args', [
('-r',), ('-r',),
('-r', ''), ('-r', ''),
('-r', 'herp'), ('-r', 'herp'),
@ -154,8 +131,7 @@ def test_verbose_and_single_child(flag1, flag2):
('-r', '15:12', '-r'), ('-r', '15:12', '-r'),
('-r', '15:12', '-r', '0'), ('-r', '15:12', '-r', '0'),
('-r', '15:12', '-r', '1:32'), ('-r', '15:12', '-r', '1:32'),
], ])
)
@pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes') @pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes')
def test_rewrite_errors(extra_args): def test_rewrite_errors(extra_args):
proc = Popen( proc = Popen(

View file

@ -1,6 +1,6 @@
import os import os
from unittest import mock
import mock
import pytest import pytest

View file

@ -1,28 +0,0 @@
import os
import shutil
from subprocess import PIPE
from subprocess import run
import pytest
@pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes')
def test_working_directories():
"""The child process must start in the working directory in which
dumb-init was invoked, but dumb-init itself should not keep a
reference to that."""
# We need absolute path to dumb-init since we pass cwd=/tmp to get
# predictable output - so we can't rely on dumb-init being found
# in the "." directory.
dumb_init = os.path.realpath(shutil.which('dumb-init'))
proc = run(
(
dumb_init,
'sh', '-c', 'readlink /proc/$PPID/cwd && readlink /proc/$$/cwd',
),
cwd='/tmp', stdout=PIPE, stderr=PIPE,
)
assert proc.returncode == 0
assert proc.stdout == b'/\n/tmp\n'

View file

@ -11,19 +11,17 @@ def test_exit_status_regular_exit(exit_status):
"""dumb-init should exit with the same exit status as the process that it """dumb-init should exit with the same exit status as the process that it
supervises when that process exits normally. supervises when that process exits normally.
""" """
proc = Popen(('dumb-init', 'sh', '-c', 'exit {}'.format(exit_status))) proc = Popen(('dumb-init', 'sh', '-c', 'exit {0}'.format(exit_status)))
proc.wait() proc.wait()
assert proc.returncode == exit_status assert proc.returncode == exit_status
@pytest.mark.parametrize( @pytest.mark.parametrize('signal', [
'signal', [
signal.SIGTERM, signal.SIGTERM,
signal.SIGHUP, signal.SIGHUP,
signal.SIGQUIT, signal.SIGQUIT,
signal.SIGKILL, signal.SIGKILL,
], ])
)
@pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes') @pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes')
def test_exit_status_terminated_by_signal(signal): def test_exit_status_terminated_by_signal(signal):
"""dumb-init should exit with status 128 + signal when the child process is """dumb-init should exit with status 128 + signal when the child process is
@ -31,10 +29,8 @@ def test_exit_status_terminated_by_signal(signal):
""" """
# We use Python because sh is "dash" on Debian and "bash" on others. # We use Python because sh is "dash" on Debian and "bash" on others.
# https://github.com/Yelp/dumb-init/issues/115 # https://github.com/Yelp/dumb-init/issues/115
proc = Popen(( proc = Popen(('dumb-init', sys.executable, '-c', 'import os; os.kill(os.getpid(), {0})'.format(
'dumb-init', sys.executable, '-c', 'import os; os.kill(os.getpid(), {})'.format(
signal, signal,
), )))
))
proc.wait() proc.wait()
assert proc.returncode == 128 + signal assert proc.returncode == 128 + signal

View file

@ -15,17 +15,16 @@ def test_proxies_signals():
with print_signals() as (proc, _): with print_signals() as (proc, _):
for signum in NORMAL_SIGNALS: for signum in NORMAL_SIGNALS:
proc.send_signal(signum) proc.send_signal(signum)
assert proc.stdout.readline() == '{}\n'.format(signum).encode('ascii') assert proc.stdout.readline() == '{0}\n'.format(signum).encode('ascii')
def _rewrite_map_to_args(rewrite_map): def _rewrite_map_to_args(rewrite_map):
return chain.from_iterable( return chain.from_iterable(
('-r', '{}:{}'.format(src, dst)) for src, dst in rewrite_map.items() ('-r', '{0}:{1}'.format(src, dst)) for src, dst in rewrite_map.items()
) )
@pytest.mark.parametrize( @pytest.mark.parametrize('rewrite_map,sequence,expected', [
'rewrite_map,sequence,expected', [
( (
{}, {},
[signal.SIGTERM, signal.SIGQUIT, signal.SIGCONT, signal.SIGINT], [signal.SIGTERM, signal.SIGQUIT, signal.SIGCONT, signal.SIGINT],
@ -54,15 +53,14 @@ def _rewrite_map_to_args(rewrite_map):
[1, 31], [1, 31],
[31, 1], [31, 1],
), ),
], ])
)
@pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes') @pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes')
def test_proxies_signals_with_rewrite(rewrite_map, sequence, expected): def test_proxies_signals_with_rewrite(rewrite_map, sequence, expected):
"""Ensure dumb-init can rewrite signals.""" """Ensure dumb-init can rewrite signals."""
with print_signals(_rewrite_map_to_args(rewrite_map)) as (proc, _): with print_signals(_rewrite_map_to_args(rewrite_map)) as (proc, _):
for send, expect_receive in zip(sequence, expected): for send, expect_receive in zip(sequence, expected):
proc.send_signal(send) proc.send_signal(send)
assert proc.stdout.readline() == '{}\n'.format(expect_receive).encode('ascii') assert proc.stdout.readline() == '{0}\n'.format(expect_receive).encode('ascii')
@pytest.mark.usefixtures('both_debug_modes', 'setsid_enabled') @pytest.mark.usefixtures('both_debug_modes', 'setsid_enabled')
@ -80,12 +78,12 @@ def test_default_rewrites_can_be_overriden_with_setsid_enabled():
assert process_state(proc.pid) in ['running', 'sleeping'] assert process_state(proc.pid) in ['running', 'sleeping']
proc.send_signal(send) proc.send_signal(send)
assert proc.stdout.readline() == '{}\n'.format(expect_receive).encode('ascii') assert proc.stdout.readline() == '{0}\n'.format(expect_receive).encode('ascii')
os.waitpid(proc.pid, os.WUNTRACED) os.waitpid(proc.pid, os.WUNTRACED)
assert process_state(proc.pid) == 'stopped' assert process_state(proc.pid) == 'stopped'
proc.send_signal(signal.SIGCONT) proc.send_signal(signal.SIGCONT)
assert proc.stdout.readline() == '{}\n'.format(signal.SIGCONT).encode('ascii') assert proc.stdout.readline() == '{0}\n'.format(signal.SIGCONT).encode('ascii')
assert process_state(proc.pid) in ['running', 'sleeping'] assert process_state(proc.pid) in ['running', 'sleeping']
@ -100,8 +98,8 @@ def test_ignored_signals_are_not_proxied():
with print_signals(_rewrite_map_to_args(rewrite_map)) as (proc, _): with print_signals(_rewrite_map_to_args(rewrite_map)) as (proc, _):
proc.send_signal(signal.SIGTERM) proc.send_signal(signal.SIGTERM)
proc.send_signal(signal.SIGINT) proc.send_signal(signal.SIGINT)
assert proc.stdout.readline() == '{}\n'.format(signal.SIGQUIT).encode('ascii') assert proc.stdout.readline() == '{0}\n'.format(signal.SIGQUIT).encode('ascii')
proc.send_signal(signal.SIGWINCH) proc.send_signal(signal.SIGWINCH)
proc.send_signal(signal.SIGHUP) proc.send_signal(signal.SIGHUP)
assert proc.stdout.readline() == '{}\n'.format(signal.SIGHUP).encode('ascii') assert proc.stdout.readline() == '{0}\n'.format(signal.SIGHUP).encode('ascii')

View file

@ -31,7 +31,7 @@ def test_shell_background_support_setsid():
# and then both wake up again # and then both wake up again
proc.send_signal(SIGCONT) proc.send_signal(SIGCONT)
assert ( assert (
proc.stdout.readline() == '{}\n'.format(SIGCONT).encode('ascii') proc.stdout.readline() == '{0}\n'.format(SIGCONT).encode('ascii')
) )
assert process_state(pid) in ['running', 'sleeping'] assert process_state(pid) in ['running', 'sleeping']
assert process_state(proc.pid) in ['running', 'sleeping'] assert process_state(proc.pid) in ['running', 'sleeping']
@ -46,12 +46,12 @@ def test_shell_background_support_without_setsid():
for signum in SUSPEND_SIGNALS: for signum in SUSPEND_SIGNALS:
assert process_state(proc.pid) in ['running', 'sleeping'] assert process_state(proc.pid) in ['running', 'sleeping']
proc.send_signal(signum) proc.send_signal(signum)
assert proc.stdout.readline() == '{}\n'.format(signum).encode('ascii') assert proc.stdout.readline() == '{0}\n'.format(signum).encode('ascii')
os.waitpid(proc.pid, os.WUNTRACED) os.waitpid(proc.pid, os.WUNTRACED)
assert process_state(proc.pid) == 'stopped' assert process_state(proc.pid) == 'stopped'
proc.send_signal(SIGCONT) proc.send_signal(SIGCONT)
assert ( assert (
proc.stdout.readline() == '{}\n'.format(SIGCONT).encode('ascii') proc.stdout.readline() == '{0}\n'.format(SIGCONT).encode('ascii')
) )
assert process_state(proc.pid) in ['running', 'sleeping'] assert process_state(proc.pid) in ['running', 'sleeping']

View file

@ -77,7 +77,7 @@ def test_child_gets_controlling_tty_if_we_had_one():
output = readall(sfd) output = readall(sfd)
assert os.waitpid(pid, 0) == (pid, 0), output assert os.waitpid(pid, 0) == (pid, 0), output
m = re.search(b'flags are: \\[\\[([a-zA-Z]+)\\]\\]\n', output) m = re.search(b'flags are: \[\[([a-zA-Z]+)\]\]\n', output)
assert m, output assert m, output
# "m" is job control # "m" is job control
@ -111,8 +111,8 @@ def test_sighup_sigcont_ignored_if_was_session_leader():
output = readall(fd).decode('UTF-8') output = readall(fd).decode('UTF-8')
assert 'Ignoring tty hand-off signal {}.'.format(signal.SIGHUP) in output assert 'Ignoring signal {} during its first receive.'.format(signal.SIGHUP) in output
assert 'Ignoring tty hand-off signal {}.'.format(signal.SIGCONT) in output assert 'Ignoring signal {} during its first receive.'.format(signal.SIGCONT) in output
assert '[dumb-init] Forwarded signal {} to children.'.format(signal.SIGHUP) in output assert '[dumb-init] Forwarded signal {} to children.'.format(signal.SIGHUP) in output
assert '[dumb-init] Forwarded signal {} to children.'.format(signal.SIGCONT) not in output assert '[dumb-init] Forwarded signal {} to children.'.format(signal.SIGCONT) not in output

View file

@ -1,21 +1,20 @@
[tox] [tox]
envlist = py38,gcov envlist = py27,py36,gcov
[testenv] [testenv]
deps = -r{toxinidir}/requirements-dev.txt deps = -r{toxinidir}/requirements-dev.txt
commands = commands =
pytest python -m pytest
[testenv:gcov] [testenv:gcov]
skip_install = True skip_install = True
basepython = /usr/bin/python3.8
commands = commands =
{toxinidir}/ci/gcov-build {envbindir} {toxinidir}/ci/gcov-build {envbindir}
{[testenv]commands} {[testenv]commands}
{toxinidir}/ci/gcov-report {toxinidir}/ci/gcov-report
[testenv:pre-commit] [testenv:pre-commit]
basepython = /usr/bin/python3.8 basepython = /usr/bin/python3.6
commands = pre-commit {posargs:run --all-files} commands = pre-commit {posargs:run --all-files}
[flake8] [flake8]