tests/acceptance.py

Summary

Maintainability
A
0 mins
Test Coverage
"""Acceptance tests for yala executable."""
import re
from concurrent.futures import ThreadPoolExecutor
from io import StringIO
from unittest import TestCase
from unittest.mock import patch

from yala.main import main


class TestAcceptance(TestCase):
    """Acceptance test."""

    @classmethod
    @patch("yala.main.sys.exit")
    @patch("yala.main.sys.stdout", new_callable=StringIO)
    @patch("yala.main.Pool")
    def setUpClass(cls, pool_mock, stdout_mock, exit_mock):
        """Get yala's output to be used in tests.

        As coverage outputs random results with --concurrency=multiprocessing,
        we use Python threads instead.
        """
        # Ignore params of patch decorators:
        # pylint: disable=arguments-differ
        cls._exit = exit_mock
        # Replace multiprocessing by Python threads
        pool_mock.return_value = ThreadPoolExecutor()
        with patch("yala.main.sys.argv", ["yala", "tests_data/"]):
            main()
        cls._output = stdout_mock.getvalue()

    def _assert_results(self, lines, linter_name):
        """Assert all lines are in the output."""
        for line in lines:
            self._assert_result(line, linter_name)

    def _assert_result(self, result, linter_name):
        self.assertTrue(
            self._output_has_result(result, linter_name),
            f"Couldn't match:\n{result}\nOutput:\n{self._output}",
        )

    def _assert_any_result(self, results, linter_name):
        first_result = any(self._output_has_result(r, linter_name)
                           for r in results)  # fmt: skip
        lines = "\n".join(results)
        self.assertTrue(
            first_result, f"None matched:\n{lines}\nOutput:\n{self._output}"
        )

    def _output_has_result(self, result, linter_name):
        expected_regex = self._get_expected_regex(result, linter_name)
        return re.match(expected_regex, self._output, re.M | re.S) is not None

    @staticmethod
    def _get_expected_regex(line, linter_name):
        """Return a regex to match both Linux and Windows paths."""
        regex = r".*?^tests_data[/\\].*?\.py\|{} \[{}\]$"
        escaped_output = re.escape(line)
        return regex.format(escaped_output, linter_name)

    def test_exit_error(self):
        """Should exit with error if there's linter output."""
        arg = self._exit.call_args_list[0][0][0]
        self.assertTrue(arg, "Exit argument should not be empty")

    def test_flake8(self):
        """Check pycodestyle output."""
        expected = (
            "1:1|F401 'os' imported but unused",
            "2:1|F401 'abc' imported but unused",
            "7:20|E211 whitespace before '('",
        )
        self._assert_results(expected, "flake8")

    def test_isort(self):
        """Check isort output."""
        expected = "None:None|Imports are incorrectly sorted and/or formatted."
        self._assert_result(expected, "isort")

    def test_mypy(self):
        """Check mypy output."""
        expected = (
            '4:None|error: Need type annotation for "untyped_list" '
            '(hint: "untyped_list: List[<type>] = ...")  '
            "[var-annotated]"
        )
        expected_py37 = (
            "4:None|error: Need type annotation for "
            "'untyped_list' (hint: \"untyped_list: List[<type>] = ...\")"
        )
        possible_results = expected, expected_py37
        self._assert_any_result(possible_results, "mypy")

    def test_pycodestyle(self):
        """Check pycodestyle output."""
        expected = "7:20|E211 whitespace before '('"
        self._assert_result(expected, "pycodestyle")

    def test_pydocstyle(self):
        """Check pydocstyle ouput."""
        expected = "1:None|D100: Missing docstring in public module"
        self._assert_result(expected, "pydocstyle")

    def test_pyflakes(self):
        """Check Pyflakes output."""
        expected_any = (
            "1:1|'os' imported but unused",  # pyflakes 2.2.0
            "1:None|'os' imported but unused",  # pyflakes 2.1.1
        )
        self._assert_any_result(expected_any, "pyflakes")

        expected_any = (
            "2:1|'abc' imported but unused",
            "2:None|'abc' imported but unused",
        )
        self._assert_any_result(expected_any, "pyflakes")

    def test_pylint(self):
        """Check Pylint output."""
        expected = (
            "1:0|Missing module docstring (C0114, missing-module-docstring)",
            "1:0|Unused import os (W0611, unused-import)",
            "2:0|Unused import abc (W0611, unused-import)",
            "7:0|Too many branches (20/12) (R0912, too-many-branches)",
            "1:0|Similar lines in 2 files\n"
            "==duplicate1:[5:10]\n"
            "==duplicate2:[5:10]\n"
            "    aaa = 0\n"
            "    bbb = 1\n"
            "    ccc = 2\n"
            "    ddd = 3\n"
            "    print(aaa + bbb + ccc + ddd) (R0801, duplicate-code)",
        )
        self._assert_results(expected, "pylint")

    def test_rest_radon_cc(self):
        """Check radon cc ouput."""
        expected = "7:0|high_complexity - D"
        self._assert_result(expected, "radon cc")

    def test_black(self):
        """Check black --check ouput."""
        expected = "None:None|would reformat"
        self._assert_result(expected, "black")