#![allow(dead_code)]
use embedded_charts::{
chart::traits::{Chart, ChartConfig},
data::{point::Point2D, series::StaticDataSeries, DataSeries},
error::ChartResult,
};
use embedded_graphics::{
mock_display::MockDisplay, pixelcolor::Rgb565, prelude::*, primitives::Rectangle,
};
use super::{create_test_display, TEST_VIEWPORT};
pub struct VisualTester;
impl VisualTester {
pub fn capture_chart_output<T>(
chart: &T,
data: &StaticDataSeries<Point2D, 256>,
) -> ChartResult<ChartSnapshot>
where
T: Chart<Rgb565, Data = StaticDataSeries<Point2D, 256>, Config = ChartConfig<Rgb565>>,
{
let config = super::create_test_config();
let mut display = create_test_display();
chart.draw(data, &config, TEST_VIEWPORT, &mut display)?;
Ok(ChartSnapshot {
affected_area: display.affected_area(),
pixel_count: Self::count_drawn_pixels(&display),
bounds: display.bounding_box(),
})
}
pub fn compare_snapshots(baseline: &ChartSnapshot, current: &ChartSnapshot) -> VisualDiff {
VisualDiff {
area_match: baseline.affected_area == current.affected_area,
pixel_count_diff: current.pixel_count as i32 - baseline.pixel_count as i32,
bounds_match: baseline.bounds == current.bounds,
similarity_score: Self::calculate_similarity(baseline, current),
}
}
pub fn validate_visual_quality<T>(
chart: &T,
data: &StaticDataSeries<Point2D, 256>,
) -> ChartResult<VisualQualityReport>
where
T: Chart<Rgb565, Data = StaticDataSeries<Point2D, 256>, Config = ChartConfig<Rgb565>>,
{
let snapshot = Self::capture_chart_output(chart, data)?;
let mut report = VisualQualityReport::new();
if !data.is_empty() && snapshot.pixel_count == 0 {
report
.issues
.push(heapless::String::try_from("Chart draws no pixels with valid data").unwrap())
.ok();
}
if snapshot.bounds.size.width > TEST_VIEWPORT.size.width
|| snapshot.bounds.size.height > TEST_VIEWPORT.size.height
{
report
.issues
.push(
heapless::String::try_from("Chart rendering exceeds viewport bounds").unwrap(),
)
.ok();
}
let viewport_area = TEST_VIEWPORT.size.width * TEST_VIEWPORT.size.height;
let pixel_ratio = snapshot.pixel_count as f32 / viewport_area as f32;
if pixel_ratio > 0.8 {
report
.issues
.push(heapless::String::try_from("Chart may be over-rendered (too dense)").unwrap())
.ok();
} else if pixel_ratio < 0.01 && !data.is_empty() {
report
.issues
.push(
heapless::String::try_from("Chart may be under-rendered (too sparse)").unwrap(),
)
.ok();
}
Ok(report)
}
pub fn test_rendering_consistency<T>(
chart: &T,
data: &StaticDataSeries<Point2D, 256>,
iterations: usize,
) -> ChartResult<bool>
where
T: Chart<Rgb565, Data = StaticDataSeries<Point2D, 256>, Config = ChartConfig<Rgb565>>,
{
if iterations == 0 {
return Ok(true);
}
let baseline = Self::capture_chart_output(chart, data)?;
for _ in 1..iterations {
let current = Self::capture_chart_output(chart, data)?;
let diff = Self::compare_snapshots(&baseline, ¤t);
if !diff.is_identical() {
return Ok(false);
}
}
Ok(true)
}
pub fn test_color_themes<T>(
chart: &T,
data: &StaticDataSeries<Point2D, 256>,
) -> ChartResult<heapless::Vec<ChartSnapshot, 8>>
where
T: Chart<Rgb565, Data = StaticDataSeries<Point2D, 256>, Config = ChartConfig<Rgb565>>,
{
let themes = [
ChartConfig {
title: None,
background_color: Some(Rgb565::WHITE),
margins: super::TEST_MARGINS,
grid_color: Some(Rgb565::CSS_LIGHT_GRAY),
show_grid: true,
},
ChartConfig {
title: None,
background_color: Some(Rgb565::BLACK),
margins: super::TEST_MARGINS,
grid_color: Some(Rgb565::CSS_DARK_GRAY),
show_grid: true,
},
ChartConfig {
title: None,
background_color: None,
margins: super::TEST_MARGINS,
grid_color: Some(Rgb565::BLUE),
show_grid: false,
},
];
let mut snapshots = heapless::Vec::new();
for theme in &themes {
let mut display = create_test_display();
chart.draw(data, theme, TEST_VIEWPORT, &mut display)?;
snapshots
.push(ChartSnapshot {
affected_area: display.affected_area(),
pixel_count: Self::count_drawn_pixels(&display),
bounds: display.bounding_box(),
})
.ok();
}
Ok(snapshots)
}
pub fn generate_test_pattern() -> StaticDataSeries<Point2D, 256> {
let mut series = StaticDataSeries::new();
let test_points = [
(0.0, 0.0), (10.0, 10.0), (20.0, 0.0), (30.0, 20.0), (40.0, 5.0), ];
for (x, y) in test_points.iter() {
series.push(Point2D::new(*x, *y)).ok();
}
series
}
fn count_drawn_pixels(display: &MockDisplay<Rgb565>) -> usize {
let area = display.affected_area();
(area.size.width * area.size.height) as usize
}
fn calculate_similarity(baseline: &ChartSnapshot, current: &ChartSnapshot) -> f32 {
let area_match = if baseline.affected_area == current.affected_area {
1.0
} else {
0.0
};
let bounds_match = if baseline.bounds == current.bounds {
1.0
} else {
0.0
};
let pixel_diff = (baseline.pixel_count as i32 - current.pixel_count as i32).abs();
let max_pixels = baseline.pixel_count.max(current.pixel_count).max(1);
let pixel_similarity = 1.0 - (pixel_diff as f32 / max_pixels as f32);
(area_match + bounds_match + pixel_similarity) / 3.0
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ChartSnapshot {
pub affected_area: Rectangle,
pub pixel_count: usize,
pub bounds: Rectangle,
}
#[derive(Debug, Clone)]
pub struct VisualDiff {
pub area_match: bool,
pub pixel_count_diff: i32,
pub bounds_match: bool,
pub similarity_score: f32,
}
impl VisualDiff {
pub fn is_identical(&self) -> bool {
self.area_match && self.bounds_match && self.pixel_count_diff == 0
}
pub fn is_acceptable(&self, tolerance: f32) -> bool {
self.similarity_score >= tolerance
}
}
#[derive(Debug, Clone)]
pub struct VisualQualityReport {
pub issues: heapless::Vec<heapless::String<64>, 10>,
pub score: f32,
}
impl VisualQualityReport {
pub fn new() -> Self {
Self {
issues: heapless::Vec::new(),
score: 1.0,
}
}
pub fn has_issues(&self) -> bool {
!self.issues.is_empty()
}
pub fn is_acceptable(&self, min_score: f32) -> bool {
self.score >= min_score && !self.has_issues()
}
}
pub struct PixelAnalyzer;
impl PixelAnalyzer {
pub fn analyze_pixel_distribution(snapshot: &ChartSnapshot) -> PixelDistribution {
PixelDistribution {
total_pixels: snapshot.pixel_count,
density: snapshot.pixel_count as f32
/ (snapshot.bounds.size.width * snapshot.bounds.size.height) as f32,
coverage_area: snapshot.affected_area,
}
}
pub fn validate_pixel_density(distribution: &PixelDistribution) -> bool {
distribution.density >= 0.01 && distribution.density <= 0.8
}
}
#[derive(Debug, Clone)]
pub struct PixelDistribution {
pub total_pixels: usize,
pub density: f32,
pub coverage_area: Rectangle,
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "line")]
use super::super::data_generators;
#[test]
#[cfg(feature = "line")]
#[ignore = "MockDisplay has limitations with pixel overlap detection"]
fn test_visual_capture() {
use embedded_charts::chart::line::LineChart;
let chart = LineChart::new();
let data = data_generators::generate_test_data(super::super::TestDataPattern::Linear, 5);
let result = VisualTester::capture_chart_output(&chart, &data);
assert!(result.is_ok());
}
#[test]
fn test_snapshot_comparison() {
let snap1 = ChartSnapshot {
affected_area: Rectangle::new(Point::new(0, 0), Size::new(100, 100)),
pixel_count: 1000,
bounds: Rectangle::new(Point::new(0, 0), Size::new(100, 100)),
};
let snap2 = snap1.clone();
let diff = VisualTester::compare_snapshots(&snap1, &snap2);
assert!(diff.is_identical());
assert_eq!(diff.similarity_score, 1.0);
}
#[test]
fn test_visual_quality_report() {
let mut report = VisualQualityReport::new();
assert!(!report.has_issues());
assert!(report.is_acceptable(0.8));
report
.issues
.push(heapless::String::try_from("Test issue").unwrap())
.ok();
assert!(report.has_issues());
}
#[test]
fn test_pixel_analyzer() {
let snapshot = ChartSnapshot {
affected_area: Rectangle::new(Point::new(0, 0), Size::new(100, 100)),
pixel_count: 5000,
bounds: Rectangle::new(Point::new(0, 0), Size::new(100, 100)),
};
let distribution = PixelAnalyzer::analyze_pixel_distribution(&snapshot);
assert_eq!(distribution.total_pixels, 5000);
assert!(PixelAnalyzer::validate_pixel_density(&distribution));
}
}