Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(167)

Side by Side Diff: utils/docgen/CreateDocs.py

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

Powered by Google App Engine
This is Rietveld 408576698