Skip to content

Add tox replacement #1558

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 56 commits into from
Closed

Add tox replacement #1558

wants to merge 56 commits into from

Conversation

Kyle-Verhoog
Copy link
Member

@Kyle-Verhoog Kyle-Verhoog commented Jul 7, 2020

Tox is slow and the configuration language is unwieldy with our use case.

This PR introduces a custom solution that aims to provide a better specified and more optimized solution for running our matrix of tests.

Preliminary results (without pre-generating and sharing base virtual envs, which could save about 2m in the best case):

Configuration Example (Django)

    Suite(
        name="django",
        command="pytest tests/contrib/django",
        env=[("TEST_DATADOG_DJANGO_MIGRATION", [None, "1"])],
        cases=[
            Case(
                pys=[2.7, 3.5, 3.6],
                pkgs=[
                    ("django-pylibmc", [">=0.6,<0.7"]),
                    ("django-redis", [">=4.5,<4.6"]),
                    ("pylibmc", [""]),
                    ("python-memcached", [""]),
                    ("django", [">=1.8,<1.9", ">=1.11,<1.12"]),
                ],
            ),
            Case(
                pys=[3.5],
                pkgs=[
                    ("django-pylibmc", [">=0.6,<0.7"]),
                    ("django-redis", [">=4.5,<4.6"]),
                    ("pylibmc", [""]),
                    ("python-memcached", [""]),
                    ("django", [">=2.0,<2.1", ">=2.1,<2.2"]),
                ],
            ),
            Case(
                pys=[3.6, 3.7, 3.8],
                pkgs=[
                    ("django-pylibmc", [">=0.6,<0.7"]),
                    ("django-redis", [">=4.5,<4.6"]),
                    ("pylibmc", [""]),
                    ("python-memcached", [""]),
                    ("django", [">=2.0,<2.1", ">=2.1,<2.2", ">=2.2,<2.3", ">=3.0,<3.1", ""]),
                ],
            ),
        ],
    ),

Equivalent in tox:

[tox]
envlist =
    django_contrib{,_migration}-py{27,35,36}-django{18,111}-djangopylibmc06-djangoredis45-pylibmc-redis{210}-memcached
    django_contrib{,_migration}-py35-django{20,21,22}-djangopylibmc06-djangoredis45-pylibmc-redis{210}-memcached
    django_contrib{,_migration}-py{36,37,38}-django{20,21,22,30,}-djangopylibmc06-djangoredis45-pylibmc-redis{210}-memcached

# ...

[testenv]
setenv =
    django_contrib_migration: TEST_DATADOG_DJANGO_MIGRATION=1
# ...
deps=
    django: django
    django18: django>=1.8,<1.9
    django111: django>=1.11,<1.12
    django20: django>=2.0,<2.1
    django21: django>=2.1,<2.2
    django22: django>=2.2,<2.3
    django30: django>=3.0,<3.1
    djangopylibmc06: django-pylibmc>=0.6,<0.7
    djangoredis45: django-redis>=4.5,<4.6
    memcached: python-memcached
    pylibmc: pylibmc

# ...

commands =
    django_contrib: pytest {posargs} tests/contrib/django
    django_contrib_migration: pytest {posargs} tests/contrib/django

Configuration Example (Profiling)

Configuration is done in Python.

    Suite(
        name="profiling",
        command="python -m tests.profiling.run pytest --capture=no --verbose tests/profiling/",
        env=[("DD_PROFILE_TEST_GEVENT", lambda case: "1" if "gevent" in case.pkgs else None),],
        cases=[
            Case(
                 pys=[2.7, 3.5, 3.6, 3.7, 3.8],
                 pkgs=[("gevent", [None, ""])],
            ),
            # Min reqs tests
            Case(
                 pys=[2.7],
                 pkgs=[("gevent", ["==1.1.0"]), ("protobuf", ["==3.0.0"]), ("tenacity", ["==5.0.1"]),]
            ),
            Case(
                pys=[3.5, 3.6, 3.7, 3.8],
                pkgs=[("gevent", ["==1.4.0"]), ("protobuf", ["==3.0.0"]), ("tenacity", ["==5.0.1"]),]
            ),
        ],
    ),

equivalent tox configuration:

[tox]
envlist =
    {py27,py35,py36,py37,py38}-profile{,-gevent}
    {py27,py35,py36,py37,py38}-profile-minreqs{,-gevent}

# ...

[testenv]
setenv =
    profile-gevent: DD_PROFILE_TEST_GEVENT=1

# ...

deps=
    profile-minreqs: protobuf==3.0.0
    profile-minreqs: tenacity==5.0.1
    profile-!minreqs-gevent: gevent
    py27-profile-minreqs-gevent: gevent==1.1.0
    !py27-profile-minreqs-gevent: gevent==1.4.0

# ...

commands =
      profile: python -m tests.profiling.run pytest --capture=no --verbose {posargs} tests/profiling

Copy link
Member

@brettlangdon brettlangdon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from a high level, I like it.

Like we discussed before, this style syntax feels more verbose looking than the normal tox redis_contrib_py{27,35,36,37,38}-redis{21,20,32,34,35} env definition but think it is much more clear what is going on, less jumping around a massive file.

I think any other nit-picks I had (not specified) will be solved when this is it's own module/package.

riot.py Outdated
pkgs=[
("django-pylibmc", [">=0.6,<0.7"]),
("django-redis", [">=4.5,<4.6"]),
("pylibmc", [""]),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should alias "latest" -> "" would it make it more clear to read?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or just latest = "" -> ("pylibmc", [latest]). Either way I think we're in agreement that "" is kinda subtle.

riot.py Outdated
"""Return the command string used to execute `cmd` in virtual env located
at `venv_path`.
"""
return "source %s/bin/activate && %s" % (venv_path, cmd)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you probably don't have to call source. If you read through activate majority of it is setting up deactivate, and then setting a few environment variables:

export VIRTUAL_ENV=<path_to_venv>
export PATH=<venv>/bin:$PATH
unset PYTHONHOME

And that is in.

In fact, you can generally get by just calling from <venv>/bin/<cmd> directly without modifying any env variables and it'll work as expected.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling source .../bin/activate is probably the right thing to do

Copy link
Contributor

@jd jd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok so I'm a bit surprised by this but it looks definitely good.
I think it needs a few more iteration and polish, but this is a really good starting point to do something cool.

We might want to polish the architecture a bit, using a few classes here and there — and maybe some docstrings.

default: ""
steps:
- checkout
- run: pip3 install virtualenv # TODO: this can be baked into the image
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though the image is not updated that often, so it might be better to have the latest virtualenv deployed each time.

- run_tox_scenario:
pattern: '^py..-profile'
- run_test:
pattern: "profiling"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

detail, but you don't need the "" I think

riot.py Outdated


# It's easier to read `Suite`, `Case` rather than AttrDict or dict.
CaseInstance = Case = Suite = AttrDict
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having custom classes such as:

class Case(AttrDict): pass

Is going to be a little bit better when introspecting or debugging.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yepyepyep definitely, a lot of the functions laying around are really "methods" as well. This is purely just a result of iterating a ton and not wanting to deal with class boilerplate 🙂

riot.py Outdated

ref: https://www.python.org/dev/peps/pep-0508/
"""
return "%s%s" % (libname, version)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return "%s%s" % (libname, version)
return f"{libname}{version}"

;)

riot.py Outdated
Comment on lines 152 to 153
def get_env_str(envs: t.List[t.Tuple]):
return " ".join("%s=%s" % (k, v) for k, v in envs)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def get_env_str(envs: t.List[t.Tuple]):
return " ".join("%s=%s" % (k, v) for k, v in envs)
def get_env_str(envs: t.List[t.Tuple]):
return " ".join(f"{k}={v}" for k, v in envs)

riot.py Outdated
yield case, env_cfg, py, pkg_cfg


def suites_iter(suites, pattern, py=None):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like that this could be a little more organized by using classes.

riot.py Outdated
# Copy the base venv to use for this case.
logger.info("Copying base virtualenv '%s' into case virtual env '%s'.", base_venv, venv)
try:
run_cmd(["cp", "-r", base_venv, venv], stdout=subprocess.PIPE)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd use shutil.copytree instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Originally had it but it won't allow you to overwrite a directory if it already exists.

riot.py Outdated
s = "%s %s: %s python%s %s" % (status_char, r.case.suite.name, env_str, r.case.py, r.pkgstr)
print(s, file=out)

if any(True for r in results if r.code != 0):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if any(True for r in results if r.code != 0):
if any(r.code != 0 for r in results):

@Kyle-Verhoog Kyle-Verhoog force-pushed the riot branch 3 times, most recently from 242c390 to 43e6f58 Compare July 16, 2020 16:48
@Kyle-Verhoog
Copy link
Member Author

going to close now since @majorgreys and I have got most of this functionality merged in other PRs now. #1737 #1771 #1747 #1786 #1783 and more

@Kyle-Verhoog Kyle-Verhoog deleted the riot branch November 11, 2020 02:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants