1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2026 the original author or authors.
4 //
5 // This library is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU Lesser General Public
7 // License as published by the Free Software Foundation; either
8 // version 2.1 of the License, or (at your option) any later version.
9 //
10 // This library is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 // Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public
16 // License along with this library; if not, write to the Free Software
17 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 ///////////////////////////////////////////////////////////////////////////////////////////////
19
20 package com.puppycrawl.tools.checkstyle.internal.utils;
21
22 import static org.junit.jupiter.api.Assertions.assertThrows;
23
24 import java.io.File;
25 import java.io.OutputStream;
26 import java.lang.reflect.Constructor;
27 import java.lang.reflect.Field;
28 import java.lang.reflect.Method;
29 import java.lang.reflect.Modifier;
30 import java.util.Arrays;
31 import java.util.Collection;
32 import java.util.List;
33 import java.util.Locale;
34 import java.util.Map;
35 import java.util.Objects;
36 import java.util.Optional;
37 import java.util.Set;
38 import java.util.concurrent.Callable;
39 import java.util.concurrent.FutureTask;
40 import java.util.concurrent.TimeUnit;
41 import java.util.function.Predicate;
42 import java.util.function.Supplier;
43 import java.util.regex.Pattern;
44 import java.util.stream.Stream;
45
46 import org.junit.jupiter.api.function.Executable;
47 import org.mockito.internal.util.Checks;
48
49 import com.puppycrawl.tools.checkstyle.AbstractAutomaticBean;
50 import com.puppycrawl.tools.checkstyle.PackageNamesLoader;
51 import com.puppycrawl.tools.checkstyle.PackageObjectFactory;
52 import com.puppycrawl.tools.checkstyle.TreeWalkerAuditEvent;
53 import com.puppycrawl.tools.checkstyle.TreeWalkerFilter;
54 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
55 import com.puppycrawl.tools.checkstyle.api.AuditListener;
56 import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
57 import com.puppycrawl.tools.checkstyle.api.DetailAST;
58 import com.puppycrawl.tools.checkstyle.api.TextBlock;
59
60 public final class TestUtil {
61
62 /**
63 * The stack size used in {@link TestUtil#getResultWithLimitedResources}.
64 * This value should be as small as possible. Some JVM requires this value to be
65 * at least 144k.
66 *
67 * @see <a href="https://www.baeldung.com/jvm-configure-stack-sizes">
68 * Configuring Stack Sizes in the JVM</a>
69 */
70 private static final int MINIMAL_STACK_SIZE = 147456;
71
72 /**
73 * The stack size used in {@link TestUtil#runWithLimitedXpathResources}.
74 * This value must be large enough for Saxon's XPath engine to initialize, but
75 * small enough to detect stack overflows in deep AST XPath traversal.
76 */
77 private static final int MINIMAL_XPATH_STACK_SIZE = 1_048_576;
78
79 private TestUtil() {
80 }
81
82 /**
83 * Verifies that utils class has private constructor and invokes it to satisfy code coverage.
84 *
85 * @param utilClass class to test for c-tor
86 * @return true if constructor is expected.
87 */
88 public static boolean isUtilsClassHasPrivateConstructor(final Class<?> utilClass)
89 throws ReflectiveOperationException {
90 final Constructor<?> constructor = utilClass.getDeclaredConstructor();
91 final boolean result = Modifier.isPrivate(constructor.getModifiers());
92 constructor.setAccessible(true);
93 constructor.newInstance();
94 return result;
95 }
96
97 /**
98 * Retrieves the specified field by its name in the class or its direct super.
99 *
100 * @param targetClass the class to retrieve the field for
101 * @param fieldName the name of the field to retrieve
102 * @return the class' field if found
103 */
104 private static Field getClassDeclaredField(Class<?> targetClass, String fieldName) {
105 return Stream.<Class<?>>iterate(targetClass, Objects::nonNull, Class::getSuperclass)
106 .flatMap(cls -> Arrays.stream(cls.getDeclaredFields()))
107 .filter(field -> fieldName.equals(field.getName()))
108 .findFirst()
109 .map(field -> {
110 field.setAccessible(true);
111 return field;
112 })
113 .orElseThrow(() -> {
114 return new IllegalStateException(String.format(Locale.ROOT,
115 "Field '%s' not found in '%s'", fieldName, targetClass.getCanonicalName()));
116 });
117 }
118
119 /**
120 * Retrieves the specified method by its name in the class or its direct super.
121 *
122 * @param targetClass the class to retrieve the method for
123 * @param methodName the name of the method to retrieve
124 * @param parameters the expected number of parameters
125 * @return the class' method
126 */
127 private static Method getClassDeclaredMethod(Class<?> targetClass,
128 String methodName,
129 int parameters) {
130 final Stream<Method> methods = Stream.<Class<?>>iterate(targetClass, Class::getSuperclass)
131 .flatMap(cls -> Arrays.stream(cls.getDeclaredMethods()))
132 .filter(method -> {
133 return methodName.equals(method.getName());
134 });
135
136 final Supplier<String> exceptionMessage = () -> {
137 return String.format(Locale.ROOT, "Method '%s' with %d parameters not found in '%s'",
138 methodName, parameters, targetClass.getCanonicalName());
139 };
140
141 return getMatchingExecutable(methods, parameters, exceptionMessage);
142 }
143
144 /**
145 * Retrieves the specified executable from a class.
146 *
147 * @param <T> the type of executable to search
148 * @param execs The stream of executables to search
149 * @param parameters the expected number of parameters
150 * @param exceptionMessage the exception message to use if executable is not found
151 * @return the matching executable
152 */
153 private static <T extends java.lang.reflect.Executable> T getMatchingExecutable(
154 Stream<T> execs, int parameters, Supplier<String> exceptionMessage) {
155 return execs.filter(method -> {
156 return parameters == method.getParameterCount();
157 })
158 .findFirst()
159 .map(method -> {
160 method.setAccessible(true);
161 return method;
162 })
163 .orElseThrow(() -> {
164 return new IllegalStateException(exceptionMessage.get());
165 });
166 }
167
168 /**
169 * Checks if stateful field is cleared during {@link AbstractCheck#beginTree} in check.
170 *
171 * @param check check object which field is to be verified
172 * @param astToVisit ast to pass into check methods
173 * @param fieldName name of the field to be checked
174 * @param isClear function for checking field state
175 * @return {@code true} if state of the field is cleared
176 */
177 public static boolean isStatefulFieldClearedDuringBeginTree(AbstractCheck check,
178 DetailAST astToVisit,
179 String fieldName,
180 Predicate<Object> isClear) {
181 check.beginTree(astToVisit);
182 check.visitToken(astToVisit);
183 check.beginTree(null);
184 return isClear.test(getInternalState(check, fieldName, Object.class));
185 }
186
187 /**
188 * Checks if stateful field is cleared during {@link AbstractAutomaticBean}'s finishLocalSetup.
189 *
190 * @param filter filter object which field is to be verified
191 * @param event event to pass into filter methods
192 * @param fieldName name of the field to be checked
193 * @param isClear function for checking field state
194 * @return {@code true} if state of the field is cleared
195 * @throws Exception if there was an error.
196 */
197 public static boolean isStatefulFieldClearedDuringLocalSetup(
198 TreeWalkerFilter filter, TreeWalkerAuditEvent event,
199 String fieldName, Predicate<Object> isClear) throws Exception {
200 filter.accept(event);
201 invokeVoidMethod(filter, "finishLocalSetup");
202 final Field resultField = getClassDeclaredField(filter.getClass(), fieldName);
203 return isClear.test(resultField.get(filter));
204 }
205
206 /**
207 * Returns the default PackageObjectFactory with the default package names.
208 *
209 * @return the default PackageObjectFactory.
210 */
211 public static PackageObjectFactory getPackageObjectFactory() throws CheckstyleException {
212 final ClassLoader cl = TestUtil.class.getClassLoader();
213 final Set<String> packageNames = PackageNamesLoader.getPackageNames(cl);
214 return new PackageObjectFactory(packageNames, cl);
215 }
216
217 /**
218 * Finds node of specified type among root children, siblings, siblings children
219 * on any deep level.
220 *
221 * @param root DetailAST
222 * @param predicate predicate
223 * @return {@link Optional} of {@link DetailAST} node which matches the predicate.
224 */
225 public static Optional<DetailAST> findTokenInAstByPredicate(DetailAST root,
226 Predicate<DetailAST> predicate) {
227 DetailAST curNode = root;
228 while (!predicate.test(curNode)) {
229 DetailAST toVisit = curNode.getFirstChild();
230 while (curNode != null && toVisit == null) {
231 toVisit = curNode.getNextSibling();
232 if (toVisit == null) {
233 curNode = curNode.getParent();
234 }
235 }
236
237 if (Objects.equals(curNode, toVisit) || Objects.equals(curNode, root.getParent())) {
238 curNode = null;
239 break;
240 }
241
242 curNode = toVisit;
243 }
244 return Optional.ofNullable(curNode);
245 }
246
247 /**
248 * Returns the JDK version as a number that is easy to compare.
249 *
250 * <p>
251 * For JDK "1.8" it will be 8; for JDK "11" it will be 11.
252 * </p>
253 *
254 * @return JDK version as integer
255 */
256 public static int getJdkVersion() {
257 String version = System.getProperty("java.specification.version");
258 if (version.startsWith("1.")) {
259 version = version.substring(2);
260 }
261 return Integer.parseInt(version);
262 }
263
264 /**
265 * Adjusts the expected number of flushes for tests that call {@link OutputStream#close} method.
266 *
267 * <p>
268 * After <a href="https://bugs.openjdk.java.net/browse/JDK-8220477">JDK-8220477</a>
269 * there is one additional flush from {@code sun.nio.cs.StreamEncoder#implClose}.
270 * </p>
271 *
272 * @param flushCount flush count to adjust
273 * @return adjusted flush count
274 */
275 public static int adjustFlushCountForOutputStreamClose(int flushCount) {
276 int result = flushCount;
277 if (getJdkVersion() >= 13) {
278 ++result;
279 }
280 return result;
281 }
282
283 /**
284 * Runs a given task with limited stack size and time duration, then
285 * returns the result. See AbstractModuleTestSupport#verifyWithLimitedResources
286 * for an example of how to use this method when task does not return a result, i.e.
287 * the given method's return type is {@code void}.
288 *
289 * @param callable the task to execute
290 * @param <V> return type of task - {@code Void} if task does not return result
291 * @return result
292 * @throws Exception if getting result fails
293 */
294 public static <V> V getResultWithLimitedResources(Callable<V> callable) throws Exception {
295 final FutureTask<V> futureTask = new FutureTask<>(callable);
296 final Thread thread = new Thread(null, futureTask,
297 "LimitedStackSizeThread", MINIMAL_STACK_SIZE);
298 thread.start();
299 return futureTask.get(10, TimeUnit.SECONDS);
300 }
301
302 /**
303 * Runs a given task with limited stack size suitable for XPath-based checks.
304 * The stack size is larger than
305 * {@link TestUtil#getResultWithLimitedResources} to allow Saxon's XPath engine
306 * to initialize, but still small enough to detect stack overflows in deep AST
307 * XPath traversal.
308 *
309 * @param callable the task to execute
310 * @throws Exception if getting result fails
311 */
312 public static void runWithLimitedXpathResources(Callable<Void> callable)
313 throws Exception {
314 final FutureTask<Void> futureTask = new FutureTask<>(callable);
315 final Thread thread = new Thread(null, futureTask,
316 "LimitedXpathStackSizeThread", MINIMAL_XPATH_STACK_SIZE);
317 thread.start();
318 futureTask.get(10, TimeUnit.SECONDS);
319 }
320
321 /**
322 * Reads the value of a field using reflection. This method will traverse the
323 * super class hierarchy until a field with name {@code fieldName} is found.
324 *
325 * @param instance the instance to read
326 * @param fieldName the name of the field
327 * @throws RuntimeException if the field can't be read
328 */
329 public static <T> T getInternalState(Object instance, String fieldName, Class<T> clazz) {
330 try {
331 final Field field = getClassDeclaredField(instance.getClass(), fieldName);
332 return clazz.cast(field.get(instance));
333 }
334 catch (ReflectiveOperationException exc) {
335 final String message = String.format(Locale.ROOT,
336 "Failed to get field '%s' for instance of class '%s'",
337 fieldName, instance.getClass().getSimpleName());
338 throw new IllegalStateException(message, exc);
339 }
340 }
341
342 /**
343 * Helper method for casting collection type Map.
344 *
345 * @param instance the instance to read
346 * @param fieldName the name of the field
347 * @throws RuntimeException if the field can't be read
348 * @noinspection unchecked
349 * @noinspectionreason unchecked - unchecked cast is ok on test code
350 */
351 public static Map<String, String> getInternalStateMap(Object instance, String fieldName) {
352 return getInternalState(instance, fieldName, Map.class);
353 }
354
355 /**
356 * Helper method for casting collection type Map.
357 *
358 * @param instance the instance to read
359 * @param fieldName the name of the field
360 * @throws RuntimeException if the field can't be read
361 * @noinspection unchecked
362 * @noinspectionreason unchecked - unchecked cast is ok on test code
363 */
364 public static Map<Integer, List<TextBlock>> getInternalStateMapIntegerList(
365 Object instance, String fieldName) {
366 return getInternalState(instance, fieldName, Map.class);
367 }
368
369 /**
370 * Helper method for casting collection type List.
371 *
372 * @param instance the instance to read
373 * @param fieldName the name of the field
374 * @throws RuntimeException if the field can't be read
375 * @noinspection unchecked
376 * @noinspectionreason unchecked - unchecked cast is ok on test code
377 */
378 public static List<AuditListener> getInternalStateListAuditListener(
379 Object instance, String fieldName) {
380 return getInternalState(instance, fieldName, List.class);
381 }
382
383 /**
384 * Helper method for casting collection type List.
385 *
386 * @param instance the instance to read
387 * @param fieldName the name of the field
388 * @throws RuntimeException if the field can't be read
389 * @noinspection unchecked
390 * @noinspectionreason unchecked - unchecked cast is ok on test code
391 */
392 public static List<Pattern> getInternalStateListPattern(
393 Object instance, String fieldName) {
394 return getInternalState(instance, fieldName, List.class);
395 }
396
397 /**
398 * Helper method for casting collection type List.
399 *
400 * @param instance the instance to read
401 * @param fieldName the name of the field
402 * @throws RuntimeException if the field can't be read
403 * @noinspection unchecked
404 * @noinspectionreason unchecked - unchecked cast is ok on test code
405 */
406 public static List<Comparable<Object>> getInternalStateListComparable(
407 Object instance, String fieldName) {
408 return getInternalState(instance, fieldName, List.class);
409 }
410
411 /**
412 * Helper method for casting to Collection.
413 *
414 * @param instance the instance to read
415 * @param fieldName the name of the field
416 * @throws RuntimeException if the field can't be read
417 * @noinspection unchecked
418 * @noinspectionreason unchecked - unchecked cast is ok on test code
419 */
420 public static Collection<Checks> getInternalStateCollectionChecks(
421 Object instance, String fieldName) {
422 return getInternalState(instance, fieldName, Collection.class);
423 }
424
425 /**
426 * Helper method for casting to collection type Set.
427 *
428 * @param instance the instance to read
429 * @param fieldName the name of the field
430 * @throws RuntimeException if the field can't be read
431 * @noinspection unchecked
432 * @noinspectionreason unchecked - unchecked cast is ok on test code
433 */
434 public static Set<TreeWalkerFilter> getInternalStateSetTreeWalkerFilter(
435 Object instance, String fieldName) {
436 return getInternalState(instance, fieldName, Set.class);
437 }
438
439 /**
440 * Reads the value of a static field using reflection. This method will traverse the
441 * super class hierarchy until a field with name {@code fieldName} is found.
442 *
443 * @param targetClass the class of the field
444 * @param fieldName the name of the field
445 * @param clazz the expected type of the field value, used for type-safe casting
446 * @throws RuntimeException if the field can't be read
447 */
448 public static <T> T getInternalStaticState(Class<?> targetClass, String fieldName,
449 Class<T> clazz) {
450 try {
451 final Field field = getClassDeclaredField(targetClass, fieldName);
452 return clazz.cast(field.get(null));
453 }
454 catch (ReflectiveOperationException exc) {
455 final String message = String.format(Locale.ROOT,
456 "Failed to get static field '%s' for class '%s'",
457 fieldName, targetClass);
458 throw new IllegalStateException(message, exc);
459 }
460 }
461
462 /**
463 * Helper method for casting to collection type Map.
464 *
465 * @param targetClass the class of the field
466 * @param fieldName the name of the field
467 * @throws RuntimeException if the field can't be read
468 * @noinspection unchecked
469 * @noinspectionreason unchecked - unchecked cast is ok on test code
470 */
471 public static Map<String, String> getInternalStaticStateMap(Class<?> targetClass,
472 String fieldName) {
473 return getInternalStaticState(targetClass, fieldName, Map.class);
474 }
475
476 /**
477 * Helper method for casting to collection type Map.
478 *
479 * @param targetClass the class of the field
480 * @param fieldName the name of the field
481 * @throws RuntimeException if the field can't be read
482 * @noinspection unchecked
483 * @noinspectionreason unchecked - unchecked cast is ok on test code
484 */
485 public static ThreadLocal<List<Object>> getInternalStaticStateThreadLocal(
486 Class<?> targetClass, String fieldName) {
487 return getInternalStaticState(targetClass, fieldName, ThreadLocal.class);
488 }
489
490 /**
491 * Writes the value of a field using reflection. This method will traverse the
492 * super class hierarchy until a field with name {@code fieldName} is found.
493 *
494 * @param instance the instance whose field to modify
495 * @param fieldName the name of the field
496 * @param value the new value of the field
497 * @throws RuntimeException if the field can't be changed
498 */
499 public static void setInternalState(Object instance, String fieldName, Object value) {
500 try {
501 final Field field = getClassDeclaredField(instance.getClass(), fieldName);
502 field.set(instance, value);
503 }
504 catch (ReflectiveOperationException exc) {
505 final String message = String.format(Locale.ROOT,
506 "Failed to set field '%s' for instance of class '%s'",
507 fieldName, instance.getClass().getSimpleName());
508 throw new IllegalStateException(message, exc);
509 }
510 }
511
512 /**
513 * Invokes a private method for an instance.
514 *
515 * @param instance the instance whose method to invoke
516 * @param methodToExecute the name of the method to invoke
517 * @param resultClazz used for cast of result
518 * @param arguments the optional arguments
519 * @param <T> the type of the result
520 * @return the method's result
521 * @throws ReflectiveOperationException if the method invocation failed
522 */
523 public static <T> T invokeMethod(Object instance, String methodToExecute,
524 Class<T> resultClazz, Object... arguments)
525 throws ReflectiveOperationException {
526 final Class<?> ownerClass = instance.getClass();
527 final Method method = getClassDeclaredMethod(ownerClass, methodToExecute, arguments.length);
528 return resultClazz.cast(method.invoke(instance, arguments));
529 }
530
531 /**
532 * Helper method to invoke private method for an instance with cast to Object.
533 *
534 * @param instance the instance whose method to invoke
535 * @param methodToExecute the name of the method to invoke
536 * @param arguments the optional arguments
537 * @throws ReflectiveOperationException if the method invocation failed
538 */
539 public static void invokeVoidMethod(Object instance,
540 String methodToExecute, Object... arguments)
541 throws ReflectiveOperationException {
542 invokeMethod(instance, methodToExecute, Object.class, arguments);
543 }
544
545 /**
546 * Helper method to invoke private method for an instance with cast to Set.
547 *
548 * @param instance the instance whose method to invoke
549 * @param methodToExecute the name of the method to invoke
550 * @param arguments the optional arguments
551 * @return the method's result
552 * @throws ReflectiveOperationException if the method invocation failed
553 * @noinspection unchecked
554 * @noinspectionreason unchecked - unchecked cast is ok on test code
555 */
556 public static Set<String> invokeMethodSet(Object instance, String methodToExecute,
557 Object... arguments) throws ReflectiveOperationException {
558 return (Set<String>) invokeMethod(instance, methodToExecute, Set.class, arguments);
559 }
560
561 /**
562 * Invokes a static private method for a class.
563 *
564 * @param ownerClass the class whose static method to invoke
565 * @param methodToExecute the name of the method to invoke
566 * @param resultClass used for cast of result
567 * @param arguments the optional arguments
568 * @param <T> the type of the result
569 * @return the method's result
570 * @throws ReflectiveOperationException if the method invocation failed
571 */
572 public static <T> T invokeStaticMethod(Class<?> ownerClass,
573 String methodToExecute, Class<T> resultClass, Object... arguments)
574 throws ReflectiveOperationException {
575 final Method method = getClassDeclaredMethod(ownerClass, methodToExecute, arguments.length);
576 return resultClass.cast(method.invoke(null, arguments));
577 }
578
579 /**
580 * Helper method to invoke static private method for an instance with cast to Object.
581 *
582 * @param ownerClass the class whose static method to invoke
583 * @param methodToExecute the name of the method to invoke
584 * @param arguments the optional arguments
585 * @throws ReflectiveOperationException if the method invocation failed
586 */
587 public static void invokeVoidStaticMethod(Class<?> ownerClass,
588 String methodToExecute, Object... arguments)
589 throws ReflectiveOperationException {
590 invokeStaticMethod(ownerClass, methodToExecute, Object.class, arguments);
591 }
592
593 /**
594 * Helper method to invoke static private method for an instance with cast to List.
595 *
596 * @param ownerClass the class whose static method to invoke
597 * @param methodToExecute the name of the method to invoke
598 * @param arguments the optional arguments
599 * @return the method's result
600 * @throws ReflectiveOperationException if the method invocation failed
601 * @noinspection unchecked
602 * @noinspectionreason unchecked - unchecked cast is ok on test code
603 */
604 public static List<File> invokeStaticMethodList(Class<?> ownerClass,
605 String methodToExecute, Object... arguments)
606 throws ReflectiveOperationException {
607 return (List<File>) invokeStaticMethod(ownerClass, methodToExecute, List.class, arguments);
608 }
609
610 /**
611 * Instantiates an object of the given class with the given arguments,
612 * even if the constructor is private.
613 *
614 * @param targetClass The class to instantiate
615 * @param arguments The arguments to pass to the constructor
616 * @param <T> the type of the object to instantiate
617 * @return The instantiated object
618 * @throws ReflectiveOperationException if the constructor invocation failed
619 */
620 @SuppressWarnings("unchecked")
621 public static <T> T instantiate(Class<T> targetClass, Object... arguments)
622 throws ReflectiveOperationException {
623
624 final Stream<Constructor<T>> ctors =
625 Arrays.stream(targetClass.getDeclaredConstructors()).map(Constructor.class::cast);
626
627 final Supplier<String> exceptionMessage = () -> {
628 return String.format(Locale.ROOT, "Constructor with %d parameters not found in '%s'",
629 arguments.length, targetClass.getCanonicalName());
630 };
631
632 final Constructor<T> constructor =
633 getMatchingExecutable(ctors, arguments.length, exceptionMessage);
634 constructor.setAccessible(true);
635
636 return constructor.newInstance(arguments);
637 }
638
639 /**
640 * Returns the inner class type by its name.
641 *
642 * @param declaringClass the class in which the inner class is declared
643 * @param name the unqualified name (simple name) of the inner class
644 * @return the inner class type
645 * @throws ClassNotFoundException if the class not found
646 * @noinspection unchecked
647 * @noinspectionreason unchecked - unchecked cast is ok on test code
648 */
649 public static <T> Class<T> getInnerClassType(Class<?> declaringClass, String name)
650 throws ClassNotFoundException {
651 return (Class<T>) Class.forName(declaringClass.getName() + "$" + name);
652 }
653
654 /**
655 * Executes the provided executable and expects it to throw an exception of the specified type.
656 *
657 * @param expectedType the class of the expected exception type.
658 * @param executable the executable to be executed
659 * @return the expected exception thrown by the executable.
660 */
661 public static <T extends Throwable> T getExpectedThrowable(Class<T> expectedType,
662 Executable executable) {
663 return assertThrows(expectedType, executable);
664 }
665
666 /**
667 * Executes the provided executable and expects it to throw an exception of the specified type.
668 *
669 * @param expectedType the class of the expected exception type.
670 * @param executable the executable to be executed
671 * @param message the message to be used in case of assertion failure.
672 * @return the expected exception thrown by the executable.
673 */
674 public static <T extends Throwable> T getExpectedThrowable(Class<T> expectedType,
675 Executable executable,
676 String message) {
677 return assertThrows(expectedType, executable, message);
678 }
679
680 }