holys3 0.5.1

Indexed regex search for local files and private S3 buckets
#![cfg(unix)]

use anyhow::{Context, Result};
use serde_json::Value;
use std::io::{BufRead, BufReader};
use std::process::{Command, Stdio};
use std::sync::mpsc::{self, Receiver};
use std::time::{Duration, Instant};

fn receive_event(
    receiver: &Receiver<String>,
    event_type: &str,
    minimum_cycle: u64,
    matches: impl Fn(&Value) -> bool,
) -> Result<Value> {
    let deadline = Instant::now() + Duration::from_secs(15);
    loop {
        let remaining = deadline
            .checked_duration_since(Instant::now())
            .with_context(|| format!("timed out waiting for {event_type} event"))?;
        let line = receiver
            .recv_timeout(remaining)
            .with_context(|| format!("receiving {event_type} event"))?;
        let event = serde_json::from_str::<Value>(&line).context("parsing index status JSON")?;
        if event["type"] == event_type
            && event["cycle"]
                .as_u64()
                .is_some_and(|cycle| cycle >= minimum_cycle)
            && matches(&event)
        {
            return Ok(event);
        }
    }
}

#[test]
fn watch_indexes_changes_and_stops_on_sigterm() -> Result<()> {
    let target = tempfile::tempdir()?;
    let index = tempfile::tempdir()?;
    std::fs::write(target.path().join("first.log"), "first\n")?;
    let mut child = Command::new("/bin/sh")
        .arg("-c")
        .arg(
            r#"trap '' INT
exec "$1" index "$2" --out "$3" --watch --interval 1 --json"#,
        )
        .arg("holys3-watch-test")
        .arg(env!("CARGO_BIN_EXE_holys3"))
        .arg(target.path())
        .arg(index.path())
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .stderr(Stdio::null())
        .spawn()
        .context("starting index watch")?;
    let stdout = child.stdout.take().context("taking index watch stdout")?;
    let (sender, receiver) = mpsc::channel();
    let reader = std::thread::spawn(move || {
        for line in BufReader::new(stdout).lines() {
            let Ok(line) = line else {
                break;
            };
            if sender.send(line).is_err() {
                break;
            }
        }
    });

    let test_result = (|| -> Result<()> {
        let initial = receive_event(&receiver, "indexed", 1, |_| true)?;
        anyhow::ensure!(
            initial["total_docs"] == 1,
            "unexpected initial event: {initial}"
        );
        std::fs::write(target.path().join("second.log"), "WATCHNEEDLE\n")?;
        let changed = receive_event(&receiver, "indexed", 2, |event| {
            event["added"] == 1 && event["total_docs"] == 2
        })?;
        let status = Command::new("kill")
            .args(["-TERM", &child.id().to_string()])
            .status()
            .context("sending SIGTERM to index watch")?;
        anyhow::ensure!(status.success(), "kill -TERM failed with {status}");
        let stopped = receive_event(
            &receiver,
            "stopped",
            changed["cycle"].as_u64().context("missing changed cycle")?,
            |_| true,
        )?;
        anyhow::ensure!(
            stopped["target"].as_str() == target.path().to_str(),
            "unexpected stopped event"
        );
        let status = child.wait().context("waiting for index watch")?;
        anyhow::ensure!(status.success(), "index watch exited with {status}");
        let output = Command::new(env!("CARGO_BIN_EXE_holys3"))
            .arg("WATCHNEEDLE")
            .arg(target.path())
            .arg("--index")
            .arg(index.path())
            .output()
            .context("searching watched index")?;
        anyhow::ensure!(
            output.status.success(),
            "search failed with {}",
            output.status
        );
        Ok(())
    })();

    if child.try_wait()?.is_none() {
        child.kill()?;
        child.wait()?;
    }
    reader
        .join()
        .map_err(|_| anyhow::anyhow!("index stdout reader panicked"))?;
    test_result
}