diff --git a/CHANGES.txt b/CHANGES.txt index 4ab9f6eb1c..17ffe682db 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -15,10 +15,47 @@ Changed: org.testng.internal.ClonedMethod.getConstructorOrMethod() returns the w Fixed: org.testng.internal.TestNGMethod.clone() no longer throws a NullPointerException when the method has not been bound to a test class yet. It wrapped getTestClass() in a NoOpTestClass, which dereferences it on the spot; the absence is now propagated, which is what ConfigurationMethod.clone() already did (Julien Herr) Fixed: A configuration method that is not a @BeforeGroups or @AfterGroups method now reports an empty array from getBeforeGroups() and getAfterGroups() instead of null. TestNGMethodFinder wrote null into fields whose declaration says {}, and MethodGroupsHelper iterates them without testing (Julien Herr) Fixed: Sorting test methods by index no longer throws a NullPointerException when a tag carries no name (Julien Herr) +Changed: org.testng is now declared @NullMarked, so every member of the published API states whether it can answer null. Thirty-seven members widen to @Nullable because their implementations already answered null, and the rest promise not to. This is binary compatible and source compatible for Java; a Kotlin caller that dereferences one of the thirty-seven without testing it stops compiling. They are listed under Possible backward incompatible changes below (Julien Herr) +Fixed: org.testng.internal.MethodSorting.INSTANCES orders two invocations of the same method on different @Factory instances instead of leaving the decision to a hash code comparison. Its identity branch asked IInstanceIdentity.isIdentityAware about the ids it had just resolved rather than about the methods, which could never hold, so the branch had never run (Julien Herr) +Fixed: org.testng.IAnnotationTransformer.transform(IFactoryAnnotation, Method) is now declared to accept a null method, which is what TestNG has always passed for a @Factory annotation found on a constructor (Julien Herr) +Changed: org.testng.internal.MethodInstance.SORT_BY_INDEX no longer throws a NullPointerException when a method a @Factory produced belongs to no tag. It answers that the two methods cannot be compared, which is what the neighbouring branch already answers for a missing (Julien Herr) +Changed: org.testng.internal.IInstanceIdentity.getInstanceId(Object) answers the new NO_INSTANCE token instead of null for a method that carries no instance, so the value can be used as a map key without every caller deciding what an absent key means. The grouping is unchanged: every method without an instance still lands in one bucket (Julien Herr) +Fixed: In memory friendly mode (testng.memory.friendly), ITestNGMethod.getDataProviderMethod() answers null for a method that has no data provider, instead of a stand-in whose getName() answered an empty string and whose getMethod() threw UnsupportedOperationException. The interface has always documented null for that case, and the three call sites in TestNG already tested for it (Julien Herr) Fixed: A tag that carries no name attribute is now reported the way an unreadable package already was, instead of raising a NullPointerException from inside PackageUtils.findClassesInPackage (Julien Herr) Possible backward incompatible changes: +- org.testng is declared @NullMarked, and thirty-seven members of it answer @Nullable because that + is what their implementations already answered. ITestNGMethod.getTestClass, getInstance, getId, + getDescription, getMissingGroup, getXmlTest, getRetryAnalyzer, getDataProviderMethod and the + deprecated getFactoryMethodParamsInfo; ITestResult.getMethod, getName, getTestName, getInstance, + getInstanceName, getHost, getThrowable and getTestContext; IClass.getXmlTest, getXmlClass, + getTestName and getInstanceHashCodes; IAttributes.getAttribute and removeAttribute; + IDataProviderMethod.getInstance and getMethod; IMethodInstance.getInstance; + ITestClassFinder.getIClass; ITestNGListenerFactory.createListener; + ITestObjectFactory.newInstance(Constructor, Object...); ITestContext.getName, getEndDate, + getHost and getInjectorFactory; ISuite.getHost, getParameter, getParentInjector and + getObjectFactory. A Java caller is unaffected. A Kotlin caller that dereferences one of them + without testing it stops compiling, and must add a test or a !!. +- The same mark widens the parameters of IAnnotationTransformer.transform for ITestAnnotation and + IConfigurationAnnotation -- testClass, testConstructor and testMethod, of which the javadoc has + always said only one is non-null -- of the four IConfigurationListener callbacks that take an + ITestNGMethod, of the three IDataProviderListener callbacks and IDataProviderInterceptor.intercept + that take an ITestContext, of IModuleFactory.createModule, of IMethodSelector.includeMethod, of IClass.getInstances' + error message prefix, and of Reporter.setCurrentTestResult, which TestNG calls with null to clear + the current result. A Java + implementation is unaffected. A Kotlin implementation whose override declares the parameter + non-null stops overriding and must add the question mark. +- org.testng.internal.MethodSorting.INSTANCES, the default method order, produces a different order + for two invocations of the same method on different @Factory instances. Both the old and the new + order are arbitrary -- instance ids are random UUIDs -- so no run that did not already depend on + an arbitrary order is affected, but a run that pinned the old one will see it change. +- org.testng.internal.IInstanceIdentity.getInstanceId(Object) answers IInstanceIdentity.NO_INSTANCE + rather than null for a method that carries no instance. Code testing the result for null must test + for the token instead. The package is internal and OSGi exported. +- testng-core and testng-core-api no longer declare a compileOnly dependency on + com.github.spotbugs:spotbugs. Nothing in TestNG uses javax.annotation any more. + - testng-failed.xml no longer records the index of a @Factory produced instance as an invocation-number. That attribute selects rows of a method's own data provider, which is the only thing TestNG ever reads it back as, so a factory powered failure produced a file that looked filtered and re-ran everything -- and, for a method that had a data provider of its own, re-ran the wrong rows because the factory index had overwritten the row index. The instance index now goes to the new factory-instances attribute of , which is honoured on re-run. Tooling that parses testng-failed.xml to learn which factory instance failed must read factory-instances rather than invocation-numbers; a method with its own data provider now re-runs the rows that actually failed. A file generated by 7.13 and re-run by an older TestNG ignores the unknown attribute and re-runs every instance, which is what those versions already did. (GITHUB-3111, GITHUB-2517, GITHUB-2521) - The constructors of org.testng.internal.ParameterInfo and org.testng.internal.LazyParameterInfo now take an org.testng.internal.FactoryInstance instead of a loose index and parameter array. Both are implementation classes of an internal package; only code constructing them directly is affected. (GITHUB-3111) - org.testng.internal.ClonedMethod.getConstructorOrMethod() answers the wrapped method rather than null. Code that tested the result for null now takes the other branch; no caller in TestNG did, and the method's own toString() could never run before. ITestNGMethod.getConstructorOrMethod() therefore stays non-null when org.testng is marked in turn. diff --git a/testng-cli/src/main/java/org/testng/cli/CliConfigurer.java b/testng-cli/src/main/java/org/testng/cli/CliConfigurer.java index f5f6bff6d7..0aedf701a4 100644 --- a/testng-cli/src/main/java/org/testng/cli/CliConfigurer.java +++ b/testng-cli/src/main/java/org/testng/cli/CliConfigurer.java @@ -129,7 +129,9 @@ public static void configure(TestNG testng, CliOptions cli) { .map(it -> it.asSubclass(IExecutorServiceFactory.class)) .ifPresent(testng::setExecutorServiceFactoryClass); - testng.setOutputDirectory(cli.outputDirectory); + if (cli.outputDirectory != null) { + testng.setOutputDirectory(cli.outputDirectory); + } String testClasses = cli.testClass; if (null != testClasses) { diff --git a/testng-core-api/src/main/java/org/testng/IAnnotationTransformer.java b/testng-core-api/src/main/java/org/testng/IAnnotationTransformer.java index 4546968975..842460751b 100644 --- a/testng-core-api/src/main/java/org/testng/IAnnotationTransformer.java +++ b/testng-core-api/src/main/java/org/testng/IAnnotationTransformer.java @@ -2,6 +2,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Method; +import org.jspecify.annotations.Nullable; import org.testng.annotations.IConfigurationAnnotation; import org.testng.annotations.IDataProviderAnnotation; import org.testng.annotations.IFactoryAnnotation; @@ -27,7 +28,10 @@ public interface IAnnotationTransformer extends ITestNGListener { * method (null otherwise). */ default void transform( - ITestAnnotation annotation, Class testClass, Constructor testConstructor, Method testMethod) { + ITestAnnotation annotation, + @Nullable Class testClass, + @Nullable Constructor testConstructor, + @Nullable Method testMethod) { // not implemented } @@ -47,9 +51,9 @@ default void transform( */ default void transform( IConfigurationAnnotation annotation, - Class testClass, - Constructor testConstructor, - Method testMethod) { + @Nullable Class testClass, + @Nullable Constructor testConstructor, + @Nullable Method testMethod) { // not implemented } @@ -67,9 +71,10 @@ default void transform(IDataProviderAnnotation annotation, Method method) { * Transform an IFactory annotation. * * @param annotation The annotation factory - * @param method The method annotated with the IFactory annotation. + * @param method The method annotated with the IFactory annotation, or {@code null} when the + * annotation was found on a constructor. */ - default void transform(IFactoryAnnotation annotation, Method method) { + default void transform(IFactoryAnnotation annotation, @Nullable Method method) { // not implemented } diff --git a/testng-core-api/src/main/java/org/testng/IAttributes.java b/testng-core-api/src/main/java/org/testng/IAttributes.java index 98e925cc9c..760a1503f6 100644 --- a/testng-core-api/src/main/java/org/testng/IAttributes.java +++ b/testng-core-api/src/main/java/org/testng/IAttributes.java @@ -1,13 +1,15 @@ package org.testng; import java.util.Set; +import org.jspecify.annotations.Nullable; /** A trait that is used by all interfaces that lets the user add or remove their own attributes. */ public interface IAttributes { /** * @param name The name of the attribute to return - * @return The attribute + * @return The attribute, or {@code null} when no attribute is registered under that name. */ + @Nullable Object getAttribute(String name); /** @@ -27,5 +29,6 @@ public interface IAttributes { * @param name The attribute name * @return the attribute value if found, null otherwise */ + @Nullable Object removeAttribute(String name); } diff --git a/testng-core-api/src/main/java/org/testng/IClass.java b/testng-core-api/src/main/java/org/testng/IClass.java index 3f013ba88a..f8833baf31 100644 --- a/testng-core-api/src/main/java/org/testng/IClass.java +++ b/testng-core-api/src/main/java/org/testng/IClass.java @@ -1,5 +1,6 @@ package org.testng; +import org.jspecify.annotations.Nullable; import org.testng.xml.XmlClass; import org.testng.xml.XmlTest; @@ -9,13 +10,22 @@ public interface IClass { /** @return this test class name. This is the name of the corresponding Java class. */ String getName(); - /** @return the <test> tag this class was found in. */ + /** + * @return the <test> tag this class was found in, or {@code null} when it was not found in + * one. + */ + @Nullable XmlTest getXmlTest(); - /** @return the *lt;class> tag this class was found in. */ + /** + * @return the *lt;class> tag this class was found in, or {@code null} when it was not found in + * one. + */ + @Nullable XmlClass getXmlClass(); /** @return its test name if this class implements org.testng.ITest, null otherwise. */ + @Nullable String getTestName(); /** @return the Java class corresponding to this IClass. */ @@ -43,13 +53,13 @@ public interface IClass { * @deprecated - As of TestNG v7.10.0 */ @Deprecated - default Object[] getInstances(boolean create, String errorMsgPrefix) { + default Object[] getInstances(boolean create, @Nullable String errorMsgPrefix) { return getInstances(create); } /** @deprecated - As of TestNG v7.10.0 */ @Deprecated - long[] getInstanceHashCodes(); + long @Nullable [] getInstanceHashCodes(); /** * @param instance - The instance to be added. diff --git a/testng-core-api/src/main/java/org/testng/IConfigurationListener.java b/testng-core-api/src/main/java/org/testng/IConfigurationListener.java index 9d755de79d..bb2f32682a 100644 --- a/testng-core-api/src/main/java/org/testng/IConfigurationListener.java +++ b/testng-core-api/src/main/java/org/testng/IConfigurationListener.java @@ -1,5 +1,7 @@ package org.testng; +import org.jspecify.annotations.Nullable; + /** Listener interface for events related to configuration methods. */ public interface IConfigurationListener extends ITestNGListener { @@ -16,9 +18,9 @@ default void onConfigurationSuccess(ITestResult tr) { * Invoked whenever a configuration method succeeded. * * @param tr The test result - * @param tm The test method + * @param tm The test method, or {@code null} when the configuration method is not bound to one */ - default void onConfigurationSuccess(ITestResult tr, ITestNGMethod tm) { + default void onConfigurationSuccess(ITestResult tr, @Nullable ITestNGMethod tm) { // not implemented } @@ -35,9 +37,9 @@ default void onConfigurationFailure(ITestResult tr) { * Invoked whenever a configuration method failed. * * @param tr The test result - * @param tm The test method + * @param tm The test method, or {@code null} when the configuration method is not bound to one */ - default void onConfigurationFailure(ITestResult tr, ITestNGMethod tm) { + default void onConfigurationFailure(ITestResult tr, @Nullable ITestNGMethod tm) { // not implemented } @@ -54,9 +56,9 @@ default void onConfigurationSkip(ITestResult tr) { * Invoked whenever a configuration method was skipped. * * @param tr The test result - * @param tm The test method + * @param tm The test method, or {@code null} when the configuration method is not bound to one */ - default void onConfigurationSkip(ITestResult tr, ITestNGMethod tm) { + default void onConfigurationSkip(ITestResult tr, @Nullable ITestNGMethod tm) { // not implemented } @@ -73,9 +75,9 @@ default void beforeConfiguration(ITestResult tr) { * Invoked before a configuration method is invoked. * * @param tr The test result - * @param tm The test method + * @param tm The test method, or {@code null} when the configuration method is not bound to one */ - default void beforeConfiguration(ITestResult tr, ITestNGMethod tm) { + default void beforeConfiguration(ITestResult tr, @Nullable ITestNGMethod tm) { // not implemented } } diff --git a/testng-core-api/src/main/java/org/testng/IDataProviderInterceptor.java b/testng-core-api/src/main/java/org/testng/IDataProviderInterceptor.java index 8eb816bdc2..b5b4fed2bd 100644 --- a/testng-core-api/src/main/java/org/testng/IDataProviderInterceptor.java +++ b/testng-core-api/src/main/java/org/testng/IDataProviderInterceptor.java @@ -1,6 +1,7 @@ package org.testng; import java.util.Iterator; +import org.jspecify.annotations.Nullable; /** * This interface helps define an interceptor for data providers. Implementations of this TestNG @@ -25,5 +26,5 @@ Iterator intercept( Iterator original, IDataProviderMethod dataProviderMethod, ITestNGMethod method, - ITestContext iTestContext); + @Nullable ITestContext iTestContext); } diff --git a/testng-core-api/src/main/java/org/testng/IDataProviderListener.java b/testng-core-api/src/main/java/org/testng/IDataProviderListener.java index b4a59bbde9..67be7c7630 100644 --- a/testng-core-api/src/main/java/org/testng/IDataProviderListener.java +++ b/testng-core-api/src/main/java/org/testng/IDataProviderListener.java @@ -1,5 +1,7 @@ package org.testng; +import org.jspecify.annotations.Nullable; + /** A listener that gets invoked before and after a data provider is invoked by TestNG. */ public interface IDataProviderListener extends ITestNGListener { @@ -12,7 +14,9 @@ public interface IDataProviderListener extends ITestNGListener { * @param iTestContext - The current test context */ default void beforeDataProviderExecution( - IDataProviderMethod dataProviderMethod, ITestNGMethod method, ITestContext iTestContext) { + IDataProviderMethod dataProviderMethod, + ITestNGMethod method, + @Nullable ITestContext iTestContext) { // not implemented } @@ -25,7 +29,9 @@ default void beforeDataProviderExecution( * @param iTestContext - The current test context */ default void afterDataProviderExecution( - IDataProviderMethod dataProviderMethod, ITestNGMethod method, ITestContext iTestContext) { + IDataProviderMethod dataProviderMethod, + ITestNGMethod method, + @Nullable ITestContext iTestContext) { // not implemented } @@ -39,7 +45,8 @@ default void afterDataProviderExecution( * @param t - The {@link RuntimeException} that embeds the actual exception. Use {@link * RuntimeException#getCause()} to get to the actual exception. */ - default void onDataProviderFailure(ITestNGMethod method, ITestContext ctx, RuntimeException t) { + default void onDataProviderFailure( + ITestNGMethod method, @Nullable ITestContext ctx, RuntimeException t) { // not implemented } } diff --git a/testng-core-api/src/main/java/org/testng/IDataProviderMethod.java b/testng-core-api/src/main/java/org/testng/IDataProviderMethod.java index fb972de800..f4079b3bbc 100644 --- a/testng-core-api/src/main/java/org/testng/IDataProviderMethod.java +++ b/testng-core-api/src/main/java/org/testng/IDataProviderMethod.java @@ -2,6 +2,7 @@ import java.lang.reflect.Method; import java.util.List; +import org.jspecify.annotations.Nullable; /** Represents the attributes of a {@link org.testng.annotations.DataProvider} annotated method. */ public interface IDataProviderMethod { @@ -9,12 +10,16 @@ public interface IDataProviderMethod { * @return - The instance to which the data provider belongs to. null if the data * provider is a static one. */ + @Nullable Object getInstance(); /** * @return - A {@link Method} object that represents the actual {@literal @}{@link - * org.testng.annotations.DataProvider} method. + * org.testng.annotations.DataProvider} method, or {@code null} once TestNG has released it -- + * which it does as soon as the data provider has yielded its rows, so that the method and its + * instance do not outlive the run. */ + @Nullable Method getMethod(); /** @return The name of this DataProvider. */ diff --git a/testng-core-api/src/main/java/org/testng/IInjectorFactory.java b/testng-core-api/src/main/java/org/testng/IInjectorFactory.java index 7c0db1af3d..e2c94ecf64 100644 --- a/testng-core-api/src/main/java/org/testng/IInjectorFactory.java +++ b/testng-core-api/src/main/java/org/testng/IInjectorFactory.java @@ -3,7 +3,7 @@ import com.google.inject.Injector; import com.google.inject.Module; import com.google.inject.Stage; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; /** Allows customization of the {@link Injector} creation when working with dependency injection. */ public interface IInjectorFactory { diff --git a/testng-core-api/src/main/java/org/testng/IMethodInstance.java b/testng-core-api/src/main/java/org/testng/IMethodInstance.java index 5a181db231..364a8322d7 100644 --- a/testng-core-api/src/main/java/org/testng/IMethodInstance.java +++ b/testng-core-api/src/main/java/org/testng/IMethodInstance.java @@ -1,9 +1,16 @@ package org.testng; +import org.jspecify.annotations.Nullable; + /** This interface captures a test method along with all the instances it should be run on. */ public interface IMethodInstance { ITestNGMethod getMethod(); + /** + * @return The instance the method will be invoked on, or {@code null} when the method carries no + * instance. + */ + @Nullable Object getInstance(); } diff --git a/testng-core-api/src/main/java/org/testng/IModuleFactory.java b/testng-core-api/src/main/java/org/testng/IModuleFactory.java index 8055be8393..8d48c49609 100644 --- a/testng-core-api/src/main/java/org/testng/IModuleFactory.java +++ b/testng-core-api/src/main/java/org/testng/IModuleFactory.java @@ -1,6 +1,7 @@ package org.testng; import com.google.inject.Module; +import org.jspecify.annotations.Nullable; /** * This interface is used by the moduleFactory attribute of the @Guice annotation. It allows users @@ -13,5 +14,5 @@ public interface IModuleFactory { * @param testClass The test class * @return The Guice module that should be used to get an instance of this test class. */ - Module createModule(ITestContext context, Class testClass); + Module createModule(@Nullable ITestContext context, Class testClass); } diff --git a/testng-core-api/src/main/java/org/testng/ISuite.java b/testng-core-api/src/main/java/org/testng/ISuite.java index 827cb04600..57c80f1cfc 100644 --- a/testng-core-api/src/main/java/org/testng/ISuite.java +++ b/testng-core-api/src/main/java/org/testng/ISuite.java @@ -4,6 +4,7 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.testng.internal.annotations.IAnnotationFinder; import org.testng.xml.XmlSuite; @@ -21,6 +22,7 @@ public interface ISuite extends IAttributes { Map getResults(); /** @return The object factory used to create all test instances. */ + @Nullable ITestObjectFactory getObjectFactory(); /** @return The output directory used for the reports. */ @@ -37,6 +39,7 @@ public interface ISuite extends IAttributes { * @param parameterName The name of the parameter * @return The value of this parameter, or null if none was specified. */ + @Nullable String getParameter(String parameterName); /** @@ -59,6 +62,7 @@ public interface ISuite extends IAttributes { * @return The host where this suite was run, or null if it was run locally. The returned string * has the form: host:port */ + @Nullable String getHost(); /** @@ -76,6 +80,7 @@ public interface ISuite extends IAttributes { void addListener(ITestNGListener listener); + @Nullable Injector getParentInjector(); void setParentInjector(Injector injector); diff --git a/testng-core-api/src/main/java/org/testng/ITestClassFinder.java b/testng-core-api/src/main/java/org/testng/ITestClassFinder.java index 1d82c0e5bd..e29f2c63e4 100644 --- a/testng-core-api/src/main/java/org/testng/ITestClassFinder.java +++ b/testng-core-api/src/main/java/org/testng/ITestClassFinder.java @@ -1,5 +1,7 @@ package org.testng; +import org.jspecify.annotations.Nullable; + /** * This class is used by TestNG to locate the test classes. * @@ -18,7 +20,9 @@ public interface ITestClassFinder { * Return the IClass for a given class * * @param cls The class - * @return The related IClass + * @return The related IClass, or {@code null} when this finder holds none for that + * class. */ + @Nullable IClass getIClass(Class cls); } diff --git a/testng-core-api/src/main/java/org/testng/ITestContext.java b/testng-core-api/src/main/java/org/testng/ITestContext.java index 5de1055d5b..fe53d2f151 100644 --- a/testng-core-api/src/main/java/org/testng/ITestContext.java +++ b/testng-core-api/src/main/java/org/testng/ITestContext.java @@ -2,6 +2,7 @@ import java.util.Collection; import java.util.Date; +import org.jspecify.annotations.Nullable; import org.testng.xml.XmlTest; /** @@ -14,12 +15,14 @@ public interface ITestContext extends IAttributes { /** @return The name of this test. */ + @Nullable String getName(); /** @return When this test started running. */ Date getStartDate(); /** @return When this test stopped running. */ + @Nullable Date getEndDate(); /** @return A list of all the tests that run successfully. */ @@ -59,6 +62,7 @@ public interface ITestContext extends IAttributes { * @return The host where this test was run, or null if it was run locally. The returned string * has the form: host:port */ + @Nullable String getHost(); /** @return All the methods that were not included in this test run. */ @@ -76,6 +80,7 @@ public interface ITestContext extends IAttributes { /** @return the current XmlTest. */ XmlTest getCurrentXmlTest(); + @Nullable default IInjectorFactory getInjectorFactory() { return null; } diff --git a/testng-core-api/src/main/java/org/testng/ITestNGListenerFactory.java b/testng-core-api/src/main/java/org/testng/ITestNGListenerFactory.java index a4b48dbfca..7c0ba681e7 100644 --- a/testng-core-api/src/main/java/org/testng/ITestNGListenerFactory.java +++ b/testng-core-api/src/main/java/org/testng/ITestNGListenerFactory.java @@ -1,5 +1,7 @@ package org.testng; +import org.jspecify.annotations.Nullable; + /** * A factory used to create instances of ITestNGListener. Users can implement this interface in any * of their test classes but there can be only one such instance. @@ -13,5 +15,6 @@ public interface ITestNGListenerFactory { * @param listenerClass The class of listener to create * @return The created listener */ + @Nullable ITestNGListener createListener(Class listenerClass); } diff --git a/testng-core-api/src/main/java/org/testng/ITestNGMethod.java b/testng-core-api/src/main/java/org/testng/ITestNGMethod.java index 48d956ce54..a6c8680f6f 100644 --- a/testng-core-api/src/main/java/org/testng/ITestNGMethod.java +++ b/testng-core-api/src/main/java/org/testng/ITestNGMethod.java @@ -5,6 +5,7 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.Callable; +import org.jspecify.annotations.Nullable; import org.testng.annotations.CustomAttribute; import org.testng.internal.ConstructorOrMethod; import org.testng.internal.IParameterInfo; @@ -23,6 +24,11 @@ public interface ITestNGMethod extends Cloneable { */ Class getRealClass(); + /** + * @return The test class this method is bound to, or {@code null} while it has not been bound to + * one yet. + */ + @Nullable ITestClass getTestClass(); /** @@ -39,6 +45,11 @@ public interface ITestNGMethod extends Cloneable { */ String getMethodName(); + /** + * @return The instance this method will be invoked on, or {@code null} when the method carries no + * instance. + */ + @Nullable Object getInstance(); /** @@ -58,10 +69,11 @@ public interface ITestNGMethod extends Cloneable { */ String[] getGroupsDependedUpon(); - /** @return If a group was not found. */ + /** @return The group that was not found, or {@code null} when every group was found. */ + @Nullable String getMissingGroup(); - void setMissingGroup(String group); + void setMissingGroup(@Nullable String group); String[] getBeforeGroups(); @@ -145,7 +157,8 @@ default boolean hasAfterGroupsConfiguration() { /** @return the success percentage for this method (between 0 and 100). */ int getSuccessPercentage(); - /** @return The id of the thread this method was run in. */ + /** @return The id of the thread this method was run in, or {@code null} before it has run. */ + @Nullable String getId(); void setId(String id); @@ -170,9 +183,11 @@ default boolean hasAfterGroupsConfiguration() { boolean getEnabled(); + /** @return The description of this method, or {@code null} when it declares none. */ + @Nullable String getDescription(); - void setDescription(String description); + void setDescription(@Nullable String description); void incrementCurrentInvocationCount(); @@ -188,6 +203,11 @@ default boolean hasAfterGroupsConfiguration() { ITestNGMethod clone(); + /** + * @param result The result to pick a retry analyzer for. + * @return The retry analyzer for that result, or {@code null} when the method declares none. + */ + @Nullable IRetryAnalyzer getRetryAnalyzer(ITestResult result); void setRetryAnalyzerClass(Class clazz); @@ -240,7 +260,8 @@ default boolean hasAfterGroupsConfiguration() { void setInterceptedPriority(int priority); - /** @return the XmlTest this method belongs to. */ + /** @return the XmlTest this method belongs to, or {@code null} when it belongs to none. */ + @Nullable XmlTest getXmlTest(); ConstructorOrMethod getConstructorOrMethod(); @@ -264,11 +285,13 @@ default boolean isDataDriven() { /** * @return - A {@link IParameterInfo} object that represents details about the parameters - * associated with the factory method. + * associated with the factory method, or {@code null} when no factory produced the test + * class. * @deprecated - As of TestNG v7.13.0. It exposes a type from an internal package; * use {@link #getFactoryInstance()} instead. */ @Deprecated + @Nullable default IParameterInfo getFactoryMethodParamsInfo() { return null; } @@ -296,6 +319,7 @@ default CustomAttribute[] getAttributes() { * @return - An {@link IDataProviderMethod} for a data provider powered test method and null * otherwise. */ + @Nullable default IDataProviderMethod getDataProviderMethod() { return null; } diff --git a/testng-core-api/src/main/java/org/testng/ITestObjectFactory.java b/testng-core-api/src/main/java/org/testng/ITestObjectFactory.java index b2c9505589..97f7d4f46d 100644 --- a/testng-core-api/src/main/java/org/testng/ITestObjectFactory.java +++ b/testng-core-api/src/main/java/org/testng/ITestObjectFactory.java @@ -1,6 +1,7 @@ package org.testng; import java.lang.reflect.Constructor; +import org.jspecify.annotations.Nullable; import org.testng.internal.objects.InstanceCreator; /** Parent interface of all the object factories. */ @@ -14,7 +15,11 @@ default T newInstance(String clsName, Object... parameters) { return InstanceCreator.newInstance(clsName, parameters); } - default T newInstance(Constructor constructor, Object... parameters) { + /** + * @return The new instance, or {@code null} when the factory could not build one -- which the + * default factory answers for a class whose constructor it cannot reach. + */ + default @Nullable T newInstance(Constructor constructor, Object... parameters) { return InstanceCreator.newInstance(constructor, parameters); } } diff --git a/testng-core-api/src/main/java/org/testng/ITestResult.java b/testng-core-api/src/main/java/org/testng/ITestResult.java index 7dffa837d9..bd76939f63 100644 --- a/testng-core-api/src/main/java/org/testng/ITestResult.java +++ b/testng-core-api/src/main/java/org/testng/ITestResult.java @@ -4,6 +4,7 @@ import java.util.Collections; import java.util.List; import java.util.Optional; +import org.jspecify.annotations.Nullable; import org.testng.internal.thread.ThreadTimeoutException; /** @@ -32,7 +33,11 @@ default int getParameterIndex() { void setStatus(int status); - /** @return The test method this result represents. */ + /** + * @return The test method this result represents, or {@code null} while the result has not been + * bound to one. + */ + @Nullable ITestNGMethod getMethod(); /** @return The parameters this method was invoked with. */ @@ -47,9 +52,10 @@ default int getParameterIndex() { * @return The throwable that was thrown while running the method, or null if no exception was * thrown. */ + @Nullable Throwable getThrowable(); - void setThrowable(Throwable throwable); + void setThrowable(@Nullable Throwable throwable); /** @return the start date for this test, in milliseconds. */ long getStartMillis(); @@ -59,7 +65,11 @@ default int getParameterIndex() { void setEndMillis(long millis); - /** @return The name of this TestResult, typically identical to the name of the method. */ + /** + * @return The name of this TestResult, typically identical to the name of the method, or {@code + * null} while the result has not been bound to a method. + */ + @Nullable String getName(); /** @return true if if this test run is a SUCCESS */ @@ -69,9 +79,14 @@ default int getParameterIndex() { * @return The host where this suite was run, or null if it was run locally. The returned string * has the form: host:port */ + @Nullable String getHost(); - /** @return The instance on which this method was run. */ + /** + * @return The instance on which this method was run, or {@code null} when the method carries no + * instance. + */ + @Nullable Object getInstance(); /** @@ -97,15 +112,24 @@ default Optional getFactoryInstance() { * @return The test name if this result's related instance implements ITest or * use @Test(testName=...), null otherwise. */ + @Nullable String getTestName(); + /** + * @return The name of the instance this method was run on, or {@code null} when it carries none. + */ + @Nullable String getInstanceName(); - /** @return the {@link ITestContext} for this test result. */ + /** + * @return the {@link ITestContext} for this test result, or {@code null} when the result was + * built outside a test context. + */ + @Nullable ITestContext getTestContext(); /** @param name - The new name to be used as a test name */ - void setTestName(String name); + void setTestName(@Nullable String name); /** * @return - true if the test was retried again by an implementation of {@link diff --git a/testng-core-api/src/main/java/org/testng/Reporter.java b/testng-core-api/src/main/java/org/testng/Reporter.java index 759fd8bb5a..319ae7433f 100644 --- a/testng-core-api/src/main/java/org/testng/Reporter.java +++ b/testng-core-api/src/main/java/org/testng/Reporter.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.jspecify.annotations.Nullable; import org.testng.internal.AutoCloseableLock; import org.testng.internal.Utils; import org.testng.util.Strings; @@ -41,7 +42,11 @@ public class Reporter { // valid TestResult objects. private static final ThreadLocal> m_orphanedOutput = new InheritableThreadLocal<>(); - public static void setCurrentTestResult(ITestResult m) { + /** + * @param m The result the current thread is reporting into, or {@code null} to clear it once the + * invocation is over. + */ + public static void setCurrentTestResult(@Nullable ITestResult m) { m_currentTestResult.set(m); } @@ -70,13 +75,13 @@ public static void setEscapeHtml(boolean escapeHtml) { private static final AutoCloseableLock lockForLogging = new AutoCloseableLock(); - private static void log(String s, ITestResult m) { + private static void log(String s, @Nullable ITestResult m) { try (AutoCloseableLock ignore = lockForLogging.lock()) { logToReports(s, m); } } - private static void logToReports(String s, ITestResult m) { + private static void logToReports(String s, @Nullable ITestResult m) { // Escape for the HTML reports. if (m_escapeHtml) { s = Strings.escapeHtml(s); @@ -161,8 +166,8 @@ public static void log(String s, int level) { } } - /** @return the current test result. */ - public static ITestResult getCurrentTestResult() { + /** @return the current test result, or {@code null} outside an invocation. */ + public static @Nullable ITestResult getCurrentTestResult() { return m_currentTestResult.get(); } diff --git a/testng-core-api/src/main/java/org/testng/TestNGException.java b/testng-core-api/src/main/java/org/testng/TestNGException.java index fd1e31c9bd..7a2676ff82 100644 --- a/testng-core-api/src/main/java/org/testng/TestNGException.java +++ b/testng-core-api/src/main/java/org/testng/TestNGException.java @@ -1,5 +1,7 @@ package org.testng; +import org.jspecify.annotations.Nullable; + /** The base class for all exceptions thrown by TestNG. */ public class TestNGException extends RuntimeException { @@ -9,11 +11,11 @@ public TestNGException(Throwable t) { super(t); } - public TestNGException(String string) { + public TestNGException(@Nullable String string) { super("\n" + string); } - public TestNGException(String string, Throwable t) { + public TestNGException(@Nullable String string, Throwable t) { super("\n" + string, t); } } diff --git a/testng-core-api/src/main/java/org/testng/internal/ReporterConfig.java b/testng-core-api/src/main/java/org/testng/internal/ReporterConfig.java index c6060d8e0f..1e84f92700 100644 --- a/testng-core-api/src/main/java/org/testng/internal/ReporterConfig.java +++ b/testng-core-api/src/main/java/org/testng/internal/ReporterConfig.java @@ -44,9 +44,9 @@ public String serialize() { return sb.toString(); } - public static @Nullable ReporterConfig deserialize(String inputString) { + public static @Nullable ReporterConfig deserialize(@Nullable String inputString) { - if (Utils.isStringEmpty(inputString)) { + if (inputString == null || Utils.isStringEmpty(inputString)) { return null; } diff --git a/testng-core-api/src/main/java/org/testng/internal/Utils.java b/testng-core-api/src/main/java/org/testng/internal/Utils.java index f3d2d3e042..bee4baa590 100644 --- a/testng-core-api/src/main/java/org/testng/internal/Utils.java +++ b/testng-core-api/src/main/java/org/testng/internal/Utils.java @@ -15,12 +15,17 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; import org.jspecify.annotations.Nullable; +import org.testng.ITestClass; +import org.testng.ITestContext; import org.testng.ITestNGMethod; +import org.testng.ITestResult; import org.testng.TestNGException; import org.testng.log4testng.Logger; import org.testng.reporters.XMLStringBuffer; @@ -446,6 +451,65 @@ public static String toString(Object object, Class objectClass) { } } + /** + * The test method a result was produced for. + * + *

{@link ITestResult#getMethod()} answers {@code null} on the parameter carrier the invoker + * builds before it knows which invocation it is reporting; that carrier is replaced by a + * method-bearing result before any listener or reporter sees it, so every result that reaches one + * has a method. Use this rather than dereferencing the accessor, so that the day a carrier does + * escape it is named at the boundary instead of raising a bare {@code NullPointerException} + * somewhere further in. + * + * @param result The result to read the method of. + * @return The test method, never {@code null}. + */ + public static ITestNGMethod requireMethodOf(ITestResult result) { + return Objects.requireNonNull(result.getMethod(), "a reported result carries a test method"); + } + + /** + * The test class a method was bound to. + * + *

{@link ITestNGMethod#getTestClass()} answers {@code null} until the finder binds the method + * to its class, which happens before the method is scheduled; every method a runner, a reporter + * or a listener sees is bound. Use this rather than dereferencing the accessor, so that an + * unbound method is named at the boundary. + * + * @param method The method to read the test class of. + * @return The test class, never {@code null}. + */ + public static ITestClass requireTestClassOf(ITestNGMethod method) { + return Objects.requireNonNull(method.getTestClass(), "a scheduled method is bound to a class"); + } + + /** + * The test context a result was produced in. + * + *

{@link ITestResult#getTestContext()} answers {@code null} for a result built outside a run + * -- the invoker always passes the context it is running under. Use this rather than + * dereferencing the accessor, so that a context-less result is named at the boundary. + * + * @param result The result to read the context of. + * @return The test context, never {@code null}. + */ + public static ITestContext requireTestContextOf(ITestResult result) { + return Objects.requireNonNull(result.getTestContext(), "a reported result carries a context"); + } + + /** + * The moment a test context finished. + * + *

{@link ITestContext#getEndDate()} answers {@code null} while the <test> is still + * running; a reporter only ever sees a finished one. + * + * @param context The context to read the end date of. + * @return The end date, never {@code null}. + */ + public static Date requireEndDateOf(ITestContext context) { + return Objects.requireNonNull(context.getEndDate(), "a reported test context has finished"); + } + public static String detailedMethodName(ITestNGMethod method, boolean fqn) { String tempName = annotationFormFor(method); if (!tempName.isEmpty()) { diff --git a/testng-core-api/src/main/java/org/testng/package-info.java b/testng-core-api/src/main/java/org/testng/package-info.java new file mode 100644 index 0000000000..5d0a938f61 --- /dev/null +++ b/testng-core-api/src/main/java/org/testng/package-info.java @@ -0,0 +1,5 @@ +/** The published API: the interfaces a test is written against, and the runner that drives them. */ +@NullMarked +package org.testng; + +import org.jspecify.annotations.NullMarked; diff --git a/testng-core-api/src/main/java/org/testng/xml/XmlSuite.java b/testng-core-api/src/main/java/org/testng/xml/XmlSuite.java index 1123e2e5ab..ab76c43983 100644 --- a/testng-core-api/src/main/java/org/testng/xml/XmlSuite.java +++ b/testng-core-api/src/main/java/org/testng/xml/XmlSuite.java @@ -271,7 +271,7 @@ public void setObjectFactoryClass( * * @param parallel The parallel mode. */ - public void setParallel(ParallelMode parallel) { + public void setParallel(@Nullable ParallelMode parallel) { m_parallel = parallel == null ? DEFAULT_PARALLEL : parallel; } diff --git a/testng-core-api/testng-core-api-build.gradle.kts b/testng-core-api/testng-core-api-build.gradle.kts index 49b4bc6e2b..b603ea51f7 100644 --- a/testng-core-api/testng-core-api-build.gradle.kts +++ b/testng-core-api/testng-core-api-build.gradle.kts @@ -8,7 +8,6 @@ registerOptionalFeatureVariants("guice", buildParameters.targetJavaVersion, task dependencies { api(projects.testngCollections) - compileOnly("com.github.spotbugs:spotbugs:4.10.3") "guiceApi"(platform("com.google.inject:guice-bom:6.0.0")) "guiceApi"("com.google.inject:guice") diff --git a/testng-core/src/main/java/org/testng/ClassMethodMap.java b/testng-core/src/main/java/org/testng/ClassMethodMap.java index f86bd5c194..966ca89366 100644 --- a/testng-core/src/main/java/org/testng/ClassMethodMap.java +++ b/testng-core/src/main/java/org/testng/ClassMethodMap.java @@ -3,9 +3,11 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; +import org.jspecify.annotations.Nullable; import org.testng.internal.IInstanceIdentity; import org.testng.internal.XmlMethodSelector; @@ -23,7 +25,8 @@ public class ClassMethodMap { private final Map> beforeClassMethods = new ConcurrentHashMap<>(); private final Map> afterClassMethods = new ConcurrentHashMap<>(); - public ClassMethodMap(List methods, XmlMethodSelector xmlMethodSelector) { + public ClassMethodMap( + List methods, @Nullable XmlMethodSelector xmlMethodSelector) { for (ITestNGMethod m : methods) { // Only add to the class map methods that are included in the // method selector. We can pass a null context here since the selector @@ -46,7 +49,7 @@ public ClassMethodMap(List methods, XmlMethodSelector xmlMethodSe * @param instance The test instance * @return true if it is the last of its class */ - public boolean removeAndCheckIfLast(ITestNGMethod m, Object instance) { + public boolean removeAndCheckIfLast(ITestNGMethod m, @Nullable Object instance) { // Look up by the method's own per-instance id so this matches the id-keyed map above (and never // instantiates anything); the passed instance is retained only for the diagnostic message. Collection l = classMap.get(IInstanceIdentity.getInstanceId(m)); @@ -58,7 +61,7 @@ public boolean removeAndCheckIfLast(ITestNGMethod m, Object instance) { // It's the last method of this class if all the methods remaining in the list belong to a // different class for (ITestNGMethod tm : l) { - if (tm.getEnabled() && tm.getTestClass().equals(m.getTestClass())) { + if (tm.getEnabled() && Objects.equals(tm.getTestClass(), m.getTestClass())) { return false; } } diff --git a/testng-core/src/main/java/org/testng/CliRunners.java b/testng-core/src/main/java/org/testng/CliRunners.java index 37a5052be1..2db4d4cf22 100644 --- a/testng-core/src/main/java/org/testng/CliRunners.java +++ b/testng-core/src/main/java/org/testng/CliRunners.java @@ -3,6 +3,7 @@ import java.util.Iterator; import java.util.ServiceConfigurationError; import java.util.ServiceLoader; +import org.jspecify.annotations.Nullable; import org.testng.log4testng.Logger; /** @@ -23,19 +24,19 @@ final class CliRunners { + "(along with its parsing library), or drive TestNG through the org.testng.TestNG Java " + "API instead."; - private static volatile ITestNGCliRunner cached; + private static volatile @Nullable ITestNGCliRunner cached; /** * Why the last lookup came back empty. A provider that is present but fails to load reports the * very same "nothing found" outcome as a provider that is simply absent, so the cause has to be * carried along or the diagnostic tells people to install what they already installed. */ - private static volatile Throwable lastFailure; + private static volatile @Nullable Throwable lastFailure; private CliRunners() {} /** @return the installed runner, or {@code null} when none is available. */ - static ITestNGCliRunner find() { + static @Nullable ITestNGCliRunner find() { ITestNGCliRunner local = cached; if (local != null) { return local; @@ -74,7 +75,7 @@ static ITestNGCliRunner required() { return runner; } - private static ITestNGCliRunner load(ClassLoader loader) { + private static @Nullable ITestNGCliRunner load(ClassLoader loader) { try { Iterator it = ServiceLoader.load(ITestNGCliRunner.class, loader).iterator(); if (!it.hasNext()) { diff --git a/testng-core/src/main/java/org/testng/CommandLineArgs.java b/testng-core/src/main/java/org/testng/CommandLineArgs.java index 30e2144370..809bf75685 100644 --- a/testng-core/src/main/java/org/testng/CommandLineArgs.java +++ b/testng-core/src/main/java/org/testng/CommandLineArgs.java @@ -2,6 +2,7 @@ import java.util.ArrayList; import java.util.List; +import org.jspecify.annotations.Nullable; import org.testng.xml.XmlSuite; /** @@ -26,22 +27,22 @@ public class CommandLineArgs { public static final String VERBOSE = "-verbose"; /** Level of verbosity. */ - public Integer verbose; + public @Nullable Integer verbose; public static final String GROUPS = "-groups"; /** Comma-separated list of group names to be run. */ - public String groups; + public @Nullable String groups; public static final String EXCLUDED_GROUPS = "-excludegroups"; /** Comma-separated list of group names to exclude. */ - public String excludedGroups; + public @Nullable String excludedGroups; public static final String OUTPUT_DIRECTORY = "-d"; /** Output directory. */ - public String outputDirectory; + public @Nullable String outputDirectory; public static final String MIXED = "-mixed"; @@ -54,57 +55,57 @@ public class CommandLineArgs { public static final String LISTENER = "-listener"; /** List of .class files or list of class names implementing ITestListener or ISuiteListener. */ - public String listener; + public @Nullable String listener; public static final String LISTENER_COMPARATOR = "-listenercomparator"; /** An implementation of ListenerComparator that determines order of execution for listeners. */ - public String listenerComparator; + public @Nullable String listenerComparator; public static final String METHOD_SELECTORS = "-methodselectors"; /** List of .class files or list of class names implementing IMethodSelector. */ - public String methodSelectors; + public @Nullable String methodSelectors; public static final String OBJECT_FACTORY = "-objectfactory"; /** Fully qualified class name that implements org.testng.ITestObjectFactory. */ - public String objectFactory; + public @Nullable String objectFactory; public static final String PARALLEL = "-parallel"; /** Parallel mode (methods, tests or classes). */ - public XmlSuite.ParallelMode parallelMode; + public XmlSuite.@Nullable ParallelMode parallelMode; public static final String CONFIG_FAILURE_POLICY = "-configfailurepolicy"; /** Configuration failure policy (skip or continue). */ - public String configFailurePolicy; + public @Nullable String configFailurePolicy; public static final String THREAD_COUNT = "-threadcount"; /** Number of threads to use when running tests in parallel. */ - public Integer threadCount; + public @Nullable Integer threadCount; public static final String DATA_PROVIDER_THREAD_COUNT = "-dataproviderthreadcount"; /** Number of threads to use when running data providers. */ - public Integer dataProviderThreadCount; + public @Nullable Integer dataProviderThreadCount; public static final String SUITE_NAME = "-suitename"; /** Default name of test suite, if not specified in suite definition file or source code. */ - public String suiteName; + public @Nullable String suiteName; public static final String TEST_NAME = "-testname"; /** Default name of test, if not specified in suite definition file or source code. */ - public String testName; + public @Nullable String testName; public static final String REPORTER = "-reporter"; /** Extended configuration for custom report listener. */ - public String reporter; + public @Nullable String reporter; public static final String USE_DEFAULT_LISTENERS = "-usedefaultlisteners"; @@ -113,17 +114,17 @@ public class CommandLineArgs { public static final String SKIP_FAILED_INVOCATION_COUNTS = "-skipfailedinvocationcounts"; - public Boolean skipFailedInvocationCounts; + public @Nullable Boolean skipFailedInvocationCounts; public static final String TEST_CLASS = "-testclass"; /** The list of test classes. */ - public String testClass; + public @Nullable String testClass; public static final String TEST_NAMES = "-testnames"; /** The list of test names to run. */ - public String testNames; + public @Nullable String testNames; public static final String IGNORE_MISSED_TEST_NAMES = "-ignoreMissedTestNames"; @@ -133,7 +134,7 @@ public class CommandLineArgs { public static final String TEST_JAR = "-testjar"; /** A jar file containing the tests. */ - public String testJar; + public @Nullable String testJar; public static final String XML_PATH_IN_JAR = "-xmlpathinjar"; public static final String XML_PATH_IN_JAR_DEFAULT = "testng.xml"; @@ -144,12 +145,12 @@ public class CommandLineArgs { public static final String TEST_RUNNER_FACTORY = "-testrunfactory"; /** The factory used to create tests. */ - public String testRunnerFactory; + public @Nullable String testRunnerFactory; public static final String LISTENER_FACTORY = "-listenerfactory"; /** The factory used to create TestNG listeners. */ - public String listenerFactory; + public @Nullable String listenerFactory; public static final String METHODS = "-methods"; @@ -175,12 +176,12 @@ public class CommandLineArgs { public static final String THREAD_POOL_FACTORY_CLASS = "-threadpoolfactoryclass"; /** The threadpool executor factory implementation that TestNG should use. */ - public String threadPoolFactoryClass; + public @Nullable String threadPoolFactoryClass; public static final String DEPENDENCY_INJECTOR_FACTORY = "-dependencyinjectorfactory"; /** The dependency injector factory implementation that TestNG should use. */ - public String dependencyInjectorFactoryClass; + public @Nullable String dependencyInjectorFactoryClass; public static final String FAIL_IF_ALL_TESTS_SKIPPED = "-failwheneverythingskipped"; diff --git a/testng-core/src/main/java/org/testng/DataProviderHolder.java b/testng-core/src/main/java/org/testng/DataProviderHolder.java index f014ec8ce5..9d5efb591a 100644 --- a/testng-core/src/main/java/org/testng/DataProviderHolder.java +++ b/testng-core/src/main/java/org/testng/DataProviderHolder.java @@ -7,6 +7,7 @@ import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; +import org.jspecify.annotations.Nullable; import org.testng.internal.IConfiguration; /** @@ -17,7 +18,7 @@ public class DataProviderHolder { private final Map, IDataProviderListener> listeners = new ConcurrentHashMap<>(); private final Collection interceptors = new HashSet<>(); - private final ListenerComparator listenerComparator; + private final @Nullable ListenerComparator listenerComparator; public DataProviderHolder(IConfiguration configuration) { this.listenerComparator = Objects.requireNonNull(configuration).getListenerComparator(); diff --git a/testng-core/src/main/java/org/testng/DependencyMap.java b/testng-core/src/main/java/org/testng/DependencyMap.java index 24a4b622ef..d7a9f33331 100644 --- a/testng-core/src/main/java/org/testng/DependencyMap.java +++ b/testng-core/src/main/java/org/testng/DependencyMap.java @@ -14,6 +14,7 @@ import org.testng.internal.IInstanceIdentity; import org.testng.internal.MethodHelper; import org.testng.internal.RuntimeBehavior; +import org.testng.internal.Utils; /** Helper class to keep track of dependencies. */ public class DependencyMap { @@ -109,8 +110,8 @@ private static boolean hasInstance( // Check for the presence of an instance via the per-instance id so a lazy @Factory instance is // not created just to resolve dependencies during collection. boolean result = - IInstanceIdentity.getInstanceId(derivedClassMethod) != null - || IInstanceIdentity.getInstanceId(baseClassMethod) != null; + IInstanceIdentity.carriesInstance(derivedClassMethod) + || IInstanceIdentity.carriesInstance(baseClassMethod); boolean params = baseClassMethod.getFactoryInstance().isPresent(); if (result && params && RuntimeBehavior.enforceThreadAffinity()) { @@ -138,10 +139,10 @@ private static boolean hasSameParameters( private static boolean isSameInstance( ITestNGMethod baseClassMethod, ITestNGMethod derivedClassMethod) { - boolean nonNullInstances = - IInstanceIdentity.getInstanceId(derivedClassMethod) != null - && IInstanceIdentity.getInstanceId(baseClassMethod) != null; - if (!nonNullInstances) { + boolean bothCarryAnInstance = + IInstanceIdentity.carriesInstance(derivedClassMethod) + && IInstanceIdentity.carriesInstance(baseClassMethod); + if (!bothCarryAnInstance) { return false; } Class baseClass = instanceClassOf(baseClassMethod); @@ -170,7 +171,8 @@ private static String constructMethodNameUsingTestClass( String currentMethodName, ITestNGMethod m) { int lastIndex = currentMethodName.lastIndexOf('.'); if (lastIndex != -1) { - return m.getTestClass().getRealClass().getName() + currentMethodName.substring(lastIndex); + return Utils.requireTestClassOf(m).getRealClass().getName() + + currentMethodName.substring(lastIndex); } return currentMethodName; } diff --git a/testng-core/src/main/java/org/testng/ITestNGCliRunner.java b/testng-core/src/main/java/org/testng/ITestNGCliRunner.java index 1ea21c4844..41d98173fc 100644 --- a/testng-core/src/main/java/org/testng/ITestNGCliRunner.java +++ b/testng-core/src/main/java/org/testng/ITestNGCliRunner.java @@ -1,5 +1,7 @@ package org.testng; +import org.jspecify.annotations.Nullable; + /** * Service provider interface backing {@link TestNG#main(String[])}. Implementations are discovered * with {@link java.util.ServiceLoader}, which keeps the command line parsing library out of {@code @@ -29,7 +31,7 @@ public interface ITestNGCliRunner { * @throws TestNGException when {@code argv} cannot be parsed or does not select anything to run. * The message is meant to be shown to the user as is. */ - TestNG run(String[] argv, ITestListener listener); + TestNG run(String[] argv, @Nullable ITestListener listener); /** * Prints the command line usage banner. Callers treat a throw as "no banner available" and fall diff --git a/testng-core/src/main/java/org/testng/JarFileUtils.java b/testng-core/src/main/java/org/testng/JarFileUtils.java index d0700210f8..8c6b99e73a 100644 --- a/testng-core/src/main/java/org/testng/JarFileUtils.java +++ b/testng-core/src/main/java/org/testng/JarFileUtils.java @@ -12,6 +12,7 @@ import java.util.Objects; import java.util.jar.JarEntry; import java.util.jar.JarFile; +import org.jspecify.annotations.Nullable; import org.testng.internal.Utils; import org.testng.util.Strings; import org.testng.xml.IPostProcessor; @@ -25,7 +26,7 @@ class JarFileUtils { private final IPostProcessor processor; private final String xmlPathInJar; private final boolean ignoreMissedTestNames; - private final List testNames; + private final @Nullable List testNames; private final List suites = new LinkedList<>(); private final XmlSuite.ParallelMode mode; @@ -37,7 +38,7 @@ class JarFileUtils { IPostProcessor processor, String xmlPathInJar, List testNames, - XmlSuite.ParallelMode mode) { + XmlSuite.@Nullable ParallelMode mode) { this(processor, xmlPathInJar, testNames, mode, false); } @@ -52,8 +53,8 @@ class JarFileUtils { JarFileUtils( IPostProcessor processor, String xmlPathInJar, - List testNames, - XmlSuite.ParallelMode mode, + @Nullable List testNames, + XmlSuite.@Nullable ParallelMode mode, boolean ignoreMissedTestNames) { this.processor = processor; this.xmlPathInJar = xmlPathInJar; diff --git a/testng-core/src/main/java/org/testng/ListenerComparator.java b/testng-core/src/main/java/org/testng/ListenerComparator.java index c02c116f76..9bb2e2832e 100644 --- a/testng-core/src/main/java/org/testng/ListenerComparator.java +++ b/testng-core/src/main/java/org/testng/ListenerComparator.java @@ -5,6 +5,7 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; +import org.jspecify.annotations.Nullable; /** * Listener interface that can be used to determine listener execution order. This interface will @@ -21,7 +22,8 @@ */ @FunctionalInterface public interface ListenerComparator extends Comparator { - static List sort(List list, ListenerComparator comparator) { + static List sort( + List list, @Nullable ListenerComparator comparator) { if (comparator == null) { return Collections.unmodifiableList(list); } @@ -31,7 +33,7 @@ static List sort(List list, ListenerComparator } static Collection sort( - Collection list, ListenerComparator comparator) { + Collection list, @Nullable ListenerComparator comparator) { if (comparator == null) { return Collections.unmodifiableCollection(list); } diff --git a/testng-core/src/main/java/org/testng/SkipException.java b/testng-core/src/main/java/org/testng/SkipException.java index 1e6fafbc8d..43850469fa 100644 --- a/testng-core/src/main/java/org/testng/SkipException.java +++ b/testng-core/src/main/java/org/testng/SkipException.java @@ -1,5 +1,6 @@ package org.testng; +import org.jspecify.annotations.Nullable; import org.testng.internal.AutoCloseableLock; /** @@ -13,7 +14,7 @@ public class SkipException extends RuntimeException { private static final long serialVersionUID = 4052142657885527260L; - private StackTraceElement[] m_stackTrace; + private StackTraceElement @Nullable [] m_stackTrace; private volatile boolean m_stackReduced; public SkipException(String skipMessage) { diff --git a/testng-core/src/main/java/org/testng/SuiteResult.java b/testng-core/src/main/java/org/testng/SuiteResult.java index d10a300262..01f8183a6f 100644 --- a/testng-core/src/main/java/org/testng/SuiteResult.java +++ b/testng-core/src/main/java/org/testng/SuiteResult.java @@ -1,12 +1,16 @@ package org.testng; -import javax.annotation.Nonnull; +import java.util.Comparator; import org.testng.collections.Objects; import org.testng.log4testng.Logger; import org.testng.xml.XmlSuite; /** This class logs the result of an entire Test Suite (defined by a property file). */ class SuiteResult implements ISuiteResult, Comparable { + + /** A <test> that carries no name sorts ahead of the ones that do. */ + private static final Comparator NAME_ORDER = Comparator.nullsFirst(String::compareTo); + private final XmlSuite m_suite; private final ITestContext m_testContext; @@ -26,12 +30,12 @@ public XmlSuite getSuite() { } @Override - public int compareTo(@Nonnull SuiteResult other) { + public int compareTo(SuiteResult other) { int result = 0; try { String n1 = getTestContext().getName(); String n2 = other.getTestContext().getName(); - result = n1.compareTo(n2); + result = NAME_ORDER.compare(n1, n2); } catch (Exception ex) { Logger.getLogger(SuiteResult.class).error(ex.getMessage(), ex); } diff --git a/testng-core/src/main/java/org/testng/SuiteRunner.java b/testng-core/src/main/java/org/testng/SuiteRunner.java index 3b1b022762..4eee3accac 100644 --- a/testng-core/src/main/java/org/testng/SuiteRunner.java +++ b/testng-core/src/main/java/org/testng/SuiteRunner.java @@ -8,6 +8,7 @@ import java.lang.reflect.Method; import java.util.*; import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; import org.testng.internal.*; import org.testng.internal.annotations.IAnnotationFinder; import org.testng.internal.invokers.ConfigMethodArguments; @@ -38,26 +39,26 @@ public class SuiteRunner implements ISuite, ISuiteRunnerListener { private final Map, ISuiteListener> listeners = new LinkedHashMap<>(); - private String outputDir; + private @Nullable String outputDir; private final XmlSuite xmlSuite; - private Injector parentInjector; + private @Nullable Injector parentInjector; private final List testListeners = new ArrayList<>(); private final Map, IClassListener> classListeners = new LinkedHashMap<>(); - private final ITestRunnerFactory tmpRunnerFactory; + private final @Nullable ITestRunnerFactory tmpRunnerFactory; private final DataProviderHolder holder; private boolean useDefaultListeners = true; // The remote host where this suite was run, or null if run locally - private String remoteHost; + private @Nullable String remoteHost; // The configuration // Note: adjust test.multiplelisteners.SimpleReporter#generateReport test if renaming the field private final IConfiguration configuration; - private ITestObjectFactory objectFactory; + private @Nullable ITestObjectFactory objectFactory; private Boolean skipFailedInvocationCounts = Boolean.FALSE; private final List reporters = new ArrayList<>(); @@ -82,7 +83,7 @@ public SuiteRunner( IConfiguration configuration, XmlSuite suite, String outputDir, - ITestRunnerFactory runnerFactory, + @Nullable ITestRunnerFactory runnerFactory, boolean useDefaultListeners, Comparator comparator) { this( @@ -103,12 +104,12 @@ protected SuiteRunner( IConfiguration configuration, XmlSuite suite, String outputDir, - ITestRunnerFactory runnerFactory, + @Nullable ITestRunnerFactory runnerFactory, boolean useDefaultListeners, List methodInterceptors, - Collection invokedMethodListener, + @Nullable Collection invokedMethodListener, TestListenersContainer container, - Collection classListeners, + @Nullable Collection classListeners, DataProviderHolder holder, Comparator comparator) { if (comparator == null) { @@ -123,24 +124,25 @@ protected SuiteRunner( List localMethodInterceptors = Optional.ofNullable(methodInterceptors).orElse(new ArrayList<>()); setOutputDir(outputDir); - if (configuration.getObjectFactory() == null) { - configuration.setObjectFactory(new ObjectFactoryImpl()); + ITestObjectFactory declaredFactory = configuration.getObjectFactory(); + if (declaredFactory == null) { + declaredFactory = new ObjectFactoryImpl(); + configuration.setObjectFactory(declaredFactory); } + // The anonymous factory below closes over it, so it has to be effectively final. + final ITestObjectFactory configuredFactory = declaredFactory; if (suite.getObjectFactoryClass() == null) { - objectFactory = configuration.getObjectFactory(); + objectFactory = configuredFactory; } else { - boolean create = - !configuration.getObjectFactory().getClass().equals(suite.getObjectFactoryClass()); + boolean create = !configuredFactory.getClass().equals(suite.getObjectFactoryClass()); final ITestObjectFactory suiteObjectFactory; if (create) { - if (objectFactory == null) { - objectFactory = configuration.getObjectFactory(); - } - // Dont keep creating the object factory repeatedly since our current object factory - // Was already created based off of a suite level object factory. - suiteObjectFactory = objectFactory.newInstance(suite.getObjectFactoryClass()); + suiteObjectFactory = + Objects.requireNonNull( + configuredFactory.newInstance(suite.getObjectFactoryClass()), + "the object factory produced a suite level factory"); } else { - suiteObjectFactory = configuration.getObjectFactory(); + suiteObjectFactory = configuredFactory; } objectFactory = new ITestObjectFactory() { @@ -149,7 +151,7 @@ public T newInstance(Class cls, Object... parameters) { try { return suiteObjectFactory.newInstance(cls, parameters); } catch (Exception e) { - return configuration.getObjectFactory().newInstance(cls, parameters); + return configuredFactory.newInstance(cls, parameters); } } @@ -158,16 +160,16 @@ public T newInstance(String clsName, Object... parameters) { try { return suiteObjectFactory.newInstance(clsName, parameters); } catch (Exception e) { - return configuration.getObjectFactory().newInstance(clsName, parameters); + return configuredFactory.newInstance(clsName, parameters); } } @Override - public T newInstance(Constructor constructor, Object... parameters) { + public @Nullable T newInstance(Constructor constructor, Object... parameters) { try { return suiteObjectFactory.newInstance(constructor, parameters); } catch (Exception e) { - return configuration.getObjectFactory().newInstance(constructor, parameters); + return configuredFactory.newInstance(constructor, parameters); } } }; @@ -304,7 +306,7 @@ public String getGuiceStage() { } @Override - public Injector getParentInjector() { + public @Nullable Injector getParentInjector() { return parentInjector; } @@ -532,7 +534,7 @@ public Map getResults() { * @see org.testng.ISuite#getParameter(java.lang.String) */ @Override - public String getParameter(String parameterName) { + public @Nullable String getParameter(String parameterName) { return xmlSuite.getParameter(parameterName); } @@ -565,7 +567,7 @@ public Collection getExcludedMethods() { } @Override - public ITestObjectFactory getObjectFactory() { + public @Nullable ITestObjectFactory getObjectFactory() { return objectFactory; } @@ -728,7 +730,7 @@ public void setHost(String host) { } @Override - public String getHost() { + public @Nullable String getHost() { return remoteHost; } @@ -745,7 +747,7 @@ public void setSkipFailedInvocationCounts(Boolean skipFailedInvocationCounts) { } @Override - public Object getAttribute(String name) { + public @Nullable Object getAttribute(String name) { return attributes.getAttribute(name); } @@ -760,7 +762,7 @@ public Set getAttributeNames() { } @Override - public Object removeAttribute(String name) { + public @Nullable Object removeAttribute(String name) { return attributes.removeAttribute(name); } @@ -804,8 +806,11 @@ public List getAllInvokedMethods() { return results.stream(); }) .filter(tr -> tr.getMethod() instanceof IInvocationStatus) - .filter(tr -> ((IInvocationStatus) tr.getMethod()).getInvocationTime() > 0) - .map(tr -> new InvokedMethod(((IInvocationStatus) tr.getMethod()).getInvocationTime(), tr)) + .filter(tr -> ((IInvocationStatus) Utils.requireMethodOf(tr)).getInvocationTime() > 0) + .map( + tr -> + new InvokedMethod( + ((IInvocationStatus) Utils.requireMethodOf(tr)).getInvocationTime(), tr)) .collect(Collectors.toList()); } @@ -824,7 +829,8 @@ static class TestListenersContainer { this(Collections.emptyList(), null); } - TestListenersContainer(List listeners, ITestListener exitCodeListener) { + TestListenersContainer( + List listeners, @Nullable ITestListener exitCodeListener) { this.listeners.addAll(listeners); this.exitCodeListener = Objects.requireNonNullElseGet(exitCodeListener, () -> new ITestListener() {}); diff --git a/testng-core/src/main/java/org/testng/SuiteRunnerWorker.java b/testng-core/src/main/java/org/testng/SuiteRunnerWorker.java index 7e5281b46f..2ca33022ab 100644 --- a/testng-core/src/main/java/org/testng/SuiteRunnerWorker.java +++ b/testng-core/src/main/java/org/testng/SuiteRunnerWorker.java @@ -5,7 +5,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import javax.annotation.Nonnull; import org.testng.collections.Objects; import org.testng.internal.Utils; import org.testng.internal.invokers.SuiteRunnerMap; @@ -48,7 +47,7 @@ private void runSuite(SuiteRunnerMap suiteRunnerMap /* OUT */, XmlSuite xmlSuite Utils.log("TestNG", 0, "Running:\n" + allFiles); } - SuiteRunner suiteRunner = (SuiteRunner) suiteRunnerMap.get(xmlSuite); + SuiteRunner suiteRunner = (SuiteRunner) suiteRunnerMap.require(xmlSuite); suiteRunner.run(); // TODO: this should be handled properly @@ -96,7 +95,7 @@ public void run() { } @Override - public int compareTo(@Nonnull IWorker arg0) { + public int compareTo(IWorker arg0) { /* * Dummy Implementation * @@ -159,9 +158,9 @@ public void calculateResultCounts(XmlSuite xmlSuite, SuiteRunnerMap suiteRunnerM ITestContext ctx = isr.getTestContext(); int passes = ctx.getPassedTests().size(); Map segregated = seggregateSkippedTests(ctx); - int skipped = segregated.get(SKIPPED); + int skipped = segregated.getOrDefault(SKIPPED, 0); m_skipped += skipped; - int retried = segregated.get(RETRIED); + int retried = segregated.getOrDefault(RETRIED, 0); m_retries += retried; int failed = ctx.getFailedTests().size() + ctx.getFailedButWithinSuccessPercentageTests().size(); diff --git a/testng-core/src/main/java/org/testng/SuiteTaskExecutor.java b/testng-core/src/main/java/org/testng/SuiteTaskExecutor.java index 194a66edea..4fa87cbdc7 100644 --- a/testng-core/src/main/java/org/testng/SuiteTaskExecutor.java +++ b/testng-core/src/main/java/org/testng/SuiteTaskExecutor.java @@ -1,8 +1,10 @@ package org.testng; +import java.util.Objects; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; +import org.jspecify.annotations.Nullable; import org.testng.internal.IConfiguration; import org.testng.internal.Utils; import org.testng.internal.thread.TestNGThreadFactory; @@ -18,7 +20,7 @@ class SuiteTaskExecutor { private final int threadPoolSize; - private ExecutorService service; + private @Nullable ExecutorService service; private static final Logger LOGGER = Logger.getLogger(SuiteTaskExecutor.class); @@ -54,8 +56,9 @@ public void execute() { public void awaitCompletion() { Utils.log("TestNG", 2, "Starting executor for all suites"); try { - boolean ignored = service.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS); - service.shutdownNow(); + ExecutorService running = Objects.requireNonNull(service, "execute() has started the pool"); + boolean ignored = running.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS); + running.shutdownNow(); } catch (InterruptedException handled) { Thread.currentThread().interrupt(); LOGGER.error(handled.getMessage(), handled); diff --git a/testng-core/src/main/java/org/testng/TestClass.java b/testng-core/src/main/java/org/testng/TestClass.java index 7f2fea456c..0a0de5b298 100644 --- a/testng-core/src/main/java/org/testng/TestClass.java +++ b/testng-core/src/main/java/org/testng/TestClass.java @@ -2,10 +2,12 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.UUID; +import org.jspecify.annotations.Nullable; import org.testng.collections.Objects; import org.testng.internal.ConfigurationMethod; import org.testng.internal.ConstructorOrMethod; @@ -26,16 +28,16 @@ */ class TestClass extends NoOpTestClass implements ITestClass, ITestClassConfigInfo, IObject { - private IAnnotationFinder annotationFinder = null; + private IAnnotationFinder annotationFinder; // The Strategy used to locate test methods (TestNG, JUnit, etc...) - private ITestMethodFinder testMethodFinder = null; + private ITestMethodFinder testMethodFinder; - private IClass iClass = null; - private String testName; + private IClass iClass; + private @Nullable String testName; private XmlTest xmlTest; - private XmlClass xmlClass; + private @Nullable XmlClass xmlClass; private final ITestObjectFactory objectFactory; - private final String m_errorMsgPrefix; + private final @Nullable String m_errorMsgPrefix; // Keyed by the per-instance id (UUID) rather than the instantiated instance so that binding // per-instance @BeforeClass/@AfterClass methods never forces a lazy @Factory instance to be @@ -67,13 +69,13 @@ private static List getAllClassLevelConfigs(Map getInstanceBeforeClassMethods(UUID instanceId) { - return beforeClassConfig.get(instanceId); + public List getInstanceBeforeClassMethods(@Nullable UUID instanceId) { + return beforeClassConfig.getOrDefault(instanceId, Collections.emptyList()); } @Override - public List getInstanceAfterClassMethods(UUID instanceId) { - return afterClassConfig.get(instanceId); + public List getInstanceAfterClassMethods(@Nullable UUID instanceId) { + return afterClassConfig.getOrDefault(instanceId, Collections.emptyList()); } private static final Logger LOG = Logger.getLogger(TestClass.class); @@ -84,15 +86,15 @@ protected TestClass( ITestMethodFinder testMethodFinder, IAnnotationFinder annotationFinder, XmlTest xmlTest, - XmlClass xmlClass, - String errorMsgPrefix) { + @Nullable XmlClass xmlClass, + @Nullable String errorMsgPrefix) { this.objectFactory = objectFactory; this.m_errorMsgPrefix = errorMsgPrefix; init(cls, testMethodFinder, annotationFinder, xmlTest, xmlClass); } @Override - public String getTestName() { + public @Nullable String getTestName() { return testName; } @@ -102,7 +104,7 @@ public XmlTest getXmlTest() { } @Override - public XmlClass getXmlClass() { + public @Nullable XmlClass getXmlClass() { return xmlClass; } @@ -115,7 +117,7 @@ private void init( ITestMethodFinder testMethodFinder, IAnnotationFinder annotationFinder, XmlTest xmlTest, - XmlClass xmlClass) { + @Nullable XmlClass xmlClass) { log(3, "Creating TestClass for " + cls); iClass = cls; m_testClass = cls.getRealClass(); @@ -152,12 +154,12 @@ public Object[] getInstances(boolean create) { } @Override - public Object[] getInstances(boolean create, String errorMsgPrefix) { + public Object[] getInstances(boolean create, @Nullable String errorMsgPrefix) { return iClass.getInstances(create, this.m_errorMsgPrefix); } @Override - public IObject.IdentifiableObject[] getObjects(boolean create, String errorMsgPrefix) { + public IObject.IdentifiableObject[] getObjects(boolean create, @Nullable String errorMsgPrefix) { return IObject.objects(iClass, create, errorMsgPrefix); } @@ -177,28 +179,29 @@ public void addObject(IObject.IdentifiableObject instance) { } private void initMethods() { - ITestNGMethod[] methods = testMethodFinder.getTestMethods(m_testClass, xmlTest); + Class realClass = getRealClass(); + ITestNGMethod[] methods = testMethodFinder.getTestMethods(realClass, xmlTest); m_testMethods = createTestMethods(methods); for (IdentifiableObject eachInstance : IObject.objects(iClass, false)) { m_beforeSuiteMethods = ConfigurationMethod.createSuiteConfigurationMethods( objectFactory, - testMethodFinder.getBeforeSuiteMethods(m_testClass), + testMethodFinder.getBeforeSuiteMethods(realClass), annotationFinder, true, eachInstance); m_afterSuiteMethods = ConfigurationMethod.createSuiteConfigurationMethods( objectFactory, - testMethodFinder.getAfterSuiteMethods(m_testClass), + testMethodFinder.getAfterSuiteMethods(realClass), annotationFinder, false, eachInstance); m_beforeTestConfMethods = ConfigurationMethod.createTestConfigurationMethods( objectFactory, - testMethodFinder.getBeforeTestConfigurationMethods(m_testClass), + testMethodFinder.getBeforeTestConfigurationMethods(realClass), annotationFinder, true, this.xmlTest, @@ -206,7 +209,7 @@ private void initMethods() { m_afterTestConfMethods = ConfigurationMethod.createTestConfigurationMethods( objectFactory, - testMethodFinder.getAfterTestConfigurationMethods(m_testClass), + testMethodFinder.getAfterTestConfigurationMethods(realClass), annotationFinder, false, this.xmlTest, @@ -214,7 +217,7 @@ private void initMethods() { m_beforeClassMethods = ConfigurationMethod.createClassConfigurationMethods( objectFactory, - testMethodFinder.getBeforeClassMethods(m_testClass), + testMethodFinder.getBeforeClassMethods(realClass), annotationFinder, true, xmlTest, @@ -223,7 +226,7 @@ private void initMethods() { m_afterClassMethods = ConfigurationMethod.createClassConfigurationMethods( objectFactory, - testMethodFinder.getAfterClassMethods(m_testClass), + testMethodFinder.getAfterClassMethods(realClass), annotationFinder, false, xmlTest, @@ -232,21 +235,21 @@ private void initMethods() { m_beforeGroupsMethods = ConfigurationMethod.createBeforeConfigurationMethods( objectFactory, - testMethodFinder.getBeforeGroupsConfigurationMethods(m_testClass), + testMethodFinder.getBeforeGroupsConfigurationMethods(realClass), annotationFinder, true, eachInstance); m_afterGroupsMethods = ConfigurationMethod.createAfterConfigurationMethods( objectFactory, - testMethodFinder.getAfterGroupsConfigurationMethods(m_testClass), + testMethodFinder.getAfterGroupsConfigurationMethods(realClass), annotationFinder, false, eachInstance); m_beforeTestMethods.addAll( ConfigurationMethod.createTestMethodConfigurationMethods( objectFactory, - testMethodFinder.getBeforeTestMethods(m_testClass), + testMethodFinder.getBeforeTestMethods(realClass), annotationFinder, true, xmlTest, @@ -254,7 +257,7 @@ private void initMethods() { m_afterTestMethods.addAll( ConfigurationMethod.createTestMethodConfigurationMethods( objectFactory, - testMethodFinder.getAfterTestMethods(m_testClass), + testMethodFinder.getAfterTestMethods(realClass), annotationFinder, false, xmlTest, @@ -267,16 +270,18 @@ private void initMethods() { * class). */ private ITestNGMethod[] createTestMethods(ITestNGMethod[] methods) { + Class realClass = getRealClass(); List vResult = new ArrayList<>(); for (ITestNGMethod tm : methods) { ConstructorOrMethod m = tm.getConstructorOrMethod(); - if (m.getDeclaringClass().isAssignableFrom(m_testClass)) { + if (m.getDeclaringClass().isAssignableFrom(realClass)) { for (IdentifiableObject o : IObject.objects(iClass, false)) { - log(4, "Adding method " + tm + " on TestClass " + m_testClass); - vResult.add(new TestNGMethod(objectFactory, m.getMethod(), annotationFinder, xmlTest, o)); + log(4, "Adding method " + tm + " on TestClass " + realClass); + vResult.add( + new TestNGMethod(objectFactory, m.requireMethod(), annotationFinder, xmlTest, o)); } } else { - log(4, "Rejecting method " + tm + " for TestClass " + m_testClass); + log(4, "Rejecting method " + tm + " for TestClass " + realClass); } } @@ -292,7 +297,7 @@ private void log(int level, String s) { } protected void dump() { - LOG.info("===== Test class\n" + m_testClass.getName()); + LOG.info("===== Test class\n" + getRealClass().getName()); for (ITestNGMethod m : m_beforeClassMethods) { LOG.info(" @BeforeClass " + m); } @@ -313,7 +318,7 @@ protected void dump() { @Override public String toString() { - return Objects.toStringHelper(getClass()).add("name", m_testClass).toString(); + return Objects.toStringHelper(getClass()).add("name", getRealClass()).toString(); } public IClass getIClass() { diff --git a/testng-core/src/main/java/org/testng/TestNG.java b/testng-core/src/main/java/org/testng/TestNG.java index 64930f44ae..790421588e 100644 --- a/testng-core/src/main/java/org/testng/TestNG.java +++ b/testng-core/src/main/java/org/testng/TestNG.java @@ -23,6 +23,7 @@ import java.util.ServiceLoader; import java.util.Set; import java.util.concurrent.LinkedBlockingQueue; +import org.jspecify.annotations.Nullable; import org.testng.SuiteRunner.TestListenersContainer; import org.testng.annotations.ITestAnnotation; import org.testng.internal.ClassHelper; @@ -121,19 +122,19 @@ public class TestNG { /** The default name of the result's output directory (keep public, used by Eclipse). */ public static final String DEFAULT_OUTPUTDIR = "test-output"; - private static TestNG m_instance; + private static @Nullable TestNG m_instance; - private List m_commandLineMethods; + private @Nullable List m_commandLineMethods; protected List m_suites = new ArrayList<>(); - private List m_cmdlineSuites; + private @Nullable List m_cmdlineSuites; private String m_outputDir = DEFAULT_OUTPUTDIR; - private String[] m_includedGroups; - private String[] m_excludedGroups; + private String @Nullable [] m_includedGroups; + private String @Nullable [] m_excludedGroups; protected boolean m_useDefaultListeners = true; private boolean m_failIfAllTestsSkipped = false; private final List m_listenersToSkipFromBeingWiredIn = new ArrayList<>(); - private ITestRunnerFactory m_testRunnerFactory; + private @Nullable ITestRunnerFactory m_testRunnerFactory; // These listeners can be overridden from the command line private final Map, IClassListener> m_classListeners = @@ -152,9 +153,9 @@ public class TestNG { // Command line suite parameters private int m_threadCount = -1; - private XmlSuite.ParallelMode m_parallelMode = null; - private XmlSuite.FailurePolicy m_configFailurePolicy; - private Class[] m_commandLineTestClasses; + private XmlSuite.@Nullable ParallelMode m_parallelMode = null; + private XmlSuite.@Nullable FailurePolicy m_configFailurePolicy; + private Class @Nullable [] m_commandLineTestClasses; private String m_defaultSuiteName = DEFAULT_COMMAND_LINE_SUITE_NAME; private String m_defaultTestName = DEFAULT_COMMAND_LINE_TEST_NAME; @@ -169,17 +170,17 @@ public class TestNG { private final Map, IInvokedMethodListener> m_invokedMethodListeners = new LinkedHashMap<>(); - private Integer m_dataProviderThreadCount = null; + private @Nullable Integer m_dataProviderThreadCount = null; - private String m_jarPath; + private @Nullable String m_jarPath; /** The path of the testng.xml file inside the jar file */ private String m_xmlPathInJar = CommandLineArgs.XML_PATH_IN_JAR_DEFAULT; private List m_stringSuites = new ArrayList<>(); private final List> m_listenerClasses = new ArrayList<>(); - private IHookable m_hookable; - private IConfigurable m_configurable; + private @Nullable IHookable m_hookable; + private @Nullable IConfigurable m_configurable; protected long m_end; protected long m_start; @@ -191,7 +192,7 @@ public class TestNG { private boolean isSuiteInitialized = false; private final org.testng.internal.ExitCodeListener exitCodeListener = new org.testng.internal.ExitCodeListener(); - private ExitCode exitCode; + private @Nullable ExitCode exitCode; private final Map, IExecutionVisualiser> m_executionVisualisers = new LinkedHashMap<>(); @@ -240,7 +241,7 @@ public int getStatus() { if (exitCodeListener.noTestsFound()) { return ExitCode.HAS_NO_TEST; } - return exitCode.getExitCode(); + return requireExitCode().getExitCode(); } /** @@ -283,7 +284,7 @@ public void setListenerComparatorClass( setListenerComparator(m_objectFactory.newInstance(listenerComparatorClass)); } - public ListenerComparator getListenerComparator() { + public @Nullable ListenerComparator getListenerComparator() { return m_configuration.getListenerComparator(); } @@ -292,7 +293,7 @@ public ListenerComparator getListenerComparator() { * * @param jarPath - Path of the jar */ - public void setTestJar(String jarPath) { + public void setTestJar(@Nullable String jarPath) { m_jarPath = jarPath; } @@ -421,7 +422,9 @@ public void initializeSuitesAndJarFile() { } // We have a jar file and no XML file was specified: try to find an XML file inside the jar - File jarFile = new File(m_jarPath); + File jarFile = + new File( + Objects.requireNonNull(m_jarPath, "a jar suite is only read once -testjar was given")); JarFileUtils utils = new JarFileUtils( @@ -568,7 +571,7 @@ private List createCommandLineSuitesForClasses(Class[] classes) { } XmlTest xmlTest = null; for (XmlTest xt : xmlSuite.getTests()) { - if (xt.getName().equals(testName)) { + if (testName.equals(xt.getName())) { xmlTest = xt; break; } @@ -585,7 +588,7 @@ private List createCommandLineSuitesForClasses(Class[] classes) { return new ArrayList<>(suites.values()); } - public void addMethodSelector(String className, int priority) { + public void addMethodSelector(@Nullable String className, int priority) { if (Strings.isNotNullAndNotEmpty(className)) { m_methodDescriptors.put(className, priority); } @@ -676,7 +679,7 @@ public void setXmlSuites(List suites) { * * @param groups A list of group names separated by a comma. */ - public void setExcludedGroups(String groups) { + public void setExcludedGroups(@Nullable String groups) { m_excludedGroups = Utils.split(groups, ","); } @@ -685,7 +688,7 @@ public void setExcludedGroups(String groups) { * * @param groups A list of group names separated by a comma. */ - public void setGroups(String groups) { + public void setGroups(@Nullable String groups) { m_includedGroups = Utils.split(groups, ","); } @@ -725,7 +728,10 @@ public void setListenerClasses(List> classes) { return; } for (Class cls : classes) { - addListener(factory.createListener(cls)); + ITestNGListener created = factory.createListener(cls); + if (created != null) { + addListener(created); + } } } @@ -739,7 +745,10 @@ private void instantiatePendingListenerClasses() { for (Class cls : m_listenerClasses) { BasicAttributes basic = new BasicAttributes(null, cls); CreationAttributes attributes = new CreationAttributes(basic, context); - addListener((ITestNGListener) dispenser.dispense(attributes)); + Object created = dispenser.dispense(attributes); + if (created != null) { + addListener((ITestNGListener) created); + } } m_listenerClasses.clear(); } @@ -852,17 +861,17 @@ public List getSuiteListeners() { } /** If m_verbose gets set, it will override the verbose setting in testng.xml */ - private Integer m_verbose = null; + private @Nullable Integer m_verbose = null; private final IAnnotationTransformer m_defaultAnnoProcessor = new DefaultAnnotationTransformer(); private IAnnotationTransformer m_annotationTransformer = m_defaultAnnoProcessor; - private Boolean m_skipFailedInvocationCounts = false; + private @Nullable Boolean m_skipFailedInvocationCounts = false; private final List m_methodInterceptors = new ArrayList<>(); /** The list of test names to run from the given suite */ - private List m_testNames; + private @Nullable List m_testNames; private boolean m_ignoreMissedTestNames; @@ -873,7 +882,7 @@ public List getSuiteListeners() { private boolean m_alwaysRun = Boolean.TRUE; private Boolean m_preserveOrder = XmlSuite.DEFAULT_PRESERVE_ORDER; - private Boolean m_groupByInstances; + private @Nullable Boolean m_groupByInstances; private boolean m_generateResultsPerSuite = false; private IConfiguration m_configuration; @@ -921,11 +930,13 @@ public void setGenerateResultsPerSuite(boolean generateResultsPerSuite) { private void initializeCommandLineSuites() { if (m_commandLineTestClasses != null || m_commandLineMethods != null) { - if (null != m_commandLineMethods) { - m_cmdlineSuites = createCommandLineSuitesForMethods(m_commandLineMethods); - } else { - m_cmdlineSuites = createCommandLineSuitesForClasses(m_commandLineTestClasses); - } + List cliMethods = m_commandLineMethods; + Class[] cliClasses = m_commandLineTestClasses; + m_cmdlineSuites = + cliMethods != null + ? createCommandLineSuitesForMethods(cliMethods) + : createCommandLineSuitesForClasses( + Objects.requireNonNull(cliClasses, "one of the two command line inputs is set")); for (XmlSuite s : m_cmdlineSuites) { for (XmlTest t : s.getTests()) { @@ -960,32 +971,22 @@ private void initializeCommandLineSuitesParams() { private void initializeCommandLineSuitesGroups() { // If groups were specified on the command line, they should override groups // specified in the XML file - boolean hasIncludedGroups = null != m_includedGroups && m_includedGroups.length > 0; - boolean hasExcludedGroups = null != m_excludedGroups && m_excludedGroups.length > 0; List suites = m_cmdlineSuites != null ? m_cmdlineSuites : m_suites; - if (hasIncludedGroups || hasExcludedGroups) { - for (XmlSuite s : suites) { - initializeCommandLineSuitesGroups( - s, hasIncludedGroups, m_includedGroups, hasExcludedGroups, m_excludedGroups); - } + for (XmlSuite s : suites) { + initializeCommandLineSuitesGroups(s, m_includedGroups, m_excludedGroups); } } private static void initializeCommandLineSuitesGroups( - XmlSuite s, - boolean hasIncludedGroups, - String[] m_includedGroups, - boolean hasExcludedGroups, - String[] m_excludedGroups) { - if (hasIncludedGroups) { - s.setIncludedGroups(Arrays.asList(m_includedGroups)); + XmlSuite s, String @Nullable [] included, String @Nullable [] excluded) { + if (included != null && included.length > 0) { + s.setIncludedGroups(Arrays.asList(included)); } - if (hasExcludedGroups) { - s.setExcludedGroups(Arrays.asList(m_excludedGroups)); + if (excluded != null && excluded.length > 0) { + s.setExcludedGroups(Arrays.asList(excluded)); } for (XmlSuite child : s.getChildSuites()) { - initializeCommandLineSuitesGroups( - child, hasIncludedGroups, m_includedGroups, hasExcludedGroups, m_excludedGroups); + initializeCommandLineSuitesGroups(child, included, excluded); } } @@ -1073,7 +1074,9 @@ private void addListeners(XmlSuite s) { BasicAttributes basic = new BasicAttributes(null, listenerClass); CreationAttributes attribute = new CreationAttributes(basic, context); Object listener = dispenser.dispense(attribute); - addListener((ITestNGListener) listener); + if (listener != null) { + addListener((ITestNGListener) listener); + } } // Add the child suite listeners @@ -1322,7 +1325,7 @@ public List runSuitesLocally() { return new ArrayList<>(suiteRunnerMap.values()); } - private static void error(String s) { + private static void error(@Nullable String s) { LOGGER.error(s); } @@ -1352,7 +1355,7 @@ private void runSuitesSequentially( } SuiteRunnerWorker srw = new SuiteRunnerWorker( - suiteRunnerMap.get(xmlSuite), suiteRunnerMap, verbose, defaultSuiteName); + suiteRunnerMap.require(xmlSuite), suiteRunnerMap, verbose, defaultSuiteName); srw.run(); } @@ -1369,11 +1372,11 @@ private void populateSuiteGraph( IDynamicGraph suiteGraph /* OUT */, SuiteRunnerMap suiteRunnerMap, XmlSuite xmlSuite) { - ISuite parentSuiteRunner = suiteRunnerMap.get(xmlSuite); + ISuite parentSuiteRunner = suiteRunnerMap.require(xmlSuite); suiteGraph.addNode(parentSuiteRunner); if (!xmlSuite.getChildSuites().isEmpty()) { for (XmlSuite childSuite : xmlSuite.getChildSuites()) { - suiteGraph.addEdge(0, parentSuiteRunner, suiteRunnerMap.get(childSuite)); + suiteGraph.addEdge(0, parentSuiteRunner, suiteRunnerMap.require(childSuite)); populateSuiteGraph(suiteGraph, suiteRunnerMap, childSuite); } } @@ -1505,7 +1508,7 @@ public static void main(String[] argv) { * of your choice. Scheduled for removal in 8.0. */ @Deprecated - public static TestNG privateMain(String[] argv, ITestListener listener) { + public static TestNG privateMain(String[] argv, @Nullable ITestListener listener) { return CliRunners.required().run(argv, listener); } @@ -1560,7 +1563,9 @@ protected void configure(CommandLineArgs cla) { .map(it -> (IExecutorServiceFactory) it) .ifPresent(this::setExecutorServiceFactory); - setOutputDirectory(cla.outputDirectory); + if (cla.outputDirectory != null) { + setOutputDirectory(cla.outputDirectory); + } String testClasses = cla.testClass; if (null != testClasses) { @@ -1573,8 +1578,6 @@ protected void configure(CommandLineArgs cla) { setTestClasses(classes.toArray(new Class[0])); } - setOutputDirectory(cla.outputDirectory); - if (cla.testNames != null) { setTestNames(Arrays.asList(cla.testNames.split(","))); setIgnoreMissedTestNames(cla.ignoreMissedTestNames); @@ -1744,7 +1747,7 @@ public void setSourcePath(String path) { // nop } - private static int parseInt(Object value) { + private static int parseInt(@Nullable Object value) { if (value == null) { return -1; } @@ -1793,8 +1796,11 @@ public void configure(Map cmdLineArgs) { result.groups = (String) cmdLineArgs.get(CommandLineArgs.GROUPS); result.excludedGroups = (String) cmdLineArgs.get(CommandLineArgs.EXCLUDED_GROUPS); result.testJar = (String) cmdLineArgs.get(CommandLineArgs.TEST_JAR); - result.xmlPathInJar = (String) cmdLineArgs.get(CommandLineArgs.XML_PATH_IN_JAR); - result.mixed = (Boolean) cmdLineArgs.get(CommandLineArgs.MIXED); + result.xmlPathInJar = + (String) + cmdLineArgs.getOrDefault( + CommandLineArgs.XML_PATH_IN_JAR, CommandLineArgs.XML_PATH_IN_JAR_DEFAULT); + result.mixed = (Boolean) cmdLineArgs.getOrDefault(CommandLineArgs.MIXED, Boolean.FALSE); Object tmpValue = cmdLineArgs.get(CommandLineArgs.INCLUDE_ALL_DATA_DRIVEN_TESTS_WHEN_SKIPPING); if (tmpValue != null) { result.includeAllDataDrivenTestsWhenSkipping = Boolean.parseBoolean(tmpValue.toString()); @@ -1896,7 +1902,7 @@ public void setTestNames(List testNames) { m_testNames = testNames; } - public void setSkipFailedInvocationCounts(Boolean skip) { + public void setSkipFailedInvocationCounts(@Nullable Boolean skip) { m_skipFailedInvocationCounts = skip; } @@ -1911,7 +1917,7 @@ public void setSkipFailedInvocationCounts(Boolean skip) { * @param reporterConfigString the serialized reporter configuration. * @throws TestNGException if the named class is not an {@link IReporter}. */ - public void addReporter(String reporterConfigString) { + public void addReporter(@Nullable String reporterConfigString) { ReporterConfig reporterConfig = ReporterConfig.deserialize(reporterConfigString); if (reporterConfig != null) { addReporter(reporterConfig); @@ -1928,7 +1934,7 @@ private void addReporter(ReporterConfig reporterConfig) { } /** Creates a reporter based on the configuration */ - private IReporter newReporterInstance(ReporterConfig config) { + private @Nullable IReporter newReporterInstance(ReporterConfig config) { Class reporterClass = ClassHelper.forName(config.getClassName()); if (reporterClass == null) { @@ -1979,22 +1985,27 @@ protected static void validateCommandLineParameters(CommandLineArgs args) { } } + /** The outcome of the run; absent until {@link #run()} has produced one. */ + private ExitCode requireExitCode() { + return Objects.requireNonNull(this.exitCode, "the run has not produced an exit code yet"); + } + /** @return true if at least one test failed. */ public boolean hasFailure() { - return this.exitCode.hasFailure(); + return requireExitCode().hasFailure(); } /** @return true if at least one test failed within success percentage. */ public boolean hasFailureWithinSuccessPercentage() { - return this.exitCode.hasFailureWithinSuccessPercentage(); + return requireExitCode().hasFailureWithinSuccessPercentage(); } /** @return true if at least one test was skipped. */ public boolean hasSkip() { - return this.exitCode.hasSkip(); + return requireExitCode().hasSkip(); } - static void exitWithError(String msg) { + static void exitWithError(@Nullable String msg) { // Trimmed because TestNGException prefixes every message with a newline, which would show up // as a stray blank line ahead of the error. System.err.println(msg == null ? "" : msg.trim()); @@ -2045,7 +2056,7 @@ public void setDefaultTestName(String defaultTestName) { * * @param failurePolicy the configuration failure policy */ - public void setConfigFailurePolicy(XmlSuite.FailurePolicy failurePolicy) { + public void setConfigFailurePolicy(XmlSuite.@Nullable FailurePolicy failurePolicy) { m_configFailurePolicy = failurePolicy; } @@ -2054,7 +2065,7 @@ public void setConfigFailurePolicy(XmlSuite.FailurePolicy failurePolicy) { * * @return config failure policy */ - public XmlSuite.FailurePolicy getConfigFailurePolicy() { + public XmlSuite.@Nullable FailurePolicy getConfigFailurePolicy() { return m_configFailurePolicy; } @@ -2064,7 +2075,7 @@ public XmlSuite.FailurePolicy getConfigFailurePolicy() { * @deprecated since 5.1 */ @Deprecated - public static TestNG getDefault() { + public static @Nullable TestNG getDefault() { return m_instance; } @@ -2123,7 +2134,7 @@ public void setGroupByInstances(boolean b) { // ServiceLoader testing // - private URLClassLoader m_serviceLoaderClassLoader; + private @Nullable URLClassLoader m_serviceLoaderClassLoader; private final Map, ITestNGListener> serviceLoaderListeners = new HashMap<>(); diff --git a/testng-core/src/main/java/org/testng/TestRunner.java b/testng-core/src/main/java/org/testng/TestRunner.java index 8c89e92ecb..49d72456d3 100644 --- a/testng-core/src/main/java/org/testng/TestRunner.java +++ b/testng-core/src/main/java/org/testng/TestRunner.java @@ -20,9 +20,9 @@ import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.PriorityBlockingQueue; -import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; import org.testng.internal.Attributes; import org.testng.internal.BaseTestMethod; import org.testng.internal.ClassBasedWrapper; @@ -77,14 +77,14 @@ public class TestRunner private final Comparator comparator; private ISuite m_suite; private XmlTest m_xmlTest; - private String m_testName; + private @Nullable String m_testName; private IInjectorFactory m_injectorFactory; private ITestObjectFactory m_objectFactory; - private List m_testClassesFromXml = null; + private List m_testClassesFromXml; - private IInvoker m_invoker = null; - private IAnnotationFinder m_annotationFinder = null; + private IInvoker m_invoker; + private IAnnotationFinder m_annotationFinder; /** ITestListeners support. */ private final List m_testListeners = new ArrayList<>(); @@ -99,7 +99,7 @@ public class TestRunner private final DataProviderHolder holder; private Date m_startDate = new Date(); - private Date m_endDate = null; + private @Nullable Date m_endDate = null; private final IContainer testMethodsContainer = new TestMethodContainer(this::computeAndGetAllTestMethods); @@ -112,7 +112,7 @@ public class TestRunner // The XML method selector (groups/methods included/excluded in XML) private final XmlMethodSelector m_xmlMethodSelector = new XmlMethodSelector(); - private ITestListener exitCodeListener; + private @Nullable ITestListener exitCodeListener; // // These next fields contain all the configuration methods found on this class. @@ -128,7 +128,7 @@ public class TestRunner private ITestNGMethod[] m_beforeXmlTestMethods = {}; private ITestNGMethod[] m_afterXmlTestMethods = {}; private final List m_excludedMethods = new ArrayList<>(); - private ConfigurationGroupMethods m_groupMethods = null; + private @Nullable ConfigurationGroupMethods m_groupMethods = null; // Meta groups private final Map> m_metaGroups = new HashMap<>(); @@ -142,13 +142,13 @@ public class TestRunner private final RunInfo m_runInfo = new RunInfo(this::getCurrentXmlTest); // The host where this test was run, or null if run locally - private String m_host; + private @Nullable String m_host; // Defined dynamically depending on - private List m_methodInterceptors; + private final List m_methodInterceptors = new ArrayList<>(); - private ClassMethodMap m_classMethodMap; - private TestNGClassFinder m_testClassFinder; + private @Nullable ClassMethodMap m_classMethodMap; + private @Nullable TestNGClassFinder m_testClassFinder; private IConfiguration m_configuration; public enum PriorityWeight { @@ -248,7 +248,9 @@ private void init( m_host = suite.getHost(); m_testClassesFromXml = test.getXmlClasses(); m_injectorFactory = m_configuration.getInjectorFactory(); - m_objectFactory = suite.getObjectFactory(); + m_objectFactory = + Objects.requireNonNull( + suite.getObjectFactory(), "a running suite carries an object factory"); setVerbose(test.getVerbose()); if (suiteRunner == null) { if (suite instanceof ISuiteRunnerListener) { @@ -263,7 +265,7 @@ private void init( preserveOrder ? new PreserveOrderMethodInterceptor() : new InstanceOrderingMethodInterceptor(); - m_methodInterceptors = new ArrayList<>(); + m_methodInterceptors.clear(); // Add the built-in interceptor as the first interceptor. That way we let our users determine // the final order // by plugging in their own custom interceptors as well. @@ -387,8 +389,10 @@ private void initListeners() { // Instantiate all the listeners for (Class c : listenerClasses) { - ITestNGListener listener = factory.createListener(c); - addListener(listener); + ITestNGListener created = factory.createListener(c); + if (created != null) { + addListener(created); + } } } @@ -619,6 +623,16 @@ public void run() { } } + /** Dropped by {@link #forgetHeavyReferencesIfNeeded()} once the run is over. */ + private ClassMethodMap requireClassMethodMap() { + return Objects.requireNonNull(m_classMethodMap, "the run still holds its method map"); + } + + /** Dropped by {@link #forgetHeavyReferencesIfNeeded()} once the run is over. */ + private ConfigurationGroupMethods requireGroupMethods() { + return Objects.requireNonNull(m_groupMethods, "the run still holds its group methods"); + } + private void forgetHeavyReferencesIfNeeded() { if (RuntimeBehavior.isMemoryFriendlyMode()) { testMethodsContainer.clearItems(); @@ -657,7 +671,7 @@ private void invokeTestConfigurations(ITestNGMethod[] testConfigurationMethods) } } - private static Comparator newComparator(boolean needPrioritySort) { + private static @Nullable Comparator newComparator(boolean needPrioritySort) { return needPrioritySort ? new TestMethodComparator() : null; } @@ -677,15 +691,10 @@ private void privateRun(XmlTest xmlTest) { // removing methods would cause the graph never to terminate (because it would expect // termination from methods that never get invoked). ITestNGMethod[] interceptedOrder = intercept(getAllTestMethods()); - AtomicReference> reference = new AtomicReference<>(); - TimeUtils.computeAndShowTime( - "DynamicGraphHelper.createDynamicGraph()", - () -> { - IDynamicGraph ref = - DynamicGraphHelper.createDynamicGraph(interceptedOrder, getCurrentXmlTest()); - reference.set(ref); - }); - IDynamicGraph graph = reference.get(); + IDynamicGraph graph = + TimeUtils.computeAndShowTime( + "DynamicGraphHelper.createDynamicGraph()", + () -> DynamicGraphHelper.createDynamicGraph(interceptedOrder, getCurrentXmlTest())); for (ITestNGMethod each : interceptedOrder) { if (each instanceof BaseTestMethod) { @@ -773,11 +782,12 @@ private ITestNGMethod[] intercept(ITestNGMethod[] methods) { // Check if an interceptor had altered the effective test method count. If yes, then we need to // update our configurationGroupMethod object with that information. if (resultArray.length != testMethodsContainer.getItems().length) { + ConfigurationGroupMethods current = requireGroupMethods(); m_groupMethods = new ConfigurationGroupMethods( new TestMethodContainer(() -> resultArray), - m_groupMethods.getBeforeGroupsMethods(), - m_groupMethods.getAfterGroupsMethods()); + current.getBeforeGroupsMethods(), + current.getAfterGroupsMethods()); } // If the user specified a method interceptor, whatever that returns is the order we're going @@ -803,8 +813,8 @@ private ITestNGMethod[] intercept(ITestNGMethod[] methods) { public List> createWorkers(List methods) { AbstractParallelWorker.Arguments args = new AbstractParallelWorker.Arguments.Builder() - .classMethodMap(this.m_classMethodMap) - .configMethods(this.m_groupMethods) + .classMethodMap(requireClassMethodMap()) + .configMethods(requireGroupMethods()) .finder(this.m_annotationFinder) .invoker(this.m_invoker) .methods(methods) @@ -867,7 +877,7 @@ private void fireEvent(boolean isStart) { ListenerOrderDeterminer.order(m_testListeners, m_configuration.getListenerComparator())) { itl.onStart(this); } - this.exitCodeListener.onStart(this); + getExitCodeListener().onStart(this); } else { List testListenersReversed = @@ -876,7 +886,7 @@ private void fireEvent(boolean isStart) { for (ITestListener itl : testListenersReversed) { itl.onFinish(this); } - this.exitCodeListener.onFinish(this); + getExitCodeListener().onFinish(this); } if (!isStart) { MethodHelper.clear(methods(this.getPassedConfigurations())); @@ -898,7 +908,7 @@ private static Stream methods(Stream methods) { // ITestContext // @Override - public String getName() { + public @Nullable String getName() { return m_testName; } @@ -910,7 +920,7 @@ public Date getStartDate() { /** @return Returns the endDate. */ @Override - public Date getEndDate() { + public @Nullable Date getEndDate() { return m_endDate; } @@ -963,7 +973,7 @@ public ITestNGMethod[] getAllTestMethods() { } @Override - public String getHost() { + public @Nullable String getHost() { return m_host; } @@ -1184,7 +1194,9 @@ public void onConfigurationSuccess(ITestResult itr) { private void removeConfigurationResultAfterExecution(ITestResult itr) { // The remove method of ResultMap removes based on hashCode // So lets find the result based on the method and remove it off. - m_configsToBeInvoked.getAllResults().removeIf(tr -> tr.getMethod().equals(itr.getMethod())); + m_configsToBeInvoked + .getAllResults() + .removeIf(tr -> Objects.equals(tr.getMethod(), itr.getMethod())); } } @@ -1204,7 +1216,7 @@ public XmlTest getCurrentXmlTest() { private final IAttributes m_attributes = new Attributes(); @Override - public Object getAttribute(String name) { + public @Nullable Object getAttribute(String name) { return m_attributes.getAttribute(name); } @@ -1219,7 +1231,7 @@ public Set getAttributeNames() { } @Override - public Object removeAttribute(String name) { + public @Nullable Object removeAttribute(String name) { return m_attributes.removeAttribute(name); } diff --git a/testng-core/src/main/java/org/testng/TestTaskExecutor.java b/testng-core/src/main/java/org/testng/TestTaskExecutor.java index 96d7b1126a..0897439091 100644 --- a/testng-core/src/main/java/org/testng/TestTaskExecutor.java +++ b/testng-core/src/main/java/org/testng/TestTaskExecutor.java @@ -1,10 +1,12 @@ package org.testng; import java.util.Comparator; +import java.util.Objects; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; import org.testng.internal.IConfiguration; import org.testng.internal.ObjectBag; import org.testng.internal.Utils; @@ -17,15 +19,15 @@ class TestTaskExecutor { private final BlockingQueue queue; - private final Comparator comparator; + private final @Nullable Comparator comparator; private final IDynamicGraph graph; private final XmlTest xmlTest; private final IThreadWorkerFactory factory; private final IConfiguration configuration; private final long timeOut; - private ExecutorService service; - private GraphOrchestrator orchestrator; + private @Nullable ExecutorService service; + private @Nullable GraphOrchestrator orchestrator; private boolean reUse; private static final Logger LOGGER = Logger.getLogger(TestTaskExecutor.class); @@ -36,7 +38,7 @@ public TestTaskExecutor( IThreadWorkerFactory factory, BlockingQueue queue, IDynamicGraph graph, - Comparator comparator) { + @Nullable Comparator comparator) { this.configuration = configuration; this.xmlTest = xmlTest; this.factory = factory; @@ -91,10 +93,13 @@ public void awaitCompletion() { if (reUse) { // Shared global pool: wait for this test's graph to finish, but leave the pool running for // the other s. It is disposed once, at the end of the run, via ObjectBag cleanup. - boolean ignored = orchestrator.awaitCompletion(timeOut, TimeUnit.MILLISECONDS); + boolean ignored = + Objects.requireNonNull(orchestrator, "execute() has started the graph") + .awaitCompletion(timeOut, TimeUnit.MILLISECONDS); } else { - boolean ignored = service.awaitTermination(timeOut, TimeUnit.MILLISECONDS); - service.shutdownNow(); + ExecutorService running = Objects.requireNonNull(service, "execute() has started the pool"); + boolean ignored = running.awaitTermination(timeOut, TimeUnit.MILLISECONDS); + running.shutdownNow(); } } catch (InterruptedException handled) { LOGGER.error(handled.getMessage(), handled); diff --git a/testng-core/src/main/java/org/testng/TimeBombSkipException.java b/testng-core/src/main/java/org/testng/TimeBombSkipException.java index 60a2eccbeb..536e975777 100644 --- a/testng-core/src/main/java/org/testng/TimeBombSkipException.java +++ b/testng-core/src/main/java/org/testng/TimeBombSkipException.java @@ -7,6 +7,7 @@ import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; +import org.jspecify.annotations.Nullable; /** * A {@link SkipException} extension that transforms a skipped method into a failed method based on @@ -24,7 +25,7 @@ public class TimeBombSkipException extends SkipException { private static final String FORMAT = "yyyy/MM/dd"; private final SimpleDateFormat sdf = new SimpleDateFormat(FORMAT); - private Calendar m_expireDate; + private final Calendar m_expireDate; private DateFormat m_inFormat = sdf; private DateFormat m_outFormat = sdf; @@ -51,7 +52,7 @@ public TimeBombSkipException(String msg, Date expirationDate, String format) { super(msg); m_inFormat = new SimpleDateFormat(format); m_outFormat = new SimpleDateFormat(format); - initExpireDate(expirationDate); + m_expireDate = expireDateOf(expirationDate); } /** @@ -63,7 +64,7 @@ public TimeBombSkipException(String msg, Date expirationDate, String format) { */ public TimeBombSkipException(String msg, String date) { super(msg); - initExpireDate(date); + m_expireDate = expireDateOf(date); } /** @@ -92,7 +93,7 @@ public TimeBombSkipException(String msg, String date, String inFormat, String ou super(msg); m_inFormat = new SimpleDateFormat(inFormat); m_outFormat = new SimpleDateFormat(outFormat); - initExpireDate(date); + m_expireDate = expireDateOf(date); } /** @@ -107,7 +108,7 @@ public TimeBombSkipException(String msg, String date, String inFormat, String ou */ public TimeBombSkipException(String msg, Date expirationDate, Throwable cause) { super(msg, cause); - initExpireDate(expirationDate); + m_expireDate = expireDateOf(expirationDate); } /** @@ -125,7 +126,7 @@ public TimeBombSkipException(String msg, Date expirationDate, String format, Thr super(msg, cause); m_inFormat = new SimpleDateFormat(format); m_outFormat = new SimpleDateFormat(format); - initExpireDate(expirationDate); + m_expireDate = expireDateOf(expirationDate); } /** @@ -140,7 +141,7 @@ public TimeBombSkipException(String msg, Date expirationDate, String format, Thr */ public TimeBombSkipException(String msg, String date, Throwable cause) { super(msg, cause); - initExpireDate(date); + m_expireDate = expireDateOf(date); } /** @@ -176,18 +177,18 @@ public TimeBombSkipException( super(msg, cause); m_inFormat = new SimpleDateFormat(inFormat); m_outFormat = new SimpleDateFormat(outFormat); - initExpireDate(date); + m_expireDate = expireDateOf(date); } - private void initExpireDate(Date expireDate) { - m_expireDate = Calendar.getInstance(); - m_expireDate.setTime(expireDate); + private static Calendar expireDateOf(Date expireDate) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(expireDate); + return calendar; } - private void initExpireDate(String date) { + private Calendar expireDateOf(String date) { try { - Date d = m_inFormat.parse(date); - initExpireDate(d); + return expireDateOf(m_inFormat.parse(date)); } catch (ParseException pex) { throw new TestNGException("Cannot parse date:" + date + " using pattern: " + m_inFormat, pex); } @@ -195,10 +196,6 @@ private void initExpireDate(String date) { @Override public boolean isSkip() { - if (null == m_expireDate) { - return false; - } - try { Calendar now = Calendar.getInstance(); Date nowDate = m_inFormat.parse(m_inFormat.format(now.getTime())); @@ -211,7 +208,7 @@ public boolean isSkip() { } @Override - public String getMessage() { + public @Nullable String getMessage() { if (isSkip()) { return super.getMessage(); } else { diff --git a/testng-core/src/main/java/org/testng/internal/ClassImpl.java b/testng-core/src/main/java/org/testng/internal/ClassImpl.java index 7fdb5e9efa..34fafdcaea 100644 --- a/testng-core/src/main/java/org/testng/internal/ClassImpl.java +++ b/testng-core/src/main/java/org/testng/internal/ClassImpl.java @@ -1,5 +1,7 @@ package org.testng.internal; +import static java.util.Objects.requireNonNull; + import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -94,7 +96,7 @@ public XmlTest getXmlTest() { } private IObject.@Nullable IdentifiableObject getDefaultInstance( - boolean create, String errMsgPrefix) { + boolean create, @Nullable String errMsgPrefix) { if (m_defaultInstance == null) { if (m_instance != null) { m_defaultInstance = m_instance; @@ -103,7 +105,9 @@ public XmlTest getXmlTest() { if (factory instanceof DefaultTestObjectFactory) { factory = m_testContext.getSuite().getObjectFactory(); } - IObjectDispenser dispenser = Dispenser.newInstance(factory); + IObjectDispenser dispenser = + Dispenser.newInstance( + requireNonNull(factory, "a running suite carries an object factory")); BasicAttributes basic = new BasicAttributes(this, null); DetailedAttributes detailed = newDetailedAttributes(create, errMsgPrefix); CreationAttributes attributes = new CreationAttributes(m_testContext, basic, detailed); @@ -122,7 +126,7 @@ public Object[] getInstances(boolean create) { } @Override - public Object[] getInstances(boolean create, String errorMsgPrefix) { + public Object[] getInstances(boolean create, @Nullable String errorMsgPrefix) { return Arrays.stream(getObjects(create, errorMsgPrefix)) .map(IdentifiableObject::getInstance) .toArray(Object[]::new); @@ -134,7 +138,7 @@ public void addObject(IdentifiableObject instance) { } @Override - public IdentifiableObject[] getObjects(boolean create, String errorMsgPrefix) { + public IdentifiableObject[] getObjects(boolean create, @Nullable String errorMsgPrefix) { IdentifiableObject[] result = {}; if (!identifiableObjects.isEmpty()) { @@ -172,12 +176,12 @@ private static int computeHashCode(IdentifiableObject identifiable) { // derive a stable one from its unique instance id instead. return identifiable.getInstanceId().hashCode(); } - return java.util.Objects.requireNonNull( + return requireNonNull( IParameterInfo.embeddedInstance(instance), "the factory instance is not available") .hashCode(); } - private DetailedAttributes newDetailedAttributes(boolean create, String errMsgPrefix) { + private DetailedAttributes newDetailedAttributes(boolean create, @Nullable String errMsgPrefix) { return new DetailedAttributes( m_class, m_classes, diff --git a/testng-core/src/main/java/org/testng/internal/ClonedMethod.java b/testng-core/src/main/java/org/testng/internal/ClonedMethod.java index d81de758d0..d27fd72bd2 100644 --- a/testng-core/src/main/java/org/testng/internal/ClonedMethod.java +++ b/testng-core/src/main/java/org/testng/internal/ClonedMethod.java @@ -67,7 +67,7 @@ public String getDescription() { } @Override - public void setDescription(String description) { + public void setDescription(@Nullable String description) { m_method.setDescription(description); } @@ -97,7 +97,7 @@ public long[] getInstanceHashCodes() { } @Override - public Object getInstance() { + public @Nullable Object getInstance() { return m_method.getInstance(); } @@ -150,7 +150,7 @@ public Class getRealClass() { } @Override - public IRetryAnalyzer getRetryAnalyzer(ITestResult result) { + public @Nullable IRetryAnalyzer getRetryAnalyzer(ITestResult result) { return m_method.getRetryAnalyzer(result); } @@ -170,7 +170,7 @@ public int getSuccessPercentage() { } @Override - public ITestClass getTestClass() { + public @Nullable ITestClass getTestClass() { return m_method.getTestClass(); } @@ -276,7 +276,7 @@ public void setIgnoreMissingDependencies(boolean ignore) {} public void setInvocationCount(int count) {} @Override - public void setMissingGroup(String group) {} + public void setMissingGroup(@Nullable String group) {} @Override public void setParameterInvocationCount(int n) {} @@ -348,7 +348,7 @@ public void setInterceptedPriority(int priority) { } @Override - public XmlTest getXmlTest() { + public @Nullable XmlTest getXmlTest() { return m_method.getXmlTest(); } diff --git a/testng-core/src/main/java/org/testng/internal/DynamicGraphHelper.java b/testng-core/src/main/java/org/testng/internal/DynamicGraphHelper.java index af20ddfbdf..fd159bf32b 100644 --- a/testng-core/src/main/java/org/testng/internal/DynamicGraphHelper.java +++ b/testng-core/src/main/java/org/testng/internal/DynamicGraphHelper.java @@ -130,7 +130,7 @@ private static ListMultiMap createClassDependencie ListMultiMap methodsFromClass = Maps.newListMultiMap(); for (ITestNGMethod m : methods) { - methodsFromClass.put(m.getTestClass().getName(), m); + methodsFromClass.put(Utils.requireTestClassOf(m).getName(), m); } final List classesWithMethods = @@ -159,7 +159,7 @@ private static ListMultiMap createClassDependencie ListMultiMap result = Maps.newListMultiMap(); for (ITestNGMethod m : methods) { - String name = m.getTestClass().getName(); + String name = Utils.requireTestClassOf(m).getName(); Integer index = indexedClasses1.get(name); // The index could be null if the classes listed in the XML are different // from the methods being run (e.g. the .xml only contains a factory that diff --git a/testng-core/src/main/java/org/testng/internal/FactoryMethod.java b/testng-core/src/main/java/org/testng/internal/FactoryMethod.java index dd2e89b287..3c9068d1cf 100644 --- a/testng-core/src/main/java/org/testng/internal/FactoryMethod.java +++ b/testng-core/src/main/java/org/testng/internal/FactoryMethod.java @@ -265,14 +265,17 @@ public IParameterInfo[] invoke() { // we // snapshot the row to keep each instance bound to its own parameters. Object[] rowParameters = parameters.clone(); - Constructor constructor = com.getConstructor(); + Constructor constructor = com.requireConstructor(); result.add( new LazyParameterInfo( new FactoryInstance(position, 0, rowParameters, factory), com.getDeclaringClass(), () -> m_objectFactory.newInstance(constructor, rowParameters))); } else { - Object instance = m_objectFactory.newInstance(com.getConstructor(), parameters); + Object instance = + Objects.requireNonNull( + m_objectFactory.newInstance(com.requireConstructor(), parameters), + "the object factory produced a @Factory instance"); result.add( new ParameterInfo( instance, new FactoryInstance(position, 0, parameters, factory))); diff --git a/testng-core/src/main/java/org/testng/internal/IInstanceIdentity.java b/testng-core/src/main/java/org/testng/internal/IInstanceIdentity.java index 466931a2f6..d2210dda63 100644 --- a/testng-core/src/main/java/org/testng/internal/IInstanceIdentity.java +++ b/testng-core/src/main/java/org/testng/internal/IInstanceIdentity.java @@ -1,12 +1,26 @@ package org.testng.internal; -import java.util.Arrays; -import java.util.Objects; import java.util.UUID; import org.jspecify.annotations.Nullable; public interface IInstanceIdentity { + /** + * The token {@link #getInstanceId(Object)} answers for a method that carries no instance. + * + *

Grouping by instance keys on the answer, and a map key that may be absent forces every such + * map to accept a null key and every caller to decide what an absent one means. One shared token + * keeps the key present and the grouping identical: every method without an instance lands in the + * same bucket, which is what a null key did. + */ + Object NO_INSTANCE = + new Object() { + @Override + public String toString() { + return "NO_INSTANCE"; + } + }; + /** * @return - A {@link UUID} that represents a unique id which is associated with * every test class object, or {@code null} when the implementation carries no instance. @@ -14,19 +28,25 @@ public interface IInstanceIdentity { @Nullable UUID getInstanceId(); - static @Nullable Object getInstanceId(Object object) { - if (object instanceof IInstanceIdentity) { - return ((IInstanceIdentity) object).getInstanceId(); - } - return object; + /** + * @param object - The object to inspect. + * @return - true when the object carries an instance, that is when {@link + * #getInstanceId(Object)} answers something other than {@link #NO_INSTANCE}. + */ + static boolean carriesInstance(Object object) { + return getInstanceId(object) != NO_INSTANCE; } /** - * @param objects - The objects to inspect - * @return - true if all the objects passed are of type {@link IInstanceIdentity} + * @param object - The object to read an instance id from. + * @return - The object's instance id when it is identity aware, {@link #NO_INSTANCE} when it is + * identity aware but carries no instance, and the object itself otherwise. */ - static boolean isIdentityAware(Object... objects) { - return Arrays.stream(Objects.requireNonNull(objects)) - .allMatch(it -> it instanceof IInstanceIdentity); + static Object getInstanceId(Object object) { + if (object instanceof IInstanceIdentity) { + UUID instanceId = ((IInstanceIdentity) object).getInstanceId(); + return instanceId == null ? NO_INSTANCE : instanceId; + } + return object; } } diff --git a/testng-core/src/main/java/org/testng/internal/IObject.java b/testng-core/src/main/java/org/testng/internal/IObject.java index 2091a5ffd4..5c56430bde 100644 --- a/testng-core/src/main/java/org/testng/internal/IObject.java +++ b/testng-core/src/main/java/org/testng/internal/IObject.java @@ -17,10 +17,10 @@ public interface IObject { * * @param create - true if objects should be created before returning. * @param errorMsgPrefix - Text that should be prefixed to the error message when there are - * issues. Can be empty. + * issues. Can be empty, and can be {@code null}, which is passed through unchanged. * @return - An array of {@link IdentifiableObject} objects */ - IdentifiableObject[] getObjects(boolean create, String errorMsgPrefix); + IdentifiableObject[] getObjects(boolean create, @Nullable String errorMsgPrefix); /** @return - An array representing the hash codes of the corresponding instances. */ long[] getInstanceHashCodes(); @@ -50,12 +50,12 @@ static IdentifiableObject[] objects(@Nullable Object object, boolean create) { * @param object - The object that should be inspected for its compatibility with {@link IObject}. * @param create - true if objects should be created before returning. * @param errorMsgPrefix - Text that should be prefixed to the error message when there are - * issues. Can be empty. + * issues. Can be empty, and can be {@code null}, which is passed through unchanged. * @return - An array (can be empty is instance compatibility fails) of {@link IdentifiableObject} * objects. */ static IdentifiableObject[] objects( - @Nullable Object object, boolean create, String errorMsgPrefix) { + @Nullable Object object, boolean create, @Nullable String errorMsgPrefix) { return cast(object) .map(it -> it.getObjects(create, errorMsgPrefix)) .orElse(new IdentifiableObject[] {}); diff --git a/testng-core/src/main/java/org/testng/internal/MethodHelper.java b/testng-core/src/main/java/org/testng/internal/MethodHelper.java index c102df74e8..d3bb387cfb 100644 --- a/testng-core/src/main/java/org/testng/internal/MethodHelper.java +++ b/testng-core/src/main/java/org/testng/internal/MethodHelper.java @@ -12,7 +12,6 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.Predicate; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -58,7 +57,6 @@ public static ITestNGMethod[] collectAndOrderMethods( boolean unique, List outExcludedMethods, Comparator comparator) { - AtomicReference results = new AtomicReference<>(); List includedMethods = new ArrayList<>(); TimeUtils.computeAndShowTime( "MethodGroupsHelper.collectMethodsByGroup()", @@ -71,13 +69,9 @@ public static ITestNGMethod[] collectAndOrderMethods( runInfo, finder, unique)); - TimeUtils.computeAndShowTime( + return TimeUtils.computeAndShowTime( "MethodGroupsHelper.sortMethods()", - () -> - results.set( - sortMethods(forTests, includedMethods, comparator) - .toArray(new ITestNGMethod[] {}))); - return Objects.requireNonNull(results.get(), "the sorted methods were never published"); + () -> sortMethods(forTests, includedMethods, comparator).toArray(new ITestNGMethod[] {})); } /** @@ -341,10 +335,9 @@ private static Graph topologicalSort( String[] methodsDependedUpon = m.getMethodsDependedUpon(); if (methodsDependedUpon.length > 0) { ITestNGMethod[] methodsNamed; - Object instanceId = IInstanceIdentity.getInstanceId(m); - // Method has instance - List instanceMethods = - instanceId == null ? null : testInstances.get(instanceId); + // sortMethodsByInstance keeps NO_INSTANCE out of the map, so a method that carries no + // instance simply misses here. + List instanceMethods = testInstances.get(IInstanceIdentity.getInstanceId(m)); if (instanceMethods != null) { try { // Search for other methods that depends upon with the same instance @@ -422,7 +415,7 @@ private static Map> sortMethodsByInstance(ITestNGMet // dependency graph never forces a lazy @Factory instance to be created during collection. return Arrays.stream(methods) .parallel() - .filter(m -> Objects.nonNull(IInstanceIdentity.getInstanceId(m))) + .filter(IInstanceIdentity::carriesInstance) .collect(Collectors.groupingBy(IInstanceIdentity::getInstanceId, Collectors.toList())); } @@ -513,7 +506,7 @@ public static List getMethodsDependedUpon( // TODO: This needs to be revisited so that, we dont update the parameter list "methodList" // but we are returning the values. public static void fixMethodsWithClass( - ITestNGMethod[] methods, ITestClass testCls, List methodList) { + ITestNGMethod[] methods, ITestClass testCls, @Nullable List methodList) { for (ITestNGMethod itm : methods) { itm.setTestClass(testCls); diff --git a/testng-core/src/main/java/org/testng/internal/MethodInstance.java b/testng-core/src/main/java/org/testng/internal/MethodInstance.java index 6f6dd683d8..335bc9a5cb 100644 --- a/testng-core/src/main/java/org/testng/internal/MethodInstance.java +++ b/testng-core/src/main/java/org/testng/internal/MethodInstance.java @@ -4,6 +4,7 @@ import java.util.List; import org.jspecify.annotations.Nullable; import org.testng.IMethodInstance; +import org.testng.ITestClass; import org.testng.ITestNGMethod; import org.testng.collections.Objects; import org.testng.xml.XmlClass; @@ -23,7 +24,7 @@ public ITestNGMethod getMethod() { } @Override - public Object getInstance() { + public @Nullable Object getInstance() { return m_method.getInstance(); } @@ -40,11 +41,16 @@ public String toString() { @Override public int compare(IMethodInstance o1, IMethodInstance o2) { // If the two methods are in different - XmlTest test1 = o1.getMethod().getTestClass().getXmlTest(); - XmlTest test2 = o2.getMethod().getTestClass().getXmlTest(); + ITestClass testClass1 = Utils.requireTestClassOf(o1.getMethod()); + ITestClass testClass2 = Utils.requireTestClassOf(o2.getMethod()); + XmlTest test1 = testClass1.getXmlTest(); + XmlTest test2 = testClass2.getXmlTest(); - // If the two methods are not in the same , we can't compare them - if (!java.util.Objects.equals(test1.getName(), test2.getName())) { + // If the two methods are not in the same , we can't compare them. A method a + // @Factory produced has no tag of its own, which reads the same way here. + String testName1 = test1 == null ? null : test1.getName(); + String testName2 = test2 == null ? null : test2.getName(); + if (!java.util.Objects.equals(testName1, testName2)) { return 0; } @@ -52,8 +58,8 @@ public int compare(IMethodInstance o1, IMethodInstance o2) { // If the two methods are in the same , compare them by their method // index, otherwise compare them with their class index. - XmlClass class1 = o1.getMethod().getTestClass().getXmlClass(); - XmlClass class2 = o2.getMethod().getTestClass().getXmlClass(); + XmlClass class1 = testClass1.getXmlClass(); + XmlClass class2 = testClass2.getXmlClass(); // This can happen if these classes came from a @Factory, in which case, they // don't have an associated XmlClass diff --git a/testng-core/src/main/java/org/testng/internal/MethodSorting.java b/testng-core/src/main/java/org/testng/internal/MethodSorting.java index d4b702ff24..cc420cacba 100644 --- a/testng-core/src/main/java/org/testng/internal/MethodSorting.java +++ b/testng-core/src/main/java/org/testng/internal/MethodSorting.java @@ -46,7 +46,11 @@ private int objectEquality(ITestNGMethod a, ITestNGMethod b) { // sorting never forces a lazy @Factory instance to be created during collection. Object one = IInstanceIdentity.getInstanceId(a); Object two = IInstanceIdentity.getInstanceId(b); - if (one != null && two != null && IInstanceIdentity.isIdentityAware(one, two)) { + // getInstanceId answers the UUID for an identity aware method and the method itself + // otherwise, so the test belongs on what came back. Testing the inputs for + // IInstanceIdentity, as this did, could never hold: a UUID is not one, and a method that + // is one never reaches this branch as itself. + if (one instanceof UUID && two instanceof UUID) { return ((UUID) one).compareTo((UUID) two); } return Integer.compare(Objects.hashCode(one), Objects.hashCode(two)); diff --git a/testng-core/src/main/java/org/testng/internal/NoOpTestClass.java b/testng-core/src/main/java/org/testng/internal/NoOpTestClass.java index b64dadcced..d910363e10 100644 --- a/testng-core/src/main/java/org/testng/internal/NoOpTestClass.java +++ b/testng-core/src/main/java/org/testng/internal/NoOpTestClass.java @@ -159,7 +159,7 @@ public void addInstance(Object instance) {} public void addObject(IdentifiableObject instance) {} @Override - public IdentifiableObject[] getObjects(boolean create, String errorMsgPrefix) { + public IdentifiableObject[] getObjects(boolean create, @Nullable String errorMsgPrefix) { return m_instances; } diff --git a/testng-core/src/main/java/org/testng/internal/OverrideProcessor.java b/testng-core/src/main/java/org/testng/internal/OverrideProcessor.java index a994c175d9..5108244962 100644 --- a/testng-core/src/main/java/org/testng/internal/OverrideProcessor.java +++ b/testng-core/src/main/java/org/testng/internal/OverrideProcessor.java @@ -2,6 +2,7 @@ import java.util.Arrays; import java.util.Collection; +import org.jspecify.annotations.Nullable; import org.testng.xml.IPostProcessor; import org.testng.xml.XmlSuite; import org.testng.xml.XmlTest; @@ -9,10 +10,10 @@ /** Override the groups included in the XML file with groups specified on the command line. */ public class OverrideProcessor implements IPostProcessor { - private final String[] m_groups; - private final String[] m_excludedGroups; + private final String @Nullable [] m_groups; + private final String @Nullable [] m_excludedGroups; - public OverrideProcessor(String[] groups, String[] excludedGroups) { + public OverrideProcessor(String @Nullable [] groups, String @Nullable [] excludedGroups) { m_groups = groups; m_excludedGroups = excludedGroups; } diff --git a/testng-core/src/main/java/org/testng/internal/Parameters.java b/testng-core/src/main/java/org/testng/internal/Parameters.java index 8daab4e817..2dacdb13f4 100644 --- a/testng-core/src/main/java/org/testng/internal/Parameters.java +++ b/testng-core/src/main/java/org/testng/internal/Parameters.java @@ -795,7 +795,7 @@ public static ParameterHolder handleParameters( findDataProvider( objectFactory, instance, - testMethod.getTestClass(), + Utils.requireTestClassOf(testMethod), testMethod.getConstructorOrMethod(), annotationFinder, methodParams.context); @@ -839,9 +839,13 @@ public static ParameterHolder handleParameters( MethodInvocationHelper.invokeDataProvider( dataProviderMethod .getInstance(), /* a test instance or null if the data provider is static*/ - dataProviderMethod.getMethod(), + Objects.requireNonNull( + dataProviderMethod.getMethod(), + "the data provider still holds its method while it yields rows"), testMethod, - methodParams.requireContext(), + Objects.requireNonNull( + methodParams.context, + "a data provider is invoked from inside a test context"), fedInstance, annotationFinder); shouldRetry = false; @@ -1006,14 +1010,6 @@ public MethodParameters( parameterValues = pv; testResult = tr; } - - /** - * The test context, for the callees that dereference it. The two-argument constructor builds - * parameters for a constructor injection, which happens before any context exists. - */ - ITestContext requireContext() { - return Objects.requireNonNull(context, "these method parameters carry no test context"); - } } private static final class ImmutableDataProvidable implements IDataProvidable { diff --git a/testng-core/src/main/java/org/testng/internal/ResultMap.java b/testng-core/src/main/java/org/testng/internal/ResultMap.java index cf7286dfbb..ef293bdbaa 100644 --- a/testng-core/src/main/java/org/testng/internal/ResultMap.java +++ b/testng-core/src/main/java/org/testng/internal/ResultMap.java @@ -21,7 +21,7 @@ public void addResult(ITestResult result) { @Override public Set getResults(ITestNGMethod method) { return results.stream() - .filter(result -> result.getMethod().equals(method)) + .filter(result -> method.equals(result.getMethod())) .collect(Collectors.toSet()); } diff --git a/testng-core/src/main/java/org/testng/internal/WrappedTestNGMethod.java b/testng-core/src/main/java/org/testng/internal/WrappedTestNGMethod.java index 15c7071223..ebe457ebdd 100644 --- a/testng-core/src/main/java/org/testng/internal/WrappedTestNGMethod.java +++ b/testng-core/src/main/java/org/testng/internal/WrappedTestNGMethod.java @@ -40,7 +40,7 @@ public Class getRealClass() { } @Override - public ITestClass getTestClass() { + public @Nullable ITestClass getTestClass() { return testNGMethod.getTestClass(); } @@ -55,7 +55,7 @@ public String getMethodName() { } @Override - public Object getInstance() { + public @Nullable Object getInstance() { return testNGMethod.getInstance(); } @@ -80,12 +80,12 @@ public String[] getGroupsDependedUpon() { } @Override - public String getMissingGroup() { + public @Nullable String getMissingGroup() { return testNGMethod.getMissingGroup(); } @Override - public void setMissingGroup(String group) { + public void setMissingGroup(@Nullable String group) { testNGMethod.setMissingGroup(group); } @@ -190,7 +190,7 @@ public int getSuccessPercentage() { } @Override - public String getId() { + public @Nullable String getId() { return testNGMethod.getId(); } @@ -235,12 +235,12 @@ public boolean getEnabled() { } @Override - public String getDescription() { + public @Nullable String getDescription() { return testNGMethod.getDescription(); } @Override - public void setDescription(String description) { + public void setDescription(@Nullable String description) { testNGMethod.setDescription(description); } @@ -280,7 +280,7 @@ public ITestNGMethod clone() { } @Override - public IRetryAnalyzer getRetryAnalyzer(ITestResult result) { + public @Nullable IRetryAnalyzer getRetryAnalyzer(ITestResult result) { return testNGMethod.getRetryAnalyzer(result); } @@ -360,7 +360,7 @@ public void setInterceptedPriority(int priority) { } @Override - public XmlTest getXmlTest() { + public @Nullable XmlTest getXmlTest() { return testNGMethod.getXmlTest(); } diff --git a/testng-core/src/main/java/org/testng/internal/XmlMethodSelector.java b/testng-core/src/main/java/org/testng/internal/XmlMethodSelector.java index 18a5b2b0ea..4da7023aeb 100644 --- a/testng-core/src/main/java/org/testng/internal/XmlMethodSelector.java +++ b/testng-core/src/main/java/org/testng/internal/XmlMethodSelector.java @@ -54,7 +54,7 @@ public class XmlMethodSelector implements IMethodSelector { @Override public boolean includeMethod( - IMethodSelectorContext context, ITestNGMethod tm, boolean isTestMethod) { + @Nullable IMethodSelectorContext context, ITestNGMethod tm, boolean isTestMethod) { if (!m_isInitialized) { m_isInitialized = true; @@ -350,7 +350,7 @@ private static void log(String s) { Utils.log("XmlMethodSelector", 4, s); } - public void setScript(XmlScript script) { + public void setScript(@Nullable XmlScript script) { scriptSelector = script == null ? null : ScriptSelectorFactory.getScriptSelector(script); } @@ -369,7 +369,7 @@ public void setOverrideIncludedMethods(boolean overrideIncludedMethods) { m_overrideIncludedMethods = overrideIncludedMethods; } - private void init(IMethodSelectorContext context) { + private void init(@Nullable IMethodSelectorContext context) { String[] groups = m_includedGroups.keySet().toArray(new String[0]); Set groupClosure = new HashSet<>(); Set methodClosure = new HashSet<>(); diff --git a/testng-core/src/main/java/org/testng/internal/annotations/DefaultAnnotationTransformer.java b/testng-core/src/main/java/org/testng/internal/annotations/DefaultAnnotationTransformer.java index 9eb899cdbb..38c625a2d7 100644 --- a/testng-core/src/main/java/org/testng/internal/annotations/DefaultAnnotationTransformer.java +++ b/testng-core/src/main/java/org/testng/internal/annotations/DefaultAnnotationTransformer.java @@ -10,7 +10,10 @@ public class DefaultAnnotationTransformer extends IgnoreListener implements IAnn @Override public void transform( - ITestAnnotation annotation, Class testClass, Constructor testConstructor, Method testMethod) { + ITestAnnotation annotation, + @Nullable Class testClass, + @Nullable Constructor testConstructor, + @Nullable Method testMethod) { super.transform(annotation, testClass, testConstructor, testMethod); } diff --git a/testng-core/src/main/java/org/testng/internal/annotations/IgnoreListener.java b/testng-core/src/main/java/org/testng/internal/annotations/IgnoreListener.java index e87150c9c5..d90e809fb3 100644 --- a/testng-core/src/main/java/org/testng/internal/annotations/IgnoreListener.java +++ b/testng-core/src/main/java/org/testng/internal/annotations/IgnoreListener.java @@ -12,7 +12,10 @@ public class IgnoreListener implements IAnnotationTransformer { @Override public void transform( - ITestAnnotation annotation, Class testClass, Constructor testConstructor, Method testMethod) { + ITestAnnotation annotation, + @Nullable Class testClass, + @Nullable Constructor testConstructor, + @Nullable Method testMethod) { transform(annotation, testClass, testConstructor, testMethod, null); } diff --git a/testng-core/src/main/java/org/testng/internal/annotations/JDK15AnnotationFinder.java b/testng-core/src/main/java/org/testng/internal/annotations/JDK15AnnotationFinder.java index 3ce9c8ce1b..e3aed1bbf4 100644 --- a/testng-core/src/main/java/org/testng/internal/annotations/JDK15AnnotationFinder.java +++ b/testng-core/src/main/java/org/testng/internal/annotations/JDK15AnnotationFinder.java @@ -186,11 +186,15 @@ private void transform( IConfigurationAnnotation configuration = (IConfigurationAnnotation) a; m_transformer.transform(configuration, testClass, testConstructor, testMethod); } else if (a instanceof IDataProviderAnnotation) { - m_transformer.transform((IDataProviderAnnotation) a, testMethod); + m_transformer.transform( + (IDataProviderAnnotation) a, + Objects.requireNonNull(testMethod, "a @DataProvider annotation is read off a method")); } else if (a instanceof IFactoryAnnotation) { m_transformer.transform((IFactoryAnnotation) a, testMethod); } else if (a instanceof IListenersAnnotation) { - m_transformer.transform((IListenersAnnotation) a, testClass); + m_transformer.transform( + (IListenersAnnotation) a, + Objects.requireNonNull(testClass, "a @Listeners annotation is read off a class")); } } diff --git a/testng-core/src/main/java/org/testng/internal/invokers/BaseInvoker.java b/testng-core/src/main/java/org/testng/internal/invokers/BaseInvoker.java index cfdc185988..b47d9b133d 100644 --- a/testng-core/src/main/java/org/testng/internal/invokers/BaseInvoker.java +++ b/testng-core/src/main/java/org/testng/internal/invokers/BaseInvoker.java @@ -68,7 +68,8 @@ protected void runInvokedMethodListeners( } InvokedMethodListenerInvoker invoker = - new InvokedMethodListenerInvoker(listenerMethod, testResult, testResult.getTestContext()); + new InvokedMethodListenerInvoker( + listenerMethod, testResult, Utils.requireTestContextOf(testResult)); // For BEFORE_INVOCATION method, still run as insert order, but regarding AFTER_INVOCATION, it // should be reverse order boolean isAfterInvocation = InvokedMethodListenerMethod.AFTER_INVOCATION == listenerMethod; diff --git a/testng-core/src/main/java/org/testng/internal/invokers/ClassBasedParallelWorker.java b/testng-core/src/main/java/org/testng/internal/invokers/ClassBasedParallelWorker.java index d53f28b4f0..c938a31689 100644 --- a/testng-core/src/main/java/org/testng/internal/invokers/ClassBasedParallelWorker.java +++ b/testng-core/src/main/java/org/testng/internal/invokers/ClassBasedParallelWorker.java @@ -13,6 +13,7 @@ import org.testng.IMethodInstance; import org.testng.ITestNGMethod; import org.testng.internal.MethodInstance; +import org.testng.internal.Utils; import org.testng.thread.IWorker; import org.testng.xml.XmlSuite; import org.testng.xml.XmlTest; @@ -50,7 +51,7 @@ public List> createWorkers(Arguments arguments) { Map params = null; Class prevClass = null; for (IMethodInstance im : methodInstances) { - Class c = im.getMethod().getTestClass().getRealClass(); + Class c = Utils.requireTestClassOf(im.getMethod()).getRealClass(); if (!c.equals(prevClass)) { // Calculate the parameters to be injected only once per Class and NOT for every iteration. params = getParameters(im); @@ -85,7 +86,7 @@ private static boolean shouldRunSequentially(Class c, Set> sequentia private static List findClasses( List methodInstances, Class c) { return methodInstances.stream() - .filter(mi -> mi.getMethod().getTestClass().getRealClass() == c) + .filter(mi -> Utils.requireTestClassOf(mi.getMethod()).getRealClass() == c) .collect(Collectors.toList()); } @@ -119,7 +120,9 @@ private static boolean isSequential( } private static Map getParameters(IMethodInstance im) { - XmlTest xmlTest = im.getMethod().getXmlTest(); - return im.getMethod().findMethodParameters(xmlTest); + ITestNGMethod method = im.getMethod(); + XmlTest xmlTest = + Objects.requireNonNull(method.getXmlTest(), "a scheduled method belongs to a "); + return method.findMethodParameters(xmlTest); } } diff --git a/testng-core/src/main/java/org/testng/internal/invokers/ConfigInvoker.java b/testng-core/src/main/java/org/testng/internal/invokers/ConfigInvoker.java index d654f6af40..32fa6b7b24 100644 --- a/testng-core/src/main/java/org/testng/internal/invokers/ConfigInvoker.java +++ b/testng-core/src/main/java/org/testng/internal/invokers/ConfigInvoker.java @@ -260,7 +260,7 @@ public void invokeConfigurations(ConfigMethodArguments arguments) { for (ITestNGMethod tm : methods) { if (null == arguments.getTestClass()) { - arguments.setTestClass(tm.getTestClass()); + arguments.setTestClass(Utils.requireTestClassOf(tm)); } // Defaulted just above, so it is set from here on. IClass testClass = Objects.requireNonNull(arguments.getTestClass()); diff --git a/testng-core/src/main/java/org/testng/internal/invokers/ConfigMethodArguments.java b/testng-core/src/main/java/org/testng/internal/invokers/ConfigMethodArguments.java index 86a7e989b4..acf4e3193b 100644 --- a/testng-core/src/main/java/org/testng/internal/invokers/ConfigMethodArguments.java +++ b/testng-core/src/main/java/org/testng/internal/invokers/ConfigMethodArguments.java @@ -100,7 +100,7 @@ public Builder usingParameterValues(Object @Nullable [] parameterValues) { return this; } - public Builder usingInstance(Object instance) { + public Builder usingInstance(@Nullable Object instance) { this.instance = instance; return this; } diff --git a/testng-core/src/main/java/org/testng/internal/invokers/GroupConfigMethodArguments.java b/testng-core/src/main/java/org/testng/internal/invokers/GroupConfigMethodArguments.java index 7813630bb2..6b9c70a469 100644 --- a/testng-core/src/main/java/org/testng/internal/invokers/GroupConfigMethodArguments.java +++ b/testng-core/src/main/java/org/testng/internal/invokers/GroupConfigMethodArguments.java @@ -41,7 +41,9 @@ public Object getInstance() { } public XmlSuite getSuite() { - return getTestMethod().getXmlTest().getSuite(); + return Objects.requireNonNull( + getTestMethod().getXmlTest(), "a grouped configuration method belongs to a ") + .getSuite(); } public static class Builder { @@ -66,7 +68,7 @@ public Builder withParameters(Map params) { return this; } - public Builder forInstance(Object instance) { + public Builder forInstance(@Nullable Object instance) { this.instance = instance; return this; } diff --git a/testng-core/src/main/java/org/testng/internal/invokers/ITestInvoker.java b/testng-core/src/main/java/org/testng/internal/invokers/ITestInvoker.java index 0c3b72a7cb..150c8a9ec4 100644 --- a/testng-core/src/main/java/org/testng/internal/invokers/ITestInvoker.java +++ b/testng-core/src/main/java/org/testng/internal/invokers/ITestInvoker.java @@ -29,7 +29,7 @@ class FailureContext { List invokeTestMethods( ITestNGMethod testMethod, ConfigurationGroupMethods groupMethods, - Object instance, + @Nullable Object instance, ITestContext context); ITestResult invokeTestMethod( diff --git a/testng-core/src/main/java/org/testng/internal/invokers/InvokedMethod.java b/testng-core/src/main/java/org/testng/internal/invokers/InvokedMethod.java index 14a1347323..52c64b4178 100644 --- a/testng-core/src/main/java/org/testng/internal/invokers/InvokedMethod.java +++ b/testng-core/src/main/java/org/testng/internal/invokers/InvokedMethod.java @@ -3,6 +3,7 @@ import org.testng.IInvokedMethod; import org.testng.ITestNGMethod; import org.testng.ITestResult; +import org.testng.internal.Utils; public class InvokedMethod implements IInvokedMethod { @@ -19,7 +20,7 @@ public InvokedMethod(long date, ITestResult testResult) { */ @Override public boolean isTestMethod() { - return m_testResult.getMethod().isTest(); + return Utils.requireMethodOf(m_testResult).isTest(); } @Override @@ -39,7 +40,7 @@ public String toString() { */ @Override public boolean isConfigurationMethod() { - return TestNgMethodUtils.isConfigurationMethod(m_testResult.getMethod()); + return TestNgMethodUtils.isConfigurationMethod(Utils.requireMethodOf(m_testResult)); } /* (non-Javadoc) @@ -47,7 +48,7 @@ public boolean isConfigurationMethod() { */ @Override public ITestNGMethod getTestMethod() { - return m_testResult.getMethod(); + return Utils.requireMethodOf(m_testResult); } /* (non-Javadoc) diff --git a/testng-core/src/main/java/org/testng/internal/invokers/Invoker.java b/testng-core/src/main/java/org/testng/internal/invokers/Invoker.java index f918468cc6..40116e8295 100644 --- a/testng-core/src/main/java/org/testng/internal/invokers/Invoker.java +++ b/testng-core/src/main/java/org/testng/internal/invokers/Invoker.java @@ -14,6 +14,7 @@ import org.testng.SuiteRunState; import org.testng.internal.IConfiguration; import org.testng.internal.ITestResultNotifier; +import org.testng.internal.Utils; /** * This class is responsible for invoking methods: - test methods - configuration methods - possibly @@ -26,7 +27,7 @@ public class Invoker implements IInvoker { ITestNGMethod::canRunFromClass; /** Predicate to filter methods */ static final BiPredicate SAME_CLASS = - (m, c) -> c == null || m.getTestClass().getName().equals(c.getName()); + (m, c) -> c == null || Utils.requireTestClassOf(m).getName().equals(c.getName()); private final TestInvoker m_testInvoker; private final ConfigInvoker m_configInvoker; diff --git a/testng-core/src/main/java/org/testng/internal/invokers/MethodInvocationHelper.java b/testng-core/src/main/java/org/testng/internal/invokers/MethodInvocationHelper.java index 7a778b707c..051e79c2f8 100644 --- a/testng-core/src/main/java/org/testng/internal/invokers/MethodInvocationHelper.java +++ b/testng-core/src/main/java/org/testng/internal/invokers/MethodInvocationHelper.java @@ -66,7 +66,7 @@ public class MethodInvocationHelper { private static final long TIMEOUT_STARTUP_POLL_MILLIS = 10; protected static Object invokeMethodNoCheckedException( - Method thisMethod, Object instance, List parameters) { + Method thisMethod, @Nullable Object instance, List parameters) { try { return invokeMethod(thisMethod, instance, parameters); } catch (InvocationTargetException | IllegalAccessException e) { @@ -90,20 +90,26 @@ protected static void invokeMethodConsideringTimeout( MethodInvocationHelper.invokeWithTimeout(config, tm, targetInstance, params, testResult); if (!testResult.isSuccess()) { // A time out happened - Throwable ex = testResult.getThrowable(); + // invokeWithTimeout only leaves the result unsuccessful by recording what went wrong. + Throwable ex = + Objects.requireNonNull( + testResult.getThrowable(), "a failed invocation records what it failed with"); testResult.setStatus(ITestResult.FAILURE); - testResult.setThrowable(ex.getCause() == null ? ex : ex.getCause()); - throw testResult.getThrowable(); + Throwable cause = ex.getCause() == null ? ex : ex.getCause(); + testResult.setThrowable(cause); + throw cause; } } } - protected static Object invokeMethod(Method thisMethod, Object instance, List parameters) + protected static Object invokeMethod( + Method thisMethod, @Nullable Object instance, List parameters) throws InvocationTargetException, IllegalAccessException { return invokeMethod(thisMethod, instance, parameters.toArray(new Object[0])); } - protected static Object invokeMethod(Method thisMethod, Object instance, Object[] parameters) + protected static Object invokeMethod( + Method thisMethod, @Nullable Object instance, Object[] parameters) throws InvocationTargetException, IllegalAccessException { Utils.checkInstanceOrStatic(instance, thisMethod); @@ -165,7 +171,7 @@ private static boolean canAccess(Method thisMethod) { @SuppressWarnings("unchecked") public static CloseableIterator invokeDataProvider( - Object instance, + @Nullable Object instance, Method dataProvider, ITestNGMethod method, ITestContext testContext, @@ -309,7 +315,7 @@ protected static boolean invokeWithTimeout( @Nullable IHookable hookable) throws InterruptedException, ThreadExecutionException { if (ThreadUtil.isTestNGThread() - && testResult.getTestContext().getCurrentXmlTest().getParallel() + && Utils.requireTestContextOf(testResult).getCurrentXmlTest().getParallel() != XmlSuite.ParallelMode.TESTS) { // We are already running in our own executor, don't create another one (or we will // lose the time out of the enclosing executor). diff --git a/testng-core/src/main/java/org/testng/internal/invokers/ParameterHandler.java b/testng-core/src/main/java/org/testng/internal/invokers/ParameterHandler.java index 8e1d2ccb2e..135eaec14e 100644 --- a/testng-core/src/main/java/org/testng/internal/invokers/ParameterHandler.java +++ b/testng-core/src/main/java/org/testng/internal/invokers/ParameterHandler.java @@ -61,7 +61,7 @@ ParameterBag createParameters( private ParameterBag handleParameters( ITestNGMethod testMethod, - Object instance, + @Nullable Object instance, Map allParameterNames, Map parameters, ITestContext testContext, diff --git a/testng-core/src/main/java/org/testng/internal/invokers/SuiteRunnerMap.java b/testng-core/src/main/java/org/testng/internal/invokers/SuiteRunnerMap.java index d0bf623177..b90a891f06 100644 --- a/testng-core/src/main/java/org/testng/internal/invokers/SuiteRunnerMap.java +++ b/testng-core/src/main/java/org/testng/internal/invokers/SuiteRunnerMap.java @@ -3,6 +3,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import org.jspecify.annotations.Nullable; import org.testng.ISuite; import org.testng.TestNGException; @@ -24,6 +25,14 @@ public void put(XmlSuite xmlSuite, ISuite suite) { return m_map.get(xmlSuite.getName()); } + /** + * @param xmlSuite The suite to look a runner up for. + * @return Its runner, which {@code createSuiteRunners} put here before the map was walked. + */ + public ISuite require(XmlSuite xmlSuite) { + return Objects.requireNonNull(get(xmlSuite), "every suite has a runner in the map"); + } + public Collection values() { return m_map.values(); } diff --git a/testng-core/src/main/java/org/testng/internal/invokers/TestInvoker.java b/testng-core/src/main/java/org/testng/internal/invokers/TestInvoker.java index fd457d5bbd..5573109f17 100644 --- a/testng-core/src/main/java/org/testng/internal/invokers/TestInvoker.java +++ b/testng-core/src/main/java/org/testng/internal/invokers/TestInvoker.java @@ -61,6 +61,7 @@ import org.testng.internal.RuntimeBehavior; import org.testng.internal.TestListenerHelper; import org.testng.internal.TestResult; +import org.testng.internal.Utils; import org.testng.internal.invokers.GroupConfigMethodArguments.Builder; import org.testng.internal.invokers.InvokeMethodRunnable.TestNGRuntimeException; import org.testng.internal.thread.ThreadExecutionException; @@ -107,7 +108,7 @@ public ITestResultNotifier getNotifier() { public List invokeTestMethods( ITestNGMethod testMethod, ConfigurationGroupMethods groupMethods, - Object instance, + @Nullable Object instance, ITestContext context) { // Potential bug here if the test method was declared on a parent class if (testMethod.getTestClass() == null) { @@ -505,12 +506,14 @@ private boolean failuresPresentInUpstreamDependency( /** @return the test results that apply to one of the instances of the testMethod. */ private Set keepSameInstances(ITestNGMethod method, Set results) { + Class methodRealClass = Utils.requireTestClassOf(method).getRealClass(); return results .parallelStream() .filter( r -> { + Object resultInstance = r.getInstance(); Object instance = - Optional.ofNullable(r.getInstance()).orElse(r.getMethod().getInstance()); + resultInstance != null ? resultInstance : Utils.requireMethodOf(r).getInstance(); if (method.getGroupsDependedUpon().length == 0) { // Consider equality of objects alone if we are NOT dealing with group dependency. return instance == method.getInstance(); @@ -518,8 +521,7 @@ private Set keepSameInstances(ITestNGMethod method, Set invokePooledTestMethods( */ private void invokeTimeOnlyConfigurations( ITestNGMethod testMethod, Map parameters, boolean before) { - ITestClass testClass = testMethod.getTestClass(); + ITestClass testClass = Utils.requireTestClassOf(testMethod); XmlSuite suite = m_testContext.getSuite().getXmlSuite(); for (IObject.IdentifiableObject identifiable : IObject.objects(testClass, true)) { Object instance = identifiable.getInstance(); @@ -667,7 +669,12 @@ private void handleInvocationResult( String key = Arrays.toString(testResult.getParameters()); count = failure.counter.computeIfAbsent(key, k -> new AtomicInteger()).incrementAndGet(); } - handleException(testResult.getThrowable(), testMethod, testResult, count); + handleException( + Objects.requireNonNull( + testResult.getThrowable(), "a failed result records what it failed with"), + testMethod, + testResult, + count); } } } @@ -814,7 +821,7 @@ private ITestResult invokeMethod( && hookableInstance != null && willfullyIgnored && testStatusRemainedUnchanged) { - TestNotInvokedException tn = new TestNotInvokedException(arguments.tm); + TestNotInvokedException tn = new TestNotInvokedException(arguments.getTestMethod()); testResult.setThrowable(tn); setTestStatus(testResult, ITestResult.FAILURE); } diff --git a/testng-core/src/main/java/org/testng/internal/invokers/TestMethodArguments.java b/testng-core/src/main/java/org/testng/internal/invokers/TestMethodArguments.java index 1abf6083e9..6e21abe8e7 100644 --- a/testng-core/src/main/java/org/testng/internal/invokers/TestMethodArguments.java +++ b/testng-core/src/main/java/org/testng/internal/invokers/TestMethodArguments.java @@ -81,7 +81,7 @@ public static class Builder { private ITestNGMethod @Nullable [] afterMethods; private @Nullable ConfigurationGroupMethods groupMethods; - public Builder usingInstance(Object instance) { + public Builder usingInstance(@Nullable Object instance) { this.instance = instance; return this; } diff --git a/testng-core/src/main/java/org/testng/internal/invokers/TestMethodWorker.java b/testng-core/src/main/java/org/testng/internal/invokers/TestMethodWorker.java index f0e6a89030..17c2000b10 100644 --- a/testng-core/src/main/java/org/testng/internal/invokers/TestMethodWorker.java +++ b/testng-core/src/main/java/org/testng/internal/invokers/TestMethodWorker.java @@ -29,6 +29,7 @@ import org.testng.internal.RuntimeBehavior; import org.testng.internal.TestMethodComparator; import org.testng.internal.TestMethodContainer; +import org.testng.internal.Utils; import org.testng.internal.invokers.ConfigMethodArguments.Builder; import org.testng.thread.IWorker; @@ -132,7 +133,8 @@ && doesTaskHavePreRequisites() for (IMethodInstance testMethodInstance : m_methodInstances) { ITestNGMethod testMethod = testMethodInstance.getMethod(); - Object key = Objects.requireNonNull(IInstanceIdentity.getInstanceId(testMethod)); + ITestClass testClassOfMethod = Utils.requireTestClassOf(testMethod); + Object key = IInstanceIdentity.getInstanceId(testMethod); // For a lazy @Factory instance this is the just-in-time construction point. Trigger creation // here so that a constructor failure is localized to this instance's method (reported as a @@ -152,7 +154,7 @@ && doesTaskHavePreRequisites() if (canInvokeBeforeClassMethods()) { try (KeyAwareAutoCloseableLock.AutoReleasable ignored = lock.lockForObject(key)) { - invokeBeforeClassMethods(testMethod.getTestClass(), testMethodInstance); + invokeBeforeClassMethods(testClassOfMethod, testMethodInstance); } } @@ -161,7 +163,7 @@ && doesTaskHavePreRequisites() invokeTestMethods(testMethod, testMethod.getInstance()); } finally { try (KeyAwareAutoCloseableLock.AutoReleasable ignored = lock.lockForObject(key)) { - invokeAfterClassMethods(testMethod.getTestClass(), testMethodInstance); + invokeAfterClassMethods(testClassOfMethod, testMethodInstance); } } } @@ -171,7 +173,7 @@ private boolean doesTaskHavePreRequisites() { return threadIdToRunOn != -1; } - protected void invokeTestMethods(ITestNGMethod tm, Object instance) { + protected void invokeTestMethods(ITestNGMethod tm, @Nullable Object instance) { // Potential bug here: we look up the method index of tm among all // the test methods (not very efficient) but if this method appears // several times and these methods are run in parallel, the results diff --git a/testng-core/src/main/java/org/testng/internal/invokers/TestNgMethodUtils.java b/testng-core/src/main/java/org/testng/internal/invokers/TestNgMethodUtils.java index 83ef2535a8..94b54c6243 100644 --- a/testng-core/src/main/java/org/testng/internal/invokers/TestNgMethodUtils.java +++ b/testng-core/src/main/java/org/testng/internal/invokers/TestNgMethodUtils.java @@ -74,12 +74,16 @@ private static boolean containsConfigurationMethod( } static ITestNGMethod[] filterBeforeTestMethods( - Object instance, ITestClass testClass, BiPredicate predicate) { + @Nullable Object instance, + ITestClass testClass, + BiPredicate predicate) { return filterMethods(instance, testClass, testClass.getBeforeTestMethods(), predicate); } static ITestNGMethod[] filterAfterTestMethods( - Object instance, ITestClass testClass, BiPredicate predicate) { + @Nullable Object instance, + ITestClass testClass, + BiPredicate predicate) { return filterMethods(instance, testClass, testClass.getAfterTestMethods(), predicate); } diff --git a/testng-core/src/main/java/org/testng/internal/objects/GuiceBackedInjectorFactory.java b/testng-core/src/main/java/org/testng/internal/objects/GuiceBackedInjectorFactory.java index 3ac2988f07..a730713405 100644 --- a/testng-core/src/main/java/org/testng/internal/objects/GuiceBackedInjectorFactory.java +++ b/testng-core/src/main/java/org/testng/internal/objects/GuiceBackedInjectorFactory.java @@ -4,7 +4,7 @@ import com.google.inject.Injector; import com.google.inject.Module; import com.google.inject.Stage; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; import org.testng.IInjectorFactory; public class GuiceBackedInjectorFactory implements IInjectorFactory { diff --git a/testng-core/src/main/java/org/testng/internal/objects/GuiceHelper.java b/testng-core/src/main/java/org/testng/internal/objects/GuiceHelper.java index d113c8960e..b518162e32 100644 --- a/testng-core/src/main/java/org/testng/internal/objects/GuiceHelper.java +++ b/testng-core/src/main/java/org/testng/internal/objects/GuiceHelper.java @@ -14,6 +14,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.ServiceLoader; import java.util.function.BiPredicate; import java.util.stream.StreamSupport; @@ -39,7 +40,7 @@ class GuiceHelper { Maps.newListMultiMap(); private final String parentModule; private final String stageString; - private final String testName; + private final @Nullable String testName; private final @Nullable ITestContext context; private static final BiPredicate CLASS_EQUALITY = @@ -65,7 +66,7 @@ Injector getInjector(IClass iClass, IInjectorFactory injectorFactory) { } @Nullable - Injector getInjector(Class cls, IInjectorFactory injectorFactory) { + Injector getInjector(Class cls, @Nullable IInjectorFactory injectorFactory) { Guice guice = AnnotationHelper.findAnnotationSuperClasses(Guice.class, cls); if (guice == null) { return null; @@ -85,7 +86,7 @@ Injector getInjector(Class cls, IInjectorFactory injectorFactory) { return injector; } - private Injector getParentInjector(IInjectorFactory factory) { + private Injector getParentInjector(@Nullable IInjectorFactory factory) { // Reuse the previous parent injector, if any Injector injector = null; ISuite suite = null; @@ -171,8 +172,14 @@ private List getGuiceModules(Class cls) { return (Class) parentModule; } + private static IInjectorFactory requireInjectorFactory(@Nullable IInjectorFactory factory) { + return Objects.requireNonNull(factory, "a running suite carries an injector factory"); + } + private Injector createInjector( - @Nullable Injector parent, IInjectorFactory injectorFactory, List moduleInstances) { + @Nullable Injector parent, + @Nullable IInjectorFactory injectorFactory, + List moduleInstances) { Stage stage = Stage.DEVELOPMENT; if (isStringNotEmpty(stageString)) { stage = Stage.valueOf(stageString); @@ -183,10 +190,10 @@ private Injector createInjector( if (parent == null || getParentModuleClass() == null) { // there is no parent module in this suite defined therefore tree of injectors shouldn't // be created letting individual test modules to redefine bindings between each other - return injectorFactory.getInjector(null, stage, modules); + return requireInjectorFactory(injectorFactory).getInjector(null, stage, modules); } - return injectorFactory.getInjector(parent, stage, modules); + return requireInjectorFactory(injectorFactory).getInjector(parent, stage, modules); } private List getModules(Guice guice, Injector parentInjector, Class testClass) { diff --git a/testng-core/src/main/java/org/testng/internal/objects/SimpleObjectDispenser.java b/testng-core/src/main/java/org/testng/internal/objects/SimpleObjectDispenser.java index ea90fed14b..f793aa6b72 100644 --- a/testng-core/src/main/java/org/testng/internal/objects/SimpleObjectDispenser.java +++ b/testng-core/src/main/java/org/testng/internal/objects/SimpleObjectDispenser.java @@ -73,7 +73,7 @@ public void setNextDispenser(IObjectDispenser dispenser) { IAnnotationFinder finder, ITestObjectFactory objectFactory, boolean create, - String errorMsgPrefix) { + @Nullable String errorMsgPrefix) { T result = null; try { @@ -180,7 +180,8 @@ private static T instantiateUsingDefaultConstructor( parameters = new Object[] {xmlTest.getName()}; } ct.setAccessible(true); - return factory.newInstance(ct, parameters); + return Objects.requireNonNull( + factory.newInstance(ct, parameters), "the object factory produced an instance"); } private static Object computeParameters( @@ -194,7 +195,8 @@ private static Object computeParameters( } IObject.IdentifiableObject[] enclosingInstances = IObject.objects(enclosingIClass, false); if (enclosingInstances.length == 0) { - return factory.newInstance(ec.getConstructor(ec)); + return Objects.requireNonNull( + factory.newInstance(ec.getConstructor(ec)), "the object factory produced an instance"); } return enclosingInstances[0].getInstance(); } diff --git a/testng-core/src/main/java/org/testng/internal/objects/pojo/DetailedAttributes.java b/testng-core/src/main/java/org/testng/internal/objects/pojo/DetailedAttributes.java index 2f99079972..873c213c80 100644 --- a/testng-core/src/main/java/org/testng/internal/objects/pojo/DetailedAttributes.java +++ b/testng-core/src/main/java/org/testng/internal/objects/pojo/DetailedAttributes.java @@ -1,6 +1,7 @@ package org.testng.internal.objects.pojo; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.testng.IClass; import org.testng.internal.annotations.IAnnotationFinder; import org.testng.xml.XmlTest; @@ -13,7 +14,7 @@ public class DetailedAttributes { private final XmlTest xmlTest; private final IAnnotationFinder finder; private final boolean create; - private final String errorMsgPrefix; + private final @Nullable String errorMsgPrefix; public DetailedAttributes( Class declaringClass, @@ -21,7 +22,7 @@ public DetailedAttributes( XmlTest xmlTest, IAnnotationFinder finder, boolean create, - String errorMsgPrefix) { + @Nullable String errorMsgPrefix) { this.declaringClass = declaringClass; this.classes = classes; this.xmlTest = xmlTest; @@ -50,7 +51,7 @@ public boolean isCreate() { return create; } - public String getErrorMsgPrefix() { + public @Nullable String getErrorMsgPrefix() { return errorMsgPrefix; } } diff --git a/testng-core/src/main/java/org/testng/internal/reporters/ParameterSnapshots.java b/testng-core/src/main/java/org/testng/internal/reporters/ParameterSnapshots.java index fab5099198..241b4b973e 100644 --- a/testng-core/src/main/java/org/testng/internal/reporters/ParameterSnapshots.java +++ b/testng-core/src/main/java/org/testng/internal/reporters/ParameterSnapshots.java @@ -6,6 +6,7 @@ import org.testng.ISuite; import org.testng.ITestContext; import org.testng.ITestResult; +import org.testng.internal.Utils; /** * The {@link ParameterSnapshot} taken for each invocation of a suite, produced once and read by @@ -108,7 +109,8 @@ public void captureIfAbsent(ITestResult result) { ParameterSnapshot snapshot; try { snapshot = - ParameterSnapshot.of(result.getParameters(), result.getMethod().getParameterTypes()); + ParameterSnapshot.of( + result.getParameters(), Utils.requireMethodOf(result).getParameterTypes()); } catch (Throwable rendering) { // Rendering a value calls the user's toString(). One that throws would otherwise fail the // invocation it is only being reported on; leaving the result unsnapshotted hands it back to diff --git a/testng-core/src/main/java/org/testng/reporters/EmailableReporter2.java b/testng-core/src/main/java/org/testng/reporters/EmailableReporter2.java index 08111d8068..01df5b33eb 100644 --- a/testng-core/src/main/java/org/testng/reporters/EmailableReporter2.java +++ b/testng-core/src/main/java/org/testng/reporters/EmailableReporter2.java @@ -180,7 +180,7 @@ protected void writeSuiteSummary() { .append("") - .append(Utils.escapeHtml(testResult.getTestName())) + .append(escapeHtmlOrEmpty(testResult.getTestName())) .append("") .toString()); writeTableData(integerFormat.format(passedTests), "num"); @@ -253,7 +253,7 @@ protected void writeScenarioSummary() { for (TestResult testResult : suiteResult.getTestResults()) { writer.printf("", testIndex); - String testName = Utils.escapeHtml(testResult.getTestName()); + String testName = escapeHtmlOrEmpty(testResult.getTestName()); int startIndex = scenarioIndex; scenarioIndex += @@ -336,7 +336,7 @@ private int writeScenarioSummary( int resultsCount = results.size(); ITestResult firstResult = results.iterator().next(); - String methodName = Utils.escapeHtml(firstResult.getMethod().getMethodName()); + String methodName = Utils.escapeHtml(Utils.requireMethodOf(firstResult).getMethodName()); long start = firstResult.getStartMillis(); long duration = firstResult.getEndMillis() - start; @@ -413,7 +413,7 @@ protected void writeScenarioDetails() { for (SuiteResult suiteResult : suiteResults) { for (TestResult testResult : suiteResult.getTestResults()) { writer.print("

"); - writer.print(Utils.escapeHtml(testResult.getTestName())); + writer.print(escapeHtmlOrEmpty(testResult.getTestName())); writer.print("

"); scenarioIndex += @@ -441,7 +441,7 @@ private int writeScenarioDetails(List classResults, int startingSce String label = Utils.escapeHtml( - className + "#" + results.iterator().next().getMethod().getMethodName()); + className + "#" + Utils.requireMethodOf(results.iterator().next()).getMethodName()); for (ITestResult result : results) { writeScenario(scenarioIndex, label, result); scenarioIndex++; @@ -467,7 +467,7 @@ private void writeScenario(int scenarioIndex, String label, ITestResult result) boolean hasRows = dumpParametersInfo("Factory Parameter", result.getFactoryParameters()); int parameterCount = parameters == null ? 0 : parameters.length; hasRows = dumpParametersInfo("Parameter", result.getParameters()); - dumpAttributesInfo(result.getMethod().getAttributes()); + dumpAttributesInfo(Utils.requireMethodOf(result).getAttributes()); // Write reporter messages (if any) List reporterMessages = Reporter.getOutput(result); @@ -648,6 +648,11 @@ protected void writeTag(String tag, String html, @Nullable String cssClasses) { writer.print(">"); } + /** A <test> that carries no name renders as an empty cell rather than the text "null". */ + private static String escapeHtmlOrEmpty(@Nullable String text) { + return text == null ? "" : Utils.escapeHtml(text); + } + /** Groups {@link TestResult}s by suite. */ protected static class SuiteResult { private final String suiteName; @@ -681,9 +686,9 @@ protected static class TestResult { /** Orders test results by class name and then by method name (in lexicographic order). */ protected static final Comparator RESULT_COMPARATOR = Comparator.comparing((ITestResult o) -> o.getTestClass().getName()) - .thenComparing(o -> o.getMethod().getMethodName()); + .thenComparing(o -> Utils.requireMethodOf(o).getMethodName()); - private final String testName; + private final @Nullable String testName; private final List failedConfigurationResults; private final List failedTestResults; private final List skippedConfigurationResults; @@ -722,7 +727,7 @@ public TestResult(ITestContext context) { skippedTestCount = skippedTests.size(); passedTestCount = passedTests.size(); - duration = context.getEndDate().getTime() - context.getStartDate().getTime(); + duration = Utils.requireEndDateOf(context).getTime() - context.getStartDate().getTime(); includedGroups = formatGroups(context.getIncludedGroups()); excludedGroups = formatGroups(context.getExcludedGroups()); @@ -759,7 +764,7 @@ protected List groupResults(Set results) { resultsPerMethod.add(result); String previousClassName = result.getTestClass().getName(); - String previousMethodName = result.getMethod().getMethodName(); + String previousMethodName = Utils.requireMethodOf(result).getMethodName(); while (resultsIterator.hasNext()) { result = resultsIterator.next(); @@ -776,9 +781,9 @@ protected List groupResults(Set results) { resultsPerClass = new ArrayList<>(); previousClassName = className; - previousMethodName = result.getMethod().getMethodName(); + previousMethodName = Utils.requireMethodOf(result).getMethodName(); } else { - String methodName = result.getMethod().getMethodName(); + String methodName = Utils.requireMethodOf(result).getMethodName(); if (!previousMethodName.equals(methodName)) { if (resultsPerMethod.isEmpty()) { throw new IllegalStateException("Results per method should NOT have been empty"); @@ -800,7 +805,7 @@ protected List groupResults(Set results) { return classResults; } - public String getTestName() { + public @Nullable String getTestName() { return testName; } diff --git a/testng-core/src/main/java/org/testng/reporters/FailedReporter.java b/testng-core/src/main/java/org/testng/reporters/FailedReporter.java index 3dd9471548..5d2c4d3d6e 100644 --- a/testng-core/src/main/java/org/testng/reporters/FailedReporter.java +++ b/testng-core/src/main/java/org/testng/reporters/FailedReporter.java @@ -103,8 +103,8 @@ private void clearKeyCache(ITestContext ctx) { } private static MethodInvocationKey key(ITestResult it) { - return new MethodInvocationKey( - it.getMethod(), it.getParameters(), it.getMethod().getCurrentInvocationCount()); + ITestNGMethod method = Utils.requireMethodOf(it); + return new MethodInvocationKey(method, it.getParameters(), method.getCurrentInvocationCount()); } private static Map buildMap(Set passed) { @@ -116,7 +116,7 @@ private static Map buildMap(Set passed } private boolean isFlakyTest(Set passed, ITestResult result) { - String ctxKey = result.getTestContext().getName(); + String ctxKey = Utils.requireTestContextOf(result).getName(); MethodInvocationKey individualKey = key(result); return keyCache.computeIfAbsent(ctxKey, k -> buildMap(passed)).containsKey(individualKey); } @@ -140,12 +140,12 @@ private boolean generateXmlTest(ITestContext context) { allTests.addAll(skippedTests); ITestNGMethod[] allTestMethods = context.getAllTestMethods(); for (ITestResult failedTest : allTests) { - ITestNGMethod current = failedTest.getMethod(); + ITestNGMethod current = Utils.requireMethodOf(failedTest); if (!current.isTest()) { // Don't count configuration methods continue; } - boolean repetitiveTest = failedTest.getMethod().getInvocationCount() > 0; - boolean isDataDriven = failedTest.getMethod().isDataDriven(); + boolean repetitiveTest = current.getInvocationCount() > 0; + boolean isDataDriven = current.isDataDriven(); if ((repetitiveTest || isDataDriven) && isFlakyTest(passedTests, failedTest)) { continue; } @@ -177,7 +177,7 @@ private boolean generateXmlTest(ITestContext context) { } if (methodsToReRun.contains(m)) { result.add(m); - getAllApplicableConfigs(relevantConfigs, m.getTestClass()); + getAllApplicableConfigs(relevantConfigs, Utils.requireTestClassOf(m)); getAllGroupApplicableConfigs(context, relevantConfigs, m); } } diff --git a/testng-core/src/main/java/org/testng/reporters/JUnitReportReporter.java b/testng-core/src/main/java/org/testng/reporters/JUnitReportReporter.java index ed8b4d80a4..e1bbc234d5 100644 --- a/testng-core/src/main/java/org/testng/reporters/JUnitReportReporter.java +++ b/testng-core/src/main/java/org/testng/reporters/JUnitReportReporter.java @@ -51,10 +51,11 @@ public void generateReport( addResults(tc.getSkippedTests().getAllResults(), results); addResults(tc.getFailedConfigurations().getAllResults(), results); for (ITestResult tr : tc.getPassedConfigurations().getAllResults()) { - if (tr.getMethod().isBeforeMethodConfiguration()) { + ITestNGMethod configMethod = Utils.requireMethodOf(tr); + if (configMethod.isBeforeMethodConfiguration()) { befores.put(tr.getInstance(), tr); } - if (tr.getMethod().isAfterMethodConfiguration()) { + if (configMethod.isAfterMethodConfiguration()) { afters.put(tr.getInstance(), tr); } } @@ -172,7 +173,7 @@ public void generateReport( private static Collection sort(Set results) { List sortedResults = new ArrayList<>(results); - sortedResults.sort(Comparator.comparingInt(o -> o.getMethod().getPriority())); + sortedResults.sort(Comparator.comparingInt(o -> Utils.requireMethodOf(o).getPriority())); return Collections.unmodifiableList(sortedResults); } @@ -216,7 +217,7 @@ private TestTag createTestTagFor(ITestResult tr, Class cls) { return testTag; } - private static void handleFailure(TestTag testTag, Throwable t) { + private static void handleFailure(TestTag testTag, @Nullable Throwable t) { testTag.childTag = t instanceof AssertionError ? XMLConstants.FAILURE : XMLConstants.ERROR; if (t != null) { StringWriter sw = new StringWriter(); @@ -275,7 +276,7 @@ protected String getFileName(Class cls) { } protected String getTestName(ITestResult tr) { - return tr.getMethod().getMethodName(); + return Utils.requireMethodOf(tr).getMethodName(); } private String formatTime(float time) { @@ -304,7 +305,7 @@ private static class TestTag { private void addResults(Set allResults, Map, Set> out) { for (ITestResult tr : allResults) { - Class cls = tr.getMethod().getTestClass().getRealClass(); + Class cls = tr.getTestClass().getRealClass(); Set l = out.computeIfAbsent(cls, k -> new HashSet<>()); l.add(tr); } diff --git a/testng-core/src/main/java/org/testng/reporters/JUnitXMLReporter.java b/testng-core/src/main/java/org/testng/reporters/JUnitXMLReporter.java index ab68cfdfe9..69bd5454ef 100644 --- a/testng-core/src/main/java/org/testng/reporters/JUnitXMLReporter.java +++ b/testng-core/src/main/java/org/testng/reporters/JUnitXMLReporter.java @@ -11,6 +11,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.regex.Pattern; +import org.jspecify.annotations.Nullable; import org.testng.ITestContext; import org.testng.ITestNGMethod; import org.testng.ITestResult; @@ -127,7 +128,8 @@ protected void generateReport(ITestContext context) { attrs.setProperty( XMLConstants.ATTR_TIME, Double.toString( - (context.getEndDate().getTime() - context.getStartDate().getTime()) / 1000.0)); + (Utils.requireEndDateOf(context).getTime() - context.getStartDate().getTime()) + / 1000.0)); attrs.setProperty(XMLConstants.ATTR_TIMESTAMP, formattedTime()); @@ -193,8 +195,9 @@ private Set getPackages(ITestContext context) { private void createElement(XMLStringBuffer doc, ITestResult tr) { long elapsedTimeMillis = tr.getEndMillis() - tr.getStartMillis(); - Properties attrs = getPropertiesFor(tr.getMethod(), elapsedTimeMillis); - if (tr.getMethod().isTest()) { + ITestNGMethod method = Utils.requireMethodOf(tr); + Properties attrs = getPropertiesFor(method, elapsedTimeMillis); + if (method.isTest()) { attrs.setProperty(XMLConstants.ATTR_NAME, tr.getName()); } @@ -280,7 +283,7 @@ private void resetAll() { * @param context test context * @return unique name for the file associated with this test context. */ - private String generateFileName(ITestContext context) { + private @Nullable String generateFileName(ITestContext context) { String fileName; String keyToSearch = context.getSuite().getName() + context.getName(); if (m_fileNameMap.get(keyToSearch) == null) { diff --git a/testng-core/src/main/java/org/testng/reporters/SuiteHTMLReporter.java b/testng-core/src/main/java/org/testng/reporters/SuiteHTMLReporter.java index bb49a8d0c9..00b5b2af8c 100644 --- a/testng-core/src/main/java/org/testng/reporters/SuiteHTMLReporter.java +++ b/testng-core/src/main/java/org/testng/reporters/SuiteHTMLReporter.java @@ -567,7 +567,7 @@ private void generateTableOfContents(XmlSuite xmlSuite, ISuite suite) { // Collect testClasses for (ITestNGMethod tm : methods) { - ITestClass tc = tm.getTestClass(); + ITestClass tc = Utils.requireTestClassOf(tm); m_classes.put(tc.getRealClass().getName(), tc); } } @@ -711,7 +711,7 @@ private ISuiteResult[] sortResults(Collection r) { } private void generateSuiteResult( - String suiteName, ISuiteResult sr, String cssClass, StringBuilder tableOfContents) { + @Nullable String suiteName, ISuiteResult sr, String cssClass, StringBuilder tableOfContents) { ITestContext tc = sr.getTestContext(); int passed = tc.getPassedTests().size(); int failed = tc.getFailedTests().size(); diff --git a/testng-core/src/main/java/org/testng/reporters/TestHTMLReporter.java b/testng-core/src/main/java/org/testng/reporters/TestHTMLReporter.java index a130af352c..1dd8303efb 100644 --- a/testng-core/src/main/java/org/testng/reporters/TestHTMLReporter.java +++ b/testng-core/src/main/java/org/testng/reporters/TestHTMLReporter.java @@ -89,7 +89,7 @@ public static void generateTable( pw.append("\n"); // Test method - ITestNGMethod method = tr.getMethod(); + ITestNGMethod method = Utils.requireMethodOf(tr); String name = method.getMethodName(); pw.append("= 0; x--) { if (cname.equals(stack[x].getClassName()) && method.getMethodName().equals(stack[x].getMethodName())) { diff --git a/testng-core/src/main/java/org/testng/util/TimeUtils.java b/testng-core/src/main/java/org/testng/util/TimeUtils.java index 93dae86c6f..138e01b279 100644 --- a/testng-core/src/main/java/org/testng/util/TimeUtils.java +++ b/testng-core/src/main/java/org/testng/util/TimeUtils.java @@ -4,6 +4,7 @@ import java.time.Duration; import java.time.Instant; import java.util.TimeZone; +import java.util.function.Supplier; import org.testng.internal.RuntimeBehavior; import org.testng.internal.Utils; @@ -40,9 +41,26 @@ public interface Task { * @param task - A {@link Task} that represents the task to be executed. */ public static void computeAndShowTime(String msg, Task task) { + computeAndShowTime( + msg, + () -> { + task.execute(); + return null; + }); + } + + /** + * Helper method that can be used to compute the time a task that answers something takes. + * + * @param msg - A user friendly message to be shown in the logs. + * @param task - The task to be executed. + * @param - What the task answers. + * @return - Whatever the task answered. + */ + public static T computeAndShowTime(String msg, Supplier task) { Instant start = Instant.now(); try { - task.execute(); + return task.get(); } finally { Instant finish = Instant.now(); long timeElapsed = Duration.between(start, finish).toMillis(); diff --git a/testng-core/src/main/java/org/testng/xml/internal/Parser.java b/testng-core/src/main/java/org/testng/xml/internal/Parser.java index d07875a7b6..82bf7f251a 100644 --- a/testng-core/src/main/java/org/testng/xml/internal/Parser.java +++ b/testng-core/src/main/java/org/testng/xml/internal/Parser.java @@ -85,7 +85,7 @@ private void init(@Nullable String fileName, @Nullable InputStream is) { m_inputStream = is; } - public void setPostProcessor(IPostProcessor processor) { + public void setPostProcessor(@Nullable IPostProcessor processor) { m_postProcessor = processor; } @@ -235,7 +235,7 @@ public List parseToList() throws IOException { return new ArrayList<>(parse()); } - public static Collection parse(String suite, IPostProcessor processor) + public static Collection parse(@Nullable String suite, IPostProcessor processor) throws IOException { return newParser(suite, processor).parse(); } @@ -255,7 +255,7 @@ public static boolean canParse(String fileName) { return DEFAULT_FILE_PARSER.accept(fileName); } - private static Parser newParser(String path, IPostProcessor processor) { + private static Parser newParser(@Nullable String path, @Nullable IPostProcessor processor) { Parser result = new Parser(path); result.setPostProcessor(processor); return result; diff --git a/testng-core/src/test/java/org/testng/internal/MethodSortingTest.java b/testng-core/src/test/java/org/testng/internal/MethodSortingTest.java new file mode 100644 index 0000000000..7e882aebff --- /dev/null +++ b/testng-core/src/test/java/org/testng/internal/MethodSortingTest.java @@ -0,0 +1,74 @@ +package org.testng.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.UUID; +import org.testng.ITestNGMethod; +import org.testng.annotations.Test; +import org.testng.internal.MethodInstanceTest.TestNGMethodStub; + +/** + * {@link MethodSorting#INSTANCES} used to fall through to a hash code comparison for every pair, + * because its identity branch tested the wrong values. + * + *

These do not distinguish the two: the ids are random UUIDs, so comparing them and comparing + * their hash codes both produce an arbitrary order, and the hash collision that would tell them + * apart is not reachable from a test. What they pin is the contract the identity branch restores + * unconditionally and the hash comparison only kept by luck -- two different instances are ordered + * rather than tied, and the answer does not depend on which one is asked first. + */ +public class MethodSortingTest { + + @Test(description = "Two methods carrying different instance ids are strictly ordered") + public void instancesOfTheSameMethodAreOrderedByTheirIdentity() { + ITestNGMethod one = new IdentifiableStub(UUID.randomUUID()); + ITestNGMethod two = new IdentifiableStub(UUID.randomUUID()); + + assertThat(MethodSorting.INSTANCES.compare(one, two)).isNotZero(); + } + + @Test(description = "The order two methods are compared in does not change the answer") + public void theComparisonIsAntisymmetric() { + ITestNGMethod one = new IdentifiableStub(UUID.randomUUID()); + ITestNGMethod two = new IdentifiableStub(UUID.randomUUID()); + + assertThat(MethodSorting.INSTANCES.compare(one, two)) + .isEqualTo(-MethodSorting.INSTANCES.compare(two, one)); + } + + @Test(description = "A method is tied with itself") + public void aMethodIsTiedWithItself() { + ITestNGMethod only = new IdentifiableStub(UUID.randomUUID()); + + assertThat(MethodSorting.INSTANCES.compare(only, only)).isZero(); + } + + /** + * Everything the comparator reads before it reaches the identity is deliberately equal between + * two of these, so that the identity is what decides. + */ + private static class IdentifiableStub extends TestNGMethodStub implements IInstanceIdentity { + + private final UUID instanceId; + + IdentifiableStub(UUID instanceId) { + super("sample", null); + this.instanceId = instanceId; + } + + @Override + public UUID getInstanceId() { + return instanceId; + } + + @Override + public Class getRealClass() { + return MethodSortingTest.class; + } + + @Override + public String toString() { + return "IdentifiableStub"; + } + } +} diff --git a/testng-core/src/test/kotlin/org/testng/dataprovider/sample/issue2724/TestTimeListener.kt b/testng-core/src/test/kotlin/org/testng/dataprovider/sample/issue2724/TestTimeListener.kt index f19db126c1..da9d15425d 100644 --- a/testng-core/src/test/kotlin/org/testng/dataprovider/sample/issue2724/TestTimeListener.kt +++ b/testng-core/src/test/kotlin/org/testng/dataprovider/sample/issue2724/TestTimeListener.kt @@ -6,11 +6,11 @@ import org.testng.ITestListener class TestTimeListener : ITestListener { private var startTime: Long = 0 - override fun onStart(context: ITestContext?) { + override fun onStart(context: ITestContext) { startTime = System.currentTimeMillis() } - override fun onFinish(context: ITestContext?) { + override fun onFinish(context: ITestContext) { testRunTime = System.currentTimeMillis() - startTime } diff --git a/testng-core/src/test/resources/testng.xml b/testng-core/src/test/resources/testng.xml index 83d62e0201..e781299b43 100644 --- a/testng-core/src/test/resources/testng.xml +++ b/testng-core/src/test/resources/testng.xml @@ -82,6 +82,7 @@ + diff --git a/testng-core/testng-core-build.gradle.kts b/testng-core/testng-core-build.gradle.kts index 5c96ebcfa6..b67fde24d1 100644 --- a/testng-core/testng-core-build.gradle.kts +++ b/testng-core/testng-core-build.gradle.kts @@ -21,8 +21,6 @@ tasks.withType().configureEach { dependencies { api(projects.testngCoreApi) - // Annotations have to be available on the compile classpath for the proper compilation - compileOnly("com.github.spotbugs:spotbugs:4.10.3") "guiceApi"(platform("com.google.inject:guice-bom:6.0.0")) "guiceApi"("com.google.inject:guice") diff --git a/testng-runner-api/src/main/java/org/testng/internal/LiteWeightTestNGMethod.java b/testng-runner-api/src/main/java/org/testng/internal/LiteWeightTestNGMethod.java index b4090150ba..284b66db5f 100644 --- a/testng-runner-api/src/main/java/org/testng/internal/LiteWeightTestNGMethod.java +++ b/testng-runner-api/src/main/java/org/testng/internal/LiteWeightTestNGMethod.java @@ -5,6 +5,7 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.Callable; import org.jspecify.annotations.Nullable; @@ -20,19 +21,19 @@ public class LiteWeightTestNGMethod implements ITestNGMethod { private final Class realClass; - private ITestClass testClass; + private @Nullable ITestClass testClass; private final String methodName; - private final Object instance; + private final @Nullable Object instance; private final long[] instanceHashCodes; private final String[] groups; private final String[] groupsDependedUpon; - private String missingGroup; + private @Nullable String missingGroup; private final String[] beforeGroups; private final String[] afterGroups; private final List methodsDependedUpon = new ArrayList<>(); private int priority; private int interceptedPriority; - private final XmlTest xmlTest; + private final @Nullable XmlTest xmlTest; private final String qualifiedName; private final boolean isBeforeTestConfiguration; private final boolean isAfterTestConfiguration; @@ -55,18 +56,18 @@ public class LiteWeightTestNGMethod implements ITestNGMethod { private long timeout; private int invocationCount; private final int successPercentage; - private String id; + private @Nullable String id; private long date; private final boolean isAlwaysRun; private int threadPoolSize; private final boolean enabled; - private String description; + private @Nullable String description; private final int currentInvocationCount; private int parameterInvocationCount; private final boolean hasMoreInvocation; private final Class retryAnalyzerClass; private final String toString; - private final IDataProviderMethod dataProviderMethod; + private final @Nullable IDataProviderMethod dataProviderMethod; private final int hashCode; private final Class[] parameterTypes; @@ -121,41 +122,34 @@ public LiteWeightTestNGMethod(ITestNGMethod iTestNGMethod) { toString = iTestNGMethod.toString(); IDataProviderMethod dp = iTestNGMethod.getDataProviderMethod(); dataProviderMethod = - new IDataProviderMethod() { - @Override - public Object getInstance() { - return dp.getInstance(); - } - - @Override - public Method getMethod() { - throw new UnsupportedOperationException("method() retrieval not supported"); - } - - @Override - public String getName() { - if (dp == null) { - return ""; - } - return dp.getName(); - } - - @Override - public boolean isParallel() { - if (dp == null) { - return false; - } - return dp.isParallel(); - } - - @Override - public List getIndices() { - if (dp == null) { - return new ArrayList<>(); - } - return dp.getIndices(); - } - }; + dp == null + ? null + : new IDataProviderMethod() { + @Override + public @Nullable Object getInstance() { + return dp.getInstance(); + } + + @Override + public Method getMethod() { + throw new UnsupportedOperationException("method() retrieval not supported"); + } + + @Override + public String getName() { + return dp.getName(); + } + + @Override + public boolean isParallel() { + return dp.isParallel(); + } + + @Override + public List getIndices() { + return dp.getIndices(); + } + }; hashCode = iTestNGMethod.hashCode(); parameterTypes = iTestNGMethod.getConstructorOrMethod().getParameterTypes(); } @@ -171,7 +165,7 @@ public Class getRealClass() { } @Override - public ITestClass getTestClass() { + public @Nullable ITestClass getTestClass() { return testClass; } @@ -186,7 +180,7 @@ public String getMethodName() { } @Override - public Object getInstance() { + public @Nullable Object getInstance() { return instance; } @@ -211,12 +205,12 @@ public String[] getGroupsDependedUpon() { } @Override - public String getMissingGroup() { + public @Nullable String getMissingGroup() { return missingGroup; } @Override - public void setMissingGroup(String group) { + public void setMissingGroup(@Nullable String group) { this.missingGroup = group; } @@ -321,7 +315,7 @@ public int getSuccessPercentage() { } @Override - public String getId() { + public @Nullable String getId() { return id; } @@ -342,7 +336,7 @@ public void setDate(long date) { @Override public boolean canRunFromClass(IClass testClass) { - return getTestClass().getRealClass().isAssignableFrom(testClass.getRealClass()); + return Utils.requireTestClassOf(this).getRealClass().isAssignableFrom(testClass.getRealClass()); } @Override @@ -366,12 +360,12 @@ public boolean getEnabled() { } @Override - public String getDescription() { + public @Nullable String getDescription() { return description; } @Override - public void setDescription(String description) { + public void setDescription(@Nullable String description) { this.description = description; } @@ -485,7 +479,7 @@ public void setInterceptedPriority(int priority) { } @Override - public XmlTest getXmlTest() { + public @Nullable XmlTest getXmlTest() { return xmlTest; } @@ -496,7 +490,12 @@ public ConstructorOrMethod getConstructorOrMethod() { @Override public Map findMethodParameters(XmlTest test) { - return XmlTestUtils.findMethodParameters(xmlTest, getTestClass().getName(), getMethodName()); + // This wrapper answers from the it snapshotted, not from the one it is handed. + ITestClass boundClass = getTestClass(); + return XmlTestUtils.findMethodParameters( + Objects.requireNonNull(xmlTest, "a lite weight method snapshots the it belongs to"), + boundClass == null ? null : boundClass.getName(), + getMethodName()); } @Override @@ -505,7 +504,7 @@ public String getQualifiedName() { } @Override - public IDataProviderMethod getDataProviderMethod() { + public @Nullable IDataProviderMethod getDataProviderMethod() { return dataProviderMethod; } diff --git a/testng-runner-api/src/main/java/org/testng/internal/TestResult.java b/testng-runner-api/src/main/java/org/testng/internal/TestResult.java index f93ed7d17f..86f28f96f5 100644 --- a/testng-runner-api/src/main/java/org/testng/internal/TestResult.java +++ b/testng-runner-api/src/main/java/org/testng/internal/TestResult.java @@ -1,5 +1,7 @@ package org.testng.internal; +import static java.util.Objects.requireNonNull; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -13,6 +15,7 @@ import org.testng.IClass; import org.testng.IFactoryInstance; import org.testng.ITest; +import org.testng.ITestClass; import org.testng.ITestContext; import org.testng.ITestNGMethod; import org.testng.ITestResult; @@ -103,7 +106,8 @@ private void init( long start, long end) { m_throwable = t; - m_instanceName = method.getTestClass().getName(); + ITestClass boundClass = Utils.requireTestClassOf(method); + m_instanceName = boundClass.getName(); if (null == m_throwable) { m_status = ITestResult.SUCCESS; } @@ -135,8 +139,9 @@ private void init( } return; } - if (method.getTestClass().getTestName() != null) { - m_name = method.getTestClass().getTestName(); + String boundName = boundClass.getTestName(); + if (boundName != null) { + m_name = boundName; return; } String string = instance.toString(); @@ -170,10 +175,7 @@ public void setEndMillis(long millis) { if (instance instanceof ITest) { return ((ITest) instance).getTestName(); } - if (m_method.getTestClass().getTestName() != null) { - return m_method.getTestClass().getTestName(); - } - return null; + return Utils.requireTestClassOf(m_method).getTestName(); } @Override @@ -193,7 +195,7 @@ public void setEndMillis(long millis) { * builds a carrier that has no method, and those members are not reachable on it. */ private ITestNGMethod requireMethod() { - return java.util.Objects.requireNonNull( + return requireNonNull( m_method, "This TestResult carries parameters only; it has no test method"); } @@ -222,7 +224,7 @@ public boolean isSuccess() { /** @return Returns the testClass. */ @Override public IClass getTestClass() { - return requireMethod().getTestClass(); + return Utils.requireTestClassOf(requireMethod()); } /** @return Returns the throwable. */ @@ -302,7 +304,8 @@ public void setParameters(Object[] parameters) { @Override public @Nullable Object getInstance() { - return IParameterInfo.embeddedInstance(requireMethod().getInstance()); + Object instance = requireMethod().getInstance(); + return instance == null ? null : IParameterInfo.embeddedInstance(instance); } @Override @@ -314,7 +317,7 @@ public Object[] getFactoryParameters() { } @Override - public Object getAttribute(String name) { + public @Nullable Object getAttribute(String name) { return m_attributes.getAttribute(name); } @@ -329,7 +332,7 @@ public Set getAttributeNames() { } @Override - public Object removeAttribute(String name) { + public @Nullable Object removeAttribute(String name) { return m_attributes.removeAttribute(name); } @@ -468,12 +471,12 @@ public String id() { } private static boolean isGlobalFailure(ITestResult result) { - ITestNGMethod m = result.getMethod(); + ITestNGMethod m = Utils.requireMethodOf(result); return m.isBeforeTestConfiguration() || m.isBeforeSuiteConfiguration(); } private boolean isRelated(ITestResult result) { - ITestNGMethod m = result.getMethod(); + ITestNGMethod m = Utils.requireMethodOf(result); if (!m.isBeforeClassConfiguration() && !m.isBeforeMethodConfiguration()) { return false; } @@ -487,7 +490,7 @@ private boolean isRelated(ITestResult result) { } private boolean belongToSameGroup(ITestResult result) { - ITestNGMethod m = result.getMethod(); + ITestNGMethod m = Utils.requireMethodOf(result); if (!m.isBeforeGroupsConfiguration()) { return false; } @@ -503,6 +506,12 @@ private boolean belongToSameGroup(ITestResult result) { public static void copyAttributes(ITestResult source, ITestResult target) { source .getAttributeNames() - .forEach(name -> target.setAttribute(name, source.getAttribute(name))); + .forEach( + name -> { + Object value = source.getAttribute(name); + if (value != null) { + target.setAttribute(name, value); + } + }); } } diff --git a/testng-test-kit/src/main/kotlin/test/SimpleBaseTest.kt b/testng-test-kit/src/main/kotlin/test/SimpleBaseTest.kt index 67e589c4c4..e31b212a84 100644 --- a/testng-test-kit/src/main/kotlin/test/SimpleBaseTest.kt +++ b/testng-test-kit/src/main/kotlin/test/SimpleBaseTest.kt @@ -309,7 +309,7 @@ open class SimpleBaseTest { /** Compare a list of ITestResult with a list of String method names, */ @JvmStatic protected fun assertTestResultsEqual(results: List, methods: List) { - results.map { it.method.methodName } + results.map { it.method!!.methodName } .toList() .run { assertThat(this).containsAll(methods) @@ -373,7 +373,7 @@ open class SimpleBaseTest { val methods = testResultList.stream() .map { r: ITestResult -> AbstractMap.SimpleEntry( - r.method.qualifiedName, r.throwable + r.method!!.qualifiedName, r.throwable ) } .map { (key, value): AbstractMap.SimpleEntry ->