From 88bb42b1e577ac0853f58d697abe14fb9936e387 Mon Sep 17 00:00:00 2001 From: Krishnan Mahadevan Date: Mon, 28 Nov 2022 16:18:32 +0530 Subject: [PATCH] Support getting dependencies info for a test Closes #893 --- CHANGES.txt | 1 + .../main/java/org/testng/IDynamicGraph.java | 4 ++ .../main/java/org/testng/ITestNGMethod.java | 20 ++++++ .../src/main/java/org/testng/TestRunner.java | 12 ++++ .../org/testng/internal/BaseTestMethod.java | 30 +++++++++ .../org/testng/internal/DynamicGraph.java | 18 ++++-- .../java/test/dependent/DependentTest.java | 62 +++++++++++++++++++ .../issue893/DependencyTrackingListener.java | 44 +++++++++++++ ...MultiLevelDependenciesTestClassSample.java | 17 +++++ .../dependent/issue893/TestClassSample.java | 15 +++++ 10 files changed, 218 insertions(+), 5 deletions(-) create mode 100644 testng-core/src/test/java/test/dependent/issue893/DependencyTrackingListener.java create mode 100644 testng-core/src/test/java/test/dependent/issue893/MultiLevelDependenciesTestClassSample.java create mode 100644 testng-core/src/test/java/test/dependent/issue893/TestClassSample.java diff --git a/CHANGES.txt b/CHANGES.txt index 6302c9196b..86593d9e75 100644 --- a/CHANGES.txt +++ b/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) diff --git a/testng-core-api/src/main/java/org/testng/IDynamicGraph.java b/testng-core-api/src/main/java/org/testng/IDynamicGraph.java index b0734a2c47..269d7ed9be 100644 --- a/testng-core-api/src/main/java/org/testng/IDynamicGraph.java +++ b/testng-core-api/src/main/java/org/testng/IDynamicGraph.java @@ -21,6 +21,10 @@ public interface IDynamicGraph { List getFreeNodes(); + default List getUpstreamDependenciesFor(T node) { + throw new UnsupportedOperationException("Pending implementation"); + } + List getDependenciesFor(T node); void setStatus(Collection nodes, Status status); 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 98663322e2..e045e29fdd 100644 --- a/testng-core-api/src/main/java/org/testng/ITestNGMethod.java +++ b/testng-core-api/src/main/java/org/testng/ITestNGMethod.java @@ -1,7 +1,9 @@ package org.testng; +import java.util.Collections; 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; @@ -71,6 +73,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 downstreamDependencies() { + return Collections.emptySet(); + } + + /** + * @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 upstreamDependencies() { + return Collections.emptySet(); + } + void addMethodDependedUpon(String methodName); /** @return true if this method was annotated with @Test */ diff --git a/testng-core/src/main/java/org/testng/TestRunner.java b/testng-core/src/main/java/org/testng/TestRunner.java index 7705db831d..0d34d51957 100644 --- a/testng-core/src/main/java/org/testng/TestRunner.java +++ b/testng-core/src/main/java/org/testng/TestRunner.java @@ -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; @@ -746,6 +747,17 @@ private void privateRun(XmlTest xmlTest) { }); IDynamicGraph 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 downstream = Sets.newHashSet(graph.getDependenciesFor(each)); + ((BaseTestMethod) each).setDownstreamDependencies(downstream); + Set 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 diff --git a/testng-core/src/main/java/org/testng/internal/BaseTestMethod.java b/testng-core/src/main/java/org/testng/internal/BaseTestMethod.java index c0cddaecf8..9e33a9715e 100644 --- a/testng-core/src/main/java/org/testng/internal/BaseTestMethod.java +++ b/testng-core/src/main/java/org/testng/internal/BaseTestMethod.java @@ -70,6 +70,8 @@ public abstract class BaseTestMethod implements ITestNGMethod, IInvocationStatus private long m_invocationTimeOut = 0L; private List m_invocationNumbers = Lists.newArrayList(); + private Set downstreamDependencies = Sets.newHashSet(); + private Set upstreamDependencies = Sets.newHashSet(); private final Collection m_failedInvocationNumbers = new ConcurrentLinkedQueue<>(); private long m_timeOut = 0; @@ -176,6 +178,34 @@ public String[] getMethodsDependedUpon() { return m_methodsDependedUpon; } + @Override + public Set downstreamDependencies() { + return Collections.unmodifiableSet(downstreamDependencies); + } + + @Override + public Set upstreamDependencies() { + return Collections.unmodifiableSet(upstreamDependencies); + } + + public void setDownstreamDependencies(Set methods) { + if (RuntimeBehavior.isMemoryFriendlyMode()) { + downstreamDependencies = + methods.stream().map(LiteWeightTestNGMethod::new).collect(Collectors.toSet()); + } else { + downstreamDependencies.addAll(methods); + } + } + + public void setUpstreamDependencies(Set methods) { + if (RuntimeBehavior.isMemoryFriendlyMode()) { + upstreamDependencies = + methods.stream().map(LiteWeightTestNGMethod::new).collect(Collectors.toSet()); + } else { + upstreamDependencies.addAll(methods); + } + } + /** {@inheritDoc} */ @Override public boolean isTest() { diff --git a/testng-core/src/main/java/org/testng/internal/DynamicGraph.java b/testng-core/src/main/java/org/testng/internal/DynamicGraph.java index 6e8d6564ef..77a4900eeb 100644 --- a/testng-core/src/main/java/org/testng/internal/DynamicGraph.java +++ b/testng-core/src/main/java/org/testng/internal/DynamicGraph.java @@ -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; @@ -76,12 +77,19 @@ public List getFreeNodes() { return finalResult; } + @Override + public List getUpstreamDependenciesFor(T node) { + return dependencies(m_edges.from(node)); + } + public List getDependenciesFor(T node) { - Map data = m_edges.to(node); - if (data == null) { - return Lists.newArrayList(); - } - return Lists.newArrayList(data.keySet()); + return dependencies(m_edges.to(node)); + } + + private List dependencies(Map dependencies) { + return Optional.ofNullable(dependencies) + .map(found -> Lists.newArrayList(found.keySet())) + .orElse(Lists.newArrayList()); } /** Set the status for a set of nodes. */ diff --git a/testng-core/src/test/java/test/dependent/DependentTest.java b/testng-core/src/test/java/test/dependent/DependentTest.java index 9c8d75ff8b..cc0cc51e6c 100644 --- a/testng-core/src/test/java/test/dependent/DependentTest.java +++ b/testng-core/src/test/java/test/dependent/DependentTest.java @@ -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; @@ -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 { @@ -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 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 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 asString = diff --git a/testng-core/src/test/java/test/dependent/issue893/DependencyTrackingListener.java b/testng-core/src/test/java/test/dependent/issue893/DependencyTrackingListener.java new file mode 100644 index 0000000000..4f4018d312 --- /dev/null +++ b/testng-core/src/test/java/test/dependent/issue893/DependencyTrackingListener.java @@ -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> downstreamDependencies = new HashMap<>(); + private final Map> 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> getUpstreamDependencies() { + return upstreamDependencies; + } + + public Map> getDownstreamDependencies() { + return downstreamDependencies; + } +} diff --git a/testng-core/src/test/java/test/dependent/issue893/MultiLevelDependenciesTestClassSample.java b/testng-core/src/test/java/test/dependent/issue893/MultiLevelDependenciesTestClassSample.java new file mode 100644 index 0000000000..151311310f --- /dev/null +++ b/testng-core/src/test/java/test/dependent/issue893/MultiLevelDependenciesTestClassSample.java @@ -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() {} +} diff --git a/testng-core/src/test/java/test/dependent/issue893/TestClassSample.java b/testng-core/src/test/java/test/dependent/issue893/TestClassSample.java new file mode 100644 index 0000000000..df0d15cddf --- /dev/null +++ b/testng-core/src/test/java/test/dependent/issue893/TestClassSample.java @@ -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() {} +}