Skip to content

Commit

Permalink
Support getting dependencies info for a test
Browse files Browse the repository at this point in the history
Closes #893
  • Loading branch information
krmahadevan committed Nov 29, 2022
1 parent 39f8fa5 commit fd465c6
Show file tree
Hide file tree
Showing 10 changed files with 221 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGES.txt
@@ -1,4 +1,5 @@
Current
Fixed: GITHUB-893: TestNG should provide an Api which allow to find all dependent of a specific test (Krishnan Mahadevan)
New: Added .yml file extension for yaml suite files, previously only .yaml was allowed for yaml (Steven Jubb)
Fixed: GITHUB-2770: FileAlreadyExistsException when report is generated (melloware)
Fixed: GITHUB-2825: Programically Loading TestNG Suite from JAR File Fails to Delete Temporary Copy of Suite File (Steven Jubb)
Expand Down
4 changes: 4 additions & 0 deletions testng-core-api/src/main/java/org/testng/IDynamicGraph.java
Expand Up @@ -21,6 +21,10 @@ public interface IDynamicGraph<T> {

List<T> getFreeNodes();

default List<T> getUpstreamDependenciesFor(T node) {
throw new UnsupportedOperationException("Pending implementation");
}

List<T> getDependenciesFor(T node);

void setStatus(Collection<T> nodes, Status status);
Expand Down
19 changes: 19 additions & 0 deletions testng-core-api/src/main/java/org/testng/ITestNGMethod.java
Expand Up @@ -2,6 +2,7 @@

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import org.testng.annotations.CustomAttribute;
import org.testng.internal.ConstructorOrMethod;
Expand Down Expand Up @@ -71,6 +72,24 @@ public interface ITestNGMethod extends Cloneable {
*/
String[] getMethodsDependedUpon();

/**
* @return - The set of methods that are dependent on the current method. This information can
* help in deciding what other TestNG methods will be skipped if the current method fails. If
* the current method is a configuration method, then an empty set is returned.
*/
default Set<ITestNGMethod> downstreamDependencies() {
throw new UnsupportedOperationException("Pending implementation");
}

/**
* @return - The set of methods upon which the current method has a dependency. This information
* can help in deciding what all TestNG methods need to pass before the current method can be
* executed. If the current method is a configuration method, then an empty set is returned.
*/
default Set<ITestNGMethod> upstreamDependencies() {
throw new UnsupportedOperationException("Pending implementation");
}

void addMethodDependedUpon(String methodName);

/** @return true if this method was annotated with @Test */
Expand Down
12 changes: 12 additions & 0 deletions testng-core/src/main/java/org/testng/TestRunner.java
Expand Up @@ -24,6 +24,7 @@
import org.testng.collections.Maps;
import org.testng.collections.Sets;
import org.testng.internal.Attributes;
import org.testng.internal.BaseTestMethod;
import org.testng.internal.ClassBasedWrapper;
import org.testng.internal.ClassInfoMap;
import org.testng.internal.ConfigurationGroupMethods;
Expand Down Expand Up @@ -746,6 +747,17 @@ private void privateRun(XmlTest xmlTest) {
});
IDynamicGraph<ITestNGMethod> graph = reference.get();

for (ITestNGMethod each : interceptedOrder) {
if (each instanceof BaseTestMethod) {
// We don't want our users to change this vital info. That is why the setter is NOT
// being exposed via the interface, and so we resort to an "instanceof" check.
Set<ITestNGMethod> downstream = Sets.newHashSet(graph.getDependenciesFor(each));
((BaseTestMethod) each).setDownstreamDependencies(downstream);
Set<ITestNGMethod> upstream = Sets.newHashSet(graph.getUpstreamDependenciesFor(each));
((BaseTestMethod) each).setUpstreamDependencies(upstream);
}
}

graph.setVisualisers(this.visualisers);
// In some cases, additional sorting is needed to make sure tests run in the appropriate order.
// If the user specified a method interceptor, or if we have any methods that have a non-default
Expand Down
34 changes: 34 additions & 0 deletions testng-core/src/main/java/org/testng/internal/BaseTestMethod.java
Expand Up @@ -70,6 +70,8 @@ public abstract class BaseTestMethod implements ITestNGMethod, IInvocationStatus
private long m_invocationTimeOut = 0L;

private List<Integer> m_invocationNumbers = Lists.newArrayList();
private final Set<ITestNGMethod> downstreamDependencies = Sets.newHashSet();
private final Set<ITestNGMethod> upstreamDependencies = Sets.newHashSet();
private final Collection<Integer> m_failedInvocationNumbers = new ConcurrentLinkedQueue<>();
private long m_timeOut = 0;

Expand Down Expand Up @@ -176,6 +178,38 @@ public String[] getMethodsDependedUpon() {
return m_methodsDependedUpon;
}

@Override
public Set<ITestNGMethod> downstreamDependencies() {
return Collections.unmodifiableSet(downstreamDependencies);
}

@Override
public Set<ITestNGMethod> upstreamDependencies() {
return Collections.unmodifiableSet(upstreamDependencies);
}

public void setDownstreamDependencies(Set<ITestNGMethod> methods) {
if (!downstreamDependencies.isEmpty()) {
downstreamDependencies.clear();
}
Set<ITestNGMethod> toAdd = methods;
if (RuntimeBehavior.isMemoryFriendlyMode()) {
toAdd = methods.stream().map(LiteWeightTestNGMethod::new).collect(Collectors.toSet());
}
downstreamDependencies.addAll(toAdd);
}

public void setUpstreamDependencies(Set<ITestNGMethod> methods) {
if (!upstreamDependencies.isEmpty()) {
upstreamDependencies.clear();
}
Set<ITestNGMethod> toAdd = methods;
if (RuntimeBehavior.isMemoryFriendlyMode()) {
toAdd = methods.stream().map(LiteWeightTestNGMethod::new).collect(Collectors.toSet());
}
upstreamDependencies.addAll(toAdd);
}

/** {@inheritDoc} */
@Override
public boolean isTest() {
Expand Down
18 changes: 13 additions & 5 deletions testng-core/src/main/java/org/testng/internal/DynamicGraph.java
Expand Up @@ -4,6 +4,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.testng.IDynamicGraph;
Expand Down Expand Up @@ -76,12 +77,19 @@ public List<T> getFreeNodes() {
return finalResult;
}

@Override
public List<T> getUpstreamDependenciesFor(T node) {
return dependencies(m_edges.from(node));
}

public List<T> getDependenciesFor(T node) {
Map<T, Integer> data = m_edges.to(node);
if (data == null) {
return Lists.newArrayList();
}
return Lists.newArrayList(data.keySet());
return dependencies(m_edges.to(node));
}

private List<T> dependencies(Map<T, Integer> dependencies) {
return Optional.ofNullable(dependencies)
.map(found -> Lists.newArrayList(found.keySet()))
.orElse(Lists.newArrayList());
}

/** Set the status for a set of nodes. */
Expand Down
62 changes: 62 additions & 0 deletions testng-core/src/test/java/test/dependent/DependentTest.java
Expand Up @@ -5,6 +5,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import org.testng.Assert;
import org.testng.ITestListener;
Expand All @@ -23,6 +24,9 @@
import test.dependent.github1380.GitHub1380Sample4;
import test.dependent.issue2658.FailingClassSample;
import test.dependent.issue2658.PassingClassSample;
import test.dependent.issue893.DependencyTrackingListener;
import test.dependent.issue893.MultiLevelDependenciesTestClassSample;
import test.dependent.issue893.TestClassSample;

public class DependentTest extends SimpleBaseTest {

Expand Down Expand Up @@ -218,6 +222,64 @@ public void testMethodDependencyAmidstInheritance() {
assertThat(listener.getSkippedMethodNames()).containsExactly("failingMethod");
}

@Test(description = "GITHUB-893", dataProvider = "getTestData")
public void testDownstreamDependencyRetrieval(
Class<?> clazz, String independentMethod, String[] dependentMethods) {
TestNG testng = create(clazz);
DependencyTrackingListener listener = new DependencyTrackingListener();
testng.addListener(listener);
testng.run();
String cls = clazz.getCanonicalName();
String key = cls + "." + independentMethod;
Set<String> downstream = listener.getDownstreamDependencies().get(key);
dependentMethods =
Arrays.stream(dependentMethods).map(each -> cls + "." + each).toArray(String[]::new);
assertThat(downstream).containsExactly(dependentMethods);
}

@DataProvider(name = "getTestData")
public Object[][] getTestData() {
return new Object[][] {
{
TestClassSample.class,
"independentTest",
new String[] {"anotherDependentTest", "dependentTest"}
},
{MultiLevelDependenciesTestClassSample.class, "father", new String[] {"child"}},
{
MultiLevelDependenciesTestClassSample.class,
"grandFather",
new String[] {"father", "mother"}
},
{MultiLevelDependenciesTestClassSample.class, "child", new String[] {}}
};
}

@Test(description = "GITHUB-893", dataProvider = "getUpstreamTestData")
public void testUpstreamDependencyRetrieval(
Class<?> clazz, String independentMethod, String[] dependentMethods) {
TestNG testng = create(clazz);
DependencyTrackingListener listener = new DependencyTrackingListener();
testng.addListener(listener);
testng.run();
String cls = clazz.getCanonicalName();
String key = cls + "." + independentMethod;
Set<String> upstream = listener.getUpstreamDependencies().get(key);
dependentMethods =
Arrays.stream(dependentMethods).map(each -> cls + "." + each).toArray(String[]::new);
assertThat(upstream).containsExactly(dependentMethods);
}

@DataProvider(name = "getUpstreamTestData")
public Object[][] getUpstreamTestData() {
return new Object[][] {
{TestClassSample.class, "dependentTest", new String[] {"independentTest"}},
{MultiLevelDependenciesTestClassSample.class, "father", new String[] {"grandFather"}},
{MultiLevelDependenciesTestClassSample.class, "child", new String[] {"father", "mother"}},
{MultiLevelDependenciesTestClassSample.class, "grandFather", new String[] {}}
};
}

public static class MethodNameCollector implements ITestListener {

private static final Function<ITestResult, String> asString =
Expand Down
@@ -0,0 +1,44 @@
package test.dependent.issue893;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.testng.ITestContext;
import org.testng.ITestListener;
import org.testng.ITestNGMethod;
import org.testng.ITestResult;
import org.testng.collections.Sets;

public class DependencyTrackingListener implements ITestListener {
private final Map<String, Set<String>> downstreamDependencies = new HashMap<>();
private final Map<String, Set<String>> upstreamDependencies = new HashMap<>();

@Override
public void onTestStart(ITestResult result) {
ITestContext context = result.getTestContext();
for (ITestNGMethod method : context.getAllTestMethods()) {
String key = method.getQualifiedName();
downstreamDependencies
.computeIfAbsent(key, k -> Sets.newHashSet())
.addAll(
method.downstreamDependencies().stream()
.map(ITestNGMethod::getQualifiedName)
.collect(Collectors.toList()));
upstreamDependencies
.computeIfAbsent(key, k -> Sets.newHashSet())
.addAll(
method.upstreamDependencies().stream()
.map(ITestNGMethod::getQualifiedName)
.collect(Collectors.toList()));
}
}

public Map<String, Set<String>> getUpstreamDependencies() {
return upstreamDependencies;
}

public Map<String, Set<String>> getDownstreamDependencies() {
return downstreamDependencies;
}
}
@@ -0,0 +1,17 @@
package test.dependent.issue893;

import org.testng.annotations.Test;

public class MultiLevelDependenciesTestClassSample {
@Test
public void grandFather() {}

@Test(dependsOnMethods = "grandFather")
public void father() {}

@Test(dependsOnMethods = "grandFather")
public void mother() {}

@Test(dependsOnMethods = {"father", "mother"})
public void child() {}
}
@@ -0,0 +1,15 @@
package test.dependent.issue893;

import org.testng.annotations.Test;

public class TestClassSample {

@Test
public void independentTest() {}

@Test(dependsOnMethods = "independentTest")
public void dependentTest() {}

@Test(dependsOnMethods = "independentTest")
public void anotherDependentTest() {}
}

0 comments on commit fd465c6

Please sign in to comment.