opentf-toolkit-nightly 0.55.0.dev916__py3-none-any.whl → 0.55.0.dev921__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.
@@ -0,0 +1,337 @@
1
+ # Copyright (c) 2024 Henix, Henix.fr
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Test case metadata retrieval helpers"""
16
+
17
+ from typing import Any, Dict, Generator, List, Optional, Set
18
+ from collections import defaultdict
19
+
20
+ from opentf.commons.expressions import evaluate_bool
21
+ from opentf.toolkit.core import warning
22
+
23
+
24
+ ########################################################################
25
+ # Constants
26
+
27
+ DETAILS_KEYS = ('failureDetails', 'errorDetails', 'warningDetails')
28
+ FAILURE_STATUSES = ('FAILURE', 'ERROR')
29
+
30
+ ########################################################################
31
+ ## Test results handling
32
+
33
+
34
+ def in_scope(expr: str, contexts: Dict[str, Any], scopes_errors: Set[str]) -> bool:
35
+ """Safely evaluate quality gate scope."""
36
+ try:
37
+ return evaluate_bool(expr, contexts)
38
+ except ValueError as err:
39
+ msg = f'Invalid conditional {expr}: {err}.'
40
+ scopes_errors.add(msg)
41
+ except KeyError as err:
42
+ msg = f'Nonexisting context entry in expression {expr}: {err}.'
43
+ scopes_errors.add(msg)
44
+ return False
45
+
46
+
47
+ def get_testresults(
48
+ events: List[Dict[str, Any]]
49
+ ) -> Generator[Dict[str, Any], None, None]:
50
+ """Return a possibly empty list of Notifications.
51
+
52
+ Each notification in the list is guaranteed to have a
53
+ `spec.testResults` entry.
54
+ """
55
+ return (item for item in events if _has_testresult(item))
56
+
57
+
58
+ def _has_testresult(item: Dict[str, Any]) -> bool:
59
+ """..."""
60
+ return item.get('kind') == 'Notification' and item.get('spec', {}).get(
61
+ 'testResults', False
62
+ )
63
+
64
+
65
+ def _as_list(what) -> List[str]:
66
+ return [what] if isinstance(what, str) else what
67
+
68
+
69
+ def _get_testresult_params(param_step_id: str, job: Dict[str, Any]) -> Dict[str, Any]:
70
+ """Get .with.data field of param_step_id.
71
+
72
+ # Required parameters
73
+
74
+ - param_step_id: a string
75
+ - job: a dictionary
76
+
77
+ # Returned value
78
+
79
+ A dictionary, the `.with.data` part of the params step.
80
+
81
+ # Raised exceptions
82
+
83
+ An _IndexError_ exception is raised if no params step is found.
84
+ """
85
+ return [
86
+ step['with']['data'] for step in job['steps'] if step.get('id') == param_step_id
87
+ ].pop()
88
+
89
+
90
+ def _create_testresult_labels(
91
+ exec_step: Dict[str, Any],
92
+ job_name: str,
93
+ job: Dict[str, Any],
94
+ parent: Dict[str, Any],
95
+ ) -> Dict[str, Any]:
96
+ """Create labels for test result.
97
+
98
+ # Required parameters
99
+
100
+ - exec_step: a dictionary, the 'execute' step
101
+ - job_name: a string (the name of the job containing exec_step)
102
+ - job: a dictionary, the job containing exec_step
103
+ - parent: a dictionary, the event defining the job
104
+
105
+ # Returned value
106
+
107
+ A labels dictionary.
108
+ """
109
+ exec_step_id = exec_step['id']
110
+ labels = {
111
+ 'job': job_name.split()[0],
112
+ 'uses': exec_step['uses'],
113
+ 'technology': exec_step['uses'].partition('/')[0],
114
+ 'runs-on': _as_list(job['runs-on']),
115
+ 'managed': False,
116
+ }
117
+
118
+ if not (managedtests := parent['metadata'].get('managedTests')):
119
+ return labels
120
+ testcases = managedtests.get('testCases')
121
+ if not testcases or exec_step_id not in testcases:
122
+ if not testcases:
123
+ warning(
124
+ f'Was expecting a "testCases" part in parent of step {exec_step_id}, ignoring.'
125
+ )
126
+ return labels
127
+
128
+ labels['managed'] = True
129
+ testcase_metadata = testcases[exec_step_id]
130
+ labels['technology-name'] = testcase_metadata['technology']
131
+ labels['collection'] = managedtests.get('testPlan', {})
132
+ labels.update(
133
+ {
134
+ key: value
135
+ for key, value in testcase_metadata.items()
136
+ if key
137
+ in (
138
+ 'name',
139
+ 'reference',
140
+ 'importance',
141
+ 'nature',
142
+ 'path',
143
+ 'type',
144
+ 'uuid',
145
+ )
146
+ }
147
+ )
148
+ try:
149
+ params = _get_testresult_params(testcase_metadata['param_step_id'], job)
150
+ labels['global'] = params.get('global', {})
151
+ labels['data'] = params.get('test', {})
152
+ except IndexError:
153
+ warning(
154
+ f'Could not find "params" step associated to "execute" step {exec_step_id}, ignoring.'
155
+ )
156
+ return labels
157
+
158
+
159
+ def _get_testresult_steporigin(attachment_origin, events) -> Optional[str]:
160
+ """Find the step that produced the attachment.
161
+
162
+ # Required parameters
163
+
164
+ - attachment_origin: a string (the attachment uuid)
165
+ - events: a list of events
166
+
167
+ # Returned value
168
+
169
+ A step ID (a string) or None.
170
+ """
171
+ for event in events:
172
+ if not (event['kind'] == 'ExecutionResult' and event.get('attachments')):
173
+ continue
174
+ metadata = event['metadata']
175
+ for value in metadata.get('attachments', {}).values():
176
+ if value['uuid'] != attachment_origin:
177
+ continue
178
+ return (
179
+ metadata['step_origin'][0]
180
+ if metadata['step_origin']
181
+ else metadata['step_id']
182
+ )
183
+ return None
184
+
185
+
186
+ def _get_testresult_labels(
187
+ attachment_origin: str, events: List[Dict[str, Any]]
188
+ ) -> Optional[Dict[str, Any]]:
189
+ """Get labels for test result.
190
+
191
+ # Required parameters
192
+
193
+ - attachment_origin: a string (the attachment uuid)
194
+ - events: a list of events
195
+
196
+ # Returned value
197
+
198
+ A _labels_ dictionary or None.
199
+ """
200
+ if step_origin := _get_testresult_steporigin(attachment_origin, events):
201
+ jobs_with_steps = {
202
+ job_name + ' ' + event['metadata'].get('job_id', ''): (job, event)
203
+ for event in events
204
+ for job_name, job in event.get('jobs', {}).items()
205
+ if event['kind'] in ('Workflow', 'GeneratorResult') and job.get('steps')
206
+ }
207
+ for job_name, (job, parent) in jobs_with_steps.items():
208
+ for exec_step in job['steps']:
209
+ if exec_step.get('id') == step_origin:
210
+ return _create_testresult_labels(exec_step, job_name, job, parent)
211
+ return None
212
+
213
+
214
+ def _get_timestamp(
215
+ event: Dict[str, Any], providerid_creationtimestamps: Dict[str, str]
216
+ ) -> str:
217
+ """Return first provider creationtimestamp or ''.
218
+
219
+ # Required parameters
220
+
221
+ - event: an ExecutionResult object
222
+ - providerid_creationtimestamps: a dictionary
223
+ """
224
+ for origin_id in event['metadata'].get('step_origin', []):
225
+ if origin_id in providerid_creationtimestamps:
226
+ return providerid_creationtimestamps[origin_id]
227
+ return ''
228
+
229
+
230
+ def _get_testresult_timestamps(
231
+ events: List[Dict[str, Any]],
232
+ testcase_metadata: Dict[str, Any],
233
+ ):
234
+ """Set timestamp for each testcase in testcase_metadata.
235
+
236
+ The timestamp is the one of the originating ProviderResult.
237
+ """
238
+ providerid_creationtimestamps = {
239
+ event['metadata']['step_id']: event['metadata'].get('creationTimestamp', '')
240
+ for event in events
241
+ if event['kind'] == 'ProviderResult'
242
+ }
243
+
244
+ origins_results = defaultdict(list)
245
+ for item in get_testresults(events):
246
+ for result in item['spec']['testResults']:
247
+ origins_results[result['attachment_origin']].append(result['id'])
248
+
249
+ for event in filter(lambda event: event['kind'] == 'ExecutionResult', events):
250
+ for attachment in event['metadata'].get('attachments', {}).values():
251
+ if attachment['uuid'] in origins_results:
252
+ timestamp = _get_timestamp(event, providerid_creationtimestamps)
253
+ for result_id in origins_results[attachment['uuid']]:
254
+ testcase_metadata[result_id]['timestamp'] = timestamp
255
+
256
+
257
+ def get_testcases(events: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]:
258
+ """Extract metadata for each test result.
259
+
260
+ Test results are Notification events with a `.spec.testResults`
261
+ entry.
262
+
263
+ # Required parameters
264
+
265
+ - events: a list of events
266
+
267
+ # Returned value
268
+
269
+ A possibly empty dictionary. Keys are the testresult IDs, values
270
+ are dictionaries with the following entries:
271
+
272
+ - name: a string, the test case name
273
+ - status: a string, the test case status
274
+ - test: a dictionary, the test case metadata
275
+
276
+ `testcases` is a dictionary of entries like:
277
+
278
+ ```
279
+ "<<<testcase_uuid>>>": {
280
+ "name": "<<<[Test suite#]Test case name>>>",
281
+ "status": "<<<SUCCESS|FAILURE|ERROR|SKIPPED>>>",
282
+ "duration": "<<<test execution time in ms>>>",
283
+ "timestamp": "<<<provider creation timestamp>>>"
284
+ "test": {
285
+ "job": "<<<job name>>>",
286
+ "uses": "<<<provider function>>>",
287
+ "technology": "<<<test technology>>>",
288
+ "runs-on": [<<<list of execution environment tags>>>],
289
+ "managed": boolean, True for test cases managed by a test referential
290
+ "status": "<<<SUCCESS|FAILURE|ERROR|SKIPPED>>>"
291
+ },
292
+ "failureDetails"|"errorDetails"|"warningDetails": {
293
+ "message": "<<<error message>>>",
294
+ "type": "<<<error type>>>",
295
+ "text": "<<<error trace>>>"
296
+ },
297
+ "errorsList": [
298
+ {
299
+ "message": "<<<Robot Framework general error message>>>",
300
+ "timestamp": "<<<Robot Framework error message timestamp>>>"
301
+ }
302
+ ]
303
+ }
304
+ ```
305
+
306
+ # Raised exceptions
307
+
308
+ A _ValueError_ exception is raised if there were no test results in
309
+ `events`.
310
+ """
311
+ testcases = {}
312
+ results = False
313
+ for testresult in get_testresults(events):
314
+ results = True
315
+ labels = _get_testresult_labels(
316
+ testresult['metadata']['attachment_origin'][0], events
317
+ )
318
+ if not labels:
319
+ continue
320
+ for testcase in testresult['spec']['testResults']:
321
+ testcases[testcase['id']] = {
322
+ 'name': testcase['name'],
323
+ 'status': testcase['status'],
324
+ 'duration': testcase.get('duration', 0),
325
+ 'test': labels.copy(),
326
+ }
327
+ testcases[testcase['id']]['test']['status'] = testcase['status']
328
+ data = {}
329
+ if testcase['status'] in FAILURE_STATUSES:
330
+ data = {key: testcase[key] for key in DETAILS_KEYS if testcase.get(key)}
331
+ if testcase.get('errorsList'):
332
+ data['errorsList'] = testcase['errorsList']
333
+ testcases[testcase['id']].update(data)
334
+ if not results:
335
+ raise ValueError('No test results in events.')
336
+ _get_testresult_timestamps(events, testcases)
337
+ return testcases
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: opentf-toolkit-nightly
3
- Version: 0.55.0.dev916
3
+ Version: 0.55.0.dev921
4
4
  Summary: OpenTestFactory Orchestrator Toolkit
5
5
  Home-page: https://gitlab.com/henixdevelopment/open-source/opentestfactory/python-toolkit
6
6
  Author: Martin Lafaix
@@ -1,6 +1,7 @@
1
1
  opentf/commons/__init__.py,sha256=ITzg1zfZgA5-4wvmJfjLN94_Z06HeMl0szd6dalrrKY,21839
2
2
  opentf/commons/auth.py,sha256=bM2Z3kxm2Wku1lKXaRAIg37LHvXWAXIZIqjplDfN2P8,15899
3
3
  opentf/commons/config.py,sha256=GmvInVnUsXIwlNfgTQeQ_pPs97GeGTGn2S2QZEFwss8,7828
4
+ opentf/commons/datasources.py,sha256=Ux7DIo1Bd4qKTB8JOPYSBHtUjtS93Lgxz_Y0K3Dd1uY,10925
4
5
  opentf/commons/expressions.py,sha256=A68F27Our8oVVphUrRvB5haSlqj2YCrH2OxHPNLBio4,19251
5
6
  opentf/commons/pubsub.py,sha256=7khxAHVZiwJRcwIBJ6MPR-f3xY9144-2eNLROwq5F-4,5894
6
7
  opentf/commons/schemas.py,sha256=lokZCU-wmsIkzVA-TVENtC7Io_GmYxrP-FQaOOowg4s,4044
@@ -47,8 +48,8 @@ opentf/scripts/startup.py,sha256=Da2zo93pBWbdRmj-wgekgLcF94rpNc3ZkbvR8R0w8XY,212
47
48
  opentf/toolkit/__init__.py,sha256=g3DiTZlSvvzZWKgM8qU47muLqjQrpWZ6M6PWZ-sBsvQ,19610
48
49
  opentf/toolkit/channels.py,sha256=Cng3b4LUsxvCHUbp_skys9CFcKZMfcKhA_ODg_EAlIE,17156
49
50
  opentf/toolkit/core.py,sha256=L1fT4YzwZjqE7PUXhJL6jSVQge3ohBQv5UBb9DAC6oo,9320
50
- opentf_toolkit_nightly-0.55.0.dev916.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
51
- opentf_toolkit_nightly-0.55.0.dev916.dist-info/METADATA,sha256=0lRoViWSBXvHBkaeXthUoblXTTBH9dE1m2UiNtP_dh8,1945
52
- opentf_toolkit_nightly-0.55.0.dev916.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
53
- opentf_toolkit_nightly-0.55.0.dev916.dist-info/top_level.txt,sha256=_gPuE6GTT6UNXy1DjtmQSfCcZb_qYA2vWmjg7a30AGk,7
54
- opentf_toolkit_nightly-0.55.0.dev916.dist-info/RECORD,,
51
+ opentf_toolkit_nightly-0.55.0.dev921.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
52
+ opentf_toolkit_nightly-0.55.0.dev921.dist-info/METADATA,sha256=BhP_cydTKTO1fEMwhaJY1gUGwO155pD_7QOgp-krb7o,1945
53
+ opentf_toolkit_nightly-0.55.0.dev921.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
54
+ opentf_toolkit_nightly-0.55.0.dev921.dist-info/top_level.txt,sha256=_gPuE6GTT6UNXy1DjtmQSfCcZb_qYA2vWmjg7a30AGk,7
55
+ opentf_toolkit_nightly-0.55.0.dev921.dist-info/RECORD,,