Skip to content

Commit d214566

Browse files
authored
Merge pull request #651 from joonaskuisma/enhancement
Major release: Improve subprocess handling, artifact management, and ordering logic
2 parents fd29ed2 + 63f7040 commit d214566

File tree

10 files changed

+634
-189
lines changed

10 files changed

+634
-189
lines changed

.github/workflows/update-status-label.yml

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ on:
44
issues:
55
types: [opened, closed]
66
pull_request:
7-
types: [opened, edited, synchronize, closed]
7+
types: [closed]
88

99
jobs:
1010
update-status:
@@ -76,24 +76,7 @@ jobs:
7676
const pr = context.payload.pull_request;
7777
const prNumber = pr.number;
7878
await removeOldStatusLabels(prNumber);
79-
80-
if (['opened', 'edited', 'synchronize'].includes(context.payload.action)) {
81-
const body = pr.body || "";
82-
const linkedIssue = body.match(/(?:fixes|closes|resolves)\s+#\d+/i);
83-
if (linkedIssue) {
84-
await github.rest.issues.addLabels({
85-
...context.repo,
86-
issue_number: prNumber,
87-
labels: [issueLabels.pr_submitted]
88-
});
89-
} else {
90-
await github.rest.issues.addLabels({
91-
...context.repo,
92-
issue_number: prNumber,
93-
labels: [issueLabels.needs_review]
94-
});
95-
}
96-
} else if (context.payload.action === 'closed' && pr.merged) {
79+
if (context.payload.action === 'closed' && pr.merged) {
9780
await github.rest.issues.addLabels({
9881
...context.repo,
9982
issue_number: prNumber,

README.md

Lines changed: 97 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,22 @@ A parallel executor for [Robot Framework](http://www.robotframework.org) tests.
1313

1414
[![Pabot presentation at robocon.io 2018](http://img.youtube.com/vi/i0RV6SJSIn8/0.jpg)](https://youtu.be/i0RV6SJSIn8 "Pabot presentation at robocon.io 2018")
1515

16+
## Table of Contents
17+
18+
- [Installation](#installation)
19+
- [Basic use](#basic-use)
20+
- [Contact](#contact)
21+
- [Contributing](#contributing-to-the-project)
22+
- [Command-line options](#command-line-options)
23+
- [PabotLib](#pabotlib)
24+
- [Controlling execution order](#controlling-execution-order-and-level-of-parallelism)
25+
- [Programmatic use](#programmatic-use)
26+
- [Global variables](#global-variables)
27+
- [Output Files Generated by Pabot](#output-files-generated-by-pabot)
28+
- [Artifacts Handling and Parallel Execution Notes](#artifacts-handling-and-parallel-execution-notes)
29+
30+
----
31+
1632
## Installation:
1733

1834
From PyPi:
@@ -245,11 +261,24 @@ Note: The `--ordering` file is intended only for defining the execution order of
245261
There different possibilities to influence the execution:
246262

247263
* The order of suites can be changed.
248-
* If a directory (or a directory structure) should be executed sequentially, add the directory suite name to a row as a ```--suite``` option.
249-
* 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.
264+
* If a directory (or a directory structure) should be executed sequentially, add the directory suite name to a row as a ```--suite``` option. This usage is also supported when `--testlevelsplit` is enabled. As an alternative to using `--suite` options, you can also group tests into sequential batches using `{}` braces. (See below for details.) Note that if multiple `--suite` options are used, they must not reference the same test case. This means you cannot specify both parent and child suite names at the same time. For instance:
265+
266+
```
267+
--suite Top Suite.Sub Suite
268+
--suite Top Suite
269+
```
270+
271+
* If the base suite name is changing with robot option [```--name / -N```](https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#setting-the-name) you can use either the new or old full test path. For example:
272+
273+
```
274+
--test New Suite Name.Sub Suite.Test 1
275+
OR
276+
--test Old Suite Name.Sub Suite.Test 1
277+
```
278+
250279
* You can add a line with text `#WAIT` to force executor to wait until all previous suites have been executed.
251280
* 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.
252-
* 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.
281+
* 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. The ordering algorithm is designed to preserve the exact user-defined order as closely as possible. However, if a test's execution dependencies are not yet satisfied, the test is postponed and moved to the earliest possible stage where all its dependencies are fulfilled. 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 `{}` braces.
253282
* 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.
254283
* An example could be:
255284

@@ -343,4 +372,68 @@ Pabot will insert following global variables to Robot Framework namespace. These
343372
PABOTEXECUTIONPOOLID - this contains the pool id (an integer) for the current Robot Framework executor. This is helpful for example when visualizing the execution flow from your own listener.
344373
PABOTNUMBEROFPROCESSES - max number of concurrent processes that pabot may use in execution.
345374
CALLER_ID - a universally unique identifier for this execution.
346-
375+
376+
377+
### Output Files Generated by Pabot
378+
379+
Pabot generates several output files and folders during execution, both for internal use and for analysis purposes.
380+
381+
#### Internal File: `.pabotsuitenames`
382+
383+
Pabot creates a `.pabotsuitenames` file in the working directory. This is an internal hash file used to speed up execution in certain scenarios.
384+
This file can also be used as a base for the `--ordering` file as described earlier. Although technically it can be modified, it will be overwritten during the next execution.
385+
Therefore, it is **recommended** to maintain a separate file for the `--ordering` option if needed.
386+
387+
#### Output Directory Structure
388+
389+
In addition to the standard `log.html`, `report.html`, and `output.xml` files, the specified `--outputdir` will contain:
390+
391+
- A folder named `pabot_results`, and
392+
- All defined artifacts (default: `.png` files)
393+
- Optionally, artifacts from subfolders if `--artifactsinsubfolders` is used
394+
395+
Artifacts are **copied** into the output directory and renamed with the following structure:
396+
397+
```
398+
TIMESTAMP-ARGUMENT_INDEX-PABOTQUEUEINDEX
399+
```
400+
401+
- **TIMESTAMP** = Time of `pabot` command invocation (not the screenshot's actual timestamp), format: `YYYYmmdd_HHMMSS`
402+
- **ARGUMENT_INDEX** = Optional index number, only used if `--argumentfileN` options are given
403+
- **PABOTQUEUEINDEX** = Process queue index (see section [Global Variables](#global-variables))
404+
405+
#### `pabot_results` Folder Structure
406+
407+
The structure of the `pabot_results` folder is as follows:
408+
409+
```
410+
pabot_results/
411+
├── [N]/ # Optional: N = argument file index (if --argumentfileN is used)
412+
│ └── PABOTQUEUEINDEX/ # One per subprocess
413+
│ ├── output.xml
414+
│ ├── robot_argfile.txt
415+
│ ├── robot_stdout.out
416+
│ ├── robot_stderr.out
417+
│ └── artifacts...
418+
```
419+
420+
Each `PABOTQUEUEINDEX` folder contains as default:
421+
422+
- `robot_argfile.txt` – Arguments used in that subprocess
423+
- `robot_stdout.out` and `robot_stderr.out` – Stdout and stderr of the subprocess
424+
- `output.xml` – The partial output file to be merged later
425+
- Artifacts – Screenshots or other files copied from subprocess folders
426+
427+
> **Note:** The entire `pabot_results` folder is considered temporary and will be **deleted/overwritten** on the next `pabot` run using the same `--outputdir`.
428+
429+
430+
### Artifacts Handling and Parallel Execution Notes
431+
432+
Due to parallel execution, artifacts like screenshots should ideally be:
433+
434+
- Embedded directly into the XML using tools like [SeleniumLibrary](https://robotframework.org/SeleniumLibrary/SeleniumLibrary.html#Set%20Screenshot%20Directory) with the `EMBED` option
435+
_Example:_
436+
`Library SeleniumLibrary screenshot_root_directory=EMBED`
437+
- Or saved to the subprocess’s working directory (usually default behavior), ensuring separation across processes
438+
439+
If you manually specify a shared screenshot directory in your test code, **all processes will write to it concurrently**, which may cause issues such as overwriting or missing files if screenshots are taken simultaneously.

src/pabot/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@
77
except ImportError:
88
pass
99

10-
__version__ = "4.3.2"
10+
__version__ = "5.0.0"

src/pabot/execution_items.py

Lines changed: 67 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from functools import total_ordering
2-
from typing import Dict, List, Optional, Tuple, Union
2+
from typing import Dict, List, Optional, Tuple, Union, Set
33

44
from robot import __version__ as ROBOT_VERSION
55
from robot.errors import DataError
@@ -8,36 +8,68 @@
88
import re
99

1010

11-
def create_dependency_tree(items):
11+
def create_dependency_tree(items):
1212
# 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)
13+
dependency_tree = [] # type: List[List[ExecutionItem]]
14+
scheduled = set() # type: Set[str]
15+
name_to_item = {item.name: item for item in items} # type: Dict[str, ExecutionItem]
16+
17+
while items:
18+
stage = [] #type: List[ExecutionItem]
19+
stage_names = set() # type: Set[str]
20+
21+
for item in items:
22+
if all(dep in scheduled for dep in item.depends):
23+
stage.append(item)
24+
stage_names.add(item.name)
2825
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-
)
26+
break # Preserve input order
27+
28+
if not stage:
29+
# Try to find any schedulable item even if it's out of order
30+
for item in items:
31+
if all(dep in scheduled for dep in item.depends):
32+
stage = [item]
33+
stage_names = {item.name}
34+
break
35+
36+
if not stage:
37+
# Prepare a detailed error message
38+
unscheduled_items = [item.name for item in items]
39+
unsatisfied_deps = {
40+
item.name: [d for d in item.depends if d not in scheduled and d not in name_to_item]
41+
for item in items
42+
}
43+
potential_cycles = {
44+
item.name: [d for d in item.depends if d in unscheduled_items]
45+
for item in items if item.depends
46+
}
47+
48+
message = ["Invalid test configuration:"]
49+
50+
message_unsatisfied = []
51+
for item, deps in unsatisfied_deps.items():
52+
if deps:
53+
message_unsatisfied.append(f" - {item} depends on missing: {', '.join(deps)}")
54+
if message_unsatisfied:
55+
message.append(" Unsatisfied dependencies:")
56+
message.extend(message_unsatisfied)
57+
message.append(" For these tests, check that there is not #WAIT between them and that they are not inside different groups { }")
58+
59+
message_cycles = []
60+
for item, deps in potential_cycles.items():
61+
if deps:
62+
message_cycles.append(f" - {item} <-> {', '.join(deps)}")
63+
if message_cycles:
64+
message.append(" Possible circular dependencies:")
65+
message.extend(message_cycles)
66+
67+
raise DataError("\n".join(message))
68+
69+
dependency_tree.append(stage)
70+
scheduled.update(stage_names)
71+
items = [item for item in items if item.name not in stage_names]
72+
4173
return dependency_tree
4274

4375

@@ -47,6 +79,7 @@ class ExecutionItem(object):
4779
type = None # type: str
4880
name = None # type: str
4981
sleep = 0 # type: int
82+
depends = [] # type: List[str] # Note that depends is used by RunnableItems.
5083

5184
def top_name(self):
5285
# type: () -> str
@@ -156,7 +189,6 @@ def modify_options_for_executor(self, options):
156189
class RunnableItem(ExecutionItem):
157190
pass
158191

159-
depends = None # type: List[str]
160192
depends_keyword = "#DEPENDS"
161193

162194
def _split_dependencies(self, line_name, depends_indexes):
@@ -182,7 +214,7 @@ def set_name_and_depends(self, name):
182214
self.depends = (
183215
self._split_dependencies(line_name, depends_indexes)
184216
if len(depends_indexes) != 0
185-
else None
217+
else []
186218
)
187219

188220
def line(self):
@@ -243,6 +275,10 @@ def tags(self):
243275
# TODO Make this happen
244276
return []
245277

278+
def modify_options_for_executor(self, options):
279+
if not(options.get("runemptysuite") and options.get("suite")):
280+
options[self.type] = self.name
281+
246282

247283
class TestItem(RunnableItem):
248284
type = "test"

0 commit comments

Comments
 (0)