001/*
002 *    GeoAPI - Java interfaces for OGC/ISO standards
003 *    http://www.geoapi.org
004 *
005 *    Copyright (C) 2008-2019 Open Geospatial Consortium, Inc.
006 *    All Rights Reserved. http://www.opengeospatial.org/ogc/legal
007 *
008 *    Permission to use, copy, and modify this software and its documentation, with
009 *    or without modification, for any purpose and without fee or royalty is hereby
010 *    granted, provided that you include the following on ALL copies of the software
011 *    and documentation or portions thereof, including modifications, that you make:
012 *
013 *    1. The full text of this NOTICE in a location viewable to users of the
014 *       redistributed or derivative work.
015 *    2. Notice of any changes or modifications to the OGC files, including the
016 *       date changes were made.
017 *
018 *    THIS SOFTWARE AND DOCUMENTATION IS PROVIDED "AS IS," AND COPYRIGHT HOLDERS MAKE
019 *    NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
020 *    TO, WARRANTIES OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT
021 *    THE USE OF THE SOFTWARE OR DOCUMENTATION WILL NOT INFRINGE ANY THIRD PARTY
022 *    PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS.
023 *
024 *    COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL OR
025 *    CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR DOCUMENTATION.
026 *
027 *    The name and trademarks of copyright holders may NOT be used in advertising or
028 *    publicity pertaining to the software without specific, written prior permission.
029 *    Title to copyright in this software and any associated documentation will at all
030 *    times remain with copyright holders.
031 */
032package org.opengis.test;
033
034import java.util.Collection;
035import java.awt.Shape;
036import java.awt.geom.Rectangle2D;
037import java.awt.geom.PathIterator;
038import java.awt.geom.AffineTransform;
039import java.awt.image.RenderedImage;
040
041import org.opengis.util.InternationalString;
042import org.opengis.metadata.Identifier;
043import org.opengis.metadata.citation.Citation;
044import org.opengis.referencing.operation.Matrix;
045import org.opengis.referencing.cs.AxisDirection;
046import org.opengis.referencing.cs.CoordinateSystem;
047import org.opengis.test.coverage.image.PixelIterator;
048
049
050/**
051 * Extension to JUnit assertion methods.
052 * This class inherits all assertion methods from the {@link org.junit.Assert org.junit.Assert} class.
053 * Consequently, developers can replace the following statement:
054 *
055 * <blockquote><pre>import static org.junit.Assert.*;</pre></blockquote>
056 *
057 * by
058 *
059 * <blockquote><pre>import static org.opengis.test.Assert.*;</pre></blockquote>
060 *
061 * if they wish to use the assertion methods defined here in addition of JUnit methods.
062 *
063 * @author  Martin Desruisseaux (Geomatys)
064 * @version 3.1
065 * @since   2.2
066 */
067public strictfp class Assert extends org.junit.Assert {
068    /**
069     * The keyword for unrestricted value in {@link String} arguments.
070     */
071    private static final String UNRESTRICTED = "##unrestricted";
072
073    /**
074     * For subclass constructors only.
075     */
076    protected Assert() {
077    }
078
079    /**
080     * Returns the given message, or an empty string if the message is null.
081     */
082    private static String nonNull(final String message) {
083        return (message != null) ? message.trim().concat(" ") : "";
084    }
085
086    /**
087     * Returns the concatenation of the given message with the given extension.
088     * This method returns the given extension if the message is null or empty.
089     *
090     * <p>Invoking this method is equivalent to invoking {@code nonNull(message) + ext},
091     * but avoid the creation of temporary objects in the common case where the message
092     * is null.</p>
093     *
094     * @param  message  the message, or {@code null}.
095     * @param  ext      the extension to append after the message.
096     * @return the concatenated string.
097     */
098    private static String concat(String message, final String ext) {
099        if (message == null || (message = message.trim()).isEmpty()) {
100            return ext;
101        }
102        return message + ' ' + ext;
103    }
104
105    /**
106     * Verifies if we expected a null value, then returns {@code true} if the value is null as expected.
107     */
108    private static boolean isNull(final String message, final Object expected, final Object actual) {
109        final boolean isNull = (actual == null);
110        if (isNull != (expected == null)) {
111            fail(concat(message, isNull ? "Value is null." : "Expected null."));
112        }
113        return isNull;
114    }
115
116    /**
117     * Asserts that the given value is an instance of the given class. No tests are performed if
118     * the type is {@code null}. If the type is not-null but the value is null, this is considered
119     * as a failure.
120     *
121     * @param message       header of the exception message in case of failure, or {@code null} if none.
122     * @param expectedType  the expected parent class of the value, or {@code null} if unrestricted.
123     * @param value         the value to test, or {@code null} (which is a failure).
124     */
125    public static void assertInstanceOf(final String message, final Class<?> expectedType, final Object value) {
126        if (expectedType != null && !expectedType.isInstance(value)) {
127            if (value == null) {
128                fail(nonNull(message) + "Value is null.");
129            } else {
130                String expectedName = expectedType.getSimpleName();
131                String actualName = value.getClass().getSimpleName();
132                if (expectedName.equals(actualName)) {
133                    expectedName = expectedType.getCanonicalName();
134                    actualName = value.getClass().getCanonicalName();
135                }
136                fail(nonNull(message) + "Value \"" + value + "\" is of type " + actualName +
137                        " while the expected type was " + expectedName + " or a subtype.");
138            }
139        }
140    }
141
142    /**
143     * Asserts that the given integer value is positive, including zero.
144     *
145     * @param message  header of the exception message in case of failure, or {@code null} if none.
146     * @param value   The value to test.
147     */
148    public static void assertPositive(final String message, final int value) {
149        if (value < 0) {
150            fail(nonNull(message) + "Value is " + value + '.');
151        }
152    }
153
154    /**
155     * Asserts that the given integer value is strictly positive, excluding zero.
156     *
157     * @param message  header of the exception message in case of failure, or {@code null} if none.
158     * @param value    the value to test.
159     */
160    public static void assertStrictlyPositive(final String message, final int value) {
161        if (value <= 0) {
162            fail(nonNull(message) + "Value is " + value + '.');
163        }
164    }
165
166    /**
167     * Asserts that the given minimum and maximum values make a valid range. More specifically
168     * asserts that if both values are non-null, then the minimum value is not greater than the
169     * maximum value.
170     *
171     * @param <T>      the type of values being compared.
172     * @param message  header of the exception message in case of failure, or {@code null} if none.
173     * @param minimum  the lower bound of the range to test, or {@code null} if unbounded.
174     * @param maximum  the upper bound of the range to test, or {@code null} if unbounded.
175     */
176    @SuppressWarnings("unchecked")
177    public static <T> void assertValidRange(final String message, final Comparable<T> minimum, final Comparable<T> maximum) {
178        if (minimum != null && maximum != null) {
179            if (minimum.compareTo((T) maximum) > 0) {
180                fail(nonNull(message) + "Range found is [" + minimum + " ... " + maximum + "].");
181            }
182        }
183    }
184
185    /**
186     * Asserts that the given minimum is smaller or equals to the given maximum.
187     *
188     * @param message  header of the exception message in case of failure, or {@code null} if none.
189     * @param minimum  the lower bound of the range to test.
190     * @param maximum  the upper bound of the range to test.
191     */
192    public static void assertValidRange(final String message, final int minimum, final int maximum) {
193        if (minimum > maximum) {
194            fail(nonNull(message) + "Range found is [" + minimum + " ... " + maximum + "].");
195        }
196    }
197
198    /**
199     * Asserts that the given minimum is smaller or equals to the given maximum.
200     * If one bound is or both bounds are {@linkplain Double#NaN NaN}, then the test fails.
201     *
202     * @param message  header of the exception message in case of failure, or {@code null} if none.
203     * @param minimum  the lower bound of the range to test.
204     * @param maximum  the upper bound of the range to test.
205     */
206    public static void assertValidRange(final String message, final double minimum, final double maximum) {
207        if (!(minimum <= maximum)) { // Use '!' for catching NaN.
208            fail(nonNull(message) + "Range found is [" + minimum + " ... " + maximum + "].");
209        }
210    }
211
212    /**
213     * Asserts that the given value is inside the given range. This method does <strong>not</strong>
214     * test the validity of the given [{@code minimum} … {@code maximum}] range.
215     *
216     * @param <T>      the type of values being compared.
217     * @param message  header of the exception message in case of failure, or {@code null} if none.
218     * @param minimum  the lower bound of the range (inclusive), or {@code null} if unbounded.
219     * @param maximum  the upper bound of the range (inclusive), or {@code null} if unbounded.
220     * @param value    the value to test, or {@code null} (which is a failure).
221     */
222    public static <T> void assertBetween(final String message, final Comparable<T> minimum, final Comparable<T> maximum, T value) {
223        if (minimum != null) {
224            if (minimum.compareTo(value) > 0) {
225                fail(nonNull(message) + "Value " + value + " is less than " + minimum + '.');
226            }
227        }
228        if (maximum != null) {
229            if (maximum.compareTo(value) < 0) {
230                fail(nonNull(message) + "Value " + value + " is greater than " + maximum + '.');
231            }
232        }
233    }
234
235    /**
236     * Asserts that the given value is inside the given range. This method does <strong>not</strong>
237     * test the validity of the given [{@code minimum} … {@code maximum}] range.
238     *
239     * @param message  header of the exception message in case of failure, or {@code null} if none.
240     * @param minimum  the lower bound of the range, inclusive.
241     * @param maximum  the upper bound of the range, inclusive.
242     * @param value    the value to test.
243     */
244    public static void assertBetween(final String message, final int minimum, final int maximum, final int value) {
245        if (value < minimum) {
246            fail(nonNull(message) + "Value " + value + " is less than " + minimum + '.');
247        }
248        if (value > maximum) {
249            fail(nonNull(message) + "Value " + value + " is greater than " + maximum + '.');
250        }
251    }
252
253    /**
254     * Asserts that the given value is inside the given range. If the given {@code value} is
255     * {@linkplain Double#NaN NaN}, then this test passes silently. This method does <strong>not</strong>
256     * test the validity of the given [{@code minimum} … {@code maximum}] range.
257     *
258     * @param message  header of the exception message in case of failure, or {@code null} if none.
259     * @param minimum  the lower bound of the range, inclusive.
260     * @param maximum  the upper bound of the range, inclusive.
261     * @param value    the value to test.
262     */
263    public static void assertBetween(final String message, final double minimum, final double maximum, final double value) {
264        if (value < minimum) {
265            fail(nonNull(message) + "Value " + value + " is less than " + minimum + '.');
266        }
267        if (value > maximum) {
268            fail(nonNull(message) + "Value " + value + " is greater than " + maximum + '.');
269        }
270    }
271
272    /**
273     * Asserts that the given value is contained in the given collection. If the given collection
274     * is null, then this test passes silently (a null collection is considered as "unknown", not
275     * empty). If the given value is null, then the test passes only if the given collection
276     * contains the null element.
277     *
278     * @param message     header of the exception message in case of failure, or {@code null} if none.
279     * @param collection  the collection where to look for inclusion, or {@code null} if unrestricted.
280     * @param value       the value to test for inclusion.
281     */
282    public static void assertContains(final String message, final Collection<?> collection, final Object value) {
283        if (collection != null) {
284            if (!collection.contains(value)) {
285                fail(nonNull(message) + "Looked for value \"" + value + "\" in a collection of " +
286                        collection.size() + "elements.");
287            }
288        }
289    }
290
291    /**
292     * Asserts that the title or an alternate title of the given citation is equal to the given string.
293     * This method is typically used for testing if a citation stands for the OGC, OGP or EPSG authority
294     * for instance. Such abbreviations are often declared as {@linkplain Citation#getAlternateTitles()
295     * alternate titles} rather than the main {@linkplain Citation#getTitle() title}, but this method
296     * tests both for safety.
297     *
298     * @param message   header of the exception message in case of failure, or {@code null} if none.
299     * @param expected  the expected title or alternate title.
300     * @param actual    the citation to test.
301     *
302     * @since 3.1
303     */
304    public static void assertAnyTitleEquals(final String message, final String expected, final Citation actual) {
305        if (isNull(message, expected, actual)) {
306            return;
307        }
308        InternationalString title = actual.getTitle();
309        if (title != null && expected.equals(title.toString())) {
310            return;
311        }
312        for (final InternationalString t : actual.getAlternateTitles()) {
313            if (expected.equals(t.toString())) {
314                return;
315            }
316        }
317        fail(concat(message, '"' + expected + "\" not found in title or alternate titles."));
318    }
319
320    /**
321     * Asserts that the given identifier is equals to the given authority, code space, version and code.
322     * If any of the above-cited properties is {@code ""##unrestricted"}, then it will not be verified.
323     * This flexibility is useful in the common case where a test accepts any {@code version} value.
324     *
325     * @param message    header of the exception message in case of failure, or {@code null} if none.
326     * @param authority  the expected authority title or alternate title (may be {@code null}), or {@code "##unrestricted"}.
327     * @param codeSpace  the expected code space (may be {@code null}), or {@code "##unrestricted"}.
328     * @param version    the expected version    (may be {@code null}), or {@code "##unrestricted"}.
329     * @param code       the expected code value (may be {@code null}), or {@code "##unrestricted"}.
330     * @param actual     the identifier to test.
331     *
332     * @since 3.1
333     */
334    public static void assertIdentifierEquals(final String message, final String authority, final String codeSpace,
335            final String version, final String code, final Identifier actual)
336    {
337        if (actual == null) {
338            fail(concat(message, "Identifier is null"));
339        } else {
340            if (!UNRESTRICTED.equals(authority)) assertAnyTitleEquals(message,                     authority, actual.getAuthority());
341            if (!UNRESTRICTED.equals(codeSpace)) assertEquals(concat(message, "Wrong code space"), codeSpace, actual.getCodeSpace());
342            if (!UNRESTRICTED.equals(version))   assertEquals(concat(message, "Wrong version"),    version,   actual.getVersion());
343            if (!UNRESTRICTED.equals(code))      assertEquals(concat(message, "Wrong code"),       code,      actual.getCode());
344        }
345    }
346
347    /**
348     * @deprecated Renamed {@link #assertUnicodeIdentifierEquals(String, CharSequence, CharSequence, boolean)}
349     * for avoiding confusion with the {@code Identifier} interface.
350     *
351     * @param message   header of the exception message in case of failure, or {@code null} if none.
352     * @param expected  the expected character sequence.
353     * @param value     the character sequence to compare.
354     */
355    @Deprecated
356    public static void assertIdentifierEquals(final String message, final CharSequence expected, final CharSequence value) {
357        assertUnicodeIdentifierEquals(message, expected, value, true);
358    }
359
360    /**
361     * Asserts that the character sequences are equal, ignoring any characters that are not valid for Unicode identifiers.
362     * First, this method locates the {@linkplain Character#isUnicodeIdentifierStart(int) Unicode identifier start}
363     * in each sequences, ignoring any other characters before them. Then, starting from the identifier starts, this
364     * method compares only the {@linkplain Character#isUnicodeIdentifierPart(int) Unicode identifier parts} until
365     * the end of character sequences.
366     *
367     * <p><b>Examples:</b> {@code "WGS 84"} and {@code "WGS84"} as equal according this method.</p>
368     *
369     * @param message     header of the exception message in case of failure, or {@code null} if none.
370     * @param expected    the expected character sequence (may be {@code null}), or {@code "##unrestricted"}.
371     * @param actual      the character sequence to compare, or {@code null}.
372     * @param ignoreCase  {@code true} for ignoring case.
373     *
374     * @since 3.1
375     */
376    public static void assertUnicodeIdentifierEquals(final String message,
377            final CharSequence expected, final CharSequence actual, final boolean ignoreCase)
378    {
379        if (UNRESTRICTED.equals(expected) || isNull(message, expected, actual)) {
380            return;
381        }
382        final int expLength = expected.length();
383        final int valLength = actual.length();
384        int       expOffset = 0;
385        int       valOffset = 0;
386        boolean   expPart   = false;
387        boolean   valPart   = false;
388        while (expOffset < expLength) {
389            int expCode = Character.codePointAt(expected, expOffset);
390            if (isUnicodeIdentifier(expCode, expPart)) {
391                expPart = true;
392                int valCode;
393                do {
394                    if (valOffset >= valLength) {
395                        fail(nonNull(message) + "Expected \"" + expected + "\" but got \"" + actual + "\". "
396                                + "Missing part: \"" + expected.subSequence(expOffset, expLength) + "\".");
397                        return;
398                    }
399                    valCode    = Character.codePointAt(actual, valOffset);
400                    valOffset += Character.charCount(valCode);
401                } while (!isUnicodeIdentifier(valCode, valPart));
402                valPart = true;
403                if (ignoreCase) {
404                    expCode = Character.toLowerCase(expCode);
405                    valCode = Character.toLowerCase(valCode);
406                }
407                if (valCode != expCode) {
408                    fail(nonNull(message) + "Expected \"" + expected + "\" but got \"" + actual + "\".");
409                    return;
410                }
411            }
412            expOffset += Character.charCount(expCode);
413        }
414        while (valOffset < valLength) {
415            final int valCode = Character.codePointAt(actual, valOffset);
416            if (isUnicodeIdentifier(valCode, valPart)) {
417                fail(nonNull(message) + "Expected \"" + expected + "\", but found it with a unexpected "
418                        + "trailing string: \"" + actual.subSequence(valOffset, valLength) + "\".");
419            }
420            valOffset += Character.charCount(valCode);
421        }
422    }
423
424    /**
425     * Returns {@code true} if the given codepoint is an unicode identifier start or part.
426     */
427    private static boolean isUnicodeIdentifier(final int codepoint, final boolean part) {
428        return part ? Character.isUnicodeIdentifierPart (codepoint)
429                    : Character.isUnicodeIdentifierStart(codepoint);
430    }
431
432    /**
433     * Asserts that all axes in the given coordinate system are pointing toward the given
434     * directions, in the same order.
435     *
436     * @param message   header of the exception message in case of failure, or {@code null} if none.
437     * @param cs        the coordinate system to test.
438     * @param expected  the expected axis directions.
439     *
440     * @since 3.1
441     */
442    public static void assertAxisDirectionsEqual(String message,
443            final CoordinateSystem cs, final AxisDirection... expected)
444    {
445        assertEquals(concat(message, "Wrong coordinate system dimension."), expected.length, cs.getDimension());
446        message = concat(message, "Wrong axis direction.");
447        for (int i=0; i<expected.length; i++) {
448            assertEquals(message, expected[i], cs.getAxis(i).getDirection());
449        }
450    }
451
452    /**
453     * Asserts that the given matrix is equals to the expected one, up to the given tolerance value.
454     *
455     * @param message    header of the exception message in case of failure, or {@code null} if none.
456     * @param expected   the expected matrix, which may be {@code null}.
457     * @param actual     the matrix to compare, or {@code null}.
458     * @param tolerance  the tolerance threshold.
459     *
460     * @since 3.1
461     *
462     * @see org.opengis.test.referencing.TransformTestCase#assertMatrixEquals(String, Matrix, Matrix, Matrix)
463     */
464    public static void assertMatrixEquals(final String message, final Matrix expected, final Matrix actual, final double tolerance) {
465        if (isNull(message, expected, actual)) {
466            return;
467        }
468        final int numRow = actual.getNumRow();
469        final int numCol = actual.getNumCol();
470        assertEquals("numRow", expected.getNumRow(), numRow);
471        assertEquals("numCol", expected.getNumCol(), numCol);
472        for (int j=0; j<numRow; j++) {
473            for (int i=0; i<numCol; i++) {
474                final double e = expected.getElement(j,i);
475                final double a = actual.getElement(j,i);
476                if (!(StrictMath.abs(e - a) <= tolerance) && Double.doubleToLongBits(a) != Double.doubleToLongBits(e)) {
477                    fail(nonNull(message) + "Matrix.getElement(" + j + ", " + i + "): expected " + e + " but got " + a);
478                }
479            }
480        }
481    }
482
483    /**
484     * Asserts that all control points of two shapes are equal.
485     * This method performs the following checks:
486     *
487     * <ol>
488     *   <li>Ensures that the {@linkplain Shape#getBounds2D() shape bounds} are equal,
489     *       up to the given tolerance thresholds.</li>
490     *   <li>{@linkplain Shape#getPathIterator(AffineTransform) Gets the path iterator} of each shape.</li>
491     *   <li>Ensures that the {@linkplain PathIterator#getWindingRule() winding rules} are equal.</li>
492     *   <li>Iterates over all path segments until the iteration {@linkplain PathIterator#isDone() is done}.
493     *       For each iteration step:<ol>
494     *       <li>Invokes {@link PathIterator#currentSegment(double[])}.</li>
495     *       <li>Ensures that the segment type (one of the {@code SEG_*} constants) is the same.</li>
496     *       <li>Ensures that the ordinate values are equal, up to the given tolerance thresholds.</li>
497     *   </ol></li>
498     * </ol>
499     *
500     * @param message     header of the exception message in case of failure, or {@code null} if none.
501     * @param expected    the expected shape, which may be {@code null}.
502     * @param actual      the actual shape, or {@code null}.
503     * @param toleranceX  the tolerance threshold for <var>x</var> ordinate values.
504     * @param toleranceY  the tolerance threshold for <var>y</var> ordinate values.
505     *
506     * @since 3.1
507     */
508    public static void assertShapeEquals(String message, final Shape expected,
509            final Shape actual, final double toleranceX, final double toleranceY)
510    {
511        if (isNull(message, expected, actual)) {
512            return;
513        }
514        final Rectangle2D b0 = expected.getBounds2D();
515        final Rectangle2D b1 = actual  .getBounds2D();
516        final String mismatch = concat(message, "Mismatched bounds.");
517        assertEquals(mismatch, b0.getMinX(), b1.getMinX(), toleranceX);
518        assertEquals(mismatch, b0.getMaxX(), b1.getMaxX(), toleranceX);
519        assertEquals(mismatch, b0.getMinY(), b1.getMinY(), toleranceY);
520        assertEquals(mismatch, b0.getMaxY(), b1.getMaxY(), toleranceY);
521        assertPathEquals(message, expected.getPathIterator(null), actual.getPathIterator(null), toleranceX, toleranceY);
522    }
523
524    /**
525     * Asserts that all control points in two geometric paths are equal.
526     * This method performs the following checks:
527     *
528     * <ol>
529     *   <li>Ensures that the {@linkplain PathIterator#getWindingRule() winding rules} are equal.</li>
530     *   <li>Iterates over all path segments until the iteration {@linkplain PathIterator#isDone() is done}.
531     *       For each iteration step:<ol>
532     *       <li>Invokes {@link PathIterator#currentSegment(double[])}.</li>
533     *       <li>Ensures that the segment type (one of the {@code SEG_*} constants) is the same.</li>
534     *       <li>Ensures that the ordinate values are equal, up to the given tolerance thresholds.</li>
535     *   </ol></li>
536     * </ol>
537     *
538     * This method can be used instead of {@link #assertShapeEquals(String, Shape, Shape, double, double)}
539     * when the tester needs to compare the shapes with a non-null affine transform or a flatness factor.
540     * in such case, the tester needs to invoke the {@link Shape#getPathIterator(AffineTransform, double)}
541     * method himself.
542     *
543     * @param message     header of the exception message in case of failure, or {@code null} if none.
544     * @param expected    the expected path, which may be {@code null}.
545     * @param actual      the actual path, or {@code null}.
546     * @param toleranceX  the tolerance threshold for <var>x</var> ordinate values.
547     * @param toleranceY  the tolerance threshold for <var>y</var> ordinate values.
548     *
549     * @since 3.1
550     */
551    public static void assertPathEquals(final String message, final PathIterator expected,
552            final PathIterator actual, final double toleranceX, final double toleranceY)
553    {
554        if (isNull(message, expected, actual)) {
555            return;
556        }
557        assertEquals(concat(message, "Mismatched winding rule."), expected.getWindingRule(), actual.getWindingRule());
558        final String   mismatchedType = concat(message, "Mismatched path segment type.");
559        final String   mismatchedX    = concat(message, "Mismatched X ordinate value.");
560        final String   mismatchedY    = concat(message, "Mismatched Y ordinate value.");
561        final String   endOfPath      = concat(message, "Premature end of path.");
562        final double[] expectedCoords = new double[6];
563        final double[] actualCoords   = new double[6];
564        while (!expected.isDone()) {
565            assertFalse(endOfPath, actual.isDone());
566            final int type = expected.currentSegment(expectedCoords);
567            assertEquals(mismatchedType, type, actual.currentSegment(actualCoords));
568            final int length;
569            switch (type) {
570                case PathIterator.SEG_CLOSE:   length = 0; break;
571                case PathIterator.SEG_MOVETO:  // Fallthrough
572                case PathIterator.SEG_LINETO:  length = 2; break;
573                case PathIterator.SEG_QUADTO:  length = 4; break;
574                case PathIterator.SEG_CUBICTO: length = 6; break;
575                default: throw new AssertionError(nonNull(message) + "Unknown segment type: " + type);
576            }
577            for (int i=0; i<length;) {
578                assertEquals(mismatchedX, expectedCoords[i], actualCoords[i++], toleranceX);
579                assertEquals(mismatchedY, expectedCoords[i], actualCoords[i++], toleranceY);
580            }
581            actual.next();
582            expected.next();
583        }
584        assertTrue(concat(message, "Expected end of path."), actual.isDone());
585    }
586
587    /**
588     * Asserts that all sample values in the given images are equal. This method requires the images
589     * {@linkplain RenderedImage#getWidth() width}, {@linkplain RenderedImage#getHeight() height}
590     * and the {@linkplain java.awt.image.SampleModel#getNumBands() number of bands} to be equal,
591     * but does <em>not</em> require the {@linkplain RenderedImage#getTile(int, int) tiling},
592     * {@linkplain java.awt.image.ColorModel color model} or
593     * {@linkplain java.awt.image.SampleModel#getDataType() datatype} to be equal.
594     *
595     * @param message    header of the exception message in case of failure, or {@code null} if none.
596     * @param expected   an image containing the expected values, which may be {@code null}.
597     * @param actual     the actual image containing the sample values to compare, or {@code null}.
598     * @param tolerance  tolerance threshold for floating point comparisons.
599     *                   This threshold is ignored if both images use integer datatype.
600     *
601     * @see PixelIterator#assertSampleValuesEqual(PixelIterator, double)
602     *
603     * @since 3.1
604     */
605    public static void assertSampleValuesEqual(final String message, final RenderedImage expected,
606            final RenderedImage actual, final double tolerance)
607    {
608        if (isNull(message, expected, actual)) {
609            return;
610        }
611        assertEquals(concat(message, "Mismatched image width."),  expected.getWidth(),  actual.getWidth());
612        assertEquals(concat(message, "Mismatched image height."), expected.getHeight(), actual.getHeight());
613        assertEquals(concat(message, "Mismatched number of bands."),
614                expected.getSampleModel().getNumBands(), actual.getSampleModel().getNumBands());
615        final PixelIterator iterator = new PixelIterator(expected);
616        iterator.assertSampleValuesEqual(new PixelIterator(actual), tolerance);
617    }
618}