package org.testng.internal;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.testng.IDynamicGraph;
import org.testng.IExecutionVisualiser;
import org.testng.collections.Lists;
import org.testng.collections.Maps;
import org.testng.collections.Sets;
public class DynamicGraph<T> implements IDynamicGraph<T> {
private final Set<T> m_nodesReady = Sets.newLinkedHashSet();
private final Set<T> m_nodesRunning = Sets.newLinkedHashSet();
private final Set<T> m_nodesFinished = Sets.newLinkedHashSet();
private final Edges<T> m_edges = new Edges<>();
private Set<IExecutionVisualiser> visualisers = Sets.newHashSet();
public boolean addNode(T node) {
return m_nodesReady.add(node);
}
public void addEdge(int weight, T from, T to) {
m_edges.addEdge(weight, from, to, false);
}
public void setVisualisers(Set<IExecutionVisualiser> listener) {
visualisers = listener;
}
public void addEdges(int weight, T from, Iterable<T> tos) {
for (T to : tos) {
addEdge(weight, from, to);
}
}
public List<T> getFreeNodes() {
Set<T> free = Sets.newLinkedHashSet(m_nodesReady);
free.removeAll(m_edges.fromNodes());
if (free.isEmpty() && m_nodesRunning.isEmpty()) {
int lowestWeight = m_edges.getLowestEdgeWeight(m_nodesReady);
for (T node : m_nodesReady) {
if (m_edges.hasAllEdgesWithWeight(node, lowestWeight)) {
free.add(node);
}
}
}
List<T> finalResult = Lists.newArrayList();
for (T node : free) {
Map<T, Integer> edges = m_edges.from(node);
if (edges == null || Collections.disjoint(edges.keySet(), free)) {
finalResult.add(node);
}
}
return finalResult;
}
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());
}
public void setStatus(Collection<T> nodes, Status status) {
for (T n : nodes) {
setStatus(n, status);
}
}
public void setStatus(T node, Status status) {
switch (status) {
case RUNNING:
m_nodesReady.remove(node);
m_nodesRunning.add(node);
break;
case FINISHED:
m_nodesReady.remove(node);
m_nodesRunning.remove(node);
m_nodesFinished.add(node);
Map<T, Integer> outgoingEdges = m_edges.from(node);
Map<T, Integer> incomingEdges = m_edges.to(node);
if (outgoingEdges != null && incomingEdges != null) {
for (Map.Entry<T, Integer> out : outgoingEdges.entrySet()) {
for (Map.Entry<T, Integer> in : incomingEdges.entrySet()) {
if (in.getKey() == out.getKey()) {
continue;
} else if (in.getValue() > out.getValue()) {
continue;
}
int weight = Math.max(in.getValue(), out.getValue());
m_edges.addEdge(weight, in.getKey(), out.getKey(), true);
}
}
}
m_edges.removeNode(node);
break;
case READY:
m_nodesReady.add(node);
m_nodesRunning.remove(node);
break;
default:
throw new IllegalArgumentException("Unsupported status: " + status);
}
this.visualisers.forEach(visualiser -> visualiser.consumeDotDefinition(toDot()));
}
public int getNodeCount() {
return m_nodesReady.size() + m_nodesRunning.size() + m_nodesFinished.size();
}
public int getNodeCountWithStatus(Status status) {
return getNodesWithStatus(status).size();
}
public Set<T> getNodesWithStatus(Status status) {
switch (status) {
case READY:
return m_nodesReady;
case RUNNING:
return m_nodesRunning;
case FINISHED:
return m_nodesFinished;
default:
throw new IllegalArgumentException();
}
}
@Override
public String toString() {
return "[DynamicGraph "
+ "\n Ready:"
+ m_nodesReady
+ "\n Running:"
+ m_nodesRunning
+ "\n Finished:"
+ m_nodesFinished
+ "\n Edges:\n"
+ m_edges
+ "]";
}
private static <T> String dotShortName(T t) {
String s = t.toString();
int n2 = s.indexOf('(');
return s.substring(0, n2).replaceAll("\\Q.\\E", "_");
}
public String toDot() {
String FREE = "[style=filled color=yellow]";
String RUNNING = "[style=filled color=green]";
String FINISHED = "[style=filled color=grey]";
StringBuilder result = new StringBuilder("digraph g {\n");
List<T> freeNodes = getFreeNodes();
String color;
for (T n : m_nodesReady) {
color = freeNodes.contains(n) ? FREE : "";
result.append(" ").append(dotShortName(n)).append(color).append("\n");
}
for (T n : m_nodesRunning) {
color = freeNodes.contains(n) ? FREE : RUNNING;
result.append(" ").append(dotShortName(n)).append(color).append("\n");
}
for (T n : m_nodesFinished) {
result.append(" ").append(dotShortName(n)).append(FINISHED).append("\n");
}
m_edges.appendDotEdges(result, m_nodesFinished);
result.append("}\n");
return result.toString();
}
Map<T, Map<T, Integer>> getEdges() {
return m_edges.getEdges();
}
private static class Edges<T> {
private final Map<T, Map<T, Integer>> m_incomingEdges = new HashMap<>();
private final Map<T, Map<T, Integer>> m_outgoingEdges = new HashMap<>();
public void addEdge(int weight, T from, T to, boolean ignoreCycles) {
if (from.equals(to)) {
return;
}
Integer reversedEdgeWeight = findReversedEdge(from, to);
if (reversedEdgeWeight != null && reversedEdgeWeight == weight) {
if (!ignoreCycles) {
throw new IllegalStateException("Circular dependency: " + from + " <-> " + to);
} else {
return;
}
}
addEdgeToMap(m_incomingEdges, to, from, weight);
addEdgeToMap(m_outgoingEdges, from, to, weight);
}
Set<T> fromNodes() {
return m_outgoingEdges.keySet();
}
Map<T, Integer> from(T node) {
Map<T, Integer> edges = m_outgoingEdges.get(node);
return edges == null ? null : Collections.unmodifiableMap(edges);
}
Map<T, Integer> to(T node) {
Map<T, Integer> edges = m_incomingEdges.get(node);
return edges == null ? null : Collections.unmodifiableMap(edges);
}
private Integer findReversedEdge(T from, T to) {
Map<T, Integer> edges = m_outgoingEdges.get(to);
return edges == null ? null : edges.get(from);
}
void removeNode(T node) {
Map<T, Integer> outgoingEdges = m_outgoingEdges.remove(node);
if (outgoingEdges != null) {
removeEdgesFromMap(m_incomingEdges, outgoingEdges.keySet(), node);
}
Map<T, Integer> incomingEdges = m_incomingEdges.remove(node);
if (incomingEdges != null) {
removeEdgesFromMap(m_outgoingEdges, incomingEdges.keySet(), node);
}
}
int getLowestEdgeWeight(Set<T> nodes) {
if (nodes.isEmpty()) {
return 0;
}
Set<T> intersection = Sets.newHashSet(nodes);
intersection.retainAll(m_outgoingEdges.keySet());
if (intersection.isEmpty()) {
return 0;
}
int lowestWeight = Integer.MAX_VALUE;
for (T node : intersection) {
Map<T, Integer> weightMap = m_outgoingEdges.get(node);
lowestWeight = Math.min(lowestWeight, Collections.min(weightMap.values()));
}
return lowestWeight;
}
boolean hasAllEdgesWithWeight(T node, int level) {
Map<T, Integer> weights = m_outgoingEdges.get(node);
if (weights == null) {
return true;
}
for (int weight : weights.values()) {
if (weight != level) {
return false;
}
}
return true;
}
private static <T> void removeEdgesFromMap(
Map<T, Map<T, Integer>> map, Collection<T> nodes, T node) {
for (T k : nodes) {
Map<T, Integer> edges = map.get(k);
if (edges == null) {
throw new IllegalStateException("Edge not found in map.");
}
edges.remove(node);
if (edges.isEmpty()) {
map.remove(k);
}
}
}
private static <T> void addEdgeToMap(Map<T, Map<T, Integer>> map, T n1, T n2, int weight) {
Map<T, Integer> edges = map.computeIfAbsent(n1, k -> new HashMap<>());
Integer existingWeight = edges.get(n2);
edges.put(n2, Math.max(weight, existingWeight != null ? existingWeight : Integer.MIN_VALUE));
}
Map<T, Map<T, Integer>> getEdges() {
Map<T, Map<T, Integer>> edges = Maps.newHashMap();
for (Map.Entry<T, Map<T, Integer>> es : m_outgoingEdges.entrySet()) {
edges.put(es.getKey(), Collections.unmodifiableMap(es.getValue()));
}
return Collections.unmodifiableMap(edges);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (Map.Entry<T, Map<T, Integer>> es : m_outgoingEdges.entrySet()) {
sb.append(" ").append(es.getKey()).append("\n");
for (Map.Entry<T, Integer> to : es.getValue().entrySet()) {
sb.append(" (")
.append(to.getValue())
.append(") ")
.append(to.getKey())
.append("\n");
}
}
return sb.toString();
}
void appendDotEdges(StringBuilder sb, Set<T> finished) {
for (Map.Entry<T, Map<T, Integer>> es : m_outgoingEdges.entrySet()) {
T from = es.getKey();
for (T to : es.getValue().keySet()) {
String dotted = finished.contains(from) ? "style=dotted" : "";
sb.append(" ")
.append(dotShortName(from))
.append(" -> ")
.append(dotShortName(to))
.append(" [dir=back ")
.append(dotted)
.append("]\n");
}
}
}
}
}