1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2026-04-18 13:01:22 +00:00

Replace getMethodNames/callMethod with annotations (#447)

When creating a peripheral or custom Lua object, one must implement two
methods:

 - getMethodNames(): String[] - Returns the name of the methods
 - callMethod(int, ...): Object[] - Invokes the method using an index in
   the above array.

This has a couple of problems:
 - It's somewhat unwieldy to use - you need to keep track of array
   indices, which leads to ugly code.
 - Functions which yield (for instance, those which run on the main
   thread) are blocking. This means we need to spawn new threads for
   each CC-side yield.

We replace this system with a few changes:

 - @LuaFunction annotation: One may annotate a public instance method
   with this annotation. This then exposes a peripheral/lua object
   method.

   Furthermore, this method can accept and return a variety of types,
   which often makes functions cleaner (e.g. can return an int rather
   than an Object[], and specify and int argument rather than
   Object[]).

 - MethodResult: Instead of returning an Object[] and having blocking
   yields, functions return a MethodResult. This either contains an
   immediate return, or an instruction to yield with some continuation
   to resume with.

   MethodResult is then interpreted by the Lua runtime (i.e. Cobalt),
   rather than our weird bodgey hacks before. This means we no longer
   spawn new threads when yielding within CC.

 - Methods accept IArguments instead of a raw Object array. This has a
   few benefits:
   - Consistent argument handling - people no longer need to use
     ArgumentHelper (as it doesn't exist!), or even be aware of its
     existence - you're rather forced into using it.
   - More efficient code in some cases. We provide a Cobalt-specific
     implementation of IArguments, which avoids the boxing/unboxing when
     handling numbers and binary strings.
This commit is contained in:
Jonathan Coates
2020-05-15 13:21:16 +01:00
committed by GitHub
parent d0deab3519
commit d5f82fa458
91 changed files with 5288 additions and 4133 deletions

View File

@@ -0,0 +1,54 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeDiagnosingMatcher;
import java.util.function.Function;
public class ContramapMatcher<T, U> extends TypeSafeDiagnosingMatcher<T>
{
private final String desc;
private final Function<T, U> convert;
private final Matcher<U> matcher;
public ContramapMatcher( String desc, Function<T, U> convert, Matcher<U> matcher )
{
this.desc = desc;
this.convert = convert;
this.matcher = matcher;
}
@Override
protected boolean matchesSafely( T item, Description mismatchDescription )
{
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 );
}
public static <T, U> Matcher<T> contramap( Matcher<U> matcher, String desc, Function<T, U> convert )
{
return new ContramapMatcher<>( desc, convert, matcher );
}
public static <T, U> Matcher<T> contramap( Matcher<U> matcher, Function<T, U> convert )
{
return new ContramapMatcher<>( "-f(_)->", convert, matcher );
}
}

View File

@@ -8,8 +8,8 @@ package dan200.computercraft.core;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.api.filesystem.IWritableMount;
import dan200.computercraft.api.lua.ILuaAPI;
import dan200.computercraft.api.lua.ILuaContext;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.core.computer.BasicEnvironment;
import dan200.computercraft.core.computer.Computer;
@@ -44,8 +44,7 @@ import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Stream;
import static dan200.computercraft.api.lua.ArgumentHelper.getTable;
import static dan200.computercraft.api.lua.ArgumentHelper.getType;
import static dan200.computercraft.api.lua.LuaValues.getType;
/**
* Loads tests from {@code test-rom/spec} and executes them.
@@ -107,189 +106,7 @@ public class ComputerTestDelegate
computer = new Computer( new BasicEnvironment( mount ), term, 0 );
computer.getEnvironment().setPeripheral( ComputerSide.TOP, new FakeModem() );
computer.addApi( new ILuaAPI()
{
@Override
public String[] getNames()
{
return new String[] { "cct_test" };
}
@Nonnull
@Override
public String[] getMethodNames()
{
return new String[] { "start", "submit", "finish" };
}
@Override
public void startup()
{
try
{
computer.getAPIEnvironment().getFileSystem().mount(
"test-rom", "test-rom",
BasicEnvironment.createMount( ComputerTestDelegate.class, "test-rom", "test" )
);
}
catch( FileSystemException e )
{
throw new IllegalStateException( e );
}
}
@Nullable
@Override
public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull Object[] arguments ) throws LuaException, InterruptedException
{
switch( method )
{
case 0: // start: Submit several tests and signal for #get to run
{
LOG.info( "Received tests from computer" );
DynamicNodeBuilder root = new DynamicNodeBuilder( "" );
for( Object key : getTable( arguments, 0 ).keySet() )
{
if( !(key instanceof String) ) throw new LuaException( "Non-key string " + getType( key ) );
String name = (String) key;
String[] parts = name.split( "\0" );
DynamicNodeBuilder builder = root;
for( int i = 0; i < parts.length - 1; i++ ) builder = builder.get( parts[i] );
builder.runs( parts[parts.length - 1], () -> {
// Run it
lock.lockInterruptibly();
try
{
// Set the current test
runResult = null;
runFinished = false;
currentTest = name;
// Tell the computer to run it
LOG.info( "Starting '{}'", formatName( name ) );
computer.queueEvent( "cct_test_run", new Object[] { name } );
long remaining = TIMEOUT;
while( remaining > 0 && computer.isOn() && !runFinished )
{
tick();
long waiting = hasRun.awaitNanos( TICK_TIME );
if( waiting > 0 ) break;
remaining -= TICK_TIME;
}
LOG.info( "Finished '{}'", formatName( name ) );
if( remaining <= 0 )
{
throw new IllegalStateException( "Timed out waiting for test" );
}
else if( !computer.isOn() )
{
throw new IllegalStateException( "Computer turned off mid-execution" );
}
if( runResult != null ) throw runResult;
}
finally
{
lock.unlock();
currentTest = null;
}
} );
}
lock.lockInterruptibly();
try
{
tests = root;
hasTests.signal();
}
finally
{
lock.unlock();
}
return null;
}
case 1: // submit: Submit the result of a test, allowing the test executor to continue
{
Map<?, ?> tbl = getTable( arguments, 0 );
String name = (String) tbl.get( "name" );
String status = (String) tbl.get( "status" );
String message = (String) tbl.get( "message" );
String trace = (String) tbl.get( "trace" );
StringBuilder wholeMessage = new StringBuilder();
if( message != null ) wholeMessage.append( message );
if( trace != null )
{
if( wholeMessage.length() != 0 ) wholeMessage.append( '\n' );
wholeMessage.append( trace );
}
lock.lockInterruptibly();
try
{
LOG.info( "'{}' finished with {}", formatName( name ), status );
// Skip if a test mismatch
if( !name.equals( currentTest ) )
{
LOG.warn( "Skipping test '{}', as we're currently executing '{}'", formatName( name ), formatName( currentTest ) );
return null;
}
switch( status )
{
case "ok":
case "pending":
break;
case "fail":
runResult = new AssertionFailedError( wholeMessage.toString() );
break;
case "error":
runResult = new IllegalStateException( wholeMessage.toString() );
break;
}
runFinished = true;
hasRun.signal();
}
finally
{
lock.unlock();
}
return null;
}
case 2: // finish: Signal to after that execution has finished
LOG.info( "Finished" );
lock.lockInterruptibly();
try
{
finished = true;
if( arguments.length > 0 )
{
@SuppressWarnings( "unchecked" )
Map<String, Map<Double, Double>> finished = (Map<String, Map<Double, Double>>) arguments[0];
finishedWith = finished;
}
hasFinished.signal();
}
finally
{
lock.unlock();
}
return null;
default:
return null;
}
}
} );
computer.addApi( new CctTestAPI() );
computer.turnOn();
}
@@ -475,4 +292,198 @@ public class ComputerTestDelegate
return this == other;
}
}
public class CctTestAPI implements ILuaAPI
{
@Override
public String[] getNames()
{
return new String[] { "cct_test" };
}
@Override
public void startup()
{
try
{
computer.getAPIEnvironment().getFileSystem().mount(
"test-rom", "test-rom",
BasicEnvironment.createMount( ComputerTestDelegate.class, "test-rom", "test" )
);
}
catch( FileSystemException e )
{
throw new IllegalStateException( e );
}
}
@LuaFunction
public final void start( Map<?, ?> tests ) throws LuaException
{
// Submit several tests and signal for #get to run
LOG.info( "Received tests from computer" );
DynamicNodeBuilder root = new DynamicNodeBuilder( "" );
for( Object key : tests.keySet() )
{
if( !(key instanceof String) ) throw new LuaException( "Non-key string " + getType( key ) );
String name = (String) key;
String[] parts = name.split( "\0" );
DynamicNodeBuilder builder = root;
for( int i = 0; i < parts.length - 1; i++ ) builder = builder.get( parts[i] );
builder.runs( parts[parts.length - 1], () -> {
// Run it
lock.lockInterruptibly();
try
{
// Set the current test
runResult = null;
runFinished = false;
currentTest = name;
// Tell the computer to run it
LOG.info( "Starting '{}'", formatName( name ) );
computer.queueEvent( "cct_test_run", new Object[] { name } );
long remaining = TIMEOUT;
while( remaining > 0 && computer.isOn() && !runFinished )
{
tick();
long waiting = hasRun.awaitNanos( TICK_TIME );
if( waiting > 0 ) break;
remaining -= TICK_TIME;
}
LOG.info( "Finished '{}'", formatName( name ) );
if( remaining <= 0 )
{
throw new IllegalStateException( "Timed out waiting for test" );
}
else if( !computer.isOn() )
{
throw new IllegalStateException( "Computer turned off mid-execution" );
}
if( runResult != null ) throw runResult;
}
finally
{
lock.unlock();
currentTest = null;
}
} );
}
try
{
lock.lockInterruptibly();
}
catch( InterruptedException e )
{
throw new RuntimeException( e );
}
try
{
ComputerTestDelegate.this.tests = root;
hasTests.signal();
}
finally
{
lock.unlock();
}
}
@LuaFunction
public final void submit( Map<?, ?> tbl )
{
// Submit the result of a test, allowing the test executor to continue
String name = (String) tbl.get( "name" );
if( name == null )
{
ComputerCraft.log.error( "Oh no: {}", tbl );
}
String status = (String) tbl.get( "status" );
String message = (String) tbl.get( "message" );
String trace = (String) tbl.get( "trace" );
StringBuilder wholeMessage = new StringBuilder();
if( message != null ) wholeMessage.append( message );
if( trace != null )
{
if( wholeMessage.length() != 0 ) wholeMessage.append( '\n' );
wholeMessage.append( trace );
}
try
{
lock.lockInterruptibly();
}
catch( InterruptedException e )
{
throw new RuntimeException( e );
}
try
{
LOG.info( "'{}' finished with {}", formatName( name ), status );
// Skip if a test mismatch
if( !name.equals( currentTest ) )
{
LOG.warn( "Skipping test '{}', as we're currently executing '{}'", formatName( name ), formatName( currentTest ) );
return;
}
switch( status )
{
case "ok":
case "pending":
break;
case "fail":
runResult = new AssertionFailedError( wholeMessage.toString() );
break;
case "error":
runResult = new IllegalStateException( wholeMessage.toString() );
break;
}
runFinished = true;
hasRun.signal();
}
finally
{
lock.unlock();
}
}
@LuaFunction
public final void finish( Optional<Map<?, ?>> result )
{
@SuppressWarnings( "unchecked" )
Map<String, Map<Double, Double>> finishedResult = (Map<String, Map<Double, Double>>) result.orElse( null );
LOG.info( "Finished" );
// Signal to after that execution has finished
try
{
lock.lockInterruptibly();
}
catch( InterruptedException e )
{
throw new RuntimeException( e );
}
try
{
finished = true;
if( finishedResult != null ) finishedWith = finishedResult;
hasFinished.signal();
}
finally
{
lock.unlock();
}
}
}
}

View File

@@ -5,52 +5,47 @@
*/
package dan200.computercraft.core.apis;
import dan200.computercraft.api.lua.ILuaContext;
import dan200.computercraft.api.lua.ILuaObject;
import dan200.computercraft.api.lua.ILuaTask;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.*;
import dan200.computercraft.core.asm.LuaMethod;
import dan200.computercraft.core.asm.NamedMethod;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public class ObjectWrapper implements ILuaContext
{
private final ILuaObject object;
private final String[] methods;
private final Object object;
private final Map<String, LuaMethod> methodMap;
public ObjectWrapper( ILuaObject object )
public ObjectWrapper( Object object )
{
this.object = object;
this.methods = object.getMethodNames();
}
String[] dynamicMethods = object instanceof IDynamicLuaObject
? Objects.requireNonNull( ((IDynamicLuaObject) object).getMethodNames(), "Methods cannot be null" )
: LuaMethod.EMPTY_METHODS;
private int findMethod( String method )
{
for( int i = 0; i < methods.length; i++ )
List<NamedMethod<LuaMethod>> methods = LuaMethod.GENERATOR.getMethods( object.getClass() );
Map<String, LuaMethod> methodMap = this.methodMap = new HashMap<>( methods.size() + dynamicMethods.length );
for( int i = 0; i < dynamicMethods.length; i++ )
{
if( method.equals( methods[i] ) ) return i;
methodMap.put( dynamicMethods[i], LuaMethod.DYNAMIC.get( i ) );
}
for( NamedMethod<LuaMethod> method : methods )
{
methodMap.put( method.getName(), method.getMethod() );
}
return -1;
}
public boolean hasMethod( String method )
{
return findMethod( method ) >= 0;
}
public Object[] call( String name, Object... args ) throws LuaException
{
int method = findMethod( name );
if( method < 0 ) throw new IllegalStateException( "No such method '" + name + "'" );
LuaMethod method = methodMap.get( name );
if( method == null ) throw new IllegalStateException( "No such method '" + name + "'" );
try
{
return object.callMethod( this, method, args );
}
catch( InterruptedException e )
{
throw new IllegalStateException( "Should never be interrupted", e );
}
return method.apply( object, this, new ObjectArguments( args ) ).getResult();
}
@SuppressWarnings( "unchecked" )
@@ -64,34 +59,6 @@ public class ObjectWrapper implements ILuaContext
return klass.cast( call( name, args )[0] );
}
@Nonnull
@Override
public Object[] pullEvent( @Nullable String filter )
{
throw new IllegalStateException( "Method should never yield" );
}
@Nonnull
@Override
public Object[] pullEventRaw( @Nullable String filter )
{
throw new IllegalStateException( "Method should never yield" );
}
@Nonnull
@Override
public Object[] yield( @Nullable Object[] arguments )
{
throw new IllegalStateException( "Method should never yield" );
}
@Nullable
@Override
public Object[] executeMainThreadTask( @Nonnull ILuaTask task )
{
throw new IllegalStateException( "Method should never yield" );
}
@Override
public long issueMainThreadTask( @Nonnull ILuaTask task )
{

View File

@@ -9,6 +9,7 @@ import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.core.apis.ObjectWrapper;
import org.junit.jupiter.api.Test;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
@@ -27,17 +28,16 @@ public class BinaryReadableHandleTest
public void testReadShortComplete() throws LuaException
{
ObjectWrapper wrapper = fromLength( 10 );
assertEquals( 5, wrapper.<byte[]>callOf( "read", 5 ).length );
assertEquals( 5, wrapper.<ByteBuffer>callOf( "read", 5 ).remaining() );
}
@Test
public void testReadShortPartial() throws LuaException
{
ObjectWrapper wrapper = fromLength( 5 );
assertEquals( 5, wrapper.<byte[]>callOf( "read", 10 ).length );
assertEquals( 5, wrapper.<ByteBuffer>callOf( "read", 10 ).remaining() );
}
@Test
public void testReadLongComplete() throws LuaException
{
@@ -56,13 +56,13 @@ public class BinaryReadableHandleTest
public void testReadLongPartialSmaller() throws LuaException
{
ObjectWrapper wrapper = fromLength( 1000 );
assertEquals( 1000, wrapper.<byte[]>callOf( "read", 11000 ).length );
assertEquals( 1000, wrapper.<ByteBuffer>callOf( "read", 11000 ).remaining() );
}
@Test
public void testReadLine() throws LuaException
{
ObjectWrapper wrapper = new ObjectWrapper( new BinaryReadableHandle( new ArrayByteChannel( "hello\r\nworld\r!".getBytes( StandardCharsets.UTF_8 ) ) ) );
ObjectWrapper wrapper = new ObjectWrapper( BinaryReadableHandle.of( new ArrayByteChannel( "hello\r\nworld\r!".getBytes( StandardCharsets.UTF_8 ) ) ) );
assertArrayEquals( "hello".getBytes( StandardCharsets.UTF_8 ), wrapper.callOf( "readLine" ) );
assertArrayEquals( "world\r!".getBytes( StandardCharsets.UTF_8 ), wrapper.callOf( "readLine" ) );
assertNull( wrapper.call( "readLine" ) );
@@ -71,7 +71,7 @@ public class BinaryReadableHandleTest
@Test
public void testReadLineTrailing() throws LuaException
{
ObjectWrapper wrapper = new ObjectWrapper( new BinaryReadableHandle( new ArrayByteChannel( "hello\r\nworld\r!".getBytes( StandardCharsets.UTF_8 ) ) ) );
ObjectWrapper wrapper = new ObjectWrapper( BinaryReadableHandle.of( new ArrayByteChannel( "hello\r\nworld\r!".getBytes( StandardCharsets.UTF_8 ) ) ) );
assertArrayEquals( "hello\r\n".getBytes( StandardCharsets.UTF_8 ), wrapper.callOf( "readLine", true ) );
assertArrayEquals( "world\r!".getBytes( StandardCharsets.UTF_8 ), wrapper.callOf( "readLine", true ) );
assertNull( wrapper.call( "readLine", true ) );
@@ -81,6 +81,6 @@ public class BinaryReadableHandleTest
{
byte[] input = new byte[length];
Arrays.fill( input, (byte) 'A' );
return new ObjectWrapper( new BinaryReadableHandle( new ArrayByteChannel( input ) ) );
return new ObjectWrapper( BinaryReadableHandle.of( new ArrayByteChannel( input ) ) );
}
}

View File

@@ -0,0 +1,253 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.asm;
import dan200.computercraft.api.lua.*;
import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.core.computer.ComputerSide;
import org.hamcrest.Matcher;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static dan200.computercraft.ContramapMatcher.contramap;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class GeneratorTest
{
@Test
public void testBasic()
{
List<NamedMethod<LuaMethod>> methods = LuaMethod.GENERATOR.getMethods( Basic.class );
assertThat( methods, contains(
allOf(
named( "go" ),
contramap( is( true ), "non-yielding", NamedMethod::nonYielding )
)
) );
}
@Test
public void testIdentical()
{
List<NamedMethod<LuaMethod>> methods = LuaMethod.GENERATOR.getMethods( Basic.class );
List<NamedMethod<LuaMethod>> methods2 = LuaMethod.GENERATOR.getMethods( Basic.class );
assertThat( methods, sameInstance( methods2 ) );
}
@Test
public void testIdenticalMethods()
{
List<NamedMethod<LuaMethod>> methods = LuaMethod.GENERATOR.getMethods( Basic.class );
List<NamedMethod<LuaMethod>> methods2 = LuaMethod.GENERATOR.getMethods( Basic2.class );
assertThat( methods, contains( named( "go" ) ) );
assertThat( methods.get( 0 ).getMethod(), sameInstance( methods2.get( 0 ).getMethod() ) );
}
@Test
public void testEmptyClass()
{
assertThat( LuaMethod.GENERATOR.getMethods( Empty.class ), is( empty() ) );
}
@Test
public void testNonPublicClass()
{
assertThat( LuaMethod.GENERATOR.getMethods( NonPublic.class ), is( empty() ) );
}
@Test
public void testNonInstance()
{
assertThat( LuaMethod.GENERATOR.getMethods( NonInstance.class ), is( empty() ) );
}
@Test
public void testIllegalThrows()
{
assertThat( LuaMethod.GENERATOR.getMethods( IllegalThrows.class ), is( empty() ) );
}
@Test
public void testCustomNames()
{
List<NamedMethod<LuaMethod>> methods = LuaMethod.GENERATOR.getMethods( CustomNames.class );
assertThat( methods, contains( named( "go1" ), named( "go2" ) ) );
}
@Test
public void testArgKinds()
{
List<NamedMethod<LuaMethod>> methods = LuaMethod.GENERATOR.getMethods( ArgKinds.class );
assertThat( methods, containsInAnyOrder(
named( "objectArg" ), named( "intArg" ), named( "optIntArg" ),
named( "context" ), named( "arguments" )
) );
}
@Test
public void testEnum() throws LuaException
{
List<NamedMethod<LuaMethod>> methods = LuaMethod.GENERATOR.getMethods( EnumMethods.class );
assertThat( methods, containsInAnyOrder( named( "getEnum" ), named( "optEnum" ) ) );
assertThat( apply( methods, new EnumMethods(), "getEnum", "front" ), one( is( "FRONT" ) ) );
assertThat( apply( methods, new EnumMethods(), "optEnum", "front" ), one( is( "FRONT" ) ) );
assertThat( apply( methods, new EnumMethods(), "optEnum" ), one( is( "?" ) ) );
assertThrows( LuaException.class, () -> apply( methods, new EnumMethods(), "getEnum", "not as side" ) );
}
@Test
public void testMainThread() throws LuaException
{
List<NamedMethod<LuaMethod>> methods = LuaMethod.GENERATOR.getMethods( MainThread.class );
assertThat( methods, contains( allOf(
named( "go" ),
contramap( is( false ), "non-yielding", NamedMethod::nonYielding )
) ) );
assertThat( apply( methods, new MainThread(), "go" ),
contramap( notNullValue(), "callback", MethodResult::getCallback ) );
}
public static class Basic
{
@LuaFunction
public final void go()
{ }
}
public static class Basic2 extends Basic
{
}
public static class Empty
{
}
static class NonPublic
{
@LuaFunction
public final void go()
{ }
}
public static class NonInstance
{
@LuaFunction
public static void go()
{ }
}
public static class IllegalThrows
{
@LuaFunction
public final void go() throws IOException
{
throw new IOException();
}
}
public static class CustomNames
{
@LuaFunction( { "go1", "go2" } )
public final void go()
{ }
}
public static class ArgKinds
{
@LuaFunction
public final void objectArg( Object arg )
{ }
@LuaFunction
public final void intArg( int arg )
{ }
@LuaFunction
public final void optIntArg( Optional<Integer> arg )
{ }
@LuaFunction
public final void context( ILuaContext arg )
{ }
@LuaFunction
public final void arguments( IArguments arg )
{ }
@LuaFunction
public final void unknown( IComputerAccess arg )
{ }
@LuaFunction
public final void illegalMap( Map<String, Integer> arg )
{ }
@LuaFunction
public final void optIllegalMap( Optional<Map<String, Integer>> arg )
{ }
}
public static class EnumMethods
{
@LuaFunction
public final String getEnum( ComputerSide side )
{
return side.name();
}
@LuaFunction
public final String optEnum( Optional<ComputerSide> side )
{
return side.map( ComputerSide::name ).orElse( "?" );
}
}
public static class MainThread
{
@LuaFunction( mainThread = true )
public final void go()
{ }
}
private static <T> T find( Collection<NamedMethod<T>> methods, String name )
{
return methods.stream()
.filter( x -> x.getName().equals( name ) )
.map( NamedMethod::getMethod )
.findAny()
.orElseThrow( NullPointerException::new );
}
public static MethodResult apply( Collection<NamedMethod<LuaMethod>> methods, Object instance, String name, Object... args ) throws LuaException
{
return find( methods, name ).apply( instance, CONTEXT, new ObjectArguments( args ) );
}
public static Matcher<MethodResult> one( Matcher<Object> object )
{
return allOf(
contramap( nullValue(), "callback", MethodResult::getCallback ),
contramap( array( object ), "result", MethodResult::getResult )
);
}
public static <T> Matcher<NamedMethod<T>> named( String method )
{
return contramap( is( method ), "name", NamedMethod::getName );
}
private static final ILuaContext CONTEXT = task -> 0;
}

View File

@@ -0,0 +1,208 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.asm;
import dan200.computercraft.api.lua.*;
import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.api.peripheral.IDynamicPeripheral;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.core.computer.ComputerBootstrap;
import dan200.computercraft.core.computer.ComputerSide;
import org.junit.jupiter.api.Test;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Collections;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
public class MethodTest
{
@Test
public void testMainThread()
{
ComputerBootstrap.run( "assert(main_thread.go() == 123)", x -> x.addApi( new MainThread() ), 50 );
}
@Test
public void testMainThreadPeripheral()
{
ComputerBootstrap.run( "assert(peripheral.call('top', 'go') == 123)",
x -> x.getEnvironment().setPeripheral( ComputerSide.TOP, new MainThread() ),
50 );
}
@Test
public void testDynamic()
{
ComputerBootstrap.run(
"assert(dynamic.foo() == 123, 'foo: ' .. tostring(dynamic.foo()))\n" +
"assert(dynamic.bar() == 321, 'bar: ' .. tostring(dynamic.bar()))",
x -> x.addApi( new Dynamic() ), 50 );
}
@Test
public void testDynamicPeripheral()
{
ComputerBootstrap.run(
"local dynamic = peripheral.wrap('top')\n" +
"assert(dynamic.foo() == 123, 'foo: ' .. tostring(dynamic.foo()))\n" +
"assert(dynamic.bar() == 321, 'bar: ' .. tostring(dynamic.bar()))",
x -> x.getEnvironment().setPeripheral( ComputerSide.TOP, new Dynamic() ),
50
);
}
@Test
public void testExtra()
{
ComputerBootstrap.run( "assert(extra.go, 'go')\nassert(extra.go2, 'go2')",
x -> x.addApi( new ExtraObject() ),
50 );
}
@Test
public void testPeripheralThrow()
{
ComputerBootstrap.run(
"local throw = peripheral.wrap('top')\n" +
"local _, err = pcall(throw.thisThread) assert(err == 'pcall: !', err)\n" +
"local _, err = pcall(throw.mainThread) assert(err == 'pcall: !', err)",
x -> x.getEnvironment().setPeripheral( ComputerSide.TOP, new PeripheralThrow() ),
50
);
}
public static class MainThread implements ILuaAPI, IPeripheral
{
public final String thread = Thread.currentThread().getName();
@Override
public String[] getNames()
{
return new String[] { "main_thread" };
}
@LuaFunction( mainThread = true )
public final int go()
{
assertThat( Thread.currentThread().getName(), is( thread ) );
return 123;
}
@Nonnull
@Override
public String getType()
{
return "main_thread";
}
@Override
public boolean equals( @Nullable IPeripheral other )
{
return this == other;
}
}
public static class Dynamic implements IDynamicLuaObject, ILuaAPI, IDynamicPeripheral
{
@Nonnull
@Override
public String[] getMethodNames()
{
return new String[] { "foo" };
}
@Nonnull
@Override
public MethodResult callMethod( @Nonnull ILuaContext context, int method, @Nonnull IArguments arguments )
{
return MethodResult.of( 123 );
}
@Nonnull
@Override
public MethodResult callMethod( @Nonnull IComputerAccess computer, @Nonnull ILuaContext context, int method, @Nonnull IArguments arguments )
{
return callMethod( context, method, arguments );
}
@LuaFunction
public final int bar()
{
return 321;
}
@Override
public String[] getNames()
{
return new String[] { "dynamic" };
}
@Nonnull
@Override
public String getType()
{
return "dynamic";
}
@Override
public boolean equals( @Nullable IPeripheral other )
{
return this == other;
}
}
public static class ExtraObject implements ObjectSource, ILuaAPI
{
@Override
public String[] getNames()
{
return new String[] { "extra" };
}
@LuaFunction
public final void go2()
{
}
@Override
public Iterable<Object> getExtra()
{
return Collections.singletonList( new GeneratorTest.Basic() );
}
}
public static class PeripheralThrow implements IPeripheral
{
@LuaFunction
public final void thisThread() throws LuaException
{
throw new LuaException( "!" );
}
@LuaFunction( mainThread = true )
public final void mainThread() throws LuaException
{
throw new LuaException( "!" );
}
@Nonnull
@Override
public String getType()
{
return "throw";
}
@Override
public boolean equals( @Nullable IPeripheral other )
{
return this == other;
}
}
}

View File

@@ -7,16 +7,14 @@ package dan200.computercraft.core.computer;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.api.filesystem.IWritableMount;
import dan200.computercraft.api.lua.IArguments;
import dan200.computercraft.api.lua.ILuaAPI;
import dan200.computercraft.api.lua.ILuaContext;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.ArgumentHelper;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.core.filesystem.MemoryMount;
import dan200.computercraft.core.terminal.Terminal;
import org.junit.jupiter.api.Assertions;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Arrays;
import java.util.function.Consumer;
@@ -26,20 +24,26 @@ import java.util.function.Consumer;
public class ComputerBootstrap
{
private static final int TPS = 20;
private static final int MAX_TIME = 10;
public static final int MAX_TIME = 10;
public static void run( String program )
public static void run( String program, Consumer<Computer> setup, int maxTimes )
{
MemoryMount mount = new MemoryMount()
.addFile( "test.lua", program )
.addFile( "startup", "assertion.assert(pcall(loadfile('test.lua', nil, _ENV))) os.shutdown()" );
.addFile( "startup.lua", "assertion.assert(pcall(loadfile('test.lua', nil, _ENV))) os.shutdown()" );
run( mount, x -> { } );
run( mount, setup, maxTimes );
}
public static void run( IWritableMount mount, Consumer<Computer> setup )
public static void run( String program, int maxTimes )
{
run( program, x -> { }, maxTimes );
}
public static void run( IWritableMount mount, Consumer<Computer> setup, int maxTicks )
{
ComputerCraft.logPeripheralErrors = true;
ComputerCraft.maxMainComputerTime = ComputerCraft.maxMainGlobalTime = Integer.MAX_VALUE;
Terminal term = new Terminal( ComputerCraft.terminalWidth_computer, ComputerCraft.terminalHeight_computer );
final Computer computer = new Computer( new BasicEnvironment( mount ), term, 0 );
@@ -54,7 +58,7 @@ public class ComputerBootstrap
computer.turnOn();
boolean everOn = false;
for( int tick = 0; tick < TPS * MAX_TIME; tick++ )
for( int tick = 0; tick < TPS * maxTicks; tick++ )
{
long start = System.currentTimeMillis();
@@ -99,7 +103,7 @@ public class ComputerBootstrap
}
}
private static class AssertApi implements ILuaAPI
public static class AssertApi implements ILuaAPI
{
boolean didAssert;
String message;
@@ -110,39 +114,25 @@ public class ComputerBootstrap
return new String[] { "assertion" };
}
@Nonnull
@Override
public String[] getMethodNames()
@LuaFunction
public final void log( IArguments arguments )
{
return new String[] { "assert", "log" };
ComputerCraft.log.info( "[Computer] {}", Arrays.toString( arguments.getAll() ) );
}
@Nullable
@Override
public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull Object[] arguments ) throws LuaException, InterruptedException
@LuaFunction( "assert" )
public final Object[] doAssert( IArguments arguments ) throws LuaException
{
switch( method )
didAssert = true;
Object arg = arguments.get( 0 );
if( arg == null || arg == Boolean.FALSE )
{
case 0: // assert
{
didAssert = true;
Object arg = arguments.length >= 1 ? arguments[0] : null;
if( arg == null || arg == Boolean.FALSE )
{
message = ArgumentHelper.optString( arguments, 1, "Assertion failed" );
throw new LuaException( message );
}
return arguments;
}
case 1:
ComputerCraft.log.info( "[Computer] {}", Arrays.toString( arguments ) );
return null;
default:
return null;
message = arguments.optString( 1, "Assertion failed" );
throw new LuaException( message );
}
return arguments.getAll();
}
}
}

View File

@@ -5,9 +5,15 @@
*/
package dan200.computercraft.core.computer;
import com.google.common.io.CharStreams;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import static java.time.Duration.ofSeconds;
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
@@ -19,7 +25,7 @@ public class ComputerTest
assertTimeoutPreemptively( ofSeconds( 20 ), () -> {
try
{
ComputerBootstrap.run( "print('Hello') while true do end" );
ComputerBootstrap.run( "print('Hello') while true do end", ComputerBootstrap.MAX_TIME );
}
catch( AssertionError e )
{
@@ -30,4 +36,14 @@ public class ComputerTest
Assertions.fail( "Expected computer to timeout" );
} );
}
public static void main( String[] args ) throws Exception
{
InputStream stream = ComputerTest.class.getClassLoader().getResourceAsStream( "benchmark.lua" );
try( InputStreamReader reader = new InputStreamReader( Objects.requireNonNull( stream ), StandardCharsets.UTF_8 ) )
{
String contents = CharStreams.toString( reader );
ComputerBootstrap.run( contents, 1000 );
}
}
}

View File

@@ -27,8 +27,8 @@ public class FileSystemTest
* Ensures writing a file truncates it.
*
* @throws FileSystemException When the file system cannot be constructed.
* @throws LuaException When Lua functions fail.
* @throws IOException When reading and writing from strings
* @throws LuaException When Lua functions fail.
* @throws IOException When reading and writing from strings
*/
@Test
public void testWriteTruncates() throws FileSystemException, LuaException, IOException

View File

@@ -8,13 +8,10 @@ package dan200.computercraft.shared.wired;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.lua.ILuaContext;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.network.wired.IWiredElement;
import dan200.computercraft.api.network.wired.IWiredNetwork;
import dan200.computercraft.api.network.wired.IWiredNetworkChange;
import dan200.computercraft.api.network.wired.IWiredNode;
import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.shared.util.DirectionUtil;
import net.minecraft.util.Direction;
@@ -397,20 +394,6 @@ public class NetworkTest
return "test";
}
@Nonnull
@Override
public String[] getMethodNames()
{
return new String[0];
}
@Nullable
@Override
public Object[] callMethod( @Nonnull IComputerAccess computer, @Nonnull ILuaContext context, int method, @Nonnull Object[] arguments ) throws LuaException, InterruptedException
{
return new Object[0];
}
@Override
public boolean equals( @Nullable IPeripheral other )
{