~launchpad-pqm/launchpad/devel

10637.3.1 by Guilherme Salgado
Use the default python version instead of a hard-coded version
1
#!/usr/bin/python
8687.15.4 by Karl Fogel
Add the copyright header block to more files; tweak format in a few files.
2
#
3
# Copyright 2009 Canonical Ltd.  This software is licensed under the
4
# GNU Affero General Public License version 3 (see the file LICENSE).
5
6912.5.11 by Curtis Hovey
Minor fixes.
6
"""Create a XXX comment reports in many formats."""
6912.5.1 by Curtis Hovey
Initial addition of the XXX report from the old 3732 branch.
7
8
9
__metaclass__ = type
10
11
12
import cgi
6912.5.10 by Curtis Hovey
Added the optionsparser, moved several functions into the base Report class.
13
from optparse import OptionParser
6912.5.1 by Curtis Hovey
Initial addition of the XXX report from the old 3732 branch.
14
import os
15
import re
16
import sys
6912.5.10 by Curtis Hovey
Added the optionsparser, moved several functions into the base Report class.
17
from textwrap import dedent
6912.5.1 by Curtis Hovey
Initial addition of the XXX report from the old 3732 branch.
18
import time
19
20
from bzrlib import bzrdir
21
from bzrlib.errors import (NotBranchError)
22
23
6912.5.17 by Curtis Hovey
Revised the variable and method names. Simplified the extractComment loop.
24
excluded_dir_re = re.compile(r'.*(not-used|lib/mailman)')
25
excluded_file_re = re.compile(r'.*(pyc$)')
6912.5.1 by Curtis Hovey
Initial addition of the XXX report from the old 3732 branch.
26
27
6912.5.9 by Curtis Hovey
Moved the HTML report code into a single class.
28
class Report:
29
    """The base class for an XXX report."""
6912.5.10 by Curtis Hovey
Added the optionsparser, moved several functions into the base Report class.
30
    # Match XXX comments.
31
    xxx_re = re.compile('^\s*(<!--|//|#) XXX[:,]?')
32
33
    def __init__(self, root_dir, output_name=None):
34
        """Create and write the HTML report to a file.
35
36
        :param root_dir: The root directory that contains files with comments.
37
        :param output_name: The name of the html file to write to.
38
        """
39
        assert os.path.isdir(root_dir), (
40
            "Root directory does not exist: %s." % root_dir)
41
        self.root_dir = root_dir
42
        self.output_name = output_name
6912.5.17 by Curtis Hovey
Revised the variable and method names. Simplified the extractComment loop.
43
        self.revno = self._getBranchRevno()
44
        self.comments = self._findComments()
6912.5.10 by Curtis Hovey
Added the optionsparser, moved several functions into the base Report class.
45
46
    def _close(self, output_file):
47
        """Close the output_file if it was opened."""
48
        if self.output_name is not None:
49
            output_file.close()
50
6912.5.17 by Curtis Hovey
Revised the variable and method names. Simplified the extractComment loop.
51
    def _getBranchRevno(self):
6912.5.10 by Curtis Hovey
Added the optionsparser, moved several functions into the base Report class.
52
        """Return the bazaar revision number of the branch or None."""
53
        # pylint: disable-msg=W0612
54
        a_bzrdir = bzrdir.BzrDir.open_containing(self.root_dir)[0]
55
        try:
56
            branch = a_bzrdir.open_branch()
57
            branch.lock_read()
58
            try:
59
                revno, head = branch.last_revision_info()
60
            finally:
61
                branch.unlock()
62
        except NotBranchError:
63
            revno = None
64
        return revno
65
6912.5.17 by Curtis Hovey
Revised the variable and method names. Simplified the extractComment loop.
66
    def _findComments(self):
6912.5.10 by Curtis Hovey
Added the optionsparser, moved several functions into the base Report class.
67
        """Set the list of XXX comments in files below a directory."""
68
        comments = []
6912.5.17 by Curtis Hovey
Revised the variable and method names. Simplified the extractComment loop.
69
        for file_path in self._findFiles():
70
            comments.extend(self._extractComments(file_path))
6912.5.10 by Curtis Hovey
Added the optionsparser, moved several functions into the base Report class.
71
        return comments
72
73
6912.5.17 by Curtis Hovey
Revised the variable and method names. Simplified the extractComment loop.
74
    def _findFiles(self):
6912.5.15 by Curtis Hovey
added lib/mailman to the skip list.
75
        """Generate a list of matching files below a directory."""
6912.5.10 by Curtis Hovey
Added the optionsparser, moved several functions into the base Report class.
76
        for path, subdirs, files in os.walk(self.root_dir):
6912.5.15 by Curtis Hovey
added lib/mailman to the skip list.
77
            subdirs[:] = [dir_name for dir_name in subdirs
6912.5.17 by Curtis Hovey
Revised the variable and method names. Simplified the extractComment loop.
78
                          if self._isTraversable(path, dir_name)]
6912.5.10 by Curtis Hovey
Added the optionsparser, moved several functions into the base Report class.
79
            for file in files:
80
                file_path = os.path.join(path, file)
81
                if os.path.islink(file_path):
82
                    continue
6912.5.17 by Curtis Hovey
Revised the variable and method names. Simplified the extractComment loop.
83
                if excluded_file_re.match(file) is None:
6912.5.10 by Curtis Hovey
Added the optionsparser, moved several functions into the base Report class.
84
                    yield os.path.join(path, file)
85
6912.5.17 by Curtis Hovey
Revised the variable and method names. Simplified the extractComment loop.
86
    def _isTraversable(self, path, dir_name):
6912.5.16 by Curtis Hovey
Minor changes per review.
87
        """Return True if path/dir_name does not match dir_re pattern."""
6912.5.17 by Curtis Hovey
Revised the variable and method names. Simplified the extractComment loop.
88
        return excluded_dir_re.match(os.path.join(path, dir_name)) is None
6912.5.15 by Curtis Hovey
added lib/mailman to the skip list.
89
6912.5.17 by Curtis Hovey
Revised the variable and method names. Simplified the extractComment loop.
90
    def _extractComments(self, file_path):
6912.5.10 by Curtis Hovey
Added the optionsparser, moved several functions into the base Report class.
91
        """Return a list of XXX comments in a file.
92
93
        :param file_path: The path of the file that contains XXX comments.
94
        """
95
        comments = []
96
        file = open(file_path, 'r')
97
        try:
98
            comment = None
99
            for line_num, line in enumerate(file):
100
                xxx_mark = self.xxx_re.match(line)
101
                if xxx_mark is None and comment is None:
102
                    # The loop is not in a comment or starting a comment.
103
                    continue
6912.5.17 by Curtis Hovey
Revised the variable and method names. Simplified the extractComment loop.
104
                if xxx_mark is not None and comment is not None:
105
                    # Two XXX comments run together.
106
                    self._finaliseComment(comments, comment)
107
                    comment = None
108
                if xxx_mark is not None and comment is None:
6912.5.10 by Curtis Hovey
Added the optionsparser, moved several functions into the base Report class.
109
                    # Start a new comment.
6912.5.17 by Curtis Hovey
Revised the variable and method names. Simplified the extractComment loop.
110
                    comment = self.extractMetadata(line)
111
                    comment['file_path'] = file_path
112
                    comment['line_no'] = line_num + 1
113
                    comment['context_list'] = []
6912.5.10 by Curtis Hovey
Added the optionsparser, moved several functions into the base Report class.
114
                elif '#' in line and '##' not in line:
115
                    # Continue collecting the comment text.
6912.5.17 by Curtis Hovey
Revised the variable and method names. Simplified the extractComment loop.
116
                    leading_, text = line.split('#', 1)
117
                    comment['text_list'].append(text.lstrip())
118
                elif xxx_mark is None and len(comment['context_list']) < 2:
6912.5.10 by Curtis Hovey
Added the optionsparser, moved several functions into the base Report class.
119
                    # Collect the code context of the comment.
6912.5.17 by Curtis Hovey
Revised the variable and method names. Simplified the extractComment loop.
120
                    comment['context_list'].append(line)
121
                elif xxx_mark is None and len(comment['context_list']) == 2:
6912.5.10 by Curtis Hovey
Added the optionsparser, moved several functions into the base Report class.
122
                    # Finalise the comment.
6912.5.17 by Curtis Hovey
Revised the variable and method names. Simplified the extractComment loop.
123
                    comment['context_list'].append(line)
124
                    self._finaliseComment(comments, comment)
6912.5.10 by Curtis Hovey
Added the optionsparser, moved several functions into the base Report class.
125
                    comment = None
126
                else:
127
                    raise ValueError, (
128
                        "comment or xxx_mark are in an unknown state.")
6912.5.16 by Curtis Hovey
Minor changes per review.
129
            if comment is not None:
6912.5.17 by Curtis Hovey
Revised the variable and method names. Simplified the extractComment loop.
130
                self._finaliseComment(comments, comment)
6912.5.10 by Curtis Hovey
Added the optionsparser, moved several functions into the base Report class.
131
        finally:
132
            file.close()
133
        return comments
134
6912.5.17 by Curtis Hovey
Revised the variable and method names. Simplified the extractComment loop.
135
    def _finaliseComment(self, comments, comment):
136
        """Replace the lists with strs and append the comment to comments."""
137
        context = ''.join(comment['context_list'])
138
        if context.strip() == '':
139
            # Whitespace is not context; do not store it.
140
            context = ''
141
        comment['context'] = context
142
        comment['text'] = ''.join(comment['text_list']).strip()
143
        del comment['context_list']
144
        del comment['text_list']
6912.5.16 by Curtis Hovey
Minor changes per review.
145
        comments.append(comment)
146
6912.5.10 by Curtis Hovey
Added the optionsparser, moved several functions into the base Report class.
147
    # The standard XXX comment form of:
148
    # 'XXX: First Last Name 2007-07-01 bug=nnnn spec=cccc:'
6912.5.16 by Curtis Hovey
Minor changes per review.
149
    # Colons, commas, and spaces may follow each token.
6912.5.10 by Curtis Hovey
Added the optionsparser, moved several functions into the base Report class.
150
    xxx_person_date_re = re.compile(r"""
6912.5.11 by Curtis Hovey
Minor fixes.
151
        .*XXX[:,]?[ ]                               # The XXX indicator.
152
        (?P<person>[a-zA-Z][^:]*[\w])[,: ]*         # The persons's nick.
153
        (?P<date>\d\d\d\d[/-]?\d\d[/-]?\d\d)[,: ]*  # The date in YYYY-MM-DD.
154
        (?:[(]?bug[s]?[= ](?P<bug>[\w-]*)[),: ]*)?  # The bug id.
155
        (?:[(]?spec[= ](?P<spec>[\w-]*)[),: ]*)?    # The spec name.
156
        (?P<text>.*)                                # The comment text.
6912.5.10 by Curtis Hovey
Added the optionsparser, moved several functions into the base Report class.
157
        """, re.VERBOSE)
158
159
    # An uncommon XXX comment form of:
160
    # 'XXX: 2007-01-01 First Last Name bug=nnnn spec=cccc:'
161
    # Colons, commas, and spaces may follow each token.
162
    xxx_date_person_re = re.compile(r"""
6912.5.11 by Curtis Hovey
Minor fixes.
163
        .*XXX[:,]?[ ]                               # The XXX indicator.
164
        (?P<date>\d\d\d\d[/-]?\d\d[/-]?\d\d)[,: ]*  # The date in YYYY-MM-DD.
165
        (?P<person>[a-zA-Z][\w]+)[,: ]*             # The person's nick.
166
        (?:[(]?bug[s]?[= ](?P<bug>[\w-]*)[),: ]*)?  # The bug id.
167
        (?:[(]?spec[= ](?P<spec>[\w-]*)[),: ]*)?    # The spec name.
168
        (?P<text>.*)                                # The comment text.
6912.5.10 by Curtis Hovey
Added the optionsparser, moved several functions into the base Report class.
169
        """, re.VERBOSE)
170
6912.5.17 by Curtis Hovey
Revised the variable and method names. Simplified the extractComment loop.
171
    def extractMetadata(self, comment_line):
6912.5.10 by Curtis Hovey
Added the optionsparser, moved several functions into the base Report class.
172
        """Return a dict of metadata extracted from the comment line.
173
174
        :param comment_line: The first line of an XXX comment contains the
175
            metadata.
176
        :return: dict(person, date, bug, spec, and [text]). The text is the
177
        same as remainder of the comment_line after the metadata is extracted.
178
        """
179
        comment = dict(person=None, date=None, bug=None, spec=None, text=[])
180
        match = (self.xxx_person_date_re.match(comment_line)
181
                 or self.xxx_date_person_re.match(comment_line))
182
        if match is not None:
183
            # This comment follows a known style.
184
            comment['person'] = match.group('person')
185
            comment['date'] = match.group('date')
186
            comment['bug'] = match.group('bug') or None
187
            comment['spec'] = match.group('spec') or None
188
            text = match.group('text').lstrip(':, ')
189
        else:
190
            # Unknown comment format.
191
            text = comment_line
192
193
        text = text.strip()
6912.5.17 by Curtis Hovey
Revised the variable and method names. Simplified the extractComment loop.
194
        comment['text_list'] = [text + '\n']
6912.5.10 by Curtis Hovey
Added the optionsparser, moved several functions into the base Report class.
195
        return comment
6912.5.1 by Curtis Hovey
Initial addition of the XXX report from the old 3732 branch.
196
6912.5.14 by Curtis Hovey
Added 'not-used' to the list of dirs not to traverse.
197
    def write(self):
198
        """Write the total count of comments."""
199
        output_file = self._open()
200
        try:
201
            output_file.write('%s\n' % len(self.comments))
202
            output_file.flush()
203
        finally:
204
            self._close(output_file)
205
206
    def _open(self):
207
        """Open the output_name or use STDOUT."""
208
        if self.output_name is not None:
209
            return open(self.output_name, 'w')
210
        return sys.stdout
211
6912.5.1 by Curtis Hovey
Initial addition of the XXX report from the old 3732 branch.
212
6912.5.9 by Curtis Hovey
Moved the HTML report code into a single class.
213
class HTMLReport(Report):
214
    """A HTML XXX report."""
215
    # Match URLs.
216
    http_re = re.compile('(https?://[^ \n&]*)')
217
218
    # Match bugs.
219
    bug_link_re = re.compile(r'\b(bugs?:?) #?(\d+)', re.IGNORECASE)
220
221
    report_top = """\
6912.5.1 by Curtis Hovey
Initial addition of the XXX report from the old 3732 branch.
222
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
223
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
224
<html>
225
  <head>
226
    <title>XXX Comment report for Launchpad</title>
227
    <style type="text/css" media="screen, print">
228
      @import url(https://launchpad.net/+icing/rev6895/+style-slimmer.css);
229
      .context {
230
        border: #666 1px dashed;
231
        width: 50em;
232
      }
233
    </style>
234
  </head>
235
236
  <body style="margin: 1em;">
237
    <h1>XXX Comment report for Launchpad</h1>
238
239
    <p>
240
      A report of the XXX comments in the rocketfuel branch. All files
241
      except *.pyc files were examined.
242
      <br />This report may also be available as a tab-delimted file:
6912.5.13 by Curtis Hovey
Fixed spec links.
243
      <a href="xxx-report.csv">xxx-report.csv</a>
6912.5.1 by Curtis Hovey
Initial addition of the XXX report from the old 3732 branch.
244
    </p>
245
246
    <h3>Summary</h3>
247
248
    <p>
249
      There are <strong>%(commentcount)s XXX comments</strong>
250
      in <strong>revno: %(revno)s</strong>.
251
      <br />Generated on %(reporttime)s.
252
    </p>
253
254
    <hr/>
255
256
    <h3>Listing</h3>
257
6912.5.9 by Curtis Hovey
Moved the HTML report code into a single class.
258
    <ol>"""
6912.5.8 by Curtis Hovey
Added documentation to xxxreport.
259
6912.5.9 by Curtis Hovey
Moved the HTML report code into a single class.
260
    report_comment = """
6912.5.1 by Curtis Hovey
Initial addition of the XXX report from the old 3732 branch.
261
      <li>
262
        <div>
263
          <strong>File: %(file_path)s:%(line_no)s</strong>
264
        </div>
265
        <div style="margin: .5em 0em 0em 0em;">
266
        <strong class="xxx">XXX</strong>:
267
        <strong class="person">%(person)s</strong>
268
        <strong class="date">%(date)s</strong>
269
        bug %(bugurl)s
6912.5.13 by Curtis Hovey
Fixed spec links.
270
        spec %(specurl)s
6912.5.1 by Curtis Hovey
Initial addition of the XXX report from the old 3732 branch.
271
        </div>
272
        <pre style="margin-top: 0px;">%(text)s</pre>
273
        <pre class="context">%(context)s</pre>
6912.5.9 by Curtis Hovey
Moved the HTML report code into a single class.
274
      </li>"""
6912.5.8 by Curtis Hovey
Added documentation to xxxreport.
275
6912.5.9 by Curtis Hovey
Moved the HTML report code into a single class.
276
    report_bottom = """
6912.5.1 by Curtis Hovey
Initial addition of the XXX report from the old 3732 branch.
277
    </ol>
278
  </body>
6912.5.8 by Curtis Hovey
Added documentation to xxxreport.
279
</html>
280
"""
281
6912.5.9 by Curtis Hovey
Moved the HTML report code into a single class.
282
    def write(self):
6912.5.10 by Curtis Hovey
Added the optionsparser, moved several functions into the base Report class.
283
        """Write the report in HTML format."""
6912.5.9 by Curtis Hovey
Moved the HTML report code into a single class.
284
        report_time = time.strftime(
285
            "%a, %d %b %Y %H:%M:%S UTC", time.gmtime())
6912.5.10 by Curtis Hovey
Added the optionsparser, moved several functions into the base Report class.
286
        output_file = self._open()
6912.5.9 by Curtis Hovey
Moved the HTML report code into a single class.
287
        try:
288
            output_file.write(
289
                self.report_top % {"commentcount": len(self.comments),
290
                              "reporttime": report_time,
291
                              "revno": self.revno})
292
293
            for comment in self.comments:
6912.5.17 by Curtis Hovey
Revised the variable and method names. Simplified the extractComment loop.
294
                comment['text'] = self.markupText(comment['text'])
295
                comment['context'] = self.markupText(comment['context'])
6912.5.9 by Curtis Hovey
Moved the HTML report code into a single class.
296
                if comment['bug'] is not None:
297
                    comment['bugurl'] = (
6912.5.13 by Curtis Hovey
Fixed spec links.
298
                        '<a href="https://bugs.launchpad.net/bugs/%s">%s</a>'
6912.5.9 by Curtis Hovey
Moved the HTML report code into a single class.
299
                        % (comment['bug'], comment['bug']))
300
                else:
301
                    comment['bugurl'] = comment['bug']
6912.5.13 by Curtis Hovey
Fixed spec links.
302
                if comment['spec'] is not None:
303
                    comment['specurl'] = (
304
                        '<a href="https://blueprints.launchpad.net'
305
                        '/launchpad-project/+specs?searchtext=%s">%s</a>'
306
                        % (comment['spec'], comment['spec']))
307
                else:
308
                    comment['specurl'] = comment['spec']
6912.5.9 by Curtis Hovey
Moved the HTML report code into a single class.
309
                output_file.write(self.report_comment % comment)
310
311
            output_file.write(self.report_bottom)
312
            output_file.flush()
313
        finally:
6912.5.10 by Curtis Hovey
Added the optionsparser, moved several functions into the base Report class.
314
            self._close(output_file)
6912.5.9 by Curtis Hovey
Moved the HTML report code into a single class.
315
6912.5.17 by Curtis Hovey
Revised the variable and method names. Simplified the extractComment loop.
316
    def markupText(self, text):
6912.5.9 by Curtis Hovey
Moved the HTML report code into a single class.
317
        """Return the line as HTML markup.
318
319
        :param text: The text to escape and link.
320
        """
321
        text = cgi.escape(text)
322
        text = self.http_re.sub(r'<a href="\1">\1</a>', text)
323
        bug_sub = r'<a href="https://bugs.launchpad.net/bugs/\2">\1 \2</a>'
324
        text = self.bug_link_re.sub(bug_sub, text)
325
        return text
6912.5.8 by Curtis Hovey
Added documentation to xxxreport.
326
327
6912.5.10 by Curtis Hovey
Added the optionsparser, moved several functions into the base Report class.
328
class CSVReport(Report):
329
    """A CSV XXX report."""
330
    report_header = (
331
        'File_Path, Line_No, Person, Date, Bug, Spec, Text\n')
332
    report_comment = (
333
        '%(file_path)s, %(line_no)s, '
334
        '%(person)s, %(date)s, %(bug)s, %(spec)s, %(text)s\n')
335
6912.5.17 by Curtis Hovey
Revised the variable and method names. Simplified the extractComment loop.
336
    def markupText(self, text):
6912.5.10 by Curtis Hovey
Added the optionsparser, moved several functions into the base Report class.
337
        """Return the line as TSV markup.
338
339
        :param text: The text to escape.
340
        """
341
        if text is not None:
342
            return text.replace('\n', ' ').replace(',', ';')
343
344
    def write(self):
345
        """Write the report in CSV format."""
346
        output_file = self._open()
347
        try:
348
            output_file.write(self.report_header)
349
            for comment in self.comments:
6912.5.17 by Curtis Hovey
Revised the variable and method names. Simplified the extractComment loop.
350
                comment['person'] = self.markupText(comment['person'])
351
                comment['text'] = self.markupText(comment['text'])
6912.5.10 by Curtis Hovey
Added the optionsparser, moved several functions into the base Report class.
352
                output_file.write(self.report_comment % comment)
353
            output_file.flush()
354
        finally:
355
            self._close(output_file)
356
357
358
class TSVReport(CSVReport):
359
    """A TSV XXX report."""
360
    report_header = (
361
        'File_Path\tLine_No\tPerson\tDate\tBug\tSpec\tText\n')
362
    report_comment = (
363
        '%(file_path)s\t%(line_no)s\t'
364
        '%(person)s\t%(date)s\t%(bug)s\t%(spec)s\t%(text)s\n')
365
6912.5.17 by Curtis Hovey
Revised the variable and method names. Simplified the extractComment loop.
366
    def markupText(self, text):
6912.5.10 by Curtis Hovey
Added the optionsparser, moved several functions into the base Report class.
367
        """Return the line as TSV markup.
368
369
        :param text: The text to escape.
370
        """
371
        if text is not None:
372
            return text.replace('\n', ' ').replace('\t', ' ')
373
374
375
def get_option_parser():
376
    """Return the option parser for this program."""
377
    usage = dedent("""    %prog [options] <root-dir>
378
379
    Create a report of all the XXX comments in the files below a directory.
380
    Set the -f option to 'count' to print the total number of XXX comments,
381
    which is the default when -f is not set.""")
382
    parser = OptionParser(usage=usage)
383
    parser.add_option(
384
        "-f", "--format", dest="format", default="count",
385
        help="the format of the report: count, html, csv, tsv")
386
    parser.add_option(
387
        "-o", "--output", dest="output_name",
388
        help="the name of the output file, otherwise STDOUT is used")
389
    return parser
6912.5.1 by Curtis Hovey
Initial addition of the XXX report from the old 3732 branch.
390
391
392
def main(argv=None):
393
    """Run the command line operations."""
394
    if argv is None:
395
        argv = sys.argv
6912.5.10 by Curtis Hovey
Added the optionsparser, moved several functions into the base Report class.
396
    parser = get_option_parser()
397
    (options, arguments) = parser.parse_args(args=argv[1:])
6912.5.12 by Curtis Hovey
Revised Makefile rules for xxxreport.
398
    if len(arguments) != 1:
6912.5.10 by Curtis Hovey
Added the optionsparser, moved several functions into the base Report class.
399
        parser.error('No root directory was provided.')
6912.5.12 by Curtis Hovey
Revised Makefile rules for xxxreport.
400
    root_dir = arguments[0]
6912.5.1 by Curtis Hovey
Initial addition of the XXX report from the old 3732 branch.
401
6912.5.10 by Curtis Hovey
Added the optionsparser, moved several functions into the base Report class.
402
    if options.format.lower() == 'html':
403
        report = HTMLReport(root_dir, options.output_name)
404
    elif options.format.lower() == 'tsv':
405
        report = TSVReport(root_dir, options.output_name)
406
    elif options.format.lower() == 'csv':
407
        report = CSVReport(root_dir, options.output_name)
6912.5.1 by Curtis Hovey
Initial addition of the XXX report from the old 3732 branch.
408
    else:
6912.5.12 by Curtis Hovey
Revised Makefile rules for xxxreport.
409
        report = Report(root_dir, options.output_name)
6912.5.10 by Curtis Hovey
Added the optionsparser, moved several functions into the base Report class.
410
    report.write()
411
    sys.exit(0)
6912.5.1 by Curtis Hovey
Initial addition of the XXX report from the old 3732 branch.
412
413
414
if __name__ == '__main__':
415
    sys.exit(main())
416