blob: 36d0edce43910dacc22951c9402ac9a975ad38c7 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sis.test;
import java.util.Set;
import java.util.Map;
import java.util.Objects;
import java.util.Iterator;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.LinkedHashMap;
import java.util.stream.Stream;
import java.util.function.Consumer;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentHashMap;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import org.apache.sis.util.Utilities;
import org.apache.sis.util.CharSequences;
import org.apache.sis.util.ComparisonMode;
import org.apache.sis.util.Exceptions;
import org.apache.sis.util.Classes;
/**
* Assertion methods used by the SIS project in addition of the JUnit and GeoAPI assertions.
*
* @author Martin Desruisseaux (Geomatys)
* @author Alexis Manin (Geomatys)
* @version 1.0
* @since 0.3
* @module
*/
public strictfp class Assert extends org.opengis.test.Assert {
/**
* For subclass constructor only.
*/
protected Assert() {
}
/**
* Asserts that the two given objects are not equal.
* This method tests all {@link ComparisonMode} except {@code DEBUG}.
*
* @param o1 the first object.
* @param o2 the second object.
*/
public static void assertNotDeepEquals(final Object o1, final Object o2) {
assertNotSame("same", o1, o2);
assertFalse("equals", Objects .equals (o1, o2));
assertFalse("deepEquals", Objects .deepEquals(o1, o2));
assertFalse("deepEquals(STRICT)", Utilities.deepEquals(o1, o2, ComparisonMode.STRICT));
assertFalse("deepEquals(BY_CONTRACT)", Utilities.deepEquals(o1, o2, ComparisonMode.BY_CONTRACT));
assertFalse("deepEquals(IGNORE_METADATA)", Utilities.deepEquals(o1, o2, ComparisonMode.IGNORE_METADATA));
assertFalse("deepEquals(APPROXIMATE)", Utilities.deepEquals(o1, o2, ComparisonMode.APPROXIMATE));
}
/**
* Asserts that the two given objects are approximately equal, while slightly different.
* More specifically, this method asserts that the given objects are equal according the
* {@link ComparisonMode#APPROXIMATE} criterion, but not equal according the
* {@link ComparisonMode#IGNORE_METADATA} criterion.
*
* @param expected the expected object.
* @param actual the actual object.
*/
public static void assertAlmostEquals(final Object expected, final Object actual) {
assertFalse("Shall not be strictly equals", Utilities.deepEquals(expected, actual, ComparisonMode.STRICT));
assertFalse("Shall be slightly different", Utilities.deepEquals(expected, actual, ComparisonMode.IGNORE_METADATA));
assertTrue ("Shall be approximately equals", Utilities.deepEquals(expected, actual, ComparisonMode.DEBUG));
assertTrue ("DEBUG inconsistent with APPROXIMATE", Utilities.deepEquals(expected, actual, ComparisonMode.APPROXIMATE));
}
/**
* Asserts that the two given objects are equal ignoring metadata.
* See {@link ComparisonMode#IGNORE_METADATA} for more information.
*
* @param expected the expected object.
* @param actual the actual object.
*/
public static void assertEqualsIgnoreMetadata(final Object expected, final Object actual) {
assertTrue("Shall be approximately equals", Utilities.deepEquals(expected, actual, ComparisonMode.DEBUG));
assertTrue("DEBUG inconsistent with APPROXIMATE", Utilities.deepEquals(expected, actual, ComparisonMode.APPROXIMATE));
assertTrue("Shall be equal, ignoring metadata", Utilities.deepEquals(expected, actual, ComparisonMode.IGNORE_METADATA));
}
/**
* Asserts that the two given arrays contains objects that are equal ignoring metadata.
* See {@link ComparisonMode#IGNORE_METADATA} for more information.
*
* @param expected the expected objects (array can be {@code null}).
* @param actual the actual objects (array can be {@code null}).
*
* @since 0.7
*/
public static void assertArrayEqualsIgnoreMetadata(final Object[] expected, final Object[] actual) {
if (expected != actual) {
if (expected == null) {
assertNull("Expected null array.", actual);
} else {
assertNotNull("Expected non-null array.", actual);
final int length = StrictMath.min(expected.length, actual.length);
for (int i=0; i<length; i++) try {
assertEqualsIgnoreMetadata(expected[i], actual[i]);
} catch (AssertionError e) {
throw new AssertionError(Exceptions.formatChainedMessages(null, "Comparison failure at index "
+ i + " (a " + Classes.getShortClassName(actual[i]) + ").", e), e);
}
assertEquals("Unexpected array length.", expected.length, actual.length);
}
}
}
/**
* Asserts that two strings are equal, ignoring the differences in EOL characters.
* The comparisons is performed one a line-by-line basis. For each line, trailing
* spaces (but not leading spaces) are ignored.
*
* @param expected the expected string.
* @param actual the actual string.
*/
public static void assertMultilinesEquals(final CharSequence expected, final CharSequence actual) {
assertMultilinesEquals(null, expected, actual);
}
/**
* Asserts that two strings are equal, ignoring the differences in EOL characters.
* The comparisons is performed one a line-by-line basis. For each line, trailing
* spaces (but not leading spaces) are ignored.
*
* @param message the message to print in case of failure, or {@code null} if none.
* @param expected the expected string.
* @param actual the actual string.
*/
public static void assertMultilinesEquals(final String message, final CharSequence expected, final CharSequence actual) {
final CharSequence[] expectedLines = CharSequences.splitOnEOL(expected);
final CharSequence[] actualLines = CharSequences.splitOnEOL(actual);
final int length = StrictMath.min(expectedLines.length, actualLines.length);
final StringBuilder buffer = new StringBuilder(message != null ? message : "Line").append('[');
final int base = buffer.length();
for (int i=0; i<length; i++) {
CharSequence e = expectedLines[i];
CharSequence a = actualLines[i];
e = e.subSequence(0, CharSequences.skipTrailingWhitespaces(e, 0, e.length()));
a = a.subSequence(0, CharSequences.skipTrailingWhitespaces(a, 0, a.length()));
assertEquals(buffer.append(i).append(']').toString(), e, a);
buffer.setLength(base);
}
if (expectedLines.length > actualLines.length) {
fail(buffer.append(length).append("] missing line: ").append(expectedLines[length]).toString());
}
if (expectedLines.length < actualLines.length) {
fail(buffer.append(length).append("] extraneous line: ").append(actualLines[length]).toString());
}
}
/**
* Verifies that the given stream produces the same values than the given iterator, in same order.
* This method assumes that the given stream is sequential.
*
* @param <E> the type of values to test.
* @param expected the expected values.
* @param actual the stream to compare with the expected values.
*
* @since 0.8
*/
public static <E> void assertSequentialStreamEquals(final Iterator<E> expected, final Stream<E> actual) {
actual.forEach(new Consumer<E>() {
private int count;
@Override
public void accept(final Object value) {
if (!expected.hasNext()) {
fail("Expected " + count + " elements, but the stream contains more.");
}
final Object ex = expected.next();
if (!Objects.equals(ex, value)) {
fail("Expected " + ex + " at index " + count + " but got " + value);
}
count++;
}
});
assertFalse("Unexpected end of stream.", expected.hasNext());
}
/**
* Verifies that the given stream produces the same values than the given iterator, in any order.
* This method is designed for use with parallel streams, but works with sequential streams too.
*
* @param <E> the type of values to test.
* @param expected the expected values.
* @param actual the stream to compare with the expected values.
*
* @since 0.8
*/
public static <E> void assertParallelStreamEquals(final Iterator<E> expected, final Stream<E> actual) {
final Integer ONE = 1; // For doing autoboxing only once.
final ConcurrentMap<E,Integer> count = new ConcurrentHashMap<>();
while (expected.hasNext()) {
count.merge(expected.next(), ONE, (old, one) -> old + 1);
}
/*
* Following may be parallelized in an arbitrary amount of threads.
*/
actual.forEach((value) -> {
if (count.computeIfPresent(value, (key, old) -> old - 1) == null) {
fail("Stream returned unexpected value: " + value);
}
});
/*
* Back to sequential order, verify that all elements have been traversed
* by the stream and no more.
*/
for (final Map.Entry<E,Integer> entry : count.entrySet()) {
int n = entry.getValue();
if (n != 0) {
final String message;
if (n < 0) {
message = "Stream returned too many occurrences of %s%n%d extraneous were found.";
} else {
message = "Stream did not returned all expected occurrences of %s%n%d are missing.";
}
fail(String.format(message, entry.getKey(), StrictMath.abs(n)));
}
}
}
/**
* Asserts that the given set contains the same elements, ignoring order.
* In case of failure, this method lists the missing or unexpected elements.
*
* <p>The given collections are typically instances of {@link Set}, but this is not mandatory.</p>
*
* @param expected the expected set, or {@code null}.
* @param actual the actual set, or {@code null}.
*/
public static void assertSetEquals(final Collection<?> expected, final Collection<?> actual) {
if (expected != null && actual != null && !expected.isEmpty()) {
final Set<Object> r = new LinkedHashSet<>(expected);
assertTrue("The two sets are disjoint.", r.removeAll(actual));
assertTrue("The set is missing elements: " + r, r.isEmpty());
assertTrue("The set unexpectedly became empty.", r.addAll(actual));
assertTrue("The two sets are disjoint.", r.removeAll(expected));
assertTrue("The set contains unexpected elements: " + r, r.isEmpty());
}
if (expected instanceof Set<?> && actual instanceof Set<?>) {
assertEquals("Set.equals(Object) failed:", expected, actual);
}
}
/**
* Asserts that the given map contains the same entries.
* In case of failure, this method lists the missing or unexpected entries.
*
* @param expected the expected map, or {@code null}.
* @param actual the actual map, or {@code null}.
*/
public static void assertMapEquals(final Map<?,?> expected, final Map<?,?> actual) {
if (expected != null && actual != null && !expected.isEmpty()) {
final Map<Object,Object> r = new LinkedHashMap<>(expected);
for (final Map.Entry<?,?> entry : actual.entrySet()) {
final Object key = entry.getKey();
if (!r.containsKey(key)) {
fail("Unexpected entry for key " + key);
}
final Object ve = r.remove(key);
final Object va = entry.getValue();
if (!Objects.equals(ve, va)) {
fail("Wrong value for key " + key + ": expected " + ve + " but got " + va);
}
}
if (!r.isEmpty()) {
fail("The map is missing entries: " + r);
}
r.putAll(actual);
for (final Map.Entry<?,?> entry : expected.entrySet()) {
final Object key = entry.getKey();
if (!r.containsKey(key)) {
fail("Missing an entry for key " + key);
}
final Object ve = entry.getValue();
final Object va = r.remove(key);
if (!Objects.equals(ve, va)) {
fail("Wrong value for key " + key + ": expected " + ve + " but got " + va);
}
}
if (!r.isEmpty()) {
fail("The map contains unexpected elements:" + r);
}
}
assertEquals("Map.equals(Object) failed:", expected, actual);
}
/**
* Serializes the given object in memory, deserializes it and ensures that the deserialized
* object is equals to the original one. This method does not write anything to the disk.
*
* <p>If the serialization fails, then this method throws an {@link AssertionError}
* as do the other JUnit assertion methods.</p>
*
* @param <T> the type of the object to serialize.
* @param object the object to serialize.
* @return the deserialized object.
*/
public static <T> T assertSerializedEquals(final T object) {
Objects.requireNonNull(object);
final Object deserialized;
try {
final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try (ObjectOutputStream out = new ObjectOutputStream(buffer)) {
out.writeObject(object);
}
// Now reads the object we just serialized.
final byte[] data = buffer.toByteArray();
try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(data))) {
try {
deserialized = in.readObject();
} catch (ClassNotFoundException e) {
throw new AssertionError(e);
}
}
} catch (IOException e) {
throw new AssertionError(e.toString(), e);
}
assertNotNull("Deserialized object shall not be null.", deserialized);
/*
* Compare with the original object and return it.
*/
@SuppressWarnings("unchecked")
final Class<? extends T> type = (Class<? extends T>) object.getClass();
assertEquals("Deserialized object not equal to the original one.", object, deserialized);
assertEquals("Deserialized object has a different hash code.",
object.hashCode(), deserialized.hashCode());
return type.cast(deserialized);
}
}