8935c2d1fe818b457869370c8fc6d11408e4a73c
[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 mock_handle_style_error(self):
373         pass
374
375     def dispatch(self, file_path):
376         """Call dispatch() with the given file path."""
377         dispatcher = CheckerDispatcher()
378         checker = dispatcher.dispatch(file_path,
379                                       self.mock_handle_style_error,
380                                       min_confidence=3)
381         return checker
382
383     def assert_checker_none(self, file_path):
384         """Assert that the dispatched checker is None."""
385         checker = self.dispatch(file_path)
386         self.assertTrue(checker is None, 'Checking: "%s"' % file_path)
387
388     def assert_checker(self, file_path, expected_class):
389         """Assert the type of the dispatched checker."""
390         checker = self.dispatch(file_path)
391         got_class = checker.__class__
392         self.assertEquals(got_class, expected_class,
393                           'For path "%(file_path)s" got %(got_class)s when '
394                           "expecting %(expected_class)s."
395                           % {"file_path": file_path,
396                              "got_class": got_class,
397                              "expected_class": expected_class})
398
399     def assert_checker_changelog(self, file_path):
400         """Assert that the dispatched checker is a ChangeLogChecker."""
401         self.assert_checker(file_path, ChangeLogChecker)
402
403     def assert_checker_cpp(self, file_path):
404         """Assert that the dispatched checker is a CppChecker."""
405         self.assert_checker(file_path, CppChecker)
406
407     def assert_checker_python(self, file_path):
408         """Assert that the dispatched checker is a PythonChecker."""
409         self.assert_checker(file_path, PythonChecker)
410
411     def assert_checker_text(self, file_path):
412         """Assert that the dispatched checker is a TextChecker."""
413         self.assert_checker(file_path, TextChecker)
414
415     def assert_checker_xml(self, file_path):
416         """Assert that the dispatched checker is a XMLChecker."""
417         self.assert_checker(file_path, XMLChecker)
418
419     def test_changelog_paths(self):
420         """Test paths that should be checked as ChangeLog."""
421         paths = [
422                  "ChangeLog",
423                  "ChangeLog-2009-06-16",
424                  os.path.join("Source", "WebCore", "ChangeLog"),
425                  ]
426
427         for path in paths:
428             self.assert_checker_changelog(path)
429
430         # Check checker attributes on a typical input.
431         file_path = "ChangeLog"
432         self.assert_checker_changelog(file_path)
433         checker = self.dispatch(file_path)
434         self.assertEquals(checker.file_path, file_path)
435         self.assertEquals(checker.handle_style_error,
436                           self.mock_handle_style_error)
437
438     def test_cpp_paths(self):
439         """Test paths that should be checked as C++."""
440         paths = [
441             "-",
442             "foo.c",
443             "foo.cpp",
444             "foo.h",
445             ]
446
447         for path in paths:
448             self.assert_checker_cpp(path)
449
450         # Check checker attributes on a typical input.
451         file_base = "foo"
452         file_extension = "c"
453         file_path = file_base + "." + file_extension
454         self.assert_checker_cpp(file_path)
455         checker = self.dispatch(file_path)
456         self.assertEquals(checker.file_extension, file_extension)
457         self.assertEquals(checker.file_path, file_path)
458         self.assertEquals(checker.handle_style_error, self.mock_handle_style_error)
459         self.assertEquals(checker.min_confidence, 3)
460         # Check "-" for good measure.
461         file_base = "-"
462         file_extension = ""
463         file_path = file_base
464         self.assert_checker_cpp(file_path)
465         checker = self.dispatch(file_path)
466         self.assertEquals(checker.file_extension, file_extension)
467         self.assertEquals(checker.file_path, file_path)
468
469     def test_python_paths(self):
470         """Test paths that should be checked as Python."""
471         paths = [
472            "foo.py",
473            "Tools/Scripts/modules/text_style.py",
474         ]
475
476         for path in paths:
477             self.assert_checker_python(path)
478
479         # Check checker attributes on a typical input.
480         file_base = "foo"
481         file_extension = "css"
482         file_path = file_base + "." + file_extension
483         self.assert_checker_text(file_path)
484         checker = self.dispatch(file_path)
485         self.assertEquals(checker.file_path, file_path)
486         self.assertEquals(checker.handle_style_error,
487                           self.mock_handle_style_error)
488
489     def test_text_paths(self):
490         """Test paths that should be checked as text."""
491         paths = [
492            "foo.ac",
493            "foo.cc",
494            "foo.cgi",
495            "foo.css",
496            "foo.exp",
497            "foo.flex",
498            "foo.gyp",
499            "foo.gypi",
500            "foo.html",
501            "foo.idl",
502            "foo.in",
503            "foo.js",
504            "foo.mm",
505            "foo.php",
506            "foo.pl",
507            "foo.pm",
508            "foo.pri",
509            "foo.pro",
510            "foo.rb",
511            "foo.sh",
512            "foo.txt",
513            "foo.wm",
514            "foo.xhtml",
515            "foo.y",
516            os.path.join("Source", "WebCore", "inspector", "front-end", "inspector.js"),
517            os.path.join("Tools", "Scripts", "check-webkit-style"),
518         ]
519
520         for path in paths:
521             self.assert_checker_text(path)
522
523         # Check checker attributes on a typical input.
524         file_base = "foo"
525         file_extension = "css"
526         file_path = file_base + "." + file_extension
527         self.assert_checker_text(file_path)
528         checker = self.dispatch(file_path)
529         self.assertEquals(checker.file_path, file_path)
530         self.assertEquals(checker.handle_style_error, self.mock_handle_style_error)
531
532     def test_xml_paths(self):
533         """Test paths that should be checked as XML."""
534         paths = [
535            "Source/WebCore/WebCore.vcproj/WebCore.vcproj",
536            "WebKitLibraries/win/tools/vsprops/common.vsprops",
537         ]
538
539         for path in paths:
540             self.assert_checker_xml(path)
541
542         # Check checker attributes on a typical input.
543         file_base = "foo"
544         file_extension = "vcproj"
545         file_path = file_base + "." + file_extension
546         self.assert_checker_xml(file_path)
547         checker = self.dispatch(file_path)
548         self.assertEquals(checker.file_path, file_path)
549         self.assertEquals(checker.handle_style_error,
550                           self.mock_handle_style_error)
551
552     def test_none_paths(self):
553         """Test paths that have no file type.."""
554         paths = [
555            "Makefile",
556            "foo.asdf",  # Non-sensical file extension.
557            "foo.png",
558            "foo.exe",
559             ]
560
561         for path in paths:
562             self.assert_checker_none(path)
563
564
565 class StyleProcessorConfigurationTest(unittest.TestCase):
566
567     """Tests the StyleProcessorConfiguration class."""
568
569     def setUp(self):
570         self._error_messages = []
571         """The messages written to _mock_stderr_write() of this class."""
572
573     def _mock_stderr_write(self, message):
574         self._error_messages.append(message)
575
576     def _style_checker_configuration(self, output_format="vs7"):
577         """Return a StyleProcessorConfiguration instance for testing."""
578         base_rules = ["-whitespace", "+whitespace/tab"]
579         filter_configuration = FilterConfiguration(base_rules=base_rules)
580
581         return StyleProcessorConfiguration(
582                    filter_configuration=filter_configuration,
583                    max_reports_per_category={"whitespace/newline": 1},
584                    min_confidence=3,
585                    output_format=output_format,
586                    stderr_write=self._mock_stderr_write)
587
588     def test_init(self):
589         """Test the __init__() method."""
590         configuration = self._style_checker_configuration()
591
592         # Check that __init__ sets the "public" data attributes correctly.
593         self.assertEquals(configuration.max_reports_per_category,
594                           {"whitespace/newline": 1})
595         self.assertEquals(configuration.stderr_write, self._mock_stderr_write)
596         self.assertEquals(configuration.min_confidence, 3)
597
598     def test_is_reportable(self):
599         """Test the is_reportable() method."""
600         config = self._style_checker_configuration()
601
602         self.assertTrue(config.is_reportable("whitespace/tab", 3, "foo.txt"))
603
604         # Test the confidence check code path by varying the confidence.
605         self.assertFalse(config.is_reportable("whitespace/tab", 2, "foo.txt"))
606
607         # Test the category check code path by varying the category.
608         self.assertFalse(config.is_reportable("whitespace/line", 4, "foo.txt"))
609
610     def _call_write_style_error(self, output_format):
611         config = self._style_checker_configuration(output_format=output_format)
612         config.write_style_error(category="whitespace/tab",
613                                  confidence_in_error=5,
614                                  file_path="foo.h",
615                                  line_number=100,
616                                  message="message")
617
618     def test_write_style_error_emacs(self):
619         """Test the write_style_error() method."""
620         self._call_write_style_error("emacs")
621         self.assertEquals(self._error_messages,
622                           ["foo.h:100:  message  [whitespace/tab] [5]\n"])
623
624     def test_write_style_error_vs7(self):
625         """Test the write_style_error() method."""
626         self._call_write_style_error("vs7")
627         self.assertEquals(self._error_messages,
628                           ["foo.h(100):  message  [whitespace/tab] [5]\n"])
629
630
631 class StyleProcessor_EndToEndTest(LoggingTestCase):
632
633     """Test the StyleProcessor class with an emphasis on end-to-end tests."""
634
635     def setUp(self):
636         LoggingTestCase.setUp(self)
637         self._messages = []
638
639     def _mock_stderr_write(self, message):
640         """Save a message so it can later be asserted."""
641         self._messages.append(message)
642
643     def test_init(self):
644         """Test __init__ constructor."""
645         configuration = StyleProcessorConfiguration(
646                             filter_configuration=FilterConfiguration(),
647                             max_reports_per_category={},
648                             min_confidence=3,
649                             output_format="vs7",
650                             stderr_write=self._mock_stderr_write)
651         processor = StyleProcessor(configuration)
652
653         self.assertEquals(processor.error_count, 0)
654         self.assertEquals(self._messages, [])
655
656     def test_process(self):
657         configuration = StyleProcessorConfiguration(
658                             filter_configuration=FilterConfiguration(),
659                             max_reports_per_category={},
660                             min_confidence=3,
661                             output_format="vs7",
662                             stderr_write=self._mock_stderr_write)
663         processor = StyleProcessor(configuration)
664
665         processor.process(lines=['line1', 'Line with tab:\t'],
666                           file_path='foo.txt')
667         self.assertEquals(processor.error_count, 1)
668         expected_messages = ['foo.txt(2):  Line contains tab character.  '
669                              '[whitespace/tab] [5]\n']
670         self.assertEquals(self._messages, expected_messages)
671
672
673 class StyleProcessor_CodeCoverageTest(LoggingTestCase):
674
675     """Test the StyleProcessor class with an emphasis on code coverage.
676
677     This class makes heavy use of mock objects.
678
679     """
680
681     class MockDispatchedChecker(object):
682
683         """A mock checker dispatched by the MockDispatcher."""
684
685         def __init__(self, file_path, min_confidence, style_error_handler):
686             self.file_path = file_path
687             self.min_confidence = min_confidence
688             self.style_error_handler = style_error_handler
689
690         def check(self, lines):
691             self.lines = lines
692
693     class MockDispatcher(object):
694
695         """A mock CheckerDispatcher class."""
696
697         def __init__(self):
698             self.dispatched_checker = None
699
700         def should_skip_with_warning(self, file_path):
701             return file_path.endswith('skip_with_warning.txt')
702
703         def should_skip_without_warning(self, file_path):
704             return file_path.endswith('skip_without_warning.txt')
705
706         def should_check_and_strip_carriage_returns(self, file_path):
707             return not file_path.endswith('carriage_returns_allowed.txt')
708
709         def dispatch(self, file_path, style_error_handler, min_confidence):
710             if file_path.endswith('do_not_process.txt'):
711                 return None
712
713             checker = StyleProcessor_CodeCoverageTest.MockDispatchedChecker(
714                           file_path,
715                           min_confidence,
716                           style_error_handler)
717
718             # Save the dispatched checker so the current test case has a
719             # way to access and check it.
720             self.dispatched_checker = checker
721
722             return checker
723
724     def setUp(self):
725         LoggingTestCase.setUp(self)
726         # We can pass an error-message swallower here because error message
727         # output is tested instead in the end-to-end test case above.
728         configuration = StyleProcessorConfiguration(
729                             filter_configuration=FilterConfiguration(),
730                             max_reports_per_category={"whitespace/newline": 1},
731                             min_confidence=3,
732                             output_format="vs7",
733                             stderr_write=self._swallow_stderr_message)
734
735         mock_carriage_checker_class = self._create_carriage_checker_class()
736         mock_dispatcher = self.MockDispatcher()
737         # We do not need to use a real incrementer here because error-count
738         # incrementing is tested instead in the end-to-end test case above.
739         mock_increment_error_count = self._do_nothing
740
741         processor = StyleProcessor(configuration=configuration,
742                         mock_carriage_checker_class=mock_carriage_checker_class,
743                         mock_dispatcher=mock_dispatcher,
744                         mock_increment_error_count=mock_increment_error_count)
745
746         self._configuration = configuration
747         self._mock_dispatcher = mock_dispatcher
748         self._processor = processor
749
750     def _do_nothing(self):
751         # We provide this function so the caller can pass it to the
752         # StyleProcessor constructor.  This lets us assert the equality of
753         # the DefaultStyleErrorHandler instance generated by the process()
754         # method with an expected instance.
755         pass
756
757     def _swallow_stderr_message(self, message):
758         """Swallow a message passed to stderr.write()."""
759         # This is a mock stderr.write() for passing to the constructor
760         # of the StyleProcessorConfiguration class.
761         pass
762
763     def _create_carriage_checker_class(self):
764
765         # Create a reference to self with a new name so its name does not
766         # conflict with the self introduced below.
767         test_case = self
768
769         class MockCarriageChecker(object):
770
771             """A mock carriage-return checker."""
772
773             def __init__(self, style_error_handler):
774                 self.style_error_handler = style_error_handler
775
776                 # This gives the current test case access to the
777                 # instantiated carriage checker.
778                 test_case.carriage_checker = self
779
780             def check(self, lines):
781                 # Save the lines so the current test case has a way to access
782                 # and check them.
783                 self.lines = lines
784
785                 return lines
786
787         return MockCarriageChecker
788
789     def test_should_process__skip_without_warning(self):
790         """Test should_process() for a skip-without-warning file."""
791         file_path = "foo/skip_without_warning.txt"
792
793         self.assertFalse(self._processor.should_process(file_path))
794
795     def test_should_process__skip_with_warning(self):
796         """Test should_process() for a skip-with-warning file."""
797         file_path = "foo/skip_with_warning.txt"
798
799         self.assertFalse(self._processor.should_process(file_path))
800
801         self.assertLog(['WARNING: File exempt from style guide. '
802                         'Skipping: "foo/skip_with_warning.txt"\n'])
803
804     def test_should_process__true_result(self):
805         """Test should_process() for a file that should be processed."""
806         file_path = "foo/skip_process.txt"
807
808         self.assertTrue(self._processor.should_process(file_path))
809
810     def test_process__checker_dispatched(self):
811         """Test the process() method for a path with a dispatched checker."""
812         file_path = 'foo.txt'
813         lines = ['line1', 'line2']
814         line_numbers = [100]
815
816         expected_error_handler = DefaultStyleErrorHandler(
817             configuration=self._configuration,
818             file_path=file_path,
819             increment_error_count=self._do_nothing,
820             line_numbers=line_numbers)
821
822         self._processor.process(lines=lines,
823                                 file_path=file_path,
824                                 line_numbers=line_numbers)
825
826         # Check that the carriage-return checker was instantiated correctly
827         # and was passed lines correctly.
828         carriage_checker = self.carriage_checker
829         self.assertEquals(carriage_checker.style_error_handler,
830                           expected_error_handler)
831         self.assertEquals(carriage_checker.lines, ['line1', 'line2'])
832
833         # Check that the style checker was dispatched correctly and was
834         # passed lines correctly.
835         checker = self._mock_dispatcher.dispatched_checker
836         self.assertEquals(checker.file_path, 'foo.txt')
837         self.assertEquals(checker.min_confidence, 3)
838         self.assertEquals(checker.style_error_handler, expected_error_handler)
839
840         self.assertEquals(checker.lines, ['line1', 'line2'])
841
842     def test_process__no_checker_dispatched(self):
843         """Test the process() method for a path with no dispatched checker."""
844         path = os.path.join('foo', 'do_not_process.txt')
845         self.assertRaises(AssertionError, self._processor.process,
846                           lines=['line1', 'line2'], file_path=path,
847                           line_numbers=[100])
848
849     def test_process__carriage_returns_not_stripped(self):
850         """Test that carriage returns aren't stripped from files that are allowed to contain them."""
851         file_path = 'carriage_returns_allowed.txt'
852         lines = ['line1\r', 'line2\r']
853         line_numbers = [100]
854         self._processor.process(lines=lines,
855                                 file_path=file_path,
856                                 line_numbers=line_numbers)
857         # The carriage return checker should never have been invoked, and so
858         # should not have saved off any lines.
859         self.assertFalse(hasattr(self.carriage_checker, 'lines'))