3

I would like to run set of specific tests within Docker container and not sure how to tackle this. Tests I want to perform are security-related, like create user(s), manage GPG keys for them and similar - which I am reluctant to run on PC running the tests.

I tried pytest-xdist/socketserver combo and also copying tests into running Docker container and use pytest-json-report to get result(s) as json saved to a volume shared with the host, but not sure this approach is good.

For now, I would settle with all tests (without mark or similar other features) are executed "remotely" (in Docker) and results are presented like everything is ran on local PC.

Don't mind writing a specific plugin, but not sure if this is a good way: do I have to make sure than, my plugin is loaded before say, pytest-xdist (or some others)? Additionally, if I use say, pytest_sessionstart in my conftest.py to build Docker image that I can then target with xdist; but my tests have also some "dependency" that I have to put within conftest.py - I cant use same conftest.py within container and in my PC running the test.

Thank you in advance

nikoladsp
  • 115
  • 2
  • 8
  • Normally you just run `pytest` inside a container with all your test files. Not sure if I understand correctly, but if you're trying to use `pytest_xdist` to remotely execute code in another host (which is a single docker container) then that's just too much – EDG956 Aug 26 '22 at 06:56
  • @EDG956 Yes, running pytest inside a container is fine. In that case, how/what to call from host? Shall I make bash/python script, or use pytest call itself - this is what I am not sure what to do in such scenario, how to present the results so it looks like call was made on local host and "hide" that execution was performed inside container? Preferably, in such situation, I would like to make call to pytest on local PC, which in turn build an image, run it and collect results that are displayed - if that is possible – nikoladsp Aug 26 '22 at 07:07
  • If this docker container is only for running tests you could even say that your image's entrypoint is `pytest` and just pass extra commands to your `docker run` command. Normally, though, you'd have a generic entrypoint, the simplest of which would be `bash -c` and you'd run `docker run pytest ` – EDG956 Aug 26 '22 at 07:08
  • If you want to avoid calling docker directly to run these tests then a bash script doing that would do, sure – EDG956 Aug 26 '22 at 07:09
  • @EDG956 would this be a problem if some CI eventually gets involved? I would like to make an impression call was made as pytest. Sorry, maybe I am not sounding sane :) – nikoladsp Aug 26 '22 at 07:13
  • Many CI tools allow you to define steps inside a docker environment, in which you would just issue commands as if running them in a normal host (physical or virtual, but not docker). In that case, you'd just run `pytest` and know that it will be executed inside a container spawned from the image you built. Then, it's likely that you'll want to have a generic `entrypoint` such that you can use the same command (`CMD`) as you would in a shell – EDG956 Aug 26 '22 at 07:17
  • [Using `entrypoint` and `cmd`](https://stackoverflow.com/a/53544006/8868327) should give you some understanding on what I mean – EDG956 Aug 26 '22 at 07:18
  • @EDG956 Forgot to ask: is it possible to have one conftest.py for calling pytest on host and separate for tests run in Docker? – nikoladsp Aug 26 '22 at 07:29
  • I think there are a few ways you can achieve that. Off the top of my head, I'd say you could overwrite a conftest in docker with another one with a different name in local, but the details escape me. You'd be better off asking a new question. – EDG956 Aug 26 '22 at 07:50
  • Follow-up or closely related questions are normally not encouraged, as this [question on StackOverflow meta](https://meta.stackoverflow.com/questions/266767/what-is-the-the-best-way-to-ask-follow-up-questions) shows. – EDG956 Aug 26 '22 at 07:53

1 Answers1

1

In case anyone else maybe have similar need, I will share what I did.

First of all, there is already an excellent pytest-json-report to export JSON results. However, I made simpler and with less functionality plugin that uses pytest_report_to_serializable directly:

import json
from socket import gethostname


def pytest_addoption(parser, pluginmanager):
    parser.addoption(
        '--report-file', default='%s.json' % gethostname(), help='path to JSON report'
    )


def pytest_configure(config):
    plugin = JsonTestsExporter(config=config)
    config._json_report = plugin
    config.pluginmanager.register(plugin)


def pytest_unconfigure(config):
    plugin = getattr(config, '_json_report', None)
    if plugin is not None:
        del config._json_report
        config.pluginmanager.unregister(plugin)

    print('Report saved in: %s' % config.getoption('--report-file'))


class JsonTestsExporter(object):

    def __init__(self, config):
        self._config = config
        self._export_data = {'collected': 0, 'results': []}

    def pytest_report_collectionfinish(self, config, start_path, startdir, items):
        self._export_data['collected'] = len(items)

    def pytest_runtest_logreport(self, report):
        data = self._config.hook.pytest_report_to_serializable(
            config=self._config, report=report
        )
        self._export_data['results'].append(data)

    def pytest_sessionfinish(self, session):
        report_file = self._config.getoption('--report-file')
        with open(report_file, 'w+') as fd:
            fd.write(json.dumps(self._export_data))

Reason beyond this is that I wanted results also imported using pytest_report_from_serializable.

Simplified Dockerfile:

FROM debian:buster-slim AS builder

COPY [ "requirements.txt", "run.py", "/artifacts/" ]
COPY [ "json_tests_exporter", "/artifacts/json_tests_exporter/" ]

RUN apt-get update\
# install necesssary packages
 && apt-get install --no-install-recommends -y python3-pip python3-setuptools\
# build json_tests_exporter *.whl
 && pip3 install wheel\
 && sh -c 'cd /artifacts/json_tests_exporter && python3 setup.py bdist_wheel'

FROM debian:buster-slim

ARG USER_UID=1000
ARG USER_GID=${USER_UID}

COPY --from=builder --chown=${USER_UID}:${USER_GID} /artifacts /artifacts

RUN apt-get update\
# install necesssary packages
 && apt-get install --no-install-recommends -y wget gpg openssl python3-pip\
# create user to perform tests
 && groupadd -g ${USER_GID} pytest\
 && adduser --disabled-password --gecos "" --uid ${USER_UID} --gid ${USER_GID} pytest\
# copy/install entrypoint script and preserver permissions
 && cp -p /artifacts/run.py /usr/local/bin/run.py\
# install required Python libraries
 && su pytest -c "pip3 install -r /artifacts/requirements.txt"\
 && su pytest -c "pip3 install /artifacts/json_tests_exporter/dist/*.whl"\
# make folder for tests and results
 && su pytest -c "mkdir -p /home/pytest/tests /home/pytest/results"

VOLUME [ "/home/pytest/tests", "/home/pytest/results" ]

USER pytest

WORKDIR /home/pytest/tests

ENTRYPOINT [ "/usr/local/bin/run.py" ]

JSON exporter plugin is located in same folder as Dockerfile

run.py is as simple as:

#!/usr/bin/python3

import pytest
import sys
from socket import gethostname


def main():
    if 1 == len(sys.argv):
        # use default arguments
        args = [
            '--report-file=/home/pytest/results/%s.json' % gethostname(),
            '-qvs',
            '/home/pytest/tests'
            ]
    else:
        # caller passed custom arguments
        args = sys.argv[1:]

    try:        
        res = pytest.main(args)
    except Exception as e:
        print(e)
        res = 1

    return res


if __name__ == "__main__":
    sys.exit(main())

requirements.txt only contains:

python-gnupg==0.4.4
pytest>=7.1.2

So basically, I can run everything with:

docker build -t pytest-runner ./tests/docker/pytest_runner
docker run --rm -it -v $(pwd)/tests/results:/home/pytest/results -v $(pwd)/tests/fixtures:/home/pytest/tests pytest-runner

Last two lines I made programatically run from Python in pytest_sessionstart(session) hook using Docker API.

nikoladsp
  • 115
  • 2
  • 8