-
Notifications
You must be signed in to change notification settings - Fork 2
/
Examples.py
214 lines (179 loc) · 9.9 KB
/
Examples.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
from robot.running.model import TestSuite
from robot.model.testcase import TestCases
from robot.libraries.BuiltIn import BuiltIn
from robot.running.context import EXECUTION_CONTEXTS
from robot.api import logger
from robot.api.deco import library, keyword
from robot.errors import VariableError
from RoboPandas import create_dataframe
import random
@library(scope='TEST SUITE', doc_format='reST')
class Examples(object):
"""Examples library adds support of a list of Example test data to Robot Framewok test cases.
With Examples, a Data driven test case can be defined where the data and the test process
are kept together in the same part of the test case definition file, eliminating a
cognitive split between these parts of a test scenario definition.
Unlike template test cases, multiple similar scenarios can be present in the same robot file.
To use the Examples: keyword, this library must be referenced in the test suite.
As long as autoexpand is not set to false, Expand Test Examples (below) is automatically called.
An example of a test case defined with Examples:
.. code:: robotframework
*** Settings ***
Library Examples
*** Test cases ***
My test with examples for ${name}
Log Hello ${name}, welcome to ${where welcome} console=True
Examples: name where welcome --
... Joe the world!
... Arthur Camelot (clip clop).
... Patsy it's only a model!
After expansion, this is equivalent to:
.. code:: robotframework
*** Test cases ***
My test with examples for Joe
Log Hello Joe, welcome to the world! console=True
My test with examples for Arthur
Log Hello Arthur, welcome to Camelot (clip clop). console=True
My test with examples for Patsy
Log Hello Patsy, welcome to it's only a model console=True
"""
ROBOT_LISTENER_API_VERSION = 3
def __init__(self, autoexpand=True, max_examples=None, random=None):
"""max_example and random can be specified globally as described above.
These arguments can be over-ridden for individual calls to Expand Test Examples.
In certain scenario's, data may be retrieved from external sources or defined by other keywords.
When this is needed, Library Examples should have autoexpand=False.
In this case, the variables needed for the example data to be resolved can be defined first during
Suite setup, then Expand test examples can be explicitly called once the data is ready.
"""
self.ROBOT_LIBRARY_LISTENER = self
self.current_suite = None
self.autoexpand = False if hasattr(autoexpand, 'lower') and autoexpand.lower() in ['false', 'no', 'off', 'f', '0'] else autoexpand
self.max_examples = int(max_examples) if max_examples else max_examples
self.random = random
def start_suite(self, suite, result):
# save current suite so that we can modify it later
self.current_suite = suite
if self.autoexpand:
self.expand_test_examples()
def _localise_scope(self):
# Create a local scope for providing keyword arguments to user
# specified keywords.
variables = EXECUTION_CONTEXTS.current.variables
outside_scope = variables.current
variables.start_keyword()
# Copy the outside scope to the internal scope.
# This is done because one would expect the variables from the current scope of this keyword to be available.
# Therefore, local script variables can also be referred to in the
# spreadsheet.
variables.current.update(outside_scope)
return variables
@keyword()
def examples(self, *examples_data):
"""This keyword is not called. This keyword is searched for and removed by Expand test examples.
The arguments are expanded to create concrete test cases as described by Expand Test Examples."""
BuiltIn().fail('Expand Test Examples should be called in Suite setup.')
@keyword()
def expand_test_examples(self, max_examples=None, random=None):
"""The "Examples:" keyword is searched for in test cases by this keyword, Either automatically
by the library import, or explicitly when autoexpand is False.
When Examples: is found, the following occurs:
* Column headers are determined as the first arguments before the '--' delimiter argument
* Data rows are combined with the headers to form a dataframe.
* The number of data arguments MUST be an exact multiple of the number of headers.
* A new test case is created for each row in the table of examples.
* If max_examples is specified, no more than max_examples test cases are produced for this scenario.
* When random is specified, the examples are chosen in a random order.
If this is a number, it is also used as max_examples.
* The new test case is search for variables where the variable name matches a coloumn header name.
When a matching variable name is found, it is replaced with the example value.
* Any variables in scope at the time of example replacement (e.g. global variables) are replaced as
actual values in the test.
* Test case name is searched and replaced. This is necessary for Robot Framework to have unique test
names.
* Keyword names, argument values, Tags, IF conditions and FOR loops arguments are replaced
with variable values in scope at the time of replacement.
Optional argument random can be used to specify that examples should be in a random order.
When random is an integer and max_examples is not specified, it will be used as the number of examples to choose.
Argument max_examples can be used to specify the maximum number of examples to process.
This can be helpful if the examples are dynamically defined using generated arguments and for
development purposes."""
self._max_examples = int(max_examples) if max_examples else self.max_examples
self._random = random if random else self.random
if self._random and not max_examples:
try:
self._max_examples = int(self._random)
except ValueError:
pass
self._expand_tcs_in_suite(self.current_suite)
def _expand_tcs_in_suite(self, suite):
current_tests = suite.tests
suite.tests = TestCases()
for tc in current_tests:
if not self._expand_example_tc(tc):
suite.tests.append(tc)
for suite in suite.suites:
self._expand_tcs_in_suite(suite)
def _expand_example_tc(self, example_tc):
self.example_tc = example_tc
for kw in example_tc.body:
try:
if kw.name.lower() == 'examples:':
args = BuiltIn()._variables.replace_list(kw.args)
break
except AttributeError:
continue
else:
return False
records = create_dataframe(*args).to_dict('records')
if self._random:
example_data = random.sample(records, self._max_examples or len(records))
else:
example_data = records[0:self._max_examples]
self.variables = self._localise_scope()
self.first_tc = True
for example in example_data:
self.variables.current.store.data.update(example)
filled_tc = self.current_suite.tests.create(self.variables.replace_scalar(example_tc.name, ignore_errors=True))
filled_tc.setup = example_tc.setup
filled_tc.teardown = example_tc.teardown
filled_tc.tags = self.replace_list(example_tc.tags)
self._populate_example_to_body(example_tc.body, filled_tc.body)
self.first_tc = False
self.variables.end_keyword()
return True
def _populate_example_to_body(self, body, target):
for kw in body:
self.kw = kw
if kw.type == 'KEYWORD':
if kw.name.lower() == 'examples:':
continue
extra_args = {}
if hasattr(kw, "tags"):
extra_args["tags"] = self.replace_list(kw.tags)
if hasattr(kw, "timeout"):
extra_args["timeout"] = kw.timeout
target.create_keyword(self.variables.replace_scalar(kw.name, ignore_errors=True),
args = self.replace_list(kw.args),
assign=kw.assign,
lineno=kw.lineno,
**extra_args)
else:
new_kw = kw.deepcopy()
if hasattr(new_kw, 'values'):
new_kw.values = self.replace_list(kw.values)
if hasattr(new_kw, 'condition'):
new_kw.condition = self.variables.replace_scalar(kw.condition, ignore_errors=True)
# TODO: replacement for IF is also necessary - I currently don't have an example of this use-case to test
if hasattr(kw, "body"):
new_kw.body = None
self._populate_example_to_body(kw.body, new_kw.body)
target.append(new_kw)
def replace_list(self, args):
try:
return self.variables.replace_list(args)
except VariableError as e:
result = self.variables.replace_list(args, ignore_errors=True)
if self.first_tc:
logger.info(f'Replacing in tc {self.example_tc.longname}, line {self.kw.lineno}\n{e}\nCurrent result is {result}')
return result