zoukankan      html  css  js  c++  java
  • Robot Framework源码解析(2)

    我们再来看 src/robot/run.py 的工作原理。摘录部分代码:

     1 from robot.conf import RobotSettings
     2 from robot.model import ModelModifier
     3 from robot.output import LOGGER, pyloggingconf
     4 from robot.reporting import ResultWriter
     5 from robot.running import TestSuiteBuilder
     6 from robot.utils import Application, unic, text
     7 
     8 class RobotFramework(Application):
     9 
    10     def __init__(self):
    11         Application.__init__(self, USAGE, arg_limits=(1,),
    12                              env_options='ROBOT_OPTIONS', logger=LOGGER)
    13 
    14     def main(self, datasources, **options):
    15         ......
    16 
    17 def run_cli(arguments=None, exit=True):
    18     if arguments is None:
    19         arguments = sys.argv[1:]
    20     return RobotFramework().execute_cli(arguments, exit=exit)
    21 
    22 
    23 def run(*tests, **options):
    24    return RobotFramework().execute(*tests, **options)
    25 
    26 
    27 if __name__ == '__main__':
    28     run_cli(sys.argv[1:])

    在上一章我们提到Java的命令行入口其实最终还是转到了其它入口点,例如robot.run的run_cli(mytests.robot)  

    这里就先看第51行的run_cli方法 ,方法很简单,只是调用了RobotFramework类中的execute_cli方法。RobotFramework是run.py的一个内部类,也是Application的子类。通过第6行的 from robot.utils import Application可查看Application是做什么的。

    src/robot/utils/application.py

    摘录部分代码:

     1 class Application(object):
     2 
     3     def __init__(self, usage, name=None, version=None, arg_limits=None,
     4                  env_options=None, logger=None, **auto_options):
     5         self._ap = ArgumentParser(usage, name, version, arg_limits,
     6                                   self.validate, env_options, **auto_options)
     7         self._logger = logger or DefaultLogger()
     8 
     9     def main(self, arguments, **options):
    10         raise NotImplementedError
    11 
    12 ......
    13 
    14     def execute_cli(self, cli_arguments, exit=True):
    15         with self._logger:
    16             self._logger.info('%s %s' % (self._ap.name, self._ap.version))
    17             options, arguments = self._parse_arguments(cli_arguments)
    18             rc = self._execute(arguments, options)
    19         if exit:
    20             self._exit(rc)
    21         return rc
    22     def _parse_arguments(self, cli_args):
    23         try:
    24             options, arguments = self.parse_arguments(cli_args)
    25         except Information as msg:
    26             self._report_info(msg.message)
    27         except DataError as err:
    28             self._report_error(err.message, help=True, exit=True)
    29         else:
    30             self._logger.info('Arguments: %s' % ','.join(arguments))
    31             return options, arguments
    32 
    33     def parse_arguments(self, cli_args):
    34         """Public interface for parsing command line arguments.
    35 
    36         :param    cli_args: Command line arguments as a list
    37         :returns: options (dict), arguments (list)
    38         :raises:  :class:`~robot.errors.Information` when --help or --version used
    39         :raises:  :class:`~robot.errors.DataError` when parsing fails
    40         """
    41         return self._ap.parse_args(cli_args)
    42 
    43     def execute(self, *arguments, **options):
    44         with self._logger:
    45             self._logger.info('%s %s' % (self._ap.name, self._ap.version))
    46             return self._execute(list(arguments), options)
    47 
    48     def _execute(self, arguments, options):
    49         try:
    50             rc = self.main(arguments, **options)
    51         except DataError as err:
    52             return self._report_error(err.message, help=True)
    53         except (KeyboardInterrupt, SystemExit):
    54             return self._report_error('Execution stopped by user.',
    55                                       rc=STOPPED_BY_USER)
    56         except:
    57             error, details = get_error_details(exclude_robot_traces=False)
    58             return self._report_error('Unexpected error: %s' % error,
    59                                       details, rc=FRAMEWORK_ERROR)
    60         else:
    61             return rc or 0

    Application的execute_cli方法,其实也只是做了参数的解析工作(请看第17行 和 第18行的方法调用),具体的任务如何执行交给了本实例的main方法(第50行)。那么仍然回到 src/robot/run.py 看RobotFramework的main方法:

     1     def main(self, datasources, **options):
     2         settings = RobotSettings(options)
     3         LOGGER.register_console_logger(**settings.console_output_config)
     4         LOGGER.info('Settings:
    %s' % unic(settings))
     5         builder = TestSuiteBuilder(settings['SuiteNames'],
     6                                    extension=settings.extension,
     7                                    rpa=settings.rpa)
     8         suite = builder.build(*datasources)
     9         settings.rpa = builder.rpa
    10         suite.configure(**settings.suite_config)
    11         if settings.pre_run_modifiers:
    12             suite.visit(ModelModifier(settings.pre_run_modifiers,
    13                                       settings.run_empty_suite, LOGGER))
    14         with pyloggingconf.robot_handler_enabled(settings.log_level):
    15             old_max_error_lines = text.MAX_ERROR_LINES
    16             text.MAX_ERROR_LINES = settings.max_error_lines
    17             try:
    18                 result = suite.run(settings)
    19             finally:
    20                 text.MAX_ERROR_LINES = old_max_error_lines
    21             LOGGER.info("Tests execution ended. Statistics:
    %s"
    22                         % result.suite.stat_message)
    23             if settings.log or settings.report or settings.xunit:
    24                 writer = ResultWriter(settings.output if settings.log
    25                                       else result)
    26                 writer.write_results(settings.get_rebot_settings())
    27         return result.return_code

    在这个方法里,进行了设置项的赋值(第2行),真正执行测试并输出测试结果(第18行)。通过第5,8,18行可以看到测试的执行过程首先是通过TestSuiteBuilder构建了一个suite,然后执行该suite的run方法。那么我们来看看TestSuiteBuilder是如何构建一个suite的。

    通过from robot.running import TestSuiteBuilder可以知道TestSuiteBuilder是在robot.running路径下,我们先看看这个包的__init__.py

    1 from .builder import TestSuiteBuilder, ResourceFileBuilder
    2 from .context import EXECUTION_CONTEXTS
    3 from .model import Keyword, TestCase, TestSuite
    4 from .testlibraries import TestLibrary
    5 from .usererrorhandler import UserErrorHandler
    6 from .userkeyword import UserLibrary
    7 from .runkwregister import RUN_KW_REGISTER

    由第1行可以看出TestSuiteBuilder在 src/robot/running/builder.py:摘录的部分代码:

     1 class TestSuiteBuilder(object):
     2 
     3     def __init__(self, include_suites=None, warn_on_skipped='DEPRECATED',
     4                  extension=None, rpa=None):
     5         self.include_suites = include_suites
     6         self.extensions = self._get_extensions(extension)
     7         builder = StepBuilder()
     8         self._build_steps = builder.build_steps
     9         self._build_step = builder.build_step
    10         self.rpa = rpa
    11         self._rpa_not_given = rpa is None
    12         # TODO: Remove in RF 3.2.
    13         if warn_on_skipped != 'DEPRECATED':
    14             warnings.warn("Option 'warn_on_skipped' is deprecated and has no "
    15                           "effect.", DeprecationWarning)
    16 ......
    17 
    18     def build(self, *paths):
    19         """
    20         :param paths: Paths to test data files or directories.
    21         :return: :class:`~robot.running.model.TestSuite` instance.
    22         """
    23         if not paths:
    24             raise DataError('One or more source paths required.')
    25         if len(paths) == 1:
    26             return self._parse_and_build(paths[0])
    27         root = TestSuite()
    28         for path in paths:
    29             root.suites.append(self._parse_and_build(path))
    30         root.rpa = self.rpa
    31         return root
    32 ......

    build方法的最后返回了一个TestSuite对象。走到这里好像有点太快了,为了更好的理解这个TestSuite,我们回过头来,顺藤摸瓜看看这个build的参数paths是什么:  def build(self, *paths)(builder.py) <-- builder.build(*datasources) (run.py)<-- def main(self, datasources, **options): <-- self.main(arguments, **options)(Application.py) <-- def _execute(self, arguments, options): <-- self._execute(arguments, options) <-- def execute_cli(self, cli_arguments, exit=True):(Application.py) <--RobotFramework().execute_cli(arguments, exit=exit)(run.py) <-- def run_cli(arguments=None, exit=True):(run.py)

    原来这个paths是命令后选项参数或者是方法调用时传递过来的参数。例如

    1 from robot import run_cli
    2 
    3 # Run tests and return the return code.
    4 rc = run_cli(['--name', 'Example', 'tests.robot'], exit=False)
    5 
    6 # Run tests and exit to the system automatically.
    7 run_cli(['--name', 'Example', 'tests.robot'])

    或者 像第一篇文章中 java -jar robotframework.jar run mytests.robot这个命令,经过JarRunner解析会最终调用robot.run的run_cli("mytests.robot")这个方法

    所以这个TestSuiteBuilder的目的是通过解析datasource来构建一个TestSuite ,接着回到builder.py的 build方法最后的TestSuite对象上,来看看TestSuite什么。

    通过robot.running的_init_.py :from .model import Keyword, TestCase, TestSuite,可以看出TestSuite在 src/robot/running/model.py,摘录有关TestSuite的代码:

     1 class TestSuite(model.TestSuite):
     2     """Represents a single executable test suite.
     3 
     4     See the base class for documentation of attributes not documented here.
     5     """
     6     __slots__ = ['resource']
     7     test_class = TestCase    #: Internal usage only.
     8     keyword_class = Keyword  #: Internal usage only.
     9 
    10     def __init__(self,  name='', doc='', metadata=None, source=None, rpa=False):
    11         model.TestSuite.__init__(self, name, doc, metadata, source, rpa)
    12         #: :class:`ResourceFile` instance containing imports, variables and
    13         #: keywords the suite owns. When data is parsed from the file system,
    14         #: this data comes from the same test case file that creates the suite.
    15         self.resource = ResourceFile(source=source)
    16 。。。。。。
    17     def run(self, settings=None, **options):
    18         from .namespace import IMPORTER
    19         from .signalhandler import STOP_SIGNAL_MONITOR
    20         from .runner import Runner
    21 
    22         with LOGGER:
    23             if not settings:
    24                 settings = RobotSettings(options)
    25                 LOGGER.register_console_logger(**settings.console_output_config)
    26             with pyloggingconf.robot_handler_enabled(settings.log_level):
    27                 with STOP_SIGNAL_MONITOR:
    28                     IMPORTER.reset()
    29                     output = Output(settings)
    30                     runner = Runner(output, settings)
    31                     self.visit(runner)
    32                 output.close(runner.result)
    33         return runner.result

    通过第10行的__init__方法可以看到,TestSuite初始化的时候包括name,doc,metadata,import,Variabel等数据。通过同一个图片我想大家应该就可以更 好的理解这里封装的信息了:

    是的,就是这个可视化工具RIDE里的信息.当然这个类里面封装的信息并不全,因为它是model.TestSuite的子类,在父类中封装了更多的信息。

    仍然回到  src/robot/run.py 的main方法,suite构建后会调用suite.run方法收集result。看 TestSuite类的第31行 self.visit(runner),这个visit方法都做了写什么?参数runner有时什么呢? 我们先在父类中看visit方法

    /src/robot/model/testsuite.py

    1     def visit(self, visitor):
    2         """:mod:`Visitor interface <robot.model.visitor>` entry-point."""
    3         visitor.visit_suite(self)

    方法很简单,只是去调用这个runner参数的visit_suite()方法。我们通过TestSuite类run方法中的from .runner import Runner可以知道 这个runner参数是:

    src/robot/running/runner.py

     1 class Runner(SuiteVisitor):
     2 
     3 。。。。。。

    Runner是SuiteVisitor的子类,里面并没有visit_suite 方法。去看看SuiteVisitor。 通过model包__init__.py的 from .visitor import SuiteVisitor 可知,SuiteVisitor在  /src/robot/model/visitor.py,摘录部分代码:

     1 class SuiteVisitor(object):
     2     """Abstract class to ease traversing through the test suite structure.
     3 
     4     See the :mod:`module level <robot.model.visitor>` documentation for more
     5     information and an example.
     6     """
     7 
     8     def visit_suite(self, suite):
     9         """Implements traversing through the suite and its direct children.
    10 
    11         Can be overridden to allow modifying the passed in ``suite`` without
    12         calling :func:`start_suite` or :func:`end_suite` nor visiting child
    13         suites, tests or keywords (setup and teardown) at all.
    14         """
    15         if self.start_suite(suite) is not False:
    16             suite.keywords.visit(self)
    17             suite.suites.visit(self)
    18             suite.tests.visit(self)
    19             self.end_suite(suite)
    20 
    21     def start_suite(self, suite):
    22         """Called when suite starts. Default implementation does nothing.
    23 
    24         Can return explicit ``False`` to stop visiting.
    25         """
    26         pass
    27 
    28     def end_suite(self, suite):
    29         """Called when suite ends. Default implementation does nothing."""
    30         pass

    在visit_suite方法中,开始了测试的执行,start_suite,end_suite 都在Runner具体实现. 今天先写到这里,下一章再接着分析visit_suite()里调用的各个方法的具体实现.

    如果喜欢作者的文章,请关注"写代码的猿"订阅号以便第一时间获得最新内容。本文版权归作者所有,欢迎转载. 

  • 相关阅读:
    读书笔记
    JavaScript
    Vue
    读书笔记
    Python
    Python
    概率论07 联合分布
    概率论06 连续分布
    概率论05 离散分布
    概率论04 随机变量
  • 原文地址:https://www.cnblogs.com/yuan1225/p/10655977.html
Copyright © 2011-2022 走看看