use std::fs::File;
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
const CHUNK: usize = 64 * 1024;
const NOTE_ROOM: u64 = 2048;
pub const MIN_LIMIT: u64 = 16 * 1024;
pub const MARK: &str = "[qex]";
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Dropped {
pub bytes: u64,
pub lines: u64,
}
#[derive(Debug, Clone, Copy)]
struct Parts {
head: u64,
tail: u64,
fill: u64,
max: u64,
}
fn parts(max: u64) -> Parts {
let room = NOTE_ROOM.min(max / 8);
let head = max / 4;
Parts {
head,
tail: max.saturating_sub(head + room).max(1),
fill: max.saturating_sub(room).max(head),
max,
}
}
pub struct CapWriter {
file: File,
parts: Option<Parts>,
head_len: u64,
overflow_bytes: u64,
overflow_lines: u64,
dropped: Dropped,
tail: Option<Ring>,
overflowing: bool,
tail_path: PathBuf,
fault: Option<String>,
}
impl CapWriter {
pub fn new(path: &Path, file: File, existing: u64, max_bytes: Option<u64>) -> Self {
Self {
file,
parts: max_bytes.map(parts),
head_len: existing,
overflow_bytes: 0,
overflow_lines: 0,
dropped: Dropped::default(),
tail: None,
overflowing: false,
tail_path: tail_path(path),
fault: None,
}
}
pub fn write(&mut self, buf: &[u8]) {
let Some(parts) = self.parts else {
self.push_head(buf);
return;
};
let mut rest = buf;
if !self.overflowing && self.head_len < parts.fill {
let room = (parts.fill - self.head_len) as usize;
let take = room.min(rest.len());
self.push_head(&rest[..take]);
rest = &rest[take..];
}
if rest.is_empty() {
return;
}
self.push_overflow(rest);
}
fn push_head(&mut self, data: &[u8]) {
if data.is_empty() {
return;
}
match self.file.write_all(data) {
Ok(()) => self.head_len += data.len() as u64,
Err(e) => self.record_fault(&format!("writing the output of the job: {e}")),
}
}
fn push_overflow(&mut self, data: &[u8]) {
if !self.overflowing {
self.start_overflow();
}
self.overflow_bytes += data.len() as u64;
self.overflow_lines += count_lines(data);
let result = match &mut self.tail {
Some(ring) => ring.write(data),
None => Ok(()),
};
if let Err(e) = result {
self.record_fault(&format!("writing the last part of the output: {e}"));
}
}
fn start_overflow(&mut self) {
let Some(parts) = self.parts else { return };
self.overflowing = true;
let mut fragment = false;
if self.head_len > parts.head {
let cut = head_cut(&mut self.file, parts.head).unwrap_or(None);
fragment = cut.is_none();
let keep = cut.unwrap_or(parts.head);
let extra = self.head_len - keep;
let lines = count_lines_in(&mut self.file, keep, extra).unwrap_or(0);
match self.file.set_len(keep) {
Ok(()) => {
self.dropped.bytes += extra;
self.dropped.lines += lines;
self.head_len = keep;
}
Err(e) => self.record_fault(&format!("cutting the earlier output: {e}")),
}
if let Err(e) = self.file.seek(SeekFrom::End(0)) {
self.record_fault(&format!("moving to the end of the output: {e}"));
}
}
let mut note = String::new();
if fragment {
note.push_str(&format!(
"\n{MARK} The line above is not complete. qex removed the output after it.\n"
));
}
note.push_str(&format!(
"{MARK} The output of this job reached the limit `[logs] max_bytes` = {}. \
qex keeps the last part of the output beside this file. It writes that part \
here when the job stops.\n",
crate::units::format_size(parts.max)
));
if let Err(e) = self.file.write_all(note.as_bytes()) {
self.record_fault(&format!("writing the note about the limit: {e}"));
}
match Ring::create(&self.tail_path, parts.tail) {
Ok(ring) => self.tail = Some(ring),
Err(e) => self.record_fault(&format!("making the file for the last output: {e}")),
}
}
pub fn finish(mut self) -> Dropped {
let (Some(ring), Some(parts)) = (self.tail.take(), self.parts) else {
self.dropped.bytes += self.overflow_bytes;
self.dropped.lines += self.overflow_lines;
if let Some(parts) = self.parts.filter(|_| self.overflowing) {
let note = format!(
"{MARK} ---- {} and {} line(s) of the output are not in this file ----\n\
{MARK} qex could not make the file for the last part of the output, so \
the last part is not here. The limit is `[logs] max_bytes` = {}. Read \
`supervisor.log` in this directory for the fault of the machine.\n",
crate::units::format_size(self.dropped.bytes),
self.dropped.lines,
crate::units::format_size(parts.max),
);
if let Err(e) = self.file.write_all(note.as_bytes()) {
self.record_fault(&format!("writing the line about the removed output: {e}"));
}
}
self.file.flush().ok();
return self.dropped;
};
let mut ring = ring;
let gap = ring.wrapped || self.dropped.bytes > 0;
let first_end = ring.first_line_end().unwrap_or(None);
let trim = gap && matches!(first_end, Some(n) if n <= parts.tail / 4);
let cut_line = gap && !trim;
let (kept_bytes, kept_lines) = ring.walk(None, trim).unwrap_or((0, 0));
self.dropped.bytes += self.overflow_bytes.saturating_sub(kept_bytes);
self.dropped.lines += self.overflow_lines.saturating_sub(kept_lines);
let mut note = format!(
"{MARK} ---- {} and {} line(s) of the output are not in this file ----\n\
{MARK} The limit is `[logs] max_bytes` = {}. qex kept the first {} and the last {}. \
To keep more, make max_bytes larger in the configuration file.\n",
crate::units::format_size(self.dropped.bytes),
self.dropped.lines,
crate::units::format_size(parts.max),
crate::units::format_size(parts.head),
crate::units::format_size(kept_bytes),
);
if cut_line {
note.push_str(&format!(
"{MARK} The text that follows starts in the middle of a line.\n"
));
}
if let Err(e) = self.file.write_all(note.as_bytes()) {
self.record_fault(&format!("writing the line about the removed output: {e}"));
}
if let Err(e) = ring.walk(Some(&mut self.file), trim) {
self.record_fault(&format!("writing the last part of the output: {e}"));
}
self.file.flush().ok();
ring.remove();
self.dropped
}
fn record_fault(&mut self, message: &str) {
if self.fault.is_some() {
return;
}
self.fault = Some(message.to_string());
eprintln!("qex: {message}. The job continues, and its output is not complete.");
}
}
struct Ring {
path: PathBuf,
file: File,
size: u64,
pos: u64,
wrapped: bool,
}
impl Ring {
fn create(path: &Path, size: u64) -> std::io::Result<Self> {
use std::os::unix::fs::OpenOptionsExt;
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
.open(path)?;
Ok(Self {
path: path.to_path_buf(),
file,
size,
pos: 0,
wrapped: false,
})
}
fn write(&mut self, buf: &[u8]) -> std::io::Result<()> {
let mut buf = if buf.len() as u64 > self.size {
self.wrapped = true;
&buf[buf.len() - self.size as usize..]
} else {
buf
};
while !buf.is_empty() {
let room = (self.size - self.pos) as usize;
let take = room.min(buf.len());
self.file.seek(SeekFrom::Start(self.pos))?;
self.file.write_all(&buf[..take])?;
self.pos += take as u64;
if self.pos == self.size {
self.pos = 0;
self.wrapped = true;
}
buf = &buf[take..];
}
Ok(())
}
fn segments(&self) -> [(u64, u64); 2] {
if self.wrapped {
[(self.pos, self.size - self.pos), (0, self.pos)]
} else {
[(0, self.pos), (0, 0)]
}
}
fn first_line_end(&mut self) -> std::io::Result<Option<u64>> {
let mut buf = vec![0u8; CHUNK];
let mut seen = 0u64;
for (start, len) in self.segments() {
if len == 0 {
continue;
}
self.file.seek(SeekFrom::Start(start))?;
let mut left = len;
while left > 0 {
let want = (left as usize).min(buf.len());
let n = self.file.read(&mut buf[..want])?;
if n == 0 {
break;
}
left -= n as u64;
if let Some(i) = buf[..n].iter().position(|b| *b == b'\n') {
return Ok(Some(seen + i as u64));
}
seen += n as u64;
}
}
Ok(None)
}
fn walk(&mut self, mut out: Option<&mut File>, trim: bool) -> std::io::Result<(u64, u64)> {
let segments = self.segments();
let mut trim = trim;
let mut bytes = 0u64;
let mut lines = 0u64;
let mut buf = vec![0u8; CHUNK];
for (start, len) in segments {
if len == 0 {
continue;
}
self.file.seek(SeekFrom::Start(start))?;
let mut left = len;
while left > 0 {
let want = (left as usize).min(buf.len());
let n = self.file.read(&mut buf[..want])?;
if n == 0 {
break;
}
left -= n as u64;
let mut data = &buf[..n];
if trim {
match data.iter().position(|b| *b == b'\n') {
Some(i) => {
data = &data[i + 1..];
trim = false;
}
None => data = &[],
}
}
bytes += data.len() as u64;
lines += count_lines(data);
if let Some(file) = out.as_deref_mut() {
file.write_all(data)?;
}
}
}
Ok((bytes, lines))
}
fn remove(&self) {
std::fs::remove_file(&self.path).ok();
}
}
pub fn tail_path(log: &Path) -> PathBuf {
log.with_extension("log.tail")
}
fn count_lines(data: &[u8]) -> u64 {
data.iter().filter(|b| **b == b'\n').count() as u64
}
fn head_cut(file: &mut File, head: u64) -> std::io::Result<Option<u64>> {
if head == 0 {
return Ok(None);
}
let window = (head / 4).clamp(1, CHUNK as u64);
let start = head - window.min(head);
file.seek(SeekFrom::Start(start))?;
let mut buf = vec![0u8; (head - start) as usize];
file.read_exact(&mut buf)?;
Ok(buf
.iter()
.rposition(|b| *b == b'\n')
.map(|i| start + i as u64 + 1))
}
fn count_lines_in(file: &mut File, start: u64, len: u64) -> std::io::Result<u64> {
file.seek(SeekFrom::Start(start))?;
let mut buf = vec![0u8; CHUNK];
let mut left = len;
let mut lines = 0u64;
while left > 0 {
let want = (left as usize).min(buf.len());
let n = file.read(&mut buf[..want])?;
if n == 0 {
break;
}
left -= n as u64;
lines += count_lines(&buf[..n]);
}
Ok(lines)
}
#[derive(Debug, Clone, Copy)]
pub enum Report {
Eof,
Done(Dropped),
}
pub fn pump(mut source: impl Read, mut writer: CapWriter, on_eof: impl FnOnce()) -> Dropped {
let mut buf = vec![0u8; CHUNK];
loop {
match source.read(&mut buf) {
Ok(0) => break,
Ok(n) => writer.write(&buf[..n]),
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(_) => break,
}
}
on_eof();
writer.finish()
}
#[cfg(test)]
mod tests {
use super::*;
struct Dir(PathBuf);
impl Dir {
fn new(name: &str) -> Self {
let path = std::env::temp_dir().join(format!(
"qex-logcap-{}-{}-{name}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.subsec_nanos()
));
std::fs::create_dir_all(&path).unwrap();
Self(path)
}
fn log(&self) -> PathBuf {
self.0.join("stdout.log")
}
}
impl Drop for Dir {
fn drop(&mut self) {
std::fs::remove_dir_all(&self.0).ok();
}
}
fn open(path: &Path) -> File {
std::fs::OpenOptions::new()
.write(true)
.read(true)
.create(true)
.truncate(true)
.open(path)
.unwrap()
}
fn write_lines(writer: &mut CapWriter, from: usize, to: usize) {
for i in from..=to {
writer.write(format!("line-{i}\n").as_bytes());
}
}
#[test]
fn the_first_lines_and_the_last_lines_stay_and_the_file_says_what_went() {
let dir = Dir::new("both-ends");
let path = dir.log();
let mut w = CapWriter::new(&path, open(&path), 0, Some(64 * 1024));
write_lines(&mut w, 1, 20000);
let dropped = w.finish();
let text = std::fs::read_to_string(&path).unwrap();
assert!(text.contains("line-1\n"), "the first line went");
assert!(text.contains("line-20000\n"), "the last line went");
assert!(
!text.contains("line-10000\n"),
"the middle must go, and it stayed"
);
assert!(dropped.bytes > 0 && dropped.lines > 0, "{dropped:?}");
assert!(
text.contains("are not in this file"),
"the file must say what went: {text:.400}"
);
assert!(
text.contains(&dropped.lines.to_string()),
"the file must give the number of lines"
);
let size = std::fs::metadata(&path).unwrap().len();
assert!(size <= 64 * 1024, "the file is {size} bytes");
}
#[test]
fn the_disk_stays_below_the_limit_while_the_job_writes() {
let dir = Dir::new("during");
let path = dir.log();
let limit = 32 * 1024;
let mut w = CapWriter::new(&path, open(&path), 0, Some(limit));
for i in 1..=5000 {
w.write(format!("line-{i} aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n").as_bytes());
if i % 100 == 0 {
let mut total = 0;
for entry in std::fs::read_dir(&dir.0).unwrap() {
total += entry.unwrap().metadata().unwrap().len();
}
assert!(total <= limit, "the disk holds {total} bytes at line {i}");
}
}
w.finish();
let mut total = 0;
for entry in std::fs::read_dir(&dir.0).unwrap() {
total += entry.unwrap().metadata().unwrap().len();
}
assert!(total <= limit, "the disk holds {total} bytes at the end");
}
#[test]
fn the_file_that_holds_the_tail_goes_at_the_end() {
let dir = Dir::new("cleanup");
let path = dir.log();
let mut w = CapWriter::new(&path, open(&path), 0, Some(32 * 1024));
write_lines(&mut w, 1, 5000);
w.finish();
let left: Vec<_> = std::fs::read_dir(&dir.0)
.unwrap()
.map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
.collect();
assert_eq!(left, vec!["stdout.log".to_string()], "a file stayed");
}
#[test]
fn output_below_the_limit_arrives_complete() {
let dir = Dir::new("small");
let path = dir.log();
let mut w = CapWriter::new(&path, open(&path), 0, Some(MIN_LIMIT));
write_lines(&mut w, 1, 20);
let dropped = w.finish();
let text = std::fs::read_to_string(&path).unwrap();
assert_eq!(dropped, Dropped::default());
assert!(!text.contains(MARK), "qex wrote a note with no reason");
assert_eq!(text.lines().count(), 20);
}
#[test]
fn no_limit_keeps_every_byte() {
let dir = Dir::new("none");
let path = dir.log();
let mut w = CapWriter::new(&path, open(&path), 0, None);
write_lines(&mut w, 1, 5000);
let dropped = w.finish();
let text = std::fs::read_to_string(&path).unwrap();
assert_eq!(dropped, Dropped::default());
assert_eq!(text.lines().count(), 5000);
}
#[test]
fn a_reader_sees_the_limit_before_the_job_stops() {
let dir = Dir::new("live");
let path = dir.log();
let mut w = CapWriter::new(&path, open(&path), 0, Some(32 * 1024));
write_lines(&mut w, 1, 5000);
let text = std::fs::read_to_string(&path).unwrap();
assert!(
text.contains("reached the limit"),
"the file must say that the output continues: {text:.300}"
);
}
#[test]
fn a_second_attempt_shares_the_limit_of_the_first() {
let dir = Dir::new("retry");
let path = dir.log();
let limit = 32 * 1024;
let mut w = CapWriter::new(&path, open(&path), 0, Some(limit));
write_lines(&mut w, 1, 5000);
w.finish();
let existing = std::fs::metadata(&path).unwrap().len();
let again = std::fs::OpenOptions::new()
.append(true)
.read(true)
.open(&path)
.unwrap();
let mut w = CapWriter::new(&path, again, existing, Some(limit));
write_lines(&mut w, 5001, 10000);
let dropped = w.finish();
let size = std::fs::metadata(&path).unwrap().len();
assert!(size <= limit, "two attempts hold {size} bytes");
assert!(dropped.bytes > 0, "the second attempt removed nothing");
assert!(
dropped.lines > 0,
"the count of the lines must hold the lines of the first attempt"
);
let text = std::fs::read_to_string(&path).unwrap();
assert!(text.contains("line-1\n"), "the start of the job went");
assert!(text.contains("line-10000\n"), "the last line went");
}
#[test]
fn one_write_that_is_larger_than_the_tail_keeps_its_end() {
let dir = Dir::new("big-write");
let path = dir.log();
let mut w = CapWriter::new(&path, open(&path), 0, Some(MIN_LIMIT));
w.write(b"first\n");
let mut big = vec![b'x'; 200 * 1024];
big.extend_from_slice(b"\nTHE-END\n");
w.write(&big);
w.finish();
let text = std::fs::read_to_string(&path).unwrap();
assert!(text.contains("first"), "the head went");
assert!(text.contains("THE-END"), "the end of the write went");
assert!(std::fs::metadata(&path).unwrap().len() <= MIN_LIMIT);
}
#[test]
fn output_with_no_line_end_keeps_its_last_part() {
let dir = Dir::new("no-line-end");
let path = dir.log();
let mut w = CapWriter::new(&path, open(&path), 0, Some(MIN_LIMIT));
w.write(b"the start of the output, and then one very long line: ");
for _ in 0..40 {
w.write(&vec![b'x'; 8 * 1024]);
}
w.write(b"THE-VERY-END");
let dropped = w.finish();
let text = std::fs::read_to_string(&path).unwrap();
assert!(text.contains("the start of the output"), "the head went");
assert!(
text.ends_with("THE-VERY-END"),
"the end of the output went, and the file holds the head only"
);
let kept = text.rfind(MARK).map(|i| text[i..].len()).unwrap_or(0);
assert!(kept > 1000, "the tail holds {kept} bytes only");
assert!(dropped.bytes > 0);
assert!(
text.contains("middle of a line"),
"the file must say that the last part starts in the middle of a line"
);
assert!(std::fs::metadata(&path).unwrap().len() <= MIN_LIMIT);
}
#[test]
fn a_second_attempt_that_fits_the_limit_loses_nothing() {
let dir = Dir::new("retry-fits");
let path = dir.log();
let limit = 1 << 20;
let mut w = CapWriter::new(&path, open(&path), 0, Some(limit));
write_lines(&mut w, 1, 30000);
let first = w.finish();
assert_eq!(first, Dropped::default(), "the first attempt fits");
let existing = std::fs::metadata(&path).unwrap().len();
assert!(existing < limit, "the test needs an attempt that fits");
let mut again = std::fs::OpenOptions::new()
.append(true)
.read(true)
.open(&path)
.unwrap();
again.write_all(b"\n--- attempt 2 ---\n").unwrap();
let existing = std::fs::metadata(&path).unwrap().len();
let mut w = CapWriter::new(&path, again, existing, Some(limit));
w.write(b"the second attempt\n");
let second = w.finish();
assert_eq!(
second,
Dropped::default(),
"the second attempt removed data"
);
let text = std::fs::read_to_string(&path).unwrap();
assert!(!text.contains(MARK), "qex wrote a note with no reason");
assert!(
text.contains("line-1\n"),
"the first attempt lost its start"
);
assert!(
text.contains("line-30000\n"),
"the first attempt lost its end"
);
assert!(
text.contains("--- attempt 2 ---"),
"the mark between the attempts went"
);
assert!(text.contains("the second attempt"));
}
#[test]
fn a_line_end_that_is_far_into_the_tail_does_not_cost_the_tail() {
let dir = Dir::new("late-line-end");
let path = dir.log();
let mut w = CapWriter::new(&path, open(&path), 0, Some(MIN_LIMIT));
w.write(b"the start\n");
for i in 0..4000 {
w.write(format!("\rstep {i} of 4000").as_bytes());
}
w.write(b"\nBUILD FAILED: the compiler stopped\n");
w.finish();
let text = std::fs::read_to_string(&path).unwrap();
assert!(text.contains("BUILD FAILED"), "the last line went");
let tail = text.rsplit(MARK).next().unwrap_or("");
assert!(
tail.len() > 1000,
"the tail holds {} bytes only, so the trim removed the output",
tail.len()
);
assert!(
text.contains("middle of a line"),
"the file must say that the last part is not a whole line"
);
assert!(std::fs::metadata(&path).unwrap().len() <= MIN_LIMIT);
}
#[test]
fn a_head_that_cannot_cut_back_keeps_its_bytes_and_says_so() {
let dir = Dir::new("head-fragment");
let path = dir.log();
let parts = parts(MIN_LIMIT);
let mut w = CapWriter::new(&path, open(&path), 0, Some(MIN_LIMIT));
w.write(b"the start of the job\n");
for _ in 0..40 {
w.write(&vec![b'x'; 8 * 1024]);
}
w.finish();
let text = std::fs::read_to_string(&path).unwrap();
let head = &text[..text.find(MARK).expect("the file must hold a note of qex")];
assert!(
head.len() as u64 > parts.head / 2,
"the head holds {} bytes of the {} that it must hold, so the cut back removed \
the start of the output",
head.len(),
parts.head
);
assert!(head.starts_with("the start of the job\n"));
assert!(
text.contains("The line above is not complete"),
"the head ends with a fragment of a line, and the file does not say so: \
{text:.400}"
);
assert!(std::fs::metadata(&path).unwrap().len() <= MIN_LIMIT);
}
#[test]
fn a_tail_that_starts_in_the_middle_of_a_line_says_so() {
let dir = Dir::new("mid-line");
let path = dir.log();
let body = "A".repeat(59);
let mut w = CapWriter::new(&path, open(&path), 0, Some(MIN_LIMIT));
let mut written = 0u64;
for i in 0..380 {
let line = format!("{body}-{i}\n");
written += line.len() as u64;
w.write(line.as_bytes());
}
w.finish();
let parts = parts(MIN_LIMIT);
assert!(
written > parts.fill && written - parts.fill <= parts.tail,
"the job wrote {written} bytes; this test needs more than {} and not more \
than {}",
parts.fill,
parts.fill + parts.tail
);
let text = std::fs::read_to_string(&path).unwrap();
let after = text.rsplit(MARK).next().unwrap_or("");
let tail: Vec<&str> = after.lines().skip(1).collect();
let first = tail.first().copied().unwrap_or("");
let whole = first.starts_with(&body) && first[body.len()..].starts_with('-');
assert!(
whole || text.contains("middle of a line"),
"the first line of the last part is `{first}`, which is the end of a line that \
the reader cannot see, and the file does not say so"
);
}
#[test]
fn a_tail_file_that_the_machine_refuses_still_stops_the_output() {
let dir = Dir::new("no-ring");
let path = dir.log();
std::fs::create_dir_all(tail_path(&path)).unwrap();
let mut written = 0u64;
let mut lines = 0u64;
let mut w = CapWriter::new(&path, open(&path), 0, Some(MIN_LIMIT));
for i in 1..=20000 {
let line = format!("line-{i}\n");
written += line.len() as u64;
lines += 1;
w.write(line.as_bytes());
}
let dropped = w.finish();
let size = std::fs::metadata(&path).unwrap().len();
assert!(size <= MIN_LIMIT, "the file holds {size} bytes");
let text = std::fs::read_to_string(&path).unwrap();
let kept_text = &text[..text.find(MARK).expect("the file must hold a note of qex")];
assert!(
kept_text.ends_with('\n'),
"the head must end at a line end, and it ends `{}`",
&kept_text[kept_text.len().saturating_sub(12)..]
);
let kept = kept_text.len() as u64;
assert_eq!(
dropped.bytes,
written - kept,
"the file holds {kept} byte(s) of the {written} that the job wrote, so exactly \
{} went, and the count says {}",
written - kept,
dropped.bytes
);
assert_eq!(
dropped.lines,
lines - count_lines(kept_text.as_bytes()),
"the file holds {} line(s) of the {lines} that the job wrote, and the count says \
that {} went",
count_lines(kept_text.as_bytes()),
dropped.lines
);
assert_eq!(
text.matches("reached the limit").count(),
1,
"the note must appear one time"
);
assert!(
text.contains("are not in this file"),
"the file promises a last part that never arrives, and it gives no count: \
{text:.600}"
);
assert!(
text.contains("could not make the file for the last part"),
"the file must say why the last part is missing: {text:.600}"
);
}
#[test]
fn the_last_part_of_short_lines_starts_at_a_whole_line() {
let dir = Dir::new("trim");
let path = dir.log();
let mut w = CapWriter::new(&path, open(&path), 0, Some(64 * 1024));
write_lines(&mut w, 1, 20000);
w.finish();
let text = std::fs::read_to_string(&path).unwrap();
let tail = text
.rsplit_once(&format!("{MARK} The limit is"))
.expect("the file must hold the note about the limit")
.1;
let first = tail.lines().nth(1).expect("the last part is empty");
assert!(
first.starts_with("line-"),
"the last part starts with `{first}`, which is the end of a line and not a \
whole line"
);
assert!(
!text.contains("middle of a line"),
"qex removed the fragment, so the file must not say that one stayed"
);
for line in tail.lines().skip(1).filter(|l| !l.is_empty()) {
let n: u64 = line
.strip_prefix("line-")
.unwrap_or("")
.parse()
.unwrap_or_else(|_| panic!("`{line}` is not a line that the job wrote"));
assert!((1..=20000).contains(&n), "`{line}` is not in the output");
}
}
#[test]
fn the_parts_of_the_limit_fit_the_limit() {
for max in [MIN_LIMIT, 1 << 20, 32 << 20, 1 << 30] {
let p = parts(max);
assert!(
p.head + p.tail <= max,
"the parts of {max} are larger than the limit"
);
assert!(p.tail > p.head, "the tail must hold more than the head");
}
}
}