This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/master by this push:
new a1c408d5a Test modernization
a1c408d5a is described below
commit a1c408d5a55f81fb2449327f9a9ab4cab27fd019
Author: James Bognar <[email protected]>
AuthorDate: Thu Sep 11 12:20:50 2025 -0400
Test modernization
---
.../src/test/java/org/apache/juneau/TestUtils.java | 10 +-
.../java/org/apache/juneau/cp/BeanStore_Test.java | 8 +-
.../juneau/dto/swagger/ResponseInfo_Test.java | 2 +-
.../java/org/apache/juneau/junit/Assertions2.java | 65 +--
.../apache/juneau/junit/BasicBeanConverter.java | 165 ++----
.../juneau/junit/BasicBeanConverter_Test.java | 16 +-
.../java/org/apache/juneau/junit/Listifiers.java | 346 +++++++++++
.../org/apache/juneau/junit/Listifiers_Test.java | 411 +++++++++++++
.../apache/juneau/junit/PropertyExtractors.java | 1 +
.../juneau/junit/PropertyExtractors_Test.java | 12 +-
.../java/org/apache/juneau/junit/Stringifiers.java | 650 +++++++++++++++++++++
.../org/apache/juneau/junit/Stringifiers_Test.java | 529 +++++++++++++++++
.../java/org/apache/juneau/junit/Swappers.java | 213 +++++++
.../org/apache/juneau/junit/Swappers_Test.java | 227 +++++++
.../test/java/org/apache/juneau/junit/Utils.java | 20 +-
.../java/org/apache/juneau/junit/Utils_Test.java | 70 ++-
16 files changed, 2537 insertions(+), 208 deletions(-)
diff --git a/juneau-utest/src/test/java/org/apache/juneau/TestUtils.java
b/juneau-utest/src/test/java/org/apache/juneau/TestUtils.java
index 7344e6e1a..cf97e1d95 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/TestUtils.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/TestUtils.java
@@ -316,10 +316,6 @@ public class TestUtils extends Utils2 {
Assertions2.assertBean(actual, fields, expected);
}
- private static List<NestedTokenizer.Token> tokenize(String fields) {
- return NestedTokenizer.tokenize(fields);
- }
-
/**
* Asserts that multiple beans in a collection have the expected
property values.
*
@@ -633,10 +629,10 @@ public class TestUtils extends Utils2 {
}
/**
- * Asserts value when stringified matches the specified pattern.
+ * Asserts value when stringified matches the specified glob-style
pattern.
*/
- public static void assertMatches(String pattern, Object value) {
- Assertions2.assertMatches(pattern, value);
+ public static void assertMatchesGlob(String pattern, Object value) {
+ Assertions2.assertMatchesGlob(pattern, value);
}
/**
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/cp/BeanStore_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/cp/BeanStore_Test.java
index 0973d7097..7373aa4a4 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/cp/BeanStore_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/cp/BeanStore_Test.java
@@ -156,10 +156,10 @@ class BeanStore_Test extends SimpleTestBase {
assertList(b.stream(A2.class).map(BeanStoreEntry::get),
a2a);
}
-
assertMatches("{*,entries:[{type:'A1',bean:'"+identity(a1b)+"'},{type:'A1',bean:'"+identity(a1a)+"'}]}",
b1p);
-
assertMatches("{*,entries:[{type:'A2',bean:'"+identity(a2a)+"'}],parent:{*,entries:[{type:'A1',bean:'"+identity(a1b)+"'},{type:'A1',bean:'"+identity(a1a)+"'}]}}",
b1c);
-
assertMatches("{*,entries:[{type:'A1',bean:'"+identity(a1b)+"'},{type:'A1',bean:'"+identity(a1a)+"'}],threadSafe:true}",
b2p);
-
assertMatches("{*,entries:[{type:'A2',bean:'"+identity(a2a)+"'}],parent:{*,entries:[{type:'A1',bean:'"+identity(a1b)+"'},{type:'A1',bean:'"+identity(a1a)+"'}],threadSafe:true},threadSafe:true}",
b2c);
+
assertMatchesGlob("{*,entries:[{type:'A1',bean:'"+identity(a1b)+"'},{type:'A1',bean:'"+identity(a1a)+"'}]}",
b1p);
+
assertMatchesGlob("{*,entries:[{type:'A2',bean:'"+identity(a2a)+"'}],parent:{*,entries:[{type:'A1',bean:'"+identity(a1b)+"'},{type:'A1',bean:'"+identity(a1a)+"'}]}}",
b1c);
+
assertMatchesGlob("{*,entries:[{type:'A1',bean:'"+identity(a1b)+"'},{type:'A1',bean:'"+identity(a1a)+"'}],threadSafe:true}",
b2p);
+
assertMatchesGlob("{*,entries:[{type:'A2',bean:'"+identity(a2a)+"'}],parent:{*,entries:[{type:'A1',bean:'"+identity(a1b)+"'},{type:'A1',bean:'"+identity(a1a)+"'}],threadSafe:true},threadSafe:true}",
b2c);
b1p.removeBean(A1.class);
b1c.clear().addBean(A1.class, a1a);
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/dto/swagger/ResponseInfo_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/dto/swagger/ResponseInfo_Test.java
index 9ca403e86..386ee618c 100644
---
a/juneau-utest/src/test/java/org/apache/juneau/dto/swagger/ResponseInfo_Test.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/dto/swagger/ResponseInfo_Test.java
@@ -60,7 +60,7 @@ class ResponseInfo_Test extends SimpleTestBase {
);
assertMap(
t.setExamples(map()).addExample("text/a",
"a").addExample("text/b", null).addExample(null, "c").getExamples(),
- "text/a,text/b,<NULL>",
+ "text/a,text/b,<null>",
"a,<null>,c"
);
}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/junit/Assertions2.java
b/juneau-utest/src/test/java/org/apache/juneau/junit/Assertions2.java
index 72e8ea97f..f8c073857 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/junit/Assertions2.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/junit/Assertions2.java
@@ -870,13 +870,13 @@ public class Assertions2 {
* <h5 class='section'>Usage Examples:</h5>
* <p class='bjava'>
* <jc>// Test string length</jc>
- * assertSize(5, <js>"hello"</js>);
+ * <jsm>assertSize</jsm>(5, <js>"hello"</js>);
*
* <jc>// Test collection size</jc>
- * assertSize(3, List.<jsm>of</jsm>(<js>"a"</js>, <js>"b"</js>,
<js>"c"</js>));
+ * <jsm>assertSize</jsm>(3, List.<jsm>of</jsm>(<js>"a"</js>,
<js>"b"</js>, <js>"c"</js>));
*
* <jc>// Test array size</jc>
- * assertSize(2, <jk>new</jk> String[]{<js>"x"</js>,
<js>"y"</js>});
+ * <jsm>assertSize</jsm>(2, <jk>new</jk> String[]{<js>"x"</js>,
<js>"y"</js>});
* </p>
*
* @param expected The expected size/length.
@@ -919,23 +919,23 @@ public class Assertions2 {
* <h5 class='section'>Usage Examples:</h5>
* <p class='bjava'>
* <jc>// Test exact string conversion</jc>
- * <jsm>assertString</jsm>(<js>"John,30,true"</js>, user); <jc>//
Assuming user converts to this format</jc>
+ * <jsm>assertString</jsm>(<js>"John,30,true"</js>,
<jv>user</jv>); <jc>// Assuming user converts to this format</jc>
*
* <jc>// Test formatted dates or numbers</jc>
- * <jsm>assertString</jsm>(<js>"2023-12-01"</js>, localDate);
+ * <jsm>assertString</jsm>(<js>"2023-12-01"</js>,
<jv>localDate</jv>);
*
* <jc>// Test complex object serialization</jc>
- * <jsm>assertString</jsm>(<js>"{name=John,age=30}"</js>, userMap);
+ * <jsm>assertString</jsm>(<js>"{name=John,age=30}"</js>,
<jv>userMap</jv>);
*
* <jc>// Test array/collection formatting</jc>
- * <jsm>assertString</jsm>(<js>"[red,green,blue]"</js>, colors);
+ * <jsm>assertString</jsm>(<js>"[red,green,blue]"</js>,
<jv>colors</jv>);
* </p>
*
* @param expected The exact string that the actual object should
convert to
* @param actual The object to test. Must not be null.
* @throws AssertionError if the actual object is null or its string
representation doesn't exactly match expected
* @see #assertContains(String, Object) for partial string matching
- * @see #assertMatches(String, Object) for pattern-based matching
+ * @see #assertMatchesGlob(String, Object) for pattern-based matching
*/
public static void assertString(String expected, Object actual) {
assertString(args(), expected, actual);
@@ -958,62 +958,59 @@ public class Assertions2 {
}
/**
- * Asserts that an object's string representation matches the specified
regular expression pattern.
+ * Asserts that an object's string representation matches the specified
glob-style pattern.
*
* <p>This method converts the actual object to its string
representation using the current
- * {@link BeanConverter} and then tests it against the provided regular
expression pattern.
- * This is useful for testing string formats, validating patterns, or
checking flexible formats.</p>
+ * {@link BeanConverter} and then tests it against the provided
glob-style pattern.
+ * This is useful for testing string formats with simple wildcard
patterns.</p>
*
* <h5 class='section'>Pattern Syntax:</h5>
- * <p>The pattern parameter supports:</p>
+ * <p>The pattern uses glob-style wildcards:</p>
* <ul>
- * <li><b>Standard regex:</b> Full Java regular expression
syntax</li>
- * <li><b>Case-insensitive:</b> Patterns ending with "/i" are
automatically made case-insensitive</li>
- * <li><b>Dotall mode:</b> Patterns ending with "/s" enable dotall
mode (. matches newlines)</li>
- * <li><b>Combined flags:</b> Use "/is" for both case-insensitive
and dotall modes</li>
+ * <li><b>{@code *}</b> matches any sequence of characters
(including none)</li>
+ * <li><b>{@code ?}</b> matches exactly one character</li>
+ * <li><b>All other characters</b> are treated literally</li>
* </ul>
*
* <h5 class='section'>Usage Examples:</h5>
* <p class='bjava'>
- * <jc>// Test email format</jc>
- * <jsm>assertMatches</jsm>(<js>"\\w+@\\w+\\.\\w+"</js>,
user.getEmail());
+ * <jc>// Test filename patterns</jc>
+ * <jsm>assertMatchesGlob</jsm>(<js>"user_*_temp"</js>,
<jv>filename</jv>);
*
- * <jc>// Test case-insensitive pattern</jc>
- * <jsm>assertMatches</jsm>(<js>"error.*timeout/i"</js>,
logMessage);
+ * <jc>// Test single character wildcards</jc>
+ * <jsm>assertMatchesGlob</jsm>(<js>"file?.txt"</js>,
<jv>fileName</jv>);
*
- * <jc>// Test flexible number format</jc>
- *
<jsm>assertMatches</jsm>(<js>"\\d{1,3}(,\\d{3})*(\\.\\d{2})?"</js>,
formattedPrice);
- *
- * <jc>// Test multiline content with dotall mode</jc>
- * <jsm>assertMatches</jsm>(<js>".*error.*exception.*"</js>,
<jv>multiLineLog</jv>);
+ * <jc>// Test combined patterns</jc>
+ * <jsm>assertMatchesGlob</jsm>(<js>"log_*_?.txt"</js>,
<jv>logFile</jv>);
* </p>
*
- * @param pattern The regular expression pattern to match against.
Supports "/i" and "/s" flags.
+ * @param pattern The glob-style pattern to match against.
* @param value The object to test. Must not be null.
* @throws AssertionError if the value is null or its string
representation doesn't match the pattern
* @see #assertString(String, Object) for exact string matching
* @see #assertContains(String, Object) for substring matching
+ * @see Utils#getGlobMatchPattern(String) for pattern compilation
details
*/
- public static void assertMatches(String pattern, Object value) {
- assertMatches(args(), pattern, value);
+ public static void assertMatchesGlob(String pattern, Object value) {
+ assertMatchesGlob(args(), pattern, value);
}
/**
- * Same as {@link #assertMatches(String, Object)} but with configurable
assertion behavior.
+ * Same as {@link #assertMatchesGlob(String, Object)} but with
configurable assertion behavior.
*
* @param args Assertion configuration. See {@link #args()} for usage
examples.
- * @param pattern The regular expression pattern to match against.
+ * @param pattern The glob-style pattern to match against.
* @param value The object to test. Must not be null.
- * @see #assertMatches(String, Object)
+ * @see #assertMatchesGlob(String, Object)
* @see #args()
*/
- public static void assertMatches(AssertionArgs args, String pattern,
Object value) {
+ public static void assertMatchesGlob(AssertionArgs args, String
pattern, Object value) {
assertArgNotNull("args", args);
assertArgNotNull("pattern", pattern);
assertNotNull(value, "Value was null.");
var v =
args.getBeanConverter().orElse(DEFAULT_CONVERTER).stringify(value);
- var m = getMatchPattern(pattern).matcher(v);
- assertTrue(m.matches(), args.getMessage("Pattern didn't match.
==> pattern: <{0}> but was: <{1}>", pattern, v));
+ var m = getGlobMatchPattern(pattern).matcher(v);
+ assertTrue(m.matches(), args.getMessage("Pattern didn''t match.
==> pattern: <{0}> but was: <{1}>", pattern, v));
}
}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/junit/BasicBeanConverter.java
b/juneau-utest/src/test/java/org/apache/juneau/junit/BasicBeanConverter.java
index 5fbf6a042..423bc12cb 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/junit/BasicBeanConverter.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/junit/BasicBeanConverter.java
@@ -12,19 +12,12 @@
//
***************************************************************************************************************************
package org.apache.juneau.junit;
-import static java.util.stream.Collectors.*;
-import static java.util.stream.StreamSupport.*;
-import static java.time.format.DateTimeFormatter.*;
-import static java.util.Collections.*;
import static java.util.Optional.*;
import static org.apache.juneau.junit.Utils.*;
+import static java.util.stream.Collectors.*;
import java.io.*;
-
-import static java.util.Spliterators.*;
-
import java.lang.reflect.*;
-import java.nio.file.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.*;
@@ -98,7 +91,7 @@ import java.util.stream.*;
* <ul>
* <li><b>Optional:</b> Unwrapped to contained value or <jk>null</jk></li>
* <li><b>Supplier:</b> Called to get supplied value</li>
- * <li><b>Future:</b> Extracts completed result or returns
"<pending>" for incomplete futures</li>
+ * <li><b>Future:</b> Extracts completed result or returns
<js>"<pending>"</js> for incomplete futures (via {@link
Swappers#futureSwapper()})</li>
* </ul>
*
* <h5 class='section'>Configuration Settings:</h5>
@@ -107,6 +100,9 @@ import java.util.stream.*;
* <dt><code>nullValue</code></dt>
* <dd>String representation for null values (default:
<js>"<null>"</js>)</dd>
*
+ * <dt><code>selfValue</code></dt>
+ * <dd>Special property name that returns the object itself (default:
<js>"<self>"</js>)</dd>
+ *
* <dt><code>emptyValue</code></dt>
* <dd>String representation for empty collections (default:
<js>"<empty>"</js>)</dd>
*
@@ -160,6 +156,20 @@ import java.util.stream.*;
* <jk>var</jk> <jv>orderCount</jv> =
<jv>converter</jv>.getEntry(<jv>user</jv>, <js>"orders.length"</js>);
* </p>
*
+ * <p><b>Special Property Values:</b></p>
+ * <p class='bjava'>
+ * <jc>// Use special property names</jc>
+ * <jk>var</jk> <jv>userObj</jv> =
<jv>converter</jv>.getEntry(<jv>user</jv>, <js>"<self>"</js>); <jc>//
Returns the user object itself</jc>
+ * <jk>var</jk> <jv>nullValue</jv> =
<jv>converter</jv>.getEntry(<jv>user</jv>, <js>"<null>"</js>); <jc>//
Returns null</jc>
+ *
+ * <jc>// Custom self value</jc>
+ * <jk>var</jk> <jv>customConverter</jv> =
BasicBeanConverter.<jsm>builder</jsm>()
+ * .defaultSettings()
+ * .addSetting(<jsf>SETTING_selfValue</jsf>, <js>"this"</js>)
+ * .build();
+ * <jk>var</jk> <jv>selfRef</jv> =
<jv>customConverter</jv>.getEntry(<jv>user</jv>, <js>"this"</js>); <jc>//
Returns user object</jc>
+ * </p>
+ *
* <h5 class='section'>Performance Characteristics:</h5>
* <ul>
* <li><b>Handler Lookup:</b> O(1) average case via ConcurrentHashMap
caching</li>
@@ -206,6 +216,7 @@ public class BasicBeanConverter implements BeanConverter {
public static final String
SETTING_nullValue = "nullValue",
+ SETTING_selfValue = "selfValue",
SETTING_fieldSeparator = "fieldSeparator",
SETTING_collectionPrefix = "collectionPrefix",
SETTING_collectionSuffix = "collectionSuffix",
@@ -320,7 +331,15 @@ public class BasicBeanConverter implements BeanConverter {
// Original logic for regular property access
var pn = token.getValue();
- var e = ofNullable(getProperty(o, pn.equals("<NULL>") ? null :
pn)).orElse(null);
+ var selfValue = getSetting(SETTING_selfValue, "<self>");
+
+ // Handle special values
+ Object e;
+ if (pn.equals(selfValue)) {
+ e = o; // Return the object itself
+ } else {
+ e = ofNullable(getProperty(o, pn)).orElse(null);
+ }
if (e == null || ! token.hasNested()) return stringify(e);
return token.getNested().stream().map(x -> getNested(e,
x)).map(this::stringify).collect(joining(",","{","}"));
}
@@ -602,43 +621,33 @@ public class BasicBeanConverter implements BeanConverter {
*/
public Builder defaultSettings() {
addSetting(SETTING_nullValue, "<null>");
+ addSetting(SETTING_selfValue, "<self>");
addSetting(SETTING_classNameFormat, "simple");
- addStringifier(Map.Entry.class, (bc, o) ->
bc.stringify(o.getKey()) + bc.getSetting(SETTING_mapEntrySeparator, "=") +
bc.stringify(o.getValue()));
- addStringifier(GregorianCalendar.class, (bc, o) ->
o.toZonedDateTime().format(bc.getSetting(SETTING_calendarFormat, ISO_INSTANT)));
- addStringifier(Date.class, (bc, o) ->
o.toInstant().toString());
- addStringifier(InputStream.class, (bc, o) ->
stringify(o));
- addStringifier(byte[].class, (bc, o) -> stringify(o));
- addStringifier(Reader.class, (bc, o) -> stringify(o));
- addStringifier(File.class, (bc, o) -> stringify(o));
- addStringifier(Enum.class, (bc, o) -> o.name());
- addStringifier(Class.class, (bc, o) -> stringify(bc,
o));
- addStringifier(Constructor.class, (bc, o) ->
stringify(bc, o));
- addStringifier(Method.class, (bc, o) -> stringify(bc,
o));
- addStringifier(List.class, (bc, o) ->
((List<?>)o).stream().map(bc::stringify).collect(joining(bc.getSetting(SETTING_fieldSeparator,
","), bc.getSetting(SETTING_collectionPrefix, "["),
bc.getSetting(SETTING_collectionSuffix, "]"))));
- addStringifier(Map.class, (bc, o) ->
((Map<?,?>)o).entrySet().stream().map(bc::stringify).collect(joining(bc.getSetting(SETTING_fieldSeparator,
","), bc.getSetting(SETTING_mapPrefix, "{"), bc.getSetting(SETTING_mapSuffix,
"}"))));
-
- addListifier(Collection.class, (bc, o) -> new
ArrayList<>(o));
- addListifier(Iterable.class, (bc, o) ->
stream(((Iterable<Object>)o).spliterator(), false).toList());
- addListifier(Iterator.class, (bc, o) ->
stream(spliteratorUnknownSize(o, 0), false).toList());
- addListifier(Enumeration.class, (bc, o) -> list(o));
- addListifier(Stream.class, (bc, o) -> o.toList());
- addListifier(Optional.class, (bc, o) -> o.isEmpty() ?
emptyList() : singletonList(o.get()));
- addListifier(Map.class, (bc, o) -> new
ArrayList<>(((Map<?,?>)o).entrySet()));
-
- addSwapper(Optional.class, (bc, o) -> o.orElse(null));
- addSwapper(Supplier.class, (bc, o) -> o.get());
- addSwapper(Future.class, (bc, future) -> {
- var f = (Future<?>)future;
- if (f.isDone() && !f.isCancelled()) {
- try {
- return f.get();
- } catch (Exception e) {
- return "<error: " +
e.getMessage() + ">";
- }
- }
- return f.isCancelled() ? "<cancelled>" :
"<pending>";
- });
+ addStringifier(Map.Entry.class,
Stringifiers.mapEntryStringifier());
+ addStringifier(GregorianCalendar.class,
Stringifiers.calendarStringifier());
+ addStringifier(Date.class,
Stringifiers.dateStringifier());
+ addStringifier(InputStream.class,
Stringifiers.inputStreamStringifier());
+ addStringifier(byte[].class,
Stringifiers.byteArrayStringifier());
+ addStringifier(Reader.class,
Stringifiers.readerStringifier());
+ addStringifier(File.class,
Stringifiers.fileStringifier());
+ addStringifier(Enum.class,
Stringifiers.enumStringifier());
+ addStringifier(Class.class,
Stringifiers.classStringifier());
+ addStringifier(Constructor.class,
Stringifiers.constructorStringifier());
+ addStringifier(Method.class,
Stringifiers.methodStringifier());
+ addStringifier(List.class,
Stringifiers.listStringifier());
+ addStringifier(Map.class,
Stringifiers.mapStringifier());
+
+ addListifier(Collection.class,
Listifiers.collectionListifier());
+ addListifier(Iterable.class,
Listifiers.iterableListifier());
+ addListifier(Iterator.class,
Listifiers.iteratorListifier());
+ addListifier(Enumeration.class,
Listifiers.enumerationListifier());
+ addListifier(Stream.class,
Listifiers.streamListifier());
+ addListifier(Map.class, Listifiers.mapListifier());
+
+ addSwapper(Optional.class, Swappers.optionalSwapper());
+ addSwapper(Supplier.class, Swappers.supplierSwapper());
+ addSwapper(Future.class, Swappers.futureSwapper());
addPropertyExtractor(new
PropertyExtractors.ObjectPropertyExtractor());
addPropertyExtractor(new
PropertyExtractors.ListPropertyExtractor());
@@ -690,70 +699,4 @@ public class BasicBeanConverter implements BeanConverter {
this.function = function;
}
}
-
-
//---------------------------------------------------------------------------------------------
- // Helper methods.
-
//---------------------------------------------------------------------------------------------
-
- private static final char[] HEX = "0123456789ABCDEF".toCharArray();
-
- private static String stringify(byte[] o) {
- var sb = new StringBuilder(o.length * 2);
- for (var element : o) {
- var v = element & 0xFF;
- sb.append(HEX[v >>> 4]).append(HEX[v & 0x0F]);
- }
- return sb.toString();
- }
-
- private static String stringify(InputStream o) {
- return safe(() -> {
- try (var o2 = o) {
- var buff = new ByteArrayOutputStream(1024);
- var nRead = 0;
- var b = new byte[1024];
- while ((nRead = o2.read(b, 0, b.length)) != -1)
- buff.write(b, 0, nRead);
- buff.flush();
- return stringify(buff.toByteArray());
- }
- });
- }
-
- private static String stringify(Reader o) {
- return safe(() -> {
- try (var o2 = o) {
- var sb = new StringBuilder();
- var buf = new char[1024];
- var i = 0;
- while ((i = o2.read(buf)) != -1)
- sb.append(buf, 0, i);
- return sb.toString();
- }
- });
- }
-
- private static String stringify(File o) {
- return safe(()->stringify(Files.newBufferedReader(o.toPath())));
- }
-
- private static String stringify(BeanConverter bc, Class<?> o) {
- return switch(bc.getSetting(SETTING_classNameFormat,
"default")) {
- case "simple" -> o.getSimpleName();
- case "canonical" -> o.getCanonicalName();
- default -> o.getName();
- };
- }
-
- private static String stringify(BeanConverter bc, Constructor o) {
- return new StringBuilder().append(stringify(bc,
o.getDeclaringClass())).append('(').append(stringify(bc,
o.getParameterTypes())).append(')').toString();
- }
-
- private static String stringify(BeanConverter bc, Method o) {
- return new
StringBuilder().append(o.getName()).append('(').append(stringify(bc,
o.getParameterTypes())).append(')').toString();
- }
-
- private static String stringify(BeanConverter bc, Class[] o) {
- return Arrays.stream(o).map(x -> stringify(bc,
x)).collect(joining(","));
- }
}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/junit/BasicBeanConverter_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/junit/BasicBeanConverter_Test.java
index f48f15620..c264c621b 100644
---
a/juneau-utest/src/test/java/org/apache/juneau/junit/BasicBeanConverter_Test.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/junit/BasicBeanConverter_Test.java
@@ -27,7 +27,7 @@ import org.junit.jupiter.api.*;
* Unit tests for BasicBeanConverter.
*/
@DisplayName("BasicBeanConverter")
-public class BasicBeanConverter_Test extends TestBase {
+class BasicBeanConverter_Test extends TestBase {
//------------------------------------------------------------------------------------------------------------------
// Builder Tests
@@ -35,7 +35,7 @@ public class BasicBeanConverter_Test extends TestBase {
@Nested
@DisplayName("Builder")
- class BuilderTest {
+ class a_BuilderTest {
@Test
@DisplayName("a01_builder() creates new Builder instance")
@@ -146,7 +146,7 @@ public class BasicBeanConverter_Test extends TestBase {
@Nested
@DisplayName("Core Functionality")
- class CoreFunctionalityTest {
+ class b_CoreFunctionalityTest {
private BasicBeanConverter converter;
@@ -266,7 +266,7 @@ public class BasicBeanConverter_Test extends TestBase {
@Nested
@DisplayName("Property Access")
- class PropertyAccessTest {
+ class c_PropertyAccessTest {
private BasicBeanConverter converter;
@@ -306,7 +306,7 @@ public class BasicBeanConverter_Test extends TestBase {
@Nested
@DisplayName("Settings")
- class SettingsTest {
+ class d_SettingsTest {
@Test
@DisplayName("d01_nullValue setting changes null
representation")
@@ -390,7 +390,7 @@ public class BasicBeanConverter_Test extends TestBase {
@Nested
@DisplayName("Error Handling")
- class ErrorHandlingTest {
+ class e_ErrorHandlingTest {
private BasicBeanConverter converter;
@@ -425,7 +425,7 @@ public class BasicBeanConverter_Test extends TestBase {
public String getName() { return name; }
public int getAge() { return age; }
- public void setName(String name) { this.name = name; }
- public void setAge(int age) { this.age = age; }
+ void setName(String name) { this.name = name; }
+ void setAge(int age) { this.age = age; }
}
}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/junit/Listifiers.java
b/juneau-utest/src/test/java/org/apache/juneau/junit/Listifiers.java
new file mode 100644
index 000000000..bc4def86b
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/junit/Listifiers.java
@@ -0,0 +1,346 @@
+//
***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright
ownership. The ASF licenses this file *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance *
+// * with the License. You may obtain a copy of the License at
*
+// *
*
+// * http://www.apache.org/licenses/LICENSE-2.0
*
+// *
*
+// * Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
express or implied. See the License for the *
+// * specific language governing permissions and limitations under the
License. *
+//
***************************************************************************************************************************
+package org.apache.juneau.junit;
+
+import static java.util.Collections.*;
+import static java.util.stream.StreamSupport.*;
+import static java.util.Spliterators.*;
+
+import java.util.*;
+import java.util.stream.*;
+
+/**
+ * Collection of standard listifier implementations for the Bean-Centric
Testing framework.
+ *
+ * <p>This class provides built-in list conversion strategies that handle
common Java collection
+ * and iterable types. These listifiers are automatically registered when using
+ * {@link BasicBeanConverter.Builder#defaultSettings()}.</p>
+ *
+ * <h5 class='section'>Purpose:</h5>
+ * <p>Listifiers convert various collection-like objects to {@link List}
instances for uniform
+ * processing in BCT assertions. This enables consistent iteration and element
access across
+ * different data structures like arrays, iterators, streams, and custom
collections.</p>
+ *
+ * <h5 class='section'>Built-in Listifiers:</h5>
+ * <ul>
+ * <li><b>{@link #collectionListifier()}</b> - Converts {@link Collection}
objects to {@link ArrayList}</li>
+ * <li><b>{@link #iterableListifier()}</b> - Converts {@link Iterable}
objects using streams</li>
+ * <li><b>{@link #iteratorListifier()}</b> - Converts {@link Iterator}
objects to lists (consumes iterator)</li>
+ * <li><b>{@link #enumerationListifier()}</b> - Converts {@link
Enumeration} objects to lists</li>
+ * <li><b>{@link #streamListifier()}</b> - Converts {@link Stream} objects
to lists (terminates stream)</li>
+ * <li><b>{@link #optionalListifier()}</b> - Converts {@link Optional} to
empty or single-element list</li>
+ * <li><b>{@link #mapListifier()}</b> - Converts {@link Map} to list of
{@link Map.Entry} objects</li>
+ * </ul>
+ *
+ * <h5 class='section'>Usage Example:</h5>
+ * <p class='bjava'>
+ * <jc>// Register listifiers using builder</jc>
+ * <jk>var</jk> <jv>converter</jv> =
BasicBeanConverter.<jsm>builder</jsm>()
+ * .defaultSettings()
+ * .addListifier(Collection.<jk>class</jk>,
Listifiers.<jsm>collectionListifier</jsm>())
+ * .addListifier(Stream.<jk>class</jk>,
Listifiers.<jsm>streamListifier</jsm>())
+ * .build();
+ * </p>
+ *
+ * <h5 class='section'>Iterator Consumption:</h5>
+ * <p><b>Warning:</b> Some listifiers consume their input objects during
conversion:</p>
+ * <ul>
+ * <li><b>{@link Iterator}:</b> Elements are consumed and iterator becomes
exhausted</li>
+ * <li><b>{@link Enumeration}:</b> Elements are consumed and enumeration
becomes exhausted</li>
+ * <li><b>{@link Stream}:</b> Stream is terminated and cannot be
reused</li>
+ * </ul>
+ *
+ * <h5 class='section'>Custom Listifier Development:</h5>
+ * <p>When creating custom listifiers, follow these patterns:</p>
+ * <ul>
+ * <li><b>Null Safety:</b> Handle <jk>null</jk> inputs gracefully</li>
+ * <li><b>Immutability:</b> Return new lists rather than modifying
inputs</li>
+ * <li><b>Type Safety:</b> Ensure proper generic type handling</li>
+ * <li><b>Performance:</b> Consider memory usage for large collections</li>
+ * </ul>
+ *
+ * @see Listifier
+ * @see BasicBeanConverter.Builder#addListifier(Class, Listifier)
+ * @see BasicBeanConverter.Builder#defaultSettings()
+ */
+@SuppressWarnings({"unchecked","rawtypes"})
+public class Listifiers {
+
+ /**
+ * Constructor.
+ */
+ private Listifiers() {}
+
+ /**
+ * Returns a listifier for {@link Collection} objects that converts
them to {@link ArrayList}.
+ *
+ * <p>This listifier creates a new ArrayList containing all elements
from the source collection.
+ * It works with any Collection subtype including List, Set, Queue, and
Deque.</p>
+ *
+ * <h5 class='section'>Behavior:</h5>
+ * <ul>
+ * <li><b>Non-empty collections:</b> Returns new ArrayList with
all elements in iteration order</li>
+ * <li><b>Empty collections:</b> Returns new empty ArrayList</li>
+ * <li><b>Preserves order:</b> Maintains the iteration order of
the source collection</li>
+ * </ul>
+ *
+ * <h5 class='section'>Usage Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// Test with different collection types</jc>
+ * <jk>var</jk> <jv>list</jv> = List.<jsm>of</jsm>(<js>"a"</js>,
<js>"b"</js>, <js>"c"</js>);
+ * <jsm>assertList</jsm>(<jv>list</jv>, <js>"a"</js>,
<js>"b"</js>, <js>"c"</js>);
+ *
+ * <jk>var</jk> <jv>set</jv> = Set.<jsm>of</jsm>(<js>"x"</js>,
<js>"y"</js>);
+ * <jsm>assertList</jsm>(<jv>set</jv>, <js>"x"</js>,
<js>"y"</js>); <jc>// Order may vary</jc>
+ *
+ * <jk>var</jk> <jv>queue</jv> = <jk>new</jk>
LinkedList<>(Arrays.<jsm>asList</jsm>(<js>"first"</js>,
<js>"second"</js>));
+ * <jsm>assertList</jsm>(<jv>queue</jv>, <js>"first"</js>,
<js>"second"</js>);
+ * </p>
+ *
+ * <h5 class='section'>Performance:</h5>
+ * <p>This listifier creates a new ArrayList and copies all elements,
so it has O(n) time
+ * and space complexity. For large collections, consider the memory
implications.</p>
+ *
+ * @return A {@link Listifier} for {@link Collection} objects
+ * @see Collection
+ * @see ArrayList
+ */
+ public static Listifier<Collection> collectionListifier() {
+ return (bc, collection) -> new ArrayList<>(collection);
+ }
+
+ /**
+ * Returns a listifier for {@link Iterable} objects that converts them
using streams.
+ *
+ * <p>This listifier handles any Iterable implementation by creating a
stream from its
+ * spliterator and collecting elements to a list. It works with custom
iterables and
+ * collection types not covered by more specific listifiers.</p>
+ *
+ * <h5 class='section'>Behavior:</h5>
+ * <ul>
+ * <li><b>Standard iterables:</b> Converts elements to list
maintaining iteration order</li>
+ * <li><b>Custom iterables:</b> Works with any object implementing
Iterable interface</li>
+ * <li><b>Large iterables:</b> Processes all elements regardless
of size</li>
+ * </ul>
+ *
+ * <h5 class='section'>Usage Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// Test with custom iterable</jc>
+ * <jk>var</jk> <jv>range</jv> =
IntStream.<jsm>range</jsm>(<jv>1</jv>,
<jv>4</jv>).<jsm>boxed</jsm>().<jsm>collect</jsm>(toList());
+ * <jsm>assertList</jsm>(<jv>range</jv>, <jv>1</jv>, <jv>2</jv>,
<jv>3</jv>);
+ *
+ * <jc>// Test with custom Iterable implementation</jc>
+ * <jk>var</jk> <jv>fibonacci</jv> = <jk>new</jk>
FibonacciIterable(<jv>5</jv>);
+ * <jsm>assertList</jsm>(<jv>fibonacci</jv>, <jv>1</jv>,
<jv>1</jv>, <jv>2</jv>, <jv>3</jv>, <jv>5</jv>);
+ * </p>
+ *
+ * @return A {@link Listifier} for {@link Iterable} objects
+ * @see Iterable
+ */
+ public static Listifier<Iterable> iterableListifier() {
+ return (bc, iterable) -> stream(iterable.spliterator(),
false).toList();
+ }
+
+ /**
+ * Returns a listifier for {@link Iterator} objects that converts them
to lists.
+ *
+ * <p><b>Warning:</b> This listifier consumes the iterator during
conversion. After listification,
+ * the iterator will be exhausted and cannot be used again.</p>
+ *
+ * <h5 class='section'>Behavior:</h5>
+ * <ul>
+ * <li><b>Element extraction:</b> Consumes all remaining elements
from the iterator</li>
+ * <li><b>Order preservation:</b> Maintains the iterator's order
in the resulting list</li>
+ * <li><b>Iterator exhaustion:</b> The iterator becomes unusable
after conversion</li>
+ * </ul>
+ *
+ * <h5 class='section'>Usage Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// Test with list iterator</jc>
+ * <jk>var</jk> <jv>list</jv> = List.<jsm>of</jsm>(<js>"a"</js>,
<js>"b"</js>, <js>"c"</js>);
+ * <jk>var</jk> <jv>iterator</jv> = <jv>list</jv>.iterator();
+ * <jsm>assertList</jsm>(<jv>iterator</jv>, <js>"a"</js>,
<js>"b"</js>, <js>"c"</js>);
+ * <jc>// iterator is now exhausted</jc>
+ *
+ * <jc>// Test with custom iterator</jc>
+ * <jk>var</jk> <jv>numbers</jv> =
IntStream.<jsm>range</jsm>(<jv>1</jv>, <jv>4</jv>).iterator();
+ * <jsm>assertList</jsm>(<jv>numbers</jv>, <jv>1</jv>, <jv>2</jv>,
<jv>3</jv>);
+ * </p>
+ *
+ * <h5 class='section'>Important Notes:</h5>
+ * <ul>
+ * <li><b>One-time use:</b> The iterator becomes exhausted and
unusable after conversion</li>
+ * <li><b>Side effects:</b> Any side effects in the iterator's
next() method will occur</li>
+ * <li><b>Exception handling:</b> Exceptions from the iterator are
propagated</li>
+ * </ul>
+ *
+ * @return A {@link Listifier} for {@link Iterator} objects
+ * @see Iterator
+ */
+ public static Listifier<Iterator> iteratorListifier() {
+ return (bc, iterator) ->
stream(spliteratorUnknownSize(iterator, 0), false).toList();
+ }
+
+ /**
+ * Returns a listifier for {@link Enumeration} objects that converts
them to lists.
+ *
+ * <p><b>Warning:</b> This listifier consumes the enumeration during
conversion. After listification,
+ * the enumeration will be exhausted and cannot be used again.</p>
+ *
+ * <h5 class='section'>Behavior:</h5>
+ * <ul>
+ * <li><b>Element extraction:</b> Consumes all remaining elements
from the enumeration</li>
+ * <li><b>Order preservation:</b> Maintains the enumeration's
order in the resulting list</li>
+ * <li><b>Enumeration exhaustion:</b> The enumeration becomes
unusable after conversion</li>
+ * </ul>
+ *
+ * <h5 class='section'>Usage Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// Test with Vector enumeration</jc>
+ * <jk>var</jk> <jv>vector</jv> = <jk>new</jk>
Vector<>(List.<jsm>of</jsm>(<js>"x"</js>, <js>"y"</js>, <js>"z"</js>));
+ * <jk>var</jk> <jv>enumeration</jv> = <jv>vector</jv>.elements();
+ * <jsm>assertList</jsm>(<jv>enumeration</jv>, <js>"x"</js>,
<js>"y"</js>, <js>"z"</js>);
+ *
+ * <jc>// Test with Hashtable enumeration</jc>
+ * <jk>var</jk> <jv>table</jv> = <jk>new</jk>
Hashtable<>(Map.<jsm>of</jsm>(<js>"key1"</js>, <js>"value1"</js>));
+ * <jk>var</jk> <jv>keys</jv> = <jv>table</jv>.keys();
+ * <jsm>assertList</jsm>(<jv>keys</jv>, <js>"key1"</js>);
+ * </p>
+ *
+ * @return A {@link Listifier} for {@link Enumeration} objects
+ * @see Enumeration
+ */
+ public static Listifier<Enumeration> enumerationListifier() {
+ return (bc, enumeration) -> list(enumeration);
+ }
+
+ /**
+ * Returns a listifier for {@link Stream} objects that converts them to
lists.
+ *
+ * <p><b>Warning:</b> This listifier terminates the stream during
conversion. After listification,
+ * the stream is closed and cannot be used again.</p>
+ *
+ * <h5 class='section'>Behavior:</h5>
+ * <ul>
+ * <li><b>Stream termination:</b> Calls toList() to collect all
stream elements</li>
+ * <li><b>Order preservation:</b> Maintains stream order in the
resulting list</li>
+ * <li><b>Stream closure:</b> The stream becomes unusable after
conversion</li>
+ * </ul>
+ *
+ * <h5 class='section'>Usage Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// Test with filtered stream</jc>
+ * <jk>var</jk> <jv>numbers</jv> =
IntStream.<jsm>range</jsm>(<jv>1</jv>, <jv>10</jv>)
+ * .filter(<jv>n</jv> -> <jv>n</jv> % <jv>2</jv> ==
<jv>0</jv>)
+ * .boxed();
+ * <jsm>assertList</jsm>(<jv>numbers</jv>, <jv>2</jv>, <jv>4</jv>,
<jv>6</jv>, <jv>8</jv>);
+ *
+ * <jc>// Test with mapped stream</jc>
+ * <jk>var</jk> <jv>words</jv> =
Stream.<jsm>of</jsm>(<js>"hello"</js>, <js>"world"</js>)
+ * .map(String::toUpperCase);
+ * <jsm>assertList</jsm>(<jv>words</jv>, <js>"HELLO"</js>,
<js>"WORLD"</js>);
+ * </p>
+ *
+ * <h5 class='section'>Important Notes:</h5>
+ * <ul>
+ * <li><b>One-time use:</b> The stream is terminated and cannot be
reused</li>
+ * <li><b>Lazy evaluation:</b> Stream operations are executed
during listification</li>
+ * <li><b>Exception handling:</b> Stream operation exceptions are
propagated</li>
+ * </ul>
+ *
+ * @return A {@link Listifier} for {@link Stream} objects
+ * @see Stream
+ */
+ public static Listifier<Stream> streamListifier() {
+ return (bc, stream) -> stream.toList();
+ }
+
+ /**
+ * Returns a listifier for {@link Optional} objects that converts them
to single-element or empty lists.
+ *
+ * <p>This listifier enables Optional values to be processed as lists
in BCT assertions,
+ * making it easy to test Optional-wrapped collections and values
uniformly.</p>
+ *
+ * <h5 class='section'>Behavior:</h5>
+ * <ul>
+ * <li><b>Present value:</b> Returns single-element list
containing the Optional's value</li>
+ * <li><b>Empty Optional:</b> Returns empty list</li>
+ * <li><b>Type preservation:</b> The list element type matches the
Optional's type parameter</li>
+ * </ul>
+ *
+ * <h5 class='section'>Usage Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// Test Optional with value</jc>
+ * <jk>var</jk> <jv>optional</jv> =
Optional.<jsm>of</jsm>(<js>"Hello"</js>);
+ * <jsm>assertList</jsm>(<jv>optional</jv>, <js>"Hello"</js>);
+ *
+ * <jc>// Test empty Optional</jc>
+ * <jk>var</jk> <jv>empty</jv> = Optional.<jsm>empty</jsm>();
+ * <jsm>assertEmpty</jsm>(<jv>empty</jv>);
+ *
+ * <jc>// Test Optional in collection context</jc>
+ * <jk>var</jk> <jv>user</jv> = <jk>new</jk>
User().setNickname(Optional.<jsm>of</jsm>(<js>"Johnny"</js>));
+ * <jsm>assertList</jsm>(<jv>user</jv>, <js>"nickname"</js>,
<js>"Johnny"</js>);
+ * </p>
+ *
+ * <h5 class='section'>Integration:</h5>
+ * <p>This listifier is particularly useful when working with APIs that
return Optional
+ * collections or when testing Optional values in a list context.</p>
+ *
+ * @return A {@link Listifier} for {@link Optional} objects
+ * @see Optional
+ */
+ public static Listifier<Optional> optionalListifier() {
+ return (bc, optional) -> optional.isEmpty() ? emptyList() :
singletonList(optional.get());
+ }
+
+ /**
+ * Returns a listifier for {@link Map} objects that converts them to
lists of {@link Map.Entry} objects.
+ *
+ * <p>This listifier enables maps to be processed as lists in BCT
assertions, making it easy
+ * to test map contents using list-based assertion methods.</p>
+ *
+ * <h5 class='section'>Behavior:</h5>
+ * <ul>
+ * <li><b>Entry conversion:</b> Each key-value pair becomes a
Map.Entry in the list</li>
+ * <li><b>Order preservation:</b> Maintains the map's iteration
order</li>
+ * <li><b>Empty maps:</b> Returns empty list for empty maps</li>
+ * </ul>
+ *
+ * <h5 class='section'>Usage Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// Test map contents</jc>
+ * <jk>var</jk> <jv>map</jv> = Map.<jsm>of</jsm>(<js>"key1"</js>,
<js>"value1"</js>, <js>"key2"</js>, <js>"value2"</js>);
+ * <jsm>assertSize</jsm>(<jv>map</jv>, <jv>2</jv>);
+ *
+ * <jc>// Test empty map</jc>
+ * <jk>var</jk> <jv>emptyMap</jv> = Map.<jsm>of</jsm>();
+ * <jsm>assertEmpty</jsm>(<jv>emptyMap</jv>);
+ *
+ * <jc>// Test map in object property</jc>
+ * <jk>var</jk> <jv>config</jv> = <jk>new</jk>
Configuration().setProperties(<jv>map</jv>);
+ * <jsm>assertSize</jsm>(<jv>config</jv>, <js>"properties"</js>,
<jv>2</jv>);
+ * </p>
+ *
+ * <h5 class='section'>Entry Processing:</h5>
+ * <p>The resulting Map.Entry objects can be further processed by other
parts of the
+ * conversion system, typically being stringified to
<js>"key=value"</js> format.</p>
+ *
+ * @return A {@link Listifier} for {@link Map} objects
+ * @see Map
+ * @see Map.Entry
+ */
+ public static Listifier<Map> mapListifier() {
+ return (bc, map) -> new ArrayList<>(map.entrySet());
+ }
+}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/junit/Listifiers_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/junit/Listifiers_Test.java
new file mode 100644
index 000000000..6c4260d1f
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/junit/Listifiers_Test.java
@@ -0,0 +1,411 @@
+//
***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright
ownership. The ASF licenses this file *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance *
+// * with the License. You may obtain a copy of the License at
*
+// *
*
+// * http://www.apache.org/licenses/LICENSE-2.0
*
+// *
*
+// * Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
express or implied. See the License for the *
+// * specific language governing permissions and limitations under the
License. *
+//
***************************************************************************************************************************
+package org.apache.juneau.junit;
+
+import static org.apache.juneau.junit.Assertions2.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.*;
+import java.util.stream.*;
+
+import org.junit.jupiter.api.*;
+
+/**
+ * Unit tests for {@link Listifiers}.
+ */
+class Listifiers_Test extends TestBase {
+
+ @Nested
+ class a_CollectionListifier {
+
+ @Test
+ void a01_listifyList() {
+ var listifier = Listifiers.collectionListifier();
+ var input = List.of("a", "b", "c");
+ var result = listifier.apply(null, input);
+
+ assertNotSame(input, result); // Should create new list
+ assertList(result, "a", "b", "c");
+ }
+
+ @Test
+ void a02_listifySet() {
+ var listifier = Listifiers.collectionListifier();
+ var input = Set.of("x", "y", "z");
+ var result = listifier.apply(null, input);
+
+ assertEquals(3, result.size());
+ assertTrue(result.contains("x"));
+ assertTrue(result.contains("y"));
+ assertTrue(result.contains("z"));
+ }
+
+ @Test
+ void a03_listifyQueue() {
+ var listifier = Listifiers.collectionListifier();
+ var input = new LinkedList<>(List.of("first",
"second"));
+ var result = listifier.apply(null, input);
+
+ assertList(result, "first", "second");
+ }
+
+ @Test
+ void a04_listifyEmptyCollection() {
+ var listifier = Listifiers.collectionListifier();
+ var input = List.of();
+ var result = listifier.apply(null, input);
+
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ void a05_listifyWithConverter() {
+ var converter =
BasicBeanConverter.builder().defaultSettings().build();
+ var listifier = Listifiers.collectionListifier();
+ var input = List.of(1, 2, 3);
+ var result = listifier.apply(converter, input);
+
+ assertList(result, 1, 2, 3);
+ }
+ }
+
+ @Nested
+ class b_IterableListifier {
+
+ @Test
+ void b01_listifyIterable() {
+ var listifier = Listifiers.iterableListifier();
+ Iterable<String> input = List.of("a", "b", "c");
+ var result = listifier.apply(null, input);
+
+ assertList(result, "a", "b", "c");
+ }
+
+ @Test
+ void b02_listifyCustomIterable() {
+ var listifier = Listifiers.iterableListifier();
+ // Create a simple custom iterable
+ Iterable<Integer> input = () -> IntStream.range(1,
4).iterator();
+ var result = listifier.apply(null, input);
+
+ assertList(result, 1, 2, 3);
+ }
+
+ @Test
+ void b03_listifyEmptyIterable() {
+ var listifier = Listifiers.iterableListifier();
+ Iterable<String> input = List.of();
+ var result = listifier.apply(null, input);
+
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ void b04_listifyWithConverter() {
+ var converter =
BasicBeanConverter.builder().defaultSettings().build();
+ var listifier = Listifiers.iterableListifier();
+ Iterable<String> input = Set.of("x", "y");
+ var result = listifier.apply(converter, input);
+
+ assertEquals(2, result.size());
+ assertTrue(result.contains("x"));
+ assertTrue(result.contains("y"));
+ }
+ }
+
+ @Nested
+ class c_IteratorListifier {
+
+ @Test
+ void c01_listifyIterator() {
+ var listifier = Listifiers.iteratorListifier();
+ var input = List.of("a", "b", "c").iterator();
+ var result = listifier.apply(null, input);
+
+ assertList(result, "a", "b", "c");
+ // Iterator should be exhausted
+ assertFalse(input.hasNext());
+ }
+
+ @Test
+ void c02_listifyEmptyIterator() {
+ var listifier = Listifiers.iteratorListifier();
+ var input = List.of().iterator();
+ var result = listifier.apply(null, input);
+
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ void c03_listifyLargeIterator() {
+ var listifier = Listifiers.iteratorListifier();
+ var input = IntStream.range(0, 1000).iterator();
+ var result = listifier.apply(null, input);
+
+ assertEquals(1000, result.size());
+ assertEquals(0, result.get(0));
+ assertEquals(999, result.get(999));
+ }
+
+ @Test
+ void c04_listifyWithConverter() {
+ var converter =
BasicBeanConverter.builder().defaultSettings().build();
+ var listifier = Listifiers.iteratorListifier();
+ var input = List.of("x", "y", "z").iterator();
+ var result = listifier.apply(converter, input);
+
+ assertList(result, "x", "y", "z");
+ }
+ }
+
+ @Nested
+ class d_EnumerationListifier {
+
+ @Test
+ void d01_listifyEnumeration() {
+ var listifier = Listifiers.enumerationListifier();
+ var vector = new Vector<>(List.of("a", "b", "c"));
+ var input = vector.elements();
+ var result = listifier.apply(null, input);
+
+ assertList(result, "a", "b", "c");
+ // Enumeration should be exhausted
+ assertFalse(input.hasMoreElements());
+ }
+
+ @Test
+ void d02_listifyEmptyEnumeration() {
+ var listifier = Listifiers.enumerationListifier();
+ var vector = new Vector<String>();
+ var input = vector.elements();
+ var result = listifier.apply(null, input);
+
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ void d03_listifyHashtableKeys() {
+ var listifier = Listifiers.enumerationListifier();
+ var hashtable = new Hashtable<String, String>();
+ hashtable.put("key1", "value1");
+ hashtable.put("key2", "value2");
+ var input = hashtable.keys();
+ var result = listifier.apply(null, input);
+
+ assertEquals(2, result.size());
+ assertTrue(result.contains("key1"));
+ assertTrue(result.contains("key2"));
+ }
+
+ @Test
+ void d04_listifyWithConverter() {
+ var converter =
BasicBeanConverter.builder().defaultSettings().build();
+ var listifier = Listifiers.enumerationListifier();
+ var vector = new Vector<>(List.of(1, 2, 3));
+ var input = vector.elements();
+ var result = listifier.apply(converter, input);
+
+ assertList(result, 1, 2, 3);
+ }
+ }
+
+ @Nested
+ class e_StreamListifier {
+
+ @Test
+ void e01_listifyStream() {
+ var listifier = Listifiers.streamListifier();
+ var input = Stream.of("a", "b", "c");
+ var result = listifier.apply(null, input);
+
+ assertList(result, "a", "b", "c");
+ }
+
+ @Test
+ void e02_listifyEmptyStream() {
+ var listifier = Listifiers.streamListifier();
+ var input = Stream.empty();
+ var result = listifier.apply(null, input);
+
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ void e03_listifyFilteredStream() {
+ var listifier = Listifiers.streamListifier();
+ var input = IntStream.range(1, 10)
+ .filter(n -> n % 2 == 0)
+ .boxed();
+ var result = listifier.apply(null, input);
+
+ assertList(result, 2, 4, 6, 8);
+ }
+
+ @Test
+ void e04_listifyMappedStream() {
+ var listifier = Listifiers.streamListifier();
+ var input = Stream.of("hello", "world")
+ .map(String::toUpperCase);
+ var result = listifier.apply(null, input);
+
+ assertList(result, "HELLO", "WORLD");
+ }
+
+ @Test
+ void e05_listifyWithConverter() {
+ var converter =
BasicBeanConverter.builder().defaultSettings().build();
+ var listifier = Listifiers.streamListifier();
+ var input = Stream.of(1, 2, 3);
+ var result = listifier.apply(converter, input);
+
+ assertList(result, 1, 2, 3);
+ }
+ }
+
+ @Nested
+ class f_OptionalListifier {
+
+ @Test
+ void f01_listifyPresentOptional() {
+ var listifier = Listifiers.optionalListifier();
+ var input = Optional.of("hello");
+ var result = listifier.apply(null, input);
+
+ assertList(result, "hello");
+ }
+
+ @Test
+ void f02_listifyEmptyOptional() {
+ var listifier = Listifiers.optionalListifier();
+ var input = Optional.empty();
+ var result = listifier.apply(null, input);
+
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ void f03_listifyOptionalWithNull() {
+ var listifier = Listifiers.optionalListifier();
+ var input = Optional.ofNullable(null);
+ var result = listifier.apply(null, input);
+
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ void f04_listifyOptionalWithComplexObject() {
+ var listifier = Listifiers.optionalListifier();
+ var complexObject = Map.of("key", "value");
+ var input = Optional.of(complexObject);
+ var result = listifier.apply(null, input);
+
+ assertEquals(1, result.size());
+ assertSame(complexObject, result.get(0));
+ }
+
+ @Test
+ void f05_listifyWithConverter() {
+ var converter =
BasicBeanConverter.builder().defaultSettings().build();
+ var listifier = Listifiers.optionalListifier();
+ var input = Optional.of(42);
+ var result = listifier.apply(converter, input);
+
+ assertList(result, 42);
+ }
+ }
+
+ @Nested
+ class g_MapListifier {
+
+ @Test
+ void g01_listifyMap() {
+ var listifier = Listifiers.mapListifier();
+ var input = Map.of("key1", "value1", "key2", "value2");
+ var result = listifier.apply(null, input);
+
+ assertEquals(2, result.size());
+ // Result should contain Map.Entry objects
+ assertTrue(result.stream().allMatch(obj -> obj
instanceof Map.Entry));
+ }
+
+ @Test
+ void g02_listifyEmptyMap() {
+ var listifier = Listifiers.mapListifier();
+ var input = Map.of();
+ var result = listifier.apply(null, input);
+
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ void g03_listifyMapWithNullValues() {
+ var listifier = Listifiers.mapListifier();
+ var input = new HashMap<String, String>();
+ input.put("key1", "value1");
+ input.put("key2", null);
+ var result = listifier.apply(null, input);
+
+ assertEquals(2, result.size());
+ // Check that we have the expected entries
+ var hasKey1Entry = result.stream()
+ .filter(obj -> obj instanceof Map.Entry)
+ .map(obj -> (Map.Entry<?, ?>) obj)
+ .anyMatch(entry ->
"key1".equals(entry.getKey()) && "value1".equals(entry.getValue()));
+ assertTrue(hasKey1Entry);
+
+ var hasKey2Entry = result.stream()
+ .filter(obj -> obj instanceof Map.Entry)
+ .map(obj -> (Map.Entry<?, ?>) obj)
+ .anyMatch(entry ->
"key2".equals(entry.getKey()) && entry.getValue() == null);
+ assertTrue(hasKey2Entry);
+ }
+
+ @Test
+ void g04_listifyWithConverter() {
+ var converter =
BasicBeanConverter.builder().defaultSettings().build();
+ var listifier = Listifiers.mapListifier();
+ var input = Map.of("a", 1, "b", 2);
+ var result = listifier.apply(converter, input);
+
+ assertEquals(2, result.size());
+ assertTrue(result.stream().allMatch(obj -> obj
instanceof Map.Entry));
+ }
+ }
+
+ @Nested
+ class h_Integration {
+
+ @Test
+ void h01_useInBasicBeanConverter() {
+ // Test various listifiable objects
+ assertList(List.of("a", "b"), "a", "b");
+ assertList(new LinkedHashSet<>(Arrays.asList("x",
"y")), "x", "y");
+ assertSize(3, Stream.of(1, 2, 3));
+ assertEmpty(Optional.empty());
+ }
+
+ @Test
+ void h02_customListifierRegistration() {
+ // Test that custom registration works
+ assertList(List.of("custom"), "custom");
+ }
+
+ @Test
+ void h03_listifierChaining() {
+ // Test that listified objects can be processed by
other listifiers
+ // Stream of optionals
+ var streamOfOptionals = Stream.of(Optional.of("a"),
Optional.empty(), Optional.of("b"));
+ assertSize(3, streamOfOptionals);
+ }
+ }
+}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/junit/PropertyExtractors.java
b/juneau-utest/src/test/java/org/apache/juneau/junit/PropertyExtractors.java
index ca8127af9..189611128 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/junit/PropertyExtractors.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/junit/PropertyExtractors.java
@@ -247,6 +247,7 @@ public class PropertyExtractors {
@Override
public Object extract(BeanConverter converter, Object o, String
name) {
var m = (Map<?,?>)o;
+ if (eq(name,
converter.getSetting(BasicBeanConverter.SETTING_nullValue, "<null>"))) name =
null;
if (m.containsKey(name)) return m.get(name);
if ("size".equals(name)) return m.size();
return super.extract(converter, o, name);
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/junit/PropertyExtractors_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/junit/PropertyExtractors_Test.java
index e0811338d..637cbe68a 100644
---
a/juneau-utest/src/test/java/org/apache/juneau/junit/PropertyExtractors_Test.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/junit/PropertyExtractors_Test.java
@@ -39,7 +39,7 @@ class PropertyExtractors_Test extends TestBase {
@Nested
@DisplayName("ObjectPropertyExtractor")
- class ObjectPropertyExtractorTest {
+ class a_ObjectPropertyExtractorTest {
private PropertyExtractors.ObjectPropertyExtractor extractor =
new PropertyExtractors.ObjectPropertyExtractor();
@@ -129,7 +129,7 @@ class PropertyExtractors_Test extends TestBase {
@Nested
@DisplayName("ListPropertyExtractor")
- class ListPropertyExtractorTest {
+ class b_ListPropertyExtractorTest {
private PropertyExtractors.ListPropertyExtractor extractor =
new PropertyExtractors.ListPropertyExtractor();
@@ -208,7 +208,7 @@ class PropertyExtractors_Test extends TestBase {
@Nested
@DisplayName("MapPropertyExtractor")
- class MapPropertyExtractorTest {
+ class c_MapPropertyExtractorTest {
private PropertyExtractors.MapPropertyExtractor extractor = new
PropertyExtractors.MapPropertyExtractor();
@@ -316,9 +316,9 @@ class PropertyExtractors_Test extends TestBase {
public int getAge() { return age; }
public boolean isActive() { return active; }
- public void setName(String name) { this.name = name; }
- public void setAge(int age) { this.age = age; }
- public void setActive(boolean active) { this.active = active; }
+ void setName(String name) { this.name = name; }
+ void setAge(int age) { this.age = age; }
+ void setActive(boolean active) { this.active = active; }
}
public static class TestBeanWithFields {
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/junit/Stringifiers.java
b/juneau-utest/src/test/java/org/apache/juneau/junit/Stringifiers.java
new file mode 100644
index 000000000..46b8714f8
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/junit/Stringifiers.java
@@ -0,0 +1,650 @@
+//
***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright
ownership. The ASF licenses this file *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance *
+// * with the License. You may obtain a copy of the License at
*
+// *
*
+// * http://www.apache.org/licenses/LICENSE-2.0
*
+// *
*
+// * Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
express or implied. See the License for the *
+// * specific language governing permissions and limitations under the
License. *
+//
***************************************************************************************************************************
+package org.apache.juneau.junit;
+
+import static java.time.format.DateTimeFormatter.*;
+import static java.util.stream.Collectors.*;
+import static org.apache.juneau.junit.Utils.*;
+
+import java.io.*;
+import java.lang.reflect.*;
+import java.nio.file.*;
+import java.util.*;
+
+/**
+ * Collection of standard stringifier implementations for the Bean-Centric
Testing framework.
+ *
+ * <p>This class provides built-in string conversion strategies that handle
common Java types
+ * and objects. These stringifiers are automatically registered when using
+ * {@link BasicBeanConverter.Builder#defaultSettings()}.</p>
+ *
+ * <h5 class='section'>Purpose:</h5>
+ * <p>Stringifiers convert objects to human-readable string representations
for use in BCT
+ * assertions and test output. They provide consistent, meaningful string
formats across
+ * different object types while supporting customization for specific testing
needs.</p>
+ *
+ * <h5 class='section'>Built-in Stringifiers:</h5>
+ * <ul>
+ * <li><b>{@link #mapEntryStringifier()}</b> - Converts {@link Map.Entry}
to <js>"key=value"</js> format</li>
+ * <li><b>{@link #calendarStringifier()}</b> - Converts {@link
GregorianCalendar} to ISO-8601 format</li>
+ * <li><b>{@link #dateStringifier()}</b> - Converts {@link Date} to ISO
instant format</li>
+ * <li><b>{@link #inputStreamStringifier()}</b> - Converts {@link
InputStream} content to hex strings</li>
+ * <li><b>{@link #byteArrayStringifier()}</b> - Converts byte arrays to
hex strings</li>
+ * <li><b>{@link #readerStringifier()}</b> - Converts {@link Reader}
content to strings</li>
+ * <li><b>{@link #fileStringifier()}</b> - Converts {@link File} content
to strings</li>
+ * <li><b>{@link #enumStringifier()}</b> - Converts {@link Enum} values to
name format</li>
+ * <li><b>{@link #classStringifier()}</b> - Converts {@link Class} objects
to name format</li>
+ * <li><b>{@link #constructorStringifier()}</b> - Converts {@link
Constructor} to signature format</li>
+ * <li><b>{@link #methodStringifier()}</b> - Converts {@link Method} to
signature format</li>
+ * <li><b>{@link #listStringifier()}</b> - Converts {@link List} to
bracket-delimited format</li>
+ * <li><b>{@link #mapStringifier()}</b> - Converts {@link Map} to
brace-delimited format</li>
+ * </ul>
+ *
+ * <h5 class='section'>Usage Example:</h5>
+ * <p class='bjava'>
+ * <jc>// Register stringifiers using builder</jc>
+ * <jk>var</jk> <jv>converter</jv> =
BasicBeanConverter.<jsm>builder</jsm>()
+ * .defaultSettings()
+ * .addStringifier(Date.<jk>class</jk>,
Stringifiers.<jsm>dateStringifier</jsm>())
+ * .addStringifier(File.<jk>class</jk>,
Stringifiers.<jsm>fileStringifier</jsm>())
+ * .build();
+ * </p>
+ *
+ * <h5 class='section'>Resource Handling:</h5>
+ * <p><b>Warning:</b> Some stringifiers consume or close their input
resources:</p>
+ * <ul>
+ * <li><b>{@link InputStream}:</b> Stream is consumed and closed during
stringification</li>
+ * <li><b>{@link Reader}:</b> Reader is consumed and closed during
stringification</li>
+ * <li><b>{@link File}:</b> File content is read completely during
stringification</li>
+ * </ul>
+ *
+ * <h5 class='section'>Custom Stringifier Development:</h5>
+ * <p>When creating custom stringifiers, follow these patterns:</p>
+ * <ul>
+ * <li><b>Null Safety:</b> Handle <jk>null</jk> inputs gracefully</li>
+ * <li><b>Resource Management:</b> Properly close resources after use</li>
+ * <li><b>Exception Handling:</b> Convert exceptions to meaningful error
messages</li>
+ * <li><b>Performance:</b> Consider string building efficiency for complex
objects</li>
+ * <li><b>Readability:</b> Ensure output is useful for debugging and
assertions</li>
+ * </ul>
+ *
+ * @see Stringifier
+ * @see BasicBeanConverter.Builder#addStringifier(Class, Stringifier)
+ * @see BasicBeanConverter.Builder#defaultSettings()
+ */
+@SuppressWarnings({"rawtypes"})
+public class Stringifiers {
+
+ private static final char[] HEX = "0123456789ABCDEF".toCharArray();
+
+ /**
+ * Constructor.
+ */
+ private Stringifiers() {}
+
+ /**
+ * Returns a stringifier for {@link Map.Entry} objects that formats
them as <js>"key=value"</js>.
+ *
+ * <p>This stringifier creates a human-readable representation of map
entries by converting
+ * both the key and value to strings and joining them with the
configured entry separator.</p>
+ *
+ * <h5 class='section'>Behavior:</h5>
+ * <ul>
+ * <li><b>Format:</b> Uses the pattern
<js>"{key}{separator}{value}"</js></li>
+ * <li><b>Separator:</b> Uses the {@code mapEntrySeparator}
setting (default: <js>"="</js>)</li>
+ * <li><b>Recursive conversion:</b> Both key and value are
converted using the same converter</li>
+ * </ul>
+ *
+ * <h5 class='section'>Usage Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// Test map entry stringification</jc>
+ * <jk>var</jk> <jv>entry</jv> =
Map.<jsm>entry</jsm>(<js>"name"</js>, <js>"John"</js>);
+ * <jsm>assertBean</jsm>(<jv>entry</jv>, <js>"<self>"</js>,
<js>"name=John"</js>);
+ *
+ * <jc>// Test with custom separator</jc>
+ * <jk>var</jk> <jv>converter</jv> =
BasicBeanConverter.<jsm>builder</jsm>()
+ * .defaultSettings()
+ * .addSetting(<jsf>SETTING_mapEntrySeparator</jsf>,
<js>": "</js>)
+ * .build();
+ * <jsm>assertBean</jsm>(<jv>entry</jv>, <js>"<self>"</js>,
<js>"name: John"</js>);
+ * </p>
+ *
+ * @return A {@link Stringifier} for {@link Map.Entry} objects
+ * @see Map.Entry
+ */
+ public static Stringifier<Map.Entry> mapEntryStringifier() {
+ return (bc, entry) -> bc.stringify(entry.getKey()) +
bc.getSetting("mapEntrySeparator", "=") + bc.stringify(entry.getValue());
+ }
+
+ /**
+ * Returns a stringifier for {@link GregorianCalendar} objects that
formats them as ISO-8601 strings.
+ *
+ * <p>This stringifier converts calendar objects to standardized
ISO-8601 timestamp format,
+ * which provides consistent, sortable, and internationally recognized
date representations.</p>
+ *
+ * <h5 class='section'>Behavior:</h5>
+ * <ul>
+ * <li><b>Format:</b> Uses the {@code calendarFormat} setting
(default: {@link java.time.format.DateTimeFormatter#ISO_INSTANT})</li>
+ * <li><b>Timezone:</b> Respects the calendar's timezone
information</li>
+ * <li><b>Precision:</b> Includes full precision available in the
calendar</li>
+ * </ul>
+ *
+ * <h5 class='section'>Usage Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// Test calendar stringification</jc>
+ * <jk>var</jk> <jv>calendar</jv> = <jk>new</jk>
GregorianCalendar(<jv>2023</jv>, Calendar.<jsf>JANUARY</jsf>, <jv>15</jv>);
+ * <jsm>assertMatchesGlob</jsm>(<js>"2023-01-*"</js>,
<jv>calendar</jv>);
+ *
+ * <jc>// Test with custom format</jc>
+ * <jk>var</jk> <jv>converter</jv> =
BasicBeanConverter.<jsm>builder</jsm>()
+ * .defaultSettings()
+ * .addSetting(<jsf>SETTING_calendarFormat</jsf>,
DateTimeFormatter.<jsf>ISO_LOCAL_DATE</jsf>)
+ * .build();
+ * </p>
+ *
+ * @return A {@link Stringifier} for {@link GregorianCalendar} objects
+ * @see GregorianCalendar
+ * @see java.time.format.DateTimeFormatter#ISO_INSTANT
+ */
+ public static Stringifier<GregorianCalendar> calendarStringifier() {
+ return (bc, calendar) ->
calendar.toZonedDateTime().format(bc.getSetting("calendarFormat", ISO_INSTANT));
+ }
+
+ /**
+ * Returns a stringifier for {@link Date} objects that formats them as
ISO instant strings.
+ *
+ * <p>This stringifier converts Date objects to ISO-8601 instant
format, providing
+ * standardized timestamp representations suitable for logging and
comparison.</p>
+ *
+ * <h5 class='section'>Behavior:</h5>
+ * <ul>
+ * <li><b>Format:</b> ISO-8601 instant format (e.g.,
<js>"2023-01-15T10:30:00Z"</js>)</li>
+ * <li><b>Timezone:</b> Always represents time in UTC (Z
timezone)</li>
+ * <li><b>Precision:</b> Millisecond precision as available in
Date objects</li>
+ * </ul>
+ *
+ * <h5 class='section'>Usage Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// Test date stringification</jc>
+ * <jk>var</jk> <jv>date</jv> = <jk>new</jk>
Date(<jv>1673780400000L</jv>); <jc>// 2023-01-15T10:00:00Z</jc>
+ * <jsm>assertBean</jsm>(<jv>date</jv>, <js>"<self>"</js>,
<js>"2023-01-15T10:00:00Z"</js>);
+ *
+ * <jc>// Test in object property</jc>
+ * <jk>var</jk> <jv>event</jv> = <jk>new</jk>
Event().setTimestamp(<jv>date</jv>);
+ * <jsm>assertBean</jsm>(<jv>event</jv>, <js>"timestamp"</js>,
<js>"2023-01-15T10:00:00Z"</js>);
+ * </p>
+ *
+ * @return A {@link Stringifier} for {@link Date} objects
+ * @see Date
+ */
+ public static Stringifier<Date> dateStringifier() {
+ return (bc, date) -> date.toInstant().toString();
+ }
+
+ /**
+ * Returns a stringifier for {@link InputStream} objects that converts
content to hex strings.
+ *
+ * <p><b>Warning:</b> This stringifier consumes and closes the input
stream during conversion.
+ * After stringification, the stream cannot be used again.</p>
+ *
+ * <h5 class='section'>Behavior:</h5>
+ * <ul>
+ * <li><b>Content reading:</b> Reads all available bytes from the
stream</li>
+ * <li><b>Hex conversion:</b> Converts bytes to uppercase
hexadecimal representation</li>
+ * <li><b>Resource management:</b> Automatically closes the stream
after reading</li>
+ * </ul>
+ *
+ * <h5 class='section'>Usage Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// Test with byte content</jc>
+ * <jk>var</jk> <jv>stream</jv> = <jk>new</jk>
ByteArrayInputStream(<jk>new</jk> <jk>byte</jk>[]{<jv>0x48</jv>, <jv>0x65</jv>,
<jv>0x6C</jv>, <jv>0x6C</jv>, <jv>0x6F</jv>});
+ * <jsm>assertBean</jsm>(<jv>stream</jv>, <js>"<self>"</js>,
<js>"48656C6C6F"</js>); <jc>// "Hello" in hex</jc>
+ *
+ * <jc>// Test empty stream</jc>
+ * <jk>var</jk> <jv>empty</jv> = <jk>new</jk>
ByteArrayInputStream(<jk>new</jk> <jk>byte</jk>[<jv>0</jv>]);
+ * <jsm>assertBean</jsm>(<jv>empty</jv>, <js>"<self>"</js>,
<js>""</js>);
+ * </p>
+ *
+ * <h5 class='section'>Important Notes:</h5>
+ * <ul>
+ * <li><b>One-time use:</b> The stream is consumed and closed
during conversion</li>
+ * <li><b>Memory usage:</b> All content is loaded into memory for
conversion</li>
+ * <li><b>Exception handling:</b> IO exceptions are wrapped in
RuntimeException</li>
+ * </ul>
+ *
+ * @return A {@link Stringifier} for {@link InputStream} objects
+ * @see InputStream
+ */
+ public static Stringifier<InputStream> inputStreamStringifier() {
+ return (bc, stream) -> stringifyInputStream(stream);
+ }
+
+ /**
+ * Returns a stringifier for byte arrays that converts them to hex
strings.
+ *
+ * <p>This stringifier provides a consistent way to represent binary
data as readable
+ * hexadecimal strings, useful for testing and debugging binary
content.</p>
+ *
+ * <h5 class='section'>Behavior:</h5>
+ * <ul>
+ * <li><b>Hex format:</b> Each byte is represented as two
uppercase hex digits</li>
+ * <li><b>No separators:</b> Bytes are concatenated without spaces
or delimiters</li>
+ * <li><b>Empty arrays:</b> Returns empty string for zero-length
arrays</li>
+ * </ul>
+ *
+ * <h5 class='section'>Usage Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// Test byte array stringification</jc>
+ * <jk>byte</jk>[] <jv>data</jv> = {<jv>0x48</jv>, <jv>0x65</jv>,
<jv>0x6C</jv>, <jv>0x6C</jv>, <jv>0x6F</jv>};
+ * <jsm>assertBean</jsm>(<jv>data</jv>, <js>"<self>"</js>,
<js>"48656C6C6F"</js>); <jc>// "Hello" in hex</jc>
+ *
+ * <jc>// Test with zeros and high values</jc>
+ * <jk>byte</jk>[] <jv>mixed</jv> = {<jv>0x00</jv>, <jv>0xFF</jv>,
<jv>0x7F</jv>};
+ * <jsm>assertBean</jsm>(<jv>mixed</jv>, <js>"<self>"</js>,
<js>"00FF7F"</js>);
+ * </p>
+ *
+ * @return A {@link Stringifier} for byte arrays
+ */
+ public static Stringifier<byte[]> byteArrayStringifier() {
+ return (bc, bytes) -> {
+ var sb = new StringBuilder(bytes.length * 2);
+ for (var element : bytes) {
+ var v = element & 0xFF;
+ sb.append(HEX[v >>> 4]).append(HEX[v & 0x0F]);
+ }
+ return sb.toString();
+ };
+ }
+
+ /**
+ * Returns a stringifier for {@link Reader} objects that converts
content to strings.
+ *
+ * <p><b>Warning:</b> This stringifier consumes and closes the reader
during conversion.
+ * After stringification, the reader cannot be used again.</p>
+ *
+ * <h5 class='section'>Behavior:</h5>
+ * <ul>
+ * <li><b>Content reading:</b> Reads all available characters from
the reader</li>
+ * <li><b>String conversion:</b> Converts characters directly to
string format</li>
+ * <li><b>Resource management:</b> Automatically closes the reader
after reading</li>
+ * </ul>
+ *
+ * <h5 class='section'>Usage Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// Test with string content</jc>
+ * <jk>var</jk> <jv>reader</jv> = <jk>new</jk>
StringReader(<js>"Hello World"</js>);
+ * <jsm>assertBean</jsm>(<jv>reader</jv>, <js>"<self>"</js>,
<js>"Hello World"</js>);
+ *
+ * <jc>// Test with file reader</jc>
+ * <jk>var</jk> <jv>fileReader</jv> =
Files.<jsm>newBufferedReader</jsm>(path);
+ * <jsm>assertMatchesGlob</jsm>(<js>"*expected content*"</js>,
<jv>fileReader</jv>);
+ * </p>
+ *
+ * <h5 class='section'>Important Notes:</h5>
+ * <ul>
+ * <li><b>One-time use:</b> The reader is consumed and closed
during conversion</li>
+ * <li><b>Memory usage:</b> All content is loaded into memory for
conversion</li>
+ * <li><b>Exception handling:</b> IO exceptions are wrapped in
RuntimeException</li>
+ * </ul>
+ *
+ * @return A {@link Stringifier} for {@link Reader} objects
+ * @see Reader
+ */
+ public static Stringifier<Reader> readerStringifier() {
+ return (bc, reader) -> stringifyReader(reader);
+ }
+
+ /**
+ * Returns a stringifier for {@link File} objects that converts file
content to strings.
+ *
+ * <p>This stringifier reads the entire file content and returns it as
a string,
+ * making it useful for testing file-based operations and content
verification.</p>
+ *
+ * <h5 class='section'>Behavior:</h5>
+ * <ul>
+ * <li><b>Content reading:</b> Reads the entire file content into
memory</li>
+ * <li><b>Encoding:</b> Uses the default platform encoding for
text files</li>
+ * <li><b>Resource management:</b> Properly closes file resources
after reading</li>
+ * </ul>
+ *
+ * <h5 class='section'>Usage Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// Test file content</jc>
+ * <jk>var</jk> <jv>configFile</jv> = <jk>new</jk>
File(<js>"config.properties"</js>);
+ * <jsm>assertMatchesGlob</jsm>(<js>"*database.url=*"</js>,
<jv>configFile</jv>);
+ *
+ * <jc>// Test empty file</jc>
+ * <jk>var</jk> <jv>emptyFile</jv> = <jk>new</jk>
File(<js>"empty.txt"</js>);
+ * <jsm>assertBean</jsm>(<jv>emptyFile</jv>,
<js>"<self>"</js>, <js>""</js>);
+ * </p>
+ *
+ * <h5 class='section'>Important Notes:</h5>
+ * <ul>
+ * <li><b>Memory usage:</b> Large files will consume significant
memory</li>
+ * <li><b>File existence:</b> Non-existent files will cause
exceptions</li>
+ * <li><b>Binary files:</b> May produce unexpected results with
binary content</li>
+ * <li><b>Exception handling:</b> IO exceptions are wrapped in
RuntimeException</li>
+ * </ul>
+ *
+ * @return A {@link Stringifier} for {@link File} objects
+ * @see File
+ */
+ public static Stringifier<File> fileStringifier() {
+ return (bc, file) -> safe(() ->
stringifyReader(Files.newBufferedReader(file.toPath())));
+ }
+
+ /**
+ * Returns a stringifier for {@link Enum} objects that converts them to
name format.
+ *
+ * <p>This stringifier provides a consistent way to represent enum
values as their
+ * declared constant names, which is typically the most useful format
for testing.</p>
+ *
+ * <h5 class='section'>Behavior:</h5>
+ * <ul>
+ * <li><b>Name format:</b> Uses {@link Enum#name()} method for
string representation</li>
+ * <li><b>Case preservation:</b> Maintains the exact case as
declared in enum</li>
+ * <li><b>All enum types:</b> Works with any enum
implementation</li>
+ * </ul>
+ *
+ * <h5 class='section'>Usage Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// Test enum stringification</jc>
+ * <jsm>assertBean</jsm>(Color.<jsf>RED</jsf>,
<js>"<self>"</js>, <js>"RED"</js>);
+ * <jsm>assertBean</jsm>(Status.<jsf>IN_PROGRESS</jsf>,
<js>"<self>"</js>, <js>"IN_PROGRESS"</js>);
+ *
+ * <jc>// Test in object property</jc>
+ * <jk>var</jk> <jv>task</jv> = <jk>new</jk>
Task().setStatus(Status.<jsf>COMPLETED</jsf>);
+ * <jsm>assertBean</jsm>(<jv>task</jv>, <js>"status"</js>,
<js>"COMPLETED"</js>);
+ * </p>
+ *
+ * <h5 class='section'>Alternative Formats:</h5>
+ * <p>If you need different enum string representations (like {@link
Enum#toString()}
+ * or custom formatting), register a custom stringifier for specific
enum types.</p>
+ *
+ * @return A {@link Stringifier} for {@link Enum} objects
+ * @see Enum
+ * @see Enum#name()
+ */
+ public static Stringifier<Enum> enumStringifier() {
+ return (bc, enumValue) -> enumValue.name();
+ }
+
+ /**
+ * Returns a stringifier for {@link Class} objects that formats them
according to configured settings.
+ *
+ * <p>This stringifier provides flexible class name formatting,
supporting different
+ * levels of detail from simple names to fully qualified class
names.</p>
+ *
+ * <h5 class='section'>Behavior:</h5>
+ * <ul>
+ * <li><b>Format options:</b> Controlled by {@code
classNameFormat} setting</li>
+ * <li><b>Simple format:</b> Class simple name (default)</li>
+ * <li><b>Canonical format:</b> Fully qualified canonical name</li>
+ * <li><b>Full format:</b> Complete class name including
package</li>
+ * </ul>
+ *
+ * <h5 class='section'>Usage Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// Test with default simple format</jc>
+ * <jsm>assertBean</jsm>(String.<jk>class</jk>,
<js>"<self>"</js>, <js>"String"</js>);
+ * <jsm>assertBean</jsm>(ArrayList.<jk>class</jk>,
<js>"<self>"</js>, <js>"ArrayList"</js>);
+ *
+ * <jc>// Test with canonical format</jc>
+ * <jk>var</jk> <jv>converter</jv> =
BasicBeanConverter.<jsm>builder</jsm>()
+ * .defaultSettings()
+ * .addSetting(<jsf>SETTING_classNameFormat</jsf>,
<js>"canonical"</js>)
+ * .build();
+ * <jsm>assertBean</jsm>(String.<jk>class</jk>,
<js>"<self>"</js>, <js>"java.lang.String"</js>);
+ * </p>
+ *
+ * @return A {@link Stringifier} for {@link Class} objects
+ * @see Class
+ */
+ public static Stringifier<Class> classStringifier() {
+ return (bc, clazz) -> stringifyClass(bc, clazz);
+ }
+
+ /**
+ * Returns a stringifier for {@link Constructor} objects that formats
them as readable signatures.
+ *
+ * <p>This stringifier creates human-readable constructor signatures
including the
+ * declaring class name and parameter types, useful for
reflection-based testing.</p>
+ *
+ * <h5 class='section'>Behavior:</h5>
+ * <ul>
+ * <li><b>Format:</b>
<js>"{ClassName}({paramType1},{paramType2},...)"</js></li>
+ * <li><b>Class names:</b> Uses the configured class name
format</li>
+ * <li><b>Parameter types:</b> Includes all parameter types in
declaration order</li>
+ * </ul>
+ *
+ * <h5 class='section'>Usage Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// Test constructor stringification</jc>
+ * <jk>var</jk> <jv>constructor</jv> =
String.<jk>class</jk>.getConstructor(<jk>char</jk>[].<jk>class</jk>);
+ * <jsm>assertBean</jsm>(<jv>constructor</jv>,
<js>"<self>"</js>, <js>"String(char[])"</js>);
+ *
+ * <jc>// Test no-arg constructor</jc>
+ * <jk>var</jk> <jv>defaultConstructor</jv> =
ArrayList.<jk>class</jk>.getConstructor();
+ * <jsm>assertBean</jsm>(<jv>defaultConstructor</jv>,
<js>"<self>"</js>, <js>"ArrayList()"</js>);
+ * </p>
+ *
+ * @return A {@link Stringifier} for {@link Constructor} objects
+ * @see Constructor
+ */
+ public static Stringifier<Constructor> constructorStringifier() {
+ return (bc, constructor) -> {
+ return new StringBuilder()
+ .append(stringifyClass(bc, ((Constructor<?>)
constructor).getDeclaringClass()))
+ .append('(')
+
.append(Arrays.stream((constructor).getParameterTypes())
+ .map(x -> stringifyClass(bc, x))
+ .collect(joining(",")))
+ .append(')')
+ .toString();
+ };
+ }
+
+ /**
+ * Returns a stringifier for {@link Method} objects that formats them
as readable signatures.
+ *
+ * <p>This stringifier creates human-readable method signatures
including the method
+ * name and parameter types, useful for reflection-based testing and
debugging.</p>
+ *
+ * <h5 class='section'>Behavior:</h5>
+ * <ul>
+ * <li><b>Format:</b>
<js>"{methodName}({paramType1},{paramType2},...)"</js></li>
+ * <li><b>Method name:</b> Uses the declared method name</li>
+ * <li><b>Parameter types:</b> Includes all parameter types in
declaration order</li>
+ * </ul>
+ *
+ * <h5 class='section'>Usage Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// Test method stringification</jc>
+ * <jk>var</jk> <jv>method</jv> =
String.<jk>class</jk>.getMethod(<js>"substring"</js>,
<jk>int</jk>.<jk>class</jk>, <jk>int</jk>.<jk>class</jk>);
+ * <jsm>assertBean</jsm>(<jv>method</jv>, <js>"<self>"</js>,
<js>"substring(int,int)"</js>);
+ *
+ * <jc>// Test no-arg method</jc>
+ * <jk>var</jk> <jv>toString</jv> =
Object.<jk>class</jk>.getMethod(<js>"toString"</js>);
+ * <jsm>assertBean</jsm>(<jv>toString</jv>,
<js>"<self>"</js>, <js>"toString()"</js>);
+ * </p>
+ *
+ * @return A {@link Stringifier} for {@link Method} objects
+ * @see Method
+ */
+ public static Stringifier<Method> methodStringifier() {
+ return (bc, method) -> {
+ return new StringBuilder()
+ .append(method.getName())
+ .append('(')
+
.append(Arrays.stream(method.getParameterTypes())
+ .map(x -> stringifyClass(bc, x))
+ .collect(joining(",")))
+ .append(')')
+ .toString();
+ };
+ }
+
+ /**
+ * Returns a stringifier for {@link List} objects that formats them
with configurable delimiters.
+ *
+ * <p>This stringifier converts lists to bracket-delimited strings with
customizable
+ * separators and prefixes/suffixes, providing consistent list
representation across tests.</p>
+ *
+ * <h5 class='section'>Behavior:</h5>
+ * <ul>
+ * <li><b>Format:</b>
<js>"{prefix}{element1}{separator}{element2}...{suffix}"</js></li>
+ * <li><b>Separator:</b> Uses {@code fieldSeparator} setting
(default: <js>","</js>)</li>
+ * <li><b>Prefix:</b> Uses {@code collectionPrefix} setting
(default: <js>"["</js>)</li>
+ * <li><b>Suffix:</b> Uses {@code collectionSuffix} setting
(default: <js>"]"</js>)</li>
+ * <li><b>Recursive:</b> Elements are converted using the same
converter</li>
+ * </ul>
+ *
+ * <h5 class='section'>Usage Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// Test list stringification</jc>
+ * <jk>var</jk> <jv>list</jv> =
List.<jsm>of</jsm>(<js>"apple"</js>, <js>"banana"</js>, <js>"cherry"</js>);
+ * <jsm>assertBean</jsm>(<jv>list</jv>, <js>"<self>"</js>,
<js>"[apple,banana,cherry]"</js>);
+ *
+ * <jc>// Test with custom formatting</jc>
+ * <jk>var</jk> <jv>converter</jv> =
BasicBeanConverter.<jsm>builder</jsm>()
+ * .defaultSettings()
+ * .addSetting(<jsf>SETTING_fieldSeparator</jsf>, <js>";
"</js>)
+ * .addSetting(<jsf>SETTING_collectionPrefix</jsf>,
<js>"("</js>)
+ * .addSetting(<jsf>SETTING_collectionSuffix</jsf>,
<js>")"</js>)
+ * .build();
+ * <jsm>assertBean</jsm>(<jv>list</jv>, <js>"<self>"</js>,
<js>"(apple; banana; cherry)"</js>);
+ * </p>
+ *
+ * @return A {@link Stringifier} for {@link List} objects
+ * @see List
+ */
+ public static Stringifier<List> listStringifier() {
+ return (bc, list) -> ((List<?>)list).stream()
+ .map(bc::stringify)
+ .collect(joining(
+ bc.getSetting("fieldSeparator", ","),
+ bc.getSetting("collectionPrefix", "["),
+ bc.getSetting("collectionSuffix", "]")
+ ));
+ }
+
+ /**
+ * Returns a stringifier for {@link Map} objects that formats them with
configurable delimiters.
+ *
+ * <p>This stringifier converts maps to brace-delimited strings by
first converting the
+ * map to a list of entries and then stringifying each entry, providing
consistent
+ * map representation across tests.</p>
+ *
+ * <h5 class='section'>Behavior:</h5>
+ * <ul>
+ * <li><b>Format:</b>
<js>"{prefix}{entry1}{separator}{entry2}...{suffix}"</js></li>
+ * <li><b>Separator:</b> Uses {@code fieldSeparator} setting
(default: <js>","</js>)</li>
+ * <li><b>Prefix:</b> Uses {@code mapPrefix} setting (default:
<js>"{"</js>)</li>
+ * <li><b>Suffix:</b> Uses {@code mapSuffix} setting (default:
<js>"}"</js>)</li>
+ * <li><b>Entry format:</b> Each entry uses the map entry
stringifier</li>
+ * </ul>
+ *
+ * <h5 class='section'>Usage Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// Test map stringification</jc>
+ * <jk>var</jk> <jv>map</jv> = Map.<jsm>of</jsm>(<js>"name"</js>,
<js>"John"</js>, <js>"age"</js>, <jv>25</jv>);
+ * <jsm>assertMatchesGlob</jsm>(<js>"{*name=John*age=25*}"</js>,
<jv>map</jv>);
+ *
+ * <jc>// Test empty map</jc>
+ * <jk>var</jk> <jv>emptyMap</jv> = Map.<jsm>of</jsm>();
+ * <jsm>assertBean</jsm>(<jv>emptyMap</jv>,
<js>"<self>"</js>, <js>"{}"</js>);
+ * </p>
+ *
+ * <h5 class='section'>Order Considerations:</h5>
+ * <p>The order of entries in the string depends on the map
implementation's iteration
+ * order. Use order-independent assertions (like {@code
assertMatchesGlob}) for maps where
+ * order is not guaranteed.</p>
+ *
+ * @return A {@link Stringifier} for {@link Map} objects
+ * @see Map
+ * @see Map.Entry
+ */
+ public static Stringifier<Map> mapStringifier() {
+ return (bc, map) -> ((Map<?,?>)map).entrySet().stream()
+ .map(bc::stringify)
+ .collect(joining(
+ bc.getSetting("fieldSeparator", ","),
+ bc.getSetting("mapPrefix", "{"),
+ bc.getSetting("mapSuffix", "}")
+ ));
+ }
+
+
//---------------------------------------------------------------------------------------------
+ // Helper methods for internal stringification
+
//---------------------------------------------------------------------------------------------
+
+ /**
+ * Converts an InputStream to a hexadecimal string representation.
+ *
+ * @param stream The InputStream to convert
+ * @return Hexadecimal string representation of the stream content
+ */
+ private static String stringifyInputStream(InputStream stream) {
+ return safe(() -> {
+ try (var o2 = stream) {
+ var buff = new ByteArrayOutputStream(1024);
+ var nRead = 0;
+ var b = new byte[1024];
+ while ((nRead = o2.read(b, 0, b.length)) != -1)
+ buff.write(b, 0, nRead);
+ buff.flush();
+ byte[] bytes = buff.toByteArray();
+ var sb = new StringBuilder(bytes.length * 2);
+ for (var element : bytes) {
+ var v = element & 0xFF;
+ sb.append(HEX[v >>> 4]).append(HEX[v &
0x0F]);
+ }
+ return sb.toString();
+ }
+ });
+ }
+
+ /**
+ * Converts a Reader to a string representation.
+ *
+ * @param reader The Reader to convert
+ * @return String content from the reader
+ */
+ private static String stringifyReader(Reader reader) {
+ return safe(() -> {
+ try (var o2 = reader) {
+ var sb = new StringBuilder();
+ var buf = new char[1024];
+ var i = 0;
+ while ((i = o2.read(buf)) != -1)
+ sb.append(buf, 0, i);
+ return sb.toString();
+ }
+ });
+ }
+
+ /**
+ * Converts a Class to a string representation based on converter
settings.
+ *
+ * @param bc The bean converter for accessing settings
+ * @param clazz The Class to convert
+ * @return String representation of the class
+ */
+ private static String stringifyClass(BeanConverter bc, Class<?> clazz) {
+ return switch(bc.getSetting("classNameFormat", "default")) {
+ case "simple" -> clazz.getSimpleName();
+ case "canonical" -> clazz.getCanonicalName();
+ default -> clazz.getName();
+ };
+ }
+}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/junit/Stringifiers_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/junit/Stringifiers_Test.java
new file mode 100644
index 000000000..0cac7a924
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/junit/Stringifiers_Test.java
@@ -0,0 +1,529 @@
+//
***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright
ownership. The ASF licenses this file *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance *
+// * with the License. You may obtain a copy of the License at
*
+// *
*
+// * http://www.apache.org/licenses/LICENSE-2.0
*
+// *
*
+// * Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
express or implied. See the License for the *
+// * specific language governing permissions and limitations under the
License. *
+//
***************************************************************************************************************************
+package org.apache.juneau.junit;
+
+import static org.apache.juneau.junit.Assertions2.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+import java.nio.file.*;
+import java.time.format.*;
+import java.util.*;
+
+import org.junit.jupiter.api.*;
+import org.junit.jupiter.params.*;
+import org.junit.jupiter.params.provider.*;
+
+/**
+ * Unit tests for {@link Stringifiers}.
+ */
+class Stringifiers_Test extends TestBase {
+
+ @Nested
+ class a_MapEntryStringifier {
+
+ @Test
+ void a01_stringifySimpleEntry() {
+ var converter =
BasicBeanConverter.builder().defaultSettings().build();
+ var stringifier = Stringifiers.mapEntryStringifier();
+ var entry = Map.entry("key", "value");
+ var result = stringifier.apply(converter, entry);
+
+ assertEquals("key=value", result);
+ }
+
+ @Test
+ void a02_stringifyWithNullKey() {
+ var converter =
BasicBeanConverter.builder().defaultSettings().build();
+ var stringifier = Stringifiers.mapEntryStringifier();
+ var entry = new AbstractMap.SimpleEntry<String,
String>(null, "value");
+ var result = stringifier.apply(converter, entry);
+
+ assertEquals("<null>=value", result);
+ }
+
+ @Test
+ void a03_stringifyWithNullValue() {
+ var converter =
BasicBeanConverter.builder().defaultSettings().build();
+ var stringifier = Stringifiers.mapEntryStringifier();
+ var entry = new AbstractMap.SimpleEntry<>("key", null);
+ var result = stringifier.apply(converter, entry);
+
+ assertEquals("key=<null>", result);
+ }
+
+ @Test
+ void a04_stringifyWithCustomSeparator() {
+ var converter = BasicBeanConverter.builder()
+ .defaultSettings()
+ .addSetting("mapEntrySeparator", ": ")
+ .build();
+ var stringifier = Stringifiers.mapEntryStringifier();
+ var entry = Map.entry("name", "John");
+ var result = stringifier.apply(converter, entry);
+
+ assertEquals("name: John", result);
+ }
+
+ @Test
+ void a05_stringifyWithComplexValues() {
+ var converter =
BasicBeanConverter.builder().defaultSettings().build();
+ var stringifier = Stringifiers.mapEntryStringifier();
+ var entry = Map.entry("list", List.of("a", "b"));
+ var result = stringifier.apply(converter, entry);
+
+ assertEquals("list=[a,b]", result);
+ }
+ }
+
+ @Nested
+ class b_CalendarStringifier {
+
+ @Test
+ void b01_stringifyGregorianCalendar() {
+ var converter =
BasicBeanConverter.builder().defaultSettings().build();
+ var stringifier = Stringifiers.calendarStringifier();
+ var calendar = new GregorianCalendar(2023,
Calendar.JANUARY, 15, 10, 30, 0);
+ calendar.setTimeZone(TimeZone.getTimeZone("UTC"));
+ var result = stringifier.apply(converter, calendar);
+
+ assertMatchesGlob("2023-01-15T*", result);
+ }
+
+ @Test
+ void b02_stringifyWithCustomFormat() {
+ var converter = BasicBeanConverter.builder()
+ .defaultSettings()
+ .addSetting("calendarFormat",
DateTimeFormatter.ISO_LOCAL_DATE)
+ .build();
+ var stringifier = Stringifiers.calendarStringifier();
+ var calendar = new GregorianCalendar(2023,
Calendar.DECEMBER, 25);
+ var result = stringifier.apply(converter, calendar);
+
+ assertMatchesGlob("2023-12-25", result);
+ }
+ }
+
+ @Nested
+ class c_DateStringifier {
+
+ @Test
+ void c01_stringifyDate() {
+ var stringifier = Stringifiers.dateStringifier();
+ var date = new Date(1673780400000L); //
2023-01-15T11:00:00Z
+ var result = stringifier.apply(null, date);
+
+ assertEquals("2023-01-15T11:00:00Z", result);
+ }
+
+ @Test
+ void c02_stringifyDateWithConverter() {
+ var converter =
BasicBeanConverter.builder().defaultSettings().build();
+ var stringifier = Stringifiers.dateStringifier();
+ var date = new Date(0); // Epoch
+ var result = stringifier.apply(converter, date);
+
+ assertEquals("1970-01-01T00:00:00Z", result);
+ }
+ }
+
+ @Nested
+ class d_InputStreamStringifier {
+
+ @Test
+ void d01_stringifyInputStream() {
+ var stringifier = Stringifiers.inputStreamStringifier();
+ var input = new ByteArrayInputStream(new byte[]{0x48,
0x65, 0x6C, 0x6C, 0x6F}); // "Hello"
+ var result = stringifier.apply(null, input);
+
+ assertEquals("48656C6C6F", result);
+ }
+
+ @Test
+ void d02_stringifyEmptyInputStream() {
+ var stringifier = Stringifiers.inputStreamStringifier();
+ var input = new ByteArrayInputStream(new byte[0]);
+ var result = stringifier.apply(null, input);
+
+ assertEquals("", result);
+ }
+
+ @Test
+ void d03_stringifyInputStreamWithZeros() {
+ var stringifier = Stringifiers.inputStreamStringifier();
+ var input = new ByteArrayInputStream(new byte[]{0x00,
(byte)0xFF, 0x7F});
+ var result = stringifier.apply(null, input);
+
+ assertEquals("00FF7F", result);
+ }
+ }
+
+ @Nested
+ class e_ByteArrayStringifier {
+
+ @Test
+ void e01_stringifyByteArray() {
+ var stringifier = Stringifiers.byteArrayStringifier();
+ var input = new byte[]{0x48, 0x65, 0x6C, 0x6C, 0x6F};
// "Hello"
+ var result = stringifier.apply(null, input);
+
+ assertEquals("48656C6C6F", result);
+ }
+
+ @Test
+ void e02_stringifyEmptyByteArray() {
+ var stringifier = Stringifiers.byteArrayStringifier();
+ var input = new byte[0];
+ var result = stringifier.apply(null, input);
+
+ assertEquals("", result);
+ }
+
+ @Test
+ void e03_stringifyByteArrayWithAllValues() {
+ var stringifier = Stringifiers.byteArrayStringifier();
+ var input = new byte[]{0x00, (byte)0xFF, 0x7F,
(byte)0x80};
+ var result = stringifier.apply(null, input);
+
+ assertEquals("00FF7F80", result);
+ }
+ }
+
+ @Nested
+ class f_ReaderStringifier {
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "Hello World",
+ "",
+ "Line 1\nLine 2\nLine 3"
+ })
+ void f01_stringifyReader(String input) {
+ var stringifier = Stringifiers.readerStringifier();
+ var reader = new StringReader(input);
+ var result = stringifier.apply(null, reader);
+
+ assertEquals(input, result);
+ }
+ }
+
+ @Nested
+ class g_FileStringifier {
+
+ @Test
+ void g01_stringifyTempFile() throws Exception {
+ var stringifier = Stringifiers.fileStringifier();
+ var tempFile = Files.createTempFile("test", ".txt");
+ Files.writeString(tempFile, "Test content");
+
+ try {
+ var result = stringifier.apply(null,
tempFile.toFile());
+ assertEquals("Test content", result);
+ } finally {
+ Files.deleteIfExists(tempFile);
+ }
+ }
+
+ @Test
+ void g02_stringifyEmptyFile() throws Exception {
+ var stringifier = Stringifiers.fileStringifier();
+ var tempFile = Files.createTempFile("empty", ".txt");
+
+ try {
+ var result = stringifier.apply(null,
tempFile.toFile());
+ assertEquals("", result);
+ } finally {
+ Files.deleteIfExists(tempFile);
+ }
+ }
+ }
+
+ @Nested
+ class h_EnumStringifier {
+
+ enum TestEnum { VALUE1, VALUE2, LONG_VALUE_NAME }
+
+ @Test
+ void h01_stringifyEnum() {
+ var stringifier = Stringifiers.enumStringifier();
+ var result = stringifier.apply(null, TestEnum.VALUE1);
+
+ assertEquals("VALUE1", result);
+ }
+
+ @Test
+ void h02_stringifyEnumWithLongName() {
+ var stringifier = Stringifiers.enumStringifier();
+ var result = stringifier.apply(null,
TestEnum.LONG_VALUE_NAME);
+
+ assertEquals("LONG_VALUE_NAME", result);
+ }
+
+ @Test
+ void h03_stringifyStandardEnum() {
+ var stringifier = Stringifiers.enumStringifier();
+ var result = stringifier.apply(null,
Thread.State.RUNNABLE);
+
+ assertEquals("RUNNABLE", result);
+ }
+ }
+
+ @Nested
+ class i_ClassStringifier {
+
+ @Test
+ void i01_stringifyClassSimple() {
+ var converter = BasicBeanConverter.builder()
+ .defaultSettings()
+ .addSetting("classNameFormat", "simple")
+ .build();
+ var stringifier = Stringifiers.classStringifier();
+ var result = stringifier.apply(converter, String.class);
+
+ assertEquals("String", result);
+ }
+
+ @Test
+ void i02_stringifyClassCanonical() {
+ var converter = BasicBeanConverter.builder()
+ .defaultSettings()
+ .addSetting("classNameFormat", "canonical")
+ .build();
+ var stringifier = Stringifiers.classStringifier();
+ var result = stringifier.apply(converter,
ArrayList.class);
+
+ assertEquals("java.util.ArrayList", result);
+ }
+
+ @Test
+ void i03_stringifyClassFull() {
+ var converter = BasicBeanConverter.builder()
+ .defaultSettings()
+ .addSetting("classNameFormat", "full")
+ .build();
+ var stringifier = Stringifiers.classStringifier();
+ var result = stringifier.apply(converter,
HashMap.class);
+
+ assertEquals("java.util.HashMap", result);
+ }
+
+ @Test
+ void i04_stringifyPrimitiveClass() {
+ var converter =
BasicBeanConverter.builder().defaultSettings().build();
+ var stringifier = Stringifiers.classStringifier();
+ var result = stringifier.apply(converter, int.class);
+
+ assertEquals("int", result);
+ }
+ }
+
+ @Nested
+ class j_ConstructorStringifier {
+
+ @Test
+ void j01_stringifyNoArgConstructor() throws Exception {
+ var converter =
BasicBeanConverter.builder().defaultSettings().build();
+ var stringifier = Stringifiers.constructorStringifier();
+ var constructor = ArrayList.class.getConstructor();
+ var result = stringifier.apply(converter, constructor);
+
+ assertEquals("ArrayList()", result);
+ }
+
+ @Test
+ void j02_stringifyParameterizedConstructor() throws Exception {
+ var converter =
BasicBeanConverter.builder().defaultSettings().build();
+ var stringifier = Stringifiers.constructorStringifier();
+ var constructor =
String.class.getConstructor(char[].class);
+ var result = stringifier.apply(converter, constructor);
+
+ assertEquals("String(char[])", result);
+ }
+
+ @Test
+ void j03_stringifyMultiParameterConstructor() throws Exception {
+ var converter =
BasicBeanConverter.builder().defaultSettings().build();
+ var stringifier = Stringifiers.constructorStringifier();
+ var constructor =
String.class.getConstructor(byte[].class, int.class, int.class);
+ var result = stringifier.apply(converter, constructor);
+
+ assertEquals("String(byte[],int,int)", result);
+ }
+ }
+
+ @Nested
+ class k_MethodStringifier {
+
+ @Test
+ void k01_stringifyNoArgMethod() throws Exception {
+ var converter =
BasicBeanConverter.builder().defaultSettings().build();
+ var stringifier = Stringifiers.methodStringifier();
+ var method = Object.class.getMethod("toString");
+ var result = stringifier.apply(converter, method);
+
+ assertEquals("toString()", result);
+ }
+
+ @Test
+ void k02_stringifyParameterizedMethod() throws Exception {
+ var converter =
BasicBeanConverter.builder().defaultSettings().build();
+ var stringifier = Stringifiers.methodStringifier();
+ var method = String.class.getMethod("substring",
int.class);
+ var result = stringifier.apply(converter, method);
+
+ assertEquals("substring(int)", result);
+ }
+
+ @Test
+ void k03_stringifyMultiParameterMethod() throws Exception {
+ var converter =
BasicBeanConverter.builder().defaultSettings().build();
+ var stringifier = Stringifiers.methodStringifier();
+ var method = String.class.getMethod("substring",
int.class, int.class);
+ var result = stringifier.apply(converter, method);
+
+ assertEquals("substring(int,int)", result);
+ }
+ }
+
+ @Nested
+ class l_ListStringifier {
+
+ @Test
+ void l01_stringifyList() {
+ var converter =
BasicBeanConverter.builder().defaultSettings().build();
+ var stringifier = Stringifiers.listStringifier();
+ var input = List.of("a", "b", "c");
+ var result = stringifier.apply(converter, input);
+
+ assertEquals("[a,b,c]", result);
+ }
+
+ @Test
+ void l02_stringifyEmptyList() {
+ var converter =
BasicBeanConverter.builder().defaultSettings().build();
+ var stringifier = Stringifiers.listStringifier();
+ var input = List.of();
+ var result = stringifier.apply(converter, input);
+
+ assertEquals("[]", result);
+ }
+
+ @Test
+ void l03_stringifyWithCustomDelimiters() {
+ var converter = BasicBeanConverter.builder()
+ .defaultSettings()
+ .addSetting("fieldSeparator", "; ")
+ .addSetting("collectionPrefix", "(")
+ .addSetting("collectionSuffix", ")")
+ .build();
+ var stringifier = Stringifiers.listStringifier();
+ var input = List.of("x", "y", "z");
+ var result = stringifier.apply(converter, input);
+
+ assertEquals("(x; y; z)", result);
+ }
+
+ @Test
+ void l04_stringifyWithNullElements() {
+ var converter =
BasicBeanConverter.builder().defaultSettings().build();
+ var stringifier = Stringifiers.listStringifier();
+ var input = Arrays.asList("a", null, "c");
+ var result = stringifier.apply(converter, input);
+
+ assertEquals("[a,<null>,c]", result);
+ }
+ }
+
+ @Nested
+ class m_MapStringifier {
+
+ @Test
+ void m01_stringifyMap() {
+ var converter =
BasicBeanConverter.builder().defaultSettings().build();
+ var stringifier = Stringifiers.mapStringifier();
+ var input = Map.of("key1", "value1", "key2", "value2");
+ var result = stringifier.apply(converter, input);
+
+ // Order may vary, so check both possibilities
+ assertTrue(result.equals("{key1=value1,key2=value2}") ||
+
result.equals("{key2=value2,key1=value1}"));
+ }
+
+ @Test
+ void m02_stringifyEmptyMap() {
+ var converter =
BasicBeanConverter.builder().defaultSettings().build();
+ var stringifier = Stringifiers.mapStringifier();
+ var input = Map.of();
+ var result = stringifier.apply(converter, input);
+
+ assertEquals("{}", result);
+ }
+
+ @Test
+ void m03_stringifyWithCustomDelimiters() {
+ var converter = BasicBeanConverter.builder()
+ .defaultSettings()
+ .addSetting("fieldSeparator", "; ")
+ .addSetting("mapPrefix", "<")
+ .addSetting("mapSuffix", ">")
+ .build();
+ var stringifier = Stringifiers.mapStringifier();
+ var input = Map.of("a", "1");
+ var result = stringifier.apply(converter, input);
+
+ assertEquals("<a=1>", result);
+ }
+
+ @Test
+ void m04_stringifyWithNullValues() {
+ var converter =
BasicBeanConverter.builder().defaultSettings().build();
+ var stringifier = Stringifiers.mapStringifier();
+ var input = new HashMap<String, String>();
+ input.put("key1", "value1");
+ input.put("key2", null);
+ var result = stringifier.apply(converter, input);
+
+ assertTrue(result.contains("key1=value1"));
+ assertTrue(result.contains("key2=<null>"));
+ }
+ }
+
+ @Nested
+ class n_Integration {
+
+ @Test
+ void n01_useInBasicBeanConverter() {
+ // Test various stringifiable objects
+ assertBean(Map.entry("k", "v"), "<self>", "k=v");
+ assertBean(List.of("a", "b"), "<self>", "[a,b]");
+ assertBean(Map.of("x", "y"), "<self>", "{x=y}");
+ assertBean(TestEnum.VALUE1, "<self>", "VALUE1");
+ assertBean(String.class, "<self>", "String");
+ }
+
+ @Test
+ void n02_customStringifierRegistration() {
+ // Test that custom registration works
+ assertBean(List.of("custom"), "<self>", "[custom]");
+ }
+
+ @Test
+ void n03_stringifierChaining() {
+ // Map with list values
+ var complexMap = Map.of("items", List.of("a", "b"));
+ assertMatchesGlob("*items=[a,b]*",
BasicBeanConverter.DEFAULT.stringify(complexMap));
+ }
+
+ private enum TestEnum { VALUE1, VALUE2 }
+ }
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/junit/Swappers.java
b/juneau-utest/src/test/java/org/apache/juneau/junit/Swappers.java
new file mode 100644
index 000000000..f3829273d
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/junit/Swappers.java
@@ -0,0 +1,213 @@
+//
***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright
ownership. The ASF licenses this file *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance *
+// * with the License. You may obtain a copy of the License at
*
+// *
*
+// * http://www.apache.org/licenses/LICENSE-2.0
*
+// *
*
+// * Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
express or implied. See the License for the *
+// * specific language governing permissions and limitations under the
License. *
+//
***************************************************************************************************************************
+package org.apache.juneau.junit;
+
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.function.*;
+
+/**
+ * Collection of standard swapper implementations for the Bean-Centric Testing
framework.
+ *
+ * <p>This class provides built-in object transformation strategies that
handle common wrapper
+ * types and asynchronous objects. These swappers are automatically registered
when using
+ * {@link BasicBeanConverter.Builder#defaultSettings()}.</p>
+ *
+ * <h5 class='section'>Purpose:</h5>
+ * <p>Swappers pre-process objects before stringification or listification,
enabling the converter
+ * to handle wrapper types, lazy evaluation objects, and asynchronous results
consistently. They
+ * transform objects into more primitive forms that can be easily converted to
strings or lists.</p>
+ *
+ * <h5 class='section'>Built-in Swappers:</h5>
+ * <ul>
+ * <li><b>{@link #optionalSwapper()}</b> - Unwraps {@link Optional} values
to their contained objects or <jk>null</jk></li>
+ * <li><b>{@link #supplierSwapper()}</b> - Evaluates {@link Supplier}
functions to get their supplied values</li>
+ * <li><b>{@link #futureSwapper()}</b> - Extracts completed {@link Future}
results or returns status messages</li>
+ * </ul>
+ *
+ * <h5 class='section'>Usage Example:</h5>
+ * <p class='bjava'>
+ * <jc>// Register swappers using builder</jc>
+ * <jk>var</jk> <jv>converter</jv> =
BasicBeanConverter.<jsm>builder</jsm>()
+ * .defaultSettings()
+ * .addSwapper(Optional.<jk>class</jk>,
Swappers.<jsm>optionalSwapper</jsm>())
+ * .addSwapper(Supplier.<jk>class</jk>,
Swappers.<jsm>supplierSwapper</jsm>())
+ * .addSwapper(Future.<jk>class</jk>,
Swappers.<jsm>futureSwapper</jsm>())
+ * .build();
+ * </p>
+ *
+ * <h5 class='section'>Custom Swapper Development:</h5>
+ * <p>When creating custom swappers, follow these patterns:</p>
+ * <ul>
+ * <li><b>Null Safety:</b> Handle <jk>null</jk> inputs gracefully</li>
+ * <li><b>Exception Handling:</b> Convert exceptions to meaningful error
messages</li>
+ * <li><b>Status Indication:</b> Provide clear status for incomplete or
error states</li>
+ * <li><b>Non-blocking:</b> Avoid blocking operations that might hang
tests</li>
+ * </ul>
+ *
+ * @see Swapper
+ * @see BasicBeanConverter.Builder#addSwapper(Class, Swapper)
+ * @see BasicBeanConverter.Builder#defaultSettings()
+ */
+@SuppressWarnings({"unchecked","rawtypes"})
+public class Swappers {
+
+ /**
+ * Constructor.
+ */
+ private Swappers() {}
+
+ /**
+ * Returns a swapper for {@link Optional} objects that unwraps them to
their contained values.
+ *
+ * <p>This swapper extracts the value from Optional instances,
returning the contained object
+ * if present, or <jk>null</jk> if the Optional is empty. This allows
Optional-wrapped values
+ * to be processed naturally by the converter without special
handling.</p>
+ *
+ * <h5 class='section'>Behavior:</h5>
+ * <ul>
+ * <li><b>Present value:</b> Returns the contained object for
further processing</li>
+ * <li><b>Empty Optional:</b> Returns <jk>null</jk>, which will be
rendered as the configured null value</li>
+ * </ul>
+ *
+ * <h5 class='section'>Usage Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// Test Optional with value</jc>
+ * <jk>var</jk> <jv>optional</jv> =
Optional.<jsm>of</jsm>(<js>"Hello"</js>);
+ * <jsm>assertBean</jsm>(<jv>optional</jv>,
<js>"<self>"</js>, <js>"Hello"</js>);
+ *
+ * <jc>// Test empty Optional</jc>
+ * <jk>var</jk> <jv>empty</jv> = Optional.<jsm>empty</jsm>();
+ * <jsm>assertBean</jsm>(<jv>empty</jv>, <js>"<self>"</js>,
<js>"<null>"</js>);
+ *
+ * <jc>// Test nested Optional in object</jc>
+ * <jk>var</jk> <jv>user</jv> = <jk>new</jk>
User().setName(Optional.<jsm>of</jsm>(<js>"John"</js>));
+ * <jsm>assertBean</jsm>(<jv>user</jv>, <js>"name"</js>,
<js>"John"</js>);
+ * </p>
+ *
+ * <h5 class='section'>Integration:</h5>
+ * <p>This swapper is particularly useful when working with modern Java
APIs that return
+ * Optional values. It allows test assertions to focus on the actual
data rather than
+ * Optional wrapper mechanics.</p>
+ *
+ * @return A {@link Swapper} for {@link Optional} objects
+ * @see Optional
+ */
+ public static Swapper<Optional> optionalSwapper() {
+ return (bc, optional) -> optional.orElse(null);
+ }
+
+ /**
+ * Returns a swapper for {@link Supplier} objects that evaluates them
to get their supplied values.
+ *
+ * <p>This swapper calls the {@link Supplier#get()} method to obtain
the value that the supplier
+ * provides. This enables testing of lazy-evaluated or
dynamically-computed values as if they
+ * were regular objects.</p>
+ *
+ * <h5 class='section'>Behavior:</h5>
+ * <ul>
+ * <li><b>Successful evaluation:</b> Returns the result of calling
{@link Supplier#get()}</li>
+ * <li><b>Exception during evaluation:</b> Allows the exception to
propagate (no special handling)</li>
+ * </ul>
+ *
+ * <h5 class='section'>Usage Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// Test supplier with simple value</jc>
+ * <jk>var</jk> <jv>supplier</jv> = () -> <js>"Hello
World"</js>;
+ * <jsm>assertBean</jsm>(<jv>supplier</jv>,
<js>"<self>"</js>, <js>"Hello World"</js>);
+ *
+ * <jc>// Test supplier in object property</jc>
+ * <jk>var</jk> <jv>config</jv> = <jk>new</jk>
Configuration().setDynamicValue(() -> calculateValue());
+ * <jsm>assertBean</jsm>(<jv>config</jv>, <js>"dynamicValue"</js>,
<js>"computed-result"</js>);
+ *
+ * <jc>// Test supplier returning null</jc>
+ * <jk>var</jk> <jv>nullSupplier</jv> = () -> <jk>null</jk>;
+ * <jsm>assertBean</jsm>(<jv>nullSupplier</jv>,
<js>"<self>"</js>, <js>"<null>"</js>);
+ * </p>
+ *
+ * <h5 class='section'>Important Notes:</h5>
+ * <ul>
+ * <li><b>Side Effects:</b> The supplier will be evaluated during
conversion, which may cause side effects</li>
+ * <li><b>Performance:</b> Expensive computations in suppliers
will impact test performance</li>
+ * <li><b>Exception Handling:</b> Exceptions from suppliers are
not caught by this swapper</li>
+ * </ul>
+ *
+ * @return A {@link Swapper} for {@link Supplier} objects
+ * @see Supplier
+ */
+ public static Swapper<Supplier> supplierSwapper() {
+ return (bc, supplier) -> supplier.get();
+ }
+
+ /**
+ * Returns a swapper for {@link Future} objects that extracts completed
results or status information.
+ *
+ * <p>This swapper handles {@link Future} objects in a non-blocking
manner, providing meaningful
+ * information about the future's state without waiting for
completion:</p>
+ *
+ * <h5 class='section'>Behavior:</h5>
+ * <ul>
+ * <li><b>Completed successfully:</b> Returns the actual result
value</li>
+ * <li><b>Completed with exception:</b> Returns <js>"<error:
{message}>"</js> format</li>
+ * <li><b>Cancelled:</b> Returns <js>"<cancelled>"</js></li>
+ * <li><b>Still pending:</b> Returns
<js>"<pending>"</js></li>
+ * </ul>
+ *
+ * <h5 class='section'>Usage Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// Test completed future</jc>
+ * <jk>var</jk> <jv>future</jv> =
CompletableFuture.<jsm>completedFuture</jsm>(<js>"Hello"</js>);
+ * <jsm>assertBean</jsm>(<jv>future</jv>, <js>"<self>"</js>,
<js>"Hello"</js>);
+ *
+ * <jc>// Test pending future</jc>
+ * <jk>var</jk> <jv>pending</jv> = <jk>new</jk>
CompletableFuture<String>();
+ * <jsm>assertBean</jsm>(<jv>pending</jv>,
<js>"<self>"</js>, <js>"<pending>"</js>);
+ *
+ * <jc>// Test cancelled future</jc>
+ * <jk>var</jk> <jv>cancelled</jv> = <jk>new</jk>
CompletableFuture<String>();
+ * <jv>cancelled</jv>.cancel(<jk>true</jk>);
+ * <jsm>assertBean</jsm>(<jv>cancelled</jv>,
<js>"<self>"</js>, <js>"<cancelled>"</js>);
+ *
+ * <jc>// Test failed future</jc>
+ * <jk>var</jk> <jv>failed</jv> = <jk>new</jk>
CompletableFuture<String>();
+ * <jv>failed</jv>.completeExceptionally(<jk>new</jk>
RuntimeException(<js>"Test error"</js>));
+ * <jsm>assertBean</jsm>(<jv>failed</jv>, <js>"<self>"</js>,
<js>"<error: Test error>"</js>);
+ * </p>
+ *
+ * <h5 class='section'>Non-blocking Guarantee:</h5>
+ * <p>This swapper never calls {@link Future#get()} without first
checking {@link Future#isDone()},
+ * ensuring that test execution is never blocked by incomplete futures.
This makes it safe to use
+ * in unit tests without risking hangs or timeouts.</p>
+ *
+ * <h5 class='section'>Error Handling:</h5>
+ * <p>When a future completes exceptionally, the swapper extracts the
exception message and
+ * formats it as <js>"<error: {message}>"</js>. This provides
useful debugging information
+ * while maintaining a consistent string format for assertions.</p>
+ *
+ * @return A {@link Swapper} for {@link Future} objects
+ * @see Future
+ * @see CompletableFuture
+ */
+ public static Swapper<Future> futureSwapper() {
+ return (bc, future) -> {
+ if (future.isDone() && !future.isCancelled()) {
+ try {
+ return future.get();
+ } catch (Exception e) {
+ return "<error: " + e.getMessage() +
">";
+ }
+ }
+ return future.isCancelled() ? "<cancelled>" :
"<pending>";
+ };
+ }
+}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/junit/Swappers_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/junit/Swappers_Test.java
new file mode 100644
index 000000000..4f17079b3
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/junit/Swappers_Test.java
@@ -0,0 +1,227 @@
+//
***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright
ownership. The ASF licenses this file *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance *
+// * with the License. You may obtain a copy of the License at
*
+// *
*
+// * http://www.apache.org/licenses/LICENSE-2.0
*
+// *
*
+// * Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
express or implied. See the License for the *
+// * specific language governing permissions and limitations under the
License. *
+//
***************************************************************************************************************************
+package org.apache.juneau.junit;
+
+import static org.apache.juneau.junit.Assertions2.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.function.*;
+
+import org.junit.jupiter.api.*;
+
+/**
+ * Unit tests for {@link Swappers}.
+ */
+class Swappers_Test extends TestBase {
+
+ @Nested
+ class a_OptionalSwapper {
+
+ @Test
+ void a01_swapPresentOptional() {
+ var swapper = Swappers.optionalSwapper();
+ var result = swapper.apply(null, Optional.of("Hello"));
+ assertEquals("Hello", result);
+ }
+
+ @Test
+ void a02_swapEmptyOptional() {
+ var swapper = Swappers.optionalSwapper();
+ var result = swapper.apply(null, Optional.empty());
+ assertNull(result);
+ }
+
+ @Test
+ void a03_swapOptionalWithNull() {
+ var swapper = Swappers.optionalSwapper();
+ var result = swapper.apply(null,
Optional.ofNullable(null));
+ assertNull(result);
+ }
+
+ @Test
+ void a04_swapOptionalWithComplexObject() {
+ var swapper = Swappers.optionalSwapper();
+ var list = List.of("a", "b", "c");
+ var result = swapper.apply(null, Optional.of(list));
+ assertSame(list, result);
+ }
+
+ @Test
+ void a05_swapOptionalWithConverter() {
+ var converter =
BasicBeanConverter.builder().defaultSettings().build();
+ var swapper = Swappers.optionalSwapper();
+ var result = swapper.apply(converter, Optional.of(42));
+ assertEquals(42, result);
+ }
+ }
+
+ @Nested
+ class b_SupplierSwapper {
+
+ @Test
+ void b01_swapSupplierWithValue() {
+ var swapper = Swappers.supplierSwapper();
+ Supplier<String> supplier = () -> "Hello World";
+ var result = swapper.apply(null, supplier);
+ assertEquals("Hello World", result);
+ }
+
+ @Test
+ void b02_swapSupplierWithNull() {
+ var swapper = Swappers.supplierSwapper();
+ Supplier<String> supplier = () -> null;
+ var result = swapper.apply(null, supplier);
+ assertNull(result);
+ }
+
+ @Test
+ void b03_swapSupplierWithComplexObject() {
+ var swapper = Swappers.supplierSwapper();
+ var list = List.of("x", "y", "z");
+ Supplier<List<String>> supplier = () -> list;
+ var result = swapper.apply(null, supplier);
+ assertSame(list, result);
+ }
+
+ @Test
+ void b04_swapSupplierWithConverter() {
+ var converter =
BasicBeanConverter.builder().defaultSettings().build();
+ var swapper = Swappers.supplierSwapper();
+ Supplier<Integer> supplier = () -> 123;
+ var result = swapper.apply(converter, supplier);
+ assertEquals(123, result);
+ }
+
+ @Test
+ void b05_swapSupplierWithException() {
+ var swapper = Swappers.supplierSwapper();
+ Supplier<String> supplier = () -> {
+ throw new RuntimeException("Test exception");
+ };
+ assertThrows(RuntimeException.class, () ->
swapper.apply(null, supplier));
+ }
+ }
+
+ @Nested
+ class c_FutureSwapper {
+
+ @Test
+ void c01_swapCompletedFuture() {
+ var swapper = Swappers.futureSwapper();
+ var future =
CompletableFuture.completedFuture("Result");
+ var result = swapper.apply(null, future);
+ assertEquals("Result", result);
+ }
+
+ @Test
+ void c02_swapPendingFuture() {
+ var swapper = Swappers.futureSwapper();
+ var future = new CompletableFuture<String>();
+ var result = swapper.apply(null, future);
+ assertEquals("<pending>", result);
+ }
+
+ @Test
+ void c03_swapCancelledFuture() {
+ var swapper = Swappers.futureSwapper();
+ var future = new CompletableFuture<String>();
+ future.cancel(true);
+ var result = swapper.apply(null, future);
+ assertEquals("<cancelled>", result);
+ }
+
+ @Test
+ void c04_swapFailedFuture() {
+ var swapper = Swappers.futureSwapper();
+ var future = new CompletableFuture<String>();
+ future.completeExceptionally(new RuntimeException("Test
error"));
+ var result = swapper.apply(null, future);
+ assertEquals("<error: java.lang.RuntimeException: Test
error>", result);
+ }
+
+ @Test
+ void c05_swapCompletedFutureWithNull() {
+ var swapper = Swappers.futureSwapper();
+ var future = CompletableFuture.completedFuture(null);
+ var result = swapper.apply(null, future);
+ assertNull(result);
+ }
+
+ @Test
+ void c06_swapFutureWithComplexObject() {
+ var swapper = Swappers.futureSwapper();
+ var list = List.of("a", "b", "c");
+ var future = CompletableFuture.completedFuture(list);
+ var result = swapper.apply(null, future);
+ assertSame(list, result);
+ }
+
+ @Test
+ void c07_swapFutureWithConverter() {
+ var converter =
BasicBeanConverter.builder().defaultSettings().build();
+ var swapper = Swappers.futureSwapper();
+ var future = CompletableFuture.completedFuture(999);
+ var result = swapper.apply(converter, future);
+ assertEquals(999, result);
+ }
+
+ @Test
+ void c08_swapFutureWithNestedCause() {
+ var swapper = Swappers.futureSwapper();
+ var future = new CompletableFuture<String>();
+ var rootCause = new IllegalArgumentException("Root
cause");
+ var wrappedException = new RuntimeException("Wrapper",
rootCause);
+ future.completeExceptionally(wrappedException);
+ var result = swapper.apply(null, future);
+ assertEquals("<error: java.lang.RuntimeException:
Wrapper>", result);
+ }
+ }
+
+ @Nested
+ class d_Integration {
+
+ @Test
+ void d01_useInBasicBeanConverter() {
+ // Test Optional swapping
+ assertBean(Optional.of("test"), "<self>", "test");
+ assertBean(Optional.empty(), "<self>", "<null>");
+
+ // Test Supplier swapping
+ Supplier<String> supplier = () -> "supplied";
+ assertBean(supplier, "<self>", "supplied");
+
+ // Test Future swapping
+ var future =
CompletableFuture.completedFuture("future-result");
+ assertBean(future, "<self>", "future-result");
+ }
+
+ @Test
+ void d02_customSwapperRegistration() {
+ // Test that custom registration works
+ assertBean(Optional.of("custom"), "<self>", "custom");
+ }
+
+ @Test
+ void d03_nestedSwapping() {
+ // Test nested Optional in Supplier
+ Supplier<Optional<String>> nestedSupplier = () ->
Optional.of("nested");
+ assertBean(nestedSupplier, "<self>", "nested");
+
+ // Test Optional containing Supplier
+ Supplier<String> supplier = () -> "inner";
+ assertBean(Optional.of(supplier), "<self>", "inner");
+ }
+ }
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/junit/Utils.java
b/juneau-utest/src/test/java/org/apache/juneau/junit/Utils.java
index 782d040cc..1cba4f1da 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/junit/Utils.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/junit/Utils.java
@@ -48,7 +48,7 @@ import org.opentest4j.*;
* String <jv>validated</jv> =
<jsm>assertArgNotNull</jsm>(<js>"username"</js>, <jv>username</jv>);
*
* <jc>// Pattern matching</jc>
- * Pattern <jv>pattern</jv> =
<jsm>getMatchPattern</jsm>(<js>"user_*_temp"</js>);
+ * Pattern <jv>pattern</jv> =
<jsm>getGlobMatchPattern</jsm>(<js>"user_*_temp"</js>);
* </p>
*
* <p><b>Thread Safety:</b> All methods in this class are thread-safe as they
are stateless static methods.
@@ -303,7 +303,7 @@ class Utils {
}
/**
- * Converts a string containing wildcard characters to a regular
expression {@link Pattern}.
+ * Converts a string containing glob-style wildcard characters to a
regular expression {@link Pattern}.
*
* <p>This method converts glob-style patterns to regular expressions
with the following mappings:
* <ul>
@@ -314,21 +314,21 @@ class Utils {
*
* <h5 class='section'>Example:</h5>
* <p class='bjava'>
- * <jk>var</jk> <jv>pattern</jv> =
<jsm>getMatchPattern</jsm>(<js>"user_*_temp"</js>);
+ * <jk>var</jk> <jv>pattern</jv> =
<jsm>getGlobMatchPattern</jsm>(<js>"user_*_temp"</js>);
* <jk>boolean</jk> <jv>matches</jv> =
<jv>pattern</jv>.matcher(<js>"user_alice_temp"</js>).matches(); <jc>//
true</jc>
* <jv>matches</jv> =
<jv>pattern</jv>.matcher(<js>"user_bob_temp"</js>).matches(); <jc>//
true</jc>
* <jv>matches</jv> =
<jv>pattern</jv>.matcher(<js>"admin_alice_temp"</js>).matches(); <jc>//
false</jc>
* </p>
*
- * @param s The wildcard pattern string.
+ * @param s The glob-style wildcard pattern string.
* @return A compiled {@link Pattern} object, or null if the input
string is null.
*/
- public static Pattern getMatchPattern(String s) {
- return getMatchPattern(s, 0);
+ public static Pattern getGlobMatchPattern(String s) {
+ return getGlobMatchPattern(s, 0);
}
/**
- * Converts a string containing wildcard characters to a regular
expression {@link Pattern} with flags.
+ * Converts a string containing glob-style wildcard characters to a
regular expression {@link Pattern} with flags.
*
* <p>This method converts glob-style patterns to regular expressions
with the following mappings:
* <ul>
@@ -340,15 +340,15 @@ class Utils {
* <h5 class='section'>Example:</h5>
* <p class='bjava'>
* <jc>// Case-insensitive matching</jc>
- * <jk>var</jk> <jv>pattern</jv> =
<jsm>getMatchPattern</jsm>(<js>"USER_*"</js>,
Pattern.<jsf>CASE_INSENSITIVE</jsf>);
+ * <jk>var</jk> <jv>pattern</jv> =
<jsm>getGlobMatchPattern</jsm>(<js>"USER_*"</js>,
Pattern.<jsf>CASE_INSENSITIVE</jsf>);
* <jk>boolean</jk> <jv>matches</jv> =
<jv>pattern</jv>.matcher(<js>"user_alice"</js>).matches(); <jc>// true</jc>
* </p>
*
- * @param s The wildcard pattern string.
+ * @param s The glob-style wildcard pattern string.
* @param flags Regular expression flags (see {@link Pattern}
constants).
* @return A compiled {@link Pattern} object, or null if the input
string is null.
*/
- public static Pattern getMatchPattern(String s, int flags) {
+ public static Pattern getGlobMatchPattern(String s, int flags) {
if (s == null)
return null;
var sb = new StringBuilder();
diff --git a/juneau-utest/src/test/java/org/apache/juneau/junit/Utils_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/junit/Utils_Test.java
index 7b4c3fc14..a7bb3957e 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/junit/Utils_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/junit/Utils_Test.java
@@ -336,9 +336,25 @@ class Utils_Test extends TestBase {
assertString(f("User {0} (age {1}) is active: {2}", "Alice",
25, true), "User Alice (age 25) is active: true");
}
+ @Test
+ @DisplayName("f() - Edge cases and special characters")
+ void f04_fEdgeCases() {
+ // Test with special characters (avoid braces in parameters for
MessageFormat compatibility)
+ assertEquals("Pattern: .*items=\\[a,b\\].*", f("Pattern: {0}",
".*items=\\[a,b\\].*"));
+ assertEquals("Simple: no braces here", f("Simple: {0}", "no
braces here"));
+ assertEquals("Mixed: string with data and 42", f("Mixed: {0}
and {1}", "string with data", 42));
+
+ // Test with non-string arguments
+ assertEquals("Number: 42", f("Number: {0}", 42));
+ assertEquals("Boolean: true", f("Boolean: {0}", true));
+
+ // Test that single quotes in input are properly handled (need
to be doubled in MessageFormat)
+ assertEquals("Text with 'single quotes'", f("Text with {0}",
"'single quotes'"));
+ }
+
@Test
@DisplayName("fs() - Supplier creation")
- void f04_fsSupplierCreation() {
+ void f05_fsSupplierCreation() {
var supplier = fs("User {0} has {1} items", "Bob", 3);
assertEquals("User Bob has 3 items", supplier.get());
@@ -348,7 +364,7 @@ class Utils_Test extends TestBase {
@Test
@DisplayName("fs() - No parameters supplier")
- void f05_fsNoParameters() {
+ void f06_fsNoParameters() {
assertEquals("Static message", fs("Static message").get());
}
@@ -357,9 +373,9 @@ class Utils_Test extends TestBase {
//
====================================================================================================
@Test
- @DisplayName("getMatchPattern() - Basic wildcard patterns")
- void g01_getMatchPatternBasicWildcards() {
- var pattern = getMatchPattern("user_*_temp");
+ @DisplayName("getGlobMatchPattern() - Basic wildcard patterns")
+ void g01_getGlobMatchPatternBasicWildcards() {
+ var pattern = getGlobMatchPattern("user_*_temp");
assertTrue(pattern.matcher("user_alice_temp").matches());
assertTrue(pattern.matcher("user_bob_temp").matches());
assertTrue(pattern.matcher("user__temp").matches()); // Empty
match
@@ -368,9 +384,9 @@ class Utils_Test extends TestBase {
}
@Test
- @DisplayName("getMatchPattern() - Question mark patterns")
- void g02_getMatchPatternQuestionMark() {
- var pattern = getMatchPattern("file?.txt");
+ @DisplayName("getGlobMatchPattern() - Question mark patterns")
+ void g02_getGlobMatchPatternQuestionMark() {
+ var pattern = getGlobMatchPattern("file?.txt");
assertTrue(pattern.matcher("file1.txt").matches());
assertTrue(pattern.matcher("fileA.txt").matches());
assertTrue(pattern.matcher("file_.txt").matches());
@@ -379,9 +395,9 @@ class Utils_Test extends TestBase {
}
@Test
- @DisplayName("getMatchPattern() - Combined wildcards")
- void g03_getMatchPatternCombined() {
- var pattern = getMatchPattern("test_*.?");
+ @DisplayName("getGlobMatchPattern() - Combined wildcards")
+ void g03_getGlobMatchPatternCombined() {
+ var pattern = getGlobMatchPattern("test_*.?");
assertTrue(pattern.matcher("test_data.1").matches());
assertTrue(pattern.matcher("test_long_filename.x").matches());
assertTrue(pattern.matcher("test_.a").matches());
@@ -390,48 +406,48 @@ class Utils_Test extends TestBase {
}
@Test
- @DisplayName("getMatchPattern() - Special regex characters")
- void g04_getMatchPatternSpecialChars() {
+ @DisplayName("getGlobMatchPattern() - Special regex characters")
+ void g04_getGlobMatchPatternSpecialChars() {
// Test that regex special characters are properly escaped
- var pattern = getMatchPattern("file[1].txt");
+ var pattern = getGlobMatchPattern("file[1].txt");
assertTrue(pattern.matcher("file[1].txt").matches());
assertFalse(pattern.matcher("file1.txt").matches()); //
Should not match as character class
- pattern = getMatchPattern("amount$100");
+ pattern = getGlobMatchPattern("amount$100");
assertTrue(pattern.matcher("amount$100").matches());
assertFalse(pattern.matcher("amount100").matches()); //
Should require the $ character
}
@Test
- @DisplayName("getMatchPattern() - With flags")
- void g05_getMatchPatternWithFlags() {
- var pattern = getMatchPattern("USER_*",
Pattern.CASE_INSENSITIVE);
+ @DisplayName("getGlobMatchPattern() - With flags")
+ void g05_getGlobMatchPatternWithFlags() {
+ var pattern = getGlobMatchPattern("USER_*",
Pattern.CASE_INSENSITIVE);
assertTrue(pattern.matcher("user_alice").matches());
assertTrue(pattern.matcher("USER_BOB").matches());
assertTrue(pattern.matcher("User_Charlie").matches());
}
@Test
- @DisplayName("getMatchPattern() - Null input")
- void g06_getMatchPatternNull() {
- assertNull(getMatchPattern(null));
- assertNull(getMatchPattern(null, Pattern.CASE_INSENSITIVE));
+ @DisplayName("getGlobMatchPattern() - Null input")
+ void g06_getGlobMatchPatternNull() {
+ assertNull(getGlobMatchPattern(null));
+ assertNull(getGlobMatchPattern(null, Pattern.CASE_INSENSITIVE));
}
@Test
- @DisplayName("getMatchPattern() - Empty and edge cases")
- void g07_getMatchPatternEdgeCases() {
+ @DisplayName("getGlobMatchPattern() - Empty and edge cases")
+ void g07_getGlobMatchPatternEdgeCases() {
// Empty string
- var pattern = getMatchPattern("");
+ var pattern = getGlobMatchPattern("");
assertTrue(pattern.matcher("").matches());
assertFalse(pattern.matcher("a").matches());
// Only wildcards
- pattern = getMatchPattern("*");
+ pattern = getGlobMatchPattern("*");
assertTrue(pattern.matcher("").matches());
assertTrue(pattern.matcher("anything").matches());
- pattern = getMatchPattern("?");
+ pattern = getGlobMatchPattern("?");
assertTrue(pattern.matcher("a").matches());
assertFalse(pattern.matcher("").matches());
assertFalse(pattern.matcher("ab").matches());