robotframework-pabot 5.0.0__py3-none-any.whl → 5.2.0b1__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/ProcessManager.py +376 -0
- pabot/__init__.py +1 -1
- pabot/arguments.py +80 -24
- pabot/pabot.py +392 -166
- pabot/pabotlib.py +1 -1
- pabot/result_merger.py +2 -2
- pabot/robotremoteserver.py +27 -7
- pabot/skip_listener.py +7 -0
- pabot/timeout_listener.py +5 -0
- pabot/writer.py +110 -0
- {robotframework_pabot-5.0.0.dist-info → robotframework_pabot-5.2.0b1.dist-info}/METADATA +93 -42
- robotframework_pabot-5.2.0b1.dist-info/RECORD +25 -0
- robotframework_pabot-5.0.0.dist-info/RECORD +0 -22
- robotframework_pabot-5.0.0.dist-info/licenses/LICENSE.txt +0 -202
- {robotframework_pabot-5.0.0.dist-info → robotframework_pabot-5.2.0b1.dist-info}/WHEEL +0 -0
- {robotframework_pabot-5.0.0.dist-info → robotframework_pabot-5.2.0b1.dist-info}/entry_points.txt +0 -0
- {robotframework_pabot-5.0.0.dist-info → robotframework_pabot-5.2.0b1.dist-info}/top_level.txt +0 -0
pabot/pabot.py
CHANGED
|
@@ -48,6 +48,7 @@ from glob import glob
|
|
|
48
48
|
from io import BytesIO, StringIO
|
|
49
49
|
from multiprocessing.pool import ThreadPool
|
|
50
50
|
from natsort import natsorted
|
|
51
|
+
from pathlib import Path
|
|
51
52
|
|
|
52
53
|
from robot import __version__ as ROBOT_VERSION
|
|
53
54
|
from robot import rebot
|
|
@@ -83,6 +84,7 @@ from .execution_items import (
|
|
|
83
84
|
create_dependency_tree,
|
|
84
85
|
)
|
|
85
86
|
from .result_merger import merge
|
|
87
|
+
from .writer import get_writer
|
|
86
88
|
|
|
87
89
|
try:
|
|
88
90
|
import queue # type: ignore
|
|
@@ -100,10 +102,10 @@ try:
|
|
|
100
102
|
except ImportError:
|
|
101
103
|
METADATA_AVAILABLE = False
|
|
102
104
|
|
|
103
|
-
from typing import
|
|
105
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
104
106
|
|
|
105
107
|
CTRL_C_PRESSED = False
|
|
106
|
-
MESSAGE_QUEUE = queue.Queue()
|
|
108
|
+
#MESSAGE_QUEUE = queue.Queue()
|
|
107
109
|
EXECUTION_POOL_IDS = [] # type: List[int]
|
|
108
110
|
EXECUTION_POOL_ID_LOCK = threading.Lock()
|
|
109
111
|
POPEN_LOCK = threading.Lock()
|
|
@@ -130,6 +132,15 @@ _ALL_ELAPSED = [] # type: List[Union[int, float]]
|
|
|
130
132
|
# Python version check for supporting importlib.metadata (requires Python 3.8+)
|
|
131
133
|
IS_PYTHON_3_8_OR_NEWER = sys.version_info >= (3, 8)
|
|
132
134
|
|
|
135
|
+
_PROCESS_MANAGER = None
|
|
136
|
+
|
|
137
|
+
def _ensure_process_manager():
|
|
138
|
+
global _PROCESS_MANAGER
|
|
139
|
+
if _PROCESS_MANAGER is None:
|
|
140
|
+
from pabot.ProcessManager import ProcessManager
|
|
141
|
+
_PROCESS_MANAGER = ProcessManager()
|
|
142
|
+
return _PROCESS_MANAGER
|
|
143
|
+
|
|
133
144
|
|
|
134
145
|
def read_args_from_readme():
|
|
135
146
|
"""Reads a specific section from package METADATA or development README.md if available."""
|
|
@@ -194,8 +205,13 @@ def extract_section(lines, start_marker="<!-- START DOCSTRING -->", end_marker="
|
|
|
194
205
|
if end_marker in line:
|
|
195
206
|
break
|
|
196
207
|
if inside_section:
|
|
197
|
-
# Remove Markdown
|
|
198
|
-
|
|
208
|
+
# Remove Markdown hyperlinks but keep text
|
|
209
|
+
line = re.sub(r'\[([^\]]+)\]\(https?://[^\)]+\)', r'\1', line)
|
|
210
|
+
# Remove Markdown section links but keep text
|
|
211
|
+
line = re.sub(r'\[([^\]]+)\]\(#[^\)]+\)', r'\1', line)
|
|
212
|
+
# Remove ** and backticks `
|
|
213
|
+
line = re.sub(r'(\*\*|`)', '', line)
|
|
214
|
+
extracted_lines.append(line)
|
|
199
215
|
|
|
200
216
|
return "".join(extracted_lines).strip()
|
|
201
217
|
|
|
@@ -210,7 +226,7 @@ class Color:
|
|
|
210
226
|
|
|
211
227
|
|
|
212
228
|
def execute_and_wait_with(item):
|
|
213
|
-
# type: ('QueueItem') ->
|
|
229
|
+
# type: ('QueueItem') -> int
|
|
214
230
|
global CTRL_C_PRESSED, _NUMBER_OF_ITEMS_TO_BE_EXECUTED
|
|
215
231
|
is_last = _NUMBER_OF_ITEMS_TO_BE_EXECUTED == 1
|
|
216
232
|
_NUMBER_OF_ITEMS_TO_BE_EXECUTED -= 1
|
|
@@ -229,6 +245,7 @@ def execute_and_wait_with(item):
|
|
|
229
245
|
run_cmd, run_options = _create_command_for_execution(
|
|
230
246
|
caller_id, datasources, is_last, item, outs_dir
|
|
231
247
|
)
|
|
248
|
+
rc = 0
|
|
232
249
|
if item.hive:
|
|
233
250
|
_hived_execute(
|
|
234
251
|
item.hive,
|
|
@@ -241,7 +258,7 @@ def execute_and_wait_with(item):
|
|
|
241
258
|
item.index,
|
|
242
259
|
)
|
|
243
260
|
else:
|
|
244
|
-
_try_execute_and_wait(
|
|
261
|
+
rc = _try_execute_and_wait(
|
|
245
262
|
run_cmd,
|
|
246
263
|
run_options,
|
|
247
264
|
outs_dir,
|
|
@@ -259,6 +276,7 @@ def execute_and_wait_with(item):
|
|
|
259
276
|
)
|
|
260
277
|
except:
|
|
261
278
|
_write(traceback.format_exc())
|
|
279
|
+
return rc
|
|
262
280
|
|
|
263
281
|
|
|
264
282
|
def _create_command_for_execution(caller_id, datasources, is_last, item, outs_dir):
|
|
@@ -276,6 +294,7 @@ def _create_command_for_execution(caller_id, datasources, is_last, item, outs_di
|
|
|
276
294
|
item.index,
|
|
277
295
|
item.last_level,
|
|
278
296
|
item.processes,
|
|
297
|
+
item.skip,
|
|
279
298
|
)
|
|
280
299
|
+ datasources
|
|
281
300
|
)
|
|
@@ -312,7 +331,7 @@ def _try_execute_and_wait(
|
|
|
312
331
|
process_timeout=None,
|
|
313
332
|
sleep_before_start=0
|
|
314
333
|
):
|
|
315
|
-
# type: (List[str], List[str], str, str, bool, int, str, int, bool, Optional[int], int) ->
|
|
334
|
+
# type: (List[str], List[str], str, str, bool, int, str, int, bool, Optional[int], int) -> int
|
|
316
335
|
plib = None
|
|
317
336
|
is_ignored = False
|
|
318
337
|
if _pabotlib_in_use():
|
|
@@ -354,7 +373,8 @@ def _try_execute_and_wait(
|
|
|
354
373
|
show_stdout_on_failure,
|
|
355
374
|
)
|
|
356
375
|
if is_ignored and os.path.isdir(outs_dir):
|
|
357
|
-
|
|
376
|
+
_rmtree_with_path(outs_dir)
|
|
377
|
+
return rc
|
|
358
378
|
|
|
359
379
|
|
|
360
380
|
def _result_to_stdout(
|
|
@@ -551,88 +571,39 @@ def _run(
|
|
|
551
571
|
process_timeout,
|
|
552
572
|
sleep_before_start,
|
|
553
573
|
):
|
|
554
|
-
# type: (List[str], List[str], IO[Any], IO[Any], str, bool, int, int, str, Optional[int], int) -> Tuple[Union[subprocess.Popen[bytes], subprocess.Popen], Tuple[int, float]]
|
|
555
574
|
timestamp = datetime.datetime.now()
|
|
575
|
+
|
|
556
576
|
if sleep_before_start > 0:
|
|
557
|
-
_write(
|
|
558
|
-
"%s [%s] [ID:%s] SLEEPING %s SECONDS BEFORE STARTING %s"
|
|
559
|
-
% (timestamp, pool_id, item_index, sleep_before_start, item_name),
|
|
560
|
-
)
|
|
577
|
+
_write(f"{timestamp} [{pool_id}] [ID:{item_index}] SLEEPING {sleep_before_start} SECONDS BEFORE STARTING {item_name}")
|
|
561
578
|
time.sleep(sleep_before_start)
|
|
562
|
-
|
|
579
|
+
|
|
563
580
|
command_name = run_command[-1].replace(" ", "_")
|
|
564
581
|
argfile_path = os.path.join(outs_dir, f"{command_name}_argfile.txt")
|
|
565
582
|
_write_internal_argument_file(run_options, filename=argfile_path)
|
|
566
|
-
cmd = ' '.join(run_command + ['-A'] + [argfile_path])
|
|
567
|
-
if PY2:
|
|
568
|
-
cmd = cmd.decode("utf-8").encode(SYSTEM_ENCODING)
|
|
569
|
-
# avoid hitting https://bugs.python.org/issue10394
|
|
570
|
-
with POPEN_LOCK:
|
|
571
|
-
my_env = os.environ.copy()
|
|
572
|
-
syslog_file = my_env.get("ROBOT_SYSLOG_FILE", None)
|
|
573
|
-
if syslog_file:
|
|
574
|
-
my_env["ROBOT_SYSLOG_FILE"] = os.path.join(
|
|
575
|
-
outs_dir, os.path.basename(syslog_file)
|
|
576
|
-
)
|
|
577
|
-
process = subprocess.Popen(
|
|
578
|
-
cmd, shell=True, stderr=stderr, stdout=stdout, env=my_env
|
|
579
|
-
)
|
|
580
|
-
if verbose:
|
|
581
|
-
_write_with_id(
|
|
582
|
-
process,
|
|
583
|
-
pool_id,
|
|
584
|
-
item_index,
|
|
585
|
-
"EXECUTING PARALLEL %s with command:\n%s" % (item_name, cmd),
|
|
586
|
-
timestamp=timestamp,
|
|
587
|
-
)
|
|
588
|
-
else:
|
|
589
|
-
_write_with_id(
|
|
590
|
-
process,
|
|
591
|
-
pool_id,
|
|
592
|
-
item_index,
|
|
593
|
-
"EXECUTING %s" % item_name,
|
|
594
|
-
timestamp=timestamp,
|
|
595
|
-
)
|
|
596
|
-
return process, _wait_for_return_code(
|
|
597
|
-
process, item_name, pool_id, item_index, process_timeout
|
|
598
|
-
)
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
def _wait_for_return_code(process, item_name, pool_id, item_index, process_timeout):
|
|
602
|
-
rc = None
|
|
603
|
-
elapsed = 0
|
|
604
|
-
ping_time = ping_interval = 150
|
|
605
|
-
while rc is None:
|
|
606
|
-
rc = process.poll()
|
|
607
|
-
time.sleep(0.1)
|
|
608
|
-
elapsed += 1
|
|
609
|
-
|
|
610
|
-
if process_timeout and elapsed / 10.0 >= process_timeout:
|
|
611
|
-
process.terminate()
|
|
612
|
-
process.wait()
|
|
613
|
-
rc = (
|
|
614
|
-
-1
|
|
615
|
-
) # Set a return code indicating that the process was killed due to timeout
|
|
616
|
-
_write_with_id(
|
|
617
|
-
process,
|
|
618
|
-
pool_id,
|
|
619
|
-
item_index,
|
|
620
|
-
"Process %s killed due to exceeding the maximum timeout of %s seconds"
|
|
621
|
-
% (item_name, process_timeout),
|
|
622
|
-
)
|
|
623
|
-
break
|
|
624
583
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
584
|
+
cmd = run_command + ['-A', argfile_path]
|
|
585
|
+
my_env = os.environ.copy()
|
|
586
|
+
syslog_file = my_env.get("ROBOT_SYSLOG_FILE", None)
|
|
587
|
+
if syslog_file:
|
|
588
|
+
my_env["ROBOT_SYSLOG_FILE"] = os.path.join(outs_dir, os.path.basename(syslog_file))
|
|
589
|
+
|
|
590
|
+
log_path = os.path.join(outs_dir, f"{command_name}_{item_index}.log")
|
|
591
|
+
|
|
592
|
+
manager = _ensure_process_manager()
|
|
593
|
+
process, (rc, elapsed) = manager.run(
|
|
594
|
+
cmd,
|
|
595
|
+
env=my_env,
|
|
596
|
+
stdout=stdout,
|
|
597
|
+
stderr=stderr,
|
|
598
|
+
timeout=process_timeout,
|
|
599
|
+
verbose=verbose,
|
|
600
|
+
item_name=item_name,
|
|
601
|
+
log_file=log_path,
|
|
602
|
+
pool_id=pool_id,
|
|
603
|
+
item_index=item_index,
|
|
604
|
+
)
|
|
634
605
|
|
|
635
|
-
return rc, elapsed
|
|
606
|
+
return process, (rc, elapsed)
|
|
636
607
|
|
|
637
608
|
|
|
638
609
|
def _read_file(file_handle):
|
|
@@ -692,6 +663,7 @@ def _options_for_executor(
|
|
|
692
663
|
queueIndex,
|
|
693
664
|
last_level,
|
|
694
665
|
processes,
|
|
666
|
+
skip,
|
|
695
667
|
):
|
|
696
668
|
options = options.copy()
|
|
697
669
|
options["log"] = "NONE"
|
|
@@ -728,6 +700,11 @@ def _options_for_executor(
|
|
|
728
700
|
options["argumentfile"] = argfile
|
|
729
701
|
if options.get("test", False) and options.get("include", []):
|
|
730
702
|
del options["include"]
|
|
703
|
+
if skip:
|
|
704
|
+
this_dir = os.path.dirname(os.path.abspath(__file__))
|
|
705
|
+
listener_path = os.path.join(this_dir, "skip_listener.py")
|
|
706
|
+
options["dryrun"] = True
|
|
707
|
+
options["listener"].append(listener_path)
|
|
731
708
|
return _set_terminal_coloring_options(options)
|
|
732
709
|
|
|
733
710
|
|
|
@@ -750,10 +727,13 @@ def _modify_options_for_argfile_use(argfile, options):
|
|
|
750
727
|
|
|
751
728
|
|
|
752
729
|
def _replace_base_name(new_name, options, key):
|
|
753
|
-
if isinstance(options.get(key
|
|
754
|
-
options[key] = new_name
|
|
730
|
+
if isinstance(options.get(key), str):
|
|
731
|
+
options[key] = f"{new_name}.{options[key].split('.', 1)[1]}" if '.' in options[key] else new_name
|
|
755
732
|
elif key in options:
|
|
756
|
-
options[key] = [
|
|
733
|
+
options[key] = [
|
|
734
|
+
f"{new_name}.{s.split('.', 1)[1]}" if '.' in s else new_name
|
|
735
|
+
for s in options.get(key, [])
|
|
736
|
+
]
|
|
757
737
|
|
|
758
738
|
|
|
759
739
|
def _set_terminal_coloring_options(options):
|
|
@@ -1438,6 +1418,117 @@ def keyboard_interrupt(*args):
|
|
|
1438
1418
|
CTRL_C_PRESSED = True
|
|
1439
1419
|
|
|
1440
1420
|
|
|
1421
|
+
def _get_depends(item):
|
|
1422
|
+
return getattr(item.execution_item, "depends", [])
|
|
1423
|
+
|
|
1424
|
+
|
|
1425
|
+
def _dependencies_satisfied(item, completed):
|
|
1426
|
+
return all(dep in completed for dep in _get_depends(item))
|
|
1427
|
+
|
|
1428
|
+
|
|
1429
|
+
def _collect_transitive_dependents(failed_name, pending_items):
|
|
1430
|
+
"""
|
|
1431
|
+
Returns all pending items that (directly or indirectly) depend on failed_name.
|
|
1432
|
+
"""
|
|
1433
|
+
to_skip = set()
|
|
1434
|
+
queue = [failed_name]
|
|
1435
|
+
|
|
1436
|
+
# Build dependency map once
|
|
1437
|
+
depends_map = {
|
|
1438
|
+
item.execution_item.name: set(_get_depends(item))
|
|
1439
|
+
for item in pending_items
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
while queue:
|
|
1443
|
+
current = queue.pop(0)
|
|
1444
|
+
for item_name, deps in depends_map.items():
|
|
1445
|
+
if current in deps and item_name not in to_skip:
|
|
1446
|
+
to_skip.add(item_name)
|
|
1447
|
+
queue.append(item_name)
|
|
1448
|
+
|
|
1449
|
+
return to_skip
|
|
1450
|
+
|
|
1451
|
+
|
|
1452
|
+
def _parallel_execute_dynamic(
|
|
1453
|
+
items,
|
|
1454
|
+
processes,
|
|
1455
|
+
datasources,
|
|
1456
|
+
outs_dir,
|
|
1457
|
+
opts_for_run,
|
|
1458
|
+
pabot_args,
|
|
1459
|
+
):
|
|
1460
|
+
original_signal_handler = signal.signal(signal.SIGINT, keyboard_interrupt)
|
|
1461
|
+
|
|
1462
|
+
max_processes = processes or len(items)
|
|
1463
|
+
pool = ThreadPool(max_processes)
|
|
1464
|
+
|
|
1465
|
+
pending = set(items)
|
|
1466
|
+
running = {}
|
|
1467
|
+
completed = set()
|
|
1468
|
+
failed = set()
|
|
1469
|
+
|
|
1470
|
+
failure_policy = pabot_args.get("ordering", {}).get("failure_policy", "run_all")
|
|
1471
|
+
lock = threading.Lock()
|
|
1472
|
+
|
|
1473
|
+
def on_complete(it, rc):
|
|
1474
|
+
nonlocal pending, running, completed, failed
|
|
1475
|
+
|
|
1476
|
+
with lock:
|
|
1477
|
+
running.pop(it, None)
|
|
1478
|
+
completed.add(it.execution_item.name)
|
|
1479
|
+
|
|
1480
|
+
if rc != 0:
|
|
1481
|
+
failed.add(it.execution_item.name)
|
|
1482
|
+
|
|
1483
|
+
if failure_policy == "skip":
|
|
1484
|
+
to_skip_names = _collect_transitive_dependents(
|
|
1485
|
+
it.execution_item.name,
|
|
1486
|
+
pending,
|
|
1487
|
+
)
|
|
1488
|
+
|
|
1489
|
+
for other in list(pending):
|
|
1490
|
+
if other.execution_item.name in to_skip_names:
|
|
1491
|
+
_write(
|
|
1492
|
+
f"Skipping '{other.execution_item.name}' because dependency "
|
|
1493
|
+
f"'{it.execution_item.name}' failed (transitive).",
|
|
1494
|
+
Color.YELLOW,
|
|
1495
|
+
)
|
|
1496
|
+
other.skip = True
|
|
1497
|
+
|
|
1498
|
+
try:
|
|
1499
|
+
while pending or running:
|
|
1500
|
+
with lock:
|
|
1501
|
+
ready = [
|
|
1502
|
+
item for item in list(pending)
|
|
1503
|
+
if _dependencies_satisfied(item, completed)
|
|
1504
|
+
]
|
|
1505
|
+
|
|
1506
|
+
while ready and len(running) < max_processes:
|
|
1507
|
+
item = ready.pop(0)
|
|
1508
|
+
pending.remove(item)
|
|
1509
|
+
|
|
1510
|
+
result = pool.apply_async(
|
|
1511
|
+
execute_and_wait_with,
|
|
1512
|
+
(item,),
|
|
1513
|
+
callback=lambda rc, it=item: on_complete(it, rc),
|
|
1514
|
+
)
|
|
1515
|
+
running[item] = result
|
|
1516
|
+
|
|
1517
|
+
dynamic_items = _get_dynamically_created_execution_items(
|
|
1518
|
+
datasources, outs_dir, opts_for_run, pabot_args
|
|
1519
|
+
)
|
|
1520
|
+
if dynamic_items:
|
|
1521
|
+
with lock:
|
|
1522
|
+
for di in dynamic_items:
|
|
1523
|
+
pending.add(di)
|
|
1524
|
+
|
|
1525
|
+
time.sleep(0.1)
|
|
1526
|
+
|
|
1527
|
+
finally:
|
|
1528
|
+
pool.close()
|
|
1529
|
+
signal.signal(signal.SIGINT, original_signal_handler)
|
|
1530
|
+
|
|
1531
|
+
|
|
1441
1532
|
def _parallel_execute(
|
|
1442
1533
|
items, processes, datasources, outs_dir, opts_for_run, pabot_args
|
|
1443
1534
|
):
|
|
@@ -1473,15 +1564,30 @@ def _output_dir(options, cleanup=True):
|
|
|
1473
1564
|
outputdir = options.get("outputdir", ".")
|
|
1474
1565
|
outpath = os.path.join(outputdir, "pabot_results")
|
|
1475
1566
|
if cleanup and os.path.isdir(outpath):
|
|
1476
|
-
|
|
1567
|
+
_rmtree_with_path(outpath)
|
|
1477
1568
|
return outpath
|
|
1478
1569
|
|
|
1479
1570
|
|
|
1480
|
-
def
|
|
1481
|
-
|
|
1571
|
+
def _rmtree_with_path(path):
|
|
1572
|
+
"""
|
|
1573
|
+
Remove a directory tree and, if a PermissionError occurs,
|
|
1574
|
+
re-raise it with the absolute path included in the message.
|
|
1575
|
+
"""
|
|
1576
|
+
try:
|
|
1577
|
+
shutil.rmtree(path)
|
|
1578
|
+
except PermissionError as e:
|
|
1579
|
+
abs_path = os.path.abspath(path)
|
|
1580
|
+
raise PermissionError(f"Failed to delete path {abs_path}") from e
|
|
1581
|
+
|
|
1582
|
+
|
|
1583
|
+
def _get_timestamp_id(timestamp_str, add_timestamp):
|
|
1584
|
+
# type: (str, bool) -> Optional[str]
|
|
1585
|
+
if add_timestamp:
|
|
1586
|
+
return str(datetime.datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S.%f").strftime("%Y%m%d_%H%M%S"))
|
|
1587
|
+
return None
|
|
1482
1588
|
|
|
1483
1589
|
|
|
1484
|
-
def _copy_output_artifacts(options, timestamp_id, file_extensions=None, include_subfolders=False, index=None):
|
|
1590
|
+
def _copy_output_artifacts(options, timestamp_id=None, file_extensions=None, include_subfolders=False, index=None):
|
|
1485
1591
|
file_extensions = file_extensions or ["png"]
|
|
1486
1592
|
pabot_outputdir = _output_dir(options, cleanup=False)
|
|
1487
1593
|
outputdir = options.get("outputdir", ".")
|
|
@@ -1505,9 +1611,9 @@ def _copy_output_artifacts(options, timestamp_id, file_extensions=None, include_
|
|
|
1505
1611
|
dst_folder_path = os.path.join(outputdir, subfolder_path)
|
|
1506
1612
|
if not os.path.isdir(dst_folder_path):
|
|
1507
1613
|
os.makedirs(dst_folder_path)
|
|
1508
|
-
|
|
1509
|
-
if
|
|
1510
|
-
|
|
1614
|
+
dst_file_name_parts = [timestamp_id, index, prefix, file_name]
|
|
1615
|
+
filtered_name = [str(p) for p in dst_file_name_parts if p is not None]
|
|
1616
|
+
dst_file_name = "-".join(filtered_name)
|
|
1511
1617
|
shutil.copy2(
|
|
1512
1618
|
os.path.join(location, file_name),
|
|
1513
1619
|
os.path.join(dst_folder_path, dst_file_name),
|
|
@@ -1517,16 +1623,34 @@ def _copy_output_artifacts(options, timestamp_id, file_extensions=None, include_
|
|
|
1517
1623
|
|
|
1518
1624
|
|
|
1519
1625
|
def _check_pabot_results_for_missing_xml(base_dir, command_name, output_xml_name):
|
|
1626
|
+
"""
|
|
1627
|
+
Check for missing Robot Framework output XML files in pabot result directories,
|
|
1628
|
+
taking into account the optional timestamp added by the -T option.
|
|
1629
|
+
|
|
1630
|
+
Args:
|
|
1631
|
+
base_dir: The root directory containing pabot subdirectories
|
|
1632
|
+
command_name: Name of the command that generated the output (used for fallback stderr filename)
|
|
1633
|
+
output_xml_name: Expected XML filename, e.g., 'output.xml'
|
|
1634
|
+
|
|
1635
|
+
Returns:
|
|
1636
|
+
List of paths to stderr output files for directories where the XML is missing.
|
|
1637
|
+
"""
|
|
1520
1638
|
missing = []
|
|
1639
|
+
# Prepare regex to match timestamped filenames like output-YYYYMMDD-hhmmss.xml
|
|
1640
|
+
name_stem = os.path.splitext(output_xml_name)[0]
|
|
1641
|
+
name_suffix = os.path.splitext(output_xml_name)[1]
|
|
1642
|
+
pattern = re.compile(rf"^{re.escape(name_stem)}(-\d{{8}}-\d{{6}})?{re.escape(name_suffix)}$")
|
|
1643
|
+
|
|
1521
1644
|
for root, dirs, _ in os.walk(base_dir):
|
|
1522
1645
|
if root == base_dir:
|
|
1523
1646
|
for subdir in dirs:
|
|
1524
1647
|
subdir_path = os.path.join(base_dir, subdir)
|
|
1525
|
-
|
|
1648
|
+
# Check if any file matches the expected XML name or timestamped variant
|
|
1649
|
+
has_xml = any(pattern.match(fname) for fname in os.listdir(subdir_path))
|
|
1526
1650
|
if not has_xml:
|
|
1527
|
-
|
|
1528
|
-
missing.append(os.path.join(subdir_path, f
|
|
1529
|
-
break
|
|
1651
|
+
sanitized_cmd = command_name.replace(" ", "_")
|
|
1652
|
+
missing.append(os.path.join(subdir_path, f"{sanitized_cmd}_stderr.out"))
|
|
1653
|
+
break # only check immediate subdirectories
|
|
1530
1654
|
return missing
|
|
1531
1655
|
|
|
1532
1656
|
|
|
@@ -1551,7 +1675,7 @@ def _report_results(outs_dir, pabot_args, options, start_time_string, tests_root
|
|
|
1551
1675
|
outputs = [] # type: List[str]
|
|
1552
1676
|
for index, _ in pabot_args["argumentfiles"]:
|
|
1553
1677
|
copied_artifacts = _copy_output_artifacts(
|
|
1554
|
-
options, _get_timestamp_id(start_time_string), pabot_args["artifacts"], pabot_args["artifactsinsubfolders"], index
|
|
1678
|
+
options, _get_timestamp_id(start_time_string, pabot_args["artifactstimestamps"]), pabot_args["artifacts"], pabot_args["artifactsinsubfolders"], index
|
|
1555
1679
|
)
|
|
1556
1680
|
outputs += [
|
|
1557
1681
|
_merge_one_run(
|
|
@@ -1560,7 +1684,7 @@ def _report_results(outs_dir, pabot_args, options, start_time_string, tests_root
|
|
|
1560
1684
|
tests_root_name,
|
|
1561
1685
|
stats,
|
|
1562
1686
|
copied_artifacts,
|
|
1563
|
-
timestamp_id=_get_timestamp_id(start_time_string),
|
|
1687
|
+
timestamp_id=_get_timestamp_id(start_time_string, pabot_args["artifactstimestamps"]),
|
|
1564
1688
|
outputfile=os.path.join("pabot_results", "output%s.xml" % index),
|
|
1565
1689
|
)
|
|
1566
1690
|
]
|
|
@@ -1607,16 +1731,35 @@ def _write_stats(stats):
|
|
|
1607
1731
|
_write("===================================================")
|
|
1608
1732
|
|
|
1609
1733
|
|
|
1734
|
+
def add_timestamp_to_filename(file_path: str, timestamp: str) -> str:
|
|
1735
|
+
"""
|
|
1736
|
+
Rename the given file by inserting a timestamp before the extension.
|
|
1737
|
+
Format: YYYYMMDD-hhmmss
|
|
1738
|
+
Example: output.xml -> output-20251222-152233.xml
|
|
1739
|
+
"""
|
|
1740
|
+
file_path = Path(file_path)
|
|
1741
|
+
if not file_path.exists():
|
|
1742
|
+
raise FileNotFoundError(f"{file_path} does not exist")
|
|
1743
|
+
|
|
1744
|
+
new_name = f"{file_path.stem}-{timestamp}{file_path.suffix}"
|
|
1745
|
+
new_path = file_path.with_name(new_name)
|
|
1746
|
+
file_path.rename(new_path)
|
|
1747
|
+
return str(new_path)
|
|
1748
|
+
|
|
1749
|
+
|
|
1610
1750
|
def _report_results_for_one_run(
|
|
1611
1751
|
outs_dir, pabot_args, options, start_time_string, tests_root_name, stats
|
|
1612
1752
|
):
|
|
1613
1753
|
copied_artifacts = _copy_output_artifacts(
|
|
1614
|
-
options, _get_timestamp_id(start_time_string), pabot_args["artifacts"], pabot_args["artifactsinsubfolders"]
|
|
1754
|
+
options, _get_timestamp_id(start_time_string, pabot_args["artifactstimestamps"]), pabot_args["artifacts"], pabot_args["artifactsinsubfolders"]
|
|
1615
1755
|
)
|
|
1616
1756
|
output_path = _merge_one_run(
|
|
1617
|
-
outs_dir, options, tests_root_name, stats, copied_artifacts, _get_timestamp_id(start_time_string)
|
|
1757
|
+
outs_dir, options, tests_root_name, stats, copied_artifacts, _get_timestamp_id(start_time_string, pabot_args["artifactstimestamps"])
|
|
1618
1758
|
)
|
|
1619
1759
|
_write_stats(stats)
|
|
1760
|
+
ts = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
1761
|
+
if "timestampoutputs" in options and options["timestampoutputs"]:
|
|
1762
|
+
output_path = add_timestamp_to_filename(output_path, ts)
|
|
1620
1763
|
if (
|
|
1621
1764
|
"report" in options
|
|
1622
1765
|
and options["report"].upper() == "NONE"
|
|
@@ -1629,7 +1772,7 @@ def _report_results_for_one_run(
|
|
|
1629
1772
|
else:
|
|
1630
1773
|
_write("Output: %s" % output_path)
|
|
1631
1774
|
options["output"] = None # Do not write output again with rebot
|
|
1632
|
-
return rebot(output_path, **_options_for_rebot(options, start_time_string,
|
|
1775
|
+
return rebot(output_path, **_options_for_rebot(options, start_time_string, ts))
|
|
1633
1776
|
|
|
1634
1777
|
|
|
1635
1778
|
def _merge_one_run(
|
|
@@ -1640,7 +1783,16 @@ def _merge_one_run(
|
|
|
1640
1783
|
os.path.join(options.get("outputdir", "."), outputfile)
|
|
1641
1784
|
)
|
|
1642
1785
|
filename = options.get("output") or "output.xml"
|
|
1643
|
-
|
|
1786
|
+
base_name, ext = os.path.splitext(filename)
|
|
1787
|
+
# Glob all candidates
|
|
1788
|
+
candidate_files = glob(os.path.join(outs_dir, "**", f"*{base_name}*{ext}"), recursive=True)
|
|
1789
|
+
|
|
1790
|
+
# Regex: basename or basename-YYYYMMDD-hhmmss.ext
|
|
1791
|
+
ts_pattern = re.compile(rf"^{re.escape(base_name)}(?:-\d{{8}}-\d{{6}})?{re.escape(ext)}$")
|
|
1792
|
+
|
|
1793
|
+
files = [f for f in candidate_files if ts_pattern.search(os.path.basename(f))]
|
|
1794
|
+
files = natsorted(files)
|
|
1795
|
+
|
|
1644
1796
|
if not files:
|
|
1645
1797
|
_write('WARN: No output files in "%s"' % outs_dir, Color.YELLOW)
|
|
1646
1798
|
return ""
|
|
@@ -1691,19 +1843,9 @@ def _glob_escape(pathname):
|
|
|
1691
1843
|
return drive + pathname
|
|
1692
1844
|
|
|
1693
1845
|
|
|
1694
|
-
def _writer():
|
|
1695
|
-
while True:
|
|
1696
|
-
message = MESSAGE_QUEUE.get()
|
|
1697
|
-
if message is None:
|
|
1698
|
-
MESSAGE_QUEUE.task_done()
|
|
1699
|
-
return
|
|
1700
|
-
print(message)
|
|
1701
|
-
sys.stdout.flush()
|
|
1702
|
-
MESSAGE_QUEUE.task_done()
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
1846
|
def _write(message, color=None):
|
|
1706
|
-
|
|
1847
|
+
writer = get_writer()
|
|
1848
|
+
writer.write(message, color=color)
|
|
1707
1849
|
|
|
1708
1850
|
|
|
1709
1851
|
def _wrap_with(color, message):
|
|
@@ -1716,19 +1858,19 @@ def _is_output_coloring_supported():
|
|
|
1716
1858
|
return sys.stdout.isatty() and os.name in Color.SUPPORTED_OSES
|
|
1717
1859
|
|
|
1718
1860
|
|
|
1719
|
-
def
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1861
|
+
def _is_port_available(port):
|
|
1862
|
+
"""Check if a given port on localhost is available."""
|
|
1863
|
+
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
|
|
1864
|
+
try:
|
|
1865
|
+
s.bind(("localhost", port))
|
|
1866
|
+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
1867
|
+
return True
|
|
1868
|
+
except OSError:
|
|
1869
|
+
return False
|
|
1727
1870
|
|
|
1728
1871
|
|
|
1729
|
-
def _get_free_port(
|
|
1730
|
-
|
|
1731
|
-
return pabot_args["pabotlibport"]
|
|
1872
|
+
def _get_free_port():
|
|
1873
|
+
"""Return a free TCP port on localhost."""
|
|
1732
1874
|
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
|
|
1733
1875
|
s.bind(("localhost", 0))
|
|
1734
1876
|
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
@@ -1737,29 +1879,43 @@ def _get_free_port(pabot_args):
|
|
|
1737
1879
|
|
|
1738
1880
|
def _start_remote_library(pabot_args): # type: (dict) -> Optional[subprocess.Popen]
|
|
1739
1881
|
global _PABOTLIBURI
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
if not pabot_args["pabotlib"]:
|
|
1882
|
+
# If pabotlib is not enabled, do nothing
|
|
1883
|
+
if not pabot_args.get("pabotlib"):
|
|
1743
1884
|
return None
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
)
|
|
1885
|
+
|
|
1886
|
+
host = pabot_args.get("pabotlibhost", "127.0.0.1")
|
|
1887
|
+
port = pabot_args.get("pabotlibport", 8270)
|
|
1888
|
+
|
|
1889
|
+
# If host is default and user specified a non-zero port, check if it's available
|
|
1890
|
+
if host == "127.0.0.1" and port != 0 and not _is_port_available(port):
|
|
1891
|
+
_write(
|
|
1892
|
+
f"Warning: specified pabotlibport {port} is already in use. "
|
|
1893
|
+
"A free port will be assigned automatically.",
|
|
1894
|
+
Color.YELLOW,
|
|
1895
|
+
)
|
|
1896
|
+
port = _get_free_port()
|
|
1897
|
+
|
|
1898
|
+
# If host is default and port = 0, assign a free port
|
|
1899
|
+
if host == "127.0.0.1" and port == 0:
|
|
1900
|
+
port = _get_free_port()
|
|
1901
|
+
|
|
1902
|
+
_PABOTLIBURI = f"{host}:{port}"
|
|
1903
|
+
resourcefile = pabot_args.get("resourcefile") or ""
|
|
1904
|
+
if resourcefile and not os.path.exists(resourcefile):
|
|
1747
1905
|
_write(
|
|
1748
1906
|
"Warning: specified resource file doesn't exist."
|
|
1749
1907
|
" Some tests may fail or continue forever.",
|
|
1750
1908
|
Color.YELLOW,
|
|
1751
1909
|
)
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
shell=True,
|
|
1762
|
-
)
|
|
1910
|
+
resourcefile = ""
|
|
1911
|
+
cmd = [
|
|
1912
|
+
sys.executable,
|
|
1913
|
+
"-m", pabotlib.__name__,
|
|
1914
|
+
resourcefile,
|
|
1915
|
+
pabot_args["pabotlibhost"],
|
|
1916
|
+
str(port),
|
|
1917
|
+
]
|
|
1918
|
+
return subprocess.Popen(cmd)
|
|
1763
1919
|
|
|
1764
1920
|
|
|
1765
1921
|
def _stop_remote_library(process): # type: (subprocess.Popen) -> None
|
|
@@ -1807,8 +1963,9 @@ class QueueItem(object):
|
|
|
1807
1963
|
hive=None,
|
|
1808
1964
|
processes=0,
|
|
1809
1965
|
timeout=None,
|
|
1966
|
+
skip=False,
|
|
1810
1967
|
):
|
|
1811
|
-
# type: (List[str], str, Dict[str, object], ExecutionItem, List[str], bool, Tuple[str, Optional[str]], Optional[str], int, Optional[int]) -> None
|
|
1968
|
+
# type: (List[str], str, Dict[str, object], ExecutionItem, List[str], bool, Tuple[str, Optional[str]], Optional[str], int, Optional[int], bool) -> None
|
|
1812
1969
|
self.datasources = datasources
|
|
1813
1970
|
self.outs_dir = (
|
|
1814
1971
|
outs_dir.encode("utf-8") if PY2 and is_unicode(outs_dir) else outs_dir
|
|
@@ -1828,6 +1985,7 @@ class QueueItem(object):
|
|
|
1828
1985
|
self.processes = processes
|
|
1829
1986
|
self.timeout = timeout
|
|
1830
1987
|
self.sleep_before_start = execution_item.get_sleep()
|
|
1988
|
+
self.skip = skip
|
|
1831
1989
|
|
|
1832
1990
|
@property
|
|
1833
1991
|
def index(self):
|
|
@@ -2022,7 +2180,16 @@ def _get_dynamically_created_execution_items(
|
|
|
2022
2180
|
if not _pabotlib_in_use():
|
|
2023
2181
|
return None
|
|
2024
2182
|
plib = Remote(_PABOTLIBURI)
|
|
2025
|
-
|
|
2183
|
+
try:
|
|
2184
|
+
new_suites = plib.run_keyword("get_added_suites", [], {})
|
|
2185
|
+
except RuntimeError as err:
|
|
2186
|
+
_write(
|
|
2187
|
+
"[WARN] PabotLib unreachable during post-run phase, "
|
|
2188
|
+
"assuming no dynamically added suites. "
|
|
2189
|
+
"Original error: %s",
|
|
2190
|
+
err,
|
|
2191
|
+
)
|
|
2192
|
+
new_suites = []
|
|
2026
2193
|
if len(new_suites) == 0:
|
|
2027
2194
|
return None
|
|
2028
2195
|
suite_group = [DynamicSuiteItem(s, v) for s, v in new_suites]
|
|
@@ -2054,6 +2221,7 @@ def main(args=None):
|
|
|
2054
2221
|
|
|
2055
2222
|
def main_program(args):
|
|
2056
2223
|
global _PABOTLIBPROCESS
|
|
2224
|
+
outs_dir = None
|
|
2057
2225
|
args = args or sys.argv[1:]
|
|
2058
2226
|
if len(args) == 0:
|
|
2059
2227
|
print(
|
|
@@ -2067,7 +2235,6 @@ def main_program(args):
|
|
|
2067
2235
|
start_time_string = _now()
|
|
2068
2236
|
# NOTE: timeout option
|
|
2069
2237
|
try:
|
|
2070
|
-
_start_message_writer()
|
|
2071
2238
|
options, datasources, pabot_args, opts_for_run = parse_args(args)
|
|
2072
2239
|
if pabot_args["help"]:
|
|
2073
2240
|
help_print = __doc__.replace(
|
|
@@ -2075,15 +2242,22 @@ def main_program(args):
|
|
|
2075
2242
|
read_args_from_readme()
|
|
2076
2243
|
)
|
|
2077
2244
|
print(help_print.replace("[PABOT_VERSION]", PABOT_VERSION))
|
|
2078
|
-
return
|
|
2245
|
+
return 251
|
|
2079
2246
|
if len(datasources) == 0:
|
|
2080
2247
|
print("[ " + _wrap_with(Color.RED, "ERROR") + " ]: No datasources given.")
|
|
2081
2248
|
print("Try --help for usage information.")
|
|
2082
2249
|
return 252
|
|
2250
|
+
outs_dir = _output_dir(options)
|
|
2251
|
+
|
|
2252
|
+
# These ensure MessageWriter and ProcessManager are ready before any parallel execution.
|
|
2253
|
+
writer = get_writer(log_dir=outs_dir)
|
|
2254
|
+
_ensure_process_manager()
|
|
2255
|
+
_write(f"Initialized logging in {outs_dir}")
|
|
2256
|
+
|
|
2083
2257
|
_PABOTLIBPROCESS = _start_remote_library(pabot_args)
|
|
2084
2258
|
if _pabotlib_in_use():
|
|
2085
2259
|
_initialize_queue_index()
|
|
2086
|
-
|
|
2260
|
+
|
|
2087
2261
|
suite_groups = _group_suites(outs_dir, datasources, options, pabot_args)
|
|
2088
2262
|
if pabot_args["verbose"]:
|
|
2089
2263
|
_write("Suite names resolved in %s seconds" % str(time.time() - start_time))
|
|
@@ -2094,16 +2268,30 @@ def main_program(args):
|
|
|
2094
2268
|
execution_items = _create_execution_items(
|
|
2095
2269
|
suite_groups, datasources, outs_dir, options, opts_for_run, pabot_args
|
|
2096
2270
|
)
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2271
|
+
if pabot_args.get("ordering", {}).get("mode") == "dynamic":
|
|
2272
|
+
# flatten stages
|
|
2273
|
+
all_items = []
|
|
2274
|
+
for stage in execution_items:
|
|
2275
|
+
all_items.extend(stage)
|
|
2276
|
+
_parallel_execute_dynamic(
|
|
2277
|
+
all_items,
|
|
2101
2278
|
pabot_args["processes"],
|
|
2102
2279
|
datasources,
|
|
2103
2280
|
outs_dir,
|
|
2104
2281
|
opts_for_run,
|
|
2105
2282
|
pabot_args,
|
|
2106
2283
|
)
|
|
2284
|
+
else:
|
|
2285
|
+
while execution_items:
|
|
2286
|
+
items = execution_items.pop(0)
|
|
2287
|
+
_parallel_execute(
|
|
2288
|
+
items,
|
|
2289
|
+
pabot_args["processes"],
|
|
2290
|
+
datasources,
|
|
2291
|
+
outs_dir,
|
|
2292
|
+
opts_for_run,
|
|
2293
|
+
pabot_args,
|
|
2294
|
+
)
|
|
2107
2295
|
if pabot_args["no-rebot"]:
|
|
2108
2296
|
_write((
|
|
2109
2297
|
"All tests were executed, but the --no-rebot argument was given, "
|
|
@@ -2111,7 +2299,7 @@ def main_program(args):
|
|
|
2111
2299
|
f"All results have been saved in the {outs_dir} folder."
|
|
2112
2300
|
))
|
|
2113
2301
|
_write("===================================================")
|
|
2114
|
-
return
|
|
2302
|
+
return 253
|
|
2115
2303
|
result_code = _report_results(
|
|
2116
2304
|
outs_dir,
|
|
2117
2305
|
pabot_args,
|
|
@@ -2124,24 +2312,62 @@ def main_program(args):
|
|
|
2124
2312
|
version_print = __doc__.replace("\nPLACEHOLDER_README.MD\n", "")
|
|
2125
2313
|
print(version_print.replace("[PABOT_VERSION]", PABOT_VERSION))
|
|
2126
2314
|
print(i.message)
|
|
2315
|
+
return 251
|
|
2127
2316
|
except DataError as err:
|
|
2128
2317
|
print(err.message)
|
|
2129
2318
|
return 252
|
|
2130
2319
|
except Exception:
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2320
|
+
if not CTRL_C_PRESSED:
|
|
2321
|
+
_write("[ERROR] EXCEPTION RAISED DURING PABOT EXECUTION", Color.RED)
|
|
2322
|
+
_write(
|
|
2323
|
+
"[ERROR] PLEASE CONSIDER REPORTING THIS ISSUE TO https://github.com/mkorpela/pabot/issues",
|
|
2324
|
+
Color.RED,
|
|
2325
|
+
)
|
|
2326
|
+
_write("Pabot: %s" % PABOT_VERSION)
|
|
2327
|
+
_write("Python: %s" % sys.version)
|
|
2328
|
+
_write("Robot Framework: %s" % ROBOT_VERSION)
|
|
2329
|
+
import traceback
|
|
2330
|
+
traceback.print_exc()
|
|
2331
|
+
sys.exit(255)
|
|
2332
|
+
else:
|
|
2333
|
+
_write("[ERROR] Execution stopped by user (Ctrl+C)", Color.RED)
|
|
2334
|
+
sys.exit(253)
|
|
2140
2335
|
finally:
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2336
|
+
# Ensure that writer exists
|
|
2337
|
+
writer = None
|
|
2338
|
+
try:
|
|
2339
|
+
if outs_dir is not None:
|
|
2340
|
+
writer = get_writer(log_dir=outs_dir)
|
|
2341
|
+
except Exception as e:
|
|
2342
|
+
print(f"[WARN] Could not initialize writer in finally: {e}")
|
|
2343
|
+
# Try to stop remote library
|
|
2344
|
+
try:
|
|
2345
|
+
if _PABOTLIBPROCESS:
|
|
2346
|
+
_stop_remote_library(_PABOTLIBPROCESS)
|
|
2347
|
+
except Exception as e:
|
|
2348
|
+
if writer:
|
|
2349
|
+
writer.write(f"[WARN] Failed to stop remote library cleanly: {e}", Color.YELLOW)
|
|
2350
|
+
else:
|
|
2351
|
+
print(f"[WARN] Failed to stop remote library cleanly: {e}")
|
|
2352
|
+
# print elapsed time
|
|
2353
|
+
try:
|
|
2354
|
+
_print_elapsed(start_time, time.time())
|
|
2355
|
+
except Exception as e:
|
|
2356
|
+
if writer:
|
|
2357
|
+
writer.write(f"[WARN] Failed to print elapsed time: {e}", Color.YELLOW)
|
|
2358
|
+
else:
|
|
2359
|
+
print(f"[WARN] Failed to print elapsed time: {e}")
|
|
2360
|
+
# Flush and stop writer
|
|
2361
|
+
if writer:
|
|
2362
|
+
try:
|
|
2363
|
+
writer.flush()
|
|
2364
|
+
writer.write("Logs flushed successfully.")
|
|
2365
|
+
except Exception as e:
|
|
2366
|
+
print(f"[WARN] Could not flush writer: {e}")
|
|
2367
|
+
try:
|
|
2368
|
+
writer.stop()
|
|
2369
|
+
except Exception as e:
|
|
2370
|
+
print(f"[WARN] Could not stop writer: {e}")
|
|
2145
2371
|
|
|
2146
2372
|
|
|
2147
2373
|
def _parse_ordering(filename): # type: (str) -> List[ExecutionItem]
|
|
@@ -2189,7 +2415,7 @@ def _check_ordering(ordering_file, suite_names): # type: (List[ExecutionItem],
|
|
|
2189
2415
|
def _group_suites(outs_dir, datasources, options, pabot_args):
|
|
2190
2416
|
suite_names = solve_suite_names(outs_dir, datasources, options, pabot_args)
|
|
2191
2417
|
_verify_depends(suite_names)
|
|
2192
|
-
ordering_arg = _parse_ordering(pabot_args.get("ordering")) if (pabot_args.get("ordering")) is not None else None
|
|
2418
|
+
ordering_arg = _parse_ordering(pabot_args.get("ordering").get("file")) if (pabot_args.get("ordering")) is not None else None
|
|
2193
2419
|
if ordering_arg:
|
|
2194
2420
|
_verify_depends(ordering_arg)
|
|
2195
2421
|
if options.get("name"):
|