mod common;
use common::render_helpers;
use labelize::skill::diff_scanner;
use labelize::skill::element_analyzer;
use labelize::skill::models::DiffStatus;
use labelize::skill::snippet_extractor;
#[test]
fn test_load_and_parse_real_diff_report() {
let testdata = render_helpers::testdata_dir();
let report = diff_scanner::load_diff_report(&testdata);
match report {
Ok(report) => {
assert!(!report.entries.is_empty(), "report should have entries");
assert!(
report.entries.len() >= 50,
"expected at least 50 labels, got {}",
report.entries.len()
);
let sum = report.perfect_count
+ report.good_count
+ report.minor_count
+ report.moderate_count
+ report.high_count
+ report.skip_count
+ report.error_count;
assert_eq!(sum, report.total_labels);
let bstc = diff_scanner::scan_label(&report, "bstc").unwrap();
assert_eq!(bstc.status, DiffStatus::Perfect);
assert_eq!(bstc.diff_percent, 0.0);
let ups = diff_scanner::scan_label(&report, "ups");
assert!(ups.is_ok(), "ups label should exist");
assert!(ups.unwrap().diff_percent > 1.0, "ups should have diff > 1%");
println!("Parsed {} labels from diff report", report.entries.len());
println!(
" {} perfect, {} good, {} minor, {} moderate, {} high",
report.perfect_count,
report.good_count,
report.minor_count,
report.moderate_count,
report.high_count
);
}
Err(e) => {
eprintln!(
"Diff report not found (run `cargo test --test e2e_diff_report` first): {}",
e
);
}
}
}
#[test]
fn test_load_report_with_tolerances() {
let testdata = render_helpers::testdata_dir();
let report = diff_scanner::load_diff_report(&testdata);
if let Ok(mut report) = report {
let thresholds_path = std::path::Path::new("docs/DIFF_THRESHOLDS.md");
if thresholds_path.exists() {
let thresholds_text = std::fs::read_to_string(thresholds_path).unwrap();
diff_scanner::enrich_with_tolerances(&mut report, &thresholds_text);
let with_tol: Vec<_> = report
.entries
.iter()
.filter(|e| e.tolerance.is_some())
.collect();
assert!(
!with_tol.is_empty(),
"some entries should have tolerances from DIFF_THRESHOLDS.md"
);
let high_diff = diff_scanner::find_high_diff_labels(&report);
println!("{} labels exceed their tolerance:", high_diff.len());
for entry in &high_diff {
println!(
" {} — diff: {:.2}%, tolerance: {:.1}%",
entry.label_name,
entry.diff_percent,
entry.tolerance.unwrap_or(0.0)
);
}
}
}
}
#[test]
fn test_analyze_bstc_label() {
let testdata = render_helpers::testdata_dir();
let zpl_path = testdata.join("bstc.zpl");
let diff_path = testdata.join("diffs").join("bstc_diff.png");
if !zpl_path.exists() || !diff_path.exists() {
eprintln!("Skipping: bstc.zpl or diff image not found");
return;
}
let zpl = std::fs::read_to_string(&zpl_path).unwrap();
let labels = render_helpers::parse_zpl(zpl.as_bytes());
assert!(!labels.is_empty());
let result = element_analyzer::analyze_label(&labels[0], &diff_path);
match result {
Ok(contributions) => {
for c in &contributions {
assert_eq!(
c.diff_pixels_in_bbox, 0,
"bstc is perfect — no diff pixels expected"
);
}
println!(
"bstc analysis: {} elements, all with 0 diff pixels",
contributions.len()
);
}
Err(e) => {
eprintln!("Analysis error (expected for PERFECT labels): {}", e);
}
}
}
#[test]
fn test_analyze_label_with_diff() {
let testdata = render_helpers::testdata_dir();
let zpl_path = testdata.join("amazon.zpl");
let diff_path = testdata.join("diffs").join("amazon_diff.png");
if !zpl_path.exists() || !diff_path.exists() {
eprintln!("Skipping: amazon.zpl or diff image not found");
return;
}
let zpl = std::fs::read_to_string(&zpl_path).unwrap();
let labels = render_helpers::parse_zpl(zpl.as_bytes());
assert!(!labels.is_empty());
let result = element_analyzer::analyze_label(&labels[0], &diff_path);
match result {
Ok(contributions) => {
assert!(
!contributions.is_empty(),
"should have element contributions"
);
let report = element_analyzer::format_analysis_report(&contributions);
println!("{}", report);
for w in contributions.windows(2) {
assert!(
w[0].contribution_to_total >= w[1].contribution_to_total,
"contributions should be sorted descending"
);
}
let nonzero: Vec<_> = contributions
.iter()
.filter(|c| c.diff_pixels_in_bbox > 0)
.collect();
assert!(
!nonzero.is_empty(),
"amazon should have elements with diff pixels"
);
}
Err(e) => {
eprintln!("Analysis error: {}", e);
}
}
}
#[test]
fn test_extract_snippets_from_real_label() {
let testdata = render_helpers::testdata_dir();
let zpl_path = testdata.join("amazon.zpl");
if !zpl_path.exists() {
eprintln!("Skipping: amazon.zpl not found");
return;
}
let zpl = std::fs::read_to_string(&zpl_path).unwrap();
let commands = snippet_extractor::split_zpl_commands(&zpl);
let spans = snippet_extractor::group_commands_into_spans(&commands);
let globals = snippet_extractor::extract_global_state_commands(&commands);
println!(
"amazon.zpl: {} commands, {} element spans, {} global state commands",
commands.len(),
spans.len(),
globals.len()
);
if !spans.is_empty() {
let snippet = snippet_extractor::extract_element(&zpl, "amazon", 0, 2.26).unwrap();
assert!(snippet.zpl_content.starts_with("^XA"));
assert!(snippet.zpl_content.contains("^XZ"));
println!("Snippet 0 content:\n{}", snippet.zpl_content);
let opts = render_helpers::default_options();
let result = std::panic::catch_unwind(|| {
render_helpers::render_zpl_to_png(&snippet.zpl_content, opts)
});
assert!(
result.is_ok(),
"extracted snippet should render successfully"
);
}
}
#[test]
fn test_full_analysis_pipeline() {
let testdata = render_helpers::testdata_dir();
let report = match diff_scanner::load_diff_report(&testdata) {
Ok(r) => r,
Err(e) => {
eprintln!("Skipping: {}", e);
return;
}
};
let above_1pct = diff_scanner::find_labels_above_threshold(&report, 1.0);
println!("{} labels above 1% diff:", above_1pct.len());
if let Some(entry) = above_1pct.first() {
let zpl_path = testdata.join(format!("{}.{}", entry.label_name, entry.extension));
let diff_path = testdata
.join("diffs")
.join(format!("{}_diff.png", entry.label_name));
if !zpl_path.exists() || !diff_path.exists() {
eprintln!(
"Skipping full pipeline: files not found for {}",
entry.label_name
);
return;
}
let zpl = std::fs::read_to_string(&zpl_path).unwrap();
let labels = render_helpers::parse_zpl(zpl.as_bytes());
if labels.is_empty() {
eprintln!("No labels parsed from {}", entry.label_name);
return;
}
let contributions = element_analyzer::analyze_label(&labels[0], &diff_path).unwrap();
println!(
"\nAnalysis for {} (diff: {:.2}%):",
entry.label_name, entry.diff_percent
);
println!(
"{}",
element_analyzer::format_analysis_report(&contributions)
);
if let Some(top) = contributions.first() {
if top.diff_pixels_in_bbox > 0 {
let snippet = snippet_extractor::extract_element(
&zpl,
&entry.label_name,
top.bbox.element_index,
entry.diff_percent,
);
match snippet {
Ok(s) => {
println!(
"Extracted snippet for element {} ({:?}):\n{}",
top.bbox.element_index, top.bbox.element_type, s.zpl_content
);
}
Err(e) => {
eprintln!("Extract error: {}", e);
}
}
}
}
}
}
#[test]
fn test_classify_no_diff_element() {
use labelize::skill::models::{
DiffClassification, ElementBBox, ElementDiffContribution, ElementType,
};
let contrib = ElementDiffContribution {
bbox: ElementBBox {
x: 0,
y: 0,
width: 100,
height: 50,
element_index: 0,
element_type: ElementType::Text,
zpl_command: "^FD".to_string(),
},
diff_pixels_in_bbox: 0,
total_pixels_in_bbox: 5000,
local_diff_percent: 0.0,
contribution_to_total: 0.0,
classification: None,
position_offset: None,
};
assert_eq!(contrib.classification, None);
assert!(contrib.position_offset.is_none());
}
#[test]
fn test_fix_category_from_classification_content() {
use labelize::skill::models::{DiffClassification, ElementType, FixCategory};
assert_eq!(
FixCategory::from_classification(&ElementType::Text, &DiffClassification::ContentDiff),
FixCategory::FontMetrics
);
assert_eq!(
FixCategory::from_classification(
&ElementType::Barcode128,
&DiffClassification::ContentDiff
),
FixCategory::BarcodeEncoding
);
assert_eq!(
FixCategory::from_classification(
&ElementType::GraphicBox,
&DiffClassification::ContentDiff
),
FixCategory::GraphicRendering
);
}
#[test]
fn test_fix_category_from_classification_position() {
use labelize::skill::models::{DiffClassification, ElementType, FixCategory};
assert_eq!(
FixCategory::from_classification(&ElementType::Text, &DiffClassification::PositionDiff),
FixCategory::PositionOffset
);
assert_eq!(
FixCategory::from_classification(
&ElementType::Barcode128,
&DiffClassification::PositionDiff
),
FixCategory::PositionOffset
);
assert_eq!(
FixCategory::from_classification(&ElementType::Text, &DiffClassification::Mixed),
FixCategory::PositionOffset
);
}
#[test]
fn test_centroid_shift_detection_synthetic() {
use image::{Rgba, RgbaImage};
use labelize::skill::diff_classifier::compute_diff_centroid;
use labelize::skill::models::{ElementBBox, ElementType};
let mut img = RgbaImage::from_pixel(400, 400, Rgba([255u8, 255, 255, 255]));
for y in 50u32..70 {
for x in 300u32..320 {
img.put_pixel(x, y, Rgba([255, 0, 0, 255]));
}
}
let bbox = ElementBBox {
x: 0,
y: 0,
width: 400,
height: 400,
element_index: 0,
element_type: ElementType::Text,
zpl_command: "^FD".to_string(),
};
let centroid = compute_diff_centroid(&img, &bbox);
assert!(
centroid.is_some(),
"centroid should be computed for non-empty diff"
);
let (cx, cy) = centroid.unwrap();
assert!(
(cx - 309.5).abs() < 2.0,
"centroid x={} expected ~309.5",
cx
);
assert!((cy - 59.5).abs() < 2.0, "centroid y={} expected ~59.5", cy);
}
#[test]
fn test_centroid_shift_no_pixels() {
use image::{Rgba, RgbaImage};
use labelize::skill::diff_classifier::compute_diff_centroid;
use labelize::skill::models::{ElementBBox, ElementType};
let img = RgbaImage::from_pixel(200, 200, Rgba([255u8, 255, 255, 255]));
let bbox = ElementBBox {
x: 0,
y: 0,
width: 200,
height: 200,
element_index: 0,
element_type: ElementType::GraphicBox,
zpl_command: "^GB".to_string(),
};
assert!(compute_diff_centroid(&img, &bbox).is_none());
}
#[test]
fn test_compute_image_diff_percent_identical() {
use image::{Rgba, RgbaImage};
use labelize::skill::diff_classifier::compute_image_diff_percent;
let img = RgbaImage::from_pixel(100, 100, Rgba([0u8, 0, 0, 255]));
assert_eq!(compute_image_diff_percent(&img, &img), 0.0);
}
#[test]
fn test_compute_image_diff_percent_completely_different() {
use image::{Rgba, RgbaImage};
use labelize::skill::diff_classifier::compute_image_diff_percent;
let black = RgbaImage::from_pixel(100, 100, Rgba([0u8, 0, 0, 255]));
let white = RgbaImage::from_pixel(100, 100, Rgba([255u8, 255, 255, 255]));
let diff = compute_image_diff_percent(&black, &white);
assert!(
diff > 90.0,
"completely different images should have high diff, got {}",
diff
);
}
#[test]
fn test_compute_image_diff_percent_size_mismatch() {
use image::{Rgba, RgbaImage};
use labelize::skill::diff_classifier::compute_image_diff_percent;
let a = RgbaImage::from_pixel(100, 100, Rgba([0u8, 0, 0, 255]));
let b = RgbaImage::from_pixel(200, 200, Rgba([0u8, 0, 0, 255]));
assert_eq!(
compute_image_diff_percent(&a, &b),
100.0,
"size mismatch should be 100%"
);
}
#[test]
fn test_render_snippet_isolated() {
use labelize::skill::diff_classifier::render_snippet_isolated;
let zpl = "^XA^FO50,50^ADN,36,20^FDHello^FS^XZ";
let result = render_snippet_isolated(zpl);
assert!(
result.is_ok(),
"simple snippet should render without error: {:?}",
result.err()
);
let img = result.unwrap();
assert_eq!(img.width(), 813, "expected 813px width");
assert_eq!(img.height(), 1626, "expected 1626px height");
}
#[test]
fn test_classify_real_label_diffs_offline() {
use labelize::skill::diff_classifier::{classify_element_diffs, ClassifyOptions};
let testdata = render_helpers::testdata_dir();
let zpl_path = testdata.join("amazon.zpl");
let diff_path = testdata.join("diffs").join("amazon_diff.png");
if !zpl_path.exists() || !diff_path.exists() {
eprintln!("Skipping test_classify_real_label_diffs_offline: files not found");
return;
}
let zpl = std::fs::read_to_string(&zpl_path).unwrap();
let labels = render_helpers::parse_zpl(zpl.as_bytes());
assert!(!labels.is_empty());
let mut contributions = match element_analyzer::analyze_label(&labels[0], &diff_path) {
Ok(c) => c,
Err(e) => {
eprintln!("Skipping: analyze_label error: {}", e);
return;
}
};
let diff_img = image::open(&diff_path).unwrap().to_rgba8();
let opts = ClassifyOptions {
use_labelary: false, ..Default::default()
};
let result = classify_element_diffs(&mut contributions, &zpl, &diff_img, &opts);
assert!(
result.is_ok(),
"classification should succeed: {:?}",
result.err()
);
let with_diff: Vec<_> = contributions
.iter()
.filter(|c| c.diff_pixels_in_bbox > 0)
.collect();
let with_class: Vec<_> = with_diff
.iter()
.filter(|c| c.classification.is_some())
.collect();
println!(
"amazon: {} elements with diff, {} classified",
with_diff.len(),
with_class.len()
);
for c in &contributions {
if c.diff_pixels_in_bbox > 0 {
println!(
" elem {} ({:?}): diff={} pixels, classification={:?}, offset={:?}",
c.bbox.element_index,
c.bbox.element_type,
c.diff_pixels_in_bbox,
c.classification,
c.position_offset
);
}
}
assert!(
!with_class.is_empty() || with_diff.is_empty(),
"elements with diff pixels should get a classification"
);
let report = element_analyzer::format_analysis_report(&contributions);
println!("\n{}", report);
}