robotframework-pabot 4.1.1__py3-none-any.whl → 4.3.0__py3-none-any.whl

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.
pabot/__init__.py CHANGED
@@ -1,5 +1,10 @@
1
1
  from __future__ import absolute_import
2
2
 
3
- from .pabotlib import PabotLib
3
+ # Avoid import errors during setup/build
4
+ try:
5
+ from .pabotlib import PabotLib
6
+ __all__ = ["PabotLib"]
7
+ except ImportError:
8
+ pass
4
9
 
5
- __version__ = "4.1.1"
10
+ __version__ = "4.3.0"
pabot/arguments.py CHANGED
@@ -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
@@ -16,6 +20,7 @@ from .execution_items import (
16
20
  SuiteItem,
17
21
  TestItem,
18
22
  WaitItem,
23
+ SleepItem,
19
24
  )
20
25
 
21
26
  ARGSMATCHER = re.compile(r"--argumentfile(\d+)")
@@ -65,9 +70,72 @@ def parse_args(
65
70
  options_for_subprocesses["name"] = "Suites"
66
71
  opts = _delete_none_keys(options)
67
72
  opts_sub = _delete_none_keys(options_for_subprocesses)
73
+ _replace_arg_files(pabot_args, opts_sub)
68
74
  return opts, datasources, pabot_args, opts_sub
69
75
 
70
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
+
71
139
  def _parse_shard(arg):
72
140
  # type: (str) -> Tuple[int, int]
73
141
  parts = arg.split("/")
@@ -91,6 +159,7 @@ def _parse_pabot_args(args): # type: (List[str]) -> Tuple[List[str], Dict[str,
91
159
  "shardindex": 0,
92
160
  "shardcount": 1,
93
161
  "chunk": False,
162
+ "no-rebot": False,
94
163
  }
95
164
  # Explicitly define argument types for validation
96
165
  flag_args = {
@@ -100,6 +169,7 @@ def _parse_pabot_args(args): # type: (List[str]) -> Tuple[List[str], Dict[str,
100
169
  "pabotlib",
101
170
  "artifactsinsubfolders",
102
171
  "chunk",
172
+ "no-rebot"
103
173
  }
104
174
  value_args = {
105
175
  "hive": str,
@@ -217,6 +287,8 @@ def parse_execution_item_line(text): # type: (str) -> ExecutionItem
217
287
  if text.startswith("DYNAMICTEST"):
218
288
  suite, test = text[12:].split(" :: ")
219
289
  return DynamicTestItem(test, suite)
290
+ if text.startswith("#SLEEP "):
291
+ return SleepItem(text[7:])
220
292
  if text == "#WAIT":
221
293
  return WaitItem()
222
294
  if text == "{":
pabot/execution_items.py CHANGED
@@ -7,11 +7,46 @@ 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
13
47
  type = None # type: str
14
48
  name = None # type: str
49
+ sleep = 0 # type: int
15
50
 
16
51
  def top_name(self):
17
52
  # type: () -> str
@@ -29,6 +64,14 @@ class ExecutionItem(object):
29
64
  # type: () -> str
30
65
  return ""
31
66
 
67
+ def set_sleep(self, sleep_time):
68
+ # type: (int) -> None
69
+ self.sleep = sleep_time
70
+
71
+ def get_sleep(self):
72
+ # type: () -> int
73
+ return self.sleep
74
+
32
75
  def modify_options_for_executor(self, options):
33
76
  options[self.type] = self.name
34
77
 
@@ -69,7 +112,7 @@ class GroupItem(ExecutionItem):
69
112
  type = "group"
70
113
 
71
114
  def __init__(self):
72
- self.name = "Group_"
115
+ self.name = "Group"
73
116
  self._items = []
74
117
  self._element_type = None
75
118
 
@@ -82,11 +125,26 @@ class GroupItem(ExecutionItem):
82
125
  )
83
126
  if len(self._items) > 0:
84
127
  self.name += "_"
128
+ if self.get_sleep() < item.get_sleep():
129
+ self.set_sleep(item.get_sleep())
85
130
  self.name += item.name
86
131
  self._element_type = item.type
87
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
88
142
 
89
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'] = []
90
148
  for item in self._items:
91
149
  if item.type not in options:
92
150
  options[item.type] = []
@@ -120,6 +178,7 @@ class RunnableItem(ExecutionItem):
120
178
  if len(depends_indexes) == 0
121
179
  else line_name[0:depends_indexes[0]].strip()
122
180
  )
181
+ assert len(self.name) != 0, f"Suite or test name cannot be empty and then contain #DEPENDS like: {name}"
123
182
  self.depends = (
124
183
  self._split_dependencies(line_name, depends_indexes)
125
184
  if len(depends_indexes) != 0
@@ -197,6 +256,8 @@ class TestItem(RunnableItem):
197
256
  def modify_options_for_executor(self, options):
198
257
  if "rerunfailed" in options:
199
258
  del options["rerunfailed"]
259
+ if "rerunfailedsuites" in options:
260
+ del options["rerunfailedsuites"]
200
261
  name = self.name
201
262
  for char in ["[", "?", "*"]:
202
263
  name = name.replace(char, "[" + char + "]")
@@ -207,6 +268,8 @@ class TestItem(RunnableItem):
207
268
  def modify_options_for_executor(self, options):
208
269
  if "rerunfailed" in options:
209
270
  del options["rerunfailed"]
271
+ if "rerunfailedsuites" in options:
272
+ del options["rerunfailedsuites"]
210
273
 
211
274
  def difference(self, from_items):
212
275
  # type: (List[ExecutionItem]) -> List[ExecutionItem]
@@ -274,6 +337,23 @@ class WaitItem(ExecutionItem):
274
337
  return self.name
275
338
 
276
339
 
340
+ class SleepItem(ExecutionItem):
341
+ type = "sleep"
342
+
343
+ def __init__(self, time):
344
+ try:
345
+ assert 3600 >= int(time) >= 0 # 1 h max.
346
+ self.name = time
347
+ self.sleep = int(time)
348
+ except ValueError:
349
+ raise ValueError("#SLEEP value %s is not integer" % time)
350
+ except AssertionError:
351
+ raise ValueError("#SLEEP value %s is not in between 0 and 3600" % time)
352
+
353
+ def line(self):
354
+ return "#SLEEP " + self.name
355
+
356
+
277
357
  class GroupStartItem(ExecutionItem):
278
358
  type = "group"
279
359
 
pabot/pabot.py CHANGED
@@ -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,8 @@ from .execution_items import (
78
79
  SuiteItems,
79
80
  TestItem,
80
81
  RunnableItem,
82
+ SleepItem,
83
+ create_dependency_tree,
81
84
  )
82
85
  from .result_merger import merge
83
86
 
@@ -262,6 +265,7 @@ def execute_and_wait_with(item):
262
265
  item.index,
263
266
  item.execution_item.type != "test",
264
267
  process_timeout=item.timeout,
268
+ sleep_before_start=item.sleep_before_start
265
269
  )
266
270
  outputxml_preprocessing(
267
271
  item.options, outs_dir, name, item.verbose, _make_id(), caller_id
@@ -287,7 +291,9 @@ def _create_command_for_execution(caller_id, datasources, is_last, item, outs_di
287
291
  item.last_level,
288
292
  item.processes,
289
293
  )
290
- + datasources
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]
291
297
  )
292
298
  return _mapOptionalQuote(cmd)
293
299
 
@@ -320,8 +326,9 @@ def _try_execute_and_wait(
320
326
  my_index=-1,
321
327
  show_stdout_on_failure=False,
322
328
  process_timeout=None,
329
+ sleep_before_start=0
323
330
  ):
324
- # type: (List[str], str, str, bool, int, str, int, bool, Optional[int]) -> None
331
+ # type: (List[str], str, str, bool, int, str, int, bool, Optional[int], int) -> None
325
332
  plib = None
326
333
  is_ignored = False
327
334
  if _pabotlib_in_use():
@@ -339,6 +346,7 @@ def _try_execute_and_wait(
339
346
  my_index,
340
347
  outs_dir,
341
348
  process_timeout,
349
+ sleep_before_start
342
350
  )
343
351
  except:
344
352
  _write(traceback.format_exc())
@@ -521,8 +529,16 @@ def _run(
521
529
  item_index,
522
530
  outs_dir,
523
531
  process_timeout,
532
+ sleep_before_start,
524
533
  ):
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]]
534
+ # 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]]
535
+ timestamp = datetime.datetime.now()
536
+ if sleep_before_start > 0:
537
+ _write(
538
+ "%s [%s] [ID:%s] SLEEPING %s SECONDS BEFORE STARTING %s"
539
+ % (timestamp, pool_id, item_index, sleep_before_start, item_name),
540
+ )
541
+ time.sleep(sleep_before_start)
526
542
  timestamp = datetime.datetime.now()
527
543
  cmd = " ".join(command)
528
544
  if PY2:
@@ -748,6 +764,7 @@ def _options_to_cli_arguments(opts): # type: (dict) -> List[str]
748
764
 
749
765
 
750
766
  def _group_by_groups(tokens):
767
+ # type: (List[ExecutionItem]) -> List[ExecutionItem]
751
768
  result = []
752
769
  group = None
753
770
  for token in tokens:
@@ -757,6 +774,7 @@ def _group_by_groups(tokens):
757
774
  "Ordering: Group can not contain a group. Encoutered '{'"
758
775
  )
759
776
  group = GroupItem()
777
+ group.set_sleep(token.get_sleep())
760
778
  result.append(group)
761
779
  continue
762
780
  if isinstance(token, GroupEndItem):
@@ -764,6 +782,7 @@ def _group_by_groups(tokens):
764
782
  raise DataError(
765
783
  "Ordering: Group end tag '}' encountered before start '{'"
766
784
  )
785
+ group.change_items_order_by_depends()
767
786
  group = None
768
787
  continue
769
788
  if group is not None:
@@ -935,6 +954,13 @@ def solve_suite_names(outs_dir, datasources, options, pabot_args):
935
954
  )
936
955
  execution_item_lines = [parse_execution_item_line(l) for l in lines[4:]]
937
956
  if corrupted or h != file_h or file_hash != hash_of_file or pabot_args.get("pabotprerunmodifier"):
957
+ if file_h is not None and file_h[0] != h[0] and file_h[2] == h[2]:
958
+ suite_names = _levelsplit(
959
+ generate_suite_names_with_builder(outs_dir, datasources, options),
960
+ pabot_args,
961
+ )
962
+ store_suite_names(h, suite_names)
963
+ return suite_names
938
964
  return _regenerate(
939
965
  file_h,
940
966
  h,
@@ -964,7 +990,8 @@ def _levelsplit(
964
990
 
965
991
 
966
992
  def _group_by_wait(lines):
967
- suites = [[]] # type: List[List[ExecutionItem]]
993
+ # type: (List[ExecutionItem]) -> List[List[ExecutionItem]]
994
+ suites = [[]]
968
995
  for suite in lines:
969
996
  if not suite.isWait:
970
997
  if suite:
@@ -1067,8 +1094,6 @@ def _fix_items(items): # type: (List[ExecutionItem]) -> List[ExecutionItem]
1067
1094
  _remove_empty_groups(result)
1068
1095
  if result and result[0].isWait:
1069
1096
  result = result[1:]
1070
- if result and result[-1].isWait:
1071
- result = result[:-1]
1072
1097
  return result
1073
1098
 
1074
1099
 
@@ -1313,6 +1338,8 @@ def _options_for_rebot(options, start_time_string, end_time_string):
1313
1338
  rebot_options["test"] = []
1314
1339
  rebot_options["exclude"] = []
1315
1340
  rebot_options["include"] = []
1341
+ if rebot_options.get("runemptysuite"):
1342
+ rebot_options["processemptysuite"] = True
1316
1343
  if ROBOT_VERSION >= "2.8":
1317
1344
  options["monitormarkers"] = "off"
1318
1345
  for key in [
@@ -1337,6 +1364,7 @@ def _options_for_rebot(options, start_time_string, end_time_string):
1337
1364
  "randomize",
1338
1365
  "runemptysuite",
1339
1366
  "rerunfailed",
1367
+ "rerunfailedsuites",
1340
1368
  "skip",
1341
1369
  "skiponfailure",
1342
1370
  "skipteardownonexit",
@@ -1455,6 +1483,19 @@ def _copy_output_artifacts(options, file_extensions=None, include_subfolders=Fal
1455
1483
  return copied_artifacts
1456
1484
 
1457
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
+
1458
1499
  def _report_results(outs_dir, pabot_args, options, start_time_string, tests_root_name):
1459
1500
  if "pythonpath" in options:
1460
1501
  del options["pythonpath"]
@@ -1470,6 +1511,7 @@ def _report_results(outs_dir, pabot_args, options, start_time_string, tests_root
1470
1511
  "failed": 0,
1471
1512
  "skipped": 0,
1472
1513
  }
1514
+ missing_outputs = []
1473
1515
  if pabot_args["argumentfiles"]:
1474
1516
  outputs = [] # type: List[str]
1475
1517
  for index, _ in pabot_args["argumentfiles"]:
@@ -1486,14 +1528,27 @@ def _report_results(outs_dir, pabot_args, options, start_time_string, tests_root
1486
1528
  outputfile=os.path.join("pabot_results", "output%s.xml" % index),
1487
1529
  )
1488
1530
  ]
1531
+ missing_outputs.extend(_check_pabot_results_for_missing_xml(os.path.join(outs_dir, index)))
1489
1532
  if "output" not in options:
1490
1533
  options["output"] = "output.xml"
1491
1534
  _write_stats(stats)
1492
- return rebot(*outputs, **_options_for_rebot(options, start_time_string, _now()))
1535
+ exit_code = rebot(*outputs, **_options_for_rebot(options, start_time_string, _now()))
1493
1536
  else:
1494
- return _report_results_for_one_run(
1537
+ exit_code = _report_results_for_one_run(
1495
1538
  outs_dir, pabot_args, options, start_time_string, tests_root_name, stats
1496
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
1497
1552
 
1498
1553
 
1499
1554
  def _write_stats(stats):
@@ -1528,9 +1583,9 @@ def _report_results_for_one_run(
1528
1583
  _write_stats(stats)
1529
1584
  if (
1530
1585
  "report" in options
1531
- and options["report"] == "NONE"
1586
+ and options["report"].upper() == "NONE"
1532
1587
  and "log" in options
1533
- and options["log"] == "NONE"
1588
+ and options["log"].upper() == "NONE"
1534
1589
  ):
1535
1590
  options[
1536
1591
  "output"
@@ -1735,6 +1790,7 @@ class QueueItem(object):
1735
1790
  self.hive = hive
1736
1791
  self.processes = processes
1737
1792
  self.timeout = timeout
1793
+ self.sleep_before_start = execution_item.get_sleep()
1738
1794
 
1739
1795
  @property
1740
1796
  def index(self):
@@ -1845,19 +1901,28 @@ def _chunk_items(items, chunk_size):
1845
1901
  base_item = chunked_items[0]
1846
1902
  if not base_item:
1847
1903
  continue
1848
- execution_items = SuiteItems([item.execution_item for item in chunked_items])
1849
- chunked_item = QueueItem(
1850
- base_item.datasources,
1851
- base_item.outs_dir,
1852
- base_item.options,
1853
- execution_items,
1854
- base_item.command,
1855
- base_item.verbose,
1856
- (base_item.argfile_index, base_item.argfile),
1857
- processes=base_item.processes,
1858
- timeout=base_item.timeout,
1859
- )
1860
- yield chunked_item
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
+ )
1861
1926
 
1862
1927
 
1863
1928
  def _find_ending_level(name, group):
@@ -1969,8 +2034,8 @@ def main_program(args):
1969
2034
  options, datasources, pabot_args, opts_for_run = parse_args(args)
1970
2035
  if pabot_args["help"]:
1971
2036
  help_print = __doc__.replace(
1972
- "PLACEHOLDER_README.MD",
1973
- read_args_from_readme()
2037
+ "PLACEHOLDER_README.MD",
2038
+ read_args_from_readme()
1974
2039
  )
1975
2040
  print(help_print.replace("[PABOT_VERSION]", PABOT_VERSION))
1976
2041
  return 0
@@ -2002,6 +2067,14 @@ def main_program(args):
2002
2067
  opts_for_run,
2003
2068
  pabot_args,
2004
2069
  )
2070
+ if pabot_args["no-rebot"]:
2071
+ _write((
2072
+ "All tests were executed, but the --no-rebot argument was given, "
2073
+ "so the results were not compiled, and no summary was generated. "
2074
+ f"All results have been saved in the {outs_dir} folder."
2075
+ ))
2076
+ _write("===================================================")
2077
+ return 0 if not _ABNORMAL_EXIT_HAPPENED else 252
2005
2078
  result_code = _report_results(
2006
2079
  outs_dir,
2007
2080
  pabot_args,
@@ -2039,19 +2112,40 @@ def _parse_ordering(filename): # type: (str) -> List[ExecutionItem]
2039
2112
  with open(filename, "r") as orderingfile:
2040
2113
  return [
2041
2114
  parse_execution_item_line(line.strip())
2042
- for line in orderingfile.readlines()
2115
+ for line in orderingfile.readlines() if line.strip() != ""
2043
2116
  ]
2044
2117
  except FileNotFoundError:
2045
2118
  raise DataError("Error: File '%s' not found." % filename)
2046
- except:
2119
+ except (ValueError, AssertionError) as e:
2120
+ raise DataError("Error in ordering file: %s: %s" % (filename, e))
2121
+ except Exception:
2047
2122
  raise DataError("Error parsing ordering file '%s'" % filename)
2048
2123
 
2049
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
+ if item.name != ' Invalid':
2134
+ 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))
2135
+ number_of_tests_or_suites += 1
2136
+ if number_of_tests_or_suites > len(list_of_suite_names):
2137
+ 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.')
2138
+
2139
+
2050
2140
  def _group_suites(outs_dir, datasources, options, pabot_args):
2051
2141
  suite_names = solve_suite_names(outs_dir, datasources, options, pabot_args)
2052
2142
  _verify_depends(suite_names)
2053
2143
  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)
2144
+ if ordering_arg:
2145
+ _verify_depends(ordering_arg)
2146
+ _check_ordering(ordering_arg, suite_names)
2147
+ ordering_arg_with_sleep = _set_sleep_times(ordering_arg)
2148
+ ordered_suites = _preserve_order(suite_names, ordering_arg_with_sleep)
2055
2149
  shard_suites = solve_shard_suites(ordered_suites, pabot_args)
2056
2150
  grouped_suites = (
2057
2151
  _chunked_suite_names(shard_suites, pabot_args["processes"])
@@ -2062,6 +2156,29 @@ def _group_suites(outs_dir, datasources, options, pabot_args):
2062
2156
  return grouped_by_depend
2063
2157
 
2064
2158
 
2159
+ def _set_sleep_times(ordering_arg):
2160
+ # type: (List[ExecutionItem]) -> List[ExecutionItem]
2161
+ set_sleep_value = 0
2162
+ in_group = False
2163
+ output = copy.deepcopy(ordering_arg)
2164
+ if output is not None:
2165
+ if len(output) >= 2:
2166
+ for i in range(len(output) - 1):
2167
+ if isinstance(output[i], SleepItem):
2168
+ set_sleep_value = output[i].get_sleep()
2169
+ else:
2170
+ set_sleep_value = 0
2171
+ if isinstance(output[i], GroupStartItem):
2172
+ in_group = True
2173
+ if isinstance(output[i], GroupEndItem):
2174
+ in_group = False
2175
+ if isinstance(output[i + 1], GroupStartItem) and set_sleep_value > 0:
2176
+ output[i + 1].set_sleep(set_sleep_value)
2177
+ if isinstance(output[i + 1], RunnableItem) and set_sleep_value > 0 and not in_group:
2178
+ output[i + 1].set_sleep(set_sleep_value)
2179
+ return output
2180
+
2181
+
2065
2182
  def _chunked_suite_names(suite_names, processes):
2066
2183
  q, r = divmod(len(suite_names), processes)
2067
2184
  result = []
@@ -2086,7 +2203,7 @@ def _verify_depends(suite_names):
2086
2203
  suites_with_found_dependencies = list(
2087
2204
  filter(
2088
2205
  lambda suite: any(
2089
- runnable_suite.name == suite.depends
2206
+ runnable_suite.name in suite.depends
2090
2207
  for runnable_suite in runnable_suites
2091
2208
  ),
2092
2209
  suites_with_depends,
@@ -2097,63 +2214,31 @@ def _verify_depends(suite_names):
2097
2214
  "Invalid test configuration: Some test suites have dependencies (#DEPENDS) that cannot be found."
2098
2215
  )
2099
2216
  suites_with_circular_dependencies = list(
2100
- filter(lambda suite: suite.depends == suite.name, suites_with_depends)
2217
+ filter(lambda suite: suite.name in suite.depends, suites_with_depends)
2101
2218
  )
2102
2219
  if suites_with_circular_dependencies:
2103
2220
  raise DataError(
2104
2221
  "Invalid test configuration: Test suites cannot depend on themselves."
2105
2222
  )
2106
- grouped_suites = list(
2107
- filter(lambda suite: isinstance(suite, GroupItem), suite_names)
2108
- )
2109
- if grouped_suites and suites_with_depends:
2110
- raise DataError(
2111
- "Invalid test configuration: Cannot use both #DEPENDS and grouped suites."
2112
- )
2113
2223
 
2114
2224
 
2115
2225
  def _group_by_depend(suite_names):
2226
+ # type: (List[ExecutionItem]) -> List[List[ExecutionItem]]
2116
2227
  group_items = list(filter(lambda suite: isinstance(suite, GroupItem), suite_names))
2117
2228
  runnable_suites = list(
2118
2229
  filter(lambda suite: isinstance(suite, RunnableItem), suite_names)
2119
2230
  )
2120
- if group_items or not runnable_suites:
2121
- return [suite_names]
2122
- independent_tests = list(filter(lambda suite: not suite.depends, runnable_suites))
2123
- dependency_tree = [independent_tests]
2124
- dependent_tests = list(filter(lambda suite: suite.depends, runnable_suites))
2125
- unknown_dependent_tests = dependent_tests
2126
- while len(unknown_dependent_tests) > 0:
2127
- run_in_this_stage, run_later = [], []
2128
- for d in unknown_dependent_tests:
2129
- stage_indexes = []
2130
- for i, stage in enumerate(dependency_tree):
2131
- for test in stage:
2132
- if test.name in d.depends:
2133
- stage_indexes.append(i)
2134
- # All #DEPENDS test are already run:
2135
- if len(stage_indexes) == len(d.depends):
2136
- run_in_this_stage.append(d)
2137
- else:
2138
- run_later.append(d)
2139
- unknown_dependent_tests = run_later
2140
- if len(run_in_this_stage) == 0:
2141
- text = "There are circular or unmet dependencies using #DEPENDS. Check this/these test(s): " + str(run_later)
2142
- raise DataError(text)
2143
- else:
2144
- dependency_tree.append(run_in_this_stage)
2145
- flattened_dependency_tree = sum(dependency_tree, [])
2146
- if len(flattened_dependency_tree) != len(runnable_suites):
2147
- raise DataError(
2148
- "Invalid test configuration: Circular or unmet dependencies detected between test suites. Please check your #DEPENDS definitions."
2149
- )
2231
+ dependency_tree = create_dependency_tree(runnable_suites)
2232
+ # Since groups cannot depend on others, they are placed at the beginning.
2233
+ dependency_tree[0][0:0] = group_items
2150
2234
  return dependency_tree
2151
2235
 
2152
2236
 
2153
2237
  def _all_grouped_suites_by_depend(grouped_suites):
2238
+ # type: (List[List[ExecutionItem]]) -> List[List[ExecutionItem]]
2154
2239
  grouped_by_depend = []
2155
- for group_suite in grouped_suites:
2156
- grouped_by_depend += _group_by_depend(group_suite)
2240
+ for group_suite in grouped_suites: # These groups are divided by #WAIT
2241
+ grouped_by_depend.extend(_group_by_depend(group_suite))
2157
2242
  return grouped_by_depend
2158
2243
 
2159
2244
 
@@ -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.3.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
 
@@ -154,7 +156,7 @@ Supports all [Robot Framework command line options](https://robotframework.org/r
154
156
  Indicator for a file that can contain shared variables for distributing resources. This needs to be used together with
155
157
  pabotlib option. Resource file syntax is same as Windows ini files. Where a section is a shared set of variables.
156
158
 
157
- --argumentfile [INTEGER] [FILEPATH]
159
+ --argumentfile[INTEGER] [FILEPATH]
158
160
  Run same suites with multiple [argumentfile](http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#argument-files) options.
159
161
 
160
162
  For example:
@@ -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
 
@@ -259,14 +266,18 @@ After this come the suite names.
259
266
 
260
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.
261
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
+
262
271
  There different possibilities to influence the execution:
263
272
 
264
273
  * The order of suites can be changed.
265
274
  * If a directory (or a directory structure) should be executed sequentially, add the directory suite name to a row as a ```--suite``` option.
266
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.
267
- * You can add a line with text `#WAIT` to force executor to wait until all previous suites have been executed.
268
- * You can group suites and tests together to same executor process by adding line `{` before the group and `}`after.
269
- * You can introduce dependencies using the word `#DEPENDS` after a test declaration. 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. An example could be.
276
+ * You can add a line with text to force executor to wait until all previous suites have been executed.
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:
270
281
 
271
282
  ```
272
283
  --test robotTest.1 Scalar.Test With Environment Variables #DEPENDS robotTest.1 Scalar.Test with BuiltIn Variables of Robot Framework
@@ -274,8 +285,10 @@ There different possibilities to influence the execution:
274
285
  --test robotTest.2 Lists.Test with Keywords and a list
275
286
  #WAIT
276
287
  --test robotTest.2 Lists.Test with a Keyword that accepts multiple arguments
288
+ {
277
289
  --test robotTest.2 Lists.Test with some Collections keywords
278
290
  --test robotTest.2 Lists.Test to access list entries
291
+ }
279
292
  --test robotTest.3 Dictionary.Test that accesses Dictionaries
280
293
  --test robotTest.3 Dictionary.Dictionaries for named arguments #DEPENDS robotTest.3 Dictionary.Test that accesses Dictionaries
281
294
  --test robotTest.1 Scalar.Test Case With Variables #DEPENDS robotTest.3 Dictionary.Test that accesses Dictionaries
@@ -284,6 +297,51 @@ There different possibilities to influence the execution:
284
297
  --test robotTest.1 Scalar.Test With Arguments and Return Values
285
298
  --test robotTest.3 Dictionary.Test with Dictionaries as Arguments
286
299
  --test robotTest.3 Dictionary.Test with FOR loops and Dictionaries #DEPENDS robotTest.1 Scalar.Test Case with Return Values
300
+ ```
301
+
302
+ * By using the command `#SLEEP X`, where `X` is an integer in the range [0-3600] (in seconds), you can
303
+ define a startup delay for each subprocess. `#SLEEP` affects the next line unless the next line starts a
304
+ group with `{`, in which case the delay applies to the entire group. If the next line begins with `--test`
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.
307
+
308
+ The following example clarifies the behavior:
309
+
310
+ ```sh
311
+ pabot --processes 2 --ordering order.txt data_1
312
+ ```
313
+
314
+ where order.txt is:
315
+
316
+ ```
317
+ #SLEEP 1
318
+ {
319
+ #SLEEP 2
320
+ --suite Data 1.suite A
321
+ #SLEEP 3
322
+ --suite Data 1.suite B
323
+ #SLEEP 4
324
+ }
325
+ #SLEEP 5
326
+ #SLEEP 6
327
+ --suite Data 1.suite C
328
+ #SLEEP 7
329
+ --suite Data 1.suite D
330
+ #SLEEP 8
331
+ ```
332
+
333
+ prints something like this:
334
+
335
+ ```
336
+ 2025-02-15 19:15:00.408321 [0] [ID:1] SLEEPING 6 SECONDS BEFORE STARTING Data 1.suite C
337
+ 2025-02-15 19:15:00.408321 [1] [ID:0] SLEEPING 1 SECONDS BEFORE STARTING Group_Data 1.suite A_Data 1.suite B
338
+ 2025-02-15 19:15:01.409389 [PID:52008] [1] [ID:0] EXECUTING Group_Data 1.suite A_Data 1.suite B
339
+ 2025-02-15 19:15:06.409024 [PID:1528] [0] [ID:1] EXECUTING Data 1.suite C
340
+ 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
341
+ 2025-02-15 19:15:09.259067 [1] [ID:2] SLEEPING 7 SECONDS BEFORE STARTING Data 1.suite D
342
+ 2025-02-15 19:15:09.647342 [PID:1528] [0] [ID:1] PASSED Data 1.suite C in 3.2 seconds
343
+ 2025-02-15 19:15:16.260432 [PID:48156] [1] [ID:2] EXECUTING Data 1.suite D
344
+ 2025-02-15 19:15:18.696420 [PID:48156] [1] [ID:2] PASSED Data 1.suite D in 2.4 seconds
287
345
  ```
288
346
 
289
347
  ### Programmatic use
@@ -1,10 +1,10 @@
1
1
  pabot/SharedLibrary.py,sha256=mIipGs3ZhKYEakKprcbrMI4P_Un6qI8gE7086xpHaLY,2552
2
- pabot/__init__.py,sha256=KIz2lUOAX4LIQ5wjdtv4z7UJ4g2DDn7QCTslKSdY4NA,94
3
- pabot/arguments.py,sha256=XBaWujhuw_5dnZnEwAJcl4mArgzMkjNfNHBty6jqZRo,6842
2
+ pabot/__init__.py,sha256=uuwf2W7FsicbiL8y-0LayoZ4Ztsqa2N2yJAgcz2EUoE,200
3
+ pabot/arguments.py,sha256=m38y8mXKJ5BHlxSrsEI0gXlkzR5hv88G9i-FL_BouQ4,9168
4
4
  pabot/clientwrapper.py,sha256=yz7battGs0exysnDeLDWJuzpb2Q-qSjitwxZMO2TlJw,231
5
5
  pabot/coordinatorwrapper.py,sha256=nQQ7IowD6c246y8y9nsx0HZbt8vS2XODhPVDjm-lyi0,195
6
- pabot/execution_items.py,sha256=qs15SZjGE5CQAO0UpNuVWZ60wTLdWGDK1CkbBz7JtfQ,8892
7
- pabot/pabot.py,sha256=WrQsrgUVQZmzmfYLu0m01c2nbyc_Aj-Kz8sTpPo2jC8,70522
6
+ pabot/execution_items.py,sha256=HCd54LsIEZJjnL0TZC_tuac2DSVL4JHes6veJlpCE94,12058
7
+ pabot/pabot.py,sha256=GtoxhW3NTIE7It8IXtl-m6t0x0GkuT9kx2YkQs4OWhE,74910
8
8
  pabot/pabotlib.py,sha256=FRZKaKy1ybyRkE-0SpaCsUWzxZAzNNU5dAywSm1QoPk,22324
9
9
  pabot/result_merger.py,sha256=8iIptBn5MdgiW-OdhwVR2DZ0hUYuQeQXwIHAEPkMTuw,9095
10
10
  pabot/robotremoteserver.py,sha256=L3O2QRKSGSE4ux5M1ip5XJMaelqaxQWJxd9wLLdtpzM,22272
@@ -14,9 +14,9 @@ pabot/py3/client.py,sha256=Od9L4vZ0sozMHq_W_ITQHBBt8kAej40DG58wnxmbHGM,1434
14
14
  pabot/py3/coordinator.py,sha256=kBshCzA_1QX_f0WNk42QBJyDYSwSlNM-UEBxOReOj6E,2313
15
15
  pabot/py3/messages.py,sha256=7mFr4_0x1JHm5sW8TvKq28Xs_JoeIGku2bX7AyO0kng,2557
16
16
  pabot/py3/worker.py,sha256=5rfp4ZiW6gf8GRz6eC0-KUkfx847A91lVtRYpLAv2sg,1612
17
- robotframework_pabot-4.1.1.dist-info/LICENSE.txt,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
18
- robotframework_pabot-4.1.1.dist-info/METADATA,sha256=5cHJ-pp3VvOWIjwvrCbBdcVt2_wtBdZHnBvXlD_x-aw,13484
19
- robotframework_pabot-4.1.1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
20
- robotframework_pabot-4.1.1.dist-info/entry_points.txt,sha256=JpAIFADTeFOQWdwmn56KpAil8V3-41ZC5ICXCYm3Ng0,43
21
- robotframework_pabot-4.1.1.dist-info/top_level.txt,sha256=t3OwfEAsSxyxrhjy_GCJYHKbV_X6AIsgeLhYeHvObG4,6
22
- robotframework_pabot-4.1.1.dist-info/RECORD,,
17
+ robotframework_pabot-4.3.0.dist-info/licenses/LICENSE.txt,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
18
+ robotframework_pabot-4.3.0.dist-info/METADATA,sha256=coN7rNN_R3aY76vawuow6gGITRFi1Jc3nOeobID27gw,16410
19
+ robotframework_pabot-4.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
+ robotframework_pabot-4.3.0.dist-info/entry_points.txt,sha256=JpAIFADTeFOQWdwmn56KpAil8V3-41ZC5ICXCYm3Ng0,43
21
+ robotframework_pabot-4.3.0.dist-info/top_level.txt,sha256=t3OwfEAsSxyxrhjy_GCJYHKbV_X6AIsgeLhYeHvObG4,6
22
+ robotframework_pabot-4.3.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5