use super::*;
use std::{
fs::File,
io::{Cursor, Write},
};
use zip::{CompressionMethod, ZipWriter, write::SimpleFileOptions};
fn raster_image_bytes(format: ImageFormat, width_px: u32, height_px: u32) -> Vec<u8> {
let mut image = RgbaImage::new(width_px, height_px);
for pixel in image.pixels_mut() {
*pixel = Rgba([32, 128, 224, 255]);
}
let mut bytes = Vec::new();
DynamicImage::ImageRgba8(image)
.write_to(&mut Cursor::new(&mut bytes), format)
.expect("failed to write raster image bytes");
bytes
}
fn write_binary_zip_entries(path: &Path, entries: &[(&str, &[u8])]) {
let file = File::create(path).expect("failed to create zip");
let mut zip = ZipWriter::new(file);
let options = SimpleFileOptions::default().compression_method(CompressionMethod::Stored);
for (name, contents) in entries {
zip.start_file(name, options)
.expect("failed to start zip entry");
zip.write_all(contents).expect("failed to write zip entry");
}
zip.finish().expect("failed to finish zip");
}
fn wait_for_preview_prefetch(app: &mut App) {
for _ in 0..200 {
let _ = app.process_background_jobs();
let _ = app.process_preview_prefetch_timers();
if app.pending_preview_prefetch_timer().is_none() {
return;
}
thread::sleep(Duration::from_millis(10));
}
panic!("timed out waiting for preview prefetch");
}
#[test]
fn cached_adjacent_comic_page_queues_background_image_prepare() {
let root = temp_root("comic-adjacent-image-prepare");
fs::create_dir_all(&root).expect("failed to create temp root");
let archive = root.join("issue.cbz");
fs::write(&archive, b"cbz").expect("failed to write archive placeholder");
let archive_metadata = fs::metadata(&archive).expect("archive metadata should exist");
let current_page = root.join("page-1.jpg");
let next_page = root.join("page-2.jpg");
write_test_raster_image(¤t_page, ImageFormat::Jpeg, 1600, 900);
write_test_raster_image(&next_page, ImageFormat::Jpeg, 1600, 900);
let next_page_metadata = fs::metadata(&next_page).expect("next page metadata should exist");
let mut app = App::new_at(root.clone()).expect("app should initialize");
configure_terminal_image_support(&mut app);
app.navigation.entries = vec![Entry {
path: archive.clone(),
name: "issue.cbz".to_string(),
name_key: "issue.cbz".to_string(),
kind: EntryKind::File,
size: archive_metadata.len(),
modified: archive_metadata.modified().ok(),
readonly: false,
}];
app.navigation.selected = 0;
app.input.frame_state.preview_media_area = Some(Rect {
x: 2,
y: 3,
width: 48,
height: 20,
});
app.sync_comic_preview_selection();
app.preview.state.content = PreviewContent::new(PreviewKind::Comic, Vec::new())
.with_navigation_position("Page", 0, 2, None)
.with_preview_visual(PreviewVisual {
kind: PreviewVisualKind::PageImage,
layout: PreviewVisualLayout::FullHeight,
path: current_page,
size: 11 * 1024,
modified: None,
});
app.apply_current_comic_preview_metadata();
let adjacent_preview = PreviewContent::new(PreviewKind::Comic, Vec::new())
.with_navigation_position("Page", 1, 2, None)
.with_preview_visual(PreviewVisual {
kind: PreviewVisualKind::PageImage,
layout: PreviewVisualLayout::FullHeight,
path: next_page,
size: next_page_metadata.len(),
modified: next_page_metadata.modified().ok(),
});
let entry = app
.selected_entry()
.cloned()
.expect("selected entry should exist");
app.cache_preview_result(
&entry,
&preview::PreviewRequestOptions::ComicPage(1),
&adjacent_preview,
);
let adjacent_request = app.preview_visual_overlay_request_for_visual(
PreviewKind::Comic,
adjacent_preview
.preview_visual
.as_ref()
.expect("adjacent preview should have a visual"),
app.input
.frame_state
.preview_media_area
.expect("preview media area should exist"),
);
let adjacent_key = StaticImageKey::from_request(&adjacent_request);
app.refresh_static_image_preloads();
assert!(app.preview.image.pending_prepares.contains(&adjacent_key));
fs::remove_dir_all(root).expect("failed to remove temp root");
}
#[test]
fn cached_adjacent_epub_section_queues_background_image_prepare() {
let root = temp_root("epub-adjacent-image-prepare");
fs::create_dir_all(&root).expect("failed to create temp root");
let epub = root.join("story.epub");
fs::write(&epub, b"epub").expect("failed to write epub placeholder");
let epub_metadata = fs::metadata(&epub).expect("epub metadata should exist");
let current_page = root.join("page-1.jpg");
let next_page = root.join("page-2.jpg");
write_test_raster_image(¤t_page, ImageFormat::Jpeg, 1600, 900);
write_test_raster_image(&next_page, ImageFormat::Jpeg, 1600, 900);
let next_page_metadata = fs::metadata(&next_page).expect("next page metadata should exist");
let mut app = App::new_at(root.clone()).expect("app should initialize");
configure_terminal_image_support(&mut app);
app.navigation.entries = vec![Entry {
path: epub.clone(),
name: "story.epub".to_string(),
name_key: "story.epub".to_string(),
kind: EntryKind::File,
size: epub_metadata.len(),
modified: epub_metadata.modified().ok(),
readonly: false,
}];
app.navigation.selected = 0;
app.input.frame_state.preview_media_area = Some(Rect {
x: 2,
y: 3,
width: 48,
height: 20,
});
app.sync_epub_preview_selection();
app.preview.state.content = PreviewContent::new(PreviewKind::Document, Vec::new())
.with_ebook_section(0, 2, Some("Page 1".to_string()))
.with_preview_visual(PreviewVisual {
kind: PreviewVisualKind::PageImage,
layout: PreviewVisualLayout::FullHeight,
path: current_page,
size: 11 * 1024,
modified: None,
});
app.apply_current_epub_preview_metadata();
let adjacent_preview = PreviewContent::new(PreviewKind::Document, Vec::new())
.with_ebook_section(1, 2, Some("Page 2".to_string()))
.with_preview_visual(PreviewVisual {
kind: PreviewVisualKind::PageImage,
layout: PreviewVisualLayout::FullHeight,
path: next_page,
size: next_page_metadata.len(),
modified: next_page_metadata.modified().ok(),
});
let entry = app
.selected_entry()
.cloned()
.expect("selected entry should exist");
app.cache_preview_result(
&entry,
&preview::PreviewRequestOptions::EpubSection(1),
&adjacent_preview,
);
let adjacent_request = app.preview_visual_overlay_request_for_visual(
PreviewKind::Document,
adjacent_preview
.preview_visual
.as_ref()
.expect("adjacent preview should have a visual"),
app.input
.frame_state
.preview_media_area
.expect("preview media area should exist"),
);
let adjacent_key = StaticImageKey::from_request(&adjacent_request);
app.refresh_static_image_preloads();
assert!(app.preview.image.pending_prepares.contains(&adjacent_key));
fs::remove_dir_all(root).expect("failed to remove temp root");
}
#[test]
fn cached_adjacent_audio_cover_queues_background_image_prepare() {
let root = temp_root("audio-adjacent-cover-prepare");
fs::create_dir_all(&root).expect("failed to create temp root");
let first = root.join("001.mp3");
let second = root.join("002.mp3");
fs::write(&first, b"audio-one").expect("failed to write first audio fixture");
fs::write(&second, b"audio-two").expect("failed to write second audio fixture");
let first_metadata = fs::metadata(&first).expect("first audio metadata should exist");
let second_metadata = fs::metadata(&second).expect("second audio metadata should exist");
let current_cover = root.join("cover-1.png");
let next_cover = root.join("cover-2.png");
write_test_raster_image(¤t_cover, ImageFormat::Png, 900, 900);
write_test_raster_image(&next_cover, ImageFormat::Png, 900, 900);
let next_cover_metadata = fs::metadata(&next_cover).expect("next cover metadata should exist");
let mut app = App::new_at(root.clone()).expect("app should initialize");
configure_terminal_image_support(&mut app);
app.preview.terminal_images.protocol = ImageProtocol::ItermInline;
app.navigation.entries = vec![
Entry {
path: first.clone(),
name: "001.mp3".to_string(),
name_key: "001.mp3".to_string(),
kind: EntryKind::File,
size: first_metadata.len(),
modified: first_metadata.modified().ok(),
readonly: false,
},
Entry {
path: second.clone(),
name: "002.mp3".to_string(),
name_key: "002.mp3".to_string(),
kind: EntryKind::File,
size: second_metadata.len(),
modified: second_metadata.modified().ok(),
readonly: false,
},
];
app.navigation.selected = 0;
app.input.frame_state.preview_media_area = Some(Rect {
x: 2,
y: 3,
width: 48,
height: 20,
});
app.preview.state.content = PreviewContent::new(PreviewKind::Audio, Vec::new())
.with_detail("MP3 audio")
.with_preview_visual(PreviewVisual {
kind: PreviewVisualKind::Cover,
layout: PreviewVisualLayout::Inline,
path: current_cover,
size: 11 * 1024,
modified: None,
});
let adjacent_preview = PreviewContent::new(PreviewKind::Audio, Vec::new())
.with_detail("MP3 audio")
.with_preview_visual(PreviewVisual {
kind: PreviewVisualKind::Cover,
layout: PreviewVisualLayout::Inline,
path: next_cover,
size: next_cover_metadata.len(),
modified: next_cover_metadata.modified().ok(),
});
let adjacent_entry = app.navigation.entries[1].clone();
app.cache_preview_result(
&adjacent_entry,
&preview::PreviewRequestOptions::Default,
&adjacent_preview,
);
let adjacent_request = app.preview_visual_overlay_request_for_visual(
PreviewKind::Audio,
adjacent_preview
.preview_visual
.as_ref()
.expect("adjacent preview should have a visual"),
app.input
.frame_state
.preview_media_area
.expect("preview media area should exist"),
);
let adjacent_key = StaticImageKey::from_request(&adjacent_request);
app.refresh_static_image_preloads();
assert!(app.preview.image.pending_prepares.contains(&adjacent_key));
fs::remove_dir_all(root).expect("failed to remove temp root");
}
#[test]
fn stale_adjacent_comic_preview_result_immediately_queues_image_prepare() {
let root = temp_root("comic-stale-adjacent-image-prepare");
fs::create_dir_all(&root).expect("failed to create temp root");
let archive = root.join("issue.cbz");
let page_one = raster_image_bytes(ImageFormat::Jpeg, 1600, 900);
let page_two = raster_image_bytes(ImageFormat::Jpeg, 1600, 900);
write_binary_zip_entries(&archive, &[("1.jpg", &page_one), ("2.jpg", &page_two)]);
let mut app = App::new_at(root.clone()).expect("app should initialize");
configure_terminal_image_support(&mut app);
app.input.frame_state.preview_media_area = Some(Rect {
x: 2,
y: 3,
width: 48,
height: 20,
});
wait_for_preview_prefetch(&mut app);
let mut adjacent_key = None;
for _ in 0..200 {
let _ = app.process_preview_prefetch_timers();
let _ = app.process_background_jobs();
if !app.has_cached_comic_preview_page(&archive, 1) {
thread::sleep(Duration::from_millis(10));
continue;
}
adjacent_key = app
.nearby_comic_preview_visual_overlay_requests()
.into_iter()
.next()
.map(|request| StaticImageKey::from_request(&request));
if adjacent_key.is_some() {
break;
}
thread::sleep(Duration::from_millis(10));
}
let adjacent_key = adjacent_key.expect("adjacent comic preview should be cached");
assert!(
app.preview.image.pending_prepares.contains(&adjacent_key)
|| app.preview.image.dimensions.contains_key(&adjacent_key)
);
fs::remove_dir_all(root).expect("failed to remove temp root");
}
#[test]
fn cached_adjacent_comic_entry_preview_immediately_queues_image_prepare() {
let root = temp_root("comic-entry-adjacent-image-prepare");
fs::create_dir_all(&root).expect("failed to create temp root");
let first = root.join("001.cbz");
let second = root.join("002.cbz");
let first_page = raster_image_bytes(ImageFormat::Jpeg, 1600, 900);
let second_page = raster_image_bytes(ImageFormat::Jpeg, 1600, 900);
write_binary_zip_entries(&first, &[("1.jpg", &first_page)]);
write_binary_zip_entries(&second, &[("1.jpg", &second_page)]);
let mut app = App::new_at(root.clone()).expect("app should initialize");
configure_terminal_image_support(&mut app);
app.input.frame_state.preview_media_area = Some(Rect {
x: 2,
y: 3,
width: 48,
height: 20,
});
wait_for_displayed_preview_overlay(&mut app);
wait_for_preview_prefetch(&mut app);
let mut adjacent_key = None;
for _ in 0..200 {
let _ = app.process_preview_prefetch_timers();
let _ = app.process_background_jobs();
if !app.has_cached_comic_preview_page(&second, 0) {
thread::sleep(Duration::from_millis(10));
continue;
}
adjacent_key = app
.nearby_comic_entry_preview_visual_overlay_requests()
.into_iter()
.next()
.map(|request| StaticImageKey::from_request(&request));
if adjacent_key.is_some() {
break;
}
thread::sleep(Duration::from_millis(10));
}
let adjacent_key = adjacent_key.expect("adjacent comic entry preview should be cached");
assert!(
app.preview.image.pending_prepares.contains(&adjacent_key)
|| app.preview.image.dimensions.contains_key(&adjacent_key)
);
fs::remove_dir_all(root).expect("failed to remove temp root");
}