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)
200 throws Exception {
201 filter.accept(event);
202 invokeVoidMethod(filter, "finishLocalSetup");
203 final Field resultField = getClassDeclaredField(filter.getClass(), fieldName);
204 return isClear.test(resultField.get(filter));
205 }
206
207 /**
208 * Returns the default PackageObjectFactory with the default package names.
209 *
210 * @return the default PackageObjectFactory.
211 */
212 public static PackageObjectFactory getPackageObjectFactory() throws CheckstyleException {
213 final ClassLoader cl = TestUtil.class.getClassLoader();
214 final Set<String> packageNames = PackageNamesLoader.getPackageNames(cl);
215 return new PackageObjectFactory(packageNames, cl);
216 }
217
218 /**
219 * Finds node of specified type among root children, siblings, siblings children
220 * on any deep level.
221 *
222 * @param root DetailAST
223 * @param predicate predicate
224 * @return {@link Optional} of {@link DetailAST} node which matches the predicate.
225 */
226 public static Optional<DetailAST> findTokenInAstByPredicate(DetailAST root,
227 Predicate<DetailAST> predicate) {
228 DetailAST curNode = root;
229 while (!predicate.test(curNode)) {
230 DetailAST toVisit = curNode.getFirstChild();
231 while (curNode != null && toVisit == null) {
232 toVisit = curNode.getNextSibling();
233 if (toVisit == null) {
234 curNode = curNode.getParent();
235 }
236 }
237
238 if (Objects.equals(curNode, toVisit) || Objects.equals(curNode, root.getParent())) {
239 curNode = null;
240 break;
241 }
242
243 curNode = toVisit;
244 }
245 return Optional.ofNullable(curNode);
246 }
247
248 /**
249 * Returns the JDK version as a number that is easy to compare.
250 *
251 * <p>
252 * For JDK "1.8" it will be 8; for JDK "11" it will be 11.
253 * </p>
254 *
255 * @return JDK version as integer
256 */
257 public static int getJdkVersion() {
258 String version = System.getProperty("java.specification.version");
259 if (version.startsWith("1.")) {
260 version = version.substring(2);
261 }
262 return Integer.parseInt(version);
263 }
264
265 /**
266 * Adjusts the expected number of flushes for tests that call {@link OutputStream#close} method.
267 *
268 * <p>
269 * After <a href="https://bugs.openjdk.java.net/browse/JDK-8220477">JDK-8220477</a>
270 * there is one additional flush from {@code sun.nio.cs.StreamEncoder#implClose}.
271 * </p>
272 *
273 * @param flushCount flush count to adjust
274 * @return adjusted flush count
275 */
276 public static int adjustFlushCountForOutputStreamClose(int flushCount) {
277 int result = flushCount;
278 if (getJdkVersion() >= 13) {
279 ++result;
280 }
281 return result;
282 }
283
284 /**
285 * Runs a given task with limited stack size and time duration, then
286 * returns the result. See AbstractModuleTestSupport#verifyWithLimitedResources
287 * for an example of how to use this method when task does not return a result, i.e.
288 * the given method's return type is {@code void}.
289 *
290 * @param <V> return type of task - {@code Void} if task does not return result
291 * @param callable the task to execute
292 * @return result
293 * @throws Exception if getting result fails
294 */
295 public static <V> V getResultWithLimitedResources(Callable<V> callable) throws Exception {
296 final FutureTask<V> futureTask = new FutureTask<>(callable);
297 final Thread thread = new Thread(null, futureTask,
298 "LimitedStackSizeThread", MINIMAL_STACK_SIZE);
299 thread.start();
300 return futureTask.get(10, TimeUnit.SECONDS);
301 }
302
303 /**
304 * Runs a given task with limited stack size suitable for XPath-based checks.
305 * The stack size is larger than
306 * {@link TestUtil#getResultWithLimitedResources} to allow Saxon's XPath engine
307 * to initialize, but still small enough to detect stack overflows in deep AST
308 * XPath traversal.
309 *
310 * @param callable the task to execute
311 * @throws Exception if getting result fails
312 */
313 public static void runWithLimitedXpathResources(Callable<Void> callable)
314 throws Exception {
315 final FutureTask<Void> futureTask = new FutureTask<>(callable);
316 final Thread thread = new Thread(null, futureTask,
317 "LimitedXpathStackSizeThread", MINIMAL_XPATH_STACK_SIZE);
318 thread.start();
319 futureTask.get(10, TimeUnit.SECONDS);
320 }
321
322 /**
323 * Reads the value of a field using reflection. This method will traverse the
324 * super class hierarchy until a field with name {@code fieldName} is found.
325 *
326 * @param instance the instance to read
327 * @param fieldName the name of the field
328 * @throws RuntimeException if the field can't be read
329 */
330 public static <T> T getInternalState(Object instance, String fieldName, Class<T> clazz) {
331 try {
332 final Field field = getClassDeclaredField(instance.getClass(), fieldName);
333 return clazz.cast(field.get(instance));
334 }
335 catch (ReflectiveOperationException exc) {
336 final String message = String.format(Locale.ROOT,
337 "Failed to get field '%s' for instance of class '%s'",
338 fieldName, instance.getClass().getSimpleName());
339 throw new IllegalStateException(message, exc);
340 }
341 }
342
343 /**
344 * Helper method for casting collection type Map.
345 *
346 * @param instance the instance to read
347 * @param fieldName the name of the field
348 * @throws RuntimeException if the field can't be read
349 * @noinspection unchecked
350 * @noinspectionreason unchecked - unchecked cast is ok on test code
351 */
352 public static Map<String, String> getInternalStateMap(Object instance, String fieldName) {
353 return getInternalState(instance, fieldName, Map.class);
354 }
355
356 /**
357 * Helper method for casting collection type Map.
358 *
359 * @param instance the instance to read
360 * @param fieldName the name of the field
361 * @throws RuntimeException if the field can't be read
362 * @noinspection unchecked
363 * @noinspectionreason unchecked - unchecked cast is ok on test code
364 */
365 public static Map<Integer, List<TextBlock>> getInternalStateMapIntegerList(
366 Object instance, String fieldName) {
367 return getInternalState(instance, fieldName, Map.class);
368 }
369
370 /**
371 * Helper method for casting collection type List.
372 *
373 * @param instance the instance to read
374 * @param fieldName the name of the field
375 * @throws RuntimeException if the field can't be read
376 * @noinspection unchecked
377 * @noinspectionreason unchecked - unchecked cast is ok on test code
378 */
379 public static List<AuditListener> getInternalStateListAuditListener(
380 Object instance, String fieldName) {
381 return getInternalState(instance, fieldName, List.class);
382 }
383
384 /**
385 * Helper method for casting collection type List.
386 *
387 * @param instance the instance to read
388 * @param fieldName the name of the field
389 * @throws RuntimeException if the field can't be read
390 * @noinspection unchecked
391 * @noinspectionreason unchecked - unchecked cast is ok on test code
392 */
393 public static List<Pattern> getInternalStateListPattern(
394 Object instance, String fieldName) {
395 return getInternalState(instance, fieldName, List.class);
396 }
397
398 /**
399 * Helper method for casting collection type List.
400 *
401 * @param instance the instance to read
402 * @param fieldName the name of the field
403 * @throws RuntimeException if the field can't be read
404 * @noinspection unchecked
405 * @noinspectionreason unchecked - unchecked cast is ok on test code
406 */
407 public static List<Comparable<Object>> getInternalStateListComparable(
408 Object instance, String fieldName) {
409 return getInternalState(instance, fieldName, List.class);
410 }
411
412 /**
413 * Helper method for casting to Collection.
414 *
415 * @param instance the instance to read
416 * @param fieldName the name of the field
417 * @throws RuntimeException if the field can't be read
418 * @noinspection unchecked
419 * @noinspectionreason unchecked - unchecked cast is ok on test code
420 */
421 public static Collection<Checks> getInternalStateCollectionChecks(
422 Object instance, String fieldName) {
423 return getInternalState(instance, fieldName, Collection.class);
424 }
425
426 /**
427 * Helper method for casting to collection type Set.
428 *
429 * @param instance the instance to read
430 * @param fieldName the name of the field
431 * @throws RuntimeException if the field can't be read
432 * @noinspection unchecked
433 * @noinspectionreason unchecked - unchecked cast is ok on test code
434 */
435 public static Set<TreeWalkerFilter> getInternalStateSetTreeWalkerFilter(
436 Object instance, String fieldName) {
437 return getInternalState(instance, fieldName, Set.class);
438 }
439
440 /**
441 * Reads the value of a static field using reflection. This method will traverse the
442 * super class hierarchy until a field with name {@code fieldName} is found.
443 *
444 * @param targetClass the class of the field
445 * @param fieldName the name of the field
446 * @param clazz the expected type of the field value, used for type-safe casting
447 * @throws RuntimeException if the field can't be read
448 */
449 public static <T> T getInternalStaticState(Class<?> targetClass, String fieldName,
450 Class<T> clazz) {
451 try {
452 final Field field = getClassDeclaredField(targetClass, fieldName);
453 return clazz.cast(field.get(null));
454 }
455 catch (ReflectiveOperationException exc) {
456 final String message = String.format(Locale.ROOT,
457 "Failed to get static field '%s' for class '%s'",
458 fieldName, targetClass);
459 throw new IllegalStateException(message, exc);
460 }
461 }
462
463 /**
464 * Helper method for casting to collection type Map.
465 *
466 * @param targetClass the class of the field
467 * @param fieldName the name of the field
468 * @throws RuntimeException if the field can't be read
469 * @noinspection unchecked
470 * @noinspectionreason unchecked - unchecked cast is ok on test code
471 */
472 public static Map<String, String> getInternalStaticStateMap(Class<?> targetClass,
473 String fieldName) {
474 return getInternalStaticState(targetClass, fieldName, Map.class);
475 }
476
477 /**
478 * Helper method for casting to collection type Map.
479 *
480 * @param targetClass the class of the field
481 * @param fieldName the name of the field
482 * @throws RuntimeException if the field can't be read
483 * @noinspection unchecked
484 * @noinspectionreason unchecked - unchecked cast is ok on test code
485 */
486 public static ThreadLocal<List<Object>> getInternalStaticStateThreadLocal(
487 Class<?> targetClass, String fieldName) {
488 return getInternalStaticState(targetClass, fieldName, ThreadLocal.class);
489 }
490
491 /**
492 * Writes the value of a field using reflection. This method will traverse the
493 * super class hierarchy until a field with name {@code fieldName} is found.
494 *
495 * @param instance the instance whose field to modify
496 * @param fieldName the name of the field
497 * @param value the new value of the field
498 * @throws RuntimeException if the field can't be changed
499 */
500 public static void setInternalState(Object instance, String fieldName, Object value) {
501 try {
502 final Field field = getClassDeclaredField(instance.getClass(), fieldName);
503 field.set(instance, value);
504 }
505 catch (ReflectiveOperationException exc) {
506 final String message = String.format(Locale.ROOT,
507 "Failed to set field '%s' for instance of class '%s'",
508 fieldName, instance.getClass().getSimpleName());
509 throw new IllegalStateException(message, exc);
510 }
511 }
512
513 /**
514 * Invokes a private method for an instance.
515 *
516 * @param <T> the type of the result
517 * @param instance the instance whose method to invoke
518 * @param methodToExecute the name of the method to invoke
519 * @param resultClazz used for cast of result
520 * @param arguments the optional arguments
521 * @return the method's result
522 * @throws ReflectiveOperationException if the method invocation failed
523 */
524 public static <T> T invokeMethod(Object instance, String methodToExecute,
525 Class<T> resultClazz, Object... arguments)
526 throws ReflectiveOperationException {
527 final Class<?> ownerClass = instance.getClass();
528 final Method method = getClassDeclaredMethod(ownerClass, methodToExecute, arguments.length);
529 return resultClazz.cast(method.invoke(instance, arguments));
530 }
531
532 /**
533 * Helper method to invoke private method for an instance with cast to Object.
534 *
535 * @param instance the instance whose method to invoke
536 * @param methodToExecute the name of the method to invoke
537 * @param arguments the optional arguments
538 * @throws ReflectiveOperationException if the method invocation failed
539 */
540 public static void invokeVoidMethod(Object instance,
541 String methodToExecute, Object... arguments)
542 throws ReflectiveOperationException {
543 invokeMethod(instance, methodToExecute, Object.class, arguments);
544 }
545
546 /**
547 * Helper method to invoke private method for an instance with cast to Set.
548 *
549 * @param instance the instance whose method to invoke
550 * @param methodToExecute the name of the method to invoke
551 * @param arguments the optional arguments
552 * @return the method's result
553 * @throws ReflectiveOperationException if the method invocation failed
554 * @noinspection unchecked
555 * @noinspectionreason unchecked - unchecked cast is ok on test code
556 */
557 public static Set<String> invokeMethodSet(Object instance, String methodToExecute,
558 Object... arguments)
559 throws ReflectiveOperationException {
560 return (Set<String>) invokeMethod(instance, methodToExecute, Set.class, arguments);
561 }
562
563 /**
564 * Invokes a static private method for a class.
565 *
566 * @param <T> the type of the result
567 * @param ownerClass the class whose static method to invoke
568 * @param methodToExecute the name of the method to invoke
569 * @param resultClass used for cast of result
570 * @param arguments the optional arguments
571 * @return the method's result
572 * @throws ReflectiveOperationException if the method invocation failed
573 */
574 public static <T> T invokeStaticMethod(Class<?> ownerClass,
575 String methodToExecute, Class<T> resultClass, Object... arguments)
576 throws ReflectiveOperationException {
577 final Method method = getClassDeclaredMethod(ownerClass, methodToExecute, arguments.length);
578 return resultClass.cast(method.invoke(null, arguments));
579 }
580
581 /**
582 * Helper method to invoke static private method for an instance with cast to Object.
583 *
584 * @param ownerClass the class whose static method to invoke
585 * @param methodToExecute the name of the method to invoke
586 * @param arguments the optional arguments
587 * @throws ReflectiveOperationException if the method invocation failed
588 */
589 public static void invokeVoidStaticMethod(Class<?> ownerClass,
590 String methodToExecute, Object... arguments)
591 throws ReflectiveOperationException {
592 invokeStaticMethod(ownerClass, methodToExecute, Object.class, arguments);
593 }
594
595 /**
596 * Helper method to invoke static private method for an instance with cast to List.
597 *
598 * @param ownerClass the class whose static method to invoke
599 * @param methodToExecute the name of the method to invoke
600 * @param arguments the optional arguments
601 * @return the method's result
602 * @throws ReflectiveOperationException if the method invocation failed
603 * @noinspection unchecked
604 * @noinspectionreason unchecked - unchecked cast is ok on test code
605 */
606 public static List<File> invokeStaticMethodList(Class<?> ownerClass,
607 String methodToExecute, Object... arguments)
608 throws ReflectiveOperationException {
609 return (List<File>) invokeStaticMethod(ownerClass, methodToExecute, List.class, arguments);
610 }
611
612 /**
613 * Instantiates an object of the given class with the given arguments,
614 * even if the constructor is private.
615 *
616 * @param <T> the type of the object to instantiate
617 * @param targetClass The class to instantiate
618 * @param arguments The arguments to pass to the constructor
619 * @return The instantiated object
620 * @throws ReflectiveOperationException if the constructor invocation failed
621 */
622 @SuppressWarnings("unchecked")
623 public static <T> T instantiate(Class<T> targetClass, Object... arguments)
624 throws ReflectiveOperationException {
625
626 final Stream<Constructor<T>> ctors =
627 Arrays.stream(targetClass.getDeclaredConstructors()).map(Constructor.class::cast);
628
629 final Supplier<String> exceptionMessage = () -> {
630 return String.format(Locale.ROOT, "Constructor with %d parameters not found in '%s'",
631 arguments.length, targetClass.getCanonicalName());
632 };
633
634 final Constructor<T> constructor =
635 getMatchingExecutable(ctors, arguments.length, exceptionMessage);
636 constructor.setAccessible(true);
637
638 return constructor.newInstance(arguments);
639 }
640
641 /**
642 * Returns the inner class type by its name.
643 *
644 * @param declaringClass the class in which the inner class is declared
645 * @param name the unqualified name (simple name) of the inner class
646 * @return the inner class type
647 * @throws ClassNotFoundException if the class not found
648 * @noinspection unchecked
649 * @noinspectionreason unchecked - unchecked cast is ok on test code
650 */
651 public static <T> Class<T> getInnerClassType(Class<?> declaringClass, String name)
652 throws ClassNotFoundException {
653 return (Class<T>) Class.forName(declaringClass.getName() + "$" + name);
654 }
655
656 /**
657 * Executes the provided executable and expects it to throw an exception of the specified type.
658 *
659 * @param expectedType the class of the expected exception type.
660 * @param executable the executable to be executed
661 * @return the expected exception thrown by the executable.
662 */
663 public static <T extends Throwable> T getExpectedThrowable(Class<T> expectedType,
664 Executable executable) {
665 return assertThrows(expectedType, executable);
666 }
667
668 /**
669 * Executes the provided executable and expects it to throw an exception of the specified type.
670 *
671 * @param expectedType the class of the expected exception type.
672 * @param executable the executable to be executed
673 * @param message the message to be used in case of assertion failure.
674 * @return the expected exception thrown by the executable.
675 */
676 public static <T extends Throwable> T getExpectedThrowable(Class<T> expectedType,
677 Executable executable,
678 String message) {
679 return assertThrows(expectedType, executable, message);
680 }
681
682 }