Skip to content

Commit 58c8b2b

Browse files
committed
Scrap approval of test results, introduce color scale instead
Show failed (some required testcase failed) as red, missing (some required testcase missing) as yellow, inconclusive (some required testcase aborted) as green with asterisk, passed (all required testcases passed) as green. The reduces the probability of confusing people with false positives, while no longer requiring (unrealistically) labor-intensive approvals. Besides, the two views 'table_full' vs 'table' now only differ in that the former shows draft versions; similarly for 'details[_full]'. Further work remains: - remove approval column from database - remove approval-related code from sql.py - remove endpoint for POSTing /results - simplify, consolidate, isolate business logic - etc. We're doing this incrementally... for now, let's see whether this works at all. Signed-off-by: Matthias Büchse <matthias.buechse@alasca.cloud>
1 parent 7a2d25d commit 58c8b2b

4 files changed

Lines changed: 74 additions & 80 deletions

File tree

Tests/scs_cert_lib.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ def _resolve_spec(spec: dict):
8484
# step 4. resolve references
8585
# step 4a. resolve references to modules in includes
8686
# in this step, we also normalize the include form
87-
for version in spec['versions'].values():
87+
for idx, version in enumerate(spec['versions'].values()):
88+
version['_idx'] = idx
8889
version['include'] = [
8990
{'module': module_lookup[inc], 'parameters': {}} if isinstance(inc, str) else
9091
{'module': module_lookup[inc['ref']], 'parameters': inc.get('parameters', {})}
@@ -205,8 +206,8 @@ def eval_buckets(results, testcase_ids) -> dict:
205206

206207
def evaluate(results, testcase_ids) -> int:
207208
"""returns overall result"""
208-
return min([
209-
# here, we treat None (MISSING) as 0 (ABORT)
210-
results.get(testcase_id, {}).get('result') or 0
211-
for testcase_id in testcase_ids
212-
], default=0)
209+
buckets = eval_buckets(results, testcase_ids)
210+
for value in (-1, None, 0):
211+
if buckets[value]:
212+
return value
213+
return 1

compliance-monitor/monitor.py

Lines changed: 66 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ def _evaluate_version(version, scope_results):
257257
for tname, tc_ids in version['targets'].items()
258258
}
259259
return {
260+
'_idx': version['_idx'],
260261
'result': target_results['main']['result'],
261262
'targets': target_results,
262263
'tc_target': version['tc_target'],
@@ -273,21 +274,22 @@ def _evaluate_scope(spec, scope_results, include_drafts=False):
273274
for vname, version in versions.items()
274275
if version['_explicit_validity']
275276
}
276-
by_validity = defaultdict(list)
277-
for vname, version in versions.items():
278-
by_validity[version['_explicit_validity']].append(vname)
279-
# go through worsening validity values until a passing version is found
277+
winner = None # first passed version that's not a draft
278+
result = -1
279+
passed = []
280280
relevant = []
281-
best_passed = None
282-
for validity in ('effective', 'warn', 'deprecated'):
283-
vnames = by_validity[validity]
284-
relevant.extend(vnames)
285-
if any(version_results[vname]['result'] == 1 for vname in vnames):
286-
best_passed = validity
287-
break
288-
if include_drafts:
289-
relevant.extend(by_validity['draft'])
290-
passed = [vname for vname in relevant if version_results[vname]['result'] == 1]
281+
# assumption: versions are listed in spec in descending order recency
282+
# first the drafts, then effective, then warn, then the rest
283+
for vname, version_result in version_results.items():
284+
if version_result['validity'] == 'draft' and not include_drafts:
285+
continue
286+
relevant.append(vname)
287+
result = version_result['result']
288+
if result != -1:
289+
passed.append(vname)
290+
if version_result['validity'] != 'draft':
291+
winner = vname
292+
break
291293
# only list testcases that occur in any relevant version
292294
relevant_testcases = set()
293295
for vname in relevant:
@@ -304,12 +306,14 @@ def _evaluate_scope(spec, scope_results, include_drafts=False):
304306
},
305307
'versions': version_results,
306308
'relevant': relevant,
309+
'result': result,
307310
'passed': passed,
308311
'passed_str': ', '.join([
309-
vname + ASTERISK_LOOKUP[versions[vname]['validity']]
312+
vname + ASTERISK_LOOKUP[version_results[vname]['validity']]
310313
for vname in passed
311314
]),
312-
'best_passed': best_passed,
315+
'best_passed': None if winner is None else version_results[winner]['_idx'],
316+
'validity': 'deprecated' if winner is None else version_results[winner]['validity'],
313317
}
314318

315319

@@ -672,20 +676,25 @@ async def get_detail(
672676
subject: str,
673677
scopeuuid: str,
674678
):
679+
return _make_detail_view(conn, view_type, subject, scopeuuid)
680+
681+
682+
def _make_detail_view(conn, view_type, subject, scopeuuid, include_drafts=False):
675683
scopeuuid = _resolve_scope(scopeuuid)
676684
with conn.cursor() as cur:
677685
group, subjects = _resolve_group(cur, subject)
678686
rows2 = []
679687
for subj in subjects:
680-
rows2.extend(db_get_relevant_results2(cur, subj, scopeuuid, approved_only=True))
688+
rows2.extend(db_get_relevant_results2(cur, subj, scopeuuid))
681689
results2 = convert_result_rows_to_dict2(
682-
rows2, get_scopes(), include_report=True, grace_period_days=GRACE_PERIOD_DAYS,
690+
rows2, get_scopes(), include_report=True, include_drafts=include_drafts,
683691
subjects=subjects, scopes=(scopeuuid, ),
684692
)
685693
title = f'Details for group {group}' if group else f'Details for subject {subject}'
694+
if include_drafts:
695+
title += ' (incl. drafts)'
686696
return render_view(
687-
VIEW_DETAIL, view_type, results=results2, base_url=settings.base_url,
688-
title=title,
697+
VIEW_DETAIL, view_type, results=results2, base_url=settings.base_url, title=title,
689698
)
690699

691700

@@ -697,21 +706,7 @@ async def get_detail_full(
697706
subject: str,
698707
scopeuuid: str,
699708
):
700-
scopeuuid = _resolve_scope(scopeuuid)
701-
with conn.cursor() as cur:
702-
group, subjects = _resolve_group(cur, subject)
703-
rows2 = []
704-
for subj in subjects:
705-
rows2.extend(db_get_relevant_results2(cur, subj, scopeuuid, approved_only=False))
706-
results2 = convert_result_rows_to_dict2(
707-
rows2, get_scopes(), include_report=True, include_drafts=True,
708-
subjects=subjects, scopes=(scopeuuid, ),
709-
)
710-
title = f'Details for group {group}' if group else f'Details for subject {subject}'
711-
return render_view(
712-
VIEW_DETAIL, view_type, results=results2, base_url=settings.base_url,
713-
title=f'{title} (incl. unverified results)',
714-
)
709+
return _make_detail_view(conn, view_type, subject, scopeuuid, include_drafts=True)
715710

716711

717712
@app.get("/{view_type}/table")
@@ -720,13 +715,20 @@ async def get_table(
720715
conn: Annotated[connection, Depends(get_conn)],
721716
view_type: ViewType,
722717
):
718+
return _make_table_view(conn, view_type, detail_page='detail')
719+
720+
721+
def _make_table_view(conn, view_type, detail_page, include_drafts=False):
723722
with conn.cursor() as cur:
724723
groups = db_get_groups(cur)
725-
rows2 = db_get_relevant_results2(cur, approved_only=True)
726-
results2 = convert_result_rows_to_dict2(rows2, get_scopes(), grace_period_days=GRACE_PERIOD_DAYS)
724+
rows2 = db_get_relevant_results2(cur)
725+
results2 = convert_result_rows_to_dict2(rows2, get_scopes(), include_drafts=include_drafts)
726+
title = 'SCS compliance overview'
727+
if include_drafts:
728+
title += ' (incl. drafts)'
727729
return render_view(
728-
VIEW_TABLE, view_type, results=results2, base_url=settings.base_url, detail_page='detail',
729-
title="SCS compliance overview", groups=groups,
730+
VIEW_TABLE, view_type, results=results2, base_url=settings.base_url, detail_page=detail_page,
731+
title=title, groups=groups,
730732
)
731733

732734

@@ -736,14 +738,7 @@ async def get_table_full(
736738
conn: Annotated[connection, Depends(get_conn)],
737739
view_type: ViewType,
738740
):
739-
with conn.cursor() as cur:
740-
groups = db_get_groups(cur)
741-
rows2 = db_get_relevant_results2(cur, approved_only=False)
742-
results2 = convert_result_rows_to_dict2(rows2, get_scopes(), include_drafts=True)
743-
return render_view(
744-
VIEW_TABLE, view_type, results=results2, base_url=settings.base_url, detail_page='detail_full',
745-
title="SCS compliance overview (incl. unverified results)", unverified=True, groups=groups,
746-
)
741+
return _make_table_view(conn, view_type, detail_page='detail_full', include_drafts=True)
747742

748743

749744
@app.get("/{view_type}/scope/{scopeuuid}")
@@ -756,13 +751,12 @@ async def get_scope(
756751
scopeuuid = _resolve_scope(scopeuuid)
757752
spec = get_scopes()[scopeuuid]
758753
versions = spec['versions']
759-
# sort by name, and all drafts after all non-drafts
760-
column_data = [
761-
(version['_explicit_validity'].lower() == 'draft', name)
754+
# use same order as in details view
755+
relevant = [
756+
name
762757
for name, version in versions.items()
763758
if version['_explicit_validity']
764759
]
765-
relevant = [name for _, name in sorted(column_data)]
766760
modules_chart = {}
767761
for name in relevant:
768762
for include in versions[name]['include']:
@@ -839,40 +833,42 @@ def pick_filter(ctx, results, scopeuuid, *subjects):
839833
return [r for r in rs if r is not None]
840834

841835

842-
STATUS_ORDERING = {
843-
'effective': 10,
844-
'warn': 5,
845-
'deprecated': 1,
836+
COLOR_MAP = {
837+
-1: '🛑', # fail
838+
None: '🟧', # missing
839+
0: '✅*', # inconclusive
840+
1: '✅', # pass
846841
}
847842

848843

849844
def summary_filter(scope_results):
850845
"""Jinja filter to construct summary from `scope_results`"""
846+
# be prepared for empty dicts here because they are created to avoid KeyError in jinja2
851847
if not isinstance(scope_results, dict):
852848
# new generalized case: "aggregate" results for multiple subjects
853849
# simplified computation: just select the worst subject to represent the group
850+
scope_results = [sr for sr in scope_results if sr.get('best_passed') is not None]
854851
scope_results = min(
855852
scope_results,
856853
default={},
857-
key=lambda sr: STATUS_ORDERING.get(sr.get('best_passed'), -1),
854+
key=lambda sr: -sr['best_passed'],
858855
)
859-
passed_str = scope_results.get('passed_str', '') or '–'
860-
best_passed = scope_results.get('best_passed')
861-
# avoid simple 🟢🔴 (hard to distinguish for color-blind folks)
862-
color = {
863-
'effective': '✅',
864-
'warn': '✅', # forgo differentiation here in favor of simplicity (will be apparent in version list)
865-
'deprecated': '🟧',
866-
}.get(best_passed, '🛑')
856+
if not scope_results:
857+
return '🛑 –'
858+
result = scope_results['result']
859+
color = COLOR_MAP[result]
860+
# if the result is not pass anyway, deduct points if the version is outdated
861+
# (this case should happen very rarely because we usually don't consider those)
862+
if result != -1:
863+
validity = scope_results['validity']
864+
if validity == 'warn':
865+
color = '🟧'
866+
elif validity == 'deprecated':
867+
color = '🛑'
868+
passed_str = scope_results['passed_str'] or '–'
867869
return f'{color} {passed_str}'
868870

869871

870-
def verdict_filter(value):
871-
"""Jinja filter to turn a canonical result value into a written verdict (PASS, MISS, or FAIL)"""
872-
# be fault-tolerant here and turn every non-canonical value into a MISS
873-
return {1: 'PASS', -1: 'FAIL'}.get(value, 'MISS')
874-
875-
876872
def verdict_check_filter(value):
877873
"""Jinja filter to turn a canonical result value into a symbolic verdict (✔, ⚠, or ✘)"""
878874
# be fault-tolerant here and turn every non-canonical value into a MISS
@@ -905,7 +901,6 @@ def reload_static_config(*args, do_ensure_schema=False):
905901
env.filters.update(
906902
pick=pick_filter,
907903
summary=summary_filter,
908-
verdict=verdict_filter,
909904
verdict_check=verdict_check_filter,
910905
markdown=markdown,
911906
validity_symbol=ASTERISK_LOOKUP.get,

compliance-monitor/sql.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ def db_insert_result2(
386386

387387
def db_get_relevant_results2(
388388
cur: cursor,
389-
subject=None, scopeuuid=None, version=None, approved_only=True,
389+
subject=None, scopeuuid=None, version=None, approved_only=False,
390390
):
391391
"""for each combination of scope/version/check, get the most recent test result that is still valid"""
392392
# find the latest result per subject/scopeuuid/version/checkid for this subject

compliance-monitor/templates/overview.md.j2

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ we could of course iterate over results etc., but hardcode the table (except the
33
for the time being to have the highest degree of control
44
-#}
55

6-
{% if unverified %}These tables show the most recent results, including unverified ones. Consumers are referred to the [verified tables]({{base_url}}page/table). **Beware of false positives!**{% else %}These tables show the most recent **verified** results.{% endif %}
7-
86
Version numbers are suffixed by a symbol depending on state: * for _draft_, † for _warn_ (soon to be deprecated), and †† for _deprecated_.
97

108
### SCS-compatible IaaS

0 commit comments

Comments
 (0)