4242phase_report_key = pytest .StashKey [Dict [str , pytest .CollectReport ]]()
4343
4444
45- def _select_sniff_interface_name (host , capture_cfg : dict ) -> str :
45+ def _select_sniff_interface (host , capture_cfg : dict ):
4646 def _pci_device_id (nic ) -> str :
4747 """Return lowercased PCI vendor:device identifier (e.g., '8086:1592')."""
48- pci_device = getattr (nic , "pci_device" , None )
49- if pci_device is None :
50- return ""
51-
52- vendor_id = getattr (pci_device , "vendor_id" , None )
53- device_id = getattr (pci_device , "device_id" , None )
54- if vendor_id is None or device_id is None :
55- return ""
56-
57- return f"{ vendor_id } :{ device_id } " .lower ()
48+ return f"{ nic .pci_device .vendor_id } :{ nic .pci_device .device_id } " .lower ()
5849
5950 sniff_interface = capture_cfg .get ("sniff_interface" )
6051 if sniff_interface :
61- return str (sniff_interface )
52+ for nic in host .network_interfaces :
53+ if nic .name == str (sniff_interface ):
54+ return nic
55+ available = []
56+ for nic in host .network_interfaces :
57+ available .append (f"{ nic .name } ({ nic .pci_address .lspci } )" )
58+ raise RuntimeError (
59+ f"capture_cfg.sniff_interface={ sniff_interface } not found on host { host .name } . "
60+ f"Available interfaces: { ', ' .join (available )} "
61+ )
6262
6363 sniff_interface_index = capture_cfg .get ("sniff_interface_index" )
6464 if sniff_interface_index is not None :
65- return host .network_interfaces [int (sniff_interface_index )]. name
65+ return host .network_interfaces [int (sniff_interface_index )]
6666
6767 sniff_pci_device = capture_cfg .get ("sniff_pci_device" )
6868 if sniff_pci_device :
@@ -72,14 +72,11 @@ def _pci_device_id(nic) -> str:
7272 nic for nic in host .network_interfaces if target == _pci_device_id (nic )
7373 ]
7474 if direct_matches :
75- return (
76- direct_matches [1 ] if len (direct_matches ) > 1 else direct_matches [0 ]
77- ).name
75+ return direct_matches [1 ] if len (direct_matches ) > 1 else direct_matches [0 ]
7876
7977 available = []
8078 for nic in host .network_interfaces :
81- pci_addr = getattr (getattr (nic , "pci_address" , None ), "lspci" , None )
82- available .append (f"{ nic .name } ({ pci_addr } )" )
79+ available .append (f"{ nic .name } ({ nic .pci_address .lspci } )" )
8380 raise RuntimeError (
8481 f"capture_cfg.sniff_pci_device={ sniff_pci_device } not found on host { host .name } . "
8582 f"Available interfaces: { ', ' .join (available )} "
@@ -92,7 +89,82 @@ def _pci_device_id(nic) -> str:
9289 f"Cannot select 2nd PF for capture. Add more interfaces to config or turn off capture."
9390 )
9491
95- return host .network_interfaces [1 ].name
92+ return host .network_interfaces [1 ]
93+
94+
95+ def _select_sniff_interface_name (host , capture_cfg : dict ) -> str :
96+ return _select_sniff_interface (host , capture_cfg ).name
97+
98+
99+ def _select_capture_host (hosts : dict ):
100+ return hosts ["client" ] if "client" in hosts else list (hosts .values ())[0 ]
101+
102+
103+ @pytest .fixture (scope = "session" )
104+ def phc2sys_session (test_config : dict , hosts ):
105+ """Start phc2sys for the capture interface before any tests.
106+
107+ - Uses the same interface selection logic as PCAP capture.
108+ - Detects the PTP Hardware Clock via `ethtool -T`.
109+ - Runs `phc2sys -s /dev/ptpX -c CLOCK_REALTIME -O 0 -m`.
110+
111+ The process is stopped at the end of the session.
112+ """
113+
114+ capture_cfg = test_config .get ("capture_cfg" , {})
115+ if not (capture_cfg and capture_cfg .get ("enable" )):
116+ yield
117+ return
118+
119+ host = _select_capture_host (hosts )
120+ sniff_nic = _select_sniff_interface (host , capture_cfg )
121+ capture_iface = sniff_nic .name
122+
123+ ptp_details = host .connection .execute_command (
124+ f"sudo ethtool -T '{ capture_iface } ' 2>/dev/null || true"
125+ )
126+ ptp_idx = ""
127+ for line in (ptp_details .stdout or "" ).splitlines ():
128+ # Keep this equivalent to: awk -F': ' '/PTP Hardware Clock:/ {print $2; exit}'
129+ if "PTP Hardware Clock:" in line :
130+ ptp_idx = line .split (": " , 1 )[1 ].strip () if ": " in line else ""
131+ break
132+
133+ if not ptp_idx .isdigit ():
134+ raise RuntimeError (
135+ "ERROR: failed to parse PTP Hardware Clock index for "
136+ f"{ capture_iface } . Details: { ptp_details .stdout } { ptp_details .stderr } "
137+ )
138+
139+ capture_ptp = f"/dev/ptp{ ptp_idx } "
140+
141+ logger .info (
142+ f"Starting phc2sys: { capture_ptp } -> CLOCK_REALTIME (iface={ capture_iface } )"
143+ )
144+
145+ log_path = f"/tmp/phc2sys-{ capture_iface } .log"
146+ start_cmd = (
147+ "sudo phc2sys "
148+ f"-s '{ capture_ptp } ' -c CLOCK_REALTIME -O 0 -m "
149+ f"> '{ log_path } ' 2>&1 < /dev/null & echo $!"
150+ )
151+ start_res = host .connection .execute_command (start_cmd )
152+ pid = (
153+ (start_res .stdout or "" ).strip ().splitlines ()[- 1 ].strip ()
154+ if start_res .stdout
155+ else ""
156+ )
157+
158+ if not pid .isdigit ():
159+ raise RuntimeError (
160+ f"Failed to start phc2sys (iface={ capture_iface } , ptp={ capture_ptp } ). "
161+ f"stdout={ start_res .stdout !r} , stderr={ start_res .stderr !r} , log={ log_path } "
162+ )
163+
164+ try :
165+ yield
166+ finally :
167+ host .connection .execute_command (f"sudo kill -SIGINT { pid } || true" )
96168
97169
98170@pytest .hookimpl (wrapper = True , tryfirst = True )
@@ -346,7 +418,7 @@ def log_session():
346418
347419
348420@pytest .fixture (scope = "function" )
349- def pcap_capture (request , media_file , test_config , hosts , mtl_path ):
421+ def pcap_capture (request , media_file , test_config , hosts , mtl_path , phc2sys_session ):
350422 capture_cfg = test_config .get ("capture_cfg" , {})
351423 capturer = None
352424 if capture_cfg and capture_cfg .get ("enable" ):
0 commit comments