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

aas-core-works / aas-core-codegen / 23304045215

19 Mar 2026 03:58PM UTC coverage: 83.702% (+0.03%) from 83.675%
23304045215

push

github

web-flow
Unify tests for main (#598)

We group all the tests for the main method into a since test file. This
makes it easier to run only parts of the test suite, and also indicates
more clearly how to include tests for novel targets and cases.

7 of 7 new or added lines in 1 file covered. (100.0%)

86 existing lines in 8 files now uncovered.

30882 of 36895 relevant lines covered (83.7%)

3.35 hits per line

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

75.97
/aas_core_codegen/intermediate/pattern_verification.py
1
"""
2
Understand the verification functions.
3

4
The classes
5
:py:class:`aas_core_codegen.intermediate._types.ImplementationSpecificVerification`
6
and :py:class:`aas_core_codegen.intermediate._types.PatternVerification` had to be
7
defined in :py:mod:`aas_core_codegen.intermediate._types` to avoid circular imports.
8
"""
9
import collections
4✔
10
import re
4✔
11
from typing import Optional, Tuple, List, MutableMapping, Mapping
4✔
12

13
from icontract import require, ensure
4✔
14

15
from aas_core_codegen import parse
4✔
16
from aas_core_codegen.common import Error, Identifier, assert_never
4✔
17
from aas_core_codegen.parse import tree as parse_tree
4✔
18

19

20
def _check_support(
4✔
21
    node: parse_tree.Node, argument: Identifier
22
) -> Optional[List[Error]]:
23
    """
24
    Check that we understand the ``node`` in the pattern-matching function.
25

26
    The ``argument`` specifies the argument to the verification function, which should
27
    not be used.
28
    """
29
    # NOTE (mristin, 2021-12-19):
30
    # This run-time check is necessary as we already burned our fingers with it.
31
    assert isinstance(node, parse_tree.Node), f"{node=}"
4✔
32

33
    if isinstance(node, parse_tree.Constant):
4✔
34
        if isinstance(node.value, str):
4✔
35
            return None
4✔
36
        else:
37
            return [
×
38
                Error(
39
                    node.original_node,
40
                    f"We did not implement the support for non-string constants "
41
                    f"in pattern matching: {parse_tree.dump(node)}.\n"
42
                    f"\n"
43
                    f"Please notify the developers if you need this.",
44
                )
45
            ]
46

47
    elif isinstance(node, parse_tree.JoinedStr):
4✔
48
        errors = []  # type: List[Error]
4✔
49

50
        for value in node.values:
4✔
51
            # noinspection PyTypeChecker
52
            if isinstance(value, str):
4✔
53
                continue
4✔
54
            elif isinstance(value, parse_tree.FormattedValue):
4✔
55
                underlying_errors = _check_support(node=value.value, argument=argument)
4✔
56
                if underlying_errors is not None:
4✔
57
                    errors.extend(underlying_errors)
×
58
            else:
59
                assert_never(value)
×
60

61
        if len(errors) == 0:
4✔
62
            return None
4✔
63

64
        return errors
×
65

66
    elif isinstance(node, parse_tree.Name):
4✔
67
        if node.identifier == argument:
4✔
68
            return [
×
69
                Error(
70
                    node.original_node,
71
                    f"The verification arguments, {argument!r}, is not expected "
72
                    f"to be accessed neither for reading nor for writing.",
73
                )
74
            ]
75
        else:
76
            return None
4✔
77

78
    elif isinstance(node, parse_tree.Assignment):
4✔
79
        if not isinstance(node.target, parse_tree.Name):
4✔
80
            return [
×
81
                Error(
82
                    node.target.original_node,
83
                    f"We currently support only assignments to simple variables, "
84
                    f"but got: {parse_tree.dump(node.target)}.\n"
85
                    f"\n"
86
                    f"Please notify the developers if you need this.",
87
                )
88
            ]
89

90
        return _check_support(node=node.value, argument=argument)
4✔
91

92
    else:
93
        return [
×
94
            Error(
95
                node.original_node,
96
                f"We did not implement the support for this construct "
97
                f"in pattern matching: {parse_tree.dump(node)}.\n"
98
                f"\n"
99
                f"Please notify the developers if you need this.",
100
            )
101
        ]
102

103

104
@ensure(lambda result: (result[0] is not None) ^ (result[1] is not None))
4✔
105
def _evaluate(
4✔
106
    expr: parse_tree.Expression, state: Mapping[Identifier, str]
107
) -> Tuple[Optional[str], Optional[Error]]:
108
    """Evaluate the expression to a string constant."""
109
    if isinstance(expr, parse_tree.Constant):
4✔
110
        assert isinstance(expr.value, str)
4✔
111
        return expr.value, None
4✔
112

113
    elif isinstance(expr, parse_tree.Name):
4✔
114
        value = state.get(expr.identifier, None)
4✔
115
        if value is None:
4✔
116
            return (
×
117
                None,
118
                Error(
119
                    expr.original_node,
120
                    f"The value of variable {expr.identifier} has not been assigned "
121
                    f"before",
122
                ),
123
            )
124

125
        return value, None
4✔
126

127
    elif isinstance(expr, parse_tree.JoinedStr):
4✔
128
        parts = []  # type: List[str]
4✔
129
        for joined_str_value in expr.values:
4✔
130
            if isinstance(joined_str_value, str):
4✔
131
                parts.append(joined_str_value)
4✔
132

133
            elif isinstance(joined_str_value, parse_tree.FormattedValue):
4✔
134

135
                part, error = _evaluate(joined_str_value.value, state=state)
4✔
136
                if error is not None:
4✔
137
                    return None, error
×
138

139
                assert part is not None
4✔
140
                parts.append(part)
4✔
141
            else:
142
                assert_never(joined_str_value)
×
143

144
        return "".join(parts), None
4✔
145

146
    else:
147
        raise AssertionError(f"Unexpected expression: {parse_tree.dump(expr)}")
×
148

149

150
# fmt: off
151
@require(
4✔
152
    lambda parsed:
153
    parsed.verification,
154
    "Understand only verification functions"
155
)
156
@ensure(
4✔
157
    lambda result:
158
    not (result[2] is None)
159
    or (
160
        (result[0] is not None) ^ (result[1] is not None)
161
    ),
162
    "Valid match and cause if no error"
163
)
164
@ensure(
4✔
165
    lambda result:
166
    not (result[2] is not None)
167
    or (result[0] is None and result[1] is None),
168
    "No match and cause if error"
169
)
170
# fmt: on
171
def try_to_understand(
4✔
172
    parsed: parse.UnderstoodMethod,
173
) -> Tuple[Optional[str], Optional[Error], Optional[Error]]:
174
    """
175
    Try to understand the given verification function as a pattern matching function.
176

177
    :param parsed: Verification function as parsed in the parsing phase
178
    :return: tuple of (pattern, reason for not matching, error)
179

180
    We distinguish between two causes why the method could not be understood. The first
181
    cause, returned as the middle value in the tuple, indicates why the method could
182
    not be matched, but this non-matching is actually expected.
183

184
    The second error, the last value in the return tuple, is an unexpected error. For
185
    example, if everything looks like a pattern matching function, but there are flags
186
    in the call to ``match(...)``.
187
    """
188
    # Understand only functions that take a single string argument
189
    if not (
4✔
190
        len(parsed.arguments) == 1
191
        and isinstance(parsed.arguments[0].type_annotation, parse.AtomicTypeAnnotation)
192
        and parsed.arguments[0].type_annotation.identifier == "str"
193
    ):
194
        return None, Error(parsed.node, "Expected a single ``str`` argument"), None
4✔
195

196
    # We need to return something,
197
    if not (
4✔
198
        parsed.returns is not None
199
        and isinstance(parsed.returns, parse.AtomicTypeAnnotation)
200
        and parsed.returns.identifier == "bool"
201
    ):
UNCOV
202
        return None, Error(parsed.node, "Expected a ``bool`` return value"), None
×
203

204
    if len(parsed.body) == 0:
4✔
205
        return None, Error(parsed.node, "Unexpected empty body"), None
×
206

207
    if not isinstance(parsed.body[-1], parse_tree.Return):
4✔
208
        return (
×
209
            None,
210
            Error(parsed.body[-1].original_node, "Last statement not a a return"),
211
            None,
212
        )
213

214
    return_node = parsed.body[-1]
4✔
215
    assert isinstance(return_node, parse_tree.Return)
4✔
216
    # noinspection PyUnresolvedReferences
217
    if return_node.value is None:
4✔
218
        return (
×
219
            None,
220
            Error(parsed.body[-1].original_node, "Expected to return a value"),
221
            None,
222
        )
223

224
    # noinspection PyUnresolvedReferences
225
    if return_node.value is None:
4✔
226
        return (
×
227
            None,
228
            Error(parsed.body[-1].original_node, "Expected to return a value"),
229
            None,
230
        )
231

232
    if (
4✔
233
        isinstance(return_node.value, parse_tree.FunctionCall)
234
        and return_node.value.name.identifier == "match"
235
    ):
236
        return (
×
237
            None,
238
            None,
239
            Error(
240
                return_node.original_node,
241
                "The ``match`` function returns a re.Match object, "
242
                "but this function expected the return value to be a boolean. "
243
                "Did you maybe want to write ``return match(...) is not None``?",
244
            ),
245
        )
246

247
    if not isinstance(return_node.value, parse_tree.IsNotNone):
4✔
248
        return (
×
249
            None,
250
            Error(
251
                return_node.value.original_node,
252
                f"Expected to return a ``match(...) is not None``, "
253
                f"but got: {parse_tree.dump(return_node.value)}",
254
            ),
255
            None,
256
        )
257

258
    if not isinstance(return_node.value.value, parse_tree.FunctionCall):
4✔
259
        return (
×
260
            None,
261
            Error(
262
                return_node.value.value.original_node,
263
                f"Expected a function call ``match(...)``, "
264
                f"but got: {parse_tree.dump(return_node.value.value)}",
265
            ),
266
            None,
267
        )
268

269
    if return_node.value.value.name.identifier != "match":
4✔
270
        return (
×
271
            None,
272
            Error(
273
                return_node.value.value.name.original_node,
274
                f"Expected a call to the function ``match(...)``, "
275
                f"but got a call "
276
                f"to function: {return_node.value.value.name.identifier!r}",
277
            ),
278
            None,
279
        )
280

281
    # NOTE (mristin, 2021-12-19):
282
    # From here on we return errors. The verification function looks like a pattern
283
    # matching so if we can not match (no pun intended), we should signal the user that
284
    # something was unexpected.
285

286
    match_call = return_node.value.value
4✔
287
    assert isinstance(match_call, parse_tree.FunctionCall)
4✔
288
    assert match_call.name.identifier == "match"
4✔
289

290
    if len(match_call.args) < 2:
4✔
291
        return (
×
292
            None,
293
            None,
294
            Error(
295
                match_call.original_node,
296
                f"The ``match`` function expects two arguments "
297
                f"(pattern and the text to be matched), "
298
                f"but you provided {len(match_call.args)} argument(s)",
299
            ),
300
        )
301

302
    if len(match_call.args) > 2:
4✔
303
        return (
×
304
            None,
305
            None,
306
            Error(
307
                match_call.original_node,
308
                "We do not support calls to the ``match`` function with more than "
309
                "two arguments (pattern and the text to be matched) "
310
                "since we could not transpile to other languages and schemas "
311
                "(*e.g.*, flags such as multi-line matching)",
312
            ),
313
        )
314

315
    # noinspection PyUnresolvedReferences
316
    if not (
4✔
317
        isinstance(match_call.args[1], parse_tree.Name)
318
        and match_call.args[1].identifier == parsed.arguments[0].name
319
    ):
320
        return (
×
321
            None,
322
            None,
323
            Error(
324
                match_call.original_node,
325
                f"The second argument to ``match`` function, the text to be matched, "
326
                f"needs to correspond to the single argument of "
327
                f"the verification function, {parsed.arguments[0].name!r}. "
328
                f"Otherwise, we can not transpile the pattern to schemas.\n"
329
                f"\n"
330
                f"However, we got: {parse_tree.dump(match_call.args[1])}",
331
            ),
332
        )
333

334
    # noinspection PyUnresolvedReferences
335
    if (
4✔
336
        isinstance(match_call.args[0], parse_tree.Name)
337
        and match_call.args[0].identifier == parsed.arguments[0].name
338
    ):
339
        return (
×
340
            None,
341
            None,
342
            Error(
343
                match_call.original_node,
344
                f"The first argument, the pattern, to the ``match`` function "
345
                f"must not be the argument supplied to "
346
                f"the verification function, {parsed.arguments[0].name!r}.",
347
            ),
348
        )
349

350
    # region Check the support of the statements
351

352
    errors = []  # type: List[Error]
4✔
353
    for i, stmt in enumerate(parsed.body):
4✔
354
        # Skip the return statement
355
        if i == len(parsed.body) - 1:
4✔
356
            break
4✔
357

358
        underlying_errors = _check_support(node=stmt, argument=parsed.arguments[0].name)
4✔
359

360
        if underlying_errors is not None:
4✔
361
            errors.extend(underlying_errors)
×
362

363
    underlying_errors = _check_support(
4✔
364
        node=match_call.args[0], argument=parsed.arguments[0].name
365
    )
366

367
    if underlying_errors is not None:
4✔
368
        errors.extend(underlying_errors)
×
369

370
    if len(errors) > 0:
4✔
371
        return (
×
372
            None,
373
            None,
374
            Error(
375
                parsed.node,
376
                f"We could not understand "
377
                f"the pattern matching function {parsed.name!r}",
378
                errors,
379
            ),
380
        )
381

382
    # endregion
383

384
    # region Re-execute the function to infer the pattern
385

386
    state = collections.OrderedDict()  # type: MutableMapping[Identifier, str]
4✔
387

388
    pattern = None  # type: Optional[str]
4✔
389
    for i, stmt in enumerate(parsed.body):
4✔
390
        if i < len(parsed.body) - 1:
4✔
391
            assert isinstance(stmt, parse_tree.Assignment)
4✔
392
            assert isinstance(stmt.target, parse_tree.Name)
4✔
393

394
            value, error = _evaluate(expr=stmt.value, state=state)
4✔
395
            if error is not None:
4✔
396
                return None, None, error
×
397

398
            assert value is not None
4✔
399

400
            state[stmt.target.identifier] = value
4✔
401

402
        else:
403
            pattern, error = _evaluate(expr=match_call.args[0], state=state)
4✔
404
            if error is not None:
4✔
405
                return None, None, error
×
406

407
            assert pattern is not None
4✔
408

409
    # endregion
410

411
    assert pattern is not None
4✔
412

413
    try:
4✔
414
        re.compile(pattern)
4✔
415
    except re.error as exception:
×
416
        return (
×
417
            None,
418
            None,
419
            Error(
420
                match_call.args[0].original_node,
421
                f"Failed to compile the pattern with the Python's ``re`` module.\n"
422
                f"\n"
423
                f"The evaluated pattern was: {pattern!r}.\n"
424
                f"The error message was: {exception}",
425
            ),
426
        )
427

428
    return pattern, None, None
4✔
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