# 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 $