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) 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 }