Repository: tox-dev/tox

Issue:
tox4: Not reporting skipped testenvs
So I trial-ran `tox==4.0.0a6` on the [latest master branch of aiosmtpd](https://github.com/aio-libs/aiosmtpd).

With tox3, I got the following summary conclusion:

```
  qa: commands succeeded
SKIPPED:  static: platform mismatch ('win32' does not match '^(?!win32)(?!cygwin)')
  docs: commands succeeded
```

On tox4, the "static" testenv is just missing:

```
  qa: OK (45.77=setup[2.86]+cmd[0.16,3.84,37.09,1.81] seconds)
  docs: OK (9.81=setup[2.89]+cmd[0.17,3.73,1.25,1.77] seconds)
```

Scrolling up in Terminal, tox4 never mentioned anything about the missing "static" testenv:

```
qa: OK ✔ in 45.77 seconds
docs: install_package> python -I -m pip install --no-deps --force-reinstall --no-build-isolation -e C:\Repos\aiosmtpd
docs: commands[0]> python housekeep.py prep
```

It would be nice if this tox3 behavior (i.e., reporting on skipped testenvs, and why) is kept in tox4.

Discussion:
jugmac00: on my local machine I get a traceback from the coverage env oO

```
cov: commands[0]> coverage combine
cov: commands[1]> coverage report -m
Name    Stmts   Miss Branch BrPart  Cover   Missing
---------------------------------------------------
---------------------------------------------------
TOTAL    7797      0   1542      0   100%

178 files skipped due to complete coverage.
cov: commands[2]> coverage xml -o /home/jugmac00/Projects/tox/.tox/4/coverage.xml
cov: commands[3]> coverage html -d /home/jugmac00/Projects/tox/.tox/4/htmlcov
cov: commands[4]> diff-cover --compare-branch upstream/rewrite /home/jugmac00/Projects/tox/.tox/4/coverage.xml
Traceback (most recent call last):
  File "/home/jugmac00/Projects/tox/.tox/4/cov/bin/diff-cover", line 8, in <module>
    sys.exit(main())
  File "/home/jugmac00/Projects/tox/.tox/4/cov/lib/python3.8/site-packages/diff_cover/diff_cover_tool.py", line 230, in main
    percent_covered = generate_coverage_report(
  File "/home/jugmac00/Projects/tox/.tox/4/cov/lib/python3.8/site-packages/diff_cover/diff_cover_tool.py", line 212, in generate_coverage_report
    reporter.generate_report(output_file)
  File "/home/jugmac00/Projects/tox/.tox/4/cov/lib/python3.8/site-packages/diff_cover/report_generator.py", line 251, in generate_report
    report = template.render(self._context())
  File "/home/jugmac00/Projects/tox/.tox/4/cov/lib/python3.8/site-packages/diff_cover/report_generator.py", line 300, in _context
    context = super().report_dict()
  File "/home/jugmac00/Projects/tox/.tox/4/cov/lib/python3.8/site-packages/diff_cover/report_generator.py", line 183, in report_dict
    src_stats = {src: self._src_path_stats(src) for src in self.src_paths()}
  File "/home/jugmac00/Projects/tox/.tox/4/cov/lib/python3.8/site-packages/diff_cover/report_generator.py", line 85, in src_paths
    for src, summary in self._diff_violations().items()
  File "/home/jugmac00/Projects/tox/.tox/4/cov/lib/python3.8/site-packages/diff_cover/report_generator.py", line 178, in _diff_violations
    for src_path in self._diff.src_paths_changed()
  File "/home/jugmac00/Projects/tox/.tox/4/cov/lib/python3.8/site-packages/diff_cover/diff_reporter.py", line 151, in src_paths_changed
    diff_dict = self._git_diff()
  File "/home/jugmac00/Projects/tox/.tox/4/cov/lib/python3.8/site-packages/diff_cover/diff_reporter.py", line 200, in _git_diff
    for diff_str in self._get_included_diff_results():
  File "/home/jugmac00/Projects/tox/.tox/4/cov/lib/python3.8/site-packages/diff_cover/diff_reporter.py", line 173, in _get_included_diff_results
    included = [self._git_diff_tool.diff_committed(self._compare_branch)]
  File "/home/jugmac00/Projects/tox/.tox/4/cov/lib/python3.8/site-packages/diff_cover/git_diff.py", line 66, in diff_committed
    return execute(self._default_git_args + self._default_diff_args + [diff_range])[
  File "/home/jugmac00/Projects/tox/.tox/4/cov/lib/python3.8/site-packages/diff_cover/command_runner.py", line 45, in execute
    raise CommandError(stderr)
diff_cover.command_runner.CommandError: fatal: ambiguous argument 'upstream/rewrite...HEAD': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'

cov: exit 1 (0.29 seconds) /home/jugmac00/Projects/tox> diff-cover --compare-branch upstream/rewrite /home/jugmac00/Projects/tox/.tox/4/coverage.xml pid=8407
```

It's late, so I had no chance to dig deeper why coverage is failing...
codecov[bot]: # [Codecov](https://codecov.io/gh/tox-dev/tox/pull/1970?src=pr&el=h1) Report
> Merging [#1970](https://codecov.io/gh/tox-dev/tox/pull/1970?src=pr&el=desc) (b0433c8) into [rewrite](https://codecov.io/gh/tox-dev/tox/commit/d0c428ed89457cb88d4f273ea8e1081bcc1f66bc?el=desc) (d0c428e) will **increase** coverage by `0.17%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/tox-dev/tox/pull/1970/graphs/tree.svg?width=650&height=150&src=pr&token=DYodAwDCZ5)](https://codecov.io/gh/tox-dev/tox/pull/1970?src=pr&el=tree)

```diff
@@             Coverage Diff              @@
##           rewrite     #1970      +/-   ##
============================================
+ Coverage    99.82%   100.00%   +0.17%     
============================================
  Files          146       146              
  Lines         7908      7806     -102     
  Branches       794       767      -27     
============================================
- Hits          7894      7806      -88     
+ Misses           3         0       -3     
+ Partials        11         0      -11     
```

| Flag | Coverage Δ | |
|---|---|---|
| tests | `100.00% <100.00%> (+0.17%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/tox-dev/tox/pull/1970?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [.../tox\_env/python/virtual\_env/test\_virtualenv\_api.py](https://codecov.io/gh/tox-dev/tox/pull/1970/diff?src=pr&el=tree#diff-dGVzdHMvdG94X2Vudi9weXRob24vdmlydHVhbF9lbnYvdGVzdF92aXJ0dWFsZW52X2FwaS5weQ==) | `100.00% <ø> (ø)` | |
| [src/tox/session/cmd/run/common.py](https://codecov.io/gh/tox-dev/tox/pull/1970/diff?src=pr&el=tree#diff-c3JjL3RveC9zZXNzaW9uL2NtZC9ydW4vY29tbW9uLnB5) | `100.00% <100.00%> (ø)` | |
| [src/tox/tox\_env/api.py](https://codecov.io/gh/tox-dev/tox/pull/1970/diff?src=pr&el=tree#diff-c3JjL3RveC90b3hfZW52L2FwaS5weQ==) | `100.00% <100.00%> (ø)` | |
| [tests/session/cmd/test\_single.py](https://codecov.io/gh/tox-dev/tox/pull/1970/diff?src=pr&el=tree#diff-dGVzdHMvc2Vzc2lvbi9jbWQvdGVzdF9zaW5nbGUucHk=) | `100.00% <100.00%> (ø)` | |
| [src/tox/execute/request.py](https://codecov.io/gh/tox-dev/tox/pull/1970/diff?src=pr&el=tree#diff-c3JjL3RveC9leGVjdXRlL3JlcXVlc3QucHk=) | `100.00% <0.00%> (ø)` | |
| [src/tox/util/pep517/backend.py](https://codecov.io/gh/tox-dev/tox/pull/1970/diff?src=pr&el=tree#diff-c3JjL3RveC91dGlsL3BlcDUxNy9iYWNrZW5kLnB5) | `100.00% <0.00%> (ø)` | |
| [...ecute/local\_sub\_process/read\_via\_thread\_windows.py](https://codecov.io/gh/tox-dev/tox/pull/1970/diff?src=pr&el=tree#diff-c3JjL3RveC9leGVjdXRlL2xvY2FsX3N1Yl9wcm9jZXNzL3JlYWRfdmlhX3RocmVhZF93aW5kb3dzLnB5) | | |
| [src/tox/tox\_env/python/api.py](https://codecov.io/gh/tox-dev/tox/pull/1970/diff?src=pr&el=tree#diff-c3JjL3RveC90b3hfZW52L3B5dGhvbi9hcGkucHk=) | `100.00% <0.00%> (+0.68%)` | :arrow_up: |
| ... and [3 more](https://codecov.io/gh/tox-dev/tox/pull/1970/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/tox-dev/tox/pull/1970?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/tox-dev/tox/pull/1970?src=pr&el=footer). Last update [d0c428e...b0433c8](https://codecov.io/gh/tox-dev/tox/pull/1970?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

gaborbernat: > diff_cover.command_runner.CommandError: fatal: ambiguous argument 'upstream/rewrite...HEAD': unknown revision or path not in the working tree.

https://github.com/tox-dev/tox/blob/rewrite/tox.ini#L65 If you don't have an upstream remote (and work of origin), you should change it ``origin/rewrite``. Essentially you want to point that env-var to the base of the merge to get locally a coverage diff.

jugmac00: > > diff_cover.command_runner.CommandError: fatal: ambiguous argument 'upstream/rewrite...HEAD': unknown revision or path not in the working tree.
> 
> https://github.com/tox-dev/tox/blob/rewrite/tox.ini#L65 If you don't have an upstream remote (and work of origin), you should change it `origin/rewrite`. Essentially you want to point that env-var to the base of the merge to get locally a coverage diff.

Thanks for the clarification!

Indeed, I always keep `origin` for `upstream` and use `fork` for ... my fork :-) Made the most sense to me, but I guess most go with `upstream`.

As we are at 100% coverage, does this make sense any more? Or would it be sufficient to run `coverage` with `--fail-under 100`?
gaborbernat: > As we are at 100% coverage, does this make sense anymore? Or would it be sufficient to run `coverage` with `--fail-under 100`?

Not yet on all platforms/python versions. Once we get there we can drop it 👍🏻 
jugmac00: The coverage fail is probably because I have not yet rebased. My local cov is 100%
jugmac00: Oh my... After "Ubuntu / Python 3.8 and Windows / Python 3.7" failed in the last run, now Mac 3.7 is failing... 

I am running out of ideas. :white_flag: 


gaborbernat: > Oh my... After "Ubuntu / Python 3.8 and Windows / Python 3.7" failed in the last run, now Mac 3.7 is failing...

I Will take a look at this weekend but I've been overrun with work stuff this week, so had not availability on looking at this.
gaborbernat: So the issue here seems there are two things building the host wheel at the same time and they are stepping on each others toe, one is the `tox_wheel`, not sure yet about the other...
gaborbernat: Ah wait I think I know, tox_wheel is conflicting with itself due to having to two tests using it, https://github.com/pytest-dev/pytest-xdist#making-session-scoped-fixtures-execute-only-once

Patch:
diff --git a/docs/changelog/1926.bugfix.rst b/docs/changelog/1926.bugfix.rst
new file mode 100644
index 00000000..513595ce
--- /dev/null
+++ b/docs/changelog/1926.bugfix.rst
@@ -0,0 +1,2 @@
+ Environments with a platform mismatch are no longer silently skipped,
+ but properly reported - by :user:`jugmac00`.
diff --git a/setup.cfg b/setup.cfg
index fad34bd9..8eaeea2b 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -62,6 +62,7 @@ testing =
     covdefaults>=1.2
     devpi-client>=5
     devpi-server>=5
+    filelock>=3
     flaky>=3.7
     freezegun>=1
     psutil>=5.7
diff --git a/src/tox/provision.py b/src/tox/provision.py
index 9710a70e..ef06a813 100644
--- a/src/tox/provision.py
+++ b/src/tox/provision.py
@@ -14,7 +14,9 @@ from tox.config.loader.memory import MemoryLoader
 from tox.config.sets import CoreConfigSet
 from tox.execute.api import StdinSource
 from tox.plugin.impl import impl
+from tox.report import HandledError
 from tox.session.state import State
+from tox.tox_env.errors import Skip
 from tox.tox_env.python.req_file import RequirementsFile
 from tox.tox_env.python.runner import PythonRun
 from tox.tox_env.python.virtual_env.package.api import PackageType
@@ -108,7 +110,10 @@ def run_provision(deps: List[Requirement], state: State) -> int:  # noqa
     env_python = tox_env.env_python()
     logging.info("will run in a automatically provisioned python environment under %s", env_python)
     recreate = state.options.no_recreate_provision is False if state.options.recreate else False
-    tox_env.ensure_setup(recreate=recreate)
+    try:
+        tox_env.ensure_setup(recreate=recreate)
+    except Skip as exception:
+        raise HandledError(f"cannot provision tox environment {tox_env.conf['env_name']} because {exception}")
     args: List[str] = [str(env_python), "-m", "tox"]
     args.extend(state.args)
     outcome = tox_env.execute(cmd=args, stdin=StdinSource.user_only(), show=True, run_id="provision")
diff --git a/src/tox/session/cmd/run/common.py b/src/tox/session/cmd/run/common.py
index cf5b753d..4e28e848 100644
--- a/src/tox/session/cmd/run/common.py
+++ b/src/tox/session/cmd/run/common.py
@@ -179,9 +179,9 @@ def execute(state: State, max_workers: Optional[int], has_spinner: bool, live: b
     results: List[ToxEnvRunResult] = []
     future_to_env: Dict["Future[ToxEnvRunResult]", ToxEnv] = {}
     to_run_list: List[str] = []
-    for env in state.env_list():  # ensure envs can be constructed and are active
-        if state.tox_env(env).active:
-            to_run_list.append(env)
+    for env in state.env_list():  # ensure envs can be constructed
+        state.tox_env(env)
+        to_run_list.append(env)
     previous, has_previous = None, False
     try:
         spinner = ToxSpinner(has_spinner, state, len(to_run_list))
diff --git a/src/tox/session/cmd/run/single.py b/src/tox/session/cmd/run/single.py
index 21a347e8..ba65d702 100644
--- a/src/tox/session/cmd/run/single.py
+++ b/src/tox/session/cmd/run/single.py
@@ -42,7 +42,7 @@ def _evaluate(tox_env: RunToxEnv, recreate: bool, no_test: bool) -> Tuple[bool,
             tox_env.ensure_setup(recreate=recreate)
             code, outcomes = run_commands(tox_env, no_test)
         except Skip as exception:
-            LOGGER.warning("skipped environment because %s", exception)
+            LOGGER.warning("skipped because %s", exception)
             skipped = True
         except ToxBackendFailed as exception:
             LOGGER.error("%s", exception)
diff --git a/src/tox/tox_env/api.py b/src/tox/tox_env/api.py
index 5e6a999a..51e81d01 100644
--- a/src/tox/tox_env/api.py
+++ b/src/tox/tox_env/api.py
@@ -18,7 +18,7 @@ from tox.execute.api import Execute, ExecuteStatus, Outcome, StdinSource
 from tox.execute.request import ExecuteRequest
 from tox.journal import EnvJournal
 from tox.report import OutErr, ToxHandler
-from tox.tox_env.errors import Recreate
+from tox.tox_env.errors import Recreate, Skip
 
 from .info import Info
 
@@ -169,6 +169,7 @@ class ToxEnv(ABC):
             self._handle_env_tmp_dir()
 
     def ensure_setup(self, recreate: bool = False) -> None:
+        self.check_platform()
         if self.setup_done is True:
             return
         if self.conf["recreate"]:
@@ -183,6 +184,14 @@ class ToxEnv(ABC):
                 self.setup()
         self.setup_has_been_done()
 
+    def check_platform(self) -> None:
+        """skip env when platform does not match"""
+        platform_str: str = self.conf["platform"]
+        if platform_str:
+            match = re.fullmatch(platform_str, self.runs_on_platform)
+            if match is None:
+                raise Skip(f"platform {self.runs_on_platform} does not match {platform_str}")
+
     def setup_has_been_done(self) -> None:
         """called when setup is done"""
 
@@ -348,13 +357,5 @@ class ToxEnv(ABC):
     def runs_on_platform(self) -> str:
         raise NotImplementedError
 
-    @property
-    def active(self) -> bool:
-        platform_str: str = self.conf["platform"]
-        if not platform_str:
-            return True
-        match = re.fullmatch(platform_str, self.runs_on_platform)
-        return match is not None
-
 
 _CWD = Path.cwd()
diff --git a/src/tox/tox_env/python/runner.py b/src/tox/tox_env/python/runner.py
index 2b2f3a7a..257e4761 100644
--- a/src/tox/tox_env/python/runner.py
+++ b/src/tox/tox_env/python/runner.py
@@ -10,7 +10,7 @@ from tox.config.cli.parser import Parsed
 from tox.config.sets import CoreConfigSet, EnvConfigSet
 from tox.journal import EnvJournal
 from tox.report import ToxHandler
-from tox.tox_env.errors import Recreate
+from tox.tox_env.errors import Recreate, Skip
 from tox.tox_env.package import PackageToxEnv
 
 from ..runner import RunToxEnv
@@ -65,7 +65,10 @@ class PythonRun(Python, RunToxEnv, ABC):
         if explicit_install_package is None:
             # 1. install package dependencies
             with package_env.display_context(suspend=self.has_display_suspended):
-                package_deps = package_env.get_package_dependencies(self.conf)
+                try:
+                    package_deps = package_env.get_package_dependencies(self.conf)
+                except Skip as exception:
+                    raise Skip(f"{exception.args[0]} for package environment {package_env.conf['env_name']}")
             self.cached_install([PythonDep(p) for p in package_deps], PythonRun.__name__, "package_deps")
 
             # 2. install the package
diff --git a/tox.ini b/tox.ini
index 74d0ff54..b8a77ec7 100644
--- a/tox.ini
+++ b/tox.ini
@@ -39,11 +39,11 @@ wheel_build_env = .pkg
 description = run style checker on the code
 skip_install = true
 deps =
-    flake8==3.8.4
-    flake8-bugbear==20.11.1
-    flake8-comprehensions==3.3.1
-    flake8-pytest-style==1.3
-    flake8-spellcheck==0.23
+    flake8==3.9
+    flake8-bugbear==21.3.2
+    flake8-comprehensions==3.4
+    flake8-pytest-style==1.4
+    flake8-spellcheck==0.24
     flake8-unused-arguments==0.0.6
 commands =
     flake8 src tests docs
diff --git a/whitelist.txt b/whitelist.txt
index 315a808f..c38e58c6 100644
--- a/whitelist.txt
+++ b/whitelist.txt
@@ -23,8 +23,8 @@ chdir
 cmd
 Cmd
 colorama
-Conf
 conf
+Conf
 configs
 conftest
 const
@@ -64,8 +64,8 @@ envs
 epilog
 eq
 esscaped
-Eval
 eval
+Eval
 exc
 exe
 executables
@@ -73,6 +73,7 @@ expr
 extlinks
 extractall
 favicon
+filelock
 fmt
 formatter
 Formatter
@@ -81,6 +82,7 @@ fromlist
 fs
 fullmatch
 func
+getbasetemp
 getfqdn
 getitem
 getoption
@@ -134,8 +136,8 @@ parsers
 pathlib
 pathname
 pathsep
-Pep517
 pep517
+Pep517
 pluggy
 Popen
 pos


Test patch:
diff --git a/src/tox/pytest.py b/src/tox/pytest.py
index 03c73750..29c98107 100644
--- a/src/tox/pytest.py
+++ b/src/tox/pytest.py
@@ -401,7 +401,7 @@ def pytest_configure(config: PyTestConfig) -> None:
     config.addinivalue_line("markers", "plugin_test")
 
 
-@pytest.mark.trylast
+@pytest.mark.trylast()
 def pytest_collection_modifyitems(config: PyTestConfig, items: List[Function]) -> None:
     # do not require flags if called directly
     if len(items) == 1:  # pragma: no cover # hard to test
diff --git a/tests/plugin/test_inline.py b/tests/plugin/test_inline.py
index b52f3072..24fe6f7e 100644
--- a/tests/plugin/test_inline.py
+++ b/tests/plugin/test_inline.py
@@ -3,7 +3,7 @@ import pytest
 from tox.pytest import ToxProjectCreator
 
 
-@pytest.mark.plugin_test
+@pytest.mark.plugin_test()
 def test_inline_tox_py(tox_project: ToxProjectCreator) -> None:
     ini = """
     from tox.plugin.impl import impl
diff --git a/tests/session/cmd/test_devenv.py b/tests/session/cmd/test_devenv.py
index b9c32ed2..be9d00ac 100644
--- a/tests/session/cmd/test_devenv.py
+++ b/tests/session/cmd/test_devenv.py
@@ -12,7 +12,7 @@ def test_devenv_fail_multiple_target(tox_project: ToxProjectCreator) -> None:
     outcome.assert_out_err(msg, "")
 
 
-@pytest.mark.integration
+@pytest.mark.integration()
 def test_devenv_ok(tox_project: ToxProjectCreator, enable_pip_pypi_access: Optional[str]) -> None:  # noqa: U100
     content = {"setup.py": "from setuptools import setup\nsetup(name='demo', version='1.0')"}
     outcome = tox_project(content).run("d", "-e", "py")
diff --git a/tests/session/cmd/test_sequential.py b/tests/session/cmd/test_sequential.py
index 333e5f39..e92b484d 100644
--- a/tests/session/cmd/test_sequential.py
+++ b/tests/session/cmd/test_sequential.py
@@ -43,7 +43,7 @@ def test_run_sequential_fail(tox_project: ToxProjectCreator) -> None:
     assert Matches(r"  a: FAIL code 1 \(.*=setup\[.*\]\+cmd\[.*\] seconds\)") == reports[-3]
 
 
-@pytest.mark.integration
+@pytest.mark.integration()
 def test_result_json_sequential(
     tox_project: ToxProjectCreator, enable_pip_pypi_access: Optional[str]  # noqa: U100
 ) -> None:
@@ -137,7 +137,7 @@ def test_rerun_sequential_wheel(tox_project: ToxProjectCreator, demo_pkg_inline:
     result_rerun.assert_success()
 
 
-@pytest.mark.integration
+@pytest.mark.integration()
 def test_rerun_sequential_sdist(tox_project: ToxProjectCreator, demo_pkg_inline: Path) -> None:
     proj = tox_project(
         {"tox.ini": "[testenv]\npackage=sdist\ncommands=python -c 'from demo_pkg_inline import do; do()'"}
@@ -431,7 +431,7 @@ def test_commands_post_fails_exit_code(tox_project: ToxProjectCreator) -> None:
 )
 def test_commands_ignore_errors(tox_project: ToxProjectCreator, pre: int, main: int, post: int, outcome: int) -> None:
     def _s(key: str, code: int) -> str:
-        return f"\ncommands{key}=\n {_c(code)}\n {'' if code == 0 else _c(code+1)}"
+        return f"\ncommands{key}=\n {_c(code)}\n {'' if code == 0 else _c(code + 1)}"
 
     ini = f"[testenv]\npackage=skip\nignore_errors=True{_s('_pre', pre)}{_s('', main)}{_s('_post', post)}"
     proj = tox_project({"tox.ini": ini})
@@ -452,3 +452,34 @@ def test_ignore_outcome(tox_project: ToxProjectCreator) -> None:
 
     assert Matches(r"  py: IGNORED FAIL code 1 .*") == reports[-2]
     assert Matches(r"  congratulations :\) .*") == reports[-1]
+
+
+def test_platform_does_not_match_run_env(tox_project: ToxProjectCreator) -> None:
+    ini = "[testenv]\npackage=skip\nplatform=wrong_platform"
+    proj = tox_project({"tox.ini": ini})
+
+    result = proj.run("r")
+    result.assert_success()
+    exp = f"py: skipped because platform {sys.platform} does not match wrong_platform"
+    assert exp in result.out
+
+
+def test_platform_matches_run_env(tox_project: ToxProjectCreator) -> None:
+    ini = f"[testenv]\npackage=skip\nplatform={sys.platform}"
+    proj = tox_project({"tox.ini": ini})
+    result = proj.run("r")
+    result.assert_success()
+
+
+def test_platform_does_not_match_package_env(tox_project: ToxProjectCreator, demo_pkg_inline: Path) -> None:
+    toml = (demo_pkg_inline / "pyproject.toml").read_text()
+    build = (demo_pkg_inline / "build.py").read_text()
+    ini = "[testenv]\npackage=wheel\n[testenv:.pkg]\nplatform=wrong_platform"
+    proj = tox_project({"tox.ini": ini, "pyproject.toml": toml, "build.py": build})
+    result = proj.run("r", "-e", "a,b")
+    result.assert_success()
+    assert "a: SKIP" in result.out
+    assert "b: SKIP" in result.out
+    msg = f"skipped because platform {sys.platform} does not match wrong_platform for package environment .pkg"
+    assert f"a: {msg}" in result.out
+    assert f"b: {msg}" in result.out
diff --git a/tests/test_provision.py b/tests/test_provision.py
index c3fe5b37..b6235173 100644
--- a/tests/test_provision.py
+++ b/tests/test_provision.py
@@ -9,6 +9,7 @@ from typing import Iterator, List, Optional
 from zipfile import ZipFile
 
 import pytest
+from filelock import FileLock
 from packaging.requirements import Requirement
 
 from tox.pytest import Index, IndexServer, MonkeyPatch, TempPathFactory, ToxProjectCreator
@@ -31,7 +32,22 @@ def elapsed(msg: str) -> Iterator[None]:
 
 
 @pytest.fixture(scope="session")
-def tox_wheel(tmp_path_factory: TempPathFactory) -> Path:
+def tox_wheel(tmp_path_factory: TempPathFactory, worker_id: str) -> Path:
+    if worker_id == "master":  # if not running under xdist we can just return
+        return _make_tox_wheel(tmp_path_factory)  # pragma: no cover
+    # otherwise we need to ensure only one worker creates the wheel, and the rest reuses
+    root_tmp_dir = tmp_path_factory.getbasetemp().parent
+    cache_file = root_tmp_dir / "tox_wheel.json"
+    with FileLock(f"{cache_file}.lock"):
+        if cache_file.is_file():
+            data = Path(json.loads(cache_file.read_text()))
+        else:
+            data = _make_tox_wheel(tmp_path_factory)
+            cache_file.write_text(json.dumps(str(data)))
+    return data
+
+
+def _make_tox_wheel(tmp_path_factory: TempPathFactory) -> Path:
     with elapsed("acquire current tox wheel"):  # takes around 3.2s on build
         package: Optional[Path] = None
         if "TOX_PACKAGE" in os.environ:
@@ -101,7 +117,7 @@ def test_provision_requires_nok(tox_project: ToxProjectCreator) -> None:
     )
 
 
-@pytest.mark.integration
+@pytest.mark.integration()
 def test_provision_requires_ok(
     tox_project: ToxProjectCreator, pypi_index_self: Index, monkeypatch: MonkeyPatch, tmp_path: Path
 ) -> None:
@@ -134,3 +150,17 @@ def test_provision_requires_ok(
     result_recreate.assert_success()
     assert prov_msg in result_recreate.out
     assert f"ROOT: remove tox env folder {provision_env}" in result_recreate.out, result_recreate.out
+
+
+@pytest.mark.integration()
+def test_provision_platform_check(
+    tox_project: ToxProjectCreator, pypi_index_self: Index, monkeypatch: MonkeyPatch
+) -> None:
+    pypi_index_self.use(monkeypatch)
+    ini = "[tox]\nrequires=demo-pkg-inline\n[testenv]\npackage=skip\n[testenv:.tox]\nplatform=wrong_platform"
+    proj = tox_project({"tox.ini": ini})
+
+    result = proj.run("r")
+    result.assert_failed(-2)
+    msg = f"cannot provision tox environment .tox because platform {sys.platform} does not match wrong_platform"
+    assert msg in result.out
diff --git a/tests/tox_env/python/virtual_env/test_setuptools.py b/tests/tox_env/python/virtual_env/test_setuptools.py
index dee00de0..3b8942e4 100644
--- a/tests/tox_env/python/virtual_env/test_setuptools.py
+++ b/tests/tox_env/python/virtual_env/test_setuptools.py
@@ -9,7 +9,7 @@ from tox.pytest import ToxProjectCreator
 from tox.tox_env.python.virtual_env.package.api import Pep517VirtualEnvPackage
 
 
-@pytest.mark.integration
+@pytest.mark.integration()
 def test_setuptools_package(
     tox_project: ToxProjectCreator,
     demo_pkg_setuptools: Path,
diff --git a/tests/tox_env/python/virtual_env/test_virtualenv_api.py b/tests/tox_env/python/virtual_env/test_virtualenv_api.py
index 3793edbd..e9ce7ac6 100644
--- a/tests/tox_env/python/virtual_env/test_virtualenv_api.py
+++ b/tests/tox_env/python/virtual_env/test_virtualenv_api.py
@@ -1,5 +1,4 @@
 import os
-import sys
 from pathlib import Path
 from typing import Tuple
 
@@ -155,23 +154,6 @@ def test_list_dependencies_command(tox_project: ToxProjectCreator) -> None:
     assert request.cmd == ["python", "-m", "pip", "freeze"]
 
 
-@pytest.mark.parametrize("keep", [True, False])
-def test_platform_filter(tox_project: ToxProjectCreator, keep: bool) -> None:
-    platform = sys.platform
-    if not keep:
-        platform += "1"
-    ini = f"[testenv]\npackage=skip\nplatform={platform}"
-    proj = tox_project({"tox.ini": ini})
-    result = proj.run("r", "-e", "py")
-    result.assert_success()
-
-    assert result.state.tox_env("py").active is keep
-    if keep:
-        assert "py" in result.out
-    else:
-        assert "py" not in result.out
-
-
 @pytest.mark.parametrize("mode", ["r", "p", "le"])
 def test_install_pkg(tox_project: ToxProjectCreator, mode: str) -> None:
     proj = tox_project({"tox.ini": "[testenv]\npackage=wheel"})