Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions .github/workflows/pullrequest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,29 @@ jobs:
build:
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
# Run the full build (incl. the per-platform plugin startup tests) on more than one JDK so a
# JDK-version-dependent DI/reflective startup regression is caught in CI. 17 is the primary
# (release) toolchain and the only one that publishes artifacts; 21 is the highest JDK
# Gradle 8.5 can run on. The Java-26-class reflective/DI failures (Guice 7 provisioning,
# Libp2pEndpointRuntime constructor arity) are additionally guarded by signature-level
# reflective tests that are independent of the running JDK, so they are covered even though
# the build cannot run on JDK 26 until Gradle is upgraded.
java: ['17', '21']

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for git describe

- name: Set up JDK 17
- name: Set up JDK ${{ matrix.java }}
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
java-version: ${{ matrix.java }}
cache: 'gradle'

- name: Setup Gradle
Expand All @@ -29,21 +41,21 @@ jobs:

- name: Archive artifacts (Connect Bungee)
uses: actions/upload-artifact@v4
if: success()
if: success() && matrix.java == '17'
with:
name: Connect Bungee
path: bungee/build/libs/connect-bungee.jar

- name: Archive artifacts (Connect Spigot)
uses: actions/upload-artifact@v4
if: success()
if: success() && matrix.java == '17'
with:
name: Connect Spigot
path: spigot/build/libs/connect-spigot.jar

- name: Archive artifacts (Connect Velocity)
uses: actions/upload-artifact@v4
if: success()
if: success() && matrix.java == '17'
with:
name: Connect Velocity
path: velocity/build/libs/connect-velocity.jar
15 changes: 15 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,21 @@ Do not call production fixed from a Connect Java release alone. Confirm the
released jar is in the hub image, the hub pod logs the expected plugin version,
and Moxy accepts a tunnel with the same `connectorVersion`.

### Per-platform startup/DI regression guard

- Each platform (Velocity/Spigot/Bungee) has a plugin startup smoke test
(`<platform>/src/test/.../<Platform>PluginStartupTest`) plus core
`startup/PluginGraphStartupTest`, sharing the `core` test fixture
`core/src/testFixtures/.../startup/StartupGraphProvisioning`: it walks the real
`@Inject` graph and replays Guice 7's injectable-constructor rule, so a provider
made unprovisionable on any platform's injector (the Velocity 4 / Guice 7 class
of bug) fails the suite. The Velocity test fails on the pre-fix `javax.inject`
annotations and passes on the fix. Add new platform DI classes to that
platform test's `*GraphRoots()`.
- `pullrequest.yml` runs `./gradlew build` on a JDK matrix (17, 21); the
Java-26-class reflective bugs (Guice 7 DI, `Libp2pEndpointRuntime` ctor arity)
are guarded by signature-level tests independent of the running JDK.

## Maintaining this file

Keep this file for knowledge useful to almost every future agent session in this project.
Expand Down
12 changes: 12 additions & 0 deletions bungee/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ var guavaVersion = "21.0"
dependencies {
api(projects.core)
implementation("cloud.commandframework", "cloud-bungee", Versions.cloudVersion)

testImplementation("org.junit.jupiter:junit-jupiter:5.10.5")
testImplementation("org.mockito:mockito-core:4.11.0")
testImplementation(testFixtures(projects.core))
// Bungee plugin classes (e.g. BungeeListener) are needed on the test classpath for the
// per-platform startup test's reflective DI-graph walk.
testImplementation("net.md-5", "bungeecord-api", bungeeApiVersion)
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

tasks.test {
useJUnitPlatform()
}

relocate("com.google.inject")
Expand Down
134 changes: 134 additions & 0 deletions bungee/src/test/java/com/minekube/connect/BungeePluginStartupTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Floodgate
*/

package com.minekube.connect;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;

import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.name.Names;
import com.minekube.connect.api.ConnectApi;
import com.minekube.connect.api.logger.ConnectLogger;
import com.minekube.connect.api.packet.PacketHandlers;
import com.minekube.connect.bedrock.BedrockAdmissionCoordinator;
import com.minekube.connect.bedrock.BedrockIdentityEnforcer;
import com.minekube.connect.bedrock.BedrockIdentityKeyProvider;
import com.minekube.connect.inject.CommonPlatformInjector;
import com.minekube.connect.inject.bungee.BungeeInjector;
import com.minekube.connect.listener.BungeeListener;
import com.minekube.connect.listener.BungeeListenerRegistration;
import com.minekube.connect.module.ProxyCommonModule;
import com.minekube.connect.platform.util.PlatformUtils;
import com.minekube.connect.pluginmessage.BungeeSkinApplier;
import com.minekube.connect.startup.StartupGraphProvisioning;
import com.minekube.connect.util.BungeeCommandUtil;
import com.minekube.connect.util.BungeePlatformUtils;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

/**
* Per-platform startup / smoke test for the BungeeCord plugin.
*
* <p>Asserts the Bungee plugin DI graph — the {@code ProxyCommonModule} + {@code BungeePlatformModule}
* graph {@link BungeePlugin#onLoad()} builds — including the concrete platform/listener bindings —
* is provisionable under Velocity 4-class Guice 7 and
* that a real Guice injector wires the shared graph up cleanly with no {@code ProvisionException}.
* Like every platform's injector it carries the Bedrock identity graph, so it fails on the pre-fix
* commit (javax-annotated Bedrock providers) and passes on the fix — see {@link
* StartupGraphProvisioning}.
*
* <p><b>Test limitation.</b> {@code BungeePlugin} extends Bungee's {@code Plugin} and relies on
* server-side {@code init(...)} for its data folder/proxy, so the real-provision half wires the
* Bungee proxy graph ({@code ProxyCommonModule}) with the platform-supplied bindings stubbed rather
* than booting a BungeeCord proxy. That still provisions the full Connect object graph a DI
* regression would break; the Guice-7 provisionability check additionally covers the Bungee-specific
* DI classes ({@code BungeeListener}, …).
*/
class BungeePluginStartupTest {
@TempDir Path tempDir;

private static List<Class<?>> bungeeGraphRoots() {
List<Class<?>> roots = new ArrayList<>(StartupGraphProvisioning.coreRuntimeGraphRoots());
roots.add(BungeeListener.class);
roots.add(BungeeCommandUtil.class);
roots.add(BungeePlatformUtils.class);
roots.add(BungeeInjector.class);
roots.add(BungeeSkinApplier.class);
roots.add(BungeeListenerRegistration.class);
return roots;
}

@Test
void bungeePluginGraphIsProvisionableUnderGuice7() {
Set<Class<?>> graph = StartupGraphProvisioning.reachableInjectedTypes(bungeeGraphRoots());

List<String> violations = StartupGraphProvisioning.guice7ProvisioningViolations(graph);

assertTrue(violations.isEmpty(),
"Bungee plugin DI classes are not provisionable under Velocity 4.0.0's Guice 7:\n"
+ String.join("\n", violations));
assertTrue(graph.contains(BedrockIdentityKeyProvider.class),
"walk must reach BedrockIdentityKeyProvider (the class that failed on Velocity 4)");
assertTrue(graph.contains(BungeeListener.class),
"walk must include member-injected BungeeListener");
}

/**
* Real Guice provisioning of the Bungee proxy graph ({@code ProxyCommonModule}) with the
* platform-supplied bindings stubbed. Proves the modules wire up and the Bedrock/config graph
* resolves without a provisioning error. Config load is not driven (it makes a network call);
* the Guice-7 check above covers the parts not instantiated here.
*/
@Test
void bungeeProxyGraphProvisionsKeySingletonsWithoutError() {
Injector injector = Guice.createInjector(
new ProxyCommonModule(tempDir),
new AbstractModule() {
@Override
protected void configure() {
bind(ConnectLogger.class).toInstance(mock(ConnectLogger.class));
bind(PlatformUtils.class).toInstance(mock(PlatformUtils.class));
bind(CommonPlatformInjector.class)
.toInstance(mock(CommonPlatformInjector.class));
bind(String.class).annotatedWith(Names.named("platformName"))
.toInstance("BungeeCord");
}
});

assertDoesNotThrow(() -> injector.getInstance(ConnectApi.class));
assertDoesNotThrow(() -> injector.getInstance(PacketHandlers.class));
assertDoesNotThrow(() -> injector.getInstance(BedrockAdmissionCoordinator.class));
assertDoesNotThrow(() -> injector.getInstance(BedrockIdentityEnforcer.class));
assertDoesNotThrow(() -> injector.getInstance(BedrockIdentityKeyProvider.class));
}
}
1 change: 1 addition & 0 deletions core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import net.kyori.blossom.BlossomExtension

plugins {
idea // used to let Intellij recognize protobuf generated sources
`java-test-fixtures` // shared startup/DI provisioning checks reused by per-platform tests
id("net.kyori.blossom")
id("com.google.protobuf")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Floodgate
*/

package com.minekube.connect.startup;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;

import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.name.Names;
import com.minekube.connect.api.ConnectApi;
import com.minekube.connect.api.logger.ConnectLogger;
import com.minekube.connect.api.packet.PacketHandlers;
import com.minekube.connect.bedrock.BedrockAdmissionCoordinator;
import com.minekube.connect.bedrock.BedrockIdentityEnforcer;
import com.minekube.connect.bedrock.BedrockIdentityKeyProvider;
import com.minekube.connect.config.ConfigHolder;
import com.minekube.connect.inject.CommonPlatformInjector;
import com.minekube.connect.module.ServerCommonModule;
import com.minekube.connect.platform.util.PlatformUtils;
import com.minekube.connect.register.CommandRegister;
import com.minekube.connect.register.ListenerRegister;
import com.minekube.connect.register.WatcherRegister;
import java.nio.file.Path;
import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

/**
* Startup smoke test for the shared Connect plugin object graph — the portion of the DI graph every
* platform (Velocity/Spigot/Bungee) provisions identically. It is the workhorse behind the
* per-platform startup tests: those reuse {@link StartupGraphProvisioning#coreRuntimeGraphRoots()}
* and add their platform-specific roots on top.
*
* <p>This is the captain-directed prevention for the class of DI/startup regression that produced
* the Velocity 4.0.0 {@code BedrockIdentityKeyProvider} "Cant create plugin connect" failure. It
* fails on the pre-fix code (which annotated the Bedrock providers with {@code javax.inject},
* invisible to Velocity 4's Guice 7) and passes on the fixed code. See
* {@link StartupGraphProvisioning} for why a Guice 7 rule replica is used instead of a live Guice 7
* injector, and {@code BedrockVelocityGuice7ProvisioningTest} for the original narrower guard.
*
* <p>The reflective-constructor regression class (the Java-26 {@code Libp2pEndpointRuntime}
* constructor arity mismatch) is guarded by {@code Libp2pEndpointRuntimeInitTest} and
* {@code Libp2pRuntimeBoundaryTest}, which run in this same module's test task; {@code Libp2pEndpoint}
* and {@code Libp2pTunnelTransport} are also part of the graph asserted here.
*/
class PluginGraphStartupTest {
@TempDir Path tempDir;

/**
* The whole shared runtime graph must be provisionable under Velocity-4-class Guice 7. This is
* the assertion that fails on the pre-fix commit (javax-annotated Bedrock providers) and passes
* on the fixed code.
*/
@Test
void sharedRuntimeGraphIsProvisionableUnderGuice7() {
Set<Class<?>> graph =
StartupGraphProvisioning.reachableInjectedTypes(
StartupGraphProvisioning.coreRuntimeGraphRoots());

List<String> violations = StartupGraphProvisioning.guice7ProvisioningViolations(graph);

assertTrue(violations.isEmpty(),
"Connect DI classes on the shared plugin graph are not provisionable under "
+ "Velocity 4.0.0's Guice 7:\n" + String.join("\n", violations));
}

/**
* Guards the guard: the reflective walk must actually reach the class that regressed
* ({@code BedrockIdentityKeyProvider}) and the rest of the Bedrock identity graph, otherwise
* {@link #sharedRuntimeGraphIsProvisionableUnderGuice7()} could pass vacuously by covering
* nothing.
*/
@Test
void reflectiveWalkReachesTheClassesThatRegressed() {
Set<Class<?>> graph =
StartupGraphProvisioning.reachableInjectedTypes(
StartupGraphProvisioning.coreRuntimeGraphRoots());

assertTrue(graph.contains(BedrockIdentityKeyProvider.class),
"walk must reach BedrockIdentityKeyProvider (the class that failed on Velocity 4)");
assertTrue(graph.contains(BedrockAdmissionCoordinator.class),
"walk must reach BedrockAdmissionCoordinator");
assertTrue(graph.contains(BedrockIdentityEnforcer.class),
"walk must reach BedrockIdentityEnforcer");
assertTrue(graph.contains(WatcherRegister.class),
"walk must include member-injected WatcherRegister");
assertTrue(graph.contains(CommandRegister.class),
"walk must include eager CommandRegister");
assertTrue(graph.contains(ListenerRegister.class),
"walk must include eager ListenerRegister");
}

/**
* Real Guice-6 provisioning of the hermetic slice of the server graph: proves the modules wire
* up and the Bedrock/config graph resolves without a {@code ProvisionException}. Config loading
* ({@code ConnectPlatform.init()}) is intentionally not driven here because it performs a
* network call to generate an endpoint name; the reflective Guice-7 check above covers the parts
* of the graph this real provision does not instantiate.
*/
@Test
void serverGraphProvisionsKeySingletonsWithoutError() {
Injector injector = Guice.createInjector(
new ServerCommonModule(tempDir),
new AbstractModule() {
@Override
protected void configure() {
// Bindings the platform module would normally supply.
bind(ConnectLogger.class).toInstance(mock(ConnectLogger.class));
bind(PlatformUtils.class).toInstance(mock(PlatformUtils.class));
bind(CommonPlatformInjector.class)
.toInstance(mock(CommonPlatformInjector.class));
bind(String.class).annotatedWith(Names.named("platformName"))
.toInstance("test");
}
});

assertNotNull(injector.getInstance(ConnectApi.class));
assertNotNull(injector.getInstance(ConfigHolder.class));
assertNotNull(injector.getInstance(BedrockAdmissionCoordinator.class));
assertNotNull(injector.getInstance(BedrockIdentityEnforcer.class));
assertNotNull(injector.getInstance(BedrockIdentityKeyProvider.class));
assertNotNull(injector.getInstance(PacketHandlers.class));
}
}
Loading
Loading