blob: 1aaa5f5db3efcaa8b791ba08cf07d2db07c0439e [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.
"""Unit tests for trace_writing.fxt."""
import io
import os
import struct
import tempfile
import unittest
from trace_processing import trace_importing_fxt, trace_model
from trace_writing import fxt
def create_model_from_fxt_builder(builder: fxt.Builder) -> trace_model.Model:
"""Create a trace model from an fxt.Builder via a temporary file."""
with tempfile.TemporaryDirectory() as tmp_dir:
temp_path = os.path.join(tmp_dir, "trace.fxt")
with open(temp_path, "wb") as f:
f.write(builder.to_bytes())
return trace_importing_fxt.create_model_from_fxt_path_directly(
temp_path
)
class FxtTest(unittest.TestCase):
"""Test suite validating fxt.Builder functionality."""
def test_magic_and_initialization(self) -> None:
builder = fxt.Builder(ticks_per_second=1000000000)
data = builder.to_bytes()
self.assertGreaterEqual(len(data), 24)
magic = struct.unpack_from("<Q", data, 0)[0]
self.assertEqual(magic, 0x0016547846040010)
init_header = struct.unpack_from("<Q", data, 8)[0]
self.assertEqual(init_header & 0xF, 1) # Type 1 = Initialization
self.assertEqual((init_header >> 4) & 0xFFF, 2) # Size 2 words
ticks = struct.unpack_from("<Q", data, 16)[0]
self.assertEqual(ticks, 1000000000)
def test_argument_types(self) -> None:
builder = fxt.Builder()
builder.add_process(pid=100, name="test_process")
builder.add_thread(pid=100, tid=200, name="test_thread")
builder.add_duration_complete(
category="test_cat",
name="test_event",
timestamp_ns=1000,
duration_ns=500,
pid=100,
tid=200,
args={
"int_val": 42,
"int64_val": 0x100000000,
"neg_int64_val": -10000000000,
"float_val": 3.14,
"str_val": "hello",
"bool_val": True,
"process": 100,
},
)
model = create_model_from_fxt_builder(builder)
events = list(model.all_events())
self.assertEqual(len(events), 1)
ev = events[0]
self.assertEqual(ev.name, "test_event")
self.assertEqual(ev.args["int_val"], 42)
self.assertEqual(ev.args["int64_val"], 0x100000000)
self.assertEqual(ev.args["neg_int64_val"], -10000000000)
self.assertAlmostEqual(ev.args["float_val"], 3.14)
self.assertEqual(ev.args["str_val"], "hello")
self.assertEqual(ev.args["bool_val"], True)
def test_end_to_end_model_creation(self) -> None:
"""Verify that fxt.Builder produces a valid trace model."""
builder = fxt.Builder()
builder.add_process(pid=100, name="my_process")
builder.add_thread(pid=100, tid=200, name="my_thread")
builder.add_duration_complete(
category="benchmark",
name="slice_a",
timestamp_ns=1000,
duration_ns=5000,
pid=100,
tid=200,
args={"key": "value"},
)
builder.add_instant(
category="benchmark",
name="instant_mark",
timestamp_ns=2000,
pid=100,
tid=200,
scope="t",
)
model: trace_model.Model = create_model_from_fxt_builder(builder)
processes = {p.pid: p.name for p in model.processes}
self.assertIn(100, processes)
self.assertEqual(processes[100], "my_process")
events = list(model.all_events())
event_names = [e.name for e in events]
self.assertIn("slice_a", event_names)
self.assertIn("instant_mark", event_names)
def test_all_event_types(self) -> None:
builder = fxt.Builder()
builder.add_process(pid=100, name="proc")
builder.add_thread(pid=100, tid=200, name="thread")
builder.add_duration_begin(
category="cat", name="dur_b", timestamp_ns=1000, pid=100, tid=200
)
builder.add_duration_end(
category="cat", name="dur_b", timestamp_ns=2000, pid=100, tid=200
)
builder.add_instant(
category="cat",
name="inst_p",
timestamp_ns=3000,
pid=100,
tid=200,
scope="p",
)
builder.add_counter(
category="cat",
name="counter",
timestamp_ns=4000,
pid=100,
tid=200,
args={"metric": 10},
)
model = create_model_from_fxt_builder(builder)
events = list(model.all_events())
self.assertEqual(len(events), 3) # duration, instant, counter
names = [e.name for e in events]
self.assertIn("dur_b", names)
self.assertIn("inst_p", names)
self.assertIn("counter", names)
def test_flow_and_async_events(self) -> None:
builder = fxt.Builder()
builder.add_process(pid=100, name="proc")
builder.add_thread(pid=100, tid=200, name="thread")
builder.add_flow_begin("cat", "flow", 1000, 100, 200, flow_id=1)
builder.add_flow_step("cat", "flow", 2000, 100, 200, flow_id=1)
builder.add_flow_end("cat", "flow", 3000, 100, 200, flow_id=1)
builder.add_async_begin("cat", "async", 1000, 100, 200, async_id=1)
builder.add_async_instant("cat", "async", 2000, 100, 200, async_id=1)
builder.add_async_end("cat", "async", 3000, 100, 200, async_id=1)
data = builder.to_bytes()
self.assertGreater(len(data), 0)
def test_scheduler_events(self) -> None:
builder = fxt.Builder()
builder.add_process(pid=100, name="proc")
builder.add_thread(pid=100, tid=200, name="thread")
builder.add_thread_wakeup(timestamp_ns=1000, cpu_number=0, tid=200)
builder.add_context_switch(
timestamp_ns=2000,
cpu_number=0,
outgoing_thread_state=2,
outgoing_tid=200,
incoming_tid=0,
outgoing_priority=10,
incoming_priority=0,
)
model = create_model_from_fxt_builder(builder)
self.assertIn(0, model.scheduling_records)
records = model.scheduling_records[0]
self.assertEqual(len(records), 2)
self.assertIsInstance(records[0], trace_model.Waking)
self.assertIsInstance(records[1], trace_model.ContextSwitch)
def test_string_too_long_raises(self) -> None:
builder = fxt.Builder()
too_long = "x" * 0x8000
with self.assertRaises(ValueError):
builder.add_process(pid=1, name=too_long)
def test_thread_limit_raises(self) -> None:
builder = fxt.Builder()
builder.add_process(pid=1, name="proc")
# Add 255 threads (valid indices 1..255)
for i in range(1, 256):
builder.add_thread(pid=1, tid=i, name=f"thread_{i}")
# The 256th thread must raise ValueError
with self.assertRaises(ValueError):
builder.add_thread(pid=1, tid=256, name="thread_256")
def test_from_json_dict_and_string(self) -> None:
json_data = {
"traceEvents": [
{
"ph": "M",
"name": "process_name",
"pid": 50,
"args": {"name": "p_json"},
},
{
"ph": "M",
"name": "thread_name",
"pid": 50,
"tid": 51,
"args": {"name": "t_json"},
},
{
"ph": "X",
"cat": "json_cat",
"name": "json_slice",
"ts": 10.0,
"dur": 5.0,
"pid": 50,
"tid": 51,
"args": {"k": "v"},
},
{
"ph": "I",
"cat": "json_cat",
"name": "json_inst",
"ts": 12.0,
"s": "p",
"pid": 50,
"tid": 51,
"args": {},
},
]
}
builder_from_dict = fxt.Builder.from_json_dict(json_data)
model_dict = create_model_from_fxt_builder(builder_from_dict)
events_dict = list(model_dict.all_events())
self.assertEqual(len(events_dict), 2)
names = [e.name for e in events_dict]
self.assertIn("json_slice", names)
self.assertIn("json_inst", names)
# Also test from_json_string
import json
json_str = json.dumps(json_data)
builder_from_str = fxt.Builder.from_json_string(json_str)
model_str = create_model_from_fxt_builder(builder_from_str)
self.assertEqual(len(list(model_str.all_events())), 2)
def test_write_to_file(self) -> None:
builder = fxt.Builder()
builder.add_process(pid=1, name="p")
# Test stream write
stream = io.BytesIO()
builder.write_to_stream(stream)
self.assertEqual(stream.getvalue(), builder.to_bytes())
# Test file path write
with tempfile.TemporaryDirectory() as tmp_dir:
temp_path = os.path.join(tmp_dir, "test.fxt")
builder.write_to_file(temp_path)
with open(temp_path, "rb") as f:
self.assertEqual(f.read(), builder.to_bytes())
if __name__ == "__main__":
unittest.main()