| # 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() |