testbench2robotframework 2.0.0__tar.gz → 2.1.0rc1__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 (102) hide show
  1. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/.gitignore +0 -1
  2. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/PKG-INFO +6 -4
  3. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/README.md +3 -3
  4. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/docs/configuration/overview.md +115 -0
  5. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/docs/usage/fetch_results.md +39 -2
  6. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/docs/usage/generate_tests.md +35 -0
  7. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/pyproject.toml +6 -0
  8. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/__init__.py +1 -1
  9. testbench2robotframework-2.1.0rc1/testbench2robotframework/attachments.py +66 -0
  10. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/cli.py +20 -0
  11. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/config.py +10 -0
  12. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/execution_artifacts.py +10 -2
  13. testbench2robotframework-2.1.0rc1/testbench2robotframework/json_codec.py +55 -0
  14. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/json_reader.py +2 -4
  15. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/json_writer.py +6 -25
  16. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/model_utils.py +14 -2
  17. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/result_writer.py +89 -24
  18. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/robotframework2testbench.py +4 -2
  19. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/testbench2rf.py +130 -7
  20. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/testbench2robotframework.py +7 -29
  21. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/testsuite_write.py +26 -14
  22. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/utils.py +68 -22
  23. testbench2robotframework-2.1.0rc1/tests/test_attachment_parameters.py +159 -0
  24. testbench2robotframework-2.1.0rc1/tests/test_attachments_export.py +247 -0
  25. testbench2robotframework-2.1.0rc1/tests/test_blocked_filter.py +177 -0
  26. testbench2robotframework-2.1.0rc1/tests/test_clean.py +122 -0
  27. testbench2robotframework-2.1.0rc1/tests/test_config_optional_options.py +96 -0
  28. testbench2robotframework-2.1.0rc1/tests/test_execution_artifacts.py +96 -0
  29. testbench2robotframework-2.1.0rc1/tests/test_fetch_results_output.py +160 -0
  30. testbench2robotframework-2.1.0rc1/tests/test_json_codec.py +29 -0
  31. testbench2robotframework-2.1.0rc1/tests/test_json_writer_tree_order.py +96 -0
  32. testbench2robotframework-2.1.0rc1/tests/test_keyword_comment.py +207 -0
  33. testbench2robotframework-2.1.0rc1/tests/test_main_protocol_reader.py +51 -0
  34. testbench2robotframework-2.1.0rc1/tests/test_open_report.py +67 -0
  35. testbench2robotframework-2.1.0rc1/tests/test_protocol_merge.py +196 -0
  36. testbench2robotframework-2.1.0rc1/tests/test_protocol_references.py +109 -0
  37. testbench2robotframework-2.1.0rc1/tests/test_protocol_rows.py +136 -0
  38. testbench2robotframework-2.1.0rc1/tests/test_report_creator_tester.py +234 -0
  39. testbench2robotframework-2.1.0rc1/tests/test_subdivision_patterns.py +284 -0
  40. testbench2robotframework-2.1.0rc1/tests/test_tree_verdicts.py +172 -0
  41. testbench2robotframework-2.0.0/README_old.md +0 -284
  42. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/.github/dependabot.yml +0 -0
  43. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/.github/workflows/release-binaries.yml +0 -0
  44. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/.github/workflows/trigger-docs-release.yml +0 -0
  45. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/DEVELOPMENT.md +0 -0
  46. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/ExampleConfiguration/json_config.json +0 -0
  47. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/ExampleConfiguration/pyproject_example.toml +0 -0
  48. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/ExampleConfiguration/toml_config.toml +0 -0
  49. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/LICENSE +0 -0
  50. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/MANIFEST.in +0 -0
  51. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/atest/json_config_tests/1_tfs.robot +0 -0
  52. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/atest/robot/libs/json_config.py +0 -0
  53. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/atest/robot/libs/pyproject_config.py +0 -0
  54. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/atest/robot/resources/file_management.resource +0 -0
  55. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/atest/robot/resources/testbench2robotframework_cli.resource +0 -0
  56. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/atest/robot/rf_tests/cli_interface/write/json_config.robot +0 -0
  57. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/atest/robot/rf_tests/cli_interface/write/no_config_argument.robot +0 -0
  58. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/atest/robot/rf_tests/cli_interface/write/toml_config.robot +0 -0
  59. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/create_json_schema.py +0 -0
  60. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/docs/configuration/_category_.json +0 -0
  61. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/docs/configuration/cli_options.md +0 -0
  62. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/docs/configuration/pyproject_config.md +0 -0
  63. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/docs/getting_started/_category_.json +0 -0
  64. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/docs/getting_started/installation.md +0 -0
  65. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/docs/getting_started/quick_start.md +0 -0
  66. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/docs/intro.md +0 -0
  67. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/docs/media/func_kw.png +0 -0
  68. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/docs/media/itorx-exec-overview.png +0 -0
  69. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/docs/media/itorx-exec.png +0 -0
  70. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/docs/media/robot-setup-teardown.png +0 -0
  71. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/docs/media/robot-suite-code.png +0 -0
  72. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/docs/media/robot-suite-log.png +0 -0
  73. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/docs/media/tc-table.png +0 -0
  74. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/docs/media/tcs-browser.png +0 -0
  75. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/docs/media/tcs-exec-comment.png +0 -0
  76. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/docs/media/tcs-exec-in-TB.png +0 -0
  77. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/docs/media/tcs-sequence.png +0 -0
  78. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/docs/media/te-tree.png +0 -0
  79. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/docs/usage/_category_.json +0 -0
  80. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/images/LibrarySubdivision.PNG +0 -0
  81. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/images/Unbenannt.PNG +0 -0
  82. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/images/generated.png +0 -0
  83. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/images/libraries.PNG +0 -0
  84. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/images/resources.PNG +0 -0
  85. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/images/rfLibraryRootsTestBench.PNG +0 -0
  86. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/images/testbench_rfLibraryRegex.PNG +0 -0
  87. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/images/testbench_rfResourceRegex.PNG +0 -0
  88. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/images/testthemen.PNG +0 -0
  89. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/pyinstaller/imbusTB.ico +0 -0
  90. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/pyinstaller/run.py +0 -0
  91. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/requirements.txt +0 -0
  92. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/robot.toml +0 -0
  93. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/tasks.py +0 -0
  94. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/testbench-tools.zip +0 -0
  95. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/__main__.py +0 -0
  96. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/blocked_filter.py +0 -0
  97. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/execution_comment.py +0 -0
  98. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/html_parser.py +0 -0
  99. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/keyword_comment.py +0 -0
  100. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/log.py +0 -0
  101. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/model.py +0 -0
  102. {testbench2robotframework-2.0.0 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/protocol_merge.py +0 -0
@@ -7,7 +7,6 @@ __pycache__/
7
7
  *.so
8
8
  ignore/
9
9
  .ruff_cache/
10
- tests/
11
10
 
12
11
  # Distribution / packaging
13
12
  .Python
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: testbench2robotframework
3
- Version: 2.0.0
3
+ Version: 2.1.0rc1
4
4
  Summary: Robot Framework Code Generator from Keyword-Driven Tests in imbus TestBench 3.0 and newer
5
5
  Author-email: imbus AG <support@imbus.de>
6
6
  Requires-Python: >= 3.10
@@ -22,8 +22,10 @@ Requires-Dist: robotframework-tidy ; extra == "dev"
22
22
  Requires-Dist: datamodel-code-generator ; extra == "dev"
23
23
  Requires-Dist: ruff ; extra == "dev"
24
24
  Requires-Dist: twine ; extra == "dev"
25
+ Requires-Dist: orjson ; extra == "fast"
25
26
  Project-URL: Repository, https://github.com/imbus/testbench2robotframework
26
27
  Provides-Extra: dev
28
+ Provides-Extra: fast
27
29
 
28
30
  # TestBench2RobotFramework
29
31
 
@@ -33,14 +35,14 @@ This can be used for automated test execution with Robot Framework, triggered by
33
35
 
34
36
  ## Documentation
35
37
 
36
- The documentation is available in the [docs](./docs) folder of this repository.
38
+ The documentation is available in the [TestBench Ecosystem Documentation](https://imbus.github.io/testbench-ecosystem-documentation/testbench2robotframework/intro).
37
39
 
38
40
  ## Releases
39
41
 
40
- Releases are published on GitHub and available via [PyPI](https://pypi.org/project/testbench2robotframework/).
42
+ Releases are published on [GitHub](https://github.com/imbus/testbench2robotframework/releases) and available via [PyPI](https://pypi.org/project/testbench2robotframework/).
41
43
 
42
44
  ## Requirements
43
45
 
44
46
  - Python 3.10 or higher
45
- - TestBench version >= 4
47
+ - TestBench version >= 4.1
46
48
 
@@ -6,13 +6,13 @@ This can be used for automated test execution with Robot Framework, triggered by
6
6
 
7
7
  ## Documentation
8
8
 
9
- The documentation is available in the [docs](./docs) folder of this repository.
9
+ The documentation is available in the [TestBench Ecosystem Documentation](https://imbus.github.io/testbench-ecosystem-documentation/testbench2robotframework/intro).
10
10
 
11
11
  ## Releases
12
12
 
13
- Releases are published on GitHub and available via [PyPI](https://pypi.org/project/testbench2robotframework/).
13
+ Releases are published on [GitHub](https://github.com/imbus/testbench2robotframework/releases) and available via [PyPI](https://pypi.org/project/testbench2robotframework/).
14
14
 
15
15
  ## Requirements
16
16
 
17
17
  - Python 3.10 or higher
18
- - TestBench version >= 4
18
+ - TestBench version >= 4.1
@@ -85,7 +85,11 @@ file. This is noted per option below.
85
85
  | [`resource-mapping`](#library-mapping--resource-mapping) | `--resource-mapping` | ✅ | G | mapping (`{}`) |
86
86
  | [`forced-import`](#forced-import) | — | ✅ | G | table of lists (`{}`) |
87
87
  | [`metadata`](#metadata) | `--metadata` | ✅ | G | mapping (`{}`) |
88
+ | [`attachments-directory`](#attachments-directory) | `--attachments-directory` | ✅ | G | path (empty) |
89
+ | [`attachments-variable`](#attachments-variable) | — | ✅ | G | variable name (`ITB_ATTACHMENTS_DIR`) |
90
+ | [`keep-extracted-report`](#keep-extracted-report) | — | ✅ | G · F | bool (`false`) |
88
91
  | [`merge-protocol`](#merge-protocol) | `--no-merge-protocol` | ✅ | F | bool (`true`) |
92
+ | [`set-tester-from-report-creator`](#set-tester-from-report-creator) | `--set-tester-from-report-creator` | ✅ | F | bool (`false`) |
89
93
  | [`keyword-comment-style`](#keyword-comment-style) | — | ✅ | F | `STRUCTURED` \| `FLAT` (`STRUCTURED`) |
90
94
  | [`keyword-comment-max-depth`](#keyword-comment-max-depth) | — | ✅ | F | int (`5`) |
91
95
  | [`keyword-comment-max-rows`](#keyword-comment-max-rows) | — | ✅ | F | int, `0` = no limit (`300`) |
@@ -117,6 +121,94 @@ testbench2robotframework generate-tests -d ./suites.zip my_report.zip
117
121
  output-directory = "{root}/Generated"
118
122
  ```
119
123
 
124
+ #### `output-directory` with `fetch-results`
125
+
126
+ For `fetch-results` the option names the updated report. The input report is only
127
+ read when `-d` is given; **without `-d` the input report itself is updated** and
128
+ the original, unexecuted report is gone afterwards.
129
+
130
+ | Input report | `-d` | Result |
131
+ |---|---|---|
132
+ | `report.zip` | `result.zip` | `result.zip` is created; `report.zip` is unchanged. |
133
+ | `report.zip` | `result/` | `result/` is created (or merged into) as a directory; `report.zip` is unchanged. |
134
+ | `report/` | `result.zip` | `result.zip` is created; `report/` is unchanged. |
135
+ | `report/` | `result/` | `result/` is created (or merged into); `report/` is unchanged. |
136
+ | `report.zip` | *none* | `report.zip` is **overwritten** with the updated report. |
137
+ | `report/` | *none* | The files in `report/` are **updated in place**; no ZIP is created. |
138
+
139
+ Keep the original report if you need it for a second run — for example to merge
140
+ another execution with [`merge-protocol`](#merge-protocol) — or always pass `-d`.
141
+
142
+ ### `keep-extracted-report`
143
+
144
+ Where a `.zip` report is extracted to while a command reads it. By default that is
145
+ a temporary directory in the working directory, removed when the command is done.
146
+ With `true` the report is extracted to a directory of the same name next to the
147
+ ZIP (`report.zip` → `report/`), replacing that directory if it exists, and left
148
+ there afterwards — for example to look at the report's attachments. Has no effect
149
+ when the report is given as a directory. **Configuration file only.**
150
+
151
+ - **Values:** `true` / `false`. Default: `false`.
152
+
153
+ ```toml
154
+ keep-extracted-report = true
155
+ ```
156
+
157
+ :::note
158
+ Up to version 2.0 `fetch-results` always left the extracted report next to the
159
+ ZIP. Set `keep-extracted-report = true` to keep that behaviour.
160
+ :::
161
+
162
+ ### `attachments-directory`
163
+
164
+ Where `generate-tests` copies the report's `attachments/` folder to — every
165
+ attachment: representatives of reference data types (`representatives/DT-<key>/`),
166
+ files attached to test case sets and test cases, and whatever else is in there.
167
+ Empty (the default) exports nothing. The target is emptied before copying.
168
+
169
+ - **Values:** a path, empty, or a path starting with `{root}`. Default: empty.
170
+ - **CLI:** `--attachments-directory`.
171
+
172
+ How the path is given decides how the generated suites refer to it:
173
+
174
+ | Value | Copied to | Value of the [attachments variable](#attachments-variable) in every suite |
175
+ |---|---|---|
176
+ | relative, e.g. `attachments` | `<output-directory>/attachments` | `${CURDIR}/../attachments` — relative to the suite (`..` per directory level), so the output directory can be moved as a whole |
177
+ | absolute, e.g. `/data/run1/attachments` | exactly there | `/data/run1/attachments` |
178
+ | `{root}/…` | the absolute path it resolves to | that absolute path |
179
+
180
+ With a `.zip` `output-directory`, a relative attachments directory ends up inside
181
+ the archive.
182
+
183
+ ```bash
184
+ testbench2robotframework generate-tests --attachments-directory attachments my_report.zip
185
+ robot ./Generated # finds the attachments by itself
186
+ ```
187
+ ```toml
188
+ attachments-directory = "attachments"
189
+ ```
190
+
191
+ ### `attachments-variable`
192
+
193
+ The Robot Framework variable through which generated keyword calls address
194
+ attachments, for example
195
+ `${ITB_ATTACHMENTS_DIR}/representatives/DT-6917529030000126275/Vorlage.xml`.
196
+ When [`attachments-directory`](#attachments-directory) is set, every suite whose
197
+ keyword calls refer to the variable gets a `*** Variables ***` section defining it
198
+ (see the table above) — suites without attachments and `__init__.robot` files stay
199
+ as they are. A `--variable` on the `robot` command line still overrides that
200
+ default. Without `attachments-directory` no section is written and the variable
201
+ has to be passed to `robot`. **Configuration file only.**
202
+
203
+ - **Values:** a variable name without `${}`, or empty. Default: `ITB_ATTACHMENTS_DIR`.
204
+ - **Empty:** attachments are written as paths relative to the report's
205
+ `attachments` folder (`representatives/DT-…/Vorlage.xml`) and no section is
206
+ generated — for runs started from inside that folder.
207
+
208
+ ```toml
209
+ attachments-variable = "ITB_ATTACHMENTS_DIR"
210
+ ```
211
+
120
212
  ### `create-output-zip`
121
213
 
122
214
  For `generate-tests` only: in addition to the normal directory output, also
@@ -444,6 +536,29 @@ testbench2robotframework fetch-results --no-merge-protocol output.xml my_report.
444
536
  merge-protocol = true
445
537
  ```
446
538
 
539
+ ### `set-tester-from-report-creator`
540
+
541
+ Sets the user who created the TestBench report as **tester** of the test cases the
542
+ current Robot run executed. The user is read from `reportCreation.creator` in the
543
+ report's `manifest.json`: its `userKey` becomes the `testerKey` of the test case in
544
+ `protocol.json`, and its `userKey` and `userName` become `exec.tester` in the test
545
+ case's JSON file. An existing tester is overwritten.
546
+
547
+ Only test cases with the verdict `Pass` or `Fail` get a tester. Skipped and not run
548
+ tests, and executions kept from the input protocol when merging, stay unchanged. If
549
+ `manifest.json` is missing or names no creator, a warning is logged and no tester is
550
+ set.
551
+
552
+ - **Values:** `true` / `false`. Default: `false`.
553
+ - **CLI:** `--set-tester-from-report-creator` (turns it on).
554
+
555
+ ```bash
556
+ testbench2robotframework fetch-results --set-tester-from-report-creator output.xml my_report.zip
557
+ ```
558
+ ```toml
559
+ set-tester-from-report-creator = true
560
+ ```
561
+
447
562
  ### `keyword-comment-style`
448
563
 
449
564
  How the execution comment of a keyword is rendered. `STRUCTURED` shows the whole
@@ -36,8 +36,10 @@ TestBench report the suites were generated from.
36
36
  - **In:** the original TestBench report (the same directory or ZIP file the suites
37
37
  were generated from) **and** the Robot Framework `output.xml` of the run.
38
38
  - **Out:** an updated TestBench JSON report you can import back into TestBench.
39
- With `-d` it is written to a new location; without it, the input report is
40
- updated in place.
39
+ With `-d` it is written to a new location and the input report stays untouched;
40
+ **without `-d` the input report is overwritten**. All six input/output
41
+ combinations are listed under
42
+ [`output-directory` with `fetch-results`](../configuration/overview.md#output-directory-with-fetch-results).
41
43
 
42
44
  ![Robot Framework log.html](../media/robot-suite-log.png)<br />
43
45
  **Img-1:** Robot Framework `log.html` of a test run
@@ -112,6 +114,13 @@ second run starts over. To combine several Robot runs, merge the output XMLs fir
112
114
  (`rebot --merge output1.xml output2.xml`) and call `fetch-results` once.
113
115
  :::
114
116
 
117
+ ## Tester
118
+
119
+ The results do not name a tester by default. With
120
+ [`set-tester-from-report-creator` / `--set-tester-from-report-creator`](../configuration/overview.md#set-tester-from-report-creator)
121
+ the user who created the TestBench report becomes the tester of every test case the
122
+ run executed with the verdict `Pass` or `Fail`.
123
+
115
124
  ---
116
125
 
117
126
  ## Execution comments
@@ -146,6 +155,34 @@ is controlled by
146
155
  decides what happens when an attachment of the same name already exists. Files
147
156
  larger than 10 MB are skipped with an error message.
148
157
 
158
+ ### The `itb-reference:` marker
159
+
160
+ A marker is the word `itb-reference:` followed by one value without whitespace.
161
+ The marker is removed from the message before it becomes the execution comment;
162
+ a message may carry several markers.
163
+
164
+ ```robotframework
165
+ Set Test Message Screenshot taken.\n\nitb-reference: screenshot.png append=True
166
+ ```
167
+
168
+ The value is resolved like a URI reference:
169
+
170
+ | Value | Meaning |
171
+ |---|---|
172
+ | `screenshot.png`, `results/run.zip` | **Relative** to the directory of the `output.xml` — the Robot output directory. |
173
+ | `file:///var/log/run.zip`, `file:///C:/log/run.zip` | **Absolute** path. A `file:` URI is always absolute ([RFC 8089](https://www.rfc-editor.org/rfc/rfc8089)); `file:///run.zip` is the file `run.zip` in the file system root, *not* in the output directory. |
174
+ | `my%20file.png` | Percent-encoding is decoded. |
175
+
176
+ A relative value is the right choice for files Robot wrote into its output
177
+ directory, and the only form that resolves to the same file both here and in a
178
+ browser showing `log.html`. Use an absolute `file:` URI for files outside the
179
+ output directory. Both are looked up first as given (relative to the current
180
+ working directory), then relative to the `output.xml`.
181
+
182
+ A file that cannot be found is skipped with a warning; with
183
+ `reference-behaviour = "REFERENCE"` an absolute path is stored even if it does
184
+ not exist on the machine running `fetch-results`.
185
+
149
186
  ---
150
187
 
151
188
  ## The full round trip
@@ -260,3 +260,38 @@ Higher-level (compound) TestBench keywords can be rendered as a Robot Framework
260
260
  Extra `Metadata` entries — including values pulled from the test case set model
261
261
  via `{$tcs...}` placeholders — can be added to every suite with
262
262
  [`metadata`](../configuration/overview.md#metadata).
263
+
264
+ ### Use attachments as parameter values
265
+
266
+ A parameter whose representative is a file attached in TestBench arrives in the
267
+ generated keyword call as a path into the report's attachments:
268
+
269
+ ```robotframework
270
+ ${message} Get File ${ITB_ATTACHMENTS_DIR}/representatives/DT-6917529030000126275/file.xml
271
+ ```
272
+
273
+ TestBench exports such files to `attachments/representatives/DT-<data type key>/`
274
+ inside the report. Two ways to make them available at execution time:
275
+
276
+ - Let `generate-tests` copy them next to the suites with
277
+ [`attachments-directory`](../configuration/overview.md#attachments-directory).
278
+ Every suite that uses attachments then defines `${ITB_ATTACHMENTS_DIR}`
279
+ relative to itself and the output directory runs as it is:
280
+
281
+ ```bash
282
+ testbench2robotframework generate-tests --attachments-directory attachments my_report.zip
283
+ robot ./Generated
284
+ ```
285
+
286
+ - Or keep the report where it is and point the variable to its `attachments`
287
+ directory when running:
288
+
289
+ ```bash
290
+ robot --variable ITB_ATTACHMENTS_DIR:/path/to/my_report/attachments ./Generated
291
+ ```
292
+
293
+ The variable's name is set by
294
+ [`attachments-variable`](../configuration/overview.md#attachments-variable).
295
+ Inside compound keywords the data type is taken from the outermost call the value
296
+ was passed down from. If no data type can be found, the file name is written as is
297
+ and a warning is logged.
@@ -24,6 +24,8 @@ dependencies = [
24
24
  ]
25
25
 
26
26
  [project.optional-dependencies]
27
+ # Faster reading and writing of large reports; used automatically when installed.
28
+ fast = ["orjson"]
27
29
  dev = [
28
30
  "build",
29
31
  "check-manifest",
@@ -112,6 +114,10 @@ exclude = [
112
114
  [tool.pytest.ini_options]
113
115
  minversion = "6.0"
114
116
  addopts = "-ra --spec"
117
+ # Test classes end in 'Tests'. The default 'Test*' would also match the TestBench
118
+ # model classes (TestCaseSet, TestStructureTree, ...) imported into test modules
119
+ # and flood the run with PytestCollectionWarnings.
120
+ python_classes = "*Tests"
115
121
  testpaths = [
116
122
  "tests",
117
123
  ]
@@ -17,4 +17,4 @@ suites and enhancing the TestBench report with execution results from Robot Fram
17
17
 
18
18
  from .testbench2robotframework import testbench2robotframework # noqa: F401
19
19
 
20
- __version__ = "2.0.0"
20
+ __version__ = "2.1.0rc1"
@@ -0,0 +1,66 @@
1
+ """Exporting the attachments of a TestBench report next to the generated suites.
2
+
3
+ The report keeps every attached file below 'attachments/': representatives of
4
+ reference data types under 'representatives/DT-<key>/', files of test case sets
5
+ and test cases directly in the folder, and whatever else a TestBench extension
6
+ put there. The whole folder is copied - the library does not know all consumers.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import posixpath
12
+ import shutil
13
+ from dataclasses import dataclass
14
+ from pathlib import Path, PurePath
15
+
16
+ from .config import Configuration
17
+ from .log import logger
18
+ from .utils import get_generation_directory, resolve_root_placeholder
19
+
20
+ REPORT_ATTACHMENTS_DIR = "attachments"
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class AttachmentsExport:
25
+ """Where the attachments go, and how the suites refer to that place."""
26
+
27
+ target: Path
28
+ # Set when 'attachments-directory' is a relative path: the target lies inside the
29
+ # output directory and suites address it relative to themselves, so the whole
30
+ # output directory stays movable. None for an absolute target.
31
+ relative_to_output: PurePath | None
32
+
33
+
34
+ def attachments_export(config: Configuration) -> AttachmentsExport | None:
35
+ if not config.attachments_directory:
36
+ return None
37
+ given = Path(resolve_root_placeholder(config.attachments_directory))
38
+ if given.is_absolute():
39
+ return AttachmentsExport(given, None)
40
+ output_directory = get_generation_directory(config.output_directory)
41
+ return AttachmentsExport(output_directory.with_suffix("") / given, PurePath(given))
42
+
43
+
44
+ def variable_value(export: AttachmentsExport, suite_directory: PurePath) -> str:
45
+ """The value the attachments variable gets in a suite lying in 'suite_directory'.
46
+
47
+ 'suite_directory' is relative to the output directory. Forward slashes
48
+ throughout; Robot Framework and Python accept them on every platform.
49
+ """
50
+ if export.relative_to_output is None:
51
+ return export.target.as_posix()
52
+ relative = posixpath.relpath(export.relative_to_output.as_posix(), suite_directory.as_posix())
53
+ return f"${{CURDIR}}/{relative}"
54
+
55
+
56
+ def copy_attachments(report_directory: Path, target: Path) -> None:
57
+ """Copies the report's attachments folder to 'target', replacing what is there."""
58
+ if target.exists():
59
+ shutil.rmtree(target)
60
+ source = report_directory / REPORT_ATTACHMENTS_DIR
61
+ if not source.is_dir():
62
+ logger.debug(f"The report has no '{REPORT_ATTACHMENTS_DIR}' folder.")
63
+ target.mkdir(parents=True)
64
+ return
65
+ shutil.copytree(source, target)
66
+ logger.info(f"Attachments copied to {target}.")
@@ -100,6 +100,13 @@ def testbench2robotframework_cli():
100
100
  type=click.Path(path_type=Path),
101
101
  help="Directory or ZIP archive containing the generated test suites.",
102
102
  )
103
+ @click.option(
104
+ "--attachments-directory",
105
+ type=str,
106
+ help="""Copies the report's attachments there. A relative path lies inside the
107
+ output directory and the suites address it relative to themselves; an absolute
108
+ path is used as given. Without it, attachments are not exported.""",
109
+ )
103
110
  @click.option(
104
111
  "--compound-keyword-logging",
105
112
  type=click.Choice(["GROUP", "COMMENT", "NONE"], case_sensitive=False),
@@ -173,6 +180,7 @@ def testbench2robotframework_cli():
173
180
  )
174
181
  @click.argument("testbench-report", type=click.Path(path_type=Path))
175
182
  def generate_tests( # noqa: PLR0913
183
+ attachments_directory: str | None,
176
184
  clean: bool | None,
177
185
  compound_keyword_logging: str,
178
186
  config: Path,
@@ -244,6 +252,9 @@ def generate_tests( # noqa: PLR0913
244
252
  configuration["resource-root"] = list(resource_root) or configuration.get(
245
253
  "resource-root", DEFAULT_RESOURCE_ROOTS
246
254
  )
255
+ configuration["attachments-directory"] = attachments_directory or configuration.get(
256
+ "attachments-directory", ""
257
+ )
247
258
  testbench2robotframework(testbench_report, configuration)
248
259
 
249
260
 
@@ -256,6 +267,12 @@ def generate_tests( # noqa: PLR0913
256
267
  help="""Overwrite the main protocol instead of merging the Robot Framework results
257
268
  into the protocol.json that is already part of the TestBench report.""",
258
269
  )
270
+ @click.option(
271
+ "--set-tester-from-report-creator",
272
+ is_flag=True,
273
+ help="""Set the user who created the TestBench report (manifest.json) as tester of
274
+ every test case that this run executed with the verdict Pass or Fail.""",
275
+ )
259
276
  @click.argument("robot-result", type=click.Path(path_type=Path))
260
277
  @click.argument("testbench-report", type=click.Path(path_type=Path))
261
278
  def fetch_results(
@@ -264,6 +281,7 @@ def fetch_results(
264
281
  output_directory: Path,
265
282
  testbench_report: Path,
266
283
  no_merge_protocol: bool,
284
+ set_tester_from_report_creator: bool,
267
285
  ):
268
286
  """
269
287
  Fetch Robot Framework execution results from <output XML> and save to a <TestBench Report>.
@@ -273,6 +291,8 @@ def fetch_results(
273
291
  configuration["merge-protocol"] = False
274
292
  else:
275
293
  configuration["merge-protocol"] = configuration.get("merge-protocol", True)
294
+ if set_tester_from_report_creator:
295
+ configuration["set-tester-from-report-creator"] = True
276
296
  robot2testbench(testbench_report, robot_result, output_directory, configuration)
277
297
 
278
298
 
@@ -237,6 +237,10 @@ class Configuration:
237
237
  _: KW_ONLY
238
238
  include_blocked: bool = False
239
239
  merge_protocol: bool = True
240
+ set_tester_from_report_creator: bool = False
241
+ keep_extracted_report: bool = False
242
+ attachments_directory: str = ""
243
+ attachments_variable: str = "ITB_ATTACHMENTS_DIR"
240
244
  clean_mode: CleanMode = CleanMode.GENERATED
241
245
  keyword_comment_style: KeywordCommentStyle = KeywordCommentStyle.STRUCTURED
242
246
  keyword_comment_max_depth: int = 5
@@ -271,6 +275,12 @@ class Configuration:
271
275
  }
272
276
  ),
273
277
  merge_protocol=dictionary.get("merge-protocol", True),
278
+ set_tester_from_report_creator=dictionary.get(
279
+ "set-tester-from-report-creator", False
280
+ ),
281
+ keep_extracted_report=dictionary.get("keep-extracted-report", False),
282
+ attachments_directory=dictionary.get("attachments-directory", "").replace("\\", "/"),
283
+ attachments_variable=dictionary.get("attachments-variable", "ITB_ATTACHMENTS_DIR"),
274
284
  keyword_comment_style=KeywordCommentStyle(
275
285
  dictionary.get("keyword-comment-style", "STRUCTURED").upper()
276
286
  ),
@@ -9,6 +9,10 @@ from .config import AttachmentConflictBehaviour, ReferenceBehaviour
9
9
  from .log import logger
10
10
  from .model import ReferenceAssignment, ReferenceKind
11
11
 
12
+ # A 'file:' URI is always absolute (RFC 8089): 'file:///x.zip' is '/x.zip', the
13
+ # file system root. Files in the Robot output directory are referenced by a
14
+ # relative value without scheme, which 'ExecutionArtifactInfo' resolves against
15
+ # the directory of the output.xml. See docs/usage/fetch_results.md.
12
16
  FILE_URI_SCHEME = "file://"
13
17
  MEGABYTE = 1000 * 1000
14
18
 
@@ -137,8 +141,12 @@ class ExecutionArtifactStorage:
137
141
  if not attachment_folder_path.exists():
138
142
  attachment_folder_path.mkdir(parents=True, exist_ok=True)
139
143
  if not (attachment_folder_path / filename).exists():
140
- return self._use_new_attachment(filename, artifact_value, attachment_folder_path)
141
- return self._dispatch_attachment_copy(filename, artifact_value, attachment_folder_path)
144
+ stored_file = self._use_new_attachment(filename, artifact_value, attachment_folder_path)
145
+ else:
146
+ stored_file = self._dispatch_attachment_copy(
147
+ filename, artifact_value, attachment_folder_path
148
+ )
149
+ return f"{attachment_folder_path.name}/{stored_file}"
142
150
 
143
151
  def _process_artifact(self, artifact: str) -> str | None:
144
152
  artifact_info = ExecutionArtifactInfo(artifact, self.output_xml)
@@ -0,0 +1,55 @@
1
+ """JSON encoding and decoding of report files - with orjson when it is installed.
2
+
3
+ orjson (Rust) parses and serializes several times faster than the standard
4
+ library, which matters for reports whose test case files reach megabytes.
5
+ It is optional: ``pip install testbench2robotframework[fast]``. Without it the
6
+ standard library is used; the files are identical apart from key order and
7
+ whitespace details neither TestBench nor this package depend on.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ from enum import Enum
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ try:
18
+ import orjson
19
+ except ImportError: # pragma: no cover - depends on the environment
20
+ orjson = None
21
+
22
+ JSONDecodeError = json.JSONDecodeError
23
+
24
+
25
+ def _default(value: Any) -> Any:
26
+ return value.value if isinstance(value, Enum) else str(value)
27
+
28
+
29
+ def dumps(content: Any) -> str:
30
+ """Compact JSON, the way TestBench exports it."""
31
+ if orjson is not None:
32
+ return orjson.dumps(content, default=_default).decode("utf-8")
33
+ return json.dumps(content, default=_default)
34
+
35
+
36
+ def loads(text: str | bytes) -> Any:
37
+ """Raises 'JSONDecodeError' (the standard library's) for invalid input."""
38
+ if orjson is not None:
39
+ try:
40
+ return orjson.loads(text)
41
+ except orjson.JSONDecodeError as error:
42
+ raise JSONDecodeError(str(error), "", 0) from error
43
+ return json.loads(text)
44
+
45
+
46
+ def write_json(filepath: str | Path, content: Any) -> None:
47
+ Path(filepath).write_text(dumps(content), encoding="utf-8")
48
+
49
+
50
+ def read_json_file(filepath: str | Path) -> Any:
51
+ return loads(Path(filepath).read_bytes())
52
+
53
+
54
+ def using_orjson() -> bool:
55
+ return orjson is not None
@@ -1,9 +1,8 @@
1
- import json
2
1
  import sys
3
2
  from dataclasses import dataclass
4
- from json import JSONDecodeError
5
3
  from pathlib import Path
6
4
 
5
+ from .json_codec import JSONDecodeError, read_json_file
7
6
  from .log import logger
8
7
  from .model import (
9
8
  ReferenceAssignment,
@@ -162,8 +161,7 @@ class TestBenchJsonReader:
162
161
 
163
162
  def read_json(filepath: str | Path, silent=True):
164
163
  try:
165
- with Path(filepath).open(encoding="utf-8") as json_file:
166
- return json.load(json_file)
164
+ return read_json_file(filepath)
167
165
  except FileNotFoundError:
168
166
  if not silent:
169
167
  logger.error(f"File '{filepath}' does not exist.")
@@ -1,9 +1,9 @@
1
1
  import json
2
2
  from dataclasses import asdict
3
- from enum import Enum
4
3
  from pathlib import Path
5
4
 
6
5
  from .config import Configuration
6
+ from .json_codec import write_json
7
7
  from .log import logger
8
8
  from .model import (
9
9
  ReferenceAssignment,
@@ -41,36 +41,17 @@ def write_test_structure_element(
41
41
  else:
42
42
  filepath = Path(json_dir) / Path(f"{test_structure_element.uniqueID}.json")
43
43
  content = asdict(test_structure_element)
44
- with Path(filepath).open("w+", encoding="utf8") as output_file:
45
- json.dump(
46
- content,
47
- output_file,
48
- indent=2,
49
- default=lambda o: o.value if isinstance(o, Enum) else str(o),
50
- )
44
+ write_json(filepath, content)
51
45
 
52
46
 
53
47
  def write_main_protocol(json_dir: str, main_protocol: list[TestCaseSetExecutionForImport]) -> None:
54
- protocol = [asdict(tcs) for tcs in main_protocol]
55
- filepath = Path(json_dir) / Path("protocol.json")
56
- with Path(filepath).open("w+", encoding="utf8") as output_file:
57
- json.dump(
58
- protocol,
59
- output_file,
60
- indent=2,
61
- default=lambda o: o.value if isinstance(o, Enum) else str(o),
62
- )
48
+ write_json(Path(json_dir) / "protocol.json", [asdict(tcs) for tcs in main_protocol])
63
49
 
64
50
 
65
51
  def write_references(json_dir: str, references: list[ReferenceAssignment]) -> None:
66
- filepath = Path(json_dir) / Path("references.json")
67
- with Path(filepath).open("w+", encoding="utf8") as output_file:
68
- json.dump(
69
- [asdict(ref) for ref in references],
70
- output_file,
71
- indent=2,
72
- default=lambda o: o.value if isinstance(o, Enum) else str(o),
73
- )
52
+ write_json(Path(json_dir) / "references.json", [asdict(ref) for ref in references])
53
+
54
+
74
55
 
75
56
 
76
57
  def write_default_config(config_file):