From d2c3153b4258333819e708b288af9ca75c31f4a7 Mon Sep 17 00:00:00 2001 From: Julien Herr Date: Wed, 19 Aug 2026 21:42:37 +0200 Subject: [PATCH 01/14] refactor(testng): state the nullness of the published interfaces Thirty members of the published API answer null, and the implementations that say so were annotated by the batches that marked org.testng.internal and org.testng.internal.invokers. Declaring their nullness here is what lets org.testng be marked in turn: without it the mark reports thirty-one "method returns @Nullable, but superclass returns @NonNull" and no honest answer exists at the implementation, because the null is what it really has. ITestNGMethod getTestClass getInstance getId getDescription getMissingGroup getXmlTest getRetryAnalyzer getDataProviderMethod getFactoryMethodParamsInfo ITestResult getMethod getName getTestName getInstance getInstanceName getHost getThrowable getTestContext IClass getXmlTest getXmlClass getTestName getInstanceHashCodes IAttributes getAttribute removeAttribute IDataProviderMethod getInstance getMethod ITestClassFinder getIClass ITestNGListenerFactory createListener ITestObjectFactory newInstance(Constructor, Object...) Each is binary compatible and source compatible for Java. For Kotlin it is a source break the moment a caller dereferences the result without testing it: SimpleBaseTest, the only Kotlin caller in the tree, needed two !! -- which is also the proof that the Kotlin compiler reads these annotations at all. setMissingGroup and setDescription widen with their getters, so that the two halves of the pair keep agreeing and Kotlin still synthesises a mutable property; ClonedMethod, WrappedTestNGMethod and LiteWeightTestNGMethod follow, the last one widening the fields the setters assign to. TestClass accepts the nullable instance id its ITestClassConfigInfo supertype already declared. The annotations are inert until org.testng carries @NullMarked, so this commit moves no diagnostic on its own; it is the one that decides what the published API promises. --- .../src/main/java/org/testng/IAttributes.java | 5 ++- .../src/main/java/org/testng/IClass.java | 16 +++++++-- .../java/org/testng/IDataProviderMethod.java | 7 +++- .../java/org/testng/ITestClassFinder.java | 6 +++- .../org/testng/ITestNGListenerFactory.java | 3 ++ .../main/java/org/testng/ITestNGMethod.java | 36 +++++++++++++++---- .../java/org/testng/ITestObjectFactory.java | 7 +++- .../src/main/java/org/testng/ITestResult.java | 36 +++++++++++++++---- .../src/main/java/org/testng/TestClass.java | 5 +-- .../org/testng/internal/ClonedMethod.java | 4 +-- .../testng/internal/WrappedTestNGMethod.java | 4 +-- .../internal/LiteWeightTestNGMethod.java | 12 +++---- .../src/main/kotlin/test/SimpleBaseTest.kt | 4 +-- 13 files changed, 112 insertions(+), 33 deletions(-) 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..02e1ea98b0 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. */ @@ -49,7 +59,7 @@ default Object[] getInstances(boolean create, String errorMsgPrefix) { /** @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/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/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/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/src/main/java/org/testng/TestClass.java b/testng-core/src/main/java/org/testng/TestClass.java index 7f2fea456c..8870c48d32 100644 --- a/testng-core/src/main/java/org/testng/TestClass.java +++ b/testng-core/src/main/java/org/testng/TestClass.java @@ -6,6 +6,7 @@ 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; @@ -67,12 +68,12 @@ private static List getAllClassLevelConfigs(Map getInstanceBeforeClassMethods(UUID instanceId) { + public List getInstanceBeforeClassMethods(@Nullable UUID instanceId) { return beforeClassConfig.get(instanceId); } @Override - public List getInstanceAfterClassMethods(UUID instanceId) { + public List getInstanceAfterClassMethods(@Nullable UUID instanceId) { return afterClassConfig.get(instanceId); } 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..26096d3638 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); } @@ -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) {} 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..b1fadd6625 100644 --- a/testng-core/src/main/java/org/testng/internal/WrappedTestNGMethod.java +++ b/testng-core/src/main/java/org/testng/internal/WrappedTestNGMethod.java @@ -85,7 +85,7 @@ public String getMissingGroup() { } @Override - public void setMissingGroup(String group) { + public void setMissingGroup(@Nullable String group) { testNGMethod.setMissingGroup(group); } @@ -240,7 +240,7 @@ public String getDescription() { } @Override - public void setDescription(String description) { + public void setDescription(@Nullable String description) { testNGMethod.setDescription(description); } 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..adc3a9884a 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 @@ -26,7 +26,7 @@ public class LiteWeightTestNGMethod implements ITestNGMethod { 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<>(); @@ -60,7 +60,7 @@ public class LiteWeightTestNGMethod implements ITestNGMethod { 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; @@ -211,12 +211,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; } @@ -366,12 +366,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; } 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 -> From 1c2c56b91d6439c4fe2b01ddf0d8c9bd2bbdaf6a Mon Sep 17 00:00:00 2001 From: Julien Herr Date: Wed, 19 Aug 2026 22:12:37 +0200 Subject: [PATCH 02/14] refactor(testng): resolve the ripple the published contracts open Widening thirty members of org.testng closed the thirty-one override diagnostics the mark reports and opened a hundred and twenty-two downstream, in packages that were marked and green two batches ago. Almost all of them are one sentence: ITestResult.getMethod() and ITestNGMethod.getTestClass() are read without being tested, forty-five times between them. Three assertions in org.testng.internal.Utils carry that answer once instead of forty-five times -- requireMethodOf, requireTestClassOf and requireTestContextOf, each documenting why the absence cannot be observed where it is used. A result that reaches a reporter has been through the invoker, which binds it to its method; the parameter carrier the invoker starts from is replaced before any listener sees it. Every reporter reads through them now, and the thing that used to raise a bare NullPointerException somewhere further in is named at the edge. Six more published members had to widen, each one measured rather than chosen: IMethodInstance.getInstance forced by ITestNGMethod.getInstance IAnnotationTransformer.transform testClass/testConstructor/testMethod, on the two overloads whose own javadoc already said "only one of the three will be non-null" IConfigurationListener tm, on the four listener callbacks; the invoker has declared it nullable since #3393 Reporter.setCurrentTestResult called with null to clear the thread's result TestNGException(String) concatenates, so it never dereferenced JDK15AnnotationFinder asserts on the other side where it can: a @DataProvider is read off a method and a @Listeners off a class by construction, so the finder names that rather than widening two more published signatures. A @Factory is not in that set -- it can sit on a constructor, and the transform has been handed null there all along -- so that one widens too. MethodInstance.SORT_BY_INDEX no longer raises a NullPointerException when a method a @Factory produced carries no tag; it reads the same way the neighbouring branch already reads a missing , and answers that the two cannot be compared. Still inert: org.testng is not marked yet, so this commit moves no diagnostic of its own either. --- .../org/testng/IAnnotationTransformer.java | 17 ++++--- .../org/testng/IConfigurationListener.java | 18 ++++--- .../main/java/org/testng/IMethodInstance.java | 7 +++ .../src/main/java/org/testng/Reporter.java | 7 ++- .../main/java/org/testng/TestNGException.java | 4 +- .../main/java/org/testng/internal/Utils.java | 50 +++++++++++++++++++ .../main/java/org/testng/ClassMethodMap.java | 3 +- .../java/org/testng/ListenerComparator.java | 6 ++- .../org/testng/internal/ClonedMethod.java | 8 +-- .../testng/internal/DynamicGraphHelper.java | 4 +- .../org/testng/internal/MethodInstance.java | 17 ++++--- .../java/org/testng/internal/Parameters.java | 2 +- .../java/org/testng/internal/ResultMap.java | 2 +- .../testng/internal/WrappedTestNGMethod.java | 14 +++--- .../DefaultAnnotationTransformer.java | 5 +- .../internal/annotations/IgnoreListener.java | 5 +- .../annotations/JDK15AnnotationFinder.java | 8 ++- .../testng/internal/invokers/BaseInvoker.java | 3 +- .../invokers/ClassBasedParallelWorker.java | 9 ++-- .../internal/invokers/ConfigInvoker.java | 2 +- .../invokers/ConfigMethodArguments.java | 2 +- .../invokers/GroupConfigMethodArguments.java | 6 ++- .../internal/invokers/ITestInvoker.java | 2 +- .../internal/invokers/InvokedMethod.java | 7 +-- .../org/testng/internal/invokers/Invoker.java | 3 +- .../invokers/MethodInvocationHelper.java | 12 +++-- .../internal/invokers/ParameterHandler.java | 2 +- .../testng/internal/invokers/TestInvoker.java | 21 +++++--- .../invokers/TestMethodArguments.java | 2 +- .../internal/invokers/TestMethodWorker.java | 7 +-- .../internal/invokers/TestNgMethodUtils.java | 8 ++- .../objects/SimpleObjectDispenser.java | 6 ++- .../testng/reporters/EmailableReporter2.java | 14 +++--- .../org/testng/reporters/FailedReporter.java | 14 +++--- .../testng/reporters/JUnitReportReporter.java | 13 ++--- .../testng/reporters/JUnitXMLReporter.java | 5 +- .../testng/reporters/SuiteHTMLReporter.java | 2 +- .../testng/reporters/TestHTMLReporter.java | 12 ++--- .../org/testng/reporters/TextReporter.java | 23 +++++---- .../org/testng/reporters/VerboseReporter.java | 6 +-- .../reporters/XMLSuiteResultWriter.java | 21 ++++---- .../reporters/jq/ChronologicalPanel.java | 8 +-- .../reporters/jq/IgnoredMethodsPanel.java | 3 +- .../java/org/testng/reporters/jq/Model.java | 9 ++-- .../testng/reporters/jq/ResultsByClass.java | 3 +- .../org/testng/reporters/jq/SuitePanel.java | 6 +-- .../org/testng/reporters/jq/TimesPanel.java | 5 +- .../reporters/util/StackTraceTools.java | 3 +- .../internal/LiteWeightTestNGMethod.java | 31 ++++++++---- .../java/org/testng/internal/TestResult.java | 35 ++++++++----- 50 files changed, 315 insertions(+), 167 deletions(-) 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/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/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/Reporter.java b/testng-core-api/src/main/java/org/testng/Reporter.java index 759fd8bb5a..7232c38db6 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); } 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..33ccf4dd17 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,7 +11,7 @@ public TestNGException(Throwable t) { super(t); } - public TestNGException(String string) { + public TestNGException(@Nullable String string) { super("\n" + string); } 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..10ef23ceb7 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 @@ -18,9 +18,13 @@ 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 +450,52 @@ 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}. + */ + /** + * 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}. + */ + /** + * 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"); + } + + public static ITestClass requireTestClassOf(ITestNGMethod method) { + return Objects.requireNonNull(method.getTestClass(), "a scheduled method is bound to a class"); + } + + public static ITestNGMethod requireMethodOf(ITestResult result) { + return Objects.requireNonNull(result.getMethod(), "a reported result carries a test method"); + } + public static String detailedMethodName(ITestNGMethod method, boolean fqn) { String tempName = annotationFormFor(method); if (!tempName.isEmpty()) { diff --git a/testng-core/src/main/java/org/testng/ClassMethodMap.java b/testng-core/src/main/java/org/testng/ClassMethodMap.java index f86bd5c194..c8efc33f43 100644 --- a/testng-core/src/main/java/org/testng/ClassMethodMap.java +++ b/testng-core/src/main/java/org/testng/ClassMethodMap.java @@ -6,6 +6,7 @@ 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; @@ -46,7 +47,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)); 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/internal/ClonedMethod.java b/testng-core/src/main/java/org/testng/internal/ClonedMethod.java index 26096d3638..d27fd72bd2 100644 --- a/testng-core/src/main/java/org/testng/internal/ClonedMethod.java +++ b/testng-core/src/main/java/org/testng/internal/ClonedMethod.java @@ -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(); } @@ -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/MethodInstance.java b/testng-core/src/main/java/org/testng/internal/MethodInstance.java index 6f6dd683d8..da039e4c2f 100644 --- a/testng-core/src/main/java/org/testng/internal/MethodInstance.java +++ b/testng-core/src/main/java/org/testng/internal/MethodInstance.java @@ -23,7 +23,7 @@ public ITestNGMethod getMethod() { } @Override - public Object getInstance() { + public @Nullable Object getInstance() { return m_method.getInstance(); } @@ -40,11 +40,14 @@ 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(); + XmlTest test1 = Utils.requireTestClassOf(o1.getMethod()).getXmlTest(); + XmlTest test2 = Utils.requireTestClassOf(o2.getMethod()).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 +55,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 = Utils.requireTestClassOf(o1.getMethod()).getXmlClass(); + XmlClass class2 = Utils.requireTestClassOf(o2.getMethod()).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/Parameters.java b/testng-core/src/main/java/org/testng/internal/Parameters.java index 8daab4e817..b508ab1227 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); 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 b1fadd6625..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,7 +80,7 @@ public String[] getGroupsDependedUpon() { } @Override - public String getMissingGroup() { + public @Nullable String getMissingGroup() { return testNGMethod.getMissingGroup(); } @@ -190,7 +190,7 @@ public int getSuccessPercentage() { } @Override - public String getId() { + public @Nullable String getId() { return testNGMethod.getId(); } @@ -235,7 +235,7 @@ public boolean getEnabled() { } @Override - public String getDescription() { + public @Nullable String getDescription() { return testNGMethod.getDescription(); } @@ -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/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..f9bfd7fdc1 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(); + XmlTest xmlTest = + Objects.requireNonNull( + im.getMethod().getXmlTest(), "a scheduled method belongs to a "); return im.getMethod().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..252d3f78d7 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 @@ -90,10 +90,14 @@ 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; } } } @@ -309,7 +313,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/TestInvoker.java b/testng-core/src/main/java/org/testng/internal/invokers/TestInvoker.java index fd457d5bbd..887f74d230 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) { @@ -510,7 +511,8 @@ private Set keepSameInstances(ITestNGMethod method, Set { Object instance = - Optional.ofNullable(r.getInstance()).orElse(r.getMethod().getInstance()); + Optional.ofNullable(r.getInstance()) + .orElse(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(); @@ -519,7 +521,9 @@ 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 +671,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 +823,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..bea2b13ef9 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; @@ -152,7 +153,7 @@ && doesTaskHavePreRequisites() if (canInvokeBeforeClassMethods()) { try (KeyAwareAutoCloseableLock.AutoReleasable ignored = lock.lockForObject(key)) { - invokeBeforeClassMethods(testMethod.getTestClass(), testMethodInstance); + invokeBeforeClassMethods(Utils.requireTestClassOf(testMethod), testMethodInstance); } } @@ -161,7 +162,7 @@ && doesTaskHavePreRequisites() invokeTestMethods(testMethod, testMethod.getInstance()); } finally { try (KeyAwareAutoCloseableLock.AutoReleasable ignored = lock.lockForObject(key)) { - invokeAfterClassMethods(testMethod.getTestClass(), testMethodInstance); + invokeAfterClassMethods(Utils.requireTestClassOf(testMethod), testMethodInstance); } } } @@ -171,7 +172,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/SimpleObjectDispenser.java b/testng-core/src/main/java/org/testng/internal/objects/SimpleObjectDispenser.java index ea90fed14b..3be7a9d72c 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 @@ -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/reporters/EmailableReporter2.java b/testng-core/src/main/java/org/testng/reporters/EmailableReporter2.java index 08111d8068..cad8fd98f6 100644 --- a/testng-core/src/main/java/org/testng/reporters/EmailableReporter2.java +++ b/testng-core/src/main/java/org/testng/reporters/EmailableReporter2.java @@ -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; @@ -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); @@ -681,7 +681,7 @@ 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 List failedConfigurationResults; @@ -759,7 +759,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 +776,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"); 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..fb5e937f28 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 = Utils.requireTestClassOf(Utils.requireMethodOf(tr)).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..4bbd1a22cd 100644 --- a/testng-core/src/main/java/org/testng/reporters/JUnitXMLReporter.java +++ b/testng-core/src/main/java/org/testng/reporters/JUnitXMLReporter.java @@ -193,8 +193,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()); } 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..e5212a255d 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); } } 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..ef9565c9b4 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-runner-api/src/main/java/org/testng/internal/LiteWeightTestNGMethod.java b/testng-runner-api/src/main/java/org/testng/internal/LiteWeightTestNGMethod.java index adc3a9884a..883d3c7c71 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,9 +21,9 @@ 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; @@ -32,7 +33,7 @@ public class LiteWeightTestNGMethod implements ITestNGMethod { 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,7 +56,7 @@ 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; @@ -123,7 +124,10 @@ public LiteWeightTestNGMethod(ITestNGMethod iTestNGMethod) { dataProviderMethod = new IDataProviderMethod() { @Override - public Object getInstance() { + public @Nullable Object getInstance() { + if (dp == null) { + return null; + } return dp.getInstance(); } @@ -171,7 +175,7 @@ public Class getRealClass() { } @Override - public ITestClass getTestClass() { + public @Nullable ITestClass getTestClass() { return testClass; } @@ -186,7 +190,7 @@ public String getMethodName() { } @Override - public Object getInstance() { + public @Nullable Object getInstance() { return instance; } @@ -321,7 +325,7 @@ public int getSuccessPercentage() { } @Override - public String getId() { + public @Nullable String getId() { return id; } @@ -342,7 +346,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 @@ -485,7 +489,7 @@ public void setInterceptedPriority(int priority) { } @Override - public XmlTest getXmlTest() { + public @Nullable XmlTest getXmlTest() { return xmlTest; } @@ -496,7 +500,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 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..03efcf74e8 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 @@ -103,7 +103,7 @@ private void init( long start, long end) { m_throwable = t; - m_instanceName = method.getTestClass().getName(); + m_instanceName = Utils.requireTestClassOf(method).getName(); if (null == m_throwable) { m_status = ITestResult.SUCCESS; } @@ -135,8 +135,9 @@ private void init( } return; } - if (method.getTestClass().getTestName() != null) { - m_name = method.getTestClass().getTestName(); + String boundName = Utils.requireTestClassOf(method).getTestName(); + if (boundName != null) { + m_name = boundName; return; } String string = instance.toString(); @@ -170,8 +171,9 @@ public void setEndMillis(long millis) { if (instance instanceof ITest) { return ((ITest) instance).getTestName(); } - if (m_method.getTestClass().getTestName() != null) { - return m_method.getTestClass().getTestName(); + String boundTestName = Utils.requireTestClassOf(m_method).getTestName(); + if (boundTestName != null) { + return boundTestName; } return null; } @@ -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); + } + }); } } From 4fe9276ec637df2b0aa5ac69d13a884e490c9c20 Mon Sep 17 00:00:00 2001 From: Julien Herr Date: Wed, 19 Aug 2026 22:45:59 +0200 Subject: [PATCH 03/14] refactor(testng): state the nullness of the runner state TestNG, TestRunner, TestClass and SuiteRunner hold what the command line, the suite file and the run did not supply: ninety-eight of the diagnostics the mark reports are those four classes reading their own optional state. Nothing here is a new decision -- the fields were already assigned null by their setters, and every read already tested for it or crashed. Three shapes answer them, and which one applies was measured rather than chosen: - a field only some paths bind is @Nullable, and its accessor with it. That is what makes ITestContext.getName, getEndDate and getHost, ISuite.getHost, getParameter, getParentInjector and getObjectFactory, and ISuiteRunnerListener.getExitCodeListener widen: TestRunner reads a name from XmlTest.getName, which the xml batch declared nullable two batches ago, and a host from a suite that was never told about one. - a field every path binds loses its "= null" seed instead of gaining an annotation. TestClass and TestRunner had eight between them; the seed was the only reason NullAway read them as absent, since init() assigns them all. - a value read after the run has dropped it is asserted, not annotated: TestRunner.requireClassMethodMap and requireGroupMethods name what forgetHeavyReferencesIfNeeded released, TestNG.requireExitCode what run() has not produced yet, and TimeBombSkipException.requireExpireDate the date the exception was built without. CommandLineArgs takes the twenty-three in one go. Every one of its fields is tested for absence by TestNG.configure -- Optional.ofNullable for most, an explicit null test for the rest -- so seeding a default at the declaration would make configure take the other branch and apply a setting nobody asked for. The fields that do have a meaningful default already carry it and stay non-null. Two widenings were measured and then withdrawn, because Kotlin priced them: - TestNG.addListener(ITestNGListener) kept its non-null parameter. Widening it made addListener(this) ambiguous against the deprecated addListener(Object) overload, which is a source break for every Kotlin caller. Its null guard stays as residue and the three call sites test before calling instead. - TestNG.setOutputDirectory kept its non-null parameter, because getOutputDirectory answers a default and cannot widen with it. A nullable setter without a nullable getter makes Kotlin synthesise a val rather than a var, and SimpleBaseTest assigns through it twice. Both were caught by :testng-test-kit:compileKotlin, and neither by any Java compile -- which is the whole reason that task is in the guard set. --- .../src/main/java/org/testng/IClass.java | 2 +- .../org/testng/IDataProviderInterceptor.java | 3 +- .../org/testng/IDataProviderListener.java | 13 +- .../main/java/org/testng/IMethodSelector.java | 4 +- .../main/java/org/testng/IModuleFactory.java | 3 +- .../src/main/java/org/testng/ISuite.java | 7 +- .../main/java/org/testng/ITestContext.java | 5 + .../org/testng/internal/ReporterConfig.java | 4 +- .../main/java/org/testng/internal/Utils.java | 13 ++ .../main/java/org/testng/xml/XmlSuite.java | 2 +- .../main/java/org/testng/ClassMethodMap.java | 6 +- .../src/main/java/org/testng/CliRunners.java | 9 +- .../main/java/org/testng/CommandLineArgs.java | 47 +++--- .../java/org/testng/DataProviderHolder.java | 3 +- .../main/java/org/testng/DependencyMap.java | 4 +- .../java/org/testng/ISuiteRunnerListener.java | 3 + .../java/org/testng/ITestNGCliRunner.java | 4 +- .../main/java/org/testng/JarFileUtils.java | 17 ++- .../main/java/org/testng/SkipException.java | 3 +- .../src/main/java/org/testng/SuiteResult.java | 3 +- .../src/main/java/org/testng/SuiteRunner.java | 84 +++++----- .../java/org/testng/SuiteRunnerWorker.java | 9 +- .../java/org/testng/SuiteTaskExecutor.java | 9 +- .../src/main/java/org/testng/TestClass.java | 74 +++++---- .../src/main/java/org/testng/TestNG.java | 144 +++++++++++------- .../src/main/java/org/testng/TestRunner.java | 80 ++++++---- .../java/org/testng/TestTaskExecutor.java | 19 ++- .../org/testng/TimeBombSkipException.java | 12 +- .../java/org/testng/internal/ClassImpl.java | 12 +- .../org/testng/internal/FactoryMethod.java | 7 +- .../java/org/testng/internal/IObject.java | 4 +- .../org/testng/internal/MethodHelper.java | 2 +- .../org/testng/internal/NoOpTestClass.java | 2 +- .../testng/internal/OverrideProcessor.java | 7 +- .../java/org/testng/internal/Parameters.java | 4 +- .../testng/internal/XmlMethodSelector.java | 6 +- .../invokers/MethodInvocationHelper.java | 10 +- .../testng/internal/objects/GuiceHelper.java | 20 ++- .../objects/SimpleObjectDispenser.java | 2 +- .../objects/pojo/DetailedAttributes.java | 7 +- .../testng/reporters/EmailableReporter2.java | 31 ++-- .../testng/reporters/JUnitXMLReporter.java | 6 +- .../testng/reporters/SuiteHTMLReporter.java | 2 +- .../testng/reporters/TestHTMLReporter.java | 2 +- .../org/testng/reporters/TextReporter.java | 4 +- .../reporters/XMLSuiteResultWriter.java | 3 +- .../org/testng/reporters/jq/TimesPanel.java | 2 +- .../java/org/testng/xml/internal/Parser.java | 8 +- 48 files changed, 449 insertions(+), 278 deletions(-) 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 02e1ea98b0..f8833baf31 100644 --- a/testng-core-api/src/main/java/org/testng/IClass.java +++ b/testng-core-api/src/main/java/org/testng/IClass.java @@ -53,7 +53,7 @@ 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); } 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/IMethodSelector.java b/testng-core-api/src/main/java/org/testng/IMethodSelector.java index 16af0633cf..354e07b516 100644 --- a/testng-core-api/src/main/java/org/testng/IMethodSelector.java +++ b/testng-core-api/src/main/java/org/testng/IMethodSelector.java @@ -1,6 +1,7 @@ package org.testng; import java.util.List; +import org.jspecify.annotations.Nullable; /** * This interface is used to augment or replace TestNG's algorithm to decide whether a test method @@ -17,7 +18,8 @@ public interface IMethodSelector { * @param isTestMethod true if this is a @Test method, false if it's a configuration method * @return true if this method should be included in the test run, false otherwise */ - boolean includeMethod(IMethodSelectorContext context, ITestNGMethod method, boolean isTestMethod); + boolean includeMethod( + @Nullable IMethodSelectorContext context, ITestNGMethod method, boolean isTestMethod); /** * Invoked when all the test methods are known so that the method selector can perform additional 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..d6666ce09f 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(); /** @@ -74,8 +78,9 @@ public interface ISuite extends IAttributes { /** @return The representation of the current XML suite file. */ XmlSuite getXmlSuite(); - void addListener(ITestNGListener listener); + void addListener(@Nullable ITestNGListener listener); + @Nullable Injector getParentInjector(); void setParentInjector(Injector injector); 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/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 10ef23ceb7..41934c1567 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 @@ -484,6 +484,19 @@ public static String toString(Object object, Class objectClass) { * @param result The result to read the context of. * @return The test context, never {@code null}. */ + /** + * 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 java.util.Date requireEndDateOf(ITestContext context) { + return Objects.requireNonNull(context.getEndDate(), "a reported test context has finished"); + } + public static ITestContext requireTestContextOf(ITestResult result) { return Objects.requireNonNull(result.getTestContext(), "a reported result carries a context"); } 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/src/main/java/org/testng/ClassMethodMap.java b/testng-core/src/main/java/org/testng/ClassMethodMap.java index c8efc33f43..93c1e9bcfa 100644 --- a/testng-core/src/main/java/org/testng/ClassMethodMap.java +++ b/testng-core/src/main/java/org/testng/ClassMethodMap.java @@ -8,6 +8,7 @@ import java.util.concurrent.ConcurrentLinkedQueue; import org.jspecify.annotations.Nullable; import org.testng.internal.IInstanceIdentity; +import org.testng.internal.Utils; import org.testng.internal.XmlMethodSelector; /** @@ -24,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 @@ -59,7 +61,7 @@ public boolean removeAndCheckIfLast(ITestNGMethod m, @Nullable 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() && Utils.requireTestClassOf(tm).equals(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..9b38ea7a45 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 { @@ -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/ISuiteRunnerListener.java b/testng-core/src/main/java/org/testng/ISuiteRunnerListener.java index 062f7b1c56..994dca21bc 100644 --- a/testng-core/src/main/java/org/testng/ISuiteRunnerListener.java +++ b/testng-core/src/main/java/org/testng/ISuiteRunnerListener.java @@ -1,7 +1,10 @@ package org.testng; +import org.jspecify.annotations.Nullable; + public interface ISuiteRunnerListener { + @Nullable ITestListener getExitCodeListener(); void beforeInvocation(IInvokedMethod method, ITestResult testResult); 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..2d28df58c3 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,26 +26,26 @@ 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; + private final XmlSuite.@Nullable ParallelMode mode; - JarFileUtils(IPostProcessor processor, String xmlPathInJar, List testNames) { + JarFileUtils(IPostProcessor processor, String xmlPathInJar, @Nullable List testNames) { this(processor, xmlPathInJar, testNames, XmlSuite.ParallelMode.NONE); } JarFileUtils( IPostProcessor processor, String xmlPathInJar, - List testNames, - XmlSuite.ParallelMode mode) { + @Nullable List testNames, + XmlSuite.@Nullable ParallelMode mode) { this(processor, xmlPathInJar, testNames, mode, false); } JarFileUtils( IPostProcessor processor, String xmlPathInJar, - List testNames, + @Nullable List testNames, boolean ignoreMissedTestNames) { this(processor, xmlPathInJar, testNames, XmlSuite.ParallelMode.NONE, ignoreMissedTestNames); } @@ -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/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..81ff3482ae 100644 --- a/testng-core/src/main/java/org/testng/SuiteResult.java +++ b/testng-core/src/main/java/org/testng/SuiteResult.java @@ -31,7 +31,8 @@ public int compareTo(@Nonnull SuiteResult other) { try { String n1 = getTestContext().getName(); String n2 = other.getTestContext().getName(); - result = n1.compareTo(n2); + result = + java.util.Objects.compare(n1, n2, java.util.Comparator.nullsFirst(String::compareTo)); } 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..8c0d9e0907 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,27 +39,27 @@ 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 Boolean skipFailedInvocationCounts = Boolean.FALSE; + private @Nullable ITestObjectFactory objectFactory; + private @Nullable Boolean skipFailedInvocationCounts = Boolean.FALSE; private final List reporters = new ArrayList<>(); private final Map, IInvokedMethodListener> @@ -73,7 +74,7 @@ public SuiteRunner( IConfiguration configuration, XmlSuite suite, String outputDir, - ITestRunnerFactory runnerFactory, + @Nullable ITestRunnerFactory runnerFactory, Comparator comparator) { this(configuration, suite, outputDir, runnerFactory, false, comparator); } @@ -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 List methodInterceptors, + @Nullable Collection invokedMethodListener, TestListenersContainer container, - Collection classListeners, + @Nullable Collection classListeners, DataProviderHolder holder, Comparator comparator) { if (comparator == null) { @@ -126,21 +127,26 @@ protected SuiteRunner( if (configuration.getObjectFactory() == null) { configuration.setObjectFactory(new ObjectFactoryImpl()); } + ITestObjectFactory configuredFactory = + Objects.requireNonNull( + configuration.getObjectFactory(), "the configuration carries an object factory"); 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(); + objectFactory = configuredFactory; } // 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( + objectFactory.newInstance(suite.getObjectFactoryClass()), + "the object factory produced a suite level factory"); } else { - suiteObjectFactory = configuration.getObjectFactory(); + suiteObjectFactory = configuredFactory; } objectFactory = new ITestObjectFactory() { @@ -149,7 +155,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 +164,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); } } }; @@ -238,7 +244,7 @@ public void setReportResults(boolean reportResults) { useDefaultListeners = reportResults; } - public ITestListener getExitCodeListener() { + public @Nullable ITestListener getExitCodeListener() { return exitCodeListener; } @@ -276,7 +282,7 @@ private ITestRunnerFactory buildRunnerFactory(Comparator comparat configuration, testListeners.toArray(new ITestListener[0]), useDefaultListeners, - skipFailedInvocationCounts, + skipFailedInvocationCounts != null && skipFailedInvocationCounts, comparator, this); } else { @@ -304,7 +310,7 @@ public String getGuiceStage() { } @Override - public Injector getParentInjector() { + public @Nullable Injector getParentInjector() { return parentInjector; } @@ -473,12 +479,14 @@ public void run() { } /** @param reporter The ISuiteListener interested in reporting the result of the current suite. */ - protected void addListener(ISuiteListener reporter) { - listeners.putIfAbsent(reporter.getClass(), reporter); + protected void addListener(@Nullable ISuiteListener reporter) { + if (reporter != null) { + listeners.putIfAbsent(reporter.getClass(), reporter); + } } @Override - public void addListener(ITestNGListener listener) { + public void addListener(@Nullable ITestNGListener listener) { if (listener instanceof IInvokedMethodListener) { IInvokedMethodListener invokedMethodListener = (IInvokedMethodListener) listener; invokedMethodListeners.put(invokedMethodListener.getClass(), invokedMethodListener); @@ -532,7 +540,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 +573,7 @@ public Collection getExcludedMethods() { } @Override - public ITestObjectFactory getObjectFactory() { + public @Nullable ITestObjectFactory getObjectFactory() { return objectFactory; } @@ -728,7 +736,7 @@ public void setHost(String host) { } @Override - public String getHost() { + public @Nullable String getHost() { return remoteHost; } @@ -745,7 +753,7 @@ public void setSkipFailedInvocationCounts(Boolean skipFailedInvocationCounts) { } @Override - public Object getAttribute(String name) { + public @Nullable Object getAttribute(String name) { return attributes.getAttribute(name); } @@ -760,7 +768,7 @@ public Set getAttributeNames() { } @Override - public Object removeAttribute(String name) { + public @Nullable Object removeAttribute(String name) { return attributes.removeAttribute(name); } @@ -804,8 +812,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 +835,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..b7d6af7e43 100644 --- a/testng-core/src/main/java/org/testng/SuiteRunnerWorker.java +++ b/testng-core/src/main/java/org/testng/SuiteRunnerWorker.java @@ -48,7 +48,10 @@ private void runSuite(SuiteRunnerMap suiteRunnerMap /* OUT */, XmlSuite xmlSuite Utils.log("TestNG", 0, "Running:\n" + allFiles); } - SuiteRunner suiteRunner = (SuiteRunner) suiteRunnerMap.get(xmlSuite); + SuiteRunner suiteRunner = + (SuiteRunner) + java.util.Objects.requireNonNull( + suiteRunnerMap.get(xmlSuite), "every suite has a runner in the map"); suiteRunner.run(); // TODO: this should be handled properly @@ -159,9 +162,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..e3d37d82a7 100644 --- a/testng-core/src/main/java/org/testng/SuiteTaskExecutor.java +++ b/testng-core/src/main/java/org/testng/SuiteTaskExecutor.java @@ -3,6 +3,7 @@ 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 +19,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 +55,10 @@ 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 = + java.util.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 8870c48d32..bc5e5d2960 100644 --- a/testng-core/src/main/java/org/testng/TestClass.java +++ b/testng-core/src/main/java/org/testng/TestClass.java @@ -27,16 +27,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 @@ -69,12 +69,21 @@ private static List getAllClassLevelConfigs(Map getInstanceBeforeClassMethods(@Nullable UUID instanceId) { - return beforeClassConfig.get(instanceId); + List methods = beforeClassConfig.get(instanceId); + return methods == null ? new ArrayList<>() : methods; } @Override public List getInstanceAfterClassMethods(@Nullable UUID instanceId) { - return afterClassConfig.get(instanceId); + List methods = afterClassConfig.get(instanceId); + return methods == null ? new ArrayList<>() : methods; + } + + /** + * The real class this TestClass was built for; {@code init} binds it before anything reads it. + */ + private Class realClass() { + return java.util.Objects.requireNonNull(m_testClass, "a TestClass is bound to its real class"); } private static final Logger LOG = Logger.getLogger(TestClass.class); @@ -85,15 +94,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; } @@ -103,7 +112,7 @@ public XmlTest getXmlTest() { } @Override - public XmlClass getXmlClass() { + public @Nullable XmlClass getXmlClass() { return xmlClass; } @@ -116,7 +125,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(); @@ -153,12 +162,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); } @@ -178,28 +187,28 @@ public void addObject(IObject.IdentifiableObject instance) { } private void initMethods() { - ITestNGMethod[] methods = testMethodFinder.getTestMethods(m_testClass, xmlTest); + 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, @@ -207,7 +216,7 @@ private void initMethods() { m_afterTestConfMethods = ConfigurationMethod.createTestConfigurationMethods( objectFactory, - testMethodFinder.getAfterTestConfigurationMethods(m_testClass), + testMethodFinder.getAfterTestConfigurationMethods(realClass()), annotationFinder, false, this.xmlTest, @@ -215,7 +224,7 @@ private void initMethods() { m_beforeClassMethods = ConfigurationMethod.createClassConfigurationMethods( objectFactory, - testMethodFinder.getBeforeClassMethods(m_testClass), + testMethodFinder.getBeforeClassMethods(realClass()), annotationFinder, true, xmlTest, @@ -224,7 +233,7 @@ private void initMethods() { m_afterClassMethods = ConfigurationMethod.createClassConfigurationMethods( objectFactory, - testMethodFinder.getAfterClassMethods(m_testClass), + testMethodFinder.getAfterClassMethods(realClass()), annotationFinder, false, xmlTest, @@ -233,21 +242,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, @@ -255,7 +264,7 @@ private void initMethods() { m_afterTestMethods.addAll( ConfigurationMethod.createTestMethodConfigurationMethods( objectFactory, - testMethodFinder.getAfterTestMethods(m_testClass), + testMethodFinder.getAfterTestMethods(realClass()), annotationFinder, false, xmlTest, @@ -271,13 +280,14 @@ private ITestNGMethod[] createTestMethods(ITestNGMethod[] methods) { 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()); } } @@ -293,7 +303,7 @@ private void log(int level, String s) { } protected void dump() { - LOG.info("===== Test class\n" + m_testClass.getName()); + LOG.info("===== Test class\n" + realClass().getName()); for (ITestNGMethod m : m_beforeClassMethods) { LOG.info(" @BeforeClass " + m); } @@ -314,7 +324,7 @@ protected void dump() { @Override public String toString() { - return Objects.toStringHelper(getClass()).add("name", m_testClass).toString(); + return Objects.toStringHelper(getClass()).add("name", realClass()).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..9fe2ad704f 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( @@ -535,7 +538,7 @@ private List createCommandLineSuitesForMethods(List commandLin return result; } - private List createCommandLineSuitesForClasses(Class[] classes) { + private List createCommandLineSuitesForClasses(Class @Nullable [] classes) { // // See if any of the classes has an xmlSuite or xmlTest attribute. // If it does, create the appropriate XmlSuite, otherwise, create @@ -543,7 +546,10 @@ private List createCommandLineSuitesForClasses(Class[] classes) { // XmlClass[] xmlClasses = - Arrays.stream(classes).map(clazz -> new XmlClass(clazz, true)).toArray(XmlClass[]::new); + Arrays.stream( + Objects.requireNonNull(classes, "command line suites are built from a class list")) + .map(clazz -> new XmlClass(clazz, true)) + .toArray(XmlClass[]::new); Map suites = new HashMap<>(); IAnnotationFinder finder = m_configuration.getAnnotationFinder(); @@ -568,7 +574,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 +591,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 +682,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 +691,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 +731,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 +748,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 +864,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 +885,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; @@ -974,9 +986,9 @@ private void initializeCommandLineSuitesGroups() { private static void initializeCommandLineSuitesGroups( XmlSuite s, boolean hasIncludedGroups, - String[] m_includedGroups, + String @Nullable [] m_includedGroups, boolean hasExcludedGroups, - String[] m_excludedGroups) { + String @Nullable [] m_excludedGroups) { if (hasIncludedGroups) { s.setIncludedGroups(Arrays.asList(m_includedGroups)); } @@ -1073,7 +1085,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 +1336,12 @@ public List runSuitesLocally() { return new ArrayList<>(suiteRunnerMap.values()); } - private static void error(String s) { + /** Every suite the map is walked over was put in it by {@code createSuiteRunners}. */ + private static ISuite requireRunnerFor(SuiteRunnerMap map, XmlSuite suite) { + return Objects.requireNonNull(map.get(suite), "every suite has a runner in the map"); + } + + private static void error(@Nullable String s) { LOGGER.error(s); } @@ -1352,7 +1371,7 @@ private void runSuitesSequentially( } SuiteRunnerWorker srw = new SuiteRunnerWorker( - suiteRunnerMap.get(xmlSuite), suiteRunnerMap, verbose, defaultSuiteName); + requireRunnerFor(suiteRunnerMap, xmlSuite), suiteRunnerMap, verbose, defaultSuiteName); srw.run(); } @@ -1369,11 +1388,11 @@ private void populateSuiteGraph( IDynamicGraph suiteGraph /* OUT */, SuiteRunnerMap suiteRunnerMap, XmlSuite xmlSuite) { - ISuite parentSuiteRunner = suiteRunnerMap.get(xmlSuite); + ISuite parentSuiteRunner = requireRunnerFor(suiteRunnerMap, xmlSuite); suiteGraph.addNode(parentSuiteRunner); if (!xmlSuite.getChildSuites().isEmpty()) { for (XmlSuite childSuite : xmlSuite.getChildSuites()) { - suiteGraph.addEdge(0, parentSuiteRunner, suiteRunnerMap.get(childSuite)); + suiteGraph.addEdge(0, parentSuiteRunner, requireRunnerFor(suiteRunnerMap, childSuite)); populateSuiteGraph(suiteGraph, suiteRunnerMap, childSuite); } } @@ -1505,7 +1524,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 +1579,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,7 +1594,9 @@ protected void configure(CommandLineArgs cla) { setTestClasses(classes.toArray(new Class[0])); } - setOutputDirectory(cla.outputDirectory); + if (cla.outputDirectory != null) { + setOutputDirectory(cla.outputDirectory); + } if (cla.testNames != null) { setTestNames(Arrays.asList(cla.testNames.split(","))); @@ -1744,7 +1767,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 +1816,14 @@ 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); + String xmlPathInJarValue = (String) cmdLineArgs.get(CommandLineArgs.XML_PATH_IN_JAR); + if (xmlPathInJarValue != null) { + result.xmlPathInJar = xmlPathInJarValue; + } + Boolean mixedValue = (Boolean) cmdLineArgs.get(CommandLineArgs.MIXED); + if (mixedValue != null) { + result.mixed = mixedValue; + } Object tmpValue = cmdLineArgs.get(CommandLineArgs.INCLUDE_ALL_DATA_DRIVEN_TESTS_WHEN_SKIPPING); if (tmpValue != null) { result.includeAllDataDrivenTestsWhenSkipping = Boolean.parseBoolean(tmpValue.toString()); @@ -1892,11 +1921,11 @@ public void configure(Map cmdLineArgs) { } /** @param testNames Only run the specified tests from the suite. */ - public void setTestNames(List testNames) { + public void setTestNames(@Nullable List testNames) { m_testNames = testNames; } - public void setSkipFailedInvocationCounts(Boolean skip) { + public void setSkipFailedInvocationCounts(@Nullable Boolean skip) { m_skipFailedInvocationCounts = skip; } @@ -1911,7 +1940,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 +1957,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 +2008,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 +2079,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 +2088,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 +2098,7 @@ public XmlSuite.FailurePolicy getConfigFailurePolicy() { * @deprecated since 5.1 */ @Deprecated - public static TestNG getDefault() { + public static @Nullable TestNG getDefault() { return m_instance; } @@ -2123,7 +2157,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..2299cb7420 100644 --- a/testng-core/src/main/java/org/testng/TestRunner.java +++ b/testng-core/src/main/java/org/testng/TestRunner.java @@ -23,6 +23,7 @@ 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 +78,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 +100,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 +113,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 +129,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 +143,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 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 +249,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) { @@ -387,8 +390,7 @@ private void initListeners() { // Instantiate all the listeners for (Class c : listenerClasses) { - ITestNGListener listener = factory.createListener(c); - addListener(listener); + addListener(factory.createListener(c)); } } @@ -619,6 +621,16 @@ public void run() { } } + /** Both are dropped by {@link #forgetHeavyReferencesIfNeeded()} once the run is over. */ + private ClassMethodMap requireClassMethodMap() { + return java.util.Objects.requireNonNull(m_classMethodMap, "the run still holds its method map"); + } + + private ConfigurationGroupMethods requireGroupMethods() { + return java.util.Objects.requireNonNull( + m_groupMethods, "the run still holds its group methods"); + } + private void forgetHeavyReferencesIfNeeded() { if (RuntimeBehavior.isMemoryFriendlyMode()) { testMethodsContainer.clearItems(); @@ -657,7 +669,7 @@ private void invokeTestConfigurations(ITestNGMethod[] testConfigurationMethods) } } - private static Comparator newComparator(boolean needPrioritySort) { + private static @Nullable Comparator newComparator(boolean needPrioritySort) { return needPrioritySort ? new TestMethodComparator() : null; } @@ -685,7 +697,8 @@ private void privateRun(XmlTest xmlTest) { DynamicGraphHelper.createDynamicGraph(interceptedOrder, getCurrentXmlTest()); reference.set(ref); }); - IDynamicGraph graph = reference.get(); + IDynamicGraph graph = + Objects.requireNonNull(reference.get(), "the run computed its dependency graph"); for (ITestNGMethod each : interceptedOrder) { if (each instanceof BaseTestMethod) { @@ -773,11 +786,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 +817,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 +881,7 @@ private void fireEvent(boolean isStart) { ListenerOrderDeterminer.order(m_testListeners, m_configuration.getListenerComparator())) { itl.onStart(this); } - this.exitCodeListener.onStart(this); + requireExitCodeListener().onStart(this); } else { List testListenersReversed = @@ -876,7 +890,7 @@ private void fireEvent(boolean isStart) { for (ITestListener itl : testListenersReversed) { itl.onFinish(this); } - this.exitCodeListener.onFinish(this); + requireExitCodeListener().onFinish(this); } if (!isStart) { MethodHelper.clear(methods(this.getPassedConfigurations())); @@ -898,7 +912,7 @@ private static Stream methods(Stream methods) { // ITestContext // @Override - public String getName() { + public @Nullable String getName() { return m_testName; } @@ -910,7 +924,7 @@ public Date getStartDate() { /** @return Returns the endDate. */ @Override - public Date getEndDate() { + public @Nullable Date getEndDate() { return m_endDate; } @@ -963,7 +977,7 @@ public ITestNGMethod[] getAllTestMethods() { } @Override - public String getHost() { + public @Nullable String getHost() { return m_host; } @@ -1088,7 +1102,7 @@ void addTestListener(ITestListener listener) { } } - public void addListener(ITestNGListener listener) { + public void addListener(@Nullable ITestNGListener listener) { if (listener instanceof IMethodInterceptor) { m_methodInterceptors.add((IMethodInterceptor) listener); } @@ -1139,7 +1153,11 @@ void addConfigurationListener(IConfigurationListener icl) { } } - private void setExitCodeListener(ITestListener exitCodeListener) { + private ITestListener requireExitCodeListener() { + return Objects.requireNonNull(exitCodeListener, "ExitCodeListener cannot be null."); + } + + private void setExitCodeListener(@Nullable ITestListener exitCodeListener) { this.exitCodeListener = exitCodeListener; } @@ -1184,7 +1202,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 -> java.util.Objects.equals(tr.getMethod(), itr.getMethod())); } } @@ -1204,7 +1224,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 +1239,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..e722f6c7a3 100644 --- a/testng-core/src/main/java/org/testng/TestTaskExecutor.java +++ b/testng-core/src/main/java/org/testng/TestTaskExecutor.java @@ -5,6 +5,7 @@ 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 +18,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 +37,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 +92,14 @@ 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 = + java.util.Objects.requireNonNull(orchestrator, "execute() has started the graph") + .awaitCompletion(timeOut, TimeUnit.MILLISECONDS); } else { - boolean ignored = service.awaitTermination(timeOut, TimeUnit.MILLISECONDS); - service.shutdownNow(); + ExecutorService running = + java.util.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..5daff0bc18 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 @Nullable Calendar m_expireDate; private DateFormat m_inFormat = sdf; private DateFormat m_outFormat = sdf; @@ -193,6 +194,11 @@ private void initExpireDate(String date) { } } + /** The date this exception stops skipping; absent when it was built without one. */ + private Calendar requireExpireDate() { + return java.util.Objects.requireNonNull(m_expireDate, "the exception carries an expiry date"); + } + @Override public boolean isSkip() { if (null == m_expireDate) { @@ -211,13 +217,13 @@ public boolean isSkip() { } @Override - public String getMessage() { + public @Nullable String getMessage() { if (isSkip()) { return super.getMessage(); } else { return super.getMessage() + "; Test must have been enabled by: " - + m_outFormat.format(m_expireDate.getTime()); + + m_outFormat.format(requireExpireDate().getTime()); } } 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..4ed4f90a65 100644 --- a/testng-core/src/main/java/org/testng/internal/ClassImpl.java +++ b/testng-core/src/main/java/org/testng/internal/ClassImpl.java @@ -94,7 +94,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 +103,9 @@ public XmlTest getXmlTest() { if (factory instanceof DefaultTestObjectFactory) { factory = m_testContext.getSuite().getObjectFactory(); } - IObjectDispenser dispenser = Dispenser.newInstance(factory); + IObjectDispenser dispenser = + Dispenser.newInstance( + java.util.Objects.requireNonNull(factory, "a 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 +124,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 +136,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()) { @@ -177,7 +179,7 @@ private static int computeHashCode(IdentifiableObject identifiable) { .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/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/IObject.java b/testng-core/src/main/java/org/testng/internal/IObject.java index 2091a5ffd4..a3fa37b212 100644 --- a/testng-core/src/main/java/org/testng/internal/IObject.java +++ b/testng-core/src/main/java/org/testng/internal/IObject.java @@ -20,7 +20,7 @@ public interface IObject { * issues. Can be empty. * @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(); @@ -55,7 +55,7 @@ static IdentifiableObject[] objects(@Nullable Object object, boolean create) { * 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..18f179cc1c 100644 --- a/testng-core/src/main/java/org/testng/internal/MethodHelper.java +++ b/testng-core/src/main/java/org/testng/internal/MethodHelper.java @@ -513,7 +513,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/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 b508ab1227..352c67ce56 100644 --- a/testng-core/src/main/java/org/testng/internal/Parameters.java +++ b/testng-core/src/main/java/org/testng/internal/Parameters.java @@ -839,7 +839,9 @@ 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(), fedInstance, 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/invokers/MethodInvocationHelper.java b/testng-core/src/main/java/org/testng/internal/invokers/MethodInvocationHelper.java index 252d3f78d7..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) { @@ -102,12 +102,14 @@ protected static void invokeMethodConsideringTimeout( } } - 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); @@ -169,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, 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..d0ae63e38e 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 @@ -39,7 +39,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 = @@ -60,12 +60,12 @@ class GuiceHelper { } @Nullable - Injector getInjector(IClass iClass, IInjectorFactory injectorFactory) { + Injector getInjector(IClass iClass, @Nullable IInjectorFactory injectorFactory) { return getInjector(iClass.getRealClass(), 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 +85,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 +171,14 @@ private List getGuiceModules(Class cls) { return (Class) parentModule; } + private static IInjectorFactory requireInjectorFactory(@Nullable IInjectorFactory factory) { + return java.util.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 +189,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 3be7a9d72c..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 { 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/reporters/EmailableReporter2.java b/testng-core/src/main/java/org/testng/reporters/EmailableReporter2.java index cad8fd98f6..be0b29b534 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"); @@ -188,8 +188,8 @@ protected void writeSuiteSummary() { writeTableData(integerFormat.format(retriedTests), retriedTests > 0 ? "num attn" : "num"); writeTableData(integerFormat.format(failedTests), failedTests > 0 ? "num attn" : "num"); writeTableData(decimalFormat.format(duration), "num"); - writeTableData(testResult.getIncludedGroups()); - writeTableData(testResult.getExcludedGroups()); + writeTableData(orEmpty(testResult.getIncludedGroups())); + writeTableData(orEmpty(testResult.getExcludedGroups())); writer().println(""); @@ -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 += @@ -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 += @@ -649,6 +649,15 @@ protected void writeTag(String tag, String html, @Nullable String cssClasses) { } /** Groups {@link TestResult}s by suite. */ + /** A <test> that carries no name renders as an empty cell rather than the text "null". */ + private static String orEmpty(@Nullable String text) { + return text == null ? "" : text; + } + + private static String escapeHtmlOrEmpty(@Nullable String text) { + return text == null ? "" : Utils.escapeHtml(text); + } + protected static class SuiteResult { private final String suiteName; private final List testResults = new ArrayList<>(); @@ -683,7 +692,7 @@ protected static class TestResult { Comparator.comparing((ITestResult o) -> o.getTestClass().getName()) .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; @@ -695,7 +704,7 @@ protected static class TestResult { private final int skippedTestCount; private final int passedTestCount; private final long duration; - private final String includedGroups; + private final @Nullable String includedGroups; private final String excludedGroups; public TestResult(ITestContext context) { @@ -722,7 +731,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()); @@ -800,7 +809,7 @@ protected List groupResults(Set results) { return classResults; } - public String getTestName() { + public @Nullable String getTestName() { return testName; } @@ -853,11 +862,11 @@ public long getDuration() { return duration; } - public String getIncludedGroups() { + public @Nullable String getIncludedGroups() { return includedGroups; } - public String getExcludedGroups() { + public @Nullable String getExcludedGroups() { return excludedGroups; } 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 4bbd1a22cd..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()); @@ -281,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 e5212a255d..00b5b2af8c 100644 --- a/testng-core/src/main/java/org/testng/reporters/SuiteHTMLReporter.java +++ b/testng-core/src/main/java/org/testng/reporters/SuiteHTMLReporter.java @@ -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 ef9565c9b4..37456c1af9 100644 --- a/testng-core/src/main/java/org/testng/reporters/TestHTMLReporter.java +++ b/testng-core/src/main/java/org/testng/reporters/TestHTMLReporter.java @@ -332,7 +332,7 @@ public static void generateLog( .append("\n"); Date startDate = testContext.getStartDate(); - Date endDate = testContext.getEndDate(); + Date endDate = Utils.requireEndDateOf(testContext); long duration = (endDate.getTime() - startDate.getTime()) / 1000; int passed = testContext.getPassedTests().size() diff --git a/testng-core/src/main/java/org/testng/reporters/TextReporter.java b/testng-core/src/main/java/org/testng/reporters/TextReporter.java index fc0a6c50c3..beddf96c39 100644 --- a/testng-core/src/main/java/org/testng/reporters/TextReporter.java +++ b/testng-core/src/main/java/org/testng/reporters/TextReporter.java @@ -24,9 +24,9 @@ public class TextReporter implements ITestListener { private static final String LINE = "\n===============================================\n"; private final int m_verbose; - private final String m_testName; + private final @Nullable String m_testName; - public TextReporter(String testName, int verbose) { + public TextReporter(@Nullable String testName, int verbose) { m_testName = testName; m_verbose = verbose; } diff --git a/testng-core/src/main/java/org/testng/reporters/XMLSuiteResultWriter.java b/testng-core/src/main/java/org/testng/reporters/XMLSuiteResultWriter.java index 474e5c932a..2588bc1514 100644 --- a/testng-core/src/main/java/org/testng/reporters/XMLSuiteResultWriter.java +++ b/testng-core/src/main/java/org/testng/reporters/XMLSuiteResultWriter.java @@ -103,7 +103,8 @@ private Properties getSuiteResultAttributes(ISuiteResult suiteResult) { Properties attributes = new Properties(); ITestContext tc = suiteResult.getTestContext(); attributes.setProperty(XMLReporterConfig.ATTR_NAME, tc.getName()); - XMLReporter.setDurationAttributes(config, attributes, tc.getStartDate(), tc.getEndDate()); + XMLReporter.setDurationAttributes( + config, attributes, tc.getStartDate(), Utils.requireEndDateOf(tc)); return attributes; } diff --git a/testng-core/src/main/java/org/testng/reporters/jq/TimesPanel.java b/testng-core/src/main/java/org/testng/reporters/jq/TimesPanel.java index 15e6151b8e..7ef98abd28 100644 --- a/testng-core/src/main/java/org/testng/reporters/jq/TimesPanel.java +++ b/testng-core/src/main/java/org/testng/reporters/jq/TimesPanel.java @@ -158,6 +158,6 @@ private long maxTime(ISuite suite) { } private static Long time(ITestContext ctx) { - return ctx.getEndDate().getTime() - ctx.getStartDate().getTime(); + return Utils.requireEndDateOf(ctx).getTime() - ctx.getStartDate().getTime(); } } 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..968cb3ec6d 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,8 +235,8 @@ public List parseToList() throws IOException { return new ArrayList<>(parse()); } - public static Collection parse(String suite, IPostProcessor processor) - throws IOException { + public static Collection parse( + @Nullable String suite, @Nullable 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; From 31732b72c07fc08db3c559e158177efc6747e113 Mon Sep 17 00:00:00 2001 From: Julien Herr Date: Wed, 19 Aug 2026 22:46:51 +0200 Subject: [PATCH 04/14] refactor(testng): declare the package null-marked Fifty-four files in testng-core-api and twenty-five in testng-core: org.testng is the last package of the published API to carry the mark, and the first whose nullness is a promise to callers rather than a note to ourselves. The package-info goes in testng-core-api, which testng-core depends on, and the per-module control confirms both halves are covered -- a throwaway null-returning method is clean in both modules before the file and fails after it, at SuiteRunState.java:22 and, separately, at TestNGUtils.java:21. One hundred and ninety-nine diagnostics, zero javac errors; the three commits before this one answer every one of them, so the mark lands on a tree that is already green under it. TestTimeListener is the only Kotlin file the mark breaks, and it breaks before any of the 199 are visible: it declares onStart(context: ITestContext?), which stops overriding ITestListener.onStart once the parameter is non-null. The listener keeps its non-null parameter and the test file loses its question mark. TestRunner:868,870,877,879 and SuiteRunner:239,246 are the only callers and all pass `this`, so a nullable parameter there would be a lie no caller tests -- and widening it would push the question mark onto every Kotlin listener instead. --- testng-core-api/src/main/java/org/testng/package-info.java | 5 +++++ .../testng/dataprovider/sample/issue2724/TestTimeListener.kt | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 testng-core-api/src/main/java/org/testng/package-info.java 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/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 } From 6c5195946915540c5ac94f0cce979349b56dc207 Mon Sep 17 00:00:00 2001 From: Julien Herr Date: Wed, 19 Aug 2026 22:53:11 +0200 Subject: [PATCH 05/14] refactor(internal): key the instance groups on a shared token IInstanceIdentity.getInstanceId(Object) answered null for a method that is identity aware but carries no instance, and that null became a map key. Six sites then had to decide what an absent key meant -- two in DependencyMap, two in MethodHelper, one in ClassMethodMap, one in TestMethodWorker, the last one wrapping it in requireNonNull to get a non-null key back out. A shared NO_INSTANCE token keeps the key present and the grouping identical: every method without an instance still lands in one bucket, which is what a null key did. The helper's contract stops being "or null" and the six sites test for the token instead. MultiMap keeps its @Nullable K. Narrowing it was the point of the token, but it does not hold: DynamicGraphHelper keys on a class that may be absent and JUnitReportReporter keys on ITestResult.getInstance() at three sites, none of which are instance ids. Those four are what @Nullable K is now for, and they are a separate decision. --- .../main/java/org/testng/DependencyMap.java | 12 +++--- .../testng/internal/IInstanceIdentity.java | 40 ++++++++++++++----- .../org/testng/internal/MethodHelper.java | 2 +- .../internal/invokers/TestMethodWorker.java | 2 +- 4 files changed, 38 insertions(+), 18 deletions(-) diff --git a/testng-core/src/main/java/org/testng/DependencyMap.java b/testng-core/src/main/java/org/testng/DependencyMap.java index 9b38ea7a45..4e8d540e86 100644 --- a/testng-core/src/main/java/org/testng/DependencyMap.java +++ b/testng-core/src/main/java/org/testng/DependencyMap.java @@ -110,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.getInstanceId(derivedClassMethod) != IInstanceIdentity.NO_INSTANCE + || IInstanceIdentity.getInstanceId(baseClassMethod) != IInstanceIdentity.NO_INSTANCE; boolean params = baseClassMethod.getFactoryInstance().isPresent(); if (result && params && RuntimeBehavior.enforceThreadAffinity()) { @@ -139,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.getInstanceId(derivedClassMethod) != IInstanceIdentity.NO_INSTANCE + && IInstanceIdentity.getInstanceId(baseClassMethod) != IInstanceIdentity.NO_INSTANCE; + if (!bothCarryAnInstance) { return false; } Class baseClass = instanceClassOf(baseClassMethod); 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..9d38ab9edb 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 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. + */ /** * @param objects - The objects to inspect * @return - true if all the objects passed are of type {@link IInstanceIdentity} */ static boolean isIdentityAware(Object... objects) { - return Arrays.stream(Objects.requireNonNull(objects)) + return java.util.Arrays.stream(java.util.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/MethodHelper.java b/testng-core/src/main/java/org/testng/internal/MethodHelper.java index 18f179cc1c..dc8c78b79f 100644 --- a/testng-core/src/main/java/org/testng/internal/MethodHelper.java +++ b/testng-core/src/main/java/org/testng/internal/MethodHelper.java @@ -422,7 +422,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(m -> IInstanceIdentity.getInstanceId(m) != IInstanceIdentity.NO_INSTANCE) .collect(Collectors.groupingBy(IInstanceIdentity::getInstanceId, Collectors.toList())); } 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 bea2b13ef9..4cda9e9f7d 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 @@ -133,7 +133,7 @@ && doesTaskHavePreRequisites() for (IMethodInstance testMethodInstance : m_methodInstances) { ITestNGMethod testMethod = testMethodInstance.getMethod(); - Object key = Objects.requireNonNull(IInstanceIdentity.getInstanceId(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 From 4330fded55052e362226f5eebeae0c77604d33ad Mon Sep 17 00:00:00 2001 From: Julien Herr Date: Wed, 19 Aug 2026 22:51:01 +0200 Subject: [PATCH 06/14] refactor(internal)!: order instances by their identity again MethodSorting.INSTANCES ends its comparator chain on the per-instance id, so that two invocations of the same method on different @Factory instances are ordered rather than tied. That branch has never run. IInstanceIdentity.getInstanceId(Object) answers the object's UUID when it is identity aware and the object itself otherwise, and objectEquality then asked isIdentityAware about those *results*: a UUID is not an IInstanceIdentity, and a method that is one has already been replaced by its UUID by the time the test runs. Unreachable in both directions, so every pair fell through to the hash code comparison below it. The test now reads what came back. This changes the order of MethodSorting.INSTANCES, which is the default. Both orders are arbitrary -- the ids are random UUIDs -- so the change is not one a test can distinguish, and MethodSortingTest pins what it does establish: the identity branch orders two different instances by construction, where the hash comparison only did so as long as two random UUIDs did not collide. --- .../testng/internal/IInstanceIdentity.java | 9 --- .../org/testng/internal/MethodSorting.java | 6 +- .../testng/internal/MethodSortingTest.java | 74 +++++++++++++++++++ testng-core/src/test/resources/testng.xml | 1 + 4 files changed, 80 insertions(+), 10 deletions(-) create mode 100644 testng-core/src/test/java/org/testng/internal/MethodSortingTest.java 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 9d38ab9edb..d8487cd52a 100644 --- a/testng-core/src/main/java/org/testng/internal/IInstanceIdentity.java +++ b/testng-core/src/main/java/org/testng/internal/IInstanceIdentity.java @@ -33,15 +33,6 @@ public String toString() { * @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. */ - /** - * @param objects - The objects to inspect - * @return - true if all the objects passed are of type {@link IInstanceIdentity} - */ - static boolean isIdentityAware(Object... objects) { - return java.util.Arrays.stream(java.util.Objects.requireNonNull(objects)) - .allMatch(it -> it instanceof IInstanceIdentity); - } - static Object getInstanceId(Object object) { if (object instanceof IInstanceIdentity) { UUID instanceId = ((IInstanceIdentity) object).getInstanceId(); 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/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/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 @@ + From efb931f350e2ec1e6da64dc3191d9be33e87997b Mon Sep 17 00:00:00 2001 From: Julien Herr Date: Wed, 19 Aug 2026 22:55:34 +0200 Subject: [PATCH 07/14] build: drop the last javax.annotation uses Four were left in the tree, all four inside this batch's blast radius. SuiteResult and SuiteRunnerWorker carried @Nonnull on a compareTo parameter, which says nothing a marked package does not already say. IInjectorFactory and GuiceBackedInjectorFactory carried @Nullable on getInjector's parent injector, which does carry meaning and moves to org.jspecify.annotations. That empties both compileOnly("com.github.spotbugs:spotbugs") declarations, so they go with them. testng-test-osgi resolves jsr305 through versionAsInProject(), which is the reason to check rather than assume: :testng-test-osgi:test passes 4 of 4 before and after the removal. testng-cli is pulled in as well. TestNGException(String, Throwable) wraps another exception, whose getMessage() is allowed to be absent -- which is exactly what CliParseException documents one level down -- and CliConfigurer tests the output directory before handing it over, the way TestNG.configure already does. --- testng-cli/src/main/java/org/testng/cli/CliConfigurer.java | 4 +++- .../src/main/java/org/testng/IInjectorFactory.java | 2 +- testng-core-api/src/main/java/org/testng/TestNGException.java | 2 +- testng-core-api/testng-core-api-build.gradle.kts | 1 - testng-core/src/main/java/org/testng/SuiteResult.java | 3 +-- testng-core/src/main/java/org/testng/SuiteRunnerWorker.java | 3 +-- .../testng/internal/objects/GuiceBackedInjectorFactory.java | 2 +- testng-core/testng-core-build.gradle.kts | 1 - 8 files changed, 8 insertions(+), 10 deletions(-) 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/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/TestNGException.java b/testng-core-api/src/main/java/org/testng/TestNGException.java index 33ccf4dd17..7a2676ff82 100644 --- a/testng-core-api/src/main/java/org/testng/TestNGException.java +++ b/testng-core-api/src/main/java/org/testng/TestNGException.java @@ -15,7 +15,7 @@ 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/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/SuiteResult.java b/testng-core/src/main/java/org/testng/SuiteResult.java index 81ff3482ae..42d80cc796 100644 --- a/testng-core/src/main/java/org/testng/SuiteResult.java +++ b/testng-core/src/main/java/org/testng/SuiteResult.java @@ -1,6 +1,5 @@ package org.testng; -import javax.annotation.Nonnull; import org.testng.collections.Objects; import org.testng.log4testng.Logger; import org.testng.xml.XmlSuite; @@ -26,7 +25,7 @@ public XmlSuite getSuite() { } @Override - public int compareTo(@Nonnull SuiteResult other) { + public int compareTo(SuiteResult other) { int result = 0; try { String n1 = getTestContext().getName(); diff --git a/testng-core/src/main/java/org/testng/SuiteRunnerWorker.java b/testng-core/src/main/java/org/testng/SuiteRunnerWorker.java index b7d6af7e43..03ec653ef5 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; @@ -99,7 +98,7 @@ public void run() { } @Override - public int compareTo(@Nonnull IWorker arg0) { + public int compareTo(IWorker arg0) { /* * Dummy Implementation * 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/testng-core-build.gradle.kts b/testng-core/testng-core-build.gradle.kts index 5c96ebcfa6..7c3856c39f 100644 --- a/testng-core/testng-core-build.gradle.kts +++ b/testng-core/testng-core-build.gradle.kts @@ -22,7 +22,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") From 7b1dc1326526734f1920710daecee071e64adb6d Mon Sep 17 00:00:00 2001 From: Julien Herr Date: Thu, 20 Aug 2026 00:10:56 +0200 Subject: [PATCH 08/14] refactor(testng): drop the annotations the classification pass did not justify Removing each of the 301 @Nullable this batch adds, one at a time, and recompiling every module says the checker demands 284 of them. Of the seventeen it does not, eleven were residue this batch introduced and are gone: - SuiteRunner.addListener(ISuiteListener) and its null guard. The guard did not exist before; widening the parameter is what created the dereference it answers, and nothing passes null. - SuiteRunner.skipFailedInvocationCounts, and the "!= null &&" the widening made necessary at the one place it is read. - the two delegating SuiteRunner constructors, the three delegating JarFileUtils constructors and its parallel mode field, GuiceHelper.getInjector(IClass, ..) and Parser.parse's post processor -- every one an overload whose terminal form carries the annotation the checker actually asked for. - TestNG.setTestNames, which writes a nullable field but is never handed null. Six stay, and each is a contract rather than a checker demand: - IMethodSelector.includeMethod's context. ClassMethodMap calls it with null, so a user implementation is handed null; NullAway only checks the override for narrowing, so it never asks. - ITestResult.setTestName, paired with the nullable getTestName. Kotlin synthesises a mutable property only when both halves agree. - SuiteRunner.objectFactory and getExitCodeListener, paired with ISuite.getObjectFactory and ISuiteRunnerListener.getExitCodeListener, which the pass does demand. - EmailableReporter2's includedGroups field and getExcludedGroups, the twins of a getter and a field it demands, filled by the same call. Of the 284 it demands, 189 report at the declaring file and 95 at a call site -- a widened parameter is answered by whoever passes to it, which is a precise verdict rather than a cascade. --- CHANGES.txt | 35 +++++++++++++++++++ .../main/java/org/testng/JarFileUtils.java | 8 ++--- .../src/main/java/org/testng/SuiteRunner.java | 14 ++++---- .../src/main/java/org/testng/TestNG.java | 2 +- .../testng/internal/objects/GuiceHelper.java | 2 +- .../java/org/testng/xml/internal/Parser.java | 4 +-- 6 files changed, 49 insertions(+), 16 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 4ab9f6eb1c..3a48254368 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -15,10 +15,45 @@ 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-six 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-six 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: 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-six members of it widen to @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, getInjectorFactory and getParameter; 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, 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-core/src/main/java/org/testng/JarFileUtils.java b/testng-core/src/main/java/org/testng/JarFileUtils.java index 2d28df58c3..8c6b99e73a 100644 --- a/testng-core/src/main/java/org/testng/JarFileUtils.java +++ b/testng-core/src/main/java/org/testng/JarFileUtils.java @@ -28,16 +28,16 @@ class JarFileUtils { private final boolean ignoreMissedTestNames; private final @Nullable List testNames; private final List suites = new LinkedList<>(); - private final XmlSuite.@Nullable ParallelMode mode; + private final XmlSuite.ParallelMode mode; - JarFileUtils(IPostProcessor processor, String xmlPathInJar, @Nullable List testNames) { + JarFileUtils(IPostProcessor processor, String xmlPathInJar, List testNames) { this(processor, xmlPathInJar, testNames, XmlSuite.ParallelMode.NONE); } JarFileUtils( IPostProcessor processor, String xmlPathInJar, - @Nullable List testNames, + List testNames, XmlSuite.@Nullable ParallelMode mode) { this(processor, xmlPathInJar, testNames, mode, false); } @@ -45,7 +45,7 @@ class JarFileUtils { JarFileUtils( IPostProcessor processor, String xmlPathInJar, - @Nullable List testNames, + List testNames, boolean ignoreMissedTestNames) { this(processor, xmlPathInJar, testNames, XmlSuite.ParallelMode.NONE, ignoreMissedTestNames); } diff --git a/testng-core/src/main/java/org/testng/SuiteRunner.java b/testng-core/src/main/java/org/testng/SuiteRunner.java index 8c0d9e0907..0354d2ff3b 100644 --- a/testng-core/src/main/java/org/testng/SuiteRunner.java +++ b/testng-core/src/main/java/org/testng/SuiteRunner.java @@ -59,7 +59,7 @@ public class SuiteRunner implements ISuite, ISuiteRunnerListener { private final IConfiguration configuration; private @Nullable ITestObjectFactory objectFactory; - private @Nullable Boolean skipFailedInvocationCounts = Boolean.FALSE; + private Boolean skipFailedInvocationCounts = Boolean.FALSE; private final List reporters = new ArrayList<>(); private final Map, IInvokedMethodListener> @@ -74,7 +74,7 @@ public SuiteRunner( IConfiguration configuration, XmlSuite suite, String outputDir, - @Nullable ITestRunnerFactory runnerFactory, + ITestRunnerFactory runnerFactory, Comparator comparator) { this(configuration, suite, outputDir, runnerFactory, false, comparator); } @@ -106,7 +106,7 @@ protected SuiteRunner( String outputDir, @Nullable ITestRunnerFactory runnerFactory, boolean useDefaultListeners, - @Nullable List methodInterceptors, + List methodInterceptors, @Nullable Collection invokedMethodListener, TestListenersContainer container, @Nullable Collection classListeners, @@ -282,7 +282,7 @@ private ITestRunnerFactory buildRunnerFactory(Comparator comparat configuration, testListeners.toArray(new ITestListener[0]), useDefaultListeners, - skipFailedInvocationCounts != null && skipFailedInvocationCounts, + skipFailedInvocationCounts, comparator, this); } else { @@ -479,10 +479,8 @@ public void run() { } /** @param reporter The ISuiteListener interested in reporting the result of the current suite. */ - protected void addListener(@Nullable ISuiteListener reporter) { - if (reporter != null) { - listeners.putIfAbsent(reporter.getClass(), reporter); - } + protected void addListener(ISuiteListener reporter) { + listeners.putIfAbsent(reporter.getClass(), reporter); } @Override diff --git a/testng-core/src/main/java/org/testng/TestNG.java b/testng-core/src/main/java/org/testng/TestNG.java index 9fe2ad704f..0af9015ab1 100644 --- a/testng-core/src/main/java/org/testng/TestNG.java +++ b/testng-core/src/main/java/org/testng/TestNG.java @@ -1921,7 +1921,7 @@ public void configure(Map cmdLineArgs) { } /** @param testNames Only run the specified tests from the suite. */ - public void setTestNames(@Nullable List testNames) { + public void setTestNames(List testNames) { m_testNames = testNames; } 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 d0ae63e38e..f0733b61fd 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 @@ -60,7 +60,7 @@ class GuiceHelper { } @Nullable - Injector getInjector(IClass iClass, @Nullable IInjectorFactory injectorFactory) { + Injector getInjector(IClass iClass, IInjectorFactory injectorFactory) { return getInjector(iClass.getRealClass(), injectorFactory); } 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 968cb3ec6d..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 @@ -235,8 +235,8 @@ public List parseToList() throws IOException { return new ArrayList<>(parse()); } - public static Collection parse( - @Nullable String suite, @Nullable IPostProcessor processor) throws IOException { + public static Collection parse(@Nullable String suite, IPostProcessor processor) + throws IOException { return newParser(suite, processor).parse(); } From 8abe66f6fda165b05f72d51baf6be4fb883f06d6 Mon Sep 17 00:00:00 2001 From: Julien Herr Date: Thu, 20 Aug 2026 00:45:09 +0200 Subject: [PATCH 09/14] refactor(testng): fold the cleanup review back in Four reviews of the diff, one per angle. What they found, and what it cost: Duplicated guards. TestClass grew a private realClass() asserting m_testClass, while the NoOpTestClass it extends already guards the same field in getRealClass() -- and throws IllegalStateException where the new one threw NullPointerException, so the same absence surfaced as one of two exceptions depending on which path reached the field first. Fifteen call sites now use the inherited accessor. TestRunner.requireExitCodeListener was byte-identical to the public getter eight lines below it. The "every suite has a runner in the map" assertion existed twice, inline in two files; it belongs to SuiteRunnerMap, which already enforces the same invariant in put(), and is now a require() there. Widenings that did not need to happen. ISuite.addListener published "you may pass null" to serve one call site in TestRunner, while the identical expression in TestNG was guarded properly in this same batch -- two answers to one question. ISuiteRunnerListener.getExitCodeListener widened for a field TestListenersContainer guarantees non-null through requireNonNullElseGet. Both are back to non-null, and the SuiteRunner branch that re-derived an object factory it had already resolved is gone with them. createCommandLineSuitesForClasses keeps a non-null parameter now that its caller says which of the two command line inputs it holds. Repeated work in hot paths. MethodInstance.SORT_BY_INDEX resolved both test classes twice per comparison, on the default sort path; TestMethodWorker resolved one twice per method instance; TestInvoker built the fallback instance for every result because Optional.orElse evaluates its argument eagerly. SuitePanel and TestHTMLReporter resolved an accessor they had already bound to a local, and JUnitReportReporter nested two assertions where ITestResult.getTestClass() is non-null and already implemented as exactly that pair. Every one of the 34 requireNonNull messages was checked for concatenation. All are constant literals. Also here: the four Utils helpers had their javadoc stacked ahead of one method rather than one each, EmailableReporter2's helpers were wedged between SuiteResult and its javadoc, includedGroups and excludedGroups are filled by a method that cannot answer null, TestResult had a guard whose branches returned the same value, TestRunner allocated its interceptor list twice, and java.util.Objects was spelled out in five files that import no competing Objects. --- .../src/main/java/org/testng/ISuite.java | 2 +- .../main/java/org/testng/internal/Utils.java | 27 ++++++------- .../java/org/testng/ISuiteRunnerListener.java | 3 -- .../src/main/java/org/testng/SuiteRunner.java | 9 ++--- .../java/org/testng/SuiteRunnerWorker.java | 5 +-- .../java/org/testng/SuiteTaskExecutor.java | 4 +- .../src/main/java/org/testng/TestClass.java | 39 ++++++++----------- .../src/main/java/org/testng/TestNG.java | 32 ++++++--------- .../src/main/java/org/testng/TestRunner.java | 26 ++++++------- .../java/org/testng/TestTaskExecutor.java | 6 +-- .../org/testng/TimeBombSkipException.java | 3 +- .../org/testng/internal/MethodInstance.java | 11 ++++-- .../invokers/ClassBasedParallelWorker.java | 6 +-- .../internal/invokers/SuiteRunnerMap.java | 9 +++++ .../testng/internal/invokers/TestInvoker.java | 4 +- .../internal/invokers/TestMethodWorker.java | 5 ++- .../testng/internal/objects/GuiceHelper.java | 3 +- .../testng/reporters/EmailableReporter2.java | 16 +++----- .../testng/reporters/JUnitReportReporter.java | 2 +- .../testng/reporters/TestHTMLReporter.java | 2 +- .../org/testng/reporters/jq/SuitePanel.java | 8 ++-- .../internal/LiteWeightTestNGMethod.java | 4 +- .../java/org/testng/internal/TestResult.java | 10 ++--- 23 files changed, 112 insertions(+), 124 deletions(-) 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 d6666ce09f..57c80f1cfc 100644 --- a/testng-core-api/src/main/java/org/testng/ISuite.java +++ b/testng-core-api/src/main/java/org/testng/ISuite.java @@ -78,7 +78,7 @@ public interface ISuite extends IAttributes { /** @return The representation of the current XML suite file. */ XmlSuite getXmlSuite(); - void addListener(@Nullable ITestNGListener listener); + void addListener(ITestNGListener listener); @Nullable Injector getParentInjector(); 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 41934c1567..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,6 +15,7 @@ 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; @@ -463,6 +464,10 @@ public static String toString(Object object, Class objectClass) { * @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. * @@ -474,6 +479,10 @@ public static String toString(Object object, Class objectClass) { * @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. * @@ -484,6 +493,10 @@ public static String toString(Object object, Class objectClass) { * @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. * @@ -493,22 +506,10 @@ public static String toString(Object object, Class objectClass) { * @param context The context to read the end date of. * @return The end date, never {@code null}. */ - public static java.util.Date requireEndDateOf(ITestContext context) { + public static Date requireEndDateOf(ITestContext context) { return Objects.requireNonNull(context.getEndDate(), "a reported test context has finished"); } - public static ITestContext requireTestContextOf(ITestResult result) { - return Objects.requireNonNull(result.getTestContext(), "a reported result carries a context"); - } - - public static ITestClass requireTestClassOf(ITestNGMethod method) { - return Objects.requireNonNull(method.getTestClass(), "a scheduled method is bound to a class"); - } - - public static ITestNGMethod requireMethodOf(ITestResult result) { - return Objects.requireNonNull(result.getMethod(), "a reported result carries a test method"); - } - public static String detailedMethodName(ITestNGMethod method, boolean fqn) { String tempName = annotationFormFor(method); if (!tempName.isEmpty()) { diff --git a/testng-core/src/main/java/org/testng/ISuiteRunnerListener.java b/testng-core/src/main/java/org/testng/ISuiteRunnerListener.java index 994dca21bc..062f7b1c56 100644 --- a/testng-core/src/main/java/org/testng/ISuiteRunnerListener.java +++ b/testng-core/src/main/java/org/testng/ISuiteRunnerListener.java @@ -1,10 +1,7 @@ package org.testng; -import org.jspecify.annotations.Nullable; - public interface ISuiteRunnerListener { - @Nullable ITestListener getExitCodeListener(); void beforeInvocation(IInvokedMethod method, ITestResult testResult); diff --git a/testng-core/src/main/java/org/testng/SuiteRunner.java b/testng-core/src/main/java/org/testng/SuiteRunner.java index 0354d2ff3b..af58f02eb4 100644 --- a/testng-core/src/main/java/org/testng/SuiteRunner.java +++ b/testng-core/src/main/java/org/testng/SuiteRunner.java @@ -136,14 +136,11 @@ protected SuiteRunner( boolean create = !configuredFactory.getClass().equals(suite.getObjectFactoryClass()); final ITestObjectFactory suiteObjectFactory; if (create) { - if (objectFactory == null) { - objectFactory = configuredFactory; - } // Dont keep creating the object factory repeatedly since our current object factory // Was already created based off of a suite level object factory. suiteObjectFactory = Objects.requireNonNull( - objectFactory.newInstance(suite.getObjectFactoryClass()), + configuredFactory.newInstance(suite.getObjectFactoryClass()), "the object factory produced a suite level factory"); } else { suiteObjectFactory = configuredFactory; @@ -244,7 +241,7 @@ public void setReportResults(boolean reportResults) { useDefaultListeners = reportResults; } - public @Nullable ITestListener getExitCodeListener() { + public ITestListener getExitCodeListener() { return exitCodeListener; } @@ -484,7 +481,7 @@ protected void addListener(ISuiteListener reporter) { } @Override - public void addListener(@Nullable ITestNGListener listener) { + public void addListener(ITestNGListener listener) { if (listener instanceof IInvokedMethodListener) { IInvokedMethodListener invokedMethodListener = (IInvokedMethodListener) listener; invokedMethodListeners.put(invokedMethodListener.getClass(), invokedMethodListener); diff --git a/testng-core/src/main/java/org/testng/SuiteRunnerWorker.java b/testng-core/src/main/java/org/testng/SuiteRunnerWorker.java index 03ec653ef5..2ca33022ab 100644 --- a/testng-core/src/main/java/org/testng/SuiteRunnerWorker.java +++ b/testng-core/src/main/java/org/testng/SuiteRunnerWorker.java @@ -47,10 +47,7 @@ private void runSuite(SuiteRunnerMap suiteRunnerMap /* OUT */, XmlSuite xmlSuite Utils.log("TestNG", 0, "Running:\n" + allFiles); } - SuiteRunner suiteRunner = - (SuiteRunner) - java.util.Objects.requireNonNull( - suiteRunnerMap.get(xmlSuite), "every suite has a runner in the map"); + SuiteRunner suiteRunner = (SuiteRunner) suiteRunnerMap.require(xmlSuite); suiteRunner.run(); // TODO: this should be handled properly diff --git a/testng-core/src/main/java/org/testng/SuiteTaskExecutor.java b/testng-core/src/main/java/org/testng/SuiteTaskExecutor.java index e3d37d82a7..4fa87cbdc7 100644 --- a/testng-core/src/main/java/org/testng/SuiteTaskExecutor.java +++ b/testng-core/src/main/java/org/testng/SuiteTaskExecutor.java @@ -1,5 +1,6 @@ package org.testng; +import java.util.Objects; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; @@ -55,8 +56,7 @@ public void execute() { public void awaitCompletion() { Utils.log("TestNG", 2, "Starting executor for all suites"); try { - ExecutorService running = - java.util.Objects.requireNonNull(service, "execute() has started the pool"); + ExecutorService running = Objects.requireNonNull(service, "execute() has started the pool"); boolean ignored = running.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS); running.shutdownNow(); } catch (InterruptedException handled) { diff --git a/testng-core/src/main/java/org/testng/TestClass.java b/testng-core/src/main/java/org/testng/TestClass.java index bc5e5d2960..266f1845e7 100644 --- a/testng-core/src/main/java/org/testng/TestClass.java +++ b/testng-core/src/main/java/org/testng/TestClass.java @@ -79,13 +79,6 @@ public List getInstanceAfterClassMethods(@Nullable UUID instanceI return methods == null ? new ArrayList<>() : methods; } - /** - * The real class this TestClass was built for; {@code init} binds it before anything reads it. - */ - private Class realClass() { - return java.util.Objects.requireNonNull(m_testClass, "a TestClass is bound to its real class"); - } - private static final Logger LOG = Logger.getLogger(TestClass.class); protected TestClass( @@ -187,28 +180,28 @@ public void addObject(IObject.IdentifiableObject instance) { } private void initMethods() { - ITestNGMethod[] methods = testMethodFinder.getTestMethods(realClass(), xmlTest); + ITestNGMethod[] methods = testMethodFinder.getTestMethods(getRealClass(), xmlTest); m_testMethods = createTestMethods(methods); for (IdentifiableObject eachInstance : IObject.objects(iClass, false)) { m_beforeSuiteMethods = ConfigurationMethod.createSuiteConfigurationMethods( objectFactory, - testMethodFinder.getBeforeSuiteMethods(realClass()), + testMethodFinder.getBeforeSuiteMethods(getRealClass()), annotationFinder, true, eachInstance); m_afterSuiteMethods = ConfigurationMethod.createSuiteConfigurationMethods( objectFactory, - testMethodFinder.getAfterSuiteMethods(realClass()), + testMethodFinder.getAfterSuiteMethods(getRealClass()), annotationFinder, false, eachInstance); m_beforeTestConfMethods = ConfigurationMethod.createTestConfigurationMethods( objectFactory, - testMethodFinder.getBeforeTestConfigurationMethods(realClass()), + testMethodFinder.getBeforeTestConfigurationMethods(getRealClass()), annotationFinder, true, this.xmlTest, @@ -216,7 +209,7 @@ private void initMethods() { m_afterTestConfMethods = ConfigurationMethod.createTestConfigurationMethods( objectFactory, - testMethodFinder.getAfterTestConfigurationMethods(realClass()), + testMethodFinder.getAfterTestConfigurationMethods(getRealClass()), annotationFinder, false, this.xmlTest, @@ -224,7 +217,7 @@ private void initMethods() { m_beforeClassMethods = ConfigurationMethod.createClassConfigurationMethods( objectFactory, - testMethodFinder.getBeforeClassMethods(realClass()), + testMethodFinder.getBeforeClassMethods(getRealClass()), annotationFinder, true, xmlTest, @@ -233,7 +226,7 @@ private void initMethods() { m_afterClassMethods = ConfigurationMethod.createClassConfigurationMethods( objectFactory, - testMethodFinder.getAfterClassMethods(realClass()), + testMethodFinder.getAfterClassMethods(getRealClass()), annotationFinder, false, xmlTest, @@ -242,21 +235,21 @@ private void initMethods() { m_beforeGroupsMethods = ConfigurationMethod.createBeforeConfigurationMethods( objectFactory, - testMethodFinder.getBeforeGroupsConfigurationMethods(realClass()), + testMethodFinder.getBeforeGroupsConfigurationMethods(getRealClass()), annotationFinder, true, eachInstance); m_afterGroupsMethods = ConfigurationMethod.createAfterConfigurationMethods( objectFactory, - testMethodFinder.getAfterGroupsConfigurationMethods(realClass()), + testMethodFinder.getAfterGroupsConfigurationMethods(getRealClass()), annotationFinder, false, eachInstance); m_beforeTestMethods.addAll( ConfigurationMethod.createTestMethodConfigurationMethods( objectFactory, - testMethodFinder.getBeforeTestMethods(realClass()), + testMethodFinder.getBeforeTestMethods(getRealClass()), annotationFinder, true, xmlTest, @@ -264,7 +257,7 @@ private void initMethods() { m_afterTestMethods.addAll( ConfigurationMethod.createTestMethodConfigurationMethods( objectFactory, - testMethodFinder.getAfterTestMethods(realClass()), + testMethodFinder.getAfterTestMethods(getRealClass()), annotationFinder, false, xmlTest, @@ -280,14 +273,14 @@ private ITestNGMethod[] createTestMethods(ITestNGMethod[] methods) { List vResult = new ArrayList<>(); for (ITestNGMethod tm : methods) { ConstructorOrMethod m = tm.getConstructorOrMethod(); - if (m.getDeclaringClass().isAssignableFrom(realClass())) { + if (m.getDeclaringClass().isAssignableFrom(getRealClass())) { for (IdentifiableObject o : IObject.objects(iClass, false)) { - log(4, "Adding method " + tm + " on TestClass " + realClass()); + log(4, "Adding method " + tm + " on TestClass " + getRealClass()); vResult.add( new TestNGMethod(objectFactory, m.requireMethod(), annotationFinder, xmlTest, o)); } } else { - log(4, "Rejecting method " + tm + " for TestClass " + realClass()); + log(4, "Rejecting method " + tm + " for TestClass " + getRealClass()); } } @@ -303,7 +296,7 @@ private void log(int level, String s) { } protected void dump() { - LOG.info("===== Test class\n" + realClass().getName()); + LOG.info("===== Test class\n" + getRealClass().getName()); for (ITestNGMethod m : m_beforeClassMethods) { LOG.info(" @BeforeClass " + m); } @@ -324,7 +317,7 @@ protected void dump() { @Override public String toString() { - return Objects.toStringHelper(getClass()).add("name", realClass()).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 0af9015ab1..894c9ec55d 100644 --- a/testng-core/src/main/java/org/testng/TestNG.java +++ b/testng-core/src/main/java/org/testng/TestNG.java @@ -538,7 +538,7 @@ private List createCommandLineSuitesForMethods(List commandLin return result; } - private List createCommandLineSuitesForClasses(Class @Nullable [] classes) { + private List createCommandLineSuitesForClasses(Class[] classes) { // // See if any of the classes has an xmlSuite or xmlTest attribute. // If it does, create the appropriate XmlSuite, otherwise, create @@ -546,10 +546,7 @@ private List createCommandLineSuitesForClasses(Class @Nullable [] clas // XmlClass[] xmlClasses = - Arrays.stream( - Objects.requireNonNull(classes, "command line suites are built from a class list")) - .map(clazz -> new XmlClass(clazz, true)) - .toArray(XmlClass[]::new); + Arrays.stream(classes).map(clazz -> new XmlClass(clazz, true)).toArray(XmlClass[]::new); Map suites = new HashMap<>(); IAnnotationFinder finder = m_configuration.getAnnotationFinder(); @@ -933,10 +930,14 @@ public void setGenerateResultsPerSuite(boolean generateResultsPerSuite) { private void initializeCommandLineSuites() { if (m_commandLineTestClasses != null || m_commandLineMethods != null) { - if (null != m_commandLineMethods) { - m_cmdlineSuites = createCommandLineSuitesForMethods(m_commandLineMethods); + List cliMethods = m_commandLineMethods; + Class[] cliClasses = m_commandLineTestClasses; + if (null != cliMethods) { + m_cmdlineSuites = createCommandLineSuitesForMethods(cliMethods); + } else if (null != cliClasses) { + m_cmdlineSuites = createCommandLineSuitesForClasses(cliClasses); } else { - m_cmdlineSuites = createCommandLineSuitesForClasses(m_commandLineTestClasses); + return; } for (XmlSuite s : m_cmdlineSuites) { @@ -1336,11 +1337,6 @@ public List runSuitesLocally() { return new ArrayList<>(suiteRunnerMap.values()); } - /** Every suite the map is walked over was put in it by {@code createSuiteRunners}. */ - private static ISuite requireRunnerFor(SuiteRunnerMap map, XmlSuite suite) { - return Objects.requireNonNull(map.get(suite), "every suite has a runner in the map"); - } - private static void error(@Nullable String s) { LOGGER.error(s); } @@ -1371,7 +1367,7 @@ private void runSuitesSequentially( } SuiteRunnerWorker srw = new SuiteRunnerWorker( - requireRunnerFor(suiteRunnerMap, xmlSuite), suiteRunnerMap, verbose, defaultSuiteName); + suiteRunnerMap.require(xmlSuite), suiteRunnerMap, verbose, defaultSuiteName); srw.run(); } @@ -1388,11 +1384,11 @@ private void populateSuiteGraph( IDynamicGraph suiteGraph /* OUT */, SuiteRunnerMap suiteRunnerMap, XmlSuite xmlSuite) { - ISuite parentSuiteRunner = requireRunnerFor(suiteRunnerMap, xmlSuite); + ISuite parentSuiteRunner = suiteRunnerMap.require(xmlSuite); suiteGraph.addNode(parentSuiteRunner); if (!xmlSuite.getChildSuites().isEmpty()) { for (XmlSuite childSuite : xmlSuite.getChildSuites()) { - suiteGraph.addEdge(0, parentSuiteRunner, requireRunnerFor(suiteRunnerMap, childSuite)); + suiteGraph.addEdge(0, parentSuiteRunner, suiteRunnerMap.require(childSuite)); populateSuiteGraph(suiteGraph, suiteRunnerMap, childSuite); } } @@ -1594,10 +1590,6 @@ protected void configure(CommandLineArgs cla) { setTestClasses(classes.toArray(new Class[0])); } - if (cla.outputDirectory != null) { - setOutputDirectory(cla.outputDirectory); - } - if (cla.testNames != null) { setTestNames(Arrays.asList(cla.testNames.split(","))); setIgnoreMissedTestNames(cla.ignoreMissedTestNames); diff --git a/testng-core/src/main/java/org/testng/TestRunner.java b/testng-core/src/main/java/org/testng/TestRunner.java index 2299cb7420..2c0bed1448 100644 --- a/testng-core/src/main/java/org/testng/TestRunner.java +++ b/testng-core/src/main/java/org/testng/TestRunner.java @@ -146,7 +146,7 @@ public class TestRunner private @Nullable String m_host; // Defined dynamically depending on - private List m_methodInterceptors = new ArrayList<>(); + private final List m_methodInterceptors = new ArrayList<>(); private @Nullable ClassMethodMap m_classMethodMap; private @Nullable TestNGClassFinder m_testClassFinder; @@ -266,7 +266,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. @@ -390,7 +390,10 @@ private void initListeners() { // Instantiate all the listeners for (Class c : listenerClasses) { - addListener(factory.createListener(c)); + ITestNGListener created = factory.createListener(c); + if (created != null) { + addListener(created); + } } } @@ -623,12 +626,11 @@ public void run() { /** Both are dropped by {@link #forgetHeavyReferencesIfNeeded()} once the run is over. */ private ClassMethodMap requireClassMethodMap() { - return java.util.Objects.requireNonNull(m_classMethodMap, "the run still holds its method map"); + return Objects.requireNonNull(m_classMethodMap, "the run still holds its method map"); } private ConfigurationGroupMethods requireGroupMethods() { - return java.util.Objects.requireNonNull( - m_groupMethods, "the run still holds its group methods"); + return Objects.requireNonNull(m_groupMethods, "the run still holds its group methods"); } private void forgetHeavyReferencesIfNeeded() { @@ -881,7 +883,7 @@ private void fireEvent(boolean isStart) { ListenerOrderDeterminer.order(m_testListeners, m_configuration.getListenerComparator())) { itl.onStart(this); } - requireExitCodeListener().onStart(this); + getExitCodeListener().onStart(this); } else { List testListenersReversed = @@ -890,7 +892,7 @@ private void fireEvent(boolean isStart) { for (ITestListener itl : testListenersReversed) { itl.onFinish(this); } - requireExitCodeListener().onFinish(this); + getExitCodeListener().onFinish(this); } if (!isStart) { MethodHelper.clear(methods(this.getPassedConfigurations())); @@ -1102,7 +1104,7 @@ void addTestListener(ITestListener listener) { } } - public void addListener(@Nullable ITestNGListener listener) { + public void addListener(ITestNGListener listener) { if (listener instanceof IMethodInterceptor) { m_methodInterceptors.add((IMethodInterceptor) listener); } @@ -1153,10 +1155,6 @@ void addConfigurationListener(IConfigurationListener icl) { } } - private ITestListener requireExitCodeListener() { - return Objects.requireNonNull(exitCodeListener, "ExitCodeListener cannot be null."); - } - private void setExitCodeListener(@Nullable ITestListener exitCodeListener) { this.exitCodeListener = exitCodeListener; } @@ -1204,7 +1202,7 @@ private void removeConfigurationResultAfterExecution(ITestResult itr) { // So lets find the result based on the method and remove it off. m_configsToBeInvoked .getAllResults() - .removeIf(tr -> java.util.Objects.equals(tr.getMethod(), itr.getMethod())); + .removeIf(tr -> Objects.equals(tr.getMethod(), itr.getMethod())); } } diff --git a/testng-core/src/main/java/org/testng/TestTaskExecutor.java b/testng-core/src/main/java/org/testng/TestTaskExecutor.java index e722f6c7a3..0897439091 100644 --- a/testng-core/src/main/java/org/testng/TestTaskExecutor.java +++ b/testng-core/src/main/java/org/testng/TestTaskExecutor.java @@ -1,6 +1,7 @@ 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; @@ -93,11 +94,10 @@ public void awaitCompletion() { // 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 = - java.util.Objects.requireNonNull(orchestrator, "execute() has started the graph") + Objects.requireNonNull(orchestrator, "execute() has started the graph") .awaitCompletion(timeOut, TimeUnit.MILLISECONDS); } else { - ExecutorService running = - java.util.Objects.requireNonNull(service, "execute() has started the pool"); + ExecutorService running = Objects.requireNonNull(service, "execute() has started the pool"); boolean ignored = running.awaitTermination(timeOut, TimeUnit.MILLISECONDS); running.shutdownNow(); } diff --git a/testng-core/src/main/java/org/testng/TimeBombSkipException.java b/testng-core/src/main/java/org/testng/TimeBombSkipException.java index 5daff0bc18..c87462b97a 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 java.util.Objects; import org.jspecify.annotations.Nullable; /** @@ -196,7 +197,7 @@ private void initExpireDate(String date) { /** The date this exception stops skipping; absent when it was built without one. */ private Calendar requireExpireDate() { - return java.util.Objects.requireNonNull(m_expireDate, "the exception carries an expiry date"); + return Objects.requireNonNull(m_expireDate, "the exception carries an expiry date"); } @Override 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 da039e4c2f..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; @@ -40,8 +41,10 @@ public String toString() { @Override public int compare(IMethodInstance o1, IMethodInstance o2) { // If the two methods are in different - XmlTest test1 = Utils.requireTestClassOf(o1.getMethod()).getXmlTest(); - XmlTest test2 = Utils.requireTestClassOf(o2.getMethod()).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. A method a // @Factory produced has no tag of its own, which reads the same way here. @@ -55,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 = Utils.requireTestClassOf(o1.getMethod()).getXmlClass(); - XmlClass class2 = Utils.requireTestClassOf(o2.getMethod()).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/invokers/ClassBasedParallelWorker.java b/testng-core/src/main/java/org/testng/internal/invokers/ClassBasedParallelWorker.java index f9bfd7fdc1..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 @@ -120,9 +120,9 @@ private static boolean isSequential( } private static Map getParameters(IMethodInstance im) { + ITestNGMethod method = im.getMethod(); XmlTest xmlTest = - Objects.requireNonNull( - im.getMethod().getXmlTest(), "a scheduled method belongs to a "); - return im.getMethod().findMethodParameters(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/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 887f74d230..378b000707 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 @@ -510,9 +510,9 @@ private Set keepSameInstances(ITestNGMethod method, Set { + Object resultInstance = r.getInstance(); Object instance = - Optional.ofNullable(r.getInstance()) - .orElse(Utils.requireMethodOf(r).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(); 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 4cda9e9f7d..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 @@ -133,6 +133,7 @@ && doesTaskHavePreRequisites() for (IMethodInstance testMethodInstance : m_methodInstances) { ITestNGMethod testMethod = testMethodInstance.getMethod(); + 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 @@ -153,7 +154,7 @@ && doesTaskHavePreRequisites() if (canInvokeBeforeClassMethods()) { try (KeyAwareAutoCloseableLock.AutoReleasable ignored = lock.lockForObject(key)) { - invokeBeforeClassMethods(Utils.requireTestClassOf(testMethod), testMethodInstance); + invokeBeforeClassMethods(testClassOfMethod, testMethodInstance); } } @@ -162,7 +163,7 @@ && doesTaskHavePreRequisites() invokeTestMethods(testMethod, testMethod.getInstance()); } finally { try (KeyAwareAutoCloseableLock.AutoReleasable ignored = lock.lockForObject(key)) { - invokeAfterClassMethods(Utils.requireTestClassOf(testMethod), testMethodInstance); + invokeAfterClassMethods(testClassOfMethod, testMethodInstance); } } } 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 f0733b61fd..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; @@ -172,7 +173,7 @@ private List getGuiceModules(Class cls) { } private static IInjectorFactory requireInjectorFactory(@Nullable IInjectorFactory factory) { - return java.util.Objects.requireNonNull(factory, "a running suite carries an injector factory"); + return Objects.requireNonNull(factory, "a running suite carries an injector factory"); } private Injector createInjector( 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 be0b29b534..01df5b33eb 100644 --- a/testng-core/src/main/java/org/testng/reporters/EmailableReporter2.java +++ b/testng-core/src/main/java/org/testng/reporters/EmailableReporter2.java @@ -188,8 +188,8 @@ protected void writeSuiteSummary() { writeTableData(integerFormat.format(retriedTests), retriedTests > 0 ? "num attn" : "num"); writeTableData(integerFormat.format(failedTests), failedTests > 0 ? "num attn" : "num"); writeTableData(decimalFormat.format(duration), "num"); - writeTableData(orEmpty(testResult.getIncludedGroups())); - writeTableData(orEmpty(testResult.getExcludedGroups())); + writeTableData(testResult.getIncludedGroups()); + writeTableData(testResult.getExcludedGroups()); writer().println(""); @@ -648,16 +648,12 @@ protected void writeTag(String tag, String html, @Nullable String cssClasses) { writer.print(">"); } - /** Groups {@link TestResult}s by suite. */ /** A <test> that carries no name renders as an empty cell rather than the text "null". */ - private static String orEmpty(@Nullable String text) { - return text == null ? "" : text; - } - 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; private final List testResults = new ArrayList<>(); @@ -704,7 +700,7 @@ protected static class TestResult { private final int skippedTestCount; private final int passedTestCount; private final long duration; - private final @Nullable String includedGroups; + private final String includedGroups; private final String excludedGroups; public TestResult(ITestContext context) { @@ -862,11 +858,11 @@ public long getDuration() { return duration; } - public @Nullable String getIncludedGroups() { + public String getIncludedGroups() { return includedGroups; } - public @Nullable String getExcludedGroups() { + public String getExcludedGroups() { return excludedGroups; } 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 fb5e937f28..e1bbc234d5 100644 --- a/testng-core/src/main/java/org/testng/reporters/JUnitReportReporter.java +++ b/testng-core/src/main/java/org/testng/reporters/JUnitReportReporter.java @@ -305,7 +305,7 @@ private static class TestTag { private void addResults(Set allResults, Map, Set> out) { for (ITestResult tr : allResults) { - Class cls = Utils.requireTestClassOf(Utils.requireMethodOf(tr)).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/TestHTMLReporter.java b/testng-core/src/main/java/org/testng/reporters/TestHTMLReporter.java index 37456c1af9..1dd8303efb 100644 --- a/testng-core/src/main/java/org/testng/reporters/TestHTMLReporter.java +++ b/testng-core/src/main/java/org/testng/reporters/TestHTMLReporter.java @@ -175,7 +175,7 @@ public static void generateTable( pw.append("\n"); // Custom attributes - CustomAttribute[] attributes = Utils.requireMethodOf(tr).getAttributes(); + CustomAttribute[] attributes = method.getAttributes(); if (attributes != null && attributes.length > 0) { pw.append(""); String divId = "attributes-" + tr.hashCode(); diff --git a/testng-core/src/main/java/org/testng/reporters/jq/SuitePanel.java b/testng-core/src/main/java/org/testng/reporters/jq/SuitePanel.java index 5566430f65..907ef26605 100644 --- a/testng-core/src/main/java/org/testng/reporters/jq/SuitePanel.java +++ b/testng-core/src/main/java/org/testng/reporters/jq/SuitePanel.java @@ -4,6 +4,7 @@ import java.util.List; import java.util.stream.Collectors; import org.testng.ISuite; +import org.testng.ITestNGMethod; import org.testng.ITestResult; import org.testng.annotations.CustomAttribute; import org.testng.internal.Utils; @@ -71,7 +72,8 @@ private void generateMethod(ITestResult tr, XMLStringBuffer xsb) { xsb.push(D, C, "method-content"); xsb.push("a", "name", Model.getTestResultName(tr)); xsb.pop("a"); - xsb.addOptional(S, Utils.requireMethodOf(tr).getMethodName(), C, "method-name"); + ITestNGMethod method = Utils.requireMethodOf(tr); + xsb.addOptional(S, method.getMethodName(), C, "method-name"); // Parameters? if (tr.getParameters().length > 0) { @@ -80,7 +82,7 @@ private void generateMethod(ITestResult tr, XMLStringBuffer xsb) { xsb.addOptional(S, "(" + text + ")", C, "parameters"); } - CustomAttribute[] attributes = Utils.requireMethodOf(tr).getAttributes(); + CustomAttribute[] attributes = method.getAttributes(); if (attributes != null && attributes.length > 0) { String text = Arrays.stream(attributes) @@ -97,7 +99,7 @@ private void generateMethod(ITestResult tr, XMLStringBuffer xsb) { } // Description? - String description = Utils.requireMethodOf(tr).getDescription(); + String description = method.getDescription(); if (!Strings.isNullOrEmpty(description)) { xsb.push("em"); xsb.addString("(" + description + ")"); 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 883d3c7c71..b3f3224e42 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 @@ -346,7 +346,9 @@ public void setDate(long date) { @Override public boolean canRunFromClass(IClass testClass) { - return Utils.requireTestClassOf(this).getRealClass().isAssignableFrom(testClass.getRealClass()); + return Objects.requireNonNull(this.testClass, "a scheduled method is bound to a class") + .getRealClass() + .isAssignableFrom(testClass.getRealClass()); } @Override 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 03efcf74e8..9a7dbbda66 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 @@ -13,6 +13,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 +104,8 @@ private void init( long start, long end) { m_throwable = t; - m_instanceName = Utils.requireTestClassOf(method).getName(); + ITestClass boundClass = Utils.requireTestClassOf(method); + m_instanceName = boundClass.getName(); if (null == m_throwable) { m_status = ITestResult.SUCCESS; } @@ -171,11 +173,7 @@ public void setEndMillis(long millis) { if (instance instanceof ITest) { return ((ITest) instance).getTestName(); } - String boundTestName = Utils.requireTestClassOf(m_method).getTestName(); - if (boundTestName != null) { - return boundTestName; - } - return null; + return Utils.requireTestClassOf(m_method).getTestName(); } @Override From b71c97fc14031a595af3659b7c8531894a77af64 Mon Sep 17 00:00:00 2001 From: Julien Herr Date: Thu, 20 Aug 2026 00:46:50 +0200 Subject: [PATCH 10/14] docs(changes): correct the member count and the getParameter attribution getParameter is on ISuite, not ITestContext, the return-widening count is thirty-seven rather than thirty-six, and IClass.getInstances' error message prefix belongs with the widened parameters rather than being left out. --- CHANGES.txt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 3a48254368..17122d6bdf 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -24,7 +24,7 @@ Fixed: A tag that carries no name attribute is now reported the way an Possible backward incompatible changes: -- org.testng is declared @NullMarked, and thirty-six members of it widen to @Nullable because that +- 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, @@ -33,15 +33,16 @@ Possible backward incompatible changes: IDataProviderMethod.getInstance and getMethod; IMethodInstance.getInstance; ITestClassFinder.getIClass; ITestNGListenerFactory.createListener; ITestObjectFactory.newInstance(Constructor, Object...); ITestContext.getName, getEndDate, - getHost, getInjectorFactory and getParameter; ISuite.getHost, getParameter, getParentInjector and + 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, and - of Reporter.setCurrentTestResult, which TestNG calls with null to clear the current result. A Java + 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 From 60e832b3b6aa9cec4f0856df6f037afa44bb2fa6 Mon Sep 17 00:00:00 2001 From: Julien Herr Date: Thu, 20 Aug 2026 10:31:13 +0200 Subject: [PATCH 11/14] style(testng): stop spelling out imports the compiler can resolve SuiteResult compared two possibly-absent names by writing out both java.util.Objects.compare and java.util.Comparator.nullsFirst on one line, where neither name clashes with anything the file imports. The comparator is now a named constant that says what the order is -- an unnamed sorts first -- and the call site is one short expression. ClassImpl and TestResult keep java.util.Objects out of their bodies with a static import of requireNonNull. Both files import org.testng.collections.Objects for toStringHelper, which is what forced the qualified form; the repo already static imports JDK members this way (StandardCharsets.UTF_8) and its own helpers (Utils.isStringNotEmpty, ListenerComparator.sort). MethodInstance keeps the one qualified java.util.Objects.equals it has: org.testng.collections.Objects offers no equals, and a bare static-imported equals(a, b) inside a class reads like this.equals. testng-core's dependency block also loses the comment about annotations needing to be on the compile classpath. It explained the spotbugs compileOnly that went with the last javax.annotation uses, and described nothing once that left. --- testng-core/src/main/java/org/testng/SuiteResult.java | 8 ++++++-- .../src/main/java/org/testng/internal/ClassImpl.java | 7 ++++--- testng-core/testng-core-build.gradle.kts | 1 - .../src/main/java/org/testng/internal/TestResult.java | 4 +++- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/testng-core/src/main/java/org/testng/SuiteResult.java b/testng-core/src/main/java/org/testng/SuiteResult.java index 42d80cc796..01f8183a6f 100644 --- a/testng-core/src/main/java/org/testng/SuiteResult.java +++ b/testng-core/src/main/java/org/testng/SuiteResult.java @@ -1,11 +1,16 @@ package org.testng; +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; @@ -30,8 +35,7 @@ public int compareTo(SuiteResult other) { try { String n1 = getTestContext().getName(); String n2 = other.getTestContext().getName(); - result = - java.util.Objects.compare(n1, n2, java.util.Comparator.nullsFirst(String::compareTo)); + 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/internal/ClassImpl.java b/testng-core/src/main/java/org/testng/internal/ClassImpl.java index 4ed4f90a65..915f1cf51f 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; @@ -104,8 +106,7 @@ public XmlTest getXmlTest() { factory = m_testContext.getSuite().getObjectFactory(); } IObjectDispenser dispenser = - Dispenser.newInstance( - java.util.Objects.requireNonNull(factory, "a suite carries an object factory")); + Dispenser.newInstance(requireNonNull(factory, "a suite carries an object factory")); BasicAttributes basic = new BasicAttributes(this, null); DetailedAttributes detailed = newDetailedAttributes(create, errMsgPrefix); CreationAttributes attributes = new CreationAttributes(m_testContext, basic, detailed); @@ -174,7 +175,7 @@ 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(); } diff --git a/testng-core/testng-core-build.gradle.kts b/testng-core/testng-core-build.gradle.kts index 7c3856c39f..b67fde24d1 100644 --- a/testng-core/testng-core-build.gradle.kts +++ b/testng-core/testng-core-build.gradle.kts @@ -21,7 +21,6 @@ tasks.withType().configureEach { dependencies { api(projects.testngCoreApi) - // Annotations have to be available on the compile classpath for the proper compilation "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/TestResult.java b/testng-runner-api/src/main/java/org/testng/internal/TestResult.java index 9a7dbbda66..40d9948de5 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; @@ -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"); } From fc1c8c7ab18cf13947d80ef0e3ad38f06d63e966 Mon Sep 17 00:00:00 2001 From: Julien Herr Date: Thu, 20 Aug 2026 11:16:41 +0200 Subject: [PATCH 12/14] refactor(testng): fold the second cleanup review back in A second pass of the same four reviews over the finished diff. The first one took the duplicated guards; this one took the places where the annotation was the bandaid and the shape underneath was the answer. Widenings withdrawn, because the null they described cannot reach the callee: - IMethodSelector.includeMethod's context. The one caller that passes null, ClassMethodMap, holds a concrete XmlMethodSelector, so the null never dispatches through the SPI; RunInfo, the only polymorphic call, always builds a context. The implementation keeps the @Nullable, which is legal for an override and is where the fact actually lives. - TimeBombSkipException's expiry date. Every one of the ten constructors either delegates or ends in initExpireDate, so the field is never absent -- it was only non-final. Helpers that return the calendar rather than assign it let it be final, which deletes the annotation, the guard, and the branch in isSkip that could never be taken. - TestRunner.setExitCodeListener's parameter, now that ISuiteRunnerListener.getExitCodeListener is non-null again. Assertions removed by giving the mechanism what it was missing: - TimeUtils grew a Supplier overload. Two callers were writing into an AtomicReference from a lambda that runs synchronously and then asserting the value came back; both are now a plain assignment. - IInstanceIdentity.carriesInstance replaces five open-coded comparisons against NO_INSTANCE, and takes the slot the deleted isIdentityAware left. MethodHelper had the one site the sentinel had not reached: a null test that can no longer hold, guarded by a comment describing it. LiteWeightTestNGMethod stops contradicting the interface this batch declares. Its data provider proxy fabricated an empty name and an unsupported method rather than answering the null getDataProviderMethod() documents; the three call sites in TestNG all test for null already. In CHANGES.txt. Also: TestClass resolves its real class once per method rather than thirteen times and answers Collections.emptyList rather than a fresh ArrayList; TestInvoker hoists a resolution out of a parallel stream; SuiteRunner reads the configured factory once and loses a comment that described a branch the first pass deleted; TestNG uses the getOrDefault its own file uses twelve lines lower, and initializeCommandLineSuitesGroups states in its signature what two boolean parameters were carrying; ClassMethodMap compares both test classes rather than asserting one; two guards on the same accessor say the same thing. --- CHANGES.txt | 1 + .../main/java/org/testng/IMethodSelector.java | 4 +- .../main/java/org/testng/ClassMethodMap.java | 4 +- .../main/java/org/testng/DependencyMap.java | 8 +- .../src/main/java/org/testng/SuiteRunner.java | 13 ++-- .../src/main/java/org/testng/TestClass.java | 37 +++++----- .../src/main/java/org/testng/TestNG.java | 51 +++++-------- .../src/main/java/org/testng/TestRunner.java | 18 ++--- .../org/testng/TimeBombSkipException.java | 34 ++++----- .../java/org/testng/internal/ClassImpl.java | 3 +- .../testng/internal/IInstanceIdentity.java | 9 +++ .../org/testng/internal/MethodHelper.java | 19 ++--- .../java/org/testng/internal/Parameters.java | 12 +-- .../testng/internal/invokers/TestInvoker.java | 6 +- .../main/java/org/testng/util/TimeUtils.java | 20 ++++- .../internal/LiteWeightTestNGMethod.java | 74 ++++++++----------- .../java/org/testng/internal/TestResult.java | 2 +- 17 files changed, 145 insertions(+), 170 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 17122d6bdf..30a91bc62e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -20,6 +20,7 @@ Fixed: org.testng.internal.MethodSorting.INSTANCES orders two invocations of the 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: diff --git a/testng-core-api/src/main/java/org/testng/IMethodSelector.java b/testng-core-api/src/main/java/org/testng/IMethodSelector.java index 354e07b516..16af0633cf 100644 --- a/testng-core-api/src/main/java/org/testng/IMethodSelector.java +++ b/testng-core-api/src/main/java/org/testng/IMethodSelector.java @@ -1,7 +1,6 @@ package org.testng; import java.util.List; -import org.jspecify.annotations.Nullable; /** * This interface is used to augment or replace TestNG's algorithm to decide whether a test method @@ -18,8 +17,7 @@ public interface IMethodSelector { * @param isTestMethod true if this is a @Test method, false if it's a configuration method * @return true if this method should be included in the test run, false otherwise */ - boolean includeMethod( - @Nullable IMethodSelectorContext context, ITestNGMethod method, boolean isTestMethod); + boolean includeMethod(IMethodSelectorContext context, ITestNGMethod method, boolean isTestMethod); /** * Invoked when all the test methods are known so that the method selector can perform additional diff --git a/testng-core/src/main/java/org/testng/ClassMethodMap.java b/testng-core/src/main/java/org/testng/ClassMethodMap.java index 93c1e9bcfa..966ca89366 100644 --- a/testng-core/src/main/java/org/testng/ClassMethodMap.java +++ b/testng-core/src/main/java/org/testng/ClassMethodMap.java @@ -3,12 +3,12 @@ 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.Utils; import org.testng.internal.XmlMethodSelector; /** @@ -61,7 +61,7 @@ public boolean removeAndCheckIfLast(ITestNGMethod m, @Nullable 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() && Utils.requireTestClassOf(tm).equals(m.getTestClass())) { + if (tm.getEnabled() && Objects.equals(tm.getTestClass(), m.getTestClass())) { return false; } } diff --git a/testng-core/src/main/java/org/testng/DependencyMap.java b/testng-core/src/main/java/org/testng/DependencyMap.java index 4e8d540e86..d7a9f33331 100644 --- a/testng-core/src/main/java/org/testng/DependencyMap.java +++ b/testng-core/src/main/java/org/testng/DependencyMap.java @@ -110,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) != IInstanceIdentity.NO_INSTANCE - || IInstanceIdentity.getInstanceId(baseClassMethod) != IInstanceIdentity.NO_INSTANCE; + IInstanceIdentity.carriesInstance(derivedClassMethod) + || IInstanceIdentity.carriesInstance(baseClassMethod); boolean params = baseClassMethod.getFactoryInstance().isPresent(); if (result && params && RuntimeBehavior.enforceThreadAffinity()) { @@ -140,8 +140,8 @@ private static boolean hasSameParameters( private static boolean isSameInstance( ITestNGMethod baseClassMethod, ITestNGMethod derivedClassMethod) { boolean bothCarryAnInstance = - IInstanceIdentity.getInstanceId(derivedClassMethod) != IInstanceIdentity.NO_INSTANCE - && IInstanceIdentity.getInstanceId(baseClassMethod) != IInstanceIdentity.NO_INSTANCE; + IInstanceIdentity.carriesInstance(derivedClassMethod) + && IInstanceIdentity.carriesInstance(baseClassMethod); if (!bothCarryAnInstance) { return false; } diff --git a/testng-core/src/main/java/org/testng/SuiteRunner.java b/testng-core/src/main/java/org/testng/SuiteRunner.java index af58f02eb4..4eee3accac 100644 --- a/testng-core/src/main/java/org/testng/SuiteRunner.java +++ b/testng-core/src/main/java/org/testng/SuiteRunner.java @@ -124,20 +124,19 @@ 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); } - ITestObjectFactory configuredFactory = - Objects.requireNonNull( - configuration.getObjectFactory(), "the configuration carries an object factory"); + // The anonymous factory below closes over it, so it has to be effectively final. + final ITestObjectFactory configuredFactory = declaredFactory; if (suite.getObjectFactoryClass() == null) { objectFactory = configuredFactory; } else { boolean create = !configuredFactory.getClass().equals(suite.getObjectFactoryClass()); final ITestObjectFactory suiteObjectFactory; if (create) { - // Dont keep creating the object factory repeatedly since our current object factory - // Was already created based off of a suite level object factory. suiteObjectFactory = Objects.requireNonNull( configuredFactory.newInstance(suite.getObjectFactoryClass()), diff --git a/testng-core/src/main/java/org/testng/TestClass.java b/testng-core/src/main/java/org/testng/TestClass.java index 266f1845e7..0a0de5b298 100644 --- a/testng-core/src/main/java/org/testng/TestClass.java +++ b/testng-core/src/main/java/org/testng/TestClass.java @@ -2,6 +2,7 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -69,14 +70,12 @@ private static List getAllClassLevelConfigs(Map getInstanceBeforeClassMethods(@Nullable UUID instanceId) { - List methods = beforeClassConfig.get(instanceId); - return methods == null ? new ArrayList<>() : methods; + return beforeClassConfig.getOrDefault(instanceId, Collections.emptyList()); } @Override public List getInstanceAfterClassMethods(@Nullable UUID instanceId) { - List methods = afterClassConfig.get(instanceId); - return methods == null ? new ArrayList<>() : methods; + return afterClassConfig.getOrDefault(instanceId, Collections.emptyList()); } private static final Logger LOG = Logger.getLogger(TestClass.class); @@ -180,28 +179,29 @@ public void addObject(IObject.IdentifiableObject instance) { } private void initMethods() { - ITestNGMethod[] methods = testMethodFinder.getTestMethods(getRealClass(), 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(getRealClass()), + testMethodFinder.getBeforeSuiteMethods(realClass), annotationFinder, true, eachInstance); m_afterSuiteMethods = ConfigurationMethod.createSuiteConfigurationMethods( objectFactory, - testMethodFinder.getAfterSuiteMethods(getRealClass()), + testMethodFinder.getAfterSuiteMethods(realClass), annotationFinder, false, eachInstance); m_beforeTestConfMethods = ConfigurationMethod.createTestConfigurationMethods( objectFactory, - testMethodFinder.getBeforeTestConfigurationMethods(getRealClass()), + testMethodFinder.getBeforeTestConfigurationMethods(realClass), annotationFinder, true, this.xmlTest, @@ -209,7 +209,7 @@ private void initMethods() { m_afterTestConfMethods = ConfigurationMethod.createTestConfigurationMethods( objectFactory, - testMethodFinder.getAfterTestConfigurationMethods(getRealClass()), + testMethodFinder.getAfterTestConfigurationMethods(realClass), annotationFinder, false, this.xmlTest, @@ -217,7 +217,7 @@ private void initMethods() { m_beforeClassMethods = ConfigurationMethod.createClassConfigurationMethods( objectFactory, - testMethodFinder.getBeforeClassMethods(getRealClass()), + testMethodFinder.getBeforeClassMethods(realClass), annotationFinder, true, xmlTest, @@ -226,7 +226,7 @@ private void initMethods() { m_afterClassMethods = ConfigurationMethod.createClassConfigurationMethods( objectFactory, - testMethodFinder.getAfterClassMethods(getRealClass()), + testMethodFinder.getAfterClassMethods(realClass), annotationFinder, false, xmlTest, @@ -235,21 +235,21 @@ private void initMethods() { m_beforeGroupsMethods = ConfigurationMethod.createBeforeConfigurationMethods( objectFactory, - testMethodFinder.getBeforeGroupsConfigurationMethods(getRealClass()), + testMethodFinder.getBeforeGroupsConfigurationMethods(realClass), annotationFinder, true, eachInstance); m_afterGroupsMethods = ConfigurationMethod.createAfterConfigurationMethods( objectFactory, - testMethodFinder.getAfterGroupsConfigurationMethods(getRealClass()), + testMethodFinder.getAfterGroupsConfigurationMethods(realClass), annotationFinder, false, eachInstance); m_beforeTestMethods.addAll( ConfigurationMethod.createTestMethodConfigurationMethods( objectFactory, - testMethodFinder.getBeforeTestMethods(getRealClass()), + testMethodFinder.getBeforeTestMethods(realClass), annotationFinder, true, xmlTest, @@ -257,7 +257,7 @@ private void initMethods() { m_afterTestMethods.addAll( ConfigurationMethod.createTestMethodConfigurationMethods( objectFactory, - testMethodFinder.getAfterTestMethods(getRealClass()), + testMethodFinder.getAfterTestMethods(realClass), annotationFinder, false, xmlTest, @@ -270,17 +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(getRealClass())) { + if (m.getDeclaringClass().isAssignableFrom(realClass)) { for (IdentifiableObject o : IObject.objects(iClass, false)) { - log(4, "Adding method " + tm + " on TestClass " + getRealClass()); + 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 " + getRealClass()); + log(4, "Rejecting method " + tm + " for TestClass " + realClass); } } diff --git a/testng-core/src/main/java/org/testng/TestNG.java b/testng-core/src/main/java/org/testng/TestNG.java index 894c9ec55d..790421588e 100644 --- a/testng-core/src/main/java/org/testng/TestNG.java +++ b/testng-core/src/main/java/org/testng/TestNG.java @@ -932,13 +932,11 @@ private void initializeCommandLineSuites() { if (m_commandLineTestClasses != null || m_commandLineMethods != null) { List cliMethods = m_commandLineMethods; Class[] cliClasses = m_commandLineTestClasses; - if (null != cliMethods) { - m_cmdlineSuites = createCommandLineSuitesForMethods(cliMethods); - } else if (null != cliClasses) { - m_cmdlineSuites = createCommandLineSuitesForClasses(cliClasses); - } else { - return; - } + 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()) { @@ -973,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 @Nullable [] m_includedGroups, - boolean hasExcludedGroups, - String @Nullable [] 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); } } @@ -1808,14 +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); - String xmlPathInJarValue = (String) cmdLineArgs.get(CommandLineArgs.XML_PATH_IN_JAR); - if (xmlPathInJarValue != null) { - result.xmlPathInJar = xmlPathInJarValue; - } - Boolean mixedValue = (Boolean) cmdLineArgs.get(CommandLineArgs.MIXED); - if (mixedValue != null) { - result.mixed = mixedValue; - } + 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()); diff --git a/testng-core/src/main/java/org/testng/TestRunner.java b/testng-core/src/main/java/org/testng/TestRunner.java index 2c0bed1448..49d72456d3 100644 --- a/testng-core/src/main/java/org/testng/TestRunner.java +++ b/testng-core/src/main/java/org/testng/TestRunner.java @@ -20,7 +20,6 @@ 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; @@ -624,11 +623,12 @@ public void run() { } } - /** Both are dropped by {@link #forgetHeavyReferencesIfNeeded()} once the run is over. */ + /** 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"); } @@ -691,16 +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 = - Objects.requireNonNull(reference.get(), "the run computed its dependency graph"); + TimeUtils.computeAndShowTime( + "DynamicGraphHelper.createDynamicGraph()", + () -> DynamicGraphHelper.createDynamicGraph(interceptedOrder, getCurrentXmlTest())); for (ITestNGMethod each : interceptedOrder) { if (each instanceof BaseTestMethod) { @@ -1155,7 +1149,7 @@ void addConfigurationListener(IConfigurationListener icl) { } } - private void setExitCodeListener(@Nullable ITestListener exitCodeListener) { + private void setExitCodeListener(ITestListener exitCodeListener) { this.exitCodeListener = exitCodeListener; } diff --git a/testng-core/src/main/java/org/testng/TimeBombSkipException.java b/testng-core/src/main/java/org/testng/TimeBombSkipException.java index c87462b97a..696c56ad89 100644 --- a/testng-core/src/main/java/org/testng/TimeBombSkipException.java +++ b/testng-core/src/main/java/org/testng/TimeBombSkipException.java @@ -26,7 +26,7 @@ public class TimeBombSkipException extends SkipException { private static final String FORMAT = "yyyy/MM/dd"; private final SimpleDateFormat sdf = new SimpleDateFormat(FORMAT); - private @Nullable Calendar m_expireDate; + private final Calendar m_expireDate; private DateFormat m_inFormat = sdf; private DateFormat m_outFormat = sdf; @@ -53,7 +53,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); } /** @@ -65,7 +65,7 @@ public TimeBombSkipException(String msg, Date expirationDate, String format) { */ public TimeBombSkipException(String msg, String date) { super(msg); - initExpireDate(date); + m_expireDate = expireDateOf(date); } /** @@ -94,7 +94,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); } /** @@ -109,7 +109,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); } /** @@ -127,7 +127,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); } /** @@ -142,7 +142,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); } /** @@ -178,18 +178,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); } @@ -202,10 +202,6 @@ private Calendar requireExpireDate() { @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())); @@ -224,7 +220,7 @@ public boolean isSkip() { } else { return super.getMessage() + "; Test must have been enabled by: " - + m_outFormat.format(requireExpireDate().getTime()); + + m_outFormat.format(m_expireDate.getTime()); } } 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 915f1cf51f..34fafdcaea 100644 --- a/testng-core/src/main/java/org/testng/internal/ClassImpl.java +++ b/testng-core/src/main/java/org/testng/internal/ClassImpl.java @@ -106,7 +106,8 @@ public XmlTest getXmlTest() { factory = m_testContext.getSuite().getObjectFactory(); } IObjectDispenser dispenser = - Dispenser.newInstance(requireNonNull(factory, "a suite carries an object factory")); + 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); 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 d8487cd52a..b397e5fb15 100644 --- a/testng-core/src/main/java/org/testng/internal/IInstanceIdentity.java +++ b/testng-core/src/main/java/org/testng/internal/IInstanceIdentity.java @@ -33,6 +33,15 @@ public String toString() { * @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. */ + /** + * @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; + } + static Object getInstanceId(Object object) { if (object instanceof IInstanceIdentity) { UUID instanceId = ((IInstanceIdentity) object).getInstanceId(); 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 dc8c78b79f..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 -> IInstanceIdentity.getInstanceId(m) != IInstanceIdentity.NO_INSTANCE) + .filter(IInstanceIdentity::carriesInstance) .collect(Collectors.groupingBy(IInstanceIdentity::getInstanceId, Collectors.toList())); } 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 352c67ce56..2dacdb13f4 100644 --- a/testng-core/src/main/java/org/testng/internal/Parameters.java +++ b/testng-core/src/main/java/org/testng/internal/Parameters.java @@ -843,7 +843,9 @@ public static ParameterHolder handleParameters( 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; @@ -1008,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/invokers/TestInvoker.java b/testng-core/src/main/java/org/testng/internal/invokers/TestInvoker.java index 378b000707..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 @@ -506,6 +506,7 @@ 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( @@ -520,10 +521,7 @@ private Set keepSameInstances(ITestNGMethod method, Set { + 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-runner-api/src/main/java/org/testng/internal/LiteWeightTestNGMethod.java b/testng-runner-api/src/main/java/org/testng/internal/LiteWeightTestNGMethod.java index b3f3224e42..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 @@ -67,7 +67,7 @@ public class LiteWeightTestNGMethod implements ITestNGMethod { 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; @@ -122,44 +122,34 @@ public LiteWeightTestNGMethod(ITestNGMethod iTestNGMethod) { toString = iTestNGMethod.toString(); IDataProviderMethod dp = iTestNGMethod.getDataProviderMethod(); dataProviderMethod = - new IDataProviderMethod() { - @Override - public @Nullable Object getInstance() { - if (dp == null) { - return null; - } - 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(); } @@ -346,9 +336,7 @@ public void setDate(long date) { @Override public boolean canRunFromClass(IClass testClass) { - return Objects.requireNonNull(this.testClass, "a scheduled method is bound to a class") - .getRealClass() - .isAssignableFrom(testClass.getRealClass()); + return Utils.requireTestClassOf(this).getRealClass().isAssignableFrom(testClass.getRealClass()); } @Override @@ -516,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 40d9948de5..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 @@ -139,7 +139,7 @@ private void init( } return; } - String boundName = Utils.requireTestClassOf(method).getTestName(); + String boundName = boundClass.getTestName(); if (boundName != null) { m_name = boundName; return; From 3a5b914374ca00644925c74b441b025278f8a5d7 Mon Sep 17 00:00:00 2001 From: Julien Herr Date: Fri, 21 Aug 2026 17:55:26 +0200 Subject: [PATCH 13/14] refactor(testng): answer the review Seven of the nine points hold; two do not, and are left alone. Applied: - Reporter.getCurrentTestResult() is @Nullable. Its setter already was, and logToReports has an explicit m == null branch that files the output as orphaned -- the getter was the one half of the pair still claiming the thread local is always set. The two private log methods widen with it. - XMLSuiteResultWriter guards the name before handing it to Properties.setProperty, which rejects a null value. NullAway does not model Properties, so the mark could not have caught this one. - IInstanceIdentity had two javadoc blocks stacked ahead of carriesInstance, leaving getInstanceId undocumented -- the same mistake the first review caught in Utils, made again while inserting the new method. - VerboseReporter.getMethodDeclaration loses the ITestResult it stopped using when the cleanup pass replaced tr.getMethod() with the method already in hand. - TimeBombSkipException loses requireExpireDate. Making the field final was supposed to take it; the removal was written against javadoc that an earlier edit had already changed, so it silently matched nothing and the method survived with no caller. - IObject's two javadocs say that the error message prefix may be null and is passed through, which is what the widened parameter means. - CHANGES.txt said thirty-six in the summary and thirty-seven in the incompatible-changes list. Counted by hand against the list: thirty-seven. Not applied: - TestClass.getInstances(boolean, String) forwards its own m_errorMsgPrefix rather than the argument. Real, but it predates the module split (cd8988f86) and is identical in this batch's base; the method is part of an API deprecated since 7.10, and choosing which prefix wins changes the message a user sees when instantiation fails. That belongs in its own commit. - Renaming the value-returning TimeUtils.computeAndShowTime overload. The concern is overload ambiguity, and a probe compiled against the built classes says there is none: method references returning a value and returning void, block lambdas, expression lambdas whose result is discarded, and the assigned form all resolve. Renaming a published utility for a hazard that does not exist is churn. --- CHANGES.txt | 2 +- testng-core-api/src/main/java/org/testng/Reporter.java | 8 ++++---- .../main/java/org/testng/TimeBombSkipException.java | 6 ------ .../java/org/testng/internal/IInstanceIdentity.java | 10 +++++----- .../src/main/java/org/testng/internal/IObject.java | 4 ++-- .../java/org/testng/reporters/VerboseReporter.java | 4 ++-- .../org/testng/reporters/XMLSuiteResultWriter.java | 7 ++++++- 7 files changed, 20 insertions(+), 21 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 30a91bc62e..17ffe682db 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -15,7 +15,7 @@ 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-six 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-six without testing it stops compiling. They are listed under Possible backward incompatible changes below (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) 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 7232c38db6..319ae7433f 100644 --- a/testng-core-api/src/main/java/org/testng/Reporter.java +++ b/testng-core-api/src/main/java/org/testng/Reporter.java @@ -75,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); @@ -166,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/src/main/java/org/testng/TimeBombSkipException.java b/testng-core/src/main/java/org/testng/TimeBombSkipException.java index 696c56ad89..536e975777 100644 --- a/testng-core/src/main/java/org/testng/TimeBombSkipException.java +++ b/testng-core/src/main/java/org/testng/TimeBombSkipException.java @@ -7,7 +7,6 @@ import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; -import java.util.Objects; import org.jspecify.annotations.Nullable; /** @@ -195,11 +194,6 @@ private Calendar expireDateOf(String date) { } } - /** The date this exception stops skipping; absent when it was built without one. */ - private Calendar requireExpireDate() { - return Objects.requireNonNull(m_expireDate, "the exception carries an expiry date"); - } - @Override public boolean isSkip() { try { 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 b397e5fb15..d2210dda63 100644 --- a/testng-core/src/main/java/org/testng/internal/IInstanceIdentity.java +++ b/testng-core/src/main/java/org/testng/internal/IInstanceIdentity.java @@ -28,11 +28,6 @@ public String toString() { @Nullable UUID getInstanceId(); - /** - * @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. - */ /** * @param object - The object to inspect. * @return - true when the object carries an instance, that is when {@link @@ -42,6 +37,11 @@ static boolean carriesInstance(Object object) { return getInstanceId(object) != NO_INSTANCE; } + /** + * @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 Object getInstanceId(Object object) { if (object instanceof IInstanceIdentity) { UUID instanceId = ((IInstanceIdentity) object).getInstanceId(); 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 a3fa37b212..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,7 +17,7 @@ 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, @Nullable String errorMsgPrefix); @@ -50,7 +50,7 @@ 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. */ diff --git a/testng-core/src/main/java/org/testng/reporters/VerboseReporter.java b/testng-core/src/main/java/org/testng/reporters/VerboseReporter.java index 09ee99fe8c..c1ab21cb21 100644 --- a/testng-core/src/main/java/org/testng/reporters/VerboseReporter.java +++ b/testng-core/src/main/java/org/testng/reporters/VerboseReporter.java @@ -181,7 +181,7 @@ private void logTestResult(Status st, ITestResult itr, boolean isConfMethod) { } ITestNGMethod tm = Utils.requireMethodOf(itr); int identLevel = sb.length(); - sb.append(getMethodDeclaration(tm, itr)); + sb.append(getMethodDeclaration(tm)); Object[] params = itr.getParameters(); Class[] paramTypes = tm.getParameterTypes(); if (null != params && params.length > 0) { @@ -255,7 +255,7 @@ protected void log(String message) { * @return FQN of a class + method declaration for a method passed in ie. * test.triangle.CheckCount.testCheckCount(java.lang.String) */ - private String getMethodDeclaration(ITestNGMethod method, ITestResult tr) { + private String getMethodDeclaration(ITestNGMethod method) { // see Utils.detailedMethodName // perhaps should rather adopt the original method instead diff --git a/testng-core/src/main/java/org/testng/reporters/XMLSuiteResultWriter.java b/testng-core/src/main/java/org/testng/reporters/XMLSuiteResultWriter.java index 2588bc1514..b7da032289 100644 --- a/testng-core/src/main/java/org/testng/reporters/XMLSuiteResultWriter.java +++ b/testng-core/src/main/java/org/testng/reporters/XMLSuiteResultWriter.java @@ -102,7 +102,12 @@ private File referenceSuiteResult( private Properties getSuiteResultAttributes(ISuiteResult suiteResult) { Properties attributes = new Properties(); ITestContext tc = suiteResult.getTestContext(); - attributes.setProperty(XMLReporterConfig.ATTR_NAME, tc.getName()); + String testName = tc.getName(); + if (testName != null) { + // Properties rejects a null value, and a tag is only unnamed when it was built + // through the no-argument XmlTest constructor, which is what the YAML parser uses. + attributes.setProperty(XMLReporterConfig.ATTR_NAME, testName); + } XMLReporter.setDurationAttributes( config, attributes, tc.getStartDate(), Utils.requireEndDateOf(tc)); return attributes; From cb6a3807c871a035e9cbbdf1aec1a49b24a7f51e Mon Sep 17 00:00:00 2001 From: Julien Herr Date: Fri, 21 Aug 2026 18:05:37 +0200 Subject: [PATCH 14/14] refactor(testng): resolve the ripple the rebase exposed #3393 and #3396 merged, so their branches are gone and this one now sits on master. The five commits master gained after them bring a parameter snapshot mechanism, and two of its sites read ITestResult.getMethod() -- which this batch declares @Nullable. ParameterSnapshots.record and TextReporter.reportedParametersOf both go through Utils.requireMethodOf, the assertion the rest of the reporters already use. TextReporter's own conflict resolved the other way: upstream replaced the (parameters, parameterTypes) pair with a snapshot lookup, which removes the very call this branch had rewritten, so the snapshot wins and only the method binding survives. --- .../org/testng/internal/reporters/ParameterSnapshots.java | 4 +++- .../src/main/java/org/testng/reporters/TextReporter.java | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) 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/TextReporter.java b/testng-core/src/main/java/org/testng/reporters/TextReporter.java index beddf96c39..d8fb755942 100644 --- a/testng-core/src/main/java/org/testng/reporters/TextReporter.java +++ b/testng-core/src/main/java/org/testng/reporters/TextReporter.java @@ -189,7 +189,7 @@ private void logResult( ParameterSnapshot captured = snapshots != null ? snapshots.find(tr) : null; return captured != null ? captured - : ParameterSnapshot.of(tr.getParameters(), tr.getMethod().getParameterTypes()); + : ParameterSnapshot.of(tr.getParameters(), Utils.requireMethodOf(tr).getParameterTypes()); } private void logExceptions(