blob: 8f31c18b869deb0b1e38a2610d1f77d82ebef69b [file]
#!/usr/bin/env fuchsia-vendored-python
#
# Copyright 2026 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import argparse
import contextlib
import io
import os
import re
import subprocess
import tempfile
import unittest
from unittest import mock
import dtc
# Captured from dtc 1.7.0.
DUPLICATE_LABEL_STDERR = """\
{path}:6.9-8.4: ERROR (duplicate_label): /b: Duplicate label 'foo' on /b and /a
Warning: Input tree has errors, output forced
"""
SAME_LINE_WARNING_STDERR = """\
{path}:4.3-13: Warning (reg_format): /c@1:reg: property has invalid length
out.dtb: Warning (simple_bus_reg): Failed prerequisite 'reg_format'
"""
SYNTAX_ERROR_STDERR = """\
Error: {path}:4.1-2 syntax error
FATAL ERROR: Unable to parse input tree
"""
DTS_SOURCE = """\
/dts-v1/;
/ {
\tfoo: a {
\t\treg = <2>;
\t};
\tfoo: b {
\t\ty = <2>;
\t};
};
"""
class TargetRegexTest(unittest.TestCase):
def test_single_position(self) -> None:
match = re.search(dtc.TARGET_REGEX, "a.dts:3.6: Warning (x): msg")
assert match is not None
self.assertEqual(
match.groups(), ("a.dts", "3", "6", "Warning (x): msg")
)
def test_same_line_range(self) -> None:
match = re.search(dtc.TARGET_REGEX, "a.dts:4.3-13: Warning (x): msg")
assert match is not None
self.assertEqual(
match.groups(), ("a.dts", "4", "3", "Warning (x): msg")
)
def test_multi_line_range(self) -> None:
match = re.search(dtc.TARGET_REGEX, "a.dts:6.9-8.4: ERROR (x): msg")
assert match is not None
self.assertEqual(match.groups(), ("a.dts", "6", "9", "ERROR (x): msg"))
def test_parse_error(self) -> None:
match = re.search(dtc.TARGET_REGEX, "Error: a.dts:4.1-2 syntax error")
assert match is not None
self.assertEqual(match.groups(), ("a.dts", "4", "1", "syntax error"))
def test_no_position(self) -> None:
for line in [
"out.dtb: Warning (simple_bus_reg): Failed prerequisite 'reg'",
"Warning: Input tree has errors, output forced",
"FATAL ERROR: Unable to parse input tree",
]:
self.assertIsNone(re.search(dtc.TARGET_REGEX, line), line)
class RunDtcTest(unittest.TestCase):
def setUp(self) -> None:
self.tmpdir = tempfile.TemporaryDirectory()
self.addCleanup(self.tmpdir.cleanup)
self.src = os.path.join(self.tmpdir.name, "board.dts")
with open(self.src, "w") as f:
f.write(DTS_SOURCE)
self.dest = os.path.join(self.tmpdir.name, "board.dtb")
self.dep = os.path.join(self.tmpdir.name, "board.d")
self.includes = os.path.join(self.tmpdir.name, "includes.txt")
with open(self.includes, "w") as f:
f.write("gen/include_dir\n")
def compile_with(
self, returncode: int, stderr: str
) -> tuple[bool, list[str], str]:
"""Runs dts_compile against a fake dtc that writes its output file.
Returns the result, the dtc command line, and the captured stderr.
"""
calls: list[list[str]] = []
def fake_run(
cmd_args: list[str], **kwargs: object
) -> subprocess.CompletedProcess[str]:
calls.append(cmd_args)
with open(self.dest, "w") as f:
f.write("forced output")
return subprocess.CompletedProcess(
cmd_args, returncode, "", stderr.format(path=self.src)
)
with open(self.includes) as includes_file:
args = argparse.Namespace(
dtc_path="dtc",
src_file=self.src,
dest_file=self.dest,
dep_file=self.dep,
includes_file=includes_file,
)
out = io.StringIO()
err = io.StringIO()
with mock.patch.object(
subprocess, "run", side_effect=fake_run
), contextlib.redirect_stderr(err), contextlib.redirect_stdout(out):
result = dtc.dts_compile(args, ["-Wno-foo"])
self.assertEqual(out.getvalue(), "")
self.assertEqual(len(calls), 1)
return result, calls[0], err.getvalue()
def test_success(self) -> None:
result, cmd, err = self.compile_with(0, "")
self.assertFalse(result)
self.assertEqual(err, "")
self.assertTrue(os.path.exists(self.dest))
self.assertEqual(
cmd,
[
"dtc",
"--force",
"--out-dependency=" + self.dep,
"--out=" + self.dest,
"--include=gen/include_dir",
"-Wno-foo",
self.src,
],
)
def test_forced_error_fails_with_context(self) -> None:
result, _, err = self.compile_with(0, DUPLICATE_LABEL_STDERR)
self.assertTrue(result)
self.assertFalse(os.path.exists(self.dest))
# Context around line 6 of the source, with a marker under column 9.
self.assertIn(" 6: \tfoo: b {", err)
self.assertIn(
" " * (dtc.LINENUM_WIDTH + 9)
+ f"{dtc.RED}^--- ERROR (duplicate_label)",
err,
)
self.assertIn("Returning failure", err)
def test_warning_fails_with_context(self) -> None:
result, _, err = self.compile_with(0, SAME_LINE_WARNING_STDERR)
self.assertTrue(result)
self.assertFalse(os.path.exists(self.dest))
self.assertIn(" 4: \t\treg = <2>;", err)
self.assertIn(
" " * (dtc.LINENUM_WIDTH + 3)
+ f"{dtc.RED}^--- Warning (reg_format)",
err,
)
self.assertIn("Returning failure", err)
def test_dtc_failure(self) -> None:
result, _, err = self.compile_with(1, SYNTAX_ERROR_STDERR)
self.assertTrue(result)
self.assertFalse(os.path.exists(self.dest))
self.assertIn(
" " * (dtc.LINENUM_WIDTH + 1) + f"{dtc.RED}^--- syntax error",
err,
)
# dtc's own failure needs no extra explanation.
self.assertNotIn("Returning failure", err)
def test_missing_source_continues(self) -> None:
stderr = (
"missing.dts:1.1-2: Warning (x): first\n"
"{path}:4.3-13: Warning (y): second\n"
)
result, _, err = self.compile_with(0, stderr)
self.assertTrue(result)
self.assertIn("Error: 'missing.dts' not found.", err)
self.assertIn(f"{dtc.RED}^--- Warning (y): second", err)
def test_decompile_forces_output(self) -> None:
args = argparse.Namespace(
dtc_path="dtc", src_file="in.dtb", dest_file=self.dest
)
with mock.patch.object(
subprocess,
"run",
return_value=subprocess.CompletedProcess([], 0, "", ""),
) as run:
self.assertFalse(dtc.dtb_decompile(args, []))
self.assertEqual(
run.call_args.args[0],
[
"dtc",
"--force",
"--sort",
"--in-format=dtb",
"--out-format=dts",
"--out=" + self.dest,
"in.dtb",
],
)
if __name__ == "__main__":
unittest.main()