ci: Reduce number of Jobs by parameterizing Mitogen Docker SSH tests

This reduces the number of jobs from 48 to 24. The Mitogen part of the test
suite has been parameterized on the Linux container targets to be run against.
Both the Ansible tests & Mitogen tests now use the same source of truth to
control which targets to use: environment variable MITOGEN_TEST_DISTRO_SPECS.
This replaces the two mutually exclusive env vars DISTRO and DISTROS. I've
also removed vestgial traces of an unused env var MITOGEN_TEST_DISTRO.

Parameterization adapted from
https://eli.thegreenplace.net/2014/04/02/dynamically-generating-python-test-cases

refs #1058, #1059
pull/1159/head
Alex Willmer 1 month ago
parent 9859e44ee8
commit 28e08ef94c

@ -28,14 +28,15 @@ for doing `setup.py install` while pulling a Docker container, for example.
### Environment Variables ### Environment Variables
* `DISTRO`: the `mitogen_` tests need a target Docker container distro. This * `MITOGEN_TEST_DISTRO_SPECS`: a space delimited list of distro specs to run
name comes from the Docker Hub `mitogen` user, i.e. `mitogen/$DISTRO-test` the tests against. (e.g. `centos6 ubuntu2004-py3*4`). Each spec determines
* `DISTROS`: the `ansible_` tests can run against multiple targets the Linux distribution, target Python interepreter & number of instances.
simultaneously, which speeds things up. This is a space-separated list of Only distributions with a pre-built Linux container image can be used.
DISTRO names, but additionally, supports:
* `debian-py3`: when generating Ansible inventory file, set * `debian-py3`: when generating Ansible inventory file, set
`ansible_python_interpreter` to `python3`, i.e. run a test where the `ansible_python_interpreter` to `python3`, i.e. run a test where the
target interpreter is Python 3. target interpreter is Python 3.
* `debian*16`: generate 16 Docker containers running Debian. Also works * `debian*16`: generate 16 Docker containers running Debian. Also works
with -py3. with -py3.
* `MITOGEN_TEST_IMAGE_TEMPLATE`: specifies the Linux container image name,
and hence the container registry used for test targets.

@ -35,7 +35,7 @@ ci_lib.check_stray_processes(interesting)
with ci_lib.Fold('docker_setup'): with ci_lib.Fold('docker_setup'):
containers = ci_lib.container_specs(ci_lib.DISTROS) containers = ci_lib.container_specs(ci_lib.DISTRO_SPECS.split())
ci_lib.start_containers(containers) ci_lib.start_containers(containers)

@ -28,6 +28,10 @@ os.chdir(
) )
DISTRO_SPECS = os.environ.get(
'MITOGEN_TEST_DISTRO_SPECS',
'centos6 centos8 debian9 debian11 ubuntu1604 ubuntu2004',
)
IMAGE_TEMPLATE = os.environ.get( IMAGE_TEMPLATE = os.environ.get(
'MITOGEN_TEST_IMAGE_TEMPLATE', 'MITOGEN_TEST_IMAGE_TEMPLATE',
'public.ecr.aws/n5z0e8q9/%(distro)s-test', 'public.ecr.aws/n5z0e8q9/%(distro)s-test',
@ -196,10 +200,6 @@ class Fold(object):
GIT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) GIT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
# Used only when MODE=mitogen
DISTRO = os.environ.get('DISTRO', 'debian9')
# Used only when MODE=ansible
DISTROS = os.environ.get('DISTROS', 'centos6 centos8 debian9 debian11 ubuntu1604 ubuntu2004').split()
TMP = TempDir().path TMP = TempDir().path

@ -8,8 +8,6 @@ import ci_lib
os.environ.update({ os.environ.update({
'NOCOVERAGE': '1', 'NOCOVERAGE': '1',
'UNIT2': '/usr/local/python2.4.6/bin/unit2', 'UNIT2': '/usr/local/python2.4.6/bin/unit2',
'MITOGEN_TEST_DISTRO': ci_lib.DISTRO,
'MITOGEN_LOG_LEVEL': 'debug', 'MITOGEN_LOG_LEVEL': 'debug',
'SKIP_ANSIBLE': '1', 'SKIP_ANSIBLE': '1',
}) })

@ -6,7 +6,6 @@ import os
import ci_lib import ci_lib
os.environ.update({ os.environ.update({
'MITOGEN_TEST_DISTRO': ci_lib.DISTRO,
'MITOGEN_LOG_LEVEL': 'debug', 'MITOGEN_LOG_LEVEL': 'debug',
'SKIP_ANSIBLE': '1', 'SKIP_ANSIBLE': '1',
}) })

@ -67,80 +67,14 @@ jobs:
python_version: '3.13' python_version: '3.13'
tox_env: py313-mode_ansible-ansible10-strategy_linear tox_env: py313-mode_ansible-ansible10-strategy_linear
- name: Mito_27_centos6 - name: Mito_27
tox_env: py27-mode_mitogen-distro_centos6 tox_env: py27-mode_mitogen
- name: Mito_27_centos7 - name: Mito_36
tox_env: py27-mode_mitogen-distro_centos7
- name: Mito_27_centos8
tox_env: py27-mode_mitogen-distro_centos8
- name: Mito_27_debian9
tox_env: py27-mode_mitogen-distro_debian9
- name: Mito_27_debian10
tox_env: py27-mode_mitogen-distro_debian10
- name: Mito_27_debian11
tox_env: py27-mode_mitogen-distro_debian11
- name: Mito_27_ubuntu1604
tox_env: py27-mode_mitogen-distro_ubuntu1604
- name: Mito_27_ubuntu1804
tox_env: py27-mode_mitogen-distro_ubuntu1804
- name: Mito_27_ubuntu2004
tox_env: py27-mode_mitogen-distro_ubuntu2004
- name: Mito_36_centos6
python_version: '3.6'
tox_env: py36-mode_mitogen-distro_centos6
- name: Mito_36_centos7
python_version: '3.6'
tox_env: py36-mode_mitogen-distro_centos7
- name: Mito_36_centos8
python_version: '3.6'
tox_env: py36-mode_mitogen-distro_centos8
- name: Mito_36_debian9
python_version: '3.6'
tox_env: py36-mode_mitogen-distro_debian9
- name: Mito_36_debian10
python_version: '3.6'
tox_env: py36-mode_mitogen-distro_debian10
- name: Mito_36_debian11
python_version: '3.6'
tox_env: py36-mode_mitogen-distro_debian11
- name: Mito_36_ubuntu1604
python_version: '3.6'
tox_env: py36-mode_mitogen-distro_ubuntu1604
- name: Mito_36_ubuntu1804
python_version: '3.6'
tox_env: py36-mode_mitogen-distro_ubuntu1804
- name: Mito_36_ubuntu2004
python_version: '3.6' python_version: '3.6'
tox_env: py36-mode_mitogen-distro_ubuntu2004 tox_env: py36-mode_mitogen
- name: Mito_313
- name: Mito_313_centos6
python_version: '3.13'
tox_env: py313-mode_mitogen-distro_centos6
- name: Mito_313_centos7
python_version: '3.13'
tox_env: py313-mode_mitogen-distro_centos7
- name: Mito_313_centos8
python_version: '3.13'
tox_env: py313-mode_mitogen-distro_centos8
- name: Mito_313_debian9
python_version: '3.13'
tox_env: py313-mode_mitogen-distro_debian9
- name: Mito_313_debian10
python_version: '3.13'
tox_env: py313-mode_mitogen-distro_debian10
- name: Mito_313_debian11
python_version: '3.13'
tox_env: py313-mode_mitogen-distro_debian11
- name: Mito_313_ubuntu1604
python_version: '3.13'
tox_env: py313-mode_mitogen-distro_ubuntu1604
- name: Mito_313_ubuntu1804
python_version: '3.13'
tox_env: py313-mode_mitogen-distro_ubuntu1804
- name: Mito_313_ubuntu2004
python_version: '3.13' python_version: '3.13'
tox_env: py313-mode_mitogen-distro_ubuntu2004 tox_env: py313-mode_mitogen
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4

@ -21,6 +21,8 @@ To avail of fixes in an unreleased version, please download a ZIP file
In progress (unreleased) In progress (unreleased)
------------------------ ------------------------
* :gh:issue:`1159` CI: Reduce number of Jobs by parameterizing Mitogen Docker
SSH tests
v0.3.13 (2024-10-09) v0.3.13 (2024-10-09)

@ -30,11 +30,19 @@ and run the tests there.
1. Run ``test`` 1. Run ``test``
# Selecting a target distribution # Selecting target distributions
Docker target images exist for testing against CentOS and Debian, with the Linux container images for testing are available at
default being Debian. To select CentOS, specify `MITOGEN_TEST_DISTRO=centos` in
the environment. - https://github.com/orgs/mitogen-hq/packages
- https://public.ecr.aws/n5z0e8q9
The images used are determined by two environment variables
- `MITOGEN_TEST_DISTRO_SPECS`
- `MITOGEN_TEST_IMAGE_TEMPLATE`
Defaults for these can be found in `.ci/ci_lib.py` & `tests/testlib.py`
# User Accounts # User Accounts

@ -7,8 +7,8 @@ import mitogen.fakessh
import testlib import testlib
class RsyncTest(testlib.DockerMixin, testlib.TestCase):
@unittest.skip('broken') @unittest.skip('broken')
class RsyncTest(testlib.DockerMixin, testlib.TestCase):
def test_rsync_from_master(self): def test_rsync_from_master(self):
context = self.docker_ssh_any() context = self.docker_ssh_any()
@ -24,7 +24,6 @@ class RsyncTest(testlib.DockerMixin, testlib.TestCase):
self.assertTrue(context.call(os.path.exists, '/tmp/data')) self.assertTrue(context.call(os.path.exists, '/tmp/data'))
self.assertTrue(context.call(os.path.exists, '/tmp/data/simple_pkg/a.py')) self.assertTrue(context.call(os.path.exists, '/tmp/data/simple_pkg/a.py'))
@unittest.skip('broken')
def test_rsync_between_direct_children(self): def test_rsync_between_direct_children(self):
# master -> SSH -> mitogen__has_sudo_pubkey -> rsync(.ssh) -> master -> # master -> SSH -> mitogen__has_sudo_pubkey -> rsync(.ssh) -> master ->
# mitogen__has_sudo -> rsync # mitogen__has_sudo -> rsync

@ -37,7 +37,7 @@ class ConstructorTest(testlib.RouterMixin, testlib.TestCase):
self.assertEqual(3, context.call(plain_old_module.add, 1, 2)) self.assertEqual(3, context.call(plain_old_module.add, 1, 2))
class SshTest(testlib.DockerMixin, testlib.TestCase): class SshMixin(testlib.DockerMixin):
def test_debug_decoding(self): def test_debug_decoding(self):
# ensure filter_debug_logs() decodes the logged string. # ensure filter_debug_logs() decodes the logged string.
capture = testlib.LogCapturer() capture = testlib.LogCapturer()
@ -176,7 +176,18 @@ class SshTest(testlib.DockerMixin, testlib.TestCase):
fp.close() fp.close()
class BannerTest(testlib.DockerMixin, testlib.TestCase): for distro_spec in testlib.DISTRO_SPECS.split():
dockerized_ssh = testlib.DockerizedSshDaemon(distro_spec)
klass_name = 'SshTest%s' % (dockerized_ssh.distro.capitalize(),)
klass = type(
klass_name,
(SshMixin, testlib.TestCase),
{'dockerized_ssh': dockerized_ssh},
)
globals()[klass_name] = klass
class BannerMixin(testlib.DockerMixin):
# Verify the ability to disambiguate random spam appearing in the SSHd's # Verify the ability to disambiguate random spam appearing in the SSHd's
# login banner from a legitimate password prompt. # login banner from a legitimate password prompt.
def test_verbose_enabled(self): def test_verbose_enabled(self):
@ -193,6 +204,17 @@ class BannerTest(testlib.DockerMixin, testlib.TestCase):
context.shutdown(wait=True) context.shutdown(wait=True)
for distro_spec in testlib.DISTRO_SPECS.split():
dockerized_ssh = testlib.DockerizedSshDaemon(distro_spec)
klass_name = 'BannerTest%s' % (dockerized_ssh.distro.capitalize(),)
klass = type(
klass_name,
(BannerMixin, testlib.TestCase),
{'dockerized_ssh': dockerized_ssh},
)
globals()[klass_name] = klass
class StubPermissionDeniedTest(StubSshMixin, testlib.TestCase): class StubPermissionDeniedTest(StubSshMixin, testlib.TestCase):
def test_classic_prompt(self): def test_classic_prompt(self):
self.assertRaises(mitogen.ssh.PasswordError, self.assertRaises(mitogen.ssh.PasswordError,

@ -23,7 +23,7 @@ class ConstructorTest(testlib.RouterMixin, testlib.TestCase):
self.assertEqual(argv[2], '-c') self.assertEqual(argv[2], '-c')
class SuTest(testlib.DockerMixin, testlib.TestCase): class SuMixin(testlib.DockerMixin):
stub_su_path = testlib.data_path('stubs/stub-su.py') stub_su_path = testlib.data_path('stubs/stub-su.py')
def test_slow_auth_failure(self): def test_slow_auth_failure(self):
@ -64,3 +64,14 @@ class SuTest(testlib.DockerMixin, testlib.TestCase):
) )
context = self.router.su(via=ssh, password='rootpassword') context = self.router.su(via=ssh, password='rootpassword')
self.assertEqual(0, context.call(os.getuid)) self.assertEqual(0, context.call(os.getuid))
for distro_spec in testlib.DISTRO_SPECS.split():
dockerized_ssh = testlib.DockerizedSshDaemon(distro_spec)
klass_name = 'SuTest%s' % (dockerized_ssh.distro.capitalize(),)
klass = type(
klass_name,
(SuMixin, testlib.TestCase),
{'dockerized_ssh': dockerized_ssh},
)
globals()[klass_name] = klass

@ -51,7 +51,10 @@ except NameError:
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
DISTRO = os.environ.get('MITOGEN_TEST_DISTRO', 'debian9') DISTRO_SPECS = os.environ.get(
'MITOGEN_TEST_DISTRO_SPECS',
'centos6 centos8 debian9 debian11 ubuntu1604 ubuntu2004',
)
IMAGE_TEMPLATE = os.environ.get( IMAGE_TEMPLATE = os.environ.get(
'MITOGEN_TEST_IMAGE_TEMPLATE', 'MITOGEN_TEST_IMAGE_TEMPLATE',
'public.ecr.aws/n5z0e8q9/%(distro)s-test', 'public.ecr.aws/n5z0e8q9/%(distro)s-test',
@ -555,8 +558,9 @@ class DockerizedSshDaemon(object):
self.image, self.image,
] ]
subprocess.check_output(args) subprocess.check_output(args)
self.port = self.get_port(self.container_name)
def __init__(self, distro=DISTRO, image_template=IMAGE_TEMPLATE): def __init__(self, distro_spec, image_template=IMAGE_TEMPLATE):
# Code duplicated in ci_lib.py, both should be updated together # Code duplicated in ci_lib.py, both should be updated together
distro_pattern = re.compile(r''' distro_pattern = re.compile(r'''
(?P<distro>(?P<family>[a-z]+)[0-9]+) (?P<distro>(?P<family>[a-z]+)[0-9]+)
@ -565,7 +569,10 @@ class DockerizedSshDaemon(object):
''', ''',
re.VERBOSE, re.VERBOSE,
) )
d = distro_pattern.match(distro).groupdict(default=None) d = distro_pattern.match(distro_spec).groupdict(default=None)
self.distro = d['distro']
self.family = d['family']
if d.pop('py') == 'py3': if d.pop('py') == 'py3':
self.python_path = '/usr/bin/python3' self.python_path = '/usr/bin/python3'
@ -573,9 +580,7 @@ class DockerizedSshDaemon(object):
self.python_path = '/usr/bin/python' self.python_path = '/usr/bin/python'
self.image = image_template % d self.image = image_template % d
self.start_container()
self.host = get_docker_host() self.host = get_docker_host()
self.port = self.get_port(self.container_name)
def wait_for_sshd(self): def wait_for_sshd(self):
wait_for_port(self.host, self.port, pattern='OpenSSH') wait_for_port(self.host, self.port, pattern='OpenSSH')
@ -648,12 +653,10 @@ class DockerMixin(RouterMixin):
if os.environ.get('SKIP_DOCKER_TESTS'): if os.environ.get('SKIP_DOCKER_TESTS'):
raise unittest.SkipTest('SKIP_DOCKER_TESTS is set') raise unittest.SkipTest('SKIP_DOCKER_TESTS is set')
# we want to be able to override test distro for some tests that need a different container spun up # cls.dockerized_ssh is injected by dynamically generating TestCase
daemon_args = {} # subclasses.
if hasattr(cls, 'mitogen_test_distro'): # TODO Bite the bullet, switch to e.g. pytest
daemon_args['mitogen_test_distro'] = cls.mitogen_test_distro cls.dockerized_ssh.start_container()
cls.dockerized_ssh = DockerizedSshDaemon(**daemon_args)
cls.dockerized_ssh.wait_for_sshd() cls.dockerized_ssh.wait_for_sshd()
@classmethod @classmethod

@ -56,9 +56,7 @@ envlist =
py{27,36}-mode_ansible-ansible{2.10,3,4}, py{27,36}-mode_ansible-ansible{2.10,3,4},
py{311}-mode_ansible-ansible{2.10,3,4,5}, py{311}-mode_ansible-ansible{2.10,3,4,5},
py{313}-mode_ansible-ansible{6,7,8,9,10}, py{313}-mode_ansible-ansible{6,7,8,9,10},
py{27,36,313}-mode_mitogen-distro_centos{6,7,8}, py{27,36,313}-mode_mitogen,
py{27,36,313}-mode_mitogen-distro_debian{9,10,11},
py{27,36,313}-mode_mitogen-distro_ubuntu{1604,1804,2004},
report, report,
[testenv] [testenv]
@ -105,39 +103,27 @@ setenv =
NOCOVERAGE_ERASE = 1 NOCOVERAGE_ERASE = 1
NOCOVERAGE_REPORT = 1 NOCOVERAGE_REPORT = 1
PIP_CONSTRAINT={toxinidir}/tests/constraints.txt PIP_CONSTRAINT={toxinidir}/tests/constraints.txt
# Only applicable to MODE=mitogen
distro_centos5: DISTRO=centos5
distro_centos6: DISTRO=centos6
distro_centos7: DISTRO=centos7
distro_centos8: DISTRO=centos8
distro_debian9: DISTRO=debian9
distro_debian10: DISTRO=debian10
distro_debian11: DISTRO=debian11
distro_ubuntu1604: DISTRO=ubuntu1604
distro_ubuntu1804: DISTRO=ubuntu1804
distro_ubuntu2004: DISTRO=ubuntu2004
# Note the plural, only applicable to MODE=ansible
# Ansible 6 - 8 (ansible-core 2.13 - 2.15) require Python 2.7 or >= 3.5 on targets # Ansible 6 - 8 (ansible-core 2.13 - 2.15) require Python 2.7 or >= 3.5 on targets
ansible6: DISTROS=centos7 centos8 debian9 debian10 debian11 ubuntu1604 ubuntu1804 ubuntu2004 ansible6: MITOGEN_TEST_DISTRO_SPECS=centos7 centos8 debian9 debian10 debian11 ubuntu1604 ubuntu1804 ubuntu2004
ansible7: DISTROS=centos7 centos8 debian9 debian10 debian11 ubuntu1604 ubuntu1804 ubuntu2004 ansible7: MITOGEN_TEST_DISTRO_SPECS=centos7 centos8 debian9 debian10 debian11 ubuntu1604 ubuntu1804 ubuntu2004
ansible8: DISTROS=centos7 centos8 debian9 debian10 debian11 ubuntu1604 ubuntu1804 ubuntu2004 ansible8: MITOGEN_TEST_DISTRO_SPECS=centos7 centos8 debian9 debian10 debian11 ubuntu1604 ubuntu1804 ubuntu2004
# Ansible 9 (ansible-core 2.16) requires Python 2.7 or >= 3.6 on targets # Ansible 9 (ansible-core 2.16) requires Python 2.7 or >= 3.6 on targets
ansible9: DISTROS=centos7 centos8 debian9 debian10 debian11 ubuntu1804 ubuntu2004 ansible9: MITOGEN_TEST_DISTRO_SPECS=centos7 centos8 debian9 debian10 debian11 ubuntu1804 ubuntu2004
# Ansible 10 (ansible-core 2.17) requires Python >= 3.7 on targets # Ansible 10 (ansible-core 2.17) requires Python >= 3.7 on targets
ansible10: DISTROS=debian10-py3 debian11-py3 ubuntu2004-py3 ansible10: MITOGEN_TEST_DISTRO_SPECS=debian10-py3 debian11-py3 ubuntu2004-py3
distros_centos: DISTROS=centos6 centos7 centos8 distros_centos: MITOGEN_TEST_DISTRO_SPECS=centos6 centos7 centos8
distros_centos5: DISTROS=centos5 distros_centos5: MITOGEN_TEST_DISTRO_SPECS=centos5
distros_centos6: DISTROS=centos6 distros_centos6: MITOGEN_TEST_DISTRO_SPECS=centos6
distros_centos7: DISTROS=centos7 distros_centos7: MITOGEN_TEST_DISTRO_SPECS=centos7
distros_centos8: DISTROS=centos8 distros_centos8: MITOGEN_TEST_DISTRO_SPECS=centos8
distros_debian: DISTROS=debian9 debian10 debian11 distros_debian: MITOGEN_TEST_DISTRO_SPECS=debian9 debian10 debian11
distros_debian9: DISTROS=debian9 distros_debian9: MITOGEN_TEST_DISTRO_SPECS=debian9
distros_debian10: DISTROS=debian10 distros_debian10: MITOGEN_TEST_DISTRO_SPECS=debian10
distros_debian11: DISTROS=debian11 distros_debian11: MITOGEN_TEST_DISTRO_SPECS=debian11
distros_ubuntu: DISTROS=ubuntu1604 ubuntu1804 ubuntu2004 distros_ubuntu: MITOGEN_TEST_DISTRO_SPECS=ubuntu1604 ubuntu1804 ubuntu2004
distros_ubuntu1604: DISTROS=ubuntu1604 distros_ubuntu1604: MITOGEN_TEST_DISTRO_SPECS=ubuntu1604
distros_ubuntu1804: DISTROS=ubuntu1804 distros_ubuntu1804: MITOGEN_TEST_DISTRO_SPECS=ubuntu1804
distros_ubuntu2004: DISTROS=ubuntu2004 distros_ubuntu2004: MITOGEN_TEST_DISTRO_SPECS=ubuntu2004
mode_ansible: MODE=ansible mode_ansible: MODE=ansible
mode_ansible: ANSIBLE_SKIP_TAGS=resource_intensive mode_ansible: ANSIBLE_SKIP_TAGS=resource_intensive
mode_ansible: ANSIBLE_CALLBACK_WHITELIST=profile_tasks mode_ansible: ANSIBLE_CALLBACK_WHITELIST=profile_tasks

Loading…
Cancel
Save