Skip to content

Commit db2bf56

Browse files
committed
tools: DTS-property inspector + XSA-reference parity test for AD9081
Every System-API hw-probe failure we debugged today was visible in the rendered DTS *before* anything was flashed — but each fix still cost one CI round-trip (push → ~5 min → parse logs → diff → fix → repeat). The iteration loop is the bottleneck, not the breadth of the bugs. Add `adidt.tools.dts_inspect`: a small text-grep parser that pulls kernel-critical properties out of an emitted DTS and returns them keyed by "grouping ancestor" — e.g. `tx-dacs:adi,link-mode`, `channel@2:adi,divider`, `ad9081:clocks`. A second helper, `compare_properties`, returns a human-readable diff for a given set of keys. Commit a golden reference DTS pulled from a passing `hw-coord-mini2` workflow artifact as `test/devices/fixtures/ad9081_zcu102_xsa_reference.dts`, and parametrize a new test file over the full `KERNEL_CRITICAL_KEYS` set — each property that diverges between the XSA path (known to probe on hardware) and the System-API path becomes an individual focused failure in <1 s. Plus `adidt.tools.dts_compare_cli` for interactive diffing: python3 -m adidt.tools.dts_compare_cli \ test/devices/fixtures/ad9081_zcu102_xsa_reference.dts \ /tmp/candidate.dts Running this test suite replays every DT-emission fix chased in today's debug session as regression coverage: ad9081:clocks (dev_clk phandle) ad9081:clock-names (dev_clk name) tx-dacs / rx-adcs:adi,link-mode (9 vs 10 per side) tx-dacs / rx-adcs:adi,converters-per-device (M=8 vs M=4 bug) tx-dacs / rx-adcs:adi,lanes-per-device (L=4 vs L=0 bug) tx-dacs / rx-adcs:adi,octets-per-frame (F=4 vs F=2) channel@{0,2,6}:adi,divider (12 vs 8 bug) channel@{8,12}:adi,divider (6 vs 4 bug) channel@{3,13}:adi,divider (1536 vs 1024)
1 parent 3baf58d commit db2bf56

5 files changed

Lines changed: 1265 additions & 0 deletions

File tree

adidt/tools/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Developer-facing tools (CLI diffing, DTS inspection, etc.)."""

adidt/tools/dts_compare_cli.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""Standalone DTS-comparison CLI.
2+
3+
Usage:
4+
5+
python3 -m adidt.tools.dts_compare_cli REFERENCE.dts CANDIDATE.dts
6+
7+
Prints a diff of the kernel-critical properties between ``REFERENCE``
8+
and ``CANDIDATE`` — handy for iterating on the System API DT emission
9+
when you have a known-good DTS (e.g. pulled from a passing CI run's
10+
``hw-coord-<place>-output`` artifact) and want to see at a glance
11+
what the declarative path is doing differently.
12+
"""
13+
14+
from __future__ import annotations
15+
16+
import argparse
17+
import sys
18+
from pathlib import Path
19+
20+
from .dts_inspect import KERNEL_CRITICAL_KEYS, compare_properties, extract_props
21+
22+
23+
def main(argv: list[str] | None = None) -> int:
24+
parser = argparse.ArgumentParser(
25+
description=(
26+
"Diff kernel-critical DT properties between two DTS files "
27+
"(e.g. XSA-pipeline output vs System-API output)."
28+
)
29+
)
30+
parser.add_argument("reference", type=Path, help="Reference DTS (known-good)")
31+
parser.add_argument("candidate", type=Path, help="Candidate DTS (under test)")
32+
parser.add_argument(
33+
"--keys",
34+
nargs="+",
35+
default=KERNEL_CRITICAL_KEYS,
36+
help="Subset of property keys to compare (default: every kernel-critical key).",
37+
)
38+
args = parser.parse_args(argv)
39+
40+
ref = extract_props(args.reference.read_text())
41+
cand = extract_props(args.candidate.read_text())
42+
diffs = compare_properties(ref, cand, keys=args.keys)
43+
if not diffs:
44+
print("OK — no diff on kernel-critical keys.")
45+
return 0
46+
print(f"Diff on {len(diffs)} key(s):")
47+
for line in diffs:
48+
print(f" {line}")
49+
return 1
50+
51+
52+
if __name__ == "__main__":
53+
sys.exit(main())

adidt/tools/dts_inspect.py

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
"""DTS property inspection + cross-path comparison helpers.
2+
3+
Most hardware-probe failures on the System-API path today look like
4+
"System emits property X; XSA path (known to probe) emits Y". That
5+
diff is visible in the generated DTS *before* anything is flashed, so
6+
a plain-text inspector that can pull a handful of kernel-critical
7+
properties out of any DTS and diff them against a reference catches
8+
the bug in unit tests rather than on hardware.
9+
10+
Two entry points:
11+
12+
``extract_props``
13+
Grep the emitted DTS text for a curated set of kernel-critical
14+
properties (``compatible``, ``adi,link-mode``,
15+
``adi,converters-per-device``, HMC7044 channel dividers, …).
16+
Returns a ``{name: value}`` dict of first occurrences, with a
17+
channel-qualified key for HMC7044 channels. Intentionally a
18+
text grep — sufficient for the properties the AD9081 /
19+
ad_ip_jesd204_tpl driver probes read.
20+
21+
``compare_properties``
22+
Compute the set difference between two inspectors' outputs and
23+
return a list of human-readable diffs. No-op on keys only
24+
present in one side.
25+
"""
26+
27+
from __future__ import annotations
28+
29+
import re
30+
from collections.abc import Iterable
31+
32+
__all__ = [
33+
"extract_props",
34+
"compare_properties",
35+
"KERNEL_CRITICAL_KEYS",
36+
]
37+
38+
39+
# Properties whose value actually feeds the AD9081 / jesd204 driver
40+
# probe path. Ordered roughly by where they appear in the DTS.
41+
KERNEL_CRITICAL_KEYS: tuple[str, ...] = (
42+
# Top-level AD9081 dev_clk wiring.
43+
"ad9081:clocks",
44+
"ad9081:clock-names",
45+
# DAC (host-TX / jrx) framing.
46+
"tx-dacs:adi,dac-frequency-hz",
47+
"tx-dacs:adi,link-mode",
48+
"tx-dacs:adi,converters-per-device",
49+
"tx-dacs:adi,lanes-per-device",
50+
"tx-dacs:adi,octets-per-frame",
51+
"tx-dacs:adi,frames-per-multiframe",
52+
"tx-dacs:adi,bits-per-sample",
53+
"tx-dacs:adi,samples-per-converter-per-frame",
54+
# ADC (host-RX / jtx) framing.
55+
"rx-adcs:adi,adc-frequency-hz",
56+
"rx-adcs:adi,link-mode",
57+
"rx-adcs:adi,converters-per-device",
58+
"rx-adcs:adi,lanes-per-device",
59+
"rx-adcs:adi,octets-per-frame",
60+
"rx-adcs:adi,frames-per-multiframe",
61+
# HMC7044 channel dividers (full set; missing channels just
62+
# get filtered out by the compare).
63+
"channel@0:adi,divider",
64+
"channel@2:adi,divider",
65+
"channel@3:adi,divider",
66+
"channel@6:adi,divider",
67+
"channel@8:adi,divider",
68+
"channel@10:adi,divider",
69+
"channel@12:adi,divider",
70+
"channel@13:adi,divider",
71+
)
72+
73+
74+
# Matches a single property line like ``adi,link-mode = <9>;``,
75+
# capturing the name (group 1) and the RHS (group 2, without trailing ;).
76+
_PROP_RE = re.compile(r"^\s*([A-Za-z0-9,#_.-]+)\s*=\s*(.+?)\s*;\s*$")
77+
78+
# Matches the opening brace of a named DT node or anonymous node with
79+
# reg@N addressing: ``label: node@addr {``, ``node {``, ``node@0 {``.
80+
_NODE_RE = re.compile(
81+
r"^\s*(?:([A-Za-z0-9_]+):\s*)?([A-Za-z][A-Za-z0-9,#_-]*(?:@[0-9a-fA-F]+)?)\s*\{\s*$"
82+
)
83+
84+
85+
# When choosing a key prefix for a property, prefer one of these
86+
# ancestors over the innermost node — they're the "grouping nodes" the
87+
# tests care about (``tx-dacs:adi,link-mode`` is far more useful than
88+
# ``link@0:adi,link-mode`` since a DTS can technically nest arbitrarily).
89+
_GROUPING_ANCESTORS: tuple[str, ...] = (
90+
"channel@0",
91+
"channel@1",
92+
"channel@2",
93+
"channel@3",
94+
"channel@4",
95+
"channel@5",
96+
"channel@6",
97+
"channel@7",
98+
"channel@8",
99+
"channel@9",
100+
"channel@10",
101+
"channel@11",
102+
"channel@12",
103+
"channel@13",
104+
"adi,tx-dacs",
105+
"adi,rx-adcs",
106+
"ad9081",
107+
"hmc7044",
108+
)
109+
110+
# Normalise ancestor names that include a ``adi,`` prefix or ``@addr``
111+
# suffix to the short form used as a key prefix.
112+
_ANCESTOR_ALIAS: dict[str, str] = {
113+
"adi,tx-dacs": "tx-dacs",
114+
"adi,rx-adcs": "rx-adcs",
115+
}
116+
117+
118+
def _choose_prefix(stack: list[str]) -> str | None:
119+
"""Pick the most specific 'interesting' ancestor from *stack*.
120+
121+
Walk the stack innermost-first, return the first ancestor that
122+
appears in :data:`_GROUPING_ANCESTORS`, with ``@addr`` stripped
123+
off of ad9081/hmc7044-style nodes.
124+
"""
125+
for node in reversed(stack):
126+
# Try the exact form first (channel@N, adi,tx-dacs).
127+
if node in _GROUPING_ANCESTORS:
128+
return _ANCESTOR_ALIAS.get(node, node)
129+
# Then the @addr-stripped form (ad9081@0 → ad9081).
130+
base = node.split("@", 1)[0]
131+
if base in _GROUPING_ANCESTORS:
132+
return _ANCESTOR_ALIAS.get(base, base)
133+
return None
134+
135+
136+
def extract_props(dts_text: str) -> dict[str, str]:
137+
"""Return ``{"<grouping-ancestor>:<prop>": "<rhs>"}`` for the DTS text.
138+
139+
The grouping ancestor is the most specific enclosing node from
140+
:data:`_GROUPING_ANCESTORS` — so a property nested several levels
141+
deep inside ``adi,tx-dacs`` still gets keyed under ``tx-dacs:``
142+
rather than whatever the innermost node happens to be (e.g.
143+
``link@0``). First-occurrence wins; the base node's value is what
144+
the kernel sees at probe time before any ``&label`` overlays.
145+
"""
146+
node_stack: list[str] = []
147+
out: dict[str, str] = {}
148+
149+
for line in dts_text.splitlines():
150+
stripped = line.strip()
151+
if not stripped:
152+
continue
153+
if stripped == "};":
154+
if node_stack:
155+
node_stack.pop()
156+
continue
157+
node_match = _NODE_RE.match(line)
158+
if node_match:
159+
_label, name = node_match.groups()
160+
node_stack.append(name)
161+
continue
162+
prop_match = _PROP_RE.match(line)
163+
if prop_match:
164+
name, rhs = prop_match.groups()
165+
prefix = _choose_prefix(node_stack)
166+
if prefix is None:
167+
continue
168+
key = f"{prefix}:{name}"
169+
if key not in out:
170+
out[key] = rhs
171+
return out
172+
173+
174+
def compare_properties(
175+
reference: dict[str, str],
176+
candidate: dict[str, str],
177+
keys: Iterable[str] = KERNEL_CRITICAL_KEYS,
178+
) -> list[str]:
179+
"""Compare two ``extract_props`` outputs. Returns diff lines.
180+
181+
- Keys present in both with different values → a ``"X: ref=... cand=..."`` line.
182+
- Keys in ``reference`` but missing from ``candidate`` → ``"X: missing in candidate"``.
183+
- Keys in ``candidate`` but missing from ``reference`` → not flagged
184+
(reference may be less verbose; tests use ``keys`` to pin what
185+
they actually care about).
186+
"""
187+
diffs: list[str] = []
188+
for key in keys:
189+
if key not in reference:
190+
continue
191+
ref = reference[key]
192+
cand = candidate.get(key)
193+
if cand is None:
194+
diffs.append(f"{key}: missing in candidate (reference={ref})")
195+
elif cand != ref:
196+
diffs.append(f"{key}: reference={ref!r} candidate={cand!r}")
197+
return diffs

0 commit comments

Comments
 (0)