├── requirements.txt ├── tests ├── test-bindings-include │ ├── README.rst │ ├── include-invalid-type.yaml │ ├── include-2.yaml │ ├── allowlist.yaml │ ├── blocklist.yaml │ ├── include-no-list.yaml │ ├── include-no-name.yaml │ ├── empty-allowlist.yaml │ ├── empty-blocklist.yaml │ ├── intermixed.yaml │ ├── allow-not-list.yaml │ ├── block-not-list.yaml │ ├── include-invalid-keys.yaml │ ├── allow-and-blocklist.yaml │ ├── filter-child-bindings.yaml │ ├── allow-and-blocklist-child.yaml │ └── include.yaml ├── test-bindings │ ├── foo-optional.yaml │ ├── foo-required.yaml │ ├── multidir.yaml │ ├── device-on-any-bus.yaml │ ├── bar-bus.yaml │ ├── foo-bus.yaml │ ├── grandchild-2.yaml │ ├── grandchild-3.yaml │ ├── device-on-bar-bus.yaml │ ├── device-on-foo-bus.yaml │ ├── gpio-dst.yaml │ ├── false-positive.yaml │ ├── order-1.yaml │ ├── order-2.yaml │ ├── interrupt-1-cell.yaml │ ├── child.yaml │ ├── phandle-array-controller-0.yaml │ ├── gpio-src.yaml │ ├── grandchild-1.yaml │ ├── interrupt-2-cell.yaml │ ├── interrupt-3-cell.yaml │ ├── phandle-array-controller-2.yaml │ ├── phandle-array-controller-1.yaml │ ├── parent.yaml │ ├── deprecated.yaml │ ├── child-binding.yaml │ ├── child-binding-with-compat.yaml │ ├── enums.yaml │ ├── defaults.yaml │ └── props.yaml ├── test-bindings-2 │ └── multidir.yaml ├── test-wrong-bindings │ ├── wrong-phandle-array-name.yaml │ └── wrong-specifier-space-type.yaml ├── test-multidir.dts ├── test.dts ├── test_edtlib.py └── test_dtlib.py ├── .gitignore ├── src └── devicetree │ ├── __init__.py │ ├── grutils.py │ └── dtlib.py ├── doc ├── source │ ├── dtlib.rst │ ├── edtlib.rst │ ├── conf.py │ └── index.rst └── Makefile ├── tox.ini └── setup.py /requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx_rtd_theme # docs 2 | -------------------------------------------------------------------------------- /tests/test-bindings-include/README.rst: -------------------------------------------------------------------------------- 1 | This directory contains bindings used to test the 'include:' feature. 2 | -------------------------------------------------------------------------------- /tests/test-bindings/foo-optional.yaml: -------------------------------------------------------------------------------- 1 | properties: 2 | foo: 3 | type: int 4 | required: false 5 | -------------------------------------------------------------------------------- /tests/test-bindings/foo-required.yaml: -------------------------------------------------------------------------------- 1 | properties: 2 | foo: 3 | type: int 4 | required: true 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | src/devicetree.egg-info/ 3 | build/ 4 | devicetree.egg-info/ 5 | __pycache__/ 6 | .tox/ 7 | doc/build/ 8 | -------------------------------------------------------------------------------- /src/devicetree/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Nordic Semiconductor ASA 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | __all__ = ['edtlib', 'dtlib'] 5 | -------------------------------------------------------------------------------- /tests/test-bindings/multidir.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: Binding in test-bindings/ 4 | 5 | compatible: "in-dir-1" 6 | -------------------------------------------------------------------------------- /tests/test-bindings-2/multidir.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: Binding in test-bindings-2/ 4 | 5 | compatible: "in-dir-2" 6 | -------------------------------------------------------------------------------- /tests/test-bindings/device-on-any-bus.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: Device on any bus 4 | 5 | compatible: "on-any-bus" 6 | -------------------------------------------------------------------------------- /tests/test-bindings/bar-bus.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: Bar bus controller 4 | 5 | compatible: "bar-bus" 6 | 7 | bus: "bar" 8 | -------------------------------------------------------------------------------- /tests/test-bindings/foo-bus.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: Foo bus controller 4 | 5 | compatible: "foo-bus" 6 | 7 | bus: "foo" 8 | -------------------------------------------------------------------------------- /tests/test-bindings/grandchild-2.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | properties: 4 | baz: 5 | required: true 6 | type: int 7 | -------------------------------------------------------------------------------- /tests/test-bindings/grandchild-3.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | properties: 4 | qaz: 5 | required: true 6 | type: int 7 | -------------------------------------------------------------------------------- /tests/test-bindings/device-on-bar-bus.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: Device on bar bus 4 | 5 | compatible: "on-bus" 6 | 7 | on-bus: "bar" 8 | -------------------------------------------------------------------------------- /tests/test-bindings/device-on-foo-bus.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: Device on foo bus 4 | 5 | compatible: "on-bus" 6 | 7 | on-bus: "foo" 8 | -------------------------------------------------------------------------------- /tests/test-bindings-include/include-invalid-type.yaml: -------------------------------------------------------------------------------- 1 | description: | 2 | Invalid include: wrong top level type. 3 | compatible: include-invalid-type 4 | include: 5 | a-map-is-not-allowed-here: 3 6 | -------------------------------------------------------------------------------- /tests/test-bindings/gpio-dst.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: GPIO destination for mapping test 4 | 5 | compatible: "gpio-dst" 6 | 7 | gpio-cells: 8 | - val 9 | -------------------------------------------------------------------------------- /tests/test-bindings/false-positive.yaml: -------------------------------------------------------------------------------- 1 | # A file that mentions a 'compatible' string without actually implementing it. 2 | # Used to check for issues with how we optimize binding loading. 3 | 4 | # props 5 | -------------------------------------------------------------------------------- /tests/test-bindings/order-1.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: Include ordering test 4 | 5 | compatible: "order-1" 6 | 7 | include: ["foo-required.yaml", "foo-optional.yaml"] 8 | -------------------------------------------------------------------------------- /tests/test-bindings/order-2.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: Include ordering test 4 | 5 | compatible: "order-2" 6 | 7 | include: ["foo-optional.yaml", "foo-required.yaml"] 8 | -------------------------------------------------------------------------------- /tests/test-bindings-include/include-2.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: Second file for testing "intermixed" includes. 4 | compatible: include-2 5 | properties: 6 | a: 7 | type: int 8 | -------------------------------------------------------------------------------- /tests/test-bindings/interrupt-1-cell.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: Interrupt controller with one cell 4 | 5 | compatible: "interrupt-one-cell" 6 | 7 | interrupt-cells: 8 | - one 9 | -------------------------------------------------------------------------------- /tests/test-bindings-include/allowlist.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: Valid property-allowlist. 4 | compatible: allowlist 5 | include: 6 | - name: include.yaml 7 | property-allowlist: [x] 8 | -------------------------------------------------------------------------------- /tests/test-bindings-include/blocklist.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: Valid property-blocklist. 4 | compatible: blocklist 5 | include: 6 | - name: include.yaml 7 | property-blocklist: [x] 8 | -------------------------------------------------------------------------------- /tests/test-bindings/child.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | include: [grandchild-1.yaml, grandchild-2.yaml, grandchild-3.yaml] 4 | 5 | properties: 6 | bar: 7 | required: true 8 | type: int 9 | -------------------------------------------------------------------------------- /tests/test-bindings/phandle-array-controller-0.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: Controller with zero data values 4 | 5 | compatible: "phandle-array-controller-0" 6 | 7 | phandle-array-foo-cells: [] 8 | -------------------------------------------------------------------------------- /tests/test-bindings/gpio-src.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: GPIO source for mapping test 4 | 5 | compatible: "gpio-src" 6 | 7 | properties: 8 | foo-gpios: 9 | type: phandle-array 10 | -------------------------------------------------------------------------------- /tests/test-bindings/grandchild-1.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | properties: 4 | foo: 5 | required: false 6 | type: int 7 | 8 | baz: 9 | required: true 10 | type: int 11 | -------------------------------------------------------------------------------- /tests/test-bindings-include/include-no-list.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: A map element with just a name is valid, and has no filters. 4 | compatible: include-no-list 5 | include: 6 | - name: include.yaml 7 | -------------------------------------------------------------------------------- /tests/test-bindings-include/include-no-name.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: | 4 | Invalid include element: no name key is present. 5 | compatible: include-no-name 6 | include: 7 | - property-allowlist: [x] 8 | -------------------------------------------------------------------------------- /tests/test-bindings/interrupt-2-cell.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: Interrupt controller with two cells 4 | 5 | compatible: "interrupt-two-cell" 6 | 7 | interrupt-cells: 8 | - one 9 | - two 10 | -------------------------------------------------------------------------------- /tests/test-bindings-include/empty-allowlist.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: An empty property-allowlist is valid. 4 | compatible: empty-allowlist 5 | include: 6 | - name: include.yaml 7 | property-allowlist: [] 8 | -------------------------------------------------------------------------------- /tests/test-bindings-include/empty-blocklist.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: An empty property-blocklist is valid. 4 | compatible: empty-blocklist 5 | include: 6 | - name: include.yaml 7 | property-blocklist: [] 8 | -------------------------------------------------------------------------------- /tests/test-bindings/interrupt-3-cell.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: Interrupt controller with three cells 4 | 5 | compatible: "interrupt-three-cell" 6 | 7 | interrupt-cells: 8 | - one 9 | - two 10 | - three 11 | -------------------------------------------------------------------------------- /tests/test-bindings/phandle-array-controller-2.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: Controller with two data values 4 | 5 | compatible: "phandle-array-controller-2" 6 | 7 | phandle-array-foo-cells: 8 | - one 9 | - two 10 | -------------------------------------------------------------------------------- /tests/test-bindings-include/intermixed.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: Including intermixed file names and maps is valid. 4 | compatible: intermixed 5 | include: 6 | - name: include.yaml 7 | property-allowlist: [x] 8 | - include-2.yaml 9 | -------------------------------------------------------------------------------- /tests/test-bindings/phandle-array-controller-1.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: Controller with one data value 4 | 5 | compatible: "phandle-array-controller-1" 6 | 7 | phandle-array-foo-cells: 8 | - one 9 | 10 | gpio-cells: 11 | - gpio-one 12 | -------------------------------------------------------------------------------- /tests/test-wrong-bindings/wrong-phandle-array-name.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: Device.wrong_phandle_array_name test 4 | 5 | compatible: "wrong_phandle_array_name" 6 | 7 | properties: 8 | wrong-phandle-array-name: 9 | type: phandle-array 10 | -------------------------------------------------------------------------------- /tests/test-bindings-include/allow-not-list.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: | 4 | A property-allowlist, if given, must be a list. This binding should 5 | cause an error. 6 | compatible: allow-not-list 7 | include: 8 | - name: include.yaml 9 | property-allowlist: 10 | foo: 11 | -------------------------------------------------------------------------------- /tests/test-bindings-include/block-not-list.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: | 4 | A property-blocklist, if given, must be a list. This binding should 5 | cause an error. 6 | compatible: block-not-list 7 | include: 8 | - name: include.yaml 9 | property-blocklist: 10 | foo: 11 | -------------------------------------------------------------------------------- /tests/test-bindings-include/include-invalid-keys.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: | 4 | Invalid include element: invalid keys are present. 5 | compatible: include-invalid-keys 6 | include: 7 | - name: include.yaml 8 | property-allowlist: [x] 9 | bad-key-1: 3 10 | bad-key-2: 3 11 | -------------------------------------------------------------------------------- /tests/test-wrong-bindings/wrong-specifier-space-type.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: Device.wrong_specifier_space_type test 4 | 5 | compatible: "wrong_specifier_space_type" 6 | 7 | properties: 8 | wrong-type-for-specifier-space: 9 | type: phandle 10 | specifier-space: foobar 11 | -------------------------------------------------------------------------------- /tests/test-bindings/parent.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: Parent binding 4 | 5 | compatible: "binding-include-test" 6 | 7 | include: child.yaml 8 | 9 | properties: 10 | foo: 11 | # Changed from not being required in grandchild-1.yaml 12 | required: true 13 | # Type set in grandchild 14 | -------------------------------------------------------------------------------- /tests/test-bindings/deprecated.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: Property deprecated value test 4 | 5 | compatible: "test-deprecated" 6 | 7 | properties: 8 | oldprop: 9 | type: int 10 | deprecated: true 11 | required: false 12 | 13 | curprop: 14 | type: int 15 | required: false 16 | -------------------------------------------------------------------------------- /doc/source/dtlib.rst: -------------------------------------------------------------------------------- 1 | .. _dtlib: 2 | 3 | dtlib 4 | ===== 5 | 6 | This is the low-level DTS parser. 7 | 8 | .. automodule:: devicetree.dtlib 9 | .. autoclass:: devicetree.dtlib.DT 10 | :members: 11 | :special-members: __init__ 12 | .. autoclass:: devicetree.dtlib.Node 13 | .. autoclass:: devicetree.dtlib.Property 14 | .. autoclass:: devicetree.dtlib.DTError 15 | -------------------------------------------------------------------------------- /tests/test-bindings-include/allow-and-blocklist.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: | 4 | An include must not give both an allowlist and a blocklist. 5 | This binding should cause an error. 6 | compatible: allow-and-blocklist 7 | include: 8 | - name: include.yaml 9 | property-blocklist: [x] 10 | property-allowlist: [y] 11 | -------------------------------------------------------------------------------- /tests/test-bindings-include/filter-child-bindings.yaml: -------------------------------------------------------------------------------- 1 | description: Test binding for filtering 'child-binding' properties 2 | 3 | include: 4 | - name: include.yaml 5 | property-allowlist: [x] 6 | child-binding: 7 | property-blocklist: [child-prop-1] 8 | child-binding: 9 | property-allowlist: [grandchild-prop-1] 10 | 11 | compatible: filter-child-bindings 12 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist=py3 3 | 4 | [testenv] 5 | deps = 6 | setuptools-scm 7 | pytest 8 | types-PyYAML 9 | mypy 10 | setenv = 11 | TOXTEMPDIR={envtmpdir} 12 | commands = 13 | python -m pytest {posargs:tests} 14 | python -m mypy --config-file={toxinidir}/tox.ini --package=devicetree 15 | 16 | [mypy] 17 | mypy_path=src 18 | ignore_missing_imports=True 19 | 20 | -------------------------------------------------------------------------------- /tests/test-multidir.dts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Nordic Semiconductor 3 | * 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | */ 6 | 7 | // Used by testedtlib.py. Dedicated file for testing having multiple binding 8 | // directories. 9 | 10 | /dts-v1/; 11 | 12 | / { 13 | in-dir-1 { 14 | compatible = "in-dir-1"; 15 | }; 16 | in-dir-2 { 17 | compatible = "in-dir-2"; 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /tests/test-bindings-include/allow-and-blocklist-child.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: | 4 | An include must not give both an allowlist and a blocklist in a 5 | child binding. This binding should cause an error. 6 | compatible: allow-and-blocklist-child 7 | include: 8 | - name: include.yaml 9 | child-binding: 10 | property-blocklist: [x] 11 | property-allowlist: [y] 12 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | SPHINXOPTS ?= -jauto 2 | SPHINXBUILD ?= sphinx-build 3 | SOURCEDIR = source 4 | BUILDDIR = build 5 | 6 | help: 7 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 8 | 9 | .PHONY: help Makefile 10 | 11 | # Catch-all target: route all unknown targets to Sphinx using the new 12 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 13 | %: Makefile 14 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 15 | -------------------------------------------------------------------------------- /tests/test-bindings/child-binding.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: child-binding test 4 | 5 | compatible: "top-binding" 6 | 7 | child-binding: 8 | description: child node 9 | properties: 10 | child-prop: 11 | type: int 12 | required: true 13 | 14 | child-binding: 15 | description: grandchild node 16 | properties: 17 | grandchild-prop: 18 | type: int 19 | required: true 20 | -------------------------------------------------------------------------------- /tests/test-bindings-include/include.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: Test file for including other bindings 4 | compatible: include 5 | properties: 6 | x: 7 | type: int 8 | y: 9 | type: int 10 | z: 11 | type: int 12 | child-binding: 13 | properties: 14 | child-prop-1: 15 | type: int 16 | child-prop-2: 17 | type: int 18 | 19 | child-binding: 20 | properties: 21 | grandchild-prop-1: 22 | type: int 23 | grandchild-prop-2: 24 | type: int 25 | -------------------------------------------------------------------------------- /tests/test-bindings/child-binding-with-compat.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: child-binding with separate compatible than the parent 4 | 5 | compatible: "top-binding-with-compat" 6 | 7 | child-binding: 8 | compatible: child-compat 9 | description: child node 10 | properties: 11 | child-prop: 12 | type: int 13 | required: true 14 | 15 | child-binding: 16 | description: grandchild node 17 | properties: 18 | grandchild-prop: 19 | type: int 20 | required: true 21 | -------------------------------------------------------------------------------- /doc/source/edtlib.rst: -------------------------------------------------------------------------------- 1 | .. _edtlib: 2 | 3 | edtlib 4 | ====== 5 | 6 | This is the higher level representation that incorporates standard conventions 7 | for well known properties as well as extra information in Zephyr's bindings 8 | syntax. 9 | 10 | .. automodule:: devicetree.edtlib 11 | 12 | .. autoclass:: devicetree.edtlib.EDT 13 | :special-members: __init__ 14 | .. autoclass:: devicetree.edtlib.Binding 15 | :special-members: __init__ 16 | .. autoclass:: devicetree.edtlib.PropertySpec 17 | .. autoclass:: devicetree.edtlib.Node 18 | .. autoclass:: devicetree.edtlib.Property 19 | .. autoclass:: devicetree.edtlib.Register 20 | .. autoclass:: devicetree.edtlib.ControllerAndData 21 | .. autoclass:: devicetree.edtlib.PinCtrl 22 | .. autoclass:: devicetree.edtlib.EDTError 23 | -------------------------------------------------------------------------------- /tests/test-bindings/enums.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020 Nordic Semiconductor ASA 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | 4 | description: Property enum test 5 | 6 | compatible: "enums" 7 | 8 | properties: 9 | int-enum: 10 | type: int 11 | enum: 12 | - 1 13 | - 2 14 | - 3 15 | 16 | string-enum: # not tokenizable 17 | type: string 18 | enum: 19 | - foo bar 20 | - foo_bar 21 | 22 | tokenizable-lower-enum: # tokenizable in lowercase only 23 | type: string 24 | enum: 25 | - bar 26 | - BAR 27 | 28 | tokenizable-enum: # tokenizable in lower and uppercase 29 | type: string 30 | enum: 31 | - bar 32 | - whitespace is ok 33 | - 123 is ok 34 | 35 | no-enum: 36 | type: string 37 | -------------------------------------------------------------------------------- /tests/test-bindings/defaults.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: Property default value test 4 | 5 | compatible: "defaults" 6 | 7 | properties: 8 | int: 9 | type: int 10 | required: false 11 | default: 123 12 | 13 | array: 14 | type: array 15 | required: false 16 | default: [1, 2, 3] 17 | 18 | uint8-array: 19 | type: uint8-array 20 | required: false 21 | default: [0x89, 0xAB, 0xCD] 22 | 23 | string: 24 | type: string 25 | required: false 26 | default: "hello" 27 | 28 | string-array: 29 | type: string-array 30 | required: false 31 | default: ["hello", "there"] 32 | 33 | default-not-used: 34 | type: int 35 | required: false 36 | default: 123 37 | -------------------------------------------------------------------------------- /doc/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | 3 | import sphinx_rtd_theme 4 | 5 | # -- Project information ----------------------------------------------------- 6 | 7 | project = 'python-devicetree' 8 | 9 | # Placeholders based on initial provenance of the documentation. 10 | copyright = u'2021 Zephyr Project members and individual contributors' 11 | author = 'Zephyr Project' 12 | 13 | # The full version, including alpha/beta/rc tags 14 | release = '0.0.2' 15 | 16 | 17 | # -- General configuration --------------------------------------------------- 18 | 19 | extensions = [ 20 | 'sphinx.ext.autodoc', 21 | 'sphinx_rtd_theme', 22 | # Keep this sorted; don't just append new items. 23 | ] 24 | 25 | templates_path = ['_templates'] 26 | 27 | exclude_patterns = [] 28 | 29 | rst_epilog = f''' 30 | .. |release| replace:: {release} 31 | ''' 32 | 33 | # -- Options for HTML output ------------------------------------------------- 34 | 35 | html_theme = 'sphinx_rtd_theme' 36 | 37 | html_static_path = [] 38 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | python-devicetree 2 | ================= 3 | 4 | This is part of an ongoing effort to extract the Zephyr Project's 5 | `devicetree tools`_ to a standalone location for wider use. 6 | 7 | This is version |release|. It is based on the Zephyr tools as of commit 8 | `f5409dec01`_. 9 | 10 | .. _devicetree tools: 11 | https://docs.zephyrproject.org/latest/guides/dts/intro.html#scripts-and-tools 12 | 13 | .. _f5409dec01: 14 | https://github.com/zephyrproject-rtos/zephyr/commit/f5409dec01c84aa8eda1830c64309abe3d5868d0 15 | 16 | Quickstart 17 | ========== 18 | 19 | .. code-block:: sh 20 | 21 | $ pip3 install devicetree 22 | $ python3 23 | [...] 24 | >>> from devicetree import edtlib, dtlib 25 | 26 | Then you should be able to use :ref:`edtlib` and :ref:`dtlib` in the same way 27 | Zephyr does with its in-tree versions of these modules. 28 | 29 | This is meant as a way to get started. Breaking API changes to make this code 30 | easier to use standalone are possible. 31 | 32 | .. toctree:: 33 | :maxdepth: 2 34 | :hidden: 35 | 36 | dtlib 37 | edtlib 38 | -------------------------------------------------------------------------------- /tests/test-bindings/props.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | 3 | description: Device.props test 4 | 5 | compatible: "props" 6 | 7 | properties: 8 | nonexistent-boolean: 9 | type: boolean 10 | 11 | existent-boolean: 12 | type: boolean 13 | 14 | int: 15 | type: int 16 | const: 1 17 | 18 | array: 19 | type: array 20 | 21 | uint8-array: 22 | type: uint8-array 23 | 24 | string: 25 | type: string 26 | const: "foo" 27 | 28 | string-array: 29 | type: string-array 30 | 31 | phandle-ref: 32 | type: phandle 33 | 34 | phandle-refs: 35 | type: phandles 36 | 37 | phandle-array-foos: 38 | type: phandle-array 39 | 40 | phandle-array-foo-names: 41 | type: string-array 42 | 43 | # There's some slight special-casing for GPIOs in that 'foo-gpios = ...' 44 | # gets resolved to #gpio-cells rather than #foo-gpio-cells, so test that 45 | # too 46 | foo-gpios: 47 | type: phandle-array 48 | 49 | path: 50 | type: path 51 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021, Nordic Semiconductor ASA 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | import setuptools 6 | 7 | long_description = ''' 8 | Placeholder 9 | =========== 10 | 11 | This is just a placeholder for moving Zephyr's devicetree libraries 12 | to PyPI. 13 | ''' 14 | 15 | version = '0.0.2' 16 | 17 | setuptools.setup( 18 | # TBD, just use these for now. 19 | author='Zephyr Project', 20 | author_email='devel@lists.zephyrproject.org', 21 | 22 | name='devicetree', 23 | version=version, 24 | description='Python libraries for devicetree', 25 | long_description=long_description, 26 | # http://docutils.sourceforge.net/FAQ.html#what-s-the-official-mime-type-for-restructuredtext-data 27 | long_description_content_type="text/x-rst", 28 | url='https://github.com/zephyrproject-rtos/python-devicetree', 29 | packages=setuptools.find_packages(where='src'), 30 | package_dir={'': 'src'}, 31 | classifiers=[ 32 | 'Programming Language :: Python :: 3 :: Only', 33 | 'License :: OSI Approved :: Apache Software License', 34 | 'Operating System :: POSIX :: Linux', 35 | 'Operating System :: MacOS :: MacOS X', 36 | 'Operating System :: Microsoft :: Windows', 37 | ], 38 | install_requires=[ 39 | 'PyYAML>=5.1', 40 | ], 41 | python_requires='>=3.6', 42 | ) 43 | -------------------------------------------------------------------------------- /src/devicetree/grutils.py: -------------------------------------------------------------------------------- 1 | # Copyright 2009-2013, 2019 Peter A. Bigot 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | # This implementation is derived from the one in 6 | # [PyXB](https://github.com/pabigot/pyxb), stripped down and modified 7 | # specifically to manage edtlib Node instances. 8 | 9 | import collections 10 | 11 | class Graph: 12 | """ 13 | Represent a directed graph with edtlib Node objects as nodes. 14 | 15 | This is used to determine order dependencies among nodes in a 16 | devicetree. An edge from C{source} to C{target} indicates that 17 | some aspect of C{source} requires that some aspect of C{target} 18 | already be available. 19 | """ 20 | 21 | def __init__(self, root=None): 22 | self.__roots = None 23 | if root is not None: 24 | self.__roots = {root} 25 | self.__edge_map = collections.defaultdict(set) 26 | self.__reverse_map = collections.defaultdict(set) 27 | self.__nodes = set() 28 | 29 | def add_edge(self, source, target): 30 | """ 31 | Add a directed edge from the C{source} to the C{target}. 32 | 33 | The nodes are added to the graph if necessary. 34 | """ 35 | self.__edge_map[source].add(target) 36 | if source != target: 37 | self.__reverse_map[target].add(source) 38 | self.__nodes.add(source) 39 | self.__nodes.add(target) 40 | 41 | def roots(self): 42 | """ 43 | Return the set of nodes calculated to be roots (i.e., those 44 | that have no incoming edges). 45 | 46 | This caches the roots calculated in a previous invocation. 47 | 48 | @rtype: C{set} 49 | """ 50 | if not self.__roots: 51 | self.__roots = set() 52 | for n in self.__nodes: 53 | if n not in self.__reverse_map: 54 | self.__roots.add(n) 55 | return self.__roots 56 | 57 | def _tarjan(self): 58 | # Execute Tarjan's algorithm on the graph. 59 | # 60 | # Tarjan's algorithm 61 | # (http://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm) 62 | # computes the strongly-connected components 63 | # (http://en.wikipedia.org/wiki/Strongly_connected_component) 64 | # of the graph: i.e., the sets of nodes that form a minimal 65 | # closed set under edge transition. In essence, the loops. 66 | # We use this to detect groups of components that have a 67 | # dependency cycle, and to impose a total order on components 68 | # based on dependencies. 69 | 70 | self.__stack = [] 71 | self.__scc_order = [] 72 | self.__index = 0 73 | self.__tarjan_index = {} 74 | self.__tarjan_low_link = {} 75 | for v in self.__nodes: 76 | self.__tarjan_index[v] = None 77 | roots = sorted(self.roots(), key=node_key) 78 | if self.__nodes and not roots: 79 | raise Exception('TARJAN: No roots found in graph with {} nodes'.format(len(self.__nodes))) 80 | 81 | for r in roots: 82 | self._tarjan_root(r) 83 | 84 | # Assign ordinals for edtlib 85 | ordinal = 0 86 | for scc in self.__scc_order: 87 | # Zephyr customization: devicetree Node graphs should have 88 | # no loops, so all SCCs should be singletons. That may 89 | # change in the future, but for now we only give an 90 | # ordinal to singletons. 91 | if len(scc) == 1: 92 | scc[0].dep_ordinal = ordinal 93 | ordinal += 1 94 | 95 | def _tarjan_root(self, v): 96 | # Do the work of Tarjan's algorithm for a given root node. 97 | 98 | if self.__tarjan_index.get(v) is not None: 99 | # "Root" was already reached. 100 | return 101 | self.__tarjan_index[v] = self.__tarjan_low_link[v] = self.__index 102 | self.__index += 1 103 | self.__stack.append(v) 104 | source = v 105 | for target in sorted(self.__edge_map[source], key=node_key): 106 | if self.__tarjan_index[target] is None: 107 | self._tarjan_root(target) 108 | self.__tarjan_low_link[v] = min(self.__tarjan_low_link[v], self.__tarjan_low_link[target]) 109 | elif target in self.__stack: 110 | self.__tarjan_low_link[v] = min(self.__tarjan_low_link[v], self.__tarjan_low_link[target]) 111 | 112 | if self.__tarjan_low_link[v] == self.__tarjan_index[v]: 113 | scc = [] 114 | while True: 115 | scc.append(self.__stack.pop()) 116 | if v == scc[-1]: 117 | break 118 | self.__scc_order.append(scc) 119 | 120 | def scc_order(self): 121 | """Return the strongly-connected components in order. 122 | 123 | The data structure is a list, in dependency order, of strongly 124 | connected components (which can be single nodes). Appearance 125 | of a node in a set earlier in the list indicates that it has 126 | no dependencies on any node that appears in a subsequent set. 127 | This order is preferred over a depth-first-search order for 128 | code generation, since it detects loops. 129 | """ 130 | if not self.__scc_order: 131 | self._tarjan() 132 | return self.__scc_order 133 | __scc_order = None 134 | 135 | def depends_on(self, node): 136 | """Get the nodes that 'node' directly depends on.""" 137 | return sorted(self.__edge_map[node], key=node_key) 138 | 139 | def required_by(self, node): 140 | """Get the nodes that directly depend on 'node'.""" 141 | return sorted(self.__reverse_map[node], key=node_key) 142 | 143 | def node_key(node): 144 | # This sort key ensures that sibling nodes with the same name will 145 | # use unit addresses as tiebreakers. That in turn ensures ordinals 146 | # for otherwise indistinguishable siblings are in increasing order 147 | # by unit address, which is convenient for displaying output. 148 | 149 | if node.parent: 150 | parent_path = node.parent.path 151 | else: 152 | parent_path = '/' 153 | 154 | if node.unit_addr is not None: 155 | name = node.name.rsplit('@', 1)[0] 156 | unit_addr = node.unit_addr 157 | else: 158 | name = node.name 159 | unit_addr = -1 160 | 161 | return (parent_path, name, unit_addr) 162 | -------------------------------------------------------------------------------- /tests/test.dts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, Nordic Semiconductor 3 | * 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | */ 6 | 7 | // Used by testedtlib.py 8 | 9 | /dts-v1/; 10 | 11 | / { 12 | // 13 | // Interrupts 14 | // 15 | 16 | interrupt-parent-test { 17 | controller { 18 | compatible = "interrupt-three-cell"; 19 | #interrupt-cells = <3>; 20 | interrupt-controller; 21 | }; 22 | node { 23 | interrupts = <1 2 3 4 5 6>; 24 | interrupt-names = "foo", "bar"; 25 | interrupt-parent = <&{/interrupt-parent-test/controller}>; 26 | }; 27 | }; 28 | interrupts-extended-test { 29 | controller-0 { 30 | compatible = "interrupt-one-cell"; 31 | #interrupt-cells = <1>; 32 | interrupt-controller; 33 | }; 34 | controller-1 { 35 | compatible = "interrupt-two-cell"; 36 | #interrupt-cells = <2>; 37 | interrupt-controller; 38 | }; 39 | controller-2 { 40 | compatible = "interrupt-three-cell"; 41 | #interrupt-cells = <3>; 42 | interrupt-controller; 43 | }; 44 | node { 45 | interrupts-extended = < 46 | &{/interrupts-extended-test/controller-0} 1 47 | &{/interrupts-extended-test/controller-1} 2 3 48 | &{/interrupts-extended-test/controller-2} 4 5 6>; 49 | }; 50 | }; 51 | interrupt-map-test { 52 | #address-cells = <2>; 53 | #size-cells = <0>; 54 | 55 | controller-0 { 56 | compatible = "interrupt-one-cell"; 57 | #address-cells = <1>; 58 | #interrupt-cells = <1>; 59 | interrupt-controller; 60 | }; 61 | controller-1 { 62 | compatible = "interrupt-two-cell"; 63 | #address-cells = <2>; 64 | #interrupt-cells = <2>; 65 | interrupt-controller; 66 | }; 67 | controller-2 { 68 | compatible = "interrupt-three-cell"; 69 | #address-cells = <3>; 70 | #interrupt-cells = <3>; 71 | interrupt-controller; 72 | }; 73 | nexus { 74 | #interrupt-cells = <2>; 75 | interrupt-map = < 76 | 0 0 0 0 &{/interrupt-map-test/controller-0} 0 0 77 | 0 0 0 1 &{/interrupt-map-test/controller-1} 0 0 0 1 78 | 0 0 0 2 &{/interrupt-map-test/controller-2} 0 0 0 0 0 2 79 | 0 1 0 0 &{/interrupt-map-test/controller-0} 0 3 80 | 0 1 0 1 &{/interrupt-map-test/controller-1} 0 0 0 4 81 | 0 1 0 2 &{/interrupt-map-test/controller-2} 0 0 0 0 0 5>; 82 | }; 83 | node@0 { 84 | reg = <0 0>; 85 | interrupts = <0 0 0 1 0 2>; 86 | interrupt-parent = <&{/interrupt-map-test/nexus}>; 87 | }; 88 | node@1 { 89 | reg = <0 1>; 90 | interrupts-extended = < 91 | &{/interrupt-map-test/nexus} 0 0 92 | &{/interrupt-map-test/nexus} 0 1 93 | &{/interrupt-map-test/nexus} 0 2>; 94 | }; 95 | }; 96 | interrupt-map-bitops-test { 97 | #address-cells = <2>; 98 | #size-cells = <0>; 99 | 100 | controller { 101 | compatible = "interrupt-two-cell"; 102 | #address-cells = <0>; 103 | #interrupt-cells = <2>; 104 | interrupt-controller; 105 | }; 106 | nexus { 107 | #interrupt-cells = <2>; 108 | interrupt-map = < 109 | 6 6 6 6 &{/interrupt-map-bitops-test/controller} 2 1 110 | >; 111 | interrupt-map-mask = <0xE 0x7 0xE 0x7>; 112 | // Not specified in the DT spec., but shows up due to 113 | // common code with GPIO. Might as well test it here. 114 | interrupt-map-pass-thru = <1 2 3 3>; 115 | }; 116 | // Child unit specifier: 00000007 0000000E 00000007 0000000E 117 | // Mask: 0000000E 00000007 0000000E 00000007 118 | // Pass-thru: 00000001 00000002 00000003 00000003 119 | node@70000000E { 120 | reg = <0x7 0xE>; 121 | interrupt-parent = <&{/interrupt-map-bitops-test/nexus}>; 122 | interrupts = <0x7 0xE>; 123 | }; 124 | }; 125 | 126 | // 127 | // 'ranges' 128 | // 129 | 130 | ranges-zero-cells { 131 | #address-cells = <0>; 132 | 133 | node { 134 | #address-cells = <0>; 135 | #size-cells = <0>; 136 | 137 | ranges; 138 | }; 139 | }; 140 | 141 | ranges-zero-parent-cells { 142 | #address-cells = <0>; 143 | 144 | node { 145 | #address-cells = <1>; 146 | #size-cells = <0>; 147 | 148 | ranges = <0xA>, 149 | <0x1A>, 150 | <0x2A>; 151 | }; 152 | }; 153 | 154 | ranges-one-address-cells { 155 | #address-cells = <0>; 156 | 157 | node { 158 | reg = <1>; 159 | #address-cells = <1>; 160 | 161 | ranges = <0xA 0xB>, 162 | <0x1A 0x1B>, 163 | <0x2A 0x2B>; 164 | }; 165 | }; 166 | 167 | ranges-one-address-two-size-cells { 168 | #address-cells = <0>; 169 | 170 | node { 171 | reg = <1>; 172 | #address-cells = <1>; 173 | #size-cells = <2>; 174 | 175 | ranges = <0xA 0xB 0xC>, 176 | <0x1A 0x1B 0x1C>, 177 | <0x2A 0x2B 0x2C>; 178 | }; 179 | }; 180 | 181 | ranges-two-address-cells { 182 | #address-cells = <1>; 183 | 184 | node@1 { 185 | reg = <1 2>; 186 | 187 | ranges = <0xA 0xB 0xC 0xD>, 188 | <0x1A 0x1B 0x1C 0x1D>, 189 | <0x2A 0x2B 0x2C 0x2D>; 190 | }; 191 | }; 192 | 193 | ranges-two-address-two-size-cells { 194 | #address-cells = <1>; 195 | 196 | node@1 { 197 | reg = <1 2>; 198 | #size-cells = <2>; 199 | 200 | ranges = <0xA 0xB 0xC 0xD 0xE>, 201 | <0x1A 0x1B 0x1C 0x1D 0x1E>, 202 | <0x2A 0x2B 0x2C 0x2D 0x1D>; 203 | }; 204 | }; 205 | 206 | ranges-three-address-cells { 207 | node@1 { 208 | reg = <0 1 2>; 209 | #address-cells = <3>; 210 | 211 | ranges = <0xA 0xB 0xC 0xD 0xE 0xF>, 212 | <0x1A 0x1B 0x1C 0x1D 0x1E 0x1F>, 213 | <0x2A 0x2B 0x2C 0x2D 0x2E 0x2F>; 214 | }; 215 | }; 216 | 217 | ranges-three-address-two-size-cells { 218 | node@1 { 219 | reg = <0 1 2>; 220 | #address-cells = <3>; 221 | #size-cells = <2>; 222 | 223 | ranges = <0xA 0xB 0xC 0xD 0xE 0xF 0x10>, 224 | <0x1A 0x1B 0x1C 0x1D 0x1E 0x1F 0x110>, 225 | <0x2A 0x2B 0x2C 0x2D 0x2E 0x2F 0x210>; 226 | }; 227 | }; 228 | 229 | 230 | // 231 | // 'reg' 232 | // 233 | 234 | reg-zero-address-cells { 235 | #address-cells = <0>; 236 | #size-cells = <1>; 237 | 238 | node { 239 | reg = <1 2>; 240 | }; 241 | }; 242 | reg-zero-size-cells { 243 | #address-cells = <1>; 244 | #size-cells = <0>; 245 | 246 | node { 247 | reg = <1 2>; 248 | }; 249 | }; 250 | // Use implied #size-cells = <1> 251 | reg-ranges { 252 | #address-cells = <2>; 253 | 254 | parent { 255 | #address-cells = <1>; 256 | ranges = <1 0xA 0xB 1 /* 1 -> 0xA 0xB */ 257 | 2 0xC 0xD 2 /* 2..3 -> 0xC 0xD */ 258 | 4 0xE 0xF 1 /* 4 -> 0xE 0xF */ 259 | >; 260 | 261 | node { 262 | reg = <5 1 /* Matches no range */ 263 | 4 1 /* Matches third range */ 264 | 3 1 /* Matches second range */ 265 | 2 1 /* Matches second range */ 266 | 1 1 /* Matches first range */ 267 | 0 1 /* Matches no range */ 268 | >; 269 | }; 270 | }; 271 | }; 272 | // Build up <3 2 1> address with nested 'ranges' 273 | reg-nested-ranges { 274 | #address-cells = <3>; 275 | 276 | grandparent { 277 | #address-cells = <2>; 278 | #size-cells = <2>; 279 | ranges = <0 0 3 0 0 2 2>; 280 | 281 | parent { 282 | #address-cells = <1>; 283 | ranges = <0 2 0 2>; 284 | 285 | node { 286 | reg = <1 1>; 287 | }; 288 | }; 289 | }; 290 | }; 291 | 292 | // 293 | // 'pinctrl-' 294 | // 295 | 296 | pinctrl { 297 | dev { 298 | pinctrl-0 = <>; 299 | pinctrl-1 = <&{/pinctrl/pincontroller/state-1}>; 300 | pinctrl-2 = <&{/pinctrl/pincontroller/state-1} 301 | &{/pinctrl/pincontroller/state-2}>; 302 | pinctrl-names = "zero", "one", "two"; 303 | }; 304 | pincontroller { 305 | state-1 { 306 | }; 307 | state-2 { 308 | }; 309 | }; 310 | }; 311 | 312 | // 313 | // For testing hierarchy. 314 | // 315 | 316 | parent { 317 | child-1 { 318 | }; 319 | child-2 { 320 | grandchild { 321 | }; 322 | }; 323 | }; 324 | 325 | // 326 | // For testing 'include:' 327 | // 328 | 329 | binding-include { 330 | compatible = "binding-include-test"; 331 | foo = <0>; 332 | bar = <1>; 333 | baz = <2>; 334 | qaz = <3>; 335 | }; 336 | 337 | // 338 | // For testing Node.props (derived from 'properties:' in the binding) 339 | // 340 | 341 | props { 342 | compatible = "props"; 343 | existent-boolean; 344 | int = <1>; 345 | array = <1 2 3>; 346 | uint8-array = [ 12 34 ]; 347 | string = "foo"; 348 | string-array = "foo", "bar", "baz"; 349 | phandle-ref = < &{/ctrl-1} >; 350 | phandle-refs = < &{/ctrl-1} &{/ctrl-2} >; 351 | phandle-array-foos = < &{/ctrl-1} 1 &{/ctrl-2} 2 3 >; 352 | foo-gpios = < &{/ctrl-1} 1 >; 353 | path = &{/ctrl-1}; 354 | }; 355 | 356 | ctrl-1 { 357 | compatible = "phandle-array-controller-1"; 358 | #phandle-array-foo-cells = <1>; 359 | #gpio-cells = <1>; 360 | }; 361 | 362 | ctrl-2 { 363 | compatible = "phandle-array-controller-2"; 364 | #phandle-array-foo-cells = <2>; 365 | }; 366 | 367 | props-2 { 368 | compatible = "props"; 369 | phandle-array-foos = < &{/ctrl-0-1} 0 &{/ctrl-0-2} >; 370 | phandle-array-foo-names = "a", "missing", "b"; 371 | }; 372 | 373 | ctrl-0-1 { 374 | compatible = "phandle-array-controller-0"; 375 | #phandle-array-foo-cells = <0>; 376 | }; 377 | 378 | ctrl-0-2 { 379 | compatible = "phandle-array-controller-0"; 380 | #phandle-array-foo-cells = <0>; 381 | }; 382 | 383 | // 384 | // Test -map, via gpio-map 385 | // 386 | 387 | gpio-map { 388 | source { 389 | compatible = "gpio-src"; 390 | foo-gpios = <&{/gpio-map/connector} 3 4 391 | &{/gpio-map/connector} 1 2>; 392 | }; 393 | connector { 394 | #gpio-cells = <2>; 395 | // Use different data lengths for source and 396 | // destination to make it a bit trickier 397 | gpio-map = <1 2 &{/gpio-map/destination} 5 398 | 3 4 &{/gpio-map/destination} 6>; 399 | }; 400 | destination { 401 | compatible = "gpio-dst"; 402 | gpio-controller; 403 | #gpio-cells = <1>; 404 | }; 405 | }; 406 | 407 | // 408 | // For testing Node.props with 'default:' values in binding 409 | // 410 | 411 | defaults { 412 | compatible = "defaults"; 413 | // Should override the 'default:' in the binding 414 | default-not-used = <234>; 415 | }; 416 | 417 | // 418 | // For testing 'enum:' 419 | // 420 | 421 | enums { 422 | compatible = "enums"; 423 | int-enum = <1>; 424 | string-enum = "foo_bar"; 425 | tokenizable-enum = "123 is ok"; 426 | tokenizable-lower-enum = "bar"; 427 | no-enum = "baz"; 428 | }; 429 | 430 | // 431 | // For testing 'bus:' and 'on-bus:' 432 | // 433 | 434 | buses { 435 | // The 'node' nodes below will map to different bindings since 436 | // they appear on different buses 437 | foo-bus { 438 | compatible = "foo-bus"; 439 | node1 { 440 | compatible = "on-bus", "on-any-bus"; 441 | nested { 442 | compatible = "on-bus"; 443 | }; 444 | }; 445 | node2 { 446 | compatible = "on-any-bus", "on-bus"; 447 | }; 448 | }; 449 | bar-bus { 450 | compatible = "bar-bus"; 451 | node { 452 | compatible = "on-bus"; 453 | }; 454 | }; 455 | no-bus-node { 456 | compatible = "on-any-bus"; 457 | }; 458 | }; 459 | 460 | // 461 | // Node with 'child-binding:' in binding (along with a recursive 462 | // 'child-binding:') 463 | // 464 | 465 | child-binding { 466 | compatible = "top-binding"; 467 | child-1 { 468 | child-prop = <1>; 469 | grandchild { 470 | grandchild-prop = <2>; 471 | }; 472 | }; 473 | child-2 { 474 | child-prop = <3>; 475 | }; 476 | }; 477 | 478 | // 479 | // zephyr,user binding inference 480 | // 481 | 482 | zephyr,user { 483 | boolean; 484 | bytes = [81 82 83]; 485 | number = <23>; 486 | numbers = <1>, <2>, <3>; 487 | string = "text"; 488 | strings = "a", "b", "c"; 489 | handle = <&{/ctrl-1}>; 490 | phandles = <&{/ctrl-1}>, <&{/ctrl-2}>; 491 | phandle-array-foos = <&{/ctrl-2} 1 2>; 492 | }; 493 | 494 | // 495 | // For testing that neither 'include: [foo.yaml, bar.yaml]' nor 496 | // 'include: [bar.yaml, foo.yaml]' causes errors when one of the files 497 | // has 'required: true' and the other 'required: false' 498 | // 499 | 500 | include-order { 501 | node-1 { 502 | compatible = "order-1"; 503 | foo = <1>; 504 | }; 505 | node-2 { 506 | compatible = "order-2"; 507 | foo = <2>; 508 | }; 509 | }; 510 | 511 | // 512 | // For testing deprecated property 513 | // 514 | test-deprecated { 515 | compatible = "test-deprecated"; 516 | oldprop = <4>; /* deprecated property */ 517 | curprop = <5>; 518 | }; 519 | 520 | 521 | // 522 | // For testing deprecated features 523 | // 524 | 525 | deprecated { 526 | compatible = "deprecated"; 527 | required = <1>; 528 | required-2 = <2>; 529 | #foo-cells = <2>; 530 | sub-node { 531 | foos = <&{/deprecated} 1 2>; 532 | }; 533 | }; 534 | }; 535 | -------------------------------------------------------------------------------- /tests/test_edtlib.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Nordic Semiconductor ASA 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | 4 | import contextlib 5 | import io 6 | from logging import WARNING 7 | import os 8 | from pathlib import Path 9 | 10 | import pytest 11 | 12 | from devicetree import edtlib 13 | 14 | # Test suite for edtlib.py. 15 | # 16 | # Run it using pytest (https://docs.pytest.org/en/stable/usage.html): 17 | # 18 | # $ pytest testedtlib.py 19 | # 20 | # See the comment near the top of testdtlib.py for additional pytest advice. 21 | # 22 | # test.dts is the main test file. test-bindings/ and test-bindings-2/ has 23 | # bindings. The tests mostly use string comparisons via the various __repr__() 24 | # methods. 25 | 26 | HERE = os.path.dirname(__file__) 27 | 28 | @contextlib.contextmanager 29 | def from_here(): 30 | # Convenience hack to minimize diff from zephyr. 31 | cwd = os.getcwd() 32 | try: 33 | os.chdir(HERE) 34 | yield 35 | finally: 36 | os.chdir(cwd) 37 | 38 | def hpath(filename): 39 | '''Convert 'filename' to the host path syntax.''' 40 | return os.fspath(Path(filename)) 41 | 42 | def test_warnings(caplog): 43 | '''Tests for situations that should cause warnings.''' 44 | 45 | with from_here(): edtlib.EDT("test.dts", ["test-bindings"]) 46 | 47 | enums_hpath = hpath('test-bindings/enums.yaml') 48 | expected_warnings = [ 49 | f"'oldprop' is marked as deprecated in 'properties:' in {hpath('test-bindings/deprecated.yaml')} for node /test-deprecated.", 50 | "unit address and first address in 'reg' (0x1) don't match for /reg-zero-size-cells/node", 51 | "unit address and first address in 'reg' (0x5) don't match for /reg-ranges/parent/node", 52 | "unit address and first address in 'reg' (0x30000000200000001) don't match for /reg-nested-ranges/grandparent/parent/node", 53 | f"compatible 'enums' in binding '{enums_hpath}' has non-tokenizable enum for property 'string-enum': 'foo bar', 'foo_bar'", 54 | f"compatible 'enums' in binding '{enums_hpath}' has enum for property 'tokenizable-lower-enum' that is only tokenizable in lowercase: 'bar', 'BAR'", 55 | ] 56 | assert caplog.record_tuples == [('devicetree.edtlib', WARNING, warning_message) 57 | for warning_message in expected_warnings] 58 | 59 | def test_interrupts(): 60 | '''Tests for the interrupts property.''' 61 | with from_here(): 62 | edt = edtlib.EDT("test.dts", ["test-bindings"]) 63 | filenames = {i: hpath(f'test-bindings/interrupt-{i}-cell.yaml') 64 | for i in range(1, 4)} 65 | 66 | assert str(edt.get_node("/interrupt-parent-test/node").interrupts) == \ 67 | f"[, data: OrderedDict([('one', 1), ('two', 2), ('three', 3)])>, , data: OrderedDict([('one', 4), ('two', 5), ('three', 6)])>]" 68 | 69 | assert str(edt.get_node("/interrupts-extended-test/node").interrupts) == \ 70 | f"[, data: OrderedDict([('one', 1)])>, , data: OrderedDict([('one', 2), ('two', 3)])>, , data: OrderedDict([('one', 4), ('two', 5), ('three', 6)])>]" 71 | 72 | assert str(edt.get_node("/interrupt-map-test/node@0").interrupts) == \ 73 | f"[, data: OrderedDict([('one', 0)])>, , data: OrderedDict([('one', 0), ('two', 1)])>, , data: OrderedDict([('one', 0), ('two', 0), ('three', 2)])>]" 74 | 75 | assert str(edt.get_node("/interrupt-map-test/node@1").interrupts) == \ 76 | f"[, data: OrderedDict([('one', 3)])>, , data: OrderedDict([('one', 0), ('two', 4)])>, , data: OrderedDict([('one', 0), ('two', 0), ('three', 5)])>]" 77 | 78 | assert str(edt.get_node("/interrupt-map-bitops-test/node@70000000E").interrupts) == \ 79 | f"[, data: OrderedDict([('one', 3), ('two', 2)])>]" 80 | 81 | def test_ranges(): 82 | '''Tests for the ranges property''' 83 | with from_here(): 84 | edt = edtlib.EDT("test.dts", ["test-bindings"]) 85 | 86 | assert str(edt.get_node("/reg-ranges/parent").ranges) == \ 87 | "[, , ]" 88 | 89 | assert str(edt.get_node("/reg-nested-ranges/grandparent").ranges) == \ 90 | "[]" 91 | 92 | assert str(edt.get_node("/reg-nested-ranges/grandparent/parent").ranges) == \ 93 | "[]" 94 | 95 | assert str(edt.get_node("/ranges-zero-cells/node").ranges) == "[]" 96 | 97 | assert str(edt.get_node("/ranges-zero-parent-cells/node").ranges) == \ 98 | "[, , ]" 99 | 100 | assert str(edt.get_node("/ranges-one-address-cells/node").ranges) == \ 101 | "[, , ]" 102 | 103 | assert str(edt.get_node("/ranges-one-address-two-size-cells/node").ranges) == \ 104 | "[, , ]" 105 | 106 | assert str(edt.get_node("/ranges-two-address-cells/node@1").ranges) == \ 107 | "[, , ]" 108 | 109 | assert str(edt.get_node("/ranges-two-address-two-size-cells/node@1").ranges) == \ 110 | "[, , ]" 111 | 112 | assert str(edt.get_node("/ranges-three-address-cells/node@1").ranges) == \ 113 | "[, , ]" 114 | 115 | assert str(edt.get_node("/ranges-three-address-two-size-cells/node@1").ranges) == \ 116 | "[, , ]" 117 | 118 | def test_reg(): 119 | '''Tests for the regs property''' 120 | with from_here(): 121 | edt = edtlib.EDT("test.dts", ["test-bindings"]) 122 | 123 | assert str(edt.get_node("/reg-zero-address-cells/node").regs) == \ 124 | "[, ]" 125 | 126 | assert str(edt.get_node("/reg-zero-size-cells/node").regs) == \ 127 | "[, ]" 128 | 129 | assert str(edt.get_node("/reg-ranges/parent/node").regs) == \ 130 | "[, , , , , ]" 131 | 132 | assert str(edt.get_node("/reg-nested-ranges/grandparent/parent/node").regs) == \ 133 | "[]" 134 | 135 | def test_pinctrl(): 136 | '''Test 'pinctrl-'.''' 137 | with from_here(): 138 | edt = edtlib.EDT("test.dts", ["test-bindings"]) 139 | 140 | assert str(edt.get_node("/pinctrl/dev").pinctrls) == \ 141 | "[, ]>, , ]>]" 142 | 143 | def test_hierarchy(): 144 | '''Test Node.parent and Node.children''' 145 | with from_here(): 146 | edt = edtlib.EDT("test.dts", ["test-bindings"]) 147 | 148 | assert edt.get_node("/").parent is None 149 | 150 | assert str(edt.get_node("/parent/child-1").parent) == \ 151 | "" 152 | 153 | assert str(edt.get_node("/parent/child-2/grandchild").parent) == \ 154 | "" 155 | 156 | assert str(edt.get_node("/parent").children) == \ 157 | "OrderedDict([('child-1', ), ('child-2', )])" 158 | 159 | assert edt.get_node("/parent/child-1").children == {} 160 | 161 | def test_child_index(): 162 | '''Test Node.child_index.''' 163 | with from_here(): 164 | edt = edtlib.EDT("test.dts", ["test-bindings"]) 165 | 166 | parent, child_1, child_2 = [edt.get_node(path) for path in 167 | ("/parent", 168 | "/parent/child-1", 169 | "/parent/child-2")] 170 | assert parent.child_index(child_1) == 0 171 | assert parent.child_index(child_2) == 1 172 | with pytest.raises(KeyError): 173 | parent.child_index(parent) 174 | 175 | def test_include(): 176 | '''Test 'include:' and the legacy 'inherits: !include ...' in bindings''' 177 | with from_here(): 178 | edt = edtlib.EDT("test.dts", ["test-bindings"]) 179 | 180 | assert str(edt.get_node("/binding-include").description) == \ 181 | "Parent binding" 182 | 183 | assert str(edt.get_node("/binding-include").props) == \ 184 | "OrderedDict([('foo', ), ('bar', ), ('baz', ), ('qaz', )])" 185 | 186 | def test_include_filters(): 187 | '''Test property-allowlist and property-blocklist in an include.''' 188 | 189 | fname2path = {'include.yaml': 'test-bindings-include/include.yaml', 190 | 'include-2.yaml': 'test-bindings-include/include-2.yaml'} 191 | 192 | with pytest.raises(edtlib.EDTError) as e: 193 | with from_here(): 194 | edtlib.Binding("test-bindings-include/allow-and-blocklist.yaml", fname2path) 195 | assert ("should not specify both 'property-allowlist:' and 'property-blocklist:'" 196 | in str(e.value)) 197 | 198 | with pytest.raises(edtlib.EDTError) as e: 199 | with from_here(): 200 | edtlib.Binding("test-bindings-include/allow-and-blocklist-child.yaml", fname2path) 201 | assert ("should not specify both 'property-allowlist:' and 'property-blocklist:'" 202 | in str(e.value)) 203 | 204 | with pytest.raises(edtlib.EDTError) as e: 205 | with from_here(): 206 | edtlib.Binding("test-bindings-include/allow-not-list.yaml", fname2path) 207 | value_str = str(e.value) 208 | assert value_str.startswith("'property-allowlist' value") 209 | assert value_str.endswith("should be a list") 210 | 211 | with pytest.raises(edtlib.EDTError) as e: 212 | with from_here(): 213 | edtlib.Binding("test-bindings-include/block-not-list.yaml", fname2path) 214 | value_str = str(e.value) 215 | assert value_str.startswith("'property-blocklist' value") 216 | assert value_str.endswith("should be a list") 217 | 218 | with pytest.raises(edtlib.EDTError) as e: 219 | with from_here(): 220 | binding = edtlib.Binding("test-bindings-include/include-invalid-keys.yaml", fname2path) 221 | value_str = str(e.value) 222 | assert value_str.startswith( 223 | "'include:' in test-bindings-include/include-invalid-keys.yaml should not have these " 224 | "unexpected contents: ") 225 | assert 'bad-key-1' in value_str 226 | assert 'bad-key-2' in value_str 227 | 228 | with pytest.raises(edtlib.EDTError) as e: 229 | with from_here(): 230 | binding = edtlib.Binding("test-bindings-include/include-invalid-type.yaml", fname2path) 231 | value_str = str(e.value) 232 | assert value_str.startswith( 233 | "'include:' in test-bindings-include/include-invalid-type.yaml " 234 | "should be a string or list, but has type ") 235 | 236 | with pytest.raises(edtlib.EDTError) as e: 237 | with from_here(): 238 | binding = edtlib.Binding("test-bindings-include/include-no-name.yaml", fname2path) 239 | value_str = str(e.value) 240 | assert value_str.startswith("'include:' element") 241 | assert value_str.endswith( 242 | "in test-bindings-include/include-no-name.yaml should have a 'name' key") 243 | 244 | with from_here(): 245 | binding = edtlib.Binding("test-bindings-include/allowlist.yaml", fname2path) 246 | assert set(binding.prop2specs.keys()) == {'x'} # 'x' is allowed 247 | 248 | binding = edtlib.Binding("test-bindings-include/empty-allowlist.yaml", fname2path) 249 | assert set(binding.prop2specs.keys()) == set() # nothing is allowed 250 | 251 | binding = edtlib.Binding("test-bindings-include/blocklist.yaml", fname2path) 252 | assert set(binding.prop2specs.keys()) == {'y', 'z'} # 'x' is blocked 253 | 254 | binding = edtlib.Binding("test-bindings-include/empty-blocklist.yaml", fname2path) 255 | assert set(binding.prop2specs.keys()) == {'x', 'y', 'z'} # nothing is blocked 256 | 257 | binding = edtlib.Binding("test-bindings-include/intermixed.yaml", fname2path) 258 | assert set(binding.prop2specs.keys()) == {'x', 'a'} 259 | 260 | binding = edtlib.Binding("test-bindings-include/include-no-list.yaml", fname2path) 261 | assert set(binding.prop2specs.keys()) == {'x', 'y', 'z'} 262 | 263 | binding = edtlib.Binding("test-bindings-include/filter-child-bindings.yaml", fname2path) 264 | child = binding.child_binding 265 | grandchild = child.child_binding 266 | assert set(binding.prop2specs.keys()) == {'x'} 267 | assert set(child.prop2specs.keys()) == {'child-prop-2'} 268 | assert set(grandchild.prop2specs.keys()) == {'grandchild-prop-1'} 269 | 270 | 271 | def test_bus(): 272 | '''Test 'bus:' and 'on-bus:' in bindings''' 273 | with from_here(): 274 | edt = edtlib.EDT("test.dts", ["test-bindings"]) 275 | 276 | assert edt.get_node("/buses/foo-bus").bus == "foo" 277 | 278 | # foo-bus does not itself appear on a bus 279 | assert edt.get_node("/buses/foo-bus").on_bus is None 280 | assert edt.get_node("/buses/foo-bus").bus_node is None 281 | 282 | # foo-bus/node1 is not a bus node... 283 | assert edt.get_node("/buses/foo-bus/node1").bus is None 284 | # ...but is on a bus 285 | assert edt.get_node("/buses/foo-bus/node1").on_bus == "foo" 286 | assert edt.get_node("/buses/foo-bus/node1").bus_node.path == \ 287 | "/buses/foo-bus" 288 | 289 | # foo-bus/node2 is not a bus node... 290 | assert edt.get_node("/buses/foo-bus/node2").bus is None 291 | # ...but is on a bus 292 | assert edt.get_node("/buses/foo-bus/node2").on_bus == "foo" 293 | 294 | # no-bus-node is not a bus node... 295 | assert edt.get_node("/buses/no-bus-node").bus is None 296 | # ... and is not on a bus 297 | assert edt.get_node("/buses/no-bus-node").on_bus is None 298 | 299 | # Same compatible string, but different bindings from being on different 300 | # buses 301 | assert str(edt.get_node("/buses/foo-bus/node1").binding_path) == \ 302 | hpath("test-bindings/device-on-foo-bus.yaml") 303 | assert str(edt.get_node("/buses/foo-bus/node2").binding_path) == \ 304 | hpath("test-bindings/device-on-any-bus.yaml") 305 | assert str(edt.get_node("/buses/bar-bus/node").binding_path) == \ 306 | hpath("test-bindings/device-on-bar-bus.yaml") 307 | assert str(edt.get_node("/buses/no-bus-node").binding_path) == \ 308 | hpath("test-bindings/device-on-any-bus.yaml") 309 | 310 | # foo-bus/node/nested also appears on the foo-bus bus 311 | assert edt.get_node("/buses/foo-bus/node1/nested").on_bus == "foo" 312 | assert str(edt.get_node("/buses/foo-bus/node1/nested").binding_path) == \ 313 | hpath("test-bindings/device-on-foo-bus.yaml") 314 | 315 | def test_child_binding(): 316 | '''Test 'child-binding:' in bindings''' 317 | with from_here(): 318 | edt = edtlib.EDT("test.dts", ["test-bindings"]) 319 | child1 = edt.get_node("/child-binding/child-1") 320 | child2 = edt.get_node("/child-binding/child-2") 321 | grandchild = edt.get_node("/child-binding/child-1/grandchild") 322 | 323 | assert str(child1.binding_path) == hpath("test-bindings/child-binding.yaml") 324 | assert str(child1.description) == "child node" 325 | assert str(child1.props) == "OrderedDict([('child-prop', )])" 326 | 327 | assert str(child2.binding_path) == hpath("test-bindings/child-binding.yaml") 328 | assert str(child2.description) == "child node" 329 | assert str(child2.props) == "OrderedDict([('child-prop', )])" 330 | 331 | assert str(grandchild.binding_path) == hpath("test-bindings/child-binding.yaml") 332 | assert str(grandchild.description) == "grandchild node" 333 | assert str(grandchild.props) == "OrderedDict([('grandchild-prop', )])" 334 | 335 | with from_here(): 336 | binding_file = Path("test-bindings/child-binding.yaml").resolve() 337 | top = edtlib.Binding(binding_file, {}) 338 | child = top.child_binding 339 | assert Path(top.path) == binding_file 340 | assert Path(child.path) == binding_file 341 | assert top.compatible == 'top-binding' 342 | assert child.compatible is None 343 | 344 | with from_here(): 345 | binding_file = Path("test-bindings/child-binding-with-compat.yaml").resolve() 346 | top = edtlib.Binding(binding_file, {}) 347 | child = top.child_binding 348 | assert Path(top.path) == binding_file 349 | assert Path(child.path) == binding_file 350 | assert top.compatible == 'top-binding-with-compat' 351 | assert child.compatible == 'child-compat' 352 | 353 | def test_props(): 354 | '''Test Node.props (derived from DT and 'properties:' in the binding)''' 355 | with from_here(): 356 | edt = edtlib.EDT("test.dts", ["test-bindings"]) 357 | filenames = {i: hpath(f'test-bindings/phandle-array-controller-{i}.yaml') 358 | for i in range(0, 4)} 359 | 360 | assert str(edt.get_node("/props").props["int"]) == \ 361 | "" 362 | 363 | assert str(edt.get_node("/props").props["existent-boolean"]) == \ 364 | "" 365 | 366 | assert str(edt.get_node("/props").props["nonexistent-boolean"]) == \ 367 | "" 368 | 369 | assert str(edt.get_node("/props").props["array"]) == \ 370 | "" 371 | 372 | assert str(edt.get_node("/props").props["uint8-array"]) == \ 373 | r"" 374 | 375 | assert str(edt.get_node("/props").props["string"]) == \ 376 | "" 377 | 378 | assert str(edt.get_node("/props").props["string-array"]) == \ 379 | "" 380 | 381 | assert str(edt.get_node("/props").props["phandle-ref"]) == \ 382 | f">" 383 | 384 | assert str(edt.get_node("/props").props["phandle-refs"]) == \ 385 | f", ]>" 386 | 387 | assert str(edt.get_node("/props").props["phandle-array-foos"]) == \ 388 | f", data: OrderedDict([('one', 1)])>, , data: OrderedDict([('one', 2), ('two', 3)])>]>" 389 | 390 | assert str(edt.get_node("/props-2").props["phandle-array-foos"]) == \ 391 | (", data: OrderedDict()>, " 393 | "None, " 394 | f", data: OrderedDict()>]>") 395 | 396 | assert str(edt.get_node("/props").props["foo-gpios"]) == \ 397 | f", data: OrderedDict([('gpio-one', 1)])>]>" 398 | 399 | assert str(edt.get_node("/props").props["path"]) == \ 400 | f">" 401 | 402 | def test_nexus(): 403 | '''Test -map via gpio-map (the most common case).''' 404 | with from_here(): 405 | edt = edtlib.EDT("test.dts", ["test-bindings"]) 406 | filename = hpath('test-bindings/gpio-dst.yaml') 407 | 408 | assert str(edt.get_node("/gpio-map/source").props["foo-gpios"]) == \ 409 | f", data: OrderedDict([('val', 6)])>, , data: OrderedDict([('val', 5)])>]>" 410 | 411 | assert str(edt.get_node("/gpio-map/source").props["foo-gpios"].val[0].basename) == f"gpio" 412 | 413 | def test_prop_defaults(): 414 | '''Test property default values given in bindings''' 415 | with from_here(): 416 | edt = edtlib.EDT("test.dts", ["test-bindings"]) 417 | 418 | assert str(edt.get_node("/defaults").props) == \ 419 | r"OrderedDict([('int', ), ('array', ), ('uint8-array', ), ('string', ), ('string-array', ), ('default-not-used', )])" 420 | 421 | def test_prop_enums(): 422 | '''test properties with enum: in the binding''' 423 | 424 | with from_here(): 425 | edt = edtlib.EDT("test.dts", ["test-bindings"]) 426 | props = edt.get_node('/enums').props 427 | int_enum = props['int-enum'] 428 | string_enum = props['string-enum'] 429 | tokenizable_enum = props['tokenizable-enum'] 430 | tokenizable_lower_enum = props['tokenizable-lower-enum'] 431 | no_enum = props['no-enum'] 432 | 433 | assert int_enum.val == 1 434 | assert int_enum.enum_index == 0 435 | assert not int_enum.spec.enum_tokenizable 436 | assert not int_enum.spec.enum_upper_tokenizable 437 | 438 | assert string_enum.val == 'foo_bar' 439 | assert string_enum.enum_index == 1 440 | assert not string_enum.spec.enum_tokenizable 441 | assert not string_enum.spec.enum_upper_tokenizable 442 | 443 | assert tokenizable_enum.val == '123 is ok' 444 | assert tokenizable_enum.val_as_token == '123_is_ok' 445 | assert tokenizable_enum.enum_index == 2 446 | assert tokenizable_enum.spec.enum_tokenizable 447 | assert tokenizable_enum.spec.enum_upper_tokenizable 448 | 449 | assert tokenizable_lower_enum.val == 'bar' 450 | assert tokenizable_lower_enum.val_as_token == 'bar' 451 | assert tokenizable_lower_enum.enum_index == 0 452 | assert tokenizable_lower_enum.spec.enum_tokenizable 453 | assert not tokenizable_lower_enum.spec.enum_upper_tokenizable 454 | 455 | assert no_enum.enum_index is None 456 | assert not no_enum.spec.enum_tokenizable 457 | assert not no_enum.spec.enum_upper_tokenizable 458 | 459 | def test_binding_inference(): 460 | '''Test inferred bindings for special zephyr-specific nodes.''' 461 | warnings = io.StringIO() 462 | with from_here(): 463 | edt = edtlib.EDT("test.dts", ["test-bindings"], warnings) 464 | 465 | assert str(edt.get_node("/zephyr,user").props) == r"OrderedDict()" 466 | 467 | with from_here(): 468 | edt = edtlib.EDT("test.dts", ["test-bindings"], warnings, 469 | infer_binding_for_paths=["/zephyr,user"]) 470 | filenames = {i: hpath(f'test-bindings/phandle-array-controller-{i}.yaml') 471 | for i in range(1, 3)} 472 | 473 | assert str(edt.get_node("/zephyr,user").props) == \ 474 | rf"OrderedDict([('boolean', ), ('bytes', ), ('number', ), ('numbers', ), ('string', ), ('strings', ), ('handle', >), ('phandles', , ]>), ('phandle-array-foos', , data: OrderedDict([('one', 1), ('two', 2)])>]>)])" 475 | 476 | def test_multi_bindings(): 477 | '''Test having multiple directories with bindings''' 478 | with from_here(): 479 | edt = edtlib.EDT("test-multidir.dts", ["test-bindings", "test-bindings-2"]) 480 | 481 | assert str(edt.get_node("/in-dir-1").binding_path) == \ 482 | hpath("test-bindings/multidir.yaml") 483 | 484 | assert str(edt.get_node("/in-dir-2").binding_path) == \ 485 | hpath("test-bindings-2/multidir.yaml") 486 | 487 | def test_dependencies(): 488 | ''''Test dependency relations''' 489 | with from_here(): 490 | edt = edtlib.EDT("test-multidir.dts", ["test-bindings", "test-bindings-2"]) 491 | 492 | assert edt.get_node("/").dep_ordinal == 0 493 | assert edt.get_node("/in-dir-1").dep_ordinal == 1 494 | assert edt.get_node("/") in edt.get_node("/in-dir-1").depends_on 495 | assert edt.get_node("/in-dir-1") in edt.get_node("/").required_by 496 | 497 | def test_slice_errs(tmp_path): 498 | '''Test error messages from the internal _slice() helper''' 499 | 500 | dts_file = tmp_path / "error.dts" 501 | 502 | verify_error(""" 503 | /dts-v1/; 504 | 505 | / { 506 | #address-cells = <1>; 507 | #size-cells = <2>; 508 | 509 | sub { 510 | reg = <3>; 511 | }; 512 | }; 513 | """, 514 | dts_file, 515 | f"'reg' property in has length 4, which is not evenly divisible by 12 (= 4*(<#address-cells> (= 1) + <#size-cells> (= 2))). Note that #*-cells properties come either from the parent node or from the controller (in the case of 'interrupts').") 516 | 517 | verify_error(""" 518 | /dts-v1/; 519 | 520 | / { 521 | sub { 522 | interrupts = <1>; 523 | interrupt-parent = < &{/controller} >; 524 | }; 525 | controller { 526 | interrupt-controller; 527 | #interrupt-cells = <2>; 528 | }; 529 | }; 530 | """, 531 | dts_file, 532 | f"'interrupts' property in has length 4, which is not evenly divisible by 8 (= 4*<#interrupt-cells>). Note that #*-cells properties come either from the parent node or from the controller (in the case of 'interrupts').") 533 | 534 | verify_error(""" 535 | /dts-v1/; 536 | 537 | / { 538 | #address-cells = <1>; 539 | 540 | sub-1 { 541 | #address-cells = <2>; 542 | #size-cells = <3>; 543 | ranges = <4 5>; 544 | 545 | sub-2 { 546 | reg = <1 2 3 4 5>; 547 | }; 548 | }; 549 | }; 550 | """, 551 | dts_file, 552 | f"'ranges' property in has length 8, which is not evenly divisible by 24 (= 4*(<#address-cells> (= 2) + <#address-cells for parent> (= 1) + <#size-cells> (= 3))). Note that #*-cells properties come either from the parent node or from the controller (in the case of 'interrupts').") 553 | 554 | def test_bad_compatible(tmp_path): 555 | # An invalid compatible should cause an error, even on a node with 556 | # no binding. 557 | 558 | dts_file = tmp_path / "error.dts" 559 | 560 | verify_error(""" 561 | /dts-v1/; 562 | 563 | / { 564 | foo { 565 | compatible = "no, whitespace"; 566 | }; 567 | }; 568 | """, 569 | dts_file, 570 | r"node '/foo' compatible 'no, whitespace' must match this regular expression: '^[a-zA-Z][a-zA-Z0-9,+\-._]+$'") 571 | 572 | def test_wrong_props(): 573 | '''Test Node.wrong_props (derived from DT and 'properties:' in the binding)''' 574 | 575 | with from_here(): 576 | with pytest.raises(edtlib.EDTError) as e: 577 | edtlib.Binding("test-wrong-bindings/wrong-specifier-space-type.yaml", None) 578 | assert ("'specifier-space' in 'properties: wrong-type-for-specifier-space' has type 'phandle', expected 'phandle-array'" 579 | in str(e.value)) 580 | 581 | with pytest.raises(edtlib.EDTError) as e: 582 | edtlib.Binding("test-wrong-bindings/wrong-phandle-array-name.yaml", None) 583 | value_str = str(e.value) 584 | assert value_str.startswith("'wrong-phandle-array-name' in 'properties:'") 585 | assert value_str.endswith("but no 'specifier-space' was provided.") 586 | 587 | 588 | def verify_error(dts, dts_file, expected_err): 589 | # Verifies that parsing a file 'dts_file' with the contents 'dts' 590 | # (a string) raises an EDTError with the message 'expected_err'. 591 | # 592 | # The path 'dts_file' is written with the string 'dts' before the 593 | # test is run. 594 | 595 | with open(dts_file, "w", encoding="utf-8") as f: 596 | f.write(dts) 597 | f.flush() # Can't have unbuffered text IO, so flush() instead 598 | 599 | with pytest.raises(edtlib.EDTError) as e: 600 | edtlib.EDT(dts_file, []) 601 | 602 | assert str(e.value) == expected_err 603 | -------------------------------------------------------------------------------- /tests/test_dtlib.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019, Nordic Semiconductor 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | 4 | import contextlib 5 | import os 6 | import re 7 | import tempfile 8 | 9 | import pytest 10 | 11 | from devicetree import dtlib 12 | 13 | # Test suite for dtlib.py. 14 | # 15 | # Run it using pytest (https://docs.pytest.org/en/stable/usage.html): 16 | # 17 | # $ pytest tests/test_dtlib.py 18 | # 19 | # Extra options you can pass to pytest for debugging: 20 | # 21 | # - to stop on the first failure with shorter traceback output, 22 | # use '-x --tb=native' 23 | # - to drop into a debugger on failure, use '--pdb' 24 | # - to run a particular test function or functions, use 25 | # '-k test_function_pattern_goes_here' 26 | 27 | def parse(dts, include_path=(), **kwargs): 28 | '''Parse a DTS string 'dts', using the given include path. 29 | 30 | Any kwargs are passed on to DT().''' 31 | 32 | fd, path = tempfile.mkstemp(prefix='pytest-', suffix='.dts') 33 | try: 34 | os.write(fd, dts.encode('utf-8')) 35 | return dtlib.DT(path, include_path, **kwargs) 36 | finally: 37 | os.close(fd) 38 | os.unlink(path) 39 | 40 | def verify_parse(dts, expected, include_path=()): 41 | '''Like parse(), but also verifies that the parsed DT object's string 42 | representation is expected[1:-1]. 43 | 44 | The [1:] is so that the first line can be put on a separate line 45 | after triple quotes, as is done below.''' 46 | 47 | dt = parse(dts[1:], include_path) 48 | 49 | actual = str(dt) 50 | expected = expected[1:-1] 51 | assert actual == expected, f'unexpected round-trip on {dts}' 52 | 53 | return dt 54 | 55 | def verify_error(dts, expected_msg): 56 | '''Verify that parsing 'dts' results in a DTError with the 57 | given error message 'msg'. The message must match exactly.''' 58 | 59 | with pytest.raises(dtlib.DTError) as e: 60 | parse(dts[1:]) 61 | actual_msg = str(e.value) 62 | assert actual_msg == expected_msg, f'wrong error from {dts}' 63 | 64 | def verify_error_endswith(dts, expected_msg): 65 | ''' 66 | Like verify_error(), but checks the message ends with 67 | 'expected_msg' instead of checking for strict equality. 68 | ''' 69 | 70 | with pytest.raises(dtlib.DTError) as e: 71 | parse(dts[1:]) 72 | actual_msg = str(e.value) 73 | assert actual_msg.endswith(expected_msg), f'wrong error from {dts}' 74 | 75 | def verify_error_matches(dts, expected_re): 76 | ''' 77 | Like verify_error(), but checks the message fully matches regular 78 | expression 'expected_re' instead of checking for strict equality. 79 | ''' 80 | 81 | with pytest.raises(dtlib.DTError) as e: 82 | parse(dts[1:]) 83 | actual_msg = str(e.value) 84 | assert re.fullmatch(expected_re, actual_msg), \ 85 | f'wrong error from {dts}' \ 86 | f'actual message:\n{actual_msg!r}\n' \ 87 | f'does not match:\n{expected_re!r}' 88 | 89 | @contextlib.contextmanager 90 | def temporary_chdir(dirname): 91 | '''A context manager that changes directory to 'dirname'. 92 | 93 | The current working directory is unconditionally returned to its 94 | present location after the context manager exits. 95 | ''' 96 | here = os.getcwd() 97 | try: 98 | os.chdir(dirname) 99 | yield 100 | finally: 101 | os.chdir(here) 102 | 103 | def test_invalid_nodenames(): 104 | # Regression test that verifies node names are not matched against 105 | # the more permissive set of rules used for property names. 106 | 107 | verify_error_endswith(""" 108 | /dts-v1/; 109 | / { node? {}; }; 110 | """, 111 | "/node?: bad character '?' in node name") 112 | 113 | def test_cell_parsing(): 114 | '''Miscellaneous properties containing zero or more cells''' 115 | 116 | verify_parse(""" 117 | /dts-v1/; 118 | 119 | / { 120 | a; 121 | b = < >; 122 | c = [ ]; 123 | d = < 10 20 >; 124 | e = < 0U 1L 2UL 3LL 4ULL >; 125 | f = < 0x10 0x20 >; 126 | g = < 010 020 >; 127 | h = /bits/ 8 < 0x10 0x20 (-1) >; 128 | i = /bits/ 16 < 0x10 0x20 (-1) >; 129 | j = /bits/ 32 < 0x10 0x20 (-1) >; 130 | k = /bits/ 64 < 0x10 0x20 (-1) >; 131 | l = < 'a' 'b' 'c' >; 132 | }; 133 | """, 134 | """ 135 | /dts-v1/; 136 | 137 | / { 138 | a; 139 | b; 140 | c; 141 | d = < 0xa 0x14 >; 142 | e = < 0x0 0x1 0x2 0x3 0x4 >; 143 | f = < 0x10 0x20 >; 144 | g = < 0x8 0x10 >; 145 | h = [ 10 20 FF ]; 146 | i = /bits/ 16 < 0x10 0x20 0xffff >; 147 | j = < 0x10 0x20 0xffffffff >; 148 | k = /bits/ 64 < 0x10 0x20 0xffffffffffffffff >; 149 | l = < 0x61 0x62 0x63 >; 150 | }; 151 | """) 152 | 153 | verify_error_endswith(""" 154 | /dts-v1/; 155 | 156 | / { 157 | a = /bits/ 16 < 0x10000 >; 158 | }; 159 | """, 160 | ":4 (column 18): parse error: 65536 does not fit in 16 bits") 161 | 162 | verify_error_endswith(""" 163 | /dts-v1/; 164 | 165 | / { 166 | a = < 0x100000000 >; 167 | }; 168 | """, 169 | ":4 (column 8): parse error: 4294967296 does not fit in 32 bits") 170 | 171 | verify_error_endswith(""" 172 | /dts-v1/; 173 | 174 | / { 175 | a = /bits/ 128 < 0 >; 176 | }; 177 | """, 178 | ":4 (column 13): parse error: expected 8, 16, 32, or 64") 179 | 180 | def test_bytes_parsing(): 181 | '''Properties with byte array values''' 182 | 183 | verify_parse(""" 184 | /dts-v1/; 185 | 186 | / { 187 | a = [ ]; 188 | b = [ 12 34 ]; 189 | c = [ 1234 ]; 190 | }; 191 | """, 192 | """ 193 | /dts-v1/; 194 | 195 | / { 196 | a; 197 | b = [ 12 34 ]; 198 | c = [ 12 34 ]; 199 | }; 200 | """) 201 | 202 | verify_error_endswith(""" 203 | /dts-v1/; 204 | 205 | / { 206 | a = [ 123 ]; 207 | }; 208 | """, 209 | ":4 (column 10): parse error: expected two-digit byte or ']'") 210 | 211 | def test_string_parsing(): 212 | '''Properties with string values''' 213 | 214 | verify_parse(r""" 215 | /dts-v1/; 216 | 217 | / { 218 | a = ""; 219 | b = "ABC"; 220 | c = "\\\"\xab\377\a\b\t\n\v\f\r"; 221 | }; 222 | """, 223 | r""" 224 | /dts-v1/; 225 | 226 | / { 227 | a = ""; 228 | b = "ABC"; 229 | c = "\\\"\xab\xff\a\b\t\n\v\f\r"; 230 | }; 231 | """) 232 | 233 | verify_error_endswith(r""" 234 | /dts-v1/; 235 | 236 | / { 237 | a = "\400"; 238 | }; 239 | """, 240 | ":4 (column 6): parse error: octal escape out of range (> 255)") 241 | 242 | def test_char_literal_parsing(): 243 | '''Properties with character literal values''' 244 | 245 | verify_parse(r""" 246 | /dts-v1/; 247 | 248 | / { 249 | a = < '\'' >; 250 | b = < '\x12' >; 251 | }; 252 | """, 253 | """ 254 | /dts-v1/; 255 | 256 | / { 257 | a = < 0x27 >; 258 | b = < 0x12 >; 259 | }; 260 | """) 261 | 262 | verify_error_endswith(""" 263 | /dts-v1/; 264 | 265 | / { 266 | // Character literals are not allowed at the top level 267 | a = 'x'; 268 | }; 269 | """, 270 | ":5 (column 6): parse error: malformed value") 271 | 272 | verify_error_endswith(""" 273 | /dts-v1/; 274 | 275 | / { 276 | a = < '' >; 277 | }; 278 | """, 279 | ":4 (column 7): parse error: character literals must be length 1") 280 | 281 | verify_error_endswith(""" 282 | /dts-v1/; 283 | 284 | / { 285 | a = < '12' >; 286 | }; 287 | """, 288 | ":4 (column 7): parse error: character literals must be length 1") 289 | 290 | def test_incbin(tmp_path): 291 | '''Test /incbin/, an undocumented feature that allows for 292 | binary file inclusion. 293 | 294 | https://github.com/dgibson/dtc/commit/e37ec7d5889fa04047daaa7a4ff55150ed7954d4''' 295 | 296 | open(tmp_path / "tmp_bin", "wb").write(b"\00\01\02\03") 297 | 298 | verify_parse(f""" 299 | /dts-v1/; 300 | 301 | / {{ 302 | a = /incbin/ ("{tmp_path}/tmp_bin"); 303 | b = /incbin/ ("{tmp_path}/tmp_bin", 1, 1); 304 | c = /incbin/ ("{tmp_path}/tmp_bin", 1, 2); 305 | }}; 306 | """, 307 | """ 308 | /dts-v1/; 309 | 310 | / { 311 | a = [ 00 01 02 03 ]; 312 | b = [ 01 ]; 313 | c = [ 01 02 ]; 314 | }; 315 | """) 316 | 317 | verify_parse(""" 318 | /dts-v1/; 319 | 320 | / { 321 | a = /incbin/ ("tmp_bin"); 322 | }; 323 | """, 324 | """ 325 | /dts-v1/; 326 | 327 | / { 328 | a = [ 00 01 02 03 ]; 329 | }; 330 | """, 331 | include_path=(tmp_path,)) 332 | 333 | verify_error_endswith(r""" 334 | /dts-v1/; 335 | 336 | / { 337 | a = /incbin/ ("missing"); 338 | }; 339 | """, 340 | ":4 (column 25): parse error: 'missing' could not be found") 341 | 342 | def test_node_merging(): 343 | ''' 344 | Labels and properties specified for the same node in different 345 | statements should be merged. 346 | ''' 347 | 348 | verify_parse(""" 349 | /dts-v1/; 350 | 351 | / { 352 | l1: l2: l1: foo { 353 | foo1 = [ 01 ]; 354 | l4: l5: bar { 355 | bar1 = [ 01 ]; 356 | }; 357 | }; 358 | }; 359 | 360 | l3: &l1 { 361 | foo2 = [ 02 ]; 362 | l6: l7: bar { 363 | bar2 = [ 02 ]; 364 | }; 365 | }; 366 | 367 | &l3 { 368 | foo3 = [ 03 ]; 369 | }; 370 | 371 | &{/foo} { 372 | foo4 = [ 04 ]; 373 | }; 374 | 375 | &{/foo/bar} { 376 | bar3 = [ 03 ]; 377 | l8: baz {}; 378 | }; 379 | 380 | / { 381 | }; 382 | 383 | / { 384 | top = [ 01 ]; 385 | }; 386 | """, 387 | """ 388 | /dts-v1/; 389 | 390 | / { 391 | top = [ 01 ]; 392 | l1: l2: l3: foo { 393 | foo1 = [ 01 ]; 394 | foo2 = [ 02 ]; 395 | foo3 = [ 03 ]; 396 | foo4 = [ 04 ]; 397 | l4: l5: l6: l7: bar { 398 | bar1 = [ 01 ]; 399 | bar2 = [ 02 ]; 400 | bar3 = [ 03 ]; 401 | l8: baz { 402 | }; 403 | }; 404 | }; 405 | }; 406 | """) 407 | 408 | verify_error_endswith(""" 409 | /dts-v1/; 410 | 411 | / { 412 | }; 413 | 414 | &missing { 415 | }; 416 | """, 417 | ":6 (column 1): parse error: undefined node label 'missing'") 418 | 419 | verify_error_endswith(""" 420 | /dts-v1/; 421 | 422 | / { 423 | }; 424 | 425 | &{foo} { 426 | }; 427 | """, 428 | ":6 (column 1): parse error: node path 'foo' does not start with '/'") 429 | 430 | verify_error_endswith(""" 431 | /dts-v1/; 432 | 433 | / { 434 | }; 435 | 436 | &{/foo} { 437 | }; 438 | """, 439 | ":6 (column 1): parse error: component 'foo' in path '/foo' does not exist") 440 | 441 | def test_property_labels(): 442 | '''Like nodes, properties can have labels too.''' 443 | 444 | def verify_label2prop(label, expected): 445 | actual = dt.label2prop[label].name 446 | assert actual == expected, f"label '{label}' mapped to wrong property" 447 | 448 | dt = verify_parse(""" 449 | /dts-v1/; 450 | 451 | / { 452 | a; 453 | b; 454 | l2: c; 455 | l4: l5: l5: l4: d = < 0 >; 456 | }; 457 | 458 | / { 459 | l1: b; 460 | l3: c; 461 | l6: d; 462 | }; 463 | """, 464 | """ 465 | /dts-v1/; 466 | 467 | / { 468 | a; 469 | l1: b; 470 | l2: l3: c; 471 | l4: l5: l6: d = < 0x0 >; 472 | }; 473 | """) 474 | 475 | verify_label2prop("l1", "b") 476 | verify_label2prop("l2", "c") 477 | verify_label2prop("l3", "c") 478 | verify_label2prop("l4", "d") 479 | verify_label2prop("l5", "d") 480 | verify_label2prop("l6", "d") 481 | 482 | def test_property_offset_labels(): 483 | ''' 484 | It's possible to give labels to data at nonnegative byte offsets 485 | within a property value. 486 | ''' 487 | 488 | def verify_label2offset(label, expected_prop, expected_offset): 489 | actual_prop, actual_offset = dt.label2prop_offset[label] 490 | actual_prop = actual_prop.name 491 | assert (actual_prop, actual_offset) == \ 492 | (expected_prop, expected_offset), \ 493 | f"label '{label}' maps to wrong offset or property" 494 | 495 | dt = verify_parse(""" 496 | /dts-v1/; 497 | 498 | / { 499 | a = l01: l02: < l03: &node l04: l05: 2 l06: >, 500 | l07: l08: [ l09: 03 l10: l11: 04 l12: l13: ] l14:, "A"; 501 | 502 | b = < 0 > l23: l24:; 503 | 504 | node: node { 505 | }; 506 | }; 507 | """, 508 | """ 509 | /dts-v1/; 510 | 511 | / { 512 | a = l01: l02: < l03: &node l04: l05: 0x2 l06: l07: l08: >, [ l09: 03 l10: l11: 04 l12: l13: l14: ], "A"; 513 | b = < 0x0 l23: l24: >; 514 | node: node { 515 | phandle = < 0x1 >; 516 | }; 517 | }; 518 | """) 519 | 520 | verify_label2offset("l01", "a", 0) 521 | verify_label2offset("l02", "a", 0) 522 | verify_label2offset("l04", "a", 4) 523 | verify_label2offset("l05", "a", 4) 524 | verify_label2offset("l06", "a", 8) 525 | verify_label2offset("l09", "a", 8) 526 | verify_label2offset("l10", "a", 9) 527 | 528 | verify_label2offset("l23", "b", 4) 529 | verify_label2offset("l24", "b", 4) 530 | 531 | def test_unit_addr(): 532 | '''Node unit addresses must be correctly extracted from their names.''' 533 | 534 | def verify_unit_addr(path, expected): 535 | node = dt.get_node(path) 536 | assert node.unit_addr == expected, \ 537 | f"{node!r} has unexpected unit address" 538 | 539 | dt = verify_parse(""" 540 | /dts-v1/; 541 | 542 | / { 543 | no-unit-addr { 544 | }; 545 | 546 | unit-addr@ABC { 547 | }; 548 | 549 | unit-addr-non-numeric@foo-bar { 550 | }; 551 | }; 552 | """, 553 | """ 554 | /dts-v1/; 555 | 556 | / { 557 | no-unit-addr { 558 | }; 559 | unit-addr@ABC { 560 | }; 561 | unit-addr-non-numeric@foo-bar { 562 | }; 563 | }; 564 | """) 565 | 566 | verify_unit_addr("/no-unit-addr", "") 567 | verify_unit_addr("/unit-addr@ABC", "ABC") 568 | verify_unit_addr("/unit-addr-non-numeric@foo-bar", "foo-bar") 569 | 570 | def test_node_path_references(): 571 | '''Node phandles may be specified using a reference to the node's path.''' 572 | 573 | verify_parse(""" 574 | /dts-v1/; 575 | 576 | / { 577 | a = &label; 578 | b = [ 01 ], &label; 579 | c = [ 01 ], &label, <2>; 580 | d = &{/abc}; 581 | label: abc { 582 | e = &label; 583 | f = &{/abc}; 584 | }; 585 | }; 586 | """, 587 | """ 588 | /dts-v1/; 589 | 590 | / { 591 | a = &label; 592 | b = [ 01 ], &label; 593 | c = [ 01 ], &label, < 0x2 >; 594 | d = &{/abc}; 595 | label: abc { 596 | e = &label; 597 | f = &{/abc}; 598 | }; 599 | }; 600 | """) 601 | 602 | verify_error(""" 603 | /dts-v1/; 604 | 605 | / { 606 | sub { 607 | x = &missing; 608 | }; 609 | }; 610 | """, 611 | "/sub: undefined node label 'missing'") 612 | 613 | verify_error(""" 614 | /dts-v1/; 615 | 616 | / { 617 | sub { 618 | x = &{/sub/missing}; 619 | }; 620 | }; 621 | """, 622 | "/sub: component 'missing' in path '/sub/missing' does not exist") 623 | 624 | def test_phandles(): 625 | '''Various tests related to phandles.''' 626 | 627 | verify_parse(""" 628 | /dts-v1/; 629 | 630 | / { 631 | x = < &a &{/b} &c >; 632 | 633 | dummy1 { 634 | phandle = < 1 >; 635 | }; 636 | 637 | dummy2 { 638 | phandle = < 3 >; 639 | }; 640 | 641 | a: a { 642 | }; 643 | 644 | b { 645 | }; 646 | 647 | c: c { 648 | phandle = < 0xFF >; 649 | }; 650 | }; 651 | """, 652 | """ 653 | /dts-v1/; 654 | 655 | / { 656 | x = < &a &{/b} &c >; 657 | dummy1 { 658 | phandle = < 0x1 >; 659 | }; 660 | dummy2 { 661 | phandle = < 0x3 >; 662 | }; 663 | a: a { 664 | phandle = < 0x2 >; 665 | }; 666 | b { 667 | phandle = < 0x4 >; 668 | }; 669 | c: c { 670 | phandle = < 0xff >; 671 | }; 672 | }; 673 | """) 674 | 675 | # Check that a node can be assigned a phandle to itself. This just forces a 676 | # phandle to be allocated on it. The C tools support this too. 677 | verify_parse(""" 678 | /dts-v1/; 679 | 680 | / { 681 | dummy { 682 | phandle = < 1 >; 683 | }; 684 | 685 | a { 686 | foo: phandle = < &{/a} >; 687 | }; 688 | 689 | label: b { 690 | bar: phandle = < &label >; 691 | }; 692 | }; 693 | """, 694 | """ 695 | /dts-v1/; 696 | 697 | / { 698 | dummy { 699 | phandle = < 0x1 >; 700 | }; 701 | a { 702 | foo: phandle = < &{/a} >; 703 | }; 704 | label: b { 705 | bar: phandle = < &label >; 706 | }; 707 | }; 708 | """) 709 | 710 | verify_error(""" 711 | /dts-v1/; 712 | 713 | / { 714 | sub { 715 | x = < &missing >; 716 | }; 717 | }; 718 | """, 719 | "/sub: undefined node label 'missing'") 720 | 721 | verify_error_endswith(""" 722 | /dts-v1/; 723 | 724 | / { 725 | a: sub { 726 | x = /bits/ 16 < &a >; 727 | }; 728 | }; 729 | """, 730 | ":5 (column 19): parse error: phandle references are only allowed in arrays with 32-bit elements") 731 | 732 | verify_error(""" 733 | /dts-v1/; 734 | 735 | / { 736 | foo { 737 | phandle = [ 00 ]; 738 | }; 739 | }; 740 | """, 741 | "/foo: bad phandle length (1), expected 4 bytes") 742 | 743 | verify_error(""" 744 | /dts-v1/; 745 | 746 | / { 747 | foo { 748 | phandle = < 0 >; 749 | }; 750 | }; 751 | """, 752 | "/foo: bad value 0x00000000 for phandle") 753 | 754 | verify_error(""" 755 | /dts-v1/; 756 | 757 | / { 758 | foo { 759 | phandle = < (-1) >; 760 | }; 761 | }; 762 | """, 763 | "/foo: bad value 0xffffffff for phandle") 764 | 765 | verify_error(""" 766 | /dts-v1/; 767 | 768 | / { 769 | foo { 770 | phandle = < 17 >; 771 | }; 772 | 773 | bar { 774 | phandle = < 17 >; 775 | }; 776 | }; 777 | """, 778 | "/bar: duplicated phandle 0x11 (seen before at /foo)") 779 | 780 | verify_error(""" 781 | /dts-v1/; 782 | 783 | / { 784 | foo { 785 | phandle = < &{/bar} >; 786 | }; 787 | 788 | bar { 789 | }; 790 | }; 791 | """, 792 | "/foo: phandle refers to another node") 793 | 794 | def test_phandle2node(): 795 | '''Test the phandle2node dict in a dt instance.''' 796 | 797 | def verify_phandle2node(prop, offset, expected_name): 798 | phandle = dtlib.to_num(dt.root.props[prop].value[offset:offset + 4]) 799 | actual_name = dt.phandle2node[phandle].name 800 | 801 | assert actual_name == expected_name, \ 802 | f"'{prop}' is a phandle for the wrong thing" 803 | 804 | dt = parse(""" 805 | /dts-v1/; 806 | 807 | / { 808 | phandle_ = < &{/node1} 0 1 >; 809 | phandles = < 0 &{/node2} 1 &{/node3} >; 810 | 811 | node1 { 812 | phandle = < 123 >; 813 | }; 814 | 815 | node2 { 816 | }; 817 | 818 | node3 { 819 | }; 820 | }; 821 | """) 822 | 823 | verify_phandle2node("phandle_", 0, "node1") 824 | verify_phandle2node("phandles", 4, "node2") 825 | verify_phandle2node("phandles", 12, "node3") 826 | 827 | def test_mixed_assign(): 828 | '''Test mixed value type assignments''' 829 | 830 | verify_parse(""" 831 | /dts-v1/; 832 | 833 | / { 834 | x = /bits/ 8 < 0xFF 0xFF >, 835 | &abc, 836 | < 0xFF &abc 0xFF &abc >, 837 | &abc, 838 | [ FF FF ], 839 | "abc"; 840 | 841 | abc: abc { 842 | }; 843 | }; 844 | """, 845 | """ 846 | /dts-v1/; 847 | 848 | / { 849 | x = [ FF FF ], &abc, < 0xff &abc 0xff &abc >, &abc, [ FF FF ], "abc"; 850 | abc: abc { 851 | phandle = < 0x1 >; 852 | }; 853 | }; 854 | """) 855 | 856 | def test_deletion(): 857 | '''Properties and nodes may be deleted from the tree.''' 858 | 859 | # Test property deletion 860 | 861 | verify_parse(""" 862 | /dts-v1/; 863 | 864 | / { 865 | keep = < 1 >; 866 | delete = < &sub >, ⊂ 867 | /delete-property/ missing; 868 | /delete-property/ delete; 869 | sub: sub { 870 | y = < &sub >, ⊂ 871 | }; 872 | }; 873 | 874 | &sub { 875 | /delete-property/ y; 876 | }; 877 | """, 878 | """ 879 | /dts-v1/; 880 | 881 | / { 882 | keep = < 0x1 >; 883 | sub: sub { 884 | }; 885 | }; 886 | """) 887 | 888 | # Test node deletion 889 | 890 | verify_parse(""" 891 | /dts-v1/; 892 | 893 | / { 894 | sub1 { 895 | x = < 1 >; 896 | sub2 { 897 | x = < &sub >, ⊂ 898 | }; 899 | /delete-node/ sub2; 900 | }; 901 | 902 | sub3: sub3 { 903 | x = < &sub >, ⊂ 904 | }; 905 | 906 | sub4 { 907 | x = < &sub >, ⊂ 908 | }; 909 | }; 910 | 911 | /delete-node/ &sub3; 912 | /delete-node/ &{/sub4}; 913 | """, 914 | """ 915 | /dts-v1/; 916 | 917 | / { 918 | sub1 { 919 | x = < 0x1 >; 920 | }; 921 | }; 922 | """) 923 | 924 | verify_error_endswith(""" 925 | /dts-v1/; 926 | 927 | / { 928 | }; 929 | 930 | /delete-node/ &missing; 931 | """, 932 | ":6 (column 15): parse error: undefined node label 'missing'") 933 | 934 | verify_error_endswith(""" 935 | /dts-v1/; 936 | 937 | /delete-node/ { 938 | """, 939 | ":3 (column 15): parse error: expected label (&foo) or path (&{/foo/bar}) reference") 940 | 941 | def test_include_curdir(tmp_path): 942 | '''Verify that /include/ (which is handled in the lexer) searches the 943 | current directory''' 944 | 945 | with temporary_chdir(tmp_path): 946 | with open("same-dir-1", "w") as f: 947 | f.write(""" 948 | x = [ 00 ]; 949 | /include/ "same-dir-2" 950 | """) 951 | with open("same-dir-2", "w") as f: 952 | f.write(""" 953 | y = [ 01 ]; 954 | /include/ "same-dir-3" 955 | """) 956 | with open("same-dir-3", "w") as f: 957 | f.write(""" 958 | z = [ 02 ]; 959 | """) 960 | with open("test.dts", "w") as f: 961 | f.write(""" 962 | /dts-v1/; 963 | 964 | / { 965 | /include/ "same-dir-1" 966 | }; 967 | """) 968 | dt = dtlib.DT("test.dts") 969 | assert str(dt) == """ 970 | /dts-v1/; 971 | 972 | / { 973 | x = [ 00 ]; 974 | y = [ 01 ]; 975 | z = [ 02 ]; 976 | }; 977 | """[1:-1] 978 | 979 | def test_include_is_lexical(tmp_path): 980 | '''/include/ is done in the lexer, which means that property 981 | definitions can span multiple included files in different 982 | directories.''' 983 | 984 | with open(tmp_path / "tmp2.dts", "w") as f: 985 | f.write(""" 986 | /dts-v1/; 987 | / { 988 | """) 989 | with open(tmp_path / "tmp3.dts", "w") as f: 990 | f.write(""" 991 | x = <1>; 992 | """) 993 | 994 | subdir_1 = tmp_path / "subdir-1" 995 | subdir_1.mkdir() 996 | with open(subdir_1 / "via-include-path-1", "w") as f: 997 | f.write(""" 998 | = /include/ "via-include-path-2" 999 | """) 1000 | 1001 | subdir_2 = tmp_path / "subdir-2" 1002 | subdir_2.mkdir() 1003 | with open(subdir_2 / "via-include-path-2", "w") as f: 1004 | f.write(""" 1005 | <2>; 1006 | }; 1007 | """) 1008 | 1009 | with open(tmp_path / "test.dts", "w") as test_dts: 1010 | test_dts.write(""" 1011 | /include/ "tmp2.dts" 1012 | /include/ "tmp3.dts" 1013 | y /include/ "via-include-path-1" 1014 | """) 1015 | 1016 | with temporary_chdir(tmp_path): 1017 | dt = dtlib.DT("test.dts", include_path=(subdir_1, subdir_2)) 1018 | expected_dt = """ 1019 | /dts-v1/; 1020 | 1021 | / { 1022 | x = < 0x1 >; 1023 | y = < 0x2 >; 1024 | }; 1025 | """[1:-1] 1026 | assert str(dt) == expected_dt 1027 | 1028 | def test_include_misc(tmp_path): 1029 | '''Miscellaneous /include/ tests.''' 1030 | 1031 | # Missing includes should error out. 1032 | 1033 | verify_error_endswith(""" 1034 | /include/ "missing" 1035 | """, 1036 | ":1 (column 1): parse error: 'missing' could not be found") 1037 | 1038 | # Verify that an error in an included file points to the right location 1039 | 1040 | with temporary_chdir(tmp_path): 1041 | with open("tmp2.dts", "w") as f: 1042 | f.write("""\ 1043 | 1044 | 1045 | x 1046 | """) 1047 | with open("tmp.dts", "w") as f: 1048 | f.write(""" 1049 | 1050 | 1051 | /include/ "tmp2.dts" 1052 | """) 1053 | with pytest.raises(dtlib.DTError) as e: 1054 | dtlib.DT("tmp.dts") 1055 | 1056 | assert str(e.value) == \ 1057 | "tmp2.dts:3 (column 3): parse error: expected '/dts-v1/;' at start of file" 1058 | 1059 | def test_include_recursion(tmp_path): 1060 | '''Test recursive /include/ detection''' 1061 | 1062 | with temporary_chdir(tmp_path): 1063 | with open("tmp2.dts", "w") as f: 1064 | f.write('/include/ "tmp3.dts"\n') 1065 | with open("tmp3.dts", "w") as f: 1066 | f.write('/include/ "tmp.dts"\n') 1067 | 1068 | with open("tmp.dts", "w") as f: 1069 | f.write('/include/ "tmp2.dts"\n') 1070 | with pytest.raises(dtlib.DTError) as e: 1071 | dtlib.DT("tmp.dts") 1072 | 1073 | expected_err = """\ 1074 | tmp3.dts:1 (column 1): parse error: recursive /include/: 1075 | tmp.dts:1 -> 1076 | tmp2.dts:1 -> 1077 | tmp3.dts:1 -> 1078 | tmp.dts""" 1079 | assert str(e.value) == expected_err 1080 | 1081 | with open("tmp.dts", "w") as f: 1082 | f.write('/include/ "tmp.dts"\n') 1083 | with pytest.raises(dtlib.DTError) as e: 1084 | dtlib.DT("tmp.dts") 1085 | expected_err = """\ 1086 | tmp.dts:1 (column 1): parse error: recursive /include/: 1087 | tmp.dts:1 -> 1088 | tmp.dts""" 1089 | assert str(e.value) == expected_err 1090 | 1091 | def test_omit_if_no_ref(): 1092 | '''The /omit-if-no-ref/ marker is a bit of undocumented 1093 | dtc magic that removes a node from the tree if it isn't 1094 | referred to elsewhere. 1095 | 1096 | https://elinux.org/Device_Tree_Source_Undocumented 1097 | ''' 1098 | 1099 | verify_parse(""" 1100 | /dts-v1/; 1101 | 1102 | / { 1103 | x = < &{/referenced} >, &referenced2; 1104 | 1105 | /omit-if-no-ref/ referenced { 1106 | }; 1107 | 1108 | referenced2: referenced2 { 1109 | }; 1110 | 1111 | /omit-if-no-ref/ unreferenced { 1112 | }; 1113 | 1114 | l1: /omit-if-no-ref/ unreferenced2 { 1115 | }; 1116 | 1117 | /omit-if-no-ref/ l2: unreferenced3 { 1118 | }; 1119 | 1120 | unreferenced4: unreferenced4 { 1121 | }; 1122 | 1123 | unreferenced5 { 1124 | }; 1125 | }; 1126 | 1127 | /omit-if-no-ref/ &referenced2; 1128 | /omit-if-no-ref/ &unreferenced4; 1129 | /omit-if-no-ref/ &{/unreferenced5}; 1130 | """, 1131 | """ 1132 | /dts-v1/; 1133 | 1134 | / { 1135 | x = < &{/referenced} >, &referenced2; 1136 | referenced { 1137 | phandle = < 0x1 >; 1138 | }; 1139 | referenced2: referenced2 { 1140 | }; 1141 | }; 1142 | """) 1143 | 1144 | verify_error_endswith(""" 1145 | /dts-v1/; 1146 | 1147 | / { 1148 | /omit-if-no-ref/ x = ""; 1149 | }; 1150 | """, 1151 | ":4 (column 21): parse error: /omit-if-no-ref/ can only be used on nodes") 1152 | 1153 | verify_error_endswith(""" 1154 | /dts-v1/; 1155 | 1156 | / { 1157 | /omit-if-no-ref/ x; 1158 | }; 1159 | """, 1160 | ":4 (column 20): parse error: /omit-if-no-ref/ can only be used on nodes") 1161 | 1162 | verify_error_endswith(""" 1163 | /dts-v1/; 1164 | 1165 | / { 1166 | /omit-if-no-ref/ { 1167 | }; 1168 | }; 1169 | """, 1170 | ":4 (column 19): parse error: expected node or property name") 1171 | 1172 | verify_error_endswith(""" 1173 | /dts-v1/; 1174 | 1175 | / { 1176 | /omit-if-no-ref/ = < 0 >; 1177 | }; 1178 | """, 1179 | ":4 (column 19): parse error: expected node or property name") 1180 | 1181 | verify_error_endswith(""" 1182 | /dts-v1/; 1183 | 1184 | / { 1185 | }; 1186 | 1187 | /omit-if-no-ref/ &missing; 1188 | """, 1189 | ":6 (column 18): parse error: undefined node label 'missing'") 1190 | 1191 | verify_error_endswith(""" 1192 | /dts-v1/; 1193 | 1194 | /omit-if-no-ref/ { 1195 | """, 1196 | ":3 (column 18): parse error: expected label (&foo) or path (&{/foo/bar}) reference") 1197 | 1198 | def test_expr(): 1199 | '''Property values may contain expressions.''' 1200 | 1201 | verify_parse(""" 1202 | /dts-v1/; 1203 | 1204 | / { 1205 | ter1 = < (0 ? 1 : 0 ? 2 : 3) >; 1206 | ter2 = < (0 ? 1 : 1 ? 2 : 3) >; 1207 | ter3 = < (1 ? 1 : 0 ? 2 : 3) >; 1208 | ter4 = < (1 ? 1 : 1 ? 2 : 3) >; 1209 | or1 = < (0 || 0) >; 1210 | or2 = < (0 || 1) >; 1211 | or3 = < (1 || 0) >; 1212 | or4 = < (1 || 1) >; 1213 | and1 = < (0 && 0) >; 1214 | and2 = < (0 && 1) >; 1215 | and3 = < (1 && 0) >; 1216 | and4 = < (1 && 1) >; 1217 | bitor = < (1 | 2) >; 1218 | bitxor = < (7 ^ 2) >; 1219 | bitand = < (3 & 6) >; 1220 | eq1 = < (1 == 0) >; 1221 | eq2 = < (1 == 1) >; 1222 | neq1 = < (1 != 0) >; 1223 | neq2 = < (1 != 1) >; 1224 | lt1 = < (1 < 2) >; 1225 | lt2 = < (2 < 2) >; 1226 | lt3 = < (3 < 2) >; 1227 | lteq1 = < (1 <= 2) >; 1228 | lteq2 = < (2 <= 2) >; 1229 | lteq3 = < (3 <= 2) >; 1230 | gt1 = < (1 > 2) >; 1231 | gt2 = < (2 > 2) >; 1232 | gt3 = < (3 > 2) >; 1233 | gteq1 = < (1 >= 2) >; 1234 | gteq2 = < (2 >= 2) >; 1235 | gteq3 = < (3 >= 2) >; 1236 | lshift = < (2 << 3) >; 1237 | rshift = < (16 >> 3) >; 1238 | add = < (3 + 4) >; 1239 | sub = < (7 - 4) >; 1240 | mul = < (3 * 4) >; 1241 | div = < (11 / 3) >; 1242 | mod = < (11 % 3) >; 1243 | unary_minus = < (-3) >; 1244 | bitnot = < (~1) >; 1245 | not0 = < (!-1) >; 1246 | not1 = < (!0) >; 1247 | not2 = < (!1) >; 1248 | not3 = < (!2) >; 1249 | nest = < (((--3) + (-2)) * (--(-2))) >; 1250 | char_lits = < ('a' + 'b') >; 1251 | }; 1252 | """, 1253 | """ 1254 | /dts-v1/; 1255 | 1256 | / { 1257 | ter1 = < 0x3 >; 1258 | ter2 = < 0x2 >; 1259 | ter3 = < 0x1 >; 1260 | ter4 = < 0x1 >; 1261 | or1 = < 0x0 >; 1262 | or2 = < 0x1 >; 1263 | or3 = < 0x1 >; 1264 | or4 = < 0x1 >; 1265 | and1 = < 0x0 >; 1266 | and2 = < 0x0 >; 1267 | and3 = < 0x0 >; 1268 | and4 = < 0x1 >; 1269 | bitor = < 0x3 >; 1270 | bitxor = < 0x5 >; 1271 | bitand = < 0x2 >; 1272 | eq1 = < 0x0 >; 1273 | eq2 = < 0x1 >; 1274 | neq1 = < 0x1 >; 1275 | neq2 = < 0x0 >; 1276 | lt1 = < 0x1 >; 1277 | lt2 = < 0x0 >; 1278 | lt3 = < 0x0 >; 1279 | lteq1 = < 0x1 >; 1280 | lteq2 = < 0x1 >; 1281 | lteq3 = < 0x0 >; 1282 | gt1 = < 0x0 >; 1283 | gt2 = < 0x0 >; 1284 | gt3 = < 0x1 >; 1285 | gteq1 = < 0x0 >; 1286 | gteq2 = < 0x1 >; 1287 | gteq3 = < 0x1 >; 1288 | lshift = < 0x10 >; 1289 | rshift = < 0x2 >; 1290 | add = < 0x7 >; 1291 | sub = < 0x3 >; 1292 | mul = < 0xc >; 1293 | div = < 0x3 >; 1294 | mod = < 0x2 >; 1295 | unary_minus = < 0xfffffffd >; 1296 | bitnot = < 0xfffffffe >; 1297 | not0 = < 0x0 >; 1298 | not1 = < 0x1 >; 1299 | not2 = < 0x0 >; 1300 | not3 = < 0x0 >; 1301 | nest = < 0xfffffffe >; 1302 | char_lits = < 0xc3 >; 1303 | }; 1304 | """) 1305 | 1306 | verify_error_endswith(""" 1307 | /dts-v1/; 1308 | 1309 | / { 1310 | a = < (1/(-1 + 1)) >; 1311 | }; 1312 | """, 1313 | ":4 (column 18): parse error: division by zero") 1314 | 1315 | verify_error_endswith(""" 1316 | /dts-v1/; 1317 | 1318 | / { 1319 | a = < (1%0) >; 1320 | }; 1321 | """, 1322 | ":4 (column 11): parse error: division by zero") 1323 | 1324 | def test_comment_removal(): 1325 | '''Comments should be removed when round-tripped to a str.''' 1326 | 1327 | verify_parse(""" 1328 | /**//dts-v1//**/;// 1329 | // 1330 | // foo 1331 | / /**/{// foo 1332 | x/**/=/* 1333 | foo 1334 | *//****/;/**/}/*/**/; 1335 | """, 1336 | """ 1337 | /dts-v1/; 1338 | 1339 | / { 1340 | x = < 0x1 >; 1341 | }; 1342 | """) 1343 | 1344 | def verify_path_is(path, node_name, dt): 1345 | '''Verify 'node.name' matches 'node_name' in 'dt'.''' 1346 | 1347 | try: 1348 | node = dt.get_node(path) 1349 | assert node.name == node_name, f'unexpected path {path}' 1350 | except dtlib.DTError: 1351 | assert False, f'no node found for path {path}' 1352 | 1353 | def verify_path_error(path, msg, dt): 1354 | '''Verify that an attempt to get node 'path' from 'dt' raises 1355 | a DTError whose str is 'msg'.''' 1356 | 1357 | with pytest.raises(dtlib.DTError) as e: 1358 | dt.get_node(path) 1359 | assert str(e.value) == msg, f"'{path}' gives the wrong error" 1360 | 1361 | def test_get_node(): 1362 | '''Test DT.get_node().''' 1363 | 1364 | dt = parse(""" 1365 | /dts-v1/; 1366 | 1367 | / { 1368 | foo { 1369 | bar { 1370 | }; 1371 | }; 1372 | 1373 | baz { 1374 | }; 1375 | }; 1376 | """) 1377 | 1378 | verify_path_is("/", "/", dt) 1379 | verify_path_is("//", "/", dt) 1380 | verify_path_is("///", "/", dt) 1381 | verify_path_is("/foo", "foo", dt) 1382 | verify_path_is("//foo", "foo", dt) 1383 | verify_path_is("///foo", "foo", dt) 1384 | verify_path_is("/foo/bar", "bar", dt) 1385 | verify_path_is("//foo//bar", "bar", dt) 1386 | verify_path_is("///foo///bar", "bar", dt) 1387 | verify_path_is("/baz", "baz", dt) 1388 | 1389 | verify_path_error( 1390 | "", 1391 | "no alias '' found -- did you forget the leading '/' in the node path?", 1392 | dt) 1393 | verify_path_error( 1394 | "missing", 1395 | "no alias 'missing' found -- did you forget the leading '/' in the node path?", 1396 | dt) 1397 | verify_path_error( 1398 | "/missing", 1399 | "component 'missing' in path '/missing' does not exist", 1400 | dt) 1401 | verify_path_error( 1402 | "/foo/missing", 1403 | "component 'missing' in path '/foo/missing' does not exist", 1404 | dt) 1405 | 1406 | def verify_path_exists(path): 1407 | assert dt.has_node(path), f"path '{path}' does not exist" 1408 | 1409 | def verify_path_missing(path): 1410 | assert not dt.has_node(path), f"path '{path}' exists" 1411 | 1412 | verify_path_exists("/") 1413 | verify_path_exists("/foo") 1414 | verify_path_exists("/foo/bar") 1415 | 1416 | verify_path_missing("/missing") 1417 | verify_path_missing("/foo/missing") 1418 | 1419 | def test_aliases(): 1420 | '''Test /aliases''' 1421 | 1422 | dt = parse(""" 1423 | /dts-v1/; 1424 | 1425 | / { 1426 | aliases { 1427 | alias1 = &l1; 1428 | alias2 = &l2; 1429 | alias3 = &{/sub/node3}; 1430 | alias4 = &{/node4}; 1431 | }; 1432 | 1433 | l1: node1 { 1434 | }; 1435 | 1436 | l2: node2 { 1437 | }; 1438 | 1439 | sub { 1440 | node3 { 1441 | }; 1442 | }; 1443 | 1444 | node4 { 1445 | node5 { 1446 | }; 1447 | }; 1448 | }; 1449 | """) 1450 | 1451 | def verify_alias_target(alias, node_name): 1452 | verify_path_is(alias, node_name, dt) 1453 | assert alias in dt.alias2node 1454 | assert dt.alias2node[alias].name == node_name, f"bad result for {alias}" 1455 | 1456 | verify_alias_target("alias1", "node1") 1457 | verify_alias_target("alias2", "node2") 1458 | verify_alias_target("alias3", "node3") 1459 | verify_path_is("alias4/node5", "node5", dt) 1460 | 1461 | verify_path_error( 1462 | "alias4/node5/node6", 1463 | "component 'node6' in path 'alias4/node5/node6' does not exist", 1464 | dt) 1465 | 1466 | verify_error_matches(""" 1467 | /dts-v1/; 1468 | 1469 | / { 1470 | aliases { 1471 | a = [ 00 ]; 1472 | }; 1473 | }; 1474 | """, 1475 | "expected property 'a' on /aliases in .*" + 1476 | re.escape("to be assigned with either 'a = &foo' or 'a = \"/path/to/node\"', not 'a = [ 00 ];'")) 1477 | 1478 | verify_error_matches(r""" 1479 | /dts-v1/; 1480 | 1481 | / { 1482 | aliases { 1483 | a = "\xFF"; 1484 | }; 1485 | }; 1486 | """, 1487 | re.escape(r"value of property 'a' (b'\xff\x00') on /aliases in ") + 1488 | ".* is not valid UTF-8") 1489 | 1490 | verify_error(""" 1491 | /dts-v1/; 1492 | 1493 | / { 1494 | aliases { 1495 | A = "/aliases"; 1496 | }; 1497 | }; 1498 | """, 1499 | "/aliases: alias property name 'A' should include only characters from [0-9a-z-]") 1500 | 1501 | verify_error_matches(r""" 1502 | /dts-v1/; 1503 | 1504 | / { 1505 | aliases { 1506 | a = "/missing"; 1507 | }; 1508 | }; 1509 | """, 1510 | "property 'a' on /aliases in .* points to the non-existent node \"/missing\"") 1511 | 1512 | def test_prop_type(): 1513 | '''Test Property.type''' 1514 | 1515 | def verify_type(prop, expected): 1516 | actual = dt.root.props[prop].type 1517 | assert actual == expected, f'{prop} has wrong type' 1518 | 1519 | dt = parse(""" 1520 | /dts-v1/; 1521 | 1522 | / { 1523 | empty; 1524 | bytes1 = [ ]; 1525 | bytes2 = [ 01 ]; 1526 | bytes3 = [ 01 02 ]; 1527 | bytes4 = foo: [ 01 bar: 02 ]; 1528 | bytes5 = /bits/ 8 < 1 2 3 >; 1529 | num = < 1 >; 1530 | nums1 = < >; 1531 | nums2 = < >, < >; 1532 | nums3 = < 1 2 >; 1533 | nums4 = < 1 2 >, < 3 >, < 4 >; 1534 | string = "foo"; 1535 | strings = "foo", "bar"; 1536 | path1 = &node; 1537 | path2 = &{/node}; 1538 | phandle1 = < &node >; 1539 | phandle2 = < &{/node} >; 1540 | phandles1 = < &node &node >; 1541 | phandles2 = < &node >, < &node >; 1542 | phandle-and-nums-1 = < &node 1 >; 1543 | phandle-and-nums-2 = < &node 1 2 &node 3 4 >; 1544 | phandle-and-nums-3 = < &node 1 2 >, < &node 3 4 >; 1545 | compound1 = < 1 >, [ 02 ]; 1546 | compound2 = "foo", < >; 1547 | 1548 | node: node { 1549 | }; 1550 | }; 1551 | """) 1552 | 1553 | verify_type("empty", dtlib.Type.EMPTY) 1554 | verify_type("bytes1", dtlib.Type.BYTES) 1555 | verify_type("bytes2", dtlib.Type.BYTES) 1556 | verify_type("bytes3", dtlib.Type.BYTES) 1557 | verify_type("bytes4", dtlib.Type.BYTES) 1558 | verify_type("bytes5", dtlib.Type.BYTES) 1559 | verify_type("num", dtlib.Type.NUM) 1560 | verify_type("nums1", dtlib.Type.NUMS) 1561 | verify_type("nums2", dtlib.Type.NUMS) 1562 | verify_type("nums3", dtlib.Type.NUMS) 1563 | verify_type("nums4", dtlib.Type.NUMS) 1564 | verify_type("string", dtlib.Type.STRING) 1565 | verify_type("strings", dtlib.Type.STRINGS) 1566 | verify_type("phandle1", dtlib.Type.PHANDLE) 1567 | verify_type("phandle2", dtlib.Type.PHANDLE) 1568 | verify_type("phandles1", dtlib.Type.PHANDLES) 1569 | verify_type("phandles2", dtlib.Type.PHANDLES) 1570 | verify_type("phandle-and-nums-1", dtlib.Type.PHANDLES_AND_NUMS) 1571 | verify_type("phandle-and-nums-2", dtlib.Type.PHANDLES_AND_NUMS) 1572 | verify_type("phandle-and-nums-3", dtlib.Type.PHANDLES_AND_NUMS) 1573 | verify_type("path1", dtlib.Type.PATH) 1574 | verify_type("path2", dtlib.Type.PATH) 1575 | verify_type("compound1", dtlib.Type.COMPOUND) 1576 | verify_type("compound2", dtlib.Type.COMPOUND) 1577 | 1578 | def test_prop_type_casting(): 1579 | '''Test Property.to_{num,nums,string,strings,node}()''' 1580 | 1581 | dt = parse(r""" 1582 | /dts-v1/; 1583 | 1584 | / { 1585 | u = < 1 >; 1586 | s = < 0xFFFFFFFF >; 1587 | u8 = /bits/ 8 < 1 >; 1588 | u16 = /bits/ 16 < 1 2 >; 1589 | u64 = /bits/ 64 < 1 >; 1590 | bytes = [ 01 02 03 ]; 1591 | empty; 1592 | zero = < >; 1593 | two_u = < 1 2 >; 1594 | two_s = < 0xFFFFFFFF 0xFFFFFFFE >; 1595 | three_u = < 1 2 3 >; 1596 | three_u_split = < 1 >, < 2 >, < 3 >; 1597 | empty_string = ""; 1598 | string = "foo\tbar baz"; 1599 | invalid_string = "\xff"; 1600 | strings = "foo", "bar", "baz"; 1601 | invalid_strings = "foo", "\xff", "bar"; 1602 | ref = <&{/target}>; 1603 | refs = <&{/target} &{/target2}>; 1604 | refs2 = <&{/target}>, <&{/target2}>; 1605 | path = &{/target}; 1606 | manualpath = "/target"; 1607 | missingpath = "/missing"; 1608 | 1609 | target { 1610 | phandle = < 100 >; 1611 | }; 1612 | 1613 | target2 { 1614 | }; 1615 | }; 1616 | """) 1617 | 1618 | # Test Property.to_num() 1619 | 1620 | def verify_to_num(prop, signed, expected): 1621 | signed_str = "a signed" if signed else "an unsigned" 1622 | actual = dt.root.props[prop].to_num(signed) 1623 | assert actual == expected, \ 1624 | f"{prop} has bad {signed_str} numeric value" 1625 | 1626 | def verify_to_num_error_matches(prop, expected_re): 1627 | with pytest.raises(dtlib.DTError) as e: 1628 | dt.root.props[prop].to_num() 1629 | actual_msg = str(e.value) 1630 | assert re.fullmatch(expected_re, actual_msg), \ 1631 | f"'{prop}' to_num gives the wrong error: " \ 1632 | f"actual message:\n{actual_msg!r}\n" \ 1633 | f"does not match:\n{expected_re!r}" 1634 | 1635 | verify_to_num("u", False, 1) 1636 | verify_to_num("u", True, 1) 1637 | verify_to_num("s", False, 0xFFFFFFFF) 1638 | verify_to_num("s", True, -1) 1639 | 1640 | verify_to_num_error_matches( 1641 | "two_u", 1642 | "expected property 'two_u' on / in .* to be assigned with " + 1643 | re.escape("'two_u = < (number) >;', not 'two_u = < 0x1 0x2 >;'")) 1644 | verify_to_num_error_matches( 1645 | "u8", 1646 | "expected property 'u8' on / in .* to be assigned with " + 1647 | re.escape("'u8 = < (number) >;', not 'u8 = [ 01 ];'")) 1648 | verify_to_num_error_matches( 1649 | "u16", 1650 | "expected property 'u16' on / in .* to be assigned with " + 1651 | re.escape("'u16 = < (number) >;', not 'u16 = /bits/ 16 < 0x1 0x2 >;'")) 1652 | verify_to_num_error_matches( 1653 | "u64", 1654 | "expected property 'u64' on / in .* to be assigned with " + 1655 | re.escape("'u64 = < (number) >;', not 'u64 = /bits/ 64 < 0x1 >;'")) 1656 | verify_to_num_error_matches( 1657 | "string", 1658 | "expected property 'string' on / in .* to be assigned with " + 1659 | re.escape("'string = < (number) >;', not 'string = \"foo\\tbar baz\";'")) 1660 | 1661 | # Test Property.to_nums() 1662 | 1663 | def verify_to_nums(prop, signed, expected): 1664 | signed_str = "signed" if signed else "unsigned" 1665 | actual = dt.root.props[prop].to_nums(signed) 1666 | assert actual == expected, \ 1667 | f"'{prop}' gives the wrong {signed_str} numbers" 1668 | 1669 | def verify_to_nums_error_matches(prop, expected_re): 1670 | with pytest.raises(dtlib.DTError) as e: 1671 | dt.root.props[prop].to_nums() 1672 | assert re.fullmatch(expected_re, str(e.value)), \ 1673 | f"'{prop}' to_nums gives the wrong error" 1674 | 1675 | verify_to_nums("zero", False, []) 1676 | verify_to_nums("u", False, [1]) 1677 | verify_to_nums("two_u", False, [1, 2]) 1678 | verify_to_nums("two_u", True, [1, 2]) 1679 | verify_to_nums("two_s", False, [0xFFFFFFFF, 0xFFFFFFFE]) 1680 | verify_to_nums("two_s", True, [-1, -2]) 1681 | verify_to_nums("three_u", False, [1, 2, 3]) 1682 | verify_to_nums("three_u_split", False, [1, 2, 3]) 1683 | 1684 | verify_to_nums_error_matches( 1685 | "empty", 1686 | "expected property 'empty' on / in .* to be assigned with " + 1687 | re.escape("'empty = < (number) (number) ... >;', not 'empty;'")) 1688 | verify_to_nums_error_matches( 1689 | "string", 1690 | "expected property 'string' on / in .* to be assigned with " + 1691 | re.escape("'string = < (number) (number) ... >;', ") + 1692 | re.escape("not 'string = \"foo\\tbar baz\";'")) 1693 | 1694 | # Test Property.to_bytes() 1695 | 1696 | def verify_to_bytes(prop, expected): 1697 | actual = dt.root.props[prop].to_bytes() 1698 | assert actual == expected, f"'{prop}' gives the wrong bytes" 1699 | 1700 | def verify_to_bytes_error_matches(prop, expected_re): 1701 | with pytest.raises(dtlib.DTError) as e: 1702 | dt.root.props[prop].to_bytes() 1703 | assert re.fullmatch(expected_re, str(e.value)), \ 1704 | f"'{prop}' gives the wrong error" 1705 | 1706 | verify_to_bytes("u8", b"\x01") 1707 | verify_to_bytes("bytes", b"\x01\x02\x03") 1708 | 1709 | verify_to_bytes_error_matches( 1710 | "u16", 1711 | "expected property 'u16' on / in .* to be assigned with " + 1712 | re.escape("'u16 = [ (byte) (byte) ... ];', ") + 1713 | re.escape("not 'u16 = /bits/ 16 < 0x1 0x2 >;'")) 1714 | verify_to_bytes_error_matches( 1715 | "empty", 1716 | "expected property 'empty' on / in .* to be assigned with " + 1717 | re.escape("'empty = [ (byte) (byte) ... ];', not 'empty;'")) 1718 | 1719 | # Test Property.to_string() 1720 | 1721 | def verify_to_string(prop, expected): 1722 | actual = dt.root.props[prop].to_string() 1723 | assert actual == expected, f"'{prop}' to_string gives the wrong string" 1724 | 1725 | def verify_to_string_error_matches(prop, expected_re): 1726 | with pytest.raises(dtlib.DTError) as e: 1727 | dt.root.props[prop].to_string() 1728 | assert re.fullmatch(expected_re, str(e.value)), \ 1729 | f"'{prop}' gives the wrong error" 1730 | 1731 | verify_to_string("empty_string", "") 1732 | verify_to_string("string", "foo\tbar baz") 1733 | 1734 | verify_to_string_error_matches( 1735 | "u", 1736 | "expected property 'u' on / in .* to be assigned with " + 1737 | re.escape("'u = \"string\";', not 'u = < 0x1 >;'")) 1738 | verify_to_string_error_matches( 1739 | "strings", 1740 | "expected property 'strings' on / in .* to be assigned with " + 1741 | re.escape("'strings = \"string\";', ")+ 1742 | re.escape("not 'strings = \"foo\", \"bar\", \"baz\";'")) 1743 | verify_to_string_error_matches( 1744 | "invalid_string", 1745 | re.escape(r"value of property 'invalid_string' (b'\xff\x00') on / ") + 1746 | "in .* is not valid UTF-8") 1747 | 1748 | # Test Property.to_strings() 1749 | 1750 | def verify_to_strings(prop, expected): 1751 | actual = dt.root.props[prop].to_strings() 1752 | assert actual == expected, f"'{prop}' to_strings gives the wrong value" 1753 | 1754 | def verify_to_strings_error_matches(prop, expected_re): 1755 | with pytest.raises(dtlib.DTError) as e: 1756 | dt.root.props[prop].to_strings() 1757 | assert re.fullmatch(expected_re, str(e.value)), \ 1758 | f"'{prop}' gives the wrong error" 1759 | 1760 | verify_to_strings("empty_string", [""]) 1761 | verify_to_strings("string", ["foo\tbar baz"]) 1762 | verify_to_strings("strings", ["foo", "bar", "baz"]) 1763 | 1764 | verify_to_strings_error_matches( 1765 | "u", 1766 | "expected property 'u' on / in .* to be assigned with " + 1767 | re.escape("'u = \"string\", \"string\", ... ;', not 'u = < 0x1 >;'")) 1768 | verify_to_strings_error_matches( 1769 | "invalid_strings", 1770 | "value of property 'invalid_strings' " + 1771 | re.escape(r"(b'foo\x00\xff\x00bar\x00') on / in ") + 1772 | ".* is not valid UTF-8") 1773 | 1774 | # Test Property.to_node() 1775 | 1776 | def verify_to_node(prop, path): 1777 | actual = dt.root.props[prop].to_node().path 1778 | assert actual == path, f"'{prop}' points at wrong path" 1779 | 1780 | def verify_to_node_error_matches(prop, expected_re): 1781 | with pytest.raises(dtlib.DTError) as e: 1782 | dt.root.props[prop].to_node() 1783 | assert re.fullmatch(expected_re, str(e.value)), \ 1784 | f"'{prop} gives the wrong error" 1785 | 1786 | verify_to_node("ref", "/target") 1787 | 1788 | verify_to_node_error_matches( 1789 | "u", 1790 | "expected property 'u' on / in .* to be assigned with " + 1791 | re.escape("'u = < &foo >;', not 'u = < 0x1 >;'")) 1792 | verify_to_node_error_matches( 1793 | "string", 1794 | "expected property 'string' on / in .* to be assigned with " + 1795 | re.escape("'string = < &foo >;', not 'string = \"foo\\tbar baz\";'")) 1796 | 1797 | # Test Property.to_nodes() 1798 | 1799 | def verify_to_nodes(prop, paths): 1800 | actual = [node.path for node in dt.root.props[prop].to_nodes()] 1801 | assert actual == paths, f"'{prop} gives wrong node paths" 1802 | 1803 | def verify_to_nodes_error_matches(prop, expected_re): 1804 | with pytest.raises(dtlib.DTError) as e: 1805 | dt.root.props[prop].to_nodes() 1806 | assert re.fullmatch(expected_re, str(e.value)), \ 1807 | f"'{prop} gives wrong error" 1808 | 1809 | verify_to_nodes("zero", []) 1810 | verify_to_nodes("ref", ["/target"]) 1811 | verify_to_nodes("refs", ["/target", "/target2"]) 1812 | verify_to_nodes("refs2", ["/target", "/target2"]) 1813 | 1814 | verify_to_nodes_error_matches( 1815 | "u", 1816 | "expected property 'u' on / in .* to be assigned with " + 1817 | re.escape("'u = < &foo &bar ... >;', not 'u = < 0x1 >;'")) 1818 | verify_to_nodes_error_matches( 1819 | "string", 1820 | "expected property 'string' on / in .* to be assigned with " + 1821 | re.escape("'string = < &foo &bar ... >;', ") + 1822 | re.escape("not 'string = \"foo\\tbar baz\";'")) 1823 | 1824 | # Test Property.to_path() 1825 | 1826 | def verify_to_path(prop, path): 1827 | actual = dt.root.props[prop].to_path().path 1828 | assert actual == path, f"'{prop} gives the wrong path" 1829 | 1830 | def verify_to_path_error_matches(prop, expected_re): 1831 | with pytest.raises(dtlib.DTError) as e: 1832 | dt.root.props[prop].to_path() 1833 | assert re.fullmatch(expected_re, str(e.value)), \ 1834 | f"'{prop} gives the wrong error" 1835 | 1836 | verify_to_path("path", "/target") 1837 | verify_to_path("manualpath", "/target") 1838 | 1839 | verify_to_path_error_matches( 1840 | "u", 1841 | "expected property 'u' on / in .* to be assigned with either " + 1842 | re.escape("'u = &foo' or 'u = \"/path/to/node\"', not 'u = < 0x1 >;'")) 1843 | verify_to_path_error_matches( 1844 | "missingpath", 1845 | "property 'missingpath' on / in .* points to the non-existent node " 1846 | '"/missing"') 1847 | 1848 | # Test top-level to_num() and to_nums() 1849 | 1850 | def verify_raw_to_num(fn, prop, length, signed, expected): 1851 | actual = fn(dt.root.props[prop].value, length, signed) 1852 | assert actual == expected, \ 1853 | f"{fn.__name__}(<{prop}>, {length}, {signed}) gives wrong value" 1854 | 1855 | def verify_raw_to_num_error(fn, data, length, msg): 1856 | with pytest.raises(dtlib.DTError) as e: 1857 | fn(data, length) 1858 | assert str(e.value) == msg, \ 1859 | (f"{fn.__name__}() called with data='{data}', length='{length}' " 1860 | "gives the wrong error") 1861 | 1862 | verify_raw_to_num(dtlib.to_num, "u", None, False, 1) 1863 | verify_raw_to_num(dtlib.to_num, "u", 4, False, 1) 1864 | verify_raw_to_num(dtlib.to_num, "s", None, False, 0xFFFFFFFF) 1865 | verify_raw_to_num(dtlib.to_num, "s", None, True, -1) 1866 | verify_raw_to_num(dtlib.to_nums, "empty", 4, False, []) 1867 | verify_raw_to_num(dtlib.to_nums, "u16", 2, False, [1, 2]) 1868 | verify_raw_to_num(dtlib.to_nums, "two_s", 4, False, [0xFFFFFFFF, 0xFFFFFFFE]) 1869 | verify_raw_to_num(dtlib.to_nums, "two_s", 4, True, [-1, -2]) 1870 | 1871 | verify_raw_to_num_error(dtlib.to_num, 0, 0, "'0' has type 'int', expected 'bytes'") 1872 | verify_raw_to_num_error(dtlib.to_num, b"", 0, "'length' must be greater than zero, was 0") 1873 | verify_raw_to_num_error(dtlib.to_num, b"foo", 2, "b'foo' is 3 bytes long, expected 2") 1874 | verify_raw_to_num_error(dtlib.to_nums, 0, 0, "'0' has type 'int', expected 'bytes'") 1875 | verify_raw_to_num_error(dtlib.to_nums, b"", 0, "'length' must be greater than zero, was 0") 1876 | verify_raw_to_num_error(dtlib.to_nums, b"foooo", 2, "b'foooo' is 5 bytes long, expected a length that's a a multiple of 2") 1877 | 1878 | def test_duplicate_labels(): 1879 | ''' 1880 | It is an error to duplicate labels in most conditions, but there 1881 | are some exceptions where it's OK. 1882 | ''' 1883 | 1884 | verify_error(""" 1885 | /dts-v1/; 1886 | 1887 | / { 1888 | sub1 { 1889 | label: foo { 1890 | }; 1891 | }; 1892 | 1893 | sub2 { 1894 | label: bar { 1895 | }; 1896 | }; 1897 | }; 1898 | """, 1899 | "Label 'label' appears on /sub1/foo and on /sub2/bar") 1900 | 1901 | verify_error(""" 1902 | /dts-v1/; 1903 | 1904 | / { 1905 | sub { 1906 | label: foo { 1907 | }; 1908 | }; 1909 | }; 1910 | / { 1911 | sub { 1912 | label: bar { 1913 | }; 1914 | }; 1915 | }; 1916 | """, 1917 | "Label 'label' appears on /sub/bar and on /sub/foo") 1918 | 1919 | verify_error(""" 1920 | /dts-v1/; 1921 | 1922 | / { 1923 | foo: a = < 0 >; 1924 | foo: node { 1925 | }; 1926 | }; 1927 | """, 1928 | "Label 'foo' appears on /node and on property 'a' of node /") 1929 | 1930 | verify_error(""" 1931 | /dts-v1/; 1932 | 1933 | / { 1934 | foo: a = < 0 >; 1935 | node { 1936 | foo: b = < 0 >; 1937 | }; 1938 | }; 1939 | """, 1940 | "Label 'foo' appears on property 'a' of node / and on property 'b' of node /node") 1941 | 1942 | verify_error(""" 1943 | /dts-v1/; 1944 | 1945 | / { 1946 | foo: a = foo: < 0 >; 1947 | }; 1948 | """, 1949 | "Label 'foo' appears in the value of property 'a' of node / and on property 'a' of node /") 1950 | 1951 | # Giving the same label twice for the same node is fine 1952 | verify_parse(""" 1953 | /dts-v1/; 1954 | 1955 | / { 1956 | sub { 1957 | label: foo { 1958 | }; 1959 | }; 1960 | }; 1961 | / { 1962 | 1963 | sub { 1964 | label: foo { 1965 | }; 1966 | }; 1967 | }; 1968 | """, 1969 | """ 1970 | /dts-v1/; 1971 | 1972 | / { 1973 | sub { 1974 | label: foo { 1975 | }; 1976 | }; 1977 | }; 1978 | """) 1979 | 1980 | # Duplicate labels are fine if one of the nodes is deleted 1981 | verify_parse(""" 1982 | /dts-v1/; 1983 | 1984 | / { 1985 | label: foo { 1986 | }; 1987 | label: bar { 1988 | }; 1989 | }; 1990 | 1991 | /delete-node/ &{/bar}; 1992 | """, 1993 | """ 1994 | /dts-v1/; 1995 | 1996 | / { 1997 | label: foo { 1998 | }; 1999 | }; 2000 | """) 2001 | 2002 | # 2003 | # Test overriding/deleting a property with references 2004 | # 2005 | 2006 | verify_parse(""" 2007 | /dts-v1/; 2008 | 2009 | / { 2010 | x = &foo, < &foo >; 2011 | y = &foo, < &foo >; 2012 | foo: foo { 2013 | }; 2014 | }; 2015 | 2016 | / { 2017 | x = < 1 >; 2018 | /delete-property/ y; 2019 | }; 2020 | """, 2021 | """ 2022 | /dts-v1/; 2023 | 2024 | / { 2025 | x = < 0x1 >; 2026 | foo: foo { 2027 | }; 2028 | }; 2029 | """) 2030 | 2031 | # 2032 | # Test self-referential node 2033 | # 2034 | 2035 | verify_parse(""" 2036 | /dts-v1/; 2037 | 2038 | / { 2039 | label: foo { 2040 | x = &{/foo}, &label, < &label >; 2041 | }; 2042 | }; 2043 | """, 2044 | """ 2045 | /dts-v1/; 2046 | 2047 | / { 2048 | label: foo { 2049 | x = &{/foo}, &label, < &label >; 2050 | phandle = < 0x1 >; 2051 | }; 2052 | }; 2053 | """) 2054 | 2055 | # 2056 | # Test /memreserve/ 2057 | # 2058 | 2059 | dt = verify_parse(""" 2060 | /dts-v1/; 2061 | 2062 | l1: l2: /memreserve/ (1 + 1) (2 * 2); 2063 | /memreserve/ 0x100 0x200; 2064 | 2065 | / { 2066 | }; 2067 | """, 2068 | """ 2069 | /dts-v1/; 2070 | 2071 | l1: l2: /memreserve/ 0x0000000000000002 0x0000000000000004; 2072 | /memreserve/ 0x0000000000000100 0x0000000000000200; 2073 | 2074 | / { 2075 | }; 2076 | """) 2077 | 2078 | expected = [(["l1", "l2"], 2, 4), ([], 0x100, 0x200)] 2079 | assert dt.memreserves == expected 2080 | 2081 | verify_error_endswith(""" 2082 | /dts-v1/; 2083 | 2084 | foo: / { 2085 | }; 2086 | """, 2087 | ":3 (column 6): parse error: expected /memreserve/ after labels at beginning of file") 2088 | 2089 | def test_reprs(): 2090 | '''Test the __repr__() functions.''' 2091 | 2092 | dts = """ 2093 | /dts-v1/; 2094 | 2095 | / { 2096 | x = < 0 >; 2097 | sub { 2098 | y = < 1 >; 2099 | }; 2100 | }; 2101 | """ 2102 | 2103 | dt = parse(dts, include_path=("foo", "bar")) 2104 | 2105 | assert re.fullmatch(r"DT\(filename='.*', include_path=.'foo', 'bar'.\)", 2106 | repr(dt)) 2107 | assert re.fullmatch("", 2108 | repr(dt.root.props["x"])) 2109 | assert re.fullmatch("", 2110 | repr(dt.root.nodes["sub"])) 2111 | 2112 | dt = parse(dts, include_path=iter(("foo", "bar"))) 2113 | 2114 | assert re.fullmatch(r"DT\(filename='.*', include_path=.'foo', 'bar'.\)", 2115 | repr(dt)) 2116 | 2117 | def test_names(): 2118 | '''Tests for node/property names.''' 2119 | 2120 | verify_parse(r""" 2121 | /dts-v1/; 2122 | 2123 | / { 2124 | // A leading \ is accepted but ignored in node/propert names 2125 | \aA0,._+*#?- = &_, &{/aA0,._+@-}; 2126 | 2127 | // Names that overlap with operators and integer literals 2128 | 2129 | + = [ 00 ]; 2130 | * = [ 02 ]; 2131 | - = [ 01 ]; 2132 | ? = [ 03 ]; 2133 | 0 = [ 04 ]; 2134 | 0x123 = [ 05 ]; 2135 | 2136 | // Node names are more restrictive than property names. 2137 | _: \aA0,._+@- { 2138 | }; 2139 | 2140 | 0 { 2141 | }; 2142 | }; 2143 | """, 2144 | """ 2145 | /dts-v1/; 2146 | 2147 | / { 2148 | aA0,._+*#?- = &_, &{/aA0,._+@-}; 2149 | + = [ 00 ]; 2150 | * = [ 02 ]; 2151 | - = [ 01 ]; 2152 | ? = [ 03 ]; 2153 | 0 = [ 04 ]; 2154 | 0x123 = [ 05 ]; 2155 | _: aA0,._+@- { 2156 | }; 2157 | 0 { 2158 | }; 2159 | }; 2160 | """) 2161 | 2162 | verify_error_endswith(r""" 2163 | /dts-v1/; 2164 | 2165 | / { 2166 | foo@3; 2167 | }; 2168 | """, 2169 | ":4 (column 7): parse error: '@' is only allowed in node names") 2170 | 2171 | verify_error_endswith(r""" 2172 | /dts-v1/; 2173 | 2174 | / { 2175 | foo@3 = < 0 >; 2176 | }; 2177 | """, 2178 | ":4 (column 8): parse error: '@' is only allowed in node names") 2179 | 2180 | verify_error_endswith(r""" 2181 | /dts-v1/; 2182 | 2183 | / { 2184 | foo@2@3 { 2185 | }; 2186 | }; 2187 | """, 2188 | ":4 (column 10): parse error: multiple '@' in node name") 2189 | 2190 | def test_dense_input(): 2191 | ''' 2192 | Test that a densely written DTS input round-trips to something 2193 | readable. 2194 | ''' 2195 | 2196 | verify_parse(""" 2197 | /dts-v1/;/{l1:l2:foo{l3:l4:bar{l5:x=l6:/bits/8l9:,[03],"a";};};}; 2198 | """, 2199 | """ 2200 | /dts-v1/; 2201 | 2202 | / { 2203 | l1: l2: foo { 2204 | l3: l4: bar { 2205 | l5: x = l6: [ l7: 01 l8: 02 l9: ], [ 03 ], "a"; 2206 | }; 2207 | }; 2208 | }; 2209 | """) 2210 | 2211 | def test_misc(): 2212 | '''Test miscellaneous errors and non-errors.''' 2213 | 2214 | verify_error_endswith("", ":1 (column 1): parse error: expected '/dts-v1/;' at start of file") 2215 | 2216 | verify_error_endswith(""" 2217 | /dts-v1/; 2218 | """, 2219 | ":2 (column 1): parse error: no root node defined") 2220 | 2221 | verify_error_endswith(""" 2222 | /dts-v1/; /plugin/; 2223 | """, 2224 | ":1 (column 11): parse error: /plugin/ is not supported") 2225 | 2226 | verify_error_endswith(""" 2227 | /dts-v1/; 2228 | 2229 | / { 2230 | foo: foo { 2231 | }; 2232 | }; 2233 | 2234 | // Only one label supported before label references at the top level 2235 | l1: l2: &foo { 2236 | }; 2237 | """, 2238 | ":9 (column 5): parse error: expected label reference (&foo)") 2239 | 2240 | verify_error_endswith(""" 2241 | /dts-v1/; 2242 | 2243 | / { 2244 | foo: {}; 2245 | }; 2246 | """, 2247 | ":4 (column 14): parse error: expected node or property name") 2248 | 2249 | # Multiple /dts-v1/ at the start of a file is fine 2250 | verify_parse(""" 2251 | /dts-v1/; 2252 | /dts-v1/; 2253 | 2254 | / { 2255 | }; 2256 | """, 2257 | """ 2258 | /dts-v1/; 2259 | 2260 | / { 2261 | }; 2262 | """) 2263 | 2264 | def test_dangling_alias(): 2265 | dt = parse(''' 2266 | /dts-v1/; 2267 | 2268 | / { 2269 | aliases { foo = "/missing"; }; 2270 | }; 2271 | ''', force=True) 2272 | assert dt.get_node('/aliases').props['foo'].to_string() == '/missing' 2273 | -------------------------------------------------------------------------------- /src/devicetree/dtlib.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019, Nordic Semiconductor 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | 4 | # Tip: You can view just the documentation with 'pydoc3 devicetree.dtlib' 5 | 6 | """ 7 | A library for extracting information from .dts (devicetree) files. See the 8 | documentation for the DT and Node classes for more information. 9 | 10 | The top-level entry point of the library is the DT class. DT.__init__() takes a 11 | .dts file to parse and a list of directories to search for any /include/d 12 | files. 13 | """ 14 | 15 | import collections 16 | import enum 17 | import errno 18 | import os 19 | import re 20 | import string 21 | import sys 22 | import textwrap 23 | from typing import Any, Dict, Iterable, List, \ 24 | NamedTuple, NoReturn, Optional, Tuple, Union 25 | 26 | # NOTE: tests/test_dtlib.py is the test suite for this library. 27 | 28 | class DTError(Exception): 29 | "Exception raised for devicetree-related errors" 30 | 31 | class Node: 32 | r""" 33 | Represents a node in the devicetree ('node-name { ... };'). 34 | 35 | These attributes are available on Node instances: 36 | 37 | name: 38 | The name of the node (a string). 39 | 40 | unit_addr: 41 | The portion after the '@' in the node's name, or the empty string if the 42 | name has no '@' in it. 43 | 44 | Note that this is a string. Run int(node.unit_addr, 16) to get an 45 | integer. 46 | 47 | props: 48 | A collections.OrderedDict that maps the properties defined on the node to 49 | their values. 'props' is indexed by property name (a string), and values 50 | are Property objects. 51 | 52 | To convert property values to Python numbers or strings, use 53 | dtlib.to_num(), dtlib.to_nums(), or dtlib.to_string(). 54 | 55 | Property values are represented as 'bytes' arrays to support the full 56 | generality of DTS, which allows assignments like 57 | 58 | x = "foo", < 0x12345678 >, [ 9A ]; 59 | 60 | This gives x the value b"foo\0\x12\x34\x56\x78\x9A". Numbers in DTS are 61 | stored in big-endian format. 62 | 63 | nodes: 64 | A collections.OrderedDict containing the subnodes of the node, indexed by 65 | name. 66 | 67 | labels: 68 | A list with all labels pointing to the node, in the same order as the 69 | labels appear, but with duplicates removed. 70 | 71 | 'label_1: label_2: node { ... };' gives 'labels' the value 72 | ["label_1", "label_2"]. 73 | 74 | parent: 75 | The parent Node of the node. 'None' for the root node. 76 | 77 | path: 78 | The path to the node as a string, e.g. "/foo/bar". 79 | 80 | dt: 81 | The DT instance this node belongs to. 82 | """ 83 | 84 | # 85 | # Public interface 86 | # 87 | 88 | def __init__(self, name: str, parent: Optional['Node'], dt: 'DT'): 89 | """ 90 | Node constructor. Not meant to be called directly by clients. 91 | """ 92 | self.name = name 93 | self.parent = parent 94 | self.dt = dt 95 | 96 | if name.count("@") > 1: 97 | dt._parse_error("multiple '@' in node name") 98 | if not name == "/": 99 | for char in name: 100 | if char not in _nodename_chars: 101 | dt._parse_error(f"{self.path}: bad character '{char}' " 102 | "in node name") 103 | 104 | self.props: Dict[str, 'Property'] = collections.OrderedDict() 105 | self.nodes: Dict[str, 'Node'] = collections.OrderedDict() 106 | self.labels: List[str] = [] 107 | self._omit_if_no_ref = False 108 | self._is_referenced = False 109 | 110 | @property 111 | def unit_addr(self) -> str: 112 | """ 113 | See the class documentation. 114 | """ 115 | return self.name.partition("@")[2] 116 | 117 | @property 118 | def path(self) -> str: 119 | """ 120 | See the class documentation. 121 | """ 122 | node_names = [] 123 | 124 | cur = self 125 | while cur.parent: 126 | node_names.append(cur.name) 127 | cur = cur.parent 128 | 129 | return "/" + "/".join(reversed(node_names)) 130 | 131 | def node_iter(self) -> Iterable['Node']: 132 | """ 133 | Returns a generator for iterating over the node and its children, 134 | recursively. 135 | 136 | For example, this will iterate over all nodes in the tree (like 137 | dt.node_iter()). 138 | 139 | for node in dt.root.node_iter(): 140 | ... 141 | """ 142 | yield self 143 | for node in self.nodes.values(): 144 | yield from node.node_iter() 145 | 146 | def _get_prop(self, name: str) -> 'Property': 147 | # Returns the property named 'name' on the node, creating it if it 148 | # doesn't already exist 149 | 150 | prop = self.props.get(name) 151 | if not prop: 152 | prop = Property(self, name) 153 | self.props[name] = prop 154 | return prop 155 | 156 | def _del(self) -> None: 157 | # Removes the node from the tree 158 | self.parent.nodes.pop(self.name) # type: ignore 159 | 160 | def __str__(self): 161 | """ 162 | Returns a DTS representation of the node. Called automatically if the 163 | node is print()ed. 164 | """ 165 | s = "".join(label + ": " for label in self.labels) 166 | 167 | s += f"{self.name} {{\n" 168 | 169 | for prop in self.props.values(): 170 | s += "\t" + str(prop) + "\n" 171 | 172 | for child in self.nodes.values(): 173 | s += textwrap.indent(child.__str__(), "\t") + "\n" 174 | 175 | s += "};" 176 | 177 | return s 178 | 179 | def __repr__(self): 180 | """ 181 | Returns some information about the Node instance. Called automatically 182 | if the Node instance is evaluated. 183 | """ 184 | return f"" 185 | 186 | # See Property.type 187 | class Type(enum.IntEnum): 188 | EMPTY = 0 189 | BYTES = 1 190 | NUM = 2 191 | NUMS = 3 192 | STRING = 4 193 | STRINGS = 5 194 | PATH = 6 195 | PHANDLE = 7 196 | PHANDLES = 8 197 | PHANDLES_AND_NUMS = 9 198 | COMPOUND = 10 199 | 200 | class _MarkerType(enum.IntEnum): 201 | # Types of markers in property values 202 | 203 | # References 204 | PATH = 0 # &foo 205 | PHANDLE = 1 # <&foo> 206 | LABEL = 2 # foo: <1 2 3> 207 | 208 | # Start of data blocks of specific type 209 | UINT8 = 3 # [00 01 02] (and also used for /incbin/) 210 | UINT16 = 4 # /bits/ 16 <1 2 3> 211 | UINT32 = 5 # <1 2 3> 212 | UINT64 = 6 # /bits/ 64 <1 2 3> 213 | STRING = 7 # "foo" 214 | 215 | class Property: 216 | """ 217 | Represents a property ('x = ...'). 218 | 219 | These attributes are available on Property instances: 220 | 221 | name: 222 | The name of the property (a string). 223 | 224 | value: 225 | The value of the property, as a 'bytes' string. Numbers are stored in 226 | big-endian format, and strings are null-terminated. Putting multiple 227 | comma-separated values in an assignment (e.g., 'x = < 1 >, "foo"') will 228 | concatenate the values. 229 | 230 | See the to_*() methods for converting the value to other types. 231 | 232 | type: 233 | The type of the property, inferred from the syntax used in the 234 | assignment. This is one of the following constants (with example 235 | assignments): 236 | 237 | Assignment | Property.type 238 | ----------------------------+------------------------ 239 | foo; | dtlib.Type.EMPTY 240 | foo = []; | dtlib.Type.BYTES 241 | foo = [01 02]; | dtlib.Type.BYTES 242 | foo = /bits/ 8 <1>; | dtlib.Type.BYTES 243 | foo = <1>; | dtlib.Type.NUM 244 | foo = <>; | dtlib.Type.NUMS 245 | foo = <1 2 3>; | dtlib.Type.NUMS 246 | foo = <1 2>, <3>; | dtlib.Type.NUMS 247 | foo = "foo"; | dtlib.Type.STRING 248 | foo = "foo", "bar"; | dtlib.Type.STRINGS 249 | foo = <&l>; | dtlib.Type.PHANDLE 250 | foo = <&l1 &l2 &l3>; | dtlib.Type.PHANDLES 251 | foo = <&l1 &l2>, <&l3>; | dtlib.Type.PHANDLES 252 | foo = <&l1 1 2 &l2 3 4>; | dtlib.Type.PHANDLES_AND_NUMS 253 | foo = <&l1 1 2>, <&l2 3 4>; | dtlib.Type.PHANDLES_AND_NUMS 254 | foo = &l; | dtlib.Type.PATH 255 | *Anything else* | dtlib.Type.COMPOUND 256 | 257 | *Anything else* includes properties mixing phandle (<&label>) and node 258 | path (&label) references with other data. 259 | 260 | Data labels in the property value do not influence the type. 261 | 262 | labels: 263 | A list with all labels pointing to the property, in the same order as the 264 | labels appear, but with duplicates removed. 265 | 266 | 'label_1: label2: x = ...' gives 'labels' the value 267 | {"label_1", "label_2"}. 268 | 269 | offset_labels: 270 | A dictionary that maps any labels within the property's value to their 271 | offset, in bytes. For example, 'x = < 0 label_1: 1 label_2: >' gives 272 | 'offset_labels' the value {"label_1": 4, "label_2": 8}. 273 | 274 | Iteration order will match the order of the labels on Python versions 275 | that preserve dict insertion order. 276 | 277 | node: 278 | The Node the property is on. 279 | """ 280 | 281 | # 282 | # Public interface 283 | # 284 | 285 | def __init__(self, node: Node, name: str): 286 | if "@" in name: 287 | node.dt._parse_error("'@' is only allowed in node names") 288 | 289 | self.name = name 290 | self.node = node 291 | self.value = b"" 292 | self.labels: List[str] = [] 293 | self._label_offset_lst: List[Tuple[str, int]] = [] 294 | # We have to wait to set this until later, when we've got 295 | # the entire tree. 296 | self.offset_labels: Dict[str, int] = {} 297 | 298 | # A list of [offset, label, type] lists (sorted by offset), 299 | # giving the locations of references within the value. 'type' 300 | # is either _MarkerType.PATH, for a node path reference, 301 | # _MarkerType.PHANDLE, for a phandle reference, or 302 | # _MarkerType.LABEL, for a label on/within data. Node paths 303 | # and phandles need to be patched in after parsing. 304 | self._markers: List[List] = [] 305 | 306 | def to_num(self, signed=False) -> int: 307 | """ 308 | Returns the value of the property as a number. 309 | 310 | Raises DTError if the property was not assigned with this syntax (has 311 | Property.type Type.NUM): 312 | 313 | foo = < 1 >; 314 | 315 | signed (default: False): 316 | If True, the value will be interpreted as signed rather than 317 | unsigned. 318 | """ 319 | if self.type is not Type.NUM: 320 | _err("expected property '{0}' on {1} in {2} to be assigned with " 321 | "'{0} = < (number) >;', not '{3}'" 322 | .format(self.name, self.node.path, self.node.dt.filename, 323 | self)) 324 | 325 | return int.from_bytes(self.value, "big", signed=signed) 326 | 327 | def to_nums(self, signed=False) -> List[int]: 328 | """ 329 | Returns the value of the property as a list of numbers. 330 | 331 | Raises DTError if the property was not assigned with this syntax (has 332 | Property.type Type.NUM or Type.NUMS): 333 | 334 | foo = < 1 2 ... >; 335 | 336 | signed (default: False): 337 | If True, the values will be interpreted as signed rather than 338 | unsigned. 339 | """ 340 | if self.type not in (Type.NUM, Type.NUMS): 341 | _err("expected property '{0}' on {1} in {2} to be assigned with " 342 | "'{0} = < (number) (number) ... >;', not '{3}'" 343 | .format(self.name, self.node.path, self.node.dt.filename, 344 | self)) 345 | 346 | return [int.from_bytes(self.value[i:i + 4], "big", signed=signed) 347 | for i in range(0, len(self.value), 4)] 348 | 349 | def to_bytes(self) -> bytes: 350 | """ 351 | Returns the value of the property as a raw 'bytes', like 352 | Property.value, except with added type checking. 353 | 354 | Raises DTError if the property was not assigned with this syntax (has 355 | Property.type Type.BYTES): 356 | 357 | foo = [ 01 ... ]; 358 | """ 359 | if self.type is not Type.BYTES: 360 | _err("expected property '{0}' on {1} in {2} to be assigned with " 361 | "'{0} = [ (byte) (byte) ... ];', not '{3}'" 362 | .format(self.name, self.node.path, self.node.dt.filename, 363 | self)) 364 | 365 | return self.value 366 | 367 | def to_string(self) -> str: 368 | """ 369 | Returns the value of the property as a string. 370 | 371 | Raises DTError if the property was not assigned with this syntax (has 372 | Property.type Type.STRING): 373 | 374 | foo = "string"; 375 | 376 | This function might also raise UnicodeDecodeError if the string is 377 | not valid UTF-8. 378 | """ 379 | if self.type is not Type.STRING: 380 | _err("expected property '{0}' on {1} in {2} to be assigned with " 381 | "'{0} = \"string\";', not '{3}'" 382 | .format(self.name, self.node.path, self.node.dt.filename, 383 | self)) 384 | 385 | try: 386 | ret = self.value.decode("utf-8")[:-1] # Strip null 387 | except UnicodeDecodeError: 388 | _err(f"value of property '{self.name}' ({self.value!r}) " 389 | f"on {self.node.path} in {self.node.dt.filename} " 390 | "is not valid UTF-8") 391 | 392 | return ret # The separate 'return' appeases the type checker. 393 | 394 | def to_strings(self) -> List[str]: 395 | """ 396 | Returns the value of the property as a list of strings. 397 | 398 | Raises DTError if the property was not assigned with this syntax (has 399 | Property.type Type.STRING or Type.STRINGS): 400 | 401 | foo = "string", "string", ... ; 402 | 403 | Also raises DTError if any of the strings are not valid UTF-8. 404 | """ 405 | if self.type not in (Type.STRING, Type.STRINGS): 406 | _err("expected property '{0}' on {1} in {2} to be assigned with " 407 | "'{0} = \"string\", \"string\", ... ;', not '{3}'" 408 | .format(self.name, self.node.path, self.node.dt.filename, 409 | self)) 410 | 411 | try: 412 | ret = self.value.decode("utf-8").split("\0")[:-1] 413 | except UnicodeDecodeError: 414 | _err(f"value of property '{self.name}' ({self.value!r}) " 415 | f"on {self.node.path} in {self.node.dt.filename} " 416 | "is not valid UTF-8") 417 | 418 | return ret # The separate 'return' appeases the type checker. 419 | 420 | def to_node(self) -> Node: 421 | """ 422 | Returns the Node the phandle in the property points to. 423 | 424 | Raises DTError if the property was not assigned with this syntax (has 425 | Property.type Type.PHANDLE). 426 | 427 | foo = < &bar >; 428 | """ 429 | if self.type is not Type.PHANDLE: 430 | _err("expected property '{0}' on {1} in {2} to be assigned with " 431 | "'{0} = < &foo >;', not '{3}'" 432 | .format(self.name, self.node.path, self.node.dt.filename, 433 | self)) 434 | 435 | return self.node.dt.phandle2node[int.from_bytes(self.value, "big")] 436 | 437 | def to_nodes(self) -> List[Node]: 438 | """ 439 | Returns a list with the Nodes the phandles in the property point to. 440 | 441 | Raises DTError if the property value contains anything other than 442 | phandles. All of the following are accepted: 443 | 444 | foo = < > 445 | foo = < &bar >; 446 | foo = < &bar &baz ... >; 447 | foo = < &bar ... >, < &baz ... >; 448 | """ 449 | def type_ok(): 450 | if self.type in (Type.PHANDLE, Type.PHANDLES): 451 | return True 452 | # Also accept 'foo = < >;' 453 | return self.type is Type.NUMS and not self.value 454 | 455 | if not type_ok(): 456 | _err("expected property '{0}' on {1} in {2} to be assigned with " 457 | "'{0} = < &foo &bar ... >;', not '{3}'" 458 | .format(self.name, self.node.path, 459 | self.node.dt.filename, self)) 460 | 461 | return [self.node.dt.phandle2node[int.from_bytes(self.value[i:i + 4], 462 | "big")] 463 | for i in range(0, len(self.value), 4)] 464 | 465 | def to_path(self) -> Node: 466 | """ 467 | Returns the Node referenced by the path stored in the property. 468 | 469 | Raises DTError if the property was not assigned with either of these 470 | syntaxes (has Property.type Type.PATH or Type.STRING): 471 | 472 | foo = &bar; 473 | foo = "/bar"; 474 | 475 | For the second case, DTError is raised if the path does not exist. 476 | """ 477 | if self.type not in (Type.PATH, Type.STRING): 478 | _err("expected property '{0}' on {1} in {2} to be assigned with " 479 | "either '{0} = &foo' or '{0} = \"/path/to/node\"', not '{3}'" 480 | .format(self.name, self.node.path, self.node.dt.filename, 481 | self)) 482 | 483 | try: 484 | path = self.value.decode("utf-8")[:-1] 485 | except UnicodeDecodeError: 486 | _err(f"value of property '{self.name}' ({self.value!r}) " 487 | f"on {self.node.path} in {self.node.dt.filename} " 488 | "is not valid UTF-8") 489 | 490 | try: 491 | ret = self.node.dt.get_node(path) 492 | except DTError: 493 | _err(f"property '{self.name}' on {self.node.path} in " 494 | f"{self.node.dt.filename} points to the non-existent node " 495 | f'"{path}"') 496 | 497 | return ret # The separate 'return' appeases the type checker. 498 | 499 | @property 500 | def type(self) -> int: 501 | """ 502 | See the class docstring. 503 | """ 504 | # Data labels (e.g. 'foo = label: <3>') are irrelevant, so filter them 505 | # out 506 | types = [marker[1] for marker in self._markers 507 | if marker[1] != _MarkerType.LABEL] 508 | 509 | if not types: 510 | return Type.EMPTY 511 | 512 | if types == [_MarkerType.UINT8]: 513 | return Type.BYTES 514 | 515 | if types == [_MarkerType.UINT32]: 516 | return Type.NUM if len(self.value) == 4 else Type.NUMS 517 | 518 | # Treat 'foo = <1 2 3>, <4 5>, ...' as Type.NUMS too 519 | if set(types) == {_MarkerType.UINT32}: 520 | return Type.NUMS 521 | 522 | if set(types) == {_MarkerType.STRING}: 523 | return Type.STRING if len(types) == 1 else Type.STRINGS 524 | 525 | if types == [_MarkerType.PATH]: 526 | return Type.PATH 527 | 528 | if types == [_MarkerType.UINT32, _MarkerType.PHANDLE] and \ 529 | len(self.value) == 4: 530 | return Type.PHANDLE 531 | 532 | if set(types) == {_MarkerType.UINT32, _MarkerType.PHANDLE}: 533 | if len(self.value) == 4*types.count(_MarkerType.PHANDLE): 534 | # Array with just phandles in it 535 | return Type.PHANDLES 536 | # Array with both phandles and numbers 537 | return Type.PHANDLES_AND_NUMS 538 | 539 | return Type.COMPOUND 540 | 541 | def __str__(self): 542 | s = "".join(label + ": " for label in self.labels) + self.name 543 | if not self.value: 544 | return s + ";" 545 | 546 | s += " =" 547 | 548 | for i, (pos, marker_type, ref) in enumerate(self._markers): 549 | if i < len(self._markers) - 1: 550 | next_marker = self._markers[i + 1] 551 | else: 552 | next_marker = None 553 | 554 | # End of current marker 555 | end = next_marker[0] if next_marker else len(self.value) 556 | 557 | if marker_type is _MarkerType.STRING: 558 | # end - 1 to strip off the null terminator 559 | s += f' "{_decode_and_escape(self.value[pos:end - 1])}"' 560 | if end != len(self.value): 561 | s += "," 562 | elif marker_type is _MarkerType.PATH: 563 | s += " &" + ref 564 | if end != len(self.value): 565 | s += "," 566 | else: 567 | # <> or [] 568 | 569 | if marker_type is _MarkerType.LABEL: 570 | s += f" {ref}:" 571 | elif marker_type is _MarkerType.PHANDLE: 572 | s += " &" + ref 573 | pos += 4 574 | # Subtle: There might be more data between the phandle and 575 | # the next marker, so we can't 'continue' here 576 | else: # marker_type is _MarkerType.UINT* 577 | elm_size = _TYPE_TO_N_BYTES[marker_type] 578 | s += _N_BYTES_TO_START_STR[elm_size] 579 | 580 | while pos != end: 581 | num = int.from_bytes(self.value[pos:pos + elm_size], 582 | "big") 583 | if elm_size == 1: 584 | s += f" {num:02X}" 585 | else: 586 | s += f" {hex(num)}" 587 | 588 | pos += elm_size 589 | 590 | if pos != 0 and \ 591 | (not next_marker or 592 | next_marker[1] not in (_MarkerType.PHANDLE, _MarkerType.LABEL)): 593 | 594 | s += _N_BYTES_TO_END_STR[elm_size] 595 | if pos != len(self.value): 596 | s += "," 597 | 598 | return s + ";" 599 | 600 | 601 | def __repr__(self): 602 | return f"" 604 | 605 | # 606 | # Internal functions 607 | # 608 | 609 | def _add_marker(self, marker_type: _MarkerType, data: Any = None): 610 | # Helper for registering markers in the value that are processed after 611 | # parsing. See _fixup_props(). 'marker_type' identifies the type of 612 | # marker, and 'data' has any optional data associated with the marker. 613 | 614 | # len(self.value) gives the current offset. This function is called 615 | # while the value is built. We use a list instead of a tuple to be able 616 | # to fix up offsets later (they might increase if the value includes 617 | # path references, e.g. 'foo = &bar, <3>;', which are expanded later). 618 | self._markers.append([len(self.value), marker_type, data]) 619 | 620 | # For phandle references, add a dummy value with the same length as a 621 | # phandle. This is handy for the length check in _register_phandles(). 622 | if marker_type is _MarkerType.PHANDLE: 623 | self.value += b"\0\0\0\0" 624 | 625 | class _T(enum.IntEnum): 626 | # Token IDs used by the DT lexer. 627 | 628 | # These values must be contiguous and start from 1. 629 | INCLUDE = 1 630 | LINE = 2 631 | STRING = 3 632 | DTS_V1 = 4 633 | PLUGIN = 5 634 | MEMRESERVE = 6 635 | BITS = 7 636 | DEL_PROP = 8 637 | DEL_NODE = 9 638 | OMIT_IF_NO_REF = 10 639 | LABEL = 11 640 | CHAR_LITERAL = 12 641 | REF = 13 642 | INCBIN = 14 643 | SKIP = 15 644 | EOF = 16 645 | 646 | # These values must be larger than the above contiguous range. 647 | NUM = 17 648 | PROPNODENAME = 18 649 | MISC = 19 650 | BYTE = 20 651 | BAD = 21 652 | 653 | class _FileStackElt(NamedTuple): 654 | # Used for maintaining the /include/ stack. 655 | 656 | filename: str 657 | lineno: int 658 | contents: str 659 | pos: int 660 | 661 | _TokVal = Union[int, str] 662 | 663 | class _Token(NamedTuple): 664 | id: int 665 | val: _TokVal 666 | 667 | def __repr__(self): 668 | id_repr = _T(self.id).name 669 | return f'Token(id=_T.{id_repr}, val={repr(self.val)})' 670 | 671 | class DT: 672 | """ 673 | Represents a devicetree parsed from a .dts file (or from many files, if the 674 | .dts file /include/s other files). Creating many instances of this class is 675 | fine. The library has no global state. 676 | 677 | These attributes are available on DT instances: 678 | 679 | root: 680 | A Node instance representing the root (/) node. 681 | 682 | alias2node: 683 | A dictionary that maps maps alias strings (from /aliases) to Node 684 | instances 685 | 686 | label2node: 687 | A dictionary that maps each node label (a string) to the Node instance 688 | for the node. 689 | 690 | label2prop: 691 | A dictionary that maps each property label (a string) to a Property 692 | instance. 693 | 694 | label2prop_offset: 695 | A dictionary that maps each label (a string) within a property value 696 | (e.g., 'x = label_1: < 1 label2: 2 >;') to a (prop, offset) tuple, where 697 | 'prop' is a Property instance and 'offset' the byte offset (0 for label_1 698 | and 4 for label_2 in the example). 699 | 700 | phandle2node: 701 | A dictionary that maps each phandle (a number) to a Node instance. 702 | 703 | memreserves: 704 | A list of (labels, address, length) tuples for the /memreserve/s in the 705 | .dts file, in the same order as they appear in the file. 706 | 707 | 'labels' is a possibly empty set with all labels preceding the memreserve 708 | (e.g., 'label1: label2: /memreserve/ ...'). 'address' and 'length' are 709 | numbers. 710 | 711 | filename: 712 | The filename passed to the DT constructor. 713 | """ 714 | 715 | # 716 | # Public interface 717 | # 718 | 719 | def __init__(self, filename: str, include_path: Iterable[str] = (), 720 | force: bool = False): 721 | """ 722 | Parses a DTS file to create a DT instance. Raises OSError if 'filename' 723 | can't be opened, and DTError for any parse errors. 724 | 725 | filename: 726 | Path to the .dts file to parse. 727 | 728 | include_path: 729 | An iterable (e.g. list or tuple) containing paths to search for 730 | /include/d and /incbin/'d files. By default, files are only looked up 731 | relative to the .dts file that contains the /include/ or /incbin/. 732 | 733 | force: 734 | Try not to raise DTError even if the input tree has errors. 735 | For experimental use; results not guaranteed. 736 | """ 737 | self.filename = filename 738 | self._include_path = list(include_path) 739 | self._force = force 740 | 741 | with open(filename, encoding="utf-8") as f: 742 | self._file_contents = f.read() 743 | 744 | self._tok_i = self._tok_end_i = 0 745 | self._filestack: List[_FileStackElt] = [] 746 | 747 | self.alias2node: Dict[str, Node] = {} 748 | 749 | self._lexer_state: int = _DEFAULT 750 | self._saved_token: Optional[_Token] = None 751 | 752 | self._lineno: int = 1 753 | 754 | self._root: Optional[Node] = None 755 | 756 | self._parse_dt() 757 | 758 | self._register_phandles() 759 | self._fixup_props() 760 | self._register_aliases() 761 | self._remove_unreferenced() 762 | self._register_labels() 763 | 764 | @property 765 | def root(self) -> Node: 766 | """ 767 | See the class documentation. 768 | """ 769 | # This is necessary because mypy can't tell that we never 770 | # treat self._root as a non-None value until it's initialized 771 | # properly in _parse_dt(). 772 | return self._root # type: ignore 773 | 774 | def get_node(self, path: str) -> Node: 775 | """ 776 | Returns the Node instance for the node with path or alias 'path' (a 777 | string). Raises DTError if the path or alias doesn't exist. 778 | 779 | For example, both dt.get_node("/foo/bar") and dt.get_node("bar-alias") 780 | will return the 'bar' node below: 781 | 782 | /dts-v1/; 783 | 784 | / { 785 | foo { 786 | bar_label: bar { 787 | baz { 788 | }; 789 | }; 790 | }; 791 | 792 | aliases { 793 | bar-alias = &bar-label; 794 | }; 795 | }; 796 | 797 | Fetching subnodes via aliases is supported: 798 | dt.get_node("bar-alias/baz") returns the 'baz' node. 799 | """ 800 | if path.startswith("/"): 801 | return _root_and_path_to_node(self.root, path, path) 802 | 803 | # Path does not start with '/'. First component must be an alias. 804 | alias, _, rest = path.partition("/") 805 | if alias not in self.alias2node: 806 | _err(f"no alias '{alias}' found -- did you forget the leading " 807 | "'/' in the node path?") 808 | 809 | return _root_and_path_to_node(self.alias2node[alias], rest, path) 810 | 811 | def has_node(self, path: str) -> bool: 812 | """ 813 | Returns True if the path or alias 'path' exists. See Node.get_node(). 814 | """ 815 | try: 816 | self.get_node(path) 817 | return True 818 | except DTError: 819 | return False 820 | 821 | def node_iter(self) -> Iterable[Node]: 822 | """ 823 | Returns a generator for iterating over all nodes in the devicetree. 824 | 825 | For example, this will print the name of each node that has a property 826 | called 'foo': 827 | 828 | for node in dt.node_iter(): 829 | if "foo" in node.props: 830 | print(node.name) 831 | """ 832 | yield from self.root.node_iter() 833 | 834 | def __str__(self): 835 | """ 836 | Returns a DTS representation of the devicetree. Called automatically if 837 | the DT instance is print()ed. 838 | """ 839 | s = "/dts-v1/;\n\n" 840 | 841 | if self.memreserves: 842 | for labels, address, offset in self.memreserves: 843 | # List the labels in a consistent order to help with testing 844 | for label in labels: 845 | s += f"{label}: " 846 | s += f"/memreserve/ {address:#018x} {offset:#018x};\n" 847 | s += "\n" 848 | 849 | return s + str(self.root) 850 | 851 | def __repr__(self): 852 | """ 853 | Returns some information about the DT instance. Called automatically if 854 | the DT instance is evaluated. 855 | """ 856 | return f"DT(filename='{self.filename}', " \ 857 | f"include_path={self._include_path})" 858 | 859 | # 860 | # Parsing 861 | # 862 | 863 | def _parse_dt(self): 864 | # Top-level parsing loop 865 | 866 | self._parse_header() 867 | self._parse_memreserves() 868 | 869 | while True: 870 | tok = self._next_token() 871 | 872 | if tok.val == "/": 873 | # '/ { ... };', the root node 874 | if not self._root: 875 | self._root = Node(name="/", parent=None, dt=self) 876 | self._parse_node(self.root) 877 | 878 | elif tok.id in (_T.LABEL, _T.REF): 879 | # '&foo { ... };' or 'label: &foo { ... };'. The C tools only 880 | # support a single label here too. 881 | 882 | if tok.id == _T.LABEL: 883 | label = tok.val 884 | tok = self._next_token() 885 | if tok.id != _T.REF: 886 | self._parse_error("expected label reference (&foo)") 887 | else: 888 | label = None 889 | 890 | try: 891 | node = self._ref2node(tok.val) 892 | except DTError as e: 893 | self._parse_error(e) 894 | node = self._parse_node(node) 895 | 896 | if label: 897 | _append_no_dup(node.labels, label) 898 | 899 | elif tok.id == _T.DEL_NODE: 900 | self._next_ref2node()._del() 901 | self._expect_token(";") 902 | 903 | elif tok.id == _T.OMIT_IF_NO_REF: 904 | self._next_ref2node()._omit_if_no_ref = True 905 | self._expect_token(";") 906 | 907 | elif tok.id == _T.EOF: 908 | if not self._root: 909 | self._parse_error("no root node defined") 910 | return 911 | 912 | else: 913 | self._parse_error("expected '/' or label reference (&foo)") 914 | 915 | def _parse_header(self): 916 | # Parses /dts-v1/ (expected) and /plugin/ (unsupported) at the start of 917 | # files. There may be multiple /dts-v1/ at the start of a file. 918 | 919 | has_dts_v1 = False 920 | 921 | while self._peek_token().id == _T.DTS_V1: 922 | has_dts_v1 = True 923 | self._next_token() 924 | self._expect_token(";") 925 | # /plugin/ always comes after /dts-v1/ 926 | if self._peek_token().id == _T.PLUGIN: 927 | self._parse_error("/plugin/ is not supported") 928 | 929 | if not has_dts_v1: 930 | self._parse_error("expected '/dts-v1/;' at start of file") 931 | 932 | def _parse_memreserves(self): 933 | # Parses /memreserve/, which appears after /dts-v1/ 934 | 935 | self.memreserves = [] 936 | while True: 937 | # Labels before /memreserve/ 938 | labels = [] 939 | while self._peek_token().id == _T.LABEL: 940 | _append_no_dup(labels, self._next_token().val) 941 | 942 | if self._peek_token().id == _T.MEMRESERVE: 943 | self._next_token() 944 | self.memreserves.append( 945 | (labels, self._eval_prim(), self._eval_prim())) 946 | self._expect_token(";") 947 | elif labels: 948 | self._parse_error("expected /memreserve/ after labels at " 949 | "beginning of file") 950 | else: 951 | return 952 | 953 | def _parse_node(self, node): 954 | # Parses the '{ ... };' part of 'node-name { ... };'. Returns the new 955 | # Node. 956 | 957 | self._expect_token("{") 958 | while True: 959 | labels, omit_if_no_ref = self._parse_propnode_labels() 960 | tok = self._next_token() 961 | 962 | if tok.id == _T.PROPNODENAME: 963 | if self._peek_token().val == "{": 964 | # ' { ...', expect node 965 | 966 | # Fetch the existing node if it already exists. This 967 | # happens when overriding nodes. 968 | child = node.nodes.get(tok.val) or \ 969 | Node(name=tok.val, parent=node, dt=self) 970 | 971 | for label in labels: 972 | _append_no_dup(child.labels, label) 973 | 974 | if omit_if_no_ref: 975 | child._omit_if_no_ref = True 976 | 977 | node.nodes[child.name] = child 978 | self._parse_node(child) 979 | 980 | else: 981 | # Not ' { ...', expect property assignment 982 | 983 | if omit_if_no_ref: 984 | self._parse_error( 985 | "/omit-if-no-ref/ can only be used on nodes") 986 | 987 | prop = node._get_prop(tok.val) 988 | 989 | if self._check_token("="): 990 | self._parse_assignment(prop) 991 | elif not self._check_token(";"): 992 | # ';' is for an empty property, like 'foo;' 993 | self._parse_error("expected '{', '=', or ';'") 994 | 995 | for label in labels: 996 | _append_no_dup(prop.labels, label) 997 | 998 | elif tok.id == _T.DEL_NODE: 999 | tok2 = self._next_token() 1000 | if tok2.id != _T.PROPNODENAME: 1001 | self._parse_error("expected node name") 1002 | if tok2.val in node.nodes: 1003 | node.nodes[tok2.val]._del() 1004 | self._expect_token(";") 1005 | 1006 | elif tok.id == _T.DEL_PROP: 1007 | tok2 = self._next_token() 1008 | if tok2.id != _T.PROPNODENAME: 1009 | self._parse_error("expected property name") 1010 | node.props.pop(tok2.val, None) 1011 | self._expect_token(";") 1012 | 1013 | elif tok.val == "}": 1014 | self._expect_token(";") 1015 | return node 1016 | 1017 | else: 1018 | self._parse_error("expected node name, property name, or '}'") 1019 | 1020 | def _parse_propnode_labels(self): 1021 | # _parse_node() helpers for parsing labels and /omit-if-no-ref/s before 1022 | # nodes and properties. Returns a (