From 52e45a36771641609d8b8531ababe3bfb710ba61 Mon Sep 17 00:00:00 2001 From: Evgeny Nikitin <enikitin@openjdk.org> Date: Fri, 9 Oct 2020 16:48:49 +0000 Subject: [PATCH] 8229186: Improve error messages for TestStringIntrinsics failures Reviewed-by: iignatyev, lmesnik --- .../string/TestStringIntrinsics.java | 63 ++- .../jdk/test/lib/format/ArrayDiffTest.java | 409 ++++++++++++++++++ test/lib/jdk/test/lib/format/ArrayCodec.java | 340 +++++++++++++++ test/lib/jdk/test/lib/format/ArrayDiff.java | 207 +++++++++ test/lib/jdk/test/lib/format/Diff.java | 58 +++ test/lib/jdk/test/lib/format/Format.java | 127 ++++++ 6 files changed, 1190 insertions(+), 14 deletions(-) create mode 100644 test/lib-test/jdk/test/lib/format/ArrayDiffTest.java create mode 100644 test/lib/jdk/test/lib/format/ArrayCodec.java create mode 100644 test/lib/jdk/test/lib/format/ArrayDiff.java create mode 100644 test/lib/jdk/test/lib/format/Diff.java create mode 100644 test/lib/jdk/test/lib/format/Format.java diff --git a/test/hotspot/jtreg/compiler/intrinsics/string/TestStringIntrinsics.java b/test/hotspot/jtreg/compiler/intrinsics/string/TestStringIntrinsics.java index 36cf6827b9b..65984029397 100644 --- a/test/hotspot/jtreg/compiler/intrinsics/string/TestStringIntrinsics.java +++ b/test/hotspot/jtreg/compiler/intrinsics/string/TestStringIntrinsics.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,12 +25,16 @@ * @test * @bug 8054307 * @summary Tests correctness of string related intrinsics and C2 optimizations. + * @library /test/lib * * @run main/timeout=240 compiler.intrinsics.string.TestStringIntrinsics */ package compiler.intrinsics.string; +import jdk.test.lib.format.Format; +import jdk.test.lib.format.ArrayCodec; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -77,8 +81,8 @@ public class TestStringIntrinsics { for (Method m : TestStringIntrinsics.class.getMethods()) { if (m.isAnnotationPresent(Test.class)) { System.out.print("Checking " + m.getName() + "... "); - Operation op = m.getAnnotation(Test.class).op(); Test antn = m.getAnnotation(Test.class); + Operation op = antn.op(); if (isStringConcatTest(op)) { checkStringConcat(op, m, antn); } else { @@ -121,13 +125,13 @@ public class TestStringIntrinsics { switch (op) { case ARR_EQUALS_B: - invokeAndCheck(m, (incL == 0), latin1.getBytes("ISO-8859-1"), latin1Copy.getBytes("ISO-8859-1")); - invokeAndCheck(m, true, new byte[] {1, 2, 3}, new byte[] {1, 2, 3}); - invokeAndCheck(m, true, new byte[] {1}, new byte[] {1}); - invokeAndCheck(m, true, new byte[] {}, new byte[] {}); + invokeAndCompareArrays(m, (incL == 0), latin1.getBytes("ISO-8859-1"), latin1Copy.getBytes("ISO-8859-1")); + invokeAndCompareArrays(m, true, new byte[] {1, 2, 3}, new byte[] {1, 2, 3}); + invokeAndCompareArrays(m, true, new byte[] {1}, new byte[] {1}); + invokeAndCompareArrays(m, true, new byte[] {}, new byte[] {}); break; case ARR_EQUALS_C: - invokeAndCheck(m, (incU == 0), utf16.toCharArray(), arrU); + invokeAndCompareArrays(m, (incU == 0), utf16.toCharArray(), arrU); break; case EQUALS: invokeAndCheck(m, (incL == 0), latin1, latin1Copy); @@ -240,18 +244,49 @@ public class TestStringIntrinsics { } } + /** + * Invokes method 'm' by passing arguments the two 'args' (which are supposed to be arrays) + * checks if the returned value. In case of error and arrays being not equal, prints their difference. + */ + private void invokeAndCompareArrays(Method m, boolean expectedResult, Object arg0, Object arg1) throws Exception { + boolean result = (Boolean)m.invoke(null, arg0, arg1); + if (expectedResult == result) + return; + + String cause = String.format("Result: (%b) of '%s' is not equal to expected (%b)", + result, m.getName(), expectedResult); + + if (expectedResult == true) { + System.err.println(cause); + System.err.println(Format.arrayDiff(arg0, arg1)); + } else { + System.err.println(cause); + System.err.printf("First array argument: %n %s%n", ArrayCodec.format(arg0)); + } + + throw new RuntimeException(cause); + } + /** * Invokes method 'm' by passing arguments 'args' and checks if the * returned value equals 'expectedResult'. */ private void invokeAndCheck(Method m, Object expectedResult, Object... args) throws Exception { - Object result = m.invoke(null, args); - if (!result.equals(expectedResult)) { -// System.out.println("Expected:"); -// System.out.println(expectedResult); -// System.out.println("Returned:"); -// System.out.println(result); - throw new RuntimeException("Result of '" + m.getName() + "' not equal to expected value."); + Object actualResult = m.invoke(null, args); + if (!actualResult.equals(expectedResult)) { + var nl = System.lineSeparator(); + StringBuilder msgBuilder = new StringBuilder(); + msgBuilder.append("Actual result of '" + m.getName() + "' is not equal to expected value." + nl); + msgBuilder.append("Expected: " + Format.asLiteral(expectedResult) + nl); + msgBuilder.append("Actual: " + Format.asLiteral(actualResult)); + + for (int i = 0; i < args.length; i++) { + msgBuilder.append(nl + " Arg" + i + ": " + Format.asLiteral(args[i])); + } + + final String message = msgBuilder.toString(); + System.err.println(message); + throw new RuntimeException(message); } } diff --git a/test/lib-test/jdk/test/lib/format/ArrayDiffTest.java b/test/lib-test/jdk/test/lib/format/ArrayDiffTest.java new file mode 100644 index 00000000000..7b7dfc086f1 --- /dev/null +++ b/test/lib-test/jdk/test/lib/format/ArrayDiffTest.java @@ -0,0 +1,409 @@ +/* + * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.test.lib.format; + +import org.testng.annotations.Test; + +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertEquals; + +/* + * @test + * @summary Check ArrayDiff formatting + * @library /test/lib + * @run testng jdk.test.lib.format.ArrayDiffTest + */ +public class ArrayDiffTest { + + @Test + public void testEqualArrays() { + char[] first = new char[] {'a', 'b', 'c', 'd', 'e', 'f', 'g'}; + char[] second = new char[] {'a', 'b', 'c', 'd', 'e', 'f', 'g'}; + + assertTrue(ArrayDiff.of(first, second).areEqual()); + } + + @Test + public void testOutputFitsWidth() { + new AssertBuilder() + .withDefaultParams() + .withArrays( + new byte[] {7, 8, 9, 10, 11, 12, 13}, + new byte[] {7, 8, 9, 10, 125, 12, 13}) + .thatResultIs(false) + .thatFormattedValuesAre( + 4, + "[7, 8, 9, 10, 11, 12, 13]", + "[7, 8, 9, 10, 125, 12, 13]", + " ^^^^") + .assertTwoWay(); + } + + @Test + public void testIntegers() { + new AssertBuilder() + .withDefaultParams() + .withArrays( + new int[] {7, 8, 10, 11, 12}, + new int[] {7, 8, 9, 10, 11, 12, 13}) + .thatResultIs(false) + .thatFormattedValuesAre( + 2, + "[7, 8, 10, 11, 12]", + "[7, 8, 9, 10, 11, 12, 13]", + " ^^^") + .assertTwoWay(); + } + + @Test + public void testLongs() { + new AssertBuilder() + .withDefaultParams() + .withArrays( + new long[] {1, 2, 3, 4}, + new long[] {1, 2, 3, 10}) + .thatResultIs(false) + .thatFormattedValuesAre( + 3, + "[1, 2, 3, 4]", + "[1, 2, 3, 10]", + " ^^^") + .assertTwoWay(); + } + + @Test + public void testFirstElementIsWrong() { + new AssertBuilder() + .withDefaultParams() + .withArrays( + new byte[] {122}, + new byte[] {7, 8, 9, 10, 125, 12, 13}) + .thatResultIs(false) + .thatFormattedValuesAre( + 0, + "[122]", + "[ 7, 8, 9, 10, 125, 12, 13]", + " ^^^") + .assertTwoWay(); + } + + @Test + public void testOneElementIsEmpty() { + new AssertBuilder() + .withDefaultParams() + .withArrays( + new byte[] {7, 8, 9, 10, 125, 12, 13}, + new byte[] {}) + .thatResultIs(false) + .thatFormattedValuesAre( + 0, + "[7, 8, 9, 10, 125, 12, 13]", + "[]", + " ^") + .assertTwoWay(); + } + + @Test + public void testOutputDoesntFitWidth() { + new AssertBuilder() + .withParams(20, Integer.MAX_VALUE) + .withArrays( + new char[] {'1', '2', '3', '4', '5', '6', '7'}, + new char[] {'1', 'F', '3', '4', '5', '6', '7'}) + .thatResultIs(false) + .thatFormattedValuesAre( + 1, + "[1, 2, 3, 4, 5, ...", + "[1, F, 3, 4, 5, ...", + " ^^") + .assertTwoWay(); + } + + @Test + public void testVariableElementWidthOutputDoesntFitWidth() { + new AssertBuilder() + .withParams(20, Integer.MAX_VALUE) + .withArrays( + new byte[] {1, 2, 3, 4, 5, 6, 7}, + new byte[] {1, 112, 3, 4, 5, 6, 7}) + .thatResultIs(false) + .thatFormattedValuesAre( + 1, + "[1, 2, 3, 4, 5, ...", + "[1, 112, 3, 4, 5, ...", + " ^^^^") + .assertTwoWay(); + } + + @Test + public void testContextBefore() { + new AssertBuilder() + .withParams(20, 2) + .withArrays( + new char[] {'1', '2', '3', '4', '5', '6', '7'}, + new char[] {'1', '2', '3', '4', 'F', '6', '7'}) + .thatResultIs(false) + .thatFormattedValuesAre( + 4, + "... 3, 4, 5, 6, 7]", + "... 3, 4, F, 6, 7]", + " ^^") + .assertTwoWay(); + } + + @Test + public void testBoundedBytesWithDifferentWidth() { + new AssertBuilder() + .withParams(24, 2) + .withArrays( + new byte[] {0, 1, 2, 3, 125, 5, 6, 7}, + new byte[] {0, 1, 2, 3, 4, 5, 6, 7}) + .thatResultIs(false) + .thatFormattedValuesAre( + 4, + "... 2, 3, 125, 5, 6, 7]", + "... 2, 3, 4, 5, 6, 7]", + " ^^^^") + .assertTwoWay(); + } + + @Test + public void testBoundedFirstElementIsWrong() { + new AssertBuilder() + .withParams(25, 2) + .withArrays( + new byte[] {101, 102, 103, 104, 105, 110}, + new byte[] {2}) + .thatResultIs(false) + .thatFormattedValuesAre( + 0, + "[101, 102, 103, 104, ...", + "[ 2]", + " ^^^") + .assertTwoWay(); + } + + @Test + public void testBoundedOneArchiveIsEmpty() { + new AssertBuilder() + .withParams(10, 2) + .withArrays( + new char[] {'a', 'b', 'c', 'd', 'e'}, + new char[] {}) + .thatResultIs(false) + .thatFormattedValuesAre( + 0, + "[a, b, ...", + "[]", + " ^") + .assertTwoWay(); + } + + @Test + public void testUnboundedOneArchiveIsEmpty() { + new AssertBuilder() + .withDefaultParams() + .withArrays( + new char[] {'a', 'b', 'c', 'd', 'e'}, + new char[] {}) + .thatResultIs(false) + .thatFormattedValuesAre( + 0, + "[a, b, c, d, e]", + "[]", + " ^") + .assertTwoWay(); + } + + @Test + public void testUnprintableCharFormatting() { + new AssertBuilder() + .withDefaultParams() + .withArrays( + new char[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, + new char[] {0, 1, 2, 3, 4, 5, 6, 125, 8, 9, 10, 11, 12, 13, 14, 15, 16}) + .thatResultIs(false) + .thatFormattedValuesAre( + 7, + "... \\u0005, \\u0006, \\u0007, \\u0008, \\u0009, \\n, \\u000B, \\u000C, \\r, \\u000E, ...", + "... \\u0005, \\u0006, }, \\u0008, \\u0009, \\n, \\u000B, \\u000C, \\r, \\u000E, ...", + " ^^^^^^^") + .assertTwoWay(); + } + + @Test + public void testStringElements() { + new AssertBuilder() + .withDefaultParams() + .withArrays( + new String[] {"first", "second", "third", "u\nprintable"}, + new String[] {"first", "second", "incorrect", "u\nprintable"}) + .thatResultIs(false) + .thatFormattedValuesAre( + 2, + "[\"first\", \"second\", \"third\", \"u\\nprintable\"]", + "[\"first\", \"second\", \"incorrect\", \"u\\nprintable\"]", + " ^^^^^^^^^^^^") + .assertTwoWay(); + } + + @Test + public void testToStringableObjects() { + class StrObj { + private final String value; + public boolean equals(Object another) { return ((StrObj)another).value.equals(value); } + public StrObj(String value) { this.value = value; } + public String toString() { return value; } + } + + new AssertBuilder() + .withDefaultParams() + .withArrays( + new StrObj[] {new StrObj("1"), new StrObj("Unp\rintable"), new StrObj("5")}, + new StrObj[] {new StrObj("1"), new StrObj("2"), new StrObj("5")}) + .thatResultIs(false) + .thatFormattedValuesAre( + 1, + "[1, Unp\\rintable, 5]", + "[1, 2, 5]", + " ^^^^^^^^^^^^^") + .assertTwoWay(); + } + + @Test + public void testNullElements() { + new AssertBuilder() + .withDefaultParams() + .withArrays( + new String[] {"Anna", null, "Bill", "Julia"}, + new String[] {"Anna", "null", "William", "Julia"}) + .thatResultIs(false) + .thatFormattedValuesAre( + 1, + "[\"Anna\", null, \"Bill\", \"Julia\"]", + "[\"Anna\", \"null\", \"William\", \"Julia\"]", + " ^^^^^^^") + .assertTwoWay(); + } + + @Test (expectedExceptions = NullPointerException.class) + public void testFirstArrayIsNull() { + var diff = ArrayDiff.of(null, new String[] {"a", "b"}); + } + + @Test (expectedExceptions = NullPointerException.class) + public void testSecondArrayIsNull() { + var diff = ArrayDiff.of(null, new String[] {"a", "b"}); + } + + class AssertBuilder { + private boolean defaultParameters; + private int width; + private int contextBefore; + private Object firstArray; + private Object secondArray; + private boolean expectedResult; + private int expectedIndex; + private String firstFormattedArray; + private String secondFormattedArray; + private String failureMark; + + public AssertBuilder withDefaultParams() { + defaultParameters = true; + return this; + } + + public AssertBuilder withParams(int width, int contextBefore) { + defaultParameters = false; + this.width = width; + this.contextBefore = contextBefore; + return this; + } + + public AssertBuilder withArrays(Object first, Object second) { + firstArray = first; + secondArray = second; + return this; + } + + public AssertBuilder thatResultIs(boolean result) { + expectedResult = result; + return this; + } + + public AssertBuilder thatFormattedValuesAre( + int idx, String first, String second, String mark) { + expectedIndex = idx; + firstFormattedArray = first; + secondFormattedArray = second; + failureMark = mark; + return this; + } + + public void assertTwoWay() { + ArrayDiff diff; + + // Direct + if (defaultParameters) { + diff = ArrayDiff.of(firstArray, secondArray); + } else { + diff = ArrayDiff.of(firstArray, secondArray, width, contextBefore); + } + + if (expectedResult == true) { + assertTrue(diff.areEqual()); + } else { + String expected = String.format( + "Arrays differ starting from [index: %d]:%n" + + "%s%n" + "%s%n" + "%s", + expectedIndex, firstFormattedArray, secondFormattedArray, failureMark); + + assertFalse(diff.areEqual()); + assertEquals(diff.format(), expected); + } + + // Reversed + if (defaultParameters) { + diff = ArrayDiff.of(secondArray, firstArray); + } else { + diff = ArrayDiff.of(secondArray, firstArray, width, contextBefore); + } + + if (expectedResult == true) { + assertTrue(diff.areEqual()); + } else { + String expected = String.format( + "Arrays differ starting from [index: %d]:%n" + + "%s%n" + "%s%n" + "%s", + expectedIndex, secondFormattedArray, firstFormattedArray, failureMark); + + assertFalse(diff.areEqual()); + assertEquals(diff.format(), expected); + } + } + + } + +} diff --git a/test/lib/jdk/test/lib/format/ArrayCodec.java b/test/lib/jdk/test/lib/format/ArrayCodec.java new file mode 100644 index 00000000000..0e34e7cd811 --- /dev/null +++ b/test/lib/jdk/test/lib/format/ArrayCodec.java @@ -0,0 +1,340 @@ +/* + * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.test.lib.format; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * A codec helping representing arrays in a string form. + * + * Encoding can be done in a controllable fashion (allowing the user to encode two + * or more arrays in a time) or as a single operation. + */ +public class ArrayCodec<E> { + private static final String ELLIPSIS = "..."; + + private boolean exhausted; + private StringBuilder encoded; + + private List<E> source; + private String element; + + private boolean bounded = false; + private int maxWidth; + private int idx; + + private ArrayCodec(List<E> source) { + this.source = source; + } + + /** + * Creates a codec for a char array + * + * @param array source array + * @return an ArrayCodec for the provided array + */ + public static ArrayCodec<Character> of(char[] array) { + var source = new ArrayList<Character>(array.length); + for (char value: array) { + source.add(value); + } + return new ArrayCodec<>(source); + } + + /** + * Creates a codec for a byte array + * + * @param array source array + * @return an ArrayCodec for the provided array + */ + public static ArrayCodec<Byte> of(byte[] array) { + var source = new ArrayList<Byte>(array.length); + for (byte value: array) { + source.add(value); + } + return new ArrayCodec<>(source); + } + + /** + * Creates a codec for an int array + * + * @param array source array + * @return an ArrayCodec for the provided array + */ + public static ArrayCodec<Integer> of(int[] array) { + var source = new ArrayList<Integer>(array.length); + for (int value: array) { + source.add(value); + } + return new ArrayCodec<>(source); + } + + /** + * Creates a codec for a long array + * + * @param array source array + * @return an ArrayCodec for the provided array + */ + public static ArrayCodec<Long> of(long[] array) { + var source = new ArrayList<Long>(array.length); + for (long value: array) { + source.add(value); + } + return new ArrayCodec<>(source); + } + + /** + * Creates a codec for a String array + * + * @param array source array + * @return an ArrayCodec for the provided array + */ + public static ArrayCodec<String> of(String[] array) { + var source = new ArrayList<String>(array.length); + for (String value: array) { + source.add(value); + } + return new ArrayCodec<>(source); + } + + /** + * Creates a codec for a generic Object array + * + * @param array source array + * @return an ArrayCodec for the provided array + */ + public static ArrayCodec<Object> of(Object[] array) { + var source = new ArrayList<Object>(array.length); + for (Object value: array) { + source.add(value); + } + return new ArrayCodec<Object>(source); + } + + /** + * Creates a codec for a generic array, trying to recognize its component type + * + * @param array source array + * @throws IllegalArgumentException if {@code array}'s component type is not supported + * @return an ArrayCodec for the provided array + */ + public static ArrayCodec of(Object array) { + var type = array.getClass().getComponentType(); + if (type == byte.class) { + return ArrayCodec.of((byte[])array); + } else if (type == int.class) { + return ArrayCodec.of((int[])array); + } else if (type == long.class) { + return ArrayCodec.of((long[])array); + } else if (type == char.class) { + return ArrayCodec.of((char[])array); + } else if (type == String.class) { + return ArrayCodec.of((String[])array); + } else if (!type.isPrimitive() && !type.isArray()) { + return ArrayCodec.of((Object[])array); + } + + throw new IllegalArgumentException("Unsupported array component type: " + type); + } + + /** + * Formats an array at-once. + * The array is enclosed in brackets, its elements are separated with + * commas. String elements are additionally surrounded by double quotes. + * Unprintable symbols are C-stye escaped. + * + * <p>Sample outputs: + * + * <pre> + * [0, 1, 2, 3, 4] + * ["one", "first", "tree"] + * [object1, object2, object3] + * [a, b, \n, \u0002/, c] + * </pre> + * + * @throws IllegalArgumentException if {@code array}'s component type is not supported + * @return an ArrayCodec for the provided array + */ + public static String format(Object array) { + var codec = ArrayCodec.of(array); + codec.startFormatting(0, -1); + while (!codec.isExhausted()) { + codec.formatNext(); + codec.appendFormatted(); + } + return codec.getEncoded(); + } + + /** + * Starts formatting with the given parameters. + * + * @param startIdx first element's index to start formattig with + * @param maxWidth maximum allowed formatting width (in characters). + * @return an ArrayCodec for the provided array + */ + public void startFormatting(int startIdx, int maxWidth) { + encoded = new StringBuilder(startIdx == 0 ? "[" : ELLIPSIS); + exhausted = false; + this.maxWidth = maxWidth; + bounded = (maxWidth > 0); + idx = startIdx; + } + + /** + * Format next element, store it in the internal element storage. + */ + public void formatNext() { + int limit = source.size(); + + String prefix = idx == 0 || idx >= limit ? "" : " "; + String suffix = (idx + 1 == limit) || (source.isEmpty() && idx == 0) + ? "]" + : idx >= limit ? "" : ","; + element = prefix + + (idx >= limit ? "" : Format.asLiteral(source.get(idx))) + + suffix; + } + + /** + * Append formatted element to internal StringBuilder. + * + * The formatted-so-far string can be accessed via {@link #getEncoded} + * no elements in array left the method silently does nothing. + */ + public void appendFormatted() { + if (exhausted) { + return; + } + + boolean isLast = idx == source.size() - 1; + if (isLast || source.isEmpty()) { + exhausted = true; + } + + if (bounded && encoded.length() + element.length() > maxWidth - ELLIPSIS.length()) { + encoded.append(isLast ? element : " " + ELLIPSIS); + exhausted = true; + } else { + encoded.append(element); + } + idx++; + } + + /** + * Aligns the element by another codec. + * + * If another codec's last encoded element string is longer than this + * codec's, widens this codec's encoded element with spaces so the + * two strings have the same length; + * + * @param another Another codec to compare encoded element width with + */ + public void alignBy(ArrayCodec<E> another) { + if (!element.equals("") && !element.equals("]")) { + int delta = another.element.length() - element.length(); + if (delta > 0) { + element = Format.paddingForWidth(delta) + element; + } + } + } + + /** + * Indicates if there are no elements left in the source array + * + * @return {@code true} if there are no elements left, {@code false} otherwise + */ + public boolean isExhausted() { + return exhausted; + } + + /** + * Returns the string encoded-so-far + * + * @return the string encoded-so-far + */ + public String getEncoded() { + return encoded.toString(); + } + + /** + * Returns the length of the string encoded-so-far + * + * @return the length of the string encoded-so-far + */ + public int getEncodedLength() { + return encoded.length(); + } + + /** + * Returns the length of the last encoded element + * + * @return the length of the last encoded element + */ + public int getElementLength() { + return element.length(); + } + + /** + * Finds and returns the first mismatch index in another codec + * + * @param another a codec mismatch with whom is to be found + * @return the first mismatched element's index or -1 if arrays are identical + */ + public int findMismatchIndex(ArrayCodec<E> another) { + int result = 0; + while ((source.size() > result) && (another.source.size() > result)) { + Object first = source.get(result); + Object second = another.source.get(result); + + if (first == null || second == null) { + if (first == null && second == null) { + continue; // Both elements are null (i.e. equal) + } else { + return result; // Only one element is null, here's the failure index + } + } + + if (!first.equals(second)) { + return result; + } + + result++; + } + + return source.size() != another.source.size() + ? result // Lengths are different, but the shorter arrays is a preffix to the longer array. + : -1; // Arrays are identical, there's no mismatch index + } + + /** + * Indicates whether source array for another codec is equal to this codec's array + * + * @return {@code true} if source arrays are equal, {@code false} otherwise + */ + public boolean equals(ArrayCodec<E> another) { + return source.equals(another.source); + } +} diff --git a/test/lib/jdk/test/lib/format/ArrayDiff.java b/test/lib/jdk/test/lib/format/ArrayDiff.java new file mode 100644 index 00000000000..6755a693b7c --- /dev/null +++ b/test/lib/jdk/test/lib/format/ArrayDiff.java @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.test.lib.format; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * A difference between two arrays, which can be pretty formatted. + * For the calculated difference, user can request if the two arrays + * are equal (in terms of {@link Object#equals Object.equals()} for their + * elements). For the arrays that differ, a human-readable difference can + * be provided. + * + * <p>The difference is represented as a four-line text block, comprising of the + * first different element index, arrays printouts in the difference area, + * and a difference mark. For Primitive and Object elements in the source + * arrays their C-style escaped {@link String#valueOf String.valueOf()} are + * printed, element in String[] arrays are additionally surrounded with quotes. + * Additional formatting parameters, like maximum allowed width and number of + * elements printed before difference, can be specified. + * + * <p>Output examples: + * + * <p> two int arrays: </p> + * <pre> + * Arrays differ starting from [index: 4]: + * ... 3, 4, 5, 6, 7] + * ... 3, 4, 225, 6, 7] + * ^^^^ + * </pre> + * <p> two String arrays: </p> + * <pre> + * Arrays differ starting from [index: 2]: + * ["first", "second", "third", "u\nprintable"] + * ["first", "second", "incorrect", "u\nprintable"] + * ^^^^^^^^^^^^ + * </pre> + * <p> two char arrays arrays: </p> + * <pre> + * Arrays differ starting from [index: 7]: + * ... \u0001, \u0002, \u0007, a, b, \n, ... + * ... \u0001, \u0002, }, a, b, \n, ... + * ^^^^^^^ + * </pre> + */ +public class ArrayDiff<E> implements Diff { + + private int failureIdx; + private final int maxWidth; + private final int contextBefore; + + private final ArrayCodec<E> first; + private final ArrayCodec<E> second; + + private ArrayDiff(ArrayCodec<E> first, ArrayCodec<E> second, + int width, int getContextBefore) { + this.first = first; + this.second = second; + this.maxWidth = width; + this.contextBefore = getContextBefore; + failureIdx = first.findMismatchIndex(second); + } + + /** + * Creates an ArrayDiff fom two arrays and default limits. The given arguments must be of the same + * component type. + * + * @param first the first array + * @param second the second array + * @return an ArrayDiff instance for the two arrays + */ + public static ArrayDiff of(Object first, Object second) { + return ArrayDiff.of(first, second, Diff.Defaults.WIDTH, Diff.Defaults.CONTEXT_BEFORE); + } + + /** + * Creates an ArrayDiff fom two arrays with the given limits. The given arguments must be of the same + * component type. + * + * @param first the first array + * @param second the second array + * @param width the maximum allowed width in characters for the formatting + * @param contextBefore maximum number of elements to print before those that differ + * @throws IllegalArgumentException if component types of arrays is not supported or are not the same + * @throws NullPointerException if at least one of the arrays is null + * @return an ArrayDiff instance for the two arrays and formatting parameters provided + */ + public static ArrayDiff of(Object first, Object second, int width, int contextBefore) { + Objects.requireNonNull(first); + Objects.requireNonNull(second); + + boolean bothAreArrays = first.getClass().isArray() && second.getClass().isArray(); + boolean componentTypesAreSame = + first.getClass().getComponentType() == second.getClass().getComponentType(); + + if (!bothAreArrays || !componentTypesAreSame) { + throw new IllegalArgumentException("Both arguments should be arrays of the same type"); + } + + return new ArrayDiff( + ArrayCodec.of(first), + ArrayCodec.of(second), + width, contextBefore); + } + + /** + * Formats the given diff. + * + * @return formatted difference representation. + */ + @Override + public String format() { + if (areEqual()) { + return ""; + } + + return format(false) + .orElseGet(() -> format(true).get()); + } + + /** + * Indicates whether the two source arrays are equal + * + * @return {@code true} if the arrays are different, {@code false} otherwise + */ + @Override + public boolean areEqual() { + return first.equals(second); + } + + private void extractAndAlignElements() { + first.formatNext(); + second.formatNext(); + + first.alignBy(second); + second.alignBy(first); + } + + private static String failureMarkForWidth(int width) { + return new String("^").repeat(width); + } + + private Optional<String> format(boolean bounded) { + int idx = bounded ? Math.max(0, failureIdx - contextBefore) : 0; + + first.startFormatting(idx, bounded ? maxWidth : -1); + second.startFormatting(idx, bounded ? maxWidth : -1); + StringBuilder failureMark = new StringBuilder( + Format.paddingForWidth(first.getEncodedLength())); + + for (; !(first.isExhausted() && second.isExhausted()); idx++) { + extractAndAlignElements(); + + first.appendFormatted(); + second.appendFormatted(); + + { // Process failure mark + if (idx < failureIdx) { + failureMark.append(Format.paddingForWidth(first.getElementLength())); + } else if (idx == failureIdx) { + int markLength = Math.max(first.getElementLength(), second.getElementLength()) - 1; + failureMark.append(failureMarkForWidth(markLength)); + } + } + + final int maxEncodedLength = Math.max( + first.getEncodedLength(), + second.getEncodedLength()); + if (!bounded && maxEncodedLength > maxWidth) { + return Optional.empty(); + } + } + + return Optional.of(String.format( + "Arrays differ starting from [index: %d]:%n%s%n%s%n%s", + failureIdx, + first.getEncoded(), + second.getEncoded(), + failureMark.toString())); + } + +} + diff --git a/test/lib/jdk/test/lib/format/Diff.java b/test/lib/jdk/test/lib/format/Diff.java new file mode 100644 index 00000000000..02ed3430e5d --- /dev/null +++ b/test/lib/jdk/test/lib/format/Diff.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * Represents a possible difference between two objects. + */ +package jdk.test.lib.format; + +/** + * An abstraction representing formattable difference between two or more objects + */ +public interface Diff { + + /** + * Default limits for formatters + */ + public static class Defaults { + private Defaults() { } // This class should not be instantiated + public final static int WIDTH = 80; + public final static int CONTEXT_BEFORE = 2; + } + + /** + * Formats the given diff. Different implementations can provide different + * result and formatting style. + * + * @return formatted difference representation. + */ + String format(); + + /** + * Indicates whether the two source arrays are equal. Different + * implementations can treat this notion differently. + * + * @return {@code true} if the source objects are different, {@code false} otherwise + */ + boolean areEqual(); +} diff --git a/test/lib/jdk/test/lib/format/Format.java b/test/lib/jdk/test/lib/format/Format.java new file mode 100644 index 00000000000..9770e33c232 --- /dev/null +++ b/test/lib/jdk/test/lib/format/Format.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.test.lib.format; + +/** + * A collection of formatting utilities + */ +public class Format { + + /** + * Formats character as literal, using C-style escaping for unprintable symbols + * + * @param c character to format + * @return formatted string representation of the character + */ + public static String asLiteral(char c) { + StringBuilder sb = new StringBuilder(); + appendCharToSb(c, sb); + return sb.toString(); + } + + /** + * Escapes String in C-style + * + * @param src source string + * @return C-style escaped source string + */ + public static String escapeString(String src) { + StringBuilder sb = new StringBuilder(); + src.chars().forEachOrdered( + (c) -> appendCharToSb((char) c, sb)); + return sb.toString(); + } + + /** + * Formats Object as literal, using its String representation C-style escaped. + * + * @param o object to format + * @return C-style escaped String representation of the object + */ + public static String asLiteral(Object o) { + if (o instanceof String) { + return '"' + escapeString((String)o) + '"'; + } else if (o instanceof Character) { + return asLiteral((char) o); + } else if (o instanceof Byte) { + return String.valueOf(o); + } else { + return escapeString(String.valueOf(o)); + } + } + + /** + * Formats a difference between two arrays with index of the first mismatch element, + * and slices of arrays necessary to understand the problem, along with a failure mark. + * + * @param first first array to compare + * @param second second array to compare + * @return the difference, generated by the {@link ArrayDiff ArrayDiff} + */ + public static String arrayDiff(Object first, Object second) { + return ArrayDiff.of(first, second).format(); + } + + /** + * Formats a difference between two arrays with index of the first mismatch element, + * and slices of arrays necessary to understand the problem, along with a failure mark. + * Takes into account maximum allowed width and context (in elements) before the mismatch. + * + * @param first first array to compare + * @param second second array to compare + * @param width the maximum allowed width in characters for the formatting + * @param contextBefore maximum number of elements to print before those that differ + * @return the difference, generated by the {@link ArrayDiff ArrayDiff} + */ + public static String arrayDiff(Object first, Object second, int width, int contextBefore) { + return ArrayDiff.of(first, second, width, contextBefore).format(); + } + + + /** + * Returns a string of spaces with length specified. + * + * @param width number of spaces in the resulting string + * @return Padding string of spaces + */ + public static String paddingForWidth(int width) { + return " ".repeat(width); + } + + private static void appendCharToSb(char c, StringBuilder sb) { + if (c == 10) { + sb.append("\\n"); + } else if (c == 13) { + sb.append("\\r"); + } else if (c == 92) { + sb.append("\\\\"); + } else if (c == 34) { + sb.append("\\\""); + } else if (c < 32 || c > 126) { + sb.append("\\u" + String.format("%04X", (int) c)); + } else { + sb.append(c); + } + } +}