1#!/usr/bin/python
2# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6""" Parse suite control files and make HTML documentation from included tests.
7
8This program will create a list of test cases found in suite files by parsing
9through each suite control file and making a list of all of the jobs called from
10it. Once it has a list of tests, it will parse the AutoTest control file for
11each test and grab the doc strings. These doc strings, along with any
12constraints in the suite control file, will be added to the original test
13script. These new scripts will be placed in a stand alone directory. Doxygen
14will then use these files for the sole purpose of producing HTML documentation
15for all of the tests. Once HTML docs are created some post processing will be
16done against the docs to change a few strings.
17
18If this script is executed without a --src argument, it will assume it is being
19executed from <ChromeOS>/src/third_party/autotest/files/utils/docgen/ directory.
20
21Classes:
22
23  DocCreator
24    This class is responsible for all processing. It requires the following:
25      - Absolute path of suite control files.
26      - Absolute path of where to place temporary files it constructs from the
27        control files and test scripts.
28    This class makes the following assumptions:
29      - Each master suite has a README.txt file with general instructions on
30        test preparation and usage.
31      - The control file for each test has doc strings with labels of:
32        - PURPOSE: one line description of why this test exists.
33        - CRITERIA: Pass/Failure conditions.
34        - DOC: additional test details.
35  ReadNode
36    This class parses a node from a control file into a key/value pair. In this
37    context, a node represents a syntactic construct of an abstract syntax tree.
38    The root of the tree is the module object (in this case a control file). If
39    suite=True, it will assume the node is from a suite control file.
40
41Doxygen should already be configured with a configuration file called:
42doxygen.conf. This file should live in the same directory with this program.
43If you haven't installed doxygen, you'll need to install this program before
44this script is executed. This program will automatically update the doxygen.conf
45file to match self.src_tests and self.html.
46
47TODO: (kdlucas@google.com) Update ReadNode class to use the replacement module
48for the compiler module, as that has been deprecated.
49"""
50
51__author__ = 'kdlucas@google.com (Kelly Lucas)'
52__version__ = '0.9.1'
53
54import compiler
55import fileinput
56import glob
57import logging
58import optparse
59import os
60import shutil
61import subprocess
62import sys
63
64import fs_find_tests
65
66
67class DocCreator(object):
68    """Process suite control files to combine docstrings and create HTML docs.
69
70    The DocCreator class is designed to parse AutoTest suite control files to
71    find all of the tests referenced, and build HTML documentation based on the
72    docstrings in those files. It will cross reference the test control file
73    and any parameters passed through the suite file, with the original test
74    case. DocCreator relies on doxygen to actually generate the HTML documents.
75
76    The workflow is as follows:
77        - Parse the suite file(s) and generate a test list.
78        - Locate the test source, and grab the docstrings from the associated
79          AutoTest control file.
80        - Combine the docstring from the control file with any parameters passed
81          in from the suite control file, with the original test case.
82        - Write a new test file with the combined docstrings to src_tests.
83        - Create HTML documentation by running doxygen against the tests stored
84          in self.src_tests.
85
86    Implements the following methods:
87        - GetTests() - Parse suite control files, create a dictionary of tests.
88        - ParseControlFiles() - Runs through all tests and parses control files
89        - _CleanDir() - Remove any files in a direcory and create an empty one.
90        - _GetDoctString() - Parses docstrings and joins it with constraints.
91        - _CreateTest() - Add docstrings and constraints to existing test script
92          to form a new test script.
93        - CreateMainPage() - Create a mainpage.txt file based on contents of the
94          suite README file.
95        - _ConfigDoxygen - Updates doxygen.conf to match some attributes this
96          script was run with.
97        - RunDoxygen() - Executes the doxygen program.
98        - CleanDocs() - Changes some text in the HTML files to conform to our
99          naming conventions and style.
100
101    Depends upon class ReadNode.
102    """
103    def __init__(self, options, args, logger):
104        """Parse command line arguments and set some initial variables."""
105
106        self.options = options
107        self.args = args
108        self.logger = logger
109
110        # Make parameters a little shorter by making the following assignments.
111        if options.all_tests:
112            self.suite = 'suite_All'
113        else:
114            self.suite = self.options.suite
115
116        self.autotest_root = self.options.autotest_dir
117        self.debug = self.options.debug
118        self.docversion = self.options.docversion
119        self.doxyconf = self.options.doxyconf
120        self.html = '%s_%s' % (self.suite, self.options.html)
121        self.latex = self.options.latex
122        self.layout = self.options.layout
123        self.logfile = self.options.logfile
124        self.readme = self.options.readme
125        self.src_tests = '%s_%s' % (self.suite, self.options.src_tests)
126
127        self.testcase = {}
128        self.testcase_src = {}
129
130        self.site_dir = os.path.join(self.autotest_root, 'client', 'site_tests')
131        self.test_dir = os.path.join(self.autotest_root, 'client', 'tests')
132        self.suite_dir = os.path.join(self.site_dir, self.suite)
133
134        self.logger.debug('Executing with debug level: %s', self.debug)
135        self.logger.debug('Writing to logfile: %s', self.logfile)
136        self.logger.debug('New test directory: %s', self.src_tests)
137        self.logger.debug('Test suite: %s', self.suite)
138
139        self.suitename = {
140                          'suite_All': 'All Existing Autotest Tests',
141                          'suite_Factory': 'Factory Testing',
142                          'suite_HWConfig': 'Hardware Configuration',
143                          'suite_HWQual': 'Hardware Qualification',
144                         }
145
146    def GetAllTests(self):
147        """Create list of all discovered tests."""
148        for path in [ 'server/tests', 'server/site_tests', 'client/tests',
149                      'client/site_tests']:
150            test_path = os.path.join(self.autotest_root, path)
151            if not os.path.exists(test_path):
152                continue
153            self.logger.info("Scanning %s", test_path)
154            tests, tests_src = fs_find_tests.GetTestsFromFS(test_path,
155                                                            self.logger)
156            test_intersection = set(self.testcase) & set(tests)
157            if test_intersection:
158                self.logger.warning("Duplicates found: %s", test_intersection)
159            self.testcase.update(tests)
160            self.testcase_src.update(tests_src)
161
162    def GetTestsFromSuite(self):
163        """Create list of tests invoked by a suite."""
164
165        suite_search = os.path.join(self.suite_dir, 'control.*')
166        for suitefile in glob.glob(suite_search):
167            self.logger.debug('Scanning %s for tests', suitefile)
168            if os.path.isfile(suitefile):
169                try:
170                    suite = compiler.parseFile(suitefile)
171                except SyntaxError, e:
172                    self.logger.error('Error parsing (gettests): %s\n%s',
173                                      suitefile, e)
174                    raise SystemExit
175
176            # Walk through each node found in the control file, which in our
177            # case will be a call to a test. compiler.walk() will walk through
178            # each component node, and call the appropriate function in class
179            # ReadNode. The returned key should be a string, and the name of a
180            # test. visitor.value should be any extra arguments found in the
181            # suite file that are used with that test case.
182            for n in suite.node.nodes:
183                visitor = ReadNode(suite=True)
184                compiler.walk(n, visitor)
185                if len(visitor.key) > 1:
186                    filtered_input = ''
187                    # Lines in value should start with '  -' for bullet item.
188                    if visitor.value:
189                        lines = visitor.value.split('\n')
190                        for line in lines:
191                            if line.startswith('  -'):
192                                filtered_input += line + '\n'
193                    # A test could be called multiple times, so see if the key
194                    # already exists, and if so append the new value.
195                    if visitor.key in self.testcase:
196                        s = self.testcase[visitor.key] + filtered_input
197                        self.testcase[visitor.key] = s
198                    else:
199                        self.testcase[visitor.key] = filtered_input
200
201    def GetTests(self):
202        """Create dictionary of tests based on suite control file contents."""
203        if self.options.all_tests:
204            self.GetAllTests()
205        else:
206            self.GetTestsFromSuite()
207
208    def _CleanDir(self, directory):
209        """Ensure the directory is available and empty.
210
211        Args:
212            directory: string, path of directory
213        """
214
215        if os.path.isdir(directory):
216            try:
217                shutil.rmtree(directory)
218            except IOError, err:
219                self.logger.error('Error cleaning %s\n%s', directory, err)
220        try:
221            os.makedirs(directory)
222        except IOError, err:
223            self.logger.error('Error creating %s\n%s', directory, err)
224            self.logger.error('Check your permissions of %s', directory)
225            raise SystemExit
226
227    def LocateTest(self, test_name):
228        """Determine the full path location of the test."""
229        if test_name in self.testcase_src:
230            return os.path.join(self.testcase_src[test_name], test_name)
231
232        test_dir = os.path.join(self.site_dir, test_name)
233        if not os.path.isdir(test_dir):
234            test_dir = os.path.join(self.test_dir, test_name)
235        if os.path.isdir(test_dir):
236            return test_dir
237
238        self.logger.warning('Cannot find test: %s', test)
239        return None
240
241
242    def ParseControlFiles(self):
243        """Get docstrings from control files and add them to new test scripts.
244
245        This method will cycle through all of the tests and attempt to find
246        their control file. If found, it will parse the docstring from the
247        control file, add this to any parameters found in the suite file, and
248        add this combined docstring to the original test. These new tests will
249        be written in the self.src_tests directory.
250        """
251        # Clean some target directories.
252        for d in [self.src_tests, self.html]:
253            self._CleanDir(d)
254
255        for test in self.testcase:
256            test_dir = self.LocateTest(test)
257            if test_dir:
258                control_file = os.path.join(test_dir, 'control')
259                test_file = os.path.join(test_dir, test + '.py')
260                docstring = self._GetDocString(control_file, test)
261                self._CreateTest(test_file, docstring, test)
262
263    def _GetDocString(self, control_file, test):
264        """Get the docstrings from control file and join to suite file params.
265
266        Args:
267            control_file: string, absolute path to test control file.
268            test: string, name of test.
269        Returns:
270            string: combined docstring with needed markup language for doxygen.
271        """
272
273        # Doxygen needs the @package marker.
274        package_doc = '## @package '
275        # To allow doxygen to use special commands, we must use # for comments.
276        comment = '# '
277        endlist = '  .\n'
278        control_dict = {}
279        output = []
280        temp = []
281        tempstring = ''
282        docstring = ''
283        keys = ['\\brief\n', '<H3>Pass/Fail Criteria:</H3>\n',
284                '<H3>Author</H3>\n', '<H3>Test Duration</H3>\n',
285                '<H3>Category</H3>\n', '<H3>Test Type</H3>\n',
286                '<H3>Test Class</H3>\n', '<H3>Notest</H3>\n',
287               ]
288
289        if not os.path.isfile(control_file):
290            self.logger.error('Cannot find: %s', control_file)
291            return None
292        try:
293            control = compiler.parseFile(control_file)
294        except SyntaxError, e:
295            self.logger.error('Error parsing (docstring): %s\n%s',
296                              control_file, e)
297            return None
298
299        for n in control.node.nodes:
300            visitor = ReadNode()
301            compiler.walk(n, visitor)
302            control_dict[visitor.key] = visitor.value
303
304        for k in keys:
305            if k in control_dict:
306                if len(control_dict[k]) > 1:
307                    if k != test:
308                        temp.append(k)
309                    temp.append(control_dict[k])
310                    if control_dict[k]:
311                        temp.append(endlist)
312                    # Add constraints and extra args after the Criteria section.
313                    if 'Criteria:' in k:
314                        if self.testcase[test]:
315                            temp.append('<H3>Arguments:</H3>\n')
316                            temp.append(self.testcase[test])
317                            # '.' character at the same level as the '-' tells
318                            # doxygen this is the end of the list.
319                            temp.append(endlist)
320
321        output.append(package_doc + test + '\n')
322        tempstring = "".join(temp)
323        lines = tempstring.split('\n')
324        for line in lines:
325            # Doxygen requires a '#' character to add special doxygen commands.
326            comment_line = comment + line + '\n'
327            output.append(comment_line)
328
329        docstring = "".join(output)
330
331        return docstring
332
333
334    def _CreateTest(self, test_file, docstring, test):
335        """Create a new test with the combined docstrings from multiple sources.
336
337        Args:
338            test_file: string, file name of new test to write.
339            docstring: string, the docstring to add to the existing test.
340            test: string, name of the test.
341
342        This method is used to create a temporary copy of a new test, that will
343        be a combination of the original test plus the docstrings from the
344        control file, and any constraints from the suite control file.
345        """
346
347        class_def = 'class ' + test
348        pathname = os.path.join(self.src_tests, test + '.py')
349
350        # Open the test and write out new test with added docstrings
351        try:
352            f = open(test_file, 'r')
353        except IOError, err:
354            self.logger.error('Error while reading %s\n%s', test_file, err)
355            return
356        lines = f.readlines()
357        f.close()
358
359        try:
360            f = open(pathname, 'w')
361        except IOError, err:
362            self.logger.error('Error creating %s\n%s', pathname, err)
363            return
364
365        for line in lines:
366            if class_def in line and docstring:
367                f.write(docstring)
368                f.write('\n')
369            f.write(line)
370        f.close()
371
372    def CreateMainPage(self, current_dir):
373        """Create a main page to provide content for index.html.
374
375        This method assumes a file named README.txt is located in your suite
376        directory with general instructions on setting up and using the suite.
377        If your README file is in another file, ensure you pass a --readme
378        option with the correct filename. To produce a better looking
379        landing page, use the '-' character for list items. This method assumes
380        os commands start with '$'.
381        """
382
383        # Define some strings that Doxygen uses for specific formatting.
384        cstart = '/**'
385        cend = '**/'
386        mp = '@mainpage'
387        section_begin = '@section '
388        vstart = '@verbatim '
389        vend = ' @endverbatim\n'
390
391        # Define some characters we expect to delineate sections in the README.
392        sec_char = '=========='
393        command_prompt = '$ '
394        crosh_prompt = 'crosh>'
395        command_cont = '\\'
396
397        command = False
398        comment = False
399        section = False
400        sec_ctr = 0
401
402        if self.options.all_tests:
403            readme_file = os.path.join(current_dir, self.readme)
404        else:
405            readme_file = os.path.join(self.suite_dir, self.readme)
406        mainpage_file = os.path.join(self.src_tests, 'mainpage.txt')
407
408        try:
409            f = open(readme_file, 'r')
410        except IOError, err:
411            self.logger.error('Error opening %s\n%s', readme_file, err)
412            return
413        try:
414            fw = open(mainpage_file, 'w')
415        except IOError, err:
416            self.logger.error('Error opening %s\n%s', mainpage_file, err)
417            return
418
419        lines = f.readlines()
420        f.close()
421
422        fw.write(cstart)
423        fw.write('\n')
424        fw.write(mp)
425        fw.write('\n')
426
427        for line in lines:
428            if sec_char in line:
429                comment = True
430                section = not section
431            elif section:
432                sec_ctr += 1
433                section_name = ' section%d ' % sec_ctr
434                fw.write(section_begin + section_name + line)
435            else:
436                # comment is used to denote when we should start recording text
437                # from the README file. Some of the initial text is not needed.
438                if comment:
439                    if command_prompt in line or crosh_prompt in line:
440                        line = line.rstrip()
441                        if line[-1] == command_cont:
442                            fw.write(vstart + line[:-1])
443                            command = True
444                        else:
445                            fw.write(vstart + line + vend)
446                    elif command:
447                        line = line.strip()
448                        if line[-1] == command_cont:
449                          fw.write(line)
450                        else:
451                          fw.write(line + vend)
452                          command = False
453                    else:
454                        fw.write(line)
455
456        fw.write('\n')
457        fw.write(cend)
458        fw.close()
459
460    def _ConfigDoxygen(self):
461        """Set Doxygen configuration to match our options."""
462
463        doxy_config = {
464                       'ALPHABETICAL_INDEX': 'YES',
465                       'EXTRACT_ALL': 'YES',
466                       'EXTRACT_LOCAL_METHODS': 'YES',
467                       'EXTRACT_PRIVATE': 'YES',
468                       'EXTRACT_STATIC': 'YES',
469                       'FILE_PATTERNS': '*.py *.txt',
470                       'FULL_PATH_NAMES ': 'YES',
471                       'GENERATE_TREEVIEW': 'YES',
472                       'HTML_DYNAMIC_SECTIONS': 'YES',
473                       'HTML_FOOTER': 'footer.html',
474                       'HTML_HEADER': 'header.html',
475                       'HTML_OUTPUT ': self.html,
476                       'INLINE_SOURCES': 'YES',
477                       'INPUT ': self.src_tests,
478                       'JAVADOC_AUTOBRIEF': 'YES',
479                       'LATEX_OUTPUT ': self.latex,
480                       'LAYOUT_FILE ': self.layout,
481                       'OPTIMIZE_OUTPUT_JAVA': 'YES',
482                       'PROJECT_NAME ': self.suitename[self.suite],
483                       'PROJECT_NUMBER': self.docversion,
484                       'SOURCE_BROWSER': 'YES',
485                       'STRIP_CODE_COMMENTS': 'NO',
486                       'TAB_SIZE': '4',
487                       'USE_INLINE_TREES': 'YES',
488                      }
489
490        doxy_layout = {
491                       'tab type="mainpage"': 'title="%s"' %
492                         self.suitename[self.suite],
493                       'tab type="namespaces"': 'title="Tests"',
494                       'tab type="namespacemembers"': 'title="Test Functions"',
495                      }
496
497        for line in fileinput.input(self.doxyconf, inplace=1):
498            for k in doxy_config:
499                if line.startswith(k):
500                    line = '%s = %s\n' % (k, doxy_config[k])
501            sys.stdout.write(line)
502
503        for line in fileinput.input('header.html', inplace=1):
504            if line.startswith('<H2>'):
505                line = '<H2>%s</H2>\n' % self.suitename[self.suite]
506            sys.stdout.write(line)
507
508        for line in fileinput.input(self.layout, inplace=1):
509            for k in doxy_layout:
510                if line.find(k) != -1:
511                    line = line.replace('title=""', doxy_layout[k])
512            sys.stdout.write(line.rstrip() + '\n')
513
514    def RunDoxygen(self, doxyargs):
515        """Execute Doxygen on the files in the self.src_tests directory.
516
517        Args:
518          doxyargs: string, any command line args to be passed to doxygen.
519        """
520
521        doxycmd = 'doxygen %s' % doxyargs
522
523        p = subprocess.Popen(doxycmd, shell=True, stdout=subprocess.PIPE,
524                             stderr=subprocess.PIPE)
525        stdout, stderr = p.communicate()
526        if p.returncode:
527            self.logger.error('Error while running %s', doxycmd)
528            self.logger.error(stdout)
529            self.logger.error(stderr)
530        else:
531            self.logger.info('%s successfully ran', doxycmd)
532
533    def CreateDocs(self):
534        """Configure and execute Doxygen to create HTML docuements."""
535
536        # First run doxygen with args to create default configuration files.
537        # Create layout xml file.
538        doxyargs = '-l %s' % self.layout
539        self.RunDoxygen(doxyargs)
540
541        # Create doxygen configuration file.
542        doxyargs = '-g %s' % self.doxyconf
543        self.RunDoxygen(doxyargs)
544
545        # Edit the configuration files to match our options.
546        self._ConfigDoxygen()
547
548        # Run doxygen with configuration file as argument.
549        self.RunDoxygen(self.doxyconf)
550
551    def PostProcessDocs(self, current_dir):
552        """Run some post processing on the newly created docs."""
553
554        # Key = original string, value = replacement string.
555        replace = {
556                   '>Package': '>Test',
557                  }
558
559        docpages = os.path.join(self.html, '*.html')
560        files = glob.glob(docpages)
561        for file in files:
562            for line in fileinput.input(file, inplace=1):
563                for k in replace:
564                    if line.find(k) != -1:
565                        line = line.replace(k, replace[k])
566                print line,
567
568        logo_image = 'customLogo.gif'
569        html_root = os.path.join(current_dir, self.html)
570        shutil.copy(os.path.join(current_dir, logo_image), html_root)
571
572        # Copy under dashboard.
573        if self.options.dashboard:
574            dashboard_root = os.path.join(self.autotest_root, 'results',
575                                          'dashboard', 'testdocs')
576            if not os.path.isdir(dashboard_root):
577                try:
578                    os.makedirs(dashboard_root)
579                except e:
580                    self.logger.error('Error creating %s:%s', dashboard_root, e)
581                    return
582            os.system('cp -r %s/* %s' % (html_root, dashboard_root))
583            os.system('find %s -type d -exec chmod 755 {} \;' % dashboard_root)
584            os.system('find %s -type f -exec chmod 644 {} \;' % dashboard_root)
585
586        self.logger.info('Sanitized documentation completed.')
587
588
589class ReadNode(object):
590    """Parse a compiler node object from a control file.
591
592    Args:
593        suite: boolean, set to True if parsing nodes from a suite control file.
594    """
595
596    def __init__(self, suite=False):
597        self.key = ''
598        self.value = ''
599        self.testdef = False
600        self.suite = suite
601        self.bullet = '  - '
602
603    def visitName(self, n):
604        if n.name == 'job':
605            self.testdef = True
606
607    def visitConst(self, n):
608        if self.testdef:
609            self.key = str(n.value)
610            self.testdef = False
611        else:
612            self.value += str(n.value) + '\n'
613
614    def visitKeyword(self, n):
615        if n.name != 'constraints':
616            self.value += self.bullet + n.name + ': '
617        for item in n.expr:
618            if isinstance(item, compiler.ast.Const):
619                for i in item:
620                    self.value += self.bullet + str(i) + '\n'
621                self.value += '  .\n'
622            else:
623                self.value += str(item) + '\n'
624
625
626    def visitAssName(self, n):
627        # To remove section from appearing in the documentation, set value = ''.
628        sections = {
629                    'AUTHOR': '',
630                    'CRITERIA': '<H3>Pass/Fail Criteria:</H3>\n',
631                    'DOC': '<H3>Notes</H3>\n',
632                    'NAME': '',
633                    'PURPOSE': '\\brief\n',
634                    'TIME': '<H3>Test Duration</H3>\n',
635                    'TEST_CATEGORY': '<H3>Category</H3>\n',
636                    'TEST_CLASS': '<H3>Test Class</H3>\n',
637                    'TEST_TYPE': '<H3>Test Type</H3>\n',
638                   }
639
640        if not self.suite:
641            self.key = sections.get(n.name, n.name)
642
643
644def ParseOptions(current_dir):
645    """Common processing of command line options."""
646
647    desc="""%prog will scan AutoTest suite control files to build a list of
648    test cases called in the suite, and build HTML documentation based on
649    the docstrings it finds in the tests, control files, and suite control
650    files.
651    """
652    parser = optparse.OptionParser(description=desc,
653                                   prog='CreateDocs',
654                                   version=__version__,
655                                   usage='%prog')
656    parser.add_option('--alltests',
657                      help='Scan for all tests',
658                      action='store_true',
659                      default=False,
660                      dest='all_tests')
661    parser.add_option('--autotest_dir',
662                      help='path to autotest root directory'
663                           ' [default: %default]',
664                      default=None,
665                      dest='autotest_dir')
666    parser.add_option('--dashboard',
667                      help='Copy output under dashboard',
668                      action='store_true',
669                      default=False,
670                      dest='dashboard')
671    parser.add_option('--debug',
672                      help='Debug level [default: %default]',
673                      default='debug',
674                      dest='debug')
675    parser.add_option('--docversion',
676                      help='Specify a version for the documentation'
677                           '[default: %default]',
678                      default=None,
679                      dest='docversion')
680    parser.add_option('--doxy',
681                      help='doxygen configuration file [default: %default]',
682                      default=os.path.join(current_dir, 'doxygen.conf'),
683                      dest='doxyconf')
684    parser.add_option('--html',
685                      help='path to store html docs [default: %default]',
686                      default='html',
687                      dest='html')
688    parser.add_option('--latex',
689                      help='path to store latex docs [default: %default]',
690                      default='latex',
691                      dest='latex')
692    parser.add_option('--layout',
693                      help='doxygen layout file [default: %default]',
694                      default=os.path.join(current_dir, 'doxygenLayout.xml'),
695                      dest='layout')
696    parser.add_option('--log',
697                      help='Logfile for program output [default: %default]',
698                      default=os.path.join(current_dir, 'docCreator.log'),
699                      dest='logfile')
700    parser.add_option('--readme',
701                      help='filename of suite documentation'
702                           '[default: %default]',
703                      default='README.txt',
704                      dest='readme')
705    parser.add_option('--suite',
706                      help='Directory name of suite [default: %default]',
707                      type='choice',
708                      default='suite_HWQual',
709                      choices = [
710                                 'suite_Factory',
711                                 'suite_HWConfig',
712                                 'suite_HWQual',
713                                ],
714                      dest='suite')
715    parser.add_option('--tests',
716                      help='Absolute path of temporary test files'
717                           ' [default: %default]',
718                      default='testsource',
719                      dest='src_tests')
720    return parser.parse_args()
721
722
723def CheckOptions(options, logger):
724    """Verify required command line options."""
725
726    if not options.autotest_dir:
727        logger.error('You must supply --autotest_dir')
728        raise SystemExit
729
730    if not os.path.isfile(options.doxyconf):
731        logger.error('Unable to locate --doxy: %s', options.doxyconf)
732        raise SystemExit
733
734    if not os.path.isfile(options.layout):
735        logger.error('Unable to locate --layout: %s', options.layout)
736        raise SystemExit
737
738
739def SetLogger(namespace, options):
740    """Create a logger with some good formatting options.
741
742    Args:
743        namespace: string, name associated with this logger.
744    Returns:
745        Logger object.
746    This method assumes logfile and debug are already set.
747    This logger will write to stdout as well as a log file.
748    """
749
750    loglevel = {'debug': logging.DEBUG,
751                'info': logging.INFO,
752                'warning': logging.WARNING,
753                'error': logging.ERROR,
754                'critical': logging.CRITICAL,
755               }
756
757    logger = logging.getLogger(namespace)
758    c = logging.StreamHandler()
759    h = logging.FileHandler(
760        os.path.join(os.path.abspath('.'), options.logfile))
761    hf = logging.Formatter(
762        '%(asctime)s %(process)d %(levelname)s: %(message)s')
763    cf = logging.Formatter('%(levelname)s: %(message)s')
764    logger.addHandler(h)
765    logger.addHandler(c)
766    h.setFormatter(hf)
767    c.setFormatter(cf)
768
769    logger.setLevel(loglevel.get(options.debug, logging.INFO))
770
771    return logger
772
773
774def main():
775    current_dir = os.path.dirname(sys.argv[0])
776    options, args = ParseOptions(current_dir)
777    logger = SetLogger('docCreator', options)
778    CheckOptions(options, logger)
779    doc = DocCreator(options, args, logger)
780    doc.GetTests()
781    doc.ParseControlFiles()
782    doc.CreateMainPage(current_dir)
783    doc.CreateDocs()
784    doc.PostProcessDocs(current_dir)
785
786
787if __name__ == '__main__':
788    main()
789