Robot Framework 源码解析(2)- 执行测试的入口点

接着上一章,我们先来看 src/robot/run.py 的 run_cli方法。

def run_cli(arguments=None, exit=True):

   if arguments is None:

        arguments = sys.argv[1:]

    return RobotFramework().execute_cli(arguments, exit=exit)

方法很简单,只是调用了RobotFramework类中的execute_cli方法。RobotFramework是run.py的一个内部类,也是Application的子类。通过from robot.utils import Application, unic, text可查看Application是做什么的。

src/robot/utils/application.py

摘录部分代码:

class Application(object):

......

    def main(self, arguments, **options):

        raise NotImplementedError

  ....

    def execute_cli(self, cli_arguments, exit=True):

        with self._logger:

            self._logger.info('%s %s' % (self._ap.name, self._ap.version))

            options, arguments = self._parse_arguments(cli_arguments)

            rc = self._execute(arguments, options)

        if exit:

            self._exit(rc)

        return rc

......

    def _parse_arguments(self, cli_args):

        try:

            options, arguments = self.parse_arguments(cli_args)

        except Information as msg:

            self._report_info(msg.message)

        except DataError as err:

            self._report_error(err.message, help=True, exit=True)

        else:

            self._logger.info('Arguments: %s' % ','.join(arguments))

            return options, arguments

    def parse_arguments(self, cli_args):

        """Public interface for parsing command line arguments.

        :param    cli_args: Command line arguments as a list

        :returns: options (dict), arguments (list)

        :raises:  :class:`~robot.errors.Information` when --help or --version used

        :raises:  :class:`~robot.errors.DataError` when parsing fails

        """

        return self._ap.parse_args(cli_args)

    def execute(self, *arguments, **options):

        with self._logger:

            self._logger.info('%s %s' % (self._ap.name, self._ap.version))

            return self._execute(list(arguments), options)

    def _execute(self, arguments, options):

        try:

            rc = self.main(arguments, **options)

       .....

Application的execute_cli方法其实也只是做了参数的解析工作,具体的任务交给了本实例的main方法。仍然回到 src/robot/run.py 看RobotFramework的main方法:

def main(self, datasources, **options):

        settings = RobotSettings(options)

        LOGGER.register_console_logger(**settings.console_output_config)

        LOGGER.info('Settings:\n%s' % unic(settings))

        builder = TestSuiteBuilder(settings['SuiteNames'],

                                  extension=settings.extension,

                                  rpa=settings.rpa)

        suite = builder.build(*datasources)

        settings.rpa = builder.rpa

        suite.configure(**settings.suite_config)

        if settings.pre_run_modifiers:

            suite.visit(ModelModifier(settings.pre_run_modifiers,

                                      settings.run_empty_suite, LOGGER))

        with pyloggingconf.robot_handler_enabled(settings.log_level):

            old_max_error_lines = text.MAX_ERROR_LINES

            text.MAX_ERROR_LINES = settings.max_error_lines

            try:

                result = suite.run(settings)

            finally:

                text.MAX_ERROR_LINES = old_max_error_lines

            LOGGER.info("Tests execution ended. Statistics:\n%s"

                        % result.suite.stat_message)

            if settings.log or settings.report or settings.xunit:

                writer = ResultWriter(settings.output if settings.log

                                      else result)

                writer.write_results(settings.get_rebot_settings())

        return result.return_code

在这个方法里,进行了设置项的赋值,并且真正开始了执行测试。看TestSuiteBuilder是做什么的。

src/robot/running/builder.py

class TestSuiteBuilder(object):

    def __init__(self, include_suites=None, warn_on_skipped='DEPRECATED',

                extension=None, rpa=None):

        self.include_suites = include_suites

        self.extensions = self._get_extensions(extension)

        builder = StepBuilder()

        self._build_steps = builder.build_steps

        self._build_step = builder.build_step

        self.rpa = rpa

        self._rpa_not_given = rpa is None

......

    def build(self, *paths):

        """

        :param paths: Paths to test data files or directories.

        :return: :class:`~robot.running.model.TestSuite` instance.

        """

        if not paths:

            raise DataError('One or more source paths required.')

        if len(paths) == 1:

            return self._parse_and_build(paths[0])

        root = TestSuite()

        for path in paths:

            root.suites.append(self._parse_and_build(path))

        root.rpa = self.rpa

        return root

  ......

这个TestSuiteBuilder的目的是通过解析datasource来构建一个TestSuite。那TestSuite又是什么的呢?

从from .model import Keyword, TestCase, TestSuite可知,TestSuite是在

src/robot/running/model.py

class TestSuite(model.TestSuite):

    """Represents a single executable test suite.

    See the base class for documentation of attributes not documented here.

    """

    __slots__ = ['resource']

    test_class = TestCase    #: Internal usage only.

    keyword_class = Keyword  #: Internal usage only.

    def __init__(self,  name='', doc='', metadata=None, source=None, rpa=False):

        model.TestSuite.__init__(self, name, doc, metadata, source, rpa)

        #: :class:`ResourceFile` instance containing imports, variables and

        #: keywords the suite owns. When data is parsed from the file system,

        #: this data comes from the same test case file that creates the suite.

        self.resource = ResourceFile(source=source)

    def configure(self, randomize_suites=False, randomize_tests=False,

                  randomize_seed=None, **options)

        model.TestSuite.configure(self, **options)

        self.randomize(randomize_suites, randomize_tests, randomize_seed)

    def randomize(self, suites=True, tests=True, seed=None):

        """Randomizes the order of suites and/or tests, recursively.

        :param suites: Boolean controlling should suites be randomized.

        :param tests: Boolean controlling should tests be randomized.

        :param seed: Random seed. Can be given if previous random order needs

            to be re-created. Seed value is always shown in logs and reports.

        """

        self.visit(Randomizer(suites, tests, seed))

    def run(self, settings=None, **options):

       .......

        from .namespace import IMPORTER

        from .signalhandler import STOP_SIGNAL_MONITOR

        from .runner import Runner

        with LOGGER:

            if not settings:

                settings = RobotSettings(options)

                LOGGER.register_console_logger(**settings.console_output_config)

            with pyloggingconf.robot_handler_enabled(settings.log_level):

                with STOP_SIGNAL_MONITOR:

                    IMPORTER.reset()

                    output = Output(settings)

                    runner = Runner(output, settings)

                    self.visit(runner)

                output.close(runner.result)

        return runner.result

这里TestSuite是model.TestSuite的子类

/src/robot/model/testsuite.py

def visit(self, visitor):

        """:mod:`Visitor interface <robot.model.visitor>` entry-point."""

        visitor.visit_suite(self)

这里只是调用了Runner的visit_suite方法,来看一下

src/robot/running/runner.py

class Runner(SuiteVisitor):

Runner只是SuiteVisitor的一个子类,看看SuiteVisitor

/src/robot/model/visitor.py

class SuiteVisitor(object):

    """Abstract class to ease traversing through the test suite structure.

    See the :mod:`module level <robot.model.visitor>` documentation for more

    information and an example.

    """

    def visit_suite(self, suite):

        """Implements traversing through the suite and its direct children.

        Can be overridden to allow modifying the passed in ``suite`` without

        calling :func:`start_suite` or :func:`end_suite` nor visiting child

        suites, tests or keywords (setup and teardown) at all.

        """

        if self.start_suite(suite) is not False:

            suite.keywords.visit(self)

            suite.suites.visit(self)

            suite.tests.visit(self)

            self.end_suite(suite)

  .......

start_suite / end_suite 就是在Runner具体实现的。

具体看start_suite是做什么的:

def start_suite(self, suite):

        self._output.library_listeners.new_suite_scope()

        result = TestSuite(source=suite.source,

                          name=suite.name,

                          doc=suite.doc,

                          metadata=suite.metadata,

                          starttime=get_timestamp(),

                          rpa=self._settings.rpa)

        if not self.result:

            result.set_criticality(self._settings.critical_tags,

                                  self._settings.non_critical_tags)

            self.result = Result(root_suite=result, rpa=self._settings.rpa)

            self.result.configure(status_rc=self._settings.status_rc,

                                  stat_config=self._settings.statistics_config)

        else:

            self._suite.suites.append(result)

        self._suite = result

        self._suite_status = SuiteStatus(self._suite_status,

                                        self._settings.exit_on_failure,

                                        self._settings.exit_on_error,

                                        self._settings.skip_teardown_on_exit)

        ns = Namespace(self._variables, result, suite.resource)

        ns.start_suite()

        ns.variables.set_from_variable_table(suite.resource.variables)

        EXECUTION_CONTEXTS.start_suite(result, ns, self._output,

                                      self._settings.dry_run)

        self._context.set_suite_variables(result)

        if not self._suite_status.failures:

            ns.handle_imports()

            ns.variables.resolve_delayed()

        result.doc = self._resolve_setting(result.doc)

        result.metadata = [(self._resolve_setting(n), self._resolve_setting(v))

                          for n, v in result.metadata.items()]

        self._context.set_suite_variables(result)

        self._output.start_suite(ModelCombiner(suite, result,

                                              tests=suite.tests,

                                              suites=suite.suites,

                                              test_count=suite.test_count))

        self._output.register_error_listener(self._suite_status.error_occurred)

        self._run_setup(suite.keywords.setup, self._suite_status)

        self._executed_tests = NormalizedDict(ignore='_')

def end_suite(self, suite):

        self._suite.message = self._suite_status.message

        self._context.report_suite_status(self._suite.status,

                                          self._suite.full_message)

        with self._context.suite_teardown():

            failure = self._run_teardown(suite.keywords.teardown, self._suite_status)

            if failure:

                self._suite.suite_teardown_failed(unic(failure))

                if self._suite.statistics.critical.failed:

                    self._suite_status.critical_failure_occurred()

        self._suite.endtime = get_timestamp()

        self._suite.message = self._suite_status.message

        self._context.end_suite(ModelCombiner(suite, self._suite))

        self._suite = self._suite.parent

        self._suite_status = self._suite_status.parent

        self._output.library_listeners.discard_suite_scope()

执行到这里,会根据设置和datasource 已经开始了收集测试结果。

回到最初的 src/robot/run.py

根据

if __name__ == '__main__':

    run_cli(sys.argv[1:])

可以看出,run.py不仅可以通过java -jar robotframework.jar run mytests.robot,被最终调用,还可以直接使用命令,例如:

        robot path/to/tests.robot

        robot --include tag1 --include tag2 --splitlog tests.robot

        robot --name Example --log NONE t1.robot t2.robot > stdout.txt

来执行robotframework的测试用例。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,873评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,483评论 1 306
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,525评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,595评论 0 218
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,018评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,958评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,118评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,873评论 0 208
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,643评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,813评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,293评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,615评论 3 262
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,306评论 3 242
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,170评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,968评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,107评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,894评论 2 278

推荐阅读更多精彩内容

  • 因为unittest支持的html报告在作为邮件附加时耗时较长,故将报告扩展支持为unishark框架。 基于un...
    五娃儿阅读 492评论 0 0
  • Robotframwork 自带的重试参数-R Note:out.html路径,同时要保证有log.html文件和...
    五娃儿阅读 1,682评论 1 1
  • 准备开赛啦!准备开赛啦!还有不到一个月的时间,2016-2017赛季NBA常规赛即将开打啦。快点大声告诉我,你是哪...
    产品运营运营阅读 371评论 0 1
  • 简单版 给自己设立3条做事的准则 一、个人 1、注重做事情的细节 2、做好提前准备工作 3、不断的学习,提升自我 ...
    鸽子蛋m阅读 123评论 0 0
  • 孩子刚刚满月,便不给我衣服,说要回去,手疼,回去休息休息,最后我说孩子大点要去天津买房,这件事情罢休。还有一次老头...
    紫荆花花阅读 706评论 0 0