1# SPDX-License-Identifier: Apache-2.0
2# Copyright (c) 2024 Christophe Dufaza
3
4"""Unit tests dedicated to edtlib.Binding objects initialization.
5
6Running the assumption that any (valid) YAML binding file is
7something we can make a Binding instance with:
8- check which properties are defined at which level (binding, child-binding,
9  grandchild-binding, etc) and their specifications once the binding
10  is initialized
11- check how including bindings are permitted to specialize
12  the specifications of inherited properties
13- check the rules applied when overwriting a binding's description
14  or compatible string (at the binding, child-binding, etc, levels)
15
16At any level, an including binding is permitted to:
17- filter the properties it chooses to inherit with either "property:allowlist"
18  or "property:blocklist" but not both
19- extend inherited properties:
20  - override (implicit or) explicit "required: false" with "required: true"
21  - add constraints to the possible value(s) of a property with "const:"
22    or "enum:"
23  - add a "default:" value to a property
24  - add or overwrite a property's "description:"; when overwritten multiple
25    times by several included binding files, "include:"ed first "wins"
26- define new properties
27
28At any level, an including binding is NOT permitted to:
29- remove a requirement by overriding "required: true" with "required: false"
30- change existing constrains applied to the possible values of
31  an inherited property with "const:" or "enum:"
32- change the "default:" value of an inherited property
33- change the "type:" of an inherited property
34
35Rules applying to bindings' descriptions and compatible strings:
36- included files can't overwrite the description or compatible string set
37  by the including binding (despite that "description:" appears before
38  "include:"): this seems consistent, the top-level binding file "wins"
39- an including binding can overwrite descriptions and compatible strings
40  inherited at the child-binding levels: this seems consistent,
41  the top-level binding file "wins"
42- when we include multiple files overwriting a description or compatible
43  string inherited at the child-binding levels, order of inclusion matters,
44  the first "wins"; this is consistent with property descriptions
45
46For all tests, the entry point is a Binding instance initialized
47by loading the YAML file which represents the test case: our focus here
48really is what happens when we (recursively) call Binding's constructor,
49independently of any actual devicetree model (edtlib.EDT instance).
50"""
51
52# pylint: disable=too-many-statements
53
54import contextlib
55import os
56from collections.abc import Generator
57from typing import Any
58
59import pytest
60from devicetree import edtlib
61
62YAML_KNOWN_BASE_BINDINGS: dict[str, str] = {
63    # Base properties, bottom of the diamond test case.
64    "base.yaml": "test-bindings-init/base.yaml",
65    # Amended properties, left (first) "include:" in the diamond test case.
66    "base_amend.yaml": "test-bindings-init/base_amend.yaml",
67    # Amended properties, right (last) "include:" in the diamond test case.
68    "thing.yaml": "test-bindings-init/thing.yaml",
69    # Used for testing property filters when "A includes B includes C".
70    "simple.yaml": "test-bindings-init/simple.yaml",
71    "simple_inherit.yaml": "test-bindings-init/simple_inherit.yaml",
72    "simple_allowlist.yaml": "test-bindings-init/simple_allowlist.yaml",
73    "simple_blocklist.yaml": "test-bindings-init/simple_blocklist.yaml",
74    # Test applied rules for compatible strings and descriptions.
75    "compat_desc_base.yaml": "test-bindings-init/compat_desc_base.yaml",
76    "compat_desc.yaml": "test-bindings-init/compat_desc.yaml",
77}
78
79
80def load_binding(path: str) -> edtlib.Binding:
81    """Load YAML file as Binding instance,
82    using YAML_BASE to resolve includes.
83
84    Args:
85        path: Path relative to ZEPHYR_BASE/scripts/dts/python-devicetree/tests.
86    """
87    with _from_here():
88        binding = edtlib.Binding(
89            path=path,
90            fname2path=YAML_KNOWN_BASE_BINDINGS,
91            raw=None,
92            require_compatible=False,
93            require_description=False,
94        )
95    return binding
96
97
98def child_binding_of(path: str) -> edtlib.Binding:
99    """Load YAML file as Binding instance, and returns its child-binding.
100    The child-binding must exist.
101
102    Args:
103        path: Path relative to ZEPHYR_BASE/scripts/dts/python-devicetree/tests.
104    """
105    binding = load_binding(path)
106    assert binding.child_binding
107    return binding.child_binding
108
109
110def grandchild_binding_of(path: str) -> edtlib.Binding:
111    """Load YAML file as Binding instance, and returns its grandchild-binding.
112    The grandchild-binding must exist.
113
114    Args:
115        path: Path relative to ZEPHYR_BASE/scripts/dts/python-devicetree/tests.
116    """
117    child_binding = child_binding_of(path)
118    assert child_binding.child_binding
119    return child_binding.child_binding
120
121
122def verify_expected_propspec(
123    propspec: edtlib.PropertySpec,
124    /,
125    *,
126    # Most properties are integers.
127    expect_type: str = "int",
128    expect_req: bool = False,
129    expect_desc: str | None = None,
130    expect_enum: list[int | str] | None = None,
131    expect_const: Any | None = None,
132    expect_default: Any | None = None,
133) -> None:
134    """Compare a property specification with the definitions
135    we (finally) expect.
136
137    All definitions are tested for equality.
138
139    Args:
140        propsec: The property specification to verify.
141        expect_type: The expected property type definition.
142        expect_req: Whether the property is expected to be required.
143        expect_desc: The expected property description.
144        expect_enum: The expected property "enum:" definition.
145        expect_const: The expected property "const:" definition.
146        expect_default: The expected property "default:" definition.
147    """
148    assert expect_type == propspec.type
149    assert expect_req == propspec.required
150    assert expect_desc == propspec.description
151    assert expect_enum == propspec.enum
152    assert expect_const == propspec.const
153    assert expect_default == propspec.default
154
155
156def verify_binding_propspecs_consistency(binding: edtlib.Binding) -> None:
157    """Verify consistency between what's in Binding.prop2specs
158    and Binding.raw.
159
160    Asserts that:
161        Binding.raw["properties"][prop] == Binding.prop2specs[prop]._raw
162
163    If the binding has a child-binding, also recursively verify child-bindings.
164
165    NOTE: do not confuse with binding.prop2specs[prop].binding == binding,
166    which we do not assume here.
167    """
168    if binding.prop2specs:
169        assert set(binding.raw["properties"].keys()) == set(binding.prop2specs.keys())
170        assert all(
171            binding.raw["properties"][prop] == propspec._raw
172            for prop, propspec in binding.prop2specs.items()
173        )
174    if binding.child_binding:
175        verify_binding_propspecs_consistency(binding.child_binding)
176
177
178def test_expect_propspecs_inherited_bindings() -> None:
179    """Test the basics of including property specifications.
180
181    Specifications are simply inherited without modifications
182    up to the grandchild-binding level.
183
184    Check that we actually inherit all expected definitions as-is.
185    """
186    binding = load_binding("test-bindings-init/base_inherit.yaml")
187
188    # Binding level.
189    assert {
190        "prop-1",
191        "prop-2",
192        "prop-enum",
193        "prop-req",
194        "prop-const",
195        "prop-default",
196    } == set(binding.prop2specs.keys())
197    propspec = binding.prop2specs["prop-1"]
198    verify_expected_propspec(propspec, expect_desc="Base property 1.")
199    propspec = binding.prop2specs["prop-2"]
200    verify_expected_propspec(propspec, expect_type="string")
201    propspec = binding.prop2specs["prop-enum"]
202    verify_expected_propspec(propspec, expect_type="string", expect_enum=["FOO", "BAR"])
203    propspec = binding.prop2specs["prop-const"]
204    verify_expected_propspec(propspec, expect_const=8)
205    propspec = binding.prop2specs["prop-req"]
206    verify_expected_propspec(propspec, expect_req=True)
207    propspec = binding.prop2specs["prop-default"]
208    verify_expected_propspec(propspec, expect_default=1)
209
210    # Child-Binding level.
211    assert binding.child_binding
212    child_binding = binding.child_binding
213    assert {
214        "child-prop-1",
215        "child-prop-2",
216        "child-prop-enum",
217        "child-prop-req",
218        "child-prop-const",
219        "child-prop-default",
220    } == set(child_binding.prop2specs.keys())
221    propspec = child_binding.prop2specs["child-prop-1"]
222    verify_expected_propspec(propspec, expect_desc="Base child-prop 1.")
223    propspec = child_binding.prop2specs["child-prop-2"]
224    verify_expected_propspec(propspec, expect_type="string")
225    propspec = child_binding.prop2specs["child-prop-enum"]
226    verify_expected_propspec(propspec, expect_type="string", expect_enum=["CHILD_FOO", "CHILD_BAR"])
227    propspec = child_binding.prop2specs["child-prop-const"]
228    verify_expected_propspec(propspec, expect_const=16)
229    propspec = child_binding.prop2specs["child-prop-req"]
230    verify_expected_propspec(propspec, expect_req=True)
231    propspec = child_binding.prop2specs["child-prop-default"]
232    verify_expected_propspec(propspec, expect_default=2)
233
234    # GrandChild-Binding level.
235    assert child_binding.child_binding
236    grandchild_binding = child_binding.child_binding
237    assert {
238        "grandchild-prop-1",
239        "grandchild-prop-2",
240        "grandchild-prop-enum",
241        "grandchild-prop-req",
242        "grandchild-prop-const",
243        "grandchild-prop-default",
244    } == set(grandchild_binding.prop2specs.keys())
245    propspec = grandchild_binding.prop2specs["grandchild-prop-1"]
246    verify_expected_propspec(propspec, expect_desc="Base grandchild-prop 1.")
247    propspec = grandchild_binding.prop2specs["grandchild-prop-2"]
248    verify_expected_propspec(propspec, expect_type="string")
249    propspec = grandchild_binding.prop2specs["grandchild-prop-enum"]
250    verify_expected_propspec(
251        propspec,
252        expect_type="string",
253        expect_enum=["GRANDCHILD_FOO", "GRANDCHILD_BAR"],
254    )
255    propspec = grandchild_binding.prop2specs["grandchild-prop-const"]
256    verify_expected_propspec(propspec, expect_const=32)
257    propspec = grandchild_binding.prop2specs["grandchild-prop-req"]
258    verify_expected_propspec(propspec, expect_req=True)
259    propspec = grandchild_binding.prop2specs["grandchild-prop-default"]
260    verify_expected_propspec(propspec, expect_default=3)
261
262
263def test_expect_propspecs_amended_bindings() -> None:
264    """Test the basics of including and amending property specifications.
265
266    Base specifications are included once at the binding level:
267
268        include: base.yaml
269        properties:
270          # Amend base.yaml
271        child-binding:
272          properties:
273            # Amend base.yaml
274          child-binding:
275            properties:
276              # Amend base.yaml
277
278    Check that we finally get the expected property specifications
279    up to the grandchild-binding level.
280    """
281    binding = load_binding("test-bindings-init/base_amend.yaml")
282
283    # Binding level.
284    #
285    assert {
286        "prop-1",
287        "prop-2",
288        "prop-enum",
289        "prop-req",
290        "prop-const",
291        "prop-default",
292        "prop-new",
293    } == set(binding.prop2specs.keys())
294    propspec = binding.prop2specs["prop-1"]
295    verify_expected_propspec(
296        propspec,
297        # Amended in base_amend.yaml.
298        expect_desc="Overwritten description.",
299        expect_const=0xF0,
300    )
301    propspec = binding.prop2specs["prop-2"]
302    verify_expected_propspec(
303        propspec,
304        expect_type="string",
305        # Amended in base_amend.yaml.
306        expect_desc="New description.",
307        expect_enum=["EXT_FOO", "EXT_BAR"],
308        expect_default="EXT_FOO",
309    )
310    propspec = binding.prop2specs["prop-enum"]
311    verify_expected_propspec(
312        propspec,
313        expect_type="string",
314        expect_enum=["FOO", "BAR"],
315        # Amended in base_amend.yaml.
316        expect_req=True,
317    )
318    # Inherited from base.yaml without modification.
319    propspec = binding.prop2specs["prop-const"]
320    verify_expected_propspec(propspec, expect_const=8)
321    propspec = binding.prop2specs["prop-req"]
322    verify_expected_propspec(propspec, expect_req=True)
323    propspec = binding.prop2specs["prop-default"]
324    verify_expected_propspec(propspec, expect_default=1)
325
326    # New property in base_amend.yaml.
327    propspec = binding.prop2specs["prop-new"]
328    verify_expected_propspec(propspec)
329
330    # Child-Binding level.
331    #
332    assert binding.child_binding
333    child_binding = binding.child_binding
334    assert {
335        "child-prop-1",
336        "child-prop-2",
337        "child-prop-enum",
338        "child-prop-req",
339        "child-prop-const",
340        "child-prop-default",
341        "child-prop-new",
342    } == set(child_binding.prop2specs.keys())
343    propspec = child_binding.prop2specs["child-prop-1"]
344    verify_expected_propspec(
345        propspec,
346        # Amended in base_amend.yaml.
347        expect_desc="Overwritten description (child).",
348        expect_const=0xF1,
349    )
350    propspec = child_binding.prop2specs["child-prop-2"]
351    verify_expected_propspec(
352        propspec,
353        expect_type="string",
354        # Amended in base_amend.yaml.
355        expect_desc="New description (child).",
356        expect_enum=["CHILD_EXT_FOO", "CHILD_EXT_BAR"],
357        expect_default="CHILD_EXT_FOO",
358    )
359    propspec = child_binding.prop2specs["child-prop-enum"]
360    verify_expected_propspec(
361        propspec,
362        expect_type="string",
363        expect_enum=["CHILD_FOO", "CHILD_BAR"],
364        # Amended in base_amend.yaml.
365        expect_req=True,
366    )
367    # Inherited from base.yaml without modification.
368    propspec = child_binding.prop2specs["child-prop-const"]
369    verify_expected_propspec(propspec, expect_const=16)
370    propspec = child_binding.prop2specs["child-prop-req"]
371    verify_expected_propspec(propspec, expect_req=True)
372    propspec = child_binding.prop2specs["child-prop-default"]
373    verify_expected_propspec(propspec, expect_default=2)
374
375    # New property in base_amend.yaml.
376    propspec = child_binding.prop2specs["child-prop-new"]
377    verify_expected_propspec(propspec)
378
379    # GrandChild-Binding level.
380    #
381    assert child_binding.child_binding
382    grandchild_binding = child_binding.child_binding
383    assert {
384        "grandchild-prop-1",
385        "grandchild-prop-2",
386        "grandchild-prop-enum",
387        "grandchild-prop-req",
388        "grandchild-prop-const",
389        "grandchild-prop-default",
390        "grandchild-prop-new",
391    } == set(grandchild_binding.prop2specs.keys())
392    propspec = grandchild_binding.prop2specs["grandchild-prop-1"]
393    verify_expected_propspec(
394        propspec,
395        # Amended in base_amend.yaml.
396        expect_desc="Overwritten description (grandchild).",
397        expect_const=0xF2,
398    )
399    propspec = grandchild_binding.prop2specs["grandchild-prop-2"]
400    verify_expected_propspec(
401        propspec,
402        expect_type="string",
403        # Amended in base_amend.yaml.
404        expect_desc="New description (grandchild).",
405        expect_enum=["GRANDCHILD_EXT_FOO", "GRANDCHILD_EXT_BAR"],
406        expect_default="GRANDCHILD_EXT_FOO",
407    )
408    propspec = grandchild_binding.prop2specs["grandchild-prop-enum"]
409    verify_expected_propspec(
410        propspec,
411        expect_type="string",
412        expect_enum=["GRANDCHILD_FOO", "GRANDCHILD_BAR"],
413        # Amended in base_amend.yaml.
414        expect_req=True,
415    )
416    # Inherited from base.yaml without modification.
417    propspec = grandchild_binding.prop2specs["grandchild-prop-const"]
418    verify_expected_propspec(propspec, expect_const=32)
419    propspec = grandchild_binding.prop2specs["grandchild-prop-req"]
420    verify_expected_propspec(propspec, expect_req=True)
421    propspec = grandchild_binding.prop2specs["grandchild-prop-default"]
422    verify_expected_propspec(propspec, expect_default=3)
423
424    # New property in base_amend.yaml.
425    propspec = grandchild_binding.prop2specs["grandchild-prop-new"]
426    verify_expected_propspec(propspec)
427
428
429def test_expect_propspecs_multi_child_binding() -> None:
430    """Test including base bindings at multiple levels.
431
432    Base specifications are included at the binding, child-binding
433    and child-binding levels:
434
435        include: base.yaml
436        child-binding:
437          include: base.yaml
438          child-binding:
439            include: base.yaml
440
441    This test checks that we finally get the expected property specifications
442    at the child-binding level.
443    """
444    binding = child_binding_of("test-bindings-init/base_multi.yaml")
445
446    assert {
447        # From top-level "include:" element.
448        "child-prop-1",
449        "child-prop-2",
450        "child-prop-enum",
451        # From "child-binding: include:" element.
452        "prop-1",
453        "prop-2",
454        "prop-enum",
455    } == set(binding.prop2specs.keys())
456
457    # Inherited from base.yaml without modification.
458    propspec = binding.prop2specs["child-prop-2"]
459    verify_expected_propspec(propspec, expect_type="string")
460    propspec = binding.prop2specs["child-prop-enum"]
461    verify_expected_propspec(propspec, expect_type="string", expect_enum=["CHILD_FOO", "CHILD_BAR"])
462
463    propspec = binding.prop2specs["child-prop-1"]
464    verify_expected_propspec(
465        propspec,
466        expect_desc="Base child-prop 1.",
467        # Amended in base_multi.yaml.
468        expect_const=0xF1,
469    )
470    propspec = binding.prop2specs["prop-1"]
471    verify_expected_propspec(
472        propspec,
473        expect_desc="Base property 1.",
474        # Amended in base_multi.yaml.
475        expect_const=0xF1,
476    )
477    propspec = binding.prop2specs["prop-2"]
478    verify_expected_propspec(
479        propspec,
480        expect_type="string",
481        # Amended in base_multi.yaml.
482        expect_desc="New description (child).",
483    )
484    propspec = binding.prop2specs["prop-enum"]
485    verify_expected_propspec(
486        propspec,
487        expect_type="string",
488        expect_enum=["FOO", "BAR"],
489        # Amended in base_multi.yaml.
490        expect_default="FOO",
491        expect_req=True,
492    )
493
494
495def test_expect_propspecs_multi_grandchild_binding() -> None:
496    """Test including base bindings at multiple levels.
497
498    This test checks that we finally get the expected property specifications
499    at the grandchild-binding level.
500
501    See also: test_expect_propspecs_multi_child_binding()
502    """
503    binding = grandchild_binding_of("test-bindings-init/base_multi.yaml")
504
505    assert {
506        # From top-level "include:" element.
507        "grandchild-prop-1",
508        "grandchild-prop-2",
509        "grandchild-prop-enum",
510        # From "child-binding: include:" element.
511        "child-prop-1",
512        "child-prop-2",
513        "child-prop-enum",
514        # From "child-binding: child-binding: include:" element.
515        "prop-1",
516        "prop-2",
517        "prop-enum",
518    } == set(binding.prop2specs.keys())
519
520    # Inherited from base.yaml without modification.
521    propspec = binding.prop2specs["grandchild-prop-2"]
522    verify_expected_propspec(propspec, expect_type="string")
523    propspec = binding.prop2specs["grandchild-prop-enum"]
524    verify_expected_propspec(
525        propspec,
526        expect_type="string",
527        expect_enum=["GRANDCHILD_FOO", "GRANDCHILD_BAR"],
528    )
529    propspec = binding.prop2specs["child-prop-2"]
530    verify_expected_propspec(propspec, expect_type="string")
531    propspec = binding.prop2specs["child-prop-enum"]
532    verify_expected_propspec(propspec, expect_type="string", expect_enum=["CHILD_FOO", "CHILD_BAR"])
533
534    propspec = binding.prop2specs["grandchild-prop-1"]
535    verify_expected_propspec(
536        propspec,
537        expect_desc="Base grandchild-prop 1.",
538        # Amended in base_multi.yaml.
539        expect_const=0xF2,
540    )
541    propspec = binding.prop2specs["child-prop-1"]
542    verify_expected_propspec(
543        propspec,
544        expect_desc="Base child-prop 1.",
545        # Amended in base_multi.yaml.
546        expect_const=0xF2,
547    )
548    propspec = binding.prop2specs["prop-1"]
549    verify_expected_propspec(
550        propspec,
551        expect_desc="Base property 1.",
552        # Amended in base_amend.yaml.
553        expect_const=0xF2,
554    )
555    propspec = binding.prop2specs["prop-2"]
556    verify_expected_propspec(
557        propspec,
558        expect_type="string",
559        # Amended in base_amend.yaml.
560        expect_desc="New description (grandchild).",
561    )
562    propspec = binding.prop2specs["prop-enum"]
563    verify_expected_propspec(
564        propspec,
565        expect_type="string",
566        expect_enum=["FOO", "BAR"],
567        # Amended in base_amend.yaml.
568        expect_req=True,
569        expect_default="FOO",
570    )
571
572
573def test_expect_propspecs_multi_grand_grandchild_binding() -> None:
574    """Test including base bindings at multiple levels.
575
576    This test checks that we finally get the expected property specifications
577    at the grand-grandchild-binding level.
578
579    See also: test_expect_propspecs_multi_child_binding()
580    """
581    binding = grandchild_binding_of("test-bindings-init/base_multi.yaml").child_binding
582    assert binding
583
584    assert {
585        # From "child-binding: include:" element.
586        "child-prop-1",
587        "child-prop-2",
588        "child-prop-enum",
589        # From "child-binding: child-binding: include:" element.
590        "grandchild-prop-1",
591        "grandchild-prop-2",
592        "grandchild-prop-enum",
593    } == set(binding.prop2specs.keys())
594
595    # Inherited from base.yaml without modification.
596    propspec = binding.prop2specs["child-prop-1"]
597    verify_expected_propspec(propspec, expect_desc="Base child-prop 1.")
598    propspec = binding.prop2specs["child-prop-2"]
599    verify_expected_propspec(propspec, expect_type="string")
600    propspec = binding.prop2specs["child-prop-enum"]
601    verify_expected_propspec(propspec, expect_type="string", expect_enum=["CHILD_FOO", "CHILD_BAR"])
602    propspec = binding.prop2specs["grandchild-prop-1"]
603    verify_expected_propspec(propspec, expect_desc="Base grandchild-prop 1.")
604    propspec = binding.prop2specs["grandchild-prop-2"]
605    verify_expected_propspec(propspec, expect_type="string")
606    propspec = binding.prop2specs["grandchild-prop-enum"]
607    verify_expected_propspec(
608        propspec,
609        expect_type="string",
610        expect_enum=["GRANDCHILD_FOO", "GRANDCHILD_BAR"],
611    )
612
613
614def test_expect_propspecs_diamond_binding() -> None:
615    """Test property specifications produced by diamond inheritance.
616
617    This test checks that we finally get the expected property specifications
618    at the binding level.
619    """
620    binding = load_binding("test-bindings-init/diamond.yaml")
621
622    assert {
623        # From base.yaml, amended in base_amend.yaml (left),
624        # last modified in thing.yaml (right).
625        "prop-1",
626        # From base.yaml, amended in base_amend.yaml (left),
627        # and thing.yaml (right), last modified in diamond.yaml(top).
628        "prop-enum",
629        # From base.yaml, inherited in base_amend.yaml (left).
630        "prop-default",
631        # From thing.yaml (right).
632        "prop-thing",
633        # From diamond.yaml (top).
634        "prop-diamond",
635    } == set(binding.prop2specs.keys())
636
637    # Inherited from base.yaml without modification.
638    propspec = binding.prop2specs["prop-default"]
639    verify_expected_propspec(propspec, expect_default=1)
640    # Inherited from thing.yaml without modification.
641    propspec = binding.prop2specs["prop-thing"]
642    verify_expected_propspec(propspec, expect_desc="Thing property.")
643
644    # New property in diamond.yaml.
645    propspec = binding.prop2specs["prop-diamond"]
646    verify_expected_propspec(propspec)
647
648    propspec = binding.prop2specs["prop-1"]
649    verify_expected_propspec(
650        propspec,
651        # From base_amend.yaml.
652        expect_const=0xF0,
653        # Included first wins.
654        expect_desc="Overwritten description.",
655        # From thing.yaml.
656        expect_default=1,
657    )
658    propspec = binding.prop2specs["prop-enum"]
659    verify_expected_propspec(
660        propspec,
661        expect_type="string",
662        expect_enum=["FOO", "BAR"],
663        # From base_amend.yaml.
664        expect_req=True,
665        # From diamond.yaml.
666        expect_desc="Overwritten in diamond.yaml.",
667        expect_default="FOO",
668    )
669
670
671def test_expect_propspecs_diamond_child_binding() -> None:
672    """Test property specifications produced by diamond inheritance.
673
674    This test checks that we finally get the expected property specifications
675    at the child-binding level.
676    """
677    binding = child_binding_of("test-bindings-init/diamond.yaml")
678
679    assert {
680        # From base.yaml, amended in base_amend.yaml (left),
681        # last modified in thing.yaml (right).
682        "child-prop-1",
683        # From base.yaml, amended in base_amend.yaml (left),
684        # and thing.yaml (right), last modified in diamond.yaml(top).
685        "child-prop-enum",
686        # From base.yaml, inherited in base_amend.yaml (left).
687        "child-prop-default",
688        # From thing.yaml (right).
689        "child-prop-thing",
690        # From diamond.yaml (top).
691        "child-prop-diamond",
692    } == set(binding.prop2specs.keys())
693
694    propspec = binding.prop2specs["child-prop-1"]
695    verify_expected_propspec(
696        propspec,
697        # From base_amend.yaml.
698        expect_const=0xF1,
699        # Included first wins.
700        expect_desc="Overwritten description (child).",
701        # From thing.yaml.
702        expect_default=2,
703    )
704
705    propspec = binding.prop2specs["child-prop-enum"]
706    verify_expected_propspec(
707        propspec,
708        expect_type="string",
709        expect_enum=["CHILD_FOO", "CHILD_BAR"],
710        # From base_amend.yaml.
711        # ORed with thing.yaml.
712        expect_req=True,
713        # From diamond.yaml.
714        expect_default="CHILD_FOO",
715        expect_desc="Overwritten in diamond.yaml (child).",
716    )
717
718    # Inherited from base.yaml without modification.
719    propspec = binding.prop2specs["child-prop-default"]
720    verify_expected_propspec(propspec, expect_default=2)
721    # Inherited from thing.yaml without modification.
722    propspec = binding.prop2specs["child-prop-thing"]
723    verify_expected_propspec(propspec, expect_desc="Thing child-binding property.")
724
725    # New property in diamond.yaml.
726    propspec = binding.prop2specs["child-prop-diamond"]
727    verify_expected_propspec(propspec)
728
729
730def test_expect_propspecs_diamond_grandchild_binding() -> None:
731    """Test property specifications produced by diamond inheritance.
732
733    This test checks that we finally get the expected property specifications
734    at the grandchild-binding level.
735    """
736    binding = grandchild_binding_of("test-bindings-init/diamond.yaml")
737
738    assert {
739        # From base.yaml, amended in base_amend.yaml (left),
740        # last modified in thing.yaml (right).
741        "grandchild-prop-1",
742        # From base.yaml, amended in base_amend.yaml (left),
743        # last modified in diamond.yaml (top).
744        "grandchild-prop-enum",
745        # From base.yaml, inherited in base_amend.yaml (left).
746        "grandchild-prop-default",
747        # From thing.yaml (right).
748        "grandchild-prop-thing",
749        # From diamond.yaml (top).
750        "grandchild-prop-diamond",
751    } == set(binding.prop2specs.keys())
752
753    propspec = binding.prop2specs["grandchild-prop-1"]
754    verify_expected_propspec(
755        propspec,
756        # From base_amend.yaml.
757        expect_const=0xF2,
758        # Included first wins.
759        expect_desc="Overwritten description (grandchild).",
760        # From thing.yaml.
761        expect_default=3,
762    )
763
764    propspec = binding.prop2specs["grandchild-prop-enum"]
765    verify_expected_propspec(
766        propspec,
767        expect_type="string",
768        expect_enum=["GRANDCHILD_FOO", "GRANDCHILD_BAR"],
769        # From base_amend.yaml.
770        # ORed with thing.yaml.
771        expect_req=True,
772        # From diamond.yaml.
773        expect_default="GRANDCHILD_FOO",
774        expect_desc="Overwritten in diamond.yaml (grandchild).",
775    )
776
777    # Inherited from base.yaml without modification.
778    propspec = binding.prop2specs["grandchild-prop-default"]
779    verify_expected_propspec(propspec, expect_default=3)
780    # Inherited from thing.yaml without modification.
781    propspec = binding.prop2specs["grandchild-prop-thing"]
782    verify_expected_propspec(propspec, expect_desc="Thing grandchild-binding property.")
783
784    # New property in diamond.yaml.
785    propspec = binding.prop2specs["grandchild-prop-diamond"]
786    verify_expected_propspec(propspec)
787
788
789def test_binding_description_overwrite() -> None:
790    """Test whether we can overwrite a binding's description.
791
792    Included files can't overwrite the description set by the including binding
793    (despite that "description:" appears before "include:").
794
795    This seems consistent: the top-level binding file "wins".
796    """
797    binding = load_binding("test-bindings-init/compat_desc.yaml")
798    assert binding.description == "Binding description."
799
800    binding = load_binding("test-bindings-init/compat_desc_multi.yaml")
801    assert binding.description == "Binding description (multi)."
802
803
804def test_binding_compat_overwrite() -> None:
805    """Test whether we can overwrite a binding's compatible string.
806
807    Included files can't overwrite the compatible string set by the
808    including binding (despite that "compatible:" appears before "include:").
809
810    This seems consistent: the top-level binding file "wins".
811    """
812    binding = load_binding("test-bindings-init/compat_desc.yaml")
813    assert binding.compatible == "vnd,compat-desc"
814
815    binding = load_binding("test-bindings-init/compat_desc_multi.yaml")
816    assert binding.compatible == "vnd,compat-desc-multi"
817
818
819def test_child_binding_description_overwrite() -> None:
820    """Test whether we can overwrite a child-binding's description.
821
822    An including binding can overwrite an inherited child-binding's description.
823
824    When we include multiple files overwriting the description
825    at the child-binding level, the first "wins".
826    """
827    child_binding = child_binding_of("test-bindings-init/compat_desc.yaml")
828    # Overwrite inherited description.
829    assert child_binding.description == "Child-binding description."
830
831    child_binding = child_binding_of("test-bindings-init/compat_desc_multi.yaml")
832    # When inherited multiple times, the first "description:" wins.
833    assert child_binding.description == "Child-binding description (base)."
834
835
836def test_child_binding_compat_overwrite() -> None:
837    """Test whether we can overwrite a child-binding's compatible string.
838
839    An including binding can overwrite an inherited child-binding's
840    compatible string.
841
842    When we include multiple files overwriting the compatible string
843    at the child-binding level, the first "wins".
844    """
845    child_binding = child_binding_of("test-bindings-init/compat_desc.yaml")
846    # Overwrite inherited description.
847    assert child_binding.compatible == "vnd,child-compat-desc"
848
849    child_binding = child_binding_of("test-bindings-init/compat_desc_multi.yaml")
850    # When inherited multiple times, the first "compatible:" wins.
851    assert child_binding.compatible == "vnd,child-compat-desc-base"
852
853
854def test_grandchild_binding_description_overwrite() -> None:
855    """Test whether we can overwrite a grandchild-binding's description.
856
857    See also: test_child_binding_description_overwrite()
858    """
859    grandchild_binding = grandchild_binding_of("test-bindings-init/compat_desc.yaml")
860    assert grandchild_binding.description == "Grandchild-binding description."
861
862    grandchild_binding = grandchild_binding_of("test-bindings-init/compat_desc_multi.yaml")
863    assert grandchild_binding.description == "Grandchild-binding description (base)."
864
865
866def test_grandchild_binding_compat_overwrite() -> None:
867    """Test whether we can overwrite a grandchild-binding's compatible string.
868
869    See also: test_child_binding_compat_overwrite()
870    """
871    grandchild_binding = grandchild_binding_of("test-bindings-init/compat_desc.yaml")
872    # Overwrite inherited description.
873    assert grandchild_binding.compatible == "vnd,grandchild-compat-desc"
874
875    grandchild_binding = grandchild_binding_of("test-bindings-init/compat_desc_multi.yaml")
876    # When inherited multiple times, the first "compatible:" wins.
877    assert grandchild_binding.compatible == "vnd,grandchild-compat-desc-base"
878
879
880def test_filter_inherited_propspecs_basics() -> None:
881    """Test the basics of filtering properties inherited via an intermediary
882    binding file.
883
884    Use-case "B filters I includes X":
885    - X is a base binding file, specifying common properties
886    - I is an intermediary binding file, which includes X without modification
887      nor filter
888    - B includes I, filtering the properties it chooses to inherit
889      with an allowlist or a blocklist
890
891    Checks, up to the grandchild-binding level, that the properties inherited
892    from X via I are actually filtered as B intends to.
893    """
894    # Binding level.
895    binding = load_binding("test-bindings-init/simple_allowlist.yaml")
896    # Allowed properties.
897    assert {"prop-1", "prop-2"} == set(binding.prop2specs.keys())
898    binding = load_binding("test-bindings-init/simple_blocklist.yaml")
899    # Non blocked properties.
900    assert {"prop-2", "prop-3"} == set(binding.prop2specs.keys())
901
902    # Child-binding level.
903    child_binding = child_binding_of("test-bindings-init/simple_allowlist.yaml")
904    # Allowed properties.
905    assert {"child-prop-1", "child-prop-2"} == set(child_binding.prop2specs.keys())
906    child_binding = child_binding_of("test-bindings-init/simple_blocklist.yaml")
907    # Non blocked properties.
908    assert {"child-prop-2", "child-prop-3"} == set(child_binding.prop2specs.keys())
909
910    # GrandChild-binding level.
911    grandchild_binding = grandchild_binding_of("test-bindings-init/simple_allowlist.yaml")
912    # Allowed properties.
913    assert {"grandchild-prop-1", "grandchild-prop-2"} == set(grandchild_binding.prop2specs.keys())
914    grandchild_binding = grandchild_binding_of("test-bindings-init/simple_blocklist.yaml")
915    # Non blocked properties.
916    assert {"grandchild-prop-2", "grandchild-prop-3"} == set(grandchild_binding.prop2specs.keys())
917
918
919def test_filter_inherited_propspecs_among_allowed() -> None:
920    """Test filtering properties which have been allowed by an intermediary
921    binding file.
922
923    Complementary to test_filter_inherited_propspecs_basics().
924
925    Use-case "B filters I filters X":
926    - X is a base binding file, specifying common properties
927    - I is an intermediary binding file, filtering the properties specified
928      in X with an allowlist
929    - B includes I, filtering the properties it chooses to inherit
930      also with an allowlist
931
932    Checks, up to the grandchild-binding level, that B inherits the properties
933    specified in X which are first allowed in I, then also allowed in B.
934
935    For that, we check that if B allows only properties that are not allowed in I,
936    we then end up with no property at all.
937    """
938    binding = load_binding("test-bindings-init/filter_among_allowed.yaml")
939    assert not set(binding.prop2specs.keys())
940    assert binding.child_binding
941    child_binding = binding.child_binding
942    assert not set(child_binding.prop2specs.keys())
943    assert child_binding.child_binding
944    grandchild_binding = child_binding.child_binding
945    assert not set(grandchild_binding.prop2specs.keys())
946
947
948def test_filter_inherited_propspecs_among_notblocked() -> None:
949    """Test filtering properties which have not been blocked by an intermediary
950    binding file.
951
952    Complementary to test_filter_inherited_propspecs_basics().
953
954    Use-case "B filters I filters X":
955    - X is a base binding file, specifying common properties
956    - I is an intermediary binding file, filtering the properties specified
957      in X with a blocklist
958    - B includes I, filtering the properties it chooses to inherit
959      also with a blocklist
960
961    Checks, up to the grandchild-binding level, that B inherits the properties
962    specified in X which are not blocked in I, then neither blocked in B.
963
964    For that, we check that if B blocks all properties that are not blocked in I,
965    we then end up with no property at all.
966    """
967    binding = load_binding("test-bindings-init/filter_among_notblocked.yaml")
968    assert not set(binding.prop2specs.keys())
969    assert binding.child_binding
970    child_binding = binding.child_binding
971    assert not set(child_binding.prop2specs.keys())
972    assert child_binding.child_binding
973    grandchild_binding = child_binding.child_binding
974    assert not set(grandchild_binding.prop2specs.keys())
975
976
977def test_filter_inherited_propspecs_allows_notblocked() -> None:
978    """Test allowing properties which have not been blocked by an intermediary
979    binding file.
980
981    Complementary to test_filter_inherited_propspecs_basics().
982
983    Use-case "B filters I filters X":
984    - X is a base binding file, specifying common properties
985    - I is an intermediary binding file, filtering the properties specified
986      in X with a blocklist
987    - B includes I, filtering the properties it chooses to inherit
988      also with an allowlist
989
990    Checks, up to the grandchild-binding level, that B inherits the properties
991    specified in X which are not blocked in I, then allowed in B.
992
993    Permits to verify we can apply both "property-allowlist"
994    and "property-blocklist" filters to the same file,
995    as long as they're not applied simultaneously
996    (i.e. within the same YAML "include:" map).
997    """
998    binding = load_binding("test-bindings-init/filter_allows_notblocked.yaml")
999    assert {"prop-2"} == set(binding.prop2specs.keys())
1000    assert binding.child_binding
1001    child_binding = binding.child_binding
1002    assert {"child-prop-2"} == set(child_binding.prop2specs.keys())
1003    assert child_binding.child_binding
1004    grandchild_binding = child_binding.child_binding
1005    assert {"grandchild-prop-2"} == set(grandchild_binding.prop2specs.keys())
1006
1007
1008def test_invalid_binding_type_override() -> None:
1009    """An including binding should not try to override the "type:"
1010    of an inherited property.
1011
1012    Tested up to the grandchild-binding level.
1013    """
1014    with pytest.raises(edtlib.EDTError) as e:
1015        load_binding("test-bindings-init/invalid_proptype.yaml")
1016    assert "prop-1" in str(e)
1017    assert "'int' replaced with 'string'" in str(e)
1018
1019    with pytest.raises(edtlib.EDTError) as e:
1020        load_binding("test-bindings-init/invalid_child_proptype.yaml")
1021    assert "child-prop-1" in str(e)
1022    assert "'int' replaced with 'string'" in str(e)
1023
1024    with pytest.raises(edtlib.EDTError) as e:
1025        load_binding("test-bindings-init/invalid_grandchild_proptype.yaml")
1026    assert "grandchild-prop-1" in str(e)
1027    assert "'int' replaced with 'string'" in str(e)
1028
1029
1030def test_invalid_binding_const_override() -> None:
1031    """An including binding should not try to override the "const:" value
1032    in a property specification inherited from an included file.
1033
1034    Tested up to the grandchild-binding level.
1035    """
1036    with pytest.raises(edtlib.EDTError) as e:
1037        load_binding("test-bindings-init/invalid_propconst.yaml")
1038    assert "prop-const" in str(e)
1039    assert "'8' replaced with '999'" in str(e)
1040
1041    with pytest.raises(edtlib.EDTError) as e:
1042        load_binding("test-bindings-init/invalid_child_propconst.yaml")
1043    assert "child-prop-const" in str(e)
1044    assert "'16' replaced with '999'" in str(e)
1045
1046    with pytest.raises(edtlib.EDTError) as e:
1047        load_binding("test-bindings-init/invalid_grandchild_propconst.yaml")
1048    assert "grandchild-prop-const" in str(e)
1049    assert "'32' replaced with '999'" in str(e)
1050
1051
1052def test_invalid_binding_required_override() -> None:
1053    """An including binding should not try to override "required: true"
1054    in a property specification inherited from an included file.
1055
1056    Tested up to the grandchild-binding level.
1057    """
1058    with pytest.raises(edtlib.EDTError) as e:
1059        load_binding("test-bindings-init/invalid_propreq.yaml")
1060    assert "prop-req" in str(e)
1061    assert "'True' replaced with 'False'" in str(e)
1062
1063    with pytest.raises(edtlib.EDTError) as e:
1064        load_binding("test-bindings-init/invalid_child_propreq.yaml")
1065    assert "child-prop-req" in str(e)
1066    assert "'True' replaced with 'False'" in str(e)
1067
1068    with pytest.raises(edtlib.EDTError) as e:
1069        load_binding("test-bindings-init/invalid_grandchild_propreq.yaml")
1070    assert "grandchild-prop-req" in str(e)
1071    assert "'True' replaced with 'False'" in str(e)
1072
1073
1074def test_invalid_binding_default_override() -> None:
1075    """An including binding should not try to override the "default:" value
1076    in a property specification inherited from an included file.
1077
1078    Tested up to the grandchild-binding level.
1079    """
1080    with pytest.raises(edtlib.EDTError) as e:
1081        load_binding("test-bindings-init/invalid_propdefault.yaml")
1082    assert "prop-default" in str(e)
1083    assert "'1' replaced with '999'" in str(e)
1084
1085    with pytest.raises(edtlib.EDTError) as e:
1086        load_binding("test-bindings-init/invalid_child_propdefault.yaml")
1087    assert "child-prop-default" in str(e)
1088    assert "'2' replaced with '999'" in str(e)
1089
1090    with pytest.raises(edtlib.EDTError) as e:
1091        load_binding("test-bindings-init/invalid_grandchild_propdefault.yaml")
1092    assert "grandchild-prop-default" in str(e)
1093    assert "'3' replaced with '999'" in str(e)
1094
1095
1096def test_invalid_binding_enum_override() -> None:
1097    """An including binding should not try to override the "enum:" values
1098    in a property specification inherited from an included file.
1099
1100    Tested up to the grandchild-binding level.
1101    """
1102    with pytest.raises(edtlib.EDTError) as e:
1103        load_binding("test-bindings-init/invalid_propenum.yaml")
1104    assert "prop-enum" in str(e)
1105    assert "'['FOO', 'BAR']' replaced with '['OTHER_FOO', 'OTHER_BAR']'" in str(e)
1106
1107    with pytest.raises(edtlib.EDTError) as e:
1108        load_binding("test-bindings-init/invalid_child_propenum.yaml")
1109    assert "child-prop-enum" in str(e)
1110    assert "'['CHILD_FOO', 'CHILD_BAR']' replaced with '['OTHER_FOO', 'OTHER_BAR']'" in str(e)
1111
1112    with pytest.raises(edtlib.EDTError) as e:
1113        load_binding("test-bindings-init/invalid_grandchild_propenum.yaml")
1114    assert "grandchild-prop-enum" in str(e)
1115    assert (
1116        "'['GRANDCHILD_FOO', 'GRANDCHILD_BAR']' replaced with '['OTHER_FOO', 'OTHER_BAR']'"
1117        in str(e)
1118    )
1119
1120
1121def test_bindings_propspecs_consistency() -> None:
1122    """Verify property specifications consistency.
1123
1124    Consistency is recursively checked for all defined properties,
1125    from top-level binding files down to their child bindings.
1126
1127    Consistency is checked with:
1128        Binding.raw["properties"][prop] == Binding.prop2specs[prop]._raw
1129
1130    See verify_binding_propspecs_consistency().
1131    """
1132    binding = load_binding("test-bindings-init/base_inherit.yaml")
1133    verify_binding_propspecs_consistency(binding)
1134
1135    binding = load_binding("test-bindings-init/base_amend.yaml")
1136    verify_binding_propspecs_consistency(binding)
1137
1138    binding = load_binding("test-bindings-init/base_multi.yaml")
1139    verify_binding_propspecs_consistency(binding)
1140
1141    binding = load_binding("test-bindings-init/thing.yaml")
1142    verify_binding_propspecs_consistency(binding)
1143
1144    binding = load_binding("test-bindings-init/diamond.yaml")
1145    verify_binding_propspecs_consistency(binding)
1146
1147
1148# Borrowed from test_edtlib.py.
1149@contextlib.contextmanager
1150def _from_here() -> Generator[None, None, None]:
1151    cwd = os.getcwd()
1152    try:
1153        os.chdir(os.path.dirname(__file__))
1154        yield
1155    finally:
1156        os.chdir(cwd)
1157
1158
1159def _basename(path: str | None) -> str:
1160    return os.path.basename(path or "?")
1161