From 9387cf56b4773e0be9b7c00f72a585cd310e57cd Mon Sep 17 00:00:00 2001 From: Christophe Dufaza Date: Thu, 8 Feb 2024 18:34:12 +0100 Subject: [PATCH] ci: backport RFC-DTSh as dtsh v0.2.0-rc1 Mirror and package RFC-DTSh code base. See https://github.com/zephyrproject-rtos/zephyr/pull/59863 --- .gitignore | 2 +- README.md | 192 ++ README.org | 836 -------- README.rst | 60 +- doc/img/buses.png | Bin 0 -> 124693 bytes doc/img/devicetree-logo.png | Bin 7331 -> 0 bytes doc/img/devicetree-logo.svg | 14 - doc/img/dtsh_home.png | Bin 526311 -> 0 bytes doc/img/soc.svg | 290 --- doc/ug/DTSh.pdf | Bin 0 -> 223183 bytes etc/sh/interactive-tests.sh | 428 ---- pyproject.toml | 17 + pyrightconfig.json | 25 - pytest.ini | 9 - requirements-dev.txt | 7 +- requirements-lsp.txt | 3 + requirements.txt | 3 + setup.cfg | 173 ++ setup.py | 176 +- src/dtsh/__init__.py | 5 + src/dtsh/autocomp.py | 839 +++++++- src/dtsh/builtin_alias.py | 134 -- src/dtsh/builtin_cat.py | 131 -- src/dtsh/builtin_cd.py | 68 - src/dtsh/builtin_chosen.py | 130 -- src/dtsh/builtin_find.py | 733 ------- src/dtsh/builtin_ls.py | 276 --- src/dtsh/builtin_man.py | 149 -- src/dtsh/builtin_pwd.py | 57 - src/dtsh/builtin_tree.py | 179 -- src/dtsh/builtin_uname.py | 436 ---- src/dtsh/builtins/__init__.py | 5 + src/dtsh/builtins/alias.py | 73 + src/dtsh/builtins/cd.py | 39 + src/dtsh/builtins/chosen.py | 73 + src/dtsh/builtins/find.py | 297 +++ src/dtsh/builtins/ls.py | 217 ++ src/dtsh/builtins/pwd.py | 31 + src/dtsh/builtins/tree.py | 120 ++ src/dtsh/cli.py | 147 +- src/dtsh/config.py | 607 +++++- src/dtsh/dts.py | 824 +++++++ src/dtsh/dtsh.ini | 367 ++++ src/dtsh/dtsh.py | 1627 -------------- src/dtsh/io.py | 259 +++ src/dtsh/man.py | 683 ------ src/dtsh/model.py | 1889 +++++++++++++++++ src/dtsh/modelutils.py | 724 +++++++ src/dtsh/rich/__init__.py | 5 + src/dtsh/rich/autocomp.py | 226 ++ src/dtsh/rich/io.py | 494 +++++ src/dtsh/rich/modelview.py | 1421 +++++++++++++ src/dtsh/rich/session.py | 142 ++ src/dtsh/rich/shellutils.py | 440 ++++ src/dtsh/rich/svg.py | 767 +++++++ src/dtsh/rich/text.py | 229 ++ src/dtsh/rich/theme.ini | 149 ++ src/dtsh/rich/theme.py | 145 ++ src/dtsh/rich/tui.py | 221 ++ src/dtsh/rl.py | 299 ++- src/dtsh/session.py | 603 +++--- src/dtsh/shell.py | 1323 +++++++++++- src/dtsh/shellutils.py | 924 ++++++++ src/dtsh/systools.py | 286 --- src/dtsh/term.py | 111 - src/dtsh/theme | 121 -- src/dtsh/tui.py | 1814 ---------------- src/dtsh/typed.py | 5 + tests/__init__.py | 3 + tests/bindings/bar-bus.yaml | 7 - tests/bindings/child-binding-with-compat.yaml | 20 - tests/bindings/child-binding.yaml | 19 - tests/bindings/child.yaml | 8 - tests/bindings/defaults.yaml | 36 - tests/bindings/deprecated.yaml | 15 - tests/bindings/device-on-any-bus.yaml | 5 - tests/bindings/device-on-bar-bus.yaml | 7 - tests/bindings/device-on-foo-bus.yaml | 7 - tests/bindings/enums.yaml | 36 - tests/bindings/false-positive.yaml | 4 - tests/bindings/foo-bus.yaml | 7 - tests/bindings/foo-optional.yaml | 4 - tests/bindings/foo-required.yaml | 4 - tests/bindings/gpio-dst.yaml | 8 - tests/bindings/gpio-src.yaml | 9 - tests/bindings/grandchild-1.yaml | 10 - tests/bindings/grandchild-2.yaml | 6 - tests/bindings/grandchild-3.yaml | 6 - tests/bindings/interrupt-1-cell.yaml | 8 - tests/bindings/interrupt-2-cell.yaml | 9 - tests/bindings/interrupt-3-cell.yaml | 10 - tests/bindings/multidir.yaml | 5 - tests/bindings/order-1.yaml | 7 - tests/bindings/order-2.yaml | 7 - tests/bindings/parent.yaml | 13 - .../bindings/phandle-array-controller-0.yaml | 7 - .../bindings/phandle-array-controller-1.yaml | 11 - .../bindings/phandle-array-controller-2.yaml | 9 - tests/bindings/props.yaml | 50 - tests/dtsh_uthelpers.py | 634 ++++++ tests/res/Build_gnuarm/CMakeCache.txt | 572 +++++ tests/res/Build_gnuarm/zephyr/zephyr.dts | 812 +++++++ tests/res/Build_zephyr/CMakeCache.txt | 578 +++++ tests/res/Build_zephyr/zephyr/zephyr.dts | 812 +++++++ tests/res/CMakeCache.txt | 8 + tests/res/README | 67 + tests/res/fs/A.txt | 1 + tests/res/fs/A/dummy.txt | 1 + tests/res/fs/a.txt | 1 + tests/res/fs/a/x/dummy.txt | 1 + tests/res/fs/ab.txt | 1 + tests/res/fs/foo/dummy.txt | 1 + tests/res/ini/override.ini | 5 + tests/res/ini/test.ini | 37 + tests/res/theme/override.ini | 5 + tests/res/theme/test.ini | 7 + tests/res/yaml/i2c-device.yaml | 13 + tests/res/yaml/power.yaml | 31 + tests/res/yaml/sensor-device.yaml | 19 + tests/res/zephyr.dts | 812 +++++++ tests/test.dts | 534 ----- tests/test_autocomp.py | 128 -- tests/test_dtsh.py | 422 ---- tests/test_dtsh_autocomp.py | 444 ++++ tests/test_dtsh_builtin_alias.py | 48 + tests/test_dtsh_builtin_cd.py | 35 + tests/test_dtsh_builtin_chosen.py | 48 + tests/test_dtsh_builtin_find.py | 86 + tests/test_dtsh_builtin_ls.py | 60 + tests/test_dtsh_builtin_pwd.py | 28 + tests/test_dtsh_builtin_tree.py | 59 + tests/test_dtsh_config.py | 136 ++ tests/test_dtsh_dts.py | 345 +++ tests/test_dtsh_io.py | 45 + tests/test_dtsh_model.py | 981 +++++++++ tests/test_dtsh_modelutils.py | 1111 ++++++++++ tests/test_dtsh_rich_shellutils.py | 166 ++ tests/test_dtsh_rich_svg.py | 223 ++ tests/test_dtsh_shell.py | 675 ++++++ tests/test_dtsh_shellutils.py | 1452 +++++++++++++ tests/test_dtsh_theme.py | 90 + tests/test_dtsh_uthelpers.py | 59 + tests/test_shell.py | 69 - tests/typed.py | 5 + 144 files changed, 24212 insertions(+), 10891 deletions(-) create mode 100644 README.md delete mode 100644 README.org create mode 100644 doc/img/buses.png delete mode 100644 doc/img/devicetree-logo.png delete mode 100644 doc/img/devicetree-logo.svg delete mode 100644 doc/img/dtsh_home.png delete mode 100644 doc/img/soc.svg create mode 100644 doc/ug/DTSh.pdf delete mode 100755 etc/sh/interactive-tests.sh delete mode 100644 pyrightconfig.json delete mode 100644 pytest.ini create mode 100644 requirements-lsp.txt create mode 100644 requirements.txt create mode 100644 setup.cfg delete mode 100644 src/dtsh/builtin_alias.py delete mode 100644 src/dtsh/builtin_cat.py delete mode 100644 src/dtsh/builtin_cd.py delete mode 100644 src/dtsh/builtin_chosen.py delete mode 100644 src/dtsh/builtin_find.py delete mode 100644 src/dtsh/builtin_ls.py delete mode 100644 src/dtsh/builtin_man.py delete mode 100644 src/dtsh/builtin_pwd.py delete mode 100644 src/dtsh/builtin_tree.py delete mode 100644 src/dtsh/builtin_uname.py create mode 100644 src/dtsh/builtins/__init__.py create mode 100644 src/dtsh/builtins/alias.py create mode 100644 src/dtsh/builtins/cd.py create mode 100644 src/dtsh/builtins/chosen.py create mode 100644 src/dtsh/builtins/find.py create mode 100644 src/dtsh/builtins/ls.py create mode 100644 src/dtsh/builtins/pwd.py create mode 100644 src/dtsh/builtins/tree.py create mode 100644 src/dtsh/dts.py create mode 100644 src/dtsh/dtsh.ini delete mode 100644 src/dtsh/dtsh.py create mode 100644 src/dtsh/io.py delete mode 100644 src/dtsh/man.py create mode 100644 src/dtsh/model.py create mode 100644 src/dtsh/modelutils.py create mode 100644 src/dtsh/rich/__init__.py create mode 100644 src/dtsh/rich/autocomp.py create mode 100644 src/dtsh/rich/io.py create mode 100644 src/dtsh/rich/modelview.py create mode 100644 src/dtsh/rich/session.py create mode 100644 src/dtsh/rich/shellutils.py create mode 100644 src/dtsh/rich/svg.py create mode 100644 src/dtsh/rich/text.py create mode 100644 src/dtsh/rich/theme.ini create mode 100644 src/dtsh/rich/theme.py create mode 100644 src/dtsh/rich/tui.py create mode 100644 src/dtsh/shellutils.py delete mode 100644 src/dtsh/systools.py delete mode 100644 src/dtsh/term.py delete mode 100644 src/dtsh/theme delete mode 100644 src/dtsh/tui.py create mode 100644 src/dtsh/typed.py create mode 100644 tests/__init__.py delete mode 100644 tests/bindings/bar-bus.yaml delete mode 100644 tests/bindings/child-binding-with-compat.yaml delete mode 100644 tests/bindings/child-binding.yaml delete mode 100644 tests/bindings/child.yaml delete mode 100644 tests/bindings/defaults.yaml delete mode 100644 tests/bindings/deprecated.yaml delete mode 100644 tests/bindings/device-on-any-bus.yaml delete mode 100644 tests/bindings/device-on-bar-bus.yaml delete mode 100644 tests/bindings/device-on-foo-bus.yaml delete mode 100644 tests/bindings/enums.yaml delete mode 100644 tests/bindings/false-positive.yaml delete mode 100644 tests/bindings/foo-bus.yaml delete mode 100644 tests/bindings/foo-optional.yaml delete mode 100644 tests/bindings/foo-required.yaml delete mode 100644 tests/bindings/gpio-dst.yaml delete mode 100644 tests/bindings/gpio-src.yaml delete mode 100644 tests/bindings/grandchild-1.yaml delete mode 100644 tests/bindings/grandchild-2.yaml delete mode 100644 tests/bindings/grandchild-3.yaml delete mode 100644 tests/bindings/interrupt-1-cell.yaml delete mode 100644 tests/bindings/interrupt-2-cell.yaml delete mode 100644 tests/bindings/interrupt-3-cell.yaml delete mode 100644 tests/bindings/multidir.yaml delete mode 100644 tests/bindings/order-1.yaml delete mode 100644 tests/bindings/order-2.yaml delete mode 100644 tests/bindings/parent.yaml delete mode 100644 tests/bindings/phandle-array-controller-0.yaml delete mode 100644 tests/bindings/phandle-array-controller-1.yaml delete mode 100644 tests/bindings/phandle-array-controller-2.yaml delete mode 100644 tests/bindings/props.yaml create mode 100644 tests/dtsh_uthelpers.py create mode 100644 tests/res/Build_gnuarm/CMakeCache.txt create mode 100644 tests/res/Build_gnuarm/zephyr/zephyr.dts create mode 100644 tests/res/Build_zephyr/CMakeCache.txt create mode 100644 tests/res/Build_zephyr/zephyr/zephyr.dts create mode 100644 tests/res/CMakeCache.txt create mode 100644 tests/res/README create mode 100644 tests/res/fs/A.txt create mode 100644 tests/res/fs/A/dummy.txt create mode 100644 tests/res/fs/a.txt create mode 100644 tests/res/fs/a/x/dummy.txt create mode 100644 tests/res/fs/ab.txt create mode 100644 tests/res/fs/foo/dummy.txt create mode 100644 tests/res/ini/override.ini create mode 100644 tests/res/ini/test.ini create mode 100644 tests/res/theme/override.ini create mode 100644 tests/res/theme/test.ini create mode 100644 tests/res/yaml/i2c-device.yaml create mode 100644 tests/res/yaml/power.yaml create mode 100644 tests/res/yaml/sensor-device.yaml create mode 100644 tests/res/zephyr.dts delete mode 100644 tests/test.dts delete mode 100644 tests/test_autocomp.py delete mode 100644 tests/test_dtsh.py create mode 100644 tests/test_dtsh_autocomp.py create mode 100644 tests/test_dtsh_builtin_alias.py create mode 100644 tests/test_dtsh_builtin_cd.py create mode 100644 tests/test_dtsh_builtin_chosen.py create mode 100644 tests/test_dtsh_builtin_find.py create mode 100644 tests/test_dtsh_builtin_ls.py create mode 100644 tests/test_dtsh_builtin_pwd.py create mode 100644 tests/test_dtsh_builtin_tree.py create mode 100644 tests/test_dtsh_config.py create mode 100644 tests/test_dtsh_dts.py create mode 100644 tests/test_dtsh_io.py create mode 100644 tests/test_dtsh_model.py create mode 100644 tests/test_dtsh_modelutils.py create mode 100644 tests/test_dtsh_rich_shellutils.py create mode 100644 tests/test_dtsh_rich_svg.py create mode 100644 tests/test_dtsh_shell.py create mode 100644 tests/test_dtsh_shellutils.py create mode 100644 tests/test_dtsh_theme.py create mode 100644 tests/test_dtsh_uthelpers.py delete mode 100644 tests/test_shell.py create mode 100644 tests/typed.py diff --git a/.gitignore b/.gitignore index f75d8ba..2bbcf11 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -.venv/ +.venv*/ __pycache__ /build/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..c51dc59 --- /dev/null +++ b/README.md @@ -0,0 +1,192 @@ +# DTSh + +**DTSh** is a Devicetree source (DTS) file viewer with a shell-like command line interface: + +- browse a devicetree through a hierarchical file system metaphor +- search for devices, bindings, buses or interrupts with flexible criteria +- filter, sort and format commands output +- generate simple documentation artifacts (text, HTML, SVG) by redirecting the output of commands to files +- *rich* Textual User Interface, command line auto-completion, command history, user themes + +The considered use cases include: + +- to help getting started with Devicetree: hierarchically or semantically explore a devicetree, contextually access binding files or the Devicetree specification, save figures to illustrate notes +- to have on hand a simple DTS file viewer: quickly check the enabled buses and connected devices or the RAM and Flash memory, get a first insight when debugging a Devicetree issue, document hardware configurations + +![dtsh](doc/img/buses.png) + + +## Status + +This project started as a Proof of Concept for a simple tool that could assist newcomers to Zephyr in understanding what a devicetree is, and how bindings describe and constrain its content: source code and documentation for this prototype are still available as the [main](https://github.com/dottspina/dtsh/tree/main) branch of this repository. + +The PoC has since been rewritten and the new code base serves as a proposal to upstream DTSh as a West command: [RFC-DTSh, shell-like interface with Devicetree](https://github.com/zephyrproject-rtos/zephyr/pull/59863) + +The **default** branch (`dtsh-next`) now [mirrors](https://github.com/dottspina/zephyr/tree/rfc-dtsh/scripts/dts/dtsh) and [packages](https://pypi.org/project/dtsh/) this work to ensure that interested users won't install and test outdated software, or comment on obsolete issues. + +DTSh releases target versions of the Zephyr hardware model, refer to the table bellow: + +| DTSh | Zephyr | python-devicetree | +|---------------------------------------------------------------------|-------------|------------------------------------------------------------------------| +| [0.2rc1](https://github.com/dottspina/dtsh/releases/tag/v0.2.0-rc1) | 3.5.x | [403640b](https://github.com/zephyrproject-rtos/zephyr/commit/403640b) | +| [0.2rc1](https://github.com/dottspina/dtsh/releases/tag/v0.2.0-rc1) | *3.6.0-rc1* | [403640b](https://github.com/zephyrproject-rtos/zephyr/commit/403640b) | + +**Latest stable release**: [0.2rc1](https://github.com/dottspina/dtsh/releases/tag/v0.2.0-rc1) + + +## Getting Started + +The DTSh [User Guide](doc/ug/DTSh.pdf) contains extensive documentation and examples (just replace `west dtsh` with `dtsh`): bellow are simple instructions to, well, get started. + + +### Requirements + +DTSh should install and run OOTB on GNU Linux (including WSL) and macOS with Python 3.8 to 3.11. + +DTSh will install the following requirements from PyPI: + +| Requirement | PyPI | +|------------------------------------------------------|------------------------------------------------------| +| PyYAML, YAML parser | [PyYAML](https://pypi.org/project/PyYAML/) | +| Textualize's rich library for *beautiful formatting* | [rich](https://pypi.org/project/rich/) | +| Stand-alone GNU readline module (macOS only) | [gnureadline](https://pypi.org/project/gnureadline/) | + +On **Windows**, the [GNU Readline](https://tiswww.cwru.edu/php/chet/readline/rltop.html) support will likely be disabled, resulting in a **degraded user experience**: no command line auto-completion nor command history. + +> ⚠ **python-devicetree**: +> +> DTSh relies on the [python-devicetree](https://github.com/zephyrproject-rtos/zephyr/tree/main/scripts/dts) library, part of Zephyr, to parse DTS and binding files into Devicetree models. +> +> Although this API should eventually become a *standalone source code library*, it's not currently a priority: +> - it's not tagged independently of the main Zephyr project +> - the [PyPI](https://pypi.org/project/devicetree/) package is no longer updated +> +> When distributed independently of Zephyr, DTSh has therefore to re-package snapshots ([src/devicetree](src/devicetree)) of this library (see [ci: bundle devicetree Python package ](https://github.com/dottspina/dtsh/commit/5e803eb) for details). +> +> As a consequence, it's very likely that you'll generate the DTS files (e.g. `west build`) and open them (e.g. `dtsh build/zephyr/zephyr.dts`) with different versions of the library: although compatibility is mostly determined by the bindings, this might prove confusing in certain circumstances. + + +### Install + +DTSh can be installed in the same Python virtual environment as the West workspace you use for Zephyr development, or stand-alone in any Python environment. + +#### Stand-alone + +This method installs DTSh in a dedicated Python virtual environment: it's a bit less convenient, but recommended if you prefer to test DTSh without installing anything in a development environment you actually depend on. + +For example (Linux and macOS): + +``` sh +# Initialize Python virtual environment. +mkdir dtsh +cd dtsh +python -m venv .venv + +# Activate and update system tools. +. .venv/bin/activate +pip install --upgrade pip setuptools + +# Install DTSh from PyPI. +pip install dtsh +``` + +To uninstall, just remove your test directory, e.g. `rm -r dtsh`. + + +#### West Workspace + +This method installs DTSh in the same Python virtual environment as a Zephyr workspace. + +Assuming you've followed Zephyr [Getting Started Guide](https://docs.zephyrproject.org/latest/develop/getting_started/index.html), the workspace should look like this: + +``` +zephyrproject/ +├── .venv +├── .west +├── bootloader +├── modules +├── tools +└── zephyr +``` + +Then: + +``` sh +# Active the Python virtual environment if not already done. +zephyrproject/.venv/bin/activate + +# Install DTSh from PyPI. +pip install dtsh +``` + +Uninstall as usual: `pip uninstall dtsh`. + +**Note**: Installing DTSh in a Zephyr workspace does not make it available as a West command. + + +### Run + +Once installed, DTSh should be available as `dtsh`: + +``` +$ dtsh -h +usage: dtsh [-h] [-b DIR] [-u] [--preferences FILE] [--theme FILE] [DTS] + +shell-like interface with Devicetree + +options: + -h, --help show this help message and exit + +open a DTS file: + -b DIR, --bindings DIR + directory to search for binding files + DTS path to the DTS file + +user files: + -u, --user-files initialize per-user configuration files and exit + --preferences FILE load additional preferences file + --theme FILE load additional styles file +``` + +To open a DTS file, simply pass its path as the command argument: + +``` +$ dtsh build/zephyr/zephyr.dts +dtsh (0.2rc1): Shell-like interface with Devicetree +How to exit: q, or quit, or exit, or press Ctrl-D + +/ +❭ cd /soc/flash-controller@4001e000 + +/soc/flash-controller@4001e000 +❭ tree -l + Description + ───────────────────────────────────────────────────────────────── +flash-controller@4001e000 Nordic NVMC (Non-Volatile Memory Controller) +└── flash@0 Flash node + └── partitions This binding is used to describe fixed partitions of a flash (or… + ├── partition@0 Each child node of the fixed-partitions node represents… + ├── partition@c000 Each child node of the fixed-partitions node represents… + ├── partition@82000 Each child node of the fixed-partitions node represents… + └── partition@f8000 Each child node of the fixed-partitions node represents… +``` + +A DTS file alone is actually an incomplete Devicetree source: interpreting its contents requires finding the defining bindings. + +By default, DTSh will fist try to retrieve the bindings Zephyr has used at build-time, when the DTS file was generated. For this, it will rely on the CMake cache file contents, assuming a typical build layout: + +``` +build/ +├── CMakeCache.txt +└── zephyr/ + └── zephyr.dts +``` + +When no suitable CMake cache is available, DTSh will instead try to work out the search path Zephyr would use if it were to generate the DTS *now* (see [Where bindings are located](https://docs.zephyrproject.org/latest/build/dts/bindings-intro.html#where-bindings-are-located)): a valid `ZEPHYR_BASE` is then required. + +If the command line does not specify a DTS file path, `dtsh` will try to open the devicetree at `build/zephy/zephyr.dts`. When DTSh is installed in a Zephyr workspace, opening the devicetree of a project you're working on is then as simple as: + +``` +$ west build +$ dtsh +``` diff --git a/README.org b/README.org deleted file mode 100644 index bcaba2d..0000000 --- a/README.org +++ /dev/null @@ -1,836 +0,0 @@ -#+title: dtsh - -*dtsh* is an interactive /shell-like/ interface with a devicetree and its bindings: - -- browse the devicetree through a familiar hierarchical file-system metaphor -- retrieve nodes and bindings with accustomed command names and command line syntax -- generate simple documentation artifacts by redirecting commands output to files (text, HTML, SVG) -- common command line interface paradigms (auto-completion, history) and keybindings - -It's said an ~ls /soc -l > soc.svg~ speaks a thousand words: - -[[./doc/img/soc.svg]] - -#+begin_quote -DISCLAIMER: This software was created as a Proof of Concept for a simple tool -that could assist newcomers to Zephyr in understanding what a devicetree is, -and how bindings describe and constrain its content. - -It's still in its inception phase: - -- while the current feature set may already prove helpful to beginners, - it might also quickly frustrate more knowledgeable users -- possible bugs could instead disastrously confuse beginners -- should install and run more or less Out Of The Box™ on most GNU/Linux distributions - and BSD-like systems (e.g. macOS); will likely refuse to run on Windows due to missing readline support -- should be compatible with any source file in DTS format, but requires that the bindings are consistently available as YAML files: - unfortunately, this does NOT directly apply to the devicetree use by the Linux kernel - -All kinds of feedback and contribution are encouraged: please refer to the bottom CONTRIBUTE section. - -*See also the project's status*. -#+end_quote - -- [[https://github.com/dottspina/dtsh#status][Status]] -- [[https://github.com/dottspina/dtsh#get-started][Get started]] - - [[https://github.com/dottspina/dtsh#requirements][Requirements]] - - [[https://github.com/dottspina/dtsh#install][Install]] - - [[https://github.com/dottspina/dtsh#run][Run]] - - [[https://github.com/dottspina/dtsh#zephyr-integration][Zephyr integration]] -- [[https://github.com/dottspina/dtsh#users-guide][User's guide]] - - [[https://github.com/dottspina/dtsh#the-shell][The shell]] - - [[https://github.com/dottspina/dtsh#file-system-metaphot][Fle system metaphor]] - - [[https://github.com/dottspina/dtsh#the-command-string][The command string]] - - [[https://github.com/dottspina/dtsh#output-redirection][Output redirection]] - - [[https://github.com/dottspina/dtsh#built-ins][Built-ins]] - - [[https://github.com/dottspina/dtsh#manual-pages][Manual pages]] - - [[https://github.com/dottspina/dtsh#system-information][System information]] - - [[https://github.com/dottspina/dtsh#find-nodes][Find nodes]] - - [[https://github.com/dottspina/dtsh#format-strings][Format strings]] - - [[https://github.com/dottspina/dtsh#user-interface][User interface]] - - [[https://github.com/dottspina/dtsh#the-prompt][The prompt]] - - [[https://github.com/dottspina/dtsh#commands-history][Commands history]] - - [[https://github.com/dottspina/dtsh#auto-completion][Auto-completion]] - - [[https://github.com/dottspina/dtsh#the-pager][The pager]] - - [[https://github.com/dottspina/dtsh#external-links][External links]] - - [[https://github.com/dottspina/dtsh#keybindings][Keybindings]] - - [[https://github.com/dottspina/dtsh#theme][Theme]] - - [[https://github.com/dottspina/dtsh#how-to][How To]] - - [[https://github.com/dottspina/dtsh#soc-overview][SoC overview]] - - [[https://github.com/dottspina/dtsh#board-definition][Board definition]] - - [[https://github.com/dottspina/dtsh#compatibles-overview][Compatibles overview]] - - [[https://github.com/dottspina/dtsh#bus-devices-overview][Bus devices overview]] - - [[https://github.com/dottspina/dtsh#interrupts-overview][Interrupts overview]] - - [[https://github.com/dottspina/dtsh#commands-cheat-sheet][Commands Cheat Sheet]] -- [[https://github.com/dottspina/dtsh#contribute][Contribute]] -- [[https://github.com/dottspina/dtsh#references][References]] - -* Status - -Latest stable release: [[https://github.com/dottspina/dtsh/releases/tag/v0.1.0b2][v0.1.0b2]] ([[https://pypi.org/project/dtsh/0.1.0b2/][PyPI]]) - -*Latest Changes*: - -- [[https://github.com/dottspina/dtsh/commit/96ffa28][96ffa28]] update python-devicetree to Zephyr 3.5.0 -- [[https://github.com/dottspina/dtsh/commit/55b2b9e0faf537997bd7104cadb17d29708e4e5f][55b2b9e]] update python-devicetree to Zephyr 3.4.0 - -*Note*: - -- this prototype is now /stale/: I may update it e.g. when Zephyr 3.6 is out, or to fix obvious bugs if asked to, but issues that would require more work (e.g. fixing the poorly wrapped output, or adding support for DTS labels in paths) won't be addressed -- for some time now, all real work has moved to an upstream [[https://github.com/zephyrproject-rtos/zephyr/pull/59863][RFC]] (PR's [[https://github.com/dottspina/zephyr/tree/rfc-dtsh][origin]]) that may eventually add ~dtsh~ as a West extension to Zephyr: report feedback about this version preferably (see [[https://github.com/dottspina/dtsh#try-the-west-command-rfc][Try the West command RFC]]) - -* Get started - -#+begin_example -# Install dtsh in a dedicated Python virtual environment -$ python -m venv .venv -$ . .venv/bin/activate -$ pip install --upgrade pip setuptools -$ pip install --upgrade dtsh - -# Setting ZEPHYR_BASE will help dtsh in building a default bindings search path -export ZEPHYR_BASE=/path/to/zephyrproject/zephyr - -# Open an existing DTS file, using Zephyr bindings -$ dtsh /path/to/build/zephyr/zephyr.dts -dtsh (0.1.0a6): Shell-like interface to a devicetree -Help: man dtsh -How to exit: q, or quit, or exit, or press Ctrl-D - -/ -❯ tree -L 1 -l -/ -├── chosen -├── aliases -├── soc -├── pin-controller The nRF pin controller is a singleton node responsible for controlling… -├── entropy_bt_hci Bluetooth module that uses Zephyr's Bluetooth Host Controller Interface as… -├── cpus -├── sw-pwm nRFx S/W PWM -├── leds This allows you to define a group of LEDs. Each LED in the group is… -├── pwmleds PWM LEDs parent node -├── buttons GPIO KEYS parent node -├── connector GPIO pins exposed on Arduino Uno (R3) headers… -└── analog-connector ADC channels exposed on Arduino Uno (R3) headers… -#+end_example - -To get the /big picture/: - -- may be this [[https://youtu.be/pc2AMx1iPPE][short abrupt video]], that at least illustrates the main /shell metaphor/, the auto-completion behavior - and the most useful keybindings -- if already comfortable with Zephyr, try running the [[https://github.com/dottspina/dtsh#interactive-tests][interactive tests]] to explore ~dtsh~ with various configurations - and DTS source files - -** Requirements - -*** Host tools and libraries - -**** POSIX - -This is an abusive keyword for facilities most POSIX-like operating systems provide one way or another: - -- the [[https://tiswww.cwru.edu/php/chet/readline/rltop.html][GNU readline]] library we rely upon for command line auto-completion, commands history, - and standardized keybindings -- an ANSI ([[https://www.ecma-international.org/publications-and-standards/standards/ecma-48/][ECMA-48]]) terminal emulator, preferably 256 colors support and a font that includes unicode glyphs - for a few common symbols -- a /pager/, preferably with ANSI escape codes support, e.g. [[https://www.greenwoodsoftware.com/less/faq.html][less]] - -Note: on BSD-like systems, where the Python ~readline~ package will likely depend on [[https://www.thrysoee.dk/editline/][editline]], -~dtsh~ will try to install ~gnureadline~ ([[https://pypi.org/project/gnureadline/][PyPI]]) and to load the ~readline~ package implementation -from there instead of from the standard Python library. - -**** CMake - -~dtsh~ may need to access a few CMake cached variables for setting sensible default values, -e.g. when building the default bindings search path. - -*** Python requirements - -The minimal requirement is set to Python 3.8, with proper support for virtual environments, [[https://pip.pypa.io/en/stable/][pip]], and [[https://setuptools.pypa.io/en/latest/setuptools.html][setuptools]]. - -Most ~dtsh~ software requirements are common Python libraries that will be installed as declared dependencies (e.g. by ~setup.py~): - -- « rich text and beautiful formatting in the terminal »: [[https://www.textualize.io/][Textualize]] /rich/ API ([[https://github.com/Textualize/rich][GitHub]], [[https://pypi.org/project/rich/][PyPI]]) -- syntax highlighting support: [[https://pygments.org/][Pygments]] ([[https://pypi.org/project/Pygments/][PyPI]]) -- [[https://pyyaml.org/][PyYAML]] ([[https://pypi.org/project/PyYAML][PyPI]]) -- DT sources and bindings /parser/, devicetree model: ~edtlib~, maintained as part of the Zephyr project ([[https://github.com/zephyrproject-rtos/python-devicetree][GitHub]], [[https://pypi.org/project/devicetree/][PyPI]]); - see bellow - -**** edtlib - -The EDT library is no longer installed from PyPI, but bundled with ~dtsh~ (see [[https://github.com/dottspina/dtsh/commit/5e803ebdd3482db75dc752baa3cca6866750eff5][5e803eb]]). - -** Install - -It's recommended to install ~dtsh~ in a dedicated Python virtual environment. - -*** Python virtual environment - -A Python /best practice/ is to always install a consistent set of /scripts/ and their dependencies in a dedicated -[[https://peps.python.org/pep-0405/][virtual environment]], with up-to-date ~pip~, ~setuptools~ and ~wheel~ packages. - -See also [[https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/][Installing packages using pip and virtual environments]]. - -*** Install from sources - -Install from sources in a dedicated Python virtual environment: - -#+begin_src sh -git clone https://github.com/dottspina/dtsh.git -cd dtsh -python -m venv .venv -. .venv/bin/activate -pip install --upgrade pip setuptools -pip install . -#+end_src - -*** Install from PyPI - -Install from [[https://pypi.org/project/dtsh/][PyPI]] in a dedicated Python virtual environment: - -#+begin_src sh -python -m venv .venv -. .venv/bin/activate -pip install --upgrade pip setuptools -pip install --upgrade dtsh -#+end_src - -*** Uninstall - -To remove ~dtsh~ and all its direct dependencies from a dedicated virtual environment: - -#+begin_src sh -cd dtsh -. .venv/bin/activate -pip uninstall dtsh rich Pygments -#+end_src - -** Run - -To start a shell session: ~dtsh [] [*]~ - -where: - -- ~~: path to the devicetree source file in [[https://devicetree-specification.readthedocs.io/en/latest/chapter6-source-language.html][DTS Format]] (~.dts~); - if unspecified, defaults to ~$PWD/build/zephyr/zephyr.dts~ -- ~~: directory to search for [[https://yaml.org/][YAML]] binding files; - if unspecified, but the environment variable ~ZEPHYR_BASE~ is set, - defaults to the [[https://github.com/dottspina/dtsh#zephyr-bindings-search-path][Zephyr bindings search path]] bellow - -To open an arbitrary DTS file with custom bindings: - -#+begin_example -$ dtsh /path/to/foobar.dts /path/to/custom/bindings /path/to/other/custom/bindings -#+end_example - -To open the same DTS file, with /default/ bindings: - -#+begin_example -$ export ZEPHYR_BASE=/path/to/zephyr -$ dtsh /path/to/foobar.dts -#+end_example - -On startup, ~dtsh~ will output a banner, followed by the first prompt: - -#+begin_example -dtsh (0.1.0a5): Shell-like interface to a devicetree -Help: man dtsh -How to exit: q, or quit, or exit, or press Ctrl-D - -/ -❯ -#+end_example - -*** Zephyr bindings search path - -When no bindings are explicitly provided, ~dtsh~ will try to reassemble the /bindings search path/ Zephyr would rely on at build time (see [[https://docs.zephyrproject.org/latest/build/dts/bindings.html#where-bindings-are-located][Where bindings are located]]): - -- the zephyr repository: ~$ZEPHYR_BASE/dts/bindings~ -- the application source directory: ~APPLICATION_SOURCE_DIR/dts/bindings~; if ~dtsh~ fails to access the CMake - variable ~APPLICATION_SOURCE_DIR~, will fallback to ~$PWD/dts/bindings~ (assuming the current directory is - the /project/ directory) -- the board directory: ~BOARD_DIR/dts/bindings~; if ~dtsh~ fails to access the CMake variable ~BOARD_DIR~, will - fallback to ~$ZEPHYR_BASE/boards~ (to include /all/ Zephyr defined boards) plus ~$PWD/boards~ (to include a possible - custom boards directory) -- any directories in ~DTS_ROOT~: all ~DTS_ROOT/**/dts/bindings~ directories ~dtsh~ will find if the CMake variable - ~DTS_ROOT~ is available -- any module that defines a ~dts_root~ in its build: ~dtsh~ does NOT honor this part of the search path, - and likely will not until a test case is submitted for investigation - -Only the ~ZEPHYR_BASE~ environment variable is required, and will typically suffice to setup an -appropriate bindings search path. - -See also issue [[https://github.com/dottspina/dtsh/issues/1#issuecomment-1278281428][Incomplete Zephyr bindings #1]]. - -** Zephyr integration - -Installing ~dtsh~ into the same Python virtual environment as the ~west~ development environment -is now strongly discouraged (and should never have been advised to). - -Recommended use: - -- install Zephyr and generate DTS files with ~west build~ as usual -- install ~dtsh~ into its own Python virtual environment, - optionally set ~ZEPHYR_BASE~ to get most DT bindings for free, - and open DTS files from there - -* User's guide - -The preferred entry point to the ~dtsh~ documentation should be its manual pages: - -- ~man dtsh~: open the shell manual page (mostly similar to this user guide) -- ~man ~: open the manual page for the command ~~ - -** The shell - -~dtsh~ defines a set of /built-in/ commands that interface with a devicetree and its bindings through a hierarchical file-system metaphor. - -Loading of /external commands/ is not (yet) supported. - -*** File system metaphor - -Within a ~dtsh~ session, a devicetree shows itself as a familiar hierarchical file-system, -where [[https://devicetree-specification.readthedocs.io/en/stable/devicetree-basics.html#path-names][path names]] /look like/ paths to files or directories, depending on the acting shell command. - -A current /working node/ is defined, similar to any shell's current working directory, -allowing ~dtsh~ to also support relative paths. - -A leading ~.~ represents the current working node, and ~..~ its parent. -The devicetree root node is its own parent. - -To designate properties, ~dtsh~ uses ~$~ as a separator between DT path names and [[https://devicetree-specification.readthedocs.io/en/stable/devicetree-basics.html#property-names][property names]] -(should be safe since ~$~ is an invalid character for both node and property names). - -Some commands support filtering or /globbing/ with trailing wild-cards ~*~. - -*** The command string - -The ~dtsh~ command string is based on the [[https://www.gnu.org/software/libc/manual/html_node/Using-Getopt.html][GNU getopt]] syntax. - -**** Synopsis - -All built-ins share the same synopsis: - -#+begin_example -CMD [OPTIONS] [PARAMS] -#+end_example - -where: - -- ~CMD~: the built-in name, e.g. ~ls~ -- ~OPTIONS~: the options the command is invoked with, e.g. ~-l~ -- ~PARAMS~: the parameters the command is invoked for, e.g. a path name - -~OPTIONS~ and ~PARAMS~ are not positional: ~ls -l /soc~ is equivalent to ~ls /soc -l~. - -**** Options - -An option may support: - -- a short name, starting with a single ~-~ (e.g. ~-h~) -- a long name, starting with ~--~ (e.g. ~--help~) - -Short option names can combine: ~-lR~ is equivalent to ~-l -R~. - -An Option may also require an argument, e.g. ~find /soc --interrupt 12~. - -Options semantic should be consistent across commands, e.g. ~-l~ always means /long format/. - -We also try to re-use /well-known/ option names, e.g. ~-r~ for /reverse sort/ or ~-R~ for /recursive/. - -ℹ Trigger ~TAB~ completion after a single ~-~ to /pull/ a summary of a command's options, e.g: - -#+begin_example -❯ find -[TAB][TAB] --c print nodes count --q quiet, only print nodes count --l use rich listing format --f visible columns format string --h --help print usage summary ---name find by name ---compat find by compatible ---bus find by bus device ---interrupt find by interrupt ---enabled-only search only enabled nodes ---pager page command output -❯ find - -#+end_example - -*** Output redirection - -Command output redirection uses the well-known syntax: - -#+begin_example -CMD [OPTIONS] [PARAMS] > PATH -#+end_example - -where ~PATH~ is the absolute or relative path to the file the command output will be redirected to. - -Depending on the extension, the command output may be saved as an HTML page (~.html~), an SVG image (~.svg~), -or a text file (default). - -For example: - -#+begin_example -/ -❯ ls -l soc > soc.html - -#+end_example - -*** Built-ins - -| Built-in | | -|----------+-------------------------------------------| -| ~alias~ | print defined aliases | -| ~chosen~ | print chosen configuration | -| ~pwd~ | print current working node's path | -| ~cd~ | change current working node | -| ~ls~ | list devicetree nodes | -| ~tree~ | list devicetree nodes in tree-like format | -| ~cat~ | concatenate and print devicetree content | -| ~find~ | find devicetree nodes | -| ~uname~ | print system information | -| ~man~ | open a manual page | - -*** Manual pages - -As expected, the ~man~ command will open the manual page for the shell itself (~man dtsh~), -or one of its built-ins (e.g. ~man ls~). - -Additionally, ~man~ can also open a manual page for a [[https://devicetree-specification.readthedocs.io/en/latest/chapter2-devicetree-basics.html#compatible][compatible]], which is essentially a view of its (YAML) bindings: e.g. ~man --compat nordic,nrf-radio~ - -~man~ should eventually also serve as an entry point to external useful or normative documents, -e.g. the Devicetree Specifications or the Zephyr project's documentation. - -*** System information - -*dtsh* may also expose /system/ information, including: - -- the Zephyr kernel version, e.g. ~zephyr-3.1.0~, with a link to the corresponding - release notes when available -- board information, based on the content of its YAML binding file, - with a link to the corresponding documentation when the board - is [[https://docs.zephyrproject.org/latest/boards/index.html][supported by Zephyr]] -- the configured /toolchain/, either Zephyr SDK or GNU Arm Embedded - -Retrieving this information may involve environment variables (e.g. ~ZEPHYR_BASE~), -CMake cached variables (e.g. ~BOARD_DIR~), and ~git~ or ~GCC~. - -Refer to ~man uname~ for details. - -*** Find nodes - -The ~find~ command permits to search the devicetree by: - -- node names -- compatible strings -- bus devices -- interrupt names or numbers - -For example, the command line bellow would list all enabled bus devices that generate IRQs : - -#+begin_example -❯ find --enabled-only --bus * --interrupt * -#+end_example - -~find~ is quite versatile and supports a handful of options. Refer to its extensive manual page (~man find~). - -*** Format strings - -When the ~-l~ flag is set (aka /use long listing format/), the ~ls~ and ~find~ commands accept -an additional ~-f~ option, the /format string/, that permits to choose the visible node fields. - -A format string is a list of specifier characters, each selecting a node field. - - | Specifier | Format | - |-----------+-------------------------------------------| - | ~N~ | The node name | - | ~a~ | The unit-address | - | ~n~ | The node name with the address striped | - | ~d~ | The description from the node binding | - | ~p~ | The node path name | - | ~l~ | The node 'label' property | - | ~L~ | All known labels for the node | - | ~s~ | The node 'status' property | - | ~c~ | The 'compatible' property for the node | - | ~C~ | The node binding (aka matched compatible) | - | ~A~ | The node aliases | - | ~b~ | The bus device information for the node | - | ~r~ | The node 'reg' property | - | ~i~ | The interrupts generated by the node | - -For example, the format string ~naib~ will refer to the node's name, address, -IRQs and bus information columns. - -** User interface - -The ~dtsh~ command line interface paradigms and keybindings should sound familiar. - -*** The prompt - -The default shell prompt is ❯. -The line immediately above the prompt shows the current working node's path. - -#+begin_example -/ -❯ pwd -/ - -/ -❯ cd /soc/i2c@40003000/bme680@76 - -/soc/i2c@40003000/bme680@76 -❯ pwd -/soc/i2c@40003000/bme680@76 - -#+end_example - -Pressing ~C-d~ (aka ~CTRL-D~) at the prompt will exit the ~dtsh~ session. - -*** Commands history - -Commands history is provided through GNU readline integration. - -At the shell prompt, press: - -- up arrow (↑) to navigate the commands history backward -- down arrow (↓) to navigate the commands history forward -- ~C-r~ (aka ~CTRL-R~) to /reverse search/ the commands history - -The history file (typically ~$HOME/.config/dtsh/history~) is saved on exit, and loaded on startup. - -*** Auto-completion - -Command line auto-completion is provided through GNU readline integration. - -Auto-completion is triggered by first pressing the ~TAB~ key twice, -then once for subsequent completions of the same command line, and may apply to: - -- command names (aka built-ins) -- command options -- command parameters such as node paths or compatibles - -*** The pager - -Built-ins that may produce large outputs support the ~--pager~ option: the command's output is then -/paged/ using the system pager, typically ~less~: - -- use up (↑) and down (↓) arrows to navigate line by line -- use page up (⇑) and down (⇓) to navigate /window/ by /window/ -- press ~g~ go to first line -- press ~G~ go to last line -- press ~/~ to enter search mode -- press ~h~ for help -- press ~q~ to quit the pager and return to the ~dtsh~ prompt - -On the contrary, the ~man~ command uses the pager by default and defines a ~--no-pager~ option to disable it. - -*** External links - -~dtsh~ commands output may contain links to external documents such as: - -- the local YAML binding files, that should open in the system's default text editor -- the Devicetree specifications or the Zephyr project's documentation, - that should open in the system's default web browser - -How these links will appear in the console, and whether they are /actionable/ or not, -eventually depend on the terminal and the desktop environment. - -⚠ In particular, the environment may assume DTS files are DTS audio streams -(e.g. the VLC media player could have registered itself for handling the ~.dts~ file extension). -In this case, the external link won't open in the default text editor, -possibly without any error message. -A work-around is to configure the desktop environment to open DTS files with -a text editor (e.g. with the /Open with/ paradigm). - -*** Keybindings - -Familiar keybindings are provided through GNU readline integration. - -| Keyboard shortcut | | -|-------------------+----------------------------------------------| -| ~C-l~ | clear terminal screen | -| ~C-a~ | move cursor to beginning of command line | -| ~C-e~ | move cursor to end of command line | -| ~C-k~ | /kill/ text from cursor to end of command line | -| ~M-d~ | /kill/ word at cursor | -| ~C-y~ | /yank/ (paste) the content of the /kill buffer/ | -| ~C-←~ | move cursor one word backward | -| ~C-→~ | move cursor one word forward | -| ~↑~ | navigate the commands history backward | -| ~↓~ | navigate the commands history forward | -| ~C-r~ | search the commands history | -| ~TAB~ | trigger auto-completion | - -where: - -- e.g. ~C-c~ means hold the ~CTRL~ key, then press ~C~ -- e.g. ~M-d~ means hold the ~Alt~ (/meta/) key, then press ~D~ - -*** Theme - -Colors and such are subjective, and most importantly the rendering will -eventually depend on the terminal's font and palette, -possibly resulting in severe accessibility issues, e.g. grey text on white background -or a weird shell prompt. - -In such situations, or to accommodate personal preferences, users can try to override -~dtsh~ colors (and prompt) by creating a /theme/ file (typically ~$HOME/.config/dtsh/theme~). - -Use the [[https://github.com/dottspina/dtsh/blob/main/src/dtsh/theme][default theme]] as template: - -#+begin_src sh -cp src/dtsh/theme ~/.config/dtsh/theme -#+end_src - -** How To -*** SoC overview - -Try ~ls -lR --pager /soc~ - -*** Board definition - -Try ~uname -ml~ - -*** Compatibles overview - -Try ~find / --compat * -l~ to list all nodes that have a ~compatible~ DT property. - -ℹ See also the ~TAB~ completion for the ~man --compat~ command. - -*** Bus devices overview - -Try ~find / --bus * -f pibcd~ - -Use the ~--enabled-only~ flag to filter out disabled bus devices. - -*** Interrupts overview - -Try ~find / --interrupt * -f picd~ - -Use the ~--enabled-only~ flag to filter out disabled IRQs. - -*** Commands Cheat Sheet - -To list all commands and their short descriptions (press ~TAB~ twice at the prompt): - -#+begin_example -/ -❯[TAB][TAB] -pwd print current working node's path -alias print defined aliases -chosen print chosen configuration -cd change current working node -ls list devicetree nodes -tree list devicetree nodes in tree-like format -cat concatenate and print devicetree content -uname print system information -find find devicetree nodes -man open a manual page -#+end_example - -Command options list: - -#+begin_example -/ -❯ ls -h -ls [-d] [-l] [-r] [-R] [--pager] [-h --help] [PATH] -#+end_example - -Command options summary (press ~TAB~ twice after the ~-~ character that starts -option names): - -#+begin_example -/ -❯ ls -[TAB][TAB] --d list node itself, not its content --l use rich listing format --r reverse order while sorting --R list node contents recursively --h --help print usage summary ---pager page command output -#+end_example - -Command manual page: ~man ls~ - -* Contribute - -** Open issues - -All kinds of feedback and contribution are encouraged: open an [[https://github.com/dottspina/dtsh/issues/new][issue]] or a [[https://github.com/dottspina/dtsh/pulls][pull request]] with the appropriate [[https://github.com/dottspina/dtsh/issues/labels][label]] -(if unsure, just ignore labels). - -| Label | | -|-------+--------------------------------------------------------| -| ~RFC~ | Participate in Request For Comments | -| ~bug~ | The software does not behave as expected or documented | - -*** Request For Comments - -This project is still exploring /what could be/: - -- an educational tool that would assist students and professors when introducing /devicetrees/ -- an handy debug or discovery tool that would at a glance show how a /board/ is configured, - which buses and devices are supported and if they are enabled, the memory layout for mapped peripherals and suchlike - -To provide feedback regarding theses topics, please open issues with the ~RFC~ label. - -*** Report bugs - -Bugs are expected, please open issues with the ~bug~ label. - -*** Getting Help - -Feel free to also open issues: - -- when the documentation is lacking, confusing or incorrect -- to request any kind of help or support - -** Hacking dtsh - -Hack into ~dtsh~ and contribute [[https://github.com/dottspina/dtsh/pulls][pull requests]] (bug fix, features, documentation, code review). - -*** Development mode installation - -Install ~dtsh~ in development mode: - -#+begin_src sh -git clone https://github.com/dottspina/dtsh.git -cd dtsh -python -m venv .venv -. .venv/bin/activate -pip install --upgrade pip setuptools -pip install -r requirements-dev.txt -pip install --editable . -#+end_src - -The ~--editable~ option asks ~pip~ to install ~dtsh~ as an editable /working copy/. - -*** Unit tests - -To run a few unit tests: - -#+begin_src sh -cd dtsh -. .venv/bin/activate -python -m pytest tests -#+end_src - -*** Interactive tests - -The [[https://github.com/dottspina/dtsh/tree/main/etc/sh][etc/sh]] folder contains a few helper scrips that, while not originally written -with a public use in mind, may prove helpful in hacking through ~dtsh~. - -In particular ~interactive-tests.sh~, that will sequentially run ~dtsh~ -for various boards and configurations: - -#+begin_example -==== UC7: DTS from Zephyr build, Zephyr bindings - Bindings search path: $ZEPHYR_BASE/dts/bindings - Toolchain (dtsh): Zephyr SDK - Application: coap_client - Board: mimxrt1170_evk_cm7 -Run test [yN]: -#+end_example - -The synopsis is: - -#+begin_example -etc/sh/interactive-tests.sh [ZEPHYR_BASE TOOLCHAIN_BASE] -#+end_example - -Where: - -- ~ZEPHYR_BASE~ would be a valid value for the environment variable ~ZEPHYR_BASE~ (sic) -- ~TOOLCHAIN_BASE~ would be a valid value for ~ZEPHYR_SDK_INSTALL_DIR~ or - ~$GNUARMEMB_TOOLCHAIN_PATH~ (the script /should/ auto-detect the toolchain variant - and set ~ZEPHYR_TOOLCHAIN_VARIANT~ accordingly) - -When started without parameters, ~interactive-tests.sh~ will default to hard-coded values -that match the test platform file-system, and won't make sense anywhere else. -They are easy to change, though. - -WARNING: - -- tests ~UC3~ to ~UC9~ will install (uninstall) ~dtsh~ into (from) the Python environment of - the West workspace parent of ~ZEPHYR_BASE~ -- tests ~UC8~ and ~UC9~ are expected to fail if GCC Arm 10 and 11 are not installed at the - locations determined by the above hard-coded values - -*** Notes - -While probably not so /pythonesque/, the source code should eventually seem obvious, -and friendly to hacking and prototyping. - -For example, to define a new built-in: - -- look for the ~DtshCommand~ and ~DtshCommandOption~ classes ([[https://github.com/dottspina/dtsh/blob/main/src/dtsh/dtsh.py][dtsh.dtsh]] module) to get the basics -- copy an existing command (e.g. [[https://github.com/dottspina/dtsh/blob/main/src/dtsh/builtin_ls.py][ls]]) as a template, and customize it -- re-use or improve helpers and views in the [[https://github.com/dottspina/dtsh/blob/main/src/dtsh/tui.py][dtsh.tui]] module to assemble the command output - (see also the /rich/ [[https://rich.readthedocs.io/en/stable/console.html][Console API]]) -- when ready, register it in the ~dtsh.shell.DevicetreeShell~ constructor - -** Try the West command RFC - -To test the current status of ~west dtsh~, initialize a West workspace with the Zephyr fork that hosts the RFC/PR: - -#+begin_src shell - -# Initialize virtual environment as usual. -mkdir tmp-west-dtsh -cd tmp-west-dtsh -python -m venv .venv -. .venv/bin/activate -pip install -U pip setuptools - -# Install West as usual. -pip install -U west - -# Initialize a West workspace with the Zephyr fork that hosts the RFC/PR. -west init -m https://github.com/dottspina/zephyr -cd zephyr -git fetch --all -git checkout rfc-dtsh -west update -pip install -U -r scripts/requirements.txt - -# Configure workspace as usual, e.g.: -west config --local build.pristine auto -west config --local build.cmake-args -- -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -west config --local build.generator "Unix Makefiles" -west config --local build.board nrf52840dk_nrf52840 - -# Setup build environment. -. ./zephyr-env.sh -export ZEPHYR_TOOLCHAIN_VARIANT=zephyr -export ZEPHYR_SDK_INSTALL_DIR=/mnt/platform/devemb/zephyr-rtos/sdk/zephyr-sdk-0.16.1 - -# Build some sample and open the generated DTS with the devicetree shell. -cd samples/sensor/bme680 -west build -west dtsh -#+end_src - -* References - -More or less introductory references about /devicetrees/. - -** Devicetree Specifications - -- [[https://devicetree-specification.readthedocs.io/en/latest/][Online Devicetree Specifications]] (latest) -- [[https://devicetree-specification.readthedocs.io/en/stable/][Online Devicetree Specifications]] (stable) - -** Zephyr - -- [[https://docs.zephyrproject.org/latest/build/dts/intro.html][Introduction to devicetree]] -- [[https://docs.zephyrproject.org/latest/build/dts/bindings.html][Devicetree bindings]] -- [[https://docs.zephyrproject.org/latest/build/dts/api/bindings.html][Bindings index]] -- [[https://docs.zephyrproject.org/latest/build/dts/api/api.html#zephyr-specific-chosen-nodes][Zephyr-specific chosen nodes]] -- [[https://docs.zephyrproject.org/latest/build/dts/dt-vs-kconfig.html][Devicetree versus Kconfig]] - -** Linux - -- [[https://docs.kernel.org/devicetree/index.html][Open Firmware and Devicetree]] -- [[https://elinux.org/Device_Tree_Usage][Device Tree Usage]] -- [[https://elinux.org/Device_Tree_Reference][Device Tree Reference]] -- [[https://elinux.org/Device_Tree_What_It_Is][Device Tree What It Is]] diff --git a/README.rst b/README.rst index daf7a80..ee263ef 100644 --- a/README.rst +++ b/README.rst @@ -1,47 +1,35 @@ ==== -dtsh +DTSh ==== -:Author: Chris Duf +:Author: Christophe Dufaza -**dtsh** is an interactive *shell-like* interface with a devicetree and -its bindings: +Shell-like command line interface with Devicetree: -- browse the devicetree through a familiar hierarchical file-system - metaphor -- retrieve nodes and bindings with accustomed command names and command - line syntax -- generate simple documentation artifacts by redirecting commands - output to files (text, HTML, SVG) -- common command line interface paradigms (auto-completion, history) - and keybindings +- browse a devicetree through a hierarchical file system metaphor +- search for devices, bindings, buses or interrupts with flexible criteria +- filter, sort and format commands output +- generate simple documentation artifacts (text, HTML, SVG) by redirecting the output + of commands to files +- *rich* Textual User Interface, command line auto-completion, command history, user themes :: - $ dtsh build/gzephyr/zephyr.dts - dtsh (0.1.0a4): Shell-like interface to a devicetree - Help: man dtsh + $ dtsh build/zephyr/zephyr.dts + dtsh (0.2rc1): Shell-like interface with Devicetree How to exit: q, or quit, or exit, or press Ctrl-D / - > tree -L 1 -l - / - ├── chosen - ├── aliases - ├── soc - ├── pin-controller The nRF pin controller is a singleton node responsible for controlling… - ├── entropy_bt_hci Bluetooth module that uses Zephyr's Bluetooth Host Controller Interface as… - ├── cpus - ├── sw-pwm nRFx S/W PWM - ├── leds This allows you to define a group of LEDs. Each LED in the group is… - ├── pwmleds PWM LEDs parent node - ├── buttons GPIO KEYS parent node - ├── connector GPIO pins exposed on Arduino Uno (R3) headers… - └── analog-connector ADC channels exposed on Arduino Uno (R3) headers… - - -This software was created as a Proof of Concept for a: - -- simple tool that could assist newcomers to Zephyr in understanding - what a devicetree is, and how bindings describe and constrain its content -- an on hand DTS file viewer + > cd /soc/flash-controller@4001e000 + + /soc/flash-controller@4001e000 + > tree -l + Description + ───────────────────────────────────────────────────────────────── + flash-controller@4001e000 Nordic NVMC (Non-Volatile Memory Controller) + └── flash@0 Flash node + └── partitions This binding is used to describe fixed partitions of a flash (or… + ├── partition@0 Each child node of the fixed-partitions node represents… + ├── partition@c000 Each child node of the fixed-partitions node represents… + ├── partition@82000 Each child node of the fixed-partitions node represents… + └── partition@f8000 Each child node of the fixed-partitions node represents… diff --git a/doc/img/buses.png b/doc/img/buses.png new file mode 100644 index 0000000000000000000000000000000000000000..0b2261ee00c567ff43be270ec96f78d9885e0b8e GIT binary patch literal 124693 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 - - - - - - - - - - - - - \ No newline at end of file diff --git a/doc/img/dtsh_home.png b/doc/img/dtsh_home.png deleted file mode 100644 index 7434ffc98617ee4e6bb392a4aa8eaf2c78f4a7aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 526311 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 z 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
qb9k`B$D`4Ku^nK*zj@87?#OXk3UIk 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%0AM9j5HIBh1Pi5SUb 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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /soc:                                                                                                                                                                -Name                Address   Labels               Aliases  Compatible                   Description                                                       -──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── -interrupt-controller0xe000e100nvicarm,v7m-nvicARMv7-M NVIC (Nested Vectored Interrupt Controller)               -timer               0xe000e010systickarm,armv7m-systick -ficr                0x10000000ficrnordic,nrf-ficrNordic FICR (Factory Information Configuration Registers)         -uicr                0x10001000uicrnordic,nrf-uicrNordic UICR (User Information Configuration Registers)            -memory              0x20000000sram0mmio-sramGeneric on-chip SRAM description                                  -clock               0x40000000clocknordic,nrf-clockNordic nRF clock control node                                     -power               0x40000000powernordic,nrf-powerNordic nRF power control node                                     -radio               0x40001000radionordic,nrf-radioNordic nRF family RADIO peripheral…                               -uart                0x40002000uart0nordic,nrf-uarteNordic nRF family UARTE (UART with EasyDMA)                       -i2c                 0x40003000i2c0arduino_i2cnordic,nrf-twiNordic nRF family TWI (TWI master)…                               -spi                 0x40003000spi0nordic,nrf-spiNordic nRF family SPI (SPI master)                                -i2c                 0x40004000i2c1nordic,nrf-twiNordic nRF family TWI (TWI master)…                               -spi                 0x40004000spi1nordic,nrf-spiNordic nRF family SPI (SPI master)                                -nfct                0x40005000nfctnordic,nrf-nfctNordic nRF family NFCT (Near Field Communication Tag)             -gpiote              0x40006000gpiotenordic,nrf-gpioteNRF5 GPIOTE node                                                  -adc                 0x40007000adcnordic,nrf-saadcNordic Semiconductor nRF family SAADC node                        -timer               0x40008000timer0nordic,nrf-timerNordic nRF timer node                                             -timer               0x40009000timer1nordic,nrf-timerNordic nRF timer node                                             -timer               0x4000a000timer2nordic,nrf-timerNordic nRF timer node                                             -rtc                 0x4000b000rtc0nordic,nrf-rtcNordic nRF RTC (Real-Time Counter)                                -temp                0x4000c000tempnordic,nrf-tempNordic nRF family TEMP node                                       -random              0x4000d000rngnordic,nrf-rngNordic nRF family RNG (Random Number Generator)                   -ecb                 0x4000e000ecbnordic,nrf-ecbNordic ECB (AES electronic codebook mode encryption)              -ccm                 0x4000f000ccmnordic,nrf-ccmNordic nRF family CCM (AES CCM mode encryption)                   -watchdog            0x40010000wdtwdt0watchdog0nordic,nrf-wdtNordic nRF family WDT (Watchdog Timer)                            -rtc                 0x40011000rtc1nordic,nrf-rtcNordic nRF RTC (Real-Time Counter)                                -qdec                0x40012000qdecqdec0nordic,nrf-qdecNordic nRF quadrature decoder (QDEC) node                         -comparator          0x40013000compnordic,nrf-compNordic nRF family COMP (Comparator)                               -egu                 0x40014000egu0swi0nordic,nrf-egunordic,nrf-swiNordic EGU (Event Generator Unit)                                 -egu                 0x40015000egu1swi1nordic,nrf-egunordic,nrf-swiNordic EGU (Event Generator Unit)                                 -egu                 0x40016000egu2swi2nordic,nrf-egunordic,nrf-swiNordic EGU (Event Generator Unit)                                 -egu                 0x40017000egu3swi3nordic,nrf-egunordic,nrf-swiNordic EGU (Event Generator Unit)                                 -egu                 0x40018000egu4swi4nordic,nrf-egunordic,nrf-swiNordic EGU (Event Generator Unit)                                 -egu                 0x40019000egu5swi5nordic,nrf-egunordic,nrf-swiNordic EGU (Event Generator Unit)                                 -timer               0x4001a000timer3nordic,nrf-timerNordic nRF timer node                                             -timer               0x4001b000timer4nordic,nrf-timerNordic nRF timer node                                             -pwm                 0x4001c000pwm0nordic,nrf-pwmnRF PWM                                                           -pdm                 0x4001d000pdm0nordic,nrf-pdmNordic PDM (Pulse Density Modulation interface)                   -acl                 0x4001e000aclnordic,nrf-aclNordic nRF family ACL (Access Control List)                       -flash-controller    0x4001e000flash_controllernordic,nrf52-flash-controllerNordic NVMC (Non-Volatile Memory Controller)                      -ppi                 0x4001f000ppinordic,nrf-ppiNordic nRF family PPI (Programmable Peripheral Interconnect)      -mwu                 0x40020000mwunordic,nrf-mwuNordic nRF family MWU (Memory Watch Unit)                         -pwm                 0x40021000pwm1nordic,nrf-pwmnRF PWM                                                           -pwm                 0x40022000pwm2nordic,nrf-pwmnRF PWM                                                           -spi                 0x40023000spi2nordic,nrf-spiNordic nRF family SPI (SPI master)                                -rtc                 0x40024000rtc2nordic,nrf-rtcNordic nRF RTC (Real-Time Counter)                                -i2s                 0x40025000i2s0nordic,nrf-i2sNordic I2S (Inter-IC sound interface)                             -usbd                0x40027000usbdzephyr_udc0nordic,nrf-usbdNordic nRF52 USB device controller                                -uart                0x40028000uart1arduino_serialnordic,nrf-uarteNordic nRF family UARTE (UART with EasyDMA)                       -qspi                0x40029000qspinordic,nrf-qspiProperties defining the interface for the Nordic QSPI peripheral… -pwm                 0x4002d000pwm3nordic,nrf-pwmnRF PWM                                                           -spi                 0x4002f000spi3arduino_spinordic,nrf-spimNordic nRF family SPIM (SPI master with EasyDMA)                  -gpio                0x50000000gpio0nordic,nrf-gpioNRF5 GPIO node                                                    -gpio                0x50000300gpio1nordic,nrf-gpioNRF5 GPIO node                                                    -crypto              0x5002a000cryptocellnordic,nrf-cc310Nordic Control Interface for ARM TrustZone CryptoCell 310         - - - - diff --git a/doc/ug/DTSh.pdf b/doc/ug/DTSh.pdf new file mode 100644 index 0000000000000000000000000000000000000000..11d95ecb92144ca8b0023b200a156184f2c5ab5c GIT binary patch literal 223183 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�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 E2Z?75t^fc4 literal 0 HcmV?d00001 diff --git a/etc/sh/interactive-tests.sh b/etc/sh/interactive-tests.sh deleted file mode 100755 index 255c1a5..0000000 --- a/etc/sh/interactive-tests.sh +++ /dev/null @@ -1,428 +0,0 @@ -# Copyright (c) 2022 Chris Duf -# -# SPDX-License-Identifier: Apache-2.0 - -# Helper for starting dtsh with various test configurations. - -thisfile=$(readlink -f "$0") -thisdir=$(dirname "$thisfile") -DTSH_HOME=$(readlink -f "$thisdir/../..") -unset thisfile -unset thisdir - -arg_zephyr_base="$1" -arg_zephyr_sdk="$2" - -if [ -n "$arg_zephyr_base" ]; then - TEST_ZEPHYR_BASE="$arg_zephyr_base" - TEST_WEST_BASE=$(realpath -m "$arg_zephyr_base/..") -else - TEST_WEST_BASE=$(realpath -m /mnt/platform/embedded/zephyr-rtos/workspaces/zephyr-sandbox) - TEST_ZEPHYR_BASE="$TEST_WEST_BASE/zephyr" -fi -if [ -n "$arg_zephyr_sdk" ]; then - TEST_ZEPHYR_SDK_DIR=$(realpath -m "$arg_zephyr_sdk") -else - TEST_ZEPHYR_SDK_DIR=$(realpath -m /mnt/platform/embedded/zephyr-rtos/sdk/zephyr-sdk-0.15) -fi -unset arg_zephyr_base -unset arg_zephyr_sdk - - -TEST_GCCARM10_DIR=$(realpath -m /mnt/platform/embedded/arm-gnu/gcc-arm-none-eabi-10) -TEST_GCCARM11_DIR=$(realpath -m /mnt/platform/embedded/arm-gnu/gcc-arm-none-eabi-11) - -TEST_DIR="$DTSH_HOME/tmp-tests" - -TEST_DTS_EDTLIB="$DTSH_HOME/tests/test.dts" -TEST_BINDINGS_EDTLIB="$DTSH_HOME/tests/bindings" - -TEST_PROJECT_SENSOR="$TEST_ZEPHYR_BASE/samples/sensor/bme680" -TEST_PROJECT_CAN="$TEST_ZEPHYR_BASE/samples/drivers/can/counter" -TEST_PROJECT_COAP="$TEST_ZEPHYR_BASE/samples/net/sockets/coap_client" -TEST_PROJECT_USB="$TEST_ZEPHYR_BASE/samples/subsys/usb/testusb" -TEST_PROJECT_BLE="$TEST_ZEPHYR_BASE/samples/bluetooth/eddystone" - -TEST_BOARD_NRF52='nrf52840dk_nrf52840' -TEST_BOARD_F407GZ='black_f407zg_pro' -#FIXME: Link to documentation is broken for mimxrt1170_evk_cm7, -# which has the same documentation page as mimxrt1170_evk. -TEST_BOARD_NXP='mimxrt1170_evk_cm7' -TEST_BOARD_NANO='arduino_nano_33_ble' - - -. "$DTSH_HOME/etc/sh/zef" -. "$DTSH_HOME/etc/sh/dtsh" - - -test_run_yn() { - echo -n 'Run test [yN]: ' - read yes_no - case "$yes_no" in - y|Y) - return 1 - ;; - *) - ;; - esac - return 0 -} - - -test_build_zephyr_project() { - echo '==== Build Zephyr project' - local arg_project="$1" - local arg_board="$2" - if [ -n "$arg_project" ]; then - arg_project=$(realpath -m "$arg_project") - else - echo 'What project ?' - zef_abort - fi - local previous_pwd=$(pwd) - echo "*** Build Zephyr project: $arg_project" - cd "$TEST_DIR" || zef_abort - zef_open "$TEST_WEST_BASE" "$TEST_ZEPHYR_SDK_DIR" || zef_abort - west build -b "$arg_board" "$arg_project" || zef_abort - zef_close || zef_abort - echo - cd "$previous_pwd" || zef_abort -} - - -run_unittests() { - echo '==== Unit tests: run unit tests before interactive sessions ?' - test_run_yn - if [ "$?" = 0 ]; then - return - fi - dtsh_unittests - sleep 2 -} - - -run_interactive_use_case1() { - echo '==== UC1: DTS and bindings from edtlib unit tests' - echo ' DTS: tests/test.dts' - echo ' Bindings search path: tests/bindings' - test_run_yn - if [ "$?" = 0 ]; then - return - fi - local test_venv="$TEST_DIR/.venv" - echo '==== Setup test environment' - dtsh_clean - dtsh_venv "$test_venv" - echo 'done.' - echo - "$VIRTUAL_ENV/bin/dtsh" "$TEST_DTS_EDTLIB" "$TEST_BINDINGS_EDTLIB" || zef_abort - echo 'done.' - echo - echo '==== Dispose test environment' - deactivate - rm -r "$TEST_DIR" - echo 'done.' -} - - -run_interactive_use_case2() { - echo '==== UC2: DTS from Zephyr build, Zephyr bindings' - echo ' Bindings search path: $ZEPHYR_BASE/dts/bindings' - # Only ZEPHYR_BASE is set. - echo ' Toolchain: unavailable' - echo " Application: $(basename "$TEST_PROJECT_SENSOR")" - echo " Board: $TEST_BOARD_NRF52" - test_run_yn - if [ "$?" = 0 ]; then - return - fi - local test_venv="$TEST_DIR/.venv" - if [ -d "$TEST_DIR" ]; then - rm -rf "$TEST_DIR" - fi - mkdir -p "$TEST_DIR" - test_build_zephyr_project "$TEST_PROJECT_SENSOR" "$TEST_BOARD_NRF52" - echo 'done.' - echo - echo '==== Setup test environment' - dtsh_clean - dtsh_venv "$test_venv" - export ZEPHYR_BASE="$TEST_ZEPHYR_BASE" - echo 'done.' - echo - "$VIRTUAL_ENV/bin/dtsh" "$TEST_DIR/build/zephyr/zephyr.dts" || zef_abort - echo 'done.' - echo - echo '==== Dispose test environment' - unset ZEPHYR_BASE - deactivate - rm -r "$TEST_DIR" - echo 'done.' -} - - -run_interactive_use_case3() { - echo '==== UC3: DTS from Zephyr build, Zephyr bindings' - echo ' Bindings search path: $ZEPHYR_BASE/dts/bindings' - echo ' Toolchain: Zephyr SDK' - echo " Application: $(basename "$TEST_PROJECT_SENSOR")" - echo " Board: $TEST_BOARD_NRF52" - test_run_yn - if [ "$?" = 0 ]; then - return - fi - local test_venv="$TEST_DIR/.venv" - if [ -d "$TEST_DIR" ]; then - rm -rf "$TEST_DIR" - fi - mkdir -p "$TEST_DIR" - test_build_zephyr_project "$TEST_PROJECT_SENSOR" "$TEST_BOARD_NRF52" - echo 'done.' - echo - echo '==== Setup test environment' - dtsh_clean - zef_open "$TEST_WEST_BASE" "$TEST_ZEPHYR_SDK_DIR" || zef_abort - pip install "$DTSH_HOME" || zef_abort - echo 'done.' - echo - "$VIRTUAL_ENV/bin/dtsh" "$TEST_DIR/build/zephyr/zephyr.dts" || zef_abort - echo 'done.' - echo - echo '==== Dispose test environment' - pip uninstall --yes dtsh || zef_abort - zef_close || zef_abort - rm -r "$TEST_DIR" - echo 'done.' -} - - -run_interactive_use_case4() { - echo '==== UC4: DTS from Zephyr build, Zephyr bindings' - echo ' Bindings search path: $ZEPHYR_BASE/dts/bindings' - echo ' Toolchain (dtsh): Zephyr SDK' - echo " Application: $(basename "$TEST_PROJECT_CAN")" - echo " Board: $TEST_BOARD_F407GZ" - test_run_yn - if [ "$?" = 0 ]; then - return - fi - local test_venv="$TEST_DIR/.venv" - if [ -d "$TEST_DIR" ]; then - rm -rf "$TEST_DIR" - fi - mkdir -p "$TEST_DIR" - test_build_zephyr_project "$TEST_PROJECT_CAN" "$TEST_BOARD_F407GZ" - echo 'done.' - echo - echo '==== Setup test environment' - dtsh_clean - zef_open "$TEST_WEST_BASE" "$TEST_ZEPHYR_SDK_DIR" || zef_abort - pip install "$DTSH_HOME" || zef_abort - echo 'done.' - echo - "$VIRTUAL_ENV/bin/dtsh" "$TEST_DIR/build/zephyr/zephyr.dts" || zef_abort - echo 'done.' - echo - echo '==== Dispose test environment' - pip uninstall --yes dtsh || zef_abort - zef_close || zef_abort - rm -r "$TEST_DIR" - echo 'done.' -} - - -run_interactive_use_case5() { - echo '==== UC5: DTS from Zephyr build, Zephyr bindings' - echo ' Bindings search path: $ZEPHYR_BASE/dts/bindings' - echo ' Toolchain (dtsh): Zephyr SDK' - echo " Application: $(basename "$TEST_PROJECT_COAP")" - echo " Board: $TEST_BOARD_NXP" - test_run_yn - if [ "$?" = 0 ]; then - return - fi - local test_venv="$TEST_DIR/.venv" - if [ -d "$TEST_DIR" ]; then - rm -rf "$TEST_DIR" - fi - mkdir -p "$TEST_DIR" - test_build_zephyr_project "$TEST_PROJECT_COAP" "$TEST_BOARD_NXP" - echo 'done.' - echo - echo '==== Setup test environment' - dtsh_clean - zef_open "$TEST_WEST_BASE" "$TEST_ZEPHYR_SDK_DIR" || zef_abort - pip install "$DTSH_HOME" || zef_abort - echo 'done.' - echo - "$VIRTUAL_ENV/bin/dtsh" "$TEST_DIR/build/zephyr/zephyr.dts" || zef_abort - echo 'done.' - echo - echo '==== Dispose test environment' - pip uninstall --yes dtsh || zef_abort - zef_close || zef_abort - rm -r "$TEST_DIR" - echo 'done.' -} - - -run_interactive_use_case6() { - echo '==== UC6: DTS from Zephyr build, Zephyr bindings' - echo ' Bindings search path: $ZEPHYR_BASE/dts/bindings' - echo ' Toolchain (dtsh): Zephyr SDK' - echo " Application: $(basename "$TEST_PROJECT_USB")" - echo " Board: $TEST_BOARD_NXP" - test_run_yn - if [ "$?" = 0 ]; then - return - fi - local test_venv="$TEST_DIR/.venv" - if [ -d "$TEST_DIR" ]; then - rm -rf "$TEST_DIR" - fi - mkdir -p "$TEST_DIR" - test_build_zephyr_project "$TEST_PROJECT_USB" "$TEST_BOARD_NXP" - echo 'done.' - echo - echo '==== Setup test environment' - dtsh_clean - zef_open "$TEST_WEST_BASE" "$TEST_ZEPHYR_SDK_DIR" || zef_abort - pip install "$DTSH_HOME" || zef_abort - echo 'done.' - echo - "$VIRTUAL_ENV/bin/dtsh" "$TEST_DIR/build/zephyr/zephyr.dts" || zef_abort - echo 'done.' - echo - echo '==== Dispose test environment' - pip uninstall --yes dtsh || zef_abort - zef_close || zef_abort - rm -r "$TEST_DIR" - echo 'done.' -} - - -run_interactive_use_case7() { - echo '==== UC7: DTS from Zephyr build, Zephyr bindings' - echo ' Bindings search path: $ZEPHYR_BASE/dts/bindings' - echo ' Toolchain (dtsh): Zephyr SDK' - echo " Application: $(basename "$TEST_PROJECT_BLE")" - echo " Board: $TEST_BOARD_NANO" - test_run_yn - if [ "$?" = 0 ]; then - return - fi - local test_venv="$TEST_DIR/.venv" - if [ -d "$TEST_DIR" ]; then - rm -rf "$TEST_DIR" - fi - mkdir -p "$TEST_DIR" - test_build_zephyr_project "$TEST_PROJECT_BLE" "$TEST_BOARD_NANO" - echo 'done.' - echo - echo '==== Setup test environment' - dtsh_clean - zef_open "$TEST_WEST_BASE" "$TEST_ZEPHYR_SDK_DIR" || zef_abort - pip install "$DTSH_HOME" || zef_abort - echo 'done.' - echo - "$VIRTUAL_ENV/bin/dtsh" "$TEST_DIR/build/zephyr/zephyr.dts" || zef_abort - echo 'done.' - echo - echo '==== Dispose test environment' - pip uninstall --yes dtsh || zef_abort - zef_close || zef_abort - rm -r "$TEST_DIR" - echo 'done.' -} - - -run_interactive_use_case8() { - echo '==== UC8: DTS from Zephyr build, Zephyr bindings' - echo ' Bindings search path: $ZEPHYR_BASE/dts/bindings' - echo ' Toolchain: GCC Arm 10' - echo " Application: $(basename "$TEST_PROJECT_SENSOR")" - echo " Board: $TEST_BOARD_NRF52" - test_run_yn - if [ "$?" = 0 ]; then - return - fi - local test_venv="$TEST_DIR/.venv" - if [ -d "$TEST_DIR" ]; then - rm -rf "$TEST_DIR" - fi - mkdir -p "$TEST_DIR" - test_build_zephyr_project "$TEST_PROJECT_SENSOR" "$TEST_BOARD_NRF52" - echo 'done.' - echo - echo '==== Setup test environment' - dtsh_clean - zef_open "$TEST_WEST_BASE" "$TEST_GCCARM10_DIR" || zef_abort - pip install "$DTSH_HOME" || zef_abort - echo 'done.' - echo - "$VIRTUAL_ENV/bin/dtsh" "$TEST_DIR/build/zephyr/zephyr.dts" || zef_abort - echo 'done.' - echo - echo '==== Dispose test environment' - pip uninstall --yes dtsh || zef_abort - zef_close || zef_abort - rm -r "$TEST_DIR" - echo 'done.' -} - - -run_interactive_use_case9() { - echo '==== UC9: DTS from Zephyr build, Zephyr bindings' - echo ' Bindings search path: $ZEPHYR_BASE/dts/bindings' - echo ' Toolchain: GCC Arm 11' - echo " Application: $(basename "$TEST_PROJECT_SENSOR")" - echo " Board: $TEST_BOARD_NRF52" - test_run_yn - if [ "$?" = 0 ]; then - return - fi - local test_venv="$TEST_DIR/.venv" - if [ -d "$TEST_DIR" ]; then - rm -rf "$TEST_DIR" - fi - mkdir -p "$TEST_DIR" - test_build_zephyr_project "$TEST_PROJECT_SENSOR" "$TEST_BOARD_NRF52" - echo 'done.' - echo - echo '==== Setup test environment' - dtsh_clean - zef_open "$TEST_WEST_BASE" "$TEST_GCCARM11_DIR" || zef_abort - pip install "$DTSH_HOME" || zef_abort - echo 'done.' - echo - "$VIRTUAL_ENV/bin/dtsh" "$TEST_DIR/build/zephyr/zephyr.dts" || zef_abort - echo 'done.' - echo - echo '==== Dispose test environment' - pip uninstall --yes dtsh || zef_abort - zef_close || zef_abort - rm -r "$TEST_DIR" - echo 'done.' -} - - -clear -run_unittests -clear -run_interactive_use_case1 -clear -run_interactive_use_case2 -clear -run_interactive_use_case3 -clear -run_interactive_use_case4 -clear -run_interactive_use_case5 -clear -run_interactive_use_case6 -clear -run_interactive_use_case7 -clear -run_interactive_use_case8 -clear -run_interactive_use_case9 diff --git a/pyproject.toml b/pyproject.toml index fed528d..1a268f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,20 @@ [build-system] requires = ["setuptools"] build-backend = "setuptools.build_meta" + + +[tool.black] +# References: +# - https://black.readthedocs.io/en/stable/usage_and_configuration/ +line-length = 80 +target-version = ['py38'] +include = '\.pyi?$' +# 'extend-exclude' excludes files or directories in addition to the defaults +extend-exclude = ''' +# A regex preceded with ^/ will apply only to files and directories +# in the root of the project. +( + ^/foo.py # exclude a file named foo.py in the root of the project + | .*_pb2.py # exclude autogenerated Protocol Buffer files anywhere in the project +) +''' diff --git a/pyrightconfig.json b/pyrightconfig.json deleted file mode 100644 index a701a4e..0000000 --- a/pyrightconfig.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "include": [ - "src", - "tests" - ], - - "exclude": [ - "tests/bindings" - ], - - "pythonVersion": "3.8", - "pythonPlatform": "All", - - "venvPath": ".", - "venv": ".venv", - - "executionEnvironments": [ - { - "root": "tests", - "extraPaths": [ - "src" - ] - } - ] -} diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 52e264c..0000000 --- a/pytest.ini +++ /dev/null @@ -1,9 +0,0 @@ -# pytest configuration. -# -# See: -# - https://docs.pytest.org/en/7.3.x/reference/customize.html#pytest-ini -# - https://docs.pytest.org/en/7.3.x/reference/reference.html#ini-options-ref - -[pytest] -testpaths = tests -pythonpath = src diff --git a/requirements-dev.txt b/requirements-dev.txt index aa65804..a12df06 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,7 +1,6 @@ -# Python requirements for development/tests. -# +pycodestyle +flake8 +pylint mypy types-PyYAML -pyright -pylint pytest diff --git a/requirements-lsp.txt b/requirements-lsp.txt new file mode 100644 index 0000000..1f7b4b9 --- /dev/null +++ b/requirements-lsp.txt @@ -0,0 +1,3 @@ +python-lsp-server[all] +pylsp-mypy +python-lsp-black diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..fb49c72 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +PyYAML +rich +gnureadline ; sys_platform == 'darwin' diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..8236127 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,173 @@ +[metadata] +name = dtsh +version = 0.2rc1 +author = Christophe Dufaza +author_email = chris@openmarl.org +description = Shell-like interface with Zephyr Devicetree +long_description = file: README.rst +license = Apache License version 2.0 +url = https://github.com/dottspina/dtsh +keywords = devicetree, zephyr, dts, embedded + +classifiers = + Development Status :: 4 - Beta + Programming Language :: Python :: 3 + Intended Audience :: Developers + Topic :: Software Development :: Embedded Systems + License :: OSI Approved :: Apache Software License + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3 :: Only + +[options] +python_requires = >=3.8 +install_requires = + PyYAML + rich + gnureadline ; sys_platform == 'darwin' + +packages = + dtsh + dtsh.builtins + dtsh.rich + devicetree + +package_dir = + = src + +[options.package_data] +dtsh = + py.typed + dtsh.ini +dtsh.rich = + theme.ini + +[options.entry_points] +console_scripts = + dtsh = dtsh.cli:run + +[options.extras_require] +# Linters, type hinting, unit tests, etc. +dev = + pycodestyle + flake8 + pylint + mypy + types-PyYAML + pytest +# IDE/LSP integration. +lsp = + python-lsp-server[all] + pylsp-mypy + python-lsp-black +# Package distribution only +dist = + build + twine + +[tool:pytest] +pythonpath = src +testpaths = tests + + +[pycodestyle] +# References: +# - https://pycodestyle.pycqa.org/en/latest/intro.html#configuration +max-line-length = 80 + + +[pydocstyle] +# References: +# - https://peps.python.org/pep-0257/ +# - http://www.pydocstyle.org/en/stable/usage.html +# - https://google.github.io/styleguide/pyguide.html#Comments +# +# Cannot pass both ignore and convention (unfortunately). +#ignore = D105 + +# Relax pydocstyle for test source code. +convention = google +match_dir = ^(?!tests|build|\.venv).* + + +[pylint.] +# References: +# - https://pylint.readthedocs.io/en/latest/user_guide/usage/run.html +disable = + # invalid-name + # Fix: except Exception as e + C0103, + # too-many-ancestor + # Fix: _Loader(YAMLLoader) + R0901, + # too-many-instance-attributes + R0902, + # too-few-public-methods + # Example: abstract base class DTNodeCriterion + R0903, + # too-many-public-methods + R0904, + # too-many-return-statements + R0911, + # too-many-branches + R0912, + # too-many-function-args + R0913, + # too-many-locals + R0914, + # line-too-long + # Example: URL in docstrings + C0301, + # too-many-lines + # Example: dtsh.model module + C0302, + # missing-function-docstring + # C0116, + # missing-class-docstring + # C0115, + # protected-access + # W0212, + # pointless-statement + # W0104 +# To ignore files or directories (base names, not paths): +# ignore= +ignore = setup.py + +# Zephyr linter configuration. +min-similarity-lines = 10 + + +[flake8] +# References: +# - https://flake8.pycqa.org/en/latest/user/configuration.html +extend-ignore = + # line-too-long: we rely on black for this + E501 + # black formatting would fail with "whitespace before ':'" + # See https://github.com/psf/black/issues/280 + E203 + + +[mypy] +# References: +# - https://mypy.readthedocs.io/en/stable/config_file.html +mypy_path = src:tests +exclude = tests/res +python_version = 3.8 +packages = dtsh + + +[pylsp-mypy] +# References: +# - https://github.com/python-lsp/pylsp-mypy +enabled = true +dmypy = false +live_mode = true +strict = true + + +[pep8] +aggressive = 3 diff --git a/setup.py b/setup.py index de42962..c87d899 100644 --- a/setup.py +++ b/setup.py @@ -1,172 +1,10 @@ -"""Project configuration (setuptools). -""" +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 -# Always prefer setuptools over distutils -from setuptools import setup, find_packages -import pathlib +"""Required to bootstrap setipt.cfg in some situations.""" -here = pathlib.Path(__file__).parent.resolve() +from setuptools import setup # type: ignore -long_description = (here / "README.rst").read_text(encoding="utf-8") - -setup( - # There are some restrictions on what makes a valid project name - # specification here: - # https://packaging.python.org/specifications/core-metadata/#name - # - name="dtsh", - - # Versions should comply with PEP 440: - # https://www.python.org/dev/peps/pep-0440/ - # - # For a discussion on single-sourcing the version across setup.py and the - # project code, see - # https://packaging.python.org/guides/single-sourcing-package-version/ - # - # See also: https://peps.python.org/pep-0440/ - # - version="0.1.0b2", - - # This is a one-line description or tagline of what your project does. This - # corresponds to the "Summary" metadata field: - # https://packaging.python.org/specifications/core-metadata/#summary - # - description="Shell-like interface with Zephyr devicetree and bindings", - - # This field corresponds to the "Description" metadata field: - # https://packaging.python.org/specifications/core-metadata/#description-optional - # - long_description=long_description, - - # Denotes that our long_description is in Markdown; valid values are - # text/plain, text/x-rst, and text/markdown - # - # Optional if long_description is written in reStructuredText (rst) but - # required for plain-text or Markdown; if unspecified, "applications should - # attempt to render [the long_description] as text/x-rst; charset=UTF-8 and - # fall back to text/plain if it is not valid rst" (see link below) - # - # This field corresponds to the "Description-Content-Type" metadata field: - # https://packaging.python.org/specifications/core-metadata/#description-content-type-optional - # - #long_description_content_type="text/markdown", - - # This field corresponds to the "Home-Page" metadata field: - # https://packaging.python.org/specifications/core-metadata/#home-page-optional - # - url="https://github.com/dottspina/dtsh", - - author="Chris Duf", - author_email="chris@openmarl.org", - - license="Apache License version 2.0", - - # Classifiers help users find your project by categorizing it. - # - # For a list of valid classifiers, see https://pypi.org/classifiers/ - # - classifiers=[ - # How mature is this project? Common values are - # 3 - Alpha - # 4 - Beta - # 5 - Production/Stable - "Development Status :: 4 - Beta", - # Indicate who your project is intended for - "Intended Audience :: Developers", - "Topic :: Software Development :: Embedded Systems", - # Pick your license as you wish - "License :: OSI Approved :: Apache Software License", - # Specify the Python versions you support here. In particular, ensure - # that you indicate you support Python 3. These classifiers are *not* - # checked by 'pip install'. See instead 'python_requires' below. - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3 :: Only", - ], - - # Note that this is a list of additional keywords, separated - # by commas, to be used to assist searching for the distribution in a - # larger catalog. - # - keywords="devicetree, zephyr, dts, embedded", - - package_dir={"": "src"}, - packages=find_packages(where="src"), - - # Specify which Python versions you support. In contrast to the - # 'Programming Language' classifiers above, 'pip install' will check this - # and refuse to install the project if the version does not match. See - # https://packaging.python.org/guides/distributing-packages-using-setuptools/#python-requires - python_requires=">=3.8, <4", - - # This field lists other packages that your project depends on to run. - # Any package you put here will be installed by pip when your project is - # installed, so they must be valid existing projects. - # - # For an analysis of "install_requires" vs pip's requirements files see: - # https://packaging.python.org/discussions/install-requires-vs-requirements/ - # - # Requirements for both devicetree and dtsh. - install_requires=[ - "PyYAML>=5.1", "rich", "Pygments", - # Preferred GNU readline support for macOS. - "gnureadline;sys_platform=='darwin'", - ], - - # List additional groups of dependencies here (e.g. development - # dependencies). Users will be able to install these using the "extras" - # syntax, for example: - # - # $ pip install sampleproject[dev] - # - # Similar to `install_requires` above, these must be valid existing - # projects. - # - # Optional. - extras_require={ - "dev": ["mypy", "types-PyYAML", "pyright", "pylint", "pytest"], - "test": ["pytest"], - "dist": ["build", "twine"], - }, - - # If there are data files included in your packages that need to be - # installed, specify them here. - package_data={ - "dtsh": ["theme"], - }, - - # Although 'package_data' is the preferred approach, in some case you may - # need to place data files outside of your packages. See: - # http://docs.python.org/distutils/setupscript.html#installing-additional-files - # - # In this case, 'data_file' will be installed into '/my_data' - # - # Optional. - # data_files=[("my_data", ["data/data_file"])], # Optional - - # To provide executable scripts, use entry points in preference to the - # "scripts" keyword. Entry points provide cross-platform support and allow - # `pip` to create the appropriate form of executable for the target - # platform. - # - entry_points={ - "console_scripts": [ - "dtsh=dtsh.cli:run", - ], - }, - - # List additional URLs that are relevant to your project as a dict. - # - # This field corresponds to the "Project-URL" metadata fields: - # https://packaging.python.org/specifications/core-metadata/#project-url-multiple-use - # - project_urls={ - # 'Documentation': 'https://packaging.python.org/tutorials/distributing-packages/', - "Bug Reports": "https://github.com/dottspina/dtsh/issues", - # "Funding": "https://donate.pypi.org", - # "Say Thanks!": "http://saythanks.io/to/example", - "Source": "https://github.com/dottspina/dtsh", - }, -) +if __name__ == "__main__": + setup() diff --git a/src/dtsh/__init__.py b/src/dtsh/__init__.py index e69de29..c8394a3 100644 --- a/src/dtsh/__init__.py +++ b/src/dtsh/__init__.py @@ -0,0 +1,5 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Core devicetree shell API.""" diff --git a/src/dtsh/autocomp.py b/src/dtsh/autocomp.py index d07f0f1..324821c 100644 --- a/src/dtsh/autocomp.py +++ b/src/dtsh/autocomp.py @@ -1,135 +1,766 @@ -# Copyright (c) 2022 Chris Duf +# Copyright (c) 2023 Christophe Dufaza # # SPDX-License-Identifier: Apache-2.0 -"""Auto-completion with GNU readline for devicetree shells.""" +"""Completion logic and base display callbacks for GNU Readline integration. -from typing import cast, Any, Dict, List +Unit tests and examples: tests/test_dtsh_autocomp.py +""" -from devicetree.edtlib import Node, Binding, Property -from dtsh.dtsh import Dtsh, DtshCommand, DtshAutocomp +from typing import cast, List, Set +import os -class DevicetreeAutocomp(DtshAutocomp): - """Devicetree shell commands auto-completion support with GNU readline. - """ +from dtsh.model import DTPath, DTNode, DTBinding +from dtsh.rl import DTShReadline +from dtsh.io import DTShOutput +from dtsh.config import DTShConfig +from dtsh.shell import ( + DTSh, + DTShCommand, + DTShOption, + DTShArg, + DTShCommandNotFoundError, + DTPathNotFoundError, +) - # Maps completion state (strings) and model (objects). - # - _autocomp_state: Dict[str, Any] - # Autocomp mode. - # - _mode: int +_dtshconf: DTShConfig = DTShConfig.getinstance() - def __init__(self, shell: Dtsh) -> None: - """Initialize the completion engine. + +class RlStateDTShCommand(DTShReadline.CompleterState): + """RL completer state for DTSh commands.""" + + def __init__(self, rlstr: str, cmd: DTShCommand) -> None: + """Initialize completer state. + + Args: + rlstr: The command name to substitute the RL completion scope with. + cmd: The corresponding command. + """ + super().__init__(rlstr, cmd) + + @property + def cmd(self) -> DTShCommand: + """The corresponding command.""" + return cast(DTShCommand, self._item) + + +class RlStateDTShOption(DTShReadline.CompleterState): + """RL completer state for DTSh command options.""" + + def __init__(self, rlstr: str, opt: DTShOption) -> None: + """Initialize completer state. + + Args: + rlstr: The option name to substitute the RL completion scope with. + opt: The corresponding option. + """ + super().__init__(rlstr, opt) + + @property + def opt(self) -> DTShOption: + """The corresponding option.""" + return cast(DTShOption, self._item) + + +class RlStateDTPath(DTShReadline.CompleterState): + """RL completer state for Devicetree paths.""" + + def __init__(self, rlstr: str, node: DTNode) -> None: + """Initialize completer state. + + Args: + rlstr: The absolute or relative Devicetree path to substitute + the RL completion scope with. + node: The corresponding Devicetree node. + """ + super().__init__(rlstr, node) + + @property + def node(self) -> DTNode: + """The corresponding Devicetree node.""" + return cast(DTNode, self._item) + + +class RlStateCompatStr(DTShReadline.CompleterState): + """RL completer state for compatible strings.""" + + _bindings: Set[DTBinding] + + def __init__(self, rlstr: str, bindings: Set[DTBinding]) -> None: + """Initialize completer state. + + Args: + rlstr: The compatible string to substitute the RL completion scope with. """ - self._dtsh = shell - self._mode = DtshAutocomp.MODE_ANY - self._autocomp_state = {} + super().__init__(rlstr, None) + self._bindings = bindings + + @property + def compatstr(self) -> str: + """The corresponding compatible string value.""" + return self._rlstr @property - def count(self) -> int: - """Implements DtshAutocomp.count(). + def bindings(self) -> Set[DTBinding]: + """All bindings for this compatible (e.g. for different buses).""" + return self._bindings + + +class RlStateDTVendor(DTShReadline.CompleterState): + """RL completer state for device or device class vendors.""" + + def __init__(self, rlstr: str, vendor: str) -> None: + """Initialize completer state. + + Args: + rlstr: The vendor prefix to substitute the RL completion scope with. + vendor: The corresponding vendor name. """ - return len(self._autocomp_state) + super().__init__(rlstr, vendor) @property - def hints(self) -> List[str]: - """Implements DtshAutocomp.hints(). + def prefix(self) -> str: + """The corresponding vendor prefix.""" + return self._rlstr + + @property + def vendor(self) -> str: + """The corresponding vendor name.""" + return cast(str, self._item) + + +class RlStateDTBus(DTShReadline.CompleterState): + """RL completer state for Devicetree bus protocols.""" + + def __init__(self, rlstr: str) -> None: + """Initialize completer state. + + Args: + rlstr: The bus protocol to substitute the RL completion scope with. """ - return list(self._autocomp_state.keys()) + super().__init__(rlstr, None) @property - def model(self) -> List[Any]: - """Implements DtshAutocomp.model(). + def proto(self) -> str: + """The bus protocol.""" + return self._rlstr + + +class RlStateDTAlias(DTShReadline.CompleterState): + """RL completer state for aliased nodes.""" + + def __init__(self, rlstr: str, node: DTNode) -> None: + """Initialize completer state. + + Args: + rlstr: The alias name to substitute the RL completion scope with. + node: The aliased node. """ - return list(self._autocomp_state.values()) + super().__init__(rlstr, node) @property - def mode(self) -> int: - """Implements DtshAutocomp.mode(). + def alias(self) -> str: + """The corresponding alias name.""" + return self._rlstr + + @property + def node(self) -> DTNode: + """The aliased node.""" + return cast(DTNode, self._item) + + +class RlStateDTChosen(DTShReadline.CompleterState): + """RL completer state for chosen nodes.""" + + def __init__(self, rlstr: str, node: DTNode) -> None: + """Initialize completer state. + + Args: + rlstr: The parameter name to substitute the RL completion scope with. + node: The chosen node. + """ + super().__init__(rlstr, node) + + @property + def chosen(self) -> str: + """The chosen parameter name.""" + return self._rlstr + + @property + def node(self) -> DTNode: + """The chosen node.""" + return cast(DTNode, self._item) + + +class RlStateDTLabel(DTShReadline.CompleterState): + """RL completer state for labeled nodes.""" + + def __init__(self, rlstr: str, node: DTNode) -> None: + """Initialize completer state. + + Args: + rlstr: The label candidate, prefixed with "&" when completing + path parameters, without the leading "&" otherwise. + node: The labeled node. + """ + super().__init__(rlstr, node) + + @property + def label(self) -> str: + """The label (without the leading "&").""" + return self._rlstr[1:] if self._rlstr.startswith("&") else self._rlstr + + @property + def node(self) -> DTNode: + """The labeled node.""" + return cast(DTNode, self._item) + + +class RlStateFsEntry(DTShReadline.CompleterState): + """RL completer state for file-system paths.""" + + def __init__(self, rlstr: str, dirent: os.DirEntry[str]) -> None: + """Initialize completer state. + + Args: + rlstr: The file-system path to substitute the RL completion scope with. + dirent: The corresponding file or directory. + """ + super().__init__(rlstr, dirent) + + @property + def dirent(self) -> os.DirEntry[str]: + """The corresponding file or directory.""" + return cast(os.DirEntry[str], self._item) + + +class RlStateEnum(DTShReadline.CompleterState): + """RL completer state for enumerated values.""" + + def __init__(self, rlstr: str, brief: str) -> None: + """Initialize completer state. + + Args: + rlstr: The value string to substitute the RL completion scope with. + brief: The value semantic. + """ + super().__init__(rlstr, brief) + + @property + def value(self) -> str: + """The enumerated value.""" + return self._rlstr + + @property + def brief(self) -> str: + """The corresponding description.""" + return cast(str, self._item) + + +class DTShAutocomp: + """Base completion logic and display callbacks for GNU readline integration.""" + + # The shell this will auto-complete the command line of. + _dtsh: DTSh + + @staticmethod + def complete_dtshcmd( + cs_txt: str, sh: DTSh + ) -> List[DTShReadline.CompleterState]: + """Complete command name. + + Args: + cs_txt: The command name to complete. + sh: The context shell. + + Returns: + The commands that are valid completer states. """ - return self._mode + return sorted( + RlStateDTShCommand(cmd.name, cmd) + for cmd in sh.commands + if cmd.name.startswith(cs_txt) + ) - def reset(self) -> None: - """Implements DtshAutocomp.reset(). + @staticmethod + def complete_dtshopt( + cs_txt: str, cmd: DTShCommand + ) -> List[DTShReadline.CompleterState]: + """Complete option name. + + Args: + cs_txt: The option name to complete, starting with "--" or "-". + cmd: The command to search for matching options. + + Returns: + The options that are valid completer states. """ - self._mode = DtshAutocomp.MODE_ANY - self._autocomp_state.clear() + states: List[DTShReadline.CompleterState] = [] + + if cs_txt.startswith("--"): + # Only options with a long name. + prefix = cs_txt[2:] + states.extend( + sorted( + [ + RlStateDTShOption(f"--{opt.longname}", opt) + for opt in cmd.options + if opt.longname and opt.longname.startswith(prefix) + ], + key=lambda x: "-" + if x.rlstr == "--help" + else x.rlstr.lower(), + ) + ) + elif cs_txt.startswith("-"): + if cs_txt == "-": + # All options, those with a short name first. + states.extend( + sorted( + [ + RlStateDTShOption(f"-{opt.shortname}", opt) + for opt in cmd.options + if opt.shortname + ], + key=lambda x: "-" + if x.rlstr == "-h" + else x.rlstr.lower(), + ) + ) + # Then options with only a long name. + states.extend( + sorted( + [ + RlStateDTShOption(f"--{opt.longname}", opt) + for opt in cmd.options + if not opt.shortname + ], + key=lambda x: x.rlstr.lower(), + ) + ) + else: + # Unique match, assuming the completion scope is "-". + opt = cmd.option(cs_txt) + if opt: + states.append(RlStateDTShOption(f"-{opt.shortname}", opt)) + + return states + + @staticmethod + def complete_dtpath( + cs_txt: str, sh: DTSh + ) -> List[DTShReadline.CompleterState]: + """Complete Devicetree path. + + Args: + cs_txt: The absolute or relative Devicetree path to complete. + sh: The context shell. - def autocomplete(self, - cmdline: str, - prefix: str, - cursor: int = 0) -> List[str]: # pyright: ignore reportUnusedVariable - """Implements DtshAutocomp.autocomplete(). + Returns: + The Devicetree paths that are valid completer states. """ - self.reset() - cmdline_vstr = cmdline.lstrip().split() - if len(cmdline_vstr) == 0: - self._autocomp_empty_cmdline() - elif prefix and (len(cmdline_vstr) == 1): - self._autocomp_with_commands(prefix) + if cs_txt.startswith("&"): + parts = DTPath.split(cs_txt) + if len(parts) == 1: + # Auto-complete with labels only for the first path component. + return DTShAutocomp.complete_dtlabel(cs_txt, sh) + + dirname = DTPath.dirname(cs_txt) + if (dirname == ".") and not cs_txt.startswith("./"): + # We'll search the current working branch + # because we complete a relative DT path. + # Though, we don't want to include the "." path component + # into the substitution strings if it's not actually part + # of the completion scope. + dirname = "" + + states: List[DTShReadline.CompleterState] = [] + try: + dirnode = sh.node_at(dirname) + except DTPathNotFoundError: + # Won't complete invalid path. + pass else: - cmd_name = cmdline_vstr[0] - cmd = self._dtsh.builtin(cmd_name) - if cmd: - if prefix.startswith('-'): - self._autocomp_with_options(cmd, prefix) - else: - self._autocomp_with_params(cmd, prefix) - - return self.hints - - def _autocomp_empty_cmdline(self) -> None: - self._mode = DtshAutocomp.MODE_DTSH_CMD - for cmd in self._dtsh.builtins: - self._autocomp_state[cmd.name] = cmd - - def _autocomp_with_commands(self, prefix: str) -> None: - self._mode = DtshAutocomp.MODE_DTSH_CMD - for cmd in self._dtsh.builtins: - if cmd.name.startswith(prefix) and (len(cmd.name) > len(prefix)): - self._autocomp_state[cmd.name] = cmd - - def _autocomp_with_options(self, cmd: DtshCommand, prefix: str) -> None: - self._mode = DtshAutocomp.MODE_DTSH_OPT - for opt in cmd.autocomplete_option(prefix): - # When building the options hints for rl_completion_matches(), - # we must answer the longest possible hints for the given prefix: - # we'll use the option's long name when it does not have any short - # name or the prefix starts with '--', its short name otherwise. - # The syntactic characters '-' are included in the hints since - # they're part of the prefix. - if opt.shortname and (not prefix.startswith('--')): - self._autocomp_state[f'-{opt.shortname}'] = opt - elif opt.longname: - self._autocomp_state[f'--{opt.longname}'] = opt - - def _autocomp_with_params(self, cmd:DtshCommand, prefix: str) -> None: - self._mode, model = cmd.autocomplete_param(prefix) - if self._mode == DtshAutocomp.MODE_DT_NODE: - for node in cast(List[Node], model): - hint = node.path - if node.children: - # Prepare auto-completion state for TABing - # the node's children enumeration. - # See readline_completions_hook(() in dtsh.session. - hint += '/' - self._autocomp_state[hint] = node - elif self._mode == DtshAutocomp.MODE_DT_PROP: - for prop in cast(List[Property], model): - hint = f'{prop.node.path}${prop.name}' - self._autocomp_state[hint] = prop - elif self._mode == DtshAutocomp.MODE_DT_BINDING: - for binding in cast(List[Binding], model): - self._autocomp_state[binding.compatible] = binding - elif self._mode == DtshAutocomp.MODE_DTSH_CMD: - for cmd in cast(List[DtshCommand], model): - self._autocomp_state[cmd.name] = cmd + prefix = DTPath.basename(cs_txt) + states.extend( + # Intelligently join and + # to get a valid substitution string. + RlStateDTPath(DTPath.join(dirname, child.name), child) + for child in dirnode.children + if child.name.startswith(prefix) + ) + + return sorted(states) + + @staticmethod + def complete_dtcompat( + cs_txt: str, sh: DTSh + ) -> List[DTShReadline.CompleterState]: + """Complete compatible string. + + Args: + cs_txt: The compatible string to complete. + sh: The context shell. + + Returns: + The compatible string values that are valid completer states, + associated with the relevant bindings. + """ + states: List[DTShReadline.CompleterState] = [] + for compatible in sorted( + ( + compat + for compat in sh.dt.compatible_strings + if compat.startswith(cs_txt) + ) + ): + bindings: Set[DTBinding] = set() + # 1st, try the natural API, with unknown bus. + binding = sh.dt.get_compatible_binding(compatible) + if binding: + # Exact single match (binding that does not expect + # a bus of appearance). + bindings.add(binding) + else: + # The compatible string is associated with a bus of appearance: + # - look for nodes specified by a binding whose compatible string + # matches our search + # - for each node, add its binding to those associated with + # the compatible string candidate + for node in sh.dt.get_compatible_devices(compatible): + if node.binding: + bindings.add(node.binding) + + states.append(RlStateCompatStr(compatible, bindings)) + + return states + + @staticmethod + def complete_dtvendor( + cs_txt: str, sh: DTSh + ) -> List[DTShReadline.CompleterState]: + """Complete vendor prefix. + + Args: + cs_txt: The vendor prefix to complete. + sh: The context shell. + + Returns: + The vendors that are valid completer states. + """ + return sorted( + RlStateDTVendor(vendor.prefix, vendor.name) + for vendor in sh.dt.vendors + if vendor.prefix.startswith(cs_txt) + ) + + @staticmethod + def complete_dtbus( + cs_txt: str, sh: DTSh + ) -> List[DTShReadline.CompleterState]: + """Complete bus protocol. + + Args: + cs_txt: The bus protocol to complete. + sh: The context shell. + + Returns: + The bus protocols that are valid completer states. + """ + return sorted( + RlStateDTBus(proto) + for proto in sh.dt.bus_protocols + if proto.startswith(cs_txt) + ) + + @staticmethod + def complete_dtalias( + cs_txt: str, sh: DTSh + ) -> List[DTShReadline.CompleterState]: + """Complete alias name. + + Args: + cs_txt: The alias name to complete. + sh: The context shell. + + Returns: + The aliased nodes that are valid completer states. + """ + return sorted( + RlStateDTAlias(alias, node) + for alias, node in sh.dt.aliased_nodes.items() + if alias.startswith(cs_txt) + ) + + @classmethod + def complete_dtchosen( + cls, cs_txt: str, sh: DTSh + ) -> List[DTShReadline.CompleterState]: + """Complete chosen parameter name. + + Args: + cs_txt: The chosen parameter name to complete. + sh: The context shell. + + Returns: + The chosen nodes that are valid completer states. + """ + return sorted( + RlStateDTChosen(chosen, node) + for chosen, node in sh.dt.chosen_nodes.items() + if chosen.startswith(cs_txt) + ) + + @classmethod + def complete_dtlabel( + cls, cs_txt: str, sh: DTSh + ) -> List[DTShReadline.CompleterState]: + """Complete devicetree label. + + Args: + cs_txt: The devicetree label to complete, + with or without the leading "&". + If present, it's retained within the completion state. + sh: The context shell. + + Returns: + The labeled nodes that are valid completer states. + """ + # The label pattern is shifted when the completion scope starts with "&". + i_label = 1 if cs_txt.startswith("&") else 0 + + if cs_txt.endswith("/"): + # We assume we're completing a devicetree path like "&label/": + # the path to the node with label "label" is the only match. + label = cs_txt[i_label:-1] + try: + node = sh.dt.labeled_nodes[label] + return [RlStateDTPath(node.path, node)] + except KeyError: + return [] + + # Complete with matching labels. + pattern = cs_txt[i_label:] + states: List[RlStateDTLabel] = [ + # "&" is retained within the completion state + # when it's present in the completion scope. + RlStateDTLabel(f"&{label}" if (i_label > 0) else label, node) + for label, node in sh.dt.labeled_nodes.items() + if label.startswith(pattern) + ] + + if len(states) == 1: + # Exact match: convert completion state to path. + return [RlStateDTPath(states[0].node.path, states[0].node)] + return sorted(states) + + @staticmethod + def complete_fspath(cs_txt: str) -> List[DTShReadline.CompleterState]: + """Complete file-system path. + + Args: + cs_txt: The file-system path to complete. + + Returns: + The file-system paths that are valid completer states. + """ + if cs_txt.startswith("~"): + # We'll always expand "~" in both the auto-completion input + # and the completion states. + cs_txt = cs_txt.replace("~", os.path.expanduser("~"), 1) + + dirname = os.path.dirname(cs_txt) + if dirname: + dirpath = os.path.abspath(dirname) else: - for completion in model: - self._autocomp_state[str(completion)] = completion + dirpath = os.getcwd() + + if not os.path.isdir(dirpath): + return [] + + basename = os.path.basename(cs_txt) + fs_entries = [ + entry + for entry in os.scandir(dirpath) + if entry.name.startswith(basename) + ] + if _dtshconf.pref_fs_hide_dotted: + # Hide commonly hidden files and directories on POSIX-like systems. + fs_entries = [ + entry for entry in fs_entries if not entry.name.startswith(".") + ] + + fs_entries.sort(key=lambda entry: entry.name) + + # Directories 1st. + states: List[DTShReadline.CompleterState] = [ + # Append "/" to completion matches for directories. + RlStateFsEntry( + f"{os.path.join(dirname, entry.name)}{os.path.sep}", entry + ) + for entry in fs_entries + if entry.is_dir() + ] + # Then files. + states.extend( + RlStateFsEntry(os.path.join(dirname, entry.name), entry) + for entry in fs_entries + if entry.is_file() + ) + + return states + + def __init__(self, sh: DTSh) -> None: + """Initialize the auto-completion helper. + + Args: + sh: The context shell. + """ + self._dtsh = sh + + def complete( + self, cs_txt: str, rlbuf: str, cs_begin: int, cs_end: int + ) -> List[DTShReadline.CompleterState]: + """Auto-complete a DTSh command line. + + Derived classes may override this method to post-process + the completions, e.g.: + - append a space to completion matches, exiting the completion process + when there remains only a single match + - pretend there's no completion when there's a single match, + also to exit the completion process + - append "/" to matched directories when completing file system entries + + Implements DTShReadline.CompletionCallback. + + Args: + cs_txt: The input text to complete (aka the RL completion scope + content). + rlbuf: The readline input buffer content (aka the current content + of the command string). + begin: Beginning of completion scope, index of the first character + in the completion scope). + end: End of the completion scope, index of the character immediately + after the completion scope, such that (end - begin) is the + length of the completion scope. This is also the input caret + index wrt the RL buffer. + + Returns: + The list of candidate completions. + """ + if (cs_end < len(rlbuf)) and (rlbuf[cs_end] != " "): + # Some auto-completion providers (e.g. zsh) have an option + # to auto-complete the command line regardless of the caret position. + # This sometimes results in a confusing user experience + # when not completing past the end of a word. + # DTSh will always answer there's no completion on such situations. + return [] + + # Auto-completion is asking for possible command names + # when the command line before the completion scope + # contains only spaces. + ante_cs_txt = rlbuf[:cs_begin].strip() + if not ante_cs_txt: + return DTShAutocomp.complete_dtshcmd(cs_txt, self._dtsh) + + try: + cmd, _, redir2 = self._dtsh.parse_cmdline(rlbuf) + except DTShCommandNotFoundError: + # Won't auto-complete a command line deemed to fail. + return [] + + if redir2: + # Auto-completion is asking for possible redirection file paths + # when we're past the last redirection operator. + if cs_begin > rlbuf.rfind(">"): + return DTShAutocomp.complete_fspath(cs_txt) + + # At this point, auto-completion may be asking for: + # - either a command's option name + # - or a command's argument possible values + # - or a command's parameter possible values + + if cs_txt.startswith("-"): + # We assume possible argument and parameter values + # won't start with "-". + return DTShAutocomp.complete_dtshopt(cs_txt, cmd) + + # Auto-completion is asking for possible argument values + # when the completion scope immediately follows a command's + # argument name. + ante_opts = ante_cs_txt.split() + if ante_opts: + last_opt = cmd.option(ante_opts[-1]) + if isinstance(last_opt, DTShArg): + return last_opt.autocomp(cs_txt, self._dtsh) + + # Eventually, try to auto-complete with parameter values. + if cmd.param: + return cmd.param.autocomp(cs_txt, self._dtsh) + + return [] + + def display( + self, + out: DTShOutput, + states: List[DTShReadline.CompleterState], + ) -> None: + """Default DTSh callback for displaying the completion candidates. + + The completion model is typically produced by DtshAutocomp.complete(). + + Derived classes may override this method + to provide an alternative (aka rich) completion matches display. + + Implements DTShReadline.DisplayCallback. + + Args: + out: Where to display these completions. + completions: The completions to display. + """ + for state in states: + if isinstance(state, RlStateDTShCommand): + cmd = state.cmd + out.write(f"{cmd.name} {cmd.brief}") + + elif isinstance(state, RlStateDTShOption): + opt = state.opt + out.write(f"{opt.usage} {opt.brief}") + + elif isinstance(state, RlStateDTPath): + node = state.node + out.write(node.name) + + elif isinstance(state, RlStateCompatStr): + compat = state.compatstr + out.write(compat) + + elif isinstance(state, RlStateDTVendor): + out.write(f"{state.prefix} {state.vendor}") + + elif isinstance(state, RlStateDTBus): + proto = state.proto + out.write(proto) + + elif isinstance(state, RlStateDTAlias): + out.write(state.alias) + + elif isinstance(state, RlStateDTChosen): + out.write(state.chosen) + + elif isinstance(state, RlStateFsEntry): + dirent = state.dirent + if dirent.is_dir(): + out.write(f"{dirent.name}{os.path.sep}") + else: + out.write(dirent.name) + + elif isinstance(state, RlStateEnum): + out.write(f"{state.value} {state.brief}") + + else: + out.write(state.rlstr) diff --git a/src/dtsh/builtin_alias.py b/src/dtsh/builtin_alias.py deleted file mode 100644 index a9cfa16..0000000 --- a/src/dtsh/builtin_alias.py +++ /dev/null @@ -1,134 +0,0 @@ -# Copyright (c) 2022 Chris Duf -# -# SPDX-License-Identifier: Apache-2.0 - -"""Built-in 'alias' command.""" - - -from typing import Tuple, List - -from rich.table import Table - -from dtsh.dtsh import Dtsh, DtshCommand, DtshAutocomp, DtshVt -from dtsh.dtsh import DtshError, DtshCommandUsageError -from dtsh.dtsh import DtshCommandFlagLongFmt -from dtsh.tui import DtshTui - - -class DtshBuiltinAlias(DtshCommand): - """Print defined aliases. - -DESCRIPTION -The `alias` command prints the aliases defined -in the `/aliases` node of this devicetree. - -EXAMPLES - -``` -/ -❯ alias -led0 → /leds/led_0 -led1 → /leds/led_1 -led2 → /leds/led_2 -led3 → /leds/led_3 -pwm-led0 → /pwmleds/pwm_led_0 -sw0 → /buttons/button_0 -sw1 → /buttons/button_1 -sw2 → /buttons/button_2 -sw3 → /buttons/button_3 -bootloader-led0 → /leds/led_0 -mcuboot-button0 → /buttons/button_0 -mcuboot-led0 → /leds/led_0 -watchdog0 → /soc/watchdog@40010000 -spi-flash0 → /soc/qspi@40029000/mx25r6435f@0 - -/ -❯ alias watchdog0 -l -watchdog0 → /soc/watchdog@40010000 nordic,nrf-wdt -``` -""" - def __init__(self, shell: Dtsh): - super().__init__( - 'alias', - "print defined aliases", - True, - [ - DtshCommandFlagLongFmt(), - ] - ) - self._dtsh = shell - - @property - def usage(self) -> str: - """Overrides DtshCommand.usage(). - """ - return super().usage + ' [ALIAS]' - - def parse_argv(self, argv: List[str]) -> None: - """Overrides DtshCommand.parse_argv(). - """ - super().parse_argv(argv) - if len(self._params) > 1: - raise DtshCommandUsageError(self, 'too many parameters') - - def execute(self, vt: DtshVt) -> None: - """Implements DtshCommand.execute(). - """ - if self._params: - arg_aliases = [self._params[0]] - else: - arg_aliases = list(self._dtsh.dt_aliases.keys()) - - if self.with_pager: - vt.pager_enter() - if self.with_longfmt: - grid = self._mk_grid_aliases_rich(arg_aliases) - else: - grid = self._mk_grid_aliases(arg_aliases) - vt.write(grid) - if self.with_pager: - vt.pager_exit() - - def autocomplete_param(self, prefix: str) -> Tuple[int, List]: - """Overrides DtshCommand.autocomplete_param(). - """ - completions: List[str] = [] - for alias in self._dtsh.dt_aliases: - if alias.startswith(prefix) and (len(alias) > len(prefix)): - completions.append(alias) - return (DtshAutocomp.MODE_ANY, completions) - - def _mk_grid_aliases(self, arg_aliases: List[str]) -> Table: - grid = DtshTui.mk_grid(3) - for alias in arg_aliases: - try: - node = self._dtsh.dt_aliases[alias] - except KeyError: - raise DtshError(f'no such alias: {arg_aliases[0]}') - txt_alias = DtshTui.mk_txt(alias) - txt_arrow = DtshTui.mk_txt(DtshTui.WCHAR_ARROW) - txt_path = DtshTui.mk_txt(node.path) - if node.status != 'okay': - DtshTui.txt_dim(txt_alias) - DtshTui.txt_dim(txt_arrow) - DtshTui.txt_dim(txt_path) - grid.add_row(txt_alias, txt_arrow, txt_path) - return grid - - def _mk_grid_aliases_rich(self, arg_aliases: List[str]) -> Table: - grid = DtshTui.mk_grid(4) - for alias in arg_aliases: - try: - node = self._dtsh.dt_aliases[alias] - except KeyError: - raise DtshError(f'no such alias: {arg_aliases[0]}') - txt_alias = DtshTui.mk_txt(alias, DtshTui.style(DtshTui.STYLE_DT_ALIAS)) - txt_arrow = DtshTui.mk_txt(DtshTui.WCHAR_ARROW) - txt_path = DtshTui.mk_txt_node_path(node.path) - txt_binding = DtshTui.mk_txt_node_binding(node, True, True) - if node.status != 'okay': - DtshTui.txt_dim(txt_alias) - DtshTui.txt_dim(txt_arrow) - DtshTui.txt_dim(txt_path) - grid.add_row(txt_alias, txt_arrow, txt_path, txt_binding) - return grid diff --git a/src/dtsh/builtin_cat.py b/src/dtsh/builtin_cat.py deleted file mode 100644 index a1bb294..0000000 --- a/src/dtsh/builtin_cat.py +++ /dev/null @@ -1,131 +0,0 @@ -# Copyright (c) 2022 Chris Duf -# -# SPDX-License-Identifier: Apache-2.0 - -"""Built-in 'cat' command.""" - - -from typing import Tuple, List - -from dtsh.dtsh import Dtsh, DtshError, DtshVt, DtshCommand, DtshAutocomp -from dtsh.dtsh import DtshCommandUsageError -from dtsh.tui import DtNodeView, DtPropertyView - - -class DtshBuiltinCat(DtshCommand): - """Concatenate and print devicetree content. - -DESCRIPTION -The `cat` command will concatenate and print devicetree content at `PATH`. - -Think Linux `/proc` file systems, e.g. `cat /proc/cpuinfo`. - -`cat` supports the `$` character as a separator between a node's path and -a property name: `PATH := [$]` - -Set the **--pager** option to page the command's output using the system pager. - -EXAMPLES -``` -/ -❯ cat /soc/usbd@40027000 -Node - Path: /soc/usbd@40027000 - Name: usbd - Unit address: 0x40027000 - Compatible: nordic,nrf-usbd - Status: okay - -Description - Nordic nRF52 USB device controller - - -Depends-on - soc - interrupt-controller@e000e100 arm,v7m-nvic - -Required-by - There's no other node that directly depends on this - node. - -[...] - -/ -❯ cat /soc/i2c@40003000$interrupts -Property - Name: interrupts - Type: array - Required: True - Default: - Value: [3, 1] - -Description - interrupts for device -``` -""" - - def __init__(self, shell: Dtsh) -> None: - super().__init__( - 'cat', - 'concatenate and print devicetree content', - True - ) - self._dtsh = shell - - @property - def usage(self) -> str: - """Overrides DtshCommand.usage(). - """ - return super().usage + ' PATH' - - def parse_argv(self, argv: List[str]) -> None: - """Overrides Dtsh.parse_argv(). - """ - super().parse_argv(argv) - if len(self._params) == 0: - raise DtshCommandUsageError(self, 'what do you want to cat?') - if len(self._params) > 1: - raise DtshCommandUsageError(self, 'too many parameters') - - def execute(self, vt: DtshVt) -> None: - """Implements DtshCommand.execute(). - """ - if self._params: - arg_path = self._dtsh.realpath(self._params[0]) - else: - arg_path = self._dtsh.pwd - - i_prop = arg_path.rfind('$') - if i_prop != -1: - node = self._dtsh.path2node(arg_path[:i_prop]) - prop = node.props.get(arg_path[i_prop+1:]) - if prop is None: - raise DtshError(f'no such property: {arg_path[i_prop+1:]}') - view = DtPropertyView(prop) - else: - node = self._dtsh.path2node(arg_path) - view = DtNodeView(node, self._dtsh) - - view.show(vt, self.with_pager) - - def autocomplete_param(self, prefix: str) -> Tuple[int, List]: - """Overrides DtshCommand.autocomplete_param(). - """ - # := //.../ - # := [@] - # may contain alphanum, and: , . _ + - - # - # Property names may additionaly contain ? and #. - # - # AFAICT, the $ does not appear in the DT Specifications, - # we'll is it as separator between a node's path and a property name. - i_prop = prefix.rfind('$') - - if i_prop != -1: - comps = DtshAutocomp.autocomplete_with_properties(prefix[:i_prop], - prefix[i_prop+1:], - self._dtsh) - return (DtshAutocomp.MODE_DT_PROP, comps) - - return (DtshAutocomp.MODE_DT_NODE, - DtshAutocomp.autocomplete_with_nodes(prefix, self._dtsh)) diff --git a/src/dtsh/builtin_cd.py b/src/dtsh/builtin_cd.py deleted file mode 100644 index 8068a5b..0000000 --- a/src/dtsh/builtin_cd.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright (c) 2022 Chris Duf -# -# SPDX-License-Identifier: Apache-2.0 - -"""Built-in 'cd' command.""" - - -from typing import List, Tuple -from dtsh.dtsh import Dtsh, DtshVt, DtshCommand, DtshAutocomp -from dtsh.dtsh import DtshCommandUsageError - - -class DtshBuiltinCd(DtshCommand): - """Change current working node. - -DESCRIPTION - The `cd` command changes the shell current working node to `PATH`. - -If `PATH` is unspecified, `cd` will change the current working node -to the devicetree's root. - -EXAMPLES -``` -/ -❯ cd /soc/flash-controller@4001e000/ - -/soc/flash-controller@4001e000 -❯ cd - -/ -❯ -``` -""" - def __init__(self, shell: Dtsh) -> None: - super().__init__( - 'cd', - 'change current working node' - ) - self._dtsh = shell - - @property - def usage(self) -> str: - """Overrides DtshCommand.usage(). - """ - return super().usage + ' [PATH]' - - def parse_argv(self, argv: List[str]) -> None: - """Overrides Dtsh.parse_argv(). - """ - super().parse_argv(argv) - if len(self._params) > 1: - raise DtshCommandUsageError(self, 'too many parameters') - - def execute(self, vt: DtshVt) -> None: - """Implements DtshCommand.execute(). - """ - if self._params: - arg_path = self._dtsh.realpath(self._params[0]) - else: - arg_path = '/' - - self._dtsh.cd(arg_path) - - def autocomplete_param(self, prefix: str) -> Tuple[int, List]: - """Overrides DtshCommand.autocomplete_param(). - """ - return (DtshAutocomp.MODE_DT_NODE, - DtshAutocomp.autocomplete_with_nodes(prefix, self._dtsh)) diff --git a/src/dtsh/builtin_chosen.py b/src/dtsh/builtin_chosen.py deleted file mode 100644 index a37049e..0000000 --- a/src/dtsh/builtin_chosen.py +++ /dev/null @@ -1,130 +0,0 @@ -# Copyright (c) 2022 Chris Duf -# -# SPDX-License-Identifier: Apache-2.0 - -"""Built-in 'chosen' command.""" - - -from typing import List, Tuple - -from rich.table import Table - -from dtsh.dtsh import Dtsh, DtshCommand, DtshAutocomp, DtshVt -from dtsh.dtsh import DtshError, DtshCommandUsageError -from dtsh.dtsh import DtshCommandFlagLongFmt -from dtsh.tui import DtshTui - - -class DtshBuiltinChosen(DtshCommand): - """Print chosen system configuration. - -DESCRIPTION -The `chosen` command prints the /system configuration choices/ - defined in the `/chosen` node of this devicetree. - -EXAMPLES -``` -/ -❯ chosen -zephyr,entropy → /soc/random@4000d000 -zephyr,flash-controller → /soc/flash-controller@4001e000 -zephyr,console → /soc/uart@40002000 -zephyr,shell-uart → /soc/uart@40002000 -zephyr,uart-mcumgr → /soc/uart@40002000 -zephyr,bt-mon-uart → /soc/uart@40002000 -zephyr,bt-c2h-uart → /soc/uart@40002000 -zephyr,sram → /soc/memory@20000000 -zephyr,flash → /soc/flash-controller@4001e000/flash@0 -zephyr,code-partition → /soc/flash-controller@4001e000/flash@0/partitions/partition@c000 -zephyr,ieee802154 → /soc/radio@40001000/ieee802154 - -/ -❯ chosen zephyr,entropy -l -zephyr,entropy → /soc/random@4000d000 nordic,nrf-rng -``` -""" - def __init__(self, shell: Dtsh): - super().__init__( - 'chosen', - "print chosen configuration", - True, - [ - DtshCommandFlagLongFmt(), - ] - ) - self._dtsh = shell - - @property - def usage(self) -> str: - """Overrides DtshCommand.usage(). - """ - return super().usage + ' [CHOICE]' - - def parse_argv(self, argv: List[str]) -> None: - """Overrides DtshCommand.parse_argv(). - """ - super().parse_argv(argv) - if len(self._params) > 1: - raise DtshCommandUsageError(self, 'too many parameters') - - def execute(self, vt: DtshVt) -> None: - """Implements DtshCommand.execute(). - """ - if self._params: - arg_chosen = [self._params[0]] - else: - arg_chosen = list(self._dtsh.dt_chosen.keys()) - - if self.with_pager: - vt.pager_enter() - if self.with_longfmt: - grid = self._mk_grid_chosen_rich(arg_chosen) - else: - grid = self._mk_grid_chosen(arg_chosen) - vt.write(grid) - if self.with_pager: - vt.pager_exit() - - def autocomplete_param(self, prefix: str) -> Tuple[int, List]: - """Overrides DtshCommand.autocomplete_param(). - """ - completions: List[str] = [] - for choice in self._dtsh.dt_chosen: - if choice.startswith(prefix) and (len(choice) > len(prefix)): - completions.append(choice) - return (DtshAutocomp.MODE_ANY, completions) - - def _mk_grid_chosen(self, arg_chosen: List[str]) -> Table: - grid = DtshTui.mk_grid(3) - for choice in arg_chosen: - try: - node = self._dtsh.dt_chosen[choice] - except KeyError: - raise DtshError(f'no such configuration choice: {arg_chosen[0]}') - txt_choice = DtshTui.mk_txt(choice) - txt_arrow = DtshTui.mk_txt(DtshTui.WCHAR_ARROW) - txt_path = DtshTui.mk_txt(node.path) - if node.status != 'okay': - DtshTui.txt_dim(txt_choice) - DtshTui.txt_dim(txt_arrow) - DtshTui.txt_dim(txt_path) - grid.add_row(txt_choice, txt_arrow, txt_path) - return grid - - def _mk_grid_chosen_rich(self, arg_chosen: List[str]) -> Table: - grid = DtshTui.mk_grid(4) - for choice in arg_chosen: - try: - node = self._dtsh.dt_chosen[choice] - except KeyError: - raise DtshError(f'no such configuration choice: {arg_chosen[0]}') - txt_choice = DtshTui.mk_txt(choice, DtshTui.style(DtshTui.STYLE_DT_ALIAS)) - txt_arrow = DtshTui.mk_txt(DtshTui.WCHAR_ARROW) - txt_path = DtshTui.mk_txt_node_path(node.path) - txt_binding = DtshTui.mk_txt_node_binding(node, True, True) - if node.status != 'okay': - DtshTui.txt_dim(txt_choice) - DtshTui.txt_dim(txt_arrow) - DtshTui.txt_dim(txt_path) - grid.add_row(txt_choice, txt_arrow, txt_path, txt_binding) - return grid diff --git a/src/dtsh/builtin_find.py b/src/dtsh/builtin_find.py deleted file mode 100644 index 822ec26..0000000 --- a/src/dtsh/builtin_find.py +++ /dev/null @@ -1,733 +0,0 @@ -# Copyright (c) 2022 Chris Duf -# -# SPDX-License-Identifier: Apache-2.0 - -"""Built-in 'find' command.""" - - -import re - -from abc import abstractmethod -from typing import Tuple, List, Union - -from devicetree.edtlib import Node - -from dtsh.dtsh import DtshCommand, DtshCommandOption, Dtsh, DtshAutocomp, DtshVt -from dtsh.dtsh import DtshCommandFlagLongFmt, DtshCommandArgLongFmt -from dtsh.dtsh import DtshCommandUsageError -from dtsh.tui import LsNodeTable, DtshTui - - -class FindCriterion(object): - """Interface for search criteria. - """ - - # Plain text pattern (used only for plain text search, w/o wildcard) - _pattern: str - - def __init__(self, pattern: str) -> None: - """Initialize criterion. - - Arguments: - pattern - the search pattern - """ - self._pattern = pattern - - @abstractmethod - def match(self, node: Node) -> bool: - """Returns True if the node does match this criterion. - """ - - -class DtshBuiltinFind(DtshCommand): - """Find devicetree nodes. - -DESCRIPTION -The `find` command will search `PATH` for devicetree nodes, where `PATH` is either: - -- an absolute path to a devicetree node, e.g. `/soc` -- a relative path to a devicetree node, e.g. `soc` - -`PATH` supports simple path substitution: - -- a leading `.` is interpreted as the current working node -- a leading `..` is interpreted as the current working node's parent - -If `PATH` is unspecified, `find` will search the current working node. - -The `find` command always search recursively. - -The search supports multiple criteria: - -- **--name**: match nodes by names -- **--compat**: match nodes by compatible strings -- **--bus**: match nodes by *bus device* -- **--interrupt**: match nodes by interrupts -- **--enabled-only**: search only enabled (aka *okay*) nodes - -Criteria are additive (logical `AND`), such that: - - find --bus * --interrupt * - -will match all bus devices that generate IRQs. - -Without any criterion, the `find` command will behave like its -POSIX homonym: the search will match all nodes at `PATH`. - -Criteria are defined in the next sections. - -By default, `find` will only print the found node paths: use the **-l** option to -enable a more detailed (aka *rich*) output. - -The **-f ** option allows to specify the visible columns with a -format string. - -Valid column specifiers are: - - | Specifier | Format | DTSpec | - |-----------|-------------------------------------------|---------| - | `N` | The node name | 2.2.1 | - | `a` | The unit-address | | - | `n` | The node name with the address striped | | - | `d` | The description from the node binding | | - | `p` | The node path name | 2.2.3 | - | `l` | The node 'label' property | | - | `L` | All known labels for the node | | - | `s` | The node 'status' property | 2.3.4 | - | `c` | The 'compatible' property for the node | 2.3.1 | - | `C` | The node binding (aka matched compatible) | | - | `A` | The node aliases | | - | `b` | The bus device information for the node | | - | `r` | The node 'reg' property | 2.3.6 | - | `i` | The interrupts generated by the node | 2.4.1.1 | - -Use the **-c** options to count the matched nodes. -Add the *quiet* flag (**-q**) to only print the nodes count, -not the nodes themselves. - -Set the **--pager** option to page the command's output using the system pager. - -**Search by name** - -Match nodes by the `node-name` component -([DTSpec 2.2.1](https://devicetree-specification.readthedocs.io/en/stable/devicetree-basics.html#node-names)). - -If the search pattern does not contain any wildcard, -defaults to plain text search. - -If the search pattern contains `*` wildcards, -a regular expression type search is assumed -where the wildcards are replaced by a character class appropriate for node names. - -Then: - -- `*` will match any node-name -- `*` will match a node-name if it starts with `` -- `*` will match a node-name if it ends with `` -- `*` will match a node-name if it starts with `` - and ends with `` - -**Search by compatible** - -Match nodes by `compatible` string -([DTSpec 2.3.1](https://devicetree-specification.readthedocs.io/en/stable/devicetree-basics.html#compatible)). - -If the search pattern does not contain any wildcard, -defaults to plain text search. - -If the search pattern contains `*` wildcards, -a regular expression type search is assumed -where the wildcards are replaced by a character class appropriate for -compatible strings. - -Then: - -- `*` will match any compatible string -- `*` will match a compatible string if it starts with `` -- `*` will match a compatible string if it ends with `` -- `*` will match a compatible string if it starts with - `` and ends with `` - -**Search by bus device** - -Match nodes that provide or appear on a *bus device*, e.g. `i2c` or `spi`. - -If the search pattern does not contain any wildcard, -defaults to plain text search. - -If the search pattern contains `*` wildcards, -a regular expression type search is assumed -where the wildcards are replaced by a character class appropriate for -bus devices. - -Then: - -- `*` will match any bus device -- `*` will match a bus device if it starts with `` -- `*` will match a bus device if it ends with `` -- `*` will match a bus device if it starts with `` - and ends with `` - -**Search by IRQ** - -Match nodes by interrupt numbers or names. - -If the search pattern successfully converts to an integer, -a search by IRQ number is assumed. - -If the conversion fails, a search by interrupt name is assumed. - -If the pattern equals to `*`, the search will match all interrupts: this permits -to *find* all nodes that generate IRQs. - -If the search pattern does not contain any wildcard, -defaults to plain text search. - -If the search pattern contains `*` wildcards, -a regular expression type search is assumed -where the wildcards are replaced by a character class appropriate for IRQ names. - -Then: - -- `*` will match any IRQ name -- `*` will match an IRQ name if it starts with `` -- `*` will match an IRQ name if it ends with `` -- `*` will match an IRQ name if it starts with `` - and ends with `` - -EXAMPLES - -1. All nodes - -Without any criterion, the search will match all devicetree nodes. - -Count the devicetree nodes: - -``` -/ -❯ find -cq - -132 nodes. -``` - -Dump the enabled nodes: - -``` -/ -❯ find -c --enabled-only -/ -/chosen -/aliases -/soc -/soc/interrupt-controller@e000e100 -/soc/ficr@10000000 -/soc/uicr@10001000 -... -/buttons/button_3 -/connector -/analog-connector - -119 nodes. -``` - -2. Find nodes by name - -To find nodes which name contains `gpio` (*plain text* search): - -``` -/ -❯ find --name gpio -l -Path Aliases Labels -───────────────────────────────────── -/soc/gpiote@40006000 gpiote -/soc/gpio@50000000 gpio0 -/soc/gpio@50000300 gpio1 -``` - -To find nodes which name ends with `gpio` (RE search): - -``` -/ -❯ find --name *gpio -l -Path Aliases Labels -───────────────────────────────────── -/soc/gpio@50000000 gpio0 -/soc/gpio@50000300 gpio1 -``` - -3. Find nodes by compatible strings - -To find nodes that may involve a TWI *compatible* driver: - -``` -❯ find --compat twi -l -Path Compatible Description -────────────────────────────────────────────────────────────────────── -/soc/i2c@40003000 nordic,nrf-twi Nordic nRF family TWI (TWI master)… -/soc/i2c@40004000 nordic,nrf-twi Nordic nRF family TWI (TWI master)… -``` - -4. Find nodes by bus devices - -To find all enabled bus devices: - -``` -/ -❯ find --enabled-only --bus * -l -Path Bus -──────────────────────────────────────── -/soc/uart@40002000 uart -/soc/i2c@40003000 i2c -/soc/spi@40004000 spi -/soc/usbd@40027000 usb -/soc/uart@40028000 uart -/soc/qspi@40029000 qspi -/soc/qspi@40029000/mx25r6435f@0 on qspi -/soc/spi@4002f000 spi -``` - -5. Find nodes by interrupts - -To find all nodes that generate IRQs: - -``` -❯ find --interrupt * -l -Path Interrupts -────────────────────────────────────── -/soc/clock@40000000 IRQ_0/1 -/soc/power@40000000 IRQ_0/1 -/soc/radio@40001000 IRQ_1/1 -``` - -To find nodes by interrupt number: - -``` -/ -❯ find --interrupt 28 -l -Path Interrupts -───────────────────────────── -/soc/pwm@4001c000 IRQ_28/1 -``` - -6. Custom search and *reporting* - -To dump all enabled bus devices that generate IRQs, -configuring the table columns with the `-f` option: - -``` -/ -❯ find -c --enabled-only --bus * --interrupt * -f naibcd -Name Address Interrupts Bus Compatible Description -─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── -uart 0x40002000 IRQ_2/1 uart nordic,nrf-uarte Nordic nRF family UARTE (UART with EasyDMA) -i2c 0x40003000 IRQ_3/1 i2c nordic,nrf-twi Nordic nRF family TWI (TWI master)… -spi 0x40004000 IRQ_4/1 spi nordic,nrf-spi Nordic nRF family SPI (SPI master) -usbd 0x40027000 IRQ_39/1 usb nordic,nrf-usbd Nordic nRF52 USB device controller -uart 0x40028000 IRQ_40/1 uart nordic,nrf-uarte Nordic nRF family UARTE (UART with EasyDMA) -qspi 0x40029000 IRQ_41/1 qspi nordic,nrf-qspi Properties defining the interface for the Nordic QSPI peripheral… -spi 0x4002f000 IRQ_47/1 spi nordic,nrf-spim Nordic nRF family SPIM (SPI master with EasyDMA) - -7 nodes. -``` - -To keep this information *at-hand*, -just rely on `dtsh` command output redirection, e.g: - -``` -/ -❯ find -c --enabled-only --bus * --interrupt * -f naibcd > interrupts.txt -``` -""" - # Search criteria. - _criteria: List[FindCriterion] - - # Nodes matched during the last search. - _found: List[Node] - - def __init__(self, shell: Dtsh) -> None: - super().__init__( - 'find', - 'find devicetree nodes', - True, - [ - DtshCommandOption('find by name', None, 'name', 'pattern'), - DtshCommandOption('find by compatible', None, 'compat', 'pattern'), - DtshCommandOption('find by bus device', None, 'bus', 'pattern'), - DtshCommandOption('find by interrupt', None, 'interrupt', 'pattern'), - DtshCommandOption('search only enabled nodes', None, 'enabled-only', None), - DtshCommandOption('print nodes count', 'c', None, None), - DtshCommandOption('quiet, only print nodes count', 'q', None, None), - DtshCommandFlagLongFmt(), - DtshCommandArgLongFmt() - ] - ) - self._dtsh = shell - self._criteria = [] - self._found = [] - - @property - def usage(self) -> str: - """Overrides DtshCommand.usage(). - """ - return super().usage + ' [PATH]' - - @property - def arg_pattern_name(self) -> Union[str, None]: - return self.arg_value('--name') - - @property - def arg_pattern_compat(self) -> Union[str, None]: - return self.arg_value('--compat') - - @property - def arg_pattern_bus(self) -> Union[str, None]: - return self.arg_value('--bus') - - @property - def arg_pattern_irq(self) -> Union[str, None]: - return self.arg_value('--interrupt') - - @property - def with_only_enabled(self) -> bool: - return self.with_flag('--enabled-only') - - @property - def with_node_count(self) -> bool: - return self.with_flag('-c') - - @property - def with_quiet(self) -> bool: - return self.with_flag('-q') - - def reset(self) -> None: - """Overrides DtshCommand.reset(). - """ - super().reset() - self._found.clear() - self._criteria.clear() - - def parse_argv(self, argv: List[str]) -> None: - """Overrides DtshCommand.parse_argv(). - """ - super().parse_argv(argv) - if len(self._params) > 1: - raise DtshCommandUsageError(self, 'too many parameters') - if self.arg_pattern_name: - self._criteria.append(FindByNameCriterion(self.arg_pattern_name)) - if self.arg_pattern_compat: - self._criteria.append(FindByCompatCriterion(self.arg_pattern_compat)) - if self.arg_pattern_bus: - self._criteria.append(FindByBusCriterion(self.arg_pattern_bus)) - if self.arg_pattern_irq: - self._criteria.append(FindByIrqCriterion(self.arg_pattern_irq)) - - def execute(self, vt: DtshVt) -> None: - """Implements DtshCommand.execute(). - """ - if self._params: - arg_path = self._dtsh.realpath(self._params[0]) - else: - arg_path = self._dtsh.pwd - - self._find_nodes(self._dtsh.path2node(arg_path)) - # Like the POSIX find command, does not touch stdout if no match. - if len(self._found) > 0: - - if self.with_pager: - vt.pager_enter() - - if not self.with_quiet: - longfmt = self.arg_longfmt - if self.with_longfmt and not longfmt: - longfmt = self._mk_default_longmt() - - if longfmt: - self._write_found_long(vt, longfmt) - else: - self._write_found_default(vt) - - if self.with_node_count: - vt.write() - vt.write(f"{len(self._found)} nodes.") - - if self.with_pager: - vt.pager_exit() - - def autocomplete_param(self, prefix: str) -> Tuple[int, List]: - """Overrides DtshCommand.autocomplete_param(). - """ - return (DtshAutocomp.MODE_DT_NODE, - DtshAutocomp.autocomplete_with_nodes(prefix, self._dtsh)) - - def _find_nodes(self, root: Node) -> None: - if self.with_only_enabled and not Dtsh.is_node_enabled(root): - return - if self._match_criteria(root): - self._found.append(root) - for node in root.children.values(): - self._find_nodes(node) - - def _match_criteria(self, node: Node) -> bool: - for criterion in self._criteria: - if not criterion.match(node): - return False - return True - - def _write_found_default(self, vt: DtshVt) -> None: - for node in self._found: - vt.write(node.path) - - def _write_found_long(self, vt: DtshVt, longfmt: str) -> None: - ls_table = LsNodeTable(self._dtsh, longfmt) - for node in self._found: - ls_table.add_node_row(node) - vt.write(ls_table.as_view()) - - def _mk_default_longmt(self) -> str: - longfmt = 'p' - if self.arg_pattern_name: - longfmt += 'A' - longfmt += 'L' - if self.arg_pattern_irq: - longfmt += 'i' - if self.arg_pattern_bus: - longfmt += 'b' - if self.arg_pattern_compat: - longfmt += 'c' - longfmt += 'd' - return longfmt - -class FindByNameCriterion(FindCriterion): - """Find nodes by names. - - Match nodes by the node-name component (DTSpec 2.2.1). - - If the search pattern does not contain any wildcard, - defaults to plain text search. - - If the search pattern contains '*' wildcards, - a regular expression type search is assumed - where '*' are replaced by the character class for node names - (see CLASS_NODE_NAME). - - Then: - - * will match any node-name - - * will match a node-name if it starts with - - * will match a node-name if it ends with - - * will match a node-name if it starts with - and ends with - """ - - # Character class for node names (DTSpec 2.2.1). - CLASS_NODE_NAME = r'[\w,.+\-]*' - - # Pattern for regular expression type search, None for plain text search. - _re: Union[re.Pattern, None] - - def __init__(self, pattern: str) -> None: - """Initialize criterion. - - Arguments: - pattern - the search pattern - """ - super().__init__(pattern) - self._re = None - if pattern.find('*') != -1: - pattern = pattern.replace('*', FindByNameCriterion.CLASS_NODE_NAME) - re_expr = f'^{pattern}$' - self._re = re.compile(re_expr) - - def match(self, node: Node) -> bool: - name = DtshTui.get_node_nick(node) - if self._re: - # RE type search. - return self._re.match(name) is not None - # Plain text type search. - return name.find(self._pattern) != -1 - - -class FindByCompatCriterion(FindCriterion): - """Find nodes by compatible string. - - Match nodes by the 'compatible' property value (DTSpec 2.3.1). - - If the search pattern does not contain any wildcard, - defaults to plain text search. - - If the search pattern contains '*' wildcards, - a regular expression type search is assumed - where '*' are replaced by the character class for 'compatible' values - (see CLASS_COMPATIBLE). - - Then: - - * will match any compatible string - - * will match a compatible if it starts with - - * will match a compatible if it ends with - - * will match a compatible if it starts with - and ends with - """ - - # Character class for 'compatible' property (DTSpec 2.3.1). - CLASS_COMPATIBLE = r'[a-z0-9\-,]*' - - # Pattern for regular expression type search, None for plain text search. - _re: Union[re.Pattern, None] - - def __init__(self, pattern: str) -> None: - """Initialize criterion. - - Arguments: - pattern - the search pattern - """ - super().__init__(pattern) - self._re = None - if pattern.find('*') != -1: - pattern = pattern.replace('*', FindByCompatCriterion.CLASS_COMPATIBLE) - re_expr = f'^{pattern}$' - self._re = re.compile(re_expr) - - def match(self, node: Node) -> bool: - for compat in node.compats: - if self._re: - # RE type search. - if self._re.match(compat) is not None: - return True - else: - # Plain text type search. - if compat.find(self._pattern) != -1: - return True - return False - - -class FindByBusCriterion(FindCriterion): - """Find nodes by bus device. - - If the search pattern does not contain any wildcard, - defaults to plain text search. - - If the search pattern contains '*' wildcards, - a regular expression type search is assumed - where '*' are replaced by the character class for bus devices - (see CLASS_BUS). - - Then: - - * will match any bus device - - * will match a bus device if it starts with - - * will match a bus device if it ends with - - * will match a bus device if it starts with - and ends with - """ - - # Character class for bus name (alphanumeric ? lowercase ?). - CLASS_BUS = r'[\w]*' - - # Pattern for regular expression type search, None for plain text search. - _re: Union[re.Pattern, None] - - def __init__(self, pattern: str) -> None: - """Initialize criterion. - - Arguments: - pattern - the search pattern - """ - super().__init__(pattern) - self._re = None - if pattern.find('*') != -1: - pattern = pattern.replace('*', FindByBusCriterion.CLASS_BUS) - re_expr = f'^{pattern}$' - self._re = re.compile(re_expr) - - def match(self, node: Node) -> bool: - for bus in node.buses: - if self._re: - if self._re.match(bus) is not None: - return True - else: - if bus.find(self._pattern) != -1: - return True - for bus in node.on_buses: - if self._re: - if self._re.match(bus) is not None: - return True - else: - if bus.find(self._pattern) != -1: - return True - return False - - -class FindByIrqCriterion(FindCriterion): - """Find nodes by interrupts. - - If the search pattern successfully converts to an integer, - a search by IRQ number is assumed. - - If the conversion fails, a search by interrupt name is assumed. - - If the search pattern does not contain any wildcard, - defaults to plain text search. - - If the search pattern contains '*' wildcards, - a regular expression type search is assumed - where '*' are replaced by the character class for IRQ names - (see CLASS_IRQ_NAME). - - Then: - - * will match any IRQ name - - * will match an IRQ name if it starts with - - * will match an IRQ name if it ends with - - * will match an IRQ name if it starts with - and ends with - """ - - # Character class for IRQ names (empiric ?). - CLASS_IRQ_NAME = r'[\w\-]*' - - # Search by IRQ number, None for search by IRQ name. - _irq_num: Union[int, None] - - # Search by IRQ name: pattern for regular expression type search, - # None for plain text search. - _re: Union[re.Pattern, None] - - def __init__(self, pattern: str) -> None: - """Initialize criterion. - - Arguments: - pattern - the search pattern - """ - super().__init__(pattern) - self._irq_num = None - self._re = None - - # We do NOT actually configure the criterion filters when - # the pattern equals to '*': this allows to use find to list all - # interrupts, including interrupts without name. - if pattern != '*': - try: - self._irq_num = int(pattern) - except ValueError: - if pattern.find('*') != -1: - pattern = pattern.replace('*', FindByIrqCriterion.CLASS_IRQ_NAME) - self._re = re.compile(f'^{pattern}$') - - def match(self, node: Node) -> bool: - for irq in node.interrupts: - if self._pattern == '*': - # Allows find to list all interrupts with '--interrupt *'. - return True - if self._irq_num is not None: - # Search by IRQ number - if irq.data.get('irq') == self._irq_num: - return True - else: - # Search by IRQ name - if self._re is not None: - # RE type search - if irq.name and (self._re.match(irq.name) is not None): - return True - else: - # Plain text search - if irq.name: - if irq.name.find(self._pattern) != -1: - return True - return False diff --git a/src/dtsh/builtin_ls.py b/src/dtsh/builtin_ls.py deleted file mode 100644 index 41598d4..0000000 --- a/src/dtsh/builtin_ls.py +++ /dev/null @@ -1,276 +0,0 @@ -# Copyright (c) 2022 Chris Duf -# -# SPDX-License-Identifier: Apache-2.0 - -"""Built-in 'ls' command.""" - - -from typing import Tuple, Dict, List -from devicetree.edtlib import Node - -from dtsh.dtsh import DtshCommand, DtshCommandOption, Dtsh, DtshAutocomp, DtshVt -from dtsh.dtsh import DtshCommandFlagLongFmt, DtshCommandArgLongFmt -from dtsh.dtsh import DtshCommandUsageError -from dtsh.tui import DtNodeListView - - -class DtshBuiltinLs(DtshCommand): - """List devicetree nodes. - -DESCRIPTION -The `ls` command will list devicetree nodes at `PATH`, which is either: - -- an absolute path to a devicetree node, e.g. `/soc` -- a relative path to a devicetree node, e.g. `soc` -- a glob pattern filtering devicetree nodes, e.g. `soc/uart*` - -`PATH` supports simple path substitution: - -- a leading `.` is interpreted as the current working node -- a leading `..` is interpreted as the current working node's parent - -If `PATH` is unspecified, `ls` will list the current working node. - -By default, `ls` will enumerate the immediate children of the devicetree node(s) -at `PATH`: use the **-R** option to enumerate the children recursively, -the **-d** option to list the node itself without its children. - -By default, `ls` will only print the nodes path: use the **-l** option to -enable a more detailed (aka *rich*) output. - -The **-f ** option allows to specify the visible columns with a -format string. - -Valid column specifiers are: - - | Specifier | Format | DTSpec | - |-----------|-------------------------------------------|---------| - | `N` | The node name | 2.2.1 | - | `a` | The unit-address | | - | `n` | The node name with the address striped | | - | `d` | The description from the node binding | | - | `p` | The node path name | 2.2.3 | - | `l` | The node 'label' property | | - | `L` | All known labels for the node | | - | `s` | The node 'status' property | 2.3.4 | - | `c` | The 'compatible' property for the node | 2.3.1 | - | `C` | The node binding (aka matched compatible) | | - | `A` | The node aliases | | - | `b` | The bus device information for the node | | - | `r` | The node 'reg' property | 2.3.6 | - | `i` | The interrupts generated by the node | 2.4.1.1 | - -By default, nodes should be sorted by ascending unit address: use the **-r** -option to reverse the sort order. - -Set the **--pager** option to page the command's output using the system pager. - -EXAMPLES -Assuming the current working node is the devicetree's root: - -1. default to `ls /`: - -``` -/ -❯ ls -/chosen -/aliases -/soc -/pin-controller -/entropy_bt_hci -/cpus -/sw-pwm -/leds -/pwmleds -/buttons -/connector -/analog-connector -``` - -2. same with rich output: - -``` -❯ ls -l -/: -Name Addr Labels Alias Compatible Description -──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── -chosen -aliases -soc nordic,nRF52840-QIAA nordic,nRF52840 nordic,nRF52 simple-bus -pin-controller pinctrl nordic,nrf-pinctrl The nRF pin controller is a singleton node responsible for controlling… -entropy_bt_hci rng_hci zephyr,bt-hci-entropy Bluetooth module that uses Zephyr's Bluetooth Host Controller Interface as… -cpus -sw-pwm sw_pwm nordic,nrf-sw-pwm nRFx S/W PWM -leds gpio-leds This allows you to define a group of LEDs. Each LED in the group is… -pwmleds pwm-leds PWM LEDs parent node -buttons gpio-keys GPIO KEYS parent node -connector arduino_header arduino-header-r3 GPIO pins exposed on Arduino Uno (R3) headers… -analog-connector arduino_adc arduino,uno-adc ADC channels exposed on Arduino Uno (R3) headers… -``` - -Globing: - -1. *for all* wild-card: - -``` -/ -❯ ls * -/chosen: - -/aliases: - -/soc: -/soc/interrupt-controller@e000e100 -/soc/timer@e000e010 -/soc/ficr@10000000 -/soc/uicr@10001000 -/soc/memory@20000000 -/soc/clock@40000000 -/soc/power@40000000 -/soc/radio@40001000 -/soc/uart@40002000 -/soc/i2c@40003000 -/soc/spi@40003000 - -[...] - -/buttons: -/buttons/button_0 -/buttons/button_1 -/buttons/button_2 -/buttons/button_3 - -/connector: - -/analog-connector: -``` - -2. filter wild-card: - -``` -/ -❯ ls /soc/gpio* -ld -Name Address Labels Aliases Compatible Description -──────────────────────────────────────────────────────────────────────── -gpiote 0x40006000 gpiote nordic,nrf-gpiote NRF5 GPIOTE node -gpio 0x50000000 gpio0 nordic,nrf-gpio NRF5 GPIO node -gpio 0x50000300 gpio1 nordic,nrf-gpio NRF5 GPIO node -```` - -Set a format string to specify the visible columns: - -```` -/ -❯ ls -l --format pbi /soc/ -/soc: -Path Bus Interrupts -──────────────────────────────────────────────────── -/soc/interrupt-controller@e000e100 -/soc/timer@e000e010 -/soc/ficr@10000000 -/soc/uicr@10001000 -/soc/memory@20000000 -/soc/clock@40000000 IRQ_0/1 -/soc/power@40000000 IRQ_0/1 -/soc/radio@40001000 IRQ_1/1 -/soc/uart@40002000 uart IRQ_2/1 -/soc/i2c@40003000 i2c IRQ_3/1 -/soc/spi@40003000 spi IRQ_3/1 -/soc/i2c@40004000 i2c IRQ_4/1 -/soc/spi@40004000 spi IRQ_4/1 -/soc/nfct@40005 -``` -""" - def __init__(self, shell: Dtsh) -> None: - super().__init__( - 'ls', - 'list devicetree nodes', - True, - [ - DtshCommandOption('list node itself, not its content', 'd', None, None), - DtshCommandOption('reverse order while sorting', 'r', None, None), - DtshCommandOption('list node contents recursively', 'R', None, None), - DtshCommandFlagLongFmt(), - DtshCommandArgLongFmt() - ] - ) - self._dtsh = shell - - @property - def usage(self) -> str: - """Overrides DtshCommand.usage(). - """ - return super().usage + ' [PATH]' - - @property - def with_no_content(self) -> bool: - return self.with_flag('-d') - - @property - def with_recursive(self) -> bool: - return self.with_flag('-R') - - @property - def with_reverse(self) -> bool: - return self.with_flag('-r') - - def parse_argv(self, argv: List[str]) -> None: - """Overrides DtshCommand.parse_argv(). - """ - super().parse_argv(argv) - if len(self._params) > 1: - raise DtshCommandUsageError(self, 'too many parameters') - - def execute(self, vt: DtshVt) -> None: - """Implements DtshCommand.execute(). - """ - if self._params: - arg_path = self._dtsh.realpath(self._params[0]) - else: - arg_path = self._dtsh.pwd - - if arg_path.endswith('*'): - # Globing. - roots = self._dtsh.ls(arg_path) - else: - roots = [ - self._dtsh.path2node(arg_path) - ] - - if self.with_reverse: - roots.reverse() - - node_map: Dict[str, List[Node]] = {} - for root in roots: - if self.with_no_content: - node_map[root.path] = [] - else: - if self.with_recursive: - self._follow_node_content(root, node_map) - else: - node_map[root.path] = self._dtsh.ls(root.path) - - if self.with_reverse: - for _, contents in node_map.items(): - contents.reverse() - - view = DtNodeListView(node_map, - self._dtsh, - self.with_no_content, - self.with_longfmt, - self.arg_longfmt) - view.show(vt, self.with_pager) - - def autocomplete_param(self, prefix: str) -> Tuple[int, List]: - """Overrides DtshCommand.autocomplete_param(). - """ - return (DtshAutocomp.MODE_DT_NODE, - DtshAutocomp.autocomplete_with_nodes(prefix, self._dtsh)) - - def _follow_node_content(self, - parent: Node, - node_map: Dict[str, List[Node]]) -> None: - node_map[parent.path] = [] - for _, node in parent.children.items(): - node_map[parent.path].append(node) - self._follow_node_content(node, node_map) diff --git a/src/dtsh/builtin_man.py b/src/dtsh/builtin_man.py deleted file mode 100644 index bab37a7..0000000 --- a/src/dtsh/builtin_man.py +++ /dev/null @@ -1,149 +0,0 @@ -# Copyright (c) 2022 Chris Duf -# -# SPDX-License-Identifier: Apache-2.0 - -"""Built-in 'man' command.""" - -from typing import Tuple, List - -from devicetree.edtlib import Binding - -from dtsh.rl import readline -from dtsh.dtsh import Dtsh, DtshCommand, DtshCommandOption, DtshAutocomp, DtshVt -from dtsh.dtsh import DtshError, DtshCommandUsageError, DtshCommandFailedError -from dtsh.man import DtshManPageBinding, DtshManPageBuiltin, DtshManPageDtsh - - -class DtshBuiltinMan(DtshCommand): - """Print current working node's path. - -DESCRIPTION -The `man` command opens the *reference* manual page `PAGE`, -where `PAGES`: - -- is either a devicetree shell built-in (e.g. `tree`) -- or a [*compatible*](https://devicetree-specification.readthedocs.io/en/latest/chapter2-devicetree-basics.html#compatible) - specification if the **--compat** option is set - -By default, `man` will page its output: use the **--no-pager** option to -disable the pager. - -EXAMPLES -To open the `ls` shell built-in's manual page: - -``` -/ -❯ man ls - -``` - -To open a the manual page for a DT compatible (ARMv7-M NVIC): - -``` -/ -❯ man --compat arm,v7m-nvic - -``` -""" - def __init__(self, shell: Dtsh): - super().__init__( - 'man', - "open a manual page", - # Won't support the --pager option, since enabled by default for - # man pages (see --no-pager). - False, - [ - DtshCommandOption("page for a DT compatible", None, 'compat', None), - DtshCommandOption('no pager', None, 'no-pager', None), - ] - ) - self._dtsh = shell - - @property - def usage(self) -> str: - """Overrides DtshCommand.usage(). - """ - return super().usage + ' [PAGE]' - - @property - def with_compat(self) -> bool: - return self.with_flag('--compat') - - @property - def with_no_pager(self) -> bool: - return self.with_flag('--no-pager') - - def parse_argv(self, argv: List[str]) -> None: - """Overrides DtshCommand.parse_argv(). - """ - super().parse_argv(argv) - if len(self._params) == 0: - raise DtshCommandUsageError(self, 'what manual page do you want?') - if len(self._params) > 1: - raise DtshCommandUsageError(self, 'too many parameters') - - def execute(self, vt: DtshVt) -> None: - """Implements DtshCommand.execute(). - """ - arg_page = self._params[0] - man_page = None - - if self.with_compat: - binding = self._dtsh.dt_bindings.get(arg_page) - if binding: - man_page = DtshManPageBinding(binding) - else: - builtin = self._dtsh.builtin(arg_page) - if builtin: - man_page = DtshManPageBuiltin(builtin) - - if (not man_page) and (arg_page == 'dtsh'): - man_page = DtshManPageDtsh() - - if man_page is not None: - man_page.show(vt, self.with_no_pager) - else: - raise DtshCommandFailedError(self, f'page not found: {arg_page}') - - def autocomplete_param(self, prefix: str) -> Tuple[int, List]: - """Overrides DtshCommand.autocomplete_param(). - """ - # 1st, complete according to flags. - cmdline = readline.get_line_buffer() - cmdline_vstr = cmdline.split() - if len(cmdline_vstr) > 1: - argv = cmdline_vstr[1:] - try: - self.parse_argv(argv) - except DtshError: - # Dry parsing of incomplete command line. - pass - if self.with_compat: - completions = self._autocomplete_dt_binding(prefix) - if completions: - return (DtshAutocomp.MODE_DT_BINDING, completions) - - # Then, try command name (default). - completions = self._autocomplete_dtsh_cmd(prefix) - if completions: - return (DtshAutocomp.MODE_DTSH_CMD, completions) - - return (DtshAutocomp.MODE_ANY, []) - - def _autocomplete_dtsh_cmd(self, prefix: str) -> List[DtshCommand]: - completions: List[DtshCommand] = [] - if prefix.find('/') == -1: - for cmd in self._dtsh.builtins: - if (not prefix) or (cmd.name.startswith(prefix) and (len(cmd.name) > len(prefix))): - completions.append(cmd) - return completions - - def _autocomplete_dt_binding(self, prefix: str) -> List[Binding]: - completions: List[Binding] = [] - for compat, binding in self._dtsh.dt_bindings.items(): - if prefix: - if compat.startswith(prefix) and (len(compat) > len(prefix)): - completions.append(binding) - else: - completions.append(binding) - return completions diff --git a/src/dtsh/builtin_pwd.py b/src/dtsh/builtin_pwd.py deleted file mode 100644 index bcbcbda..0000000 --- a/src/dtsh/builtin_pwd.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright (c) 2022 Chris Duf -# -# SPDX-License-Identifier: Apache-2.0 - -"""Built-in 'pwd' command.""" - - -from typing import List - -from dtsh.dtsh import Dtsh, DtshCommand, DtshVt -from dtsh.dtsh import DtshCommandUsageError - - -class DtshBuiltinPwd(DtshCommand): - """Print current working node's path. - -DESCRIPTION -The `pwd` command prints the current working node's path. - -The current working node's path is also part of the shell multi-line prompt. - -EXAMPLES - -``` -/ -❯ pwd -/ - -/ -❯ cd soc - -/soc -❯ pwd -/soc - -/soc -❯ -``` -""" - def __init__(self, shell: Dtsh): - super().__init__( - 'pwd', - "print current working node's path" - ) - self._dtsh = shell - - def parse_argv(self, argv: List[str]) -> None: - """Overrides DtshCommand.parse_argv(). - """ - super().parse_argv(argv) - if len(self._params) > 0: - raise DtshCommandUsageError(self, 'too many parameters') - - def execute(self, stdout: DtshVt) -> None: - """Implements DtshCommand.execute(). - """ - stdout.write(self._dtsh.pwd) diff --git a/src/dtsh/builtin_tree.py b/src/dtsh/builtin_tree.py deleted file mode 100644 index 363681e..0000000 --- a/src/dtsh/builtin_tree.py +++ /dev/null @@ -1,179 +0,0 @@ -# Copyright (c) 2022 Chris Duf -# -# SPDX-License-Identifier: Apache-2.0 - -"""Built-in 'tree' command.""" - - -from typing import Tuple, List, Union - -from dtsh.dtsh import Dtsh, DtshVt, DtshCommand, DtshCommandOption, DtshAutocomp -from dtsh.dtsh import DtshCommandFlagLongFmt -from dtsh.dtsh import DtshCommandUsageError - -from dtsh.tui import DtNodeTreeView - - -class DtshBuiltinTree(DtshCommand): - """List devicetree nodes in tree-like format. - -DESCRIPTION -The `tree` command list devicetree nodes at `PATH`, which is either: - -- an absolute path to a devicetree node, e.g. `/soc` -- a relative path to a devicetree node, e.g. `soc` - -`PATH` supports simple path substitution: - -- a leading `.` is interpreted as the current working node -- a leading `..` is interpreted as the current working node's parent - -If `PATH` is unspecified, `tree` will list the current working node. - -The `tree` command list nodes hierarchically as trees. - -By default, `tree` will recursively walk through all not `disabled` branches: use -the **-L** option to set a maximum tree depth. - -By default, `tree` will only print the nodes path: use the **-l** option to -enable a more detailed (aka *rich*) output. - -Set the **--pager** option to page the command's output using the system pager. - -EXAMPLES -Assuming the current working node is the devicetree's root: - -1. default to `tree /`, unlimited depth: - -``` -/ -❯ tree -/ -├── chosen -├── aliases -├── soc -│ ├── interrupt-controller@e000e100 -│ ├── timer@e000e010 -│ ├── ficr@10000000 -│ ├── uicr@10001000 -│ ├── memory@20000000 -│ ├── clock@40000000 -│ ├── power@40000000 - -[...] - -│ ├── acl@4001e000 -│ ├── flash-controller@4001e000 -│ │ └── flash@0 -│ │ └── partitions -│ │ ├── partition@0 -│ │ ├── partition@c000 -│ │ ├── partition@73000 -│ │ ├── partition@da000 -│ │ └── partition@f8000 - -[...] - -├── connector -└── analog-connector -``` - -2. Example of rich output with a tree depth of 2: - -``` -/ -❯ tree -L 2 -l -/ -├── chosen -├── aliases -├── soc -│ ├── 0xe000e100 interrupt-controller ARMv7-M NVIC (Nested Vectored Interrupt Controller) -│ ├── 0xe000e010 timer -│ ├── 0x10000000 ficr Nordic FICR (Factory Information Configuration Registers) -│ ├── 0x10001000 uicr Nordic UICR (User Information Configuration Registers) -│ ├── 0x20000000 memory Generic on-chip SRAM description -│ ├── 0x40000000 clock Nordic nRF clock control node -│ ├── 0x40000000 power Nordic nRF power control node -│ ├── 0x40001000 radio Nordic nRF family RADIO peripheral… -│ ├── 0x40002000 uart Nordic nRF family UARTE (UART with EasyDMA) -│ ├── 0x40003000 i2c Nordic nRF family TWI (TWI master)… -│ ├── 0x40003000 spi Nordic nRF family SPI (SPI master) -│ ├── 0x40004000 i2c Nordic nRF family TWI (TWI master)… - -[...] - -├── connector GPIO pins exposed on Arduino Uno (R3) headers… -└── analog-connector ADC channels exposed on Arduino Uno (R3) headers… -``` -""" - - # Maximum display depth, 0 to follow all non disabled nodes. - _level: int - - def __init__(self, shell: Dtsh): - super().__init__( - 'tree', - 'list devicetree nodes in tree-like format', - True, - [ - DtshCommandOption('max display depth of the tree', 'L', 'depth', 'level'), - DtshCommandFlagLongFmt(), - ] - ) - self._dtsh = shell - self._level = 0 - - @property - def usage(self) -> str: - """Overrides DtshCommand.usage(). - """ - return super().usage + ' [PATH]' - - @property - def arg_level(self) -> Union[str, None]: - """Maximum display depth, 0 to follow all non disabled nodes. - """ - return self.arg_value('-L') - - def reset(self) -> None: - """Overrides DtshCommand.reset(). - """ - super().reset() - self._level = 0 - - def parse_argv(self, argv: List[str]) -> None: - """Overrides DtshCommand.parse_argv(). - """ - super().parse_argv(argv) - if len(self._params) > 1: - raise DtshCommandUsageError(self, 'too many parameters') - if self.arg_level is not None: - try: - self._level = int(self.arg_level) - except ValueError: - raise DtshCommandUsageError( - self, - f"'{self.arg_level}' is not a valid level" - ) - - def execute(self, vt: DtshVt) -> None: - """Implements DtshCommand.execute(). - """ - if self._params: - arg_path = self._dtsh.realpath(self._params[0]) - else: - arg_path = self._dtsh.pwd - - root = self._dtsh.path2node(arg_path) - - view = DtNodeTreeView(root, - self._dtsh, - self._level, - self.with_longfmt) - view.show(vt, self.with_pager) - - def autocomplete_param(self, prefix: str) -> Tuple[int, List]: - """Overrides DtshCommand.autocomplete_param(). - """ - return (DtshAutocomp.MODE_DT_NODE, - DtshAutocomp.autocomplete_with_nodes(prefix, self._dtsh)) diff --git a/src/dtsh/builtin_uname.py b/src/dtsh/builtin_uname.py deleted file mode 100644 index a8395fa..0000000 --- a/src/dtsh/builtin_uname.py +++ /dev/null @@ -1,436 +0,0 @@ -# Copyright (c) 2022 Chris Duf -# -# SPDX-License-Identifier: Apache-2.0 - -"""Built-in 'uname' command.""" - - -from typing import List, Union - -import os - -from rich.table import Table -from rich.text import Text - -from dtsh.dtsh import Dtsh, DtshCommand, DtshCommandOption, DtshUname, DtshVt -from dtsh.dtsh import DtshCommandFlagLongFmt -from dtsh.dtsh import DtshCommandUsageError -from dtsh.systools import GitHub, YamlFile -from dtsh.tui import DtshTui, DtshTuiBulletList, DtshTuiForm, DtshTuiMemo, DtshTuiYaml - - -class DtshBuiltinUname(DtshCommand): - """Print current working node's path. - -DESCRIPTION -The `uname` command will print *system* information, including: - -- `kernel-version` (option **-v**): the Zephyr kernel version, - e.g. `zephyr-3.1.0`; clicking the version string should open - the corresponding release notes in the system default browser -- `machine` (option **-m**): based on the content of the board binding file, - e.g. `nrf52840dk_nrf52840.yaml`; links to the board's DTS file and - its Zephyr documentation - (if [supported](https://docs.zephyrproject.org/latest/boards/index.html)) - should also show up -- `toolchain` (non *standard* option **-t**): build toolchain variant and - version, based on the currently configured Zephyr command line developement - environment (e.g. after sourcing `$ZEPHYR_BASE/zephyr-env.sh`); - clicking the toolchain name or version should open related information - in the system default browser - -Retrieving this information may involve environment variables (e.g. `ZEPHYR_BASE` -or `ZEPHYR_TOOLCHAIN_VARIANT`), CMake cached variables, invoking `git` or GCC. - - | | Environment variables | CMake cache | git | GCC | - |---------------+--------------------------+--------------+-----+-----| - | Zephyr kernel | ZEPHYR_BASE | | x | | - | Toolchain | ZEPHYR_TOOLCHAIN_VARIANT | | | | - | | ZEPHYR_SDK_INSTALL_DIR | | x | | - | | GNUARMEMB_TOOLCHAIN_PATH | | | x | - | Board | BOARD | BOARD_DIR | | | - | | | CACHED_BOARD | | | - -By default, `uname` will print brief system information: `kernel-version - machine`. - -The **-l** option will enable a more detailed (aka *rich*) output. - -To filter the printed information, explicitly set the **-v**, **-m** and -**-t** options. - -Use the **-a** option to request all information. - -Set the **--pager** option to page the command's output using the system pager -(only with **-l**). - -EXAMPLES -Default brief system information: - -``` -/ -❯ uname -Zephyr v3.1.0 - nrf52840dk_nrf52840 -``` - -Filter detailed board (`machine`) information: - - / - ❯ uname -tl - BOARD - Board directory: $ZEPHYR_BASE/boards/arm/nrf52840dk_nrf52840 - Name: nRF52840-DK-NRF52840 (Supported Boards) - Board: nrf52840dk_nrf52840 (DTS) - - nrf52840dk_nrf52840.yaml - - identifier: nrf52840dk_nrf52840 - name: nRF52840-DK-NRF52840 - type: mcu - arch: arm - ram: 256 - flash: 1024 - toolchain: - - zephyr - - gnuarmemb - - xtools - supported: - - adc - - arduino_gpio - - arduino_i2c - - arduino_spi - - ble - - counter - - gpio - - i2c - - i2s - - ieee802154 - - pwm - - spi - - usb_cdc - - usb_device - - watchdog - - netif:openthread - -Filter detailed toolchain information: - - / - ❯ uname -tl - TOOLCHAIN - Path: /mnt/platform/zephyr-rtos/SDKs/zephyr-sdk-0.15.1 - Variant: Zephyr SDK - Version: v0.15.1 -""" - def __init__(self, shell: Dtsh): - super().__init__( - 'uname', - "print system information", - True, - [ - DtshCommandOption('print Zephyr kernel version', - 'v', - 'kernel-version', - None), - DtshCommandOption('print board', - 'm', - 'machine', - None), - DtshCommandOption('print toolchain', - 't', - 'toolchain', - None), - DtshCommandOption("print all information", - 'a', - 'all', - None), - DtshCommandFlagLongFmt(), - ] - ) - self._dtsh = shell - - @property - def with_kernel_version(self) -> bool: - return self.with_flag('-v') - - @property - def with_machine(self) -> bool: - return self.with_flag('-m') - - @property - def with_toolchain(self) -> bool: - return self.with_flag('-t') - - @property - def with_all(self) -> bool: - return self.with_flag('-a') - - def parse_argv(self, argv: List[str]) -> None: - """Overrides DtshCommand.parse_argv(). - """ - super().parse_argv(argv) - if len(self._params) > 0: - raise DtshCommandUsageError(self, 'too many parameters') - - def execute(self, vt: DtshVt) -> None: - """Implements DtshCommand.execute(). - """ - if self.with_longfmt: - self._uname_long(vt) - else: - self._uname_brief(vt) - - def _uname_brief(self, vt: DtshVt) -> None: - msg = "" - no_with = not ( - self.with_flag('-v') - or self.with_flag('-m') - or self.with_flag('-t') - ) - if self.with_all or no_with or self.with_kernel_version: - if self._dtsh.uname.zephyr_kernel_tags: - msg += f"Zephyr {self._dtsh.uname.zephyr_kernel_tags[0]}" - elif self._dtsh.uname.zephyr_kernel_rev: - msg += f"Zephyr {self._dtsh.uname.zephyr_kernel_rev}" - else: - msg += "Unknown" - if self.with_all or no_with or self.with_machine: - if msg: - msg += " - " - if self._dtsh.uname.board: - msg += f"{self._dtsh.uname.board}" - else: - msg += "Unknown" - if self.with_all or self.with_toolchain: - if msg: - if self._dtsh.uname.zephyr_toolchain: - msg += f" ({self._dtsh.uname.zephyr_toolchain})" - else: - msg += " (Unknown)" - else: - if self._dtsh.uname.zephyr_toolchain: - msg += f"{self._dtsh.uname.zephyr_toolchain}" - else: - msg += "Unknown" - vt.write(msg) - - def _uname_long(self, vt: DtshVt) -> None: - view = DtshTuiMemo() - # When no explicit choice, and long format, - # we'll show all available info. - def_all = not (self.with_flag('-v') - or self.with_flag('-m') - or self.with_flag('-t')) - - if self.with_all or def_all or self.with_kernel_version: - view.add_entry("zephyr kernel", self._mk_layout_zephyr_kernel()) - - if self.with_all or def_all or self.with_toolchain: - if self._dtsh.uname.zephyr_toolchain: - content = ZephyrToolchainForm(self._dtsh.uname).as_renderable() - else: - content = None - view.add_entry("toolchain", content) - - if self.with_all or def_all or self.with_machine: - view.add_entry("board", self._mk_layout_board()) - - view.show(vt, self.with_pager) - - def _mk_layout_zephyr_kernel(self) -> Union[Table, None]: - if self._dtsh.uname.zephyr_base: - layout = DtshTui.mk_grid(1) - layout.add_row(ZephyrKernelForm(self._dtsh.uname).as_renderable()) - layout.add_row() - if self._dtsh.uname.dt_binding_dirs: - r_list = DtshTuiBulletList("Bindings search path:") - for path in self._dtsh.uname.dt_binding_dirs: - if path.startswith(self._dtsh.uname.zephyr_base): - path = path.replace(self._dtsh.uname.zephyr_base, "$ZEPHYR_BASE") - r_list.add_item(path) - layout.add_row(r_list.as_renderable()) - else: - r_warn = DtshTui.mk_txt_warn("Empty bindings search path !") - layout.add_row(r_warn) - return layout - return None - - def _mk_layout_board(self) -> Union[Table, None]: - if self._dtsh.uname.board: - layout = DtshTui.mk_grid(1) - layout.add_row(ZephyrBoardForm(self._dtsh.uname).as_renderable()) - if self._dtsh.uname.board_binding_file: - if os.path.isfile(self._dtsh.uname.board_binding_file): - w_yaml = DtshTuiYaml(self._dtsh.uname.board_binding_file, - with_title=True) - layout.add_row() - layout.add_row(w_yaml.as_renderable()) - return layout - return None - - -class ZephyrKernelForm(DtshTuiForm): - """Simple form for Zephyr's path, revision and tags. - """ - - def __init__(self, uname: DtshUname) -> None: - """Initialize the form. - - Arguments: - uname -- dtsh system-like information - """ - super().__init__() - if not uname.zephyr_base: - # We won't get far without ZEPHYR_BASE. - return - gh = GitHub() - self.add_field('Path', uname.zephyr_base) - if uname.zephyr_kernel_tags: - # Tags or version. - version = uname.zephyr_kernel_version - if version: - r_version = DtshTui.mk_txt_link( - version, - gh.get_tag(version), - style='dtsh.zephyr' - ) - tags = uname.zephyr_kernel_tags.copy() - tags.remove(version) - if tags: - r_tags = DtshTui.mk_txt(f" ({', '.join(tags)})") - r_version.append_text(r_tags) - self.add_field_rich("Version", r_version) - else: - self.add_field("Tags", ', '.join(uname.zephyr_kernel_tags)) - if uname.zephyr_kernel_rev: - r_revision = DtshTui.mk_txt_link( - uname.zephyr_kernel_rev, - gh.get_commit(uname.zephyr_kernel_rev), - style='default' if uname.zephyr_kernel_version else 'dtsh.commit' - ) - else: - # Show revision field even when information is unavailable. - r_revision = DtshTui.mk_txt_dim("Unknown") - self.add_field_rich("Revision", r_revision) - - -class ZephyrToolchainForm(DtshTuiForm): - """Simple form for Zephyr's path, revision and tags. - """ - - def __init__(self, uname: DtshUname) -> None: - """Initialize the form. - - Requires: zephyr_toolchain - Optional: - - zephyr_sdk_version, zephyr_sdk_dir - or - - gnuarm_version, gnuarm_dir - - Arguments: - uname -- dtsh system-like information - """ - super().__init__() - if not uname.zephyr_toolchain: - # We won't get far if we don't know the toolchain variant. - return - - r_version = None - if uname.zephyr_toolchain == 'zephyr': - # Zephyr SDK toolchain. - self.add_field('Path', uname.zephyr_sdk_dir) - r_variant = DtshTui.mk_txt_link( - "Zephyr SDK", - "https://docs.zephyrproject.org/latest/develop/toolchains/zephyr_sdk.html", - style='dtsh.zephyr' - ) - if uname.zephyr_sdk_version: - sdk_version = f"v{uname.zephyr_sdk_version}" - r_version = DtshTui.mk_txt_link( - sdk_version, - f"https://github.com/zephyrproject-rtos/sdk-ng/releases/tag/{sdk_version}", - style='dtsh.zephyr' - ) - else: - r_version = DtshTui.mk_txt_dim("Unknown") - - elif uname.zephyr_toolchain == 'gnuarmemb': - # GNU Arm Embedded toolchain. - self.add_field('Path', uname.gnuarm_dir) - r_variant = DtshTui.mk_txt_link( - "GNU Arm Embedded", - "https://developer.arm.com/Tools%20and%20Software/GNU%20Toolchain", - style='dtsh.gnuarmemb' - ) - if uname.gnuarm_version: - r_version = DtshTui.mk_txt(uname.gnuarm_version) - else: - r_version = DtshTui.mk_txt_dim("Unknown") - else: - r_variant = DtshTui.mk_txt_dim("Unknown") - - self.add_field_rich("Variant", r_variant) - if r_version: - self.add_field_rich("Version", r_version) - - -class ZephyrBoardForm(DtshTuiForm): - """Simple form for Zephyr's path, revision and tags. - """ - - def __init__(self, uname: DtshUname) -> None: - """Initialize the form. - - Requires: uname.board - Optional: uname.bord_dir - - Arguments: - uname -- dtsh system-like information - """ - super().__init__() - if not uname.board_dir: - # We won't get far if without at least the directory. - return - - board_dir = uname.board_dir - if uname.zephyr_base and board_dir.startswith(uname.zephyr_base): - board_dir = board_dir.replace(uname.zephyr_base, "$ZEPHYR_BASE") - self.add_field("Board directory", board_dir) - - # Remaining fields depend on the YAML file content. - if not uname.board_binding_file: - return - # Remaining fields depend on BOARD (e.g. nrf52840dk_nrf52840). - if not uname.board: - return - - yaml_file = YamlFile(uname.board_binding_file) - - r_name = None - board_name = yaml_file.get('name') - if board_name: - r_name = DtshTui.mk_txt_bold(board_name) - if uname.zephyr_base and uname.board_dir.startswith(uname.zephyr_base): - # We then assume it's a Zephyr board with online doc. - arch = yaml_file.get('arch') - if arch: - url = f'https://docs.zephyrproject.org/latest/boards/{arch}/{uname.board}/doc/index.html' - r_www = DtshTui.mk_txt_link( - "Supported Boards", - url, - style='dtsh.zephyr' - ) - r_name = Text().append_text(r_name) - r_name.append_text(DtshTui.mk_txt(' (')) - r_name.append_text(r_www) - r_name.append_text(DtshTui.mk_txt(')')) - else: - r_name = DtshTui.mk_txt_dim("Unknown") - self.add_field_rich("Name", r_name) - - r_board = DtshTui.mk_txt(uname.board, style='dtsh.board') - if uname.board_dts_file: - r_dts = DtshTui.mk_txt("DTS") - DtshTui.txt_update_link_file(r_dts, uname.board_dts_file) - r_board.append_text(DtshTui.mk_txt(' (')) - r_board.append_text(r_dts) - r_board.append_text(DtshTui.mk_txt(')')) - self.add_field_rich("Board", r_board) diff --git a/src/dtsh/builtins/__init__.py b/src/dtsh/builtins/__init__.py new file mode 100644 index 0000000..7799b57 --- /dev/null +++ b/src/dtsh/builtins/__init__.py @@ -0,0 +1,5 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Built-in devicetree shell commands.""" diff --git a/src/dtsh/builtins/alias.py b/src/dtsh/builtins/alias.py new file mode 100644 index 0000000..e27eae0 --- /dev/null +++ b/src/dtsh/builtins/alias.py @@ -0,0 +1,73 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Devicetree shell built-in "aliases". + +List aliased nodes. + +Unit tests and examples: tests/test_dtsh_builtin_alias.py +""" + + +from typing import Sequence, Mapping + +from dtsh.model import DTNode +from dtsh.io import DTShOutput +from dtsh.shell import DTSh +from dtsh.shellutils import DTShFlagEnabledOnly, DTShParamAlias + +from dtsh.rich.shellutils import DTShCommandLongFmt +from dtsh.rich.modelview import ViewNodeAkaList + + +class DTShBuiltinAlias(DTShCommandLongFmt): + """Devicetree shell built-in 'alias'.""" + + def __init__(self) -> None: + super().__init__( + "alias", + "list aliased nodes", + [ + DTShFlagEnabledOnly(), + ], + DTShParamAlias(), + ) + + def execute(self, argv: Sequence[str], sh: DTSh, out: DTShOutput) -> None: + """Overrides DTShCommand.execute().""" + super().execute(argv, sh, out) + + param_alias = self.with_param(DTShParamAlias).alias + + alias2node: Mapping[str, DTNode] + if param_alias: + # Aliased nodes that match the alias parameter. + alias2node = { + alias: node + for alias, node in sh.dt.aliased_nodes.items() + if alias.find(param_alias) != -1 + } + else: + # All aliased nodes. + alias2node = sh.dt.aliased_nodes + + if self.with_flag(DTShFlagEnabledOnly): + # Filter out aliased nodes which are disabled. + alias2node = { + alias: node + for alias, node in alias2node.items() + if node.enabled + } + + # Silently output nothing if no matched aliased nodes. + if alias2node: + if self.has_longfmt: + # Format output (unordered list view). + # Default format string: "Path", "Binding". + view = ViewNodeAkaList(alias2node, self.get_longfmt("pC")) + out.write(view) + else: + # POSIX-like symlinks (link -> file). + for alias, node in alias2node.items(): + out.write(f"{alias} -> {node.path}") diff --git a/src/dtsh/builtins/cd.py b/src/dtsh/builtins/cd.py new file mode 100644 index 0000000..09fece9 --- /dev/null +++ b/src/dtsh/builtins/cd.py @@ -0,0 +1,39 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Devicetree shell built-in "cd". + +Change the current working branch. + +Unit tests and examples: tests/test_dtsh_builtin_cd.py +""" + + +from typing import Sequence + +from dtsh.io import DTShOutput +from dtsh.shell import DTSh, DTShCommand, DTPathNotFoundError, DTShCommandError +from dtsh.shellutils import DTShParamDTPath + + +class DTShBuiltinCd(DTShCommand): + """Devicetree shell built-in "cd".""" + + def __init__(self) -> None: + super().__init__( + "cd", + "change the current working branch", + [], + DTShParamDTPath(), + ) + + def execute(self, argv: Sequence[str], sh: DTSh, out: DTShOutput) -> None: + """Overrides DTShCommand.execute().""" + super().execute(argv, sh, out) + + param_path = self.with_param(DTShParamDTPath).path + try: + sh.cd(param_path) + except DTPathNotFoundError as e: + raise DTShCommandError(self, e.msg) from e diff --git a/src/dtsh/builtins/chosen.py b/src/dtsh/builtins/chosen.py new file mode 100644 index 0000000..43d54ff --- /dev/null +++ b/src/dtsh/builtins/chosen.py @@ -0,0 +1,73 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Devicetree shell built-in "chosen". + +List chosen nodes. + +Unit tests and examples: tests/test_dtsh_builtin_chosen.py +""" + + +from typing import Sequence, Mapping + +from dtsh.model import DTNode +from dtsh.io import DTShOutput +from dtsh.shell import DTSh +from dtsh.shellutils import DTShFlagEnabledOnly, DTShParamChosen + +from dtsh.rich.shellutils import DTShCommandLongFmt +from dtsh.rich.modelview import ViewNodeAkaList + + +class DTShBuiltinChosen(DTShCommandLongFmt): + """Devicetree shell built-in "chosen".""" + + def __init__(self) -> None: + super().__init__( + "chosen", + "list chosen nodes", + [ + DTShFlagEnabledOnly(), + ], + DTShParamChosen(), + ) + + def execute(self, argv: Sequence[str], sh: DTSh, out: DTShOutput) -> None: + """Overrides DTShCommand.execute().""" + super().execute(argv, sh, out) + + param_chosen = self.with_param(DTShParamChosen).chosen + + chosen2node: Mapping[str, DTNode] + if param_chosen: + # Chosen nodes that match the chosen parameter. + chosen2node = { + chosen: node + for chosen, node in sh.dt.chosen_nodes.items() + if chosen.find(param_chosen) != -1 + } + else: + # All chosen nodes. + chosen2node = sh.dt.chosen_nodes + + if self.with_flag(DTShFlagEnabledOnly): + # Filter out chosen nodes which are disabled. + chosen2node = { + chosen: node + for chosen, node in chosen2node.items() + if node.enabled + } + + # Silently output nothing if no matched chosen nodes. + if chosen2node: + if self.has_longfmt: + # Format output (unordered list view). + # Default format string: "Path", "Binding". + view = ViewNodeAkaList(chosen2node, self.get_longfmt("NC")) + out.write(view) + else: + # POSIX-like symlinks (link -> file). + for choice, node in chosen2node.items(): + out.write(f"{choice} -> {node.path}") diff --git a/src/dtsh/builtins/find.py b/src/dtsh/builtins/find.py new file mode 100644 index 0000000..b7b68e4 --- /dev/null +++ b/src/dtsh/builtins/find.py @@ -0,0 +1,297 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Devicetree shell built-in "find". + +Search for nodes with multiple criteria. + +Unit tests and examples: tests/test_dtsh_builtin_find.py +""" + + +from typing import List, Sequence, Dict, Mapping, Tuple + +from dtsh.model import DTWalkable, DTNode, DTNodeCriterion, DTNodeCriteria +from dtsh.modelutils import DTWalkableComb +from dtsh.io import DTShOutput +from dtsh.shell import DTSh, DTShError, DTShCommandError +from dtsh.shellutils import ( + DTShFlagReverse, + DTShFlagEnabledOnly, + DTShFlagPager, + DTShFlagRegex, + DTShFlagIgnoreCase, + DTShFlagCount, + DTShFlagTreeLike, + DTShFlagLogicalOr, + DTShFlagLogicalNot, + DTShArgOrderBy, + DTShArgCriterion, + DTSH_ARG_NODE_CRITERIA, + DTShParamDTPaths, +) + +from dtsh.rich.shellutils import DTShCommandLongFmt +from dtsh.rich.modelview import ( + SketchMV, + ViewNodeList, + ViewNodeTreePOSIX, + ViewNodeTwoSided, +) +from dtsh.rich.text import TextUtil + + +class DTShBuiltinFind(DTShCommandLongFmt): + """Devicetree shell built-in "find".""" + + def __init__(self) -> None: + super().__init__( + "find", + "search branches for nodes", + [ + DTShFlagLogicalOr(), + DTShFlagLogicalNot(), + DTShFlagRegex(), + DTShFlagIgnoreCase(), + DTShFlagEnabledOnly(), + DTShFlagCount(), + DTShFlagReverse(), + DTShFlagTreeLike(), + DTShFlagPager(), + DTShArgOrderBy(), + *DTSH_ARG_NODE_CRITERIA, + ], + DTShParamDTPaths(), + ) + + def execute(self, argv: Sequence[str], sh: DTSh, out: DTShOutput) -> None: + """Overrides DTShCommand.execute().""" + super().execute(argv, sh, out) + + # Expand path parameter. + path_expansions: Sequence[DTSh.PathExpansion] = self.with_param( + DTShParamDTPaths + ).expand(self, sh) + + if self.with_flag(DTShFlagPager): + out.pager_enter() + + # What to do here depends on the appropriate model kind: + # - either a simple list of found nodes + # - or a virtual subtree defined with the found nodes as its leaves + if self.with_flag(DTShFlagTreeLike): + self._find_tree(path_expansions, sh, out) + else: + self._find_nodes(path_expansions, sh, out) + + if self.with_flag(DTShFlagPager): + out.pager_exit() + + def _find_nodes( + self, + path_expansions: Sequence[DTSh.PathExpansion], + sh: DTSh, + out: DTShOutput, + ) -> None: + # Get model, mapping pathways to the nodes found there. + path2node: Mapping[str, DTNode] = self._get_path2node( + path_expansions, sh + ) + if not path2node: + return + count = len(path2node) + + if self.has_longfmt: + # Formatted output (list view). + self._output_nodes_longfmt(path2node, count, out) + else: + # POSIX-like. + self._output_nodes_raw(path2node, count, out) + + def _get_path2node( + self, + path_expansions: Sequence[DTSh.PathExpansion], + sh: DTSh, + ) -> Mapping[str, DTNode]: + # Collect criterion chain. + criteria = self._get_criteria() + + path2node: Dict[str, DTNode] = {} + for expansion in path_expansions: + for branch in self.sort(expansion.nodes): + for node in branch.find( + criteria, + order_by=self.arg_sorter, + reverse=self.flag_reverse, + enabled_only=self.flag_enabled_only, + ): + path = sh.pathway(node, expansion.prefix) + path2node[path] = node + + return path2node + + def _output_nodes_raw( + self, path2node: Mapping[str, DTNode], count: int, out: DTShOutput + ) -> None: + # Output paths of found nodes. + for path in path2node: + out.write(path) + if self.with_flag(DTShFlagCount): + out.write() + self._output_count_raw(count, out) + + def _output_nodes_longfmt( + self, path2node: Mapping[str, DTNode], count: int, out: DTShOutput + ) -> None: + # Output found nodes as formatted list. + sketch = self.get_sketch(SketchMV.Layout.LIST_VIEW) + cols = self.get_longfmt(sketch.default_fmt) + view = ViewNodeList(cols, sketch) + view.extend(path2node.values()) + out.write(view) + if self.with_flag(DTShFlagCount): + out.write() + self._output_count_longftm(count, out) + + def _find_tree( + self, + path_expansions: Sequence[DTSh.PathExpansion], + sh: DTSh, + out: DTShOutput, + ) -> None: + # Get model, mapping pathways to a subtree containing + # the nodes found there as its leaves. + count: int + path2walkable: Mapping[str, DTWalkable] + count, path2walkable = self._get_path2walkable(path_expansions, sh) + if not path2walkable: + return + + if self.has_longfmt: + # Formatted output. + self._output_treelike_longfmt(path2walkable, count, out) + else: + # POSIX-like. + self._output_treelike_raw(path2walkable, count, out) + + def _output_treelike_raw( + self, + path2walkable: Mapping[str, DTWalkable], + count: int, + out: DTShOutput, + ) -> None: + # Tree-like views (POSIX output). + N = len(path2walkable) + for i, (path, comb) in enumerate(path2walkable.items()): + view = ViewNodeTreePOSIX(path, comb) + view.do_layout( + self.arg_sorter, + self.flag_reverse, + self.flag_enabled_only, + ) + out.write(view) + + if i != N - 1: + out.write() + + if self.with_flag(DTShFlagCount): + out.write() + self._output_count_raw(count, out) + + def _output_treelike_longfmt( + self, + path2walkable: Mapping[str, DTWalkable], + count: int, + out: DTShOutput, + ) -> None: + # Tree-like views (2-sided). + sketch = self.get_sketch(SketchMV.Layout.TWO_SIDED) + cols = self.get_longfmt(sketch.default_fmt) + + N = len(path2walkable) + for i, (_, comb) in enumerate(path2walkable.items()): + view = ViewNodeTwoSided(comb, cols) + view.do_layout( + self.arg_sorter, + self.flag_reverse, + self.flag_enabled_only, + ) + out.write(view) + + if i != N - 1: + out.write() + + if self.with_flag(DTShFlagCount): + out.write() + self._output_count_longftm(count, out) + + def _get_path2walkable( + self, + path_expansions: Sequence[DTSh.PathExpansion], + sh: DTSh, + ) -> Tuple[int, Mapping[str, DTWalkable]]: + # Collect criterion chain. + criteria = self._get_criteria() + + # One tree per root: for each expanded path, map its pathway + # to a subtree containing the nodes found there. + path2walkable: Dict[str, DTWalkable] = {} + count: int = 0 + for expansion in path_expansions: + for branch in self.sort(expansion.nodes): + path = sh.pathway(branch, expansion.prefix) + nodes = list( + branch.find( + criteria, + order_by=self.arg_sorter, + reverse=self.flag_reverse, + enabled_only=self.flag_enabled_only, + ) + ) + if nodes: + count += len(nodes) + comb = DTWalkableComb(branch, nodes) + path2walkable[path] = comb + + return (count, path2walkable) + + def _output_count_longftm(self, count: int, out: DTShOutput) -> None: + out.write( + TextUtil.assemble("Found ", TextUtil.bold(str(count))), + "nodes.", + ) + + def _output_count_raw(self, count: int, out: DTShOutput) -> None: + out.write(f"Found: {count}") + + def _get_criteria(self) -> DTNodeCriteria: + # All defined command arguments that may participate in the search. + args_criterion: List[DTShArgCriterion] = [ + self.with_arg(type(option)) + for option in self.options + if isinstance(option, DTShArgCriterion) + ] + + try: + arg_criteria: List[DTNodeCriterion] = [ + criterion + for criterion in ( + arg_criterion.get_criterion( + re_strict=self.with_flag(DTShFlagRegex), + ignore_case=self.with_flag(DTShFlagIgnoreCase), + ) + for arg_criterion in args_criterion + ) + if criterion + ] + + return DTNodeCriteria( + arg_criteria, + ored_chain=self.with_flag(DTShFlagLogicalOr), + negative_chain=self.with_flag(DTShFlagLogicalNot), + ) + + except DTShError as e: + # Invalid criterion pattern or expression. + raise DTShCommandError(self, e.msg) from e diff --git a/src/dtsh/builtins/ls.py b/src/dtsh/builtins/ls.py new file mode 100644 index 0000000..a4a0e54 --- /dev/null +++ b/src/dtsh/builtins/ls.py @@ -0,0 +1,217 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Devicetree shell built-in "ls". + +List branch contents. + +Unit tests and examples: tests/test_dtsh_builtin_ls.py +""" + + +from typing import Sequence, Dict, Mapping + +from dtsh.model import DTNode +from dtsh.io import DTShOutput +from dtsh.shell import DTSh +from dtsh.shellutils import ( + DTShFlagReverse, + DTShFlagEnabledOnly, + DTShFlagPager, + DTShFlagRecursive, + DTShFlagNoChildren, + DTShArgOrderBy, + DTShArgFixedDepth, + DTShParamDTPaths, +) + +from dtsh.rich.shellutils import DTShCommandLongFmt +from dtsh.rich.text import TextUtil +from dtsh.rich.modelview import DTModelView, SketchMV, ViewNodeList + + +class DTShBuiltinLs(DTShCommandLongFmt): + """Devicetree shell built-in "ls".""" + + def __init__(self) -> None: + super().__init__( + "ls", + "list branch contents", + [ + DTShFlagNoChildren(), + DTShFlagReverse(), + DTShFlagRecursive(), + DTShFlagEnabledOnly(), + DTShFlagPager(), + DTShArgOrderBy(), + DTShArgFixedDepth(), + ], + DTShParamDTPaths(), + ) + + def execute(self, argv: Sequence[str], sh: DTSh, out: DTShOutput) -> None: + """Overrides DTShCommand.execute().""" + super().execute(argv, sh, out) + + # Expand path parameter. + path_expansions: Sequence[DTSh.PathExpansion] = self.with_param( + DTShParamDTPaths + ).expand(self, sh) + + if self.with_flag(DTShFlagPager): + out.pager_enter() + + # What to do here depends on the appropriate model kind, + # "files" (nodes) or "directories" (branches). + if self.with_flag(DTShFlagNoChildren): + # List nodes, not branche(s) contents ("-d"). + self._ls_nodes(path_expansions, sh, out) + else: + # List branches as directories. + self._ls_contents(path_expansions, sh, out) + + if self.with_flag(DTShFlagPager): + out.pager_exit() + + def _ls_nodes( + self, + path_expansions: Sequence[DTSh.PathExpansion], + sh: DTSh, + out: DTShOutput, + ) -> None: + # Get the nodes to list as "files". + path2node: Mapping[str, DTNode] = self._get_path2node( + path_expansions, sh + ) + if not path2node: + return + + if self.has_longfmt: + # Formatted output. + self._output_nodes_longfmt(path2node, out) + else: + # POSIX-like. + self._output_nodes_raw(path2node, out) + + def _get_path2node( + self, + path_expansions: Sequence[DTSh.PathExpansion], + sh: DTSh, + ) -> Mapping[str, DTNode]: + path2node: Dict[str, DTNode] = {} + for expansion in path_expansions: + for node in self.sort(expansion.nodes): + path = sh.pathway(node, expansion.prefix) + path2node[path] = node + return path2node + + def _output_nodes_raw( + self, path2node: Mapping[str, DTNode], out: DTShOutput + ) -> None: + for path in path2node: + out.write(path) + + def _output_nodes_longfmt( + self, path2node: Mapping[str, DTNode], out: DTShOutput + ) -> None: + sketch = self.get_sketch(SketchMV.Layout.LIST_VIEW) + cols = self.get_longfmt(sketch.default_fmt) + + lv_nodes = ViewNodeList(cols, sketch) + lv_nodes.extend(path2node.values()) + out.write(lv_nodes) + + def _ls_contents( + self, + path_expansions: Sequence[DTSh.PathExpansion], + sh: DTSh, + out: DTShOutput, + ) -> None: + # Get the branches to list the contents of as "directories". + path2contents: Mapping[str, Sequence[DTNode]] = self._get_path2contents( + path_expansions, sh + ) + if not path2contents: + return + + if self.has_longfmt: + # Formatted output. + self._output_contents_longfmt(path2contents, sh, out) + else: + # POSIX-like. + self._output_contents_raw(path2contents, out) + + def _get_path2contents( + self, + path_expansions: Sequence[DTSh.PathExpansion], + sh: DTSh, + ) -> Mapping[str, Sequence[DTNode]]: + mode_recursive = ( + self.with_flag(DTShFlagRecursive) + or self.with_arg(DTShArgFixedDepth).isset + ) + + path2contents: Dict[str, Sequence[DTNode]] = {} + for expansion in path_expansions: + for node in self.sort(expansion.nodes): + if mode_recursive: + for branch in node.walk( + order_by=self.arg_sorter, + reverse=self.flag_reverse, + enabled_only=self.flag_enabled_only, + fixed_depth=self.with_arg(DTShArgFixedDepth).depth, + ): + path = sh.pathway(branch, expansion.prefix) + path2contents[path] = self.sort(branch.children) + else: + path = sh.pathway(node, expansion.prefix) + # Filter out disabled nodes if asked to. + contents = self.prune(node.children) + # Sort contents if asked to. + path2contents[path] = self.sort(contents) + + return path2contents + + def _output_contents_raw( + self, path2contents: Mapping[str, Sequence[DTNode]], out: DTShOutput + ) -> None: + N = len(path2contents) + for i, (dirpath, contents) in enumerate(path2contents.items()): + if N > 1: + out.write(f"{dirpath}:") + + for node in contents: + out.write(node.name) + + if i != N - 1: + # Insert empty line between "directories". + out.write() + + def _output_contents_longfmt( + self, + path2contents: Mapping[str, Sequence[DTNode]], + sh: DTSh, + out: DTShOutput, + ) -> None: + N = len(path2contents) + sketch = self.get_sketch(SketchMV.Layout.LIST_VIEW) + cols = self.get_longfmt(sketch.default_fmt) + + for i, (dirpath, contents) in enumerate(path2contents.items()): + if N > 1: + tv_dirpath = DTModelView.mk_path(dirpath) + if not sh.node_at(dirpath).enabled: + TextUtil.disabled(tv_dirpath) + out.write(tv_dirpath, ":", sep="") + + if contents: + # Output branch contents as list view. + lv_nodes = ViewNodeList(cols, sketch) + lv_nodes.left_indent(1) + lv_nodes.extend(contents) + out.write(lv_nodes) + + if i != N - 1: + # Insert empty line between "directories". + out.write() diff --git a/src/dtsh/builtins/pwd.py b/src/dtsh/builtins/pwd.py new file mode 100644 index 0000000..2f7ca72 --- /dev/null +++ b/src/dtsh/builtins/pwd.py @@ -0,0 +1,31 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Devicetree shell built-in "pwd". + +Print current working branch. + +Unit tests and examples: tests/test_dtsh_builtin_pwd.py +""" + + +from typing import Sequence + +from dtsh.io import DTShOutput +from dtsh.shell import DTSh, DTShCommand + + +class DTShBuiltinPwd(DTShCommand): + """Devicetree shell built-in "pwd".""" + + def __init__(self) -> None: + """Command definition.""" + super().__init__( + "pwd", "print path of current working branch", [], None + ) + + def execute(self, argv: Sequence[str], sh: DTSh, out: DTShOutput) -> None: + """Overrides DTShCommand.execute().""" + super().execute(argv, sh, out) + out.write(sh.pwd) diff --git a/src/dtsh/builtins/tree.py b/src/dtsh/builtins/tree.py new file mode 100644 index 0000000..56b311f --- /dev/null +++ b/src/dtsh/builtins/tree.py @@ -0,0 +1,120 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Devicetree shell built-in "tree". + +List nodes in tree-like format. + +Unit tests and examples: tests/test_dtsh_builtin_tree.py +""" + + +from typing import Sequence, Mapping, Dict + +from dtsh.model import DTNode +from dtsh.io import DTShOutput +from dtsh.shell import DTSh +from dtsh.shellutils import ( + DTShFlagReverse, + DTShFlagEnabledOnly, + DTShFlagPager, + DTShArgOrderBy, + DTShArgFixedDepth, + DTShParamDTPaths, +) + +from dtsh.rich.modelview import ( + SketchMV, + ViewNodeTreePOSIX, + ViewNodeTwoSided, +) +from dtsh.rich.shellutils import DTShCommandLongFmt + + +class DTShBuiltinTree(DTShCommandLongFmt): + """Devicetree shell built-in "tree".""" + + def __init__(self) -> None: + super().__init__( + "tree", + "list branch contents in tree-like format", + [ + DTShFlagReverse(), + DTShFlagEnabledOnly(), + DTShFlagPager(), + DTShArgOrderBy(), + DTShArgFixedDepth(), + ], + DTShParamDTPaths(), + ) + + def execute(self, argv: Sequence[str], sh: DTSh, out: DTShOutput) -> None: + """Overrides DTShCommand.execute().""" + super().execute(argv, sh, out) + + # Expand path parameter: can't be empty. + path_expansions: Sequence[DTSh.PathExpansion] = self.with_param( + DTShParamDTPaths + ).expand(self, sh) + # Get the model, mapping the branches to list to their expected pathways. + # pathway -> node. + path2branch: Mapping[str, DTNode] = self._get_path2branch( + path_expansions, sh + ) + + if self.with_flag(DTShFlagPager): + out.pager_enter() + + N = len(path2branch) + for i, (path, branch) in enumerate(path2branch.items()): + if self.has_longfmt: + # Formatted output (2sided view). + self._output_longfmt(branch, out) + else: + # POSIX-like simple tree. + self._output_raw(path, branch, out) + + if i != N - 1: + # Insert empty line between trees. + out.write() + + if self.with_flag(DTShFlagPager): + out.pager_exit() + + def _get_path2branch( + self, + path_expansions: Sequence[DTSh.PathExpansion], + sh: DTSh, + ) -> Mapping[str, DTNode]: + path2branch: Dict[str, DTNode] = {} + for expansion in path_expansions: + for branch in self.sort(expansion.nodes): + path = sh.pathway(branch, expansion.prefix) + path2branch[path] = branch + + return path2branch + + def _output_longfmt(self, branch: DTNode, out: DTShOutput) -> None: + # Formatted branch output (2-sided view). + sketch = self.get_sketch(SketchMV.Layout.TWO_SIDED) + cells = self.get_longfmt(sketch.default_fmt) + view_2sided = ViewNodeTwoSided(branch, cells) + view_2sided.do_layout( + self.arg_sorter, + self.flag_reverse, + self.flag_enabled_only, + self.with_arg(DTShArgFixedDepth).depth, + ) + out.write(view_2sided) + + def _output_raw(self, path: str, branch: DTNode, out: DTShOutput) -> None: + # Branch as tree (POSIX output). + tree = ViewNodeTreePOSIX(path, branch) + tree.do_layout( + self.arg_sorter, + self.flag_reverse, + self.flag_enabled_only, + self.with_arg(DTShArgFixedDepth).depth, + ) + out.write(tree) diff --git a/src/dtsh/cli.py b/src/dtsh/cli.py index 73a816f..40e5c42 100644 --- a/src/dtsh/cli.py +++ b/src/dtsh/cli.py @@ -1,29 +1,150 @@ -# Copyright (c) 2022 Chris Duf +# Copyright (c) 2023 Christophe Dufaza # # SPDX-License-Identifier: Apache-2.0 -"""Shell-like CLI to a devicetree.""" +"""Devicetree shell CLI. +Run the devicetree shell without West. +""" +from typing import cast, Optional, List + +import argparse +import os import sys -from dtsh.dtsh import DtshError -from dtsh.session import DevicetreeShellSession +from dtsh.config import DTShConfig +from dtsh.shell import DTShError +from dtsh.rich.theme import DTShTheme +from dtsh.rich.session import DTShRichSession + + +class DTShCliArgv: + """Command line arguments parser.""" + + _parser: argparse.ArgumentParser + _argv: argparse.Namespace + + def __init__(self) -> None: + self._parser = argparse.ArgumentParser( + prog="dtsh", + description="shell-like interface with Devicetree", + # See e.g. https://github.com/zephyrproject-rtos/zephyr/issues/53495 + allow_abbrev=False, + ) + + grp_open_dts = self._parser.add_argument_group("open a DTS file") + grp_open_dts.add_argument( + "-b", + "--bindings", + help="directory to search for binding files", + action="append", + metavar="DIR", + ) + grp_open_dts.add_argument( + "dts", help="path to the DTS file", nargs="?", metavar="DTS" + ) + + grp_user_files = self._parser.add_argument_group("user files") + grp_user_files.add_argument( + "-u", + "--user-files", + help="initialize per-user configuration files and exit", + action="store_true", + ) + grp_user_files.add_argument( + "--preferences", + help="load additional preferences file", + nargs=1, + metavar="FILE", + ) + grp_user_files.add_argument( + "--theme", + help="load additional styles file", + nargs=1, + metavar="FILE", + ) + self._argv = self._parser.parse_args() -def run(): - dt_src_path = sys.argv[1] if len(sys.argv) > 1 else None - dt_bindings_path = sys.argv[2:] if len(sys.argv) > 2 else None + @property + def binding_dirs(self) -> Optional[List[str]]: + """Directories to search for binding files.""" + if self._argv.bindings: + return cast(List[str], self._argv.bindings) + return None + @property + def dts(self) -> str: + """Path to the Devicetree source file.""" + if self._argv.dts: + return str(self._argv.dts) + return os.path.join(os.path.abspath("build"), "zephyr", "zephyr.dts") + + @property + def user_files(self) -> bool: + """Initialize user files and exit.""" + return bool(self._argv.user_files) + + @property + def preferences(self) -> Optional[str]: + """Additional preferences file.""" + return ( + str(self._argv.preferences[0]) if self._argv.preferences else None + ) + + @property + def theme(self) -> Optional[str]: + """Additional styles file.""" + return str(self._argv.theme[0]) if self._argv.theme else None + + +def _load_preference_file(path: str) -> None: try: - DevicetreeShellSession.open(dt_src_path, dt_bindings_path).run() - except DtshError as e: - print(f'{str(e)}\n') - if e.cause: - print(f'{str(e.cause)}\n') - # -EINVAL + DTShConfig.getinstance().load_ini_file(path) + except DTShConfig.Error as e: + print(e, file=sys.stderr) + print(f"Failed to load preferences file: {path}", file=sys.stderr) sys.exit(-22) +def _load_theme_file(path: str) -> None: + try: + DTShTheme.getinstance().load_theme_file(path) + except DTShConfig.Error as e: + print(e, file=sys.stderr) + print(f"Failed to load styles file: {path}", file=sys.stderr) + sys.exit(-22) + + +def run() -> None: + """Open a devicetree shell session and run its interactive loop.""" + argv = DTShCliArgv() + + if argv.user_files: + # Initialize per-user configuration files and exit. + ret = DTShConfig.getinstance().init_user_files() + sys.exit(ret) + + if argv.preferences: + # Load additional preference file. + _load_preference_file(argv.preferences) + + if argv.theme: + # Load additional styles file. + _load_theme_file(argv.theme) + + session = None + try: + session = DTShRichSession.create(argv.dts, argv.binding_dirs) + except DTShError as e: + print(e.msg, file=sys.stderr) + print("Failed to initialize devicetree", file=sys.stderr) + sys.exit(-22) + + if session: + session.run() + + if __name__ == "__main__": run() diff --git a/src/dtsh/config.py b/src/dtsh/config.py index d2a37e1..2383352 100644 --- a/src/dtsh/config.py +++ b/src/dtsh/config.py @@ -1,125 +1,560 @@ -# Copyright (c) 2022 Chris Duf +# Copyright (c) 2023 Christophe Dufaza # # SPDX-License-Identifier: Apache-2.0 -"""Shell configuration API.""" +"""Devicetree shell configuration and data. +User-specific configuration and data are stored into a +platform-dependent directory. +DTSh is configured by simple INI files named "dtsh.ini": + +- the bundled configuration file which sets the default configuration +- an optional user's configuration file which customizes the defaults + +The command history file (if GNU readline support is enabled) +is loaded from and stored into the same directory. + +Unit tests and examples: tests/test_dtsh_config.py +""" + + +from typing import Optional + +import configparser +import codecs +import enum import os +import re +import shutil import sys -from rich.theme import Theme -from dtsh.rl import readline -from dtsh.dtsh import DtshError +class ActionableType(enum.Enum): + """Control the rendering of actionable text elements (aka links). + When acted on (clicked) actionable UIs elements should open local + files in the system default text editor (e.g. YAML binding files), + or open external URLs in the system default browser (e.g. links to + the Zephyr project documentation or Devicetree Specification). -class DtshConfig(object): - """Shell configuration manager. + Valid values: + + - "none": won't create any link + - "default": the text itself (e.g. "nordic,nrf-twi") is made actionable + - "sparse": an additional actionable view (e.g. "[↗]") is appended + to the original text + + For sparse links, the additional view content is configured + by the DTSh option "wchar.actionable". """ - @staticmethod - def usr_config_base_posix() -> str: - """User configuration directory (Linux). + NONE = "none" + """No actions.""" - On Linux, the user configuration is $XDG_CONFIG_HOME/dtsh. + LINK = "link" + """Make linked text itself actionable.""" - According to the XDG Base Directory Specification, - should default to ~/.config/dtsh if XDG_CONFIG_HOME is not set. + ALT = "alt" + """Append additional actionable view.""" - See: - - https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html - """ - cfg_base = os.environ.get('XDG_CONFIG_HOME') - if not cfg_base: - cfg_base = os.path.join(os.path.expanduser('~'), '.config') - return os.path.join(cfg_base, 'dtsh') - @staticmethod - def usr_config_base_nt() -> str: - r"""User configuration directory (Windows). +class DTShConfig: + """Devicetree shell application data and configuration.""" + + class Error(BaseException): + """Error loading configuration file.""" + + @classmethod + def getinstance(cls) -> "DTShConfig": + """Access the preferences configuration instance.""" + return _dtshconf + + # RE for ASCII escape sequences that may appear in Python strings. + # See: + # - DTShConfig.getstr() + # - "unicode_escape doesn't work in general" (https://stackoverflow.com/a/24519338) + _RE_ESCAPE_SEQ: re.Pattern[str] = re.compile( + r""" + ( \\U........ + | \\u.... + | \\x.. + | \\[0-7]{1,3} + | \\N\{[^}]+\} + | \\[\\'"abfnrtv] + )""", + re.UNICODE | re.VERBOSE, + ) - On Windows (NT+), the user configuration is %LOCALAPPDATA%\dtsh, - and should default to ~\AppData\Local\Dtsh. + # Parsed configuration. + _cfg: configparser.ConfigParser + + # Path to the per-user DTSh configuration and data directory + _app_dir: str + + def __init__(self, path: Optional[str] = None) -> None: + """Initialize DTSh configuration. + + If a configuration path is explicitly set, + only this configuration file is loaded. + + Otherwise, proceed to default configuration initialization: + + - 1st, load bundled default configuration file + - then, load user's configuration file to customize defaults + + Args: + path: Path to configuration file, + or None for default configuration initialization. """ - cfg_base = os.environ.get('LOCALAPPDATA') - if not cfg_base: - cfg_base = os.path.join(os.path.expanduser('~'), 'AppData', 'Local') - return os.path.join(cfg_base, 'Dtsh') + self._init_app_dir() - @staticmethod - def usr_config_base_darwin() -> str: - """User configuration directory (macOS). + self._cfg = configparser.ConfigParser( + # Despite its name, extended interpolation seems more intuitive + # to us than basic interpolation. + interpolation=configparser.ExtendedInterpolation() + ) - On macOS, default to ~/Library/Dtsh. + if path: + # If explicitly specified, load only this one. + self.load_ini_file(path) + else: + # Load defaults from bundled configuration file. + path = os.path.join(os.path.dirname(__file__), "dtsh.ini") + self.load_ini_file(path) + # Load user's configuration file if any. + path = self.get_user_file("dtsh.ini") + if os.path.isfile(path): + self.load_ini_file(path) + + @property + def app_dir(self) -> str: + r"""Path to the per-user DTSh configuration and data directory. + + Location is platform-dependent: + + - POSIX: "$XDG_CONFIG_HOME/dtsh", or "~/.config/dtsh" if XDG_CONFIG_HOME + is not set (Freedesktop XDG Base Directory Specification) + - Windows: "%LOCALAPPDATA%\DTSh", or "~\AppData\Local\DTSh" + if LOCALAPPDATA is unset (unlikely) + - macOS: "~/Library/DTSh" + + Returns: + The path to the user's directory for DTSh application data + and configuration files. The directory is not granted to exist. """ - return os.path.join(os.path.expanduser('~'), 'Libray', 'Dtsh') + return self._app_dir + + @property + def wchar_ellipsis(self) -> str: + """Ellipsis.""" + return self.getstr("wchar.ellipsis") + + @property + def wchar_arrow_ne(self) -> str: + """North-East Arrow.""" + return self.getstr("wchar.arrow_ne") + + @property + def wchar_arrow_nw(self) -> str: + """North-West Arrow.""" + return self.getstr("wchar.arrow_nw") + + @property + def wchar_arrow_right(self) -> str: + """Rightwards Arrow.""" + return self.getstr("wchar.arrow_right") + + @property + def wchar_arrow_right_hook(self) -> str: + """Rightwards Arrow.""" + return self.getstr("wchar.arrow_right_hook") + + @property + def wchar_dash(self) -> str: + """Tiret.""" + return self.getstr("wchar.dash") + + @property + def wchar_link(self) -> str: + """Tiret.""" + return self.getstr("wchar.link") + + @property + def prompt_default(self) -> str: + """Default ANSI prompt for DTSh sessions.""" + return self.getstr("prompt.default") + + @property + def prompt_alt(self) -> str: + """Alternate ANSI prompt for DTSh sessions.""" + return self.getstr("prompt.alt") + + @property + def prompt_sparse(self) -> bool: + """Whether to append an empty line after DTSh command outputs.""" + return self.getbool("prompt.sparse") + + @property + def pref_redir2_maxwidth(self) -> int: + """Maximum width in number characters for command output redirection.""" + return self.getint("pref.redir2_maxwidth") + + @property + def pref_always_longfmt(self) -> bool: + """Whether to assume the flag "use long listing format" is always set.""" + return self.getbool("pref.always_longfmt") + + @property + def pref_fs_hide_dotted(self) -> bool: + """Whether to hide files and directories whose name starts with ".".""" + return self.getbool("pref.fs.hide_dotted") + + @property + def pref_fs_no_spaces(self) -> bool: + """Whether to forbid spaces in redirection file paths.""" + return self.getbool("pref.fs.no_spaces") + + @property + def pref_fs_no_overwrite(self) -> bool: + """Whether to forbid redirection to overwrite existing files.""" + return self.getbool("pref.fs.no_overwrite") + + @property + def pref_sizes_si(self) -> bool: + """Whether to print sizes with SI units (bytes, kB, MB).""" + return self.getbool("pref.sizes_si") + + @property + def pref_hex_upper(self) -> bool: + """Whether to print hexadicimal digits upper case.""" + return self.getbool("pref.hex_upper") + + @property + def pref_list_headers(self) -> bool: + """Whether to show the headers in list views.""" + return self.getbool("pref.list.headers") + + @property + def pref_list_placeholder(self) -> str: + """Placeholder for missing values in list views.""" + return self.getstr("pref.list.place_holder") + + @property + def pref_list_fmt(self) -> str: + """Default format string for node fields in list views.""" + return self.getstr("pref.list.fmt") + + @property + def pref_list_actionable_type(self) -> ActionableType: + """Actionable type for list views.""" + return self.get_actionable_type("pref.list.actionable_type") + + @property + def pref_list_multi(self) -> bool: + """Whether to allow multiple-line cells in list views.""" + return self.getbool("pref.list.multi") + + @property + def pref_tree_headers(self) -> bool: + """Whether to show the headers in tree views.""" + return self.getbool("pref.tree.headers") + + @property + def pref_tree_placeholder(self) -> str: + """Placeholder for missing values in tree views.""" + return self.getstr("pref.tree.place_holder") - @staticmethod - def usr_config_base(enforce: bool = False) -> str: - """Returns the user's configuration directory for dtsh sessions. + @property + def pref_tree_fmt(self) -> str: + """Default format string for node fields in tree views.""" + return self.getstr("pref.tree.fmt") - Arguments: - enforce -- if True, will try to create the configuration directory - when necessary + @property + def pref_tree_actionable_type(self) -> ActionableType: + """Actionable type for tree anchors.""" + return self.get_actionable_type("pref.tree.actionable_type") - Raises IOError when the requested directory can't be initialized. + @property + def pref_2Sided_actionable_type(self) -> ActionableType: + """Actionable type for the left list view of a 2-sided.""" + return self.get_actionable_type("pref.2sided.actionable_type") + + @property + def pref_tree_cb_anchor(self) -> str: + """Symbol to anchor child-bindings to their parent in tree-views.""" + return self.getstr("pref.tree.cb_anchor") + + @property + def pref_actionable_type(self) -> ActionableType: + """Default rendering for actionable texts.""" + actionable_type = self.getstr("pref.actionable_type") + try: + return ActionableType(actionable_type) + except ValueError: + print( + f"Invalid actionable type: {actionable_type}", file=sys.stderr + ) + return ActionableType.LINK + + @property + def pref_actionable_text(self) -> str: + """Alternate actionable view.""" + return self.getstr("pref.actionable_wchar") + + @property + def pref_svg_theme(self) -> str: + """CSS theme for command output redirection to SVG.""" + return self.getstr("pref.svg.theme") + + @property + def pref_svg_font_family(self) -> str: + """Font family for command output redirection to SVG.""" + return self.getstr("pref.svg.font_family") + + @property + def pref_svg_font_ratio(self) -> float: + """Font aspect ratio for command output redirection to SVG.""" + return self.getfloat("pref.svg.font_ratio") + + @property + def pref_html_theme(self) -> str: + """CSS theme for command output redirection to HTML.""" + return self.getstr("pref.html.theme") + + @property + def pref_html_font_family(self) -> str: + """Font family for command output redirection to HTML.""" + return self.getstr("pref.html.font_family") + + def init_user_files(self) -> int: + """Initialize per-user configuration files.""" + if os.path.isdir(self._app_dir): + src_dir = os.path.dirname(os.path.abspath(__file__)) + + try: + dst = self.get_user_file("dtsh.ini") + if os.path.exists(dst): + print(f"File exists, skipped: {dst}") + else: + shutil.copyfile(os.path.join(src_dir, "dtsh.ini"), dst) + print(f"User preferences: {dst}") + + dst = self.get_user_file("theme.ini") + if os.path.exists(dst): + print(f"File exists, skipped: {dst}") + else: + shutil.copyfile( + os.path.join(src_dir, "rich", "theme.ini"), dst + ) + print(f"User theme: {dst}") + + return 0 + + except (OSError, OSError) as e: + print(f"Failed to create file: {dst}", file=sys.stderr) + print(f"Cause: {e}", file=sys.stderr) + + # Per-user configuration files don't exist (-ENOENT). + return -2 + + def get_user_file(self, *paths: str) -> str: + """Get path to a use file within the DTSh application directory. + + Args: + paths: Relative path to the resource. """ - if sys.platform == 'darwin': - cfg_dir = DtshConfig.usr_config_base_darwin() - elif os.name == 'nt': - cfg_dir = DtshConfig.usr_config_base_nt() - else: - cfg_dir = DtshConfig.usr_config_base_posix() - if enforce and not os.path.isdir(cfg_dir): - os.mkdir(cfg_dir) - return cfg_dir + return os.path.join(self._app_dir, *paths) - @staticmethod - def get_history_path() -> str: - """Returns the history file's path. + def getbool(self, option: str, fallback: bool = False) -> bool: + """Access a configuration option's value as a boolean. - Raises IOError when the configuration directory can't be initialized. + Boolean: + - True: '1', 'yes', 'true', and 'on' + - False: '0', 'no', 'false', and 'off' + + Args: + option: The option's name. + fallback: If set, represents the fall-back value + for an undefined option or an invalid value. + Defaults to False. + + Returns: + The option's value as a boolean. """ - return os.path.join(DtshConfig.usr_config_base(True), 'history') + try: + return self._cfg.getboolean("dtsh", option) + except (configparser.Error, ValueError) as e: + print(f"configuration error: {option}: {e}", file=sys.stderr) + return fallback + + def getint(self, option: str, fallback: int = 0) -> int: + """Access a configuration option's value as a integer. + + Integers: + + - base-2, -8, -10 and -16 are supported + - if not base 10, the actual base is determined + by the prefix "0b/0B" (base-2), "0o/0O" (base-8), + or "0x/0X" (base-16) - @staticmethod - def get_theme_path() -> str: - """Returns the rich theme's path. + Args: + option: The option's name. + fallback: If set, represents the fall-back value + for an undefined option or an invalid value. + Defaults to 0. + + Returns: + The option's value as an integer. """ - theme_path = os.path.join(DtshConfig.usr_config_base(), 'theme') - if not os.path.isfile(theme_path): - # Fallback to default theme. - theme_path = os.path.join(os.path.dirname(__file__), 'theme') - return theme_path - - @staticmethod - def readline_read_history() -> None: - """Load history file. + try: + return int(self._cfg.get("dtsh", option), base=0) + except (configparser.Error, ValueError) as e: + print(f"configuration error: {option}: {e}", file=sys.stderr) + return fallback + + def getfloat(self, option: str, fallback: float = 0) -> float: + """Access a configuration option's value as float. + + Args: + option: The option's name. + fallback: If set, represents the fall-back value + for an undefined option or an invalid value. + Defaults to 0. + + Returns: + The option's value as a float. """ try: - history_path = DtshConfig.get_history_path() - if os.path.isfile(history_path): - readline.read_history_file(history_path) - except IOError as e: - print(f"Failed to load history: {str(e)}") - - @staticmethod - def readline_write_history() -> None: - """Save history file. + return float(self._cfg.get("dtsh", option)) + except (configparser.Error, ValueError) as e: + print(f"configuration error: {option}: {e}", file=sys.stderr) + return fallback + + def getstr(self, option: str, fallback: str = "") -> str: + r"""Access a configuration option's value as wide string. + + Wide strings may contain: + - actual Unicode characters (e.g. "↗") + - UTF-8 character literals (e.g. "\u276d") + - other ASCII escape sequences (e.g. "\t") + + Double-quotes are optional, excepted when the string value + ends with trailing spaces (e.g. "❯ "). + + Args: + option: The option's name. + fallback: If set, represents the fall-back value + for an undefined option or an invalid value. + Defaults to an empty string. + + Returns: + The option's value as a wide string. """ + # To support what we named "wide strings", relying on Python UTF-8 + # codecs won't work: + # + # "00î00".encode('utf-8').decode('unicode_escape') + # '00î00' + # + # Reason (see "unicode_escape doesn't work in general" bellow): + # The unicode_escape codec, despite its name, turns out to assume + # that all non-ASCII bytes are in the Latin-1 (ISO-8859-1) encoding. + # + # The best approach seems to be: + # - to not encode values, Python 3 strings are already Unicode + # - decode only escape sequences + # + # Adapted from @rspeer answer "unicode_escape doesn't work in general" + # at https://stackoverflow.com/a/24519338. + try: + # Quoted strings permit to define empty strings and strings + # that end with spaces: strip leading and trailing spaces. + val = self._cfg.get("dtsh", option).strip('"') + # Multi-line strings permit to define long strings: replace + # newlines with spaces. + val = val.replace("\n", " ") + return str( + DTShConfig._RE_ESCAPE_SEQ.sub( + lambda match: codecs.decode( + match.group(0), "unicode-escape" + ), + val, + ) + ) + except configparser.Error as e: + print(f"Configuration error: {option}: {e}", file=sys.stderr) + return fallback + + def get_actionable_type(self, pref: str) -> ActionableType: + """Access a preference as actionable type.""" + actionable_type = self.getstr(pref) try: - readline.write_history_file(DtshConfig.get_history_path()) - except IOError as e: - print(f"Failed to save history: {str(e)}") + return ActionableType(actionable_type) + except ValueError: + print( + f"{pref}: invalid actionable type '{actionable_type}'", + file=sys.stderr, + ) + # Fall-back, we don't want to fault. + return ActionableType.LINK - @staticmethod - def rich_read_theme() -> Theme: - """Returns the rich theme. + def load_ini_file(self, path: str) -> None: + """Load options from configuration file (INI format). - Raises DtshError when the theme file is invalid. + Overrides already loaded values with the same keys. + + Args: + path: Path to a configuration file. + + Raises: + DTShConfig.Error: Failed to load configuration file. """ try: - return Theme.from_file(open(DtshConfig.get_theme_path())) - except Exception as e: - raise DtshError("Failed to load theme", e) + f = open( # pylint: disable=consider-using-with + path, "r", encoding="utf-8" + ) + self._cfg.read_file(f) + + except (OSError, configparser.Error) as e: + raise DTShConfig.Error(str(e)) from e + + def _init_app_dir(self) -> None: + if sys.platform == "darwin": + self._app_dir = self._init_app_dir_darwin() + elif os.name == "nt": + self._app_dir = self._init_app_dir_nt() + else: + self._app_dir = self._init_app_dir_posix() + + if not os.path.isdir(self._app_dir): + try: + os.makedirs(self._app_dir, mode=0o750) + except OSError as e: + print( + f"Failed to create directory: {self._app_dir}", + file=sys.stderr, + ) + print(f"Cause: {e}", file=sys.stderr) + + def _init_app_dir_darwin(self) -> str: + return os.path.abspath( + os.path.join(os.path.expanduser("~"), "Library", "DTSh") + ) + + def _init_app_dir_nt(self) -> str: + local_app_data = os.environ.get( + "LOCALAPPDATA", + os.path.join(os.path.expanduser("~"), "AppData", "Local"), + ) + return os.path.abspath(os.path.join(local_app_data, "DTSh")) + + def _init_app_dir_posix(self) -> str: + xdg_cfg_home = os.environ.get( + "XDG_CONFIG_HOME", + os.path.join(os.path.expanduser("~"), ".config"), + ) + return os.path.abspath(os.path.join(xdg_cfg_home, "dtsh")) + + +_dtshconf = DTShConfig() diff --git a/src/dtsh/dts.py b/src/dtsh/dts.py new file mode 100644 index 0000000..861978a --- /dev/null +++ b/src/dtsh/dts.py @@ -0,0 +1,824 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# Copyright (c) 2018 Open Source Foundries Limited. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Devicetree source definition. + +A devicetree is fully defined by: + +- a DTS file in Devicetree source format (DTSpec 6) +- all of the YAML binding files the DTS recursively depends on +- the Devicetree Specification + +This module may rely on cached CMake variables to locate +the binding files the DTS file was actually generated with. +The herein CMake cache reader implementation is adapted from +the zcmake.py module in zephyr/scripts/west_commands. + +This module eventually introduces a YAML file system API +that should cover the devicetree shell needs: + +- access the DT binding files with their base name +- recursively access YAML-included bindings +- the name2path semantic expected for edtlib.Binding objects initialization + +Unit tests and examples: tests/test_dtsh_dts.py +""" + + +from typing import ( + cast, + Optional, + Union, + List, + Sequence, + Dict, + Iterator, + Mapping, +) + +import os +import re +import sys + +import yaml + +# Custom PyYAML loader with support for the legacy '!include syntax. +from devicetree.edtlib import _BindingLoader as YAMLBindingLoader + + +class DTS: + """Devicetree source definition. + + A devicetree source is defined by: + + - a DTS file in Devicetree source format (DTSpec 6) + - all of the YAML binding files the DTS recursively depends on; + the list of directories to search for the YAML files is + termed "bindings search path" + + When not explicitly set, a default bindings search path + can be retrieved, or worked out, based on: + + - cached CMake variables: this is the preferred method, + assuming the cache file was produced by the same build as the DTS file + - environment variables: less reliable than the CMake cache, + since their *current* values are not bound to the build that + produced the DTS file + - assumptions on files layout, e.g. the CMake cache file is expected + to be named CMakeCache.txt, and located in the grand-parent directory + of where the DTS file itself is located (as shown bellow) + + The CMake cache file is searched for according to the typical + Zephyr build layout: + + build/ + ├── CMakeCache.txt + └── zephyr/ + └── zephyr.dts + + """ + + # See path(). + _dts_path: str + + # See bindings_search_path(). + _binding_dirs: List[str] + + # See zephyr_base(). + _zephyr_base: Optional[str] + + # See vendors_file(). + _vendors_file: Optional[str] + + _cmake: Optional["CMakeCache"] + _yamlfs: "YAMLFilesystem" + + def __init__( + self, + dts_path: str, + binding_dirs: Optional[Sequence[str]] = None, + vendors_file: Optional[str] = None, + ) -> None: + """Define a devicetree source. + + Args: + dts_path: Path to the devicetree source file. + binding_dirs: The list of directories to search for the + YAML binding files the DTS depends on. + If not set, the default bindings search path is assumed. + vendors_file: Path to a file in vendor-prefixes.txt format. + """ + self._dts_path = os.path.abspath(dts_path) + self._cmake = self._init_cmake_cache() + self._zephyr_base = self._init_zephyr_base() + self._vendors_file = self._init_vendors_file(vendors_file) + self._binding_dirs = self._init_binding_dirs(binding_dirs) + self._yamlfs = YAMLFilesystem(self._binding_dirs) + + @property + def path(self) -> str: + """Path to the DTS file.""" + return self._dts_path + + @property + def vendors_file(self) -> Optional[str]: + "Path to the vendors file." "" + return self._vendors_file + + @property + def bindings_search_path(self) -> Sequence[str]: + """Directories where the YAML binding files are located. + + Either: + + - set explicitly when defining the DT source + - or retrieved from the CMake cached variable CACHED_DTS_ROOT_BINDINGS + - or worked out (*best effort*) according to "Where bindings are located" + + In the later case, depending on defined environment variables + or cached CMake variables, the bindings search path should contain + the dts/bindings sub-directories of: + + - the zephyr repository + - the application source directory + - any custom board directory + - shield directories + + This search path is not expunged from non existing directories: + it's intended to represent the *considered* directories. + + Refer to zephyr/cmake/modules/dts.cmake for the DTS_ROOT + and DTS_ROOT_BINDINGS values set at build-time: + + list(APPEND + DTS_ROOT + ${APPLICATION_SOURCE_DIR} + ${BOARD_DIR} + ${SHIELD_DIRS} + ${ZEPHYR_BASE} + ) + + foreach(dts_root ${DTS_ROOT}) + set(bindings_path ${dts_root}/dts/bindings) + if(EXISTS ${bindings_path}) + list(APPEND + DTS_ROOT_BINDINGS + ${bindings_path} + ) + endif() + + """ + return self._binding_dirs + + @property + def yamlfs(self) -> "YAMLFilesystem": + """YAML bindings search support.""" + return self._yamlfs + + @property + def app_binary_dir(self) -> str: + """Application binary directory (aka build directory). + + Derived from the DTS file path. + """ + return os.path.dirname(os.path.dirname(self._dts_path)) + + @property + def app_source_dir(self) -> Optional[str]: + """Application source directory (aka project directory). + + Either retrieved from the CMake cache (APPLICATION_SOURCE_DIR), + or derived from the application binary directory. + """ + app_src_dir = None + if self._cmake: + app_src_dir = self._cmake.getstr("APPLICATION_SOURCE_DIR") + if not app_src_dir: + app_src_dir = os.path.dirname(self.app_binary_dir) + return app_src_dir + + @property + def board_dir(self) -> Optional[str]: + """Board directory. + + Retrieved from the CMake cache (BOARD_DIR). + """ + return self._cmake.getstr("BOARD_DIR") if self._cmake else None + + @property + def board(self) -> Optional[str]: + """Board name. + + Retrieved from the CMake cache (BOARD, CACHED_BOARD). + """ + board = None + if self._cmake: + board = self._cmake.getstr("BOARD") + if not board: + board = self._cmake.getstr("CACHED_BOARD") + return board + + @property + def board_file(self) -> Optional[str]: + """Board DTS file. + + Shortcut to "${BOARD_DIR}/${BOARD}.dts". + """ + if self.board_dir and self.board: + return os.path.join(self.board_dir, f"{self.board}.dts") + return None + + @property + def shield_dirs(self) -> Sequence[str]: + """Shield directories. + + Retrieved from the CMake cache (SHIELD_DIRS, CACHED_SHIELD_DIRS). + """ + shield_dirs = [] + if self._cmake: + shield_dirs = self._cmake.getstrs("SHIELD_DIRS") + if not shield_dirs: + shield_dirs = self._cmake.getstrs("CACHED_SHIELD_DIRS") + return shield_dirs + + @property + def fw_name(self) -> Optional[str]: + """Application name. + + Retrieved from the CMake cache (CMAKE_PROJECT_NAME). + """ + if self._cmake: + return self._cmake.getstr("CMAKE_PROJECT_NAME") + return None + + @property + def fw_version(self) -> Optional[str]: + """Application version. + + Retrieved from the CMake cache (CMAKE_PROJECT_VERSION). + """ + if self._cmake: + return self._cmake.getstr("CMAKE_PROJECT_VERSION") + return None + + @property + def zephyr_base(self) -> Optional[str]: + """Path to Zephyr repository. + + Either: + + - retrieved from the CMake cache (ZEPHYR_BASE) + - or retrieved from the shell environment (ZEPHYR_BASE) + - or set according the expected file layout ZEPHYR_BASE/scripts/dts/dtsh + - or unvailable + """ + return self._zephyr_base + + @property + def zephyr_sdk_dir(self) -> Optional[str]: + """Path to Zephyr SDK. + + Either retrieved from the CMake cache or the shell + environment (ZEPHYR_SDK_INSTALL_DIR). + """ + zephyr_sdk_dir = None + if self._cmake: + zephyr_sdk_dir = self._cmake.getstr("ZEPHYR_SDK_INSTALL_DIR") + if not zephyr_sdk_dir: + zephyr_sdk_dir = os.environ.get("ZEPHYR_SDK_INSTALL_DIR") + return zephyr_sdk_dir + + @property + def toolchain_variant(self) -> Optional[str]: + """Zephyr build toolchain variant. + + Either retrieved from the CMake cache or the shell + environment (ZEPHYR_TOOLCHAIN_VARIANT). + """ + toolchain_variant = None + if self._cmake: + toolchain_variant = self._cmake.getstr("ZEPHYR_TOOLCHAIN_VARIANT") + if not toolchain_variant: + toolchain_variant = os.environ.get("ZEPHYR_TOOLCHAIN_VARIANT") + return toolchain_variant + + @property + def toolchain_dir(self) -> Optional[str]: + """Path to build toolchain. + + Depends on toolchain variant: + + - "zephyr": path to the Zephyr SDK + - other variants: build system variable {TOOLCHAIN}_TOOLCHAIN_PATH + (CMake cache or environment) + """ + toolchain_dir = None + if self.toolchain_variant: + if self.toolchain_variant == "zephyr": + toolchain_dir = self.zephyr_sdk_dir + else: + var = f"{self.toolchain_variant.upper()}_TOOLCHAIN_PATH" + if self._cmake: + toolchain_dir = self._cmake.getstr(var) + if not toolchain_dir: + toolchain_dir = os.environ.get(var) + return toolchain_dir + + def _init_cmake_cache(self) -> Optional["CMakeCache"]: + # Is the CMake cache available at ../CMakeCache.txt from the DTS file ? + path = os.path.join(self.app_binary_dir, "CMakeCache.txt") + if os.path.isfile(path): + return CMakeCache.open(path) + return None + + def _init_zephyr_base(self) -> Optional[str]: + if self._cmake: + zephyr_base = self._cmake.getstr("ZEPHYR_BASE") + else: + zephyr_base = os.environ.get("ZEPHYR_BASE") + if not zephyr_base: + # dtsh/src/dtsh/__file__ + dtsh_base = os.path.dirname( + os.path.dirname(os.path.dirname(__file__)) + ) + # ZEPHYR_BASE/scripts/dts/dtsh + zephyr_base = os.path.dirname( + os.path.dirname(os.path.dirname(dtsh_base)) + ) + return zephyr_base + + def _init_vendors_file(self, vendors_file: Optional[str]) -> Optional[str]: + if (not vendors_file) and self._zephyr_base: + vendors_file = os.path.join( + self._zephyr_base, "dts", "bindings", "vendor-prefixes.txt" + ) + return vendors_file + + def _init_binding_dirs( + self, binding_dirs: Optional[Sequence[str]] + ) -> List[str]: + if binding_dirs: + binding_dirs = [os.path.abspath(path) for path in binding_dirs] + else: + if self._cmake: + binding_dirs = self._cmake.getstrs("CACHED_DTS_ROOT_BINDINGS") + if not binding_dirs: + # Fallback to "Where bindings are located". + # NOTE: Possible missing DTS roots ? + # + # - any directories manually included in the + # DTS_ROOT CMake variable + # - any module that defines a dts_root in its Build settings + # + # IIUC, this might be a non-issue, since either the CMake cache: + # - is available, and we should be able to get all bindings + # from the CACHED_DTS_ROOT_BINDINGS value + # - is unavailable, and we won't access any build settings + dts_roots: List[Optional[str]] = [ + self.app_source_dir, + self.board_dir, + *self.shield_dirs, + self.zephyr_base, + ] + binding_dirs = [ + os.path.join(dtsroot, "dts", "bindings") + for dtsroot in dts_roots + if dtsroot + ] + # cast() is required to avoid type hinting error since + # binding_dirs is first typed as an optional Sequence. + return cast(List[str], binding_dirs) + + +class YAMLFilesystem: + """Find YAML files within a fixed search path (set of directories). + + Rationale: + + - retrieve YAML files with their base name + - provide the name2path semantic expected for edtlib.Binding + objects initialization + """ + + _name2path: Dict[str, str] + + def __init__(self, yaml_dirs: Sequence[str]) -> None: + """Initialize the YAML file system. + + Args: + yaml_dirs: The YAML search path as a set of absolute or relative + directory paths. + """ + self._name2path = {} + for yaml_dir in [os.path.abspath(path) for path in yaml_dirs]: + for root, _, basenames in os.walk(yaml_dir): + for name in basenames: + if name.endswith((".yaml", ".yml")): + self._name2path[name] = os.path.join(root, name) + + @property + def name2path(self) -> Mapping[str, str]: + """Mapping from YAML base names to absolute file paths. + + This mapping contains all YAML files within this file system. + """ + return self._name2path + + def find_path(self, name: str) -> Optional[str]: + """Find a YAML file by name. + + Args: + name: The base name of a YAML file. + + Returns: + The absolute path to the requested YAML file, + or None if not found. + """ + return self._name2path.get(name) + + def find_file(self, name: str) -> Optional["YAMLFile"]: + """Find a YAML file by name. + + Args: + name: The base name of a YAML file. + + Returns: + A wrapper to the requested YAML file, + or None if not found. + """ + path = self.find_path((name)) + return YAMLFile(path) if path else None + + +class CMakeCache: + """CMake cache reader. + + Adapted from zcmake.CMakeCache. + """ + + _entries: Dict[str, "CMakeCacheEntry"] + + @classmethod + def open(cls, path: str) -> Optional["CMakeCache"]: + """Open a CMake cache file for reading. + + Args: + path: Path to the CMake cache file (CMakeCache.txt) to open. + + Returns: + The CMakeCache content. + """ + try: + return CMakeCache(path) + except OSError as e: + print(f"CMakeCache file error: {e}", file=sys.stderr) + except ValueError as e: + print(f"CMakeCache content error: {e}", file=sys.stderr) + return None + + def __init__(self, path: str) -> None: + """Open a CMake cache file. + + Args: + path: Path to the CMake cache file (CMakeCache.txt) to open. + + Raises: + OSError: CMakeCache file error. + ValueError: CMakeCache content error. + """ + with open(path, "r", encoding="utf-8") as cache: + entries = [ + CMakeCacheEntry.from_line(line, line_no) + for line_no, line in enumerate(cache) + ] + self._entries = {entry.name: entry for entry in entries if entry} + + def get(self, name: str) -> Optional["CMakeCacheEntry.ValueType"]: + """Access a cache entry by name. + + Arg: + name: Cache entry name. + + Returns: + The raw cache entry value, or None if not set. + """ + if name in self._entries: + return self._entries[name].value + return None + + def getbool(self, name: str) -> bool: + """Access a cache entry as boolean. + + Arg: + name: Cache entry name. + + Returns: + True if the entry is of type boolean and set ("ON", "YES", etc), + false otherwise. + """ + val = self.get(name) + # May not be Pythonic, but is consistent with type hinting. + return val is True + + def getstr(self, name: str) -> Optional[str]: + """Access a cache entry as string. + + Arg: + name: Cache entry name. + + Returns: + A string value if the entry exists and is actually of type string, + None otherwise. + """ + val = self.get(name) + if val and isinstance(val, str): + return val + return None + + def getstrs(self, name: str) -> List[str]: + """Access a cache entry as a list of strings. + + Arg: + name: Cache entry name. + + Returns: + A list of string values if the entry exists and is either of type + string or of type list of strings, an empty list otherwise. + """ + val = self.get(name) + if val and isinstance(val, str): + return [val] + if isinstance(val, list): + # Assuming list of string. + return val + return [] + + def __contains__(self, name: str) -> bool: + """Map protocol.""" + return name in self._entries + + def __getitem__(self, name: str) -> "CMakeCacheEntry.ValueType": + """Map protocol.""" + return self._entries[name].value + + def __iter__(self) -> Iterator[str]: + """Iterate on the CMake cache entries.""" + return iter(self._entries.keys()) + + def __len__(self) -> int: + """Number of entries in this CMake cache.""" + return len(self._entries) + + +class CMakeCacheEntry: + """CMake cache entry. + + This class understands the type system in a CMakeCache.txt, and + converts the following cache types to Python types: + + Cache Type Python type + ---------- ------------------------------------------- + FILEPATH str + PATH str + STRING str OR list of str (if ';' is in the value) + BOOL bool + INTERNAL str OR list of str (if ';' is in the value) + STATIC str OR list of str (if ';' is in the value) + UNINITIALIZED str OR list of str (if ';' is in the value) + ---------- ------------------------------------------- + + Adapted from zcmake.CMakeCacheEntry. + """ + + # Regular expression for a cache entry. + # + # CMake variable names can include escape characters, allowing a + # wider set of names than is easy to match with a regular + # expression. To be permissive here, use a non-greedy match up to + # the first colon (':'). This breaks if the variable name has a + # colon inside, but it's good enough. + CACHE_ENTRY = re.compile( + r"""(?P.*?) + :(?PFILEPATH|PATH|STRING|BOOL|INTERNAL|STATIC|UNINITIALIZED) + =(?P.*) + """, + re.X, + ) + + ValueType = Union[str, List[str], bool] + + _name: str + _value: "CMakeCacheEntry.ValueType" + + @classmethod + def from_line(cls, line: str, line_no: int) -> Optional["CMakeCacheEntry"]: + """Create an entry from a cache line. + + Args: + line: The cache line content. + line_no: The cache file line number (used for error reporting). + + Returns: + A cache entry or `None` if the line was a comment, empty, + or malformed. + + Raises: + ValueError: Failed conversion to bool. + """ + # Comments can only occur at the beginning of a line. + # (The value of an entry could contain a comment character). + if line.startswith("//") or line.startswith("#"): + return None + + # Whitespace-only lines do not contain cache entries. + if not line.strip(): + return None + + m = cls.CACHE_ENTRY.match(line) + if not m: + return None + + name, type_, value = (m.group(g) for g in ("name", "type", "value")) + if type_ == "BOOL": + try: + value = cls._to_bool(value) + except ValueError as exc: + args = exc.args + (f"on line {line_no}: {line}",) + raise ValueError(args) from exc + elif type_ in {"STRING", "INTERNAL", "STATIC", "UNINITIALIZED"}: + # If the value is a CMake list (i.e. is a string which + # contains a ';'), convert to a Python list. + if ";" in value: + value = value.split(";") + + return CMakeCacheEntry(name, value) + + @classmethod + def _to_bool(cls, val: str) -> bool: + # Convert a CMake BOOL string into a Python bool. + # + # "True if the constant is 1, ON, YES, TRUE, Y, or a + # non-zero number. False if the constant is 0, OFF, NO, + # FALSE, N, IGNORE, NOTFOUND, the empty string, or ends in + # the suffix -NOTFOUND. Named boolean constants are + # case-insensitive. If the argument is not one of these + # constants, it is treated as a variable." + # + # https://cmake.org/cmake/help/v3.0/command/if.html + val = val.upper() + if val in ("ON", "YES", "TRUE", "Y"): + return True + if val in ("OFF", "NO", "FALSE", "N", "IGNORE", "NOTFOUND", ""): + return False + if val.endswith("-NOTFOUND"): + return False + try: + v = int(val) + return v != 0 + except ValueError as e: + raise ValueError(f"not a bool: {val}") from e + + def __init__(self, name: str, value: "CMakeCacheEntry.ValueType") -> None: + """Initialize a new cache entry. + + Args: + name: Entry name. + value: Entry value. + """ + self._name = name + self._value = value + + @property + def name(self) -> str: + """Cache entry name.""" + return self._name + + @property + def value(self) -> "CMakeCacheEntry.ValueType": + """Cache entry raw value.""" + return self._value + + def __repr__(self) -> str: + return f"{self._name}: {self._value}" + + +class YAMLFile: + """Cheap wrapper around a YAML file. + + This API is: + + - lazy-initialized: properties are initialized when accessed + - fail-safe: errors are logged once to stderr, and empty values are returned + """ + + # Absolute file path. + _path: str + + # Lazy-initialized file content. + _content: Optional[str] + + # Lazy-initialized YAML model. + _raw: Optional[Dict[str, object]] + + # Lazy-initialized YAML "include: ". + _includes: Optional[List[str]] + + def __init__(self, path: str) -> None: + """Lazy-initialize wrapper. + + Only the YAML file path is set upon initialization. + + Then accessing: + + - the *includes* will require initializing the YAML model + - the YAML model will require loading the YAML file content + + Args: + path: Absolute path to a YAML file. + """ + self._path = path + # Lazy-initialized. + self._content = None + self._raw = None + self._includes = None + + @property + def path(self) -> str: + """Absolute file path.""" + return self._path + + @property + def content(self) -> str: + """YAML file content.""" + # Will Initialize an empty content if fails to load the YAML file. + self._init_content() + return self._content # type: ignore + + @property + def raw(self) -> Dict[str, object]: + """YAML model.""" + # Will Initialize an empty model if the YAML file content unavailable. + self._init_model() + return self._raw # type: ignore + + @property + def includes(self) -> Sequence[str]: + """Names of included YAML files.""" + # Will Initialize an empty list if the YAML model is unavailable. + self._init_includes() + return self._includes # type: ignore + + def _init_content(self) -> None: + if self._content is not None: + return + # Only one attempt to initialize content. + self._content = "" + + try: + with open(self._path, mode="r", encoding="utf-8") as f: + self._content = f.read().strip() + except OSError as e: + print(f"YAML: {e}", file=sys.stderr) + + def _init_model(self) -> None: + if self._raw is not None: + return + # Only one attempt to initialize model. + self._raw = {} + + # Depends on YAML content. + self._init_content() + if not self._content: + return + + try: + self._raw = yaml.load(self._content, Loader=YAMLBindingLoader) + except yaml.YAMLError as e: + print(f"YAML: {self._path}: {e}", file=sys.stderr) + + def _init_includes(self) -> None: + if self._includes is not None: + return + # Only one attempt to initialize includes. + self._includes = [] + + # Depends on YAML model. + self._init_model() + if not self._raw: + return + + # See edtlib.Binding._merge_includes() + yaml_inc = self._raw.get("include") + if isinstance(yaml_inc, str): + self._includes.append(yaml_inc) + elif isinstance(yaml_inc, list): + for inc in yaml_inc: + if isinstance(inc, str): + self._includes.append(inc) + elif isinstance(inc, dict): + basename = inc.get("name") + if basename: + self._includes.append(basename) diff --git a/src/dtsh/dtsh.ini b/src/dtsh/dtsh.ini new file mode 100644 index 0000000..104e05f --- /dev/null +++ b/src/dtsh/dtsh.ini @@ -0,0 +1,367 @@ +# Copyright (c) 2022 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +# Devicetree shell default configuration. +# +# Configuration files are written in standard Python configparser +# format (aka Microsoft Windows INI files). + +[dtsh] +# Devicetree shell configuration. +# +# String values: +# - double-quote when ending with trailing spaces +# - supports 'u' escape sequences followed by four hex digits +# giving an Unicode code point (e.g. "\u2768") +# +# Boolean values: +# - True: '1', 'yes', 'true', and 'on' +# - False: '0', 'no', 'false', and 'off' +# +# Integer values: +# - base-2, -8, -10 and -16 are supported +# - if not base 10, the actual base is determined by the prefix +# "0b/0B" (base-2), "0o/0O" (base-8), or "0x/0X" (base-16) +# +# Float values: +# - decimal notation: e.g. 0.1 +# - scientific notation: e.g. 1e-1 +# +# Option values support (extended) interpolation: +# hello = hello +# hello_world = ${hello} world +# +# Use a $$ to escape the dollar sign: +# dollar = $$ + + +################################################################################ +# UNICODE Symbols. + +# Ellipsis. +# Default: u2026 ("…") +wchar.ellipsis = \u2026 + +# North-East Arrow. +# Default: u2197 ("↗") +wchar.arrow_ne = \u2197 + +# Notrh-West Arrow. +# Default: u2196 ("↖") +wchar.arrow_nw = \u2196 + +# Rightwards arrow. +# Default: u2192 ("→") +wchar.arrow_right = \u2192 + +# Rightwards arrow with hook. +# Default: u21B3 ("↳") +wchar.arrow_right_hook = \u21B3 + +# Tiret. +# Default: u2014 ("—"). +wchar.dash = \u2014 + +# Link. +# Default: u1f517 ("🔗") +wchar.link = \u1F517 + + +################################################################################ +# Stateful ANSI prompt. +# +# Shell prompt: +# - ANSI: may contain ANSI escape sequences (Select Graphic Rendition, SGR) +# - support an alternative value, e.g. for changing the prompt color +# when the last command has failed +# +# To use ANSI escape codes in `input()` without breaking +# the GNU readline cursor position, please protect SGR parameters +# with RL_PROMPT_{START,STOP}_IGNORE markers: +# +# := m +# := +# +# := '\001' +# := '\002' +# +# := ESC[ +# := \x1b[ +# := \033[ +# +# Additionally, both prompts should occupy the same physical space +# on the terminal screen: i.e. they should involve the same number +# of characters outside of these RL markers. +# +# For example, a bold (1) 8-bit color (38;5;N;1) stateful prompt: +# +# default: "\001\x1b[38;5;99;1m\002>\001\x1b[0m\002 " +# +# alt: "\001\x1b[38;5;88;1m\002>\001\x1b[0m\002 " +# +# See: +# - ANSI/VT100 Terminal Control Escape Sequences +# https://www2.ccs.neu.edu/research/gpc/VonaUtils/vona/terminal/vtansi.htm +# - How to fix column calculation in Python readline if using color prompt +# https://stackoverflow.com/questions/9468435 +# - ANSI escape code +# https://en.wikipedia.org/wiki/ANSI_escape_code + +# Prompt character (or string, actually) from which +# are derived the default ANSI prompts. +# +# Common UTF-8 prompt characters: +# +# - Single Right-Pointing Angle Quotation Mark: u203a (›) +# - Medium Right-Pointing Angle Bracket Ornament: u276d (❭) +# - Heavy Right-Pointing Angle Bracket Ornament: u2771 (❱) +# - Heavy Right-Pointing Angle Quotation Mark Ornament: u276f (❯) +# - Right-Pointing Curved Angle Bracket: u29fd (⧽) +# - BLACK RIGHT-POINTING TRIANGLE: u25b6 (▶) +# - Right shaded arrow \u27a9 (➩) +# +# Or simply: +# - ">" +# - "$" +# - "dtsh:" +# +# Type: String +# +# Default: "\u276D" +prompt.wchar = \u276D + +# Default ANSI prompt. +# +# Note: the trailing space is intentional but optional. +# +# Type: String +prompt.default = "\001\x1b[38;5;99m\002${prompt.wchar}\001\x1b[0m\002 " + +# Alternative prompt, e.g. after a command has failed. +# +# Note: the trailing space is intentional but optional. +# +# Type: String +prompt.alt = "\001\x1b[38;5;88m\002${prompt.wchar}\001\x1b[0m\002 " + +# Whether to append an empty line after commands output. +# Type: Bool +# Default: True +prompt.sparse = yes + + +# Whether to assume the "use a long listing format" flag (-l) flag is always set. +# Type: Bool +# Default: True +pref.always_longfmt = no + +# Maximum width in number characters for commands output redirection. +# Type: Integer +# Default: 255 +pref.redir2_maxwidth = 255 + +# Whether to print sizes with SI units (bytes, kB, MB). +# Otherwise, sizes are printed in hexadecimal format. +# Type: Bool +# Default: True +pref.sizes_si = yes + +# Whether to print hexadecimal digits upper case, +# e.g. "0xFF" rather than "0xff". +# +# Type: Bool +# Default: False +pref.hex_upper = no + + +# Whether to hide files and directories whose +# name starts with "." (e.g. when completing file paths). +# +# Type: Bool +# Default: True +pref.fs.hide_dotted = yes + +# Whether to forbid spaces in redirection file paths. +# +# Type: Bool +# Default: True +pref.fs.no_spaces = yes + +# Whether to forbid command output redirection +# to overwrite existing files. +# +# Type: Bool +# Default: True +pref.fs.no_overwrite = yes + + +# List views: whether to show the headers. +# +# Type: Bool +# Default: True +pref.list.headers = yes + +# List views: placeholder for missing values. +# +# Type: String +# Default: Unset (no place holder) +pref.list.place_holder = + +# List views: default format string for node fields. +# +# Type: String +# Default: NLC +pref.list.fmt = NLC + +# List views: rendering for actionable texts (aka links). +# +# Type: String +# - "none": do not create hyperlinks +# - "link" (default): link text like browsers do +# - "alt": append alternative actionable view +pref.list.actionable_type = link + +# List views: whether to allow multiple-line cells. +# +# Type: Bool +# Default: True +pref.list.multi = no + +# Tree views: whether to show the headers. +# +# Type: Bool +# Default: True +pref.tree.headers = yes + +# Tree views: placeholder for missing values. +# +# Type: String +# Default: Ellipsis +pref.tree.place_holder = ${wchar.ellipsis} + +# Tree views: default format string for node fields. +# +# The first field specifies anchors, +# the remaining fields the list view columns +# of 2-sided views. +# +# Type: String +# Default: Nd +pref.tree.fmt = Nd + +# Tree views: rendering for actionable texts (aka links) +# in anchors. +# +# Type: String +# - "none": do not create hyperlinks +# - "link" (default): link text like browsers do +# - "alt": append alternative actionable view +pref.tree.actionable_type = none + +# Tree views: rendering for actionable texts (aka links) +# in the left list view of a 2-sided (long format). +# +# Type: String +# - "none": do not create hyperlinks +# - "link" (default): link text like browsers do +# - "alt": append alternative actionable view +pref.2sided.actionable_type = link + + +# Symbol to anchor child-bindings to their parent in tree-views. +# Set it to an empty value to disable. +# +# Type: String +# Default: Rightwards arrow with hook. +pref.tree.cb_anchor = ${wchar.arrow_right_hook} + + +# Default rendering for actionable texts (aka links). +# +# Type: String +# - "none": do not create hyperlinks +# - "link" (default): link text like browsers do +# - "alt": append alternative actionable view +pref.actionable_type = link + +# Alternative actionable text. +# +# This is the appended text element that will +# actually be actionable. +# Depending on availability, one may try: +# - Link symbol (U+1F517) +# - External link symbol (not yet standardized, +# e.g. U+F08E AwesomeFont) +# +# Type: String +# Default: "[North-East Arrow]" +pref.actionable_wchar = [${wchar.arrow_ne}] + + +# Command output redirection to HTML: theme. +# +# Configure text and background colors for HTML documents. +# +# Possible values: +# - "svg": default theme for SVG documents (dark bakground, light text) +# - "html": default theme for HTML documents (dark bakground, light text) +# - "dark": darker +# - "light": lighter +# - "night": darkest +# +# Type: String +# Default: default +pref.html.theme = html + +# Command output redirection to HTML: font family. +# This the family name, e.g. "Source Code Pro". +# +# Note: +# - multiple coma separated values allowed, +# e.g. "Source Code Pro, Courier New" +# - the generic "monospace" family is automatically appended last +# - the "Courier New" default font family is installed nearly "everywhere", +# but may appear a bit dull, and might not support the box drawing +# characters range that make trees sharp +# +# Type: String +# Default: Courier New +pref.html.font_family = Courier New + +# Command output redirection to SVG: theme. +# +# Configure text and background colors for SVG documents. +# +# Possible values: +# - "svg": default theme for SVG documents (dark bakground, light text) +# - "html": default theme for HTML documents (dark bakground, light text) +# - "dark": darker +# - "light": lighter +# - "night": darkest +# +# Type: String +# Default: default +pref.svg.theme = svg + +# Command output redirection to SVG: font family. +# This the family name, e.g. "Source Code Pro". +# +# Note: +# - multiple coma separated values allowed, +# e.g. "Source Code Pro, Courier New" +# - the generic "monospace" family is automatically appended last +# - the "Courier New" default font family is installed nearly "everywhere", +# but may appear a bit dull, and might not support the box drawing +# characters range that make trees sharp +# +# Type: String +# Default: Courier New +pref.svg.font_family = Courier New + +# Command output redirection to SVG: font aspect ratio. +# This is the width to height ratio, typically 3:5. +# +# Type: Float +# Default: 0.6 +pref.svg.font_ratio = 0.6 diff --git a/src/dtsh/dtsh.py b/src/dtsh/dtsh.py deleted file mode 100644 index b793756..0000000 --- a/src/dtsh/dtsh.py +++ /dev/null @@ -1,1627 +0,0 @@ -# Copyright (c) 2022 Chris Duf -# -# SPDX-License-Identifier: Apache-2.0 - -"""Devicetree shell core API.""" - - -import getopt -import os -import re -from pathlib import Path - -from abc import abstractmethod -from typing import Any, ClassVar, Dict, List, Optional, Tuple, Union - -from devicetree.edtlib import EDT, EDTError, Node, Binding, Property - -from dtsh.systools import Git, CMakeCache, GCCArm - - -class DtshVt(object): - """Devicetree shells standard I/O API. - """ - - @abstractmethod - def write(self, *args, **kwargs) -> None: - """Write (aka print) to stdout. - - Arguments: - args -- positional arguments for the underlying implementation - kwargs -- keyword arguments for the underlying implementation - """ - - @abstractmethod - def pager_enter(self) -> None: - """Enter pager context. - - Output will be paged until a call to pager_exit(). - """ - - @abstractmethod - def pager_exit(self) -> None: - """Exit pager context. - """ - - @abstractmethod - def clear(self) -> None: - """Clear stdout. - """ - - @abstractmethod - def readline(self, prompt: str) -> str: - """Read a line from stdin. - - Will block until ENTER or EOF. - - Arguments: - prompt -- the prompt to use (see interactive sessions) - - Returns the next line from stdin, with leading and trailing spaces - striped. - - Raises EOFError when the input stream meets an EOF character. - """ - - @abstractmethod - def abort(self) -> None: - """Abort current I/O, since we're either writing to stdout, - or reading from stdin. - """ - - -class DtshCommandOption(object): - """Devicetree shell command option. - - Option definitions are compatible with GNU getopt. - - An option that does not expect any value is a boolean flag. - - An option that expects a named value is an argument. - - An option may admit a short name (e.g. 'v'), - and/or a long name (e.g. 'verbose'). - """ - - _desc: str - _shortname: Union[str, None] - _longname: Union[str, None] - _arg: Union[str, None] - _value: Union[str, bool, None] - - def __init__(self, - desc: str, - shortname: Optional[str], - longname: Optional[str], - arg: Optional[str] = None) -> None: - """Define a command option. - - Arguments: - desc -- short description, e.g. 'use a long listing format' - shortname -- short option name (e.g. the 'v' in '-v), - or None if the option does not admit a short name - longname -- long option name (e.g. 'verbose' in '--verbose'), - or None if the option does not admit a long name - arg -- the argument name (e.g. in '-a i2c0', the argument name could - be 'alias'), or None if the option is a flag - """ - self._desc = desc - self._shortname = shortname - self._longname = longname - self._arg = arg - self._value = None - - @property - def desc(self) -> str: - """The option's description. - """ - return self._desc - - @property - def shortname(self) -> Union[str, None]: - """The option's short name, e.g. 'v'. - - This name does not include the '-' prefix, - neither the ':' postfix when an argument is expected. - - Retutns the option's short name, or None if the option does not admit - a short name. - """ - return self._shortname - - @property - def longname(self) -> Union[str, None]: - """The option's long name, e.g. 'verbose'. - - This name does not include the '--' prefix, - neither the '=' postfix when an argument is expected. - - Returns the option's long name, or None if the option does not admit - a long name. - """ - return self._longname - - @property - def argname(self) -> Union[str, None]: - """The option's argument name, or None if the options is a flag. - """ - return self._arg - - @property - def usage(self) -> str: - """The option's usage string, e.g. '-a -v --verbose'. - """ - txt = '' - if self._shortname: - txt = f"-{self._shortname}" - if self._longname: - if txt: - txt += f" --{self._longname}" - else: - txt = f"--{self._longname}" - if self._arg: - txt += f" <{self._arg}>" - return txt - - @property - def value(self) -> Union[str, bool, None]: - """The option's value. - - Before a command's options are parsed, this value is None. - - After the command string is successfully parsed, an option value is: - - True (set) or False (unset) for a flag - - a string value for an argument - """ - return self._value - - @value.setter - def value(self, v: Union[bool, str]) -> None: - """Set the option's value. - - The options values are typically set when parsing a command string. - - Arguments: - v -- True (False) to set (unset) a flag, - or a string value to set an argument - """ - self._value = v - - def is_flag(self) -> bool: - """Returns True if the option does not expect any value. - """ - return self._arg is None - - def reset(self): - """Reset this option's value, typically before parsing a command string. - """ - self._value = None - - -class DtshCommandFlagHelp(DtshCommandOption): - """Common "help" flag ('-h', '--help). - - If True, print usage summary. - """ - - def __init__(self) -> None: - super().__init__('print usage summary', 'h', 'help', None) - - -class DtshCommandFlagLongFmt(DtshCommandOption): - """Common "long format" flag ('-l') - - If True, use long (aka rich) listing format. - """ - - def __init__(self) -> None: - super().__init__('use long (rich) listing format', 'l', None, None) - - -class DtshCommandArgLongFmt(DtshCommandOption): - """Common "long format" command argument ('-f') - - Specifies columns for a node table. - """ - - def __init__(self) -> None: - super().__init__('visible columns format string', 'f', None, 'fmt') - - -class DtshCommandFlagPager(DtshCommandOption): - """Common "page output" flag ('--pager') - - If True, page command output. - """ - - def __init__(self) -> None: - super().__init__('page command output', None, 'pager', None) - - -class DtshCommand(object): - """Devicetree shell command. - """ - - # Name, e.g. 'ls'. - _name: str - # Description, e.g. 'list nodes content'. - _desc: str - # Supported options. - _options: List[DtshCommandOption] - # Parsed parameters (command string components that are not parsed options). - _params: List[str] - - def __init__(self, - name: str, - desc: str, - with_pager: bool = False, - options: List[DtshCommandOption] = []) -> None: - """Defines a devicetree shell command. - - Arguments: - name -- the command's name (e.g. 'ls') - desc -- the command's description - with_pager -- if True, enables pager option support - options -- the command's options - """ - self._name = name - self._desc= desc - self._params = [] - self._options = [] - self._options.extend(options) - if with_pager: - self._options.append(DtshCommandFlagPager()) - self._options.append(DtshCommandFlagHelp()) - - @property - def name(self) -> str: - """Command's name, e.g. 'ls'. - """ - return self._name - - @property - def desc(self) -> str: - """Command's description, e.g. 'list nodes content'. - """ - return self._desc - - @property - def usage(self) -> str: - """The command's usage string, ' [options]'. - """ - txt = self._name - for opt in self._options: - txt += f" [{opt.usage}]" - return txt - - @property - def options(self) -> List[DtshCommandOption]: - """Available options. - """ - return self._options - - @property - def getopt_short(self) -> str: - """Short options specification string compatible with GNU getopt. - - e.g. 'ha:' when the option supports a flag '-h', - and an argument '-a:'. - """ - shortopts = '' - for opt in self._options: - if opt.shortname: - shortopts += opt.shortname - if opt.argname: - shortopts += ':' - return shortopts - - @property - def getopt_long(self) -> List[str]: - """Long options specification list compatible with GNU getopt. - - e.g. ['help','alias='] when the option supports a flag '--help', - and an argument '--alias='. - """ - longopts = [] - for opt in self._options: - if opt.longname: - longopt = opt.longname - if opt.argname: - longopt += '=' - longopts.append(longopt) - return longopts - - @property - def with_pager(self) -> bool: - return self.with_flag('--pager') - - @property - def with_help(self) -> bool: - return self.with_flag('-h') - - @property - def with_longfmt(self) -> bool: - return self.with_flag('-l') - - @property - def arg_longfmt(self) -> Union[str, None]: - return self.arg_value('-f') - - def option(self, name: str) -> Union[DtshCommandOption, None]: - """Access a supported option. - - Arguments: - name -- an option's name, either a short form (e.g. '-h'), - or a long form (e.g. '--help') - - Returns None if the option is not supported by this command. - """ - for opt in self._options: - if name.startswith('--') and opt.longname: - if name[2:] == opt.longname: - return opt - elif name.startswith('-') and opt.shortname: - if name[1:] == opt.shortname: - return opt - return None - - def with_flag(self, name: str) -> bool: - """Access a command's flag. - - Arguments: - name -- the flag's name, either a short form (e.g. '-v'), - or a long form (e.g. '--verbose') - - Returns True if name refers to a set flag, False otherwise. - """ - opt = self.option(name) - if opt: - return opt.is_flag() and (opt.value == True) - return False - - def arg_value(self, name) -> Union[str, None]: - """Access an argument value. - - Arguments: - name -- the option name - - Returns the argument value, None if the option was not provided on - the command line, or if the option is a flag. - """ - opt = self.option(name) - # Note that parse_argv() would have failed if the argument - # if actually defined as an argument (parse error otherwise). - if opt and (not opt.is_flag()) and (opt.value is not None): - return str(opt.value) - return None - - def reset(self) -> None: - """Reset command options and parameters. - """ - for opt in self._options: - opt.reset() - self._params.clear() - - def parse_argv(self, argv: List[str]) -> None: - """Parse command line arguments, setting options and parameters. - - Arguments: - argv -- the command's arguments - - Raises DtshCommandUsageError when the arguments do not match the - command's usage (getopt). - """ - self.reset() - try: - parsed_opts, self._params = getopt.gnu_getopt(argv, - self.getopt_short, - self.getopt_long) - except getopt.GetoptError as e: - raise DtshCommandUsageError(self, str(e), e) - - for opt_name, opt_arg in parsed_opts: - opt = self.option(opt_name) - if opt: - if opt.argname: - opt.value = opt_arg - else: - opt.value = True - - if self.with_help: - # Should print usage summary, see DevicetreeShellSession.run(). - raise DtshCommandUsageError(self) - - def autocomplete_option(self, prefix: str) -> List[DtshCommandOption]: - """Auto-complete a command's options name. - - Arguments: - prefix -- the option's name prefix, starting with '-' or '--' - - Returns a list of matching options. - """ - completions: List[DtshCommandOption] = [] - - if prefix == '-': - # Match all options, sorting with short names first. - shortopts = [o for o in self._options if o.shortname] - completions.extend(shortopts) - # Then options with long names only. - otheropts = [ - o for o in self._options if o.longname and (o not in shortopts) - ] - completions.extend(otheropts) - - elif prefix.startswith('--'): - # Auto-comp long option names only. - p = prefix[2:] - for opt in self._options: - if not opt.longname: - continue - if not p: - completions.append(opt) - continue - if opt.longname.startswith(p) and (len(opt.longname) > len(p)): - completions.append(opt) - - return completions - - def autocomplete_argument(self, - arg: DtshCommandOption, # pyright: ignore reportUnusedVariable - prefix: str) -> List[str]: # pyright: ignore reportUnusedVariable - """Auto-complete a command's option value (aka argument). - - Arguments: - arg -- the option expecting a value - prefix -- the option's name prefix, starting with '-' or '--' - - Returns a list of matching arguments. - """ - return [] - - def autocomplete_param(self, prefix: str) -> Tuple[int, List]: # pyright: ignore reportUnusedVariable - """Auto-complete a command's parameter value. - - Completions are represented by the tagged list of possible - parameter objects. - - The tag will help client code to interpret (type) these parameter values. - - Arguments: - prefix -- the startswith pattern for parameter values - - Returns the tagged list of matching parameters as a tuple. - """ - return DtshAutocomp.MODE_ANY, [] - - @abstractmethod - def execute(self, vt: DtshVt) -> None: - """Execute the shell command. - - Arguments: - vt -- where the command will write its output - - Raises DtshError when the command execution has failed. - """ - - -class DtshUname(object): - """System information inferred from environment variables, - CMake cached variables and Zephyr's Git repository state. - - All paths are resolved (absolute, resolving any symlinks, - “..” components are also eliminated). - """ - - # Resolved DTS file path. - _dts_path: str - - # Resolved binding directories. - _binding_dirs: List[str] - - # Resolved $ZEPHYR_BASE. - _zephyr_base: Union[str, None] - - # Resolved $ZEPHYR_SDK_INSTALL_DIR. - _zephyr_sdk_dir: Union[str, None] - - # Cached $ZEPHYR_SDK_INSTALL_DIR/sdk_version file content. - _zephyr_sdk_version: Union[str, None] - - # Resolved $GNUARMEMB_TOOLCHAIN_PATH. - _gnuarm_dir: Union[str, None] - - # $ZEPHYR_TOOLCHAIN_VARIANT ('gnuarmemb' or 'zephyr'). - _zephyr_toolchain: Union[str, None] - - # git -C $ZEPHYR_BASE log -n 1 --pretty=format:"%h" - _zephyr_rev: Union[str, None] - - # git tag --points-at HEAD - _zephyr_tags: List[str] - - # Resolved BOARD_DIR (CMake). - _board_dir: Union[str, None] - - # CMake cached variables. - _cmake_cache: CMakeCache - - def __init__(self, dts_path:str, binding_dirs: Optional[List[str]]) -> None: - """Initialize system info. - - Arguments: - dts_path -- Path to a devicetree source file. - binding_dirs -- List of path to search for DT bindings. - If unspecified, and ZEPHYR_BASE is set, - defaults to Zephyr's DT bindings. - - Raises DtshError when a specified path is invalid. - """ - try: - self._dts_path = str(Path(dts_path).resolve(strict=True)) - except FileNotFoundError as e: - raise DtshError(f"DTS file not found: {dts_path}", e) - - self._binding_dirs = [] - self._zephyr_tags = [] - self._zephyr_base = None - self._zephyr_sdk_dir = None - self._zephyr_sdk_version = None - self._gnuarm_dir = None - self._zephyr_toolchain = None - self._zephyr_rev = None - self._board_dir = None - - self._load_environment() - self._load_cmake_cache() - - if self._zephyr_base: - git = Git() - self._zephyr_rev = git.get_head_commit(self._zephyr_base) - self._zephyr_tags = git.get_head_tags(self._zephyr_base) - - if binding_dirs: - for binding_dir in binding_dirs: - path = Path(binding_dir).resolve() - if os.path.isdir(path): - self._binding_dirs.append(str(path)) - else: - raise DtshError(f"Bindings directory not found: {binding_dir}") - elif self._zephyr_base: - self._init_zephyr_bindings_search_path() - - @property - def dts_path(self) -> str: - """Returns the resolved path to the session's DT source file. - """ - return self._dts_path - - @property - def dt_binding_dirs(self) -> List[str]: - """Returns the DT bindings search path as a list of resolved path. - - When no bindings are specified by the dtsh command line, - and the environment variable ZEPHYR_BASE is set, - we'll try to default to the bindings Zephyr would use (has used) - at build-time. - - "Where are bindings located ?" specifies that binding files are - expected to be located in dts/bindings sub-directory of: - - the zephyr repository - - the application source directory - - the board directory - - any directories in DTS_ROOT - - any module that defines a dts_root in its build - - Walking through the modules' build settings seems a lot of work - (needs investigation, and confirmation that it's worth the effort), - but we'll at least try to include: - - $ZEPHYR_BASE/dts/bindings - - APPLICATION_SOURCE_DIR/dts/bindings - - BOARD_DIR/dts/bindings - - DTS_ROOT/**/dts/bindings - - This implies we get the value of the CMake cached variables - APPLICATION_SOURCE_DIR, BOARD_DIR and DTS_ROOT. - To invoke CMake, we'll first need a value for APPLICATION_BINARY_DIR: - we'll assume its the parent of the directory containing the DTS file, - as in /build/zephyr/zephyr.dts. - - If that fails: - - APPLICATION_SOURCE_DIR will default to $PWD - - we will substitute BOARD_DIR/dts/bindings with $ZEPHYR_BASE/boards - and $PWD/boards (we don't know if it's a Zephyr board or a custom board, - we don't know wich //dts/bindings subdirectory to select) - - Only directories that actually exist are included. - - See: - - $ZEPHYR_BASE/cmake/modules/dts.cmake - - https://docs.zephyrproject.org/latest/build/dts/bindings.html#where-bindings-are-located - """ - return self._binding_dirs - - @property - def zephyr_base(self) -> Union[str, None]: - """Returns the resolved path to the Zephyr kernel repository set by - the environment variable ZEPHYR_BASE, or None if unset. - """ - return self._zephyr_base - - @property - def zephyr_toolchain(self) -> Union[str, None]: - """Returns the toolchain variant ('zephyr' or 'gnuarmemb') set by the - environment variable ZEPHYR_TOOLCHAIN_VARIANT, or None if unset. - """ - return self._zephyr_toolchain - - @property - def zephyr_sdk_dir(self) -> Union[str, None]: - """Returns resolved path the Zephyr SDK directory set by the environment - variable ZEPHYR_SDK_INSTALL_DIR, or None if unset. - """ - return self._zephyr_sdk_dir - - @property - def gnuarm_dir(self) -> Union[str, None]: - """Value of the environment variable GNUARMEMB_TOOLCHAIN_PATH, or None. - """ - """Returns the GCC Arm base directory set by the environment variable - GNUARMEMB_TOOLCHAIN_PATH, or None if unset. - """ - return self._gnuarm_dir - - @property - def zephyr_kernel_rev(self) -> Union[str, None]: - """Returns the Zephyr kernel revision as given by - git -C $ZEPHYR_BASE log -n 1 --pretty=format:"%h", - or None when unavailable. - """ - return self._zephyr_rev - - @property - def zephyr_kernel_tags(self) -> List[str]: - """Returns the Zephyr kernel tags for the current - repository state, as given by git tag --points-at HEAD, - or None when unavailable. - """ - return self._zephyr_tags - - @property - def zephyr_kernel_version(self) -> Union[str, None]: - """Returns the Zephyr kernel version tag for the current - repository state, e.g. 'zephyr-v3.1.0', - or None if the state does not match a tagged Zephyr kernel release. - """ - version = None - if self.zephyr_kernel_tags: - # Include stable and RC releases. - regex = re.compile(r'^zephyr-(v\d.\d.\d[rc\-\d]*)$') - for tag in self.zephyr_kernel_tags: - m = regex.match(tag) - if m: - version = tag - break - return version - - @property - def zephyr_sdk_version(self) -> Union[str, None]: - """Returns the Zephyr SDK version set in the file - $ZEPHYR_SDK_INSTALL_DIR/sdk_version, or None if unavailable. - """ - if self._zephyr_sdk_version is None: - if self._zephyr_sdk_dir: - path = os.path.join(self._zephyr_sdk_dir, 'sdk_version') - try: - with open(path, 'r') as f: - self._zephyr_sdk_version = f.read().strip() - except IOError: - # Silently fail. - pass - return self._zephyr_sdk_version - - @property - def gnuarm_version(self) -> Union[str, None]: - """Returns GCC Arm toolchain version, or None if unavailable. - """ - if self._gnuarm_dir: - return GCCArm(self._gnuarm_dir).version - return None - - @property - def board_dir(self) -> Union[str, None]: - """Returns the resolved path to the board directory set by - the CMake cached variable BOARD_DIR, or None if unavailable. - """ - return self._board_dir - - @property - def board(self) -> Union[str, None]: - """Returns the best guess fo the board (try BOARD environment variable - and CMake cache) or None if unavailable. - """ - # 1st, try environmant variable. - found_board = os.getenv('BOARD') - if not found_board: - # Then try CMake cache. - found_board = self._cmake_cache.get('BOARD') - if not found_board: - # More likely than above. - found_board = self._cmake_cache.get('CACHED_BOARD') - if (not found_board) and self.board_dir: - # Fallback: extract BOARD from BOARD_DIR - found_board = os.path.basename(self.board_dir) - return found_board - - @property - def board_dts_file(self) -> Union[str, None]: - """Returns the best guess for the the DTS file path (relies on - CMake cache), or None if unavailable. - """ - if self.board_dir and self.board: - path = os.path.join(self.board_dir, f'{self.board}.dts') - if os.path.isfile(path): - return path - return None - - @property - def board_binding_file(self) -> Union[str, None]: - """Returns the best guess for the board binding file path (relies on - CMake cache), or None if unavailable. - """ - if self.board_dir and self.board: - path = os.path.join(self.board_dir, f'{self.board}.yaml') - if os.path.isfile(path): - return path - return None - - def _load_environment(self) -> None: - env = os.getenv('ZEPHYR_BASE') - if env: - path = Path(env).resolve() - self._zephyr_base = str(path) - env = os.getenv('ZEPHYR_SDK_INSTALL_DIR') - if env: - path = Path(env).resolve() - self._zephyr_sdk_dir = str(path) - env = os.getenv('GNUARMEMB_TOOLCHAIN_PATH') - if env: - path = Path(env).resolve() - self._gnuarm_dir = str(path) - self._zephyr_toolchain = os.getenv('ZEPHYR_TOOLCHAIN_VARIANT') - - def _load_cmake_cache(self) -> None: - # self._dts_path is already resolved. - dts_dir = os.path.dirname(self._dts_path) - if os.path.isdir(dts_dir): - build_dir = str(Path(dts_dir).parent.absolute()) - self._cmake_cache = CMakeCache(build_dir) - - def _init_zephyr_bindings_search_path(self) -> None: - if not self._zephyr_base: - return - # self._zephyr_base is already resolved. - path = Path(os.path.join(self._zephyr_base, 'dts', 'bindings')) - self._binding_dirs.append(str(path)) - - app_src_dir = self._cmake_cache.get('APPLICATION_SOURCE_DIR') - if not app_src_dir: - # APPLICATION_SOURCE_DIR will default to $PWD. - app_src_dir = os.getcwd() - path = Path(os.path.join(app_src_dir, 'dts', 'bindings')).resolve() - if os.path.isdir(path): - self._binding_dirs.append(str(path)) - - board_dir = self._cmake_cache.get('BOARD_DIR') - if board_dir: - board_path = Path(board_dir).resolve() - self._board_dir = str(board_path) - binding_path = Path(os.path.join(board_dir, 'dts', 'bindings')).resolve() - if os.path.isdir(binding_path): - self._binding_dirs.append(str(binding_path)) - else: - # When BOARD_DIR is unset, we add both $ZEPHYR_BASE/boards - # and $PWD/boards (we don't know if it's a Zephyr board - # or a custom board). - # - # ISSUE: may we have multiple YAML binding files with the same name, - # but for different boards (in different directories) ? - path = Path(os.path.join(self._zephyr_base, 'boards')).resolve() - if os.path.isdir(path): - self._binding_dirs.append(str(path)) - path = Path(os.path.join(os.getcwd(), 'boards')).resolve() - if os.path.isdir(path): - self._binding_dirs.append(str(path)) - - dts_root = self._cmake_cache.get('DTS_ROOT') - if dts_root: - # Append all DTS_ROOT/**/dts/bindings we find. - for root, _, _ in os.walk(dts_root): - path = Path(os.path.join(root, 'dts', 'bindings')).resolve() - if os.path.isdir(path): - self._binding_dirs.append(str(path)) - - -class Dtsh(object): - """Shell-like interface to a devicetree. - - The global metaphor is: - - a filesystem-like view of the devicetree model - - a command string interface to POSIX-like shell commands (aka built-ins) - """ - - API_VERSION = '0.1.0b2' - """API version for the dtsh module. - - Should match 'version' in setup.py. - """ - - # Devicetree model (edtlib). - _edt: EDT - - # Current working node. - _cwd: Node - - # Built-in commands. - _builtins: Dict[str, DtshCommand] - - # Cached bindings map. - _bindings: Dict[str, Binding] - - # Cached available DT binding paths (including YAML files that do - # not describe a compatible). - # Memory trade-off: this map may contain about 2000 entries. - _binding2path: Dict[str, str] - - # Sysinfo. - _uname: DtshUname - - def __init__(self, edt: EDT, uname: DtshUname) -> None: - """Initialize a shell-like interface to a devicetree. - - The current working node is initialized to the devicetree's root. - - The built-in list is empty. - - Arguments: - edt -- devicetree model (sources and bindings), provided by edtlib - uname -- system information inferred from environment variables, - CMake cached variables, Zephyr's Git repository state. - """ - self._edt = edt - self._uname = uname - self._cwd = self._edt.get_node('/') - self._builtins = {} - self._bindings = {} - self._binding2path = {} - self._init_binding_paths() - self._init_bindings() - - @property - def uname(self) -> DtshUname: - """System information inferred from environment variables, - CMake cached variables and Zephyr's Git repository state. - - This is the system information used to initialize the - devicetree and its bindings. - """ - return self._uname - - @property - def cwd(self) -> Node: - """Current working node. - """ - return self._cwd - - @property - def pwd(self) -> str: - """Current working node's path. - """ - return self._cwd.path - - @property - def builtins(self) -> List[DtshCommand]: - """Available shell built-ins as a list. - """ - return [cmd for _, cmd in self._builtins.items()] - - @property - def dt_bindings(self) -> Dict[str, Binding]: - """Map each compatible to its binding. - - This collection should include all compatibles that are both: - - matched (by a node's "compatible" property) - - described (by a corresponding YAML file) - - However, the current implementation of the devicetree model initialization - may filter out bindings that never appear first (i.e. as most specific) - in the "compatible" list of a node. - For example, the binding for "nordic,nrf-swi" is likely to - always be masked by a more specific compatible, e.g. "nordic,nrf-egu". - """ - return self._bindings - - def dt_binding(self, compat: str) -> Union[Binding, None]: - """Access bindings by their compatible. - - See Dtsh.dt_bindings() for limitations. - - Arguments: - compat -- a compatible (DTSpec 2.3.1) - - Returns the binding describing this compatible, - or None when this compatible is either unmatched or not described. - """ - return self._bindings.get(compat) - - def dt_binding_path(self, fname: str) -> Union[str, None]: - """Search binding directories for a given DT specification file name. - - Contrary to the Dtsh.dt_binding() API, this search is not limited - to bindings that describe a compatible. - - Arguments - fname -- the YAML file name, e.g. "nordic,nrf-swi.yaml" - - Returns the full path of the YAML file, or None when not found. - """ - return self._binding2path.get(fname) - - @property - def dt_aliases(self) -> Dict[str, Node]: - aliases: Dict[str, Node] = {} - for alias, dt_node in self._edt._dt.alias2node.items(): - edt_node = self._edt.get_node(dt_node.path) - aliases[alias] = edt_node - return aliases - - @property - def dt_chosen(self) -> Dict[str, Node]: - return self._edt.chosen_nodes - - def builtin(self, name: str) -> Union[DtshCommand, None]: - """Access a built-in by command name. - - Arguments: - name -- a command name - - Returns None if this built-in name is not supported by this command. - """ - return self._builtins.get(name) - - def realpath(self, path: str) -> str: - """Resolve a node's path. - - The devicetree's root path resolves to '/'. - - An absolute path resolves to itself. - - When the path starts with '.', wildcard substitution occurs: - - a leading '.' represents the current working node - - a leading '..' represents the current working node's parent; - the devicetree's root is its own parent - - Otherwise, path is concatenated to the current working node's path. - - Path resolution will always: - - strip any trailing '/' (excepted for the devicetree's root) - - preserve any trailing wildcard ('*') - - See also: man realpath(1), but here none of the path components - is required to exist (i.e. to actually represent a devicetree node). - - Arguments: - path -- the node's path to resolve - - Returns the resolved path. - - Raises ValueError when path is unspecified. - """ - if not path: - raise ValueError('path must be specified') - - if path.startswith('/'): - # The devicetree's root path resolves to '/'. - # A path which starts with '/' resolves to itself but any trailing '/. - if path.endswith('/') and len(path) > 1: - path = path[:-1] - else: - if path.startswith('.'): - # Wildcard substitution. - if path.startswith('..'): - dirpath = Dtsh.dirname(self.pwd) - path_trailing = path[2:] - else: - dirpath = self.pwd - path_trailing = path[1:] - # Handle '../dir' and './dir' (typical case). - if path_trailing.startswith('/'): - path_trailing = path_trailing[1:] - - if path_trailing: - path = Dtsh.path_concat(dirpath, path_trailing) - else: - path = dirpath - else: - # Otherwise, path is concatenated to the current - # working node's path. - path = Dtsh.path_concat(self.pwd, path) - - return path - - def path2node(self, path:str) -> Node: - """Access devicetree nodes by path. - - Arguments: - path -- an absolute devicetree node's path - - Returns a devicetree node (EDT). - - Raises: - - ValueError when path is unspecified - - DtshError when path does not represent an actual devicetree node - """ - if not path: - raise ValueError('path must be specified') - - try: - return self._edt.get_node(path) - except EDTError as e: - raise DtshError(f'no such node: {path}', e) - - def isnode(self, path: str) -> bool: - """Answsers whether a path represents an actual devicetree node. - - Arguments: - path -- an absolute devicetree node's path - - Raises: - - ValueError when path is unspecified - """ - try: - self._edt.get_node(path) - return True - except EDTError: - pass - return False - - def cd(self, path: str) -> None: - """Change the current working node. - - The path is first resolved (see `Dtsh.realpath()`). - - Arguments: - path -- a node's path, either absolute or relative - - Raises: - - ValueError when path is unspecified - - DtshError when the destination node does not exist - """ - path = self.realpath(path) - self._cwd = self.path2node(path) - - def ls(self, path: str) -> List[Node]: - """List a devicetree node's children. - - The path is first resolved (see `Dtsh.realpath()`) to: - - [] - - := [/][]'*' - - Filtering is thereby applied if the resolved path ends - with a trailing '*', such that: - - ls('/[]*') - - will list the children of the node with path '', - whose name starts with prefix ''. - - When 'prefix' is not set, - - ls('/*') - - is equivalent to: - - ls('') - - Note that - - ls('/parent*') - - is interpreted as - - ls('/*') - - with '' equals to 'parent', even if '/parent' is the path - to an actual devicetree node. - - Arguments: - path -- a node's path, either absolute or relative - - Returns the listed nodes. - - Raises: - - ValueError when path is unspecified - - DtshError on devicetree node path's resolution failure - """ - path = self.realpath(path) - - if path.endswith('*'): - dirpath = Dtsh.dirname(path) - prefix = path[:-1] - return [n for n in self.ls(dirpath) if n.path.startswith(prefix)] - else: - dirnode = self.path2node(path) - return list(dirnode.children.values()) - - def exec_command_string(self, cmd_str: str, vt: DtshVt) -> None: - """Execute a command string. - - Note that the command string content after any '--' token - will be interpreted as command parameters. - - See: - - https://docs.python.org/3.9/library/getopt.html - - Arguments: - cmd_str -- the command string in GNU getopt format - vt -- where the command will write its output, - or None for quiet execution - - Raises: - - DtshCommandNotFoundError when the requested command is not supported - - DtshCommandUsageError when the command string does not match - the associated GNU getopt usage - - DtshCommandFailedError when the command execution has failed - """ - if not cmd_str: - return - - cmdline_vstr = cmd_str.strip().split() - cmd_name = cmdline_vstr[0] - - cmd = self._builtins.get(cmd_name) - if not cmd: - raise DtshCommandNotFoundError(cmd_name) - - # Set command options and parameters (raises DtshCommandUsageError). - cmd_argv = cmdline_vstr[1:] - cmd.parse_argv(cmd_argv) - - try: - cmd.execute(vt) - except DtshError as e: - raise DtshCommandFailedError(cmd, e.msg, e) - - @staticmethod - def is_node_enabled(node: Node): - """Returns True if the node is enabled according to its status. - """ - return node.status in ['ok', 'okay'] - - @staticmethod - def nodename(path: str) -> str: - """Strip directory and suffix ('/') components from a node's path. - - See also: man basename(1) - - Arguments: - path -- a node's path, either absolute or relative - - Returns path with any leading directory components removed. - - Raises ValueError when path is unspecified. - """ - if not path: - raise ValueError('path must be specified') - - if path == '/': - return '/' - - x = path.rfind('/') - if x < 0: - return path - - if path.endswith('/'): - path = path[:-1] - x = path.rfind('/') - - return path[x+1:] - - @staticmethod - def dirname(path: str) -> str: - """Strip last component from a node's name. - - See also: man dirname(1) - - Arguments: - path -- a node's path, either absolute or relative - - Returns path with its last non-slash component and trailing slashes removed, - or '.' when path does not contain any '/'. - - Raises ValueError when path is unspecified. - """ - if not path: - raise ValueError('path must be specified') - - x = path.rfind('/') - if x == 0: - # dirname('/[]') = '/' - return '/' - if x > 0: - # dirname('/[]') = '' - if path.endswith('/'): - x = path.rfind('/', 0, len(path) - 1) - return path[0:x] - - # Path does not contain any '/'. - return '.' - - @staticmethod - def path_concat(path_prefix: str, path: str) -> str: - """Devicetree node path concatenation. - - This helper will: - - assert path is relative (does not start with '/') - - append '/' to path_prefix when appropriate - - drop any trailing '/' from path - - Arguments: - path_prefix -- the leading path prefix - path -- the relative path to concatenate - - Returns the resulting path. - - Raises ValueError when path_prefix or path is unspecified, - or when path starts with '/'. - """ - if not path_prefix: - raise ValueError('path prefix must specified') - if not path: - raise ValueError('path must specified') - if path.startswith('/'): - raise ValueError('path must be relative') - - if not path_prefix.endswith('/'): - path_prefix += '/' - if path.endswith('/'): - path = path[:-1] - - return path_prefix + path - - def _init_bindings(self) -> None: - # EDT.compat2nodes includes all compatibles matched by a devicetree node. - # See also EDT._init_luts(). - for compat, nodes in self._edt.compat2nodes.items(): - # A compatible may not map to any binding in the devicetree - # underlying model: - # - a compatible that represents a board, for which the binding - # is looked up with the board identifier, and describes the board - # itself (e.g. architecture, supported toolchains and Zephyr subsystems) - # and not a devicetree content; for example, the board identified - # by "nrf52840dk_nrf52840" is described by its binding file nrf52840dk_nrf52840.yaml, - # while its DTS file nrf52840dk_nrf52840.dts will set the - # compatible property of the devicetree root node to "nordic,nrf52840-dk-nrf52840" - # - a compatible somewhat part of the DT core specifications - # (e.g. "simple-bus", DTSpec 4.5) - # - a compatible that does not define any property beside those - # inherited from the base bindings (e.g. "arm,armv7m-systick") - # - typically a compatible that isn't described by any YAML file - # - # See also edtlib.Binding.compatible: - # For example, it's None when the Binding is inferred - # from node properties. It can also be None for Binding objects - # created using 'child-binding:' with no compatible. - binding = None - for node in nodes: - # There are handfull of issues here: - # - we access the private member edtlib.Node._binding, - # and assume Node.matching_compat will equal to - # Node._binding.compatible wherever a node has a binding - # - filtering by Node.matching_compat may filter out - # compatibles that are actually matched by devicetree nodes; - # e.g. the compatible "nordic,nrf-swi" that's matched by - # nodes with the more specific compatible "nordic,nrf-egu" - # will remain undefined despite the proper binding file - # (nordic,nrf-swi.yaml) being available - # - not filtering on Node.matching_compat would /define/ - # inconsistent bindings, e.g. the compatible "nordic,nrf-swi" - # would bind with nordic,nrf-egu.yaml - # - # See also edtlib.EDT._init_compat2binding() - if node._binding and (node.matching_compat == compat): - binding = node._binding - break - if not binding: - # We may have missed a binding for a compatible that never - # appears as the most specific (see above): if a corresponding - # YAML file seems to actually exist, try to instantiate an - # out-of-devicetree Binding. - path = self.dt_binding_path(f'{compat}.yaml') - if path: - # WARNING: this may fail with an exception, - # for now let it crash to better know how and when. - binding = Binding(path, self._binding2path) - if binding: - self._bindings[compat] = binding - - def _init_binding_paths(self) -> None: - # Mostly duplicates code from edtlib._binding_paths() - # and edtlib.EDT._init_compat2binding(). - yaml_paths: List[str] = [] - for bindings_dir in self._edt.bindings_dirs: - for root, _, filenames in os.walk(bindings_dir): - for filename in filenames: - if filename.endswith(".yaml") or filename.endswith(".yml"): - yaml_paths.append(os.path.join(root, filename)) - for path in yaml_paths: - self._binding2path[os.path.basename(path)] = path - - -class DtshAutocomp(object): - """Devicetree shell command line completer. - - Usually associated to the shell session's input buffer - shared with GNU readline. - - The auto-completion state machine is made of: - - the completion state, which is the sequence of possible input strings - (hints) matching a given prefix - - a model, which is the list of the actual possible objects matching the - given prefix - - a mode, that tags the model semantic (may help client code to avoid - calling isinstance()) - """ - - MODE_ANY: ClassVar[int] = 0 - MODE_DTSH_CMD: ClassVar[int] = 1 - MODE_DTSH_OPT: ClassVar[int] = 2 - MODE_DTSH_PAGE: ClassVar[int] = 3 - MODE_DT_NODE: ClassVar[int] = 4 - MODE_DT_PROP: ClassVar[int] = 5 - MODE_DT_BINDING: ClassVar[int] = 6 - - @property - @abstractmethod - def count(self) -> int: - """Current completions count. - """ - - @property - @abstractmethod - def hints(self) -> List[str]: - """Current completion state. - - This is the list of completion strings that match the last prefix - provided to autocomplete(). - """ - - @property - @abstractmethod - def model(self) -> List[Any]: - """Current completion model. - - This is the model objects correponding to the current completion hints. - - Permits rich implementation of the rl_completion_display_matches_hook() - callback. - """ - - @property - @abstractmethod - def mode(self) -> int: - """Current completion mode. - - Tag describing the current completion model. - """ - - @abstractmethod - def reset(self) -> None: - """Reset current completion state and model. - """ - - @abstractmethod - def autocomplete(self, - cmdline: str, - prefix: str, - cursor: int = -1) -> List[str]: - """Auto-complete command line. - - Arguments: - cmdline -- the command line's current content - prefix -- the prefix word to complete - cursor -- required to get the full command line's state, - but unsupported - - Returns the completion state (hint strings) matching the prefix. - """ - - @staticmethod - def autocomplete_with_nodes(prefix: str, shell: Dtsh) -> List[Node]: - """Helper function to auto-complete with a list of nodes. - - Arguments: - prefix -- the node path prefix - shell -- the shell instance the nodes belong to - - Returns a list of matching nodes. - """ - completions: List[Node] = [] - - if prefix: - path_prefix = shell.realpath(prefix) - if prefix.endswith('/'): - path = path_prefix - else: - path = Dtsh.dirname(path_prefix) - else: - path_prefix = shell.pwd - path = shell.pwd - - try: - roots = [n for n in shell.ls(path) if n.path.startswith(path_prefix)] - for child in roots: - if len(child.path) > len(path_prefix): - completions.append(child) - except DtshError: - # No completions for invalid path. - pass - - return completions - - @staticmethod - def autocomplete_with_properties(node_prefix: str, - prop_prefix: str, - shell: Dtsh) -> List[Property]: - - completions: List[Property] = [] - path_prefix = shell.realpath(node_prefix) - if shell.isnode(path_prefix): - node = shell.path2node(path_prefix) - for _, p in node.props.items(): - if p.name.startswith(prop_prefix) and len(p.name) > len(prop_prefix): - completions.append(p) - return completions - - -class DtshError(Exception): - """Base exception for devicetree shell errors. - """ - - _msg: Union[str, None] - _cause: Union[Exception, None] - - def __init__(self, - msg: Optional[str], - cause: Optional[Exception] = None) -> None: - """Create an error. - - Arguments: - msg -- the error message - cause -- the exception that caused this error, if any - """ - super().__init__(msg) - self._msg = msg - self._cause = cause - - @property - def msg(self) -> str: - """The error message. - """ - return self._msg or '' - - @property - def cause(self) -> Union[Exception, None]: - """The error cause as an exception, or None. - """ - return self._cause - - -class DtshCommandUsageError(DtshError): - """A devicetree shell command execution has failed. - """ - - def __init__(self, - command: DtshCommand, - msg: Optional[str] = None, - cause: Optional[Exception] = None) -> None: - """Create a new error. - - Arguments: - command -- the failed command - msg -- a message describing the usage error - cause -- the cause exception, if any - """ - super().__init__(msg, cause) - self._command = command - - @property - def command(self): - """The failed command. - """ - return self._command - - -class DtshCommandFailedError(DtshError): - """A devicetree shell command execution has failed. - """ - - def __init__(self, - command: DtshCommand, - msg: str, - cause: Optional[Exception] = None) -> None: - """Create a new error. - - Arguments: - command -- the failed command - msg -- the failure message - cause -- the failure cause, if any - """ - super().__init__(msg, cause) - self._command = command - - @property - def command(self): - """The failed command. - """ - return self._command - - -class DtshCommandNotFoundError(DtshError): - """The requested command is not supported by this devicetree shell. - """ - - def __init__(self, name: str) -> None: - """Create a new error. - - Arguments: - name -- the command name - """ - super().__init__(f'command not found: {name}') - self._name = name - - @property - def name(self): - """The not supported built-in name. - """ - return self._name - - -class DtshSession(object): - """Interactive devicetree shell session. - """ - - @property - @abstractmethod - def shell(self) -> Dtsh: - """The session's shell. - """ - - @property - @abstractmethod - def vt(self) -> DtshVt: - """The session's VT. - """ - - @property - @abstractmethod - def autocomp(self) -> DtshAutocomp: - """The session's command line completer. - """ - - @property - @abstractmethod - def last_err(self) -> Union[DtshError, None]: - """Last error triggered by a command execution. - """ - - @abstractmethod - def run(self): - """Enter interactive mode main loop. - """ - - @abstractmethod - def close(self) -> None: - """Close session, leaving interactive mode. - """ diff --git a/src/dtsh/io.py b/src/dtsh/io.py new file mode 100644 index 0000000..85c6c28 --- /dev/null +++ b/src/dtsh/io.py @@ -0,0 +1,259 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""I/O streams for devicetree shells. + +- "/dev/null" I/O semantic: DTShOutput, DTShInput +- base terminal API: DTShVT +- parsed command redirection: DTShRedirection +- command redirection to file: DTShOutputFile + +Unit tests and examples: tests/test_dtsh_io.py +""" + +from typing import Any, IO, Tuple + +import os +import sys + +from dtsh.config import DTShConfig + +_dtshconf: DTShConfig = DTShConfig.getinstance() + + +class DTShOutput: + """Base for shell output streams. + + This base implementation behaves like "/dev/null". + """ + + def write(self, *args: Any, **kwargs: Any) -> None: + """Write to output stream. + + Args: + *args: Positional arguments. + Semantic depends on the actual concrete stream. + **kwargs: Keyword arguments. + Semantic depends on the actual concrete stream. + """ + + def flush(self) -> None: + """Flush output stream. + + Semantic depends on the actual concrete stream. + """ + + def pager_enter(self) -> None: + """Page output until a call to pager_exit().""" + + def pager_exit(self) -> None: + """Stop paging output.""" + + +class DTShInput: + """Base for shell input streams. + + This base implementation behaves like "/dev/null". + """ + + def readline(self, prompt: str = "") -> str: + """Print the prompt and read a command line. + + Args: + prompt: The command line prompt to use. + Defaults to an empty string. + """ + raise EOFError() + + +class DTShVT(DTShInput, DTShOutput): + """Base terminal for devicetree shells. + + This base implementation writes to stdout, reads from stdin + and ignores paging. + """ + + def readline(self, prompt: str = "> ") -> str: + r"""Print the prompt and read a command line. + + To use ANSI escape codes in the prompt without breaking + the GNU readline cursor position, please protect SGR parameters + with RL_PROMPT_{START,STOP}_IGNORE markers: + + := m + := + + := \001 + := \002 + + Overrides DTShInput.readline(). + + Args: + prompt: The command line prompt to use. + """ + return input(prompt) + + def write(self, *args: Any, **kwargs: Any) -> None: + """Write to stdout. + + Overrides DTShOutput.write(). + + Args: + *args: Positional arguments, standard Python print() semantic. + **kwargs: Keyword arguments, standard Python print() semantic. + """ + print(*args, **kwargs) + + def flush(self) -> None: + """Flush stdout. + + Overrides DTShOutput.flush(). + """ + sys.stdout.flush() + + def clear(self) -> None: + """Clear VT. + + Ignored by base VT. + + NOTE: duplicate implementation from rich.Console.clear() ? + """ + + +class DTShOutputFile(DTShOutput): + """Output file for command output redirection.""" + + _out: IO[str] + + def __init__(self, path: str, append: bool) -> None: + """Initialize output file. + + Args: + path: The output file path. + append: Whether to redirect the command's output in "append" mode. + + Raises: + DTShRedirect.Error: Invalid path or permission errors. + """ + try: + # We can't use a context manager here, we just want to open + # the file for later subsequent writes. + self._out = open( # pylint: disable=consider-using-with + path, + "a" if append else "w", + encoding="utf-8", + ) + if append: + # Insert blank line between command outputs. + self._out.write(os.linesep) + except OSError as e: + raise DTShRedirect.Error(e.strerror) from e + + def write(self, *args: Any, **kwargs: Any) -> None: + """Write to output file. + + Overrides DTShOutput.write(). + + Args: + *args: Positional arguments, standard Python print(file=) semantic. + **kwargs: Keyword arguments, standard Python print(file=) semantic. + """ + print(*args, **kwargs, file=self._out) + + def flush(self) -> None: + """Close output file. + + Overrides DTShOutput.flush(). + """ + self._out.close() + + +class DTShRedirect(DTShOutput): + """Setup command output redirection.""" + + class Error(BaseException): + """Failed to setup redirection stream.""" + + @classmethod + def parse_redir2(cls, redir2: str) -> Tuple[str, bool]: + """Parse redirection stream into path and mode. + + Does not validate the path. + + Args: + redir2: The redirection expression, starting with ">". + + Returns: + Path and mode. + + Raises: + DTShRedirect.Error: Invalid redirection string. + """ + if not redir2.startswith(">"): + raise DTShRedirect.Error(f"invalid redirection: '{redir2}'") + + if redir2.startswith(">>"): + append = True + path = redir2[2:].strip() + else: + append = False + path = redir2[1:].strip() + + if not path: + raise DTShRedirect.Error( + "don't know where to redirect the output to ?" + ) + + return (path, append) + + _path: str + _append: bool + _out: IO[str] + + def __init__(self, redir2: str) -> None: + """New redirection. + + Args: + redir2: The redirection expression, starting with ">" or ">>", + followed by a file path. + + Raises: + DTShRedirect.Error: Forbidden spaces or overwrite. + """ + path, append = self.parse_redir2(redir2) + + if _dtshconf.pref_fs_no_spaces and " " in path: + raise DTShRedirect.Error( + f"spaces not allowed in redirection: '{path}'" + ) + + if path.startswith("~"): + # abspath() won't expand a leading "~". + path = path.replace("~", os.path.expanduser("~"), 1) + path = os.path.abspath(path) + + if os.path.isfile(path): + if _dtshconf.pref_fs_no_overwrite: + raise DTShRedirect.Error(f"file exists: '{path}'") + else: + # We won't actually append if the file does not exist, + # checking this here will help actual DTShOutput implementations + # to do the right thing. + append = False + + self._append = append + self._path = path + + @property + def append(self) -> bool: + """Whether to redirect output in append-mode. + + Can be true only when the output file actually already exists. + """ + return self._append + + @property + def path(self) -> str: + """Redirection real path.""" + return self._path diff --git a/src/dtsh/man.py b/src/dtsh/man.py deleted file mode 100644 index ec9d4b0..0000000 --- a/src/dtsh/man.py +++ /dev/null @@ -1,683 +0,0 @@ -# Copyright (c) 2022 Chris Duf -# -# SPDX-License-Identifier: Apache-2.0 - -"""Manual pages for devicetree shells.""" - -import re - -from abc import abstractmethod -from typing import List, Union - -from devicetree.edtlib import Binding - -from rich.console import RenderableType -from rich.markdown import Markdown -from rich.padding import Padding -from rich.table import Table -from rich.text import Text - -from dtsh.dtsh import Dtsh, DtshCommand, DtshVt -from dtsh.tui import DtshTui - - -class DtshManPage(object): - """Abstract manual page. - """ - - SECTION_DTSH = 'dtsh' - SECTION_COMPATS = 'Compatibles' - - _section: str - _page: str - _view: Table - - def __init__(self, section: str, page: str) -> None: - """Create a manual page. - - Arguments: - section -- the manual section - page -- the manual page - """ - self._section = section - self._page = page - self._view = DtshTui.mk_grid(1) - self._view.expand = True - - @property - def section(self) -> str: - """The manual page's section. - """ - return self._section - - @property - def page(self) -> str: - """The manual page. - """ - return self._page - - def show(self, vt: DtshVt, no_pager: bool = False) -> None: - """Show this man page. - - Arguments: - vt -- the VT to show the man page on - no_pager -- print the man page without pager - """ - self._add_header() - self.add_content() - self._add_footer() - - if not no_pager: - vt.pager_enter() - vt.write(self._view) - if not no_pager: - vt.pager_exit() - - def _add_header(self) -> None: - """ - """ - bar = DtshTui.mk_grid_statusbar() - bar.add_row( - DtshTui.mk_txt_bold(self.section.upper()), - None, - DtshTui.mk_txt_bold(self.page.upper()) - ) - self._view.add_row(bar) - bar.add_row(None) - - def _add_footer(self) -> None: - """ - """ - bar = DtshTui.mk_grid_statusbar() - bar.add_row( - DtshTui.mk_txt_bold(Dtsh.API_VERSION), - DtshTui.mk_txt('Shell-like interface with devicetrees'), - DtshTui.mk_txt_bold('DTSH') - ) - self._view.add_row(bar) - - def _add_named_content(self, name:str, content: RenderableType) -> None: - self._view.add_row(DtshTui.mk_txt_bold(name.upper())) - self._view.add_row(Padding(content, (0,8))) - self._view.add_row(None) - - @abstractmethod - def add_content(self) -> None: - """Callback invoked by show() to setup view content. - """ - - -class DtshManPageBuiltin(DtshManPage): - """ - """ - - # Documented dtsh command. - _builtin: DtshCommand - - # Regexp for page sections. - _re: re.Pattern = re.compile('^[A-Z]+$') - - def __init__(self, builtin: DtshCommand) -> None: - super().__init__(DtshManPage.SECTION_DTSH, builtin.name) - self._builtin = builtin - - def add_content(self) -> None: - self._add_content_name() - self._add_content_synopsis() - self._add_markdown() - - def _add_content_name(self) -> None: - txt = DtshTui.mk_txt(self._builtin.name) - txt.append_text(Text(f' {DtshTui.WCHAR_HYPHEN} ', DtshTui.style_default())) - txt = DtshTui.mk_txt(self._builtin.desc) - self._add_named_content('name', txt) - - def _add_content_synopsis(self) -> None: - grid = DtshTui.mk_grid(1) - grid.add_row(DtshTui.mk_txt(self._builtin.usage)) - grid.add_row(None) - for opt in self._builtin.options: - grid.add_row(DtshTui.mk_txt_bold(opt.usage)) - grid.add_row(DtshTui.mk_txt(f' {opt.desc}')) - self._add_named_content('synopsis', grid) - - def _add_markdown(self) -> None: - content = self._builtin.__doc__ - if content: - content = content.strip() - content_vstr = content.splitlines() - # Skip until 1st section - for i, line in enumerate(content_vstr): - if self._is_section_header(line): - content_vstr = content_vstr[i:] - break - # Parse all sections. - sec_name: Union[str, None] = None - sec_vstr: Union[List[str], None] = None - for line in content_vstr: - line = line.rstrip() - if self._is_section_header(line): - # Add current section's content to view if any. - if sec_name and sec_vstr: - self._add_section(sec_name, sec_vstr) - # Init new section's content. - sec_vstr = [] - sec_name = line - else: - # Append line to current section. - if sec_vstr is not None: - sec_vstr.append(line) - - if sec_name and sec_vstr: - self._add_section(sec_name, sec_vstr) - - def _is_section_header(self, line: str) -> bool: - return self._re.match(line) is not None - - def _add_section(self, name: str, vstr: List[str]) -> None: - md_src = '\n'.join(vstr) - md = Markdown(md_src) - self._add_named_content(name, md) - - -class DtshManPageBinding(DtshManPage): - """ - """ - - _binding: Binding - - def __init__(self, binding: Binding) -> None: - super().__init__(DtshManPage.SECTION_COMPATS, binding.compatible) - self._binding = binding - - def add_content(self) -> None: - self._add_content_compat() - self._add_content_desc() - self._add_content_cell_specs() - self._add_content_bus() - self._add_content_properties() - self._add_content_binding() - - def _add_content_compat(self) -> None: - grid = DtshTui.mk_form() - grid.add_row(DtshTui.mk_txt('Compatible: '), - DtshTui.mk_txt_binding(self._binding)) - grid.add_row(DtshTui.mk_txt('Summary: '), - DtshTui.mk_txt_desc_short(self._binding.description)) - self._add_named_content('binding', grid) - - def _add_content_desc(self) -> None: - self._add_named_content('description', - DtshTui.mk_txt_desc(self._binding.description)) - - def _add_content_bus(self) -> None: - if not (self._binding.buses or self._binding.on_bus): - return - - if self._binding.buses: - str_label = "Nodes with this compatible's binding support buses" - str_bus = " ".join(self._binding.buses) - else: - str_label = "Nodes with this compatible's binding appear on bus" - str_bus = self._binding.on_bus - - txt = DtshTui.mk_txt(f'{str_label}: ') - txt.append_text( - DtshTui.mk_txt(str_bus, DtshTui.style(DtshTui.STYLE_DT_BUS)) - ) - self._add_named_content('bus', txt) - - def _add_content_cell_specs(self) -> None: - # Maps specifier space names (e.g. 'gpio') to list of - # cell names (e.g. ['pin', 'flags']). - spec_map = self._binding.specifier2cells - # Number of specifier spaces. - N = len(spec_map) - if N == 0: - return - grid = DtshTui.mk_grid(1) - i_spec = 0 - for spec_space, spec_names in spec_map.items(): - grid.add_row(f'{spec_space}-cells:') - for name in spec_names: - grid.add_row(f'- {name}') - if i_spec < (N - 1): - grid.add_row(None) - i_spec += 1 - self._add_named_content('cell specifiers', grid) - - def _add_content_properties(self) -> None: - # Maps property names to specifications (PropertySpec). - spec_map = self._binding.prop2specs - # Number of property specs. - N = len(spec_map) - if N == 0: - return - grid = DtshTui.mk_grid(1) - i_spec = 0 - for _, spec in spec_map.items(): - grid.add_row(DtshTui.mk_form_prop_spec(spec)) - if i_spec < (N - 1): - grid.add_row(None) - i_spec += 1 - self._add_named_content('properties', grid) - - def _add_content_binding(self) -> None: - self._add_named_content('binding', - DtshTui.mk_yaml_binding(self._binding)) - - -class DtshManPageDtsh(DtshManPage): - """ - """ - - # Regexp for page sections. - _re: re.Pattern = re.compile('^[A-Z]+$') - - def __init__(self) -> None: - super().__init__(DtshManPage.SECTION_DTSH, 'dtsh') - - def add_content(self) -> None: - self._add_content_as_md() - - def _add_content_as_md(self): - md_src = _DTSH_MAN_PAGE.strip() - md = Markdown(md_src) - self._view.add_row(Padding(md, (0,8))) - self._view.add_row(None) - - def _add_content_as_sections(self): - # Parse all sections. - sec_name: Union[str, None] = None - sec_vstr: Union[List[str], None] = None - content_vstr = _DTSH_MAN_PAGE.strip().splitlines() - for line in content_vstr: - line = line.rstrip() - if self._is_section_header(line): - # Add current section's content to view if any. - if sec_name and sec_vstr: - self._add_section(sec_name, sec_vstr) - # Init new section's content. - sec_vstr = [] - sec_name = line - else: - # Append line to current section. - if sec_vstr is not None: - sec_vstr.append(line) - - if sec_name and sec_vstr: - self._add_section(sec_name, sec_vstr) - - - def _is_section_header(self, line: str) -> bool: - return self._re.match(line) is not None - - def _add_section(self, name: str, vstr: List[str]) -> None: - md_src = '\n'.join(vstr) - md = Markdown(md_src) - self._add_named_content(name, md) - - -_DTSH_MAN_PAGE=""" -# dtsh - -[Home](https://github.com/dottspina/dtsh) [PyPI](https://pypi.org/project/devicetree/) [Known issues](https://github.com/dottspina/dtsh/issues) - -**dtsh** is an interactive *shell-like* interface with a devicetree and its bindings: - -- browse the devicetree through a familiar hierarchical file-system metaphor -- retrieve nodes and bindings with accustomed command names and command line syntax -- generate simple documentation artifacts by redirecting commands output to files (text, HTML, SVG) -- common command line interface paradigms (auto-completion, history) and keybindings - -## SYNOPSIS - -To start a shell session: `dtsh [] [*]` - -where: - -- ``: path to the device tree source file in [DTS Format](https://devicetree-specification.readthedocs.io/en/latest/chapter6-source-language.html) (`.dts`); - if unspecified, defaults to `$PWD/build/zephyr/zephyr.dts` -- ``: directory to search for [YAML](https://yaml.org/) binding files; - if unspecified, and the environment variable `ZEPHYR_BASE` is set, - defaults to [Zephyr’s bindings](https://docs.zephyrproject.org/latest/build/dts/bindings.html#where-bindings-are-located) - -ℹ See [Incomplete Zephyr bindings search path #1](https://github.com/dottspina/dtsh/issues/1) -for details and limitations. - -To open an arbitrary DTS file with custom bindings: - - $ dtsh /path/to/foobar.dts /path/to/custom/bindings /path/to/other/custom/bindings - -To open the same DTS file with Zephyr’s bindings: - - $ export ZEPHYR_BASE=/path/to/zephyr - $ dtsh /path/to/foobar.dts - -## THE SHELL - -`dtsh` defines a set of *built-in* commands that interface with a devicetree -and its bindings through a hierarchical file-system metaphor. - -### File system metaphor - -Within a `dtsh` session, a devicetree shows itself as a familiar hierarchical file-system, -where [path names](https://devicetree-specification.readthedocs.io/en/stable/devicetree-basics.html#path-names) -*look like* paths to files or directories, depending on the acting shell command. - -A current *working node* is defined, similar to any shell’s current working directory, -allowing `dtsh` to also support relative paths. - -A leading `.` represents the current working node, and `..` its parent. -The devicetree root node is its own parent. - -To designate properties, `dtsh` uses `$` as a separator between DT path names and [property names](https://devicetree-specification.readthedocs.io/en/stable/devicetree-basics.html#property-names) -(should be safe since `$` is an invalid character for both node and property names). - -Some commands support filtering or *globbing* with trailing wild-cards `*`. - -### Command strings - -The `dtsh` command string is based on the -[GNU getopt](https://www.gnu.org/software/libc/manual/html_node/Using-Getopt.html) syntax. - -#### Synopsis - -All built-ins share the same synopsis: - - CMD [OPTIONS] [PARAMS] - -where: - -- `CMD`: the built-in name, e.g. `ls` -- `OPTIONS`: the options the command is invoked with (see bellow), e.g. `-l` -- `PARAMS`: the parameters the command is invoked for, e.g. a path name - -`OPTIONS` and `PARAMS` are not positional: `ls -l /soc` is equivalent to `ls /soc -l`. - -#### Options - -An option may support: - -- a short name, starting with a single `-` (e.g. `-h`) -- a long name, starting with `--` (e.g. `--help`) - -Short option names can combine: `-lR` is equivalent to `-l -R`. - -An Option may also require an argument, e.g. `find /soc --interrupt 12`. - -Options semantic should be consistent across commands, e.g. `-l` always means *long format*. - -We also try to re-use *well-known* option names, e.g. `-r` for *reverse sort* or `-R` for *recursive*. - - -ℹ Trigger `TAB` completion after a single `-` to *pull* a summary -of a command's options, e.g: - -``` -❯ find -[TAB][TAB] --c print nodes count --q quiet, only print nodes count --l use rich listing format --f visible columns format string --h --help print usage summary ---name find by name ---compat find by compatible ---bus find by bus device ---interrupt find by interrupt ---enabled-only search only enabled nodes ---pager page command output -❯ find - -``` - -### Built-ins - - | Built-in | | - |------------+-------------------------------------------| - | alias | print defined aliases | - | chosen | print chosen configuration | - | pwd | print current working node's path | - | cd | change current working node | - | ls | list devicetree nodes | - | tree | list devicetree nodes in tree-like format | - | cat | concatenate and print devicetree content | - | find | find devicetree nodes | - | uname | print system information | - | man | open a manual page | - -Use `man ` to print a command's manual page, -e.g. `man ls`. - -### Manual pages - -As expected, the `man` command will open the manual page for the shell itself (`man dtsh`), -or one of its built-ins (e.g. `man ls`). - -Additionally, `man` can also open a manual page for a -[compatible](https://devicetree-specification.readthedocs.io/en/latest/chapter2-devicetree-basics.html#compatible), -which is essentially a view of its (YAML) bindings: e.g. `man --compat nordic,nrf-radio` - -`man` should eventually also serve as an entry point to external useful or normative documents, -e.g. the Devicetree Specifications or the Zephyr project’s documentation. - -### System information - -**dtsh** may also expose *system* information, including: - -- the Zephyr kernel version, e.g. `zephyr-3.1.0`, with a link to the corresponding - release notes when available -- board information, based on the content of its YAML binding file, - with a link to the corresponding documentation when the board - is [supported by Zephyr](https://docs.zephyrproject.org/latest/boards/index.html) -- the configured *toolchain*, either Zephyr SDK or GNU Arm Embedded - -For example: - - BOARD - Board directory: $ZEPHYR_BASE/boards/arm/nrf52840dk_nrf52840 - Name: nRF52840-DK-NRF52840 (Supported Boards) - Board: nrf52840dk_nrf52840 (DTS) - - nrf52840dk_nrf52840.yaml - - identifier: nrf52840dk_nrf52840 - name: nRF52840-DK-NRF52840 - type: mcu - arch: arm - ram: 256 - flash: 1024 - toolchain: - - zephyr - - gnuarmemb - - xtools - supported: - - adc - - arduino_gpio - - arduino_i2c - - arduino_spi - - ble - - counter - - gpio - - i2c - - i2s - - ieee802154 - - pwm - - spi - - usb_cdc - - usb_device - - watchdog - - netif:openthread - -Retrieving this information may involve environment variables (e.g. `ZEPHYR_BASE` -or `ZEPHYR_TOOLCHAIN_VARIANT`), CMake cached variables, `git` or GCC. - -Refer to `man uname` for details. - -### Find nodes - -The `find` command permits to search the devicetree by: - -- node names -- compatible strings -- bus devices -- interrupt names or numbers - -For example, the command line bellow would list all enabled bus devices -that generate IRQs : - - - ❯ find --enabled-only --bus * --interrupt * - -`find` is quite versatile and supports a handful of options. -Refer to its extensive manual page (`man find`). - -## USER INTERFACE - -The `dtsh` command line interface paradigms and keybindings should sound familiar. - -### The prompt - -The default shell prompt is ❯. -The line immediately above the prompt shows the current working node’s path. - - / - ❯ pwd - / - - / - ❯ cd /soc/i2c@40003000/bme680@76 - - /soc/i2c@40003000/bme680@76 - ❯ pwd - /soc/i2c@40003000/bme680@76 - -Pressing `C-d` (aka `CTRL-D`) at the prompt will exit the `dtsh` session. - -### Commands history - -Commands history is provided through GNU readline integration. - -At the shell prompt, press: - -- up arrow (↑) to navigate the commands history backward -- down arrow (↓) to navigate the commands history forward -- `C-r` (aka `CTRL-R`) to search the commands history - -The history file (typically `$HOME/.config/dtsh/history`) is saved on exit, and loaded on startup. - -### Auto-completion - -Command line auto-completion is provided through GNU readline integration. - -Auto-completion is triggered by first pressing the `TAB` key twice, -then once for subsequent completions of the same command line, and may apply to: - -- command names (aka built-ins) -- command options -- command parameters - -### The pager - -Built-ins that may produce large outputs support the `--pager` option: the command’s -output is then *paged* using the system pager, typically `less`: - -- use up (↑) and down (↓) arrows to navigate line by line -- use page up (⇑) and down (⇓) to navigate *window* by *window* -- press `g` go to first line -- press `G` go to last line -- press `/` to enter search mode -- press `h` for help -- press `q` to quit the pager and return to the `dtsh` prompt - -On the contrary, the `man` command uses the pager by default -and defines a `--no-pager` option to disable it. - -### External links - -`dtsh` commands output may contain links to external documents such as: - -- the local YAML binding files, that should open in the system’s - default text editor -- the Devicetree specifications or the Zephyr project’s documentation, - that should open in the system’s default web browser - -How these links will appear in the console, and whether they are *actionable* or not, -eventually depend on the terminal and the desktop environment. - -This is an example of such links: [Device Tree What It Is](https://elinux.org/Device_Tree_What_It_Is) - -ℹ In particular, the environment may assume DTS files are DTS audio streams -(e.g. the VLC media player could have registered itself for handling the `.dts` file extension). -In this case, the external link won't open, possibly without any error message. -A work-around is to configure the desktop environment to open DTS files with -a text editor (e.g. with the *Open with* paradigm). - -### Output redirection - -Command output redirection uses the well-known syntax: - - CMD [OPTIONS] [PARAMS] > PATH - -where `PATH` is the absolute or relative path to the file the command output will be redirected to. - -Depending on the extension, the command output may be saved as an HTML page (`.html`), an SVG image (`.svg`), -or a text file (default). - -For example: - - / - ❯ ls -l soc > soc.html - -### Keybindings - -Familiar keybindings are set through GNU readline integration. - -- `C-l` clear terminal screen -- `C-a` move cursor to beginning of command line -- `C-e` move cursor to end of command line -- `C-k` *kill* text from cursor to end of command line -- `M-d` *kill* word at cursor -- `C-y` *yank* (paste) the content of the *kill buffer* -- `C-←` move cursor one word backward -- `C-→` move cursor one word forward -- `↑` navigate the commands history backward -- `↓` navigate the commands history forward -- `C-r` search the commands history -- `TAB` trigger auto-completion - -### Theme - -Colors and such are subjective, and most importantly the rendering will -eventually depend on the terminal’s font and palette, -possibly resulting in severe accessibility issues, e.g. grey text on white background -or a weird shell prompt. - -In such situations, or to accommodate personal preferences, users can try to override -`dtsh` colors (and prompt) by creating a *theme* file (typically `$HOME/.config/dtsh/theme`). - -Use the [default theme](https://github.com/dottspina/dtsh/blob/main/src/dtsh/theme) as template: - - cp src/dtsh/theme ~/.config/dtsh/theme - -## References - -**Devicetree Specifications** - -- [Online Devicetree Specifications](https://devicetree-specification.readthedocs.io/en/latest/) (latest) -- [Online Devicetree Specifications](https://devicetree-specification.readthedocs.io/en/stable/) (stable) - -**Zephyr** - -- [Introduction to devicetree](https://docs.zephyrproject.org/latest/build/dts/intro.html) -- [Devicetree bindings](https://docs.zephyrproject.org/latest/build/dts/bindings.html) -- [Bindings index](https://docs.zephyrproject.org/latest/build/dts/api/bindings.html) -- [Zephyr-specific chosen nodes](https://docs.zephyrproject.org/latest/build/dts/api/api.html#zephyr-specific-chosen-nodes) -- [Devicetree versus Kconfig](https://docs.zephyrproject.org/latest/build/dts/dt-vs-kconfig.html) - -**Linux** - -- [Open Firmware and Devicetree](https://docs.kernel.org/devicetree/index.html) -- [Device Tree Usage](https://elinux.org/Device_Tree_Usage) -- [Device Tree Reference](https://elinux.org/Device_Tree_Reference) -- [Device Tree What It Is](https://elinux.org/Device_Tree_What_It_Is) -""" diff --git a/src/dtsh/model.py b/src/dtsh/model.py new file mode 100644 index 0000000..85fe1d2 --- /dev/null +++ b/src/dtsh/model.py @@ -0,0 +1,1889 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Devicetree model. + +Rationale: + +- factorize the dtsh interface with edtlib (aka python-devicetree) +- support the hierarchical file system metaphor at the model layer +- unified API for sorting and matching nodes +- provide model objects (nodes, bindings, etc) with identity, equality + and a default order-by semantic +- write most dtsh unit tests at the model layer + +Implementation notes: + +- identity: permits to build sets (uniqueness) of model objects + and filter out duplicates, should imply equality +- equality: testing equality for model objects of different types + is allowed and answers false, because an orange is not an apple +- default order: "less than" typically means "appears first"; + comparing model objects of different types is an API violation because + we can't sort oranges and apples without an additional heuristic, + such as preferring oranges + +Unit tests and examples: tests/test_dtsh_model.py +""" + + +from typing import ( + Any, + Optional, + Iterator, + List, + Set, + Mapping, + Dict, + Tuple, + Sequence, +) + +import os +import posixpath +import sys + +from devicetree import edtlib + +from dtsh.dts import DTS, YAMLFile + + +class DTPath: + """Devicetree paths for the hierarchical file-system metaphor. + + This API does not involve actual Devicetree nodes, only path strings. + + An absolute path is a path name, + as defined in DTSpec 2.2.1 and 2.2.3.. + + A path that does not start from the devicetree root ("/") + is a relative path. + + API functions may: + + - work equally with absolute and relative path parameters + - expect an additional "current working branch" parameter + to interpret a relative path parameter + - require an absolute path parameter, and fault if invoked + with a relative path + + POSIX-like path references are supported: + + - "." represents the current working branch + - ".." represents the parent of the current working branch; + + By convention, the Devicetree root is its own parent. + """ + + @staticmethod + def split(path: str) -> List[str]: + """Convert a DT path into a list of path segments. + + Each path segment is a node name: + + split('a') == ['a'] + split('a/b/c') == ['a', 'b', 'c'] + + By convention, the DT root "/" always represent the first + node name of a path name: + + split('/') == ['/'] + split('/x') == ['/', 'x'] + split('/x/y/z') == ['/', 'x', 'y', 'z'] + + Any Trailing empty node name component is stripped: + + split('/a/') == ['/', "a"] + split('') == [] + + Args: + path: A DT path. + + Returns: + The list of the node names in path. + The number of names in this list minus one + represents the (relative) depth of the node at path. + """ + if not path: + return [] + splits = path.split("/") + if not splits[0]: + # path := /[] + # Substitute 1st empty split with "/". + splits[0] = "/" + if not splits[-1]: + # path := / + # Remove trailing empty split. + splits = splits[:-1] + return splits + + @staticmethod + def join(path: str, *paths: str) -> str: + """Concatenate DT paths. + + Join the paths *intelligently*, as defined by posixpath.join(). + + For example: + + join('/', 'a', 'b') == "/a/b" + join('/a', 'b') == '/a/b' + join('a/', 'b') == 'a/b' + + Paths are not normalized: + + join('a', '.') == 'a/.' + join('a', 'b/') == 'a/b/' + join('a', '') == 'a/' + + Joining an absolute path will reset the join chain: + + join('/a', 'b', '/x', 'y') == '/x/y' + + Args: + path: A DT path. + *paths: The path segments to concatenate to path. + + Returns: + The joined path segments. + """ + return posixpath.join(path, *paths) + + @staticmethod + def normpath(path: str) -> str: + """Normalize a DT path. + + Normalize a path by collapsing redundant or trailing separators + and common path references ("." and ".."), + as defined in posixpath.normpath(). + + For example: + + normpath('/.') == '/' + normpath('a/b/') == 'a/b' + normpath('a//b') == 'a/b' + normpath('a/foo/./../b') == 'a/b' + + The devicetree root is its own parent: + + normpath('/../a') == '/a' + + The normalized form of an empty path is a reference + to the current working branch. + + normpath('') == '.' + + Note: a relative path that starts with a reference to a parent, + e.g. '../a', cannot be normalized alone. See also DTPath.abspath(). + + Args: + path: A DT path. + + Returns: + The normalized path. + """ + return posixpath.normpath(path) + + @staticmethod + def abspath(path: str, pwd: str = "/") -> str: + """Absolutize and normalize a DT path. + + This is equivalent to normpath(join(pwd, path)). + + For example: + + abspath('') == '/' + abspath('/') == '/' + abspath('a/b') == '/a/b' + abspath('/a/b', '/x') == '/a/b' + abspath('a', '/foo') == '/foo/a' + + Args: + path: A DT path. + pwd: The DT path name of the current working branch. + + Returns: + The normalized absolute writing of path. + """ + DTPath.check_path_name(pwd) + return DTPath.normpath(DTPath.join(pwd, path)) + + @staticmethod + def relpath(pathname: str, pwd: str = "/") -> str: + """Get the relative DT path from one node to another. + + For example: + + relpath('/') == '.' + relpath('/a') == 'a' + relpath('/a', '/a') == '.' + relpath('/a/b/c', '/a') == 'b/c' + + If going backward is necessary, ".." references are used: + + relpath('/foo/a/b/c', '/bar') == '../foo/a/b/c' + + Args: + pathname: The DT path name of the final node. + pwd: The DT path name of the initial node. + + Returns: + The relative DT path from pwd to path. + """ + DTPath.check_path_name(pathname) + DTPath.check_path_name(pwd) + return posixpath.relpath(pathname, pwd) + + @staticmethod + def dirname(path: str) -> str: + """Get the head of the node names in a DT path. + + Most often, dirname() semantic will match the parent node's path: + + dirname(node.path) == node.parent.path + + For example: + + dirname('/a') == '/' + dirname('/a/b/c') == '/a/b' + dirname('a/b') == 'a' + + The root node is its own parent: + + dirname('/') == '/' + + When the path does not contain any '/', dirname() always + returns a reference to the *current working branch*: + + dirname('') == '.' + dirname('a') == '.' + dirname('.') == '.' + dirname('..') == '.' + + A trailing '/' is interpreted as an empty trailing node name: + + dirname('/a/') == '/a' + + Args: + path: A DT path. + + Returns: + The absolute or relative head of the split path, + or "." when path does not contain any "/". + """ + # Note: the sematic here is NOT DtPath.split(). + head, _ = posixpath.split(path) + return head or "." + + @staticmethod + def basename(path: str) -> str: + """Get the tail of the node names in a DT path. + + Most often, basename() semantic will match the node name: + + basename(node.path) == node.name + + For example: + + basename('/a') == 'a' + basename('a') == 'a' + basename('a/b') == 'b' + + A trailing '/' is interpreted as an empty trailing node name: + + basename('/') == '' + basename('a/') == '' + + And by convention: + + basename('') == '' + basename('.') == '.' + basename('..') == '..' + + Args: + path: A DT path. + + Returns: + The tail of the split path, + or an empty string when path ends with "/". + """ + # Note: the sematic here is NOT DtPath.split(). + _, tail = posixpath.split(path) + return tail + + @staticmethod + def check_path_name(path: str) -> None: + """Check path names. + + Will fault if: + - path is not absolute (does not start with "/") + - path contains path references ("." or "..") + """ + if not path.startswith("/"): + # Path names are absolute DT paths. + raise ValueError(path) + if "." in path: + # Path names do not contain path references ("." or ".."). + raise ValueError(path) + + +class DTVendor: + """Device or device class vendor. + + Identity, equality and default order relationship are based on + the vendor prefix. + + See: + - DTSpec 2.3.1. compatible + - zephyr/dts/bindings/vendor-prefixes.txt + """ + + _prefix: str + _name: str + + def __init__(self, prefix: str, name: str) -> None: + """Initialize vendor. + + Args + prefix: Vendor prefix, e.g. "nordic". + name: Vendor name, e.g. "Nordic Semiconductor" + """ + self._prefix = prefix + self._name = name + + @property + def prefix(self) -> str: + """The vendor prefix. + + This prefix appears as the manufacturer component + in compatible strings. + """ + return self._prefix + + @property + def name(self) -> str: + """The vendor name.""" + return self._name + + def __eq__(self, other: object) -> bool: + if isinstance(other, DTVendor): + return self.prefix == other.prefix + return False + + def __lt__(self, other: object) -> bool: + if isinstance(other, DTVendor): + return self.prefix < other.prefix + raise TypeError(other) + + def __hash__(self) -> int: + return hash(self.prefix) + + def __repr__(self) -> str: + return self._prefix + + +class DTBinding: + """Devicetree binding (DTSpec 4. Device Bindings). + + Bindings include: + + - bindings that specify a device or device class identified by + a compatible string + - child-bindings of those, which may or not be identified by + their own compatible string + - the base bindings recursively included by the above + + Bindings identity, equality and default order are based on: + + - the base name of the YAML file: bindings that originate from + different YAML files are different and ordered by base name + - a child-binding depth that permits to distinguish and compare + bindings that originate from the same YAML file + """ + + # Peer edtlib binding (permits to avoid "if self.binding" statements + # when accessing most properties). + _edtbinding: edtlib.Binding + + # Child-binding depth. + _cb_depth: int + + # YAML binding file. + _yaml: YAMLFile + + # Nested child-binding this binding defines, if any. + _child_binding: Optional["DTBinding"] + + # The parent model (used to resolve child binding). + def __init__( + self, + edtbinding: edtlib.Binding, + cb_depth: int, + child_binding: Optional["DTBinding"], + ) -> None: + """Initialize binding. + + Args: + edtbinding: Peer edtlib binding object. + cb_depth: The child-binding depth. + child_binding: Nested child-binding this binding defines, if any. + """ + if not edtbinding.path: + # DTModel hypothesis. + raise ValueError(edtbinding) + + self._edtbinding = edtbinding + self._cb_depth = cb_depth + self._child_binding = child_binding + # Lazy-initialized: won't read/parse YAML content until needed. + self._yaml = YAMLFile(edtbinding.path) + + @property + def path(self) -> str: + """Absolute path to the YAML file defining the binding.""" + return self._yaml.path + + @property + def compatible(self) -> Optional[str]: + """Compatible string for the devices specified by this binding. + + None for child-bindings without compatible string, + and for bindings that do not specify a device or device class. + """ + return self._edtbinding.compatible + + @property + def buses(self) -> Sequence[str]: + """Bus protocols that the nodes specified by this binding should support. + + Empty list if this binding does not specify a bus node. + """ + return self._edtbinding.buses + + @property + def on_bus(self) -> Optional[str]: + """The bus that the nodes specified by this binding should appear on. + + None if this binding does not expect a bus of appearance. + """ + return self._edtbinding.on_bus + + @property + def description(self) -> Optional[str]: + """The description of this binding, if any.""" + return self._edtbinding.description + + @property + def includes(self) -> Sequence[str]: + """The bindings included by this binding file.""" + return self._yaml.includes + + @property + def cb_depth(self) -> int: + """Child-binding depth. + + Zero if this is not a child-binding. + """ + return self._cb_depth + + @property + def child_binding(self) -> Optional["DTBinding"]: + """The nested child-binding this binding defines, if any.""" + return self._child_binding + + def get_headline(self) -> Optional[str]: + """The headline of this binding description, if any.""" + desc = self._edtbinding.description + if desc: + return desc.lstrip().split("\n", 1)[0] + return None + + def __eq__(self, other: object) -> bool: + if isinstance(other, DTBinding): + this_fname = os.path.basename(self.path) + other_fname = os.path.basename(other.path) + return (this_fname == other_fname) and ( + self.cb_depth == other.cb_depth + ) + return False + + def __lt__(self, other: object) -> bool: + if isinstance(other, DTBinding): + this_fname = os.path.basename(self.path) + other_fname = os.path.basename(other.path) + if this_fname == other_fname: + # Bindings that originate from the same YAML file + # are ordered from parent to child-bindings. + return self.cb_depth < other.cb_depth + # Bindings that originate from different YAML files + # are ordered by file name. + return this_fname < other_fname + raise TypeError(other) + + def __hash__(self) -> int: + return hash((os.path.basename(self.path), self.cb_depth)) + + def __repr__(self) -> str: + return f"yaml:{os.path.basename(self.path)}, cb_depth:{self.cb_depth}" + + +class DTNodeInterrupt: + """Interrupts a node may generate. + + See DTSpec 2.4.2. Properties for Interrupt Controllers. + """ + + _edtirq: edtlib.ControllerAndData + _node: "DTNode" + + @classmethod + def sort_by_number( + cls, irqs: Sequence["DTNodeInterrupt"], reverse: bool + ) -> List["DTNodeInterrupt"]: + """Sort interrupts by IRQ number.""" + return sorted(irqs, key=lambda irq: irq.number, reverse=reverse) + + @classmethod + def sort_by_priority( + cls, irqs: Sequence["DTNodeInterrupt"], reverse: bool + ) -> List["DTNodeInterrupt"]: + """Sort interrupts by IRQ priority.""" + return sorted( + irqs, + key=lambda irq: irq.priority + if irq.priority is not None + else sys.maxsize, + reverse=reverse, + ) + + def __init__( + self, edtirq: edtlib.ControllerAndData, node: "DTNode" + ) -> None: + """Initialize interrupt. + + Args: + edtirq: Peer edtlib IRQ object. + node: The device node that may generate this IRQ. + """ + self._edtirq = edtirq + self._node = node + + @property + def number(self) -> int: + """The IRQ number.""" + return self._edtirq.data.get("irq", sys.maxsize) + + @property + def priority(self) -> Optional[int]: + """The IRQ priority. + + Although interrupts have a priority on most platforms, + it's not actually true for all boards, e.g. the EPS32 SoC. + """ + # NOTE[PR-edtlib]: ControllerAndData docstring may be misleading + # about the "priority" and/or "level" data. + return self._edtirq.data.get("priority") + + @property + def name(self) -> Optional[str]: + """The IRQ name.""" + return self._edtirq.name + + @property + def emitter(self) -> "DTNode": + """The device node that may generate this IRQ.""" + return self._node + + @property + def controller(self) -> "DTNode": + """The controller this interrupt gets sent to.""" + return self._node.dt[self._edtirq.controller.path] + + def __eq__(self, other: object) -> bool: + """Interrupts equal when IRQ numbers, priorities equal.""" + if isinstance(other, DTNodeInterrupt): + return (self.number == other.number) and ( + self.priority == other.priority + ) + return False + + def __lt__(self, other: object) -> bool: + """By default, interrupts are sorted by IRQ numbers, then priorities.""" + if isinstance(other, DTNodeInterrupt): + if self.number == other.number: + if (self.priority is not None) and (other.priority is not None): + return self.priority < other.priority + return self.number < other.number + raise TypeError(other) + + def __hash__(self) -> int: + """Identity inlcludes IRQ number and priority, and emitter.""" + return hash((self.number, self.priority, self.emitter)) + + def __repr__(self) -> str: + return ( + f"IRQ_{self.number}, prio:{self.priority}, src:{self.emitter.path}" + ) + + +class DTNodeRegister: + """Address of a node resource. + + A register describes the address of a resource + within the address space defined by its parent bus. + + According to DTSpec 2.3.6 reg: + + - the reg property is composed of an arbitrary number of pairs + of address and length + - if the parent node specifies a value of 0 for #size-cells, + the length field in the value of reg shall be omitted + + This API will then assume: + + - all node registers should have an address (will fallback to MAXINT + rather than fault when unset) + - an unspecified size represents a zero-size register (an address) + + A default order is defined, based on register addresses, + which is meaningful only when sorting registers within a same parent bus. + """ + + _edtreg: edtlib.Register + _addr: int + + @classmethod + def sort_by_addr( + cls, regs: Sequence["DTNodeRegister"], reverse: bool + ) -> List["DTNodeRegister"]: + """Sort registers by address.""" + return sorted(regs, key=lambda reg: reg.address, reverse=reverse) + + @classmethod + def sort_by_size( + cls, regs: Sequence["DTNodeRegister"], reverse: bool + ) -> List["DTNodeRegister"]: + """Sort registers by size.""" + return sorted(regs, key=lambda reg: reg.size, reverse=reverse) + + def __init__(self, edtreg: edtlib.Register) -> None: + self._edtreg = edtreg + if edtreg.addr is not None: + self._addr = edtreg.addr + else: + # We assume node registers have an address. + # Fallback to funny value (not 0). + self._addr = sys.maxsize + + @property + def address(self) -> int: + """The address within the address space defined by the parent bus.""" + return self._addr + + @property + def size(self) -> int: + """The register size. + + Mostly meaningful for memory mapped IO. + """ + return self._edtreg.size or 0 + + @property + def tail(self) -> int: + """The last address accessible through this register. + + Mostly meaningful for memory mapped IO. + """ + if self.size > 0: + return self.address + self.size - 1 + return self.address + + @property + def name(self) -> Optional[str]: + """The register name, if any.""" + return self._edtreg.name + + def __lt__(self, other: object) -> bool: + if isinstance(other, DTNodeRegister): + return self.address < other.address + raise TypeError(other) + + def __repr__(self) -> str: + return f"addr:{hex(self.address)}, size:{hex(self.size)}" + + +class DTWalkable: + """Virtual devicetree we can walk through. + + The simplest walk-able is a Devicetree branch, + implemented by DTNode objects. + + DTWalkableComb permits to define a virtual devicetree as a root node + and a set of selected leaves. + """ + + def walk( + self, + /, + order_by: Optional["DTNodeSorter"] = None, + reverse: bool = False, + enabled_only: bool = False, + fixed_depth: int = 0, + ) -> Iterator["DTNode"]: + """Walk through the virtual devicetree. + + Args: + order_by: Children ordering while walking branches. + None will preserve the DTS order. + reverse: Whether to reverse children order. + If set and no order_by is given, means reversed DTS order. + enabled_only: Whether to stop at disabled branches. + fixed_depth: The depth limit, defaults to 0, + walking through to leaf nodes, according to enabled_only. + + Returns: + An iterator yielding the nodes in order of traversal. + """ + del order_by + del reverse + del enabled_only + del fixed_depth + yield from () + + +class DTNode(DTWalkable): + """Devicetree nodes. + + The nodes API has a double aspect: + + - node-oriented: properties, identity, equality, + and default order-by relationship + - branch-oriented: walk-able, search-able + """ + + # Peer edtlib node. + _edtnode: edtlib.Node + + # The devicetree model this node belongs to. + _dt: "DTModel" + + # Parent node. + _parent: "DTNode" + + # Child nodes (DTS-order). + _children: List["DTNode"] + + # The binding that specifies the node content, if any. + _binding: Optional[DTBinding] + + def __init__( + self, + edtnode: edtlib.Node, + model: "DTModel", + parent: Optional["DTNode"], + ) -> None: + """Insert a node in a devicetree model. + + Nodes are first inserted childless at the location pointed to by + the parent parameter, then children are appended while the model + initialization process walks the devicetree in DTS order. + + Args: + edtnode: The peer edtlib.Node node. + model: The devicetree model this node is inserted into. + parent: The parent node (point of insertion), + or None when creating the model's root. + """ + self._edtnode = edtnode + self._dt = model + # The devicetree root is its own parent. + self._parent = parent or self + # Nodes are first inserted childless. + self._children = [] + # Device bindings are initialized on start-up. + self._binding = self._dt.get_device_binding(self) + + @property + def dt(self) -> "DTModel": + """The devicetree model this node belongs to.""" + return self._dt + + @property + def path(self) -> str: + """The path name (DTSpec 2.2.3).""" + return self._edtnode.path + + @property + def name(self) -> str: + """The node name (DTSpec 2.2.1).""" + return self._edtnode.name + + @property + def unit_name(self) -> str: + """The unit-name component of the node name. + + The term unit-name is not widely used for the node-name component + of node names: for an example, see in DTSpec 3.4. /memory node. + """ + return self._edtnode.name.split("@")[0] + + @property + def unit_addr(self) -> Optional[int]: + """The (value of the) unit-address component of the node name. + + May be None if this node has no unit address. + """ + # NOTE: edtlib.Node.unit_addr may answer a string in the (near) future. + return self._edtnode.unit_addr + + @property + def status(self) -> str: + """The node's status string (DTSpec 2.3.4). + + While the devicetree specifications allows this property + to have values "okay", "disabled", "reserved", "fail", and "fail-sss", + only the values "okay" and "disabled" are currently relevant to Zephyr. + """ + return self._edtnode.status + + @property + def enabled(self) -> bool: + """Whether this node is enabled, according to its status property. + + For backwards compatibility, the value "ok" is treated the same + as "okay", but this usage is deprecated. + """ + # edtlib.Node.status() has already substituted "ok" with "okay", + # no need to test both values again. + return self._edtnode.status == "okay" + + @property + def aliases(self) -> Sequence[str]: + """The names this node is aliased to. + + Retrieved from the "/aliases" node content (DTSpec 3.3). + """ + return self._edtnode.aliases + + @property + def chosen(self) -> List[str]: + """The parameters this node is a chosen for. + + Retrieved from the "/chosen" node content (DTSpec 3.3). + """ + return [ + chosen + for chosen, node in self._dt.chosen_nodes.items() + if node is self + ] + + @property + def labels(self) -> Sequence[str]: + """The labels attached to the node in the DTS (DTSpec 6.2).""" + return self._edtnode.labels + + @property + def label(self) -> Optional[str]: + """A human readable description of the node device (DTSpec 4.1.2.3).""" + return self._edtnode.label + + @property + def compatibles(self) -> Sequence[str]: + """Compatible strings, from most specific to most general (DTSpec 3.3.1).""" + return self._edtnode.compats + + @property + def compatible(self) -> Optional[str]: + """The compatible string value that actually specifies the node content. + + A few nodes have no compatible value. + """ + return self._edtnode.matching_compat + + @property + def vendor(self) -> Optional[DTVendor]: + """The device vendor. + + This is the manufacturer for the compatible string + that specifies the node content. + """ + if self.compatible: + return self._dt.get_vendor(self.compatible) + + if self.binding: + # Search parent nodes for vendor up to child-binding depth. + node = self.parent + cb_depth = self.binding.cb_depth + while cb_depth != 0: + if node.vendor: + return node.vendor + + cb_depth -= 1 + node = node.parent + + return None + + @property + def binding_path(self) -> Optional[str]: + """Path to the binding file that specifies this node content.""" + return self._edtnode.binding_path + + @property + def binding(self) -> Optional[DTBinding]: + """The binding that specifies the node content, if any.""" + return self._binding + + @property + def on_bus(self) -> Optional[str]: + """The bus this node appears on, if any.""" + # NOTE[edtlib]: + # What's the actual semantic of edtlib.Node.on_buses() ? + # Doesn't the node appear on a single bus in a given devicetree ? + return self.binding.on_bus if self.binding else None + + @property + def on_bus_device(self) -> Optional["DTNode"]: + """The bus controller if this node is connected to a bus.""" + if self._edtnode.bus_node: + return self._dt[self._edtnode.bus_node.path] + return None + + @property + def buses(self) -> Sequence[str]: + """List of supported protocols if this node is a bus device. + + Empty list if this node is not a bus node. + """ + return self._edtnode.buses + + @property + def interrupts(self) -> List[DTNodeInterrupt]: + """The interrupts generated by the node.""" + return [ + DTNodeInterrupt(edtirq, self) for edtirq in self._edtnode.interrupts + ] + + @property + def registers(self) -> List[DTNodeRegister]: + """Addresses of the device resources (DTSpec 2.3.6).""" + return [DTNodeRegister(edtreg) for edtreg in self._edtnode.regs] + + @property + def description(self) -> Optional[str]: + """The node description retrieved from its binding, if any.""" + return self._edtnode.description + + @property + def children(self) -> Sequence["DTNode"]: + """The node children, in DTS-order.""" + return self._children + + @property + def parent(self) -> "DTNode": + """The parent node. + + By convention, the root node is its own parent. + """ + return self._parent + + @property + def dep_ordinal(self) -> int: + """Dependency ordinal. + + Non-negative integer value such that the value for a node is less + than the value for all nodes that depend on it. + + See edtlib.Node.dep_ordinal. + """ + return self._edtnode.dep_ordinal + + @property + def required_by(self) -> List["DTNode"]: + """The nodes that directly depend on this device.""" + return [ + self._dt[edt_node.path] for edt_node in self._edtnode.required_by + ] + + @property + def depends_on(self) -> List["DTNode"]: + """The nodes this device directly depends on.""" + return [ + self._dt[edt_node.path] for edt_node in self._edtnode.depends_on + ] + + def get_child(self, name: str) -> "DTNode": + """Retrieve a child node by name. + + The requested child MUST exist. + + Args: + name: The child node name to search for. + + Returns: + The requested child. + """ + for node in self._children: + if node.name == name: + return node + raise KeyError(name) + + def walk( + self, + /, + order_by: Optional["DTNodeSorter"] = None, + reverse: bool = False, + enabled_only: bool = False, + fixed_depth: int = 0, + ) -> Iterator["DTNode"]: + """Walk the devicetree branch under this node. + + Args: + order_by: Children ordering while walking through branches. + None will preserve the DTS order. + reverse: Whether to reverse sort order. + If set and no order_by is given, means reverse DTS-order. + enabled_only: Whether to stop at disabled branches. + fixed_depth: The depth limit. + Defaults to 0, which means walking through to leaf nodes, + according to enabled_only. + + Returns: + An iterator yielding the nodes in order of traversal. + """ + return self._walk( + self, + order_by=order_by, + reverse=reverse, + enabled_only=enabled_only, + fixed_depth=fixed_depth, + ) + + def rwalk(self) -> Iterator["DTNode"]: + """Walk the devicetree backward from this node through to the root node. + + Returns: + An iterator yielding the nodes in order of traversal. + """ + return self._rwalk(self) + + def find( + self, + criterion: "DTNodeCriterion", + /, + order_by: Optional["DTNodeSorter"] = None, + reverse: bool = False, + enabled_only: bool = False, + ) -> List["DTNode"]: + """Search the devicetree branch under this node. + + Args: + criterion: The search criterion children must match. + order_by: Sort matched nodes, None will preserve the DTS order. + reverse: Whether to reverse sort order. + If set and no order_by is given, means reverse DTS-order. + enabled_only: Whether to stop at disabled branches. + + Returns: + The list of matched nodes. + """ + nodes: List[DTNode] = [ + node + for node in self.walk( + enabled_only=enabled_only, + # We don't want children ordering here, + # we'll sort results once finished. + order_by=None, + ) + if criterion.match(node) + ] + + # Sort matched nodes. + if order_by: + nodes = order_by.sort(nodes, reverse=reverse) + elif reverse: + # Reverse DTS-order. + nodes.reverse() + return nodes + + def _walk( + self, + node: "DTNode", + /, + order_by: Optional["DTNodeSorter"], + reverse: bool, + enabled_only: bool, + fixed_depth: int, + at_depth: int = 0, + ) -> Iterator["DTNode"]: + if enabled_only and not node.enabled: + # Abort early on disabled branches when enabled_only is set. + return + + # Yield branch and increment depth. + yield node + if fixed_depth > 0: + if at_depth == fixed_depth: + return + at_depth += 1 + + # Filter and sort children. + children = node.children + if enabled_only: + children = [child for child in children if child.enabled] + if order_by: + children = order_by.sort(children, reverse=reverse) + elif reverse: + children = list(reversed(children)) + + for child in children: + yield from self._walk( + child, + order_by=order_by, + reverse=reverse, + enabled_only=enabled_only, + fixed_depth=fixed_depth, + at_depth=at_depth, + ) + + def _rwalk(self, node: "DTNode") -> Iterator["DTNode"]: + # Walk the subtree backward to the root node, + # which is by convention its own parent. + yield node + if node.parent != node: + yield from self._rwalk(node.parent) + + def __eq__(self, other: object) -> bool: + if isinstance(other, DTNode): + return other.path == self.path + return False + + def __lt__(self, other: object) -> bool: + if isinstance(other, DTNode): + return self.path < other.path + raise TypeError(other) + + def __hash__(self) -> int: + return hash(self.path) + + def __repr__(self) -> str: + return self.path + + +class DTModel: + """Devicetree model. + + This is the main entry point dtsh will rely on to access + a successfully loaded devicetree. + + It's a facade to an edtlib.EDT devicetree model, + with extensions and helpers for implementing the Devicetree shell. + """ + + # The EDT model this is a facade of. + _edt: edtlib.EDT + + # Devicetree root. + _root: DTNode + + # Devicetree source definition. + _dts: DTS + + # Map path names to nodes, + # redundant with edtlib.EDT but "cheap". + _nodes: Dict[str, DTNode] + + # Bindings identified by a compatible string: + # (compatible string, bus of appearance) -> binding + _compatible_bindings: Dict[Tuple[str, Optional[str]], DTBinding] + + # Bindings without compatible string. + # (YAML file name, cb_depth) -> binding + _compatless_bindings: Dict[Tuple[str, int], DTBinding] + + # YAML binding files included by actual device bindings. + # YAML file name -> binding + _base_bindings: Dict[str, DTBinding] + + # Device and device class vendors that appear in this model. + # prefix (aka manufacturer) -> vendor + _vendors: Dict[str, DTVendor] + + # See aliased_nodes(). + _aliased_nodes: Dict[str, DTNode] + + # See chosen_nodes(). + _chosen_nodes: Dict[str, DTNode] + + # See labeled_nodes(). + _labeled_nodes: Dict[str, DTNode] + + @staticmethod + def get_cb_depth(node: DTNode) -> int: + """Compute the child-binding depth for a node. + + This is a non negative integer: + + - initialized to zero + - incremented while walking the devicetree backward + until we reach a node whose binding does not have child-binding + """ + cb_depth = 0 + edtparent = node._edtnode.parent # pylint: disable=protected-access + while edtparent: + parent_binding = ( + edtparent._binding # pylint: disable=protected-access + ) + if parent_binding and parent_binding.child_binding: + cb_depth += 1 + edtparent = edtparent.parent + else: + break + return cb_depth + + @classmethod + def create( + cls, + dts_path: str, + binding_dirs: Optional[Sequence[str]] = None, + vendors_file: Optional[str] = None, + ) -> "DTModel": + """Devicetree model factory. + + Args: + dts_path: The DTS file path. + binding_dirs: The DT bindings search path as a list of directories. + These directories are not required to exist. + If unset, a default search path is retrieved or worked out. + vendors_file: Path to a file in vendor-prefixes.txt format. + + Returns: + An initialized devicetree model. + + Raises: + OSError: Failed to open the DTS file. + EDTError: Failed to initialize the devicetree model. + """ + return cls(DTS(dts_path, binding_dirs, vendors_file)) + + def __init__(self, dts: DTS) -> None: + """Initialize a Devicetree model. + + Args: + dts: The devicetree source definition. + + Raises: + OSError: Failed to open the DTS file. + EDTError: Failed to parse the DTS file, missing bindings. + """ + self._dts = dts + + # Vendors file support initialization. + self._vendors = {} + if self._dts.vendors_file: + # Load vendor definitions, and get the prefixes mapping + # to provide EDT() with. + vendor_prefixes = self._load_vendors_file(self._dts.vendors_file) + else: + vendor_prefixes = None + + self._edt = edtlib.EDT( + self._dts.path, + # NOTE[PR-edtlib]: could EDT() accept a Sequence + # as const-qualified argument ? + list(self._dts.bindings_search_path), + vendor_prefixes=vendor_prefixes, + ) + + # Lazy-initialized base bindings. + self._base_bindings = {} + # Mappings initialized on 1st access. + self._labeled_nodes = {} + self._aliased_nodes = {} + self._chosen_nodes = {} + + # The root node is its own parent. + self._root = DTNode(self._edt.get_node("/"), self, parent=None) + + # Lazy-initialization. + self._compatible_bindings = {} + self._compatless_bindings = {} + + # Walk the EDT model, recursively initializing peer nodes. + self._nodes = {} + self._init_dt(self._root) + + @property + def dts(self) -> DTS: + """The devicetree source definition.""" + return self._dts + + @property + def root(self) -> DTNode: + """The devicetree root node.""" + return self._root + + @property + def size(self) -> int: + """The number of nodes in this model (including the root node).""" + return len(self._edt.nodes) + + @property + def aliased_nodes(self) -> Mapping[str, DTNode]: + """Aliases retrieved from the "/aliases" node.""" + if not self._aliased_nodes: + self._init_aliased_nodes() + return self._aliased_nodes + + @property + def chosen_nodes(self) -> Mapping[str, DTNode]: + """Chosen configurations retrieved from the "/chosen" node.""" + if not self._chosen_nodes: + self._init_chosen_nodes() + return self._chosen_nodes + + @property + def labeled_nodes(self) -> Mapping[str, DTNode]: + """Nodes that can be referenced with a devicetree label.""" + if not self._labeled_nodes: + self._init_labeled_nodes() + return self._labeled_nodes + + @property + def bus_protocols(self) -> List[str]: + """All bus protocols supported by this model.""" + buses: Set[str] = set() + for node in self._nodes.values(): + buses.update(node.buses) + return list(buses) + + @property + def compatible_strings(self) -> List[str]: + """All compatible strings that appear on some node in this model.""" + return list(self._edt.compat2nodes.keys()) + + @property + def vendors(self) -> List[DTVendor]: + """Vendors for this model compatible strings. + + NOTE: Will return an empty list if this model contains + compatible strings with undefined manufacturers. + """ + if not self._vendors: + return [] + + try: + return [ + self._vendors[prefix] + for prefix in { + compat.split(",", 1)[0] + for compat in self.compatible_strings + if "," in compat + } + ] + except KeyError as e: + print(f"WARN: invalid manufacturer: {e}", file=sys.stderr) + return [] + + def get_device_binding(self, node: DTNode) -> Optional[DTBinding]: + """Retrieve a device binding. + + Args: + node: The device to get the bindings for. + + Returns: + The device or device class binding, + or None if the node does not represent an actual device, + e.g. "/cpus". + """ + edtnode, edtbinding = self._edtnode_edtbinding(node) + if not edtbinding: + return None + + compat = edtbinding.compatible + if compat: + bus = edtbinding.on_bus + return self.get_compatible_binding(compat, bus) + + cb_depth = self._edtnode_cb_depth(edtnode) + return self._get_compatless_binding(edtbinding, cb_depth) + + def get_compatible_binding( + self, compat: str, bus: Optional[str] = None + ) -> Optional[DTBinding]: + """Access bindings identified by a compatible string. + + If the lookup fails for the requested bus of appearance, + a binding is searched for the compatible string only. + This permits client code to uniformly enumerate the node bindings, + not all of which will relate to the bus the node may appear on: + + for compat in node.compatibles: + binding = model.get_compatible_binding(node.compatible, node.on_bus) + if binding: + # Do something if the binding + + Args: + compat: A compatible string to search the defining binding of. + bus: The bus that nodes with this binding should appear on, if any. + + Returns: + The binding for compatible devices, or None if not found. + """ + binding: Optional[DTBinding] = None + + binding = self._compatible_bindings.get((compat, bus)) + if not binding and bus: + binding = self._compatible_bindings.get((compat, None)) + + if not binding: + edtbinding = self._edt_compat2binding(compat, bus) + if edtbinding: + cb_depth = self._edtbinding_cb_depth(edtbinding) + binding = self._init_binding(edtbinding, cb_depth) + + return binding + + def get_base_binding(self, basename: str) -> DTBinding: + """Retrieve a base binding by its file name. + + The requested binding file MUST exist. + + This API is a factory and a cache. + + Args: + basename: The binding file name. + + Returns: + The binding the file specifies. + """ + if basename not in self._base_bindings: + path = self.dts.yamlfs.find_path(basename) + if path: + self._base_bindings[basename] = self._load_binding_file(path) + else: + # Should not happen: all included YAML files have been + # loaded by EDT at this point, failing now to retrieve + # them again is worth a fault. + pass + return self._base_bindings[basename] + + def get_vendor(self, compat: str) -> Optional[DTVendor]: + """Retrieve the vendor for a compatible string. + + Args: + compat: The compatible string to search the vendor for. + + Returns: + The device or device class vendor, + or None if the compatible string has no vendor prefix. + """ + if self._dts.vendors_file and ("," in compat): + manufacturer, _ = compat.split(",", 1) + try: + return self._vendors[manufacturer] + except KeyError: + # All vendors that appear as manufacturer in compatible + # strings should exit: don't fault but log this nonetheless. + print( + f"WARN: unknown manufacturer: {manufacturer}", + file=sys.stderr, + ) + return None + + def get_compatible_devices(self, compat: str) -> List[DTNode]: + """Retrieve devices whose binding matches a given compatible string. + + Args: + compat: The compatible string to match. + + Returns: + The matched nodes. + """ + return [ + self[edt_node.path] + for edt_node in self._edt.compat2nodes[compat] + if edt_node.matching_compat == compat + ] + + def walk( + self, + /, + order_by: Optional["DTNodeSorter"] = None, + reverse: bool = False, + enabled_only: bool = False, + fixed_depth: int = 0, + ) -> Iterator[DTNode]: + """Walk the devicetree from the root node through to all leaves. + + Shortcut for root.walk(). + + Args: + order_by: Children ordering while walking branches. + None will preserve the DTS-order. + reverse: Whether to reverse walk order. + If set and no order_by is given, means reverse DTS-order. + enabled_only: Whether to stop at disabled branches. + fixed_depth: The depth limit. + Defaults to 0, which means walking through to leaf nodes, + according to enabled_only. + + Returns: + A generator yielding the devicetree nodes in order of traversal. + """ + return self._root.walk( + order_by=order_by, + reverse=reverse, + enabled_only=enabled_only, + fixed_depth=fixed_depth, + ) + + def find( + self, + criterion: "DTNodeCriterion", + /, + order_by: Optional["DTNodeSorter"] = None, + reverse: bool = False, + enabled_only: bool = False, + ) -> List[DTNode]: + """Search the devicetree. + + Shortcut for root.find(). + + Args: + criterion: The search criterion nodes must match. + order_by: Sort matched nodes, None will preserve the DTS-order. + reverse: Whether to reverse found nodes order. + If set and no order_by is given, means reverse DTS-order. + enabled_only: Whether to stop at disabled branches. + + Returns: + The list of matched nodes. + """ + return self._root.find( + criterion, + order_by=order_by, + reverse=reverse, + enabled_only=enabled_only, + ) + + def __contains__(self, pathname: str) -> bool: + return pathname in self._nodes + + def __getitem__(self, pathname: str) -> DTNode: + return self._nodes[pathname] + + def __len__(self) -> int: + return self.size + + def __iter__(self) -> Iterator[DTNode]: + return self.walk() + + def _init_dt(self, branch: DTNode) -> None: + self._nodes[branch.path] = branch + # Append children in DTS order. + for ( + edtchild + ) in ( + branch._edtnode.children.values() # pylint: disable=protected-access + ): + child = DTNode(edtchild, self, branch) + self._nodes[child.path] = child + branch._children.append(child) # pylint: disable=protected-access + self._init_dt(child) + + def _init_aliased_nodes(self) -> None: + self._aliased_nodes.update( + { + alias: self[dtnode.path] + # NOTE[PR-edtlib]: Could we have something like EDT.aliased_nodes ? + for alias, dtnode in self._edt._dt.alias2node.items() # pylint: disable=protected-access + } + ) + + def _init_chosen_nodes(self) -> None: + self._chosen_nodes.update( + { + chosen: self[edtnode.path] + for chosen, edtnode in self._edt.chosen_nodes.items() + } + ) + + def _init_labeled_nodes(self) -> None: + for node, labels in [ + (node_, node_.labels) for node_ in self._nodes.values() + ]: + self._labeled_nodes.update({label: node for label in labels}) + + def _get_compatless_binding( + self, edtbinding: edtlib.Binding, cb_depth: int + ) -> DTBinding: + if not edtbinding.path: + raise ValueError(edtbinding) + + basename = os.path.basename(edtbinding.path) + if (basename, cb_depth) not in self._compatless_bindings: + binding = self._init_binding(edtbinding, cb_depth) + self._compatless_bindings[(basename, cb_depth)] = binding + + return self._compatless_bindings[(basename, cb_depth)] + + def _init_binding( + self, edtbinding: edtlib.Binding, cb_depth: int + ) -> DTBinding: + if not edtbinding.path: + raise ValueError(f"Binding file expected: {edtlib.Binding}") + + edtbinding_child = edtbinding.child_binding + if edtbinding_child: + child_binding = self._init_binding(edtbinding_child, cb_depth + 1) + else: + child_binding = None + + binding = DTBinding(edtbinding, cb_depth, child_binding) + + if edtbinding.compatible: + compat = edtbinding.compatible + bus = edtbinding.on_bus + self._compatible_bindings[(compat, bus)] = binding + else: + basename = os.path.basename(edtbinding.path) + self._compatless_bindings[(basename, cb_depth)] = binding + + return binding + + def _edt_compat2binding( + self, compat: str, bus: Optional[str] + ) -> Optional[edtlib.Binding]: + edtbinding = ( + self._edt._compat2binding.get( # pylint: disable=protected-access + (compat, bus) + ) + ) + if not edtbinding and bus: + edtbinding = self._edt._compat2binding.get( # pylint: disable=protected-access + (compat, None) + ) + return edtbinding + + def _edtbinding_cb_depth(self, edtbinding: edtlib.Binding) -> int: + if not edtbinding.compatible: + raise ValueError(edtbinding) + + # We're computing the child-binding depth of any node + # whose compatible value and bus of appearance + # match this binding. + compat = edtbinding.compatible + bus = edtbinding.on_bus + + for edtnode in self._edt.compat2nodes[compat]: + binding = edtnode._binding # pylint: disable=protected-access + if binding and (binding.on_bus == bus): + return self._edtnode_cb_depth(edtnode) + + # The binding does not appear in any compatible string in the model. + raise ValueError(edtbinding) + + def _edtnode_cb_depth(self, edtnode: edtlib.Node) -> int: + cb_depth = 0 + + # Walk the devicetree backward until we're not specified + # by a child-binding. + parent = edtnode.parent + while parent: + binding = parent._binding # pylint: disable=protected-access + if binding and binding.child_binding: + cb_depth += 1 + parent = parent.parent + else: + parent = None + + return cb_depth + + def _edtnode_edtbinding( + self, node: DTNode + ) -> Tuple[edtlib.Node, Optional[edtlib.Binding]]: + return ( + node._edtnode, # pylint: disable=protected-access + node._edtnode._binding, # pylint: disable=protected-access + ) + + def _load_binding_file(self, path: str) -> DTBinding: + edtbinding = edtlib.Binding( + path, + # NOTE[PR-edtlib]: patch Binding ctor to accept a Mapping + # as const-qualified argument. + fname2path=dict(self._dts.yamlfs.name2path), + require_compatible=False, + require_description=False, + ) + return DTBinding(edtbinding, 0, None) + + def _load_vendors_file(self, vendors_file: str) -> Dict[str, str]: + vendor_prefixes = edtlib.load_vendor_prefixes_txt(vendors_file) + self._vendors.update( + { + prefix: DTVendor(prefix, name) + for prefix, name in vendor_prefixes.items() + } + ) + return vendor_prefixes + + +class DTNodeSorter: + """Basis for order relationships applicable to nodes. + + This defines stateless adapters for the standard Python sorted() function + that support input lists containing nodes for which the key function + would return None values. + + This is a 3-stage sort: + + - split nodes into sortable and unsortable + - sort those that can be sorted using + - append the unsortable to the sorted (by default, nodes for which + the key function would have no value appear last) + + This base implementation sort nodes into "natural order". + """ + + def split_sortable_unsortable( + self, nodes: Sequence[DTNode] + ) -> Tuple[List[DTNode], List[DTNode]]: + """Split nodes into sortable and unsortable. + + Returns: + The tuple of (sortable, unsortable) nodes. + This base implementation returns all nodes. + """ + return (list(nodes), []) + + def weight(self, node: DTNode) -> Any: + """The key function. + + Args: + node: The node to get the weight of. + + Returns: + The weight this sorter gives to the node. + This base implementation returns the node itself ("natural order"). + """ + # TODO[python]: Where is SupportsLessThanT defined ? + return node + + def sort( + self, nodes: Sequence[DTNode], reverse: bool = False + ) -> List[DTNode]: + """Sort nodes. + + Args: + nodes: The nodes to sort. + reverse: Whether to reverse sort order. + + Returns: + A list with the nodes sorted. + """ + sortable, unsortable = self.split_sortable_unsortable(nodes) + + # We can't rely on Python sort(reverse=True) when multiple + # items would pretend for the "same" position in the sorted + # list (e.g. sorting nodes with the same unit-name by unit-name): + # the Python sort() implementation is not granted to actually + # reverse the items in the way we'd expect, + # e.g. for the "-r" option that should always reverse + # the command output order. + sortable.sort(key=self.weight) + if reverse: + sortable.reverse() + # Reverse order also applies to unsortable (sic). + unsortable.reverse() + + # Unsortable first. + unsortable.extend(sortable) + return unsortable + + # Unsortable last. + sortable.extend(unsortable) + return sortable + + +class DTNodeCriterion: + """Base criterion for searching or filtering DT nodes. + + This base criterion does not match with any node. + """ + + def match(self, node: DTNode) -> bool: + """Match a node with this criterion. + + Args: + node: The DT node to match. + + Returns: + True if the node matches with this criterion. + """ + del node # Unused argument. + return False + + +class DTNodeCriteria(DTNodeCriterion): + """Chain of criterion for searching or filtering DT nodes. + + The criterion chain is evaluated either as: + + - a logical conjunction, and fails at the first unmatched criterion (default) + - a logical disjunction, and succeeds at the first matched criterion + + By default, an empty chain will match any node. + + A logical negation may eventually be applied to the chain. + """ + + _criteria: List[DTNodeCriterion] + _ored_chain: bool + _negative_chain: bool + + def __init__( + self, + criterion_chain: Optional[List[DTNodeCriterion]] = None, + ored_chain: bool = False, + negative_chain: bool = False, + ) -> None: + """Initialize a new chain of criterion. + + Args: + *criteria: Initial criterion chain. + ored_chain: Whether this chain is a logical disjonction of + criterion (default is logical conjunction). + negative_chain: Whether to apply a logical negation to the chain. + """ + self._criteria = criterion_chain if criterion_chain else [] + self._ored_chain = ored_chain + self._negative_chain = negative_chain + + def append_criterion(self, criterion: DTNodeCriterion) -> None: + """Append a criterion. + + Args: + criterion: The criterion to append to this chain. + """ + self._criteria.append(criterion) + + def match(self, node: DTNode) -> bool: + """Does a node match this chain of criterion ? + + Args: + node: The node to match. + + Returns: + True if the node matches this criteria. + """ + if self._ored_chain: + chain_match = any( + criterion.match(node) for criterion in self._criteria + ) + else: + chain_match = all( + criterion.match(node) for criterion in self._criteria + ) + + return (not chain_match) if self._negative_chain else chain_match diff --git a/src/dtsh/modelutils.py b/src/dtsh/modelutils.py new file mode 100644 index 0000000..9ccdf3b --- /dev/null +++ b/src/dtsh/modelutils.py @@ -0,0 +1,724 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Devicetree model helpers. + +- Match node with text based criteria (implement DTNodeTextCriterion) +- Match node with integer based criteria (implement DTNodeIntCriterion) +- Sort nodes (implement DTNodeSorter) +- Arbitrary virtual devicetree (DTWalkableComb) + +Unit tests and examples: tests/test_dtsh_modelutils.py +""" + + +from typing import ( + Any, + Callable, + Tuple, + Set, + List, + Optional, + Sequence, + Iterator, + Mapping, +) + +import operator +import re +import sys + +from dtsh.model import DTWalkable, DTNode, DTNodeSorter, DTNodeCriterion + + +class DTNodeSortByAttr(DTNodeSorter): + """Base for sorters that weight node attributes.""" + + # The name of the dtsh.model.Node attribute this sorter is based on. + _attname: str + + # Whether we should compare minimums or maximums when the attribute + # value is a list. + _reverse: bool = False + + def __init__(self, attname: str) -> None: + """Initialize sorter. + + Args: + attname: The Python attribute name. + """ + self._attname = attname + + def split_sortable_unsortable( + self, nodes: Sequence[DTNode] + ) -> Tuple[List[DTNode], List[DTNode]]: + """Overrides DTNodeSorter.split_sortable_unsortable(). + + Returns: + The tuple of (sortable, unsortable) where unsortable include + nodes for which : + - the attribute has no value + - the attribute value is an empty lists: [] is less than + any non empty list, which would not match + the expected semantic (empty lists would appear first) + """ + sortable = [] + unsortable = [] + for node in nodes: + attr = getattr(node, self._attname) + if (attr is not None) and (attr != []): + sortable.append(node) + else: + unsortable.append(node) + return (sortable, unsortable) + + def gravity(self, node: DTNode) -> Any: + """Input of the weight function. + + This is typically the value of the attribute this sorter is based on, + but subclasses may override this method to provide a more precise + semantic: e.g. a sorter based on the node registers may either + weight the register addresses or the register sizes. + + Args: + node: The node to weight. + + Returns: + The input for the weight function. + """ + return getattr(node, self._attname) + + def weight(self, node: DTNode) -> Any: + """Overrides DTNodeSorter.weight(). + + The node weight is based on its gravity. + + If the gravity value is a list: + + - in ascending order: we expect to compare minimum, + so the weight is min(gravity) + - in descending order: we expect to compare maximum, + so the weight is max(gravity) + + Returns: + The node weight. + """ + w = self.gravity(node) + if isinstance(w, list): + w = max(w) if self._reverse else min(w) + return w + + def sort( + self, nodes: Sequence[DTNode], reverse: bool = False + ) -> List[DTNode]: + """Overrides DTNodeSorter.sort().""" + # Set the reverse flag that the weight function will rely on. + self._reverse = reverse + # Then sort nodes with the base DTNodeSorter implementation. + return super().sort(nodes, reverse) + + +class DTNodeSortByPathName(DTNodeSortByAttr): + """Sort nodes by path name.""" + + def __init__(self) -> None: + super().__init__("path") + + +class DTNodeSortByNodeName(DTNodeSortByAttr): + """Sort nodes by node name.""" + + def __init__(self) -> None: + super().__init__("name") + + +class DTNodeSortByUnitName(DTNodeSortByAttr): + """Sort nodes by unit-name.""" + + def __init__(self) -> None: + super().__init__("unit_name") + + +class DTNodeSortByUnitAddr(DTNodeSortByAttr): + """Sort nodes by unit-address.""" + + def __init__(self) -> None: + super().__init__("unit_addr") + + +class DTNodeSortByCompatible(DTNodeSortByAttr): + """Sort nodes by compatible strings.""" + + def __init__(self) -> None: + super().__init__("compatibles") + + +class DTNodeSortByBinding(DTNodeSortByAttr): + """Sort nodes by binding (compatible value).""" + + def __init__(self) -> None: + super().__init__("compatible") + + +class DTNodeSortByVendor(DTNodeSortByAttr): + """Sort nodes by vendor name.""" + + def __init__(self) -> None: + super().__init__("vendor") + + def gravity(self, node: DTNode) -> Any: + """Overrides DTNodeSortByAttrValue.weight(). + + Returns: + The vendor name. + """ + # At this point, we know the device has a vendor. + return [node.vendor.name] # type: ignore + + +class DTNodeSortByDeviceLabel(DTNodeSortByAttr): + """Sort nodes by device label.""" + + def __init__(self) -> None: + super().__init__("label") + + +class DTNodeSortByNodeLabel(DTNodeSortByAttr): + """Sort nodes by DTS label.""" + + def __init__(self) -> None: + super().__init__("labels") + + +class DTNodeSortByAlias(DTNodeSortByAttr): + """Sort nodes by alias.""" + + def __init__(self) -> None: + super().__init__("aliases") + + +class DTNodeSortByBus(DTNodeSortByAttr): + """Sort nodes by supported bus protocols.""" + + def __init__(self) -> None: + super().__init__("buses") + + +class DTNodeSortByOnBus(DTNodeSortByAttr): + """Sort nodes by bus of appearance.""" + + def __init__(self) -> None: + super().__init__("on_bus") + + +class DTNodeSortByDepOrdinal(DTNodeSortByAttr): + """Sort nodes by dependency ordinal.""" + + def __init__(self) -> None: + super().__init__("dep_ordinal") + + +class DTNodeSortByIrqNumber(DTNodeSortByAttr): + """Sort nodes by interrupt number.""" + + def __init__(self) -> None: + super().__init__("interrupts") + + def gravity(self, node: DTNode) -> Any: + """Overrides DTNodeSortByAttrValue.weight(). + + Returns: + The interrupt numbers. + """ + return [irq.number for irq in node.interrupts] + + +class DTNodeSortByIrqPriority(DTNodeSortByAttr): + """Sort nodes by interrupt priority.""" + + def __init__(self) -> None: + super().__init__("interrupts") + + def gravity(self, node: DTNode) -> Any: + """Overrides DTNodeSortByAttrValue.weight(). + + Returns: + The interrupt priorities. + """ + return [ + irq.priority if irq.priority is not None else sys.maxsize + for irq in node.interrupts + ] + + +class DTNodeSortByRegAddr(DTNodeSortByAttr): + """Sort nodes by register address.""" + + def __init__(self) -> None: + super().__init__("registers") + + def gravity(self, node: DTNode) -> Any: + """Overrides DTNodeSortByAttrValue.weight(). + + Returns: + The register addresses. + """ + return [reg.address for reg in node.registers] + + +class DTNodeSortByRegSize(DTNodeSortByAttr): + """Sort nodes by register size.""" + + def __init__(self) -> None: + super().__init__("registers") + + def gravity(self, node: DTNode) -> Any: + """Overrides DTNodeSortByAttrValue.weight(). + + Returns: + The register addresses. + """ + return [reg.size for reg in node.registers] + + +class DTNodeSortByBindingDepth(DTNodeSortByAttr): + """Sort nodes by child-binding depth.""" + + def __init__(self) -> None: + super().__init__("binding") + + def gravity(self, node: DTNode) -> Any: + """Overrides DTNodeSortByAttrValue.weight(). + + Returns: + The interrupt priorities. + """ + return node.binding.cb_depth if node.binding else None + + +class DTNodeTextCriterion(DTNodeCriterion): + """Basis for text-based (pattern) criteria. + + A search pattern is matched to a node aspect that has + a textual representation. + + This criterion may either match a Regular Expression + or search for plain text. + + When the pattern is a strict RE, the criterion behaves as a RE-match, + and any character in the pattern may be interpreted as special character: + + - in particular, "*" will represent a repetition qualifier, + not a wild-card for any character: e.g. a pattern starting with "*" + would be an invalid RE because there's nothing to repeat + - parenthesis will group sub-expressions, as in "(image|storage).*" + - brackets will mark the beginning ("[") and end ("]") of a character set + + When the pattern is not a strict RE, but contains at least one "*": + + - "*" is actually interpreted as a wild-card and not a repetition qualifier: + here "*" is a valid expression that actually means "anything" + - the criterion behaves as a RE-match: "*pattern" means ends with "pattern", + "pattern*" starts with "pattern", and "*pattern*" contains "pattern" + + If the pattern is not a strict RE, and does not contain any "*": + + - specials characters won't be interpreted (plain text search) + - the criterion will behave as a RE-search + """ + + # The PATTERN argument from the command line. + _pattern: str + + # The RE that implements this criterion. + _re: re.Pattern[str] + + def __init__( + self, pattern: str, re_strict: bool = False, ignore_case: bool = False + ) -> None: + """Initialize criterion. + + Args: + pattern: The string pattern. + re_strict: Whether to assume the pattern us a Regular Expression. + Default is plain text search with wild-card substitution. + ignore_case: Whether to ignore case. + Default is case sensitive search. + + Raises: + re.error: Malformed regular expression. + """ + self._pattern = pattern + if re_strict: + self._re = self._init_strict_re(pattern, ignore_case) + else: + self._re = self._init_plain_text(pattern, ignore_case) + + @property + def pattern(self) -> str: + """The pattern string this criterion is built on.""" + return self._pattern + + def match(self, node: DTNode) -> bool: + """Overrides DTNodeCriterion.match().""" + return any( + self._re.match(txt) is not None for txt in self.get_haystack(node) + ) + + def get_haystack(self, node: DTNode) -> Sequence[str]: + """Get the textual representation of the haystack to search. + + The criterion is always False when the haystack is empty. + + Returns: + The strings that this pattern may match. + """ + del node + return [] + + def _init_strict_re( + self, pattern: str, ignore_case: bool + ) -> re.Pattern[str]: + # RE strict mode, use pattern (e.g. from command string) as-is. + return re.compile(pattern, flags=re.IGNORECASE if ignore_case else 0) + + def _init_plain_text( + self, pattern: str, ignore_case: bool + ) -> re.Pattern[str]: + # Plain text search, escape all. + pattern = re.escape(pattern) + if r"\*" in pattern: + # Convert wild-card to repeated printable. + pattern = pattern.replace(r"\*", ".*") + # Ensure starts/ends with semantic. + pattern = f"^{pattern}$" + else: + # Convert RE-match to RE-search. + pattern = f".*{pattern}.*" + + return re.compile(pattern, flags=re.IGNORECASE if ignore_case else 0) + + def __repr__(self) -> str: + return self._re.pattern + + +class DTNodeWithPath(DTNodeTextCriterion): + """Match path.""" + + def get_haystack(self, node: DTNode) -> Sequence[str]: + """Overrides DTNodeTextCriterion.get_haystack().""" + return [node.path] + + +class DTNodeWithStatus(DTNodeTextCriterion): + """Match status string.""" + + def get_haystack(self, node: DTNode) -> Sequence[str]: + """Overrides DTNodeTextCriterion.get_haystack().""" + return [node.status] + + +class DTNodeWithName(DTNodeTextCriterion): + """Match node name.""" + + def get_haystack(self, node: DTNode) -> Sequence[str]: + """Overrides DTNodeTextCriterion.get_haystack().""" + return [node.name] + + +class DTNodeWithUnitName(DTNodeTextCriterion): + """Match unit-name.""" + + def get_haystack(self, node: DTNode) -> Sequence[str]: + """Overrides DTNodeTextCriterion.get_haystack().""" + return [node.unit_name] + + +class DTNodeWithCompatible(DTNodeTextCriterion): + """Match compatible value.""" + + def get_haystack(self, node: DTNode) -> Sequence[str]: + """Overrides DTNodeTextCriterion.get_haystack().""" + return node.compatibles + + +class DTNodeWithBinding(DTNodeTextCriterion): + """Match binding compatible string. + + If the pattern is "*", will match any node with a binding, + including bindings without compatible string. + """ + + def match(self, node: DTNode) -> bool: + """Overrides DTNodeCriterion.match().""" + if self.pattern == ".*": + return node.binding is not None + return super().match(node) + + def get_haystack(self, node: DTNode) -> Sequence[str]: + """Overrides DTNodeTextCriterion.get_haystack().""" + if not node.binding: + return [] + + haystack = [] + if node.binding.compatible: + haystack.append(node.binding.compatible) + + headline = node.binding.get_headline() + if headline: + haystack.append(headline) + + return haystack + + +class DTNodeWithVendor(DTNodeTextCriterion): + """Match vendor prefix or name.""" + + def get_haystack(self, node: DTNode) -> Sequence[str]: + """Overrides DTNodeTextCriterion.get_haystack().""" + return [node.vendor.prefix, node.vendor.name] if node.vendor else [] + + +class DTNodeWithDeviceLabel(DTNodeTextCriterion): + """Match device label.""" + + def get_haystack(self, node: DTNode) -> Sequence[str]: + """Overrides DTNodeTextCriterion.get_haystack().""" + return [node.label] if node.label else [] + + +class DTNodeWithNodeLabel(DTNodeTextCriterion): + """Match DTS labels.""" + + def get_haystack(self, node: DTNode) -> Sequence[str]: + """Overrides DTNodeTextCriterion.get_haystack().""" + return node.labels + + +class DTNodeWithAlias(DTNodeTextCriterion): + """Match aliases.""" + + def get_haystack(self, node: DTNode) -> Sequence[str]: + """Overrides DTNodeTextCriterion.get_haystack().""" + return node.aliases + + +class DTNodeWithChosen(DTNodeTextCriterion): + """Match chosen.""" + + def get_haystack(self, node: DTNode) -> Sequence[str]: + """Overrides DTNodeTextCriterion.get_haystack().""" + return node.chosen + + +class DTNodeWithBus(DTNodeTextCriterion): + """Match nodes with supported bus protocols.""" + + def get_haystack(self, node: DTNode) -> Sequence[str]: + """Overrides DTNodeTextCriterion.get_haystack().""" + return node.buses + + +class DTNodeWithOnBus(DTNodeTextCriterion): + """Match nodes with bus of appearance.""" + + def get_haystack(self, node: DTNode) -> Sequence[str]: + """Overrides DTNodeTextCriterion.get_haystack().""" + return [node.on_bus] if node.on_bus else [] + + +class DTNodeWithDescription(DTNodeTextCriterion): + """Match node with description line by line.""" + + def get_haystack(self, node: DTNode) -> Sequence[str]: + """Overrides DTNodeTextCriterion.get_haystack().""" + if not node.description: + return [] + return node.description.splitlines() + + +class DTNodeAlsoKnownAs(DTNodeTextCriterion): + """Match nodes with labels and aliases.""" + + def get_haystack(self, node: DTNode) -> Sequence[str]: + """Overrides DTNodeTextCriterion.get_haystack().""" + aka = [ + *node.aliases, + *node.labels, + ] + if node.label: + aka.append(node.label) + return aka + + +class DTNodeIntCriterion(DTNodeCriterion): + """Basis for integer-based (expression) criteria.""" + + OPERATORS: Mapping[str, Callable[[int, int], bool]] = { + "<": operator.lt, + ">": operator.gt, + "=": operator.eq, + ">=": operator.ge, + "<=": operator.le, + "!=": operator.ne, + } + + _operator: Callable[[int, int], bool] + _int: Optional[int] + + def __init__( + self, + criter_op: Optional[Callable[[int, int], bool]], + criter_int: Optional[int], + ) -> None: + """Initialize criterion. + + Args: + criter_op: The expression operator, + one of DTNodeIntCriterion.OPERATORS. + Defaults to equality. + criter_int: The integer value the criterion expression should match. + None means any integer value will match. + """ + self._operator = criter_op or operator.eq + self._int = criter_int + + def match(self, node: DTNode) -> bool: + """Overrides DTNodeCriterion.match().""" + return any( + ((self._int is None) or self._operator(hay, self._int)) + for hay in self.get_haystack(node) + ) + + def get_haystack(self, node: DTNode) -> Sequence[int]: + """Get the integer representation of the haystack to search. + + The criterion is always False when the haystack is empty. + + Returns: + The integers that this expression may match. + """ + del node + return [] + + +class DTNodeWithUnitAddr(DTNodeIntCriterion): + """Match unit-address.""" + + def get_haystack(self, node: DTNode) -> Sequence[int]: + """Overrides DTNodeIntCriterion.get_haystack().""" + return [node.unit_addr] if node.unit_addr is not None else [] + + +class DTNodeWithIrqNumber(DTNodeIntCriterion): + """Match IRQ number.""" + + def get_haystack(self, node: DTNode) -> Sequence[int]: + """Overrides DTNodeIntCriterion.get_haystack().""" + return [irq.number for irq in node.interrupts] + + +class DTNodeWithIrqPriority(DTNodeIntCriterion): + """Match IRQ priority.""" + + def get_haystack(self, node: DTNode) -> Sequence[int]: + """Overrides DTNodeIntCriterion.get_haystack().""" + return [ + irq.priority for irq in node.interrupts if irq.priority is not None + ] + + +class DTNodeWithRegAddr(DTNodeIntCriterion): + """Match register address.""" + + def get_haystack(self, node: DTNode) -> Sequence[int]: + """Overrides DTNodeIntCriterion.get_haystack().""" + return [reg.address for reg in node.registers] + + +class DTNodeWithRegSize(DTNodeIntCriterion): + """Match register size.""" + + def get_haystack(self, node: DTNode) -> Sequence[int]: + """Overrides DTNodeIntCriterion.get_haystack().""" + return [reg.size for reg in node.registers] + + +class DTNodeWithBindingDepth(DTNodeIntCriterion): + """Match child-binding depth.""" + + def get_haystack(self, node: DTNode) -> Sequence[int]: + """Overrides DTNodeIntCriterion.get_haystack().""" + return [node.binding.cb_depth] if node.binding else [] + + +class DTWalkableComb(DTWalkable): + """Walk an arbitrary subset of a devicetree. + + Permits to define a virtual devicetree as the minimal graph + that will contain the paths from a given root to a selected + set of leaves. + + The comb is the set of nodes this graph includes. + """ + + _root: DTNode + _comb: Set[DTNode] + + def __init__(self, root: DTNode, leaves: Sequence[DTNode]) -> None: + """Initialize the virtual devicetree. + + Args: + root: Set the root node from where we'll later on + walk the virtual devicetree. + leaves: The leaf nodes of the virtual devicetree. + """ + self._root = root + self._comb: Set[DTNode] = set() + for leaf in leaves: + self._comb.update(list(leaf.rwalk())) + + @property + def comb(self) -> Set[DTNode]: + """All the nodes required to represent this virtual devicetree.""" + return self._comb + + def walk( + self, + /, + order_by: Optional[DTNodeSorter] = None, + reverse: bool = False, + enabled_only: bool = False, + fixed_depth: int = 0, + ) -> Iterator[DTNode]: + """Walk from the predefined root node through to all leaves. + + Overrides DTWalkable.walk(). + + Args: + enabled_only: Ignored, will always walk through to its leaves. + fixed_depth: Ignored, will always walk through to its leaves. + """ + return self._walk(self._root, order_by=order_by, reverse=reverse) + + def _walk( + self, + branch: Optional[DTNode] = None, + /, + order_by: Optional[DTNodeSorter] = None, + reverse: bool = False, + ) -> Iterator[DTNode]: + if branch in self._comb: + yield branch + children = branch.children + if children: + if order_by: + children = order_by.sort(children, reverse=reverse) + elif reverse: + # Reverse DTS-order. + children = list(reversed(children)) + for child in children: + yield from self._walk( + child, order_by=order_by, reverse=reverse + ) diff --git a/src/dtsh/rich/__init__.py b/src/dtsh/rich/__init__.py new file mode 100644 index 0000000..c9d1cc5 --- /dev/null +++ b/src/dtsh/rich/__init__.py @@ -0,0 +1,5 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Rich Text User Interface.""" diff --git a/src/dtsh/rich/autocomp.py b/src/dtsh/rich/autocomp.py new file mode 100644 index 0000000..d8172d4 --- /dev/null +++ b/src/dtsh/rich/autocomp.py @@ -0,0 +1,226 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Rich display callback for GNU readline integration.""" + + +from typing import Optional, Sequence, Set + +import os + +from rich.text import Text + +from dtsh.io import DTShOutput +from dtsh.rl import DTShReadline +from dtsh.autocomp import ( + DTShAutocomp, + RlStateDTShCommand, + RlStateDTShOption, + RlStateDTPath, + RlStateCompatStr, + RlStateDTVendor, + RlStateDTBus, + RlStateDTAlias, + RlStateDTChosen, + RlStateDTLabel, + RlStateFsEntry, + RlStateEnum, +) + +from dtsh.rich.theme import DTShTheme +from dtsh.rich.text import TextUtil +from dtsh.rich.tui import GridLayout + + +class DTShRichAutocomp(DTShAutocomp): + """Rich display callbacks for GNU readline integration.""" + + def display( + self, + out: DTShOutput, + states: Sequence[DTShReadline.CompleterState], + ) -> None: + """Rich display callback. + + Style completions based on their semantic. + + Implements DTShReadline.DisplayCallback. + Overrides DTShAutocomp.display(). + + Args: + out: Where to display these completions. + states: The completer states to display. + """ + grid = GridLayout(2, padding=(0, 4, 0, 0)) + + for state in states: + if isinstance(state, RlStateDTShCommand): + self._rlstates_view_add_dtshcmd(grid, state) + + elif isinstance(state, RlStateDTShOption): + self._rlstates_view_add_dtshopt(grid, state) + + elif isinstance(state, RlStateDTPath): + self._rlstates_view_add_dtpath(grid, state) + + elif isinstance(state, RlStateCompatStr): + self._rlstates_view_add_compatstr(grid, state) + + elif isinstance(state, RlStateDTVendor): + self._rlstates_view_add_vendor(grid, state) + + elif isinstance(state, RlStateDTBus): + self._rlstates_view_add_bus(grid, state) + + elif isinstance(state, RlStateDTAlias): + self._rlstates_view_add_alias(grid, state) + + elif isinstance(state, RlStateDTChosen): + self._rlstates_view_add_chosen(grid, state) + + elif isinstance(state, RlStateDTLabel): + self._rlstates_view_add_label(grid, state) + + elif isinstance(state, RlStateFsEntry): + self._rlstates_view_add_fspath(grid, state) + + elif isinstance(state, RlStateEnum): + self._rlstates_view_add_enum(grid, state) + + else: + grid.add_row(state.rlstr, None) + + out.write(grid) + + def _rlstates_view_add_dtshcmd( + self, grid: GridLayout, state: RlStateDTShCommand + ) -> None: + grid.add_row(TextUtil.bold(state.cmd.name), state.cmd.brief) + + def _rlstates_view_add_dtshopt( + self, grid: GridLayout, state: RlStateDTShOption + ) -> None: + grid.add_row(TextUtil.bold(state.opt.usage), state.opt.brief) + + def _rlstates_view_add_dtpath( + self, grid: GridLayout, state: RlStateDTPath + ) -> None: + txt = TextUtil.mk_text(state.node.name, DTShTheme.STYLE_DT_NODE_NAME) + if not state.node.enabled: + TextUtil.dim(txt) + grid.add_row(txt, None) + + def _rlstates_view_add_compatstr( + self, grid: GridLayout, state: RlStateCompatStr + ) -> None: + txt_desc: Optional[Text] = None + if state.bindings: + # The compatible string associates bindings, + # look for description. + if len(state.bindings) == 1: + # Single binding, use its description if any. + binding = state.bindings.pop() + if binding.description: + txt_desc = TextUtil.mk_headline( + binding.description, DTShTheme.STYLE_DT_BINDING_DESC + ) + else: + headlines: Set[str] = set() + buses: Set[str] = set() + + for binding in state.bindings: + if binding.on_bus: + buses.add(binding.on_bus) + headline = binding.get_headline() + if headline: + headlines.add(headline) + + if len(headlines) == 1: + # All associated bindings have the same description + # headline, use it. + txt_desc = TextUtil.mk_headline( + headlines.pop(), DTShTheme.STYLE_DT_BINDING_DESC + ) + elif buses: + # Tell user about different buses of appearance. + txt_desc = TextUtil.assemble( + TextUtil.italic("Available for different buses: "), + TextUtil.mk_text( + ", ".join(buses), DTShTheme.STYLE_DT_BUS + ), + ) + + txt_compat = TextUtil.mk_text( + state.compatstr, DTShTheme.STYLE_DT_COMPAT_STR + ) + + grid.add_row(txt_compat, txt_desc) + + def _rlstates_view_add_vendor( + self, grid: GridLayout, state: RlStateDTVendor + ) -> None: + grid.add_row( + TextUtil.mk_text(state.prefix, DTShTheme.STYLE_DT_COMPAT_STR), + TextUtil.mk_text(state.vendor, DTShTheme.STYLE_DT_VENDOR_NAME), + ) + + def _rlstates_view_add_bus( + self, grid: GridLayout, state: RlStateDTBus + ) -> None: + grid.add_row( + TextUtil.mk_text(state.proto, DTShTheme.STYLE_DT_BUS), + None, + ) + + def _rlstates_view_add_alias( + self, grid: GridLayout, state: RlStateDTAlias + ) -> None: + txt = TextUtil.mk_text(state.alias, DTShTheme.STYLE_DT_ALIAS) + if not state.node.enabled: + TextUtil.dim(txt) + grid.add_row(txt, None) + + def _rlstates_view_add_chosen( + self, grid: GridLayout, state: RlStateDTChosen + ) -> None: + txt = TextUtil.mk_text(state.chosen, DTShTheme.STYLE_DT_CHOSEN) + if not state.node.enabled: + TextUtil.dim(txt) + grid.add_row(txt, None) + + def _rlstates_view_add_label( + self, grid: GridLayout, state: RlStateDTLabel + ) -> None: + txt_label = TextUtil.mk_text(state.label, DTShTheme.STYLE_DT_NODE_LABEL) + if not state.node.enabled: + TextUtil.dim(txt_label) + if state.node.description: + txt_desc = TextUtil.mk_headline( + state.node.description, DTShTheme.STYLE_DT_BINDING_DESC + ) + if not state.node.enabled: + TextUtil.dim(txt_desc) + else: + txt_desc = None + grid.add_row(txt_label, txt_desc) + + def _rlstates_view_add_fspath( + self, layout: GridLayout, state: RlStateFsEntry + ) -> None: + if state.dirent.is_dir(): + txt = TextUtil.mk_text( + f"{state.dirent.name}{os.sep}", + style=DTShTheme.STYLE_FS_DIR, + ) + else: + txt = TextUtil.mk_text( + state.dirent.name, + style=DTShTheme.STYLE_FS_FILE, + ) + layout.add_row(txt, None) + + def _rlstates_view_add_enum( + self, grid: GridLayout, state: RlStateEnum + ) -> None: + grid.add_row(TextUtil.bold(state.value), state.brief) diff --git a/src/dtsh/rich/io.py b/src/dtsh/rich/io.py new file mode 100644 index 0000000..f58b7af --- /dev/null +++ b/src/dtsh/rich/io.py @@ -0,0 +1,494 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Rich I/O streams for devicetree shells. + +- rich VT +- rich redirection streams for SVG and HTML + +Rich I/O streams implementations are based on the rich.console module. +""" + + +from typing import Any, IO, List, Mapping, Optional + +import os + +from rich.console import Console, PagerContext +from rich.measure import Measurement +from rich.theme import Theme +from rich.terminal_theme import ( + SVG_EXPORT_THEME, + DEFAULT_TERMINAL_THEME, + MONOKAI, + DIMMED_MONOKAI, + NIGHT_OWLISH, + TerminalTheme, +) + +from dtsh.config import DTShConfig +from dtsh.io import DTShVT, DTShOutput, DTShRedirect + +from dtsh.rich.theme import DTShTheme +from dtsh.rich.svg import SVGContentsFmt, SVGContents + +_dtshconf: DTShConfig = DTShConfig.getinstance() +_theme: DTShTheme = DTShTheme.getinstance() + + +class DTShRichVT(DTShVT): + """Rich terminal for devicetree shells.""" + + _console: Console + _pager: Optional[PagerContext] + + def __init__(self) -> None: + """Initialize VT.""" + super().__init__() + self._console = Console(theme=Theme(_theme.styles), highlight=False) + self._pager = None + + def write(self, *args: Any, **kwargs: Any) -> None: + """Write to rich console. + + Overrides DTShOutput.write(). + + Args: + *args: Positional arguments, Console.print() semantic. + **kwargs: Keyword arguments, Console.print() semantic. + + """ + self._console.print(*args, **kwargs) + + def flush(self) -> None: + """Flush rich console output without sending LF. + + Overrides DTShOutput.flush(). + """ + self._console.print("", end="") + + def clear(self) -> None: + """Overrides DTShVT.clear().""" + self._console.clear() + + def pager_enter(self) -> None: + """Overrides DTShOutput.pager_enter().""" + if not self._pager: + self._console.clear() + self._pager = self._console.pager(styles=True, links=True) + # We have to explicitly enter the context since we need + # more control on it than the context manager would permit. + self._pager.__enter__() # pylint: disable=unnecessary-dunder-call + + def pager_exit(self) -> None: + """Overrides DTShOutput.pager_exit().""" + if self._pager: + self._pager.__exit__(None, None, None) + self._pager = None + + +class DTShOutputFileText(DTShOutput): + """Text output file for commands output redirection.""" + + _out: IO[str] + _console: Console + + def __init__(self, path: str, append: bool) -> None: + """Initialize output file. + + Args: + path: The output file path. + append: Whether to redirect the command's output in "append" mode. + + Raises: + DTShRedirect.Error: Invalid path or permission errors. + """ + try: + # We can't use a context manager here, we just want to open + # the file for later subsequent writes. + self._out = open( # pylint: disable=consider-using-with + path, + "a" if append else "w", + encoding="utf-8", + ) + if append: + # Insert blank line between command outputs. + self._out.write(os.linesep) + except OSError as e: + raise DTShRedirect.Error(e.strerror) from e + + self._console = Console( + highlight=False, + theme=Theme(_theme.styles), + record=True, + # Set the console's width to the configured maximum, + # we'll strip the rich segments on flush. + width=_dtshconf.pref_redir2_maxwidth, + ) + + def write(self, *args: Any, **kwargs: Any) -> None: + """Capture command's output. + + Overrides DTShOutput.write(). + + Args: + *args: Positional arguments, Console.print() semantic. + **kwargs: Keyword arguments, Console.print() semantic. + """ + with self._console.capture(): + self._console.print(*args, **kwargs) + + def flush(self) -> None: + """Format (HTML) the captured output and write it + to the redirection file. + + Overrides DTShOutput.flush(). + """ + contents = self._console.export_text() + # Exported lines are padded up to the (maximum) console width: + # strip these trailing whitespaces, which could make the text file + # unreadable. + for line_nopad in (line.rstrip() for line in contents.splitlines()): + print(line_nopad, file=self._out) + self._out.close() + + +class DTShOutputFileHtml(DTShOutput): + """HTML output file for commands output redirection.""" + + _out: IO[str] + _append: bool + _console: Console + + def __init__(self, path: str, append: bool) -> None: + """Initialize output file. + + Args: + path: The output file path. + append: Whether to redirect the command's output in "append" mode. + + Raises: + DTShRedirect.Error: Invalid path or permission errors. + """ + try: + self._append = append + self._out = open( # pylint: disable=consider-using-with + path, + "r+" if append else "w", + encoding="utf-8", + ) + except OSError as e: + raise DTShRedirect.Error(e.strerror) from e + + self._console = Console( + highlight=False, + theme=Theme(_theme.styles), + record=True, + # Set the console's width to the configured maximum, + # we'll post-process the generated HTML document on flush. + width=_dtshconf.pref_redir2_maxwidth, + ) + + if self._append: + # Write a blank line to the captured output + # as a commands separator when we append. + self.write() + + def write(self, *args: Any, **kwargs: Any) -> None: + """Capture command's output. + + Overrides DTShOutput.write(). + + Args: + *args: Positional arguments, Console.print() semantic. + **kwargs: Keyword arguments, Console.print() semantic. + """ + with self._console.capture(): + self._console.print(*args, **kwargs) + + def flush(self) -> None: + """Format (HTML) the captured output and write it + to the redirection file. + + Overrides DTShOutput.flush(). + """ + # Text and bakcround colors. + theme = DTSH_EXPORT_THEMES.get( + _dtshconf.pref_html_theme, DEFAULT_TERMINAL_THEME + ) + + html_fmt = DTSH_HTML_FORMAT.replace( + "|font_family|", _dtshconf.pref_html_font_family + ) + + html = self._console.export_html( + theme=theme, + code_format=html_fmt, + # Use inline CSS styles in "append" mode. + inline_styles=self._append, + ) + + # The generated HTML pad lines with withe spaces + # up to the console's width, which is ugly if you + # want to re-use the HTML source: clean this up. + html_lines: List[str] = [line.rstrip() for line in html.splitlines()] + + # Index of the first line we'll write to the HTML output file: + # - either 0, pointing to the first line for the current redirection + # contents, if we're creating a new file + # - or, in "append" mode, the index of the line containing + # the

 tag that represents the actual command's output
+        #
+        # Since this 
 tag appears immediately before the HTML epilog,
+        # we'll then just have to write the current re-direction's contents
+        # starting from this index.
+        i_output: int = 0
+
+        if self._append:
+            # Appending to an existing file: seek to the appropriate
+            # point of insertion.
+            self._seek_last_content()
+            self._out.write(os.linesep)
+
+            # Find command's output contents.
+            for i, line in enumerate(html_lines):
+                if line.find(" None:
+        # Offset for the point of insertion, just before the HTML epilog.
+        offset: int = self._out.tell()
+        line = self._out.readline()
+        while line and not line.startswith(""):
+            offset = self._out.tell()
+            line = self._out.readline()
+        self._out.seek(offset, os.SEEK_SET)
+
+
+class DTShOutputFileSVG(DTShOutput):
+    """SVG output file for commands output redirection."""
+
+    _out: IO[str]
+    _append: bool
+    _console: Console
+
+    _maxwidth: int
+    _width: int
+
+    def __init__(self, path: str, append: bool) -> None:
+        """Initialize output file.
+
+        Args:
+            path: The output file path.
+            append: Whether to redirect the command's output in "append" mode.
+
+        Raises:
+             DTShRedirect.Error: Invalid path or permission errors.
+        """
+        try:
+            self._append = append
+            self._out = open(  # pylint: disable=consider-using-with
+                path,
+                "r+" if append else "w",
+                encoding="utf-8",
+            )
+        except OSError as e:
+            raise DTShRedirect.Error(e.strerror) from e
+
+        # Maximum width allowed in preferences.
+        self._maxwidth = _dtshconf.pref_redir2_maxwidth
+
+        # Width required to output the command's output without wrapping or
+        # cropping, up to the configured maximum.
+        self._width = 0
+
+        self._console = Console(
+            highlight=False,
+            theme=Theme(_theme.styles),
+            record=True,
+            # Set the console's width to the configured maximum,
+            # we'll shrink it on flush.
+            width=self._maxwidth,
+        )
+
+        if self._append:
+            # Write a blank line to the captured output
+            # as a commands separator when we append.
+            self.write()
+
+    def write(self, *args: Any, **kwargs: Any) -> None:
+        """Capture command's output.
+
+        Overrides DTShOutput.write().
+
+        Args:
+            *args: Positional arguments, Console.print() semantic.
+            **kwargs: Keyword arguments, Console.print() semantic.
+        """
+        # Update required width.
+        for arg in args:
+            if (
+                isinstance(arg, str)
+                # Aka RichCast.
+                or hasattr(arg, "__rich__")
+                # Aka ConsoleRenderable.
+                or hasattr(arg, "__rich_console__")
+            ):
+                measure = Measurement.get(
+                    self._console, self._console.options, arg
+                )
+                if (measure.maximum > self._width) and not (
+                    measure.maximum > self._maxwidth
+                ):
+                    self._width = measure.maximum
+
+        with self._console.capture():
+            self._console.print(*args, **kwargs)
+
+    def flush(self) -> None:
+        """Format (SVG) the captured output and write it
+        to the redirection file.
+
+        Overrides DTShOutput.flush().
+        """
+        # Text and bakcround colors.
+        theme = DTSH_EXPORT_THEMES.get(
+            _dtshconf.pref_svg_theme, DEFAULT_TERMINAL_THEME
+        )
+
+        # Shrink the console's width, to prevent the SVG document from being
+        # unnecessarily wide.
+        self._console.width = self._width
+
+        # Get captured command output as SVG contents lines.
+        contents: List[str] = self._console.export_svg(
+            theme=theme,
+            title="",
+            code_format=SVGContentsFmt.get_format(
+                _dtshconf.pref_svg_font_family
+            ),
+            font_aspect_ratio=_dtshconf.pref_svg_font_ratio,
+        ).splitlines()
+
+        svg: SVGContents
+        try:
+            if self._append:
+                svg = self._svg_append(contents)
+            else:
+                svg = self._svg_create(contents)
+
+        except SVGContentsFmt.Error as e:
+            raise DTShRedirect.Error(str(e)) from e
+
+        self._svg_write(svg)
+        self._out.close()
+
+    def _svg_create(self, contents: List[str]) -> SVGContents:
+        svg: SVGContents = SVGContents(contents)
+        svg.top_padding_correction()
+        return svg
+
+    def _svg_append(self, contents: List[str]) -> SVGContents:
+        svgnew: SVGContents = SVGContents(contents)
+        svgnew.top_padding_correction()
+
+        svg: SVGContents = self._svg_read()
+        svg.append(svgnew)
+        return svg
+
+    def _svg_read(self) -> SVGContents:
+        contents: List[str] = self._out.read().splitlines()
+        self._out.seek(0, os.SEEK_SET)
+
+        # Padding correction was already done.
+        svg = SVGContents(contents)
+        return svg
+
+    def _svg_write(self, svg: SVGContents) -> None:
+        self._svg_write_prolog()
+        self._svg_writeln()
+
+        self._svg_write_styles(svg)
+        self._svg_writeln()
+
+        self._svg_write_defs(svg)
+        self._svg_writeln()
+
+        self._svg_write_chrome(svg)
+        self._svg_writeln()
+
+        self._svg_write_gterms(svg)
+        self._svg_write_epilog()
+
+    def _svg_write_prolog(self) -> None:
+        print(SVGContentsFmt.PROLOG, file=self._out)
+
+    def _svg_write_styles(self, svg: SVGContents) -> None:
+        print(SVGContentsFmt.CSS_STYLES_BEGIN, file=self._out)
+        for line in svg.styles:
+            print(line, file=self._out)
+        print(SVGContentsFmt.CSS_STYLES_END, file=self._out)
+
+    def _svg_write_defs(self, svg: SVGContents) -> None:
+        print(SVGContentsFmt.SVG_DEFS_BEGIN, file=self._out)
+        for line in svg.defs:
+            print(line, file=self._out)
+        print(SVGContentsFmt.SVG_DEFS_END, file=self._out)
+
+    def _svg_write_chrome(self, svg: SVGContents) -> None:
+        print(SVGContentsFmt.MARK_CHROME, file=self._out)
+        print(svg.rect, file=self._out)
+
+    def _svg_write_gterms(self, svg: SVGContents) -> None:
+        for gterm in svg.gterms:
+            print(SVGContentsFmt.MARK_GTERM_BEGIN, file=self._out)
+            for line in gterm.contents:
+                print(line, file=self._out)
+            print(SVGContentsFmt.MARK_GTERM_END, file=self._out)
+            self._svg_writeln()
+
+    def _svg_write_epilog(self) -> None:
+        print(SVGContentsFmt.EPILOG, file=self._out)
+
+    def _svg_writeln(self) -> None:
+        print(file=self._out)
+
+
+DTSH_HTML_FORMAT = """\
+
+
+
+
+
+
+
+
+
+
+
+    
{code}
+ + + + +""" + +DTSH_EXPORT_THEMES: Mapping[str, TerminalTheme] = { + "svg": SVG_EXPORT_THEME, + "html": DEFAULT_TERMINAL_THEME, + "dark": DIMMED_MONOKAI, + "light": NIGHT_OWLISH, + "night": MONOKAI, +} diff --git a/src/dtsh/rich/modelview.py b/src/dtsh/rich/modelview.py new file mode 100644 index 0000000..466c45d --- /dev/null +++ b/src/dtsh/rich/modelview.py @@ -0,0 +1,1421 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Devicetree model to views. + +Stateless factories of base Devicetree model elements. + +Context-aware views of DT nodes. +""" + +from typing import ( + Type, + Optional, + Union, + Iterable, + Sequence, + List, + Generator, + Mapping, + Dict, +) + +import enum + +from rich import box +from rich.console import RenderableType +from rich.padding import PaddingDimensions +from rich.style import StyleType +from rich.text import Text +from rich.tree import Tree + +from dtsh.model import ( + DTPath, + DTWalkable, + DTNode, + DTNodeRegister, + DTNodeInterrupt, + DTNodeSorter, +) +from dtsh.modelutils import ( + DTNodeSortByNodeLabel, + DTNodeSortByAlias, + DTNodeSortByCompatible, + DTNodeSortByRegSize, + DTNodeSortByRegAddr, + DTNodeSortByIrqNumber, + DTNodeSortByIrqPriority, + DTNodeSortByBus, +) +from dtsh.config import DTShConfig + +from dtsh.rich.tui import View, TableLayout, GridLayout +from dtsh.rich.text import TextUtil +from dtsh.rich.theme import DTShTheme + + +_dtshconf: DTShConfig = DTShConfig.getinstance() + + +class DTModelView: + """Stateless factory of base Devicetree model elements.""" + + SI_UNITS: Mapping[int, str] = { + 0: "bytes", + 1: "kB", + 2: "MB", + 3: "GB", + } + SI_KB: int = 1024 + + @classmethod + def mk_path_name(cls, pathname: str) -> Text: + """Text view factory for path names. + + Args: + pathname: A DT path name. + """ + DTPath.check_path_name(pathname) + + # Starts with "/" since pathname is absolute, + # equals (at least) to "/" for both the root node + # and its immediate children. + dirname: str = DTPath.dirname(pathname) + # The devicetree root basename will be empty. + basename: str = DTPath.basename(pathname) + + tv_branch: Optional[Text] + if basename: + if dirname != "/": + # Append the path separator to the branch path + # if not the devicetree root. + dirname += "/" + tv_branch = TextUtil.mk_text( + f"{dirname}", DTShTheme.STYLE_DT_PATH_BRANCH + ) + else: + # Empty base name (devicetree root): promote "/" to base name. + basename = "/" + # And skip the branch part. + tv_branch = None + + tv_node = TextUtil.mk_text(basename, DTShTheme.STYLE_DT_PATH_NODE) + if not tv_branch: + return tv_node + + return TextUtil.assemble(tv_branch, tv_node) + + @classmethod + def mk_path(cls, path: str) -> Text: + """Generic text view factory for DT paths. + + Args: + path: An absolute or relative DT path. + """ + names = DTPath.split(path) + branch_names: Sequence[str] = names[:-1] + node_name: str = names[-1] + + if branch_names: + if branch_names[0] == "/": + branch = "/" + "/".join(branch_names[1:]) + else: + branch = "/".join(branch_names) + tv_branch = TextUtil.mk_text( + f"{branch}/", DTShTheme.STYLE_DT_PATH_BRANCH + ) + else: + tv_branch = None + + tv_node = TextUtil.mk_text(node_name, DTShTheme.STYLE_DT_PATH_NODE) + if not tv_branch: + return tv_node + + return TextUtil.assemble(tv_branch, tv_node) + + @classmethod + def mk_addr(cls, addr: int, style: Optional[StyleType] = None) -> Text: + """Base text view factory for addresses. + + Address representation is an hexadecimal number, + uppercase if "pref_hex_upper" is set. + + Args: + addr: Address value. + style: Text style. + """ + straddr = hex(addr) + if _dtshconf.pref_hex_upper: + straddr = straddr.upper() + return TextUtil.mk_text(straddr, style) + + @classmethod + def mk_size(cls, size: int, style: Optional[StyleType] = None) -> Text: + """Base text view factory for memory sizes. + + Size representation is either: + + - if "pref_sizes_si" is set, a floating point number, + in SI units (bytes, kB, MB, GB) + - or an hexadecimal number, uppercase if "pref_hex_upper" is set + + Args: + size: Size in bytes. + style: Text style. + """ + # String representation of the size. + strsize: str + + if _dtshconf.pref_sizes_si: + # Retrieve appropriate SI unit. + left: int = size + pow_of_k = 0 + while left >= cls.SI_KB: + left >>= 10 + pow_of_k += 1 + si_unit = cls.SI_UNITS[pow_of_k] + si_unit_size = cls.SI_KB**pow_of_k + + # Format as integer or float depending on the remainder + # once we've subtracted the size that can be written + # as a multiple of the SI unit size. + si_quotient: int = size >> (10 * pow_of_k) + remainder: int = size - (si_quotient * si_unit_size) + if remainder: + si_size: float = si_quotient + (remainder / si_unit_size) + strsize = f"{si_size} {si_unit}" + else: + strsize = f"{si_quotient} {si_unit}" + else: + # Show size as hex. + strsize = hex(size) + if _dtshconf.pref_hex_upper: + strsize = strsize.upper() + + return TextUtil.mk_text(strsize, style) + + @classmethod + def mk_node_name(cls, name: str) -> Text: + """Text view factory for node names.""" + return TextUtil.mk_text(name, DTShTheme.STYLE_DT_NODE_NAME) + + @classmethod + def mk_unit_name(cls, unit_name: str) -> Text: + """Text view factory for unit names.""" + return TextUtil.mk_text(unit_name, DTShTheme.STYLE_DT_UNIT_NAME) + + @classmethod + def mk_unit_addr(cls, unit_addr: int) -> Text: + """Text view factory for unit addresses.""" + return cls.mk_addr(unit_addr, DTShTheme.STYLE_DT_UNIT_ADDR) + + @classmethod + def mk_device_label(cls, label: str) -> Text: + """Text view factory for "label" property values.""" + return TextUtil.mk_text(label, DTShTheme.STYLE_DT_DEVICE_LABEL) + + @classmethod + def mk_dts_label(cls, label: str) -> Text: + """Text view factory for DTS labels.""" + return TextUtil.mk_text(label, DTShTheme.STYLE_DT_NODE_LABEL) + + @classmethod + def mk_compat_str(cls, compat: str) -> Text: + """Text view factory for compatible strings.""" + return TextUtil.mk_text(compat, DTShTheme.STYLE_DT_COMPAT_STR) + + @classmethod + def mk_binding_compat(cls, compat: str) -> Text: + """Text view factory for compatible strings (from bindings).""" + return TextUtil.mk_text(compat, DTShTheme.STYLE_DT_BINDING_COMPAT) + + @classmethod + def mk_binding_headline(cls, desc: str) -> Text: + """Text view factory for description headlines.""" + return TextUtil.mk_headline(desc, DTShTheme.STYLE_DT_BINDING_DESC) + + @classmethod + def mk_binding_depth(cls, cb_depth: int) -> Text: + """Text view factory child-binding depth.""" + return TextUtil.mk_text( + str(cb_depth), + DTShTheme.STYLE_DT_IS_CHILD_BINDING + if (cb_depth > 0) + else DTShTheme.STYLE_DT_CB_ORDER, + ) + + @classmethod + def mk_vendor_name(cls, vendor: str) -> Text: + """Text view factory vendors.""" + return TextUtil.mk_text(vendor, DTShTheme.STYLE_DT_VENDOR_NAME) + + @classmethod + def mk_status(cls, status: str) -> Text: + """Text view factory status strings.""" + return TextUtil.mk_text( + status, + DTShTheme.STYLE_DT_STATUS_ENABLED + if status == "okay" + else DTShTheme.STYLE_DT_STATUS_DISABLED, + ) + + @classmethod + def mk_alias(cls, alias: str) -> Text: + """Text view factory for alias names.""" + return TextUtil.mk_text(alias, DTShTheme.STYLE_DT_ALIAS) + + @classmethod + def mk_bus(cls, bus: str) -> Text: + """Text view factory for bus protocols.""" + return TextUtil.mk_text(bus, DTShTheme.STYLE_DT_BUS) + + @classmethod + def mk_interrupt(cls, irq: DTNodeInterrupt) -> Text: + """Text view factory for interrupts.""" + tv_irq = TextUtil.join( + ":", + ( + TextUtil.mk_text( + str(irq.number), DTShTheme.STYLE_DT_IRQ_NUMBER + ), + TextUtil.mk_text( + str(irq.priority), DTShTheme.STYLE_DT_IRQ_PRIORITY + ), + ), + ) + if irq.name: + tv_irq = TextUtil.join( + " ", (tv_irq, TextUtil.mk_text(f"({irq.name})")) + ) + + return tv_irq + + @classmethod + def mk_reg_addr(cls, addr: int) -> Text: + """Text view factory for register addresses.""" + return cls.mk_addr(addr, DTShTheme.STYLE_DT_REG_ADDR) + + @classmethod + def mk_reg_size(cls, size: int) -> Text: + """Text view factory for register sizes.""" + return cls.mk_size(size, DTShTheme.STYLE_DT_REG_SIZE) + + @classmethod + def mk_register(cls, reg: DTNodeRegister) -> Text: + """Text view factory for registers (base address, size).""" + tv_addr = cls.mk_reg_addr(reg.address) + if reg.size: + tv_size = TextUtil.assemble("(", cls.mk_reg_size(reg.size), ")") + tv_reg = TextUtil.mk_text(" ").join((tv_addr, tv_size)) + else: + tv_reg = tv_addr + return tv_reg + + @classmethod + def mk_register_range(cls, reg: DTNodeRegister) -> Text: + """Text view factory for registers (address range).""" + tv_reg = cls.mk_reg_addr(reg.address) + if reg.size: + tv_reg = TextUtil.join( + f" {_dtshconf.wchar_arrow_right} ", + (tv_reg, cls.mk_reg_addr(reg.tail)), + ) + return tv_reg + + @classmethod + def mk_depends_on(cls, depends_on: str, dep_failed: bool) -> Text: + """Text view factory for node dependencies.""" + return TextUtil.mk_text( + depends_on, + DTShTheme.STYLE_DT_DEP_FAILED + if dep_failed + else DTShTheme.STYLE_DT_DEP_ON, + ) + + @classmethod + def mk_requiredy_by(cls, req_by: str) -> Text: + """Text view factory for dependent nodes.""" + return TextUtil.mk_text(req_by, DTShTheme.STYLE_DT_REQ_BY) + + +class SketchMV: + """Rendering context for view factories.""" + + class Layout(enum.Enum): + """Rendering layout.""" + + LIST_VIEW = 1 + """Rendering happends in the context of a list view. + + This is the widest rendering context. + """ + + TREE_VIEW = 2 + """Rendering happends in the context of a tree view. + + This is the rendering context for tree anchors. + """ + + TWO_SIDED = 3 + """Rendering happends in the context of a 2-sided view. + + A 2-sided view holds a tree (left) and a table (right) + where each node in the tree matches a table row.""" + + LIST_MULTI = 4 + """Rendering happends in the context of a list view. + + Same as LIST_VIEW but allows multiple-line cells. + """ + + _layout: "SketchMV.Layout" + _reversed: bool + _sorter: Optional[DTNodeSorter] + + def __init__( + self, + layout: "SketchMV.Layout", + sorter: Optional[DTNodeSorter] = None, + reverse: bool = False, + ) -> None: + """Initialize sketch. + + args: + layout: Rendering layout. + sorter: The order-by relationship applied to sort the nodes + we're rendering. + reverse: Whether to reverse output. + """ + self._layout = layout + self._sorter = sorter + self._reversed = reverse + + @property + def layout(self) -> "SketchMV.Layout": + """Rendering layout.""" + return self._layout + + @property + def default_fmt(self) -> str: + """The preferred default format string in this context. + + Depends on the rendering layout and user preferences. + """ + if self._layout == SketchMV.Layout.LIST_VIEW: + fmt = _dtshconf.pref_list_fmt + elif self._layout == SketchMV.Layout.TREE_VIEW: + fmt = _dtshconf.pref_tree_fmt + elif self._layout == SketchMV.Layout.TWO_SIDED: + # NOTE: should we add a preference for this ? + fmt = _dtshconf.pref_tree_fmt + elif self._layout == SketchMV.Layout.LIST_MULTI: + # NOTE: should we add a preference for this ? + fmt = _dtshconf.pref_list_fmt + else: + raise ValueError(self._layout) + return fmt + + def mk_placeholder(self) -> Optional[Text]: + """Make a placeholder. + + The actual placeholder character depends on the rendering layout + and user preferences. + """ + placeholder: Optional[str] = None + if self._layout == SketchMV.Layout.LIST_VIEW: + placeholder = _dtshconf.pref_list_placeholder + elif self._layout == SketchMV.Layout.TREE_VIEW: + placeholder = _dtshconf.pref_tree_placeholder + elif self._layout == SketchMV.Layout.TWO_SIDED: + # NOTE: should we add a preference for this ? + placeholder = _dtshconf.pref_tree_placeholder + elif self._layout == SketchMV.Layout.LIST_MULTI: + # NOTE: should we add a preference for this ? + placeholder = _dtshconf.pref_list_placeholder + else: + raise ValueError(self._layout) + + return TextUtil().mk_text(placeholder) if placeholder else None + + def link(self, text: Union[str, Text], uri: str) -> Text: + """Link text. + + The actual actionable view link depends on + the rendering context and user preferences. + + Args: + text: The text to link. + uri: The link destination. + + Return: + An actionable text. + """ + if self._layout == SketchMV.Layout.LIST_VIEW: + action_type = _dtshconf.pref_list_actionable_type + elif self._layout == SketchMV.Layout.TREE_VIEW: + action_type = _dtshconf.pref_tree_actionable_type + elif self._layout == SketchMV.Layout.TWO_SIDED: + action_type = _dtshconf.pref_2Sided_actionable_type + elif self._layout == SketchMV.Layout.LIST_MULTI: + # Use default. + action_type = _dtshconf.pref_actionable_type + else: + raise ValueError(self._layout) + return TextUtil.link(text, uri, action_type) + + def with_sorter(self, sorter_t: Type[DTNodeSorter]) -> bool: + """Check if the rendering context includes a sorter. + + Args: + sorter_t: The order-by relationship to test. + + Returns: + True if we're rendering nodes sorted by this order-by relationship. + """ + return isinstance(self._sorter, sorter_t) + + def with_reverse(self) -> bool: + """Whether the rendering should reverse output.""" + return self._reversed + + +class NodeMV: + """Stateless view factory for some node aspect. + + A node aspect is some information about the node that can be represented + by a list of text views: this is the raw representation returned + by mk_text(). + + This representation is post-processed by mk_view() to factorize + boilerplate code: + + - possibly create a placeholder when the aspect does not have a value + (e.g. a node without unit address) + - dim text views for disabled nodes + - build the final view depending on the rendering layout + """ + + T = Type["NodeMV"] + """Type to represent a concrete stateless factory.""" + + @classmethod + def mk_text(cls, node: DTNode, sketch: SketchMV) -> Sequence[Text]: + """Make the raw representation. + + Args: + node: The node for which the rendering happens. + sketch: The rendering context. + + Returns: + A list of text views. + """ + del node # Unused in base class. + del sketch # Unused in base class. + return [] + + @classmethod + def mk_view( + cls, node: DTNode, sketch: SketchMV + ) -> Optional[RenderableType]: + """Make the view that represents this node aspect. + + Args: + node: The node for which the rendering happens. + sketch: The rendering context. + + returns: + A view. + """ + tvs = cls.mk_text(node, sketch) + + if not tvs: + placeholder = sketch.mk_placeholder() + if placeholder: + tvs = [placeholder] + else: + # The aspect does not evaluate for the node, and no placeholder. + return None + + if not node.enabled: + for tv in tvs: + TextUtil.disabled(tv) + + if len(tvs) == 1: + # Single value: answer the text view, even if layout supports + # multiple-line cells. + return tvs[0] + + # Multiple value, we assume mk_text() was called with LIST_MULTI. + view = GridLayout() + for tv in tvs: + view.add_row(tv) + return view + + +class NodeColumnMV: + """A node column is a view factory to be used in node tables.""" + + _header: str + _modelview: NodeMV.T + + def __init__(self, header: str, modelview: NodeMV.T) -> None: + """Define column. + + Args: + header: The column header. + modelview: The view factory. + """ + self._header = header + self._modelview = modelview + + @property + def header(self) -> str: + """The column header.""" + return self._header + + @property + def modelview(self) -> NodeMV.T: + """The view factory.""" + return self._modelview + + def mk_view( + self, node: DTNode, sketch: SketchMV + ) -> Optional[RenderableType]: + """Shortcut to call view factory. + + Args: + node: The node for which the rendering happens. + sketch: The rendering context. + """ + return self._modelview.mk_view(node, sketch) + + +class ViewNodeTable(TableLayout): + """Table view for DT nodes.""" + + _cols: Sequence[NodeColumnMV] + _sketch: SketchMV + + def __init__( + self, + cols: Sequence[NodeColumnMV], + sketch: SketchMV, + /, + padding: PaddingDimensions = (0, 0, 0, 0), + show_header: bool = True, + no_wrap: bool = True, + expand: bool = False, + ) -> None: + """Initialize table view. + + Args: + cols: The table columns. + sketch: Rendering context. + padding: Cells padding (CSS style), as one integer (TPLR), + or a pair of integers (TB, LR), or 3 integers (T, LR, B), + or 4 integers (T, R, B, L). Defaults to 0. + show_header: Whether to show the table's header row. + no_wrap: Disable wrapping entirely for this layout. + expand: Whether to expand the table to fit the available space. + """ + super().__init__( + [col.header for col in cols], + padding=padding, + show_header=show_header, + no_wrap=no_wrap, + expand=expand, + ) + self._cols = cols + self._sketch = sketch + + def append(self, node: DTNode) -> None: + """Append a DT node to this table. + + NOTE: Won't check for duplicates. + """ + cols: List[Optional[RenderableType]] = [ + col.mk_view(node, self._sketch) for col in self._cols + ] + self.add_row(*cols) + + def extend(self, nodes: Iterable[DTNode]) -> None: + """Append DT nodes to this list. + + NOTE: Won't check for duplicates. + """ + for node in nodes: + self.append(node) + + +class ViewNodeList(ViewNodeTable): + """List view for DT nodes.""" + + def __init__( + self, + cols: Sequence[NodeColumnMV], + sketch: SketchMV, + ) -> None: + """Initialize view. + + Args: + cols: The table columns. + sketch: Rendering context. + """ + super().__init__( + cols, + sketch, + padding=(0, 1, 0, 1), + show_header=_dtshconf.pref_list_headers, + no_wrap=True, + ) + if self._sketch.layout == SketchMV.Layout.LIST_MULTI: + # When we allow multiple-line cells, draw lines to distinguish rows. + self._table.show_lines = True + self._table.box = box.HORIZONTALS + + +class ViewDTWalkable(View): + """Base view for DT walk-able.""" + + _tree: Optional[Tree] + _walkable: DTWalkable + + def __init__(self, walkable: DTWalkable) -> None: + """New view. + + The view will remain an empty placeholder until walk_layout() is called. + + Args: + walkable: The virtual devicetree to render. + """ + super().__init__() + self._walkable = walkable + # Initialized on walk_layout(). + self._tree = None + + @property + def renderable(self) -> RenderableType: + """A rich Tree.""" + return self._tree or super().renderable + + def walk_layout( + self, + order_by: Optional[DTNodeSorter] = None, + reverse: bool = False, + enabled_only: bool = False, + fixed_depth: int = 0, + ) -> Generator[DTNode, None, None]: + """Layout this tree in order of traversal. + + Derived classes may override this method to insert additional + initialization before mk_anchor() and mk_label() are called. + + Args: + order_by: Children ordering while walking branches. + None will preserve the DTS order. + reverse: Whether to reverse children order. + If set and no order_by is given, means reversed DTS order. + enabled_only: Whether to stop at disabled branches. + fixed_depth: The depth limit, defaults to 0, + walking through to leaf nodes, according to enabled_only. + + Yields: + The added nodes in order of traversal. + """ + # Map devicetree branches (nodes) to their Tree representation. + branch2tree: Dict[DTNode, Tree] = {} + + walker = self._walkable.walk( + order_by=order_by, + reverse=reverse, + enabled_only=enabled_only, + fixed_depth=fixed_depth, + ) + + # Initialize tree view with the walk-able root. + try: + root: DTNode = next(walker) + except StopIteration: + # Empty walk-able. + return + self._tree = Tree(self.mk_anchor(root)) + branch2tree[root] = self._tree + yield root + + for node in walker: + anchor = branch2tree[node.parent] + branch2tree[node] = anchor.add(self.mk_label(node)) + yield node + + def do_layout( + self, + order_by: Optional[DTNodeSorter] = None, + reverse: bool = False, + enabled_only: bool = False, + fixed_depth: int = 0, + ) -> None: + """Same as walk_layout(), but consuming the generator.""" + for _ in self.walk_layout(order_by, reverse, enabled_only, fixed_depth): + pass + + def mk_anchor(self, root: DTNode) -> RenderableType: + """View factory for the Tree anchor (root). + + Derived classes may override this method. + + Args: + root: The walk-able root. + + Returns: + The Tree view's anchor (default to the node name). + """ + return root.name + + def mk_label(self, node: DTNode) -> RenderableType: + """View factory for the Tree labels (nodes). + + Derived classes may override this method. + + Args: + node: The node to make the label of. + + Returns: + The Tree label (default to the node name). + """ + return node.name + + +class ViewNodeTreePOSIX(ViewDTWalkable): + """POSIX-like tree view of a DT walk-able.""" + + _path: str + + def __init__( + self, + path: str, + walkable: DTWalkable, + ) -> None: + """New view. + + Args: + path: The DT path expression to use as anchor. + Like with the POSIX tree command, + "." will be substituted for an empty path. + """ + super().__init__(walkable) + self._path = path or "." + + def mk_anchor(self, root: DTNode) -> RenderableType: + """Overrides ViewDTWalkable.mk_anchor().""" + return self._path + + +class ViewDTWalkableMV(ViewDTWalkable): + """Tree view for DT walk-able with formatted node labels. + + Typically embedded in a parent layout. + """ + + # Initialized on walk_layout(). + _sketch: Optional[SketchMV] + + def __init__( + self, + walkable: DTWalkable, + mv: NodeMV.T, + ) -> None: + """Initialize view. + + Args: + walkable: The virtual devicetree to render. + mv: The node format to render tree labels with. + """ + super().__init__(walkable) + self._mv = mv + self._sketch = None + + def walk_layout( + self, + order_by: Optional[DTNodeSorter] = None, + reverse: bool = False, + enabled_only: bool = False, + fixed_depth: int = 0, + ) -> Generator[DTNode, None, None]: + """Overrides ViewDTWalkable.walk_layout().""" + self._sketch = SketchMV(SketchMV.Layout.TREE_VIEW, order_by, reverse) + return super().walk_layout(order_by, reverse, enabled_only, fixed_depth) + + def mk_anchor(self, root: DTNode) -> RenderableType: + """Overrides ViewDTWalkable.mk_anchor().""" + return self.mk_label(root) + + def mk_label(self, node: DTNode) -> RenderableType: + """Overrides ViewDTWalkable.mk_label().""" + label = self._mv.mk_view(node, self._sketch) if self._sketch else None + return label or View.SUB + + +class ViewNodeTwoSided(GridLayout): + """Two-sided view for DT nodes.""" + + _walkable: DTWalkable + _cols: Sequence[NodeColumnMV] + + def __init__( + self, walkable: DTWalkable, cols: Sequence[NodeColumnMV] + ) -> None: + """Initialize view. + + No rendering happens until do_layout() is called. + + Args: + walkable: The virtual devicetree to render (left-side). + cols: The table columns (right-side). + """ + super().__init__(2) + self._walkable = walkable + self._cols = cols + + def do_layout( + self, + order_by: Optional[DTNodeSorter] = None, + reverse: bool = False, + enabled_only: bool = False, + fixed_depth: int = 0, + ) -> None: + """Layout this view. + + Args: + order_by: Children ordering while walking branches. + None will preserve the DTS order. + reverse: Whether to reverse children order. + If set and no order_by is given, means reversed DTS order. + enabled_only: Whether to stop at disabled branches. + fixed_depth: The depth limit, defaults to 0, + walking through to leaf nodes, according to enabled_only. + """ + # Left side: tree view + left_treeview = ViewDTWalkableMV( + self._walkable, self._cols[0].modelview + ) + if _dtshconf.pref_tree_headers and self._cols[1:]: + # If a non empty right-side list-view shows its headers, + # move the left-side tree-view two lines bellow. + left_treeview.top_indent(2) + + # Right side: tree view + right_listview = ViewNodeList( + self._cols[1:], + SketchMV(SketchMV.Layout.TWO_SIDED, order_by, reverse), + ) + # Left-indented with two spaces relative to the tree at left-side. + right_listview.left_indent(2) + + # Layout both sides in sync. + for node in left_treeview.walk_layout( + order_by, reverse, enabled_only, fixed_depth + ): + right_listview.append(node) + + self.add_row(left_treeview, right_listview) + + +class PathNameNodeMV(NodeMV): + """View factory for node paths.""" + + @classmethod + def mk_text(cls, node: DTNode, sketch: SketchMV) -> Sequence[Text]: + """Overrides NodeMV.mk_text().""" + return (DTModelView.mk_path_name(node.path),) + + +class NodeNameNodeMV(NodeMV): + """View factory for node names.""" + + @classmethod + def mk_text(cls, node: DTNode, sketch: SketchMV) -> Sequence[Text]: + """Overrides NodeMV.mk_text().""" + return (DTModelView.mk_node_name(node.name),) + + +class UnitNameNodeMV(NodeMV): + """View factory for unit names.""" + + @classmethod + def mk_text(cls, node: DTNode, sketch: SketchMV) -> Sequence[Text]: + """Overrides NodeMV.mk_text().""" + return (DTModelView.mk_unit_name(node.unit_name),) + + +class UnitAddrNodeMV(NodeMV): + """View factory for unit names.""" + + @classmethod + def mk_text(cls, node: DTNode, sketch: SketchMV) -> Sequence[Text]: + """Overrides NodeMV.mk_text().""" + if node.unit_addr is None: + return [] + return (DTModelView.mk_unit_addr(node.unit_addr),) + + +class DepOrdinalNodeMV(NodeMV): + """View factory for dependency ordinals.""" + + @classmethod + def mk_text(cls, node: DTNode, sketch: SketchMV) -> Sequence[Text]: + """Overrides NodeMV.mk_text().""" + return ( + TextUtil.mk_text(str(node.dep_ordinal), DTShTheme.STYLE_DT_ORDINAL), + ) + + +class DeviceLabelNodeMV(NodeMV): + """View factory for device labels. + + Note: it seems the "label" property is now deprecated by Zephyr. + """ + + @classmethod + def mk_text(cls, node: DTNode, sketch: SketchMV) -> Sequence[Text]: + """Overrides NodeMV.mk_text().""" + if not node.label: + return [] + return (DTModelView.mk_device_label(node.label),) + + +class NodeLabelsNodeMV(NodeMV): + """View factory for DTS labels.""" + + @classmethod + def mk_text(cls, node: DTNode, sketch: SketchMV) -> Sequence[Text]: + """Overrides NodeMV.mk_text().""" + if not node.labels: + return [] + + # In-cell sort. + labels: Sequence[str] + if sketch.with_sorter(DTNodeSortByNodeLabel): + labels = sorted(node.labels, reverse=sketch.with_reverse()) + else: + labels = node.labels + + tvs_labels = (DTModelView.mk_dts_label(label) for label in labels) + + if sketch.layout == SketchMV.Layout.LIST_MULTI: + return list(tvs_labels) + + return (TextUtil.join(", ", tvs_labels),) + + +class CompatibleNodeMV(NodeMV): + """View factory for "compatible" property values.""" + + @classmethod + def mk_text(cls, node: DTNode, sketch: SketchMV) -> Sequence[Text]: + """Overrides NodeMV.mk_text().""" + if not node.compatibles: + return [] + + # In-cell sort. + compats: Sequence[str] + if sketch.with_sorter(DTNodeSortByCompatible): + compats = sorted(node.compatibles, reverse=sketch.with_reverse()) + else: + compats = node.compatibles + + tvs_compats: List[Text] = [] + for compat in compats: + binding_path: Optional[str] = None + if compat == node.compatible: + txt_compat = DTModelView.mk_binding_compat(compat) + binding_path = node.binding_path + else: + txt_compat = DTModelView.mk_compat_str(compat) + binding = node.dt.get_compatible_binding(compat, node.on_bus) + if binding: + binding_path = binding.path + + if binding_path: + txt_compat = sketch.link(txt_compat, binding_path) + + tvs_compats.append(txt_compat) + + if sketch.layout == SketchMV.Layout.LIST_MULTI: + return tvs_compats + + return (TextUtil.join(" ", tvs_compats),) + + +class BindingNodeMV(NodeMV): + """View factory for "compatible" property values.""" + + @classmethod + def mk_text(cls, node: DTNode, sketch: SketchMV) -> Sequence[Text]: + """Overrides NodeMV.mk_text().""" + if not node.binding: + return [] + + binding = node.binding + if not (binding.compatible or binding.description): + return [] + + tv_binding: Optional[Text] = None + if binding.compatible: + tv_binding = DTModelView.mk_binding_compat(binding.compatible) + elif binding.description: + tv_binding = DTModelView.mk_binding_headline(binding.description) + + if not tv_binding: + # Binding has no representation. + return [] + + # Child-bindings layout. + cb_depth: int = binding.cb_depth + # Should we anchor child-bindings to their parent node's binding ? + cb_anchor: Optional[str] = _dtshconf.pref_tree_cb_anchor + cb_anchored = ( + sketch.layout == SketchMV.Layout.TWO_SIDED + and cb_depth + and cb_anchor + ) + + if cb_anchored: + spc_indent = 2 * (cb_depth - 1) * " " + tv_cb = TextUtil.mk_text( + f"{spc_indent}{cb_anchor} ", + DTShTheme.STYLE_DT_CB_ANCHOR, + ) + tv_binding = TextUtil.assemble(tv_cb, tv_binding) + else: + # Link only to top-level bindings, + # child-bindings are defined by the same binding file. + tv_binding = sketch.link(tv_binding, binding.path) + + return (tv_binding,) + + +class BindingDepthNodeMV(NodeMV): + """View factory for child-binding depths.""" + + @classmethod + def mk_text(cls, node: DTNode, sketch: SketchMV) -> Sequence[Text]: + """Overrides NodeMV.mk_text().""" + if not node.binding: + return [] + tv_depth = DTModelView.mk_binding_depth(node.binding.cb_depth) + tv_depth.justify = "center" + return (tv_depth,) + + +class DescriptionNodeMV(NodeMV): + """View factory for descriptions.""" + + @classmethod + def mk_text(cls, node: DTNode, sketch: SketchMV) -> Sequence[Text]: + """Overrides NodeMV.mk_text().""" + if not node.description: + return [] + + tv_desc = DTModelView.mk_binding_headline(node.description) + if node.binding_path: + tv_desc = sketch.link(tv_desc, node.binding_path) + + return (tv_desc,) + + +class VendorNodeMV(NodeMV): + """View factory for vendor names.""" + + @classmethod + def mk_text(cls, node: DTNode, sketch: SketchMV) -> Sequence[Text]: + """Overrides NodeMV.mk_text().""" + if not node.vendor: + return [] + return (DTModelView.mk_vendor_name(node.vendor.name),) + + +class StatusNodeMV(NodeMV): + """View factory for status strings.""" + + @classmethod + def mk_text(cls, node: DTNode, sketch: SketchMV) -> Sequence[Text]: + """Overrides NodeMV.mk_text().""" + return (DTModelView.mk_status(node.status),) + + +class AliasesNodeMV(NodeMV): + """View factory for node aliases.""" + + @classmethod + def mk_text(cls, node: DTNode, sketch: SketchMV) -> Sequence[Text]: + """Overrides NodeMV.mk_text().""" + if not node.aliases: + return [] + + # In-cell sort. + aliases: Sequence[str] + if sketch.with_sorter(DTNodeSortByAlias): + aliases = sorted(node.aliases, reverse=sketch.with_reverse()) + else: + aliases = node.aliases + + tvs_aliases = (DTModelView.mk_alias(alias) for alias in aliases) + + if sketch.layout == SketchMV.Layout.LIST_MULTI: + return list(tvs_aliases) + + return (TextUtil.join(", ", tvs_aliases),) + + +class AlsoKnownAsNodeMV(NodeMV): + """View factory for all labels aliases.""" + + @classmethod + def mk_text(cls, node: DTNode, sketch: SketchMV) -> Sequence[Text]: + """Overrides NodeMV.mk_text().""" + if not (node.aliases or node.label or node.labels): + return [] + + tvs_aka = ( + *DeviceLabelNodeMV.mk_text(node, sketch), + *NodeLabelsNodeMV.mk_text(node, sketch), + *AliasesNodeMV.mk_text(node, sketch), + ) + + if sketch.layout == SketchMV.Layout.LIST_MULTI: + return list(tvs_aka) + + return (TextUtil.join(", ", tvs_aka),) + + +class BusesNodeMV(NodeMV): + """View factory for bus protocols.""" + + @classmethod + def mk_text(cls, node: DTNode, sketch: SketchMV) -> Sequence[Text]: + """Overrides NodeMV.mk_text().""" + if not node.buses: + return [] + + # In-cell sort. + buses: Sequence[str] + if sketch.with_sorter(DTNodeSortByBus): + buses = sorted(node.buses, reverse=sketch.with_reverse()) + else: + buses = node.buses + + tvs_buses = (DTModelView.mk_bus(bus) for bus in buses) + + if sketch.layout == SketchMV.Layout.LIST_MULTI: + return list(tvs_buses) + + return (TextUtil.join(" ", tvs_buses),) + + +class OnBusNodeMV(NodeMV): + """View factory for buses of appearance.""" + + @classmethod + def mk_text(cls, node: DTNode, sketch: SketchMV) -> Sequence[Text]: + """Overrides NodeMV.mk_text().""" + if not node.on_bus: + return [] + return (DTModelView.mk_bus(node.on_bus),) + + +class BusNodeMV(NodeMV): + """View factory for bus info.""" + + @classmethod + def mk_text(cls, node: DTNode, sketch: SketchMV) -> Sequence[Text]: + """Overrides NodeMV.mk_text().""" + tv_businfo: Optional[Text] = None + tvs_buses = BusesNodeMV.mk_text(node, sketch) + tvs_on_bus = OnBusNodeMV.mk_text(node, sketch) + + if tvs_buses: + tv_businfo = TextUtil.join(", ", tvs_buses) + + if tvs_on_bus: + if tv_businfo: + tv_businfo = TextUtil.join(" on ", (tv_businfo, tvs_on_bus[0])) + else: + tv_businfo = TextUtil.assemble("on ", tvs_on_bus[0]) + + return (tv_businfo,) if tv_businfo else [] + + +class InterruptsNodeMV(NodeMV): + """View factory for generated interrupts.""" + + @classmethod + def mk_text(cls, node: DTNode, sketch: SketchMV) -> Sequence[Text]: + """Overrides NodeMV.mk_text().""" + if not node.interrupts: + return [] + + # In-cell sort. + irqs: Sequence[DTNodeInterrupt] + if sketch.with_sorter(DTNodeSortByIrqNumber): + irqs = DTNodeInterrupt.sort_by_number( + node.interrupts, sketch.with_reverse() + ) + elif sketch.with_sorter(DTNodeSortByIrqPriority): + irqs = DTNodeInterrupt.sort_by_priority( + node.interrupts, sketch.with_reverse() + ) + else: + irqs = node.interrupts + + tvs_irqs = (DTModelView.mk_interrupt(irq) for irq in irqs) + + if sketch.layout == SketchMV.Layout.LIST_MULTI: + return list(tvs_irqs) + + return (TextUtil.join(", ", tvs_irqs),) + + +class RegistersNodeMV(NodeMV): + """View factory for node registers (base address, size).""" + + @classmethod + def mk_text(cls, node: DTNode, sketch: SketchMV) -> Sequence[Text]: + """Overrides NodeMV.mk_text().""" + if not node.registers: + return [] + + # In-cell sort. + regs: Sequence[DTNodeRegister] + if sketch.with_sorter(DTNodeSortByRegAddr): + regs = DTNodeRegister.sort_by_addr( + node.registers, sketch.with_reverse() + ) + elif sketch.with_sorter(DTNodeSortByRegSize): + regs = DTNodeRegister.sort_by_size( + node.registers, sketch.with_reverse() + ) + else: + regs = node.registers + + tvs_regs = (DTModelView.mk_register(reg) for reg in regs) + + if sketch.layout == SketchMV.Layout.LIST_MULTI: + return list(tvs_regs) + + return (TextUtil.join(", ", tvs_regs),) + + +class RegisterRangesNodeMV(NodeMV): + """View factory for node registers (address range).""" + + @classmethod + def mk_text(cls, node: DTNode, sketch: SketchMV) -> Sequence[Text]: + """Overrides NodeMV.mk_text().""" + if not node.registers: + return [] + + # In-cell sort. + regs: Sequence[DTNodeRegister] + if sketch.with_sorter(DTNodeSortByRegAddr): + regs = DTNodeRegister.sort_by_addr( + node.registers, sketch.with_reverse() + ) + elif sketch.with_sorter(DTNodeSortByRegSize): + regs = DTNodeRegister.sort_by_size( + node.registers, sketch.with_reverse() + ) + else: + regs = node.registers + + tvs_regs = (DTModelView.mk_register_range(reg) for reg in regs) + + if sketch.layout == SketchMV.Layout.LIST_MULTI: + return list(tvs_regs) + + return (TextUtil.join(", ", tvs_regs),) + + +class DepOnNodeMV(NodeMV): + """View factory for depend-on.""" + + @classmethod + def mk_text(cls, node: DTNode, sketch: SketchMV) -> Sequence[Text]: + """Overrides NodeMV.mk_text().""" + if not node.depends_on: + return [] + + tvs_dep_on: List[Text] = [] + for dep in node.depends_on: + dep_failed = False + if node.enabled: + # Node is enabled, dependencies should be enabled. + for parent in dep.rwalk(): + if not parent.enabled: + dep_failed = True + + tvs_dep_on.append(DTModelView.mk_depends_on(dep.name, dep_failed)) + + if sketch.layout == SketchMV.Layout.LIST_MULTI: + return tvs_dep_on + + return (TextUtil.join(", ", tvs_dep_on),) + + +class ReqByNodeMV(NodeMV): + """View factory for required-by.""" + + @classmethod + def mk_text(cls, node: DTNode, sketch: SketchMV) -> Sequence[Text]: + """Overrides NodeMV.mk_text().""" + if not node.required_by: + return [] + + tvs_req_by = ( + DTModelView.mk_requiredy_by(node.name) for node in node.required_by + ) + # NOTE: if nodes are ordered, also order required_by ? + + if sketch.layout == SketchMV.Layout.LIST_MULTI: + return list(tvs_req_by) + + return (TextUtil.join(", ", tvs_req_by),) + + +class ViewNodeAkaList(GridLayout): + """Base view for Also Known As (e.g. aliases).""" + + def __init__( + self, + aka2node: Mapping[str, DTNode], + cols: Sequence[NodeColumnMV], + no_wrap: bool = True, + expand: bool = False, + ) -> None: + """Initialize view. + + Args: + aka2node: Map "Also Known As" to nodes. + cols: The list view columns. + no_wrap: Disable wrapping entirely for this layout. + expand: Whether to expand the table to fit the available space. + """ + super().__init__( + 2, + padding=(0, 2, 0, 0), + no_wrap=no_wrap, + expand=expand, + ) + + grid_aka = GridLayout(2, padding=(0, 1, 0, 1)) + for name in aka2node: + text = TextUtil.mk_text(name, DTShTheme.STYLE_DT_ALIAS) + grid_aka.add_row(text, _dtshconf.wchar_arrow_right) + + listview = ViewNodeList(cols, SketchMV(SketchMV.Layout.LIST_VIEW)) + listview.extend(list(node for node in aka2node.values())) + + if _dtshconf.pref_list_headers: + grid_aka.top_indent(2) + + self.add_row(grid_aka, listview) diff --git a/src/dtsh/rich/session.py b/src/dtsh/rich/session.py new file mode 100644 index 0000000..153dbad --- /dev/null +++ b/src/dtsh/rich/session.py @@ -0,0 +1,142 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Rich interactive devicetree shell session. + +Extend the base session with a few rich TUI elements +and support for SVG and HTML command output redirection formats. +""" + +import os + +from dtsh.shell import ( + DTSh, + DTShCommandNotFoundError, + DTShUsageError, + DTShCommandError, +) +from dtsh.session import DTShSession +from dtsh.io import DTShOutput, DTShRedirect + +from dtsh.rich.io import ( + DTShRichVT, + DTShOutputFileText, + DTShOutputFileHtml, + DTShOutputFileSVG, +) +from dtsh.rich.autocomp import DTShRichAutocomp +from dtsh.rich.theme import DTShTheme +from dtsh.rich.text import TextUtil + + +_theme: DTShTheme = DTShTheme.getinstance() + + +class DTShRichSession(DTShSession): + """Rich interactive devicetree shell session.""" + + def __init__(self, sh: DTSh) -> None: + """Initialize rich session. + + Extend the base session with a few rich TUI elements + and support for SVG and HTML command output redirection formats. + + Initialized with: + + - a rich VT based on Textualize's rich.Console (DTShRichVT) + - a rich auto-completion display hook (DTShRichAutocomp) + + Args: + sh: The session's shell. + """ + super().__init__(sh, DTShRichVT(), DTShRichAutocomp(sh)) + + def preamble_hook(self) -> None: + """Overrides DTShSession.preamble_hook().""" + self._vt.write( + TextUtil.join( + " ", + [ + TextUtil.bold("dtsh"), + TextUtil.mk_text(f"({DTSh.VERSION_STRING}):"), + TextUtil.italic("Shell-like interface with Devicetree"), + ], + ) + ) + self._vt.write( + TextUtil.assemble( + TextUtil.mk_text("How to exit: "), + TextUtil.bold("q"), + TextUtil.mk_text(", or "), + TextUtil.bold("quit"), + TextUtil.mk_text(", or "), + TextUtil.bold("exit"), + TextUtil.mk_text(", or press "), + TextUtil.bold("Ctrl-D"), + ) + ) + self._vt.write() + + def open_redir2(self, redir2: str) -> DTShOutput: + """Overrides DTShSession.open_redir2(). + + This redirection supports SVG and HTML exports. + """ + redirect = DTShRedirect(redir2) + ext: str = os.path.splitext(redirect.path)[1].lower() + + if ext == ".html": + return DTShOutputFileHtml(redirect.path, redirect.append) + if ext == ".svg": + return DTShOutputFileSVG(redirect.path, redirect.append) + + return DTShOutputFileText(redirect.path, redirect.append) + + def on_cmd_not_found_error(self, e: DTShCommandNotFoundError) -> None: + """Overrides DTShSession.on_cmd_not_found_error().""" + self._vt.write( + TextUtil.mk_text("command not found: ").join( + ( + TextUtil.mk_text("dtsh: "), + TextUtil.mk_text(e.name, DTShTheme.STYLE_ERROR), + ) + ) + ) + + def on_cmd_usage_error(self, e: DTShUsageError) -> None: + """Overrides DTShSession.on_cmd_usage_error().""" + self._vt.write( + TextUtil.mk_text(": ").join( + ( + TextUtil.mk_text(e.cmd.name), + TextUtil.mk_text(e.msg, DTShTheme.STYLE_ERROR), + ) + ) + ) + + def on_cmd_failed_error(self, e: DTShCommandError) -> None: + """Overrides DTShSession.on_cmd_failed_error().""" + self._vt.write( + TextUtil.mk_text(": ").join( + ( + TextUtil.mk_text(e.cmd.name), + TextUtil.mk_text(e.msg, DTShTheme.STYLE_ERROR), + ) + ) + ) + + def on_redir2_error(self, e: DTShRedirect.Error) -> None: + """Overrides DTShSession.on_redir2_error().""" + self._vt.write( + TextUtil.assemble( + TextUtil.mk_text("dtsh: "), + TextUtil.mk_text(str(e), DTShTheme.STYLE_WARNING), + ) + ) + + def pre_exit_hook(self, status: int) -> None: + """Overrides DTShSession.pre_exit_hook().""" + style = DTShTheme.STYLE_ERROR if status else DTShTheme.STYLE_DEFAULT + txt = TextUtil.mk_text("bye.", style) + self._vt.write(TextUtil.italic(txt)) diff --git a/src/dtsh/rich/shellutils.py b/src/dtsh/rich/shellutils.py new file mode 100644 index 0000000..2010375 --- /dev/null +++ b/src/dtsh/rich/shellutils.py @@ -0,0 +1,440 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Helpers for formatted command output. + +Command options to configure formatted outputs. + +Base command with boilerplate code to support formatted outputs. + +Unit tests and examples: tests/test_dtsh_rich_shellutils.py +""" + + +from typing import Optional, Sequence, List, Mapping + +import sys + +from dtsh.config import DTShConfig +from dtsh.model import DTNode, DTNodeSorter +from dtsh.rl import DTShReadline +from dtsh.shell import ( + DTSh, + DTShOption, + DTShParameter, + DTShFlag, + DTShArg, + DTShError, + DTShCommand, +) +from dtsh.shellutils import DTShFlagReverse, DTShFlagEnabledOnly, DTShArgOrderBy +from dtsh.autocomp import RlStateEnum + +from dtsh.rich.modelview import ( + SketchMV, + NodeColumnMV, + PathNameNodeMV, + NodeNameNodeMV, + UnitNameNodeMV, + UnitAddrNodeMV, + DepOrdinalNodeMV, + DeviceLabelNodeMV, + NodeLabelsNodeMV, + CompatibleNodeMV, + BindingNodeMV, + BindingDepthNodeMV, + DescriptionNodeMV, + VendorNodeMV, + StatusNodeMV, + AliasesNodeMV, + AlsoKnownAsNodeMV, + OnBusNodeMV, + BusesNodeMV, + BusNodeMV, + InterruptsNodeMV, + RegistersNodeMV, + RegisterRangesNodeMV, + DepOnNodeMV, + ReqByNodeMV, +) + + +_dtshconf: DTShConfig = DTShConfig.getinstance() + + +class DTShFlagLongList(DTShFlag): + """Whether to use a long listing format.""" + + BRIEF = "use a long listing format" + SHORTNAME = "l" + + +class DTShNodeFmt: + """Node output format. + + An output format is a list of columns specified by a format string, + e.g. "pd" is a format string where specifiers "p" and "d" represent + the "Path" and "Description" columns. + """ + + T = Sequence[NodeColumnMV] + """A node output format is actually a list of columns (view factories).""" + + class Spec: + """Output format specifier. + + Associate meta-data to view factories (columns). + """ + + _key: str + _brief: str + _col: NodeColumnMV + + def __init__(self, spec: str, brief: str, col: NodeColumnMV) -> None: + """Define output format specifier. + + Args: + spec: Format specifier (single character). + header: Human readable name for the field. + brief: Brief description. + factory: Formatted view factory. + """ + self._key = spec + self._brief = brief + self._col = col + + @property + def key(self) -> str: + """Format specifier key (single character).""" + return self._key + + @property + def brief(self) -> str: + """Brief description.""" + return self._brief + + @property + def col(self) -> NodeColumnMV: + """View factory associated to this specifier.""" + return self._col + + def __eq__(self, other: object) -> bool: + if isinstance(other, DTShNodeFmt.Spec): + return self._key == other._key + return False + + def __lt__(self, other: object) -> bool: + if isinstance(other, DTShNodeFmt.Spec): + return self._key < other._key + raise TypeError(other) + + def __hash__(self) -> int: + return hash(self._key) + + @staticmethod + def parse(fmt: str) -> "DTShNodeFmt.T": + """Parse format string into node output format. + + Args: + fmt: A format string. + + Returns: + The format columns (view factories). + """ + try: + return [DTSH_NODE_FMT_SPEC[spec].col for spec in fmt] + except KeyError as e: + raise DTShError(f"invalid format specifier: '{e}'") from e + + @staticmethod + def is_valid(fmt: str) -> bool: + """Validate format string. + + Args: + fmt: A format string. + + Returns: + False if the format string is invalid. + """ + return all(spec in DTSH_NODE_FMT_SPEC for spec in fmt) + + +class DTShArgLongFmt(DTShArg): + """Argument to set the output format.""" + + BRIEF = "node output format" + LONGNAME = "format" + + _fmt: Optional[DTShNodeFmt.T] + + def __init__(self) -> None: + super().__init__(argname="fmt") + + @property + def fmt(self) -> Optional[DTShNodeFmt.T]: + """The parsed node output format.""" + return self._fmt + + def reset(self) -> None: + """Overrides DTShOption.reset().""" + super().reset() + self._fmt = None + + def parsed(self, value: Optional[str] = None) -> None: + """Overrides DTShArg.parsed().""" + super().parsed(value) + if self._raw: + self._fmt = DTShNodeFmt.parse(self._raw) + + def autocomp(self, txt: str, sh: DTSh) -> List[DTShReadline.CompleterState]: + """Overrides DTShArg.autocomp(). + + Auto-complete argument with format specifiers. + """ + if not DTShNodeFmt.is_valid(txt): + # Don't complete when current format string is invalid. + return [] + # Answer all available cells but those already set. + return sorted( + [ + RlStateEnum(key, spec.brief) + for key, spec in DTSH_NODE_FMT_SPEC.items() + if key not in txt + ], + key=lambda x: x.rlstr.lower(), + ) + + +class DTShCommandLongFmt(DTShCommand): + """Base for commands that enumerate nodes with formatted output support. + + Provide options and boilerplate code for: + - formatted output: the options DTShFlagLongList and DTShArgLongFmt + are appended to command specific options, get_sketch() and get_longfmt() + will help getting the appropriate formatted columns + - sorting nodes: if the options DTShFlagReverse and DTShArgOrderBy + are supported, they can be accessed as properties flag_reverse() + and arg_sorter(), and sort() will sort nodes according to + the command's supported options + - filtering disabled nodes if DTShFlagEnabledOnly is supported + """ + + def __init__( + self, + name: str, + brief: str, + options: Optional[Sequence[DTShOption]], + parameter: Optional[DTShParameter], + ) -> None: + """Initialize command. + + Args: + name: The command's name. + brief: Brief command description. + options: The options supported by this command. + Options DTShFlagLongList and DTShArgLongFmt are appended + to the command specific options. + parameter: The parameter expected by this command, if any. + """ + super().__init__(name, brief, options, parameter) + # Append flag and parameters related to long listing formats. + self._options.append(DTShFlagLongList()) + self._options.append(DTShArgLongFmt()) + + @property + def has_longfmt(self) -> bool: + """Whether formatted output is disabled by an option or preference.""" + return ( + _dtshconf.pref_always_longfmt + or self.with_flag(DTShFlagLongList) + or self.with_arg(DTShArgLongFmt).isset + ) + + @property + def flag_reverse(self) -> bool: + """Shortcut to the "reverse output" flag if supported.""" + try: + return self.with_flag(DTShFlagReverse) + except KeyError: + # Option not supported. + pass + return False + + @property + def arg_sorter(self) -> Optional[DTNodeSorter]: + """Shortcut to the "order-by" argument's value if supported.""" + try: + return self.with_arg(DTShArgOrderBy).sorter + except KeyError: + # Option not supported. + pass + return None + + @property + def flag_enabled_only(self) -> bool: + """Shortcut to the "ennabled only" flag if supported.""" + try: + return self.with_flag(DTShFlagEnabledOnly) + except KeyError: + # Option not supported. + pass + return False + + def sort(self, nodes: Sequence[DTNode]) -> Sequence[DTNode]: + """Sort nodes if the "order-by" argument is set + and/or the "reverse output" flag if set. + + Args: + nodes: The nodes to sort. + + Returns: + The sorted nodes. + """ + if self.arg_sorter: + return self.arg_sorter.sort(nodes, reverse=self.flag_reverse) + if self.flag_reverse: + return list(reversed(nodes)) + return nodes + + def prune(self, nodes: Sequence[DTNode]) -> Sequence[DTNode]: + """Filter out disabled nodes if the "ennabled only" flag is set. + + Args: + nodes: The nodes to filter. + + Returns: + The filtered nodes. + """ + enabled_only = self.flag_enabled_only + return [node for node in nodes if node.enabled or not enabled_only] + + def get_sketch(self, layout: "SketchMV.Layout") -> SketchMV: + """Initialize a rendering context for formatted output. + + Args: + layout: The rendering layout to configure. + LIST_VIEW is promoted to LIST_MULTI if "pref_list_multi" is set. + + Returns: + A rendering context. + """ + if (layout == SketchMV.Layout.LIST_VIEW) and _dtshconf.pref_list_multi: + # Allow multiple-line cells. + layout = SketchMV.Layout.LIST_MULTI + + return SketchMV(layout, self.arg_sorter, self.flag_reverse) + + def get_longfmt(self, default_fmt: Optional[str] = None) -> DTShNodeFmt.T: + """Get the node output format to use. + + Args: + default_fmt: A default format string to use when none + was parsed from the command line. + Typically set by the rendering context. + + Returns: + The formatted columns as a list of view factories. + """ + cols = self.with_arg(DTShArgLongFmt).fmt + if (not cols) and default_fmt: + try: + cols = DTShNodeFmt.parse(default_fmt) + except DTShError as e: + print( + f"Invalid preferred format: '{default_fmt}' ('{e}')", + file=sys.stderr, + ) + + return cols or [DTShNodeFmt.parse("p")[0]] + + +DTSH_NODE_FMT_SPEC: Mapping[str, DTShNodeFmt.Spec] = { + spec.key: spec + for spec in [ + DTShNodeFmt.Spec( + "p", "path name", NodeColumnMV("Path", PathNameNodeMV) + ), + DTShNodeFmt.Spec( + "N", "node name", NodeColumnMV("Name", NodeNameNodeMV) + ), + DTShNodeFmt.Spec( + "n", "unit name", NodeColumnMV("Name", UnitNameNodeMV) + ), + DTShNodeFmt.Spec( + "a", "unit address", NodeColumnMV("Address", UnitAddrNodeMV) + ), + DTShNodeFmt.Spec( + "o", "dependency ordinal", NodeColumnMV("Ordinal", DepOrdinalNodeMV) + ), + DTShNodeFmt.Spec( + "l", "device label", NodeColumnMV("Label", DeviceLabelNodeMV) + ), + DTShNodeFmt.Spec( + "L", "DTS labels", NodeColumnMV("Labels", NodeLabelsNodeMV) + ), + DTShNodeFmt.Spec( + "c", + "compatible strings", + NodeColumnMV("Compatible", CompatibleNodeMV), + ), + DTShNodeFmt.Spec( + "C", + "binding's compatible or headline", + NodeColumnMV("Binding", BindingNodeMV), + ), + DTShNodeFmt.Spec( + "X", + "child-binding depth", + NodeColumnMV("Binding Depth", BindingDepthNodeMV), + ), + DTShNodeFmt.Spec( + "d", "description", NodeColumnMV("Description", DescriptionNodeMV) + ), + DTShNodeFmt.Spec( + "v", "vendor name", NodeColumnMV("Vendor", VendorNodeMV) + ), + DTShNodeFmt.Spec( + "s", "status string", NodeColumnMV("Status", StatusNodeMV) + ), + DTShNodeFmt.Spec( + "A", "node aliases", NodeColumnMV("Aliases", AliasesNodeMV) + ), + DTShNodeFmt.Spec( + "K", + "all labels and aliases", + NodeColumnMV("Also Known As", AlsoKnownAsNodeMV), + ), + DTShNodeFmt.Spec( + "b", "bus of appearance", NodeColumnMV("On Bus", OnBusNodeMV) + ), + DTShNodeFmt.Spec( + "B", "supported bus protocols", NodeColumnMV("Buses", BusesNodeMV) + ), + DTShNodeFmt.Spec( + "Y", "bus information", NodeColumnMV("Bus", BusNodeMV) + ), + DTShNodeFmt.Spec( + "i", + "generated interrupts", + NodeColumnMV("IRQs", InterruptsNodeMV), + ), + DTShNodeFmt.Spec( + "r", + "registers (base address and size)", + NodeColumnMV("Registers", RegistersNodeMV), + ), + DTShNodeFmt.Spec( + "R", + "registers (address range)", + NodeColumnMV("Registers", RegisterRangesNodeMV), + ), + DTShNodeFmt.Spec( + "D", "node dependencies", NodeColumnMV("Depends-on", DepOnNodeMV) + ), + DTShNodeFmt.Spec( + "T", "dependent nodes", NodeColumnMV("Required-by", ReqByNodeMV) + ), + ] +} +"""Map meta-data to node format specifiers and view factories.""" diff --git a/src/dtsh/rich/svg.py b/src/dtsh/rich/svg.py new file mode 100644 index 0000000..695872d --- /dev/null +++ b/src/dtsh/rich/svg.py @@ -0,0 +1,767 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""SVG contents format for command output redirection. + +Rationale: +- the SVG documents we can generate directly with the Textualize's rich library + API do not really suit the DTSh use case +- we need an additional abstraction layer to properly support the "append" mode + +The original fomrat of the SVG docment generated by the Textualize's rich +library is like bellow: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + output text + + + + + + +Fonts +----- +We can't do much with the public API as long as the internal character height +remains inaccessible: we could set a CSS font-size property, +but this would mess with all elements, the computation of which +would still use the internal (and different) character height. + +Setting the font-family and aspect ration would be quite simple though, +although not a priority. + +We'll remove from the format string the corresponding Fira Code CSS rules, +which ask the SVG client for downloading a font that is probably not +installed on the OS, and replace them with a simple font specification. + + +ViewBox +------- +Setting the ViewBox will trigger SVG "zoom-in" if its dimensions are smaller +than the viewport, e.g. a Web browser's window: remvove it. + + +Window Decoration +----------------- +These macOS-like window buttons are at least unnecessarily, +and setting a title does not really match our use case, +that would rather be using the generated SVG file +as an image in another document(HTML, LaTeX, Word, etc), +thus setting its title and other "decorations" there. + +We'll entirely remove title and buttons. +Assuming this is the reason for the difference between the top padding +and all others (left, bottom, right), we get the vertical space dedicated +to this window decoration as: 40 - 8 = 32px + +Regarding titles: something more useful could be to "echo" +the command string to the file the command is redirected to. + + +Chrome's rectangle +----------------- +The rectangle's dimensions computed by export_svg() take into account +the title and buttons we removed. + +We must then subtract 32px from the rectangle's height. + +Rectangle dimensions are computed as: +- with: ceil(width * char_width + padding_width) +- height: (y + 1) * line_height + padding_height + + +Terminal group coordinates +---------------------------- +As we've seen, the Terminal's coordinates are const expressions: +- x: margin_left + padding_left = 1 + 8 = 9 +- y: margin_top + padding_top = 1 + 40 = 41 + +Since we removed 32px of previous vertical space, +we must subtract these from y. + +Note: this is the same as assuming the top padding equals to 8, giving x=y=9. + + +Append mode +----------- +When "appending" SVG' to SVG: +- the . + self._styles = list(contents[begin + 1 : end]) + i_contents = end + 1 + + # Parse SVG definitions. + begin, end = SVGContentsFmt.find_defs(contents, i_contents) + # Exclude the lines with and . + self._defs = list(contents[begin + 1 : end]) + i_contents = end + 1 + + # Parse chrome's rectangle. + i_chrome = SVGContentsFmt.find_chrome(contents, i_contents) + i_rect = i_chrome + 1 + self._rect = contents[i_rect] + self._width, self._height = SVGContentsFmt.get_rect_width_heigt( + self._rect + ) + i_contents = i_rect + 1 + + # Parse Terminal SVG groups. + self._gterms = [] + while True: + try: + begin, end = SVGContentsFmt.find_gterm(contents, i_contents) + # Exclude the lines with ":gterm_begin" and "gterm_end". + self._gterms.append( + SVGContents.GTerm(contents[begin + 1 : end]) + ) + i_contents = end + 1 + except SVGContentsFmt.Error: + # Assuming no more Terminal groups. + break + + @property + def styles(self) -> Sequence[str]: + """Contents lines for CSS styles and rules. + + This is the contents of the " + """End of styles.""" + + SVG_DEFS_BEGIN = "" + """Where start the SVG paths definitions.""" + + SVG_DEFS_END = "" + """End of styles.""" + + MARK_CHROME = " " + """Where the "chrome" starts. + + + + + + + + + """ + + MARK_GTERM_BEGIN = " " + """Where the Terminal's output (redirected command) starts. + + + + + {matrix} + + + + """ + + MARK_GTERM_END = " " + """End of Terminal's SVG group.""" + + FONT_PLACEHOLDER = "|font_family|" + """Placeholder string for the font family.""" + + PROLOG = '' + + EPILOG = "" + + PAD_TOP_CORRECTION: int = 32 + """Assumed height of the macOS-like window decoration in pixels. + + We'll apply this correction to the vertical dimensions of the + SVG document generated by Console.svg_export(). + """ + + @classmethod + def get_format(cls, font_family: str = "Source Code Pro") -> str: + """Make an SVG format string parameter value. + + Args: + font_family: A font family string, like "Source Code Pro". + + Returns: + A format string compatible with the export_sgv() API. + """ + return DTSH_SVG_FORMAT.replace(cls.FONT_PLACEHOLDER, font_family, 1) + + @classmethod + def find(cls, mark: str, contents: Sequence[str], start: int = 0) -> int: + """Find mark in contents. + + mark: The string to find. + contents: The SVG contents to search. + start: Start offset in contents (in number of lines). + + Returns: + The index of the first contents line where the mark is found, + or -1 if not found. + """ + for i, line in enumerate(contents[start:]): + if line.find(mark) != -1: + return i + start + return -1 + + @classmethod + def find_range( + cls, + mark_begin: str, + mark_end: str, + contents: Sequence[str], + start: int = 0, + ) -> Tuple[int, int]: + """Find the lines range between begin and end marks. + + Args: + mark_begin: Mark where the range begins. + mark_end: Mark where the range ends. + contents: The SVG contents to search. + start: Start offset in contents (in number of lines). + + Returns: + The found range as the tuple (begin, end). + + Raises: + SVGContentsFmt.Error: Unexpected SVG format. + """ + begin = cls.find(mark_begin, contents, start) + if begin == -1: + raise SVGContentsFmt.Error(mark_begin) + end = cls.find(mark_end, contents, begin + 1) + if end == -1: + raise SVGContentsFmt.Error(mark_end) + return (begin, end) + + @classmethod + def find_styles( + cls, contents: Sequence[str], start: int = 0 + ) -> Tuple[int, int]: + """Find the CSS styles' range. + + contents: The SVG contents to search. + start: Start offset in contents (in number of lines). + + Returns: + The range from CSS styles start (the "" line). + + Raises: + SVGContentsFmt.Error: Unexpected SVG format. + """ + begin, end = cls.find_range( + cls.CSS_STYLES_BEGIN, cls.CSS_STYLES_END, contents, start + ) + return (begin, end) + + @classmethod + def find_defs( + cls, contents: Sequence[str], start: int = 0 + ) -> Tuple[int, int]: + """Find SVG definitions' range. + + contents: The SVG contents to search. + start: Start offset in contents (in number of lines). + + Returns: + The range from definitions start (the "" line). + to end (the "" line). + + Raises: + SVGContentsFmt.Error: Unexpected SVG format. + """ + begin, end = cls.find_range( + cls.SVG_DEFS_BEGIN, cls.SVG_DEFS_END, contents, start + ) + return (begin, end) + + @classmethod + def find_chrome(cls, contents: Sequence[str], start: int = 0) -> int: + """Find where the chrome starts (see MARK_CHROME). + + contents: The SVG contents to search. + start: Start offset in contents (in number of lines). + + Returns: + The index of the line with the chrome's mark. + + Raises: + SVGContentsFmt.Error: Unexpected SVG format. + """ + i_chrome = cls.find(cls.MARK_CHROME, contents, start) + if i_chrome == -1: + raise SVGContentsFmt.Error(cls.MARK_CHROME) + return i_chrome + + @classmethod + def find_gterm( + cls, contents: Sequence[str], start: int = 0 + ) -> Tuple[int, int]: + """Find the Terminal's SVG group (see MARK_TERM_START). + + contents: The SVG contents to search. + start: Start offset in contents (in number of lines). + + Returns: + The range from group start (the line with the begin mark), + to end (the line with the end mark). + + Raises: + SVGContentsFmt.Error: Unexpected SVG format. + """ + begin, end = cls.find_range( + cls.MARK_GTERM_BEGIN, cls.MARK_GTERM_END, contents, start + ) + return (begin, end) + + @classmethod + def get_gtranslate_xy(cls, gtranslate: str) -> Tuple[int, int]: + """Get the coordinates of an SVG group translation. + + The SVG group must start with something like: + str: + """Set the coordinates of an SVG group translation. + + See also get_gtranslate_xy(). + + Args: + gtranslate: The SVG group tag. + x: The horizontal translation. + y: The vertical translation. + + Returns: + The SVG group tag with updated coordinates. + + Raises: + SVGContentsFmt.Error: Malformed SVG translation. + """ + m = cls._RE_TRANSFORM_TRANSLATE.match(gtranslate) + if m: + translate: str = m.group("translate") + return gtranslate.replace(translate, f"translate({x}, {y})", 1) + raise SVGContentsFmt.Error(gtranslate) + + @classmethod + def get_rect_width_heigt(cls, rect: str) -> Tuple[int, int]: + """Get the width and height of an SVG rectangle. + + The SVG rectangle is something like: + str: + """Set the width and height of an SVG rectangle. + + Args: + rect: The SVG rectangle tag. + width: The new width. + height: The new height. + + Returns: + The SVG rectangle tag with updated width and height. + + Raises: + SVGContentsFmt.Error: Malformed SVG rectangle. + """ + m_width = cls._RE_RECT_WIDTH.match(rect) + m_height = cls._RE_RECT_HEIGHT.match(rect) + if m_width and m_height: + rect_width: str = m_width.group("w") + rect_height: str = m_height.group("h") + rect = rect.replace(rect_width, f'width="{width}"') + rect = rect.replace(rect_height, f'height="{height}"') + return rect + raise SVGContentsFmt.Error(rect) + + # + _RE_TRANSFORM_XY = re.compile( + r'\s*translate\([\d,\s]+\))' + ) + + # + _RE_RECT = re.compile( + r'\s*[\d.]+)".*height="(?P[\d.]+)"' + ) + + _RE_RECT_WIDTH = re.compile(r'\s*width="[\d.]+")') + _RE_RECT_HEIGHT = re.compile(r'\s*height="[\d.]+")') + + +DTSH_SVG_FORMAT = """\ + + + + + + + + + {lines} + + + + + {chrome} + + + + + {matrix} + + + + + +""" diff --git a/src/dtsh/rich/text.py b/src/dtsh/rich/text.py new file mode 100644 index 0000000..1ec0c73 --- /dev/null +++ b/src/dtsh/rich/text.py @@ -0,0 +1,229 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Text view factories and other text related helpers. + +Factories for styled and actionable (aka linked) rich text views, +and miscellaneous text related helpers. +""" + +from typing import Optional, Union, Iterable + +from urllib.parse import urlparse +import os +import pathlib + +from rich.console import JustifyMethod +from rich.style import Style, StyleType +from rich.text import Text + +from dtsh.config import DTShConfig, ActionableType +from dtsh.rich.theme import DTShTheme + + +_dtshconf: DTShConfig = DTShConfig.getinstance() + + +class TextUtil: + """Text view factories.""" + + @classmethod + def mk_text( + cls, + content: str, + style: Optional[StyleType] = None, + justify: Optional[JustifyMethod] = None, + ) -> Text: + """Text view factory. + + Args: + content: + style: + justify: + + Returns: + A new text view. + """ + return Text( + content, + style=style or DTShTheme.STYLE_DEFAULT, + justify=justify, + ) + + @classmethod + def dim(cls, txt: Union[str, Text]) -> Text: + """Dim an existing text view or create a new "dim" text. + + Args: + txt: A text view or a string. + + Returns: + A dim text view. + """ + if isinstance(txt, str): + return cls.mk_text(txt, "dim") + txt.stylize("dim") + return txt + + @classmethod + def bold(cls, text: Union[str, Text]) -> Text: + """Bold an existing text view or create a new "bold" text. + + Args: + text: A text view or a string. + + Returns: + A bold text view. + """ + if isinstance(text, str): + return cls.mk_text(text, "bold") + text.stylize("bold") + return text + + @classmethod + def italic(cls, text: Union[str, Text]) -> Text: + """Emphasize an existing text view or create a "italic" text. + + Args: + text: A text view or a string. + + Returns: + An italic text view. + """ + if isinstance(text, str): + return cls.mk_text(text, "italic") + text.stylize("italic") + return text + + @classmethod + def underline(cls, text: Union[str, Text]) -> Text: + """Underline an existing text view or create a new "underline" text. + + Args: + text: A text view or a string. + + Returns: + An underlined text view. + """ + if isinstance(text, str): + return cls.mk_text(text, "underline") + text.stylize("underline") + return text + + @classmethod + def strike(cls, text: Union[str, Text]) -> Text: + """Strike an existing text view or create a new "strike" text. + + Args: + text: A text view or a string. + + Returns: + An stroked text view. + """ + if isinstance(text, str): + return cls.mk_text(text, "strike") + text.stylize("strike") + return text + + @classmethod + def disabled(cls, text: Union[str, Text]) -> Text: + """Style text item as disabled. + + Args: + text: A text view or a string. + + Returns: + An text view styled as disabled. + """ + if isinstance(text, str): + return cls.mk_text(text, DTShTheme.STYLE_DISABLED) + text.stylize(DTShTheme.STYLE_DISABLED) + return text + + @classmethod + def link( + cls, + text: Union[str, Text], + uri: str, + linktype: Optional[ActionableType] = None, + ) -> Text: + """Link text to file or URI. + + Actual links rendering, and whether they are actually actionable, + will eventually depend on the host terminal. + + Args: + text: A text view or the string to create a view of. + uri: The destination URI. + linktype: How to represent actionable text. + + Returns: + An actionable text view. + """ + if isinstance(text, str): + text = TextUtil.mk_text(text) + + linktype = linktype or _dtshconf.pref_actionable_type + if linktype is ActionableType.NONE: + return text + + scheme = urlparse(uri).scheme + if not scheme: + # Assume "file" URI scheme when missing. + uri = pathlib.Path(os.path.abspath(uri)).as_uri() + + if linktype is ActionableType.ALT: + # Append actionable text. + actionable = TextUtil.mk_text( + _dtshconf.pref_actionable_text, style=text.style + ) + actionable.stylize(Style(link=uri)) + text = TextUtil.join(" ", (text, actionable)) + else: + # Make text itself actionable (ActionableType.LINK). + text.stylize(Style(link=uri)) + + return text + + @classmethod + def mk_headline( + cls, content: str, style: Optional[Union[str, Style]] = None + ) -> Text: + """Extract headline of a multi-line content. + + Args: + content: Multi-line content. + style: Style or style name. + + Returns: + A text view. + """ + headline, _, rest = content.lstrip().partition("\n") + is_multiline = bool(rest) + + if is_multiline: + if headline.endswith("."): + # Remove trailing dot if ellipsis. + headline = headline[:-1] + headline = f"{headline}{_dtshconf.wchar_ellipsis}" + + return cls.mk_text(headline, style) + + @classmethod + def join(cls, sep: Union[str, Text], parts: Iterable[Text]) -> Text: + """Join rich text elements.""" + if isinstance(sep, str): + sep = cls.mk_text(sep) + return sep.join(parts) + + @classmethod + def assemble(cls, *parts: Union[Text, str]) -> Text: + """Assemble Text views into one. + + Args: + parts: The views to assemble. + """ + # Note: Text.append_tokens(), Text.append_text() and such + # would "merge" the Text styles. + return Text.assemble(*parts) diff --git a/src/dtsh/rich/theme.ini b/src/dtsh/rich/theme.ini new file mode 100644 index 0000000..6c1ddc1 --- /dev/null +++ b/src/dtsh/rich/theme.ini @@ -0,0 +1,149 @@ +[styles] +# Rich styles (aka dtsh theme). +# +# dtsh styles are prefixed by "dtsh" to avoid collisions +# with styles inherited from rich.Theme. +# +# Colors: +# - Standard color: color(N), 0 <= N <= 255 +# https://rich.readthedocs.io/en/latest/appendix/colors.html +# - 24-bit color: CSS-like syntax, #rrggbb or rgb(r,g,b) +# +# Attributes: +# - dim +# - bold +# - italic +# - blink +# - reverse +# - strike +# - underline +# +# Styles: [dim|bold|...|underline] on +# +# See also: +# - https://rich.readthedocs.io/en/stable/style.html. + +# Terminal default foreground on terminal default background. +# Please, do not change this, whenever possible prefer +# to configure the terminal appearance (background, colors palette, etc). +dtsh.default = default on default + +# Style for shell error and warning messages. +dtsh.error = red1 +dtsh.warning = dark_orange + +# Style modifier for disabled items. +# By default, disabled items (e.g. nodes) are dim, +# which may not be legible with some light desktop +# themes: try "strike" instead. +dtsh.disabled = dim + + +# Styles for file system entries (files). +dtsh.fs.file = default + +# Styles for file system entries (directories). +dtsh.fs.dir = italic + + +# Styles for links to local files. +dtsh.link.local = light_sky_blue3 + +# Styles for external links. +dtsh.link.www = medium_orchid3 + + +# Style for all list views headers. +dtsh.list.header = default + +# Style for all tree views headers. +dtsh.tree.header = default + + +# Style for the branch part of a devicetree path. +dtsh.dt.path_branch = default + +# Style for the node part (rightest) of a devicetree path. +dtsh.dt.path_node = bold %(dtsh.dt.path_branch)s + +# Style for DT node names. +dtsh.dt.node_name = default + +# Style for unit names. +dtsh.dt.unit_name = default + +# Style for unit addresses. +dtsh.dt.unit_addr = default + +# Style for device labels (the 'label' property). +dtsh.dt.device_label = deep_sky_blue1 + +# Style for device labels (the labels attached in the DTS). +dtsh.dt.node_label = italic deep_sky_blue1 + +# Base style for compatible strings. +dtsh.dt.compat_str = light_sea_green + +# Style for compatible strings when they represent device bindings. +dtsh.dt.binding_compat = bold %(dtsh.dt.compat_str)s + +# Style for device bindings descriptions. +dtsh.dt.binding_desc = dark_khaki + +# Style for child-binding depth. +dtsh.dt.cb_depth = grey84 + +# Style for child-binding orders greater than 0 +# (the binding is actually a child-binding). +dtsh.dt.is_child_binding = bold light_steel_blue3 + +# Style for child-binding anchors. +dtsh.dt.cb_anchor = white + +# Style for DT alias names. +dtsh.dt.alias = italic sky_blue2 + +# Style for DT chosen names. +dtsh.dt.chosen = italic plum4 + +# Base style for bus protocols. +dtsh.dt.bus = royal_blue1 + +# Style for buses when they represent a bus of appearance. +dtsh.dt.on_bus = %(dtsh.dt.bus)s + +# Style for IRQ numbers. +dtsh.dt.irq_number = sandy_brown + +# Style for IRQ priorities. +dtsh.dt.irq_priority = sandy_brown + +# Style for register addresses. +dtsh.dt.reg_addr = plum2 + +# Style for register sizes. +dtsh.dt.reg_size = plum3 + +# Style for device requirements. +dtsh.dt.depend_on = light_cyan3 + +# Style for disabled requirements. +dtsh.dt.depend_failed = bold dark_orange3 + +# Style for dependent devices. +dtsh.dt.required_by = default + +# Style for device vendor prefixes. +dtsh.dt.vendor_prefix = cadet_blue + +# Style for device vendor names. +dtsh.dt.vendor_name = cadet_blue + +# Style for node dependency ordinals. +dtsh.dt.dep_ordinal = default + +# Style for okay status string. +dtsh.dt.status_enabled = dark_cyan + +# Style for disabled status string. +dtsh.dt.status_disabled = dim italic orange1 diff --git a/src/dtsh/rich/theme.py b/src/dtsh/rich/theme.py new file mode 100644 index 0000000..7967142 --- /dev/null +++ b/src/dtsh/rich/theme.py @@ -0,0 +1,145 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Devicetree shell theme (aka rich styles). + +A theme is a collection of styles, each of which is consistently used +to represent a type of information: e.g. by default compatible strings +are always green, and things that behave like symbolic links (e.g. aliases) +are all in italic. + +Theme files are simple INI files named "theme.ini": + +- the bundled theme file which sets the default appearance +- an optional user's theme file which customizes the defaults + +The user's theme file is located in the DTSh application data directory. + +Unit tests and examples: tests/test_dtsh_theme.py +""" + + +from typing import Optional, Dict, Mapping + +import os + + +from rich.errors import StyleError, StyleSyntaxError +from rich.style import Style +from rich.theme import Theme + +from dtsh.config import DTShConfig + +_dtshconf: DTShConfig = DTShConfig.getinstance() + + +class DTShTheme: + """Rich styles.""" + + STYLE_DEFAULT = "dtsh.default" + STYLE_ERROR = "dtsh.error" + STYLE_WARNING = "dtsh.warning" + STYLE_DISABLED = "dtsh.disabled" + + STYLE_FS_FILE = "dtsh.fs.file" + STYLE_FS_DIR = "dtsh.fs.dir" + + STYLE_LINK_LOCAL = "dtsh.link.local" + STYLE_LINK_WWW = "dtsh.link.www" + + STYLE_LIST_HEADER = "dtsh.list.header" + STYLE_TREE_HEADER = "dtsh.tree.header" + + STYLE_DT_PATH_BRANCH = "dtsh.dt.path_branch" + STYLE_DT_PATH_NODE = "dtsh.dt.path_node" + + STYLE_DT_NODE_NAME = "dtsh.dt.node_name" + STYLE_DT_UNIT_NAME = "dtsh.dt.unit_name" + STYLE_DT_UNIT_ADDR = "dtsh.dt.unit_addr" + STYLE_DT_DEVICE_LABEL = "dtsh.dt.device_label" + STYLE_DT_NODE_LABEL = "dtsh.dt.node_label" + STYLE_DT_COMPAT_STR = "dtsh.dt.compat_str" + STYLE_DT_BINDING_COMPAT = "dtsh.dt.binding_compat" + STYLE_DT_BINDING_DESC = "dtsh.dt.binding_desc" + STYLE_DT_CB_ORDER = "dtsh.dt.cb_depth" + STYLE_DT_CB_ANCHOR = "dtsh.dt.cb_anchor" + STYLE_DT_IS_CHILD_BINDING = "dtsh.dt.is_child_binding" + STYLE_DT_ALIAS = "dtsh.dt.alias" + STYLE_DT_CHOSEN = "dtsh.dt.chosen" + STYLE_DT_BUS = "dtsh.dt.bus" + STYLE_DT_ON_BUS = "dtsh.dt.on_bus" + STYLE_DT_IRQ_NUMBER = "dtsh.dt.irq_number" + STYLE_DT_IRQ_PRIORITY = "dtsh.dt.irq_priority" + STYLE_DT_STATUS_ENABLED = "dtsh.dt.status_enabled" + STYLE_DT_STATUS_DISABLED = "dtsh.dt.status_disabled" + STYLE_DT_REG_ADDR = "dtsh.dt.reg_addr" + STYLE_DT_REG_SIZE = "dtsh.dt.reg_size" + STYLE_DT_ORDINAL = "dtsh.dt.dep_ordinal" + STYLE_DT_DEP_ON = "dtsh.dt.depend_on" + STYLE_DT_DEP_FAILED = "dtsh.dt.depend_failed" + STYLE_DT_REQ_BY = "dtsh.dt.required_by" + STYLE_DT_VENDOR_NAME = "dtsh.dt.vendor_name" + STYLE_DT_VENDOR_PREFIX = "dtsh.dt.vendor_prefix" + + class Error(BaseException): + """Error loading styles file.""" + + @classmethod + def getinstance(cls) -> "DTShTheme": + """Access the rich styles configuration instance.""" + return _dtshtheme + + # Rich styles. + _styles: Dict[str, Style] + + def __init__(self, path: Optional[str] = None) -> None: + """Initialize DTSh theme. + + If a theme path is explicitly set, + only this theme file is loaded. + + Otherwise, proceed to default theme initialization: + + - 1st, load bundled theme file + - then, load user's theme file to override defaults + + Args: + path: Path to theme file, + or None for default theme initialization. + """ + self._styles = {} + + if path: + # If explicitly specified, load only this one. + self.load_theme_file(path) + else: + # Load defaults from bundled configuration file. + path = os.path.join(os.path.dirname(__file__), "theme.ini") + self.load_theme_file(path) + # Load user's theme file if any. + path = _dtshconf.get_user_file("theme.ini") + if path and os.path.isfile(path): + self.load_theme_file(path) + + @property + def styles(self) -> Mapping[str, Style]: + """The theme's rich styles.""" + return self._styles + + def load_theme_file(self, path: str) -> None: + """Load a rich styles file. + + Args: + path: Path to a styles file. + + Raises: + DTShTheme.Error: Failed to styles file. + """ + try: + self._styles.update(Theme.read(path, encoding="utf-8").styles) + except (OSError, StyleError, StyleSyntaxError) as e: + raise DTShTheme.Error(str(e)) from e + + +_dtshtheme = DTShTheme() diff --git a/src/dtsh/rich/tui.py b/src/dtsh/rich/tui.py new file mode 100644 index 0000000..307e2a4 --- /dev/null +++ b/src/dtsh/rich/tui.py @@ -0,0 +1,221 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Rich console protocol. + +Base view definitions compatible with the rich console protocol, __rich__(). +""" + + +from typing import Optional, Union, Sequence + +import rich.box +from rich.console import RenderableType +from rich.padding import PaddingDimensions, Padding +from rich.table import Table +from rich.text import Text + +from dtsh.rich.theme import DTShTheme + + +class View: + """View interface. + + DTSh user interface will print to kind of views: + + - rich API objects like Text + - specialized views that implement this interface, + typically named ViewXXX + """ + + SUB = Text() + """View placecholder (empty Text).""" + + _ileft: int + _itop: int + + def __init__(self) -> None: + """New view.""" + self._ileft = 0 + self._itop = 0 + + @property + def renderable(self) -> RenderableType: + """The rich console protocol object that represents this view. + + To be overridden by concrete views. + """ + return View.SUB + + def left_indent(self, spaces: int) -> None: + """Left-indent the view (left margin). + + Args: + spaces: Indentation in number of characters. + """ + self._ileft = spaces + + def top_indent(self, spaces: int) -> None: + """Top-indent the view (top margin). + + Args: + spaces: Indentation in number of characters. + """ + self._itop = spaces + + def __rich__(self) -> RenderableType: + """Rich console protocol.""" + view = self.renderable + if self._ileft or self._itop: + view = Padding(view, (self._itop, 0, 0, self._ileft)) + return view + + +class GridLayout(View): + """Base for grid layouts. + + This is a simple vertical grid layout without header. + """ + + _grid: Table + + def __init__( + self, + ncols: int = 1, + /, + padding: PaddingDimensions = 0, + no_wrap: bool = False, + expand: bool = False, + ) -> None: + """Initialize layout. + + Args: + ncols: Number of columns (default to 1). + padding: Cells padding (CSS style), as one integer (TPLR), + or a pair of integers (TB, LR), or 3 integers (T, LR, B), + or 4 integers (T, R, B, L). + Defaults to 0. + no_wrap: Disable wrapping entirely for this layout. + expand: Whether to expand the table to fit the available space. + """ + super().__init__() + self._grid = Table.grid( + padding=padding, + expand=expand, + ) + + for _ in range(0, ncols): + self._grid.add_column(no_wrap=no_wrap) + + @property + def renderable(self) -> RenderableType: + """Rich Grid. + + Overrides View.layout(). + """ + return self._grid + + def add_row(self, *views: Optional[RenderableType]) -> None: + """Add a row to this grid layout. + + Args: + views: The row's render-able columns. + """ + if len(self._grid.columns) != len(views): + raise ValueError( + f"Expected {len(self._grid.columns)} views, got {len(views)}" + ) + self._grid.add_row(*views) + + +class TableLayout(View): + """Base for table layouts. + + This is a simple vertical grid layout with optional header row. + """ + + _table: Table + + def __init__( + self, + headers: Sequence[str], + /, + padding: PaddingDimensions = 0, + show_header: bool = True, + no_wrap: bool = True, + expand: bool = False, + ) -> None: + """Initialize layout. + + Args: + headers: The table column headers. + padding: Cells padding (CSS style), as one integer (TPLR), + or a pair of integers (TB, LR), or 3 integers (T, LR, B), + or 4 integers (T, R, B, L). Defaults to 0. + show_header: Whether to show the table's header row. + no_wrap: Disable wrapping entirely for this layout. + expand: Whether to expand the table to fit the available space. + """ + super().__init__() + self._table = Table( + box=rich.box.SIMPLE_HEAD, + padding=padding, + collapse_padding=True, + pad_edge=False, + show_edge=False, + show_header=show_header, + header_style=DTShTheme.STYLE_LIST_HEADER, + expand=expand, + ) + for header in headers: + self._table.add_column(header, no_wrap=no_wrap) + + @property + def renderable(self) -> RenderableType: + """Rich Table. + + Overrides View.layout(). + """ + return self._table + + def add_row(self, *views: Optional[RenderableType]) -> None: + """Add a row to this table layout. + + Args: + views: The row's render-able columns. + """ + if len(self._table.columns) != len(views): + raise ValueError( + f"Expected {len(self._table.columns)} views, got {len(views)}" + ) + self._table.add_row(*views) + + +class StatusBar(GridLayout): + """Status bar view. + + Single row expanded layout, with 3 equally sized columns (left, center, + and right), suitable for top or bottom status bars. + """ + + def __init__( + self, + text_left: Optional[Union[str, Text]], + text_center: Optional[Union[str, Text]], + text_right: Optional[Union[str, Text]], + ) -> None: + """Initialize status bar. + + Args: + text_left: Left side text content. + text_center: Center text content. + text_right: Right side text content. + """ + super().__init__(3, expand=True) + self._grid.columns[0].justify = "left" + self._grid.columns[1].justify = "center" + self._grid.columns[2].justify = "right" + for col in self._grid.columns: + col.ratio = 1 + self._grid.add_row(text_left, text_center, text_right) diff --git a/src/dtsh/rl.py b/src/dtsh/rl.py index 63e278c..441cbb8 100644 --- a/src/dtsh/rl.py +++ b/src/dtsh/rl.py @@ -1,34 +1,291 @@ -# Copyright (c) 2022 Chris Duf +# Copyright (c) 2023 Christophe Dufaza # # SPDX-License-Identifier: Apache-2.0 +"""GNU Readline integration. + +Rationale: + +- provide DTSh client code with a single entry-point API for + GNU Readline integration +- isolate the completion logic (find matches) and view providers + (display matches) from the readline hooks machinery + +The GNU Readline integration with Python is initialized: + +- with the standalone Python module gnureadline if installed: this + is likely necessary on macOS, where readline is typically backed by libedit + instead of libreadline +- with the readline module of the Python standard library on other POSIX systems + +On windows, the readline support will likely be disabled. """ -GNU readline integration. - -GNU readline support initialization: -- 1st, try to import the stand-alone GNU readline module - (`gnureadline`), that should be used on macOS - to override editline (libedit) -- if failed, try to import the `readline` module from - the Python standard library, which should be the preferred option - on systems where the GNU readline shared library is available -- if both failed, abort dtsh (e.g. unlucky MS Windows' users) - -See also: - gnureadline: https://pypi.org/project/gnureadline/ -""" + +from typing import Callable, Sequence, List, Optional + +import os import sys +from dtsh.config import DTShConfig +from dtsh.io import DTShOutput + +_has_readline = False try: + # Allow all POSIX-like systems to load the gnureadline stand-alone module. + # https://pypi.org/project/gnureadline/ import gnureadline as readline # type: ignore -except ImportError: + _has_readline = True +except ImportError: try: - import readline # type: ignore + import readline + + _has_readline = True except ImportError: - # This version of dtsh won't run without readline support. - print("dtsh: GNU readline support not found.", file=sys.stderr) - print("dtsh: Abort.", file=sys.stderr) - sys.exit(-1) + print("GNU readline support disabled.", file=sys.stderr) + + +_dtshconf: DTShConfig = DTShConfig.getinstance() + + +class DTShReadline: + """GNU Readline integration. + + Callback-based API that isolates the completion (find matches) + and view (display matches) providers from the GNU Readline machinery. + """ + + class CompleterState: + """A completer state is a completion candidate. + + States are initialized by the RL completion hook callback. + """ + + _rlstr: str + _item: Optional[object] + + def __init__(self, rlstr: str, item: Optional[object]) -> None: + """Initialize completer state. + + Args: + rlstr: The string to substitute the RL completion scope with, + e.g. a command name or a Devicetree path. + Must have at least the length of the completion scope. + item: The corresponding model object, if any, + e.g. a command option or a Devicetree node. + """ + self._rlstr = rlstr + self._item = item + + @property + def rlstr(self) -> str: + """The string to substitute the RL completion scope with.""" + return self._rlstr + + @property + def item(self) -> Optional[object]: + """The corresponding model object, if any.""" + return self._item + + def __eq__(self, other: object) -> bool: + """States equal when their RL substitution strings equal.""" + if isinstance(other, DTShReadline.CompleterState): + return self._rlstr == other._rlstr + return False + + def __lt__(self, other: object) -> bool: + """Alphabetical order of RL substitution strings.""" + if isinstance(other, DTShReadline.CompleterState): + return self._rlstr < other._rlstr + raise ValueError(other) + + def __repr__(self) -> str: + return self._rlstr + + CompletionCallback = Callable[ + [str, str, int, int], List["DTShReadline.CompleterState"] + ] + """Completer states provider callback prototype. + + That's where the completion logic is implemented. + + Args: + cs_txt: The text to complete, + i.e. the content of the Readline completion scope. + rlbuf: The current command line, + i.e. the Readline buffer's content. + cs_begin: The index within the Readline buffer where + the completion scope starts. + cs_end: The index within the Readline buffer's where + the completion scope ends. + + Returns: + The list of completer states. + """ + + DisplayCallback = Callable[ + [DTShOutput, List["DTShReadline.CompleterState"]], None + ] + """Completer states display callback prototype. + + That's where the completion matches are displayed. + + Args: + out: Where to display the completion matches. + states: The completer states to display. + """ + + # Completer states provider. + _completion_callback: CompletionCallback + + # Optional completion views provider. + # If unset, defaults to the readline module's implementation. + _display_callback: Optional[DisplayCallback] + + # Completer states, See rl_complete(). + _completer_states: List["DTShReadline.CompleterState"] + + # Where to display completion matches and restore the command line. + _stdout: DTShOutput + + # Path to the command history file. + _histfile: str + + def __init__( + self, + stdout: DTShOutput, + completion_callback: "DTShReadline.CompletionCallback", + display_callback: Optional["DTShReadline.DisplayCallback"] = None, + ) -> None: + """Initialize GNU readline integration. + + If readline support is enabled, read history file and set RL hooks. + + Args: + stdout: Bound terminal to restore the command line after the + completion matches display callback has been called. + completion_callback: The completions provider callback. + display_callback: The completion matches display callback. + """ + self._stdout = stdout + self._completer_states = [] + self._completion_callback = completion_callback + self._display_callback = display_callback + + self._histfile = _dtshconf.get_user_file("history") + if _has_readline: + self._rl_init() + self.read_history() + + def read_history(self) -> Optional[str]: + """Load command history file. + + Returns: + The history file path, or None if the history file is unavailable. + """ + if _has_readline: + if os.path.isfile(self._histfile): + try: + readline.read_history_file(self._histfile) + return self._histfile + except OSError as e: + print( + f"Failed to read command history: {e}", file=sys.stderr + ) + return None + + def save_history(self) -> Optional[str]: + """Write command history file. + + Returns: + The history file path, or None if failed to save history. + """ + if _has_readline: + try: + readline.write_history_file(self._histfile) + return self._histfile + except OSError as e: + print(f"Failed to write command history: {e}", file=sys.stderr) + return None + + def rl_complete(self, cs_txt: str, state: int) -> Optional[str]: + """Setup the GNU readline's completion hook. + + Actual completion is delegated to the completions provider callback. + + Args: + cs_txt: The readline completion scope content. + state: The state integer that iterates on the completion + candidates. + """ + if state == 0: + self._completer_states.clear() + + rlbuf = readline.get_line_buffer() + begin = readline.get_begidx() + end = readline.get_endidx() + + self._completer_states = self._completion_callback( + cs_txt, rlbuf, begin, end + ) + + try: + return self._completer_states[state].rlstr + except IndexError: + # No completion. + pass + return None + + def rl_display_matches_hook( + self, + substitution: str, + matches: Sequence[str], + longest_match_length: int, + ) -> None: + """Setup the GNU readline's display matches hook. + + Actual display is delegated to the completions display callback. + + Command line content and cursor position are eventually restored. + """ + if not self._display_callback: + raise NotImplementedError() + + del substitution # Unused. + del longest_match_length # Unused. + if not matches: + return + + rlbuf = readline.get_line_buffer() + end = readline.get_endidx() + # Backward steps needed to restore the cursor position. + n_back = len(rlbuf) - end + + # Move cursor bellow input line. + self._stdout.write() + + # Display completions. + self._display_callback(self._stdout, self._completer_states) + + # Restore command line. + self._stdout.write(_dtshconf.prompt_default, end="") + self._stdout.write(rlbuf, end="") + if n_back > 0: + # Move cursor back to insertion point. + # ANSI Cursor Back: CSI n D + self._stdout.write(f"\033[{n_back}D", end="") + # Update stdout without sending LF. + self._stdout.flush() + + def _rl_init(self) -> None: + readline.set_completer(self.rl_complete) + readline.set_completer_delims(f" \t{os.linesep}") + readline.parse_and_bind("tab: complete") + + if self._display_callback: + # Enable custom display callback if asked to. + readline.set_completion_display_matches_hook( + self.rl_display_matches_hook + ) diff --git a/src/dtsh/session.py b/src/dtsh/session.py index 9304dc0..7b3a527 100644 --- a/src/dtsh/session.py +++ b/src/dtsh/session.py @@ -1,402 +1,331 @@ -# Copyright (c) 2022 Chris Duf +# Copyright (c) 2023 Christophe Dufaza # # SPDX-License-Identifier: Apache-2.0 -"""Devicetree shell session.""" +"""Base interactive devicetree shell session. +A session binds a devicetree shell and a VT to run an interactive loop. +""" -from typing import cast, List, Optional, Union +from types import FrameType +from typing import Any, Optional, Sequence, List -import os -import re import signal import sys -from devicetree.edtlib import Node, Binding, Property +from devicetree import edtlib -from rich.console import Console -from rich.text import Text +from dtsh.model import DTModel +from dtsh.rl import DTShReadline +from dtsh.config import DTShConfig +from dtsh.autocomp import DTShAutocomp +from dtsh.io import DTShOutput, DTShOutputFile, DTShRedirect, DTShVT +from dtsh.shell import ( + DTSh, + DTShCommand, + DTShFlagHelp, + DTShError, + DTShUsageError, + DTShCommandError, + DTShCommandNotFoundError, +) +from dtsh.builtins.pwd import DTShBuiltinPwd +from dtsh.builtins.cd import DTShBuiltinCd +from dtsh.builtins.ls import DTShBuiltinLs +from dtsh.builtins.tree import DTShBuiltinTree +from dtsh.builtins.find import DTShBuiltinFind +from dtsh.builtins.alias import DTShBuiltinAlias +from dtsh.builtins.chosen import DTShBuiltinChosen -from dtsh.rl import readline -from dtsh.dtsh import Dtsh, DtshAutocomp, DtshCommand, DtshCommandOption, DtshSession, DtshVt -from dtsh.dtsh import DtshError, DtshCommandNotFoundError, DtshCommandUsageError, DtshCommandFailedError -from dtsh.shell import DevicetreeShell -from dtsh.term import DevicetreeTerm -from dtsh.autocomp import DevicetreeAutocomp -from dtsh.tui import DtshTui -from dtsh.config import DtshConfig +_dtshconf: DTShConfig = DTShConfig.getinstance() -class DevicetreeShellSession(DtshSession): - """Shell session with GNU readline history support. - """ - _dtsh: Dtsh - _term: DevicetreeTerm - _last_err: Union[DtshError, None] +class DTShSession: + """Base for interactive devicetree shell sessions. - def __init__(self, shell: Dtsh, term: DevicetreeTerm) -> None: - """Creates a session. + A session binds a devicetree shell and a VT to run + an interactive loop until the user quits or EOF (aka CTRL-D). - Arguments: - shell - the session's shell instance - term - the session's rich VT - """ - self._dtsh = shell - self._term = term - self._last_err = None + The session is also responsible for: - DtshConfig.readline_read_history() + - initializing the auto-completion support (depends on GNU readline) + - handling command output redirection streams - @property - def term(self) -> DevicetreeTerm: - """Session's VT.""" - return self._term + The run loop will trigger events which handlers may be overridden + by derived (rich) sessions. + """ - @property - def last_err(self) -> Union[DtshError, None]: - return self._last_err + _dtsh: DTSh + _vt: DTShVT - def run(self): - """Overrides DtshSession.run(). - """ - self._term.clear() - self.banner() + _rl: DTShReadline + _autocomp: DTShAutocomp - while True: - redir2vt = None - try: - self._term.write(DtshTui.mk_txt_node_path(self._dtsh.pwd)) - prompt = DtshTui.mk_ansi_prompt(self._last_err is not None) - cmdline = self._term.readline(prompt) - self._last_err = None - if cmdline: - if cmdline in ['q', 'quit', 'exit']: - # Exit process. - self.close() - - i = cmdline.rfind('>') - if i != -1: - redir2vt = FileStdoutVt(cmdline[i+1:].strip()) - self._dtsh.exec_command_string(cmdline[:i], redir2vt) - else: - self._dtsh.exec_command_string(cmdline, self._term) - - except DtshCommandNotFoundError as e: - self._last_err = e - self._term.write(f'dtsh: Command not found: {e.name}') - except DtshCommandUsageError as e: - if e.command.with_help: - self._term.write(e.command.usage) - else: - self._last_err = e - self._term.write(f'{e.command.name}: {e.msg}') - except DtshCommandFailedError as e: - self._last_err = e - self._term.write(f'{e.command.name}: {e.msg}') - except EOFError: - self._term.abort() - self.close() + _last_err: Optional[BaseException] - if redir2vt: - # Actually writing to the file happens in the VT dtor. - del redir2vt - redir2vt = None - if DtshTui.PROMPT_SPARSE: - self._term.write() + @classmethod + def create( + cls, dts_path: str, binding_dirs: Optional[Sequence[str]] = None + ) -> "DTShSession": + """Create a new devicetree shell session. - def close(self) -> None: - """Overrides DtshSession.close(). - """ - self._term.write('bye.', style=DtshTui.style_italic()) - DtshConfig.readline_write_history() - sys.exit(0) + Args: + dts_path: Path to the Devicetree source to open the session with. + binding_dirs: List of directories to search for + the YAML binding files this Devicetree depends on. - def banner(self): - """ + Raises: + DTShError: Typically DTS file not found or invalid, + or missing or invalid bindings. """ - self._term.write( - Text().append_tokens( - [ - ('dtsh', DtshTui.style_bold()), - (f" ({Dtsh.API_VERSION}): ", DtshTui.style_default()), - ('Shell-like interface to a devicetree', DtshTui.style_italic()) - ] - ) - ) - self._term.write( - Text().append_tokens( - [ - ('Help: ', DtshTui.style_default()), - ('man dtsh', DtshTui.style_bold()) - ] - ) - ) - self._term.write( - Text().append_tokens( - [ - ('How to exit: ', DtshTui.style_default()), - ('q', DtshTui.style_bold()), - (', or ', DtshTui.style_default()), - ('quit', DtshTui.style_bold()), - (', or ', DtshTui.style_default()), - ('exit', DtshTui.style_bold()), - (', or press ', DtshTui.style_default()), - ('Ctrl-D', DtshTui.style_bold()), - ] - ) + try: + dt = DTModel.create(dts_path, binding_dirs) + except (OSError, edtlib.EDTError) as e: + raise DTShError(f"DTS error: {e}") from e + + sh = DTSh( + dt, + [ + DTShBuiltinPwd(), + DTShBuiltinCd(), + DTShBuiltinLs(), + DTShBuiltinTree(), + DTShBuiltinFind(), + DTShBuiltinAlias(), + DTShBuiltinChosen(), + ], ) - self._term.write() + return cls(sh) - @staticmethod - def open(dt_source_path: Optional[str] = None, - dt_bindings_path: Optional[List[str]] = None) -> DtshSession: - """ - """ - global _session - global _autocomp - - if _session is not None: - raise DtshError('Session already opened') - - shell = DevicetreeShell.create(dt_source_path, dt_bindings_path) - term = DevicetreeTerm( - readline_completions_hook, - readline_display_hook - ) - _session = DevicetreeShellSession(shell, term) - _autocomp = DevicetreeAutocomp(shell) + def __init__( + self, + sh: DTSh, + vt: Optional[DTShVT] = None, + autocomp: Optional[DTShAutocomp] = None, + ) -> None: + """Initialize a session. - # We disable SIGINT (CTRL-C event). - signal.signal(signal.SIGINT, dtsh_session_sig_handler) + Will initialize the VT and auto-completion support. - return _session + Won't start the interactive loop. + Args: + sh: The session's shell. + vt: The session's VT. Defaults to DTShVT. + autocomp: GNU readline callbacks for completion and matches display. + Defaults to DTShAutocomp(). + """ + self._dtsh = sh + self._last_err = None -# Shell session singleton state. -_session: Union[DevicetreeShellSession, None] = None -_autocomp: Union[DevicetreeAutocomp, None] = None + self._vt = vt or DTShVT() + self._autocomp = autocomp or DTShAutocomp(self._dtsh) + self._rl = DTShReadline( + self._vt, + self._autocomp.complete, + self._autocomp.display, + ) -# GNU readline completer function callback for rl_completion_matches(). -# MUST answer completions that actually match te given prefix. -def readline_completions_hook(text: str, state: int) -> Union[str, None]: - if _autocomp is None: - return None + def run(self) -> None: # pylint: disable=too-many-branches + """Enter interactive loop. + + This will: + - disable the SIGINT signal + - clear VT and print banner + - repeat until user quits or EOF (e.g. CTRL-D): + - run pre_input_hook() + - read a command line from VT + - parse the command line + - setup command output redirection if asked to + - execute the command string + - dispatch the event to its handler if an error occurs + - if the command output was redirected, + explicitly flush the redirection stream + """ + # closing() the session when the pager is active breaks the TTY. + # As a work-around, we ignore SIGINT. + signal.signal(signal.SIGINT, self._sig_handler) - cmdline = readline.get_line_buffer() - completions = _autocomp.autocomplete(cmdline, text, 0) + self._vt.clear() + self.preamble_hook() - if state < len(completions): - hint = completions[state] - if len(completions) == 1: - # GNU readline will eventually replace 'text' with these - # state values (or their longest prefix). - # When there's only one possible completion, - # we can tell the user it's useless to press TAB again - # by appending a space to the corresponding 'state' value. - # - # We assume a hint that ends with '/': - # - is a node path - # - the node has children, pressing TAB again should show them - if not hint.endswith('/'): - hint += ' ' - return hint + # Session error state. + self._last_err = None - return None + while True: + self.pre_input_hook() + cmdline: Optional[str] = None + try: + cmdline = self._vt.readline( + _dtshconf.prompt_alt + if self._last_err + else _dtshconf.prompt_default + ) + except EOFError: + self._vt.write() + # Exit DTSh on EOF. + self.close() -# GNU readline implementation for rl_completion_display_matches_hook(). -# -def readline_display_hook(substitution, matches, longest_match_length) -> None: - if (_session is None) or (_autocomp is None): - return - - cmdline = readline.get_line_buffer() - - # Go bellow input line - _session.term.write() - - if _autocomp.model: - if _autocomp.mode == DtshAutocomp.MODE_DTSH_CMD: - model = cast(List[DtshCommand], _autocomp.model) - view = DtshTui.mk_command_hints_display(model) - elif _autocomp.mode == DtshAutocomp.MODE_DTSH_OPT: - model = cast(List[DtshCommandOption], _autocomp.model) - view = DtshTui.mk_option_hints_display(model) - elif _autocomp.mode == DtshAutocomp.MODE_DT_BINDING: - model = cast(List[Binding], _autocomp.model) - view = DtshTui.mk_binding_hints_display(model) - elif _autocomp.mode == DtshAutocomp.MODE_DT_NODE: - model = cast(List[Node], _autocomp.model) - view = DtshTui.mk_node_hints_display(model) - elif _autocomp.mode == DtshAutocomp.MODE_DT_PROP: - model = cast(List[Property], _autocomp.model) - view = DtshTui.mk_property_hints_display(model) - else: - # Autcomp mode MODE_ANY. - view = DtshTui.mk_grid(1) - for m in _autocomp.model: - view.add_row(DtshTui.mk_txt(str(m))) - _session.term.write(view) + if cmdline: + if cmdline.strip() in ["q", "quit", "exit"]: + # Exit DTSh process. + self.close() + + cmd: DTShCommand + argv: List[str] + redir2: Optional[str] + try: + # Parse command line into the command to execute, + # its arguments, and the redirection directive, if any. + # Won't parse the command arguments yet, + # but will fault if the command is undefined. + cmd, argv, redir2 = self._dtsh.parse_cmdline(cmdline) + + out: DTShOutput = ( + self.open_redir2(redir2) if redir2 else self._vt + ) + + except DTShRedirect.Error as e: + # Failed to initialize redirection stream. + self._last_err = e + self.on_redir2_error(e) - _session.term.write(DtshTui.mk_ansi_prompt(), end='') - _session.term.write(cmdline, end='') - sys.stdout.flush() + except DTShCommandNotFoundError as e: + self._last_err = e + self.on_cmd_not_found_error(e) + else: + try: + cmd.execute(argv, self._dtsh, out) + + # Last command line succeeded, reset error state. + self._last_err = None + + except DTShUsageError as e: + if e.cmd.with_flag(DTShFlagHelp): + # The user asked for help (-h or --h). + self.on_cmd_help(e.cmd) + else: + # Invalid command arguments. + self._last_err = e + self.on_cmd_usage_error(e) + + except DTShCommandError as e: + # Command execution failed. + self._last_err = e + self.on_cmd_failed_error(e) + + finally: + if out is not self._vt: + # Flush the file the command output + # was redirected to, even on error. + # Note that the shell (error) messages themselves + # are always written to the session VT, + # and never redirected. + out.flush() + + if _dtshconf.prompt_sparse: + self._vt.write() + + def close(self, status: int = 0) -> None: + """Terminate interactive session. + + This will: + - run pre_exit_hook() + - save readline history file, if supported + - close the session's VT + - exit the dtsh process + + Args: + status: exit status, defaults to 0, aka "no error". + """ + self.pre_exit_hook(status) + self._rl.save_history() + sys.exit(status) -def dtsh_session_sig_handler(signum, frame): - # FIXME: closing() the session when the pager is active - # breaks the TTY. - # As a work-around, we ignore SIGINT. - if signum == signal.SIGINT: - return + def open_redir2(self, redir2: str) -> DTShOutput: + """Open DTSh redirection output stream. + Args: + redir2: Command line redirection. + Returns: + The redirection output stream. -class FileStdoutVt(DtshVt): - """VT implementation for command output redirection. - """ + Raises: + DTShRedirect.Error: Failed to setup redirection output. + """ + redirect = DTShRedirect(redir2) + return DTShOutputFile(redirect.path, redirect.append) - _console: Console + def preamble_hook(self) -> None: + """Session's preamble, aka banner.""" + self._vt.write("dtsh: shell-like interface with Zephyr Devicetree") + self._vt.write() + def pre_input_hook(self) -> None: + """Hook called before the session prompts for the next command line.""" + self._vt.write(self._dtsh.pwd) - def __init__(self, path: str) -> None: - """ Creates a new VT. + def on_cmd_help(self, cmd: DTShCommand) -> None: + """Called when the user's asked for a command's help. - Arguments: - path -- Path of the file the command output is redirected to; - the file format (HTML, SVG, text) is determined by the filename - extension (.html, .svg, default). - """ - try: - path = os.path.abspath(path) - self._file = open(path, 'w') - except IOError as e: - raise DtshError(f'could not open file: {path}', e) - self._console = Console(highlight=False, - theme=DtshTui.theme(), - record=True) - - def pager_enter(self) -> None: - """Overrides DtshVt.pager_enter(). - - Ignored, no pager. + Args: + cmd: The command to help with. """ - pass + self._vt.write(f"usage: {cmd.synopsis}") + self._vt.write() + self._vt.write(f"{cmd.brief.capitalize()}.") - def pager_exit(self) -> None: - """Overrides DtshVt.pager_exit(). + def on_cmd_not_found_error(self, e: DTShCommandNotFoundError) -> None: + """Called when the user's asked for an unknown command. - Ignored, no pager. + Args: + e: The error event. """ - pass + self._vt.write(f"dtsh: command not found: {e.name}") - def write(self, *args, **kwargs) -> None: - """Overrides DtshVt.write(). + def on_cmd_usage_error(self, e: DTShUsageError) -> None: + """Called when the user's misused a command. - Quietly write to console, recording output. - - Arguments: - args -- Positional arguments for Console.print() - kwargs -- Keyword arguments for Console.print() + Args: + e: The error event. """ - with self._console.capture(): - self._console.print(*args, **kwargs) + self._vt.write(f"{e.cmd.name}: {e.msg}") - def clear(self) -> None: - """Overrides DtshVt.clear(). + def on_cmd_failed_error(self, e: DTShCommandError) -> None: + """Called when the last command execution has failed. - Ignored, does not clear the console. + Args: + e: The error event. """ - pass + self._vt.write(f"{e.cmd.name}: {e.msg}") - def readline(self, ansi_prompt: str) -> str: - """Overrides DtshVt.readline(). + def on_redir2_error(self, e: DTShRedirect.Error) -> None: + """Called when failed to setup redirection stream. - Returns an empty string (no input stream). + Args: + e: The error event. """ - return '' + self._vt.write(f"dtsh: {e}") - def abort(self) -> None: - """Overrides DtshVt.abort(). + def pre_exit_hook(self, status: int) -> None: + """Hook called when the user terminates the session. - Ignored. + Args: + status: The exit status. """ - pass - - # Actually writing to file happens in he dtor. - # - def __del__(self): - if self._file.name.endswith('.html'): - html = self._console.export_html() - self._file.write(html) - elif self._file.name.endswith('.svg'): - self._write_svg() + if status: + self._vt.write(f"bye ({status}).") else: - txt = self._console.export_text() - self._file.write(txt) - self._file.close() - - def _write_svg(self): - svg = self._console.export_svg(title='') - # Remove macOS-ish windows buttons. - s = re.search(_RE_SVG_BUTTONS, svg) - if s: - svg = svg[:s.start()] + svg[s.end() + 1:] - # Remove top padding - svg_vstr: List[str] = [] - re_view = re.compile(_RE_SVG_VIEWPORT) - re_rect = re.compile(_RE_SVG_RECT) - re_trans = re.compile(_RE_SVG_TRANSFORM) - for line in svg.splitlines(keepends=True): - # Substract hard coded padding to viewport's height. - m = re_view.match(line) - if m and m.groups(): - width = m.groups()[0] - height = m.groups()[1] - line = line.replace( - f'viewBox="0 0 {width} {height}"', - f'viewBox="0 0 {width} {float(height) - _SVG_HARD_PADDING}"' - ) - - # Substract hard coded padding to viewport's height. - m = re_rect.match(line) - if m and m.groups(): - height = m.groups()[0] - line = line.replace( - f'height="{height}"', - f'height="{float(height) - _SVG_HARD_PADDING}"') - - # Substract hard coded padding to transformation y. - m = re_trans.match(line) - if m and m.groups(): - x = m.groups()[0] - y = m.groups()[1] - line = line.replace( - f'translate({x}, {y})', - f'translate({x}, {int(y) - _SVG_HARD_PADDING})' - ) - - svg_vstr.append(line) - - self._file.writelines(svg_vstr) - - -# Hard coded top padding in rich.console.export_svg(). -# -_SVG_HARD_PADDING = 40 - -# All values in this regex are hard coded in rich.console.export_svg(), -# and do not seem to depen on e.g. a theme. -# We'll remove the whole pattern. -_RE_SVG_BUTTONS = r'''\s*\s*\s*\s*''' - -# We'll substract the hard coded padding to the viewport height (the second group). -_RE_SVG_VIEWPORT = r'''\s*\s*''' - -# We'll substract the hard coded padding to the rect height. -_RE_SVG_RECT = r'''\s*\s*''' - -# We'll substract the hard coded padding to the transformation y (the second group). -_RE_SVG_TRANSFORM = r'''\s*\s*''' + self._vt.write("bye.") + + def _sig_handler(self, signum: int, frame: Optional[FrameType]) -> Any: + # closing() the session when the pager is active breaks the TTY. + # As a work-around, we ignore SIGINT. + del frame # Unused. + if signum == signal.SIGINT: + pass diff --git a/src/dtsh/shell.py b/src/dtsh/shell.py index 7f224e1..fa56389 100644 --- a/src/dtsh/shell.py +++ b/src/dtsh/shell.py @@ -1,89 +1,1260 @@ -# Copyright (c) 2022 Chris Duf +# Copyright (c) 2023 Christophe Dufaza # # SPDX-License-Identifier: Apache-2.0 -"""Devicetree shell PoC implementation.""" +"""Devicetree shell. -import os -from typing import List, Optional +Shell-like interface with a devicetree model: -from devicetree.edtlib import EDT +- parse command lines into commands, arguments and parameters +- walk the devicetree through a hierarchical file-system metaphor + (current working branch, relative paths and path references, + Devicetree labels) +- match nodes with flexible criteria +- sort and filter nodes -from dtsh.dtsh import Dtsh, DtshUname, DtshError -from dtsh.builtin_pwd import DtshBuiltinPwd -from dtsh.builtin_alias import DtshBuiltinAlias -from dtsh.builtin_chosen import DtshBuiltinChosen -from dtsh.builtin_cd import DtshBuiltinCd -from dtsh.builtin_ls import DtshBuiltinLs -from dtsh.builtin_tree import DtshBuiltinTree -from dtsh.builtin_find import DtshBuiltinFind -from dtsh.builtin_cat import DtshBuiltinCat -from dtsh.builtin_man import DtshBuiltinMan -from dtsh.builtin_uname import DtshBuiltinUname +Unit tests and examples: tests/test_dtsh_shell.py +""" -class DevicetreeShell(Dtsh): - """Devicetree shell PoC implementation. +from typing import ( + cast, + Type, + TypeVar, + Union, + Optional, + Sequence, + List, + Tuple, + Dict, +) + +import getopt +import re +import shlex + +from dtsh.model import DTPath, DTModel, DTNode, DTNodeSorter, DTNodeCriterion +from dtsh.io import DTShOutput +from dtsh.rl import DTShReadline + + +DTShOptionT = TypeVar("DTShOptionT", bound="DTShOption") +"""Type variable used for polymorphic access to command options. + + Automatically downcast an option to its specialized type. + + See DTShCommand.with_option(). +""" + +DTShParamT = TypeVar("DTShParamT", bound="DTShParameter") +"""Type variable used for polymorphic access to command parameters. + + Automatically downcast a parameter to its specialized type. + + See DTShCommand.with_param(). +""" + + +class DTShOption: + """Base definition for shell command options. + + All options are optional (sic): an option is either a boolean flag that + defaults to False, or an optional argument with or without default value. + + Please, assume these are const-qualified class attributes: BRIEF, SHORTNAME, + and LONGNAME. + """ + + BRIEF: str = "" + """Brief description. + + Constant to be defined by concrete options. + """ + + SHORTNAME: Optional[str] = None + """Single letter option name, e.g. "h". + + Constant to be defined by concrete options that accept a short getopt form. + """ + + LONGNAME: Optional[str] = None + """Long option name, e.g. "help". + + Constant to be defined by concrete options that accept a long getopt form. + """ + + def __init__(self) -> None: + """Initialize the command option, reset state.""" + self.reset() + + @property + def shortname(self) -> Optional[str]: + """Single letter option name (without the "-" prefix).""" + return type(self).SHORTNAME + + @property + def longname(self) -> Optional[str]: + """Long option name (without the "--" prefix).""" + return type(self).LONGNAME + + @property + def brief(self) -> Optional[str]: + """Brief description.""" + return type(self).BRIEF + + @property + def getopt_short(self) -> Optional[str]: + """Short GNU getopt option form. + + If the option is a flag, this is its short name. + + If the option is an argument, the short name is post-fixed with ":", + see DTShArg.getopt_short(). + """ + return self.shortname + + @property + def getopt_long(self) -> Optional[str]: + """Long GNU getopt option form. + + If the option is a flag, this is its long name. + + If the option is an argument, the long name is post-fixed with "=", + see DTShArg.getopt_long(). + """ + return self.longname + + @property + def usage(self) -> str: + """Usage string. + + If the option is a flag, this is the space separated list of + the option's lexical forms, e.g. "-h --help". + + If the option is an argument, + the usage is post-fixed with the argument's name, see DTShArg.usage(). + """ + tokens = [] + if self.shortname: + tokens.append(f"-{self.shortname}") + if self.longname: + tokens.append(f"--{self.longname}") + return " ".join(tokens) + + @property + def isset(self) -> bool: + """Whether this option has been set when parsing the command string.""" + return False + + def reset(self) -> None: + """Reset this option before parsing a new command string. + + This will unset a flag. + An argument is either unset or reset to a default value. + """ + + def parsed(self, value: Optional[str] = None) -> None: + """Set this option. + + If the option is a flag, set the flag. + + If the option is an argument that expects a value, + set the argument raw valued parsed from the command line. + + Invoked by the supporting command when parsing + a command string into options and parameters, + after the GNU getopt parser has successfully finished. + + Args: + value: The option's value parsed from the command string. + A flag does not expect any value. + """ + + def __eq__(self, other: object) -> bool: + """Options equal when their names equal.""" + if isinstance(other, DTShOption): + return (other.shortname == self.shortname) and ( + other.longname == self.longname + ) + return False + + def __hash__(self) -> int: + """An option identity is based on its names.""" + return hash(f"-{self.shortname}--{self.longname}") + + +class DTShFlag(DTShOption): + """Devicetree shell command flag. + + A flag enabled a command's option and does not expect any value. + """ + + # The flag's state. + _on: bool + + @property + def isset(self) -> bool: + """Whether this flag has been set when parsing the command string. + + Overrides DTShOption.isset(). + """ + return self._on + + def reset(self) -> None: + """Unset this flag. + + Overrides DTShOption.reset(). + """ + self._on = False + + def parsed(self, value: Optional[str] = None) -> None: + """Set this flag. + + Overrides DTShOption.parsed(). + + Args: + value: A flag does not expect a value. + """ + if value: + # Should be None since DTShFlag.parsed() is called once + # the GNU getopt parser has successfully finished. + raise ValueError(value) + self._on = True + + def __repr__(self) -> str: + return f"{type(self)}: {self.isset}" + + +class DTShArg(DTShOption): + """Devicetree shell command argument. + + An argument is a command option that expects a value. + """ + + # Name the argument will appear with in the command synopsis. + _name: str + + # See raw(). + _raw: Optional[str] + + def __init__(self, argname: str) -> None: + """ + Initialize argument. + + Args: + name: Name the argument will appear with in the command synopsis. + """ + super().__init__() + self._name = argname.upper() + + @property + def getopt_short(self) -> Optional[str]: + """Add argument's post-fix to the short getopt option form. + + Overrides DTShOption.getopt_short(). + """ + return f"{self.shortname}:" if self.shortname else None + + @property + def getopt_long(self) -> Optional[str]: + """Add argument's post-fix to the long getopt option form. + + Overrides DTShOption.getopt_long(). + """ + return f"{self.longname}=" if self.longname else None + + @property + def usage(self) -> str: + """Add argument's name to the usage string. + + Overrides DTShOption.usage(). + """ + return " ".join([super().usage, self._name]) + + @property + def raw(self) -> Optional[str]: + """Raw argument value parsed on the command string. + + None if the argument has not been parsed(). + """ + return self._raw + + @property + def isset(self) -> bool: + """Whether this argument has been set when parsing the command string. + + Overrides DTShOption.isset(). + """ + return self._raw is not None + + def reset(self) -> None: + """Reset this argument. + + Overrides DTShOption.reset(). + """ + self._raw = None + + def parsed(self, value: Optional[str] = None) -> None: + """Parsing the command string has set this argument. + + Overrides DTShOption.parsed(). + + Args: + value: The raw argument value parsed from the command string. + + Raises: + DTShError: The value does not match the option's usage. + """ + # Should neither be None nor an empty string, since DTShArg.parsed() + # is called once the GNU getopt parser has successfully finished. + if not value: + raise ValueError(None) + self._raw = value + + def autocomp( + self, txt: str, sh: "DTSh" + ) -> List[DTShReadline.CompleterState]: + """Auto-complete with possible values for this argument. + + Args: + txt: The input text to complete. + sh: The shell context. + + Returns: + A list of argument values that are valid completion states. + """ + del txt + del sh + return [] + + +class DTShParameter: + """Devicetree shell command parameter. + + Devicetree shell commands support at most one parameter, + that may expect one or several values, + depending on its multiplicity. """ - def __init__(self, edt: EDT, sysinfo: DtshUname) -> None: - """Initialize a devicetree shell with a PoC set of built-in commands. - - Arguments: - edt -- devicetree model (sources and bindings), provided by edtlib - """ - super().__init__(edt, sysinfo) - for cmd in [ - DtshBuiltinPwd(self), - DtshBuiltinAlias(self), - DtshBuiltinChosen(self), - DtshBuiltinCd(self), - DtshBuiltinLs(self), - DtshBuiltinTree(self), - DtshBuiltinCat(self), - DtshBuiltinUname(self), - DtshBuiltinFind(self), - DtshBuiltinMan(self) - ]: - self._builtins[cmd.name] = cmd - - @staticmethod - def create(dt_source_path: Optional[str] = None, - dt_bindings_path: Optional[List[str]] = None) -> Dtsh: - """Create a shell-like interface to a devicetree . - - Factory method that loads a devicetree source file and its bindings - to build a devicetree model (EDT). - On success, creates a shell-like interface to this devicetree. - - Arguments: - dt_source_path -- Path to a device tree source file. - If unspecified, defaults to '$PWD/build/zephyr/zephyr.dts' - dt_bindings_path -- List of path to search for DT bindings. - If unspecified, and ZEPHYR_BASE is set, - defaults to Zephyr's DT bindings. - - Return a new devicetree shell instance. - - Raises DtshError when the devicetree model initialization has failed. - """ - if not dt_source_path: - dt_source_path = os.path.join(os.getcwd(), - # Default to current Zephyr project DTS - 'build', 'zephyr', 'zephyr.dts') - if not os.path.isfile(dt_source_path): - raise DtshError(f"DT source file not found: {dt_source_path}") - - # Configure initialization with command line arguments, - # environment variables, and CMake cached variables. - sysinfo = DtshUname(dt_source_path, dt_bindings_path) - - if not sysinfo.dt_binding_dirs: - raise DtshError('Please provide DT bindings or set ZEPHYR_BASE.') + MultiplicityT = Union[str, int] + + # Parameter definition. + _name: str + _multiplicity: "DTShParameter.MultiplicityT" + _brief: str + + # Raw parameter values parsed from the command string. + _raw: List[str] + + def __init__( + self, name: str, multiplicity: "DTShParameter.MultiplicityT", brief: str + ) -> None: + """Initialize the command's parameter. + + Args: + name: Name the parameter will appear with in the command synopsis. + multiplicity: Parameter multiplicity. + brief: Brief description of the parameter. + """ + self._name = name.upper() + self._multiplicity = multiplicity + self._brief = brief + self.reset() + + @property + def brief(self) -> str: + """Brief parameter description.""" + return self._brief + + @property + def multiplicity(self) -> "DTShParameter.MultiplicityT": + """Parameter multiplicity. + + - "?": optional parameter, at most one value + - "+": required parameter, at least one value + - "*": optional parameter, any number of values + - N: requirement parameter, exactly N values + """ + return self._multiplicity + + @property + def usage(self) -> str: + """Parameter usage string, e.g. "[PATH]".""" + if self._multiplicity == "?": + return f"[{self._name}]" + if self._multiplicity == "+": + return f"{self._name} [{self._name} ...]" + if self._multiplicity == "*": + return f"[{self._name} ...]" + if isinstance(self._multiplicity, int): + N: int = self._multiplicity + return " ".join(N * [self._name]) + raise ValueError(self._multiplicity) + + @property + def raw(self) -> Sequence[str]: + """Raw parameter values parsed from the command string.""" + return self._raw + + def parsed(self, values: List[str]) -> None: + """Parsing the command string has set this parameter. + + Args: + values: The parameter value(s) parsed from the command line. + + Raises: + DTShError: Parameter multiplicity error. + """ + argc = len(values) + + if self._multiplicity == "?": + # Expect argc in [0, 1]. + if argc > 1: + raise DTShError( + f"{self._name}: expects at most one value (got {argc})" + ) + + elif self._multiplicity == "+": + # Expect argc >= 1 + if argc == 0: + raise DTShError(f"{self._name}: missing parameter value") + + elif self._multiplicity == "*": + # Any number of values is allowed. + pass + + elif isinstance(self._multiplicity, int): + N: int = self._multiplicity + if argc != N: + raise DTShError( + f"{self._name}: expects {N} value(s) (got {argc})" + ) + + else: + # Invalid parameter definition. + raise ValueError(self._multiplicity) + + self._raw = values + + def reset(self) -> None: + """Reset this parameter values before parsing the command line.""" + self._raw = [] + + def autocomp( + self, txt: str, sh: "DTSh" + ) -> List[DTShReadline.CompleterState]: + """Auto-complete parameter value. + + Args: + txt: The input text to complete. + sh: The shell context. + + Returns: + A list of parameter values that are valid completion states. + """ + del txt + del sh + return [] + + +class DTShCommand: + """Devicetree shell command.""" + + # See name(). + _name: str + + # See brief(). + _brief: str + + # See options(). + _options: List[DTShOption] + + # See param(). + _param: Optional[DTShParameter] + + def __init__( + self, + name: str, + brief: str, + options: Optional[Sequence[DTShOption]], + parameter: Optional[DTShParameter], + ) -> None: + """Initialize a new devicetree shell command. + + Args: + name: The command's name. + brief: Brief command description. + options: The options supported by this command. + parameter: The parameter expected by this command; if any. + """ + self._name = name + self._brief = brief + self._options = list(options) if options else [] + self._options.insert(0, DTShFlagHelp()) + self._param = parameter + + @property + def name(self) -> str: + """Command name.""" + return self._name + + @property + def brief(self) -> str: + """Brief description.""" + return self._brief + + @property + def options(self) -> Sequence[DTShOption]: + """Supported options. + + All commands support the "-h --help" flag to request + the command's usage. + """ + return self._options + + @property + def param(self) -> Optional[DTShParameter]: + """Expected parameter.""" + return self._param + + @property + def synopsis(self) -> str: + """The command's synopsis.""" + tokens = [self._name] + for opt in self._options: + # All [option]s are optional (sic). + tokens.append(f"[{opt.usage}]") + if self._param: + tokens.append(self._param.usage) + return " ".join(tokens) + + @property + def getopt_short(self) -> str: + """Short options specification string compatible with GNU getopt. + + E.g. "hL:" for a command that supports a flag "-h" + and an argument "-L". + """ + return "".join( + # getopt_short should be defined, the or clause is added + # for type hinting consistency. + [opt.getopt_short or "" for opt in self._options if opt.shortname] + ) + + @property + def getopt_long(self) -> List[str]: + """Long options specification list compatible with GNU getopt. + + E.g. ["help", "depth="] for a command that supports a flag "--help", + and an argument "--depth ". + """ + # getopt_long should be defined, the or clause is added + # for type hinting consistency. + return [opt.getopt_long or "" for opt in self._options if opt.longname] + + def option(self, name: str) -> Optional[DTShOption]: + """Retrieve command options by lexical name. + + Args: + name: An option's lexical name, either in short form (e.g. "-h"), + or in long form (e.g. "--help"). + + Returns: + The searched for command's option, None if not found. + """ + if name.startswith("--"): + longname = name[2:] + for opt in self._options: + if longname == opt.longname: + return opt + elif name.startswith("-"): + shortname = name[1:] + for opt in self._options: + if shortname == opt.shortname: + return opt + return None + + def with_option(self, option_t: Type[DTShOptionT]) -> DTShOptionT: + """Polymorphic access to the command options. + + Args: + option_t: The option type. + The command MUST support this option type. + + Returns: + The command's option downcast to its specialized type. + """ + opt = None + if option_t.SHORTNAME: + opt = self.option(f"-{option_t.SHORTNAME}") + elif option_t.LONGNAME: + opt = self.option(f"--{option_t.LONGNAME}") + if not opt: + raise KeyError(option_t) + return cast(DTShOptionT, opt) + + def with_flag(self, flag_t: Type[DTShFlag]) -> bool: + """Access a flag state. + + Args: + flag_t: The flag type. + The command MUST support this flag type. + + Returns: + True when the command flag is set. + """ + return self.with_option(flag_t).isset + + def with_arg(self, arg_t: Type[DTShOptionT]) -> DTShOptionT: + """Polymorphic access to the command arguments. + + Args: + arg_t: The argument type. + The command MUST support this argument type. + + Returns: + The command argument downcast to its specialized type. + """ + return self.with_option(arg_t) + + def with_param(self, param_t: Type[DTShParamT]) -> DTShParamT: + """Polymorphic access to the command parameter. + + Args: + param_t: The parameter type. + The command MUST expect a parameter. + + Returns: + The command parameter downcast to its specialized type. + """ + if self._param: + return cast(DTShParamT, self._param) + raise KeyError(param_t) + + def reset(self) -> None: + """Reset the command's options and parameter.""" + for opt in self._options: + opt.reset() + if self._param: + self._param.reset() + + def parse_argv(self, argv: Sequence[str]) -> None: + """Parse a command string into options and parameter. + + Args: + argv: The command string lexical split. + + Raises: + DTShUsageError: The arguments do not match the command usage. + """ + # Always reset options and parameter before parsing. + self.reset() + try: + parsed_opts, parsed_param_values = getopt.gnu_getopt( + list(argv), self.getopt_short, self.getopt_long + ) + except (getopt.GetoptError, DTShError) as e: + raise DTShUsageError(self, e.msg) from e try: - edt = EDT(sysinfo.dts_path, sysinfo.dt_binding_dirs) - except Exception as e: - raise DtshError('Devicetree initialization failed.', e) + for opt_name, opt_arg in parsed_opts: + opt = self.option(opt_name) + if opt: + opt.parsed(opt_arg) + else: + # Should not happen, since the getopt parser + # has just successfully finished. + pass + except DTShError as e: + raise DTShUsageError(self, f"{opt_name}: {e.msg}") from e + + if self._param: + try: + self._param.parsed(parsed_param_values) + except DTShError as e: + raise DTShUsageError(self, e.msg) from e + + elif parsed_param_values: + raise DTShUsageError( + self, f"unexpected parameter: {' '.join(parsed_param_values)}" + ) + + if self.with_flag(DTShFlagHelp): + # User's asked for help. + raise DTShUsageError(self) + + def execute(self, argv: Sequence[str], sh: "DTSh", out: DTShOutput) -> None: + """Execute the command. + + Args: + argv: The command arguments parsed from the command string + (options and parameter values). + sh: The shell context. + out: Where the command will write its output. + + Raises: + DTShUsageError: The arguments does not match the command usage. + DTShCommandError: The command execution has failed. + """ + del sh # Unused by base implementation. + del out # Unused by base implementation. + self.parse_argv(argv) + + def __eq__(self, other: object) -> bool: + """Commands equal when their names equal.""" + if isinstance(other, DTShCommand): + return other.name == self.name + return False + + def __lt__(self, other: object) -> bool: + """Default order based on command names.""" + if isinstance(other, DTShCommand): + return self._name < other._name + raise TypeError(other) + + def __hash__(self) -> int: + """A command identity is based on its names.""" + return hash(self.name) + + +class DTSh: + """Devicetree shell. + + Shell-like interface with a devicetree: + + - hierarchical file-system metaphor: view the device tree from a + current working branch, support for relative paths and path references + - built-in commands to walk, search and describe the devicetree + - parse command lines and execute command strings + """ + + VERSION_STRING = "0.2rc1" + """The devicetree shell version string. + + This version identifies: + + - the Zephyr West command syntax and arguments + - the devicetree shell command line syntax + - the defined built-in devicetree shell commands, + including their options and parameters + - the devicetree shell configuration file syntax (see dtsh.ini) + and the bundled default configuration + - the devicetree shell theme file syntax (see rich/theme.ini) + and the bundled default theme + """ + + class PathExpansion: + """Path expansion. + + See path_expansion(). + """ + + _prefix: str + _nodes: Sequence[DTNode] + + def __init__(self, prefix: str, nodes: Sequence[DTNode]) -> None: + self._prefix = prefix + self._nodes = nodes + + @property + def prefix(self) -> str: + """Expansion's prefix.""" + return self._prefix + + @property + def nodes(self) -> Sequence[DTNode]: + """Matched nodes.""" + return self._nodes + + def __repr__(self) -> str: + return f"{self.prefix}: {self.nodes}" + + def __eq__(self, other: object) -> bool: + if isinstance(other, DTSh.PathExpansion): + return (other.prefix == self.prefix) and ( + other.nodes == self.nodes + ) + return False + + # Match label references (think &) in devicetree paths. + _re_labelref: re.Pattern[str] = re.compile(r"^(?P&[^/]+)") + + # See commands(). + _commands: Dict[str, DTShCommand] + + # See dt(). + _dt: DTModel + + # See cwd(). + _cwd: DTNode + + def __init__( + self, + dt: DTModel, + builtins: Sequence[DTShCommand], + ) -> None: + """Initialize a devicetree shell. + + The current working branch is set to the devicetree root. + + Args: + dt: The devicetree to interface with. + builtins: The shell built-in commands. + """ + self._dt = dt + self._cwd = self._dt.root + self._commands = {cmd.name: cmd for cmd in builtins} + + @property + def dt(self) -> DTModel: + """The devicetree model this shell interfaces with.""" + return self._dt + + @property + def cwd(self) -> DTNode: + """The current working branch.""" + return self._cwd + + @property + def pwd(self) -> str: + """Path name of the current working branch.""" + return self._cwd.path + + @property + def commands(self) -> List[DTShCommand]: + """The commands supported by this shell.""" + return list(self._commands.values()) + + def realpath(self, path: str) -> str: + """Normalize a devicetree path and resolve DT labels. + + Args: + path: An absolute or relative path, possibly containing + path references and DT labels. + + Returns: + A devicetree path name. + + Raises: + DTPathNotFoundError: Failed to resolve a devicetree label. + """ + m = DTSh._re_labelref.match(path) + if m and m.group("labelref"): + labelref = m.group("labelref") + label = labelref[1:] + try: + node = self._dt.labeled_nodes[label] + path = path.replace(m.group("labelref"), node.path, 1) + except KeyError as e: + raise DTPathNotFoundError(labelref) from e + return DTPath.abspath(path, self._cwd.path) + + def pathway(self, node: DTNode, prefix: str) -> str: + """Get the pathway from an origin to a node. + + The origin node is represented by a prefix: it may be any expression + that resolves to a devicetree branch. + + The pathway is then the relative path from origin to node, + where the prefix is substituted for the origin's path. + + Pathways match how common POSIX shell commands like "ls", "find" + or "tree" would output file and directory paths. + + Args: + node: The pathway destination node. + prefix: The origin's prefix. An empty prefix resolves to + the shell's current working branch, represented by an empty string. + + Returns: + The pathway from origin to node. + + Raises: + DTPathNotFoundError: The prefix does not resolve to a node. + """ + relpath = DTPath.relpath(node.path, self.node_at(prefix).path) + + if relpath == ".": + path = prefix + else: + path = DTPath.join(prefix, relpath) + + return path + + def path_expansion( + self, path: str, enabled_only: bool = False + ) -> PathExpansion: + """Expand a DT path expression that may contain "*" wild-cards. + + Boilerplate code to expand a DT path expression into: + - the nodes the expression expands to + - a prefix to use as origin when getting pathways + + If the path expression actually contains wild-cards: + - the expression expands to the matching nodes (globbing), + according to enabled_only + - in pathways: + - prefix is a valid origin to substitute for the dirname + in the matched nodes' paths + - the current working branch is represented by an empty string + unless the path expression explicitly starts with "." + + Otherwise, if no expansion actually happens: + - the expression should resolve to a unique node, ignoring enabled_only + - in pathways: + - prefix will represent this node's path as a common origin + for relative pathways + - "." is substituted for an empty prefix + + See also DTSh.pathway(). + + Args: + path: A DT path expression. + enabled_only: Whether to filter out disabled nodes. + + Returns: + The expanded path. May be empty if all nodes have been + filtered out because they are disabled. + + Raises: + DTPathNotFoundError: The expanded path is not a node's path, + or the path expansion didn't match any node. + """ + prefix: str + nodes: List[DTNode] + + basename = DTPath.basename(path) + if "*" in basename: + dirname = DTPath.dirname(path) + if dirname == "." and not path.startswith("."): + prefix = "" + else: + prefix = dirname + + pattern = re.escape(basename).replace(r"\*", ".*") + pattern = f"^{pattern}$" + re_basename = re.compile(pattern) + + # Path expansion. + globs = [ + node + for node in self.node_at(dirname).children + if re_basename.match(node.name) + ] + if not globs: + # We consider empty expansions as "path not found" errors, + # like most Un*x shells do. + raise DTPathNotFoundError(path) + + if enabled_only: + nodes = [node for node in globs if node.enabled] + else: + nodes = globs + + else: + prefix = path or "." + nodes = [self.node_at(path)] + + return DTSh.PathExpansion(prefix, nodes) + + def node_at(self, path: str) -> DTNode: + """Access the devicetree node at path. + + The path is normalized before the node lookup. + + The node MUST exist. + + Args: + path: An absolute or relative DT path. + An empty path will answer the current working branch. + + Returns: + The node at path. + + Raises: + DTPathNotFoundError: Invalid devicetree path. + """ + pathname = self.realpath(path) + try: + return self._dt[pathname] + except KeyError as e: + raise DTPathNotFoundError(pathname) from e + + def cd(self, path: Optional[str] = None) -> None: + """Change the current working branch. + + Args: + path: Absolute or relative DT path to the new working branch. + Defaults to the devicetree root. + + Raises: + DTPathNotFoundError: The destination branch does not exist. + """ + self._cwd = self.node_at(path) if path else self.dt.root + + def find( + self, + path: str, + criterion: DTNodeCriterion, + /, + order_by: Optional[DTNodeSorter] = None, + reverse: bool = False, + enabled_only: bool = False, + ) -> List[DTNode]: + """Search branch at path. + + Args: + path: Absolute or relative path to the devicetree branch to search. + An empty path will represent the current working branch. + criterion: The criterion the nodes must match. + order_by: Sort matched nodes, None will preserve DTS-order. + reverse: Whether to reverse sort order. + If set and no order_by is given, means reverse DTS-order. + enabled_only: Whether to stop at disabled branches. + + Returns: + The list of matched nodes. + + Raises: + DTPathNotFoundError: A search branch does not exist. + """ + root = self.node_at(path) + nodes = root.find( + criterion, + # We don't want children ordering here. + order_by=None, + enabled_only=enabled_only, + ) + + # Sort only matched nodes. + if order_by: + nodes = order_by.sort(nodes, reverse=reverse) + elif reverse: + nodes.reverse() + + return nodes + + def parse_cmdline( + self, cmdline: str + ) -> Tuple[DTShCommand, List[str], Optional[str]]: + """Parse a command line. + + A command line is defined as a command string followed by optional + output redirection: + + COMMAND_LINE := COMMAND_STRING [REDIR2_STRING] + + Command strings are defined according to the Utility Syntax Guidelines, + and getopt syntax: + + COMMAND_STRING := COMMAND [OPTION]... [PARAM]... + + where: + + - CMD is a shell command name + - OPTIONs and PARAMs are not positional + + Parameter values can't start with ">". + + The output redirection string is parsed from the first lex + that starts with ">" and is not an option value: + + REDIR2_STRING := >|>> PATH + + The redirection operators (">" and ">>") have POSIX-like semantic + (i.e resp. "create" and "append"). + + The extension of the file the command's output is redirected to + determines its actual format (HTML, SVG, or simple text file). + + See: + + - DTShOption, DTShParameter + - https://docs.python.org/3.9/library/getopt.html + - https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html + + Args: + cmdline: The non empty command line to parse. + + Returns: + The parsed tuple (command, arguments, redirection), + where arguments include the parsed options and parameters. + + Raises: + DTShCommandNotFoundError: The first lex of the command line does + not represent a valid shell command. + """ + if not cmdline: + raise ValueError() + + v_cmdline: List[str] = [ + # If operating in POSIX mode, shlex will preserve the literal value + # of the next character that follows a non-quoted escape characters: + # e.g. input string \d is parsed as d: this would require + # RE patterns that contain character classes to be quoted, + # e.g find --with-label "led[\d]". + # If operating in non POSIX mode, escape characters are not + # recognized (thus \d stays \d), but neither are the + # quote characters within words, and a parsed command line argument + # may contain quotes. + # Operating in non POSIX mode, while striping leading and trailing + # quotes, seems to work fine for all our use cases. + lex.strip('"') + for lex in shlex.split(cmdline, posix=False) + ] + + cmd_name = v_cmdline[0] + try: + cmd = self._commands[cmd_name] + except KeyError as e: + raise DTShCommandNotFoundError(cmd_name) from e + + cmd_argc = 0 + expect_val = False + for lex in v_cmdline[1:]: + # The lex starts a command output redirection: + # - it's not a parameter (parameter values can't start with ">") + # - it's not an option name (would start with "-") + # - it's not an option value + if lex.startswith(">") and not expect_val: + break + if expect_val: + # Consume expected option value. + expect_val = False + else: + opt = cmd.option(lex) + if opt and isinstance(opt, DTShArg): + # Current lex is an option that expects a value. + expect_val = True + # Any lex before the redirection statement is a command argument + # (option or parameter). + cmd_argc += 1 + + cmd_argv = v_cmdline[1 : cmd_argc + 1] + + v_redir2 = v_cmdline[cmd_argc + 1 :] + if v_redir2: + redir2 = " ".join(v_redir2) + else: + redir2 = None + + return (cmd, cmd_argv, redir2) + + +class DTShError(Exception): + """Base for devicetree shell errors.""" + + _msg: Optional[str] + + def __init__(self, msg: Optional[str] = None) -> None: + """A shell error happened. + + Args: + msg: A message describing the error. + """ + self._msg = msg + + @property + def msg(self) -> str: + """A (may be empty) message describing the error.""" + return self._msg or "" + + +class DTShCommandError(DTShError): + """An exceptional condition happened while executing a shell command. + + This may indicate: + + - the command was misused or the user's simply asked for help + (see DTShUsageError) + - the command execution has failed + + """ + + _cmd: DTShCommand + + def __init__(self, cmd: DTShCommand, msg: Optional[str] = None) -> None: + """New error. + + Args: + cmd: The failed command. + msg: A message describing the error. + """ + super().__init__(msg) + self._cmd = cmd + + @property + def cmd(self) -> DTShCommand: + """The failed command.""" + return self._cmd + + +class DTShUsageError(DTShCommandError): + """An exceptional condition happened while parsing a command string. + + This may indicate: + + - the command string does not match a command's usage + - the user has asked for the command's usage by setting + the DTShFlagHelp flag on the command string + + For example: + + try: + sh.execute(...) + except DTShUsageError as e: + if e.cmd.with_flag(DTShFlagHelp): + # The User's asked for help. + else: + # The command string does not match the command's usage. + + """ + + +class DTPathNotFoundError(DTShError): + """A DT node or branch does not exist.""" + + _path: str + + def __init__(self, path: str) -> None: + """New error. + + Args: + path: The invalid path name. + """ + super().__init__(f"node not found: {path}") + self._path = path + + @property + def path(self) -> str: + """The invalid path name.""" + return self._path + + +class DTShCommandNotFoundError(DTShError): + """A shell command does not exist.""" + + def __init__(self, name: str) -> None: + """New error. + + Args: + name: The undefined command name. + """ + super().__init__(f"{name}: command not found") + self._name = name + + @property + def name(self) -> str: + """The undefined command name.""" + return self._name + + +class DTShFlagHelp(DTShFlag): + """Help flag, supported by all devicetree shell commands. + + When set, the command will display it's usage string. + """ - return DevicetreeShell(edt, sysinfo) + BRIEF: str = "print command help" + SHORTNAME: Optional[str] = "h" + LONGNAME: Optional[str] = "help" diff --git a/src/dtsh/shellutils.py b/src/dtsh/shellutils.py new file mode 100644 index 0000000..701a4e4 --- /dev/null +++ b/src/dtsh/shellutils.py @@ -0,0 +1,924 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Devicetree shell helpers. + +Flags, arguments and parameters for DTSh commands. + +Unit tests and examples: tests/test_dtsh_shellutils.py +""" + + +from typing import Any, Type, Optional, Sequence, Mapping, List + +import re + +from dtsh.model import DTNodeSorter, DTNodeCriterion +from dtsh.modelutils import ( + DTNodeSortByPathName, + DTNodeSortByNodeName, + DTNodeSortByUnitName, + DTNodeSortByUnitAddr, + DTNodeSortByDeviceLabel, + DTNodeSortByNodeLabel, + DTNodeSortByAlias, + DTNodeSortByCompatible, + DTNodeSortByBinding, + DTNodeSortByVendor, + DTNodeSortByBus, + DTNodeSortByOnBus, + DTNodeSortByDepOrdinal, + DTNodeSortByIrqNumber, + DTNodeSortByIrqPriority, + DTNodeSortByRegSize, + DTNodeSortByRegAddr, + DTNodeSortByBindingDepth, + # Text-based criteria. + DTNodeTextCriterion, + DTNodeWithPath, + DTNodeWithStatus, + DTNodeWithName, + DTNodeWithUnitName, + DTNodeWithCompatible, + DTNodeWithBinding, + DTNodeWithVendor, + DTNodeWithDeviceLabel, + DTNodeWithNodeLabel, + DTNodeWithAlias, + DTNodeWithChosen, + DTNodeAlsoKnownAs, + DTNodeWithBus, + DTNodeWithOnBus, + DTNodeWithDescription, + # Integer-based criteria. + DTNodeIntCriterion, + DTNodeWithUnitAddr, + DTNodeWithIrqPriority, + DTNodeWithIrqNumber, + DTNodeWithRegAddr, + DTNodeWithRegSize, + DTNodeWithBindingDepth, +) +from dtsh.rl import DTShReadline +from dtsh.autocomp import DTShAutocomp, RlStateEnum +from dtsh.shell import ( + DTSh, + DTShCommand, + DTShFlag, + DTShArg, + DTShParameter, + DTShError, + DTPathNotFoundError, + DTShCommandError, +) + + +class DTShFlagReverse(DTShFlag): + """Flag to reverse command output. + + If the command outputs a tree, reverse the children order when walking. + If the command outputs a list, reverse this list. + """ + + BRIEF = "reverse command output" + SHORTNAME = "r" + + +class DTShFlagEnabledOnly(DTShFlag): + """Flag to filter out disabled nodes or stop at disabled branches.""" + + BRIEF = "filter out disabled nodes or branches" + LONGNAME = "enabled-only" + + +class DTShFlagPager(DTShFlag): + """Flag to page command output.""" + + BRIEF = "page command output" + LONGNAME = "pager" + + +class DTShFlagRegex(DTShFlag): + """Flag to enforce patterns are interpreted as Regular Expressions. + + If unset, a text search (with wild-card substitution) is assumed. + """ + + BRIEF = "patterns are regular expressions" + SHORTNAME = "E" + + +class DTShFlagIgnoreCase(DTShFlag): + """Flag to ignore case in patterns. + + If unset, case-insensitive patterns are assumed. + """ + + BRIEF = "ignore case" + SHORTNAME = "i" + + +class DTShFlagCount(DTShFlag): + """Print matches count.""" + + BRIEF = "print matches count" + LONGNAME = "count" + + +class DTShFlagTreeLike(DTShFlag): + """Whether to list results in tree-like format.""" + + BRIEF = "list results in tree-like format" + SHORTNAME = "T" + + +class DTShFlagNoChildren(DTShFlag): + """Whether to list branches themselves, not their contents.""" + + BRIEF = "list nodes, not branch contents" + SHORTNAME = "d" + + +class DTShFlagRecursive(DTShFlag): + """Whether to list branches rescursively.""" + + BRIEF = "list recursively" + SHORTNAME = "R" + + +class DTShFlagLogicalOr(DTShFlag): + """Whether to match any criterion instead of all.""" + + BRIEF = "match any criterion instead of all" + LONGNAME = "OR" + + +class DTShFlagLogicalNot(DTShFlag): + """Whether to negate the criterion chain.""" + + BRIEF = "negate the criterion chain" + LONGNAME = "NOT" + + +class DTShArgFixedDepth(DTShArg): + """Argument that limits the depth when walking the devicetree. + + The default value is 0, which means unlimited depth. + """ + + BRIEF = "limit devicetree depth" + LONGNAME = "fixed-depth" + + # Argument state: depth parsed on the command line. + _depth: Optional[int] + + def __init__(self) -> None: + super().__init__(argname="depth") + + @property + def depth(self) -> int: + """The fixed depth value. + + Defaults to zero if unset. + """ + if self._depth is not None: + return self._depth + return 0 + + def reset(self) -> None: + """Reset this argument to its default value (zero). + + Overrides DTShOption.reset(). + """ + super().reset() + self._depth = None + + def parsed(self, value: Optional[str] = None) -> None: + """Overrides DTShOption.parsed().""" + super().parsed(value) + if self._raw: + try: + depth = int(self._raw or "0") + except ValueError as e: + raise DTShError( + f"expects an integer value (got '{self._raw}')" + ) from e + + if depth < 0: + raise DTShError(f"expects a non negative value (got {depth})") + + self._depth = depth + + +class DTShArgCriterion(DTShArg): + """Base for arguments that append a criterion to the chain.""" + + def get_criterion( # pylint: disable=useless-return + self, **kwargs: Any + ) -> Optional[DTNodeCriterion]: + """Get the criterion set by this argument. + + Args: + kwargs: Criterion-specific parameters as keyword arguments. + + Returns: + The criterion set by the command line, if any. + + Raises: + DTShError: Failed to initialize criterion, should eventually + come up as a command usage error. + """ + del kwargs + return None + + +class DTShArgIntCriterion(DTShArgCriterion): + """Base for arguments that append an integer-based (expression) criterion. + + Argument format: [OPERATOR] N [UNIT] + + OPERATOR: "<" | ">" | "<=" | ">=" | "=" | "!=" + N: the integer to compare with + UNIT (case-insensitive): "k" | "kb" | "m" | "mb" + """ + + # Match argument with . + _re: re.Pattern[str] = re.compile( + r"^(?P[<>=!]+)?[\s]*(?P[\da-fA-FxX]+)[\s]*(?P[kKbBmM]+)?$" + ) + + # Concrete criterion class. + _criter_cls: Type[DTNodeIntCriterion] + + # Parsed criterion instance. + _criterion: Optional[DTNodeIntCriterion] + + def __init__(self, criter_cls: Type[DTNodeIntCriterion]) -> None: + """Initialize criterion. + + Args: + criter_cls: The concrete type for this criterion. + """ + super().__init__(argname="expr") + self._criter_cls = criter_cls + + def parsed(self, value: Optional[str] = None) -> None: + """Overrides DTShArg.parsed().""" + super().parsed(value) + if not self._raw: + return + + if self._raw == "*": + # Match any integer value. + self._criterion = self._criter_cls(None, None) + else: + match = DTShArgIntCriterion._re.match(self._raw) + if match: + op_str = match.group("operator") + if op_str: + # Optional spaces after operator. + op_str = op_str.rstrip() + try: + criter_op = DTNodeIntCriterion.OPERATORS[op_str] + except KeyError as e: + raise DTShError(f"invalid operator: '{op_str}'") from e + else: + criter_op = None + + int_str = match.group("integer") + try: + criter_int = int(int_str, base=0) + except ValueError as e: + raise DTShError(f"not a number: '{int_str}'") from e + + unit_str = match.group("unit") + if unit_str: + unit_str = unit_str.lower() + if unit_str in ("k", "kb"): + criter_int *= 1024 + elif unit_str in ("m", "mb"): + criter_int *= 1024**2 + else: + raise DTShError(f"not an SI unit: '{unit_str}'") + + self._criterion = self._criter_cls(criter_op, criter_int) + else: + raise DTShError(f"invalid integer expression: '{self._raw}'") + + def reset(self) -> None: + """Reset this argument. + + Overrides DTShOption.reset(). + """ + super().reset() + self._criterion = None + + def get_criterion(self, **kwargs: Any) -> Optional[DTNodeCriterion]: + """Overrides DTShArgCriterion.get_criterion().""" + return self._criterion + + +class DTShArgNodeWithUnitAddr(DTShArgIntCriterion): + """Match unit address.""" + + BRIEF = "match unit addresse" + LONGNAME = "with-unit-addr" + + def __init__(self) -> None: + super().__init__(DTNodeWithUnitAddr) + + +class DTShArgNodeWithIrqNumber(DTShArgIntCriterion): + """Match IRQ number.""" + + BRIEF = "match IRQ numbers" + LONGNAME = "with-irq-number" + + def __init__(self) -> None: + super().__init__(DTNodeWithIrqNumber) + + +class DTShArgNodeWithIrqPriority(DTShArgIntCriterion): + """Match IRQ priority.""" + + BRIEF = "match IRQ priorities" + LONGNAME = "with-irq-priority" + + def __init__(self) -> None: + super().__init__(DTNodeWithIrqPriority) + + +class DTShArgNodeWithRegAddr(DTShArgIntCriterion): + """Match register address.""" + + BRIEF = "match register addresses" + LONGNAME = "with-reg-addr" + + def __init__(self) -> None: + super().__init__(DTNodeWithRegAddr) + + +class DTShArgNodeWithRegSize(DTShArgIntCriterion): + """Match register address.""" + + BRIEF = "match register sizes" + LONGNAME = "with-reg-size" + + def __init__(self) -> None: + super().__init__(DTNodeWithRegSize) + + +class DTShArgNodeWithBindingDepth(DTShArgIntCriterion): + """Match child-binding depth.""" + + BRIEF = "match child-binding depth" + LONGNAME = "with-binding-depth" + + def __init__(self) -> None: + super().__init__(DTNodeWithBindingDepth) + + +class DTShArgTextCriterion(DTShArgCriterion): + """Base for arguments that append a text-based (pattern) criterion. + + See DTNodeTextCriterion. + """ + + # Concrete criterion class. + _criter_cls: Type[DTNodeTextCriterion] + + def __init__(self, criter_cls: Type[DTNodeTextCriterion]) -> None: + """Initialize criterion. + + Args: + criter_cls: The concrete type for this criterion. + """ + super().__init__(argname="pattern") + self._criter_cls = criter_cls + + def get_criterion(self, **kwargs: Any) -> Optional[DTNodeCriterion]: + """Overrides DTShArgCriterion.get_criterion().""" + if self._raw: + try: + return self._criter_cls( + self._raw, + re_strict=kwargs.get("re_strict", False), + ignore_case=kwargs.get("ignore_case", False), + ) + except re.error as e: + raise DTShError(f"invalid RE '{self._raw}': {e.msg}") from e + return None + + +class DTShArgNodeWithPath(DTShArgTextCriterion): + """Match path name.""" + + BRIEF = "match path name" + LONGNAME = "with-path" + + def __init__(self) -> None: + super().__init__(DTNodeWithPath) + + +class DTShArgNodeWithStatus(DTShArgTextCriterion): + """Match status string.""" + + BRIEF = "match status string" + LONGNAME = "with-status" + + def __init__(self) -> None: + super().__init__(DTNodeWithStatus) + + +class DTShArgNodeWithName(DTShArgTextCriterion): + """Match unit name.""" + + BRIEF = "match node name" + LONGNAME = "with-name" + + def __init__(self) -> None: + super().__init__(DTNodeWithName) + + +class DTShArgNodeWithUnitName(DTShArgTextCriterion): + """Match unit name.""" + + BRIEF = "match unit name" + LONGNAME = "with-unit-name" + + def __init__(self) -> None: + super().__init__(DTNodeWithUnitName) + + +class DTShArgNodeWithCompatible(DTShArgTextCriterion): + """Match compatible strings.""" + + BRIEF = "match compatible strings" + LONGNAME = "with-compatible" + + def __init__(self) -> None: + super().__init__(DTNodeWithCompatible) + + def autocomp(self, txt: str, sh: DTSh) -> List[DTShReadline.CompleterState]: + """Auto-complete with compatible strings. + + Overrides DTShArg.autocomp(). + """ + return DTShAutocomp.complete_dtcompat(txt, sh) + + +class DTShArgNodeWithBinding(DTShArgTextCriterion): + """Match binding compatible or description.""" + + BRIEF = "match binding's compatible or headline" + LONGNAME = "with-binding" + + def __init__(self) -> None: + super().__init__(DTNodeWithBinding) + + def autocomp(self, txt: str, sh: DTSh) -> List[DTShReadline.CompleterState]: + """Auto-complete with compatible strings. + + Overrides DTShArg.autocomp(). + """ + return DTShAutocomp.complete_dtcompat(txt, sh) + + +class DTShArgNodeWithVendor(DTShArgTextCriterion): + """Match vendor prefix or name.""" + + BRIEF = "match vendor prefix or name" + LONGNAME = "with-vendor" + + def __init__(self) -> None: + super().__init__(DTNodeWithVendor) + + def autocomp(self, txt: str, sh: DTSh) -> List[DTShReadline.CompleterState]: + """Auto-complete with vendor prefixes. + + Overrides DTShArg.autocomp(). + """ + return DTShAutocomp.complete_dtvendor(txt, sh) + + +class DTShArgNodeWithDescription(DTShArgTextCriterion): + """Match description.""" + + BRIEF = "grep binding's description" + LONGNAME = "with-description" + + def __init__(self) -> None: + super().__init__(DTNodeWithDescription) + + +class DTShArgNodeWithBus(DTShArgTextCriterion): + """Match supported bus protocol.""" + + BRIEF = "match supported bus protocols" + LONGNAME = "with-bus" + + def __init__(self) -> None: + super().__init__(DTNodeWithBus) + + def autocomp(self, txt: str, sh: DTSh) -> List[DTShReadline.CompleterState]: + """Auto-complete with bus protocols. + + Overrides DTShArg.autocomp(). + """ + return DTShAutocomp.complete_dtbus(txt, sh) + + +class DTShArgNodeWithOnBus(DTShArgTextCriterion): + """Match bus of appearance.""" + + BRIEF = "match bus of appearance" + LONGNAME = "on-bus" + + def __init__(self) -> None: + super().__init__(DTNodeWithOnBus) + + def autocomp(self, txt: str, sh: DTSh) -> List[DTShReadline.CompleterState]: + """Auto-complete with bus protocols. + + Overrides DTShArg.autocomp(). + """ + return DTShAutocomp.complete_dtbus(txt, sh) + + +class DTShArgNodeWithDeviceLabel(DTShArgTextCriterion): + """Match device label.""" + + BRIEF = "match device label" + LONGNAME = "with-device-label" + + def __init__(self) -> None: + super().__init__(DTNodeWithDeviceLabel) + + +class DTShArgNodeWithNodeLabel(DTShArgTextCriterion): + """Match node labels.""" + + BRIEF = "match node labels" + LONGNAME = "with-label" + + def __init__(self) -> None: + super().__init__(DTNodeWithNodeLabel) + + def autocomp(self, txt: str, sh: DTSh) -> List[DTShReadline.CompleterState]: + """Auto-complete with node labels. + + Overrides DTShArg.autocomp(). + """ + if txt.startswith("&"): + # We're completing labels, and labels can't start with "&". + return [] + return DTShAutocomp.complete_dtlabel(txt, sh) + + +class DTShArgNodeWithAlias(DTShArgTextCriterion): + """Match nodes with device label.""" + + BRIEF = "match aliases" + LONGNAME = "with-alias" + + def __init__(self) -> None: + super().__init__(DTNodeWithAlias) + + def autocomp(self, txt: str, sh: DTSh) -> List[DTShReadline.CompleterState]: + """Auto-complete with alias names. + + Overrides DTShArg.autocomp(). + """ + return DTShAutocomp.complete_dtalias(txt, sh) + + +class DTShArgNodeWithChosen(DTShArgTextCriterion): + """Match chosen nodes.""" + + BRIEF = "match chosen nodes" + LONGNAME = "chosen-for" + + def __init__(self) -> None: + super().__init__(DTNodeWithChosen) + + def autocomp(self, txt: str, sh: DTSh) -> List[DTShReadline.CompleterState]: + """Auto-complete with parameter names. + + Overrides DTShArg.autocomp(). + """ + return DTShAutocomp.complete_dtchosen(txt, sh) + + +class DTShArgNodeAlsoKnownAs(DTShArgTextCriterion): + """Match labels and aliases.""" + + BRIEF = "match labels or aliases" + LONGNAME = "also-known-as" + + def __init__(self) -> None: + super().__init__(DTNodeAlsoKnownAs) + + +DTSH_ARG_NODE_CRITERIA: Sequence[DTShArgCriterion] = [ + # Text-based criteria. + DTShArgNodeWithPath(), + DTShArgNodeWithStatus(), + DTShArgNodeWithName(), + DTShArgNodeWithUnitName(), + DTShArgNodeWithCompatible(), + DTShArgNodeWithBinding(), + DTShArgNodeWithVendor(), + DTShArgNodeWithDescription(), + DTShArgNodeWithBus(), + DTShArgNodeWithOnBus(), + DTShArgNodeWithDeviceLabel(), + DTShArgNodeWithNodeLabel(), + DTShArgNodeWithAlias(), + DTShArgNodeWithChosen(), + DTShArgNodeAlsoKnownAs(), + # Integer-based criteria. + DTShArgNodeWithUnitAddr(), + DTShArgNodeWithIrqNumber(), + DTShArgNodeWithIrqPriority(), + DTShArgNodeWithRegAddr(), + DTShArgNodeWithRegSize(), + DTShArgNodeWithBindingDepth(), +] +"""Pre-defined criteria shell commands may use to match nodes.""" + + +class DTShNodeOrderBy: + """Node sorter definition to be used by shell commands. + + Associate user friendly meta-data to sorter implementations. + """ + + _key: str + # Order by what (human readable form) ? + _what: str + + # The sorter implementation. + _sorter: DTNodeSorter + + def __init__(self, key: str, by_what: str, sorter: DTNodeSorter) -> None: + """Initialize sorted definition. + + Args: + key: A single character key to reference the sorter with. + by_what: A human readable name for the node aspect this sorter + relies on. + sorter: The corresponding sorter implementation. + """ + self._key = key + self._what = by_what + self._sorter = sorter + + @property + def key(self) -> str: + """A single character key to reference the sorter with.""" + return self._key + + @property + def brief(self) -> str: + """A brief description of the sorter.""" + return f"sort by {self._what}" + + @property + def sorter(self) -> DTNodeSorter: + """The sorter implementation.""" + return self._sorter + + def __eq__(self, other: object) -> bool: + if isinstance(other, DTShNodeOrderBy): + return self._key == other._key + return False + + def __lt__(self, other: object) -> bool: + if isinstance(other, DTShNodeOrderBy): + return self._key < other._key + raise TypeError(other) + + def __hash__(self) -> int: + return hash(self._key) + + +DTSH_NODE_ORDER_BY: Mapping[str, DTShNodeOrderBy] = { + order_by.key: order_by + for order_by in [ + DTShNodeOrderBy("p", "node path", DTNodeSortByPathName()), + DTShNodeOrderBy("N", "node name", DTNodeSortByNodeName()), + DTShNodeOrderBy("n", "unit name", DTNodeSortByUnitName()), + DTShNodeOrderBy("a", "unit address", DTNodeSortByUnitAddr()), + DTShNodeOrderBy("c", "compatible strings", DTNodeSortByCompatible()), + DTShNodeOrderBy("C", "binding", DTNodeSortByBinding()), + DTShNodeOrderBy("v", "vendor name", DTNodeSortByVendor()), + DTShNodeOrderBy("l", "device label", DTNodeSortByDeviceLabel()), + DTShNodeOrderBy("L", "node labels", DTNodeSortByNodeLabel()), + DTShNodeOrderBy("A", "aliases", DTNodeSortByAlias()), + DTShNodeOrderBy("B", "supported bus protocols", DTNodeSortByBus()), + DTShNodeOrderBy("b", "bus of appearance", DTNodeSortByOnBus()), + DTShNodeOrderBy("o", "dependency ordinal", DTNodeSortByDepOrdinal()), + DTShNodeOrderBy("i", "IRQ numbers", DTNodeSortByIrqNumber()), + DTShNodeOrderBy("I", "IRQ priorities", DTNodeSortByIrqPriority()), + DTShNodeOrderBy("r", "register addresses", DTNodeSortByRegAddr()), + DTShNodeOrderBy("s", "register sizes", DTNodeSortByRegSize()), + DTShNodeOrderBy("X", "child-binding depth", DTNodeSortByBindingDepth()), + ] +} +"""Pre-defined order relationships shell commands may use to sort nodes.""" + + +class DTShArgOrderBy(DTShArg): + """Argument to set the nodes sorter. + + The relationship is selected with a key specifier. + """ + + BRIEF = "sort nodes or branches" + LONGNAME = "order-by" + + # Argument state: sorter implementation, no default value. + _sorter: Optional[DTNodeSorter] + + def __init__(self) -> None: + super().__init__(argname="key") + + def reset(self) -> None: + """Overrides DTShOption.reset().""" + super().reset() + self._sorter = None + + def parsed(self, value: Optional[str] = None) -> None: + """Overrides DTShOption.parsed().""" + super().parsed(value) + if self._raw: + try: + self._sorter = DTSH_NODE_ORDER_BY[self._raw].sorter + except KeyError as e: + raise DTShError(f"invalid sort key: '{self._raw}'") from e + + @property + def sorter(self) -> Optional[DTNodeSorter]: + """The sorter argument parsed on the command line.""" + return self._sorter + + def autocomp(self, txt: str, sh: DTSh) -> List[DTShReadline.CompleterState]: + """Auto-complete with the predefined sorter definitions. + + Overrides DTShArg.autocomp(). + """ + return sorted( + [ + RlStateEnum(key, order_by.brief) + for key, order_by in DTSH_NODE_ORDER_BY.items() + if key.startswith(txt) + ], + key=lambda x: x.rlstr.lower(), + ) + + +class DTShParamDTPath(DTShParameter): + """Devicetree path parameter. + + This parameter accepts an optional single value, + and is not intended to support path expansion (globbing). + """ + + def __init__(self) -> None: + super().__init__( + name="path", + multiplicity="?", + brief="devicetree path", + ) + + @property + def path(self) -> str: + """The path parameter value set by the command line. + + If unset, will answer an empty string, + the semantic of which will depend on the + supporting command. + """ + return self._raw[0] if self._raw else "" + + def autocomp(self, txt: str, sh: DTSh) -> List[DTShReadline.CompleterState]: + """Auto-complete with Devicetree paths. + + Overrides DTShParameter.autocomp(). + """ + return DTShAutocomp.complete_dtpath(txt, sh) + + +class DTShParamDTPaths(DTShParameter): + """Devicetree paths parameter. + + This parameter accepts any number of values, + each representing a path expression that may expand (globbing) + to several Devicetree paths. + """ + + def __init__(self) -> None: + super().__init__( + name="path", + multiplicity="*", + brief="devicetree path", + ) + + @property + def paths(self) -> Sequence[str]: + """The path parameter values set by the command line. + + If unset, will answer an single empty value, + representing the current working branch. + """ + return self._raw if self._raw else [""] + + def expand(self, cmd: DTShCommand, sh: DTSh) -> List[DTSh.PathExpansion]: + """Expand this parameter. + + Args: + cmd: The executing command. + sh: The context shell. + + Returns: + A list containing one path expansion per parameter value. + For convenience, nodes are filtered if the command supports + the "enabled only" flag. + + Raises: + DTShCommandError: Path expansion failed (node not found). + """ + enabled_only: bool = False + try: + enabled_only = cmd.with_flag(DTShFlagEnabledOnly) + except KeyError: + # Unsupported option, no filter. + enabled_only = False + + try: + return [ + sh.path_expansion(path, enabled_only) + for path in self._raw or [""] + ] + except DTPathNotFoundError as e: + raise DTShCommandError(cmd, e.msg) from e + + def autocomp(self, txt: str, sh: DTSh) -> List[DTShReadline.CompleterState]: + """Auto-complete with Devicetree paths. + + Overrides DTShParameter.autocomp(). + """ + return DTShAutocomp.complete_dtpath(txt, sh) + + +class DTShParamAlias(DTShParameter): + """Alias name parameter.""" + + def __init__(self) -> None: + super().__init__( + name="name", + multiplicity="?", + brief="alias name", + ) + + @property + def alias(self) -> str: + """The parameter value set by the command line. + + If unset, will answer an empty string means (any/all aliased nodes). + """ + return self._raw[0] if self._raw else "" + + def autocomp(self, txt: str, sh: DTSh) -> List[DTShReadline.CompleterState]: + """Auto-complete parameter with alias names. + + Overrides DTShParameter.autocomp(). + """ + return DTShAutocomp.complete_dtalias(txt, sh) + + +class DTShParamChosen(DTShParameter): + """Chosen name parameter.""" + + def __init__(self) -> None: + super().__init__( + name="name", + multiplicity="?", + brief="chosen name", + ) + + @property + def chosen(self) -> str: + """The parameter value set by the command line. + + If unset, will answer an empty string means (any/all aliased nodes). + """ + return self._raw[0] if self._raw else "" + + def autocomp(self, txt: str, sh: DTSh) -> List[DTShReadline.CompleterState]: + """Auto-complete argument value with chosen names. + + Overrides DTShParameter.autocomp(). + """ + return DTShAutocomp.complete_dtchosen(txt, sh) diff --git a/src/dtsh/systools.py b/src/dtsh/systools.py deleted file mode 100644 index 416a4cf..0000000 --- a/src/dtsh/systools.py +++ /dev/null @@ -1,286 +0,0 @@ -# Copyright (c) 2022 Chris Duf -# -# SPDX-License-Identifier: Apache-2.0 - -"""Host system tools helpers.""" - - -from typing import Any, Dict, List, Union - -import os -import re -import sys -import yaml -from pathlib import Path -from subprocess import Popen, PIPE - -from devicetree.edtlib import Loader as edtlib_YamlLoader - - -class CMakeCache(object): - """Access CMake cached variables. - """ - - # CMake cached variables name to value. - _cache: Dict[str,str] - - def __init__(self, build_dir: str) -> None: - """Initialize the CMake helper with a build directory content. - - Will silently fail with an empty cache if the CMake binary is not found, - or the build directory is invalid. - - Argument: - build_dir -- path to a valid CMake build directory - """ - self._cache = {} - try: - argv = [ - 'cmake.exe' if os.name == 'nt' else 'cmake', - # List non-advanced cached variables. - '-L', - # Only load the cache. Do not actually run configure and generate steps. - '-N', - '-B', build_dir - ] - cmake = Popen(argv, stdout=PIPE, stderr=PIPE) - stdout, stderr = cmake.communicate() - if cmake.returncode == 0: - self._init_cache(str(stdout, 'utf-8')) - else: - # Dump CMake error. - print(stderr, file=sys.stderr) - except Exception: - # Silently fail (cmake is probably unavailable). - pass - - def get(self, name: str) -> Union[str, None]: - """Access CMake cached variables. - - Arguments: - name -- the variable name, e.g. APPLICATION_SOURCE_DIR - - Returns the variable value or None. - """ - return self._cache.get(name) - - def _init_cache(self, cmake_stdout: str) -> None: - regex = re.compile(r'^(\w+):(\w+)=(\S+)$') - for line in cmake_stdout.splitlines(): - m = regex.match(line) - if m and (len(m.groups()) == 3): - self._cache[m.groups()[0]] = m.groups()[2] - - -class Git(object): - """Git helper. - """ - - def __init__(self) -> None: - """Initialize helper for host operating system. - """ - self._git = 'git.exe' if os.name == 'nt' else 'git' - - def get_head_commit(self, repo_path: str) -> Union[str, None]: - """Returns git -C $ZEPHYR_BASE log -n 1 --pretty=format:"%h", or None. - """ - rev = None - try: - argv = [ - self._git, - '-C', f'{repo_path}', - 'log', - '-n', '1', - '--pretty=format:%h' - ] - git = Popen(argv, stdout=PIPE, stderr=PIPE) - stdout, stderr = git.communicate() - if git.returncode == 0: - rev = str(stdout, 'utf-8').strip() - else: - # Dump git error. - print(stderr, file=sys.stderr) - except Exception: - # Silently fail (git is probably unavailable). - pass - return rev - - def get_head_tags(self, repo_path: str) -> List[str]: - """Returns git tag --points-at HEAD, or None. - """ - tags: List[str] = [] - try: - argv = [ - self._git, - '-C', f'{repo_path}', - 'tag', - '--points-at', 'HEAD', - ] - git = Popen(argv, stdout=PIPE, stderr=PIPE) - stdout, stderr = git.communicate() - if git.returncode == 0: - for tag in str(stdout, 'utf-8').splitlines(): - tags.append(tag.strip()) - else: - # Dump git error. - print(stderr, file=sys.stderr) - except Exception: - # Silently fail (git is probably unavailable). - pass - return tags - - -class GCCArm(object): - """GCC Arm Embedded Toolchain helper. - """ - - # Resolved path to arm-none-eabi-gcc. - _gcc: Union[str, None] - - # Toolchain, e.g.: GNU Arm Embedded Toolchain 10.3-2021.10 - _toolchain: Union[str, None] - - # GCC version. - _version: Union[str, None] - - # Build date, e.g. 20210824 - _build_date: Union[str, None] - - def __init__(self, gnuarm_dir: str) -> None: - """Initialize helper for host operating system. - """ - self._gcc = None - self._toolchain = None - self._version = None - self._build_date = None - - gnuarm_path = Path(os.path.join(gnuarm_dir, 'bin')).resolve() - if os.path.isdir(gnuarm_path): - gcc_name = 'arm-none-eabi-gcc.exe' if os.name == "nt" else 'arm-none-eabi-gcc' - gcc_path = Path(os.path.join(gnuarm_path, gcc_name)).resolve() - self._gcc = str(gcc_path) - try: - argv = [self._gcc, '--version'] - gcc = Popen(argv, stdout=PIPE, stderr=PIPE) - stdout, stderr = gcc.communicate() - if gcc.returncode == 0: - self._init_version(str(stdout, 'utf-8')) - else: - # Dump gcc error. - print(stderr, file=sys.stderr) - except Exception: - # Silently fail (gcc is probably unavailable). - pass - - @property - def toolchain(self) -> Union[str, None]: - return self._toolchain - - @property - def version(self) -> Union[str, None]: - return self._version - - @property - def build_date(self) -> Union[str, None]: - return self._build_date - - def _init_version(self, cmake_stdout: str) -> None: - # GCC Arm 10: - # arm-none-eabi-gcc (GNU Arm Embedded Toolchain 10.3-2021.10) 10.3.1 20210824 (release) - # - # GCC Arm 11: - # arm-none-eabi-gcc (GNU Toolchain for the Arm Architecture 11.2-2022.02 (arm-11.14)) 11.2.1 20220111 - regex = re.compile(r'^[\w.\-]+\s([\w .\-()]+)\s([\d.]+)\s(\d+)( [\w()]+)?$') - for line in cmake_stdout.splitlines(): - m = regex.match(line.strip()) - if m: - self._toolchain = m.groups()[0] - self._version = m.groups()[1] - self._build_date = m.groups()[2] - - -class GitHub(object): - """GitHub helper. - """ - - def __init__(self, - user: str = "zephyrproject-rtos", - project: str = "zephyr") -> None: - """Initialize helper. - - Arguments: - user -- GitHub user, defaults to "zephyr-rtos" - project - GitHub project, defaults to "zephyr" - """ - self._user = user - self._project = project - - @property - def home(self) -> str: - """Home URL. - """ - return f"https://github.com/{self._user}/{self._project}" - - def get_tag(self, tag: str): - """Returns a tag URL. - """ - return f"{self.home}/releases/tag/{tag}" - - def get_commit(self, commit: str): - """Returns a commit URL. - """ - return f"{self.home}/commit/{commit}" - - -class YamlFile(object): - """YAML binding file. - """ - - _yaml: Union[Any, None] - _content: Union[str, None] - - def __init__(self, path: str): - """ - """ - yaml_file = None - try: - yaml_file = open(path, mode='r', encoding="utf-8") - self._content = yaml_file.read().strip() - self._yaml = yaml.load(self._content, edtlib_YamlLoader) - except IOError: - self._content = None - self._yaml = None - finally: - if yaml_file: - yaml_file.close() - - @property - def content(self) -> Union[str, None]: - """Returns the YAML file content, or None. - """ - return self._content - - @property - def include(self) -> List[str]: - """Returns the list of included YAML files. - """ - inc_names: List[str] = [] - if self._yaml: - yaml_include = self._yaml.get('include') - if isinstance(yaml_include, str): - inc_names.append(yaml_include) - elif isinstance(yaml_include, list): - for name in yaml_include: - inc_names.append(name) - return inc_names - - def get(self, key: str) -> Union[Any, None]: - """Access YAML content by key. - - Argument: - key -- the YAML key - Returns the mapped content value, or None. - """ - if self._yaml: - return self._yaml.get(key) - return None diff --git a/src/dtsh/term.py b/src/dtsh/term.py deleted file mode 100644 index b351d5e..0000000 --- a/src/dtsh/term.py +++ /dev/null @@ -1,111 +0,0 @@ -# Copyright (c) 2022 Chris Duf -# -# SPDX-License-Identifier: Apache-2.0 - -"""Rich terminal for devicetree shells.""" - -from typing import Union - - -from rich.console import Console, PagerContext - -from dtsh.rl import readline -from dtsh.dtsh import DtshVt -from dtsh.tui import DtshTui - - -class DevicetreeTerm(DtshVt): - """Rich terminal for devicetree shells. - - Rich standard output and pager support with the Python rich library, - standard input with GNU readline completion. - """ - - _console: Console - _pager: Union[PagerContext, None] - - def __init__(self, - readline_comp_hook = None, - readline_display_hook = None) -> None: - """Initialize a rich terminal. - - Creates a rich console and setup GNU readline completion support. - - Arguments: - readline_comp_hook -- GNU readline completions hook or None - readline_display_hook -- GNU readline display hook or None - """ - # We do not want Console syntax highlighting by default. - self._console = Console(highlight=False, theme=DtshTui.theme()) - self._pager = None - - if readline_comp_hook is not None: - # Setup readline autocomp support. - readline.set_completer(readline_comp_hook) - # We want to treat '/' as part of a word - readline.set_completer_delims(' \t\n') - if readline_display_hook is not None: - readline.set_completion_display_matches_hook(readline_display_hook) - readline.parse_and_bind("tab: complete") - - def write(self, *args, **kwargs) -> None: - """Overrides DtshVt.write() - - Print to rich console. - - See: - - https://rich.readthedocs.io/en/stable/reference/console.html#rich.console.Console.print - - Arguments: - args -- Positional arguments for Console.print() - kwargs -- Keyword arguments for Console.print() - """ - self._console.print(*args, **kwargs) - - def pager_enter(self) -> None: - """Overrides DtshVt.pager_enter(). - """ - if self._pager is None: - self._console.clear() - self._pager = self._console.pager(styles=True, links=True) - self._pager.__enter__() - - def pager_exit(self) -> None: - """Overrides DtshVt.pager_exit(). - """ - if self._pager is not None: - self._pager.__exit__(None, None, None) - self._pager = None - - def clear(self) -> None: - """Overrides DtshVt.clear(). - """ - self._console.clear() - - def readline(self, ansi_prompt: str) -> str: - """Overrides DtshVt.readline(). - - Arguments: - ansi_prompt -- raw ANSI prompt (with ANSI codes) - - See: - - https://en.wikipedia.org/w/index.php?title=ANSI_escape_code - """ - # Using rich.Console.input() with GNU readline enabled would 'eat' (remove) - # the command prompt when navigating commands history. - # - # See: - # - "Backspacing deletes the prompt in console.input using a custom theme" - # (https://github.com/Textualize/rich/issues/299) - # - "Backspacing deletes the prompt in console.input using readline" - # (https://github.com/Textualize/rich/issues/2293) - # - https://wiki.hackzine.org/development/misc/readline-color-prompt.html - # - # Will block till ENTER or EOF. - cmdline = input(ansi_prompt) - return cmdline.strip() - - def abort(self) -> None: - """Overrides DtshVt.abort(). - """ - self._console.print() diff --git a/src/dtsh/theme b/src/dtsh/theme deleted file mode 100644 index fd61b6d..0000000 --- a/src/dtsh/theme +++ /dev/null @@ -1,121 +0,0 @@ -# Copyright (c) 2022 Chris Duf -# -# SPDX-License-Identifier: Apache-2.0 - -# Devicetree shell default theme -# -# See: -# - Styles: https://rich.readthedocs.io/en/stable/style.html#styles -# - Standard colors: https://rich.readthedocs.io/en/stable/appendix/colors.html -# - Theme: https://rich.readthedocs.io/en/stable/style.html#style-themes - -[styles] - -# Default style. -dtsh.default = default on default - -# Node path anchor (1st and last segments). -# Color: DeepSkyBlue3 -dtsh.path.anchor = #00afff bold -# Node path segments. -# Color: DeepSkyBlue1 -dtsh.path.segment = #0087af - -# Apply to a node's (unique) matching compatible (aka binding). -# Typically: node.matching_compat is defined, -# and node.binding_path might point to the corresponding yaml file. -# -dtsh.binding = light_sea_green - -# Apply to compatible strings as in node.compats. -# -dtsh.compats = light_sea_green - -# Apply to descriptions from DT bindings (nodes or properties). -# -dtsh.desc = medium_orchid3 - -# Apply to a node's label property. -# -dtsh.labels = deep_sky_blue1 italic - -# Apply to a node's labels. -# -dtsh.label = deep_sky_blue2 - -# Apply to a bus device. -# -dtsh.bus = cadet_blue bold - -# Indicates a node is on a bus. -# -dtsh.on_bus = cadet_blue - -# Apply to a node interrupt numbers and names. -# -dtsh.irq = yellow3 - -# Apply to a node's aliases. -# -dtsh.alias = turquoise2 - -# Apply to a property names. -# -dtsh.property = dark_sea_green - -# Apply to included YAML files that are not DT compatibles. -dtsh.include = dark_khaki - -# Apply to Zephyr commits (not release tags), -# aka somewhat unstable versions. -dtsh.commit = dark_goldenrod - -# Default for Zephyr information, e.g. version number -dtsh.zephyr = dodger_blue1 - -# Default for DTS file name or link. -dtsh.dts = medium_purple - -# Default for GCC Arm Embedded information, e.g. version number -dtsh.gnuarmemb = gold3 - -# Apply to board name -dtsh.board = light_sea_green - -# Default for file basenames. -dtsh.basename = wheat4 - -# Apply to node status 'okay'. -# Green example: spring_green3 -# -dtsh.okay = default - -# Apply to node status not 'okay'. -# -dtsh.not_okay = dim - -# Apply when the required data to show, -# e.g. a structured view section, -# is not available -dtsh.apology = dim italic - -# Apply to boolean values. -# -dtsh.true = default -dtsh.false = dim - -# dtsh warning messages -dtsh.warning = dark_orange - -[dtsh] - -# Prompt colors. -# See https://en.wikipedia.org/w/index.php?title=ANSI_escape_code#Colors -# -dtsh.prompt.color = 88 -dtsh.prompt.color.error = 99 -dtsh.prompt.wchar = ❯ - -# Bullet default -# Example: -, ✓, • -dtsh.bullet.wchar = • diff --git a/src/dtsh/tui.py b/src/dtsh/tui.py deleted file mode 100644 index f8fe3d6..0000000 --- a/src/dtsh/tui.py +++ /dev/null @@ -1,1814 +0,0 @@ -# Copyright (c) 2022 Chris Duf -# -# SPDX-License-Identifier: Apache-2.0 - -"""Devicetree shell UI components.""" - - -from abc import abstractmethod -from typing import ClassVar, Optional, Union, List, Dict - -import configparser -import os -import pathlib -import yaml - -from devicetree.edtlib import ControllerAndData, Loader as edtlib_YamlLoader -from devicetree.edtlib import Node, Binding, Property, PropertySpec, Register - -from rich import box -from rich.console import RenderableType -from rich.padding import Padding -from rich.style import Style, StyleType -from rich.syntax import Syntax -from rich.table import Table -from rich.text import Text -from rich.theme import Theme -from rich.tree import Tree - -from dtsh.dtsh import Dtsh, DtshCommand, DtshCommandOption, DtshVt, DtshError -from dtsh.config import DtshConfig - - -class DtshTui: - - # DTSH theme. - _theme: ClassVar[Union[Theme, None]] = None - - # Common UTF-8 symbols. - # - WCHAR_ELLIPSIS = '\u2026' - WCHAR_COPYRIGHT = '\u00a9' - WCHAR_HYPHEN = '\u2014' - WCHAR_DASH = '\ufe4d' - WCHAR_ARROW = '\u2192' - WCHAR_BULLET = '-' - - # Base styles. - # - STYLE_DEFAULT = 'dtsh.default' - STYLE_BOLD = 'bold' - STYLE_DIM = 'dim' - STYLE_ITALIC = 'italic' - STYLE_UNDERLINE = 'underline' - STYLE_STRIKE = 'strike' - STYLE_APOLOGY = 'dtsh.apology' - STYLE_TRUE = 'dtsh.true' - STYLE_FALSE = 'dtsh.false' - - # Devicetree styles. - # - STYLE_DT_BINDING = 'dtsh.binding' - STYLE_DT_COMPATS = 'dtsh.compats' - STYLE_DT_LABEL = 'dtsh.label' - STYLE_DT_LABELS = 'dtsh.labels' - STYLE_DT_ALIAS = 'dtsh.alias' - STYLE_DT_PROPERTY = 'dtsh.property' - STYLE_DT_DESC = 'dtsh.desc' - STYLE_DT_BUS = 'dtsh.bus' - STYLE_DT_ON_BUS = 'dtsh.on_bus' - STYLE_DT_IRQ = 'dtsh.irq' - STYLE_DT_OKAY = 'dtsh.okay' - STYLE_DT_NOT_OKAY = 'dtsh.not_okay' - STYLE_DT_INCLUDE = 'dtsh.include' - - # Prompt (may be overidden by dtsh.prompt configuration) - # - PROMPT_WCHAR = '\u276f' - PROMPT_COLOR = 88 - PROMPT_COLOR_ERROR = 99 - PROMPT_SPARSE = True - - @staticmethod - def mk_ansi_prompt(has_error: bool = False) -> str: - """Create an ANSI 255 colors compatible prompt. - - Arguments: - has_error -- True if last command execution has failed. - - Returns a prompt compatible with ANSI 255 colors terminals. - """ - # Using ANSI escape codes in input() breaks the GNU readline cursor. - # - # The hand-made prompt bellow uses the RL_PROMPT_{START,STOP}_IGNORE markers - # to keep the readline state consistent. - # - # := m - # := ESC[ - # := \x1b[ - # - # := '\001' - # := '\002' - # - # See: - # - https://en.wikipedia.org/w/index.php?title=ANSI_escape_code - # - https://wiki.hackzine.org/development/misc/readline-color-prompt.html - # - https://en.wikipedia.org/w/index.php?title=ANSI_escape_code#Colors - # - # We assume terminal has at least 255 colors. - if has_error: - sgr_color = f'38;5;{DtshTui.PROMPT_COLOR}' - else: - sgr_color = f'38;5;{DtshTui.PROMPT_COLOR_ERROR}' - return f'\001\x1b[{sgr_color};1m\002{DtshTui.PROMPT_WCHAR}\001\x1b[0m\002 ' - - @staticmethod - def theme() -> Theme: - if DtshTui._theme is None: - DtshTui._theme = DtshTui._load_theme() - return DtshTui._theme - - @staticmethod - def style(name: str) -> Style: - style = DtshTui.theme().styles.get(name) - if not style: - style = Style() - return style - - @staticmethod - def style_default() -> Style: - return DtshTui.theme().styles[DtshTui.STYLE_DEFAULT] - - @staticmethod - def style_bold() -> Style: - return DtshTui.theme().styles[DtshTui.STYLE_BOLD] - - @staticmethod - def style_dim() -> Style: - return DtshTui.theme().styles[DtshTui.STYLE_DIM] - - @staticmethod - def style_italic() -> Style: - return DtshTui.theme().styles[DtshTui.STYLE_ITALIC] - - @staticmethod - def style_underline() -> Style: - return DtshTui.theme().styles[DtshTui.STYLE_UNDERLINE] - - @staticmethod - def style_strike() -> Style: - return DtshTui.theme().styles[DtshTui.STYLE_STRIKE] - - @staticmethod - def style_apology() -> Style: - return DtshTui.theme().styles[DtshTui.STYLE_APOLOGY] - - ############################################################################ - # Utils. - ############################################################################ - - @staticmethod - def get_node_nick(node: Node) -> str: - """Returns the node's name with the unit address part striped. - """ - if node.unit_addr is not None: - return node.name[0:node.name.rfind('@')] - return node.name - - @staticmethod - def get_text_summary(txt: str) -> str: - lines = txt.strip().split('\n') - str_short = lines[0] - if len(lines) > 1: - if str_short.endswith('.'): - str_short = str_short[:-1] - str_short += DtshTui.WCHAR_ELLIPSIS - return str_short - - ############################################################################ - # Text - ############################################################################ - - @staticmethod - def mk_txt(txt: str, style=None) -> Text: - if not style: - style = DtshTui.style_default() - return Text(txt, style) - - @staticmethod - def mk_txt_bold(txt: str) -> Text: - return Text(txt, DtshTui.style_bold()) - - @staticmethod - def mk_txt_italic(txt: str) -> Text: - return Text(txt, DtshTui.style_italic()) - - @staticmethod - def mk_txt_dim(txt: str) -> Text: - return Text(txt, DtshTui.style_dim()) - - @staticmethod - def mk_txt_bool(is_true: bool, - true_str: str = 'Yes', - false_str: str = 'No',) -> Text: - if is_true: - val_str = true_str - style = DtshTui.style(DtshTui.STYLE_TRUE) - else: - val_str = false_str - style = DtshTui.style(DtshTui.STYLE_FALSE) - return Text(val_str, style) - - @staticmethod - def mk_txt_warn(msg: str) -> Text: - return Text(msg, style='dtsh.warning') - - @staticmethod - def mk_txt_desc(desc: Optional[str]) -> Text: - if not desc: - return Text("No description available.", - DtshTui.style(DtshTui.STYLE_APOLOGY)) - return Text(desc.strip(), DtshTui.style(DtshTui.STYLE_DT_DESC)) - - @staticmethod - def mk_txt_desc_short(desc: Optional[str]) -> Text: - if not desc: - return Text() - desc_lines = desc.strip().split('\n') - desc_short = desc_lines[0] - if len(desc_lines) > 1: - if desc_short.endswith('.'): - desc_short = desc_short[:-1] - desc_short += DtshTui.WCHAR_ELLIPSIS - return Text(desc_short, DtshTui.style(DtshTui.STYLE_DT_DESC)) - - @staticmethod - def mk_txt_link(label: str, - url: str, - style: Optional[StyleType] = None) -> Text: - """Returns a text link. - - Arguments: - label - the link label - link - the link URL, e.g. https://docs.zephyrproject.org/ - style - the label's style - """ - txt = Text(label, style=style or 'default') - txt.stylize(Style(link=f'{url}')) - return txt - - @staticmethod - def txt_update_link_file(txt: Text, path: str) -> None: - uri = pathlib.Path(path).as_uri() - txt.stylize(Style(link=uri)) - - @staticmethod - def txt_dim(txt: Text) -> None: - if isinstance(txt.style, Style): - style = txt.style.without_color - else: - style = Style.parse(txt.style).without_color - # Note: Style.combine([DtshTui.style('dim')]) would also work - style += DtshTui.style('dim') - txt.style = style - - @staticmethod - def mk_txt_node_status(node: Node) -> Text: - if node.status == 'okay': - style = DtshTui.style(DtshTui.STYLE_DT_OKAY) - else: - style = DtshTui.style(DtshTui.STYLE_DT_NOT_OKAY) - return Text(node.status, style) - - @staticmethod - def mk_txt_node_nick(node: Node, with_status: bool = False) -> Text: - txt = Text(DtshTui.get_node_nick(node)) - if with_status and (node.status != 'okay'): - DtshTui.txt_dim(txt) - return txt - - @staticmethod - def mk_txt_node_name(node: Node, with_status: bool = False) -> Text: - txt = Text(node.name) - if with_status and (node.status != 'okay'): - DtshTui.txt_dim(txt) - return txt - - @staticmethod - def mk_txt_node_bus_device(node: Node, with_status: bool = False) -> Text: - txt = Text() - if node.buses: - buses: List[str] = node.buses - txt = txt.append_text( - DtshTui.mk_txt(' '.join(buses), - DtshTui.style(DtshTui.STYLE_DT_BUS)) - ) - if node.on_buses: - buses: List[str] = node.on_buses - prefix = "on " - if len(txt.plain) > 0: - prefix = " " + prefix - txt = txt.append_text(DtshTui.mk_txt(prefix)) - txt = txt.append_text( - DtshTui.mk_txt(' '.join(buses), - DtshTui.style(DtshTui.STYLE_DT_ON_BUS)) - ) - if (len(txt.plain) > 0) and with_status and (node.status != 'okay'): - DtshTui.txt_dim(txt) - return txt - - @staticmethod - def mk_txt_node_register(node: Node, - with_status: bool = False) -> Text: - for reg in node.regs: - if (reg.addr is not None) and (reg.addr == node.unit_addr): - if reg.size is not None: - txt = Text(f"<{hex(reg.addr)} {hex(reg.size)}>") - else: - txt = Text(f"<{hex(reg.addr)}>") - if with_status and not Dtsh.is_node_enabled(reg.node): - DtshTui.txt_dim(txt) - return txt - return Text() - - @staticmethod - def mk_txt_node_interrupts(node: Node, - with_status: bool = False) -> RenderableType: - if not node.interrupts: - return Text() - irq_rows: List[Text] = [] - for irq in node.interrupts: - txt = DtshTui.mk_txt_node_irq(irq) - if with_status and (node.status != 'okay'): - DtshTui.txt_dim(txt) - irq_rows.append(txt) - if len(irq_rows) == 1: - return irq_rows[0] - grid = DtshTui.mk_grid(1) - for irq_row in irq_rows: - grid.add_row(irq_row) - return grid - - @staticmethod - def mk_txt_node_irq(ctrl_data: ControllerAndData) -> Text: - irq = ctrl_data.data.get('irq') - level = ctrl_data.data.get('priority') - if (irq is not None) and (level is not None): - if ctrl_data.name: - txt = Text(f"{ctrl_data.name}[{irq}]", - DtshTui.style(DtshTui.STYLE_DT_IRQ)) - else: - txt = Text(f"IRQ_{irq}", DtshTui.style(DtshTui.STYLE_DT_IRQ)) - txt.append_text(DtshTui.mk_txt(f"/{level}")) - else: - irq_data: List[Text] = [] - for k, v in ctrl_data.data.items(): - irq_data.append(DtshTui.mk_txt(f"{k}:{str(v)}")) - txt = Text(" ").join(irq_data) - return txt - - @staticmethod - def mk_txt_node_path(path:str) -> Text: - """Create a rich node path. - - Arguments: - path -- the node path - - Returns a rich text. - """ - if path == '/': - return Text('/', DtshTui.style('dtsh.path.anchor')) - - path_segments = path.split('/') - # Skip path_segments[0] == ''. - path_segments = path_segments[1:] - - txt_segments: List[Text] = [] - for i, seg in enumerate(path_segments): - if (i == 0) or (i == len(path_segments) - 1): - txt_segments.append( - DtshTui.mk_txt(seg, DtshTui.style('dtsh.path.anchor')) - ) - else: - txt_segments.append( - DtshTui.mk_txt(seg, DtshTui.style('dtsh.path.segment')) - ) - - txt_sep = DtshTui.mk_txt('/', DtshTui.style('dtsh.path.segment')) - return txt_sep.append_text(txt_sep.join(txt_segments)) - - @staticmethod - def mk_txt_node_addr(node: Node, with_status: bool = False) -> Text: - if node.unit_addr is None: - return Text() - txt = Text(hex(node.unit_addr)) - if with_status and (node.status != 'okay'): - DtshTui.txt_dim(txt) - return txt - - @staticmethod - def mk_txt_node_binding(node: Node, - with_link: bool = True, - with_status: bool = False) -> Text: - if not node.matching_compat: - return Text() - txt = Text(node.matching_compat, DtshTui.style(DtshTui.STYLE_DT_BINDING)) - if node.binding_path and with_link: - DtshTui.txt_update_link_file(txt, node.binding_path) - if with_status and (node.status != 'okay'): - DtshTui.txt_dim(txt) - return txt - - @staticmethod - def mk_txt_node_compats(node: Node, - shell: Dtsh, - with_link: bool = True, - with_status: bool = False) -> Text: - if not node.compats: - return Text() - txt_bindings: List[Text] = [] - for compat in node.compats: - txt = Text(compat, DtshTui.style(DtshTui.STYLE_DT_COMPATS)) - if compat == node.matching_compat: - txt.stylize(DtshTui.style('bold')) - if with_link: - binding = shell.dt_binding(compat) - if binding and binding.path: - DtshTui.txt_update_link_file(txt, binding.path) - if with_status and (node.status != 'okay'): - DtshTui.txt_dim(txt) - txt_bindings.append(txt) - return Text(' ').join(txt_bindings) - - @staticmethod - def mk_txt_node_label(node: Node, with_status: bool = False) -> Text: - """Returns a rich Text element for the node 'label' property's value, - or an empty Text(). - """ - if not node.label: - return Text() - txt = Text(node.label, DtshTui.style(DtshTui.STYLE_DT_LABEL)) - if with_status and (node.status != 'okay'): - DtshTui.txt_dim(txt) - return txt - - @staticmethod - def mk_txt_node_labels(node: Node, with_status: bool = False) -> Text: - """Returns a rich Text element with all DT labels for the node, - in the order they appear in the DT source, or an empty Text(). - """ - if not node.labels: - return Text() - txt_labels: List[Text] = [] - for label in node.labels: - txt = Text(label, DtshTui.style(DtshTui.STYLE_DT_LABELS)) - if with_status and (node.status != 'okay'): - DtshTui.txt_dim(txt) - txt_labels.append(txt) - txt = Text(', ', DtshTui.style(DtshTui.STYLE_DEFAULT)).join(txt_labels) - return txt - - @staticmethod - def mk_txt_node_all_labels(node: Node, with_status: bool = False) -> Text: - """Returns a rich Text element with all known labels for the node, - or an empty Text(). - - See: - - mk_txt_node_label() - - mk_txt_node_labels() - """ - txt = DtshTui.mk_txt_node_label(node, with_status=with_status) - if len(txt.plain) > 0: - txt.append_text(Text(', ', DtshTui.style(DtshTui.STYLE_DEFAULT))) - txt.append_text(DtshTui.mk_txt_node_labels(node, with_status=with_status)) - return txt - - @staticmethod - def mk_txt_node_aliases(node: Node, with_status: bool = False) -> Text: - if not node.aliases: - return Text() - txt_aliases: List[Text] = [] - for alias in node.aliases: - txt_aliases.append(Text(alias, DtshTui.style(DtshTui.STYLE_DT_ALIAS))) - txt = Text(' ').join(txt_aliases) - if with_status and (node.status != 'okay'): - DtshTui.txt_dim(txt) - return txt - - @staticmethod - def mk_txt_node_desc_short(node: Node, - with_link: bool = True, - with_status: bool = False) -> Text: - txt = DtshTui.mk_txt_desc_short(node.description) - if with_link and node.binding_path: - if not node.matching_compat: - # Nodes may have a binding without a matching compat: set - # the link on the description - DtshTui.txt_update_link_file(txt, node.binding_path) - if with_status and (node.status != 'okay'): - DtshTui.txt_dim(txt) - return txt - - @staticmethod - def mk_txt_binding(binding: Binding, with_link: bool = True) -> Text: - if not binding.compatible: - return Text() - txt = Text(binding.compatible, DtshTui.style(DtshTui.STYLE_DT_BINDING)) - if binding.path and with_link: - DtshTui.txt_update_link_file(txt, binding.path) - return txt - - @staticmethod - def mk_txt_reg_size(reg: Register) -> Text: - if reg.size is None: - return Text() - return Text(str(reg.size)) - - @staticmethod - def mk_txt_reg_end_addr(reg: Register) -> Text: - if reg.size is None: - return Text() - return Text(hex(reg.addr + reg.size - 1)) - - @staticmethod - def mk_txt_reg_name(reg: Register) -> Text: - if reg.name is None: - return Text() - return Text(reg.name) - - @staticmethod - def mk_txt_prop_spec_path(prop_spec: PropertySpec, - with_link: bool = True) -> Text: - if not prop_spec.path: - return Text() - txt = Text(os.path.basename(prop_spec.path), - DtshTui.style(DtshTui.STYLE_DT_BINDING)) - if prop_spec.path and with_link: - DtshTui.txt_update_link_file(txt, prop_spec.path) - return txt - - @staticmethod - def mk_txt_prop_desc(prop: Property) -> Text: - if prop.spec and prop.spec.description: - txt = DtshTui.mk_txt_desc(prop.spec.description) - else: - txt = Text("No description available.", - DtshTui.style(DtshTui.STYLE_APOLOGY)) - return txt - - @staticmethod - def mk_txt_prop_value(prop: Property) -> Text: - return DtshTui.mk_txt_dt_value(prop.val, prop.type) - - @staticmethod - def mk_txt_dt_value(dt_val: object, dt_type: str) -> Text: - if dt_type in ['phandle', 'path']: - # prop value is the pointed Node - return Text(dt_val.name) - elif dt_type == 'boolean': - return DtshTui.mk_txt_bool(dt_val) - elif dt_type == 'phandles': - # prop value is a list of pointed Node - names = [node.name for node in dt_val] - return Text(str(names)) - elif dt_type == 'phandle-array': - # prop value is a list of ControllerAndData - # controller is a Node - controllers = [cad.controller.name for cad in dt_val] - return Text(str(controllers)) - return Text(str(dt_val)) - - ############################################################################ - # Autocomp hints. - ############################################################################ - - @staticmethod - def mk_command_hints_display(model: List[DtshCommand]) -> Table: - """Layout command completion hints. - - Arguments: - model -- a command list to display as hints - - Returns a rich table. - """ - tab = DtshTui.mk_grid(2) - for cmd in model: - tab.add_row(DtshTui.mk_txt(cmd.name), DtshTui.mk_txt_dim(cmd.desc)) - return tab - - @staticmethod - def mk_option_hints_display(model: List[DtshCommandOption]) -> Table: - """Layout option completion hints. - - Arguments: - model -- an option list to display as hints - - Returns a rich table. - """ - tab = DtshTui.mk_grid(2) - for opt in model: - tab.add_row(DtshTui.mk_txt(opt.usage), DtshTui.mk_txt_dim(opt.desc)) - return tab - - @staticmethod - def mk_node_hints_display(model: List[Node]) -> Table: - """Layout node completion hints. - - Arguments: - model -- a node list to display as hints - - Returns a rich table. - """ - tab = DtshTui.mk_grid(2) - for node in model: - if node.status == 'disabled': - style = DtshTui.style_dim() - else: - style = DtshTui.style_default() - txt_name = DtshTui.mk_txt(node.name, style) - if node.description: - txt_desc = DtshTui.mk_txt_dim( - DtshTui.get_text_summary(node.description) - ) - else: - txt_desc = None - tab.add_row(txt_name, txt_desc) - return tab - - @staticmethod - def mk_property_hints_display(model: List[Property]) -> Table: - """Layout property completion hints. - - Arguments: - model -- a property list to display as hints - - Returns a rich table. - """ - # ISSUE: edtlib would raise p.description.strip() not defined on NoneType, - # let's rely on p.spec. - tab = DtshTui.mk_grid(2) - for prop in model: - txt_desc = None - if prop.spec and prop.spec.description: - txt_desc = DtshTui.mk_txt_dim( - DtshTui.get_text_summary(prop.spec.description) - ) - tab.add_row(DtshTui.mk_txt(prop.name), txt_desc) - return tab - - @staticmethod - def mk_binding_hints_display(model: List[Binding]) -> Table: - """Layout bindings completion hints. - - Arguments: - model -- a binding list to display as hints - - Returns a rich table. - """ - tab = DtshTui.mk_grid(2) - for binding in model: - txt_compat = DtshTui.mk_txt(binding.compatible) - if binding.description: - txt_desc = DtshTui.mk_txt_dim( - DtshTui.get_text_summary(binding.description) - ) - else: - txt_desc = None - tab.add_row(txt_compat, txt_desc) - return tab - - - ############################################################################ - # Layouts: DT objects - ############################################################################ - - @staticmethod - def mk_form_node_common(node: Node, shell: Dtsh) -> Table: - form = DtshTui.mk_form() - form.add_row('Path:', node.path) - form.add_row('Name:', DtshTui.get_node_nick(node)) - if node.unit_addr is not None: - form.add_row('Unit address:', DtshTui.mk_txt_node_addr(node)) - if node.compats: - form.add_row('Compatible:', DtshTui.mk_txt_node_compats(node, shell)) - if node.label: - form.add_row('Label:', DtshTui.mk_txt_node_label(node)) - if node.labels: - form.add_row('Labels:', DtshTui.mk_txt_node_labels(node)) - if node.aliases: - form.add_row('Aliases:', DtshTui.mk_txt_node_aliases(node)) - form.add_row('Status:', DtshTui.mk_txt_node_status(node)) - return form - - @staticmethod - def mk_grid_node_depends_on(node: Node) -> Table: - if node.depends_on: - grid = DtshTui.mk_grid(2) - for node in node.depends_on: - grid.add_row(node.name, DtshTui.mk_txt_node_binding(node)) - else: - grid = DtshTui.mk_grid(1) - grid.add_row(Text("This node does not directly depend on any node.", - DtshTui.style(DtshTui.STYLE_APOLOGY))) - return grid - - @staticmethod - def mk_grid_node_required_by(node: Node) -> Table: - if node.required_by: - grid = DtshTui.mk_grid(2) - for node in node.required_by: - grid.add_row(node.name, DtshTui.mk_txt_node_binding(node)) - else: - grid = DtshTui.mk_grid(1) - grid.add_row(Text("There's no other node that directly depends on this node.", - DtshTui.style(DtshTui.STYLE_APOLOGY))) - return grid - - @staticmethod - def mk_grid_node_registers(node: Node) -> Table: - if node.regs: - grid = DtshTui.mk_grid_simple_head( - ['Address', 'Size', 'End', 'Name'] - ) - for reg in node.regs: - grid.add_row(Text(hex(reg.addr)), - DtshTui.mk_txt_reg_size(reg), - DtshTui.mk_txt_reg_end_addr(reg), - DtshTui.mk_txt_reg_name(reg)) - else: - grid = DtshTui.mk_grid(1) - grid.add_row(Text("This node does not define any register.", - DtshTui.style(DtshTui.STYLE_APOLOGY))) - return grid - - @staticmethod - def mk_grid_node_properties(node: Node) -> Table: - if node.props: - grid = DtshTui.mk_grid_simple_head(['Name', 'Type', 'Value']) - for _, prop in node.props.items(): - grid.add_row( - DtshTui.mk_txt(prop.name, DtshTui.style(DtshTui.STYLE_DT_PROPERTY)), - prop.type, - DtshTui.mk_txt_prop_value(prop)) - else: - grid = DtshTui.mk_grid(1) - grid.add_row(Text("This node does not define any property.", - DtshTui.style(DtshTui.STYLE_APOLOGY))) - return grid - - @staticmethod - def mk_form_property(prop:Property) -> Table: - form = DtshTui.mk_form() - form.add_row( - 'Name:', - DtshTui.mk_txt(prop.name, DtshTui.style(DtshTui.STYLE_DT_PROPERTY)) - ) - form.add_row('Type:', prop.type) - form.add_row('Required:', DtshTui.mk_txt_bool(prop.spec.required)) - form.add_row('Value:', DtshTui.mk_txt_prop_value(prop)) - if prop.spec.path: - form.add_row('From:', DtshTui.mk_txt_prop_spec_path(prop.spec)) - if prop.spec.default: - form.add_row('Default:', - DtshTui.mk_txt_dt_value(prop.spec.default, prop.type)) - return form - - @staticmethod - def mk_form_prop_name_val(prop:Property) -> Table: - form = DtshTui.mk_form() - form.add_row('Name:', prop.name) - form.add_row('Value:', DtshTui.mk_txt_prop_value(prop)) - return form - - @staticmethod - def mk_form_prop_spec(prop_spec: PropertySpec) -> Table: - form = DtshTui.mk_form() - form.add_row( - 'Name:', - DtshTui.mk_txt(prop_spec.name, - DtshTui.style(DtshTui.STYLE_DT_PROPERTY)) - ) - form.add_row('Type:', prop_spec.type) - form.add_row('Required:', DtshTui.mk_txt_bool(prop_spec.required)) - if prop_spec.default: - form.add_row( - 'Default:', - DtshTui.mk_txt_dt_value(prop_spec.default, prop_spec.type) - ) - return form - - @staticmethod - def mk_node_tree_item(node: Node, - width: List[int], - with_status: bool = False) -> Table: - grid = DtshTui.mk_grid(3) - for i, w in enumerate(width): - if w > 0: - grid.columns[i].width = w - grid.add_row( - DtshTui.mk_txt_node_addr(node, with_status=with_status), - DtshTui.mk_txt_node_nick(node, with_status=with_status), - DtshTui.mk_txt_node_desc_short(node, with_link=False, with_status=True) - ) - return grid - - @staticmethod - def mk_tree_node_binding(node: Node, - binding: Binding, - shell: Dtsh) -> Tree: - """Build the bindings tree for a node's compatible. - - Arguments: - node -- the node the binding belongs to (used to know if the binding is - the matched compatible for this node) - binding -- a binding matched by this node's compatible string - shell - the dtsh context - - Returns a tree representing the binding specifications this compatible. - """ - anchor = DtshTui.mk_txt_binding(binding) - if node.matching_compat == binding.compatible: - anchor.stylize(DtshTui.style(DtshTui.STYLE_BOLD)) - tree = Tree(anchor) - - with open(binding.path, encoding="utf-8") as f: - yaml_content = f.read() - yaml_py = yaml.load(yaml_content, edtlib_YamlLoader) - - for inc_path in DtshTui._yaml_include_as_paths(yaml_py, shell): - DtshTui.mk_branch_binding_path(inc_path, tree, shell) - return tree - - @staticmethod - def mk_branch_binding_path(path: str, - tree: Tree, - shell: Dtsh) -> None: - with open(path, encoding="utf-8") as f: - yaml_content = f.read() - yaml_py = yaml.load(yaml_content, edtlib_YamlLoader) - - compat = yaml_py.get('compatible') - if compat: - anchor = Text(str(compat), DtshTui.style(DtshTui.STYLE_DT_COMPATS)) - else: - anchor = Text(os.path.basename(path), - DtshTui.style(DtshTui.STYLE_DT_INCLUDE)) - DtshTui.txt_update_link_file(anchor, path) - - branch = tree.add(anchor) - for inc_path in DtshTui._yaml_include_as_paths(yaml_py, shell): - DtshTui.mk_branch_binding_path(inc_path, branch, shell) - - @staticmethod - def _yaml_include_as_paths(yaml_py, shell: Dtsh) -> List[str]: - # Paths for the YAML files included with "include:" statements. - inc_paths: List[str] = [] - # See edtlib.Binding._merge_includes() - yaml_inc = yaml_py.get('include') - if isinstance(yaml_inc, str): - path = shell.dt_binding_path(yaml_inc) - if path: - inc_paths.append(path) - elif isinstance(yaml_inc, list): - for fname in yaml_inc: - path = shell.dt_binding_path(fname) - if path: - inc_paths.append(path) - return inc_paths - - ############################################################################ - # Layouts: yaml - ############################################################################ - - @staticmethod - def mk_yaml(path: str, theme: str = 'ansi_dark') -> Syntax: - return Syntax.from_path(path, lexer='yaml', theme=theme,) - - @staticmethod - def mk_yaml_node_binding(node: Node) -> RenderableType: - if not node.binding_path: - return Text("No binding source available.", - DtshTui.style(DtshTui.STYLE_APOLOGY)) - grid = DtshTui.mk_grid(1) - txt_path = Text(os.path.basename(node.binding_path)) - DtshTui.txt_update_link_file(txt_path, node.binding_path) - grid.add_row(txt_path) - grid.add_row(None) - grid.add_row(DtshTui.mk_yaml(node.binding_path)) - return grid - - @staticmethod - def mk_yaml_binding(binding: Binding) -> RenderableType: - if not (binding and binding.path): - return Text("No binding source available.", - DtshTui.style(DtshTui.STYLE_APOLOGY)) - grid = DtshTui.mk_grid(1) - txt_path = Text(os.path.basename(binding.path)) - DtshTui.txt_update_link_file(txt_path, binding.path) - grid.add_row(txt_path) - grid.add_row(None) - grid.add_row(DtshTui.mk_yaml(binding.path)) - return grid - - ############################################################################ - # Layouts: base - ############################################################################ - - @staticmethod - def mk_grid(cols: int) -> Table: - grid = Table.grid(padding=(0, 1)) - for _ in range(0, cols): - grid.add_column() - return grid - - @staticmethod - def mk_form(name_style: Optional[Style] = None, - value_style: Optional[Style] = None ) -> Table: - form = DtshTui.mk_grid(2) - if name_style: - form.columns[0].style = name_style - if value_style: - form.columns[1].style = value_style - return form - - @staticmethod - def form_add(form: Table, name: str, val: str): - form.add_row(name, val) - - @staticmethod - def mk_grid_simple_head(cols: List[str]) -> Table: - grid = Table.grid(padding=(0, 1)) - grid.box = box.SIMPLE_HEAD - grid.show_header = True - grid.header_style = DtshTui.style(DtshTui.STYLE_DEFAULT) - for header in cols: - grid.add_column(header=header) - return grid - - @staticmethod - def mk_grid_statusbar() -> Table: - bar = Table.grid(padding=(0, 1), expand=True) - bar.add_column(justify='left', style=DtshTui.style_default(), ratio=1) - bar.add_column(justify='center', style=DtshTui.style_default(), ratio=1) - bar.add_column(justify='right', style=DtshTui.style_default(), ratio=1) - return bar - - ############################################################################ - # Internals - ############################################################################ - - @staticmethod - def _load_theme() -> Theme: - theme = DtshConfig.rich_read_theme() - # load custom dtsh config - config = configparser.ConfigParser() - config.read_file(open(DtshConfig.get_theme_path())) - for name, value in config.items('dtsh'): - if name == 'dtsh.prompt.wchar': - DtshTui.PROMPT_WCHAR = value - elif name == 'dtsh.bullet.wchar': - DtshTui.WCHAR_BULLET = value - elif name == 'dtsh.prompt.color': - DtshTui.PROMPT_COLOR = value - elif name == 'dtsh.prompt.color.error': - DtshTui.PROMPT_COLOR_ERROR = value - return theme - - -############################################################################ -# Widget: re-usable components alternative to DtshTui.mk_xxx() API -############################################################################ - -class DtshTuiWidget(object): - """A widget is a renderable factory for model or state objects. - """ - - @abstractmethod - def as_renderable(self) -> RenderableType: - """Returns the renderable for the model or state object this widget - represents. - """ - - -class DtshTuiBulletList(DtshTuiWidget): - """Simple bullet list. - """ - - # List layout. - _grid: Table - - # List item prefix, default to " - ". - _bullet: str - - def __init__(self, - label: Union[str, Text], - bullet: Optional[str] = None) -> None: - """Initialize the widget. - - Arguments: - label -- the list label (typically ends with ":"), - as a string or a rich Text - bulet -- the bullet symbol, defaults to "-" - """ - self._grid = Table.grid(padding=(0, 1)) - self._bullet = bullet or f" {DtshTui.WCHAR_BULLET} " - self._grid.add_row(label) - - def add_item(self, item: Union[str, Text]) -> None: - """ - """ - r_item = DtshTui.mk_txt(self._bullet) - if isinstance(item, Text): - r_item = r_item.append_text(item) - else: - r_item.append(item) - self._grid.add_row(r_item) - - def as_renderable(self) -> RenderableType: - """Implements DtshTuiWidget.as_renderable(). - """ - return self._grid - - -class DtshTuiYaml(DtshTuiWidget): - """A widget is a renderable factory for YAML files. - """ - - _grid: Table - - def __init__(self, path:str, with_title: bool = True) -> None: - """Initialize YAML layout. - """ - self._grid = DtshTui.mk_grid(1) - if with_title: - r_name = DtshTui.mk_txt(os.path.basename(path), - style='dtsh.basename') - DtshTui.txt_update_link_file(r_name, path) - self._grid.add_row(r_name) - self._grid.add_row() - self._grid.add_row(DtshTui.mk_yaml(path)) - - def as_renderable(self) -> RenderableType: - """Implements DtshTuiWidget.as_renderable(). - """ - return self._grid - - -class DtshTuiForm(DtshTuiWidget): - """A widget is a renderable factory for model or state objects. - """ - - # Field separator. - FIELD_SEP: Text = Text(":", style=DtshTui.style_default()) - - # 2-columns form layout. - _form: Table - - # Style for all field labels. - _label_style: StyleType - - # Default style for field values. - _value_style: StyleType - - def __init__(self, - label_style: Optional[StyleType] = None, - value_style: Optional[StyleType] = None) -> None: - """ - Arguments: - label_style -- - value_style -- - """ - self._label_style = label_style or DtshTui.style_default() - self._value_style = value_style or DtshTui.style_default() - self._form = Table.grid(padding=(0, 1)) - self._form.add_column() - self._form.add_column() - - def add_field(self, - label: str, - value: Optional[str], - default: str = "Unknown", - style: Optional[StyleType] = None) -> None: - """Add a string field. - - Arguments: - label -- the field label - value -- the field value as a rich Text - default -- value string representing None values - style -- - """ - r_label = Text(label, style=self._label_style) - r_label.append_text(DtshTuiForm.FIELD_SEP) - if value: - r_value = Text(value, style=style or self._value_style) - else: - r_value = DtshTui.mk_txt_dim(default) - self._form.add_row(r_label, r_value) - - def add_field_rich(self, - label: str, - value: Optional[Text], - default: str = "Unknown") -> None: - """Add a rich field. - - Arguments: - label -- the field label - value -- the field value as a rich Text - default -- value string representing None values - """ - r_label = Text(label, style=self._label_style) - r_label.append_text(DtshTuiForm.FIELD_SEP) - r_value = value or DtshTui.mk_txt_dim(default) - self._form.add_row(r_label, r_value) - - def as_renderable(self) -> RenderableType: - """Implements DtshTuiWidget.as_renderable(). - """ - return self._form - - -############################################################################ -# Flexible node table with format string support. -############################################################################ - -class LsNodeColumn(object): - """Nodes table view column. - """ - - # E.g. "b" - _spec: str - - # E.g. "Bus" - _header: str - - def __init__(self, spec: str, header: str) -> None: - """ - Arguments: - spec -- the column's format specifier, e.g. "N" for the node name - header -- the column's header, e.g. "Name" - """ - self._spec = spec - self._header = header - - @property - def spec(self) -> str: - """The column's format specifier. - """ - return self._spec - - @property - def header(self) -> str: - """The column's header. - """ - return self._header - - @abstractmethod - def mk_view(self, node: Node, shell: Dtsh) -> RenderableType: - """Returns a rich view for the colum information, - or an empty Text() element when the node does not define - the requested information. - """ - -class LsColumnNodeName(LsNodeColumn): - """The node name (DTSpec 2.2.1). - - Format specifier is "N". - """ - - def __init__(self) -> None: - super().__init__("N", "Name") - - def mk_view(self, node: Node, shell: Dtsh) -> RenderableType: - return DtshTui.mk_txt_node_name(node, with_status=True) - - -class LsColumnNodeAddr(LsNodeColumn): - """The unit-address component of the node name. - - Format specifier is "a". - """ - - def __init__(self) -> None: - super().__init__("a", "Address") - - def mk_view(self, node: Node, shell: Dtsh) -> RenderableType: - return DtshTui.mk_txt_node_addr(node, with_status=True) - - -class LsColumnNodeNick(LsNodeColumn): - """The node name with the unit address component striped. - - Format specifier is "n". - """ - - def __init__(self) -> None: - super().__init__("n", "Name") - - def mk_view(self, node: Node, shell: Dtsh) -> RenderableType: - return DtshTui.mk_txt_node_nick(node, with_status=True) - - -class LsColumnNodeDesc(LsNodeColumn): - """A summary of the desricription string from the node binding. - - Format specifier is "d". - """ - - def __init__(self) -> None: - super().__init__("d", "Description") - - def mk_view(self, node: Node, shell: Dtsh) -> RenderableType: - return DtshTui.mk_txt_node_desc_short(node, with_status=True) - - -class LsColumnNodePath(LsNodeColumn): - """The node path name (DT 2.2.3). - - Format specifier is "p". - """ - - def __init__(self) -> None: - super().__init__("p", "Path") - - def mk_view(self, node: Node, shell: Dtsh) -> RenderableType: - txt = DtshTui.mk_txt(node.path) - if node.status != 'okay': - DtshTui.txt_dim(txt) - return txt - - -class LsColumnNodeLabel(LsNodeColumn): - """The node label that is the value of its 'label' property. - - Format specifier is "l". - """ - - def __init__(self) -> None: - super().__init__("l", "Label") - - def mk_view(self, node: Node, shell: Dtsh) -> RenderableType: - return DtshTui.mk_txt_node_label(node, with_status=True) - - -class LsColumnNodeLabels(LsNodeColumn): - """All known labels for the node, apppending the DT labels - to its 'label' property value. - - Format specifier is "L". - """ - - def __init__(self) -> None: - """Format specifier is "L". - """ - super().__init__("L", "Labels") - - def mk_view(self, node: Node, shell: Dtsh) -> RenderableType: - return DtshTui.mk_txt_node_all_labels(node, with_status=True) - - -class LsColumnNodeStatus(LsNodeColumn): - """The node status that is the value of its 'status' property (DTSpec 2.3.4). - - Format specifier is "s". - """ - - def __init__(self) -> None: - super().__init__("s", "Status") - - def mk_view(self, node: Node, shell: Dtsh) -> RenderableType: - return DtshTui.mk_txt_node_status(node) - - -class LsColumnNodeCompatible(LsNodeColumn): - """The value of the compatible property for the node (DTSpec 2.3.1), - should be ordered from most to lesser specific. - - Format specifier is "c". - """ - - def __init__(self) -> None: - super().__init__("c", "Compatible") - - def mk_view(self, node: Node, shell: Dtsh) -> RenderableType: - return DtshTui.mk_txt_node_compats(node, shell, with_status=True) - - -class LsColumnNodeBinding(LsNodeColumn): - """The compatible from the binding that matched the node. - - Format specifier is "C". - """ - - def __init__(self) -> None: - super().__init__("C", "Binding") - - def mk_view(self, node: Node, shell: Dtsh) -> RenderableType: - return DtshTui.mk_txt_node_binding(node, - with_link=True, - with_status=True) - -class LsColumnNodeAliases(LsNodeColumn): - """The aliases for the node, fetched from /aliases. - - Format specifier is "A". - """ - - def __init__(self) -> None: - super().__init__("A", "Aliases") - - def mk_view(self, node: Node, shell: Dtsh) -> RenderableType: - return DtshTui.mk_txt_node_aliases(node, with_status=True) - - -class LsColumnNodeBus(LsNodeColumn): - """The bus device information for the node. - - Format specifier is "b". - """ - - def __init__(self) -> None: - super().__init__("b", "Bus") - - def mk_view(self, node: Node, shell: Dtsh) -> RenderableType: - return DtshTui.mk_txt_node_bus_device(node, with_status=True) - - -class LsColumnNodeReg(LsNodeColumn): - """The bus device information for the node. - - Format specifier is "r". - """ - - def __init__(self) -> None: - super().__init__("r", "Register") - - def mk_view(self, node: Node, shell: Dtsh) -> RenderableType: - return DtshTui.mk_txt_node_register(node, with_status=True) - - -class LsColumnNodeInterrupts(LsNodeColumn): - """The interrupts generated by this node. - - Format specifier is "i". - """ - - def __init__(self) -> None: - super().__init__("i", "Interrupts") - - def mk_view(self, node: Node, shell: Dtsh) -> RenderableType: - return DtshTui.mk_txt_node_interrupts(node, with_status=True) - - -class LsNodeTable(object): - """Configurable table view for a list of nodes. - - Visible columns are configured through a format string, e.g. "naLcd". - - Defined format specifiers: - - | Specifier | Format | DTSpec | - |-----------|-------------------------------------------|---------| - | `N` | The node name | 2.2.1 | - | `a` | The unit-address | | - | `n` | The node name with the address striped | | - | `d` | The description from the node binding | | - | `p` | The node path name | 2.2.3 | - | `l` | The node 'label' property | | - | `L` | All known labels for the node | | - | `s` | The node 'status' property | 2.3.4 | - | `c` | The 'compatible' property for the node | 2.3.1 | - | `C` | The node binding (aka matched compatible) | | - | `A` | The node aliases | | - | `b` | The bus device information for the node | | - | `r` | The node 'reg' property | 2.3.6 | - | `i` | The interrupts generated by the node | 2.4.1.1 | - """ - - _dtsh: Dtsh - - # The columns for the requested format. - _cols: List[LsNodeColumn] - - # The actual view. - _grid: Table - - def __init__(self, shell: Dtsh, longfmt: str) -> None: - """Initialize a new node table. - - Arguments: - shell -- the client DT shell - longfmt -- the visible columns format string - - Raises DtshError when the format specifiers string is invalid. - """ - self._dtsh = shell - self._cols = [] - for spec in longfmt: - col = LsNodeTable._colspecs.get(spec) - if not col: - raise DtshError(f"unknwon format specifier {spec}") - self._cols.append(col) - self._grid = DtshTui.mk_grid_simple_head( - [col.header for col in self._cols] - ) - - def add_node_row(self, node: Node) -> None: - """Add a node to the table. - """ - colviews = [col.mk_view(node, self._dtsh) for col in self._cols] - self._grid.add_row(*colviews) - - def as_view(self) -> Table: - """Returns the view for the node table. - """ - return self._grid - - # Available columns. - _colspecs: Dict[str, LsNodeColumn] = { - col.spec: col for col in [ - LsColumnNodeName(), - LsColumnNodeAddr(), - LsColumnNodeNick(), - LsColumnNodeDesc(), - LsColumnNodePath(), - LsColumnNodeLabel(), - LsColumnNodeLabels(), - LsColumnNodeStatus(), - LsColumnNodeCompatible(), - LsColumnNodeBinding(), - LsColumnNodeAliases(), - LsColumnNodeBus(), - LsColumnNodeReg(), - LsColumnNodeInterrupts(), - ] - } - - -############################################################################ -# Views -############################################################################ - -class DtshTuiView(object): - """A view will eventually show itself on a rich VT. - """ - - @abstractmethod - def show(self, vt: DtshVt, with_pager: bool = False) -> None: - """Show this view on a console. - - Arguments: - vt -- the VT to write the view to - with_pager -- if True, the output will be paged - """ - - -class DtshTuiGridView(DtshTuiView): - """Base grid layout with pager support. - """ - - # View rich table layout. - _grid: Table - - def __init__(self, cols: int = 1, expand: bool = False) -> None: - """Initialize the grid. - - Arguments: - cols -- number of columns, defaults to 1 - expand -- if True, axpand the layout to fit the available horizontal - space, defaults to False - """ - self._grid = Table.grid(padding=(0, 1), expand=expand) - for _ in range(0, cols): - self._grid.add_column() - - def show(self, vt: DtshVt, with_pager: bool = False) -> None: - """Implements DtshTView.show(). - """ - if with_pager: - vt.pager_enter() - vt.write(self._grid) - if with_pager: - vt.pager_exit() - - -class DtshTuiStructuredView(DtshTuiGridView): - """View with content divided into named sections. - - Two-columns grid: the former column holds section names, - the later section contents. - """ - - def __init__(self) -> None: - """Initialize the view. - """ - super().__init__(cols=2) - - def add_section(self, name: str, content: RenderableType) -> None: - """Add a section. - - Note that this unconditionally adds an empty row bellow the content row. - - Arguments: - name -- the section's label - content -- the section's content - """ - label = Text(name, DtshTui.style('bold')) - self._grid.add_row(label, None) - self._grid.add_row(None, content) - self._grid.add_row(None, None) - - -class DtshTuiPortraitView(DtshTuiGridView): - """Vertical layout with indented contents. - - One column grid where different rows have different indentation levels. - """ - - def __init__(self, expand: bool = False) -> None: - """Initialize the view. - - Arguments: - expand -- if True, axpand the layout to fit the available horizontal - space, defaults to False - """ - super().__init__(cols=1, expand=expand) - - def add(self, content: RenderableType, indent_size: int = 0) -> None: - """Add conttent to this view. - - Arguments: - content -- the rich content to add - indent_size -- indentation in number of characters - """ - self._grid.add_row(Padding(content, (0, indent_size))) - - -class DtshTuiMemo(DtshTuiPortraitView): - """Portrait view organized in named entries. - - This is a more predictable layout alternative to DtshTuiStructuredView. - - Entries will show up as bellow, where dots represent the memo indentation: - FOO - ........FOO content begins - content continue - - BAR - ........BAR content begins - content continue - """ - - def __init__(self, indent_size:int = 8, expand: bool = False) -> None: - """Initialize the view. - - Arguments: - indent_size -- content indentation in number of characters, - defaults to 8 - expand -- if True, axpand the layout to fit the available horizontal - space, defaults to False - """ - super().__init__(expand=expand) - self._indent_size = indent_size - - def add_entry(self, name: str, content: Optional[RenderableType]) -> None: - """Add a named entry to the memo. - - Arguments: - name -- the entry's label (will be uppercased) - content -- the rich content to add - is_last -- if False, an empty row is appended bellow the content row - """ - if self._grid.row_count > 0: - # Add empty row after previous entry. - self._grid.add_row(None) - self._grid.add_row(DtshTui.mk_txt_bold(name.upper())) - if content is None: - content = DtshTui.mk_txt("Information not available.", - style=DtshTui.style_apology()) - self._grid.add_row(Padding(content, (0, self._indent_size))) - - -class DtNodeView(DtshTuiStructuredView): - """Structured view for detailed node information. - """ - - def __init__(self, node:Node, shell:Dtsh) -> None: - """Initialize the view. - - Arguments: - node -- the node to show - shell -- the context shell - """ - super().__init__() - self.add_section('Node', - DtshTui.mk_form_node_common(node, shell)) - self.add_section('Description', - DtshTui.mk_txt_desc(node.description)) - self.add_section('Depends-on', - DtshTui.mk_grid_node_depends_on(node)) - self.add_section('Required-by', - DtshTui.mk_grid_node_required_by(node)) - self.add_section('Registers', - DtshTui.mk_grid_node_registers(node)) - self.add_section('Properties', - DtshTui.mk_grid_node_properties(node)) - self.add_section('Specified-by', - DtshTui.mk_yaml_node_binding(node)) - self._add_section_bindings_tree(node, shell) - - def _add_section_bindings_tree(self, node: Node, shell:Dtsh) -> None: - grid = DtshTui.mk_grid(1) - N = len(node.compats) - for i, compat in enumerate(node.compats): - binding = shell.dt_binding(compat) - if binding: - tree = DtshTui.mk_tree_node_binding(node, binding, shell) - grid.add_row(tree) - i += 1 - if i < N: - grid.add_row(None) - self.add_section("Bindings", grid) - - -class DtPropertyView(DtshTuiStructuredView): - """Structured view for detailed property information. - - Most of the information will show up only when the property's specification - is available. - """ - def __init__(self, prop:Property) -> None: - """Initialize the view. - - Arguments: - prop -- the property to show - shell -- the context shell - """ - super().__init__() - if prop.spec: - self.add_section('Property', - DtshTui.mk_form_property(prop)) - self.add_section('Description', DtshTui.mk_txt_prop_desc(prop)) - self.add_section('Binding', - DtshTui.mk_yaml_binding(prop.spec.binding)) - else: - self.add_section('Property', - DtshTui.mk_form_prop_name_val(prop)) - - -class DtNodeListView(DtshTuiView): - """Node list view. - - By default, will show node contents (aka children). - - This view handles both the default and "rich output" cases. - """ - - # View rich table layout. - _view: Table - - # Default columns. - DEFAULT_LONGFMT = 'naLAcd' - - def __init__(self, - node_map: Dict[str, List[Node]], - shell: Dtsh, - with_no_content: bool = False, - with_longfmt: bool = False, - longfmt: Optional[str] = None) -> None: - """Initialize the view. - - Arguments: - node_map -- maps node paths to contents (aka child nodes) - shell -- the context shell - with_no_content -- if True, will show nodes, not their content - with_longfmt -- if True, use long (aka rich) listing format - longfmt -- specifies visible columns, implies long format if not None - """ - if with_longfmt and not longfmt: - # '-l' defaults columns. - longfmt = DtNodeListView.DEFAULT_LONGFMT - - if longfmt: - # '-f' implies '-l'. - self._init_view_longfmt(node_map, shell, with_no_content, longfmt) - else: - self._init_view_default(node_map, with_no_content) - - def show(self, vt: DtshVt, with_pager: bool = False) -> None: - """Implements DtshTView.show(). - """ - if with_pager: - vt.pager_enter() - vt.write(self._view) - if with_pager: - vt.pager_exit() - - def _init_view_default(self, - node_map: Dict[str, List[Node]], - with_no_content: bool) -> None: - self._view = DtshTui.mk_grid(1) - N = len(node_map) - n = 0 - for path, nodes in node_map.items(): - if with_no_content: - self._view.add_row(f'{path}') - else: - if N > 1: - self._view.add_row(f'{path}:') - for node in nodes: - self._view.add_row(f'{node.path}') - if n < (N - 1): - self._view.add_row(None) - n += 1 - - def _init_view_longfmt(self, - node_map: Dict[str, List[Node]], - shell: Dtsh, - with_no_content: bool, - longfmt: str) -> None: - if with_no_content: - ls_table = LsNodeTable(shell, longfmt) - for path, _ in node_map.items(): - node = shell.path2node(path) - ls_table.add_node_row(node) - self._view = ls_table.as_view() - else: - self._view = DtshTui.mk_grid(1) - N = len(node_map) - n = 0 - for path, content in node_map.items(): - self._view.add_row(Text(f"{path}:", DtshTui.style('bold'))) - if content: - ls_table = LsNodeTable(shell, longfmt) - for node in content: - ls_table.add_node_row(node) - self._view.add_row(ls_table.as_view()) - if n < (N - 1): - self._view.add_row(None) - n += 1 - - -class DtNodeTreeView(DtshTuiView): - """Node tree view. - """ - - # View rich tree layout. - _view: Tree - - def __init__(self, - root: Node, - shell: Dtsh, - level: int, - with_rich_fmt: bool) -> None: - """Initialize the view. - - Arguments: - root - the tree's root node - shell -- the context shell - level -- the maximum tree depth; if 0, will ignore depth and stop only - when reaching a disabled (not okay) node - with_rich_fmt -- if True, produce "rich output" - """ - self._rich_fmt = with_rich_fmt - self._dtsh = shell - self._level = level - self._depth = 0 - - anchor = Text(root.path, DtshTui.style('bold')) - self._view = Tree(anchor) - self._follow_node_branch(root, self._view) - - def show(self, vt: DtshVt, with_pager: bool = False) -> None: - """Implements DtshTView.show(). - """ - if with_pager: - vt.pager_enter() - vt.write(self._view) - if with_pager: - vt.pager_exit() - - def _follow_node_branch(self, - root: Node, - tree: Tree) -> None: - # Increase depth when following a branch. - self._depth += 1 - - # Maximum length of child nodes nickname. - width = self._get_branch_width(root) - - for _, node in root.children.items(): - if self._rich_fmt: - anchor = DtshTui.mk_node_tree_item(node, width, True) - else: - anchor = node.name - branch = tree.add(anchor) - - if (self._level == 0) or (self._depth < self._level): - if node.status != 'disabled': - self._follow_node_branch(node, branch) - - # Decrease depth on return. - self._depth -= 1 - - def _get_branch_width(self, root: Node) -> List[int]: - width_addr = 0 - width_nick = 0 - for _, node in root.children.items(): - nick = DtshTui.get_node_nick(node) - w = len(nick) - if w > width_nick: - width_nick = w - w = len(DtshTui.mk_txt_node_addr(node).plain) - if w > width_addr: - width_addr = w - return [width_addr, width_nick] diff --git a/src/dtsh/typed.py b/src/dtsh/typed.py new file mode 100644 index 0000000..90fdf91 --- /dev/null +++ b/src/dtsh/typed.py @@ -0,0 +1,5 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +# Marker file for PEP 561. diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..95f0cb0 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 diff --git a/tests/bindings/bar-bus.yaml b/tests/bindings/bar-bus.yaml deleted file mode 100644 index e429e30..0000000 --- a/tests/bindings/bar-bus.yaml +++ /dev/null @@ -1,7 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -description: Bar bus controller - -compatible: "bar-bus" - -bus: "bar" diff --git a/tests/bindings/child-binding-with-compat.yaml b/tests/bindings/child-binding-with-compat.yaml deleted file mode 100644 index 1744ad2..0000000 --- a/tests/bindings/child-binding-with-compat.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -description: child-binding with separate compatible than the parent - -compatible: "top-binding-with-compat" - -child-binding: - compatible: child-compat - description: child node - properties: - child-prop: - type: int - required: true - - child-binding: - description: grandchild node - properties: - grandchild-prop: - type: int - required: true diff --git a/tests/bindings/child-binding.yaml b/tests/bindings/child-binding.yaml deleted file mode 100644 index 5319dea..0000000 --- a/tests/bindings/child-binding.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -description: child-binding test - -compatible: "top-binding" - -child-binding: - description: child node - properties: - child-prop: - type: int - required: true - - child-binding: - description: grandchild node - properties: - grandchild-prop: - type: int - required: true diff --git a/tests/bindings/child.yaml b/tests/bindings/child.yaml deleted file mode 100644 index 091c659..0000000 --- a/tests/bindings/child.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -include: [grandchild-1.yaml, grandchild-2.yaml, grandchild-3.yaml] - -properties: - bar: - required: true - type: int diff --git a/tests/bindings/defaults.yaml b/tests/bindings/defaults.yaml deleted file mode 100644 index 706067c..0000000 --- a/tests/bindings/defaults.yaml +++ /dev/null @@ -1,36 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -description: Property default value test - -compatible: "defaults" - -properties: - int: - type: int - required: false - default: 123 - - array: - type: array - required: false - default: [1, 2, 3] - - uint8-array: - type: uint8-array - required: false - default: [0x89, 0xAB, 0xCD] - - string: - type: string - required: false - default: "hello" - - string-array: - type: string-array - required: false - default: ["hello", "there"] - - default-not-used: - type: int - required: false - default: 123 diff --git a/tests/bindings/deprecated.yaml b/tests/bindings/deprecated.yaml deleted file mode 100644 index d8c72e5..0000000 --- a/tests/bindings/deprecated.yaml +++ /dev/null @@ -1,15 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -description: Property deprecated value test - -compatible: "test-deprecated" - -properties: - oldprop: - type: int - deprecated: true - required: false - - curprop: - type: int - required: false diff --git a/tests/bindings/device-on-any-bus.yaml b/tests/bindings/device-on-any-bus.yaml deleted file mode 100644 index 06c29e4..0000000 --- a/tests/bindings/device-on-any-bus.yaml +++ /dev/null @@ -1,5 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -description: Device on any bus - -compatible: "on-any-bus" diff --git a/tests/bindings/device-on-bar-bus.yaml b/tests/bindings/device-on-bar-bus.yaml deleted file mode 100644 index d2c72bb..0000000 --- a/tests/bindings/device-on-bar-bus.yaml +++ /dev/null @@ -1,7 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -description: Device on bar bus - -compatible: "on-bus" - -on-bus: "bar" diff --git a/tests/bindings/device-on-foo-bus.yaml b/tests/bindings/device-on-foo-bus.yaml deleted file mode 100644 index 3ed4a75..0000000 --- a/tests/bindings/device-on-foo-bus.yaml +++ /dev/null @@ -1,7 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -description: Device on foo bus - -compatible: "on-bus" - -on-bus: "foo" diff --git a/tests/bindings/enums.yaml b/tests/bindings/enums.yaml deleted file mode 100644 index f36b89b..0000000 --- a/tests/bindings/enums.yaml +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright (c) 2020 Nordic Semiconductor ASA -# SPDX-License-Identifier: BSD-3-Clause - -description: Property enum test - -compatible: "enums" - -properties: - int-enum: - type: int - enum: - - 1 - - 2 - - 3 - - string-enum: # not tokenizable - type: string - enum: - - foo bar - - foo_bar - - tokenizable-lower-enum: # tokenizable in lowercase only - type: string - enum: - - bar - - BAR - - tokenizable-enum: # tokenizable in lower and uppercase - type: string - enum: - - bar - - whitespace is ok - - 123 is ok - - no-enum: - type: string diff --git a/tests/bindings/false-positive.yaml b/tests/bindings/false-positive.yaml deleted file mode 100644 index 30de8fb..0000000 --- a/tests/bindings/false-positive.yaml +++ /dev/null @@ -1,4 +0,0 @@ -# A file that mentions a 'compatible' string without actually implementing it. -# Used to check for issues with how we optimize binding loading. - -# props diff --git a/tests/bindings/foo-bus.yaml b/tests/bindings/foo-bus.yaml deleted file mode 100644 index a3a98d3..0000000 --- a/tests/bindings/foo-bus.yaml +++ /dev/null @@ -1,7 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -description: Foo bus controller - -compatible: "foo-bus" - -bus: "foo" diff --git a/tests/bindings/foo-optional.yaml b/tests/bindings/foo-optional.yaml deleted file mode 100644 index 5383ade..0000000 --- a/tests/bindings/foo-optional.yaml +++ /dev/null @@ -1,4 +0,0 @@ -properties: - foo: - type: int - required: false diff --git a/tests/bindings/foo-required.yaml b/tests/bindings/foo-required.yaml deleted file mode 100644 index 1544e1c..0000000 --- a/tests/bindings/foo-required.yaml +++ /dev/null @@ -1,4 +0,0 @@ -properties: - foo: - type: int - required: true diff --git a/tests/bindings/gpio-dst.yaml b/tests/bindings/gpio-dst.yaml deleted file mode 100644 index 19db316..0000000 --- a/tests/bindings/gpio-dst.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -description: GPIO destination for mapping test - -compatible: "gpio-dst" - -gpio-cells: - - val diff --git a/tests/bindings/gpio-src.yaml b/tests/bindings/gpio-src.yaml deleted file mode 100644 index b7e5c33..0000000 --- a/tests/bindings/gpio-src.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -description: GPIO source for mapping test - -compatible: "gpio-src" - -properties: - foo-gpios: - type: phandle-array diff --git a/tests/bindings/grandchild-1.yaml b/tests/bindings/grandchild-1.yaml deleted file mode 100644 index 249158f..0000000 --- a/tests/bindings/grandchild-1.yaml +++ /dev/null @@ -1,10 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -properties: - foo: - required: false - type: int - - baz: - required: true - type: int diff --git a/tests/bindings/grandchild-2.yaml b/tests/bindings/grandchild-2.yaml deleted file mode 100644 index d7d0b5c..0000000 --- a/tests/bindings/grandchild-2.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -properties: - baz: - required: true - type: int diff --git a/tests/bindings/grandchild-3.yaml b/tests/bindings/grandchild-3.yaml deleted file mode 100644 index c1ba6b8..0000000 --- a/tests/bindings/grandchild-3.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -properties: - qaz: - required: true - type: int diff --git a/tests/bindings/interrupt-1-cell.yaml b/tests/bindings/interrupt-1-cell.yaml deleted file mode 100644 index ab2db66..0000000 --- a/tests/bindings/interrupt-1-cell.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -description: Interrupt controller with one cell - -compatible: "interrupt-one-cell" - -interrupt-cells: - - one diff --git a/tests/bindings/interrupt-2-cell.yaml b/tests/bindings/interrupt-2-cell.yaml deleted file mode 100644 index 2c3144d..0000000 --- a/tests/bindings/interrupt-2-cell.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -description: Interrupt controller with two cells - -compatible: "interrupt-two-cell" - -interrupt-cells: - - one - - two diff --git a/tests/bindings/interrupt-3-cell.yaml b/tests/bindings/interrupt-3-cell.yaml deleted file mode 100644 index 846dee0..0000000 --- a/tests/bindings/interrupt-3-cell.yaml +++ /dev/null @@ -1,10 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -description: Interrupt controller with three cells - -compatible: "interrupt-three-cell" - -interrupt-cells: - - one - - two - - three diff --git a/tests/bindings/multidir.yaml b/tests/bindings/multidir.yaml deleted file mode 100644 index 8504475..0000000 --- a/tests/bindings/multidir.yaml +++ /dev/null @@ -1,5 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -description: Binding in test-bindings/ - -compatible: "in-dir-1" diff --git a/tests/bindings/order-1.yaml b/tests/bindings/order-1.yaml deleted file mode 100644 index e6024a8..0000000 --- a/tests/bindings/order-1.yaml +++ /dev/null @@ -1,7 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -description: Include ordering test - -compatible: "order-1" - -include: ["foo-required.yaml", "foo-optional.yaml"] diff --git a/tests/bindings/order-2.yaml b/tests/bindings/order-2.yaml deleted file mode 100644 index cad1f9b..0000000 --- a/tests/bindings/order-2.yaml +++ /dev/null @@ -1,7 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -description: Include ordering test - -compatible: "order-2" - -include: ["foo-optional.yaml", "foo-required.yaml"] diff --git a/tests/bindings/parent.yaml b/tests/bindings/parent.yaml deleted file mode 100644 index 8a0e0f4..0000000 --- a/tests/bindings/parent.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -description: Parent binding - -compatible: "binding-include-test" - -include: child.yaml - -properties: - foo: - # Changed from not being required in grandchild-1.yaml - required: true - # Type set in grandchild diff --git a/tests/bindings/phandle-array-controller-0.yaml b/tests/bindings/phandle-array-controller-0.yaml deleted file mode 100644 index 3b2430c..0000000 --- a/tests/bindings/phandle-array-controller-0.yaml +++ /dev/null @@ -1,7 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -description: Controller with zero data values - -compatible: "phandle-array-controller-0" - -phandle-array-foo-cells: [] diff --git a/tests/bindings/phandle-array-controller-1.yaml b/tests/bindings/phandle-array-controller-1.yaml deleted file mode 100644 index 9d5c6c5..0000000 --- a/tests/bindings/phandle-array-controller-1.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -description: Controller with one data value - -compatible: "phandle-array-controller-1" - -phandle-array-foo-cells: - - one - -gpio-cells: - - gpio-one diff --git a/tests/bindings/phandle-array-controller-2.yaml b/tests/bindings/phandle-array-controller-2.yaml deleted file mode 100644 index e7a3cab..0000000 --- a/tests/bindings/phandle-array-controller-2.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -description: Controller with two data values - -compatible: "phandle-array-controller-2" - -phandle-array-foo-cells: - - one - - two diff --git a/tests/bindings/props.yaml b/tests/bindings/props.yaml deleted file mode 100644 index 40b4cb9..0000000 --- a/tests/bindings/props.yaml +++ /dev/null @@ -1,50 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -description: Device.props test - -compatible: "props" - -properties: - nonexistent-boolean: - type: boolean - - existent-boolean: - type: boolean - - int: - type: int - const: 1 - - array: - type: array - - uint8-array: - type: uint8-array - - string: - type: string - const: "foo" - - string-array: - type: string-array - - phandle-ref: - type: phandle - - phandle-refs: - type: phandles - - phandle-array-foos: - type: phandle-array - - phandle-array-foo-names: - type: string-array - - # There's some slight special-casing for GPIOs in that 'foo-gpios = ...' - # gets resolved to #gpio-cells rather than #foo-gpio-cells, so test that - # too - foo-gpios: - type: phandle-array - - path: - type: path diff --git a/tests/dtsh_uthelpers.py b/tests/dtsh_uthelpers.py new file mode 100644 index 0000000..3928283 --- /dev/null +++ b/tests/dtsh_uthelpers.py @@ -0,0 +1,634 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Helpers for DTSh unit tests.""" + +from typing import Optional, Dict, List, Generator + +from pathlib import Path +import os +import contextlib + +import pytest + +from devicetree import edtlib + +from dtsh.model import DTModel +from dtsh.io import DTShOutput +from dtsh.shell import ( + DTSh, + DTShCommand, + DTShOption, + DTShFlag, + DTShArg, + DTShParameter, + DTShFlagHelp, + DTShError, + DTShUsageError, + DTShCommandError, +) +from dtsh.shellutils import ( + DTShArgFixedDepth, + DTShArgOrderBy, + DTShParamDTPath, + DTShParamDTPaths, + DTShParamAlias, + DTShParamChosen, + DTSH_NODE_ORDER_BY, +) +from dtsh.rich.shellutils import DTShArgLongFmt, DTSH_NODE_FMT_SPEC + + +class DTShTests: + """Access test resource files, data samples, etc.""" + + ZEPHYR_BASE = os.environ.get("ZEPHYR_BASE") or str( + Path(__file__).parent.parent.parent.parent.parent + ) + """A ZEPHYR_BASE test runners may explicitly set.""" + + RES_BASE = os.path.join(Path(__file__).parent.parent, "tests", "res") + """Unit tests resource files directory.""" + + ANON_ZEPHYR_BASE = "/path/to/zephyr_base" + """Anonymized ZEPHYR_BASE that appears in CMake cache files.""" + + ANON_ZEPHYR_SDK = "/path/to/zephyr-sdk" + """Anonymized Zephyr SDK path that appears in CMake cache files.""" + + ANON_GNUARMEMB = "/path/to/gnuarmemb" + """Anonymized Arm toolchain that path appears in CMake cache files.""" + + ZEPHYR_VERSION = "3.4.99" + """Zephyr project version that appears in CMake cache files.""" + + BOARD = "nrf52840dk_nrf52840" + """The unit tests board model.""" + + _sample_edt: Optional[edtlib.EDT] = None + _sample_dtmodel: Optional[DTModel] = None + + @classmethod + @contextlib.contextmanager + def mock_env( + cls, tmpenv: Dict[str, Optional[str]] + ) -> Generator[None, None, None]: + """Temporarily change the OS environment. + + None values will unset the variables. + + with mock_env({"ZEPHYR_BASE": None, "NAME": "VALUE"}): + # Unit test for API behaviors that depend on environment variables. + + Args: + tmpenv: The OS environment changes. + """ + # Save the environment subset the mock will change or unset the values of. + diffenv = {name: os.environ.get(name) for name in tmpenv} + for name, value in tmpenv.items(): + if value is not None: + # Change or add environment values. + os.environ[name] = value + elif name in os.environ: + # Unset defined environment variables. + del os.environ[name] + + try: + # Work with temp OS environment. + yield + + finally: + for name, value in diffenv.items(): + if value is not None: + # Restore the environment subset the mock had changed + # the values of. + os.environ[name] = value + elif name in os.environ: + # Unset the environment subset the mock added. + del os.environ[name] + + @classmethod + @contextlib.contextmanager + def from_res(cls) -> Generator[None, None, None]: + """Temporarily change the working directory to the resources location. + + Implementation idea borrowed from the "from_here()" pattern + in Zephyr's python-devicetree (test_edtlib.py). + """ + cwd = os.getcwd() + try: + os.chdir(cls.RES_BASE) + yield + finally: + os.chdir(cwd) + + @classmethod + @contextlib.contextmanager + def from_there(cls, *name: str) -> Generator[None, None, None]: + """Temporarily change the working directory.""" + cwd = os.getcwd() + try: + os.chdir(os.path.join(cls.RES_BASE, *name)) + yield + finally: + os.chdir(cwd) + + @classmethod + def get_resource_path(cls, *name: str) -> str: + """Translate path name components to an absolute resource file path. + + Args: + *name: Path components. + + Returns: + The absolute path to the resource file. + """ + return os.path.join(cls.RES_BASE, *name) + + @classmethod + def get_sample_edt(cls, force_reload: bool = False) -> edtlib.EDT: + """Get the sample Devicetree model (EDT). + + Args: + force_reload: Force reloading the model (default is to return + a cached model). + + Returns: + The sample Devicetree model (EDT). + """ + if force_reload: + return cls._read_sample_edt() + if not cls._sample_edt: + cls._sample_edt = cls._read_sample_edt() + return cls._sample_edt + + @classmethod + def get_sample_dtmodel(cls, force_reload: bool = False) -> DTModel: + """Get the sample Devicetree model (DTModel). + + Args: + force_reload: Force reloading the model (default is to return + a cached model). + + Returns: + The sample Devicetree model (DTModel). + """ + if force_reload: + return cls._read_sample_dtmodel() + if not cls._sample_dtmodel: + cls._sample_dtmodel = cls._read_sample_dtmodel() + return cls._sample_dtmodel + + @classmethod + def check_option_meta(cls, opt: DTShOption) -> None: + """Check meta-data all options must provide. + + Args: + opt: The shell option under test. + """ + assert opt.shortname or opt.longname + if opt.shortname: + assert 1 == len(opt.shortname) + if opt.longname: + assert 1 < len(opt.longname) + assert opt.brief + + @classmethod + def check_flag(cls, flag: DTShFlag) -> None: + """Check flag meta-data and state life-cycle. + + The flag is cleared on return. + + Args: + flag: The shell flag under test. + """ + cls.check_option_meta(flag) + + flag.reset() + assert not flag.isset + flag.parsed() + assert flag.isset + flag.reset() + + @classmethod + def check_arg(cls, arg: DTShArg, parsed: str) -> None: + """Check argument meta-data and state life-cycle. + + The argument value parsed is retained on return. + + Args: + arg: The shell argument under test. + parsed: A valid argument to parse. + """ + cls.check_option_meta(arg) + + arg.reset() + assert not arg.isset + assert arg.raw is None + + arg.parsed(parsed) + assert arg.isset + assert parsed == arg.raw + + @classmethod + def get_param_placeholder(cls, param: DTShParameter) -> List[str]: + """Get a parameter placeholder with valid multiplicity.""" + if param.multiplicity in ["?", "*"]: + return [] + if param.multiplicity == "+": + return ["value"] + if isinstance(param.multiplicity, int): + n: int = param.multiplicity + return n * ["value"] + raise ValueError(param.multiplicity) + + @classmethod + def get_param_placeholder_inval(cls, multiplicity: int) -> List[str]: + """Get a parameter placeholder with invalid multiplicity.""" + n_inval: int = multiplicity - 1 + return n_inval * ["param"] + + @classmethod + def check_param(cls, param: DTShParameter) -> None: + """Check parameter's life-cycle and multiplicity. + + The parameter is reset on return. + + Args: + The parameter to test. + """ + assert param.multiplicity in ["*", "?", "+"] or isinstance( + param.multiplicity, int + ) + if isinstance(param.multiplicity, int): + assert param.multiplicity > 0 + + assert [] == param.raw + values = cls.get_param_placeholder(param) + param.parsed(values) + assert values == param.raw + param.reset() + assert [] == param.raw + + if param.multiplicity == "?": + # Allows zero or one value. + param.parsed([]) + assert [] == param.raw + param.parsed(["value"]) + assert ["value"] == param.raw + # Fails with more than one value. + with pytest.raises(DTShError): + param.parsed(["param", "param"]) + + elif param.multiplicity == "+": + # Fails without value. + with pytest.raises(DTShError): + param.parsed([]) + # Allows more than one value. + param.parsed(["value"]) + assert ["value"] == param.raw + param.parsed(["value", "value"]) + assert ["value", "value"] == param.raw + + elif isinstance(param.multiplicity, int): + # Fails when the number of values does not match multiplicity. + with pytest.raises(DTShError): + param.parsed( + cls.get_param_placeholder_inval(param.multiplicity) + ) + + param.reset() + + @classmethod + def check_cmd_meta(cls, cmd: DTShCommand, name: str) -> None: + """Check meta-data all options must provide. + + Args: + opt: The shell option under test. + """ + assert name == cmd.name + assert cmd.brief + + assert DTShCommand(name, "", [], None) == cmd + assert DTShCommand("other", "", [], None) != cmd + + @classmethod + def check_cmd_flags( + cls, cmd: DTShCommand, sh: DTSh, out: DTShOutput + ) -> None: + """Check flags state upon command execution. + + On return, all the command's flags are set. + + Args: + cmd: The command under test. + sh: The shell that will execute the command. + out: An output stream. + """ + # Retrieve all command flags. + flags: List[DTShFlag] = [ + opt for opt in cmd.options if isinstance(opt, DTShFlag) + ] + + # Assert all flags are initially unset. + cmd.reset() + for flag in flags: + assert not flag.isset + assert not cmd.with_flag(flag.__class__) + + # Parse command arguments that set all flags under test. + argv: List[str] = [ + f"-{flag.shortname}" if flag.shortname else f"--{flag.longname}" + for flag in flags + # Would raise DTShUsageError to trigger command help. + if not isinstance(flag, DTShFlagHelp) + ] + if cmd.param: + argv += cls.get_param_placeholder(cmd.param) + + try: + cmd.execute(argv, sh, out) + except DTShCommandError as e: + # We won't craft valid parameter values, + # we just want to parse flags. + assert not isinstance(e, DTShUsageError) + + # Assert all flags are now set. + for flag in flags: + if not isinstance(flag, DTShFlagHelp): + assert flag.isset + assert cmd.with_flag(flag.__class__) + + @classmethod + def check_cmd_arg_order_by( + cls, cmd: DTShCommand, sh: DTSh, out: DTShOutput + ) -> None: + """Check "--order-by KEY" argument upon command execution. + + Test: + - the expected sorter argument for all valid keys + - expect DTShUsageError for invalid keys + + Args: + cmd: The command under test. + sh: The context shell. + out: An output stream. + """ + arg = cmd.with_arg(DTShArgOrderBy) + + for key in DTSH_NODE_ORDER_BY: + cmd.execute(["--order-by", key], sh, out) + assert DTSH_NODE_ORDER_BY[key].sorter is arg.sorter + assert ( + DTSH_NODE_ORDER_BY[key].sorter + is cmd.with_arg(DTShArgOrderBy).sorter + ) + + with pytest.raises(DTShUsageError): + cmd.execute(["--order-by", "not a key"], sh, out) + + @classmethod + def check_cmd_arg_fixed_depth( + cls, cmd: DTShCommand, sh: DTSh, out: DTShOutput + ) -> None: + """Check "--fixed-depth DEPTH" argument upon command execution. + + Test: + - the expected depth for valid argument values + - expect DTShUsageError for invalid argument values + + Args: + cmd: The command under test. + sh: The context shell. + out: An output stream. + """ + arg = cmd.with_arg(DTShArgFixedDepth) + + cmd.execute(["--fixed-depth", "2"], sh, out) + assert arg.isset + assert 2 == arg.depth + assert 2 == cmd.with_arg(DTShArgFixedDepth).depth + + with pytest.raises(DTShUsageError): + cmd.execute(["--fixed-depth", "not an int"], sh, out) + with pytest.raises(DTShUsageError): + # Negative values not allowed. + cmd.execute(["--fixed-depth", "-2"], sh, out) + + @classmethod + def check_cmd_arg_longfmt( + cls, cmd: DTShCommand, sh: DTSh, out: DTShOutput + ) -> None: + """Check "--format FMT" argument when executing the command. + + Test: + - the expected fields argument for all valid specifiers + - expect DTShUsageError for invalid format strings + + Args: + cmd: The command under test. + sh: The shell that will execute the command. + out: An output stream. + """ + arg = cmd.with_arg(DTShArgLongFmt) + + for key, spec in DTSH_NODE_FMT_SPEC.items(): + cmd.execute(["--format", key], sh, out) + assert arg.fmt + assert spec.col is arg.fmt[0] + + all_fmt = "".join(spec for spec in DTSH_NODE_FMT_SPEC) + cmd.execute(["--format", all_fmt], sh, out) + assert [DTSH_NODE_FMT_SPEC[spec].col for spec in all_fmt] == arg.fmt + + with pytest.raises(DTShCommandError): + cmd.execute(["--format", "invalid format string"], sh, out) + + @classmethod + def check_cmd_param_dtpath( + cls, cmd: DTShCommand, sh: DTSh, out: DTShOutput + ) -> None: + """Check the PATH parameter upon command execution. + + Will test: + - parameter's state and multiplicity + - invalid parameter values (i.e. invalid paths) + + The metadata and parameter specific properties of DTShParamDTPath + are tested in test_dtsh_shellutils.py. + + Args: + cmd: The command under test, that does not support path expansion. + sh: The context shell. + out: An output stream. + """ + param = cmd.with_param(DTShParamDTPath) + + # Parameter's state and multiplicity. + cls.check_param(param) + + with pytest.raises(DTShCommandError): + # Path not found. + cmd.execute(["/not/a/node"], sh, out) + with pytest.raises(DTShCommandError): + # No path expansion: despite "/leds" exists, this should fail. + cmd.execute(["/leds*"], sh, out) + + @classmethod + def check_cmd_param_dtpaths( + cls, cmd: DTShCommand, sh: DTSh, out: DTShOutput + ) -> None: + """Check the PATHs parameter upon command execution. + + Will test: + - parameter's state and multiplicity + - invalid parameter values (i.e. invalid paths) + + The metadata and parameter specific properties of DTShParamDTPaths + are tested in test_dtsh_shellutils.py. + + Args: + cmd: The command under test, that does support path expansion. + sh: The context shell. + out: An output stream. + """ + param = cmd.with_param(DTShParamDTPaths) + + # Parameter's state and multiplicity. + cls.check_param(param) + + with pytest.raises(DTShCommandError): + # Path not found. + cmd.execute(["/not/a/node"], sh, out) + with pytest.raises(DTShCommandError): + # Empty expansions are errors. + cmd.execute(["/soc/empty*"], sh, out) + + @classmethod + def check_cmd_param_alias( + cls, cmd: DTShCommand, sh: DTSh, out: DTShOutput + ) -> None: + """Check the ALIAS parameter upon command execution. + + Will test: + - parameter's state and multiplicity + - invalid parameter values (i.e. invalid alias names) + + The metadata and parameter specific properties of DTShParamAlias + are tested in test_dtsh_shellutils.py. + + Args: + cmd: The command under test, that does support path expansion. + sh: The context shell. + out: An output stream. + """ + param = cmd.with_param(DTShParamAlias) + + # Parameter's state and multiplicity. + cls.check_param(param) + + alias = "led0" + cmd.execute([alias], sh, out) + assert alias == param.alias + assert alias == cmd.with_param(DTShParamAlias).alias + + # Should not fail: the parameter is interpreted as a search string. + cmd.execute(["not-an-alias"], sh, out) + + @classmethod + def check_cmd_param_chosen( + cls, cmd: DTShCommand, sh: DTSh, out: DTShOutput + ) -> None: + """Check the CHOSEN parameter upon command execution. + + Will test: + - parameter's state and multiplicity + - invalid parameter values (i.e. invalid chosen names) + + The metadata and parameter specific properties of DTShParamChosen + are tested in test_dtsh_shellutils.py. + + Args: + cmd: The command under test, that does support path expansion. + sh: The context shell. + out: An output stream. + """ + param = cmd.with_param(DTShParamChosen) + + # Parameter's state and multiplicity. + cls.check_param(param) + assert "" == param.chosen + + # Must be a valid choice. + chosen = "zephyr,entropy" + cmd.execute([chosen], sh, out) + assert chosen == param.chosen + assert chosen == cmd.with_param(DTShParamChosen).chosen + + # Should not fail: the parameter is interpreted as a search string. + cmd.execute(["not-a-chosen"], sh, out) + + @classmethod + def check_cmd_execute( + cls, cmd: DTShCommand, sh: DTSh, out: DTShOutput + ) -> None: + """Check common exception cases when executing a command. + + Test: + - triggering help + - expect DTShUsageError if undefined option + - expect DTShUsageError if invalid parameter multiplicity + + Args: + cmd: The command under test. + sh: The shell that will execute the command. + out: An output stream. + """ + # Trigger help. + with pytest.raises(DTShUsageError): + cmd.execute(["-h"], sh, out) + assert cmd.with_flag(DTShFlagHelp) + + with pytest.raises(DTShUsageError): + cmd.execute(["--not-an-option"], sh, out) + + if cmd.param: + # When can test multiplicity with string parameters since these + # should fail before the actual parameter type/value is invovled. + multiplicity = cmd.param.multiplicity + + if multiplicity == "?": + # 0 <= N <= 1 + with pytest.raises(DTShUsageError): + cmd.execute(["value", "value"], sh, out) + + elif multiplicity == "+": + # N = 1 + with pytest.raises(DTShUsageError): + cmd.execute([], sh, out) + with pytest.raises(DTShUsageError): + cmd.execute(["value", "value"], sh, out) + + elif isinstance(multiplicity, int): + param_values = cls.get_param_placeholder_inval(multiplicity) + with pytest.raises(DTShUsageError): + cmd.execute(param_values, sh, out) + + @classmethod + def _read_sample_edt(cls) -> edtlib.EDT: + with cls.from_res(): + return edtlib.EDT( + dts="zephyr.dts", + bindings_dirs=[ + os.path.join(cls.ZEPHYR_BASE, "dts", "bindings") + ], + ) + + @classmethod + def _read_sample_dtmodel(cls) -> DTModel: + with cls.from_res(): + return DTModel.create( + dts_path="zephyr.dts", + binding_dirs=[os.path.join(cls.ZEPHYR_BASE, "dts", "bindings")], + ) diff --git a/tests/res/Build_gnuarm/CMakeCache.txt b/tests/res/Build_gnuarm/CMakeCache.txt new file mode 100644 index 0000000..26722fb --- /dev/null +++ b/tests/res/Build_gnuarm/CMakeCache.txt @@ -0,0 +1,572 @@ +# This is the CMakeCache file. +# For build in directory: /path/to/zephyr_base/samples/sensor/bme680/build +# It was generated by CMake: /usr/bin/cmake +# You can edit this file to change values found and used by cmake. +# If you do not want to change any of the values, simply exit the editor. +# If you do want to change a value, simply edit, save, and exit the editor. +# The syntax for the file is as follows: +# KEY:TYPE=VALUE +# KEY is the name of a variable in the cache. +# TYPE is a hint to GUIs for the type of VALUE, DO NOT EDIT TYPE!. +# VALUE is the current value for the KEY. + +######################## +# EXTERNAL cache entries +######################## + +//Application Binary Directory +APPLICATION_BINARY_DIR:PATH=/path/to/zephyr_base/samples/sensor/bme680/build + +//Application Source Directory +APPLICATION_SOURCE_DIR:PATH=/path/to/zephyr_base/samples/sensor/bme680 + +//Selected board +BOARD:STRING=nrf52840dk_nrf52840 + +//Path to a file. +BOARD_DIR:PATH=/path/to/zephyr_base/boards/arm/nrf52840dk_nrf52840 + +//Support board extensions +BOARD_EXTENSIONS:BOOL=ON + +//Path to a program. +BOSSAC:FILEPATH=BOSSAC-NOTFOUND + +//Kernel binary file +BYPRODUCT_KERNEL_BIN_NAME:FILEPATH=/path/to/zephyr_base/samples/sensor/bme680/build/zephyr/zephyr.bin + +//Kernel elf file +BYPRODUCT_KERNEL_ELF_NAME:FILEPATH=/path/to/zephyr_base/samples/sensor/bme680/build/zephyr/zephyr.elf + +//Kernel hex file +BYPRODUCT_KERNEL_HEX_NAME:FILEPATH=/path/to/zephyr_base/samples/sensor/bme680/build/zephyr/zephyr.hex + +//Selected board +CACHED_BOARD:STRING=nrf52840dk_nrf52840 + +//If desired, you can build the application usingthe configuration +// settings specified in an alternate .conf file using this parameter. +// These settings will override the settings in the application’s +// .config file or its default .conf file.Multiple files may be +// listed, e.g. CONF_FILE="prj1.conf;prj2.conf" The CACHED_CONF_FILE +// is internal Zephyr variable used between CMake runs. To change +// CONF_FILE, use the CONF_FILE variable. +CACHED_CONF_FILE:STRING=/path/to/zephyr_base/samples/sensor/bme680/prj.conf + +//Selected shield +CACHED_SHIELD:STRING= + +//Selected snippet +CACHED_SNIPPET:STRING= + +//Path to a program. +CCACHE_FOUND:FILEPATH=/usr/bin/ccache + +//Path to a program. +CMAKE_ADDR2LINE:FILEPATH=/path/to/gnuarmemb/bin/arm-none-eabi-addr2line + +//Path to a program. +CMAKE_AR:FILEPATH=/path/to/gnuarmemb/bin/arm-none-eabi-ar + +//Path to a program. +CMAKE_AS:FILEPATH=/path/to/gnuarmemb/bin/arm-none-eabi-as + +//ASM compiler +CMAKE_ASM_COMPILER:FILEPATH=/path/to/gnuarmemb/bin/arm-none-eabi-gcc + +//A wrapper around 'ar' adding the appropriate '--plugin' option +// for the GCC compiler +CMAKE_ASM_COMPILER_AR:FILEPATH=/path/to/gnuarmemb/bin/arm-none-eabi-gcc-ar + +//A wrapper around 'ranlib' adding the appropriate '--plugin' option +// for the GCC compiler +CMAKE_ASM_COMPILER_RANLIB:FILEPATH=/path/to/gnuarmemb/bin/arm-none-eabi-gcc-ranlib + +//Flags used by the ASM compiler during all build types. +CMAKE_ASM_FLAGS:STRING= + +//Flags used by the ASM compiler during DEBUG builds. +CMAKE_ASM_FLAGS_DEBUG:STRING=-g + +//Flags used by the ASM compiler during MINSIZEREL builds. +CMAKE_ASM_FLAGS_MINSIZEREL:STRING=-Os -DNDEBUG + +//Flags used by the ASM compiler during RELEASE builds. +CMAKE_ASM_FLAGS_RELEASE:STRING=-O3 -DNDEBUG + +//Flags used by the ASM compiler during RELWITHDEBINFO builds. +CMAKE_ASM_FLAGS_RELWITHDEBINFO:STRING=-O2 -g -DNDEBUG + +//Choose the type of build, options are: None Debug Release RelWithDebInfo +// MinSizeRel ... +CMAKE_BUILD_TYPE:STRING= + +//Enable/Disable color output during build. +CMAKE_COLOR_MAKEFILE:BOOL=ON + +//CXX compiler +CMAKE_CXX_COMPILER:STRING=/path/to/gnuarmemb/bin/arm-none-eabi-g++ + +//A wrapper around 'ar' adding the appropriate '--plugin' option +// for the GCC compiler +CMAKE_CXX_COMPILER_AR:FILEPATH=/path/to/gnuarmemb/bin/arm-none-eabi-gcc-ar + +//A wrapper around 'ranlib' adding the appropriate '--plugin' option +// for the GCC compiler +CMAKE_CXX_COMPILER_RANLIB:FILEPATH=/path/to/gnuarmemb/bin/arm-none-eabi-gcc-ranlib + +//Flags used by the CXX compiler during all build types. +CMAKE_CXX_FLAGS:STRING= + +//Flags used by the CXX compiler during DEBUG builds. +CMAKE_CXX_FLAGS_DEBUG:STRING=-g + +//Flags used by the CXX compiler during MINSIZEREL builds. +CMAKE_CXX_FLAGS_MINSIZEREL:STRING=-Os -DNDEBUG + +//Flags used by the CXX compiler during RELEASE builds. +CMAKE_CXX_FLAGS_RELEASE:STRING=-O3 -DNDEBUG + +//Flags used by the CXX compiler during RELWITHDEBINFO builds. +CMAKE_CXX_FLAGS_RELWITHDEBINFO:STRING=-O2 -g -DNDEBUG + +//C compiler +CMAKE_C_COMPILER:STRING=/path/to/gnuarmemb/bin/arm-none-eabi-gcc + +//A wrapper around 'ar' adding the appropriate '--plugin' option +// for the GCC compiler +CMAKE_C_COMPILER_AR:FILEPATH=/path/to/gnuarmemb/bin/arm-none-eabi-gcc-ar + +//A wrapper around 'ranlib' adding the appropriate '--plugin' option +// for the GCC compiler +CMAKE_C_COMPILER_RANLIB:FILEPATH=/path/to/gnuarmemb/bin/arm-none-eabi-gcc-ranlib + +//Flags used by the C compiler during all build types. +CMAKE_C_FLAGS:STRING= + +//Flags used by the C compiler during DEBUG builds. +CMAKE_C_FLAGS_DEBUG:STRING=-g + +//Flags used by the C compiler during MINSIZEREL builds. +CMAKE_C_FLAGS_MINSIZEREL:STRING=-Os -DNDEBUG + +//Flags used by the C compiler during RELEASE builds. +CMAKE_C_FLAGS_RELEASE:STRING=-O3 -DNDEBUG + +//Flags used by the C compiler during RELWITHDEBINFO builds. +CMAKE_C_FLAGS_RELWITHDEBINFO:STRING=-O2 -g -DNDEBUG + +//Path to a program. +CMAKE_DLLTOOL:FILEPATH=CMAKE_DLLTOOL-NOTFOUND + +//Flags used by the linker during all build types. +CMAKE_EXE_LINKER_FLAGS:STRING= + +//Flags used by the linker during DEBUG builds. +CMAKE_EXE_LINKER_FLAGS_DEBUG:STRING= + +//Flags used by the linker during MINSIZEREL builds. +CMAKE_EXE_LINKER_FLAGS_MINSIZEREL:STRING= + +//Flags used by the linker during RELEASE builds. +CMAKE_EXE_LINKER_FLAGS_RELEASE:STRING= + +//Flags used by the linker during RELWITHDEBINFO builds. +CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO:STRING= + +//Export CMake compile commands. Used by gen_app_partitions.py +// script +CMAKE_EXPORT_COMPILE_COMMANDS:BOOL=TRUE + +//Path to a program. +CMAKE_GCOV:FILEPATH=/path/to/gnuarmemb/bin/arm-none-eabi-gcov + +//Path to a program. +CMAKE_GDB:FILEPATH=/path/to/gnuarmemb/bin/arm-none-eabi-gdb + +//Path to a program. +CMAKE_GDB_NO_PY:FILEPATH=/path/to/gnuarmemb/bin/arm-none-eabi-gdb + +//Install path prefix, prepended onto install directories. +CMAKE_INSTALL_PREFIX:PATH=/usr/local + +//Path to a program. +CMAKE_MAKE_PROGRAM:FILEPATH=/usr/bin/gmake + +//Flags used by the linker during the creation of modules during +// all build types. +CMAKE_MODULE_LINKER_FLAGS:STRING= + +//Flags used by the linker during the creation of modules during +// DEBUG builds. +CMAKE_MODULE_LINKER_FLAGS_DEBUG:STRING= + +//Flags used by the linker during the creation of modules during +// MINSIZEREL builds. +CMAKE_MODULE_LINKER_FLAGS_MINSIZEREL:STRING= + +//Flags used by the linker during the creation of modules during +// RELEASE builds. +CMAKE_MODULE_LINKER_FLAGS_RELEASE:STRING= + +//Flags used by the linker during the creation of modules during +// RELWITHDEBINFO builds. +CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO:STRING= + +//Path to a program. +CMAKE_NM:FILEPATH=/path/to/gnuarmemb/bin/arm-none-eabi-nm + +//Path to a program. +CMAKE_OBJCOPY:FILEPATH=/path/to/gnuarmemb/bin/arm-none-eabi-objcopy + +//Path to a program. +CMAKE_OBJDUMP:FILEPATH=/path/to/gnuarmemb/bin/arm-none-eabi-objdump + +//Value Computed by CMake +CMAKE_PROJECT_DESCRIPTION:STATIC= + +//Value Computed by CMake +CMAKE_PROJECT_HOMEPAGE_URL:STATIC= + +//Value Computed by CMake +CMAKE_PROJECT_NAME:STATIC=bme680 + +//Value Computed by CMake +CMAKE_PROJECT_VERSION:STATIC=3.4.99 + +//Value Computed by CMake +CMAKE_PROJECT_VERSION_MAJOR:STATIC=3 + +//Value Computed by CMake +CMAKE_PROJECT_VERSION_MINOR:STATIC=4 + +//Value Computed by CMake +CMAKE_PROJECT_VERSION_PATCH:STATIC=99 + +//Value Computed by CMake +CMAKE_PROJECT_VERSION_TWEAK:STATIC= + +//Path to a program. +CMAKE_RANLIB:FILEPATH=/path/to/gnuarmemb/bin/arm-none-eabi-ranlib + +//Path to a program. +CMAKE_READELF:FILEPATH=/path/to/gnuarmemb/bin/arm-none-eabi-readelf + +//Flags used by the linker during the creation of shared libraries +// during all build types. +CMAKE_SHARED_LINKER_FLAGS:STRING= + +//Flags used by the linker during the creation of shared libraries +// during DEBUG builds. +CMAKE_SHARED_LINKER_FLAGS_DEBUG:STRING= + +//Flags used by the linker during the creation of shared libraries +// during MINSIZEREL builds. +CMAKE_SHARED_LINKER_FLAGS_MINSIZEREL:STRING= + +//Flags used by the linker during the creation of shared libraries +// during RELEASE builds. +CMAKE_SHARED_LINKER_FLAGS_RELEASE:STRING= + +//Flags used by the linker during the creation of shared libraries +// during RELWITHDEBINFO builds. +CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO:STRING= + +//If set, runtime paths are not added when installing shared libraries, +// but are added when building. +CMAKE_SKIP_INSTALL_RPATH:BOOL=NO + +//If set, runtime paths are not added when using shared libraries. +CMAKE_SKIP_RPATH:BOOL=NO + +//Flags used by the linker during the creation of static libraries +// during all build types. +CMAKE_STATIC_LINKER_FLAGS:STRING= + +//Flags used by the linker during the creation of static libraries +// during DEBUG builds. +CMAKE_STATIC_LINKER_FLAGS_DEBUG:STRING= + +//Flags used by the linker during the creation of static libraries +// during MINSIZEREL builds. +CMAKE_STATIC_LINKER_FLAGS_MINSIZEREL:STRING= + +//Flags used by the linker during the creation of static libraries +// during RELEASE builds. +CMAKE_STATIC_LINKER_FLAGS_RELEASE:STRING= + +//Flags used by the linker during the creation of static libraries +// during RELWITHDEBINFO builds. +CMAKE_STATIC_LINKER_FLAGS_RELWITHDEBINFO:STRING= + +//Path to a program. +CMAKE_STRIP:FILEPATH=/path/to/gnuarmemb/bin/arm-none-eabi-strip + +//If this value is on, makefiles will be generated without the +// .SILENT directive, and all commands will be echoed to the console +// during the make. This is useful for debugging only. With Visual +// Studio IDE projects all commands are done without /nologo. +CMAKE_VERBOSE_MAKEFILE:BOOL=FALSE + +//Path to a program. +DTC:FILEPATH=/usr/bin/dtc + +//If desired, you can build the application using the DT configuration +// settings specified in an alternate .overlay file using this +// parameter. These settings will override the settings in the +// board's .dts file. Multiple files may be listed, e.g. DTC_OVERLAY_FILE="dts1.overlay +// dts2.overlay" +DTC_OVERLAY_FILE:STRING=/path/to/zephyr_base/samples/sensor/bme680/boards/nrf52840dk_nrf52840.overlay + +//Linker BFD compatibility (compiler reported) +GNULD_LINKER_IS_BFD:BOOL=ON + +//GNU ld version +GNULD_VERSION_STRING:STRING=2.38.20220708 + +//Path to a program. +GPERF:FILEPATH=/usr/bin/gperf + +//Path to a program. +IMGTOOL:FILEPATH=/path/to/.venv/bin/imgtool + +//nrfx Directory +NRFX_DIR:PATH=/path/to/modules/hal/nordic/nrfx + +//Path to a program. +OPENOCD:FILEPATH=OPENOCD-NOTFOUND + +//Path to a program. +PAHOLE:FILEPATH=PAHOLE-NOTFOUND + +//Path to a program. +PUNCOVER:FILEPATH=PUNCOVER-NOTFOUND + +//Value Computed by CMake +Picolibc_BINARY_DIR:STATIC=/path/to/zephyr_base/samples/sensor/bme680/build/modules/picolibc + +//Value Computed by CMake +Picolibc_SOURCE_DIR:STATIC=/path/to/modules/lib/picolibc + +//True if toolchain supports newlib +TOOLCHAIN_HAS_NEWLIB:BOOL=ON + +//Zephyr toolchain root +TOOLCHAIN_ROOT:STRING=/path/to/zephyr_base + +//No help, variable specified on the command line. +WEST_PYTHON:UNINITIALIZED=/path/to/.venv/bin/python3.11 + +//Zephyr base +ZEPHYR_BASE:PATH=/path/to/zephyr_base + +//Path to Zephyr git repository index file +ZEPHYR_GIT_DIR:PATH=/path/to/zephyr_base/.git/index + +//Path to Zephyr git repository index file +ZEPHYR_GIT_INDEX:PATH=ZEPHYR_GIT_INDEX-NOTFOUND + +//Value Computed by CMake +Zephyr-Kernel_BINARY_DIR:STATIC=/path/to/zephyr_base/samples/sensor/bme680/build + +//Value Computed by CMake +Zephyr-Kernel_SOURCE_DIR:STATIC=/path/to/zephyr_base/samples/sensor/bme680 + +//The directory containing a CMake configuration file for ZephyrAppConfiguration. +ZephyrAppConfiguration_DIR:PATH=ZephyrAppConfiguration_DIR-NOTFOUND + +//The directory containing a CMake configuration file for ZephyrBuildConfiguration. +ZephyrBuildConfiguration_DIR:PATH=ZephyrBuildConfiguration_DIR-NOTFOUND + +//The directory containing a CMake configuration file for Zephyr. +Zephyr_DIR:PATH=/path/to/zephyr_base/share/zephyr-package/cmake + +//Value Computed by CMake +bme680_BINARY_DIR:STATIC=/path/to/zephyr_base/samples/sensor/bme680/build + +//Value Computed by CMake +bme680_SOURCE_DIR:STATIC=/path/to/zephyr_base/samples/sensor/bme680 + + +######################## +# INTERNAL cache entries +######################## + +//The application configuration folder +APPLICATION_CONFIG_DIR:INTERNAL=/path/to/zephyr_base/samples/sensor/bme680 +//DT bindings root directories +CACHED_DTS_ROOT_BINDINGS:INTERNAL=/path/to/zephyr_base/dts/bindings +//ADVANCED property for variable: CMAKE_ADDR2LINE +CMAKE_ADDR2LINE-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_AR +CMAKE_AR-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_ASM_COMPILER +CMAKE_ASM_COMPILER-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_ASM_COMPILER_AR +CMAKE_ASM_COMPILER_AR-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_ASM_COMPILER_RANLIB +CMAKE_ASM_COMPILER_RANLIB-ADVANCED:INTERNAL=1 +CMAKE_ASM_COMPILER_WORKS:INTERNAL=1 +//ADVANCED property for variable: CMAKE_ASM_FLAGS +CMAKE_ASM_FLAGS-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_ASM_FLAGS_DEBUG +CMAKE_ASM_FLAGS_DEBUG-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_ASM_FLAGS_MINSIZEREL +CMAKE_ASM_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_ASM_FLAGS_RELEASE +CMAKE_ASM_FLAGS_RELEASE-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_ASM_FLAGS_RELWITHDEBINFO +CMAKE_ASM_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 +//This is the directory where this CMakeCache.txt was created +CMAKE_CACHEFILE_DIR:INTERNAL=/path/to/zephyr_base/samples/sensor/bme680/build +//Major version of cmake used to create the current loaded cache +CMAKE_CACHE_MAJOR_VERSION:INTERNAL=3 +//Minor version of cmake used to create the current loaded cache +CMAKE_CACHE_MINOR_VERSION:INTERNAL=20 +//Patch version of cmake used to create the current loaded cache +CMAKE_CACHE_PATCH_VERSION:INTERNAL=2 +//ADVANCED property for variable: CMAKE_COLOR_MAKEFILE +CMAKE_COLOR_MAKEFILE-ADVANCED:INTERNAL=1 +//Path to CMake executable. +CMAKE_COMMAND:INTERNAL=/usr/bin/cmake +//Path to cpack program executable. +CMAKE_CPACK_COMMAND:INTERNAL=/usr/bin/cpack +//Path to ctest program executable. +CMAKE_CTEST_COMMAND:INTERNAL=/usr/bin/ctest +//ADVANCED property for variable: CMAKE_CXX_COMPILER +CMAKE_CXX_COMPILER-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_CXX_COMPILER_AR +CMAKE_CXX_COMPILER_AR-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_CXX_COMPILER_RANLIB +CMAKE_CXX_COMPILER_RANLIB-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_CXX_FLAGS +CMAKE_CXX_FLAGS-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_CXX_FLAGS_DEBUG +CMAKE_CXX_FLAGS_DEBUG-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_CXX_FLAGS_MINSIZEREL +CMAKE_CXX_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_CXX_FLAGS_RELEASE +CMAKE_CXX_FLAGS_RELEASE-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_CXX_FLAGS_RELWITHDEBINFO +CMAKE_CXX_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_C_COMPILER +CMAKE_C_COMPILER-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_C_COMPILER_AR +CMAKE_C_COMPILER_AR-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_C_COMPILER_RANLIB +CMAKE_C_COMPILER_RANLIB-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_C_FLAGS +CMAKE_C_FLAGS-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_C_FLAGS_DEBUG +CMAKE_C_FLAGS_DEBUG-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_C_FLAGS_MINSIZEREL +CMAKE_C_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_C_FLAGS_RELEASE +CMAKE_C_FLAGS_RELEASE-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_C_FLAGS_RELWITHDEBINFO +CMAKE_C_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_DLLTOOL +CMAKE_DLLTOOL-ADVANCED:INTERNAL=1 +//Path to cache edit program executable. +CMAKE_EDIT_COMMAND:INTERNAL=/usr/bin/ccmake +//Executable file format +CMAKE_EXECUTABLE_FORMAT:INTERNAL=ELF +//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS +CMAKE_EXE_LINKER_FLAGS-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS_DEBUG +CMAKE_EXE_LINKER_FLAGS_DEBUG-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS_MINSIZEREL +CMAKE_EXE_LINKER_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS_RELEASE +CMAKE_EXE_LINKER_FLAGS_RELEASE-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO +CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 +//Name of external makefile project generator. +CMAKE_EXTRA_GENERATOR:INTERNAL= +//Name of generator. +CMAKE_GENERATOR:INTERNAL=Unix Makefiles +//Generator instance identifier. +CMAKE_GENERATOR_INSTANCE:INTERNAL= +//Name of generator platform. +CMAKE_GENERATOR_PLATFORM:INTERNAL= +//Name of generator toolset. +CMAKE_GENERATOR_TOOLSET:INTERNAL= +//Source directory with the top level CMakeLists.txt file for this +// project +CMAKE_HOME_DIRECTORY:INTERNAL=/path/to/zephyr_base/samples/sensor/bme680 +//ADVANCED property for variable: CMAKE_MAKE_PROGRAM +CMAKE_MAKE_PROGRAM-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS +CMAKE_MODULE_LINKER_FLAGS-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS_DEBUG +CMAKE_MODULE_LINKER_FLAGS_DEBUG-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS_MINSIZEREL +CMAKE_MODULE_LINKER_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS_RELEASE +CMAKE_MODULE_LINKER_FLAGS_RELEASE-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO +CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_NM +CMAKE_NM-ADVANCED:INTERNAL=1 +//number of local generators +CMAKE_NUMBER_OF_MAKEFILES:INTERNAL=130 +//ADVANCED property for variable: CMAKE_OBJCOPY +CMAKE_OBJCOPY-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_OBJDUMP +CMAKE_OBJDUMP-ADVANCED:INTERNAL=1 +//Platform information initialized +CMAKE_PLATFORM_INFO_INITIALIZED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_RANLIB +CMAKE_RANLIB-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_READELF +CMAKE_READELF-ADVANCED:INTERNAL=1 +//Path to CMake installation. +CMAKE_ROOT:INTERNAL=/usr/share/cmake +//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS +CMAKE_SHARED_LINKER_FLAGS-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS_DEBUG +CMAKE_SHARED_LINKER_FLAGS_DEBUG-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS_MINSIZEREL +CMAKE_SHARED_LINKER_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS_RELEASE +CMAKE_SHARED_LINKER_FLAGS_RELEASE-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO +CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_SKIP_INSTALL_RPATH +CMAKE_SKIP_INSTALL_RPATH-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_SKIP_RPATH +CMAKE_SKIP_RPATH-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS +CMAKE_STATIC_LINKER_FLAGS-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS_DEBUG +CMAKE_STATIC_LINKER_FLAGS_DEBUG-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS_MINSIZEREL +CMAKE_STATIC_LINKER_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS_RELEASE +CMAKE_STATIC_LINKER_FLAGS_RELEASE-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS_RELWITHDEBINFO +CMAKE_STATIC_LINKER_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_STRIP +CMAKE_STRIP-ADVANCED:INTERNAL=1 +//uname command +CMAKE_UNAME:INTERNAL=/usr/bin/uname +//ADVANCED property for variable: CMAKE_VERBOSE_MAKEFILE +CMAKE_VERBOSE_MAKEFILE-ADVANCED:INTERNAL=1 +//Details about finding Dtc +FIND_PACKAGE_MESSAGE_DETAILS_Dtc:INTERNAL=[/usr/bin/dtc][v1.6.0(1.4.6)] +//Details about finding GnuLd +FIND_PACKAGE_MESSAGE_DETAILS_GnuLd:INTERNAL=[/path/to/gnuarmemb/bin/../lib/gcc/arm-none-eabi/11.3.1/../../../../arm-none-eabi/bin/ld.bfd][v2.38.20220708()] +//Details about finding Python3 +FIND_PACKAGE_MESSAGE_DETAILS_Python3:INTERNAL=[/path/to/.venv/bin/python3.11][cfound components: Interpreter ][v3.11.2(3.8)] +//Cached environment variable GNUARMEMB_TOOLCHAIN_PATH +GNUARMEMB_TOOLCHAIN_PATH:INTERNAL=/path/to/gnuarmemb +//West +WEST:INTERNAL=/path/to/.venv/bin/python3.11;-m;west +//a configuration file for the runners Python package +ZEPHYR_RUNNERS_YAML:INTERNAL=/path/to/zephyr_base/samples/sensor/bme680/build/zephyr/runners.yaml +//Cached environment variable ZEPHYR_TOOLCHAIN_VARIANT +ZEPHYR_TOOLCHAIN_VARIANT:INTERNAL=gnuarmemb +_Python3_EXECUTABLE:INTERNAL=/path/to/.venv/bin/python3.11 +//Python3 Properties +_Python3_INTERPRETER_PROPERTIES:INTERNAL=Python;3;11;2;64;;cpython-311-x86_64-linux-gnu;/usr/lib64/python3.11;/usr/lib64/python3.11;/path/to/.venv/lib/python3.11/site-packages;/path/to/.venv/lib64/python3.11/site-packages +_Python3_INTERPRETER_SIGNATURE:INTERNAL=2fbe5bcb206741dc7d34d11b11b11abf diff --git a/tests/res/Build_gnuarm/zephyr/zephyr.dts b/tests/res/Build_gnuarm/zephyr/zephyr.dts new file mode 100644 index 0000000..662ae6b --- /dev/null +++ b/tests/res/Build_gnuarm/zephyr/zephyr.dts @@ -0,0 +1,812 @@ +/dts-v1/; + +/ { + #address-cells = < 0x1 >; + #size-cells = < 0x1 >; + model = "Nordic nRF52840 DK NRF52840"; + compatible = "nordic,nrf52840-dk-nrf52840"; + chosen { + zephyr,entropy = &rng; + zephyr,flash-controller = &flash_controller; + zephyr,console = &uart0; + zephyr,shell-uart = &uart0; + zephyr,uart-mcumgr = &uart0; + zephyr,bt-mon-uart = &uart0; + zephyr,bt-c2h-uart = &uart0; + zephyr,sram = &sram0; + zephyr,flash = &flash0; + zephyr,code-partition = &slot0_partition; + zephyr,ieee802154 = &ieee802154; + }; + aliases { + led0 = &led0; + led1 = &led1; + led2 = &led2; + led3 = &led3; + pwm-led0 = &pwm_led0; + sw0 = &button0; + sw1 = &button1; + sw2 = &button2; + sw3 = &button3; + bootloader-led0 = &led0; + mcuboot-button0 = &button0; + mcuboot-led0 = &led0; + watchdog0 = &wdt0; + spi-flash0 = &mx25r64; + }; + soc { + #address-cells = < 0x1 >; + #size-cells = < 0x1 >; + compatible = "nordic,nrf52840-qiaa", "nordic,nrf52840", "nordic,nrf52", + "simple-bus"; + interrupt-parent = < &nvic >; + ranges; + nvic: interrupt-controller@e000e100 { + #address-cells = < 0x1 >; + compatible = "arm,v7m-nvic"; + reg = < 0xe000e100 0xc00 >; + interrupt-controller; + #interrupt-cells = < 0x2 >; + arm,num-irq-priority-bits = < 0x3 >; + phandle = < 0x1 >; + }; + systick: timer@e000e010 { + compatible = "arm,armv7m-systick"; + reg = < 0xe000e010 0x10 >; + status = "disabled"; + }; + ficr: ficr@10000000 { + compatible = "nordic,nrf-ficr"; + reg = < 0x10000000 0x1000 >; + status = "okay"; + }; + uicr: uicr@10001000 { + compatible = "nordic,nrf-uicr"; + reg = < 0x10001000 0x1000 >; + status = "okay"; + gpio-as-nreset; + }; + sram0: memory@20000000 { + compatible = "mmio-sram"; + reg = < 0x20000000 0x40000 >; + }; + clock: clock@40000000 { + compatible = "nordic,nrf-clock"; + reg = < 0x40000000 0x1000 >; + interrupts = < 0x0 0x1 >; + status = "okay"; + }; + power: power@40000000 { + compatible = "nordic,nrf-power"; + reg = < 0x40000000 0x1000 >; + interrupts = < 0x0 0x1 >; + status = "okay"; + #address-cells = < 0x1 >; + #size-cells = < 0x1 >; + gpregret1: gpregret1@4000051c { + #address-cells = < 0x1 >; + #size-cells = < 0x1 >; + compatible = "nordic,nrf-gpregret"; + reg = < 0x4000051c 0x1 >; + status = "okay"; + }; + gpregret2: gpregret2@40000520 { + #address-cells = < 0x1 >; + #size-cells = < 0x1 >; + compatible = "nordic,nrf-gpregret"; + reg = < 0x40000520 0x1 >; + status = "okay"; + }; + }; + radio: radio@40001000 { + compatible = "nordic,nrf-radio"; + reg = < 0x40001000 0x1000 >; + interrupts = < 0x1 0x1 >; + status = "okay"; + ieee802154-supported; + ble-2mbps-supported; + ble-coded-phy-supported; + tx-high-power-supported; + ieee802154: ieee802154 { + compatible = "nordic,nrf-ieee802154"; + status = "okay"; + }; + }; + uart0: uart@40002000 { + compatible = "nordic,nrf-uarte"; + reg = < 0x40002000 0x1000 >; + interrupts = < 0x2 0x1 >; + status = "okay"; + current-speed = < 0x1c200 >; + pinctrl-0 = < &uart0_default >; + pinctrl-1 = < &uart0_sleep >; + pinctrl-names = "default", "sleep"; + }; + i2c0: arduino_i2c: i2c@40003000 { + compatible = "nordic,nrf-twi"; + #address-cells = < 0x1 >; + #size-cells = < 0x0 >; + reg = < 0x40003000 0x1000 >; + clock-frequency = < 0x186a0 >; + interrupts = < 0x3 0x1 >; + easydma-maxcnt-bits = < 0x10 >; + interrupt-names = "IRQ_i2c0"; + status = "okay"; + pinctrl-0 = < &i2c0_default >; + pinctrl-1 = < &i2c0_sleep >; + pinctrl-names = "default", "sleep"; + bme680_i2c: bme680@76 { + compatible = "bosch,bme680"; + reg = < 0x76 >; + }; + }; + spi0: spi@40003000 { + compatible = "nordic,nrf-spi"; + #address-cells = < 0x1 >; + #size-cells = < 0x0 >; + reg = < 0x40003000 0x1000 >; + interrupts = < 0x3 0x1 >; + max-frequency = < 0x7a1200 >; + easydma-maxcnt-bits = < 0x10 >; + status = "disabled"; + pinctrl-0 = < &spi0_default >; + pinctrl-1 = < &spi0_sleep >; + pinctrl-names = "default", "sleep"; + }; + i2c1: i2c@40004000 { + compatible = "nordic,nrf-twi"; + #address-cells = < 0x1 >; + #size-cells = < 0x0 >; + reg = < 0x40004000 0x1000 >; + clock-frequency = < 0x186a0 >; + interrupts = < 0x4 0x1 >; + status = "disabled"; + pinctrl-0 = < &i2c1_default >; + pinctrl-1 = < &i2c1_sleep >; + pinctrl-names = "default", "sleep"; + }; + spi1: spi@40004000 { + compatible = "nordic,nrf-spi"; + #address-cells = < 0x1 >; + #size-cells = < 0x0 >; + reg = < 0x40004000 0x1000 >; + interrupts = < 0x4 0x1 >; + max-frequency = < 0x7a1200 >; + easydma-maxcnt-bits = < 0x10 >; + status = "okay"; + pinctrl-0 = < &spi1_default >; + pinctrl-1 = < &spi1_sleep >; + pinctrl-names = "default", "sleep"; + bme680_spi: bme680@0 { + compatible = "bosch,bme680"; + reg = < 0x0 >; + spi-max-frequency = < 0xf4240 >; + }; + }; + nfct: nfct@40005000 { + compatible = "nordic,nrf-nfct"; + reg = < 0x40005000 0x1000 >; + interrupts = < 0x5 0x1 >; + status = "okay"; + }; + gpiote: gpiote@40006000 { + compatible = "nordic,nrf-gpiote"; + reg = < 0x40006000 0x1000 >; + interrupts = < 0x6 0x5 >; + instance = <0>; + status = "okay"; + }; + adc: adc@40007000 { + compatible = "nordic,nrf-saadc"; + reg = < 0x40007000 0x1000 >; + interrupts = < 0x7 0x1 >; + status = "okay"; + #io-channel-cells = < 0x1 >; + phandle = < 0x1b >; + }; + timer0: timer@40008000 { + compatible = "nordic,nrf-timer"; + status = "disabled"; + reg = < 0x40008000 0x1000 >; + cc-num = < 0x4 >; + max-bit-width = < 0x20 >; + interrupts = < 0x8 0x1 >; + prescaler = < 0x0 >; + }; + timer1: timer@40009000 { + compatible = "nordic,nrf-timer"; + status = "disabled"; + reg = < 0x40009000 0x1000 >; + cc-num = < 0x4 >; + max-bit-width = < 0x20 >; + interrupts = < 0x9 0x1 >; + prescaler = < 0x0 >; + phandle = < 0x17 >; + }; + timer2: timer@4000a000 { + compatible = "nordic,nrf-timer"; + status = "disabled"; + reg = < 0x4000a000 0x1000 >; + cc-num = < 0x4 >; + max-bit-width = < 0x20 >; + interrupts = < 0xa 0x1 >; + prescaler = < 0x0 >; + }; + rtc0: rtc@4000b000 { + compatible = "nordic,nrf-rtc"; + reg = < 0x4000b000 0x1000 >; + cc-num = < 0x3 >; + interrupts = < 0xb 0x1 >; + status = "disabled"; + clock-frequency = < 0x8000 >; + prescaler = < 0x1 >; + }; + temp: temp@4000c000 { + compatible = "nordic,nrf-temp"; + reg = < 0x4000c000 0x1000 >; + interrupts = < 0xc 0x1 >; + status = "okay"; + }; + rng: random@4000d000 { + compatible = "nordic,nrf-rng"; + reg = < 0x4000d000 0x1000 >; + interrupts = < 0xd 0x1 >; + status = "okay"; + }; + ecb: ecb@4000e000 { + compatible = "nordic,nrf-ecb"; + reg = < 0x4000e000 0x1000 >; + interrupts = < 0xe 0x1 >; + status = "okay"; + }; + ccm: ccm@4000f000 { + compatible = "nordic,nrf-ccm"; + reg = < 0x4000f000 0x1000 >; + interrupts = < 0xf 0x1 >; + length-field-length-8-bits; + status = "okay"; + }; + wdt: wdt0: watchdog@40010000 { + compatible = "nordic,nrf-wdt"; + reg = < 0x40010000 0x1000 >; + interrupts = < 0x10 0x1 >; + status = "okay"; + }; + rtc1: rtc@40011000 { + compatible = "nordic,nrf-rtc"; + reg = < 0x40011000 0x1000 >; + cc-num = < 0x4 >; + interrupts = < 0x11 0x1 >; + status = "disabled"; + clock-frequency = < 0x8000 >; + prescaler = < 0x1 >; + }; + qdec: qdec0: qdec@40012000 { + compatible = "nordic,nrf-qdec"; + reg = < 0x40012000 0x1000 >; + interrupts = < 0x12 0x1 >; + status = "disabled"; + }; + comp: comparator@40013000 { + compatible = "nordic,nrf-comp"; + reg = < 0x40013000 0x1000 >; + interrupts = < 0x13 0x1 >; + status = "disabled"; + #io-channel-cells = < 0x1 >; + }; + egu0: swi0: egu@40014000 { + compatible = "nordic,nrf-egu", "nordic,nrf-swi"; + reg = < 0x40014000 0x1000 >; + interrupts = < 0x14 0x1 >; + status = "okay"; + }; + egu1: swi1: egu@40015000 { + compatible = "nordic,nrf-egu", "nordic,nrf-swi"; + reg = < 0x40015000 0x1000 >; + interrupts = < 0x15 0x1 >; + status = "okay"; + }; + egu2: swi2: egu@40016000 { + compatible = "nordic,nrf-egu", "nordic,nrf-swi"; + reg = < 0x40016000 0x1000 >; + interrupts = < 0x16 0x1 >; + status = "okay"; + }; + egu3: swi3: egu@40017000 { + compatible = "nordic,nrf-egu", "nordic,nrf-swi"; + reg = < 0x40017000 0x1000 >; + interrupts = < 0x17 0x1 >; + status = "okay"; + }; + egu4: swi4: egu@40018000 { + compatible = "nordic,nrf-egu", "nordic,nrf-swi"; + reg = < 0x40018000 0x1000 >; + interrupts = < 0x18 0x1 >; + status = "okay"; + }; + egu5: swi5: egu@40019000 { + compatible = "nordic,nrf-egu", "nordic,nrf-swi"; + reg = < 0x40019000 0x1000 >; + interrupts = < 0x19 0x1 >; + status = "okay"; + }; + timer3: timer@4001a000 { + compatible = "nordic,nrf-timer"; + status = "disabled"; + reg = < 0x4001a000 0x1000 >; + cc-num = < 0x6 >; + max-bit-width = < 0x20 >; + interrupts = < 0x1a 0x1 >; + prescaler = < 0x0 >; + }; + timer4: timer@4001b000 { + compatible = "nordic,nrf-timer"; + status = "disabled"; + reg = < 0x4001b000 0x1000 >; + cc-num = < 0x6 >; + max-bit-width = < 0x20 >; + interrupts = < 0x1b 0x1 >; + prescaler = < 0x0 >; + }; + pwm0: pwm@4001c000 { + compatible = "nordic,nrf-pwm"; + reg = < 0x4001c000 0x1000 >; + interrupts = < 0x1c 0x1 >; + status = "okay"; + #pwm-cells = < 0x3 >; + pinctrl-0 = < &pwm0_default >; + pinctrl-1 = < &pwm0_sleep >; + pinctrl-names = "default", "sleep"; + phandle = < 0x19 >; + }; + pdm0: pdm@4001d000 { + compatible = "nordic,nrf-pdm"; + reg = < 0x4001d000 0x1000 >; + interrupts = < 0x1d 0x1 >; + status = "disabled"; + }; + acl: acl@4001e000 { + compatible = "nordic,nrf-acl"; + reg = < 0x4001e000 0x1000 >; + status = "okay"; + }; + flash_controller: flash-controller@4001e000 { + compatible = "nordic,nrf52-flash-controller"; + reg = < 0x4001e000 0x1000 >; + partial-erase; + #address-cells = < 0x1 >; + #size-cells = < 0x1 >; + flash0: flash@0 { + compatible = "soc-nv-flash"; + erase-block-size = < 0x1000 >; + write-block-size = < 0x4 >; + reg = < 0x0 0x100000 >; + partitions { + compatible = "fixed-partitions"; + #address-cells = < 0x1 >; + #size-cells = < 0x1 >; + boot_partition: partition@0 { + label = "mcuboot"; + reg = < 0x0 0xc000 >; + }; + slot0_partition: partition@c000 { + label = "image-0"; + reg = < 0xc000 0x76000 >; + }; + slot1_partition: partition@82000 { + label = "image-1"; + reg = < 0x82000 0x76000 >; + }; + storage_partition: partition@f8000 { + label = "storage"; + reg = < 0xf8000 0x8000 >; + }; + }; + }; + }; + ppi: ppi@4001f000 { + compatible = "nordic,nrf-ppi"; + reg = < 0x4001f000 0x1000 >; + status = "okay"; + }; + mwu: mwu@40020000 { + compatible = "nordic,nrf-mwu"; + reg = < 0x40020000 0x1000 >; + status = "okay"; + }; + pwm1: pwm@40021000 { + compatible = "nordic,nrf-pwm"; + reg = < 0x40021000 0x1000 >; + interrupts = < 0x21 0x1 >; + status = "disabled"; + #pwm-cells = < 0x3 >; + }; + pwm2: pwm@40022000 { + compatible = "nordic,nrf-pwm"; + reg = < 0x40022000 0x1000 >; + interrupts = < 0x22 0x1 >; + status = "disabled"; + #pwm-cells = < 0x3 >; + }; + spi2: spi@40023000 { + compatible = "nordic,nrf-spi"; + #address-cells = < 0x1 >; + #size-cells = < 0x0 >; + reg = < 0x40023000 0x1000 >; + interrupts = < 0x23 0x1 >; + max-frequency = < 0x7a1200 >; + easydma-maxcnt-bits = < 0x10 >; + status = "disabled"; + pinctrl-0 = < &spi2_default >; + pinctrl-1 = < &spi2_sleep >; + pinctrl-names = "default", "sleep"; + }; + rtc2: rtc@40024000 { + compatible = "nordic,nrf-rtc"; + reg = < 0x40024000 0x1000 >; + cc-num = < 0x4 >; + interrupts = < 0x24 0x1 >; + status = "disabled"; + clock-frequency = < 0x8000 >; + prescaler = < 0x1 >; + }; + i2s0: i2s@40025000 { + compatible = "nordic,nrf-i2s"; + #address-cells = < 0x1 >; + #size-cells = < 0x0 >; + reg = < 0x40025000 0x1000 >; + interrupts = < 0x25 0x1 >; + status = "disabled"; + }; + usbd: zephyr_udc0: usbd@40027000 { + compatible = "nordic,nrf-usbd"; + reg = < 0x40027000 0x1000 >; + interrupts = < 0x27 0x1 >; + num-bidir-endpoints = < 0x1 >; + num-in-endpoints = < 0x7 >; + num-out-endpoints = < 0x7 >; + num-isoin-endpoints = < 0x1 >; + num-isoout-endpoints = < 0x1 >; + status = "okay"; + }; + uart1: arduino_serial: uart@40028000 { + compatible = "nordic,nrf-uarte"; + reg = < 0x40028000 0x1000 >; + interrupts = < 0x28 0x1 >; + status = "disabled"; + current-speed = < 0x1c200 >; + pinctrl-0 = < &uart1_default >; + pinctrl-1 = < &uart1_sleep >; + pinctrl-names = "default", "sleep"; + }; + qspi: qspi@40029000 { + compatible = "nordic,nrf-qspi"; + #address-cells = < 0x1 >; + #size-cells = < 0x0 >; + reg = < 0x40029000 0x1000 >, < 0x12000000 0x8000000 >; + reg-names = "qspi", "qspi_mm"; + interrupts = < 0x29 0x1 >; + status = "okay"; + pinctrl-0 = < &qspi_default >; + pinctrl-1 = < &qspi_sleep >; + pinctrl-names = "default", "sleep"; + mx25r64: mx25r6435f@0 { + compatible = "nordic,qspi-nor"; + reg = < 0x0 >; + writeoc = "pp4io"; + readoc = "read4io"; + sck-frequency = < 0x7a1200 >; + jedec-id = [ C2 28 17 ]; + sfdp-bfp = [ + E5 20 F1 FF FF FF FF 03 44 EB 08 6B + 08 3B 04 BB EE FF FF FF FF FF 00 FF + FF FF 00 FF 0C 20 0F 52 10 D8 00 FF + 23 72 F5 00 82 ED 04 CC 44 83 68 44 + 30 B0 30 B0 F7 C4 D5 5C 00 BE 29 FF + F0 D0 FF FF ]; + size = < 0x4000000 >; + has-dpd; + t-enter-dpd = < 0x2710 >; + t-exit-dpd = < 0x88b8 >; + }; + }; + pwm3: pwm@4002d000 { + compatible = "nordic,nrf-pwm"; + reg = < 0x4002d000 0x1000 >; + interrupts = < 0x2d 0x1 >; + status = "disabled"; + #pwm-cells = < 0x3 >; + }; + spi3: arduino_spi: spi@4002f000 { + compatible = "nordic,nrf-spim"; + #address-cells = < 0x1 >; + #size-cells = < 0x0 >; + reg = < 0x4002f000 0x1000 >; + interrupts = < 0x2f 0x1 >; + max-frequency = < 0x1e84800 >; + easydma-maxcnt-bits = < 0x10 >; + rx-delay-supported; + rx-delay = < 0x2 >; + status = "okay"; + cs-gpios = < &arduino_header 0x10 0x1 >; + pinctrl-0 = < &spi3_default >; + pinctrl-1 = < &spi3_sleep >; + pinctrl-names = "default", "sleep"; + }; + gpio0: gpio@50000000 { + compatible = "nordic,nrf-gpio"; + gpio-controller; + reg = < 0x50000000 0x200 0x50000500 0x300 >; + #gpio-cells = < 0x2 >; + status = "okay"; + port = < 0x0 >; + phandle = < 0x18 >; + }; + gpio1: gpio@50000300 { + compatible = "nordic,nrf-gpio"; + gpio-controller; + reg = < 0x50000300 0x200 0x50000800 0x300 >; + #gpio-cells = < 0x2 >; + ngpios = < 0x10 >; + status = "okay"; + port = < 0x1 >; + phandle = < 0x1a >; + }; + cryptocell: crypto@5002a000 { + compatible = "nordic,cryptocell", "arm,cryptocell-310"; + reg = < 0x5002a000 0x1000 >, < 0x5002b000 0x1000 >; + reg-names = "wrapper", "core"; + interrupts = < 0x2a 0x1 >; + status = "disabled"; + }; + }; + pinctrl: pin-controller { + compatible = "nordic,nrf-pinctrl"; + uart0_default: uart0_default { + phandle = < 0x2 >; + group1 { + psels = < 0x6 >, < 0x20005 >; + }; + group2 { + psels = < 0x10008 >, < 0x30007 >; + bias-pull-up; + }; + }; + uart0_sleep: uart0_sleep { + phandle = < 0x3 >; + group1 { + psels = < 0x6 >, < 0x10008 >, < 0x20005 >, < 0x30007 >; + low-power-enable; + }; + }; + uart1_default: uart1_default { + phandle = < 0x10 >; + group1 { + psels = < 0x10021 >; + bias-pull-up; + }; + group2 { + psels = < 0x22 >; + }; + }; + uart1_sleep: uart1_sleep { + phandle = < 0x11 >; + group1 { + psels = < 0x10021 >, < 0x22 >; + low-power-enable; + }; + }; + i2c0_default: i2c0_default { + phandle = < 0x4 >; + group1 { + psels = < 0xc001a >, < 0xb001b >; + }; + }; + i2c0_sleep: i2c0_sleep { + phandle = < 0x5 >; + group1 { + psels = < 0xc001a >, < 0xb001b >; + low-power-enable; + }; + }; + i2c1_default: i2c1_default { + phandle = < 0x8 >; + group1 { + psels = < 0xc001e >, < 0xb001f >; + }; + }; + i2c1_sleep: i2c1_sleep { + phandle = < 0x9 >; + group1 { + psels = < 0xc001e >, < 0xb001f >; + low-power-enable; + }; + }; + pwm0_default: pwm0_default { + phandle = < 0xc >; + group1 { + psels = < 0x16000d >; + nordic,invert; + }; + }; + pwm0_sleep: pwm0_sleep { + phandle = < 0xd >; + group1 { + psels = < 0x16000d >; + low-power-enable; + }; + }; + spi0_default: spi0_default { + phandle = < 0x6 >; + group1 { + psels = < 0x4001b >, < 0x5001a >, < 0x6001d >; + }; + }; + spi0_sleep: spi0_sleep { + phandle = < 0x7 >; + group1 { + psels = < 0x4001b >, < 0x5001a >, < 0x6001d >; + low-power-enable; + }; + }; + spi1_default: spi1_default { + phandle = < 0xa >; + group1 { + psels = < 0x4001f >, < 0x5001e >, < 0x60028 >; + }; + }; + spi1_sleep: spi1_sleep { + phandle = < 0xb >; + group1 { + psels = < 0x4001f >, < 0x5001e >, < 0x60028 >; + low-power-enable; + }; + }; + spi2_default: spi2_default { + phandle = < 0xe >; + group1 { + psels = < 0x40013 >, < 0x50014 >, < 0x60015 >; + }; + }; + spi2_sleep: spi2_sleep { + phandle = < 0xf >; + group1 { + psels = < 0x40013 >, < 0x50014 >, < 0x60015 >; + low-power-enable; + }; + }; + qspi_default: qspi_default { + phandle = < 0x12 >; + group1 { + psels = < 0x1d0013 >, < 0x1f0014 >, < 0x200015 >, < 0x210016 >, + < 0x220017 >, < 0x1e0011 >; + nordic,drive-mode = < 0x3 >; + }; + }; + qspi_sleep: qspi_sleep { + phandle = < 0x13 >; + group1 { + psels = < 0x1d0013 >, < 0x1f0014 >, < 0x200015 >, < 0x210016 >, + < 0x220017 >; + low-power-enable; + }; + group2 { + psels = < 0x1e0011 >; + low-power-enable; + bias-pull-up; + }; + }; + spi3_default: spi3_default { + phandle = < 0x15 >; + group1 { + psels = < 0x4002f >, < 0x6002e >, < 0x5002d >; + }; + }; + spi3_sleep: spi3_sleep { + phandle = < 0x16 >; + group1 { + psels = < 0x4002f >, < 0x6002e >, < 0x5002d >; + low-power-enable; + }; + }; + }; + rng_hci: entropy_bt_hci { + compatible = "zephyr,bt-hci-entropy"; + status = "okay"; + }; + sw_pwm: sw-pwm { + compatible = "nordic,nrf-sw-pwm"; + status = "disabled"; + generator = < &timer1 >; + clock-prescaler = < 0x0 >; + #pwm-cells = < 0x3 >; + }; + cpus { + #address-cells = < 0x1 >; + #size-cells = < 0x0 >; + cpu@0 { + device_type = "cpu"; + compatible = "arm,cortex-m4f"; + reg = < 0x0 >; + #address-cells = < 0x1 >; + #size-cells = < 0x1 >; + itm: itm@e0000000 { + compatible = "arm,armv7m-itm"; + reg = < 0xe0000000 0x1000 >; + swo-ref-frequency = < 0x1e84800 >; + }; + }; + }; + leds { + compatible = "gpio-leds"; + led0: led_0 { + gpios = < &gpio0 0xd 0x1 >; + label = "Green LED 0"; + }; + led1: led_1 { + gpios = < &gpio0 0xe 0x1 >; + label = "Green LED 1"; + }; + led2: led_2 { + gpios = < &gpio0 0xf 0x1 >; + label = "Green LED 2"; + }; + led3: led_3 { + gpios = < &gpio0 0x10 0x1 >; + label = "Green LED 3"; + }; + }; + pwmleds { + compatible = "pwm-leds"; + pwm_led0: pwm_led_0 { + pwms = < &pwm0 0x0 0x1312d00 0x1 >; + }; + }; + buttons { + compatible = "gpio-keys"; + button0: button_0 { + gpios = < &gpio0 0xb 0x11 >; + label = "Push button switch 0"; + zephyr,code = < 0xb >; + }; + button1: button_1 { + gpios = < &gpio0 0xc 0x11 >; + label = "Push button switch 1"; + zephyr,code = < 0x2 >; + }; + button2: button_2 { + gpios = < &gpio0 0x18 0x11 >; + label = "Push button switch 2"; + zephyr,code = < 0x3 >; + }; + button3: button_3 { + gpios = < &gpio0 0x19 0x11 >; + label = "Push button switch 3"; + zephyr,code = < 0x4 >; + }; + }; + arduino_header: connector { + compatible = "arduino-header-r3"; + #gpio-cells = < 0x2 >; + gpio-map-mask = < 0xffffffff 0xffffffc0 >; + gpio-map-pass-thru = < 0x0 0x3f >; + gpio-map = < 0x0 0x0 &gpio0 0x3 0x0 >, < 0x1 0x0 &gpio0 0x4 0x0 >, + < 0x2 0x0 &gpio0 0x1c 0x0 >, < 0x3 0x0 &gpio0 0x1d 0x0 >, + < 0x4 0x0 &gpio0 0x1e 0x0 >, < 0x5 0x0 &gpio0 0x1f 0x0 >, + < 0x6 0x0 &gpio1 0x1 0x0 >, < 0x7 0x0 &gpio1 0x2 0x0 >, + < 0x8 0x0 &gpio1 0x3 0x0 >, < 0x9 0x0 &gpio1 0x4 0x0 >, + < 0xa 0x0 &gpio1 0x5 0x0 >, < 0xb 0x0 &gpio1 0x6 0x0 >, + < 0xc 0x0 &gpio1 0x7 0x0 >, < 0xd 0x0 &gpio1 0x8 0x0 >, + < 0xe 0x0 &gpio1 0xa 0x0 >, < 0xf 0x0 &gpio1 0xb 0x0 >, + < 0x10 0x0 &gpio1 0xc 0x0 >, < 0x11 0x0 &gpio1 0xd 0x0 >, + < 0x12 0x0 &gpio1 0xe 0x0 >, < 0x13 0x0 &gpio1 0xf 0x0 >, + < 0x14 0x0 &gpio0 0x1a 0x0 >, < 0x15 0x0 &gpio0 0x1b 0x0 >; + phandle = < 0x14 >; + }; + arduino_adc: analog-connector { + compatible = "arduino,uno-adc"; + #io-channel-cells = < 0x1 >; + io-channel-map = < 0x0 &adc 0x1 >, < 0x1 &adc 0x2 >, < 0x2 &adc 0x4 >, + < 0x3 &adc 0x5 >, < 0x4 &adc 0x6 >, < 0x5 &adc 0x7 >; + }; +}; diff --git a/tests/res/Build_zephyr/CMakeCache.txt b/tests/res/Build_zephyr/CMakeCache.txt new file mode 100644 index 0000000..841cb4a --- /dev/null +++ b/tests/res/Build_zephyr/CMakeCache.txt @@ -0,0 +1,578 @@ +# This is the CMakeCache file. +# For build in directory: /path/to/zephyr_base/samples/sensor/bme680/build +# It was generated by CMake: /usr/bin/cmake +# You can edit this file to change values found and used by cmake. +# If you do not want to change any of the values, simply exit the editor. +# If you do want to change a value, simply edit, save, and exit the editor. +# The syntax for the file is as follows: +# KEY:TYPE=VALUE +# KEY is the name of a variable in the cache. +# TYPE is a hint to GUIs for the type of VALUE, DO NOT EDIT TYPE!. +# VALUE is the current value for the KEY. + +######################## +# EXTERNAL cache entries +######################## + +//Application Binary Directory +APPLICATION_BINARY_DIR:PATH=/path/to/zephyr_base/samples/sensor/bme680/build + +//Application Source Directory +APPLICATION_SOURCE_DIR:PATH=/path/to/zephyr_base/samples/sensor/bme680 + +//Selected board +BOARD:STRING=nrf52840dk_nrf52840 + +//Path to a file. +BOARD_DIR:PATH=/path/to/zephyr_base/boards/arm/nrf52840dk_nrf52840 + +//Support board extensions +BOARD_EXTENSIONS:BOOL=ON + +//Path to a program. +BOSSAC:FILEPATH=/path/to/zephyr-sdk/sysroots/x86_64-pokysdk-linux/usr/bin/bossac + +//Kernel binary file +BYPRODUCT_KERNEL_BIN_NAME:FILEPATH=/path/to/zephyr_base/samples/sensor/bme680/build/zephyr/zephyr.bin + +//Kernel elf file +BYPRODUCT_KERNEL_ELF_NAME:FILEPATH=/path/to/zephyr_base/samples/sensor/bme680/build/zephyr/zephyr.elf + +//Kernel hex file +BYPRODUCT_KERNEL_HEX_NAME:FILEPATH=/path/to/zephyr_base/samples/sensor/bme680/build/zephyr/zephyr.hex + +//Selected board +CACHED_BOARD:STRING=nrf52840dk_nrf52840 + +//If desired, you can build the application usingthe configuration +// settings specified in an alternate .conf file using this parameter. +// These settings will override the settings in the application’s +// .config file or its default .conf file.Multiple files may be +// listed, e.g. CONF_FILE="prj1.conf;prj2.conf" The CACHED_CONF_FILE +// is internal Zephyr variable used between CMake runs. To change +// CONF_FILE, use the CONF_FILE variable. +CACHED_CONF_FILE:STRING=/path/to/zephyr_base/samples/sensor/bme680/prj.conf + +//Selected shield +CACHED_SHIELD:STRING= + +//Selected snippet +CACHED_SNIPPET:STRING= + +//Path to a program. +CCACHE_FOUND:FILEPATH=/usr/bin/ccache + +//Path to a program. +CMAKE_ADDR2LINE:FILEPATH=/path/to/zephyr-sdk/arm-zephyr-eabi/bin/arm-zephyr-eabi-addr2line + +//Path to a program. +CMAKE_AR:FILEPATH=/path/to/zephyr-sdk/arm-zephyr-eabi/bin/arm-zephyr-eabi-ar + +//Path to a program. +CMAKE_AS:FILEPATH=/path/to/zephyr-sdk/arm-zephyr-eabi/bin/arm-zephyr-eabi-as + +//ASM compiler +CMAKE_ASM_COMPILER:FILEPATH=/path/to/zephyr-sdk/arm-zephyr-eabi/bin/arm-zephyr-eabi-gcc + +//A wrapper around 'ar' adding the appropriate '--plugin' option +// for the GCC compiler +CMAKE_ASM_COMPILER_AR:FILEPATH=/path/to/zephyr-sdk/arm-zephyr-eabi/bin/arm-zephyr-eabi-gcc-ar + +//A wrapper around 'ranlib' adding the appropriate '--plugin' option +// for the GCC compiler +CMAKE_ASM_COMPILER_RANLIB:FILEPATH=/path/to/zephyr-sdk/arm-zephyr-eabi/bin/arm-zephyr-eabi-gcc-ranlib + +//Flags used by the ASM compiler during all build types. +CMAKE_ASM_FLAGS:STRING= + +//Flags used by the ASM compiler during DEBUG builds. +CMAKE_ASM_FLAGS_DEBUG:STRING=-g + +//Flags used by the ASM compiler during MINSIZEREL builds. +CMAKE_ASM_FLAGS_MINSIZEREL:STRING=-Os -DNDEBUG + +//Flags used by the ASM compiler during RELEASE builds. +CMAKE_ASM_FLAGS_RELEASE:STRING=-O3 -DNDEBUG + +//Flags used by the ASM compiler during RELWITHDEBINFO builds. +CMAKE_ASM_FLAGS_RELWITHDEBINFO:STRING=-O2 -g -DNDEBUG + +//Choose the type of build, options are: None Debug Release RelWithDebInfo +// MinSizeRel ... +CMAKE_BUILD_TYPE:STRING= + +//Enable/Disable color output during build. +CMAKE_COLOR_MAKEFILE:BOOL=ON + +//CXX compiler +CMAKE_CXX_COMPILER:STRING=/path/to/zephyr-sdk/arm-zephyr-eabi/bin/arm-zephyr-eabi-g++ + +//A wrapper around 'ar' adding the appropriate '--plugin' option +// for the GCC compiler +CMAKE_CXX_COMPILER_AR:FILEPATH=/path/to/zephyr-sdk/arm-zephyr-eabi/bin/arm-zephyr-eabi-gcc-ar + +//A wrapper around 'ranlib' adding the appropriate '--plugin' option +// for the GCC compiler +CMAKE_CXX_COMPILER_RANLIB:FILEPATH=/path/to/zephyr-sdk/arm-zephyr-eabi/bin/arm-zephyr-eabi-gcc-ranlib + +//Flags used by the CXX compiler during all build types. +CMAKE_CXX_FLAGS:STRING= + +//Flags used by the CXX compiler during DEBUG builds. +CMAKE_CXX_FLAGS_DEBUG:STRING=-g + +//Flags used by the CXX compiler during MINSIZEREL builds. +CMAKE_CXX_FLAGS_MINSIZEREL:STRING=-Os -DNDEBUG + +//Flags used by the CXX compiler during RELEASE builds. +CMAKE_CXX_FLAGS_RELEASE:STRING=-O3 -DNDEBUG + +//Flags used by the CXX compiler during RELWITHDEBINFO builds. +CMAKE_CXX_FLAGS_RELWITHDEBINFO:STRING=-O2 -g -DNDEBUG + +//C compiler +CMAKE_C_COMPILER:STRING=/path/to/zephyr-sdk/arm-zephyr-eabi/bin/arm-zephyr-eabi-gcc + +//A wrapper around 'ar' adding the appropriate '--plugin' option +// for the GCC compiler +CMAKE_C_COMPILER_AR:FILEPATH=/path/to/zephyr-sdk/arm-zephyr-eabi/bin/arm-zephyr-eabi-gcc-ar + +//A wrapper around 'ranlib' adding the appropriate '--plugin' option +// for the GCC compiler +CMAKE_C_COMPILER_RANLIB:FILEPATH=/path/to/zephyr-sdk/arm-zephyr-eabi/bin/arm-zephyr-eabi-gcc-ranlib + +//Flags used by the C compiler during all build types. +CMAKE_C_FLAGS:STRING= + +//Flags used by the C compiler during DEBUG builds. +CMAKE_C_FLAGS_DEBUG:STRING=-g + +//Flags used by the C compiler during MINSIZEREL builds. +CMAKE_C_FLAGS_MINSIZEREL:STRING=-Os -DNDEBUG + +//Flags used by the C compiler during RELEASE builds. +CMAKE_C_FLAGS_RELEASE:STRING=-O3 -DNDEBUG + +//Flags used by the C compiler during RELWITHDEBINFO builds. +CMAKE_C_FLAGS_RELWITHDEBINFO:STRING=-O2 -g -DNDEBUG + +//Path to a program. +CMAKE_DLLTOOL:FILEPATH=CMAKE_DLLTOOL-NOTFOUND + +//Flags used by the linker during all build types. +CMAKE_EXE_LINKER_FLAGS:STRING= + +//Flags used by the linker during DEBUG builds. +CMAKE_EXE_LINKER_FLAGS_DEBUG:STRING= + +//Flags used by the linker during MINSIZEREL builds. +CMAKE_EXE_LINKER_FLAGS_MINSIZEREL:STRING= + +//Flags used by the linker during RELEASE builds. +CMAKE_EXE_LINKER_FLAGS_RELEASE:STRING= + +//Flags used by the linker during RELWITHDEBINFO builds. +CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO:STRING= + +//Export CMake compile commands. Used by gen_app_partitions.py +// script +CMAKE_EXPORT_COMPILE_COMMANDS:BOOL=TRUE + +//Path to a program. +CMAKE_GCOV:FILEPATH=/path/to/zephyr-sdk/aarch64-zephyr-elf/bin/aarch64-zephyr-elf-gcov + +//Path to a program. +CMAKE_GDB:FILEPATH=/path/to/zephyr-sdk/arm-zephyr-eabi/bin/arm-zephyr-eabi-gdb + +//Path to a program. +CMAKE_GDB_NO_PY:FILEPATH=/path/to/zephyr-sdk/arm-zephyr-eabi/bin/arm-zephyr-eabi-gdb + +//Install path prefix, prepended onto install directories. +CMAKE_INSTALL_PREFIX:PATH=/usr/local + +//Path to a program. +CMAKE_MAKE_PROGRAM:FILEPATH=/usr/bin/gmake + +//Flags used by the linker during the creation of modules during +// all build types. +CMAKE_MODULE_LINKER_FLAGS:STRING= + +//Flags used by the linker during the creation of modules during +// DEBUG builds. +CMAKE_MODULE_LINKER_FLAGS_DEBUG:STRING= + +//Flags used by the linker during the creation of modules during +// MINSIZEREL builds. +CMAKE_MODULE_LINKER_FLAGS_MINSIZEREL:STRING= + +//Flags used by the linker during the creation of modules during +// RELEASE builds. +CMAKE_MODULE_LINKER_FLAGS_RELEASE:STRING= + +//Flags used by the linker during the creation of modules during +// RELWITHDEBINFO builds. +CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO:STRING= + +//Path to a program. +CMAKE_NM:FILEPATH=/path/to/zephyr-sdk/arm-zephyr-eabi/bin/arm-zephyr-eabi-nm + +//Path to a program. +CMAKE_OBJCOPY:FILEPATH=/path/to/zephyr-sdk/arm-zephyr-eabi/bin/arm-zephyr-eabi-objcopy + +//Path to a program. +CMAKE_OBJDUMP:FILEPATH=/path/to/zephyr-sdk/arm-zephyr-eabi/bin/arm-zephyr-eabi-objdump + +//Value Computed by CMake +CMAKE_PROJECT_DESCRIPTION:STATIC= + +//Value Computed by CMake +CMAKE_PROJECT_HOMEPAGE_URL:STATIC= + +//Value Computed by CMake +CMAKE_PROJECT_NAME:STATIC=bme680 + +//Value Computed by CMake +CMAKE_PROJECT_VERSION:STATIC=3.4.99 + +//Value Computed by CMake +CMAKE_PROJECT_VERSION_MAJOR:STATIC=3 + +//Value Computed by CMake +CMAKE_PROJECT_VERSION_MINOR:STATIC=4 + +//Value Computed by CMake +CMAKE_PROJECT_VERSION_PATCH:STATIC=99 + +//Value Computed by CMake +CMAKE_PROJECT_VERSION_TWEAK:STATIC= + +//Path to a program. +CMAKE_RANLIB:FILEPATH=/path/to/zephyr-sdk/arm-zephyr-eabi/bin/arm-zephyr-eabi-ranlib + +//Path to a program. +CMAKE_READELF:FILEPATH=/path/to/zephyr-sdk/arm-zephyr-eabi/bin/arm-zephyr-eabi-readelf + +//Flags used by the linker during the creation of shared libraries +// during all build types. +CMAKE_SHARED_LINKER_FLAGS:STRING= + +//Flags used by the linker during the creation of shared libraries +// during DEBUG builds. +CMAKE_SHARED_LINKER_FLAGS_DEBUG:STRING= + +//Flags used by the linker during the creation of shared libraries +// during MINSIZEREL builds. +CMAKE_SHARED_LINKER_FLAGS_MINSIZEREL:STRING= + +//Flags used by the linker during the creation of shared libraries +// during RELEASE builds. +CMAKE_SHARED_LINKER_FLAGS_RELEASE:STRING= + +//Flags used by the linker during the creation of shared libraries +// during RELWITHDEBINFO builds. +CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO:STRING= + +//If set, runtime paths are not added when installing shared libraries, +// but are added when building. +CMAKE_SKIP_INSTALL_RPATH:BOOL=NO + +//If set, runtime paths are not added when using shared libraries. +CMAKE_SKIP_RPATH:BOOL=NO + +//Flags used by the linker during the creation of static libraries +// during all build types. +CMAKE_STATIC_LINKER_FLAGS:STRING= + +//Flags used by the linker during the creation of static libraries +// during DEBUG builds. +CMAKE_STATIC_LINKER_FLAGS_DEBUG:STRING= + +//Flags used by the linker during the creation of static libraries +// during MINSIZEREL builds. +CMAKE_STATIC_LINKER_FLAGS_MINSIZEREL:STRING= + +//Flags used by the linker during the creation of static libraries +// during RELEASE builds. +CMAKE_STATIC_LINKER_FLAGS_RELEASE:STRING= + +//Flags used by the linker during the creation of static libraries +// during RELWITHDEBINFO builds. +CMAKE_STATIC_LINKER_FLAGS_RELWITHDEBINFO:STRING= + +//Path to a program. +CMAKE_STRIP:FILEPATH=/path/to/zephyr-sdk/arm-zephyr-eabi/bin/arm-zephyr-eabi-strip + +//If this value is on, makefiles will be generated without the +// .SILENT directive, and all commands will be echoed to the console +// during the make. This is useful for debugging only. With Visual +// Studio IDE projects all commands are done without /nologo. +CMAKE_VERBOSE_MAKEFILE:BOOL=FALSE + +//Path to a program. +DTC:FILEPATH=/path/to/zephyr-sdk/sysroots/x86_64-pokysdk-linux/usr/bin/dtc + +//If desired, you can build the application using the DT configuration +// settings specified in an alternate .overlay file using this +// parameter. These settings will override the settings in the +// board's .dts file. Multiple files may be listed, e.g. DTC_OVERLAY_FILE="dts1.overlay +// dts2.overlay" +DTC_OVERLAY_FILE:STRING=/path/to/zephyr_base/samples/sensor/bme680/boards/nrf52840dk_nrf52840.overlay + +//Linker BFD compatibility (compiler reported) +GNULD_LINKER_IS_BFD:BOOL=ON + +//GNU ld version +GNULD_VERSION_STRING:STRING=2.38 + +//Path to a program. +GPERF:FILEPATH=/usr/bin/gperf + +//Path to a program. +IMGTOOL:FILEPATH=/path/to/.venv/bin/imgtool + +//nrfx Directory +NRFX_DIR:PATH=/path/to/modules/hal/nordic/nrfx + +//Path to a program. +OPENOCD:FILEPATH=/path/to/zephyr-sdk/sysroots/x86_64-pokysdk-linux/usr/bin/openocd + +//Path to a program. +PAHOLE:FILEPATH=PAHOLE-NOTFOUND + +//Path to a program. +PUNCOVER:FILEPATH=PUNCOVER-NOTFOUND + +//Value Computed by CMake +Picolibc_BINARY_DIR:STATIC=/path/to/zephyr_base/samples/sensor/bme680/build/modules/picolibc + +//Value Computed by CMake +Picolibc_SOURCE_DIR:STATIC=/path/to/modules/lib/picolibc + +//True if toolchain supports newlib +TOOLCHAIN_HAS_NEWLIB:BOOL=ON + +//True if toolchain supports picolibc +TOOLCHAIN_HAS_PICOLIBC:BOOL=ON + +//Zephyr toolchain root +TOOLCHAIN_ROOT:STRING=/path/to/zephyr_base + +//No help, variable specified on the command line. +WEST_PYTHON:UNINITIALIZED=/path/to/.venv/bin/python3.11 + +//Zephyr base +ZEPHYR_BASE:PATH=/path/to/zephyr_base + +//Path to Zephyr git repository index file +ZEPHYR_GIT_DIR:PATH=/path/to/zephyr_base/.git/index + +//Path to Zephyr git repository index file +ZEPHYR_GIT_INDEX:PATH=ZEPHYR_GIT_INDEX-NOTFOUND + +//Value Computed by CMake +Zephyr-Kernel_BINARY_DIR:STATIC=/path/to/zephyr_base/samples/sensor/bme680/build + +//Value Computed by CMake +Zephyr-Kernel_SOURCE_DIR:STATIC=/path/to/zephyr_base/samples/sensor/bme680 + +//The directory containing a CMake configuration file for Zephyr-sdk. +Zephyr-sdk_DIR:PATH=/path/to/zephyr-sdk/cmake + +//The directory containing a CMake configuration file for ZephyrAppConfiguration. +ZephyrAppConfiguration_DIR:PATH=ZephyrAppConfiguration_DIR-NOTFOUND + +//The directory containing a CMake configuration file for ZephyrBuildConfiguration. +ZephyrBuildConfiguration_DIR:PATH=ZephyrBuildConfiguration_DIR-NOTFOUND + +//The directory containing a CMake configuration file for Zephyr. +Zephyr_DIR:PATH=/path/to/zephyr_base/share/zephyr-package/cmake + +//Value Computed by CMake +bme680_BINARY_DIR:STATIC=/path/to/zephyr_base/samples/sensor/bme680/build + +//Value Computed by CMake +bme680_SOURCE_DIR:STATIC=/path/to/zephyr_base/samples/sensor/bme680 + + +######################## +# INTERNAL cache entries +######################## + +//The application configuration folder +APPLICATION_CONFIG_DIR:INTERNAL=/path/to/zephyr_base/samples/sensor/bme680 +//DT bindings root directories +CACHED_DTS_ROOT_BINDINGS:INTERNAL=/path/to/zephyr_base/dts/bindings +//ADVANCED property for variable: CMAKE_ADDR2LINE +CMAKE_ADDR2LINE-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_AR +CMAKE_AR-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_ASM_COMPILER +CMAKE_ASM_COMPILER-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_ASM_COMPILER_AR +CMAKE_ASM_COMPILER_AR-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_ASM_COMPILER_RANLIB +CMAKE_ASM_COMPILER_RANLIB-ADVANCED:INTERNAL=1 +CMAKE_ASM_COMPILER_WORKS:INTERNAL=1 +//ADVANCED property for variable: CMAKE_ASM_FLAGS +CMAKE_ASM_FLAGS-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_ASM_FLAGS_DEBUG +CMAKE_ASM_FLAGS_DEBUG-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_ASM_FLAGS_MINSIZEREL +CMAKE_ASM_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_ASM_FLAGS_RELEASE +CMAKE_ASM_FLAGS_RELEASE-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_ASM_FLAGS_RELWITHDEBINFO +CMAKE_ASM_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 +//This is the directory where this CMakeCache.txt was created +CMAKE_CACHEFILE_DIR:INTERNAL=/path/to/zephyr_base/samples/sensor/bme680/build +//Major version of cmake used to create the current loaded cache +CMAKE_CACHE_MAJOR_VERSION:INTERNAL=3 +//Minor version of cmake used to create the current loaded cache +CMAKE_CACHE_MINOR_VERSION:INTERNAL=20 +//Patch version of cmake used to create the current loaded cache +CMAKE_CACHE_PATCH_VERSION:INTERNAL=2 +//ADVANCED property for variable: CMAKE_COLOR_MAKEFILE +CMAKE_COLOR_MAKEFILE-ADVANCED:INTERNAL=1 +//Path to CMake executable. +CMAKE_COMMAND:INTERNAL=/usr/bin/cmake +//Path to cpack program executable. +CMAKE_CPACK_COMMAND:INTERNAL=/usr/bin/cpack +//Path to ctest program executable. +CMAKE_CTEST_COMMAND:INTERNAL=/usr/bin/ctest +//ADVANCED property for variable: CMAKE_CXX_COMPILER +CMAKE_CXX_COMPILER-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_CXX_COMPILER_AR +CMAKE_CXX_COMPILER_AR-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_CXX_COMPILER_RANLIB +CMAKE_CXX_COMPILER_RANLIB-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_CXX_FLAGS +CMAKE_CXX_FLAGS-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_CXX_FLAGS_DEBUG +CMAKE_CXX_FLAGS_DEBUG-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_CXX_FLAGS_MINSIZEREL +CMAKE_CXX_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_CXX_FLAGS_RELEASE +CMAKE_CXX_FLAGS_RELEASE-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_CXX_FLAGS_RELWITHDEBINFO +CMAKE_CXX_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_C_COMPILER +CMAKE_C_COMPILER-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_C_COMPILER_AR +CMAKE_C_COMPILER_AR-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_C_COMPILER_RANLIB +CMAKE_C_COMPILER_RANLIB-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_C_FLAGS +CMAKE_C_FLAGS-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_C_FLAGS_DEBUG +CMAKE_C_FLAGS_DEBUG-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_C_FLAGS_MINSIZEREL +CMAKE_C_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_C_FLAGS_RELEASE +CMAKE_C_FLAGS_RELEASE-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_C_FLAGS_RELWITHDEBINFO +CMAKE_C_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_DLLTOOL +CMAKE_DLLTOOL-ADVANCED:INTERNAL=1 +//Path to cache edit program executable. +CMAKE_EDIT_COMMAND:INTERNAL=/usr/bin/ccmake +//Executable file format +CMAKE_EXECUTABLE_FORMAT:INTERNAL=ELF +//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS +CMAKE_EXE_LINKER_FLAGS-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS_DEBUG +CMAKE_EXE_LINKER_FLAGS_DEBUG-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS_MINSIZEREL +CMAKE_EXE_LINKER_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS_RELEASE +CMAKE_EXE_LINKER_FLAGS_RELEASE-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO +CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 +//Name of external makefile project generator. +CMAKE_EXTRA_GENERATOR:INTERNAL= +//Name of generator. +CMAKE_GENERATOR:INTERNAL=Unix Makefiles +//Generator instance identifier. +CMAKE_GENERATOR_INSTANCE:INTERNAL= +//Name of generator platform. +CMAKE_GENERATOR_PLATFORM:INTERNAL= +//Name of generator toolset. +CMAKE_GENERATOR_TOOLSET:INTERNAL= +//Source directory with the top level CMakeLists.txt file for this +// project +CMAKE_HOME_DIRECTORY:INTERNAL=/path/to/zephyr_base/samples/sensor/bme680 +//ADVANCED property for variable: CMAKE_MAKE_PROGRAM +CMAKE_MAKE_PROGRAM-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS +CMAKE_MODULE_LINKER_FLAGS-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS_DEBUG +CMAKE_MODULE_LINKER_FLAGS_DEBUG-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS_MINSIZEREL +CMAKE_MODULE_LINKER_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS_RELEASE +CMAKE_MODULE_LINKER_FLAGS_RELEASE-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO +CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_NM +CMAKE_NM-ADVANCED:INTERNAL=1 +//number of local generators +CMAKE_NUMBER_OF_MAKEFILES:INTERNAL=130 +//ADVANCED property for variable: CMAKE_OBJCOPY +CMAKE_OBJCOPY-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_OBJDUMP +CMAKE_OBJDUMP-ADVANCED:INTERNAL=1 +//Platform information initialized +CMAKE_PLATFORM_INFO_INITIALIZED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_RANLIB +CMAKE_RANLIB-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_READELF +CMAKE_READELF-ADVANCED:INTERNAL=1 +//Path to CMake installation. +CMAKE_ROOT:INTERNAL=/usr/share/cmake +//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS +CMAKE_SHARED_LINKER_FLAGS-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS_DEBUG +CMAKE_SHARED_LINKER_FLAGS_DEBUG-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS_MINSIZEREL +CMAKE_SHARED_LINKER_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS_RELEASE +CMAKE_SHARED_LINKER_FLAGS_RELEASE-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO +CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_SKIP_INSTALL_RPATH +CMAKE_SKIP_INSTALL_RPATH-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_SKIP_RPATH +CMAKE_SKIP_RPATH-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS +CMAKE_STATIC_LINKER_FLAGS-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS_DEBUG +CMAKE_STATIC_LINKER_FLAGS_DEBUG-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS_MINSIZEREL +CMAKE_STATIC_LINKER_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS_RELEASE +CMAKE_STATIC_LINKER_FLAGS_RELEASE-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS_RELWITHDEBINFO +CMAKE_STATIC_LINKER_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_STRIP +CMAKE_STRIP-ADVANCED:INTERNAL=1 +//uname command +CMAKE_UNAME:INTERNAL=/usr/bin/uname +//ADVANCED property for variable: CMAKE_VERBOSE_MAKEFILE +CMAKE_VERBOSE_MAKEFILE-ADVANCED:INTERNAL=1 +//Details about finding Dtc +FIND_PACKAGE_MESSAGE_DETAILS_Dtc:INTERNAL=[/path/to/zephyr-sdk/sysroots/x86_64-pokysdk-linux/usr/bin/dtc][v1.6.0(1.4.6)] +//Details about finding GnuLd +FIND_PACKAGE_MESSAGE_DETAILS_GnuLd:INTERNAL=[/path/to/zephyr-sdk/arm-zephyr-eabi/bin/../lib/gcc/arm-zephyr-eabi/12.2.0/../../../../arm-zephyr-eabi/bin/ld.bfd][v2.38()] +//Details about finding Python3 +FIND_PACKAGE_MESSAGE_DETAILS_Python3:INTERNAL=[/path/to/.venv/bin/python3.11][cfound components: Interpreter ][v3.11.2(3.8)] +//West +WEST:INTERNAL=/path/to/.venv/bin/python3.11;-m;west +//a configuration file for the runners Python package +ZEPHYR_RUNNERS_YAML:INTERNAL=/path/to/zephyr_base/samples/sensor/bme680/build/zephyr/runners.yaml +//Cached environment variable ZEPHYR_SDK_INSTALL_DIR +ZEPHYR_SDK_INSTALL_DIR:INTERNAL=/path/to/zephyr-sdk +//Cached environment variable ZEPHYR_TOOLCHAIN_VARIANT +ZEPHYR_TOOLCHAIN_VARIANT:INTERNAL=zephyr +_Python3_EXECUTABLE:INTERNAL=/path/to/.venv/bin/python3.11 +//Python3 Properties +_Python3_INTERPRETER_PROPERTIES:INTERNAL=Python;3;11;2;64;;cpython-311-x86_64-linux-gnu;/usr/lib64/python3.11;/usr/lib64/python3.11;/path/to/.venv/lib/python3.11/site-packages;/path/to/.venv/lib64/python3.11/site-packages +_Python3_INTERPRETER_SIGNATURE:INTERNAL=2fbe5bcb206741dc7d34d11b11b11abf diff --git a/tests/res/Build_zephyr/zephyr/zephyr.dts b/tests/res/Build_zephyr/zephyr/zephyr.dts new file mode 100644 index 0000000..662ae6b --- /dev/null +++ b/tests/res/Build_zephyr/zephyr/zephyr.dts @@ -0,0 +1,812 @@ +/dts-v1/; + +/ { + #address-cells = < 0x1 >; + #size-cells = < 0x1 >; + model = "Nordic nRF52840 DK NRF52840"; + compatible = "nordic,nrf52840-dk-nrf52840"; + chosen { + zephyr,entropy = &rng; + zephyr,flash-controller = &flash_controller; + zephyr,console = &uart0; + zephyr,shell-uart = &uart0; + zephyr,uart-mcumgr = &uart0; + zephyr,bt-mon-uart = &uart0; + zephyr,bt-c2h-uart = &uart0; + zephyr,sram = &sram0; + zephyr,flash = &flash0; + zephyr,code-partition = &slot0_partition; + zephyr,ieee802154 = &ieee802154; + }; + aliases { + led0 = &led0; + led1 = &led1; + led2 = &led2; + led3 = &led3; + pwm-led0 = &pwm_led0; + sw0 = &button0; + sw1 = &button1; + sw2 = &button2; + sw3 = &button3; + bootloader-led0 = &led0; + mcuboot-button0 = &button0; + mcuboot-led0 = &led0; + watchdog0 = &wdt0; + spi-flash0 = &mx25r64; + }; + soc { + #address-cells = < 0x1 >; + #size-cells = < 0x1 >; + compatible = "nordic,nrf52840-qiaa", "nordic,nrf52840", "nordic,nrf52", + "simple-bus"; + interrupt-parent = < &nvic >; + ranges; + nvic: interrupt-controller@e000e100 { + #address-cells = < 0x1 >; + compatible = "arm,v7m-nvic"; + reg = < 0xe000e100 0xc00 >; + interrupt-controller; + #interrupt-cells = < 0x2 >; + arm,num-irq-priority-bits = < 0x3 >; + phandle = < 0x1 >; + }; + systick: timer@e000e010 { + compatible = "arm,armv7m-systick"; + reg = < 0xe000e010 0x10 >; + status = "disabled"; + }; + ficr: ficr@10000000 { + compatible = "nordic,nrf-ficr"; + reg = < 0x10000000 0x1000 >; + status = "okay"; + }; + uicr: uicr@10001000 { + compatible = "nordic,nrf-uicr"; + reg = < 0x10001000 0x1000 >; + status = "okay"; + gpio-as-nreset; + }; + sram0: memory@20000000 { + compatible = "mmio-sram"; + reg = < 0x20000000 0x40000 >; + }; + clock: clock@40000000 { + compatible = "nordic,nrf-clock"; + reg = < 0x40000000 0x1000 >; + interrupts = < 0x0 0x1 >; + status = "okay"; + }; + power: power@40000000 { + compatible = "nordic,nrf-power"; + reg = < 0x40000000 0x1000 >; + interrupts = < 0x0 0x1 >; + status = "okay"; + #address-cells = < 0x1 >; + #size-cells = < 0x1 >; + gpregret1: gpregret1@4000051c { + #address-cells = < 0x1 >; + #size-cells = < 0x1 >; + compatible = "nordic,nrf-gpregret"; + reg = < 0x4000051c 0x1 >; + status = "okay"; + }; + gpregret2: gpregret2@40000520 { + #address-cells = < 0x1 >; + #size-cells = < 0x1 >; + compatible = "nordic,nrf-gpregret"; + reg = < 0x40000520 0x1 >; + status = "okay"; + }; + }; + radio: radio@40001000 { + compatible = "nordic,nrf-radio"; + reg = < 0x40001000 0x1000 >; + interrupts = < 0x1 0x1 >; + status = "okay"; + ieee802154-supported; + ble-2mbps-supported; + ble-coded-phy-supported; + tx-high-power-supported; + ieee802154: ieee802154 { + compatible = "nordic,nrf-ieee802154"; + status = "okay"; + }; + }; + uart0: uart@40002000 { + compatible = "nordic,nrf-uarte"; + reg = < 0x40002000 0x1000 >; + interrupts = < 0x2 0x1 >; + status = "okay"; + current-speed = < 0x1c200 >; + pinctrl-0 = < &uart0_default >; + pinctrl-1 = < &uart0_sleep >; + pinctrl-names = "default", "sleep"; + }; + i2c0: arduino_i2c: i2c@40003000 { + compatible = "nordic,nrf-twi"; + #address-cells = < 0x1 >; + #size-cells = < 0x0 >; + reg = < 0x40003000 0x1000 >; + clock-frequency = < 0x186a0 >; + interrupts = < 0x3 0x1 >; + easydma-maxcnt-bits = < 0x10 >; + interrupt-names = "IRQ_i2c0"; + status = "okay"; + pinctrl-0 = < &i2c0_default >; + pinctrl-1 = < &i2c0_sleep >; + pinctrl-names = "default", "sleep"; + bme680_i2c: bme680@76 { + compatible = "bosch,bme680"; + reg = < 0x76 >; + }; + }; + spi0: spi@40003000 { + compatible = "nordic,nrf-spi"; + #address-cells = < 0x1 >; + #size-cells = < 0x0 >; + reg = < 0x40003000 0x1000 >; + interrupts = < 0x3 0x1 >; + max-frequency = < 0x7a1200 >; + easydma-maxcnt-bits = < 0x10 >; + status = "disabled"; + pinctrl-0 = < &spi0_default >; + pinctrl-1 = < &spi0_sleep >; + pinctrl-names = "default", "sleep"; + }; + i2c1: i2c@40004000 { + compatible = "nordic,nrf-twi"; + #address-cells = < 0x1 >; + #size-cells = < 0x0 >; + reg = < 0x40004000 0x1000 >; + clock-frequency = < 0x186a0 >; + interrupts = < 0x4 0x1 >; + status = "disabled"; + pinctrl-0 = < &i2c1_default >; + pinctrl-1 = < &i2c1_sleep >; + pinctrl-names = "default", "sleep"; + }; + spi1: spi@40004000 { + compatible = "nordic,nrf-spi"; + #address-cells = < 0x1 >; + #size-cells = < 0x0 >; + reg = < 0x40004000 0x1000 >; + interrupts = < 0x4 0x1 >; + max-frequency = < 0x7a1200 >; + easydma-maxcnt-bits = < 0x10 >; + status = "okay"; + pinctrl-0 = < &spi1_default >; + pinctrl-1 = < &spi1_sleep >; + pinctrl-names = "default", "sleep"; + bme680_spi: bme680@0 { + compatible = "bosch,bme680"; + reg = < 0x0 >; + spi-max-frequency = < 0xf4240 >; + }; + }; + nfct: nfct@40005000 { + compatible = "nordic,nrf-nfct"; + reg = < 0x40005000 0x1000 >; + interrupts = < 0x5 0x1 >; + status = "okay"; + }; + gpiote: gpiote@40006000 { + compatible = "nordic,nrf-gpiote"; + reg = < 0x40006000 0x1000 >; + interrupts = < 0x6 0x5 >; + instance = <0>; + status = "okay"; + }; + adc: adc@40007000 { + compatible = "nordic,nrf-saadc"; + reg = < 0x40007000 0x1000 >; + interrupts = < 0x7 0x1 >; + status = "okay"; + #io-channel-cells = < 0x1 >; + phandle = < 0x1b >; + }; + timer0: timer@40008000 { + compatible = "nordic,nrf-timer"; + status = "disabled"; + reg = < 0x40008000 0x1000 >; + cc-num = < 0x4 >; + max-bit-width = < 0x20 >; + interrupts = < 0x8 0x1 >; + prescaler = < 0x0 >; + }; + timer1: timer@40009000 { + compatible = "nordic,nrf-timer"; + status = "disabled"; + reg = < 0x40009000 0x1000 >; + cc-num = < 0x4 >; + max-bit-width = < 0x20 >; + interrupts = < 0x9 0x1 >; + prescaler = < 0x0 >; + phandle = < 0x17 >; + }; + timer2: timer@4000a000 { + compatible = "nordic,nrf-timer"; + status = "disabled"; + reg = < 0x4000a000 0x1000 >; + cc-num = < 0x4 >; + max-bit-width = < 0x20 >; + interrupts = < 0xa 0x1 >; + prescaler = < 0x0 >; + }; + rtc0: rtc@4000b000 { + compatible = "nordic,nrf-rtc"; + reg = < 0x4000b000 0x1000 >; + cc-num = < 0x3 >; + interrupts = < 0xb 0x1 >; + status = "disabled"; + clock-frequency = < 0x8000 >; + prescaler = < 0x1 >; + }; + temp: temp@4000c000 { + compatible = "nordic,nrf-temp"; + reg = < 0x4000c000 0x1000 >; + interrupts = < 0xc 0x1 >; + status = "okay"; + }; + rng: random@4000d000 { + compatible = "nordic,nrf-rng"; + reg = < 0x4000d000 0x1000 >; + interrupts = < 0xd 0x1 >; + status = "okay"; + }; + ecb: ecb@4000e000 { + compatible = "nordic,nrf-ecb"; + reg = < 0x4000e000 0x1000 >; + interrupts = < 0xe 0x1 >; + status = "okay"; + }; + ccm: ccm@4000f000 { + compatible = "nordic,nrf-ccm"; + reg = < 0x4000f000 0x1000 >; + interrupts = < 0xf 0x1 >; + length-field-length-8-bits; + status = "okay"; + }; + wdt: wdt0: watchdog@40010000 { + compatible = "nordic,nrf-wdt"; + reg = < 0x40010000 0x1000 >; + interrupts = < 0x10 0x1 >; + status = "okay"; + }; + rtc1: rtc@40011000 { + compatible = "nordic,nrf-rtc"; + reg = < 0x40011000 0x1000 >; + cc-num = < 0x4 >; + interrupts = < 0x11 0x1 >; + status = "disabled"; + clock-frequency = < 0x8000 >; + prescaler = < 0x1 >; + }; + qdec: qdec0: qdec@40012000 { + compatible = "nordic,nrf-qdec"; + reg = < 0x40012000 0x1000 >; + interrupts = < 0x12 0x1 >; + status = "disabled"; + }; + comp: comparator@40013000 { + compatible = "nordic,nrf-comp"; + reg = < 0x40013000 0x1000 >; + interrupts = < 0x13 0x1 >; + status = "disabled"; + #io-channel-cells = < 0x1 >; + }; + egu0: swi0: egu@40014000 { + compatible = "nordic,nrf-egu", "nordic,nrf-swi"; + reg = < 0x40014000 0x1000 >; + interrupts = < 0x14 0x1 >; + status = "okay"; + }; + egu1: swi1: egu@40015000 { + compatible = "nordic,nrf-egu", "nordic,nrf-swi"; + reg = < 0x40015000 0x1000 >; + interrupts = < 0x15 0x1 >; + status = "okay"; + }; + egu2: swi2: egu@40016000 { + compatible = "nordic,nrf-egu", "nordic,nrf-swi"; + reg = < 0x40016000 0x1000 >; + interrupts = < 0x16 0x1 >; + status = "okay"; + }; + egu3: swi3: egu@40017000 { + compatible = "nordic,nrf-egu", "nordic,nrf-swi"; + reg = < 0x40017000 0x1000 >; + interrupts = < 0x17 0x1 >; + status = "okay"; + }; + egu4: swi4: egu@40018000 { + compatible = "nordic,nrf-egu", "nordic,nrf-swi"; + reg = < 0x40018000 0x1000 >; + interrupts = < 0x18 0x1 >; + status = "okay"; + }; + egu5: swi5: egu@40019000 { + compatible = "nordic,nrf-egu", "nordic,nrf-swi"; + reg = < 0x40019000 0x1000 >; + interrupts = < 0x19 0x1 >; + status = "okay"; + }; + timer3: timer@4001a000 { + compatible = "nordic,nrf-timer"; + status = "disabled"; + reg = < 0x4001a000 0x1000 >; + cc-num = < 0x6 >; + max-bit-width = < 0x20 >; + interrupts = < 0x1a 0x1 >; + prescaler = < 0x0 >; + }; + timer4: timer@4001b000 { + compatible = "nordic,nrf-timer"; + status = "disabled"; + reg = < 0x4001b000 0x1000 >; + cc-num = < 0x6 >; + max-bit-width = < 0x20 >; + interrupts = < 0x1b 0x1 >; + prescaler = < 0x0 >; + }; + pwm0: pwm@4001c000 { + compatible = "nordic,nrf-pwm"; + reg = < 0x4001c000 0x1000 >; + interrupts = < 0x1c 0x1 >; + status = "okay"; + #pwm-cells = < 0x3 >; + pinctrl-0 = < &pwm0_default >; + pinctrl-1 = < &pwm0_sleep >; + pinctrl-names = "default", "sleep"; + phandle = < 0x19 >; + }; + pdm0: pdm@4001d000 { + compatible = "nordic,nrf-pdm"; + reg = < 0x4001d000 0x1000 >; + interrupts = < 0x1d 0x1 >; + status = "disabled"; + }; + acl: acl@4001e000 { + compatible = "nordic,nrf-acl"; + reg = < 0x4001e000 0x1000 >; + status = "okay"; + }; + flash_controller: flash-controller@4001e000 { + compatible = "nordic,nrf52-flash-controller"; + reg = < 0x4001e000 0x1000 >; + partial-erase; + #address-cells = < 0x1 >; + #size-cells = < 0x1 >; + flash0: flash@0 { + compatible = "soc-nv-flash"; + erase-block-size = < 0x1000 >; + write-block-size = < 0x4 >; + reg = < 0x0 0x100000 >; + partitions { + compatible = "fixed-partitions"; + #address-cells = < 0x1 >; + #size-cells = < 0x1 >; + boot_partition: partition@0 { + label = "mcuboot"; + reg = < 0x0 0xc000 >; + }; + slot0_partition: partition@c000 { + label = "image-0"; + reg = < 0xc000 0x76000 >; + }; + slot1_partition: partition@82000 { + label = "image-1"; + reg = < 0x82000 0x76000 >; + }; + storage_partition: partition@f8000 { + label = "storage"; + reg = < 0xf8000 0x8000 >; + }; + }; + }; + }; + ppi: ppi@4001f000 { + compatible = "nordic,nrf-ppi"; + reg = < 0x4001f000 0x1000 >; + status = "okay"; + }; + mwu: mwu@40020000 { + compatible = "nordic,nrf-mwu"; + reg = < 0x40020000 0x1000 >; + status = "okay"; + }; + pwm1: pwm@40021000 { + compatible = "nordic,nrf-pwm"; + reg = < 0x40021000 0x1000 >; + interrupts = < 0x21 0x1 >; + status = "disabled"; + #pwm-cells = < 0x3 >; + }; + pwm2: pwm@40022000 { + compatible = "nordic,nrf-pwm"; + reg = < 0x40022000 0x1000 >; + interrupts = < 0x22 0x1 >; + status = "disabled"; + #pwm-cells = < 0x3 >; + }; + spi2: spi@40023000 { + compatible = "nordic,nrf-spi"; + #address-cells = < 0x1 >; + #size-cells = < 0x0 >; + reg = < 0x40023000 0x1000 >; + interrupts = < 0x23 0x1 >; + max-frequency = < 0x7a1200 >; + easydma-maxcnt-bits = < 0x10 >; + status = "disabled"; + pinctrl-0 = < &spi2_default >; + pinctrl-1 = < &spi2_sleep >; + pinctrl-names = "default", "sleep"; + }; + rtc2: rtc@40024000 { + compatible = "nordic,nrf-rtc"; + reg = < 0x40024000 0x1000 >; + cc-num = < 0x4 >; + interrupts = < 0x24 0x1 >; + status = "disabled"; + clock-frequency = < 0x8000 >; + prescaler = < 0x1 >; + }; + i2s0: i2s@40025000 { + compatible = "nordic,nrf-i2s"; + #address-cells = < 0x1 >; + #size-cells = < 0x0 >; + reg = < 0x40025000 0x1000 >; + interrupts = < 0x25 0x1 >; + status = "disabled"; + }; + usbd: zephyr_udc0: usbd@40027000 { + compatible = "nordic,nrf-usbd"; + reg = < 0x40027000 0x1000 >; + interrupts = < 0x27 0x1 >; + num-bidir-endpoints = < 0x1 >; + num-in-endpoints = < 0x7 >; + num-out-endpoints = < 0x7 >; + num-isoin-endpoints = < 0x1 >; + num-isoout-endpoints = < 0x1 >; + status = "okay"; + }; + uart1: arduino_serial: uart@40028000 { + compatible = "nordic,nrf-uarte"; + reg = < 0x40028000 0x1000 >; + interrupts = < 0x28 0x1 >; + status = "disabled"; + current-speed = < 0x1c200 >; + pinctrl-0 = < &uart1_default >; + pinctrl-1 = < &uart1_sleep >; + pinctrl-names = "default", "sleep"; + }; + qspi: qspi@40029000 { + compatible = "nordic,nrf-qspi"; + #address-cells = < 0x1 >; + #size-cells = < 0x0 >; + reg = < 0x40029000 0x1000 >, < 0x12000000 0x8000000 >; + reg-names = "qspi", "qspi_mm"; + interrupts = < 0x29 0x1 >; + status = "okay"; + pinctrl-0 = < &qspi_default >; + pinctrl-1 = < &qspi_sleep >; + pinctrl-names = "default", "sleep"; + mx25r64: mx25r6435f@0 { + compatible = "nordic,qspi-nor"; + reg = < 0x0 >; + writeoc = "pp4io"; + readoc = "read4io"; + sck-frequency = < 0x7a1200 >; + jedec-id = [ C2 28 17 ]; + sfdp-bfp = [ + E5 20 F1 FF FF FF FF 03 44 EB 08 6B + 08 3B 04 BB EE FF FF FF FF FF 00 FF + FF FF 00 FF 0C 20 0F 52 10 D8 00 FF + 23 72 F5 00 82 ED 04 CC 44 83 68 44 + 30 B0 30 B0 F7 C4 D5 5C 00 BE 29 FF + F0 D0 FF FF ]; + size = < 0x4000000 >; + has-dpd; + t-enter-dpd = < 0x2710 >; + t-exit-dpd = < 0x88b8 >; + }; + }; + pwm3: pwm@4002d000 { + compatible = "nordic,nrf-pwm"; + reg = < 0x4002d000 0x1000 >; + interrupts = < 0x2d 0x1 >; + status = "disabled"; + #pwm-cells = < 0x3 >; + }; + spi3: arduino_spi: spi@4002f000 { + compatible = "nordic,nrf-spim"; + #address-cells = < 0x1 >; + #size-cells = < 0x0 >; + reg = < 0x4002f000 0x1000 >; + interrupts = < 0x2f 0x1 >; + max-frequency = < 0x1e84800 >; + easydma-maxcnt-bits = < 0x10 >; + rx-delay-supported; + rx-delay = < 0x2 >; + status = "okay"; + cs-gpios = < &arduino_header 0x10 0x1 >; + pinctrl-0 = < &spi3_default >; + pinctrl-1 = < &spi3_sleep >; + pinctrl-names = "default", "sleep"; + }; + gpio0: gpio@50000000 { + compatible = "nordic,nrf-gpio"; + gpio-controller; + reg = < 0x50000000 0x200 0x50000500 0x300 >; + #gpio-cells = < 0x2 >; + status = "okay"; + port = < 0x0 >; + phandle = < 0x18 >; + }; + gpio1: gpio@50000300 { + compatible = "nordic,nrf-gpio"; + gpio-controller; + reg = < 0x50000300 0x200 0x50000800 0x300 >; + #gpio-cells = < 0x2 >; + ngpios = < 0x10 >; + status = "okay"; + port = < 0x1 >; + phandle = < 0x1a >; + }; + cryptocell: crypto@5002a000 { + compatible = "nordic,cryptocell", "arm,cryptocell-310"; + reg = < 0x5002a000 0x1000 >, < 0x5002b000 0x1000 >; + reg-names = "wrapper", "core"; + interrupts = < 0x2a 0x1 >; + status = "disabled"; + }; + }; + pinctrl: pin-controller { + compatible = "nordic,nrf-pinctrl"; + uart0_default: uart0_default { + phandle = < 0x2 >; + group1 { + psels = < 0x6 >, < 0x20005 >; + }; + group2 { + psels = < 0x10008 >, < 0x30007 >; + bias-pull-up; + }; + }; + uart0_sleep: uart0_sleep { + phandle = < 0x3 >; + group1 { + psels = < 0x6 >, < 0x10008 >, < 0x20005 >, < 0x30007 >; + low-power-enable; + }; + }; + uart1_default: uart1_default { + phandle = < 0x10 >; + group1 { + psels = < 0x10021 >; + bias-pull-up; + }; + group2 { + psels = < 0x22 >; + }; + }; + uart1_sleep: uart1_sleep { + phandle = < 0x11 >; + group1 { + psels = < 0x10021 >, < 0x22 >; + low-power-enable; + }; + }; + i2c0_default: i2c0_default { + phandle = < 0x4 >; + group1 { + psels = < 0xc001a >, < 0xb001b >; + }; + }; + i2c0_sleep: i2c0_sleep { + phandle = < 0x5 >; + group1 { + psels = < 0xc001a >, < 0xb001b >; + low-power-enable; + }; + }; + i2c1_default: i2c1_default { + phandle = < 0x8 >; + group1 { + psels = < 0xc001e >, < 0xb001f >; + }; + }; + i2c1_sleep: i2c1_sleep { + phandle = < 0x9 >; + group1 { + psels = < 0xc001e >, < 0xb001f >; + low-power-enable; + }; + }; + pwm0_default: pwm0_default { + phandle = < 0xc >; + group1 { + psels = < 0x16000d >; + nordic,invert; + }; + }; + pwm0_sleep: pwm0_sleep { + phandle = < 0xd >; + group1 { + psels = < 0x16000d >; + low-power-enable; + }; + }; + spi0_default: spi0_default { + phandle = < 0x6 >; + group1 { + psels = < 0x4001b >, < 0x5001a >, < 0x6001d >; + }; + }; + spi0_sleep: spi0_sleep { + phandle = < 0x7 >; + group1 { + psels = < 0x4001b >, < 0x5001a >, < 0x6001d >; + low-power-enable; + }; + }; + spi1_default: spi1_default { + phandle = < 0xa >; + group1 { + psels = < 0x4001f >, < 0x5001e >, < 0x60028 >; + }; + }; + spi1_sleep: spi1_sleep { + phandle = < 0xb >; + group1 { + psels = < 0x4001f >, < 0x5001e >, < 0x60028 >; + low-power-enable; + }; + }; + spi2_default: spi2_default { + phandle = < 0xe >; + group1 { + psels = < 0x40013 >, < 0x50014 >, < 0x60015 >; + }; + }; + spi2_sleep: spi2_sleep { + phandle = < 0xf >; + group1 { + psels = < 0x40013 >, < 0x50014 >, < 0x60015 >; + low-power-enable; + }; + }; + qspi_default: qspi_default { + phandle = < 0x12 >; + group1 { + psels = < 0x1d0013 >, < 0x1f0014 >, < 0x200015 >, < 0x210016 >, + < 0x220017 >, < 0x1e0011 >; + nordic,drive-mode = < 0x3 >; + }; + }; + qspi_sleep: qspi_sleep { + phandle = < 0x13 >; + group1 { + psels = < 0x1d0013 >, < 0x1f0014 >, < 0x200015 >, < 0x210016 >, + < 0x220017 >; + low-power-enable; + }; + group2 { + psels = < 0x1e0011 >; + low-power-enable; + bias-pull-up; + }; + }; + spi3_default: spi3_default { + phandle = < 0x15 >; + group1 { + psels = < 0x4002f >, < 0x6002e >, < 0x5002d >; + }; + }; + spi3_sleep: spi3_sleep { + phandle = < 0x16 >; + group1 { + psels = < 0x4002f >, < 0x6002e >, < 0x5002d >; + low-power-enable; + }; + }; + }; + rng_hci: entropy_bt_hci { + compatible = "zephyr,bt-hci-entropy"; + status = "okay"; + }; + sw_pwm: sw-pwm { + compatible = "nordic,nrf-sw-pwm"; + status = "disabled"; + generator = < &timer1 >; + clock-prescaler = < 0x0 >; + #pwm-cells = < 0x3 >; + }; + cpus { + #address-cells = < 0x1 >; + #size-cells = < 0x0 >; + cpu@0 { + device_type = "cpu"; + compatible = "arm,cortex-m4f"; + reg = < 0x0 >; + #address-cells = < 0x1 >; + #size-cells = < 0x1 >; + itm: itm@e0000000 { + compatible = "arm,armv7m-itm"; + reg = < 0xe0000000 0x1000 >; + swo-ref-frequency = < 0x1e84800 >; + }; + }; + }; + leds { + compatible = "gpio-leds"; + led0: led_0 { + gpios = < &gpio0 0xd 0x1 >; + label = "Green LED 0"; + }; + led1: led_1 { + gpios = < &gpio0 0xe 0x1 >; + label = "Green LED 1"; + }; + led2: led_2 { + gpios = < &gpio0 0xf 0x1 >; + label = "Green LED 2"; + }; + led3: led_3 { + gpios = < &gpio0 0x10 0x1 >; + label = "Green LED 3"; + }; + }; + pwmleds { + compatible = "pwm-leds"; + pwm_led0: pwm_led_0 { + pwms = < &pwm0 0x0 0x1312d00 0x1 >; + }; + }; + buttons { + compatible = "gpio-keys"; + button0: button_0 { + gpios = < &gpio0 0xb 0x11 >; + label = "Push button switch 0"; + zephyr,code = < 0xb >; + }; + button1: button_1 { + gpios = < &gpio0 0xc 0x11 >; + label = "Push button switch 1"; + zephyr,code = < 0x2 >; + }; + button2: button_2 { + gpios = < &gpio0 0x18 0x11 >; + label = "Push button switch 2"; + zephyr,code = < 0x3 >; + }; + button3: button_3 { + gpios = < &gpio0 0x19 0x11 >; + label = "Push button switch 3"; + zephyr,code = < 0x4 >; + }; + }; + arduino_header: connector { + compatible = "arduino-header-r3"; + #gpio-cells = < 0x2 >; + gpio-map-mask = < 0xffffffff 0xffffffc0 >; + gpio-map-pass-thru = < 0x0 0x3f >; + gpio-map = < 0x0 0x0 &gpio0 0x3 0x0 >, < 0x1 0x0 &gpio0 0x4 0x0 >, + < 0x2 0x0 &gpio0 0x1c 0x0 >, < 0x3 0x0 &gpio0 0x1d 0x0 >, + < 0x4 0x0 &gpio0 0x1e 0x0 >, < 0x5 0x0 &gpio0 0x1f 0x0 >, + < 0x6 0x0 &gpio1 0x1 0x0 >, < 0x7 0x0 &gpio1 0x2 0x0 >, + < 0x8 0x0 &gpio1 0x3 0x0 >, < 0x9 0x0 &gpio1 0x4 0x0 >, + < 0xa 0x0 &gpio1 0x5 0x0 >, < 0xb 0x0 &gpio1 0x6 0x0 >, + < 0xc 0x0 &gpio1 0x7 0x0 >, < 0xd 0x0 &gpio1 0x8 0x0 >, + < 0xe 0x0 &gpio1 0xa 0x0 >, < 0xf 0x0 &gpio1 0xb 0x0 >, + < 0x10 0x0 &gpio1 0xc 0x0 >, < 0x11 0x0 &gpio1 0xd 0x0 >, + < 0x12 0x0 &gpio1 0xe 0x0 >, < 0x13 0x0 &gpio1 0xf 0x0 >, + < 0x14 0x0 &gpio0 0x1a 0x0 >, < 0x15 0x0 &gpio0 0x1b 0x0 >; + phandle = < 0x14 >; + }; + arduino_adc: analog-connector { + compatible = "arduino,uno-adc"; + #io-channel-cells = < 0x1 >; + io-channel-map = < 0x0 &adc 0x1 >, < 0x1 &adc 0x2 >, < 0x2 &adc 0x4 >, + < 0x3 &adc 0x5 >, < 0x4 &adc 0x6 >, < 0x5 &adc 0x7 >; + }; +}; diff --git a/tests/res/CMakeCache.txt b/tests/res/CMakeCache.txt new file mode 100644 index 0000000..faf9e30 --- /dev/null +++ b/tests/res/CMakeCache.txt @@ -0,0 +1,8 @@ +######################### +# CMakeCache test entries +######################### + +DTSH_TEST_STRING_LIST:STRING=foo;bar +DTSH_TEST_STRING:STRING=foobar +DTSH_TEST_BOOL_TRUE:BOOL=ON +DTSH_TEST_BOOL_FALSE:BOOL=0 diff --git a/tests/res/README b/tests/res/README new file mode 100644 index 0000000..87bdd5a --- /dev/null +++ b/tests/res/README @@ -0,0 +1,67 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +DTSh: Unit tests resource files + ++ Build_gnuarm/ + Sample DTS and corresponding CMakeCache.txt, + built with "gnuarmemb" toolchain variant. + ++ Build_gnuarm/ + Sample DTS and corresponding CMakeCache.txt, + built with "zephyr" toolchain variant. + ++ fs/ + Pseudo file-system root for file path auto-completion unit tests. + ++ ini/ + Configuration test files. + ++ theme/ + Theme test files. + ++ yaml/ + YAML test files. + ++ CMakeCache.txt: CMake cache file parser test file. + ++ zephyr.dts: A DTS file for which we won't be able + to find a corresponding CMakeCache.txt. + + +Devicetree sample: + +Sample DTS and CMakeCache.txt files are generated +when building zephyr/samples/sensor/bme680: +- for the nrf52840dk_nrf52840 board +- edited to connect a second BME680 to the SPI bus + and add an 'interrupt-names' property to test with + +nrf52840dk_nrf52840.overlay: + + &i2c0 { + bme680_i2c: bme680@76 { + compatible = "bosch,bme680"; + reg = <0x76>; + }; + }; + + &spi1 { + bme680_spi: bme680@0 { + compatible = "bosch,bme680"; + reg = <0>; + spi-max-frequency = <1000000>; /* conservatively set to 1MHz */ + }; + }; + + +prj.conf: + + CONFIG_SPI=y + +zephyr.dts: + + i2c0: arduino_i2c: i2c@40003000 { + interrupt-names = "IRQ_i2c0"; + }; diff --git a/tests/res/fs/A.txt b/tests/res/fs/A.txt new file mode 100644 index 0000000..8b77ca3 --- /dev/null +++ b/tests/res/fs/A.txt @@ -0,0 +1 @@ +# Dummy file for DtShAutocomp.complete_fspath() unit tests. diff --git a/tests/res/fs/A/dummy.txt b/tests/res/fs/A/dummy.txt new file mode 100644 index 0000000..8b77ca3 --- /dev/null +++ b/tests/res/fs/A/dummy.txt @@ -0,0 +1 @@ +# Dummy file for DtShAutocomp.complete_fspath() unit tests. diff --git a/tests/res/fs/a.txt b/tests/res/fs/a.txt new file mode 100644 index 0000000..8b77ca3 --- /dev/null +++ b/tests/res/fs/a.txt @@ -0,0 +1 @@ +# Dummy file for DtShAutocomp.complete_fspath() unit tests. diff --git a/tests/res/fs/a/x/dummy.txt b/tests/res/fs/a/x/dummy.txt new file mode 100644 index 0000000..8b77ca3 --- /dev/null +++ b/tests/res/fs/a/x/dummy.txt @@ -0,0 +1 @@ +# Dummy file for DtShAutocomp.complete_fspath() unit tests. diff --git a/tests/res/fs/ab.txt b/tests/res/fs/ab.txt new file mode 100644 index 0000000..8b77ca3 --- /dev/null +++ b/tests/res/fs/ab.txt @@ -0,0 +1 @@ +# Dummy file for DtShAutocomp.complete_fspath() unit tests. diff --git a/tests/res/fs/foo/dummy.txt b/tests/res/fs/foo/dummy.txt new file mode 100644 index 0000000..8b77ca3 --- /dev/null +++ b/tests/res/fs/foo/dummy.txt @@ -0,0 +1 @@ +# Dummy file for DtShAutocomp.complete_fspath() unit tests. diff --git a/tests/res/ini/override.ini b/tests/res/ini/override.ini new file mode 100644 index 0000000..1a765cb --- /dev/null +++ b/tests/res/ini/override.ini @@ -0,0 +1,5 @@ +[dtsh] +# Override an existing option. +test.string = overridden +# Add an option. +test.new = new diff --git a/tests/res/ini/test.ini b/tests/res/ini/test.ini new file mode 100644 index 0000000..00a7cb0 --- /dev/null +++ b/tests/res/ini/test.ini @@ -0,0 +1,37 @@ +[dtsh] + +# Missing right-value in assignment. +test.novalue = + +# Booleans, API DtshConfig.getbool(): +# - True: '1', 'yes', 'true', and 'on' +# - False: '0', 'no', 'false', and 'off' +test.true = yes +test.false = no +test.bool.inval = not a bool + +# Integers, API DtshConfig.getint(): +# - base- 2, -8, -10 and -16 are supported +# - base-2, -8, and -16 literals can be optionally prefixed +# with 0b/0B, 0o/0O, or 0x/0X +test.int = 255 +test.hex = 0xff +test.int.inval = not an int + +# Strings, API DtshConfig.getstring(): +# - double-quote with " when containing spaces +# - \u escape sequence, which is followed by four hex digits giving +# the code point (e.g. \u2768) +# - the \U escape sequence is similar, but expects eight hex digits +test.string = a string +test.string.quoted = "quoted string " +test.string.unicode = ❯ +test.string.literal = \u276F +test.string.mixed = "\u276F ❯" + +# Interpolation. +test.hello = hello +test.interpolation = ${test.hello} world + +# Double quotes themselves. +test.string.quotes = "a"b" diff --git a/tests/res/theme/override.ini b/tests/res/theme/override.ini new file mode 100644 index 0000000..44137d5 --- /dev/null +++ b/tests/res/theme/override.ini @@ -0,0 +1,5 @@ +[styles] +# Override an existing style. +test.default = green on black +# Add new style. +test.new = cyan diff --git a/tests/res/theme/test.ini b/tests/res/theme/test.ini new file mode 100644 index 0000000..d0e82c8 --- /dev/null +++ b/tests/res/theme/test.ini @@ -0,0 +1,7 @@ +[styles] +test.default = default on default + +test.red = red +test.bold = bold + +test.interpolation = %(test.red)s italic diff --git a/tests/res/yaml/i2c-device.yaml b/tests/res/yaml/i2c-device.yaml new file mode 100644 index 0000000..de3824f --- /dev/null +++ b/tests/res/yaml/i2c-device.yaml @@ -0,0 +1,13 @@ +# Copyright (c) 2017, Linaro Limited +# SPDX-License-Identifier: Apache-2.0 + +# Common fields for I2C devices + +include: [base.yaml, power.yaml] + +on-bus: i2c + +properties: + reg: + required: true + description: device address on i2c bus diff --git a/tests/res/yaml/power.yaml b/tests/res/yaml/power.yaml new file mode 100644 index 0000000..1091257 --- /dev/null +++ b/tests/res/yaml/power.yaml @@ -0,0 +1,31 @@ +# Copyright (c) 2020, Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +# Properties for nodes with controllable power supplies. + +properties: + supply-gpios: + type: phandle-array + description: | + GPIO specifier that controls power to the device. + + This property should be provided when the device has a dedicated + switch that controls power to the device. The supply state is + entirely the responsibility of the device driver. + + Contrast with vin-supply. + + vin-supply: + type: phandle + description: | + Reference to the regulator that controls power to the device. + The referenced devicetree node must have a regulator compatible. + + This property should be provided when device power is supplied + by a shared regulator. The supply state is dependent on the + request status of all devices fed by the regulator. + + Contrast with supply-gpios. If both properties are provided + then the regulator must be requested before the supply GPIOS is + set to an active state, and the supply GPIOS must be set to an + inactive state before releasing the regulator. diff --git a/tests/res/yaml/sensor-device.yaml b/tests/res/yaml/sensor-device.yaml new file mode 100644 index 0000000..ad905e2 --- /dev/null +++ b/tests/res/yaml/sensor-device.yaml @@ -0,0 +1,19 @@ +# Copyright (c) 2022 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +description: Sensor Device + +include: base.yaml + +properties: + friendly-name: + type: string + description: | + Human readable string describing the sensor. It can be used to + distinguish multiple instances of the same model (e.g., lid accelerometer + vs. base accelerometer in a laptop) to a host operating system. + + This property is defined in the Generic Sensor Property Usages of the HID + Usage Tables specification + (https://usb.org/sites/default/files/hut1_3_0.pdf, section 22.5). diff --git a/tests/res/zephyr.dts b/tests/res/zephyr.dts new file mode 100644 index 0000000..662ae6b --- /dev/null +++ b/tests/res/zephyr.dts @@ -0,0 +1,812 @@ +/dts-v1/; + +/ { + #address-cells = < 0x1 >; + #size-cells = < 0x1 >; + model = "Nordic nRF52840 DK NRF52840"; + compatible = "nordic,nrf52840-dk-nrf52840"; + chosen { + zephyr,entropy = &rng; + zephyr,flash-controller = &flash_controller; + zephyr,console = &uart0; + zephyr,shell-uart = &uart0; + zephyr,uart-mcumgr = &uart0; + zephyr,bt-mon-uart = &uart0; + zephyr,bt-c2h-uart = &uart0; + zephyr,sram = &sram0; + zephyr,flash = &flash0; + zephyr,code-partition = &slot0_partition; + zephyr,ieee802154 = &ieee802154; + }; + aliases { + led0 = &led0; + led1 = &led1; + led2 = &led2; + led3 = &led3; + pwm-led0 = &pwm_led0; + sw0 = &button0; + sw1 = &button1; + sw2 = &button2; + sw3 = &button3; + bootloader-led0 = &led0; + mcuboot-button0 = &button0; + mcuboot-led0 = &led0; + watchdog0 = &wdt0; + spi-flash0 = &mx25r64; + }; + soc { + #address-cells = < 0x1 >; + #size-cells = < 0x1 >; + compatible = "nordic,nrf52840-qiaa", "nordic,nrf52840", "nordic,nrf52", + "simple-bus"; + interrupt-parent = < &nvic >; + ranges; + nvic: interrupt-controller@e000e100 { + #address-cells = < 0x1 >; + compatible = "arm,v7m-nvic"; + reg = < 0xe000e100 0xc00 >; + interrupt-controller; + #interrupt-cells = < 0x2 >; + arm,num-irq-priority-bits = < 0x3 >; + phandle = < 0x1 >; + }; + systick: timer@e000e010 { + compatible = "arm,armv7m-systick"; + reg = < 0xe000e010 0x10 >; + status = "disabled"; + }; + ficr: ficr@10000000 { + compatible = "nordic,nrf-ficr"; + reg = < 0x10000000 0x1000 >; + status = "okay"; + }; + uicr: uicr@10001000 { + compatible = "nordic,nrf-uicr"; + reg = < 0x10001000 0x1000 >; + status = "okay"; + gpio-as-nreset; + }; + sram0: memory@20000000 { + compatible = "mmio-sram"; + reg = < 0x20000000 0x40000 >; + }; + clock: clock@40000000 { + compatible = "nordic,nrf-clock"; + reg = < 0x40000000 0x1000 >; + interrupts = < 0x0 0x1 >; + status = "okay"; + }; + power: power@40000000 { + compatible = "nordic,nrf-power"; + reg = < 0x40000000 0x1000 >; + interrupts = < 0x0 0x1 >; + status = "okay"; + #address-cells = < 0x1 >; + #size-cells = < 0x1 >; + gpregret1: gpregret1@4000051c { + #address-cells = < 0x1 >; + #size-cells = < 0x1 >; + compatible = "nordic,nrf-gpregret"; + reg = < 0x4000051c 0x1 >; + status = "okay"; + }; + gpregret2: gpregret2@40000520 { + #address-cells = < 0x1 >; + #size-cells = < 0x1 >; + compatible = "nordic,nrf-gpregret"; + reg = < 0x40000520 0x1 >; + status = "okay"; + }; + }; + radio: radio@40001000 { + compatible = "nordic,nrf-radio"; + reg = < 0x40001000 0x1000 >; + interrupts = < 0x1 0x1 >; + status = "okay"; + ieee802154-supported; + ble-2mbps-supported; + ble-coded-phy-supported; + tx-high-power-supported; + ieee802154: ieee802154 { + compatible = "nordic,nrf-ieee802154"; + status = "okay"; + }; + }; + uart0: uart@40002000 { + compatible = "nordic,nrf-uarte"; + reg = < 0x40002000 0x1000 >; + interrupts = < 0x2 0x1 >; + status = "okay"; + current-speed = < 0x1c200 >; + pinctrl-0 = < &uart0_default >; + pinctrl-1 = < &uart0_sleep >; + pinctrl-names = "default", "sleep"; + }; + i2c0: arduino_i2c: i2c@40003000 { + compatible = "nordic,nrf-twi"; + #address-cells = < 0x1 >; + #size-cells = < 0x0 >; + reg = < 0x40003000 0x1000 >; + clock-frequency = < 0x186a0 >; + interrupts = < 0x3 0x1 >; + easydma-maxcnt-bits = < 0x10 >; + interrupt-names = "IRQ_i2c0"; + status = "okay"; + pinctrl-0 = < &i2c0_default >; + pinctrl-1 = < &i2c0_sleep >; + pinctrl-names = "default", "sleep"; + bme680_i2c: bme680@76 { + compatible = "bosch,bme680"; + reg = < 0x76 >; + }; + }; + spi0: spi@40003000 { + compatible = "nordic,nrf-spi"; + #address-cells = < 0x1 >; + #size-cells = < 0x0 >; + reg = < 0x40003000 0x1000 >; + interrupts = < 0x3 0x1 >; + max-frequency = < 0x7a1200 >; + easydma-maxcnt-bits = < 0x10 >; + status = "disabled"; + pinctrl-0 = < &spi0_default >; + pinctrl-1 = < &spi0_sleep >; + pinctrl-names = "default", "sleep"; + }; + i2c1: i2c@40004000 { + compatible = "nordic,nrf-twi"; + #address-cells = < 0x1 >; + #size-cells = < 0x0 >; + reg = < 0x40004000 0x1000 >; + clock-frequency = < 0x186a0 >; + interrupts = < 0x4 0x1 >; + status = "disabled"; + pinctrl-0 = < &i2c1_default >; + pinctrl-1 = < &i2c1_sleep >; + pinctrl-names = "default", "sleep"; + }; + spi1: spi@40004000 { + compatible = "nordic,nrf-spi"; + #address-cells = < 0x1 >; + #size-cells = < 0x0 >; + reg = < 0x40004000 0x1000 >; + interrupts = < 0x4 0x1 >; + max-frequency = < 0x7a1200 >; + easydma-maxcnt-bits = < 0x10 >; + status = "okay"; + pinctrl-0 = < &spi1_default >; + pinctrl-1 = < &spi1_sleep >; + pinctrl-names = "default", "sleep"; + bme680_spi: bme680@0 { + compatible = "bosch,bme680"; + reg = < 0x0 >; + spi-max-frequency = < 0xf4240 >; + }; + }; + nfct: nfct@40005000 { + compatible = "nordic,nrf-nfct"; + reg = < 0x40005000 0x1000 >; + interrupts = < 0x5 0x1 >; + status = "okay"; + }; + gpiote: gpiote@40006000 { + compatible = "nordic,nrf-gpiote"; + reg = < 0x40006000 0x1000 >; + interrupts = < 0x6 0x5 >; + instance = <0>; + status = "okay"; + }; + adc: adc@40007000 { + compatible = "nordic,nrf-saadc"; + reg = < 0x40007000 0x1000 >; + interrupts = < 0x7 0x1 >; + status = "okay"; + #io-channel-cells = < 0x1 >; + phandle = < 0x1b >; + }; + timer0: timer@40008000 { + compatible = "nordic,nrf-timer"; + status = "disabled"; + reg = < 0x40008000 0x1000 >; + cc-num = < 0x4 >; + max-bit-width = < 0x20 >; + interrupts = < 0x8 0x1 >; + prescaler = < 0x0 >; + }; + timer1: timer@40009000 { + compatible = "nordic,nrf-timer"; + status = "disabled"; + reg = < 0x40009000 0x1000 >; + cc-num = < 0x4 >; + max-bit-width = < 0x20 >; + interrupts = < 0x9 0x1 >; + prescaler = < 0x0 >; + phandle = < 0x17 >; + }; + timer2: timer@4000a000 { + compatible = "nordic,nrf-timer"; + status = "disabled"; + reg = < 0x4000a000 0x1000 >; + cc-num = < 0x4 >; + max-bit-width = < 0x20 >; + interrupts = < 0xa 0x1 >; + prescaler = < 0x0 >; + }; + rtc0: rtc@4000b000 { + compatible = "nordic,nrf-rtc"; + reg = < 0x4000b000 0x1000 >; + cc-num = < 0x3 >; + interrupts = < 0xb 0x1 >; + status = "disabled"; + clock-frequency = < 0x8000 >; + prescaler = < 0x1 >; + }; + temp: temp@4000c000 { + compatible = "nordic,nrf-temp"; + reg = < 0x4000c000 0x1000 >; + interrupts = < 0xc 0x1 >; + status = "okay"; + }; + rng: random@4000d000 { + compatible = "nordic,nrf-rng"; + reg = < 0x4000d000 0x1000 >; + interrupts = < 0xd 0x1 >; + status = "okay"; + }; + ecb: ecb@4000e000 { + compatible = "nordic,nrf-ecb"; + reg = < 0x4000e000 0x1000 >; + interrupts = < 0xe 0x1 >; + status = "okay"; + }; + ccm: ccm@4000f000 { + compatible = "nordic,nrf-ccm"; + reg = < 0x4000f000 0x1000 >; + interrupts = < 0xf 0x1 >; + length-field-length-8-bits; + status = "okay"; + }; + wdt: wdt0: watchdog@40010000 { + compatible = "nordic,nrf-wdt"; + reg = < 0x40010000 0x1000 >; + interrupts = < 0x10 0x1 >; + status = "okay"; + }; + rtc1: rtc@40011000 { + compatible = "nordic,nrf-rtc"; + reg = < 0x40011000 0x1000 >; + cc-num = < 0x4 >; + interrupts = < 0x11 0x1 >; + status = "disabled"; + clock-frequency = < 0x8000 >; + prescaler = < 0x1 >; + }; + qdec: qdec0: qdec@40012000 { + compatible = "nordic,nrf-qdec"; + reg = < 0x40012000 0x1000 >; + interrupts = < 0x12 0x1 >; + status = "disabled"; + }; + comp: comparator@40013000 { + compatible = "nordic,nrf-comp"; + reg = < 0x40013000 0x1000 >; + interrupts = < 0x13 0x1 >; + status = "disabled"; + #io-channel-cells = < 0x1 >; + }; + egu0: swi0: egu@40014000 { + compatible = "nordic,nrf-egu", "nordic,nrf-swi"; + reg = < 0x40014000 0x1000 >; + interrupts = < 0x14 0x1 >; + status = "okay"; + }; + egu1: swi1: egu@40015000 { + compatible = "nordic,nrf-egu", "nordic,nrf-swi"; + reg = < 0x40015000 0x1000 >; + interrupts = < 0x15 0x1 >; + status = "okay"; + }; + egu2: swi2: egu@40016000 { + compatible = "nordic,nrf-egu", "nordic,nrf-swi"; + reg = < 0x40016000 0x1000 >; + interrupts = < 0x16 0x1 >; + status = "okay"; + }; + egu3: swi3: egu@40017000 { + compatible = "nordic,nrf-egu", "nordic,nrf-swi"; + reg = < 0x40017000 0x1000 >; + interrupts = < 0x17 0x1 >; + status = "okay"; + }; + egu4: swi4: egu@40018000 { + compatible = "nordic,nrf-egu", "nordic,nrf-swi"; + reg = < 0x40018000 0x1000 >; + interrupts = < 0x18 0x1 >; + status = "okay"; + }; + egu5: swi5: egu@40019000 { + compatible = "nordic,nrf-egu", "nordic,nrf-swi"; + reg = < 0x40019000 0x1000 >; + interrupts = < 0x19 0x1 >; + status = "okay"; + }; + timer3: timer@4001a000 { + compatible = "nordic,nrf-timer"; + status = "disabled"; + reg = < 0x4001a000 0x1000 >; + cc-num = < 0x6 >; + max-bit-width = < 0x20 >; + interrupts = < 0x1a 0x1 >; + prescaler = < 0x0 >; + }; + timer4: timer@4001b000 { + compatible = "nordic,nrf-timer"; + status = "disabled"; + reg = < 0x4001b000 0x1000 >; + cc-num = < 0x6 >; + max-bit-width = < 0x20 >; + interrupts = < 0x1b 0x1 >; + prescaler = < 0x0 >; + }; + pwm0: pwm@4001c000 { + compatible = "nordic,nrf-pwm"; + reg = < 0x4001c000 0x1000 >; + interrupts = < 0x1c 0x1 >; + status = "okay"; + #pwm-cells = < 0x3 >; + pinctrl-0 = < &pwm0_default >; + pinctrl-1 = < &pwm0_sleep >; + pinctrl-names = "default", "sleep"; + phandle = < 0x19 >; + }; + pdm0: pdm@4001d000 { + compatible = "nordic,nrf-pdm"; + reg = < 0x4001d000 0x1000 >; + interrupts = < 0x1d 0x1 >; + status = "disabled"; + }; + acl: acl@4001e000 { + compatible = "nordic,nrf-acl"; + reg = < 0x4001e000 0x1000 >; + status = "okay"; + }; + flash_controller: flash-controller@4001e000 { + compatible = "nordic,nrf52-flash-controller"; + reg = < 0x4001e000 0x1000 >; + partial-erase; + #address-cells = < 0x1 >; + #size-cells = < 0x1 >; + flash0: flash@0 { + compatible = "soc-nv-flash"; + erase-block-size = < 0x1000 >; + write-block-size = < 0x4 >; + reg = < 0x0 0x100000 >; + partitions { + compatible = "fixed-partitions"; + #address-cells = < 0x1 >; + #size-cells = < 0x1 >; + boot_partition: partition@0 { + label = "mcuboot"; + reg = < 0x0 0xc000 >; + }; + slot0_partition: partition@c000 { + label = "image-0"; + reg = < 0xc000 0x76000 >; + }; + slot1_partition: partition@82000 { + label = "image-1"; + reg = < 0x82000 0x76000 >; + }; + storage_partition: partition@f8000 { + label = "storage"; + reg = < 0xf8000 0x8000 >; + }; + }; + }; + }; + ppi: ppi@4001f000 { + compatible = "nordic,nrf-ppi"; + reg = < 0x4001f000 0x1000 >; + status = "okay"; + }; + mwu: mwu@40020000 { + compatible = "nordic,nrf-mwu"; + reg = < 0x40020000 0x1000 >; + status = "okay"; + }; + pwm1: pwm@40021000 { + compatible = "nordic,nrf-pwm"; + reg = < 0x40021000 0x1000 >; + interrupts = < 0x21 0x1 >; + status = "disabled"; + #pwm-cells = < 0x3 >; + }; + pwm2: pwm@40022000 { + compatible = "nordic,nrf-pwm"; + reg = < 0x40022000 0x1000 >; + interrupts = < 0x22 0x1 >; + status = "disabled"; + #pwm-cells = < 0x3 >; + }; + spi2: spi@40023000 { + compatible = "nordic,nrf-spi"; + #address-cells = < 0x1 >; + #size-cells = < 0x0 >; + reg = < 0x40023000 0x1000 >; + interrupts = < 0x23 0x1 >; + max-frequency = < 0x7a1200 >; + easydma-maxcnt-bits = < 0x10 >; + status = "disabled"; + pinctrl-0 = < &spi2_default >; + pinctrl-1 = < &spi2_sleep >; + pinctrl-names = "default", "sleep"; + }; + rtc2: rtc@40024000 { + compatible = "nordic,nrf-rtc"; + reg = < 0x40024000 0x1000 >; + cc-num = < 0x4 >; + interrupts = < 0x24 0x1 >; + status = "disabled"; + clock-frequency = < 0x8000 >; + prescaler = < 0x1 >; + }; + i2s0: i2s@40025000 { + compatible = "nordic,nrf-i2s"; + #address-cells = < 0x1 >; + #size-cells = < 0x0 >; + reg = < 0x40025000 0x1000 >; + interrupts = < 0x25 0x1 >; + status = "disabled"; + }; + usbd: zephyr_udc0: usbd@40027000 { + compatible = "nordic,nrf-usbd"; + reg = < 0x40027000 0x1000 >; + interrupts = < 0x27 0x1 >; + num-bidir-endpoints = < 0x1 >; + num-in-endpoints = < 0x7 >; + num-out-endpoints = < 0x7 >; + num-isoin-endpoints = < 0x1 >; + num-isoout-endpoints = < 0x1 >; + status = "okay"; + }; + uart1: arduino_serial: uart@40028000 { + compatible = "nordic,nrf-uarte"; + reg = < 0x40028000 0x1000 >; + interrupts = < 0x28 0x1 >; + status = "disabled"; + current-speed = < 0x1c200 >; + pinctrl-0 = < &uart1_default >; + pinctrl-1 = < &uart1_sleep >; + pinctrl-names = "default", "sleep"; + }; + qspi: qspi@40029000 { + compatible = "nordic,nrf-qspi"; + #address-cells = < 0x1 >; + #size-cells = < 0x0 >; + reg = < 0x40029000 0x1000 >, < 0x12000000 0x8000000 >; + reg-names = "qspi", "qspi_mm"; + interrupts = < 0x29 0x1 >; + status = "okay"; + pinctrl-0 = < &qspi_default >; + pinctrl-1 = < &qspi_sleep >; + pinctrl-names = "default", "sleep"; + mx25r64: mx25r6435f@0 { + compatible = "nordic,qspi-nor"; + reg = < 0x0 >; + writeoc = "pp4io"; + readoc = "read4io"; + sck-frequency = < 0x7a1200 >; + jedec-id = [ C2 28 17 ]; + sfdp-bfp = [ + E5 20 F1 FF FF FF FF 03 44 EB 08 6B + 08 3B 04 BB EE FF FF FF FF FF 00 FF + FF FF 00 FF 0C 20 0F 52 10 D8 00 FF + 23 72 F5 00 82 ED 04 CC 44 83 68 44 + 30 B0 30 B0 F7 C4 D5 5C 00 BE 29 FF + F0 D0 FF FF ]; + size = < 0x4000000 >; + has-dpd; + t-enter-dpd = < 0x2710 >; + t-exit-dpd = < 0x88b8 >; + }; + }; + pwm3: pwm@4002d000 { + compatible = "nordic,nrf-pwm"; + reg = < 0x4002d000 0x1000 >; + interrupts = < 0x2d 0x1 >; + status = "disabled"; + #pwm-cells = < 0x3 >; + }; + spi3: arduino_spi: spi@4002f000 { + compatible = "nordic,nrf-spim"; + #address-cells = < 0x1 >; + #size-cells = < 0x0 >; + reg = < 0x4002f000 0x1000 >; + interrupts = < 0x2f 0x1 >; + max-frequency = < 0x1e84800 >; + easydma-maxcnt-bits = < 0x10 >; + rx-delay-supported; + rx-delay = < 0x2 >; + status = "okay"; + cs-gpios = < &arduino_header 0x10 0x1 >; + pinctrl-0 = < &spi3_default >; + pinctrl-1 = < &spi3_sleep >; + pinctrl-names = "default", "sleep"; + }; + gpio0: gpio@50000000 { + compatible = "nordic,nrf-gpio"; + gpio-controller; + reg = < 0x50000000 0x200 0x50000500 0x300 >; + #gpio-cells = < 0x2 >; + status = "okay"; + port = < 0x0 >; + phandle = < 0x18 >; + }; + gpio1: gpio@50000300 { + compatible = "nordic,nrf-gpio"; + gpio-controller; + reg = < 0x50000300 0x200 0x50000800 0x300 >; + #gpio-cells = < 0x2 >; + ngpios = < 0x10 >; + status = "okay"; + port = < 0x1 >; + phandle = < 0x1a >; + }; + cryptocell: crypto@5002a000 { + compatible = "nordic,cryptocell", "arm,cryptocell-310"; + reg = < 0x5002a000 0x1000 >, < 0x5002b000 0x1000 >; + reg-names = "wrapper", "core"; + interrupts = < 0x2a 0x1 >; + status = "disabled"; + }; + }; + pinctrl: pin-controller { + compatible = "nordic,nrf-pinctrl"; + uart0_default: uart0_default { + phandle = < 0x2 >; + group1 { + psels = < 0x6 >, < 0x20005 >; + }; + group2 { + psels = < 0x10008 >, < 0x30007 >; + bias-pull-up; + }; + }; + uart0_sleep: uart0_sleep { + phandle = < 0x3 >; + group1 { + psels = < 0x6 >, < 0x10008 >, < 0x20005 >, < 0x30007 >; + low-power-enable; + }; + }; + uart1_default: uart1_default { + phandle = < 0x10 >; + group1 { + psels = < 0x10021 >; + bias-pull-up; + }; + group2 { + psels = < 0x22 >; + }; + }; + uart1_sleep: uart1_sleep { + phandle = < 0x11 >; + group1 { + psels = < 0x10021 >, < 0x22 >; + low-power-enable; + }; + }; + i2c0_default: i2c0_default { + phandle = < 0x4 >; + group1 { + psels = < 0xc001a >, < 0xb001b >; + }; + }; + i2c0_sleep: i2c0_sleep { + phandle = < 0x5 >; + group1 { + psels = < 0xc001a >, < 0xb001b >; + low-power-enable; + }; + }; + i2c1_default: i2c1_default { + phandle = < 0x8 >; + group1 { + psels = < 0xc001e >, < 0xb001f >; + }; + }; + i2c1_sleep: i2c1_sleep { + phandle = < 0x9 >; + group1 { + psels = < 0xc001e >, < 0xb001f >; + low-power-enable; + }; + }; + pwm0_default: pwm0_default { + phandle = < 0xc >; + group1 { + psels = < 0x16000d >; + nordic,invert; + }; + }; + pwm0_sleep: pwm0_sleep { + phandle = < 0xd >; + group1 { + psels = < 0x16000d >; + low-power-enable; + }; + }; + spi0_default: spi0_default { + phandle = < 0x6 >; + group1 { + psels = < 0x4001b >, < 0x5001a >, < 0x6001d >; + }; + }; + spi0_sleep: spi0_sleep { + phandle = < 0x7 >; + group1 { + psels = < 0x4001b >, < 0x5001a >, < 0x6001d >; + low-power-enable; + }; + }; + spi1_default: spi1_default { + phandle = < 0xa >; + group1 { + psels = < 0x4001f >, < 0x5001e >, < 0x60028 >; + }; + }; + spi1_sleep: spi1_sleep { + phandle = < 0xb >; + group1 { + psels = < 0x4001f >, < 0x5001e >, < 0x60028 >; + low-power-enable; + }; + }; + spi2_default: spi2_default { + phandle = < 0xe >; + group1 { + psels = < 0x40013 >, < 0x50014 >, < 0x60015 >; + }; + }; + spi2_sleep: spi2_sleep { + phandle = < 0xf >; + group1 { + psels = < 0x40013 >, < 0x50014 >, < 0x60015 >; + low-power-enable; + }; + }; + qspi_default: qspi_default { + phandle = < 0x12 >; + group1 { + psels = < 0x1d0013 >, < 0x1f0014 >, < 0x200015 >, < 0x210016 >, + < 0x220017 >, < 0x1e0011 >; + nordic,drive-mode = < 0x3 >; + }; + }; + qspi_sleep: qspi_sleep { + phandle = < 0x13 >; + group1 { + psels = < 0x1d0013 >, < 0x1f0014 >, < 0x200015 >, < 0x210016 >, + < 0x220017 >; + low-power-enable; + }; + group2 { + psels = < 0x1e0011 >; + low-power-enable; + bias-pull-up; + }; + }; + spi3_default: spi3_default { + phandle = < 0x15 >; + group1 { + psels = < 0x4002f >, < 0x6002e >, < 0x5002d >; + }; + }; + spi3_sleep: spi3_sleep { + phandle = < 0x16 >; + group1 { + psels = < 0x4002f >, < 0x6002e >, < 0x5002d >; + low-power-enable; + }; + }; + }; + rng_hci: entropy_bt_hci { + compatible = "zephyr,bt-hci-entropy"; + status = "okay"; + }; + sw_pwm: sw-pwm { + compatible = "nordic,nrf-sw-pwm"; + status = "disabled"; + generator = < &timer1 >; + clock-prescaler = < 0x0 >; + #pwm-cells = < 0x3 >; + }; + cpus { + #address-cells = < 0x1 >; + #size-cells = < 0x0 >; + cpu@0 { + device_type = "cpu"; + compatible = "arm,cortex-m4f"; + reg = < 0x0 >; + #address-cells = < 0x1 >; + #size-cells = < 0x1 >; + itm: itm@e0000000 { + compatible = "arm,armv7m-itm"; + reg = < 0xe0000000 0x1000 >; + swo-ref-frequency = < 0x1e84800 >; + }; + }; + }; + leds { + compatible = "gpio-leds"; + led0: led_0 { + gpios = < &gpio0 0xd 0x1 >; + label = "Green LED 0"; + }; + led1: led_1 { + gpios = < &gpio0 0xe 0x1 >; + label = "Green LED 1"; + }; + led2: led_2 { + gpios = < &gpio0 0xf 0x1 >; + label = "Green LED 2"; + }; + led3: led_3 { + gpios = < &gpio0 0x10 0x1 >; + label = "Green LED 3"; + }; + }; + pwmleds { + compatible = "pwm-leds"; + pwm_led0: pwm_led_0 { + pwms = < &pwm0 0x0 0x1312d00 0x1 >; + }; + }; + buttons { + compatible = "gpio-keys"; + button0: button_0 { + gpios = < &gpio0 0xb 0x11 >; + label = "Push button switch 0"; + zephyr,code = < 0xb >; + }; + button1: button_1 { + gpios = < &gpio0 0xc 0x11 >; + label = "Push button switch 1"; + zephyr,code = < 0x2 >; + }; + button2: button_2 { + gpios = < &gpio0 0x18 0x11 >; + label = "Push button switch 2"; + zephyr,code = < 0x3 >; + }; + button3: button_3 { + gpios = < &gpio0 0x19 0x11 >; + label = "Push button switch 3"; + zephyr,code = < 0x4 >; + }; + }; + arduino_header: connector { + compatible = "arduino-header-r3"; + #gpio-cells = < 0x2 >; + gpio-map-mask = < 0xffffffff 0xffffffc0 >; + gpio-map-pass-thru = < 0x0 0x3f >; + gpio-map = < 0x0 0x0 &gpio0 0x3 0x0 >, < 0x1 0x0 &gpio0 0x4 0x0 >, + < 0x2 0x0 &gpio0 0x1c 0x0 >, < 0x3 0x0 &gpio0 0x1d 0x0 >, + < 0x4 0x0 &gpio0 0x1e 0x0 >, < 0x5 0x0 &gpio0 0x1f 0x0 >, + < 0x6 0x0 &gpio1 0x1 0x0 >, < 0x7 0x0 &gpio1 0x2 0x0 >, + < 0x8 0x0 &gpio1 0x3 0x0 >, < 0x9 0x0 &gpio1 0x4 0x0 >, + < 0xa 0x0 &gpio1 0x5 0x0 >, < 0xb 0x0 &gpio1 0x6 0x0 >, + < 0xc 0x0 &gpio1 0x7 0x0 >, < 0xd 0x0 &gpio1 0x8 0x0 >, + < 0xe 0x0 &gpio1 0xa 0x0 >, < 0xf 0x0 &gpio1 0xb 0x0 >, + < 0x10 0x0 &gpio1 0xc 0x0 >, < 0x11 0x0 &gpio1 0xd 0x0 >, + < 0x12 0x0 &gpio1 0xe 0x0 >, < 0x13 0x0 &gpio1 0xf 0x0 >, + < 0x14 0x0 &gpio0 0x1a 0x0 >, < 0x15 0x0 &gpio0 0x1b 0x0 >; + phandle = < 0x14 >; + }; + arduino_adc: analog-connector { + compatible = "arduino,uno-adc"; + #io-channel-cells = < 0x1 >; + io-channel-map = < 0x0 &adc 0x1 >, < 0x1 &adc 0x2 >, < 0x2 &adc 0x4 >, + < 0x3 &adc 0x5 >, < 0x4 &adc 0x6 >, < 0x5 &adc 0x7 >; + }; +}; diff --git a/tests/test.dts b/tests/test.dts deleted file mode 100644 index 256b672..0000000 --- a/tests/test.dts +++ /dev/null @@ -1,534 +0,0 @@ -/* - * Copyright (c) 2019, Nordic Semiconductor - * - * SPDX-License-Identifier: BSD-3-Clause - */ - -// Used by testedtlib.py - -/dts-v1/; - -/ { - // - // Interrupts - // - - interrupt-parent-test { - controller { - compatible = "interrupt-three-cell"; - #interrupt-cells = <3>; - interrupt-controller; - }; - node { - interrupts = <1 2 3 4 5 6>; - interrupt-names = "foo", "bar"; - interrupt-parent = <&{/interrupt-parent-test/controller}>; - }; - }; - interrupts-extended-test { - controller-0 { - compatible = "interrupt-one-cell"; - #interrupt-cells = <1>; - interrupt-controller; - }; - controller-1 { - compatible = "interrupt-two-cell"; - #interrupt-cells = <2>; - interrupt-controller; - }; - controller-2 { - compatible = "interrupt-three-cell"; - #interrupt-cells = <3>; - interrupt-controller; - }; - node { - interrupts-extended = < - &{/interrupts-extended-test/controller-0} 1 - &{/interrupts-extended-test/controller-1} 2 3 - &{/interrupts-extended-test/controller-2} 4 5 6>; - }; - }; - interrupt-map-test { - #address-cells = <2>; - #size-cells = <0>; - - controller-0 { - compatible = "interrupt-one-cell"; - #address-cells = <1>; - #interrupt-cells = <1>; - interrupt-controller; - }; - controller-1 { - compatible = "interrupt-two-cell"; - #address-cells = <2>; - #interrupt-cells = <2>; - interrupt-controller; - }; - controller-2 { - compatible = "interrupt-three-cell"; - #address-cells = <3>; - #interrupt-cells = <3>; - interrupt-controller; - }; - nexus { - #interrupt-cells = <2>; - interrupt-map = < - 0 0 0 0 &{/interrupt-map-test/controller-0} 0 0 - 0 0 0 1 &{/interrupt-map-test/controller-1} 0 0 0 1 - 0 0 0 2 &{/interrupt-map-test/controller-2} 0 0 0 0 0 2 - 0 1 0 0 &{/interrupt-map-test/controller-0} 0 3 - 0 1 0 1 &{/interrupt-map-test/controller-1} 0 0 0 4 - 0 1 0 2 &{/interrupt-map-test/controller-2} 0 0 0 0 0 5>; - }; - node@0 { - reg = <0 0>; - interrupts = <0 0 0 1 0 2>; - interrupt-parent = <&{/interrupt-map-test/nexus}>; - }; - node@1 { - reg = <0 1>; - interrupts-extended = < - &{/interrupt-map-test/nexus} 0 0 - &{/interrupt-map-test/nexus} 0 1 - &{/interrupt-map-test/nexus} 0 2>; - }; - }; - interrupt-map-bitops-test { - #address-cells = <2>; - #size-cells = <0>; - - controller { - compatible = "interrupt-two-cell"; - #address-cells = <0>; - #interrupt-cells = <2>; - interrupt-controller; - }; - nexus { - #interrupt-cells = <2>; - interrupt-map = < - 6 6 6 6 &{/interrupt-map-bitops-test/controller} 2 1 - >; - interrupt-map-mask = <0xE 0x7 0xE 0x7>; - // Not specified in the DT spec., but shows up due to - // common code with GPIO. Might as well test it here. - interrupt-map-pass-thru = <1 2 3 3>; - }; - // Child unit specifier: 00000007 0000000E 00000007 0000000E - // Mask: 0000000E 00000007 0000000E 00000007 - // Pass-thru: 00000001 00000002 00000003 00000003 - node@70000000E { - reg = <0x7 0xE>; - interrupt-parent = <&{/interrupt-map-bitops-test/nexus}>; - interrupts = <0x7 0xE>; - }; - }; - - // - // 'ranges' - // - - ranges-zero-cells { - #address-cells = <0>; - - node { - #address-cells = <0>; - #size-cells = <0>; - - ranges; - }; - }; - - ranges-zero-parent-cells { - #address-cells = <0>; - - node { - #address-cells = <1>; - #size-cells = <0>; - - ranges = <0xA>, - <0x1A>, - <0x2A>; - }; - }; - - ranges-one-address-cells { - #address-cells = <0>; - - node { - reg = <1>; - #address-cells = <1>; - - ranges = <0xA 0xB>, - <0x1A 0x1B>, - <0x2A 0x2B>; - }; - }; - - ranges-one-address-two-size-cells { - #address-cells = <0>; - - node { - reg = <1>; - #address-cells = <1>; - #size-cells = <2>; - - ranges = <0xA 0xB 0xC>, - <0x1A 0x1B 0x1C>, - <0x2A 0x2B 0x2C>; - }; - }; - - ranges-two-address-cells { - #address-cells = <1>; - - node@1 { - reg = <1 2>; - - ranges = <0xA 0xB 0xC 0xD>, - <0x1A 0x1B 0x1C 0x1D>, - <0x2A 0x2B 0x2C 0x2D>; - }; - }; - - ranges-two-address-two-size-cells { - #address-cells = <1>; - - node@1 { - reg = <1 2>; - #size-cells = <2>; - - ranges = <0xA 0xB 0xC 0xD 0xE>, - <0x1A 0x1B 0x1C 0x1D 0x1E>, - <0x2A 0x2B 0x2C 0x2D 0x1D>; - }; - }; - - ranges-three-address-cells { - node@1 { - reg = <0 1 2>; - #address-cells = <3>; - - ranges = <0xA 0xB 0xC 0xD 0xE 0xF>, - <0x1A 0x1B 0x1C 0x1D 0x1E 0x1F>, - <0x2A 0x2B 0x2C 0x2D 0x2E 0x2F>; - }; - }; - - ranges-three-address-two-size-cells { - node@1 { - reg = <0 1 2>; - #address-cells = <3>; - #size-cells = <2>; - - ranges = <0xA 0xB 0xC 0xD 0xE 0xF 0x10>, - <0x1A 0x1B 0x1C 0x1D 0x1E 0x1F 0x110>, - <0x2A 0x2B 0x2C 0x2D 0x2E 0x2F 0x210>; - }; - }; - - - // - // 'reg' - // - - reg-zero-address-cells { - #address-cells = <0>; - #size-cells = <1>; - - node { - reg = <1 2>; - }; - }; - reg-zero-size-cells { - #address-cells = <1>; - #size-cells = <0>; - - node { - reg = <1 2>; - }; - }; - // Use implied #size-cells = <1> - reg-ranges { - #address-cells = <2>; - - parent { - #address-cells = <1>; - ranges = <1 0xA 0xB 1 /* 1 -> 0xA 0xB */ - 2 0xC 0xD 2 /* 2..3 -> 0xC 0xD */ - 4 0xE 0xF 1 /* 4 -> 0xE 0xF */ - >; - - node { - reg = <5 1 /* Matches no range */ - 4 1 /* Matches third range */ - 3 1 /* Matches second range */ - 2 1 /* Matches second range */ - 1 1 /* Matches first range */ - 0 1 /* Matches no range */ - >; - }; - }; - }; - // Build up <3 2 1> address with nested 'ranges' - reg-nested-ranges { - #address-cells = <3>; - - grandparent { - #address-cells = <2>; - #size-cells = <2>; - ranges = <0 0 3 0 0 2 2>; - - parent { - #address-cells = <1>; - ranges = <0 2 0 2>; - - node { - reg = <1 1>; - }; - }; - }; - }; - - // - // 'pinctrl-' - // - - pinctrl { - dev { - pinctrl-0 = <>; - pinctrl-1 = <&{/pinctrl/pincontroller/state-1}>; - pinctrl-2 = <&{/pinctrl/pincontroller/state-1} - &{/pinctrl/pincontroller/state-2}>; - pinctrl-names = "zero", "one", "two"; - }; - pincontroller { - state-1 { - }; - state-2 { - }; - }; - }; - - // - // For testing hierarchy. - // - - parent { - child-1 { - }; - child-2 { - grandchild { - }; - }; - }; - - // - // For testing 'include:' - // - - binding-include { - compatible = "binding-include-test"; - foo = <0>; - bar = <1>; - baz = <2>; - qaz = <3>; - }; - - // - // For testing Node.props (derived from 'properties:' in the binding) - // - - props { - compatible = "props"; - existent-boolean; - int = <1>; - array = <1 2 3>; - uint8-array = [ 12 34 ]; - string = "foo"; - string-array = "foo", "bar", "baz"; - phandle-ref = < &{/ctrl-1} >; - phandle-refs = < &{/ctrl-1} &{/ctrl-2} >; - phandle-array-foos = < &{/ctrl-1} 1 &{/ctrl-2} 2 3 >; - foo-gpios = < &{/ctrl-1} 1 >; - path = &{/ctrl-1}; - }; - - ctrl-1 { - compatible = "phandle-array-controller-1"; - #phandle-array-foo-cells = <1>; - #gpio-cells = <1>; - }; - - ctrl-2 { - compatible = "phandle-array-controller-2"; - #phandle-array-foo-cells = <2>; - }; - - props-2 { - compatible = "props"; - phandle-array-foos = < &{/ctrl-0-1} 0 &{/ctrl-0-2} >; - phandle-array-foo-names = "a", "missing", "b"; - }; - - ctrl-0-1 { - compatible = "phandle-array-controller-0"; - #phandle-array-foo-cells = <0>; - }; - - ctrl-0-2 { - compatible = "phandle-array-controller-0"; - #phandle-array-foo-cells = <0>; - }; - - // - // Test -map, via gpio-map - // - - gpio-map { - source { - compatible = "gpio-src"; - foo-gpios = <&{/gpio-map/connector} 3 4 - &{/gpio-map/connector} 1 2>; - }; - connector { - #gpio-cells = <2>; - // Use different data lengths for source and - // destination to make it a bit trickier - gpio-map = <1 2 &{/gpio-map/destination} 5 - 3 4 &{/gpio-map/destination} 6>; - }; - destination { - compatible = "gpio-dst"; - gpio-controller; - #gpio-cells = <1>; - }; - }; - - // - // For testing Node.props with 'default:' values in binding - // - - defaults { - compatible = "defaults"; - // Should override the 'default:' in the binding - default-not-used = <234>; - }; - - // - // For testing 'enum:' - // - - enums { - compatible = "enums"; - int-enum = <1>; - string-enum = "foo_bar"; - tokenizable-enum = "123 is ok"; - tokenizable-lower-enum = "bar"; - no-enum = "baz"; - }; - - // - // For testing 'bus:' and 'on-bus:' - // - - buses { - // The 'node' nodes below will map to different bindings since - // they appear on different buses - foo-bus { - compatible = "foo-bus"; - node1 { - compatible = "on-bus", "on-any-bus"; - nested { - compatible = "on-bus"; - }; - }; - node2 { - compatible = "on-any-bus", "on-bus"; - }; - }; - bar-bus { - compatible = "bar-bus"; - node { - compatible = "on-bus"; - }; - }; - no-bus-node { - compatible = "on-any-bus"; - }; - }; - - // - // Node with 'child-binding:' in binding (along with a recursive - // 'child-binding:') - // - - child-binding { - compatible = "top-binding"; - child-1 { - child-prop = <1>; - grandchild { - grandchild-prop = <2>; - }; - }; - child-2 { - child-prop = <3>; - }; - }; - - // - // zephyr,user binding inference - // - - zephyr,user { - boolean; - bytes = [81 82 83]; - number = <23>; - numbers = <1>, <2>, <3>; - string = "text"; - strings = "a", "b", "c"; - handle = <&{/ctrl-1}>; - phandles = <&{/ctrl-1}>, <&{/ctrl-2}>; - phandle-array-foos = <&{/ctrl-2} 1 2>; - }; - - // - // For testing that neither 'include: [foo.yaml, bar.yaml]' nor - // 'include: [bar.yaml, foo.yaml]' causes errors when one of the files - // has 'required: true' and the other 'required: false' - // - - include-order { - node-1 { - compatible = "order-1"; - foo = <1>; - }; - node-2 { - compatible = "order-2"; - foo = <2>; - }; - }; - - // - // For testing deprecated property - // - test-deprecated { - compatible = "test-deprecated"; - oldprop = <4>; /* deprecated property */ - curprop = <5>; - }; - - - // - // For testing deprecated features - // - - deprecated { - compatible = "deprecated"; - required = <1>; - required-2 = <2>; - #foo-cells = <2>; - sub-node { - foos = <&{/deprecated} 1 2>; - }; - }; -}; diff --git a/tests/test_autocomp.py b/tests/test_autocomp.py deleted file mode 100644 index 318173a..0000000 --- a/tests/test_autocomp.py +++ /dev/null @@ -1,128 +0,0 @@ -# Copyright (c) 2022 Chris Duf -# -# SPDX-License-Identifier: Apache-2.0 - -"""Covers the dtsh.autocomp module.""" - -import contextlib -import os - -from devicetree.edtlib import EDT - -from dtsh.dtsh import DtshAutocomp -from dtsh.autocomp import DevicetreeAutocomp -from dtsh.shell import DevicetreeShell - - -# Context manager pattern borrowed from python-devicetree (test_edtlib.py). -# -HERE = os.path.dirname(__file__) - - -@contextlib.contextmanager -def from_here(): - cwd = os.getcwd() - try: - os.chdir(HERE) - yield - finally: - os.chdir(cwd) - - -def test_autocomp_empty_cmdline(): - """Covers completion for an empty command line. - """ - with from_here(): - shell = DevicetreeShell.create('test.dts', ['bindings']) - completer = DevicetreeAutocomp(shell) - - completions = completer.autocomplete('', '') - assert len(completions) == len(shell.builtins) - - -def test_autocomp_command_name(): - """Covers command name completion. - """ - with from_here(): - shell = DevicetreeShell.create('test.dts', ['bindings']) - - sz_model = len(shell.cwd.children) - completer = DevicetreeAutocomp(shell) - - # 'ls' - completions = completer.autocomplete('l', 'l') - assert len(completions) == 1 - assert completions[0] == 'ls' - completions = completer.autocomplete('ls', 'ls') - assert len(completions) == 0 - completions = completer.autocomplete('ls ', '') - assert len(completions) == sz_model - - -def test_autocomp_option_name(): - """Covers option name completion. - """ - with from_here(): - shell = DevicetreeShell.create('test.dts', ['bindings']) - completer = DevicetreeAutocomp(shell) - - # 'cd [-h --help]' - completions = completer.autocomplete('cd -', '-') - assert len(completions) == 1 - completions = completer.autocomplete('cd --', '--') - assert len(completions) == 1 - - # 'ls [-d] [-l] [-r] [-R] [--pager] [-h --help]' - completions = completer.autocomplete('ls -', '-') - assert len(completions) == 7 - assert completions[4] == '-f' - assert completions[5] == '-h' - assert completions[6] == '--pager' - completions = completer.autocomplete('ls --', '--') - assert len(completions) == 2 - assert completions[0] == '--pager' - assert completions[1] == '--help' - - -def test_autocomp_nodes(): - """Covers DtshAutocomp.autocomplete_with_nodes() - """ - with from_here(): - edt = EDT('test.dts', ['bindings']) - shell = DevicetreeShell.create('test.dts', ['bindings']) - - sz_model = len(edt.get_node('/').children) - - nodes = DtshAutocomp.autocomplete_with_nodes('', shell) - assert len(nodes) == sz_model - - nodes = DtshAutocomp.autocomplete_with_nodes('/', shell) - assert len(nodes) == sz_model - - nodes = DtshAutocomp.autocomplete_with_nodes('/*', shell) - assert len(nodes) == 0 - - nodes = DtshAutocomp.autocomplete_with_nodes('paren', shell) - assert len(nodes) == 1 - - nodes = DtshAutocomp.autocomplete_with_nodes('/paren', shell) - assert len(nodes) == 1 - - nodes = DtshAutocomp.autocomplete_with_nodes('parent', shell) - assert len(nodes) == 0 - - nodes = DtshAutocomp.autocomplete_with_nodes('/parent', shell) - assert len(nodes) == 0 - - nodes = DtshAutocomp.autocomplete_with_nodes('/parent/', shell) - assert len(nodes) == 2 - - shell.cd('parent') - nodes = DtshAutocomp.autocomplete_with_nodes('', shell) - assert len(nodes) == 2 - nodes = DtshAutocomp.autocomplete_with_nodes('child-', shell) - assert len(nodes) == 2 - nodes = DtshAutocomp.autocomplete_with_nodes('child_', shell) - assert len(nodes) == 0 - nodes = DtshAutocomp.autocomplete_with_nodes('child-*', shell) - assert len(nodes) == 0 diff --git a/tests/test_dtsh.py b/tests/test_dtsh.py deleted file mode 100644 index ce7ffbc..0000000 --- a/tests/test_dtsh.py +++ /dev/null @@ -1,422 +0,0 @@ -# Copyright (c) 2022 Chris Duf -# -# SPDX-License-Identifier: Apache-2.0 - -"""Covers the dtsh.dtsh module.""" - -import contextlib -import os -import pytest - -from devicetree.edtlib import EDT - -from dtsh.dtsh import Dtsh, DtshUname, DtshCommandOption, DtshCommand, DtshVt -from dtsh.dtsh import DtshError, DtshCommandUsageError, DtshCommandNotFoundError - - -OPT_LONGFMT = DtshCommandOption('long output format', 'l', None, None) -OPT_VERSION = DtshCommandOption('version', None, 'version', None) -OPT_RECURSE = DtshCommandOption('list recursively', 'R', 'recursive', None) -OPT_REVERSE = DtshCommandOption('reverse order', 'r', 'reverse', None) -OPT_ALIAS = DtshCommandOption('filter by alias', 'a', None, 'alias') - -CMD_LS_OPTIONS = [ - OPT_LONGFMT, OPT_VERSION, OPT_RECURSE, OPT_REVERSE, OPT_ALIAS -] - -CMD_LS = DtshCommand('ls', 'list node contents', True, CMD_LS_OPTIONS) - - -# Context manager pattern borrowed from python-devicetree (test_edtlib.py). -# -HERE = os.path.dirname(__file__) - - -@contextlib.contextmanager -def from_here(): - cwd = os.getcwd() - try: - os.chdir(HERE) - yield - finally: - os.chdir(cwd) - - -def test_dtsh_nodename(): - """Covers Dtsh.nodename(). - """ - with pytest.raises(ValueError): - Dtsh.nodename('') - - assert Dtsh.nodename('/') == '/' - assert Dtsh.nodename('/usr/bin/sort') == 'sort' - assert Dtsh.nodename('/usr/bin/sort/') == 'sort' - assert Dtsh.nodename('aliases') == 'aliases' - assert Dtsh.nodename('/aliases') == 'aliases' - assert Dtsh.nodename('/dir/glob*') == 'glob*' - - -def test_dtsh_dirname(): - """Covers Dtsh.dirname(). - """ - with pytest.raises(ValueError): - Dtsh.dirname('') - - assert Dtsh.dirname('/') == '/' - assert Dtsh.dirname('/usr/bin') == '/usr' - assert Dtsh.dirname('/usr/bin/') == '/usr' - assert Dtsh.dirname('dir1/str') == 'dir1' - assert Dtsh.dirname('dir1/str/') == 'dir1' - assert Dtsh.dirname('foobar') == '.' - - # Allows globing. - assert Dtsh.dirname('dir1/str*') == 'dir1' - assert Dtsh.dirname('/*') == '/' - - -def test_dtsh_path_concat(): - """Covers dtsh.Dtsh.path_concat(). - """ - with pytest.raises(ValueError): - Dtsh.path_concat('', 'dir1') - - assert Dtsh.path_concat('/', 'dir1') == '/dir1' - assert Dtsh.path_concat('/', 'dir1/') == '/dir1' - assert Dtsh.path_concat('dir1', 'dir2') == 'dir1/dir2' - assert Dtsh.path_concat('dir1/', 'dir2') == 'dir1/dir2' - assert Dtsh.path_concat('/dir1', 'dir2') == '/dir1/dir2' - assert Dtsh.path_concat('dir1/', 'dir2/') == 'dir1/dir2' - - path = '/dirname/basename' - assert Dtsh.path_concat(Dtsh.dirname(path), Dtsh.nodename(path)) == path - - -def test_dtsh_init(): - """Covers Dtsh ctor and properties. - """ - with from_here(): - sysinfo = DtshUname("test.dts", ["bindings"]) - edt = EDT(sysinfo.dts_path, sysinfo.dt_binding_dirs) - shell = Dtsh(edt, sysinfo) - assert shell.cwd == edt.get_node('/') - assert shell.pwd == edt.get_node('/').path - assert len(shell.builtins) == 0 - assert shell.builtin('not-supported') is None - - -def test_dtsh_realpath(): - """Covers Dtsh.realpath(). - """ - with from_here(): - sysinfo = DtshUname("test.dts", ["bindings"]) - edt = EDT(sysinfo.dts_path, sysinfo.dt_binding_dirs) - shell = Dtsh(edt, sysinfo) - - with pytest.raises(ValueError): - shell.realpath('') - - # The devicetree's root path resolves to '/'. - assert shell.realpath('/') == '/' - - # A path which starts with '/' resolves to itself but any trailing '/. - assert shell.realpath('/dir') == '/dir' - assert shell.realpath('/dir/') == '/dir' - - # Wildcard substitution. - assert shell.realpath('.') == '/' - assert shell.realpath('./') == '/' - assert shell.realpath('./parent') == '/parent' - # Devicetree's root is its own parent. - assert shell.realpath('..') == '/' - assert shell.realpath('../') == '/' - assert shell.realpath('../parent') == '/parent' - - # Convert to absolute path. - assert shell.realpath('parent/child-1') == '/parent/child-1' - shell.cd('/parent/child-2') - assert shell.realpath('grandchild') == '/parent/child-2/grandchild' - - # Preserve trailing wildcards. - shell.cd('/') - assert shell.realpath('*') == '/*' - assert shell.realpath('/*') == '/*' - assert shell.realpath('/dir/prefix*') == '/dir/prefix*' - - # Trim trailing slash. - assert shell.realpath('parent/') == '/parent' - - -def test_dtsh_path2node(): - """Covers Dtsh.path2node(). - """ - with from_here(): - sysinfo = DtshUname("test.dts", ["bindings"]) - edt = EDT(sysinfo.dts_path, sysinfo.dt_binding_dirs) - shell = Dtsh(edt, sysinfo) - - with pytest.raises(ValueError): - shell.path2node('') - - assert shell.path2node('/') == edt.get_node('/') - assert shell.path2node('/parent') == edt.get_node('/parent') - assert shell.path2node('/parent/child-1') == edt.get_node('/parent/child-1') - - with pytest.raises(DtshError): - shell.path2node('/does-not-exist') - - # path2node() expects an absolute path - with pytest.raises(DtshError): - shell.path2node('.') - with pytest.raises(DtshError): - shell.path2node('..') - with pytest.raises(DtshError): - shell.path2node('parent') - - -def test_dtsh_cd(): - """Covers `dtsh.Dtsh.cd()`. - """ - with from_here(): - sysinfo = DtshUname("test.dts", ["bindings"]) - edt = EDT(sysinfo.dts_path, sysinfo.dt_binding_dirs) - shell = Dtsh(edt, sysinfo) - - with pytest.raises(ValueError): - shell.cd('') - - shell.cd('parent') - assert shell.cwd == edt.get_node('/parent') - - shell.cd('/parent/child-2/grandchild') - assert shell.cwd == edt.get_node('/parent/child-2/grandchild') - - shell.cd('..') - assert shell.cwd == edt.get_node('/parent/child-2') - shell.cd('../') - assert shell.cwd == edt.get_node('/parent') - shell.cd('.') - assert shell.cwd == edt.get_node('/parent') - - shell.cd('../parent/child-1') - assert shell.cwd == edt.get_node('/parent/child-1') - - shell.cd('/') - assert shell.cwd == edt.get_node('/') - - with pytest.raises(DtshError): - shell.cd('/does-not-exist') - - -def test_dtsh_ls(): - """Covers `dtsh.Dtsh.ls()`. - """ - with from_here(): - sysinfo = DtshUname("test.dts", ["bindings"]) - edt = EDT(sysinfo.dts_path, sysinfo.dt_binding_dirs) - shell = Dtsh(edt, sysinfo) - - with pytest.raises(ValueError): - shell.ls('') - - nodes = shell.ls('/') - assert len(nodes) == len(edt.get_node('/').children) - - nodes = shell.ls('parent') - assert len(nodes) == len(edt.get_node('/parent').children) - assert nodes[0] == edt.get_node('/parent/child-1') - assert nodes[1] == edt.get_node('/parent/child-2') - - # Wildcard substitution. - shell.cd('/parent/child-2') - nodes = shell.ls('.') - assert len(nodes) == len(edt.get_node('/parent/child-2').children) - assert nodes[0] == edt.get_node('/parent/child-2/grandchild') - nodes = shell.ls('..') - assert len(nodes) == len(edt.get_node('/parent').children) - nodes = shell.ls('../child-1') - assert len(nodes) == len(edt.get_node('/parent/child-1').children) - - # 'ls *' is equivalent to 'ls $pwd'. - nodes = shell.ls('*') - assert nodes == shell.ls(shell.pwd) - assert nodes == [ - edt.get_node('/parent/child-2/grandchild'), - ] - - # 'ls dirname/*' is equivalent to 'ls dirname'. - assert shell.ls('/parent/*') == [ - edt.get_node('/parent/child-1'), - edt.get_node('/parent/child-2'), - ] - - # 'ls dirname/prefix*' will list the children of the node with path 'dirname' - # whose name starts with 'prefix'. - assert shell.ls('/parent/child*') == [ - edt.get_node('/parent/child-1'), - edt.get_node('/parent/child-2'), - ] - - # ls('/parent*') is interpreted as ls('/*') - assert shell.ls('/parent*') == [ - edt.get_node('/parent'), - ] - - # Filtering. - shell.cd('/parent') - assert shell.ls('child-*') == [ - edt.get_node('/parent/child-1'), - edt.get_node('/parent/child-2'), - ] - nodes = shell.ls('child_*') - assert len(nodes) == 0 - - with pytest.raises(DtshError): - shell.ls('/does-not-exist') - - -def test_dtsh_base_exec(): - with from_here(): - sysinfo = DtshUname("test.dts", ["bindings"]) - edt = EDT(sysinfo.dts_path, sysinfo.dt_binding_dirs) - shell = Dtsh(edt, sysinfo) - - # Ignore empty command strings. - shell.exec_command_string('', DtshVt()) - - with pytest.raises(DtshCommandNotFoundError): - shell.exec_command_string('ls', DtshVt()) - - -def test_dtsh_command_option(): - """Covers DtshCommandOption. - """ - assert OPT_REVERSE.shortname == 'r' - assert OPT_REVERSE.longname == 'reverse' - assert OPT_REVERSE.argname is None - assert OPT_REVERSE.usage == '-r --reverse' - assert OPT_REVERSE.value is None - assert OPT_REVERSE.is_flag() - - assert OPT_ALIAS.shortname == 'a' - assert OPT_ALIAS.longname is None - assert OPT_ALIAS.argname == 'alias' - assert OPT_ALIAS.usage == '-a ' - assert OPT_ALIAS.value is None - assert not OPT_ALIAS.is_flag() - - assert OPT_LONGFMT.shortname == 'l' - assert OPT_LONGFMT.longname is None - assert OPT_LONGFMT.argname is None - assert OPT_LONGFMT.usage == '-l' - assert OPT_LONGFMT.value is None - assert OPT_LONGFMT.is_flag() - - OPT_LONGFMT.value = True - assert OPT_LONGFMT.value == True - OPT_LONGFMT.reset() - assert OPT_LONGFMT.value is None - - -def test_dtsh_command_options(): - """Covers DtshCommand options API. - """ - # Test pager and help auto-support. - assert len(CMD_LS.options) == len(CMD_LS_OPTIONS) + 2 - assert CMD_LS.option('--pager') is not None - assert not CMD_LS.with_flag('--pager') - assert CMD_LS.option('-h') is not None - assert not CMD_LS.with_flag('-h') - # Test custom options - assert CMD_LS.option('-h') is CMD_LS.option('--help') - assert CMD_LS.option('-R') is not None - assert not CMD_LS.with_flag('-R') - assert CMD_LS.option('--recursive') is not None - assert not CMD_LS.with_flag('--recursive') - assert CMD_LS.option('-R') is CMD_LS.option('--recursive') - assert CMD_LS.option('-r') is not None - assert not CMD_LS.with_flag('-r') - assert CMD_LS.option('--reverse') is not None - assert not CMD_LS.with_flag('--reverse') - assert CMD_LS.option('-r') is CMD_LS.option('--reverse') - - assert CMD_LS.getopt_short == 'lRra:h' - assert CMD_LS.getopt_long == ['version', 'recursive', 'reverse', 'pager', 'help'] - - argv= [ - '--help', - '-a', - 'i2c0', - '-r', - '--recursive', - '--version', - '--pager' - ] - - with pytest.raises(DtshCommandUsageError): - # Triggered by the '--help' flag. - CMD_LS.parse_argv(argv) - assert CMD_LS.with_flag('-h') - assert CMD_LS.with_flag('--help') - assert CMD_LS.with_flag('-r') - assert CMD_LS.with_flag('--reverse') - assert CMD_LS.with_flag('-R') - assert CMD_LS.with_flag('--recursive') - assert CMD_LS.with_flag('--version') - assert not CMD_LS.with_flag('-l') - assert CMD_LS.with_help - assert CMD_LS.with_pager - opt_alias = CMD_LS.option('-a') - assert opt_alias is not None - assert opt_alias.value is not None - - CMD_LS.reset() - assert not CMD_LS.with_flag('-h') - assert not CMD_LS.with_flag('--help') - assert not CMD_LS.with_flag('-r') - assert not CMD_LS.with_flag('--reverse') - assert not CMD_LS.with_flag('-R') - assert not CMD_LS.with_flag('--recursive') - assert not CMD_LS.with_flag('--version') - assert not CMD_LS.with_flag('-l') - assert not CMD_LS.with_help - assert not CMD_LS.with_pager - opt_alias = CMD_LS.option('-a') - assert opt_alias is not None - assert opt_alias.value is None - - with pytest.raises(DtshCommandUsageError): - CMD_LS.parse_argv(['-x']) - - -def test_dtsh_command_autocomp_option(): - """Covers DtshCommand.autocomplete_option(). - """ - # Prefix does not start with '-', won't match any option. - completions = CMD_LS.autocomplete_option('') - assert len(completions) == 0 - completions = CMD_LS.autocomplete_option('x') - assert len(completions) == 0 - - # Prefix '-' should match all options. - completions = CMD_LS.autocomplete_option('-') - assert len(completions) == len(CMD_LS_OPTIONS) + 2 - # 1st, all options that have a short name. - assert completions[0] is OPT_LONGFMT - assert completions[1] is OPT_RECURSE - assert completions[2] is OPT_REVERSE - assert completions[3] is OPT_ALIAS - assert completions[4].shortname == 'h' - # Then options that have only a long name. - assert completions[-2].longname == 'version' - assert completions[-1].longname == 'pager' - - # Nothing to auto-complete. - assert len(CMD_LS.autocomplete_option('-h')) == 0 - - # Won't complete multiple short options. - assert len(CMD_LS.autocomplete_option('-ar')) == 0 - - assert len(CMD_LS.autocomplete_option('--')) == 5 - assert len(CMD_LS.autocomplete_option('--re')) == 2 - assert len(CMD_LS.autocomplete_option('--rev')) == 1 - assert len(CMD_LS.autocomplete_option('--recursive')) == 0 diff --git a/tests/test_dtsh_autocomp.py b/tests/test_dtsh_autocomp.py new file mode 100644 index 0000000..8a84784 --- /dev/null +++ b/tests/test_dtsh_autocomp.py @@ -0,0 +1,444 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Unit tests for the dtsh.autocomp module.""" + +# Relax pylint a bit for unit tests. +# pylint: disable=missing-class-docstring +# pylint: disable=missing-function-docstring + + +from typing import List + +import os + +from dtsh.model import DTPath +from dtsh.shell import DTSh, DTShCommand, DTShArg, DTShFlagHelp +from dtsh.shellutils import ( + DTShFlagPager, + DTShFlagReverse, + DTShArgFixedDepth, + DTShParamDTPath, +) +from dtsh.rl import DTShReadline +from dtsh.autocomp import ( + DTShAutocomp, + RlStateDTShCommand, + RlStateDTShOption, + RlStateDTPath, + RlStateDTVendor, + RlStateDTBus, + RlStateDTAlias, + RlStateDTChosen, + RlStateDTLabel, +) + +from .dtsh_uthelpers import DTShTests + + +class MockDTShArg(DTShArg): + brief = "mock arg" + longname = "mockarg" + + def __init__(self) -> None: + super().__init__(argname="arg") + + RL_STATES: List[DTShReadline.CompleterState] = [ + DTShReadline.CompleterState("a", 0), + DTShReadline.CompleterState("ab", 1), + DTShReadline.CompleterState("b", 2), + ] + + def autocomp( + self, txt: str, sh: "DTSh" + ) -> List[DTShReadline.CompleterState]: + return [ + state + for state in MockDTShArg.RL_STATES + if state.rlstr.startswith(txt) + ] + + +class MockDTShCmd(DTShCommand): + def __init__(self) -> None: + super().__init__( + "mockcmd", + "mock command", + [ + DTShFlagReverse(), + DTShFlagPager(), + DTShArgFixedDepth(), + MockDTShArg(), + ], + DTShParamDTPath(), + ) + + +def test_dtshautocomp_complete_dtshcmd() -> None: + cmd_mock = MockDTShCmd() + cmd_ls = DTShCommand("ls", "", None, None) + cmd_tree = DTShCommand("tree", "", None, None) + cmd_lstree = DTShCommand("lstree", "", None, None) + sh = DTSh( + DTShTests.get_sample_dtmodel(), [cmd_mock, cmd_ls, cmd_tree, cmd_lstree] + ) + + assert sorted( + [RlStateDTShCommand(cmd.name, cmd) for cmd in sh.commands] + ) == DTShAutocomp.complete_dtshcmd("", sh) + + assert sorted( + [ + RlStateDTShCommand(cmd_ls.name, cmd_ls), + RlStateDTShCommand(cmd_lstree.name, cmd_lstree), + ] + ) == DTShAutocomp.complete_dtshcmd("l", sh) + + # Unique matches are not hidden. + assert [ + RlStateDTShCommand(cmd_tree.name, cmd_tree) + ] == DTShAutocomp.complete_dtshcmd("t", sh) + + +def test_dtshautocomp_complete_dtshopt() -> None: + cmd = MockDTShCmd() + state_h = RlStateDTShOption("-h", DTShFlagHelp()) + state_help = RlStateDTShOption("--help", DTShFlagHelp()) + state_r = RlStateDTShOption("-r", DTShFlagReverse()) + state_pager = RlStateDTShOption("--pager", DTShFlagPager()) + state_depth = RlStateDTShOption("--fixed-depth", DTShArgFixedDepth()) + state_mock = RlStateDTShOption("--mockarg", MockDTShArg()) + + # All options. + assert [ + state_h, + state_r, + state_depth, + state_mock, + state_pager, + ] == DTShAutocomp.complete_dtshopt("-", cmd) + + # Only options with a long name. + assert [ + state_help, + state_depth, + state_mock, + state_pager, + ] == DTShAutocomp.complete_dtshopt("--", cmd) + + # Should start with "-". + assert not DTShAutocomp.complete_dtshopt("", cmd) + + # Unique matches are not hidden. + assert [state_r] == DTShAutocomp.complete_dtshopt("-r", cmd) + assert [state_pager] == DTShAutocomp.complete_dtshopt("--pager", cmd) + + +def test_dtshautocomp_complete_dtpath() -> None: + sh = DTSh(DTShTests.get_sample_dtmodel(), []) + + # All children of the current working branch. + assert sorted( + [RlStateDTPath(node.name, node) for node in sh.dt.root.children] + ) == DTShAutocomp.complete_dtpath("", sh) + + # Complete absolute path. + assert sorted( + [ + RlStateDTPath(DTPath.join("/soc", node.name), node) + for node in sh.dt["/soc"].children + if node.name.startswith("egu") + ] + ) == DTShAutocomp.complete_dtpath("/soc/egu", sh) + + # Complete relative path. + sh.cd("/soc") + assert sorted( + [ + RlStateDTPath(node.name, node) + for node in sh.dt["/soc"].children + if node.name.startswith("egu") + ] + ) == DTShAutocomp.complete_dtpath("egu", sh) + + +def test_dtshautocomp_complete_dtcompat() -> None: + sh = DTSh(DTShTests.get_sample_dtmodel(), []) + + # All compatible strings. + assert sorted(sh.dt.compatible_strings) == [ + state.rlstr for state in DTShAutocomp.complete_dtcompat("", sh) + ] + + # Complete compatible strings: + # - arduino,uno-adc, arduino-header-r3 + # - arm,armv7m-itm, arm,armv7m-systick, arm,cortex-m4f, arm,cryptocell-310, + # arm,v7m-nvic + assert sorted( + [ + compat + for compat in sh.dt.compatible_strings + if compat.startswith("arm") or compat.startswith("arduino") + ] + ) == [state.rlstr for state in DTShAutocomp.complete_dtcompat("a", sh)] + + # Unique matches are not hidden: arm,cortex-m4f + assert ["arm,cortex-m4f"] == [ + state.rlstr + for state in DTShAutocomp.complete_dtcompat("arm,cortex", sh) + ] + + +def test_dtshautocomp_complete_dtvendor() -> None: + sh = DTSh(DTShTests.get_sample_dtmodel(), []) + + # All vendors. + assert sorted( + [ + RlStateDTVendor(vendor.prefix, vendor.name) + for vendor in sh.dt.vendors + ] + ) == DTShAutocomp.complete_dtvendor("", sh) + + # Complete vendor prefix. + assert sorted( + [ + RlStateDTVendor(vendor.prefix, vendor.name) + for vendor in sh.dt.vendors + if vendor.prefix.startswith("a") + ] + ) == DTShAutocomp.complete_dtvendor("a", sh) + + # Unique matches are not hidden. + vendor_arduino = sh.dt.get_vendor("arduino,") + assert vendor_arduino + assert [ + RlStateDTVendor(vendor_arduino.prefix, vendor_arduino.name) + ] == DTShAutocomp.complete_dtvendor("ard", sh) + + +def test_dtshautocomp_complete_dtbus() -> None: + sh = DTSh(DTShTests.get_sample_dtmodel(), []) + + # All bus protocols. + assert sorted( + [RlStateDTBus(bus) for bus in sh.dt.bus_protocols] + ) == DTShAutocomp.complete_dtbus("", sh) + + # Complete bus protocol: i2c, i2s. + assert sorted( + [ + RlStateDTBus(bus) + for bus in sh.dt.bus_protocols + if bus.startswith("i2") + ] + ) == DTShAutocomp.complete_dtbus("i2", sh) + + # Unique matches are not hidden: spi. + assert [ + RlStateDTBus(bus) for bus in sh.dt.bus_protocols if bus == "spi" + ] == DTShAutocomp.complete_dtbus("s", sh) + + +def test_dtshautocomp_complete_dtalias() -> None: + sh = DTSh(DTShTests.get_sample_dtmodel(), []) + + # All aliased nodes. + assert sorted( + [ + RlStateDTAlias(alias, node) + for alias, node in sh.dt.aliased_nodes.items() + ] + ) == DTShAutocomp.complete_dtalias("", sh) + + # Complete alias name: led0, led1, led2, led3. + assert sorted( + [ + RlStateDTAlias(alias, node) + for alias, node in sh.dt.aliased_nodes.items() + if alias.startswith("led") + ] + ) == DTShAutocomp.complete_dtalias("l", sh) + + # Unique matches are not hidden: watchdog0 + assert [ + RlStateDTAlias(alias, node) + for alias, node in sh.dt.aliased_nodes.items() + if alias == "watchdog0" + ] == DTShAutocomp.complete_dtalias("w", sh) + + +def test_dtshautocomp_complete_dtchosen() -> None: + sh = DTSh(DTShTests.get_sample_dtmodel(), []) + + # All chosen nodes. + assert sorted( + [ + RlStateDTChosen(chosen, node) + for chosen, node in sh.dt.chosen_nodes.items() + ] + ) == DTShAutocomp.complete_dtchosen("", sh) + + # Complete chosen parameter name: zephyr,flash, zephyr,flash-controller. + assert sorted( + [ + RlStateDTChosen(chosen, node) + for chosen, node in sh.dt.chosen_nodes.items() + if chosen.startswith("zephyr,flash") + ] + ) == DTShAutocomp.complete_dtchosen("zephyr,f", sh) + + # Unique matches are not hidden: zephyr,uart-mcumgr + assert [ + RlStateDTChosen(chosen, node) + for chosen, node in sh.dt.chosen_nodes.items() + if chosen == "zephyr,uart-mcumgr" + ] == DTShAutocomp.complete_dtchosen("zephyr,u", sh) + + +def test_dtshautocomp_complete_dtlabel() -> None: + sh = DTSh(DTShTests.get_sample_dtmodel(), []) + + # The leading "&" is preserved when present in the completion scope + # (auto-complete for devicetree paths). + assert [ + RlStateDTLabel(label, node) + for label, node in [ + ("&i2c0", DTShTests.get_sample_dtmodel()["/soc/i2c@40003000"]), + ( + "&i2c0_default", + DTShTests.get_sample_dtmodel()["/pin-controller/i2c0_default"], + ), + ( + "&i2c0_sleep", + DTShTests.get_sample_dtmodel()["/pin-controller/i2c0_sleep"], + ), + ("&i2c1", DTShTests.get_sample_dtmodel()["/soc/i2c@40004000"]), + ( + "&i2c1_default", + DTShTests.get_sample_dtmodel()["/pin-controller/i2c1_default"], + ), + ( + "&i2c1_sleep", + DTShTests.get_sample_dtmodel()["/pin-controller/i2c1_sleep"], + ), + ] + ] == DTShAutocomp.complete_dtlabel("&i2c", sh) + + # A leading "&" is not enforced when not present in the completion scope + # (auto-complete for labels). + assert [ + RlStateDTLabel(label, node) + for label, node in [ + ("i2c0", DTShTests.get_sample_dtmodel()["/soc/i2c@40003000"]), + ( + "i2c0_default", + DTShTests.get_sample_dtmodel()["/pin-controller/i2c0_default"], + ), + ( + "i2c0_sleep", + DTShTests.get_sample_dtmodel()["/pin-controller/i2c0_sleep"], + ), + ] + ] == DTShAutocomp.complete_dtlabel("i2c0", sh) + + +def test_dtshautocomp_complete_fspath() -> None: + with DTShTests.from_there("fs"): + # All file and directories in $PWD. + assert [ + f"A{os.sep}", + f"a{os.sep}", + f"foo{os.sep}", + "A.txt", + "a.txt", + "ab.txt", + ] == [state.rlstr for state in DTShAutocomp.complete_fspath("")] + + # Complete path in current directory. + assert [f"a{os.sep}", "a.txt", "ab.txt"] == [ + state.rlstr for state in DTShAutocomp.complete_fspath("a") + ] + + # Complete path in sub-directory. + assert [f"a{os.sep}x{os.sep}"] == [ + state.rlstr for state in DTShAutocomp.complete_fspath(f"a{os.sep}") + ] + + # Unique matches are not hidden. + assert [f"foo{os.sep}"] == [ + state.rlstr for state in DTShAutocomp.complete_fspath("foo") + ] + + +def test_dtshautocomp_complete() -> None: + cmd_mock = MockDTShCmd() + cmd_ls = DTShCommand("ls", "", None, None) + sh = DTSh(DTShTests.get_sample_dtmodel(), [cmd_mock, cmd_ls]) + autocomp = DTShAutocomp(sh) + + # Command line: "|" + # Expects: all commands + assert sorted( + [RlStateDTShCommand(cmd.name, cmd) for cmd in sh.commands] + ) == autocomp.complete("", "", 0, 0) + + # Command line: " |" + # Expects: all commands + assert sorted( + [RlStateDTShCommand(cmd.name, cmd) for cmd in sh.commands] + ) == autocomp.complete("", "", 1, 1) + + # Command line: " | " + # Expects: all commands + assert sorted( + [RlStateDTShCommand(cmd.name, cmd) for cmd in sh.commands] + ) == autocomp.complete("", " ", 1, 1) + + # Command line: "l|" + # Expects: ls + assert [RlStateDTShCommand(cmd_ls.name, cmd_ls)] == autocomp.complete( + "l", "l", 0, 1 + ) + + # Command line: " l|" + # Expects: ls + assert [RlStateDTShCommand(cmd_ls.name, cmd_ls)] == autocomp.complete( + "l", " l", 1, 2 + ) + + # Command line: "mockcmd --mockarg |" + # Expects: possible argument values + assert sorted(MockDTShArg.RL_STATES) == autocomp.complete( + "", "mockcmd --mockarg ", 18, 18 + ) + + # Command line: "mockcmd --mockarg|" + # Expects: the option name as unique match. + assert [RlStateDTShOption("--mockarg", MockDTShArg())] == autocomp.complete( + "--mockarg", "mockcmd --mockarg", 8, 17 + ) + + # Command line: "mockcmd --mockarg > |" + # Expects: won't auto-complete redirection, ">" the argument value, + # should auto-compelete with nodes. + assert sorted( + [RlStateDTPath(node.name, node) for node in sh.dt.root.children] + ) == autocomp.complete("", "mockcmd --mockarg > ", 20, 20) + + # Command line: " ls > |" + # Expect: test file-system entries. + with DTShTests.from_there("fs"): + assert [ + f"A{os.sep}", + f"a{os.sep}", + f"foo{os.sep}", + "A.txt", + "a.txt", + "ab.txt", + ] == [state.rlstr for state in autocomp.complete("", "ls > ", 5, 5)] + + # Command line: " ls >|" + # Expects: missing space after ">". + assert [] == autocomp.complete("", "ls >", 3, 3) diff --git a/tests/test_dtsh_builtin_alias.py b/tests/test_dtsh_builtin_alias.py new file mode 100644 index 0000000..ea1c990 --- /dev/null +++ b/tests/test_dtsh_builtin_alias.py @@ -0,0 +1,48 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Unit tests for the dtsh.builtins.alias module.""" + +# Relax pylint a bit for unit tests. +# pylint: disable=missing-function-docstring + + +from dtsh.io import DTShOutput +from dtsh.shell import DTSh +from dtsh.builtins.alias import DTShBuiltinAlias + +from .dtsh_uthelpers import DTShTests + +_stdout = DTShOutput() + + +def test_dtsh_builtin_alias() -> None: + cmd = DTShBuiltinAlias() + DTShTests.check_cmd_meta(cmd, "alias") + + +def test_dtsh_builtin_alias_flags() -> None: + cmd = DTShBuiltinAlias() + sh = DTSh(DTShTests.get_sample_dtmodel(), [cmd]) + DTShTests.check_cmd_flags(cmd, sh, _stdout) + + +def test_dtsh_builtin_alias_arg_longfmt() -> None: + cmd = DTShBuiltinAlias() + sh = DTSh(DTShTests.get_sample_dtmodel(), [cmd]) + DTShTests.check_cmd_arg_longfmt(cmd, sh, _stdout) + + +def test_dtsh_builtin_alias_param() -> None: + cmd = DTShBuiltinAlias() + sh = DTSh(DTShTests.get_sample_dtmodel(), [cmd]) + DTShTests.check_cmd_param_alias(cmd, sh, _stdout) + + +def test_dtsh_builtin_alias_execute() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + cmd = DTShBuiltinAlias() + sh = DTSh(dtmodel, [cmd]) + + DTShTests.check_cmd_execute(cmd, sh, _stdout) diff --git a/tests/test_dtsh_builtin_cd.py b/tests/test_dtsh_builtin_cd.py new file mode 100644 index 0000000..7adacc3 --- /dev/null +++ b/tests/test_dtsh_builtin_cd.py @@ -0,0 +1,35 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Unit tests for the dtsh.builtins.cd module.""" + +# Relax pylint a bit for unit tests. +# pylint: disable=missing-function-docstring + + +from dtsh.io import DTShOutput +from dtsh.shell import DTSh +from dtsh.builtins.cd import DTShBuiltinCd + +from .dtsh_uthelpers import DTShTests + +_stdout = DTShOutput() + + +def test_dtsh_builtin_cd() -> None: + cmd = DTShBuiltinCd() + DTShTests.check_cmd_meta(cmd, "cd") + + +def test_dtsh_builtin_cd_param() -> None: + cmd = DTShBuiltinCd() + sh = DTSh(DTShTests.get_sample_dtmodel(), [cmd]) + DTShTests.check_cmd_param_dtpath(cmd, sh, _stdout) + + +def test_dtsh_builtin_cd_execute() -> None: + cmd = DTShBuiltinCd() + sh = DTSh(DTShTests.get_sample_dtmodel(), [cmd]) + + DTShTests.check_cmd_execute(cmd, sh, _stdout) diff --git a/tests/test_dtsh_builtin_chosen.py b/tests/test_dtsh_builtin_chosen.py new file mode 100644 index 0000000..54268fc --- /dev/null +++ b/tests/test_dtsh_builtin_chosen.py @@ -0,0 +1,48 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Unit tests for the dtsh.builtins.chosen module.""" + +# Relax pylint a bit for unit tests. +# pylint: disable=missing-function-docstring + + +from dtsh.io import DTShOutput +from dtsh.shell import DTSh +from dtsh.builtins.chosen import DTShBuiltinChosen + +from .dtsh_uthelpers import DTShTests + +_stdout = DTShOutput() + + +def test_dtsh_builtin_chosen() -> None: + cmd = DTShBuiltinChosen() + DTShTests.check_cmd_meta(cmd, "chosen") + + +def test_dtsh_builtin_chosen_flags() -> None: + cmd = DTShBuiltinChosen() + sh = DTSh(DTShTests.get_sample_dtmodel(), [cmd]) + DTShTests.check_cmd_flags(cmd, sh, _stdout) + + +def test_dtsh_builtin_chosen_arg_longfmt() -> None: + cmd = DTShBuiltinChosen() + sh = DTSh(DTShTests.get_sample_dtmodel(), [cmd]) + DTShTests.check_cmd_arg_longfmt(cmd, sh, _stdout) + + +def test_dtsh_builtin_chosen_param() -> None: + cmd = DTShBuiltinChosen() + sh = DTSh(DTShTests.get_sample_dtmodel(), [cmd]) + DTShTests.check_cmd_param_chosen(cmd, sh, _stdout) + + +def test_dtsh_builtin_chosen_execute() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + cmd = DTShBuiltinChosen() + sh = DTSh(dtmodel, [cmd]) + + DTShTests.check_cmd_execute(cmd, sh, _stdout) diff --git a/tests/test_dtsh_builtin_find.py b/tests/test_dtsh_builtin_find.py new file mode 100644 index 0000000..7e10c3c --- /dev/null +++ b/tests/test_dtsh_builtin_find.py @@ -0,0 +1,86 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Unit tests for the dtsh.builtins.find module.""" + +# Relax pylint a bit for unit tests. +# pylint: disable=missing-function-docstring + + +from typing import List + +from dtsh.io import DTShOutput +from dtsh.shell import DTSh +from dtsh.shellutils import DTSH_ARG_NODE_CRITERIA, DTShArgCriterion +from dtsh.builtins.find import DTShBuiltinFind + +from .dtsh_uthelpers import DTShTests + +_stdout = DTShOutput() + + +def test_dtsh_builtin_find() -> None: + cmd = DTShBuiltinFind() + DTShTests.check_cmd_meta(cmd, "find") + + +def test_dtsh_builtin_find_flags() -> None: + cmd = DTShBuiltinFind() + sh = DTSh(DTShTests.get_sample_dtmodel(), [cmd]) + DTShTests.check_cmd_flags(cmd, sh, _stdout) + + +def test_dtsh_builtin_find_arg_orderby() -> None: + cmd = DTShBuiltinFind() + sh = DTSh(DTShTests.get_sample_dtmodel(), [cmd]) + DTShTests.check_cmd_arg_order_by(cmd, sh, _stdout) + + +def test_dtsh_builtin_find_arg_longfmt() -> None: + cmd = DTShBuiltinFind() + sh = DTSh(DTShTests.get_sample_dtmodel(), [cmd]) + DTShTests.check_cmd_arg_longfmt(cmd, sh, _stdout) + + +def test_dtsh_builtin_find_arg_criteria() -> None: + cmd = DTShBuiltinFind() + sh = DTSh(DTShTests.get_sample_dtmodel(), [cmd]) + + args: List[DTShArgCriterion] = [ + cmd.option( + f"-{opt.sh}" if opt.shortname else f"--{opt.longname}" # type: ignore + ) + for opt in DTSH_ARG_NODE_CRITERIA + ] + + for arg in args: + assert not arg.isset + assert not arg.get_criterion() + + argv: List[str] = [] + for opt in args: + argv.append( + f"-{opt.shortname}" if opt.shortname else f"--{opt.longname}" + ) + # "*" is valid for both text-based and int-based criteria. + argv.append("*") + + cmd.execute(argv, sh, _stdout) + + for arg in args: + assert arg.isset + assert arg.get_criterion() + + +def test_dtsh_builtin_find_param() -> None: + cmd = DTShBuiltinFind() + sh = DTSh(DTShTests.get_sample_dtmodel(), [cmd]) + DTShTests.check_cmd_param_dtpaths(cmd, sh, _stdout) + + +def test_dtsh_builtin_find_execute() -> None: + cmd = DTShBuiltinFind() + sh = DTSh(DTShTests.get_sample_dtmodel(), [cmd]) + + DTShTests.check_cmd_execute(cmd, sh, _stdout) diff --git a/tests/test_dtsh_builtin_ls.py b/tests/test_dtsh_builtin_ls.py new file mode 100644 index 0000000..0419753 --- /dev/null +++ b/tests/test_dtsh_builtin_ls.py @@ -0,0 +1,60 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Unit tests for the dtsh.builtins.ls module.""" + +# Relax pylint a bit for unit tests. +# pylint: disable=missing-function-docstring + + +from dtsh.io import DTShOutput +from dtsh.shell import DTSh +from dtsh.builtins.ls import DTShBuiltinLs + +from .dtsh_uthelpers import DTShTests + +_stdout = DTShOutput() + + +def test_dtsh_builtin_ls() -> None: + cmd = DTShBuiltinLs() + DTShTests.check_cmd_meta(cmd, "ls") + + +def test_dtsh_builtin_ls_flags() -> None: + cmd = DTShBuiltinLs() + sh = DTSh(DTShTests.get_sample_dtmodel(), [cmd]) + DTShTests.check_cmd_flags(cmd, sh, _stdout) + + +def test_dtsh_builtin_ls_arg_orderby() -> None: + cmd = DTShBuiltinLs() + sh = DTSh(DTShTests.get_sample_dtmodel(), [cmd]) + DTShTests.check_cmd_arg_order_by(cmd, sh, _stdout) + + +def test_dtsh_builtin_ls_arg_longfmt() -> None: + cmd = DTShBuiltinLs() + sh = DTSh(DTShTests.get_sample_dtmodel(), [cmd]) + DTShTests.check_cmd_arg_longfmt(cmd, sh, _stdout) + + +def test_dtsh_builtin_ls_arg_fixed_depth() -> None: + cmd = DTShBuiltinLs() + sh = DTSh(DTShTests.get_sample_dtmodel(), [cmd]) + DTShTests.check_cmd_arg_fixed_depth(cmd, sh, _stdout) + + +def test_dtsh_builtin_ls_param() -> None: + cmd = DTShBuiltinLs() + sh = DTSh(DTShTests.get_sample_dtmodel(), [cmd]) + DTShTests.check_cmd_param_dtpaths(cmd, sh, _stdout) + + +def test_dtsh_builtin_ls_execute() -> None: + out = DTShOutput() + cmd = DTShBuiltinLs() + sh = DTSh(DTShTests.get_sample_dtmodel(), [cmd]) + + DTShTests.check_cmd_execute(cmd, sh, out) diff --git a/tests/test_dtsh_builtin_pwd.py b/tests/test_dtsh_builtin_pwd.py new file mode 100644 index 0000000..9c0adff --- /dev/null +++ b/tests/test_dtsh_builtin_pwd.py @@ -0,0 +1,28 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Unit tests for the dtsh.builtins.pwd module.""" + +# Relax pylint a bit for unit tests. +# pylint: disable=missing-function-docstring + + +from dtsh.io import DTShOutput +from dtsh.shell import DTSh +from dtsh.builtins.pwd import DTShBuiltinPwd + +from .dtsh_uthelpers import DTShTests + + +def test_dtsh_builtin_pwd() -> None: + cmd = DTShBuiltinPwd() + DTShTests.check_cmd_meta(cmd, "pwd") + + +def test_dtsh_builtin_pwd_execute() -> None: + out = DTShOutput() + cmd = DTShBuiltinPwd() + sh = DTSh(DTShTests.get_sample_dtmodel(), [cmd]) + + DTShTests.check_cmd_execute(cmd, sh, out) diff --git a/tests/test_dtsh_builtin_tree.py b/tests/test_dtsh_builtin_tree.py new file mode 100644 index 0000000..10ad2e2 --- /dev/null +++ b/tests/test_dtsh_builtin_tree.py @@ -0,0 +1,59 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Unit tests for the dtsh.builtins.tree module.""" + +# Relax pylint a bit for unit tests. +# pylint: disable=missing-function-docstring + +from dtsh.io import DTShOutput +from dtsh.shell import DTSh +from dtsh.builtins.tree import DTShBuiltinTree + +from .dtsh_uthelpers import DTShTests + +_stdout = DTShOutput() + + +def test_dtsh_builtin_tree() -> None: + cmd = DTShBuiltinTree() + DTShTests.check_cmd_meta(cmd, "tree") + + +def test_dtsh_builtin_tree_flags() -> None: + cmd = DTShBuiltinTree() + sh = DTSh(DTShTests.get_sample_dtmodel(), [cmd]) + DTShTests.check_cmd_flags(cmd, sh, _stdout) + + +def test_dtsh_builtin_tree_arg_orderby() -> None: + cmd = DTShBuiltinTree() + sh = DTSh(DTShTests.get_sample_dtmodel(), [cmd]) + DTShTests.check_cmd_arg_order_by(cmd, sh, _stdout) + + +def test_dtsh_builtin_tree_arg_longfmt() -> None: + cmd = DTShBuiltinTree() + sh = DTSh(DTShTests.get_sample_dtmodel(), [cmd]) + DTShTests.check_cmd_arg_longfmt(cmd, sh, _stdout) + + +def test_dtsh_builtin_tree_arg_fixed_depth() -> None: + cmd = DTShBuiltinTree() + sh = DTSh(DTShTests.get_sample_dtmodel(), [cmd]) + DTShTests.check_cmd_arg_fixed_depth(cmd, sh, _stdout) + + +def test_dtsh_builtin_tree_param() -> None: + cmd = DTShBuiltinTree() + sh = DTSh(DTShTests.get_sample_dtmodel(), [cmd]) + DTShTests.check_cmd_param_dtpaths(cmd, sh, _stdout) + + +def test_dtsh_builtin_tree_execute() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + cmd = DTShBuiltinTree() + sh = DTSh(dtmodel, [cmd]) + + DTShTests.check_cmd_execute(cmd, sh, _stdout) diff --git a/tests/test_dtsh_config.py b/tests/test_dtsh_config.py new file mode 100644 index 0000000..0187ca4 --- /dev/null +++ b/tests/test_dtsh_config.py @@ -0,0 +1,136 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Unit tests for the dtsh.config module.""" + +# Relax pylint a bit for unit tests. +# pylint: disable=missing-function-docstring +# pylint: disable=import-outside-toplevel + + +import os + +import pytest + +from dtsh.config import DTShConfig, ActionableType + +from .dtsh_uthelpers import DTShTests + + +def test_dtshconfig_load_ini_file() -> None: + # 1. Initialize configuration with bundled defaults. + cfg = DTShConfig(DTShTests.get_resource_path("ini", "test.ini")) + assert cfg.getstr("test.string") == "a string" + assert cfg.getstr("not.an.option") == "" + # 2. Load user's specific configuration (overrides defaults). + cfg.load_ini_file(DTShTests.get_resource_path("ini", "override.ini")) + assert cfg.getstr("test.string") == "overridden" + assert cfg.getstr("test.new") == "new" + + with pytest.raises(DTShConfig.Error): + cfg.load_ini_file("not/a/config/file.ini") + + +def test_dtshconfig_getbool() -> None: + cfg = DTShConfig(DTShTests.get_resource_path("ini", "test.ini")) + + assert cfg.getbool("test.true", False) + assert not cfg.getbool("test.false", True) + # getbool() is fail-safe. + assert not cfg.getbool("test.bool.inval") + assert cfg.getbool("undefined", True) + + +def test_dtshconfig_getint() -> None: + cfg = DTShConfig(DTShTests.get_resource_path("ini", "test.ini")) + + assert cfg.getint("test.int") == 255 + assert cfg.getint("test.hex") == 0xFF + # getint() is fail-safe. + assert cfg.getint("test.int.inval") == 0 + assert cfg.getint("test.int.inval", -1) == -1 + + +def test_dtshconfig_getstr() -> None: + cfg = DTShConfig(DTShTests.get_resource_path("ini", "test.ini")) + + assert cfg.getstr("test.string") == "a string" + assert cfg.getstr("test.string.quoted") == "quoted string " + assert cfg.getstr("test.string.quotes") == 'a"b' + assert cfg.getstr("test.string.unicode") == "❯" + assert cfg.getstr("test.string.literal") == "\u276F" + assert cfg.getstr("test.string.mixed") == "\u276F ❯" + # getstr() is fail-safe. + assert cfg.getstr("undefined") == "" + assert cfg.getstr("undefined", "any") == "any" + # Empty values map to empty strings. + assert cfg.getstr("test.novalue") == "" + + +def test_dtshconfig_interpolation() -> None: + cfg = DTShConfig(DTShTests.get_resource_path("ini", "test.ini")) + assert cfg.getstr("test.interpolation") == "hello world" + + +def test_dtshconfig_defaults() -> None: + dtsh_ini = os.path.abspath( + os.path.join(os.path.dirname(__file__), "..", "src", "dtsh", "dtsh.ini") + ) + assert os.path.isfile(dtsh_ini) + cfg_defaults = DTShConfig(dtsh_ini) + + # Wide characters. + assert "…" == cfg_defaults.wchar_ellipsis + assert "↗" == cfg_defaults.wchar_arrow_ne + assert "↖" == cfg_defaults.wchar_arrow_nw + assert "→" == cfg_defaults.wchar_arrow_right + assert "↳" == cfg_defaults.wchar_arrow_right_hook + assert "—" == cfg_defaults.wchar_dash + + # Prompt. + assert cfg_defaults.prompt_default + assert cfg_defaults.prompt_alt + assert cfg_defaults.prompt_sparse + + # General preferences. + assert 255 == cfg_defaults.pref_redir2_maxwidth + assert not cfg_defaults.pref_always_longfmt + assert cfg_defaults.pref_sizes_si + assert not cfg_defaults.pref_hex_upper + assert cfg_defaults.pref_fs_hide_dotted + assert cfg_defaults.pref_fs_no_spaces + assert cfg_defaults.pref_fs_no_overwrite + assert ActionableType.LINK == cfg_defaults.pref_actionable_type + + # List views. + assert cfg_defaults.pref_list_headers + assert not cfg_defaults.pref_list_placeholder + assert cfg_defaults.pref_list_fmt + assert ActionableType.LINK == ActionableType( + cfg_defaults.pref_list_actionable_type + ) + + # Tree views. + assert cfg_defaults.pref_tree_headers + assert cfg_defaults.pref_tree_placeholder + assert cfg_defaults.pref_tree_fmt + assert ActionableType.NONE == ActionableType( + cfg_defaults.pref_tree_actionable_type + ) + assert ActionableType.LINK == cfg_defaults.pref_2Sided_actionable_type + # 2-sided views child marker. + assert cfg_defaults.pref_tree_cb_anchor + + # Actionable type. + assert ActionableType.LINK == cfg_defaults.pref_actionable_type + assert cfg_defaults.pref_actionable_text + + # HTML. + assert "html" == cfg_defaults.pref_html_theme + assert "Courier New" == cfg_defaults.pref_html_font_family + + # SVG. + assert "svg" == cfg_defaults.pref_svg_theme + assert "Courier New" == cfg_defaults.pref_svg_font_family + assert 0.6 == cfg_defaults.pref_svg_font_ratio diff --git a/tests/test_dtsh_dts.py b/tests/test_dtsh_dts.py new file mode 100644 index 0000000..e4da811 --- /dev/null +++ b/tests/test_dtsh_dts.py @@ -0,0 +1,345 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Unit tests for the dtsh.dts module.""" + +# Relax pylint a bit for unit tests. +# pylint: disable=protected-access +# pylint: disable=missing-function-docstring + + +from typing import Optional + +import os + +import pytest + +from dtsh.dts import DTS, CMakeCache, YAMLFilesystem, YAMLFile + +from .dtsh_uthelpers import DTShTests + + +def test_cmakecache_init() -> None: + cache: Optional[CMakeCache] + + with DTShTests.from_res(): + assert 4 == len(CMakeCache("CMakeCache.txt")) + + # Opening a non existing file should not fault. + assert CMakeCache.open("notafile") is None + + # Opening an invalid CMakeCache file will not fault, + # and answer an empty cache instead (lines do not match expected format). + with DTShTests.from_res(): + cache = CMakeCache.open("zephyr.dts") + assert cache is not None + assert 0 == len(cache) + + +def test_cmakecache_getstr() -> None: + with DTShTests.from_res(): + cache = CMakeCache("CMakeCache.txt") + assert "foobar" == cache.getstr("DTSH_TEST_STRING") + assert cache.getstr("NOT_AN_ENTRY") is None + + +def test_cmakecache_getstrs() -> None: + with DTShTests.from_res(): + cache = CMakeCache("CMakeCache.txt") + assert ["foo", "bar"] == cache.getstrs("DTSH_TEST_STRING_LIST") + assert [] == cache.getstrs("NOT_AN_ENTRY") + + +def test_cmakecache_getbool() -> None: + with DTShTests.from_res(): + cmake_cache = CMakeCache("CMakeCache.txt") + assert cmake_cache.getbool("DTSH_TEST_BOOL_TRUE") + assert not cmake_cache.getbool("DTSH_TEST_BOOL_FALSE") + assert not cmake_cache.getbool("NOT_AN_ENTRY") + + +def test_dts_init() -> None: + # Use case: + # - CMake cache: not available + # - OS environment: reset + with DTShTests.mock_env( + { + "ZEPHYR_BASE": None, + "ZEPHYR_SDK_INSTALL_DIR": None, + "ZEPHYR_TOOLCHAIN_VARIANT": None, + } + ): + with DTShTests.from_res(): + dts = DTS("zephyr.dts") + + # App. binary directory (aka build) is always set to + # the parent of the directory that contains the DTS file + # (aka build/zephyr/zephyr.dts file layout). + app_bin_dir = os.path.dirname(os.path.dirname(dts.path)) + assert app_bin_dir == dts.app_binary_dir + # Derived from the app. bin. directory. + app_src_dir = os.path.dirname(app_bin_dir) + assert app_src_dir == dts.app_source_dir + + # Unset in mock environment. + assert dts.zephyr_sdk_dir is None + assert dts.toolchain_variant is None + assert dts.toolchain_dir is None + + # Possibly set only when a CMake cache is available. + assert dts.board_dir is None + assert dts.board is None + assert [] == dts.shield_dirs + assert dts.fw_name is None + assert dts.fw_version is None + + # Default minimal bindings search path. + assert [ + os.path.join(app_src_dir, "dts", "bindings"), + os.path.join(dts.zephyr_base or "?", "dts", "bindings"), + ] == dts.bindings_search_path + + +def test_dts_init_from_os_env() -> None: + # Use case: + # - CMake cache: not available + # - OS environment: ZEPHYR_BASE, ZEPHYR_TOOLCHAIN_VARIANT + tmpenv = { + "ZEPHYR_BASE": "dummy", + "ZEPHYR_SDK_INSTALL_DIR": None, + "ZEPHYR_TOOLCHAIN_VARIANT": "dummy", + } + with DTShTests.mock_env(tmpenv): + with DTShTests.from_res(): + dts = DTS("zephyr.dts") + + # Parent of the directory that contains the DTS file. + app_bin_dir = os.path.dirname(os.path.dirname(dts.path)) + assert app_bin_dir == dts.app_binary_dir + # Derived from the app. bin. directory. + app_src_dir = os.path.dirname(app_bin_dir) + assert app_src_dir == dts.app_source_dir + + # Possibly set only when a CMake cache is available. + assert dts.board_dir is None + assert dts.board is None + assert [] == dts.shield_dirs + assert dts.fw_name is None + assert dts.fw_version is None + + # Retrieved from mock environment. + assert dts.zephyr_base + assert tmpenv["ZEPHYR_BASE"] == dts.zephyr_base + assert ( + os.path.join( + dts.zephyr_base, "dts", "bindings", "vendor-prefixes.txt" + ) + == dts.vendors_file + ) + assert tmpenv["ZEPHYR_TOOLCHAIN_VARIANT"] == dts.toolchain_variant + assert dts.zephyr_sdk_dir is None + # Environment variable dummy_TOOLCHAIN_PATH does not exist. + assert dts.toolchain_dir is None + + # Default minimal bindings search path. + expect_dirs = [ + os.path.join(app_src_dir, "dts", "bindings"), + os.path.join(dts.zephyr_base, "dts", "bindings"), + ] + assert expect_dirs == dts.bindings_search_path + + +def test_dts_init_from_cmake_zephyr() -> None: + # Use-case: + # - CMake cache: res/Build_zephyr/CMakeCache.txt + # - Environment: reset + with DTShTests.mock_env( + { + "ZEPHYR_BASE": None, + "ZEPHYR_SDK_INSTALL_DIR": None, + "ZEPHYR_TOOLCHAIN_VARIANT": None, + } + ): + dts = DTS( + DTShTests.get_resource_path("Build_zephyr", "zephyr", "zephyr.dts") + ) + + assert dts.zephyr_base + assert ( + os.path.join( + dts.zephyr_base, "dts", "bindings", "vendor-prefixes.txt" + ) + == dts.vendors_file + ) + + # Parent of the directory that contains the DTS file. + app_bin_dir = os.path.dirname(os.path.dirname(dts.path)) + assert app_bin_dir == dts.app_binary_dir + + # Retrieved from the CMake cache. + assert ( + os.path.join( + DTShTests.ANON_ZEPHYR_BASE, "samples", "sensor", "bme680" + ) + == dts.app_source_dir + ) + assert ( + os.path.join( + DTShTests.ANON_ZEPHYR_BASE, + "boards", + "arm", + "nrf52840dk_nrf52840", + ) + == dts.board_dir + ) + assert DTShTests.BOARD == dts.board + assert [] == dts.shield_dirs + assert "bme680" == dts.fw_name + assert DTShTests.ZEPHYR_VERSION == dts.fw_version + assert os.path.join(DTShTests.ANON_ZEPHYR_BASE) == dts.zephyr_base + assert DTShTests.ANON_ZEPHYR_SDK == dts.zephyr_sdk_dir + assert "zephyr" == dts.toolchain_variant + assert DTShTests.ANON_ZEPHYR_SDK == dts.toolchain_dir + + +def test_dts_init_from_cmake_gnuarm() -> None: + # Use-case: + # - CMake cache: res/Build_gnuarm/CMakeCache.txt + # - Environment: reset + with DTShTests.mock_env( + { + "ZEPHYR_BASE": None, + "ZEPHYR_SDK_INSTALL_DIR": None, + "ZEPHYR_TOOLCHAIN_VARIANT": None, + } + ): + dts = DTS( + DTShTests.get_resource_path("Build_gnuarm", "zephyr", "zephyr.dts") + ) + + assert dts.zephyr_base + assert ( + os.path.join( + dts.zephyr_base, "dts", "bindings", "vendor-prefixes.txt" + ) + == dts.vendors_file + ) + + # Parent of the directory that contains the DTS file. + app_bin_dir = os.path.dirname(os.path.dirname(dts.path)) + assert app_bin_dir == dts.app_binary_dir + + # Retrieved from the CMake cache. + assert ( + os.path.join( + DTShTests.ANON_ZEPHYR_BASE, "samples", "sensor", "bme680" + ) + == dts.app_source_dir + ) + assert ( + os.path.join( + DTShTests.ANON_ZEPHYR_BASE, + "boards", + "arm", + "nrf52840dk_nrf52840", + ) + == dts.board_dir + ) + assert DTShTests.BOARD == dts.board + assert [] == dts.shield_dirs + assert "bme680" == dts.fw_name + assert DTShTests.ZEPHYR_VERSION == dts.fw_version + assert DTShTests.ANON_ZEPHYR_BASE == dts.zephyr_base + assert dts.zephyr_sdk_dir is None + assert "gnuarmemb" == dts.toolchain_variant + assert DTShTests.ANON_GNUARMEMB == dts.toolchain_dir + + +def test_yamlfs_find_path() -> None: + with DTShTests.from_res(): + yamlfs = YAMLFilesystem(["yaml"]) + + assert yamlfs.find_path("notafile") is None + + for name, path in [ + ("power.yaml", DTShTests.get_resource_path("yaml", "power.yaml")), + ( + "i2c-device.yaml", + DTShTests.get_resource_path("yaml", "i2c-device.yaml"), + ), + ( + "sensor-device.yaml", + DTShTests.get_resource_path("yaml", "sensor-device.yaml"), + ), + ]: + assert path == yamlfs.find_path(name) + + +def test_yamlfs_find_file() -> None: + with DTShTests.from_res(): + yamlfs = YAMLFilesystem(["yaml"]) + + fyaml = yamlfs.find_file("i2c-device.yaml") + assert fyaml + assert DTShTests.get_resource_path("yaml", "i2c-device.yaml") == fyaml.path + + # Opening a non existing file should not fault. + assert yamlfs.find_file("notafile") is None + + +def test_yamlfs_name2path() -> None: + with DTShTests.from_res(): + yamlfs = YAMLFilesystem(["yaml"]) + + assert { + "i2c-device.yaml": DTShTests.get_resource_path( + "yaml", "i2c-device.yaml" + ), + "power.yaml": DTShTests.get_resource_path("yaml", "power.yaml"), + "sensor-device.yaml": DTShTests.get_resource_path( + "yaml", "sensor-device.yaml" + ), + } == yamlfs.name2path + + with pytest.raises(KeyError): + _ = yamlfs.name2path["notafile"] + + +def test_dtshdts_yamlfs_find_path() -> None: + with DTShTests.from_res(): + dts = DTS("zephyr.dts", ["yaml"]) + + assert DTShTests.get_resource_path( + "yaml", "sensor-device.yaml" + ) == dts.yamlfs.find_path("sensor-device.yaml") + + +def test_dtshdts_yamlfs_find_file() -> None: + with DTShTests.from_res(): + dts = DTS("zephyr.dts", ["yaml"]) + + yaml = dts.yamlfs.find_file("sensor-device.yaml") + assert yaml + assert ( + DTShTests.get_resource_path("yaml", "sensor-device.yaml") == yaml.path + ) + + +def test_yamlfile() -> None: + yaml = YAMLFile(DTShTests.get_resource_path("yaml", "i2c-device.yaml")) + + # Lazy initialzation. + assert yaml._content is None + assert yaml._raw is None + assert yaml._includes is None + assert yaml.content.startswith("# Copyright (c) 2017, Linaro Limited") + assert yaml.content.endswith("i2c bus") + assert "i2c" == yaml.raw["on-bus"] + assert ["base.yaml", "power.yaml"] == yaml.includes + + # Fail-safe + yaml = YAMLFile("notafile") + assert "" == yaml.content + assert {} == yaml.raw + assert [] == yaml.includes diff --git a/tests/test_dtsh_io.py b/tests/test_dtsh_io.py new file mode 100644 index 0000000..39e7870 --- /dev/null +++ b/tests/test_dtsh_io.py @@ -0,0 +1,45 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Unit tests for the dtsh.io module.""" + +# Relax pylint a bit for unit tests. +# pylint: disable=missing-function-docstring + + +import os + +import pytest + +from dtsh.config import DTShConfig +from dtsh.io import DTShRedirect + +from .dtsh_uthelpers import DTShTests + + +_dtshconf: DTShConfig = DTShConfig.getinstance() + + +def test_dtsh_redirect() -> None: + redir2 = DTShRedirect("> path") + assert not redir2.append + assert redir2.path == os.path.abspath("path") + + redir2 = DTShRedirect(">> path") + # Won't append to non existing file. + assert not redir2.append + assert redir2.path == os.path.abspath("path") + + with pytest.raises(DTShRedirect.Error): + # Redirection to empty path. + redir2 = DTShRedirect("command string>") + + with pytest.raises(DTShRedirect.Error): + # No spaces allowed. + redir2 = DTShRedirect("command string > to/file with spaces") + + path = DTShTests.get_resource_path("README") + with pytest.raises(DTShRedirect.Error): + # File exists, won't override. + redir2 = DTShRedirect(f"command string > {path}") diff --git a/tests/test_dtsh_model.py b/tests/test_dtsh_model.py new file mode 100644 index 0000000..a97ea67 --- /dev/null +++ b/tests/test_dtsh_model.py @@ -0,0 +1,981 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Unit tests for the dtsh.model module.""" + +# Relax pylint a bit for unit tests. +# pylint: disable=protected-access +# pylint: disable=pointless-statement +# pylint: disable=missing-function-docstring +# pylint: disable=missing-class-docstring +# pylint: disable=too-many-statements + + +from typing import Any, Set, Tuple, List, Sequence + +import os +import sys + +import pytest + +from dtsh.model import ( + DTPath, + DTVendor, + DTNodeInterrupt, + DTNodeRegister, + DTNode, + DTNodeSorter, + DTNodeCriterion, + DTNodeCriteria, +) + +from .dtsh_uthelpers import DTShTests + + +def test_dtpath_split() -> None: + assert ["a"] == DTPath.split("a") + assert ["a", "b", "c"] == DTPath.split("a/b/c") + # "/" always represent the first node name of a path name. + assert ["/"] == DTPath.split("/") + assert ["/", "x"] == DTPath.split("/x") + assert ["/", "x", "y", "z"] == DTPath.split("/x/y/z") + # Removed trailing empty node name. + assert ["/", "a"] == DTPath.split("/a/") + assert [] == DTPath.split("") + + +def test_dtpath_join() -> None: + assert "/a/b" == DTPath.join("/", "a", "b") + assert "/a/b" == DTPath.join("/a", "b") + assert "a/b" == DTPath.join("a/", "b") + # Path is NOT normalized. + assert "a/." == DTPath.join("a", ".") + assert "a/b/" == DTPath.join("a", "b/") + assert "a/" == DTPath.join("a", "") + # Joining an absolute path will reset the join chain. + assert "/x/y" == DTPath.join("/a", "b", "/x", "y") + + +def test_dtpath_normpath() -> None: + # Remove redundant trailing ".". + assert "/" == DTPath.normpath("/.") + # Remove redundant "/". + assert "a/b" == DTPath.normpath("a/b/") + assert "a/b" == DTPath.normpath("a//b") + # Remove redundant "." and "..". + assert "a/b" == DTPath.normpath("a/foo/../b") + assert "a/b" == DTPath.normpath("a/./b") + # Root is its own parent. + assert "/a" == DTPath.normpath("/../a") + # Normalize empty paths. + assert "." == DTPath.normpath("") + + +def test_dtpath_abspath() -> None: + assert DTPath.abspath("") == "/" + assert DTPath.abspath("/") == "/" + assert DTPath.abspath("a") == "/a" + assert DTPath.abspath("a/b") == "/a/b" + # Path is normalized. + assert DTPath.abspath("a/.") == "/a" + assert DTPath.abspath("../a") == "/a" + # Joined path starting with "/" will reset the join chain to itself. + assert DTPath.abspath("a", "/foo") == "/foo/a" + # Preserve absolute paths regardless of the "current working node". + assert DTPath.abspath("/a/b", "/x") == "/a/b" + + with pytest.raises(ValueError): + # Current working branch must be represented by an absolute path. + DTPath.abspath("a", "relative/path") + + +def test_dtpath_relpath() -> None: + # Relative paths from "/". + assert "." == DTPath.relpath("/") + assert "a" == DTPath.relpath("/a") + # Relative paths from "/a". + assert "." == DTPath.relpath("/a", "/a") + assert "b/c" == DTPath.relpath("/a/b/c", "/a") + + with pytest.raises(ValueError): + # Expect absolute path parameter.. + DTPath.relpath("a/b") + with pytest.raises(ValueError): + # Current working branch must be represented by an absolute path. + DTPath.relpath("/a/b", "relative/path") + + +def test_dtpath_dirname() -> None: + assert "/" == DTPath.dirname("/a") + assert "/a/b" == DTPath.dirname("/a/b/c") + assert "a" == DTPath.dirname("a/b") + # The root node is its own parent. + assert "/" == DTPath.dirname("/") + # A trailing '/' is interpreted as an empty trailing node name. + assert "." == DTPath.dirname("./") + assert "/a" == DTPath.dirname("/a/") + # Returns "." when path does not contain any "/". + assert "." == DTPath.dirname("") + assert "." == DTPath.dirname("a") + assert "." == DTPath.dirname(".") + assert "." == DTPath.dirname("..") + + +def test_dtpath_basename() -> None: + assert "a" == DTPath.basename("/a") + assert "a" == DTPath.basename("a") + assert "b" == DTPath.basename("a/b") + assert "b" == DTPath.basename("/a/b") + assert "*" == DTPath.basename("/*") + assert "b*" == DTPath.basename("a/b*") + # A trailing '/' is interpreted as an empty trailing node name. + assert "" == DTPath.basename("/") + assert "" == DTPath.basename("a/") + # By convention. + assert "" == DTPath.basename("") + assert "." == DTPath.basename(".") + assert ".." == DTPath.basename("..") + + +def test_dtvendor() -> None: + vendor = DTVendor("prefix", "vendor") + assert "prefix" == vendor.prefix + assert "vendor" == vendor.name + + # Only the prefix is relevant for identity and equality. + assert 1 == len({vendor, DTVendor("prefix", "")}) + assert 2 == len({vendor, DTVendor("other", "")}) + assert DTVendor("prefix", "") == vendor + assert vendor != DTVendor("other", "") + + # Order relationship is also by prefix. + assert vendor < DTVendor("z", "") + with pytest.raises(TypeError): + vendor < "" + + +def test_dtbinding() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + dt_partitions = dtmodel["/soc/flash-controller@4001e000/flash@0/partitions"] + dt_partition0 = dt_partitions.get_child("partition@0") + dt_partition1 = dt_partitions.get_child("partition@c000") + dt_bme680 = dtmodel["/soc/i2c@40003000/bme680@76"] + + assert dt_partitions.binding + assert 0 == dt_partitions.binding.cb_depth + assert "fixed-partitions" == dt_partitions.binding.compatible + assert dt_partition0.binding + assert 1 == dt_partition0.binding.cb_depth + assert not dt_partition0.binding.compatible + assert dt_partition1.binding + assert 1 == dt_partition1.binding.cb_depth + assert not dt_partition1.binding.compatible + assert dt_bme680.binding + assert 0 == dt_bme680.binding.cb_depth + assert "bosch,bme680" == dt_bme680.binding.compatible + + # Identity. + # Nodes "partition@0" and "partition@c000" are specified by the same + # child-binding of "partitions". + assert 1 == len({dt_partition0.binding, dt_partition1.binding}) + # Bindings for "partition@0" and "partitions" are defined in the same + # YAML file, but the former is a child-binding of the later. + assert 2 == len({dt_partitions.binding, dt_partition0.binding}) + # Then ,obviously. + assert 3 == len( + {dt_partitions.binding, dt_partition0.binding, dt_bme680.binding} + ) + + assert ( + dtmodel.get_compatible_binding("bosch,bme680", "i2c") + is dt_bme680.binding + ) + assert ( + dtmodel.get_compatible_binding("fixed-partitions") + is dt_partitions.binding + ) + + # Equality + assert dt_partitions.binding == dt_partitions.binding + assert dt_partition1.binding == dt_partition0.binding + assert dt_partitions.binding != dt_partition0.binding + assert dt_bme680.binding != dt_partitions.binding + + # Default order. + assert dt_bme680.binding < dt_partitions.binding + assert dt_partitions.binding < dt_partition0.binding + with pytest.raises(TypeError): + dt_partitions.binding < "" + + +def test_dtbinding_buses() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + dt_i2c = dtmodel["/soc/i2c@40003000"] + dt_bme680_i2c = dt_i2c.get_child("bme680@76") + dt_spi = dtmodel["/soc/spi@40004000"] + dt_bme680_spi = dt_spi.get_child("bme680@0") + + assert dt_i2c.binding + assert dt_spi.binding + assert dt_bme680_i2c.binding + assert dt_bme680_spi.binding + assert ["i2c"] == dt_i2c.binding.buses + assert ["spi"] == dt_spi.binding.buses + + assert "i2c" == dt_bme680_i2c.binding.on_bus + assert "bosch,bme680-i2c.yaml" == os.path.basename( + dt_bme680_i2c.binding.path + ) + assert not dt_bme680_i2c.binding.buses + + assert "spi" == dt_bme680_spi.binding.on_bus + assert "bosch,bme680-spi.yaml" == os.path.basename( + dt_bme680_spi.binding.path + ) + assert not dt_bme680_spi.binding.buses + + +def test_dtbinding_includes() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + dt_bme680 = dtmodel["/soc/i2c@40003000/bme680@76"] + assert dt_bme680.binding + + assert sorted(["sensor-device.yaml", "i2c-device.yaml"]) == sorted( + [os.path.basename(path) for path in dt_bme680.binding.includes] + ) + + +def test_dtbinding_child_binding() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + dt_partitions = dtmodel["/soc/flash-controller@4001e000/flash@0/partitions"] + dt_partition0 = dt_partitions.get_child("partition@0") + dt_partition1 = dt_partitions.get_child("partition@c000") + assert dt_partitions.binding + assert 0 == dt_partitions.binding.cb_depth + assert dt_partitions.binding.child_binding + + assert dt_partition0.binding + assert 1 == dt_partition0.binding.cb_depth + assert not dt_partition0.binding.child_binding + assert dt_partition0.binding is dt_partitions.binding.child_binding + + assert dt_partition1.binding + assert 1 == dt_partition1.binding.cb_depth + assert not dt_partition1.binding.child_binding + assert dt_partition1.binding is dt_partitions.binding.child_binding + + +def test_dtinterrupt() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + dt_i2c0 = dtmodel["/soc/i2c@40003000"] + edtirq_i2c0 = dt_i2c0._edtnode.interrupts[0] + dt_gpiote = dtmodel["/soc/gpiote@40006000"] + edtirq_gpiote = dt_gpiote._edtnode.interrupts[0] + + irq_i2c0 = DTNodeInterrupt(edtirq_i2c0, dt_i2c0) + assert 3 == irq_i2c0.number + assert 1 == irq_i2c0.priority + assert "IRQ_i2c0" == irq_i2c0.name + assert dt_i2c0 == irq_i2c0.emitter + assert irq_i2c0.controller is dtmodel[edtirq_i2c0.controller.path] + + irq_gpiote = DTNodeInterrupt(edtirq_gpiote, dt_gpiote) + assert 6 == irq_gpiote.number + assert 5 == irq_gpiote.priority + assert dt_gpiote == irq_gpiote.emitter + assert irq_gpiote.controller is dtmodel[edtirq_gpiote.controller.path] + + # Identity. + assert 1 == len({irq_i2c0, DTNodeInterrupt(edtirq_i2c0, dt_i2c0)}) + assert 2 == len({irq_i2c0, DTNodeInterrupt(edtirq_gpiote, dt_gpiote)}) + + # Equality. + assert DTNodeInterrupt(edtirq_i2c0, dt_i2c0) == irq_i2c0 + assert DTNodeInterrupt(edtirq_gpiote, dt_gpiote) == irq_gpiote + assert irq_gpiote != irq_i2c0 + + # Default order. + assert irq_i2c0 < irq_gpiote + with pytest.raises(TypeError): + irq_i2c0 < "" + + +def test_dtregister() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + dt_qspi = dtmodel["/soc/qspi@40029000"] + edtreg_qspi = dt_qspi._edtnode.regs[0] + + reg_qspi = DTNodeRegister(edtreg_qspi) + assert 0x40029000 == reg_qspi.address + assert 0x1000 == reg_qspi.size + assert reg_qspi.address + reg_qspi.size - 1 == reg_qspi.tail + assert "qspi" == reg_qspi.name + + edtreg_bme680 = dtmodel["/soc/i2c@40003000/bme680@76"]._edtnode.regs[0] + reg_bme680 = DTNodeRegister(edtreg_bme680) + assert 0x76 == reg_bme680.address + assert 0 == reg_bme680.size + assert reg_bme680.address == reg_bme680.tail + + # Neither identity nor equality. + assert 2 == len({reg_qspi, DTNodeRegister(edtreg_qspi)}) + assert DTNodeRegister(edtreg_qspi) != reg_qspi + + # Default order. + # Meaningless, but 0x76 < 0x40029000 nonetheless + assert reg_bme680 < reg_qspi + with pytest.raises(TypeError): + reg_bme680 < "" + + +def test_dtnode() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + edt = dtmodel._edt + + # Identity. + assert 1 == len({dtmodel["/soc"], dtmodel["/soc"]}) + assert 2 == len({dtmodel["/soc"], dtmodel["/cpus"]}) + + # Equality. + assert dtmodel["/soc"] == dtmodel["/soc"] + assert dtmodel["/soc"] != dtmodel["/cpus"] + + # Default order. + assert dtmodel["/cpus"] < dtmodel["/soc"] + + # Test some known values. + dt_button0 = dtmodel["/buttons/button_0"] + assert "Push button switch 0" == dt_button0.label + assert ["button0"] == dt_button0.labels + assert edt.get_node(dt_button0.path).description + assert edt.get_node(dt_button0.path).description == dt_button0.description + assert sorted(["led0", "bootloader-led0", "mcuboot-led0"]) == sorted( + dtmodel["/leds/led_0"].aliases + ) + assert ["zephyr,entropy"] == dtmodel["/soc/random@4000d000"].chosen + assert ( + DTVendor("nordic", "Nordic Semiconductor") + == dtmodel["/soc/egu@40014000"].vendor + ) + assert "i2c" == dtmodel["/soc/i2c@40003000/bme680@76"].on_bus + + +def test_dtnodes() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + edt = dtmodel._edt + + for node in dtmodel.walk(): + edtnode = edt.get_node(node.path) + assert edtnode == node._edtnode + assert edtnode.path == node.path + assert edtnode.name == node.name + assert edtnode.unit_addr == node.unit_addr + assert edtnode.status == node.status + assert (edtnode.status == "okay") == node.enabled + assert edtnode.label == node.label + assert edtnode.aliases == node.aliases + assert edtnode.labels == node.labels + assert edtnode.compats == node.compatibles + assert edtnode.matching_compat == node.compatible + assert edtnode.binding_path == node.binding_path + assert edtnode.dep_ordinal == node.dep_ordinal + assert node.buses == edtnode.buses + assert ( + edtnode._binding.on_bus if edtnode._binding else None + ) == node.on_bus + + if node.binding: + assert node.binding.path == edtnode.binding_path + assert node.binding.compatible == edtnode.matching_compat + + assert [child.path for child in node.children] == [ + child.path for _, child in edtnode.children.items() + ] + + assert sorted([req.path for req in edtnode.required_by]) == sorted( + [req.path for req in node.required_by] + ) + assert sorted([req.path for req in edtnode.depends_on]) == sorted( + [req.path for req in node.depends_on] + ) + + if edtnode.parent: + assert node.parent._edtnode.path == edtnode.parent.path + else: + assert edtnode.path == "/" + # dtsh: root is its own parent + assert dtmodel.root == node + assert dtmodel.root == node.parent + + +def test_dtnode_binding() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + + # Binding for compatible value. + dt_partitions = dtmodel["/soc/flash-controller@4001e000/flash@0/partitions"] + assert dt_partitions.binding + assert "fixed-partitions" == dt_partitions.binding.compatible + + # Child-binding without compatible value. + dt_partition0 = dt_partitions.get_child("partition@0") + assert dt_partition0.binding + assert not dt_partition0.binding.compatible + assert dt_partitions.binding.child_binding is dt_partition0.binding + + # Node without binding (e.g. "/", "/chosen", "/aliases", "/soc", "/cpus"). + assert not dtmodel["/soc"].binding + + +def test_dtnode_get_child() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + assert dtmodel.root.get_child("soc") + assert dtmodel["/soc"].get_child("random@4000d000") + + with pytest.raises(KeyError): + dtmodel.root.get_child("notachild") + + +def test_dtnode_vendor() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + dt_bme680 = dtmodel["/soc/i2c@40003000/bme680@76"] + # Only vendor prefixes are relevant. + assert DTVendor("bosch", "") == dt_bme680.vendor + # Only the manufacturer is relevant. + assert dtmodel.get_vendor("bosch,any") == dt_bme680.vendor + + +def test_dtnode_buses() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + dt_i2c = dtmodel["/soc/i2c@40003000"] + dt_bme680_i2c = dt_i2c.get_child("bme680@76") + dt_spi = dtmodel["/soc/spi@40004000"] + dt_bme680_spi = dt_spi.get_child("bme680@0") + + assert ["i2c"] == dt_i2c.buses + assert ["spi"] == dt_spi.buses + + assert "i2c" == dt_bme680_i2c.on_bus + assert dt_i2c == dt_bme680_i2c.on_bus_device + + assert "spi" == dt_bme680_spi.on_bus + assert dt_spi == dt_bme680_spi.on_bus_device + + +def test_dtnode_registers() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + dt_qspi = dtmodel["/soc/qspi@40029000"] + reg_qspi = dt_qspi.registers[0] + + assert 0x40029000 == reg_qspi.address + assert 0x1000 == reg_qspi.size + assert reg_qspi.address + reg_qspi.size - 1 == reg_qspi.tail + assert "qspi" == reg_qspi.name + + +def test_dtnode_walk() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + edt = dtmodel._edt + + # All nodes. + assert len(edt.nodes) == len(list(dtmodel.root.walk())) == dtmodel.size + # Enabled nodes. + assert len([node for node in edt.nodes if node.status == "okay"]) == len( + list(dtmodel.root.walk(enabled_only=True)) + ) + + # flash-controller@4001e000 + # └── flash@0 + # └── partitions + # ├── partitions/partition@0 + # ├── partitions/partition@c000 + # ├── partitions/partition@82000 + # └── partitions/partition@f8000 + dt_flash_ctrl = dtmodel["/soc/flash-controller@4001e000"] + assert [ + "flash-controller@4001e000", + "flash@0", + "partitions", + "partition@0", + "partition@c000", + "partition@82000", + "partition@f8000", + ] == [node.name for node in dt_flash_ctrl.walk()] + + # Child nodes in reverse order. + assert [ + "flash-controller@4001e000", + "flash@0", + "partitions", + "partition@f8000", + "partition@82000", + "partition@c000", + "partition@0", + ] == [node.name for node in dt_flash_ctrl.walk(reverse=True)] + + # Fixed depth. + assert [ + "flash-controller@4001e000", + "flash@0", + "partitions", + ] == [node.name for node in dt_flash_ctrl.walk(fixed_depth=2)] + + +def test_dtnode_walk_order_by() -> None: + dtmodel = DTShTests.get_sample_dtmodel(force_reload=True) + edt = dtmodel._edt + + # Save initial children ordering (DTS order) + unsorted = list(node for node in dtmodel.root.walk()) + + # Walk the whole devicetree, sorting children by path name. + natural_order = DTNodeSorter() + assert sorted([edtnode.path for edtnode in edt.nodes]) == [ + node.path for node in dtmodel.root.walk(order_by=natural_order) + ] + + # Assert we didn't mess with children ordering in the model. + assert unsorted == list(node for node in dtmodel.root.walk()) + + +def test_dtnode_rwalk() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + dt_partitions = dtmodel["/soc/flash-controller@4001e000/flash@0/partitions"] + dt_partition0 = dt_partitions.get_child("partition@0") + + assert list( + reversed( + [ + "/", + "soc", + "flash-controller@4001e000", + "flash@0", + "partitions", + "partition@0", + ] + ) + ) == [node.name for node in dt_partition0.rwalk()] + + +def test_dtnode_find() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + + class FindPwmNodes(DTNodeCriterion): + def match(self, node: DTNode) -> bool: + return node.name.find("pwm") != -1 + + assert [node.name for node in dtmodel if node.name.find("pwm") != -1] == [ + node.name for node in dtmodel.root.find(FindPwmNodes()) + ] + + assert sorted( + [node.path for node in dtmodel if node.name.find("pwm") != -1] + ) == [ + node.path + for node in dtmodel.root.find(FindPwmNodes(), order_by=DTNodeSorter()) + ] + + # An empty criterion list should match all nodes, like in POSIX find. + assert dtmodel.size == len(dtmodel.root.find(DTNodeCriteria())) + + +def test_dtmodel_init() -> None: + dtmodel = DTShTests.get_sample_dtmodel(force_reload=True) + edt = dtmodel._edt + + assert edt.dts_path == dtmodel.dts.path + assert dtmodel.root.name == "/" + assert dtmodel.root.unit_name == "/" + assert len(edt.nodes) == dtmodel.size == len(dtmodel) + assert edt.bindings_dirs == dtmodel.dts.bindings_search_path + # By convention, root is its own parent. + assert dtmodel.root.parent is dtmodel.root + + # edtlib.EDT -> DTModel isomorphic ? + N = 0 + for node in dtmodel.root.walk(): + N += 1 + edtnode = edt.get_node(node.path) + assert node._edtnode is edtnode + + binding = node.binding + if binding: + edtbinding = edtnode._binding + + if binding.compatible: + assert binding._edtbinding is edtbinding + assert node.compatible == binding.compatible + assert edtnode.matching_compat == binding.compatible + assert binding is dtmodel.get_compatible_binding( + binding.compatible, node.on_bus + ) + else: + assert not edtnode.matching_compat + # This SHOULD be a child-binding without compat string. + assert edtnode.parent + assert edtnode.parent._binding + assert edtnode.parent._binding.child_binding is edtbinding + assert binding.cb_depth > 0 + assert not node.compatible + assert node.parent.binding + assert binding is node.parent.binding.child_binding + + if binding.child_binding: + assert binding.child_binding + assert binding.child_binding.cb_depth > 0 + for child in node.children: + assert binding.child_binding is child.binding + + if node.on_bus: + assert node.binding + assert node.on_bus == node.binding.on_bus + assert node._edtnode.on_buses + + if node.buses: + assert node.binding + assert node.buses == node.binding.buses + assert len(node._edtnode.buses) == len(node.buses) + + for irq in node.interrupts: + # EDT model hypothesis. + assert irq.number != sys.maxsize + assert irq.priority != sys.maxsize + + for reg in edtnode.regs: + # EDT model hypothesis. + assert reg.addr is not None + + assert len(node._edtnode.interrupts) == len(node.interrupts) + assert len(node._edtnode.regs) == len(node.registers) + + # Preserve children order. + assert [ + edt_child.path for edt_child in node._edtnode.children.values() + ] == [child.path for child in node.children] + + if node.path == "/": + # Consume EDT root node: contrary to dtsh (and according to DTSpec), + # EDT does not assume root is its own parent. + continue + assert node.parent._edtnode is edt.get_node(node.path).parent + + assert dtmodel.size == N + + +def test_dtmodel_aliased_nodes() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + edt = dtmodel._edt + + assert [ + (alias, edtnode.path) for (alias, edtnode) in edt._dt.alias2node.items() + ] == [(alias, node.path) for alias, node in dtmodel.aliased_nodes.items()] + + with pytest.raises(KeyError): + dtmodel.aliased_nodes["notanalias"] + + +def test_dtmodel_chosen_nodes() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + edt = dtmodel._edt + + assert [ + (chosen, edtnode.path) for (chosen, edtnode) in edt.chosen_nodes.items() + ] == [(chosen, node.path) for chosen, node in dtmodel.chosen_nodes.items()] + + with pytest.raises(KeyError): + dtmodel.chosen_nodes["notachosen"] + + +def test_dtmodel_labeled_nodes() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + edt = dtmodel._edt + + labeled_nodes: List[Tuple[str, DTNode]] = [] + for edtnode, labels in [(node_, node_.labels) for node_ in edt.nodes]: + labeled_nodes.extend((label, dtmodel[edtnode.path]) for label in labels) + + assert labeled_nodes == list(dtmodel.labeled_nodes.items()) + + with pytest.raises(KeyError): + dtmodel.labeled_nodes["notalabel"] + + +def test_dtmodel_bus_protocols() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + edt = dtmodel._edt + + buses: Set[str] = set() + for edtnode in edt.nodes: + buses.update(edtnode.buses) + assert buses == set(dtmodel.bus_protocols) + + +def test_dtmodel_compatible_strings() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + edt = dtmodel._edt + assert list(edt.compat2nodes.keys()) == dtmodel.compatible_strings + + +def test_dtmodel_vendors() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + edt = dtmodel._edt + + unique_vendors = set(edt.compat2vendor.values()) + assert len(unique_vendors) == len(dtmodel.vendors) + assert sorted(unique_vendors) == sorted( + [vendor.name for vendor in dtmodel.vendors] + ) + + +def test_dtmodel_get_compatible_binding() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + edt = dtmodel._edt + + binding = dtmodel.get_compatible_binding("bosch,bme680", "i2c") + assert binding + assert ( + edt.get_node("/soc/i2c@40003000/bme680@76").binding_path == binding.path + ) + + # Lazy-cache. + assert binding is dtmodel.get_compatible_binding("bosch,bme680", "i2c") + + # When the binding does expect a bus of appearance, + # get_compatible_binding() should fail with no bus parameter + # or a non relevant bus parameter. + assert dtmodel.get_compatible_binding("bosch,bme680") is None + assert dtmodel.get_compatible_binding("bosch,bme680", "qspi") is None + + # But, when the binding does not expect a bus of appearance, + # get_compatible_binding() should succeed even if a non relevant + # bus parameter is given. + assert dtmodel.get_compatible_binding("nordic,nrf-pwm") + assert dtmodel.get_compatible_binding( + "nordic,nrf-pwm" + ) == dtmodel.get_compatible_binding("nordic,nrf-pwm", "i2c") + + # get_compatible_binding() should never fault. + assert dtmodel.get_compatible_binding("notavendor,notamodel") is None + assert dtmodel.get_compatible_binding("notavendor") is None + + +def test_dtmodel_get_base_binding() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + + # Core bindings without compatible strings. + assert dtmodel.get_base_binding("base.yaml") + assert dtmodel.get_base_binding("power.yaml") + assert dtmodel.get_base_binding("sensor-device.yaml") + assert dtmodel.get_base_binding("i2c-device.yaml") + + # get_base_binding() won't fail with even more specific bindings + # that define compatible strings. + assert dtmodel.get_base_binding("nordic,nrf-pinctrl.yaml") + assert dtmodel.get_base_binding("bosch,bme680-i2c.yaml") + + with pytest.raises(KeyError): + # Included bindings MUST exist. + dtmodel.get_base_binding("notafile") + + # Lazy-cache. + assert dtmodel.get_base_binding("base.yaml") is dtmodel.get_base_binding( + "base.yaml" + ) + + +def test_dtmodel_get_vendor() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + + vendor = dtmodel.get_vendor("nordic,nrf-swi") + assert vendor + assert "Nordic Semiconductor" == vendor.name + assert "nordic" == vendor.prefix + + # get_vendor() should not fault. + assert dtmodel.get_vendor("notapreifx,notavendor") is None + assert dtmodel.get_vendor("nocomma") is None + + +def test_dtmodel_get_compatible_devices() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + + dt_bme680_i2c = dtmodel["/soc/i2c@40003000/bme680@76"] + dt_bme680_spi = dtmodel["/soc/spi@40004000/bme680@0"] + + assert [dt_bme680_i2c, dt_bme680_spi] == dtmodel.get_compatible_devices( + "bosch,bme680" + ) + + +def test_dtmodel_walk() -> None: + # This unit tests asserts the shortcut to DTModel.root.walk() honors + # all supported parameters, test_dtnode_walk_xxx() for other + # unit tests of the walk() API. + dtmodel = DTShTests.get_sample_dtmodel() + + assert list(dtmodel.root.walk()) == list(dtmodel.walk()) + assert list(dtmodel.root.walk(enabled_only=True)) == list( + dtmodel.walk(enabled_only=True) + ) + assert list(dtmodel.root.walk(fixed_depth=3)) == list( + dtmodel.walk(fixed_depth=3) + ) + + # Natural node order is by DT path name. + order_by = DTNodeSorter() + assert list(dtmodel.root.walk(order_by=order_by)) == list( + dtmodel.walk(order_by=order_by) + ) + assert list(dtmodel.root.walk(order_by=order_by, reverse=True)) == list( + dtmodel.walk(order_by=order_by, reverse=True) + ) + + +def test_dtmodel_find() -> None: + # This unit tests asserts the shortcut to DTModel.root.find() honors + # all supported parameters, test_dtnode_find() for other + # unit tests of the find() API. + dtmodel = DTShTests.get_sample_dtmodel() + + class Criterion(DTNodeCriterion): + NODES = dtmodel["/soc"].children + + def match(self, node: DTNode) -> bool: + return node in Criterion().NODES + + criterion = Criterion() + assert list(dtmodel.root.find(criterion)) == list(dtmodel.find(criterion)) + assert list(dtmodel.root.find(criterion, enabled_only=True)) == list( + dtmodel.find(criterion, enabled_only=True) + ) + + # Natural node order is by DT path name. + order_by = DTNodeSorter() + assert list(dtmodel.root.find(criterion, order_by=order_by)) == list( + dtmodel.find(criterion, order_by=order_by) + ) + assert list( + dtmodel.root.find(criterion, order_by=order_by, reverse=True) + ) == list(dtmodel.find(criterion, order_by=order_by, reverse=True)) + + +def test_dtmodel_index_based() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + + # Membership. + assert "/soc" in dtmodel + assert "notanode" not in dtmodel + + # Index-based access. + assert dtmodel["/soc"] + with pytest.raises(KeyError): + dtmodel["notanode"] + + # Iterate on the model walking through the devicetree. + assert list(node for node in dtmodel) == list(dtmodel.walk()) + + +def test_dtnode_sorter() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + + dt_flashctrl = dtmodel["/soc/flash-controller@4001e000"] + dt_flash0 = dt_flashctrl.get_child("flash@0") + dt_partitions = dt_flash0.get_child("partitions") + sample_nodes = [ + dt_flashctrl, + dt_flash0, + dt_partitions, + *dt_partitions.children, + ] + + # Natural order (by path name). + natural_order = DTNodeSorter() + assert sorted( + sample_nodes, + key=lambda x: x.path, + ) == natural_order.sort(sample_nodes) + assert sorted( + sample_nodes, key=lambda x: x.path, reverse=True + ) == natural_order.sort(sample_nodes, reverse=True) + + class OrderByUnitAddr(DTNodeSorter): + """Oder by e.g. unit addresses.""" + + def split_sortable_unsortable( + self, nodes: Sequence[DTNode] + ) -> Tuple[List[DTNode], List[DTNode]]: + return ( + [node for node in nodes if node.unit_addr is not None], + [node for node in nodes if node.unit_addr is None], + ) + + def weight(self, node: DTNode) -> Any: + return node.unit_addr + + order_by = OrderByUnitAddr() + + sorted_sortable = sorted( + [ + dt_flash0, + *dt_partitions.children, + ], + key=lambda x: x.unit_addr if x.unit_addr is not None else sys.maxsize, + ) + + assert [ + # Nodes sorted by unit address first. + *sorted_sortable, + # Nodes without unit address last, in unchanged relative order. + dt_flashctrl, + dt_partitions, + ] == order_by.sort(sample_nodes) + + sorted_sortable.reverse() + assert [ + # Nodes without unit address first, in reversed relative order. + dt_partitions, + dt_flashctrl, + # Nodes with unit address last, in reverse order. + *sorted_sortable, + ] == order_by.sort(sample_nodes, reverse=True) + + +def test_dtnode_criteria() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + + class CriterionMatchNone(DTNodeCriterion): + def match(self, node: DTNode) -> bool: + return False + + # Empty criteria match all. + assert len(dtmodel) == len(dtmodel.find(DTNodeCriteria())) + + class CriterionMatchAll(DTNodeCriterion): + def match(self, node: DTNode) -> bool: + return True + + assert len(dtmodel) == len(dtmodel.find(CriterionMatchAll())) + assert 0 == len(dtmodel.find(CriterionMatchNone())) + + # Logical conjunction (default): fails on first non matched criterion. + assert 0 == len( + dtmodel.find( + DTNodeCriteria([CriterionMatchNone(), CriterionMatchAll()]) + ) + ) + + # Logical disjunction: criteria will succeed on first match. + assert len(dtmodel) == len( + dtmodel.find( + DTNodeCriteria( + [CriterionMatchNone(), CriterionMatchAll()], + ored_chain=True, + ), + ) + ) + + # Logical negation. + assert not dtmodel.find(DTNodeCriteria(negative_chain=True)) + assert not dtmodel.find( + DTNodeCriteria([CriterionMatchAll()], negative_chain=True) + ) diff --git a/tests/test_dtsh_modelutils.py b/tests/test_dtsh_modelutils.py new file mode 100644 index 0000000..985311c --- /dev/null +++ b/tests/test_dtsh_modelutils.py @@ -0,0 +1,1111 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Unit tests for the dtsh.modelutils module.""" + +# Relax pylint a bit for unit tests. +# pylint: disable=too-many-locals +# pylint: disable=too-many-branches +# pylint: disable=missing-function-docstring + + +from typing import List, Tuple, Type +import operator +import re +import sys + +import pytest + +from dtsh.model import DTNode, DTNodeSorter +from dtsh.modelutils import ( + # Text-based criteria. + DTNodeTextCriterion, + DTNodeWithPath, + DTNodeWithName, + DTNodeWithUnitName, + DTNodeWithCompatible, + DTNodeWithBinding, + DTNodeWithVendor, + DTNodeWithDeviceLabel, + DTNodeWithNodeLabel, + DTNodeWithAlias, + DTNodeWithChosen, + DTNodeAlsoKnownAs, + DTNodeWithBus, + DTNodeWithOnBus, + DTNodeWithDescription, + # Integer-based criteria. + DTNodeIntCriterion, + DTNodeWithUnitAddr, + DTNodeWithIrqNumber, + DTNodeWithIrqPriority, + DTNodeWithRegAddr, + DTNodeWithRegSize, + DTNodeWithBindingDepth, + # Sorters. + DTNodeSortByPathName, + DTNodeSortByNodeName, + DTNodeSortByUnitName, + DTNodeSortByUnitAddr, + DTNodeSortByCompatible, + DTNodeSortByBinding, + DTNodeSortByVendor, + DTNodeSortByDeviceLabel, + DTNodeSortByNodeLabel, + DTNodeSortByAlias, + DTNodeSortByBus, + DTNodeSortByOnBus, + DTNodeSortByDepOrdinal, + DTNodeSortByIrqNumber, + DTNodeSortByIrqPriority, + DTNodeSortByRegAddr, + DTNodeSortByRegSize, + DTNodeSortByBindingDepth, + # Virtual trees. + DTWalkableComb, +) + +from .dtsh_uthelpers import DTShTests + + +def test_dtnode_sorters() -> None: + # Crash-test all sorters. + dtmodel = DTShTests.get_sample_dtmodel() + nodes = list(dtmodel) + + sorters: List[DTNodeSorter] = [ + DTNodeSortByPathName(), + DTNodeSortByNodeName(), + DTNodeSortByUnitName(), + DTNodeSortByUnitAddr(), + DTNodeSortByCompatible(), + DTNodeSortByBinding(), + DTNodeSortByVendor(), + DTNodeSortByDeviceLabel(), + DTNodeSortByNodeLabel(), + DTNodeSortByAlias(), + DTNodeSortByBus(), + DTNodeSortByOnBus(), + DTNodeSortByDepOrdinal(), + DTNodeSortByIrqNumber(), + DTNodeSortByIrqPriority(), + DTNodeSortByRegAddr(), + DTNodeSortByRegSize(), + DTNodeSortByBindingDepth(), + ] + for sorter in sorters: + sorter.sort(nodes) + sorter.sort(nodes, reverse=True) + + +def test_dtnodesorter_by_path_name() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + nodes = list(dtmodel) + sorter = DTNodeSortByPathName() + + assert sorted([node.path for node in dtmodel]) == [ + node.path for node in sorter.sort(nodes) + ] + assert sorted([node.path for node in dtmodel], reverse=True) == [ + node.path for node in sorter.sort(nodes, reverse=True) + ] + + +def test_dtnodesorter_by_node_name() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + nodes = list(dtmodel) + sorter = DTNodeSortByNodeName() + + assert sorted([node.name for node in dtmodel]) == [ + node.name for node in sorter.sort(nodes) + ] + assert sorted([node.name for node in dtmodel], reverse=True) == [ + node.name for node in sorter.sort(nodes, reverse=True) + ] + + +def test_dtnodesorter_by_unit_name() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + nodes = list(dtmodel) + sorter = DTNodeSortByUnitName() + + assert sorted([node.unit_name for node in dtmodel]) == [ + node.unit_name for node in sorter.sort(nodes) + ] + assert sorted([node.unit_name for node in dtmodel], reverse=True) == [ + node.unit_name for node in sorter.sort(nodes, reverse=True) + ] + + +def test_dtnodesorter_by_unit_addr() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + nodes = list(dtmodel) + sorter = DTNodeSortByUnitAddr() + + sortable: List[DTNode] = [] + unsortable: List[DTNode] = [] + for node in nodes: + if node.unit_addr is not None: + sortable.append(node) + else: + unsortable.append(node) + + sorted_sortable = sorted(sortable, key=lambda x: x.unit_addr) # type: ignore + assert [*sorted_sortable, *unsortable] == sorter.sort(nodes) + + sorted_sortable.reverse() + unsortable.reverse() + assert [*unsortable, *sorted_sortable] == sorter.sort(nodes, reverse=True) + + +def test_dtnodesorter_by_compatible() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + nodes = list(dtmodel) + sorter = DTNodeSortByCompatible() + + sortable: List[DTNode] = [] + unsortable: List[DTNode] = [] + for node in nodes: + if node.compatibles: + sortable.append(node) + else: + unsortable.append(node) + + sorted_sortable = sorted(sortable, key=lambda x: min(x.compatibles)) + assert [*sorted_sortable, *unsortable] == sorter.sort(nodes) + + # Note: here sorted(reverse=True) is not granted to answer + # the same order as sorted().reverse(). + sorted_sortable = sorted(sortable, key=lambda x: max(x.compatibles)) + sorted_sortable.reverse() + unsortable.reverse() + + assert [*unsortable, *sorted_sortable] == sorter.sort(nodes, reverse=True) + + +def test_dtnodesorter_by_binding() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + nodes = list(dtmodel) + sorter = DTNodeSortByBinding() + + sortable: List[DTNode] = [] + unsortable: List[DTNode] = [] + for node in nodes: + if node.compatible: + sortable.append(node) + else: + unsortable.append(node) + + sorted_sortable = sorted(sortable, key=lambda x: x.compatible) # type: ignore + assert [*sorted_sortable, *unsortable] == sorter.sort(nodes) + + sorted_sortable.reverse() + unsortable.reverse() + assert [*unsortable, *sorted_sortable] == sorter.sort(nodes, reverse=True) + + +def test_dtnodesorter_by_vendor() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + nodes = list(dtmodel) + sorter = DTNodeSortByVendor() + + sortable: List[DTNode] = [] + unsortable: List[DTNode] = [] + for node in nodes: + if node.vendor: + sortable.append(node) + else: + unsortable.append(node) + + sorted_sortable = sorted(sortable, key=lambda x: x.vendor.name) # type: ignore + assert [*sorted_sortable, *unsortable] == sorter.sort(nodes) + + sorted_sortable.reverse() + unsortable.reverse() + assert [*unsortable, *sorted_sortable] == sorter.sort(nodes, reverse=True) + + +def test_dtnodesorter_by_device_label() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + nodes = list(dtmodel) + sorter = DTNodeSortByDeviceLabel() + + sortable: List[DTNode] = [] + unsortable: List[DTNode] = [] + for node in nodes: + if node.label: + sortable.append(node) + else: + unsortable.append(node) + + sorted_sortable = sorted(sortable, key=lambda x: x.label) # type: ignore + assert [*sorted_sortable, *unsortable] == sorter.sort(nodes) + + sorted_sortable.reverse() + unsortable.reverse() + assert [*unsortable, *sorted_sortable] == sorter.sort(nodes, reverse=True) + + +def test_dtnodesorter_by_node_label() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + nodes = list(dtmodel) + sorter = DTNodeSortByNodeLabel() + + sortable: List[DTNode] = [] + unsortable: List[DTNode] = [] + for node in nodes: + if node.labels: + sortable.append(node) + else: + unsortable.append(node) + + sorted_sortable = sorted(sortable, key=lambda x: min(x.labels)) + assert [*sorted_sortable, *unsortable] == sorter.sort(nodes) + + # Note: here sorted(reverse=True) is not granted to answer + # the same order as sorted().reverse(). + sorted_sortable = sorted(sortable, key=lambda x: max(x.labels)) + sorted_sortable.reverse() + unsortable.reverse() + assert [*unsortable, *sorted_sortable] == sorter.sort(nodes, reverse=True) + + +def test_dtnodesorter_by_alias() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + nodes = list(dtmodel) + sorter = DTNodeSortByAlias() + + sortable: List[DTNode] = [] + unsortable: List[DTNode] = [] + for node in nodes: + if node.aliases: + sortable.append(node) + else: + unsortable.append(node) + + sorted_sortable = sorted(sortable, key=lambda x: min(x.aliases)) + assert [*sorted_sortable, *unsortable] == sorter.sort(nodes) + + # Note: here sorted(reverse=True) is not granted to answer + # the same order as sorted().reverse(). + sorted_sortable = sorted(sortable, key=lambda x: max(x.aliases)) + sorted_sortable.reverse() + unsortable.reverse() + assert [*unsortable, *sorted_sortable] == sorter.sort(nodes, reverse=True) + + +def test_dtnodesorter_by_bus() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + nodes = list(dtmodel) + sorter = DTNodeSortByBus() + + sortable: List[DTNode] = [] + unsortable: List[DTNode] = [] + for node in nodes: + if node.buses: + sortable.append(node) + else: + unsortable.append(node) + + sorted_sortable = sorted(sortable, key=lambda x: min(x.buses)) + assert [*sorted_sortable, *unsortable] == sorter.sort(nodes) + + # Note: here sorted(reverse=True) is not granted to answer + # the same order as sorted().reverse(). + sorted_sortable = sorted(sortable, key=lambda x: max(x.buses)) + sorted_sortable.reverse() + unsortable.reverse() + assert [*unsortable, *sorted_sortable] == sorter.sort(nodes, reverse=True) + + +def test_dtnodesorter_by_on_bus() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + nodes = list(dtmodel) + sorter = DTNodeSortByOnBus() + + sortable: List[DTNode] = [] + unsortable: List[DTNode] = [] + for node in nodes: + if node.on_bus: + sortable.append(node) + else: + unsortable.append(node) + + sorted_sortable = sorted(sortable, key=lambda x: x.on_bus) # type: ignore + assert [*sorted_sortable, *unsortable] == sorter.sort(nodes) + + sorted_sortable.reverse() + unsortable.reverse() + assert [*unsortable, *sorted_sortable] == sorter.sort(nodes, reverse=True) + + +def test_dtnodesorter_by_dep_ordinal() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + nodes = list(dtmodel) + sorter = DTNodeSortByDepOrdinal() + + assert sorted([node.dep_ordinal for node in dtmodel]) == [ + node.dep_ordinal for node in sorter.sort(nodes) + ] + assert sorted([node.dep_ordinal for node in dtmodel], reverse=True) == [ + node.dep_ordinal for node in sorter.sort(nodes, reverse=True) + ] + + +def test_dtnodesorter_by_irq_number() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + nodes = list(dtmodel) + sorter = DTNodeSortByIrqNumber() + + sortable: List[DTNode] = [] + unsortable: List[DTNode] = [] + for node in nodes: + if node.interrupts: + sortable.append(node) + else: + unsortable.append(node) + + sorted_sortable = sorted( + sortable, key=lambda node: min(irq.number for irq in node.interrupts) + ) + assert [*sorted_sortable, *unsortable] == sorter.sort(nodes) + + # Note: here sorted(reverse=True) is not granted to answer + # the same order as sorted().reverse(). + sorted_sortable = sorted( + sortable, key=lambda node: max(irq.number for irq in node.interrupts) + ) + sorted_sortable.reverse() + unsortable.reverse() + assert [*unsortable, *sorted_sortable] == sorter.sort(nodes, reverse=True) + + +def test_dtnodesorter_by_irq_priority() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + nodes = list(dtmodel) + sorter = DTNodeSortByIrqPriority() + + sortable: List[DTNode] = [] + unsortable: List[DTNode] = [] + for node in nodes: + if node.interrupts: + sortable.append(node) + else: + unsortable.append(node) + + sorted_sortable = sorted( + sortable, + key=lambda node: min( + irq.priority if irq.priority is not None else sys.maxsize + for irq in node.interrupts + ), + ) + assert [*sorted_sortable, *unsortable] == sorter.sort(nodes) + + # Note: here sorted(reverse=True) is not granted to answer + # the same order as sorted().reverse(). + sorted_sortable = sorted( + sortable, + key=lambda node: max( + irq.priority if irq.priority is not None else sys.maxsize + for irq in node.interrupts + ), + ) + sorted_sortable.reverse() + unsortable.reverse() + assert [*unsortable, *sorted_sortable] == sorter.sort(nodes, reverse=True) + + +def test_dtnodesorter_by_reg_addr() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + nodes = list(dtmodel) + sorter = DTNodeSortByRegAddr() + + sortable: List[DTNode] = [] + unsortable: List[DTNode] = [] + for node in nodes: + if node.registers: + sortable.append(node) + else: + unsortable.append(node) + + sorted_sortable = sorted( + sortable, key=lambda x: min(reg.address for reg in x.registers) + ) + assert [*sorted_sortable, *unsortable] == sorter.sort(nodes) + + # Note: here sorted(reverse=True) is not granted to answer + # the same order as sorted().reverse(). + sorted_sortable = sorted( + sortable, key=lambda x: max(reg.address for reg in x.registers) + ) + sorted_sortable.reverse() + unsortable.reverse() + assert [*unsortable, *sorted_sortable] == sorter.sort(nodes, reverse=True) + + +def test_dtnodesorter_by_reg_size() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + nodes = list(dtmodel) + sorter = DTNodeSortByRegSize() + + sortable: List[DTNode] = [] + unsortable: List[DTNode] = [] + for node in nodes: + if node.registers: + sortable.append(node) + else: + unsortable.append(node) + + sorted_sortable = sorted( + sortable, key=lambda x: min(reg.size for reg in x.registers) + ) + assert [*sorted_sortable, *unsortable] == sorter.sort(nodes) + + # Note: here sorted(reverse=True) is not granted to answer + # the same order as sorted().reverse(). + sorted_sortable = sorted( + sortable, key=lambda x: max(reg.size for reg in x.registers) + ) + sorted_sortable.reverse() + unsortable.reverse() + assert [*unsortable, *sorted_sortable] == sorter.sort(nodes, reverse=True) + + +def test_dtnodesorter_by_cb_depth() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + nodes = list(dtmodel) + sorter = DTNodeSortByBindingDepth() + + sortable: List[DTNode] = [] + unsortable: List[DTNode] = [] + for node in nodes: + if node.binding: + sortable.append(node) + else: + unsortable.append(node) + + sorted_sortable = sorted(sortable, key=lambda x: x.binding.cb_depth) # type: ignore + assert [*sorted_sortable, *unsortable] == sorter.sort(nodes) + + sorted_sortable.reverse() + unsortable.reverse() + assert [*unsortable, *sorted_sortable] == sorter.sort(nodes, reverse=True) + + +def test_dtnodecriterion_text_pattern() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + nodes = list(dtmodel) + + # Criteria with the attributes they depend on. + cls_criteria: List[Tuple[Type[DTNodeTextCriterion], str]] = [ + (DTNodeWithPath, "path"), + (DTNodeWithName, "name"), + (DTNodeWithUnitName, "unit_name"), + (DTNodeWithCompatible, "compatibles"), + (DTNodeWithBinding, "binding"), + (DTNodeWithVendor, "vendor"), + (DTNodeWithDeviceLabel, "label"), + (DTNodeWithNodeLabel, "labels"), + (DTNodeWithAlias, "aliases"), + (DTNodeWithChosen, "chosen"), + (DTNodeWithBus, "buses"), + (DTNodeWithOnBus, "on_bus"), + (DTNodeWithDescription, "description"), + ] + + criterion_by_attr: List[Tuple[DTNodeTextCriterion, str]] = [ + (cls_criterion("*"), attr) for cls_criterion, attr in cls_criteria + ] + criterion_by_attr_re: List[Tuple[DTNodeTextCriterion, str]] = [ + (cls_criterion(".*", re_strict=True), attr) + for cls_criterion, attr in cls_criteria + ] + + with_any_label_or_alias = DTNodeAlsoKnownAs("*") + re_any_label_or_alias = DTNodeAlsoKnownAs(".*", re_strict=True) + + # Match nodes for which the attribute has a value. + for node in nodes: + for criterion, attr in criterion_by_attr: + if getattr(node, attr): + assert criterion.match(node) + else: + assert not criterion.match(node) + + for criterion, attr in criterion_by_attr_re: + if getattr(node, attr): + assert criterion.match(node) + else: + assert not criterion.match(node) + + if node.aliases or node.label or node.labels: + assert with_any_label_or_alias.match(node) + assert re_any_label_or_alias.match(node) + else: + assert not with_any_label_or_alias.match(node) + assert not re_any_label_or_alias.match(node) + + for cls_criterion in [cls for cls, _ in cls_criteria]: + with pytest.raises(re.error): + # RE strict: "*" is a repeat qualifier, not a wild-card, + # and there's nothing to repeat. + cls_criterion("*", re_strict=True) + + +def test_dtnodecriterion_with_path() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + node = dtmodel["/soc/flash-controller@4001e000"] + + # Plain text search. + assert DTNodeWithPath("soc").match(node) + assert DTNodeWithPath("controller").match(node) + assert not DTNodeWithPath("Flash").match(node) + assert DTNodeWithPath("Flash", ignore_case=True).match(node) + + # Wild-card substitution. + assert DTNodeWithPath("*@4001e000").match(node) + assert not DTNodeWithPath("/Soc*").match(node) + assert DTNodeWithPath("/Soc*", ignore_case=True).match(node) + + # Strict RE. + assert not DTNodeWithPath(".*Controller.*", re_strict=True).match(node) + assert DTNodeWithPath( + ".*Controller.*", re_strict=True, ignore_case=True + ).match(node) + + +def test_dtnodecriterion_with_name() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + node = dtmodel["/soc/flash-controller@4001e000"] + + # Plain text search. + assert not DTNodeWithName("soc").match(node) + assert DTNodeWithName("controller").match(node) + assert not DTNodeWithName("Flash").match(node) + assert DTNodeWithName("Flash", ignore_case=True).match(node) + + # Wild-card substitution. + assert DTNodeWithName("*@4001e000").match(node) + assert not DTNodeWithName("Flash*").match(node) + assert DTNodeWithName("Flash*", ignore_case=True).match(node) + + # Strict RE. + assert not DTNodeWithName(".*Controller.*", re_strict=True).match(node) + assert DTNodeWithName( + ".*Controller.*", re_strict=True, ignore_case=True + ).match(node) + + +def test_dtnodecriterion_with_unit_name() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + node = dtmodel["/soc/flash-controller@4001e000"] + + # Plain text search. + assert DTNodeWithUnitName("controller").match(node) + assert not DTNodeWithUnitName("Flash").match(node) + assert DTNodeWithUnitName("Flash", ignore_case=True).match(node) + + # Wild-card substitution. + assert DTNodeWithUnitName("*controller").match(node) + assert not DTNodeWithUnitName("Flash*").match(node) + assert DTNodeWithUnitName("Flash*", ignore_case=True).match(node) + + # Strict RE. + assert not DTNodeWithUnitName(".*Controller.*", re_strict=True).match(node) + assert DTNodeWithUnitName( + ".*Controller.*", re_strict=True, ignore_case=True + ).match(node) + + +def test_dtnodecriterion_with_compatible() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + node = dtmodel["/soc/egu@40014000"] + assert ["nordic,nrf-egu", "nordic,nrf-swi"] == node.compatibles + + # Plain text search. + assert DTNodeWithCompatible("egu").match(node) + assert DTNodeWithCompatible("swi").match(node) + assert not DTNodeWithCompatible("nRF").match(node) + assert DTNodeWithCompatible("nRF", ignore_case=True).match(node) + + # Wild-card substitution. + assert DTNodeWithCompatible("*egu").match(node) + assert DTNodeWithCompatible("*swi").match(node) + assert not DTNodeWithCompatible("Nordic*").match(node) + assert DTNodeWithCompatible("Nordic*", ignore_case=True).match(node) + + # Strict RE. + assert DTNodeWithCompatible(".*egu", re_strict=True).match(node) + assert DTNodeWithCompatible(".*swi", re_strict=True).match(node) + assert not DTNodeWithCompatible(".*nRF.*", re_strict=True).match(node) + assert DTNodeWithCompatible( + ".*nRF.*", re_strict=True, ignore_case=True + ).match(node) + + +def test_dtnodecriterion_with_binding() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + node = dtmodel["/soc/i2c@40003000/bme680@76"] + assert node.binding + assert "bosch,bme680" == node.binding.compatible + assert ( + "The BME680 is an integrated environmental sensor that measures" + == node.binding.get_headline() + ) + + # Plain text search. + assert DTNodeWithBinding("bme").match(node) + assert not DTNodeWithBinding("Environmental").match(node) + assert DTNodeWithBinding("Environmental", ignore_case=True).match(node) + + # Wild-card substitution. + assert DTNodeWithBinding("bosch*").match(node) + assert DTNodeWithBinding("*bme680").match(node) + assert not DTNodeWithBinding("*Environmental*").match(node) + assert DTNodeWithBinding("*Environmental*", ignore_case=True).match(node) + + # Strict RE. + assert DTNodeWithBinding("bosch.*", re_strict=True).match(node) + assert DTNodeWithBinding(".*bme680", re_strict=True).match(node) + assert not DTNodeWithBinding(".*Sensor.*", re_strict=True).match(node) + assert DTNodeWithBinding( + ".*Sensor.*", re_strict=True, ignore_case=True + ).match(node) + + +def test_dtnodecriterion_with_vendor() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + node = dtmodel["/soc/i2c@40003000/bme680@76"] + assert node.vendor + assert "bosch" == node.vendor.prefix + assert "Bosch Sensortec GmbH" == node.vendor.name + + # Plain text search. + assert DTNodeWithVendor("bosch").match(node) + assert not DTNodeWithVendor("sensortec").match(node) + assert DTNodeWithVendor("sensortec", ignore_case=True).match(node) + + # Wild-card substitution. + assert DTNodeWithVendor("Bosch*").match(node) + assert not DTNodeWithVendor("*gmbh").match(node) + assert DTNodeWithVendor("*gmbh", ignore_case=True).match(node) + + # Strict RE. + assert DTNodeWithVendor("bosch.*", re_strict=True).match(node) + assert not DTNodeWithVendor(".*sensor.*", re_strict=True).match(node) + assert DTNodeWithVendor( + ".*sensor.*", re_strict=True, ignore_case=True + ).match(node) + + +def test_dtnodecriterion_with_device_label() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + node = dtmodel["/leds/led_0"] + assert "Green LED 0" == node.label + + # Plain text search. + assert DTNodeWithDeviceLabel("LED").match(node) + assert not DTNodeWithDeviceLabel("green").match(node) + assert DTNodeWithDeviceLabel("green", ignore_case=True).match(node) + + # Wild-card substitution. + assert DTNodeWithDeviceLabel("*0").match(node) + assert not DTNodeWithDeviceLabel("*led*").match(node) + assert DTNodeWithDeviceLabel("*led*", ignore_case=True).match(node) + + # Strict RE. + assert DTNodeWithDeviceLabel("Green.*", re_strict=True).match(node) + assert not DTNodeWithDeviceLabel(".*led.*", re_strict=True).match(node) + assert DTNodeWithDeviceLabel( + ".*led.*", re_strict=True, ignore_case=True + ).match(node) + + +def test_dtnodecriterion_with_node_label() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + node = dtmodel["/soc/i2c@40003000"] + assert ["i2c0", "arduino_i2c"] == node.labels + + # Plain text search. + assert DTNodeWithNodeLabel("i2c").match(node) + assert not DTNodeWithNodeLabel("Arduino").match(node) + assert DTNodeWithNodeLabel("Arduino", ignore_case=True).match(node) + + # Wild-card substitution. + assert DTNodeWithNodeLabel("*i2c*").match(node) + assert not DTNodeWithNodeLabel("Arduino*").match(node) + assert DTNodeWithNodeLabel("Arduino*", ignore_case=True).match(node) + + # Strict RE. + assert DTNodeWithNodeLabel(".*i2c.*", re_strict=True).match(node) + assert not DTNodeWithNodeLabel("Arduino.*", re_strict=True).match(node) + assert DTNodeWithNodeLabel( + "Arduino.*", re_strict=True, ignore_case=True + ).match(node) + + +def test_dtnodecriterion_with_alias() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + node = dtmodel["/leds/led_0"] + assert ["led0", "bootloader-led0", "mcuboot-led0"] == node.aliases + + # Plain text search. + assert DTNodeWithAlias("led").match(node) + assert DTNodeWithAlias("boot").match(node) + assert not DTNodeWithAlias("MCU").match(node) + assert DTNodeWithAlias("MCU", ignore_case=True).match(node) + + # Wild-card substitution. + assert DTNodeWithAlias("led*").match(node) + assert DTNodeWithAlias("boot*").match(node) + assert not DTNodeWithAlias("MCU*").match(node) + assert DTNodeWithAlias("MCU*", ignore_case=True).match(node) + + # Strict RE. + assert DTNodeWithAlias("led.*", re_strict=True).match(node) + assert DTNodeWithAlias("boot.*", re_strict=True).match(node) + assert not DTNodeWithAlias("MCU.*", re_strict=True).match(node) + assert DTNodeWithAlias("MCU.*", re_strict=True, ignore_case=True).match( + node + ) + + +def test_dtnodecriterion_with_chosen() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + node = dtmodel["/soc/random@4000d000"] + assert ["zephyr,entropy"] == node.chosen + + # Plain text search. + assert DTNodeWithChosen("entropy").match(node) + assert not DTNodeWithChosen("Zephyr").match(node) + assert DTNodeWithChosen("Zephyr", ignore_case=True).match(node) + + # Wild-card substitution. + assert DTNodeWithChosen("*entropy").match(node) + assert not DTNodeWithChosen("Zephyr*").match(node) + assert DTNodeWithChosen("Zephyr*", ignore_case=True).match(node) + + # Strict RE. + assert DTNodeWithChosen(".*entropy", re_strict=True).match(node) + assert not DTNodeWithChosen("Zephyr.*", re_strict=True).match(node) + assert DTNodeWithChosen("Zephyr.*", re_strict=True, ignore_case=True).match( + node + ) + + +def test_dtnodecriterion_with_bus() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + node = dtmodel["/soc/i2c@40003000"] + assert ["i2c"] == node.buses + + # Plain text search. + assert DTNodeWithBus("i2c").match(node) + assert not DTNodeWithBus("I2C").match(node) + assert DTNodeWithBus("I2C", ignore_case=True).match(node) + + # Wild-card substitution. + assert DTNodeWithBus("i*c").match(node) + assert not DTNodeWithBus("I*C").match(node) + assert DTNodeWithBus("I*C", ignore_case=True).match(node) + + # Strict RE. + assert DTNodeWithBus(r"i[\d]c", re_strict=True).match(node) + assert not DTNodeWithBus(r"I[\d]C", re_strict=True).match(node) + assert DTNodeWithBus(r"I[\d]C", re_strict=True, ignore_case=True).match( + node + ) + + +def test_dtnodecriterion_with_on_bus() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + node = dtmodel["/soc/i2c@40003000/bme680@76"] + assert "i2c" == node.on_bus + + # Plain text search. + assert DTNodeWithOnBus("i2c").match(node) + assert not DTNodeWithOnBus("I2C").match(node) + assert DTNodeWithOnBus("I2C", ignore_case=True).match(node) + + # Wild-card substitution. + assert DTNodeWithOnBus("i*c").match(node) + assert not DTNodeWithOnBus("I*C").match(node) + assert DTNodeWithOnBus("I*C", ignore_case=True).match(node) + + # Strict RE. + assert DTNodeWithOnBus(r"i[\d]c", re_strict=True).match(node) + assert not DTNodeWithOnBus(r"I[\d]C", re_strict=True).match(node) + assert DTNodeWithOnBus(r"I[\d]C", re_strict=True, ignore_case=True).match( + node + ) + + +def test_dtnodecriterion_with_desc() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + node = dtmodel["/soc/i2c@40003000/bme680@76"] + assert node.binding + assert ( + "The BME680 is an integrated environmental sensor that measures" + == node.binding.get_headline() + ) + + # Plain text search. + assert not DTNodeWithDescription("bme").match(node) + assert DTNodeWithDescription("bme", ignore_case=True).match(node) + + # Wild-card substitution. + assert not DTNodeWithDescription("*Environmental*").match(node) + assert DTNodeWithDescription("*Environmental*", ignore_case=True).match( + node + ) + + # Strict RE. + assert not DTNodeWithDescription(".*Sensor.*", re_strict=True).match(node) + assert DTNodeWithDescription( + ".*Sensor.*", re_strict=True, ignore_case=True + ).match(node) + + +def test_dtnodecriterion_with_aka() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + node = dtmodel["/leds/led_0"] + assert "Green LED 0" == node.label + assert ["led0"] == node.labels + assert ["led0", "bootloader-led0", "mcuboot-led0"] == node.aliases + + # Plain text search. + assert DTNodeAlsoKnownAs("LED").match(node) + assert DTNodeAlsoKnownAs("boot").match(node) + assert not DTNodeAlsoKnownAs("MCU").match(node) + assert DTNodeAlsoKnownAs("MCU", ignore_case=True).match(node) + + # Wild-card substitution. + assert DTNodeAlsoKnownAs("*LED*").match(node) + assert DTNodeAlsoKnownAs("boot*").match(node) + assert not DTNodeAlsoKnownAs("MCU*").match(node) + assert DTNodeAlsoKnownAs("MCU*", ignore_case=True).match(node) + + # Strict RE. + assert DTNodeAlsoKnownAs(".*LED.*", re_strict=True).match(node) + assert DTNodeAlsoKnownAs("boot*", re_strict=True).match(node) + assert not DTNodeAlsoKnownAs("MCU*", re_strict=True).match(node) + assert DTNodeAlsoKnownAs("MCU*", re_strict=True, ignore_case=True).match( + node + ) + + +def test_dtnodecriterion_integer_expr() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + nodes = list(dtmodel) + + # Criteria with the attributes they depend on. + # Would be "*" command argument value. + criterion_by_attr: List[Tuple[DTNodeIntCriterion, str]] = [ + (DTNodeWithUnitAddr(None, None), "unit_addr"), + (DTNodeWithIrqNumber(None, None), "interrupts"), + (DTNodeWithIrqPriority(None, None), "interrupts"), + (DTNodeWithRegAddr(None, None), "registers"), + (DTNodeWithRegSize(None, None), "registers"), + (DTNodeWithBindingDepth(None, None), "binding"), + ] + + # Match nodes for which the attribute has a value. + for node in nodes: + for criterion, attr in criterion_by_attr: + attval = getattr(node, attr) + if (attval is not None) and (attval != []): + assert criterion.match(node) + else: + assert not criterion.match(node) + + +def test_dtnodecriterion_with_unit_addr() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + node = dtmodel["/soc/timer@4000a000"] + assert 0x4000A000 == node.unit_addr + + # Strict equality without operator. + assert DTNodeWithUnitAddr(None, 0x4000A000).match(node) + + # Comparison operators. + assert not DTNodeWithUnitAddr(operator.lt, 0x4000A000).match(node) + assert not DTNodeWithUnitAddr(operator.gt, 0x4000A000).match(node) + assert DTNodeWithUnitAddr(operator.eq, 0x4000A000).match(node) + assert DTNodeWithUnitAddr(operator.le, 0x4000A000).match(node) + assert DTNodeWithUnitAddr(operator.ge, 0x4000A000).match(node) + assert not DTNodeWithUnitAddr(operator.ne, 0x4000A000).match(node) + + +def test_dtnodecriterion_with_irq_number() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + node = dtmodel["/soc/timer@4000a000"] + assert node.interrupts + irq_n = node.interrupts[0].number + assert 10 == irq_n + + # Strict equality without operator. + assert DTNodeWithIrqNumber(None, irq_n).match(node) + + # Comparison operators. + assert not DTNodeWithIrqNumber(operator.lt, irq_n).match(node) + assert not DTNodeWithIrqNumber(operator.gt, irq_n).match(node) + assert DTNodeWithIrqNumber(operator.eq, irq_n).match(node) + assert DTNodeWithIrqNumber(operator.le, irq_n).match(node) + assert DTNodeWithIrqNumber(operator.ge, irq_n).match(node) + assert not DTNodeWithIrqNumber(operator.ne, irq_n).match(node) + + +def test_dtnodecriterion_with_irq_priority() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + node = dtmodel["/soc/timer@4000a000"] + assert node.interrupts + irq_prio = node.interrupts[0].priority + assert 1 == irq_prio + + # Strict equality without operator. + assert DTNodeWithIrqPriority(None, irq_prio).match(node) + + # Comparison operators. + assert not DTNodeWithIrqPriority(operator.lt, irq_prio).match(node) + assert not DTNodeWithIrqPriority(operator.gt, irq_prio).match(node) + assert DTNodeWithIrqPriority(operator.eq, irq_prio).match(node) + assert DTNodeWithIrqPriority(operator.le, irq_prio).match(node) + assert DTNodeWithIrqPriority(operator.ge, irq_prio).match(node) + assert not DTNodeWithIrqPriority(operator.ne, irq_prio).match(node) + + +def test_dtnodecriterion_with_reg_addr() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + node = dtmodel["/soc/gpio@50000000"] + assert [0x50000000, 0x50000500] == [reg.address for reg in node.registers] + + # Strict equality without operator. + assert DTNodeWithRegAddr(None, 0x50000000).match(node) + assert DTNodeWithRegAddr(None, 0x50000500).match(node) + + # Comparison operators. + assert not DTNodeWithRegAddr(operator.lt, 0x50000000).match(node) + assert DTNodeWithRegAddr(operator.lt, 0x50000500).match(node) + assert not DTNodeWithRegAddr(operator.gt, 0x50000500).match(node) + assert DTNodeWithRegAddr(operator.gt, 0x50000000).match(node) + assert not DTNodeWithRegAddr(operator.gt, 0x50000500).match(node) + assert DTNodeWithRegAddr(operator.eq, 0x50000000).match(node) + assert DTNodeWithRegAddr(operator.eq, 0x50000500).match(node) + assert DTNodeWithRegAddr(operator.le, 0x50000000).match(node) + assert DTNodeWithRegAddr(operator.ge, 0x50000500).match(node) + # Equality for each address fails because of the other one. + assert DTNodeWithRegAddr(operator.ne, 0x50000000).match(node) + assert DTNodeWithRegAddr(operator.ne, 0x50000500).match(node) + + +def test_dtnode_with_reg_size() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + node = dtmodel["/soc/gpio@50000000"] + assert [512, 768] == [reg.size for reg in node.registers] + + # Strict equality without operator. + assert DTNodeWithRegSize(None, 512).match(node) + assert DTNodeWithRegSize(None, 768).match(node) + + # Comparison operators. + assert not DTNodeWithRegSize(operator.lt, 512).match(node) + assert DTNodeWithRegSize(operator.lt, 768).match(node) + assert DTNodeWithRegSize(operator.gt, 512).match(node) + assert not DTNodeWithRegSize(operator.gt, 768).match(node) + assert DTNodeWithRegSize(operator.eq, 512).match(node) + assert DTNodeWithRegSize(operator.eq, 768).match(node) + assert DTNodeWithRegSize(operator.le, 512).match(node) + assert DTNodeWithRegSize(operator.ge, 768).match(node) + # Equality for each size fails because of the other one. + assert DTNodeWithRegSize(operator.ne, 512).match(node) + assert DTNodeWithRegSize(operator.ne, 768).match(node) + + dtmodel = DTShTests.get_sample_dtmodel() + node = dtmodel["/soc/gpio@50000000"] + + +def test_dtnode_with_cb_depth() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + node = dtmodel["/pin-controller/uart0_default/group1"] + assert node.binding + assert 2 == node.binding.cb_depth + + # Strict equality without operator. + assert DTNodeWithBindingDepth(None, 2).match(node) + + # Comparison operators. + assert not DTNodeWithBindingDepth(operator.lt, 2).match(node) + assert not DTNodeWithBindingDepth(operator.gt, 2).match(node) + assert DTNodeWithBindingDepth(operator.eq, 2).match(node) + assert DTNodeWithBindingDepth(operator.le, 2).match(node) + assert DTNodeWithBindingDepth(operator.ge, 2).match(node) + assert not DTNodeWithBindingDepth(operator.ne, 2).match(node) + + +def test_dtwalkable_comb() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + + walkable = DTWalkableComb(dtmodel.root, []) + N = 0 + for _ in walkable.walk(): + N += 1 + assert 0 == N + + dt_soc = dtmodel["/soc"] + dt_flashctrl = dt_soc.get_child("flash-controller@4001e000") + dt_flash0 = dt_flashctrl.get_child("flash@0") + dt_partitions = dt_flash0.get_child("partitions") + dt_partition0 = dt_partitions.get_child("partition@0") + dt_cpus = dtmodel["/cpus"] + dt_cpu0 = dt_cpus.get_child("cpu@0") + dt_cpu_itm = dt_cpu0.get_child("itm@e0000000") + leaves = [dt_cpu_itm, dt_partition0] + + walkable = DTWalkableComb(dtmodel.root, leaves) + assert [ + dtmodel.root, + dt_soc, + dt_flashctrl, + dt_flash0, + dt_partitions, + dt_partition0, + dt_cpus, + dt_cpu0, + dt_cpu_itm, + ] == list(walkable.walk()) + + walkable = DTWalkableComb(dt_soc, leaves) + assert [ + dt_soc, + dt_flashctrl, + dt_flash0, + dt_partitions, + dt_partition0, + ] == list(walkable.walk()) + + walkable = DTWalkableComb(dtmodel.root, leaves) + order_by = DTNodeSorter() + assert [ + dtmodel.root, + dt_cpus, + dt_cpu0, + dt_cpu_itm, + dt_soc, + dt_flashctrl, + dt_flash0, + dt_partitions, + dt_partition0, + ] == list(walkable.walk(order_by=order_by)) + + assert [ + dtmodel.root, + dt_soc, + dt_flashctrl, + dt_flash0, + dt_partitions, + dt_partition0, + dt_cpus, + dt_cpu0, + dt_cpu_itm, + ] == list(walkable.walk(order_by=order_by, reverse=True)) diff --git a/tests/test_dtsh_rich_shellutils.py b/tests/test_dtsh_rich_shellutils.py new file mode 100644 index 0000000..01944d3 --- /dev/null +++ b/tests/test_dtsh_rich_shellutils.py @@ -0,0 +1,166 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Unit tests for the dtsh.rich.shellutils module.""" + +# Relax pylint a bit for unit tests. +# pylint: disable=missing-function-docstring + + +import pytest + +from dtsh.shell import DTSh, DTShError +from dtsh.shellutils import ( + DTShFlagReverse, + DTShFlagEnabledOnly, + DTShArgOrderBy, + DTSH_NODE_ORDER_BY, +) +from dtsh.rich.shellutils import ( + DTShNodeFmt, + DTShFlagLongList, + DTShArgLongFmt, + DTShCommandLongFmt, + DTSH_NODE_FMT_SPEC, +) +from dtsh.rich.modelview import SketchMV + +from .dtsh_uthelpers import DTShTests + + +NODE_FMT_ALL = "".join(spec for spec in DTSH_NODE_FMT_SPEC) +NODE_COL_ALL = [fmt.col for fmt in DTSH_NODE_FMT_SPEC.values()] + + +def test_dtsh_node_fmt_spc() -> None: + for key, spec in DTSH_NODE_FMT_SPEC.items(): + assert key == spec.key + assert spec.brief + assert spec.col + assert spec.col.header + + +def test_dtsh_node_fmt() -> None: + # Parse all valid format specifiers. + assert NODE_COL_ALL == DTShNodeFmt.parse(NODE_FMT_ALL) + + # Client code expect DTShError on invalid format string. + with pytest.raises(DTShError): + DTShNodeFmt.parse("invalid format string") + + +def test_dtshflag_longlist() -> None: + DTShTests.check_flag(DTShFlagLongList()) + + +def test_dtsharg_longfmt() -> None: + arg = DTShArgLongFmt() + assert not arg.fmt + + DTShTests.check_arg(arg, NODE_FMT_ALL) + assert NODE_COL_ALL == arg.fmt + + +def test_dtsharg_longfmt_autocomp() -> None: + sh = DTSh(DTShTests.get_sample_dtmodel(), []) + arg = DTShArgLongFmt() + + # Auto-complete with all available fields. + autocomp_states = sorted(NODE_FMT_ALL, key=lambda x: x.lower()) + assert autocomp_states == [state.rlstr for state in arg.autocomp("", sh)] + + # Auto-complete with all fields expect those already set. + autocomp_states.remove("n") + autocomp_states.remove("a") + assert autocomp_states == [state.rlstr for state in arg.autocomp("na", sh)] + + # No match + assert [] == arg.autocomp("invalid_fmt", sh) + + +def test_dtsh_command_lonfmt() -> None: + cmd = DTShCommandLongFmt("foo", "", [], None) + # DTShFlagLongList + assert cmd.option("-l") + # DTShArgLongFmt + assert cmd.option("--format") + + assert not cmd.with_flag(DTShFlagLongList) + assert not cmd.with_arg(DTShArgLongFmt).isset + assert not cmd.with_arg(DTShArgLongFmt).fmt + + cmd.parse_argv(["-l"]) + assert cmd.with_flag(DTShFlagLongList) + # Default fields are not set on parsing the command line. + assert not cmd.with_arg(DTShArgLongFmt).fmt + # get_longformat() is intended to be used once the parser has finished, + # and will answer "something" (fallback). + assert 0 < len(cmd.get_longfmt("")) + + # Caller my also provide a default value when calling get_fields(), + # typically using long lists without explicit format. + assert 3 == len(cmd.get_longfmt("nac")) + + # Set all fields in format string. + cmd.parse_argv(["--format", NODE_FMT_ALL]) + assert NODE_COL_ALL == cmd.with_arg(DTShArgLongFmt).fmt + # A format is explicitly provided: default is ignored. + assert NODE_COL_ALL == cmd.get_longfmt("") + assert NODE_COL_ALL == cmd.get_longfmt("pd") + + with pytest.raises(DTShError): + cmd.parse_argv(["--format", "invalid format string"]) + + +def test_dtsh_command_lonfmt_sort() -> None: + flag_reverse = DTShFlagReverse() + arg_orderby = DTShArgOrderBy() + cmd = DTShCommandLongFmt("mock", "", [flag_reverse, arg_orderby], None) + + assert not cmd.flag_reverse + assert not cmd.arg_sorter + + cmd.parse_argv(["--order-by", "p"]) + assert not cmd.flag_reverse + assert cmd.arg_sorter is DTSH_NODE_ORDER_BY["p"].sorter + + dt = DTShTests.get_sample_dtmodel() + nodes = dt["/leds"].children + expect_nodes = list(sorted(nodes)) + assert expect_nodes == cmd.sort(nodes) + + cmd.parse_argv(["-r", "--order-by", "p"]) + assert cmd.flag_reverse + assert cmd.arg_sorter is DTSH_NODE_ORDER_BY["p"].sorter + expect_nodes = list(reversed(expect_nodes)) + assert expect_nodes == cmd.sort(nodes) + + +def test_dtsh_command_lonfmt_prune() -> None: + dt = DTShTests.get_sample_dtmodel() + flag_enabled_only = DTShFlagEnabledOnly() + cmd = DTShCommandLongFmt("foo", "", [flag_enabled_only], None) + + # Enabled. + dt_i2c0 = dt.labeled_nodes["i2c0"] + # Disabled. + dt_i2c1 = dt.labeled_nodes["i2c1"] + nodes = [dt_i2c0, dt_i2c1] + + assert not cmd.flag_enabled_only + assert nodes == cmd.prune(nodes) + + cmd.parse_argv(["--enabled-only"]) + assert cmd.flag_enabled_only + assert [dt_i2c0] == cmd.prune(nodes) + + +def test_dtsh_node_fmt_columns() -> None: + # Crash-test: we should be able to render all nodes with all columns + # for all layouts. + sh = DTSh(DTShTests.get_sample_dtmodel(), []) + for node in sh.dt: + for col in NODE_COL_ALL: + for layout in SketchMV.Layout: + col.mk_view(node, SketchMV(layout)) diff --git a/tests/test_dtsh_rich_svg.py b/tests/test_dtsh_rich_svg.py new file mode 100644 index 0000000..098b997 --- /dev/null +++ b/tests/test_dtsh_rich_svg.py @@ -0,0 +1,223 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Unit tests for the dtsh.rich.svg module.""" + +# Relax pylint a bit for unit tests. +# pylint: disable=missing-function-docstring + +import pytest + +from dtsh.rich.svg import SVGContentsFmt, SVGContents + + +def test_svgfmt_get_rect_width_height() -> None: + rect: str = '. + SVGContentsFmt.get_rect_width_heigt(' None: + rect = '' + expect_rect = '' + assert expect_rect == SVGContentsFmt.set_rect_width_height(rect, 100, 200) + + +def test_sfgfmt_get_gtranslate_xy() -> None: + gtranslate = '' + expect_gtranslate = ' None: + contents = SVG_FMT_TEST_CONTENTS.splitlines() + svg = SVGContents(contents) + assert 700 == svg.width + assert 390 == svg.height + assert 9 == svg.term_x + assert 41 == svg.term_y + + svg.top_padding_correction() + assert 700 == svg.width + assert 390 - SVGContentsFmt.PAD_TOP_CORRECTION == svg.height + assert 9 == svg.term_x + assert 41 - SVGContentsFmt.PAD_TOP_CORRECTION == svg.term_y + + +def test_svgcontents_append() -> None: + contents = SVG_FMT_TEST_CONTENTS.splitlines() + svg = SVGContents(contents) + svg_styles = list(svg.styles) + svg_defs = list(svg.defs) + svg_width = svg.width + svg_height = svg.height + svg_x = svg.term_x + svg_y = svg.term_y + # Number of contents line. + svg_gterm_size = len(svg.gterm.contents) + + new_contents = SVG_FMT_APPEND_CONTENTS.splitlines() + svgnew = SVGContents(new_contents) + svgnew_y = svgnew.term_y + + svg.append(svgnew) + # The translation is updated upon append(). + assert svgnew_y != svgnew.term_y + + assert len(svg_styles) + len(svgnew.styles) == len(svg.styles) + assert len(svg_defs) + len(svgnew.defs) == len(svg.defs) + + assert max(svg_width, svgnew.width) == svg.width + assert svg_height + svgnew.height == svg.height + + # Old Terminal group unchanged. + assert svg_x == svg.term_x + assert svg_y == svg.term_y + + # New group must be translated verticaly. + assert svgnew.term_x == svg_x == svg.term_x + assert svgnew.term_y == svgnew_y + svg_height + + assert 2 == len(svg.gterms) + assert len(svgnew.gterm.contents) + svg_gterm_size == len( + svg.gterms[0].contents + ) + len(svg.gterms[1].contents) + + +SVG_FMT_TEST_CONTENTS = """\ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" + + +SVG_FMT_APPEND_CONTENTS = """\ + + + + + + + + + + + + + + + + + + + + + +""" diff --git a/tests/test_dtsh_shell.py b/tests/test_dtsh_shell.py new file mode 100644 index 0000000..73089c4 --- /dev/null +++ b/tests/test_dtsh_shell.py @@ -0,0 +1,675 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Unit tests for the dtsh.shell module.""" + +# Relax pylint a bit for unit tests. +# pylint: disable=missing-function-docstring +# pylint: disable=missing-class-docstring +# pylint: disable=too-many-statements +# pylint: disable=protected-access + + +import pytest + +from dtsh.model import DTNode, DTNodeCriterion, DTNodeCriteria +from dtsh.shell import ( + DTSh, + DTShOption, + DTShArg, + DTShCommand, + DTShFlag, + DTShFlagHelp, + DTShParameter, + DTShError, + DTShUsageError, + DTPathNotFoundError, + DTShCommandNotFoundError, +) + +from .dtsh_uthelpers import DTShTests + + +def test_dtshoption() -> None: + class MockOption(DTShOption): + BRIEF = "mock option" + SHORTNAME = "m" + LONGNAME = "mock" + + opt = MockOption() + assert "m" == opt.shortname + assert "mock" == opt.longname + assert "mock option" == opt.brief + + # No suffix expected. + assert "m" == opt.getopt_short + assert "mock" == opt.getopt_long + assert "-m --mock" == opt.usage + + assert not opt.isset + + class MockOptionOther(DTShOption): + SHORTNAME = "m" + LONGNAME = "mock" + + # Same option names. + assert opt == MockOptionOther() + + # Different option names. + MockOptionOther.SHORTNAME = "x" + assert opt != MockOptionOther() + + +def test_dtshflag() -> None: + class MockFlag(DTShFlag): + BRIEF = "mock flag" + SHORTNAME = "m" + LONGNAME = "mock" + + flag = MockFlag() + assert "m" == flag.shortname + assert "mock" == flag.longname + assert "mock flag" == flag.brief + + # No suffix expected. + assert "m" == flag.getopt_short + assert "mock" == flag.getopt_long + assert "-m --mock" == flag.usage + + # State life-cycle. + assert not flag.isset + flag.parsed() + assert flag.isset + flag.reset() + assert not flag.isset + + with pytest.raises(ValueError): + # Flags do not expect values. + flag.parsed("value") + + class MockFlagOther(DTShFlag): + SHORTNAME = "m" + LONGNAME = "mock" + + # Same option names. + assert flag == MockFlagOther() + + # Different option names. + MockFlagOther.SHORTNAME = "x" + assert flag != MockFlagOther() + + +def test_dtsharg() -> None: + class MockArg(DTShArg): + BRIEF = "mock argument" + SHORTNAME = "m" + LONGNAME = "mock" + + def __init__(self) -> None: + super().__init__(argname="arg") + + arg = MockArg() + assert "m" == arg.shortname + assert "mock" == arg.longname + assert "mock argument" == arg.brief + + # Suffix expected. + assert "m:" == arg.getopt_short + assert "mock=" == arg.getopt_long + assert "-m --mock ARG" == arg.usage + + # State life-cycle. + assert not arg.isset + assert arg.raw is None + arg.parsed("value") + assert arg.isset + assert "value" == arg.raw + arg.reset() + assert not arg.isset + assert arg.raw is None + + with pytest.raises(ValueError): + # Arguments expect values. + arg.parsed() + + class MockArgOther(DTShArg): + SHORTNAME = "m" + LONGNAME = "mock" + + def __init__(self) -> None: + super().__init__(argname="arg") + + # Same option names. + assert arg == MockArgOther() + + # Different option names. + MockArgOther.SHORTNAME = "x" + assert arg != MockArgOther() + + +def test_dtshparameter() -> None: + # Optional parameter. + param = DTShParameter("param", "?", "mock param") + assert "mock param" == param.brief + assert "?" == param.multiplicity + assert "[PARAM]" == param.usage + DTShTests.check_param(param) + + # State life-cycle. + param.reset() + assert not param.raw + param.parsed(["value"]) + assert ["value"] == param.raw + param.reset() + assert not param.raw + # Parameter is optional. + param.parsed([]) + assert not param.raw + with pytest.raises(DTShError): + # At most one value. + param.parsed(["value", "value"]) + + # Optional parameter, zero or more values + param = DTShParameter("param", "*", "mock param") + assert "*" == param.multiplicity + assert "[PARAM ...]" == param.usage + DTShTests.check_param(param) + + param.parsed([]) + assert not param.raw + param.parsed(["value"]) + assert ["value"] == param.raw + param.parsed(["value", "value"]) + assert ["value", "value"] == param.raw + + # Required parameter. + param = DTShParameter("param", "+", "mock param") + DTShTests.check_param(param) + + assert "+" == param.multiplicity + assert "PARAM [PARAM ...]" == param.usage + # Allows more than one value. + param.parsed(["value"]) + assert ["value"] == param.raw + param.parsed(["value", "value"]) + + # Required parameter, N values. + param = DTShParameter("param", 2, "mock param") + DTShTests.check_param(param) + + assert 2 == param.multiplicity + assert "PARAM PARAM" == param.usage + # Expects exactly two values. + param.parsed(["value", "value"]) + assert ["value", "value"] == param.raw + with pytest.raises(DTShError): + param.parsed([]) + with pytest.raises(DTShError): + param.parsed(["value"]) + with pytest.raises(DTShError): + param.parsed(["value", "value", "value"]) + + +def test_dtshcommand() -> None: + class MockFlag(DTShFlag): + SHORTNAME = "f" + LONGNAME = "flag" + + class MockArg(DTShArg): + SHORTNAME = "a" + LONGNAME = "argument" + + def __init__(self) -> None: + super().__init__(argname="arg") + + class MockParam(DTShParameter): + def __init__(self) -> None: + super().__init__(name="param", multiplicity="?", brief="mock param") + + class MockCmd(DTShCommand): + _param: MockParam + + def __init__(self) -> None: + super().__init__( + "mock", "mock command", [mock_flag, mock_arg], mock_param + ) + + mock_flag = MockFlag() + mock_arg = MockArg() + mock_param = MockParam() + cmd = MockCmd() + + assert "mock" == cmd.name + assert "mock command" == cmd.brief + assert ( + "mock [-h --help] [-f --flag] [-a --argument ARG] [PARAM]" + == cmd.synopsis + ) + assert "hfa:" == cmd.getopt_short + assert ["help", "flag", "argument="] == cmd.getopt_long + + assert [DTShFlagHelp(), mock_flag, mock_arg] == cmd.options + assert cmd.param is mock_param + + assert mock_flag == cmd.option("-f") == cmd.option("--flag") + assert mock_arg == cmd.option("-a") == cmd.option("--argument") + assert cmd.option("not_an_option") is None + + assert mock_flag is cmd.with_option(MockFlag) + # The flag is not set. + assert not cmd.with_flag(MockFlag) + + assert mock_arg is cmd.with_arg(MockArg) + assert mock_param is cmd.with_param(MockParam) + + class MockFlagInval(DTShFlag): + pass + + with pytest.raises(KeyError): + # Flag must exist. + cmd.with_flag(MockFlagInval) + + class MockArgInval(DTShArg): + pass + + with pytest.raises(KeyError): + # Argument must exist. + cmd.with_arg(MockArgInval) + + # Equality. + assert DTShCommand("mock", "", [], None) == DTShCommand( + "mock", "", [], None + ) + assert DTShCommand("mock", "", [], None) != DTShCommand( + "other", "", [], None + ) + + # Default order. + assert DTShCommand("a", "", [], None) < DTShCommand("b", "", [], None) + + +def test_dtshcommand_parse_argv() -> None: + class MockFlag(DTShFlag): + SHORTNAME = "f" + LONGNAME = "flag" + + class MockArg(DTShArg): + SHORTNAME = "a" + LONGNAME = "argument" + + def __init__(self) -> None: + super().__init__(argname="arg") + + class MockParam(DTShParameter): + def __init__(self) -> None: + super().__init__(name="param", multiplicity="?", brief="mock param") + + cmd = DTShCommand( + "mock", "mock command", [MockFlag(), MockArg()], MockParam() + ) + assert not cmd.with_option(MockFlag).isset + assert not cmd.with_flag(MockFlag) + assert not cmd.with_flag(DTShFlagHelp) + assert not cmd.with_arg(MockArg).isset + assert cmd.with_arg(MockArg).raw is None + assert [] == cmd.with_param(MockParam).raw + + cmd.parse_argv(["-f", "-a", "arg", "param"]) + assert cmd.with_flag(MockFlag) + assert cmd.with_arg(MockArg).isset + assert "arg" == cmd.with_arg(MockArg).raw + assert ["param"] == cmd.with_param(MockParam).raw + + cmd.reset() + assert not cmd.with_flag(MockFlag) + assert not cmd.with_arg(MockArg).isset + assert not cmd.with_param(MockParam).raw + + with pytest.raises(DTShUsageError): + # Undefined option "-x". + cmd.parse_argv(["-x"]) + + cmd = DTShCommand("mock", "mock command", [], None) + with pytest.raises(DTShUsageError): + # Does not expect a parameter. + cmd.parse_argv(["param"]) + + with pytest.raises(DTShUsageError): + # Requested help. + cmd.parse_argv(["-h"]) + assert cmd.with_flag(DTShFlagHelp) + + +def test_dtsh_pathway() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + sh = DTSh(dtmodel, []) + dt_soc = dtmodel["/soc"] + dt_leds = dtmodel["/leds"] + dt_flash0 = dtmodel["/soc/flash-controller@4001e000/flash@0"] + dt_partitions = dt_flash0.get_child("partitions") + + # Working branch is "/": + assert dt_soc.path == sh.pathway(dt_soc, dt_soc.path) + assert dt_soc.name == sh.pathway(dt_soc, dt_soc.name) + assert "soc" == sh.pathway(dt_soc, "") + assert "../soc" == sh.pathway(dt_soc, "..") + assert "../leds" == sh.pathway(dt_leds, "..") + + # Change working branch to "/soc": + sh.cd(dt_soc.path) + assert dt_soc.path == sh.pathway(dt_soc, dt_soc.path) + assert "../soc" == sh.pathway(dt_soc, "..") + assert "../leds" == sh.pathway(dt_leds, "..") + # Nonsense: pathway from "/soc" to "/soc" with a empty path + # representing "/soc" (e.g. "ls" would list the "/soc" children not soc). + assert "" == sh.pathway(dt_soc, "") + + # Change working branch to "/soc/flash-controller@4001e000/flash@0". + sh.cd(dt_flash0.path) + assert dt_partitions.path == sh.pathway(dt_partitions, dt_flash0.path) + assert dt_partitions.name == sh.pathway(dt_partitions, dt_partitions.name) + assert "partitions" == sh.pathway(dt_partitions, "") + assert "../flash@0/partitions" == sh.pathway(dt_partitions, "..") + assert "../flash@0/partitions" == sh.pathway(dt_partitions, "../flash@0") + + with pytest.raises(DTPathNotFoundError): + sh.pathway(dt_partitions, "/invalid/prefix") + + +def test_dtsh_path_expansion() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + dt_leds = dtmodel["/leds"] + dt_pwmleds = dtmodel["/pwmleds"] + sh = DTSh(dtmodel, []) + + assert DTSh.PathExpansion("", [dt_leds, dt_pwmleds]) == sh.path_expansion( + "*ed*" + ) + assert DTSh.PathExpansion("leds", dt_leds.children) == sh.path_expansion( + "leds/*" + ) + + sh.cd(dt_leds.path) + assert DTSh.PathExpansion("", dt_leds.children) == sh.path_expansion("led*") + + assert DTSh.PathExpansion("../leds", dt_leds.children) == sh.path_expansion( + "../leds/led*" + ) + # Empty expansions are errors. + with pytest.raises(DTPathNotFoundError): + sh.path_expansion("pwmled*") + + # Error when failed to resolve the expansion's prefix. + with pytest.raises(DTPathNotFoundError): + sh.path_expansion("/not/a/node") + + +def test_dtsh_realpath() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + dt_flash0 = dtmodel["/soc/flash-controller@4001e000/flash@0"] + dt_partitions = dt_flash0.get_child("partitions") + dt_partition0 = dt_partitions.get_child("partition@0") + sh = DTSh(dtmodel, []) + + assert "/" == sh.realpath("") + assert "/soc" == sh.realpath("soc") + + sh.cd("soc") + assert "/soc" == sh.realpath("") + assert "/soc" == sh.realpath(".") + assert "/" == sh.realpath("..") + + assert dt_flash0.path == sh.realpath("&flash0") + assert dt_partitions.path == sh.realpath("&flash0/partitions") + + sh.cd(dt_partitions.path) + assert dt_partition0.path == sh.realpath("../partitions/partition@0") + + sh.cd() + # Won't fault if realpath is evenutally not a path to a devicetree node. + assert "/node/not/found" == sh.realpath("node/not/found") + + # But will fault when a DT label resolution fails. + with pytest.raises(DTPathNotFoundError): + sh.realpath("&label_not_found") + + +def test_dtsh() -> None: + cmd_ls = DTShCommand("ls", "", [], None) + cmd_tree = DTShCommand("tree", "", [], None) + + dtmodel = DTShTests.get_sample_dtmodel() + sh = DTSh(dtmodel, [cmd_ls, cmd_tree]) + + assert dtmodel is sh.dt + assert dtmodel.root is sh.cwd + assert "/" == sh.pwd + assert [cmd_ls, cmd_tree] == sorted(sh.commands) + assert cmd_ls is sh._commands["ls"] + assert cmd_tree is sh._commands["tree"] + + +def test_dtsh_normpath() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + sh = DTSh(dtmodel, []) + + # Default to current working branch. + assert sh.pwd == sh.realpath("") + sh.cd("/soc") + assert "/soc" == sh.pwd == sh.realpath("") + sh.cd() + assert "/" == sh.pwd == sh.realpath("") + + # Won't fail on undefined node names. + assert "/" == sh.realpath("./a/../") + assert "/a/b" == sh.realpath("a/b/") + + # Resolve devicetree label. + assert "/buttons/button_0/b" == sh.realpath("&button0/b") + + # Fault on undefined label. + with pytest.raises(DTPathNotFoundError) as e: + sh.realpath("¬_a_label/b") + assert "¬_a_label" == e.value.path + + +def test_dtsh_node_at() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + sh = DTSh(dtmodel, []) + + # Default to current working branch. + assert sh.cwd is sh.node_at("") + sh.cd("/soc") + assert dtmodel["/soc"] is sh.cwd is sh.node_at("") + sh.cd() + assert dtmodel.root is sh.cwd is sh.node_at("") + + # Resolve path references. + assert dtmodel.root is sh.node_at("./soc/../") + assert dtmodel["/soc"] == sh.node_at("soc/uicr@10001000/.././") + + # Fault on undefined node names. + with pytest.raises(DTPathNotFoundError) as e: + sh.node_at("not/a/node") + assert "/not/a/node" == e.value.path + + # Resolve devicetree label. + assert dtmodel["/buttons/button_0"] is sh.node_at("&button0") + + # Fault on undefined label. + with pytest.raises(DTPathNotFoundError) as e: + sh.node_at("¬_a_label") + assert "¬_a_label" == e.value.path + + +def test_dtsh_cd() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + sh = DTSh(dtmodel, []) + assert dtmodel.root is sh.cwd + assert "/" == sh.pwd + + # Absolute path. + sh.cd("/soc/i2c@40003000") + assert dtmodel["/soc/i2c@40003000"] is sh.cwd + assert "/soc/i2c@40003000" == sh.pwd + + # Path references. + sh.cd() + assert dtmodel.root is sh.cwd + sh.cd("/./soc/i2c@40003000/..") + assert dtmodel["/soc"] is sh.cwd + + # Relative path. + sh.cd() + sh.cd("soc") + assert dtmodel["/soc"] is sh.cwd + + # Devicetree labels. + sh.cd("&i2c0") + assert dtmodel["/soc/i2c@40003000"] is sh.cwd + + sh.cd() + with pytest.raises(DTPathNotFoundError) as e: + sh.cd("not/a/node") + assert "/not/a/node" == e.value.path + with pytest.raises(DTPathNotFoundError) as e: + sh.cd("¬_a_label/path") + assert "¬_a_label" == e.value.path + + +def test_dtsh_find() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + dt_partitions = dtmodel["/soc/flash-controller@4001e000/flash@0/partitions"] + dt_i2c = dtmodel["/soc/i2c@40003000"] + sh = DTSh(dtmodel, []) + + # An Empty criterion chain matches all nodes: POSIX-like "find". + assert sorted(dtmodel.root.walk()) == sorted( + sh.find("", DTNodeCriteria([])) + ) + + class CriterionBME680(DTNodeCriterion): + def match(self, node: DTNode) -> bool: + return "bme680" == node.unit_name + + # Relative path with wild-cards. + assert [dt_i2c.get_child("bme680@76")] == sh.find( + dt_i2c.path, CriterionBME680() + ) + + # Devicetree labels. + assert [dt_i2c.get_child("bme680@76")] == sh.find( + "&i2c0", CriterionBME680() + ) + + class CriterionPartition(DTNodeCriterion): + def match(self, node: DTNode) -> bool: + return "partition" == node.unit_name + + criterion = CriterionPartition() + assert sorted(dt_partitions.children) == sorted(sh.find("/soc", criterion)) + assert [] == sh.find(dt_i2c.path, criterion) + + # ORed criterion chain. + criteria = DTNodeCriteria( + [CriterionPartition(), CriterionBME680()], ored_chain=True + ) + assert sorted( + [ + dt_i2c.get_child("bme680@76"), + dtmodel["/soc/spi@40004000/bme680@0"], + *dt_partitions.children, + ] + ) == sorted(sh.find("/soc", criteria)) + + # Negate criterion chain. + criteria = DTNodeCriteria([CriterionPartition()], negative_chain=True) + # The unit name "partitions" does not equal to "paratition". + assert [dt_partitions] == sh.find(dt_partitions.path, criteria) + + +def test_dtsh_parse_cmdline() -> None: + class MockFlag(DTShFlag): + SHORTNAME: str = "f" + LONGNAME: str = "flag" + + class MockArg(DTShArg): + SHORTNAME = "a" + LONGNAME = "argument" + + def __init__(self) -> None: + super().__init__(argname="arg") + + class MockParam(DTShParameter): + def __init__(self) -> None: + super().__init__(name="param", multiplicity=0, brief="mock param") + + class MockCommand(DTShCommand): + def __init__(self) -> None: + super().__init__("mock", "", [MockFlag(), MockArg()], MockParam()) + + dtmodel = DTShTests.get_sample_dtmodel() + cmd_mock = MockCommand() + sh = DTSh(dtmodel, [cmd_mock]) + + # Won't parse an empty command line. + with pytest.raises(ValueError): + sh.parse_cmdline("") + + # First lex is always parsed into a command name. + with pytest.raises(DTShCommandNotFoundError) as e: + sh.parse_cmdline(">") + assert ">" == e.value.name + with pytest.raises(DTShCommandNotFoundError) as e: + sh.parse_cmdline(" > ") + assert ">" == e.value.name + with pytest.raises(DTShCommandNotFoundError) as e: + sh.parse_cmdline(" >> ") + assert ">>" == e.value.name + with pytest.raises(DTShCommandNotFoundError) as e: + sh.parse_cmdline(">x") + assert ">x" == e.value.name + with pytest.raises(DTShCommandNotFoundError) as e: + sh.parse_cmdline("> x") + assert ">" == e.value.name + # No space before ">", no redirection. + with pytest.raises(DTShCommandNotFoundError) as e: + sh.parse_cmdline("mock>x") + assert "mock>x" == e.value.name + with pytest.raises(DTShCommandNotFoundError) as e: + sh.parse_cmdline("mock>> x") + assert "mock>>" == e.value.name + + # Command line without redirection. + cmd, cmd_argv, redir2 = sh.parse_cmdline(" mock ") + assert cmd_mock == cmd + assert [] == cmd_argv + assert redir2 is None + + # Command line with empty redirection path. + cmd, cmd_argv, redir2 = sh.parse_cmdline("mock >") + assert cmd_mock == cmd + assert [] == cmd_argv + assert ">" == redir2 + + cmd, cmd_argv, redir2 = sh.parse_cmdline("mock >> ") + assert cmd_mock == cmd + assert [] == cmd_argv + assert ">>" == redir2 + + # Not a redirection, expects a value. + cmd, cmd_argv, redir2 = sh.parse_cmdline("mock --argument >") + assert cmd_mock == cmd + assert ["--argument", ">"] == cmd_argv + assert redir2 is None + # Value provided, parsed redirection. + cmd, cmd_argv, redir2 = sh.parse_cmdline("mock --argument >1 >path") + assert cmd_mock == cmd + assert ["--argument", ">1"] == cmd_argv + assert ">path" == redir2 + # The flag does not expect a value, parsed redirection. + cmd, cmd_argv, redir2 = sh.parse_cmdline("mock --flag >> path") + assert cmd_mock == cmd + assert ["--flag"] == cmd_argv + assert ">> path" == redir2 diff --git a/tests/test_dtsh_shellutils.py b/tests/test_dtsh_shellutils.py new file mode 100644 index 0000000..0f62254 --- /dev/null +++ b/tests/test_dtsh_shellutils.py @@ -0,0 +1,1452 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Unit tests for the dtsh.shellutils module.""" + +# Relax pylint a bit for unit tests. +# pylint: disable=missing-function-docstring +# pylint: disable=missing-class-docstring +# pylint: disable=pointless-statement +# pylint: disable=too-many-statements +# pylint: disable=too-many-statements + +from typing import List, Tuple + +import pytest + +from dtsh.shell import DTSh, DTShCommand, DTShError +from dtsh.modelutils import ( + # Text-based criteria. + DTNodeWithPath, + DTNodeWithName, + DTNodeWithUnitName, + DTNodeWithCompatible, + DTNodeWithBinding, + DTNodeWithVendor, + DTNodeWithDescription, + DTNodeWithBus, + DTNodeWithOnBus, + DTNodeWithDeviceLabel, + DTNodeWithNodeLabel, + DTNodeWithAlias, + DTNodeWithChosen, + DTNodeAlsoKnownAs, +) +from dtsh.shellutils import ( + DTShFlagReverse, + DTShFlagEnabledOnly, + DTShFlagPager, + DTShFlagRegex, + DTShFlagIgnoreCase, + DTShFlagCount, + DTShFlagTreeLike, + DTShFlagNoChildren, + DTShFlagRecursive, + DTShFlagLogicalOr, + DTShFlagLogicalNot, + DTShArgFixedDepth, + # Arguments for text-based criteria. + DTShArgTextCriterion, + DTShArgNodeWithPath, + DTShArgNodeWithStatus, + DTShArgNodeWithName, + DTShArgNodeWithUnitName, + DTShArgNodeWithCompatible, + DTShArgNodeWithBinding, + DTShArgNodeWithVendor, + DTShArgNodeWithDescription, + DTShArgNodeWithBus, + DTShArgNodeWithOnBus, + DTShArgNodeWithDeviceLabel, + DTShArgNodeWithNodeLabel, + DTShArgNodeWithAlias, + DTShArgNodeWithChosen, + DTShArgNodeAlsoKnownAs, + # Arguments for integer-based criteria. + DTShArgIntCriterion, + DTShArgNodeWithUnitAddr, + DTShArgNodeWithIrqNumber, + DTShArgNodeWithIrqPriority, + DTShArgNodeWithRegAddr, + DTShArgNodeWithRegSize, + DTShArgNodeWithBindingDepth, + DTShArgOrderBy, + DTShParamDTPath, + DTShParamDTPaths, + DTShParamAlias, + DTShParamChosen, + DTSH_NODE_ORDER_BY, + DTSH_ARG_NODE_CRITERIA, +) + +from .dtsh_uthelpers import DTShTests + + +def test_dtshflag_reverse() -> None: + flag = DTShFlagReverse() + DTShTests.check_flag(flag) + + +def test_dtshflag_enabled_only() -> None: + flag = DTShFlagEnabledOnly() + DTShTests.check_flag(flag) + + +def test_dtshflag_pager() -> None: + flag = DTShFlagPager() + DTShTests.check_flag(flag) + + +def test_dtshflag_regex() -> None: + flag = DTShFlagRegex() + DTShTests.check_flag(flag) + + +def test_dtshflag_ignore_case() -> None: + flag = DTShFlagIgnoreCase() + DTShTests.check_flag(flag) + + +def test_dtshflag_count() -> None: + flag = DTShFlagCount() + DTShTests.check_flag(flag) + + +def test_dtshflag_tree_like() -> None: + flag = DTShFlagTreeLike() + DTShTests.check_flag(flag) + + +def test_dtshflag_nochildren() -> None: + flag = DTShFlagNoChildren() + DTShTests.check_flag(flag) + + +def test_dtshflag_recursive() -> None: + flag = DTShFlagRecursive() + DTShTests.check_flag(flag) + + +def test_dtshflag_logical_or() -> None: + flag = DTShFlagLogicalOr() + DTShTests.check_flag(flag) + + +def test_dtshflag_logical_not() -> None: + flag = DTShFlagLogicalNot() + DTShTests.check_flag(flag) + + +def test_dtsharg_fixed_depth() -> None: + arg = DTShArgFixedDepth() + # Default value. + assert 0 == arg.depth + + DTShTests.check_arg(arg, parsed="2") + assert 2 == arg.depth + + with pytest.raises(DTShError): + arg.parsed("not a number") + + with pytest.raises(DTShError): + # Negative numbers not allowed. + arg.parsed("-2") + + +def test_dtsharg_criterion_integer_expr() -> None: + # Criteria (args) with the attributes they depend on. + arg_on_attr: List[Tuple[DTShArgIntCriterion, str]] = [ + (DTShArgNodeWithUnitAddr(), "unit_addr"), + (DTShArgNodeWithIrqNumber(), "interrupts"), + (DTShArgNodeWithIrqPriority(), "interrupts"), + (DTShArgNodeWithRegAddr(), "registers"), + (DTShArgNodeWithRegSize(), "registers"), + (DTShArgNodeWithBindingDepth(), "binding"), + ] + + # Check that no one has been forgotten. + assert len( + list( + arg + for arg in DTSH_ARG_NODE_CRITERIA + if isinstance(arg, DTShArgIntCriterion) + ) + ) == len(arg_on_attr) + + for arg, attr in arg_on_attr: + # Check and parse "*": should match all nodes for which + # the attribute has a value. + DTShTests.check_arg(arg, parsed="*") + + criterion = arg.get_criterion() + assert criterion + + for node in DTShTests.get_sample_dtmodel(): + attval = getattr(node, attr) + if (attval is not None) and (attval != []): + assert criterion.match(node) + else: + assert not criterion.match(node) + + with pytest.raises(DTShError): + # Should be an int or "*". + arg.parsed("X") + + with pytest.raises(DTShError): + # Invalid operator "X". + arg.parsed("X 2") + + +def test_dtsharg_with_unit_addr() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + node = dtmodel["/soc/i2c@40003000/bme680@76"] + assert 0x76 == node.unit_addr + arg = DTShArgNodeWithUnitAddr() + + # Without operator, defaults to equality. + arg.parsed("0x76") + criterion = arg.get_criterion() + assert criterion + assert criterion.match(node) + # Test once with another unit address. + assert not criterion.match(dtmodel["/soc/i2c@40003000"]) + + # With comparison operators. + arg.parsed("= 0x76") + assert arg.get_criterion().match(node) # type: ignore + arg.parsed("!= 0x76") + assert not arg.get_criterion().match(node) # type: ignore + arg.parsed(">= 0x76") + assert arg.get_criterion().match(node) # type: ignore + arg.parsed("<= 0x76") + assert arg.get_criterion().match(node) # type: ignore + arg.parsed(">= 0") + assert arg.get_criterion().match(node) # type: ignore + arg.parsed("<= 0") + assert not arg.get_criterion().match(node) # type: ignore + arg.parsed("> 0x76") + assert not arg.get_criterion().match(node) # type: ignore + arg.parsed("< 0x76") + assert not arg.get_criterion().match(node) # type: ignore + + +def test_dtsharg_with_irq_number() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + node = dtmodel["/soc/timer@4000a000"] + assert node.interrupts + 10 == node.interrupts[0].number + arg = DTShArgNodeWithIrqNumber() + + # Without operator, defaults to equality. + arg.parsed("10") + criterion = arg.get_criterion() + assert criterion + assert criterion.match(node) + # Test once with another IRQ number. + assert not criterion.match(dtmodel["/soc/spi@40004000"]) + + # With comparison operators. + arg.parsed("= 10") + assert arg.get_criterion().match(node) # type: ignore + arg.parsed("!= 10") + assert not arg.get_criterion().match(node) # type: ignore + arg.parsed(">= 10") + assert arg.get_criterion().match(node) # type: ignore + arg.parsed("<= 10") + assert arg.get_criterion().match(node) # type: ignore + arg.parsed(">= 0") + assert arg.get_criterion().match(node) # type: ignore + arg.parsed("<= 0") + assert not arg.get_criterion().match(node) # type: ignore + arg.parsed("> 10") + assert not arg.get_criterion().match(node) # type: ignore + arg.parsed("< 10") + assert not arg.get_criterion().match(node) # type: ignore + + +def test_dtsharg_with_irq_priority() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + node = dtmodel["/soc/timer@4000a000"] + assert node.interrupts + assert 1 == node.interrupts[0].priority + arg = DTShArgNodeWithIrqPriority() + + # Without operator, defaults to equality. + arg.parsed("1") + criterion = arg.get_criterion() + assert criterion + assert criterion.match(node) + # Test once with another IRQ priority. + assert not criterion.match(dtmodel["/soc/gpiote@40006000"]) + + # With comparison operators. + arg.parsed("= 1") + assert arg.get_criterion().match(node) # type: ignore + arg.parsed("!= 1") + assert not arg.get_criterion().match(node) # type: ignore + arg.parsed(">= 1") + assert arg.get_criterion().match(node) # type: ignore + arg.parsed("<= 1") + assert arg.get_criterion().match(node) # type: ignore + arg.parsed(">= 0") + assert arg.get_criterion().match(node) # type: ignore + arg.parsed("<= 0") + assert not arg.get_criterion().match(node) # type: ignore + arg.parsed("> 1") + assert not arg.get_criterion().match(node) # type: ignore + arg.parsed("< 1") + assert not arg.get_criterion().match(node) # type: ignore + + +def test_dtsharg_with_reg_addr() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + node = dtmodel["/soc/gpio@50000000"] + assert [0x50000000, 0x50000500] == [reg.address for reg in node.registers] + + arg = DTShArgNodeWithRegAddr() + + # Without operator, defaults to equality. + arg.parsed("0x50000000") + assert arg.get_criterion().match(node) # type: ignore + arg.parsed("0x50000500") + assert arg.get_criterion().match(node) # type: ignore + # Test once with another address. + assert not arg.get_criterion().match(dtmodel["/soc/gpiote@40006000"]) # type: ignore + + # With comparison operators. + arg.parsed("= 0x50000000") + assert arg.get_criterion().match(node) # type: ignore + arg.parsed("= 0x50000500") + assert arg.get_criterion().match(node) # type: ignore + + # Values are mutually exclusive. + arg.parsed("!= 0x50000000") + assert arg.get_criterion().match(node) # type: ignore + arg.parsed("!= 0x50000500") + assert arg.get_criterion().match(node) # type: ignore + + arg.parsed(">= 0x50000000") + assert arg.get_criterion().match(node) # type: ignore + arg.parsed(">= 0x50000500") + assert arg.get_criterion().match(node) # type: ignore + + arg.parsed("<= 0x50000000") + assert arg.get_criterion().match(node) # type: ignore + arg.parsed("<= 0x50000500") + assert arg.get_criterion().match(node) # type: ignore + + arg.parsed("> 0x50000000") + assert arg.get_criterion().match(node) # type: ignore + arg.parsed("> 0x50000500") + assert not arg.get_criterion().match(node) # type: ignore + + arg.parsed("< 0x50000000") + assert not arg.get_criterion().match(node) # type: ignore + arg.parsed("< 0x50000500") + assert arg.get_criterion().match(node) # type: ignore + + +def test_dtsharg_with_reg_size() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + dt_gpio = dtmodel["/soc/gpio@50000300"] + assert [512, 768] == [reg.size for reg in dt_gpio.registers] + dt_clock = dtmodel["/soc/clock@40000000"] + # 4 kB. + assert [0x1000] == [reg.size for reg in dt_clock.registers] + dt_flash = dtmodel["/soc/flash-controller@4001e000/flash@0"] + # 1 MB. + assert [0x100000] == [reg.size for reg in dt_flash.registers] + + arg = DTShArgNodeWithRegSize() + + # Without operator, defaults to equality. + arg.parsed("512") + assert arg.get_criterion().match(dt_gpio) # type: ignore + arg.parsed("768") + assert arg.get_criterion().match(dt_gpio) # type: ignore + arg.parsed("0x1000") + assert arg.get_criterion().match(dt_clock) # type: ignore + arg.parsed("4k") + assert arg.get_criterion().match(dt_clock) # type: ignore + arg.parsed("4kB") + assert arg.get_criterion().match(dt_clock) # type: ignore + arg.parsed("0x100000") + assert arg.get_criterion().match(dt_flash) # type: ignore + arg.parsed("1M") + assert arg.get_criterion().match(dt_flash) # type: ignore + arg.parsed("1MB") + assert arg.get_criterion().match(dt_flash) # type: ignore + + # Equality with comparison operators. + arg.parsed("= 512") + assert arg.get_criterion().match(dt_gpio) # type: ignore + arg.parsed("= 768") + assert arg.get_criterion().match(dt_gpio) # type: ignore + arg.parsed("= 0x1000") + assert arg.get_criterion().match(dt_clock) # type: ignore + arg.parsed("= 4k") + assert arg.get_criterion().match(dt_clock) # type: ignore + arg.parsed("= 4kB") + assert arg.get_criterion().match(dt_clock) # type: ignore + arg.parsed("= 0x100000") + assert arg.get_criterion().match(dt_flash) # type: ignore + arg.parsed("= 1M") + assert arg.get_criterion().match(dt_flash) # type: ignore + arg.parsed("= 1MB") + assert arg.get_criterion().match(dt_flash) # type: ignore + + # Other comparison operators. + arg.parsed(">= 512") + assert arg.get_criterion().match(dt_gpio) # type: ignore + arg.parsed(">= 768") + assert arg.get_criterion().match(dt_gpio) # type: ignore + arg.parsed(">= 0x1000") + assert arg.get_criterion().match(dt_clock) # type: ignore + arg.parsed(">= 4k") + assert arg.get_criterion().match(dt_clock) # type: ignore + arg.parsed(">= 1M") + assert arg.get_criterion().match(dt_flash) # type: ignore + arg.parsed("<= 512") + assert arg.get_criterion().match(dt_gpio) # type: ignore + arg.parsed("<= 768") + assert arg.get_criterion().match(dt_gpio) # type: ignore + arg.parsed("<= 0x1000") + assert arg.get_criterion().match(dt_clock) # type: ignore + arg.parsed("<= 4k") + assert arg.get_criterion().match(dt_clock) # type: ignore + arg.parsed("<= 1M") + assert arg.get_criterion().match(dt_flash) # type: ignore + + arg.parsed("> 512") + assert arg.get_criterion().match(dt_gpio) # type: ignore + arg.parsed("> 768") + assert not arg.get_criterion().match(dt_gpio) # type: ignore + arg.parsed("> 0x1000") + assert not arg.get_criterion().match(dt_clock) # type: ignore + arg.parsed("> 4k") + assert not arg.get_criterion().match(dt_clock) # type: ignore + arg.parsed("> 1M") + assert not arg.get_criterion().match(dt_flash) # type: ignore + + arg.parsed("< 512") + assert not arg.get_criterion().match(dt_gpio) # type: ignore + arg.parsed("< 768") + assert arg.get_criterion().match(dt_gpio) # type: ignore + arg.parsed("< 0x1000") + assert not arg.get_criterion().match(dt_clock) # type: ignore + arg.parsed("< 4k") + assert not arg.get_criterion().match(dt_clock) # type: ignore + arg.parsed("< 1M") + assert not arg.get_criterion().match(dt_flash) # type: ignore + + arg.parsed("!= 0x1000") + assert not arg.get_criterion().match(dt_clock) # type: ignore + arg.parsed("!= 4k") + assert not arg.get_criterion().match(dt_clock) # type: ignore + arg.parsed("!= 1M") + assert not arg.get_criterion().match(dt_flash) # type: ignore + # Values are mutually exclusive. + arg.parsed("!= 512") + assert arg.get_criterion().match(dt_gpio) # type: ignore + arg.parsed("!= 768") + assert arg.get_criterion().match(dt_gpio) # type: ignore + + +def test_dtsharg_criterion_text_pattern() -> None: + # Criteria (args) with the attributes they depend on. + arg_on_attr: List[Tuple[DTShArgTextCriterion, str]] = [ + (DTShArgNodeWithPath(), "path"), + (DTShArgNodeWithStatus(), "status"), + (DTShArgNodeWithName(), "name"), + (DTShArgNodeWithUnitName(), "unit_name"), + (DTShArgNodeWithCompatible(), "compatibles"), + (DTShArgNodeWithBinding(), "binding"), + (DTShArgNodeWithVendor(), "vendor"), + (DTShArgNodeWithDescription(), "description"), + (DTShArgNodeWithBus(), "buses"), + (DTShArgNodeWithOnBus(), "on_bus"), + (DTShArgNodeWithDeviceLabel(), "label"), + (DTShArgNodeWithNodeLabel(), "labels"), + (DTShArgNodeWithAlias(), "aliases"), + (DTShArgNodeWithChosen(), "chosen"), + ] + + # Check that no one has been forgotten. + assert ( + len( + list( + arg + for arg in DTSH_ARG_NODE_CRITERIA + if isinstance(arg, DTShArgTextCriterion) + ) + ) + # DTShArgNodeAlsoKnownAs may be provided by several attributes, + # and is not in our list. + == len(arg_on_attr) + 1 + ) + + for arg, attr in arg_on_attr: + # Check and parse "*": should match all nodes for which + # the attribute has a value. + DTShTests.check_arg(arg, parsed="*") + + criterion = arg.get_criterion() + assert criterion + + for node in DTShTests.get_sample_dtmodel(): + attval = getattr(node, attr) + if (attval is not None) and (attval != []): + assert criterion.match(node) + else: + assert not criterion.match(node) + + with pytest.raises(DTShError): + # RE strict: "*" is a repeat qualifier, not a wild-card, + # and there's nothing to repeat. + arg.get_criterion(re_strict=True) + + +def test_dtsharg_with_path() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + nodes = list(dtmodel) + + # Here we only test arguments passing, + # the criterion itself (DTNodeWithPath) is tested in test_dtsh_modelutils. + arg = DTShArgNodeWithPath() + + pattern = "timer" + re_strict = False + ignore_case = False + + raw_criterion = DTNodeWithPath( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + pattern = "*Timer*" + ignore_case = True + + raw_criterion = DTNodeWithPath( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + pattern = ".*Timer.*" + re_strict = True + + raw_criterion = DTNodeWithPath( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + +def test_dtsharg_with_status() -> None: + dt = DTShTests.get_sample_dtmodel() + # Enabled. + dt_i2c0 = dt.labeled_nodes["i2c0"] + # Disabled. + dt_i2c1 = dt.labeled_nodes["i2c1"] + arg = DTShArgNodeWithStatus() + + arg.parsed("ok") + criterion = arg.get_criterion() + assert criterion + assert criterion.match(dt_i2c0) + assert not criterion.match(dt_i2c1) + + arg.parsed("okay") + criterion = arg.get_criterion() + assert criterion + assert criterion.match(dt_i2c0) + assert not criterion.match(dt_i2c1) + + arg.parsed("dis") + criterion = arg.get_criterion() + assert criterion + assert not criterion.match(dt_i2c0) + assert criterion.match(dt_i2c1) + + +def test_dtsharg_with_name() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + nodes = list(dtmodel) + + # Here we only test arguments passing, + # the criterion itself (DTNodeWithName) is tested in test_dtsh_modelutils. + arg = DTShArgNodeWithName() + + pattern = "timer" + re_strict = False + ignore_case = False + + raw_criterion = DTNodeWithName( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + pattern = "*Timer*" + ignore_case = True + + raw_criterion = DTNodeWithName( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + pattern = ".*Timer.*" + re_strict = True + + raw_criterion = DTNodeWithName( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + +def test_dtsharg_with_unit_name() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + nodes = list(dtmodel) + + # Here we only test arguments passing, + # the criterion itself (DTNodeWithUnitName) is tested in test_dtsh_modelutils. + arg = DTShArgNodeWithUnitName() + + pattern = "timer" + re_strict = False + ignore_case = False + + raw_criterion = DTNodeWithUnitName( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + pattern = "*Timer*" + ignore_case = True + + raw_criterion = DTNodeWithUnitName( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + pattern = ".*Timer.*" + re_strict = True + + raw_criterion = DTNodeWithUnitName( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + +def test_dtsharg_with_compatible() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + nodes = list(dtmodel) + + # Here we only test arguments passing, + # the criterion itself (DTNodeWithCompatible) + # is tested in test_dtsh_modelutils. + arg = DTShArgNodeWithCompatible() + + pattern = "nrf-twi" + re_strict = False + ignore_case = False + + raw_criterion = DTNodeWithCompatible( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + pattern = "*nRF*" + ignore_case = True + + raw_criterion = DTNodeWithCompatible( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + pattern = ".*nRF.*" + re_strict = True + + raw_criterion = DTNodeWithCompatible( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + +def test_dtsharg_with_binding() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + nodes = list(dtmodel) + + # Here we only test arguments passing, + # the criterion itself (DTNodeWithBinding) + # is tested in test_dtsh_modelutils. + arg = DTShArgNodeWithBinding() + + pattern = "bosch" + re_strict = False + ignore_case = False + + raw_criterion = DTNodeWithBinding( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + pattern = "*Sensor*" + ignore_case = True + + raw_criterion = DTNodeWithBinding( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + pattern = ".*Sensor.*" + re_strict = True + + raw_criterion = DTNodeWithBinding( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + +def test_dtsharg_with_vendor() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + nodes = list(dtmodel) + + # Here we only test arguments passing, + # the criterion itself (DTNodeWithVendor) + # is tested in test_dtsh_modelutils. + arg = DTShArgNodeWithVendor() + + pattern = "bosch" + re_strict = False + ignore_case = False + + raw_criterion = DTNodeWithVendor( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + pattern = "*gmbh*" + ignore_case = True + + raw_criterion = DTNodeWithVendor( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + pattern = ".*gmbh.*" + re_strict = True + + raw_criterion = DTNodeWithVendor( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + +def test_dtsharg_with_device_label() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + nodes = list(dtmodel) + + # Here we only test arguments passing, + # the criterion itself (DTNodeWithDeviceLabel) + # is tested in test_dtsh_modelutils. + arg = DTShArgNodeWithDeviceLabel() + + pattern = "led" + re_strict = False + ignore_case = False + + raw_criterion = DTNodeWithDeviceLabel( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + pattern = "*LED*" + ignore_case = True + + raw_criterion = DTNodeWithDeviceLabel( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + pattern = ".*LED.*" + re_strict = True + + raw_criterion = DTNodeWithDeviceLabel( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + +def test_dtsharg_with_node_label() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + nodes = list(dtmodel) + + # Here we only test arguments passing, + # the criterion itself (DTNodeWithNodeLabel) + # is tested in test_dtsh_modelutils. + arg = DTShArgNodeWithNodeLabel() + + pattern = "led" + re_strict = False + ignore_case = False + + raw_criterion = DTNodeWithNodeLabel( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + pattern = "*LED*" + ignore_case = True + + raw_criterion = DTNodeWithNodeLabel( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + pattern = ".*LED.*" + re_strict = True + + raw_criterion = DTNodeWithNodeLabel( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + +def test_dtsharg_with_alias() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + nodes = list(dtmodel) + + # Here we only test arguments passing, + # the criterion itself (DTNodeWithAlias) + # is tested in test_dtsh_modelutils. + arg = DTShArgNodeWithAlias() + + pattern = "i2c" + re_strict = False + ignore_case = False + + raw_criterion = DTNodeWithAlias( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + pattern = "*I2C*" + ignore_case = True + + raw_criterion = DTNodeWithAlias( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + pattern = ".*I2C.*" + re_strict = True + + raw_criterion = DTNodeWithAlias( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + +def test_dtsharg_with_chosen() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + nodes = list(dtmodel) + + # Here we only test arguments passing, + # the criterion itself (DTNodeWithChosen) + # is tested in test_dtsh_modelutils. + arg = DTShArgNodeWithChosen() + + pattern = "entropy" + re_strict = False + ignore_case = False + + raw_criterion = DTNodeWithChosen( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + pattern = "*Zephyr*" + ignore_case = True + + raw_criterion = DTNodeWithChosen( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + pattern = ".*Zephyr.*" + re_strict = True + + raw_criterion = DTNodeWithChosen( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + +def test_dtsharg_with_bus() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + nodes = list(dtmodel) + + # Here we only test arguments passing, + # the criterion itself (DTNodeWithBus) + # is tested in test_dtsh_modelutils. + arg = DTShArgNodeWithBus() + + pattern = "i2c" + re_strict = False + ignore_case = False + + raw_criterion = DTNodeWithBus( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + pattern = "*I2C*" + ignore_case = True + + raw_criterion = DTNodeWithBus( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + pattern = r"I[\d]C" + re_strict = True + + raw_criterion = DTNodeWithBus( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + +def test_dtsharg_with_on_bus() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + nodes = list(dtmodel) + + # Here we only test arguments passing, + # the criterion itself (DTNodeWithOnBus) + # is tested in test_dtsh_modelutils. + arg = DTShArgNodeWithOnBus() + + pattern = "i2c" + re_strict = False + ignore_case = False + + raw_criterion = DTNodeWithOnBus( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + pattern = "*I2C*" + ignore_case = True + + raw_criterion = DTNodeWithOnBus( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + pattern = r"I[\d]C" + re_strict = True + + raw_criterion = DTNodeWithOnBus( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + +def test_dtsharg_with_desc() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + nodes = list(dtmodel) + + # Here we only test arguments passing, + # the criterion itself (DTNodeWithDescription) + # is tested in test_dtsh_modelutils. + arg = DTShArgNodeWithDescription() + + pattern = "BME" + re_strict = False + ignore_case = False + + raw_criterion = DTNodeWithDescription( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + pattern = "*Sensor*" + ignore_case = True + + raw_criterion = DTNodeWithDescription( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + pattern = ".*Sensor.*" + re_strict = True + + raw_criterion = DTNodeWithDescription( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + +def test_dtsharg_with_aka() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + nodes = list(dtmodel) + + # Here we only test arguments passing, + # the criterion itself (DTNodeAlsoKnownAs) + # is tested in test_dtsh_modelutils. + arg = DTShArgNodeAlsoKnownAs() + + pattern = "led" + re_strict = False + ignore_case = False + + raw_criterion = DTNodeAlsoKnownAs( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + pattern = "*LED*" + ignore_case = True + + raw_criterion = DTNodeAlsoKnownAs( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + pattern = ".*LED.*" + re_strict = True + + raw_criterion = DTNodeAlsoKnownAs( + pattern, re_strict=re_strict, ignore_case=ignore_case + ) + arg.parsed(pattern) + arg_criterion = arg.get_criterion( + re_strict=re_strict, ignore_case=ignore_case + ) + assert arg_criterion + for node in nodes: + assert raw_criterion.match(node) == arg_criterion.match(node) + + +def test_dtsharg_orderby() -> None: + arg = DTShArgOrderBy() + + # Here we only test the argument meta-data and parsing, + # the sorters are tested in test_dtsh_modelutils. + for key, order_by in DTSH_NODE_ORDER_BY.items(): + assert key == order_by.key + assert order_by.brief + + assert not arg.isset + assert arg.sorter is None + + DTShTests.check_arg(arg, parsed=key) + assert arg.isset + assert order_by.sorter is arg.sorter + arg.reset() + + +def test_dtsharg_orderby_autocomp() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + sh = DTSh(dtmodel, []) + arg = DTShArgOrderBy() + + # All argument candidate values. + assert sorted(DTSH_NODE_ORDER_BY.keys(), key=lambda x: x.lower()) == [ + state.rlstr for state in arg.autocomp("", sh) + ] + + # Exact match. + for key in DTSH_NODE_ORDER_BY: + assert key == arg.autocomp(key, sh)[0].rlstr + + # No match. + assert not arg.autocomp("not a key", sh) + + +def test_dtshparam_dtpath() -> None: + param = DTShParamDTPath() + assert param.brief + assert "[PATH]" == param.usage + # Default value. + assert "" == param.path + + # Parameter's state and multiplicity. + DTShTests.check_param(param) + + # Won't fault, the path parameter is returned unchanged, + # path resolution is deferred to command execution. + param.parsed(["value"]) + assert "value" == param.path + + +def test_dtshparam_dtpath_autocomp() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + sh = DTSh(dtmodel, []) + param = DTShParamDTPath() + + # Auto-complete DT path. + assert sorted([node.name for node in dtmodel.root.children]) == [ + state.rlstr for state in param.autocomp("", sh) + ] + # Exact match. + assert ["soc"] == [state.rlstr for state in param.autocomp("so", sh)] + # No match. + assert [] == param.autocomp("not/a/path", sh) + + # Auto-complete DT label. + assert ["&i2c0", "&i2c0_default", "&i2c0_sleep"] == [ + state.rlstr for state in param.autocomp("&i2c0", sh) + ] + # Exact match, substitute with path. + assert ["/pin-controller/i2c0_sleep"] == [ + state.rlstr for state in param.autocomp("&i2c0_s", sh) + ] + + +def test_dtshparam_dtpaths() -> None: + param = DTShParamDTPaths() + assert param.brief + assert "[PATH ...]" == param.usage + # Default value. + assert [""] == param.paths + + # Parameter's state and multiplicity. + DTShTests.check_param(param) + + # Won't fault, the path parameter is returned unchanged, + # path resolution is deferred to command execution. + param.parsed(["value"]) + assert ["value"] == param.paths + + +def test_dtshparam_dtpaths_expand() -> None: + param = DTShParamDTPaths() + flag_enable_only = DTShFlagEnabledOnly() + cmd = DTShCommand("cmd", "", [flag_enable_only], param) + sh = DTSh(DTShTests.get_sample_dtmodel(), [cmd]) + + assert [DTSh.PathExpansion(".", [sh.cwd])] == param.expand(cmd, sh) + + param.parsed(["."]) + assert [DTSh.PathExpansion(".", [sh.cwd])] == param.expand(cmd, sh) + + param.parsed(["*"]) + assert [DTSh.PathExpansion("", sh.cwd.children)] == param.expand(cmd, sh) + + param.parsed(["*leds"]) + assert [ + DTSh.PathExpansion("", [sh.dt["/leds"], sh.dt["/pwmleds"]]) + ] == param.expand(cmd, sh) + + sh.cd("soc") + param.parsed(["../leds*"]) + assert [DTSh.PathExpansion("..", [sh.dt["/leds"]])] == param.expand(cmd, sh) + + +def test_dtshparam_alias() -> None: + param = DTShParamAlias() + assert param.brief + assert "[NAME]" == param.usage + assert [] == param.raw + # Default value (means all aliased nodes). + assert "" == param.alias + + # Parameter's state and multiplicity. + DTShTests.check_param(param) + + # Won't fault, the alias parameter is returned unchanged, + # name resolution is deferred to command execution. + param.parsed(["not-an-alias"]) + assert ["not-an-alias"] == param.raw + assert "not-an-alias" == param.alias + + +def test_dtshparam_alias_autocomp() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + sh = DTSh(dtmodel, []) + param = DTShParamAlias() + + assert ["led0", "led1", "led2", "led3"] == [ + state.rlstr for state in param.autocomp("led", sh) + ] + # Exact match. + assert ["led0"] == [state.rlstr for state in param.autocomp("led0", sh)] + # No match. + assert [] == param.autocomp("ledX", sh) + + +def test_dtshparam_chosen() -> None: + param = DTShParamChosen() + assert param.brief + assert "[NAME]" == param.usage + assert [] == param.raw + # Default value (means all chosen nodes). + assert "" == param.chosen + + # Won't fault, the alias parameter is returned unchanged, + # name resolution is deferred to command execution. + param.parsed(["not-a-chosen"]) + assert ["not-a-chosen"] == param.raw + assert "not-a-chosen" == param.chosen + + +def test_dtshparam_chosen_autocomp() -> None: + dtmodel = DTShTests.get_sample_dtmodel() + sh = DTSh(dtmodel, []) + param = DTShParamChosen() + + assert ["zephyr,bt-c2h-uart", "zephyr,bt-mon-uart"] == [ + state.rlstr for state in param.autocomp("zephyr,bt", sh) + ] + # Exact match. + assert ["zephyr,bt-mon-uart"] == [ + state.rlstr for state in param.autocomp("zephyr,bt-mon", sh) + ] + # No match. + assert [] == param.autocomp("Zephyr,", sh) diff --git a/tests/test_dtsh_theme.py b/tests/test_dtsh_theme.py new file mode 100644 index 0000000..b343d64 --- /dev/null +++ b/tests/test_dtsh_theme.py @@ -0,0 +1,90 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Unit tests for the dtsh.rich.theme module.""" + +# Relax pylint a bit for unit tests. +# pylint: disable=missing-function-docstring + + +import os + +import pytest + +from rich.color import Color + +from dtsh.rich.theme import DTShTheme + +from .dtsh_uthelpers import DTShTests + + +def test_dtshtheme_load_theme_file() -> None: + # 1. Initialize configuration with bundled defaults. + theme = DTShTheme(DTShTests.get_resource_path("theme", "test.ini")) + + assert not theme.styles["test.default"].bold + # See Color.ANSI_COLOR_NAMES for valid color names. + assert theme.styles["test.red"].color == Color.parse("red") + + assert theme.styles["test.interpolation"].color == Color.parse("red") + + # 2. Load user's specific configuration (overrides defaults). + theme.load_theme_file(DTShTests.get_resource_path("theme", "override.ini")) + assert theme.styles["test.default"].color == Color.parse("green") + + with pytest.raises(DTShTheme.Error): + theme.load_theme_file("not/a/styles/file.ini") + + +def test_dtshtheme_defaults() -> None: + # All these constants MUST have suitable values in the bundled theme.ini. + theme_ini = os.path.abspath( + os.path.join( + os.path.dirname(__file__), "..", "src", "dtsh", "rich", "theme.ini" + ) + ) + assert os.path.isfile(theme_ini) + theme_defaults = DTShTheme(theme_ini) + + assert theme_defaults.styles[DTShTheme.STYLE_DEFAULT] + assert theme_defaults.styles[DTShTheme.STYLE_WARNING] + assert theme_defaults.styles[DTShTheme.STYLE_ERROR] + assert theme_defaults.styles[DTShTheme.STYLE_DISABLED] + + assert theme_defaults.styles[DTShTheme.STYLE_FS_FILE] + assert theme_defaults.styles[DTShTheme.STYLE_FS_DIR] + + assert theme_defaults.styles[DTShTheme.STYLE_LINK_LOCAL] + assert theme_defaults.styles[DTShTheme.STYLE_LINK_WWW] + + assert theme_defaults.styles[DTShTheme.STYLE_LIST_HEADER] + assert theme_defaults.styles[DTShTheme.STYLE_TREE_HEADER] + + assert theme_defaults.styles[DTShTheme.STYLE_DT_PATH_BRANCH] + assert theme_defaults.styles[DTShTheme.STYLE_DT_PATH_NODE] + + assert theme_defaults.styles[DTShTheme.STYLE_DT_NODE_NAME] + assert theme_defaults.styles[DTShTheme.STYLE_DT_UNIT_NAME] + assert theme_defaults.styles[DTShTheme.STYLE_DT_UNIT_ADDR] + assert theme_defaults.styles[DTShTheme.STYLE_DT_DEVICE_LABEL] + assert theme_defaults.styles[DTShTheme.STYLE_DT_NODE_LABEL] + assert theme_defaults.styles[DTShTheme.STYLE_DT_COMPAT_STR] + assert theme_defaults.styles[DTShTheme.STYLE_DT_BINDING_COMPAT] + assert theme_defaults.styles[DTShTheme.STYLE_DT_BINDING_DESC] + assert theme_defaults.styles[DTShTheme.STYLE_DT_CB_ORDER] + assert theme_defaults.styles[DTShTheme.STYLE_DT_IS_CHILD_BINDING] + assert theme_defaults.styles[DTShTheme.STYLE_DT_ALIAS] + assert theme_defaults.styles[DTShTheme.STYLE_DT_CHOSEN] + assert theme_defaults.styles[DTShTheme.STYLE_DT_BUS] + assert theme_defaults.styles[DTShTheme.STYLE_DT_ON_BUS] + assert theme_defaults.styles[DTShTheme.STYLE_DT_IRQ_NUMBER] + assert theme_defaults.styles[DTShTheme.STYLE_DT_IRQ_PRIORITY] + assert theme_defaults.styles[DTShTheme.STYLE_DT_REG_ADDR] + assert theme_defaults.styles[DTShTheme.STYLE_DT_REG_SIZE] + assert theme_defaults.styles[DTShTheme.STYLE_DT_ORDINAL] + assert theme_defaults.styles[DTShTheme.STYLE_DT_DEP_ON] + assert theme_defaults.styles[DTShTheme.STYLE_DT_DEP_FAILED] + assert theme_defaults.styles[DTShTheme.STYLE_DT_REQ_BY] + assert theme_defaults.styles[DTShTheme.STYLE_DT_VENDOR_NAME] + assert theme_defaults.styles[DTShTheme.STYLE_DT_VENDOR_PREFIX] diff --git a/tests/test_dtsh_uthelpers.py b/tests/test_dtsh_uthelpers.py new file mode 100644 index 0000000..d6aa681 --- /dev/null +++ b/tests/test_dtsh_uthelpers.py @@ -0,0 +1,59 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +"""Unit tests for the dtsh_uthelpers module.""" + +# Relax pylint a bit for unit tests. +# pylint: disable=missing-function-docstring + + +import os + +from .dtsh_uthelpers import DTShTests + + +def test_dtsh_uthelpers_mockenv() -> None: + # An existing variable we'll change. + prev_pwd = os.environ["PWD"] + assert prev_pwd + tmp_pwd = "TMP1" + # An existing variable we'll unset. + prev_user = os.environ["USER"] + assert prev_user + # A new OS environment variable, that should not exist. + tmp_kandiarok = "TMP2" + assert not os.environ.get("KONDIARONK") + + with DTShTests.mock_env( + {"PWD": tmp_pwd, "KONDIARONK": tmp_kandiarok, "USER": None} + ): + assert tmp_pwd == os.environ["PWD"] + assert not os.environ.get("USER") + assert tmp_kandiarok == os.environ["KONDIARONK"] + + assert prev_pwd == os.environ["PWD"] + assert prev_user == os.environ["USER"] + assert not os.environ.get("KONDIARONK") + + +def test_dtsh_uthelpers_zephyr_base() -> None: + assert os.path.isfile(os.path.join(DTShTests.ZEPHYR_BASE, "zephyr-env.sh")) + + +def test_dtsh_uthelpers_get_resource_path() -> None: + assert os.path.join( + DTShTests.RES_BASE, "README" + ) == DTShTests.get_resource_path("README") + + +def test_dtsh_uthelpers_from_res() -> None: + with DTShTests.from_res(): + assert os.path.isfile("README") + + +def test_get_sample_edt() -> None: + model = DTShTests.get_sample_edt() + assert model + assert model is DTShTests.get_sample_edt() + assert model is not DTShTests.get_sample_edt(force_reload=True) diff --git a/tests/test_shell.py b/tests/test_shell.py deleted file mode 100644 index 1ffc224..0000000 --- a/tests/test_shell.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright (c) 2022 Chris Duf -# -# SPDX-License-Identifier: Apache-2.0 - -"""Covers the dtsh.shell module.""" - -import contextlib -import os -import pytest - -from devicetree.edtlib import EDT - -from dtsh.shell import DevicetreeShell -from dtsh.dtsh import DtshCommandNotFoundError, DtshError, DtshVt - - -# Context manager pattern borrowed from python-devicetree (test_edtlib.py). -# -HERE = os.path.dirname(__file__) - - -@contextlib.contextmanager -def from_here(): - cwd = os.getcwd() - try: - os.chdir(HERE) - yield - finally: - os.chdir(cwd) - - -def test_shell_create(): - """Covers DevicetreeShell.create(). - """ - with from_here(): - edt = EDT('test.dts', ['bindings']) - shell = DevicetreeShell.create('test.dts', ['bindings']) - assert shell.pwd == edt.get_node('/').path - - with pytest.raises(DtshError): - shell = DevicetreeShell.create() - - with pytest.raises(DtshError): - shell = DevicetreeShell.create('invalid.dts', ['bindings']) - - with pytest.raises(DtshError): - shell = DevicetreeShell.create('test.dts', ['invalid']) - - -def test_shell_builtins(): - """Covers DevicetreeShell built-ins definition. - """ - with from_here(): - shell = DevicetreeShell.create('test.dts', ['bindings']) - - assert len(shell.builtins) == 10 - assert shell.builtin('pwd') is not None - assert shell.builtin('cd') is not None - assert shell.builtin('ls') is not None - assert shell.builtin('tree') is not None - assert shell.builtin('cat') is not None - assert shell.builtin('alias') is not None - assert shell.builtin('chosen') is not None - assert shell.builtin('find') is not None - assert shell.builtin('uname') is not None - assert shell.builtin('man') is not None - - with pytest.raises(DtshCommandNotFoundError): - shell.exec_command_string('unsupported-builtin', DtshVt()) diff --git a/tests/typed.py b/tests/typed.py new file mode 100644 index 0000000..90fdf91 --- /dev/null +++ b/tests/typed.py @@ -0,0 +1,5 @@ +# Copyright (c) 2023 Christophe Dufaza +# +# SPDX-License-Identifier: Apache-2.0 + +# Marker file for PEP 561.