Improves topological sort

This can be used for mod sorting, dependencies between registries, etc.
e.g. https://github.com/MinecraftForge/MinecraftForge/pull/4694#issuecomment-412520302

New features:
Now accepts guava graph
Performance improvement: no longer reverse the graph; changed dfs to bfs
Accepets a comparator for secondary order, e.g. natural order, index by map
Now properly reports all cycles in a graph with Tarjan's strongly connected component algorithm
Adds a test to prove the validity of the sort and cycle detection
Modified build.gradle for test source directory and dependencies

Mod loading changes:
Sort mod file info instead of suppliers (we don't have suppliers instances)
Moves cycle error reporting out of topological sort and into mod sorter
Prevent mod file dependencies between mods that share the same file

Signed-off-by: liach <liach@users.noreply.github.com>
This commit is contained in:
liach 2019-01-01 16:42:56 -08:00 committed by cpw
parent 040cc4bf16
commit c01b336095
7 changed files with 452 additions and 249 deletions

View file

@ -112,6 +112,17 @@ project(':forge') {
srcDir "$rootDir/src/main/resources"
}
}
test {
compileClasspath += sourceSets.fmllauncher.runtimeClasspath
runtimeClasspath += sourceSets.fmllauncher.runtimeClasspath
java {
// srcDir "$rootDir/src/test/java" TODO fix later
srcDir "$rootDir/src/fmllaunchertest/java"
}
resources {
srcDir "$rootDir/src/test/resources"
}
}
userdev {
compileClasspath += sourceSets.main.runtimeClasspath
runtimeClasspath += sourceSets.main.runtimeClasspath
@ -273,6 +284,9 @@ project(':forge') {
installer 'org.apache.logging.log4j:log4j-core:2.11.1'
fmllauncherImplementation 'com.google.guava:guava:21.0'
fmllauncherImplementation 'com.google.code.gson:gson:2.8.0'
testImplementation "org.junit.jupiter:junit-jupiter-api:5.0.0"
testImplementation "org.opentest4j:opentest4j:1.0.0" // needed for junit 5
testImplementation "org.hamcrest:hamcrest-all:1.3" // needs advanced matching for list order
}
task runclient(type: JavaExec, dependsOn: [":forge:downloadAssets", ":forge:extractNatives"]) {

View file

@ -19,19 +19,30 @@
package net.minecraftforge.fml.loading;
import net.minecraftforge.fml.loading.toposort.TopologicalSort;
import com.google.common.graph.GraphBuilder;
import com.google.common.graph.MutableGraph;
import net.minecraftforge.forgespi.language.IModFileInfo;
import net.minecraftforge.forgespi.language.IModInfo;
import net.minecraftforge.fml.loading.EarlyLoadingException.ExceptionData;
import net.minecraftforge.fml.loading.moddiscovery.ModFile;
import net.minecraftforge.fml.loading.moddiscovery.ModFileInfo;
import net.minecraftforge.fml.loading.moddiscovery.ModInfo;
import net.minecraftforge.fml.loading.toposort.CyclePresentException;
import net.minecraftforge.fml.loading.toposort.TopologicalSort;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.util.StringBuilderFormattable;
import org.apache.maven.artifact.versioning.ArtifactVersion;
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
import java.util.*;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -71,35 +82,53 @@ public class ModSorter
private void sort()
{
final TopologicalSort.DirectedGraph<Supplier<ModFileInfo>> topoGraph = new TopologicalSort.DirectedGraph<>();
modFiles.stream().map(ModFile::getModFileInfo).map(ModFileInfo.class::cast).forEach(mi -> topoGraph.addNode(() -> mi));
// lambdas are identity based, so sorting them is impossible unless you hold reference to them
final MutableGraph<ModFileInfo> graph = GraphBuilder.directed().build();
AtomicInteger counter = new AtomicInteger();
Map<IModFileInfo, Integer> infos = modFiles.stream().map(ModFile::getModFileInfo).collect(Collectors.toMap(Function.identity(), (e) -> counter.incrementAndGet()));
modFiles.stream().map(ModFile::getModInfos).flatMap(Collection::stream).
map(IModInfo::getDependencies).flatMap(Collection::stream).
forEach(dep -> addDependency(topoGraph, dep));
final List<Supplier<ModFileInfo>> sorted;
forEach(dep -> addDependency(graph, dep));
final List<ModFileInfo> sorted;
try
{
sorted = TopologicalSort.topologicalSort(topoGraph);
sorted = TopologicalSort.topologicalSort(graph, Comparator.comparing(infos::get));
}
catch (TopologicalSort.TopoSortException e)
catch (CyclePresentException e)
{
TopologicalSort.TopoSortException.TopoSortExceptionData<Supplier<ModInfo>> data = e.getData();
LOGGER.error(LOADING, ()-> data);
throw new EarlyLoadingException("Sorting error", e, data.toExceptionData(mi-> mi.get().getModId()));
Set<Set<ModFileInfo>> cycles = e.getCycles();
LOGGER.error(LOADING, () -> ((StringBuilderFormattable) (buffer -> {
buffer.append("Mod Sorting failed.\n");
buffer.append("Detected Cycles: ");
buffer.append(cycles);
buffer.append('\n');
})));
List<ExceptionData> dataList = cycles.stream()
.map(Set::stream)
.map(stream -> stream
.flatMap(modFileInfo -> modFileInfo.getMods().stream()
.map(IModInfo::getModId)).collect(Collectors.toList()))
.map(list -> new ExceptionData("fml.modloading.cycle", list))
.collect(Collectors.toList());
throw new EarlyLoadingException("Sorting error", e, dataList);
}
this.sortedList = sorted.stream().map(Supplier::get).map(ModFileInfo::getMods).
this.sortedList = sorted.stream().map(ModFileInfo::getMods).
flatMap(Collection::stream).map(ModInfo.class::cast).collect(Collectors.toList());
this.modFiles = sorted.stream().map(Supplier::get).map(ModFileInfo::getFile).collect(Collectors.toList());
this.modFiles = sorted.stream().map(ModFileInfo::getFile).collect(Collectors.toList());
}
private void addDependency(TopologicalSort.DirectedGraph<Supplier<ModFileInfo>> topoGraph,IModInfo.ModVersion dep)
private void addDependency(MutableGraph<ModFileInfo> topoGraph, IModInfo.ModVersion dep)
{
ModFileInfo self = (ModFileInfo)dep.getOwner().getOwningFile();
ModFileInfo target = modIdNameLookup.get(dep.getModId()).getOwningFile();
if (self == target)
return; // in case a jar has two mods that have dependencies between
switch (dep.getOrdering()) {
case BEFORE:
topoGraph.addEdge(()->(ModFileInfo)dep.getOwner().getOwningFile(), ()->modIdNameLookup.get(dep.getModId()).getOwningFile());
topoGraph.putEdge(self, target);
break;
case AFTER:
topoGraph.addEdge(()->modIdNameLookup.get(dep.getModId()).getOwningFile(), ()->(ModFileInfo)dep.getOwner().getOwningFile());
topoGraph.putEdge(target, self);
break;
case NONE:
break;

View file

@ -0,0 +1,52 @@
/*
* Minecraft Forge
* Copyright (c) 2016-2018.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation version 2.1
* of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package net.minecraftforge.fml.loading.toposort;
import java.util.Set;
/**
* An exception thrown for graphs with cycles as an argument for topological sort.
*/
public final class CyclePresentException extends IllegalArgumentException {
private final Set<Set<?>> cycles;
/**
* Creates the exception.
*
* @param cycles the cycles present
*/
CyclePresentException(Set<Set<?>> cycles) {
this.cycles = cycles;
}
/**
* Accesses the cycles present in the sorted graph.
*
* <p>Each element in the outer set represents a cycle; each cycle, or the inner set,
* forms a strongly connected component with two or more elements.
*
* @param <T> the type of node sorted
* @return the cycles identified
*/
@SuppressWarnings("unchecked")
public <T> Set<Set<T>> getCycles() {
return (Set<Set<T>>) (Set<?>) cycles;
}
}

View file

@ -0,0 +1,123 @@
/*
* Minecraft Forge
* Copyright (c) 2016-2018.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation version 2.1
* of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package net.minecraftforge.fml.loading.toposort;
import com.google.common.graph.Graph;
import java.util.BitSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* An object that splits a graph into strongly connected components lazily with
* Tarjan's Strongly Connected Components Algorithm.
*
* <p>This algorithm allows to detect all cycles in dependencies that prevent topological
* sorting.
*
* <p>This detector evaluates the graph lazily and won't reflect the modifications in the
* graph after initial evaluation.
*/
public class StronglyConnectedComponentDetector<T> {
private final Graph<T> graph;
private Map<T, Integer> ids;
private T[] elements;
private int[] dfn;
private int[] low;
private int[] stack;
private int top;
private BitSet onStack;
private Set<Set<T>> components;
public StronglyConnectedComponentDetector(Graph<T> graph) {
this.graph = graph;
}
public Set<Set<T>> getComponents() {
if (components == null) {
calculate();
}
return components;
}
@SuppressWarnings("unchecked")
private void calculate() {
components = new HashSet<>();
int t = 0;
ids = new HashMap<>();
Set<T> nodes = graph.nodes();
elements = (T[]) new Object[nodes.size()];
for (T node : nodes) {
ids.put(node, t);
elements[t] = node;
t++;
}
final int n = nodes.size();
dfn = new int[n];
low = new int[n];
stack = new int[n];
onStack = new BitSet(n);
top = -1;
for (int i = 0; i < n; i++) {
if (dfn[i] == 0) {
dfs(i, 1);
}
}
}
private void dfs(int now, int depth) {
dfn[now] = depth;
low[now] = depth;
top++;
stack[top] = now;
onStack.set(now);
for (T each : graph.successors(elements[now])) {
int to = ids.get(each);
if (dfn[to] != 0) {
if (low[now] > dfn[to]) {
low[now] = dfn[to];
}
} else {
dfs(to, depth + 1);
if (low[now] > low[to]) {
low[now] = low[to];
}
}
}
if (dfn[now] == low[now]) {
Set<T> component = new HashSet<>();
while (top >= 0) {
final int t = stack[top];
component.add(elements[t]);
onStack.clear(t);
top--;
if (t == now) {
break;
}
}
components.add(component);
}
}
}

View file

@ -19,257 +19,101 @@
package net.minecraftforge.fml.loading.toposort;
import com.google.common.collect.Sets;
import net.minecraftforge.fml.loading.EarlyLoadingException;
import org.apache.logging.log4j.message.Message;
import org.apache.logging.log4j.util.StringBuilderFormattable;
import com.google.common.base.Preconditions;
import com.google.common.graph.Graph;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
/**
* Topological sort for mod loading
*
* Based on a variety of sources, including http://keithschwarz.com/interesting/code/?dir=topological-sort
* @author cpw
* Provides a topological sort algorithm.
*
* <p>While this algorithm is used for mod loading in forge, it can be
* utilized in other fashions, e.g. topology-based registry loading, prioritization
* for renderers, and even mod module loading.
*/
public class TopologicalSort
{
public static class DirectedGraph<T> implements Iterable<T>
{
private final Map<T, SortedSet<T>> graph = new HashMap<>();
private List<T> orderedNodes = new ArrayList<>();
public boolean addNode(T node)
{
// Ignore nodes already added
if (graph.containsKey(node))
{
return false;
}
orderedNodes.add(node);
graph.put(node, new TreeSet<>(Comparator.comparingInt(o -> orderedNodes.indexOf(o))));
return true;
}
public void addEdge(T from, T to)
{
if (!(graph.containsKey(from) && graph.containsKey(to)))
{
throw new NoSuchElementException("Missing nodes from graph");
}
graph.get(from).add(to);
}
public void removeEdge(T from, T to)
{
if (!(graph.containsKey(from) && graph.containsKey(to)))
{
throw new NoSuchElementException("Missing nodes from graph");
}
graph.get(from).remove(to);
}
public boolean edgeExists(T from, T to)
{
if (!(graph.containsKey(from) && graph.containsKey(to)))
{
throw new NoSuchElementException("Missing nodes from graph");
}
return graph.get(from).contains(to);
}
public Set<T> edgesFrom(T from)
{
if (!graph.containsKey(from))
{
throw new NoSuchElementException("Missing node from graph");
}
return Collections.unmodifiableSortedSet(graph.get(from));
}
@Override
public Iterator<T> iterator()
{
return orderedNodes.iterator();
}
public int size()
{
return graph.size();
}
public boolean isEmpty()
{
return graph.isEmpty();
}
@Override
public String toString()
{
return graph.toString();
}
}
public final class TopologicalSort {
/**
* Sort the input graph into a topologically sorted list
* A breath-first-search based topological sort.
*
* Uses the reverse depth first search as outlined in ...
* @param graph
* @return The sorted mods list.
* <p>Compared to the depth-first-search version, it does not reverse the graph
* and supports custom secondary ordering specified by a comparator. It also utilizes the
* recently introduced Guava Graph API, which is more straightforward than the old directed
* graph.
*
* <p>The graph to sort must be directed, must not allow self loops, and must not contain
* cycles. {@link IllegalArgumentException} will be thrown otherwise.
*
* <p>When {@code null} is used for the comparator and multiple nodes have no
* prerequisites, the order depends on the iteration order of the set returned by the
* {@link Graph#successors(Object)} call, which is random by default.
*
* <p>Given the number of edges {@code E} and the number of vertexes {@code V},
* the time complexity of a sort without a secondary comparator is {@code O(E + V)}.
* With a secondary comparator of time complexity {@code O(T)}, the overall time
* complexity would be {@code O(E + TV log(V))}. As a result, the comparator should
* be as efficient as possible.
*
* <p>Examples of topological sort usage can be found in Forge test code.
*
* @param graph the graph to sort
* @param comparator the secondary comparator, may be null
* @param <T> the node type of the graph
* @return the ordered nodes from the graph
* @throws IllegalArgumentException if the graph is undirected or allows self loops
* @throws CyclePresentException if the graph contains cycles
*/
public static <T> List<T> topologicalSort(DirectedGraph<T> graph)
{
DirectedGraph<T> rGraph = reverse(graph);
List<T> sortedResult = new ArrayList<>();
Set<T> visitedNodes = new HashSet<>();
// A list of "fully explored" nodes. Leftovers in here indicate cycles in the graph
Set<T> expandedNodes = new HashSet<>();
public static <T> List<T> topologicalSort(Graph<T> graph, @Nullable Comparator<? super T> comparator) throws IllegalArgumentException {
Preconditions.checkArgument(graph.isDirected(), "Cannot topologically sort an undirected graph!");
Preconditions.checkArgument(!graph.allowsSelfLoops(), "Cannot topologically sort a graph with self loops!");
for (T node : rGraph)
{
explore(node, rGraph, sortedResult, visitedNodes, expandedNodes);
}
final Queue<T> queue = comparator == null ? new ArrayDeque<>() : new PriorityQueue<>(comparator);
final Map<T, Integer> degrees = new HashMap<>();
final List<T> results = new ArrayList<>();
return sortedResult;
}
public static <T> DirectedGraph<T> reverse(DirectedGraph<T> graph)
{
DirectedGraph<T> result = new DirectedGraph<>();
for (T node : graph)
{
result.addNode(node);
}
for (T from : graph)
{
for (T to : graph.edgesFrom(from))
{
result.addEdge(to, from);
for (final T node : graph.nodes()) {
final int degree = graph.inDegree(node);
if (degree == 0) {
queue.add(node);
} else {
degrees.put(node, degree);
}
}
return result;
while (!queue.isEmpty()) {
final T current = queue.remove();
results.add(current);
for (final T successor : graph.successors(current)) {
final int updated = degrees.compute(successor, (node, degree) -> Objects.requireNonNull(degree, () -> "Invalid degree present for " + node) - 1);
if (updated == 0) {
queue.add(successor);
degrees.remove(successor);
}
private static <T> void explore(T node, DirectedGraph<T> graph, List<T> sortedResult, Set<T> visitedNodes, Set<T> expandedNodes)
{
// Have we been here before?
if (visitedNodes.contains(node))
{
// And have completed this node before
if (expandedNodes.contains(node))
{
// Then we're fine
return;
}
throw new TopoSortException(new TopoSortException.TopoSortExceptionData<>(node, sortedResult, visitedNodes, expandedNodes));
}
// Visit this node
visitedNodes.add(node);
// Recursively explore inbound edges
for (T inbound : graph.edgesFrom(node))
{
explore(inbound, graph, sortedResult, visitedNodes, expandedNodes);
}
// Add ourselves now
sortedResult.add(node);
// And mark ourselves as explored
expandedNodes.add(node);
}
public static class TopoSortException extends RuntimeException {
private final TopoSortExceptionData<?> topoSortData;
public static class TopoSortExceptionData<T> implements Message, StringBuilderFormattable
{
private final List<T> sortedResult;
private final Set<T> visitedNodes;
private final Set<T> expandedNodes;
private final T node;
TopoSortExceptionData(T node, List<T> sortedResult, Set<T> visitedNodes, Set<T> expandedNodes)
{
this.node = node;
this.sortedResult = sortedResult;
this.visitedNodes = visitedNodes;
this.expandedNodes = expandedNodes;
}
@Override
public String getFormattedMessage()
{
return "";
}
@Override
public String getFormat()
{
return "";
}
@Override
public Object[] getParameters()
{
return new Object[0];
}
@Override
public Throwable getThrowable()
{
return null;
}
@Override
public void formatTo(StringBuilder buffer)
{
buffer.append("Mod Sorting failed.\n");
buffer.append("Visiting node {}\n").append(String.valueOf(node));
buffer.append("Current sorted list : {}\n").append(String.valueOf(sortedResult));
buffer.append("Visited set for this node : {}\n").append(String.valueOf(visitedNodes));
buffer.append("Explored node set : {}\n").append(expandedNodes);
buffer.append("Likely cycle is in : {}\n").append(Sets.difference(visitedNodes, expandedNodes));
}
public List<EarlyLoadingException.ExceptionData> toExceptionData(Function<T, String> nodeMapper) {
return Collections.singletonList(
new EarlyLoadingException.ExceptionData("fml.messages.cycleproblem",
nodeMapper.apply(node),
visitedNodes.stream().map(nodeMapper).collect(Collectors.joining(","))));
}
}
public <T> TopoSortException(TopoSortExceptionData<T> data)
{
this.topoSortData = data;
if (!degrees.isEmpty()) {
Set<Set<T>> components = new StronglyConnectedComponentDetector<>(graph).getComponents();
components.removeIf(set -> set.size() < 2);
throwCyclePresentException(components);
}
@SuppressWarnings("unchecked")
public <T> TopoSortExceptionData<T> getData() {
return (TopoSortExceptionData<T>)this.topoSortData;
}
return results;
}
@SuppressWarnings("unchecked") // for unchecked annotation
private static <T> void throwCyclePresentException(Set<Set<T>> components) {
throw new CyclePresentException((Set<Set<?>>) (Set<?>) components);
}
}

View file

@ -0,0 +1,140 @@
/*
* Minecraft Forge
* Copyright (c) 2016-2018.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation version 2.1
* of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package net.minecraftforge.fml.test;
import com.google.common.graph.GraphBuilder;
import com.google.common.graph.MutableGraph;
import net.minecraftforge.fml.loading.toposort.CyclePresentException;
import net.minecraftforge.fml.loading.toposort.StronglyConnectedComponentDetector;
import net.minecraftforge.fml.loading.toposort.TopologicalSort;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
/**
* Tests for topological sort.
*/
public class TopologicalSortTests {
@Test
@DisplayName("strongly connected components")
@SuppressWarnings("unchecked")
void testScc() {
MutableGraph<Integer> graph = GraphBuilder.directed().build();
graph.putEdge(2, 4);
graph.putEdge(4, 7);
graph.putEdge(4, 6);
graph.putEdge(6, 1);
graph.putEdge(3, 4);
graph.putEdge(1, 4);
graph.putEdge(3, 1);
Set<Set<Integer>> components = new StronglyConnectedComponentDetector<>(graph).getComponents();
assertThat(components, containsInAnyOrder(contains(2), contains(3), contains(7), containsInAnyOrder(1, 4, 6)));
}
@Test
@DisplayName("strongly connected components 2")
@SuppressWarnings("unchecked")
void testScc2() {
MutableGraph<Integer> graph = GraphBuilder.directed().build();
graph.putEdge(2, 4);
graph.putEdge(4, 8);
graph.putEdge(8, 2);
graph.putEdge(2, 1);
graph.putEdge(2, 3);
graph.putEdge(1, 9);
graph.putEdge(9, 7);
graph.putEdge(7, 5);
graph.putEdge(5, 1);
graph.putEdge(5, 3);
graph.putEdge(3, 6);
graph.putEdge(6, 5);
graph.putEdge(9, 3);
Set<Set<Integer>> components = new StronglyConnectedComponentDetector<>(graph).getComponents();
assertThat(components, containsInAnyOrder(containsInAnyOrder(1, 3, 5, 6, 7, 9), containsInAnyOrder(2, 4, 8)));
}
@Test
@DisplayName("basic sort")
void testBasicSort() {
MutableGraph<Integer> graph = GraphBuilder.directed().build();
graph.putEdge(2, 4);
graph.putEdge(4, 7);
graph.putEdge(4, 6);
graph.putEdge(3, 4);
graph.putEdge(1, 4);
graph.putEdge(3, 1);
List<Integer> list = TopologicalSort.topologicalSort(graph, Comparator.naturalOrder());
assertThat(list, contains(2, 3, 1, 4, 6, 7));
}
@Test
@DisplayName("basic sort 2")
void testBasicSort2() {
MutableGraph<Integer> graph = GraphBuilder.directed().build();
graph.putEdge(7, 6);
graph.putEdge(8, 6);
graph.putEdge(8, 2);
graph.putEdge(12, 2);
graph.putEdge(2, 6);
graph.putEdge(6, 1);
graph.putEdge(2, 4);
graph.putEdge(2, 1);
graph.putEdge(1, 4);
graph.putEdge(1, 3);
graph.putEdge(4, 5);
graph.putEdge(1, 5);
graph.putEdge(5, 3);
graph.putEdge(1, 10);
graph.putEdge(3, 11);
graph.putEdge(5, 9);
graph.putEdge(11, 9);
graph.putEdge(11, 13);
graph.putEdge(10, 13);
List<Integer> list = TopologicalSort.topologicalSort(graph, Collections.reverseOrder());
assertThat(list, contains(12, 8, 7, 2, 6, 1, 10, 4, 5, 3, 11, 13, 9));
}
@Test
@DisplayName("loop sort")
void testLoopSort() {
MutableGraph<Integer> graph = GraphBuilder.directed().build();
graph.putEdge(2, 4);
graph.putEdge(4, 7);
graph.putEdge(4, 6);
graph.putEdge(6, 1);
graph.putEdge(3, 4);
graph.putEdge(1, 4);
graph.putEdge(3, 1);
CyclePresentException ex = Assertions.assertThrows(CyclePresentException.class, () -> {
TopologicalSort.topologicalSort(graph, Comparator.naturalOrder());
});
assertThat(ex.getCycles(), contains(containsInAnyOrder(1, 4, 6)));
}
}

View file

@ -25,6 +25,7 @@
"fml.modloading.errorduringevent":"{0,modinfo,name} ({0,modinfo,id}) encountered an error during the {1,lower} event phase\n\u00a77{2,exc,msg}",
"fml.modloading.failedtoloadforge": "Failed to load forge",
"fml.modloading.missingdependency": "Mod \u00a7e{4}\u00a7r requires \u00a76{3}\u00a7r \u00a7o{5,vr}\u00a7r\n\u00a77Currently, \u00a76{3}\u00a7r\u00a77 is \u00a7o{6,i18n,fml.messages.artifactversion.ornotinstalled}",
"fml.modloading.cycle": "Detected a mod dependency cycle: {0}",
"fml.modloading.failedtoprocesswork":"{0,modinfo,name} ({0,modinfo,id}) encountered an error processing deferred work\n\u00a77{2,exc,msg}",
"fml.messages.artifactversion.ornotinstalled":"{0,ornull,fml.messages.artifactversion.notinstalled}",