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