zephyr/scripts/dts/python-devicetree/tests/test_edtlib_binding_init.py
Christophe Dufaza ee5c520326 edtlib: tests: refine coverage of Binding objects initialization
Add a series of unit tests which try to cover somewhat systematically
the possible inputs and what we finally get at the exit
of the Binding constructor.

Running the assumption that any (valid) YAML binding file is
something we can make a Binding instance with:
- check which properties are defined at which level (binding,
  child-binding, grandchild-binding, etc) and their specifications
  once the binding is initialized
- check how including bindings are permitted to specialize
  the specifications of inherited properties
- check the rules applied when overwriting a binding's description
  or compatible string (at the binding, child-binding, etc, levels)

Some tests covering known issues are disabled by default:
- this permits to document these issues
- while not causing CI errors (when running the python-devicetree
  unit tests)
- enabling these tests without causing errors should allow us
  to consider the related issues are fixed

Signed-off-by: Christophe Dufaza <chris@openmarl.org>
2025-01-08 19:17:51 +01:00

1160 lines
43 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2024 Christophe Dufaza
"""Unit tests dedicated to edtlib.Binding objects initialization.
Running the assumption that any (valid) YAML binding file is
something we can make a Binding instance with:
- check which properties are defined at which level (binding, child-binding,
grandchild-binding, etc) and their specifications once the binding
is initialized
- check how including bindings are permitted to specialize
the specifications of inherited properties
- check the rules applied when overwriting a binding's description
or compatible string (at the binding, child-binding, etc, levels)
At any level, an including binding is permitted to:
- filter the properties it chooses to inherit with either "property:allowlist"
or "property:blocklist" but not both
- extend inherited properties:
- override (implicit or) explicit "required: false" with "required: true"
- add constraints to the possible value(s) of a property with "const:"
or "enum:"
- add a "default:" value to a property
- add or overwrite a property's "description:"; when overwritten multiple
times by several included binding files, "include:"ed first "wins"
- define new properties
At any level, an including binding is NOT permitted to:
- remove a requirement by overriding "required: true" with "required: false"
- change existing constrains applied to the possible values of
an inherited property with "const:" or "enum:"
- change the "default:" value of an inherited property
- change the "type:" of an inherited property
Rules applying to bindings' descriptions and compatible strings:
- included files can't overwrite the description or compatible string set
by the including binding (despite that "description:" appears before
"include:"): this seems consistent, the top-level binding file "wins"
- an including binding can overwrite descriptions and compatible strings
inherited at the child-binding levels: this seems consistent,
the top-level binding file "wins"
- when we include multiple files overwriting a description or compatible
string inherited at the child-binding levels, order of inclusion matters,
the first "wins"; this is consistent with property descriptions
For all tests, the entry point is a Binding instance initialized
by loading the YAML file which represents the test case: our focus here
really is what happens when we (recursively) call Binding's constructor,
independently of any actual devicetree model (edtlib.EDT instance).
"""
# pylint: disable=too-many-statements
import contextlib
import os
from collections.abc import Generator
from typing import Any
import pytest
from devicetree import edtlib
YAML_KNOWN_BASE_BINDINGS: dict[str, str] = {
# Base properties, bottom of the diamond test case.
"base.yaml": "test-bindings-init/base.yaml",
# Amended properties, left (first) "include:" in the diamond test case.
"base_amend.yaml": "test-bindings-init/base_amend.yaml",
# Amended properties, right (last) "include:" in the diamond test case.
"thing.yaml": "test-bindings-init/thing.yaml",
# Used for testing property filters when "A includes B includes C".
"simple.yaml": "test-bindings-init/simple.yaml",
"simple_inherit.yaml": "test-bindings-init/simple_inherit.yaml",
"simple_allowlist.yaml": "test-bindings-init/simple_allowlist.yaml",
"simple_blocklist.yaml": "test-bindings-init/simple_blocklist.yaml",
# Test applied rules for compatible strings and descriptions.
"compat_desc_base.yaml": "test-bindings-init/compat_desc_base.yaml",
"compat_desc.yaml": "test-bindings-init/compat_desc.yaml",
}
def load_binding(path: str) -> edtlib.Binding:
"""Load YAML file as Binding instance,
using YAML_BASE to resolve includes.
Args:
path: Path relative to ZEPHYR_BASE/scripts/dts/python-devicetree/tests.
"""
with _from_here():
binding = edtlib.Binding(
path=path,
fname2path=YAML_KNOWN_BASE_BINDINGS,
raw=None,
require_compatible=False,
require_description=False,
)
return binding
def child_binding_of(path: str) -> edtlib.Binding:
"""Load YAML file as Binding instance, and returns its child-binding.
The child-binding must exist.
Args:
path: Path relative to ZEPHYR_BASE/scripts/dts/python-devicetree/tests.
"""
binding = load_binding(path)
assert binding.child_binding
return binding.child_binding
def grandchild_binding_of(path: str) -> edtlib.Binding:
"""Load YAML file as Binding instance, and returns its grandchild-binding.
The grandchild-binding must exist.
Args:
path: Path relative to ZEPHYR_BASE/scripts/dts/python-devicetree/tests.
"""
child_binding = child_binding_of(path)
assert child_binding.child_binding
return child_binding.child_binding
def verify_expected_propspec(
propspec: edtlib.PropertySpec,
/,
*,
# Most properties are integers.
expect_type: str = "int",
expect_req: bool = False,
expect_desc: str | None = None,
expect_enum: list[int | str] | None = None,
expect_const: Any | None = None,
expect_default: Any | None = None,
) -> None:
"""Compare a property specification with the definitions
we (finally) expect.
All definitions are tested for equality.
Args:
propsec: The property specification to verify.
expect_type: The expected property type definition.
expect_req: Whether the property is expected to be required.
expect_desc: The expected property description.
expect_enum: The expected property "enum:" definition.
expect_const: The expected property "const:" definition.
expect_default: The expected property "default:" definition.
"""
assert expect_type == propspec.type
assert expect_req == propspec.required
assert expect_desc == propspec.description
assert expect_enum == propspec.enum
assert expect_const == propspec.const
assert expect_default == propspec.default
def verify_binding_propspecs_consistency(binding: edtlib.Binding) -> None:
"""Verify consistency between what's in Binding.prop2specs
and Binding.raw.
Asserts that:
Binding.raw["properties"][prop] == Binding.prop2specs[prop]._raw
If the binding has a child-binding, also recursively verify child-bindings.
NOTE: do not confuse with binding.prop2specs[prop].binding == binding,
which we do not assume here.
"""
if binding.prop2specs:
assert set(binding.raw["properties"].keys()) == set(binding.prop2specs.keys())
assert all(
binding.raw["properties"][prop] == propspec._raw
for prop, propspec in binding.prop2specs.items()
)
if binding.child_binding:
verify_binding_propspecs_consistency(binding.child_binding)
def test_expect_propspecs_inherited_bindings() -> None:
"""Test the basics of including property specifications.
Specifications are simply inherited without modifications
up to the grandchild-binding level.
Check that we actually inherit all expected definitions as-is.
"""
binding = load_binding("test-bindings-init/base_inherit.yaml")
# Binding level.
assert {
"prop-1",
"prop-2",
"prop-enum",
"prop-req",
"prop-const",
"prop-default",
} == set(binding.prop2specs.keys())
propspec = binding.prop2specs["prop-1"]
verify_expected_propspec(propspec, expect_desc="Base property 1.")
propspec = binding.prop2specs["prop-2"]
verify_expected_propspec(propspec, expect_type="string")
propspec = binding.prop2specs["prop-enum"]
verify_expected_propspec(propspec, expect_type="string", expect_enum=["FOO", "BAR"])
propspec = binding.prop2specs["prop-const"]
verify_expected_propspec(propspec, expect_const=8)
propspec = binding.prop2specs["prop-req"]
verify_expected_propspec(propspec, expect_req=True)
propspec = binding.prop2specs["prop-default"]
verify_expected_propspec(propspec, expect_default=1)
# Child-Binding level.
assert binding.child_binding
child_binding = binding.child_binding
assert {
"child-prop-1",
"child-prop-2",
"child-prop-enum",
"child-prop-req",
"child-prop-const",
"child-prop-default",
} == set(child_binding.prop2specs.keys())
propspec = child_binding.prop2specs["child-prop-1"]
verify_expected_propspec(propspec, expect_desc="Base child-prop 1.")
propspec = child_binding.prop2specs["child-prop-2"]
verify_expected_propspec(propspec, expect_type="string")
propspec = child_binding.prop2specs["child-prop-enum"]
verify_expected_propspec(propspec, expect_type="string", expect_enum=["CHILD_FOO", "CHILD_BAR"])
propspec = child_binding.prop2specs["child-prop-const"]
verify_expected_propspec(propspec, expect_const=16)
propspec = child_binding.prop2specs["child-prop-req"]
verify_expected_propspec(propspec, expect_req=True)
propspec = child_binding.prop2specs["child-prop-default"]
verify_expected_propspec(propspec, expect_default=2)
# GrandChild-Binding level.
assert child_binding.child_binding
grandchild_binding = child_binding.child_binding
assert {
"grandchild-prop-1",
"grandchild-prop-2",
"grandchild-prop-enum",
"grandchild-prop-req",
"grandchild-prop-const",
"grandchild-prop-default",
} == set(grandchild_binding.prop2specs.keys())
propspec = grandchild_binding.prop2specs["grandchild-prop-1"]
verify_expected_propspec(propspec, expect_desc="Base grandchild-prop 1.")
propspec = grandchild_binding.prop2specs["grandchild-prop-2"]
verify_expected_propspec(propspec, expect_type="string")
propspec = grandchild_binding.prop2specs["grandchild-prop-enum"]
verify_expected_propspec(
propspec,
expect_type="string",
expect_enum=["GRANDCHILD_FOO", "GRANDCHILD_BAR"],
)
propspec = grandchild_binding.prop2specs["grandchild-prop-const"]
verify_expected_propspec(propspec, expect_const=32)
propspec = grandchild_binding.prop2specs["grandchild-prop-req"]
verify_expected_propspec(propspec, expect_req=True)
propspec = grandchild_binding.prop2specs["grandchild-prop-default"]
verify_expected_propspec(propspec, expect_default=3)
def test_expect_propspecs_amended_bindings() -> None:
"""Test the basics of including and amending property specifications.
Base specifications are included once at the binding level:
include: base.yaml
properties:
# Amend base.yaml
child-binding:
properties:
# Amend base.yaml
child-binding:
properties:
# Amend base.yaml
Check that we finally get the expected property specifications
up to the grandchild-binding level.
"""
binding = load_binding("test-bindings-init/base_amend.yaml")
# Binding level.
#
assert {
"prop-1",
"prop-2",
"prop-enum",
"prop-req",
"prop-const",
"prop-default",
"prop-new",
} == set(binding.prop2specs.keys())
propspec = binding.prop2specs["prop-1"]
verify_expected_propspec(
propspec,
# Amended in base_amend.yaml.
expect_desc="Overwritten description.",
expect_const=0xF0,
)
propspec = binding.prop2specs["prop-2"]
verify_expected_propspec(
propspec,
expect_type="string",
# Amended in base_amend.yaml.
expect_desc="New description.",
expect_enum=["EXT_FOO", "EXT_BAR"],
expect_default="EXT_FOO",
)
propspec = binding.prop2specs["prop-enum"]
verify_expected_propspec(
propspec,
expect_type="string",
expect_enum=["FOO", "BAR"],
# Amended in base_amend.yaml.
expect_req=True,
)
# Inherited from base.yaml without modification.
propspec = binding.prop2specs["prop-const"]
verify_expected_propspec(propspec, expect_const=8)
propspec = binding.prop2specs["prop-req"]
verify_expected_propspec(propspec, expect_req=True)
propspec = binding.prop2specs["prop-default"]
verify_expected_propspec(propspec, expect_default=1)
# New property in base_amend.yaml.
propspec = binding.prop2specs["prop-new"]
verify_expected_propspec(propspec)
# Child-Binding level.
#
assert binding.child_binding
child_binding = binding.child_binding
assert {
"child-prop-1",
"child-prop-2",
"child-prop-enum",
"child-prop-req",
"child-prop-const",
"child-prop-default",
"child-prop-new",
} == set(child_binding.prop2specs.keys())
propspec = child_binding.prop2specs["child-prop-1"]
verify_expected_propspec(
propspec,
# Amended in base_amend.yaml.
expect_desc="Overwritten description (child).",
expect_const=0xF1,
)
propspec = child_binding.prop2specs["child-prop-2"]
verify_expected_propspec(
propspec,
expect_type="string",
# Amended in base_amend.yaml.
expect_desc="New description (child).",
expect_enum=["CHILD_EXT_FOO", "CHILD_EXT_BAR"],
expect_default="CHILD_EXT_FOO",
)
propspec = child_binding.prop2specs["child-prop-enum"]
verify_expected_propspec(
propspec,
expect_type="string",
expect_enum=["CHILD_FOO", "CHILD_BAR"],
# Amended in base_amend.yaml.
expect_req=True,
)
# Inherited from base.yaml without modification.
propspec = child_binding.prop2specs["child-prop-const"]
verify_expected_propspec(propspec, expect_const=16)
propspec = child_binding.prop2specs["child-prop-req"]
verify_expected_propspec(propspec, expect_req=True)
propspec = child_binding.prop2specs["child-prop-default"]
verify_expected_propspec(propspec, expect_default=2)
# New property in base_amend.yaml.
propspec = child_binding.prop2specs["child-prop-new"]
verify_expected_propspec(propspec)
# GrandChild-Binding level.
#
assert child_binding.child_binding
grandchild_binding = child_binding.child_binding
assert {
"grandchild-prop-1",
"grandchild-prop-2",
"grandchild-prop-enum",
"grandchild-prop-req",
"grandchild-prop-const",
"grandchild-prop-default",
"grandchild-prop-new",
} == set(grandchild_binding.prop2specs.keys())
propspec = grandchild_binding.prop2specs["grandchild-prop-1"]
verify_expected_propspec(
propspec,
# Amended in base_amend.yaml.
expect_desc="Overwritten description (grandchild).",
expect_const=0xF2,
)
propspec = grandchild_binding.prop2specs["grandchild-prop-2"]
verify_expected_propspec(
propspec,
expect_type="string",
# Amended in base_amend.yaml.
expect_desc="New description (grandchild).",
expect_enum=["GRANDCHILD_EXT_FOO", "GRANDCHILD_EXT_BAR"],
expect_default="GRANDCHILD_EXT_FOO",
)
propspec = grandchild_binding.prop2specs["grandchild-prop-enum"]
verify_expected_propspec(
propspec,
expect_type="string",
expect_enum=["GRANDCHILD_FOO", "GRANDCHILD_BAR"],
# Amended in base_amend.yaml.
expect_req=True,
)
# Inherited from base.yaml without modification.
propspec = grandchild_binding.prop2specs["grandchild-prop-const"]
verify_expected_propspec(propspec, expect_const=32)
propspec = grandchild_binding.prop2specs["grandchild-prop-req"]
verify_expected_propspec(propspec, expect_req=True)
propspec = grandchild_binding.prop2specs["grandchild-prop-default"]
verify_expected_propspec(propspec, expect_default=3)
# New property in base_amend.yaml.
propspec = grandchild_binding.prop2specs["grandchild-prop-new"]
verify_expected_propspec(propspec)
def test_expect_propspecs_multi_child_binding() -> None:
"""Test including base bindings at multiple levels.
Base specifications are included at the binding, child-binding
and child-binding levels:
include: base.yaml
child-binding:
include: base.yaml
child-binding:
include: base.yaml
This test checks that we finally get the expected property specifications
at the child-binding level.
"""
binding = child_binding_of("test-bindings-init/base_multi.yaml")
assert {
# From top-level "include:" element.
"child-prop-1",
"child-prop-2",
"child-prop-enum",
# From "child-binding: include:" element.
"prop-1",
"prop-2",
"prop-enum",
} == set(binding.prop2specs.keys())
# Inherited from base.yaml without modification.
propspec = binding.prop2specs["child-prop-2"]
verify_expected_propspec(propspec, expect_type="string")
propspec = binding.prop2specs["child-prop-enum"]
verify_expected_propspec(propspec, expect_type="string", expect_enum=["CHILD_FOO", "CHILD_BAR"])
propspec = binding.prop2specs["child-prop-1"]
verify_expected_propspec(
propspec,
expect_desc="Base child-prop 1.",
# Amended in base_multi.yaml.
expect_const=0xF1,
)
propspec = binding.prop2specs["prop-1"]
verify_expected_propspec(
propspec,
expect_desc="Base property 1.",
# Amended in base_multi.yaml.
expect_const=0xF1,
)
propspec = binding.prop2specs["prop-2"]
verify_expected_propspec(
propspec,
expect_type="string",
# Amended in base_multi.yaml.
expect_desc="New description (child).",
)
propspec = binding.prop2specs["prop-enum"]
verify_expected_propspec(
propspec,
expect_type="string",
expect_enum=["FOO", "BAR"],
# Amended in base_multi.yaml.
expect_default="FOO",
expect_req=True,
)
def test_expect_propspecs_multi_grandchild_binding() -> None:
"""Test including base bindings at multiple levels.
This test checks that we finally get the expected property specifications
at the grandchild-binding level.
See also: test_expect_propspecs_multi_child_binding()
"""
binding = grandchild_binding_of("test-bindings-init/base_multi.yaml")
assert {
# From top-level "include:" element.
"grandchild-prop-1",
"grandchild-prop-2",
"grandchild-prop-enum",
# From "child-binding: include:" element.
"child-prop-1",
"child-prop-2",
"child-prop-enum",
# From "child-binding: child-binding: include:" element.
"prop-1",
"prop-2",
"prop-enum",
} == set(binding.prop2specs.keys())
# Inherited from base.yaml without modification.
propspec = binding.prop2specs["grandchild-prop-2"]
verify_expected_propspec(propspec, expect_type="string")
propspec = binding.prop2specs["grandchild-prop-enum"]
verify_expected_propspec(
propspec,
expect_type="string",
expect_enum=["GRANDCHILD_FOO", "GRANDCHILD_BAR"],
)
propspec = binding.prop2specs["child-prop-2"]
verify_expected_propspec(propspec, expect_type="string")
propspec = binding.prop2specs["child-prop-enum"]
verify_expected_propspec(propspec, expect_type="string", expect_enum=["CHILD_FOO", "CHILD_BAR"])
propspec = binding.prop2specs["grandchild-prop-1"]
verify_expected_propspec(
propspec,
expect_desc="Base grandchild-prop 1.",
# Amended in base_multi.yaml.
expect_const=0xF2,
)
propspec = binding.prop2specs["child-prop-1"]
verify_expected_propspec(
propspec,
expect_desc="Base child-prop 1.",
# Amended in base_multi.yaml.
expect_const=0xF2,
)
propspec = binding.prop2specs["prop-1"]
verify_expected_propspec(
propspec,
expect_desc="Base property 1.",
# Amended in base_amend.yaml.
expect_const=0xF2,
)
propspec = binding.prop2specs["prop-2"]
verify_expected_propspec(
propspec,
expect_type="string",
# Amended in base_amend.yaml.
expect_desc="New description (grandchild).",
)
propspec = binding.prop2specs["prop-enum"]
verify_expected_propspec(
propspec,
expect_type="string",
expect_enum=["FOO", "BAR"],
# Amended in base_amend.yaml.
expect_req=True,
expect_default="FOO",
)
def test_expect_propspecs_multi_grand_grandchild_binding() -> None:
"""Test including base bindings at multiple levels.
This test checks that we finally get the expected property specifications
at the grand-grandchild-binding level.
See also: test_expect_propspecs_multi_child_binding()
"""
binding = grandchild_binding_of("test-bindings-init/base_multi.yaml").child_binding
assert binding
assert {
# From "child-binding: include:" element.
"child-prop-1",
"child-prop-2",
"child-prop-enum",
# From "child-binding: child-binding: include:" element.
"grandchild-prop-1",
"grandchild-prop-2",
"grandchild-prop-enum",
} == set(binding.prop2specs.keys())
# Inherited from base.yaml without modification.
propspec = binding.prop2specs["child-prop-1"]
verify_expected_propspec(propspec, expect_desc="Base child-prop 1.")
propspec = binding.prop2specs["child-prop-2"]
verify_expected_propspec(propspec, expect_type="string")
propspec = binding.prop2specs["child-prop-enum"]
verify_expected_propspec(propspec, expect_type="string", expect_enum=["CHILD_FOO", "CHILD_BAR"])
propspec = binding.prop2specs["grandchild-prop-1"]
verify_expected_propspec(propspec, expect_desc="Base grandchild-prop 1.")
propspec = binding.prop2specs["grandchild-prop-2"]
verify_expected_propspec(propspec, expect_type="string")
propspec = binding.prop2specs["grandchild-prop-enum"]
verify_expected_propspec(
propspec,
expect_type="string",
expect_enum=["GRANDCHILD_FOO", "GRANDCHILD_BAR"],
)
def test_expect_propspecs_diamond_binding() -> None:
"""Test property specifications produced by diamond inheritance.
This test checks that we finally get the expected property specifications
at the binding level.
"""
binding = load_binding("test-bindings-init/diamond.yaml")
assert {
# From base.yaml, amended in base_amend.yaml (left),
# last modified in thing.yaml (right).
"prop-1",
# From base.yaml, amended in base_amend.yaml (left),
# and thing.yaml (right), last modified in diamond.yaml(top).
"prop-enum",
# From base.yaml, inherited in base_amend.yaml (left).
"prop-default",
# From thing.yaml (right).
"prop-thing",
# From diamond.yaml (top).
"prop-diamond",
} == set(binding.prop2specs.keys())
# Inherited from base.yaml without modification.
propspec = binding.prop2specs["prop-default"]
verify_expected_propspec(propspec, expect_default=1)
# Inherited from thing.yaml without modification.
propspec = binding.prop2specs["prop-thing"]
verify_expected_propspec(propspec, expect_desc="Thing property.")
# New property in diamond.yaml.
propspec = binding.prop2specs["prop-diamond"]
verify_expected_propspec(propspec)
propspec = binding.prop2specs["prop-1"]
verify_expected_propspec(
propspec,
# From base_amend.yaml.
expect_const=0xF0,
# Included first wins.
expect_desc="Overwritten description.",
# From thing.yaml.
expect_default=1,
)
propspec = binding.prop2specs["prop-enum"]
verify_expected_propspec(
propspec,
expect_type="string",
expect_enum=["FOO", "BAR"],
# From base_amend.yaml.
expect_req=True,
# From diamond.yaml.
expect_desc="Overwritten in diamond.yaml.",
expect_default="FOO",
)
def test_expect_propspecs_diamond_child_binding() -> None:
"""Test property specifications produced by diamond inheritance.
This test checks that we finally get the expected property specifications
at the child-binding level.
"""
binding = child_binding_of("test-bindings-init/diamond.yaml")
assert {
# From base.yaml, amended in base_amend.yaml (left),
# last modified in thing.yaml (right).
"child-prop-1",
# From base.yaml, amended in base_amend.yaml (left),
# and thing.yaml (right), last modified in diamond.yaml(top).
"child-prop-enum",
# From base.yaml, inherited in base_amend.yaml (left).
"child-prop-default",
# From thing.yaml (right).
"child-prop-thing",
# From diamond.yaml (top).
"child-prop-diamond",
} == set(binding.prop2specs.keys())
propspec = binding.prop2specs["child-prop-1"]
verify_expected_propspec(
propspec,
# From base_amend.yaml.
expect_const=0xF1,
# Included first wins.
expect_desc="Overwritten description (child).",
# From thing.yaml.
expect_default=2,
)
propspec = binding.prop2specs["child-prop-enum"]
verify_expected_propspec(
propspec,
expect_type="string",
expect_enum=["CHILD_FOO", "CHILD_BAR"],
# From base_amend.yaml.
# ORed with thing.yaml.
expect_req=True,
# From diamond.yaml.
expect_default="CHILD_FOO",
expect_desc="Overwritten in diamond.yaml (child).",
)
# Inherited from base.yaml without modification.
propspec = binding.prop2specs["child-prop-default"]
verify_expected_propspec(propspec, expect_default=2)
# Inherited from thing.yaml without modification.
propspec = binding.prop2specs["child-prop-thing"]
verify_expected_propspec(propspec, expect_desc="Thing child-binding property.")
# New property in diamond.yaml.
propspec = binding.prop2specs["child-prop-diamond"]
verify_expected_propspec(propspec)
def test_expect_propspecs_diamond_grandchild_binding() -> None:
"""Test property specifications produced by diamond inheritance.
This test checks that we finally get the expected property specifications
at the grandchild-binding level.
"""
binding = grandchild_binding_of("test-bindings-init/diamond.yaml")
assert {
# From base.yaml, amended in base_amend.yaml (left),
# last modified in thing.yaml (right).
"grandchild-prop-1",
# From base.yaml, amended in base_amend.yaml (left),
# last modified in diamond.yaml (top).
"grandchild-prop-enum",
# From base.yaml, inherited in base_amend.yaml (left).
"grandchild-prop-default",
# From thing.yaml (right).
"grandchild-prop-thing",
# From diamond.yaml (top).
"grandchild-prop-diamond",
} == set(binding.prop2specs.keys())
propspec = binding.prop2specs["grandchild-prop-1"]
verify_expected_propspec(
propspec,
# From base_amend.yaml.
expect_const=0xF2,
# Included first wins.
expect_desc="Overwritten description (grandchild).",
# From thing.yaml.
expect_default=3,
)
propspec = binding.prop2specs["grandchild-prop-enum"]
verify_expected_propspec(
propspec,
expect_type="string",
expect_enum=["GRANDCHILD_FOO", "GRANDCHILD_BAR"],
# From base_amend.yaml.
# ORed with thing.yaml.
expect_req=True,
# From diamond.yaml.
expect_default="GRANDCHILD_FOO",
expect_desc="Overwritten in diamond.yaml (grandchild).",
)
# Inherited from base.yaml without modification.
propspec = binding.prop2specs["grandchild-prop-default"]
verify_expected_propspec(propspec, expect_default=3)
# Inherited from thing.yaml without modification.
propspec = binding.prop2specs["grandchild-prop-thing"]
verify_expected_propspec(propspec, expect_desc="Thing grandchild-binding property.")
# New property in diamond.yaml.
propspec = binding.prop2specs["grandchild-prop-diamond"]
verify_expected_propspec(propspec)
def test_binding_description_overwrite() -> None:
"""Test whether we can overwrite a binding's description.
Included files can't overwrite the description set by the including binding
(despite that "description:" appears before "include:").
This seems consistent: the top-level binding file "wins".
"""
binding = load_binding("test-bindings-init/compat_desc.yaml")
assert binding.description == "Binding description."
binding = load_binding("test-bindings-init/compat_desc_multi.yaml")
assert binding.description == "Binding description (multi)."
def test_binding_compat_overwrite() -> None:
"""Test whether we can overwrite a binding's compatible string.
Included files can't overwrite the compatible string set by the
including binding (despite that "compatible:" appears before "include:").
This seems consistent: the top-level binding file "wins".
"""
binding = load_binding("test-bindings-init/compat_desc.yaml")
assert binding.compatible == "vnd,compat-desc"
binding = load_binding("test-bindings-init/compat_desc_multi.yaml")
assert binding.compatible == "vnd,compat-desc-multi"
def test_child_binding_description_overwrite() -> None:
"""Test whether we can overwrite a child-binding's description.
An including binding can overwrite an inherited child-binding's description.
When we include multiple files overwriting the description
at the child-binding level, the first "wins".
"""
child_binding = child_binding_of("test-bindings-init/compat_desc.yaml")
# Overwrite inherited description.
assert child_binding.description == "Child-binding description."
child_binding = child_binding_of("test-bindings-init/compat_desc_multi.yaml")
# When inherited multiple times, the first "description:" wins.
assert child_binding.description == "Child-binding description (base)."
def test_child_binding_compat_overwrite() -> None:
"""Test whether we can overwrite a child-binding's compatible string.
An including binding can overwrite an inherited child-binding's
compatible string.
When we include multiple files overwriting the compatible string
at the child-binding level, the first "wins".
"""
child_binding = child_binding_of("test-bindings-init/compat_desc.yaml")
# Overwrite inherited description.
assert child_binding.compatible == "vnd,child-compat-desc"
child_binding = child_binding_of("test-bindings-init/compat_desc_multi.yaml")
# When inherited multiple times, the first "compatible:" wins.
assert child_binding.compatible == "vnd,child-compat-desc-base"
def test_grandchild_binding_description_overwrite() -> None:
"""Test whether we can overwrite a grandchild-binding's description.
See also: test_child_binding_description_overwrite()
"""
grandchild_binding = grandchild_binding_of("test-bindings-init/compat_desc.yaml")
assert grandchild_binding.description == "Grandchild-binding description."
grandchild_binding = grandchild_binding_of("test-bindings-init/compat_desc_multi.yaml")
assert grandchild_binding.description == "Grandchild-binding description (base)."
def test_grandchild_binding_compat_overwrite() -> None:
"""Test whether we can overwrite a grandchild-binding's compatible string.
See also: test_child_binding_compat_overwrite()
"""
grandchild_binding = grandchild_binding_of("test-bindings-init/compat_desc.yaml")
# Overwrite inherited description.
assert grandchild_binding.compatible == "vnd,grandchild-compat-desc"
grandchild_binding = grandchild_binding_of("test-bindings-init/compat_desc_multi.yaml")
# When inherited multiple times, the first "compatible:" wins.
assert grandchild_binding.compatible == "vnd,grandchild-compat-desc-base"
def test_filter_inherited_propspecs_basics() -> None:
"""Test the basics of filtering properties inherited via an intermediary
binding file.
Use-case "B filters I includes X":
- X is a base binding file, specifying common properties
- I is an intermediary binding file, which includes X without modification
nor filter
- B includes I, filtering the properties it chooses to inherit
with an allowlist or a blocklist
Checks, up to the grandchild-binding level, that the properties inherited
from X via I are actually filtered as B intends to.
"""
# Binding level.
binding = load_binding("test-bindings-init/simple_allowlist.yaml")
# Allowed properties.
assert {"prop-1", "prop-2"} == set(binding.prop2specs.keys())
binding = load_binding("test-bindings-init/simple_blocklist.yaml")
# Non blocked properties.
assert {"prop-2", "prop-3"} == set(binding.prop2specs.keys())
# Child-binding level.
child_binding = child_binding_of("test-bindings-init/simple_allowlist.yaml")
# Allowed properties.
assert {"child-prop-1", "child-prop-2"} == set(child_binding.prop2specs.keys())
child_binding = child_binding_of("test-bindings-init/simple_blocklist.yaml")
# Non blocked properties.
assert {"child-prop-2", "child-prop-3"} == set(child_binding.prop2specs.keys())
# GrandChild-binding level.
grandchild_binding = grandchild_binding_of("test-bindings-init/simple_allowlist.yaml")
# Allowed properties.
assert {"grandchild-prop-1", "grandchild-prop-2"} == set(grandchild_binding.prop2specs.keys())
grandchild_binding = grandchild_binding_of("test-bindings-init/simple_blocklist.yaml")
# Non blocked properties.
assert {"grandchild-prop-2", "grandchild-prop-3"} == set(grandchild_binding.prop2specs.keys())
def test_filter_inherited_propspecs_among_allowed() -> None:
"""Test filtering properties which have been allowed by an intermediary
binding file.
Complementary to test_filter_inherited_propspecs_basics().
Use-case "B filters I filters X":
- X is a base binding file, specifying common properties
- I is an intermediary binding file, filtering the properties specified
in X with an allowlist
- B includes I, filtering the properties it chooses to inherit
also with an allowlist
Checks, up to the grandchild-binding level, that B inherits the properties
specified in X which are first allowed in I, then also allowed in B.
For that, we check that if B allows only properties that are not allowed in I,
we then end up with no property at all.
"""
binding = load_binding("test-bindings-init/filter_among_allowed.yaml")
assert not set(binding.prop2specs.keys())
assert binding.child_binding
child_binding = binding.child_binding
assert not set(child_binding.prop2specs.keys())
assert child_binding.child_binding
grandchild_binding = child_binding.child_binding
assert not set(grandchild_binding.prop2specs.keys())
def test_filter_inherited_propspecs_among_notblocked() -> None:
"""Test filtering properties which have not been blocked by an intermediary
binding file.
Complementary to test_filter_inherited_propspecs_basics().
Use-case "B filters I filters X":
- X is a base binding file, specifying common properties
- I is an intermediary binding file, filtering the properties specified
in X with a blocklist
- B includes I, filtering the properties it chooses to inherit
also with a blocklist
Checks, up to the grandchild-binding level, that B inherits the properties
specified in X which are not blocked in I, then neither blocked in B.
For that, we check that if B blocks all properties that are not blocked in I,
we then end up with no property at all.
"""
binding = load_binding("test-bindings-init/filter_among_notblocked.yaml")
assert not set(binding.prop2specs.keys())
assert binding.child_binding
child_binding = binding.child_binding
assert not set(child_binding.prop2specs.keys())
assert child_binding.child_binding
grandchild_binding = child_binding.child_binding
assert not set(grandchild_binding.prop2specs.keys())
def test_filter_inherited_propspecs_allows_notblocked() -> None:
"""Test allowing properties which have not been blocked by an intermediary
binding file.
Complementary to test_filter_inherited_propspecs_basics().
Use-case "B filters I filters X":
- X is a base binding file, specifying common properties
- I is an intermediary binding file, filtering the properties specified
in X with a blocklist
- B includes I, filtering the properties it chooses to inherit
also with an allowlist
Checks, up to the grandchild-binding level, that B inherits the properties
specified in X which are not blocked in I, then allowed in B.
Permits to verify we can apply both "property-allowlist"
and "property-blocklist" filters to the same file,
as long as they're not applied simultaneously
(i.e. within the same YAML "include:" map).
"""
binding = load_binding("test-bindings-init/filter_allows_notblocked.yaml")
assert {"prop-2"} == set(binding.prop2specs.keys())
assert binding.child_binding
child_binding = binding.child_binding
assert {"child-prop-2"} == set(child_binding.prop2specs.keys())
assert child_binding.child_binding
grandchild_binding = child_binding.child_binding
assert {"grandchild-prop-2"} == set(grandchild_binding.prop2specs.keys())
def test_invalid_binding_type_override() -> None:
"""An including binding should not try to override the "type:"
of an inherited property.
Tested up to the grandchild-binding level.
"""
with pytest.raises(edtlib.EDTError) as e:
load_binding("test-bindings-init/invalid_proptype.yaml")
assert "prop-1" in str(e)
assert "'int' replaced with 'string'" in str(e)
with pytest.raises(edtlib.EDTError) as e:
load_binding("test-bindings-init/invalid_child_proptype.yaml")
assert "child-prop-1" in str(e)
assert "'int' replaced with 'string'" in str(e)
with pytest.raises(edtlib.EDTError) as e:
load_binding("test-bindings-init/invalid_grandchild_proptype.yaml")
assert "grandchild-prop-1" in str(e)
assert "'int' replaced with 'string'" in str(e)
def test_invalid_binding_const_override() -> None:
"""An including binding should not try to override the "const:" value
in a property specification inherited from an included file.
Tested up to the grandchild-binding level.
"""
with pytest.raises(edtlib.EDTError) as e:
load_binding("test-bindings-init/invalid_propconst.yaml")
assert "prop-const" in str(e)
assert "'8' replaced with '999'" in str(e)
with pytest.raises(edtlib.EDTError) as e:
load_binding("test-bindings-init/invalid_child_propconst.yaml")
assert "child-prop-const" in str(e)
assert "'16' replaced with '999'" in str(e)
with pytest.raises(edtlib.EDTError) as e:
load_binding("test-bindings-init/invalid_grandchild_propconst.yaml")
assert "grandchild-prop-const" in str(e)
assert "'32' replaced with '999'" in str(e)
def test_invalid_binding_required_override() -> None:
"""An including binding should not try to override "required: true"
in a property specification inherited from an included file.
Tested up to the grandchild-binding level.
"""
with pytest.raises(edtlib.EDTError) as e:
load_binding("test-bindings-init/invalid_propreq.yaml")
assert "prop-req" in str(e)
assert "'True' replaced with 'False'" in str(e)
with pytest.raises(edtlib.EDTError) as e:
load_binding("test-bindings-init/invalid_child_propreq.yaml")
assert "child-prop-req" in str(e)
assert "'True' replaced with 'False'" in str(e)
with pytest.raises(edtlib.EDTError) as e:
load_binding("test-bindings-init/invalid_grandchild_propreq.yaml")
assert "grandchild-prop-req" in str(e)
assert "'True' replaced with 'False'" in str(e)
def test_invalid_binding_default_override() -> None:
"""An including binding should not try to override the "default:" value
in a property specification inherited from an included file.
Tested up to the grandchild-binding level.
"""
with pytest.raises(edtlib.EDTError) as e:
load_binding("test-bindings-init/invalid_propdefault.yaml")
assert "prop-default" in str(e)
assert "'1' replaced with '999'" in str(e)
with pytest.raises(edtlib.EDTError) as e:
load_binding("test-bindings-init/invalid_child_propdefault.yaml")
assert "child-prop-default" in str(e)
assert "'2' replaced with '999'" in str(e)
with pytest.raises(edtlib.EDTError) as e:
load_binding("test-bindings-init/invalid_grandchild_propdefault.yaml")
assert "grandchild-prop-default" in str(e)
assert "'3' replaced with '999'" in str(e)
def test_invalid_binding_enum_override() -> None:
"""An including binding should not try to override the "enum:" values
in a property specification inherited from an included file.
Tested up to the grandchild-binding level.
"""
with pytest.raises(edtlib.EDTError) as e:
load_binding("test-bindings-init/invalid_propenum.yaml")
assert "prop-enum" in str(e)
assert "'['FOO', 'BAR']' replaced with '['OTHER_FOO', 'OTHER_BAR']'" in str(e)
with pytest.raises(edtlib.EDTError) as e:
load_binding("test-bindings-init/invalid_child_propenum.yaml")
assert "child-prop-enum" in str(e)
assert "'['CHILD_FOO', 'CHILD_BAR']' replaced with '['OTHER_FOO', 'OTHER_BAR']'" in str(e)
with pytest.raises(edtlib.EDTError) as e:
load_binding("test-bindings-init/invalid_grandchild_propenum.yaml")
assert "grandchild-prop-enum" in str(e)
assert (
"'['GRANDCHILD_FOO', 'GRANDCHILD_BAR']' replaced with '['OTHER_FOO', 'OTHER_BAR']'"
in str(e)
)
def test_bindings_propspecs_consistency() -> None:
"""Verify property specifications consistency.
Consistency is recursively checked for all defined properties,
from top-level binding files down to their child bindings.
Consistency is checked with:
Binding.raw["properties"][prop] == Binding.prop2specs[prop]._raw
See verify_binding_propspecs_consistency().
"""
binding = load_binding("test-bindings-init/base_inherit.yaml")
verify_binding_propspecs_consistency(binding)
binding = load_binding("test-bindings-init/base_amend.yaml")
verify_binding_propspecs_consistency(binding)
binding = load_binding("test-bindings-init/base_multi.yaml")
verify_binding_propspecs_consistency(binding)
binding = load_binding("test-bindings-init/thing.yaml")
verify_binding_propspecs_consistency(binding)
binding = load_binding("test-bindings-init/diamond.yaml")
verify_binding_propspecs_consistency(binding)
# Borrowed from test_edtlib.py.
@contextlib.contextmanager
def _from_here() -> Generator[None, None, None]:
cwd = os.getcwd()
try:
os.chdir(os.path.dirname(__file__))
yield
finally:
os.chdir(cwd)
def _basename(path: str | None) -> str:
return os.path.basename(path or "?")