testbench2robotframework 2.0.1rc1__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 (101) hide show
  1. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/PKG-INFO +1 -1
  2. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/docs/configuration/overview.md +24 -0
  3. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/docs/usage/fetch_results.md +7 -0
  4. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/__init__.py +1 -1
  5. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/cli.py +9 -0
  6. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/config.py +4 -0
  7. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/result_writer.py +44 -1
  8. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/robotframework2testbench.py +4 -2
  9. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/testbench2robotframework.py +3 -1
  10. testbench2robotframework-2.1.0rc1/tests/test_report_creator_tester.py +234 -0
  11. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/.github/dependabot.yml +0 -0
  12. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/.github/workflows/release-binaries.yml +0 -0
  13. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/.github/workflows/trigger-docs-release.yml +0 -0
  14. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/.gitignore +0 -0
  15. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/DEVELOPMENT.md +0 -0
  16. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/ExampleConfiguration/json_config.json +0 -0
  17. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/ExampleConfiguration/pyproject_example.toml +0 -0
  18. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/ExampleConfiguration/toml_config.toml +0 -0
  19. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/LICENSE +0 -0
  20. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/MANIFEST.in +0 -0
  21. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/README.md +0 -0
  22. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/atest/json_config_tests/1_tfs.robot +0 -0
  23. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/atest/robot/libs/json_config.py +0 -0
  24. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/atest/robot/libs/pyproject_config.py +0 -0
  25. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/atest/robot/resources/file_management.resource +0 -0
  26. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/atest/robot/resources/testbench2robotframework_cli.resource +0 -0
  27. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/atest/robot/rf_tests/cli_interface/write/json_config.robot +0 -0
  28. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/atest/robot/rf_tests/cli_interface/write/no_config_argument.robot +0 -0
  29. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/atest/robot/rf_tests/cli_interface/write/toml_config.robot +0 -0
  30. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/create_json_schema.py +0 -0
  31. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/docs/configuration/_category_.json +0 -0
  32. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/docs/configuration/cli_options.md +0 -0
  33. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/docs/configuration/pyproject_config.md +0 -0
  34. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/docs/getting_started/_category_.json +0 -0
  35. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/docs/getting_started/installation.md +0 -0
  36. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/docs/getting_started/quick_start.md +0 -0
  37. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/docs/intro.md +0 -0
  38. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/docs/media/func_kw.png +0 -0
  39. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/docs/media/itorx-exec-overview.png +0 -0
  40. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/docs/media/itorx-exec.png +0 -0
  41. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/docs/media/robot-setup-teardown.png +0 -0
  42. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/docs/media/robot-suite-code.png +0 -0
  43. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/docs/media/robot-suite-log.png +0 -0
  44. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/docs/media/tc-table.png +0 -0
  45. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/docs/media/tcs-browser.png +0 -0
  46. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/docs/media/tcs-exec-comment.png +0 -0
  47. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/docs/media/tcs-exec-in-TB.png +0 -0
  48. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/docs/media/tcs-sequence.png +0 -0
  49. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/docs/media/te-tree.png +0 -0
  50. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/docs/usage/_category_.json +0 -0
  51. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/docs/usage/generate_tests.md +0 -0
  52. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/images/LibrarySubdivision.PNG +0 -0
  53. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/images/Unbenannt.PNG +0 -0
  54. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/images/generated.png +0 -0
  55. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/images/libraries.PNG +0 -0
  56. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/images/resources.PNG +0 -0
  57. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/images/rfLibraryRootsTestBench.PNG +0 -0
  58. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/images/testbench_rfLibraryRegex.PNG +0 -0
  59. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/images/testbench_rfResourceRegex.PNG +0 -0
  60. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/images/testthemen.PNG +0 -0
  61. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/pyinstaller/imbusTB.ico +0 -0
  62. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/pyinstaller/run.py +0 -0
  63. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/pyproject.toml +0 -0
  64. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/requirements.txt +0 -0
  65. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/robot.toml +0 -0
  66. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/tasks.py +0 -0
  67. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/testbench-tools.zip +0 -0
  68. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/__main__.py +0 -0
  69. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/attachments.py +0 -0
  70. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/blocked_filter.py +0 -0
  71. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/execution_artifacts.py +0 -0
  72. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/execution_comment.py +0 -0
  73. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/html_parser.py +0 -0
  74. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/json_codec.py +0 -0
  75. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/json_reader.py +0 -0
  76. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/json_writer.py +0 -0
  77. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/keyword_comment.py +0 -0
  78. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/log.py +0 -0
  79. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/model.py +0 -0
  80. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/model_utils.py +0 -0
  81. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/protocol_merge.py +0 -0
  82. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/testbench2rf.py +0 -0
  83. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/testsuite_write.py +0 -0
  84. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/testbench2robotframework/utils.py +0 -0
  85. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/tests/test_attachment_parameters.py +0 -0
  86. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/tests/test_attachments_export.py +0 -0
  87. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/tests/test_blocked_filter.py +0 -0
  88. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/tests/test_clean.py +0 -0
  89. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/tests/test_config_optional_options.py +0 -0
  90. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/tests/test_execution_artifacts.py +0 -0
  91. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/tests/test_fetch_results_output.py +0 -0
  92. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/tests/test_json_codec.py +0 -0
  93. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/tests/test_json_writer_tree_order.py +0 -0
  94. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/tests/test_keyword_comment.py +0 -0
  95. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/tests/test_main_protocol_reader.py +0 -0
  96. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/tests/test_open_report.py +0 -0
  97. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/tests/test_protocol_merge.py +0 -0
  98. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/tests/test_protocol_references.py +0 -0
  99. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/tests/test_protocol_rows.py +0 -0
  100. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/tests/test_subdivision_patterns.py +0 -0
  101. {testbench2robotframework-2.0.1rc1 → testbench2robotframework-2.1.0rc1}/tests/test_tree_verdicts.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: testbench2robotframework
3
- Version: 2.0.1rc1
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
@@ -89,6 +89,7 @@ file. This is noted per option below.
89
89
  | [`attachments-variable`](#attachments-variable) | — | ✅ | G | variable name (`ITB_ATTACHMENTS_DIR`) |
90
90
  | [`keep-extracted-report`](#keep-extracted-report) | — | ✅ | G · F | bool (`false`) |
91
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`) |
92
93
  | [`keyword-comment-style`](#keyword-comment-style) | — | ✅ | F | `STRUCTURED` \| `FLAT` (`STRUCTURED`) |
93
94
  | [`keyword-comment-max-depth`](#keyword-comment-max-depth) | — | ✅ | F | int (`5`) |
94
95
  | [`keyword-comment-max-rows`](#keyword-comment-max-rows) | — | ✅ | F | int, `0` = no limit (`300`) |
@@ -535,6 +536,29 @@ testbench2robotframework fetch-results --no-merge-protocol output.xml my_report.
535
536
  merge-protocol = true
536
537
  ```
537
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
+
538
562
  ### `keyword-comment-style`
539
563
 
540
564
  How the execution comment of a keyword is rendered. `STRUCTURED` shows the whole
@@ -114,6 +114,13 @@ second run starts over. To combine several Robot runs, merge the output XMLs fir
114
114
  (`rebot --merge output1.xml output2.xml`) and call `fetch-results` once.
115
115
  :::
116
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
+
117
124
  ---
118
125
 
119
126
  ## Execution comments
@@ -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.1rc1"
20
+ __version__ = "2.1.0rc1"
@@ -267,6 +267,12 @@ def generate_tests( # noqa: PLR0913
267
267
  help="""Overwrite the main protocol instead of merging the Robot Framework results
268
268
  into the protocol.json that is already part of the TestBench report.""",
269
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
+ )
270
276
  @click.argument("robot-result", type=click.Path(path_type=Path))
271
277
  @click.argument("testbench-report", type=click.Path(path_type=Path))
272
278
  def fetch_results(
@@ -275,6 +281,7 @@ def fetch_results(
275
281
  output_directory: Path,
276
282
  testbench_report: Path,
277
283
  no_merge_protocol: bool,
284
+ set_tester_from_report_creator: bool,
278
285
  ):
279
286
  """
280
287
  Fetch Robot Framework execution results from <output XML> and save to a <TestBench Report>.
@@ -284,6 +291,8 @@ def fetch_results(
284
291
  configuration["merge-protocol"] = False
285
292
  else:
286
293
  configuration["merge-protocol"] = configuration.get("merge-protocol", True)
294
+ if set_tester_from_report_creator:
295
+ configuration["set-tester-from-report-creator"] = True
287
296
  robot2testbench(testbench_report, robot_result, output_directory, configuration)
288
297
 
289
298
 
@@ -237,6 +237,7 @@ 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
240
241
  keep_extracted_report: bool = False
241
242
  attachments_directory: str = ""
242
243
  attachments_variable: str = "ITB_ATTACHMENTS_DIR"
@@ -274,6 +275,9 @@ class Configuration:
274
275
  }
275
276
  ),
276
277
  merge_protocol=dictionary.get("merge-protocol", True),
278
+ set_tester_from_report_creator=dictionary.get(
279
+ "set-tester-from-report-creator", False
280
+ ),
277
281
  keep_extracted_report=dictionary.get("keep-extracted-report", False),
278
282
  attachments_directory=dictionary.get("attachments-directory", "").replace("\\", "/"),
279
283
  attachments_variable=dictionary.get("attachments-variable", "ITB_ATTACHMENTS_DIR"),
@@ -1,4 +1,5 @@
1
1
  import html
2
+ import json
2
3
  import os
3
4
  import re
4
5
  import shutil
@@ -40,6 +41,7 @@ from .model import (
40
41
  TestCaseNode,
41
42
  TestCaseSetExecutionForImport,
42
43
  TestCaseSetNode,
44
+ UserInfo,
43
45
  UserReference,
44
46
  VerdictStatus,
45
47
  )
@@ -52,7 +54,7 @@ from .protocol_merge import (
52
54
  index_by_test_case_set_key,
53
55
  merge_test_case_executions,
54
56
  )
55
- from .utils import directory_to_zip, open_report
57
+ from .utils import directory_to_zip, open_report, read_manifest_json_from_testbench_report
56
58
 
57
59
  try:
58
60
  from robot.result import Group
@@ -200,6 +202,32 @@ def unexecuted_table_row(unique_id: str, verdict) -> TestCaseRow:
200
202
  )
201
203
 
202
204
 
205
+ def read_report_creator(json_dir: str | Path) -> UserInfo | None:
206
+ """The user who created the report, as named in its 'manifest.json'.
207
+
208
+ Returns None, and warns, when the manifest is missing, unreadable or names no creator.
209
+ """
210
+ try:
211
+ manifest = read_manifest_json_from_testbench_report(Path(json_dir))
212
+ except (FileNotFoundError, json.JSONDecodeError) as e:
213
+ logger.warning(f"Could not read the report's manifest.json, no tester is set: {e}")
214
+ return None
215
+ report_creation = manifest.get("reportCreation") if isinstance(manifest, dict) else None
216
+ creator = (report_creation or {}).get("creator") or {}
217
+ user_key = creator.get("userKey")
218
+ if user_key in (None, ""):
219
+ logger.warning(
220
+ "The report's manifest.json names no creator (reportCreation.creator.userKey), "
221
+ "no tester is set."
222
+ )
223
+ return None
224
+ return UserInfo(
225
+ userKey=str(user_key),
226
+ userLogin=creator.get("userLogin", ""),
227
+ userName=creator.get("userName", ""),
228
+ )
229
+
230
+
203
231
  class ResultWriter(ResultVisitor):
204
232
  def __init__(
205
233
  self,
@@ -243,6 +271,9 @@ class ResultWriter(ResultVisitor):
243
271
  )
244
272
  self.base_protocol_by_key = index_by_test_case_set_key(self.base_protocol)
245
273
  self.merged_verdicts: dict[str, Verdict] = {}
274
+ self.report_creator: UserInfo | None = (
275
+ read_report_creator(self.json_dir) if config.set_tester_from_report_creator else None
276
+ )
246
277
  self.keyword_comment_style = config.keyword_comment_style
247
278
  self.keyword_comment_renderer = KeywordCommentRenderer(
248
279
  max_depth=config.keyword_comment_max_depth,
@@ -319,6 +350,7 @@ class ResultWriter(ResultVisitor):
319
350
  step.exec = _empty_keyword_call_execution()
320
351
  step.exec.verdict = KeywordVerdict.Skipped
321
352
  self._set_itb_testcase_execution_result(itb_test_case, self.test_chain)
353
+ self._set_itb_testcase_tester(itb_test_case)
322
354
  self._set_itb_testcase_execution_comment(itb_test_case, self.test_chain)
323
355
  self._set_itb_testcase_references(itb_test_case, self.test_chain)
324
356
  except TypeError as e:
@@ -445,6 +477,17 @@ class ResultWriter(ResultVisitor):
445
477
  protocol_result = self._set_itb_test_case_status(itb_test_case, "undef")
446
478
  self.protocol_test_case.result = protocol_result
447
479
 
480
+ def _set_itb_testcase_tester(self, itb_test_case: TestCaseDetails):
481
+ """Makes the report's creator the tester of a test case this run performed."""
482
+ if self.report_creator is None or not itb_test_case.exec:
483
+ return
484
+ if self.protocol_test_case.result.status != ActivityStatus.Performed:
485
+ return
486
+ self.protocol_test_case.testerKey = self.report_creator.userKey
487
+ itb_test_case.exec.tester = UserReference(
488
+ key=self.report_creator.userKey, name=self.report_creator.userName
489
+ )
490
+
448
491
  def _get_test_phase_body(self, test_phase: TestCase) -> list[Keyword]:
449
492
  return self._get_keywords_from_rf_body(test_phase)
450
493
 
@@ -14,7 +14,7 @@ def robot2testbench(
14
14
  json_input_report: str | Path,
15
15
  robot_result_xml: str | Path,
16
16
  json_output_result: str | Path | None = None,
17
- config: dict | None = None,
17
+ config: dict | Configuration | None = None,
18
18
  ):
19
19
  if not Path(json_input_report).exists():
20
20
  sys.exit("No TestBench report found at the given path: expected a directory or a ZIP file.")
@@ -22,7 +22,9 @@ def robot2testbench(
22
22
  sys.exit("The Robot Framework result XML does not exist at the given path.")
23
23
  perform_version_check(Path(json_input_report))
24
24
 
25
- configuration = Configuration.from_dict(config)
25
+ configuration = (
26
+ config if isinstance(config, Configuration) else Configuration.from_dict(config or {})
27
+ )
26
28
  setup_logger(configuration)
27
29
  logger.debug("Configuration loaded.")
28
30
  result = ExecutionResult(robot_result_xml)
@@ -11,7 +11,9 @@ from .utils import PathResolver, open_report, perform_version_check
11
11
 
12
12
  def testbench2robotframework(testbench_report: str | Path, config: dict | Configuration):
13
13
  perform_version_check(Path(testbench_report))
14
- configuration = Configuration.from_dict(config) if isinstance(config, dict) else config
14
+ configuration = (
15
+ config if isinstance(config, Configuration) else Configuration.from_dict(config or {})
16
+ )
15
17
  setup_logger(configuration)
16
18
  logger.debug("Configuration loaded.")
17
19
  with open_report(Path(testbench_report), configuration.keep_extracted_report) as report:
@@ -0,0 +1,234 @@
1
+ """'set-tester-from-report-creator' makes the report's creator the tester of executed test cases.
2
+
3
+ The creator is read from 'reportCreation.creator' in the report's 'manifest.json'.
4
+ Only test cases this run actually performed (verdict Pass or Fail) get the tester,
5
+ in 'protocol.json' ('testerKey') as well as in their test case file ('exec.tester').
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ from types import SimpleNamespace
11
+
12
+ import pytest
13
+ from robot.result import TestCase
14
+ from test_fetch_results_output import make_report_dir
15
+
16
+ from testbench2robotframework import robotframework2testbench
17
+ from testbench2robotframework.config import Configuration
18
+ from testbench2robotframework.log import logger
19
+ from testbench2robotframework.model import (
20
+ ActivityStatus,
21
+ ExecStatus,
22
+ TestCaseDetails,
23
+ TestCaseExecutionDetails,
24
+ TestCaseExecutionForImport,
25
+ UserInfo,
26
+ UserReference,
27
+ VerdictStatus,
28
+ )
29
+ from testbench2robotframework.result_writer import ResultWriter, read_report_creator
30
+
31
+ UID = "itb-TC-136099-PC-421880"
32
+
33
+ MANIFEST = {
34
+ "formatVersion": "1.0",
35
+ "serverLocation": {"host": "Salsa.local", "port": 443},
36
+ "reportCreation": {
37
+ "creator": {"userKey": "0", "userLogin": "tt-admin", "userName": "Administrator"},
38
+ "startDate": "2026-09-23T15:49:45.845+02:00",
39
+ "endDate": "2026-09-23T15:49:45.975+02:00",
40
+ "scope": {"projectKey": "50012", "tovKey": "60034", "cycleKey": "40037"},
41
+ "summary": {"testThemesCount": 0, "testCaseSetsCount": 1, "testCasesCount": 3},
42
+ },
43
+ "serverVersions": {"version": "4.1", "databaseVersion": "4.1.01", "revision": "260723/5db9"},
44
+ }
45
+
46
+ CREATOR = UserInfo(userKey="0", userLogin="tt-admin", userName="Administrator")
47
+
48
+
49
+ def make_itb_test_case(tester=None):
50
+ return TestCaseDetails(
51
+ uniqueID=UID,
52
+ spec=None,
53
+ testSequence=[],
54
+ parameters=[],
55
+ keywords=[],
56
+ exec=TestCaseExecutionDetails(
57
+ key="79361",
58
+ status=ActivityStatus.Planned,
59
+ execStatus=ExecStatus.NotBlocked,
60
+ verdict=VerdictStatus.Undefined,
61
+ plannedDuration=0,
62
+ actualDuration=0,
63
+ currentUser=UserReference(key="0", name="tt-admin"),
64
+ comments="",
65
+ defects=[],
66
+ udfs=[],
67
+ tags=[],
68
+ references=[],
69
+ tester=tester,
70
+ ),
71
+ )
72
+
73
+
74
+ def make_result_writer(report_creator):
75
+ writer = ResultWriter.__new__(ResultWriter) # bypasses the file system heavy __init__
76
+ writer.report_creator = report_creator
77
+ writer.protocol_test_case = TestCaseExecutionForImport(UID, "79361", None, None, None) # type: ignore[arg-type]
78
+ return writer
79
+
80
+
81
+ def execute(writer, itb_test_case, *statuses):
82
+ phases = [TestCase(name=UID, status=status) for status in statuses]
83
+ writer._set_itb_testcase_execution_result(itb_test_case, phases)
84
+ writer._set_itb_testcase_tester(itb_test_case)
85
+
86
+
87
+ @pytest.fixture
88
+ def warnings():
89
+ records: list[logging.LogRecord] = []
90
+
91
+ class Collector(logging.Handler):
92
+ def emit(self, record):
93
+ if record.levelno == logging.WARNING:
94
+ records.append(record)
95
+
96
+ handler = Collector()
97
+ logger.addHandler(handler)
98
+ yield records
99
+ logger.removeHandler(handler)
100
+
101
+
102
+ class ReadReportCreatorTests:
103
+ def test_creator_is_read_from_manifest(self, tmp_path):
104
+ (tmp_path / "manifest.json").write_text(json.dumps(MANIFEST))
105
+
106
+ assert read_report_creator(tmp_path) == CREATOR
107
+
108
+ def test_missing_manifest_warns_and_returns_none(self, tmp_path, warnings):
109
+ assert read_report_creator(tmp_path) is None
110
+ assert len(warnings) == 1
111
+
112
+ def test_manifest_without_creator_warns_and_returns_none(self, tmp_path, warnings):
113
+ (tmp_path / "manifest.json").write_text(json.dumps({"formatVersion": "1.0"}))
114
+
115
+ assert read_report_creator(tmp_path) is None
116
+ assert len(warnings) == 1
117
+
118
+ def test_creator_without_user_key_warns_and_returns_none(self, tmp_path, warnings):
119
+ manifest = {"reportCreation": {"creator": {"userLogin": "tt-admin", "userName": "A"}}}
120
+ (tmp_path / "manifest.json").write_text(json.dumps(manifest))
121
+
122
+ assert read_report_creator(tmp_path) is None
123
+ assert len(warnings) == 1
124
+
125
+ def test_invalid_manifest_warns_and_returns_none(self, tmp_path, warnings):
126
+ (tmp_path / "manifest.json").write_text("{ not json")
127
+
128
+ assert read_report_creator(tmp_path) is None
129
+ assert len(warnings) == 1
130
+
131
+
132
+ class SetTesterTests:
133
+ @pytest.mark.parametrize("statuses", [("PASS",), ("FAIL",), ("PASS", "FAIL")])
134
+ def test_performed_test_case_gets_the_creator_as_tester(self, statuses):
135
+ writer = make_result_writer(CREATOR)
136
+ itb_test_case = make_itb_test_case()
137
+
138
+ execute(writer, itb_test_case, *statuses)
139
+
140
+ assert writer.protocol_test_case.testerKey == "0"
141
+ assert itb_test_case.exec.tester == UserReference(key="0", name="Administrator")
142
+
143
+ @pytest.mark.parametrize("statuses", [("SKIP",), ("NOT RUN",), ("PASS", "SKIP")])
144
+ def test_test_case_not_performed_gets_no_tester(self, statuses):
145
+ writer = make_result_writer(CREATOR)
146
+ itb_test_case = make_itb_test_case()
147
+
148
+ execute(writer, itb_test_case, *statuses)
149
+
150
+ assert writer.protocol_test_case.testerKey is None
151
+ assert itb_test_case.exec.tester is None
152
+
153
+ def test_existing_tester_is_overwritten(self):
154
+ writer = make_result_writer(CREATOR)
155
+ itb_test_case = make_itb_test_case(tester=UserReference(key="7", name="Neuer Benutzer 2"))
156
+
157
+ execute(writer, itb_test_case, "PASS")
158
+
159
+ assert itb_test_case.exec.tester == UserReference(key="0", name="Administrator")
160
+
161
+ def test_without_creator_nothing_is_set(self):
162
+ writer = make_result_writer(None)
163
+ existing_tester = UserReference(key="7", name="Neuer Benutzer 2")
164
+ itb_test_case = make_itb_test_case(tester=existing_tester)
165
+
166
+ execute(writer, itb_test_case, "PASS")
167
+
168
+ assert writer.protocol_test_case.testerKey is None
169
+ assert itb_test_case.exec.tester == existing_tester
170
+
171
+
172
+ class OptionTests:
173
+ def test_option_is_off_by_default(self):
174
+ assert Configuration.from_dict({}).set_tester_from_report_creator is False
175
+
176
+ def test_option_can_be_switched_on(self):
177
+ configuration = Configuration.from_dict({"set-tester-from-report-creator": True})
178
+
179
+ assert configuration.set_tester_from_report_creator is True
180
+
181
+
182
+ class ResultWriterOptionTests:
183
+ @pytest.fixture(autouse=True)
184
+ def run_in_tmp_path(self, tmp_path, monkeypatch):
185
+ # The writer creates its temporary directories in the current working directory.
186
+ monkeypatch.chdir(tmp_path)
187
+
188
+ def make_writer(self, tmp_path, manifest, **options):
189
+ report = make_report_dir(tmp_path / "report")
190
+ (report / "manifest.json").write_text(json.dumps(manifest))
191
+ (tmp_path / "output.xml").write_text("<robot/>")
192
+ cfg = Configuration.from_dict({"merge-protocol": False, **options})
193
+ return ResultWriter(str(report), None, cfg, str(tmp_path / "output.xml"))
194
+
195
+ def test_with_option_the_creator_is_read(self, tmp_path):
196
+ writer = self.make_writer(tmp_path, MANIFEST, **{"set-tester-from-report-creator": True})
197
+
198
+ assert writer.report_creator == CREATOR
199
+
200
+ def test_without_option_the_manifest_is_not_consulted(self, tmp_path, warnings):
201
+ writer = self.make_writer(tmp_path, {})
202
+
203
+ assert writer.report_creator is None
204
+ assert not [record for record in warnings if "manifest.json" in record.getMessage()]
205
+
206
+
207
+ class Robot2TestbenchConfigurationTests:
208
+ """Library callers may pass a Configuration instead of a config file dictionary.
209
+
210
+ 'dataclasses.asdict' of a Configuration uses field names, which 'from_dict' does
211
+ not read - converting it back to a dictionary would silently drop every option.
212
+ """
213
+
214
+ def test_configuration_instance_is_used_as_is(self, tmp_path, monkeypatch):
215
+ monkeypatch.chdir(tmp_path)
216
+ report = make_report_dir(tmp_path / "report")
217
+ (report / "manifest.json").write_text(json.dumps(MANIFEST))
218
+ (tmp_path / "output.xml").write_text("<robot/>")
219
+ used = {}
220
+ monkeypatch.setattr(robotframework2testbench, "perform_version_check", lambda _: None)
221
+ monkeypatch.setattr(robotframework2testbench, "setup_logger", lambda _: None)
222
+ monkeypatch.setattr(
223
+ robotframework2testbench,
224
+ "ResultWriter",
225
+ lambda _report, _result, configuration, _xml: used.setdefault("config", configuration),
226
+ )
227
+ monkeypatch.setattr(
228
+ robotframework2testbench, "ExecutionResult", lambda _: SimpleNamespace(visit=lambda _: None)
229
+ )
230
+ configuration = Configuration.from_dict({"set-tester-from-report-creator": True})
231
+
232
+ robotframework2testbench.robot2testbench(report, tmp_path / "output.xml", None, configuration)
233
+
234
+ assert used["config"] is configuration