Skip to content

Commit 0ea29e3

Browse files
test(supervisor): trim and separate log retention regressions
Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
1 parent e17204f commit 0ea29e3

2 files changed

Lines changed: 128 additions & 195 deletions

File tree

‎crates/openshell-supervisor/src/log_files.rs‎

Lines changed: 1 addition & 195 deletions
Original file line numberDiff line numberDiff line change
@@ -82,198 +82,4 @@ fn migrate_legacy_ocsf_logs(directory: &Path) -> io::Result<()> {
8282
}
8383

8484
#[cfg(test)]
85-
mod tests {
86-
use super::*;
87-
use std::collections::BTreeMap;
88-
use std::ffi::OsString;
89-
use std::io::Write as _;
90-
use std::time::{Duration, Instant};
91-
92-
fn history(directory: &Path, format: Format) -> BTreeMap<OsString, Vec<u8>> {
93-
let (prefix, suffix) = format.filename_parts();
94-
fs::read_dir(directory)
95-
.unwrap()
96-
.map(Result::unwrap)
97-
.filter(|entry| {
98-
let name = entry.file_name();
99-
let name = name.to_string_lossy();
100-
name.starts_with(&format!("{prefix}.")) && name.ends_with(&format!(".{suffix}"))
101-
})
102-
.map(|entry| (entry.file_name(), fs::read(entry.path()).unwrap()))
103-
.collect()
104-
}
105-
106-
fn seed_history(directory: &Path, format: Format, minutely: bool) {
107-
let (prefix, suffix) = format.filename_parts();
108-
for day in 1..=3 {
109-
let date = format!("2000-01-{day:02}");
110-
let date = if minutely {
111-
format!("{date}-00-00")
112-
} else {
113-
date
114-
};
115-
let filename = format!("{prefix}.{date}.{suffix}");
116-
fs::write(directory.join(&filename), filename.as_bytes()).unwrap();
117-
}
118-
}
119-
120-
#[test]
121-
fn suffixes_separate_retention_before_timestamp_selection() {
122-
// These checks precede both metadata.created() and date parsing in
123-
// tracing-appender, so separation does not depend on the filesystem.
124-
let (text_prefix, text_suffix) = Format::Shorthand.filename_parts();
125-
let (json_prefix, json_suffix) = Format::Ocsf.filename_parts();
126-
let text = format!("{text_prefix}.2000-01-01.{text_suffix}");
127-
let json = format!("{json_prefix}.2000-01-01.{json_suffix}");
128-
assert!(!json.ends_with(text_suffix));
129-
assert!(!text.ends_with(json_suffix));
130-
}
131-
132-
#[test]
133-
fn daily_initialization_retains_each_format_independently() {
134-
for formats in [
135-
[Format::Shorthand, Format::Ocsf],
136-
[Format::Ocsf, Format::Shorthand],
137-
] {
138-
let directory = tempfile::tempdir().unwrap();
139-
for format in formats {
140-
seed_history(directory.path(), format, false);
141-
}
142-
eprintln!(
143-
"log filesystem exposes creation timestamps: {}",
144-
fs::metadata(directory.path().join("openshell.2000-01-01.log"))
145-
.unwrap()
146-
.created()
147-
.is_ok()
148-
);
149-
for (format, other) in [(formats[0], formats[1]), (formats[1], formats[0])] {
150-
let other_history = history(directory.path(), other);
151-
let mut writer = appender(directory.path(), format, Rotation::DAILY).unwrap();
152-
writer.write_all(b"new event\n").unwrap();
153-
writer.flush().unwrap();
154-
assert_eq!(history(directory.path(), other), other_history);
155-
assert_eq!(history(directory.path(), format).len(), 3);
156-
}
157-
}
158-
}
159-
160-
#[test]
161-
fn legacy_jsonl_history_is_migrated_before_shorthand_pruning() {
162-
let directory = tempfile::tempdir().unwrap();
163-
seed_history(directory.path(), Format::Shorthand, false);
164-
for day in 1..=3 {
165-
fs::write(
166-
directory
167-
.path()
168-
.join(format!("openshell-ocsf.2000-01-{day:02}.log")),
169-
format!("legacy event {day}\n"),
170-
)
171-
.unwrap();
172-
}
173-
let _writer = appender(directory.path(), Format::Shorthand, Rotation::DAILY).unwrap();
174-
assert_eq!(history(directory.path(), Format::Shorthand).len(), 3);
175-
assert_eq!(history(directory.path(), Format::Ocsf).len(), 3);
176-
for day in 1..=3 {
177-
let legacy = directory
178-
.path()
179-
.join(format!("openshell-ocsf.2000-01-{day:02}.log"));
180-
assert!(!legacy.exists());
181-
assert_eq!(
182-
fs::read_to_string(legacy.with_extension("jsonl")).unwrap(),
183-
format!("legacy event {day}\n")
184-
);
185-
}
186-
// Successful migration is idempotent on subsequent starts.
187-
migrate_legacy_ocsf_logs(directory.path()).unwrap();
188-
}
189-
190-
#[test]
191-
fn migration_collision_preserves_both_files_and_prevents_pruning() {
192-
let directory = tempfile::tempdir().unwrap();
193-
seed_history(directory.path(), Format::Shorthand, false);
194-
let before = history(directory.path(), Format::Shorthand);
195-
let legacy = directory.path().join("openshell-ocsf.2000-01-01.log");
196-
let current = legacy.with_extension("jsonl");
197-
fs::write(&legacy, b"legacy security records").unwrap();
198-
fs::write(&current, b"new security records").unwrap();
199-
for format in [Format::Shorthand, Format::Ocsf] {
200-
let error = appender(directory.path(), format, Rotation::DAILY).unwrap_err();
201-
assert_eq!(error.kind(), io::ErrorKind::AlreadyExists);
202-
assert_eq!(history(directory.path(), Format::Shorthand), before);
203-
assert_eq!(fs::read(&legacy).unwrap(), b"legacy security records");
204-
assert_eq!(fs::read(&current).unwrap(), b"new security records");
205-
}
206-
}
207-
208-
#[test]
209-
fn migration_leaves_unrelated_files_and_directories_untouched() {
210-
let directory = tempfile::tempdir().unwrap();
211-
let names = [
212-
"openshell.2000-01-01.log",
213-
"openshell-ocsf.2000-01-01.jsonl",
214-
"openshell-ocsf.2000-01-01.log.bak",
215-
"openshell-ocsf.2000-01-01.LOG",
216-
"other.2000-01-01.log",
217-
];
218-
for name in names {
219-
fs::write(directory.path().join(name), name.as_bytes()).unwrap();
220-
}
221-
let nested = directory.path().join("openshell-ocsf.2000-01-02.log");
222-
fs::create_dir(&nested).unwrap();
223-
migrate_legacy_ocsf_logs(directory.path()).unwrap();
224-
for name in names {
225-
assert_eq!(
226-
fs::read(directory.path().join(name)).unwrap(),
227-
name.as_bytes()
228-
);
229-
}
230-
assert!(nested.is_dir());
231-
}
232-
233-
#[test]
234-
fn missing_log_directory_is_created() {
235-
let directory = tempfile::tempdir().unwrap();
236-
let logs = directory.path().join("logs");
237-
let _writer = appender(&logs, Format::Shorthand, Rotation::DAILY).unwrap();
238-
assert_eq!(history(&logs, Format::Shorthand).len(), 1);
239-
}
240-
241-
fn write_until_rollover(
242-
writer: &mut RollingFileAppender,
243-
directory: &Path,
244-
format: Format,
245-
deadline: Instant,
246-
) {
247-
let before = history(directory, format);
248-
loop {
249-
writer.write_all(b"new event\n").unwrap();
250-
writer.flush().unwrap();
251-
let current = history(directory, format);
252-
if current.keys().ne(before.keys()) {
253-
assert_eq!(current.len(), 3);
254-
return;
255-
}
256-
assert!(Instant::now() < deadline, "no rollover within 65 seconds");
257-
std::thread::sleep(Duration::from_millis(100));
258-
}
259-
}
260-
261-
#[test]
262-
fn rollover_retains_each_format_independently() {
263-
// MINUTELY and DAILY share the same pruning path. The dependency's
264-
// mock clock is private, so cross a real minute boundary for this test.
265-
let directory = tempfile::tempdir().unwrap();
266-
for format in [Format::Shorthand, Format::Ocsf] {
267-
seed_history(directory.path(), format, true);
268-
}
269-
let mut text = appender(directory.path(), Format::Shorthand, Rotation::MINUTELY).unwrap();
270-
let mut json = appender(directory.path(), Format::Ocsf, Rotation::MINUTELY).unwrap();
271-
let deadline = Instant::now() + Duration::from_secs(65);
272-
let before = history(directory.path(), Format::Ocsf);
273-
write_until_rollover(&mut text, directory.path(), Format::Shorthand, deadline);
274-
assert_eq!(history(directory.path(), Format::Ocsf), before);
275-
let before = history(directory.path(), Format::Shorthand);
276-
write_until_rollover(&mut json, directory.path(), Format::Ocsf, deadline);
277-
assert_eq!(history(directory.path(), Format::Shorthand), before);
278-
}
279-
}
85+
mod tests;
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use super::*;
5+
use std::collections::BTreeMap;
6+
use std::ffi::OsString;
7+
use std::io::Write as _;
8+
use std::time::{Duration, Instant};
9+
10+
fn history(directory: &Path, format: Format) -> BTreeMap<OsString, Vec<u8>> {
11+
let (prefix, suffix) = format.filename_parts();
12+
fs::read_dir(directory)
13+
.unwrap()
14+
.map(Result::unwrap)
15+
.filter(|entry| {
16+
let name = entry.file_name();
17+
let name = name.to_string_lossy();
18+
name.starts_with(&format!("{prefix}.")) && name.ends_with(&format!(".{suffix}"))
19+
})
20+
.map(|entry| (entry.file_name(), fs::read(entry.path()).unwrap()))
21+
.collect()
22+
}
23+
24+
fn seed_history(directory: &Path, format: Format, minutely: bool) {
25+
let (prefix, suffix) = format.filename_parts();
26+
let time = if minutely { "-00-00" } else { "" };
27+
for day in 1..=3 {
28+
let filename = format!("{prefix}.2000-01-{day:02}{time}.{suffix}");
29+
fs::write(directory.join(&filename), filename.as_bytes()).unwrap();
30+
}
31+
}
32+
33+
#[test]
34+
fn daily_initialization_retains_each_format_independently() {
35+
for formats in [
36+
[Format::Shorthand, Format::Ocsf],
37+
[Format::Ocsf, Format::Shorthand],
38+
] {
39+
let directory = tempfile::tempdir().unwrap();
40+
for format in formats {
41+
seed_history(directory.path(), format, false);
42+
}
43+
eprintln!(
44+
"creation timestamps available: {}",
45+
fs::metadata(directory.path().join("openshell.2000-01-01.log"))
46+
.unwrap()
47+
.created()
48+
.is_ok()
49+
);
50+
for (format, other) in [(formats[0], formats[1]), (formats[1], formats[0])] {
51+
let before = history(directory.path(), other);
52+
let _writer = appender(directory.path(), format, Rotation::DAILY).unwrap();
53+
assert_eq!(history(directory.path(), other), before);
54+
assert_eq!(history(directory.path(), format).len(), 3);
55+
}
56+
}
57+
}
58+
59+
#[test]
60+
fn migration_preserves_legacy_history_before_shorthand_pruning() {
61+
let directory = tempfile::tempdir().unwrap();
62+
seed_history(directory.path(), Format::Shorthand, false);
63+
seed_history(directory.path(), Format::Ocsf, false);
64+
let expected = history(directory.path(), Format::Ocsf);
65+
for name in expected.keys() {
66+
let path = directory.path().join(name);
67+
fs::rename(&path, path.with_extension("log")).unwrap();
68+
}
69+
let _writer = appender(directory.path(), Format::Shorthand, Rotation::DAILY).unwrap();
70+
assert_eq!(history(directory.path(), Format::Ocsf), expected);
71+
for name in expected.keys() {
72+
assert!(!directory.path().join(name).with_extension("log").exists());
73+
}
74+
}
75+
76+
#[test]
77+
fn migration_collision_does_not_overwrite_or_prune() {
78+
let directory = tempfile::tempdir().unwrap();
79+
seed_history(directory.path(), Format::Shorthand, false);
80+
let before = history(directory.path(), Format::Shorthand);
81+
let legacy = directory.path().join("openshell-ocsf.2000-01-01.log");
82+
let current = legacy.with_extension("jsonl");
83+
fs::write(&legacy, b"legacy records").unwrap();
84+
fs::write(&current, b"new records").unwrap();
85+
let error = appender(directory.path(), Format::Shorthand, Rotation::DAILY).unwrap_err();
86+
assert_eq!(error.kind(), io::ErrorKind::AlreadyExists);
87+
assert_eq!(history(directory.path(), Format::Shorthand), before);
88+
assert_eq!(fs::read(&legacy).unwrap(), b"legacy records");
89+
assert_eq!(fs::read(&current).unwrap(), b"new records");
90+
}
91+
92+
fn write_until_rollover(
93+
writer: &mut RollingFileAppender,
94+
directory: &Path,
95+
format: Format,
96+
deadline: Instant,
97+
) {
98+
let before = history(directory, format);
99+
loop {
100+
writer.write_all(b"event\n").unwrap();
101+
let current = history(directory, format);
102+
if current.keys().ne(before.keys()) {
103+
assert_eq!(current.len(), 3);
104+
return;
105+
}
106+
assert!(Instant::now() < deadline, "no rollover within 65 seconds");
107+
std::thread::sleep(Duration::from_millis(100));
108+
}
109+
}
110+
111+
#[test]
112+
fn rollover_retains_each_format_independently() {
113+
// MINUTELY uses the same pruning path as DAILY and avoids a mock-clock fork.
114+
let directory = tempfile::tempdir().unwrap();
115+
for format in [Format::Shorthand, Format::Ocsf] {
116+
seed_history(directory.path(), format, true);
117+
}
118+
let mut text = appender(directory.path(), Format::Shorthand, Rotation::MINUTELY).unwrap();
119+
let mut json = appender(directory.path(), Format::Ocsf, Rotation::MINUTELY).unwrap();
120+
let deadline = Instant::now() + Duration::from_secs(65);
121+
let before = history(directory.path(), Format::Ocsf);
122+
write_until_rollover(&mut text, directory.path(), Format::Shorthand, deadline);
123+
assert_eq!(history(directory.path(), Format::Ocsf), before);
124+
let before = history(directory.path(), Format::Shorthand);
125+
write_until_rollover(&mut json, directory.path(), Format::Ocsf, deadline);
126+
assert_eq!(history(directory.path(), Format::Shorthand), before);
127+
}

0 commit comments

Comments
 (0)