Home | History | Annotate | Download | only in common
      1 #!/usr/bin/env python
      2 #
      3 # Copyright 2007 The Closure Linter Authors. All Rights Reserved.
      4 #
      5 # Licensed under the Apache License, Version 2.0 (the "License");
      6 # you may not use this file except in compliance with the License.
      7 # You may obtain a copy of the License at
      8 #
      9 #      http://www.apache.org/licenses/LICENSE-2.0
     10 #
     11 # Unless required by applicable law or agreed to in writing, software
     12 # distributed under the License is distributed on an "AS-IS" BASIS,
     13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14 # See the License for the specific language governing permissions and
     15 # limitations under the License.
     16 
     17 """Test case that runs a checker on a file, matching errors against annotations.
     18 
     19 Runs the given checker on the given file, accumulating all errors.  The list
     20 of errors is then matched against those annotated in the file.  Based heavily
     21 on devtools/javascript/gpylint/full_test.py.
     22 """
     23 
     24 __author__ = ('robbyw (at] google.com (Robert Walker)',
     25               'ajp (at] google.com (Andy Perelson)')
     26 
     27 import re
     28 
     29 import unittest as googletest
     30 from closure_linter.common import erroraccumulator
     31 
     32 
     33 class AnnotatedFileTestCase(googletest.TestCase):
     34   """Test case to run a linter against a single file."""
     35 
     36   # Matches an all caps letters + underscores error identifer
     37   _MESSAGE = {'msg': '[A-Z][A-Z_]+'}
     38   # Matches a //, followed by an optional line number with a +/-, followed by a
     39   # list of message IDs. Used to extract expected messages from testdata files.
     40   # TODO(robbyw): Generalize to use different commenting patterns.
     41   _EXPECTED_RE = re.compile(r'\s*//\s*(?:(?P<line>[+-]?[0-9]+):)?'
     42                             r'\s*(?P<msgs>%(msg)s(?:,\s*%(msg)s)*)' % _MESSAGE)
     43 
     44   def __init__(self, filename, runner, converter):
     45     """Create a single file lint test case.
     46 
     47     Args:
     48       filename: Filename to test.
     49       runner: Object implementing the LintRunner interface that lints a file.
     50       converter: Function taking an error string and returning an error code.
     51     """
     52 
     53     googletest.TestCase.__init__(self, 'runTest')
     54     self._filename = filename
     55     self._messages = []
     56     self._runner = runner
     57     self._converter = converter
     58 
     59   def shortDescription(self):
     60     """Provides a description for the test."""
     61     return 'Run linter on %s' % self._filename
     62 
     63   def runTest(self):
     64     """Runs the test."""
     65     try:
     66       filename = self._filename
     67       stream = open(filename)
     68     except IOError, ex:
     69       raise IOError('Could not find testdata resource for %s: %s' %
     70                     (self._filename, ex))
     71 
     72     expected = self._GetExpectedMessages(stream)
     73     got = self._ProcessFileAndGetMessages(filename)
     74     self.assertEqual(expected, got)
     75 
     76   def _GetExpectedMessages(self, stream):
     77     """Parse a file and get a sorted list of expected messages."""
     78     messages = []
     79     for i, line in enumerate(stream):
     80       match = self._EXPECTED_RE.search(line)
     81       if match:
     82         line = match.group('line')
     83         msg_ids = match.group('msgs')
     84         if line is None:
     85           line = i + 1
     86         elif line.startswith('+') or line.startswith('-'):
     87           line = i + 1 + int(line)
     88         else:
     89           line = int(line)
     90         for msg_id in msg_ids.split(','):
     91           # Ignore a spurious message from the license preamble.
     92           if msg_id != 'WITHOUT':
     93             messages.append((line, self._converter(msg_id.strip())))
     94     stream.seek(0)
     95     messages.sort()
     96     return messages
     97 
     98   def _ProcessFileAndGetMessages(self, filename):
     99     """Trap gpylint's output parse it to get messages added."""
    100     errors = erroraccumulator.ErrorAccumulator()
    101     self._runner.Run([filename], errors)
    102 
    103     errors = errors.GetErrors()
    104 
    105     # Convert to expected tuple format.
    106     error_msgs = [(error.token.line_number, error.code) for error in errors]
    107     error_msgs.sort()
    108     return error_msgs
    109