summaryrefslogtreecommitdiff
path: root/tests/src/reference_utils.rs
blob: 8232d4c9f26d091f13645d19dc9518a1b8afbbb7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
//! Utilities for the reference image test suite.
//!
//! This module has utility functions that are used in the test suite
//! to compare rendered surfaces to reference images.

use cairo;

use std::convert::TryFrom;
use std::env;
use std::fs::{self, File};
use std::io::{BufReader, Read};
use std::path::{Path, PathBuf};
use std::sync::Once;

use rsvg::surface_utils::shared_surface::{SharedImageSurface, SurfaceType};

use crate::compare_surfaces::{compare_surfaces, BufferDiff, Diff};

pub struct Reference(SharedImageSurface);

impl Reference {
    pub fn from_png<P>(path: P) -> Result<Self, cairo::IoError>
    where
        P: AsRef<Path>,
    {
        let file = File::open(path).map_err(|e| cairo::IoError::Io(e))?;
        let mut reader = BufReader::new(file);
        let surface = surface_from_png(&mut reader)?;
        Self::from_surface(surface)
    }

    pub fn from_surface(surface: cairo::ImageSurface) -> Result<Self, cairo::IoError> {
        let shared = SharedImageSurface::wrap(surface, SurfaceType::SRgb)?;
        Ok(Self(shared))
    }
}

pub trait Compare {
    fn compare(self, surface: &SharedImageSurface) -> Result<BufferDiff, cairo::IoError>;
}

impl Compare for &Reference {
    fn compare(self, surface: &SharedImageSurface) -> Result<BufferDiff, cairo::IoError> {
        compare_surfaces(&self.0, surface).map_err(cairo::IoError::from)
    }
}

impl Compare for Result<Reference, cairo::IoError> {
    fn compare(self, surface: &SharedImageSurface) -> Result<BufferDiff, cairo::IoError> {
        self.map(|reference| reference.compare(surface))
            .and_then(std::convert::identity)
    }
}

pub trait Evaluate {
    fn evaluate(&self, output_surface: &SharedImageSurface, output_base_name: &str);
}

impl Evaluate for BufferDiff {
    /// Evaluates a BufferDiff and panics if there are relevant differences
    ///
    /// The `output_base_name` is used to write test results if the
    /// surfaces are different.  If this is `foo`, this will write
    /// `foo-out.png` with the `output_surf` and `foo-diff.png` with a
    /// visual diff between `output_surf` and the `Reference` that this
    /// diff was created from.
    ///
    /// # Panics
    ///
    /// Will panic if the surfaces are too different to be acceptable.
    fn evaluate(&self, output_surf: &SharedImageSurface, output_base_name: &str) {
        match self {
            BufferDiff::DifferentSizes => unreachable!("surfaces should be of the same size"),

            BufferDiff::Diff(diff) => {
                if diff.distinguishable() {
                    println!(
                        "{}: {} pixels changed with maximum difference of {}",
                        output_base_name, diff.num_pixels_changed, diff.max_diff,
                    );

                    write_to_file(output_surf, output_base_name, "out");
                    write_to_file(&diff.surface, output_base_name, "diff");

                    if diff.inacceptable() {
                        panic!("surfaces are too different");
                    }
                }
            }
        }
    }
}

impl Evaluate for Result<BufferDiff, cairo::IoError> {
    fn evaluate(&self, output_surface: &SharedImageSurface, output_base_name: &str) {
        self.as_ref()
            .map(|diff| diff.evaluate(output_surface, output_base_name))
            .unwrap();
    }
}

fn write_to_file(input: &SharedImageSurface, output_base_name: &str, suffix: &str) {
    let path = output_dir().join(&format!("{}-{}.png", output_base_name, suffix));
    println!("{}: {}", suffix, path.to_string_lossy());
    let mut output_file = File::create(path).unwrap();
    input
        .clone()
        .into_image_surface()
        .unwrap()
        .write_to_png(&mut output_file)
        .unwrap();
}

/// Creates a directory for test output and returns its path.
///
/// The location for the output directory is taken from the `TESTS_OUTPUT_DIR` environment
/// variable if that is set. Otherwise std::env::temp_dir() will be used, which is
/// a platform dependent location for temporary files.
///
/// # Panics
///
/// Will panic if the output directory can not be created.
pub fn output_dir() -> PathBuf {
    let tempdir = || {
        let mut path = env::temp_dir();
        path.push("rsvg-test-output");
        path
    };
    let path = env::var_os("TESTS_OUTPUT_DIR").map_or_else(tempdir, PathBuf::from);

    fs::create_dir_all(&path).expect("could not create output directory for tests");

    path
}

fn tolerable_difference() -> u8 {
    static mut TOLERANCE: u8 = 8;

    static ONCE: Once = Once::new();
    ONCE.call_once(|| unsafe {
        if let Ok(str) = env::var("RSVG_TEST_TOLERANCE") {
            let value: usize = str
                .parse()
                .expect("Can not parse RSVG_TEST_TOLERANCE as a number");
            TOLERANCE =
                u8::try_from(value).expect("RSVG_TEST_TOLERANCE should be between 0 and 255");
        }
    });

    unsafe { TOLERANCE }
}

pub trait Deviation {
    fn distinguishable(&self) -> bool;
    fn inacceptable(&self) -> bool;
}

impl Deviation for Diff {
    fn distinguishable(&self) -> bool {
        self.max_diff > 2
    }

    fn inacceptable(&self) -> bool {
        self.max_diff > tolerable_difference()
    }
}

/// Creates a cairo::ImageSurface from a stream of PNG data.
///
/// The surface is converted to ARGB if needed. Use this helper function with `Reference`.
pub fn surface_from_png<R>(stream: &mut R) -> Result<cairo::ImageSurface, cairo::IoError>
where
    R: Read,
{
    let png = cairo::ImageSurface::create_from_png(stream)?;
    let argb = cairo::ImageSurface::create(cairo::Format::ARgb32, png.width(), png.height())?;
    {
        // convert to ARGB; the PNG may come as Rgb24
        let cr = cairo::Context::new(&argb).expect("Failed to create a cairo context");
        cr.set_source_surface(&png, 0.0, 0.0).unwrap();
        cr.paint().unwrap();
    }
    Ok(argb)
}

/// Macro test that compares render outputs
///
/// Takes in SurfaceSize width and height, setting the cairo surface
#[macro_export]
macro_rules! test_compare_render_output {
    ($test_name:ident, $width:expr, $height:expr, $test:expr, $reference:expr $(,)?) => {
        #[test]
        fn $test_name() {
            crate::utils::setup_font_map();

            let sx: i32 = $width;
            let sy: i32 = $height;
            let svg = load_svg($test).unwrap();
            let output_surf = render_document(
                &svg,
                SurfaceSize(sx, sy),
                |_| (),
                cairo::Rectangle::new(0.0, 0.0, f64::from(sx), f64::from(sy)),
            )
            .unwrap();

            let reference = load_svg($reference).unwrap();
            let reference_surf = render_document(
                &reference,
                SurfaceSize(sx, sy),
                |_| (),
                cairo::Rectangle::new(0.0, 0.0, f64::from(sx), f64::from(sy)),
            )
            .unwrap();

            Reference::from_surface(reference_surf.into_image_surface().unwrap())
                .compare(&output_surf)
                .evaluate(&output_surf, stringify!($test_name));
        }
    };
}

/// Render two SVG files and compare them.
///
/// This is used to implement reference tests, or reftests.  Use it like this:
///
/// ```ignore
/// test_svg_reference!(test_name, "tests/fixtures/blah/foo.svg", "tests/fixtures/blah/foo-ref.svg");
/// ```
///
/// This will ensure that `foo.svg` and `foo-ref.svg` have exactly the same intrinsic dimensions,
/// and that they produce the same rendered output.
#[macro_export]
macro_rules! test_svg_reference {
    ($test_name:ident, $test_filename:expr, $reference_filename:expr) => {
        #[test]
        fn $test_name() {
            use crate::reference_utils::{Compare, Evaluate, Reference};
            use crate::utils::{render_document, setup_font_map, SurfaceSize};
            use cairo;
            use rsvg::{CairoRenderer, Loader};

            setup_font_map();

            let svg = Loader::new()
                .read_path($test_filename)
                .expect("reading SVG test file");
            let reference = Loader::new()
                .read_path($reference_filename)
                .expect("reading reference file");

            let svg_renderer = CairoRenderer::new(&svg);
            let ref_renderer = CairoRenderer::new(&reference);

            let svg_dim = svg_renderer.intrinsic_dimensions();
            let ref_dim = ref_renderer.intrinsic_dimensions();

            assert_eq!(
                svg_dim, ref_dim,
                "sizes of SVG document and reference file are different"
            );

            let pixels = svg_renderer
                .intrinsic_size_in_pixels()
                .unwrap_or((100.0, 100.0));

            let output_surf = render_document(
                &svg,
                SurfaceSize(pixels.0.ceil() as i32, pixels.1.ceil() as i32),
                |_| (),
                cairo::Rectangle::new(0.0, 0.0, pixels.0, pixels.1),
            )
            .unwrap();

            let reference_surf = render_document(
                &reference,
                SurfaceSize(pixels.0.ceil() as i32, pixels.1.ceil() as i32),
                |_| (),
                cairo::Rectangle::new(0.0, 0.0, pixels.0, pixels.1),
            )
            .unwrap();

            Reference::from_surface(reference_surf.into_image_surface().unwrap())
                .compare(&output_surf)
                .evaluate(&output_surf, stringify!($test_name));
        }
    };
}