Repository: ansible/molecule

Issue:
molecule does not honour --destroy=never and prunes ephemeral data anyway (appeared after #1739)
# Issue Type

- Bug report

# Molecule and Ansible details

```
ansible --version && molecule --version
ansible 2.7.10
  config file = None
  configured module search path = [u'/home/jenkins/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python2.7/site-packages/ansible
  executable location = /usr/bin/ansible
  python version = 2.7.5 (default, Jun 20 2019, 20:27:34) [GCC 4.8.5 20150623 (Red Hat 4.8.5-36)]
molecule, version 2.22
```

Molecule installation method (one of):

- pip

Ansible installation method (one of):

- pip

Detail any linters or test runners used:

N/A

# Desired Behaviour

When running _molecule test --destroy=never_ do not prune molecule ephemeral data, even when _destroy_ stage is a part of a scenario.

# Actual Behaviour

Molecule configuration excerpt:
```
scenario:
  name: default
  test_sequence:
  - dependency
  - syntax
  - lint
  - create
  - converge
  - idempotence
  - verify
  - destroy
```

_molecule test --destroy=never --all_ does not honour _--destroy=never_ and prunes ephemeral data when _destroy_ is a part of a scenario in the molecule configuration.

Output excerpt:
```
Verifier completed successfully.
--> Scenario: 'default'
--> Action: 'destroy'
Skipping, '--destroy=never' requested.
--> Pruning extra files from scenario ephemeral directory
```

Described behaviour contradicts the purpose of NEVER destroying an environment parameter regardless of what stages were defined in the scenario.
In fact, the environment is left intact, though ephemeral data is gone, hence environment becomes unattended and it's not possible to destroy it later.

_molecule test --destroy=never --all_
_molecule destroy --all_ <-- will not destroy the environment


Previous molecule version works as expected:
```
ansible 2.7.10
  config file = None
  configured module search path = [u'/root/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python2.7/site-packages/ansible
  executable location = /usr/bin/ansible
  python version = 2.7.5 (default, Jun 20 2019, 20:27:34) [GCC 4.8.5 20150623 (Red Hat 4.8.5-36)]
molecule, version 2.20.1
```

Output excerpt
```
Verifier completed successfully.
--> Scenario: 'default'
--> Action: 'destroy'
Skipping, '--destroy=never' requested.
```

Pruning based on presence of _destroy_ stage in the molecule configuration appeared after https://github.com/ansible/molecule/pull/1739



Discussion:
cc @seandst 
Yep: https://github.com/ansible/molecule/blob/575bf4b5a5205562811ff4ca18e35ec94a25f53e/molecule/command/base.py#L145-L146 < destroy --never is not taken into account when deciding to prune based on the destroy step.

I'll put my general thoughts here if anyone's interested in the wall of musings... :)

I don't immediately recall if it's easy to get at the click opts/args from the command-line at this point in the process, but probably some thoughtfulness about the decision to prune is needed. The quickest fix might be to put a `prune` kwarg onto `execute_scenario`, since `scenario.sequence` and the command args both appear to be available when it's called in `execute_cmdline_scenarios`.

As pruning is needed (or not) per-scenario, the scenario config might be a reasonable place to keep track of whether or not to prune at the end, instead of basing it on the sequence members after the fact.

My memory is fuzzy, but I suspect that a major reason for `execute_scenario` to have been created was to deal with some crazy unreadable indentation that happens in the try/except in `execute_cmdline_scenarios`, and also for testability.

Speaking of testability, the failure case reported by this issue is supposed to be covered: https://github.com/ansible/molecule/blob/master/molecule/test/unit/command/test_base.py#L166

But that test is invalid because the mocked `execute_scenario` masks the prune call.

So yeah, I'll fix up the test to fail like it should, and then fix up the code so it passes again. I'll also take a quick peek at some of the other tests related to this stuff and see what other bad mocks I may have made.

Resolving patch:
diff --git a/molecule/command/base.py b/molecule/command/base.py
--- a/molecule/command/base.py
+++ b/molecule/command/base.py
@@ -140,9 +140,10 @@ def execute_scenario(scenario):
     for action in scenario.sequence:
         execute_subcommand(scenario.config, action)
 
-    # pruning only if a 'destroy' step was in the sequence allows for normal
-    # debugging by manually stepping through a scenario sequence
-    if 'destroy' in scenario.sequence:
+    if (
+        'destroy' in scenario.sequence
+        and scenario.config.command_args.get('destroy') != 'never'
+    ):
         scenario.prune()
 
         if scenario.config.is_parallel:


Test patch:
diff --git a/molecule/test/unit/command/test_base.py b/molecule/test/unit/command/test_base.py
--- a/molecule/test/unit/command/test_base.py
+++ b/molecule/test/unit/command/test_base.py
@@ -135,7 +135,37 @@ def test_execute_cmdline_scenarios(
     assert _patched_execute_scenario.call_count == 1
 
 
-def test_execute_cmdline_scenarios_destroy(
+def test_execute_cmdline_scenarios_prune(
+    config_instance, _patched_prune, _patched_execute_subcommand
+):
+    # Subcommands should be executed and prune *should* run when
+    # destroy is 'always'
+    scenario_name = 'default'
+    args = {}
+    command_args = {'destroy': 'always', 'subcommand': 'test'}
+
+    base.execute_cmdline_scenarios(scenario_name, args, command_args)
+
+    assert _patched_execute_subcommand.called
+    assert _patched_prune.called
+
+
+def test_execute_cmdline_scenarios_no_prune(
+    config_instance, _patched_prune, _patched_execute_subcommand
+):
+    # Subcommands should be executed but prune *should not* run when
+    # destroy is 'never'
+    scenario_name = 'default'
+    args = {}
+    command_args = {'destroy': 'never', 'subcommand': 'test'}
+
+    base.execute_cmdline_scenarios(scenario_name, args, command_args)
+
+    assert _patched_execute_subcommand.called
+    assert not _patched_prune.called
+
+
+def test_execute_cmdline_scenarios_exit_destroy(
     config_instance,
     _patched_execute_scenario,
     _patched_prune,
@@ -163,7 +193,7 @@ def test_execute_cmdline_scenarios_destroy(
     assert _patched_sysexit.called
 
 
-def test_execute_cmdline_scenarios_nodestroy(
+def test_execute_cmdline_scenarios_exit_nodestroy(
     config_instance, _patched_execute_scenario, _patched_prune, _patched_sysexit
 ):
     # Ensure execute_cmdline_scenarios handles errors correctly when 'destroy'