use super::tsc::{read_timestamp_counter_begin, read_timestamp_counter_end};
use super::{ClockDisruptionEvent, ControlRequest};
use crate::daemon::{
async_ring_buffer::{self, SendError},
event::{self, PhcData},
time::tsc::TscCount,
time::{Duration, Instant},
};
use libc::c_uint;
use nix::ioctl_readwrite;
use std::fs::File;
use std::os::unix::io::AsRawFd;
use std::path::PathBuf;
use thiserror::Error;
use tokio::{
io,
sync::{mpsc, watch},
task,
time::{Interval, MissedTickBehavior, interval, timeout},
};
use tracing::{debug, error, trace, warn};
const PHC_SOURCE_INTERVAL_DURATION: std::time::Duration = tokio::time::Duration::from_millis(500);
const PHC_SOURCE_TIMEOUT: tokio::time::Duration = tokio::time::Duration::from_millis(20);
const NUM_SAMPLES_PER_POLL: u32 = 5;
const PTP_SYS_OFFSET_EXTENDED2: u32 = 3_300_932_882;
const PTP_MAX_SAMPLES: usize = 25;
#[repr(C)]
#[derive(Debug, Clone, Copy, Default)]
pub struct PtpClockTime {
pub sec: i64,
pub nsec: u32,
pub reserved: u32,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, Default)]
pub struct PtpSysOffsetExtended {
pub n_samples: c_uint,
pub rsv: [c_uint; 3],
pub ts: [[PtpClockTime; 3]; PTP_MAX_SAMPLES],
}
ioctl_readwrite!(
ptp_sys_offset_extended2,
b'=',
PTP_SYS_OFFSET_EXTENDED2,
PtpSysOffsetExtended
);
#[derive(Debug)]
pub enum TryFromPhcSampleError {
InvalidCounterDiff,
InvalidClockErrorBound,
UnexpectedError,
}
#[derive(Debug, Error)]
pub enum PhcError {
#[error("IO failure")]
Io(#[from] io::Error),
#[error("File does not exist")]
FileNotFound(String),
#[error("PTP device not found")]
PtpDeviceNotFound(String),
#[error("PTP device name not found")]
PtpDeviceNameNotFound(String),
#[error("PCI_SLOT_NAME not found in uevent file")]
PciSlotNameNotFound(String),
#[error("PHC clock error bound file not found for PCI slot name {pci_slot_name}")]
PhcClockErrorBoundFileNotFound { pci_slot_name: String },
#[error("PHC clock error bound read failure")]
PhcClockErrorBoundReadFailure(String),
#[error("Tokio spawn JoinError")]
AsyncTaskJoinError(task::JoinError),
#[error("Device driver name not found")]
DeviceDriverNameNotFound(String),
#[error("PhcSample to event::Phc TryFrom error")]
PhcSampleToPhcEventConversionError(TryFromPhcSampleError),
#[error("Unexpected error")]
UnexpectedError(String),
}
impl From<tokio::time::error::Elapsed> for PhcError {
fn from(err: tokio::time::error::Elapsed) -> Self {
PhcError::Io(io::Error::from(err))
}
}
impl From<task::JoinError> for PhcError {
fn from(err: task::JoinError) -> Self {
PhcError::AsyncTaskJoinError(err)
}
}
impl From<nix::Error> for PhcError {
fn from(err: nix::Error) -> Self {
PhcError::Io(io::Error::from_raw_os_error(err as i32))
}
}
#[derive(Debug, Clone, Copy)]
pub struct PhcSample {
pub counter_pre: u64,
pub counter_post: u64,
pub counter_diff: u64,
pub ptp_clock_time: PtpClockTime,
pub ptp_clock_error_bound_nsec: i64,
}
impl TryFrom<PhcSample> for event::Phc {
type Error = TryFromPhcSampleError;
fn try_from(sample: PhcSample) -> Result<Self, Self::Error> {
if sample.counter_post < sample.counter_pre {
return Err(TryFromPhcSampleError::InvalidCounterDiff);
}
if sample.ptp_clock_error_bound_nsec < 0 {
return Err(TryFromPhcSampleError::InvalidClockErrorBound);
}
#[allow(clippy::cast_possible_wrap)]
let tsc_pre = TscCount::new(sample.counter_pre as i64);
#[allow(clippy::cast_possible_wrap)]
let tsc_post = TscCount::new(sample.counter_post as i64);
let builder = event::Phc::builder()
.counter_pre(tsc_pre)
.counter_post(tsc_post)
.data(PhcData {
time: Instant::from_time(sample.ptp_clock_time.sec, sample.ptp_clock_time.nsec),
clock_error_bound: Duration::from_nanos(sample.ptp_clock_error_bound_nsec),
});
match builder.build() {
Some(phc) => Ok(phc),
None => Err(TryFromPhcSampleError::UnexpectedError),
}
}
}
#[derive(Debug)]
pub struct Phc {
event_sender: async_ring_buffer::Sender<event::Phc>,
ctrl_receiver: mpsc::Receiver<ControlRequest>,
clock_disruption_receiver: watch::Receiver<ClockDisruptionEvent>,
interval: Interval,
reader: PhcReader,
clock_error_bound_reader: PhcClockErrorBoundReader,
device_path: String,
}
impl Phc {
pub async fn construct(
event_sender: async_ring_buffer::Sender<event::Phc>,
ctrl_receiver: mpsc::Receiver<ControlRequest>,
clock_disruption_receiver: watch::Receiver<ClockDisruptionEvent>,
) -> Result<Self, PhcError> {
let (phc_reader, phc_clock_error_bound_reader, device_path) =
Self::autoconfigure_phc_readers().await?;
let mut phc_interval = interval(PHC_SOURCE_INTERVAL_DURATION);
phc_interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
Ok(Phc {
event_sender,
ctrl_receiver,
clock_disruption_receiver,
interval: phc_interval,
reader: phc_reader,
clock_error_bound_reader: phc_clock_error_bound_reader,
device_path,
})
}
pub fn device_path(&self) -> &str {
&self.device_path
}
pub async fn autoconfigure_phc_readers()
-> Result<(PhcReader, PhcClockErrorBoundReader, String), PhcError> {
tokio::task::spawn_blocking(Self::blocking_autoconfigure_phc_readers)
.await
.map_err(|e| {
PhcError::Io(io::Error::other(format!(
"PHC reader autoconfiguration task panicked: {e:?}"
)))
})?
}
#[expect(
clippy::too_many_lines,
reason = "This function is already refactored to call
separate functions for specific functionality. The big for loop is needed
because we have many `continue` statements in the loop body, and other alternate
approaches would make the code harder to follow than the current implementation."
)]
fn blocking_autoconfigure_phc_readers()
-> Result<(PhcReader, PhcClockErrorBoundReader, String), PhcError> {
let network_interfaces = match get_network_interfaces() {
Ok(network_interfaces) => network_interfaces,
Err(e) => {
warn!(
error = ?e,
"PHC reader autoconfiguration failed due to inability to get the list of network interfaces."
);
return Err(e);
}
};
let mut ptp_device_path_and_phc_clock_error_bound_sysfs_path_vec: Vec<(String, String)> =
Vec::new();
for network_interface in network_interfaces {
debug!(
?network_interface,
"Gathering information on network interface.",
);
let uevent_file_path =
match get_uevent_file_path_for_network_interface(&network_interface) {
Ok(uevent_file_path) => {
debug!(
?network_interface,
?uevent_file_path,
"Network interface association with uevent file path."
);
uevent_file_path
}
Err(e) => {
debug!(error = ?e, ?network_interface,
"Uevent file not found for network interface."
);
continue;
}
};
let is_ena = match is_ena_network_interface(&uevent_file_path) {
Ok(is_ena) => {
debug!(
?network_interface,
?is_ena,
"Network interface driver details."
);
is_ena
}
Err(e) => {
debug!(error = ?e, ?network_interface,
"Failed to determine if network interface driver is ENA.",
);
continue;
}
};
if !is_ena {
debug!(
?network_interface,
?is_ena,
"Network interface does not use the ENA driver. Skipping.",
);
continue;
}
let pci_slot_name = match get_pci_slot_name(&uevent_file_path) {
Ok(pci_slot_name) => {
debug!(
?network_interface,
?pci_slot_name,
"Network interface association with PCI slot name.",
);
pci_slot_name
}
Err(e) => {
debug!(error = ?e, ?uevent_file_path,
"PCI slot name not found for uevent file path.",
);
continue;
}
};
let phc_clock_error_bound_sysfs_path =
match get_phc_clock_error_bound_sysfs_path(&pci_slot_name) {
Ok(phc_clock_error_bound_sysfs_path) => {
debug!(
?network_interface,
?phc_clock_error_bound_sysfs_path,
"Network interface association with PHC clock error bound sysfs path.",
);
phc_clock_error_bound_sysfs_path
}
Err(e) => {
debug!(
error = ?e, ?pci_slot_name,
"PHC clock error bound sysfs path not found for PCI slot name.",
);
continue;
}
};
let ptp_uevent_file_paths = match get_ptp_uevent_file_paths_for_pci_slot(&pci_slot_name)
{
Ok(ptp_uevent_file_paths) => {
debug!(
?network_interface,
?ptp_uevent_file_paths,
"Network interface association with PTP uevent file paths.",
);
ptp_uevent_file_paths
}
Err(e) => {
debug!(
error = ?e, ?pci_slot_name,
"PTP uevent file paths not found for PCI slot name.",
);
continue;
}
};
for ptp_uevent_file_path in ptp_uevent_file_paths {
let ptp_device_name =
match get_ptp_device_name_from_uevent_file(&ptp_uevent_file_path) {
Ok(ptp_device_name) => {
debug!(
?network_interface,
?ptp_device_name,
"Network interface association with PTP device name.",
);
ptp_device_name
}
Err(e) => {
debug!(
error = ?e, ?ptp_uevent_file_path,
"Device name not found for PTP uevent file path.",
);
continue;
}
};
let ptp_device_path = match get_ptp_device_path(&ptp_device_name) {
Ok(ptp_device_path) => {
debug!(
?network_interface,
?ptp_device_path,
"Network interface association with PTP device path.",
);
ptp_device_path
}
Err(e) => {
debug!(error = ?e, ?ptp_device_name,
"Device path not found for PTP device name.",
);
continue;
}
};
ptp_device_path_and_phc_clock_error_bound_sysfs_path_vec
.push((ptp_device_path, phc_clock_error_bound_sysfs_path.clone()));
}
}
ptp_device_path_and_phc_clock_error_bound_sysfs_path_vec.sort();
debug!(
?ptp_device_path_and_phc_clock_error_bound_sysfs_path_vec,
"Candidate PHC devices sorted."
);
if let Some((ptp_device_path, phc_clock_error_bound_sysfs_path)) =
ptp_device_path_and_phc_clock_error_bound_sysfs_path_vec
.into_iter()
.next()
{
debug!(
?ptp_device_path,
?phc_clock_error_bound_sysfs_path,
"Configuring PHC readers."
);
let ptp_device_file = match File::open(ptp_device_path.clone()) {
Ok(file) => file,
Err(e) => {
let error_detail =
format!("Failed to open PTP device file: {ptp_device_path:?} {e:?}");
return Err(PhcError::Io(io::Error::new(e.kind(), error_detail)));
}
};
let phc_reader: PhcReader = PhcReader::new(ptp_device_file);
let phc_clock_error_bound_reader: PhcClockErrorBoundReader =
PhcClockErrorBoundReader::new(PathBuf::from(&phc_clock_error_bound_sysfs_path));
debug!(
?ptp_device_path,
?phc_clock_error_bound_sysfs_path,
"Done configuring PHC readers."
);
Ok((phc_reader, phc_clock_error_bound_reader, ptp_device_path))
} else {
Err(PhcError::PtpDeviceNotFound(
"No eligible PTP devices found".to_string(),
))
}
}
async fn sample(&mut self) -> Result<event::Phc, PhcError> {
let mut last_phc_error: Option<PhcError> = None;
let mut best_sample: Option<PhcSample> = None;
let mut best_sample_i: u32 = 0;
debug!("Reading {} PHC samples.", NUM_SAMPLES_PER_POLL);
for i in 0..NUM_SAMPLES_PER_POLL {
let sample = match self.read_phc_sample().await {
Ok(sample) => {
debug!(?i, ?sample, "Successfully read PHC sample.");
sample
}
Err(e) => {
debug!(?i, error = ?e, "PHC sample read failure.");
last_phc_error = Some(e);
continue;
}
};
if let Some(bs) = best_sample {
if sample.counter_diff < bs.counter_diff {
best_sample = Some(sample);
best_sample_i = i;
}
} else {
best_sample = Some(sample);
best_sample_i = i;
}
}
if let Some(best_sample) = best_sample {
trace!(?best_sample_i, ?best_sample, "Selected best PHC sample.");
match event::Phc::try_from(best_sample) {
Ok(phc) => Ok(phc),
Err(e) => Err(PhcError::PhcSampleToPhcEventConversionError(e)),
}
} else if let Some(last_phc_error) = last_phc_error {
Err(last_phc_error)
} else {
unreachable!("PHC IO source: unexpected unreachable");
}
}
async fn read_phc_sample(&self) -> Result<PhcSample, PhcError> {
let (ptp_clock_time, counter_pre, counter_post) = self
.reader
.ptp_sys_offset_extended2_with_counter_pre_and_counter_post(PHC_SOURCE_TIMEOUT)
.await?;
let ptp_clock_error_bound_nsec: i64 = match self.clock_error_bound_reader.read().await {
Ok(ceb_nsec) => {
if ceb_nsec < 0 {
return Err(PhcError::PhcClockErrorBoundReadFailure(format!(
"Negative PHC clock error bound: {ceb_nsec}"
)));
}
ceb_nsec
}
Err(e) => {
return Err(PhcError::PhcClockErrorBoundReadFailure(e.clone()));
}
};
Ok(PhcSample {
counter_pre,
counter_post,
counter_diff: counter_post - counter_pre,
ptp_clock_time,
ptp_clock_error_bound_nsec,
})
}
pub async fn run(&mut self) {
debug!("Starting PHC sampling loop.");
loop {
tokio::select! {
biased; val = self.clock_disruption_receiver.changed() => {
if let Err(e) = val {
tracing::error!(?e, "Clock disruption receiver dropped.");
break;
}
debug!("Received clock disruption signal.");
self.handle_disruption();
}
ctrl_req = self.ctrl_receiver.recv() => {
match ctrl_req {
None => {
break;
},
Some(ControlRequest::Shutdown) => {
debug!("Received shutdown signal. Exiting.");
break;
},
}
}
_ = self.interval.tick() => {
self.handle_interval_tick().await;
}
}
}
debug!("PHC runner exiting.");
}
async fn handle_interval_tick(&mut self) {
let phc_event = match self.sample().await {
Ok(phc_event) => phc_event,
Err(e) => {
debug!(error = ?e, "Failed to sample PHC source.");
return;
}
};
match self.event_sender.send(phc_event.clone()) {
Ok(()) => debug!(?phc_event, "Successfully sent PHC IO event."),
Err(SendError::Disrupted(_)) => {
debug!("Trying to send when there was a disruption event.");
}
Err(SendError::BufferClosed(e)) => {
error!(?e, "PHC channel closed. Not supported in alpha.");
panic!("PHC IO source unable to communicate with daemon. {e:?}");
}
}
}
fn handle_disruption(&mut self) {
let Self {
clock_disruption_receiver: _,
event_sender,
ctrl_receiver: _,
interval,
reader: _,
clock_error_bound_reader: _,
device_path: _,
} = self;
interval.reset();
event_sender.handle_disruption();
tracing::debug!("PHC IO handled disruption.");
}
}
#[cfg_attr(test, mockall::automock)]
mod filesystem {
#![allow(dead_code, reason = "cfg_attr too complicated to me to use expect")]
pub fn fs_try_exists(path: &String) -> std::io::Result<bool> {
std::fs::exists(path)
}
}
#[cfg(not(test))]
use filesystem::fs_try_exists;
#[cfg(test)]
use mock_filesystem::fs_try_exists;
#[cfg_attr(test, mockall::automock)]
mod phc_path_locator {
use crate::daemon::io::phc::PhcError;
use crate::daemon::io::phc::fs_try_exists;
pub fn get_network_interfaces() -> Result<Vec<String>, PhcError> {
let mut network_interfaces = Vec::new();
let network_interfaces_path = "/sys/class/net/";
if !fs_try_exists(&network_interfaces_path.to_string())? {
return Err(PhcError::FileNotFound(network_interfaces_path.into()));
}
let entries = match std::fs::read_dir(network_interfaces_path) {
Ok(entries) => entries,
Err(e) => return Err(PhcError::Io(e)),
};
for entry in entries {
let entry = match entry {
Ok(entry) => entry,
Err(e) => return Err(PhcError::Io(e)),
};
let file_name = entry.file_name().to_string_lossy().to_string();
network_interfaces.push(file_name);
}
tracing::debug!(?network_interfaces);
Ok(network_interfaces)
}
pub fn get_uevent_file_path_for_network_interface(
network_interface: &str,
) -> Result<String, PhcError> {
let uevent_file_path = format!("/sys/class/net/{network_interface}/device/uevent");
if !fs_try_exists(&uevent_file_path)? {
return Err(PhcError::FileNotFound(uevent_file_path));
}
Ok(uevent_file_path)
}
pub fn is_ena_network_interface(uevent_file_path: &str) -> Result<bool, PhcError> {
let contents = match std::fs::read_to_string(uevent_file_path) {
Ok(contents) => contents,
Err(e) => return Err(PhcError::Io(e)),
};
let driver_name = contents
.lines()
.find_map(|line| line.strip_prefix("DRIVER="))
.ok_or_else(|| {
PhcError::DeviceDriverNameNotFound(format!(
"Failed to find DRIVER at uevent file path {uevent_file_path}"
))
})
.map(std::string::ToString::to_string)?;
tracing::debug!(
?uevent_file_path,
?driver_name,
"uevent file association with DRIVER value"
);
Ok(driver_name == "ena")
}
pub fn get_pci_slot_name(uevent_file_path: &str) -> Result<String, PhcError> {
let contents = match std::fs::read_to_string(uevent_file_path) {
Ok(contents) => contents,
Err(e) => return Err(PhcError::Io(e)),
};
let pci_slot_name = contents
.lines()
.find_map(|line| line.strip_prefix("PCI_SLOT_NAME="))
.ok_or_else(|| {
PhcError::PciSlotNameNotFound(format!(
"Failed to find PCI_SLOT_NAME at uevent file path {uevent_file_path}"
))
})
.map(std::string::ToString::to_string)?;
tracing::debug!(
?uevent_file_path,
?pci_slot_name,
"uevent file association with PCI_SLOT_NAME value"
);
Ok(pci_slot_name)
}
pub fn get_ptp_uevent_file_paths_for_pci_slot(
pci_slot_name: &str,
) -> Result<Vec<String>, PhcError> {
let mut uevent_file_paths = Vec::new();
let uevent_file_search_path = format!("/sys/bus/pci/devices/{pci_slot_name}/ptp/");
if !fs_try_exists(&uevent_file_search_path)? {
return Err(PhcError::FileNotFound(uevent_file_search_path));
}
let entries = match std::fs::read_dir(&uevent_file_search_path) {
Ok(entries) => entries,
Err(e) => return Err(PhcError::Io(e)),
};
for entry in entries {
let entry = match entry {
Ok(entry) => entry,
Err(e) => return Err(PhcError::Io(e)),
};
let entry_name = entry.file_name().to_string_lossy().to_string();
if entry_name.starts_with("ptp") {
let uevent_path = format!("{uevent_file_search_path}{entry_name}/uevent");
if fs_try_exists(&uevent_path)? {
uevent_file_paths.push(uevent_path);
}
}
}
tracing::debug!(?uevent_file_paths, "PTP uevent file paths");
Ok(uevent_file_paths)
}
pub fn get_ptp_device_name_from_uevent_file(
uevent_file_path: &str,
) -> Result<String, PhcError> {
let contents = match std::fs::read_to_string(uevent_file_path) {
Ok(contents) => contents,
Err(e) => return Err(PhcError::Io(e)),
};
let ptp_device_name = contents
.lines()
.find_map(|line| line.strip_prefix("DEVNAME="))
.ok_or_else(|| {
PhcError::PtpDeviceNameNotFound(format!(
"Failed to find DEVNAME at uevent file path {uevent_file_path}"
))
})
.map(std::string::ToString::to_string)?;
tracing::debug!(
?uevent_file_path,
?ptp_device_name,
"uevent file assocation with DEVNAME value"
);
Ok(ptp_device_name)
}
pub fn get_ptp_device_path(ptp_device_name: &str) -> Result<String, PhcError> {
let ptp_device_path = format!("/dev/{ptp_device_name}");
if !fs_try_exists(&ptp_device_path)? {
return Err(PhcError::PtpDeviceNotFound(format!(
"Failed to find PTP device at path {ptp_device_path}"
)));
}
tracing::debug!(
?ptp_device_name,
?ptp_device_path,
"PTP device name association with PTP device path"
);
Ok(ptp_device_path)
}
pub fn get_phc_clock_error_bound_sysfs_path(pci_slot_name: &str) -> Result<String, PhcError> {
let phc_clock_error_bound_sysfs_path =
format!("/sys/bus/pci/devices/{pci_slot_name}/phc_error_bound");
if !fs_try_exists(&phc_clock_error_bound_sysfs_path)? {
return Err(PhcError::PhcClockErrorBoundFileNotFound {
pci_slot_name: pci_slot_name.into(),
});
}
tracing::debug!(
?pci_slot_name,
?phc_clock_error_bound_sysfs_path,
"PCI slot name assocation with PHC clock error bound sysfs path"
);
Ok(phc_clock_error_bound_sysfs_path)
}
}
use phc_path_locator::{
get_network_interfaces, get_pci_slot_name, get_phc_clock_error_bound_sysfs_path,
get_ptp_device_name_from_uevent_file, get_ptp_device_path,
get_ptp_uevent_file_paths_for_pci_slot, get_uevent_file_path_for_network_interface,
is_ena_network_interface,
};
#[derive(Debug)]
pub struct PhcReader {
phc_device_file: File,
}
#[cfg_attr(test, mockall::automock)]
impl PhcReader {
fn new(phc_device_file: File) -> Self {
Self { phc_device_file }
}
async fn ptp_sys_offset_extended2_with_counter_pre_and_counter_post(
&self,
timeout_duration: tokio::time::Duration,
) -> Result<(PtpClockTime, u64, u64), PhcError> {
let mut ptp_sys_offset_extended = PtpSysOffsetExtended {
n_samples: 1,
..Default::default()
};
let phc_device_fd = self.phc_device_file.as_raw_fd();
let result = timeout(
timeout_duration,
task::spawn_blocking(move || {
let counter_pre = read_timestamp_counter_begin();
let result = unsafe {
ptp_sys_offset_extended2(phc_device_fd, &raw mut ptp_sys_offset_extended)
};
let ptp_clock_time = match result {
Ok(_) => ptp_sys_offset_extended.ts[0][1],
Err(e) => {
return Err(e);
}
};
let counter_post = read_timestamp_counter_end();
Ok((ptp_clock_time, counter_pre, counter_post))
}),
)
.await?;
result?.map_err(PhcError::from)
}
}
#[derive(Debug, Clone, Default)]
pub struct PhcClockErrorBoundReader {
sysfs_phc_error_bound_path: PathBuf,
}
#[cfg_attr(test, mockall::automock)]
impl PhcClockErrorBoundReader {
pub(crate) fn new(phc_clock_error_bound_path: PathBuf) -> Self {
Self {
sysfs_phc_error_bound_path: phc_clock_error_bound_path,
}
}
pub(crate) async fn read(&self) -> Result<i64, String> {
let contents = tokio::fs::read_to_string(&self.sysfs_phc_error_bound_path)
.await
.map_err(|e| e.to_string())?;
contents
.trim()
.parse::<i64>()
.map_err(|e| format!("Failed to parse PHC error bound value to i64: {e}"))
}
}
#[cfg(test)]
mod test {
use rstest::rstest;
use tempfile::NamedTempFile;
use super::*;
use crate::daemon::event;
use std::io::Write;
#[rstest]
#[case::happy_path("PCI_SLOT_NAME=12345", "12345")]
#[case::happy_path_multi_line(
"
oneline
PCI_SLOT_NAME=23456
twoline",
"23456"
)]
fn get_pci_slot_name_success(#[case] file_contents_to_write: &str, #[case] return_value: &str) {
let mut test_uevent_file = NamedTempFile::new().expect("create mock uevent file failed");
test_uevent_file
.write_all(file_contents_to_write.as_bytes())
.expect("write to mock uevent file failed");
let rt = phc_path_locator::get_pci_slot_name(test_uevent_file.path().to_str().unwrap());
assert!(rt.is_ok());
assert_eq!(rt.unwrap(), return_value.to_string());
}
#[rstest]
#[case::missing_pci_slot_name("no pci slot name")]
fn get_pci_slot_name_failure(#[case] file_contents_to_write: &str) {
let mut test_uevent_file = NamedTempFile::new().expect("create mock uevent file failed");
test_uevent_file
.write_all(file_contents_to_write.as_bytes())
.expect("write to mock uevent file failed");
let rt = phc_path_locator::get_pci_slot_name(test_uevent_file.path().to_str().unwrap());
assert!(rt.is_err());
match rt.unwrap_err() {
PhcError::PciSlotNameNotFound(_) => assert!(true),
_ => assert!(false),
};
}
#[test]
fn get_pci_slot_name_file_does_not_exist() {
let rt = phc_path_locator::get_pci_slot_name("/does/not/exist");
assert!(rt.is_err());
}
#[tokio::test]
#[rstest]
#[case::happy_path("12345", 12345)]
async fn read_phc_error_bound_success(
#[case] file_contents_to_write: &str,
#[case] return_value: i64,
) {
let mut test_phc_error_bound_file =
NamedTempFile::new().expect("create mock phc error bound file failed");
test_phc_error_bound_file
.write_all(file_contents_to_write.as_bytes())
.expect("write to mock phc error bound file failed");
let phc_error_bound_reader =
PhcClockErrorBoundReader::new(test_phc_error_bound_file.path().to_path_buf());
let rt = phc_error_bound_reader.read().await;
assert!(rt.is_ok());
assert_eq!(rt.unwrap(), return_value);
}
#[tokio::test]
#[rstest]
#[case::parsing_fail("asdf_not_an_i64")]
async fn read_phc_error_bound_bad_file_contents(#[case] file_contents_to_write: &str) {
let mut test_phc_error_bound_file =
NamedTempFile::new().expect("create mock phc error bound file failed");
test_phc_error_bound_file
.write_all(file_contents_to_write.as_bytes())
.expect("write to mock phc error bound file failed");
let phc_error_bound_reader =
PhcClockErrorBoundReader::new(test_phc_error_bound_file.path().to_path_buf());
let rt = phc_error_bound_reader.read().await;
assert!(rt.is_err());
assert!(
rt.unwrap_err()
.to_string()
.contains("Failed to parse PHC error bound value to i64")
);
}
#[tokio::test]
async fn read_phc_error_bound_file_does_not_exist() {
let phc_error_bound_reader = PhcClockErrorBoundReader::new("/does/not/exist".into());
let rt = phc_error_bound_reader.read().await;
assert!(rt.is_err());
}
#[test]
fn phc_clock_error_bound_reader_new() {
let path = PathBuf::from("/test/path");
let reader = PhcClockErrorBoundReader::new(path.clone());
assert_eq!(reader.sysfs_phc_error_bound_path, path);
}
#[test]
fn phc_sample_try_from_success() {
let sample = PhcSample {
counter_pre: 100,
counter_post: 200,
counter_diff: 100,
ptp_clock_time: PtpClockTime {
sec: 1234567890,
nsec: 123456789,
reserved: 0,
},
ptp_clock_error_bound_nsec: 1000,
};
let result = event::Phc::try_from(sample);
assert!(result.is_ok());
}
#[test]
fn phc_sample_try_from_invalid_counter_diff() {
let sample = PhcSample {
counter_pre: 200,
counter_post: 100,
counter_diff: 0,
ptp_clock_time: PtpClockTime {
sec: 1234567890,
nsec: 123456789,
reserved: 0,
},
ptp_clock_error_bound_nsec: 1000,
};
let result = event::Phc::try_from(sample);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
TryFromPhcSampleError::InvalidCounterDiff
));
}
#[test]
fn phc_sample_try_from_invalid_clock_error_bound() {
let sample = PhcSample {
counter_pre: 100,
counter_post: 200,
counter_diff: 100,
ptp_clock_time: PtpClockTime {
sec: 1234567890,
nsec: 123456789,
reserved: 0,
},
ptp_clock_error_bound_nsec: -1000,
};
let result = event::Phc::try_from(sample);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
TryFromPhcSampleError::InvalidClockErrorBound
));
}
#[test]
fn get_network_interfaces_directory_not_found() {
let ctx = mock_filesystem::fs_try_exists_context();
ctx.expect().returning(|_| Ok(false));
let result = phc_path_locator::get_network_interfaces();
assert!(result.is_err());
match result.unwrap_err() {
PhcError::FileNotFound(path) => {
assert_eq!(path, "/sys/class/net/");
}
_ => panic!("Expected FileNotFound error"),
}
}
#[test]
fn get_uevent_file_path_for_network_interface_success() {
let ctx = mock_filesystem::fs_try_exists_context();
ctx.expect().returning(|_| Ok(true));
let result = phc_path_locator::get_uevent_file_path_for_network_interface("eth0");
assert!(result.is_ok());
assert_eq!(result.unwrap(), "/sys/class/net/eth0/device/uevent");
}
#[rstest]
#[case::ena_driver("DRIVER=ena", true)]
#[case::other_driver("DRIVER=e1000e", false)]
#[case::multiline_ena("SUBSYSTEM=net\nDRIVER=ena\nOTHER=value", true)]
fn is_ena_network_interface_success(#[case] file_contents: &str, #[case] expected: bool) {
let mut test_file = NamedTempFile::new().expect("create temp file failed");
test_file
.write_all(file_contents.as_bytes())
.expect("write to temp file failed");
let result = phc_path_locator::is_ena_network_interface(test_file.path().to_str().unwrap());
assert!(result.is_ok());
assert_eq!(result.unwrap(), expected);
}
#[test]
fn is_ena_network_interface_no_driver() {
let mut test_file = NamedTempFile::new().expect("create temp file failed");
test_file
.write_all(b"SUBSYSTEM=net\nOTHER=value")
.expect("write to temp file failed");
let result = phc_path_locator::is_ena_network_interface(test_file.path().to_str().unwrap());
assert!(result.is_err());
match result.unwrap_err() {
PhcError::DeviceDriverNameNotFound(_) => {}
_ => panic!("Expected DeviceDriverNameNotFound error"),
}
}
#[test]
#[ignore = "static mocks require synchronization"]
fn get_ptp_uevent_file_paths_for_pci_slot_directory_not_found() {
let ctx = mock_filesystem::fs_try_exists_context();
ctx.expect().returning(|_| Ok(false));
let result = phc_path_locator::get_ptp_uevent_file_paths_for_pci_slot("test_slot");
assert!(result.is_err());
match result.unwrap_err() {
PhcError::FileNotFound(path) => {
assert_eq!(path, "/sys/bus/pci/devices/test_slot/ptp/");
}
_ => panic!("Expected FileNotFound error"),
}
}
#[rstest]
#[case::simple_devname("DEVNAME=ptp0", "ptp0")]
#[case::multiline_devname("SUBSYSTEM=ptp\nDEVNAME=ptp1\nOTHER=value", "ptp1")]
fn get_ptp_device_name_from_uevent_file_success(
#[case] file_contents: &str,
#[case] expected: &str,
) {
let mut test_file = NamedTempFile::new().expect("create temp file failed");
test_file
.write_all(file_contents.as_bytes())
.expect("write to temp file failed");
let result = phc_path_locator::get_ptp_device_name_from_uevent_file(
test_file.path().to_str().unwrap(),
);
assert!(result.is_ok());
assert_eq!(result.unwrap(), expected);
}
#[test]
fn get_ptp_device_name_from_uevent_file_no_devname() {
let mut test_file = NamedTempFile::new().expect("create temp file failed");
test_file
.write_all(b"SUBSYSTEM=ptp\nOTHER=value")
.expect("write to temp file failed");
let result = phc_path_locator::get_ptp_device_name_from_uevent_file(
test_file.path().to_str().unwrap(),
);
assert!(result.is_err());
match result.unwrap_err() {
PhcError::PtpDeviceNameNotFound(_) => {}
_ => panic!("Expected PtpDeviceNameNotFound error"),
}
}
#[test]
fn get_phc_clock_error_bound_sysfs_path_not_found() {
let ctx = mock_filesystem::fs_try_exists_context();
ctx.expect().returning(|_| Ok(false));
let result = phc_path_locator::get_phc_clock_error_bound_sysfs_path("0000:27:00.0");
assert!(result.is_err());
match result.unwrap_err() {
PhcError::PhcClockErrorBoundFileNotFound { pci_slot_name } => {
assert_eq!(pci_slot_name, "0000:27:00.0");
}
_ => panic!("Expected PhcClockErrorBoundFileNotFound error"),
}
}
}