1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.apache.juneau;
18
19 import static java.util.stream.Collectors.*;
20 import static org.junit.jupiter.api.Assertions.*;
21
22 import java.io.*;
23 import java.lang.reflect.*;
24 import java.net.*;
25 import java.util.*;
26 import java.util.function.*;
27 import java.util.regex.*;
28 import java.util.stream.*;
29
30 import org.apache.juneau.annotation.*;
31 import org.apache.juneau.bean.swagger.*;
32 import org.apache.juneau.common.utils.Utils;
33 import org.apache.juneau.internal.*;
34 import org.apache.juneau.junit.bct.*;
35 import org.apache.juneau.marshaller.*;
36 import org.apache.juneau.rest.*;
37 import org.apache.juneau.rest.mock.*;
38 import org.apache.juneau.serializer.*;
39 import org.apache.juneau.xml.*;
40 import org.junit.jupiter.api.*;
41
42 /**
43 * Comprehensive utility class for Bean-Centric Tests (BCT) and general testing operations.
44 *
45 * <p>This class provides the core testing infrastructure for Apache Juneau, with particular emphasis
46 * on the Bean-Centric Testing (BCT) framework. BCT enables sophisticated assertion patterns for
47 * testing object properties, collections, maps, and complex nested structures with minimal code.</p>
48 *
49 * <h5 class='section'>Bean-Centric Testing (BCT) Framework:</h5>
50 * <p>The BCT framework consists of several key components:</p>
51 * <ul>
52 * <li><b>{@link BeanConverter}:</b> Core interface for object conversion and property access</li>
53 * <li><b>{@link BasicBeanConverter}:</b> Default implementation with extensible type handlers</li>
54 * <li><b>Assertion Methods:</b> High-level testing methods that leverage the converter framework</li>
55 * </ul>
56 *
57 * <h5 class='section'>Primary BCT Assertion Methods:</h5>
58 * <dl>
59 * <dt><b>{@link #assertBean(Object, String, String)}</b></dt>
60 * <dd>Tests object properties with nested syntax support and collection iteration</dd>
61 *
62 * <dt><b>{@link #assertMap(Map, String, String)}</b></dt>
63 * <dd>Tests map entries with the same nested property syntax as assertBean</dd>
64 *
65 * <dt><b>{@link #assertMapped(Object, java.util.function.BiFunction, String, String)}</b></dt>
66 * <dd>Tests custom property access using BiFunction for non-standard objects</dd>
67 *
68 * <dt><b>{@link #assertList(List, Object...)}</b></dt>
69 * <dd>Tests list/collection elements with varargs for expected values</dd>
70 *
71 * <dt><b>{@link #assertBeans(Collection, String, String...)}</b></dt>
72 * <dd>Tests collections of objects by extracting and comparing specific fields</dd>
73 * </dl>
74 *
75 * <h5 class='section'>BCT Advanced Features:</h5>
76 * <ul>
77 * <li><b>Nested Property Syntax:</b> "address{street,city}" for testing nested objects</li>
78 * <li><b>Collection Iteration:</b> "#{property}" syntax for testing all elements</li>
79 * <li><b>Universal Size Properties:</b> "length" and "size" work on all collection types</li>
80 * <li><b>Array/List Access:</b> Numeric indices for element-specific testing</li>
81 * <li><b>Method Chaining:</b> Fluent setters can be tested directly</li>
82 * <li><b>Direct Field Access:</b> Public fields accessed without getters</li>
83 * <li><b>Map Key Access:</b> Including special "<NULL>" syntax for null keys</li>
84 * </ul>
85 *
86 * <h5 class='section'>Converter Extensibility:</h5>
87 * <p>The BCT framework is built on the extensible {@link BasicBeanConverter} which allows:</p>
88 * <ul>
89 * <li><b>Custom Stringifiers:</b> Type-specific string conversion logic</li>
90 * <li><b>Custom Listifiers:</b> Collection-type conversion for iteration</li>
91 * <li><b>Custom Swapifiers:</b> Object transformation before conversion</li>
92 * <li><b>Configurable Settings:</b> Formatting, delimiters, and display options</li>
93 * </ul>
94 *
95 * <h5 class='section'>Usage Examples:</h5>
96 *
97 * <p><b>Basic Property Testing:</b></p>
98 * <p class='bjava'>
99 * <jc>// Test multiple properties</jc>
100 * assertBean(user, <js>"name,age,active"</js>, <js>"John,30,true"</js>);
101 *
102 * <jc>// Test nested properties</jc>
103 * assertBean(user, <js>"address{street,city}"</js>, <js>"{123 Main St,Springfield}"</js>);
104 * </p>
105 *
106 * <p><b>Collection and Array Testing:</b></p>
107 * <p class='bjava'>
108 * <jc>// Test collection size and iterate over all elements</jc>
109 * assertBean(order, <js>"items{length,#{name}}"</js>, <js>"{3,[{Laptop},{Phone},{Tablet}]}"</js>);
110 *
111 * <jc>// Test specific array elements</jc>
112 * assertBean(data, <js>"values{0,1,2}"</js>, <js>"{100,200,300}"</js>);
113 * </p>
114 *
115 * <p><b>Map and Collection Testing:</b></p>
116 * <p class='bjava'>
117 * <jc>// Test map entries</jc>
118 * assertMap(config, <js>"timeout,retries"</js>, <js>"30000,3"</js>);
119 *
120 * <jc>// Test list elements</jc>
121 * assertList(tags, <js>"red"</js>, <js>"green"</js>, <js>"blue"</js>);
122 * </p>
123 *
124 * <p><b>Custom Property Access:</b></p>
125 * <p class='bjava'>
126 * <jc>// Test with custom accessor function</jc>
127 * assertMapped(myObject, (obj, prop) -> obj.getProperty(prop),
128 * <js>"prop1,prop2"</js>, <js>"value1,value2"</js>);
129 * </p>
130 *
131 * <h5 class='section'>Performance and Thread Safety:</h5>
132 * <p>The BCT framework is designed for high performance with:</p>
133 * <ul>
134 * <li><b>Caching:</b> Type-to-handler mappings cached for fast lookup</li>
135 * <li><b>Thread Safety:</b> All operations are thread-safe for concurrent testing</li>
136 * <li><b>Minimal Allocation:</b> Efficient object reuse and minimal temporary objects</li>
137 * </ul>
138 *
139 * @see BeanConverter
140 * @see BasicBeanConverter
141 */
142 public class TestUtils extends Utils2 {
143
144 private static final ThreadLocal<TimeZone> SYSTEM_TIME_ZONE = new ThreadLocal<>();
145
146 public static final ThreadLocal<Locale> SYSTEM_LOCALE = new ThreadLocal<>();
147
148 /**
149 * Asserts that the fields/properties on the specified bean are the specified values after being converted to {@link Utils#r readable} strings.
150 *
151 * <p>This is the primary method for Bean-Centric Tests (BCT), supporting extensive property validation
152 * patterns including nested objects, collections, arrays, method chaining, direct field access, collection iteration
153 * with <js>#{property}</js> syntax, and universal <js>length</js>/<js>size</js> properties for all collection types.</p>
154 *
155 * <p>The method uses the {@link BasicBeanConverter#DEFAULT} converter internally for object introspection
156 * and value extraction. The converter provides sophisticated property access through the {@link BeanConverter}
157 * interface, supporting multiple fallback mechanisms for accessing object properties and values.</p>
158 *
159 * <h5 class='section'>Basic Usage:</h5>
160 * <p class='bjava'>
161 * <jc>// Test multiple properties</jc>
162 * assertBean(myBean, <js>"prop1,prop2,prop3"</js>, <js>"val1,val2,val3"</js>);
163 *
164 * <jc>// Test single property</jc>
165 * assertBean(myBean, <js>"name"</js>, <js>"John"</js>);
166 * </p>
167 *
168 * <h5 class='section'>Nested Property Testing:</h5>
169 * <p class='bjava'>
170 * <jc>// Test nested bean properties</jc>
171 * assertBean(myBean, <js>"address{street,city,state}"</js>, <js>"{123 Main St,Springfield,IL}"</js>);
172 *
173 * <jc>// Test arbitrarily deep nesting</jc>
174 * assertBean(myBean, <js>"person{address{geo{lat,lon}}}"</js>, <js>"{{{{40.7,-74.0}}}}"</js>);
175 * </p>
176 *
177 * <h5 class='section'>Array, List, and Stream Testing:</h5>
178 * <p class='bjava'>
179 * <jc>// Test array/list elements by index</jc>
180 * assertBean(myBean, <js>"items{0,1,2}"</js>, <js>"{item1,item2,item3}"</js>);
181 *
182 * <jc>// Test nested properties within array elements</jc>
183 * assertBean(myBean, <js>"orders{0{id,total}}"</js>, <js>"{{123,99.95}}"</js>);
184 *
185 * <jc>// Test array length property</jc>
186 * assertBean(myBean, <js>"items{length}"</js>, <js>"{5}"</js>);
187 *
188 * <jc>// Works with any iterable type including Streams</jc>
189 * assertBean(myBean, <js>"userStream{#{name}}"</js>, <js>"[{Alice},{Bob}]"</js>);
190 * </p>
191 *
192 * <h5 class='section'>Collection Iteration Syntax:</h5>
193 * <p class='bjava'>
194 * <jc>// Test properties across ALL elements in a collection using #{...} syntax</jc>
195 * assertBean(myBean, <js>"userList{#{name}}"</js>, <js>"[{John},{Jane},{Bob}]"</js>);
196 *
197 * <jc>// Test multiple properties from each element</jc>
198 * assertBean(myBean, <js>"orderList{#{id,status}}"</js>, <js>"[{123,ACTIVE},{124,PENDING}]"</js>);
199 *
200 * <jc>// Works with nested properties within each element</jc>
201 * assertBean(myBean, <js>"customers{#{address{city}}}"</js>, <js>"[{{New York}},{{Los Angeles}}]"</js>);
202 *
203 * <jc>// Works with arrays and any iterable collection type (including Streams)</jc>
204 * assertBean(config, <js>"itemArray{#{type}}"</js>, <js>"[{String},{Integer},{Boolean}]"</js>);
205 * assertBean(data, <js>"statusSet{#{name}}"</js>, <js>"[{ACTIVE},{PENDING},{CANCELLED}]"</js>);
206 * assertBean(processor, <js>"dataStream{#{value}}"</js>, <js>"[{A},{B},{C}]"</js>);
207 * </p>
208 *
209 * <h5 class='section'>Universal Collection Size Properties:</h5>
210 * <p class='bjava'>
211 * <jc>// Both 'length' and 'size' work universally across all collection types</jc>
212 * assertBean(myBean, <js>"myArray{length}"</js>, <js>"{5}"</js>); <jc>// Arrays</jc>
213 * assertBean(myBean, <js>"myArray{size}"</js>, <js>"{5}"</js>); <jc>// Also works for arrays</jc>
214 *
215 * assertBean(myBean, <js>"myList{size}"</js>, <js>"{3}"</js>); <jc>// Collections</jc>
216 * assertBean(myBean, <js>"myList{length}"</js>, <js>"{3}"</js>); <jc>// Also works for collections</jc>
217 *
218 * assertBean(myBean, <js>"myMap{size}"</js>, <js>"{7}"</js>); <jc>// Maps</jc>
219 * assertBean(myBean, <js>"myMap{length}"</js>, <js>"{7}"</js>); <jc>// Also works for maps</jc>
220 * </p>
221 *
222 * <h5 class='section'>Class Name Testing:</h5>
223 * <p class='bjava'>
224 * <jc>// Test class properties (prefer simple names for maintainability)</jc>
225 * assertBean(myBean, <js>"obj{class{simpleName}}"</js>, <js>"{{MyClass}}"</js>);
226 *
227 * <jc>// Test full class names when needed</jc>
228 * assertBean(myBean, <js>"obj{class{name}}"</js>, <js>"{{com.example.MyClass}}"</js>);
229 * </p>
230 *
231 * <h5 class='section'>Method Chaining Support:</h5>
232 * <p class='bjava'>
233 * <jc>// Test fluent setter chains (returns same object)</jc>
234 * assertBean(
235 * item.setType(<js>"foo"</js>).setFormat(<js>"bar"</js>).setDefault(<js>"baz"</js>),
236 * <js>"type,format,default"</js>,
237 * <js>"foo,bar,baz"</js>
238 * );
239 * </p>
240 *
241 * <h5 class='section'>Advanced Collection Analysis:</h5>
242 * <p class='bjava'>
243 * <jc>// Combine size/length, metadata, and content iteration in single assertions</jc>
244 * assertBean(myBean, <js>"users{length,class{simpleName},#{name}}"</js>,
245 * <js>"{3,{ArrayList},[{John},{Jane},{Bob}]}"</js>);
246 *
247 * <jc>// Comprehensive collection validation with multiple iteration patterns</jc>
248 * assertBean(order, <js>"items{size,#{name},#{price}}"</js>,
249 * <js>"{3,[{Laptop},{Phone},{Tablet}],[{999.99},{599.99},{399.99}]}"</js>);
250 *
251 * <jc>// Perfect for validation testing - verify error count and details</jc>
252 * assertBean(result, <js>"errors{length,#{field},#{code}}"</js>,
253 * <js>"{2,[{email},{password}],[{E001},{E002}]}"</js>);
254 *
255 * <jc>// Mixed collection types with consistent syntax</jc>
256 * assertBean(response, <js>"results{size},metadata{length}"</js>, <js>"{25},{4}"</js>);
257 * </p>
258 *
259 * <h5 class='section'>Direct Field Access:</h5>
260 * <p class='bjava'>
261 * <jc>// Test public fields directly (no getters required)</jc>
262 * assertBean(myBean, <js>"f1,f2,f3"</js>, <js>"val1,val2,val3"</js>);
263 *
264 * <jc>// Test field properties with chaining</jc>
265 * assertBean(myBean, <js>"f1{length},f2{class{simpleName}}"</js>, <js>"{5},{{String}}"</js>);
266 * </p>
267 *
268 * <h5 class='section'>Map Testing:</h5>
269 * <p class='bjava'>
270 * <jc>// Test map values by key</jc>
271 * assertBean(myBean, <js>"configMap{timeout,retries}"</js>, <js>"{30000,3}"</js>);
272 *
273 * <jc>// Test map size</jc>
274 * assertBean(myBean, <js>"settings{size}"</js>, <js>"{5}"</js>);
275 *
276 * <jc>// Test null keys using special <NULL> syntax</jc>
277 * assertBean(myBean, <js>"mapWithNullKey{<NULL>}"</js>, <js>"{nullKeyValue}"</js>);
278 * </p>
279 *
280 * <h5 class='section'>Collection and Boolean Values:</h5>
281 * <p class='bjava'>
282 * <jc>// Test boolean values</jc>
283 * assertBean(myBean, <js>"enabled,visible"</js>, <js>"true,false"</js>);
284 *
285 * <jc>// Test enum collections</jc>
286 * assertBean(myBean, <js>"statuses"</js>, <js>"[ACTIVE,PENDING]"</js>);
287 * </p>
288 *
289 * <h5 class='section'>Value Syntax Rules:</h5>
290 * <ul>
291 * <li><b>Simple values:</b> <js>"value"</js> for direct property values</li>
292 * <li><b>Nested values:</b> <js>"{value}"</js> for single-level nested properties</li>
293 * <li><b>Deep nested values:</b> <js>"{{value}}"</js>, <js>"{{{value}}}"</js> for multiple nesting levels</li>
294 * <li><b>Array/Collection values:</b> <js>"[item1,item2]"</js> for collections</li>
295 * <li><b>Collection iteration:</b> <js>"#{property}"</js> iterates over ALL collection elements, returns <js>"[{val1},{val2}]"</js></li>
296 * <li><b>Universal size properties:</b> <js>"length"</js> and <js>"size"</js> work on arrays, collections, and maps</li>
297 * <li><b>Boolean values:</b> <js>"true"</js>, <js>"false"</js></li>
298 * <li><b>Null values:</b> <js>"null"</js></li>
299 * </ul>
300 *
301 * <h5 class='section'>Property Access Priority:</h5>
302 * <ol>
303 * <li><b>Collection/Array access:</b> Numeric indices for arrays/lists (e.g., <js>"0"</js>, <js>"1"</js>)</li>
304 * <li><b>Universal size properties:</b> <js>"length"</js> and <js>"size"</js> for arrays, collections, and maps</li>
305 * <li><b>Map key access:</b> Direct key lookup for Map objects (including <js>"<NULL>"</js> for null keys)</li>
306 * <li><b>is{Property}()</b> methods (for boolean properties)</li>
307 * <li><b>get{Property}()</b> methods</li>
308 * <li><b>Public fields</b> (direct field access)</li>
309 * </ol>
310 *
311 * @param actual The bean object to test. Must not be null.
312 * @param fields Comma-delimited list of property names to test. Supports nested syntax with {}.
313 * @param expected Comma-delimited list of expected values. Must match the order of fields.
314 * @throws NullPointerException if the bean is null
315 * @throws AssertionError if any property values don't match expected values
316 * @see BeanConverter
317 * @see BasicBeanConverter
318 */
319 public static void assertBean(Object actual, String fields, String expected) {
320 BctAssertions.assertBean(actual, fields, expected);
321 }
322
323 /**
324 * Asserts that multiple beans in a collection have the expected property values.
325 *
326 * <p>This method validates that each bean in a collection has the specified property values,
327 * using the same property access logic as {@link #assertBean(Object, String, String)}.
328 * It's perfect for testing collections of similar objects or validation results.</p>
329 *
330 * <h5 class='section'>Basic Usage:</h5>
331 * <p class='bjava'>
332 * <jc>// Test list of user beans</jc>
333 * assertBeans(userList, <js>"name,age"</js>,
334 * <js>"John,25"</js>, <js>"Jane,30"</js>, <js>"Bob,35"</js>);
335 * </p>
336 *
337 * <h5 class='section'>Complex Property Testing:</h5>
338 * <p class='bjava'>
339 * <jc>// Test nested properties across multiple beans</jc>
340 * assertBeans(orderList, <js>"id,customer{name,email}"</js>,
341 * <js>"1,{John,john@example.com}"</js>,
342 * <js>"2,{Jane,jane@example.com}"</js>);
343 *
344 * <jc>// Test collection properties within beans</jc>
345 * assertBeans(cartList, <js>"items{0{name}},total"</js>,
346 * <js>"{{Laptop}},999.99"</js>,
347 * <js>"{{Phone}},599.99"</js>);
348 * </p>
349 *
350 * <h5 class='section'>Validation Testing:</h5>
351 * <p class='bjava'>
352 * <jc>// Test validation results</jc>
353 * assertBeans(validationErrors, <js>"field,message,code"</js>,
354 * <js>"email,Invalid email format,E001"</js>,
355 * <js>"age,Must be 18 or older,E002"</js>);
356 * </p>
357 *
358 * <h5 class='section'>Collection Iteration Testing:</h5>
359 * <p class='bjava'>
360 * <jc>// Test collection iteration within beans (#{...} syntax)</jc>
361 * assertBeans(departmentList, <js>"name,employees{#{name}}"</js>,
362 * <js>"Engineering,[{Alice},{Bob},{Charlie}]"</js>,
363 * <js>"Marketing,[{David},{Eve}]"</js>);
364 * </p>
365 *
366 * <h5 class='section'>Parser Result Testing:</h5>
367 * <p class='bjava'>
368 * <jc>// Test parsed object collections</jc>
369 * var parsed = JsonParser.DEFAULT.parse(jsonArray, MyBean[].class);
370 * assertBeans(Arrays.asList(parsed), <js>"prop1,prop2"</js>,
371 * <js>"val1,val2"</js>, <js>"val3,val4"</js>);
372 * </p>
373 *
374 * @param listOfBeans The collection of beans to check. Must not be null.
375 * @param fields A comma-delimited list of bean property names (supports nested syntax).
376 * @param values Array of expected value strings, one per bean. Each string contains comma-delimited values matching the fields.
377 * @throws AssertionError if the collection size doesn't match values array length or if any bean properties don't match
378 * @see #assertBean(Object, String, String)
379 */
380 public static void assertBeans(Object actual, String fields, String...expected) {
381 BctAssertions.assertBeans(actual, fields, expected);
382 }
383
384
385 /**
386 * Asserts that a List contains the expected values using flexible comparison logic.
387 *
388 * <p>This is the primary method for testing all collection-like types. For non-List collections, use
389 * {@link #l(Object)} to convert them to Lists first. This unified approach eliminates the need for
390 * separate assertion methods for arrays, sets, and other collection types.</p>
391 *
392 * <h5 class='section'>Testing Non-List Collections:</h5>
393 * <p class='bjava'>
394 * <jc>// Test a Set using l() conversion</jc>
395 * Set<String> <jv>mySet</jv> = Set.of(<js>"a"</js>, <js>"b"</js>, <js>"c"</js>);
396 * assertList(l(<jv>mySet</jv>), <js>"a"</js>, <js>"b"</js>, <js>"c"</js>);
397 *
398 * <jc>// Test an array using l() conversion</jc>
399 * String[] <jv>myArray</jv> = {<js>"x"</js>, <js>"y"</js>, <js>"z"</js>};
400 * assertList(l(<jv>myArray</jv>), <js>"x"</js>, <js>"y"</js>, <js>"z"</js>);
401 *
402 * <jc>// Test a Stream using l() conversion</jc>
403 * Stream<String> <jv>myStream</jv> = Stream.of(<js>"foo"</js>, <js>"bar"</js>);
404 * assertList(l(<jv>myStream</jv>), <js>"foo"</js>, <js>"bar"</js>);
405 * </p>
406 *
407 * <h5 class='section'>Comparison Modes:</h5>
408 * <p>The method supports three different ways to compare expected vs actual values:</p>
409 *
410 * <h6 class='section'>1. String Comparison (Readable Format):</h6>
411 * <p class='bjava'>
412 * <jc>// Elements are converted to {@link Utils#r readable} format and compared as strings</jc>
413 * assertList(List.of(1, 2, 3), <js>"1"</js>, <js>"2"</js>, <js>"3"</js>);
414 * assertList(List.of("a", "b"), <js>"a"</js>, <js>"b"</js>);
415 * </p>
416 *
417 * <h6 class='section'>2. Predicate Testing (Functional Validation):</h6>
418 * <p class='bjava'>
419 * <jc>// Use Predicate<T> for functional testing</jc>
420 * Predicate<Integer> <jv>greaterThanOne</jv> = <jv>x</jv> -> <jv>x</jv> > 1;
421 * assertList(List.of(2, 3, 4), <jv>greaterThanOne</jv>, <jv>greaterThanOne</jv>, <jv>greaterThanOne</jv>);
422 *
423 * <jc>// Mix predicates with other comparison types</jc>
424 * Predicate<String> <jv>startsWithA</jv> = <jv>s</jv> -> <jv>s</jv>.startsWith(<js>"a"</js>);
425 * assertList(List.of(<js>"apple"</js>, <js>"banana"</js>), <jv>startsWithA</jv>, <js>"banana"</js>);
426 * </p>
427 *
428 * <h6 class='section'>3. Object Equality (Direct Comparison):</h6>
429 * <p class='bjava'>
430 * <jc>// Non-String, non-Predicate objects use Objects.equals() comparison</jc>
431 * assertList(List.of(1, 2, 3), 1, 2, 3); <jc>// Integer objects</jc>
432 * assertList(List.of(<jv>myBean1</jv>, <jv>myBean2</jv>), <jv>myBean1</jv>, <jv>myBean2</jv>); <jc>// Custom objects</jc>
433 * </p>
434 *
435 * @param actual The List to test. Must not be null. For other collection types, use {@link #l(Object)} to convert first.
436 * @param expected Multiple arguments of expected values.
437 * Can be Strings (readable format comparison), Predicates (functional testing), or Objects (direct equality).
438 * @throws AssertionError if the List size or contents don't match expected values
439 * @see #l(Object) for converting other collection types to Lists
440 */
441 public static <T> void assertList(Object actual, Object...expected) {
442 BctAssertions.assertList(actual, expected);
443 }
444
445 /**
446 * Asserts that a Map contains the expected key/value pairs using flexible comparison logic.
447 *
448 * <p>This is a passthrough method to {@link BctAssertions#assertMap(Map, Object...)}.
449 * Map entries are serialized to strings as key/value pairs in the format <js>"key=value"</js>.
450 * Nested maps and collections are supported with appropriate formatting.</p>
451 *
452 * <h5 class='section'>Usage Examples:</h5>
453 * <p class='bjava'>
454 * <jc>// Test simple map entries</jc>
455 * Map<String,String> <jv>simpleMap</jv> = Map.<jsm>of</jsm>(<js>"a"</js>, <js>"1"</js>, <js>"b"</js>, <js>"2"</js>);
456 * <jsm>assertMap</jsm>(<jv>simpleMap</jv>, <js>"a=1"</js>, <js>"b=2"</js>);
457 *
458 * <jc>// Test nested maps</jc>
459 * Map<String,Map<String,Integer>> <jv>nestedMap</jv> = Map.<jsm>of</jsm>(<js>"a"</js>, Map.<jsm>of</jsm>(<js>"b"</js>, 1));
460 * <jsm>assertMap</jsm>(<jv>nestedMap</jv>, <js>"a={b=1}"</js>);
461 *
462 * <jc>// Test maps with arrays/collections</jc>
463 * Map<String,Map<String,Integer[]>> <jv>mapWithArrays</jv> = Map.<jsm>of</jsm>(<js>"a"</js>, Map.<jsm>of</jsm>(<js>"b"</js>, <jk>new</jk> Integer[]{1,2}));
464 * <jsm>assertMap</jsm>(<jv>mapWithArrays</jv>, <js>"a={b=[1,2]}"</js>);
465 * </p>
466 *
467 * @param actual The Map to test. Must not be null.
468 * @param expected Multiple arguments of expected map entries.
469 * Can be Strings (readable format comparison), Predicates (functional testing), or Objects (direct equality).
470 * @throws AssertionError if the Map size or contents don't match expected values
471 * @see BctAssertions#assertMap(Map, Object...)
472 */
473 public static void assertMap(Map<?,?> actual, Object...expected) {
474 BctAssertions.assertMap(actual, expected);
475 }
476
477 /**
478 * Asserts an object matches the expected string after it's been made {@link Utils#r readable}.
479 */
480 public static void assertContains(String expected, Object actual) {
481 BctAssertions.assertContains(expected, actual);
482 }
483
484 /**
485 * Similar to {@link #assertContains(String, Object)} but allows the expected to be a comma-delimited list of strings that
486 * all must match.
487 * @param expected
488 * @param actual
489 */
490 public static void assertContainsAll(Object actual, String...expected) {
491 BctAssertions.assertContainsAll(actual, expected);
492 }
493
494 /**
495 * Asserts that a collection is not null and empty.
496 */
497 public static void assertEmpty(Object value) {
498 BctAssertions.assertEmpty(value);
499 }
500
501 public static void assertEqualsAll(Object...values) {
502 for (var i = 1; i < values.length; i++) {
503 assertEquals(values[0], values[i], fms("Elements at index {0} and {1} did not match. {0}={2}, {1}={3}", 0, i, r(values[0]), r(values[i])));
504 }
505 }
506
507 /**
508 * Asserts the JSON5 representation of the specified object.
509 */
510 public static void assertJson(String expected, Object value) {
511 assertEquals(expected, Json5.DEFAULT_SORTED.write(value));
512 }
513
514 /**
515 * Converts the specified object to a string and then replaces any newlines with pipes for easy comparison during testing.
516 * @param value
517 * @return
518 */
519 public static String pipedLines(Object value) {
520 return r(value).replaceAll("\\r?\\n", "|");
521 }
522
523 /**
524 * Asserts that the values in the specified map are the specified values after being converted to {@link Utils#r readable} strings.
525 *
526 * <p>This method works identically to {@link #assertBean(Object, String, String)} but is optimized for Java Maps.
527 * It supports the same nested property syntax and value formatting rules as <c>assertBean</c>.</p>
528 *
529 * <h5 class='section'>Basic Map Testing:</h5>
530 * <p class='bjava'>
531 * <jc>// Test map entries</jc>
532 * assertMap(myMap, <js>"key1,key2,key3"</js>, <js>"val1,val2,val3"</js>);
533 *
534 * <jc>// Test single map entry</jc>
535 * assertMap(myMap, <js>"status"</js>, <js>"active"</js>);
536 * </p>
537 *
538 * <h5 class='section'>Nested Object Testing in Maps:</h5>
539 * <p class='bjava'>
540 * <jc>// Test nested objects within map values</jc>
541 * assertMap(myMap, <js>"user{name,email}"</js>, <js>"{John,john@example.com}"</js>);
542 *
543 * <jc>// Test class properties of map values</jc>
544 * assertMap(myMap, <js>"items{class{simpleName}}"</js>, <js>"{{ArrayList}}"</js>);
545 * </p>
546 *
547 * <h5 class='section'>Collection Testing in Maps:</h5>
548 * <p class='bjava'>
549 * <jc>// Test array/list values in maps</jc>
550 * assertMap(myMap, <js>"tags{0,1},count"</js>, <js>"{red,blue},2"</js>);
551 *
552 * <jc>// Test nested properties within collection elements</jc>
553 * assertMap(myMap, <js>"orders{0{id,total}}"</js>, <js>"{{123,99.95}}"</js>);
554 * </p>
555 *
556 * <h5 class='section'>BeanMap and JsonMap Usage:</h5>
557 * <p class='bjava'>
558 * <jc>// Excellent for testing BeanMap instances</jc>
559 * var beanMap = BeanContext.DEFAULT.toBeanMap(myBean);
560 * assertMap(beanMap, <js>"name,age,active"</js>, <js>"John,30,true"</js>);
561 *
562 * <jc>// Test JsonMap parsing results</jc>
563 * var jsonMap = JsonMap.ofJson(<js>"{foo:'bar', baz:123}"</js>);
564 * assertMap(jsonMap, <js>"foo,baz"</js>, <js>"bar,123"</js>);
565 * </p>
566 *
567 * @param actual The Map object to test. Must not be null.
568 * @param fields Comma-delimited list of map keys to test. Supports nested syntax with {}.
569 * @param expected Comma-delimited list of expected values. Must match the order of fields.
570 * @throws NullPointerException if the map is null
571 * @throws AssertionError if any map values don't match expected values
572 * @see #assertBean(Object, String, String)
573 * @see BeanConverter
574 * @see BasicBeanConverter
575 */
576 public static void assertMap(Map<?,?> actual, String fields, String expected) {
577 BctAssertions.assertBean(actual, fields, expected);
578 }
579
580 /**
581 * Asserts that mapped property access on an object returns expected values using a custom BiFunction.
582 *
583 * <p>This is the most powerful and flexible BCT method, designed for testing objects that don't follow
584 * standard JavaBean patterns or require custom property access logic. The BiFunction allows complete
585 * control over how properties are retrieved from the target object.</p>
586 *
587 * <p>When the BiFunction throws an exception, it's automatically caught and the exception's
588 * simple class name becomes the property value for comparison (e.g., "NullPointerException").</p>
589 *
590 * <p>This method creates an intermediate LinkedHashMap to collect all property values before
591 * delegating to assertMap(Map, String, String). This ensures consistent ordering
592 * and supports the full nested property syntax. The {@link BasicBeanConverter#DEFAULT} is used
593 * for value stringification and nested property access.</p>
594 *
595 * @param <T> The type of object being tested
596 * @param actual The object to test properties on
597 * @param f The BiFunction that extracts property values. Receives (object, propertyName) and returns the property value.
598 * @param properties Comma-delimited list of property names to test
599 * @param expected Comma-delimited list of expected values (exceptions become simple class names)
600 * @throws AssertionError if any mapped property values don't match expected values
601 * @see #assertBean(Object, String, String)
602 * @see #assertMap(Map, String, String)
603 * @see BeanConverter
604 * @see BasicBeanConverter
605 */
606 public static <T> void assertMapped(T actual, BiFunction<T,String,Object> f, String properties, String expected) {
607 BctAssertions.assertMapped(actual, f, properties, expected);
608 }
609
610 /**
611 * Asserts that a collection is not null and not empty.
612 */
613 public static void assertNotEmpty(Object value) {
614 BctAssertions.assertNotEmpty(value);
615 }
616
617 public static void assertNotEqualsAny(Object actual, Object...values) {
618 assertNotNull(actual, "Value was null.");
619 for (var i = 0; i < values.length; i++) {
620 assertNotEquals(values[i], actual, fms("Element at index {0} unexpectedly matched. expected={1}, actual={2}", i, values[i], s(actual)));
621 }
622 }
623
624 /**
625 * Asserts the serialized representation of the specified object.
626 */
627 public static void assertSerialized(Object actual, WriterSerializer s, String expected) {
628 assertEquals(expected, s.toString(actual));
629 }
630
631 /**
632 * Asserts that a collection-like object or string is not null and of the specified size.
633 *
634 * <p>This method can validate the size of various types of objects:</p>
635 * <ul>
636 * <li><b>String:</b> Validates character length</li>
637 * <li><b>Collection-like objects:</b> Any object that can be converted to a List via {@link #toList(Object)}</li>
638 * </ul>
639 *
640 * <h5 class='section'>Usage Examples:</h5>
641 * <p class='bjava'>
642 * <jc>// Test string length</jc>
643 * assertSize(5, <js>"hello"</js>);
644 *
645 * <jc>// Test collection size</jc>
646 * assertSize(3, List.of(<js>"a"</js>, <js>"b"</js>, <js>"c"</js>));
647 *
648 * <jc>// Test array size</jc>
649 * assertSize(2, <jk>new</jk> String[]{<js>"x"</js>, <js>"y"</js>});
650 * </p>
651 *
652 * @param expected The expected size/length.
653 * @param actual The object to test. Must not be null.
654 * @throws AssertionError if the object is null or not the expected size.
655 */
656 public static void assertSize(int expected, Object actual) {
657 BctAssertions.assertSize(expected, actual);
658 }
659
660 /**
661 * Asserts an object matches the expected string after it's been made {@link Utils#r readable}.
662 */
663 public static void assertString(String expected, Object actual) {
664 BctAssertions.assertString(expected, actual);
665 }
666
667 /**
668 * Asserts value when stringified matches the specified glob-style pattern.
669 */
670 public static void assertMatchesGlob(String pattern, Object value) {
671 BctAssertions.assertMatchesGlob(pattern, value);
672 }
673
674 /**
675 * Asserts an object matches the expected string after it's been made {@link Utils#r readable}.
676 */
677 public static void assertString(String expected, Object actual, Supplier<String> messageSupplier) {
678 BctAssertions.assertString(expected, actual);
679 }
680
681 public static <T extends Throwable> T assertThrowable(Class<? extends Throwable> expectedType, String expectedSubstring, T t) {
682 var messages = getMessages(t);
683 assertTrue(messages.contains(expectedSubstring), fms("Expected message to contain: {0}.\nActual:\n{1}", expectedSubstring, messages));
684 return t;
685 }
686
687 public static <T extends Throwable> T assertThrowsWithMessage(Class<T> expectedType, List<String> expectedSubstrings, org.junit.jupiter.api.function.Executable executable) {
688 var exception = Assertions.assertThrows(expectedType, executable);
689 var messages = getMessages(exception);
690 expectedSubstrings.stream().forEach(x -> assertTrue(messages.contains(x), fms("Expected message to contain: {0}.\nActual:\n{1}", x, messages)));
691 return exception;
692 }
693
694 public static <T extends Throwable> T assertThrowsWithMessage(Class<T> expectedType, String expectedSubstring, org.junit.jupiter.api.function.Executable executable) {
695 var exception = Assertions.assertThrows(expectedType, executable);
696 var messages = getMessages(exception);
697 assertTrue(messages.contains(expectedSubstring), fms("Expected message to contain: {0}.\nActual:\n{1}", expectedSubstring, messages));
698 return exception;
699 }
700
701 /**
702 * Validates that the whitespace is correct in the specified XML.
703 */
704 public static final void checkXmlWhitespace(String out) throws SerializeException {
705 if (out.indexOf('\u0000') != -1) {
706 for (var s : out.split("\u0000"))
707 checkXmlWhitespace(s);
708 return;
709 }
710
711 var indent = -1;
712 var startTag = Pattern.compile("^(\\s*)<[^/>]+(\\s+\\S+=['\"]\\S*['\"])*\\s*>$"); // NOSONAR
713 var endTag = Pattern.compile("^(\\s*)</[^>]+>$");
714 var combinedTag = Pattern.compile("^(\\s*)<[^>/]+(\\s+\\S+=['\"]\\S*['\"])*\\s*/>$"); // NOSONAR
715 var contentOnly = Pattern.compile("^(\\s*)[^\\s\\<]+$");
716 var tagWithContent = Pattern.compile("^(\\s*)<[^>]+>.*</[^>]+>$");
717 var lines = out.split("\n");
718 try {
719 for (var i = 0; i < lines.length; i++) {
720 var line = lines[i];
721 var m = startTag.matcher(line);
722 if (m.matches()) {
723 indent++;
724 if (m.group(1).length() != indent)
725 throw new SerializeException("Wrong indentation detected on start tag line ''{0}''", i+1);
726 continue;
727 }
728 m = endTag.matcher(line);
729 if (m.matches()) {
730 if (m.group(1).length() != indent)
731 throw new SerializeException("Wrong indentation detected on end tag line ''{0}''", i+1);
732 indent--;
733 continue;
734 }
735 m = combinedTag.matcher(line);
736 if (m.matches()) {
737 indent++;
738 if (m.group(1).length() != indent)
739 throw new SerializeException("Wrong indentation detected on combined tag line ''{0}''", i+1);
740 indent--;
741 continue;
742 }
743 m = contentOnly.matcher(line);
744 if (m.matches()) {
745 indent++;
746 if (m.group(1).length() != indent)
747 throw new SerializeException("Wrong indentation detected on content-only line ''{0}''", i+1);
748 indent--;
749 continue;
750 }
751 m = tagWithContent.matcher(line);
752 if (m.matches()) {
753 indent++;
754 if (m.group(1).length() != indent)
755 throw new SerializeException("Wrong indentation detected on tag-with-content line ''{0}''", i+1);
756 indent--;
757 continue;
758 }
759 throw new SerializeException("Unmatched whitespace line at line number ''{0}''", i+1);
760 }
761 if (indent != -1)
762 throw new SerializeException("Possible unmatched tag. indent=''{0}''", indent);
763 } catch (SerializeException e) {
764 printLines(lines);
765 throw e;
766 }
767 }
768
769 /**
770 * Returns the value of the specified field/property on the specified object.
771 * First looks for getter, then looks for field.
772 * Methods and fields can be any visibility.
773 */
774 public static Object getBeanProp(Object o, String name) {
775 return safe(() -> {
776 var f = (Field)null;
777 var c = o.getClass();
778 var n = Character.toUpperCase(name.charAt(0)) + name.substring(1);
779 var m = Arrays.stream(c.getMethods()).filter(x -> x.getName().equals("is"+n) && x.getParameterCount() == 0 && x.getAnnotation(BeanIgnore.class) == null).findFirst().orElse(null);
780 if (m != null) {
781 m.setAccessible(true);
782 return m.invoke(o);
783 }
784 m = Arrays.stream(c.getMethods()).filter(x -> x.getName().equals("get"+n) && x.getParameterCount() == 0 && x.getAnnotation(BeanIgnore.class) == null).findFirst().orElse(null);
785 if (m != null) {
786 m.setAccessible(true);
787 return m.invoke(o);
788 }
789 m = Arrays.stream(c.getMethods()).filter(x -> x.getName().equals("get") && x.getParameterCount() == 1 && x.getParameterTypes()[0] == String.class && x.getAnnotation(BeanIgnore.class) == null).findFirst().orElse(null);
790 if (m != null) {
791 m.setAccessible(true);
792 return m.invoke(o, name);
793 }
794 var c2 = c;
795 while (f == null && c2 != null) {
796 f = Arrays.stream(c2.getDeclaredFields()).filter(x -> x.getName().equals(name)).findFirst().orElse(null);
797 c2 = c2.getSuperclass();
798 }
799 if (f != null) {
800 f.setAccessible(true);
801 return f.get(o);
802 }
803 m = Arrays.stream(c.getMethods()).filter(x -> x.getName().equals(name) && x.getParameterCount() == 0).findFirst().orElse(null);
804 if (m != null) {
805 m.setAccessible(true);
806 return m.invoke(o);
807 }
808 throw runtimeException("Property {0} not found on object of type {1}", name, classNameOf(o));
809 });
810 }
811
812 private static String getMessages(Throwable t) {
813 return Stream.iterate(t, Throwable::getCause).takeWhile(e -> e != null).map(Throwable::getMessage).collect(joining("\n"));
814 }
815
816 /**
817 * Gets the swagger for the specified @Resource-annotated object.
818 * @param c
819 * @return
820 */
821 public static Swagger getSwagger(Class<?> c) {
822 try {
823 var r = c.getDeclaredConstructor().newInstance();
824 var rc = RestContext.create(r.getClass(),null,null).init(()->r).build();
825 var ctx = RestOpContext.create(TestUtils.class.getMethod("getSwagger", Class.class), rc).build();
826 var session = RestSession.create(rc).resource(r).req(new MockServletRequest()).res(new MockServletResponse()).build();
827 var req = ctx.createRequest(session);
828 var ip = rc.getSwaggerProvider();
829 return ip.getSwagger(rc, req.getLocale());
830 } catch (Exception e) {
831 throw new RuntimeException(e);
832 }
833 }
834
835 /**
836 * Creates an input stream from the specified string.
837 *
838 * @param in The contents of the reader.
839 * @return A new input stream.
840 */
841 public static final ByteArrayInputStream inputStream(String in) {
842 return new ByteArrayInputStream(in.getBytes());
843 }
844
845 public static String json(Object o) {
846 return Json5.DEFAULT_SORTED.write(o);
847 }
848
849 public static <T> T json(String o, Class<T> c) {
850 return safe(()->Json5.DEFAULT_SORTED.read(o, c));
851 }
852
853 public static <T> T jsonRoundTrip(T o, Class<T> c) {
854 return json(json(o), c);
855 }
856
857 /**
858 * Creates a reader from the specified string.
859 *
860 * @param in The contents of the reader.
861 * @return A new reader.
862 */
863 public static final StringReader reader(String in) {
864 return new StringReader(in);
865 }
866
867 /**
868 * Temporarily sets the default system locale to the specified locale.
869 * Use {@link #unsetLocale()} to unset it.
870 *
871 * @param name
872 */
873 public static final void setLocale(Locale v) {
874 SYSTEM_LOCALE.set(Locale.getDefault());
875 Locale.setDefault(v);
876 }
877
878 /**
879 * Temporarily sets the default system timezone to the specified timezone ID.
880 * Use {@link #unsetTimeZone()} to unset it.
881 *
882 * @param name
883 */
884 public static final synchronized void setTimeZone(String v) {
885 SYSTEM_TIME_ZONE.set(TimeZone.getDefault());
886 TimeZone.setDefault(TimeZone.getTimeZone(v));
887 }
888
889 public static final void unsetLocale() {
890 Locale.setDefault(SYSTEM_LOCALE.get());
891 }
892
893 public static final synchronized void unsetTimeZone() {
894 TimeZone.setDefault(SYSTEM_TIME_ZONE.get());
895 }
896
897 /**
898 * Constructs a {@link URL} object from a string.
899 */
900 public static URL url(String value) {
901 return safe(()->new URI(value).toURL());
902 }
903
904 /**
905 * Test whitespace and generated schema.
906 */
907 public static final void validateXml(Object o) throws Exception {
908 validateXml(o, XmlSerializer.DEFAULT_NS_SQ);
909 }
910
911 /**
912 * Test whitespace and generated schema.
913 */
914 public static final void validateXml(Object o, XmlSerializer s) throws Exception {
915 s = s.copy().ws().ns().addNamespaceUrisToRoot().build();
916 var xml = s.serialize(o);
917 checkXmlWhitespace(xml);
918 }
919
920 public static final <T> BeanTester<T> testBean(T bean) {
921 return (BeanTester<T>) new BeanTester<>().bean(bean);
922 }
923
924 /**
925 * Extracts HTML/XML elements from a string based on element name and attributes.
926 *
927 * <p>Uses a depth-tracking parser to handle nested elements correctly, even with malformed HTML.</p>
928 *
929 * <h5 class='section'>Examples:</h5>
930 * <pre>
931 * // Extract all div elements with class='tag-block'
932 * List<String> blocks = extractXml(html, "div", Map.of("class", "tag-block"));
933 *
934 * // Extract all span elements (no attribute filtering)
935 * List<String> spans = extractXml(html, "span", null);
936 *
937 * // Extract divs with multiple attributes
938 * List<String> divs = extractXml(html, "div", Map.of("class", "header", "id", "main"));
939 * </pre>
940 *
941 * @param html The HTML/XML content to parse
942 * @param elementName The element name to extract (e.g., "div", "span")
943 * @param withAttributes Optional map of attribute name/value pairs that must match.
944 * Pass null or empty map to match all elements of the given name.
945 * @return List of HTML content strings (inner content of matching elements)
946 */
947 public static List<String> extractXml(String html, String elementName, Map<String,String> withAttributes) {
948 List<String> results = new ArrayList<>();
949
950 if (html == null || elementName == null) {
951 return results;
952 }
953
954 // Find all opening tags of the specified element
955 String openTag = "<" + elementName;
956 int searchPos = 0;
957
958 while ((searchPos = html.indexOf(openTag, searchPos)) != -1) {
959 // Find the end of the opening tag
960 int tagEnd = html.indexOf('>', searchPos);
961 if (tagEnd == -1) break;
962
963 String fullOpenTag = html.substring(searchPos, tagEnd + 1);
964
965 // Check if attributes match
966 boolean matches = true;
967 if (withAttributes != null && !withAttributes.isEmpty()) {
968 for (Map.Entry<String, String> entry : withAttributes.entrySet()) {
969 String attrName = entry.getKey();
970 String attrValue = entry.getValue();
971
972 // Look for attribute in the tag (handle both single and double quotes)
973 String pattern1 = attrName + "=\"" + attrValue + "\"";
974 String pattern2 = attrName + "='" + attrValue + "'";
975
976
977 if (!fullOpenTag.contains(pattern1) && !fullOpenTag.contains(pattern2)) {
978 matches = false;
979 break;
980 }
981 }
982 }
983
984 if (matches) {
985 // Find matching closing tag by tracking depth
986 int contentStart = tagEnd + 1;
987 int depth = 1;
988 int pos = contentStart;
989
990 while (pos < html.length() && depth > 0) {
991 // Look for next opening or closing tag of same element
992 int nextOpen = html.indexOf("<" + elementName, pos);
993 int nextClose = html.indexOf("</" + elementName + ">", pos);
994
995 // Validate that nextOpen is actually an opening tag
996 if (nextOpen != -1 && (nextOpen < nextClose || nextClose == -1)) {
997 if (nextOpen + elementName.length() + 1 < html.length()) {
998 char nextChar = html.charAt(nextOpen + elementName.length() + 1);
999 if (nextChar == ' ' || nextChar == '>' || nextChar == '/') {
1000 depth++;
1001 pos = nextOpen + elementName.length() + 1;
1002 continue;
1003 }
1004 }
1005 // Not a valid opening tag, skip it
1006 pos = nextOpen + 1;
1007 continue;
1008 }
1009
1010 if (nextClose != -1) {
1011 depth--;
1012 if (depth == 0) {
1013 // Found matching close tag
1014 results.add(html.substring(contentStart, nextClose));
1015 break;
1016 }
1017 pos = nextClose + elementName.length() + 3;
1018 } else {
1019 // No more closing tags
1020 break;
1021 }
1022 }
1023 }
1024
1025 searchPos = tagEnd + 1;
1026 }
1027
1028 return results;
1029 }
1030 }