diff options
Diffstat (limited to 'tests')
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 |
