From 9859e44ee841fe7655d8bc9a2b74580be32a4810 Mon Sep 17 00:00:00 2001 From: Alex Willmer Date: Thu, 10 Oct 2024 15:23:49 +0100 Subject: [PATCH 1/2] tests: Standardise on DockerizedSshDaemon.host & .port --- tests/ssh_test.py | 4 ++-- tests/testlib.py | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/tests/ssh_test.py b/tests/ssh_test.py index ce7dce96..f8fad55a 100644 --- a/tests/ssh_test.py +++ b/tests/ssh_test.py @@ -68,7 +68,7 @@ class SshTest(testlib.DockerMixin, testlib.TestCase): password='has_sudo_password', ) name = 'ssh.%s:%s' % ( - self.dockerized_ssh.get_host(), + self.dockerized_ssh.host, self.dockerized_ssh.port, ) self.assertEqual(name, context.name) @@ -186,7 +186,7 @@ class BannerTest(testlib.DockerMixin, testlib.TestCase): ssh_debug_level=3, ) name = 'ssh.%s:%s' % ( - self.dockerized_ssh.get_host(), + self.dockerized_ssh.host, self.dockerized_ssh.port, ) self.assertEqual(name, context.name) diff --git a/tests/testlib.py b/tests/testlib.py index 76743e82..0fd9c7d7 100644 --- a/tests/testlib.py +++ b/tests/testlib.py @@ -574,14 +574,11 @@ class DockerizedSshDaemon(object): self.image = image_template % d self.start_container() - self.host = self.get_host() + self.host = get_docker_host() self.port = self.get_port(self.container_name) - def get_host(self): - return get_docker_host() - def wait_for_sshd(self): - wait_for_port(self.get_host(), self.port, pattern='OpenSSH') + wait_for_port(self.host, self.port, pattern='OpenSSH') def check_processes(self): # Get Accounting name (ucomm) & command line (args) of each process From 28e08ef94c5a57ee672511e06ed594d49284644f Mon Sep 17 00:00:00 2001 From: Alex Willmer Date: Thu, 10 Oct 2024 18:33:44 +0100 Subject: [PATCH 2/2] 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 --- .ci/README.md | 11 +++--- .ci/ansible_tests.py | 2 +- .ci/ci_lib.py | 8 ++-- .ci/mitogen_py24_tests.py | 2 - .ci/mitogen_tests.py | 1 - .github/workflows/tests.yml | 78 +++---------------------------------- docs/changelog.rst | 2 + tests/README.md | 16 ++++++-- tests/fakessh_test.py | 3 +- tests/ssh_test.py | 26 ++++++++++++- tests/su_test.py | 13 ++++++- tests/testlib.py | 25 ++++++------ tox.ini | 52 +++++++++---------------- 13 files changed, 101 insertions(+), 138 deletions(-) diff --git a/.ci/README.md b/.ci/README.md index 9a5e8898..17b7d2dd 100644 --- a/.ci/README.md +++ b/.ci/README.md @@ -28,14 +28,15 @@ for doing `setup.py install` while pulling a Docker container, for example. ### Environment Variables -* `DISTRO`: the `mitogen_` tests need a target Docker container distro. This - name comes from the Docker Hub `mitogen` user, i.e. `mitogen/$DISTRO-test` -* `DISTROS`: the `ansible_` tests can run against multiple targets - simultaneously, which speeds things up. This is a space-separated list of - DISTRO names, but additionally, supports: +* `MITOGEN_TEST_DISTRO_SPECS`: a space delimited list of distro specs to run + the tests against. (e.g. `centos6 ubuntu2004-py3*4`). Each spec determines + the Linux distribution, target Python interepreter & number of instances. + Only distributions with a pre-built Linux container image can be used. * `debian-py3`: when generating Ansible inventory file, set `ansible_python_interpreter` to `python3`, i.e. run a test where the target interpreter is Python 3. * `debian*16`: generate 16 Docker containers running Debian. Also works with -py3. +* `MITOGEN_TEST_IMAGE_TEMPLATE`: specifies the Linux container image name, + and hence the container registry used for test targets. diff --git a/.ci/ansible_tests.py b/.ci/ansible_tests.py index 3ec48dfd..62dfa8f5 100755 --- a/.ci/ansible_tests.py +++ b/.ci/ansible_tests.py @@ -35,7 +35,7 @@ ci_lib.check_stray_processes(interesting) 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) diff --git a/.ci/ci_lib.py b/.ci/ci_lib.py index dfe49b97..afb62e02 100644 --- a/.ci/ci_lib.py +++ b/.ci/ci_lib.py @@ -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( 'MITOGEN_TEST_IMAGE_TEMPLATE', '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__), '..')) -# 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 diff --git a/.ci/mitogen_py24_tests.py b/.ci/mitogen_py24_tests.py index 228e79bd..96b144eb 100755 --- a/.ci/mitogen_py24_tests.py +++ b/.ci/mitogen_py24_tests.py @@ -8,8 +8,6 @@ import ci_lib os.environ.update({ 'NOCOVERAGE': '1', 'UNIT2': '/usr/local/python2.4.6/bin/unit2', - - 'MITOGEN_TEST_DISTRO': ci_lib.DISTRO, 'MITOGEN_LOG_LEVEL': 'debug', 'SKIP_ANSIBLE': '1', }) diff --git a/.ci/mitogen_tests.py b/.ci/mitogen_tests.py index 4de94b4c..47aa2444 100755 --- a/.ci/mitogen_tests.py +++ b/.ci/mitogen_tests.py @@ -6,7 +6,6 @@ import os import ci_lib os.environ.update({ - 'MITOGEN_TEST_DISTRO': ci_lib.DISTRO, 'MITOGEN_LOG_LEVEL': 'debug', 'SKIP_ANSIBLE': '1', }) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f3f31d82..cc20f04a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -67,80 +67,14 @@ jobs: python_version: '3.13' tox_env: py313-mode_ansible-ansible10-strategy_linear - - name: Mito_27_centos6 - tox_env: py27-mode_mitogen-distro_centos6 - - name: Mito_27_centos7 - 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 + - name: Mito_27 + tox_env: py27-mode_mitogen + - name: Mito_36 python_version: '3.6' - tox_env: py36-mode_mitogen-distro_ubuntu2004 - - - 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 + tox_env: py36-mode_mitogen + - name: Mito_313 python_version: '3.13' - tox_env: py313-mode_mitogen-distro_ubuntu2004 + tox_env: py313-mode_mitogen steps: - uses: actions/checkout@v4 diff --git a/docs/changelog.rst b/docs/changelog.rst index 092fed26..1fad75a5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -21,6 +21,8 @@ To avail of fixes in an unreleased version, please download a ZIP file In progress (unreleased) ------------------------ +* :gh:issue:`1159` CI: Reduce number of Jobs by parameterizing Mitogen Docker + SSH tests v0.3.13 (2024-10-09) diff --git a/tests/README.md b/tests/README.md index 35a8775c..06bf7ad7 100644 --- a/tests/README.md +++ b/tests/README.md @@ -30,11 +30,19 @@ and run the tests there. 1. Run ``test`` -# Selecting a target distribution +# Selecting target distributions -Docker target images exist for testing against CentOS and Debian, with the -default being Debian. To select CentOS, specify `MITOGEN_TEST_DISTRO=centos` in -the environment. +Linux container images for testing are available at + +- 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 diff --git a/tests/fakessh_test.py b/tests/fakessh_test.py index 52321495..2ad722df 100644 --- a/tests/fakessh_test.py +++ b/tests/fakessh_test.py @@ -7,8 +7,8 @@ import mitogen.fakessh import testlib +@unittest.skip('broken') class RsyncTest(testlib.DockerMixin, testlib.TestCase): - @unittest.skip('broken') def test_rsync_from_master(self): 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/simple_pkg/a.py')) - @unittest.skip('broken') def test_rsync_between_direct_children(self): # master -> SSH -> mitogen__has_sudo_pubkey -> rsync(.ssh) -> master -> # mitogen__has_sudo -> rsync diff --git a/tests/ssh_test.py b/tests/ssh_test.py index f8fad55a..e8e0e1eb 100644 --- a/tests/ssh_test.py +++ b/tests/ssh_test.py @@ -37,7 +37,7 @@ class ConstructorTest(testlib.RouterMixin, testlib.TestCase): 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): # ensure filter_debug_logs() decodes the logged string. capture = testlib.LogCapturer() @@ -176,7 +176,18 @@ class SshTest(testlib.DockerMixin, testlib.TestCase): 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 # login banner from a legitimate password prompt. def test_verbose_enabled(self): @@ -193,6 +204,17 @@ class BannerTest(testlib.DockerMixin, testlib.TestCase): 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): def test_classic_prompt(self): self.assertRaises(mitogen.ssh.PasswordError, diff --git a/tests/su_test.py b/tests/su_test.py index 234c509b..3750454c 100644 --- a/tests/su_test.py +++ b/tests/su_test.py @@ -23,7 +23,7 @@ class ConstructorTest(testlib.RouterMixin, testlib.TestCase): 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') def test_slow_auth_failure(self): @@ -64,3 +64,14 @@ class SuTest(testlib.DockerMixin, testlib.TestCase): ) context = self.router.su(via=ssh, password='rootpassword') 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 diff --git a/tests/testlib.py b/tests/testlib.py index 0fd9c7d7..05779dc0 100644 --- a/tests/testlib.py +++ b/tests/testlib.py @@ -51,7 +51,10 @@ except NameError: 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( 'MITOGEN_TEST_IMAGE_TEMPLATE', 'public.ecr.aws/n5z0e8q9/%(distro)s-test', @@ -555,8 +558,9 @@ class DockerizedSshDaemon(object): self.image, ] 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 distro_pattern = re.compile(r''' (?P(?P[a-z]+)[0-9]+) @@ -565,7 +569,10 @@ class DockerizedSshDaemon(object): ''', 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': self.python_path = '/usr/bin/python3' @@ -573,9 +580,7 @@ class DockerizedSshDaemon(object): self.python_path = '/usr/bin/python' self.image = image_template % d - self.start_container() self.host = get_docker_host() - self.port = self.get_port(self.container_name) def wait_for_sshd(self): wait_for_port(self.host, self.port, pattern='OpenSSH') @@ -648,12 +653,10 @@ class DockerMixin(RouterMixin): if os.environ.get('SKIP_DOCKER_TESTS'): 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 - daemon_args = {} - if hasattr(cls, 'mitogen_test_distro'): - daemon_args['mitogen_test_distro'] = cls.mitogen_test_distro - - cls.dockerized_ssh = DockerizedSshDaemon(**daemon_args) + # cls.dockerized_ssh is injected by dynamically generating TestCase + # subclasses. + # TODO Bite the bullet, switch to e.g. pytest + cls.dockerized_ssh.start_container() cls.dockerized_ssh.wait_for_sshd() @classmethod diff --git a/tox.ini b/tox.ini index 6a0eb180..5afa7eb1 100644 --- a/tox.ini +++ b/tox.ini @@ -56,9 +56,7 @@ envlist = py{27,36}-mode_ansible-ansible{2.10,3,4}, py{311}-mode_ansible-ansible{2.10,3,4,5}, 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-distro_debian{9,10,11}, - py{27,36,313}-mode_mitogen-distro_ubuntu{1604,1804,2004}, + py{27,36,313}-mode_mitogen, report, [testenv] @@ -105,39 +103,27 @@ setenv = NOCOVERAGE_ERASE = 1 NOCOVERAGE_REPORT = 1 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 - ansible6: DISTROS=centos7 centos8 debian9 debian10 debian11 ubuntu1604 ubuntu1804 ubuntu2004 - ansible7: DISTROS=centos7 centos8 debian9 debian10 debian11 ubuntu1604 ubuntu1804 ubuntu2004 - ansible8: DISTROS=centos7 centos8 debian9 debian10 debian11 ubuntu1604 ubuntu1804 ubuntu2004 + ansible6: MITOGEN_TEST_DISTRO_SPECS=centos7 centos8 debian9 debian10 debian11 ubuntu1604 ubuntu1804 ubuntu2004 + ansible7: MITOGEN_TEST_DISTRO_SPECS=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 - 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 - ansible10: DISTROS=debian10-py3 debian11-py3 ubuntu2004-py3 - distros_centos: DISTROS=centos6 centos7 centos8 - distros_centos5: DISTROS=centos5 - distros_centos6: DISTROS=centos6 - distros_centos7: DISTROS=centos7 - distros_centos8: DISTROS=centos8 - distros_debian: DISTROS=debian9 debian10 debian11 - distros_debian9: DISTROS=debian9 - distros_debian10: DISTROS=debian10 - distros_debian11: DISTROS=debian11 - distros_ubuntu: DISTROS=ubuntu1604 ubuntu1804 ubuntu2004 - distros_ubuntu1604: DISTROS=ubuntu1604 - distros_ubuntu1804: DISTROS=ubuntu1804 - distros_ubuntu2004: DISTROS=ubuntu2004 + ansible10: MITOGEN_TEST_DISTRO_SPECS=debian10-py3 debian11-py3 ubuntu2004-py3 + distros_centos: MITOGEN_TEST_DISTRO_SPECS=centos6 centos7 centos8 + distros_centos5: MITOGEN_TEST_DISTRO_SPECS=centos5 + distros_centos6: MITOGEN_TEST_DISTRO_SPECS=centos6 + distros_centos7: MITOGEN_TEST_DISTRO_SPECS=centos7 + distros_centos8: MITOGEN_TEST_DISTRO_SPECS=centos8 + distros_debian: MITOGEN_TEST_DISTRO_SPECS=debian9 debian10 debian11 + distros_debian9: MITOGEN_TEST_DISTRO_SPECS=debian9 + distros_debian10: MITOGEN_TEST_DISTRO_SPECS=debian10 + distros_debian11: MITOGEN_TEST_DISTRO_SPECS=debian11 + distros_ubuntu: MITOGEN_TEST_DISTRO_SPECS=ubuntu1604 ubuntu1804 ubuntu2004 + distros_ubuntu1604: MITOGEN_TEST_DISTRO_SPECS=ubuntu1604 + distros_ubuntu1804: MITOGEN_TEST_DISTRO_SPECS=ubuntu1804 + distros_ubuntu2004: MITOGEN_TEST_DISTRO_SPECS=ubuntu2004 mode_ansible: MODE=ansible mode_ansible: ANSIBLE_SKIP_TAGS=resource_intensive mode_ansible: ANSIBLE_CALLBACK_WHITELIST=profile_tasks