From 6a091c58c44ca8d55f394e8334c4b4b5a912172b Mon Sep 17 00:00:00 2001 From: Andreas Schmid Date: Tue, 31 Oct 2017 23:48:31 +0100 Subject: [PATCH 1/5] create placeholder to resolve arguments including names (#90) * add unit test cases for named arguments placeholder --- build.gradle | 4 + .../dataprovider/FormatAcceptanceTest.java | 20 + ...DataProviderInvocationContextProvider.java | 2 + .../placeholder/NamedArgumentPlaceholder.java | 220 +++++ .../NamedArgumentPlaceholderTest.java | 783 ++++++++++++++++++ 5 files changed, 1029 insertions(+) create mode 100644 junit-jupiter/src/main/java/com/tngtech/junit/dataprovider/placeholder/NamedArgumentPlaceholder.java create mode 100644 junit-jupiter/src/test/java/com/tngtech/junit/dataprovider/placeholder/NamedArgumentPlaceholderTest.java diff --git a/build.gradle b/build.gradle index b8d31c17..f6b2d6e3 100644 --- a/build.gradle +++ b/build.gradle @@ -166,6 +166,10 @@ project(':junit-jupiter-params') { // configure after properties are set and integration tests are added subprojects { + tasks.withType(JavaCompile) { + options.compilerArgs += [ "-parameters" ] + } + jar { manifest { def now = new Date() diff --git a/junit-jupiter/src/integTest/java/com/tngtech/test/junit/dataprovider/FormatAcceptanceTest.java b/junit-jupiter/src/integTest/java/com/tngtech/test/junit/dataprovider/FormatAcceptanceTest.java index ba587646..4caf8360 100644 --- a/junit-jupiter/src/integTest/java/com/tngtech/test/junit/dataprovider/FormatAcceptanceTest.java +++ b/junit-jupiter/src/integTest/java/com/tngtech/test/junit/dataprovider/FormatAcceptanceTest.java @@ -37,4 +37,24 @@ void testMultiply(int a, int b, int expected) { // Expect: assertThat(a * b).isEqualTo(expected); } + + @DataProvider(format = "[%i] %na[0..-1]") + static Object[][] dataProviderDivide() { + // @formatter:off + return new Object[][] { + { 0, 1, 0 }, + { 1, 1, 1 }, + { -1, 1, -1 }, + { 2, 1, 2 }, + { 15, 3, 5 }, + }; + // @formatter:on + } + + @TestTemplate + @UseDataProvider + void testDivide(int dividend, int divisor, int result) { + // Expect: + assertThat(dividend / divisor).isEqualTo(result); + } } \ No newline at end of file diff --git a/junit-jupiter/src/main/java/com/tngtech/junit/dataprovider/AbstractDataProviderInvocationContextProvider.java b/junit-jupiter/src/main/java/com/tngtech/junit/dataprovider/AbstractDataProviderInvocationContextProvider.java index ed7ce3d4..bcdd8b77 100644 --- a/junit-jupiter/src/main/java/com/tngtech/junit/dataprovider/AbstractDataProviderInvocationContextProvider.java +++ b/junit-jupiter/src/main/java/com/tngtech/junit/dataprovider/AbstractDataProviderInvocationContextProvider.java @@ -23,6 +23,7 @@ import com.tngtech.junit.dataprovider.placeholder.CanonicalClassNamePlaceholder; import com.tngtech.junit.dataprovider.placeholder.CompleteMethodSignaturePlaceholder; import com.tngtech.junit.dataprovider.placeholder.IndexPlaceholder; +import com.tngtech.junit.dataprovider.placeholder.NamedArgumentPlaceholder; import com.tngtech.junit.dataprovider.placeholder.SimpleClassNamePlaceholder; import com.tngtech.junit.dataprovider.placeholder.SimpleMethodNamePlaceholder; @@ -92,6 +93,7 @@ protected List getDefaultPlaceholders() { result.add(new CanonicalClassNamePlaceholder()); // must be before SimpleClassNamePlaceholder result.add(new CompleteMethodSignaturePlaceholder()); // must be before SimpleClassNamePlaceholder result.add(new IndexPlaceholder()); + result.add(new NamedArgumentPlaceholder()); result.add(new SimpleClassNamePlaceholder()); result.add(new SimpleMethodNamePlaceholder()); return result; diff --git a/junit-jupiter/src/main/java/com/tngtech/junit/dataprovider/placeholder/NamedArgumentPlaceholder.java b/junit-jupiter/src/main/java/com/tngtech/junit/dataprovider/placeholder/NamedArgumentPlaceholder.java new file mode 100644 index 00000000..176c8d2a --- /dev/null +++ b/junit-jupiter/src/main/java/com/tngtech/junit/dataprovider/placeholder/NamedArgumentPlaceholder.java @@ -0,0 +1,220 @@ +package com.tngtech.junit.dataprovider.placeholder; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.Arrays; +import java.util.List; + +import org.junit.platform.commons.logging.Logger; +import org.junit.platform.commons.logging.LoggerFactory; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +/** + * This placeholder format the arguments including their parameter names of a dataprovider test as comma-separated + * {@link String} according to the given index or range subscript. Furthermore the following arguments are treated + * specially: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Argument valuetarget {@link String}
null<null>
"" (= empty string)<empty string>
array (e.g. String[]){@code "[" + formatPattern(array) + "]"}
other{@link Object#toString()}
+ * Note: Test code must be compiled with "-parameters" option in order to retrieve formal parameter names! + */ +// TODO a lot of equal code to ArgumentPlaceholder, e.g. javadoc above, the constants below ... +public class NamedArgumentPlaceholder extends BasePlaceholder { + + private static final Logger logger = LoggerFactory.getLogger(NamedArgumentPlaceholder.class); + + /** + * W {@link String} representation of {@code null} + */ + protected static final String STRING_NULL = ""; + + /** + * {@link String} representation of {@code ""} + */ + protected static final String STRING_EMPTY = ""; + + /** + * {@link String} representation of an non-printable character + */ + protected static final String STRING_NON_PRINTABLE = ""; + + + public NamedArgumentPlaceholder() { + super("%na\\[(-?[0-9]+|-?[0-9]+\\.\\.-?[0-9]+)\\]"); + } + + @Override + protected String getReplacementFor(String placeholder, ReplacementData data) { + String subscript = placeholder.substring(4, placeholder.length() - 1); + + int from = Integer.MAX_VALUE; + int to = Integer.MIN_VALUE; + if (subscript.contains("..")) { + String[] split = subscript.split("\\.\\."); + + from = Integer.parseInt(split[0]); + to = Integer.parseInt(split[1]); + } else { + from = Integer.parseInt(subscript); + to = from; + } + + List namedArguments = data.getArguments(); + from = (from >= 0) ? from : namedArguments.size() + from; + to = (to >= 0) ? to + 1 : namedArguments.size() + to + 1; + return formatAll(getSubArrayOfMethodParameters(data.getTestMethod(), from, to), namedArguments.subList(from, to)); + } + + /** + * Formats the given parameters and arguments to a comma-separated list of {@code $parameterName=$argumentName}. + * Arguments {@link String} representation are therefore treated specially. + * + * @param parameters used to for formatting + * @param arguments to be formatted + * @return the formatted {@link String} of the given {@link Parameter}{@code []} and {@link List}{@code } + */ + protected String formatAll(Parameter[] parameters, List arguments) { + StringBuilder stringBuilder = new StringBuilder(); + for (int idx = 0; idx < arguments.size(); idx++) { + String parameterName = (parameters.length > idx) ? parameters[idx].getName() : "?"; + Object argument = arguments.get(idx); + stringBuilder.append(parameterName).append("=").append(format(argument)); + if (idx < arguments.size() - 1) { + stringBuilder.append(", "); + } + } + return stringBuilder.toString(); + } + + @SuppressFBWarnings(value = "RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE", justification = "false positive if 'param.toString()' returns 'null'") + protected String format(Object param) { + if (param == null) { + return STRING_NULL; + + } else if (param.getClass().isArray()) { + if (param.getClass().getComponentType().isPrimitive()) { + return formatPrimitiveArray(param); + } + return "[" + formatArray((Object[]) param) + "]"; + + } else if (param instanceof String && ((String) param).isEmpty()) { + return STRING_EMPTY; + + } + + String result; + if (param instanceof String) { + result = (String) param; + } else { + result = param.toString(); + } + if (result == null) { // maybe null if "param.toString()" returns null + return STRING_NULL; + } + result = result.replaceAll("\0", "\\\\0").replaceAll("\r", "\\\\r").replaceAll("\n", "\\\\n"); + return replaceNonPrintableChars(result, STRING_NON_PRINTABLE); + } + + private Parameter[] getSubArrayOfMethodParameters(Method testMethod, int fromIndex, int toIndex) { + Parameter[] parameters = testMethod.getParameters(); + if (parameters.length > 0 && !parameters[0].isNamePresent()) { + logger.warn(() -> String.format("Parameter names on method '%s' are not available" + + ". To store formal parameter names, compile the source file with the '-parameters' option" + + ". See also https://docs.oracle.com/javase/tutorial/reflect/member/methodparameterreflection.html", + testMethod)); + } + return Arrays.copyOfRange(parameters, fromIndex, toIndex); + } + + private String formatPrimitiveArray(Object primitiveArray) { + Class componentType = primitiveArray.getClass().getComponentType(); + + if (boolean.class.equals(componentType)) { + return Arrays.toString((boolean[]) primitiveArray); + + } else if (byte.class.equals(componentType)) { + return Arrays.toString((byte[]) primitiveArray); + + } else if (char.class.equals(componentType)) { + return Arrays.toString((char[]) primitiveArray); + + } else if (short.class.equals(componentType)) { + return Arrays.toString((short[]) primitiveArray); + + } else if (int.class.equals(componentType)) { + return Arrays.toString((int[]) primitiveArray); + + } else if (long.class.equals(componentType)) { + return Arrays.toString((long[]) primitiveArray); + + } else if (float.class.equals(componentType)) { + return Arrays.toString((float[]) primitiveArray); + + } else if (double.class.equals(componentType)) { + return Arrays.toString((double[]) primitiveArray); + } + return ""; + } + + /** + * Formats the given arguments by retrieving it's {@link String} representation and separate it by comma (= + * {@code ,}). + * + * @param arguments to be formatted + * @return the {@link String} representation of the given {@link Object}{@code []} + */ + private String formatArray(Object[] array) { + StringBuilder stringBuilder = new StringBuilder(); + for (int i = 0; i < array.length; i++) { + stringBuilder.append(format(array[i])); + if (i < array.length - 1) { + stringBuilder.append(", "); + } + } + return stringBuilder.toString(); + } + + private String replaceNonPrintableChars(String input, String replacement) { + StringBuilder result = new StringBuilder(); + for (int offset = 0; offset < input.length(); ) { + int codePoint = input.codePointAt(offset); + offset += Character.charCount(codePoint); + + // Replace invisible control characters and unused code points + switch (Character.getType(codePoint)) { + case Character.CONTROL: // \p{Cc} + case Character.FORMAT: // \p{Cf} + case Character.PRIVATE_USE: // \p{Co} + case Character.SURROGATE: // \p{Cs} + case Character.UNASSIGNED: // \p{Cn} + result.append(replacement); + break; + + default: + result.append(Character.toChars(codePoint)); + break; + } + } + return result.toString(); + } +} diff --git a/junit-jupiter/src/test/java/com/tngtech/junit/dataprovider/placeholder/NamedArgumentPlaceholderTest.java b/junit-jupiter/src/test/java/com/tngtech/junit/dataprovider/placeholder/NamedArgumentPlaceholderTest.java new file mode 100644 index 00000000..459fecbb --- /dev/null +++ b/junit-jupiter/src/test/java/com/tngtech/junit/dataprovider/placeholder/NamedArgumentPlaceholderTest.java @@ -0,0 +1,783 @@ +package com.tngtech.junit.dataprovider.placeholder; + +import static com.tngtech.junit.dataprovider.placeholder.NamedArgumentPlaceholder.STRING_EMPTY; +import static com.tngtech.junit.dataprovider.placeholder.NamedArgumentPlaceholder.STRING_NON_PRINTABLE; +import static com.tngtech.junit.dataprovider.placeholder.NamedArgumentPlaceholder.STRING_NULL; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.OutputStream; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.logging.Handler; +import java.util.logging.Logger; +import java.util.logging.StreamHandler; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.platform.commons.support.ReflectionSupport; + +class NamedArgumentPlaceholderTest { + + private static OutputStream logCapturingStream; + private static StreamHandler customLogHandler; + + private final NamedArgumentPlaceholder underTest = new NamedArgumentPlaceholder(); + + private Method tenParamMethod; + + @BeforeEach + void prepareLogCapturing() { + // Logger matches Logger in ClassUnderTest + Logger logger = Logger.getLogger(NamedArgumentPlaceholder.class.getName()); + + logCapturingStream = new ByteArrayOutputStream(); + Handler[] handlers = logger.getParent().getHandlers(); + customLogHandler = new StreamHandler(logCapturingStream, handlers[0].getFormatter()); + + logger.addHandler(customLogHandler); + } + + @BeforeEach + void setup() { + tenParamMethod = ReflectionSupport + .findMethod(getClass(), "tenParamMethod", + new Class[] { char.class, int.class, long.class, double.class, String.class, char.class, + int.class, long.class, double.class, float.class }) + .orElseThrow( + () -> new IllegalStateException("Could not find method having ten parameters for testing.")); + } + + @Test + void testProcessShouldReplaceIndexSubscriptArgumentPlaceholderUsingPositiveIndex() { + // Given: + final List arguments = list('a', 1, 2l, 3.3, "four", 'f', 6, 7l, 8.8, 9.99f); + + ReplacementData data = ReplacementData.of(tenParamMethod, 0, arguments); + + // When: + String result = underTest.process(data, "%na[2]"); + + // Then: + assertThat(result).isEqualTo("l2=2"); + } + + @Test + void testProcessShouldReplaceIndexSubscriptArgumentPlaceholderUsingNegativeIndex() { + // Given: + final List arguments = list('a', 1, 2l, 3.3, "four", 'f', 6, 7l, 8.8, 9.99f); + + ReplacementData data = ReplacementData.of(tenParamMethod, 0, arguments); + + // When: + String result = underTest.process(data, "%na[-3]"); + + // Then: + assertThat(result).isEqualTo("l7=7"); + } + + @Test + void testProcessShouldReplaceWholeRangeSubscriptArgumentPlaceholderUsingOnlyPositiveIndices() { + // Given: + final List arguments = list('a', 1, 2l, 3.3, "four", 'f', 6, 7l, 8.8, 9.99f); + + ReplacementData data = ReplacementData.of(tenParamMethod, 0, arguments); + + // When: + String result = underTest.process(data, "%na[0..9]"); + + // Then: + assertThat(result).isEqualTo("c0=a, i1=1, l2=2, d3=3.3, s4=four, c5=f, i6=6, l7=7, d8=8.8, f9=9.99"); + } + + @Test + void testProcessShouldReplaceWholeRangeSubscriptArgumentPlaceholderUsingMixedIndices() { + // Given: + final List arguments = list('a', 1, 2l, 3.3, "four", 'f', 6, 7l, 8.8, 9.99f); + + ReplacementData data = ReplacementData.of(tenParamMethod, 0, arguments); + + // When: + String result = underTest.process(data, "%na[0..-1]"); + + // Then: + assertThat(result).isEqualTo("c0=a, i1=1, l2=2, d3=3.3, s4=four, c5=f, i6=6, l7=7, d8=8.8, f9=9.99"); + } + + @Test + void testProcessShouldReplaceWholeRangeSubscriptArgumentPlaceholderUsingMixedIndicesOtherWayRound() { + // Given: + final List arguments = list('a', 1, 2l, 3.3, "four", 'f', 6, 7l, 8.8, 9.99f); + + ReplacementData data = ReplacementData.of(tenParamMethod, 0, arguments); + + // When: + String result = underTest.process(data, "%na[-10..9]"); + + // Then: + assertThat(result).isEqualTo("c0=a, i1=1, l2=2, d3=3.3, s4=four, c5=f, i6=6, l7=7, d8=8.8, f9=9.99"); + } + + @Test + void testProcessShouldReplaceWholeRangeSubscriptArgumentPlaceholderUsingOnlyNegativeIndices() { + // Given: + final List arguments = list('a', 1, 2l, 3.3, "four", 'f', 6, 7l, 8.8, 9.99f); + + ReplacementData data = ReplacementData.of(tenParamMethod, 0, arguments); + + // When: + String result = underTest.process(data, "%na[-10..-1]"); + + // Then: + assertThat(result).isEqualTo("c0=a, i1=1, l2=2, d3=3.3, s4=four, c5=f, i6=6, l7=7, d8=8.8, f9=9.99"); + } + + @Test + void testProcessShouldReplaceWholeWithoutFirstRangeSubscriptArgumentPlaceholder() { + // Given: + final List arguments = list('a', 1, 2l, 3.3, "four", 'f', 6, 7l, 8.8, 9.99f); + + ReplacementData data = ReplacementData.of(tenParamMethod, 0, arguments); + + // When: + String result = underTest.process(data, "%na[1..-1]"); + + // Then: + assertThat(result).isEqualTo("i1=1, l2=2, d3=3.3, s4=four, c5=f, i6=6, l7=7, d8=8.8, f9=9.99"); + } + + @Test + void testProcessShouldReplaceWholeWithoutLastRangeSubscriptArgumentPlaceholderUsingOnlyPositiveIndices() { + // Given: + final List arguments = list('a', 1, 2l, 3.3, "four", 'f', 6, 7l, 8.8, 9.99f); + + ReplacementData data = ReplacementData.of(tenParamMethod, 0, arguments); + + // When: + String result = underTest.process(data, "%na[0..8]"); + + // Then: + assertThat(result).isEqualTo("c0=a, i1=1, l2=2, d3=3.3, s4=four, c5=f, i6=6, l7=7, d8=8.8"); + } + + @Test + void testProcessShouldReplacePartialRangeSubscriptArgumentPlaceholderContainingJustOneValueUsingOnlyPositiveIndices() { + // Given: + final List arguments = list('a', 1, 2l, 3.3, "four", 'f', 6, 7l, 8.8, 9.99f); + + ReplacementData data = ReplacementData.of(tenParamMethod, 0, arguments); + + // When: + String result = underTest.process(data, "%na[4..6]"); + + // Then: + assertThat(result).isEqualTo("s4=four, c5=f, i6=6"); + } + + @Test + void testProcessShouldReplacePartialRangeSubscriptArgumentPlaceholderContainingJustOneValueUsingMixedIndices() { + // Given: + final List arguments = list('a', 1, 2l, 3.3, "four", 'f', 6, 7l, 8.8, 9.99f); + + ReplacementData data = ReplacementData.of(tenParamMethod, 0, arguments); + + // When: + String result = underTest.process(data, "%na[4..-4]"); + + // Then: + assertThat(result).isEqualTo("s4=four, c5=f, i6=6"); + } + + @Test + void testProcessShouldReplacePartialRangeSubscriptArgumentPlaceholderContainingJustOneValueUsingMixedIndicesOtherWayRound() { + // Given: + final List arguments = list('a', 1, 2l, 3.3, "four", 'f', 6, 7l, 8.8, 9.99f); + + ReplacementData data = ReplacementData.of(tenParamMethod, 0, arguments); + + // When: + String result = underTest.process(data, "%na[-6..6]"); + + // Then: + assertThat(result).isEqualTo("s4=four, c5=f, i6=6"); + } + + @Test + void testProcessShouldReplacePartialRangeSubscriptArgumentPlaceholderContainingJustOneValueUsingOnlyNegativeIndices() { + // Given: + final List arguments = list('a', 1, 2l, 3.3, "four", 'f', 6, 7l, 8.8, 9.99f); + + ReplacementData data = ReplacementData.of(tenParamMethod, 0, arguments); + + // When: + String result = underTest.process(data, "%na[-6..-4]"); + + // Then: + assertThat(result).isEqualTo("s4=four, c5=f, i6=6"); + } + + @Test + void testProcessShouldReplaceSingleValueRangeSubscriptArgumentPlaceholderUsingOnlyPositiveIndices() { + // Given: + final List arguments = list('a', 1, 2l, 3.3, "four", 'f', 6, 7l, 8.8, 9.99f); + + ReplacementData data = ReplacementData.of(tenParamMethod, 0, arguments); + + // When: + String result = underTest.process(data, "%na[7..7]"); + + // Then: + assertThat(result).isEqualTo("l7=7"); + } + + @Test + void testProcessShouldReplaceSingleValueRangeSubscriptArgumentPlaceholderUsingMixedIndices() { + // Given: + final List arguments = list('a', 1, 2l, 3.3, "four", 'f', 6, 7l, 8.8, 9.99f); + + ReplacementData data = ReplacementData.of(tenParamMethod, 0, arguments); + + // When: + String result = underTest.process(data, "%na[7..-3]"); + + // Then: + assertThat(result).isEqualTo("l7=7"); + } + + @Test + void testProcessShouldReplaceSingleValueRangeSubscriptArgumentPlaceholderUsingOnlyNegativeIndices() { + // Given: + final List arguments = list('a', 1, 2l, 3.3, "four", 'f', 6, 7l, 8.8, 9.99f); + + ReplacementData data = ReplacementData.of(tenParamMethod, 0, arguments); + + // When: + String result = underTest.process(data, "%na[-3..-3]"); + + // Then: + assertThat(result).isEqualTo("l7=7"); + } + + @Test + void testProcessToDoShouldReplaceSingleValueRangeSubscriptArgumentPlaceholderUsingOnlyNegativeIndices() { + // Given: + final Method testMethod = ReflectionSupport + .findMethod(Assertions.class, "assertTrue", new Class[] { boolean.class }) + .orElseThrow(() -> new IllegalStateException("Could not find method")); + + final List arguments = list('x'); + + + ReplacementData data = ReplacementData.of(testMethod, 0, arguments); + + // When: + String result = underTest.process(data, "%na[0]"); + + // Then: + assertThat(result).isEqualTo("arg0=x"); + assertThat(getTestCapturedLog()).containsPattern( + "WARNING: Parameter names on method '.*' are not available. To store formal parameter names, compile the source file with the '-parameters' option"); + } + + @Test + void testFormatAllShouldHandleSingleValueCorrectly() { + // Given: + final List arguments = list(12.45); + + // When: + String result = underTest.formatAll(paramsWith(1), arguments); + + // Then: + assertThat(result).isEqualTo("c0=12.45"); + } + + @Test + void testFormatAllShouldReturnAllThreeValuesCorrectly() { + // Given: + final List arguments = list("test", 1, 2L); + + // When: + String result = underTest.formatAll(paramsWith(3), arguments); + + // Then: + assertThat(result).isEqualTo("c0=test, i1=1, l2=2"); + } + + @Test + void testFormatAllHandleNullSpecially() { + // Given: + final List arguments = list(null); + + // When: + String result = underTest.formatAll(paramsWith(1), arguments); + + // Then: + assertThat(result).isEqualTo("c0=" + STRING_NULL); + } + + @Test + void testFormatAllHandleNullNullCorrectly() { + // Given: + final List arguments = list(null, (Object) null); // cast to suppress compiler warning + + // When: + String result = underTest.formatAll(paramsWith(2), arguments); + + // Then: + assertThat(result).isEqualTo("c0=, i1="); + } + + @Test + void testFormatAllDoesNotThrowNullPointerExceptionIfParamsToStringReturningNull() { + // Given: + final List arguments = list(new TestToString(null)); + + // When: + String result = underTest.formatAll(paramsWith(1), arguments); + + // Then: + assertThat(result).isEqualTo("c0=" + STRING_NULL); + } + + @Test + void testFormatAllHandleEmtpyStringSpecially() { + // Given: + final List arguments = list(""); + + // When: + String result = underTest.formatAll(paramsWith(1), arguments); + + // Then: + assertThat(result).isEqualTo("c0=" + STRING_EMPTY); + } + + @Test + void testFormatAllReplacesNullTerminatorWithTheirPrintableCounterpart() { + // Given: + final List arguments = list("\0"); + + // When: + String result = underTest.formatAll(paramsWith(1), arguments); + + // Then: + assertThat(result).isEqualTo("c0=\\0"); + } + + @Test + void testFormatAllReplacesNullTerminatorWithTheirPrintableCounterpartEvenIfWithText() { + // Given: + final List arguments = list("test\0test\0"); + + // When: + String result = underTest.formatAll(paramsWith(1), arguments); + + // Then: + assertThat(result).isEqualTo("c0=test\\0test\\0"); + } + + @Test + void testFormatAllReplacesCarriageReturnWithTheirPrintableCounterpart() { + // Given: + final List arguments = list("\r"); + + // When: + String result = underTest.formatAll(paramsWith(1), arguments); + + // Then: + assertThat(result).isEqualTo("c0=\\r"); + } + + @Test + void testFormatAllReplacesCarriageReturnsWithTheirPrintableCounterpartEvenIfWithText() { + // Given: + final List arguments = list("test\rtest\r"); + + // When: + String result = underTest.formatAll(paramsWith(1), arguments); + + // Then: + assertThat(result).isEqualTo("c0=test\\rtest\\r"); + } + + @Test + void testFormatAllReplacesLineFeedWithTheirPrintableCounterpart() { + // Given: + final List arguments = list("\n"); + + // When: + String result = underTest.formatAll(paramsWith(1), arguments); + + // Then: + assertThat(result).isEqualTo("c0=\\n"); + } + + @Test + void testFormatAllReplacesLineFeedsWithTheirPrintableCounterpartEvenIfWithText() { + // Given: + final List arguments = list("1\n2\n3"); + + // When: + String result = underTest.formatAll(paramsWith(1), arguments); + + // Then: + assertThat(result).isEqualTo("c0=1\\n2\\n3"); + } + + @Test + void testFormatAllReplacesNonPrintableCharactersWithPredefinedPrintableCounterpart() { + // Given: + final List arguments = list("\u001F"); + + // When: + String result = underTest.formatAll(paramsWith(1), arguments); + + // Then: + assertThat(result).isEqualTo("c0=" + STRING_NON_PRINTABLE); + } + + @Test + void testFormatAllReplacesNonPrintableCharactersWithPredefinedPrintableCounterpartEvenIfWithText() { + // Given: + final List arguments = list("test\btest\uFFFF"); + + // When: + String result = underTest.formatAll(paramsWith(1), arguments); + + // Then: + assertThat(result).isEqualTo("c0=test" + STRING_NON_PRINTABLE + "test" + STRING_NON_PRINTABLE); + } + + @Test + void testFormatAllReplacesCarriageReturnsAndLineFeedsWithTheirPrintableCounterpart() { + // Given: + final List arguments = list("A very\r\nlong text\nwith multiple\rdifferent newline\n\rvariations."); + + // When: + String result = underTest.formatAll(paramsWith(1), arguments); + + // Then: + assertThat(result).isEqualTo("c0=A very\\r\\nlong text\\nwith multiple\\rdifferent newline\\n\\rvariations."); + } + + @Test + void testFormatAllShouldPrintQuestionMarkForUnknownParametersToBeFailureTolerantInsteadOfThrowAnException() { + // Given: + final List arguments = list(0, '1', 2.0); + + // When: + String result = underTest.formatAll(paramsWith(1), arguments); + + // Then: + assertThat(result).isEqualTo("c0=0, ?=1, ?=2.0"); + } + + @Test + void testFormatAllShouldOnlyPrintArgumentsEvenIfTooManyParametersAreGiven() { + // Given: + final List arguments = list(0, '1', 2.0, 3l); + + // When: + String result = underTest.formatAll(paramsWith(10), arguments); + + // Then: + assertThat(result).isEqualTo("c0=0, i1=1, l2=2.0, d3=3"); + } + + @Test + void testFormatForCustomObjectReplacesCarriageReturnWithTheirPrintableCounterpart() { + // Given: + final TestToString argument = new TestToString("\r"); + + // When: + String result = underTest.format(argument); + + // Then: + assertThat(result).isEqualTo("\\r"); + } + + @Test + void testFormatForCustomObjectReplacesCarriageReturnsWithTheirPrintableCounterpartEvenIfWithText() { + // Given: + final TestToString argument = new TestToString("test\rtest\r"); + + // When: + String result = underTest.format(argument); + + // Then: + assertThat(result).isEqualTo("test\\rtest\\r"); + } + + @Test + void testFormatForCustomObjectReplacesLineFeedWithTheirPrintableCounterpart() { + // Given: + final TestToString argument = new TestToString("\n"); + + // When: + String result = underTest.format(argument); + + // Then: + assertThat(result).isEqualTo("\\n"); + } + + @Test + void testFormatForCustomObjectReplacesLineFeedsWithTheirPrintableCounterpartEvenIfWithText() { + // Given: + final TestToString argument = new TestToString("1\n2\n3"); + + // When: + String result = underTest.format(argument); + + // Then: + assertThat(result).isEqualTo("1\\n2\\n3"); + } + + @Test + void testFormatForCustomObjectReplacesCarriageReturnsAndLineFeedsWithTheirPrintableCounterpart() { + // Given: + final TestToString argument = new TestToString( + "A very\r\nlong text\nwith multiple\rdifferent newline\n\rvariations."); + + // When: + String result = underTest.format(argument); + + // Then: + assertThat(result).isEqualTo("A very\\r\\nlong text\\nwith multiple\\rdifferent newline\\n\\rvariations."); + } + + @Test + void testFormatForCustomObjectReplacesNullFromToString() { + // Given: + final TestToString argument = new TestToString(null); + + // When: + String result = underTest.format(argument); + + // Then: + assertThat(result).isEqualTo(STRING_NULL); + } + + @Test + void testFormatAllHandleObjectArrayCorrectly() { + // Given: + final List arguments = list(new Object[] { 7.5, "test" }); + + // When: + String result = underTest.formatAll(paramsWith(1), arguments); + + // Then: + assertThat(result).isEqualTo("c0=[7.5, test]"); + } + + @Test + void testFormatAllHandlePrimitiveBooleanTypeArrayCorrectly() { + // Given: + final List arguments = list(new boolean[] { true, false }); + + // When: + String result = underTest.formatAll(paramsWith(1), arguments); + + // Then: + assertThat(result).isEqualTo("c0=[true, false]"); + } + + @Test + void testFormatAllHandlePrimitiveByteTypeArrayCorrectly() { + // Given: + final List arguments = list(new byte[] { 12, 24 }); + + // When: + String result = underTest.formatAll(paramsWith(1), arguments); + + // Then: + assertThat(result).isEqualTo("c0=[12, 24]"); + } + + @Test + void testFormatAllHandlePrimitiveCharTypeArrayCorrectly() { + // Given: + final List arguments = list(new char[] { 'a', '0' }); + + // When: + String result = underTest.formatAll(paramsWith(1), arguments); + + // Then: + assertThat(result).isEqualTo("c0=[a, 0]"); + } + + @Test + void testFormatAllHandlePrimitiveShortTypeArrayCorrectly() { + // Given: + final List arguments = list(new short[] { 1024 }); + + // When: + String result = underTest.formatAll(paramsWith(1), arguments); + + // Then: + assertThat(result).isEqualTo("c0=[1024]"); + } + + @Test + void testFormatAllHandlePrimitiveIntTypeArrayCorrectly() { + // Given: + final List arguments = list(new int[] { 11, 2 }); + + // When: + String result = underTest.formatAll(paramsWith(1), arguments); + + // Then: + assertThat(result).isEqualTo("c0=[11, 2]"); + } + + @Test + void testFormatAllHandlePrimitiveLongTypeArrayCorrectly() { + // Given: + final List arguments = list(new long[] { 111L, 222L, 333L }); + + // When: + String result = underTest.formatAll(paramsWith(1), arguments); + + // Then: + assertThat(result).isEqualTo("c0=[111, 222, 333]"); + } + + @Test + void testFormatAllHandlePrimitiveFloatTypeArrayCorrectly() { + // Given: + final List arguments = list(new float[] { 0.3f, 0.9f, 0.81f, 0.6561f }); + + // When: + String result = underTest.formatAll(paramsWith(1), arguments); + + // Then: + assertThat(result).isEqualTo("c0=[0.3, 0.9, 0.81, 0.6561]"); + } + + @Test + void testFormatAllHandlePrimitiveDoubleTypeArrayCorrectly() { + // Given: + final List arguments = list(new double[] { .78, 3.15E2 }); + + // When: + String result = underTest.formatAll(paramsWith(1), arguments); + + // Then: + assertThat(result).isEqualTo("c0=[0.78, 315.0]"); + } + + @Test + void testFormatAllHandleFurtherNestedArraysCorrectly() { + // Given: + final List arguments = list(new Object[] { 2, new char[] { 'a', 'b' }, new String[] { "a", "b" } }); + + // When: + String result = underTest.formatAll(paramsWith(2), arguments); + + // Then: + assertThat(result).isEqualTo("c0=[2, [a, b], [a, b]]"); + } + + @Test + void testFormatAllHandleObjectCorrectly() { + // Given: + final List arguments = list(new Object()); + + // When: + String result = underTest.formatAll(paramsWith(1), arguments); + + // Then: + assertThat(result).matches("c0=java.lang.Object@[0-9a-f]+"); + } + + @Test + void testFormatAllHandleListsCorrectly() { + // Given: + final List arguments = list(list("test", 1, 1723940567289346512L), 3); + + // When: + String result = underTest.formatAll(paramsWith(2), arguments); + + // Then: + assertThat(result).isEqualTo("c0=[test, 1, 1723940567289346512], i1=3"); + } + + @Test + void testFormatAllHandleEnumsCorrectly() { + // Given: + final List arguments = list(Thread.State.RUNNABLE); + + // When: + String result = underTest.formatAll(paramsWith(1), arguments); + + // Then: + assertThat(result).isEqualTo("c0=RUNNABLE"); + } + + @Test + void testFormatAllHandleComplexExampleCorrectly() { + // Given: + Date now = new Date(); + // @formatter:off + final List arguments = list( + now, + Double.valueOf(3.5), + new StringBuilder("1").append("|2").append("|3"), + new File("src/main/java/com/tngtech") + ); + // @formatter:on + + // When: + String result = underTest.formatAll(paramsWith(4), arguments); + + // Then: + assertThat(result).isEqualTo("c0=" + now.toString() + ", i1=3.5, l2=1|2|3, d3=src/main/java/com/tngtech"); + } + + // -- helper methods ----------------------------------------------------------------------------------------------- + + private static class TestToString { + private final String toString; + + TestToString(String toString) { + this.toString = toString; + } + + @Override + public String toString() { + return toString; + } + } + + @SuppressWarnings("unused") + void tenParamMethod(char c0, int i1, long l2, double d3, String s4, char c5, int i6, long l7, double d8, float f9) { + // nothing to do + } + + private List list(Object first, Object... remaining) { + List result = new ArrayList<>(); + result.add(first); + for (Object object : remaining) { + result.add(object); + } + return result; + } + + private Parameter[] paramsWith(int size) { + Parameter[] parameters = tenParamMethod.getParameters(); + if (size > parameters.length) { + fail("Max '%d' parameters are available, requested '%d'.", parameters.length, size); + } + return Arrays.copyOf(parameters, size); + } + + public String getTestCapturedLog() { + customLogHandler.flush(); + return logCapturingStream.toString(); + } +} From 30ef0b29f76196c030c70a832d60ab6ba9b5f6a3 Mon Sep 17 00:00:00 2001 From: Andreas Schmid Date: Wed, 1 Nov 2017 22:15:11 +0100 Subject: [PATCH 2/5] create abstract base class for argument placeholder which formats arguments properly (#90) * adjusted NamedArgument- and ArgumentPlaceholder to reuse features of AbstractArgumentPlaceholder * also adjusted ArgumentPlaceholder test pattern to mainly use "%a" (but still also tests deprecated "%p") --- .../AbstractArgumentPlaceholder.java | 147 +++++++++++++++++ .../placeholder/ArgumentPlaceholder.java | 133 +-------------- .../placeholder/ArgumentPlaceholderTest.java | 26 +-- .../placeholder/NamedArgumentPlaceholder.java | 151 +----------------- 4 files changed, 170 insertions(+), 287 deletions(-) create mode 100644 core/src/main/java/com/tngtech/junit/dataprovider/placeholder/AbstractArgumentPlaceholder.java diff --git a/core/src/main/java/com/tngtech/junit/dataprovider/placeholder/AbstractArgumentPlaceholder.java b/core/src/main/java/com/tngtech/junit/dataprovider/placeholder/AbstractArgumentPlaceholder.java new file mode 100644 index 00000000..6bbe7b62 --- /dev/null +++ b/core/src/main/java/com/tngtech/junit/dataprovider/placeholder/AbstractArgumentPlaceholder.java @@ -0,0 +1,147 @@ +package com.tngtech.junit.dataprovider.placeholder; + +import java.util.Arrays; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +/** + * This abstract placeholder is able to format arguments of a dataprovider test as comma-separated {@link String} + * according to the given index or range subscript. Furthermore the following arguments are treated specially: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Argument valuetarget {@link String}
null<null>
"" (= empty string)<empty string>
array (e.g. String[]){@code "[" + formatPattern(array) + "]"}
other{@link Object#toString()}
+ */ +abstract class AbstractArgumentPlaceholder extends BasePlaceholder { + + /** + * {@link String} representation of {@code null} + */ + protected static final String STRING_NULL = ""; + + /** + * {@link String} representation of {@code ""} + */ + protected static final String STRING_EMPTY = ""; + + /** + * {@link String} representation of an non-printable character + */ + protected static final String STRING_NON_PRINTABLE = ""; + + AbstractArgumentPlaceholder(String placeholderRegex) { + super(placeholderRegex); + } + + @SuppressFBWarnings(value = "RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE", justification = "false positive if 'param.toString()' returns 'null'") + protected String format(Object param) { + if (param == null) { + return STRING_NULL; + + } else if (param.getClass().isArray()) { + if (param.getClass().getComponentType().isPrimitive()) { + return formatValuesOfPrimitiveArray(param); + } + return "[" + formatValuesOfArray((Object[]) param) + "]"; + + } else if (param instanceof String && ((String) param).isEmpty()) { + return STRING_EMPTY; + + } + + String result; + if (param instanceof String) { + result = (String) param; + } else { + result = param.toString(); + } + if (result == null) { // maybe null if "param.toString()" returns null + return STRING_NULL; + } + result = result.replaceAll("\0", "\\\\0").replaceAll("\r", "\\\\r").replaceAll("\n", "\\\\n"); + return replaceNonPrintableChars(result, STRING_NON_PRINTABLE); + } + + private String formatValuesOfPrimitiveArray(Object primitiveArray) { + Class componentType = primitiveArray.getClass().getComponentType(); + + if (boolean.class.equals(componentType)) { + return Arrays.toString((boolean[]) primitiveArray); + + } else if (byte.class.equals(componentType)) { + return Arrays.toString((byte[]) primitiveArray); + + } else if (char.class.equals(componentType)) { + return Arrays.toString((char[]) primitiveArray); + + } else if (short.class.equals(componentType)) { + return Arrays.toString((short[]) primitiveArray); + + } else if (int.class.equals(componentType)) { + return Arrays.toString((int[]) primitiveArray); + + } else if (long.class.equals(componentType)) { + return Arrays.toString((long[]) primitiveArray); + + } else if (float.class.equals(componentType)) { + return Arrays.toString((float[]) primitiveArray); + + } else if (double.class.equals(componentType)) { + return Arrays.toString((double[]) primitiveArray); + } + throw new IllegalStateException("Called 'formatPrimitiveArray' on non-primitive array"); + } + + private String formatValuesOfArray(Object[] array) { + StringBuilder stringBuilder = new StringBuilder(); + for (int i = 0; i < array.length; i++) { + stringBuilder.append(format(array[i])); + if (i < array.length - 1) { + stringBuilder.append(", "); + } + } + return stringBuilder.toString(); + } + + private String replaceNonPrintableChars(String input, String replacement) { + StringBuilder result = new StringBuilder(); + for (int offset = 0; offset < input.length();) { + int codePoint = input.codePointAt(offset); + offset += Character.charCount(codePoint); + + // Replace invisible control characters and unused code points + switch (Character.getType(codePoint)) { + case Character.CONTROL: // \p{Cc} + case Character.FORMAT: // \p{Cf} + case Character.PRIVATE_USE: // \p{Co} + case Character.SURROGATE: // \p{Cs} + case Character.UNASSIGNED: // \p{Cn} + result.append(replacement); + break; + + default: + result.append(Character.toChars(codePoint)); + break; + } + } + return result.toString(); + } +} diff --git a/core/src/main/java/com/tngtech/junit/dataprovider/placeholder/ArgumentPlaceholder.java b/core/src/main/java/com/tngtech/junit/dataprovider/placeholder/ArgumentPlaceholder.java index 6b8efa47..5300c474 100644 --- a/core/src/main/java/com/tngtech/junit/dataprovider/placeholder/ArgumentPlaceholder.java +++ b/core/src/main/java/com/tngtech/junit/dataprovider/placeholder/ArgumentPlaceholder.java @@ -1,53 +1,14 @@ package com.tngtech.junit.dataprovider.placeholder; -import java.util.Arrays; import java.util.List; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - /** * This placeholder format the arguments of a dataprovider test as comma-separated {@link String} according to the given - * index or range subscript. Furthermore the following arguments are treated specially: - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
Argument valuetarget {@link String}
null<null>
"" (= empty string)<empty string>
array (e.g. String[]){@code "[" + formatPattern(array) + "]"}
other{@link Object#toString()}
+ * index or range subscript. For a list of special argument treatments, see {@link AbstractArgumentPlaceholder}. + * + * @see AbstractArgumentPlaceholder */ -public class ArgumentPlaceholder extends BasePlaceholder { - - /** - * {@link String} representation of {@code null} - */ - protected static final String STRING_NULL = ""; - - /** - * {@link String} representation of {@code ""} - */ - protected static final String STRING_EMPTY = ""; - - /** - * {@link String} representation of an non-printable character - */ - protected static final String STRING_NON_PRINTABLE = ""; - +public class ArgumentPlaceholder extends AbstractArgumentPlaceholder { public ArgumentPlaceholder() { super("%[ap]\\[(-?[0-9]+|-?[0-9]+\\.\\.-?[0-9]+)\\]"); @@ -80,7 +41,7 @@ protected String getReplacementFor(String placeholder, ReplacementData data) { * {@code ,}). * * @param arguments to be formatted - * @return the {@link String} representation of the given {@link Object}{@code []} + * @return the {@link String} representation of the given {@link List}{@code } */ protected String formatAll(List arguments) { StringBuilder stringBuilder = new StringBuilder(); @@ -92,88 +53,4 @@ protected String formatAll(List arguments) { } return stringBuilder.toString(); } - - - @SuppressFBWarnings(value = "RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE", justification = "false positive if 'param.toString()' returns 'null'") - protected String format(Object param) { - if (param == null) { - return STRING_NULL; - - } else if (param.getClass().isArray()) { - if (param.getClass().getComponentType().isPrimitive()) { - return formatPrimitiveArray(param); - } - return "[" + formatAll(Arrays.asList((Object[]) param)) + "]"; - - } else if (param instanceof String && ((String) param).isEmpty()) { - return STRING_EMPTY; - - } - - String result; - if (param instanceof String) { - result = (String) param; - } else { - result = param.toString(); - } - if (result == null) { // maybe null if "param.toString()" returns null - return STRING_NULL; - } - result = result.replaceAll("\0", "\\\\0").replaceAll("\r", "\\\\r").replaceAll("\n", "\\\\n"); - return replaceNonPrintableChars(result, STRING_NON_PRINTABLE); - } - - private String formatPrimitiveArray(Object primitiveArray) { - Class componentType = primitiveArray.getClass().getComponentType(); - - if (boolean.class.equals(componentType)) { - return Arrays.toString((boolean[]) primitiveArray); - - } else if (byte.class.equals(componentType)) { - return Arrays.toString((byte[]) primitiveArray); - - } else if (char.class.equals(componentType)) { - return Arrays.toString((char[]) primitiveArray); - - } else if (short.class.equals(componentType)) { - return Arrays.toString((short[]) primitiveArray); - - } else if (int.class.equals(componentType)) { - return Arrays.toString((int[]) primitiveArray); - - } else if (long.class.equals(componentType)) { - return Arrays.toString((long[]) primitiveArray); - - } else if (float.class.equals(componentType)) { - return Arrays.toString((float[]) primitiveArray); - - } else if (double.class.equals(componentType)) { - return Arrays.toString((double[]) primitiveArray); - } - return ""; - } - - private String replaceNonPrintableChars(String input, String replacement) { - StringBuilder result = new StringBuilder(); - for (int offset = 0; offset < input.length(); ) { - int codePoint = input.codePointAt(offset); - offset += Character.charCount(codePoint); - - // Replace invisible control characters and unused code points - switch (Character.getType(codePoint)) { - case Character.CONTROL: // \p{Cc} - case Character.FORMAT: // \p{Cf} - case Character.PRIVATE_USE: // \p{Co} - case Character.SURROGATE: // \p{Cs} - case Character.UNASSIGNED: // \p{Cn} - result.append(replacement); - break; - - default: - result.append(Character.toChars(codePoint)); - break; - } - } - return result.toString(); - } } diff --git a/core/src/test/java/com/tngtech/junit/dataprovider/placeholder/ArgumentPlaceholderTest.java b/core/src/test/java/com/tngtech/junit/dataprovider/placeholder/ArgumentPlaceholderTest.java index cb3c00de..1bca4116 100644 --- a/core/src/test/java/com/tngtech/junit/dataprovider/placeholder/ArgumentPlaceholderTest.java +++ b/core/src/test/java/com/tngtech/junit/dataprovider/placeholder/ArgumentPlaceholderTest.java @@ -40,7 +40,7 @@ public void testProcessShouldReplaceIndexSubscriptArgumentPlaceholderUsingNegati ReplacementData data = ReplacementData.of(Methods.anyMethod(), 0, arguments); // When: - String result = underTest.process(data, "%p[-3]"); + String result = underTest.process(data, "%a[-3]"); // Then: assertThat(result).isEqualTo("1"); @@ -68,7 +68,7 @@ public void testProcessShouldReplaceWholeRangeSubscriptArgumentPlaceholderUsingM ReplacementData data = ReplacementData.of(Methods.anyMethod(), 0, arguments); // When: - String result = underTest.process(data, "%p[0..-1]"); + String result = underTest.process(data, "%a[0..-1]"); // Then: assertThat(result).isEqualTo("0, 1, 2, 3, 4, 5, 6, 7, 8, 9"); @@ -82,7 +82,7 @@ public void testProcessShouldReplaceWholeRangeSubscriptArgumentPlaceholderUsingM ReplacementData data = ReplacementData.of(Methods.anyMethod(), 0, arguments); // When: - String result = underTest.process(data, "%p[-10..9]"); + String result = underTest.process(data, "%a[-10..9]"); // Then: assertThat(result).isEqualTo("0, 1, 2, 3, 4, 5, 6, 7, 8, 9"); @@ -96,7 +96,7 @@ public void testProcessShouldReplaceWholeRangeSubscriptArgumentPlaceholderUsingO ReplacementData data = ReplacementData.of(Methods.anyMethod(), 0, arguments); // When: - String result = underTest.process(data, "%p[-10..-1]"); + String result = underTest.process(data, "%a[-10..-1]"); // Then: assertThat(result).isEqualTo("0, 1, 2, 3, 4, 5, 6, 7, 8, 9"); @@ -110,7 +110,7 @@ public void testProcessShouldReplaceWholeWithoutFirstRangeSubscriptArgumentPlace ReplacementData data = ReplacementData.of(Methods.anyMethod(), 0, arguments); // When: - String result = underTest.process(data, "%p[1..-1]"); + String result = underTest.process(data, "%a[1..-1]"); // Then: assertThat(result).isEqualTo("1, 2, 3, 4, 5, 6, 7, 8, 9"); @@ -124,7 +124,7 @@ public void testProcessShouldReplaceWholeWithoutLastRangeSubscriptArgumentPlaceh ReplacementData data = ReplacementData.of(Methods.anyMethod(), 0, arguments); // When: - String result = underTest.process(data, "%p[0..8]"); + String result = underTest.process(data, "%a[0..8]"); // Then: assertThat(result).isEqualTo("0, 1, 2, 3, 4, 5, 6, 7, 8"); @@ -138,7 +138,7 @@ public void testProcessShouldReplacePartialRangeSubscriptArgumentPlaceholderCont ReplacementData data = ReplacementData.of(Methods.anyMethod(), 0, arguments); // When: - String result = underTest.process(data, "%p[4..6]"); + String result = underTest.process(data, "%a[4..6]"); // Then: assertThat(result).isEqualTo("4, 5, 6"); @@ -152,7 +152,7 @@ public void testProcessShouldReplacePartialRangeSubscriptArgumentPlaceholderCont ReplacementData data = ReplacementData.of(Methods.anyMethod(), 0, arguments); // When: - String result = underTest.process(data, "%p[4..-4]"); + String result = underTest.process(data, "%a[4..-4]"); // Then: assertThat(result).isEqualTo("4, 5, 6"); @@ -166,7 +166,7 @@ public void testProcessShouldReplacePartialRangeSubscriptArgumentPlaceholderCont ReplacementData data = ReplacementData.of(Methods.anyMethod(), 0, arguments); // When: - String result = underTest.process(data, "%p[-6..6]"); + String result = underTest.process(data, "%a[-6..6]"); // Then: assertThat(result).isEqualTo("4, 5, 6"); @@ -180,7 +180,7 @@ public void testProcessShouldReplacePartialRangeSubscriptArgumentPlaceholderCont ReplacementData data = ReplacementData.of(Methods.anyMethod(), 0, arguments); // When: - String result = underTest.process(data, "%p[-6..-4]"); + String result = underTest.process(data, "%a[-6..-4]"); // Then: assertThat(result).isEqualTo("4, 5, 6"); @@ -194,7 +194,7 @@ public void testProcessShouldReplaceSingleValueRangeSubscriptArgumentPlaceholder ReplacementData data = ReplacementData.of(Methods.anyMethod(), 0, arguments); // When: - String result = underTest.process(data, "%p[7..7]"); + String result = underTest.process(data, "%a[7..7]"); // Then: assertThat(result).isEqualTo("7"); @@ -208,7 +208,7 @@ public void testProcessShouldReplaceSingleValueRangeSubscriptArgumentPlaceholder ReplacementData data = ReplacementData.of(Methods.anyMethod(), 0, arguments); // When: - String result = underTest.process(data, "%p[7..-3]"); + String result = underTest.process(data, "%a[7..-3]"); // Then: assertThat(result).isEqualTo("7"); @@ -222,7 +222,7 @@ public void testProcessShouldReplaceSingleValueRangeSubscriptArgumentPlaceholder ReplacementData data = ReplacementData.of(Methods.anyMethod(), 0, arguments); // When: - String result = underTest.process(data, "%p[-3..-3]"); + String result = underTest.process(data, "%a[-3..-3]"); // Then: assertThat(result).isEqualTo("7"); diff --git a/junit-jupiter/src/main/java/com/tngtech/junit/dataprovider/placeholder/NamedArgumentPlaceholder.java b/junit-jupiter/src/main/java/com/tngtech/junit/dataprovider/placeholder/NamedArgumentPlaceholder.java index 176c8d2a..eae280bb 100644 --- a/junit-jupiter/src/main/java/com/tngtech/junit/dataprovider/placeholder/NamedArgumentPlaceholder.java +++ b/junit-jupiter/src/main/java/com/tngtech/junit/dataprovider/placeholder/NamedArgumentPlaceholder.java @@ -8,57 +8,17 @@ import org.junit.platform.commons.logging.Logger; import org.junit.platform.commons.logging.LoggerFactory; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - /** * This placeholder format the arguments including their parameter names of a dataprovider test as comma-separated - * {@link String} according to the given index or range subscript. Furthermore the following arguments are treated - * specially: - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
Argument valuetarget {@link String}
null<null>
"" (= empty string)<empty string>
array (e.g. String[]){@code "[" + formatPattern(array) + "]"}
other{@link Object#toString()}
- * Note: Test code must be compiled with "-parameters" option in order to retrieve formal parameter names! + * {@link String} according to the given index or range subscript. For a list of special argument treatments, see + * {@link AbstractArgumentPlaceholder}. + * + * @see AbstractArgumentPlaceholder */ -// TODO a lot of equal code to ArgumentPlaceholder, e.g. javadoc above, the constants below ... -public class NamedArgumentPlaceholder extends BasePlaceholder { +public class NamedArgumentPlaceholder extends AbstractArgumentPlaceholder { private static final Logger logger = LoggerFactory.getLogger(NamedArgumentPlaceholder.class); - /** - * W {@link String} representation of {@code null} - */ - protected static final String STRING_NULL = ""; - - /** - * {@link String} representation of {@code ""} - */ - protected static final String STRING_EMPTY = ""; - - /** - * {@link String} representation of an non-printable character - */ - protected static final String STRING_NON_PRINTABLE = ""; - - public NamedArgumentPlaceholder() { super("%na\\[(-?[0-9]+|-?[0-9]+\\.\\.-?[0-9]+)\\]"); } @@ -106,35 +66,6 @@ protected String formatAll(Parameter[] parameters, List arguments) { return stringBuilder.toString(); } - @SuppressFBWarnings(value = "RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE", justification = "false positive if 'param.toString()' returns 'null'") - protected String format(Object param) { - if (param == null) { - return STRING_NULL; - - } else if (param.getClass().isArray()) { - if (param.getClass().getComponentType().isPrimitive()) { - return formatPrimitiveArray(param); - } - return "[" + formatArray((Object[]) param) + "]"; - - } else if (param instanceof String && ((String) param).isEmpty()) { - return STRING_EMPTY; - - } - - String result; - if (param instanceof String) { - result = (String) param; - } else { - result = param.toString(); - } - if (result == null) { // maybe null if "param.toString()" returns null - return STRING_NULL; - } - result = result.replaceAll("\0", "\\\\0").replaceAll("\r", "\\\\r").replaceAll("\n", "\\\\n"); - return replaceNonPrintableChars(result, STRING_NON_PRINTABLE); - } - private Parameter[] getSubArrayOfMethodParameters(Method testMethod, int fromIndex, int toIndex) { Parameter[] parameters = testMethod.getParameters(); if (parameters.length > 0 && !parameters[0].isNamePresent()) { @@ -145,76 +76,4 @@ private Parameter[] getSubArrayOfMethodParameters(Method testMethod, int fromInd } return Arrays.copyOfRange(parameters, fromIndex, toIndex); } - - private String formatPrimitiveArray(Object primitiveArray) { - Class componentType = primitiveArray.getClass().getComponentType(); - - if (boolean.class.equals(componentType)) { - return Arrays.toString((boolean[]) primitiveArray); - - } else if (byte.class.equals(componentType)) { - return Arrays.toString((byte[]) primitiveArray); - - } else if (char.class.equals(componentType)) { - return Arrays.toString((char[]) primitiveArray); - - } else if (short.class.equals(componentType)) { - return Arrays.toString((short[]) primitiveArray); - - } else if (int.class.equals(componentType)) { - return Arrays.toString((int[]) primitiveArray); - - } else if (long.class.equals(componentType)) { - return Arrays.toString((long[]) primitiveArray); - - } else if (float.class.equals(componentType)) { - return Arrays.toString((float[]) primitiveArray); - - } else if (double.class.equals(componentType)) { - return Arrays.toString((double[]) primitiveArray); - } - return ""; - } - - /** - * Formats the given arguments by retrieving it's {@link String} representation and separate it by comma (= - * {@code ,}). - * - * @param arguments to be formatted - * @return the {@link String} representation of the given {@link Object}{@code []} - */ - private String formatArray(Object[] array) { - StringBuilder stringBuilder = new StringBuilder(); - for (int i = 0; i < array.length; i++) { - stringBuilder.append(format(array[i])); - if (i < array.length - 1) { - stringBuilder.append(", "); - } - } - return stringBuilder.toString(); - } - - private String replaceNonPrintableChars(String input, String replacement) { - StringBuilder result = new StringBuilder(); - for (int offset = 0; offset < input.length(); ) { - int codePoint = input.codePointAt(offset); - offset += Character.charCount(codePoint); - - // Replace invisible control characters and unused code points - switch (Character.getType(codePoint)) { - case Character.CONTROL: // \p{Cc} - case Character.FORMAT: // \p{Cf} - case Character.PRIVATE_USE: // \p{Co} - case Character.SURROGATE: // \p{Cs} - case Character.UNASSIGNED: // \p{Cn} - result.append(replacement); - break; - - default: - result.append(Character.toChars(codePoint)); - break; - } - } - return result.toString(); - } } From cb5102b90d49f22adc281ca339a7ea139a98b236 Mon Sep 17 00:00:00 2001 From: Andreas Schmid Date: Wed, 1 Nov 2017 22:45:13 +0100 Subject: [PATCH 3/5] switch to JUL logger (#90) * LoggerFactory and Logger of JUnit platform is "INTERNAL" API and should be avoided * also build breaks for 5.0.0-RC3 and below --- .../placeholder/NamedArgumentPlaceholder.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/junit-jupiter/src/main/java/com/tngtech/junit/dataprovider/placeholder/NamedArgumentPlaceholder.java b/junit-jupiter/src/main/java/com/tngtech/junit/dataprovider/placeholder/NamedArgumentPlaceholder.java index eae280bb..665012c6 100644 --- a/junit-jupiter/src/main/java/com/tngtech/junit/dataprovider/placeholder/NamedArgumentPlaceholder.java +++ b/junit-jupiter/src/main/java/com/tngtech/junit/dataprovider/placeholder/NamedArgumentPlaceholder.java @@ -4,9 +4,7 @@ import java.lang.reflect.Parameter; import java.util.Arrays; import java.util.List; - -import org.junit.platform.commons.logging.Logger; -import org.junit.platform.commons.logging.LoggerFactory; +import java.util.logging.Logger; /** * This placeholder format the arguments including their parameter names of a dataprovider test as comma-separated @@ -17,7 +15,7 @@ */ public class NamedArgumentPlaceholder extends AbstractArgumentPlaceholder { - private static final Logger logger = LoggerFactory.getLogger(NamedArgumentPlaceholder.class); + private static final Logger logger = Logger.getLogger(NamedArgumentPlaceholder.class.getName()); public NamedArgumentPlaceholder() { super("%na\\[(-?[0-9]+|-?[0-9]+\\.\\.-?[0-9]+)\\]"); @@ -69,7 +67,7 @@ protected String formatAll(Parameter[] parameters, List arguments) { private Parameter[] getSubArrayOfMethodParameters(Method testMethod, int fromIndex, int toIndex) { Parameter[] parameters = testMethod.getParameters(); if (parameters.length > 0 && !parameters[0].isNamePresent()) { - logger.warn(() -> String.format("Parameter names on method '%s' are not available" + logger.warning(String.format("Parameter names on method '%s' are not available" + ". To store formal parameter names, compile the source file with the '-parameters' option" + ". See also https://docs.oracle.com/javase/tutorial/reflect/member/methodparameterreflection.html", testMethod)); From 178a297167aa4d3948665075e148d838c41c03a6 Mon Sep 17 00:00:00 2001 From: Andreas Schmid Date: Sun, 5 Nov 2017 18:33:22 +0100 Subject: [PATCH 4/5] better method naming and check display name containing parameter names in integration test (#90) --- .../placeholder/AbstractArgumentPlaceholder.java | 8 ++++---- .../test/junit/dataprovider/FormatAcceptanceTest.java | 5 ++++- .../placeholder/NamedArgumentPlaceholder.java | 9 +++++---- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/com/tngtech/junit/dataprovider/placeholder/AbstractArgumentPlaceholder.java b/core/src/main/java/com/tngtech/junit/dataprovider/placeholder/AbstractArgumentPlaceholder.java index 6bbe7b62..edb1892b 100644 --- a/core/src/main/java/com/tngtech/junit/dataprovider/placeholder/AbstractArgumentPlaceholder.java +++ b/core/src/main/java/com/tngtech/junit/dataprovider/placeholder/AbstractArgumentPlaceholder.java @@ -58,9 +58,9 @@ protected String format(Object param) { } else if (param.getClass().isArray()) { if (param.getClass().getComponentType().isPrimitive()) { - return formatValuesOfPrimitiveArray(param); + return formatPrimitiveArray(param); } - return "[" + formatValuesOfArray((Object[]) param) + "]"; + return "[" + formatArray((Object[]) param) + "]"; } else if (param instanceof String && ((String) param).isEmpty()) { return STRING_EMPTY; @@ -80,7 +80,7 @@ protected String format(Object param) { return replaceNonPrintableChars(result, STRING_NON_PRINTABLE); } - private String formatValuesOfPrimitiveArray(Object primitiveArray) { + private String formatPrimitiveArray(Object primitiveArray) { Class componentType = primitiveArray.getClass().getComponentType(); if (boolean.class.equals(componentType)) { @@ -110,7 +110,7 @@ private String formatValuesOfPrimitiveArray(Object primitiveArray) { throw new IllegalStateException("Called 'formatPrimitiveArray' on non-primitive array"); } - private String formatValuesOfArray(Object[] array) { + private String formatArray(Object[] array) { StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < array.length; i++) { stringBuilder.append(format(array[i])); diff --git a/junit-jupiter/src/integTest/java/com/tngtech/test/junit/dataprovider/FormatAcceptanceTest.java b/junit-jupiter/src/integTest/java/com/tngtech/test/junit/dataprovider/FormatAcceptanceTest.java index 4caf8360..94bec564 100644 --- a/junit-jupiter/src/integTest/java/com/tngtech/test/junit/dataprovider/FormatAcceptanceTest.java +++ b/junit-jupiter/src/integTest/java/com/tngtech/test/junit/dataprovider/FormatAcceptanceTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; @@ -53,8 +54,10 @@ static Object[][] dataProviderDivide() { @TestTemplate @UseDataProvider - void testDivide(int dividend, int divisor, int result) { + void testDivide(int dividend, int divisor, int result, TestInfo testInfo) { // Expect: assertThat(dividend / divisor).isEqualTo(result); + assertThat(testInfo.getDisplayName()) + .endsWith(String.format("dividend=%d, divisor=%d, result=%d", dividend, divisor, result)); } } \ No newline at end of file diff --git a/junit-jupiter/src/main/java/com/tngtech/junit/dataprovider/placeholder/NamedArgumentPlaceholder.java b/junit-jupiter/src/main/java/com/tngtech/junit/dataprovider/placeholder/NamedArgumentPlaceholder.java index 665012c6..fa584907 100644 --- a/junit-jupiter/src/main/java/com/tngtech/junit/dataprovider/placeholder/NamedArgumentPlaceholder.java +++ b/junit-jupiter/src/main/java/com/tngtech/junit/dataprovider/placeholder/NamedArgumentPlaceholder.java @@ -37,10 +37,10 @@ protected String getReplacementFor(String placeholder, ReplacementData data) { to = from; } - List namedArguments = data.getArguments(); - from = (from >= 0) ? from : namedArguments.size() + from; - to = (to >= 0) ? to + 1 : namedArguments.size() + to + 1; - return formatAll(getSubArrayOfMethodParameters(data.getTestMethod(), from, to), namedArguments.subList(from, to)); + List arguments = data.getArguments(); + from = (from >= 0) ? from : arguments.size() + from; + to = (to >= 0) ? to + 1 : arguments.size() + to + 1; + return formatAll(getSubArrayOfMethodParameters(data.getTestMethod(), from, to), arguments.subList(from, to)); } /** @@ -56,6 +56,7 @@ protected String formatAll(Parameter[] parameters, List arguments) { for (int idx = 0; idx < arguments.size(); idx++) { String parameterName = (parameters.length > idx) ? parameters[idx].getName() : "?"; Object argument = arguments.get(idx); + stringBuilder.append(parameterName).append("=").append(format(argument)); if (idx < arguments.size() - 1) { stringBuilder.append(", "); From bbade55134fb847dfe8d12fc6abe18faaa18ba44 Mon Sep 17 00:00:00 2001 From: Andreas Schmid Date: Sun, 5 Nov 2017 19:25:44 +0100 Subject: [PATCH 5/5] move reusable subscript calculation to abstract base class (#90) --- .../AbstractArgumentPlaceholder.java | 37 +++++++++++++++++++ .../placeholder/ArgumentPlaceholder.java | 20 +--------- .../placeholder/NamedArgumentPlaceholder.java | 25 +++---------- 3 files changed, 44 insertions(+), 38 deletions(-) diff --git a/core/src/main/java/com/tngtech/junit/dataprovider/placeholder/AbstractArgumentPlaceholder.java b/core/src/main/java/com/tngtech/junit/dataprovider/placeholder/AbstractArgumentPlaceholder.java index edb1892b..a2e585b8 100644 --- a/core/src/main/java/com/tngtech/junit/dataprovider/placeholder/AbstractArgumentPlaceholder.java +++ b/core/src/main/java/com/tngtech/junit/dataprovider/placeholder/AbstractArgumentPlaceholder.java @@ -32,6 +32,15 @@ */ abstract class AbstractArgumentPlaceholder extends BasePlaceholder { + protected static final class FromAndTo { + protected final int from; + protected final int to; + protected FromAndTo(int from, int to) { + this.from = from; + this.to = to; + } + } + /** * {@link String} representation of {@code null} */ @@ -51,6 +60,34 @@ abstract class AbstractArgumentPlaceholder extends BasePlaceholder { super(placeholderRegex); } + /** + * @param placeholder containing the subscript + * @param subscriptStartIndex starting index of first subscript inner digit (to parse part within {@code []}) + * @param argumentCount used to apply the parsed subscript values and derive real {@code from} and {@code to} + * @return the wrapped {@link FromAndTo} for the subscript contained in given {@code placeholder} starting at given + * position {@code subscriptStartIndex} and applied on the given {@code argumentCount} + */ + FromAndTo calcFromAndToForSubscriptAndArguments(String placeholder, int subscriptStartIndex, + int argumentCount) { + String subscript = placeholder.substring(subscriptStartIndex, placeholder.length() - 1); + + int from = Integer.MAX_VALUE; + int to = Integer.MIN_VALUE; + if (subscript.contains("..")) { + String[] split = subscript.split("\\.\\."); + + from = Integer.parseInt(split[0]); + to = Integer.parseInt(split[1]); + } else { + from = Integer.parseInt(subscript); + to = from; + } + + from = (from >= 0) ? from : argumentCount + from; + to = (to >= 0) ? to + 1 : argumentCount + to + 1; + return new FromAndTo(from, to); + } + @SuppressFBWarnings(value = "RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE", justification = "false positive if 'param.toString()' returns 'null'") protected String format(Object param) { if (param == null) { diff --git a/core/src/main/java/com/tngtech/junit/dataprovider/placeholder/ArgumentPlaceholder.java b/core/src/main/java/com/tngtech/junit/dataprovider/placeholder/ArgumentPlaceholder.java index 5300c474..b251af8d 100644 --- a/core/src/main/java/com/tngtech/junit/dataprovider/placeholder/ArgumentPlaceholder.java +++ b/core/src/main/java/com/tngtech/junit/dataprovider/placeholder/ArgumentPlaceholder.java @@ -16,24 +16,8 @@ public ArgumentPlaceholder() { @Override protected String getReplacementFor(String placeholder, ReplacementData data) { - String subscript = placeholder.substring(3, placeholder.length() - 1); - - int from = Integer.MAX_VALUE; - int to = Integer.MIN_VALUE; - if (subscript.contains("..")) { - String[] split = subscript.split("\\.\\."); - - from = Integer.parseInt(split[0]); - to = Integer.parseInt(split[1]); - } else { - from = Integer.parseInt(subscript); - to = from; - } - - List arguments = data.getArguments(); - from = (from >= 0) ? from : arguments.size() + from; - to = (to >= 0) ? to + 1 : arguments.size() + to + 1; - return formatAll(arguments.subList(from, to)); + FromAndTo fromAndTo = calcFromAndToForSubscriptAndArguments(placeholder, 3, data.getArguments().size()); + return formatAll(data.getArguments().subList(fromAndTo.from, fromAndTo.to)); } /** diff --git a/junit-jupiter/src/main/java/com/tngtech/junit/dataprovider/placeholder/NamedArgumentPlaceholder.java b/junit-jupiter/src/main/java/com/tngtech/junit/dataprovider/placeholder/NamedArgumentPlaceholder.java index fa584907..cb91f42d 100644 --- a/junit-jupiter/src/main/java/com/tngtech/junit/dataprovider/placeholder/NamedArgumentPlaceholder.java +++ b/junit-jupiter/src/main/java/com/tngtech/junit/dataprovider/placeholder/NamedArgumentPlaceholder.java @@ -23,24 +23,9 @@ public NamedArgumentPlaceholder() { @Override protected String getReplacementFor(String placeholder, ReplacementData data) { - String subscript = placeholder.substring(4, placeholder.length() - 1); - - int from = Integer.MAX_VALUE; - int to = Integer.MIN_VALUE; - if (subscript.contains("..")) { - String[] split = subscript.split("\\.\\."); - - from = Integer.parseInt(split[0]); - to = Integer.parseInt(split[1]); - } else { - from = Integer.parseInt(subscript); - to = from; - } - - List arguments = data.getArguments(); - from = (from >= 0) ? from : arguments.size() + from; - to = (to >= 0) ? to + 1 : arguments.size() + to + 1; - return formatAll(getSubArrayOfMethodParameters(data.getTestMethod(), from, to), arguments.subList(from, to)); + FromAndTo fromAndTo = calcFromAndToForSubscriptAndArguments(placeholder, 4, data.getArguments().size()); + return formatAll(getSubArrayOfMethodParameters(data.getTestMethod(), fromAndTo), + data.getArguments().subList(fromAndTo.from, fromAndTo.to)); } /** @@ -65,7 +50,7 @@ protected String formatAll(Parameter[] parameters, List arguments) { return stringBuilder.toString(); } - private Parameter[] getSubArrayOfMethodParameters(Method testMethod, int fromIndex, int toIndex) { + private Parameter[] getSubArrayOfMethodParameters(Method testMethod, FromAndTo fromAndTo) { Parameter[] parameters = testMethod.getParameters(); if (parameters.length > 0 && !parameters[0].isNamePresent()) { logger.warning(String.format("Parameter names on method '%s' are not available" @@ -73,6 +58,6 @@ private Parameter[] getSubArrayOfMethodParameters(Method testMethod, int fromInd + ". See also https://docs.oracle.com/javase/tutorial/reflect/member/methodparameterreflection.html", testMethod)); } - return Arrays.copyOfRange(parameters, fromIndex, toIndex); + return Arrays.copyOfRange(parameters, fromAndTo.from, fromAndTo.to); } }