robotframework-pabot 5.0.0__tar.gz → 5.1.0__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 (46) hide show
  1. {robotframework_pabot-5.0.0/src/robotframework_pabot.egg-info → robotframework_pabot-5.1.0}/PKG-INFO +48 -23
  2. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/README.md +47 -22
  3. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/src/pabot/__init__.py +1 -1
  4. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/src/pabot/arguments.py +13 -1
  5. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/src/pabot/pabot.py +85 -37
  6. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/src/pabot/pabotlib.py +1 -1
  7. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/src/pabot/result_merger.py +2 -2
  8. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0/src/robotframework_pabot.egg-info}/PKG-INFO +48 -23
  9. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/tests/test_missing_subprocess_output.py +1 -0
  10. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/tests/test_pabot.py +34 -0
  11. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/tests/test_pabotprerunmodifier.py +4 -1
  12. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/LICENSE.txt +0 -0
  13. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/MANIFEST.in +0 -0
  14. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/pyproject.toml +0 -0
  15. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/setup.cfg +0 -0
  16. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/setup.py +0 -0
  17. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/src/pabot/SharedLibrary.py +0 -0
  18. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/src/pabot/clientwrapper.py +0 -0
  19. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/src/pabot/coordinatorwrapper.py +0 -0
  20. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/src/pabot/execution_items.py +0 -0
  21. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/src/pabot/py3/__init__.py +0 -0
  22. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/src/pabot/py3/client.py +0 -0
  23. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/src/pabot/py3/coordinator.py +0 -0
  24. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/src/pabot/py3/messages.py +0 -0
  25. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/src/pabot/py3/worker.py +0 -0
  26. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/src/pabot/robotremoteserver.py +0 -0
  27. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/src/pabot/workerwrapper.py +0 -0
  28. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/src/robotframework_pabot.egg-info/SOURCES.txt +0 -0
  29. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/src/robotframework_pabot.egg-info/dependency_links.txt +0 -0
  30. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/src/robotframework_pabot.egg-info/entry_points.txt +0 -0
  31. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/src/robotframework_pabot.egg-info/requires.txt +0 -0
  32. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/src/robotframework_pabot.egg-info/top_level.txt +0 -0
  33. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/tests/test_arguments_output.py +0 -0
  34. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/tests/test_basic_arguments.py +0 -0
  35. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/tests/test_depends.py +0 -0
  36. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/tests/test_functional.py +0 -0
  37. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/tests/test_ordering.py +0 -0
  38. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/tests/test_pabotlib.py +0 -0
  39. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/tests/test_pabotsuitenames_io.py +0 -0
  40. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/tests/test_prerunmodifier.py +0 -0
  41. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/tests/test_resultmerger.py +0 -0
  42. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/tests/test_run_empty_suite.py +0 -0
  43. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/tests/test_stacktrace.py +0 -0
  44. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/tests/test_suite_structure.py +0 -0
  45. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/tests/test_testlevelsplit_include.py +0 -0
  46. {robotframework_pabot-5.0.0 → robotframework_pabot-5.1.0}/tests/test_testlevelsplit_output_task_order.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: robotframework-pabot
3
- Version: 5.0.0
3
+ Version: 5.1.0
4
4
  Summary: Parallel test runner for Robot Framework
5
5
  Home-page: https://pabot.org
6
6
  Download-URL: https://pypi.python.org/pypi/robotframework-pabot
@@ -99,7 +99,16 @@ There are several ways you can help in improving this tool:
99
99
  - Contribute by programming and making a pull request (easiest way is to work on an issue from the issue tracker)
100
100
 
101
101
  ## Command-line options
102
+ <!-- NOTE:
103
+ The sections inside these docstring markers are also used in Pabot's --help output.
104
+ Currently, the following transformations are applied:
105
+ - Remove Markdown links but keep the text
106
+ - Remove ** and backticks `
107
+
108
+ If you modify this part, make sure the Markdown section still looks clean and readable in the --help output. -->
109
+
102
110
  <!-- START DOCSTRING -->
111
+ ```
103
112
  pabot [--verbose|--testlevelsplit|--command .. --end-command|
104
113
  --processes num|--no-pabotlib|--pabotlibhost host|--pabotlibport port|
105
114
  --processtimeout num|
@@ -111,52 +120,60 @@ pabot [--verbose|--testlevelsplit|--command .. --end-command|
111
120
  --no-rebot|
112
121
  --help|--version]
113
122
  [robot options] [path ...]
123
+ ```
114
124
 
115
125
  PabotLib remote server is started by default to enable locking and resource distribution between parallel test executions.
116
126
 
117
127
  Supports all [Robot Framework command line options](https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#all-command-line-options) and also following pabot options:
118
128
 
119
- --verbose
129
+ **--verbose**
120
130
  More output from the parallel execution.
121
131
 
122
- --testlevelsplit
132
+ **--testlevelsplit**
123
133
  Split execution on test level instead of default suite level. If .pabotsuitenames contains both tests and suites then
124
134
  this will only affect new suites and split only them. Leaving this flag out when both suites and tests in
125
135
  .pabotsuitenames file will also only affect new suites and add them as suite files.
126
136
 
127
- --command [ACTUAL COMMANDS TO START ROBOT EXECUTOR] --end-command
137
+ **--command [ACTUAL COMMANDS TO START ROBOT EXECUTOR] --end-command**
128
138
  RF script for situations where robot is not used directly.
129
139
 
130
- --processes [NUMBER OF PROCESSES]
140
+ **--processes [NUMBER OF PROCESSES]**
131
141
  How many parallel executors to use (default max of 2 and cpu count). Special option "all" will use as many processes as
132
142
  there are executable suites or tests.
133
143
 
134
- --no-pabotlib
144
+ **--no-pabotlib**
135
145
  Disable the PabotLib remote server if you don't need locking or resource distribution features.
136
146
 
137
- --pabotlibhost [HOSTNAME]
147
+ **--pabotlibhost [HOSTNAME]**
138
148
  Connect to an already running instance of the PabotLib remote server at the given host (disables the local PabotLib
139
149
  server start). For example, to connect to a remote PabotLib server running on another machine:
140
150
 
141
151
  pabot --pabotlibhost 192.168.1.123 --pabotlibport 8271 tests/
142
152
 
143
- The remote server can be also started and executed separately from pabot instances:
153
+ The remote server can also be started and executed separately from pabot instances:
144
154
 
145
155
  python -m pabot.pabotlib <path_to_resourcefile> <host> <port>
146
156
  python -m pabot.pabotlib resource.txt 192.168.1.123 8271
147
157
 
148
158
  This enables sharing a resource with multiple Robot Framework instances.
149
159
 
150
- --pabotlibport [PORT]
160
+ Additional details:
161
+ - The default value for --pabotlibhost is 127.0.0.1.
162
+ - If you provide a hostname other than 127.0.0.1, the local PabotLib server startup is automatically disabled.
163
+
164
+ **--pabotlibport [PORT]**
151
165
  Port number of the PabotLib remote server (default is 8270). See --pabotlibhost for more information.
152
166
 
153
- --processtimeout [TIMEOUT]
167
+ Behavior with port and host settings:
168
+ - If you set the port value to 0 and --pabotlibhost is 127.0.0.1 (default), a free port on localhost will be assigned automatically.
169
+
170
+ **--processtimeout [TIMEOUT]**
154
171
  Maximum time in seconds to wait for a process before killing it. If not set, there's no timeout.
155
172
 
156
- --shard [INDEX]/[TOTAL]
173
+ **--shard [INDEX]/[TOTAL]**
157
174
  Optionally split execution into smaller pieces. This can be used for distributing testing to multiple machines.
158
175
 
159
- --artifacts [FILE EXTENSIONS]
176
+ **--artifacts [FILE EXTENSIONS]**
160
177
  List of file extensions (comma separated). Defines which files (screenshots, videos etc.) from separate reporting
161
178
  directories would be copied and included in a final report. Possible links to copied files in RF log would be updated
162
179
  (only relative paths supported). The default value is `png`.
@@ -165,49 +182,51 @@ Supports all [Robot Framework command line options](https://robotframework.org/r
165
182
 
166
183
  --artifacts png,mp4,txt
167
184
 
168
- --artifactsinsubfolders
185
+ The artifact naming conventions are described in the README.md section: [Output Files Generated by Pabot](#output-files-generated-by-pabot).
186
+
187
+ **--artifactsinsubfolders**
169
188
  Copy artifacts located not only directly in the RF output dir, but also in it's sub-folders.
170
189
 
171
- --resourcefile [FILEPATH]
190
+ **--resourcefile [FILEPATH]**
172
191
  Indicator for a file that can contain shared variables for distributing resources. This needs to be used together with
173
192
  pabotlib option. Resource file syntax is same as Windows ini files. Where a section is a shared set of variables.
174
193
 
175
- --argumentfile[INTEGER] [FILEPATH]
194
+ **--argumentfile[INTEGER] [FILEPATH]**
176
195
  Run same suites with multiple [argumentfile](http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#argument-files) options.
177
196
 
178
197
  For example:
179
198
 
180
199
  --argumentfile1 arg1.txt --argumentfile2 arg2.txt
181
200
 
182
- --suitesfrom [FILEPATH TO OUTPUTXML]
201
+ **--suitesfrom [FILEPATH TO OUTPUTXML]**
183
202
  Optionally read suites from output.xml file. Failed suites will run first and longer running ones will be executed
184
203
  before shorter ones.
185
204
 
186
- --ordering [FILE PATH]
205
+ **--ordering [FILE PATH]**
187
206
  Optionally give execution order from a file.
188
207
 
189
- --chunk
208
+ **--chunk**
190
209
  Optionally chunk tests to PROCESSES number of robot runs. This can save time because all the suites will share the same
191
210
  setups and teardowns.
192
211
 
193
- --pabotprerunmodifier [PRERUNMODIFIER MODULE OR CLASS]
212
+ **--pabotprerunmodifier [PRERUNMODIFIER MODULE OR CLASS]**
194
213
  Like Robot Framework's --prerunmodifier, but executed only once in the pabot's main process after all other
195
214
  --prerunmodifiers. But unlike the regular --prerunmodifier command, --pabotprerunmodifier is not executed again in each
196
215
  pabot subprocesses. Depending on the intended use, this may be desirable as well as more efficient. Can be used, for
197
216
  example, to modify the list of tests to be performed.
198
217
 
199
- --no-rebot
218
+ **--no-rebot**
200
219
  If specified, the tests will execute as usual, but Rebot will not be called to merge the logs. This option is designed
201
220
  for scenarios where Rebot should be run later due to large log files, ensuring better memory and resource availability.
202
221
  Subprocess results are stored in the pabot_results folder.
203
222
 
204
- --help
223
+ **--help**
205
224
  Print usage instructions.
206
225
 
207
- --version
226
+ **--version**
208
227
  Print version information.
209
228
 
210
- Example usages:
229
+ **Example usages:**
211
230
 
212
231
  pabot test_directory
213
232
  pabot --exclude FOO directory_to_tests
@@ -424,6 +443,12 @@ Artifacts are **copied** into the output directory and renamed with the followin
424
443
  TIMESTAMP-ARGUMENT_INDEX-PABOTQUEUEINDEX
425
444
  ```
426
445
 
446
+ If you use the special option `notimestamps` at the end of the `--artifacts` command, (For example: `--artifacts png,txt,notimestamps`) the timestamp part will be omitted, and the name will be in the format:
447
+
448
+ ```
449
+ ARGUMENT_INDEX-PABOTQUEUEINDEX
450
+ ```
451
+
427
452
  - **TIMESTAMP** = Time of `pabot` command invocation (not the screenshot's actual timestamp), format: `YYYYmmdd_HHMMSS`
428
453
  - **ARGUMENT_INDEX** = Optional index number, only used if `--argumentfileN` options are given
429
454
  - **PABOTQUEUEINDEX** = Process queue index (see section [Global Variables](#global-variables))
@@ -73,7 +73,16 @@ There are several ways you can help in improving this tool:
73
73
  - Contribute by programming and making a pull request (easiest way is to work on an issue from the issue tracker)
74
74
 
75
75
  ## Command-line options
76
+ <!-- NOTE:
77
+ The sections inside these docstring markers are also used in Pabot's --help output.
78
+ Currently, the following transformations are applied:
79
+ - Remove Markdown links but keep the text
80
+ - Remove ** and backticks `
81
+
82
+ If you modify this part, make sure the Markdown section still looks clean and readable in the --help output. -->
83
+
76
84
  <!-- START DOCSTRING -->
85
+ ```
77
86
  pabot [--verbose|--testlevelsplit|--command .. --end-command|
78
87
  --processes num|--no-pabotlib|--pabotlibhost host|--pabotlibport port|
79
88
  --processtimeout num|
@@ -85,52 +94,60 @@ pabot [--verbose|--testlevelsplit|--command .. --end-command|
85
94
  --no-rebot|
86
95
  --help|--version]
87
96
  [robot options] [path ...]
97
+ ```
88
98
 
89
99
  PabotLib remote server is started by default to enable locking and resource distribution between parallel test executions.
90
100
 
91
101
  Supports all [Robot Framework command line options](https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#all-command-line-options) and also following pabot options:
92
102
 
93
- --verbose
103
+ **--verbose**
94
104
  More output from the parallel execution.
95
105
 
96
- --testlevelsplit
106
+ **--testlevelsplit**
97
107
  Split execution on test level instead of default suite level. If .pabotsuitenames contains both tests and suites then
98
108
  this will only affect new suites and split only them. Leaving this flag out when both suites and tests in
99
109
  .pabotsuitenames file will also only affect new suites and add them as suite files.
100
110
 
101
- --command [ACTUAL COMMANDS TO START ROBOT EXECUTOR] --end-command
111
+ **--command [ACTUAL COMMANDS TO START ROBOT EXECUTOR] --end-command**
102
112
  RF script for situations where robot is not used directly.
103
113
 
104
- --processes [NUMBER OF PROCESSES]
114
+ **--processes [NUMBER OF PROCESSES]**
105
115
  How many parallel executors to use (default max of 2 and cpu count). Special option "all" will use as many processes as
106
116
  there are executable suites or tests.
107
117
 
108
- --no-pabotlib
118
+ **--no-pabotlib**
109
119
  Disable the PabotLib remote server if you don't need locking or resource distribution features.
110
120
 
111
- --pabotlibhost [HOSTNAME]
121
+ **--pabotlibhost [HOSTNAME]**
112
122
  Connect to an already running instance of the PabotLib remote server at the given host (disables the local PabotLib
113
123
  server start). For example, to connect to a remote PabotLib server running on another machine:
114
124
 
115
125
  pabot --pabotlibhost 192.168.1.123 --pabotlibport 8271 tests/
116
126
 
117
- The remote server can be also started and executed separately from pabot instances:
127
+ The remote server can also be started and executed separately from pabot instances:
118
128
 
119
129
  python -m pabot.pabotlib <path_to_resourcefile> <host> <port>
120
130
  python -m pabot.pabotlib resource.txt 192.168.1.123 8271
121
131
 
122
132
  This enables sharing a resource with multiple Robot Framework instances.
123
133
 
124
- --pabotlibport [PORT]
134
+ Additional details:
135
+ - The default value for --pabotlibhost is 127.0.0.1.
136
+ - If you provide a hostname other than 127.0.0.1, the local PabotLib server startup is automatically disabled.
137
+
138
+ **--pabotlibport [PORT]**
125
139
  Port number of the PabotLib remote server (default is 8270). See --pabotlibhost for more information.
126
140
 
127
- --processtimeout [TIMEOUT]
141
+ Behavior with port and host settings:
142
+ - If you set the port value to 0 and --pabotlibhost is 127.0.0.1 (default), a free port on localhost will be assigned automatically.
143
+
144
+ **--processtimeout [TIMEOUT]**
128
145
  Maximum time in seconds to wait for a process before killing it. If not set, there's no timeout.
129
146
 
130
- --shard [INDEX]/[TOTAL]
147
+ **--shard [INDEX]/[TOTAL]**
131
148
  Optionally split execution into smaller pieces. This can be used for distributing testing to multiple machines.
132
149
 
133
- --artifacts [FILE EXTENSIONS]
150
+ **--artifacts [FILE EXTENSIONS]**
134
151
  List of file extensions (comma separated). Defines which files (screenshots, videos etc.) from separate reporting
135
152
  directories would be copied and included in a final report. Possible links to copied files in RF log would be updated
136
153
  (only relative paths supported). The default value is `png`.
@@ -139,49 +156,51 @@ Supports all [Robot Framework command line options](https://robotframework.org/r
139
156
 
140
157
  --artifacts png,mp4,txt
141
158
 
142
- --artifactsinsubfolders
159
+ The artifact naming conventions are described in the README.md section: [Output Files Generated by Pabot](#output-files-generated-by-pabot).
160
+
161
+ **--artifactsinsubfolders**
143
162
  Copy artifacts located not only directly in the RF output dir, but also in it's sub-folders.
144
163
 
145
- --resourcefile [FILEPATH]
164
+ **--resourcefile [FILEPATH]**
146
165
  Indicator for a file that can contain shared variables for distributing resources. This needs to be used together with
147
166
  pabotlib option. Resource file syntax is same as Windows ini files. Where a section is a shared set of variables.
148
167
 
149
- --argumentfile[INTEGER] [FILEPATH]
168
+ **--argumentfile[INTEGER] [FILEPATH]**
150
169
  Run same suites with multiple [argumentfile](http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#argument-files) options.
151
170
 
152
171
  For example:
153
172
 
154
173
  --argumentfile1 arg1.txt --argumentfile2 arg2.txt
155
174
 
156
- --suitesfrom [FILEPATH TO OUTPUTXML]
175
+ **--suitesfrom [FILEPATH TO OUTPUTXML]**
157
176
  Optionally read suites from output.xml file. Failed suites will run first and longer running ones will be executed
158
177
  before shorter ones.
159
178
 
160
- --ordering [FILE PATH]
179
+ **--ordering [FILE PATH]**
161
180
  Optionally give execution order from a file.
162
181
 
163
- --chunk
182
+ **--chunk**
164
183
  Optionally chunk tests to PROCESSES number of robot runs. This can save time because all the suites will share the same
165
184
  setups and teardowns.
166
185
 
167
- --pabotprerunmodifier [PRERUNMODIFIER MODULE OR CLASS]
186
+ **--pabotprerunmodifier [PRERUNMODIFIER MODULE OR CLASS]**
168
187
  Like Robot Framework's --prerunmodifier, but executed only once in the pabot's main process after all other
169
188
  --prerunmodifiers. But unlike the regular --prerunmodifier command, --pabotprerunmodifier is not executed again in each
170
189
  pabot subprocesses. Depending on the intended use, this may be desirable as well as more efficient. Can be used, for
171
190
  example, to modify the list of tests to be performed.
172
191
 
173
- --no-rebot
192
+ **--no-rebot**
174
193
  If specified, the tests will execute as usual, but Rebot will not be called to merge the logs. This option is designed
175
194
  for scenarios where Rebot should be run later due to large log files, ensuring better memory and resource availability.
176
195
  Subprocess results are stored in the pabot_results folder.
177
196
 
178
- --help
197
+ **--help**
179
198
  Print usage instructions.
180
199
 
181
- --version
200
+ **--version**
182
201
  Print version information.
183
202
 
184
- Example usages:
203
+ **Example usages:**
185
204
 
186
205
  pabot test_directory
187
206
  pabot --exclude FOO directory_to_tests
@@ -398,6 +417,12 @@ Artifacts are **copied** into the output directory and renamed with the followin
398
417
  TIMESTAMP-ARGUMENT_INDEX-PABOTQUEUEINDEX
399
418
  ```
400
419
 
420
+ If you use the special option `notimestamps` at the end of the `--artifacts` command, (For example: `--artifacts png,txt,notimestamps`) the timestamp part will be omitted, and the name will be in the format:
421
+
422
+ ```
423
+ ARGUMENT_INDEX-PABOTQUEUEINDEX
424
+ ```
425
+
401
426
  - **TIMESTAMP** = Time of `pabot` command invocation (not the screenshot's actual timestamp), format: `YYYYmmdd_HHMMSS`
402
427
  - **ARGUMENT_INDEX** = Optional index number, only used if `--argumentfileN` options are given
403
428
  - **PABOTQUEUEINDEX** = Process queue index (see section [Global Variables](#global-variables))
@@ -7,4 +7,4 @@ try:
7
7
  except ImportError:
8
8
  pass
9
9
 
10
- __version__ = "5.0.0"
10
+ __version__ = "5.1.0"
@@ -142,6 +142,14 @@ def _parse_shard(arg):
142
142
  return int(parts[0]), int(parts[1])
143
143
 
144
144
 
145
+ def _parse_artifacts(arg):
146
+ # type: (str) -> Tuple[List[str], bool]
147
+ artifacts = arg.split(',')
148
+ if artifacts[-1] == 'notimestamps':
149
+ return (artifacts[:-1], False)
150
+ return (artifacts, True)
151
+
152
+
145
153
  def _parse_pabot_args(args): # type: (List[str]) -> Tuple[List[str], Dict[str, object]]
146
154
  pabot_args = {
147
155
  "command": ["pybot" if ROBOT_VERSION < "3.1" else "robot"],
@@ -155,6 +163,7 @@ def _parse_pabot_args(args): # type: (List[str]) -> Tuple[List[str], Dict[str,
155
163
  "processes": _processes_count(),
156
164
  "processtimeout": None,
157
165
  "artifacts": ["png"],
166
+ "artifactstimestamps": True,
158
167
  "artifactsinsubfolders": False,
159
168
  "shardindex": 0,
160
169
  "shardcount": 1,
@@ -181,7 +190,7 @@ def _parse_pabot_args(args): # type: (List[str]) -> Tuple[List[str], Dict[str,
181
190
  "processtimeout": int,
182
191
  "ordering": str,
183
192
  "suitesfrom": str,
184
- "artifacts": lambda x: x.split(","),
193
+ "artifacts": _parse_artifacts,
185
194
  "shard": _parse_shard,
186
195
  }
187
196
 
@@ -239,6 +248,9 @@ def _parse_pabot_args(args): # type: (List[str]) -> Tuple[List[str], Dict[str,
239
248
  elif arg_name == "pabotlibhost":
240
249
  pabot_args["pabotlib"] = False
241
250
  pabot_args[arg_name] = value
251
+ elif arg_name == "artifacts":
252
+ pabot_args["artifacts"] = value[0]
253
+ pabot_args["artifactstimestamps"] = value[1]
242
254
  else:
243
255
  pabot_args[arg_name] = value
244
256
  i += 2
@@ -194,8 +194,13 @@ def extract_section(lines, start_marker="<!-- START DOCSTRING -->", end_marker="
194
194
  if end_marker in line:
195
195
  break
196
196
  if inside_section:
197
- # Remove Markdown links but keep the text
198
- extracted_lines.append(re.sub(r'\[([^\]]+)\]\(https?://[^\)]+\)', r'\1', line))
197
+ # Remove Markdown hyperlinks but keep text
198
+ line = re.sub(r'\[([^\]]+)\]\(https?://[^\)]+\)', r'\1', line)
199
+ # Remove Markdown section links but keep text
200
+ line = re.sub(r'\[([^\]]+)\]\(#[^\)]+\)', r'\1', line)
201
+ # Remove ** and backticks `
202
+ line = re.sub(r'(\*\*|`)', '', line)
203
+ extracted_lines.append(line)
199
204
 
200
205
  return "".join(extracted_lines).strip()
201
206
 
@@ -354,7 +359,7 @@ def _try_execute_and_wait(
354
359
  show_stdout_on_failure,
355
360
  )
356
361
  if is_ignored and os.path.isdir(outs_dir):
357
- shutil.rmtree(outs_dir)
362
+ _rmtree_with_path(outs_dir)
358
363
 
359
364
 
360
365
  def _result_to_stdout(
@@ -750,10 +755,13 @@ def _modify_options_for_argfile_use(argfile, options):
750
755
 
751
756
 
752
757
  def _replace_base_name(new_name, options, key):
753
- if isinstance(options.get(key, None), str):
754
- options[key] = new_name + '.' + options[key].split('.', 1)[1]
758
+ if isinstance(options.get(key), str):
759
+ options[key] = f"{new_name}.{options[key].split('.', 1)[1]}" if '.' in options[key] else new_name
755
760
  elif key in options:
756
- options[key] = [new_name + '.' + s.split('.', 1)[1] for s in options.get(key, [])]
761
+ options[key] = [
762
+ f"{new_name}.{s.split('.', 1)[1]}" if '.' in s else new_name
763
+ for s in options.get(key, [])
764
+ ]
757
765
 
758
766
 
759
767
  def _set_terminal_coloring_options(options):
@@ -1473,15 +1481,30 @@ def _output_dir(options, cleanup=True):
1473
1481
  outputdir = options.get("outputdir", ".")
1474
1482
  outpath = os.path.join(outputdir, "pabot_results")
1475
1483
  if cleanup and os.path.isdir(outpath):
1476
- shutil.rmtree(outpath)
1484
+ _rmtree_with_path(outpath)
1477
1485
  return outpath
1478
1486
 
1479
1487
 
1480
- def _get_timestamp_id(timestamp_str):
1481
- return datetime.datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S.%f").strftime("%Y%m%d_%H%M%S")
1488
+ def _rmtree_with_path(path):
1489
+ """
1490
+ Remove a directory tree and, if a PermissionError occurs,
1491
+ re-raise it with the absolute path included in the message.
1492
+ """
1493
+ try:
1494
+ shutil.rmtree(path)
1495
+ except PermissionError as e:
1496
+ abs_path = os.path.abspath(path)
1497
+ raise PermissionError(f"Failed to delete path {abs_path}") from e
1498
+
1499
+
1500
+ def _get_timestamp_id(timestamp_str, add_timestamp):
1501
+ # type: (str, bool) -> Optional[str]
1502
+ if add_timestamp:
1503
+ return str(datetime.datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S.%f").strftime("%Y%m%d_%H%M%S"))
1504
+ return None
1482
1505
 
1483
1506
 
1484
- def _copy_output_artifacts(options, timestamp_id, file_extensions=None, include_subfolders=False, index=None):
1507
+ def _copy_output_artifacts(options, timestamp_id=None, file_extensions=None, include_subfolders=False, index=None):
1485
1508
  file_extensions = file_extensions or ["png"]
1486
1509
  pabot_outputdir = _output_dir(options, cleanup=False)
1487
1510
  outputdir = options.get("outputdir", ".")
@@ -1505,9 +1528,9 @@ def _copy_output_artifacts(options, timestamp_id, file_extensions=None, include_
1505
1528
  dst_folder_path = os.path.join(outputdir, subfolder_path)
1506
1529
  if not os.path.isdir(dst_folder_path):
1507
1530
  os.makedirs(dst_folder_path)
1508
- dst_file_name = "-".join([timestamp_id, prefix, file_name])
1509
- if index:
1510
- dst_file_name = "-".join([timestamp_id, index, prefix, file_name])
1531
+ dst_file_name_parts = [timestamp_id, index, prefix, file_name]
1532
+ filtered_name = [str(p) for p in dst_file_name_parts if p is not None]
1533
+ dst_file_name = "-".join(filtered_name)
1511
1534
  shutil.copy2(
1512
1535
  os.path.join(location, file_name),
1513
1536
  os.path.join(dst_folder_path, dst_file_name),
@@ -1551,7 +1574,7 @@ def _report_results(outs_dir, pabot_args, options, start_time_string, tests_root
1551
1574
  outputs = [] # type: List[str]
1552
1575
  for index, _ in pabot_args["argumentfiles"]:
1553
1576
  copied_artifacts = _copy_output_artifacts(
1554
- options, _get_timestamp_id(start_time_string), pabot_args["artifacts"], pabot_args["artifactsinsubfolders"], index
1577
+ options, _get_timestamp_id(start_time_string, pabot_args["artifactstimestamps"]), pabot_args["artifacts"], pabot_args["artifactsinsubfolders"], index
1555
1578
  )
1556
1579
  outputs += [
1557
1580
  _merge_one_run(
@@ -1560,7 +1583,7 @@ def _report_results(outs_dir, pabot_args, options, start_time_string, tests_root
1560
1583
  tests_root_name,
1561
1584
  stats,
1562
1585
  copied_artifacts,
1563
- timestamp_id=_get_timestamp_id(start_time_string),
1586
+ timestamp_id=_get_timestamp_id(start_time_string, pabot_args["artifactstimestamps"]),
1564
1587
  outputfile=os.path.join("pabot_results", "output%s.xml" % index),
1565
1588
  )
1566
1589
  ]
@@ -1610,11 +1633,12 @@ def _write_stats(stats):
1610
1633
  def _report_results_for_one_run(
1611
1634
  outs_dir, pabot_args, options, start_time_string, tests_root_name, stats
1612
1635
  ):
1636
+ _write(pabot_args)
1613
1637
  copied_artifacts = _copy_output_artifacts(
1614
- options, _get_timestamp_id(start_time_string), pabot_args["artifacts"], pabot_args["artifactsinsubfolders"]
1638
+ options, _get_timestamp_id(start_time_string, pabot_args["artifactstimestamps"]), pabot_args["artifacts"], pabot_args["artifactsinsubfolders"]
1615
1639
  )
1616
1640
  output_path = _merge_one_run(
1617
- outs_dir, options, tests_root_name, stats, copied_artifacts, _get_timestamp_id(start_time_string)
1641
+ outs_dir, options, tests_root_name, stats, copied_artifacts, _get_timestamp_id(start_time_string, pabot_args["artifactstimestamps"])
1618
1642
  )
1619
1643
  _write_stats(stats)
1620
1644
  if (
@@ -1726,9 +1750,19 @@ def _stop_message_writer():
1726
1750
  MESSAGE_QUEUE.join()
1727
1751
 
1728
1752
 
1729
- def _get_free_port(pabot_args):
1730
- if pabot_args["pabotlibport"] != 0:
1731
- return pabot_args["pabotlibport"]
1753
+ def _is_port_available(port):
1754
+ """Check if a given port on localhost is available."""
1755
+ with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
1756
+ try:
1757
+ s.bind(("localhost", port))
1758
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1759
+ return True
1760
+ except OSError:
1761
+ return False
1762
+
1763
+
1764
+ def _get_free_port():
1765
+ """Return a free TCP port on localhost."""
1732
1766
  with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
1733
1767
  s.bind(("localhost", 0))
1734
1768
  s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
@@ -1737,29 +1771,43 @@ def _get_free_port(pabot_args):
1737
1771
 
1738
1772
  def _start_remote_library(pabot_args): # type: (dict) -> Optional[subprocess.Popen]
1739
1773
  global _PABOTLIBURI
1740
- free_port = _get_free_port(pabot_args)
1741
- _PABOTLIBURI = "%s:%s" % (pabot_args["pabotlibhost"], free_port)
1742
- if not pabot_args["pabotlib"]:
1774
+ # If pabotlib is not enabled, do nothing
1775
+ if not pabot_args.get("pabotlib"):
1743
1776
  return None
1744
- if pabot_args.get("resourcefile") and not os.path.exists(
1745
- pabot_args["resourcefile"]
1746
- ):
1777
+
1778
+ host = pabot_args.get("pabotlibhost", "127.0.0.1")
1779
+ port = pabot_args.get("pabotlibport", 8270)
1780
+
1781
+ # If host is default and user specified a non-zero port, check if it's available
1782
+ if host == "127.0.0.1" and port != 0 and not _is_port_available(port):
1783
+ _write(
1784
+ f"Warning: specified pabotlibport {port} is already in use. "
1785
+ "A free port will be assigned automatically.",
1786
+ Color.YELLOW,
1787
+ )
1788
+ port = _get_free_port()
1789
+
1790
+ # If host is default and port = 0, assign a free port
1791
+ if host == "127.0.0.1" and port == 0:
1792
+ port = _get_free_port()
1793
+
1794
+ _PABOTLIBURI = f"{host}:{port}"
1795
+ resourcefile = pabot_args.get("resourcefile") or ""
1796
+ if resourcefile and not os.path.exists(resourcefile):
1747
1797
  _write(
1748
1798
  "Warning: specified resource file doesn't exist."
1749
1799
  " Some tests may fail or continue forever.",
1750
1800
  Color.YELLOW,
1751
1801
  )
1752
- pabot_args["resourcefile"] = None
1753
- return subprocess.Popen(
1754
- '"{python}" -m {pabotlibname} {resourcefile} {pabotlibhost} {pabotlibport}'.format(
1755
- python=sys.executable,
1756
- pabotlibname=pabotlib.__name__,
1757
- resourcefile=pabot_args.get("resourcefile"),
1758
- pabotlibhost=pabot_args["pabotlibhost"],
1759
- pabotlibport=free_port,
1760
- ),
1761
- shell=True,
1762
- )
1802
+ resourcefile = ""
1803
+ cmd = [
1804
+ sys.executable,
1805
+ "-m", pabotlib.__name__,
1806
+ resourcefile,
1807
+ pabot_args["pabotlibhost"],
1808
+ str(port),
1809
+ ]
1810
+ return subprocess.Popen(cmd)
1763
1811
 
1764
1812
 
1765
1813
  def _stop_remote_library(process): # type: (subprocess.Popen) -> None
@@ -60,7 +60,7 @@ class _PabotLib(object):
60
60
  self, resourcefile
61
61
  ): # type: (Optional[str]) -> Dict[str, Dict[str, Any]]
62
62
  vals = {} # type: Dict[str, Dict[str, Any]]
63
- if resourcefile is None:
63
+ if not resourcefile:
64
64
  return vals
65
65
  conf = configparser.ConfigParser()
66
66
  conf.read(resourcefile)
@@ -201,13 +201,13 @@ def prefix(source, timestamp_id):
201
201
  if not id:
202
202
  return ""
203
203
  if os.path.split(path_without_id)[1] == 'pabot_results':
204
- return "-".join([timestamp_id, id])
204
+ return "-".join([str(p) for p in [timestamp_id, id] if p is not None])
205
205
  else:
206
206
  # --argumentfileN in use: (there should be one subdir level more)
207
207
  _, index = os.path.split(path_without_id)
208
208
  if not index:
209
209
  return ""
210
- return "-".join([timestamp_id, index, id])
210
+ return "-".join([str(p) for p in [timestamp_id, index, id] if p is not None])
211
211
  except:
212
212
  return ""
213
213
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: robotframework-pabot
3
- Version: 5.0.0
3
+ Version: 5.1.0
4
4
  Summary: Parallel test runner for Robot Framework
5
5
  Home-page: https://pabot.org
6
6
  Download-URL: https://pypi.python.org/pypi/robotframework-pabot
@@ -99,7 +99,16 @@ There are several ways you can help in improving this tool:
99
99
  - Contribute by programming and making a pull request (easiest way is to work on an issue from the issue tracker)
100
100
 
101
101
  ## Command-line options
102
+ <!-- NOTE:
103
+ The sections inside these docstring markers are also used in Pabot's --help output.
104
+ Currently, the following transformations are applied:
105
+ - Remove Markdown links but keep the text
106
+ - Remove ** and backticks `
107
+
108
+ If you modify this part, make sure the Markdown section still looks clean and readable in the --help output. -->
109
+
102
110
  <!-- START DOCSTRING -->
111
+ ```
103
112
  pabot [--verbose|--testlevelsplit|--command .. --end-command|
104
113
  --processes num|--no-pabotlib|--pabotlibhost host|--pabotlibport port|
105
114
  --processtimeout num|
@@ -111,52 +120,60 @@ pabot [--verbose|--testlevelsplit|--command .. --end-command|
111
120
  --no-rebot|
112
121
  --help|--version]
113
122
  [robot options] [path ...]
123
+ ```
114
124
 
115
125
  PabotLib remote server is started by default to enable locking and resource distribution between parallel test executions.
116
126
 
117
127
  Supports all [Robot Framework command line options](https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#all-command-line-options) and also following pabot options:
118
128
 
119
- --verbose
129
+ **--verbose**
120
130
  More output from the parallel execution.
121
131
 
122
- --testlevelsplit
132
+ **--testlevelsplit**
123
133
  Split execution on test level instead of default suite level. If .pabotsuitenames contains both tests and suites then
124
134
  this will only affect new suites and split only them. Leaving this flag out when both suites and tests in
125
135
  .pabotsuitenames file will also only affect new suites and add them as suite files.
126
136
 
127
- --command [ACTUAL COMMANDS TO START ROBOT EXECUTOR] --end-command
137
+ **--command [ACTUAL COMMANDS TO START ROBOT EXECUTOR] --end-command**
128
138
  RF script for situations where robot is not used directly.
129
139
 
130
- --processes [NUMBER OF PROCESSES]
140
+ **--processes [NUMBER OF PROCESSES]**
131
141
  How many parallel executors to use (default max of 2 and cpu count). Special option "all" will use as many processes as
132
142
  there are executable suites or tests.
133
143
 
134
- --no-pabotlib
144
+ **--no-pabotlib**
135
145
  Disable the PabotLib remote server if you don't need locking or resource distribution features.
136
146
 
137
- --pabotlibhost [HOSTNAME]
147
+ **--pabotlibhost [HOSTNAME]**
138
148
  Connect to an already running instance of the PabotLib remote server at the given host (disables the local PabotLib
139
149
  server start). For example, to connect to a remote PabotLib server running on another machine:
140
150
 
141
151
  pabot --pabotlibhost 192.168.1.123 --pabotlibport 8271 tests/
142
152
 
143
- The remote server can be also started and executed separately from pabot instances:
153
+ The remote server can also be started and executed separately from pabot instances:
144
154
 
145
155
  python -m pabot.pabotlib <path_to_resourcefile> <host> <port>
146
156
  python -m pabot.pabotlib resource.txt 192.168.1.123 8271
147
157
 
148
158
  This enables sharing a resource with multiple Robot Framework instances.
149
159
 
150
- --pabotlibport [PORT]
160
+ Additional details:
161
+ - The default value for --pabotlibhost is 127.0.0.1.
162
+ - If you provide a hostname other than 127.0.0.1, the local PabotLib server startup is automatically disabled.
163
+
164
+ **--pabotlibport [PORT]**
151
165
  Port number of the PabotLib remote server (default is 8270). See --pabotlibhost for more information.
152
166
 
153
- --processtimeout [TIMEOUT]
167
+ Behavior with port and host settings:
168
+ - If you set the port value to 0 and --pabotlibhost is 127.0.0.1 (default), a free port on localhost will be assigned automatically.
169
+
170
+ **--processtimeout [TIMEOUT]**
154
171
  Maximum time in seconds to wait for a process before killing it. If not set, there's no timeout.
155
172
 
156
- --shard [INDEX]/[TOTAL]
173
+ **--shard [INDEX]/[TOTAL]**
157
174
  Optionally split execution into smaller pieces. This can be used for distributing testing to multiple machines.
158
175
 
159
- --artifacts [FILE EXTENSIONS]
176
+ **--artifacts [FILE EXTENSIONS]**
160
177
  List of file extensions (comma separated). Defines which files (screenshots, videos etc.) from separate reporting
161
178
  directories would be copied and included in a final report. Possible links to copied files in RF log would be updated
162
179
  (only relative paths supported). The default value is `png`.
@@ -165,49 +182,51 @@ Supports all [Robot Framework command line options](https://robotframework.org/r
165
182
 
166
183
  --artifacts png,mp4,txt
167
184
 
168
- --artifactsinsubfolders
185
+ The artifact naming conventions are described in the README.md section: [Output Files Generated by Pabot](#output-files-generated-by-pabot).
186
+
187
+ **--artifactsinsubfolders**
169
188
  Copy artifacts located not only directly in the RF output dir, but also in it's sub-folders.
170
189
 
171
- --resourcefile [FILEPATH]
190
+ **--resourcefile [FILEPATH]**
172
191
  Indicator for a file that can contain shared variables for distributing resources. This needs to be used together with
173
192
  pabotlib option. Resource file syntax is same as Windows ini files. Where a section is a shared set of variables.
174
193
 
175
- --argumentfile[INTEGER] [FILEPATH]
194
+ **--argumentfile[INTEGER] [FILEPATH]**
176
195
  Run same suites with multiple [argumentfile](http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#argument-files) options.
177
196
 
178
197
  For example:
179
198
 
180
199
  --argumentfile1 arg1.txt --argumentfile2 arg2.txt
181
200
 
182
- --suitesfrom [FILEPATH TO OUTPUTXML]
201
+ **--suitesfrom [FILEPATH TO OUTPUTXML]**
183
202
  Optionally read suites from output.xml file. Failed suites will run first and longer running ones will be executed
184
203
  before shorter ones.
185
204
 
186
- --ordering [FILE PATH]
205
+ **--ordering [FILE PATH]**
187
206
  Optionally give execution order from a file.
188
207
 
189
- --chunk
208
+ **--chunk**
190
209
  Optionally chunk tests to PROCESSES number of robot runs. This can save time because all the suites will share the same
191
210
  setups and teardowns.
192
211
 
193
- --pabotprerunmodifier [PRERUNMODIFIER MODULE OR CLASS]
212
+ **--pabotprerunmodifier [PRERUNMODIFIER MODULE OR CLASS]**
194
213
  Like Robot Framework's --prerunmodifier, but executed only once in the pabot's main process after all other
195
214
  --prerunmodifiers. But unlike the regular --prerunmodifier command, --pabotprerunmodifier is not executed again in each
196
215
  pabot subprocesses. Depending on the intended use, this may be desirable as well as more efficient. Can be used, for
197
216
  example, to modify the list of tests to be performed.
198
217
 
199
- --no-rebot
218
+ **--no-rebot**
200
219
  If specified, the tests will execute as usual, but Rebot will not be called to merge the logs. This option is designed
201
220
  for scenarios where Rebot should be run later due to large log files, ensuring better memory and resource availability.
202
221
  Subprocess results are stored in the pabot_results folder.
203
222
 
204
- --help
223
+ **--help**
205
224
  Print usage instructions.
206
225
 
207
- --version
226
+ **--version**
208
227
  Print version information.
209
228
 
210
- Example usages:
229
+ **Example usages:**
211
230
 
212
231
  pabot test_directory
213
232
  pabot --exclude FOO directory_to_tests
@@ -424,6 +443,12 @@ Artifacts are **copied** into the output directory and renamed with the followin
424
443
  TIMESTAMP-ARGUMENT_INDEX-PABOTQUEUEINDEX
425
444
  ```
426
445
 
446
+ If you use the special option `notimestamps` at the end of the `--artifacts` command, (For example: `--artifacts png,txt,notimestamps`) the timestamp part will be omitted, and the name will be in the format:
447
+
448
+ ```
449
+ ARGUMENT_INDEX-PABOTQUEUEINDEX
450
+ ```
451
+
427
452
  - **TIMESTAMP** = Time of `pabot` command invocation (not the screenshot's actual timestamp), format: `YYYYmmdd_HHMMSS`
428
453
  - **ARGUMENT_INDEX** = Optional index number, only used if `--argumentfileN` options are given
429
454
  - **PABOTQUEUEINDEX** = Process queue index (see section [Global Variables](#global-variables))
@@ -78,6 +78,7 @@ Testing 3
78
78
  'processes': 3,
79
79
  'processtimeout': None,
80
80
  'artifacts': ['png'],
81
+ "artifactstimestamps": True,
81
82
  'artifactsinsubfolders': False,
82
83
  'shardindex': 0,
83
84
  'shardcount': 1,
@@ -1208,6 +1208,40 @@ class PabotTests(unittest.TestCase):
1208
1208
  "file copied wrongly: {}".format(f),
1209
1209
  )
1210
1210
 
1211
+ def test_copy_output_artifacts_direct_screenshots_only_without_timestamps(self):
1212
+ out_dir = os.path.join(
1213
+ os.path.abspath(os.path.dirname(__file__)),
1214
+ "outputs/outputs_with_artifacts/out_dir",
1215
+ )
1216
+ _opts = {"outputdir": out_dir}
1217
+ test_time_id = None
1218
+ pabot._copy_output_artifacts(options=_opts, timestamp_id=test_time_id)
1219
+ files_should_be_copied = [
1220
+ "0-fake_screenshot_root.png",
1221
+ "1-fake_screenshot_root.png",
1222
+ ]
1223
+
1224
+ for f in files_should_be_copied:
1225
+ file_path = os.path.join(_opts["outputdir"], f)
1226
+ self.assertTrue(os.path.isfile(file_path), "file not copied: {}".format(f))
1227
+ os.remove(file_path) # clean up
1228
+
1229
+ files_should_not_be_copied = [
1230
+ "screenshots/0-fake_screenshot_subfolder_1.png",
1231
+ "screenshots/0-fake_screenshot_subfolder_2.png"
1232
+ "screenshots/1-fake_screenshot_subfolder_1.png",
1233
+ "screenshots/2-fake_screenshot_subfolder_2.png",
1234
+ "other_artifacts/0-some_artifact.foo",
1235
+ "other_artifacts/0-another_artifact.bar",
1236
+ "other_artifacts/1-some_artifact.foo",
1237
+ "other_artifacts/1-another_artifact.bar",
1238
+ ]
1239
+ for f in files_should_not_be_copied:
1240
+ self.assertFalse(
1241
+ os.path.isfile(os.path.join(_opts["outputdir"], f)),
1242
+ "file copied wrongly: {}".format(f),
1243
+ )
1244
+
1211
1245
  def test_copy_output_artifacts_include_subfolders(self):
1212
1246
  out_dir = os.path.join(
1213
1247
  os.path.abspath(os.path.dirname(__file__)),
@@ -20,9 +20,12 @@ def check_robot_version_and_return_name():
20
20
 
21
21
 
22
22
  def get_tmpdir_name(input: str) -> str:
23
+ # Remove all underscores from the end
24
+ result = input.rstrip('_')
25
+
23
26
  # Remove everything before the first occurrence of two or more consecutive underscores,
24
27
  # while preserving underscores as spaces with the same count
25
- result = re.sub(r'^.*?(__+)', lambda m: ' ' * len(m.group(1)), input).strip()
28
+ result = re.sub(r'^.*?(__+)', lambda m: ' ' * len(m.group(1)), result).strip()
26
29
 
27
30
  # Capitalize letters following space, underscores or digits (e.g. after ' ', '_', '1')
28
31
  result = re.sub(r'([ _\d])([a-z])', lambda m: m.group(1) + m.group(2).upper(), result)