blob: cd40057e48f6777e13b59c8a970957742b884255 [file]
# Copyright 2026 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
"""Tests for ztest_runner_core."""
import os
import tempfile
import unittest
from unittest import mock
import subprocess
import fcntl
import xml.etree.ElementTree as ET
import ztest_runner_core
class TestZTestRunnerCore(unittest.TestCase):
def setUp(self):
self.temp_dir = tempfile.TemporaryDirectory()
self.xml_file = os.path.join(self.temp_dir.name, "output.xml")
def tearDown(self):
self.temp_dir.cleanup()
@mock.patch("subprocess.run")
def test_success(self, mock_run):
# Mock successful execution
mock_run.return_value = mock.MagicMock(
returncode=0,
stdout="Booting Zephyr...\nRunning tests...\nPROJECT EXECUTION SUCCESSFUL\n"
)
res = ztest_runner_core.run_ztest("fake_bin", xml_output_file=self.xml_file)
self.assertEqual(res, 0)
# Verify XML
self.assertTrue(os.path.exists(self.xml_file))
tree = ET.parse(self.xml_file)
root = tree.getroot()
self.assertEqual(root.tag, "testsuites")
testcase = root.find("testsuite/testcase")
self.assertIsNotNone(testcase)
self.assertIsNone(testcase.find("failure"))
self.assertIsNone(testcase.find("error"))
@mock.patch("subprocess.run")
def test_failure_exit_code(self, mock_run):
# Mock crash (non-zero exit code)
mock_run.return_value = mock.MagicMock(
returncode=1,
stdout="Booting Zephyr...\nAssert failed!\n"
)
res = ztest_runner_core.run_ztest("fake_bin", xml_output_file=self.xml_file)
self.assertEqual(res, 1)
# Verify XML
self.assertTrue(os.path.exists(self.xml_file))
tree = ET.parse(self.xml_file)
root = tree.getroot()
testcase = root.find("testsuite/testcase")
self.assertIsNotNone(testcase.find("error"))
self.assertEqual(testcase.find("error").get("type"), "Crash")
@mock.patch("subprocess.run")
def test_failure_missing_signature(self, mock_run):
# Mock execution that exits 0 but doesn't print success signature
mock_run.return_value = mock.MagicMock(
returncode=0,
stdout="Booting Zephyr...\nHung or exited early without message...\n"
)
res = ztest_runner_core.run_ztest("fake_bin", xml_output_file=self.xml_file)
self.assertEqual(res, 1)
# Verify XML
self.assertTrue(os.path.exists(self.xml_file))
tree = ET.parse(self.xml_file)
root = tree.getroot()
testcase = root.find("testsuite/testcase")
self.assertIsNotNone(testcase.find("failure"))
self.assertEqual(testcase.find("failure").get("type"), "Failure")
@mock.patch("subprocess.run")
def test_timeout(self, mock_run):
# Mock timeout
mock_run.side_effect = subprocess.TimeoutExpired(cmd="fake_bin", timeout=10, output="Booting...")
res = ztest_runner_core.run_ztest("fake_bin", stop_at=10, xml_output_file=self.xml_file)
self.assertEqual(res, 1)
# Verify XML
self.assertTrue(os.path.exists(self.xml_file))
tree = ET.parse(self.xml_file)
root = tree.getroot()
testcase = root.find("testsuite/testcase")
self.assertIsNotNone(testcase.find("error"))
self.assertEqual(testcase.find("error").get("type"), "Timeout")
@mock.patch("ztest_runner_core.yaml.safe_load")
@mock.patch("ztest_runner_core.serial.Serial")
@mock.patch("ztest_runner_core.fcntl.flock")
@mock.patch("ztest_runner_core.zephyr_flash_utils.flash_main")
@mock.patch("ztest_runner_core.open", create=True)
@mock.patch("ztest_runner_core.os.open")
@mock.patch("ztest_runner_core.os.write")
@mock.patch("ztest_runner_core.os.close")
@mock.patch("ztest_runner_core.os.fchmod", create=True)
def test_hw_success(self, mock_os_fchmod, mock_os_close, mock_os_write, mock_os_open, mock_open, mock_flash_main, mock_flock, mock_serial_cls, mock_yaml_load):
mock_yaml_load.return_value = {"flash-runner": "jlink"}
mock_serial = mock.MagicMock()
mock_serial.readline.side_effect = [
b"Booting...\n",
b"PROJECT EXECUTION SUCCESSFUL\n"
]
mock_serial_cls.return_value = mock_serial
mock_flash_main.return_value = 0
# Mock lock file descriptor
mock_os_open.return_value = 99
mock_file = mock.MagicMock()
mock_file.fileno.return_value = 100
mock_open.return_value = mock_file
res = ztest_runner_core.run_ztest(
binary_path="fake_bin",
runners_yaml_path="fake_runners.yaml",
serial_port="/dev/ttyFake",
serial_number="12345",
xml_output_file=self.xml_file
)
self.assertEqual(res, 0)
mock_flash_main.assert_called_once_with("fake_runners.yaml", "fake_bin", ["--dev-id", "12345"])
mock_serial_cls.assert_called_once_with("/dev/ttyFake", 115200, timeout=1)
mock_flock.assert_has_calls([
mock.call(100, fcntl.LOCK_EX | fcntl.LOCK_NB),
mock.call(100, fcntl.LOCK_UN)
])
@mock.patch("ztest_runner_core.yaml.safe_load")
@mock.patch("ztest_runner_core.serial.Serial")
@mock.patch("ztest_runner_core.fcntl.flock")
@mock.patch("ztest_runner_core.zephyr_flash_utils.flash_main")
@mock.patch("ztest_runner_core.open", create=True)
@mock.patch("ztest_runner_core.os.open")
@mock.patch("ztest_runner_core.os.write")
@mock.patch("ztest_runner_core.os.close")
@mock.patch("ztest_runner_core.os.fchmod", create=True)
def test_hw_flash_failure(self, mock_os_fchmod, mock_os_close, mock_os_write, mock_os_open, mock_open, mock_flash_main, mock_flock, mock_serial_cls, mock_yaml_load):
mock_yaml_load.return_value = {"flash-runner": "jlink"}
mock_serial = mock.MagicMock()
mock_serial_cls.return_value = mock_serial
# flash_main returns non-zero for failure
mock_flash_main.return_value = 1
mock_os_open.return_value = 99
mock_file = mock.MagicMock()
mock_file.fileno.return_value = 100
mock_open.return_value = mock_file
with self.assertRaises(RuntimeError) as context:
ztest_runner_core.run_ztest(
binary_path="fake_bin",
runners_yaml_path="fake_runners.yaml",
serial_port="/dev/ttyFake",
serial_number="12345",
xml_output_file=self.xml_file
)
self.assertIn("Flashing failed with exit code: 1", str(context.exception))
@mock.patch("ztest_runner_core.yaml.safe_load")
@mock.patch("ztest_runner_core.serial.Serial")
@mock.patch("ztest_runner_core.fcntl.flock")
@mock.patch("ztest_runner_core.open", create=True)
@mock.patch("ztest_runner_core.os.open")
@mock.patch("ztest_runner_core.os.write")
@mock.patch("ztest_runner_core.os.close")
@mock.patch("ztest_runner_core.os.fchmod", create=True)
def test_hw_lock_busy(self, mock_os_fchmod, mock_os_close, mock_os_write, mock_os_open, mock_open, mock_flock, mock_serial_cls, mock_yaml_load):
mock_yaml_load.return_value = {"flash-runner": "jlink"}
# Mock flock to raise OSError (device busy)
mock_flock.side_effect = OSError("Resource temporarily unavailable")
mock_os_open.return_value = 99
mock_file = mock.MagicMock()
mock_file.fileno.return_value = 100
mock_open.return_value = mock_file
with self.assertRaises(RuntimeError) as context:
ztest_runner_core.run_ztest(
binary_path="fake_bin",
runners_yaml_path="fake_runners.yaml",
serial_port="/dev/ttyFake",
serial_number="12345",
xml_output_file=self.xml_file
)
self.assertIn("is busy (lock acquired by another process)", str(context.exception))
@mock.patch("ztest_runner_core.yaml.safe_load")
@mock.patch("ztest_runner_core.serial.Serial")
@mock.patch("ztest_runner_core.fcntl.flock")
@mock.patch("ztest_runner_core.zephyr_flash_utils.flash_main")
@mock.patch("ztest_runner_core.open", create=True)
@mock.patch("ztest_runner_core.os.open")
@mock.patch("ztest_runner_core.os.write")
@mock.patch("ztest_runner_core.os.close")
@mock.patch("ztest_runner_core.os.fchmod", create=True)
@mock.patch("pylink.JLink")
@mock.patch("serial.tools.list_ports.comports")
def test_hw_auto_detect(self, mock_comports, mock_jlink_cls, mock_os_fchmod, mock_os_close, mock_os_write, mock_os_open, mock_open, mock_flash_main, mock_flock, mock_serial_cls, mock_yaml_load):
mock_yaml_load.return_value = {"flash-runner": "jlink"}
# Mock J-Link connected emulators
mock_jlink = mock.MagicMock()
mock_emulator_info = mock.MagicMock()
mock_emulator_info.SerialNumber = 54321
mock_jlink.connected_emulators.return_value = [mock_emulator_info]
mock_jlink_cls.return_value = mock_jlink
# Mock serial ports
mock_port = mock.MagicMock()
mock_port.device = "/dev/ttyACM0"
mock_port.serial_number = "54321"
mock_comports.return_value = [mock_port]
mock_serial = mock.MagicMock()
mock_serial.readline.side_effect = [
b"PROJECT EXECUTION SUCCESSFUL\n"
]
mock_serial_cls.return_value = mock_serial
mock_flash_main.return_value = 0
mock_os_open.return_value = 99
mock_file = mock.MagicMock()
mock_file.fileno.return_value = 100
mock_open.return_value = mock_file
res = ztest_runner_core.run_ztest(
binary_path="fake_bin",
runners_yaml_path="fake_runners.yaml",
xml_output_file=self.xml_file
)
self.assertEqual(res, 0)
mock_flash_main.assert_called_once_with("fake_runners.yaml", "fake_bin", ["--dev-id", "54321"])
mock_serial_cls.assert_called_once_with("/dev/ttyACM0", 115200, timeout=1)
mock_flock.assert_has_calls([
mock.call(100, fcntl.LOCK_EX | fcntl.LOCK_NB),
mock.call(100, fcntl.LOCK_UN)
])
if __name__ == "__main__":
unittest.main()