Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support getting dependencies info for a test #2839

Merged
merged 1 commit into from
Nov 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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()) {
krmahadevan marked this conversation as resolved.
Show resolved Hide resolved
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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() {}
}
Original file line number Diff line number Diff line change
@@ -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() {}
}