blob: 5341a265882cd381b8f6226160d13f797977704d [file]
# 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 unittest
from unittest import mock
from honeydew.affordances.tracing import tracing as tracing_affordance
from honeydew.transports.ffx import types as ffx_types
from reporting import metrics
from trace_processing import trace_model, trace_time
from wlan_test_utils import odpm_utils
class OdpmUtilsTest(unittest.IsolatedAsyncioTestCase):
def setUp(self) -> None:
self.mock_tracing = mock.create_autospec(tracing_affordance.Tracing)
async def test_start_trace(self) -> None:
self.mock_tracing.is_session_initialized.return_value = False
self.mock_tracing.is_active.return_value = False
await odpm_utils.start_trace(self.mock_tracing)
self.mock_tracing.initialize.assert_called_once_with(
categories=["kernel:meta", "power"],
)
self.mock_tracing.start.assert_awaited_once()
async def test_start_trace_configures_and_logs_polling_once_per_tracing_instance(
self,
) -> None:
self.mock_tracing.is_session_initialized.return_value = False
self.mock_tracing.is_active.return_value = False
mock_ffx = mock.MagicMock()
mock_ffx.run.return_value = (
"Successfully updated ODPM polling configuration.\n"
"ODPM Polling Configuration:\n"
" Hardware poll interval: 100.0 ms (0.100 s)\n"
" Polls per series entry: 1\n"
" Series recording interval: 0.10 s\n"
)
self.mock_tracing._fc_transport = mock.MagicMock(
_device_address_change=mock.MagicMock(ffx=mock_ffx)
)
with self.assertLogs(odpm_utils._LOGGER, level="DEBUG") as cm:
await odpm_utils.start_trace(self.mock_tracing)
await odpm_utils.start_trace(self.mock_tracing)
mock_ffx.run.assert_called_once_with(
[
"component",
"explore",
odpm_utils.ODPM_MONIKER,
"--command",
"odpm_util config set --polls-per-series-entry 1 --poll-interval-ms 100",
],
machine=ffx_types.MachineFormat.RAW,
)
debug_msgs = [
msg for msg in cm.output if "ODPM polling configuration:" in msg
]
self.assertEqual(len(debug_msgs), 1)
self.assertIn("Hardware poll interval: 100.0 ms", debug_msgs[0])
async def test_stop_trace(self) -> None:
self.mock_tracing.is_active.return_value = True
self.mock_tracing.is_session_initialized.return_value = True
self.mock_tracing.terminate_and_download.return_value = (
"/tmp/odpm_trace.fxt"
)
trace_path = await odpm_utils.stop_trace(self.mock_tracing, "/tmp")
self.assertEqual(trace_path, "/tmp/odpm_trace.fxt")
self.mock_tracing.stop.assert_awaited_once()
self.mock_tracing.terminate_and_download.assert_awaited_once_with(
directory="/tmp",
trace_file="odpm_trace.fxt",
)
@mock.patch("os.path.exists", return_value=True)
@mock.patch(
"wlan_test_utils.odpm_utils.trace_importing.create_model_from_trace_file_path"
)
@mock.patch(
"wlan_test_utils.odpm_utils.odpm_power.OdpmPowerMetricsProcessor"
)
def test_get_odpm_measurements(
self,
mock_processor_class: mock.MagicMock,
mock_create_model: mock.MagicMock,
mock_os_exists: mock.MagicMock,
) -> None:
mock_processor = mock_processor_class.return_value
# Mock processor returning Power_wlan with time-series samples [2.0, 3.0] (mean = 2.5W)
mock_processor.process_metrics.return_value = [
metrics.TestCaseResult(
label="Power_wlan",
unit=metrics.Unit.watts,
values=[2.0, 3.0],
)
]
# Mock trace_model.CounterEvent for duration calculation (2 seconds)
event1 = trace_model.CounterEvent(
id=None,
base=trace_model.Event(
category="power",
name="wlan_odpm_rail",
start=trace_time.TimePoint(1000000000),
pid=0,
tid=0,
args=None,
),
)
event2 = trace_model.CounterEvent(
id=None,
base=trace_model.Event(
category="power",
name="wlan_odpm_rail",
start=trace_time.TimePoint(3000000000),
pid=0,
tid=0,
args=None,
),
)
mock_model = mock.MagicMock()
mock_model.all_events.return_value = [event1, event2]
mock_create_model.return_value = mock_model
results = odpm_utils.get_odpm_measurements("/tmp/odpm_trace.fxt")
# We should have the original Power_wlan and the calculated TotalEnergy_Joules_wlan
self.assertEqual(len(results), 2)
self.assertEqual(results[0].label, "Power_wlan")
self.assertEqual(results[1].label, "TotalEnergy_Joules_wlan")
# Duration is 2 seconds, mean power is 2.5W -> TotalEnergy is 5.0 Joules
self.assertEqual(results[1].values, (5.0,))
@mock.patch("os.path.exists", return_value=True)
@mock.patch(
"wlan_test_utils.odpm_utils.trace_importing.create_model_from_trace_file_path"
)
def test_get_odpm_measurements_real_processor(
self,
mock_create_model: mock.MagicMock,
mock_os_exists: mock.MagicMock,
) -> None:
"""Verifies get_odpm_measurements using the real OdpmPowerMetricsProcessor (unmocked).
The metric label "Power_wlan" is expected because the processor is initialized
with sum_rails={"wlan": [rail]}, where "wlan" specifies the group metric name.
This unmocked test verifies that OdpmPowerMetricsProcessor indeed generates
"Power_wlan" as expected and ensures any future processing or label updates are caught.
"""
event1 = trace_model.CounterEvent(
id=None,
base=trace_model.Event(
category="power",
name="wlan_bt_odpm_rail",
start=trace_time.TimePoint(1000000000),
pid=0,
tid=0,
args={"mW": 2000},
),
)
event2 = trace_model.CounterEvent(
id=None,
base=trace_model.Event(
category="power",
name="wlan_bt_odpm_rail",
start=trace_time.TimePoint(3000000000),
pid=0,
tid=0,
args={"mW": 3000},
),
)
model = trace_model.Model(
processes=[
trace_model.Process(
pid=0,
threads=[
trace_model.Thread(tid=0, events=[event1, event2])
],
)
],
scheduling_records={},
)
mock_create_model.return_value = model
results = odpm_utils.get_odpm_measurements("/tmp/odpm_trace.fxt")
# Verify real OdpmPowerMetricsProcessor produced Power_wlan and get_odpm_measurements produced TotalEnergy
self.assertEqual(len(results), 2)
self.assertEqual(results[0].label, "Power_wlan")
self.assertEqual(results[0].values, (2.0, 3.0))
self.assertEqual(results[1].label, "TotalEnergy_Joules_wlan")
self.assertEqual(results[1].values, (5.0,))
@mock.patch("os.path.exists", return_value=True)
@mock.patch("os.remove")
@mock.patch("reporting.metrics.TestCaseResult.write_fuchsiaperf_json")
@mock.patch("wlan_test_utils.odpm_utils.get_odpm_measurements")
@mock.patch("wlan_test_utils.odpm_utils.stop_trace")
async def test_stop_trace_and_save_metrics(
self,
mock_stop_trace: mock.AsyncMock,
mock_get_measurements: mock.MagicMock,
mock_write_json: mock.MagicMock,
mock_os_remove: mock.MagicMock,
mock_os_exists: mock.MagicMock,
) -> None:
mock_stop_trace.return_value = "/tmp/test_dir/odpm_trace.fxt"
expected_metrics = [
metrics.TestCaseResult(
label="Power_wlan",
unit=metrics.Unit.watts,
values=[2.5],
)
]
mock_get_measurements.return_value = expected_metrics
res = await odpm_utils.stop_trace_and_save_metrics(
self.mock_tracing, "/tmp/test_dir", "my_test", save_raw_trace=False
)
self.assertEqual(res, expected_metrics)
mock_stop_trace.assert_awaited_once_with(
self.mock_tracing, trace_output_dir="/tmp/test_dir"
)
mock_get_measurements.assert_called_once_with(
"/tmp/test_dir/odpm_trace.fxt",
rail=odpm_utils.ODPM_WLAN_RAIL,
)
mock_write_json.assert_called_once()
mock_os_remove.assert_called_once_with("/tmp/test_dir/odpm_trace.fxt")
@mock.patch("os.path.exists", return_value=True)
@mock.patch("os.remove")
@mock.patch(
"wlan_test_utils.odpm_utils.get_odpm_measurements",
side_effect=ValueError("Parse error"),
)
@mock.patch("wlan_test_utils.odpm_utils.stop_trace")
async def test_stop_trace_and_save_metrics_handles_parse_exception(
self,
mock_stop_trace: mock.AsyncMock,
mock_get_measurements: mock.MagicMock,
mock_os_remove: mock.MagicMock,
mock_os_exists: mock.MagicMock,
) -> None:
mock_stop_trace.return_value = "/tmp/test_dir/odpm_trace.fxt"
res = await odpm_utils.stop_trace_and_save_metrics(
self.mock_tracing, "/tmp/test_dir", "my_test", save_raw_trace=False
)
self.assertEqual(res, [])
mock_os_remove.assert_called_once_with("/tmp/test_dir/odpm_trace.fxt")
@mock.patch("os.path.exists", return_value=True)
@mock.patch("os.remove")
@mock.patch(
"reporting.metrics.TestCaseResult.write_fuchsiaperf_json",
side_effect=OSError("Disk full"),
)
@mock.patch("wlan_test_utils.odpm_utils.get_odpm_measurements")
@mock.patch("wlan_test_utils.odpm_utils.stop_trace")
async def test_stop_trace_and_save_metrics_handles_write_exception(
self,
mock_stop_trace: mock.AsyncMock,
mock_get_measurements: mock.MagicMock,
mock_write_json: mock.MagicMock,
mock_os_remove: mock.MagicMock,
mock_os_exists: mock.MagicMock,
) -> None:
mock_stop_trace.return_value = "/tmp/test_dir/odpm_trace.fxt"
expected_metrics = [
metrics.TestCaseResult(
label="Power_wlan",
unit=metrics.Unit.watts,
values=[2.5],
)
]
mock_get_measurements.return_value = expected_metrics
res = await odpm_utils.stop_trace_and_save_metrics(
self.mock_tracing, "/tmp/test_dir", "my_test", save_raw_trace=False
)
self.assertEqual(res, expected_metrics)
mock_os_remove.assert_called_once_with("/tmp/test_dir/odpm_trace.fxt")
@mock.patch("wlan_test_utils.odpm_utils.stop_trace_and_save_metrics")
@mock.patch("wlan_test_utils.odpm_utils.start_trace")
async def test_record_odpm_metrics(
self,
mock_start_trace: mock.AsyncMock,
mock_stop_and_save: mock.AsyncMock,
) -> None:
async with odpm_utils.record_odpm_metrics(
self.mock_tracing, "/tmp/test_dir", "my_test"
):
mock_start_trace.assert_awaited_once_with(self.mock_tracing)
mock_stop_and_save.assert_not_awaited()
mock_stop_and_save.assert_awaited_once_with(
self.mock_tracing,
test_case_path="/tmp/test_dir",
test_name="my_test",
rail=odpm_utils.ODPM_WLAN_RAIL,
save_raw_trace=False,
)
@mock.patch("wlan_test_utils.odpm_utils.stop_trace_and_save_metrics")
@mock.patch("wlan_test_utils.odpm_utils.start_trace")
async def test_record_odpm_metrics_stops_on_exception(
self,
mock_start_trace: mock.AsyncMock,
mock_stop_and_save: mock.AsyncMock,
) -> None:
with self.assertRaises(RuntimeError):
async with odpm_utils.record_odpm_metrics(
self.mock_tracing, "/tmp/test_dir", "my_test"
):
raise RuntimeError("Test failure")
mock_start_trace.assert_awaited_once_with(self.mock_tracing)
mock_stop_and_save.assert_awaited_once_with(
self.mock_tracing,
test_case_path="/tmp/test_dir",
test_name="my_test",
rail=odpm_utils.ODPM_WLAN_RAIL,
save_raw_trace=False,
)
if __name__ == "__main__":
unittest.main()