tests/rotation.rs

//! Rotation / truncation detection integration tests.
//!
//! These exercise the poll engine directly rather than through the CLI
//! because simulating inotify / kqueue events is too flaky on shared CI.

use std::fs::{File, OpenOptions};
use std::io::Write;
use std::time::Duration;

use ripgrab::tail::engine::{poll, Event};
use tempfile::NamedTempFile;
use tokio::sync::mpsc;
use tokio::time::timeout;

async fn recv_first(rx: &mut mpsc::Receiver<Event>) -> Option<Event> {
    timeout(Duration::from_secs(1), rx.recv()).await.ok().flatten()
}

#[tokio::test]
async fn append_emits_grew() {
    let mut tmp = NamedTempFile::new().unwrap();
    writeln!(tmp, "initial").unwrap();
    tmp.flush().unwrap();
    let path = tmp.path().to_path_buf();
    let (tx, mut rx) = mpsc::channel(8);
    let handle = tokio::spawn(poll::run_every(
        vec![path.clone()],
        tx,
        Duration::from_millis(30),
    ));
    {
        let mut f = OpenOptions::new().append(true).open(&path).unwrap();
        writeln!(f, "second").unwrap();
        f.flush().unwrap();
    }
    let ev = recv_first(&mut rx).await;
    handle.abort();
    assert!(matches!(ev, Some(Event::Grew(_))));
}

#[tokio::test]
async fn truncate_emits_shrunk() {
    let mut tmp = NamedTempFile::new().unwrap();
    writeln!(tmp, "bigger content").unwrap();
    tmp.flush().unwrap();
    let path = tmp.path().to_path_buf();
    let (tx, mut rx) = mpsc::channel(8);
    let handle = tokio::spawn(poll::run_every(
        vec![path.clone()],
        tx,
        Duration::from_millis(30),
    ));
    {
        let f = OpenOptions::new().write(true).open(&path).unwrap();
        f.set_len(0).unwrap();
    }
    let ev = recv_first(&mut rx).await;
    handle.abort();
    assert!(matches!(ev, Some(Event::Shrunk(_))));
}

#[tokio::test]
async fn rotation_emits_rotated() {
    let tmp = tempfile::tempdir().unwrap();
    let path = tmp.path().join("app.log");
    {
        let mut f = File::create(&path).unwrap();
        writeln!(f, "first").unwrap();
    }
    let (tx, mut rx) = mpsc::channel(8);
    let handle = tokio::spawn(poll::run_every(
        vec![path.clone()],
        tx,
        Duration::from_millis(30),
    ));

    // Simulate logrotate: rename away, create fresh file.
    let rotated = tmp.path().join("app.log.1");
    std::fs::rename(&path, &rotated).unwrap();
    let mut fresh = File::create(&path).unwrap();
    writeln!(fresh, "post-rotation").unwrap();
    fresh.flush().unwrap();

    let ev = recv_first(&mut rx).await;
    handle.abort();
    assert!(matches!(ev, Some(Event::Rotated(_))));
}

#[tokio::test]
async fn vanishing_path_emits_gone() {
    let tmp = NamedTempFile::new().unwrap();
    let path = tmp.path().to_path_buf();
    drop(tmp);
    let (tx, mut rx) = mpsc::channel(8);
    let handle = tokio::spawn(poll::run_every(
        vec![path.clone()],
        tx,
        Duration::from_millis(30),
    ));
    let ev = recv_first(&mut rx).await;
    handle.abort();
    assert!(matches!(ev, Some(Event::Gone(_))));
}