我们再来看 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:\n%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:\n%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方法
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参数是:
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()里调用的各个方法的具体实现.
如果喜欢作者的文章,请关注"写代码的猿"订阅号以便第一时间获得最新内容。本文版权归作者所有,欢迎转载.