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