diff --git a/src/robotkernel/builders.py b/src/robotkernel/builders.py index 6fe4155..369acae 100644 --- a/src/robotkernel/builders.py +++ b/src/robotkernel/builders.py @@ -1,10 +1,54 @@ # -*- coding: utf-8 -*- -from robotkernel.constants import HAS_RF32_PARSER +import os +from io import StringIO +from typing import Dict +from robot.api import get_model +from robot.errors import DataError +from robot.running.builder.parsers import ErrorReporter +from robot.running.model import TestSuite +from robot.running.builder.testsettings import TestDefaults +from robot.running.builder.transformers import SettingsBuilder, SuiteBuilder -if HAS_RF32_PARSER: - from robotkernel.builders_32 import build_suite -else: - from robotkernel.builders_31 import build_suite -assert build_suite +def _get_rpa_mode(data): + if not data: + return None + tasks = [s.tasks for s in data.sections if hasattr(s, "tasks")] + if all(tasks) or not any(tasks): + return tasks[0] if tasks else None + raise DataError("One file cannot have both tests and tasks.") + + +def strip_duplicate_items(items): + """Remove duplicates from an item list.""" + new_items = {} + for item in items: + new_items[item.name] = item + items._items = list(new_items.values()) + + +def clean_items(items): + """Remove elements from an item list.""" + items._items = [] + +# TODO: Refactor to use public API only +# https://github.com/robotframework/robotframework/commit/fa024345cb58d154e1d8384552b62788d3ed6258 + + +def populate_suite(code: str, suite: TestSuite, defaults: TestDefaults): + """Build new code and populate the given test suite.""" + # Build code and populate the suite with the new keywords ands tests + ast = get_model( + StringIO(code), data_only=False, curdir=os.getcwd().replace("\\", "\\\\") + ) + ErrorReporter(code).visit(ast) + SettingsBuilder(suite, defaults).visit(ast) + SuiteBuilder(suite, defaults).visit(ast) + + # Strip duplicate keywords and variables + strip_duplicate_items(suite.resource.keywords) + strip_duplicate_items(suite.resource.variables) + + # Detect RPA + suite.rpa = _get_rpa_mode(ast) diff --git a/src/robotkernel/builders_31.py b/src/robotkernel/builders_31.py deleted file mode 100644 index 26508fe..0000000 --- a/src/robotkernel/builders_31.py +++ /dev/null @@ -1,99 +0,0 @@ -# -*- coding: utf-8 -*- -from io import BytesIO -from robot.errors import DataError -from robot.output import LOGGER -from robot.parsing import TestCaseFile -from robot.parsing.model import _TestData -from robot.parsing.model import KeywordTable -from robot.parsing.model import TestCaseFileSettingTable -from robot.parsing.populators import FromFilePopulator -from robot.parsing.robotreader import RobotReader -from robot.parsing.settings import Fixture -from robot.parsing.tablepopulators import NullPopulator -from robot.running import TestSuiteBuilder -from robot.utils import get_error_message -from typing import Dict -import os -import platform - - -def build_suite(code: str, cell_history: Dict[str, str]): - # Init - data = TestCaseString() - data.source = os.getcwd() # allow Library and Resource from CWD work - - # Populate history, but ignore tests - for historical in cell_history.values(): - data.populate(historical) - data.testcase_table.tests.clear() - - # Populate current - data.populate(code) - - # Wrap up - builder = TestSuiteBuilder() - suite = builder._build_suite(data) - suite._name = "Robocode Lab" - - return suite - - -class TestCaseString(TestCaseFile): - # noinspection PyMissingConstructor - def __init__(self, parent=None, source=None): - super(TestCaseString, self).__init__(parent, source) - self.setting_table = SafeSettingsTable(self) - self.keyword_table = OverridingKeywordTable(self) - _TestData.__init__(self, parent, source) - - # noinspection PyMethodOverriding - def populate(self, source): - FromStringPopulator(self).populate(source) - return self - - -class SafeSettingsTable(TestCaseFileSettingTable): - def __init__(self, parent): - super(SafeSettingsTable, self).__init__(parent) - self.suite_setup = OverridingFixture("Suite Setup", self) - self.suite_teardown = OverridingFixture("Suite Teardown", self) - self.test_setup = OverridingFixture("Test Setup", self) - self.test_teardown = OverridingFixture("Test Teardown", self) - - -class OverridingFixture(Fixture): - def populate(self, value, comment=None): - # Always reset setting before populating it - self.reset() - super(OverridingFixture, self).populate(value, comment) - - -class OverridingKeywordTable(KeywordTable): - def add(self, name): - # Always clear previous definition - for i in range(len(self.keywords)): - if self.keywords[i].name == name: - del self.keywords[i] - break - return super(OverridingKeywordTable, self).add(name) - - -class FromStringPopulator(FromFilePopulator): - # noinspection PyMissingConstructor - def __init__(self, datafile): - self._datafile = datafile - self._populator = NullPopulator() - # Jupyter running directory for convenience - if platform.system() == "Windows" and os.path.sep == "\\": - # Because Robot Framework uses the backslash (\) as an escape character in - # the test data, using a literal backslash requires duplicating it. - self._curdir = os.getcwd().replace(os.path.sep, os.path.sep * 2) - else: - self._curdir = os.getcwd() - - def populate(self, source): - LOGGER.info("Parsing string '%s'." % source) - try: - RobotReader().read(BytesIO(source.encode("utf-8")), self) - except Exception: - raise DataError(get_error_message()) diff --git a/src/robotkernel/builders_32.py b/src/robotkernel/builders_32.py deleted file mode 100644 index 066b89c..0000000 --- a/src/robotkernel/builders_32.py +++ /dev/null @@ -1,69 +0,0 @@ -# -*- coding: utf-8 -*- -from io import StringIO -from robot.api import get_model -from robot.errors import DataError -from robot.running.builder.parsers import ErrorReporter -from robot.running.builder.testsettings import TestDefaults -from robot.running.builder.transformers import SettingsBuilder -from robot.running.builder.transformers import SuiteBuilder -from robot.running.model import TestSuite -from typing import Dict -import os - - -def _get_rpa_mode(data): - if not data: - return None - tasks = [s.tasks for s in data.sections if hasattr(s, "tasks")] - if all(tasks) or not any(tasks): - return tasks[0] if tasks else None - raise DataError("One file cannot have both tests and tasks.") - - -# TODO: Refactor to use public API only -# https://github.com/robotframework/robotframework/commit/fa024345cb58d154e1d8384552b62788d3ed6258 - - -def build_suite(code: str, cell_history: Dict[str, str], data_only: bool = False): - # Init - suite = TestSuite(name="Robocode Lab", source=os.getcwd()) - defaults = TestDefaults(None) - - # Populate history - for historical in cell_history.values(): - ast = get_model( - StringIO(historical), - data_only=data_only, - curdir=os.getcwd().replace("\\", "\\\\"), - ) - ErrorReporter(historical).visit(ast) - SettingsBuilder(suite, defaults).visit(ast) - SuiteBuilder(suite, defaults).visit(ast) - - # Clear historical tests - suite.tests._items = [] - - # Populate current - ast = get_model( - StringIO(code), data_only=data_only, curdir=os.getcwd().replace("\\", "\\\\") - ) - ErrorReporter(code).visit(ast) - SettingsBuilder(suite, defaults).visit(ast) - SuiteBuilder(suite, defaults).visit(ast) - - # Strip duplicate keywords - keywords = {} - for keyword in suite.resource.keywords: - keywords[keyword.name] = keyword - suite.resource.keywords._items = list(keywords.values()) - - # Strip duplicate variables - variables = {} - for variable in suite.resource.variables: - variables[variable.name] = variable - suite.resource.variables._items = list(variables.values()) - - # Detect RPA - suite.rpa = _get_rpa_mode(ast) - - return suite diff --git a/src/robotkernel/executors.py b/src/robotkernel/executors.py index 261d213..fee0fb1 100644 --- a/src/robotkernel/executors.py +++ b/src/robotkernel/executors.py @@ -1,36 +1,28 @@ # -*- coding: utf-8 -*- from collections import OrderedDict -from io import BytesIO -from io import StringIO -from IPython.core.display import clear_output -from IPython.core.display import display -from PIL import Image -from robot.reporting import ResultWriter -from robot.running.model import TestSuite -from robotkernel.builders import build_suite -from robotkernel.constants import ICON_FILE_TEXT -from robotkernel.display import DisplayKernel -from robotkernel.display import ProgressUpdater -from robotkernel.listeners import ReturnValueListener -from robotkernel.listeners import RobotKeywordsIndexerListener -from robotkernel.listeners import RobotVariablesListener -from robotkernel.listeners import StatusEventListener -from robotkernel.utils import data_uri -from robotkernel.utils import display_log -from robotkernel.utils import to_mime_and_metadata -from tempfile import TemporaryDirectory -from traceback import format_exc -from typing import List -from typing import Tuple -from urllib.parse import unquote +from io import BytesIO, StringIO import base64 import binascii -import ipywidgets import os import re import sys import types import uuid +from copy import deepcopy +from tempfile import TemporaryDirectory +from traceback import format_exc +from typing import List, Tuple +from urllib.parse import unquote +import ipywidgets +from IPython.core.display import clear_output, display +from PIL import Image +from robot.reporting import ResultWriter +from robot.running.model import TestSuite +from robotkernel.builders import clean_items, populate_suite +from robotkernel.constants import ICON_FILE_TEXT +from robotkernel.display import DisplayKernel, ProgressUpdater +from robotkernel.listeners import ReturnValueListener, RobotKeywordsIndexerListener, RobotVariablesListener, StatusEventListener +from robotkernel.utils import data_uri, display_log, to_mime_and_metadata def execute_python(kernel: DisplayKernel, code: str, module: str, silent: bool): @@ -60,14 +52,20 @@ def execute_python(kernel: DisplayKernel, code: str, module: str, silent: bool): def normalize_argument(name): - return re.sub(r"\W", "_", re.sub(r"^[^\w]*|[^\w]*$", "", name, re.U), re.U) + if "=" in name: + name, default = name.split("=", 1) + else: + default = None + + return ( + name, + re.sub(r"\W", "_", re.sub(r"^[^\w]*|[^\w]*$", "", name, re.U), re.U), + default + ) def execute_ipywidget( kernel: DisplayKernel, - code: str, - history: OrderedDict, - listeners: list, silent: bool, display_id: str, rpa: bool, @@ -76,19 +74,23 @@ def execute_ipywidget( values, ): header = rpa and "Tasks" or "Test Cases" - code += f"""\ + code = f"""\ *** {header} *** {name} {name} {' '.join([values[a[1]] for a in arguments])} """ - suite = build_suite(code, history) + + # Copy the test suite + suite = deepcopy(kernel.suite) + populate_suite(code, suite, kernel.defaults) suite.rpa = True + try: with TemporaryDirectory() as path: run_robot_suite( - kernel, suite, listeners, silent, display_id, path, widget=True + kernel, suite, silent, display_id, path, widget=True ) except PermissionError: # Purging of TemporaryDirectory may fail e.g. with geckodriver.log still open @@ -97,9 +99,7 @@ def execute_ipywidget( def inject_ipywidget( kernel: DisplayKernel, - code: str, - history: OrderedDict, - listeners: list, + suite: TestSuite, silent: bool, display_id: str, rpa: bool, @@ -109,9 +109,6 @@ def inject_ipywidget( def execute(**values): execute_ipywidget( kernel, - code, - history, - listeners, silent, display_id, rpa, @@ -160,55 +157,28 @@ def update(*args): def inject_ipywidgets( kernel: DisplayKernel, - code: str, - history: OrderedDict, - listeners: list, + suite: TestSuite, silent: bool, display_id: str, rpa: bool, ): - suite_ = build_suite(code, {}) - for keyword in suite_.resource.keywords: + for keyword in kernel.new_keywords: name = keyword.name - arguments = [] - for arg in keyword.args: - if "=" in arg: - arg, default = arg.split("=", 1) - else: - default = None - arguments.append((arg, normalize_argument(arg), default)) + arguments = [normalize_argument(arg) for arg in keyword.args] + inject_ipywidget( - kernel, code, history, listeners, silent, display_id, rpa, name, arguments + kernel, suite, silent, display_id, rpa, name, arguments ) def execute_robot( kernel: DisplayKernel, - code: str, - history: OrderedDict, - listeners: list, + suite: TestSuite, silent: bool, ): display_id = str(uuid.uuid4()) - try: - suite = build_suite(code, history) - except Exception as e: - if not silent: - kernel.send_error( - { - "ename": e.__class__.__name__, - "evalue": str(e), - "traceback": list(format_exc().splitlines()), - } - ) - return { - "status": "error", - "ename": e.__class__.__name__, - "evalue": str(e), - "traceback": list(format_exc().splitlines()), - } - for listener in listeners: + for listener in kernel.listeners: # Update keywords catalog if isinstance(listener, RobotKeywordsIndexerListener): # noinspection PyProtectedMember @@ -222,14 +192,14 @@ def execute_robot( try: with TemporaryDirectory() as path: reply = run_robot_suite( - kernel, suite, listeners, silent, display_id, path + kernel, suite, silent, display_id, path ) except PermissionError: # Purging of TemporaryDirectory may fail e.g. with geckodriver.log still open pass else: inject_ipywidgets( - kernel, code, history, listeners, silent, display_id, suite.rpa + kernel, suite, silent, display_id, suite.rpa ) reply = {"status": "ok", "execution_count": kernel.execution_count} @@ -239,7 +209,6 @@ def execute_robot( def run_robot_suite( kernel: DisplayKernel, suite: TestSuite, - listeners: list, silent: bool, display_id: str, path: str, @@ -252,7 +221,7 @@ def run_robot_suite( progress = None # Init status - listeners = listeners[:] + listeners = kernel.listeners[:] if not (silent or widget): listeners.append(StatusEventListener(lambda data: progress.update(data))) if not silent: diff --git a/src/robotkernel/kernel.py b/src/robotkernel/kernel.py index 4ae5347..87fc772 100644 --- a/src/robotkernel/kernel.py +++ b/src/robotkernel/kernel.py @@ -9,41 +9,30 @@ from IPython.utils.tokenutil import line_at_cursor from ipykernel.kernelapp import IPKernelApp +from robot.running.model import TestSuite +from robot.running.builder.testsettings import TestDefaults from robotkernel import __version__ from robotkernel.completion_finders import complete_libraries -from robotkernel.constants import CONTEXT_LIBRARIES -from robotkernel.constants import HAS_NBIMPORTER -from robotkernel.constants import VARIABLE_REGEXP +from robotkernel.constants import CONTEXT_LIBRARIES, HAS_NBIMPORTER, VARIABLE_REGEXP from robotkernel.display import DisplayKernel from robotkernel.exceptions import BrokenOpenConnection -from robotkernel.executors import execute_python -from robotkernel.executors import execute_robot -from robotkernel.listeners import AppiumConnectionsListener -from robotkernel.listeners import JupyterConnectionsListener -from robotkernel.listeners import RobotKeywordsIndexerListener -from robotkernel.listeners import RobotVariablesListener -from robotkernel.listeners import RpaBrowserConnectionsListener -from robotkernel.listeners import SeleniumConnectionsListener -from robotkernel.listeners import WhiteLibraryListener -from robotkernel.monkeypatches import inject_libdoc_ipynb_support -from robotkernel.monkeypatches import inject_robot_ipynb_support -from robotkernel.selectors import clear_selector_highlights -from robotkernel.selectors import get_autoit_selector_completions -from robotkernel.selectors import get_selector_completions -from robotkernel.selectors import get_white_selector_completions -from robotkernel.selectors import get_win32_selector_completions -from robotkernel.selectors import is_autoit_selector -from robotkernel.selectors import is_selector -from robotkernel.selectors import is_white_selector -from robotkernel.selectors import is_win32_selector -from robotkernel.utils import close_current_connection -from robotkernel.utils import detect_robot_context -from robotkernel.utils import get_keyword_doc -from robotkernel.utils import get_lunr_completions -from robotkernel.utils import lunr_builder -from robotkernel.utils import lunr_query -from robotkernel.utils import scored_results -from robotkernel.utils import yield_current_connection +from robotkernel.builders import clean_items, populate_suite +from robotkernel.executors import execute_python, execute_robot +from robotkernel.listeners import ( + AppiumConnectionsListener, JupyterConnectionsListener, RobotKeywordsIndexerListener, + RobotVariablesListener, RpaBrowserConnectionsListener, SeleniumConnectionsListener, + WhiteLibraryListener +) +from robotkernel.monkeypatches import inject_libdoc_ipynb_support, inject_robot_ipynb_support +from robotkernel.selectors import ( + clear_selector_highlights, get_autoit_selector_completions, get_selector_completions, + get_white_selector_completions, get_win32_selector_completions, is_autoit_selector, + is_selector, is_white_selector, is_win32_selector +) +from robotkernel.utils import ( + close_current_connection, detect_robot_context, get_keyword_doc, get_lunr_completions, + lunr_builder, lunr_query, scored_results, yield_current_connection +) PID = os.getpid() @@ -84,6 +73,10 @@ def __init__(self, **kwargs): # Sticky connection cache (e.g. for webdrivers) self.robot_connections = [] + # Cache keywords + self.new_keywords = [] + self.keywords = [] + # Searchable index for keyword autocomplete documentation builder = lunr_builder("dottedname", ["dottedname", "name"]) self.robot_catalog = { @@ -98,6 +91,10 @@ def __init__(self, **kwargs): # noinspection PyProtectedMember populator._library_import(keywords, name) + # Create test suite + self.suite = TestSuite(name="Robocode Lab", source=os.getcwd()) + self.defaults = TestDefaults(None) + def do_shutdown(self, restart): super(RobotKernel, self).do_shutdown(restart) self.robot_history = OrderedDict() @@ -272,7 +269,7 @@ def do_execute( self.robot_variables.extend(VARIABLE_REGEXP.findall(code, re.U & re.M)) # Configure listeners - listeners = [ + self.listeners = [ RpaBrowserConnectionsListener(self.robot_connections), SeleniumConnectionsListener(self.robot_connections), JupyterConnectionsListener(self.robot_connections), @@ -282,13 +279,39 @@ def do_execute( RobotVariablesListener(self.robot_suite_variables), ] + # Build suite + try: + populate_suite(code, self.suite, self.defaults) + except Exception as e: + if not silent: + self.send_error( + { + "ename": e.__class__.__name__, + "evalue": str(e), + "traceback": list(format_exc().splitlines()), + } + ) + return { + "status": "error", + "ename": e.__class__.__name__, + "evalue": str(e), + "traceback": list(format_exc().splitlines()), + } + + # Keep new keywords in memory, they are used for constructing the widgets + self.new_keywords = [item for item in self.suite.resource.keywords._items if item not in self.keywords] + self.keywords = list(self.suite.resource.keywords._items) + # Execute test case - result = execute_robot(self, code, self.robot_history, listeners, silent,) + result = execute_robot(self, self.suite, silent) # Save history if result["status"] == "ok": self.robot_history[self.robot_cell_id or str(uuid.uuid4())] = code + # Clean the tests that were run + clean_items(self.suite.tests) + return result diff --git a/src/robotkernel/resources/notebooks/quickstart.ipynb b/src/robotkernel/resources/notebooks/quickstart.ipynb index 6199d79..49945d2 100644 --- a/src/robotkernel/resources/notebooks/quickstart.ipynb +++ b/src/robotkernel/resources/notebooks/quickstart.ipynb @@ -44,7 +44,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -53,28 +53,60 @@ "Library String" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "That said, it is ok for a cell to contain multiple headers, and the same header may occure more than once in the same notebook." - ] - }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 5, "metadata": {}, "outputs": [ { "data": { - "text/html": [ - "

Log | Report

" + "application/vnd.jupyter.widget-view+json": { + "model_id": "497cd89ab8284533aa09e9eae16558d0", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Box(children=(Button(description='Starts With', style=ButtonStyle()), Label(value='expected='), Text(value='')…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "95e9468a82754a7694b8a34cf9c5e088", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" ] }, "metadata": {}, "output_type": "display_data" } ], + "source": [ + "*** Keywords ***\n", + "\n", + "Starts With\n", + " [Arguments] ${expected} ${value}\n", + " Should Start With ${expected} ${value}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "That said, it is ok for a cell to contain multiple headers, and the same header may occure more than once in the same notebook." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "*** Variables ***\n", "\n", @@ -117,7 +149,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -158,7 +190,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -195,31 +227,9 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "

Log | Report

" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAbAAAAEgCAYAAADVKCZpAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8QZhcZAAAgAElEQVR4nO3de3CT56Em8EcXy/JNMsYykm2MMRBsbGIwCiQ9LCEwJknDkBMgXThJDyEwzuZ0d3LZdJudndM23WRCOc0G0sl0120uTNMNs9tpAwMJW4JJSEkYorGTHJrYpUE6YGMsyRdsS7Z1+/YPI2Ns2db90ys/v5nOnNhCes4M5vH7fu9FIUmSBCIiIsEo5Q5AREQUDRYYEREJiQVGRERCYoEREZGQWGBERCQkFhgREQmJBUZEREJigRERkZBYYEREJCQWGBERCYkFRkREQmKBERGRkFhgREQkJBYYEREJiQVGRERCYoEREZGQWGBERCQkFhgREQmJBUZEREJigRERkZBYYEREJCQWGBERCYkFRkREQmKBERGRkFhgREQkJBYYEREJiQVGRERCYoEREZGQWGBERCQkFhgREQmJBUZEREJigRERkZBYYEREJCQWGBERCUktd4BEKSwsRHl5udwxiIiEYrPZ4HQ65Y4RlrQtsPLyclgsFrljEBEJxWw2yx0hbJxCJCIiIbHAiIhISCwwIiISUkoV2OOPP46ioiLU1NSMfa2npwf19fVYsmQJ6uvr0dvbK2NCIiJKFSlVYI899hhOnDhxy9f27duHjRs34uLFi9i4cSP27dsnUzoiIkolKVVg69atQ0FBwS1fO3LkCHbt2gUA2LVrF9577z05ohERUYpJqQILpaurCyaTCQBgMplgt9tlTkQUvUBAwtdX++WOQZQWUr7AItHY2Aiz2Qyz2QyHwyF3HKJJ3r/Qie++9gmu9LjljkIkvJQvsHnz5qGzsxMA0NnZiaKioilf29DQAIvFAovFAoPBkKyIRGFr7RwAANi6XTInIRJfyhfYli1bcOjQIQDAoUOH8OCDD8qciCh6VudocXVeH5Y5CZH4UqrAdu7cibvuugttbW0oLS3FG2+8geeffx4nT57EkiVLcPLkSTz//PNyxySK2qUbBXaNBUYUs5Q6C/Hdd98N+fVTp04lOQlR/EmSBBtHYERxk1IjMKJ01tU/giGvHwBw7fqQzGmIxMcCI0qS4POv3Ew1R2BEccACI0qSYIGtXljAAiOKAxYYUZLYul3QqJWoK8vH9SEv3B6f3JGIhMYCI0qSSw4Xyudmo2ROFgCuRCSKFQuMKEls3S4sLMyBUccCI4oHFhhREvgDEi53u1FemAOTXguAS+mJYsUCI0qCq31D8PgDqCjMgfFGgV3rZ4ERxYIFRpQEwRM4yufmQJuhwpzsDHRyLxhRTFhgREkQPIFjoSEHAGDUZ6GzjyMwoliwwIiSwOp0IUejgiE3EwBg0mv5DIwoRiwwoiSwOl1YaMiBQqEAMFpgfAZGFBsWGFESWJ0ulM/NGftvk16LHpcHwzfORiSiyLHAiBLM4wugvdeNisKbBWbUj+4F6+IojChqLDCiBLvc40ZAAsoLbx2BAdwLRhQLFhhRggUP8V14ywgsWGBcSk8ULWEK7ODBg6ipqUF1dTUOHDggdxyisNlCFBhHYESxE6LALly4gF//+tc4f/48vvzySxw7dgwXL16UOxZRWC45XZiTnYH8bM3Y17I1auizMngeIlEMhCiwb775BnfeeSeys7OhVqtx9913449//KPcsYjCYnO6bhl9BXEvGFFshCiwmpoanDlzBt3d3XC73Xj//fdx5coVuWMRhcXqdN2ygCPIqNdyBEYUA7XcAcJRVVWFH/3oR6ivr0dubi5qa2uhVk+O3tjYiMbGRgCAw+FIdkyiSdweH671D9+yhD7IpNfiQke/DKmI0oMQIzAA2LNnD5qbm3HmzBkUFBRgyZIlk17T0NAAi8UCi8UCg8EgQ0qiW9mcbgAIPQLTZcE5OIIRHzczE0VDmAKz2+0AgMuXL+MPf/gDdu7cKXMiopnZuievQAwKrkS0948kNRNRuhBiChEAtm3bhu7ubmRkZOD111/HnDlz5I5ENCPruGtUJjKOW0o/vyA7qbmI0oEwBfbJJ5/IHYEoYlanC/N0mcjJnPyjVpzPzcxEsRBmCpFIRBMP8R0veB4iVyISRYcFRpRAVqcLFYbQBZabqUZeppp7wYiixAIjSpDrbi96XJ4pR2AA94IRxYIFRpQg1mlWIAYZ9Vo+AyOKEguMKEFCHeI7EY+TIooeC4woQS45XVAogLK5Uy+RN+mz4BgcgdcfSGIyovTAAiNKEJvThdI5WchUq6Z8jUmvhSQB9gFuZiaKFAuMKEGmW0IfFNzMfI3PwYgixgIjSgBJkmBzukIe4jue6cZeMD4HI4ocC4woAZyDHgyM+EIe4jve2HFSfSwwokixwIgSYLpDfMfTadXI1qg4AiOKAguMKAGsjvAKTKFQjG5m7uczMKJIscCIEsDa7UKGSoGS/KwZX1usz+IIjCgKLDCiBLA6XJhfkA21auYfMR4nRRQdFhhRAljDWIEYZNJrYR8YgY+bmYkiwgIjirNAQIKte+Y9YEFGvRb+gATHIDczE0VCmAJ79dVXUV1djZqaGuzcuRPDw5xyodTU2T+MEV8AC6e4RmUi07ibmYkofEIUWEdHB1577TVYLBZcuHABfr8fhw8fljsWUUhjh/iGOwLT8WJLomgIUWAA4PP5MDQ0BJ/PB7fbjeLiYrkjEYV0KVhgYY7AivM5AiOKhhAFVlJSgueeew5lZWUwmUzQ6/XYtGmT3LGIQrI5XcjKUGFenjas1+uzMqDNUPI8RKIICVFgvb29OHLkCKxWK65evQqXy4V33nln0usaGxthNpthNpvhcDhkSEo0ugJxwdxsKJWKsF6vUChg4l4woogJUWAffvghFi5cCIPBgIyMDGzduhWffvrppNc1NDTAYrHAYrHAYDDIkJRodARWEeb0YZBRx71gRJESosDKyspw7tw5uN1uSJKEU6dOoaqqSu5YRJP4/AFc7nGHvYQ+iDczE0VOiAJbs2YNtm/fjrq6OixfvhyBQAANDQ1yxyKapL13CL6ANOMZiBMZ9Vp09Q/DH5ASlIwo/ajlDhCuF154AS+88ILcMYimZXWGd4jvRKb8LPgCEroHR1CkC2/xB9FsJ8QIjEgUUReYjkvpiSLFAiOKI6vThTytGgU5moj+nJGncRBFjAVGFEfBQ3wVivCW0AcFj5PiXjCi8LHAiOLI6nShPMLpQwAoyNFAo1JyBEYUARYYUZwMe/24en0o4udfwM2bmVlgROFjgRHFyeUeNyQp8gUcQbzYkigyLDCiOLnkiG4FYlCxXovOfj4DIwoXC4woTmzdowUWzTMwADDqs9B1fQQBbmYmCgsLjChOrA4XCnM10GkzovrzJr0WHn8APW5PnJMRpScWGFGcWLtdUU8fAuP2gvXxORhROFhgRHFidboiPsR3PNPYZmY+ByMKBwuMKA4GR3xwDIyEfQtzKMER2LV+jsCIwsECI4oDW/AMxBhGYIU5mchQKbgXjChMLDCiOBg7xDeGEZhSqcA8XmxJFDYWGFEcBAtsQUH0BQYEL7bkMzCicLDAiOLA6nShWK9FlkYV0/sY9VkcgRGFSYgCa2trw4oVK8b+p9PpcODAAbljEY2J9hDfiUw3zkOUJG5mJpqJEDcyL126FF988QUAwO/3o6SkBA899JDMqYhusjpd2Hy7Keb3Meq0GPEF0Ov2RnynGNFsI8QIbLxTp05h0aJFWLBggdxRiAAAvS4Prg95Y9rEHMS9YEThE67ADh8+jJ07d8odg2jMJWdsh/iOZ8rPAgA+ByMKg1AF5vF4cPToUTz88MMhv9/Y2Aiz2Qyz2QyHw5HkdDRbBfeAxesZGADuBSMKg1AF9sEHH6Curg7z5s0L+f2GhgZYLBZYLBYYDIYkp6PZyup0QaVUYP6c7JjfqzA3EyqlgiMwojAIVWDvvvsupw8p5Vi7XZg/Jwsadew/TiqlAvPyMnGVz8CIZiRMgbndbpw8eRJbt26VOwrRLayO+CyhD+LNzEThEabAsrOz0d3dDb1eL3cUojGSJMEW4zUqE5m4mZkoLMIUGFEqsg+MwO3xx7nAuJmZKBwsMKIYWOO4hD7IqNdiyOtH/5Avbu9JlI5YYEQxCBZYLBdZTmTSj+4F6+znQg6i6bDAiGJgdbqgUStRfGMDcjwYuReMKCwsMKIYWJ0uLCjIhkqpiNt7jm1m7mOBEU2HBUYUA6szvisQAcCQlwmlArjGvWBE02KBEUXJH5Bwudsd9wLLUClhyMvkFCLRDFhgRFG62jcEjz8Q9wIDbuwF62eBEU2HBUYUJWscD/GdKLgXjIimxgIjilKwwCoSUGA8TopoZiwwoihZnS7kaFQw5GXG/b1Nei0GR3zoH/bG/b2J0gULjChKVufoIb4KRfyW0AcZ9bzYkmgmLDCiKMX7EN/xeLEl0cxYYERR8PgCuNIT/yX0QcEC414woqmxwIiicKXXjYAU30N8xyvK00Kh4AiMaDrCFFhfXx+2b9+OyspKVFVV4bPPPpM7Es1iVkfiltADgEatRGFuJp+BEU1DLXeAcD311FO477778Pvf/x4ejwdut1vuSDSLJXIJfZBJr8VVFhjRlIQosP7+fpw5cwZvv/02AECj0UCj0cgbimY1a7cL+dkZyM9O3N9Do04LW7crYe9PJDohphAvXboEg8GA3bt3Y+XKldi7dy9cLv5gk3ysjsStQAziaRxE0xOiwHw+H5qbm/Hkk0+ipaUFOTk52Ldv36TXNTY2wmw2w2w2w+FwyJCUZgtbtwsL43iJZShGfRYGhn0YHOHNzEShCFFgpaWlKC0txZo1awAA27dvR3Nz86TXNTQ0wGKxwGKxwGAwJDsmzRJDHj86rw8nfARWnB9cSs9RGFEoQhSY0WjE/Pnz0dbWBgA4deoUli1bJnMqmq2Cz6UStQIxyKhjgRFNR4hFHADwy1/+Eo888gg8Hg8qKirw1ltvyR2JZqngCsTEPwMbPU6qk5uZiUISpsBWrFgBi8UidwyipBVYkW70kGAu5CAKTYgpRKJUYnW6UJSXiZzMxP7+p81QYW6OhgVGNAUWGFGEbM7EL6EPGr0XjFOIRKGwwIgiZE1igZn0WRyBEU2BBUYUgetDXnS7PEksMC2u9bPAiEJhgRFFwOZMzhL6IKNeiz63F0Mef1I+j0gkLDCiCCTjEN/xbl5syedgRBOxwIgiYHW6oFAA8wuyk/J5Rj03MxNNhQVGFAGr04WS/CxoM1RJ+bybm5lZYEQTscCIImDrTt4KRODmFCIXchBNxgIjCpMkSUm5RmU8bYYKc7Iz+AyMKAQWGFGYul0eDIz4UJ7ga1QmMuqz+AyMKAQWGFGYxs5ANCS3wEx6La72scCIJmKBEYVprMCSPgLjZmaiUFhgRGGyOl1QKxUonZOV1M816bTocXkw7OVmZqLxWGBEYbI5XSibmw21Krk/NsG9YF0chRHdggVGFCar05X06UMAKM7nXjCiUIS50LK8vBx5eXlQqVRQq9W83JKSKhCQYOt2Ye3iwqR/Nk/jIApNmAIDgNOnT6OwMPn/gBBd6x/GsDeQtEN8xzPqguchssCIxuMUIlEYbEk+xHe8nEw1dFo1NzMTTSDMCEyhUGDTpk1QKBR44okn0NDQkJDPeefcv+HIFx0JeW9KnC21xfj+XeUJe/9LSb5GZSK5Lrbcf6IVn9t6kv65FJtn6m/Ddxal/2yVMAV29uxZFBcXw263o76+HpWVlVi3bt0tr2lsbERjYyMAwOFwRPU5KqUCGUleZUax6XF58M9H/gJthgoPm+cn5DOsThe0Gcqx6bxkM+q1SX8G1uf24H9+/C3KC3Nk+/+boqNUKOSOkBTCFFhxcTEAoKioCA899BDOnz8/qcAaGhrGRmZmszmqz9m5ugw7V5fFFpaSyuML4PG3P8d//cO/Yp5Oi3W3GeL+GTanC+Vzc6BUyvMPQ3G+Fn+52p/Uz/z4rw4EJOAXD9eirmxOUj+bKBxCDDVcLhcGBgbG/u8//elPqKmpkTkVpQqNWolfPVqHxUW5+KffNePrBPxDb3Um9xDfiYy6LDgHR+DxBZL2madb7SjI0aC2ND9pn0kUCSEKrKurC2vXrkVtbS1Wr16NBx54APfdd5/csSiF5Gkz8Pbu1cjTqrH77fPo6IvfggefP4DLPW7Znn8BN69VSdZmZn9Awkd/dWD9bQaoZBp1Es1EiCnEiooKfPnll3LHoBRn1Gvx1u478PCvPsPut87j//6H70CflRHz+7b3DsEXkOQdgelvLqVPxm3QLZd70ef24p7KooR/FlG0hBiBEYWr0qjD//r+KlidLjzxWwtGfLGfH2jtvnGIbwqMwJK1lL6p1Q6VUpGQ54lE8cICo7TzncWF+JfttTh3qQf/5fdfIRCQYno/q0P+Akv2aRxNrXaYF8yJywiWKFFYYJSW/n5lCX5471Ic+eIq/uVPbTG9l63bhTytGnNzNHFKF7k8bQZyM9VJ2Qt2tW8IrdcGsIHTh5TihHgGRhSNf1q/CB19Q/jVR9+iOD8L379zQVTvE1yBqJB5b40pSXvBTrfZAYAFRimPBUZpS6FQ4GdbqtF1fRg/OXIBRp0W9cvmRfw+VqcLqxbIvw/KqNeiMwmrEE+32lE6JwuLi3IT/llEseAUIqU1tUqJX/7DStSU6PGf3m3GF1f6Ivrzw14/OvqGUC7DNSoTmfRadMZxe0Aow14/zv6tGxsqi2QfcRLNhAVGaS9bo8Ybu+6AIS8Te97+HP92Y1VhOK70uCFJQIVB/gIz6rPgGByB15+4zcznLnVjyOvn8nkSAguMZgVDXibe3r0afknCY299jh6XJ6w/N3aIb4qMwCQJsA+MJOwzTrfaoc1Q4q6KuQn7DKJ4YYHRrLHIkIvf/KMZHX1D2Hvocwx7Z94jZpX5FPrxbi6lT8w0oiRJaGqz4+8WFUKboUrIZxDFEwuMZhVzeQEO/vsVaLnSh6cOt8A/wx4xm9OFuTmalNgPVazPApC4iy2/dQziSs8Qpw9JGCwwmnXuX27CPz+wDP/vL13478e+hiRNXWKXZD7Ed7xEb2Zuah1dPs8CI1FwGT3NSo+vXYiOviG88WcrSudkYe+/qwj5OpvTlTLHKem0amRrVAkbgTW12lFpzENJflZC3p8o3jgCo1nrv323Ct9dbsSLx7/B8a86J31/cMQH+8BIyozAFArF6F6wBDwDuz7khcXWy9EXCYUFRrOWUqnA//jeCpgXzMEz/+cLfG7rueX7Nqf8ZyBOZNJrEzIC++SiA76AhI0sMBIIC4xmNW2GCr/+R/PoNOIhC/5mHxz7njUFC8yoy0rIM7CmVjvyszOwkjcvk0BYYDTrzcnR4NDu1chQKbDrzfOwD4wWhC2F9oAFmfRa2AdG4IvjZuZAQMLHbQ7czcsrSTBCFZjf78fKlSuxefNmuaNQmplfkI03H7sDPS4PHn/7c7hGfLA6XTDptcjSpM6eKFO+Fv6ABOdgeBuxw/Flex+6XR4e3kvCEarADh48iKqqKrljUJq6vTQfrz+yEl9f7ccP/ncz/uYYTKnpQyAxF1uebrVDqQDuTpHVlkThEqbA2tvbcfz4cezdu1fuKJTGNlTOw4t/vxwftTnwVfv1lDiBYzyjLv6bmZva7Kgrm4P8bPnuOyOKhjAF9vTTT2P//v1QKoWJTIL6hzVl+I/3LAYAVKRYgd0cgcWnwOz9w7jQ0c/l8yQkITYyHzt2DEVFRVi1ahU++uijKV/X2NiIxsZGAIDD4UhSOkpH/3nTbagy6bB2SaHcUW6Rn52BTLUybuch8vJKEpkQw5mzZ8/i6NGjKC8vx44dO9DU1IRHH3100usaGhpgsVhgsVhgMHA+n6KnUCjwwO2mlDgDcTyFQhHXvWBNrXaY9FpUGvPi8n5EySREgb388stob2+HzWbD4cOHsWHDBrzzzjtyxyKShUkfn71gIz4//nzRiXt4eSUJSogCI6Kb4jUC+9zaC5fHjw1LOX1IYhLiGdh469evx/r16+WOQSQbo16Lrv5h+ANSTBuPm1rt0KiV+M5iXl5JYuIIjEgwJr0WvoCE7sHYbmY+3WbHXRVzka0R7vdYIgAsMCLhGONwseUlxyCsThc2VnH6kMTFAiMSTDz2go1dXsnnXyQwFhiRYExjNzNHvxfsdJsdS4pyMb8gO16xiJKOBUYkmIIcDTQqJTr7oxuBDY74cN7aw83LJDwWGJFggjczR7sX7M8XHfD6JR4fRcJjgREJyKjXorMvugJrarUjT6vGqgW8vJLExgIjEpBJr0Vnf+TPwAIBCafbHFh3mwEZKv74k9j4N5hIQEa9Fl3XRxAISBH9ub9c7YdjYISnb1BaYIERCcik08LjD6DHHdnNzE2tdigUwPqlPOyaxMcCIxKQKX90M3OkCzma2uyoLc3H3NzMRMQiSioWGJGAotnM7BwcwVftfVw+T2mDBUYkIONYgYW/kOOjNgckiZdXUvpggREJqDAnE2qlIqIR2OlWO+bpMlFdrEtgMqLkYYERCUipVGCeLvzNzF5/AGf+6sA9S3l5JaUPIQpseHgYq1evRm1tLaqrq/GTn/xE7khEshu92DK8KcTPbT0YGPHx9A1KK0JcBJSZmYmmpibk5ubC6/Vi7dq1uP/++3HnnXfKHY1INqb8LPxre19Yrz3daodGpcTaxYUJTkWUPEKMwBQKBXJzcwEAXq8XXq+X0yA0642OwIYhSTNvZm5qtWNNRQFyMoX4nZUoLEIUGAD4/X6sWLECRUVFqK+vx5o1a+SORCQro06LEV8AvW7vtK+73O3Gtw4X7/6itCNMgalUKnzxxRdob2/H+fPnceHChUmvaWxshNlshtlshsPhkCElUfKYwlxK39TaBYDL5yn9CFNgQfn5+Vi/fj1OnDgx6XsNDQ2wWCywWCwwGHhUDqU349jFltOvRGxqc6CiMAflhTnJiEWUNEIUmMPhQF/f6MPqoaEhfPjhh6isrJQ5FZG8TPrR46Sm2wvm9vhw7lI3Vx9SWhLiiW5nZyd27doFv9+PQCCA733ve9i8ebPcsYhkZcjLhEqpmHYEdvZv3fD4Apw+pLQkRIHdfvvtaGlpkTsGUUpRKRWYl5c57QisqdWO3Ew17igvSGIyouQQYgqRiEIz6rW4NsXFlpIk4aM2O9YuLoRGzR91Sj/8W00kMJM+C519oUdg33QOoPP6MKcPKW2xwIgEZpxmM/PpNjsAYH0lV+RSemKBEQnMpNdiyOtH/5Bv0veaWu24vVSPojytDMmIEo8FRiSwsXvBJjwH63F50HK5l6dvUFpjgREJbKq9YB//1Y4AL6+kNMcCIxKYaYrTOJpaHSjMzcTyEr0csYiSggVGJDBDXiaUCqCz7+YUos8fwMdtdqxfaoBSyVsbKH2xwIgElqFSwjBhM3Pz5T70D/s4fUhpjwVGJDijPgvX+m8WWFOrHWqlAmuX8PJKSm8sMCLBmXTaW0Zgp1vtuKO8ADpthoypiBKPBUYkOKNeO7aIo6NvCG1dA5w+pFmBBUYkuOJ8LQZHfBgY9qKpdfT0DV6fQrMBC4xIcMYbe8GuXR/G6VY7ygqyscjAyysp/bHAiAQX3At2yenCp986saGyCAoFl89T+mOBEQnOqBstsD82d2DYy8srafYQosCuXLmCe+65B1VVVaiursbBgwfljkSUMubdKLCT33QhW6PCmgpeXkmzgxA3MqvVarzyyiuoq6vDwMAAVq1ahfr6eixbtkzuaESy06iVKMzNhHNwBH+3uBCZapXckYiSQogRmMlkQl1dHQAgLy8PVVVV6OjokDkVUeoozh8dhXH6kGYTIQpsPJvNhpaWFqxZs0buKEQpI/gcjNen0GwixBRi0ODgILZt24YDBw5Ap9NN+n5jYyMaGxsBAA6HI9nxiGRzb7URc3M1Y/eDEc0GCinUXeQpyOv1YvPmzbj33nvx7LPPzvh6s9kMi8WShGREROlDpH87hZhClCQJe/bsQVVVVVjlRURE6U+IAjt79ix++9vfoqmpCStWrMCKFSvw/vvvyx2LiIhkJMQzsLVr10KQmU4iIkoSIUZgREREE7HAiIhISCwwIiISEguMiIiExAIjIiIhCbOROVKFhYUoLy+P6s86HA4YDIb4BkogkfKKlBUQK69IWQGx8oqUFYgtr81mg9PpjHOixEjbAouFSDvRAbHyipQVECuvSFkBsfKKlBUQL2+0OIVIRERCYoEREZGQVD/96U9/KneIVLRq1Sq5I0REpLwiZQXEyitSVkCsvCJlBcTLGw0+AyMiIiFxCpGIiITEApvgxIkTWLp0KRYvXox9+/bJHWdKV65cwT333IOqqipUV1fj4MGDckeakd/vx8qVK7F582a5o8yor68P27dvR2VlJaqqqvDZZ5/JHWlar776Kqqrq1FTU4OdO3dieHhY7khjHn/8cRQVFaGmpmbsaz09Paivr8eSJUtQX1+P3t5eGRPeKlTeH/7wh6isrMTtt9+Ohx56CH19fTImvClU1qBf/OIXUCgUwiyJjwYLbBy/348f/OAH+OCDD/D111/j3Xffxddffy13rJDUajVeeeUVfPPNNzh37hxef/31lM0adPDgQVRVVckdIyxPPfUU7rvvPrS2tuLLL79M6dwdHR147bXXYLFYcOHCBfj9fhw+fFjuWGMee+wxnDhx4pav7du3Dxs3bsTFixexcePGlPplMVTe+vp6XLhwAV999RVuu+02vPzyyzKlu1WorMDoL7gnT55EWVmZDKmShwU2zvnz57F48WJUVFRAo9Fgx44dOHLkiNyxQjKZTKirqwMA5OXloaqqCh0dHTKnmlp7ezuOHz+OvXv3yh1lRv39/Thz5gz27NkDANBoNMjPz5c51fR8Ph+Ghobg8/ngdrtRXFwsd6Qx69atQ0FBwS1fO3LkCHbt2gUA2LVrF9577z05ooUUKu+mTZugVo/ePnXnnXeivb1djmiThMoKAM888wz2798PhUIhQ6rkYYGN09HRgfnz54/9d2lpaUqXQtNkbhwAAAN4SURBVJDNZkNLSwvWrFkjd5QpPf3009i/fz+UytT/K3fp0iUYDAbs3r0bK1euxN69e+FyueSONaWSkhI899xzKCsrg8lkgl6vx6ZNm+SONa2uri6YTCYAo7+M2e12mROF780338T9998vd4wpHT16FCUlJaitrZU7SsKl/r8mSRRqQWaq/wYzODiIbdu24cCBA9DpdHLHCenYsWMoKioSZlmvz+dDc3MznnzySbS0tCAnJyelprgm6u3txZEjR2C1WnH16lW4XC688847csdKSy+99BLUajUeeeQRuaOE5Ha78dJLL+FnP/uZ3FGSggU2TmlpKa5cuTL23+3t7Sk1FTOR1+vFtm3b8Mgjj2Dr1q1yx5nS2bNncfToUZSXl2PHjh1oamrCo48+KnesKZWWlqK0tHRsRLt9+3Y0NzfLnGpqH374IRYuXAiDwYCMjAxs3boVn376qdyxpjVv3jx0dnYCADo7O1FUVCRzopkdOnQIx44dw+9+97uU/cX222+/hdVqRW1tLcrLy9He3o66ujpcu3ZN7mgJwQIb54477sDFixdhtVrh8Xhw+PBhbNmyRe5YIUmShD179qCqqgrPPvus3HGm9fLLL6O9vR02mw2HDx/Ghg0bUnqEYDQaMX/+fLS1tQEATp06hWXLlsmcamplZWU4d+4c3G43JEnCqVOnUnrRCQBs2bIFhw4dAjBaDA8++KDMiaZ34sQJ/PznP8fRo0eRnZ0td5wpLV++HHa7HTabDTabDaWlpWhubobRaJQ7WmJIdIvjx49LS5YskSoqKqQXX3xR7jhT+uSTTyQA0vLly6Xa2lqptrZWOn78uNyxZnT69GnpgQcekDvGjFpaWqRVq1ZJy5cvlx588EGpp6dH7kjT+vGPfywtXbpUqq6ulh599FFpeHhY7khjduzYIRmNRkmtVkslJSXSb37zG8npdEobNmyQFi9eLG3YsEHq7u6WO+aYUHkXLVoklZaWjv2sPfHEE3LHlCQpdNbxFixYIDkcDpnSJR5P4iAiIiFxCpGIiITEAiMiIiGxwIiISEgsMCIiEhILjIiIhMQCIyIiIbHAiIhISCwwIiISEguMiIiExAIjIiIhscCIiEhILDAiIhISC4yIiITEAiMiIiGxwIiISEgsMCIiEhILjIiIhMQCIyIiIbHAiIhISCwwIiIS0v8HtBV2E08jV+kAAAAASUVORK5CYII=" - }, - "metadata": { - "image/png": { - "height": 288, - "width": 432 - } - }, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "*** Settings ***\n", "\n", @@ -264,7 +274,7 @@ "language_info": { "codemirror_mode": "robotframework", "file_extension": ".robot", - "mimetype": "text/plain", + "mimetype": "text/x-robotframework", "name": "Robot Framework", "pygments_lexer": "robotframework" }, diff --git a/tests/test_builders.py b/tests/test_builders.py index b9a2691..9dd47b9 100644 --- a/tests/test_builders.py +++ b/tests/test_builders.py @@ -1,5 +1,8 @@ # -*- coding: utf-8 -*- -from robotkernel.builders import build_suite +from robot.running.model import TestSuite +from robot.running.builder.testsettings import TestDefaults + +from robotkernel.builders import populate_suite TEST_SUITE = """\ @@ -24,6 +27,9 @@ def test_string(): - suite = build_suite(TEST_SUITE, {}) + suite = TestSuite() + + populate_suite(TEST_SUITE, suite, TestDefaults()) + assert len(suite.resource.keywords) == 1 assert len(suite.tests) == 1