Repository: tox-dev/tox
Pull request #860 (2018-06-26 15:41:19)

Replace the PEP-386 parser for the more general PEP-440
This means we drop the vendor and not really tested ``_verlib.py`` (PEP-386) in favour of a more general and battle-tested PEP-440 implementation of the packaging library (new install requires dependency).

Slight refactor of the functions affected to make them more readable.

codecov[bot]: # [Codecov](https://codecov.io/gh/tox-dev/tox/pull/860?src=pr&el=h1) Report
> Merging [#860](https://codecov.io/gh/tox-dev/tox/pull/860?src=pr&el=desc) into [master](https://codecov.io/gh/tox-dev/tox/commit/88aa3052773fdc0c44663249dacadbd2622901a9?src=pr&el=desc) will **decrease** coverage by `<1%`.
> The diff coverage is `93%`.


```diff
@@          Coverage Diff           @@
##           master   #860    +/-   ##
======================================
- Coverage      93%    92%   -<1%     
======================================
  Files          12     12            
  Lines        2326   2327     +1     
  Branches      408    408            
======================================
- Hits         2153   2151     -2     
- Misses        107    108     +1     
- Partials       66     68     +2
```

asottile: I'd also comb through the official plugins and make PRs if you're going to change it -- breaking backward compatibility should not be taken lightly :)
gaborbernat: Both the functions and variables are related to PR: I've refactored the code handling the dependency parsing: As part of the dependency parsing the version parsing is used and as such the version parser is invoked via the ``_getresolvedeps`` path.

Private functions are that private. They don't promise any compatibility. So there was no compatibility guarantee there in the first place. 

Git blame has little value overall (especially given no one in the git blame are maintaining the library currently), compared to maintaining the library. Recently I've tried adding type information to the library, and the version library what we don't test was a major pain point to showcase an example. I value an easy to read code over maintainability any day. ``_getresolvedeps`` break at least snake case usage as far as readability goes. 

I'm changing the behaviour ever so slightest, as note PEP-440 is a bit more permissive than PEP-338. Private functions are **private**, they don't need a migration path. That being said let's meet in the middle:

I've kept the private function as it was but made it deprecated. We'll remove later on. As migration path, you can use the public ``get_resolved_dependencies`` function in the future.

Patch:
diff --git a/.codecov.yml b/.codecov.yml
index f82b11d3..bac24a18 100644
--- a/.codecov.yml
+++ b/.codecov.yml
@@ -24,6 +24,3 @@ comment:
   layout: "header, diff"
   behavior: default
   require_changes: no
-
-ignore:
-  - "src/tox/_verlib.py"  # vendored in and not internally tested
diff --git a/changelog/860.feature.rst b/changelog/860.feature.rst
new file mode 100644
index 00000000..6cafb5ca
--- /dev/null
+++ b/changelog/860.feature.rst
@@ -0,0 +1,1 @@
+Replace the internal version parsing logic from the not well tested `PEP-386 <https://www.python.org/dev/peps/pep-0386/>`_ parser for the more general `PEP-440 <https://www.python.org/dev/peps/pep-0440/>`_. `packaging >= 17.1 <https://pypi.org/project/packaging/>`_ is now an install dependency by :user:`gaborbernat`.
diff --git a/setup.py b/setup.py
index eb81e8de..4efb85be 100644
--- a/setup.py
+++ b/setup.py
@@ -50,7 +50,13 @@ def main():
         },
         python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*",
         setup_requires=["setuptools_scm"],
-        install_requires=["py>=1.4.17", "pluggy>=0.3.0,<1.0", "six", "virtualenv>=1.11.2"],
+        install_requires=[
+            "packaging  >= 17.1",
+            "pluggy >= 0.3.0, <1",
+            "py >= 1.4.17, <2",
+            "six >= 1.0.0, <2",
+            "virtualenv >= 1.11.2",
+        ],
         extras_require={
             "testing": [
                 "pytest >= 3.0.0",
diff --git a/src/tox/_verlib.py b/src/tox/_verlib.py
deleted file mode 100644
index da1a0a92..00000000
--- a/src/tox/_verlib.py
+++ /dev/null
@@ -1,230 +0,0 @@
-"""
-PEP386-version comparison algorithm.
-
-(c) Tarek Ziade and others
-extracted unmodified from https://bitbucket.org/tarek/distutilsversion
-licensed under the PSF license (i guess)
-
-UPDATE 2017-09-06: removed suggest_normalized_version function as it is not needed by the project
-"""
-import re
-
-
-class IrrationalVersionError(Exception):
-    """This is an irrational version."""
-
-    pass
-
-
-class HugeMajorVersionNumError(IrrationalVersionError):
-    """An irrational version because the major version number is huge
-    (often because a year or date was used).
-
-    See `error_on_huge_major_num` option in `NormalizedVersion` for details.
-    This guard can be disabled by setting that option False.
-    """
-
-    pass
-
-
-# A marker used in the second and third parts of the `parts` tuple, for
-# versions that don't have those segments, to sort properly. An example
-# of versions in sort order ('highest' last):
-#   1.0b1                 ((1,0), ('b',1), ('f',))
-#   1.0.dev345            ((1,0), ('f',),  ('dev', 345))
-#   1.0                   ((1,0), ('f',),  ('f',))
-#   1.0.post256.dev345    ((1,0), ('f',),  ('f', 'post', 256, 'dev', 345))
-#   1.0.post345           ((1,0), ('f',),  ('f', 'post', 345, 'f'))
-#                                   ^        ^                 ^
-#   'b' < 'f' ---------------------/         |                 |
-#                                            |                 |
-#   'dev' < 'f' < 'post' -------------------/                  |
-#                                                              |
-#   'dev' < 'f' ----------------------------------------------/
-# Other letters would do, but 'f' for 'final' is kind of nice.
-FINAL_MARKER = ("f",)
-
-VERSION_RE = re.compile(
-    r"""
-    ^
-    (?P<version>\d+\.\d+)          # minimum 'N.N'
-    (?P<extraversion>(?:\.\d+)*)   # any number of extra '.N' segments
-    (?:
-        (?P<prerel>[abc]|rc)       # 'a'=alpha, 'b'=beta, 'c'=release candidate
-                                   # 'rc'= alias for release candidate
-        (?P<prerelversion>\d+(?:\.\d+)*)
-    )?
-    (?P<postdev>(\.post(?P<post>\d+))?(\.dev(?P<dev>\d+))?)?
-    $""",
-    re.VERBOSE,
-)
-
-
-class NormalizedVersion(object):
-    """A rational version.
-
-    Good:
-        1.2         # equivalent to "1.2.0"
-        1.2.0
-        1.2a1
-        1.2.3a2
-        1.2.3b1
-        1.2.3c1
-        1.2.3.4
-        TODO: fill this out
-
-    Bad:
-        1           # mininum two numbers
-        1.2a        # release level must have a release serial
-        1.2.3b
-    """
-
-    def __init__(self, s, error_on_huge_major_num=True):
-        """Create a NormalizedVersion instance from a version string.
-
-        @param s {str} The version string.
-        @param error_on_huge_major_num {bool} Whether to consider an
-            apparent use of a year or full date as the major version number
-            an error. Default True. One of the observed patterns on PyPI before
-            the introduction of `NormalizedVersion` was version numbers like this:
-                2009.01.03
-                20040603
-                2005.01
-            This guard is here to strongly encourage the package author to
-            use an alternate version, because a release deployed into PyPI
-            and, e.g. downstream Linux package managers, will forever remove
-            the possibility of using a version number like "1.0" (i.e.
-            where the major number is less than that huge major number).
-        """
-        self._parse(s, error_on_huge_major_num)
-
-    @classmethod
-    def from_parts(cls, version, prerelease=FINAL_MARKER, devpost=FINAL_MARKER):
-        return cls(cls.parts_to_str((version, prerelease, devpost)))
-
-    def _parse(self, s, error_on_huge_major_num=True):
-        """Parses a string version into parts."""
-        match = VERSION_RE.search(s)
-        if not match:
-            raise IrrationalVersionError(s)
-
-        groups = match.groupdict()
-        parts = []
-
-        # main version
-        block = self._parse_numdots(groups["version"], s, False, 2)
-        extraversion = groups.get("extraversion")
-        if extraversion not in ("", None):
-            block += self._parse_numdots(extraversion[1:], s)
-        parts.append(tuple(block))
-
-        # prerelease
-        prerel = groups.get("prerel")
-        if prerel is not None:
-            block = [prerel]
-            block += self._parse_numdots(groups.get("prerelversion"), s, pad_zeros_length=1)
-            parts.append(tuple(block))
-        else:
-            parts.append(FINAL_MARKER)
-
-        # postdev
-        if groups.get("postdev"):
-            post = groups.get("post")
-            dev = groups.get("dev")
-            postdev = []
-            if post is not None:
-                postdev.extend([FINAL_MARKER[0], "post", int(post)])
-                if dev is None:
-                    postdev.append(FINAL_MARKER[0])
-            if dev is not None:
-                postdev.extend(["dev", int(dev)])
-            parts.append(tuple(postdev))
-        else:
-            parts.append(FINAL_MARKER)
-        self.parts = tuple(parts)
-        if error_on_huge_major_num and self.parts[0][0] > 1980:
-            raise HugeMajorVersionNumError(
-                "huge major version number, {!r}, which might cause future problems: {!r}".format(
-                    self.parts[0][0], s
-                )
-            )
-
-    def _parse_numdots(self, s, full_ver_str, drop_trailing_zeros=True, pad_zeros_length=0):
-        """Parse 'N.N.N' sequences, return a list of ints.
-
-        @param s {str} 'N.N.N..." sequence to be parsed
-        @param full_ver_str {str} The full version string from which this
-            comes. Used for error strings.
-        @param drop_trailing_zeros {bool} Whether to drop trailing zeros
-            from the returned list. Default True.
-        @param pad_zeros_length {int} The length to which to pad the
-            returned list with zeros, if necessary. Default 0.
-        """
-        nums = []
-        for n in s.split("."):
-            if len(n) > 1 and n[0] == "0":
-                raise IrrationalVersionError(
-                    "cannot have leading zero in version number segment: '{}' in {!r}".format(
-                        n, full_ver_str
-                    )
-                )
-            nums.append(int(n))
-        if drop_trailing_zeros:
-            while nums and nums[-1] == 0:
-                nums.pop()
-        while len(nums) < pad_zeros_length:
-            nums.append(0)
-        return nums
-
-    def __str__(self):
-        return self.parts_to_str(self.parts)
-
-    @classmethod
-    def parts_to_str(cls, parts):
-        """Transform a version expressed in tuple into its string representation."""
-        # FIXME XXX This doesn't check for invalid tuples
-        main, prerel, postdev = parts
-        s = ".".join(str(v) for v in main)
-        if prerel is not FINAL_MARKER:
-            s += prerel[0]
-            s += ".".join(str(v) for v in prerel[1:])
-        if postdev and postdev is not FINAL_MARKER:
-            if postdev[0] == "f":
-                postdev = postdev[1:]
-            i = 0
-            while i < len(postdev):
-                if i % 2 == 0:
-                    s += "."
-                s += str(postdev[i])
-                i += 1
-        return s
-
-    def __repr__(self):
-        return "{}('{}')".format(self.__class__.__name__, self)
-
-    def _cannot_compare(self, other):
-        raise TypeError(
-            "cannot compare {} and {}".format(type(self).__name__, type(other).__name__)
-        )
-
-    def __eq__(self, other):
-        if not isinstance(other, NormalizedVersion):
-            self._cannot_compare(other)
-        return self.parts == other.parts
-
-    def __lt__(self, other):
-        if not isinstance(other, NormalizedVersion):
-            self._cannot_compare(other)
-        return self.parts < other.parts
-
-    def __ne__(self, other):
-        return not self.__eq__(other)
-
-    def __gt__(self, other):
-        return not (self.__lt__(other) or self.__eq__(other))
-
-    def __le__(self, other):
-        return self.__eq__(other) or self.__lt__(other)
-
-    def __ge__(self, other):
-        return self.__eq__(other) or self.__gt__(other)
diff --git a/src/tox/config.py b/src/tox/config.py
index 609a10bc..a5095e89 100755
--- a/src/tox/config.py
+++ b/src/tox/config.py
@@ -15,9 +15,9 @@ from subprocess import list2cmdline
 import pkg_resources
 import pluggy
 import py
+from packaging.version import parse
 
 import tox
-from tox._verlib import NormalizedVersion
 from tox.interpreters import Interpreters
 
 hookimpl = tox.hookimpl
@@ -898,11 +898,13 @@ class parseini:
         # prevent parsing of tox.ini this must be the first thing checked.
         config.minversion = reader.getstring("minversion", None)
         if config.minversion:
-            minversion = NormalizedVersion(self.config.minversion)
-            toxversion = NormalizedVersion(tox.__version__)
-            if toxversion < minversion:
+            tox_version = parse(tox.__version__)
+            config_min_version = parse(self.config.minversion)
+            if config_min_version > tox_version:
                 raise tox.exception.MinVersionError(
-                    "tox version is {}, required is at least {}".format(toxversion, minversion)
+                    "tox version is {}, required is at least {}".format(
+                        tox.__version__, self.config.minversion
+                    )
                 )
         if config.option.workdir is None:
             config.toxworkdir = reader.getpath("toxworkdir", "{toxinidir}/.tox")
diff --git a/src/tox/session.py b/src/tox/session.py
index ea4b9530..99826dac 100644
--- a/src/tox/session.py
+++ b/src/tox/session.py
@@ -14,9 +14,9 @@ import sys
 import time
 
 import py
+from packaging.version import InvalidVersion, Version
 
 import tox
-from tox._verlib import IrrationalVersionError, NormalizedVersion
 from tox.config import parseconfig
 from tox.result import ResultLog
 from tox.venv import VirtualEnv
@@ -575,7 +575,7 @@ class Session:
             path = self.config.option.installpkg
             if not path:
                 path = self.config.sdistsrc
-            path = self._resolve_pkg(path)
+            path = self._resolve_package(path)
             self.report.info("using package {!r}, skipping 'sdist' activity ".format(str(path)))
         else:
             try:
@@ -730,17 +730,17 @@ class Session:
         versions.append("virtualenv-{}".format(out.decode("UTF-8").strip()))
         self.report.keyvalue("tool-versions:", " ".join(versions))
 
-    def _resolve_pkg(self, pkgspec):
+    def _resolve_package(self, package_spec):
         try:
-            return self._spec2pkg[pkgspec]
+            return self._spec2pkg[package_spec]
         except KeyError:
-            self._spec2pkg[pkgspec] = x = self._resolvepkg(pkgspec)
+            self._spec2pkg[package_spec] = x = self._get_latest_version_of_package(package_spec)
             return x
 
-    def _resolvepkg(self, pkgspec):
-        if not os.path.isabs(str(pkgspec)):
-            return pkgspec
-        p = py.path.local(pkgspec)
+    def _get_latest_version_of_package(self, package_spec):
+        if not os.path.isabs(str(package_spec)):
+            return package_spec
+        p = py.path.local(package_spec)
         if p.check():
             return p
         if not p.dirpath().check(dir=1):
@@ -748,32 +748,33 @@ class Session:
         self.report.info("determining {}".format(p))
         candidates = p.dirpath().listdir(p.basename)
         if len(candidates) == 0:
-            raise tox.exception.MissingDependency(pkgspec)
+            raise tox.exception.MissingDependency(package_spec)
         if len(candidates) > 1:
-            items = []
-            for x in candidates:
-                ver = getversion(x.basename)
-                if ver is not None:
-                    items.append((ver, x))
+            version_package = []
+            for filename in candidates:
+                version = get_version_from_filename(filename.basename)
+                if version is not None:
+                    version_package.append((version, filename))
                 else:
-                    self.report.warning("could not determine version of: {}".format(str(x)))
-            items.sort()
-            if not items:
-                raise tox.exception.MissingDependency(pkgspec)
-            return items[-1][1]
+                    self.report.warning("could not determine version of: {}".format(str(filename)))
+            if not version_package:
+                raise tox.exception.MissingDependency(package_spec)
+            version_package.sort()
+            _, package_with_largest_version = version_package[-1]
+            return package_with_largest_version
         else:
             return candidates[0]
 
 
-_rex_getversion = re.compile(r"[\w_\-\+\.]+-(.*)\.(zip|tar\.gz)")
+_REGEX_FILE_NAME_WITH_VERSION = re.compile(r"[\w_\-\+\.]+-(.*)\.(zip|tar\.gz)")
 
 
-def getversion(basename):
-    m = _rex_getversion.match(basename)
+def get_version_from_filename(basename):
+    m = _REGEX_FILE_NAME_WITH_VERSION.match(basename)
     if m is None:
         return None
     version = m.group(1)
     try:
-        return NormalizedVersion(version)
-    except IrrationalVersionError:
+        return Version(version)
+    except InvalidVersion:
         return None
diff --git a/src/tox/venv.py b/src/tox/venv.py
index baccdee1..3e0c4027 100755
--- a/src/tox/venv.py
+++ b/src/tox/venv.py
@@ -4,6 +4,7 @@ import os
 import pipes
 import re
 import sys
+import warnings
 
 import py
 
@@ -184,21 +185,29 @@ class VirtualEnv(object):
         develop = self.envconfig.usedevelop
         alwayscopy = self.envconfig.alwayscopy
         deps = []
-        for dep in self._getresolvedeps():
+        for dep in self.get_resolved_dependencies():
             raw_dep = dep.name
             md5 = getdigest(raw_dep)
             deps.append((md5, raw_dep))
         return CreationConfig(md5, python, version, sitepackages, develop, deps, alwayscopy)
 
     def _getresolvedeps(self):
-        deps = []
-        for dep in self.envconfig.deps:
-            if dep.indexserver is None:
-                res = self.session._resolve_pkg(dep.name)
-                if res != dep.name:
-                    dep = dep.__class__(res)
-            deps.append(dep)
-        return deps
+        warnings.warn(
+            "that's a private function there, use get_resolved_dependencies,"
+            "this will be removed in 3.2",
+            category=DeprecationWarning,
+        )
+        return self.get_resolved_dependencies()
+
+    def get_resolved_dependencies(self):
+        dependencies = []
+        for dependency in self.envconfig.deps:
+            if dependency.indexserver is None:
+                package = self.session._resolve_package(package_spec=dependency.name)
+                if package != dependency.name:
+                    dependency = dependency.__class__(package)
+            dependencies.append(dependency)
+        return dependencies
 
     def getsupportedinterpreter(self):
         return self.envconfig.getsupportedinterpreter()
@@ -469,7 +478,7 @@ def tox_testenv_create(venv, action):
 
 @tox.hookimpl
 def tox_testenv_install_deps(venv, action):
-    deps = venv._getresolvedeps()
+    deps = venv.get_resolved_dependencies()
     if deps:
         depinfo = ", ".join(map(str, deps))
         action.setactivity("installdeps", depinfo)
diff --git a/tox.ini b/tox.ini
index 5ec62118..2c0f0ee7 100644
--- a/tox.ini
+++ b/tox.ini
@@ -132,4 +132,4 @@ include_trailing_comma = True
 force_grid_wrap = 0
 line_length = 99
 known_first_party = tox
-known_third_party = pkg_resources,pluggy,py,pytest,setuptools,six
+known_third_party = packaging,pkg_resources,pluggy,py,pytest,setuptools,six


Test patch:
diff --git a/tests/test_session.py b/tests/test_session.py
new file mode 100644
index 00000000..010ea141
--- /dev/null
+++ b/tests/test_session.py
@@ -0,0 +1,78 @@
+import re
+
+import pytest
+
+from tox.exception import MissingDependency, MissingDirectory
+
+
+def test__resolve_pkg_missing_directory(tmpdir, mocksession):
+    distshare = tmpdir.join("distshare")
+    spec = distshare.join("pkg123-*")
+    with pytest.raises(MissingDirectory):
+        mocksession._resolve_package(spec)
+
+
+def test__resolve_pkg_missing_directory_in_distshare(tmpdir, mocksession):
+    distshare = tmpdir.join("distshare")
+    spec = distshare.join("pkg123-*")
+    distshare.ensure(dir=1)
+    with pytest.raises(MissingDependency):
+        mocksession._resolve_package(spec)
+
+
+def test__resolve_pkg_multiple_valid_versions(tmpdir, mocksession):
+    distshare = tmpdir.join("distshare")
+    distshare.ensure("pkg123-1.3.5.zip")
+    p = distshare.ensure("pkg123-1.4.5.zip")
+    result = mocksession._resolve_package(distshare.join("pkg123-*"))
+    assert result == p
+    mocksession.report.expect("info", "determin*pkg123*")
+
+
+def test__resolve_pkg_with_invalid_version(tmpdir, mocksession):
+    distshare = tmpdir.join("distshare")
+
+    distshare.ensure("pkg123-1.something_bad.zip")
+    distshare.ensure("pkg123-1.3.5.zip")
+    p = distshare.ensure("pkg123-1.4.5.zip")
+
+    result = mocksession._resolve_package(distshare.join("pkg123-*"))
+    mocksession.report.expect("warning", "*1.something_bad*")
+    assert result == p
+
+
+def test__resolve_pkg_with_alpha_version(tmpdir, mocksession):
+    distshare = tmpdir.join("distshare")
+    distshare.ensure("pkg123-1.3.5.zip")
+    distshare.ensure("pkg123-1.4.5a1.tar.gz")
+    p = distshare.ensure("pkg123-1.4.5.zip")
+    result = mocksession._resolve_package(distshare.join("pkg123-*"))
+    assert result == p
+
+
+def test__resolve_pkg_doubledash(tmpdir, mocksession):
+    distshare = tmpdir.join("distshare")
+    p = distshare.ensure("pkg-mine-1.3.0.zip")
+    res = mocksession._resolve_package(distshare.join("pkg-mine*"))
+    assert res == p
+    distshare.ensure("pkg-mine-1.3.0a1.zip")
+    res = mocksession._resolve_package(distshare.join("pkg-mine*"))
+    assert res == p
+
+
+def test_minversion(cmd, initproj):
+    initproj(
+        "interp123-0.5",
+        filedefs={
+            "tests": {"test_hello.py": "def test_hello(): pass"},
+            "tox.ini": """
+            [tox]
+            minversion = 6.0
+        """,
+        },
+    )
+    result = cmd("-v")
+    assert re.match(
+        r"ERROR: MinVersionError: tox version is .*," r" required is at least 6.0", result.out
+    )
+    assert result.ret
diff --git a/tests/test_z_cmdline.py b/tests/test_z_cmdline.py
index 4cc58a2f..64ebb3d0 100644
--- a/tests/test_z_cmdline.py
+++ b/tests/test_z_cmdline.py
@@ -11,7 +11,6 @@ import pytest
 import tox
 from tox._pytestplugin import ReportExpectMock
 from tox.config import parseconfig
-from tox.exception import MissingDependency, MissingDirectory
 from tox.session import Session
 
 pytest_plugins = "pytester"
@@ -45,41 +44,6 @@ def test_report_protocol(newconfig):
     report.expect("logpopen")
 
 
-def test__resolve_pkg(tmpdir, mocksession):
-    distshare = tmpdir.join("distshare")
-    spec = distshare.join("pkg123-*")
-    with pytest.raises(MissingDirectory):
-        mocksession._resolve_pkg(spec)
-    distshare.ensure(dir=1)
-    with pytest.raises(MissingDependency):
-        mocksession._resolve_pkg(spec)
-    distshare.ensure("pkg123-1.3.5.zip")
-    p = distshare.ensure("pkg123-1.4.5.zip")
-    mocksession.report.clear()
-    result = mocksession._resolve_pkg(spec)
-    assert result == p
-    mocksession.report.expect("info", "determin*pkg123*")
-    distshare.ensure("pkg123-1.4.7dev.zip")
-    mocksession._clearmocks()
-    result = mocksession._resolve_pkg(spec)
-    mocksession.report.expect("warning", "*1.4.7*")
-    assert result == p
-    mocksession._clearmocks()
-    distshare.ensure("pkg123-1.4.5a1.tar.gz")
-    result = mocksession._resolve_pkg(spec)
-    assert result == p
-
-
-def test__resolve_pkg_doubledash(tmpdir, mocksession):
-    distshare = tmpdir.join("distshare")
-    p = distshare.ensure("pkg-mine-1.3.0.zip")
-    res = mocksession._resolve_pkg(distshare.join("pkg-mine*"))
-    assert res == p
-    distshare.ensure("pkg-mine-1.3.0a1.zip")
-    res = mocksession._resolve_pkg(distshare.join("pkg-mine*"))
-    assert res == p
-
-
 class TestSession:
     def test_make_sdist(self, initproj):
         initproj(
@@ -187,24 +151,6 @@ class TestSession:
             session.getvenv("qwe")
 
 
-def test_minversion(cmd, initproj):
-    initproj(
-        "interp123-0.5",
-        filedefs={
-            "tests": {"test_hello.py": "def test_hello(): pass"},
-            "tox.ini": """
-            [tox]
-            minversion = 6.0
-        """,
-        },
-    )
-    result = cmd("-v")
-    assert re.match(
-        r"ERROR: MinVersionError: tox version is .*," r" required is at least 6.0", result.out
-    )
-    assert result.ret
-
-
 def test_notoxini_help_still_works(initproj, cmd):
     initproj("example123-0.5", filedefs={"tests": {"test_hello.py": "def test_hello(): pass"}})
     result = cmd("-h")