robotframework-pabot 4.1.1__tar.gz → 4.2.0__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.
Files changed (47) hide show
  1. {robotframework_pabot-4.1.1/src/robotframework_pabot.egg-info → robotframework_pabot-4.2.0}/PKG-INFO +60 -9
  2. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/README.md +57 -7
  3. robotframework_pabot-4.2.0/pyproject.toml +3 -0
  4. robotframework_pabot-4.2.0/setup.py +14 -0
  5. robotframework_pabot-4.2.0/src/pabot/__init__.py +10 -0
  6. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/src/pabot/arguments.py +5 -0
  7. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/src/pabot/execution_items.py +28 -0
  8. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/src/pabot/pabot.py +59 -3
  9. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0/src/robotframework_pabot.egg-info}/PKG-INFO +60 -9
  10. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/src/robotframework_pabot.egg-info/SOURCES.txt +1 -0
  11. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/tests/test_ordering.py +70 -0
  12. robotframework_pabot-4.2.0/tests/test_suite_structure.py +232 -0
  13. robotframework_pabot-4.1.1/pyproject.toml +0 -3
  14. robotframework_pabot-4.1.1/setup.py +0 -5
  15. robotframework_pabot-4.1.1/src/pabot/__init__.py +0 -5
  16. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/LICENSE.txt +0 -0
  17. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/MANIFEST.in +0 -0
  18. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/setup.cfg +0 -0
  19. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/src/pabot/SharedLibrary.py +0 -0
  20. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/src/pabot/clientwrapper.py +0 -0
  21. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/src/pabot/coordinatorwrapper.py +0 -0
  22. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/src/pabot/pabotlib.py +0 -0
  23. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/src/pabot/py3/__init__.py +0 -0
  24. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/src/pabot/py3/client.py +0 -0
  25. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/src/pabot/py3/coordinator.py +0 -0
  26. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/src/pabot/py3/messages.py +0 -0
  27. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/src/pabot/py3/worker.py +0 -0
  28. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/src/pabot/result_merger.py +0 -0
  29. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/src/pabot/robotremoteserver.py +0 -0
  30. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/src/pabot/workerwrapper.py +0 -0
  31. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/src/robotframework_pabot.egg-info/dependency_links.txt +0 -0
  32. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/src/robotframework_pabot.egg-info/entry_points.txt +0 -0
  33. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/src/robotframework_pabot.egg-info/requires.txt +0 -0
  34. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/src/robotframework_pabot.egg-info/top_level.txt +0 -0
  35. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/tests/test_arguments_output.py +0 -0
  36. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/tests/test_basic_arguments.py +0 -0
  37. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/tests/test_depends.py +0 -0
  38. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/tests/test_functional.py +0 -0
  39. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/tests/test_pabot.py +0 -0
  40. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/tests/test_pabotlib.py +0 -0
  41. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/tests/test_pabotprerunmodifier.py +0 -0
  42. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/tests/test_pabotsuitenames_io.py +0 -0
  43. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/tests/test_prerunmodifier.py +0 -0
  44. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/tests/test_resultmerger.py +0 -0
  45. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/tests/test_stacktrace.py +0 -0
  46. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/tests/test_testlevelsplit_include.py +0 -0
  47. {robotframework_pabot-4.1.1 → robotframework_pabot-4.2.0}/tests/test_testlevelsplit_output_task_order.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: robotframework-pabot
3
- Version: 4.1.1
3
+ Version: 4.2.0
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
@@ -22,6 +22,7 @@ Requires-Dist: robotframework>=3.2
22
22
  Requires-Dist: robotframework-stacktrace>=0.4.1
23
23
  Requires-Dist: natsort>=8.2.0
24
24
  Dynamic: download-url
25
+ Dynamic: license-file
25
26
 
26
27
  # Pabot
27
28
 
@@ -88,9 +89,10 @@ pabot [--verbose|--testlevelsplit|--command .. --end-command|
88
89
  --processtimeout num|
89
90
  --shard i/n|
90
91
  --artifacts extensions|--artifactsinsubfolders|
91
- --resourcefile file|--argumentfile[num] file|--suitesfrom file|--ordering file
92
- --chunk
93
- --pabotprerunmodifier modifier
92
+ --resourcefile file|--argumentfile[num] file|--suitesfrom file|--ordering file|
93
+ --chunk|
94
+ --pabotprerunmodifier modifier|
95
+ --no-rebot|
94
96
  --help|--version]
95
97
  [robot options] [path ...]
96
98
 
@@ -178,11 +180,16 @@ Supports all [Robot Framework command line options](https://robotframework.org/r
178
180
  pabot subprocesses. Depending on the intended use, this may be desirable as well as more efficient. Can be used, for
179
181
  example, to modify the list of tests to be performed.
180
182
 
181
- --help
182
- Print usage instructions.
183
+ --no-rebot
184
+ If specified, the tests will execute as usual, but Rebot will not be called to merge the logs. This option is designed
185
+ for scenarios where Rebot should be run later due to large log files, ensuring better memory and resource availability.
186
+ Subprocess results are stored in the pabot_results folder.
187
+
188
+ --help
189
+ Print usage instructions.
183
190
 
184
- --version
185
- Print version information.
191
+ --version
192
+ Print version information.
186
193
 
187
194
  Example usages:
188
195
 
@@ -284,6 +291,50 @@ There different possibilities to influence the execution:
284
291
  --test robotTest.1 Scalar.Test With Arguments and Return Values
285
292
  --test robotTest.3 Dictionary.Test with Dictionaries as Arguments
286
293
  --test robotTest.3 Dictionary.Test with FOR loops and Dictionaries #DEPENDS robotTest.1 Scalar.Test Case with Return Values
294
+ ```
295
+
296
+ * By using the command `#SLEEP X`, where `X` is an integer in the range [0-3600] (in seconds), you can
297
+ define a startup delay for each subprocess. `#SLEEP` affects the next line unless the next line starts a
298
+ 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.
300
+
301
+ The following example clarifies the behavior:
302
+
303
+ ```sh
304
+ pabot --process 2 --ordering order.txt data_1
305
+ ```
306
+
307
+ where order.txt is:
308
+
309
+ ```
310
+ #SLEEP 1
311
+ {
312
+ #SLEEP 2
313
+ --suite Data 1.suite A
314
+ #SLEEP 3
315
+ --suite Data 1.suite B
316
+ #SLEEP 4
317
+ }
318
+ #SLEEP 5
319
+ #SLEEP 6
320
+ --suite Data 1.suite C
321
+ #SLEEP 7
322
+ --suite Data 1.suite D
323
+ #SLEEP 8
324
+ ```
325
+
326
+ prints something like this:
327
+
328
+ ```
329
+ 2025-02-15 19:15:00.408321 [0] [ID:1] SLEEPING 6 SECONDS BEFORE STARTING Data 1.suite C
330
+ 2025-02-15 19:15:00.408321 [1] [ID:0] SLEEPING 1 SECONDS BEFORE STARTING Group_Data 1.suite A_Data 1.suite B
331
+ 2025-02-15 19:15:01.409389 [PID:52008] [1] [ID:0] EXECUTING Group_Data 1.suite A_Data 1.suite B
332
+ 2025-02-15 19:15:06.409024 [PID:1528] [0] [ID:1] EXECUTING Data 1.suite C
333
+ 2025-02-15 19:15:09.257564 [PID:52008] [1] [ID:0] PASSED Group_Data 1.suite A_Data 1.suite B in 7.8 seconds
334
+ 2025-02-15 19:15:09.259067 [1] [ID:2] SLEEPING 7 SECONDS BEFORE STARTING Data 1.suite D
335
+ 2025-02-15 19:15:09.647342 [PID:1528] [0] [ID:1] PASSED Data 1.suite C in 3.2 seconds
336
+ 2025-02-15 19:15:16.260432 [PID:48156] [1] [ID:2] EXECUTING Data 1.suite D
337
+ 2025-02-15 19:15:18.696420 [PID:48156] [1] [ID:2] PASSED Data 1.suite D in 2.4 seconds
287
338
  ```
288
339
 
289
340
  ### Programmatic use
@@ -63,9 +63,10 @@ pabot [--verbose|--testlevelsplit|--command .. --end-command|
63
63
  --processtimeout num|
64
64
  --shard i/n|
65
65
  --artifacts extensions|--artifactsinsubfolders|
66
- --resourcefile file|--argumentfile[num] file|--suitesfrom file|--ordering file
67
- --chunk
68
- --pabotprerunmodifier modifier
66
+ --resourcefile file|--argumentfile[num] file|--suitesfrom file|--ordering file|
67
+ --chunk|
68
+ --pabotprerunmodifier modifier|
69
+ --no-rebot|
69
70
  --help|--version]
70
71
  [robot options] [path ...]
71
72
 
@@ -153,11 +154,16 @@ Supports all [Robot Framework command line options](https://robotframework.org/r
153
154
  pabot subprocesses. Depending on the intended use, this may be desirable as well as more efficient. Can be used, for
154
155
  example, to modify the list of tests to be performed.
155
156
 
156
- --help
157
- Print usage instructions.
157
+ --no-rebot
158
+ If specified, the tests will execute as usual, but Rebot will not be called to merge the logs. This option is designed
159
+ for scenarios where Rebot should be run later due to large log files, ensuring better memory and resource availability.
160
+ Subprocess results are stored in the pabot_results folder.
161
+
162
+ --help
163
+ Print usage instructions.
158
164
 
159
- --version
160
- Print version information.
165
+ --version
166
+ Print version information.
161
167
 
162
168
  Example usages:
163
169
 
@@ -259,6 +265,50 @@ There different possibilities to influence the execution:
259
265
  --test robotTest.1 Scalar.Test With Arguments and Return Values
260
266
  --test robotTest.3 Dictionary.Test with Dictionaries as Arguments
261
267
  --test robotTest.3 Dictionary.Test with FOR loops and Dictionaries #DEPENDS robotTest.1 Scalar.Test Case with Return Values
268
+ ```
269
+
270
+ * By using the command `#SLEEP X`, where `X` is an integer in the range [0-3600] (in seconds), you can
271
+ define a startup delay for each subprocess. `#SLEEP` affects the next line unless the next line starts a
272
+ 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.
274
+
275
+ The following example clarifies the behavior:
276
+
277
+ ```sh
278
+ pabot --process 2 --ordering order.txt data_1
279
+ ```
280
+
281
+ where order.txt is:
282
+
283
+ ```
284
+ #SLEEP 1
285
+ {
286
+ #SLEEP 2
287
+ --suite Data 1.suite A
288
+ #SLEEP 3
289
+ --suite Data 1.suite B
290
+ #SLEEP 4
291
+ }
292
+ #SLEEP 5
293
+ #SLEEP 6
294
+ --suite Data 1.suite C
295
+ #SLEEP 7
296
+ --suite Data 1.suite D
297
+ #SLEEP 8
298
+ ```
299
+
300
+ prints something like this:
301
+
302
+ ```
303
+ 2025-02-15 19:15:00.408321 [0] [ID:1] SLEEPING 6 SECONDS BEFORE STARTING Data 1.suite C
304
+ 2025-02-15 19:15:00.408321 [1] [ID:0] SLEEPING 1 SECONDS BEFORE STARTING Group_Data 1.suite A_Data 1.suite B
305
+ 2025-02-15 19:15:01.409389 [PID:52008] [1] [ID:0] EXECUTING Group_Data 1.suite A_Data 1.suite B
306
+ 2025-02-15 19:15:06.409024 [PID:1528] [0] [ID:1] EXECUTING Data 1.suite C
307
+ 2025-02-15 19:15:09.257564 [PID:52008] [1] [ID:0] PASSED Group_Data 1.suite A_Data 1.suite B in 7.8 seconds
308
+ 2025-02-15 19:15:09.259067 [1] [ID:2] SLEEPING 7 SECONDS BEFORE STARTING Data 1.suite D
309
+ 2025-02-15 19:15:09.647342 [PID:1528] [0] [ID:1] PASSED Data 1.suite C in 3.2 seconds
310
+ 2025-02-15 19:15:16.260432 [PID:48156] [1] [ID:2] EXECUTING Data 1.suite D
311
+ 2025-02-15 19:15:18.696420 [PID:48156] [1] [ID:2] PASSED Data 1.suite D in 2.4 seconds
262
312
  ```
263
313
 
264
314
  ### Programmatic use
@@ -0,0 +1,3 @@
1
+ [build-system]
2
+ requires = ["setuptools>=42", "wheel"]
3
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env python
2
+
3
+ from setuptools import setup
4
+ import os
5
+ import sys
6
+
7
+ # Add src to path so that version can be imported
8
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
9
+ from pabot import __version__
10
+
11
+ setup(
12
+ name="robotframework-pabot",
13
+ version=__version__,
14
+ )
@@ -0,0 +1,10 @@
1
+ from __future__ import absolute_import
2
+
3
+ # Avoid import errors during setup/build
4
+ try:
5
+ from .pabotlib import PabotLib
6
+ __all__ = ["PabotLib"]
7
+ except ImportError:
8
+ pass
9
+
10
+ __version__ = "4.2.0"
@@ -16,6 +16,7 @@ from .execution_items import (
16
16
  SuiteItem,
17
17
  TestItem,
18
18
  WaitItem,
19
+ SleepItem,
19
20
  )
20
21
 
21
22
  ARGSMATCHER = re.compile(r"--argumentfile(\d+)")
@@ -91,6 +92,7 @@ def _parse_pabot_args(args): # type: (List[str]) -> Tuple[List[str], Dict[str,
91
92
  "shardindex": 0,
92
93
  "shardcount": 1,
93
94
  "chunk": False,
95
+ "no-rebot": False,
94
96
  }
95
97
  # Explicitly define argument types for validation
96
98
  flag_args = {
@@ -100,6 +102,7 @@ def _parse_pabot_args(args): # type: (List[str]) -> Tuple[List[str], Dict[str,
100
102
  "pabotlib",
101
103
  "artifactsinsubfolders",
102
104
  "chunk",
105
+ "no-rebot"
103
106
  }
104
107
  value_args = {
105
108
  "hive": str,
@@ -217,6 +220,8 @@ def parse_execution_item_line(text): # type: (str) -> ExecutionItem
217
220
  if text.startswith("DYNAMICTEST"):
218
221
  suite, test = text[12:].split(" :: ")
219
222
  return DynamicTestItem(test, suite)
223
+ if text.startswith("#SLEEP "):
224
+ return SleepItem(text[7:])
220
225
  if text == "#WAIT":
221
226
  return WaitItem()
222
227
  if text == "{":
@@ -12,6 +12,7 @@ class ExecutionItem(object):
12
12
  isWait = False
13
13
  type = None # type: str
14
14
  name = None # type: str
15
+ sleep = 0 # type: int
15
16
 
16
17
  def top_name(self):
17
18
  # type: () -> str
@@ -29,6 +30,14 @@ class ExecutionItem(object):
29
30
  # type: () -> str
30
31
  return ""
31
32
 
33
+ def set_sleep(self, sleep_time):
34
+ # type: (int) -> None
35
+ self.sleep = sleep_time
36
+
37
+ def get_sleep(self):
38
+ # type: () -> int
39
+ return self.sleep
40
+
32
41
  def modify_options_for_executor(self, options):
33
42
  options[self.type] = self.name
34
43
 
@@ -82,6 +91,8 @@ class GroupItem(ExecutionItem):
82
91
  )
83
92
  if len(self._items) > 0:
84
93
  self.name += "_"
94
+ if self.get_sleep() < item.get_sleep(): # TODO: check!
95
+ self.set_sleep(item.get_sleep())
85
96
  self.name += item.name
86
97
  self._element_type = item.type
87
98
  self._items.append(item)
@@ -274,6 +285,23 @@ class WaitItem(ExecutionItem):
274
285
  return self.name
275
286
 
276
287
 
288
+ class SleepItem(ExecutionItem):
289
+ type = "sleep"
290
+
291
+ def __init__(self, time):
292
+ try:
293
+ assert 3600 >= int(time) >= 0 # 1 h max.
294
+ self.name = time
295
+ self.sleep = int(time)
296
+ except ValueError:
297
+ raise ValueError("#SLEEP value %s is not integer" % time)
298
+ except AssertionError:
299
+ raise ValueError("#SLEEP value %s is not in between 0 and 3600" % time)
300
+
301
+ def line(self):
302
+ return "#SLEEP " + self.name
303
+
304
+
277
305
  class GroupStartItem(ExecutionItem):
278
306
  type = "group"
279
307
 
@@ -41,6 +41,7 @@ import threading
41
41
  import time
42
42
  import traceback
43
43
  import uuid
44
+ import copy
44
45
  from collections import namedtuple
45
46
  from contextlib import closing
46
47
  from glob import glob
@@ -78,6 +79,7 @@ from .execution_items import (
78
79
  SuiteItems,
79
80
  TestItem,
80
81
  RunnableItem,
82
+ SleepItem,
81
83
  )
82
84
  from .result_merger import merge
83
85
 
@@ -262,6 +264,7 @@ def execute_and_wait_with(item):
262
264
  item.index,
263
265
  item.execution_item.type != "test",
264
266
  process_timeout=item.timeout,
267
+ sleep_before_start=item.sleep_before_start
265
268
  )
266
269
  outputxml_preprocessing(
267
270
  item.options, outs_dir, name, item.verbose, _make_id(), caller_id
@@ -320,8 +323,9 @@ def _try_execute_and_wait(
320
323
  my_index=-1,
321
324
  show_stdout_on_failure=False,
322
325
  process_timeout=None,
326
+ sleep_before_start=0
323
327
  ):
324
- # type: (List[str], str, str, bool, int, str, int, bool, Optional[int]) -> None
328
+ # type: (List[str], str, str, bool, int, str, int, bool, Optional[int], int) -> None
325
329
  plib = None
326
330
  is_ignored = False
327
331
  if _pabotlib_in_use():
@@ -339,6 +343,7 @@ def _try_execute_and_wait(
339
343
  my_index,
340
344
  outs_dir,
341
345
  process_timeout,
346
+ sleep_before_start
342
347
  )
343
348
  except:
344
349
  _write(traceback.format_exc())
@@ -521,8 +526,16 @@ def _run(
521
526
  item_index,
522
527
  outs_dir,
523
528
  process_timeout,
529
+ sleep_before_start,
524
530
  ):
525
- # type: (List[str], IO[Any], IO[Any], str, bool, int, int, str, Optional[int]) -> Tuple[Union[subprocess.Popen[bytes], subprocess.Popen], Tuple[int, float]]
531
+ # type: (List[str], IO[Any], IO[Any], str, bool, int, int, str, Optional[int], int) -> Tuple[Union[subprocess.Popen[bytes], subprocess.Popen], Tuple[int, float]]
532
+ timestamp = datetime.datetime.now()
533
+ if sleep_before_start > 0:
534
+ _write(
535
+ "%s [%s] [ID:%s] SLEEPING %s SECONDS BEFORE STARTING %s"
536
+ % (timestamp, pool_id, item_index, sleep_before_start, item_name),
537
+ )
538
+ time.sleep(sleep_before_start)
526
539
  timestamp = datetime.datetime.now()
527
540
  cmd = " ".join(command)
528
541
  if PY2:
@@ -757,6 +770,7 @@ def _group_by_groups(tokens):
757
770
  "Ordering: Group can not contain a group. Encoutered '{'"
758
771
  )
759
772
  group = GroupItem()
773
+ group.set_sleep(token.get_sleep())
760
774
  result.append(group)
761
775
  continue
762
776
  if isinstance(token, GroupEndItem):
@@ -935,6 +949,13 @@ def solve_suite_names(outs_dir, datasources, options, pabot_args):
935
949
  )
936
950
  execution_item_lines = [parse_execution_item_line(l) for l in lines[4:]]
937
951
  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]:
953
+ suite_names = _levelsplit(
954
+ generate_suite_names_with_builder(outs_dir, datasources, options),
955
+ pabot_args,
956
+ )
957
+ store_suite_names(h, suite_names)
958
+ return suite_names
938
959
  return _regenerate(
939
960
  file_h,
940
961
  h,
@@ -1735,6 +1756,7 @@ class QueueItem(object):
1735
1756
  self.hive = hive
1736
1757
  self.processes = processes
1737
1758
  self.timeout = timeout
1759
+ self.sleep_before_start = execution_item.get_sleep()
1738
1760
 
1739
1761
  @property
1740
1762
  def index(self):
@@ -2002,6 +2024,14 @@ def main_program(args):
2002
2024
  opts_for_run,
2003
2025
  pabot_args,
2004
2026
  )
2027
+ if pabot_args["no-rebot"]:
2028
+ _write((
2029
+ "All tests were executed, but the --no-rebot argument was given, "
2030
+ "so the results were not compiled, and no summary was generated. "
2031
+ f"All results have been saved in the {os.path.join(os.path.curdir, 'pabot_results')} folder."
2032
+ ))
2033
+ _write("===================================================")
2034
+ return 0 if not _ABNORMAL_EXIT_HAPPENED else 252
2005
2035
  result_code = _report_results(
2006
2036
  outs_dir,
2007
2037
  pabot_args,
@@ -2043,6 +2073,8 @@ def _parse_ordering(filename): # type: (str) -> List[ExecutionItem]
2043
2073
  ]
2044
2074
  except FileNotFoundError:
2045
2075
  raise DataError("Error: File '%s' not found." % filename)
2076
+ except ValueError as e:
2077
+ raise DataError("Error in ordering file: %s: %s" % (filename, e))
2046
2078
  except:
2047
2079
  raise DataError("Error parsing ordering file '%s'" % filename)
2048
2080
 
@@ -2051,7 +2083,8 @@ def _group_suites(outs_dir, datasources, options, pabot_args):
2051
2083
  suite_names = solve_suite_names(outs_dir, datasources, options, pabot_args)
2052
2084
  _verify_depends(suite_names)
2053
2085
  ordering_arg = _parse_ordering(pabot_args.get("ordering")) if (pabot_args.get("ordering")) is not None else None
2054
- ordered_suites = _preserve_order(suite_names, ordering_arg)
2086
+ ordering_arg_with_sleep = _set_sleep_times(ordering_arg)
2087
+ ordered_suites = _preserve_order(suite_names, ordering_arg_with_sleep)
2055
2088
  shard_suites = solve_shard_suites(ordered_suites, pabot_args)
2056
2089
  grouped_suites = (
2057
2090
  _chunked_suite_names(shard_suites, pabot_args["processes"])
@@ -2062,6 +2095,29 @@ def _group_suites(outs_dir, datasources, options, pabot_args):
2062
2095
  return grouped_by_depend
2063
2096
 
2064
2097
 
2098
+ def _set_sleep_times(ordering_arg):
2099
+ # type: (List[ExecutionItem]) -> List[ExecutionItem]
2100
+ set_sleep_value = 0
2101
+ in_group = False
2102
+ output = copy.deepcopy(ordering_arg)
2103
+ if output is not None:
2104
+ if len(output) >= 2:
2105
+ for i in range(len(output) - 1):
2106
+ if isinstance(output[i], SleepItem):
2107
+ set_sleep_value = output[i].get_sleep()
2108
+ else:
2109
+ set_sleep_value = 0
2110
+ if isinstance(output[i], GroupStartItem):
2111
+ in_group = True
2112
+ if isinstance(output[i], GroupEndItem):
2113
+ in_group = False
2114
+ if isinstance(output[i + 1], GroupStartItem) and set_sleep_value > 0:
2115
+ output[i + 1].set_sleep(set_sleep_value)
2116
+ if isinstance(output[i + 1], RunnableItem) and set_sleep_value > 0 and not in_group:
2117
+ output[i + 1].set_sleep(set_sleep_value)
2118
+ return output
2119
+
2120
+
2065
2121
  def _chunked_suite_names(suite_names, processes):
2066
2122
  q, r = divmod(len(suite_names), processes)
2067
2123
  result = []
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: robotframework-pabot
3
- Version: 4.1.1
3
+ Version: 4.2.0
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
@@ -22,6 +22,7 @@ Requires-Dist: robotframework>=3.2
22
22
  Requires-Dist: robotframework-stacktrace>=0.4.1
23
23
  Requires-Dist: natsort>=8.2.0
24
24
  Dynamic: download-url
25
+ Dynamic: license-file
25
26
 
26
27
  # Pabot
27
28
 
@@ -88,9 +89,10 @@ pabot [--verbose|--testlevelsplit|--command .. --end-command|
88
89
  --processtimeout num|
89
90
  --shard i/n|
90
91
  --artifacts extensions|--artifactsinsubfolders|
91
- --resourcefile file|--argumentfile[num] file|--suitesfrom file|--ordering file
92
- --chunk
93
- --pabotprerunmodifier modifier
92
+ --resourcefile file|--argumentfile[num] file|--suitesfrom file|--ordering file|
93
+ --chunk|
94
+ --pabotprerunmodifier modifier|
95
+ --no-rebot|
94
96
  --help|--version]
95
97
  [robot options] [path ...]
96
98
 
@@ -178,11 +180,16 @@ Supports all [Robot Framework command line options](https://robotframework.org/r
178
180
  pabot subprocesses. Depending on the intended use, this may be desirable as well as more efficient. Can be used, for
179
181
  example, to modify the list of tests to be performed.
180
182
 
181
- --help
182
- Print usage instructions.
183
+ --no-rebot
184
+ If specified, the tests will execute as usual, but Rebot will not be called to merge the logs. This option is designed
185
+ for scenarios where Rebot should be run later due to large log files, ensuring better memory and resource availability.
186
+ Subprocess results are stored in the pabot_results folder.
187
+
188
+ --help
189
+ Print usage instructions.
183
190
 
184
- --version
185
- Print version information.
191
+ --version
192
+ Print version information.
186
193
 
187
194
  Example usages:
188
195
 
@@ -284,6 +291,50 @@ There different possibilities to influence the execution:
284
291
  --test robotTest.1 Scalar.Test With Arguments and Return Values
285
292
  --test robotTest.3 Dictionary.Test with Dictionaries as Arguments
286
293
  --test robotTest.3 Dictionary.Test with FOR loops and Dictionaries #DEPENDS robotTest.1 Scalar.Test Case with Return Values
294
+ ```
295
+
296
+ * By using the command `#SLEEP X`, where `X` is an integer in the range [0-3600] (in seconds), you can
297
+ define a startup delay for each subprocess. `#SLEEP` affects the next line unless the next line starts a
298
+ 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.
300
+
301
+ The following example clarifies the behavior:
302
+
303
+ ```sh
304
+ pabot --process 2 --ordering order.txt data_1
305
+ ```
306
+
307
+ where order.txt is:
308
+
309
+ ```
310
+ #SLEEP 1
311
+ {
312
+ #SLEEP 2
313
+ --suite Data 1.suite A
314
+ #SLEEP 3
315
+ --suite Data 1.suite B
316
+ #SLEEP 4
317
+ }
318
+ #SLEEP 5
319
+ #SLEEP 6
320
+ --suite Data 1.suite C
321
+ #SLEEP 7
322
+ --suite Data 1.suite D
323
+ #SLEEP 8
324
+ ```
325
+
326
+ prints something like this:
327
+
328
+ ```
329
+ 2025-02-15 19:15:00.408321 [0] [ID:1] SLEEPING 6 SECONDS BEFORE STARTING Data 1.suite C
330
+ 2025-02-15 19:15:00.408321 [1] [ID:0] SLEEPING 1 SECONDS BEFORE STARTING Group_Data 1.suite A_Data 1.suite B
331
+ 2025-02-15 19:15:01.409389 [PID:52008] [1] [ID:0] EXECUTING Group_Data 1.suite A_Data 1.suite B
332
+ 2025-02-15 19:15:06.409024 [PID:1528] [0] [ID:1] EXECUTING Data 1.suite C
333
+ 2025-02-15 19:15:09.257564 [PID:52008] [1] [ID:0] PASSED Group_Data 1.suite A_Data 1.suite B in 7.8 seconds
334
+ 2025-02-15 19:15:09.259067 [1] [ID:2] SLEEPING 7 SECONDS BEFORE STARTING Data 1.suite D
335
+ 2025-02-15 19:15:09.647342 [PID:1528] [0] [ID:1] PASSED Data 1.suite C in 3.2 seconds
336
+ 2025-02-15 19:15:16.260432 [PID:48156] [1] [ID:2] EXECUTING Data 1.suite D
337
+ 2025-02-15 19:15:18.696420 [PID:48156] [1] [ID:2] PASSED Data 1.suite D in 2.4 seconds
287
338
  ```
288
339
 
289
340
  ### Programmatic use
@@ -38,5 +38,6 @@ tests/test_pabotsuitenames_io.py
38
38
  tests/test_prerunmodifier.py
39
39
  tests/test_resultmerger.py
40
40
  tests/test_stacktrace.py
41
+ tests/test_suite_structure.py
41
42
  tests/test_testlevelsplit_include.py
42
43
  tests/test_testlevelsplit_output_task_order.py
@@ -192,3 +192,73 @@ class PabotOrderingGroupTest(unittest.TestCase):
192
192
  self.assertIn(b"PASSED", stdout, stderr)
193
193
  self.assertNotIn(b"FAILED", stdout, stderr)
194
194
  self.assertEqual(stdout.count(b"PASSED"), 2)
195
+
196
+
197
+ class PabotOrderingSleepTest(unittest.TestCase):
198
+ def setUp(self):
199
+ self.tmpdir = tempfile.mkdtemp()
200
+
201
+ def tearDown(self):
202
+ shutil.rmtree(self.tmpdir)
203
+
204
+ def _run_tests_with(self, testfile, orderfile):
205
+ robot_file = open("{}/test.robot".format(self.tmpdir), "w")
206
+ robot_file.write(textwrap.dedent(testfile))
207
+ robot_file.close()
208
+ with open("{}/order.dat".format(self.tmpdir), "w") as f:
209
+ f.write(textwrap.dedent(orderfile))
210
+ process = subprocess.Popen(
211
+ [
212
+ sys.executable,
213
+ "-m" "pabot.pabot",
214
+ "--testlevelsplit",
215
+ "--ordering",
216
+ "{}/order.dat".format(self.tmpdir),
217
+ "{}/test.robot".format(self.tmpdir),
218
+ ],
219
+ cwd=self.tmpdir,
220
+ stdout=subprocess.PIPE,
221
+ stderr=subprocess.PIPE,
222
+ )
223
+ return process.communicate()
224
+
225
+ def test_sleep_test_cases_and_group(self):
226
+ stdout, stderr = self._run_tests_with(
227
+ """
228
+ *** Test Cases ***
229
+ Test Case A
230
+ Log Hello!
231
+
232
+ Test Case B
233
+ Log Hello!
234
+
235
+ Test Case C
236
+ Log Hello!
237
+
238
+ Test Case D
239
+ Log Hello!
240
+ """,
241
+ """
242
+ #SLEEP 1
243
+ {
244
+ #SLEEP 4
245
+ --test Test.Test Case A
246
+ #SLEEP 4
247
+ --test Test.Test Case B
248
+ #SLEEP 4
249
+ }
250
+ #SLEEP 4
251
+ #SLEEP 3
252
+ --test Test.Test Case C
253
+ #SLEEP 2
254
+ --test Test.Test Case D
255
+ #SLEEP 4
256
+ """,
257
+ )
258
+ self.assertIn(b"PASSED", stdout, stderr)
259
+ self.assertNotIn(b"FAILED", stdout, stderr)
260
+ self.assertEqual(stdout.count(b"PASSED"), 3)
261
+ self.assertIn(b"SLEEPING 1 SECONDS BEFORE STARTING Group_Test.Test Case A_Test.Test Case B", stdout, stderr)
262
+ self.assertIn(b"SLEEPING 3 SECONDS BEFORE STARTING Test.Test Case C", stdout, stderr)
263
+ self.assertIn(b"SLEEPING 2 SECONDS BEFORE STARTING Test.Test Case D", stdout, stderr)
264
+ self.assertNotIn(b"SLEEPING 4", stdout, stderr)
@@ -0,0 +1,232 @@
1
+ import unittest
2
+ import tempfile
3
+ import textwrap
4
+ import shutil
5
+ import subprocess
6
+ import sys
7
+ import os
8
+ from test_pabotprerunmodifier import get_tmpdir_name
9
+
10
+
11
+ class SuiteStructureTests(unittest.TestCase):
12
+ @classmethod
13
+ def setUpClass(cls):
14
+ cls.tmpdir = tempfile.mkdtemp()
15
+ cls.tmpdir_name = get_tmpdir_name(os.path.basename(cls.tmpdir))
16
+
17
+ # robot case files
18
+ cls.robot_file_path_1 = f'{cls.tmpdir}/dir_1/suite_1.robot'
19
+ cls.robot_file_path_2 = f'{cls.tmpdir}/dir_1/dir_2/suite_2.robot'
20
+ cls.robot_file_path_3 = f'{cls.tmpdir}/dir_1/dir_2/dir_3/suite_3.robot'
21
+ cls.robot_file_path_4 = f'{cls.tmpdir}/dir_1/dir_2/dir_3/dir_4/suite_4.robot'
22
+
23
+ cls.robot_dir_1 = os.path.dirname(cls.robot_file_path_1)
24
+ cls.robot_dir_2 = os.path.dirname(cls.robot_file_path_2)
25
+ cls.robot_dir_3 = os.path.dirname(cls.robot_file_path_3)
26
+ cls.robot_dir_4 = os.path.dirname(cls.robot_file_path_4)
27
+
28
+
29
+ os.makedirs(os.path.dirname(cls.robot_file_path_4), exist_ok=True)
30
+ with open(cls.robot_file_path_1, 'w') as robot_file:
31
+ robot_file.write(
32
+ textwrap.dedent("""
33
+ *** Test Cases ***
34
+ Testing 1 1
35
+ Log hello
36
+ """))
37
+
38
+ with open(cls.robot_file_path_2, 'w') as robot_file:
39
+ robot_file.write(
40
+ textwrap.dedent("""
41
+ *** Test Cases ***
42
+ Testing 2 1
43
+ Log hello
44
+ """))
45
+
46
+ with open(cls.robot_file_path_3, 'w') as robot_file:
47
+ robot_file.write(
48
+ textwrap.dedent("""
49
+ *** Test Cases ***
50
+ Testing 3 1
51
+ Log hello
52
+ """))
53
+
54
+ with open(cls.robot_file_path_4, 'w') as robot_file:
55
+ robot_file.write(
56
+ textwrap.dedent("""
57
+ *** Test Cases ***
58
+ Testing 4 1
59
+ [Tags] tag
60
+ Log hello
61
+ """))
62
+
63
+ @classmethod
64
+ def tearDownClass(cls):
65
+ shutil.rmtree(cls.tmpdir)
66
+
67
+
68
+ def tearDown(self):
69
+ """ Deletes .pabotsuitenames ja output.xml files between tests. """
70
+ for filename in [".pabotsuitenames", "output.xml"]:
71
+ file_path = os.path.join(self.tmpdir, filename)
72
+ if os.path.exists(file_path):
73
+ os.remove(file_path)
74
+
75
+
76
+ def test_run_root_then_suite_4_then_dir_1_then_dir_4(self):
77
+ process_root_1 = subprocess.Popen(
78
+ [
79
+ sys.executable,
80
+ "-m", "pabot.pabot",
81
+ self.tmpdir
82
+ ],
83
+ cwd=self.tmpdir,
84
+ stdout=subprocess.PIPE,
85
+ stderr=subprocess.PIPE
86
+ )
87
+
88
+ stdout, stderr = process_root_1.communicate()
89
+ self.assertIn(b'4 tests, 4 passed, 0 failed, 0 skipped.', stdout)
90
+ self.assertEqual(b"", stderr)
91
+ self.assertIn(f'PASSED {self.tmpdir_name}.Dir 1.Suite 1'.encode('utf-8'), stdout)
92
+ self.assertIn(f'PASSED {self.tmpdir_name}.Dir 1.Dir 2.Suite 2'.encode('utf-8'), stdout)
93
+ self.assertIn(f'PASSED {self.tmpdir_name}.Dir 1.Dir 2.Dir 3.Suite 3'.encode('utf-8'), stdout)
94
+ self.assertIn(f'PASSED {self.tmpdir_name}.Dir 1.Dir 2.Dir 3.Dir 4.Suite 4'.encode('utf-8'), stdout)
95
+
96
+ process_suite_4 = subprocess.Popen(
97
+ [
98
+ sys.executable,
99
+ "-m", "pabot.pabot",
100
+ self.robot_file_path_4
101
+ ],
102
+ cwd=self.tmpdir,
103
+ stdout=subprocess.PIPE,
104
+ stderr=subprocess.PIPE
105
+ )
106
+
107
+ stdout, stderr = process_suite_4.communicate()
108
+ self.assertIn(b'1 tests, 1 passed, 0 failed, 0 skipped.', stdout)
109
+ self.assertEqual(b"", stderr)
110
+ self.assertNotIn(f'Suite 1'.encode('utf-8'), stdout)
111
+ self.assertNotIn(f'Suite 2'.encode('utf-8'), stdout)
112
+ self.assertNotIn(f'Suite 3'.encode('utf-8'), stdout)
113
+ self.assertIn(f'PASSED Suite 4'.encode('utf-8'), stdout)
114
+
115
+ process_dir_1 = subprocess.Popen(
116
+ [
117
+ sys.executable,
118
+ "-m", "pabot.pabot",
119
+ self.robot_dir_1
120
+ ],
121
+ cwd=self.tmpdir,
122
+ stdout=subprocess.PIPE,
123
+ stderr=subprocess.PIPE
124
+ )
125
+
126
+ stdout, stderr = process_dir_1.communicate()
127
+ self.assertIn(b'4 tests, 4 passed, 0 failed, 0 skipped.', stdout)
128
+ self.assertEqual(b"", stderr)
129
+ self.assertIn(f'PASSED Dir 1.Suite 1'.encode('utf-8'), stdout)
130
+ self.assertIn(f'PASSED Dir 1.Dir 2.Suite 2'.encode('utf-8'), stdout)
131
+ self.assertIn(f'PASSED Dir 1.Dir 2.Dir 3.Suite 3'.encode('utf-8'), stdout)
132
+ self.assertIn(f'PASSED Dir 1.Dir 2.Dir 3.Dir 4.Suite 4'.encode('utf-8'), stdout)
133
+
134
+ process_dir_4 = subprocess.Popen(
135
+ [
136
+ sys.executable,
137
+ "-m", "pabot.pabot",
138
+ self.robot_dir_4
139
+ ],
140
+ cwd=self.tmpdir,
141
+ stdout=subprocess.PIPE,
142
+ stderr=subprocess.PIPE
143
+ )
144
+
145
+ stdout, stderr = process_dir_4.communicate()
146
+ self.assertIn(b'1 tests, 1 passed, 0 failed, 0 skipped.', stdout)
147
+ self.assertEqual(b"", stderr)
148
+ self.assertNotIn(f'Suite 1'.encode('utf-8'), stdout)
149
+ self.assertNotIn(f'Suite 2'.encode('utf-8'), stdout)
150
+ self.assertNotIn(f'Suite 3'.encode('utf-8'), stdout)
151
+ self.assertIn(f'PASSED Dir 4.Suite 4'.encode('utf-8'), stdout)
152
+
153
+
154
+
155
+ def test_run_dir_2_then_dir_3(self):
156
+ process_dir_2 = subprocess.Popen(
157
+ [
158
+ sys.executable,
159
+ "-m", "pabot.pabot",
160
+ self.robot_dir_2
161
+ ],
162
+ cwd=self.tmpdir,
163
+ stdout=subprocess.PIPE,
164
+ stderr=subprocess.PIPE
165
+ )
166
+
167
+ stdout, stderr = process_dir_2.communicate()
168
+ self.assertIn(b'3 tests, 3 passed, 0 failed, 0 skipped.', stdout)
169
+ self.assertEqual(b"", stderr)
170
+ self.assertNotIn(f'Suite 1'.encode('utf-8'), stdout)
171
+ self.assertIn(f'PASSED Dir 2.Suite 2'.encode('utf-8'), stdout)
172
+ self.assertIn(f'PASSED Dir 2.Dir 3.Suite 3'.encode('utf-8'), stdout)
173
+ self.assertIn(f'PASSED Dir 2.Dir 3.Dir 4.Suite 4'.encode('utf-8'), stdout)
174
+
175
+ process_dir_3 = subprocess.Popen(
176
+ [
177
+ sys.executable,
178
+ "-m", "pabot.pabot",
179
+ self.robot_dir_3
180
+ ],
181
+ cwd=self.tmpdir,
182
+ stdout=subprocess.PIPE,
183
+ stderr=subprocess.PIPE
184
+ )
185
+
186
+ stdout, stderr = process_dir_3.communicate()
187
+ self.assertIn(b'2 tests, 2 passed, 0 failed, 0 skipped.', stdout)
188
+ self.assertEqual(b"", stderr)
189
+ self.assertNotIn(f'Suite 1'.encode('utf-8'), stdout)
190
+ self.assertNotIn(f'Suite 2'.encode('utf-8'), stdout)
191
+ self.assertIn(f'PASSED Dir 3.Suite 3'.encode('utf-8'), stdout)
192
+ self.assertIn(f'PASSED Dir 3.Dir 4.Suite 4'.encode('utf-8'), stdout)
193
+
194
+
195
+ def test_run_dir_3_then_dir_2(self):
196
+ process_dir_3 = subprocess.Popen(
197
+ [
198
+ sys.executable,
199
+ "-m", "pabot.pabot",
200
+ self.robot_dir_3
201
+ ],
202
+ cwd=self.tmpdir,
203
+ stdout=subprocess.PIPE,
204
+ stderr=subprocess.PIPE
205
+ )
206
+
207
+ stdout, stderr = process_dir_3.communicate()
208
+ self.assertIn(b'2 tests, 2 passed, 0 failed, 0 skipped.', stdout)
209
+ self.assertEqual(b"", stderr)
210
+ self.assertNotIn(f'Suite 1'.encode('utf-8'), stdout)
211
+ self.assertNotIn(f'Suite 2'.encode('utf-8'), stdout)
212
+ self.assertIn(f'PASSED Dir 3.Suite 3'.encode('utf-8'), stdout)
213
+ self.assertIn(f'PASSED Dir 3.Dir 4.Suite 4'.encode('utf-8'), stdout)
214
+
215
+ process_dir_2 = subprocess.Popen(
216
+ [
217
+ sys.executable,
218
+ "-m", "pabot.pabot",
219
+ self.robot_dir_2
220
+ ],
221
+ cwd=self.tmpdir,
222
+ stdout=subprocess.PIPE,
223
+ stderr=subprocess.PIPE
224
+ )
225
+
226
+ stdout, stderr = process_dir_2.communicate()
227
+ self.assertIn(b'3 tests, 3 passed, 0 failed, 0 skipped.', stdout)
228
+ self.assertEqual(b"", stderr)
229
+ self.assertNotIn(f'Suite 1'.encode('utf-8'), stdout)
230
+ self.assertIn(f'PASSED Dir 2.Suite 2'.encode('utf-8'), stdout)
231
+ self.assertIn(f'PASSED Dir 2.Dir 3.Suite 3'.encode('utf-8'), stdout)
232
+ self.assertIn(f'PASSED Dir 2.Dir 3.Dir 4.Suite 4'.encode('utf-8'), stdout)
@@ -1,3 +0,0 @@
1
- [build-system]
2
- requires = ["setuptools"]
3
- build-backend = "setuptools.build_meta"
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env python
2
-
3
- from setuptools import setup
4
-
5
- setup()
@@ -1,5 +0,0 @@
1
- from __future__ import absolute_import
2
-
3
- from .pabotlib import PabotLib
4
-
5
- __version__ = "4.1.1"