From 47a0ae6abfdb0b6bfe3b4b24fb8158b8e6429ec7 Mon Sep 17 00:00:00 2001 From: Technici4n <13494793+Technici4n@users.noreply.github.com> Date: Fri, 5 Nov 2021 16:24:46 +0100 Subject: [PATCH] Add event phases (#1669) * Proof of concept * Simplify and document * Allow events to be registered with default phases * Use modified Kosaraju for the toposort, and add test for cyclic dependency graphs * Separate phase-related functionality in an EventPhase class * Revert "Separate phase-related functionality in an EventPhase class" This reverts commit e433f348f4d3d056e6c5ccf1802d11103e3fe961. * Ensure that the phase order is deterministic * Add pretty graphs * Add a test, fix a bug, only do one sort for every constraint registration --- .../net/fabricmc/fabric/api/event/Event.java | 43 ++- .../fabric/api/event/EventFactory.java | 35 +++ .../impl/base/event/ArrayBackedEvent.java | 94 +++++- .../impl/base/event/EventFactoryImpl.java | 22 ++ .../impl/base/event/EventPhaseData.java | 47 +++ .../fabric/impl/base/event/PhaseSorting.java | 164 ++++++++++ .../fabricmc/fabric/test/base/EventTests.java | 291 ++++++++++++++++++ .../test/base/FabricApiBaseTestInit.java | 2 + .../net/fabricmc/fabric/test/base/graph.png | Bin 0 -> 20947 bytes 9 files changed, 685 insertions(+), 13 deletions(-) create mode 100644 fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/EventPhaseData.java create mode 100644 fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/PhaseSorting.java create mode 100644 fabric-api-base/src/testmod/java/net/fabricmc/fabric/test/base/EventTests.java create mode 100644 fabric-api-base/src/testmod/java/net/fabricmc/fabric/test/base/graph.png diff --git a/fabric-api-base/src/main/java/net/fabricmc/fabric/api/event/Event.java b/fabric-api-base/src/main/java/net/fabricmc/fabric/api/event/Event.java index 8016d5db2e..76a844e0a2 100644 --- a/fabric-api-base/src/main/java/net/fabricmc/fabric/api/event/Event.java +++ b/fabric-api-base/src/main/java/net/fabricmc/fabric/api/event/Event.java @@ -16,12 +16,17 @@ package net.fabricmc.fabric.api.event; +import org.jetbrains.annotations.ApiStatus; + +import net.minecraft.util.Identifier; + /** - * Base class for Event implementations. + * Base class for Fabric's event implementations. * * @param The listener type. * @see EventFactory */ +@ApiStatus.NonExtendable // Should only be extended by fabric API. public abstract class Event { /** * The invoker field. This should be updated by the implementation to @@ -44,9 +49,43 @@ public final T invoker() { } /** - * Register a listener to the event. + * Register a listener to the event, in the default phase. + * Have a look at {@link #addPhaseOrdering} for an explanation of event phases. * * @param listener The desired listener. */ public abstract void register(T listener); + + /** + * The identifier of the default phase. + * Have a look at {@link EventFactory#createWithPhases} for an explanation of event phases. + */ + public static final Identifier DEFAULT_PHASE = new Identifier("fabric", "default"); + + /** + * Register a listener to the event for the specified phase. + * Have a look at {@link EventFactory#createWithPhases} for an explanation of event phases. + * + * @param phase Identifier of the phase this listener should be registered for. It will be created if it didn't exist yet. + * @param listener The desired listener. + */ + public void register(Identifier phase, T listener) { + // This is done to keep compatibility with existing Event subclasses, but they should really not be subclassing Event. + register(listener); + } + + /** + * Request that listeners registered for one phase be executed before listeners registered for another phase. + * Relying on the default phases supplied to {@link EventFactory#createWithPhases} should be preferred over manually + * registering phase ordering dependencies. + * + *

Incompatible ordering constraints such as cycles will lead to inconsistent behavior: + * some constraints will be respected and some will be ignored. If this happens, a warning will be logged. + * + * @param firstPhase The identifier of the phase that should run before the other. It will be created if it didn't exist yet. + * @param secondPhase The identifier of the phase that should run after the other. It will be created if it didn't exist yet. + */ + public void addPhaseOrdering(Identifier firstPhase, Identifier secondPhase) { + // This is not abstract to avoid breaking existing Event subclasses, but they should really not be subclassing Event. + } } diff --git a/fabric-api-base/src/main/java/net/fabricmc/fabric/api/event/EventFactory.java b/fabric-api-base/src/main/java/net/fabricmc/fabric/api/event/EventFactory.java index d54ac3ee0d..98487c9481 100644 --- a/fabric-api-base/src/main/java/net/fabricmc/fabric/api/event/EventFactory.java +++ b/fabric-api-base/src/main/java/net/fabricmc/fabric/api/event/EventFactory.java @@ -18,6 +18,8 @@ import java.util.function.Function; +import net.minecraft.util.Identifier; + import net.fabricmc.fabric.impl.base.event.EventFactoryImpl; /** @@ -93,6 +95,39 @@ public static Event createArrayBacked(Class type, T emptyInvoker, Func }); } + /** + * Create an array-backed event with a list of default phases that get invoked in order. + * Exposing the identifiers of the default phases as {@code public static final} constants is encouraged. + * + *

An event phase is a named group of listeners, which may be ordered before or after other groups of listeners. + * This allows some listeners to take priority over other listeners. + * Adding separate events should be considered before making use of multiple event phases. + * + *

Phases may be freely added to events created with any of the factory functions, + * however using this function is preferred for widely used event phases. + * If more phases are necessary, discussion with the author of the Event is encouraged. + * + *

Refer to {@link Event#addPhaseOrdering} for an explanation of event phases. + * + * @param type The listener class type. + * @param invokerFactory The invoker factory, combining multiple listeners into one instance. + * @param defaultPhases The default phases of this event, in the correct order. Must contain {@link Event#DEFAULT_PHASE}. + * @param The listener type. + * @return The Event instance. + */ + public static Event createWithPhases(Class type, Function invokerFactory, Identifier... defaultPhases) { + EventFactoryImpl.ensureContainsDefault(defaultPhases); + EventFactoryImpl.ensureNoDuplicates(defaultPhases); + + Event event = createArrayBacked(type, invokerFactory); + + for (int i = 1; i < defaultPhases.length; ++i) { + event.addPhaseOrdering(defaultPhases[i-1], defaultPhases[i]); + } + + return event; + } + /** * Get the listener object name. This can be used in debugging/profiling * scenarios. diff --git a/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/ArrayBackedEvent.java b/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/ArrayBackedEvent.java index aa6b1c2fee..96a1b11e73 100644 --- a/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/ArrayBackedEvent.java +++ b/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/ArrayBackedEvent.java @@ -17,18 +17,34 @@ package net.fabricmc.fabric.impl.base.event; import java.lang.reflect.Array; -import java.util.Arrays; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import java.util.Objects; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; import java.util.function.Function; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import net.minecraft.util.Identifier; + import net.fabricmc.fabric.api.event.Event; class ArrayBackedEvent extends Event { + static final Logger LOGGER = LogManager.getLogger("fabric-api-base"); + private final Function invokerFactory; - private final Lock lock = new ReentrantLock(); + private final Object lock = new Object(); private T[] handlers; + /** + * Registered event phases. + */ + private final Map> phases = new LinkedHashMap<>(); + /** + * Phases sorted in the correct dependency order. + */ + private final List> sortedPhases = new ArrayList<>(); @SuppressWarnings("unchecked") ArrayBackedEvent(Class type, Function invokerFactory) { @@ -43,16 +59,72 @@ void update() { @Override public void register(T listener) { + register(DEFAULT_PHASE, listener); + } + + @Override + public void register(Identifier phaseIdentifier, T listener) { + Objects.requireNonNull(phaseIdentifier, "Tried to register a listener for a null phase!"); Objects.requireNonNull(listener, "Tried to register a null listener!"); - lock.lock(); + synchronized (lock) { + getOrCreatePhase(phaseIdentifier, true).addListener(listener); + rebuildInvoker(handlers.length + 1); + } + } + + private EventPhaseData getOrCreatePhase(Identifier id, boolean sortIfCreate) { + EventPhaseData phase = phases.get(id); + + if (phase == null) { + phase = new EventPhaseData<>(id, handlers.getClass().getComponentType()); + phases.put(id, phase); + sortedPhases.add(phase); + + if (sortIfCreate) { + PhaseSorting.sortPhases(sortedPhases); + } + } + + return phase; + } + + private void rebuildInvoker(int newLength) { + // Rebuild handlers. + if (sortedPhases.size() == 1) { + // Special case with a single phase: use the array of the phase directly. + handlers = sortedPhases.get(0).listeners; + } else { + @SuppressWarnings("unchecked") + T[] newHandlers = (T[]) Array.newInstance(handlers.getClass().getComponentType(), newLength); + int newHandlersIndex = 0; + + for (EventPhaseData existingPhase : sortedPhases) { + int length = existingPhase.listeners.length; + System.arraycopy(existingPhase.listeners, 0, newHandlers, newHandlersIndex, length); + newHandlersIndex += length; + } + + handlers = newHandlers; + } + + // Rebuild invoker. + update(); + } + + @Override + public void addPhaseOrdering(Identifier firstPhase, Identifier secondPhase) { + Objects.requireNonNull(firstPhase, "Tried to add an ordering for a null phase."); + Objects.requireNonNull(secondPhase, "Tried to add an ordering for a null phase."); + if (firstPhase.equals(secondPhase)) throw new IllegalArgumentException("Tried to add a phase that depends on itself."); - try { - handlers = Arrays.copyOf(handlers, handlers.length + 1); - handlers[handlers.length - 1] = listener; - update(); - } finally { - lock.unlock(); + synchronized (lock) { + EventPhaseData first = getOrCreatePhase(firstPhase, false); + EventPhaseData second = getOrCreatePhase(secondPhase, false); + first.subsequentPhases.add(second); + second.previousPhases.add(first); + PhaseSorting.sortPhases(this.sortedPhases); + rebuildInvoker(handlers.length); } } } diff --git a/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/EventFactoryImpl.java b/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/EventFactoryImpl.java index ef5ab991f2..dd07f14d04 100644 --- a/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/EventFactoryImpl.java +++ b/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/EventFactoryImpl.java @@ -27,6 +27,8 @@ import java.util.List; import java.util.function.Function; +import net.minecraft.util.Identifier; + import net.fabricmc.fabric.api.event.Event; public final class EventFactoryImpl { @@ -44,6 +46,26 @@ public static Event createArrayBacked(Class type, Function T buildEmptyInvoker(Class handlerClass, Function invokerSetup) { diff --git a/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/EventPhaseData.java b/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/EventPhaseData.java new file mode 100644 index 0000000000..f13744a19d --- /dev/null +++ b/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/EventPhaseData.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.impl.base.event; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import net.minecraft.util.Identifier; + +/** + * Data of an {@link ArrayBackedEvent} phase. + */ +class EventPhaseData { + final Identifier id; + T[] listeners; + final List> subsequentPhases = new ArrayList<>(); + final List> previousPhases = new ArrayList<>(); + int visitStatus = 0; // 0: not visited, 1: visiting, 2: visited + + @SuppressWarnings("unchecked") + EventPhaseData(Identifier id, Class listenerClass) { + this.id = id; + this.listeners = (T[]) Array.newInstance(listenerClass, 0); + } + + void addListener(T listener) { + int oldLength = listeners.length; + listeners = Arrays.copyOf(listeners, oldLength + 1); + listeners[oldLength] = listener; + } +} diff --git a/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/PhaseSorting.java b/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/PhaseSorting.java new file mode 100644 index 0000000000..edeca71e98 --- /dev/null +++ b/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/PhaseSorting.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.impl.base.event; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.PriorityQueue; + +import com.google.common.annotations.VisibleForTesting; +import org.jetbrains.annotations.ApiStatus; + +/** + * Contains phase-sorting logic for {@link ArrayBackedEvent}. + */ +@ApiStatus.Internal +public class PhaseSorting { + @VisibleForTesting + public static boolean ENABLE_CYCLE_WARNING = true; + + /** + * Deterministically sort a list of phases. + * 1) Compute phase SCCs (i.e. cycles). + * 2) Sort phases by id within SCCs. + * 3) Sort SCCs with respect to each other by respecting constraints, and by id in case of a tie. + */ + static void sortPhases(List> sortedPhases) { + // FIRST KOSARAJU SCC VISIT + List> toposort = new ArrayList<>(sortedPhases.size()); + + for (EventPhaseData phase : sortedPhases) { + forwardVisit(phase, null, toposort); + } + + clearStatus(toposort); + Collections.reverse(toposort); + + // SECOND KOSARAJU SCC VISIT + Map, PhaseScc> phaseToScc = new IdentityHashMap<>(); + + for (EventPhaseData phase : toposort) { + if (phase.visitStatus == 0) { + List> sccPhases = new ArrayList<>(); + // Collect phases in SCC. + backwardVisit(phase, sccPhases); + // Sort phases by id. + sccPhases.sort(Comparator.comparing(p -> p.id)); + // Mark phases as belonging to this SCC. + PhaseScc scc = new PhaseScc<>(sccPhases); + + for (EventPhaseData phaseInScc : sccPhases) { + phaseToScc.put(phaseInScc, scc); + } + } + } + + clearStatus(toposort); + + // Build SCC graph + for (PhaseScc scc : phaseToScc.values()) { + for (EventPhaseData phase : scc.phases) { + for (EventPhaseData subsequentPhase : phase.subsequentPhases) { + PhaseScc subsequentScc = phaseToScc.get(subsequentPhase); + + if (subsequentScc != scc) { + scc.subsequentSccs.add(subsequentScc); + subsequentScc.inDegree++; + } + } + } + } + + // Order SCCs according to priorities. When there is a choice, use the SCC with the lowest id. + // The priority queue contains all SCCs that currently have 0 in-degree. + PriorityQueue> pq = new PriorityQueue<>(Comparator.comparing(scc -> scc.phases.get(0).id)); + sortedPhases.clear(); + + for (PhaseScc scc : phaseToScc.values()) { + if (scc.inDegree == 0) { + pq.add(scc); + // Prevent adding the same SCC multiple times, as phaseToScc may contain the same value multiple times. + scc.inDegree = -1; + } + } + + while (!pq.isEmpty()) { + PhaseScc scc = pq.poll(); + sortedPhases.addAll(scc.phases); + + for (PhaseScc subsequentScc : scc.subsequentSccs) { + subsequentScc.inDegree--; + + if (subsequentScc.inDegree == 0) { + pq.add(subsequentScc); + } + } + } + } + + private static void forwardVisit(EventPhaseData phase, EventPhaseData parent, List> toposort) { + if (phase.visitStatus == 0) { + // Not yet visited. + phase.visitStatus = 1; + + for (EventPhaseData data : phase.subsequentPhases) { + forwardVisit(data, phase, toposort); + } + + toposort.add(phase); + phase.visitStatus = 2; + } else if (phase.visitStatus == 1 && ENABLE_CYCLE_WARNING) { + // Already visiting, so we have found a cycle. + ArrayBackedEvent.LOGGER.warn(String.format( + "Event phase ordering conflict detected.%nEvent phase %s is ordered both before and after event phase %s.", + phase.id, + parent.id + )); + } + } + + private static void clearStatus(List> phases) { + for (EventPhaseData phase : phases) { + phase.visitStatus = 0; + } + } + + private static void backwardVisit(EventPhaseData phase, List> sccPhases) { + if (phase.visitStatus == 0) { + phase.visitStatus = 1; + sccPhases.add(phase); + + for (EventPhaseData data : phase.previousPhases) { + backwardVisit(data, sccPhases); + } + } + } + + private static class PhaseScc { + final List> phases; + final List> subsequentSccs = new ArrayList<>(); + int inDegree = 0; + + private PhaseScc(List> phases) { + this.phases = phases; + } + } +} diff --git a/fabric-api-base/src/testmod/java/net/fabricmc/fabric/test/base/EventTests.java b/fabric-api-base/src/testmod/java/net/fabricmc/fabric/test/base/EventTests.java new file mode 100644 index 0000000000..e3e35c2e1c --- /dev/null +++ b/fabric-api-base/src/testmod/java/net/fabricmc/fabric/test/base/EventTests.java @@ -0,0 +1,291 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.test.base; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.LogManager; + +import net.minecraft.util.Identifier; + +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.fabricmc.fabric.impl.base.event.PhaseSorting; + +public class EventTests { + private static final Logger LOGGER = LogManager.getLogger("fabric-api-base"); + + public static void run() { + long time1 = System.currentTimeMillis(); + + testDefaultPhaseOnly(); + testMultipleDefaultPhases(); + testAddedPhases(); + testCycle(); + PhaseSorting.ENABLE_CYCLE_WARNING = false; + testDeterministicOrdering(); + testTwoCycles(); + PhaseSorting.ENABLE_CYCLE_WARNING = true; + + long time2 = System.currentTimeMillis(); + LOGGER.info("Event unit tests succeeded in {} milliseconds.", time2 - time1); + } + + private static final Function INVOKER_FACTORY = listeners -> () -> { + for (Test test : listeners) { + test.onTest(); + } + }; + + private static int currentListener = 0; + + private static Event createEvent() { + return EventFactory.createArrayBacked(Test.class, INVOKER_FACTORY); + } + + private static Test ensureOrder(int order) { + return () -> { + assertEquals(order, currentListener); + ++currentListener; + }; + } + + private static void testDefaultPhaseOnly() { + Event event = createEvent(); + + event.register(ensureOrder(0)); + event.register(Event.DEFAULT_PHASE, ensureOrder(1)); + event.register(ensureOrder(2)); + + event.invoker().onTest(); + assertEquals(3, currentListener); + currentListener = 0; + } + + private static void testMultipleDefaultPhases() { + Identifier first = new Identifier("fabric", "first"); + Identifier second = new Identifier("fabric", "second"); + Event event = EventFactory.createWithPhases(Test.class, INVOKER_FACTORY, first, second, Event.DEFAULT_PHASE); + + event.register(second, ensureOrder(1)); + event.register(ensureOrder(2)); + event.register(first, ensureOrder(0)); + + for (int i = 0; i < 5; ++i) { + event.invoker().onTest(); + assertEquals(3, currentListener); + currentListener = 0; + } + } + + private static void testAddedPhases() { + Event event = createEvent(); + + Identifier veryEarly = new Identifier("fabric", "very_early"); + Identifier early = new Identifier("fabric", "early"); + Identifier late = new Identifier("fabric", "late"); + Identifier veryLate = new Identifier("fabric", "very_late"); + + event.addPhaseOrdering(veryEarly, early); + event.addPhaseOrdering(early, Event.DEFAULT_PHASE); + event.addPhaseOrdering(Event.DEFAULT_PHASE, late); + event.addPhaseOrdering(late, veryLate); + + event.register(ensureOrder(4)); + event.register(ensureOrder(5)); + event.register(veryEarly, ensureOrder(0)); + event.register(early, ensureOrder(2)); + event.register(late, ensureOrder(6)); + event.register(veryLate, ensureOrder(8)); + event.register(veryEarly, ensureOrder(1)); + event.register(veryLate, ensureOrder(9)); + event.register(late, ensureOrder(7)); + event.register(early, ensureOrder(3)); + + for (int i = 0; i < 5; ++i) { + event.invoker().onTest(); + assertEquals(10, currentListener); + currentListener = 0; + } + } + + private static void testCycle() { + Event event = createEvent(); + + Identifier a = new Identifier("fabric", "a"); + Identifier b1 = new Identifier("fabric", "b1"); + Identifier b2 = new Identifier("fabric", "b2"); + Identifier b3 = new Identifier("fabric", "b3"); + Identifier c = Event.DEFAULT_PHASE; + + // A always first and C always last. + event.register(a, ensureOrder(0)); + event.register(c, ensureOrder(4)); + event.register(b1, ensureOrder(1)); + event.register(b1, ensureOrder(2)); + event.register(b1, ensureOrder(3)); + + // A -> B + event.addPhaseOrdering(a, b1); + // B -> C + event.addPhaseOrdering(b3, c); + // loop + event.addPhaseOrdering(b1, b2); + event.addPhaseOrdering(b2, b3); + event.addPhaseOrdering(b3, b1); + + for (int i = 0; i < 5; ++i) { + event.invoker().onTest(); + assertEquals(5, currentListener); + currentListener = 0; + } + } + + /** + * Ensure that phases get sorted deterministically regardless of the order in which constraints are registered. + * + *

The graph is displayed here as ascii art, and also in the file graph.png. + *

+	 *             +-------------------+
+	 *             v                   |
+	 * +---+     +---+     +---+     +---+
+	 * | a | --> | z | --> | b | --> | y |
+	 * +---+     +---+     +---+     +---+
+	 *             ^
+	 *             |
+	 *             |
+	 * +---+     +---+
+	 * | d | --> | e |
+	 * +---+     +---+
+	 * +---+
+	 * | f |
+	 * +---+
+	 * 
+ * Notice the cycle z -> b -> y -> z. The elements of the cycle are ordered [b, y, z], and the cycle itself is ordered with its lowest id "b". + * We get for the final order: [a, d, e, cycle [b, y, z], f]. + */ + private static void testDeterministicOrdering() { + Identifier a = new Identifier("fabric", "a"); + Identifier b = new Identifier("fabric", "b"); + Identifier d = new Identifier("fabric", "d"); + Identifier e = new Identifier("fabric", "e"); + Identifier f = new Identifier("fabric", "f"); + Identifier y = new Identifier("fabric", "y"); + Identifier z = new Identifier("fabric", "z"); + + List>> dependencies = List.of( + ev -> ev.addPhaseOrdering(a, z), + ev -> ev.addPhaseOrdering(d, e), + ev -> ev.addPhaseOrdering(e, z), + ev -> ev.addPhaseOrdering(z, b), + ev -> ev.addPhaseOrdering(b, y), + ev -> ev.addPhaseOrdering(y, z) + ); + + testAllPermutations(new ArrayList<>(), dependencies, selectedDependencies -> { + Event event = createEvent(); + + for (Consumer> dependency : selectedDependencies) { + dependency.accept(event); + } + + event.register(a, ensureOrder(0)); + event.register(d, ensureOrder(1)); + event.register(e, ensureOrder(2)); + event.register(b, ensureOrder(3)); + event.register(y, ensureOrder(4)); + event.register(z, ensureOrder(5)); + event.register(f, ensureOrder(6)); + + event.invoker().onTest(); + assertEquals(7, currentListener); + currentListener = 0; + }); + } + + /** + * Test deterministic phase sorting with two cycles. + *
+	 * e --> a <--> b <-- d <--> c
+	 * 
+ */ + private static void testTwoCycles() { + Identifier a = new Identifier("fabric", "a"); + Identifier b = new Identifier("fabric", "b"); + Identifier c = new Identifier("fabric", "c"); + Identifier d = new Identifier("fabric", "d"); + Identifier e = new Identifier("fabric", "e"); + + List>> dependencies = List.of( + ev -> ev.addPhaseOrdering(e, a), + ev -> ev.addPhaseOrdering(a, b), + ev -> ev.addPhaseOrdering(b, a), + ev -> ev.addPhaseOrdering(d, b), + ev -> ev.addPhaseOrdering(d, c), + ev -> ev.addPhaseOrdering(c, d) + ); + + testAllPermutations(new ArrayList<>(), dependencies, selectedDependencies -> { + Event event = createEvent(); + + for (Consumer> dependency : selectedDependencies) { + dependency.accept(event); + } + + event.register(c, ensureOrder(0)); + event.register(d, ensureOrder(1)); + event.register(e, ensureOrder(2)); + event.register(a, ensureOrder(3)); + event.register(b, ensureOrder(4)); + + event.invoker().onTest(); + assertEquals(5, currentListener); + currentListener = 0; + }); + } + + @SuppressWarnings("SuspiciousListRemoveInLoop") + private static void testAllPermutations(List selected, List toSelect, Consumer> action) { + if (toSelect.size() == 0) { + action.accept(selected); + } else { + for (int i = 0; i < toSelect.size(); ++i) { + selected.add(toSelect.get(i)); + List remaining = new ArrayList<>(toSelect); + remaining.remove(i); + testAllPermutations(selected, remaining, action); + selected.remove(selected.size()-1); + } + } + } + + @FunctionalInterface + interface Test { + void onTest(); + } + + private static void assertEquals(Object expected, Object actual) { + if (!Objects.equals(expected, actual)) { + throw new AssertionError(String.format("assertEquals failed%nexpected: %s%n but was: %s", expected, actual)); + } + } +} diff --git a/fabric-api-base/src/testmod/java/net/fabricmc/fabric/test/base/FabricApiBaseTestInit.java b/fabric-api-base/src/testmod/java/net/fabricmc/fabric/test/base/FabricApiBaseTestInit.java index 7cd17b3256..79c3a5ed69 100644 --- a/fabric-api-base/src/testmod/java/net/fabricmc/fabric/test/base/FabricApiBaseTestInit.java +++ b/fabric-api-base/src/testmod/java/net/fabricmc/fabric/test/base/FabricApiBaseTestInit.java @@ -59,5 +59,7 @@ public void onInitialize() { return 1; })); }); + + EventTests.run(); } } diff --git a/fabric-api-base/src/testmod/java/net/fabricmc/fabric/test/base/graph.png b/fabric-api-base/src/testmod/java/net/fabricmc/fabric/test/base/graph.png new file mode 100644 index 0000000000000000000000000000000000000000..905359c29047ae46263e8572437e6eabec6debc7 GIT binary patch literal 20947 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