Skip to content

Commit

Permalink
IHookable and IConfigurable callback discrepancy
Browse files Browse the repository at this point in the history
Closes #2704

Ensure that TestNG reports scenarios wherein 
User defines callbacks for configurations/test methods
But fails to invoke those callbacks explicitly 
And also fails in shipping test status to a user
recognized status (PASS|FAILURE|SKIP)
  • Loading branch information
krmahadevan committed Jan 18, 2022
1 parent fcba70f commit acab947
Show file tree
Hide file tree
Showing 14 changed files with 329 additions and 80 deletions.
1 change: 1 addition & 0 deletions CHANGES.txt
@@ -1,6 +1,7 @@
Current
7.6.0
Fixed: GITHUB-2709: Testnames not working together with suites in suite (Martin Aldrin)
Fixed: GITHUB-2704: IHookable and IConfigurable callback discrepancy (Krishnan Mahadevan)
Fixed: GITHUB-2637: Upgrade to JDK11 as the minimum JDK requirements(Krishnan Mahadevan)

7.5
Expand Down
@@ -0,0 +1,25 @@
package org.testng;

/**
* Represents an exception that is thrown when a configuration method is not invoked. One of the
* use-cases when this can happen is when the user does the following:
*
* <ul>
* <li>User defines a configuration method
* <li>The class that houses the configuration method defines support for callbacks via {@link
* IConfigurable} implementation
* <li>User willfully skips invoking the callback and also fails at altering the configuration
* method's status via {@link ITestResult#setStatus(int)}
* </ul>
*/
public class ConfigurationNotInvokedException extends TestNGException {

public ConfigurationNotInvokedException(ITestNGMethod tm) {
super(
tm.getQualifiedName()
+ " defines a callback via "
+ IConfigurable.class.getName()
+ " but neither the callback was invoked nor the status was altered to "
+ String.join("|", ITestResult.finalStatuses()));
}
}
22 changes: 22 additions & 0 deletions testng-core-api/src/main/java/org/testng/ITestResult.java
@@ -1,5 +1,6 @@
package org.testng;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.testng.internal.thread.ThreadTimeoutException;
Expand Down Expand Up @@ -114,6 +115,27 @@ default List<ITestNGMethod> getSkipCausedBy() {
*/
String id();

/**
* @return - <code>true</code> if the current test result is either {@link ITestResult#STARTED} or
* {@link ITestResult#CREATED}
*/
default boolean isNotRunning() {
return getStatus() == STARTED || getStatus() == CREATED;
}

/**
* @return - A list of all user facing statuses viz.,
* <ul>
* <li>{@link ITestResult#SUCCESS}
* <li>{@link ITestResult#SUCCESS_PERCENTAGE_FAILURE}
* <li>{@link ITestResult#FAILURE}
* <li>{@link ITestResult#SKIP}
* </ul>
*/
static List<String> finalStatuses() {
return Arrays.asList("SUCCESS", "FAILURE", "SKIP", "SUCCESS_PERCENTAGE_FAILURE");
}

/**
* @param result - The test result of a method
* @return - <code>true</code> if the test failure was due to a timeout.
Expand Down
@@ -0,0 +1,24 @@
package org.testng;

/**
* Represents an exception that is thrown when a test method is not invoked. One of the use-cases
* when this can happen is when the user does the following:
*
* <ul>
* <li>User defines a test method
* <li>The class that houses the test method defines support for callbacks via {@link IHookable}
* implementation
* <li>User willfully skips invoking the callback and also fails at altering the test method's
* status via {@link ITestResult#setStatus(int)}
* </ul>
*/
public class TestNotInvokedException extends TestNGException {
public TestNotInvokedException(ITestNGMethod tm) {
super(
tm.getQualifiedName()
+ " defines a callback via "
+ IHookable.class.getName()
+ " but neither the callback was invoked nor the status was altered to "
+ String.join("|", ITestResult.finalStatuses()));
}
}
Expand Up @@ -484,11 +484,8 @@ private static MatchResults matchMethod(ITestNGMethod[] methods, String regexp)
String thisMethodName = thisMethod.getName();
String methodName = usePackage ? calculateMethodCanonicalName(method) : thisMethodName;
Pair<String, String> cacheKey = Pair.create(regexp, methodName);
Boolean match = MATCH_CACHE.get(cacheKey);
if (match == null) {
match = pattern.matcher(methodName).matches();
MATCH_CACHE.put(cacheKey, match);
}
boolean match =
MATCH_CACHE.computeIfAbsent(cacheKey, key -> pattern.matcher(methodName).matches());
if (match) {
results.matchedMethods.add(method);
results.foundAtLeastAMethod = true;
Expand Down
Expand Up @@ -11,6 +11,7 @@
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.testng.ConfigurationNotInvokedException;
import org.testng.IClass;
import org.testng.IConfigurable;
import org.testng.IInvokedMethodListener;
Expand Down Expand Up @@ -374,15 +375,24 @@ private void invokeConfigurationMethod(
testResult.setStatus(ITestResult.SUCCESS);
return;
}
if (configurableInstance != null) {
MethodInvocationHelper.invokeConfigurable(
targetInstance, params, configurableInstance, method.getMethod(), testResult);
boolean willfullyIgnored = false;
boolean usesConfigurableInstance = configurableInstance != null;
if (usesConfigurableInstance) {
willfullyIgnored =
!MethodInvocationHelper.invokeConfigurable(
targetInstance, params, configurableInstance, method.getMethod(), testResult);
} else {
MethodInvocationHelper.invokeMethodConsideringTimeout(
tm, method, targetInstance, params, testResult);
}
boolean testStatusRemainedUnchanged = testResult.isNotRunning();
if (usesConfigurableInstance && willfullyIgnored && testStatusRemainedUnchanged) {
throw new ConfigurationNotInvokedException(tm);
}
testResult.setStatus(ITestResult.SUCCESS);
} catch (InvocationTargetException | IllegalAccessException ex) {
} catch (ConfigurationNotInvokedException
| InvocationTargetException
| IllegalAccessException ex) {
throwConfigurationFailure(testResult, ex);
testResult.setStatus(ITestResult.FAILURE);
throw ex;
Expand Down
@@ -1,13 +1,14 @@
package org.testng.internal.invokers;

import java.util.Optional;
import java.util.concurrent.Callable;
import org.testng.IHookable;
import org.testng.ITestNGMethod;
import org.testng.ITestResult;
import org.testng.internal.ConstructorOrMethod;

/** A Runnable Method invoker. */
public class InvokeMethodRunnable implements Callable<Void> {
public class InvokeMethodRunnable implements Callable<Boolean> {
private final ITestNGMethod m_method;
private final Object m_instance;
private final Object[] m_parameters;
Expand All @@ -27,51 +28,54 @@ public InvokeMethodRunnable(
m_testResult = testResult;
}

public void run() throws TestNGRuntimeException {
public boolean run() throws TestNGRuntimeException {
try {
call();
return call();
} catch (Exception e) {
throw new TestNGRuntimeException(e);
}
}

private void runOne() {
private boolean runOne() {
boolean invoked;
try {
RuntimeException t = null;
try {
ConstructorOrMethod m = m_method.getConstructorOrMethod();
if (m_hookable == null) {
invoked = true;
MethodInvocationHelper.invokeMethod(m.getMethod(), m_instance, m_parameters);
} else {
MethodInvocationHelper.invokeHookable(
m_instance, m_parameters, m_hookable, m.getMethod(), m_testResult);
invoked =
MethodInvocationHelper.invokeHookable(
m_instance, m_parameters, m_hookable, m.getMethod(), m_testResult);
}
} catch (Throwable e) {
Throwable cause = e.getCause();
if (cause == null) {
cause = e;
}
invoked = true;
Throwable cause = Optional.ofNullable(e.getCause()).orElse(e);
t = new TestNGRuntimeException(cause);
}
if (null != t) {
Thread.currentThread().interrupt();
throw t;
}
return invoked;
} finally {
m_method.incrementCurrentInvocationCount();
}
}

@Override
public Void call() throws Exception {
public Boolean call() throws Exception {
boolean flag = true;
if (m_method.getInvocationTimeOut() > 0) {
for (int i = 0; i < m_method.getInvocationCount(); i++) {
runOne();
flag = flag && runOne();
}
} else {
runOne();
flag = runOne();
}
return null;
return flag;
}

public static class TestNGRuntimeException extends RuntimeException {
Expand Down

0 comments on commit acab947

Please sign in to comment.