Skip to content

Commit

Permalink
Added exit code returning after tool output
Browse files Browse the repository at this point in the history
  • Loading branch information
Eugene Pasko committed Jul 27, 2022
1 parent 7ac1adb commit c2bd814
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 24 deletions.
52 changes: 39 additions & 13 deletions pre_commit_config_shellcheck.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
#!/usr/bin/env python


import os
import re
import sys
import tempfile
Expand Down Expand Up @@ -70,6 +68,10 @@ def construct_mapping(self, node: Node, deep: bool = False) -> Dict[str, Any]:
class PreCommitConfigShellcheck:
"""Tool for shellchecking pre-commit config files."""

EXIT_CODE_SUCCESS: int = 0
EXIT_CODE_ERROR: int = 2
EXIT_CODE_FILE_NOT_FOUND: int = 5

def __init__(self):
"""Get command line args."""
self.options: Namespace = self._get_options()
Expand Down Expand Up @@ -125,10 +127,10 @@ def _parse_file(
)
except FileNotFoundError:
sys.stderr.write(f"No file {self.options.path} found\n")
sys.exit(os.EX_OSFILE)
sys.exit(self.EXIT_CODE_FILE_NOT_FOUND)
except ScannerError:
sys.stderr.write(f"{self.options.path} is not a YAML file\n")
sys.exit(os.EX_IOERR)
sys.exit(self.EXIT_CODE_ERROR)

return file_

Expand Down Expand Up @@ -161,7 +163,7 @@ def _find_entries( # noqa: CCR001
sys.stderr.write(
f"An error happened while checking {self.options.path} file: incorrect format\n" # noqa: E501
)
sys.exit(os.EX_IOERR)
sys.exit(self.EXIT_CODE_ERROR)

return result

Expand All @@ -183,21 +185,24 @@ def _list_entries(

return []

@staticmethod
def _write_output(
entry: Dict[str, Dict[str, Union[int, str]]], output: str
) -> None:
def _create_output(
self, entry: Dict[str, Dict[str, Union[int, str]]], output: str
) -> Tuple[str, int]:
"""
Edit and write shellcheck output.
:param entry: entry data to insert into output
:type entry: Dict[str, Dict[str, Union[int, str]]]
:param output: base output to edit and process
:type output: str
:return: subprocess output with process exit code
:rtype: Tuple[str, int]
"""
# regular expression for finding line number from output text:
# returns two groups: "line #" for output replacement and line number itself
regular = re.findall(r"In entry \".*\" (?P<switch>line (?P<line>\d+))", output)
if not regular:
return output, self.EXIT_CODE_SUCCESS
for line_number in regular:
# subtract 2 because of number of lines difference
# in temporary file and source file
Expand All @@ -208,7 +213,21 @@ def _write_output(
line_number[0],
f"on line {new_line_number}",
)

return output, self.EXIT_CODE_ERROR

@staticmethod
def _write_output(output: str, code: int) -> None:
"""
Write whole tool output and exit with desired code.
:param output: output to write in console:
:type output: str
:param code: exit code for the tool
:type code: int
"""
sys.stdout.write(output)
sys.exit(code)

def _check_entry_file(
self,
Expand All @@ -233,7 +252,7 @@ def _check_entry_file(
)
except FileNotFoundError:
sys.stderr.write(f"No shellcheck found: '{self.options.shellcheck}'\n")
sys.exit(os.EX_OSFILE)
sys.exit(self.EXIT_CODE_FILE_NOT_FOUND)

try:
stdout, stderr = process.communicate()
Expand All @@ -242,18 +261,20 @@ def _check_entry_file(
sys.stderr.write(
f"Failed to check entrypoint {entry['id']['id']} on line {entry['entry']['line']}: {err.stderr}" # noqa: E501
)
sys.exit(os.EX_IOERR)
sys.exit(self.EXIT_CODE_ERROR)

if stderr:
sys.stderr.write(
f"Failed to check entrypoint {entry['id']['id']} on line {entry['entry']['line']}: {stderr.decode('UTF-8')}" # noqa: E501
)
sys.exit(os.EX_IOERR)
sys.exit(self.EXIT_CODE_ERROR)

return stdout, stderr

def _check_entries(self) -> None:
"""Check the created file for possible entrypoints issues."""
result = ""
exit_ = self.EXIT_CODE_SUCCESS
for entry in self._list_entries():
with tempfile.NamedTemporaryFile("w+") as tmp:
tmp.write("#!/bin/sh\n")
Expand All @@ -264,7 +285,12 @@ def _check_entries(self) -> None:
name = f"entry \"{entry['id']['id']}\""
output = stdout.decode("utf-8").replace(tmp.name, name)

self._write_output(entry=entry, output=output)
output, code = self._create_output(entry=entry, output=output)
result += output
if code != self.EXIT_CODE_SUCCESS:
exit_ = code

self._write_output(output=result, code=exit_)

def check(self) -> None:
"""Check file for entrypoints and verify them."""
Expand Down
87 changes: 76 additions & 11 deletions tests/test_pre_commit_config_shellcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
__all__: List[str] = [
"test_pre_commit_config_shellcheck___list_entries",
"test_pre_commit_config_shellcheck___list_entries__bad_format",
"test_pre_commit_config_shellcheck___create_output",
"test_pre_commit_config_shellcheck___write_output__empty",
"test_pre_commit_config_shellcheck___write_output",
"test_pre_commit_config_shellcheck___check_entries__wrong_shellcheck",
"test_pre_commit_config_shellcheck___list_entries__empty",
Expand Down Expand Up @@ -290,16 +292,12 @@ def test_pre_commit_config_shellcheck___list_entries__empty(
assert checker._list_entries() == []


def test_pre_commit_config_shellcheck___write_output(
mocker: MockerFixture, capsys: CaptureFixture # type: ignore
) -> None:
def test_pre_commit_config_shellcheck___create_output(mocker: MockerFixture) -> None:
"""
_check_entries method must write in stdout warning in entrypoint.
_create_output method must return process output and desired exit code.
:param mocker: mock
:type mocker: MockerFixture
:param capsys: std output fixture
:type capsys: CaptureFixture
"""
mocker.patch(
"sys.argv",
Expand All @@ -325,10 +323,43 @@ def test_pre_commit_config_shellcheck___write_output(
"""

checker = PreCommitConfigShellcheck() # type: ignore
checker._write_output(entry=entry, output=output) # type: ignore
expected = (
"""
In entry "removestar" on line 17:
removestar -i ${NAME}
^-----^ SC2086: Double quote to prevent globbing and word splitting.
captured = capsys.readouterr()
expected = """
Did you mean: \nremovestar -i "${NAME}"
For more information:
https://www.shellcheck.net/wiki/SC2086 -- Double quote to prevent globbing ...
""",
checker.EXIT_CODE_ERROR,
)

assert checker._create_output(entry=entry, output=output) == expected # type: ignore # noqa: E501


def test_pre_commit_config_shellcheck___write_output(
mocker: MockerFixture, capsys: CaptureFixture # type: ignore
) -> None:
"""
_write_output method must write in stdout all warnings in entrypoints and exit with exit code.
:param mocker: mock
:type mocker: MockerFixture
:param capsys: std output fixture
:type capsys: CaptureFixture
""" # noqa: E501
mocker.patch(
"sys.argv",
[
"pre_commit_config_shellcheck.py",
"tests/fixtures/.pre-commit-config.yaml",
],
)
checker = PreCommitConfigShellcheck() # type: ignore
output = """
In entry "removestar" on line 17:
removestar -i ${NAME}
^-----^ SC2086: Double quote to prevent globbing and word splitting.
Expand All @@ -338,7 +369,40 @@ def test_pre_commit_config_shellcheck___write_output(
For more information:
https://www.shellcheck.net/wiki/SC2086 -- Double quote to prevent globbing ...
"""
assert captured.out == expected
code = checker.EXIT_CODE_ERROR
with pytest.raises(SystemExit):
checker._write_output(output=output, code=code)

captured = capsys.readouterr()
assert captured.out == output


def test_pre_commit_config_shellcheck___write_output__empty(
mocker: MockerFixture, capsys: CaptureFixture # type: ignore
) -> None:
"""
_write_output method must exit with no errors.
:param mocker: mock
:type mocker: MockerFixture
:param capsys: std output fixture
:type capsys: CaptureFixture
"""
mocker.patch(
"sys.argv",
[
"pre_commit_config_shellcheck.py",
"tests/fixtures/.pre-commit-config.yaml",
],
)
checker = PreCommitConfigShellcheck() # type: ignore
output = ""
code = 0
with pytest.raises(SystemExit):
checker._write_output(output=output, code=code)

captured = capsys.readouterr()
assert captured.out == output


def test_pre_commit_config_shellcheck___check_entries(
Expand All @@ -361,7 +425,8 @@ def test_pre_commit_config_shellcheck___check_entries(
)

checker = PreCommitConfigShellcheck() # type: ignore
checker._check_entries()
with pytest.raises(SystemExit):
checker._check_entries()

captured = capsys.readouterr()
expected = """
Expand Down

0 comments on commit c2bd814

Please sign in to comment.