Repository: tox-dev/tox

Issue:
tox4: platform support
👍🏻 

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/tox-dev/tox/pull/1888?src=pr&el=h1) Report
> Merging [#1888](https://codecov.io/gh/tox-dev/tox/pull/1888?src=pr&el=desc) (065aed7) into [rewrite](https://codecov.io/gh/tox-dev/tox/commit/e02bdbd71708321fe6a19308eadbceae1033767e?el=desc) (e02bdbd) will **increase** coverage by `0.15%`.
> The diff coverage is `100.00%`.

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

```diff
@@             Coverage Diff             @@
##           rewrite    #1888      +/-   ##
===========================================
+ Coverage    99.81%   99.97%   +0.15%     
===========================================
  Files          135      133       -2     
  Lines         7496     7362     -134     
  Branches       749      726      -23     
===========================================
- Hits          7482     7360     -122     
+ Misses           3        1       -2     
+ Partials        11        1      -10     
```

| Flag | Coverage Δ | |
|---|---|---|
| tests | `99.97% <100.00%> (+0.15%)` | :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/1888?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/tox/session/cmd/run/common.py](https://codecov.io/gh/tox-dev/tox/pull/1888/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/1888/diff?src=pr&el=tree#diff-c3JjL3RveC90b3hfZW52L2FwaS5weQ==) | `100.00% <100.00%> (ø)` | |
| [src/tox/tox\_env/python/virtual\_env/api.py](https://codecov.io/gh/tox-dev/tox/pull/1888/diff?src=pr&el=tree#diff-c3JjL3RveC90b3hfZW52L3B5dGhvbi92aXJ0dWFsX2Vudi9hcGkucHk=) | `100.00% <100.00%> (ø)` | |
| [.../tox\_env/python/virtual\_env/test\_virtualenv\_api.py](https://codecov.io/gh/tox-dev/tox/pull/1888/diff?src=pr&el=tree#diff-dGVzdHMvdG94X2Vudi9weXRob24vdmlydHVhbF9lbnYvdGVzdF92aXJ0dWFsZW52X2FwaS5weQ==) | `100.00% <100.00%> (ø)` | |
| [src/tox/pytest.py](https://codecov.io/gh/tox-dev/tox/pull/1888/diff?src=pr&el=tree#diff-c3JjL3RveC9weXRlc3QucHk=) | `100.00% <0.00%> (ø)` | |
| [src/tox/session/state.py](https://codecov.io/gh/tox-dev/tox/pull/1888/diff?src=pr&el=tree#diff-c3JjL3RveC9zZXNzaW9uL3N0YXRlLnB5) | `100.00% <0.00%> (ø)` | |
| [src/tox/execute/request.py](https://codecov.io/gh/tox-dev/tox/pull/1888/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/1888/diff?src=pr&el=tree#diff-c3JjL3RveC91dGlsL3BlcDUxNy9iYWNrZW5kLnB5) | `100.00% <0.00%> (ø)` | |
| [src/tox/execute/pep517\_backend.py](https://codecov.io/gh/tox-dev/tox/pull/1888/diff?src=pr&el=tree#diff-c3JjL3RveC9leGVjdXRlL3BlcDUxN19iYWNrZW5kLnB5) | `100.00% <0.00%> (ø)` | |
| ... and [10 more](https://codecov.io/gh/tox-dev/tox/pull/1888/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/tox-dev/tox/pull/1888?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/1888?src=pr&el=footer). Last update [e02bdbd...065aed7](https://codecov.io/gh/tox-dev/tox/pull/1888?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/docs/changelog/1845.feature.rst b/docs/changelog/1845.feature.rst
new file mode 100644
index 00000000..9587dcb0
--- /dev/null
+++ b/docs/changelog/1845.feature.rst
@@ -0,0 +1,1 @@
+Add support for the ``platform`` settings in tox test environments - by :user:`gaborbernat`.
diff --git a/src/tox/session/cmd/run/common.py b/src/tox/session/cmd/run/common.py
index 3a260084..2f5f96fc 100644
--- a/src/tox/session/cmd/run/common.py
+++ b/src/tox/session/cmd/run/common.py
@@ -112,16 +112,6 @@ def env_run_create_flags(parser: ArgumentParser) -> None:
     )
 
 
-def run_and_report(state: State, result: List[ToxEnvRunResult]) -> int:
-    # manifest the results
-    name_to_run = {r.name: r for r in result}
-    runs = [name_to_run[n] for n in list(state.env_list(everything=False))]
-    # write the journal
-    write_journal(getattr(state.options, "result_json", None), state.journal)
-    # report the outcome
-    return report(state.options.start, runs, state.options.is_colored)
-
-
 def report(start: float, runs: List[ToxEnvRunResult], is_colored: bool) -> int:
     def _print(color: int, message: str) -> None:
         print(f"{color if is_colored else ''}{message}{Fore.RESET if is_colored else ''}")
@@ -153,15 +143,17 @@ def execute(state: State, max_workers: Optional[int], spinner: bool, live: bool)
     interrupt, done = Event(), Event()
     results: List[ToxEnvRunResult] = []
     future_to_env: Dict["Future[ToxEnvRunResult]", ToxEnv] = {}
-    for env in list(state.env_list()):  # ensure envs can be constructed
-        state.tox_env(env)
+    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)
     previous, has_previous = None, False
     try:
         try:
             thread = Thread(
                 target=_queue_and_wait,
                 name="tox-interrupt",
-                args=(state, results, future_to_env, interrupt, done, max_workers, spinner, live),
+                args=(state, to_run_list, results, future_to_env, interrupt, done, max_workers, spinner, live),
             )
             thread.start()
             thread.join()
@@ -174,7 +166,14 @@ def execute(state: State, max_workers: Optional[int], spinner: bool, live: bool)
                     tox_env.interrupt()
             done.wait()
     finally:
-        exit_code = run_and_report(state, results)
+        ordered_results: List[ToxEnvRunResult] = []
+        name_to_run = {r.name: r for r in results}
+        for env in to_run_list:
+            ordered_results.append(name_to_run[env])
+        # write the journal
+        write_journal(getattr(state.options, "result_json", None), state.journal)
+        # report the outcome
+        exit_code = report(state.options.start, ordered_results, state.options.is_colored)
         if has_previous:
             signal(SIGINT, previous)
         if "_TOX_SHOW_THREAD" in os.environ:  # pragma: no cover
@@ -207,6 +206,7 @@ class ToxSpinner(Spinner):
 
 def _queue_and_wait(
     state: State,
+    to_run_list: List[str],
     results: List[ToxEnvRunResult],
     future_to_env: Dict["Future[ToxEnvRunResult]", ToxEnv],
     interrupt: Event,
@@ -217,7 +217,6 @@ def _queue_and_wait(
 ) -> None:
     try:
         options = state.options
-        to_run_list = list(state.env_list())
         with ToxSpinner(has_spinner, state, len(to_run_list)) as spinner:
             max_workers = len(to_run_list) if max_workers is None else max_workers
             completed: Set[str] = set()
diff --git a/src/tox/tox_env/api.py b/src/tox/tox_env/api.py
index 5b9391c7..8af7b79f 100644
--- a/src/tox/tox_env/api.py
+++ b/src/tox/tox_env/api.py
@@ -92,6 +92,12 @@ class ToxEnv(ABC):
             desc="a folder that is always reset at the start of the run",
         )
         self.conf.default_set_env_loader = self.default_set_env
+        self.conf.add_config(
+            keys=["platform"],
+            of_type=str,
+            default="",
+            desc="run on platforms that match this regular expression (empty means any platform)",
+        )
 
         def pass_env_post_process(values: List[str]) -> List[str]:
             values.extend(self.default_pass_env())
@@ -337,5 +343,18 @@ class ToxEnv(ABC):
     def teardown(self) -> None:
         """Any cleanup operation on environment done"""
 
+    @property
+    @abstractmethod
+    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/virtual_env/api.py b/src/tox/tox_env/python/virtual_env/api.py
index e1a75057..ba6fba99 100644
--- a/src/tox/tox_env/python/virtual_env/api.py
+++ b/src/tox/tox_env/python/virtual_env/api.py
@@ -1,6 +1,7 @@
 """
 Declare the abstract base class for tox environments that handle the Python language via the virtualenv project.
 """
+import sys
 from abc import ABC
 from pathlib import Path
 from typing import Dict, List, Optional, Sequence, cast
@@ -232,3 +233,7 @@ class VirtualEnv(Python, ABC):
         )
         result.assert_success()
         return result.out.splitlines()
+
+    @property
+    def runs_on_platform(self) -> str:
+        return sys.platform


Test patch:
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 1fe07ebc..ae1c906e 100644
--- a/tests/tox_env/python/virtual_env/test_virtualenv_api.py
+++ b/tests/tox_env/python/virtual_env/test_virtualenv_api.py
@@ -1,4 +1,5 @@
 import os
+import sys
 from typing import Tuple
 
 import pytest
@@ -155,3 +156,22 @@ def test_list_dependencies_command(tox_project: ToxProjectCreator, monkeypatch:
     result.assert_success()
     request: ExecuteRequest = execute_calls.call_args[0][3]
     assert request.cmd == ["python", "-m", "pip", "freeze"]
+
+
+@pytest.mark.parametrize("keep", [True, False])
+def test_platform_filter(
+    tox_project: ToxProjectCreator, virtualenv_opt: VirtualEnvOptions, monkeypatch: MonkeyPatch, 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