diff --git a/requirements-dev.txt b/requirements-dev.txt index 8e5192c..1e7408b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,4 +4,5 @@ pytest>=2.9.2 pytest-cache>=1.0 pytest-cov>=2.3.0 pytest-flake8>=0.6 +pytest-mock>=1.6.0 sphinx>=1.6,<1.7 diff --git a/tests/common.py b/tests/unit/open_mock.py similarity index 100% rename from tests/common.py rename to tests/unit/open_mock.py diff --git a/tests/unit/test_checker.py b/tests/unit/test_checker.py deleted file mode 100644 index 89dc514..0000000 --- a/tests/unit/test_checker.py +++ /dev/null @@ -1,307 +0,0 @@ -import pytest - -from wscheck.checker import WhitespaceChecker, RULES -from tests.common import patch_open_read - -MOCKED_FILE_PATH = '/foo/bar' - - -def assert_check_file(checker, file_content, expected_issues): - """ - :type checker: wscheck.checker.WhitespaceChecker - :type file_content: str - :type expected_issues: list - """ - __tracebackhide__ = True # noqa - - mocked_files = {MOCKED_FILE_PATH: file_content} - - with patch_open_read(mocked_files): - checker.check_file(MOCKED_FILE_PATH) - - assert expected_issues == checker.issues - - -@pytest.fixture -def checker(): - return WhitespaceChecker() - - -class TestEof(object): - def test_one_empty_line_is_good(self, checker): - assert_check_file( - checker, - file_content='', - expected_issues=[] - ) - - def test_two_empty_lines_is_bad(self, checker): - assert_check_file( - checker, - file_content='\n', - expected_issues=[ - { - 'rule': 'WSC006', 'path': MOCKED_FILE_PATH, 'line': 1, 'col': 1, - 'context': '', 'message_suffix': '(+1)' - }, - ] - ) - - def test_three_empty_lines_is_bad(self, checker): - assert_check_file( - checker, - file_content='\n\n', - expected_issues=[ - { - 'rule': 'WSC006', 'path': MOCKED_FILE_PATH, 'line': 1, 'col': 1, - 'context': '', 'message_suffix': '(+2)' - }, - ] - ) - - def test_one_non_empty_line_wo_lf_is_bad(self, checker): - assert_check_file( - checker, - file_content='apple', - expected_issues=[ - { - 'rule': 'WSC005', 'path': MOCKED_FILE_PATH, 'line': 1, 'col': 6, - 'context': 'apple', 'message_suffix': None - }, - ] - ) - - def test_one_non_empty_line_w_lf_is_good(self, checker): - assert_check_file( - checker, - file_content='apple\n', - expected_issues=[] - ) - - def test_two_non_empty_lines_but_missing_lf_after_last_is_bad(self, checker): - assert_check_file( - checker, - file_content='apple\norange', - expected_issues=[ - { - 'rule': 'WSC005', 'path': MOCKED_FILE_PATH, 'line': 2, 'col': 7, - 'context': 'orange', 'message_suffix': None - }, - ] - ) - - def test_new_lines_at_top_is_good(self, checker): - assert_check_file( - checker, - file_content='\n\n\napple\n', - expected_issues=[] - ) - - -class TestLines(object): - def test_lf_is_good_eol(self, checker): - assert_check_file( - checker, - file_content='orange\nbanana\n', - expected_issues=[] - ) - - def test_cr_is_bad_eol(self, checker): - assert_check_file( - checker, - file_content='apple\rorange\r', - expected_issues=[ - { - 'rule': 'WSC001', 'path': MOCKED_FILE_PATH, 'line': 1, 'col': 6, - 'context': 'apple', 'message_suffix': '\'\\r\'' - }, - { - 'rule': 'WSC001', 'path': MOCKED_FILE_PATH, 'line': 2, 'col': 7, - 'context': 'orange', 'message_suffix': '\'\\r\'' - }, - ] - ) - - def test_crlf_is_bad_eol(self, checker): - assert_check_file( - checker, - file_content='apple\r\norange\r\n', - expected_issues=[ - { - 'rule': 'WSC001', 'path': MOCKED_FILE_PATH, 'line': 1, 'col': 6, - 'context': 'apple', 'message_suffix': '\'\\r\\n\'' - }, - { - 'rule': 'WSC001', 'path': MOCKED_FILE_PATH, 'line': 2, 'col': 7, - 'context': 'orange', 'message_suffix': '\'\\r\\n\'' - }, - ] - ) - - @pytest.mark.parametrize('content', [ - 'apple banana', - 'apple banana', - 'apple\tbanana', - 'apple\t\tbanana', - 'apple \t banana', - ]) - def test_infix_whitespace_is_ok(self, checker, content): - assert_check_file( - checker, - file_content='{}\n'.format(content), - expected_issues=[] - ) - - @pytest.mark.parametrize('content', [ - 'kiwi ', - 'kiwi ', - 'kiwi\t', - 'kiwi\t\t', - 'kiwi \t ', - ]) - def test_suffix_space_is_bad(self, checker, content): - assert_check_file( - checker, - file_content='{}\n'.format(content), - expected_issues=[ - { - 'rule': 'WSC002', 'path': MOCKED_FILE_PATH, 'line': 1, 'col': 5, - 'context': content, 'message_suffix': None - }, - ] - ) - - @pytest.mark.parametrize('content', [ - 'berry', - ' berry', - ' berry', - ' berry', - ]) - def test_even_indentation_is_good(self, checker, content): - assert_check_file( - checker, - file_content='{}\n'.format(content), - expected_issues=[] - ) - - @pytest.mark.parametrize('content,col', [ - (' berry', 2), # noqa: E241 - (' berry', 4), # noqa: E241 - (' berry', 6), # noqa: E241 - (' berry', 8), # noqa: E241 - ]) - def test_odd_indentation_is_bad(self, checker, content, col): - assert_check_file( - checker, - file_content='{}\n'.format(content), - expected_issues=[ - { - 'rule': 'WSC003', 'path': MOCKED_FILE_PATH, 'line': 1, 'col': col, - 'context': content, 'message_suffix': None - }, - ] - ) - - @pytest.mark.parametrize('content,col', [ - ('\tpeach', 1), # noqa: E241 - (' \tpeach', 3), # noqa: E241 - (' \t peach', 2), # noqa: E241 - (' \t\t peach', 2), # noqa: E241 - ]) - def test_non_spaces_in_indentation_is_bad(self, checker, content, col): - assert_check_file( - checker, - file_content='{}\n'.format(content), - expected_issues=[ - { - 'rule': 'WSC004', 'path': MOCKED_FILE_PATH, 'line': 1, 'col': col, - 'context': content, 'message_suffix': None - }, - ] - ) - - -class TestEmptyLines(object): - # def test_lf_is_good_eol(self, checker): - # assert_check_file( - # checker, - # file_content='orange\nbanana\n', - # expected_issues=[] - # ) - - # def test_too_many_empty_lines(self, checker): - # assert_check_file( - # checker, - # file_content='apple\n\n\norange', - # expected_issues=[ - # { - # 'rule': 'WSW007', 'path': MOCKED_FILE_PATH, 'line': 2, 'col': 1, - # 'context': 'apple', 'message_suffix': '\'\\r\'' - # }, - # ] - # ) - pass - - -class TestComplexCases(object): - def test_multiple_issues(self, checker): - assert_check_file( - checker, - file_content=' \tpineapple \rbanana', - expected_issues=[ - { - 'rule': 'WSC001', 'path': MOCKED_FILE_PATH, 'line': 1, 'col': 13, - 'context': ' \tpineapple ', 'message_suffix': '\'\\r\'' - }, - { - 'rule': 'WSC002', 'path': MOCKED_FILE_PATH, 'line': 1, 'col': 12, - 'context': ' \tpineapple ', 'message_suffix': None - }, - { - 'rule': 'WSC003', 'path': MOCKED_FILE_PATH, 'line': 1, 'col': 3, - 'context': ' \tpineapple ', 'message_suffix': None - }, - { - 'rule': 'WSC004', 'path': MOCKED_FILE_PATH, 'line': 1, 'col': 2, - 'context': ' \tpineapple ', 'message_suffix': None - }, - { - 'rule': 'WSC005', 'path': MOCKED_FILE_PATH, 'line': 2, 'col': 7, - 'context': 'banana', 'message_suffix': None - }, - ] - ) - - -class TestExcludingRules(object): - def test_add_one_exclusion_for_one_issue_type(self): - assert_check_file( - checker=WhitespaceChecker(excluded_rules=['WSC001']), - file_content='apple\r', - expected_issues=[] - ) - - def test_add_one_exclusion_for_two_issue_types(self): - assert_check_file( - checker=WhitespaceChecker(excluded_rules=['WSC001']), - file_content='\tapple\r', - expected_issues=[ - { - 'rule': 'WSC004', 'path': MOCKED_FILE_PATH, 'line': 1, 'col': 1, - 'context': '\tapple', 'message_suffix': None - }, - ] - ) - - def test_add_two_exclusions_for_one_issue_types(self): - assert_check_file( - checker=WhitespaceChecker(excluded_rules=['WSC001', 'WSC004']), - file_content='apple\r', - expected_issues=[] - ) - - def test_exclude_all_rules_makes_error(self): - with pytest.raises(RuntimeError) as e: - WhitespaceChecker(excluded_rules=list(RULES)) - - assert 'No rules to check' in str(e) diff --git a/tests/unit/test_checker_check_file.py b/tests/unit/test_checker_check_file.py new file mode 100644 index 0000000..9e2d7c2 --- /dev/null +++ b/tests/unit/test_checker_check_file.py @@ -0,0 +1,25 @@ +import pytest + +from wscheck.checker import WhitespaceChecker +from tests.unit.open_mock import patch_open_read + + +@pytest.fixture +def checker(mocker): + instance = WhitespaceChecker() + mocker.patch.object(instance, 'check_text', auto_spec=True) + + return instance + + +@pytest.mark.parametrize('file_path,file_content', [ + ('/empty/file', ''), + ('/contains/anything', ' foo\t\r\nbar '), +]) +def test_one_empty_line_is_good(checker, file_path, file_content): + mocked_files = {file_path: file_content} + + with patch_open_read(mocked_files): + checker.check_file(file_path) + + checker.check_text.assert_called_once_with(file_content, source_path=file_path) diff --git a/tests/unit/test_checker_check_text.py b/tests/unit/test_checker_check_text.py new file mode 100644 index 0000000..8aa8452 --- /dev/null +++ b/tests/unit/test_checker_check_text.py @@ -0,0 +1,243 @@ +import pytest + +from wscheck.checker import WhitespaceChecker, RULES + + +@pytest.fixture +def checker(): + return WhitespaceChecker() + + +class TestEof(object): + def test_one_empty_line_is_good(self, checker): + checker.check_text('') + assert [] == checker.issues + + def test_two_empty_lines_is_bad(self, checker): + checker.check_text('\n') + assert [ + { + 'rule': 'WSC006', 'path': '', 'line': 1, 'col': 1, + 'context': '', 'message_suffix': '(+1)' + }, + ] == checker.issues + + def test_three_empty_lines_is_bad(self, checker): + checker.check_text('\n\n') + assert [ + { + 'rule': 'WSC006', 'path': '', 'line': 1, 'col': 1, + 'context': '', 'message_suffix': '(+2)' + }, + ] == checker.issues + + def test_one_non_empty_line_wo_lf_is_bad(self, checker): + checker.check_text('apple') + assert [ + { + 'rule': 'WSC005', 'path': '', 'line': 1, 'col': 6, + 'context': 'apple', 'message_suffix': None + }, + ] == checker.issues + + def test_one_non_empty_line_w_lf_is_good(self, checker): + checker.check_text('apple\n') + assert [] == checker.issues + + def test_two_non_empty_lines_but_missing_lf_after_last_is_bad(self, checker): + checker.check_text('apple\norange') + assert [ + { + 'rule': 'WSC005', 'path': '', 'line': 2, 'col': 7, + 'context': 'orange', 'message_suffix': None + }, + ] == checker.issues + + def test_new_lines_at_top_is_good(self, checker): + checker.check_text('\n\n\napple\n') + assert [] == checker.issues + + +class TestLines(object): + def test_lf_is_good_eol(self, checker): + checker.check_text('orange\nbanana\n') + assert [] == checker.issues + + def test_cr_is_bad_eol(self, checker): + checker.check_text('apple\rorange\r') + assert [ + { + 'rule': 'WSC001', 'path': '', 'line': 1, 'col': 6, + 'context': 'apple', 'message_suffix': '\'\\r\'' + }, + { + 'rule': 'WSC001', 'path': '', 'line': 2, 'col': 7, + 'context': 'orange', 'message_suffix': '\'\\r\'' + }, + ] == checker.issues + + def test_crlf_is_bad_eol(self, checker): + checker.check_text('apple\r\norange\r\n') + assert [ + { + 'rule': 'WSC001', 'path': '', 'line': 1, 'col': 6, + 'context': 'apple', 'message_suffix': '\'\\r\\n\'' + }, + { + 'rule': 'WSC001', 'path': '', 'line': 2, 'col': 7, + 'context': 'orange', 'message_suffix': '\'\\r\\n\'' + }, + ] == checker.issues + + @pytest.mark.parametrize('content', [ + 'apple banana', + 'apple banana', + 'apple\tbanana', + 'apple\t\tbanana', + 'apple \t banana', + ]) + def test_infix_whitespace_is_ok(self, checker, content): + checker.check_text('{}\n'.format(content)) + assert [] == checker.issues + + @pytest.mark.parametrize('content', [ + 'kiwi ', + 'kiwi ', + 'kiwi\t', + 'kiwi\t\t', + 'kiwi \t ', + ]) + def test_suffix_space_is_bad(self, checker, content): + checker.check_text('{}\n'.format(content)) + assert [ + { + 'rule': 'WSC002', 'path': '', 'line': 1, 'col': 5, + 'context': content, 'message_suffix': None + }, + ] == checker.issues + + @pytest.mark.parametrize('content', [ + 'berry', + ' berry', + ' berry', + ' berry', + ]) + def test_even_indentation_is_good(self, checker, content): + checker.check_text('{}\n'.format(content)) + assert [] == checker.issues + + @pytest.mark.parametrize('content,col', [ + (' berry', 2), # noqa: E241 + (' berry', 4), # noqa: E241 + (' berry', 6), # noqa: E241 + (' berry', 8), # noqa: E241 + ]) + def test_odd_indentation_is_bad(self, checker, content, col): + checker.check_text('{}\n'.format(content)) + assert [ + { + 'rule': 'WSC003', 'path': '', 'line': 1, 'col': col, + 'context': content, 'message_suffix': None + }, + ] == checker.issues + + @pytest.mark.parametrize('content,col', [ + ('\tpeach', 1), # noqa: E241 + (' \tpeach', 3), # noqa: E241 + (' \t peach', 2), # noqa: E241 + (' \t\t peach', 2), # noqa: E241 + ]) + def test_non_spaces_in_indentation_is_bad(self, checker, content, col): + checker.check_text('{}\n'.format(content)) + assert [ + { + 'rule': 'WSC004', 'path': '', 'line': 1, 'col': col, + 'context': content, 'message_suffix': None + }, + ] == checker.issues + + +class TestComplexCases(object): + def test_multiple_issues(self, checker): + checker.check_text(' \tpineapple \rbanana') + assert [ + { + 'rule': 'WSC001', 'path': '', 'line': 1, 'col': 13, + 'context': ' \tpineapple ', 'message_suffix': '\'\\r\'' + }, + { + 'rule': 'WSC002', 'path': '', 'line': 1, 'col': 12, + 'context': ' \tpineapple ', 'message_suffix': None + }, + { + 'rule': 'WSC003', 'path': '', 'line': 1, 'col': 3, + 'context': ' \tpineapple ', 'message_suffix': None + }, + { + 'rule': 'WSC004', 'path': '', 'line': 1, 'col': 2, + 'context': ' \tpineapple ', 'message_suffix': None + }, + { + 'rule': 'WSC005', 'path': '', 'line': 2, 'col': 7, + 'context': 'banana', 'message_suffix': None + }, + ] == checker.issues + + +class TestExcludingRules(object): + def test_add_one_exclusion_for_one_issue_type(self): + checker = WhitespaceChecker(excluded_rules=['WSC001']) + checker.check_text('apple\r') + assert [] == checker.issues + + def test_add_one_exclusion_for_two_issue_types(self): + checker = WhitespaceChecker(excluded_rules=['WSC001']) + checker.check_text('\tapple\r') + assert [ + { + 'rule': 'WSC004', 'path': '', 'line': 1, 'col': 1, + 'context': '\tapple', 'message_suffix': None + }, + ] == checker.issues + + def test_add_two_exclusions_for_one_issue_types(self): + checker = WhitespaceChecker(excluded_rules=['WSC001', 'WSC004']) + checker.check_text('apple\r') + assert [] == checker.issues + + def test_exclude_all_rules_makes_error(self): + with pytest.raises(RuntimeError) as e: + WhitespaceChecker(excluded_rules=list(RULES)) + + assert 'No rules to check' in str(e) + + +class AlwaysFailChecker(WhitespaceChecker): + def __init__(self): + super(AlwaysFailChecker, self).__init__() + self._checkers = [self._check_for_fail] + + def _check_for_fail(self, source_path, lines): + self._add_issue(rule='WSC000', path=source_path, line=1, col=1, context='') + + +class TestSourcePath(object): + def test_without_specified_source(self): + fail_checker = AlwaysFailChecker() + fail_checker.check_text('') + + assert [ + {'rule': 'WSC000', 'path': '', 'line': 1, 'col': 1, 'context': '', 'message_suffix': None}, + ] == fail_checker.issues + + @pytest.mark.parametrize('source_path', [ + ('/empty/file', ''), + ('/contains/anything', ' foo\t\r\nbar '), + ]) + def test_with_source(self, source_path): + fail_checker = AlwaysFailChecker() + fail_checker.check_text('', source_path) + + assert [ + {'rule': 'WSC000', 'path': source_path, 'line': 1, 'col': 1, 'context': '', 'message_suffix': None}, + ] == fail_checker.issues diff --git a/wscheck/checker.py b/wscheck/checker.py index e1eb533..6d175cd 100644 --- a/wscheck/checker.py +++ b/wscheck/checker.py @@ -48,18 +48,26 @@ def check_file(self, file_path): """ :type file_path: str """ - lines = self._read_file_lines_w_eol(file_path) + with open(file_path) as fd: + file_content = fd.read() + + self.check_text(file_content, source_path=file_path) + + def check_text(self, text, source_path=''): + """ + :type text: str + :type source_path: str + """ + lines = self._read_text_lines_w_eol(text) for checker in self._checkers: - checker(file_path, lines) + checker(source_path, lines) - def _read_file_lines_w_eol(self, file_path): + def _read_text_lines_w_eol(self, text): """ - :type file_path: str + :type text: str :rtype: list """ - with open(file_path) as fd: - file_content = fd.read() - lines = self._LINE_TEMPLATE.findall(file_content) + lines = self._LINE_TEMPLATE.findall(text) # Workaround: can not match end of string in multi line regexp if len(lines) > 1 and lines[-2][1] == '': @@ -67,9 +75,9 @@ def _read_file_lines_w_eol(self, file_path): return lines - def _check_by_lines(self, file_path, lines): + def _check_by_lines(self, source_path, lines): """ - :type file_path: str + :type source_path: str :type lines: list """ for line, line_text_eol in enumerate(lines, start=1): @@ -77,13 +85,13 @@ def _check_by_lines(self, file_path, lines): if 'WSC001' in self._rules: if not line_eol == '' and not line_eol == '\n': - self._add_issue(rule='WSC001', path=file_path, line=line, col=len(line_text) + 1, context=line_text, + self._add_issue(rule='WSC001', path=source_path, line=line, col=len(line_text) + 1, context=line_text, message_suffix='{!r}'.format(line_eol)) if 'WSC002' in self._rules: tailing_whitespace_match = self._TAILING_WHITESPACE_TEMPLATE.search(line_text) if tailing_whitespace_match is not None: - self._add_issue(rule='WSC002', path=file_path, line=line, col=tailing_whitespace_match.start() + 1, context=line_text) + self._add_issue(rule='WSC002', path=source_path, line=line, col=tailing_whitespace_match.start() + 1, context=line_text) if line_text.strip() == '': continue @@ -94,12 +102,12 @@ def _check_by_lines(self, file_path, lines): if 'WSC003' in self._rules: if not len(line_indent.replace('\t', ' ')) % 2 == 0: - self._add_issue(rule='WSC003', path=file_path, line=line, col=len(line_indent) + 1, context=line_text) + self._add_issue(rule='WSC003', path=source_path, line=line, col=len(line_indent) + 1, context=line_text) if 'WSC004' in self._rules: character_match = self._NOT_SPACES_TEMPLATE.search(line_indent) if character_match is not None: - self._add_issue(rule='WSC004', path=file_path, line=line, col=character_match.start() + 1, + self._add_issue(rule='WSC004', path=source_path, line=line, col=character_match.start() + 1, context=line_text) def _add_issue(self, rule, path, line, col, context, message_suffix=None): @@ -114,9 +122,9 @@ def _add_issue(self, rule, path, line, col, context, message_suffix=None): self._issues.append({'rule': rule, 'path': path, 'line': line, 'col': col, 'context': context, 'message_suffix': message_suffix}) - def _check_eof(self, file_path, lines): + def _check_eof(self, source_path, lines): """ - :type file_path: str + :type source_path: str :type lines: list """ if lines == [('', '')]: @@ -131,11 +139,11 @@ def _check_eof(self, file_path, lines): if 'WSC005' in self._rules: if empty_lines == 0: line_text = lines[-1][0] - self._add_issue(rule='WSC005', path=file_path, line=len(lines), col=len(line_text) + 1, context=line_text) + self._add_issue(rule='WSC005', path=source_path, line=len(lines), col=len(line_text) + 1, context=line_text) if 'WSC006' in self._rules: if empty_lines > 1: shift = min(len(lines), empty_lines + 1) line_text = lines[-shift][0] - self._add_issue(rule='WSC006', path=file_path, line=len(lines) - shift + 1, col=len(line_text) + 1, + self._add_issue(rule='WSC006', path=source_path, line=len(lines) - shift + 1, col=len(line_text) + 1, context=line_text, message_suffix='(+{})'.format(empty_lines - 1))