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