Repository: ansible/molecule

Issue:
Support --platform parameter
Support to test only one platform image
```
export MOLECULE_PLATFORM_NAME=ubuntu-18.04
molecule test
```


### Discussed in https://github.com/ansible-community/molecule/discussions/3410

<div type='discussions-op-text'>

<sup>Originally posted by **zhan9san** January 24, 2022</sup>
Support `--platform` for `molecule test` command.

```
--platform TEXT        Name of the platform to test
```

e.g.

Only test role on `centos-7` platform in `default` scenario, if no platform found, raise error.

```
molecule test --platform centos-7
```

Q: Why is this required?
A: Assume there are 10 platform docker images defined in `molecule.yml` and only one image(centos-7) are changed.

- With `--platform`, only one changed docker image would be tested via `molecule test --platform centos-7`
- Without `--platform`, all 10 platform docker image would be test while the other 9 are redundant.

More specifically, the `run: molecule test` can ben changed to `run: molecule test --platform ${{ matrix.package }}`.
https://github.com/zhan9san/systemd/blob/main/.github/workflows/ci.yml#L70</div>


Discussion:

Resolving patch:
diff --git a/src/molecule/config.py b/src/molecule/config.py
--- a/src/molecule/config.py
+++ b/src/molecule/config.py
@@ -133,6 +133,10 @@ def config_file(self):
     def is_parallel(self):
         return self.command_args.get("parallel", False)
 
+    @property
+    def platform_name(self):
+        return self.command_args.get("platform_name", None)
+
     @property
     def debug(self):
         return self.args.get("debug", MOLECULE_DEBUG)
@@ -206,7 +210,11 @@ def lint(self):
 
     @cached_property
     def platforms(self):
-        return platforms.Platforms(self, parallelize_platforms=self.is_parallel)
+        return platforms.Platforms(
+            self,
+            parallelize_platforms=self.is_parallel,
+            platform_name=self.platform_name,
+        )
 
     @cached_property
     def provisioner(self):
diff --git a/src/molecule/platforms.py b/src/molecule/platforms.py
--- a/src/molecule/platforms.py
+++ b/src/molecule/platforms.py
@@ -68,13 +68,19 @@ class Platforms(object):
               - child_group1
     """
 
-    def __init__(self, config, parallelize_platforms=False):
+    def __init__(self, config, parallelize_platforms=False, platform_name=None):
         """
         Initialize a new platform class and returns None.
 
         :param config: An instance of a Molecule config.
+        :param parallelize_platforms: Parallel mode. Default is False.
+        :param platform_name: One platform to target only, defaults to None.
         :return: None
         """
+        if platform_name:
+            config.config["platforms"] = util._filter_platforms(
+                config.config, platform_name
+            )
         if parallelize_platforms:
             config.config["platforms"] = util._parallelize_platforms(
                 config.config, config._run_uuid
diff --git a/src/molecule/util.py b/src/molecule/util.py
--- a/src/molecule/util.py
+++ b/src/molecule/util.py
@@ -336,6 +336,14 @@ def parallelize(platform):
     return [parallelize(platform) for platform in config["platforms"]]
 
 
+def _filter_platforms(config, platform_name):
+    for platform in config["platforms"]:
+        if platform["name"] == platform_name:
+            return [platform]
+
+    return []
+
+
 @cache
 def find_vcs_root(location="", dirs=(".git", ".hg", ".svn"), default=None) -> str:
     """Return current repository root directory."""


Test patch:
diff --git a/src/molecule/command/test.py b/src/molecule/command/test.py
--- a/src/molecule/command/test.py
+++ b/src/molecule/command/test.py
@@ -31,6 +31,7 @@
 
 LOG = logging.getLogger(__name__)
 MOLECULE_PARALLEL = os.environ.get("MOLECULE_PARALLEL", False)
+MOLECULE_PLATFORM_NAME = os.environ.get("MOLECULE_PLATFORM_NAME", None)
 
 
 class Test(base.Base):
@@ -115,6 +116,12 @@ def execute(self, action_args=None):
     default=base.MOLECULE_DEFAULT_SCENARIO_NAME,
     help=f"Name of the scenario to target. ({base.MOLECULE_DEFAULT_SCENARIO_NAME})",
 )
+@click.option(
+    "--platform-name",
+    "-p",
+    default=MOLECULE_PLATFORM_NAME,
+    help="Name of the platform to target only. Default is None",
+)
 @click.option(
     "--driver-name",
     "-d",
@@ -140,7 +147,14 @@ def execute(self, action_args=None):
 )
 @click.argument("ansible_args", nargs=-1, type=click.UNPROCESSED)
 def test(
-    ctx, scenario_name, driver_name, __all, destroy, parallel, ansible_args
+    ctx,
+    scenario_name,
+    driver_name,
+    __all,
+    destroy,
+    parallel,
+    ansible_args,
+    platform_name,
 ):  # pragma: no cover
     """Test (dependency, lint, cleanup, destroy, syntax, create, prepare, converge, idempotence, side_effect, verify, cleanup, destroy)."""
     args = ctx.obj.get("args")
@@ -150,6 +164,7 @@ def test(
         "destroy": destroy,
         "subcommand": subcommand,
         "driver_name": driver_name,
+        "platform_name": platform_name,
     }
 
     if __all:
diff --git a/src/molecule/test/functional/test_command.py b/src/molecule/test/functional/test_command.py
--- a/src/molecule/test/functional/test_command.py
+++ b/src/molecule/test/functional/test_command.py
@@ -57,6 +57,14 @@ def driver_name(request: FixtureRequest) -> Optional[str]:
         return None
 
 
+@pytest.fixture
+def platform_name(request):
+    try:
+        return request.param
+    except AttributeError:
+        return None
+
+
 @pytest.mark.extensive
 @pytest.mark.parametrize(
     "scenario_to_test, driver_name, scenario_name",
@@ -293,6 +301,42 @@ def test_command_test(scenario_to_test, with_scenario, scenario_name, driver_nam
     run_test(driver_name, scenario_name)
 
 
+def run_test_with_platform_name(
+    driver_name, platform_name, scenario_name="default", parallel=False
+):
+    cmd = [
+        "molecule",
+        "-vvv",
+        "--debug",
+        "test",
+        "--scenario-name",
+        scenario_name,
+        "--platform-name",
+        platform_name,
+    ]
+    if driver_name != "delegated":
+        if scenario_name is None:
+            cmd.append("--all")
+        if parallel:
+            cmd.append("--parallel")
+
+    assert run_command(cmd).returncode == 0
+
+
+@pytest.mark.serial
+@pytest.mark.parametrize(
+    ("scenario_to_test", "driver_name", "scenario_name", "platform_name"),
+    [
+        ("driver/delegated", "delegated", "default", "instance"),
+    ],
+    indirect=["scenario_to_test", "driver_name", "scenario_name", "platform_name"],
+)
+def test_command_test_with_platform_name(
+    scenario_to_test, with_scenario, scenario_name, driver_name, platform_name
+):
+    run_test_with_platform_name(driver_name, platform_name, scenario_name)
+
+
 @pytest.mark.serial
 @pytest.mark.extensive
 @pytest.mark.parametrize(
diff --git a/src/molecule/test/unit/test_platforms.py b/src/molecule/test/unit/test_platforms.py
--- a/src/molecule/test/unit/test_platforms.py
+++ b/src/molecule/test/unit/test_platforms.py
@@ -35,3 +35,26 @@ def test_instances_property(_instance):
     ]
 
     assert x == _instance.instances
+
+
+@pytest.fixture
+def platform_name(request, config_instance):
+    return platforms.Platforms(config_instance, platform_name=request.param)
+
+
+@pytest.mark.parametrize("platform_name", ["instance-1"], indirect=True)
+def test_instances_property_with_platform_name_instance_1(platform_name):
+    x = [
+        {"groups": ["foo", "bar"], "name": "instance-1", "children": ["child1"]},
+    ]
+
+    assert x == platform_name.instances
+
+
+@pytest.mark.parametrize("platform_name", ["instance-2"], indirect=True)
+def test_instances_property_with_platform_name_instance_2(platform_name):
+    x = [
+        {"groups": ["baz", "foo"], "name": "instance-2", "children": ["child2"]},
+    ]
+
+    assert x == platform_name.instances