robotframework-pabot 3.1.0__tar.gz → 4.0.4__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 (40) hide show
  1. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/PKG-INFO +1 -1
  2. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/README.md +12 -8
  3. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/src/pabot/SharedLibrary.py +7 -4
  4. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/src/pabot/__init__.py +2 -1
  5. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/src/pabot/arguments.py +94 -94
  6. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/src/pabot/execution_items.py +2 -13
  7. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/src/pabot/pabot.py +72 -31
  8. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/src/pabot/pabotlib.py +7 -7
  9. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/src/pabot/py3/messages.py +0 -1
  10. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/src/robotframework_pabot.egg-info/PKG-INFO +1 -1
  11. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/tests/test_depends.py +16 -4
  12. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/tests/test_pabot.py +103 -21
  13. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/tests/test_pabotlib.py +1 -1
  14. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/tests/test_testlevelsplit_include.py +6 -4
  15. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/LICENSE.txt +0 -0
  16. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/MANIFEST.in +0 -0
  17. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/pyproject.toml +0 -0
  18. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/setup.cfg +0 -0
  19. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/setup.py +0 -0
  20. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/src/pabot/clientwrapper.py +0 -0
  21. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/src/pabot/coordinatorwrapper.py +0 -0
  22. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/src/pabot/py3/__init__.py +0 -0
  23. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/src/pabot/py3/client.py +0 -0
  24. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/src/pabot/py3/coordinator.py +0 -0
  25. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/src/pabot/py3/worker.py +0 -0
  26. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/src/pabot/result_merger.py +0 -0
  27. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/src/pabot/robotremoteserver.py +0 -0
  28. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/src/pabot/workerwrapper.py +0 -0
  29. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/src/robotframework_pabot.egg-info/SOURCES.txt +0 -0
  30. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/src/robotframework_pabot.egg-info/dependency_links.txt +0 -0
  31. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/src/robotframework_pabot.egg-info/entry_points.txt +0 -0
  32. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/src/robotframework_pabot.egg-info/requires.txt +0 -0
  33. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/src/robotframework_pabot.egg-info/top_level.txt +0 -0
  34. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/tests/test_arguments_output.py +0 -0
  35. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/tests/test_functional.py +0 -0
  36. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/tests/test_ordering.py +0 -0
  37. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/tests/test_pabotsuitenames_io.py +0 -0
  38. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/tests/test_resultmerger.py +0 -0
  39. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/tests/test_stacktrace.py +0 -0
  40. {robotframework_pabot-3.1.0 → robotframework_pabot-4.0.4}/tests/test_testlevelsplit_output_task_order.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: robotframework-pabot
3
- Version: 3.1.0
3
+ Version: 4.0.4
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
@@ -55,14 +55,14 @@ There are several ways you can help in improving this tool:
55
55
  ## Command-line options
56
56
 
57
57
  pabot [--verbose|--testlevelsplit|--command .. --end-command|
58
- --processes num|--pabotlib|--pabotlibhost host|--pabotlibport port|
58
+ --processes num|--no-pabotlib|--pabotlibhost host|--pabotlibport port|
59
59
  --processtimeout num|
60
60
  --shard i/n|
61
61
  --artifacts extensions|--artifactsinsubfolders|
62
62
  --resourcefile file|--argumentfile[num] file|--suitesfrom file]
63
63
  [robot options] [path ...]
64
64
 
65
- Supports all [Robot Framework command line options](https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#all-command-line-options) and also following options (these must be before RF options):
65
+ Supports all [Robot Framework command line options](https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#all-command-line-options) and also following pabot options:
66
66
 
67
67
  --verbose
68
68
  more output from the parallel execution
@@ -83,13 +83,16 @@ Supports all [Robot Framework command line options](https://robotframework.org/r
83
83
  Special option "all" will use as many processes as there are
84
84
  executable suites or tests.
85
85
 
86
- --pabotlib
87
- Start PabotLib remote server. This enables locking and resource distribution between parallel test executions.
86
+ PabotLib remote server is started by default to enable locking and resource distribution
87
+ between parallel test executions.
88
+
89
+ --no-pabotlib
90
+ Disable the PabotLib remote server if you don't need locking or resource distribution features.
88
91
 
89
92
  --pabotlibhost [HOSTNAME]
90
- Host name of the PabotLib remote server (default is 127.0.0.1)
91
- If used with --pabotlib option, will change the host listen address of the created remote server (see https://github.com/robotframework/PythonRemoteServer)
92
- If used without the --pabotlib option, will connect to already running instance of the PabotLib remote server in the given host. The remote server can be also started and executed separately from pabot instances:
93
+ Connect to an already running instance of the PabotLib remote server at the given host
94
+ (disables the local PabotLib server start).
95
+ The remote server can be also started and executed separately from pabot instances:
93
96
 
94
97
  python -m pabot.pabotlib <path_to_resourcefile> <host> <port>
95
98
  python -m pabot.pabotlib resource.txt 192.168.1.123 8271
@@ -142,8 +145,9 @@ Example usages:
142
145
  pabot --command java -jar robotframework.jar --end-command --include SMOKE tests
143
146
  pabot --processes 10 tests
144
147
  pabot --pabotlibhost 192.168.1.123 --pabotlibport 8271 --processes 10 tests
145
- pabot --pabotlib --pabotlibhost 192.168.1.111 --pabotlibport 8272 --processes 10 tests
146
148
  pabot --artifacts png,mp4,txt --artifactsinsubfolders directory_to_tests
149
+ # To disable PabotLib:
150
+ pabot --no-pabotlib tests
147
151
 
148
152
  ### PabotLib
149
153
 
@@ -11,7 +11,6 @@ from .robotremoteserver import RemoteLibraryFactory
11
11
 
12
12
 
13
13
  class SharedLibrary(object):
14
-
15
14
  ROBOT_LIBRARY_SCOPE = "GLOBAL"
16
15
 
17
16
  def __init__(self, name, args=None):
@@ -26,8 +25,10 @@ class SharedLibrary(object):
26
25
  "Not currently running pabot. Importing library for this process."
27
26
  )
28
27
  self._lib = RemoteLibraryFactory(
29
- TestLibrary.from_name(name, args=args, variables=None, create_keywords=True).instance
30
- if ROBOT_VERSION >= "7.0"
28
+ TestLibrary.from_name(
29
+ name, args=args, variables=None, create_keywords=True
30
+ ).instance
31
+ if ROBOT_VERSION >= "7.0"
31
32
  else TestLibrary(name, args=args).get_instance()
32
33
  )
33
34
  return
@@ -36,7 +37,9 @@ class SharedLibrary(object):
36
37
  remotelib = Remote(uri) if uri else None
37
38
  if remotelib:
38
39
  try:
39
- port = remotelib.run_keyword("import_shared_library", [name], {"args": args})
40
+ port = remotelib.run_keyword(
41
+ "import_shared_library", [name], {"args": args}
42
+ )
40
43
  except RuntimeError:
41
44
  logger.error("No connection - is pabot called with --pabotlib option?")
42
45
  raise
@@ -1,4 +1,5 @@
1
1
  from __future__ import absolute_import
2
2
 
3
3
  from .pabotlib import PabotLib
4
- __version__ = "3.1.0"
4
+
5
+ __version__ = "4.0.4"
@@ -80,7 +80,7 @@ def _parse_pabot_args(args): # type: (List[str]) -> Tuple[List[str], Dict[str,
80
80
  "verbose": False,
81
81
  "help": False,
82
82
  "testlevelsplit": False,
83
- "pabotlib": False,
83
+ "pabotlib": True,
84
84
  "pabotlibhost": "127.0.0.1",
85
85
  "pabotlibport": 8270,
86
86
  "processes": _processes_count(),
@@ -91,108 +91,108 @@ def _parse_pabot_args(args): # type: (List[str]) -> Tuple[List[str], Dict[str,
91
91
  "shardcount": 1,
92
92
  "chunk": False,
93
93
  }
94
+ # Explicitly define argument types for validation
95
+ flag_args = {
96
+ "verbose",
97
+ "help",
98
+ "testlevelsplit",
99
+ "pabotlib",
100
+ "artifactsinsubfolders",
101
+ "chunk",
102
+ }
103
+ value_args = {
104
+ "hive": str,
105
+ "processes": lambda x: int(x) if x != "all" else None,
106
+ "resourcefile": str,
107
+ "pabotlibhost": str,
108
+ "pabotlibport": int,
109
+ "processtimeout": int,
110
+ "ordering": _parse_ordering,
111
+ "suitesfrom": str,
112
+ "artifacts": lambda x: x.split(","),
113
+ "shard": _parse_shard,
114
+ }
115
+
94
116
  argumentfiles = []
95
- while args and (
96
- args[0]
97
- in [
98
- "--" + param
99
- for param in [
100
- "hive",
101
- "command",
102
- "processes",
103
- "verbose",
104
- "resourcefile",
105
- "testlevelsplit",
106
- "pabotlib",
107
- "pabotlibhost",
108
- "pabotlibport",
109
- "processtimeout",
110
- "ordering",
111
- "suitesfrom",
112
- "artifacts",
113
- "artifactsinsubfolders",
114
- "help",
115
- "shard",
116
- "chunk",
117
- ]
118
- ]
119
- or ARGSMATCHER.match(args[0])
120
- ):
121
- if args[0] == "--hive":
122
- pabot_args["hive"] = args[1]
123
- args = args[2:]
124
- continue
125
- if args[0] == "--command":
126
- end_index = args.index("--end-command")
127
- pabot_args["command"] = args[1:end_index]
128
- args = args[end_index + 1 :]
129
- continue
130
- if args[0] == "--processes":
131
- pabot_args["processes"] = int(args[1]) if args[1] != 'all' else None
132
- args = args[2:]
133
- continue
134
- if args[0] == "--verbose":
135
- pabot_args["verbose"] = True
136
- args = args[1:]
137
- continue
138
- if args[0] == "--chunk":
139
- pabot_args["chunk"] = True
140
- args = args[1:]
141
- continue
142
- if args[0] == "--resourcefile":
143
- pabot_args["resourcefile"] = args[1]
144
- args = args[2:]
145
- continue
146
- if args[0] == "--pabotlib":
147
- pabot_args["pabotlib"] = True
148
- args = args[1:]
149
- continue
150
- if args[0] == "--ordering":
151
- pabot_args["ordering"] = _parse_ordering(args[1])
152
- args = args[2:]
117
+ remaining_args = []
118
+ i = 0
119
+
120
+ # Track conflicting options during parsing
121
+ saw_pabotlib_flag = False
122
+ saw_no_pabotlib = False
123
+
124
+ while i < len(args):
125
+ arg = args[i]
126
+ if not arg.startswith("--"):
127
+ remaining_args.append(arg)
128
+ i += 1
153
129
  continue
154
- if args[0] == "--testlevelsplit":
155
- pabot_args["testlevelsplit"] = True
130
+
131
+ arg_name = arg[2:] # Strip '--'
132
+
133
+ if arg_name == "no-pabotlib":
134
+ saw_no_pabotlib = True
135
+ pabot_args["pabotlib"] = False # Just set the main flag
156
136
  args = args[1:]
157
137
  continue
158
- if args[0] == "--pabotlibhost":
159
- pabot_args["pabotlibhost"] = args[1]
160
- args = args[2:]
161
- continue
162
- if args[0] == "--pabotlibport":
163
- pabot_args["pabotlibport"] = int(args[1])
164
- args = args[2:]
165
- continue
166
- if args[0] == "--processtimeout":
167
- pabot_args["processtimeout"] = int(args[1])
168
- args = args[2:]
169
- continue
170
- if args[0] == "--suitesfrom":
171
- pabot_args["suitesfrom"] = args[1]
172
- args = args[2:]
173
- continue
174
- if args[0] == "--artifacts":
175
- pabot_args["artifacts"] = args[1].split(",")
176
- args = args[2:]
177
- continue
178
- if args[0] == "--artifactsinsubfolders":
179
- pabot_args["artifactsinsubfolders"] = True
138
+ if arg_name == "pabotlib":
139
+ saw_pabotlib_flag = True
180
140
  args = args[1:]
181
141
  continue
182
- if args[0] == "--shard":
183
- pabot_args["shardindex"], pabot_args["shardcount"] = _parse_shard(args[1])
184
- args = args[2:]
185
- continue
186
- match = ARGSMATCHER.match(args[0])
142
+
143
+ # Special case for command
144
+ if arg_name == "command":
145
+ try:
146
+ end_index = args.index("--end-command", i)
147
+ pabot_args["command"] = args[i + 1 : end_index]
148
+ i = end_index + 1
149
+ continue
150
+ except ValueError:
151
+ raise DataError("--command requires matching --end-command")
152
+
153
+ # Handle flag arguments
154
+ if arg_name in flag_args:
155
+ pabot_args[arg_name] = True
156
+ i += 1
157
+ continue
158
+
159
+ # Handle value arguments
160
+ if arg_name in value_args:
161
+ if i + 1 >= len(args):
162
+ raise DataError(f"--{arg_name} requires a value")
163
+ try:
164
+ value = value_args[arg_name](args[i + 1])
165
+ if arg_name == "shard":
166
+ pabot_args["shardindex"], pabot_args["shardcount"] = value
167
+ elif arg_name == "pabotlibhost":
168
+ pabot_args["pabotlib"] = False
169
+ pabot_args[arg_name] = value
170
+ else:
171
+ pabot_args[arg_name] = value
172
+ i += 2
173
+ continue
174
+ except (ValueError, TypeError) as e:
175
+ raise DataError(f"Invalid value for --{arg_name}: {args[i + 1]}")
176
+
177
+ # Handle argument files
178
+ match = ARGSMATCHER.match(arg)
187
179
  if match:
188
- argumentfiles += [(match.group(1), args[1])]
189
- args = args[2:]
180
+ if i + 1 >= len(args):
181
+ raise DataError(f"{arg} requires a value")
182
+ argumentfiles.append((match.group(1), args[i + 1]))
183
+ i += 2
190
184
  continue
191
- if args and args[0] == "--help":
192
- pabot_args["help"] = True
193
- args = args[1:]
185
+
186
+ # If we get here, it's a non-pabot argument
187
+ remaining_args.append(arg)
188
+ i += 1
189
+
190
+ if saw_pabotlib_flag and saw_no_pabotlib:
191
+ raise DataError("Cannot use both --pabotlib and --no-pabotlib options together")
192
+
194
193
  pabot_args["argumentfiles"] = argumentfiles
195
- return args, pabot_args
194
+
195
+ return remaining_args, pabot_args
196
196
 
197
197
 
198
198
  def _parse_ordering(filename): # type: (str) -> List[ExecutionItem]
@@ -8,7 +8,6 @@ from robot.utils import PY2, is_unicode
8
8
 
9
9
  @total_ordering
10
10
  class ExecutionItem(object):
11
-
12
11
  isWait = False
13
12
  type = None # type: str
14
13
  name = None # type: str
@@ -51,7 +50,6 @@ class ExecutionItem(object):
51
50
 
52
51
 
53
52
  class HivedItem(ExecutionItem):
54
-
55
53
  type = "hived"
56
54
 
57
55
  def __init__(self, item, hive):
@@ -67,7 +65,6 @@ class HivedItem(ExecutionItem):
67
65
 
68
66
 
69
67
  class GroupItem(ExecutionItem):
70
-
71
68
  type = "group"
72
69
 
73
70
  def __init__(self):
@@ -128,7 +125,6 @@ class RunnableItem(ExecutionItem):
128
125
 
129
126
 
130
127
  class SuiteItem(RunnableItem):
131
-
132
128
  type = "suite"
133
129
 
134
130
  def __init__(self, name, tests=None, suites=None, dynamictests=None):
@@ -163,9 +159,9 @@ class SuiteItem(RunnableItem):
163
159
  return False
164
160
  if self.name == other.name:
165
161
  return True
166
- if other.name.endswith('.'+self.name):
162
+ if other.name.endswith("." + self.name):
167
163
  return True
168
- if self.name.endswith('.'+other.name):
164
+ if self.name.endswith("." + other.name):
169
165
  return True
170
166
  return False
171
167
 
@@ -178,7 +174,6 @@ class SuiteItem(RunnableItem):
178
174
 
179
175
 
180
176
  class TestItem(RunnableItem):
181
-
182
177
  type = "test"
183
178
 
184
179
  def __init__(self, name):
@@ -229,7 +224,6 @@ class DynamicSuiteItem(SuiteItem):
229
224
 
230
225
 
231
226
  class DynamicTestItem(ExecutionItem):
232
-
233
227
  type = "dynamictest"
234
228
 
235
229
  def __init__(self, name, suite):
@@ -258,7 +252,6 @@ class DynamicTestItem(ExecutionItem):
258
252
 
259
253
 
260
254
  class WaitItem(ExecutionItem):
261
-
262
255
  type = "wait"
263
256
  isWait = True
264
257
 
@@ -270,7 +263,6 @@ class WaitItem(ExecutionItem):
270
263
 
271
264
 
272
265
  class GroupStartItem(ExecutionItem):
273
-
274
266
  type = "group"
275
267
 
276
268
  def __init__(self):
@@ -281,7 +273,6 @@ class GroupStartItem(ExecutionItem):
281
273
 
282
274
 
283
275
  class GroupEndItem(ExecutionItem):
284
-
285
276
  type = "group"
286
277
 
287
278
  def __init__(self):
@@ -292,7 +283,6 @@ class GroupEndItem(ExecutionItem):
292
283
 
293
284
 
294
285
  class IncludeItem(ExecutionItem):
295
-
296
286
  type = "include"
297
287
 
298
288
  def __init__(self, tag):
@@ -309,7 +299,6 @@ class IncludeItem(ExecutionItem):
309
299
 
310
300
 
311
301
  class SuiteItems(ExecutionItem):
312
-
313
302
  type = "suite"
314
303
 
315
304
  def __init__(self, suites):
@@ -44,12 +44,20 @@ options (these must be before normal RF options):
44
44
  Indicator for a file that can contain shared variables for
45
45
  distributing resources.
46
46
 
47
- --pabotlib
48
- Start PabotLib remote server. This enables locking and resource
49
- distribution between parallel test executions.
47
+ --no-pabotlib
48
+ Disable the PabotLib remote server if you don't need locking or resource distribution features.
49
+ PabotLib remote server is started by default.
50
50
 
51
- --pabotlibhost [HOSTNAME]
52
- Host name of the PabotLib remote server (default is 127.0.0.1)
51
+ --pabotlibhost [HOSTNAME]
52
+ Connect to an already running instance of the PabotLib remote server at the given host
53
+ (disables the local PabotLib server start). For example, to connect to a
54
+ remote PabotLib server running on another machine:
55
+
56
+ pabot --pabotlibhost 192.168.1.123 --pabotlibport 8271 tests/
57
+
58
+ The remote PabotLib server can be started separately using:
59
+ python -m pabot.pabotlib <path_to_resourcefile> <host> <port>
60
+ python -m pabot.pabotlib resource.txt 192.168.1.123 8271
53
61
 
54
62
  --pabotlibport [PORT]
55
63
  Port number of the PabotLib remote server (default is 8270)
@@ -236,7 +244,7 @@ def execute_and_wait_with(item):
236
244
  caller_id,
237
245
  item.index,
238
246
  item.execution_item.type != "test",
239
- process_timeout=item.timeout
247
+ process_timeout=item.timeout,
240
248
  )
241
249
  outputxml_preprocessing(
242
250
  item.options, outs_dir, name, item.verbose, _make_id(), caller_id
@@ -294,7 +302,7 @@ def _try_execute_and_wait(
294
302
  caller_id,
295
303
  my_index=-1,
296
304
  show_stdout_on_failure=False,
297
- process_timeout=None
305
+ process_timeout=None,
298
306
  ):
299
307
  # type: (List[str], str, str, bool, int, str, int, bool, Optional[int]) -> None
300
308
  plib = None
@@ -305,7 +313,15 @@ def _try_execute_and_wait(
305
313
  with open(os.path.join(outs_dir, cmd[0] + "_stdout.out"), "w") as stdout:
306
314
  with open(os.path.join(outs_dir, cmd[0] + "_stderr.out"), "w") as stderr:
307
315
  process, (rc, elapsed) = _run(
308
- cmd, stderr, stdout, item_name, verbose, pool_id, my_index, outs_dir, process_timeout
316
+ cmd,
317
+ stderr,
318
+ stdout,
319
+ item_name,
320
+ verbose,
321
+ pool_id,
322
+ my_index,
323
+ outs_dir,
324
+ process_timeout,
309
325
  )
310
326
  except:
311
327
  _write(traceback.format_exc())
@@ -478,7 +494,17 @@ def _increase_completed(plib, my_index):
478
494
  )
479
495
 
480
496
 
481
- def _run(command, stderr, stdout, item_name, verbose, pool_id, item_index, outs_dir, process_timeout):
497
+ def _run(
498
+ command,
499
+ stderr,
500
+ stdout,
501
+ item_name,
502
+ verbose,
503
+ pool_id,
504
+ item_index,
505
+ outs_dir,
506
+ process_timeout,
507
+ ):
482
508
  # type: (List[str], IO[Any], IO[Any], str, bool, int, int, str, Optional[int]) -> Tuple[Union[subprocess.Popen[bytes], subprocess.Popen], Tuple[int, float]]
483
509
  timestamp = datetime.datetime.now()
484
510
  cmd = " ".join(command)
@@ -487,10 +513,14 @@ def _run(command, stderr, stdout, item_name, verbose, pool_id, item_index, outs_
487
513
  # avoid hitting https://bugs.python.org/issue10394
488
514
  with POPEN_LOCK:
489
515
  my_env = os.environ.copy()
490
- syslog_file = my_env.get('ROBOT_SYSLOG_FILE', None)
516
+ syslog_file = my_env.get("ROBOT_SYSLOG_FILE", None)
491
517
  if syslog_file:
492
- my_env['ROBOT_SYSLOG_FILE'] = os.path.join(outs_dir, os.path.basename(syslog_file))
493
- process = subprocess.Popen(cmd, shell=True, stderr=stderr, stdout=stdout, env=my_env)
518
+ my_env["ROBOT_SYSLOG_FILE"] = os.path.join(
519
+ outs_dir, os.path.basename(syslog_file)
520
+ )
521
+ process = subprocess.Popen(
522
+ cmd, shell=True, stderr=stderr, stdout=stdout, env=my_env
523
+ )
494
524
  if verbose:
495
525
  _write_with_id(
496
526
  process,
@@ -507,7 +537,9 @@ def _run(command, stderr, stdout, item_name, verbose, pool_id, item_index, outs_
507
537
  "EXECUTING %s" % item_name,
508
538
  timestamp=timestamp,
509
539
  )
510
- return process, _wait_for_return_code(process, item_name, pool_id, item_index, process_timeout)
540
+ return process, _wait_for_return_code(
541
+ process, item_name, pool_id, item_index, process_timeout
542
+ )
511
543
 
512
544
 
513
545
  def _wait_for_return_code(process, item_name, pool_id, item_index, process_timeout):
@@ -522,12 +554,15 @@ def _wait_for_return_code(process, item_name, pool_id, item_index, process_timeo
522
554
  if process_timeout and elapsed / 10.0 >= process_timeout:
523
555
  process.terminate()
524
556
  process.wait()
525
- rc = -1 # Set a return code indicating that the process was killed due to timeout
557
+ rc = (
558
+ -1
559
+ ) # Set a return code indicating that the process was killed due to timeout
526
560
  _write_with_id(
527
561
  process,
528
562
  pool_id,
529
563
  item_index,
530
- "Process %s killed due to exceeding the maximum timeout of %s seconds" % (item_name, process_timeout),
564
+ "Process %s killed due to exceeding the maximum timeout of %s seconds"
565
+ % (item_name, process_timeout),
531
566
  )
532
567
  break
533
568
 
@@ -544,7 +579,6 @@ def _wait_for_return_code(process, item_name, pool_id, item_index, process_timeo
544
579
  return rc, elapsed / 10.0
545
580
 
546
581
 
547
-
548
582
  def _read_file(file_handle):
549
583
  try:
550
584
  with open(file_handle.name, "r") as content_file:
@@ -1121,7 +1155,6 @@ def store_suite_names(hashes, suite_names):
1121
1155
  for d in suite_lines
1122
1156
  )
1123
1157
  except IOError:
1124
-
1125
1158
  _write(
1126
1159
  "[ "
1127
1160
  + _wrap_with(Color.YELLOW, "WARNING")
@@ -1154,12 +1187,14 @@ def generate_suite_names_with_builder(outs_dir, datasources, options):
1154
1187
  # Note: first argument (included_suites) is deprecated from RobotFramework 6.1
1155
1188
  if ROBOT_VERSION >= "6.1":
1156
1189
  builder = TestSuiteBuilder(
1157
- included_extensions=settings.extension, rpa=settings.rpa, lang=opts.get("language")
1190
+ included_extensions=settings.extension,
1191
+ rpa=settings.rpa,
1192
+ lang=opts.get("language"),
1158
1193
  )
1159
1194
  else:
1160
1195
  builder = TestSuiteBuilder(
1161
1196
  settings["SuiteNames"], settings.extension, rpa=settings.rpa
1162
- )
1197
+ )
1163
1198
 
1164
1199
  suite = builder.build(*datasources)
1165
1200
  settings.rpa = builder.rpa
@@ -1657,7 +1692,7 @@ class QueueItem(object):
1657
1692
  argfile,
1658
1693
  hive=None,
1659
1694
  processes=0,
1660
- timeout=None
1695
+ timeout=None,
1661
1696
  ):
1662
1697
  # type: (List[str], str, Dict[str, object], ExecutionItem, List[str], bool, Tuple[str, Optional[str]], Optional[str], int, Optional[int]) -> None
1663
1698
  self.datasources = datasources
@@ -1755,7 +1790,7 @@ def _create_items(datasources, opts_for_run, outs_dir, pabot_args, suite_group):
1755
1790
  argfile,
1756
1791
  pabot_args.get("hive"),
1757
1792
  pabot_args["processes"],
1758
- pabot_args["processtimeout"]
1793
+ pabot_args["processtimeout"],
1759
1794
  )
1760
1795
  for suite in suite_group
1761
1796
  for argfile in pabot_args["argumentfiles"] or [("", None)]
@@ -1774,9 +1809,7 @@ def _create_execution_items_for_dry_run(
1774
1809
  datasources, opts_for_run, outs_dir, pabot_args, suite_group
1775
1810
  )
1776
1811
  chunk_size = (
1777
- round(len(items) / processes_count)
1778
- if len(items) > processes_count
1779
- else 1
1812
+ round(len(items) / processes_count) if len(items) > processes_count else 1
1780
1813
  )
1781
1814
  chunked_items = list(_chunk_items(items, chunk_size))
1782
1815
  _NUMBER_OF_ITEMS_TO_BE_EXECUTED += len(chunked_items)
@@ -1800,7 +1833,7 @@ def _chunk_items(items, chunk_size):
1800
1833
  base_item.verbose,
1801
1834
  (base_item.argfile_index, base_item.argfile),
1802
1835
  processes=base_item.processes,
1803
- timeout=base_item.timeout
1836
+ timeout=base_item.timeout,
1804
1837
  )
1805
1838
  yield chunked_item
1806
1839
 
@@ -1880,7 +1913,7 @@ def _get_dynamically_created_execution_items(
1880
1913
  ("", None),
1881
1914
  pabot_args.get("hive"),
1882
1915
  pabot_args["processes"],
1883
- pabot_args["processtimeout"]
1916
+ pabot_args["processtimeout"],
1884
1917
  )
1885
1918
  for suite in suite_group
1886
1919
  ]
@@ -1982,7 +2015,7 @@ def _group_suites(outs_dir, datasources, options, pabot_args):
1982
2015
  grouped_suites = (
1983
2016
  _chunked_suite_names(shard_suites, pabot_args["processes"])
1984
2017
  if pabot_args["chunk"]
1985
- else _group_by_wait(_group_by_groups(ordered_suites))
2018
+ else _group_by_wait(_group_by_groups(shard_suites))
1986
2019
  )
1987
2020
  grouped_by_depend = _all_grouped_suites_by_depend(grouped_suites)
1988
2021
  return grouped_by_depend
@@ -2019,17 +2052,23 @@ def _verify_depends(suite_names):
2019
2052
  )
2020
2053
  )
2021
2054
  if suites_with_depends != suites_with_found_dependencies:
2022
- raise Exception("There are unmet dependencies using #DEPENDS")
2055
+ raise DataError(
2056
+ "Invalid test configuration: Some test suites have dependencies (#DEPENDS) that cannot be found."
2057
+ )
2023
2058
  suites_with_circular_dependencies = list(
2024
2059
  filter(lambda suite: suite.depends == suite.name, suites_with_depends)
2025
2060
  )
2026
2061
  if suites_with_circular_dependencies:
2027
- raise Exception("There are suites with circular dependencies using #DEPENDS")
2062
+ raise DataError(
2063
+ "Invalid test configuration: Test suites cannot depend on themselves."
2064
+ )
2028
2065
  grouped_suites = list(
2029
2066
  filter(lambda suite: isinstance(suite, GroupItem), suite_names)
2030
2067
  )
2031
2068
  if grouped_suites and suites_with_depends:
2032
- raise Exception("#DEPENDS and grouped suites are incompatible")
2069
+ raise DataError(
2070
+ "Invalid test configuration: Cannot use both #DEPENDS and grouped suites."
2071
+ )
2033
2072
 
2034
2073
 
2035
2074
  def _group_by_depend(suite_names):
@@ -2057,7 +2096,9 @@ def _group_by_depend(suite_names):
2057
2096
  dependency_tree += [dependent_on_last_stage]
2058
2097
  flattened_dependency_tree = sum(dependency_tree, [])
2059
2098
  if len(flattened_dependency_tree) != len(runnable_suites):
2060
- raise Exception("There are circular or unmet dependencies using #DEPENDS")
2099
+ raise DataError(
2100
+ "Invalid test configuration: Circular or unmet dependencies detected between test suites. Please check your #DEPENDS definitions."
2101
+ )
2061
2102
  return dependency_tree
2062
2103
 
2063
2104
 
@@ -43,7 +43,6 @@ PABOT_MIN_QUEUE_INDEX_EXECUTING_PARALLEL_VALUE = "pabot_min_queue_index_executin
43
43
 
44
44
 
45
45
  class _PabotLib(object):
46
-
47
46
  _TAGS_KEY = "tags"
48
47
 
49
48
  def __init__(self, resourcefile=None): # type: (Optional[str]) -> None
@@ -155,17 +154,19 @@ class _PabotLib(object):
155
154
  content[self._TAGS_KEY] = []
156
155
  self._values[name] = content
157
156
 
158
- def import_shared_library(self, name, args=None): # type: (str, Iterable[Any]|None) -> int
157
+ def import_shared_library(
158
+ self, name, args=None
159
+ ): # type: (str, Iterable[Any]|None) -> int
159
160
  if name in self._remote_libraries:
160
161
  return self._remote_libraries[name][0]
161
162
  if name in STDLIBS:
162
- import_name = 'robot.libraries.' + name
163
+ import_name = "robot.libraries." + name
163
164
  else:
164
165
  import_name = name
165
- imported = Importer('library').import_class_or_module(name_or_path=import_name, instantiate_with_args=args)
166
- server = RobotRemoteServer(
167
- imported, port=0, serve=False, allow_stop=True
166
+ imported = Importer("library").import_class_or_module(
167
+ name_or_path=import_name, instantiate_with_args=args
168
168
  )
169
+ server = RobotRemoteServer(imported, port=0, serve=False, allow_stop=True)
169
170
  server_thread = threading.Thread(target=server.serve)
170
171
  server_thread.start()
171
172
  time.sleep(1)
@@ -197,7 +198,6 @@ class _PabotLib(object):
197
198
 
198
199
 
199
200
  class PabotLib(_PabotLib):
200
-
201
201
  __version__ = 0.67
202
202
  ROBOT_LIBRARY_SCOPE = "GLOBAL"
203
203
  ROBOT_LISTENER_API_VERSION = 2
@@ -34,7 +34,6 @@ def get(sock) -> str:
34
34
 
35
35
 
36
36
  class Message:
37
-
38
37
  _type: Optional[int]
39
38
  _length: int
40
39
  _data: Optional[str]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: robotframework-pabot
3
- Version: 3.1.0
3
+ Version: 4.0.4
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
@@ -96,7 +96,10 @@ class DependsTest(unittest.TestCase):
96
96
  --test Test.The Test S1Test 08
97
97
  """,
98
98
  )
99
- self.assertIn(b"circular or unmet dependencies", stderr)
99
+ self.assertIn(
100
+ b"Invalid test configuration: Circular or unmet dependencies detected between test suites",
101
+ stdout,
102
+ )
100
103
 
101
104
  def test_unmet_dependency(self):
102
105
  stdout, stderr = self._run_tests_with(
@@ -107,7 +110,10 @@ class DependsTest(unittest.TestCase):
107
110
  --test Test.The Test S1Test 08
108
111
  """,
109
112
  )
110
- self.assertIn(b"circular or unmet dependencies", stderr)
113
+ self.assertIn(
114
+ b"Invalid test configuration: Circular or unmet dependencies detected between test suites. Please check your #DEPENDS definitions.",
115
+ stdout,
116
+ )
111
117
 
112
118
  def test_same_reference(self):
113
119
  stdout, stderr = self._run_tests_with(
@@ -118,7 +124,10 @@ class DependsTest(unittest.TestCase):
118
124
  --test Test.The Test S1Test 08
119
125
  """,
120
126
  )
121
- self.assertIn(b"circular or unmet dependencies", stderr)
127
+ self.assertIn(
128
+ b"Invalid test configuration: Circular or unmet dependencies detected between test suites. Please check your #DEPENDS definitions.",
129
+ stdout,
130
+ )
122
131
 
123
132
  def test_wait(self):
124
133
  stdout, stderr = self._run_tests_with(
@@ -130,4 +139,7 @@ class DependsTest(unittest.TestCase):
130
139
  --test Test.The Test S1Test 08
131
140
  """,
132
141
  )
133
- self.assertIn(b"circular or unmet dependencies", stderr)
142
+ self.assertIn(
143
+ b"Invalid test configuration: Circular or unmet dependencies detected between test suites",
144
+ stdout,
145
+ )
@@ -86,7 +86,6 @@ class PabotTests(unittest.TestCase):
86
86
  "--resourcefile",
87
87
  "resourcefile.ini",
88
88
  "--testlevelsplit",
89
- "--pabotlib",
90
89
  "--pabotlibhost",
91
90
  "123.123.233.123",
92
91
  "--pabotlibport",
@@ -105,7 +104,7 @@ class PabotTests(unittest.TestCase):
105
104
  self.assertEqual(pabot_args["command"], ["my_own_command.sh"])
106
105
  self.assertEqual(pabot_args["processes"], 12)
107
106
  self.assertEqual(pabot_args["resourcefile"], "resourcefile.ini")
108
- self.assertEqual(pabot_args["pabotlib"], True)
107
+ self.assertEqual(pabot_args["pabotlib"], False)
109
108
  self.assertEqual(pabot_args["pabotlibhost"], "123.123.233.123")
110
109
  self.assertEqual(pabot_args["pabotlibport"], 4562)
111
110
  self.assertEqual(pabot_args["suitesfrom"], "some.xml")
@@ -1226,16 +1225,17 @@ class PabotTests(unittest.TestCase):
1226
1225
  file_path = os.path.join(_opts["outputdir"], f)
1227
1226
  self.assertTrue(os.path.isfile(file_path), "file not copied: {}".format(f))
1228
1227
  os.remove(file_path) # clean up
1229
-
1228
+
1230
1229
  def test_merge_one_run_with_and_without_legacyoutput(self):
1231
1230
  dtemp = tempfile.mkdtemp()
1232
1231
  # Create the same directory structure as pabot
1233
- test_outputs = os.path.join(dtemp, 'outputs')
1232
+ test_outputs = os.path.join(dtemp, "outputs")
1234
1233
  os.makedirs(test_outputs)
1235
- test_output = os.path.join(test_outputs, 'output.xml')
1234
+ test_output = os.path.join(test_outputs, "output.xml")
1236
1235
  # Create a minimal but valid output.xml
1237
- with open(test_output, 'w') as f:
1238
- f.write("""<?xml version="1.0" encoding="UTF-8"?>
1236
+ with open(test_output, "w") as f:
1237
+ f.write(
1238
+ """<?xml version="1.0" encoding="UTF-8"?>
1239
1239
  <robot generator="Rebot 7.1.1 (Python 3.11.7 on darwin)" generated="20241130 11:19:45.235" rpa="false" schemaversion="4">
1240
1240
  <suite id="s1" name="Suites">
1241
1241
  <suite id="s1-s1" name="Test" source="/Users/mkorpela/workspace/pabot/test.robot">
@@ -1281,16 +1281,17 @@ class PabotTests(unittest.TestCase):
1281
1281
  <msg timestamp="20241130 11:19:44.910" level="ERROR">Error in file '/Users/mkorpela/workspace/pabot/test.robot' on line 2: Library 'Easter' expected 0 arguments, got 1.</msg>
1282
1282
  <msg timestamp="20241130 11:19:44.913" level="ERROR">Error in file '/Users/mkorpela/workspace/pabot/test.robot' on line 2: Library 'Easter' expected 0 arguments, got 1.</msg>
1283
1283
  </errors>
1284
- </robot>""")
1285
-
1286
- self._options['outputdir'] = dtemp
1284
+ </robot>"""
1285
+ )
1286
+
1287
+ self._options["outputdir"] = dtemp
1287
1288
  if ROBOT_VERSION >= "7.0":
1288
- self._options['legacyoutput'] = True
1289
+ self._options["legacyoutput"] = True
1289
1290
  try:
1290
1291
  output = pabot._merge_one_run(
1291
1292
  outs_dir=dtemp,
1292
1293
  options=self._options,
1293
- tests_root_name='Test', # Should match suite name in XML
1294
+ tests_root_name="Test", # Should match suite name in XML
1294
1295
  stats={
1295
1296
  "total": 0,
1296
1297
  "passed": 0,
@@ -1298,9 +1299,12 @@ class PabotTests(unittest.TestCase):
1298
1299
  "skipped": 0,
1299
1300
  },
1300
1301
  copied_artifacts=[],
1301
- outputfile='merged_output.xml') # Use different name to avoid confusion
1302
- self.assertTrue(output, "merge_one_run returned empty string") # Verify we got output path
1303
- with open(output, 'r') as f:
1302
+ outputfile="merged_output.xml",
1303
+ ) # Use different name to avoid confusion
1304
+ self.assertTrue(
1305
+ output, "merge_one_run returned empty string"
1306
+ ) # Verify we got output path
1307
+ with open(output, "r") as f:
1304
1308
  content = f.read()
1305
1309
  if ROBOT_VERSION >= "6.1":
1306
1310
  self.assertIn('schemaversion="4"', content)
@@ -1309,11 +1313,11 @@ class PabotTests(unittest.TestCase):
1309
1313
  elif ROBOT_VERSION >= "4.0":
1310
1314
  self.assertIn('schemaversion="2"', content)
1311
1315
  if ROBOT_VERSION >= "7.0":
1312
- del self._options['legacyoutput']
1316
+ del self._options["legacyoutput"]
1313
1317
  output = pabot._merge_one_run(
1314
1318
  outs_dir=dtemp,
1315
1319
  options=self._options,
1316
- tests_root_name='Test', # Should match suite name in XML
1320
+ tests_root_name="Test", # Should match suite name in XML
1317
1321
  stats={
1318
1322
  "total": 0,
1319
1323
  "passed": 0,
@@ -1321,14 +1325,92 @@ class PabotTests(unittest.TestCase):
1321
1325
  "skipped": 0,
1322
1326
  },
1323
1327
  copied_artifacts=[],
1324
- outputfile='merged_2_output.xml') # Use different name to avoid confusion
1325
- self.assertTrue(output, "merge_one_run returned empty string") # Verify we got output path
1326
- with open(output, 'r') as f:
1328
+ outputfile="merged_2_output.xml",
1329
+ ) # Use different name to avoid confusion
1330
+ self.assertTrue(
1331
+ output, "merge_one_run returned empty string"
1332
+ ) # Verify we got output path
1333
+ with open(output, "r") as f:
1327
1334
  content = f.read()
1328
1335
  self.assertIn('schemaversion="5"', content)
1329
1336
  self.assertNotIn('schemaversion="4"', content)
1330
1337
  finally:
1331
- shutil.rmtree(dtemp)
1338
+ shutil.rmtree(dtemp)
1339
+
1340
+ def test_parse_args_mixed_order(self):
1341
+ (
1342
+ options,
1343
+ datasources,
1344
+ pabot_args,
1345
+ options_for_subprocesses,
1346
+ ) = arguments.parse_args(
1347
+ [
1348
+ "--exitonfailure",
1349
+ "--processes",
1350
+ "12",
1351
+ "--outputdir",
1352
+ "mydir",
1353
+ "--verbose",
1354
+ "--pabotlib",
1355
+ "suite",
1356
+ ]
1357
+ )
1358
+ self.assertEqual(pabot_args["processes"], 12)
1359
+ self.assertEqual(pabot_args["verbose"], True)
1360
+ self.assertEqual(pabot_args["pabotlib"], True)
1361
+ self.assertEqual(options["outputdir"], "mydir")
1362
+ self.assertEqual(options["exitonfailure"], True)
1363
+ self.assertEqual(datasources, ["suite"])
1364
+
1365
+ def test_parse_args_error_handling(self):
1366
+ with self.assertRaises(DataError) as cm:
1367
+ arguments.parse_args(["--processes"])
1368
+ self.assertIn("requires a value", str(cm.exception))
1369
+
1370
+ with self.assertRaises(DataError) as cm:
1371
+ arguments.parse_args(["--processes", "invalid"])
1372
+ self.assertIn("Invalid value for --processes", str(cm.exception))
1373
+
1374
+ with self.assertRaises(DataError) as cm:
1375
+ arguments.parse_args(["--command", "echo", "hello"])
1376
+ self.assertIn("requires matching --end-command", str(cm.exception))
1377
+
1378
+ def test_parse_args_command_with_pabot_args(self):
1379
+ options, datasources, pabot_args, _ = arguments.parse_args(
1380
+ [
1381
+ "--command",
1382
+ "script.sh",
1383
+ "--processes",
1384
+ "5",
1385
+ "--end-command",
1386
+ "--verbose",
1387
+ "suite",
1388
+ ]
1389
+ )
1390
+ self.assertEqual(pabot_args["command"], ["script.sh", "--processes", "5"])
1391
+ self.assertEqual(pabot_args["verbose"], True)
1392
+
1393
+ def test_pabotlib_defaults_to_enabled(self):
1394
+ options, _, pabot_args, _ = arguments.parse_args(["suite"])
1395
+ self.assertTrue(pabot_args["pabotlib"])
1396
+ self.assertFalse("no_pabotlib" in pabot_args) # Ensure internal flag not leaked
1397
+
1398
+ def test_no_pabotlib_disables_pabotlib(self):
1399
+ options, _, pabot_args, _ = arguments.parse_args(["--no-pabotlib", "suite"])
1400
+ self.assertFalse(pabot_args["pabotlib"])
1401
+ self.assertFalse("no_pabotlib" in pabot_args) # Ensure internal flag not leaked
1402
+
1403
+ def test_pabotlib_option_shows_warning(self):
1404
+ options, _, pabot_args, _ = arguments.parse_args(["--pabotlib", "suite"])
1405
+ self.assertTrue(pabot_args["pabotlib"])
1406
+ self.assertFalse("no_pabotlib" in pabot_args) # Ensure internal flag not leaked
1407
+
1408
+ def test_conflicting_pabotlib_options_raise_error(self):
1409
+ with self.assertRaises(DataError) as context:
1410
+ arguments.parse_args(["--pabotlib", "--no-pabotlib", "suite"])
1411
+ self.assertIn(
1412
+ "Cannot use both --pabotlib and --no-pabotlib", str(context.exception)
1413
+ )
1332
1414
 
1333
1415
 
1334
1416
  if __name__ == "__main__":
@@ -28,7 +28,7 @@ class PabotLibTests(unittest.TestCase):
28
28
  def test_shared_library_with_args(self):
29
29
  try:
30
30
  self._create_ctx() # Set up Robot Framework context
31
- lib = SharedLibrary("mylib", ["2"])
31
+ lib = SharedLibrary("mylib", ["2"])
32
32
  self.assertIsNotNone(lib)
33
33
  lib._remote = None
34
34
  lib._lib.run_keyword("mykeyword", ["arg"], {})
@@ -7,14 +7,14 @@ import shutil
7
7
  import subprocess
8
8
 
9
9
 
10
-
11
10
  class PabotPassJsonUsingVariableOptionTests(unittest.TestCase):
12
11
  def setUp(self):
13
12
  self.tmpdir = tempfile.mkdtemp()
14
13
  file_path = f"{self.tmpdir}/test.robot"
15
14
  with open(file_path, "w") as robot_file:
16
15
  robot_file.write(
17
- textwrap.dedent("""
16
+ textwrap.dedent(
17
+ """
18
18
  *** Test Cases ***
19
19
  Testing 1
20
20
  [Tags] tag
@@ -23,8 +23,10 @@ Testing 1
23
23
  Testing 2
24
24
  [Tags] tag
25
25
  Log world
26
- """))
27
-
26
+ """
27
+ )
28
+ )
29
+
28
30
  process = subprocess.Popen(
29
31
  [
30
32
  sys.executable,