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
11 changes: 11 additions & 0 deletions donkey/build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
// Donkey: the message engine. Produces donkey-model.jar and
// donkey-server.jar in donkey/setup (mirrors donkey/build.xml).

apply plugin: 'java-test-fixtures'

// The test-fixtures plugin gives the test source set the main classes through
// a dependency on this project, which the non-transitive test classpaths
// drop, so add them back directly. Remove this when transitive resolution
// is enabled.
sourceSets.test {
compileClasspath += sourceSets.main.output
runtimeClasspath += sourceSets.main.output
}

dependencies {
implementation libs.bundles.donkey.main
testImplementation libs.bundles.donkey.test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import com.google.inject.Inject;
import com.mirth.connect.donkey.model.DatabaseConstants;
import com.mirth.connect.donkey.server.channel.Channel;
import com.mirth.connect.donkey.server.controllers.ChannelController;
Expand All @@ -39,7 +38,6 @@
import com.mirth.connect.donkey.util.xstream.XStreamSerializer;

public class Donkey {
@Inject
private static Donkey instance;

public static Donkey getInstance() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import com.google.inject.Inject;
import com.mirth.connect.donkey.model.DatabaseConstants;
import com.mirth.connect.donkey.server.data.jdbc.ConnectionPool;
import com.mirth.connect.donkey.server.data.jdbc.HikariConnectionPool;
Expand All @@ -24,7 +23,6 @@ public class DonkeyConnectionPools {

private static final Logger logger = LogManager.getLogger(DonkeyConnectionPools.class);

@Inject
private static volatile DonkeyConnectionPools instance = null;

private ConnectionPool connectionPool;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

import org.apache.commons.lang3.StringUtils;

import com.google.inject.Inject;
import com.mirth.connect.donkey.model.channel.Ports;
import com.mirth.connect.donkey.model.message.Status;
import com.mirth.connect.donkey.server.Donkey;
Expand All @@ -27,7 +26,6 @@
import com.mirth.connect.donkey.server.data.DonkeyDaoFactory;

public class ChannelController {
@Inject
private static ChannelController instance;

public static ChannelController getInstance() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;

import com.google.inject.Inject;
import com.mirth.connect.donkey.model.message.ConnectorMessage;
import com.mirth.connect.donkey.model.message.Message;
import com.mirth.connect.donkey.model.message.Status;
Expand All @@ -30,7 +29,6 @@
import com.mirth.connect.donkey.util.StringUtil;

public class MessageController {
@Inject
private static MessageController instance;

public static MessageController getInstance() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,12 @@
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;

import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.mirth.connect.donkey.model.channel.ConnectorProperties;
import com.mirth.connect.donkey.model.channel.SourceConnectorProperties;
import com.mirth.connect.donkey.model.channel.SourceConnectorPropertiesInterface;
Expand All @@ -44,28 +42,31 @@
import com.mirth.connect.donkey.server.event.EventDispatcher;
import com.mirth.connect.donkey.server.message.DataType;
import com.mirth.connect.donkey.server.queue.SourceQueue;
import com.mirth.connect.donkey.testing.StaticOverride;
import com.mirth.connect.donkey.util.SerializerProvider;

public class ChannelTest {

private static StaticOverride donkeyOverride;

@BeforeClass
public static void setupControllers() throws Exception {
Donkey donkey = mock(Donkey.class);
when(donkey.getEventDispatcher()).thenReturn(mock(EventDispatcher.class));

Injector injector = Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
requestStaticInjection(Donkey.class);
bind(Donkey.class).toInstance(donkey);
}
});
injector.getInstance(Donkey.class);
donkeyOverride = StaticOverride.install(Donkey.class, "instance", donkey);

// Make sure injection succeeded
assertTrue(donkey == Donkey.getInstance());
}

@AfterClass
public static void restoreDonkey() {
if (donkeyOverride != null) {
donkeyOverride.close();
}
}

@Test
public void testRawMessageWithAttachments() throws Exception {
Channel channel = createChannel();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 Tony Germano <tony@germano.name>

package com.mirth.connect.donkey.testing;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;

import org.junit.After;
import org.junit.Test;

public class StaticOverrideTest {

private static class Holder {
private static String instance;
private static volatile String volatileInstance;
private static final String CONSTANT = "constant";
private String nonStatic;
}

@After
public void reset() {
Holder.instance = null;
Holder.volatileInstance = null;
}

@Test
public void installsAndRestoresPreviousValue() throws Exception {
Holder.instance = "original";

AutoCloseable handle = StaticOverride.install(Holder.class, "instance", "replacement");
assertEquals("replacement", Holder.instance);

handle.close();
assertEquals("original", Holder.instance);
}

@Test
public void restoresNull() throws Exception {
AutoCloseable handle = StaticOverride.install(Holder.class, "instance", "replacement");
handle.close();

assertNull(Holder.instance);
}

@Test
public void worksOnVolatileField() throws Exception {
try (AutoCloseable handle = StaticOverride.install(Holder.class, "volatileInstance", "replacement")) {
assertEquals("replacement", Holder.volatileInstance);
}

assertNull(Holder.volatileInstance);
}

@Test
public void nestedOverridesRestoreInOrder() throws Exception {
Holder.instance = "original";

AutoCloseable outer = StaticOverride.install(Holder.class, "instance", "outer");
AutoCloseable inner = StaticOverride.install(Holder.class, "instance", "inner");

inner.close();
assertEquals("outer", Holder.instance);
outer.close();
assertEquals("original", Holder.instance);
}

@Test
public void secondCloseDoesNothing() throws Exception {
Holder.instance = "original";
AutoCloseable handle = StaticOverride.install(Holder.class, "instance", "replacement");
handle.close();

Holder.instance = "set later";
handle.close();

assertEquals("set later", Holder.instance);
}

@Test
public void closeFailsWhenValueWasReplaced() throws Exception {
AutoCloseable handle = StaticOverride.install(Holder.class, "instance", "replacement");
Holder.instance = "someone else";

assertThrows(IllegalStateException.class, handle::close);
assertEquals("someone else", Holder.instance);
}

@Test
public void closingOutOfOrderFails() throws Exception {
AutoCloseable outer = StaticOverride.install(Holder.class, "instance", "outer");
StaticOverride.install(Holder.class, "instance", "inner");

assertThrows(IllegalStateException.class, outer::close);
}

@Test
public void rejectsFinalField() {
assertThrows(IllegalArgumentException.class, () -> StaticOverride.install(Holder.class, "CONSTANT", "x"));
assertSame("constant", Holder.CONSTANT);
}

@Test
public void rejectsNonStaticField() {
assertThrows(IllegalArgumentException.class, () -> StaticOverride.install(Holder.class, "nonStatic", "x"));
}

@Test
public void rejectsMissingField() {
IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> StaticOverride.install(Holder.class, "missing", "x"));
assertTrue(e.getCause() instanceof NoSuchFieldException);
}

@Test
public void rejectsWrongType() {
assertThrows(IllegalArgumentException.class, () -> StaticOverride.install(Holder.class, "instance", 42));
assertNull(Holder.instance);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 Tony Germano <tony@germano.name>

package com.mirth.connect.donkey.testing;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.concurrent.atomic.AtomicBoolean;

/**
* Temporarily replaces the value of a static field, such as a singleton instance, for the duration
* of a test.
*/
public final class StaticOverride implements AutoCloseable {
private final Class<?> owner;
private final Field field;
private final Object value;
private final Object previous;
private final AtomicBoolean closed = new AtomicBoolean();

private StaticOverride(Class<?> owner, Field field, Object value, Object previous) {
this.owner = owner;
this.field = field;
this.value = value;
this.previous = previous;
}

/**
* Sets a static field to {@code value} until the returned handle is closed, then puts the
* previous value back. Closing more than once does nothing.
*
* @throws IllegalArgumentException
* if the field does not exist, is not a non-final static field, or cannot hold
* {@code value}
*/
public static StaticOverride install(Class<?> owner, String fieldName, Object value) {
Field field;
try {
field = owner.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
throw new IllegalArgumentException(owner.getName() + " has no field named " + fieldName, e);
}

int modifiers = field.getModifiers();
if (!Modifier.isStatic(modifiers) || Modifier.isFinal(modifiers)) {
throw new IllegalArgumentException(owner.getName() + "." + fieldName + " is not a non-final static field");
}
field.setAccessible(true);

synchronized (owner) {
Object previous = get(field);
set(field, value);
return new StaticOverride(owner, field, value, previous);
}
}

/**
* Puts back the value the field had before this override was installed.
*
* @throws IllegalStateException
* if something else replaced the field's value after this override was installed
*/
@Override
public void close() {
if (!closed.compareAndSet(false, true)) {
return;
}

synchronized (owner) {
Object current = get(field);
if (current != value) {
throw new IllegalStateException(owner.getName() + "." + field.getName() + " was replaced by " + current + " while " + value + " was installed");
}
set(field, previous);
}
}

private static Object get(Field field) {
try {
return field.get(null);
} catch (IllegalAccessException e) {
throw new IllegalStateException(e);
}
}

private static void set(Field field, Object value) {
try {
field.set(null, value);
} catch (IllegalAccessException e) {
throw new IllegalStateException(e);
}
}
}
3 changes: 0 additions & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,6 @@ glassfish-ha-api = { module = "org.glassfish.ha:ha-api", version = "3.1.9" }
glassfish-management-api = { module = "org.glassfish.external:management-api", version = "3.2.1-b001" }
gmbal-api-only = { module = "org.glassfish.gmbal:gmbal-api-only", version = "3.1.0-b001" }
guava = { module = "com.google.guava:guava", version = "32.0.1-jre" }
guice = { module = "com.google.inject:guice", version = "4.1.0" }
hamcrest = { module = "org.hamcrest:hamcrest", version = "2.2" }
hapi-structures-v27 = { module = "ca.uhn.hapi:hapi-structures-v27", version.ref = "hapi" }
hapi-structures-v28 = { module = "ca.uhn.hapi:hapi-structures-v28", version.ref = "hapi" }
Expand Down Expand Up @@ -413,7 +412,6 @@ donkey-main = [
"error-prone-annotations",
"failureaccess",
"guava",
"guice",
"hikaricp",
"j2objc-annotations",
"javassist",
Expand Down Expand Up @@ -542,7 +540,6 @@ server-main = [
"glassfish-ha-api",
"glassfish-management-api",
"gmbal-api-only",
"guice",
"hapi-structures-v27",
"hapi-structures-v28",
"hapi-structures-v281",
Expand Down
2 changes: 0 additions & 2 deletions gradle/vendored-layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,6 @@
"com.google.guava:failureaccess": "guava",
"com.google.guava:guava": "guava",
"com.google.guava:listenablefuture": "guava",
"com.google.inject:guice": "",
"com.google.j2objc:j2objc-annotations": "guava",
"com.microsoft.sqlserver:mssql-jdbc": "database",
"com.mysql:mysql-connector-j": "database",
Expand Down Expand Up @@ -183,7 +182,6 @@
"com.github.mwiede:jsch": "",
"com.github.oshi:oshi-core": "",
"com.github.zafarkhaja:java-semver": "",
"com.google.inject:guice": "",
"com.microsoft.sqlserver:mssql-jdbc": "database",
"com.mysql:mysql-connector-j": "database",
"com.oracle.database.jdbc:ojdbc8": "database",
Expand Down
Loading
Loading