| # 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 json2fxt.py.""" | |
| import json | |
| import os | |
| import pathlib | |
| import struct | |
| import tempfile | |
| import unittest | |
| from json2fxt import FxtConverter | |
| class Json2FxtTest(unittest.TestCase): | |
| """Test suite validating JSON to FXT conversion.""" | |
| def test_magic_and_initialization(self) -> None: | |
| converter = FxtConverter() | |
| with tempfile.TemporaryDirectory() as tmp_dir: | |
| json_path = os.path.join(tmp_dir, "test.json") | |
| fxt_path = os.path.join(tmp_dir, "test.fxt") | |
| with open(json_path, "w") as f: | |
| json.dump({"traceEvents": []}, f) | |
| converter.convert(json_path, fxt_path) | |
| with open(fxt_path, "rb") as f: | |
| content = f.read() | |
| # Word 0: Magic number (0x0016547846040010) | |
| magic = struct.unpack_from("<Q", content, 0)[0] | |
| self.assertEqual(magic, 0x0016547846040010) | |
| # Word 1: Initialization record header | |
| # Type 1 (Initialization), length 2 words | |
| init_header = struct.unpack_from("<Q", content, 8)[0] | |
| self.assertEqual(init_header & 0xF, 1) | |
| self.assertEqual((init_header >> 4) & 0xFFF, 2) | |
| # Word 2: Ticks per second (1 GHz) | |
| ticks_per_sec = struct.unpack_from("<Q", content, 16)[0] | |
| self.assertEqual(ticks_per_sec, 1000000000) | |
| def test_golden_cpu_metric_conversion(self) -> None: | |
| """Verify that converting cpu_metric.json produces identical bytes to cpu_metric.fxt.""" | |
| runtime_deps = ( | |
| pathlib.Path(__file__).resolve().parent.parent.parent | |
| / "runtime_deps" | |
| ) | |
| json_path = runtime_deps / "cpu_metric.json" | |
| golden_fxt_path = runtime_deps / "cpu_metric.fxt" | |
| if not json_path.exists() or not golden_fxt_path.exists(): | |
| self.skipTest(f"Test data not found in {runtime_deps}") | |
| converter = FxtConverter() | |
| with tempfile.TemporaryDirectory() as tmp_dir: | |
| out_fxt_path = os.path.join(tmp_dir, "cpu_metric.fxt") | |
| converter.convert(json_path, out_fxt_path) | |
| with open(golden_fxt_path, "rb") as f_golden, open( | |
| out_fxt_path, "rb" | |
| ) as f_out: | |
| self.assertEqual(f_golden.read(), f_out.read()) | |
| def test_argument_types(self) -> None: | |
| """Verify encoding of various argument types (int32, int64, float, str, bool, process).""" | |
| converter = FxtConverter() | |
| # int32 | |
| arg_i32 = converter._pack_argument("int32_val", 42) | |
| self.assertGreater(len(arg_i32), 0) | |
| # int64 | |
| arg_i64 = converter._pack_argument("int64_val", 0x100000000) | |
| self.assertGreater(len(arg_i64), 0) | |
| # float | |
| arg_float = converter._pack_argument("float_val", 3.14) | |
| self.assertGreater(len(arg_float), 0) | |
| # str | |
| arg_str = converter._pack_argument("str_val", "hello") | |
| self.assertGreater(len(arg_str), 0) | |
| # bool | |
| arg_bool = converter._pack_argument("bool_val", True) | |
| self.assertGreater(len(arg_bool), 0) | |
| # Verify bool header type is 9 | |
| header = struct.unpack_from("<Q", arg_bool, 0)[0] | |
| self.assertEqual(header & 0xF, 9) | |
| # process koid | |
| arg_proc = converter._pack_argument("process", 1234) | |
| self.assertGreater(len(arg_proc), 0) | |
| proc_header = struct.unpack_from("<Q", arg_proc, 0)[0] | |
| self.assertEqual(proc_header & 0xF, 8) | |
| def test_string_too_long_raises(self) -> None: | |
| converter = FxtConverter() | |
| too_long = "x" * 0x8000 | |
| with self.assertRaises(ValueError): | |
| converter._make_string_ref(too_long) | |
| def test_synthetic_events_conversion(self) -> None: | |
| """Verify that synthetic trace with all event types converts without error.""" | |
| synthetic_trace = { | |
| "traceEvents": [ | |
| { | |
| "ph": "M", | |
| "name": "process_name", | |
| "pid": 100, | |
| "args": {"name": "test_proc"}, | |
| }, | |
| { | |
| "ph": "M", | |
| "name": "thread_name", | |
| "pid": 100, | |
| "tid": 200, | |
| "args": {"name": "test_thread"}, | |
| }, | |
| { | |
| "ph": "X", | |
| "cat": "category", | |
| "name": "duration_complete", | |
| "ts": 1000.0, | |
| "dur": 500.0, | |
| "pid": 100, | |
| "tid": 200, | |
| "args": {"count": 1, "flag": True}, | |
| }, | |
| { | |
| "ph": "B", | |
| "cat": "category", | |
| "name": "duration_begin", | |
| "ts": 2000.0, | |
| "pid": 100, | |
| "tid": 200, | |
| }, | |
| { | |
| "ph": "E", | |
| "cat": "category", | |
| "name": "duration_end", | |
| "ts": 2500.0, | |
| "pid": 100, | |
| "tid": 200, | |
| }, | |
| { | |
| "ph": "i", | |
| "cat": "category", | |
| "name": "instant_global", | |
| "ts": 3000.0, | |
| "s": "g", | |
| "pid": 100, | |
| "tid": 200, | |
| }, | |
| { | |
| "ph": "i", | |
| "cat": "category", | |
| "name": "instant_thread", | |
| "ts": 3100.0, | |
| "s": "t", | |
| "pid": 100, | |
| "tid": 200, | |
| }, | |
| { | |
| "ph": "C", | |
| "cat": "category", | |
| "name": "counter_event", | |
| "ts": 4000.0, | |
| "pid": 100, | |
| "tid": 200, | |
| "args": {"series_a": 10}, | |
| }, | |
| { | |
| "ph": "b", | |
| "cat": "category", | |
| "name": "async_event", | |
| "ts": 5000.0, | |
| "id": 42, | |
| "pid": 100, | |
| "tid": 200, | |
| }, | |
| { | |
| "ph": "e", | |
| "cat": "category", | |
| "name": "async_event", | |
| "ts": 5500.0, | |
| "id": 42, | |
| "pid": 100, | |
| "tid": 200, | |
| }, | |
| { | |
| "ph": "s", | |
| "cat": "category", | |
| "name": "flow_event", | |
| "ts": 6000.0, | |
| "id": "0xabc", | |
| "pid": 100, | |
| "tid": 200, | |
| }, | |
| { | |
| "ph": "f", | |
| "cat": "category", | |
| "name": "flow_event", | |
| "ts": 6500.0, | |
| "id": "0xabc", | |
| "pid": 100, | |
| "tid": 200, | |
| }, | |
| ], | |
| "systemTraceEvents": { | |
| "events": [ | |
| {"ph": "p", "pid": 100, "name": "test_proc"}, | |
| {"ph": "t", "pid": 100, "tid": 200, "name": "test_thread"}, | |
| {"ph": "w", "cpu": 0, "ts": 100.0, "tid": 200}, | |
| { | |
| "ph": "k", | |
| "cpu": 0, | |
| "ts": 105.0, | |
| "out": {"tid": 200, "prio": 16, "state": 0}, | |
| "in": {"tid": 0, "prio": 0}, | |
| }, | |
| ] | |
| }, | |
| } | |
| converter = FxtConverter() | |
| with tempfile.TemporaryDirectory() as tmp_dir: | |
| json_path = os.path.join(tmp_dir, "synthetic.json") | |
| fxt_path = os.path.join(tmp_dir, "synthetic.fxt") | |
| with open(json_path, "w") as f: | |
| json.dump(synthetic_trace, f) | |
| converter.convert(json_path, fxt_path) | |
| self.assertTrue(os.path.exists(fxt_path)) | |
| self.assertGreater(os.path.getsize(fxt_path), 0) | |
| if __name__ == "__main__": | |
| unittest.main() |