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.
- opentf/commons/datasources.py +337 -0
- {opentf_toolkit_nightly-0.55.0.dev916.dist-info → opentf_toolkit_nightly-0.55.0.dev921.dist-info}/METADATA +1 -1
- {opentf_toolkit_nightly-0.55.0.dev916.dist-info → opentf_toolkit_nightly-0.55.0.dev921.dist-info}/RECORD +6 -5
- {opentf_toolkit_nightly-0.55.0.dev916.dist-info → opentf_toolkit_nightly-0.55.0.dev921.dist-info}/LICENSE +0 -0
- {opentf_toolkit_nightly-0.55.0.dev916.dist-info → opentf_toolkit_nightly-0.55.0.dev921.dist-info}/WHEEL +0 -0
- {opentf_toolkit_nightly-0.55.0.dev916.dist-info → opentf_toolkit_nightly-0.55.0.dev921.dist-info}/top_level.txt +0 -0
|
@@ -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,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.
|
|
51
|
-
opentf_toolkit_nightly-0.55.0.
|
|
52
|
-
opentf_toolkit_nightly-0.55.0.
|
|
53
|
-
opentf_toolkit_nightly-0.55.0.
|
|
54
|
-
opentf_toolkit_nightly-0.55.0.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|