diff --git a/features/steps/givens/attributes.py b/features/steps/givens/attributes.py index 41ad8a35..0384f258 100644 --- a/features/steps/givens/attributes.py +++ b/features/steps/givens/attributes.py @@ -1,4 +1,5 @@ import ast +import functools import operator import ifcopenshell @@ -147,6 +148,34 @@ def step_impl(context, file_or_model, field, values): context.applicable = getattr(context, 'applicable', True) and applicable +@gherkin_ifc.step('a traversal over the full model originating from subtypes of {entity}') +def step_impl(context, entity): + WHITELISTED_INVERSES = {'StyledByItem'} + schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(context.model.schema_identifier) + @functools.cache + def names(entity_type): + decl = schema.declaration_by_name(entity_type) + if isinstance(decl, ifcopenshell.ifcopenshell_wrapper.entity): + non_derived_forward_attributes = map(operator.itemgetter(1), filter(lambda t: not t[0], zip(decl.derived(), decl.all_attributes()))) + whitelisted_inverse_attributes = filter(lambda attr: attr.name() in WHITELISTED_INVERSES, decl.all_inverse_attributes()) + return {a.name() for a in [*non_derived_forward_attributes, *whitelisted_inverse_attributes]} + else: + return set() + + visited = set() + def visit(inst, path=None): + if inst in visited: + return + visited.add(inst) + for attr in names(inst.is_a()): + for ref in filter(lambda inst: isinstance(inst, ifcopenshell.entity_instance), misc.iflatten(getattr(inst, attr))): + visit(ref, (path or ()) + (inst, attr,)) + + for inst in context.model.by_type(entity): + visit(inst) + + context.visited_instances = visited + @gherkin_ifc.step('Its attribute {attribute}') def step_impl(context, inst, attribute, tail="single"): yield ValidationOutcome(instance_id=getattr(inst, attribute, None), severity=OutcomeSeverity.PASSED) diff --git a/features/steps/givens/entities.py b/features/steps/givens/entities.py index ea313666..2198fe14 100644 --- a/features/steps/givens/entities.py +++ b/features/steps/givens/entities.py @@ -20,18 +20,21 @@ def step_impl(context, entity_opt_stmt, insts=False): within_model = (insts == 'instances') # True for given statement containing {insts} - entity2 = pyparsing.Word(pyparsing.alphas)('entity') - sub_stmts = ['with subtypes', 'without subtypes', pyparsing.LineEnd()] - incl_sub_stmt = functools.reduce(operator.or_, [misc.rtrn_pyparse_obj(i) for i in sub_stmts])('include_subtypes') - grammar = entity2 + incl_sub_stmt - parse = grammar.parseString(entity_opt_stmt) - entity = parse['entity'] - include_subtypes = misc.do_try(lambda: not 'without' in parse['include_subtypes'], True) - - try: - instances = context.model.by_type(entity, include_subtypes) - except: - instances = [] + if entity_opt_stmt == "entity instance": + instances = list(context.model) + else: + entity2 = pyparsing.Word(pyparsing.alphas)('entity') + sub_stmts = ['with subtypes', 'without subtypes', pyparsing.LineEnd()] + incl_sub_stmt = functools.reduce(operator.or_, [misc.rtrn_pyparse_obj(i) for i in sub_stmts])('include_subtypes') + grammar = entity2 + incl_sub_stmt + parse = grammar.parseString(entity_opt_stmt) + entity = parse['entity'] + include_subtypes = misc.do_try(lambda: not 'without' in parse['include_subtypes'], True) + + try: + instances = context.model.by_type(entity, include_subtypes) + except: + instances = [] context.within_model = getattr(context, 'within_model', True) and within_model if instances: @@ -73,4 +76,17 @@ def step_impl(context, inst, relationship_direction): def step_impl(context, inst): # Note that this includes `inst` as the first element in this list instances = context.model.traverse(inst) - yield ValidationOutcome(instance_id=instances, severity=OutcomeSeverity.PASSED) \ No newline at end of file + yield ValidationOutcome(instance_id=instances, severity=OutcomeSeverity.PASSED) + +""" +@gherkin_ifc.step("its entity type is {entity}") +def step_impl(context, inst, entity): + negate = False + entity = entity.split(' ') + if entity[0] == 'not': + negate = not negate + entity = entity[1:] + entity = entity[0] + if inst.is_a(entity): + yield ValidationOutcome(instance_id=inst, severity=OutcomeSeverity.PASSED) +""" \ No newline at end of file diff --git a/features/steps/thens/relations.py b/features/steps/thens/relations.py index 15db983d..b944e459 100644 --- a/features/steps/thens/relations.py +++ b/features/steps/thens/relations.py @@ -388,4 +388,13 @@ def schema_has_declaration_name(s): # @tfk not sure about this one, but for now empty values on a property are probably # not a universal error. This is more IDS territory. # if not values: - # yield ValidationOutcome(inst=inst, expected= {"oneOf": accepted_data_type['instance']}, observed = {'value':None}, severity=OutcomeSeverity.ERROR) \ No newline at end of file + # yield ValidationOutcome(inst=inst, expected= {"oneOf": accepted_data_type['instance']}, observed = {'value':None}, severity=OutcomeSeverity.ERROR) + +@gherkin_ifc.step('it must be referenced by an entity instance inheriting from IfcRoot directly or indirectly') +def step_impl(context, inst): + # context.visited_instances is set in the gherkin statement: + # 'Given a traversal over the full model originating from subtypes of IfcRoot' + assert hasattr(context, 'visited_instances') + + if inst not in context.visited_instances: + yield ValidationOutcome(inst=inst, severity=OutcomeSeverity.ERROR) diff --git a/features/steps/utils/misc.py b/features/steps/utils/misc.py index 4003833b..e6d3ef3e 100644 --- a/features/steps/utils/misc.py +++ b/features/steps/utils/misc.py @@ -31,6 +31,14 @@ def recursive_flatten(lst): return flattened_list +def iflatten(any): + if isinstance(any, (tuple, list)): + for v in any: + yield from iflatten(v) + else: + yield any + + def do_try(fn, default=None): try: return fn() diff --git a/features/steps/validation_handling.py b/features/steps/validation_handling.py index eb4a9f6c..8f0aed8b 100644 --- a/features/steps/validation_handling.py +++ b/features/steps/validation_handling.py @@ -65,10 +65,9 @@ class SubtypeHandling(Enum): def generate_error_message(context, errors): """ - Function to trigger the behave error mechanism so that the JSON output is generated correctly. - Miscellaneous errors also are also printed to the console this way. + Function to trigger the behave error mechanism by raising an exception so that errors are printed to the console. """ - assert not errors, "Behave errors occured:\n{}".format([str(error) for error in errors]) + assert not errors, "Errors occured:" + ''.join(f'\n - {error}' for error in errors) """