| #!/usr/bin/python |
| # |
| # Copyright (c) 2014 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| |
| """Generates some .rst files used to document parts of the factory toolkit. |
| |
| Currently this generates a list of all available pytests, in the |
| doc/pytests directory. In the future it may be extended to generate, |
| for example, a test list tree. |
| |
| (Ideally these could be generated via Sphinx extensions, but this is not |
| always practical.) |
| """ |
| |
| |
| import argparse |
| import codecs |
| import importlib |
| import inspect |
| import logging |
| import os |
| import re |
| import unittest |
| |
| import factory_common # pylint: disable=W0611 |
| from cros.factory.test import pytests |
| from cros.factory.utils.type_utils import Enum |
| from cros.factory.utils import file_utils |
| |
| |
| def Escape(text): |
| r"""Escapes characters that must be escaped in a raw tag (*, `, and \).""" |
| return re.sub(r'([*`\\])', '\\\\\\1', text) |
| |
| |
| def Indent(text, prefix, first_line_prefix=None): |
| """Indents a string. |
| |
| Args: |
| text: The input string. |
| prefix: The string to insert at the beginning of each line. |
| first_line_prefix: The string to insert at the beginning of the first line. |
| Defaults to prefix if unspecified. |
| """ |
| if first_line_prefix is None: |
| first_line_prefix = prefix |
| |
| return re.sub( |
| '(?m)^', |
| lambda match: first_line_prefix if match.start() == 0 else prefix, |
| text) |
| |
| |
| def WriteTestArgs(args, out): |
| """Writes a table describing test arguments. |
| |
| Args: |
| args: A list of Arg objects, as in the ARGS attribute of a pytest. |
| out: A stream to write to. |
| """ |
| if not args: |
| out.write('This test does not have any arguments.') |
| return |
| |
| out.write( |
| 'Test Arguments\n' |
| '--------------\n' |
| '.. list-table::\n' |
| ' :widths: 20 10 60\n' |
| ' :header-rows: 1\n' |
| '\n') |
| |
| def WriteRow(cols): |
| for i, col in enumerate(cols): |
| # Indent the first line with " - " (or " * - " if it's the first |
| # column) |
| out.write(Indent(col, ' ' * 7, ' * - ' if i == 0 else ' - ')) |
| out.write('\n') |
| |
| WriteRow(('Name', 'Type', 'Description')) |
| for arg in args: |
| description = arg.help.strip() |
| |
| annotations = [] |
| if arg.optional: |
| annotations.append('optional') |
| if arg.default is not None: |
| annotations.append('default: ``%s``' % Escape(repr(arg.default))) |
| if annotations: |
| description = '(%s) %s' % ('; '.join(annotations), description) |
| |
| def FormatArgType(arg_type): |
| if isinstance(arg_type, Enum): |
| return '[' + ', '.join(str(x) for x in sorted(arg_type)) + ']' |
| elif arg_type == type(None): |
| return 'None' |
| else: |
| return arg_type.__name__ |
| arg_types = ', '.join(FormatArgType(x) for x in arg.type) |
| |
| WriteRow((arg.name, arg_types, description)) |
| |
| |
| def GenerateTestDocs(pytest_name, module, out): |
| """Generates test docs for a pytest. |
| |
| Args: |
| pytest_name: The name of the pytest (e.g., 'foo' for foo.py). |
| module: The module of the pytest. This may be the module itself, |
| or the name of the module (e.g., cros.factory.test.pytest.foo). |
| out: A stream to write to. |
| |
| Returns: |
| A dictionary of information about the test. Currently this is just |
| |
| dict(short_docstring=short_docstring) |
| |
| where short_docstring is the first line of the docstring. |
| """ |
| if isinstance(module, str): |
| module_name = module |
| try: |
| module = importlib.import_module(module_name) |
| except ImportError: |
| logging.warn('Unable to import %s', module_name) |
| return |
| |
| # Find the TestCase object. |
| test_cases = [v for v in module.__dict__.itervalues() |
| if inspect.isclass(v) and issubclass(v, unittest.TestCase)] |
| if not test_cases: |
| logging.warn('No test cases found in %s', module.__name__) |
| return |
| if len(test_cases) > 1: |
| logging.warn('Expected only one test case in %s but found %r', |
| module.__name__, test_cases) |
| return |
| |
| test_case = test_cases[0] |
| args = getattr(test_case, 'ARGS', []) |
| |
| doc = getattr(module, '__doc__', None) |
| if doc is None: |
| doc = 'No test-level description available for pytest %s.' % pytest_name |
| |
| if isinstance(doc, str): |
| doc = doc.decode('utf-8') |
| |
| out.write(pytest_name + '\n') |
| out.write('=' * len(pytest_name) + '\n') |
| out.write(doc) |
| out.write('\n\n') |
| |
| WriteTestArgs(args, out) |
| # Remove everything after the first pair of newlines. |
| short_docstring = re.sub(r'(?s)\n\s*\n.+', '', doc).strip() |
| return dict(short_docstring=short_docstring) |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser( |
| description='Generate .rst files for the factory toolkit') |
| parser.add_argument('--output-dir', '-o', |
| help='Output directory (default: %default)', default='.') |
| args = parser.parse_args() |
| |
| pytests_output_dir = os.path.join(args.output_dir, 'pytests') |
| file_utils.TryMakeDirs(pytests_output_dir) |
| |
| pytest_module_dir = os.path.dirname(pytests.__file__) |
| |
| # Map of pytest name to info returned by GenerateTestDocs. |
| pytest_info = {} |
| |
| for root, unused_dirs, files in os.walk(pytest_module_dir): |
| for f in files: |
| if (not f.endswith('.py') or |
| f.startswith('__') or |
| f == 'factory_common.py' or |
| f.endswith('_automator.py') or |
| f.endswith('_e2etest.py') or |
| f.endswith('_unittest.py') or |
| f.endswith('_impl.py')): |
| continue |
| |
| # E.g., "foo.py" or "foo/foo.py" |
| relpath = os.path.relpath(os.path.join(root, f), |
| pytest_module_dir) |
| # E.g., "foo" or "foo/foo" |
| base = os.path.splitext(relpath)[0] |
| |
| if '/' in relpath: |
| # It's in a subpackage. Accept it only if it looks like |
| # foo/foo.py. |
| dirname, filename = os.path.split(base) |
| if dirname != filename: |
| continue |
| |
| module_name = ('cros.factory.test.pytests.' + |
| base.replace('/', '.')) |
| pytest_name = os.path.basename(base) |
| |
| with codecs.open(os.path.join(pytests_output_dir, pytest_name + '.rst'), |
| 'w', 'utf-8') as out: |
| pytest_info[pytest_name] = GenerateTestDocs( |
| pytest_name, module_name, out) |
| |
| index_rst = os.path.join(pytests_output_dir, 'index.rst') |
| with open(index_rst, 'a') as f: |
| for k, v in sorted(pytest_info.items()): |
| if v is not None: |
| f.write(' * - `%s <%s.html>`_\n' % (k, k)) |
| f.write(Indent(v['short_docstring'], ' ' * 7, ' - ')) |
| f.write('\n') |
| |
| if __name__ == '__main__': |
| main() |