smart-tests-cli 2.0.0__py3-none-any.whl

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 (96) hide show
  1. smart_tests/__init__.py +0 -0
  2. smart_tests/__main__.py +60 -0
  3. smart_tests/app.py +67 -0
  4. smart_tests/args4p/README.md +102 -0
  5. smart_tests/args4p/__init__.py +13 -0
  6. smart_tests/args4p/argument.py +45 -0
  7. smart_tests/args4p/command.py +593 -0
  8. smart_tests/args4p/converters/__init__.py +75 -0
  9. smart_tests/args4p/decorators.py +98 -0
  10. smart_tests/args4p/exceptions.py +12 -0
  11. smart_tests/args4p/option.py +85 -0
  12. smart_tests/args4p/parameter.py +84 -0
  13. smart_tests/args4p/typer/__init__.py +42 -0
  14. smart_tests/commands/__init__.py +0 -0
  15. smart_tests/commands/compare/__init__.py +11 -0
  16. smart_tests/commands/compare/subsets.py +58 -0
  17. smart_tests/commands/detect_flakes.py +105 -0
  18. smart_tests/commands/inspect/__init__.py +13 -0
  19. smart_tests/commands/inspect/model.py +52 -0
  20. smart_tests/commands/inspect/subset.py +138 -0
  21. smart_tests/commands/record/__init__.py +19 -0
  22. smart_tests/commands/record/attachment.py +38 -0
  23. smart_tests/commands/record/build.py +356 -0
  24. smart_tests/commands/record/case_event.py +190 -0
  25. smart_tests/commands/record/commit.py +157 -0
  26. smart_tests/commands/record/session.py +120 -0
  27. smart_tests/commands/record/tests.py +498 -0
  28. smart_tests/commands/stats/__init__.py +11 -0
  29. smart_tests/commands/stats/test_sessions.py +45 -0
  30. smart_tests/commands/subset.py +567 -0
  31. smart_tests/commands/test_path_writer.py +51 -0
  32. smart_tests/commands/verify.py +153 -0
  33. smart_tests/jar/exe_deploy.jar +0 -0
  34. smart_tests/plugins/__init__.py +0 -0
  35. smart_tests/test_runners/__init__.py +0 -0
  36. smart_tests/test_runners/adb.py +24 -0
  37. smart_tests/test_runners/ant.py +35 -0
  38. smart_tests/test_runners/bazel.py +103 -0
  39. smart_tests/test_runners/behave.py +62 -0
  40. smart_tests/test_runners/codeceptjs.py +33 -0
  41. smart_tests/test_runners/ctest.py +164 -0
  42. smart_tests/test_runners/cts.py +189 -0
  43. smart_tests/test_runners/cucumber.py +451 -0
  44. smart_tests/test_runners/cypress.py +46 -0
  45. smart_tests/test_runners/dotnet.py +106 -0
  46. smart_tests/test_runners/file.py +20 -0
  47. smart_tests/test_runners/flutter.py +251 -0
  48. smart_tests/test_runners/go_test.py +99 -0
  49. smart_tests/test_runners/googletest.py +34 -0
  50. smart_tests/test_runners/gradle.py +96 -0
  51. smart_tests/test_runners/jest.py +52 -0
  52. smart_tests/test_runners/maven.py +149 -0
  53. smart_tests/test_runners/minitest.py +40 -0
  54. smart_tests/test_runners/nunit.py +190 -0
  55. smart_tests/test_runners/playwright.py +252 -0
  56. smart_tests/test_runners/prove.py +74 -0
  57. smart_tests/test_runners/pytest.py +358 -0
  58. smart_tests/test_runners/raw.py +238 -0
  59. smart_tests/test_runners/robot.py +125 -0
  60. smart_tests/test_runners/rspec.py +5 -0
  61. smart_tests/test_runners/smart_tests.py +235 -0
  62. smart_tests/test_runners/vitest.py +49 -0
  63. smart_tests/test_runners/xctest.py +79 -0
  64. smart_tests/testpath.py +154 -0
  65. smart_tests/utils/__init__.py +0 -0
  66. smart_tests/utils/authentication.py +78 -0
  67. smart_tests/utils/ci_provider.py +7 -0
  68. smart_tests/utils/commands.py +14 -0
  69. smart_tests/utils/commit_ingester.py +59 -0
  70. smart_tests/utils/common_tz.py +12 -0
  71. smart_tests/utils/edit_distance.py +11 -0
  72. smart_tests/utils/env_keys.py +19 -0
  73. smart_tests/utils/exceptions.py +34 -0
  74. smart_tests/utils/fail_fast_mode.py +99 -0
  75. smart_tests/utils/file_name_pattern.py +4 -0
  76. smart_tests/utils/git_log_parser.py +53 -0
  77. smart_tests/utils/glob.py +44 -0
  78. smart_tests/utils/gzipgen.py +46 -0
  79. smart_tests/utils/http_client.py +169 -0
  80. smart_tests/utils/java.py +61 -0
  81. smart_tests/utils/link.py +149 -0
  82. smart_tests/utils/logger.py +53 -0
  83. smart_tests/utils/no_build.py +2 -0
  84. smart_tests/utils/sax.py +119 -0
  85. smart_tests/utils/session.py +73 -0
  86. smart_tests/utils/smart_tests_client.py +134 -0
  87. smart_tests/utils/subprocess.py +12 -0
  88. smart_tests/utils/tracking.py +95 -0
  89. smart_tests/utils/typer_types.py +241 -0
  90. smart_tests/version.py +7 -0
  91. smart_tests_cli-2.0.0.dist-info/METADATA +168 -0
  92. smart_tests_cli-2.0.0.dist-info/RECORD +96 -0
  93. smart_tests_cli-2.0.0.dist-info/WHEEL +5 -0
  94. smart_tests_cli-2.0.0.dist-info/entry_points.txt +2 -0
  95. smart_tests_cli-2.0.0.dist-info/licenses/LICENSE.txt +202 -0
  96. smart_tests_cli-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,189 @@
1
+ from typing import Annotated, List
2
+ from xml.etree import ElementTree as ET
3
+
4
+ import click
5
+
6
+ import smart_tests.args4p.typer as typer
7
+ from smart_tests.commands.record.case_event import CaseEvent
8
+
9
+ from ..commands.record.tests import RecordTests
10
+ from ..commands.subset import Subset
11
+ from . import smart_tests
12
+
13
+ # https://source.android.com/docs/compatibility/cts/command-console-v2
14
+ include_option = "--include-filter"
15
+ exclude_option = "--exclude-filter"
16
+
17
+
18
+ def parse_func(p: str):
19
+ """ # noqa: E501
20
+ # sample report format
21
+ success case:
22
+ <Module name="CtsAbiOverrideHostTestCases" abi="arm64-v8a" runtime="1171" done="true" pass="1" total_tests="1">
23
+ <TestCase name="android.abioverride.cts.AbiOverrideTest">
24
+ <Test result="pass" name="testAbiIs32bit" />
25
+ </TestCase>
26
+ </Module>
27
+
28
+ failure case:
29
+ <Module name="CtsAccountManagerTestCases" abi="arm64-v8a" runtime="13414" done="false" pass="2" total_tests="151">
30
+ <Reason message="Instrumentation run failed due to 'Process crashed.'" error_name="INSTRUMENTATION_CRASH" error_code="520200" />
31
+ <TestCase name="android.accounts.cts.AbstractAuthenticatorTests">
32
+ <Test result="fail" name="testFinishSessionAndStartAddAccountSessionDefaultImpl">
33
+ <Failure message="android.accounts.OperationCanceledException&#13;">
34
+ <StackTrace>android.accounts.OperationCanceledException
35
+ at android.accounts.AccountManager$AmsTask.internalGetResult(AccountManager.java:2393)
36
+ at android.accounts.AccountManager$AmsTask.getResult(AccountManager.java:2422)
37
+ at android.accounts.AccountManager$AmsTask.getResult(AccountManager.java:2337)
38
+ at android.accounts.cts.AbstractAuthenticatorTests.testFinishSessionAndStartAddAccountSessionDefaultImpl(AbstractAuthenticatorTests.java:165)
39
+ </StackTrace>
40
+ </Failure>
41
+ </Test>
42
+ </TestCase>
43
+ </Module>
44
+ """
45
+ class TestResult:
46
+ def __init__(self, test_case_name: str, test_name: str, result: str, stdout: str, stderr: str):
47
+ self.test_case_name = test_case_name
48
+ self.test_name = test_name
49
+ self.result = result
50
+ self.stdout = stdout
51
+ self.stderr = stderr
52
+
53
+ def case_event_status(self):
54
+ if self.result == "fail":
55
+ return CaseEvent.TEST_FAILED
56
+ elif self.result == "ASSUMPTION_FAILURE" or self.result == "IGNORED":
57
+ return CaseEvent.TEST_SKIPPED
58
+
59
+ return CaseEvent.TEST_PASSED
60
+
61
+ def build_record_test_path(module: str, test_case: str, test: str):
62
+ return [
63
+ {"type": "Module", "name": module},
64
+ {"type": "TestCase", "name": test_case},
65
+ {"type": "Test", "name": test},
66
+ ]
67
+
68
+ events = []
69
+ tree = ET.parse(p)
70
+
71
+ for module in tree.iter('Module'):
72
+ test_results = []
73
+ total_duration = module.get("runtime", "0")
74
+ module_name = module.get("name", "")
75
+
76
+ for test_case in module.iter("TestCase"):
77
+ test_case_name = test_case.get('name', "")
78
+
79
+ for test in test_case.iter("Test"):
80
+ result = test.get('result', "")
81
+ test_name = test.get("name", "")
82
+ stdout = ""
83
+ stderr = ""
84
+
85
+ failure = test.find('Failure')
86
+ if failure:
87
+ stack_trace = ""
88
+ stack_trace_element = failure.find("StackTrace")
89
+ if stack_trace_element is not None:
90
+ stack_trace = str(stack_trace_element.text)
91
+
92
+ stdout = failure.get("message", "")
93
+ stderr = stack_trace
94
+
95
+ test_results.append(TestResult(
96
+ test_case_name=test_case_name,
97
+ test_name=test_name,
98
+ result=result,
99
+ stdout=stdout,
100
+ stderr=stderr))
101
+
102
+ if len(test_results) == 0:
103
+ continue
104
+
105
+ test_duration_msec_per_test = int(total_duration) / len(test_results)
106
+
107
+ for test_result in test_results: # type: TestResult
108
+ if module_name == "" or test_result.test_case_name == "" or test_result.test_name == "":
109
+ continue
110
+
111
+ events.append(CaseEvent.create(
112
+ test_path=build_record_test_path(module_name, test_result.test_case_name, test_result.test_name),
113
+ duration_secs=float(test_duration_msec_per_test / 1000),
114
+ status=test_result.case_event_status(),
115
+ stdout=test_result.stdout,
116
+ stderr=test_result.stderr,
117
+ ))
118
+
119
+ return (x for x in events)
120
+
121
+
122
+ @smart_tests.record.tests
123
+ def record_tests(
124
+ client: RecordTests,
125
+ reports: Annotated[List[str], typer.Argument(
126
+ multiple=True,
127
+ help="Test report files to process"
128
+ )],
129
+ ):
130
+ """
131
+ Beta: Report test result that Compatibility Test Suite (CTS) produced. Supports only CTS v2
132
+ """
133
+ for r in reports:
134
+ client.report(r)
135
+
136
+ client.parse_func = parse_func
137
+ client.run()
138
+
139
+
140
+ @smart_tests.subset
141
+ def subset(client: Subset):
142
+ """
143
+ Beta: Produces test list from previous test sessions for Compatibility Test Suite (CTS). Supports only CTS v2
144
+ """
145
+ start_module = False
146
+
147
+ """ # noqa: E501
148
+ # This is sample output of `cts-tradefed list modules`
149
+ ==================
150
+ Notice:
151
+ We collect anonymous usage statistics in accordance with our Content Licenses (https://source.android.com/setup/start/licenses), Contributor License Agreement (https://opensource.google.com/docs/cla/), Privacy Policy (https://policies.google.com/privacy) and Terms of Service (https://policies.google.com/terms).
152
+ ==================
153
+ Android Compatibility Test Suite 12.1_r5 (9566553)
154
+ Use "help" or "help all" to get more information on running commands.
155
+ Non-interactive mode: Running initial command then exiting.
156
+ Using commandline arguments as starting command: [list, modules]
157
+ arm64-v8a CtsAbiOverrideHostTestCases[instant]
158
+ arm64-v8a CtsAbiOverrideHostTestCases[secondary_user]
159
+ arm64-v8a CtsAbiOverrideHostTestCases
160
+ armeabi-v7a CtsAbiOverrideHostTestCases
161
+ """
162
+
163
+ for t in client.stdin():
164
+ line = t.rstrip("\n") if isinstance(t, str) else str(t).rstrip("\n")
165
+
166
+ if "starting command" in line:
167
+ start_module = True
168
+ continue
169
+
170
+ if not start_module:
171
+ continue
172
+
173
+ # e.g) armeabi-v7a CtsAbiOverrideHostTestCases
174
+ device_and_module = line.split()
175
+ if len(device_and_module) != 2:
176
+ click.secho(
177
+ f"Warning: {line} is not expected Module format and skipped",
178
+ fg='yellow',
179
+ err=True)
180
+ continue
181
+
182
+ client.test_path([{"type": "Module", "name": device_and_module[1]}])
183
+
184
+ option = include_option
185
+ if client.is_output_exclusion_rules:
186
+ option = exclude_option
187
+
188
+ client.formatter = lambda x: "{option} \"{module}\"".format(option=option, module=x[0]['name'])
189
+ client.run()
@@ -0,0 +1,451 @@
1
+ import json
2
+ import os
3
+ import pathlib
4
+ import re
5
+ from copy import deepcopy
6
+ from enum import Enum
7
+ from pathlib import Path
8
+ from typing import Annotated, Dict, Generator, List, cast
9
+ from xml.etree import ElementTree as ET
10
+
11
+ import click
12
+
13
+ import smart_tests.args4p.typer as typer
14
+ from smart_tests.testpath import FilePathNormalizer, TestPath
15
+
16
+ from ..commands.record.case_event import CaseEvent, CaseEventType
17
+ from ..commands.record.tests import RecordTests
18
+ from . import smart_tests
19
+
20
+ subset = smart_tests.CommonSubsetImpls(__name__).scan_files('*_feature')
21
+
22
+
23
+ REPORT_FILE_PREFIX = "TEST-"
24
+
25
+
26
+ @smart_tests.record.tests
27
+ def record_tests(
28
+ client: RecordTests,
29
+ reports: Annotated[List[str], typer.Argument(
30
+ multiple=True,
31
+ help="Test report files to process"
32
+ )],
33
+ json_format: Annotated[bool, typer.Option(
34
+ "--json",
35
+ help="use JSON report format"
36
+ )] = False,
37
+ ):
38
+ if json_format:
39
+ for r in reports:
40
+ client.report(r)
41
+ client.parse_func = JSONReportParser(client).parse_func
42
+ else:
43
+ report_file_and_test_file_map: dict[str, str] = {}
44
+ _record_tests_from_xml(client, reports, report_file_and_test_file_map)
45
+
46
+ def parse_func(report: str) -> ET.ElementTree:
47
+ tree = cast(ET.ElementTree, ET.parse(report))
48
+ for case in tree.findall("testcase"):
49
+ case.attrib["file"] = str(report_file_and_test_file_map[report])
50
+
51
+ return tree
52
+
53
+ client.junitxml_parse_func = parse_func
54
+
55
+ client.run()
56
+
57
+
58
+ def _record_tests_from_xml(client, reports, report_file_and_test_file_map: Dict[str, str]):
59
+ base_path = client.base_path if client.base_path else os.getcwd()
60
+ for report in reports:
61
+ if REPORT_FILE_PREFIX not in report:
62
+ click.echo(f"{report} was load skipped because it doesn't look like a report file.", err=True)
63
+ continue
64
+
65
+ test_file = _find_test_file_from_report_file(base_path, report)
66
+ if test_file:
67
+ report_file_and_test_file_map[report] = str(test_file)
68
+ client.report(report)
69
+ else:
70
+ click.echo(f"Cannot find test file of {report}", err=True)
71
+
72
+
73
+ class JSONReportParser:
74
+ """
75
+ client: smart_tests.RecordTests
76
+ """
77
+
78
+ def __init__(self, client):
79
+ self.client = client
80
+ self.file_path_normalizer = FilePathNormalizer(base_path=client.base_path,
81
+ no_base_path_inference=client.no_base_path_inference)
82
+
83
+ def parse_func(self, report_file: str) -> Generator[CaseEventType, None, None]:
84
+ """
85
+ example of JSON format report
86
+ [
87
+ {
88
+ "uri": "features/foo/is_it_friday_yet.feature",
89
+ "id": "is-it-friday-yet?",
90
+ "keyword": "Feature",
91
+ "name": "Is it Friday yet?",
92
+ "description": " Everybody wants to know when it's Friday",
93
+ "line": 1,
94
+ "elements": [
95
+ {
96
+ "keyword": "Background",
97
+ "name": "",
98
+ "description": "",
99
+ "line": 4,
100
+ "type": "background",
101
+ "steps": [
102
+ {
103
+ "keyword": "Given ",
104
+ "name": "this year is 2023",
105
+ "line": 5,
106
+ "match": {
107
+ "location": "features/step_definitions/stepdefs.rb:12"
108
+ },
109
+ "result": {
110
+ "status": "passed",
111
+ "duration": 30000
112
+ },
113
+ "after": [
114
+ {
115
+ "match": {
116
+ "location": "features/support/env.rb:22"
117
+ },
118
+ "result": {
119
+ "status": "passed",
120
+ "duration": 10181000
121
+ }
122
+ }
123
+ ]
124
+ },
125
+ {
126
+ "keyword": "And ",
127
+ "name": "this month is January",
128
+ "line": 6,
129
+ "match": {
130
+ "location": "features/step_definitions/stepdefs.rb:17"
131
+ },
132
+ "result": {
133
+ "status": "passed",
134
+ "duration": 9000
135
+ }
136
+ }
137
+ ]
138
+ },
139
+ {
140
+ "id": "is-it-friday-yet?;today-is-or-is-not-friday;;2",
141
+ "keyword": "Scenario Outline",
142
+ "name": "Today is or is not Friday",
143
+ "description": "",
144
+ "line": 11,
145
+ "type": "scenario",
146
+ "steps": [
147
+ {
148
+ "keyword": "Given ",
149
+ "name": "today is \"Friday\"",
150
+ "line": 5,
151
+ "match": {
152
+ "location": "features/step_definitions/stepdefs.rb:12"
153
+ },
154
+ "result": {
155
+ "status": "passed",
156
+ "duration": 101000
157
+ }
158
+ },
159
+ {
160
+ "keyword": "When ",
161
+ "name": "I ask whether it's Friday yet",
162
+ "line": 6,
163
+ "match": {
164
+ "location": "features/step_definitions/stepdefs.rb:24"
165
+ },
166
+ "result": {
167
+ "status": "passed",
168
+ "duration": 11000
169
+ }
170
+ },
171
+ {
172
+ "keyword": "Then ",
173
+ "name": "I should be told \"TGIF\"",
174
+ "line": 7,
175
+ "match": {
176
+ "location": "features/step_definitions/stepdefs.rb:28"
177
+ },
178
+ "result": {
179
+ "status": "passed",
180
+ "duration": 481000
181
+ }
182
+ }
183
+ ],
184
+ "before": [
185
+ {
186
+ "match": {
187
+ "location": "features/support/env.rb:10"
188
+ },
189
+ "result": {
190
+ "status": "passed",
191
+ "duration": 11092000
192
+ }
193
+ },
194
+ {
195
+ "match": {
196
+ "location": "features/support/env.rb:14"
197
+ },
198
+ "result": {
199
+ "status": "passed",
200
+ "duration": 11081000
201
+ }
202
+ }
203
+ ],
204
+ "after": [
205
+ {
206
+ "match": {
207
+ "location": "features/support/env.rb:18"
208
+ },
209
+ "result": {
210
+ "status": "passed",
211
+ "duration": 11072000
212
+ }
213
+ }
214
+ ]
215
+ }
216
+ ]
217
+ """
218
+ with open(report_file, 'r') as json_file:
219
+ try:
220
+ data = json.load(json_file)
221
+ except Exception as e:
222
+ raise Exception(f"Can't read JSON format report file {report_file}. Make sure to confirm report file.") from e
223
+
224
+ if len(data) == 0:
225
+ click.echo(f"Can't find test reports from {report_file}. Make sure to confirm report file.", err=True)
226
+
227
+ for d in data:
228
+ file_name = clean_uri(d.get("uri", ""))
229
+ class_name = d.get("name", "")
230
+
231
+ # Cucumber can define repeating the same `Given` steps as a `Background`
232
+ # https://cucumber.io/docs/gherkin/reference/#background
233
+ background: TestCaseInfo | None = None
234
+
235
+ for element in d.get("elements", []):
236
+ test_case = element.get("name", "")
237
+ # Scenario hooks run for every scenario.
238
+ # https://cucumber.io/docs/cucumber/api/?lang=java#hooks
239
+ scenario_hook_information = _parse_hook_from_element(element)
240
+
241
+ if element.get("type", "") == CucumberElementType.BACKGROUND.value:
242
+ # `Background` can be defined once per scenario so won't available multiple times.
243
+ background = _parse_test_case_info_from_element(element=element)
244
+ background.append_hook_info(scenario_hook_information)
245
+ continue
246
+
247
+ test_case_info = _parse_test_case_info_from_element(element=element)
248
+ if background:
249
+ test_case_info.append_background_results(background)
250
+ # Initialize background for next scenario
251
+ background = None
252
+
253
+ test_case_info.append_hook_info(scenario_hook_information)
254
+
255
+ if test_case_info.is_failed():
256
+ status = CaseEvent.TEST_FAILED
257
+ elif test_case_info.is_skipped():
258
+ status = CaseEvent.TEST_SKIPPED
259
+ else:
260
+ status = CaseEvent.TEST_PASSED
261
+
262
+ test_path: TestPath = [
263
+ {"type": "file", "name": pathlib.Path(self.file_path_normalizer.relativize(file_name)).as_posix()},
264
+ {"type": "class", "name": class_name},
265
+ {"type": "testcase", "name": test_case},
266
+ ]
267
+ test_path.extend(test_case_info.test_path())
268
+
269
+ yield CaseEvent.create(
270
+ test_path=test_path,
271
+ duration_secs=test_case_info.duration_sec(),
272
+ status=status,
273
+ stderr="\n".join(test_case_info.stderr()))
274
+
275
+
276
+ def _find_test_file_from_report_file(base_path: str, report: str) -> Path | None:
277
+ """
278
+ Find test file from cucumber report file path format
279
+ e.g) Test-features-foo-hoge.xml -> features/foo/hoge.feature or features/foo-hoge.feature
280
+ """
281
+
282
+ report_file = os.path.basename(report)
283
+ report_file = report_file.lstrip(REPORT_FILE_PREFIX)
284
+ report_file = os.path.splitext(report_file)[0]
285
+
286
+ file_candidates = _create_file_candidate_list(report_file)
287
+ for candidate in file_candidates:
288
+ f = Path(base_path, candidate + ".feature")
289
+ if f.exists():
290
+ return f
291
+
292
+ return None
293
+
294
+
295
+ def _create_file_candidate_list(file: str) -> List[str]:
296
+ candidates = [""]
297
+ for c in file:
298
+ if c == "-":
299
+ list_length = len(candidates)
300
+ candidates += deepcopy(candidates)
301
+ for i in range(list_length):
302
+ candidates[i] += '-'
303
+ for i in range(list_length, len(candidates)):
304
+ candidates[i] += '/'
305
+ else:
306
+ for i in range(len(candidates)):
307
+ candidates[i] += c
308
+
309
+ return candidates
310
+
311
+
312
+ class Result:
313
+ def __init__(self, statuses: List[str], duration_nano_sec: int, error_message: List[str]) -> None:
314
+ self._statuses = statuses
315
+ self._duration_nano_sec = duration_nano_sec
316
+ self._error_message = error_message
317
+
318
+
319
+ class TestCaseHookInfo(Result):
320
+ def __init__(self, duration_nano_sec: int, statuses: List[str], stderr: List[str]) -> None:
321
+ super().__init__(statuses=statuses, duration_nano_sec=duration_nano_sec, error_message=stderr)
322
+
323
+
324
+ def _parse_hook_from_element(element: Dict[str, List]) -> TestCaseHookInfo:
325
+ duration_nano_sec: int = 0
326
+ statuses: List[str] = []
327
+ stderr: List[str] = []
328
+
329
+ def parse_steps(step: Dict[str, Dict]):
330
+ result = step.get("result", None)
331
+ if result:
332
+ nonlocal duration_nano_sec
333
+ duration_nano_sec += result.get("duration", 0)
334
+ if result.get("status", None):
335
+ statuses.append(result["status"])
336
+ if result.get("error_message", None):
337
+ stderr.append(result["error_message"])
338
+
339
+ for step in element.get("before", []):
340
+ parse_steps(step)
341
+
342
+ for step in element.get("after", []):
343
+ parse_steps(step)
344
+
345
+ return TestCaseHookInfo(
346
+ duration_nano_sec=duration_nano_sec,
347
+ statuses=statuses,
348
+ stderr=stderr
349
+ )
350
+
351
+
352
+ class TestCaseInfo(Result):
353
+ def __init__(
354
+ self, steps: List[List[str]],
355
+ duration_nano_sec: int, statuses: List[str],
356
+ stderr: List[str],
357
+ hooks: List[TestCaseHookInfo] = []) -> None:
358
+ super().__init__(statuses=statuses, duration_nano_sec=duration_nano_sec, error_message=stderr)
359
+ self._steps = steps
360
+ self._hooks = hooks
361
+
362
+ def steps(self) -> List[List[str]]:
363
+ return self._steps
364
+
365
+ def duration_nano(self) -> int:
366
+ return self._duration_nano_sec + sum(h._duration_nano_sec for h in self._hooks)
367
+
368
+ def duration_sec(self) -> float:
369
+ return self.duration_nano() / 1000 / 1000 / 1000
370
+
371
+ def statuses(self) -> List[str]:
372
+ return self._statuses + sum([h._statuses for h in self._hooks], [])
373
+
374
+ def stderr(self) -> List[str]:
375
+ return self._error_message + sum([h._error_message for h in self._hooks], [])
376
+
377
+ def append_hook_info(self, other: TestCaseHookInfo) -> None:
378
+ self._hooks.append(other)
379
+
380
+ def is_failed(self) -> bool:
381
+ return "failed" in self.statuses()
382
+
383
+ def is_skipped(self) -> bool:
384
+ return "undefined" in self.statuses()
385
+
386
+ def test_path(self) -> TestPath:
387
+ test_path: TestPath = []
388
+ for step in self._steps:
389
+ if len(step) == 2:
390
+ # While there isn't any cases that the size of step is not 2, we check the size just in case.
391
+ test_path.append({"type": step[0], "name": step[1]})
392
+ return test_path
393
+
394
+ # This method only for append_background_results method
395
+ def _to_hook(self) -> TestCaseHookInfo:
396
+ return TestCaseHookInfo(duration_nano_sec=self.duration_nano(), statuses=self.statuses(), stderr=self.stderr())
397
+
398
+ # Type of other TestCaseInfo (Self).. Python3.6 cannot support `Self`` type even if used typing_extensions module
399
+ def append_background_results(self, other) -> None:
400
+ # Need to merge Background steps to main test scenario to calculate correct test duration,
401
+ # then, we don't need step information of Background. So append it as hooks
402
+ self.append_hook_info(other._to_hook())
403
+
404
+
405
+ def _parse_test_case_info_from_element(element: Dict[str, List]) -> TestCaseInfo:
406
+ steps: List[List[str]] = []
407
+ duration = 0 # nano sec
408
+ statuses = []
409
+ stderr = []
410
+ hooks: List[TestCaseHookInfo] = []
411
+
412
+ for step in element.get("steps", []):
413
+ steps.append([step.get("keyword", "").strip(), step.get("name", "").strip()])
414
+ result = step.get("result", None)
415
+
416
+ if result:
417
+ # duration's unit is nano sec
418
+ # ref:
419
+ # https://github.com/cucumber/cucumber-ruby/blob/main/lib/cucumber/formatter/json.rb#L222
420
+ duration = duration + result.get("duration", 0)
421
+ statuses.append(result.get("status"))
422
+ if result.get("error_message", None):
423
+ stderr.append(result["error_message"])
424
+
425
+ # Step hooks are invoked before and after a step.
426
+ # https://cucumber.io/docs/cucumber/api/?lang=java#hooks
427
+ # When Step hooks are executed, the information about each step is registered in each element.
428
+ hooks.append(_parse_hook_from_element(step))
429
+
430
+ return TestCaseInfo(
431
+ steps=steps,
432
+ duration_nano_sec=duration,
433
+ statuses=statuses,
434
+ stderr=stderr,
435
+ hooks=hooks
436
+ )
437
+
438
+ # This type refer to https://github.com/cucumber/json-formatter/blob/v19.0.0/go/json_elements.go#L23.
439
+
440
+
441
+ class CucumberElementType(Enum):
442
+ BACKGROUND = 'background'
443
+ SCENARIO = 'scenario'
444
+
445
+
446
+ def clean_uri(uri: str) -> str:
447
+ """
448
+ Trim unused prefix from the URI string.
449
+ For example, if the URI is "file:features/foo.feature", it will return "features/foo.feature".
450
+ """
451
+ return re.sub(r'^[a-zA-Z]+:', '', uri)
@@ -0,0 +1,46 @@
1
+ from typing import Annotated, List, cast
2
+ from xml.etree import ElementTree as ET
3
+
4
+ import smart_tests.args4p.typer as typer
5
+
6
+ from ..commands.record.tests import RecordTests
7
+ from ..commands.subset import Subset
8
+ from . import smart_tests
9
+
10
+
11
+ @smart_tests.record.tests
12
+ def record_tests(
13
+ client: RecordTests,
14
+ reports: Annotated[List[str], typer.Argument(
15
+ multiple=True,
16
+ help="Test report files to process"
17
+ )],
18
+ ):
19
+ for r in reports:
20
+ client.report(r)
21
+
22
+ def parse_func(p: str) -> ET.ElementTree:
23
+ tree = cast(ET.ElementTree, ET.parse(p))
24
+ for suites in tree.iter("testsuites"):
25
+ if len(suites) == 0:
26
+ continue
27
+ root_suite = suites.find('./testsuite[@name="Root Suite"]')
28
+ if root_suite is not None:
29
+ filepath = root_suite.get("file")
30
+ if filepath is not None:
31
+ for suite in suites:
32
+ suite.attrib.update({"filepath": filepath})
33
+ return tree
34
+
35
+ client.junitxml_parse_func = parse_func
36
+ client.run()
37
+
38
+
39
+ @smart_tests.subset
40
+ def subset(client: Subset):
41
+ # read lines as test file names
42
+ for t in client.stdin():
43
+ client.test_path(t.rstrip("\n"))
44
+
45
+ client.separator = ','
46
+ client.run()