summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/conftest.py6
-rw-r--r--tests/fixtures/config_files/README.rst38
-rw-r--r--tests/fixtures/config_files/broken.ini9
-rw-r--r--tests/fixtures/config_files/cli-specified-with-inline-comments.ini16
-rw-r--r--tests/fixtures/config_files/cli-specified-without-inline-comments.ini16
-rw-r--r--tests/fixtures/config_files/cli-specified.ini9
-rw-r--r--tests/fixtures/config_files/local-config.ini3
-rw-r--r--tests/fixtures/config_files/no-flake8-section.ini20
-rw-r--r--tests/fixtures/config_files/user-config.ini5
-rw-r--r--tests/fixtures/diffs/multi_file_diff130
-rw-r--r--tests/fixtures/diffs/single_file_diff27
-rw-r--r--tests/fixtures/diffs/two_file_diff45
-rw-r--r--tests/fixtures/example-code/inline-ignores/E501.py3
-rw-r--r--tests/fixtures/example-code/inline-ignores/E731.py1
-rw-r--r--tests/integration/test_aggregator.py48
-rw-r--r--tests/unit/test_base_formatter.py143
-rw-r--r--tests/unit/test_checker_manager.py59
-rw-r--r--tests/unit/test_config_file_finder.py126
-rw-r--r--tests/unit/test_file_processor.py293
-rw-r--r--tests/unit/test_merged_config_parser.py206
-rw-r--r--tests/unit/test_notifier.py54
-rw-r--r--tests/unit/test_option.py68
-rw-r--r--tests/unit/test_option_manager.py194
-rw-r--r--tests/unit/test_plugin.py153
-rw-r--r--tests/unit/test_plugin_manager.py50
-rw-r--r--tests/unit/test_plugin_type_manager.py228
-rw-r--r--tests/unit/test_style_guide.py207
-rw-r--r--tests/unit/test_trie.py122
-rw-r--r--tests/unit/test_utils.py150
29 files changed, 2429 insertions, 0 deletions
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..9bf4f95
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,6 @@
+"""Test configuration for py.test."""
+import sys
+
+import flake8
+
+flake8.configure_logging(2, 'test-logs-%s.%s.log' % sys.version_info[0:2])
diff --git a/tests/fixtures/config_files/README.rst b/tests/fixtures/config_files/README.rst
new file mode 100644
index 0000000..b00adad
--- /dev/null
+++ b/tests/fixtures/config_files/README.rst
@@ -0,0 +1,38 @@
+About this directory
+====================
+
+The files in this directory are test fixtures for unit and integration tests.
+Their purpose is described below. Please note the list of file names that can
+not be created as they are already used by tests.
+
+New fixtures are preferred over updating existing features unless existing
+tests will fail.
+
+Files that should not be created
+--------------------------------
+
+- ``tests/fixtures/config_files/missing.ini``
+
+Purposes of existing fixtures
+-----------------------------
+
+``tests/fixtures/config_files/cli-specified.ini``
+
+ This should only be used when providing config file(s) specified by the
+ user on the command-line.
+
+``tests/fixtures/config_files/local-config.ini``
+
+ This should be used when providing config files that would have been found
+ by looking for config files in the current working project directory.
+
+
+``tests/fixtures/config_files/no-flake8-section.ini``
+
+ This should be used when parsing an ini file without a ``[flake8]``
+ section.
+
+``tests/fixtures/config_files/user-config.ini``
+
+ This is an example configuration file that would be found in the user's
+ home directory (or XDG Configuration Directory).
diff --git a/tests/fixtures/config_files/broken.ini b/tests/fixtures/config_files/broken.ini
new file mode 100644
index 0000000..33986ae
--- /dev/null
+++ b/tests/fixtures/config_files/broken.ini
@@ -0,0 +1,9 @@
+[flake8]
+exclude =
+<<<<<<< 642f88cb1b6027e184d9a662b255f7fea4d9eacc
+ tests/fixtures/,
+=======
+ tests/,
+>>>>>>> HEAD
+ docs/
+ignore = D203
diff --git a/tests/fixtures/config_files/cli-specified-with-inline-comments.ini b/tests/fixtures/config_files/cli-specified-with-inline-comments.ini
new file mode 100644
index 0000000..4d57e85
--- /dev/null
+++ b/tests/fixtures/config_files/cli-specified-with-inline-comments.ini
@@ -0,0 +1,16 @@
+[flake8]
+# This is a flake8 config, there are many like it, but this is mine
+ignore =
+ # Disable E123
+ E123,
+ # Disable W234
+ W234,
+ # Also disable E111
+ E111
+exclude =
+ # Exclude foo/
+ foo/,
+ # Exclude bar/ while we're at it
+ bar/,
+ # Exclude bogus/
+ bogus/
diff --git a/tests/fixtures/config_files/cli-specified-without-inline-comments.ini b/tests/fixtures/config_files/cli-specified-without-inline-comments.ini
new file mode 100644
index 0000000..f50ba75
--- /dev/null
+++ b/tests/fixtures/config_files/cli-specified-without-inline-comments.ini
@@ -0,0 +1,16 @@
+[flake8]
+# This is a flake8 config, there are many like it, but this is mine
+# Disable E123
+# Disable W234
+# Also disable E111
+ignore =
+ E123,
+ W234,
+ E111
+# Exclude foo/
+# Exclude bar/ while we're at it
+# Exclude bogus/
+exclude =
+ foo/,
+ bar/,
+ bogus/
diff --git a/tests/fixtures/config_files/cli-specified.ini b/tests/fixtures/config_files/cli-specified.ini
new file mode 100644
index 0000000..753604a
--- /dev/null
+++ b/tests/fixtures/config_files/cli-specified.ini
@@ -0,0 +1,9 @@
+[flake8]
+ignore =
+ E123,
+ W234,
+ E111
+exclude =
+ foo/,
+ bar/,
+ bogus/
diff --git a/tests/fixtures/config_files/local-config.ini b/tests/fixtures/config_files/local-config.ini
new file mode 100644
index 0000000..348751a
--- /dev/null
+++ b/tests/fixtures/config_files/local-config.ini
@@ -0,0 +1,3 @@
+[flake8]
+exclude = docs/
+select = E,W,F
diff --git a/tests/fixtures/config_files/no-flake8-section.ini b/tests/fixtures/config_files/no-flake8-section.ini
new file mode 100644
index 0000000..a85b709
--- /dev/null
+++ b/tests/fixtures/config_files/no-flake8-section.ini
@@ -0,0 +1,20 @@
+[tox]
+minversion=2.3.1
+envlist = py26,py27,py32,py33,py34,py35,flake8
+
+[testenv]
+deps =
+ mock
+ pytest
+commands =
+ py.test {posargs}
+
+[testenv:flake8]
+skipsdist = true
+skip_install = true
+use_develop = false
+deps =
+ flake8
+ flake8-docstrings
+commands =
+ flake8
diff --git a/tests/fixtures/config_files/user-config.ini b/tests/fixtures/config_files/user-config.ini
new file mode 100644
index 0000000..b06c24f
--- /dev/null
+++ b/tests/fixtures/config_files/user-config.ini
@@ -0,0 +1,5 @@
+[flake8]
+exclude =
+ tests/fixtures/,
+ docs/
+ignore = D203
diff --git a/tests/fixtures/diffs/multi_file_diff b/tests/fixtures/diffs/multi_file_diff
new file mode 100644
index 0000000..de86209
--- /dev/null
+++ b/tests/fixtures/diffs/multi_file_diff
@@ -0,0 +1,130 @@
+diff --git a/flake8/utils.py b/flake8/utils.py
+index f6ce384..7cd12b0 100644
+--- a/flake8/utils.py
++++ b/flake8/utils.py
+@@ -75,8 +75,8 @@ def stdin_get_value():
+ return cached_value.getvalue()
+
+
+-def parse_unified_diff():
+- # type: () -> List[str]
++def parse_unified_diff(diff=None):
++ # type: (str) -> List[str]
+ """Parse the unified diff passed on stdin.
+
+ :returns:
+@@ -84,7 +84,10 @@ def parse_unified_diff():
+ :rtype:
+ dict
+ """
+- diff = stdin_get_value()
++ # Allow us to not have to patch out stdin_get_value
++ if diff is None:
++ diff = stdin_get_value()
++
+ number_of_rows = None
+ current_path = None
+ parsed_paths = collections.defaultdict(set)
+diff --git a/tests/fixtures/diffs/single_file_diff b/tests/fixtures/diffs/single_file_diff
+new file mode 100644
+index 0000000..77ca534
+--- /dev/null
++++ b/tests/fixtures/diffs/single_file_diff
+@@ -0,0 +1,27 @@
++diff --git a/flake8/utils.py b/flake8/utils.py
++index f6ce384..7cd12b0 100644
++--- a/flake8/utils.py
+++++ b/flake8/utils.py
++@@ -75,8 +75,8 @@ def stdin_get_value():
++ return cached_value.getvalue()
++
++
++-def parse_unified_diff():
++- # type: () -> List[str]
+++def parse_unified_diff(diff=None):
+++ # type: (str) -> List[str]
++ """Parse the unified diff passed on stdin.
++
++ :returns:
++@@ -84,7 +84,10 @@ def parse_unified_diff():
++ :rtype:
++ dict
++ """
++- diff = stdin_get_value()
+++ # Allow us to not have to patch out stdin_get_value
+++ if diff is None:
+++ diff = stdin_get_value()
+++
++ number_of_rows = None
++ current_path = None
++ parsed_paths = collections.defaultdict(set)
+diff --git a/tests/fixtures/diffs/two_file_diff b/tests/fixtures/diffs/two_file_diff
+new file mode 100644
+index 0000000..5bd35cd
+--- /dev/null
++++ b/tests/fixtures/diffs/two_file_diff
+@@ -0,0 +1,45 @@
++diff --git a/flake8/utils.py b/flake8/utils.py
++index f6ce384..7cd12b0 100644
++--- a/flake8/utils.py
+++++ b/flake8/utils.py
++@@ -75,8 +75,8 @@ def stdin_get_value():
++ return cached_value.getvalue()
++
++
++-def parse_unified_diff():
++- # type: () -> List[str]
+++def parse_unified_diff(diff=None):
+++ # type: (str) -> List[str]
++ """Parse the unified diff passed on stdin.
++
++ :returns:
++@@ -84,7 +84,10 @@ def parse_unified_diff():
++ :rtype:
++ dict
++ """
++- diff = stdin_get_value()
+++ # Allow us to not have to patch out stdin_get_value
+++ if diff is None:
+++ diff = stdin_get_value()
+++
++ number_of_rows = None
++ current_path = None
++ parsed_paths = collections.defaultdict(set)
++diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
++index d69d939..21482ce 100644
++--- a/tests/unit/test_utils.py
+++++ b/tests/unit/test_utils.py
++@@ -115,3 +115,13 @@ def test_parameters_for_function_plugin():
++ plugin = plugin_manager.Plugin('plugin-name', object())
++ plugin._plugin = fake_plugin
++ assert utils.parameters_for(plugin) == ['physical_line', 'self', 'tree']
+++
+++
+++def read_diff_file(filename):
+++ """Read the diff file in its entirety."""
+++ with open(filename, 'r') as fd:
+++ content = fd.read()
+++ return content
+++
+++
+++SINGLE_FILE_DIFF = read_diff_file('tests/fixtures/diffs/single_file_diff')
+diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
+index d69d939..1461369 100644
+--- a/tests/unit/test_utils.py
++++ b/tests/unit/test_utils.py
+@@ -115,3 +115,14 @@ def test_parameters_for_function_plugin():
+ plugin = plugin_manager.Plugin('plugin-name', object())
+ plugin._plugin = fake_plugin
+ assert utils.parameters_for(plugin) == ['physical_line', 'self', 'tree']
++
++
++def read_diff_file(filename):
++ """Read the diff file in its entirety."""
++ with open(filename, 'r') as fd:
++ content = fd.read()
++ return content
++
++
++SINGLE_FILE_DIFF = read_diff_file('tests/fixtures/diffs/single_file_diff')
++TWO_FILE_DIFF = read_diff_file('tests/fixtures/diffs/two_file_diff')
diff --git a/tests/fixtures/diffs/single_file_diff b/tests/fixtures/diffs/single_file_diff
new file mode 100644
index 0000000..77ca534
--- /dev/null
+++ b/tests/fixtures/diffs/single_file_diff
@@ -0,0 +1,27 @@
+diff --git a/flake8/utils.py b/flake8/utils.py
+index f6ce384..7cd12b0 100644
+--- a/flake8/utils.py
++++ b/flake8/utils.py
+@@ -75,8 +75,8 @@ def stdin_get_value():
+ return cached_value.getvalue()
+
+
+-def parse_unified_diff():
+- # type: () -> List[str]
++def parse_unified_diff(diff=None):
++ # type: (str) -> List[str]
+ """Parse the unified diff passed on stdin.
+
+ :returns:
+@@ -84,7 +84,10 @@ def parse_unified_diff():
+ :rtype:
+ dict
+ """
+- diff = stdin_get_value()
++ # Allow us to not have to patch out stdin_get_value
++ if diff is None:
++ diff = stdin_get_value()
++
+ number_of_rows = None
+ current_path = None
+ parsed_paths = collections.defaultdict(set)
diff --git a/tests/fixtures/diffs/two_file_diff b/tests/fixtures/diffs/two_file_diff
new file mode 100644
index 0000000..5bd35cd
--- /dev/null
+++ b/tests/fixtures/diffs/two_file_diff
@@ -0,0 +1,45 @@
+diff --git a/flake8/utils.py b/flake8/utils.py
+index f6ce384..7cd12b0 100644
+--- a/flake8/utils.py
++++ b/flake8/utils.py
+@@ -75,8 +75,8 @@ def stdin_get_value():
+ return cached_value.getvalue()
+
+
+-def parse_unified_diff():
+- # type: () -> List[str]
++def parse_unified_diff(diff=None):
++ # type: (str) -> List[str]
+ """Parse the unified diff passed on stdin.
+
+ :returns:
+@@ -84,7 +84,10 @@ def parse_unified_diff():
+ :rtype:
+ dict
+ """
+- diff = stdin_get_value()
++ # Allow us to not have to patch out stdin_get_value
++ if diff is None:
++ diff = stdin_get_value()
++
+ number_of_rows = None
+ current_path = None
+ parsed_paths = collections.defaultdict(set)
+diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
+index d69d939..21482ce 100644
+--- a/tests/unit/test_utils.py
++++ b/tests/unit/test_utils.py
+@@ -115,3 +115,13 @@ def test_parameters_for_function_plugin():
+ plugin = plugin_manager.Plugin('plugin-name', object())
+ plugin._plugin = fake_plugin
+ assert utils.parameters_for(plugin) == ['physical_line', 'self', 'tree']
++
++
++def read_diff_file(filename):
++ """Read the diff file in its entirety."""
++ with open(filename, 'r') as fd:
++ content = fd.read()
++ return content
++
++
++SINGLE_FILE_DIFF = read_diff_file('tests/fixtures/diffs/single_file_diff')
diff --git a/tests/fixtures/example-code/inline-ignores/E501.py b/tests/fixtures/example-code/inline-ignores/E501.py
new file mode 100644
index 0000000..62e5c0c
--- /dev/null
+++ b/tests/fixtures/example-code/inline-ignores/E501.py
@@ -0,0 +1,3 @@
+from some.module.that.has.nested.sub.modules import ClassWithVeryVeryVeryVeryLongName # noqa: E501,F401
+
+# ClassWithVeryVeryVeryVeryLongName()
diff --git a/tests/fixtures/example-code/inline-ignores/E731.py b/tests/fixtures/example-code/inline-ignores/E731.py
new file mode 100644
index 0000000..866c79e
--- /dev/null
+++ b/tests/fixtures/example-code/inline-ignores/E731.py
@@ -0,0 +1 @@
+example = lambda: 'example' # noqa: E731
diff --git a/tests/integration/test_aggregator.py b/tests/integration/test_aggregator.py
new file mode 100644
index 0000000..929bdbf
--- /dev/null
+++ b/tests/integration/test_aggregator.py
@@ -0,0 +1,48 @@
+"""Test aggregation of config files and command-line options."""
+import os
+
+import pytest
+
+from flake8.main import options
+from flake8.options import aggregator
+from flake8.options import manager
+
+CLI_SPECIFIED_CONFIG = 'tests/fixtures/config_files/cli-specified.ini'
+
+
+@pytest.fixture
+def optmanager():
+ """Create a new OptionManager."""
+ option_manager = manager.OptionManager(
+ prog='flake8',
+ version='3.0.0',
+ )
+ options.register_default_options(option_manager)
+ return option_manager
+
+
+def test_aggregate_options_with_config(optmanager):
+ """Verify we aggregate options and config values appropriately."""
+ arguments = ['flake8', '--config', CLI_SPECIFIED_CONFIG, '--select',
+ 'E11,E34,E402,W,F', '--exclude', 'tests/*']
+ options, args = aggregator.aggregate_options(optmanager, arguments)
+
+ assert options.config == CLI_SPECIFIED_CONFIG
+ assert options.select == ['E11', 'E34', 'E402', 'W', 'F']
+ assert options.ignore == ['E123', 'W234', 'E111']
+ assert options.exclude == [os.path.abspath('tests/*')]
+
+
+def test_aggregate_options_when_isolated(optmanager):
+ """Verify we aggregate options and config values appropriately."""
+ arguments = ['flake8', '--isolated', '--select', 'E11,E34,E402,W,F',
+ '--exclude', 'tests/*']
+ optmanager.extend_default_ignore(['E8'])
+ options, args = aggregator.aggregate_options(optmanager, arguments)
+
+ assert options.isolated is True
+ assert options.select == ['E11', 'E34', 'E402', 'W', 'F']
+ assert sorted(options.ignore) == [
+ 'E121', 'E123', 'E126', 'E226', 'E24', 'E704', 'E8', 'W503', 'W504',
+ ]
+ assert options.exclude == [os.path.abspath('tests/*')]
diff --git a/tests/unit/test_base_formatter.py b/tests/unit/test_base_formatter.py
new file mode 100644
index 0000000..dc4a95c
--- /dev/null
+++ b/tests/unit/test_base_formatter.py
@@ -0,0 +1,143 @@
+"""Tests for the BaseFormatter object."""
+import optparse
+
+import mock
+import pytest
+
+from flake8 import style_guide
+from flake8.formatting import base
+
+
+def options(**kwargs):
+ """Create an optparse.Values instance."""
+ kwargs.setdefault('output_file', None)
+ return optparse.Values(kwargs)
+
+
+@pytest.mark.parametrize('filename', [None, 'out.txt'])
+def test_start(filename):
+ """Verify we open a new file in the start method."""
+ mock_open = mock.mock_open()
+ formatter = base.BaseFormatter(options(output_file=filename))
+ with mock.patch('flake8.formatting.base.open', mock_open):
+ formatter.start()
+
+ if filename is None:
+ assert mock_open.called is False
+ else:
+ mock_open.assert_called_once_with(filename, 'w')
+
+
+def test_stop():
+ """Verify we close open file objects."""
+ filemock = mock.Mock()
+ formatter = base.BaseFormatter(options())
+ formatter.output_fd = filemock
+ formatter.stop()
+
+ filemock.close.assert_called_once_with()
+ assert formatter.output_fd is None
+
+
+def test_format_needs_to_be_implemented():
+ """Ensure BaseFormatter#format raises a NotImplementedError."""
+ formatter = base.BaseFormatter(options())
+ with pytest.raises(NotImplementedError):
+ formatter.format('foo')
+
+
+def test_show_source_returns_nothing_when_not_showing_source():
+ """Ensure we return nothing when users want nothing."""
+ formatter = base.BaseFormatter(options(show_source=False))
+ assert formatter.show_source(
+ style_guide.Error('A000', 'file.py', 1, 1, 'error text', 'line')
+ ) is None
+
+
+@pytest.mark.parametrize('line, column', [
+ ('x=1\n', 2),
+ (' x=(1\n +2)\n', 5),
+ # TODO(sigmavirus24): Add more examples
+])
+def test_show_source_updates_physical_line_appropriately(line, column):
+ """Ensure the error column is appropriately indicated."""
+ formatter = base.BaseFormatter(options(show_source=True))
+ error = style_guide.Error('A000', 'file.py', 1, column, 'error', line)
+ output = formatter.show_source(error)
+ _, pointer = output.rsplit('\n', 1)
+ assert pointer.count(' ') == column
+
+
+def test_write_uses_an_output_file():
+ """Verify that we use the output file when it's present."""
+ line = 'Something to write'
+ source = 'source'
+ filemock = mock.Mock()
+
+ formatter = base.BaseFormatter(options())
+ formatter.output_fd = filemock
+ formatter.write(line, source)
+
+ assert filemock.write.called is True
+ assert filemock.write.call_count == 2
+ assert filemock.write.mock_calls == [
+ mock.call(line + formatter.newline),
+ mock.call(source + formatter.newline),
+ ]
+
+
+@mock.patch('flake8.formatting.base.print')
+def test_write_uses_print(print_function):
+ """Verify that we use the print function without an output file."""
+ line = 'Something to write'
+ source = 'source'
+
+ formatter = base.BaseFormatter(options())
+ formatter.write(line, source)
+
+ assert print_function.called is True
+ assert print_function.call_count == 2
+ assert print_function.mock_calls == [
+ mock.call(line),
+ mock.call(source),
+ ]
+
+
+class AfterInitFormatter(base.BaseFormatter):
+ """Subclass for testing after_init."""
+
+ def after_init(self):
+ """Define method to verify operation."""
+ self.post_initialized = True
+
+
+def test_after_init_is_always_called():
+ """Verify after_init is called."""
+ formatter = AfterInitFormatter(options())
+ assert getattr(formatter, 'post_initialized') is True
+
+
+class FormatFormatter(base.BaseFormatter):
+ """Subclass for testing format."""
+
+ def format(self, error):
+ """Define method to verify operation."""
+ return repr(error)
+
+
+def test_handle_formats_the_error():
+ """Verify that a formatter will call format from handle."""
+ formatter = FormatFormatter(options(show_source=False))
+ filemock = formatter.output_fd = mock.Mock()
+ error = style_guide.Error(
+ code='A001',
+ filename='example.py',
+ line_number=1,
+ column_number=1,
+ text='Fake error',
+ physical_line='a = 1',
+ )
+
+ formatter.handle(error)
+
+ filemock.write.assert_called_once_with(repr(error) + '\n')
diff --git a/tests/unit/test_checker_manager.py b/tests/unit/test_checker_manager.py
new file mode 100644
index 0000000..8240723
--- /dev/null
+++ b/tests/unit/test_checker_manager.py
@@ -0,0 +1,59 @@
+"""Tests for the Manager object for FileCheckers."""
+import errno
+
+import mock
+import pytest
+
+from flake8 import checker
+
+
+def style_guide_mock(**kwargs):
+ """Create a mock StyleGuide object."""
+ kwargs.setdefault('diff', False)
+ kwargs.setdefault('jobs', '4')
+ style_guide = mock.Mock()
+ style_guide.options = mock.Mock(**kwargs)
+ return style_guide
+
+
+def test_oserrors_cause_serial_fall_back():
+ """Verify that OSErrors will cause the Manager to fallback to serial."""
+ err = OSError(errno.ENOSPC, 'Ominous message about spaceeeeee')
+ style_guide = style_guide_mock()
+ with mock.patch('multiprocessing.Queue', side_effect=err):
+ manager = checker.Manager(style_guide, [], [])
+ assert manager.using_multiprocessing is False
+
+
+def test_oserrors_are_reraised():
+ """Verify that OSErrors will cause the Manager to fallback to serial."""
+ err = OSError(errno.EAGAIN, 'Ominous message')
+ style_guide = style_guide_mock()
+ with mock.patch('multiprocessing.Queue', side_effect=err):
+ with pytest.raises(OSError):
+ checker.Manager(style_guide, [], [])
+
+
+def test_multiprocessing_is_disabled():
+ """Verify not being able to import multiprocessing forces jobs to 0."""
+ style_guide = style_guide_mock()
+ with mock.patch('flake8.checker.multiprocessing', None):
+ manager = checker.Manager(style_guide, [], [])
+ assert manager.jobs == 0
+
+
+def test_make_checkers():
+ """Verify that we create a list of FileChecker instances."""
+ style_guide = style_guide_mock()
+ files = ['file1', 'file2']
+ with mock.patch('flake8.checker.multiprocessing', None):
+ manager = checker.Manager(style_guide, files, [])
+
+ with mock.patch('flake8.utils.filenames_from') as filenames_from:
+ filenames_from.side_effect = [['file1'], ['file2']]
+ with mock.patch('flake8.utils.fnmatch', return_value=True):
+ with mock.patch('flake8.processor.FileProcessor'):
+ manager.make_checkers()
+
+ for file_checker in manager.checkers:
+ assert file_checker.filename in files
diff --git a/tests/unit/test_config_file_finder.py b/tests/unit/test_config_file_finder.py
new file mode 100644
index 0000000..2e1a1e5
--- /dev/null
+++ b/tests/unit/test_config_file_finder.py
@@ -0,0 +1,126 @@
+"""Tests for the ConfigFileFinder."""
+import configparser
+import os
+import sys
+
+import mock
+import pytest
+
+from flake8.options import config
+
+CLI_SPECIFIED_FILEPATH = 'tests/fixtures/config_files/cli-specified.ini'
+BROKEN_CONFIG_PATH = 'tests/fixtures/config_files/broken.ini'
+
+
+def test_uses_default_args():
+ """Show that we default the args value."""
+ finder = config.ConfigFileFinder('flake8', None, [])
+ assert finder.parent == os.path.abspath('.')
+
+
+@pytest.mark.parametrize('platform,is_windows', [
+ ('win32', True),
+ ('linux', False),
+ ('darwin', False),
+])
+def test_windows_detection(platform, is_windows):
+ """Verify we detect Windows to the best of our knowledge."""
+ with mock.patch.object(sys, 'platform', platform):
+ finder = config.ConfigFileFinder('flake8', None, [])
+ assert finder.is_windows is is_windows
+
+
+def test_cli_config():
+ """Verify opening and reading the file specified via the cli."""
+ cli_filepath = CLI_SPECIFIED_FILEPATH
+ finder = config.ConfigFileFinder('flake8', None, [])
+
+ parsed_config = finder.cli_config(cli_filepath)
+ assert parsed_config.has_section('flake8')
+
+
+@pytest.mark.parametrize('args,expected', [
+ # No arguments, common prefix of abspath('.')
+ ([],
+ [os.path.abspath('setup.cfg'),
+ os.path.abspath('tox.ini'),
+ os.path.abspath('.flake8')]),
+ # Common prefix of "flake8/"
+ (['flake8/options', 'flake8/'],
+ [os.path.abspath('flake8/setup.cfg'),
+ os.path.abspath('flake8/tox.ini'),
+ os.path.abspath('flake8/.flake8'),
+ os.path.abspath('setup.cfg'),
+ os.path.abspath('tox.ini'),
+ os.path.abspath('.flake8')]),
+ # Common prefix of "flake8/options"
+ (['flake8/options', 'flake8/options/sub'],
+ [os.path.abspath('flake8/options/setup.cfg'),
+ os.path.abspath('flake8/options/tox.ini'),
+ os.path.abspath('flake8/options/.flake8'),
+ os.path.abspath('flake8/setup.cfg'),
+ os.path.abspath('flake8/tox.ini'),
+ os.path.abspath('flake8/.flake8'),
+ os.path.abspath('setup.cfg'),
+ os.path.abspath('tox.ini'),
+ os.path.abspath('.flake8')]),
+])
+def test_generate_possible_local_files(args, expected):
+ """Verify generation of all possible config paths."""
+ finder = config.ConfigFileFinder('flake8', args, [])
+
+ assert (list(finder.generate_possible_local_files()) ==
+ expected)
+
+
+@pytest.mark.parametrize('args,extra_config_files,expected', [
+ # No arguments, common prefix of abspath('.')
+ ([],
+ [],
+ [os.path.abspath('setup.cfg'),
+ os.path.abspath('tox.ini')]),
+ # Common prefix of "flake8/"
+ (['flake8/options', 'flake8/'],
+ [],
+ [os.path.abspath('setup.cfg'),
+ os.path.abspath('tox.ini')]),
+ # Common prefix of "flake8/options"
+ (['flake8/options', 'flake8/options/sub'],
+ [],
+ [os.path.abspath('setup.cfg'),
+ os.path.abspath('tox.ini')]),
+ # Common prefix of "flake8/" with extra config files specified
+ (['flake8/'],
+ [CLI_SPECIFIED_FILEPATH],
+ [os.path.abspath('setup.cfg'),
+ os.path.abspath('tox.ini'),
+ os.path.abspath(CLI_SPECIFIED_FILEPATH)]),
+ # Common prefix of "flake8/" with missing extra config files specified
+ (['flake8/'],
+ [CLI_SPECIFIED_FILEPATH,
+ 'tests/fixtures/config_files/missing.ini'],
+ [os.path.abspath('setup.cfg'),
+ os.path.abspath('tox.ini'),
+ os.path.abspath(CLI_SPECIFIED_FILEPATH)]),
+])
+def test_local_config_files(args, extra_config_files, expected):
+ """Verify discovery of local config files."""
+ finder = config.ConfigFileFinder('flake8', args, extra_config_files)
+
+ assert list(finder.local_config_files()) == expected
+
+
+def test_local_configs():
+ """Verify we return a ConfigParser."""
+ finder = config.ConfigFileFinder('flake8', None, [])
+
+ assert isinstance(finder.local_configs(), configparser.RawConfigParser)
+
+
+@pytest.mark.parametrize('files', [
+ [BROKEN_CONFIG_PATH],
+ [CLI_SPECIFIED_FILEPATH, BROKEN_CONFIG_PATH],
+])
+def test_read_config_catches_broken_config_files(files):
+ """Verify that we do not allow the exception to bubble up."""
+ assert config.ConfigFileFinder._read_config(files)[1] == []
diff --git a/tests/unit/test_file_processor.py b/tests/unit/test_file_processor.py
new file mode 100644
index 0000000..db17c2e
--- /dev/null
+++ b/tests/unit/test_file_processor.py
@@ -0,0 +1,293 @@
+"""Tests for the FileProcessor class."""
+import ast
+import optparse
+import tokenize
+
+from flake8 import processor
+
+import mock
+import pytest
+
+
+def options_from(**kwargs):
+ """Generate a Values instances with our kwargs."""
+ kwargs.setdefault('hang_closing', True)
+ kwargs.setdefault('max_line_length', 79)
+ kwargs.setdefault('verbose', False)
+ return optparse.Values(kwargs)
+
+
+def test_read_lines_splits_lines():
+ """Verify that read_lines splits the lines of the file."""
+ file_processor = processor.FileProcessor(__file__, options_from())
+ lines = file_processor.lines
+ assert len(lines) > 5
+ assert '"""Tests for the FileProcessor class."""\n' in lines
+
+
+@pytest.mark.parametrize('first_line', [
+ '\xEF\xBB\xBF"""Module docstring."""\n',
+ u'\uFEFF"""Module docstring."""\n',
+])
+def test_strip_utf_bom(first_line):
+ r"""Verify that we strip '\xEF\xBB\xBF' from the first line."""
+ lines = [first_line]
+ file_processor = processor.FileProcessor('-', options_from(), lines[:])
+ assert file_processor.lines != lines
+ assert file_processor.lines[0] == '"""Module docstring."""\n'
+
+
+@pytest.mark.parametrize('lines, expected', [
+ (['\xEF\xBB\xBF"""Module docstring."""\n'], False),
+ ([u'\uFEFF"""Module docstring."""\n'], False),
+ (['#!/usr/bin/python', '# flake8 is great', 'a = 1'], False),
+ (['#!/usr/bin/python', '# flake8: noqa', 'a = 1'], True),
+ (['# flake8: noqa', '#!/usr/bin/python', 'a = 1'], True),
+ (['#!/usr/bin/python', 'a = 1', '# flake8: noqa'], True),
+])
+def test_should_ignore_file(lines, expected):
+ """Verify that we ignore a file if told to."""
+ file_processor = processor.FileProcessor('-', options_from(), lines)
+ assert file_processor.should_ignore_file() is expected
+
+
+@mock.patch('flake8.utils.stdin_get_value')
+def test_read_lines_from_stdin(stdin_get_value):
+ """Verify that we use our own utility function to retrieve stdin."""
+ stdin_value = mock.Mock()
+ stdin_value.splitlines.return_value = []
+ stdin_get_value.return_value = stdin_value
+ file_processor = processor.FileProcessor('-', options_from())
+ stdin_get_value.assert_called_once_with()
+ stdin_value.splitlines.assert_called_once_with(True)
+
+
+@mock.patch('flake8.utils.stdin_get_value')
+def test_read_lines_sets_filename_attribute(stdin_get_value):
+ """Verify that we update the filename attribute."""
+ stdin_value = mock.Mock()
+ stdin_value.splitlines.return_value = []
+ stdin_get_value.return_value = stdin_value
+ file_processor = processor.FileProcessor('-', options_from())
+ assert file_processor.filename == 'stdin'
+
+
+def test_line_for():
+ """Verify we grab the correct line from the cached lines."""
+ file_processor = processor.FileProcessor('-', options_from(), lines=[
+ 'Line 1',
+ 'Line 2',
+ 'Line 3',
+ ])
+
+ for i in range(1, 4):
+ assert file_processor.line_for(i) == 'Line {0}'.format(i)
+
+
+def test_next_line():
+ """Verify we update the file_processor state for each new line."""
+ file_processor = processor.FileProcessor('-', options_from(), lines=[
+ 'Line 1',
+ 'Line 2',
+ 'Line 3',
+ ])
+
+ for i in range(1, 4):
+ assert file_processor.next_line() == 'Line {}'.format(i)
+ assert file_processor.line_number == i
+
+
+@pytest.mark.parametrize('error_code, line, expected_indent_char', [
+ ('E101', '\t\ta = 1', '\t'),
+ ('E101', ' a = 1', ' '),
+ ('W101', 'frobulate()', None),
+ ('F821', 'class FizBuz:', None),
+])
+def test_check_physical_error(error_code, line, expected_indent_char):
+ """Verify we update the indet char for the appropriate error code."""
+ file_processor = processor.FileProcessor('-', options_from(), lines=[
+ 'Line 1',
+ ])
+
+ file_processor.check_physical_error(error_code, line)
+ assert file_processor.indent_char == expected_indent_char
+
+
+@pytest.mark.parametrize('params, args, expected_kwargs', [
+ (['blank_before', 'blank_lines'], None, {'blank_before': 0,
+ 'blank_lines': 0}),
+ (['noqa', 'fake'], {'fake': 'foo'}, {'noqa': False, 'fake': 'foo'}),
+ (['blank_before', 'blank_lines', 'noqa'],
+ {'blank_before': 10, 'blank_lines': 5, 'noqa': True},
+ {'blank_before': 10, 'blank_lines': 5, 'noqa': True}),
+ ([], {'fake': 'foo'}, {'fake': 'foo'}),
+])
+def test_keyword_arguments_for(params, args, expected_kwargs):
+ """Verify the keyword args are generated properly."""
+ file_processor = processor.FileProcessor('-', options_from(), lines=[
+ 'Line 1',
+ ])
+ kwargs_for = file_processor.keyword_arguments_for
+
+ assert kwargs_for(params, args) == expected_kwargs
+
+
+def test_keyword_arguments_for_does_not_handle_attribute_errors():
+ """Verify we re-raise AttributeErrors."""
+ file_processor = processor.FileProcessor('-', options_from(), lines=[
+ 'Line 1',
+ ])
+
+ with pytest.raises(AttributeError):
+ file_processor.keyword_arguments_for(['fake'])
+
+
+@pytest.mark.parametrize('unsplit_line, expected_lines', [
+ ('line', []),
+ ('line 1\n', ['line 1']),
+ ('line 1\nline 2\n', ['line 1', 'line 2']),
+ ('line 1\n\nline 2\n', ['line 1', '', 'line 2']),
+])
+def test_split_line(unsplit_line, expected_lines):
+ """Verify the token line spliting."""
+ file_processor = processor.FileProcessor('-', options_from(), lines=[
+ 'Line 1',
+ ])
+
+ actual_lines = list(file_processor.split_line((1, unsplit_line)))
+ assert expected_lines == actual_lines
+
+ assert len(actual_lines) == file_processor.line_number
+
+
+def test_build_ast():
+ """Verify the logic for how we build an AST for plugins."""
+ file_processor = processor.FileProcessor('-', options_from(), lines=[
+ 'a = 1\n'
+ ])
+
+ module = file_processor.build_ast()
+ assert isinstance(module, ast.Module)
+
+
+def test_next_logical_line_updates_the_previous_logical_line():
+ """Verify that we update our tracking of the previous logical line."""
+ file_processor = processor.FileProcessor('-', options_from(), lines=[
+ 'a = 1\n'
+ ])
+
+ file_processor.indent_level = 1
+ file_processor.logical_line = 'a = 1'
+ assert file_processor.previous_logical == ''
+ assert file_processor.previous_indent_level is 0
+
+ file_processor.next_logical_line()
+ assert file_processor.previous_logical == 'a = 1'
+ assert file_processor.previous_indent_level == 1
+
+
+def test_visited_new_blank_line():
+ """Verify we update the number of blank lines seen."""
+ file_processor = processor.FileProcessor('-', options_from(), lines=[
+ 'a = 1\n'
+ ])
+
+ assert file_processor.blank_lines == 0
+ file_processor.visited_new_blank_line()
+ assert file_processor.blank_lines == 1
+
+
+def test_inside_multiline():
+ """Verify we update the line number and reset multiline."""
+ file_processor = processor.FileProcessor('-', options_from(), lines=[
+ 'a = 1\n'
+ ])
+
+ assert file_processor.multiline is False
+ assert file_processor.line_number == 0
+ with file_processor.inside_multiline(10):
+ assert file_processor.multiline is True
+ assert file_processor.line_number == 10
+
+ assert file_processor.multiline is False
+
+
+@pytest.mark.parametrize('string, expected', [
+ ('""', '""'),
+ ("''", "''"),
+ ('"a"', '"x"'),
+ ("'a'", "'x'"),
+ ('"x"', '"x"'),
+ ("'x'", "'x'"),
+ ('"abcdef"', '"xxxxxx"'),
+ ("'abcdef'", "'xxxxxx'"),
+ ('""""""', '""""""'),
+ ("''''''", "''''''"),
+ ('"""a"""', '"""x"""'),
+ ("'''a'''", "'''x'''"),
+ ('"""x"""', '"""x"""'),
+ ("'''x'''", "'''x'''"),
+ ('"""abcdef"""', '"""xxxxxx"""'),
+ ("'''abcdef'''", "'''xxxxxx'''"),
+ ('"""xxxxxx"""', '"""xxxxxx"""'),
+ ("'''xxxxxx'''", "'''xxxxxx'''"),
+])
+def test_mutate_string(string, expected):
+ """Verify we appropriately mutate the string to sanitize it."""
+ actual = processor.mutate_string(string)
+ assert expected == actual
+
+
+@pytest.mark.parametrize('string, expected', [
+ (' ', 4),
+ (' ', 6),
+ ('\t', 8),
+ ('\t\t', 16),
+ (' \t', 8),
+ (' \t', 16),
+])
+def test_expand_indent(string, expected):
+ """Verify we correctly measure the amount of indentation."""
+ actual = processor.expand_indent(string)
+ assert expected == actual
+
+
+@pytest.mark.parametrize('token, log_string', [
+ [(tokenize.COMMENT, '# this is a comment',
+ (1, 0), # (start_row, start_column)
+ (1, 19), # (end_ro, end_column)
+ '# this is a comment',),
+ "l.1\t[:19]\tCOMMENT\t'# this is a comment'"],
+ [(tokenize.COMMENT, '# this is a comment',
+ (1, 5), # (start_row, start_column)
+ (1, 19), # (end_ro, end_column)
+ '# this is a comment',),
+ "l.1\t[5:19]\tCOMMENT\t'# this is a comment'"],
+ [(tokenize.COMMENT, '# this is a comment',
+ (1, 0), # (start_row, start_column)
+ (2, 19), # (end_ro, end_column)
+ '# this is a comment',),
+ "l.1\tl.2\tCOMMENT\t'# this is a comment'"],
+])
+def test_log_token(token, log_string):
+ """Verify we use the log object passed in."""
+ LOG = mock.Mock()
+ processor.log_token(LOG, token)
+ LOG.log.assert_called_once_with(
+ 5, # flake8._EXTRA_VERBOSE
+ log_string,
+ )
+
+
+@pytest.mark.parametrize('current_count, token_text, expected', [
+ (None, '(', 1),
+ (None, '[', 1),
+ (None, '{', 1),
+ (1, ')', 0),
+ (1, ']', 0),
+ (1, '}', 0),
+ (10, '+', 10),
+])
+def test_count_parentheses(current_count, token_text, expected):
+ """Verify our arithmetic is correct."""
+ assert processor.count_parentheses(current_count, token_text) == expected
diff --git a/tests/unit/test_merged_config_parser.py b/tests/unit/test_merged_config_parser.py
new file mode 100644
index 0000000..eb57802
--- /dev/null
+++ b/tests/unit/test_merged_config_parser.py
@@ -0,0 +1,206 @@
+"""Unit tests for flake8.options.config.MergedConfigParser."""
+import os
+
+import mock
+import pytest
+
+from flake8.options import config
+from flake8.options import manager
+
+
+@pytest.fixture
+def optmanager():
+ """Generate an OptionManager with simple values."""
+ return manager.OptionManager(prog='flake8', version='3.0.0a1')
+
+
+@pytest.mark.parametrize('args,extra_config_files', [
+ (None, None),
+ (None, []),
+ (None, ['foo.ini']),
+ ('flake8/', []),
+ ('flake8/', ['foo.ini']),
+])
+def test_creates_its_own_config_file_finder(args, extra_config_files,
+ optmanager):
+ """Verify we create a ConfigFileFinder correctly."""
+ class_path = 'flake8.options.config.ConfigFileFinder'
+ with mock.patch(class_path) as ConfigFileFinder:
+ parser = config.MergedConfigParser(
+ option_manager=optmanager,
+ extra_config_files=extra_config_files,
+ args=args,
+ )
+
+ assert parser.program_name == 'flake8'
+ ConfigFileFinder.assert_called_once_with(
+ 'flake8',
+ args,
+ extra_config_files or [],
+ )
+
+
+def test_parse_cli_config(optmanager):
+ """Parse the specified config file as a cli config file."""
+ optmanager.add_option('--exclude', parse_from_config=True,
+ comma_separated_list=True,
+ normalize_paths=True)
+ optmanager.add_option('--ignore', parse_from_config=True,
+ comma_separated_list=True)
+ parser = config.MergedConfigParser(optmanager)
+
+ parsed_config = parser.parse_cli_config(
+ 'tests/fixtures/config_files/cli-specified.ini'
+ )
+ assert parsed_config == {
+ 'ignore': ['E123', 'W234', 'E111'],
+ 'exclude': [
+ os.path.abspath('foo/'),
+ os.path.abspath('bar/'),
+ os.path.abspath('bogus/'),
+ ]
+ }
+
+
+@pytest.mark.parametrize('filename,is_configured_by', [
+ ('tests/fixtures/config_files/cli-specified.ini', True),
+ ('tests/fixtures/config_files/no-flake8-section.ini', False),
+])
+def test_is_configured_by(filename, is_configured_by, optmanager):
+ """Verify the behaviour of the is_configured_by method."""
+ parsed_config, _ = config.ConfigFileFinder._read_config(filename)
+ parser = config.MergedConfigParser(optmanager)
+
+ assert parser.is_configured_by(parsed_config) is is_configured_by
+
+
+def test_parse_user_config(optmanager):
+ """Verify parsing of user config files."""
+ optmanager.add_option('--exclude', parse_from_config=True,
+ comma_separated_list=True,
+ normalize_paths=True)
+ optmanager.add_option('--ignore', parse_from_config=True,
+ comma_separated_list=True)
+ parser = config.MergedConfigParser(optmanager)
+
+ with mock.patch.object(parser.config_finder, 'user_config_file') as usercf:
+ usercf.return_value = 'tests/fixtures/config_files/cli-specified.ini'
+ parsed_config = parser.parse_user_config()
+
+ assert parsed_config == {
+ 'ignore': ['E123', 'W234', 'E111'],
+ 'exclude': [
+ os.path.abspath('foo/'),
+ os.path.abspath('bar/'),
+ os.path.abspath('bogus/'),
+ ]
+ }
+
+
+def test_parse_local_config(optmanager):
+ """Verify parsing of local config files."""
+ optmanager.add_option('--exclude', parse_from_config=True,
+ comma_separated_list=True,
+ normalize_paths=True)
+ optmanager.add_option('--ignore', parse_from_config=True,
+ comma_separated_list=True)
+ parser = config.MergedConfigParser(optmanager)
+ config_finder = parser.config_finder
+
+ with mock.patch.object(config_finder, 'local_config_files') as localcfs:
+ localcfs.return_value = [
+ 'tests/fixtures/config_files/cli-specified.ini'
+ ]
+ parsed_config = parser.parse_local_config()
+
+ assert parsed_config == {
+ 'ignore': ['E123', 'W234', 'E111'],
+ 'exclude': [
+ os.path.abspath('foo/'),
+ os.path.abspath('bar/'),
+ os.path.abspath('bogus/'),
+ ]
+ }
+
+
+def test_merge_user_and_local_config(optmanager):
+ """Verify merging of parsed user and local config files."""
+ optmanager.add_option('--exclude', parse_from_config=True,
+ comma_separated_list=True,
+ normalize_paths=True)
+ optmanager.add_option('--ignore', parse_from_config=True,
+ comma_separated_list=True)
+ optmanager.add_option('--select', parse_from_config=True,
+ comma_separated_list=True)
+ parser = config.MergedConfigParser(optmanager)
+ config_finder = parser.config_finder
+
+ with mock.patch.object(config_finder, 'local_config_files') as localcfs:
+ localcfs.return_value = [
+ 'tests/fixtures/config_files/local-config.ini'
+ ]
+ with mock.patch.object(config_finder,
+ 'user_config_file') as usercf:
+ usercf.return_value = ('tests/fixtures/config_files/'
+ 'user-config.ini')
+ parsed_config = parser.merge_user_and_local_config()
+
+ assert parsed_config == {
+ 'exclude': [
+ os.path.abspath('docs/')
+ ],
+ 'ignore': ['D203'],
+ 'select': ['E', 'W', 'F'],
+ }
+
+
+@mock.patch('flake8.options.config.ConfigFileFinder')
+def test_parse_isolates_config(ConfigFileManager, optmanager):
+ """Verify behaviour of the parse method with isolated=True."""
+ parser = config.MergedConfigParser(optmanager)
+
+ assert parser.parse(isolated=True) == {}
+ assert parser.config_finder.local_configs.called is False
+ assert parser.config_finder.user_config.called is False
+
+
+@mock.patch('flake8.options.config.ConfigFileFinder')
+def test_parse_uses_cli_config(ConfigFileManager, optmanager):
+ """Verify behaviour of the parse method with a specified config."""
+ parser = config.MergedConfigParser(optmanager)
+
+ parser.parse(cli_config='foo.ini')
+ parser.config_finder.cli_config.assert_called_once_with('foo.ini')
+
+
+@pytest.mark.parametrize('config_fixture_path', [
+ 'tests/fixtures/config_files/cli-specified.ini',
+ 'tests/fixtures/config_files/cli-specified-with-inline-comments.ini',
+ 'tests/fixtures/config_files/cli-specified-without-inline-comments.ini',
+])
+def test_parsed_configs_are_equivalent(optmanager, config_fixture_path):
+ """Verify the each file matches the expected parsed output.
+
+ This is used to ensure our documented behaviour does not regress.
+ """
+ optmanager.add_option('--exclude', parse_from_config=True,
+ comma_separated_list=True,
+ normalize_paths=True)
+ optmanager.add_option('--ignore', parse_from_config=True,
+ comma_separated_list=True)
+ parser = config.MergedConfigParser(optmanager)
+ config_finder = parser.config_finder
+
+ with mock.patch.object(config_finder, 'local_config_files') as localcfs:
+ localcfs.return_value = [config_fixture_path]
+ with mock.patch.object(config_finder,
+ 'user_config_file') as usercf:
+ usercf.return_value = []
+ parsed_config = parser.merge_user_and_local_config()
+
+ assert parsed_config['ignore'] == ['E123', 'W234', 'E111']
+ assert parsed_config['exclude'] == [
+ os.path.abspath('foo/'),
+ os.path.abspath('bar/'),
+ os.path.abspath('bogus/'),
+ ]
diff --git a/tests/unit/test_notifier.py b/tests/unit/test_notifier.py
new file mode 100644
index 0000000..6a162cf
--- /dev/null
+++ b/tests/unit/test_notifier.py
@@ -0,0 +1,54 @@
+"""Unit tests for the Notifier object."""
+import pytest
+
+from flake8.plugins import notifier
+
+
+class _Listener(object):
+ def __init__(self, error_code):
+ self.error_code = error_code
+ self.was_notified = False
+
+ def notify(self, error_code, *args, **kwargs):
+ assert error_code.startswith(self.error_code)
+ self.was_notified = True
+
+
+class TestNotifier(object):
+ """Notifier unit tests."""
+
+ @pytest.fixture(autouse=True)
+ def setup(self):
+ """Set up each TestNotifier instance."""
+ self.notifier = notifier.Notifier()
+ self.listener_map = {}
+
+ def add_listener(error_code):
+ listener = _Listener(error_code)
+ self.listener_map[error_code] = listener
+ self.notifier.register_listener(error_code, listener)
+
+ for i in range(10):
+ add_listener('E{0}'.format(i))
+ for j in range(30):
+ add_listener('E{0}{1:02d}'.format(i, j))
+
+ def test_notify(self):
+ """Show that we notify a specific error code."""
+ self.notifier.notify('E111', 'extra', 'args')
+ assert self.listener_map['E111'].was_notified is True
+ assert self.listener_map['E1'].was_notified is True
+
+ @pytest.mark.parametrize('code', ['W123', 'W12', 'W1', 'W'])
+ def test_no_listeners_for(self, code):
+ """Show that we return an empty list of listeners."""
+ assert list(self.notifier.listeners_for(code)) == []
+
+ @pytest.mark.parametrize('code,expected', [
+ ('E101', ['E101', 'E1']),
+ ('E211', ['E211', 'E2']),
+ ])
+ def test_listeners_for(self, code, expected):
+ """Verify that we retrieve the correct listeners."""
+ assert ([l.error_code for l in self.notifier.listeners_for(code)] ==
+ expected)
diff --git a/tests/unit/test_option.py b/tests/unit/test_option.py
new file mode 100644
index 0000000..67e2255
--- /dev/null
+++ b/tests/unit/test_option.py
@@ -0,0 +1,68 @@
+"""Unit tests for flake8.options.manager.Option."""
+import mock
+import pytest
+
+from flake8.options import manager
+
+
+def test_to_optparse():
+ """Test conversion to an optparse.Option class."""
+ opt = manager.Option(
+ short_option_name='-t',
+ long_option_name='--test',
+ action='count',
+ parse_from_config=True,
+ normalize_paths=True,
+ )
+ assert opt.normalize_paths is True
+ assert opt.parse_from_config is True
+
+ optparse_opt = opt.to_optparse()
+ assert not hasattr(optparse_opt, 'parse_from_config')
+ assert not hasattr(optparse_opt, 'normalize_paths')
+ assert optparse_opt.action == 'count'
+
+
+@mock.patch('optparse.Option')
+def test_to_optparse_creates_an_option_as_we_expect(Option):
+ """Show that we pass all keyword args to optparse.Option."""
+ opt = manager.Option('-t', '--test', action='count')
+ opt.to_optparse()
+ option_kwargs = {
+ 'action': 'count',
+ 'default': None,
+ 'type': None,
+ 'dest': 'test',
+ 'nargs': None,
+ 'const': None,
+ 'choices': None,
+ 'callback': None,
+ 'callback_args': None,
+ 'callback_kwargs': None,
+ 'help': None,
+ 'metavar': None,
+ }
+
+ Option.assert_called_once_with(
+ '-t', '--test', **option_kwargs
+ )
+
+
+def test_config_name_generation():
+ """Show that we generate the config name deterministically."""
+ opt = manager.Option(long_option_name='--some-very-long-option-name',
+ parse_from_config=True)
+
+ assert opt.config_name == 'some_very_long_option_name'
+
+
+def test_config_name_needs_long_option_name():
+ """Show that we error out if the Option should be parsed from config."""
+ with pytest.raises(ValueError):
+ manager.Option('-s', parse_from_config=True)
+
+
+def test_dest_is_not_overridden():
+ """Show that we do not override custom destinations."""
+ opt = manager.Option('-s', '--short', dest='something_not_short')
+ assert opt.dest == 'something_not_short'
diff --git a/tests/unit/test_option_manager.py b/tests/unit/test_option_manager.py
new file mode 100644
index 0000000..53e8bf1
--- /dev/null
+++ b/tests/unit/test_option_manager.py
@@ -0,0 +1,194 @@
+"""Unit tests for flake.options.manager.OptionManager."""
+import optparse
+import os
+
+import pytest
+
+from flake8.options import manager
+
+TEST_VERSION = '3.0.0b1'
+
+
+@pytest.fixture
+def optmanager():
+ """Generate a simple OptionManager with default test arguments."""
+ return manager.OptionManager(prog='flake8', version=TEST_VERSION)
+
+
+def test_option_manager_creates_option_parser(optmanager):
+ """Verify that a new manager creates a new parser."""
+ assert optmanager.parser is not None
+ assert isinstance(optmanager.parser, optparse.OptionParser) is True
+
+
+def test_add_option_short_option_only(optmanager):
+ """Verify the behaviour of adding a short-option only."""
+ assert optmanager.options == []
+ assert optmanager.config_options_dict == {}
+
+ optmanager.add_option('-s', help='Test short opt')
+ assert optmanager.options[0].short_option_name == '-s'
+
+
+def test_add_option_long_option_only(optmanager):
+ """Verify the behaviour of adding a long-option only."""
+ assert optmanager.options == []
+ assert optmanager.config_options_dict == {}
+
+ optmanager.add_option('--long', help='Test long opt')
+ assert optmanager.options[0].short_option_name is None
+ assert optmanager.options[0].long_option_name == '--long'
+
+
+def test_add_short_and_long_option_names(optmanager):
+ """Verify the behaviour of using both short and long option names."""
+ assert optmanager.options == []
+ assert optmanager.config_options_dict == {}
+
+ optmanager.add_option('-b', '--both', help='Test both opts')
+ assert optmanager.options[0].short_option_name == '-b'
+ assert optmanager.options[0].long_option_name == '--both'
+
+
+def test_add_option_with_custom_args(optmanager):
+ """Verify that add_option handles custom Flake8 parameters."""
+ assert optmanager.options == []
+ assert optmanager.config_options_dict == {}
+
+ optmanager.add_option('--parse', parse_from_config=True)
+ optmanager.add_option('--commas', comma_separated_list=True)
+ optmanager.add_option('--files', normalize_paths=True)
+
+ attrs = ['parse_from_config', 'comma_separated_list', 'normalize_paths']
+ for option, attr in zip(optmanager.options, attrs):
+ assert getattr(option, attr) is True
+
+
+def test_parse_args_normalize_path(optmanager):
+ """Show that parse_args handles path normalization."""
+ assert optmanager.options == []
+ assert optmanager.config_options_dict == {}
+
+ optmanager.add_option('--config', normalize_paths=True)
+
+ options, args = optmanager.parse_args(['--config', '../config.ini'])
+ assert options.config == os.path.abspath('../config.ini')
+
+
+def test_parse_args_handles_comma_separated_defaults(optmanager):
+ """Show that parse_args handles defaults that are comma-separated."""
+ assert optmanager.options == []
+ assert optmanager.config_options_dict == {}
+
+ optmanager.add_option('--exclude', default='E123,W234',
+ comma_separated_list=True)
+
+ options, args = optmanager.parse_args([])
+ assert options.exclude == ['E123', 'W234']
+
+
+def test_parse_args_handles_comma_separated_lists(optmanager):
+ """Show that parse_args handles user-specified comma-separated lists."""
+ assert optmanager.options == []
+ assert optmanager.config_options_dict == {}
+
+ optmanager.add_option('--exclude', default='E123,W234',
+ comma_separated_list=True)
+
+ options, args = optmanager.parse_args(['--exclude', 'E201,W111,F280'])
+ assert options.exclude == ['E201', 'W111', 'F280']
+
+
+def test_parse_args_normalize_paths(optmanager):
+ """Verify parse_args normalizes a comma-separated list of paths."""
+ assert optmanager.options == []
+ assert optmanager.config_options_dict == {}
+
+ optmanager.add_option('--extra-config', normalize_paths=True,
+ comma_separated_list=True)
+
+ options, args = optmanager.parse_args([
+ '--extra-config', '../config.ini,tox.ini,flake8/some-other.cfg'
+ ])
+ assert options.extra_config == [
+ os.path.abspath('../config.ini'),
+ 'tox.ini',
+ os.path.abspath('flake8/some-other.cfg'),
+ ]
+
+
+def test_format_plugin():
+ """Verify that format_plugin turns a tuple into a dictionary."""
+ plugin = manager.OptionManager.format_plugin(('Testing', '0.0.0'))
+ assert plugin['name'] == 'Testing'
+ assert plugin['version'] == '0.0.0'
+
+
+def test_generate_versions(optmanager):
+ """Verify a comma-separated string is generated of registered plugins."""
+ optmanager.registered_plugins = [
+ ('Testing 100', '0.0.0'),
+ ('Testing 101', '0.0.0'),
+ ('Testing 300', '0.0.0'),
+ ]
+ assert (optmanager.generate_versions() ==
+ 'Testing 100: 0.0.0, Testing 101: 0.0.0, Testing 300: 0.0.0')
+
+
+def test_generate_versions_with_format_string(optmanager):
+ """Verify a comma-separated string is generated of registered plugins."""
+ optmanager.registered_plugins.update([
+ ('Testing', '0.0.0'),
+ ('Testing', '0.0.0'),
+ ('Testing', '0.0.0'),
+ ])
+ assert (
+ optmanager.generate_versions() == 'Testing: 0.0.0'
+ )
+
+
+def test_update_version_string(optmanager):
+ """Verify we update the version string idempotently."""
+ assert optmanager.version == TEST_VERSION
+ assert optmanager.parser.version == TEST_VERSION
+
+ optmanager.registered_plugins = [
+ ('Testing 100', '0.0.0'),
+ ('Testing 101', '0.0.0'),
+ ('Testing 300', '0.0.0'),
+ ]
+
+ optmanager.update_version_string()
+
+ assert optmanager.version == TEST_VERSION
+ assert (optmanager.parser.version == TEST_VERSION + ' ('
+ 'Testing 100: 0.0.0, Testing 101: 0.0.0, Testing 300: 0.0.0)')
+
+
+def test_generate_epilog(optmanager):
+ """Verify how we generate the epilog for help text."""
+ assert optmanager.parser.epilog is None
+
+ optmanager.registered_plugins = [
+ ('Testing 100', '0.0.0'),
+ ('Testing 101', '0.0.0'),
+ ('Testing 300', '0.0.0'),
+ ]
+
+ expected_value = (
+ 'Installed plugins: Testing 100: 0.0.0, Testing 101: 0.0.0, Testing'
+ ' 300: 0.0.0'
+ )
+
+ optmanager.generate_epilog()
+ assert optmanager.parser.epilog == expected_value
+
+
+def test_extend_default_ignore(optmanager):
+ """Verify that we update the extended default ignore list."""
+ assert optmanager.extended_default_ignore == set()
+
+ optmanager.extend_default_ignore(['T100', 'T101', 'T102'])
+ assert optmanager.extended_default_ignore == set(['T100',
+ 'T101',
+ 'T102'])
diff --git a/tests/unit/test_plugin.py b/tests/unit/test_plugin.py
new file mode 100644
index 0000000..f69bc05
--- /dev/null
+++ b/tests/unit/test_plugin.py
@@ -0,0 +1,153 @@
+"""Tests for flake8.plugins.manager.Plugin."""
+import optparse
+
+import mock
+import pytest
+
+from flake8 import exceptions
+from flake8.plugins import manager
+
+
+def test_load_plugin_fallsback_on_old_setuptools():
+ """Verify we fallback gracefully to on old versions of setuptools."""
+ entry_point = mock.Mock(spec=['load'])
+ plugin = manager.Plugin('T000', entry_point)
+
+ plugin.load_plugin()
+ entry_point.load.assert_called_once_with(require=False)
+
+
+def test_load_plugin_avoids_deprecated_entry_point_methods():
+ """Verify we use the preferred methods on new versions of setuptools."""
+ entry_point = mock.Mock(spec=['require', 'resolve', 'load'])
+ plugin = manager.Plugin('T000', entry_point)
+
+ plugin.load_plugin(verify_requirements=True)
+ assert entry_point.load.called is False
+ entry_point.require.assert_called_once_with()
+ entry_point.resolve.assert_called_once_with()
+
+
+def test_load_plugin_is_idempotent():
+ """Verify we use the preferred methods on new versions of setuptools."""
+ entry_point = mock.Mock(spec=['require', 'resolve', 'load'])
+ plugin = manager.Plugin('T000', entry_point)
+
+ plugin.load_plugin(verify_requirements=True)
+ plugin.load_plugin(verify_requirements=True)
+ plugin.load_plugin()
+ assert entry_point.load.called is False
+ entry_point.require.assert_called_once_with()
+ entry_point.resolve.assert_called_once_with()
+
+
+def test_load_plugin_only_calls_require_when_verifying_requirements():
+ """Verify we do not call require when verify_requirements is False."""
+ entry_point = mock.Mock(spec=['require', 'resolve', 'load'])
+ plugin = manager.Plugin('T000', entry_point)
+
+ plugin.load_plugin()
+ assert entry_point.load.called is False
+ assert entry_point.require.called is False
+ entry_point.resolve.assert_called_once_with()
+
+
+def test_load_plugin_catches_and_reraises_exceptions():
+ """Verify we raise our own FailedToLoadPlugin."""
+ entry_point = mock.Mock(spec=['require', 'resolve'])
+ entry_point.resolve.side_effect = ValueError('Test failure')
+ plugin = manager.Plugin('T000', entry_point)
+
+ with pytest.raises(exceptions.FailedToLoadPlugin):
+ plugin.load_plugin()
+
+
+def test_plugin_property_loads_plugin_on_first_use():
+ """Verify that we load our plugin when we first try to use it."""
+ entry_point = mock.Mock(spec=['require', 'resolve', 'load'])
+ plugin = manager.Plugin('T000', entry_point)
+
+ assert plugin.plugin is not None
+ entry_point.resolve.assert_called_once_with()
+
+
+def test_execute_calls_plugin_with_passed_arguments():
+ """Verify that we pass arguments directly to the plugin."""
+ entry_point = mock.Mock(spec=['require', 'resolve', 'load'])
+ plugin_obj = mock.Mock()
+ plugin = manager.Plugin('T000', entry_point)
+ plugin._plugin = plugin_obj
+
+ plugin.execute('arg1', 'arg2', kwarg1='value1', kwarg2='value2')
+ plugin_obj.assert_called_once_with(
+ 'arg1', 'arg2', kwarg1='value1', kwarg2='value2'
+ )
+
+ # Extra assertions
+ assert entry_point.load.called is False
+ assert entry_point.require.called is False
+ assert entry_point.resolve.called is False
+
+
+def test_version_proxies_to_the_plugin():
+ """Verify that we pass arguments directly to the plugin."""
+ entry_point = mock.Mock(spec=['require', 'resolve', 'load'])
+ plugin_obj = mock.Mock(spec_set=['version'])
+ plugin_obj.version = 'a.b.c'
+ plugin = manager.Plugin('T000', entry_point)
+ plugin._plugin = plugin_obj
+
+ assert plugin.version == 'a.b.c'
+
+
+def test_register_options():
+ """Verify we call add_options on the plugin only if it exists."""
+ # Set up our mocks and Plugin object
+ entry_point = mock.Mock(spec=['require', 'resolve', 'load'])
+ plugin_obj = mock.Mock(spec_set=['name', 'version', 'add_options',
+ 'parse_options'])
+ option_manager = mock.Mock(spec=['register_plugin'])
+ plugin = manager.Plugin('T000', entry_point)
+ plugin._plugin = plugin_obj
+
+ # Call the method we're testing.
+ plugin.register_options(option_manager)
+
+ # Assert that we call add_options
+ plugin_obj.add_options.assert_called_once_with(option_manager)
+
+
+def test_register_options_checks_plugin_for_method():
+ """Verify we call add_options on the plugin only if it exists."""
+ # Set up our mocks and Plugin object
+ entry_point = mock.Mock(spec=['require', 'resolve', 'load'])
+ plugin_obj = mock.Mock(spec_set=['name', 'version', 'parse_options'])
+ option_manager = mock.Mock(spec=['register_plugin'])
+ plugin = manager.Plugin('T000', entry_point)
+ plugin._plugin = plugin_obj
+
+ # Call the method we're testing.
+ plugin.register_options(option_manager)
+
+ # Assert that we register the plugin
+ assert option_manager.register_plugin.called is False
+
+
+def test_provide_options():
+ """Verify we call add_options on the plugin only if it exists."""
+ # Set up our mocks and Plugin object
+ entry_point = mock.Mock(spec=['require', 'resolve', 'load'])
+ plugin_obj = mock.Mock(spec_set=['name', 'version', 'add_options',
+ 'parse_options'])
+ option_values = optparse.Values({'enable_extensions': []})
+ option_manager = mock.Mock()
+ plugin = manager.Plugin('T000', entry_point)
+ plugin._plugin = plugin_obj
+
+ # Call the method we're testing.
+ plugin.provide_options(option_manager, option_values, None)
+
+ # Assert that we call add_options
+ plugin_obj.parse_options.assert_called_once_with(
+ option_manager, option_values, None
+ )
diff --git a/tests/unit/test_plugin_manager.py b/tests/unit/test_plugin_manager.py
new file mode 100644
index 0000000..8991b96
--- /dev/null
+++ b/tests/unit/test_plugin_manager.py
@@ -0,0 +1,50 @@
+"""Tests for flake8.plugins.manager.PluginManager."""
+import mock
+
+from flake8.plugins import manager
+
+
+def create_entry_point_mock(name):
+ """Create a mocked EntryPoint."""
+ ep = mock.Mock(spec=['name'])
+ ep.name = name
+ return ep
+
+
+@mock.patch('pkg_resources.iter_entry_points')
+def test_calls_pkg_resources_on_instantiation(iter_entry_points):
+ """Verify that we call iter_entry_points when we create a manager."""
+ iter_entry_points.return_value = []
+ manager.PluginManager(namespace='testing.pkg_resources')
+
+ iter_entry_points.assert_called_once_with('testing.pkg_resources')
+
+
+@mock.patch('pkg_resources.iter_entry_points')
+def test_calls_pkg_resources_creates_plugins_automaticaly(iter_entry_points):
+ """Verify that we create Plugins on instantiation."""
+ iter_entry_points.return_value = [
+ create_entry_point_mock('T100'),
+ create_entry_point_mock('T200'),
+ ]
+ plugin_mgr = manager.PluginManager(namespace='testing.pkg_resources')
+
+ iter_entry_points.assert_called_once_with('testing.pkg_resources')
+ assert 'T100' in plugin_mgr.plugins
+ assert 'T200' in plugin_mgr.plugins
+ assert isinstance(plugin_mgr.plugins['T100'], manager.Plugin)
+ assert isinstance(plugin_mgr.plugins['T200'], manager.Plugin)
+
+
+@mock.patch('pkg_resources.iter_entry_points')
+def test_handles_mapping_functions_across_plugins(iter_entry_points):
+ """Verify we can use the PluginManager call functions on all plugins."""
+ entry_point_mocks = [
+ create_entry_point_mock('T100'),
+ create_entry_point_mock('T200'),
+ ]
+ iter_entry_points.return_value = entry_point_mocks
+ plugin_mgr = manager.PluginManager(namespace='testing.pkg_resources')
+ plugins = [plugin_mgr.plugins[name] for name in plugin_mgr.names]
+
+ assert list(plugin_mgr.map(lambda x: x)) == plugins
diff --git a/tests/unit/test_plugin_type_manager.py b/tests/unit/test_plugin_type_manager.py
new file mode 100644
index 0000000..186c2e3
--- /dev/null
+++ b/tests/unit/test_plugin_type_manager.py
@@ -0,0 +1,228 @@
+"""Tests for flake8.plugins.manager.PluginTypeManager."""
+import collections
+
+import mock
+import pytest
+
+from flake8 import exceptions
+from flake8.plugins import manager
+
+TEST_NAMESPACE = "testing.plugin-type-manager"
+
+
+def create_plugin_mock(raise_exception=False):
+ """Create an auto-spec'd mock of a flake8 Plugin."""
+ plugin = mock.create_autospec(manager.Plugin, instance=True)
+ if raise_exception:
+ plugin.load_plugin.side_effect = exceptions.FailedToLoadPlugin(
+ plugin=mock.Mock(name='T101'),
+ exception=ValueError('Test failure'),
+ )
+ return plugin
+
+
+def create_mapping_manager_mock(plugins):
+ """Create a mock for the PluginManager."""
+ # Have a function that will actually call the method underneath
+ def fake_map(func):
+ for plugin in plugins:
+ yield func(plugin)
+
+ # Mock out the PluginManager instance
+ manager_mock = mock.Mock(spec=['map'])
+ # Replace the map method
+ manager_mock.map = fake_map
+ return manager_mock
+
+
+def create_manager_with_plugins(plugins):
+ """Create a fake PluginManager with a plugins dictionary."""
+ manager_mock = mock.create_autospec(manager.PluginManager)
+ manager_mock.plugins = plugins
+ return manager_mock
+
+
+class FakeTestType(manager.PluginTypeManager):
+ """Fake PluginTypeManager."""
+
+ namespace = TEST_NAMESPACE
+
+
+@mock.patch('flake8.plugins.manager.PluginManager')
+def test_instantiates_a_manager(PluginManager):
+ """Verify we create a PluginManager on instantiation."""
+ FakeTestType()
+
+ PluginManager.assert_called_once_with(TEST_NAMESPACE)
+
+
+@mock.patch('flake8.plugins.manager.PluginManager')
+def test_proxies_names_to_manager(PluginManager):
+ """Verify we proxy the names attribute."""
+ PluginManager.return_value = mock.Mock(names=[
+ 'T100', 'T200', 'T300'
+ ])
+ type_mgr = FakeTestType()
+
+ assert type_mgr.names == ['T100', 'T200', 'T300']
+
+
+@mock.patch('flake8.plugins.manager.PluginManager')
+def test_proxies_plugins_to_manager(PluginManager):
+ """Verify we proxy the plugins attribute."""
+ PluginManager.return_value = mock.Mock(plugins=[
+ 'T100', 'T200', 'T300'
+ ])
+ type_mgr = FakeTestType()
+
+ assert type_mgr.plugins == ['T100', 'T200', 'T300']
+
+
+def test_generate_call_function():
+ """Verify the function we generate."""
+ optmanager = object()
+ plugin = mock.Mock(method_name=lambda x: x)
+ func = manager.PluginTypeManager._generate_call_function(
+ 'method_name', optmanager,
+ )
+
+ assert isinstance(func, collections.Callable)
+ assert func(plugin) is optmanager
+
+
+@mock.patch('flake8.plugins.manager.PluginManager')
+def test_load_plugins(PluginManager):
+ """Verify load plugins loads *every* plugin."""
+ # Create a bunch of fake plugins
+ plugins = [create_plugin_mock(), create_plugin_mock(),
+ create_plugin_mock(), create_plugin_mock(),
+ create_plugin_mock(), create_plugin_mock(),
+ create_plugin_mock(), create_plugin_mock()]
+ # Return our PluginManager mock
+ PluginManager.return_value = create_mapping_manager_mock(plugins)
+
+ type_mgr = FakeTestType()
+ # Load the tests (do what we're actually testing)
+ assert len(type_mgr.load_plugins()) == 8
+ # Assert that our closure does what we think it does
+ for plugin in plugins:
+ plugin.load_plugin.assert_called_once_with()
+ assert type_mgr.plugins_loaded is True
+
+
+@mock.patch('flake8.plugins.manager.PluginManager')
+def test_load_plugins_fails(PluginManager):
+ """Verify load plugins bubbles up exceptions."""
+ plugins = [create_plugin_mock(), create_plugin_mock(True),
+ create_plugin_mock(), create_plugin_mock(),
+ create_plugin_mock(), create_plugin_mock(),
+ create_plugin_mock(), create_plugin_mock()]
+ # Return our PluginManager mock
+ PluginManager.return_value = create_mapping_manager_mock(plugins)
+
+ type_mgr = FakeTestType()
+ with pytest.raises(exceptions.FailedToLoadPlugin):
+ type_mgr.load_plugins()
+
+ # Assert we didn't finish loading plugins
+ assert type_mgr.plugins_loaded is False
+ # Assert the first two plugins had their load_plugin method called
+ plugins[0].load_plugin.assert_called_once_with()
+ plugins[1].load_plugin.assert_called_once_with()
+ # Assert the rest of the plugins were not loaded
+ for plugin in plugins[2:]:
+ assert plugin.load_plugin.called is False
+
+
+@mock.patch('flake8.plugins.manager.PluginManager')
+def test_register_options(PluginManager):
+ """Test that we map over every plugin to register options."""
+ plugins = [create_plugin_mock(), create_plugin_mock(),
+ create_plugin_mock(), create_plugin_mock(),
+ create_plugin_mock(), create_plugin_mock(),
+ create_plugin_mock(), create_plugin_mock()]
+ # Return our PluginManager mock
+ PluginManager.return_value = create_mapping_manager_mock(plugins)
+ optmanager = object()
+
+ type_mgr = FakeTestType()
+ type_mgr.register_options(optmanager)
+
+ for plugin in plugins:
+ plugin.register_options.assert_called_with(optmanager)
+
+
+@mock.patch('flake8.plugins.manager.PluginManager')
+def test_provide_options(PluginManager):
+ """Test that we map over every plugin to provide parsed options."""
+ plugins = [create_plugin_mock(), create_plugin_mock(),
+ create_plugin_mock(), create_plugin_mock(),
+ create_plugin_mock(), create_plugin_mock(),
+ create_plugin_mock(), create_plugin_mock()]
+ # Return our PluginManager mock
+ PluginManager.return_value = create_mapping_manager_mock(plugins)
+ optmanager = object()
+ options = object()
+ extra_args = []
+
+ type_mgr = FakeTestType()
+ type_mgr.provide_options(optmanager, options, extra_args)
+
+ for plugin in plugins:
+ plugin.provide_options.assert_called_with(optmanager,
+ options,
+ extra_args)
+
+
+@mock.patch('flake8.plugins.manager.PluginManager')
+def test_proxy_contains_to_managers_plugins_dict(PluginManager):
+ """Verify that we proxy __contains__ to the manager's dictionary."""
+ plugins = {'T10%i' % i: create_plugin_mock() for i in range(8)}
+ # Return our PluginManager mock
+ PluginManager.return_value = create_manager_with_plugins(plugins)
+
+ type_mgr = FakeTestType()
+ for i in range(8):
+ key = 'T10%i' % i
+ assert key in type_mgr
+
+
+@mock.patch('flake8.plugins.manager.PluginManager')
+def test_proxies_getitem_to_managers_plugins_dictionary(PluginManager):
+ """Verify that we can use the PluginTypeManager like a dictionary."""
+ plugins = {'T10%i' % i: create_plugin_mock() for i in range(8)}
+ # Return our PluginManager mock
+ PluginManager.return_value = create_manager_with_plugins(plugins)
+
+ type_mgr = FakeTestType()
+ for i in range(8):
+ key = 'T10%i' % i
+ assert type_mgr[key] is plugins[key]
+
+
+class FakePluginTypeManager(manager.NotifierBuilderMixin):
+ """Provide an easy way to test the NotifierBuilderMixin."""
+
+ def __init__(self, manager):
+ """Initialize with our fake manager."""
+ self.names = sorted(manager.keys())
+ self.manager = manager
+
+
+@pytest.fixture
+def notifier_builder():
+ """Create a fake plugin type manager."""
+ return FakePluginTypeManager(manager={
+ 'T100': object(),
+ 'T101': object(),
+ 'T110': object(),
+ })
+
+
+def test_build_notifier(notifier_builder):
+ """Verify we properly build a Notifier object."""
+ notifier = notifier_builder.build_notifier()
+ for name in ('T100', 'T101', 'T110'):
+ assert list(notifier.listeners_for(name)) == [
+ notifier_builder.manager[name]
+ ]
diff --git a/tests/unit/test_style_guide.py b/tests/unit/test_style_guide.py
new file mode 100644
index 0000000..3d05528
--- /dev/null
+++ b/tests/unit/test_style_guide.py
@@ -0,0 +1,207 @@
+"""Tests for the flake8.style_guide.StyleGuide class."""
+import optparse
+
+import mock
+import pytest
+
+from flake8 import style_guide
+from flake8.formatting import base
+from flake8.plugins import notifier
+
+
+def create_options(**kwargs):
+ """Create and return an instance of optparse.Values."""
+ kwargs.setdefault('select', [])
+ kwargs.setdefault('ignore', [])
+ kwargs.setdefault('disable_noqa', False)
+ return optparse.Values(kwargs)
+
+
+@pytest.mark.parametrize('ignore_list,error_code', [
+ (['E111', 'E121'], 'E111'),
+ (['E111', 'E121'], 'E121'),
+ (['E11', 'E12'], 'E121'),
+ (['E2', 'E12'], 'E121'),
+ (['E2', 'E12'], 'E211'),
+])
+def test_is_user_ignored_ignores_errors(ignore_list, error_code):
+ """Verify we detect users explicitly ignoring an error."""
+ guide = style_guide.StyleGuide(create_options(ignore=ignore_list),
+ listener_trie=None,
+ formatter=None)
+
+ assert guide.is_user_ignored(error_code) is style_guide.Ignored.Explicitly
+
+
+@pytest.mark.parametrize('ignore_list,error_code', [
+ (['E111', 'E121'], 'E112'),
+ (['E111', 'E121'], 'E122'),
+ (['E11', 'E12'], 'W121'),
+ (['E2', 'E12'], 'E112'),
+ (['E2', 'E12'], 'E111'),
+])
+def test_is_user_ignored_implicitly_selects_errors(ignore_list, error_code):
+ """Verify we detect users does not explicitly ignore an error."""
+ guide = style_guide.StyleGuide(create_options(ignore=ignore_list),
+ listener_trie=None,
+ formatter=None)
+
+ assert guide.is_user_ignored(error_code) is style_guide.Selected.Implicitly
+
+
+@pytest.mark.parametrize('select_list,error_code', [
+ (['E111', 'E121'], 'E111'),
+ (['E111', 'E121'], 'E121'),
+ (['E11', 'E12'], 'E121'),
+ (['E2', 'E12'], 'E121'),
+ (['E2', 'E12'], 'E211'),
+])
+def test_is_user_selected_selects_errors(select_list, error_code):
+ """Verify we detect users explicitly selecting an error."""
+ guide = style_guide.StyleGuide(create_options(select=select_list),
+ listener_trie=None,
+ formatter=None)
+
+ assert (guide.is_user_selected(error_code) is
+ style_guide.Selected.Explicitly)
+
+
+def test_is_user_selected_implicitly_selects_errors():
+ """Verify we detect users implicitly selecting an error."""
+ select_list = []
+ error_code = 'E121'
+ guide = style_guide.StyleGuide(create_options(select=select_list),
+ listener_trie=None,
+ formatter=None)
+
+ assert (guide.is_user_selected(error_code) is
+ style_guide.Selected.Implicitly)
+
+
+@pytest.mark.parametrize('select_list,error_code', [
+ (['E111', 'E121'], 'E112'),
+ (['E111', 'E121'], 'E122'),
+ (['E11', 'E12'], 'E132'),
+ (['E2', 'E12'], 'E321'),
+ (['E2', 'E12'], 'E410'),
+])
+def test_is_user_selected_excludes_errors(select_list, error_code):
+ """Verify we detect users implicitly excludes an error."""
+ guide = style_guide.StyleGuide(create_options(select=select_list),
+ listener_trie=None,
+ formatter=None)
+
+ assert guide.is_user_selected(error_code) is style_guide.Ignored.Implicitly
+
+
+@pytest.mark.parametrize('select_list,ignore_list,error_code,expected', [
+ (['E111', 'E121'], [], 'E111', style_guide.Decision.Selected),
+ (['E111', 'E121'], [], 'E112', style_guide.Decision.Ignored),
+ (['E111', 'E121'], [], 'E121', style_guide.Decision.Selected),
+ (['E111', 'E121'], [], 'E122', style_guide.Decision.Ignored),
+ (['E11', 'E12'], [], 'E132', style_guide.Decision.Ignored),
+ (['E2', 'E12'], [], 'E321', style_guide.Decision.Ignored),
+ (['E2', 'E12'], [], 'E410', style_guide.Decision.Ignored),
+ (['E11', 'E121'], ['E1'], 'E112', style_guide.Decision.Selected),
+ (['E111', 'E121'], ['E2'], 'E122', style_guide.Decision.Ignored),
+ (['E11', 'E12'], ['E13'], 'E132', style_guide.Decision.Ignored),
+ (['E1', 'E3'], ['E32'], 'E321', style_guide.Decision.Ignored),
+ ([], ['E2', 'E12'], 'E410', style_guide.Decision.Selected),
+ (['E4'], ['E2', 'E12', 'E41'], 'E410', style_guide.Decision.Ignored),
+ (['E41'], ['E2', 'E12', 'E4'], 'E410', style_guide.Decision.Selected),
+])
+def test_should_report_error(select_list, ignore_list, error_code, expected):
+ """Verify we decide when to report an error."""
+ guide = style_guide.StyleGuide(create_options(select=select_list,
+ ignore=ignore_list),
+ listener_trie=None,
+ formatter=None)
+
+ assert guide.should_report_error(error_code) is expected
+
+
+@pytest.mark.parametrize('error_code,physical_line,expected_result', [
+ ('E111', 'a = 1', False),
+ ('E121', 'a = 1 # noqa: E111', False),
+ ('E121', 'a = 1 # noqa: E111,W123,F821', False),
+ ('E111', 'a = 1 # noqa: E111,W123,F821', True),
+ ('W123', 'a = 1 # noqa: E111,W123,F821', True),
+ ('E111', 'a = 1 # noqa: E11,W123,F821', True),
+])
+def test_is_inline_ignored(error_code, physical_line, expected_result):
+ """Verify that we detect inline usage of ``# noqa``."""
+ guide = style_guide.StyleGuide(create_options(select=['E', 'W', 'F']),
+ listener_trie=None,
+ formatter=None)
+ error = style_guide.Error(error_code, 'filename.py', 1, 1, 'error text',
+ None)
+ # We want `None` to be passed as the physical line so we actually use our
+ # monkey-patched linecache.getline value.
+
+ with mock.patch('linecache.getline', return_value=physical_line):
+ assert guide.is_inline_ignored(error) is expected_result
+
+
+def test_disable_is_inline_ignored():
+ """Verify that is_inline_ignored exits immediately if disabling NoQA."""
+ guide = style_guide.StyleGuide(create_options(disable_noqa=True),
+ listener_trie=None,
+ formatter=None)
+ error = style_guide.Error('E121', 'filename.py', 1, 1, 'error text',
+ 'line')
+
+ with mock.patch('linecache.getline') as getline:
+ assert guide.is_inline_ignored(error) is False
+
+ assert getline.called is False
+
+
+@pytest.mark.parametrize('select_list,ignore_list,error_code', [
+ (['E111', 'E121'], [], 'E111'),
+ (['E111', 'E121'], [], 'E121'),
+ (['E11', 'E121'], ['E1'], 'E112'),
+ ([], ['E2', 'E12'], 'E410'),
+ (['E41'], ['E2', 'E12', 'E4'], 'E410'),
+])
+def test_handle_error_notifies_listeners(select_list, ignore_list, error_code):
+ """Verify that error codes notify the listener trie appropriately."""
+ listener_trie = mock.create_autospec(notifier.Notifier, instance=True)
+ formatter = mock.create_autospec(base.BaseFormatter, instance=True)
+ guide = style_guide.StyleGuide(create_options(select=select_list,
+ ignore=ignore_list),
+ listener_trie=listener_trie,
+ formatter=formatter)
+
+ with mock.patch('linecache.getline', return_value=''):
+ guide.handle_error(error_code, 'stdin', 1, 1, 'error found')
+ error = style_guide.Error(error_code, 'stdin', 1, 1, 'error found',
+ None)
+ listener_trie.notify.assert_called_once_with(error_code, error)
+ formatter.handle.assert_called_once_with(error)
+
+
+@pytest.mark.parametrize('select_list,ignore_list,error_code', [
+ (['E111', 'E121'], [], 'E122'),
+ (['E11', 'E12'], [], 'E132'),
+ (['E2', 'E12'], [], 'E321'),
+ (['E2', 'E12'], [], 'E410'),
+ (['E111', 'E121'], ['E2'], 'E122'),
+ (['E11', 'E12'], ['E13'], 'E132'),
+ (['E1', 'E3'], ['E32'], 'E321'),
+ (['E4'], ['E2', 'E12', 'E41'], 'E410'),
+ (['E111', 'E121'], [], 'E112'),
+])
+def test_handle_error_does_not_notify_listeners(select_list, ignore_list,
+ error_code):
+ """Verify that error codes notify the listener trie appropriately."""
+ listener_trie = mock.create_autospec(notifier.Notifier, instance=True)
+ formatter = mock.create_autospec(base.BaseFormatter, instance=True)
+ guide = style_guide.StyleGuide(create_options(select=select_list,
+ ignore=ignore_list),
+ listener_trie=listener_trie,
+ formatter=formatter)
+
+ with mock.patch('linecache.getline', return_value=''):
+ guide.handle_error(error_code, 'stdin', 1, 1, 'error found')
+ assert listener_trie.notify.called is False
+ assert formatter.handle.called is False
diff --git a/tests/unit/test_trie.py b/tests/unit/test_trie.py
new file mode 100644
index 0000000..152b5b6
--- /dev/null
+++ b/tests/unit/test_trie.py
@@ -0,0 +1,122 @@
+"""Unit test for the _trie module."""
+from flake8.plugins import _trie as trie
+
+
+class TestTrie(object):
+ """Collection of tests for the Trie class."""
+
+ def test_traverse_without_data(self):
+ """Verify the behaviour when traversing an empty Trie."""
+ tree = trie.Trie()
+ assert list(tree.traverse()) == []
+
+ def test_traverse_with_data(self):
+ """Verify that traversal of our Trie is depth-first and pre-order."""
+ tree = trie.Trie()
+ tree.add('A', 'A')
+ tree.add('a', 'a')
+ tree.add('AB', 'B')
+ tree.add('Ab', 'b')
+ tree.add('AbC', 'C')
+ tree.add('Abc', 'c')
+ # The trie tree here should look something like
+ #
+ # <root>
+ # / \
+ # A a
+ # / |
+ # B b
+ # / \
+ # C c
+ #
+ # And the traversal should look like:
+ #
+ # A B b C c a
+ expected_order = ['A', 'B', 'b', 'C', 'c', 'a']
+ for expected, actual_node in zip(expected_order, tree.traverse()):
+ assert actual_node.prefix == expected
+
+ def test_find(self):
+ """Exercise the Trie.find method."""
+ tree = trie.Trie()
+ tree.add('A', 'A')
+ tree.add('a', 'a')
+ tree.add('AB', 'AB')
+ tree.add('Ab', 'Ab')
+ tree.add('AbC', 'AbC')
+ tree.add('Abc', 'Abc')
+
+ assert tree.find('AB').data == ['AB']
+ assert tree.find('AbC').data == ['AbC']
+ assert tree.find('A').data == ['A']
+ assert tree.find('X') is None
+
+
+class TestTrieNode(object):
+ """Collection of tests for the TrieNode class."""
+
+ def test_add_child(self):
+ """Verify we add children appropriately."""
+ node = trie.TrieNode('E', 'E is for Eat')
+ assert node.find_prefix('a') is None
+ added = node.add_child('a', 'a is for Apple')
+ assert node.find_prefix('a') is added
+
+ def test_add_child_overrides_previous_child(self):
+ """Verify adding a child will replace the previous child."""
+ node = trie.TrieNode('E', 'E is for Eat', children={
+ 'a': trie.TrieNode('a', 'a is for Apple')
+ })
+ previous = node.find_prefix('a')
+ assert previous is not None
+
+ added = node.add_child('a', 'a is for Ascertain')
+ assert node.find_prefix('a') is added
+
+ def test_find_prefix(self):
+ """Verify we can find a child with the specified prefix."""
+ node = trie.TrieNode('E', 'E is for Eat', children={
+ 'a': trie.TrieNode('a', 'a is for Apple')
+ })
+ child = node.find_prefix('a')
+ assert child is not None
+ assert child.prefix == 'a'
+ assert child.data == 'a is for Apple'
+
+ def test_find_prefix_returns_None_when_no_children_have_the_prefix(self):
+ """Verify we receive None from find_prefix for missing children."""
+ node = trie.TrieNode('E', 'E is for Eat', children={
+ 'a': trie.TrieNode('a', 'a is for Apple')
+ })
+ assert node.find_prefix('b') is None
+
+ def test_traverse_does_nothing_when_a_node_has_no_children(self):
+ """Verify traversing a node with no children does nothing."""
+ node = trie.TrieNode('E', 'E is for Eat')
+ assert list(node.traverse()) == []
+
+ def test_traverse(self):
+ """Verify traversal is depth-first and pre-order."""
+ root = trie.TrieNode(None, None)
+ node = root.add_child('A', 'A')
+ root.add_child('a', 'a')
+ node.add_child('B', 'B')
+ node = node.add_child('b', 'b')
+ node.add_child('C', 'C')
+ node.add_child('c', 'c')
+ # The sub-tree here should look something like
+ #
+ # <root>
+ # / \
+ # A a
+ # / |
+ # B b
+ # / \
+ # C c
+ #
+ # And the traversal should look like:
+ #
+ # A B b C c a
+ expected_order = ['A', 'B', 'b', 'C', 'c', 'a']
+ for expected, actual_node in zip(expected_order, root.traverse()):
+ assert actual_node.prefix == expected
diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
new file mode 100644
index 0000000..5d31e82
--- /dev/null
+++ b/tests/unit/test_utils.py
@@ -0,0 +1,150 @@
+"""Tests for flake8's utils module."""
+import os
+
+import mock
+import pytest
+
+from flake8 import utils
+from flake8.plugins import manager as plugin_manager
+
+RELATIVE_PATHS = ["flake8", "pep8", "pyflakes", "mccabe"]
+
+
+@pytest.mark.parametrize("value,expected", [
+ ("E123,\n\tW234,\n E206", ["E123", "W234", "E206"]),
+ ("E123,W234,E206", ["E123", "W234", "E206"]),
+ (["E123", "W234", "E206"], ["E123", "W234", "E206"]),
+ (["E123", "\n\tW234", "\n E206"], ["E123", "W234", "E206"]),
+])
+def test_parse_comma_separated_list(value, expected):
+ """Verify that similar inputs produce identical outputs."""
+ assert utils.parse_comma_separated_list(value) == expected
+
+
+@pytest.mark.parametrize("value,expected", [
+ ("flake8", "flake8"),
+ ("../flake8", os.path.abspath("../flake8")),
+ ("flake8/", os.path.abspath("flake8")),
+])
+def test_normalize_path(value, expected):
+ """Verify that we normalize paths provided to the tool."""
+ assert utils.normalize_path(value) == expected
+
+
+@pytest.mark.parametrize("value,expected", [
+ ("flake8,pep8,pyflakes,mccabe", ["flake8", "pep8", "pyflakes", "mccabe"]),
+ ("flake8,\n\tpep8,\n pyflakes,\n\n mccabe",
+ ["flake8", "pep8", "pyflakes", "mccabe"]),
+ ("../flake8,../pep8,../pyflakes,../mccabe",
+ [os.path.abspath("../" + p) for p in RELATIVE_PATHS]),
+])
+def test_normalize_paths(value, expected):
+ """Verify we normalize comma-separated paths provided to the tool."""
+ assert utils.normalize_paths(value) == expected
+
+
+def test_is_windows_checks_for_nt():
+ """Verify that we correctly detect Windows."""
+ with mock.patch.object(os, 'name', 'nt'):
+ assert utils.is_windows() is True
+
+ with mock.patch.object(os, 'name', 'posix'):
+ assert utils.is_windows() is False
+
+
+@pytest.mark.parametrize('filename,patterns,expected', [
+ ('foo.py', [], True),
+ ('foo.py', ['*.pyc'], False),
+ ('foo.pyc', ['*.pyc'], True),
+ ('foo.pyc', ['*.swp', '*.pyc', '*.py'], True),
+])
+def test_fnmatch(filename, patterns, expected):
+ """Verify that our fnmatch wrapper works as expected."""
+ assert utils.fnmatch(filename, patterns) is expected
+
+
+def test_fnmatch_returns_the_default_with_empty_default():
+ """The default parameter should be returned when no patterns are given."""
+ sentinel = object()
+ assert utils.fnmatch('file.py', [], default=sentinel) is sentinel
+
+
+def test_filenames_from_a_directory():
+ """Verify that filenames_from walks a directory."""
+ filenames = list(utils.filenames_from('src/flake8/'))
+ assert len(filenames) > 2
+ assert 'src/flake8/__init__.py' in filenames
+
+
+def test_filenames_from_a_directory_with_a_predicate():
+ """Verify that predicates filter filenames_from."""
+ filenames = list(utils.filenames_from(
+ arg='src/flake8/',
+ predicate=lambda filename: filename == 'flake8/__init__.py',
+ ))
+ assert len(filenames) > 2
+ assert 'flake8/__init__.py' not in filenames
+
+
+def test_filenames_from_a_single_file():
+ """Verify that we simply yield that filename."""
+ filenames = list(utils.filenames_from('flake8/__init__.py'))
+
+ assert len(filenames) == 1
+ assert ['flake8/__init__.py'] == filenames
+
+
+def test_parameters_for_class_plugin():
+ """Verify that we can retrieve the parameters for a class plugin."""
+ class FakeCheck(object):
+ def __init__(self, tree):
+ pass
+
+ plugin = plugin_manager.Plugin('plugin-name', object())
+ plugin._plugin = FakeCheck
+ assert utils.parameters_for(plugin) == ['tree']
+
+
+def test_parameters_for_function_plugin():
+ """Verify that we retrieve the parameters for a function plugin."""
+ def fake_plugin(physical_line, self, tree):
+ pass
+
+ plugin = plugin_manager.Plugin('plugin-name', object())
+ plugin._plugin = fake_plugin
+ assert utils.parameters_for(plugin) == ['physical_line', 'self', 'tree']
+
+
+def read_diff_file(filename):
+ """Read the diff file in its entirety."""
+ with open(filename, 'r') as fd:
+ content = fd.read()
+ return content
+
+
+SINGLE_FILE_DIFF = read_diff_file('tests/fixtures/diffs/single_file_diff')
+SINGLE_FILE_INFO = {
+ 'flake8/utils.py': set(range(75, 83)).union(set(range(84, 94))),
+}
+TWO_FILE_DIFF = read_diff_file('tests/fixtures/diffs/two_file_diff')
+TWO_FILE_INFO = {
+ 'flake8/utils.py': set(range(75, 83)).union(set(range(84, 94))),
+ 'tests/unit/test_utils.py': set(range(115, 128)),
+}
+MULTI_FILE_DIFF = read_diff_file('tests/fixtures/diffs/multi_file_diff')
+MULTI_FILE_INFO = {
+ 'flake8/utils.py': set(range(75, 83)).union(set(range(84, 94))),
+ 'tests/unit/test_utils.py': set(range(115, 129)),
+ 'tests/fixtures/diffs/single_file_diff': set(range(1, 28)),
+ 'tests/fixtures/diffs/two_file_diff': set(range(1, 46)),
+}
+
+
+@pytest.mark.parametrize("diff, parsed_diff", [
+ (SINGLE_FILE_DIFF, SINGLE_FILE_INFO),
+ (TWO_FILE_DIFF, TWO_FILE_INFO),
+ (MULTI_FILE_DIFF, MULTI_FILE_INFO),
+])
+def test_parse_unified_diff(diff, parsed_diff):
+ """Verify that what we parse from a diff matches expectations."""
+ assert utils.parse_unified_diff(diff) == parsed_diff