#![cfg(all(feature = "rendering", target_os = "linux"))]
use std::collections::HashSet;
use std::fs::{create_dir_all, read, read_to_string, write};
use std::io::Cursor;
use std::path::{Path, PathBuf};
use image::{DynamicImage, ImageFormat, load_from_memory_with_format};
use image_compare::rgba_hybrid_compare;
use martin_core::styles::StyleSources;
use rstest::rstest;
use tempfile::NamedTempFile;
const PROXIED_HOSTS: &[(&str, u16)] = &[
("https://demotiles.maplibre.org", 18081),
("https://tiles.openfreemap.org", 18082),
];
fn rewrite_style(original: &Path) -> (NamedTempFile, PathBuf) {
let body = read_to_string(original).expect("read style");
let mut rewritten = body;
for (host, port) in PROXIED_HOSTS {
rewritten = rewritten.replace(host, &format!("http://127.0.0.1:{port}"));
}
let tmp = NamedTempFile::with_suffix(".json").expect("create style tempfile");
write(tmp.path(), rewritten).expect("write rewritten style");
let path = tmp.path().to_path_buf();
(tmp, path)
}
#[rstest]
#[case::maplibre_demo_png("maplibre_demo.json", (0, 0, 0), ImageFormat::Png, "png")]
#[case::maplibre_demo_zoom1_png("maplibre_demo.json", (1, 0, 0), ImageFormat::Png, "png")]
#[case::maptiler_basic_png("src2/maptiler_basic.json", (0, 0, 0), ImageFormat::Png, "png")]
#[case::maplibre_demo_jpeg("maplibre_demo.json", (0, 0, 0), ImageFormat::Jpeg, "jpeg")]
#[case::maplibre_demo_zoom1_jpeg("maplibre_demo.json", (1, 0, 0), ImageFormat::Jpeg, "jpeg")]
#[case::maptiler_basic_jpeg("src2/maptiler_basic.json", (0, 0, 0), ImageFormat::Jpeg, "jpeg")]
#[tokio::test]
async fn render_tile_with_fixtures(
#[case] style_file: &str,
#[case] (z, x, y): (u8, u32, u32),
#[case] format: ImageFormat,
#[case] ext: &str,
) {
let style_dir = Path::new("../tests/fixtures/styles/");
let (_tmp, style_path) = rewrite_style(&style_dir.join(style_file));
let mut styles = StyleSources::default();
styles
.enable_rendering(None)
.expect("spawn render pool for tests");
let rendered = styles.render(style_path, z, x, y).await.unwrap();
let rendered_img = rendered.as_image();
let (width, height) = (rendered_img.width(), rendered_img.height());
assert_eq!((width, height), (512, 512), "Rendered tile must be 512x512");
let pixels: HashSet<_> = rendered_img.pixels().copied().collect();
assert!(
pixels.len() > 1,
"Rendered image is blank (all pixels identical)"
);
let encoded_img = if format == ImageFormat::Jpeg {
DynamicImage::ImageRgb8(DynamicImage::ImageRgba8(rendered_img.clone()).to_rgb8())
} else {
DynamicImage::ImageRgba8(rendered_img.clone())
};
let mut rendered_buf = Cursor::new(Vec::new());
encoded_img.write_to(&mut rendered_buf, format).unwrap();
let rendered_bytes = rendered_buf.into_inner();
match format {
ImageFormat::Png => {
assert!(
rendered_bytes.starts_with(&[0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A]),
"Encoded bytes are not valid PNG (wrong magic)"
);
}
ImageFormat::Jpeg => {
assert!(
rendered_bytes.starts_with(&[0xFF, 0xD8, 0xFF]),
"Encoded bytes are not valid JPEG (wrong magic)"
);
}
_ => {}
}
let reference_name = format!(
"{}_{}_{}_{}.{ext}",
style_file.replace('/', "_").replace(".json", ""),
z,
x,
y
);
let reference_path = Path::new("../tests/fixtures/rendering_references").join(&reference_name);
let reference_bytes = read(&reference_path).unwrap_or_else(|_| {
create_dir_all(reference_path.parent().unwrap()).unwrap();
assert!(
rendered_bytes.len() > 1000,
"Refusing to bless suspiciously small image ({} bytes)",
rendered_bytes.len()
);
write(&reference_path, &rendered_bytes).unwrap();
panic!(
"Created new reference image at {reference_path:?}. Commit this file and re-run the test."
);
});
let rendered_for_cmp = load_from_memory_with_format(&rendered_bytes, format)
.unwrap()
.to_rgba8();
let reference_for_cmp = load_from_memory_with_format(&reference_bytes, format)
.unwrap()
.to_rgba8();
let similarity = rgba_hybrid_compare(&reference_for_cmp, &rendered_for_cmp)
.unwrap_or_else(|e| panic!("image_compare failed: {e}"));
let min_similarity = if format == ImageFormat::Jpeg {
0.95
} else {
0.99
};
let score = similarity.score;
assert!(
score >= min_similarity,
"Rendered image {reference_name} differs from reference: similarity score {score:.4} < {min_similarity}. \
If this is expected, delete the existing reference file and regenerate it using the current rendering output.",
);
}