From c01b336095e3d443b5a61eaaedacd60cc4cf5544 Mon Sep 17 00:00:00 2001 From: liach Date: Tue, 1 Jan 2019 16:42:56 -0800 Subject: [PATCH] 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 --- build.gradle | 14 + .../minecraftforge/fml/loading/ModSorter.java | 63 +++- .../toposort/CyclePresentException.java | 52 +++ .../StronglyConnectedComponentDetector.java | 123 +++++++ .../fml/loading/toposort/TopologicalSort.java | 308 +++++------------- .../fml/test/TopologicalSortTests.java | 140 ++++++++ .../resources/assets/forge/lang/en_us.json | 1 + 7 files changed, 452 insertions(+), 249 deletions(-) create mode 100644 src/fmllauncher/java/net/minecraftforge/fml/loading/toposort/CyclePresentException.java create mode 100644 src/fmllauncher/java/net/minecraftforge/fml/loading/toposort/StronglyConnectedComponentDetector.java create mode 100644 src/fmllaunchertest/java/net/minecraftforge/fml/test/TopologicalSortTests.java diff --git a/build.gradle b/build.gradle index b5b1348e0..3968d2033 100644 --- a/build.gradle +++ b/build.gradle @@ -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"]) { diff --git a/src/fmllauncher/java/net/minecraftforge/fml/loading/ModSorter.java b/src/fmllauncher/java/net/minecraftforge/fml/loading/ModSorter.java index d62261ab4..7647deffd 100644 --- a/src/fmllauncher/java/net/minecraftforge/fml/loading/ModSorter.java +++ b/src/fmllauncher/java/net/minecraftforge/fml/loading/ModSorter.java @@ -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> 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 graph = GraphBuilder.directed().build(); + AtomicInteger counter = new AtomicInteger(); + Map 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> sorted; + forEach(dep -> addDependency(graph, dep)); + final List sorted; try { - sorted = TopologicalSort.topologicalSort(topoGraph); + sorted = TopologicalSort.topologicalSort(graph, Comparator.comparing(infos::get)); } - catch (TopologicalSort.TopoSortException e) + catch (CyclePresentException e) { - TopologicalSort.TopoSortException.TopoSortExceptionData> data = e.getData(); - LOGGER.error(LOADING, ()-> data); - throw new EarlyLoadingException("Sorting error", e, data.toExceptionData(mi-> mi.get().getModId())); + Set> 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 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> topoGraph,IModInfo.ModVersion dep) + private void addDependency(MutableGraph 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; diff --git a/src/fmllauncher/java/net/minecraftforge/fml/loading/toposort/CyclePresentException.java b/src/fmllauncher/java/net/minecraftforge/fml/loading/toposort/CyclePresentException.java new file mode 100644 index 000000000..31edacf22 --- /dev/null +++ b/src/fmllauncher/java/net/minecraftforge/fml/loading/toposort/CyclePresentException.java @@ -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> cycles; + + /** + * Creates the exception. + * + * @param cycles the cycles present + */ + CyclePresentException(Set> cycles) { + this.cycles = cycles; + } + + /** + * Accesses the cycles present in the sorted graph. + * + *

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 the type of node sorted + * @return the cycles identified + */ + @SuppressWarnings("unchecked") + public Set> getCycles() { + return (Set>) (Set) cycles; + } +} diff --git a/src/fmllauncher/java/net/minecraftforge/fml/loading/toposort/StronglyConnectedComponentDetector.java b/src/fmllauncher/java/net/minecraftforge/fml/loading/toposort/StronglyConnectedComponentDetector.java new file mode 100644 index 000000000..10c8dd553 --- /dev/null +++ b/src/fmllauncher/java/net/minecraftforge/fml/loading/toposort/StronglyConnectedComponentDetector.java @@ -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. + * + *

This algorithm allows to detect all cycles in dependencies that prevent topological + * sorting. + * + *

This detector evaluates the graph lazily and won't reflect the modifications in the + * graph after initial evaluation. + */ +public class StronglyConnectedComponentDetector { + private final Graph graph; + private Map ids; + private T[] elements; + private int[] dfn; + private int[] low; + private int[] stack; + private int top; + private BitSet onStack; + private Set> components; + + public StronglyConnectedComponentDetector(Graph graph) { + this.graph = graph; + } + + public Set> getComponents() { + if (components == null) { + calculate(); + } + return components; + } + + @SuppressWarnings("unchecked") + private void calculate() { + components = new HashSet<>(); + int t = 0; + ids = new HashMap<>(); + Set 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 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); + } + } + +} diff --git a/src/fmllauncher/java/net/minecraftforge/fml/loading/toposort/TopologicalSort.java b/src/fmllauncher/java/net/minecraftforge/fml/loading/toposort/TopologicalSort.java index 93c2828a2..dac32956e 100644 --- a/src/fmllauncher/java/net/minecraftforge/fml/loading/toposort/TopologicalSort.java +++ b/src/fmllauncher/java/net/minecraftforge/fml/loading/toposort/TopologicalSort.java @@ -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. * + *

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 implements Iterable - { - private final Map> graph = new HashMap<>(); - private List 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 edgesFrom(T from) - { - if (!graph.containsKey(from)) - { - throw new NoSuchElementException("Missing node from graph"); - } - - return Collections.unmodifiableSortedSet(graph.get(from)); - } - @Override - public Iterator 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. + *

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. + * + *

The graph to sort must be directed, must not allow self loops, and must not contain + * cycles. {@link IllegalArgumentException} will be thrown otherwise. + * + *

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. + * + *

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. + * + *

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 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 List topologicalSort(DirectedGraph graph) - { - DirectedGraph rGraph = reverse(graph); - List sortedResult = new ArrayList<>(); - Set visitedNodes = new HashSet<>(); - // A list of "fully explored" nodes. Leftovers in here indicate cycles in the graph - Set expandedNodes = new HashSet<>(); + public static List topologicalSort(Graph graph, @Nullable Comparator 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 queue = comparator == null ? new ArrayDeque<>() : new PriorityQueue<>(comparator); + final Map degrees = new HashMap<>(); + final List results = new ArrayList<>(); + + for (final T node : graph.nodes()) { + final int degree = graph.inDegree(node); + if (degree == 0) { + queue.add(node); + } else { + degrees.put(node, degree); + } } - return sortedResult; + 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); + } + } + } + + if (!degrees.isEmpty()) { + Set> components = new StronglyConnectedComponentDetector<>(graph).getComponents(); + components.removeIf(set -> set.size() < 2); + throwCyclePresentException(components); + } + + return results; } - public static DirectedGraph reverse(DirectedGraph graph) - { - DirectedGraph result = new DirectedGraph<>(); - - for (T node : graph) - { - result.addNode(node); - } - - for (T from : graph) - { - for (T to : graph.edgesFrom(from)) - { - result.addEdge(to, from); - } - } - - return result; - } - - private static void explore(T node, DirectedGraph graph, List sortedResult, Set visitedNodes, Set 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 implements Message, StringBuilderFormattable - { - private final List sortedResult; - private final Set visitedNodes; - private final Set expandedNodes; - private final T node; - - TopoSortExceptionData(T node, List sortedResult, Set visitedNodes, Set 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 toExceptionData(Function nodeMapper) { - return Collections.singletonList( - new EarlyLoadingException.ExceptionData("fml.messages.cycleproblem", - nodeMapper.apply(node), - visitedNodes.stream().map(nodeMapper).collect(Collectors.joining(",")))); - } - } - - public TopoSortException(TopoSortExceptionData data) - { - this.topoSortData = data; - } - - @SuppressWarnings("unchecked") - public TopoSortExceptionData getData() { - return (TopoSortExceptionData)this.topoSortData; - } + @SuppressWarnings("unchecked") // for unchecked annotation + private static void throwCyclePresentException(Set> components) { + throw new CyclePresentException((Set>) (Set) components); } } diff --git a/src/fmllaunchertest/java/net/minecraftforge/fml/test/TopologicalSortTests.java b/src/fmllaunchertest/java/net/minecraftforge/fml/test/TopologicalSortTests.java new file mode 100644 index 000000000..57c3088e0 --- /dev/null +++ b/src/fmllaunchertest/java/net/minecraftforge/fml/test/TopologicalSortTests.java @@ -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 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> 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 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> 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 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 list = TopologicalSort.topologicalSort(graph, Comparator.naturalOrder()); + assertThat(list, contains(2, 3, 1, 4, 6, 7)); + } + + @Test + @DisplayName("basic sort 2") + void testBasicSort2() { + MutableGraph 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 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 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))); + } +} diff --git a/src/main/resources/assets/forge/lang/en_us.json b/src/main/resources/assets/forge/lang/en_us.json index 6cf899a7d..f4e09daad 100644 --- a/src/main/resources/assets/forge/lang/en_us.json +++ b/src/main/resources/assets/forge/lang/en_us.json @@ -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}",