From 8819f2559d85c99a408993a6d80379ea8d1d498b Mon Sep 17 00:00:00 2001 From: SquidDev Date: Tue, 18 Sep 2018 18:21:00 +0100 Subject: [PATCH] Add some tests for the new executor system And fix a couple of bugs picked up by said tests --- build.gradle | 6 + .../computercraft/api/lua/ILuaObject.java | 12 +- .../computercraft/api/lua/MethodResult.java | 20 +- .../computercraft/core/computer/Computer.java | 2 +- .../core/lua/CobaltLuaContext.java | 7 +- .../core/lua/CobaltLuaMachine.java | 1 + .../core/lua/CobaltWrapperFunction.java | 34 ++- .../computer/FakeComputerEnvironment.java | 91 +++++++ .../core/computer/RunOnComputer.java | 155 +++++++++++ .../core/lua/CobaltWrapperFunctionTest.java | 243 ++++++++++++++++++ .../shared/wired/NetworkTest.java | 17 +- 11 files changed, 558 insertions(+), 30 deletions(-) create mode 100644 src/test/java/dan200/computercraft/core/computer/FakeComputerEnvironment.java create mode 100644 src/test/java/dan200/computercraft/core/computer/RunOnComputer.java create mode 100644 src/test/java/dan200/computercraft/core/lua/CobaltWrapperFunctionTest.java diff --git a/build.gradle b/build.gradle index 628eb784d..c1de95b21 100644 --- a/build.gradle +++ b/build.gradle @@ -197,3 +197,9 @@ gradle.projectsEvaluated { runClient.outputs.upToDateWhen { false } runServer.outputs.upToDateWhen { false } + +test { + testLogging { + events "failed", "standardOut", "standardError" + } +} diff --git a/src/main/java/dan200/computercraft/api/lua/ILuaObject.java b/src/main/java/dan200/computercraft/api/lua/ILuaObject.java index 2f986d930..4a13a7df4 100644 --- a/src/main/java/dan200/computercraft/api/lua/ILuaObject.java +++ b/src/main/java/dan200/computercraft/api/lua/ILuaObject.java @@ -44,10 +44,12 @@ public interface ILuaObject * for the possible values and conversion rules. * @return An array of objects, representing the values you wish to return to the Lua program. See * {@link MethodResult#of(Object...)} for the valid values and conversion rules. - * @throws LuaException If the task could not be queued, or if the task threw an exception. + * @throws LuaException If you throw any exception from this function, a lua error will be raised with the + * same message as your exception. Use this to throw appropriate errors if the wrong + * arguments are supplied to your method. * @throws InterruptedException If the user shuts down or reboots the computer the coroutine is suspended, * InterruptedException will be thrown. This exception must not be caught or - * intercepted, or the computer will leak memory and end up in a broken state.w + * intercepted, or the computer will leak memory and end up in a broken state. * @see IPeripheral#callMethod(IComputerAccess, ILuaContext, int, Object[]) * @deprecated Use {@link #callMethod(ICallContext, int, Object[])} instead. */ @@ -68,12 +70,14 @@ public interface ILuaObject * for the possible values and conversion rules. * @return The result of calling this method. Use {@link MethodResult#empty()} to return nothing or * {@link MethodResult#of(Object...)} to return several values. - * @throws LuaException If the task could not be queued, or if the task threw an exception. + * @throws LuaException If you throw any exception from this function, a lua error will be raised with the + * same message as your exception. Use this to throw appropriate errors if the wrong + * arguments are supplied to your method. * @see IPeripheral#callMethod(IComputerAccess, ICallContext, int, Object[]) * @see MethodResult */ @Nonnull - @SuppressWarnings({ "deprecation" }) + @SuppressWarnings( { "deprecation" } ) default MethodResult callMethod( @Nonnull ICallContext context, int method, @Nonnull Object[] arguments ) throws LuaException { return MethodResult.withLuaContext( lua -> callMethod( lua, method, arguments ) ); diff --git a/src/main/java/dan200/computercraft/api/lua/MethodResult.java b/src/main/java/dan200/computercraft/api/lua/MethodResult.java index 209c15364..169dd3ecd 100644 --- a/src/main/java/dan200/computercraft/api/lua/MethodResult.java +++ b/src/main/java/dan200/computercraft/api/lua/MethodResult.java @@ -89,16 +89,17 @@ public abstract class MethodResult * Normally you'll wish to consume the event using {@link #then(ILuaFunction)}. This can be done slightly more * easily with {@link #pullEvent(String, ILuaFunction)}. * + * @param filter The event name to filter on. * @return The constructed method result. This evaluates to the name of the event that occurred, and any event * parameters. * @see #pullEvent(String, ILuaFunction) * @see #pullEvent() */ @Nonnull - public static MethodResult pullEvent( @Nonnull String event ) + public static MethodResult pullEvent( @Nonnull String filter ) { - Preconditions.checkNotNull( event, "event cannot be null" ); - return new OnEvent( false, event ); + Preconditions.checkNotNull( filter, "event cannot be null" ); + return new OnEvent( false, filter ); } /** @@ -108,6 +109,7 @@ public abstract class MethodResult * If you want to listen to a specific event, it's easier to use {@link #pullEvent(String, ILuaFunction)} rather * than running until the desired event is found. * + * @param callback The function to call when the event is received. * @return The constructed method result. This evaluates to the result of the {@code callback}. * @see #pullEvent() * @see #pullEvent(String, ILuaFunction) @@ -123,6 +125,8 @@ public abstract class MethodResult * Wait for the specified event to occur on the computer, suspending the coroutine until it arises. This method to * {@link #pullEvent(String)} and {@link #then(ILuaFunction)}. * + * @param filter The event name to filter on. + * @param callback The function to call when the event is received. * @return The constructed method result. This evaluates to the result of the {@code callback}. * @see #pullEvent(String) * @see #pullEvent(ILuaFunction) @@ -151,19 +155,21 @@ public abstract class MethodResult * The same as {@link #pullEvent(String)}, except {@code terminated} events are also passed to the callback, instead * of throwing an error. Only use this if you want to prevent program termination, which is not recommended. * + * @param filter The event name to filter on. * @return The constructed method result. This evaluates to the name of the event that occurred, and any event * parameters. */ @Nonnull - public static MethodResult pullEventRaw( @Nonnull String event ) + public static MethodResult pullEventRaw( @Nonnull String filter ) { - return new OnEvent( true, event ); + return new OnEvent( true, filter ); } /** * The same as {@link #pullEvent(ILuaFunction)}, except {@code terminated} events are also passed to the callback, * instead of throwing an error. Only use this if you want to prevent program termination, which is not recommended. * + * @param callback The function to call when the event is received. * @return The constructed method result. This evaluates to the result of the {@code callback}. */ @Nonnull @@ -178,6 +184,8 @@ public abstract class MethodResult * callback, instead of throwing an error. Only use this if you want to prevent program termination, which is not * recommended. * + * @param filter The event name to filter on. + * @param callback The function to call when the event is received. * @return The constructed method result. This evaluates to the result of the {@code callback}. */ @Nonnull @@ -237,6 +245,8 @@ public abstract class MethodResult * * @param context The context to execute with. * @return The resulting values. + * @throws LuaException If an error was thrown while executing one of the methods within this future. + * @throws InterruptedException If the user shuts down or reboots the computer while the coroutine is suspended. * @see #withLuaContext(ILuaContextTask) * @deprecated This should not be used except to interface between the two call systems. */ diff --git a/src/main/java/dan200/computercraft/core/computer/Computer.java b/src/main/java/dan200/computercraft/core/computer/Computer.java index a4ce06ed6..908aa360d 100644 --- a/src/main/java/dan200/computercraft/core/computer/Computer.java +++ b/src/main/java/dan200/computercraft/core/computer/Computer.java @@ -968,7 +968,7 @@ public class Computer return; } } - + final Computer computer = this; ITask task = new ITask() { @Override diff --git a/src/main/java/dan200/computercraft/core/lua/CobaltLuaContext.java b/src/main/java/dan200/computercraft/core/lua/CobaltLuaContext.java index 7a4d723e2..c963a365e 100644 --- a/src/main/java/dan200/computercraft/core/lua/CobaltLuaContext.java +++ b/src/main/java/dan200/computercraft/core/lua/CobaltLuaContext.java @@ -157,11 +157,14 @@ class CobaltLuaContext extends CobaltCallContext implements ILuaContext } ); } - Object[] resume( LuaState state, CobaltLuaMachine machine, Object[] args ) throws LuaError, UnwindThrowable + void resume( Object[] args ) { values = args; resume.signal(); + } + Object[] await( LuaState state, CobaltLuaMachine machine ) throws LuaError, UnwindThrowable + { if( !done ) { try @@ -170,14 +173,12 @@ class CobaltLuaContext extends CobaltCallContext implements ILuaContext } catch( InterruptedException e ) { - state.debug.onReturn(); throw new LuaError( "Java Exception Thrown: " + e.toString(), 0 ); } } if( done ) { - state.debug.onReturn(); if( exception != null ) throw exception; return values; } diff --git a/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java b/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java index a7827f0d9..528b8336b 100644 --- a/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java +++ b/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java @@ -235,6 +235,7 @@ public class CobaltLuaMachine implements ILuaMachine } catch( LuaError e ) { + if( ComputerCraft.logPeripheralErrors ) ComputerCraft.log.error( "Main thread crashed", e ); m_mainRoutine.abandon(); m_mainRoutine = null; } diff --git a/src/main/java/dan200/computercraft/core/lua/CobaltWrapperFunction.java b/src/main/java/dan200/computercraft/core/lua/CobaltWrapperFunction.java index 8edb795ac..89a2bfae6 100644 --- a/src/main/java/dan200/computercraft/core/lua/CobaltWrapperFunction.java +++ b/src/main/java/dan200/computercraft/core/lua/CobaltWrapperFunction.java @@ -13,8 +13,6 @@ import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.api.lua.MethodResult; import dan200.computercraft.core.computer.Computer; import org.squiddev.cobalt.*; -import org.squiddev.cobalt.debug.DebugFrame; -import org.squiddev.cobalt.debug.DebugHandler; import org.squiddev.cobalt.debug.DebugState; import org.squiddev.cobalt.function.VarArgFunction; @@ -62,12 +60,19 @@ class CobaltWrapperFunction extends VarArgFunction implements Resumable { + } ); + } + + public static void run( String task, Consumer setup ) throws Exception + { + if( ComputerCraft.log == null ) ComputerCraft.log = LogManager.getLogger( "computercraft" ); + ComputerCraft.logPeripheralErrors = true; + + // Setup computer + Terminal terminal = new Terminal( ComputerCraft.terminalWidth_computer, ComputerCraft.terminalHeight_computer ); + Computer computer = new Computer( new FakeComputerEnvironment( 0, true ), terminal, 0 ); + + // Register APIS + TestAPI api = new TestAPI( computer ); + computer.addAPI( api ); + setup.accept( computer ); + + // Setup the startup file + try( OutputStream stream = computer.getRootMount().openForWrite( "startup.lua" ) ) + { + String program = "" + + "local function exec()\n" + + " " + task + "\n" + + "end\n" + + "test.finish(pcall(exec))\n"; + stream.write( program.getBytes( StandardCharsets.UTF_8 ) ); + } + + // Turn on + ComputerThread.start(); + computer.turnOn(); + + // Run until shutdown or we timeout + boolean everOn = false; + int ticks = 0; + do + { + computer.advance( 0.05 ); + MainThread.executePendingTasks(); + + Thread.sleep( 50 ); + ticks++; + everOn |= computer.isOn(); + } + while( (computer.isOn() || (!everOn && ticks < STARTUP_TIMEOUT)) && ticks <= RUN_TIMEOUT ); + + // If we never finished (say, startup errored) then print the terminal. Otherwise throw the error + // where needed. + if( !api.finished ) + { + int height = terminal.getHeight() - 1; + while( height >= 0 && terminal.getLine( height ).toString().trim().isEmpty() ) height--; + for( int y = 0; y <= height; y++ ) + { + System.out.printf( "%2d | %s\n", y + 1, terminal.getLine( y ) ); + } + + fail( "Computer never finished" ); + } + else if( api.error != null ) + { + fail( api.error ); + } + + ComputerThread.stop(); + } + + private static class TestAPI implements ILuaAPI + { + private final Computer computer; + + private boolean finished = false; + private String error; + + private TestAPI( Computer computer ) + { + this.computer = computer; + } + + @Override + public String[] getNames() + { + return new String[]{ "test" }; + } + + @Nonnull + @Override + public String[] getMethodNames() + { + return new String[]{ "log", "finish" }; + } + + @Nullable + @Override + @Deprecated + public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull Object[] arguments ) throws LuaException, InterruptedException + { + return callMethod( (ICallContext) context, method, arguments ).evaluate( context ); + } + + @Nonnull + @Override + public MethodResult callMethod( @Nonnull ICallContext context, int method, @Nonnull Object[] arguments ) + { + switch( method ) + { + case 0: + ComputerCraft.log.info( Objects.toString( arguments.length <= 0 ? null : arguments[0] ) ); + return MethodResult.empty(); + case 1: + { + if( arguments.length <= 0 || arguments[0] == null || arguments[0] == Boolean.FALSE ) + { + error = Objects.toString( arguments.length <= 1 ? null : arguments[1] ); + } + finished = true; + computer.shutdown(); + return MethodResult.empty(); + } + default: + return MethodResult.empty(); + } + } + } +} diff --git a/src/test/java/dan200/computercraft/core/lua/CobaltWrapperFunctionTest.java b/src/test/java/dan200/computercraft/core/lua/CobaltWrapperFunctionTest.java new file mode 100644 index 000000000..a3de7669c --- /dev/null +++ b/src/test/java/dan200/computercraft/core/lua/CobaltWrapperFunctionTest.java @@ -0,0 +1,243 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2018. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.core.lua; + +import dan200.computercraft.api.lua.*; +import dan200.computercraft.core.computer.RunOnComputer; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Collectors; + + +@RunWith( Parameterized.class ) +public class CobaltWrapperFunctionTest +{ + @Parameterized.Parameter( 0 ) + public String name; + + @Parameterized.Parameter( 1 ) + public String code; + + @Parameterized.Parameters( name = "{0}" ) + public static Collection parameters() + { + return Arrays.stream( new Object[][]{ + new Object[]{ "empty", "assert(select('#', funcs.empty()) == 0)" }, + new Object[]{ "identity", "assert(select('#', funcs.identity(1, 2, 3)) == 3)" }, + + new Object[]{ "pullEvent", "os.queueEvent('test') assert(funcs.pullEvent() == 'test')" }, + new Object[]{ "pullEventTerminate", "os.queueEvent('terminate') assert(not pcall(funcs.pullEvent))" }, + + new Object[]{ "pullEventRaw", "os.queueEvent('test') assert(funcs.pullEventRaw() == 'test')" }, + new Object[]{ "pullEventRawTerminate", "os.queueEvent('terminate') assert(funcs.pullEventRaw() == 'terminate')" }, + + new Object[]{ "mainThread", "assert(funcs.mainThread() == 1)" }, + new Object[]{ "mainThreadMany", "for i = 1, 4 do assert(funcs.mainThread() == 1) end" } + } ).collect( Collectors.toList() ); + } + + /** + * Tests executing functions defined through the {@link MethodResult} API. + */ + @Test + public void testMethodResult() throws Exception + { + RunOnComputer.run( code, c -> c.addAPI( new MethodResultAPI() ) ); + } + + /** + * Tests executing functions defined through the {@link MethodResult} API called with the + * {@link ILuaContext} evaluator. + */ + @Test + public void testMethodResultEvaluate() throws Exception + { + RunOnComputer.run( code, c -> c.addAPI( new WrapperAPI( new MethodResultAPI() ) + { + @Nullable + @Override + @Deprecated + public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull Object[] arguments ) throws LuaException, InterruptedException + { + return callMethod( (ICallContext) context, method, arguments ).evaluate( context ); + } + } ) ); + } + + /** + * Tests using {@link MethodResult#then(ILuaFunction)} afterwards + */ + @Test + public void testMethodResultThen() throws Exception + { + RunOnComputer.run( code, c -> c.addAPI( new WrapperAPI( new MethodResultAPI() ) { + @Nonnull + @Override + public MethodResult callMethod( @Nonnull ICallContext context, int method, @Nonnull Object[] arguments ) throws LuaException + { + return super.callMethod( context, method, arguments ) + .then( x -> MethodResult.onMainThread( () -> MethodResult.of( x ).then( MethodResult::of ) ) ) + .then( MethodResult::of ); + } + } ) ); + } + + /** + * Tests executing functions defined through the {@link ILuaContext} API. + */ + @Test + public void testLuaContext() throws Exception + { + RunOnComputer.run( code, c -> c.addAPI( new LuaContextAPI() ) ); + } + + /** + * Tests executing functions defined through the {@link ILuaContext} API called with the + * {@link MethodResult} evaluator. + */ + @Test + public void testWithLuaContext() throws Exception + { + RunOnComputer.run( code, c -> c.addAPI( new WrapperAPI( new LuaContextAPI() ) + { + @Nonnull + @Override + @SuppressWarnings( "deprecation" ) + public MethodResult callMethod( @Nonnull ICallContext context, int method, @Nonnull Object[] arguments ) throws LuaException + { + return MethodResult.withLuaContext( c -> callMethod( c, method, arguments ) ); + } + } ) ); + } + + private static class MethodResultAPI implements ILuaAPI + { + @Override + public String[] getNames() + { + return new String[]{ "funcs" }; + } + + @Nonnull + @Override + public String[] getMethodNames() + { + return new String[]{ "empty", "identity", "pullEvent", "pullEventRaw", "mainThread" }; + } + + @Nullable + @Override + @Deprecated + public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull Object[] arguments ) throws LuaException, InterruptedException + { + return callMethod( (ICallContext) context, method, arguments ).evaluate( context ); + } + + @Nonnull + @Override + public MethodResult callMethod( @Nonnull ICallContext context, int method, @Nonnull Object[] arguments ) + { + switch( method ) + { + case 0: + return MethodResult.empty(); + case 1: + return MethodResult.of( arguments ); + case 2: + return MethodResult.pullEvent( "test" ); + case 3: + return MethodResult.pullEventRaw( "test" ); + case 4: + return MethodResult.onMainThread( () -> MethodResult.of( 1 ) ); + default: + return MethodResult.empty(); + } + } + } + + private static class LuaContextAPI implements ILuaAPI + { + @Override + public String[] getNames() + { + return new String[]{ "funcs" }; + } + + @Nonnull + @Override + public String[] getMethodNames() + { + return new String[]{ "empty", "identity", "pullEvent", "pullEventRaw", "mainThread" }; + } + + @Nullable + @Override + @Deprecated + public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull Object[] arguments ) throws LuaException, InterruptedException + { + switch( method ) + { + case 0: + return null; + case 1: + return arguments; + case 2: + return context.pullEvent( "test" ); + case 3: + return context.pullEventRaw( "test" ); + case 4: + return context.executeMainThreadTask( () -> new Object[]{ 1 } ); + default: + return null; + } + } + } + + public static class WrapperAPI implements ILuaAPI + { + private final ILuaAPI api; + + public WrapperAPI( ILuaAPI api ) + { + this.api = api; + } + + @Override + public String[] getNames() + { + return api.getNames(); + } + + @Nonnull + @Override + public String[] getMethodNames() + { + return api.getMethodNames(); + } + + @Nullable + @Override + @Deprecated + public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull Object[] arguments ) throws LuaException, InterruptedException + { + return api.callMethod( context, method, arguments ); + } + + @Nonnull + @Override + public MethodResult callMethod( @Nonnull ICallContext context, int method, @Nonnull Object[] arguments ) throws LuaException + { + return api.callMethod( context, method, arguments ); + } + } +} diff --git a/src/test/java/dan200/computercraft/shared/wired/NetworkTest.java b/src/test/java/dan200/computercraft/shared/wired/NetworkTest.java index 5984c77ee..8083c668c 100644 --- a/src/test/java/dan200/computercraft/shared/wired/NetworkTest.java +++ b/src/test/java/dan200/computercraft/shared/wired/NetworkTest.java @@ -4,8 +4,9 @@ import com.google.common.collect.Maps; import com.google.common.collect.Sets; import dan200.computercraft.ComputerCraft; import dan200.computercraft.api.ComputerCraftAPI; +import dan200.computercraft.api.lua.ICallContext; import dan200.computercraft.api.lua.ILuaContext; -import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.MethodResult; import dan200.computercraft.api.network.wired.IWiredElement; import dan200.computercraft.api.network.wired.IWiredNetwork; import dan200.computercraft.api.network.wired.IWiredNetworkChange; @@ -249,7 +250,7 @@ public class NetworkTest } @Test - @Ignore("Takes a long time to run, mostly for stress testing") + @Ignore( "Takes a long time to run, mostly for stress testing" ) public void testLarge() { final int BRUTE_SIZE = 16; @@ -410,11 +411,19 @@ public class NetworkTest @Nullable @Override - public Object[] callMethod( @Nonnull IComputerAccess computer, @Nonnull ILuaContext context, int method, @Nonnull Object[] arguments ) throws LuaException, InterruptedException + @Deprecated + public Object[] callMethod( @Nonnull IComputerAccess computer, @Nonnull ILuaContext context, int method, @Nonnull Object[] arguments ) { return new Object[0]; } + @Nonnull + @Override + public MethodResult callMethod( @Nonnull IComputerAccess computer, @Nonnull ICallContext context, int method, @Nonnull Object[] arguments ) + { + return MethodResult.empty(); + } + @Override public boolean equals( @Nullable IPeripheral other ) { @@ -427,7 +436,7 @@ public class NetworkTest private final int size; private final T[] box; - @SuppressWarnings("unchecked") + @SuppressWarnings( "unchecked" ) public Grid( int size ) { this.size = size;