Repository: ansible/ansible-navigator (language: python)

Issue:
playbook-artifact file name with OS timezone-based time
<!-- Issues are for **concrete, actionable bugs and feature requests** only - if you're just asking for debugging help or technical support, please use:

TODO: Add ansible-navigator specific channels

We have to limit this because of limited volunteer time to respond to issues! -->

##### ISSUE TYPE

- Feature Idea

##### SUMMARY

<!-- Briefly describe the problem or desired enhancement. -->

I also want the time based on the OS time zone as a key to be included in the file name of playbook-artifact. Not just ts_utc.








Discussion / hints:

Fix patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index dd88b0118..34d1c769f 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -185,6 +185,7 @@ repos:
           - jinja2
           - libtmux
           - pytest
+          - pytest-mock
           - sphinx
           - types-docutils
           - types-setuptools # Needed logging version
@@ -200,6 +201,7 @@ repos:
           - jinja2
           - libtmux
           - pytest
+          - pytest-mock
           - sphinx
           - types-backports # Needed for checking py3.6 with a later interpreter
           - types-dataclasses # Needed for checking py3.6 with a later interpreter
@@ -232,6 +234,7 @@ repos:
           - libtmux
           - onigurumacffi
           - pytest
+          - pytest-mock
           - setuptools-scm
           - sphinx
         pass_filenames: false
diff --git a/docs/changelog-fragments.d/1085.breaking.md b/docs/changelog-fragments.d/1085.breaking.md
new file mode 100644
index 000000000..d82adb4fe
--- /dev/null
+++ b/docs/changelog-fragments.d/1085.breaking.md
@@ -0,0 +1,4 @@
+Renamed the `ts_utc` variable in the playbook artifact file name
+to `time_stamp`.
+
+-- by {user}`cidrblock`
diff --git a/docs/changelog-fragments.d/1085.feature.md b/docs/changelog-fragments.d/1085.feature.md
new file mode 100644
index 000000000..111ef7c30
--- /dev/null
+++ b/docs/changelog-fragments.d/1085.feature.md
@@ -0,0 +1,11 @@
+Enabled the use of the `time_zone` settings entry for playbook
+artifact file name creation. The new default playbook artifact name is:
+
+```
+{playbook_dir}/{playbook_name}-artifact-{time_stamp}.json
+```
+
+The time stamp is an ISO8601 formatted timestamp based on the user
+specified time zone (IANA or 'local'), or UTC as the default.
+
+-- by {user}`cidrblock`
diff --git a/src/ansible_navigator/actions/run.py b/src/ansible_navigator/actions/run.py
index 5c15a7b55..27621e5ee 100644
--- a/src/ansible_navigator/actions/run.py
+++ b/src/ansible_navigator/actions/run.py
@@ -842,7 +842,7 @@ def write_artifact(self, filename: Optional[str] = None) -> None:
             filename = filename.format(
                 playbook_dir=os.path.dirname(self._args.playbook),
                 playbook_name=os.path.splitext(os.path.basename(self._args.playbook))[0],
-                ts_utc=now_iso(time_zone="UTC"),
+                time_stamp=now_iso(self._args.time_zone),
             )
             self._logger.debug("Formatted artifact file name set to %s", filename)
             filename = abs_user_path(filename)
diff --git a/src/ansible_navigator/configuration_subsystem/navigator_configuration.py b/src/ansible_navigator/configuration_subsystem/navigator_configuration.py
index 7586eb0c9..0fa7aa25e 100644
--- a/src/ansible_navigator/configuration_subsystem/navigator_configuration.py
+++ b/src/ansible_navigator/configuration_subsystem/navigator_configuration.py
@@ -463,7 +463,7 @@ class Internals:
             short_description="Specify the name for artifacts created from completed playbooks",
             subcommands=["run"],
             value=SettingsEntryValue(
-                default="{playbook_dir}/{playbook_name}-artifact-{ts_utc}.json",
+                default="{playbook_dir}/{playbook_name}-artifact-{time_stamp}.json",
             ),
         ),
         SettingsEntry(
diff --git a/src/ansible_navigator/configuration_subsystem/navigator_post_processor.py b/src/ansible_navigator/configuration_subsystem/navigator_post_processor.py
index 194cadf0a..777fd4df9 100644
--- a/src/ansible_navigator/configuration_subsystem/navigator_post_processor.py
+++ b/src/ansible_navigator/configuration_subsystem/navigator_post_processor.py
@@ -10,6 +10,7 @@
 from itertools import chain
 from itertools import repeat
 from pathlib import Path
+from string import Formatter
 from typing import List
 from typing import Tuple
 
@@ -718,6 +719,32 @@ def playbook_artifact_replay(
                 return messages, exit_messages
         return messages, exit_messages
 
+    @staticmethod
+    @_post_processor
+    def playbook_artifact_save_as(
+        entry: SettingsEntry,
+        config: ApplicationConfiguration,
+    ) -> PostProcessorReturn:
+        # pylint: disable=unused-argument
+        """Post process playbook_artifact_save_as"""
+        messages: List[LogMessage] = []
+        exit_messages: List[ExitMessage] = []
+        # literal_text, fname, format_spec, conversion
+        found = set(f for _, f, _, _ in Formatter().parse(entry.value.current) if f)
+        available = set(f for _, f, _, _ in Formatter().parse(entry.value.default) if f)
+        unknown = found - available
+        if not unknown:
+            return messages, exit_messages
+        exit_msg = (
+            f"The playbook artifact file name '{entry.value.current}', set by"
+            f" {entry.value.source.value.lower()}, has unrecognized variables:"
+            f" {oxfordcomma(unknown, 'and')}"
+        )
+        exit_messages.append(ExitMessage(message=exit_msg))
+        exit_msg = f"Try again with only {oxfordcomma(available, 'and/or')}"
+        exit_messages.append(ExitMessage(message=exit_msg, prefix=ExitPrefix.HINT))
+        return messages, exit_messages
+
     @staticmethod
     @_post_processor
     def pull_arguments(


Test patch:
diff --git a/tests/unit/actions/run/__init__.py b/tests/unit/actions/run/__init__.py
new file mode 100644
index 000000000..762f3dc18
--- /dev/null
+++ b/tests/unit/actions/run/__init__.py
@@ -0,0 +1,1 @@
+"""Unit tests for the run action."""
diff --git a/tests/unit/actions/run/test_artifact.py b/tests/unit/actions/run/test_artifact.py
new file mode 100644
index 000000000..d052d1ac1
--- /dev/null
+++ b/tests/unit/actions/run/test_artifact.py
@@ -0,0 +1,177 @@
+"""Unit tests for artifact creation. in the run action."""
+import logging
+import os
+import re
+
+from copy import deepcopy
+from dataclasses import dataclass
+from typing import Optional
+from typing import Pattern
+
+import pytest
+
+from pytest_mock import MockerFixture
+
+from ansible_navigator.actions.run import Action as action
+from ansible_navigator.configuration_subsystem import NavigatorConfiguration
+
+
+@dataclass
+class TestData:
+    """The artifact files test data object."""
+
+    name: str
+    filename: Optional[str]
+    playbook: str
+    starts_with: Optional[str] = None
+    re_match: Optional[Pattern] = None
+    help_playbook: bool = False
+    time_zone: str = "UTC"
+
+    def __post_init__(self):
+        """Ensure one match is set.
+
+        :raises ValueError: When neither is set
+        """
+        if not (self.re_match or self.starts_with):
+            raise ValueError("re_match or starts_with required")
+
+    def __str__(self):
+        """Provide the test id.
+
+        :returns: The test id
+        """
+        return self.name
+
+
+test_data = [
+    TestData(
+        name="Filename absolute",
+        filename="/tmp/artifact.json",
+        playbook="site.yml",
+        starts_with="/tmp/artifact.json",
+    ),
+    TestData(
+        name="Filename with .",
+        filename="./artifact.json",
+        playbook="site.yml",
+        starts_with=f"{os.path.abspath('.')}/artifact.json",
+    ),
+    TestData(
+        name="Filename with ..",
+        filename="../artifact.json",
+        playbook="site.yml",
+        starts_with=f"{os.path.abspath('..')}/artifact.json",
+    ),
+    TestData(
+        name="Filename with ~",
+        filename="~/artifact.json",
+        playbook="/tmp/site.yaml",
+        starts_with="/home/test_user/artifact.json",
+    ),
+    TestData(
+        name="Playbook absolute",
+        filename=None,
+        playbook="/tmp/site.yaml",
+        starts_with="/tmp/site-artifact",
+    ),
+    TestData(
+        name="Playbook with .",
+        filename=None,
+        playbook="./site.yaml",
+        starts_with=f"{os.path.abspath('.')}/site-artifact",
+    ),
+    TestData(
+        name="Playbook with ..",
+        filename=None,
+        playbook="../site.yaml",
+        starts_with=f"{os.path.abspath('..')}/site-artifact",
+    ),
+    TestData(
+        name="Playbook with ~",
+        filename=None,
+        playbook="~/site.yaml",
+        starts_with="/home/test_user/site-artifact",
+    ),
+    TestData(
+        name="help_playbook enabled",
+        filename=None,
+        playbook="~/site.yaml",
+        starts_with="/home/test_user/site-artifact",
+        help_playbook=True,
+    ),
+    TestData(
+        name="Filename timezone",
+        filename="/tmp/{time_stamp}.json",
+        playbook="site.yml",
+        time_zone="America/Los_Angeles",
+        re_match=re.compile("^/tmp/.*-0[7,8]:00"),
+    ),
+]
+
+
+@pytest.mark.parametrize("data", test_data, ids=str)
+def test_artifact_path(
+    monkeypatch: pytest.MonkeyPatch,
+    mocker: MockerFixture,
+    caplog: pytest.LogCaptureFixture,
+    data: TestData,
+):
+    """Test the building of the artifact filename given a filename or playbook.
+
+    :param monkeypatch: The monkeypatch fixture
+    :param mocker: The mocker fixture
+    :param caplog: The log capture fixture
+    :param data: The test data
+    """
+    caplog.set_level(logging.DEBUG)
+    monkeypatch.setenv("HOME", "/home/test_user")
+
+    def make_dirs(*_args, **_kwargs):
+        return True
+
+    monkeypatch.setattr(os, "makedirs", make_dirs)
+
+    def get_status(*_args, **_kwargs):
+        return (0, 0)
+
+    monkeypatch.setattr(action, "_get_status", get_status)
+
+    mocked_write = mocker.patch(
+        "ansible_navigator.actions.run.serialize_write_file",
+        return_value=None,
+    )
+
+    args = deepcopy(NavigatorConfiguration)
+    args.entry("playbook").value.current = data.playbook
+    args.entry("help_playbook").value.current = data.help_playbook
+    args.entry("time_zone").value.current = data.time_zone
+    args.entry("playbook_artifact_enable").value.current = True
+
+    save_as = args.entry("playbook_artifact_save_as")
+
+    if data.filename:
+        save_as.value.current = data.filename
+    else:
+        save_as.value.current = save_as.value.default
+
+    args.post_processor.playbook(
+        entry=args.entry("playbook"),
+        config=args,
+    )
+    args.post_processor.playbook_artifact_save_as(
+        entry=save_as,
+        config=args,
+    )
+
+    run_action = action(args=args)
+    run_action.write_artifact(filename=data.filename)
+
+    if data.help_playbook is not True:
+        opened_filename = str(mocked_write.call_args[1]["file"])
+        if data.starts_with is not None:
+            assert opened_filename.startswith(data.starts_with), caplog.text
+        if data.re_match is not None:
+            assert data.re_match.match(opened_filename), caplog.text
+    else:
+        mocked_write.assert_not_called()
diff --git a/tests/unit/actions/run/test_runner_async.py b/tests/unit/actions/run/test_runner_async.py
new file mode 100644
index 000000000..82449aac2
--- /dev/null
+++ b/tests/unit/actions/run/test_runner_async.py
@@ -0,0 +1,138 @@
+"""Test settings through to runner."""
+
+from copy import deepcopy
+from dataclasses import dataclass
+from pathlib import Path
+from queue import Queue
+from typing import Dict
+from typing import List
+from typing import Optional
+
+import pytest
+
+from pytest_mock import MockerFixture
+
+from ansible_navigator.actions.run import Action as action
+from ansible_navigator.configuration_subsystem import NavigatorConfiguration
+
+
+@dataclass
+class TestData:
+    """The runner test data object."""
+
+    # pylint: disable=too-many-instance-attributes
+
+    name: str
+    container_engine: Optional[str]
+    container_options: Optional[List]
+    execution_environment_image: Optional[str]
+    execution_environment: Optional[bool]
+    inventory: Optional[List]
+    playbook_artifact_enable: bool
+    mode: Optional[str]
+    pass_environment_variable: Optional[List]
+    set_environment_variable: Optional[Dict]
+    playbook: Optional[str]
+    container_volume_mounts: Optional[List]
+    help_playbook: bool
+    cmdline: Optional[List]
+    private_data_dir: Optional[str]
+    rotate_artifacts: Optional[int]
+    timeout: Optional[int]
+    expected: Dict
+
+    def __str__(self):
+        """Provide the test id.
+
+        :returns: The test id
+        """
+        return self.name
+
+
+TEST_QUEUE: Queue = Queue()
+
+test_data = [
+    TestData(
+        name="Validate args passed to runner API",
+        container_engine="docker",
+        container_options=["--net=host"],
+        execution_environment_image="quay.io/ansible/network-ee:latest",
+        execution_environment=True,
+        inventory=["/test1/inv1", "/test2/inv2"],
+        playbook_artifact_enable=False,
+        mode="stdout",
+        pass_environment_variable=["pass_env1", "pass_env2"],
+        set_environment_variable={"setvar1": "env1", "setvar2": "env2"},
+        playbook="~/site.yaml",
+        container_volume_mounts=[
+            "/home/on_host/vol1:/home/in_container/vol1:Z",
+            "~/vol2:/home/user/vol2",
+        ],
+        help_playbook=True,
+        cmdline=["--tags", "test"],
+        private_data_dir="/tmp/test1",
+        rotate_artifacts=10,
+        timeout=200,
+        expected={
+            "executable_cmd": "ansible-playbook",
+            "queue": TEST_QUEUE,
+            "container_engine": "docker",
+            "container_options": ["--net=host"],
+            "execution_environment_image": "quay.io/ansible/network-ee:latest",
+            "execution_environment": True,
+            "inventory": ["/test1/inv1", "/test2/inv2"],
+            "navigator_mode": "stdout",
+            "pass_environment_variable": ["pass_env1", "pass_env2"],
+            "set_environment_variable": {"setvar1": "env1", "setvar2": "env2"},
+            "playbook": "~/site.yaml",
+            "container_volume_mounts": [
+                "/home/on_host/vol1:/home/in_container/vol1:Z",
+                "~/vol2:/home/user/vol2",
+            ],
+            "cmdline": ["--help", "--tags", "test"],
+            "host_cwd": str(Path.cwd()),
+            "private_data_dir": "/tmp/test1",
+            "rotate_artifacts": 10,
+            "timeout": 200,
+        },
+    ),
+]
+
+
+@pytest.mark.parametrize("data", test_data, ids=str)
+def test_runner_args(mocker: MockerFixture, data: TestData):
+    """Test the arguments passed to runner API.
+
+    :param mocker: The mocker fixture
+    :param data: The test data
+    """
+    args = deepcopy(NavigatorConfiguration)
+    args.entry("container_engine").value.current = data.container_engine
+    args.entry("container_options").value.current = data.container_options
+    args.entry("execution_environment_image").value.current = data.execution_environment_image
+    args.entry("execution_environment").value.current = data.execution_environment
+    args.entry("inventory").value.current = data.inventory
+    args.entry("playbook_artifact_enable").value.current = data.playbook_artifact_enable
+    args.entry("mode").value.current = data.mode
+    args.entry("pass_environment_variable").value.current = data.pass_environment_variable
+    args.entry("set_environment_variable").value.current = data.set_environment_variable
+    args.entry("playbook").value.current = data.playbook
+    args.entry("execution_environment_volume_mounts").value.current = data.container_volume_mounts
+    args.entry("help_playbook").value.current = data.help_playbook
+    args.entry("cmdline").value.current = data.cmdline
+    args.entry("ansible_runner_artifact_dir").value.current = data.private_data_dir
+    args.entry("ansible_runner_rotate_artifacts_count").value.current = data.rotate_artifacts
+    args.entry("ansible_runner_timeout").value.current = data.timeout
+
+    TestRunnerException = Exception
+    command_async = mocker.patch(
+        "ansible_navigator.actions.run.CommandAsync",
+        side_effect=TestRunnerException,
+    )
+
+    run = action(args=args)
+    run._queue = TEST_QUEUE  # pylint: disable=protected-access
+    with pytest.raises(TestRunnerException):
+        run._run_runner()  # pylint: disable=protected-access
+
+    command_async.assert_called_once_with(**data.expected)
diff --git a/tests/unit/actions/test_run.py b/tests/unit/actions/test_run.py
deleted file mode 100644
index 35de308d2..000000000
--- a/tests/unit/actions/test_run.py
+++ /dev/null
@@ -1,222 +0,0 @@
-"""Unit tests for the run action.
-"""
-import logging
-import os
-
-from copy import deepcopy
-from pathlib import Path
-from queue import Queue
-from typing import Dict
-from typing import List
-from typing import NamedTuple
-from typing import Optional
-
-# pylint: disable=preferred-module  # FIXME: remove once migrated per GH-872
-from unittest.mock import mock_open
-from unittest.mock import patch
-
-import pytest
-
-from ansible_navigator.actions.run import Action as action
-from ansible_navigator.configuration_subsystem import NavigatorConfiguration
-
-
-TEST_QUEUE: Queue = Queue()
-
-
-class ArtifactTstData(NamedTuple):
-    """The artifact files test data object."""
-
-    name: str
-    filename: Optional[str]
-    playbook: str
-    expected: str
-    help_playbook: bool = False
-
-
-def id_from_data(value):
-    """Return the name from the test data object."""
-    return f" {value.name} "
-
-
-artifact_test_data = [
-    ArtifactTstData("Filename absolute", "/tmp/artifact.json", "site.yml", "/tmp/artifact.json"),
-    ArtifactTstData(
-        "Filename with .",
-        "./artifact.json",
-        "site.yml",
-        f"{os.path.abspath('.')}/artifact.json",
-    ),
-    ArtifactTstData(
-        "Filename with ..",
-        "../artifact.json",
-        "site.yml",
-        f"{os.path.abspath('..')}/artifact.json",
-    ),
-    ArtifactTstData(
-        "Filename with ~",
-        "~/artifact.json",
-        "/tmp/site.yaml",
-        "/home/test_user/artifact.json",
-    ),
-    ArtifactTstData("Playbook absolute", None, "/tmp/site.yaml", "/tmp/site-artifact"),
-    ArtifactTstData(
-        "Playbook with .",
-        None,
-        "./site.yaml",
-        f"{os.path.abspath('.')}/site-artifact",
-    ),
-    ArtifactTstData(
-        "Playbook with ..",
-        None,
-        "../site.yaml",
-        f"{os.path.abspath('..')}/site-artifact",
-    ),
-    ArtifactTstData("Playbook with ~", None, "~/site.yaml", "/home/test_user/site-artifact"),
-    ArtifactTstData(
-        "help_playbook enabled",
-        None,
-        "~/site.yaml",
-        "/home/test_user/site-artifact",
-        True,
-    ),
-]
-
-
-@patch.dict("os.environ", {"HOME": "/home/test_user"})
-@patch("os.makedirs", return_value=True)
-@patch("ansible_navigator.actions.run.Action._get_status", return_value=(0, 0))
-@pytest.mark.parametrize("data", artifact_test_data, ids=id_from_data)
-def test_artifact_path(_mocked_get_status, _mocked_makedirs, caplog, data):
-    """Test the building of the artifact filename given a filename or playbook"""
-    caplog.set_level(logging.DEBUG)
-
-    args = deepcopy(NavigatorConfiguration)
-    args.entry("playbook").value.current = data.playbook
-    args.entry("help_playbook").value.current = data.help_playbook
-    args.post_processor.playbook(entry=args.entry("playbook"), config=args)
-    playbook_artifact_save_as = args.entry("playbook_artifact_save_as")
-    if data.filename:
-        args.entry("playbook_artifact_save_as").value.current = data.filename
-    else:
-        args.entry(
-            "playbook_artifact_save_as",
-        ).value.current = playbook_artifact_save_as.value.default
-    args.entry("playbook_artifact_enable").value.current = True
-
-    run = action(args=args)
-
-    opener = mock_open()
-
-    def mocked_open(self, *args, **kwargs):
-        return opener(self, *args, **kwargs)
-
-    with patch.object(Path, "open", mocked_open):
-        run.write_artifact(filename=data.filename)
-
-    if data.help_playbook is not True:
-        open_filename = str(opener.call_args[0][0])
-        assert open_filename.startswith(data.expected), caplog.text
-    else:
-        opener.assert_not_called()
-
-
-class RunRunnerTstData(NamedTuple):
-    """the run runner test data object"""
-
-    name: str
-    container_engine: Optional[str]
-    container_options: Optional[List]
-    execution_environment_image: Optional[str]
-    execution_environment: Optional[bool]
-    inventory: Optional[List]
-    playbook_artifact_enable: bool
-    mode: Optional[str]
-    pass_environment_variable: Optional[List]
-    set_environment_variable: Optional[Dict]
-    playbook: Optional[str]
-    container_volume_mounts: Optional[List]
-    help_playbook: bool
-    cmdline: Optional[List]
-    private_data_dir: Optional[str]
-    rotate_artifacts: Optional[int]
-    timeout: Optional[int]
-    expected: Dict
-
-
-runner_test_data = [
-    RunRunnerTstData(
-        "Validate args passed to runner API",
-        "docker",
-        ["--net=host"],
-        "quay.io/ansible/network-ee:latest",
-        True,
-        ["/test1/inv1", "/test2/inv2"],
-        False,
-        "stdout",
-        ["pass_env1", "pass_env2"],
-        {"setvar1": "env1", "setvar2": "env2"},
-        "~/site.yaml",
-        ["/home/on_host/vol1:/home/in_container/vol1:Z", "~/vol2:/home/user/vol2"],
-        True,
-        ["--tags", "test"],
-        "/tmp/test1",
-        10,
-        200,
-        {
-            "executable_cmd": "ansible-playbook",
-            "queue": TEST_QUEUE,
-            "container_engine": "docker",
-            "container_options": ["--net=host"],
-            "execution_environment_image": "quay.io/ansible/network-ee:latest",
-            "execution_environment": True,
-            "inventory": ["/test1/inv1", "/test2/inv2"],
-            "navigator_mode": "stdout",
-            "pass_environment_variable": ["pass_env1", "pass_env2"],
-            "set_environment_variable": {"setvar1": "env1", "setvar2": "env2"},
-            "playbook": "~/site.yaml",
-            "container_volume_mounts": [
-                "/home/on_host/vol1:/home/in_container/vol1:Z",
-                "~/vol2:/home/user/vol2",
-            ],
-            "cmdline": ["--help", "--tags", "test"],
-            "host_cwd": os.getcwd(),
-            "private_data_dir": "/tmp/test1",
-            "rotate_artifacts": 10,
-            "timeout": 200,
-        },
-    ),
-]
-
-
-@patch("ansible_navigator.actions.run.CommandAsync")
-@pytest.mark.parametrize("data", runner_test_data, ids=id_from_data)
-def test_runner_args(_mocked_command_runner, caplog, data):
-    """Test the arguments passed to runner API"""
-    caplog.set_level(logging.DEBUG)
-
-    args = deepcopy(NavigatorConfiguration)
-    args.entry("container_engine").value.current = data.container_engine
-    args.entry("container_options").value.current = data.container_options
-    args.entry("execution_environment_image").value.current = data.execution_environment_image
-    args.entry("execution_environment").value.current = data.execution_environment
-    args.entry("inventory").value.current = data.inventory
-    args.entry("playbook_artifact_enable").value.current = data.playbook_artifact_enable
-    args.entry("mode").value.current = data.mode
-    args.entry("pass_environment_variable").value.current = data.pass_environment_variable
-    args.entry("set_environment_variable").value.current = data.set_environment_variable
-    args.entry("playbook").value.current = data.playbook
-    args.entry("execution_environment_volume_mounts").value.current = data.container_volume_mounts
-    args.entry("help_playbook").value.current = data.help_playbook
-    args.entry("cmdline").value.current = data.cmdline
-    args.entry("ansible_runner_artifact_dir").value.current = data.private_data_dir
-    args.entry("ansible_runner_rotate_artifacts_count").value.current = data.rotate_artifacts
-    args.entry("ansible_runner_timeout").value.current = data.timeout
-
-    run = action(args=args)
-    run._queue = TEST_QUEUE  # pylint: disable=protected-access
-    run._run_runner()  # pylint: disable=protected-access
-
-    _runner_args_passed = _mocked_command_runner.call_args[1]
-    assert _runner_args_passed == data.expected
-    _mocked_command_runner.assert_called_once()
diff --git a/tests/unit/configuration_subsystem/post_processors/test_playbook_artifact_save_as.py b/tests/unit/configuration_subsystem/post_processors/test_playbook_artifact_save_as.py
new file mode 100644
index 000000000..3899e8c20
--- /dev/null
+++ b/tests/unit/configuration_subsystem/post_processors/test_playbook_artifact_save_as.py
@@ -0,0 +1,83 @@
+"""Tests for the playbook artifact save as post processor."""
+from dataclasses import dataclass
+from typing import Optional
+
+import pytest
+
+from ansible_navigator.configuration_subsystem import Constants as C
+from ansible_navigator.configuration_subsystem import NavigatorConfiguration
+from ansible_navigator.configuration_subsystem.navigator_post_processor import (
+    NavigatorPostProcessor,
+)
+
+
+@dataclass
+class Scenario:
+    """Data structure for PAS post processor tests."""
+
+    current: Optional[str] = None
+    expected: Optional[str] = None
+    exit_message_substr: str = ""
+
+    def __post_init__(self):
+        """Set the expected if errors are expected."""
+        if self.expected is None:
+            self.expected = self.current
+
+    def __str__(self):
+        """Provide a test id.
+
+        :returns: The test id
+        """
+        return f"{self.current}"
+
+
+test_data = (
+    Scenario(
+        expected="{playbook_dir}/{playbook_name}-artifact-{time_stamp}.json",
+    ),
+    Scenario(
+        current="/tmp/artifact.json",
+    ),
+    Scenario(
+        current="{playbook_dir}/{playbook_name}-artifact-{ts_utc}.json",
+        exit_message_substr=(
+            "The playbook artifact file name"
+            " '{playbook_dir}/{playbook_name}-artifact-{ts_utc}.json', set by command line,"
+            " has unrecognized variables: 'ts_utc'"
+        ),
+    ),
+    Scenario(
+        current="{name}.json",
+        exit_message_substr=(
+            "The playbook artifact file name"
+            " '{name}.json', set by command line,"
+            " has unrecognized variables: 'name'"
+        ),
+    ),
+)
+
+
+@pytest.mark.parametrize(argnames="data", argvalues=test_data, ids=str)
+def test(data: Scenario):
+    """Test the PAS post processor.
+
+    :param data: The test data
+    """
+    settings = NavigatorConfiguration
+    entry = settings.entry("playbook_artifact_save_as")
+
+    if data.current is None:
+        entry.value.current = entry.value.default
+    else:
+        entry.value.current = data.current
+    entry.value.source = C.USER_CLI
+
+    _messages, exit_messages = NavigatorPostProcessor().playbook_artifact_save_as(
+        entry=entry,
+        config=settings,
+    )
+    assert entry.value.current == data.expected
+
+    if data.exit_message_substr:
+        assert data.exit_message_substr in exit_messages[0].message