View Javadoc
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 }