testbench2robotframework 2.0.0__tar.gz → 2.0.1rc1__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 (101) hide show
  1. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/.gitignore +0 -1
  2. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/PKG-INFO +6 -4
  3. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/README.md +3 -3
  4. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/docs/configuration/overview.md +91 -0
  5. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/docs/usage/fetch_results.md +32 -2
  6. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/docs/usage/generate_tests.md +35 -0
  7. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/pyproject.toml +6 -0
  8. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/testbench2robotframework/__init__.py +1 -1
  9. testbench2robotframework-2.0.1rc1/testbench2robotframework/attachments.py +66 -0
  10. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/testbench2robotframework/cli.py +11 -0
  11. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/testbench2robotframework/config.py +6 -0
  12. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/testbench2robotframework/execution_artifacts.py +10 -2
  13. testbench2robotframework-2.0.1rc1/testbench2robotframework/json_codec.py +55 -0
  14. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/testbench2robotframework/json_reader.py +2 -4
  15. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/testbench2robotframework/json_writer.py +6 -25
  16. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/testbench2robotframework/model_utils.py +14 -2
  17. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/testbench2robotframework/result_writer.py +46 -24
  18. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/testbench2robotframework/testbench2rf.py +130 -7
  19. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/testbench2robotframework/testbench2robotframework.py +4 -28
  20. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/testbench2robotframework/testsuite_write.py +26 -14
  21. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/testbench2robotframework/utils.py +68 -22
  22. testbench2robotframework-2.0.1rc1/tests/test_attachment_parameters.py +159 -0
  23. testbench2robotframework-2.0.1rc1/tests/test_attachments_export.py +247 -0
  24. testbench2robotframework-2.0.1rc1/tests/test_blocked_filter.py +177 -0
  25. testbench2robotframework-2.0.1rc1/tests/test_clean.py +122 -0
  26. testbench2robotframework-2.0.1rc1/tests/test_config_optional_options.py +96 -0
  27. testbench2robotframework-2.0.1rc1/tests/test_execution_artifacts.py +96 -0
  28. testbench2robotframework-2.0.1rc1/tests/test_fetch_results_output.py +160 -0
  29. testbench2robotframework-2.0.1rc1/tests/test_json_codec.py +29 -0
  30. testbench2robotframework-2.0.1rc1/tests/test_json_writer_tree_order.py +96 -0
  31. testbench2robotframework-2.0.1rc1/tests/test_keyword_comment.py +207 -0
  32. testbench2robotframework-2.0.1rc1/tests/test_main_protocol_reader.py +51 -0
  33. testbench2robotframework-2.0.1rc1/tests/test_open_report.py +67 -0
  34. testbench2robotframework-2.0.1rc1/tests/test_protocol_merge.py +196 -0
  35. testbench2robotframework-2.0.1rc1/tests/test_protocol_references.py +109 -0
  36. testbench2robotframework-2.0.1rc1/tests/test_protocol_rows.py +136 -0
  37. testbench2robotframework-2.0.1rc1/tests/test_subdivision_patterns.py +284 -0
  38. testbench2robotframework-2.0.1rc1/tests/test_tree_verdicts.py +172 -0
  39. testbench2robotframework-2.0.0/README_old.md +0 -284
  40. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/.github/dependabot.yml +0 -0
  41. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/.github/workflows/release-binaries.yml +0 -0
  42. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/.github/workflows/trigger-docs-release.yml +0 -0
  43. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/DEVELOPMENT.md +0 -0
  44. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/ExampleConfiguration/json_config.json +0 -0
  45. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/ExampleConfiguration/pyproject_example.toml +0 -0
  46. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/ExampleConfiguration/toml_config.toml +0 -0
  47. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/LICENSE +0 -0
  48. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/MANIFEST.in +0 -0
  49. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/atest/json_config_tests/1_tfs.robot +0 -0
  50. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/atest/robot/libs/json_config.py +0 -0
  51. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/atest/robot/libs/pyproject_config.py +0 -0
  52. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/atest/robot/resources/file_management.resource +0 -0
  53. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/atest/robot/resources/testbench2robotframework_cli.resource +0 -0
  54. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/atest/robot/rf_tests/cli_interface/write/json_config.robot +0 -0
  55. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/atest/robot/rf_tests/cli_interface/write/no_config_argument.robot +0 -0
  56. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/atest/robot/rf_tests/cli_interface/write/toml_config.robot +0 -0
  57. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/create_json_schema.py +0 -0
  58. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/docs/configuration/_category_.json +0 -0
  59. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/docs/configuration/cli_options.md +0 -0
  60. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/docs/configuration/pyproject_config.md +0 -0
  61. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/docs/getting_started/_category_.json +0 -0
  62. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/docs/getting_started/installation.md +0 -0
  63. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/docs/getting_started/quick_start.md +0 -0
  64. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/docs/intro.md +0 -0
  65. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/docs/media/func_kw.png +0 -0
  66. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/docs/media/itorx-exec-overview.png +0 -0
  67. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/docs/media/itorx-exec.png +0 -0
  68. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/docs/media/robot-setup-teardown.png +0 -0
  69. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/docs/media/robot-suite-code.png +0 -0
  70. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/docs/media/robot-suite-log.png +0 -0
  71. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/docs/media/tc-table.png +0 -0
  72. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/docs/media/tcs-browser.png +0 -0
  73. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/docs/media/tcs-exec-comment.png +0 -0
  74. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/docs/media/tcs-exec-in-TB.png +0 -0
  75. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/docs/media/tcs-sequence.png +0 -0
  76. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/docs/media/te-tree.png +0 -0
  77. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/docs/usage/_category_.json +0 -0
  78. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/images/LibrarySubdivision.PNG +0 -0
  79. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/images/Unbenannt.PNG +0 -0
  80. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/images/generated.png +0 -0
  81. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/images/libraries.PNG +0 -0
  82. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/images/resources.PNG +0 -0
  83. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/images/rfLibraryRootsTestBench.PNG +0 -0
  84. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/images/testbench_rfLibraryRegex.PNG +0 -0
  85. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/images/testbench_rfResourceRegex.PNG +0 -0
  86. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/images/testthemen.PNG +0 -0
  87. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/pyinstaller/imbusTB.ico +0 -0
  88. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/pyinstaller/run.py +0 -0
  89. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/requirements.txt +0 -0
  90. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/robot.toml +0 -0
  91. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/tasks.py +0 -0
  92. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/testbench-tools.zip +0 -0
  93. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/testbench2robotframework/__main__.py +0 -0
  94. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/testbench2robotframework/blocked_filter.py +0 -0
  95. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/testbench2robotframework/execution_comment.py +0 -0
  96. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/testbench2robotframework/html_parser.py +0 -0
  97. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/testbench2robotframework/keyword_comment.py +0 -0
  98. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/testbench2robotframework/log.py +0 -0
  99. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/testbench2robotframework/model.py +0 -0
  100. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/testbench2robotframework/protocol_merge.py +0 -0
  101. {testbench2robotframework-2.0.0 → testbench2robotframework-2.0.1rc1}/testbench2robotframework/robotframework2testbench.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.0.1rc1
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,6 +85,9 @@ 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`) |
89
92
  | [`keyword-comment-style`](#keyword-comment-style) | — | ✅ | F | `STRUCTURED` \| `FLAT` (`STRUCTURED`) |
90
93
  | [`keyword-comment-max-depth`](#keyword-comment-max-depth) | — | ✅ | F | int (`5`) |
@@ -117,6 +120,94 @@ testbench2robotframework generate-tests -d ./suites.zip my_report.zip
117
120
  output-directory = "{root}/Generated"
118
121
  ```
119
122
 
123
+ #### `output-directory` with `fetch-results`
124
+
125
+ For `fetch-results` the option names the updated report. The input report is only
126
+ read when `-d` is given; **without `-d` the input report itself is updated** and
127
+ the original, unexecuted report is gone afterwards.
128
+
129
+ | Input report | `-d` | Result |
130
+ |---|---|---|
131
+ | `report.zip` | `result.zip` | `result.zip` is created; `report.zip` is unchanged. |
132
+ | `report.zip` | `result/` | `result/` is created (or merged into) as a directory; `report.zip` is unchanged. |
133
+ | `report/` | `result.zip` | `result.zip` is created; `report/` is unchanged. |
134
+ | `report/` | `result/` | `result/` is created (or merged into); `report/` is unchanged. |
135
+ | `report.zip` | *none* | `report.zip` is **overwritten** with the updated report. |
136
+ | `report/` | *none* | The files in `report/` are **updated in place**; no ZIP is created. |
137
+
138
+ Keep the original report if you need it for a second run — for example to merge
139
+ another execution with [`merge-protocol`](#merge-protocol) — or always pass `-d`.
140
+
141
+ ### `keep-extracted-report`
142
+
143
+ Where a `.zip` report is extracted to while a command reads it. By default that is
144
+ a temporary directory in the working directory, removed when the command is done.
145
+ With `true` the report is extracted to a directory of the same name next to the
146
+ ZIP (`report.zip` → `report/`), replacing that directory if it exists, and left
147
+ there afterwards — for example to look at the report's attachments. Has no effect
148
+ when the report is given as a directory. **Configuration file only.**
149
+
150
+ - **Values:** `true` / `false`. Default: `false`.
151
+
152
+ ```toml
153
+ keep-extracted-report = true
154
+ ```
155
+
156
+ :::note
157
+ Up to version 2.0 `fetch-results` always left the extracted report next to the
158
+ ZIP. Set `keep-extracted-report = true` to keep that behaviour.
159
+ :::
160
+
161
+ ### `attachments-directory`
162
+
163
+ Where `generate-tests` copies the report's `attachments/` folder to — every
164
+ attachment: representatives of reference data types (`representatives/DT-<key>/`),
165
+ files attached to test case sets and test cases, and whatever else is in there.
166
+ Empty (the default) exports nothing. The target is emptied before copying.
167
+
168
+ - **Values:** a path, empty, or a path starting with `{root}`. Default: empty.
169
+ - **CLI:** `--attachments-directory`.
170
+
171
+ How the path is given decides how the generated suites refer to it:
172
+
173
+ | Value | Copied to | Value of the [attachments variable](#attachments-variable) in every suite |
174
+ |---|---|---|
175
+ | 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 |
176
+ | absolute, e.g. `/data/run1/attachments` | exactly there | `/data/run1/attachments` |
177
+ | `{root}/…` | the absolute path it resolves to | that absolute path |
178
+
179
+ With a `.zip` `output-directory`, a relative attachments directory ends up inside
180
+ the archive.
181
+
182
+ ```bash
183
+ testbench2robotframework generate-tests --attachments-directory attachments my_report.zip
184
+ robot ./Generated # finds the attachments by itself
185
+ ```
186
+ ```toml
187
+ attachments-directory = "attachments"
188
+ ```
189
+
190
+ ### `attachments-variable`
191
+
192
+ The Robot Framework variable through which generated keyword calls address
193
+ attachments, for example
194
+ `${ITB_ATTACHMENTS_DIR}/representatives/DT-6917529030000126275/Vorlage.xml`.
195
+ When [`attachments-directory`](#attachments-directory) is set, every suite whose
196
+ keyword calls refer to the variable gets a `*** Variables ***` section defining it
197
+ (see the table above) — suites without attachments and `__init__.robot` files stay
198
+ as they are. A `--variable` on the `robot` command line still overrides that
199
+ default. Without `attachments-directory` no section is written and the variable
200
+ has to be passed to `robot`. **Configuration file only.**
201
+
202
+ - **Values:** a variable name without `${}`, or empty. Default: `ITB_ATTACHMENTS_DIR`.
203
+ - **Empty:** attachments are written as paths relative to the report's
204
+ `attachments` folder (`representatives/DT-…/Vorlage.xml`) and no section is
205
+ generated — for runs started from inside that folder.
206
+
207
+ ```toml
208
+ attachments-variable = "ITB_ATTACHMENTS_DIR"
209
+ ```
210
+
120
211
  ### `create-output-zip`
121
212
 
122
213
  For `generate-tests` only: in addition to the normal directory output, also
@@ -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
@@ -146,6 +148,34 @@ is controlled by
146
148
  decides what happens when an attachment of the same name already exists. Files
147
149
  larger than 10 MB are skipped with an error message.
148
150
 
151
+ ### The `itb-reference:` marker
152
+
153
+ A marker is the word `itb-reference:` followed by one value without whitespace.
154
+ The marker is removed from the message before it becomes the execution comment;
155
+ a message may carry several markers.
156
+
157
+ ```robotframework
158
+ Set Test Message Screenshot taken.\n\nitb-reference: screenshot.png append=True
159
+ ```
160
+
161
+ The value is resolved like a URI reference:
162
+
163
+ | Value | Meaning |
164
+ |---|---|
165
+ | `screenshot.png`, `results/run.zip` | **Relative** to the directory of the `output.xml` — the Robot output directory. |
166
+ | `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. |
167
+ | `my%20file.png` | Percent-encoding is decoded. |
168
+
169
+ A relative value is the right choice for files Robot wrote into its output
170
+ directory, and the only form that resolves to the same file both here and in a
171
+ browser showing `log.html`. Use an absolute `file:` URI for files outside the
172
+ output directory. Both are looked up first as given (relative to the current
173
+ working directory), then relative to the `output.xml`.
174
+
175
+ A file that cannot be found is skipped with a warning; with
176
+ `reference-behaviour = "REFERENCE"` an absolute path is stored even if it does
177
+ not exist on the machine running `fetch-results`.
178
+
149
179
  ---
150
180
 
151
181
  ## 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.0.1rc1"
@@ -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
 
@@ -237,6 +237,9 @@ class Configuration:
237
237
  _: KW_ONLY
238
238
  include_blocked: bool = False
239
239
  merge_protocol: bool = True
240
+ keep_extracted_report: bool = False
241
+ attachments_directory: str = ""
242
+ attachments_variable: str = "ITB_ATTACHMENTS_DIR"
240
243
  clean_mode: CleanMode = CleanMode.GENERATED
241
244
  keyword_comment_style: KeywordCommentStyle = KeywordCommentStyle.STRUCTURED
242
245
  keyword_comment_max_depth: int = 5
@@ -271,6 +274,9 @@ class Configuration:
271
274
  }
272
275
  ),
273
276
  merge_protocol=dictionary.get("merge-protocol", True),
277
+ keep_extracted_report=dictionary.get("keep-extracted-report", False),
278
+ attachments_directory=dictionary.get("attachments-directory", "").replace("\\", "/"),
279
+ attachments_variable=dictionary.get("attachments-variable", "ITB_ATTACHMENTS_DIR"),
274
280
  keyword_comment_style=KeywordCommentStyle(
275
281
  dictionary.get("keyword-comment-style", "STRUCTURED").upper()
276
282
  ),
@@ -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):
@@ -1,5 +1,6 @@
1
1
  from dataclasses import fields, is_dataclass
2
2
  from enum import Enum
3
+ from functools import cache
3
4
  from types import UnionType as TypesUnion
4
5
  from typing import Any, TypeVar, get_args, get_origin, get_type_hints
5
6
  from typing import Union as TypingUnion
@@ -36,14 +37,25 @@ def get_origin_from_type_hint(type_hint):
36
37
  raise ValueError(ERROR_UNKNOWN_TYPE_HINT_ORIGIN)
37
38
 
38
39
 
40
+ @cache
41
+ def _type_hints_and_fields(cls: type) -> tuple[dict[str, Any], tuple]:
42
+ """Resolved annotations and fields of a dataclass, computed once per class.
43
+
44
+ 'get_type_hints' evaluates every annotation anew on each call - with lazy
45
+ annotations (Python 3.14) that means compiling the forward references again.
46
+ A report holds hundreds of thousands of model objects, so this dominated the
47
+ whole read time before it was cached.
48
+ """
49
+ return get_type_hints(cls), fields(cls)
50
+
51
+
39
52
  def from_dict(cls: type[T], data: dict) -> T:
40
53
  if not isinstance(cls, type) or not is_dataclass(cls):
41
54
  raise ValueError(ERROR_NOT_A_DATACLASS.format(dataclass=cls.__name__))
42
55
  if data is None:
43
56
  raise ValueError(ERROR_NONETYPE_DATA)
44
57
  cls_dict = {}
45
- class_type_hints = get_type_hints(cls)
46
- class_fields = fields(cls)
58
+ class_type_hints, class_fields = _type_hints_and_fields(cls)
47
59
  if len(class_fields) < len(data):
48
60
  raise ValueError(ERROR_TOO_MANY_DATA_FIELDS)
49
61
  for cls_field in class_fields: