Skip to main content

tracexec_core/
timestamp.rs

1use std::{
2  borrow::Cow,
3  sync::LazyLock,
4};
5
6use chrono::{
7  DateTime,
8  Local,
9};
10use nutype::nutype;
11
12#[nutype(
13  validate(with = validate_strftime, error = Cow<'static,str>),
14  derive(Debug, Clone, Serialize, Deserialize, Deref, FromStr)
15)]
16pub struct TimestampFormat(String);
17
18impl Default for TimestampFormat {
19  #[allow(clippy::unwrap_used)]
20  fn default() -> Self {
21    Self::try_new("%H:%M:%S").unwrap()
22  }
23}
24
25fn validate_strftime(fmt: &str) -> Result<(), Cow<'static, str>> {
26  if fmt.contains("\n") {
27    return Err("inline timestamp format string should not contain newline(s)".into());
28  }
29  Ok(())
30}
31
32pub type Timestamp = DateTime<Local>;
33
34pub fn ts_from_boot_ns(boot_ns: u64) -> Timestamp {
35  DateTime::from_timestamp_nanos((*BOOT_TIME + boot_ns) as i64).into()
36}
37
38static BOOT_TIME: LazyLock<u64> = LazyLock::new(|| {
39  let content = std::fs::read_to_string("/proc/stat").expect("Failed to read /proc/stat");
40  for line in content.lines() {
41    if let Some(boot_time) = line
42      .strip_prefix("btime ")
43      .and_then(|value| value.parse::<u64>().ok())
44    {
45      return boot_time * 1_000_000_000;
46    }
47  }
48  panic!("btime is not available in /proc/stat. Am I running on Linux?")
49});
50
51#[cfg(test)]
52mod tests {
53  use std::str::FromStr;
54
55  use chrono::{
56    DateTime,
57    Local,
58  };
59  use test_that::prelude::*;
60
61  use super::*;
62
63  /* ---------------- TimestampFormat ---------------- */
64
65  #[test]
66  fn timestamp_format_accepts_valid_strftime() {
67    let fmt = TimestampFormat::from_str("%Y-%m-%d %H:%M:%S");
68    assert_that!(fmt, ok(anything()));
69  }
70
71  #[test]
72  fn timestamp_format_rejects_newline() {
73    let fmt = TimestampFormat::from_str("%Y-%m-%d\n%H:%M:%S");
74    assert_that!(fmt, err(anything()));
75
76    let err = fmt.unwrap_err();
77    assert_that!(err, contains_substring("should not contain newline"));
78  }
79
80  #[test]
81  fn timestamp_format_deref_works() {
82    let fmt = TimestampFormat::from_str("%s").unwrap();
83    assert_eq!(&*fmt, "%s");
84  }
85
86  /* ---------------- BOOT_TIME ---------------- */
87
88  #[test]
89  fn boot_time_is_non_zero() {
90    assert_that!(*BOOT_TIME, gt(0));
91  }
92
93  #[test]
94  fn boot_time_is_reasonable_unix_time() {
95    // boot time should be after year 2000
96    const YEAR_2000_NS: u64 = 946684800_u64 * 1_000_000_000;
97    assert_that!(*BOOT_TIME, gt(YEAR_2000_NS));
98  }
99
100  /* ---------------- ts_from_boot_ns ---------------- */
101
102  #[test]
103  fn ts_from_boot_ns_zero_matches_boot_time() {
104    let ts = ts_from_boot_ns(0);
105    let expected: DateTime<Local> = DateTime::from_timestamp_nanos(*BOOT_TIME as i64).into();
106
107    assert_eq!(ts, expected);
108  }
109
110  #[test]
111  fn ts_from_boot_ns_is_monotonic() {
112    let t1 = ts_from_boot_ns(1_000);
113    let t2 = ts_from_boot_ns(2_000);
114
115    assert_that!(t2, gt(t1));
116  }
117
118  #[test]
119  fn ts_from_boot_ns_large_offset() {
120    let one_sec = 1_000_000_000;
121    let ts = ts_from_boot_ns(one_sec);
122
123    let base: DateTime<Local> = DateTime::from_timestamp_nanos(*BOOT_TIME as i64).into();
124
125    assert_eq!(ts.timestamp(), base.timestamp() + 1);
126  }
127
128  /* ---------------- serde (nutype derive) ---------------- */
129
130  #[test]
131  fn timestamp_format_serde_roundtrip() {
132    let fmt = TimestampFormat::from_str("%H:%M:%S").unwrap();
133
134    let json = serde_json::to_string(&fmt).unwrap();
135    let de: TimestampFormat = serde_json::from_str(&json).unwrap();
136
137    assert_eq!(&*fmt, &*de);
138  }
139}