robotframework-pabot 4.2.0__tar.gz → 4.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {robotframework_pabot-4.2.0/src/robotframework_pabot.egg-info → robotframework_pabot-4.3.1}/PKG-INFO +13 -6
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/README.md +12 -5
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/src/pabot/__init__.py +1 -1
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/src/pabot/arguments.py +67 -0
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/src/pabot/execution_items.py +54 -2
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/src/pabot/pabot.py +100 -69
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1/src/robotframework_pabot.egg-info}/PKG-INFO +13 -6
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/src/robotframework_pabot.egg-info/SOURCES.txt +2 -0
- robotframework_pabot-4.3.1/tests/test_arguments_output.py +260 -0
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/tests/test_depends.py +75 -2
- robotframework_pabot-4.3.1/tests/test_missing_subprocess_output.py +134 -0
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/tests/test_ordering.py +278 -0
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/tests/test_pabot.py +12 -3
- robotframework_pabot-4.3.1/tests/test_run_empty_suite.py +74 -0
- robotframework_pabot-4.2.0/tests/test_arguments_output.py +0 -67
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/LICENSE.txt +0 -0
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/MANIFEST.in +0 -0
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/pyproject.toml +0 -0
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/setup.cfg +0 -0
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/setup.py +0 -0
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/src/pabot/SharedLibrary.py +0 -0
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/src/pabot/clientwrapper.py +0 -0
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/src/pabot/coordinatorwrapper.py +0 -0
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/src/pabot/pabotlib.py +0 -0
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/src/pabot/py3/__init__.py +0 -0
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/src/pabot/py3/client.py +0 -0
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/src/pabot/py3/coordinator.py +0 -0
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/src/pabot/py3/messages.py +0 -0
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/src/pabot/py3/worker.py +0 -0
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/src/pabot/result_merger.py +0 -0
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/src/pabot/robotremoteserver.py +0 -0
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/src/pabot/workerwrapper.py +0 -0
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/src/robotframework_pabot.egg-info/dependency_links.txt +0 -0
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/src/robotframework_pabot.egg-info/entry_points.txt +0 -0
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/src/robotframework_pabot.egg-info/requires.txt +0 -0
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/src/robotframework_pabot.egg-info/top_level.txt +0 -0
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/tests/test_basic_arguments.py +0 -0
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/tests/test_functional.py +0 -0
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/tests/test_pabotlib.py +0 -0
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/tests/test_pabotprerunmodifier.py +0 -0
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/tests/test_pabotsuitenames_io.py +0 -0
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/tests/test_prerunmodifier.py +0 -0
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/tests/test_resultmerger.py +0 -0
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/tests/test_stacktrace.py +0 -0
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/tests/test_suite_structure.py +0 -0
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/tests/test_testlevelsplit_include.py +0 -0
- {robotframework_pabot-4.2.0 → robotframework_pabot-4.3.1}/tests/test_testlevelsplit_output_task_order.py +0 -0
{robotframework_pabot-4.2.0/src/robotframework_pabot.egg-info → robotframework_pabot-4.3.1}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: robotframework-pabot
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.3.1
|
|
4
4
|
Summary: Parallel test runner for Robot Framework
|
|
5
5
|
Home-page: https://pabot.org
|
|
6
6
|
Download-URL: https://pypi.python.org/pypi/robotframework-pabot
|
|
@@ -156,7 +156,7 @@ Supports all [Robot Framework command line options](https://robotframework.org/r
|
|
|
156
156
|
Indicator for a file that can contain shared variables for distributing resources. This needs to be used together with
|
|
157
157
|
pabotlib option. Resource file syntax is same as Windows ini files. Where a section is a shared set of variables.
|
|
158
158
|
|
|
159
|
-
--argumentfile
|
|
159
|
+
--argumentfile[INTEGER] [FILEPATH]
|
|
160
160
|
Run same suites with multiple [argumentfile](http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#argument-files) options.
|
|
161
161
|
|
|
162
162
|
For example:
|
|
@@ -266,14 +266,18 @@ After this come the suite names.
|
|
|
266
266
|
|
|
267
267
|
With ```--ordering FILENAME``` you can have a list that controls order also. The syntax is same as .pabotsuitenames file syntax but does not contain 4 hash rows that are present in .pabotsuitenames.
|
|
268
268
|
|
|
269
|
+
Note: The `--ordering` file is intended only for defining the execution order of suites and tests. The actual selection of what to run must still be done using options like `--test`, `--suite`, `--include`, or `--exclude`.
|
|
270
|
+
|
|
269
271
|
There different possibilities to influence the execution:
|
|
270
272
|
|
|
271
273
|
* The order of suites can be changed.
|
|
272
274
|
* If a directory (or a directory structure) should be executed sequentially, add the directory suite name to a row as a ```--suite``` option.
|
|
273
275
|
* If the base suite name is changing with robot option [```--name / -N```](https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#setting-the-name) you can also give partial suite name without the base suite.
|
|
274
276
|
* You can add a line with text `#WAIT` to force executor to wait until all previous suites have been executed.
|
|
275
|
-
* You can group suites and tests together to same executor process by adding line `{` before the group and `}`after.
|
|
276
|
-
* You can introduce dependencies using the word `#DEPENDS` after a test declaration.
|
|
277
|
+
* You can group suites and tests together to same executor process by adding line `{` before the group and `}` after. Note that `#WAIT` cannot be used inside a group.
|
|
278
|
+
* You can introduce dependencies using the word `#DEPENDS` after a test declaration. This keyword can be used several times if it is necessary to refer to several different tests. Please take care that in case of circular dependencies an exception will be thrown. Note that each `#WAIT` splits suites into separate execution blocks, and it's not possible to define dependencies for suites or tests that are inside another `#WAIT` block or inside another `{}` brackets.
|
|
279
|
+
* Note: Within a group `{}`, neither execution order nor the `#DEPENDS` keyword currently works. This is due to limitations in Robot Framework, which is invoked within Pabot subprocesses. These limitations may be addressed in a future release of Robot Framework. For now, tests or suites within a group will be executed in the order Robot Framework discovers them — typically in alphabetical order.
|
|
280
|
+
* An example could be:
|
|
277
281
|
|
|
278
282
|
```
|
|
279
283
|
--test robotTest.1 Scalar.Test With Environment Variables #DEPENDS robotTest.1 Scalar.Test with BuiltIn Variables of Robot Framework
|
|
@@ -281,8 +285,10 @@ There different possibilities to influence the execution:
|
|
|
281
285
|
--test robotTest.2 Lists.Test with Keywords and a list
|
|
282
286
|
#WAIT
|
|
283
287
|
--test robotTest.2 Lists.Test with a Keyword that accepts multiple arguments
|
|
288
|
+
{
|
|
284
289
|
--test robotTest.2 Lists.Test with some Collections keywords
|
|
285
290
|
--test robotTest.2 Lists.Test to access list entries
|
|
291
|
+
}
|
|
286
292
|
--test robotTest.3 Dictionary.Test that accesses Dictionaries
|
|
287
293
|
--test robotTest.3 Dictionary.Dictionaries for named arguments #DEPENDS robotTest.3 Dictionary.Test that accesses Dictionaries
|
|
288
294
|
--test robotTest.1 Scalar.Test Case With Variables #DEPENDS robotTest.3 Dictionary.Test that accesses Dictionaries
|
|
@@ -296,12 +302,13 @@ There different possibilities to influence the execution:
|
|
|
296
302
|
* By using the command `#SLEEP X`, where `X` is an integer in the range [0-3600] (in seconds), you can
|
|
297
303
|
define a startup delay for each subprocess. `#SLEEP` affects the next line unless the next line starts a
|
|
298
304
|
group with `{`, in which case the delay applies to the entire group. If the next line begins with `--test`
|
|
299
|
-
or `--suite`, the delay is applied to that specific item. Any other occurrences of `#SLEEP` are ignored.
|
|
305
|
+
or `--suite`, the delay is applied to that specific item. Any other occurrences of `#SLEEP` are ignored.
|
|
306
|
+
Note that `#SLEEP` has no effect within a group, i.e., inside a subprocess.
|
|
300
307
|
|
|
301
308
|
The following example clarifies the behavior:
|
|
302
309
|
|
|
303
310
|
```sh
|
|
304
|
-
pabot --
|
|
311
|
+
pabot --processes 2 --ordering order.txt data_1
|
|
305
312
|
```
|
|
306
313
|
|
|
307
314
|
where order.txt is:
|
|
@@ -130,7 +130,7 @@ Supports all [Robot Framework command line options](https://robotframework.org/r
|
|
|
130
130
|
Indicator for a file that can contain shared variables for distributing resources. This needs to be used together with
|
|
131
131
|
pabotlib option. Resource file syntax is same as Windows ini files. Where a section is a shared set of variables.
|
|
132
132
|
|
|
133
|
-
--argumentfile
|
|
133
|
+
--argumentfile[INTEGER] [FILEPATH]
|
|
134
134
|
Run same suites with multiple [argumentfile](http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#argument-files) options.
|
|
135
135
|
|
|
136
136
|
For example:
|
|
@@ -240,14 +240,18 @@ After this come the suite names.
|
|
|
240
240
|
|
|
241
241
|
With ```--ordering FILENAME``` you can have a list that controls order also. The syntax is same as .pabotsuitenames file syntax but does not contain 4 hash rows that are present in .pabotsuitenames.
|
|
242
242
|
|
|
243
|
+
Note: The `--ordering` file is intended only for defining the execution order of suites and tests. The actual selection of what to run must still be done using options like `--test`, `--suite`, `--include`, or `--exclude`.
|
|
244
|
+
|
|
243
245
|
There different possibilities to influence the execution:
|
|
244
246
|
|
|
245
247
|
* The order of suites can be changed.
|
|
246
248
|
* If a directory (or a directory structure) should be executed sequentially, add the directory suite name to a row as a ```--suite``` option.
|
|
247
249
|
* If the base suite name is changing with robot option [```--name / -N```](https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#setting-the-name) you can also give partial suite name without the base suite.
|
|
248
250
|
* You can add a line with text `#WAIT` to force executor to wait until all previous suites have been executed.
|
|
249
|
-
* You can group suites and tests together to same executor process by adding line `{` before the group and `}`after.
|
|
250
|
-
* You can introduce dependencies using the word `#DEPENDS` after a test declaration.
|
|
251
|
+
* You can group suites and tests together to same executor process by adding line `{` before the group and `}` after. Note that `#WAIT` cannot be used inside a group.
|
|
252
|
+
* You can introduce dependencies using the word `#DEPENDS` after a test declaration. This keyword can be used several times if it is necessary to refer to several different tests. Please take care that in case of circular dependencies an exception will be thrown. Note that each `#WAIT` splits suites into separate execution blocks, and it's not possible to define dependencies for suites or tests that are inside another `#WAIT` block or inside another `{}` brackets.
|
|
253
|
+
* Note: Within a group `{}`, neither execution order nor the `#DEPENDS` keyword currently works. This is due to limitations in Robot Framework, which is invoked within Pabot subprocesses. These limitations may be addressed in a future release of Robot Framework. For now, tests or suites within a group will be executed in the order Robot Framework discovers them — typically in alphabetical order.
|
|
254
|
+
* An example could be:
|
|
251
255
|
|
|
252
256
|
```
|
|
253
257
|
--test robotTest.1 Scalar.Test With Environment Variables #DEPENDS robotTest.1 Scalar.Test with BuiltIn Variables of Robot Framework
|
|
@@ -255,8 +259,10 @@ There different possibilities to influence the execution:
|
|
|
255
259
|
--test robotTest.2 Lists.Test with Keywords and a list
|
|
256
260
|
#WAIT
|
|
257
261
|
--test robotTest.2 Lists.Test with a Keyword that accepts multiple arguments
|
|
262
|
+
{
|
|
258
263
|
--test robotTest.2 Lists.Test with some Collections keywords
|
|
259
264
|
--test robotTest.2 Lists.Test to access list entries
|
|
265
|
+
}
|
|
260
266
|
--test robotTest.3 Dictionary.Test that accesses Dictionaries
|
|
261
267
|
--test robotTest.3 Dictionary.Dictionaries for named arguments #DEPENDS robotTest.3 Dictionary.Test that accesses Dictionaries
|
|
262
268
|
--test robotTest.1 Scalar.Test Case With Variables #DEPENDS robotTest.3 Dictionary.Test that accesses Dictionaries
|
|
@@ -270,12 +276,13 @@ There different possibilities to influence the execution:
|
|
|
270
276
|
* By using the command `#SLEEP X`, where `X` is an integer in the range [0-3600] (in seconds), you can
|
|
271
277
|
define a startup delay for each subprocess. `#SLEEP` affects the next line unless the next line starts a
|
|
272
278
|
group with `{`, in which case the delay applies to the entire group. If the next line begins with `--test`
|
|
273
|
-
or `--suite`, the delay is applied to that specific item. Any other occurrences of `#SLEEP` are ignored.
|
|
279
|
+
or `--suite`, the delay is applied to that specific item. Any other occurrences of `#SLEEP` are ignored.
|
|
280
|
+
Note that `#SLEEP` has no effect within a group, i.e., inside a subprocess.
|
|
274
281
|
|
|
275
282
|
The following example clarifies the behavior:
|
|
276
283
|
|
|
277
284
|
```sh
|
|
278
|
-
pabot --
|
|
285
|
+
pabot --processes 2 --ordering order.txt data_1
|
|
279
286
|
```
|
|
280
287
|
|
|
281
288
|
where order.txt is:
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import atexit
|
|
1
2
|
import multiprocessing
|
|
3
|
+
import os
|
|
4
|
+
import glob
|
|
2
5
|
import re
|
|
6
|
+
import tempfile
|
|
3
7
|
from typing import Dict, List, Optional, Tuple
|
|
4
8
|
|
|
5
9
|
from robot import __version__ as ROBOT_VERSION
|
|
@@ -66,9 +70,72 @@ def parse_args(
|
|
|
66
70
|
options_for_subprocesses["name"] = "Suites"
|
|
67
71
|
opts = _delete_none_keys(options)
|
|
68
72
|
opts_sub = _delete_none_keys(options_for_subprocesses)
|
|
73
|
+
_replace_arg_files(pabot_args, opts_sub)
|
|
69
74
|
return opts, datasources, pabot_args, opts_sub
|
|
70
75
|
|
|
71
76
|
|
|
77
|
+
# remove options from argumentfile according to different scenarios.
|
|
78
|
+
# -t/--test/--task shall be removed if --testlevelsplit options exists
|
|
79
|
+
# -s/--suite shall be removed if --testlevelsplit options does not exist
|
|
80
|
+
def _replace_arg_files(pabot_args, opts_sub):
|
|
81
|
+
_cleanup_old_pabot_temp_files()
|
|
82
|
+
if not opts_sub.get('argumentfile') or not opts_sub['argumentfile']:
|
|
83
|
+
return
|
|
84
|
+
arg_file_list = opts_sub['argumentfile']
|
|
85
|
+
temp_file_list = []
|
|
86
|
+
test_level = pabot_args.get('testlevelsplit')
|
|
87
|
+
|
|
88
|
+
for arg_file_path in arg_file_list:
|
|
89
|
+
with open(arg_file_path, 'r') as arg_file:
|
|
90
|
+
arg_file_lines = arg_file.readlines()
|
|
91
|
+
if not arg_file_lines:
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
fd, temp_path = tempfile.mkstemp(prefix="pabot_temp_", suffix=".txt")
|
|
95
|
+
with os.fdopen(fd, 'wb') as temp_file:
|
|
96
|
+
for line in arg_file_lines:
|
|
97
|
+
if test_level and _is_test_option(line):
|
|
98
|
+
continue
|
|
99
|
+
elif not test_level and _is_suite_option(line):
|
|
100
|
+
continue
|
|
101
|
+
temp_file.write(line.encode('utf-8'))
|
|
102
|
+
|
|
103
|
+
temp_file_list.append(temp_path)
|
|
104
|
+
|
|
105
|
+
opts_sub['argumentfile'] = temp_file_list
|
|
106
|
+
atexit.register(cleanup_temp_file, temp_file_list)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _is_suite_option(line):
|
|
110
|
+
return line.startswith('-s ') or line.startswith('--suite ')
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _is_test_option(line):
|
|
114
|
+
return line.startswith('-t ') or line.startswith('--test ') or line.startswith('--task ')
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# clean the temp argument files before exiting the pabot process
|
|
118
|
+
def cleanup_temp_file(temp_file_list):
|
|
119
|
+
for temp_file in temp_file_list:
|
|
120
|
+
if os.path.exists(temp_file):
|
|
121
|
+
try:
|
|
122
|
+
os.remove(temp_file)
|
|
123
|
+
except Exception:
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# Deletes all possible pabot_temp_ files from os temp directory
|
|
128
|
+
def _cleanup_old_pabot_temp_files():
|
|
129
|
+
temp_dir = tempfile.gettempdir()
|
|
130
|
+
pattern = os.path.join(temp_dir, "pabot_temp_*.txt")
|
|
131
|
+
old_files = glob.glob(pattern)
|
|
132
|
+
for file_path in old_files:
|
|
133
|
+
try:
|
|
134
|
+
os.remove(file_path)
|
|
135
|
+
except Exception:
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
|
|
72
139
|
def _parse_shard(arg):
|
|
73
140
|
# type: (str) -> Tuple[int, int]
|
|
74
141
|
parts = arg.split("/")
|
|
@@ -7,6 +7,40 @@ from robot.utils import PY2, is_unicode
|
|
|
7
7
|
|
|
8
8
|
import re
|
|
9
9
|
|
|
10
|
+
|
|
11
|
+
def create_dependency_tree(items):
|
|
12
|
+
# type: (List[ExecutionItem]) -> List[List[ExecutionItem]]
|
|
13
|
+
independent_tests = list(filter(lambda item: not item.depends, items))
|
|
14
|
+
dependency_tree = [independent_tests]
|
|
15
|
+
dependent_tests = list(filter(lambda item: item.depends, items))
|
|
16
|
+
unknown_dependent_tests = dependent_tests
|
|
17
|
+
while len(unknown_dependent_tests) > 0:
|
|
18
|
+
run_in_this_stage, run_later = [], []
|
|
19
|
+
for d in unknown_dependent_tests:
|
|
20
|
+
stage_indexes = []
|
|
21
|
+
for i, stage in enumerate(dependency_tree):
|
|
22
|
+
for test in stage:
|
|
23
|
+
if test.name in d.depends:
|
|
24
|
+
stage_indexes.append(i)
|
|
25
|
+
# All #DEPENDS test are already run:
|
|
26
|
+
if len(stage_indexes) == len(d.depends):
|
|
27
|
+
run_in_this_stage.append(d)
|
|
28
|
+
else:
|
|
29
|
+
run_later.append(d)
|
|
30
|
+
unknown_dependent_tests = run_later
|
|
31
|
+
if len(run_in_this_stage) == 0:
|
|
32
|
+
text = "There are circular or unmet dependencies using #DEPENDS. Check this/these test(s): " + str(run_later)
|
|
33
|
+
raise DataError(text)
|
|
34
|
+
else:
|
|
35
|
+
dependency_tree.append(run_in_this_stage)
|
|
36
|
+
flattened_dependency_tree = sum(dependency_tree, [])
|
|
37
|
+
if len(flattened_dependency_tree) != len(items):
|
|
38
|
+
raise DataError(
|
|
39
|
+
"Invalid test configuration: Circular or unmet dependencies detected between test suites. Please check your #DEPENDS definitions."
|
|
40
|
+
)
|
|
41
|
+
return dependency_tree
|
|
42
|
+
|
|
43
|
+
|
|
10
44
|
@total_ordering
|
|
11
45
|
class ExecutionItem(object):
|
|
12
46
|
isWait = False
|
|
@@ -78,7 +112,7 @@ class GroupItem(ExecutionItem):
|
|
|
78
112
|
type = "group"
|
|
79
113
|
|
|
80
114
|
def __init__(self):
|
|
81
|
-
self.name = "
|
|
115
|
+
self.name = "Group"
|
|
82
116
|
self._items = []
|
|
83
117
|
self._element_type = None
|
|
84
118
|
|
|
@@ -91,13 +125,26 @@ class GroupItem(ExecutionItem):
|
|
|
91
125
|
)
|
|
92
126
|
if len(self._items) > 0:
|
|
93
127
|
self.name += "_"
|
|
94
|
-
if self.get_sleep() < item.get_sleep():
|
|
128
|
+
if self.get_sleep() < item.get_sleep():
|
|
95
129
|
self.set_sleep(item.get_sleep())
|
|
96
130
|
self.name += item.name
|
|
97
131
|
self._element_type = item.type
|
|
98
132
|
self._items.append(item)
|
|
133
|
+
|
|
134
|
+
def change_items_order_by_depends(self):
|
|
135
|
+
ordered_name = "Group"
|
|
136
|
+
dependency_tree = create_dependency_tree(self._items)
|
|
137
|
+
ordered = [item for sublist in dependency_tree for item in sublist]
|
|
138
|
+
for item in ordered:
|
|
139
|
+
ordered_name += f"_{item.name}"
|
|
140
|
+
self.name = ordered_name
|
|
141
|
+
self._items = ordered
|
|
99
142
|
|
|
100
143
|
def modify_options_for_executor(self, options):
|
|
144
|
+
# Since a GroupItem contains either tests or suites, options are cleared
|
|
145
|
+
# and only the Group's content is used to avoid duplicate execution.
|
|
146
|
+
options['suite'] = []
|
|
147
|
+
options['test'] = []
|
|
101
148
|
for item in self._items:
|
|
102
149
|
if item.type not in options:
|
|
103
150
|
options[item.type] = []
|
|
@@ -131,6 +178,7 @@ class RunnableItem(ExecutionItem):
|
|
|
131
178
|
if len(depends_indexes) == 0
|
|
132
179
|
else line_name[0:depends_indexes[0]].strip()
|
|
133
180
|
)
|
|
181
|
+
assert len(self.name) != 0, f"Suite or test name cannot be empty and then contain #DEPENDS like: {name}"
|
|
134
182
|
self.depends = (
|
|
135
183
|
self._split_dependencies(line_name, depends_indexes)
|
|
136
184
|
if len(depends_indexes) != 0
|
|
@@ -208,6 +256,8 @@ class TestItem(RunnableItem):
|
|
|
208
256
|
def modify_options_for_executor(self, options):
|
|
209
257
|
if "rerunfailed" in options:
|
|
210
258
|
del options["rerunfailed"]
|
|
259
|
+
if "rerunfailedsuites" in options:
|
|
260
|
+
del options["rerunfailedsuites"]
|
|
211
261
|
name = self.name
|
|
212
262
|
for char in ["[", "?", "*"]:
|
|
213
263
|
name = name.replace(char, "[" + char + "]")
|
|
@@ -218,6 +268,8 @@ class TestItem(RunnableItem):
|
|
|
218
268
|
def modify_options_for_executor(self, options):
|
|
219
269
|
if "rerunfailed" in options:
|
|
220
270
|
del options["rerunfailed"]
|
|
271
|
+
if "rerunfailedsuites" in options:
|
|
272
|
+
del options["rerunfailedsuites"]
|
|
221
273
|
|
|
222
274
|
def difference(self, from_items):
|
|
223
275
|
# type: (List[ExecutionItem]) -> List[ExecutionItem]
|
|
@@ -80,6 +80,7 @@ from .execution_items import (
|
|
|
80
80
|
TestItem,
|
|
81
81
|
RunnableItem,
|
|
82
82
|
SleepItem,
|
|
83
|
+
create_dependency_tree,
|
|
83
84
|
)
|
|
84
85
|
from .result_merger import merge
|
|
85
86
|
|
|
@@ -290,7 +291,9 @@ def _create_command_for_execution(caller_id, datasources, is_last, item, outs_di
|
|
|
290
291
|
item.last_level,
|
|
291
292
|
item.processes,
|
|
292
293
|
)
|
|
293
|
-
|
|
294
|
+
# If the datasource ends with a backslash '\', it is deleted to ensure
|
|
295
|
+
# correct handling of the escape character later on.
|
|
296
|
+
+ [os.path.normpath(s) for s in datasources]
|
|
294
297
|
)
|
|
295
298
|
return _mapOptionalQuote(cmd)
|
|
296
299
|
|
|
@@ -761,6 +764,7 @@ def _options_to_cli_arguments(opts): # type: (dict) -> List[str]
|
|
|
761
764
|
|
|
762
765
|
|
|
763
766
|
def _group_by_groups(tokens):
|
|
767
|
+
# type: (List[ExecutionItem]) -> List[ExecutionItem]
|
|
764
768
|
result = []
|
|
765
769
|
group = None
|
|
766
770
|
for token in tokens:
|
|
@@ -778,6 +782,7 @@ def _group_by_groups(tokens):
|
|
|
778
782
|
raise DataError(
|
|
779
783
|
"Ordering: Group end tag '}' encountered before start '{'"
|
|
780
784
|
)
|
|
785
|
+
group.change_items_order_by_depends()
|
|
781
786
|
group = None
|
|
782
787
|
continue
|
|
783
788
|
if group is not None:
|
|
@@ -949,7 +954,7 @@ def solve_suite_names(outs_dir, datasources, options, pabot_args):
|
|
|
949
954
|
)
|
|
950
955
|
execution_item_lines = [parse_execution_item_line(l) for l in lines[4:]]
|
|
951
956
|
if corrupted or h != file_h or file_hash != hash_of_file or pabot_args.get("pabotprerunmodifier"):
|
|
952
|
-
if file_h[0] != h[0] and file_h[2] == h[2]:
|
|
957
|
+
if file_h is not None and file_h[0] != h[0] and file_h[2] == h[2]:
|
|
953
958
|
suite_names = _levelsplit(
|
|
954
959
|
generate_suite_names_with_builder(outs_dir, datasources, options),
|
|
955
960
|
pabot_args,
|
|
@@ -985,7 +990,8 @@ def _levelsplit(
|
|
|
985
990
|
|
|
986
991
|
|
|
987
992
|
def _group_by_wait(lines):
|
|
988
|
-
|
|
993
|
+
# type: (List[ExecutionItem]) -> List[List[ExecutionItem]]
|
|
994
|
+
suites = [[]]
|
|
989
995
|
for suite in lines:
|
|
990
996
|
if not suite.isWait:
|
|
991
997
|
if suite:
|
|
@@ -1088,8 +1094,6 @@ def _fix_items(items): # type: (List[ExecutionItem]) -> List[ExecutionItem]
|
|
|
1088
1094
|
_remove_empty_groups(result)
|
|
1089
1095
|
if result and result[0].isWait:
|
|
1090
1096
|
result = result[1:]
|
|
1091
|
-
if result and result[-1].isWait:
|
|
1092
|
-
result = result[:-1]
|
|
1093
1097
|
return result
|
|
1094
1098
|
|
|
1095
1099
|
|
|
@@ -1334,6 +1338,8 @@ def _options_for_rebot(options, start_time_string, end_time_string):
|
|
|
1334
1338
|
rebot_options["test"] = []
|
|
1335
1339
|
rebot_options["exclude"] = []
|
|
1336
1340
|
rebot_options["include"] = []
|
|
1341
|
+
if rebot_options.get("runemptysuite"):
|
|
1342
|
+
rebot_options["processemptysuite"] = True
|
|
1337
1343
|
if ROBOT_VERSION >= "2.8":
|
|
1338
1344
|
options["monitormarkers"] = "off"
|
|
1339
1345
|
for key in [
|
|
@@ -1358,6 +1364,7 @@ def _options_for_rebot(options, start_time_string, end_time_string):
|
|
|
1358
1364
|
"randomize",
|
|
1359
1365
|
"runemptysuite",
|
|
1360
1366
|
"rerunfailed",
|
|
1367
|
+
"rerunfailedsuites",
|
|
1361
1368
|
"skip",
|
|
1362
1369
|
"skiponfailure",
|
|
1363
1370
|
"skipteardownonexit",
|
|
@@ -1476,6 +1483,19 @@ def _copy_output_artifacts(options, file_extensions=None, include_subfolders=Fal
|
|
|
1476
1483
|
return copied_artifacts
|
|
1477
1484
|
|
|
1478
1485
|
|
|
1486
|
+
def _check_pabot_results_for_missing_xml(base_dir):
|
|
1487
|
+
missing = []
|
|
1488
|
+
for root, dirs, _ in os.walk(base_dir):
|
|
1489
|
+
if root == base_dir:
|
|
1490
|
+
for subdir in dirs:
|
|
1491
|
+
subdir_path = os.path.join(base_dir, subdir)
|
|
1492
|
+
has_xml = any(fname.endswith('.xml') for fname in os.listdir(subdir_path))
|
|
1493
|
+
if not has_xml:
|
|
1494
|
+
missing.append(os.path.join(subdir_path, 'robot_stderr.out'))
|
|
1495
|
+
break
|
|
1496
|
+
return missing
|
|
1497
|
+
|
|
1498
|
+
|
|
1479
1499
|
def _report_results(outs_dir, pabot_args, options, start_time_string, tests_root_name):
|
|
1480
1500
|
if "pythonpath" in options:
|
|
1481
1501
|
del options["pythonpath"]
|
|
@@ -1491,6 +1511,7 @@ def _report_results(outs_dir, pabot_args, options, start_time_string, tests_root
|
|
|
1491
1511
|
"failed": 0,
|
|
1492
1512
|
"skipped": 0,
|
|
1493
1513
|
}
|
|
1514
|
+
missing_outputs = []
|
|
1494
1515
|
if pabot_args["argumentfiles"]:
|
|
1495
1516
|
outputs = [] # type: List[str]
|
|
1496
1517
|
for index, _ in pabot_args["argumentfiles"]:
|
|
@@ -1507,14 +1528,27 @@ def _report_results(outs_dir, pabot_args, options, start_time_string, tests_root
|
|
|
1507
1528
|
outputfile=os.path.join("pabot_results", "output%s.xml" % index),
|
|
1508
1529
|
)
|
|
1509
1530
|
]
|
|
1531
|
+
missing_outputs.extend(_check_pabot_results_for_missing_xml(os.path.join(outs_dir, index)))
|
|
1510
1532
|
if "output" not in options:
|
|
1511
1533
|
options["output"] = "output.xml"
|
|
1512
1534
|
_write_stats(stats)
|
|
1513
|
-
|
|
1535
|
+
exit_code = rebot(*outputs, **_options_for_rebot(options, start_time_string, _now()))
|
|
1514
1536
|
else:
|
|
1515
|
-
|
|
1537
|
+
exit_code = _report_results_for_one_run(
|
|
1516
1538
|
outs_dir, pabot_args, options, start_time_string, tests_root_name, stats
|
|
1517
1539
|
)
|
|
1540
|
+
missing_outputs.extend(_check_pabot_results_for_missing_xml(outs_dir))
|
|
1541
|
+
if missing_outputs:
|
|
1542
|
+
_write(("[ " + _wrap_with(Color.YELLOW, 'WARNING') + " ] "
|
|
1543
|
+
"One or more subprocesses encountered an error and the "
|
|
1544
|
+
"internal .xml files could not be generated. Please check the "
|
|
1545
|
+
"following stderr files to identify the cause:"))
|
|
1546
|
+
for missing in missing_outputs:
|
|
1547
|
+
_write(repr(missing))
|
|
1548
|
+
_write((f"[ " + _wrap_with(Color.RED, 'ERROR') + " ] "
|
|
1549
|
+
"The output, log and report files produced by Pabot are "
|
|
1550
|
+
"incomplete and do not contain all test cases."))
|
|
1551
|
+
return exit_code if not missing_outputs else 252
|
|
1518
1552
|
|
|
1519
1553
|
|
|
1520
1554
|
def _write_stats(stats):
|
|
@@ -1549,9 +1583,9 @@ def _report_results_for_one_run(
|
|
|
1549
1583
|
_write_stats(stats)
|
|
1550
1584
|
if (
|
|
1551
1585
|
"report" in options
|
|
1552
|
-
and options["report"] == "NONE"
|
|
1586
|
+
and options["report"].upper() == "NONE"
|
|
1553
1587
|
and "log" in options
|
|
1554
|
-
and options["log"] == "NONE"
|
|
1588
|
+
and options["log"].upper() == "NONE"
|
|
1555
1589
|
):
|
|
1556
1590
|
options[
|
|
1557
1591
|
"output"
|
|
@@ -1867,19 +1901,28 @@ def _chunk_items(items, chunk_size):
|
|
|
1867
1901
|
base_item = chunked_items[0]
|
|
1868
1902
|
if not base_item:
|
|
1869
1903
|
continue
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
execution_items
|
|
1876
|
-
base_item
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1904
|
+
if isinstance(base_item.execution_item, TestItem):
|
|
1905
|
+
for item in chunked_items:
|
|
1906
|
+
chunked_item = _queue_item(base_item, item.execution_item)
|
|
1907
|
+
yield chunked_item
|
|
1908
|
+
else:
|
|
1909
|
+
execution_items = SuiteItems([item.execution_item for item in chunked_items])
|
|
1910
|
+
chunked_item = _queue_item(base_item, execution_items)
|
|
1911
|
+
yield chunked_item
|
|
1912
|
+
|
|
1913
|
+
|
|
1914
|
+
def _queue_item(base_item, execution_items):
|
|
1915
|
+
return QueueItem(
|
|
1916
|
+
base_item.datasources,
|
|
1917
|
+
base_item.outs_dir,
|
|
1918
|
+
base_item.options,
|
|
1919
|
+
execution_items,
|
|
1920
|
+
base_item.command,
|
|
1921
|
+
base_item.verbose,
|
|
1922
|
+
(base_item.argfile_index, base_item.argfile),
|
|
1923
|
+
processes=base_item.processes,
|
|
1924
|
+
timeout=base_item.timeout,
|
|
1925
|
+
)
|
|
1883
1926
|
|
|
1884
1927
|
|
|
1885
1928
|
def _find_ending_level(name, group):
|
|
@@ -1991,8 +2034,8 @@ def main_program(args):
|
|
|
1991
2034
|
options, datasources, pabot_args, opts_for_run = parse_args(args)
|
|
1992
2035
|
if pabot_args["help"]:
|
|
1993
2036
|
help_print = __doc__.replace(
|
|
1994
|
-
|
|
1995
|
-
|
|
2037
|
+
"PLACEHOLDER_README.MD",
|
|
2038
|
+
read_args_from_readme()
|
|
1996
2039
|
)
|
|
1997
2040
|
print(help_print.replace("[PABOT_VERSION]", PABOT_VERSION))
|
|
1998
2041
|
return 0
|
|
@@ -2028,7 +2071,7 @@ def main_program(args):
|
|
|
2028
2071
|
_write((
|
|
2029
2072
|
"All tests were executed, but the --no-rebot argument was given, "
|
|
2030
2073
|
"so the results were not compiled, and no summary was generated. "
|
|
2031
|
-
f"All results have been saved in the {
|
|
2074
|
+
f"All results have been saved in the {outs_dir} folder."
|
|
2032
2075
|
))
|
|
2033
2076
|
_write("===================================================")
|
|
2034
2077
|
return 0 if not _ABNORMAL_EXIT_HAPPENED else 252
|
|
@@ -2069,20 +2112,40 @@ def _parse_ordering(filename): # type: (str) -> List[ExecutionItem]
|
|
|
2069
2112
|
with open(filename, "r") as orderingfile:
|
|
2070
2113
|
return [
|
|
2071
2114
|
parse_execution_item_line(line.strip())
|
|
2072
|
-
for line in orderingfile.readlines()
|
|
2115
|
+
for line in orderingfile.readlines() if line.strip() != ""
|
|
2073
2116
|
]
|
|
2074
2117
|
except FileNotFoundError:
|
|
2075
2118
|
raise DataError("Error: File '%s' not found." % filename)
|
|
2076
|
-
except ValueError as e:
|
|
2119
|
+
except (ValueError, AssertionError) as e:
|
|
2077
2120
|
raise DataError("Error in ordering file: %s: %s" % (filename, e))
|
|
2078
|
-
except:
|
|
2121
|
+
except Exception:
|
|
2079
2122
|
raise DataError("Error parsing ordering file '%s'" % filename)
|
|
2080
2123
|
|
|
2081
2124
|
|
|
2125
|
+
def _check_ordering(ordering_file, suite_names): # type: (List[ExecutionItem], List[ExecutionItem]) -> None
|
|
2126
|
+
list_of_suite_names = [s.name for s in suite_names]
|
|
2127
|
+
number_of_tests_or_suites = 0
|
|
2128
|
+
if ordering_file:
|
|
2129
|
+
for item in ordering_file:
|
|
2130
|
+
if item.type in ['suite', 'test']:
|
|
2131
|
+
if not any((s == item.name or s.endswith("." + item.name)) for s in list_of_suite_names):
|
|
2132
|
+
# If test name is too long, it gets name ' Invalid', so skip that
|
|
2133
|
+
# Additionally, the test is skipped also if the user wants a higher-level suite to be executed sequentially by using
|
|
2134
|
+
# the --suite option, and the given name is part of the full name of any test or suite.
|
|
2135
|
+
if item.name != ' Invalid' and not (item.type == 'suite' and any((s == item.name or s.startswith(item.name + ".")) for s in list_of_suite_names)):
|
|
2136
|
+
raise DataError("%s item '%s' in --ordering file does not match suite or test names in .pabotsuitenames file.\nPlease verify content of --ordering file." % (item.type.title(), item.name))
|
|
2137
|
+
number_of_tests_or_suites += 1
|
|
2138
|
+
if number_of_tests_or_suites > len(list_of_suite_names):
|
|
2139
|
+
raise DataError('Ordering file contains more tests and/or suites than exists. Check that there is no duplicates etc. in ordering file and that to .pabotsuitenames.')
|
|
2140
|
+
|
|
2141
|
+
|
|
2082
2142
|
def _group_suites(outs_dir, datasources, options, pabot_args):
|
|
2083
2143
|
suite_names = solve_suite_names(outs_dir, datasources, options, pabot_args)
|
|
2084
2144
|
_verify_depends(suite_names)
|
|
2085
2145
|
ordering_arg = _parse_ordering(pabot_args.get("ordering")) if (pabot_args.get("ordering")) is not None else None
|
|
2146
|
+
if ordering_arg:
|
|
2147
|
+
_verify_depends(ordering_arg)
|
|
2148
|
+
_check_ordering(ordering_arg, suite_names)
|
|
2086
2149
|
ordering_arg_with_sleep = _set_sleep_times(ordering_arg)
|
|
2087
2150
|
ordered_suites = _preserve_order(suite_names, ordering_arg_with_sleep)
|
|
2088
2151
|
shard_suites = solve_shard_suites(ordered_suites, pabot_args)
|
|
@@ -2142,7 +2205,7 @@ def _verify_depends(suite_names):
|
|
|
2142
2205
|
suites_with_found_dependencies = list(
|
|
2143
2206
|
filter(
|
|
2144
2207
|
lambda suite: any(
|
|
2145
|
-
runnable_suite.name
|
|
2208
|
+
runnable_suite.name in suite.depends
|
|
2146
2209
|
for runnable_suite in runnable_suites
|
|
2147
2210
|
),
|
|
2148
2211
|
suites_with_depends,
|
|
@@ -2153,63 +2216,31 @@ def _verify_depends(suite_names):
|
|
|
2153
2216
|
"Invalid test configuration: Some test suites have dependencies (#DEPENDS) that cannot be found."
|
|
2154
2217
|
)
|
|
2155
2218
|
suites_with_circular_dependencies = list(
|
|
2156
|
-
filter(lambda suite: suite.
|
|
2219
|
+
filter(lambda suite: suite.name in suite.depends, suites_with_depends)
|
|
2157
2220
|
)
|
|
2158
2221
|
if suites_with_circular_dependencies:
|
|
2159
2222
|
raise DataError(
|
|
2160
2223
|
"Invalid test configuration: Test suites cannot depend on themselves."
|
|
2161
2224
|
)
|
|
2162
|
-
grouped_suites = list(
|
|
2163
|
-
filter(lambda suite: isinstance(suite, GroupItem), suite_names)
|
|
2164
|
-
)
|
|
2165
|
-
if grouped_suites and suites_with_depends:
|
|
2166
|
-
raise DataError(
|
|
2167
|
-
"Invalid test configuration: Cannot use both #DEPENDS and grouped suites."
|
|
2168
|
-
)
|
|
2169
2225
|
|
|
2170
2226
|
|
|
2171
2227
|
def _group_by_depend(suite_names):
|
|
2228
|
+
# type: (List[ExecutionItem]) -> List[List[ExecutionItem]]
|
|
2172
2229
|
group_items = list(filter(lambda suite: isinstance(suite, GroupItem), suite_names))
|
|
2173
2230
|
runnable_suites = list(
|
|
2174
2231
|
filter(lambda suite: isinstance(suite, RunnableItem), suite_names)
|
|
2175
2232
|
)
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
dependency_tree = [independent_tests]
|
|
2180
|
-
dependent_tests = list(filter(lambda suite: suite.depends, runnable_suites))
|
|
2181
|
-
unknown_dependent_tests = dependent_tests
|
|
2182
|
-
while len(unknown_dependent_tests) > 0:
|
|
2183
|
-
run_in_this_stage, run_later = [], []
|
|
2184
|
-
for d in unknown_dependent_tests:
|
|
2185
|
-
stage_indexes = []
|
|
2186
|
-
for i, stage in enumerate(dependency_tree):
|
|
2187
|
-
for test in stage:
|
|
2188
|
-
if test.name in d.depends:
|
|
2189
|
-
stage_indexes.append(i)
|
|
2190
|
-
# All #DEPENDS test are already run:
|
|
2191
|
-
if len(stage_indexes) == len(d.depends):
|
|
2192
|
-
run_in_this_stage.append(d)
|
|
2193
|
-
else:
|
|
2194
|
-
run_later.append(d)
|
|
2195
|
-
unknown_dependent_tests = run_later
|
|
2196
|
-
if len(run_in_this_stage) == 0:
|
|
2197
|
-
text = "There are circular or unmet dependencies using #DEPENDS. Check this/these test(s): " + str(run_later)
|
|
2198
|
-
raise DataError(text)
|
|
2199
|
-
else:
|
|
2200
|
-
dependency_tree.append(run_in_this_stage)
|
|
2201
|
-
flattened_dependency_tree = sum(dependency_tree, [])
|
|
2202
|
-
if len(flattened_dependency_tree) != len(runnable_suites):
|
|
2203
|
-
raise DataError(
|
|
2204
|
-
"Invalid test configuration: Circular or unmet dependencies detected between test suites. Please check your #DEPENDS definitions."
|
|
2205
|
-
)
|
|
2233
|
+
dependency_tree = create_dependency_tree(runnable_suites)
|
|
2234
|
+
# Since groups cannot depend on others, they are placed at the beginning.
|
|
2235
|
+
dependency_tree[0][0:0] = group_items
|
|
2206
2236
|
return dependency_tree
|
|
2207
2237
|
|
|
2208
2238
|
|
|
2209
2239
|
def _all_grouped_suites_by_depend(grouped_suites):
|
|
2240
|
+
# type: (List[List[ExecutionItem]]) -> List[List[ExecutionItem]]
|
|
2210
2241
|
grouped_by_depend = []
|
|
2211
|
-
for group_suite in grouped_suites:
|
|
2212
|
-
grouped_by_depend
|
|
2242
|
+
for group_suite in grouped_suites: # These groups are divided by #WAIT
|
|
2243
|
+
grouped_by_depend.extend(_group_by_depend(group_suite))
|
|
2213
2244
|
return grouped_by_depend
|
|
2214
2245
|
|
|
2215
2246
|
|