Skip to content

Commit cd5e44d

Browse files
committed
tests: misc: dtdoctor: add real-toolchain integration test
Exercise the DT Doctor SCA scripts end to end using the ctest harness: the application builds normally, then ctest runs deliberately-failing compile and link commands with the build's own toolchain through the real SCA wrapper against the build's edt.pickle, and checks the resulting diagnosis. The hermetic pytest suite from scripts/tests/dtdoctor runs as a separate ctest entry, so the scripts are fully covered by twister with no dedicated CI workflow. The scenario builds with both SDK toolchains (integration_toolchains: zephyr/gnu and zephyr/llvm) so the gcc/GNU ld and the clang/lld error formats are both exercised. Also covered: the disabled-node diagnosis (source location, chosen/alias references) and the enabled-node headline against the real generated Kconfig environment. Assisted-by: Claude:claude-fable-5 Signed-off-by: Benjamin Cabé <benjamin@zephyrproject.org>
1 parent 40acef0 commit cd5e44d

8 files changed

Lines changed: 240 additions & 0 deletions

File tree

tests/misc/dtdoctor/CMakeLists.txt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# SPDX-FileCopyrightText: Copyright The Zephyr Project Contributors
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
cmake_minimum_required(VERSION 3.28.0)
5+
6+
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
7+
8+
project(dtdoctor_test)
9+
target_sources(app PRIVATE src/main.c)
10+
11+
enable_testing()
12+
include(CTest)
13+
14+
# Hermetic unit tests for the dtdoctor scripts (fixture DTS and Kconfig trees)
15+
add_test(
16+
NAME dtdoctor_unit
17+
COMMAND ${PYTHON_EXECUTABLE} -m pytest ${ZEPHYR_BASE}/scripts/tests/dtdoctor -v
18+
)
19+
20+
add_test(
21+
NAME dtdoctor_e2e
22+
COMMAND ${PYTHON_EXECUTABLE} -m pytest
23+
${CMAKE_CURRENT_SOURCE_DIR}/verify_dtdoctor.py
24+
--build-dir ${CMAKE_BINARY_DIR}
25+
--cc ${CMAKE_C_COMPILER}
26+
-v
27+
)
28+
29+
# Reproduce the Kconfig environment the dtdoctor SCA launcher runs with, so the
30+
# analyzer can parse the full Zephyr Kconfig tree for the enabled-node diagnosis
31+
set_tests_properties(
32+
dtdoctor_e2e
33+
PROPERTIES ENVIRONMENT "${COMMON_KCONFIG_ENV_SETTINGS}"
34+
)

tests/misc/dtdoctor/app.overlay

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* SPDX-FileCopyrightText: Copyright The Zephyr Project Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
/*
7+
* dtdoctor_disabled is the target of the deliberately-failing builds in
8+
* verify_dtdoctor.py; the chosen entry and alias below must show up in its
9+
* diagnosis. dtdoctor_enabled exercises the "enabled but no driver" path
10+
* (vnd,dtdoctor-device deliberately has no driver).
11+
*/
12+
/ {
13+
chosen {
14+
dtdoctor,dev = &dtdoctor_disabled;
15+
};
16+
17+
aliases {
18+
dtdoctor-dev = &dtdoctor_disabled;
19+
};
20+
21+
dtdoctor_disabled: dtdoctor-disabled-device {
22+
compatible = "vnd,dtdoctor-device";
23+
status = "disabled";
24+
};
25+
26+
dtdoctor_enabled: dtdoctor-enabled-device {
27+
compatible = "vnd,dtdoctor-device";
28+
status = "okay";
29+
};
30+
};

tests/misc/dtdoctor/conftest.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# SPDX-FileCopyrightText: Copyright The Zephyr Project Contributors
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""Pytest configuration for the DT Doctor integration test."""
5+
6+
import pickle
7+
import sys
8+
from pathlib import Path
9+
10+
import pytest
11+
12+
ZEPHYR_BASE = Path(__file__).parents[3]
13+
sys.path.insert(0, str(ZEPHYR_BASE / "scripts" / "dts" / "python-devicetree" / "src"))
14+
15+
16+
def pytest_addoption(parser):
17+
parser.addoption(
18+
"--build-dir",
19+
action="store",
20+
required=True,
21+
help="Path to the build directory of the test application",
22+
)
23+
parser.addoption(
24+
"--cc",
25+
action="store",
26+
required=True,
27+
help="Path to the C compiler the application was built with",
28+
)
29+
30+
31+
@pytest.fixture(scope="session")
32+
def build_dir(request):
33+
return Path(request.config.getoption("--build-dir"))
34+
35+
36+
@pytest.fixture(scope="session")
37+
def cc(request):
38+
return request.config.getoption("--cc")
39+
40+
41+
@pytest.fixture(scope="session")
42+
def edt_pickle(build_dir):
43+
return build_dir / "zephyr" / "edt.pickle"
44+
45+
46+
@pytest.fixture(scope="session")
47+
def edt(edt_pickle):
48+
from devicetree import edtlib # noqa: F401 (needed to unpickle the EDT)
49+
50+
with open(edt_pickle, "rb") as f:
51+
return pickle.load(f)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# SPDX-FileCopyrightText: Copyright The Zephyr Project Contributors
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
description: Fake device without a driver, used by the DT Doctor integration test
5+
6+
compatible: "vnd,dtdoctor-device"

tests/misc/dtdoctor/prj.conf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# nothing here

tests/misc/dtdoctor/src/main.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/*
2+
* SPDX-FileCopyrightText: Copyright The Zephyr Project Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
int main(void)
7+
{
8+
return 0;
9+
}

tests/misc/dtdoctor/tests.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
common:
2+
tags:
3+
- devicetree
4+
- sca
5+
harness: ctest
6+
timeout: 120
7+
tests:
8+
sca.dtdoctor:
9+
integration_platforms:
10+
- qemu_cortex_m3
11+
platform_allow:
12+
- qemu_cortex_m3
13+
integration_toolchains:
14+
- zephyr/gnu
15+
- zephyr/llvm
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# SPDX-FileCopyrightText: Copyright The Zephyr Project Contributors
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""
5+
End-to-end DT Doctor checks, run by ctest against a real application build.
6+
7+
The analyzer and the SCA wrapper are exercised as real subprocesses against the
8+
build's edt.pickle, with deliberately-failing compile and link commands using
9+
the same toolchain the application was built with.
10+
"""
11+
12+
import subprocess
13+
import sys
14+
from pathlib import Path
15+
16+
import pytest
17+
18+
ZEPHYR_BASE = Path(__file__).parents[3]
19+
ANALYZER = ZEPHYR_BASE / "scripts" / "dts" / "dtdoctor_analyzer.py"
20+
WRAPPER = ZEPHYR_BASE / "scripts" / "dts" / "dtdoctor_sca_wrapper.py"
21+
22+
23+
def ord_symbol(edt, label):
24+
return f"__device_dts_ord_{edt.label2node[label].dep_ordinal}"
25+
26+
27+
def run_analyzer(edt_pickle, symbol):
28+
return subprocess.run(
29+
[sys.executable, str(ANALYZER), "--edt-pickle", str(edt_pickle), "--symbol", symbol],
30+
capture_output=True,
31+
text=True,
32+
)
33+
34+
35+
def run_wrapper_around(cmd, edt_pickle):
36+
return subprocess.run(
37+
[sys.executable, str(WRAPPER), "--edt-pickle", str(edt_pickle), "--", *cmd],
38+
capture_output=True,
39+
text=True,
40+
)
41+
42+
43+
def test_analyzer_reports_disabled_node(edt, edt_pickle):
44+
proc = run_analyzer(edt_pickle, ord_symbol(edt, "dtdoctor_disabled"))
45+
assert proc.returncode == 0
46+
assert "DT Doctor" in proc.stdout
47+
assert "is disabled in" in proc.stdout
48+
assert "dtdoctor-disabled-device" in proc.stdout
49+
assert "'dtdoctor,dev'" in proc.stdout
50+
assert "'dtdoctor-dev'" in proc.stdout
51+
assert "'status' property to 'okay'" in proc.stdout
52+
53+
54+
def test_analyzer_reports_enabled_node_without_driver(edt, edt_pickle):
55+
proc = run_analyzer(edt_pickle, ord_symbol(edt, "dtdoctor_enabled"))
56+
assert proc.returncode == 0
57+
assert "is enabled but no driver" in proc.stdout
58+
59+
60+
TESTDATA_SNIPPETS = [
61+
"int dev = __device_dts_ord_{ordinal};\n",
62+
"int get_dev(void) {{ return __device_dts_ord_{ordinal}; }}\n",
63+
]
64+
65+
66+
@pytest.mark.parametrize('template', TESTDATA_SNIPPETS, ids=['file-scope', 'function-scope'])
67+
def test_wrapper_diagnoses_compile_error(edt, edt_pickle, cc, tmp_path, template):
68+
ordinal = edt.label2node["dtdoctor_disabled"].dep_ordinal
69+
bad_c = tmp_path / "bad.c"
70+
bad_c.write_text(template.format(ordinal=ordinal), encoding="utf-8")
71+
proc = run_wrapper_around([cc, "-c", str(bad_c), "-o", str(tmp_path / "bad.o")], edt_pickle)
72+
assert proc.returncode != 0
73+
assert "DT Doctor" in proc.stdout
74+
assert "is disabled in" in proc.stdout
75+
76+
77+
def test_wrapper_diagnoses_link_error(edt, edt_pickle, cc, tmp_path):
78+
ordinal = edt.label2node["dtdoctor_disabled"].dep_ordinal
79+
bad_c = tmp_path / "bad.c"
80+
bad_c.write_text(
81+
f"extern int __device_dts_ord_{ordinal};\n"
82+
f"int main(void) {{ return __device_dts_ord_{ordinal}; }}\n",
83+
encoding="utf-8",
84+
)
85+
obj = tmp_path / "bad.o"
86+
subprocess.run([cc, "-c", str(bad_c), "-o", str(obj)], check=True)
87+
88+
link_cmd = [cc, str(obj), "-nostdlib", "-o", str(tmp_path / "bad.elf")]
89+
proc = run_wrapper_around(link_cmd, edt_pickle)
90+
assert proc.returncode != 0
91+
if "undefined reference" not in proc.stderr and "undefined symbol" not in proc.stderr:
92+
pytest.skip(f"unsupported linker error format: {proc.stderr[:200]}")
93+
assert "DT Doctor" in proc.stdout
94+
assert "is disabled in" in proc.stdout

0 commit comments

Comments
 (0)