Home | History | Annotate | Download | only in style
      1 # -*- coding: utf-8; -*-
      2 #
      3 # Copyright (C) 2009 Google Inc. All rights reserved.
      4 # Copyright (C) 2009 Torch Mobile Inc.
      5 # Copyright (C) 2009 Apple Inc. All rights reserved.
      6 # Copyright (C) 2010 Chris Jerdonek (chris.jerdonek (at] gmail.com)
      7 #
      8 # Redistribution and use in source and binary forms, with or without
      9 # modification, are permitted provided that the following conditions are
     10 # met:
     11 #
     12 #    * Redistributions of source code must retain the above copyright
     13 # notice, this list of conditions and the following disclaimer.
     14 #    * Redistributions in binary form must reproduce the above
     15 # copyright notice, this list of conditions and the following disclaimer
     16 # in the documentation and/or other materials provided with the
     17 # distribution.
     18 #    * Neither the name of Google Inc. nor the names of its
     19 # contributors may be used to endorse or promote products derived from
     20 # this software without specific prior written permission.
     21 #
     22 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     23 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     24 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     25 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     26 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     27 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     28 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     29 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     30 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     31 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     32 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     33 
     34 """Unit tests for style.py."""
     35 
     36 import logging
     37 import os
     38 import webkitpy.thirdparty.unittest2 as unittest
     39 
     40 import checker as style
     41 from webkitpy.common.system.logtesting import LogTesting, TestLogStream
     42 from checker import _BASE_FILTER_RULES
     43 from checker import _MAX_REPORTS_PER_CATEGORY
     44 from checker import _PATH_RULES_SPECIFIER as PATH_RULES_SPECIFIER
     45 from checker import _all_categories
     46 from checker import check_webkit_style_configuration
     47 from checker import check_webkit_style_parser
     48 from checker import configure_logging
     49 from checker import CheckerDispatcher
     50 from checker import ProcessorBase
     51 from checker import StyleProcessor
     52 from checker import StyleProcessorConfiguration
     53 from checkers.cpp import CppChecker
     54 from checkers.jsonchecker import JSONChecker
     55 from checkers.python import PythonChecker
     56 from checkers.text import TextChecker
     57 from checkers.xml import XMLChecker
     58 from error_handlers import DefaultStyleErrorHandler
     59 from filter import validate_filter_rules
     60 from filter import FilterConfiguration
     61 from optparser import ArgumentParser
     62 from optparser import CommandOptionValues
     63 from webkitpy.common.system.logtesting import LoggingTestCase
     64 from webkitpy.style.filereader import TextFileReader
     65 
     66 
     67 class ConfigureLoggingTestBase(unittest.TestCase):
     68 
     69     """Base class for testing configure_logging().
     70 
     71     Sub-classes should implement:
     72 
     73       is_verbose: The is_verbose value to pass to configure_logging().
     74 
     75     """
     76 
     77     def setUp(self):
     78         is_verbose = self.is_verbose
     79 
     80         log_stream = TestLogStream(self)
     81 
     82         # Use a logger other than the root logger or one prefixed with
     83         # webkit so as not to conflict with test-webkitpy logging.
     84         logger = logging.getLogger("unittest")
     85 
     86         # Configure the test logger not to pass messages along to the
     87         # root logger.  This prevents test messages from being
     88         # propagated to loggers used by test-webkitpy logging (e.g.
     89         # the root logger).
     90         logger.propagate = False
     91 
     92         self._handlers = configure_logging(stream=log_stream, logger=logger,
     93                                            is_verbose=is_verbose)
     94         self._log = logger
     95         self._log_stream = log_stream
     96 
     97     def tearDown(self):
     98         """Reset logging to its original state.
     99 
    100         This method ensures that the logging configuration set up
    101         for a unit test does not affect logging in other unit tests.
    102 
    103         """
    104         logger = self._log
    105         for handler in self._handlers:
    106             logger.removeHandler(handler)
    107 
    108     def assert_log_messages(self, messages):
    109         """Assert that the logged messages equal the given messages."""
    110         self._log_stream.assertMessages(messages)
    111 
    112 
    113 class ConfigureLoggingTest(ConfigureLoggingTestBase):
    114 
    115     """Tests the configure_logging() function."""
    116 
    117     is_verbose = False
    118 
    119     def test_warning_message(self):
    120         self._log.warn("test message")
    121         self.assert_log_messages(["WARNING: test message\n"])
    122 
    123     def test_below_warning_message(self):
    124         # We test the boundary case of a logging level equal to 29.
    125         # In practice, we will probably only be calling log.info(),
    126         # which corresponds to a logging level of 20.
    127         level = logging.WARNING - 1  # Equals 29.
    128         self._log.log(level, "test message")
    129         self.assert_log_messages(["test message\n"])
    130 
    131     def test_debug_message(self):
    132         self._log.debug("test message")
    133         self.assert_log_messages([])
    134 
    135     def test_two_messages(self):
    136         self._log.info("message1")
    137         self._log.info("message2")
    138         self.assert_log_messages(["message1\n", "message2\n"])
    139 
    140 
    141 class ConfigureLoggingVerboseTest(ConfigureLoggingTestBase):
    142 
    143     """Tests the configure_logging() function with is_verbose True."""
    144 
    145     is_verbose = True
    146 
    147     def test_debug_message(self):
    148         self._log.debug("test message")
    149         self.assert_log_messages(["unittest: DEBUG    test message\n"])
    150 
    151 
    152 class GlobalVariablesTest(unittest.TestCase):
    153 
    154     """Tests validity of the global variables."""
    155 
    156     def _all_categories(self):
    157         return _all_categories()
    158 
    159     def defaults(self):
    160         return style._check_webkit_style_defaults()
    161 
    162     def test_webkit_base_filter_rules(self):
    163         base_filter_rules = _BASE_FILTER_RULES
    164         defaults = self.defaults()
    165         already_seen = []
    166         validate_filter_rules(base_filter_rules, self._all_categories())
    167         # Also do some additional checks.
    168         for rule in base_filter_rules:
    169             # Check no leading or trailing white space.
    170             self.assertEqual(rule, rule.strip())
    171             # All categories are on by default, so defaults should
    172             # begin with -.
    173             self.assertTrue(rule.startswith('-'))
    174             # Check no rule occurs twice.
    175             self.assertNotIn(rule, already_seen)
    176             already_seen.append(rule)
    177 
    178     def test_defaults(self):
    179         """Check that default arguments are valid."""
    180         default_options = self.defaults()
    181 
    182         # FIXME: We should not need to call parse() to determine
    183         #        whether the default arguments are valid.
    184         parser = ArgumentParser(all_categories=self._all_categories(),
    185                                 base_filter_rules=[],
    186                                 default_options=default_options)
    187         # No need to test the return value here since we test parse()
    188         # on valid arguments elsewhere.
    189         #
    190         # The default options are valid: no error or SystemExit.
    191         parser.parse(args=[])
    192 
    193     def test_path_rules_specifier(self):
    194         all_categories = self._all_categories()
    195         for (sub_paths, path_rules) in PATH_RULES_SPECIFIER:
    196             validate_filter_rules(path_rules, self._all_categories())
    197 
    198         config = FilterConfiguration(path_specific=PATH_RULES_SPECIFIER)
    199 
    200         def assertCheck(path, category):
    201             """Assert that the given category should be checked."""
    202             message = ('Should check category "%s" for path "%s".'
    203                        % (category, path))
    204             self.assertTrue(config.should_check(category, path))
    205 
    206         def assertNoCheck(path, category):
    207             """Assert that the given category should not be checked."""
    208             message = ('Should not check category "%s" for path "%s".'
    209                        % (category, path))
    210             self.assertFalse(config.should_check(category, path), message)
    211 
    212         assertCheck("random_path.cpp",
    213                     "build/include")
    214         assertCheck("random_path.cpp",
    215                     "readability/naming")
    216         assertNoCheck("Source/WebKit/gtk/webkit/webkit.h",
    217                       "readability/naming")
    218         assertNoCheck("Tools/DumpRenderTree/gtk/DumpRenderTree.cpp",
    219                       "readability/null")
    220         assertNoCheck("Source/WebKit/efl/ewk/ewk_view.h",
    221                       "readability/naming")
    222         assertNoCheck("Source/WebCore/css/CSSParser.cpp",
    223                       "readability/naming")
    224 
    225         # Test if Qt exceptions are indeed working
    226         assertCheck("Source/WebKit/qt/WidgetApi/qwebpage.cpp",
    227                     "readability/braces")
    228         assertCheck("Source/WebKit/qt/tests/qwebelement/tst_qwebelement.cpp",
    229                     "readability/braces")
    230         assertCheck("Source/WebKit/qt/declarative/platformplugin/WebPlugin.cpp",
    231                     "readability/braces")
    232         assertCheck("Source/WebKit/qt/examples/platformplugin/WebPlugin.cpp",
    233                     "readability/braces")
    234         assertNoCheck("Source/WebKit/qt/WidgetApi/qwebpage.cpp",
    235                       "readability/naming")
    236         assertNoCheck("Source/WebKit/qt/tests/qwebelement/tst_qwebelement.cpp",
    237                       "readability/naming")
    238         assertNoCheck("Source/WebKit/qt/declarative/platformplugin/WebPlugin.cpp",
    239                       "readability/naming")
    240         assertNoCheck("Source/WebKit/qt/examples/platformplugin/WebPlugin.cpp",
    241                       "readability/naming")
    242 
    243         assertNoCheck("Tools/MiniBrowser/qt/UrlLoader.cpp",
    244                     "build/include")
    245 
    246         assertNoCheck("Source/WebKit2/UIProcess/API/qt",
    247                     "readability/parameter_name")
    248 
    249         assertNoCheck("Source/WebCore/ForwardingHeaders/debugger/Debugger.h",
    250                       "build/header_guard")
    251 
    252         assertNoCheck("Source/WebCore/platform/graphics/gstreamer/VideoSinkGStreamer.cpp",
    253                       "readability/naming")
    254 
    255         # Third-party Python code: webkitpy/thirdparty
    256         path = "Tools/Scripts/webkitpy/thirdparty/mock.py"
    257         assertNoCheck(path, "build/include")
    258         assertNoCheck(path, "pep8/E401")  # A random pep8 category.
    259         assertCheck(path, "pep8/W191")
    260         assertCheck(path, "pep8/W291")
    261         assertCheck(path, "whitespace/carriage_return")
    262 
    263         # Test if the exception for GDBInterface.cpp is in place.
    264         assertNoCheck("Source/JavaScriptCore/jit/GDBInterface.cpp",
    265                       "readability/naming")
    266 
    267         # Javascript keywords.
    268         assertCheck("Source/JavaScriptCore/parser/Keywords.table", "whitespace/carriage_return")
    269 
    270     def test_max_reports_per_category(self):
    271         """Check that _MAX_REPORTS_PER_CATEGORY is valid."""
    272         all_categories = self._all_categories()
    273         for category in _MAX_REPORTS_PER_CATEGORY.iterkeys():
    274             self.assertIn(category, all_categories,
    275                           'Key "%s" is not a category' % category)
    276 
    277 
    278 class CheckWebKitStyleFunctionTest(unittest.TestCase):
    279 
    280     """Tests the functions with names of the form check_webkit_style_*."""
    281 
    282     def test_check_webkit_style_configuration(self):
    283         # Exercise the code path to make sure the function does not error out.
    284         option_values = CommandOptionValues()
    285         configuration = check_webkit_style_configuration(option_values)
    286 
    287     def test_check_webkit_style_parser(self):
    288         # Exercise the code path to make sure the function does not error out.
    289         parser = check_webkit_style_parser()
    290 
    291 
    292 class CheckerDispatcherSkipTest(unittest.TestCase):
    293 
    294     """Tests the "should skip" methods of the CheckerDispatcher class."""
    295 
    296     def setUp(self):
    297         self._dispatcher = CheckerDispatcher()
    298 
    299     def test_should_skip_with_warning(self):
    300         """Test should_skip_with_warning()."""
    301         # Check skipped files.
    302         paths_to_skip = [
    303            "Source/WebKit/gtk/tests/testatk.c",
    304            "Source/WebKit2/UIProcess/API/gtk/webkit2.h",
    305            "Source/WebKit2/UIProcess/API/gtk/WebKitWebView.h",
    306            "Source/WebKit2/UIProcess/API/gtk/WebKitLoader.h",
    307             ]
    308 
    309         for path in paths_to_skip:
    310             self.assertTrue(self._dispatcher.should_skip_with_warning(path),
    311                             "Checking: " + path)
    312 
    313         # Verify that some files are not skipped.
    314         paths_not_to_skip = [
    315            "foo.txt",
    316            "Source/WebKit2/UIProcess/API/gtk/HelperClass.cpp",
    317            "Source/WebKit2/UIProcess/API/gtk/HelperClass.h",
    318            "Source/WebKit2/UIProcess/API/gtk/WebKitWebView.cpp",
    319            "Source/WebKit2/UIProcess/API/gtk/WebKitWebViewPrivate.h",
    320            "Source/WebKit2/UIProcess/API/gtk/tests/WebViewTest.cpp",
    321            "Source/WebKit2/UIProcess/API/gtk/tests/WebViewTest.h",
    322             ]
    323 
    324         for path in paths_not_to_skip:
    325             self.assertFalse(self._dispatcher.should_skip_with_warning(path))
    326 
    327     def _assert_should_skip_without_warning(self, path, is_checker_none,
    328                                             expected):
    329         # Check the file type before asserting the return value.
    330         checker = self._dispatcher.dispatch(file_path=path,
    331                                             handle_style_error=None,
    332                                             min_confidence=3)
    333         message = 'while checking: %s' % path
    334         self.assertEqual(checker is None, is_checker_none, message)
    335         self.assertEqual(self._dispatcher.should_skip_without_warning(path),
    336                           expected, message)
    337 
    338     def test_should_skip_without_warning__true(self):
    339         """Test should_skip_without_warning() for True return values."""
    340         # Check a file with NONE file type.
    341         path = 'foo.asdf'  # Non-sensical file extension.
    342         self._assert_should_skip_without_warning(path,
    343                                                  is_checker_none=True,
    344                                                  expected=True)
    345 
    346         # Check files with non-NONE file type.  These examples must be
    347         # drawn from the _SKIPPED_FILES_WITHOUT_WARNING configuration
    348         # variable.
    349         path = os.path.join('LayoutTests', 'foo.txt')
    350         self._assert_should_skip_without_warning(path,
    351                                                  is_checker_none=False,
    352                                                  expected=True)
    353 
    354     def test_should_skip_without_warning__false(self):
    355         """Test should_skip_without_warning() for False return values."""
    356         paths = ['foo.txt',
    357                  os.path.join('LayoutTests', 'TestExpectations'),
    358         ]
    359 
    360         for path in paths:
    361             self._assert_should_skip_without_warning(path,
    362                                                      is_checker_none=False,
    363                                                      expected=False)
    364 
    365 
    366 class CheckerDispatcherCarriageReturnTest(unittest.TestCase):
    367     def test_should_check_and_strip_carriage_returns(self):
    368         files = {
    369             'foo.txt': True,
    370             'foo.cpp': True,
    371             'foo.vcproj': False,
    372             'foo.vsprops': False,
    373         }
    374 
    375         dispatcher = CheckerDispatcher()
    376         for file_path, expected_result in files.items():
    377             self.assertEqual(dispatcher.should_check_and_strip_carriage_returns(file_path), expected_result, 'Checking: %s' % file_path)
    378 
    379 
    380 class CheckerDispatcherDispatchTest(unittest.TestCase):
    381 
    382     """Tests dispatch() method of CheckerDispatcher class."""
    383 
    384     def dispatch(self, file_path):
    385         """Call dispatch() with the given file path."""
    386         dispatcher = CheckerDispatcher()
    387         self.mock_handle_style_error = DefaultStyleErrorHandler('', None, None, [])
    388         checker = dispatcher.dispatch(file_path,
    389                                       self.mock_handle_style_error,
    390                                       min_confidence=3)
    391         return checker
    392 
    393     def assert_checker_none(self, file_path):
    394         """Assert that the dispatched checker is None."""
    395         checker = self.dispatch(file_path)
    396         self.assertIsNone(checker, 'Checking: "%s"' % file_path)
    397 
    398     def assert_checker(self, file_path, expected_class):
    399         """Assert the type of the dispatched checker."""
    400         checker = self.dispatch(file_path)
    401         got_class = checker.__class__
    402         self.assertEqual(got_class, expected_class,
    403                           'For path "%(file_path)s" got %(got_class)s when '
    404                           "expecting %(expected_class)s."
    405                           % {"file_path": file_path,
    406                              "got_class": got_class,
    407                              "expected_class": expected_class})
    408 
    409     def assert_checker_cpp(self, file_path):
    410         """Assert that the dispatched checker is a CppChecker."""
    411         self.assert_checker(file_path, CppChecker)
    412 
    413     def assert_checker_json(self, file_path):
    414         """Assert that the dispatched checker is a JSONChecker."""
    415         self.assert_checker(file_path, JSONChecker)
    416 
    417     def assert_checker_python(self, file_path):
    418         """Assert that the dispatched checker is a PythonChecker."""
    419         self.assert_checker(file_path, PythonChecker)
    420 
    421     def assert_checker_text(self, file_path):
    422         """Assert that the dispatched checker is a TextChecker."""
    423         self.assert_checker(file_path, TextChecker)
    424 
    425     def assert_checker_xml(self, file_path):
    426         """Assert that the dispatched checker is a XMLChecker."""
    427         self.assert_checker(file_path, XMLChecker)
    428 
    429     def test_cpp_paths(self):
    430         """Test paths that should be checked as C++."""
    431         paths = [
    432             "-",
    433             "foo.c",
    434             "foo.cpp",
    435             "foo.h",
    436             ]
    437 
    438         for path in paths:
    439             self.assert_checker_cpp(path)
    440 
    441         # Check checker attributes on a typical input.
    442         file_base = "foo"
    443         file_extension = "c"
    444         file_path = file_base + "." + file_extension
    445         self.assert_checker_cpp(file_path)
    446         checker = self.dispatch(file_path)
    447         self.assertEqual(checker.file_extension, file_extension)
    448         self.assertEqual(checker.file_path, file_path)
    449         self.assertEqual(checker.handle_style_error, self.mock_handle_style_error)
    450         self.assertEqual(checker.min_confidence, 3)
    451         # Check "-" for good measure.
    452         file_base = "-"
    453         file_extension = ""
    454         file_path = file_base
    455         self.assert_checker_cpp(file_path)
    456         checker = self.dispatch(file_path)
    457         self.assertEqual(checker.file_extension, file_extension)
    458         self.assertEqual(checker.file_path, file_path)
    459 
    460     def test_json_paths(self):
    461         """Test paths that should be checked as JSON."""
    462         paths = [
    463            "Source/WebCore/inspector/Inspector.json",
    464            "Tools/BuildSlaveSupport/build.webkit.org-config/config.json",
    465         ]
    466 
    467         for path in paths:
    468             self.assert_checker_json(path)
    469 
    470         # Check checker attributes on a typical input.
    471         file_base = "foo"
    472         file_extension = "json"
    473         file_path = file_base + "." + file_extension
    474         self.assert_checker_json(file_path)
    475         checker = self.dispatch(file_path)
    476         self.assertEqual(checker._handle_style_error,
    477                           self.mock_handle_style_error)
    478 
    479     def test_python_paths(self):
    480         """Test paths that should be checked as Python."""
    481         paths = [
    482            "foo.py",
    483            "Tools/Scripts/modules/text_style.py",
    484         ]
    485 
    486         for path in paths:
    487             self.assert_checker_python(path)
    488 
    489         # Check checker attributes on a typical input.
    490         file_base = "foo"
    491         file_extension = "css"
    492         file_path = file_base + "." + file_extension
    493         self.assert_checker_text(file_path)
    494         checker = self.dispatch(file_path)
    495         self.assertEqual(checker.file_path, file_path)
    496         self.assertEqual(checker.handle_style_error,
    497                           self.mock_handle_style_error)
    498 
    499     def test_text_paths(self):
    500         """Test paths that should be checked as text."""
    501         paths = [
    502            "foo.ac",
    503            "foo.cc",
    504            "foo.cgi",
    505            "foo.css",
    506            "foo.exp",
    507            "foo.flex",
    508            "foo.gyp",
    509            "foo.gypi",
    510            "foo.html",
    511            "foo.idl",
    512            "foo.in",
    513            "foo.js",
    514            "foo.mm",
    515            "foo.php",
    516            "foo.pl",
    517            "foo.pm",
    518            "foo.pri",
    519            "foo.pro",
    520            "foo.rb",
    521            "foo.sh",
    522            "foo.txt",
    523            "foo.wm",
    524            "foo.xhtml",
    525            "foo.y",
    526            os.path.join("Source", "WebCore", "inspector", "front-end", "inspector.js"),
    527            os.path.join("Tools", "Scripts", "check-webkit-style"),
    528         ]
    529 
    530         for path in paths:
    531             self.assert_checker_text(path)
    532 
    533         # Check checker attributes on a typical input.
    534         file_base = "foo"
    535         file_extension = "css"
    536         file_path = file_base + "." + file_extension
    537         self.assert_checker_text(file_path)
    538         checker = self.dispatch(file_path)
    539         self.assertEqual(checker.file_path, file_path)
    540         self.assertEqual(checker.handle_style_error, self.mock_handle_style_error)
    541 
    542     def test_xml_paths(self):
    543         """Test paths that should be checked as XML."""
    544         paths = [
    545            "Source/WebCore/WebCore.vcproj/WebCore.vcproj",
    546            "WebKitLibraries/win/tools/vsprops/common.vsprops",
    547         ]
    548 
    549         for path in paths:
    550             self.assert_checker_xml(path)
    551 
    552         # Check checker attributes on a typical input.
    553         file_base = "foo"
    554         file_extension = "vcproj"
    555         file_path = file_base + "." + file_extension
    556         self.assert_checker_xml(file_path)
    557         checker = self.dispatch(file_path)
    558         self.assertEqual(checker._handle_style_error,
    559                           self.mock_handle_style_error)
    560 
    561     def test_none_paths(self):
    562         """Test paths that have no file type.."""
    563         paths = [
    564            "Makefile",
    565            "foo.asdf",  # Non-sensical file extension.
    566            "foo.exe",
    567             ]
    568 
    569         for path in paths:
    570             self.assert_checker_none(path)
    571 
    572 
    573 class StyleProcessorConfigurationTest(unittest.TestCase):
    574 
    575     """Tests the StyleProcessorConfiguration class."""
    576 
    577     def setUp(self):
    578         self._error_messages = []
    579         """The messages written to _mock_stderr_write() of this class."""
    580 
    581     def _mock_stderr_write(self, message):
    582         self._error_messages.append(message)
    583 
    584     def _style_checker_configuration(self, output_format="vs7"):
    585         """Return a StyleProcessorConfiguration instance for testing."""
    586         base_rules = ["-whitespace", "+whitespace/tab"]
    587         filter_configuration = FilterConfiguration(base_rules=base_rules)
    588 
    589         return StyleProcessorConfiguration(
    590                    filter_configuration=filter_configuration,
    591                    max_reports_per_category={"whitespace/newline": 1},
    592                    min_confidence=3,
    593                    output_format=output_format,
    594                    stderr_write=self._mock_stderr_write)
    595 
    596     def test_init(self):
    597         """Test the __init__() method."""
    598         configuration = self._style_checker_configuration()
    599 
    600         # Check that __init__ sets the "public" data attributes correctly.
    601         self.assertEqual(configuration.max_reports_per_category,
    602                           {"whitespace/newline": 1})
    603         self.assertEqual(configuration.stderr_write, self._mock_stderr_write)
    604         self.assertEqual(configuration.min_confidence, 3)
    605 
    606     def test_is_reportable(self):
    607         """Test the is_reportable() method."""
    608         config = self._style_checker_configuration()
    609 
    610         self.assertTrue(config.is_reportable("whitespace/tab", 3, "foo.txt"))
    611 
    612         # Test the confidence check code path by varying the confidence.
    613         self.assertFalse(config.is_reportable("whitespace/tab", 2, "foo.txt"))
    614 
    615         # Test the category check code path by varying the category.
    616         self.assertFalse(config.is_reportable("whitespace/line", 4, "foo.txt"))
    617 
    618     def _call_write_style_error(self, output_format):
    619         config = self._style_checker_configuration(output_format=output_format)
    620         config.write_style_error(category="whitespace/tab",
    621                                  confidence_in_error=5,
    622                                  file_path="foo.h",
    623                                  line_number=100,
    624                                  message="message")
    625 
    626     def test_write_style_error_emacs(self):
    627         """Test the write_style_error() method."""
    628         self._call_write_style_error("emacs")
    629         self.assertEqual(self._error_messages,
    630                           ["foo.h:100:  message  [whitespace/tab] [5]\n"])
    631 
    632     def test_write_style_error_vs7(self):
    633         """Test the write_style_error() method."""
    634         self._call_write_style_error("vs7")
    635         self.assertEqual(self._error_messages,
    636                           ["foo.h(100):  message  [whitespace/tab] [5]\n"])
    637 
    638 
    639 class StyleProcessor_EndToEndTest(LoggingTestCase):
    640 
    641     """Test the StyleProcessor class with an emphasis on end-to-end tests."""
    642 
    643     def setUp(self):
    644         LoggingTestCase.setUp(self)
    645         self._messages = []
    646 
    647     def _mock_stderr_write(self, message):
    648         """Save a message so it can later be asserted."""
    649         self._messages.append(message)
    650 
    651     def test_init(self):
    652         """Test __init__ constructor."""
    653         configuration = StyleProcessorConfiguration(
    654                             filter_configuration=FilterConfiguration(),
    655                             max_reports_per_category={},
    656                             min_confidence=3,
    657                             output_format="vs7",
    658                             stderr_write=self._mock_stderr_write)
    659         processor = StyleProcessor(configuration)
    660 
    661         self.assertEqual(processor.error_count, 0)
    662         self.assertEqual(self._messages, [])
    663 
    664     def test_process(self):
    665         configuration = StyleProcessorConfiguration(
    666                             filter_configuration=FilterConfiguration(),
    667                             max_reports_per_category={},
    668                             min_confidence=3,
    669                             output_format="vs7",
    670                             stderr_write=self._mock_stderr_write)
    671         processor = StyleProcessor(configuration)
    672 
    673         processor.process(lines=['line1', 'Line with tab:\t'],
    674                           file_path='foo.txt')
    675         self.assertEqual(processor.error_count, 1)
    676         expected_messages = ['foo.txt(2):  Line contains tab character.  '
    677                              '[whitespace/tab] [5]\n']
    678         self.assertEqual(self._messages, expected_messages)
    679 
    680 
    681 class StyleProcessor_CodeCoverageTest(LoggingTestCase):
    682 
    683     """Test the StyleProcessor class with an emphasis on code coverage.
    684 
    685     This class makes heavy use of mock objects.
    686 
    687     """
    688 
    689     class MockDispatchedChecker(object):
    690 
    691         """A mock checker dispatched by the MockDispatcher."""
    692 
    693         def __init__(self, file_path, min_confidence, style_error_handler):
    694             self.file_path = file_path
    695             self.min_confidence = min_confidence
    696             self.style_error_handler = style_error_handler
    697 
    698         def check(self, lines):
    699             self.lines = lines
    700 
    701     class MockDispatcher(object):
    702 
    703         """A mock CheckerDispatcher class."""
    704 
    705         def __init__(self):
    706             self.dispatched_checker = None
    707 
    708         def should_skip_with_warning(self, file_path):
    709             return file_path.endswith('skip_with_warning.txt')
    710 
    711         def should_skip_without_warning(self, file_path):
    712             return file_path.endswith('skip_without_warning.txt')
    713 
    714         def should_check_and_strip_carriage_returns(self, file_path):
    715             return not file_path.endswith('carriage_returns_allowed.txt')
    716 
    717         def dispatch(self, file_path, style_error_handler, min_confidence):
    718             if file_path.endswith('do_not_process.txt'):
    719                 return None
    720 
    721             checker = StyleProcessor_CodeCoverageTest.MockDispatchedChecker(
    722                           file_path,
    723                           min_confidence,
    724                           style_error_handler)
    725 
    726             # Save the dispatched checker so the current test case has a
    727             # way to access and check it.
    728             self.dispatched_checker = checker
    729 
    730             return checker
    731 
    732     def setUp(self):
    733         LoggingTestCase.setUp(self)
    734         # We can pass an error-message swallower here because error message
    735         # output is tested instead in the end-to-end test case above.
    736         configuration = StyleProcessorConfiguration(
    737                             filter_configuration=FilterConfiguration(),
    738                             max_reports_per_category={"whitespace/newline": 1},
    739                             min_confidence=3,
    740                             output_format="vs7",
    741                             stderr_write=self._swallow_stderr_message)
    742 
    743         mock_carriage_checker_class = self._create_carriage_checker_class()
    744         mock_dispatcher = self.MockDispatcher()
    745         # We do not need to use a real incrementer here because error-count
    746         # incrementing is tested instead in the end-to-end test case above.
    747         mock_increment_error_count = self._do_nothing
    748 
    749         processor = StyleProcessor(configuration=configuration,
    750                         mock_carriage_checker_class=mock_carriage_checker_class,
    751                         mock_dispatcher=mock_dispatcher,
    752                         mock_increment_error_count=mock_increment_error_count)
    753 
    754         self._configuration = configuration
    755         self._mock_dispatcher = mock_dispatcher
    756         self._processor = processor
    757 
    758     def _do_nothing(self):
    759         # We provide this function so the caller can pass it to the
    760         # StyleProcessor constructor.  This lets us assert the equality of
    761         # the DefaultStyleErrorHandler instance generated by the process()
    762         # method with an expected instance.
    763         pass
    764 
    765     def _swallow_stderr_message(self, message):
    766         """Swallow a message passed to stderr.write()."""
    767         # This is a mock stderr.write() for passing to the constructor
    768         # of the StyleProcessorConfiguration class.
    769         pass
    770 
    771     def _create_carriage_checker_class(self):
    772 
    773         # Create a reference to self with a new name so its name does not
    774         # conflict with the self introduced below.
    775         test_case = self
    776 
    777         class MockCarriageChecker(object):
    778 
    779             """A mock carriage-return checker."""
    780 
    781             def __init__(self, style_error_handler):
    782                 self.style_error_handler = style_error_handler
    783 
    784                 # This gives the current test case access to the
    785                 # instantiated carriage checker.
    786                 test_case.carriage_checker = self
    787 
    788             def check(self, lines):
    789                 # Save the lines so the current test case has a way to access
    790                 # and check them.
    791                 self.lines = lines
    792 
    793                 return lines
    794 
    795         return MockCarriageChecker
    796 
    797     def test_should_process__skip_without_warning(self):
    798         """Test should_process() for a skip-without-warning file."""
    799         file_path = "foo/skip_without_warning.txt"
    800 
    801         self.assertFalse(self._processor.should_process(file_path))
    802 
    803     def test_should_process__skip_with_warning(self):
    804         """Test should_process() for a skip-with-warning file."""
    805         file_path = "foo/skip_with_warning.txt"
    806 
    807         self.assertFalse(self._processor.should_process(file_path))
    808 
    809         self.assertLog(['WARNING: File exempt from style guide. '
    810                         'Skipping: "foo/skip_with_warning.txt"\n'])
    811 
    812     def test_should_process__true_result(self):
    813         """Test should_process() for a file that should be processed."""
    814         file_path = "foo/skip_process.txt"
    815 
    816         self.assertTrue(self._processor.should_process(file_path))
    817 
    818     def test_process__checker_dispatched(self):
    819         """Test the process() method for a path with a dispatched checker."""
    820         file_path = 'foo.txt'
    821         lines = ['line1', 'line2']
    822         line_numbers = [100]
    823 
    824         expected_error_handler = DefaultStyleErrorHandler(
    825             configuration=self._configuration,
    826             file_path=file_path,
    827             increment_error_count=self._do_nothing,
    828             line_numbers=line_numbers)
    829 
    830         self._processor.process(lines=lines,
    831                                 file_path=file_path,
    832                                 line_numbers=line_numbers)
    833 
    834         # Check that the carriage-return checker was instantiated correctly
    835         # and was passed lines correctly.
    836         carriage_checker = self.carriage_checker
    837         self.assertEqual(carriage_checker.style_error_handler,
    838                           expected_error_handler)
    839         self.assertEqual(carriage_checker.lines, ['line1', 'line2'])
    840 
    841         # Check that the style checker was dispatched correctly and was
    842         # passed lines correctly.
    843         checker = self._mock_dispatcher.dispatched_checker
    844         self.assertEqual(checker.file_path, 'foo.txt')
    845         self.assertEqual(checker.min_confidence, 3)
    846         self.assertEqual(checker.style_error_handler, expected_error_handler)
    847 
    848         self.assertEqual(checker.lines, ['line1', 'line2'])
    849 
    850     def test_process__no_checker_dispatched(self):
    851         """Test the process() method for a path with no dispatched checker."""
    852         path = os.path.join('foo', 'do_not_process.txt')
    853         self.assertRaises(AssertionError, self._processor.process,
    854                           lines=['line1', 'line2'], file_path=path,
    855                           line_numbers=[100])
    856 
    857     def test_process__carriage_returns_not_stripped(self):
    858         """Test that carriage returns aren't stripped from files that are allowed to contain them."""
    859         file_path = 'carriage_returns_allowed.txt'
    860         lines = ['line1\r', 'line2\r']
    861         line_numbers = [100]
    862         self._processor.process(lines=lines,
    863                                 file_path=file_path,
    864                                 line_numbers=line_numbers)
    865         # The carriage return checker should never have been invoked, and so
    866         # should not have saved off any lines.
    867         self.assertFalse(hasattr(self.carriage_checker, 'lines'))
    868