plavno 0.1.1

A lightweight Linux terminal music player with service waves and library shuffle
use super::*;
use std::{
    alloc::{GlobalAlloc, Layout, System},
    cell::Cell,
    io::{BufRead, BufReader, Write},
    net::{TcpListener, TcpStream},
    time::Instant,
};

thread_local! {
    static MEASURING: Cell<bool> = const { Cell::new(false) };
    static LARGEST: Cell<usize> = const { Cell::new(0) };
}

struct Allocator;
#[global_allocator]
static ALLOCATOR: Allocator = Allocator;

fn record(size: usize) {
    if MEASURING.try_with(Cell::get).unwrap_or(false) {
        let _ = LARGEST.try_with(|value| value.set(value.get().max(size)));
    }
}

unsafe impl GlobalAlloc for Allocator {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        record(layout.size());
        unsafe { System.alloc(layout) }
    }

    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
        unsafe { System.dealloc(ptr, layout) }
    }

    unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, size: usize) -> *mut u8 {
        record(size);
        unsafe { System.realloc(ptr, layout, size) }
    }
}

#[test]
fn narrow_covers_keep_render_allocations_bounded_by_the_viewport() {
    for (dimensions, area) in [
        ((1, 2048), Rect::new(0, 0, 200, 20)),
        ((2048, 1), Rect::new(0, 0, 20, 100)),
    ] {
        let mut bytes = Cursor::new(Vec::new());
        image::DynamicImage::ImageRgb8(RgbImage::from_pixel(
            dimensions.0,
            dimensions.1,
            image::Rgb([200, 100, 50]),
        ))
        .write_to(&mut bytes, image::ImageFormat::Png)
        .unwrap();
        let mut artwork = Artwork::new().unwrap();
        artwork.image = Some(Arc::new(decode(bytes.get_ref()).unwrap()));
        let mut buffer = Buffer::empty(area);
        LARGEST.set(0);
        MEASURING.set(true);
        artwork.render(area, &mut buffer);
        MEASURING.set(false);
        let largest = LARGEST.get();
        eprintln!("Largest render allocation for {dimensions:?}: {largest} bytes");
        assert!(
            largest < 2 * 1024 * 1024,
            "Render allocated {largest} bytes for {dimensions:?}"
        );
        assert_eq!(
            artwork.raster.as_ref().unwrap().dimensions(),
            (u32::from(area.width), u32::from(area.height) * 2)
        );
    }
}

#[test]
fn cropping_preserves_the_center_for_wide_and_tall_viewports() {
    for area in [Rect::new(0, 0, 40, 4), Rect::new(0, 0, 4, 20)] {
        let image = RgbImage::from_fn(192, 192, |x, y| {
            let center = if area.width > area.height * 2 {
                (75..117).contains(&y)
            } else {
                (84..108).contains(&x)
            };
            image::Rgb(if center { [0, 255, 0] } else { [255, 0, 255] })
        });
        let mut artwork = Artwork::new().unwrap();
        artwork.image = Some(Arc::new(image));
        let mut buffer = Buffer::empty(area);
        artwork.render(area, &mut buffer);
        assert!(buffer.content.iter().all(|cell| {
            cell.fg == ratatui::style::Color::Rgb(0, 56, 0)
                && cell.bg == ratatui::style::Color::Rgb(0, 56, 0)
        }));
    }
}

fn read_path(stream: &mut TcpStream) -> String {
    stream
        .set_read_timeout(Some(Duration::from_secs(3)))
        .unwrap();
    let mut reader = BufReader::new(stream);
    let mut request = String::new();
    reader.read_line(&mut request).unwrap();
    let path = request.split_whitespace().nth(1).unwrap().to_owned();
    loop {
        let start = request.len();
        assert!(reader.read_line(&mut request).unwrap() > 0);
        assert!(request.len() < 4096);
        if &request[start..] == "\r\n" {
            break;
        }
    }
    path
}

fn queued_request_after_change(next: Option<&str>, cached: bool) -> Option<String> {
    let listener = TcpListener::bind("127.0.0.1:0").unwrap();
    listener.set_nonblocking(true).unwrap();
    let base = format!("http://{}", listener.local_addr().unwrap());
    let (started, observed) = mpsc::channel();
    let (release, released) = mpsc::channel();
    let serving = std::thread::spawn(move || {
        for index in 0..2 {
            let deadline = Instant::now() + Duration::from_secs(2);
            let (mut stream, _) = loop {
                match listener.accept() {
                    Ok(pair) => break pair,
                    Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
                        if Instant::now() >= deadline {
                            return;
                        }
                        std::thread::sleep(Duration::from_millis(2));
                    }
                    Err(error) => panic!("Fixture accept failed: {error}"),
                }
            };
            started.send(read_path(&mut stream)).unwrap();
            if index == 0 {
                released.recv_timeout(Duration::from_secs(3)).unwrap();
            }
            stream
                .write_all(
                    b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
                )
                .unwrap();
        }
    });
    let mut artwork = Artwork::new().unwrap();
    artwork.update(Some(&format!("{base}/a")));
    assert_eq!(observed.recv_timeout(Duration::from_secs(3)).unwrap(), "/a");
    artwork.update(Some(&format!("{base}/b")));
    let next = next.map(|path| format!("{base}{path}"));
    if cached {
        artwork
            .cache
            .push_back((next.clone().unwrap(), Arc::new(RgbImage::new(2, 2))));
    }
    artwork.update(next.as_deref());
    release.send(()).unwrap();
    let second = observed.recv_timeout(Duration::from_secs(3)).ok();
    drop(artwork);
    serving.join().unwrap();
    second
}

#[test]
fn hiding_visualization_discards_the_queued_cover_request() {
    assert_eq!(queued_request_after_change(None, false), None);
}

#[test]
fn latest_cover_replaces_the_obsolete_queued_request() {
    assert_eq!(
        queued_request_after_change(Some("/c"), false).as_deref(),
        Some("/c")
    );
}

#[test]
fn selecting_a_cached_cover_discards_the_obsolete_queued_request() {
    assert_eq!(queued_request_after_change(Some("/c"), true), None);
}