From 6322e72110bf690c4fcb65fbbcf8d8c61266e777 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Tue, 3 May 2022 22:58:28 +0100 Subject: [PATCH] Add a test harness for ComputerThread Geesh, this is nasty. Because ComputerThread is incredibly stateful, and we want to run tests in isolation, we run each test inside its own isolated ClassLoader (and thus ComputerThread instance). Everything else is less nasty, though still a bit ... yuck. We also define a custom ILuaMachine which just runs lambdas[^1], and some utilities for starting those. This is then tied together for four very basic tests. This is sufficient for the changes I want to make, but might be nice to test some more comprehensive stuff later on (e.g. timeouts after pausing). [^1]: Which also means the ILuaMachine implementation can be changed by other mods[^2], if someone wants to have another stab at LuaJIT :p. [^2]: In theory. I doubt its possible in practice because so much is package private. --- .../core/computer/ComputerExecutor.java | 3 +- .../computercraft/core/lua/ILuaMachine.java | 7 + .../computercraft/core/asm/GeneratorTest.java | 2 +- .../core/computer/ComputerThreadTest.java | 107 +++++++++ .../core/computer/FakeComputerManager.java | 219 ++++++++++++++++++ .../core/terminal/TerminalMatchers.java | 6 +- .../core/terminal/TerminalTest.java | 2 +- .../{utils => support}/CallCounter.java | 2 +- .../support/ConcurrentHelpers.java | 56 +++++ .../{ => support}/ContramapMatcher.java | 34 ++- .../computercraft/support/IsolatedRunner.java | 110 +++++++++ src/test/resources/junit-platform.properties | 2 + 12 files changed, 522 insertions(+), 28 deletions(-) create mode 100644 src/test/java/dan200/computercraft/core/computer/ComputerThreadTest.java create mode 100644 src/test/java/dan200/computercraft/core/computer/FakeComputerManager.java rename src/test/java/dan200/computercraft/{utils => support}/CallCounter.java (95%) create mode 100644 src/test/java/dan200/computercraft/support/ConcurrentHelpers.java rename src/test/java/dan200/computercraft/{ => support}/ContramapMatcher.java (50%) create mode 100644 src/test/java/dan200/computercraft/support/IsolatedRunner.java create mode 100644 src/test/resources/junit-platform.properties diff --git a/src/main/java/dan200/computercraft/core/computer/ComputerExecutor.java b/src/main/java/dan200/computercraft/core/computer/ComputerExecutor.java index fb59fad7f..c6a3a7a34 100644 --- a/src/main/java/dan200/computercraft/core/computer/ComputerExecutor.java +++ b/src/main/java/dan200/computercraft/core/computer/ComputerExecutor.java @@ -53,6 +53,7 @@ import java.util.concurrent.locks.ReentrantLock; */ final class ComputerExecutor { + static ILuaMachine.Factory luaFactory = CobaltLuaMachine::new; private static final int QUEUE_LIMIT = 256; private final Computer computer; @@ -400,7 +401,7 @@ final class ComputerExecutor } // Create the lua machine - ILuaMachine machine = new CobaltLuaMachine( computer, timeout ); + ILuaMachine machine = luaFactory.create( computer, timeout ); // Add the APIs. We unwrap them (yes, this is horrible) to get access to the underlying object. for( ILuaAPI api : apis ) machine.addAPI( api instanceof ApiWrapper ? ((ApiWrapper) api).getDelegate() : api ); diff --git a/src/main/java/dan200/computercraft/core/lua/ILuaMachine.java b/src/main/java/dan200/computercraft/core/lua/ILuaMachine.java index f08f23788..1b981696a 100644 --- a/src/main/java/dan200/computercraft/core/lua/ILuaMachine.java +++ b/src/main/java/dan200/computercraft/core/lua/ILuaMachine.java @@ -7,6 +7,8 @@ package dan200.computercraft.core.lua; import dan200.computercraft.api.lua.IDynamicLuaObject; import dan200.computercraft.api.lua.ILuaAPI; +import dan200.computercraft.core.computer.Computer; +import dan200.computercraft.core.computer.TimeoutState; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -63,4 +65,9 @@ public interface ILuaMachine * Close the Lua machine, aborting any running functions and deleting the internal state. */ void close(); + + interface Factory + { + ILuaMachine create( Computer computer, TimeoutState timeout ); + } } diff --git a/src/test/java/dan200/computercraft/core/asm/GeneratorTest.java b/src/test/java/dan200/computercraft/core/asm/GeneratorTest.java index 51e98d257..4ebe80167 100644 --- a/src/test/java/dan200/computercraft/core/asm/GeneratorTest.java +++ b/src/test/java/dan200/computercraft/core/asm/GeneratorTest.java @@ -18,7 +18,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import static dan200.computercraft.ContramapMatcher.contramap; +import static dan200.computercraft.support.ContramapMatcher.contramap; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; import static org.junit.jupiter.api.Assertions.assertThrows; diff --git a/src/test/java/dan200/computercraft/core/computer/ComputerThreadTest.java b/src/test/java/dan200/computercraft/core/computer/ComputerThreadTest.java new file mode 100644 index 000000000..7901de7fa --- /dev/null +++ b/src/test/java/dan200/computercraft/core/computer/ComputerThreadTest.java @@ -0,0 +1,107 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.core.computer; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.core.lua.MachineResult; +import dan200.computercraft.support.ConcurrentHelpers; +import dan200.computercraft.support.IsolatedRunner; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; + +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.closeTo; +import static org.junit.jupiter.api.Assertions.*; + +@Timeout( value = 15 ) +@ExtendWith( IsolatedRunner.class ) +@Execution( ExecutionMode.CONCURRENT ) +public class ComputerThreadTest +{ + @Test + public void testSoftAbort() throws Exception + { + Computer computer = FakeComputerManager.create(); + FakeComputerManager.enqueue( computer, timeout -> { + assertFalse( timeout.isSoftAborted(), "Should not start soft-aborted" ); + + long delay = ConcurrentHelpers.waitUntil( () -> { + timeout.refresh(); + return timeout.isSoftAborted(); + } ); + assertThat( "Should be soft aborted", delay * 1e-9, closeTo( 7, 0.5 ) ); + ComputerCraft.log.info( "Slept for {}", delay ); + + computer.shutdown(); + return MachineResult.OK; + } ); + + FakeComputerManager.startAndWait( computer ); + } + + @Test + public void testHardAbort() throws Exception + { + Computer computer = FakeComputerManager.create(); + FakeComputerManager.enqueue( computer, timeout -> { + assertFalse( timeout.isHardAborted(), "Should not start soft-aborted" ); + + assertThrows( InterruptedException.class, () -> Thread.sleep( 11_000 ), "Sleep should be hard aborted" ); + assertTrue( timeout.isHardAborted(), "Thread should be hard aborted" ); + + computer.shutdown(); + return MachineResult.OK; + } ); + + FakeComputerManager.startAndWait( computer ); + } + + @Test + public void testNoPauseIfNoOtherMachines() throws Exception + { + Computer computer = FakeComputerManager.create(); + FakeComputerManager.enqueue( computer, timeout -> { + boolean didPause = ConcurrentHelpers.waitUntil( () -> { + timeout.refresh(); + return timeout.isPaused(); + }, 5, TimeUnit.SECONDS ); + assertFalse( didPause, "Machine shouldn't have paused within 5s" ); + + computer.shutdown(); + return MachineResult.OK; + } ); + + FakeComputerManager.startAndWait( computer ); + } + + @Test + public void testPauseIfSomeOtherMachine() throws Exception + { + Computer computer = FakeComputerManager.create(); + FakeComputerManager.enqueue( computer, timeout -> { + long budget = ComputerThread.scaledPeriod(); + assertEquals( budget, TimeUnit.MILLISECONDS.toNanos( 25 ), "Budget should be 25ms" ); + + long delay = ConcurrentHelpers.waitUntil( () -> { + timeout.refresh(); + return timeout.isPaused(); + } ); + assertThat( "Paused within 25ms", delay * 1e-9, closeTo( 0.025, 0.01 ) ); + + computer.shutdown(); + return MachineResult.OK; + } ); + + FakeComputerManager.createLoopingComputer(); + + FakeComputerManager.startAndWait( computer ); + } +} diff --git a/src/test/java/dan200/computercraft/core/computer/FakeComputerManager.java b/src/test/java/dan200/computercraft/core/computer/FakeComputerManager.java new file mode 100644 index 000000000..ccbf101ba --- /dev/null +++ b/src/test/java/dan200/computercraft/core/computer/FakeComputerManager.java @@ -0,0 +1,219 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.core.computer; + +import dan200.computercraft.api.lua.ILuaAPI; +import dan200.computercraft.core.lua.ILuaMachine; +import dan200.computercraft.core.lua.MachineResult; +import dan200.computercraft.core.terminal.Terminal; +import dan200.computercraft.support.IsolatedRunner; +import org.jetbrains.annotations.Nullable; + +import javax.annotation.Nonnull; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Creates "fake" computers, which just run user-defined tasks rather than Lua code. + * + * Note, this will clobber some parts of the global state. It's recommended you use this inside an {@link IsolatedRunner}. + */ +public class FakeComputerManager +{ + interface Task + { + MachineResult run( TimeoutState state ) throws Exception; + } + + private static final Map> machines = new HashMap<>(); + + private static final Lock errorLock = new ReentrantLock(); + private static final Condition hasError = errorLock.newCondition(); + private static volatile Throwable error; + + static + { + ComputerExecutor.luaFactory = ( computer, timeout ) -> new DummyLuaMachine( timeout, machines.get( computer ) ); + } + + /** + * Create a new computer which pulls from our task queue. + * + * @return The computer. This will not be started yet, you must call {@link Computer#turnOn()} and + * {@link Computer#tick()} to do so. + */ + @Nonnull + public static Computer create() + { + Computer computer = new Computer( new BasicEnvironment(), new Terminal( 51, 19 ), 0 ); + machines.put( computer, new ConcurrentLinkedQueue<>() ); + return computer; + } + + /** + * Create and start a new computer which loops forever. + */ + public static void createLoopingComputer() + { + Computer computer = create(); + enqueueForever( computer, t -> { + Thread.sleep( 100 ); + return MachineResult.OK; + } ); + computer.turnOn(); + computer.tick(); + } + + /** + * Enqueue a task on a computer. + * + * @param computer The computer to enqueue the work on. + * @param task The task to run. + */ + public static void enqueue( @Nonnull Computer computer, @Nonnull Task task ) + { + machines.get( computer ).offer( task ); + } + + /** + * Enqueue a repeated task on a computer. This is automatically requeued when the task finishes, meaning the task + * queue is never empty. + * + * @param computer The computer to enqueue the work on. + * @param task The task to run. + */ + private static void enqueueForever( @Nonnull Computer computer, @Nonnull Task task ) + { + machines.get( computer ).offer( t -> { + MachineResult result = task.run( t ); + + enqueueForever( computer, task ); + computer.queueEvent( "some_event", null ); + return result; + } ); + } + + /** + * Sleep for a given period, immediately propagating any exceptions thrown by a computer. + * + * @param delay The duration to sleep for. + * @param unit The time unit the duration is measured in. + * @throws Exception An exception thrown by a running computer. + */ + public static void sleep( long delay, TimeUnit unit ) throws Exception + { + errorLock.lock(); + try + { + rethrowIfNeeded(); + if( hasError.await( delay, unit ) ) rethrowIfNeeded(); + } + finally + { + errorLock.unlock(); + } + } + + /** + * Start a computer and wait for it to finish. + * + * @param computer The computer to wait for. + * @throws Exception An exception thrown by a running computer. + */ + public static void startAndWait( Computer computer ) throws Exception + { + computer.turnOn(); + computer.tick(); + + do + { + sleep( 100, TimeUnit.MILLISECONDS ); + } while( ComputerThread.hasPendingWork() || computer.isOn() ); + + rethrowIfNeeded(); + } + + private static void rethrowIfNeeded() throws Exception + { + if( error == null ) return; + if( error instanceof Exception ) throw (Exception) error; + if( error instanceof Error ) throw (Error) error; + rethrow( error ); + } + + @SuppressWarnings( "unchecked" ) + private static void rethrow( Throwable e ) throws T + { + throw (T) e; + } + + private static class DummyLuaMachine implements ILuaMachine + { + private final TimeoutState state; + private final Queue handleEvent; + + DummyLuaMachine( TimeoutState state, Queue handleEvent ) + { + this.state = state; + this.handleEvent = handleEvent; + } + + @Override + public void addAPI( @Nonnull ILuaAPI api ) + { + } + + @Override + public MachineResult loadBios( @Nonnull InputStream bios ) + { + return MachineResult.OK; + } + + @Override + public MachineResult handleEvent( @Nullable String eventName, @Nullable Object[] arguments ) + { + try + { + return handleEvent.remove().run( state ); + } + catch( Throwable e ) + { + errorLock.lock(); + try + { + if( error == null ) + { + error = e; + hasError.signal(); + } + else + { + error.addSuppressed( e ); + } + } + finally + { + errorLock.unlock(); + } + + if( !(e instanceof Exception) && !(e instanceof AssertionError) ) rethrow( e ); + return MachineResult.error( e.getMessage() ); + } + } + + @Override + public void close() + { + } + } +} diff --git a/src/test/java/dan200/computercraft/core/terminal/TerminalMatchers.java b/src/test/java/dan200/computercraft/core/terminal/TerminalMatchers.java index e72932472..3359f5c16 100644 --- a/src/test/java/dan200/computercraft/core/terminal/TerminalMatchers.java +++ b/src/test/java/dan200/computercraft/core/terminal/TerminalMatchers.java @@ -5,7 +5,7 @@ */ package dan200.computercraft.core.terminal; -import dan200.computercraft.ContramapMatcher; +import dan200.computercraft.support.ContramapMatcher; import org.hamcrest.Matcher; import org.hamcrest.Matchers; @@ -36,11 +36,11 @@ public class TerminalMatchers public static Matcher linesMatchWith( String kind, LineProvider getLine, Matcher[] lines ) { - return new ContramapMatcher<>( kind, terminal -> { + return ContramapMatcher.contramap( Matchers.array( lines ), kind, terminal -> { String[] termLines = new String[terminal.getHeight()]; for( int i = 0; i < termLines.length; i++ ) termLines[i] = getLine.getLine( terminal, i ).toString(); return termLines; - }, Matchers.array( lines ) ); + } ); } @FunctionalInterface diff --git a/src/test/java/dan200/computercraft/core/terminal/TerminalTest.java b/src/test/java/dan200/computercraft/core/terminal/TerminalTest.java index a68087980..044aca0d8 100644 --- a/src/test/java/dan200/computercraft/core/terminal/TerminalTest.java +++ b/src/test/java/dan200/computercraft/core/terminal/TerminalTest.java @@ -7,7 +7,7 @@ package dan200.computercraft.core.terminal; import dan200.computercraft.api.lua.LuaValues; import dan200.computercraft.shared.util.Colour; -import dan200.computercraft.utils.CallCounter; +import dan200.computercraft.support.CallCounter; import io.netty.buffer.Unpooled; import net.minecraft.nbt.CompoundNBT; import net.minecraft.network.PacketBuffer; diff --git a/src/test/java/dan200/computercraft/utils/CallCounter.java b/src/test/java/dan200/computercraft/support/CallCounter.java similarity index 95% rename from src/test/java/dan200/computercraft/utils/CallCounter.java rename to src/test/java/dan200/computercraft/support/CallCounter.java index 5dbae63f7..213ad13e0 100644 --- a/src/test/java/dan200/computercraft/utils/CallCounter.java +++ b/src/test/java/dan200/computercraft/support/CallCounter.java @@ -3,7 +3,7 @@ * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ -package dan200.computercraft.utils; +package dan200.computercraft.support; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/src/test/java/dan200/computercraft/support/ConcurrentHelpers.java b/src/test/java/dan200/computercraft/support/ConcurrentHelpers.java new file mode 100644 index 000000000..640f55700 --- /dev/null +++ b/src/test/java/dan200/computercraft/support/ConcurrentHelpers.java @@ -0,0 +1,56 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.support; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.LockSupport; +import java.util.function.BooleanSupplier; + +/** + * Utilities for working with concurrent systems. + */ +public class ConcurrentHelpers +{ + private static final long DELAY = TimeUnit.MILLISECONDS.toNanos( 2 ); + + /** + * Wait until a condition is true, checking the condition every 2ms. + * + * @param isTrue The condition to check + * @return How long we waited for. + */ + public static long waitUntil( BooleanSupplier isTrue ) + { + long start = System.nanoTime(); + while( true ) + { + if( isTrue.getAsBoolean() ) return System.nanoTime() - start; + LockSupport.parkNanos( DELAY ); + } + } + + /** + * Wait until a condition is true or a timeout is elapsed, checking the condition every 2ms. + * + * @param isTrue The condition to check + * @param timeout The delay after which we will timeout. + * @param unit The time unit the duration is measured in. + * @return {@literal true} if the condition was met, {@literal false} if we timed out instead. + */ + public static boolean waitUntil( BooleanSupplier isTrue, long timeout, TimeUnit unit ) + { + long start = System.nanoTime(); + long timeoutNs = unit.toNanos( timeout ); + while( true ) + { + long time = System.nanoTime() - start; + if( isTrue.getAsBoolean() ) return true; + if( time > timeoutNs ) return false; + + LockSupport.parkNanos( DELAY ); + } + } +} diff --git a/src/test/java/dan200/computercraft/ContramapMatcher.java b/src/test/java/dan200/computercraft/support/ContramapMatcher.java similarity index 50% rename from src/test/java/dan200/computercraft/ContramapMatcher.java rename to src/test/java/dan200/computercraft/support/ContramapMatcher.java index 5126ef280..6f09aab96 100644 --- a/src/test/java/dan200/computercraft/ContramapMatcher.java +++ b/src/test/java/dan200/computercraft/support/ContramapMatcher.java @@ -3,42 +3,34 @@ * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ -package dan200.computercraft; +package dan200.computercraft.support; -import org.hamcrest.Description; +import org.hamcrest.FeatureMatcher; import org.hamcrest.Matcher; -import org.hamcrest.TypeSafeDiagnosingMatcher; import java.util.function.Function; -public class ContramapMatcher extends TypeSafeDiagnosingMatcher +/** + * Given some function from {@code T} to {@code U}, converts a {@code Matcher} to {@code Matcher}. This is useful + * when you want to match on a particular field (or some other projection) as part of a larger matcher. + * + * @param The type of the object to be matched. + * @param The type of the projection/field to be matched. + */ +public final class ContramapMatcher extends FeatureMatcher { - private final String desc; private final Function convert; - private final Matcher matcher; public ContramapMatcher( String desc, Function convert, Matcher matcher ) { - this.desc = desc; + super( matcher, desc, desc ); this.convert = convert; - this.matcher = matcher; } @Override - protected boolean matchesSafely( T item, Description mismatchDescription ) + protected U featureValueOf( T actual ) { - U converted = convert.apply( item ); - if( matcher.matches( converted ) ) return true; - - mismatchDescription.appendText( desc ).appendText( " " ); - matcher.describeMismatch( converted, mismatchDescription ); - return false; - } - - @Override - public void describeTo( Description description ) - { - description.appendText( desc ).appendText( " " ).appendDescriptionOf( matcher ); + return convert.apply( actual ); } public static Matcher contramap( Matcher matcher, String desc, Function convert ) diff --git a/src/test/java/dan200/computercraft/support/IsolatedRunner.java b/src/test/java/dan200/computercraft/support/IsolatedRunner.java new file mode 100644 index 000000000..5d95fcd00 --- /dev/null +++ b/src/test/java/dan200/computercraft/support/IsolatedRunner.java @@ -0,0 +1,110 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.support; + +import com.google.common.io.ByteStreams; +import net.minecraftforge.fml.unsafe.UnsafeHacks; +import org.junit.jupiter.api.extension.*; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.security.CodeSource; +import java.security.SecureClassLoader; + +/** + * Runs a test method in an entirely isolated {@link ClassLoader}, so you can mess around with as much of + * {@link dan200.computercraft} as you like. + * + * This IS NOT a good idea, but helps us run some tests in parallel while having lots of (terrible) + * global state. + */ +public class IsolatedRunner implements InvocationInterceptor, BeforeEachCallback, AfterEachCallback +{ + private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace.create( new Object() ); + + @Override + public void beforeEach( ExtensionContext context ) throws Exception + { + ClassLoader loader = context.getStore( NAMESPACE ).getOrComputeIfAbsent( IsolatedClassLoader.class ); + + // Rename the global thread group to something more obvious. + ThreadGroup group = (ThreadGroup) loader.loadClass( "dan200.computercraft.shared.util.ThreadUtils" ).getMethod( "group" ).invoke( null ); + Field field = ThreadGroup.class.getDeclaredField( "name" ); + UnsafeHacks.setField( field, group, "<" + context.getDisplayName() + ">" ); + } + + @Override + public void afterEach( ExtensionContext context ) throws Exception + { + ClassLoader loader = context.getStore( NAMESPACE ).get( IsolatedClassLoader.class, IsolatedClassLoader.class ); + loader.loadClass( "dan200.computercraft.core.computer.ComputerThread" ) + .getDeclaredMethod( "stop" ) + .invoke( null ); + } + + + @Override + public void interceptTestMethod( Invocation invocation, ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext ) throws Throwable + { + invocation.skip(); + + ClassLoader loader = extensionContext.getStore( NAMESPACE ).get( IsolatedClassLoader.class, IsolatedClassLoader.class ); + Method method = invocationContext.getExecutable(); + + Class ourClass = loader.loadClass( method.getDeclaringClass().getName() ); + Method ourMethod = ourClass.getDeclaredMethod( method.getName(), method.getParameterTypes() ); + + try + { + ourMethod.invoke( ourClass.getConstructor().newInstance(), invocationContext.getArguments().toArray() ); + } + catch( InvocationTargetException e ) + { + throw e.getTargetException(); + } + } + + private static class IsolatedClassLoader extends SecureClassLoader + { + IsolatedClassLoader() + { + super( IsolatedClassLoader.class.getClassLoader() ); + } + + @Override + public Class loadClass( String name, boolean resolve ) throws ClassNotFoundException + { + synchronized( getClassLoadingLock( name ) ) + { + Class c = findLoadedClass( name ); + if( c != null ) return c; + + if( name.startsWith( "dan200.computercraft." ) ) + { + CodeSource parentSource = getParent().loadClass( name ).getProtectionDomain().getCodeSource(); + + byte[] contents; + try( InputStream stream = getResourceAsStream( name.replace( '.', '/' ) + ".class" ) ) + { + if( stream == null ) throw new ClassNotFoundException( name ); + contents = ByteStreams.toByteArray( stream ); + } + catch( IOException e ) + { + throw new ClassNotFoundException( name, e ); + } + + return defineClass( name, contents, 0, contents.length, parentSource ); + } + } + + return super.loadClass( name, resolve ); + } + } +} diff --git a/src/test/resources/junit-platform.properties b/src/test/resources/junit-platform.properties new file mode 100644 index 000000000..33ac738be --- /dev/null +++ b/src/test/resources/junit-platform.properties @@ -0,0 +1,2 @@ +junit.jupiter.execution.parallel.enabled=true +junit.jupiter.execution.parallel.config.dynamic.factor=4