2011-04-22 Yi Shen <yi.4.shen@nokia.com>
[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         assertCheck("Source/WebKit/qt/symbian/platformplugin/WebPlugin.cpp",
241                     "readability/braces")
242         assertNoCheck("Source/JavaScriptCore/qt/api/qscriptengine.cpp",
243                       "readability/naming")
244         assertNoCheck("Source/JavaScriptCore/qt/benchmarks"
245                       "/qscriptengine/tst_qscriptengine.cpp",
246                       "readability/naming")
247         assertNoCheck("Source/WebKit/qt/Api/qwebpage.cpp",
248                       "readability/naming")
249         assertNoCheck("Source/WebKit/qt/tests/qwebelement/tst_qwebelement.cpp",
250                       "readability/naming")
251         assertNoCheck("Source/WebKit/qt/declarative/platformplugin/WebPlugin.cpp",
252                       "readability/naming")
253         assertNoCheck("Source/WebKit/qt/examples/platformplugin/WebPlugin.cpp",
254                       "readability/naming")
255         assertNoCheck("Source/WebKit/qt/symbian/platformplugin/WebPlugin.cpp",
256                       "build/header_guard")
257
258         assertNoCheck("Tools/MiniBrowser/qt/UrlLoader.cpp",
259                     "build/include")
260
261         assertNoCheck("Source/WebCore/ForwardingHeaders/debugger/Debugger.h",
262                       "build/header_guard")
263
264         # Third-party Python code: webkitpy/thirdparty
265         path = "Tools/Scripts/webkitpy/thirdparty/mock.py"
266         assertNoCheck(path, "build/include")
267         assertNoCheck(path, "pep8/E401")  # A random pep8 category.
268         assertCheck(path, "pep8/W191")
269         assertCheck(path, "pep8/W291")
270         assertCheck(path, "whitespace/carriage_return")
271
272     def test_max_reports_per_category(self):
273         """Check that _MAX_REPORTS_PER_CATEGORY is valid."""
274         all_categories = self._all_categories()
275         for category in _MAX_REPORTS_PER_CATEGORY.iterkeys():
276             self.assertTrue(category in all_categories,
277                             'Key "%s" is not a category' % category)
278
279
280 class CheckWebKitStyleFunctionTest(unittest.TestCase):
281
282     """Tests the functions with names of the form check_webkit_style_*."""
283
284     def test_check_webkit_style_configuration(self):
285         # Exercise the code path to make sure the function does not error out.
286         option_values = CommandOptionValues()
287         configuration = check_webkit_style_configuration(option_values)
288
289     def test_check_webkit_style_parser(self):
290         # Exercise the code path to make sure the function does not error out.
291         parser = check_webkit_style_parser()
292
293
294 class CheckerDispatcherSkipTest(unittest.TestCase):
295
296     """Tests the "should skip" methods of the CheckerDispatcher class."""
297
298     def setUp(self):
299         self._dispatcher = CheckerDispatcher()
300
301     def test_should_skip_with_warning(self):
302         """Test should_skip_with_warning()."""
303         # Check a non-skipped file.
304         self.assertFalse(self._dispatcher.should_skip_with_warning("foo.txt"))
305
306         # Check skipped files.
307         paths_to_skip = [
308            "gtk2drawing.c",
309            "gtkdrawing.h",
310            "Source/WebCore/platform/gtk/gtk2drawing.c",
311            "Source/WebCore/platform/gtk/gtkdrawing.h",
312            "Source/WebKit/gtk/tests/testatk.c",
313             ]
314
315         for path in paths_to_skip:
316             self.assertTrue(self._dispatcher.should_skip_with_warning(path),
317                             "Checking: " + path)
318
319     def _assert_should_skip_without_warning(self, path, is_checker_none,
320                                             expected):
321         # Check the file type before asserting the return value.
322         checker = self._dispatcher.dispatch(file_path=path,
323                                             handle_style_error=None,
324                                             min_confidence=3)
325         message = 'while checking: %s' % path
326         self.assertEquals(checker is None, is_checker_none, message)
327         self.assertEquals(self._dispatcher.should_skip_without_warning(path),
328                           expected, message)
329
330     def test_should_skip_without_warning__true(self):
331         """Test should_skip_without_warning() for True return values."""
332         # Check a file with NONE file type.
333         path = 'foo.asdf'  # Non-sensical file extension.
334         self._assert_should_skip_without_warning(path,
335                                                  is_checker_none=True,
336                                                  expected=True)
337
338         # Check files with non-NONE file type.  These examples must be
339         # drawn from the _SKIPPED_FILES_WITHOUT_WARNING configuration
340         # variable.
341         path = os.path.join('LayoutTests', 'foo.txt')
342         self._assert_should_skip_without_warning(path,
343                                                  is_checker_none=False,
344                                                  expected=True)
345
346     def test_should_skip_without_warning__false(self):
347         """Test should_skip_without_warning() for False return values."""
348         paths = ['foo.txt',
349                  os.path.join('LayoutTests', 'ChangeLog'),
350         ]
351
352         for path in paths:
353             self._assert_should_skip_without_warning(path,
354                                                      is_checker_none=False,
355                                                      expected=False)
356
357
358 class CheckerDispatcherCarriageReturnTest(unittest.TestCase):
359     def test_should_check_and_strip_carriage_returns(self):
360         files = {
361             'foo.txt': True,
362             'foo.cpp': True,
363             'foo.vcproj': False,
364             'foo.vsprops': False,
365         }
366
367         dispatcher = CheckerDispatcher()
368         for file_path, expected_result in files.items():
369             self.assertEquals(dispatcher.should_check_and_strip_carriage_returns(file_path), expected_result, 'Checking: %s' % file_path)
370
371
372 class CheckerDispatcherDispatchTest(unittest.TestCase):
373
374     """Tests dispatch() method of CheckerDispatcher class."""
375
376     def dispatch(self, file_path):
377         """Call dispatch() with the given file path."""
378         dispatcher = CheckerDispatcher()
379         self.mock_handle_style_error = DefaultStyleErrorHandler('', None, None, [])
380         checker = dispatcher.dispatch(file_path,
381                                       self.mock_handle_style_error,
382                                       min_confidence=3)
383         return checker
384
385     def assert_checker_none(self, file_path):
386         """Assert that the dispatched checker is None."""
387         checker = self.dispatch(file_path)
388         self.assertTrue(checker is None, 'Checking: "%s"' % file_path)
389
390     def assert_checker(self, file_path, expected_class):
391         """Assert the type of the dispatched checker."""
392         checker = self.dispatch(file_path)
393         got_class = checker.__class__
394         self.assertEquals(got_class, expected_class,
395                           'For path "%(file_path)s" got %(got_class)s when '
396                           "expecting %(expected_class)s."
397                           % {"file_path": file_path,
398                              "got_class": got_class,
399                              "expected_class": expected_class})
400
401     def assert_checker_changelog(self, file_path):
402         """Assert that the dispatched checker is a ChangeLogChecker."""
403         self.assert_checker(file_path, ChangeLogChecker)
404
405     def assert_checker_cpp(self, file_path):
406         """Assert that the dispatched checker is a CppChecker."""
407         self.assert_checker(file_path, CppChecker)
408
409     def assert_checker_python(self, file_path):
410         """Assert that the dispatched checker is a PythonChecker."""
411         self.assert_checker(file_path, PythonChecker)
412
413     def assert_checker_text(self, file_path):
414         """Assert that the dispatched checker is a TextChecker."""
415         self.assert_checker(file_path, TextChecker)
416
417     def assert_checker_xml(self, file_path):
418         """Assert that the dispatched checker is a XMLChecker."""
419         self.assert_checker(file_path, XMLChecker)
420
421     def test_changelog_paths(self):
422         """Test paths that should be checked as ChangeLog."""
423         paths = [
424                  "ChangeLog",
425                  "ChangeLog-2009-06-16",
426                  os.path.join("Source", "WebCore", "ChangeLog"),
427                  ]
428
429         for path in paths:
430             self.assert_checker_changelog(path)
431
432         # Check checker attributes on a typical input.
433         file_path = "ChangeLog"
434         self.assert_checker_changelog(file_path)
435         checker = self.dispatch(file_path)
436         self.assertEquals(checker.file_path, file_path)
437         self.assertEquals(checker.handle_style_error,
438                           self.mock_handle_style_error)
439
440     def test_cpp_paths(self):
441         """Test paths that should be checked as C++."""
442         paths = [
443             "-",
444             "foo.c",
445             "foo.cpp",
446             "foo.h",
447             ]
448
449         for path in paths:
450             self.assert_checker_cpp(path)
451
452         # Check checker attributes on a typical input.
453         file_base = "foo"
454         file_extension = "c"
455         file_path = file_base + "." + file_extension
456         self.assert_checker_cpp(file_path)
457         checker = self.dispatch(file_path)
458         self.assertEquals(checker.file_extension, file_extension)
459         self.assertEquals(checker.file_path, file_path)
460         self.assertEquals(checker.handle_style_error, self.mock_handle_style_error)
461         self.assertEquals(checker.min_confidence, 3)
462         # Check "-" for good measure.
463         file_base = "-"
464         file_extension = ""
465         file_path = file_base
466         self.assert_checker_cpp(file_path)
467         checker = self.dispatch(file_path)
468         self.assertEquals(checker.file_extension, file_extension)
469         self.assertEquals(checker.file_path, file_path)
470
471     def test_python_paths(self):
472         """Test paths that should be checked as Python."""
473         paths = [
474            "foo.py",
475            "Tools/Scripts/modules/text_style.py",
476         ]
477
478         for path in paths:
479             self.assert_checker_python(path)
480
481         # Check checker attributes on a typical input.
482         file_base = "foo"
483         file_extension = "css"
484         file_path = file_base + "." + file_extension
485         self.assert_checker_text(file_path)
486         checker = self.dispatch(file_path)
487         self.assertEquals(checker.file_path, file_path)
488         self.assertEquals(checker.handle_style_error,
489                           self.mock_handle_style_error)
490
491     def test_text_paths(self):
492         """Test paths that should be checked as text."""
493         paths = [
494            "foo.ac",
495            "foo.cc",
496            "foo.cgi",
497            "foo.css",
498            "foo.exp",
499            "foo.flex",
500            "foo.gyp",
501            "foo.gypi",
502            "foo.html",
503            "foo.idl",
504            "foo.in",
505            "foo.js",
506            "foo.mm",
507            "foo.php",
508            "foo.pl",
509            "foo.pm",
510            "foo.pri",
511            "foo.pro",
512            "foo.rb",
513            "foo.sh",
514            "foo.txt",
515            "foo.wm",
516            "foo.xhtml",
517            "foo.y",
518            os.path.join("Source", "WebCore", "inspector", "front-end", "inspector.js"),
519            os.path.join("Tools", "Scripts", "check-webkit-style"),
520         ]
521
522         for path in paths:
523             self.assert_checker_text(path)
524
525         # Check checker attributes on a typical input.
526         file_base = "foo"
527         file_extension = "css"
528         file_path = file_base + "." + file_extension
529         self.assert_checker_text(file_path)
530         checker = self.dispatch(file_path)
531         self.assertEquals(checker.file_path, file_path)
532         self.assertEquals(checker.handle_style_error, self.mock_handle_style_error)
533
534     def test_xml_paths(self):
535         """Test paths that should be checked as XML."""
536         paths = [
537            "Source/WebCore/WebCore.vcproj/WebCore.vcproj",
538            "WebKitLibraries/win/tools/vsprops/common.vsprops",
539         ]
540
541         for path in paths:
542             self.assert_checker_xml(path)
543
544         # Check checker attributes on a typical input.
545         file_base = "foo"
546         file_extension = "vcproj"
547         file_path = file_base + "." + file_extension
548         self.assert_checker_xml(file_path)
549         checker = self.dispatch(file_path)
550         self.assertEquals(checker.file_path, file_path)
551         self.assertEquals(checker.handle_style_error,
552                           self.mock_handle_style_error)
553
554     def test_none_paths(self):
555         """Test paths that have no file type.."""
556         paths = [
557            "Makefile",
558            "foo.asdf",  # Non-sensical file extension.
559            "foo.png",
560            "foo.exe",
561             ]
562
563         for path in paths:
564             self.assert_checker_none(path)
565
566
567 class StyleProcessorConfigurationTest(unittest.TestCase):
568
569     """Tests the StyleProcessorConfiguration class."""
570
571     def setUp(self):
572         self._error_messages = []
573         """The messages written to _mock_stderr_write() of this class."""
574
575     def _mock_stderr_write(self, message):
576         self._error_messages.append(message)
577
578     def _style_checker_configuration(self, output_format="vs7"):
579         """Return a StyleProcessorConfiguration instance for testing."""
580         base_rules = ["-whitespace", "+whitespace/tab"]
581         filter_configuration = FilterConfiguration(base_rules=base_rules)
582
583         return StyleProcessorConfiguration(
584                    filter_configuration=filter_configuration,
585                    max_reports_per_category={"whitespace/newline": 1},
586                    min_confidence=3,
587                    output_format=output_format,
588                    stderr_write=self._mock_stderr_write)
589
590     def test_init(self):
591         """Test the __init__() method."""
592         configuration = self._style_checker_configuration()
593
594         # Check that __init__ sets the "public" data attributes correctly.
595         self.assertEquals(configuration.max_reports_per_category,
596                           {"whitespace/newline": 1})
597         self.assertEquals(configuration.stderr_write, self._mock_stderr_write)
598         self.assertEquals(configuration.min_confidence, 3)
599
600     def test_is_reportable(self):
601         """Test the is_reportable() method."""
602         config = self._style_checker_configuration()
603
604         self.assertTrue(config.is_reportable("whitespace/tab", 3, "foo.txt"))
605
606         # Test the confidence check code path by varying the confidence.
607         self.assertFalse(config.is_reportable("whitespace/tab", 2, "foo.txt"))
608
609         # Test the category check code path by varying the category.
610         self.assertFalse(config.is_reportable("whitespace/line", 4, "foo.txt"))
611
612     def _call_write_style_error(self, output_format):
613         config = self._style_checker_configuration(output_format=output_format)
614         config.write_style_error(category="whitespace/tab",
615                                  confidence_in_error=5,
616                                  file_path="foo.h",
617                                  line_number=100,
618                                  message="message")
619
620     def test_write_style_error_emacs(self):
621         """Test the write_style_error() method."""
622         self._call_write_style_error("emacs")
623         self.assertEquals(self._error_messages,
624                           ["foo.h:100:  message  [whitespace/tab] [5]\n"])
625
626     def test_write_style_error_vs7(self):
627         """Test the write_style_error() method."""
628         self._call_write_style_error("vs7")
629         self.assertEquals(self._error_messages,
630                           ["foo.h(100):  message  [whitespace/tab] [5]\n"])
631
632
633 class StyleProcessor_EndToEndTest(LoggingTestCase):
634
635     """Test the StyleProcessor class with an emphasis on end-to-end tests."""
636
637     def setUp(self):
638         LoggingTestCase.setUp(self)
639         self._messages = []
640
641     def _mock_stderr_write(self, message):
642         """Save a message so it can later be asserted."""
643         self._messages.append(message)
644
645     def test_init(self):
646         """Test __init__ constructor."""
647         configuration = StyleProcessorConfiguration(
648                             filter_configuration=FilterConfiguration(),
649                             max_reports_per_category={},
650                             min_confidence=3,
651                             output_format="vs7",
652                             stderr_write=self._mock_stderr_write)
653         processor = StyleProcessor(configuration)
654
655         self.assertEquals(processor.error_count, 0)
656         self.assertEquals(self._messages, [])
657
658     def test_process(self):
659         configuration = StyleProcessorConfiguration(
660                             filter_configuration=FilterConfiguration(),
661                             max_reports_per_category={},
662                             min_confidence=3,
663                             output_format="vs7",
664                             stderr_write=self._mock_stderr_write)
665         processor = StyleProcessor(configuration)
666
667         processor.process(lines=['line1', 'Line with tab:\t'],
668                           file_path='foo.txt')
669         self.assertEquals(processor.error_count, 1)
670         expected_messages = ['foo.txt(2):  Line contains tab character.  '
671                              '[whitespace/tab] [5]\n']
672         self.assertEquals(self._messages, expected_messages)
673
674
675 class StyleProcessor_CodeCoverageTest(LoggingTestCase):
676
677     """Test the StyleProcessor class with an emphasis on code coverage.
678
679     This class makes heavy use of mock objects.
680
681     """
682
683     class MockDispatchedChecker(object):
684
685         """A mock checker dispatched by the MockDispatcher."""
686
687         def __init__(self, file_path, min_confidence, style_error_handler):
688             self.file_path = file_path
689             self.min_confidence = min_confidence
690             self.style_error_handler = style_error_handler
691
692         def check(self, lines):
693             self.lines = lines
694
695     class MockDispatcher(object):
696
697         """A mock CheckerDispatcher class."""
698
699         def __init__(self):
700             self.dispatched_checker = None
701
702         def should_skip_with_warning(self, file_path):
703             return file_path.endswith('skip_with_warning.txt')
704
705         def should_skip_without_warning(self, file_path):
706             return file_path.endswith('skip_without_warning.txt')
707
708         def should_check_and_strip_carriage_returns(self, file_path):
709             return not file_path.endswith('carriage_returns_allowed.txt')
710
711         def dispatch(self, file_path, style_error_handler, min_confidence):
712             if file_path.endswith('do_not_process.txt'):
713                 return None
714
715             checker = StyleProcessor_CodeCoverageTest.MockDispatchedChecker(
716                           file_path,
717                           min_confidence,
718                           style_error_handler)
719
720             # Save the dispatched checker so the current test case has a
721             # way to access and check it.
722             self.dispatched_checker = checker
723
724             return checker
725
726     def setUp(self):
727         LoggingTestCase.setUp(self)
728         # We can pass an error-message swallower here because error message
729         # output is tested instead in the end-to-end test case above.
730         configuration = StyleProcessorConfiguration(
731                             filter_configuration=FilterConfiguration(),
732                             max_reports_per_category={"whitespace/newline": 1},
733                             min_confidence=3,
734                             output_format="vs7",
735                             stderr_write=self._swallow_stderr_message)
736
737         mock_carriage_checker_class = self._create_carriage_checker_class()
738         mock_dispatcher = self.MockDispatcher()
739         # We do not need to use a real incrementer here because error-count
740         # incrementing is tested instead in the end-to-end test case above.
741         mock_increment_error_count = self._do_nothing
742
743         processor = StyleProcessor(configuration=configuration,
744                         mock_carriage_checker_class=mock_carriage_checker_class,
745                         mock_dispatcher=mock_dispatcher,
746                         mock_increment_error_count=mock_increment_error_count)
747
748         self._configuration = configuration
749         self._mock_dispatcher = mock_dispatcher
750         self._processor = processor
751
752     def _do_nothing(self):
753         # We provide this function so the caller can pass it to the
754         # StyleProcessor constructor.  This lets us assert the equality of
755         # the DefaultStyleErrorHandler instance generated by the process()
756         # method with an expected instance.
757         pass
758
759     def _swallow_stderr_message(self, message):
760         """Swallow a message passed to stderr.write()."""
761         # This is a mock stderr.write() for passing to the constructor
762         # of the StyleProcessorConfiguration class.
763         pass
764
765     def _create_carriage_checker_class(self):
766
767         # Create a reference to self with a new name so its name does not
768         # conflict with the self introduced below.
769         test_case = self
770
771         class MockCarriageChecker(object):
772
773             """A mock carriage-return checker."""
774
775             def __init__(self, style_error_handler):
776                 self.style_error_handler = style_error_handler
777
778                 # This gives the current test case access to the
779                 # instantiated carriage checker.
780                 test_case.carriage_checker = self
781
782             def check(self, lines):
783                 # Save the lines so the current test case has a way to access
784                 # and check them.
785                 self.lines = lines
786
787                 return lines
788
789         return MockCarriageChecker
790
791     def test_should_process__skip_without_warning(self):
792         """Test should_process() for a skip-without-warning file."""
793         file_path = "foo/skip_without_warning.txt"
794
795         self.assertFalse(self._processor.should_process(file_path))
796
797     def test_should_process__skip_with_warning(self):
798         """Test should_process() for a skip-with-warning file."""
799         file_path = "foo/skip_with_warning.txt"
800
801         self.assertFalse(self._processor.should_process(file_path))
802
803         self.assertLog(['WARNING: File exempt from style guide. '
804                         'Skipping: "foo/skip_with_warning.txt"\n'])
805
806     def test_should_process__true_result(self):
807         """Test should_process() for a file that should be processed."""
808         file_path = "foo/skip_process.txt"
809
810         self.assertTrue(self._processor.should_process(file_path))
811
812     def test_process__checker_dispatched(self):
813         """Test the process() method for a path with a dispatched checker."""
814         file_path = 'foo.txt'
815         lines = ['line1', 'line2']
816         line_numbers = [100]
817
818         expected_error_handler = DefaultStyleErrorHandler(
819             configuration=self._configuration,
820             file_path=file_path,
821             increment_error_count=self._do_nothing,
822             line_numbers=line_numbers)
823
824         self._processor.process(lines=lines,
825                                 file_path=file_path,
826                                 line_numbers=line_numbers)
827
828         # Check that the carriage-return checker was instantiated correctly
829         # and was passed lines correctly.
830         carriage_checker = self.carriage_checker
831         self.assertEquals(carriage_checker.style_error_handler,
832                           expected_error_handler)
833         self.assertEquals(carriage_checker.lines, ['line1', 'line2'])
834
835         # Check that the style checker was dispatched correctly and was
836         # passed lines correctly.
837         checker = self._mock_dispatcher.dispatched_checker
838         self.assertEquals(checker.file_path, 'foo.txt')
839         self.assertEquals(checker.min_confidence, 3)
840         self.assertEquals(checker.style_error_handler, expected_error_handler)
841
842         self.assertEquals(checker.lines, ['line1', 'line2'])
843
844     def test_process__no_checker_dispatched(self):
845         """Test the process() method for a path with no dispatched checker."""
846         path = os.path.join('foo', 'do_not_process.txt')
847         self.assertRaises(AssertionError, self._processor.process,
848                           lines=['line1', 'line2'], file_path=path,
849                           line_numbers=[100])
850
851     def test_process__carriage_returns_not_stripped(self):
852         """Test that carriage returns aren't stripped from files that are allowed to contain them."""
853         file_path = 'carriage_returns_allowed.txt'
854         lines = ['line1\r', 'line2\r']
855         line_numbers = [100]
856         self._processor.process(lines=lines,
857                                 file_path=file_path,
858                                 line_numbers=line_numbers)
859         # The carriage return checker should never have been invoked, and so
860         # should not have saved off any lines.
861         self.assertFalse(hasattr(self.carriage_checker, 'lines'))