Repository: ansible/molecule

Issue:
pre-commit verifier linter
# Issue Type

- Feature request

# Linting molecule tests

I'd like to have a pre-commit linter to verify my ansible role test suite.
Molecule crash when defining environment variable for Ansible containes not string
<!--- Verify first that your issue is not already reported on GitHub -->
<!--- Please also check https://molecule.readthedocs.io/en/latest/faq.html --->
<!--- Please use https://groups.google.com/forum/#!forum/molecule-users for usage questions -->

# Issue Type

- Bug report

# Molecule and Ansible details

```
ansible 2.9.0.dev0
  config file = /home/sshnaidm/.ansible.cfg
  configured module search path = ['/home/sshnaidm/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/sshnaidm/venvs/ansible-dev/src/ansible/lib/ansible
  executable location = /home/sshnaidm/venvs/ansible-dev/bin/ansible
  python version = 3.7.3 (default, May 11 2019, 00:45:16) [GCC 8.3.1 20190223 (Red Hat 8.3.1-2)]

molecule, version 2.22rc2.dev5+g16b7551e.d20190612
```

Molecule installation method (one of):

- pip

Ansible installation method (one of):

- pip

Pre condition:
define provisioner as:
```
provisioner:
  name: ansible
  config_options:
    defaults:
      stdout_callback: debug
  env:
    ANSIBLE_KEEP_REMOTE_FILES: 1
  lint:
    name: ansible-lint
```
when setting ansible env var as int, not string.
This crashes.
The actual problem is in ``sh`` python module, which pass environment as is to ``os.spawnve`` as is: https://github.com/amoffat/sh/blob/master/sh.py#L1402 And ``os.spawnve`` doesn't like if environment are not strings.


# Desired Behavior
Molecule sets environment as string and continues to work.

# Actual Behaviour
Molecule crashes without an error message:
```
DEBUG: ANSIBLE ENVIRONMENT
---
ANSIBLE_CONFIG: /home/sshnaidm/.cache/molecule/startup/default/ansible.cfg
ANSIBLE_FILTER_PLUGINS: /home/sshnaidm/venvs/ansible-dev/src/molecule/molecule/provisioner/ansible/plugins/filters:/home/sshnaidm/.cache/molecule/startup/default/plugins/filters:/home/sshnaidm/sources/local-infr
a/roles/startup/plugins/filters
ANSIBLE_KEEP_REMOTE_FILES: 1
ANSIBLE_LIBRARY: /home/sshnaidm/venvs/ansible-dev/src/molecule/molecule/provisioner/ansible/plugins/libraries:/home/sshnaidm/.cache/molecule/startup/default/library:/home/sshnaidm/sources/local-infra/roles/start
up/library
ANSIBLE_ROLES_PATH: /home/sshnaidm/.cache/molecule/startup/default/roles:/home/sshnaidm/sources/local-infra/roles

DEBUG: MOLECULE ENVIRONMENT
---
MOLECULE_DEBUG: 'True'
MOLECULE_DEPENDENCY_NAME: galaxy
MOLECULE_DRIVER_NAME: podman
MOLECULE_ENV_FILE: /home/sshnaidm/sources/local-infra/roles/startup/.env.yml
MOLECULE_EPHEMERAL_DIRECTORY: /home/sshnaidm/.cache/molecule/startup/default
MOLECULE_FILE: /home/sshnaidm/sources/local-infra/roles/startup/molecule/default/molecule.yml
MOLECULE_INSTANCE_CONFIG: /home/sshnaidm/.cache/molecule/startup/default/instance_config.yml
MOLECULE_INVENTORY_FILE: /home/sshnaidm/.cache/molecule/startup/default/inventory/ansible_inventory.yml
MOLECULE_LINT_NAME: yamllint
MOLECULE_NO_LOG: 'False'
MOLECULE_PROJECT_DIRECTORY: /home/sshnaidm/sources/local-infra/roles/startup
MOLECULE_PROVISIONER_LINT_NAME: ansible-lint
MOLECULE_PROVISIONER_NAME: ansible
MOLECULE_SCENARIO_DIRECTORY: /home/sshnaidm/sources/local-infra/roles/startup/molecule/default
MOLECULE_SCENARIO_NAME: default
MOLECULE_VERIFIER_LINT_NAME: flake8
MOLECULE_VERIFIER_NAME: testinfra
MOLECULE_VERIFIER_TEST_DIRECTORY: /home/sshnaidm/sources/local-infra/roles/startup/molecule/default/tests

DEBUG: SHELL REPLAY
ANSIBLE_CONFIG=/home/sshnaidm/.cache/molecule/startup/default/ansible.cfg ANSIBLE_FILTER_PLUGINS=/home/sshnaidm/venvs/ansible-dev/src/molecule/molecule/provisioner/ansible/plugins/filters:/home/sshnaidm/.cache/m
olecule/startup/default/plugins/filters:/home/sshnaidm/sources/local-infra/roles/startup/plugins/filters ANSIBLE_KEEP_REMOTE_FILES=1 ANSIBLE_LIBRARY=/home/sshnaidm/venvs/ansible-dev/src/molecule/molecule/provisi
oner/ansible/plugins/libraries:/home/sshnaidm/.cache/molecule/startup/default/library:/home/sshnaidm/sources/local-infra/roles/startup/library ANSIBLE_ROLES_PATH=/home/sshnaidm/.cache/molecule/startup/default/ro
les:/home/sshnaidm/sources/local-infra/roles MOLECULE_DEBUG=True MOLECULE_DEPENDENCY_NAME=galaxy MOLECULE_DRIVER_NAME=podman MOLECULE_ENV_FILE=/home/sshnaidm/sources/local-infra/roles/startup/.env.yml MOLECULE_EPHEMERAL_DIRECTORY=/home/sshnaidm/.cache/molecule/startup/default MOLECULE_FILE=/home/sshnaidm/sources/local-infra/roles/startup/molecule/default/molecule.yml MOLECULE_INSTANCE_CONFIG=/home/sshnaidm/.cache/molecule/startup/default/instance_config.yml MOLECULE_INVENTORY_FILE=/home/sshnaidm/.cache/molecule/startup/default/inventory/ansible_inventory.yml MOLECULE_LINT_NAME=yamllint MOLECULE_NO_LOG=False MOLECULE_PROJECT_DIRECTORY=/home/sshnaidm/sources/local-infra/roles/startup MOLECULE_PROVISIONER_LINT_NAME=ansible-lint MOLECULE_PROVISIONER_NAME=ansible MOLECULE_SCENARIO_DIRECTORY=/home/sshnaidm/sources/local-infra/roles/startup/molecule/default MOLECULE_SCENARIO_NAME=default MOLECULE_VERIFIER_LINT_NAME=flake8 MOLECULE_VERIFIER_NAME=testinfra MOLECULE_VERIFIER_TEST_DIRECTORY=/home/sshnaidm/sources/local-infra/roles/startup/molecule/default/tests

DEBUG: COMMAND
/home/sshnaidm/venvs/ansible-dev/bin/ansible-playbook --skip-tags=molecule-notest,notest --diff --inventory=/home/sshnaidm/.cache/molecule/startup/default/inventory /home/sshnaidm/venvs/ansible-dev/src/molecule/molecule/provisioner/ansible/playbooks/podman/destroy.yml -vvv

ERROR: 
```




Discussion:
You can run `molecule lint` on your roles I guess...?

Please be a bit more specific with what you'd say would fulfill this feature request.
I would like to run https://github.com/pre-commit/pre-commit on the ansible tests, I'm working on a pull request to fix this... shouldn't take long.


Resolving patch:
diff --git a/molecule/model/schema_v2.py b/molecule/model/schema_v2.py
--- a/molecule/model/schema_v2.py
+++ b/molecule/model/schema_v2.py
@@ -141,6 +141,7 @@ def pre_validate_base_schema(env, keep_string):
                             'molecule_env_var': True,
                             'allowed': [
                                 'flake8',
+                                'pre-commit',
                                 'rubocop',
                                 'yamllint',
                                 'ansible-lint',
@@ -591,7 +592,9 @@ def pre_validate_base_schema(env, keep_string):
             'name': {'type': 'string', 'allowed': ['testinfra']},
             'lint': {
                 'type': 'dict',
-                'schema': {'name': {'type': 'string', 'allowed': ['flake8']}},
+                'schema': {
+                    'name': {'type': 'string', 'allowed': ['flake8', 'pre-commit']}
+                },
             },
         },
     }
diff --git a/molecule/verifier/base.py b/molecule/verifier/base.py
--- a/molecule/verifier/base.py
+++ b/molecule/verifier/base.py
@@ -24,6 +24,7 @@
 
 from molecule import util
 from molecule.verifier.lint import flake8
+from molecule.verifier.lint import precommit
 from molecule.verifier.lint import rubocop
 from molecule.verifier.lint import yamllint
 from molecule.verifier.lint import ansible_lint
@@ -106,6 +107,8 @@ def lint(self):
         lint_name = self._config.config['verifier']['lint']['name']
         if lint_name == 'flake8':
             return flake8.Flake8(self._config)
+        if lint_name == 'pre-commit':
+            return precommit.PreCommit(self._config)
         if lint_name == 'rubocop':
             return rubocop.RuboCop(self._config)
         if lint_name == 'yamllint':
diff --git a/molecule/verifier/lint/precommit.py b/molecule/verifier/lint/precommit.py
new file mode 100644
--- /dev/null
+++ b/molecule/verifier/lint/precommit.py
@@ -0,0 +1,161 @@
+#  Copyright (c) 2019 Red Hat, Inc.
+#
+#  Permission is hereby granted, free of charge, to any person obtaining a copy
+#  of this software and associated documentation files (the "Software"), to
+#  deal in the Software without restriction, including without limitation the
+#  rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+#  sell copies of the Software, and to permit persons to whom the Software is
+#  furnished to do so, subject to the following conditions:
+#
+#  The above copyright notice and this permission notice shall be included in
+#  all copies or substantial portions of the Software.
+#
+#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+#  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+#  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+#  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+#  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+#  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+#  DEALINGS IN THE SOFTWARE.
+"""Linter module for pre-commit related code."""
+
+import os
+
+import sh
+
+from molecule import logger
+from molecule import util
+from molecule.verifier.lint import base
+
+LOG = logger.get_logger(__name__)
+
+
+class PreCommit(base.Base):
+    """
+    Pre-commit tool verifier wrapper.
+
+    This class is used to lint files by executing the pre-commit
+    command line tool for files in the test folder with a prefix
+    of ``test_``.
+
+    `Pre-Commit`_ is not the default verifier linter.
+
+    `Pre-Commit`_ is a linter for python files and more.
+
+    Additional options can be passed to ``pre-commit`` through the options
+    dict.  Any option set in this section will override the defaults.
+
+    .. code-block:: yaml
+
+        verifier:
+          name: testinfra
+          lint:
+            name: pre-commit
+            options:
+              remove-tabs:
+
+    Test file linting can be disabled by setting ``enabled`` to False.
+
+    .. code-block:: yaml
+
+        verifier:
+          name: testinfra
+          lint:
+            name: pre-commit
+            enabled: False
+
+    Environment variables can be passed to lint.
+
+    .. code-block:: yaml
+
+        verifier:
+          name: testinfra
+          lint:
+            name: pre-commit
+            env:
+              FOO: bar
+
+    Example pre-commit configuration file (``.pre-commit-config.yaml``) to run
+    flake8.
+
+    .. code-block:: yaml
+
+        repos:
+          - repo: local
+            hooks:
+              - id: flake8
+                name: flake8
+                entry: python -m flake8 --max-line-length=120
+                language: system
+                types: [python]
+
+    .. _`Pre-Commit`: https://pre-commit.com/
+    """
+
+    def __init__(self, config):
+        """
+        Set up the requirements to execute ``pre-commit`` tool.
+
+        :param config: An instance of a Molecule config.
+        """
+        super(PreCommit, self).__init__(config)
+        self._precommit_command = None
+        if config:
+            self._tests = self._get_tests()
+
+    @property
+    def default_options(self):
+        """Default options for pre-commit tool runtime."""
+        return {}
+
+    @property
+    def default_env(self):
+        """Default environment variables for pre-commit tool runtime."""
+        return util.merge_dicts(os.environ.copy(), self._config.env)
+
+    def bake(self):
+        """Bake a ready to execute ``pre-commit`` command."""
+        self._precommit_command = sh.Command('pre-commit').bake(
+            'run',
+            self.options,
+            '--files',
+            self._tests,
+            _env=self.env,
+            _out=LOG.out,
+            _err=LOG.error,
+        )
+
+    def execute(self):
+        """Execute the pre-commit command."""
+        if not self.enabled:
+            msg = 'Skipping, verifier_lint is disabled.'
+            LOG.warn(msg)
+            return
+
+        if not self._tests:
+            msg = 'Skipping, no tests found.'
+            LOG.warn(msg)
+            return
+
+        if self._precommit_command is None:
+            self.bake()
+
+        msg = 'Executing pre-commit on files found in {}/...'.format(
+            self._config.verifier.directory
+        )
+        LOG.info(msg)
+
+        try:
+            util.run_command(self._precommit_command, debug=self._config.debug)
+            msg = 'Lint completed successfully.'
+            LOG.success(msg)
+        except sh.ErrorReturnCode as e:
+            util.sysexit(e.exit_code)
+
+    def _get_tests(self):
+        """
+        Get a list of test files from the verifier's directory.
+
+        :return: list
+        """
+        return list(util.os_walk(self._config.verifier.directory, 'test_*.py'))


Test patch:
diff --git a/test/functional/docker/test_scenarios.py b/test/functional/docker/test_scenarios.py
--- a/test/functional/docker/test_scenarios.py
+++ b/test/functional/docker/test_scenarios.py
@@ -383,6 +383,27 @@ def test_interpolation(scenario_to_test, with_scenario, scenario_name):
     pytest.helpers.run_command(cmd, env=env)
 
 
+@pytest.mark.parametrize(
+    'scenario_to_test, driver_name, scenario_name',
+    [('verifier', 'docker', 'testinfra-pre-commit')],
+    indirect=['scenario_to_test', 'driver_name', 'scenario_name'],
+)
+def test_command_verify_testinfra_precommit(
+    scenario_to_test, with_scenario, scenario_name
+):
+    options = {'scenario_name': scenario_name}
+    cmd = sh.molecule.bake('create', **options)
+    pytest.helpers.run_command(cmd)
+
+    options = {'scenario_name': scenario_name}
+    cmd = sh.molecule.bake('converge', **options)
+    pytest.helpers.run_command(cmd)
+
+    options = {'scenario_name': scenario_name}
+    cmd = sh.molecule.bake('verify', **options)
+    pytest.helpers.run_command(cmd)
+
+
 @pytest.mark.parametrize(
     'scenario_to_test, driver_name, scenario_name',
     [('verifier', 'docker', 'testinfra')],
diff --git a/test/scenarios/verifier/.pre-commit-config.yaml b/test/scenarios/verifier/.pre-commit-config.yaml
new file mode 100644
--- /dev/null
+++ b/test/scenarios/verifier/.pre-commit-config.yaml
@@ -0,0 +1,9 @@
+---
+repos:
+  - repo: local
+    hooks:
+      - id: flake8
+        name: flake8
+        entry: python -m flake8 --max-line-length=120
+        language: system
+        types: [python]
diff --git a/test/scenarios/verifier/molecule/testinfra-pre-commit/molecule.yml b/test/scenarios/verifier/molecule/testinfra-pre-commit/molecule.yml
new file mode 100644
--- /dev/null
+++ b/test/scenarios/verifier/molecule/testinfra-pre-commit/molecule.yml
@@ -0,0 +1,29 @@
+---
+dependency:
+  name: galaxy
+driver:
+  name: docker
+lint:
+  name: yamllint
+  options:
+    config-file: ../../resources/.yamllint
+platforms:
+  - name: instance
+    image: centos:latest
+provisioner:
+  name: ansible
+  playbooks:
+    create: ../../../../resources/playbooks/docker/create.yml
+    destroy: ../../../../resources/playbooks/docker/destroy.yml
+  lint:
+    name: ansible-lint
+scenario:
+  name: testinfra-pre-commit
+verifier:
+  name: testinfra
+  options:
+    vvv: true
+  additional_files_or_dirs:
+    - ../shared/test_*.py
+  lint:
+    name: pre-commit
diff --git a/test/scenarios/verifier/molecule/testinfra-pre-commit/playbook.yml b/test/scenarios/verifier/molecule/testinfra-pre-commit/playbook.yml
new file mode 100644
--- /dev/null
+++ b/test/scenarios/verifier/molecule/testinfra-pre-commit/playbook.yml
@@ -0,0 +1,19 @@
+---
+- name: Converge
+  hosts: all
+  tasks:
+    - name: Create /tmp/molecule
+      file:
+        dest: /etc/molecule
+        group: root
+        owner: root
+        mode: 0755
+        state: directory
+
+    - name: Create /etc/molecule/{{ ansible_hostname }}
+      copy:
+        dest: "/etc/molecule/{{ ansible_hostname }}"
+        group: root
+        owner: root
+        mode: 0644
+        content: "{{ ansible_hostname }}"
diff --git a/test/scenarios/verifier/molecule/testinfra-pre-commit/tests/test_default.py b/test/scenarios/verifier/molecule/testinfra-pre-commit/tests/test_default.py
new file mode 100644
--- /dev/null
+++ b/test/scenarios/verifier/molecule/testinfra-pre-commit/tests/test_default.py
@@ -0,0 +1,7 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+
+def test_ansible_hostname(host):
+    f = host.file('/tmp/molecule/instance-1')
+    assert not f.exists
diff --git a/test/unit/verifier/lint/test_precommit.py b/test/unit/verifier/lint/test_precommit.py
new file mode 100644
--- /dev/null
+++ b/test/unit/verifier/lint/test_precommit.py
@@ -0,0 +1,186 @@
+#  Copyright (c) 2019 Red Hat, Inc.
+#
+#  Permission is hereby granted, free of charge, to any person obtaining a copy
+#  of this software and associated documentation files (the "Software"), to
+#  deal in the Software without restriction, including without limitation the
+#  rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+#  sell copies of the Software, and to permit persons to whom the Software is
+#  furnished to do so, subject to the following conditions:
+#
+#  The above copyright notice and this permission notice shall be included in
+#  all copies or substantial portions of the Software.
+#
+#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+#  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+#  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+#  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+#  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+#  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+#  DEALINGS IN THE SOFTWARE.
+
+import pytest
+import sh
+
+from molecule import config
+from molecule.verifier.lint import precommit
+
+
+@pytest.fixture
+def _patched_get_tests(monkeypatch):
+    monkeypatch.setattr(
+        precommit.PreCommit, '_get_tests', lambda _self: ['test1', 'test2', 'test3']
+    )
+
+
+@pytest.fixture
+def _verifier_lint_section_data():
+    return {
+        'verifier': {
+            'name': 'testinfra',
+            'lint': {
+                'name': 'pre-commit',
+                'options': {'color': 'never'},
+                'env': {'FOO': 'bar'},
+            },
+        }
+    }
+
+
+# NOTE(retr0h): The use of the `patched_config_validate` fixture, disables
+# config.Config._validate from executing.  Thus preventing odd side-effects
+# throughout patched.assert_called unit tests.
+@pytest.fixture
+def _instance(patched_config_validate, config_instance):
+    return precommit.PreCommit(config_instance)
+
+
+def test_config_private_member(_instance):
+    assert isinstance(_instance._config, config.Config)
+
+
+def test_default_options_property(_instance):
+    assert {} == _instance.default_options
+
+
+def test_default_env_property(_instance):
+    assert 'MOLECULE_FILE' in _instance.default_env
+    assert 'MOLECULE_INVENTORY_FILE' in _instance.default_env
+    assert 'MOLECULE_SCENARIO_DIRECTORY' in _instance.default_env
+    assert 'MOLECULE_INSTANCE_CONFIG' in _instance.default_env
+
+
+@pytest.mark.parametrize(
+    'config_instance', ['_verifier_lint_section_data'], indirect=True
+)
+def test_env_property(_instance):
+    assert 'bar' == _instance.env['FOO']
+
+
+@pytest.mark.parametrize(
+    'config_instance', ['_verifier_lint_section_data'], indirect=True
+)
+def test_name_property(_instance):
+    assert 'pre-commit' == _instance.name
+
+
+def test_enabled_property(_instance):
+    assert _instance.enabled
+
+
+@pytest.mark.parametrize(
+    'config_instance', ['_verifier_lint_section_data'], indirect=True
+)
+def test_options_property(_instance):
+    """Test options to disable pre-commit color output."""
+    color_opt = {'color': 'never'}
+    assert color_opt == _instance.options
+
+
+@pytest.mark.parametrize(
+    'config_instance', ['_verifier_lint_section_data'], indirect=True
+)
+def test_options_property_handles_cli_args(_instance):
+    _instance._config.args = {'debug': True}
+    color_opt = {'color': 'never'}
+    # Does nothing.  The `pre-commit` command does not support
+    # a `debug` flag.
+    assert color_opt == _instance.options
+
+
+@pytest.mark.parametrize(
+    'config_instance', ['_verifier_lint_section_data'], indirect=True
+)
+def test_bake(_instance):
+    _instance._tests = ['test1', 'test2', 'test3']
+    _instance.bake()
+    cmd_str = '{} run --color=never --files test1 test2 test3'.format(
+        str(sh.Command('pre-commit'))
+    )
+
+    assert cmd_str == _instance._precommit_command
+
+
+def test_execute(
+    patched_logger_info, patched_logger_success, patched_run_command, _instance
+):
+    _instance._tests = ['test1', 'test2', 'test3']
+    _instance._precommit_command = 'patched-command'
+    _instance.execute()
+
+    patched_run_command.assert_called_once_with('patched-command', debug=False)
+
+    msg = 'Executing pre-commit on files found in {}/...'.format(
+        _instance._config.verifier.directory
+    )
+    patched_logger_info.assert_called_once_with(msg)
+
+    msg = 'Lint completed successfully.'
+    patched_logger_success.assert_called_once_with(msg)
+
+
+def test_execute_does_not_execute(patched_run_command, patched_logger_warn, _instance):
+    _instance._config.config['verifier']['lint']['enabled'] = False
+    _instance.execute()
+
+    assert not patched_run_command.called
+
+    msg = 'Skipping, verifier_lint is disabled.'
+    patched_logger_warn.assert_called_once_with(msg)
+
+
+def test_does_not_execute_without_tests(
+    patched_run_command, patched_logger_warn, _instance
+):
+    _instance.execute()
+
+    assert not patched_run_command.called
+
+    msg = 'Skipping, no tests found.'
+    patched_logger_warn.assert_called_once_with(msg)
+
+
+@pytest.mark.parametrize(
+    'config_instance', ['_verifier_lint_section_data'], indirect=True
+)
+def test_execute_bakes(patched_run_command, _instance):
+    _instance._tests = ['test1', 'test2', 'test3']
+    _instance.execute()
+
+    assert _instance._precommit_command is not None
+
+    cmd = '{} run --color=never --files test1 test2 test3'.format(
+        str(sh.Command('pre-commit'))
+    )
+    patched_run_command.assert_called_once_with(cmd, debug=False)
+
+
+def test_executes_catches_and_exits_return_code(
+    patched_run_command, _patched_get_tests, _instance
+):
+    patched_run_command.side_effect = sh.ErrorReturnCode_1(
+        sh.Command('pre-commit'), b'', b''
+    )
+    with pytest.raises(SystemExit) as e:
+        _instance.execute()
+
+    assert 1 == e.value.code