#!/usr/bin/env python3 # # Minimal, self-contained reproducer for a babeltrace2 CTF-2 sink bug. # # A single trace that contains two data stream classes sharing ONE clock class # is written to CTF 2 by sink.ctf.fs. The sink emits one `clock-class` metadata # fragment PER stream class (both with the same id, here "the_clock") instead # of emitting the shared clock class only once. Reading the resulting trace # back then fails with: # # Duplicate clock class fragment with ID `the_clock`. # # This depends only on babeltrace2 and its bundled `ctf`/`utils` plugins; no # external trace files or third-party plugins are needed. # # Usage: # python3 repro.py # # The script writes the trace in a child process (so sink.ctf.fs flushes the # metadata file on component teardown), then reads it back in the parent. import os import subprocess import sys import bt2 class TheIter(bt2._UserMessageIterator): def __init__(self, config, port): stream = port.user_data self._msgs = [ self._create_stream_beginning_message(stream), self._create_stream_end_message(stream), ] self._i = 0 def __next__(self): if self._i >= len(self._msgs): raise StopIteration msg = self._msgs[self._i] self._i += 1 return msg @bt2.plugin_component_class class TheSource(bt2._UserSourceComponent, message_iterator_class=TheIter): """One trace, two stream classes, ONE shared clock class.""" def __init__(self, config, params, obj): tc = self._create_trace_class(assigns_automatic_stream_class_id=False) cc = self._create_clock_class(frequency=1000000000, name="the_clock") sc0 = tc.create_stream_class( id=0, default_clock_class=cc, supports_packets=True, assigns_automatic_stream_id=False, ) sc1 = tc.create_stream_class( id=1, default_clock_class=cc, supports_packets=True, assigns_automatic_stream_id=False, ) trace = tc() self._add_output_port("out0", trace.create_stream(sc0, id=0)) self._add_output_port("out1", trace.create_stream(sc1, id=1)) def write_trace(out_dir): ctf = bt2.find_plugin("ctf") utils = bt2.find_plugin("utils") sink_cls = ctf.sink_component_classes["fs"] # CTF 2 requires MIP 1. The ctf.fs sink has a single input port, so mux # the two source ports into it. graph = bt2.Graph(mip_version=1) src = graph.add_component(TheSource, "src") muxer = graph.add_component(utils.filter_component_classes["muxer"], "mux") sink = graph.add_component( sink_cls, "sink", params={"path": out_dir, "ctf-version": "2"} ) out_ports = list(src.output_ports.values()) graph.connect_ports(out_ports[0], list(muxer.input_ports.values())[0]) graph.connect_ports(out_ports[1], list(muxer.input_ports.values())[1]) graph.connect_ports( list(muxer.output_ports.values())[0], list(sink.input_ports.values())[0], ) graph.run() def read_trace(trace_dir): for _ in bt2.TraceCollectionMessageIterator(trace_dir): pass def main(): if len(sys.argv) < 2: print("usage: repro.py ", file=sys.stderr) return 2 # Internal entry points, invoked as child processes. if sys.argv[1] == "--write": write_trace(sys.argv[2]) return 0 if sys.argv[1] == "--read": read_trace(sys.argv[2]) return 0 out_dir = sys.argv[1] trace_dir = os.path.join(out_dir, "trace") # Write in a child process so the sink flushes the metadata on teardown. subprocess.run([sys.executable, __file__, "--write", out_dir], check=True) print("wrote CTF-2 trace to", trace_dir) with open(os.path.join(trace_dir, "metadata"), "rb") as f: n_clock = f.read().count(b'"type":"clock-class"') print("clock-class fragments written to metadata:", n_clock) # Read back with the CLI for a clear, unwrapped error message. proc = subprocess.run( ["babeltrace2", trace_dir], stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True, ) if proc.returncode != 0: print("\nREAD-BACK FAILED (bug reproduced):", file=sys.stderr) for line in proc.stderr.splitlines(): if "Duplicate clock class" in line or "Invalid fragment" in line: print(" " + line.split("] ", 1)[-1], file=sys.stderr) return 1 print("read-back OK (bug NOT reproduced)") return 0 if __name__ == "__main__": sys.exit(main())