• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

localstack / localstack / 274ae585-9ad2-4b5f-8087-866ef08d3d6e

24 Apr 2025 05:15PM UTC coverage: 85.262% (-1.0%) from 86.266%
274ae585-9ad2-4b5f-8087-866ef08d3d6e

push

circleci

web-flow
CFn v2: support outputs (#12536)

10 of 29 new or added lines in 3 files covered. (34.48%)

1105 existing lines in 26 files now uncovered.

63256 of 74190 relevant lines covered (85.26%)

0.85 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

19.67
/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py
1
from __future__ import annotations
1✔
2

3
from typing import Any, Final, Generic, Optional, TypeVar
1✔
4

5
from localstack.services.cloudformation.engine.v2.change_set_model import (
1✔
6
    ChangeSetEntity,
7
    ChangeType,
8
    NodeArray,
9
    NodeCondition,
10
    NodeDivergence,
11
    NodeIntrinsicFunction,
12
    NodeMapping,
13
    NodeObject,
14
    NodeOutput,
15
    NodeOutputs,
16
    NodeParameter,
17
    NodeProperties,
18
    NodeProperty,
19
    NodeResource,
20
    NodeTemplate,
21
    NothingType,
22
    Scope,
23
    TerminalValue,
24
    TerminalValueCreated,
25
    TerminalValueModified,
26
    TerminalValueRemoved,
27
    TerminalValueUnchanged,
28
)
29
from localstack.services.cloudformation.engine.v2.change_set_model_visitor import (
1✔
30
    ChangeSetModelVisitor,
31
)
32

33
TBefore = TypeVar("TBefore")
1✔
34
TAfter = TypeVar("TAfter")
1✔
35

36

37
class PreprocEntityDelta(Generic[TBefore, TAfter]):
1✔
38
    before: Optional[TBefore]
1✔
39
    after: Optional[TAfter]
1✔
40

41
    def __init__(self, before: Optional[TBefore] = None, after: Optional[TAfter] = None):
1✔
UNCOV
42
        self.before = before
×
UNCOV
43
        self.after = after
×
44

45
    def __eq__(self, other):
1✔
46
        if not isinstance(other, PreprocEntityDelta):
×
47
            return False
×
48
        return self.before == other.before and self.after == other.after
×
49

50

51
class PreprocProperties:
1✔
52
    properties: dict[str, Any]
1✔
53

54
    def __init__(self, properties: dict[str, Any]):
1✔
UNCOV
55
        self.properties = properties
×
56

57
    def __eq__(self, other):
1✔
UNCOV
58
        if not isinstance(other, PreprocProperties):
×
59
            return False
×
UNCOV
60
        return self.properties == other.properties
×
61

62

63
class PreprocResource:
1✔
64
    logical_id: str
1✔
65
    physical_resource_id: Optional[str]
1✔
66
    condition: Optional[bool]
1✔
67
    resource_type: str
1✔
68
    properties: PreprocProperties
1✔
69

70
    def __init__(
1✔
71
        self,
72
        logical_id: str,
73
        physical_resource_id: str,
74
        condition: Optional[bool],
75
        resource_type: str,
76
        properties: PreprocProperties,
77
    ):
UNCOV
78
        self.logical_id = logical_id
×
UNCOV
79
        self.physical_resource_id = physical_resource_id
×
UNCOV
80
        self.condition = condition
×
UNCOV
81
        self.resource_type = resource_type
×
UNCOV
82
        self.properties = properties
×
83

84
    @staticmethod
1✔
85
    def _compare_conditions(c1: bool, c2: bool):
1✔
86
        # The lack of condition equates to a true condition.
UNCOV
87
        c1 = c1 if isinstance(c1, bool) else True
×
UNCOV
88
        c2 = c2 if isinstance(c2, bool) else True
×
UNCOV
89
        return c1 == c2
×
90

91
    def __eq__(self, other):
1✔
UNCOV
92
        if not isinstance(other, PreprocResource):
×
UNCOV
93
            return False
×
UNCOV
94
        return all(
×
95
            [
96
                self.logical_id == other.logical_id,
97
                self._compare_conditions(self.condition, other.condition),
98
                self.resource_type == other.resource_type,
99
                self.properties == other.properties,
100
            ]
101
        )
102

103

104
class PreprocOutput:
1✔
105
    name: str
1✔
106
    value: Any
1✔
107
    export: Optional[Any]
1✔
108
    condition: Optional[bool]
1✔
109

110
    def __init__(self, name: str, value: Any, export: Optional[Any], condition: Optional[bool]):
1✔
UNCOV
111
        self.name = name
×
UNCOV
112
        self.value = value
×
UNCOV
113
        self.export = export
×
114
        self.condition = condition
×
115

116
    def __eq__(self, other):
1✔
UNCOV
117
        if not isinstance(other, PreprocOutput):
×
UNCOV
118
            return False
×
UNCOV
119
        return all(
×
120
            [
121
                self.name == other.name,
122
                self.value == other.value,
123
                self.export == other.export,
124
                self.condition == other.condition,
125
            ]
126
        )
127

128

129
class ChangeSetModelPreproc(ChangeSetModelVisitor):
1✔
130
    _node_template: Final[NodeTemplate]
1✔
131
    _before_resolved_resources: Final[dict]
1✔
132
    _processed: dict[Scope, Any]
1✔
133

134
    def __init__(self, node_template: NodeTemplate, before_resolved_resources: dict):
1✔
UNCOV
135
        self._node_template = node_template
×
UNCOV
136
        self._before_resolved_resources = before_resolved_resources
×
UNCOV
137
        self._processed = dict()
×
138

139
    def process(self) -> None:
1✔
UNCOV
140
        self._processed.clear()
×
UNCOV
141
        self.visit(self._node_template)
×
142

143
    def _get_node_resource_for(
1✔
144
        self, resource_name: str, node_template: NodeTemplate
145
    ) -> NodeResource:
146
        # TODO: this could be improved with hashmap lookups if the Node contained bindings and not lists.
UNCOV
147
        for node_resource in node_template.resources.resources:
×
UNCOV
148
            if node_resource.name == resource_name:
×
UNCOV
149
                return node_resource
×
150
        # TODO
UNCOV
151
        raise RuntimeError()
×
152

153
    def _get_node_property_for(
1✔
154
        self, property_name: str, node_resource: NodeResource
155
    ) -> NodeProperty:
156
        # TODO: this could be improved with hashmap lookups if the Node contained bindings and not lists.
UNCOV
157
        for node_property in node_resource.properties.properties:
×
UNCOV
158
            if node_property.name == property_name:
×
UNCOV
159
                return node_property
×
160
        # TODO
UNCOV
161
        raise RuntimeError()
×
162

163
    def _get_node_mapping(self, map_name: str) -> NodeMapping:
1✔
UNCOV
164
        mappings: list[NodeMapping] = self._node_template.mappings.mappings
×
165
        # TODO: another scenarios suggesting property lookups might be preferable.
UNCOV
166
        for mapping in mappings:
×
UNCOV
167
            if mapping.name == map_name:
×
UNCOV
168
                return mapping
×
169
        # TODO
UNCOV
170
        raise RuntimeError()
×
171

172
    def _get_node_parameter_if_exists(self, parameter_name: str) -> Optional[NodeParameter]:
1✔
UNCOV
173
        parameters: list[NodeParameter] = self._node_template.parameters.parameters
×
174
        # TODO: another scenarios suggesting property lookups might be preferable.
UNCOV
175
        for parameter in parameters:
×
UNCOV
176
            if parameter.name == parameter_name:
×
UNCOV
177
                return parameter
×
UNCOV
178
        return None
×
179

180
    def _get_node_condition_if_exists(self, condition_name: str) -> Optional[NodeCondition]:
1✔
UNCOV
181
        conditions: list[NodeCondition] = self._node_template.conditions.conditions
×
182
        # TODO: another scenarios suggesting property lookups might be preferable.
UNCOV
183
        for condition in conditions:
×
UNCOV
184
            if condition.name == condition_name:
×
UNCOV
185
                return condition
×
UNCOV
186
        return None
×
187

188
    def _resolve_reference(self, logical_id: str) -> PreprocEntityDelta:
1✔
UNCOV
189
        node_condition = self._get_node_condition_if_exists(condition_name=logical_id)
×
UNCOV
190
        if isinstance(node_condition, NodeCondition):
×
UNCOV
191
            condition_delta = self.visit(node_condition)
×
UNCOV
192
            return condition_delta
×
193

UNCOV
194
        node_parameter = self._get_node_parameter_if_exists(parameter_name=logical_id)
×
UNCOV
195
        if isinstance(node_parameter, NodeParameter):
×
UNCOV
196
            parameter_delta = self.visit(node_parameter)
×
UNCOV
197
            return parameter_delta
×
198

199
        # TODO: check for KNOWN AFTER APPLY values for logical ids coming from intrinsic functions as arguments.
UNCOV
200
        node_resource = self._get_node_resource_for(
×
201
            resource_name=logical_id, node_template=self._node_template
202
        )
UNCOV
203
        resource_delta = self.visit(node_resource)
×
UNCOV
204
        before = resource_delta.before
×
UNCOV
205
        after = resource_delta.after
×
UNCOV
206
        return PreprocEntityDelta(before=before, after=after)
×
207

208
    def _resolve_mapping(
1✔
209
        self, map_name: str, top_level_key: str, second_level_key
210
    ) -> PreprocEntityDelta:
211
        # TODO: add support for nested intrinsic functions, and KNOWN AFTER APPLY logical ids.
UNCOV
212
        node_mapping: NodeMapping = self._get_node_mapping(map_name=map_name)
×
UNCOV
213
        top_level_value = node_mapping.bindings.bindings.get(top_level_key)
×
UNCOV
214
        if not isinstance(top_level_value, NodeObject):
×
UNCOV
215
            raise RuntimeError()
×
UNCOV
216
        second_level_value = top_level_value.bindings.get(second_level_key)
×
UNCOV
217
        mapping_value_delta = self.visit(second_level_value)
×
UNCOV
218
        return mapping_value_delta
×
219

220
    def _resolve_reference_binding(
1✔
221
        self, before_logical_id: Optional[str], after_logical_id: Optional[str]
222
    ) -> PreprocEntityDelta:
UNCOV
223
        before = None
×
UNCOV
224
        if before_logical_id is not None:
×
UNCOV
225
            before_delta = self._resolve_reference(logical_id=before_logical_id)
×
UNCOV
226
            before = before_delta.before
×
UNCOV
227
        after = None
×
UNCOV
228
        if after_logical_id is not None:
×
UNCOV
229
            after_delta = self._resolve_reference(logical_id=after_logical_id)
×
UNCOV
230
            after = after_delta.after
×
UNCOV
231
        return PreprocEntityDelta(before=before, after=after)
×
232

233
    def visit(self, change_set_entity: ChangeSetEntity) -> PreprocEntityDelta:
1✔
UNCOV
234
        delta = self._processed.get(change_set_entity.scope)
×
UNCOV
235
        if delta is not None:
×
UNCOV
236
            return delta
×
UNCOV
237
        delta = super().visit(change_set_entity=change_set_entity)
×
UNCOV
238
        self._processed[change_set_entity.scope] = delta
×
UNCOV
239
        return delta
×
240

241
    def visit_terminal_value_modified(
1✔
242
        self, terminal_value_modified: TerminalValueModified
243
    ) -> PreprocEntityDelta:
UNCOV
244
        return PreprocEntityDelta(
×
245
            before=terminal_value_modified.value,
246
            after=terminal_value_modified.modified_value,
247
        )
248

249
    def visit_terminal_value_created(
1✔
250
        self, terminal_value_created: TerminalValueCreated
251
    ) -> PreprocEntityDelta:
UNCOV
252
        return PreprocEntityDelta(after=terminal_value_created.value)
×
253

254
    def visit_terminal_value_removed(
1✔
255
        self, terminal_value_removed: TerminalValueRemoved
256
    ) -> PreprocEntityDelta:
UNCOV
257
        return PreprocEntityDelta(before=terminal_value_removed.value)
×
258

259
    def visit_terminal_value_unchanged(
1✔
260
        self, terminal_value_unchanged: TerminalValueUnchanged
261
    ) -> PreprocEntityDelta:
UNCOV
262
        return PreprocEntityDelta(
×
263
            before=terminal_value_unchanged.value,
264
            after=terminal_value_unchanged.value,
265
        )
266

267
    def visit_node_divergence(self, node_divergence: NodeDivergence) -> PreprocEntityDelta:
1✔
UNCOV
268
        before_delta = self.visit(node_divergence.value)
×
UNCOV
269
        after_delta = self.visit(node_divergence.divergence)
×
UNCOV
270
        return PreprocEntityDelta(before=before_delta.before, after=after_delta.after)
×
271

272
    def visit_node_object(self, node_object: NodeObject) -> PreprocEntityDelta:
1✔
UNCOV
273
        before = dict()
×
UNCOV
274
        after = dict()
×
UNCOV
275
        for name, change_set_entity in node_object.bindings.items():
×
276
            delta: PreprocEntityDelta = self.visit(change_set_entity=change_set_entity)
×
277
            match change_set_entity.change_type:
×
278
                case ChangeType.MODIFIED:
×
279
                    before[name] = delta.before
×
280
                    after[name] = delta.after
×
281
                case ChangeType.CREATED:
×
282
                    after[name] = delta.after
×
UNCOV
283
                case ChangeType.REMOVED:
×
UNCOV
284
                    before[name] = delta.before
×
UNCOV
285
                case ChangeType.UNCHANGED:
×
UNCOV
286
                    before[name] = delta.before
×
UNCOV
287
                    after[name] = delta.before
×
UNCOV
288
        return PreprocEntityDelta(before=before, after=after)
×
289

290
    def visit_node_intrinsic_function_fn_get_att(
1✔
291
        self, node_intrinsic_function: NodeIntrinsicFunction
292
    ) -> PreprocEntityDelta:
UNCOV
293
        arguments_delta = self.visit(node_intrinsic_function.arguments)
×
294
        # TODO: validate the return value according to the spec.
295
        before_argument_list = arguments_delta.before
×
296
        after_argument_list = arguments_delta.after
×
297

UNCOV
298
        before = None
×
UNCOV
299
        if before_argument_list:
×
300
            before_logical_name_of_resource = before_argument_list[0]
×
UNCOV
301
            before_attribute_name = before_argument_list[1]
×
UNCOV
302
            before_node_resource = self._get_node_resource_for(
×
303
                resource_name=before_logical_name_of_resource, node_template=self._node_template
304
            )
UNCOV
305
            before_node_property = self._get_node_property_for(
×
306
                property_name=before_attribute_name, node_resource=before_node_resource
307
            )
UNCOV
308
            before_property_delta = self.visit(before_node_property)
×
UNCOV
309
            before = before_property_delta.before
×
310

UNCOV
311
        after = None
×
UNCOV
312
        if after_argument_list:
×
313
            # TODO: when are values only accessible at runtime?
UNCOV
314
            after_logical_name_of_resource = after_argument_list[0]
×
UNCOV
315
            after_attribute_name = after_argument_list[1]
×
UNCOV
316
            after_node_resource = self._get_node_resource_for(
×
317
                resource_name=after_logical_name_of_resource, node_template=self._node_template
318
            )
UNCOV
319
            after_node_property = self._get_node_property_for(
×
320
                property_name=after_attribute_name, node_resource=after_node_resource
321
            )
UNCOV
322
            after_property_delta = self.visit(after_node_property)
×
UNCOV
323
            after = after_property_delta.after
×
324

UNCOV
325
        return PreprocEntityDelta(before=before, after=after)
×
326

327
    def visit_node_intrinsic_function_fn_equals(
1✔
328
        self, node_intrinsic_function: NodeIntrinsicFunction
329
    ) -> PreprocEntityDelta:
330
        # TODO: check for KNOWN AFTER APPLY values for logical ids coming from intrinsic functions as arguments.
UNCOV
331
        arguments_delta = self.visit(node_intrinsic_function.arguments)
×
UNCOV
332
        before_values = arguments_delta.before
×
UNCOV
333
        after_values = arguments_delta.after
×
UNCOV
334
        before = None
×
UNCOV
335
        if before_values:
×
UNCOV
336
            before = before_values[0] == before_values[1]
×
UNCOV
337
        after = None
×
UNCOV
338
        if after_values:
×
UNCOV
339
            after = after_values[0] == after_values[1]
×
UNCOV
340
        return PreprocEntityDelta(before=before, after=after)
×
341

342
    def visit_node_intrinsic_function_fn_if(
1✔
343
        self, node_intrinsic_function: NodeIntrinsicFunction
344
    ) -> PreprocEntityDelta:
345
        # TODO: check for KNOWN AFTER APPLY values for logical ids coming from intrinsic functions as arguments.
UNCOV
346
        arguments_delta = self.visit(node_intrinsic_function.arguments)
×
347

UNCOV
348
        def _compute_delta_for_if_statement(args: list[Any]) -> PreprocEntityDelta:
×
UNCOV
349
            condition_name = args[0]
×
UNCOV
350
            boolean_expression_delta = self._resolve_reference(logical_id=condition_name)
×
UNCOV
351
            return PreprocEntityDelta(
×
352
                before=args[1] if boolean_expression_delta.before else args[2],
353
                after=args[1] if boolean_expression_delta.after else args[2],
354
            )
355

356
        # TODO: add support for this being created or removed.
UNCOV
357
        before_outcome_delta = _compute_delta_for_if_statement(arguments_delta.before)
×
UNCOV
358
        before = before_outcome_delta.before
×
UNCOV
359
        after_outcome_delta = _compute_delta_for_if_statement(arguments_delta.after)
×
UNCOV
360
        after = after_outcome_delta.after
×
UNCOV
361
        return PreprocEntityDelta(before=before, after=after)
×
362

363
    def visit_node_intrinsic_function_fn_not(
1✔
364
        self, node_intrinsic_function: NodeIntrinsicFunction
365
    ) -> PreprocEntityDelta:
366
        # TODO: check for KNOWN AFTER APPLY values for logical ids coming from intrinsic functions as arguments.
367
        # TODO: add type checking/validation for result unit?
368
        arguments_delta = self.visit(node_intrinsic_function.arguments)
×
UNCOV
369
        before_condition = arguments_delta.before
×
UNCOV
370
        after_condition = arguments_delta.after
×
UNCOV
371
        if before_condition:
×
UNCOV
372
            before_condition_outcome = before_condition[0]
×
UNCOV
373
            before = not before_condition_outcome
×
374
        else:
UNCOV
375
            before = None
×
376

UNCOV
377
        if after_condition:
×
UNCOV
378
            after_condition_outcome = after_condition[0]
×
UNCOV
379
            after = not after_condition_outcome
×
380
        else:
UNCOV
381
            after = None
×
382
        # Implicit change type computation.
UNCOV
383
        return PreprocEntityDelta(before=before, after=after)
×
384

385
    def visit_node_intrinsic_function_fn_find_in_map(
1✔
386
        self, node_intrinsic_function: NodeIntrinsicFunction
387
    ) -> PreprocEntityDelta:
388
        # TODO: check for KNOWN AFTER APPLY values for logical ids coming from intrinsic functions as arguments.
389
        # TODO: add type checking/validation for result unit?
UNCOV
390
        arguments_delta = self.visit(node_intrinsic_function.arguments)
×
UNCOV
391
        before_arguments = arguments_delta.before
×
392
        after_arguments = arguments_delta.after
×
UNCOV
393
        if before_arguments:
×
UNCOV
394
            before_value_delta = self._resolve_mapping(*before_arguments)
×
UNCOV
395
            before = before_value_delta.before
×
396
        else:
397
            before = None
×
UNCOV
398
        if after_arguments:
×
UNCOV
399
            after_value_delta = self._resolve_mapping(*after_arguments)
×
UNCOV
400
            after = after_value_delta.after
×
401
        else:
UNCOV
402
            after = None
×
UNCOV
403
        return PreprocEntityDelta(before=before, after=after)
×
404

405
    def visit_node_mapping(self, node_mapping: NodeMapping) -> PreprocEntityDelta:
1✔
UNCOV
406
        bindings_delta = self.visit(node_mapping.bindings)
×
UNCOV
407
        return bindings_delta
×
408

409
    def visit_node_parameter(self, node_parameter: NodeParameter) -> PreprocEntityDelta:
1✔
UNCOV
410
        dynamic_value = node_parameter.dynamic_value
×
UNCOV
411
        dynamic_delta = self.visit(dynamic_value)
×
412

UNCOV
413
        default_value = node_parameter.default_value
×
UNCOV
414
        default_delta = self.visit(default_value)
×
415

UNCOV
416
        before = dynamic_delta.before or default_delta.before
×
UNCOV
417
        after = dynamic_delta.after or default_delta.after
×
418

UNCOV
419
        return PreprocEntityDelta(before=before, after=after)
×
420

421
    def visit_node_condition(self, node_condition: NodeCondition) -> PreprocEntityDelta:
1✔
UNCOV
422
        delta = self.visit(node_condition.body)
×
UNCOV
423
        return delta
×
424

425
    def _resource_physical_resource_id_from(
1✔
426
        self, logical_resource_id: str, resolved_resources: dict
427
    ) -> Optional[str]:
428
        # TODO: typing around resolved resources is needed and should be reflected here.
UNCOV
429
        resolved_resource = resolved_resources.get(logical_resource_id)
×
UNCOV
430
        if resolved_resource is None:
×
UNCOV
431
            return None
×
UNCOV
432
        physical_resource_id: Optional[str] = resolved_resource.get("PhysicalResourceId")
×
UNCOV
433
        if not isinstance(physical_resource_id, str):
×
UNCOV
434
            raise RuntimeError(f"No PhysicalResourceId found for resource '{logical_resource_id}'")
×
UNCOV
435
        return physical_resource_id
×
436

437
    def _before_resource_physical_id(self, resource_logical_id: str) -> Optional[str]:
1✔
438
        # TODO: typing around resolved resources is needed and should be reflected here.
UNCOV
439
        return self._resource_physical_resource_id_from(
×
440
            logical_resource_id=resource_logical_id,
441
            resolved_resources=self._before_resolved_resources,
442
        )
443

444
    def _after_resource_physical_id(self, resource_logical_id: str) -> Optional[str]:
1✔
UNCOV
445
        return self._before_resource_physical_id(resource_logical_id=resource_logical_id)
×
446

447
    def visit_node_intrinsic_function_ref(
1✔
448
        self, node_intrinsic_function: NodeIntrinsicFunction
449
    ) -> PreprocEntityDelta:
UNCOV
450
        arguments_delta = self.visit(node_intrinsic_function.arguments)
×
UNCOV
451
        before_logical_id = arguments_delta.before
×
UNCOV
452
        after_logical_id = arguments_delta.after
×
453

454
        # TODO: extend this to support references to other types.
UNCOV
455
        before = None
×
UNCOV
456
        if before_logical_id is not None:
×
UNCOV
457
            before_delta = self._resolve_reference(logical_id=before_logical_id)
×
UNCOV
458
            before = before_delta.before
×
UNCOV
459
            if isinstance(before, PreprocResource):
×
UNCOV
460
                before = before.physical_resource_id
×
461

UNCOV
462
        after = None
×
UNCOV
463
        if after_logical_id is not None:
×
UNCOV
464
            after_delta = self._resolve_reference(logical_id=after_logical_id)
×
UNCOV
465
            after = after_delta.after
×
UNCOV
466
            if isinstance(after, PreprocResource):
×
UNCOV
467
                after = after.physical_resource_id
×
468

UNCOV
469
        return PreprocEntityDelta(before=before, after=after)
×
470

471
    def visit_node_array(self, node_array: NodeArray) -> PreprocEntityDelta:
1✔
UNCOV
472
        before = list()
×
UNCOV
473
        after = list()
×
UNCOV
474
        for change_set_entity in node_array.array:
×
UNCOV
475
            delta: PreprocEntityDelta = self.visit(change_set_entity=change_set_entity)
×
UNCOV
476
            if delta.before:
×
UNCOV
477
                before.append(delta.before)
×
UNCOV
478
            if delta.after:
×
UNCOV
479
                after.append(delta.after)
×
UNCOV
480
        return PreprocEntityDelta(before=before, after=after)
×
481

482
    def visit_node_property(self, node_property: NodeProperty) -> PreprocEntityDelta:
1✔
UNCOV
483
        return self.visit(node_property.value)
×
484

485
    def visit_node_properties(
1✔
486
        self, node_properties: NodeProperties
487
    ) -> PreprocEntityDelta[PreprocProperties, PreprocProperties]:
UNCOV
488
        before_bindings: dict[str, Any] = dict()
×
UNCOV
489
        after_bindings: dict[str, Any] = dict()
×
UNCOV
490
        for node_property in node_properties.properties:
×
UNCOV
491
            delta = self.visit(node_property)
×
UNCOV
492
            property_name = node_property.name
×
UNCOV
493
            if node_property.change_type != ChangeType.CREATED:
×
UNCOV
494
                before_bindings[property_name] = delta.before
×
UNCOV
495
            if node_property.change_type != ChangeType.REMOVED:
×
UNCOV
496
                after_bindings[property_name] = delta.after
×
UNCOV
497
        before = PreprocProperties(properties=before_bindings)
×
UNCOV
498
        after = PreprocProperties(properties=after_bindings)
×
UNCOV
499
        return PreprocEntityDelta(before=before, after=after)
×
500

501
    def _resolve_resource_condition_reference(self, reference: TerminalValue) -> PreprocEntityDelta:
1✔
UNCOV
502
        reference_delta = self.visit(reference)
×
UNCOV
503
        before_reference = reference_delta.before
×
UNCOV
504
        after_reference = reference_delta.after
×
UNCOV
505
        condition_delta = self._resolve_reference_binding(
×
506
            before_logical_id=before_reference, after_logical_id=after_reference
507
        )
UNCOV
508
        before = condition_delta.before if not isinstance(before_reference, NothingType) else True
×
UNCOV
509
        after = condition_delta.after if not isinstance(after_reference, NothingType) else True
×
UNCOV
510
        return PreprocEntityDelta(before=before, after=after)
×
511

512
    def visit_node_resource(
1✔
513
        self, node_resource: NodeResource
514
    ) -> PreprocEntityDelta[PreprocResource, PreprocResource]:
UNCOV
515
        change_type = node_resource.change_type
×
UNCOV
516
        condition_before = None
×
UNCOV
517
        condition_after = None
×
UNCOV
518
        if node_resource.condition_reference is not None:
×
UNCOV
519
            condition_delta = self._resolve_resource_condition_reference(
×
520
                node_resource.condition_reference
521
            )
UNCOV
522
            condition_before = condition_delta.before
×
UNCOV
523
            condition_after = condition_delta.after
×
524

UNCOV
525
        type_delta = self.visit(node_resource.type_)
×
UNCOV
526
        properties_delta: PreprocEntityDelta[PreprocProperties, PreprocProperties] = self.visit(
×
527
            node_resource.properties
528
        )
529

UNCOV
530
        before = None
×
531
        after = None
×
UNCOV
532
        if change_type != ChangeType.CREATED and condition_before is None or condition_before:
×
UNCOV
533
            logical_resource_id = node_resource.name
×
534
            before_physical_resource_id = self._before_resource_physical_id(
×
535
                resource_logical_id=logical_resource_id
536
            )
537
            before = PreprocResource(
×
538
                logical_id=logical_resource_id,
539
                physical_resource_id=before_physical_resource_id,
540
                condition=condition_before,
541
                resource_type=type_delta.before,
542
                properties=properties_delta.before,
543
            )
UNCOV
544
        if change_type != ChangeType.REMOVED and condition_after is None or condition_after:
×
UNCOV
545
            logical_resource_id = node_resource.name
×
UNCOV
546
            after_physical_resource_id = self._after_resource_physical_id(
×
547
                resource_logical_id=logical_resource_id
548
            )
UNCOV
549
            after = PreprocResource(
×
550
                logical_id=logical_resource_id,
551
                physical_resource_id=after_physical_resource_id,
552
                condition=condition_after,
553
                resource_type=type_delta.after,
554
                properties=properties_delta.after,
555
            )
UNCOV
556
        return PreprocEntityDelta(before=before, after=after)
×
557

558
    def visit_node_output(
1✔
559
        self, node_output: NodeOutput
560
    ) -> PreprocEntityDelta[PreprocOutput, PreprocOutput]:
UNCOV
561
        change_type = node_output.change_type
×
UNCOV
562
        value_delta = self.visit(node_output.value)
×
563

UNCOV
564
        condition_delta = None
×
UNCOV
565
        if node_output.condition_reference is not None:
×
UNCOV
566
            condition_delta = self._resolve_resource_condition_reference(
×
567
                node_output.condition_reference
568
            )
UNCOV
569
            condition_before = condition_delta.before
×
UNCOV
570
            condition_after = condition_delta.after
×
UNCOV
571
            if not condition_before and condition_after:
×
UNCOV
572
                change_type = ChangeType.CREATED
×
UNCOV
573
            elif condition_before and not condition_after:
×
UNCOV
574
                change_type = ChangeType.REMOVED
×
575

UNCOV
576
        export_delta = None
×
UNCOV
577
        if node_output.export is not None:
×
UNCOV
578
            export_delta = self.visit(node_output.export)
×
579

UNCOV
580
        before: Optional[PreprocOutput] = None
×
UNCOV
581
        if change_type != ChangeType.CREATED:
×
UNCOV
582
            before = PreprocOutput(
×
583
                name=node_output.name,
584
                value=value_delta.before,
585
                export=export_delta.before if export_delta else None,
586
                condition=condition_delta.before if condition_delta else None,
587
            )
UNCOV
588
        after: Optional[PreprocOutput] = None
×
UNCOV
589
        if change_type != ChangeType.REMOVED:
×
UNCOV
590
            after = PreprocOutput(
×
591
                name=node_output.name,
592
                value=value_delta.after,
593
                export=export_delta.after if export_delta else None,
594
                condition=condition_delta.after if condition_delta else None,
595
            )
UNCOV
596
        return PreprocEntityDelta(before=before, after=after)
×
597

598
    def visit_node_outputs(
1✔
599
        self, node_outputs: NodeOutputs
600
    ) -> PreprocEntityDelta[list[PreprocOutput], list[PreprocOutput]]:
UNCOV
601
        before: list[PreprocOutput] = list()
×
UNCOV
602
        after: list[PreprocOutput] = list()
×
UNCOV
603
        for node_output in node_outputs.outputs:
×
UNCOV
604
            output_delta: PreprocEntityDelta[PreprocOutput, PreprocOutput] = self.visit(node_output)
×
UNCOV
605
            output_before = output_delta.before
×
UNCOV
606
            output_after = output_delta.after
×
UNCOV
607
            if output_before:
×
UNCOV
608
                before.append(output_before)
×
UNCOV
609
            if output_after:
×
UNCOV
610
                after.append(output_after)
×
UNCOV
611
        return PreprocEntityDelta(before=before, after=after)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc