opentf-toolkit-nightly 0.55.0.dev921__py3-none-any.whl → 0.55.0.dev938__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.
@@ -24,11 +24,48 @@ from opentf.toolkit.core import warning
24
24
  ########################################################################
25
25
  # Constants
26
26
 
27
+ SUCCESS = 'SUCCESS'
28
+ FAILURE = 'FAILURE'
29
+ ERROR = 'ERROR'
30
+ SKIPPED = 'SKIPPED'
31
+ TOTAL = 'total count'
32
+
27
33
  DETAILS_KEYS = ('failureDetails', 'errorDetails', 'warningDetails')
28
- FAILURE_STATUSES = ('FAILURE', 'ERROR')
34
+ STATUSES_ORDER = (SUCCESS, FAILURE, ERROR, SKIPPED)
35
+ FAILURE_STATUSES = (FAILURE, ERROR)
36
+
37
+ ########################################################################
38
+ ## Helpers
39
+
40
+
41
+ def _get_path(src: Dict[str, Any], path: List[str]) -> Any:
42
+ if not path:
43
+ return src
44
+ try:
45
+ return _get_path(src[path[0]], path[1:])
46
+ except KeyError:
47
+ return 'KeyError'
48
+
49
+
50
+ def _get_sorted_testcases(
51
+ testcase_metadata: Dict[str, Any], path: List[str]
52
+ ) -> Dict[str, Any]:
53
+ sorted_testcases = {}
54
+ for testcase, data in testcase_metadata.items():
55
+ sorted_testcases.setdefault(_get_path(data, path), {})[testcase] = data
56
+ return sorted_testcases
57
+
58
+
59
+ def _get_sum_for_status(testcases: Dict[str, Any], status: str) -> int:
60
+ return sum(1 for testcase in testcases.values() if testcase['status'] == status)
61
+
62
+
63
+ def _as_list(what) -> List[str]:
64
+ return [what] if isinstance(what, str) else what
65
+
29
66
 
30
67
  ########################################################################
31
- ## Test results handling
68
+ ## Datasource: Testcases
32
69
 
33
70
 
34
71
  def in_scope(expr: str, contexts: Dict[str, Any], scopes_errors: Set[str]) -> bool:
@@ -44,26 +81,58 @@ def in_scope(expr: str, contexts: Dict[str, Any], scopes_errors: Set[str]) -> bo
44
81
  return False
45
82
 
46
83
 
47
- def get_testresults(
48
- events: List[Dict[str, Any]]
49
- ) -> Generator[Dict[str, Any], None, None]:
84
+ def get_testresults(events: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
50
85
  """Return a possibly empty list of Notifications.
51
86
 
52
87
  Each notification in the list is guaranteed to have a
53
88
  `spec.testResults` entry.
54
89
  """
55
- return (item for item in events if _has_testresult(item))
90
+ return [item for item in events if _has_testresult(item)]
56
91
 
57
92
 
58
93
  def _has_testresult(item: Dict[str, Any]) -> bool:
59
- """..."""
94
+ """Determine if a workflow notification has a testResults element."""
60
95
  return item.get('kind') == 'Notification' and item.get('spec', {}).get(
61
96
  'testResults', False
62
97
  )
63
98
 
64
99
 
65
- def _as_list(what) -> List[str]:
66
- return [what] if isinstance(what, str) else what
100
+ def _uses_inception(events: List[Dict[str, Any]]) -> bool:
101
+ """Determine if a workflow is the inception workflow."""
102
+ workflow_event = next(
103
+ (event for event in events if event['kind'] == 'Workflow'), None
104
+ )
105
+ if not workflow_event:
106
+ raise ValueError('No Workflow event in workflow events...')
107
+ return any(
108
+ 'inception' in _as_list(job['runs-on'])
109
+ for job in workflow_event['jobs'].values()
110
+ )
111
+
112
+
113
+ def _get_inception_testresults(events: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
114
+ """Get unique testResults notifications for inception workflow.
115
+
116
+ Note: This is a kludge until we find a reliable way to map such results
117
+ to the executed tests list.
118
+ """
119
+ unique_results = set()
120
+ unique_events = []
121
+ for event in get_testresults(events):
122
+ event_results = []
123
+ for result in event['spec']['testResults']:
124
+ event_results.append(
125
+ (
126
+ result['attachment_origin'],
127
+ result['name'],
128
+ result['duration'],
129
+ result['status'],
130
+ )
131
+ )
132
+ if tuple(event_results) not in unique_results:
133
+ unique_results.add(tuple(event_results))
134
+ unique_events.append(event)
135
+ return unique_events
67
136
 
68
137
 
69
138
  def _get_testresult_params(param_step_id: str, job: Dict[str, Any]) -> Dict[str, Any]:
@@ -156,7 +225,9 @@ def _create_testresult_labels(
156
225
  return labels
157
226
 
158
227
 
159
- def _get_testresult_steporigin(attachment_origin, events) -> Optional[str]:
228
+ def _get_testresult_steporigin(
229
+ attachment_origin: str, events: List[Dict[str, Any]]
230
+ ) -> Optional[str]:
160
231
  """Find the step that produced the attachment.
161
232
 
162
233
  # Required parameters
@@ -229,6 +300,7 @@ def _get_timestamp(
229
300
 
230
301
  def _get_testresult_timestamps(
231
302
  events: List[Dict[str, Any]],
303
+ testresults: List[Dict[str, Any]],
232
304
  testcase_metadata: Dict[str, Any],
233
305
  ):
234
306
  """Set timestamp for each testcase in testcase_metadata.
@@ -242,7 +314,7 @@ def _get_testresult_timestamps(
242
314
  }
243
315
 
244
316
  origins_results = defaultdict(list)
245
- for item in get_testresults(events):
317
+ for item in testresults:
246
318
  for result in item['spec']['testResults']:
247
319
  origins_results[result['attachment_origin']].append(result['id'])
248
320
 
@@ -271,7 +343,13 @@ def get_testcases(events: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]:
271
343
 
272
344
  - name: a string, the test case name
273
345
  - status: a string, the test case status
346
+ - duration: a string, the test case execution time in ms
347
+ - timestamp: a string, provider creation timestamp
274
348
  - test: a dictionary, the test case metadata
349
+ - failureDetails|errorDetails|warningDetails: a dictionary with test
350
+ case failure details
351
+ - errorsList: a Robot Framework specific list with execution general
352
+ errors
275
353
 
276
354
  `testcases` is a dictionary of entries like:
277
355
 
@@ -310,7 +388,12 @@ def get_testcases(events: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]:
310
388
  """
311
389
  testcases = {}
312
390
  results = False
313
- for testresult in get_testresults(events):
391
+ if _uses_inception(events):
392
+ testresults = _get_inception_testresults(events)
393
+ else:
394
+ testresults = get_testresults(events)
395
+
396
+ for testresult in testresults:
314
397
  results = True
315
398
  labels = _get_testresult_labels(
316
399
  testresult['metadata']['attachment_origin'][0], events
@@ -320,7 +403,7 @@ def get_testcases(events: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]:
320
403
  for testcase in testresult['spec']['testResults']:
321
404
  testcases[testcase['id']] = {
322
405
  'name': testcase['name'],
323
- 'status': testcase['status'],
406
+ 'status': testcase['status'].lower(),
324
407
  'duration': testcase.get('duration', 0),
325
408
  'test': labels.copy(),
326
409
  }
@@ -333,5 +416,149 @@ def get_testcases(events: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]:
333
416
  testcases[testcase['id']].update(data)
334
417
  if not results:
335
418
  raise ValueError('No test results in events.')
336
- _get_testresult_timestamps(events, testcases)
419
+ _get_testresult_timestamps(events, testresults, testcases)
337
420
  return testcases
421
+
422
+
423
+ ########################################################################
424
+ ## Datasource: Tags
425
+
426
+
427
+ def get_tags(
428
+ events: List[Dict[str, Any]], testcase_metadata: Optional[Dict[str, Any]] = None
429
+ ) -> Dict[str, Any]:
430
+ """Extract metadata for each execution environment tag.
431
+
432
+ # Required parameters:
433
+
434
+ - events: a list of events
435
+
436
+ # Returned value:
437
+
438
+ A dictionary. Keys are tags names, values are dictionaries with testcase
439
+ by tag status counters.
440
+
441
+ `tags` is a dictionary of entries like:
442
+
443
+ ```
444
+ "<<<tag name>>>": {
445
+ "FAILURE": <<<failed tests count>>>,
446
+ "SUCCESS": <<<passed tests count>>>,
447
+ "SKIPPED": <<<skipped tests count>>>,
448
+ "ERROR": <<<technical KO tests count>>>,
449
+ 'total': <<<total tests count>>>,
450
+ 'other': <<<SKIPPED + ERROR tests count>>>
451
+ }
452
+ ```
453
+ """
454
+ try:
455
+ testcase_metadata = testcase_metadata or get_testcases(events)
456
+ except ValueError as err:
457
+ raise ValueError(str(err) + ' Cannot extract metadata for tags.')
458
+ tags = {}
459
+ for testcase in testcase_metadata.values():
460
+ for tag in testcase['test']['runs-on']:
461
+ tags.setdefault(tag, {SUCCESS: 0, FAILURE: 0, ERROR: 0, SKIPPED: 0})
462
+ tags[tag][testcase['status']] += 1
463
+ for tag, counts in tags.items():
464
+ counts['total'] = sum(counts[status] for status in STATUSES_ORDER)
465
+ counts['other'] = sum(counts[status] for status in (SKIPPED, ERROR))
466
+ tags[tag] = {k.lower(): v for k, v in counts.items()}
467
+ return tags
468
+
469
+
470
+ ########################################################################
471
+ ## Datasource: Jobs
472
+
473
+
474
+ def _evaluate_test_results(jobs_testcases: Dict[str, Any]) -> Dict[str, Any]:
475
+ """Summarize job testcases.
476
+
477
+ # Returned value
478
+
479
+ A dictionary with one entry per job (a dictionary with keys being
480
+ statuses and values being counts).
481
+ """
482
+ summaries = {}
483
+ for job, testcases in jobs_testcases.items():
484
+ successes, failures, errors, skipped = [
485
+ _get_sum_for_status(testcases, status) for status in STATUSES_ORDER
486
+ ]
487
+ summaries[job] = {
488
+ SUCCESS: successes,
489
+ FAILURE: failures,
490
+ ERROR: errors,
491
+ SKIPPED: skipped,
492
+ TOTAL: len(testcases),
493
+ }
494
+ return summaries
495
+
496
+
497
+ def get_jobs(
498
+ events: List[Dict[str, Any]], testcase_metadata: Optional[Dict[str, Any]] = None
499
+ ) -> Dict[str, Any]:
500
+ """Extract metadata for each job.
501
+
502
+ # Required parameters:
503
+
504
+ - events: a list of events
505
+
506
+ # Returned value:
507
+
508
+ A dictionary. Keys are job names, values are dictionaries with the
509
+ following entries:
510
+
511
+ - name: a string, job name
512
+ - runs-on: a list, execution environment tags
513
+ - testcases: a dictionary, job-related test cases
514
+ - counts: a dictionary, tests statuses count by job
515
+ - variables: a dictionary, job-related environment variables
516
+
517
+ `jobs_testcases` is a dictionary of entries like:
518
+
519
+ ```
520
+ "<<<job_name>>>": {
521
+ "runs-on": [<<<execution environment tags>>>],
522
+ "counts": {
523
+ "FAILURE": <<<failed tests count>>>,
524
+ "SUCCESS": <<<passed tests count>>>,
525
+ "ERROR": <<<technical KOs count>>>,
526
+ "SKIPPED": <<<skipped tests count>>>,
527
+ "total count": <<<total tests count>>>
528
+ },
529
+ "variables": {
530
+ "<<<variable name>>>": "<<<variable value>>>",
531
+ ...
532
+ }
533
+ }
534
+ ```
535
+ """
536
+ try:
537
+ testcase_metadata = testcase_metadata or get_testcases(events)
538
+ except ValueError as err:
539
+ raise ValueError(str(err) + ' Cannot extract metadata for jobs.')
540
+ jobs_testcases = _get_sorted_testcases(testcase_metadata, ['test', 'job'])
541
+ if 'KeyError' in jobs_testcases:
542
+ raise ValueError('Cannot get jobs-ordered dataset from testcases.')
543
+ job_events = [
544
+ event
545
+ for event in events
546
+ if event['kind'] == 'ExecutionCommand'
547
+ and event['metadata']['step_sequence_id'] == -1
548
+ and event['metadata']['name'] in jobs_testcases
549
+ ]
550
+ results = _evaluate_test_results(jobs_testcases)
551
+ jobs = {}
552
+ for job in job_events:
553
+ job_name = job['metadata']['name']
554
+ jobs.setdefault(
555
+ job['metadata']['name'],
556
+ {
557
+ 'runs-on': job['runs-on'],
558
+ 'counts': {k.lower(): v for k, v in results[job_name].items()},
559
+ 'variables': {
560
+ name: value for name, value in job.get('variables', {}).items()
561
+ },
562
+ },
563
+ )
564
+ return jobs
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: opentf-toolkit-nightly
3
- Version: 0.55.0.dev921
3
+ Version: 0.55.0.dev938
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,7 +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
+ opentf/commons/datasources.py,sha256=OkPp0allFp-G9qBkU4Fxc6aGiytkMO3Xr3KLl4E5zWM,18234
5
5
  opentf/commons/expressions.py,sha256=A68F27Our8oVVphUrRvB5haSlqj2YCrH2OxHPNLBio4,19251
6
6
  opentf/commons/pubsub.py,sha256=7khxAHVZiwJRcwIBJ6MPR-f3xY9144-2eNLROwq5F-4,5894
7
7
  opentf/commons/schemas.py,sha256=lokZCU-wmsIkzVA-TVENtC7Io_GmYxrP-FQaOOowg4s,4044
@@ -48,8 +48,8 @@ opentf/scripts/startup.py,sha256=Da2zo93pBWbdRmj-wgekgLcF94rpNc3ZkbvR8R0w8XY,212
48
48
  opentf/toolkit/__init__.py,sha256=g3DiTZlSvvzZWKgM8qU47muLqjQrpWZ6M6PWZ-sBsvQ,19610
49
49
  opentf/toolkit/channels.py,sha256=Cng3b4LUsxvCHUbp_skys9CFcKZMfcKhA_ODg_EAlIE,17156
50
50
  opentf/toolkit/core.py,sha256=L1fT4YzwZjqE7PUXhJL6jSVQge3ohBQv5UBb9DAC6oo,9320
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,,
51
+ opentf_toolkit_nightly-0.55.0.dev938.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
52
+ opentf_toolkit_nightly-0.55.0.dev938.dist-info/METADATA,sha256=1FipCBdX1nal-7wQEMhR1QrdYvJUuPMAGcEPjLAzDco,1945
53
+ opentf_toolkit_nightly-0.55.0.dev938.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
54
+ opentf_toolkit_nightly-0.55.0.dev938.dist-info/top_level.txt,sha256=_gPuE6GTT6UNXy1DjtmQSfCcZb_qYA2vWmjg7a30AGk,7
55
+ opentf_toolkit_nightly-0.55.0.dev938.dist-info/RECORD,,