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

Issue:
Return code from runner with mode stdout not provided
Reported by @GomathiselviS 

```
TASK [We have a problem] *******************************************************
task path: /home/zuul/src/github.com/ansible/network-ee/tests/integration/playbooks/site.yaml:45
<199.204.45.132> attempting to start connection
Please review the log for errors.
<199.204.45.132> using connection plugin ansible.netcommon.network_cli
Found ansible-connection at path /usr/local/bin/ansible-connection
<199.204.45.132> found existing local domain socket, using it!
[WARNING]: Persistent connection logging is enabled for 199.204.45.132. This
will log ALL interactions to /home/zuul/src/github.com/ansible/network-
ee/tests/integration/logs/ansible-debug.txt and WILL NOT redact sensitive
configuration like passwords. USE WITH CAUTION!
<199.204.45.132> updating play_context for connection
<199.204.45.132> 
<199.204.45.132> local domain socket path is /home/runner/.ansible/pc/709fa3eada
fatal: [eos]: FAILED! => {
    "changed": false,
    "msg": "One or more integration tests failed!"
}

PLAY RECAP *********************************************************************
eos                        : ok=2446 changed=403  unreachable=0    failed=2    skipped=6    rescued=0    ignored=104 
```
![image](https://user-images.githubusercontent.com/18386516/156666649-c57ab61a-e2f2-4f5d-b611-b32733b269b2.png)

Normalize stdout return from actions
Fixes #291

This normalizes the output of `run_stdout` across all actions to be predefined NamedTuple where a string is something needing to be displayed via stderr or stdout based on the return code.

Note that, there is no case where we do this by design, but it was accounted for in the `builder` action, so rather than have one be slightly different, the write of messages was moved up to `cli.py` and all actions may now return a string.

Tests updated to account for the return.




Discussion / hints:

Fix patch:
diff --git a/src/ansible_navigator/cli.py b/src/ansible_navigator/cli.py
index c4b4adbc9..b15c1431c 100644
--- a/src/ansible_navigator/cli.py
+++ b/src/ansible_navigator/cli.py
@@ -170,6 +170,7 @@ def main():
     run_message = f"{run_return.message}\n"
     if run_return.return_code != 0 and run_return.message:
         sys.stderr.write(run_message)
+        sys.exit(run_return.return_code)
     elif run_return.message:
         sys.stdout.write(run_message)
 


Test patch:
diff --git a/tests/integration/test_stdout_exit_codes.py b/tests/integration/test_stdout_exit_codes.py
index 616ed9b52..09c001bbd 100644
--- a/tests/integration/test_stdout_exit_codes.py
+++ b/tests/integration/test_stdout_exit_codes.py
@@ -1,12 +1,16 @@
 """check return codes from mode ``stdout``
 """
 import os
+import subprocess
 
+from pathlib import Path
+from typing import List
 from typing import NamedTuple
 from typing import Tuple
 
 import pytest
 
+from ansible_navigator.utils.functions import shlex_join
 from ..defaults import DEFAULT_CONTAINER_IMAGE
 from ..defaults import FIXTURES_DIR
 
@@ -116,3 +120,110 @@ def test(action_run_stdout, params, test_data):
         std_stream = stdout if params["execution_environment"] else stderr
         assert test_data.present in std_stream
         assert test_data.message in run_stdout_return.message
+
+
+class StdoutCliTest(NamedTuple):
+    """Definition of a stdout cli test."""
+
+    comment: str
+    """Description of the test"""
+    params: List[str]
+    """Parameters for the subcommand"""
+    return_code: int
+    """Expected return code"""
+    navigator_stderr: str
+    """Navigator produced stderr"""
+    navigator_stdout: str
+    """Navigator produced stdout"""
+    ansible_stdout: str
+    """Ansible produced stdout"""
+    ansible_stderr: str
+    """Ansible produced stderr"""
+    subcommand: str
+    """The name of the subcommand"""
+    mode: str = "stdout"
+    """The mode to run in"""
+
+    def __str__(self) -> str:
+        """Provide a test id."""
+        return self.comment
+
+    @property
+    def command(self) -> List[str]:
+        """Provide the constructed command"""
+        return ["ansible-navigator", self.subcommand] + self.params + ["--mode", self.mode]
+
+
+# Intentionally not using parametrize so the behavior can be documented
+StdoutCliTests = (
+    StdoutCliTest(
+        comment="run pass",
+        subcommand="run",
+        params=[PLAYBOOK],
+        return_code=0,
+        ansible_stdout="ok=1",
+        ansible_stderr="",
+        navigator_stdout="",
+        navigator_stderr="",
+    ),
+    StdoutCliTest(
+        comment="run fail",
+        subcommand="run",
+        params=["no_such_playbook.yaml"],
+        return_code=1,
+        ansible_stdout="",
+        ansible_stderr="could not be found",
+        navigator_stdout="",
+        navigator_stderr="review the log",
+    ),
+)
+
+
+@pytest.mark.parametrize(argnames="pae", argvalues=(True, False), ids=("pae_true", "pae_false"))
+@pytest.mark.parametrize(argnames="exec_env", argvalues=(True, False), ids=("ee_true", "ee_false"))
+@pytest.mark.parametrize(argnames="data", argvalues=StdoutCliTests, ids=str)
+def test_run_through_cli(tmp_path: Path, data: StdoutCliTest, exec_env: bool, pae: bool) -> None:
+    """Test for a return code from run through a shell.
+
+    :param data: The test data
+    :raises AssertionError: When no virtual environment found
+    """
+    venv_path = os.environ.get("VIRTUAL_ENV")
+    if venv_path is None:
+        raise AssertionError(
+            "VIRTUAL_ENV environment variable was not set but tox should have set it.",
+        )
+    venv = Path(venv_path, "bin", "activate")
+    log_file = str(Path(tmp_path, "log.txt"))
+    artifact_file = str(Path(tmp_path, "artifact.json"))
+
+    command = data.command + [
+        "--lf",
+        log_file,
+        "--pae",
+        str(pae),
+        "--pas",
+        artifact_file,
+        "--ee",
+        str(exec_env),
+    ]
+    bash_wrapped = f"/bin/bash -c 'source {venv!s} && {shlex_join(command)}'"
+    proc_out = subprocess.run(
+        bash_wrapped,
+        stdout=subprocess.PIPE,
+        stderr=subprocess.PIPE,
+        check=False,
+        universal_newlines=True,
+        shell=True,
+    )
+
+    assert data.ansible_stdout in proc_out.stdout
+    if not exec_env and not pae:
+        # Without an EE and PAE, ansible writes to ``stderr``
+        assert data.ansible_stderr in proc_out.stderr
+    else:
+        # Everything is routed through ``stdout``
+        assert data.ansible_stderr in proc_out.stdout
+    assert data.navigator_stdout in proc_out.stdout
+    assert data.navigator_stderr in proc_out.stderr
+    assert data.return_code == proc_out.returncode