robotframework-pabot 4.2.0__py3-none-any.whl → 4.3.1__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
@@ -7,4 +7,4 @@ try:
7
7
  except ImportError:
8
8
  pass
9
9
 
10
- __version__ = "4.2.0"
10
+ __version__ = "4.3.1"
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
@@ -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("/")
pabot/execution_items.py CHANGED
@@ -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 = "Group_"
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(): # TODO: check!
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]
pabot/pabot.py CHANGED
@@ -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
- + 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]
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
- suites = [[]] # type: List[List[ExecutionItem]]
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
- return rebot(*outputs, **_options_for_rebot(options, start_time_string, _now()))
1535
+ exit_code = rebot(*outputs, **_options_for_rebot(options, start_time_string, _now()))
1514
1536
  else:
1515
- return _report_results_for_one_run(
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
- execution_items = SuiteItems([item.execution_item for item in chunked_items])
1871
- chunked_item = QueueItem(
1872
- base_item.datasources,
1873
- base_item.outs_dir,
1874
- base_item.options,
1875
- execution_items,
1876
- base_item.command,
1877
- base_item.verbose,
1878
- (base_item.argfile_index, base_item.argfile),
1879
- processes=base_item.processes,
1880
- timeout=base_item.timeout,
1881
- )
1882
- 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
+ )
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
- "PLACEHOLDER_README.MD",
1995
- read_args_from_readme()
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 {os.path.join(os.path.curdir, 'pabot_results')} folder."
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 == suite.depends
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.depends == suite.name, suites_with_depends)
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
- if group_items or not runnable_suites:
2177
- return [suite_names]
2178
- independent_tests = list(filter(lambda suite: not suite.depends, runnable_suites))
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 += _group_by_depend(group_suite)
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: robotframework-pabot
3
- Version: 4.2.0
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 [INTEGER] [FILEPATH]
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. 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.
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 --process 2 --ordering order.txt data_1
311
+ pabot --processes 2 --ordering order.txt data_1
305
312
  ```
306
313
 
307
314
  where order.txt is:
@@ -1,10 +1,10 @@
1
1
  pabot/SharedLibrary.py,sha256=mIipGs3ZhKYEakKprcbrMI4P_Un6qI8gE7086xpHaLY,2552
2
- pabot/__init__.py,sha256=tl2NUIH_fh3hf0eeIfL8G-zT-rzPKeBtyYVBzqUc-0g,200
3
- pabot/arguments.py,sha256=1r_rZKzgi8BY4NqSkgWveR0A0dkAR62Epq3jY1JFjsM,6973
2
+ pabot/__init__.py,sha256=MXW5_GNVuWSyyda2wmC-8BdsCTCzrK7WaD4yaZ0A0e8,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=hw8s44JkHOFW-iIktaLmW1zt456GvmDCEG9yk-bqG2Y,9694
7
- pabot/pabot.py,sha256=Hh_6tPy2vSIasrxu5E6XustcvsfFZirTMPFlfFiVFIE,73074
6
+ pabot/execution_items.py,sha256=HCd54LsIEZJjnL0TZC_tuac2DSVL4JHes6veJlpCE94,12058
7
+ pabot/pabot.py,sha256=0pk3AcU5SQinY27laXRjCzAFDX_5L-vQoMTgnshMJf4,75275
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.2.0.dist-info/licenses/LICENSE.txt,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
18
- robotframework_pabot-4.2.0.dist-info/METADATA,sha256=koUhxphmN3h_7iSn2JF7oS25LXOAy4H-n84yq5CQZPY,15434
19
- robotframework_pabot-4.2.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
20
- robotframework_pabot-4.2.0.dist-info/entry_points.txt,sha256=JpAIFADTeFOQWdwmn56KpAil8V3-41ZC5ICXCYm3Ng0,43
21
- robotframework_pabot-4.2.0.dist-info/top_level.txt,sha256=t3OwfEAsSxyxrhjy_GCJYHKbV_X6AIsgeLhYeHvObG4,6
22
- robotframework_pabot-4.2.0.dist-info/RECORD,,
17
+ robotframework_pabot-4.3.1.dist-info/licenses/LICENSE.txt,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
18
+ robotframework_pabot-4.3.1.dist-info/METADATA,sha256=UVCRVuYefYU3KxOtMBnTdVi3nDATIDN1Eb69JJdM_MY,16417
19
+ robotframework_pabot-4.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
+ robotframework_pabot-4.3.1.dist-info/entry_points.txt,sha256=JpAIFADTeFOQWdwmn56KpAil8V3-41ZC5ICXCYm3Ng0,43
21
+ robotframework_pabot-4.3.1.dist-info/top_level.txt,sha256=t3OwfEAsSxyxrhjy_GCJYHKbV_X6AIsgeLhYeHvObG4,6
22
+ robotframework_pabot-4.3.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (78.1.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5