Robot Framework 源代码阅读笔记 之 一

来源:互联网 发布:无锡关键词排名优化 编辑:程序博客网 时间:2024/05/21 09:56

从源代码里应该可以帮我解答一些问题:

1. 这些关键字怎么定义的,然后怎么对应到具体的库来执行的

2. 框架的逻辑组织架构是什么样的,能学到哪些东西

3. 有没有一些可以改进的地方

4. 用到了哪些语言特性和技巧

5. 其他一些思考


安装完之后的入口robot文件如下:

https://github.com/robotframework/robotframework/tree/master/src/bin


从Robot开始,Linux应该运行的文件是下面这个

robotframework/src/bin/robot

内容如下:

#!/usr/bin/env python


import sys
from robot import run_cli


# Multiprocessing guard
# https://github.com/robotframework/robotframework/issues/2315
if __name__ == '__main__':
    run_cli(sys.argv[1:])


可以看到模块robot的run_cli才是真正的入口

而从robot模块可以看到:

https://github.com/robotframework/robotframework/blob/master/src/robot/__init__.py

from robot.rebot import rebot, rebot_cli
from robot.run import run, run_cli
from robot.version import get_version

__all__ = ['run', 'run_cli', 'rebot', 'rebot_cli']
__version__ = get_version()


其实run_cli是从robot.run模块导出的

找到模块文件:https://github.com/robotframework/robotframework/blob/master/src/robot/run.py


def run_cli(arguments, exit=True):
    """Command line execution entry point for running tests.
    :param arguments: Command line options and arguments as a list of strings.
    :param exit: If ``True``, call ``sys.exit`` with the return code denoting
        execution status, otherwise just return the rc. New in RF 3.0.1.
    Entry point used when running tests from the command line, but can also
    be used by custom scripts that execute tests. Especially useful if the
    script itself needs to accept same arguments as accepted by Robot Framework,
    because the script can just pass them forward directly along with the
    possible default values it sets itself.
    Example::
        from robot import run_cli
        # Run tests and return the return code.
        rc = run_cli(['--name', 'Example', 'tests.robot'], exit=False)
        # Run tests and exit to the system automatically.
        run_cli(['--name', 'Example', 'tests.robot'])
    See also the :func:`run` function that allows setting options as keyword
    arguments like ``name="Example"`` and generally has a richer API for
    programmatic test execution.
    """
    return RobotFramework().execute_cli(arguments, exit=exit)


run_cli是命令行执行的入口,对应到RobotFramework().execute_cli才是真正模块代码的入口

要知道RobotFramework().execute_cli是从哪里进入的,还要看看import了哪些模块

from robot.conf import RobotSettings
from robot.model import ModelModifier
from robot.output import LOGGER, pyloggingconf
from robot.reporting import ResultWriter
from robot.running import TestSuiteBuilder
from robot.utils import Application, unic

class RobotFramework(Application):


    def __init__(self):
        Application.__init__(self, USAGE, arg_limits=(1,),
                             env_options='ROBOT_OPTIONS', logger=LOGGER)


RobotFramework继承了Application,下面看看application到底有什么

https://github.com/robotframework/robotframework/blob/master/src/robot/utils/application.py


class Application(object):


    def __init__(self, usage, name=None, version=None, arg_limits=None,
                 env_options=None, logger=None, **auto_options):
        self._ap = ArgumentParser(usage, name, version, arg_limits,
                                  self.validate, env_options, **auto_options)
        self._logger = logger or DefaultLogger()


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


    def validate(self, options, arguments):
        return options, arguments


    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 _execute(self, arguments, options):
        try:
            rc = self.main(arguments, **options)
        except DataError as err:
            return self._report_error(err.message, help=True)
        except (KeyboardInterrupt, SystemExit):
            return self._report_error('Execution stopped by user.',
                                      rc=STOPPED_BY_USER)
        except:
            error, details = get_error_details(exclude_robot_traces=False)
            return self._report_error('Unexpected error: %s' % error,
                                      details, rc=FRAMEWORK_ERROR)
        else:
            return rc or 0


这个application基类的作用也就是parse了一下参数,真正的执行还是要回到RobotFramework里面定义的main

https://github.com/robotframework/robotframework/blob/master/src/robot/run.py


class RobotFramework(Application):


    def __init__(self):
        Application.__init__(self, USAGE, arg_limits=(1,),
                             env_options='ROBOT_OPTIONS', logger=LOGGER)


    def main(self, datasources, **options):
        settings = RobotSettings(options)
        LOGGER.register_console_logger(**settings.console_output_config)
        LOGGER.info('Settings:\n%s' % unic(settings))
        suite = TestSuiteBuilder(settings['SuiteNames'],
                                 settings['WarnOnSkipped'],
                                 settings['Extension']).build(*datasources)
        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):
            result = suite.run(settings)
            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,然后怎么run测试的,找找代码在哪里

https://github.com/robotframework/robotframework/blob/master/src/robot/running/__init__.py


from .builder import TestSuiteBuilder, ResourceFileBuilder
from .context import EXECUTION_CONTEXTS
from .model import Keyword, TestCase, TestSuite
from .testlibraries import TestLibrary
from .usererrorhandler import UserErrorHandler
from .userkeyword import UserLibrary
from .runkwregister import RUN_KW_REGISTER


https://github.com/robotframework/robotframework/blob/master/src/robot/running/builder.py

class TestSuiteBuilder(object):
    """Creates executable :class:`~robot.running.model.TestSuite` objects.
    Suites are build based on existing test data on the file system.
    See the overall documentation of the :mod:`robot.running` package for
    more information and examples.
    """


    def __init__(self, include_suites=None, warn_on_skipped=False, extension=None):
        """
        :param include_suites: List of suite names to include. If ``None`` or
            an empty list, all suites are included. When executing tests
            normally, these names are specified using the ``--suite`` option.
        :param warn_on_skipped: Boolean to control should a warning be emitted
            if a file is skipped because it cannot be parsed or should it be
            ignored silently. When executing tests normally, this value is set
            with the ``--warnonskippedfiles`` option.
        :param extension: Limit parsing test data to only these files. Files
            are specified as an extension that is handled case-insensitively.
            Same as ``--extension`` on the command line.
        """
        self.include_suites = include_suites
        self.warn_on_skipped = warn_on_skipped
        self.extensions = self._get_extensions(extension)
        builder = StepBuilder()
        self._build_steps = builder.build_steps
        self._build_step = builder.build_step


    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))
        return root


看起来主要目的是创建TestSuite,再看看是什么东西

https://github.com/robotframework/robotframework/blob/master/src/robot/running/model.py

就列出来看起来比较关键的部分:

from robot importmodel

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):
        model.TestSuite.__init__(self, name, doc, metadata, source)
        #: :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):
        """A shortcut to configure a suite using one method call.
        :param randomize_xxx: Passed to :meth:`randomize`.
        :param options: Passed to
            :class:`~robot.model.configurer.SuiteConfigurer` that will then
            set suite attributes, call :meth:`filter`, etc. as needed.
        Example::
            suite.configure(included_tags=['smoke'],
                            doc='Smoke test results.')
        """
        model.TestSuite.configure(self, **options)
        self.randomize(randomize_suites, randomize_tests, randomize_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

再看看这个testsuitemodel是怎么回事

https://github.com/robotframework/robotframework/blob/master/src/robot/model/testsuite.py

class TestSuite(ModelObject):
    """Base model for single suite.
    Extended by :class:`robot.running.model.TestSuite` and
    :class:`robot.result.model.TestSuite`.
    """
    __slots__ = ['parent', 'source', '_name', 'doc', '_my_visitors']
    test_class = TestCase    #: Internal usage only.
    keyword_class = Keyword  #: Internal usage only.


    def __init__(self, name='', doc='', metadata=None, source=None):
        self.parent = None  #: Parent suite. ``None`` with the root suite.
        self._name = name
        self.doc = doc  #: Test suite documentation.
        self.metadata = metadata
        self.source = source  #: Path to the source file or directory.
        self.suites = None
        self.tests = None
        self.keywords = None
        self._my_visitors = []


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

看起来,还是要看看这个Runner怎么visit_suite

https://github.com/robotframework/robotframework/blob/master/src/robot/running/runner.py

from robot.model import SuiteVisitor


class Runner(SuiteVisitor):


    def __init__(self, output, settings):
        self.result = None
        self._output = output
        self._settings = settings
        self._variables = VariableScopes(settings)
        self._suite = None
        self._suite_status = None
        self._executed_tests = None


    @property
    def _context(self):
        return EXECUTION_CONTEXTS.current


    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())
        if not self.result:
            result.set_criticality(self._settings.critical_tags,
                                   self._settings.non_critical_tags)
            self.result = Result(root_suite=result)
            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 _resolve_setting(self, value):
        if is_list_like(value):
            return self._variables.replace_list(value, ignore_errors=True)
        return self._variables.replace_string(value, ignore_errors=True)


    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()
        if not suite.parent:
            IMPORTER.close_global_library_listeners()


    def visit_test(self, test):
        if test.name in self._executed_tests:
            self._output.warn("Multiple test cases with name '%s' executed in "
                              "test suite '%s'." % (test.name, self._suite.longname))
        self._executed_tests[test.name] = True
        result = self._suite.tests.create(name=test.name,
                                          doc=self._resolve_setting(test.doc),
                                          tags=self._resolve_setting(test.tags),
                                          starttime=get_timestamp(),
                                          timeout=self._get_timeout(test))
        self._context.start_test(result)
        self._output.start_test(ModelCombiner(test, result))
        status = TestStatus(self._suite_status, result.critical)
        if not status.failures and not test.name:
            status.test_failed('Test case name cannot be empty.')
        if not status.failures and not test.keywords.normal:
            status.test_failed('Test case contains no keywords.')
        if status.exit:
            self._add_exit_combine()
            result.tags.add('robot-exit')
        self._run_setup(test.keywords.setup, status, result)
        try:
            if not status.failures:
                StepRunner(self._context,
                           test.template).run_steps(test.keywords.normal)
            else:
                status.test_failed(status.message)
        except PassExecution as exception:
            err = exception.earlier_failures
            if err:
                status.test_failed(err)
            else:
                result.message = exception.message
        except ExecutionFailed as err:
            status.test_failed(err)
        result.status = status.status
        result.message = status.message or result.message
        if status.teardown_allowed:
            with self._context.test_teardown(result):
                failure = self._run_teardown(test.keywords.teardown, status,
                                             result)
                if failure and result.critical:
                    status.critical_failure_occurred()
        if not status.failures and result.timeout and result.timeout.timed_out():
            status.test_failed(result.timeout.get_message())
            result.message = status.message
        result.status = status.status
        result.endtime = get_timestamp()
        self._output.end_test(ModelCombiner(test, result))
        self._context.end_test(result)


    def _add_exit_combine(self):
        exit_combine = ('NOT robot-exit', '')
        if exit_combine not in self._settings['TagStatCombine']:
            self._settings['TagStatCombine'].append(exit_combine)


    def _get_timeout(self, test):
        if not test.timeout:
            return None
        return TestTimeout(test.timeout.value, test.timeout.message,
                           self._variables)


    def _run_setup(self, setup, status, result=None):
        if not status.failures:
            exception = self._run_setup_or_teardown(setup)
            status.setup_executed(exception)
            if result and isinstance(exception, PassExecution):
                result.message = exception.message


    def _run_teardown(self, teardown, status, result=None):
        if status.teardown_allowed:
            exception = self._run_setup_or_teardown(teardown)
            status.teardown_executed(exception)
            failed = not isinstance(exception, PassExecution)
            if result and exception:
                result.message = status.message if failed else exception.message
            return exception if failed else None


    def _run_setup_or_teardown(self, data):
        if not data:
            return None
        try:
            name = self._variables.replace_string(data.name)
        except DataError as err:
            if self._settings.dry_run:
                return None
            return err
        if name.upper() in ('', 'NONE'):
            return None
        try:
            StepRunner(self._context).run_step(data, name=name)
        except ExecutionFailed as err:
            return err


这一部分看起来是最核心的执行测试的部分,下次继续

原创粉丝点击