mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2024-12-14 04:00:30 +00:00
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.
This commit is contained in:
parent
7ad6132494
commit
6322e72110
@ -53,6 +53,7 @@ import java.util.concurrent.locks.ReentrantLock;
|
|||||||
*/
|
*/
|
||||||
final class ComputerExecutor
|
final class ComputerExecutor
|
||||||
{
|
{
|
||||||
|
static ILuaMachine.Factory luaFactory = CobaltLuaMachine::new;
|
||||||
private static final int QUEUE_LIMIT = 256;
|
private static final int QUEUE_LIMIT = 256;
|
||||||
|
|
||||||
private final Computer computer;
|
private final Computer computer;
|
||||||
@ -400,7 +401,7 @@ final class ComputerExecutor
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create the lua machine
|
// 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.
|
// 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 );
|
for( ILuaAPI api : apis ) machine.addAPI( api instanceof ApiWrapper ? ((ApiWrapper) api).getDelegate() : api );
|
||||||
|
@ -7,6 +7,8 @@ package dan200.computercraft.core.lua;
|
|||||||
|
|
||||||
import dan200.computercraft.api.lua.IDynamicLuaObject;
|
import dan200.computercraft.api.lua.IDynamicLuaObject;
|
||||||
import dan200.computercraft.api.lua.ILuaAPI;
|
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.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
@ -63,4 +65,9 @@ public interface ILuaMachine
|
|||||||
* Close the Lua machine, aborting any running functions and deleting the internal state.
|
* Close the Lua machine, aborting any running functions and deleting the internal state.
|
||||||
*/
|
*/
|
||||||
void close();
|
void close();
|
||||||
|
|
||||||
|
interface Factory
|
||||||
|
{
|
||||||
|
ILuaMachine create( Computer computer, TimeoutState timeout );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
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.MatcherAssert.assertThat;
|
||||||
import static org.hamcrest.Matchers.*;
|
import static org.hamcrest.Matchers.*;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
@ -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 );
|
||||||
|
}
|
||||||
|
}
|
@ -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<Computer, Queue<Task>> 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 <T extends Throwable> void rethrow( Throwable e ) throws T
|
||||||
|
{
|
||||||
|
throw (T) e;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class DummyLuaMachine implements ILuaMachine
|
||||||
|
{
|
||||||
|
private final TimeoutState state;
|
||||||
|
private final Queue<Task> handleEvent;
|
||||||
|
|
||||||
|
DummyLuaMachine( TimeoutState state, Queue<Task> 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()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
package dan200.computercraft.core.terminal;
|
package dan200.computercraft.core.terminal;
|
||||||
|
|
||||||
import dan200.computercraft.ContramapMatcher;
|
import dan200.computercraft.support.ContramapMatcher;
|
||||||
import org.hamcrest.Matcher;
|
import org.hamcrest.Matcher;
|
||||||
import org.hamcrest.Matchers;
|
import org.hamcrest.Matchers;
|
||||||
|
|
||||||
@ -36,11 +36,11 @@ public class TerminalMatchers
|
|||||||
|
|
||||||
public static Matcher<Terminal> linesMatchWith( String kind, LineProvider getLine, Matcher<String>[] lines )
|
public static Matcher<Terminal> linesMatchWith( String kind, LineProvider getLine, Matcher<String>[] lines )
|
||||||
{
|
{
|
||||||
return new ContramapMatcher<>( kind, terminal -> {
|
return ContramapMatcher.contramap( Matchers.array( lines ), kind, terminal -> {
|
||||||
String[] termLines = new String[terminal.getHeight()];
|
String[] termLines = new String[terminal.getHeight()];
|
||||||
for( int i = 0; i < termLines.length; i++ ) termLines[i] = getLine.getLine( terminal, i ).toString();
|
for( int i = 0; i < termLines.length; i++ ) termLines[i] = getLine.getLine( terminal, i ).toString();
|
||||||
return termLines;
|
return termLines;
|
||||||
}, Matchers.array( lines ) );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
|
@ -7,7 +7,7 @@ package dan200.computercraft.core.terminal;
|
|||||||
|
|
||||||
import dan200.computercraft.api.lua.LuaValues;
|
import dan200.computercraft.api.lua.LuaValues;
|
||||||
import dan200.computercraft.shared.util.Colour;
|
import dan200.computercraft.shared.util.Colour;
|
||||||
import dan200.computercraft.utils.CallCounter;
|
import dan200.computercraft.support.CallCounter;
|
||||||
import io.netty.buffer.Unpooled;
|
import io.netty.buffer.Unpooled;
|
||||||
import net.minecraft.nbt.CompoundNBT;
|
import net.minecraft.nbt.CompoundNBT;
|
||||||
import net.minecraft.network.PacketBuffer;
|
import net.minecraft.network.PacketBuffer;
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
* Send enquiries to dratcliffe@gmail.com
|
* Send enquiries to dratcliffe@gmail.com
|
||||||
*/
|
*/
|
||||||
package dan200.computercraft.utils;
|
package dan200.computercraft.support;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
@ -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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,42 +3,34 @@
|
|||||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
* Send enquiries to dratcliffe@gmail.com
|
* 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.Matcher;
|
||||||
import org.hamcrest.TypeSafeDiagnosingMatcher;
|
|
||||||
|
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
public class ContramapMatcher<T, U> extends TypeSafeDiagnosingMatcher<T>
|
/**
|
||||||
|
* Given some function from {@code T} to {@code U}, converts a {@code Matcher<U>} to {@code Matcher<T>}. This is useful
|
||||||
|
* when you want to match on a particular field (or some other projection) as part of a larger matcher.
|
||||||
|
*
|
||||||
|
* @param <T> The type of the object to be matched.
|
||||||
|
* @param <U> The type of the projection/field to be matched.
|
||||||
|
*/
|
||||||
|
public final class ContramapMatcher<T, U> extends FeatureMatcher<T, U>
|
||||||
{
|
{
|
||||||
private final String desc;
|
|
||||||
private final Function<T, U> convert;
|
private final Function<T, U> convert;
|
||||||
private final Matcher<U> matcher;
|
|
||||||
|
|
||||||
public ContramapMatcher( String desc, Function<T, U> convert, Matcher<U> matcher )
|
public ContramapMatcher( String desc, Function<T, U> convert, Matcher<U> matcher )
|
||||||
{
|
{
|
||||||
this.desc = desc;
|
super( matcher, desc, desc );
|
||||||
this.convert = convert;
|
this.convert = convert;
|
||||||
this.matcher = matcher;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean matchesSafely( T item, Description mismatchDescription )
|
protected U featureValueOf( T actual )
|
||||||
{
|
{
|
||||||
U converted = convert.apply( item );
|
return convert.apply( actual );
|
||||||
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 );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <T, U> Matcher<T> contramap( Matcher<U> matcher, String desc, Function<T, U> convert )
|
public static <T, U> Matcher<T> contramap( Matcher<U> matcher, String desc, Function<T, U> convert )
|
110
src/test/java/dan200/computercraft/support/IsolatedRunner.java
Normal file
110
src/test/java/dan200/computercraft/support/IsolatedRunner.java
Normal file
@ -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 <strong>IS NOT</strong> 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<Void> invocation, ReflectiveInvocationContext<Method> 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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
2
src/test/resources/junit-platform.properties
Normal file
2
src/test/resources/junit-platform.properties
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
junit.jupiter.execution.parallel.enabled=true
|
||||||
|
junit.jupiter.execution.parallel.config.dynamic.factor=4
|
Loading…
Reference in New Issue
Block a user