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

localstack / localstack / 17144436094

21 Aug 2025 11:28PM UTC coverage: 86.843% (-0.03%) from 86.876%
17144436094

push

github

web-flow
APIGW: internalize DeleteIntegrationResponse (#13046)

40 of 45 new or added lines in 1 file covered. (88.89%)

235 existing lines in 11 files now uncovered.

67068 of 77229 relevant lines covered (86.84%)

0.87 hits per line

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

91.43
/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py
1
import copy
1✔
2
import json
1✔
3
import logging
1✔
4
import os
1✔
5
import re
1✔
6
from typing import Any, Final, TypedDict
1✔
7

8
import boto3
1✔
9
import jsonpath_ng
1✔
10
from botocore.exceptions import ClientError
1✔
11
from samtranslator.translator.transform import transform as transform_sam
1✔
12

13
from localstack.aws.connect import connect_to
1✔
14
from localstack.services.cloudformation.engine.parameters import StackParameter
1✔
15
from localstack.services.cloudformation.engine.policy_loader import create_policy_loader
1✔
16
from localstack.services.cloudformation.engine.template_preparer import parse_template
1✔
17
from localstack.services.cloudformation.engine.transformers import (
1✔
18
    FailedTransformationException,
19
    ResolveRefsRecursivelyContext,
20
    apply_language_extensions_transform,
21
)
22
from localstack.services.cloudformation.engine.v2.change_set_model import (
1✔
23
    ChangeType,
24
    FnTransform,
25
    Maybe,
26
    NodeForEach,
27
    NodeGlobalTransform,
28
    NodeIntrinsicFunction,
29
    NodeIntrinsicFunctionFnTransform,
30
    NodeProperties,
31
    NodeResource,
32
    NodeResources,
33
    NodeTransform,
34
    Nothing,
35
    Scope,
36
    is_nothing,
37
)
38
from localstack.services.cloudformation.engine.v2.change_set_model_preproc import (
1✔
39
    ChangeSetModelPreproc,
40
    PreprocEntityDelta,
41
    PreprocProperties,
42
)
43
from localstack.services.cloudformation.engine.validations import ValidationError
1✔
44
from localstack.services.cloudformation.stores import get_cloudformation_store
1✔
45
from localstack.services.cloudformation.v2.entities import ChangeSet
1✔
46
from localstack.services.cloudformation.v2.types import EngineParameter, engine_parameter_value
1✔
47
from localstack.utils import testutil
1✔
48
from localstack.utils.strings import long_uid
1✔
49

50
LOG = logging.getLogger(__name__)
1✔
51

52
SERVERLESS_TRANSFORM = "AWS::Serverless-2016-10-31"
1✔
53
EXTENSIONS_TRANSFORM = "AWS::LanguageExtensions"
1✔
54
SECRETSMANAGER_TRANSFORM = "AWS::SecretsManager-2020-07-23"
1✔
55
INCLUDE_TRANSFORM = "AWS::Include"
1✔
56

57
_SCOPE_TRANSFORM_TEMPLATE_OUTCOME: Final[Scope] = Scope("TRANSFORM_TEMPLATE_OUTCOME")
1✔
58

59

60
def engine_parameters_to_stack_parameters(
1✔
61
    engine_parameters: dict[str, EngineParameter],
62
) -> dict[str, StackParameter]:
63
    out = {}
1✔
64
    for name, engine_param in engine_parameters.items():
1✔
65
        out[name] = StackParameter(
1✔
66
            ParameterKey=name,
67
            ParameterValue=engine_parameter_value(engine_param),
68
            ResolvedValue=engine_param.get("resolved_value"),
69
            ParameterType=engine_param["type_"],
70
        )
71
    return out
1✔
72

73

74
# TODO: evaluate the use of subtypes to represent and validate types of transforms
75
class GlobalTransform:
1✔
76
    name: str
1✔
77
    parameters: Maybe[dict]
1✔
78

79
    def __init__(self, name: str, parameters: Maybe[dict]):
1✔
80
        self.name = name
1✔
81
        self.parameters = parameters
1✔
82

83

84
class TransformPreprocParameter(TypedDict):
1✔
85
    # TODO: expand
86
    ParameterKey: str
1✔
87
    ParameterValue: Any
1✔
88
    ParameterType: str | None
1✔
89

90

91
class ChangeSetModelTransform(ChangeSetModelPreproc):
1✔
92
    _before_parameters: Final[dict[str, EngineParameter] | None]
1✔
93
    _after_parameters: Final[dict[str, EngineParameter] | None]
1✔
94
    _before_template: Final[Maybe[dict]]
1✔
95
    _after_template: Final[Maybe[dict]]
1✔
96

97
    def __init__(
1✔
98
        self,
99
        change_set: ChangeSet,
100
        before_parameters: dict,
101
        after_parameters: dict,
102
        before_template: dict | None,
103
        after_template: dict | None,
104
    ):
105
        super().__init__(change_set=change_set)
1✔
106
        self._before_parameters = before_parameters
1✔
107
        self._after_parameters = after_parameters
1✔
108
        self._before_template = before_template or Nothing
1✔
109
        self._after_template = after_template or Nothing
1✔
110

111
    def transform(self) -> tuple[dict, dict]:
1✔
112
        self._setup_runtime_cache()
1✔
113
        self._execute_local_transforms()
1✔
114
        transformed_before_template, transformed_after_template = self._execute_global_transforms()
1✔
115
        self._save_runtime_cache()
1✔
116

117
        return transformed_before_template, transformed_after_template
1✔
118

119
    # Ported from v1:
120
    @staticmethod
1✔
121
    def _apply_global_serverless_transformation(
1✔
122
        region_name: str, template: dict, parameters: dict
123
    ) -> dict:
124
        """only returns string when parsing SAM template, otherwise None"""
125
        # TODO: we might also want to override the access key ID to account ID
126
        region_before = os.environ.get("AWS_DEFAULT_REGION")
1✔
127
        if boto3.session.Session().region_name is None:
1✔
128
            os.environ["AWS_DEFAULT_REGION"] = region_name
1✔
129
        loader = create_policy_loader()
1✔
130
        # The following transformation function can carry out in-place changes ensure this cannot occur.
131
        template = copy.deepcopy(template)
1✔
132
        parameters = copy.deepcopy(parameters)
1✔
133
        try:
1✔
134
            transformed = transform_sam(template, parameters, loader)
1✔
135
            return transformed
1✔
136
        except Exception as e:
×
UNCOV
137
            raise FailedTransformationException(transformation=SERVERLESS_TRANSFORM, message=str(e))
×
138
        finally:
139
            # Note: we need to fix boto3 region, otherwise AWS SAM transformer fails
140
            os.environ.pop("AWS_DEFAULT_REGION", None)
1✔
141
            if region_before is not None:
1✔
UNCOV
142
                os.environ["AWS_DEFAULT_REGION"] = region_before
×
143

144
    def _compute_include_transform(self, parameters: dict, fragment: dict) -> dict:
1✔
145
        location = parameters.get("Location")
1✔
146
        if not location or not location.startswith("s3://"):
1✔
UNCOV
147
            raise FailedTransformationException(
×
148
                transformation=INCLUDE_TRANSFORM,
149
                message="Unexpected Location parameter for AWS::Include transformer: %s" % location,
150
            )
151

152
        s3_client = connect_to(
1✔
153
            aws_access_key_id=self._change_set.account_id, region_name=self._change_set.region_name
154
        ).s3
155
        bucket, _, path = location.removeprefix("s3://").partition("/")
1✔
156
        try:
1✔
157
            content = testutil.download_s3_object(s3_client, bucket, path)
1✔
158
        except ClientError:
×
UNCOV
159
            raise FailedTransformationException(
×
160
                transformation=INCLUDE_TRANSFORM,
161
                message="Error downloading S3 object '%s/%s'" % (bucket, path),
162
            )
163
        try:
1✔
164
            template_to_include = parse_template(content)
1✔
165
        except Exception as e:
×
UNCOV
166
            raise FailedTransformationException(transformation=INCLUDE_TRANSFORM, message=str(e))
×
167

168
        return {**fragment, **template_to_include}
1✔
169

170
    def _apply_global_transform(
1✔
171
        self,
172
        global_transform: GlobalTransform,
173
        template: dict,
174
        parameters: dict[str, EngineParameter],
175
    ) -> dict:
176
        transform_name = global_transform.name
1✔
177
        if transform_name == EXTENSIONS_TRANSFORM:
1✔
178
            resources = template["Resources"]
1✔
179
            mappings = template.get("Mappings", {})
1✔
180
            conditions = template.get("Conditions", {})
1✔
181

182
            resolve_context = ResolveRefsRecursivelyContext(
1✔
183
                self._change_set.account_id,
184
                self._change_set.region_name,
185
                self._change_set.stack.stack_name,
186
                resources,
187
                mappings,
188
                conditions,
189
                parameters=engine_parameters_to_stack_parameters(parameters),
190
            )
191
            transformed_template = apply_language_extensions_transform(template, resolve_context)
1✔
192
        elif transform_name == SERVERLESS_TRANSFORM:
1✔
193
            # serverless transform just requires the key/value pairs
194
            serverless_parameters = {}
1✔
195
            for name, param in parameters.items():
1✔
196
                serverless_parameters[name] = param.get("resolved_value") or engine_parameter_value(
1✔
197
                    param
198
                )
199
            transformed_template = self._apply_global_serverless_transformation(
1✔
200
                region_name=self._change_set.region_name,
201
                template=template,
202
                parameters=serverless_parameters,
203
            )
204
        elif transform_name == SECRETSMANAGER_TRANSFORM:
1✔
205
            # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/transform-aws-secretsmanager.html
206
            LOG.warning("%s is not yet supported. Ignoring.", SECRETSMANAGER_TRANSFORM)
×
UNCOV
207
            transformed_template = template
×
208
        elif transform_name == INCLUDE_TRANSFORM:
1✔
209
            transformed_template = self._compute_include_transform(
1✔
210
                parameters=global_transform.parameters,
211
                fragment=template,
212
            )
213
        else:
214
            transformed_template = self._invoke_macro(
1✔
215
                name=global_transform.name,
216
                parameters=global_transform.parameters
217
                if not is_nothing(global_transform.parameters)
218
                else {},
219
                fragment=template,
220
                allow_string=False,
221
            )
222
        return transformed_template
1✔
223

224
    def _execute_local_transforms(self):
1✔
225
        node_template = self._change_set.update_model.node_template
1✔
226
        self.visit_node_resources(node_template.resources)
1✔
227

228
    def _execute_global_transforms(self) -> tuple[dict, dict]:
1✔
229
        node_template = self._change_set.update_model.node_template
1✔
230

231
        transform_delta: PreprocEntityDelta[list[GlobalTransform], list[GlobalTransform]] = (
1✔
232
            self.visit_node_transform(node_template.transform)
233
        )
234
        transform_before: Maybe[list[GlobalTransform]] = transform_delta.before
1✔
235
        transform_after: Maybe[list[GlobalTransform]] = transform_delta.after
1✔
236

237
        transformed_before_template = self._before_template
1✔
238
        if transform_before and not is_nothing(self._before_template):
1✔
239
            if _SCOPE_TRANSFORM_TEMPLATE_OUTCOME in self._before_cache:
1✔
240
                transformed_before_template = self._before_cache[_SCOPE_TRANSFORM_TEMPLATE_OUTCOME]
1✔
241
            else:
242
                for before_global_transform in transform_before:
1✔
243
                    if not is_nothing(before_global_transform.name):
1✔
UNCOV
244
                        transformed_before_template = self._apply_global_transform(
×
245
                            global_transform=before_global_transform,
246
                            parameters=self._before_parameters,
247
                            template=transformed_before_template,
248
                        )
249

250
                # Macro transformations won't remove the transform from the template
251
                if "Transform" in transformed_before_template:
1✔
UNCOV
252
                    transformed_before_template.pop("Transform")
×
253
                self._before_cache[_SCOPE_TRANSFORM_TEMPLATE_OUTCOME] = transformed_before_template
1✔
254

255
        transformed_after_template = self._after_template
1✔
256
        if transform_after and not is_nothing(self._after_template):
1✔
257
            transformed_after_template = self._after_template
1✔
258
            for after_global_transform in transform_after:
1✔
259
                if not is_nothing(after_global_transform.name):
1✔
260
                    transformed_after_template = self._apply_global_transform(
1✔
261
                        global_transform=after_global_transform,
262
                        parameters=self._after_parameters,
263
                        template=transformed_after_template,
264
                    )
265
            # Macro transformations won't remove the transform from the template
266
            if "Transform" in transformed_after_template:
1✔
267
                transformed_after_template.pop("Transform")
1✔
268
            self._after_cache[_SCOPE_TRANSFORM_TEMPLATE_OUTCOME] = transformed_after_template
1✔
269

270
        return transformed_before_template, transformed_after_template
1✔
271

272
    def visit_node_global_transform(
1✔
273
        self, node_global_transform: NodeGlobalTransform
274
    ) -> PreprocEntityDelta[GlobalTransform, GlobalTransform]:
275
        change_type = node_global_transform.change_type
1✔
276

277
        name_delta = self.visit(node_global_transform.name)
1✔
278
        parameters_delta = self.visit(node_global_transform.parameters)
1✔
279

280
        before = Nothing
1✔
281
        if change_type != ChangeType.CREATED:
1✔
282
            before = GlobalTransform(name=name_delta.before, parameters=parameters_delta.before)
1✔
283
        after = Nothing
1✔
284
        if change_type != ChangeType.REMOVED:
1✔
285
            after = GlobalTransform(name=name_delta.after, parameters=parameters_delta.after)
1✔
286
        return PreprocEntityDelta(before=before, after=after)
1✔
287

288
    def visit_node_transform(
1✔
289
        self, node_transform: NodeTransform
290
    ) -> PreprocEntityDelta[list[GlobalTransform], list[GlobalTransform]]:
291
        change_type = node_transform.change_type
1✔
292
        before = [] if change_type != ChangeType.CREATED else Nothing
1✔
293
        after = [] if change_type != ChangeType.REMOVED else Nothing
1✔
294
        for change_set_entity in node_transform.global_transforms:
1✔
295
            if not isinstance(change_set_entity.name.value, str):
1✔
296
                raise ValidationError("Key Name of transform definition must be a string.")
1✔
297

298
            delta: PreprocEntityDelta[GlobalTransform, GlobalTransform] = self.visit(
1✔
299
                change_set_entity=change_set_entity
300
            )
301
            delta_before = delta.before
1✔
302
            delta_after = delta.after
1✔
303
            if not is_nothing(before) and not is_nothing(delta_before):
1✔
304
                before.append(delta_before)
1✔
305
            if not is_nothing(after) and not is_nothing(delta_after):
1✔
306
                after.append(delta_after)
1✔
307
        return PreprocEntityDelta(before=before, after=after)
1✔
308

309
    def _compute_fn_transform(
1✔
310
        self, macro_definition: Any, siblings: Any, allow_string: False
311
    ) -> Any:
312
        def _normalize_transform(obj):
1✔
313
            transforms = []
1✔
314

315
            if isinstance(obj, str):
1✔
316
                transforms.append({"Name": obj, "Parameters": {}})
1✔
317

318
            if isinstance(obj, dict):
1✔
319
                transforms.append(obj)
1✔
320

321
            if isinstance(obj, list):
1✔
322
                for v in obj:
1✔
323
                    if isinstance(v, str):
1✔
UNCOV
324
                        transforms.append({"Name": v, "Parameters": {}})
×
325

326
                    if isinstance(v, dict):
1✔
327
                        if not v.get("Parameters"):
1✔
UNCOV
328
                            v["Parameters"] = {}
×
329
                        transforms.append(v)
1✔
330

331
            return transforms
1✔
332

333
        normalized_transforms = _normalize_transform(macro_definition)
1✔
334
        transform_output = copy.deepcopy(siblings)
1✔
335
        for transform in normalized_transforms:
1✔
336
            transform_name = transform["Name"]
1✔
337
            if transform_name == INCLUDE_TRANSFORM:
1✔
338
                transform_output = self._compute_include_transform(
1✔
339
                    parameters=transform["Parameters"], fragment=transform_output
340
                )
341
            else:
342
                transform_output: dict | str = self._invoke_macro(
1✔
343
                    fragment=transform_output,
344
                    name=transform["Name"],
345
                    parameters=transform.get("Parameters", {}),
346
                    allow_string=allow_string,
347
                )
348

349
        if isinstance(transform_output, dict) and FnTransform in transform_output:
1✔
350
            transform_output.pop(FnTransform)
1✔
351

352
        return transform_output
1✔
353

354
    def _replace_at_jsonpath(self, template: dict, path: str, result: Any):
1✔
355
        pattern = jsonpath_ng.parse(path)
1✔
356
        result_template = pattern.update(template, result)
1✔
357

358
        return result_template
1✔
359

360
    def visit_node_for_each(self, node_foreach: NodeForEach) -> PreprocEntityDelta:
1✔
361
        return PreprocEntityDelta()
1✔
362

363
    def visit_node_intrinsic_function_fn_transform(
1✔
364
        self, node_intrinsic_function: NodeIntrinsicFunctionFnTransform
365
    ) -> PreprocEntityDelta:
366
        arguments_delta = self.visit(node_intrinsic_function.arguments)
1✔
367
        parent_json_path = node_intrinsic_function.scope.parent.jsonpath
1✔
368

369
        # Only when a FnTransform is used as Property value the macro function is allowed to return a str
370
        property_value_regex = r"\.(Properties)"
1✔
371
        allow_string = False
1✔
372
        if re.search(property_value_regex, parent_json_path):
1✔
373
            allow_string = True
1✔
374

375
        if not is_nothing(arguments_delta.before):
1✔
UNCOV
376
            before = self._compute_fn_transform(
×
377
                arguments_delta.before,
378
                node_intrinsic_function.before_siblings,
379
                allow_string=allow_string,
380
            )
UNCOV
381
            updated_before_template = self._replace_at_jsonpath(
×
382
                self._before_template, parent_json_path, before
383
            )
UNCOV
384
            self._after_cache[_SCOPE_TRANSFORM_TEMPLATE_OUTCOME] = updated_before_template
×
385
        else:
386
            before = Nothing
1✔
387

388
        if not is_nothing(arguments_delta.after):
1✔
389
            after = self._compute_fn_transform(
1✔
390
                arguments_delta.after,
391
                node_intrinsic_function.after_siblings,
392
                allow_string=allow_string,
393
            )
394
            updated_after_template = self._replace_at_jsonpath(
1✔
395
                self._after_template, parent_json_path, after
396
            )
397
            self._after_cache[_SCOPE_TRANSFORM_TEMPLATE_OUTCOME] = updated_after_template
1✔
398
        else:
UNCOV
399
            after = Nothing
×
400

401
        self._save_runtime_cache()
1✔
402
        return PreprocEntityDelta(before=before, after=after)
1✔
403

404
    def visit_node_properties(
1✔
405
        self, node_properties: NodeProperties
406
    ) -> PreprocEntityDelta[PreprocProperties, PreprocProperties]:
407
        if not is_nothing(node_properties.fn_transform):
1✔
408
            self.visit_node_intrinsic_function_fn_transform(node_properties.fn_transform)
1✔
409

410
        return super().visit_node_properties(node_properties=node_properties)
1✔
411

412
    def visit_node_resource(self, node_resource: NodeResource) -> PreprocEntityDelta:
1✔
413
        if not is_nothing(node_resource.fn_transform):
1✔
414
            self.visit_node_intrinsic_function_fn_transform(
1✔
415
                node_intrinsic_function=node_resource.fn_transform
416
            )
417

418
        try:
1✔
419
            if delta := super().visit_node_resource(node_resource):
1✔
420
                return delta
1✔
UNCOV
421
            return super().visit_node_properties(node_resource.properties)
×
422
        except RuntimeError:
1✔
UNCOV
423
            return super().visit_node_properties(node_resource.properties)
×
424

425
    def visit_node_resources(self, node_resources: NodeResources) -> PreprocEntityDelta:
1✔
426
        if not is_nothing(node_resources.fn_transform):
1✔
427
            self.visit_node_intrinsic_function_fn_transform(
1✔
428
                node_intrinsic_function=node_resources.fn_transform
429
            )
430

431
        return super().visit_node_resources(node_resources=node_resources)
1✔
432

433
    def _invoke_macro(self, name: str, parameters: dict, fragment: dict, allow_string=False):
1✔
434
        account_id = self._change_set.account_id
1✔
435
        region_name = self._change_set.region_name
1✔
436
        macro_definition = get_cloudformation_store(
1✔
437
            account_id=account_id, region_name=region_name
438
        ).macros.get(name)
439

440
        if not macro_definition:
1✔
UNCOV
441
            raise FailedTransformationException(name, f"Transformation {name} is not supported.")
×
442

443
        simplified_parameters = {}
1✔
444
        if resolved_parameters := self._change_set.resolved_parameters:
1✔
445
            for key, resolved_parameter in resolved_parameters.items():
1✔
446
                final_value = engine_parameter_value(resolved_parameter)
1✔
447
                simplified_parameters[key] = (
1✔
448
                    final_value.split(",")
449
                    if resolved_parameter["type_"] == "CommaDelimitedList"
450
                    else final_value
451
                )
452

453
        transformation_id = f"{account_id}::{name}"
1✔
454
        event = {
1✔
455
            "region": region_name,
456
            "accountId": account_id,
457
            "fragment": fragment,
458
            "transformId": transformation_id,
459
            "params": parameters,
460
            "requestId": long_uid(),
461
            "templateParameterValues": simplified_parameters,
462
        }
463

464
        client = connect_to(aws_access_key_id=account_id, region_name=region_name).lambda_
1✔
465
        try:
1✔
466
            invocation = client.invoke(
1✔
467
                FunctionName=macro_definition["FunctionName"], Payload=json.dumps(event)
468
            )
UNCOV
469
        except ClientError:
×
UNCOV
470
            LOG.error(
×
471
                "client error executing lambda function '%s' with payload '%s'",
472
                macro_definition["FunctionName"],
473
                json.dumps(event),
474
            )
UNCOV
475
            raise
×
476
        if invocation.get("StatusCode") != 200 or invocation.get("FunctionError") == "Unhandled":
1✔
477
            raise FailedTransformationException(
1✔
478
                transformation=name,
479
                message=f"Received malformed response from transform {transformation_id}. Rollback requested by user.",
480
            )
481
        result = json.loads(invocation["Payload"].read())
1✔
482

483
        if result.get("status") != "success":
1✔
484
            error_message = result.get("errorMessage")
1✔
485
            message = (
1✔
486
                f"Transform {transformation_id} failed with: {error_message}. Rollback requested by user."
487
                if error_message
488
                else f"Transform {transformation_id} failed without an error message.. Rollback requested by user."
489
            )
490
            raise FailedTransformationException(transformation=name, message=message)
1✔
491

492
        if not isinstance(result.get("fragment"), dict) and not allow_string:
1✔
493
            raise FailedTransformationException(
1✔
494
                transformation=name,
495
                message="Template format error: unsupported structure.. Rollback requested by user.",
496
            )
497

498
        return result.get("fragment")
1✔
499

500
    def visit_node_intrinsic_function_fn_get_att(
1✔
501
        self, node_intrinsic_function: NodeIntrinsicFunction
502
    ) -> PreprocEntityDelta:
503
        return self.visit(node_intrinsic_function.arguments)
1✔
504

505
    def visit_node_intrinsic_function_fn_sub(
1✔
506
        self, node_intrinsic_function: NodeIntrinsicFunction
507
    ) -> PreprocEntityDelta:
508
        try:
1✔
509
            # If an argument is a Parameter it should be resolved, any other case, ignore it
510
            return super().visit_node_intrinsic_function_fn_sub(node_intrinsic_function)
1✔
511
        except RuntimeError:
1✔
512
            return self.visit(node_intrinsic_function.arguments)
1✔
513

514
    def visit_node_intrinsic_function_fn_split(
1✔
515
        self, node_intrinsic_function: NodeIntrinsicFunction
516
    ) -> PreprocEntityDelta:
517
        try:
1✔
518
            # If an argument is a Parameter it should be resolved, any other case, ignore it
519
            return super().visit_node_intrinsic_function_fn_split(node_intrinsic_function)
1✔
520
        except RuntimeError:
1✔
521
            return self.visit(node_intrinsic_function.arguments)
1✔
522

523
    def visit_node_intrinsic_function_fn_select(
1✔
524
        self, node_intrinsic_function: NodeIntrinsicFunction
525
    ) -> PreprocEntityDelta:
526
        try:
1✔
527
            # If an argument is a Parameter it should be resolved, any other case, ignore it
528
            return super().visit_node_intrinsic_function_fn_select(node_intrinsic_function)
1✔
529
        except RuntimeError:
1✔
530
            return self.visit(node_intrinsic_function.arguments)
1✔
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