Skip to content

Commit a1776e2

Browse files
authored
Scrap approval of test results, introduce color scale instead (#1221)
* Scrap approval of test results, introduce color scale instead * Unflake * Fix bug in selection of representative group result 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. Signed-off-by: Matthias Büchse <matthias.buechse@alasca.cloud>
1 parent af4968c commit a1776e2

4 files changed

Lines changed: 82 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: 74 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,50 @@ 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+
NIL = object() # the version in question does not have a result
837+
# used to sort multiple versions according to the "goodness" of their result
838+
RESULT_SCORE = {
839+
-1: 0,
840+
None: 1,
841+
NIL: 2, # NIL and None are basically the same, but prefer None because it has more info
842+
0: 3,
843+
1: 4,
844+
}
845+
COLOR_MAP = {
846+
-1: '🛑', # fail
847+
None: '🟧', # missing
848+
0: '✅*', # inconclusive
849+
1: '✅', # pass
846850
}
847851

848852

849853
def summary_filter(scope_results):
850854
"""Jinja filter to construct summary from `scope_results`"""
855+
# be prepared for empty dicts here because they are created to avoid KeyError in jinja2
851856
if not isinstance(scope_results, dict):
852857
# new generalized case: "aggregate" results for multiple subjects
853858
# simplified computation: just select the worst subject to represent the group
854859
scope_results = min(
855860
scope_results,
856861
default={},
857-
key=lambda sr: STATUS_ORDERING.get(sr.get('best_passed'), -1),
862+
key=lambda sr: RESULT_SCORE[sr.get('result', NIL)],
858863
)
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, '🛑')
864+
if not scope_results:
865+
return '🛑 –'
866+
result = scope_results['result']
867+
color = COLOR_MAP[result]
868+
# if the result is not pass anyway, deduct points if the version is outdated
869+
# (this case should happen very rarely because we usually don't consider those)
870+
if result != -1:
871+
validity = scope_results['validity']
872+
if validity == 'warn':
873+
color = '🟧'
874+
elif validity == 'deprecated':
875+
color = '🛑'
876+
passed_str = scope_results['passed_str'] or '–'
867877
return f'{color} {passed_str}'
868878

869879

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-
876880
def verdict_check_filter(value):
877881
"""Jinja filter to turn a canonical result value into a symbolic verdict (✔, ⚠, or ✘)"""
878882
# be fault-tolerant here and turn every non-canonical value into a MISS
@@ -905,7 +909,6 @@ def reload_static_config(*args, do_ensure_schema=False):
905909
env.filters.update(
906910
pick=pick_filter,
907911
summary=summary_filter,
908-
verdict=verdict_filter,
909912
verdict_check=verdict_check_filter,
910913
markdown=markdown,
911914
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)