# Perforce Defect Tracking Integration Project # # # TEST_CATALOG.PY -- UNIT TEST FOR CATALOG MODULE # # Gareth Rees, Ravenbrook Limited, 2001-03-14 # # # 1. INTRODUCTION # # This module defines a unit test for the catalog module. It checks that the # message catalog is used consistently, correctly and completely throughout the # P4DTI. # # It uses the PyUnit unit test framework [PyUnit]. # # The intended readership is project developers. # # This document is not confidential. import os import sys p4dti_path = os.path.join(os.getcwd(), os.pardir, 'code', 'replicator') if p4dti_path not in sys.path: sys.path.append(p4dti_path) import catalog import dircache import message import p4dti_unittest import parser import re import string import unittest # 2. TEST CASES # 2.1. Use of the message catalog # # This test case checks that the P4DTI English message catalog is used # consistently, correctly and completely. # # It reads all Python files in the replicator. In each file, it finds all # occurrences where a message is fetched from the catalog. For each # occurrence, it checks that: # # 1. The message is in the catalog; # # 2. The catalog gives a legal priority for the message; # # 3. There's a comment preceding the use of the message which gives the # correct text; and # # 4. The correct number of arguments are supplied. # # Finally it checks that all unused messages in the catalog have priority # message.NOT_USED. class catalog_use(p4dti_unittest.TestCase): # The directory in which to search for Python files. dir = os.path.join(os.getcwd(), os.pardir, 'code', 'replicator') # Regexp matching the use of a message. Group 2 is the message number. # Group 4 is the arguments to the message, if any. msg_re = re.compile("(\\.log|\\bcatalog\\.msg)\\(([0-9]+)(\\)|, *(.*)\\))") # Regexp matching the first line in a block comment that precedes the use # of a message: the comment starts with a double quote. comment_start_re = re.compile("^[ \t]*# *\"") # Regexp matching any line in a block comment. comment_re = re.compile("^[ \t]*# *(.*)$") # Messages found messages = {} # Return the length of a tuple specified by a string, for example "(1,2)" # -> 2, "(foo(1), bar(2,3), baz)" -> 3. Use the Python parser to work this # out accurately. def tuple_length(self, tuple): parse_tree = parser.expr(tuple).tolist() # The following expression burrows down through the parse tree to the # point where the elements of the tuple are represented. There are # exactly twice as many elements in the parse tree as there are items # in the tuple. return len(parse_tree[1][1][1][1][1][1][1][1][1][1][1][1][1][1][2]) / 2 def runTest(self): "Use of the message catalog" # Find Python files and check them. files = filter(lambda f: f[-3:] == '.py', dircache.listdir(self.dir)) self.failUnless(files, "No Python source files in '%s'." % self.dir) for file in files: self.check_file(file) # Check that all unused messages have priority message.NOT_USED. for (id, (priority, _)) in catalog.p4dti_en_catalog.items(): if not self.messages.has_key(id): if priority != message.NOT_USED: self.addFailure("Found no occurrence of message %d, but " "its priority is not message.NOT_USED." % id) def check_file(self, file): lines = open(os.path.join(self.dir, file), 'r').readlines() # Search forward for lines containing "catalog.msg([0-9]+" or # ".log([0-9]+". for l in range(len(lines)): match = self.msg_re.search(lines[l]) if match: msgid = int(match.group(2)) self.messages[msgid] = 1 args = match.group(4) # Check that the message exists. if not catalog.p4dti_en_catalog.has_key(msgid): self.addFailure("File %s, line %d uses message %d but " "this is missing from the catalog." % (file, l+1, msgid)) # Check that priority is legal. Note the nonintuitive sense of # the comparisons: higher priorites have lower numbers. (priority, message_text) = catalog.p4dti_en_catalog[msgid] if priority > message.DEBUG or priority < message.EMERG: self.addFailure("Message %d has priority %d: this is out " "of range." % (msgid, priority)) # Check that there's a comment preceding the message which # gives the correct text. m = l - 1 while m >= 0 and not self.comment_start_re.match(lines[m]): m = m - 1 if m < 0: self.addFailure("File %s, line %d uses message %d but " "there's no preceding comment with the " "message text." % (file, l+1, msgid)) comment_lines = [] for n in range(m,l): match = self.comment_re.match(lines[n]) if not match: self.addFailure("File %s, line %d uses message %d but " "is preceded by non-comment line %d." % (file, l+1, msgid, n+1)) comment_lines.append(match.group(1)) comment_text = string.join(comment_lines, " ")[1:-1] # Ignore whitespace when comparing. message_text = re.sub(' +', ' ', message_text) comment_text = re.sub(' +', ' ', comment_text) if comment_text != message_text: self.addFailure("File %s, line %d uses message %d. The " "preceding comment should say '%s', but " "actually says '%s'." % (file, l+1, msgid, message_text, comment_text)) # Check that the correct number of arguments have been passed. expected_nargs = len(filter(lambda s: s != "%%", re.findall("%.", message_text))) if args: if re.match("^ *\\(", args): found_nargs = self.tuple_length(args) else: found_nargs = 1 else: found_nargs = 0 if expected_nargs != found_nargs: self.addFailure("File %s, line %d uses message %d with %d " "argument%s, but that message requires %d " "argument%s." % (file, l+1, msgid, found_nargs, ['s', ''][found_nargs == 1], expected_nargs, ['s', ''][expected_nargs == 1])) # 2.2. Messages in the Administrator's Guide # # This test case reads the Administrator's Guide and checks that: # # 1. Each message has an anchor called message-P4DTI-N, formatted correctly. # # 2. The check digit is correct. # # 3. The text of the message in the AG matches the catalog. # # 4. All errors appear in the AG. class ag_messages(p4dti_unittest.TestCase): # The location of the AG. ag_filename = os.path.join(os.pardir, 'manual', 'ag', 'index.html') # Messages found in the AG. messages = {} # Regexp matching header lines which introduce a message. message_re = re.compile(" *\\(P4DTI-[0-9]+[0-9X]\\) .*") # Regexp which identifies correctly formatted header lines. header_re = re.compile(' *\\(P4DTI-\\1\\) +(.*[^ ]) *') # These are messages that have parameters substituted when they appear in # the AG, so shouldn't be checked literally. exempted_messages = [708] def analyze_line(self, line): # Check that the header is in the right format. match = self.header_re.search(line) if not match: self.addFailure("Message header line badly formatted: " + line) return # Check that the check digit is correct. id = int(match.group(2)) msg = message.message(id, "None", message.INFO, "None") if match.group(3) != msg.check_digit(): self.addFailure("Message %d has check digit %s in AG (not %s)." % (id, match.group(3), msg.check_digit())) # Check that the message text is correct (excepting messages that are # exempted). if id not in self.exempted_messages: expected_text = catalog.p4dti_en_catalog[id][1] if match.group(6) != expected_text: self.addFailure("Message %d has text '%s' in AG (not '%s')." % (id, match.group(6), expected_text)) # Record the discovery of the message self.messages[id] = 1 def runTest(self): "Messages in the Administrator's Guide" ag = open(self.ag_filename, 'r') for line in ag.readlines(): if self.message_re.search(line): self.analyze_line(line) ag.close() # Check that all errors appear in the manual. missing_messages = [] for (id, (priority, text)) in catalog.p4dti_en_catalog.items(): if priority <= message.ERR and not self.messages.has_key(id): missing_messages.append(id) missing_messages.sort() self.failIf(missing_messages, "These error messages are missing from the AG: %s." % missing_messages) # 3. RUNNING THE TESTS def tests(): suite = unittest.TestSuite() suite.addTest(catalog_use()) suite.addTest(ag_messages()) return suite if __name__ == "__main__": unittest.main(defaultTest="tests") # A. REFERENCES # # [PyUnit] "PyUnit - a unit testing framework for Python"; Steve Purcell; # . # # # B. DOCUMENT HISTORY # # 2001-03-14 GDR Created. # # 2001-03-16 GDR Added ag_messages test case. # # 2001-04-24 GDR Use p4dti_unittest to collect many failures per test case. # Use os.path so tests are independent of operating system. # # # C. COPYRIGHT AND LICENCE # # This file is copyright (c) 2001 Perforce Software, Inc. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # # # $Id: //info.ravenbrook.com/project/p4dti/branch/2001-05-15/capacity/test/test_catalog.py#1 $