diffs-rs 0.1.1

Fast terminal diff viewer and embeddable Rust diff engine
Documentation
use super::*;
use crossterm::event::KeyEvent;
use diffs_rs::engine::{Input, Options, SidebarPreference};
use ratatui::{Terminal, backend::TestBackend, buffer::Buffer, layout::Rect, style::Modifier};
use std::{
    fs,
    sync::mpsc,
    thread,
    time::{Duration, Instant},
};

fn key(reading: &mut Reading, engine: &mut Engine, code: KeyCode) {
    assert!(
        reading
            .handle(&Event::Key(KeyEvent::new(code, KeyModifiers::NONE)), engine)
            .unwrap()
    );
}

fn emphasized(buffer: &Buffer) -> Vec<(usize, String)> {
    buffer
        .content()
        .iter()
        .enumerate()
        .filter(|(_, cell)| cell.modifier.contains(Modifier::UNDERLINED))
        .map(|(index, cell)| (index, cell.symbol().to_owned()))
        .collect()
}

#[test]
fn escape_cancels_an_established_scan_without_moving_or_clearing_the_previous_match() {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("review.patch");
    fs::write(
        &path,
        "diff --git a/a b/a\n--- a/a\n+++ b/a\n@@ -1 +1 @@\n-first needle\n+second needle\n",
    )
    .unwrap();
    let (wake, ready) = mpsc::sync_channel(1);
    let mut engine = Engine::open(
        Input::Patch(path),
        Options {
            sidebar: SidebarPreference::Hide,
            ..Options::default()
        },
        wake.clone(),
    )
    .unwrap();
    let mut terminal = Terminal::new(TestBackend::new(120, 30)).unwrap();
    let pane = Rect::new(5, 3, 110, 24);
    let deadline = Instant::now() + Duration::from_secs(10);
    loop {
        engine.poll().unwrap();
        terminal
            .draw(|frame| engine.draw(frame, pane).unwrap())
            .unwrap();
        engine.after_draw();
        assert!(engine.status().error.is_none());
        if !engine.status().loading && !engine.status().coloring {
            break;
        }
        assert!(Instant::now() < deadline);
        let _ = ready.recv_timeout(Duration::from_millis(20));
    }
    let mut reading = Reading::new(wake);
    key(&mut reading, &mut engine, KeyCode::Char('/'));
    reading
        .handle(&Event::Paste("needle".into()), &mut engine)
        .unwrap();
    key(&mut reading, &mut engine, KeyCode::Enter);
    while reading.pending.is_some() {
        reading.poll(&mut engine).unwrap();
        assert!(Instant::now() < deadline);
        let _ = ready.recv_timeout(Duration::from_millis(20));
    }
    assert_eq!(reading.current.as_ref().unwrap().location().side, Side::Old);
    let position = engine.selected_source().unwrap();
    let before = terminal
        .draw(|frame| engine.draw(frame, pane).unwrap())
        .unwrap()
        .buffer
        .clone();
    let marked = emphasized(&before);
    assert_eq!(
        marked
            .iter()
            .map(|(_, text)| text.as_str())
            .collect::<String>(),
        "needle"
    );
    for (index, _) in &marked {
        let x = *index as u16 % 120;
        let y = *index as u16 / 120;
        assert!(x >= pane.x && x < pane.right() && y >= pane.y && y < pane.bottom());
    }
    let (entered, resume) = reading.worker.as_mut().unwrap().pause_next();
    key(&mut reading, &mut engine, KeyCode::F(3));
    entered.recv_timeout(Duration::from_secs(10)).unwrap();
    let pending = reading.pending.unwrap();
    for _ in 0..8 {
        key(&mut reading, &mut engine, KeyCode::F(3));
    }
    assert_eq!(reading.pending, Some(pending));
    assert!(Arc::strong_count(reading.read.as_ref().unwrap()) > 1);
    key(&mut reading, &mut engine, KeyCode::Esc);
    assert!(reading.pending.is_none());
    assert_eq!(reading.controls(120).status, Some("Search canceled"));
    resume.send(()).unwrap();
    let deadline = Instant::now() + Duration::from_secs(10);
    while Arc::strong_count(reading.read.as_ref().unwrap()) != 1 {
        assert!(
            Instant::now() < deadline,
            "canceled request retained its source owner"
        );
        thread::sleep(Duration::from_millis(1));
    }
    reading.poll(&mut engine).unwrap();
    assert_eq!(engine.selected_source().unwrap(), position);
    assert_eq!(reading.current.as_ref().unwrap().location().side, Side::Old);
    let after = terminal
        .draw(|frame| engine.draw(frame, pane).unwrap())
        .unwrap()
        .buffer
        .clone();
    assert_eq!(emphasized(&after), marked);
    key(&mut reading, &mut engine, KeyCode::F(3));
    while reading.pending.is_some() {
        reading.poll(&mut engine).unwrap();
        assert!(Instant::now() < deadline);
        let _ = ready.recv_timeout(Duration::from_millis(20));
    }
    assert_eq!(reading.current.as_ref().unwrap().location().side, Side::New);
    reading.close().unwrap();
    engine.close().unwrap();
}

#[test]
fn save_acceptance_rejects_completed_output_after_escape_or_review_close() {
    let dir = tempfile::tempdir().unwrap();
    let patch = dir.path().join("review.patch");
    fs::write(
        &patch,
        b"diff --git a/a b/a\n--- a/a\n+++ b/a\n@@ -1 +1 @@\n-old\n+new\n",
    )
    .unwrap();
    let (wake, ready) = mpsc::sync_channel(1);
    let mut engine = Engine::open(Input::Patch(patch), Options::default(), wake).unwrap();
    let deadline = Instant::now() + Duration::from_secs(10);
    while engine.status().loading {
        engine.poll().unwrap();
        assert!(Instant::now() < deadline);
        if engine.status().loading {
            let _ = ready.recv_timeout(Duration::from_millis(20));
        }
    }
    assert!(engine.status().error.is_none());
    let (wake, ready) = mpsc::sync_channel(1);
    let mut reading = Reading::new(wake);
    for close_review in [false, true] {
        let read = Arc::new(
            engine
                .source_read(SourceRange {
                    review: engine.review_id(),
                    file: 0,
                    side: Side::Old,
                    first: 1,
                    last: 1,
                })
                .unwrap(),
        );
        let owner = Arc::downgrade(&read);
        let destination = dir
            .path()
            .join(if close_review { "closed" } else { "canceled" });
        reading
            .save(read, destination.to_str().unwrap().into())
            .unwrap();
        ready.recv_timeout(Duration::from_secs(10)).unwrap();
        assert!(!destination.exists());
        assert_eq!(fs::read_dir(dir.path()).unwrap().count(), 2);
        assert!(owner.upgrade().is_some());
        if close_review {
            engine.close().unwrap();
        } else {
            key(&mut reading, &mut engine, KeyCode::Esc);
            assert_eq!(reading.controls(160).status, Some("Save canceled"));
        }
        reading.poll(&mut engine).unwrap();
        if close_review {
            assert!(
                reading
                    .controls(160)
                    .status
                    .unwrap()
                    .contains("Save failed:")
            );
        }
        assert!(owner.upgrade().is_none());
        assert!(reading.pending.is_none());
        assert!(!destination.exists());
        assert_eq!(fs::read_dir(dir.path()).unwrap().count(), 1);
    }
    reading.close().unwrap();
}

#[test]
fn copy_acceptance_discards_prepared_text_after_escape_or_review_close() {
    for close_review in [false, true] {
        let dir = tempfile::tempdir().unwrap();
        let patch = dir.path().join("review.patch");
        fs::write(
            &patch,
            b"diff --git a/a b/a\n--- a/a\n+++ b/a\n@@ -1 +1 @@\n-old\n+new\n",
        )
        .unwrap();
        let (wake, ready) = mpsc::sync_channel(1);
        let mut engine = Engine::open(Input::Patch(patch), Options::default(), wake).unwrap();
        let deadline = Instant::now() + Duration::from_secs(10);
        while engine.status().loading {
            engine.poll().unwrap();
            assert!(Instant::now() < deadline);
            if engine.status().loading {
                let _ = ready.recv_timeout(Duration::from_millis(10));
            }
        }
        let read = Arc::new(
            engine
                .source_read(SourceRange {
                    review: engine.review_id(),
                    file: 0,
                    side: Side::Old,
                    first: 1,
                    last: 1,
                })
                .unwrap(),
        );
        let owner = Arc::downgrade(&read);
        let (wake, ready) = mpsc::sync_channel(1);
        let mut reading = Reading::new(wake.clone());
        let mut worker = Worker::start(wake).unwrap();
        let id = worker.copy(read).unwrap();
        reading.pending = Some(Pending::Copy(id));
        reading.worker = Some(worker);
        ready.recv_timeout(Duration::from_secs(10)).unwrap();
        assert!(owner.upgrade().is_some());
        if close_review {
            engine.close().unwrap();
        } else {
            key(&mut reading, &mut engine, KeyCode::Esc);
        }
        reading.poll(&mut engine).unwrap();
        assert!(reading.pending.is_none());
        assert!(owner.upgrade().is_none());
        assert!(reading.notice.as_ref().unwrap().contains(if close_review {
            "Copy failed"
        } else {
            "Copy canceled"
        }));
        reading.close().unwrap();
        engine.close().unwrap();
    }
}

#[test]
fn search_and_save_prompts_report_an_interrupted_delivery() {
    for save in [false, true] {
        let dir = tempfile::tempdir().unwrap();
        let patch = dir.path().join("review.patch");
        fs::write(
            &patch,
            b"diff --git a/a b/a\n--- a/a\n+++ b/a\n@@ -1 +1 @@\n-old\n+new\n",
        )
        .unwrap();
        let (wake, ready) = mpsc::sync_channel(1);
        let mut engine = Engine::open(Input::Patch(patch), Options::default(), wake).unwrap();
        let mut terminal = Terminal::new(TestBackend::new(160, 30)).unwrap();
        let deadline = Instant::now() + Duration::from_secs(10);
        while engine.status().loading {
            engine.poll().unwrap();
            assert!(Instant::now() < deadline);
            if engine.status().loading {
                let _ = ready.recv_timeout(Duration::from_millis(10));
            }
        }
        terminal
            .draw(|frame| engine.draw(frame, frame.area()).unwrap())
            .unwrap();
        engine.execute(Command::FirstSource).unwrap();
        let read = Arc::new(Reading::source(&mut engine).unwrap());
        let (wake, ready) = mpsc::sync_channel(1);
        let mut reading = Reading::new(wake.clone());
        let mut worker = Worker::start(wake).unwrap();
        let id = worker.copy(read).unwrap();
        ready.recv_timeout(Duration::from_secs(10)).unwrap();
        let Output::Copy(prepared) = worker.poll().unwrap().unwrap().result.unwrap() else {
            panic!("Expected copy preparation")
        };
        let mut command = std::process::Command::new("/bin/sleep");
        command.arg("30");
        worker
            .deliver(id, prepared, engine.review_id(), command)
            .unwrap();
        reading.worker = Some(worker);
        reading.pending = Some(Pending::Deliver(id));
        let event = if save {
            KeyEvent::new(KeyCode::Char('s'), KeyModifiers::CONTROL)
        } else {
            KeyEvent::new(KeyCode::Char('/'), KeyModifiers::NONE)
        };
        assert!(reading.handle(&Event::Key(event), &mut engine).unwrap());
        assert!(reading.pending.is_none());
        assert_eq!(reading.notice.as_deref(), Some(COPY_INTERRUPTED));
        terminal.draw(|frame| reading.draw_prompt(frame)).unwrap();
        let contents: String = terminal
            .backend()
            .buffer()
            .content()
            .iter()
            .map(|cell| cell.symbol())
            .collect();
        assert!(contents.contains(COPY_INTERRUPTED));
        assert!(contents.contains(if save { "/save:" } else { "/diff:" }));
        reading.close().unwrap();
        engine.close().unwrap();
    }
}