Fancy up the conformance checker a bit. Clearer error messages, and allow constraints to return not just a boolean, but also (lists of) strings, containing error messages.

This commit is contained in:
Joeri Exelmans 2024-10-29 23:47:41 +01:00
parent a14676da42
commit 48f7a455fb

View file

@ -5,6 +5,8 @@ from uuid import UUID
from state.base import State from state.base import State
from typing import Dict, Tuple, Set, Any, List from typing import Dict, Tuple, Set, Any, List
from pprint import pprint from pprint import pprint
import traceback
from concrete_syntax.common import indent
from api.cd import CDAPI from api.cd import CDAPI
from api.od import ODAPI from api.od import ODAPI
@ -26,8 +28,8 @@ def render_conformance_check_result(error_list):
if len(error_list) == 0: if len(error_list) == 0:
return "CONFORM" return "CONFORM"
else: else:
joined = '\n '.join(error_list) joined = ''.join(('\n' + err for err in error_list))
return f"NOT CONFORM, {len(error_list)} errors: \n {joined}" return f"NOT CONFORM, {len(error_list)} errors:{joined}"
class Conformance: class Conformance:
@ -276,7 +278,7 @@ class Conformance:
import traceback import traceback
traceback.format_exc(e) traceback.format_exc(e)
# no or too many morphism links found # no or too many morphism links found
errors.append(f"Incorrectly typed element: {m_name}") errors.append(f"Incorrectly typed element: '{m_name}'")
return errors return errors
def check_link_typing(self): def check_link_typing(self):
@ -301,14 +303,14 @@ class Conformance:
source_type_expected = self.type_model_names[tm_source] source_type_expected = self.type_model_names[tm_source]
if source_type_actual != source_type_expected: if source_type_actual != source_type_expected:
if source_type_actual not in self.sub_types[source_type_expected]: if source_type_actual not in self.sub_types[source_type_expected]:
errors.append(f"Invalid source type {source_type_actual} for element {m_name}") errors.append(f"Invalid source type '{source_type_actual}' for element '{m_name}'")
# check if target is typed correctly # check if target is typed correctly
target_name = self.model_names[m_target] target_name = self.model_names[m_target]
target_type_actual = self.type_mapping[target_name] target_type_actual = self.type_mapping[target_name]
target_type_expected = self.type_model_names[tm_target] target_type_expected = self.type_model_names[tm_target]
if target_type_actual != target_type_expected: if target_type_actual != target_type_expected:
if target_type_actual not in self.sub_types[target_type_expected]: if target_type_actual not in self.sub_types[target_type_expected]:
errors.append(f"Invalid target type {target_type_actual} for element {m_name}") errors.append(f"Invalid target type '{target_type_actual}' for element '{m_name}'")
return errors return errors
def check_multiplicities(self): def check_multiplicities(self):
@ -323,7 +325,7 @@ class Conformance:
if tm_name in self.abstract_types: if tm_name in self.abstract_types:
type_count = list(self.type_mapping.values()).count(tm_name) type_count = list(self.type_mapping.values()).count(tm_name)
if type_count > 0: if type_count > 0:
errors.append(f"Invalid instantiation of abstract class: {tm_name}") errors.append(f"Invalid instantiation of abstract class: '{tm_name}'")
# class multiplicities # class multiplicities
if tm_name in self.multiplicities: if tm_name in self.multiplicities:
lc, uc = self.multiplicities[tm_name] lc, uc = self.multiplicities[tm_name]
@ -331,7 +333,7 @@ class Conformance:
for sub_type in self.sub_types[tm_name]: for sub_type in self.sub_types[tm_name]:
type_count += list(self.type_mapping.values()).count(sub_type) type_count += list(self.type_mapping.values()).count(sub_type)
if type_count < lc or type_count > uc: if type_count < lc or type_count > uc:
errors.append(f"Cardinality of type exceeds valid multiplicity range: {tm_name} ({type_count})") errors.append(f"Cardinality of type exceeds valid multiplicity range: '{tm_name}' ({type_count})")
# association source multiplicities # association source multiplicities
if tm_name in self.source_multiplicities: if tm_name in self.source_multiplicities:
tm_element, = self.bottom.read_outgoing_elements(self.type_model, tm_name) tm_element, = self.bottom.read_outgoing_elements(self.type_model, tm_name)
@ -350,7 +352,7 @@ class Conformance:
except KeyError: except KeyError:
pass # for elements not part of model, e.g. morphism links pass # for elements not part of model, e.g. morphism links
if count < lc or count > uc: if count < lc or count > uc:
errors.append(f"Source cardinality of type {tm_name} ({count}) out of bounds ({lc}..{uc}) in {tgt_obj_name}.") errors.append(f"Source cardinality of type '{tm_name}' ({count}) out of bounds ({lc}..{uc}) in '{tgt_obj_name}'.")
# association target multiplicities # association target multiplicities
if tm_name in self.target_multiplicities: if tm_name in self.target_multiplicities:
@ -376,7 +378,7 @@ class Conformance:
except KeyError: except KeyError:
pass # for elements not part of model, e.g. morphism links pass # for elements not part of model, e.g. morphism links
if count < lc or count > uc: if count < lc or count > uc:
errors.append(f"Target cardinality of type {tm_name} ({count}) out of bounds ({lc}..{uc}) in {src_obj_name}.") errors.append(f"Target cardinality of type '{tm_name}' ({count}) out of bounds ({lc}..{uc}) in '{src_obj_name}'.")
# else: # else:
# print(f"OK: Target cardinality of type {tm_name} ({count}) within bounds ({lc}..{uc}) in {src_obj_name}.") # print(f"OK: Target cardinality of type {tm_name} ({count}) within bounds ({lc}..{uc}) in {src_obj_name}.")
return errors return errors
@ -425,10 +427,17 @@ class Conformance:
return code return code
def check_result(result, description): def check_result(result, description):
if not isinstance(result, bool): if isinstance(result, str):
raise Exception(f"{description} evaluation result is not boolean! Instead got {result}") errors.append(f"{description} not satisfied. Reason: {result}")
elif isinstance(result, bool):
if not result: if not result:
errors.append(f"{description} not satisfied.") errors.append(f"{description} not satisfied.")
elif isinstance(result, list):
if len(result) > 0:
reasons = indent('\n'.join(result), 2)
errors.append(f"{description} not satisfied. Reasons:\n{reasons}")
else:
raise Exception(f"{description} evaluation result should be boolean or string! Instead got {result}")
# local constraints # local constraints
for type_name in self.bottom.read_keys(self.type_model): for type_name in self.bottom.read_keys(self.type_model):
@ -440,8 +449,9 @@ class Conformance:
# print(description) # print(description)
try: try:
result = self.evaluate_constraint(code, this=obj_id) result = self.evaluate_constraint(code, this=obj_id)
except Exception as e: except:
raise Exception(f"Context of above error = {description}") from e errors.append(f"Runtime error during evaluation of {description}:\n{indent(traceback.format_exc(), 6)}")
# raise Exception(f"Context of above error = {description}") from e
check_result(result, description) check_result(result, description)
# global constraints # global constraints
@ -462,8 +472,8 @@ class Conformance:
description = f"Global constraint \"{tm_name}\"" description = f"Global constraint \"{tm_name}\""
try: try:
result = self.evaluate_constraint(code, model=self.model) result = self.evaluate_constraint(code, model=self.model)
except Exception as e: except:
raise Exception(f"Context of above error = {description}") from e errors.append(f"Runtime error during evaluation of {description}:\n{indent(traceback.format_exc(), 6)}")
check_result(result, description) check_result(result, description)
return errors return errors