From c373583723557ea9a938e96c7e897aadb36f84bb Mon Sep 17 00:00:00 2001 From: SquidDev Date: Tue, 26 Feb 2019 08:44:17 +0000 Subject: [PATCH] Add a test which boots a computer and runs forever Ideally we'd add a couple more tests in the future, but this'll do for now. The bootstrap class is largely yoinked from CCTweaks-Lua, so is a tad ugly. It works though. --- .../computercraft/core/computer/Computer.java | 4 +- .../shared/computer/core/ServerComputer.java | 2 +- .../core/computer/BasicEnvironment.java | 155 ++++++++++++++++++ .../core/computer/ComputerBootstrap.java | 154 +++++++++++++++++ .../core/computer/ComputerTest.java | 29 ++++ .../core/filesystem/MemoryMount.java | 143 ++++++++++++++++ 6 files changed, 484 insertions(+), 3 deletions(-) create mode 100644 src/test/java/dan200/computercraft/core/computer/BasicEnvironment.java create mode 100644 src/test/java/dan200/computercraft/core/computer/ComputerBootstrap.java create mode 100644 src/test/java/dan200/computercraft/core/computer/ComputerTest.java create mode 100644 src/test/java/dan200/computercraft/core/filesystem/MemoryMount.java diff --git a/src/main/java/dan200/computercraft/core/computer/Computer.java b/src/main/java/dan200/computercraft/core/computer/Computer.java index be1929df3..593641d51 100644 --- a/src/main/java/dan200/computercraft/core/computer/Computer.java +++ b/src/main/java/dan200/computercraft/core/computer/Computer.java @@ -186,7 +186,7 @@ public class Computer } } - public void advance() + public void tick() { synchronized( this ) { @@ -568,7 +568,7 @@ public class Computer @SuppressWarnings( "unused" ) public void advance( double dt ) { - advance(); + tick(); } public static final String[] s_sideNames = IAPIEnvironment.SIDE_NAMES; diff --git a/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java b/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java index a3c98ddff..63195c540 100644 --- a/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java +++ b/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java @@ -105,7 +105,7 @@ public class ServerComputer extends ServerTerminal implements IComputer, IComput public void update() { super.update(); - m_computer.advance(); + m_computer.tick(); m_changedLastFrame = m_computer.pollAndResetChanged() || m_changed; m_changed = false; diff --git a/src/test/java/dan200/computercraft/core/computer/BasicEnvironment.java b/src/test/java/dan200/computercraft/core/computer/BasicEnvironment.java new file mode 100644 index 000000000..6c6af7035 --- /dev/null +++ b/src/test/java/dan200/computercraft/core/computer/BasicEnvironment.java @@ -0,0 +1,155 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2019. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.core.computer; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.filesystem.IMount; +import dan200.computercraft.api.filesystem.IWritableMount; +import dan200.computercraft.core.filesystem.FileMount; +import dan200.computercraft.core.filesystem.JarMount; +import dan200.computercraft.core.filesystem.MemoryMount; +import net.minecraftforge.fml.common.Loader; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; + +/** + * A very basic environment + */ +public class BasicEnvironment implements IComputerEnvironment +{ + private final IWritableMount mount; + + public BasicEnvironment() + { + this( new MemoryMount() ); + } + + public BasicEnvironment( IWritableMount mount ) + { + this.mount = mount; + } + + @Override + public int assignNewID() + { + return 0; + } + + @Override + public IWritableMount createSaveDirMount( String path, long space ) + { + return mount; + } + + @Override + public int getDay() + { + return 0; + } + + @Override + public double getTimeOfDay() + { + return 0; + } + + @Override + public boolean isColour() + { + return true; + } + + @Override + public long getComputerSpaceLimit() + { + return ComputerCraft.computerSpaceLimit; + } + + @Override + public String getHostString() + { + return "ComputerCraft ${version} (Minecraft " + Loader.MC_VERSION + ")"; + } + + @Override + @Deprecated + public IMount createResourceMount( String domain, String subPath ) + { + File file = getContainingFile(); + + String path = "assets/" + domain + "/" + subPath; + + if( file.isFile() ) + { + try + { + return new JarMount( file, path ); + } + catch( IOException e ) + { + throw new UncheckedIOException( e ); + } + } + else + { + File wholeFile = new File( file, path ); + + // If we don't exist, walk up the tree looking for resource folders + File baseFile = file; + while( baseFile != null && !wholeFile.exists() ) + { + baseFile = baseFile.getParentFile(); + wholeFile = new File( baseFile, "resources/main/" + path ); + } + + if( !wholeFile.exists() ) throw new IllegalStateException( "Cannot find ROM mount at " + file ); + + return new FileMount( wholeFile, 0 ); + } + } + + @Override + public InputStream createResourceFile( String domain, String subPath ) + { + return ComputerCraft.class.getClassLoader().getResourceAsStream( "assets/" + domain + "/" + subPath ); + } + + private static File getContainingFile() + { + String path = ComputerCraft.class.getProtectionDomain().getCodeSource().getLocation().getPath(); + int bangIndex = path.indexOf( "!" ); + + // Plain old file, so step up from dan200.computercraft. + if( bangIndex < 0 ) return new File( path ); + + path = path.substring( 0, bangIndex ); + URL url; + try + { + url = new URL( path ); + } + catch( MalformedURLException e ) + { + throw new IllegalStateException( e ); + } + + try + { + return new File( url.toURI() ); + } + catch( URISyntaxException e ) + { + return new File( url.getPath() ); + } + } +} diff --git a/src/test/java/dan200/computercraft/core/computer/ComputerBootstrap.java b/src/test/java/dan200/computercraft/core/computer/ComputerBootstrap.java new file mode 100644 index 000000000..1e0ec1c8b --- /dev/null +++ b/src/test/java/dan200/computercraft/core/computer/ComputerBootstrap.java @@ -0,0 +1,154 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2019. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.core.computer; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.lua.ILuaAPI; +import dan200.computercraft.api.lua.ILuaContext; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.core.apis.ArgumentHelper; +import dan200.computercraft.core.filesystem.MemoryMount; +import dan200.computercraft.core.terminal.Terminal; +import org.apache.logging.log4j.LogManager; +import org.junit.Assert; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Helper class to run a program on a computer. + */ +public class ComputerBootstrap +{ + private static final int TPS = 20; + private static final int MAX_TIME = 10; + + public static void run( String program ) + { + run( program, -1 ); + } + + public static void run( String program, int shutdownAfter ) + { + ComputerCraft.logPeripheralErrors = true; + ComputerCraft.log = LogManager.getLogger( ComputerCraft.MOD_ID ); + + MemoryMount mount = new MemoryMount() + .addFile( "test.lua", program ) + .addFile( "startup", "assertion.assert(pcall(loadfile('test.lua', _ENV))) os.shutdown()" ); + + Terminal term = new Terminal( ComputerCraft.terminalWidth_computer, ComputerCraft.terminalHeight_computer ); + final Computer computer = new Computer( new BasicEnvironment( mount ), term, 0 ); + + AssertApi api = new AssertApi(); + computer.addAPI( api ); + + try + { + computer.turnOn(); + boolean everOn = false; + + for( int tick = 0; tick < TPS * MAX_TIME; tick++ ) + { + long start = System.currentTimeMillis(); + + computer.tick(); + MainThread.executePendingTasks(); + + if( api.message != null ) + { + ComputerCraft.log.debug( "Shutting down due to error" ); + computer.shutdown(); + Assert.fail( api.message ); + return; + } + + long remaining = (1000 / TPS) - (System.currentTimeMillis() - start); + if( remaining > 0 ) Thread.sleep( remaining ); + + // Break if the computer was once on, and is now off. + everOn |= computer.isOn(); + if( (everOn || tick > TPS) && !computer.isOn() ) break; + + // Shutdown the computer after a period of time + if( shutdownAfter > 0 && tick != 0 && tick % shutdownAfter == 0 ) + { + ComputerCraft.log.info( "Shutting down: shutdown after {}", shutdownAfter ); + computer.shutdown(); + } + } + + if( computer.isOn() || !api.didAssert ) + { + StringBuilder builder = new StringBuilder().append( "Did not correctly" ); + if( !api.didAssert ) builder.append( " assert" ); + if( computer.isOn() ) builder.append( " shutdown" ); + builder.append( "\n" ); + + for( int line = 0; line < 19; line++ ) + { + builder.append( String.format( "%2d | %" + term.getWidth() + "s |\n", line + 1, term.getLine( line ) ) ); + } + + computer.shutdown(); + Assert.fail( builder.toString() ); + } + } + catch( InterruptedException ignored ) + { + Thread.currentThread().interrupt(); + } + finally + { + ComputerThread.stop(); + } + } + + private static class AssertApi implements ILuaAPI + { + boolean didAssert; + String message; + + @Override + public String[] getNames() + { + return new String[] { "assertion" }; + } + + @Nonnull + @Override + public String[] getMethodNames() + { + return new String[] { "assert" }; + } + + @Nullable + @Override + public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull Object[] arguments ) throws LuaException, InterruptedException + { + switch( method ) + { + 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; + } + + default: + return null; + } + } + } +} diff --git a/src/test/java/dan200/computercraft/core/computer/ComputerTest.java b/src/test/java/dan200/computercraft/core/computer/ComputerTest.java new file mode 100644 index 000000000..d196f1ae8 --- /dev/null +++ b/src/test/java/dan200/computercraft/core/computer/ComputerTest.java @@ -0,0 +1,29 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2019. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.core.computer; + +import org.junit.Assert; +import org.junit.Test; + +public class ComputerTest +{ + @Test( timeout = 20_000 ) + public void testTimeout() + { + try + { + ComputerBootstrap.run( "print('Hello') while true do end" ); + } + catch( AssertionError e ) + { + if( e.getMessage().equals( "test.lua:1: Too long without yielding" ) ) return; + throw e; + } + + Assert.fail( "Expected computer to timeout" ); + } +} diff --git a/src/test/java/dan200/computercraft/core/filesystem/MemoryMount.java b/src/test/java/dan200/computercraft/core/filesystem/MemoryMount.java new file mode 100644 index 000000000..92677b7ab --- /dev/null +++ b/src/test/java/dan200/computercraft/core/filesystem/MemoryMount.java @@ -0,0 +1,143 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2019. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.core.filesystem; + +import dan200.computercraft.api.filesystem.IWritableMount; + +import javax.annotation.Nonnull; +import java.io.*; +import java.util.*; + +/** + * Mounts in memory + */ +public class MemoryMount implements IWritableMount +{ + private final Map files = new HashMap<>(); + private final Set directories = new HashSet<>(); + + public MemoryMount() + { + directories.add( "" ); + } + + + @Override + public void makeDirectory( @Nonnull String path ) + { + File file = new File( path ); + while( file != null ) + { + directories.add( file.getPath() ); + file = file.getParentFile(); + } + } + + @Override + public void delete( @Nonnull String path ) + { + if( files.containsKey( path ) ) + { + files.remove( path ); + } + else + { + directories.remove( path ); + for( String file : files.keySet().toArray( new String[0] ) ) + { + if( file.startsWith( path ) ) + { + files.remove( file ); + } + } + + File parent = new File( path ).getParentFile(); + if( parent != null ) delete( parent.getPath() ); + } + } + + @Override + @Deprecated + public OutputStream openForWrite( @Nonnull final String path ) + { + return new ByteArrayOutputStream() + { + @Override + public void close() throws IOException + { + super.close(); + files.put( path, toByteArray() ); + } + }; + } + + @Override + @Deprecated + public OutputStream openForAppend( @Nonnull final String path ) throws IOException + { + ByteArrayOutputStream stream = new ByteArrayOutputStream() + { + @Override + public void close() throws IOException + { + super.close(); + files.put( path, toByteArray() ); + } + }; + + byte[] current = files.get( path ); + if( current != null ) stream.write( current ); + + return stream; + } + + @Override + public long getRemainingSpace() + { + return 1000000L; + } + + @Override + public boolean exists( @Nonnull String path ) + { + return files.containsKey( path ) || directories.contains( path ); + } + + @Override + public boolean isDirectory( @Nonnull String path ) + { + return directories.contains( path ); + } + + @Override + public void list( @Nonnull String path, @Nonnull List files ) + { + for( String file : this.files.keySet() ) + { + if( file.startsWith( path ) ) files.add( file ); + } + } + + @Override + public long getSize( String path ) + { + throw new RuntimeException( "Not implemented" ); + } + + @Override + @Deprecated + public InputStream openForRead( String path ) + { + return new ByteArrayInputStream( files.get( path ) ); + } + + public MemoryMount addFile( String file, String contents ) + { + files.put( file, contents.getBytes() ); + return this; + } +}