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
37import logging
38import os
39import unittest
40
41import checker as style
42from webkitpy.style_references import LogTesting
43from webkitpy.style_references import TestLogStream
44from checker import _BASE_FILTER_RULES
45from checker import _MAX_REPORTS_PER_CATEGORY
46from checker import _PATH_RULES_SPECIFIER as PATH_RULES_SPECIFIER
47from checker import _all_categories
48from checker import check_webkit_style_configuration
49from checker import check_webkit_style_parser
50from checker import configure_logging
51from checker import CheckerDispatcher
52from checker import ProcessorBase
53from checker import StyleProcessor
54from checker import StyleProcessorConfiguration
55from checkers.changelog import ChangeLogChecker
56from checkers.cpp import CppChecker
57from checkers.python import PythonChecker
58from checkers.text import TextChecker
59from checkers.xml import XMLChecker
60from error_handlers import DefaultStyleErrorHandler
61from filter import validate_filter_rules
62from filter import FilterConfiguration
63from optparser import ArgumentParser
64from optparser import CommandOptionValues
65from webkitpy.common.system.logtesting import LoggingTestCase
66from webkitpy.style.filereader import TextFileReader
67
68
69class 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
115class 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
143class 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
154class 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
276class 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
290class 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
354class 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
368class 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
563class 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
629class 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
671class 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'))
858