mirror of
				https://github.com/SquidDev-CC/CC-Tweaked
				synced 2025-10-31 13:42:59 +00:00 
			
		
		
		
	Merge pull request #40 from Jummit/remove-test
Remove the Java and Lua test suites
This commit is contained in:
		| @@ -52,10 +52,6 @@ dependencies { | ||||
|  | ||||
|     shade 'org.squiddev:Cobalt:0.5.1-SNAPSHOT' | ||||
|  | ||||
|     testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0' | ||||
|     testImplementation 'org.junit.jupiter:junit-jupiter-params:5.7.0' | ||||
|     testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0' | ||||
|  | ||||
|     modRuntime "me.shedaniel:RoughlyEnoughItems-api:5.8.9" | ||||
|     modRuntime "me.shedaniel:RoughlyEnoughItems:5.8.9" | ||||
| } | ||||
|   | ||||
| @@ -1,59 +0,0 @@ | ||||
| package badhtmlparser; | ||||
|  | ||||
| import java.io.BufferedReader; | ||||
| import java.io.FileNotFoundException; | ||||
| import java.io.FileOutputStream; | ||||
| import java.io.InputStreamReader; | ||||
| import java.io.PrintStream; | ||||
| import java.util.Iterator; | ||||
|  | ||||
| import com.google.common.collect.HashMultimap; | ||||
| import com.google.common.collect.Iterators; | ||||
| import com.google.common.collect.Multimap; | ||||
| import com.google.common.collect.PeekingIterator; | ||||
|  | ||||
| public class BadHtmlParser { | ||||
|     private static final Multimap<String, String> IDS = HashMultimap.create(); | ||||
|  | ||||
|     public static void main(String[] args) throws FileNotFoundException { | ||||
|         BufferedReader reader = new BufferedReader(new InputStreamReader(BadHtmlParser.class.getResourceAsStream("/tags.html"))); | ||||
|         PeekingIterator<String> iterator = Iterators.peekingIterator(reader.lines() | ||||
|                                                                            .iterator()); | ||||
|         while (iterator.hasNext()) { | ||||
|             String str = iterator.peek(); | ||||
|             try { | ||||
|                 int count = Integer.parseInt(str); | ||||
|                 iterator.next(); | ||||
|                 write(iterator, count); | ||||
|             } catch (NumberFormatException e) { | ||||
|                 write(iterator, 1); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|  | ||||
|         System.out.println(IDS); | ||||
|         print("iron_ingots"); | ||||
|  | ||||
|         // todo replace dyes by hand | ||||
|     } | ||||
|  | ||||
|     private static void print(String val) throws FileNotFoundException { | ||||
|         FileOutputStream fos = new FileOutputStream("src/main/resources/data/c/tags/items/" + val + ".json"); | ||||
|         PrintStream stream = new PrintStream(fos); | ||||
|         stream.println("{\"replace\": false,\"values\":["); | ||||
|         Iterator<String> iterator = IDS.get("c:" + val) | ||||
|                                        .iterator(); | ||||
|         while (iterator.hasNext()) { | ||||
|             String s = iterator.next(); | ||||
|             stream.printf("\"%s\"%s", s, iterator.hasNext() ? ',' : ""); | ||||
|         } | ||||
|         stream.println("]}"); | ||||
|     } | ||||
|  | ||||
|     private static void write(Iterator<String> str, int entries) { | ||||
|         String tag = str.next(); | ||||
|         for (int i = 0; i < entries; i++) { | ||||
|             IDS.put(tag, str.next()); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,489 +0,0 @@ | ||||
| /* | ||||
|  * This file is part of ComputerCraft - http://www.computercraft.info | ||||
|  * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. | ||||
|  * Send enquiries to dratcliffe@gmail.com | ||||
|  */ | ||||
| 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.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; | ||||
| import dan200.computercraft.core.computer.ComputerSide; | ||||
| import dan200.computercraft.core.computer.MainThread; | ||||
| import dan200.computercraft.core.filesystem.FileMount; | ||||
| import dan200.computercraft.core.filesystem.FileSystemException; | ||||
| import dan200.computercraft.core.terminal.Terminal; | ||||
| import dan200.computercraft.shared.peripheral.modem.ModemState; | ||||
| import dan200.computercraft.shared.peripheral.modem.wireless.WirelessModemPeripheral; | ||||
| import net.minecraft.util.math.Vec3d; | ||||
| import net.minecraft.world.World; | ||||
| import org.apache.logging.log4j.LogManager; | ||||
| import org.apache.logging.log4j.Logger; | ||||
| import org.junit.jupiter.api.*; | ||||
| import org.junit.jupiter.api.function.Executable; | ||||
| import org.opentest4j.AssertionFailedError; | ||||
|  | ||||
| import javax.annotation.Nonnull; | ||||
| import javax.annotation.Nullable; | ||||
| import java.io.BufferedWriter; | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
| import java.io.Writer; | ||||
| import java.nio.channels.Channels; | ||||
| import java.nio.channels.WritableByteChannel; | ||||
| import java.nio.charset.StandardCharsets; | ||||
| import java.nio.file.Files; | ||||
| import java.util.*; | ||||
| import java.util.concurrent.TimeUnit; | ||||
| import java.util.concurrent.locks.Condition; | ||||
| import java.util.concurrent.locks.ReentrantLock; | ||||
| import java.util.stream.Stream; | ||||
|  | ||||
| import static dan200.computercraft.api.lua.LuaValues.getType; | ||||
|  | ||||
| /** | ||||
|  * Loads tests from {@code test-rom/spec} and executes them. | ||||
|  * | ||||
|  * This spins up a new computer and runs the {@code mcfly.lua} script. This will then load all files in the {@code spec} | ||||
|  * directory and register them with {@code cct_test.start}. | ||||
|  * | ||||
|  * From the test names, we generate a tree of {@link DynamicNode}s which queue an event and wait for | ||||
|  * {@code cct_test.submit} to be called. McFly pulls these events, executes the tests and then calls the submit method. | ||||
|  * | ||||
|  * Once all tests are done, we invoke {@code cct_test.finish} in order to mark everything as complete. | ||||
|  */ | ||||
| public class ComputerTestDelegate | ||||
| { | ||||
|     private static final File REPORT_PATH = new File( "test-files/luacov.report.out" ); | ||||
|  | ||||
|     private static final Logger LOG = LogManager.getLogger( ComputerTestDelegate.class ); | ||||
|  | ||||
|     private static final long TICK_TIME = TimeUnit.MILLISECONDS.toNanos( 50 ); | ||||
|  | ||||
|     private static final long TIMEOUT = TimeUnit.SECONDS.toNanos( 10 ); | ||||
|  | ||||
|     private final ReentrantLock lock = new ReentrantLock(); | ||||
|     private Computer computer; | ||||
|  | ||||
|     private final Condition hasTests = lock.newCondition(); | ||||
|     private DynamicNodeBuilder tests; | ||||
|  | ||||
|     private final Condition hasRun = lock.newCondition(); | ||||
|     private String currentTest; | ||||
|     private boolean runFinished; | ||||
|     private Throwable runResult; | ||||
|  | ||||
|     private final Condition hasFinished = lock.newCondition(); | ||||
|     private boolean finished = false; | ||||
|     private Map<String, Map<Double, Double>> finishedWith; | ||||
|  | ||||
|     @BeforeEach | ||||
|     public void before() throws IOException | ||||
|     { | ||||
|         ComputerCraft.logPeripheralErrors = true; | ||||
|  | ||||
|         if( REPORT_PATH.delete() ) ComputerCraft.log.info( "Deleted previous coverage report." ); | ||||
|  | ||||
|         Terminal term = new Terminal( 78, 20 ); | ||||
|         IWritableMount mount = new FileMount( new File( "test-files/mount" ), 10_000_000 ); | ||||
|  | ||||
|         // Remove any existing files | ||||
|         List<String> children = new ArrayList<>(); | ||||
|         mount.list( "", children ); | ||||
|         for( String child : children ) mount.delete( child ); | ||||
|  | ||||
|         // And add our startup file | ||||
|         try( WritableByteChannel channel = mount.openForWrite( "startup.lua" ); | ||||
|              Writer writer = Channels.newWriter( channel, StandardCharsets.UTF_8.newEncoder(), -1 ) ) | ||||
|         { | ||||
|             writer.write( "loadfile('test-rom/mcfly.lua', nil, _ENV)('test-rom/spec') cct_test.finish()" ); | ||||
|         } | ||||
|  | ||||
|         computer = new Computer( new BasicEnvironment( mount ), term, 0 ); | ||||
|         computer.getEnvironment().setPeripheral( ComputerSide.TOP, new FakeModem() ); | ||||
|         computer.addApi( new CctTestAPI() ); | ||||
|  | ||||
|         computer.turnOn(); | ||||
|     } | ||||
|  | ||||
|     @AfterEach | ||||
|     public void after() throws InterruptedException, IOException | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             LOG.info( "Finished execution" ); | ||||
|             computer.queueEvent( "cct_test_run", null ); | ||||
|  | ||||
|             // Wait for test execution to fully finish | ||||
|             lock.lockInterruptibly(); | ||||
|             try | ||||
|             { | ||||
|                 long remaining = TIMEOUT; | ||||
|                 while( remaining > 0 && !finished ) | ||||
|                 { | ||||
|                     tick(); | ||||
|                     if( hasFinished.awaitNanos( TICK_TIME ) > 0 ) break; | ||||
|                     remaining -= TICK_TIME; | ||||
|                 } | ||||
|  | ||||
|                 if( remaining <= 0 ) throw new IllegalStateException( "Timed out waiting for finish." + dump() ); | ||||
|                 if( !finished ) throw new IllegalStateException( "Computer did not finish." + dump() ); | ||||
|             } | ||||
|             finally | ||||
|             { | ||||
|                 lock.unlock(); | ||||
|             } | ||||
|         } | ||||
|         finally | ||||
|         { | ||||
|             // Show a dump of computer output | ||||
|             System.out.println( dump() ); | ||||
|  | ||||
|             // And shutdown | ||||
|             computer.shutdown(); | ||||
|         } | ||||
|  | ||||
|         if( finishedWith != null ) | ||||
|         { | ||||
|             REPORT_PATH.getParentFile().mkdirs(); | ||||
|             try( BufferedWriter writer = Files.newBufferedWriter( REPORT_PATH.toPath() ) ) | ||||
|             { | ||||
|                 // new LuaCoverage( finishedWith ).write( writer ); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @TestFactory | ||||
|     public Stream<DynamicNode> get() throws InterruptedException | ||||
|     { | ||||
|         lock.lockInterruptibly(); | ||||
|         try | ||||
|         { | ||||
|             long remaining = TIMEOUT; | ||||
|             while( remaining > 0 & tests == null ) | ||||
|             { | ||||
|                 tick(); | ||||
|                 if( hasTests.awaitNanos( TICK_TIME ) > 0 ) break; | ||||
|                 remaining -= TICK_TIME; | ||||
|             } | ||||
|  | ||||
|             if( remaining <= 0 ) throw new IllegalStateException( "Timed out waiting for tests. " + dump() ); | ||||
|             if( tests == null ) throw new IllegalStateException( "Computer did not provide any tests. " + dump() ); | ||||
|         } | ||||
|         finally | ||||
|         { | ||||
|             lock.unlock(); | ||||
|         } | ||||
|  | ||||
|         return tests.buildChildren(); | ||||
|     } | ||||
|  | ||||
|     private static class DynamicNodeBuilder | ||||
|     { | ||||
|         private final String name; | ||||
|         private final Map<String, DynamicNodeBuilder> children; | ||||
|         private final Executable executor; | ||||
|  | ||||
|         DynamicNodeBuilder( String name ) | ||||
|         { | ||||
|             this.name = name; | ||||
|             this.children = new HashMap<>(); | ||||
|             this.executor = null; | ||||
|         } | ||||
|  | ||||
|         DynamicNodeBuilder( String name, Executable executor ) | ||||
|         { | ||||
|             this.name = name; | ||||
|             this.children = Collections.emptyMap(); | ||||
|             this.executor = executor; | ||||
|         } | ||||
|  | ||||
|         DynamicNodeBuilder get( String name ) | ||||
|         { | ||||
|             DynamicNodeBuilder child = children.get( name ); | ||||
|             if( child == null ) children.put( name, child = new DynamicNodeBuilder( name ) ); | ||||
|             return child; | ||||
|         } | ||||
|  | ||||
|         void runs( String name, Executable executor ) | ||||
|         { | ||||
|             DynamicNodeBuilder child = children.get( name ); | ||||
|             int id = 0; | ||||
|             while( child != null ) | ||||
|             { | ||||
|                 id++; | ||||
|                 String subName = name + "_" + id; | ||||
|                 child = children.get( subName ); | ||||
|             } | ||||
|  | ||||
|             children.put( name, new DynamicNodeBuilder( name, executor ) ); | ||||
|         } | ||||
|  | ||||
|         DynamicNode build() | ||||
|         { | ||||
|             return executor == null | ||||
|                 ? DynamicContainer.dynamicContainer( name, buildChildren() ) | ||||
|                 : DynamicTest.dynamicTest( name, executor ); | ||||
|         } | ||||
|  | ||||
|         Stream<DynamicNode> buildChildren() | ||||
|         { | ||||
|             return children.values().stream().map( DynamicNodeBuilder::build ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private String dump() | ||||
|     { | ||||
|         if( !computer.isOn() ) return "Computer is currently off."; | ||||
|  | ||||
|         Terminal term = computer.getAPIEnvironment().getTerminal(); | ||||
|         StringBuilder builder = new StringBuilder().append( "Computer is currently on.\n" ); | ||||
|  | ||||
|         for( int line = 0; line < term.getHeight(); line++ ) | ||||
|         { | ||||
|             builder.append( String.format( "%2d | %" + term.getWidth() + "s |\n", line + 1, term.getLine( line ) ) ); | ||||
|         } | ||||
|  | ||||
|         computer.shutdown(); | ||||
|         return builder.toString(); | ||||
|     } | ||||
|  | ||||
|     private void tick() | ||||
|     { | ||||
|         computer.tick(); | ||||
|         MainThread.executePendingTasks(); | ||||
|     } | ||||
|  | ||||
|     private static String formatName( String name ) | ||||
|     { | ||||
|         return name.replace( "\0", " -> " ); | ||||
|     } | ||||
|  | ||||
|     private static class FakeModem extends WirelessModemPeripheral | ||||
|     { | ||||
|         FakeModem() | ||||
|         { | ||||
|             super( new ModemState(), true ); | ||||
|         } | ||||
|  | ||||
|         @Nonnull | ||||
|         @Override | ||||
|         @SuppressWarnings( "ConstantConditions" ) | ||||
|         public World getWorld() | ||||
|         { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         @Nonnull | ||||
|         @Override | ||||
|         public Vec3d getPosition() | ||||
|         { | ||||
|             return Vec3d.ZERO; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public boolean equals( @Nullable IPeripheral other ) | ||||
|         { | ||||
|             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(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,67 +0,0 @@ | ||||
| /* | ||||
|  * This file is part of ComputerCraft - http://www.computercraft.info | ||||
|  * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. | ||||
|  * Send enquiries to dratcliffe@gmail.com | ||||
|  */ | ||||
| package dan200.computercraft.core.apis; | ||||
|  | ||||
| import dan200.computercraft.api.lua.*; | ||||
| import dan200.computercraft.core.asm.LuaMethod; | ||||
| import dan200.computercraft.core.asm.NamedMethod; | ||||
|  | ||||
| import javax.annotation.Nonnull; | ||||
| import java.util.HashMap; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import java.util.Objects; | ||||
|  | ||||
| public class ObjectWrapper implements ILuaContext | ||||
| { | ||||
|     private final Object object; | ||||
|     private final Map<String, LuaMethod> methodMap; | ||||
|  | ||||
|     public ObjectWrapper( Object object ) | ||||
|     { | ||||
|         this.object = object; | ||||
|         String[] dynamicMethods = object instanceof IDynamicLuaObject | ||||
|             ? Objects.requireNonNull( ((IDynamicLuaObject) object).getMethodNames(), "Methods cannot be null" ) | ||||
|             : LuaMethod.EMPTY_METHODS; | ||||
|  | ||||
|         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++ ) | ||||
|         { | ||||
|             methodMap.put( dynamicMethods[i], LuaMethod.DYNAMIC.get( i ) ); | ||||
|         } | ||||
|         for( NamedMethod<LuaMethod> method : methods ) | ||||
|         { | ||||
|             methodMap.put( method.getName(), method.getMethod() ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public Object[] call( String name, Object... args ) throws LuaException | ||||
|     { | ||||
|         LuaMethod method = methodMap.get( name ); | ||||
|         if( method == null ) throw new IllegalStateException( "No such method '" + name + "'" ); | ||||
|  | ||||
|         return method.apply( object, this, new ObjectArguments( args ) ).getResult(); | ||||
|     } | ||||
|  | ||||
|     @SuppressWarnings( "unchecked" ) | ||||
|     public <T> T callOf( String name, Object... args ) throws LuaException | ||||
|     { | ||||
|         return (T) call( name, args )[0]; | ||||
|     } | ||||
|  | ||||
|     public <T> T callOf( Class<T> klass, String name, Object... args ) throws LuaException | ||||
|     { | ||||
|         return klass.cast( call( name, args )[0] ); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public long issueMainThreadTask( @Nonnull ILuaTask task ) | ||||
|     { | ||||
|         throw new IllegalStateException( "Method should never queue events" ); | ||||
|     } | ||||
| } | ||||
| @@ -1,86 +0,0 @@ | ||||
| /* | ||||
|  * This file is part of ComputerCraft - http://www.computercraft.info | ||||
|  * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. | ||||
|  * Send enquiries to dratcliffe@gmail.com | ||||
|  */ | ||||
| package dan200.computercraft.core.apis.handles; | ||||
|  | ||||
| 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; | ||||
|  | ||||
| import static org.junit.jupiter.api.Assertions.*; | ||||
|  | ||||
| public class BinaryReadableHandleTest | ||||
| { | ||||
|     @Test | ||||
|     public void testReadChar() throws LuaException | ||||
|     { | ||||
|         ObjectWrapper wrapper = fromLength( 5 ); | ||||
|         assertEquals( 'A', (int) wrapper.callOf( Integer.class, "read" ) ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testReadShortComplete() throws LuaException | ||||
|     { | ||||
|         ObjectWrapper wrapper = fromLength( 10 ); | ||||
|         assertEquals( 5, wrapper.<ByteBuffer>callOf( "read", 5 ).remaining() ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testReadShortPartial() throws LuaException | ||||
|     { | ||||
|         ObjectWrapper wrapper = fromLength( 5 ); | ||||
|         assertEquals( 5, wrapper.<ByteBuffer>callOf( "read", 10 ).remaining() ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testReadLongComplete() throws LuaException | ||||
|     { | ||||
|         ObjectWrapper wrapper = fromLength( 10000 ); | ||||
|         assertEquals( 9000, wrapper.<byte[]>callOf( "read", 9000 ).length ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testReadLongPartial() throws LuaException | ||||
|     { | ||||
|         ObjectWrapper wrapper = fromLength( 10000 ); | ||||
|         assertEquals( 10000, wrapper.<byte[]>callOf( "read", 11000 ).length ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testReadLongPartialSmaller() throws LuaException | ||||
|     { | ||||
|         ObjectWrapper wrapper = fromLength( 1000 ); | ||||
|         assertEquals( 1000, wrapper.<ByteBuffer>callOf( "read", 11000 ).remaining() ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testReadLine() throws LuaException | ||||
|     { | ||||
|         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" ) ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testReadLineTrailing() throws LuaException | ||||
|     { | ||||
|         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 ) ); | ||||
|     } | ||||
|  | ||||
|     private static ObjectWrapper fromLength( int length ) | ||||
|     { | ||||
|         byte[] input = new byte[length]; | ||||
|         Arrays.fill( input, (byte) 'A' ); | ||||
|         return new ObjectWrapper( BinaryReadableHandle.of( new ArrayByteChannel( input ) ) ); | ||||
|     } | ||||
| } | ||||
| @@ -1,69 +0,0 @@ | ||||
| /* | ||||
|  * This file is part of ComputerCraft - http://www.computercraft.info | ||||
|  * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. | ||||
|  * Send enquiries to dratcliffe@gmail.com | ||||
|  */ | ||||
| package dan200.computercraft.core.apis.handles; | ||||
|  | ||||
| import dan200.computercraft.api.lua.LuaException; | ||||
| import dan200.computercraft.core.apis.ObjectWrapper; | ||||
| import org.junit.jupiter.api.Test; | ||||
|  | ||||
| import java.io.BufferedReader; | ||||
| import java.io.CharArrayReader; | ||||
| import java.util.Arrays; | ||||
|  | ||||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||||
|  | ||||
| public class EncodedReadableHandleTest | ||||
| { | ||||
|     @Test | ||||
|     public void testReadChar() throws LuaException | ||||
|     { | ||||
|         ObjectWrapper wrapper = fromLength( 5 ); | ||||
|         assertEquals( "A", wrapper.callOf( "read" ) ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testReadShortComplete() throws LuaException | ||||
|     { | ||||
|         ObjectWrapper wrapper = fromLength( 10 ); | ||||
|         assertEquals( "AAAAA", wrapper.callOf( "read", 5 ) ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testReadShortPartial() throws LuaException | ||||
|     { | ||||
|         ObjectWrapper wrapper = fromLength( 5 ); | ||||
|         assertEquals( "AAAAA", wrapper.callOf( "read", 10 ) ); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     @Test | ||||
|     public void testReadLongComplete() throws LuaException | ||||
|     { | ||||
|         ObjectWrapper wrapper = fromLength( 10000 ); | ||||
|         assertEquals( 9000, wrapper.<String>callOf( "read", 9000 ).length() ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testReadLongPartial() throws LuaException | ||||
|     { | ||||
|         ObjectWrapper wrapper = fromLength( 10000 ); | ||||
|         assertEquals( 10000, wrapper.<String>callOf( "read", 11000 ).length() ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testReadLongPartialSmaller() throws LuaException | ||||
|     { | ||||
|         ObjectWrapper wrapper = fromLength( 1000 ); | ||||
|         assertEquals( 1000, wrapper.<String>callOf( "read", 11000 ).length() ); | ||||
|     } | ||||
|  | ||||
|     private static ObjectWrapper fromLength( int length ) | ||||
|     { | ||||
|         char[] input = new char[length]; | ||||
|         Arrays.fill( input, 'A' ); | ||||
|         return new ObjectWrapper( new EncodedReadableHandle( new BufferedReader( new CharArrayReader( input ) ) ) ); | ||||
|     } | ||||
| } | ||||
| @@ -1,33 +0,0 @@ | ||||
| /* | ||||
|  * This file is part of ComputerCraft - http://www.computercraft.info | ||||
|  * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. | ||||
|  * Send enquiries to dratcliffe@gmail.com | ||||
|  */ | ||||
| package dan200.computercraft.core.apis.http.options; | ||||
|  | ||||
| import org.junit.jupiter.api.Test; | ||||
|  | ||||
| import java.net.InetSocketAddress; | ||||
| import java.util.Collections; | ||||
|  | ||||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||||
|  | ||||
| public class AddressRuleTest | ||||
| { | ||||
|     @Test | ||||
|     public void matchesPort() | ||||
|     { | ||||
|         Iterable<AddressRule> rules = Collections.singletonList( AddressRule.parse( | ||||
|             "127.0.0.1", 8080, | ||||
|             new PartialOptions( Action.ALLOW, null, null, null, null ) | ||||
|         ) ); | ||||
|  | ||||
|         assertEquals( apply( rules, "localhost", 8080 ).action, Action.ALLOW ); | ||||
|         assertEquals( apply( rules, "localhost", 8081 ).action, Action.DENY ); | ||||
|     } | ||||
|  | ||||
|     private Options apply( Iterable<AddressRule> rules, String host, int port ) | ||||
|     { | ||||
|         return AddressRule.apply( rules, host, new InetSocketAddress( host, port ) ); | ||||
|     } | ||||
| } | ||||
| @@ -1,165 +0,0 @@ | ||||
| /* | ||||
|  * This file is part of ComputerCraft - http://www.computercraft.info | ||||
|  * Copyright Daniel Ratcliffe, 2011-2021. 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 javax.annotation.Nonnull; | ||||
| 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; | ||||
|     } | ||||
|  | ||||
|     @Nonnull | ||||
|     @Override | ||||
|     public String getHostString() | ||||
|     { | ||||
|         return "ComputerCraft 1.0 (Test environment)"; | ||||
|     } | ||||
|  | ||||
|     @Nonnull | ||||
|     @Override | ||||
|     public String getUserAgent() | ||||
|     { | ||||
|         return "ComputerCraft/1.0"; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public IMount createResourceMount( String domain, String subPath ) | ||||
|     { | ||||
|         return createMount( ComputerCraft.class, "data/" + domain + "/" + subPath, "main" ); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public InputStream createResourceFile( String domain, String subPath ) | ||||
|     { | ||||
|         return ComputerCraft.class.getClassLoader().getResourceAsStream( "data/" + domain + "/" + subPath ); | ||||
|     } | ||||
|  | ||||
|     public static IMount createMount( Class<?> klass, String path, String fallback ) | ||||
|     { | ||||
|         File file = getContainingFile( klass ); | ||||
|  | ||||
|         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, "src/" + fallback + "/resources/" + path ); | ||||
|             } | ||||
|  | ||||
|             if( !wholeFile.exists() ) throw new IllegalStateException( "Cannot find ROM mount at " + file ); | ||||
|  | ||||
|             return new FileMount( wholeFile, 0 ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     private static File getContainingFile( Class<?> klass ) | ||||
|     { | ||||
|         String path = klass.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() ); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,138 +0,0 @@ | ||||
| /* | ||||
|  * This file is part of ComputerCraft - http://www.computercraft.info | ||||
|  * Copyright Daniel Ratcliffe, 2011-2021. 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.IWritableMount; | ||||
| import dan200.computercraft.api.lua.IArguments; | ||||
| import dan200.computercraft.api.lua.ILuaAPI; | ||||
| import dan200.computercraft.api.lua.LuaException; | ||||
| 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 java.util.Arrays; | ||||
| import java.util.function.Consumer; | ||||
|  | ||||
| /** | ||||
|  * Helper class to run a program on a computer. | ||||
|  */ | ||||
| public class ComputerBootstrap | ||||
| { | ||||
|     private static final int TPS = 20; | ||||
|     public static final int MAX_TIME = 10; | ||||
|  | ||||
|     public static void run( String program, Consumer<Computer> setup, int maxTimes ) | ||||
|     { | ||||
|         MemoryMount mount = new MemoryMount() | ||||
|             .addFile( "test.lua", program ) | ||||
|             .addFile( "startup.lua", "assertion.assert(pcall(loadfile('test.lua', nil, _ENV))) os.shutdown()" ); | ||||
|  | ||||
|         run( mount, setup, maxTimes ); | ||||
|     } | ||||
|  | ||||
|     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 ); | ||||
|  | ||||
|         AssertApi api = new AssertApi(); | ||||
|         computer.addApi( api ); | ||||
|  | ||||
|         setup.accept( computer ); | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             computer.turnOn(); | ||||
|             boolean everOn = false; | ||||
|  | ||||
|             for( int tick = 0; tick < TPS * maxTicks; tick++ ) | ||||
|             { | ||||
|                 long start = System.currentTimeMillis(); | ||||
|  | ||||
|                 computer.tick(); | ||||
|                 MainThread.executePendingTasks(); | ||||
|  | ||||
|                 if( api.message != null ) | ||||
|                 { | ||||
|                     ComputerCraft.log.debug( "Shutting down due to error" ); | ||||
|                     computer.shutdown(); | ||||
|                     Assertions.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; | ||||
|             } | ||||
|  | ||||
|             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(); | ||||
|                 Assertions.fail( builder.toString() ); | ||||
|             } | ||||
|         } | ||||
|         catch( InterruptedException ignored ) | ||||
|         { | ||||
|             Thread.currentThread().interrupt(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public static class AssertApi implements ILuaAPI | ||||
|     { | ||||
|         boolean didAssert; | ||||
|         String message; | ||||
|  | ||||
|         @Override | ||||
|         public String[] getNames() | ||||
|         { | ||||
|             return new String[] { "assertion" }; | ||||
|         } | ||||
|  | ||||
|         @LuaFunction | ||||
|         public final void log( IArguments arguments ) | ||||
|         { | ||||
|             ComputerCraft.log.info( "[Computer] {}", Arrays.toString( arguments.getAll() ) ); | ||||
|         } | ||||
|  | ||||
|         @LuaFunction( "assert" ) | ||||
|         public final Object[] doAssert( IArguments arguments ) throws LuaException | ||||
|         { | ||||
|             didAssert = true; | ||||
|  | ||||
|             Object arg = arguments.get( 0 ); | ||||
|             if( arg == null || arg == Boolean.FALSE ) | ||||
|             { | ||||
|                 message = arguments.optString( 1, "Assertion failed" ); | ||||
|                 throw new LuaException( message ); | ||||
|             } | ||||
|  | ||||
|             return arguments.getAll(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,49 +0,0 @@ | ||||
| /* | ||||
|  * This file is part of ComputerCraft - http://www.computercraft.info | ||||
|  * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. | ||||
|  * Send enquiries to dratcliffe@gmail.com | ||||
|  */ | ||||
| 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; | ||||
|  | ||||
| public class ComputerTest | ||||
| { | ||||
|     @Test | ||||
|     public void testTimeout() | ||||
|     { | ||||
|         assertTimeoutPreemptively( ofSeconds( 20 ), () -> { | ||||
|             try | ||||
|             { | ||||
|                 ComputerBootstrap.run( "print('Hello') while true do end", ComputerBootstrap.MAX_TIME ); | ||||
|             } | ||||
|             catch( AssertionError e ) | ||||
|             { | ||||
|                 if( e.getMessage().equals( "test.lua:1: Too long without yielding" ) ) return; | ||||
|                 throw e; | ||||
|             } | ||||
|  | ||||
|             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 ); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,81 +0,0 @@ | ||||
| /* | ||||
|  * This file is part of ComputerCraft - http://www.computercraft.info | ||||
|  * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. | ||||
|  * Send enquiries to dratcliffe@gmail.com | ||||
|  */ | ||||
| package dan200.computercraft.core.filesystem; | ||||
|  | ||||
| import com.google.common.io.Files; | ||||
| import dan200.computercraft.api.filesystem.IWritableMount; | ||||
| import dan200.computercraft.api.lua.LuaException; | ||||
| import dan200.computercraft.core.apis.ObjectWrapper; | ||||
| import dan200.computercraft.core.apis.handles.EncodedWritableHandle; | ||||
| import org.junit.jupiter.api.Test; | ||||
|  | ||||
| import java.io.BufferedWriter; | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
| import java.nio.charset.StandardCharsets; | ||||
|  | ||||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||||
| import static org.junit.jupiter.api.Assertions.assertThrows; | ||||
|  | ||||
| public class FileSystemTest | ||||
| { | ||||
|     private static final File ROOT = new File( "test-files/filesystem" ); | ||||
|     private static final long CAPACITY = 1000000; | ||||
|  | ||||
|     private static FileSystem mkFs() throws FileSystemException | ||||
|     { | ||||
|         IWritableMount writableMount = new FileMount( ROOT, CAPACITY ); | ||||
|         return new FileSystem( "hdd", writableMount ); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 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 | ||||
|      */ | ||||
|     @Test | ||||
|     public void testWriteTruncates() throws FileSystemException, LuaException, IOException | ||||
|     { | ||||
|         FileSystem fs = mkFs(); | ||||
|  | ||||
|         { | ||||
|             FileSystemWrapper<BufferedWriter> writer = fs.openForWrite( "out.txt", false, EncodedWritableHandle::openUtf8 ); | ||||
|             ObjectWrapper wrapper = new ObjectWrapper( new EncodedWritableHandle( writer.get(), writer ) ); | ||||
|             wrapper.call( "write", "This is a long line" ); | ||||
|             wrapper.call( "close" ); | ||||
|         } | ||||
|  | ||||
|         assertEquals( "This is a long line", Files.asCharSource( new File( ROOT, "out.txt" ), StandardCharsets.UTF_8 ).read() ); | ||||
|  | ||||
|         { | ||||
|             FileSystemWrapper<BufferedWriter> writer = fs.openForWrite( "out.txt", false, EncodedWritableHandle::openUtf8 ); | ||||
|             ObjectWrapper wrapper = new ObjectWrapper( new EncodedWritableHandle( writer.get(), writer ) ); | ||||
|             wrapper.call( "write", "Tiny line" ); | ||||
|             wrapper.call( "close" ); | ||||
|         } | ||||
|  | ||||
|         assertEquals( "Tiny line", Files.asCharSource( new File( ROOT, "out.txt" ), StandardCharsets.UTF_8 ).read() ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testUnmountCloses() throws FileSystemException | ||||
|     { | ||||
|         FileSystem fs = mkFs(); | ||||
|         IWritableMount mount = new FileMount( new File( ROOT, "child" ), CAPACITY ); | ||||
|         fs.mountWritable( "disk", "disk", mount ); | ||||
|  | ||||
|         FileSystemWrapper<BufferedWriter> writer = fs.openForWrite( "disk/out.txt", false, EncodedWritableHandle::openUtf8 ); | ||||
|         ObjectWrapper wrapper = new ObjectWrapper( new EncodedWritableHandle( writer.get(), writer ) ); | ||||
|  | ||||
|         fs.unmount( "disk" ); | ||||
|  | ||||
|         LuaException err = assertThrows( LuaException.class, () -> wrapper.call( "write", "Tiny line" ) ); | ||||
|         assertEquals( "attempt to use a closed file", err.getMessage() ); | ||||
|     } | ||||
| } | ||||
| @@ -1,108 +0,0 @@ | ||||
| /* | ||||
|  * This file is part of ComputerCraft - http://www.computercraft.info | ||||
|  * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. | ||||
|  * Send enquiries to dratcliffe@gmail.com | ||||
|  */ | ||||
| package dan200.computercraft.core.filesystem; | ||||
|  | ||||
| import com.google.common.io.ByteStreams; | ||||
| import dan200.computercraft.api.filesystem.IMount; | ||||
| import org.junit.jupiter.api.BeforeAll; | ||||
| import org.junit.jupiter.api.Test; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.io.FileOutputStream; | ||||
| import java.io.IOException; | ||||
| import java.nio.channels.Channels; | ||||
| import java.nio.channels.ReadableByteChannel; | ||||
| import java.nio.charset.StandardCharsets; | ||||
| import java.nio.file.attribute.BasicFileAttributes; | ||||
| import java.nio.file.attribute.FileTime; | ||||
| import java.time.Instant; | ||||
| import java.time.temporal.ChronoUnit; | ||||
| import java.util.zip.ZipEntry; | ||||
| import java.util.zip.ZipOutputStream; | ||||
|  | ||||
| import static org.junit.jupiter.api.Assertions.*; | ||||
|  | ||||
| public class JarMountTest | ||||
| { | ||||
|     private static final File ZIP_FILE = new File( "test-files/jar-mount.zip" ); | ||||
|  | ||||
|     private static final FileTime MODIFY_TIME = FileTime.from( Instant.EPOCH.plus( 2, ChronoUnit.DAYS ) ); | ||||
|  | ||||
|     @BeforeAll | ||||
|     public static void before() throws IOException | ||||
|     { | ||||
|         ZIP_FILE.getParentFile().mkdirs(); | ||||
|  | ||||
|         try( ZipOutputStream stream = new ZipOutputStream( new FileOutputStream( ZIP_FILE ) ) ) | ||||
|         { | ||||
|             stream.putNextEntry( new ZipEntry( "dir/" ) ); | ||||
|             stream.closeEntry(); | ||||
|  | ||||
|             stream.putNextEntry( new ZipEntry( "dir/file.lua" ).setLastModifiedTime( MODIFY_TIME ) ); | ||||
|             stream.write( "print('testing')".getBytes( StandardCharsets.UTF_8 ) ); | ||||
|             stream.closeEntry(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void mountsDir() throws IOException | ||||
|     { | ||||
|         IMount mount = new JarMount( ZIP_FILE, "dir" ); | ||||
|         assertTrue( mount.isDirectory( "" ), "Root should be directory" ); | ||||
|         assertTrue( mount.exists( "file.lua" ), "File should exist" ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void mountsFile() throws IOException | ||||
|     { | ||||
|         IMount mount = new JarMount( ZIP_FILE, "dir/file.lua" ); | ||||
|         assertTrue( mount.exists( "" ), "Root should exist" ); | ||||
|         assertFalse( mount.isDirectory( "" ), "Root should be a file" ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void opensFileFromFile() throws IOException | ||||
|     { | ||||
|         IMount mount = new JarMount( ZIP_FILE, "dir/file.lua" ); | ||||
|         byte[] contents; | ||||
|         try( ReadableByteChannel stream = mount.openForRead( "" ) ) | ||||
|         { | ||||
|             contents = ByteStreams.toByteArray( Channels.newInputStream( stream ) ); | ||||
|         } | ||||
|  | ||||
|         assertEquals( new String( contents, StandardCharsets.UTF_8 ), "print('testing')" ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void opensFileFromDir() throws IOException | ||||
|     { | ||||
|         IMount mount = new JarMount( ZIP_FILE, "dir" ); | ||||
|         byte[] contents; | ||||
|         try( ReadableByteChannel stream = mount.openForRead( "file.lua" ) ) | ||||
|         { | ||||
|             contents = ByteStreams.toByteArray( Channels.newInputStream( stream ) ); | ||||
|         } | ||||
|  | ||||
|         assertEquals( new String( contents, StandardCharsets.UTF_8 ), "print('testing')" ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void fileAttributes() throws IOException | ||||
|     { | ||||
|         BasicFileAttributes attributes = new JarMount( ZIP_FILE, "dir" ).getAttributes( "file.lua" ); | ||||
|         assertFalse( attributes.isDirectory() ); | ||||
|         assertEquals( "print('testing')".length(), attributes.size() ); | ||||
|         assertEquals( MODIFY_TIME, attributes.lastModifiedTime() ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void directoryAttributes() throws IOException | ||||
|     { | ||||
|         BasicFileAttributes attributes = new JarMount( ZIP_FILE, "dir" ).getAttributes( "" ); | ||||
|         assertTrue( attributes.isDirectory() ); | ||||
|         assertEquals( 0, attributes.size() ); | ||||
|     } | ||||
| } | ||||
| @@ -1,148 +0,0 @@ | ||||
| /* | ||||
|  * This file is part of ComputerCraft - http://www.computercraft.info | ||||
|  * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. | ||||
|  * Send enquiries to dratcliffe@gmail.com | ||||
|  */ | ||||
| package dan200.computercraft.core.filesystem; | ||||
|  | ||||
| import dan200.computercraft.api.filesystem.IWritableMount; | ||||
| import dan200.computercraft.core.apis.handles.ArrayByteChannel; | ||||
|  | ||||
| import javax.annotation.Nonnull; | ||||
| import java.io.ByteArrayOutputStream; | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
| import java.nio.channels.Channels; | ||||
| import java.nio.channels.ReadableByteChannel; | ||||
| import java.nio.channels.WritableByteChannel; | ||||
| import java.util.*; | ||||
|  | ||||
| /** | ||||
|  * In-memory file mounts. | ||||
|  */ | ||||
| public class MemoryMount implements IWritableMount | ||||
| { | ||||
|     private final Map<String, byte[]> files = new HashMap<>(); | ||||
|     private final Set<String> 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() ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Nonnull | ||||
|     @Override | ||||
|     public WritableByteChannel openForWrite( @Nonnull final String path ) | ||||
|     { | ||||
|         return Channels.newChannel( new ByteArrayOutputStream() | ||||
|         { | ||||
|             @Override | ||||
|             public void close() throws IOException | ||||
|             { | ||||
|                 super.close(); | ||||
|                 files.put( path, toByteArray() ); | ||||
|             } | ||||
|         } ); | ||||
|     } | ||||
|  | ||||
|     @Nonnull | ||||
|     @Override | ||||
|     public WritableByteChannel 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 Channels.newChannel( 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<String> files ) | ||||
|     { | ||||
|         for( String file : this.files.keySet() ) | ||||
|         { | ||||
|             if( file.startsWith( path ) ) files.add( file.substring( path.length() + 1 ) ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public long getSize( @Nonnull String path ) | ||||
|     { | ||||
|         throw new RuntimeException( "Not implemented" ); | ||||
|     } | ||||
|  | ||||
|     @Nonnull | ||||
|     @Override | ||||
|     public ReadableByteChannel openForRead( @Nonnull String path ) | ||||
|     { | ||||
|         return new ArrayByteChannel( files.get( path ) ); | ||||
|     } | ||||
|  | ||||
|     public MemoryMount addFile( String file, String contents ) | ||||
|     { | ||||
|         files.put( file, contents.getBytes() ); | ||||
|         return this; | ||||
|     } | ||||
| } | ||||
| @@ -1,75 +0,0 @@ | ||||
| /* | ||||
|  * This file is part of ComputerCraft - http://www.computercraft.info | ||||
|  * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. | ||||
|  * Send enquiries to dratcliffe@gmail.com | ||||
|  */ | ||||
| package dan200.computercraft.core.filesystem; | ||||
|  | ||||
| import dan200.computercraft.api.filesystem.IMount; | ||||
| import org.junit.jupiter.api.BeforeEach; | ||||
| import org.junit.jupiter.api.Test; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Arrays; | ||||
| import java.util.Comparator; | ||||
| import java.util.List; | ||||
|  | ||||
| import static org.junit.jupiter.api.Assertions.*; | ||||
|  | ||||
| public class ResourceMountTest | ||||
| { | ||||
|     private IMount mount; | ||||
|  | ||||
|     @BeforeEach | ||||
|     public void before() | ||||
|     { | ||||
| //        ReloadableResourceManager manager = new SimpleResourceReloadListener(ResourceType.SERVER_DATA); | ||||
| //        manager.addResourcePack( new FolderPack( new File( "src/main/resources" ) ) ); | ||||
| // | ||||
| //        mount = ResourceMount.get( "computercraft", "lua/rom", manager ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testList() throws IOException | ||||
|     { | ||||
|         List<String> files = new ArrayList<>(); | ||||
|         mount.list( "", files ); | ||||
|         files.sort( Comparator.naturalOrder() ); | ||||
|  | ||||
|         assertEquals( | ||||
|             Arrays.asList( "apis", "autorun", "help", "modules", "motd.txt", "programs", "startup.lua" ), | ||||
|             files | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testExists() throws IOException | ||||
|     { | ||||
|         assertTrue( mount.exists( "" ) ); | ||||
|         assertTrue( mount.exists( "startup.lua" ) ); | ||||
|         assertTrue( mount.exists( "programs/fun/advanced/paint.lua" ) ); | ||||
|  | ||||
|         assertFalse( mount.exists( "programs/fun/advance/paint.lua" ) ); | ||||
|         assertFalse( mount.exists( "programs/fun/advanced/paint.lu" ) ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testIsDir() throws IOException | ||||
|     { | ||||
|         assertTrue( mount.isDirectory( "" ) ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testIsFile() throws IOException | ||||
|     { | ||||
|         assertFalse( mount.isDirectory( "startup.lua" ) ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testSize() throws IOException | ||||
|     { | ||||
|         assertNotEquals( mount.getSize( "startup.lua" ), 0 ); | ||||
|     } | ||||
| } | ||||
| @@ -1,150 +0,0 @@ | ||||
| /* | ||||
|  * This file is part of ComputerCraft - http://www.computercraft.info | ||||
|  * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. | ||||
|  * Send enquiries to dratcliffe@gmail.com | ||||
|  */ | ||||
| package dan200.computercraft.core.terminal; | ||||
|  | ||||
| import org.junit.jupiter.api.Test; | ||||
|  | ||||
| import static org.junit.jupiter.api.Assertions.*; | ||||
|  | ||||
| class TextBufferTest | ||||
| { | ||||
|     @Test | ||||
|     void testStringConstructor() | ||||
|     { | ||||
|         TextBuffer textBuffer = new TextBuffer( "test" ); | ||||
|         assertEquals( "test", textBuffer.toString() ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     void testCharRepetitionConstructor() | ||||
|     { | ||||
|         TextBuffer textBuffer = new TextBuffer( 'a', 5 ); | ||||
|         assertEquals( "aaaaa", textBuffer.toString() ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     void testLength() | ||||
|     { | ||||
|         TextBuffer textBuffer = new TextBuffer( "test" ); | ||||
|         assertEquals( 4, textBuffer.length() ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     void testWrite() | ||||
|     { | ||||
|         TextBuffer textBuffer = new TextBuffer( ' ', 4 ); | ||||
|         textBuffer.write( "test" ); | ||||
|         assertEquals( "test", textBuffer.toString() ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     void testWriteTextBuffer() | ||||
|     { | ||||
|         TextBuffer source = new TextBuffer( "test" ); | ||||
|         TextBuffer target = new TextBuffer( "    " ); | ||||
|         target.write( source ); | ||||
|         assertEquals( "test", target.toString() ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     void testWriteFromPos() | ||||
|     { | ||||
|         TextBuffer textBuffer = new TextBuffer( "test" ); | ||||
|         textBuffer.write( "il", 1 ); | ||||
|         assertEquals( "tilt", textBuffer.toString() ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     void testWriteOutOfBounds() | ||||
|     { | ||||
|         TextBuffer textBuffer = new TextBuffer( "test" ); | ||||
|         textBuffer.write( "abcdefghijklmnop", -5 ); | ||||
|         assertEquals( "fghi", textBuffer.toString() ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     void testWriteOutOfBounds2() | ||||
|     { | ||||
|         TextBuffer textBuffer = new TextBuffer( "             " ); | ||||
|         textBuffer.write( "Hello, world!", -3 ); | ||||
|         assertEquals( "lo, world!   ", textBuffer.toString() ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     void testFill() | ||||
|     { | ||||
|         TextBuffer textBuffer = new TextBuffer( "test" ); | ||||
|         textBuffer.fill( 'c' ); | ||||
|         assertEquals( "cccc", textBuffer.toString() ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     void testFillSubstring() | ||||
|     { | ||||
|         TextBuffer textBuffer = new TextBuffer( "test" ); | ||||
|         textBuffer.fill( 'c', 1, 3 ); | ||||
|         assertEquals( "tcct", textBuffer.toString() ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     void testFillOutOfBounds() | ||||
|     { | ||||
|         TextBuffer textBuffer = new TextBuffer( "test" ); | ||||
|         textBuffer.fill( 'c', -5, 5 ); | ||||
|         assertEquals( "cccc", textBuffer.toString() ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     void testCharAt() | ||||
|     { | ||||
|         TextBuffer textBuffer = new TextBuffer( "test" ); | ||||
|         assertEquals( 'e', textBuffer.charAt( 1 ) ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     void testSetChar() | ||||
|     { | ||||
|         TextBuffer textBuffer = new TextBuffer( "test" ); | ||||
|         textBuffer.setChar( 2, 'n' ); | ||||
|         assertEquals( "tent", textBuffer.toString() ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     void testSetCharWithNegativeIndex() | ||||
|     { | ||||
|         TextBuffer textBuffer = new TextBuffer( "test" ); | ||||
|         textBuffer.setChar( -5, 'n' ); | ||||
|         assertEquals( "test", textBuffer.toString(), "Buffer should not change after setting char with negative index." ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     void testSetCharWithIndexBeyondBufferEnd() | ||||
|     { | ||||
|         TextBuffer textBuffer = new TextBuffer( "test" ); | ||||
|         textBuffer.setChar( 10, 'n' ); | ||||
|         assertEquals( "test", textBuffer.toString(), "Buffer should not change after setting char beyond buffer end." ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     void testMultipleOperations() | ||||
|     { | ||||
|         TextBuffer textBuffer = new TextBuffer( ' ', 5 ); | ||||
|         textBuffer.setChar( 0, 'H' ); | ||||
|         textBuffer.setChar( 1, 'e' ); | ||||
|         textBuffer.setChar( 2, 'l' ); | ||||
|         textBuffer.write( "lo", 3 ); | ||||
|         assertEquals( "Hello", textBuffer.toString(), "TextBuffer failed to persist over multiple operations." ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     void testEmptyBuffer() | ||||
|     { | ||||
|         TextBuffer textBuffer = new TextBuffer( "" ); | ||||
|         // exception on writing to empty buffer would fail the test | ||||
|         textBuffer.write( "test" ); | ||||
|         assertEquals( "", textBuffer.toString() ); | ||||
|     } | ||||
| } | ||||
| @@ -1,463 +0,0 @@ | ||||
| /* | ||||
|  * This file is part of ComputerCraft - http://www.computercraft.info | ||||
|  * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. | ||||
|  * Send enquiries to dratcliffe@gmail.com | ||||
|  */ | ||||
| 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.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.IPeripheral; | ||||
| import dan200.computercraft.shared.util.DirectionUtil; | ||||
| import net.minecraft.util.math.BlockPos; | ||||
| import net.minecraft.util.math.Direction; | ||||
| import net.minecraft.util.math.Vec3d; | ||||
| import net.minecraft.world.World; | ||||
| import org.junit.jupiter.api.Disabled; | ||||
| import org.junit.jupiter.api.Test; | ||||
|  | ||||
| import javax.annotation.Nonnull; | ||||
| import javax.annotation.Nullable; | ||||
| import java.util.Map; | ||||
| import java.util.Set; | ||||
| import java.util.function.BiConsumer; | ||||
| import java.util.function.BiFunction; | ||||
|  | ||||
| import static org.junit.jupiter.api.Assertions.*; | ||||
|  | ||||
| public class NetworkTest | ||||
| { | ||||
|     @Test | ||||
|     public void testConnect() | ||||
|     { | ||||
|         NetworkElement | ||||
|             aE = new NetworkElement( null, null, "a" ), | ||||
|             bE = new NetworkElement( null, null, "b" ), | ||||
|             cE = new NetworkElement( null, null, "c" ); | ||||
|  | ||||
|         IWiredNode | ||||
|             aN = aE.getNode(), | ||||
|             bN = bE.getNode(), | ||||
|             cN = cE.getNode(); | ||||
|  | ||||
|         assertNotEquals( aN.getNetwork(), bN.getNetwork(), "A's and B's network must be different" ); | ||||
|         assertNotEquals( aN.getNetwork(), cN.getNetwork(), "A's and C's network must be different" ); | ||||
|         assertNotEquals( bN.getNetwork(), cN.getNetwork(), "B's and C's network must be different" ); | ||||
|  | ||||
|         assertTrue( aN.getNetwork().connect( aN, bN ), "Must be able to add connection" ); | ||||
|         assertFalse( aN.getNetwork().connect( aN, bN ), "Cannot add connection twice" ); | ||||
|  | ||||
|         assertEquals( aN.getNetwork(), bN.getNetwork(), "A's and B's network must be equal" ); | ||||
|         assertEquals( Sets.newHashSet( aN, bN ), nodes( aN.getNetwork() ), "A's network should be A and B" ); | ||||
|  | ||||
|         assertEquals( Sets.newHashSet( "a", "b" ), aE.allPeripherals().keySet(), "A's peripheral set should be A, B" ); | ||||
|         assertEquals( Sets.newHashSet( "a", "b" ), bE.allPeripherals().keySet(), "B's peripheral set should be A, B" ); | ||||
|  | ||||
|         aN.getNetwork().connect( aN, cN ); | ||||
|  | ||||
|         assertEquals( aN.getNetwork(), bN.getNetwork(), "A's and B's network must be equal" ); | ||||
|         assertEquals( aN.getNetwork(), cN.getNetwork(), "A's and C's network must be equal" ); | ||||
|         assertEquals( Sets.newHashSet( aN, bN, cN ), nodes( aN.getNetwork() ), "A's network should be A, B and C" ); | ||||
|  | ||||
|         assertEquals( Sets.newHashSet( bN, cN ), neighbours( aN ), "A's neighbour set should be B, C" ); | ||||
|         assertEquals( Sets.newHashSet( aN ), neighbours( bN ), "B's neighbour set should be A" ); | ||||
|         assertEquals( Sets.newHashSet( aN ), neighbours( cN ), "C's neighbour set should be A" ); | ||||
|  | ||||
|         assertEquals( Sets.newHashSet( "a", "b", "c" ), aE.allPeripherals().keySet(), "A's peripheral set should be A, B, C" ); | ||||
|         assertEquals( Sets.newHashSet( "a", "b", "c" ), bE.allPeripherals().keySet(), "B's peripheral set should be A, B, C" ); | ||||
|         assertEquals( Sets.newHashSet( "a", "b", "c" ), cE.allPeripherals().keySet(), "C's peripheral set should be A, B, C" ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testDisconnectNoChange() | ||||
|     { | ||||
|         NetworkElement | ||||
|             aE = new NetworkElement( null, null, "a" ), | ||||
|             bE = new NetworkElement( null, null, "b" ), | ||||
|             cE = new NetworkElement( null, null, "c" ); | ||||
|  | ||||
|         IWiredNode | ||||
|             aN = aE.getNode(), | ||||
|             bN = bE.getNode(), | ||||
|             cN = cE.getNode(); | ||||
|  | ||||
|         aN.getNetwork().connect( aN, bN ); | ||||
|         aN.getNetwork().connect( aN, cN ); | ||||
|         aN.getNetwork().connect( bN, cN ); | ||||
|  | ||||
|         aN.getNetwork().disconnect( aN, bN ); | ||||
|  | ||||
|         assertEquals( aN.getNetwork(), bN.getNetwork(), "A's and B's network must be equal" ); | ||||
|         assertEquals( aN.getNetwork(), cN.getNetwork(), "A's and C's network must be equal" ); | ||||
|         assertEquals( Sets.newHashSet( aN, bN, cN ), nodes( aN.getNetwork() ), "A's network should be A, B and C" ); | ||||
|  | ||||
|         assertEquals( Sets.newHashSet( "a", "b", "c" ), aE.allPeripherals().keySet(), "A's peripheral set should be A, B, C" ); | ||||
|         assertEquals( Sets.newHashSet( "a", "b", "c" ), bE.allPeripherals().keySet(), "B's peripheral set should be A, B, C" ); | ||||
|         assertEquals( Sets.newHashSet( "a", "b", "c" ), cE.allPeripherals().keySet(), "C's peripheral set should be A, B, C" ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testDisconnectLeaf() | ||||
|     { | ||||
|         NetworkElement | ||||
|             aE = new NetworkElement( null, null, "a" ), | ||||
|             bE = new NetworkElement( null, null, "b" ), | ||||
|             cE = new NetworkElement( null, null, "c" ); | ||||
|  | ||||
|         IWiredNode | ||||
|             aN = aE.getNode(), | ||||
|             bN = bE.getNode(), | ||||
|             cN = cE.getNode(); | ||||
|  | ||||
|         aN.getNetwork().connect( aN, bN ); | ||||
|         aN.getNetwork().connect( aN, cN ); | ||||
|  | ||||
|         aN.getNetwork().disconnect( aN, bN ); | ||||
|  | ||||
|         assertNotEquals( aN.getNetwork(), bN.getNetwork(), "A's and B's network must not be equal" ); | ||||
|         assertEquals( aN.getNetwork(), cN.getNetwork(), "A's and C's network must be equal" ); | ||||
|         assertEquals( Sets.newHashSet( aN, cN ), nodes( aN.getNetwork() ), "A's network should be A and C" ); | ||||
|         assertEquals( Sets.newHashSet( bN ), nodes( bN.getNetwork() ), "B's network should be B" ); | ||||
|  | ||||
|         assertEquals( Sets.newHashSet( "a", "c" ), aE.allPeripherals().keySet(), "A's peripheral set should be A, C" ); | ||||
|         assertEquals( Sets.newHashSet( "b" ), bE.allPeripherals().keySet(), "B's peripheral set should be B" ); | ||||
|         assertEquals( Sets.newHashSet( "a", "c" ), cE.allPeripherals().keySet(), "C's peripheral set should be A, C" ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testDisconnectSplit() | ||||
|     { | ||||
|         NetworkElement | ||||
|             aE = new NetworkElement( null, null, "a" ), | ||||
|             aaE = new NetworkElement( null, null, "a_" ), | ||||
|             bE = new NetworkElement( null, null, "b" ), | ||||
|             bbE = new NetworkElement( null, null, "b_" ); | ||||
|  | ||||
|         IWiredNode | ||||
|             aN = aE.getNode(), | ||||
|             aaN = aaE.getNode(), | ||||
|             bN = bE.getNode(), | ||||
|             bbN = bbE.getNode(); | ||||
|  | ||||
|         aN.getNetwork().connect( aN, aaN ); | ||||
|         bN.getNetwork().connect( bN, bbN ); | ||||
|  | ||||
|         aN.getNetwork().connect( aN, bN ); | ||||
|  | ||||
|         aN.getNetwork().disconnect( aN, bN ); | ||||
|  | ||||
|         assertNotEquals( aN.getNetwork(), bN.getNetwork(), "A's and B's network must not be equal" ); | ||||
|         assertEquals( aN.getNetwork(), aaN.getNetwork(), "A's and A_'s network must be equal" ); | ||||
|         assertEquals( bN.getNetwork(), bbN.getNetwork(), "B's and B_'s network must be equal" ); | ||||
|  | ||||
|         assertEquals( Sets.newHashSet( aN, aaN ), nodes( aN.getNetwork() ), "A's network should be A and A_" ); | ||||
|         assertEquals( Sets.newHashSet( bN, bbN ), nodes( bN.getNetwork() ), "B's network should be B and B_" ); | ||||
|  | ||||
|         assertEquals( Sets.newHashSet( "a", "a_" ), aE.allPeripherals().keySet(), "A's peripheral set should be A and A_" ); | ||||
|         assertEquals( Sets.newHashSet( "b", "b_" ), bE.allPeripherals().keySet(), "B's peripheral set should be B and B_" ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testRemoveSingle() | ||||
|     { | ||||
|         NetworkElement aE = new NetworkElement( null, null, "a" ); | ||||
|         IWiredNode aN = aE.getNode(); | ||||
|  | ||||
|         IWiredNetwork network = aN.getNetwork(); | ||||
|         assertFalse( aN.remove(), "Cannot remove node from an empty network" ); | ||||
|         assertEquals( network, aN.getNetwork(), "Networks are same before and after" ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testRemoveLeaf() | ||||
|     { | ||||
|         NetworkElement | ||||
|             aE = new NetworkElement( null, null, "a" ), | ||||
|             bE = new NetworkElement( null, null, "b" ), | ||||
|             cE = new NetworkElement( null, null, "c" ); | ||||
|  | ||||
|         IWiredNode | ||||
|             aN = aE.getNode(), | ||||
|             bN = bE.getNode(), | ||||
|             cN = cE.getNode(); | ||||
|  | ||||
|         aN.getNetwork().connect( aN, bN ); | ||||
|         aN.getNetwork().connect( aN, cN ); | ||||
|  | ||||
|         assertTrue( aN.getNetwork().remove( bN ), "Must be able to remove node" ); | ||||
|         assertFalse( aN.getNetwork().remove( bN ), "Cannot remove a second time" ); | ||||
|  | ||||
|         assertNotEquals( aN.getNetwork(), bN.getNetwork(), "A's and B's network must not be equal" ); | ||||
|         assertEquals( aN.getNetwork(), cN.getNetwork(), "A's and C's network must be equal" ); | ||||
|  | ||||
|         assertEquals( Sets.newHashSet( aN, cN ), nodes( aN.getNetwork() ), "A's network should be A and C" ); | ||||
|         assertEquals( Sets.newHashSet( bN ), nodes( bN.getNetwork() ), "B's network should be B" ); | ||||
|  | ||||
|         assertEquals( Sets.newHashSet( "a", "c" ), aE.allPeripherals().keySet(), "A's peripheral set should be A, C" ); | ||||
|         assertEquals( Sets.newHashSet(), bE.allPeripherals().keySet(), "B's peripheral set should be empty" ); | ||||
|         assertEquals( Sets.newHashSet( "a", "c" ), cE.allPeripherals().keySet(), "C's peripheral set should be A, C" ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testRemoveSplit() | ||||
|     { | ||||
|         NetworkElement | ||||
|             aE = new NetworkElement( null, null, "a" ), | ||||
|             aaE = new NetworkElement( null, null, "a_" ), | ||||
|             bE = new NetworkElement( null, null, "b" ), | ||||
|             bbE = new NetworkElement( null, null, "b_" ), | ||||
|             cE = new NetworkElement( null, null, "c" ); | ||||
|  | ||||
|         IWiredNode | ||||
|             aN = aE.getNode(), | ||||
|             aaN = aaE.getNode(), | ||||
|             bN = bE.getNode(), | ||||
|             bbN = bbE.getNode(), | ||||
|             cN = cE.getNode(); | ||||
|  | ||||
|         aN.getNetwork().connect( aN, aaN ); | ||||
|         bN.getNetwork().connect( bN, bbN ); | ||||
|  | ||||
|         cN.getNetwork().connect( aN, cN ); | ||||
|         cN.getNetwork().connect( bN, cN ); | ||||
|  | ||||
|         cN.getNetwork().remove( cN ); | ||||
|  | ||||
|         assertNotEquals( aN.getNetwork(), bN.getNetwork(), "A's and B's network must not be equal" ); | ||||
|         assertEquals( aN.getNetwork(), aaN.getNetwork(), "A's and A_'s network must be equal" ); | ||||
|         assertEquals( bN.getNetwork(), bbN.getNetwork(), "B's and B_'s network must be equal" ); | ||||
|  | ||||
|         assertEquals( Sets.newHashSet( aN, aaN ), nodes( aN.getNetwork() ), "A's network should be A and A_" ); | ||||
|         assertEquals( Sets.newHashSet( bN, bbN ), nodes( bN.getNetwork() ), "B's network should be B and B_" ); | ||||
|         assertEquals( Sets.newHashSet( cN ), nodes( cN.getNetwork() ), "C's network should be C" ); | ||||
|  | ||||
|         assertEquals( Sets.newHashSet( "a", "a_" ), aE.allPeripherals().keySet(), "A's peripheral set should be A and A_" ); | ||||
|         assertEquals( Sets.newHashSet( "b", "b_" ), bE.allPeripherals().keySet(), "B's peripheral set should be B and B_" ); | ||||
|         assertEquals( Sets.newHashSet(), cE.allPeripherals().keySet(), "C's peripheral set should be empty" ); | ||||
|     } | ||||
|  | ||||
|     private static final int BRUTE_SIZE = 16; | ||||
|     private static final int TOGGLE_CONNECTION_TIMES = 5; | ||||
|     private static final int TOGGLE_NODE_TIMES = 5; | ||||
|  | ||||
|     @Test | ||||
|     @Disabled( "Takes a long time to run, mostly for stress testing" ) | ||||
|     public void testLarge() | ||||
|     { | ||||
|         Grid<IWiredNode> grid = new Grid<>( BRUTE_SIZE ); | ||||
|         grid.map( ( existing, pos ) -> new NetworkElement( null, null, "n_" + pos ).getNode() ); | ||||
|  | ||||
|         // Test connecting | ||||
|         { | ||||
|             long start = System.nanoTime(); | ||||
|  | ||||
|             grid.forEach( ( existing, pos ) -> { | ||||
|                 for( Direction facing : DirectionUtil.FACINGS ) | ||||
|                 { | ||||
|                     BlockPos offset = pos.offset( facing ); | ||||
|                     if( offset.getX() > BRUTE_SIZE / 2 == pos.getX() > BRUTE_SIZE / 2 ) | ||||
|                     { | ||||
|                         IWiredNode other = grid.get( offset ); | ||||
|                         if( other != null ) existing.getNetwork().connect( existing, other ); | ||||
|                     } | ||||
|                 } | ||||
|             } ); | ||||
|  | ||||
|             long end = System.nanoTime(); | ||||
|  | ||||
|             System.out.printf( "Connecting %s³ nodes took %s seconds\n", BRUTE_SIZE, (end - start) * 1e-9 ); | ||||
|         } | ||||
|  | ||||
|         // Test toggling | ||||
|         { | ||||
|             IWiredNode left = grid.get( new BlockPos( BRUTE_SIZE / 2, 0, 0 ) ); | ||||
|             IWiredNode right = grid.get( new BlockPos( BRUTE_SIZE / 2 + 1, 0, 0 ) ); | ||||
|             assertNotEquals( left.getNetwork(), right.getNetwork() ); | ||||
|  | ||||
|             long start = System.nanoTime(); | ||||
|             for( int i = 0; i < TOGGLE_CONNECTION_TIMES; i++ ) | ||||
|             { | ||||
|                 left.getNetwork().connect( left, right ); | ||||
|                 left.getNetwork().disconnect( left, right ); | ||||
|             } | ||||
|  | ||||
|             long end = System.nanoTime(); | ||||
|  | ||||
|             System.out.printf( "Toggling connection %s times took %s seconds\n", TOGGLE_CONNECTION_TIMES, (end - start) * 1e-9 ); | ||||
|         } | ||||
|  | ||||
|         { | ||||
|             IWiredNode left = grid.get( new BlockPos( BRUTE_SIZE / 2, 0, 0 ) ); | ||||
|             IWiredNode right = grid.get( new BlockPos( BRUTE_SIZE / 2 + 1, 0, 0 ) ); | ||||
|             IWiredNode centre = new NetworkElement( null, null, "c" ).getNode(); | ||||
|             assertNotEquals( left.getNetwork(), right.getNetwork() ); | ||||
|  | ||||
|             long start = System.nanoTime(); | ||||
|             for( int i = 0; i < TOGGLE_NODE_TIMES; i++ ) | ||||
|             { | ||||
|                 left.getNetwork().connect( left, centre ); | ||||
|                 right.getNetwork().connect( right, centre ); | ||||
|  | ||||
|                 left.getNetwork().remove( centre ); | ||||
|             } | ||||
|  | ||||
|             long end = System.nanoTime(); | ||||
|  | ||||
|             System.out.printf( "Toggling node %s times took %s seconds\n", TOGGLE_NODE_TIMES, (end - start) * 1e-9 ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static final class NetworkElement implements IWiredElement | ||||
|     { | ||||
|         private final World world; | ||||
|         private final Vec3d position; | ||||
|         private final String id; | ||||
|         private final IWiredNode node; | ||||
|         private final Map<String, IPeripheral> localPeripherals = Maps.newHashMap(); | ||||
|         private final Map<String, IPeripheral> remotePeripherals = Maps.newHashMap(); | ||||
|  | ||||
|         private NetworkElement( World world, Vec3d position, String id ) | ||||
|         { | ||||
|             this.world = world; | ||||
|             this.position = position; | ||||
|             this.id = id; | ||||
|             this.node = ComputerCraftAPI.createWiredNodeForElement( this ); | ||||
|             this.addPeripheral( id ); | ||||
|         } | ||||
|  | ||||
|         @Nonnull | ||||
|         @Override | ||||
|         public World getWorld() | ||||
|         { | ||||
|             return world; | ||||
|         } | ||||
|  | ||||
|         @Nonnull | ||||
|         @Override | ||||
|         public Vec3d getPosition() | ||||
|         { | ||||
|             return position; | ||||
|         } | ||||
|  | ||||
|         @Nonnull | ||||
|         @Override | ||||
|         public String getSenderID() | ||||
|         { | ||||
|             return id; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public String toString() | ||||
|         { | ||||
|             return "NetworkElement{" + id + "}"; | ||||
|         } | ||||
|  | ||||
|         @Nonnull | ||||
|         @Override | ||||
|         public IWiredNode getNode() | ||||
|         { | ||||
|             return node; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void networkChanged( @Nonnull IWiredNetworkChange change ) | ||||
|         { | ||||
|             remotePeripherals.keySet().removeAll( change.peripheralsRemoved().keySet() ); | ||||
|             remotePeripherals.putAll( change.peripheralsAdded() ); | ||||
|         } | ||||
|  | ||||
|         public NetworkElement addPeripheral( String name ) | ||||
|         { | ||||
|             localPeripherals.put( name, new NetworkPeripheral() ); | ||||
|             getNode().updatePeripherals( localPeripherals ); | ||||
|             return this; | ||||
|         } | ||||
|  | ||||
|         @Nonnull | ||||
|         public Map<String, IPeripheral> allPeripherals() | ||||
|         { | ||||
|             return remotePeripherals; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static class NetworkPeripheral implements IPeripheral | ||||
|     { | ||||
|         @Nonnull | ||||
|         @Override | ||||
|         public String getType() | ||||
|         { | ||||
|             return "test"; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public boolean equals( @Nullable IPeripheral other ) | ||||
|         { | ||||
|             return this == other; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static class Grid<T> | ||||
|     { | ||||
|         private final int size; | ||||
|         private final T[] box; | ||||
|  | ||||
|         @SuppressWarnings( "unchecked" ) | ||||
|         Grid( int size ) | ||||
|         { | ||||
|             this.size = size; | ||||
|             this.box = (T[]) new Object[size * size * size]; | ||||
|         } | ||||
|  | ||||
|         public T get( BlockPos pos ) | ||||
|         { | ||||
|             int x = pos.getX(), y = pos.getY(), z = pos.getZ(); | ||||
|  | ||||
|             return x >= 0 && x < size && y >= 0 && y < size && z >= 0 && z < size | ||||
|                 ? box[x * size * size + y * size + z] | ||||
|                 : null; | ||||
|         } | ||||
|  | ||||
|         public void forEach( BiConsumer<T, BlockPos> transform ) | ||||
|         { | ||||
|             for( int x = 0; x < size; x++ ) | ||||
|             { | ||||
|                 for( int y = 0; y < size; y++ ) | ||||
|                 { | ||||
|                     for( int z = 0; z < size; z++ ) | ||||
|                     { | ||||
|                         transform.accept( box[x * size * size + y * size + z], new BlockPos( x, y, z ) ); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public void map( BiFunction<T, BlockPos, T> transform ) | ||||
|         { | ||||
|             for( int x = 0; x < size; x++ ) | ||||
|             { | ||||
|                 for( int y = 0; y < size; y++ ) | ||||
|                 { | ||||
|                     for( int z = 0; z < size; z++ ) | ||||
|                     { | ||||
|                         box[x * size * size + y * size + z] = transform.apply( box[x * size * size + y * size + z], new BlockPos( x, y, z ) ); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static Set<WiredNode> nodes( IWiredNetwork network ) | ||||
|     { | ||||
|         return ((WiredNetwork) network).nodes; | ||||
|     } | ||||
|  | ||||
|     private static Set<WiredNode> neighbours( IWiredNode node ) | ||||
|     { | ||||
|         return ((WiredNode) node).neighbours; | ||||
|     } | ||||
| } | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,504 +0,0 @@ | ||||
| --- A very basic test framework for ComputerCraft | ||||
| -- | ||||
| -- Like Busted (http://olivinelabs.com/busted/), but more memorable. | ||||
| -- | ||||
| -- @usage | ||||
| -- describe("something to test", function() | ||||
| --   it("some property", function() | ||||
| --     expect(some_function()):equals("What it should equal") | ||||
| --   end) | ||||
| -- end) | ||||
|  | ||||
| --- Assert an argument to the given function has the specified type. | ||||
| -- | ||||
| -- @tparam string The function's name | ||||
| -- @tparam int    The argument index to this function | ||||
| -- @tparam string The type this argument should have. May be 'value' for any | ||||
| --                non-nil value. | ||||
| -- @param val     The value to check | ||||
| -- @raise If this value doesn't match the expected type. | ||||
| local function check(func, arg, ty, val) | ||||
|     if ty == 'value' then | ||||
|         if val == nil then | ||||
|             error(('%s: bad argument #%d (got nil)'):format(func, arg), 3) | ||||
|         end | ||||
|     elseif type(val) ~= ty then | ||||
|         return error(('%s: bad argument #%d (expected %s, got %s)'):format(func, arg, ty, type(val)), 3) | ||||
|     end | ||||
| end | ||||
|  | ||||
| local active_stubs = {} | ||||
|  | ||||
| --- Stub a global variable with a specific value | ||||
| -- | ||||
| -- @tparam string var The variable to stub | ||||
| -- @param value The value to stub it with | ||||
| local function stub(tbl, var, value) | ||||
|     table.insert(active_stubs, { tbl = tbl, var = var, value = tbl[var] }) | ||||
|     _G[var] = value | ||||
| end | ||||
|  | ||||
|  | ||||
| --- Capture the current global state of the computer | ||||
| local function push_state() | ||||
|     local stubs = active_stubs | ||||
|     active_stubs = {} | ||||
|     return { | ||||
|         term = term.current(), | ||||
|         input = io.input(), | ||||
|         output = io.output(), | ||||
|         stubs = stubs, | ||||
|     } | ||||
| end | ||||
|  | ||||
| --- Restore the global state of the computer to a previous version | ||||
| local function pop_state(state) | ||||
|     for i = #active_stubs, 1, -1 do | ||||
|         local stub = active_stubs[i] | ||||
|         stub.tbl[stub.var] = stub.value | ||||
|     end | ||||
|  | ||||
|     active_stubs = state.stubs | ||||
|  | ||||
|     term.redirect(state.term) | ||||
|     io.input(state.input) | ||||
|     io.output(state.output) | ||||
| end | ||||
|  | ||||
| local error_mt = { __tostring = function(self) return self.message end } | ||||
|  | ||||
| --- Attempt to execute the provided function, gathering a stack trace when it | ||||
| -- errors. | ||||
| -- | ||||
| -- @tparam | ||||
| -- @return[1] true | ||||
| -- @return[2] false | ||||
| -- @return[2] The error object | ||||
| local function try(fn) | ||||
|     if not debug or not debug.traceback then | ||||
|         local ok, err = pcall(fn) | ||||
|         if ok or getmetatable(err) == error_mt then | ||||
|             return ok, err | ||||
|         else | ||||
|             return ok, setmetatable({ message = tostring(err) }, error_mt) | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     local ok, err = xpcall(fn, function(err) | ||||
|         return { message = err, trace = debug.traceback(nil, 2) } | ||||
|     end) | ||||
|  | ||||
|     -- If we succeeded, propagate it | ||||
|     if ok then return ok, err end | ||||
|  | ||||
|     -- Error handling failed for some reason - just return a simpler error | ||||
|     if type(err) ~= "table" then | ||||
|         return ok, setmetatable({ message = tostring(err) }, error_mt) | ||||
|     end | ||||
|  | ||||
|     -- Find the common substring the errors' trace and the current one. Then | ||||
|     -- eliminate it. | ||||
|     local trace = debug.traceback() | ||||
|     for i = 1, #trace do | ||||
|         if trace:sub(-i) ~= err.trace:sub(-i) then | ||||
|             err.trace = err.trace:sub(1, -i) | ||||
|             break | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     -- If we've received a rethrown error, copy | ||||
|     if getmetatable(err.message) == error_mt then | ||||
|         for k, v in pairs(err.message) do err[k] = v end | ||||
|         return ok, err | ||||
|     end | ||||
|  | ||||
|     return ok, setmetatable(err, error_mt) | ||||
| end | ||||
|  | ||||
| --- Fail a test with the given message | ||||
| -- | ||||
| -- @tparam string message The message to fail with | ||||
| -- @raises An error with the given message | ||||
| local function fail(message) | ||||
|     check('fail', 1, 'string', message) | ||||
|     error(setmetatable({ message = message, fail = true }, error_mt)) | ||||
| end | ||||
|  | ||||
| --- Format an object in order to make it more readable | ||||
| -- | ||||
| -- @param value The value to format | ||||
| -- @treturn string The formatted value | ||||
| local function format(value) | ||||
|     -- TODO: Look into something like mbs's pretty printer. | ||||
|     local ok, res = pcall(textutils.serialise, value) | ||||
|     if ok then return res else return tostring(value) end | ||||
| end | ||||
|  | ||||
| local expect_mt = {} | ||||
| expect_mt.__index = expect_mt | ||||
|  | ||||
| --- Assert that this expectation has the provided value | ||||
| -- | ||||
| -- @param value The value to require this expectation to be equal to | ||||
| -- @raises If the values are not equal | ||||
| function expect_mt:equals(value) | ||||
|     if value ~= self.value then | ||||
|         fail(("Expected %s\n but got %s"):format(format(value), format(self.value))) | ||||
|     end | ||||
|  | ||||
|     return self | ||||
| end | ||||
| expect_mt.equal = expect_mt.equals | ||||
| expect_mt.eq = expect_mt.equals | ||||
|  | ||||
| --- Assert that this expectation does not equal the provided value | ||||
| -- | ||||
| -- @param value The value to require this expectation to not be equal to | ||||
| -- @raises If the values are equal | ||||
| function expect_mt:not_equals(value) | ||||
|     if value == self.value then | ||||
|         fail(("Expected any value but %s"):format(format(value))) | ||||
|     end | ||||
|  | ||||
|     return self | ||||
| end | ||||
| expect_mt.not_equal = expect_mt.not_equals | ||||
| expect_mt.ne = expect_mt.not_equals | ||||
|  | ||||
| --- Assert that this expectation has something of the provided type | ||||
| -- | ||||
| -- @tparam string exp_type The type to require this expectation to have | ||||
| -- @raises If it does not have that thpe | ||||
| function expect_mt:type(exp_type) | ||||
|     local actual_type = type(self.value) | ||||
|     if exp_type ~= actual_type then | ||||
|         fail(("Expected value of type %s\n                   got %s"):format(exp_type, actual_type)) | ||||
|     end | ||||
|  | ||||
|     return self | ||||
| end | ||||
|  | ||||
| local function matches(eq, exact, left, right) | ||||
|     if left == right then return true end | ||||
|  | ||||
|     local ty = type(left) | ||||
|     if ty ~= type(right) or ty ~= "table" then return false end | ||||
|  | ||||
|     -- If we've already explored/are exploring the left and right then return | ||||
|     if eq[left] and eq[left][right] then return true end | ||||
|     if not eq[left]  then eq[left] = {[right] = true} else eq[left][right] = true end | ||||
|     if not eq[right] then eq[right] = {[left] = true} else eq[right][left] = true end | ||||
|  | ||||
|     -- Verify all pairs in left are equal to those in right | ||||
|     for k, v in pairs(left) do | ||||
|         if not matches(eq, exact, v, right[k]) then return false end | ||||
|     end | ||||
|  | ||||
|     if exact then | ||||
|         -- And verify all pairs in right are present in left | ||||
|         for k in pairs(right) do | ||||
|             if left[k] == nil then return false end | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     return true | ||||
| end | ||||
|  | ||||
| --- Assert that this expectation is structurally equivalent to | ||||
| -- the provided object. | ||||
| -- | ||||
| -- @param value The value to check for structural equivalence | ||||
| -- @raises If they are not equivalent | ||||
| function expect_mt:same(value) | ||||
|     if not matches({}, true, self.value, value) then | ||||
|         fail(("Expected %s\n but got %s"):format(format(value), format(self.value))) | ||||
|     end | ||||
|  | ||||
|     return self | ||||
| end | ||||
|  | ||||
| --- Assert that this expectation contains all fields mentioned | ||||
| -- in the provided object. | ||||
| -- | ||||
| -- @param value The value to check against | ||||
| -- @raises If this does not match the provided value | ||||
| function expect_mt:matches(value) | ||||
|     if not matches({}, false, value, self.value) then | ||||
|         fail(("Expected %s\nto match %s"):format(format(self.value), format(value))) | ||||
|     end | ||||
|  | ||||
|     return self | ||||
| end | ||||
|  | ||||
| local expect = setmetatable( { | ||||
|     --- Construct an expectation on the error message calling this function | ||||
|     -- produces | ||||
|     -- | ||||
|     -- @tparam fun The function to call | ||||
|     -- @param ... The function arguments | ||||
|     -- @return The new expectation | ||||
|     error = function(fun, ...) | ||||
|         local ok, res = pcall(fun, ...) local _, line = pcall(error, "", 2) | ||||
|         if ok then fail("expected function to error") end | ||||
|         if res:sub(1, #line) == line then | ||||
|             res = res:sub(#line + 1) | ||||
|         elseif res:sub(1, 7) == "pcall: " then | ||||
|             res = res:sub(8) | ||||
|         end | ||||
|         return setmetatable({ value = res }, expect_mt) | ||||
|     end, | ||||
| }, { | ||||
|     --- Construct a new expectation from the provided value | ||||
|     -- | ||||
|     -- @param value The value to apply assertions to | ||||
|     -- @return The new expectation | ||||
|     __call = function(_, value) | ||||
|         return setmetatable({ value = value }, expect_mt) | ||||
|     end | ||||
| }) | ||||
|  | ||||
| --- The stack of "describe"s. | ||||
| local test_stack = { n = 0 } | ||||
|  | ||||
| --- Whether we're now running tests, and so cannot run any more. | ||||
| local tests_locked = false | ||||
|  | ||||
| --- The list of tests that we'll run | ||||
| local test_list, test_map, test_count = { }, { }, 0 | ||||
|  | ||||
| --- Add a new test to our queue. | ||||
| -- | ||||
| -- @param test The descriptor of this test | ||||
| local function do_test(test) | ||||
|     -- Set the name if it doesn't already exist | ||||
|     if not test.name then test.name = table.concat(test_stack, "\0", 1, test_stack.n) end | ||||
|     test_count = test_count + 1 | ||||
|     test_list[test_count] = test | ||||
|     test_map[test.name] = test_count | ||||
| end | ||||
|  | ||||
| --- Get the "friendly" name of this test. | ||||
| -- | ||||
| -- @treturn string This test's friendly name | ||||
| local function test_name(test) return (test.name:gsub("\0", " \26 ")) end | ||||
|  | ||||
| --- Describe something which will be tested, such as a function or situation | ||||
| -- | ||||
| -- @tparam string name   The name of the object to test | ||||
| -- @tparam function body A function which describes the tests for this object. | ||||
| local function describe(name, body) | ||||
|     check('describe', 1, 'string', name) | ||||
|     check('describe', 2, 'function', body) | ||||
|     if tests_locked then error("Cannot describe something while running tests", 2) end | ||||
|  | ||||
|     -- Push our name onto the stack, eval and pop it | ||||
|     local n = test_stack.n + 1 | ||||
|     test_stack[n], test_stack.n = name, n | ||||
|  | ||||
|     local ok, err = try(body) | ||||
|  | ||||
|     -- We count errors as a (failing) test. | ||||
|     if not ok then do_test { error = err } end | ||||
|  | ||||
|     test_stack.n = n - 1 | ||||
| end | ||||
|  | ||||
| --- Declare a single test within a context | ||||
| -- | ||||
| -- @tparam string name   What you are testing | ||||
| -- @tparam function body A function which runs the test, failing if it does | ||||
| --                       the assertions are not met. | ||||
| local function it(name, body) | ||||
|     check('it', 1, 'string', name) | ||||
|     check('it', 2, 'function', body) | ||||
|     if tests_locked then error("Cannot create test while running tests", 2) end | ||||
|  | ||||
|     -- Push name onto the stack | ||||
|     local n = test_stack.n + 1 | ||||
|     test_stack[n], test_stack.n, tests_locked = name, n, true | ||||
|  | ||||
|     do_test { action = body } | ||||
|  | ||||
|     -- Pop the test from the stack | ||||
|     test_stack.n, tests_locked = n - 1, false | ||||
| end | ||||
|  | ||||
| --- Declare a single not-yet-implemented test | ||||
| -- | ||||
| -- @tparam string name   What you really should be testing but aren't | ||||
| local function pending(name) | ||||
|     check('it', 1, 'string', name) | ||||
|     if tests_locked then error("Cannot create test while running tests", 2) end | ||||
|  | ||||
|     local _, loc = pcall(error, "", 3) | ||||
|     loc = loc:gsub(":%s*$", "") | ||||
|  | ||||
|     local n = test_stack.n + 1 | ||||
|     test_stack[n], test_stack.n = name, n | ||||
|     do_test { pending = true, trace = loc } | ||||
|     test_stack.n = n - 1 | ||||
| end | ||||
|  | ||||
| local arg = ... | ||||
| if arg == "--help" or arg == "-h" then | ||||
|     io.write("Usage: mcfly [DIR]\n") | ||||
|     io.write("\n") | ||||
|     io.write("Run tests in the provided DIRectory, or `spec` if not given.") | ||||
|     return | ||||
| end | ||||
|  | ||||
| local root_dir = shell.resolve(arg or "spec") | ||||
| if not fs.isDir(root_dir) then | ||||
|     io.stderr:write(("%q is not a directory.\n"):format(root_dir)) | ||||
|     error() | ||||
| end | ||||
|  | ||||
| do | ||||
|     -- Load in the tests from all our files | ||||
|     local env = setmetatable({}, { __index = _ENV }) | ||||
|  | ||||
|     local function set_env(tbl) | ||||
|         for k in pairs(env) do env[k] = nil end | ||||
|         for k, v in pairs(tbl) do env[k] = v end | ||||
|     end | ||||
|  | ||||
|     -- When declaring tests, you shouldn't be able to use test methods | ||||
|     set_env { describe = describe, it = it, pending = pending } | ||||
|  | ||||
|     local suffix = "_spec.lua" | ||||
|     local function run_in(sub_dir) | ||||
|         for _, name in ipairs(fs.list(sub_dir)) do | ||||
|             local file = fs.combine(sub_dir, name) | ||||
|             if fs.isDir(file) then | ||||
|                 run_in(file) | ||||
|             elseif file:sub(-#suffix) == suffix then | ||||
|                 local fun, err = loadfile(file, env) | ||||
|                 if not fun then | ||||
|                     do_test { name = file:sub(#root_dir + 2), error = { message = err } } | ||||
|                 else | ||||
|                     local ok, err = try(fun) | ||||
|                     if not ok then do_test { name = file:sub(#root_dir + 2), error = err } end | ||||
|                 end | ||||
|             end | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     run_in(root_dir) | ||||
|  | ||||
|     -- When running tests, you shouldn't be able to declare new ones. | ||||
|     set_env { expect = expect, fail = fail, stub = stub } | ||||
| end | ||||
|  | ||||
| -- Error if we've found no tests | ||||
| if test_count == 0 then | ||||
|     io.stderr:write(("Could not find any tests in %q\n"):format(root_dir)) | ||||
|     error() | ||||
| end | ||||
|  | ||||
| -- The results of each test, as well as how many passed and the count. | ||||
| local test_results, test_status, tests_run = { n = 0 }, {}, 0 | ||||
|  | ||||
| -- All possible test statuses | ||||
| local statuses = { | ||||
|     pass    = { desc = "Pass",    col = colours.green,   dot = "\7"   }, -- Circle | ||||
|     fail    = { desc = "Failed",  col = colours.red,     dot = "\4"   }, -- Diamond | ||||
|     error   = { desc = "Error",   col = colours.magenta, dot = "\4"   }, | ||||
|     pending = { desc = "Pending", col = colours.yellow,  dot = "\186" }, -- Hollow circle | ||||
| } | ||||
|  | ||||
| -- Set up each test status count. | ||||
| for k in pairs(statuses) do test_status[k] = 0 end | ||||
|  | ||||
| --- Do the actual running of our test | ||||
| local function do_run(test) | ||||
|     -- If we're a pre-computed test, determine our status message. Otherwise, | ||||
|     -- skip. | ||||
|     local status, err | ||||
|     if test.pending then | ||||
|         status = "pending" | ||||
|     elseif test.error then | ||||
|         err = test.error | ||||
|         status = "error" | ||||
|     elseif test.action then | ||||
|         local state = push_state() | ||||
|  | ||||
|         local ok | ||||
|         ok, err = try(test.action) | ||||
|         status = ok and "pass" or (err.fail and "fail" or "error") | ||||
|  | ||||
|         pop_state(state) | ||||
|     end | ||||
|  | ||||
|     -- If we've a boolean status, then convert it into a string | ||||
|     if status == true then status = "pass" | ||||
|     elseif status == false then status = err.fail and "fail" or "error" | ||||
|     end | ||||
|  | ||||
|     tests_run = tests_run + 1 | ||||
|     test_status[status] = test_status[status] + 1 | ||||
|     test_results[tests_run] = { | ||||
|         status = status, name = test.name, | ||||
|         message = test.message or err and err.message, | ||||
|         trace = test.trace or err and err.trace, | ||||
|     } | ||||
|  | ||||
|     -- If we're running under howlci, then log some info. | ||||
|     if howlci then howlci.status(status, test_name(test)) end | ||||
|     if cct_test then cct_test.submit(test_results[tests_run]) end | ||||
|  | ||||
|     -- Print our progress dot | ||||
|     local data = statuses[status] | ||||
|     term.setTextColour(data.col) io.write(data.dot) | ||||
|     term.setTextColour(colours.white) | ||||
| end | ||||
|  | ||||
| -- Loop over all our tests, running them as required. | ||||
| if cct_test then | ||||
|     -- If we're within a cct_test environment, then submit them and wait on tests | ||||
|     -- to be run. | ||||
|     cct_test.start(test_map) | ||||
|     while true do | ||||
|         local _, name = os.pullEvent("cct_test_run") | ||||
|         if not name then break end | ||||
|         do_run(test_list[test_map[name]]) | ||||
|     end | ||||
| else | ||||
|     for _, test in pairs(test_list) do do_run(test) end | ||||
| end | ||||
|  | ||||
| -- Otherwise, display the results of each failure | ||||
| io.write("\n\n") | ||||
| for i = 1, tests_run do | ||||
|     local test = test_results[i] | ||||
|     if test.status ~= "pass" then | ||||
|         local status_data = statuses[test.status] | ||||
|  | ||||
|         term.setTextColour(status_data.col) | ||||
|         io.write(status_data.desc) | ||||
|         term.setTextColour(colours.white) | ||||
|         io.write(" \26 " .. test_name(test) .. "\n") | ||||
|  | ||||
|         if test.message then | ||||
|             io.write("  " .. test.message:gsub("\n", "\n  ") .. "\n") | ||||
|         end | ||||
|  | ||||
|         if test.trace then | ||||
|             term.setTextColour(colours.lightGrey) | ||||
|             io.write("  " .. test.trace:gsub("\n", "\n  ") .. "\n") | ||||
|         end | ||||
|  | ||||
|         io.write("\n") | ||||
|     end | ||||
| end | ||||
|  | ||||
| -- And some summary statistics | ||||
| local actual_count = tests_run - test_status.pending | ||||
| local info = ("Ran %s test(s), of which %s passed (%g%%).") | ||||
|     :format(actual_count, test_status.pass, (test_status.pass / actual_count) * 100) | ||||
|  | ||||
| if test_status.pending > 0 then | ||||
|     info = info .. (" Skipped %d pending test(s)."):format(test_status.pending) | ||||
| end | ||||
|  | ||||
| term.setTextColour(colours.white) io.write(info .. "\n") | ||||
| if howlci then howlci.log("debug", info) sleep(3) end | ||||
| @@ -1,92 +0,0 @@ | ||||
| describe("The colors library", function() | ||||
|     describe("colors.combine", function() | ||||
|         it("validates arguments", function() | ||||
|             expect.error(colors.combine, 1, nil):eq("bad argument #2 (expected number, got nil)") | ||||
|             expect.error(colors.combine, 1, 1, nil):eq("bad argument #3 (expected number, got nil)") | ||||
|         end) | ||||
|  | ||||
|         it("combines colours", function() | ||||
|             expect(colors.combine()):eq(0) | ||||
|             expect(colors.combine(colors.red, colors.brown, colors.green)):eq(0x7000) | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("colors.subtract", function() | ||||
|         it("validates arguments", function() | ||||
|             expect.error(colors.subtract, nil):eq("bad argument #1 (expected number, got nil)") | ||||
|             expect.error(colors.subtract, 1, nil):eq("bad argument #2 (expected number, got nil)") | ||||
|             expect.error(colors.subtract, 1, 1, nil):eq("bad argument #3 (expected number, got nil)") | ||||
|         end) | ||||
|  | ||||
|         it("subtracts colours", function() | ||||
|             expect(colors.subtract(0x7000, colors.green)):equals(0x5000) | ||||
|             expect(colors.subtract(0x5000, colors.red)):equals(0x1000) | ||||
|         end) | ||||
|         it("does nothing when color is not present", function() | ||||
|             expect(colors.subtract(0x1000, colors.red)):equals(0x1000) | ||||
|         end) | ||||
|         it("accepts multiple arguments", function() | ||||
|             expect(colors.subtract(0x7000, colors.red, colors.green, colors.red)):equals(0x1000) | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("colors.test", function() | ||||
|         it("validates arguments", function() | ||||
|             expect.error(colors.test, nil):eq("bad argument #1 (expected number, got nil)") | ||||
|             expect.error(colors.test, 1, nil):eq("bad argument #2 (expected number, got nil)") | ||||
|         end) | ||||
|  | ||||
|         it("returns true when present", function() | ||||
|             expect(colors.test(0x7000, colors.green)):equals(true) | ||||
|         end) | ||||
|         it("returns false when absent", function() | ||||
|             expect(colors.test(0x5000, colors.green)):equals(false) | ||||
|         end) | ||||
|         it("allows multiple colors", function() | ||||
|             expect(colors.test(0x7000, 0x5000)):equals(true) | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("colors.packRGB", function() | ||||
|         it("validates arguments", function() | ||||
|             expect.error(colors.packRGB, nil):eq("bad argument #1 (expected number, got nil)") | ||||
|             expect.error(colors.packRGB, 1, nil):eq("bad argument #2 (expected number, got nil)") | ||||
|             expect.error(colors.packRGB, 1, 1, nil):eq("bad argument #3 (expected number, got nil)") | ||||
|         end) | ||||
|  | ||||
|         it("packs colours", function() | ||||
|             expect(colors.packRGB(0.3, 0.5, 0.6)):equals(0x4c7f99) | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("colors.unpackRGB", function() | ||||
|         it("validates arguments", function() | ||||
|             expect.error(colors.unpackRGB, nil):eq("bad argument #1 (expected number, got nil)") | ||||
|         end) | ||||
|  | ||||
|         it("unpacks colours", function() | ||||
|             expect({ colors.unpackRGB(0x4c7f99) }):same { 0x4c / 0xFF, 0x7f / 0xFF, 0.6 } | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     it("colors.rgb8", function() | ||||
|         expect(colors.rgb8(0.3, 0.5, 0.6)):equals(0x4c7f99) | ||||
|         expect({ colors.rgb8(0x4c7f99) }):same { 0x4c / 0xFF, 0x7f / 0xFF, 0.6 } | ||||
|     end) | ||||
|  | ||||
|     describe("colors.toBlit", function() | ||||
|         it("validates arguments", function() | ||||
|             expect.error(colors.toBlit, nil):eq("bad argument #1 (expected number, got nil)") | ||||
|         end) | ||||
|  | ||||
|         it("converts all colors", function() | ||||
|             for i = 0, 15 do | ||||
|                 expect(colors.toBlit(2 ^ i)):eq(string.format("%x", i)) | ||||
|             end | ||||
|         end) | ||||
|  | ||||
|         it("floors colors", function() | ||||
|             expect(colors.toBlit(16385)):eq("e") | ||||
|         end) | ||||
|     end) | ||||
| end) | ||||
| @@ -1,222 +0,0 @@ | ||||
| describe("The fs library", function() | ||||
|     describe("fs.complete", function() | ||||
|         it("validates arguments", function() | ||||
|             fs.complete("", "") | ||||
|             fs.complete("", "", true) | ||||
|             fs.complete("", "", nil, true) | ||||
|  | ||||
|             expect.error(fs.complete, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|             expect.error(fs.complete, "", nil):eq("bad argument #2 (expected string, got nil)") | ||||
|             expect.error(fs.complete, "", "", 1):eq("bad argument #3 (expected boolean, got number)") | ||||
|             expect.error(fs.complete, "", "", true, 1):eq("bad argument #4 (expected boolean, got number)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("fs.isDriveRoot", function() | ||||
|         it("validates arguments", function() | ||||
|             fs.isDriveRoot("") | ||||
|  | ||||
|             expect.error(fs.isDriveRoot, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|         end) | ||||
|  | ||||
|         it("correctly identifies drive roots", function() | ||||
|             expect(fs.isDriveRoot("/rom")):eq(true) | ||||
|             expect(fs.isDriveRoot("/")):eq(true) | ||||
|             expect(fs.isDriveRoot("/rom/startup.lua")):eq(false) | ||||
|             expect(fs.isDriveRoot("/rom/programs/delete.lua")):eq(false) | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("fs.list", function() | ||||
|         it("fails on files", function() | ||||
|             expect.error(fs.list, "rom/startup.lua"):eq("/rom/startup.lua: Not a directory") | ||||
|             expect.error(fs.list, "startup.lua"):eq("/startup.lua: Not a directory") | ||||
|         end) | ||||
|  | ||||
|         it("fails on non-existent nodes", function() | ||||
|             expect.error(fs.list, "rom/x"):eq("/rom/x: Not a directory") | ||||
|             expect.error(fs.list, "x"):eq("/x: Not a directory") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("fs.combine", function() | ||||
|         it("removes . and ..", function() | ||||
|             expect(fs.combine("./a/b")):eq("a/b") | ||||
|             expect(fs.combine("a/b", "../c")):eq("a/c") | ||||
|             expect(fs.combine("a", "../c")):eq("c") | ||||
|             expect(fs.combine("a", "../../c")):eq("../c") | ||||
|         end) | ||||
|  | ||||
|         it("combines empty paths", function() | ||||
|             expect(fs.combine("a")):eq("a") | ||||
|             expect(fs.combine("a", "")):eq("a") | ||||
|             expect(fs.combine("", "a")):eq("a") | ||||
|             expect(fs.combine("a", "", "b", "c")):eq("a/b/c") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("fs.getSize", function() | ||||
|         it("fails on non-existent nodes", function() | ||||
|             expect.error(fs.getSize, "rom/x"):eq("/rom/x: No such file") | ||||
|             expect.error(fs.getSize, "x"):eq("/x: No such file") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("fs.open", function() | ||||
|         describe("reading", function() | ||||
|             it("fails on directories", function() | ||||
|                 expect { fs.open("rom", "r") }:same { nil, "/rom: No such file" } | ||||
|                 expect { fs.open("", "r") }:same { nil, "/: No such file" } | ||||
|             end) | ||||
|  | ||||
|             it("fails on non-existent nodes", function() | ||||
|                 expect { fs.open("rom/x", "r") }:same { nil, "/rom/x: No such file" } | ||||
|                 expect { fs.open("x", "r") }:same { nil, "/x: No such file" } | ||||
|             end) | ||||
|  | ||||
|             it("errors when closing twice", function() | ||||
|                 local handle = fs.open("rom/startup.lua", "r") | ||||
|                 handle.close() | ||||
|                 expect.error(handle.close):eq("attempt to use a closed file") | ||||
|             end) | ||||
|         end) | ||||
|  | ||||
|         describe("reading in binary mode", function() | ||||
|             it("errors when closing twice", function() | ||||
|                 local handle = fs.open("rom/startup.lua", "rb") | ||||
|                 handle.close() | ||||
|                 expect.error(handle.close):eq("attempt to use a closed file") | ||||
|             end) | ||||
|         end) | ||||
|  | ||||
|         describe("writing", function() | ||||
|             it("fails on directories", function() | ||||
|                 expect { fs.open("", "w") }:same { nil, "/: Cannot write to directory" } | ||||
|             end) | ||||
|  | ||||
|             it("fails on read-only mounts", function() | ||||
|                 expect { fs.open("rom/x", "w") }:same { nil, "/rom/x: Access denied" } | ||||
|             end) | ||||
|  | ||||
|             it("errors when closing twice", function() | ||||
|                 local handle = fs.open("test-files/out.txt", "w") | ||||
|                 handle.close() | ||||
|                 expect.error(handle.close):eq("attempt to use a closed file") | ||||
|             end) | ||||
|  | ||||
|             it("fails gracefully when opening 'CON' on Windows", function() | ||||
|                 local ok, err = fs.open("test-files/con", "w") | ||||
|                 if ok then fs.delete("test-files/con") return end | ||||
|  | ||||
|                 -- On my Windows/Java version the message appears to be "Incorrect function.". It may not be | ||||
|                 -- consistent though, and honestly doesn't matter too much. | ||||
|                 expect(err):str_match("^/test%-files/con: .*") | ||||
|             end) | ||||
|         end) | ||||
|  | ||||
|         describe("writing in binary mode", function() | ||||
|             it("errors when closing twice", function() | ||||
|                 local handle = fs.open("test-files/out.txt", "wb") | ||||
|                 handle.close() | ||||
|                 expect.error(handle.close):eq("attempt to use a closed file") | ||||
|             end) | ||||
|         end) | ||||
|  | ||||
|         describe("appending", function() | ||||
|             it("fails on directories", function() | ||||
|                 expect { fs.open("", "a") }:same { nil, "/: Cannot write to directory" } | ||||
|             end) | ||||
|  | ||||
|             it("fails on read-only mounts", function() | ||||
|                 expect { fs.open("rom/x", "a") }:same { nil, "/rom/x: Access denied" } | ||||
|             end) | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("fs.makeDir", function() | ||||
|         it("fails on files", function() | ||||
|             expect.error(fs.makeDir, "startup.lua"):eq("/startup.lua: File exists") | ||||
|         end) | ||||
|  | ||||
|         it("fails on read-only mounts", function() | ||||
|             expect.error(fs.makeDir, "rom/x"):eq("/rom/x: Access denied") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("fs.delete", function() | ||||
|         it("fails on read-only mounts", function() | ||||
|             expect.error(fs.delete, "rom/x"):eq("/rom/x: Access denied") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("fs.copy", function() | ||||
|         it("fails on read-only mounts", function() | ||||
|             expect.error(fs.copy, "rom", "rom/startup"):eq("/rom/startup: Access denied") | ||||
|         end) | ||||
|  | ||||
|         it("fails to copy a folder inside itself", function() | ||||
|             fs.makeDir("some-folder") | ||||
|             expect.error(fs.copy, "some-folder", "some-folder/x"):eq("/some-folder: Can't copy a directory inside itself") | ||||
|             expect.error(fs.copy, "some-folder", "Some-Folder/x"):eq("/some-folder: Can't copy a directory inside itself") | ||||
|         end) | ||||
|  | ||||
|         it("copies folders", function() | ||||
|             fs.delete("some-folder") | ||||
|             fs.delete("another-folder") | ||||
|  | ||||
|             fs.makeDir("some-folder") | ||||
|             fs.copy("some-folder", "another-folder") | ||||
|             expect(fs.isDir("another-folder")):eq(true) | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("fs.move", function() | ||||
|         it("fails on read-only mounts", function() | ||||
|             expect.error(fs.move, "rom", "rom/move"):eq("Access denied") | ||||
|             expect.error(fs.move, "test-files", "rom/move"):eq("Access denied") | ||||
|             expect.error(fs.move, "rom", "test-files"):eq("Access denied") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("fs.getCapacity", function() | ||||
|         it("returns nil on read-only mounts", function() | ||||
|             expect(fs.getCapacity("rom")):eq(nil) | ||||
|         end) | ||||
|  | ||||
|         it("returns the capacity on the root mount", function() | ||||
|             expect(fs.getCapacity("")):eq(10000000) | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("fs.attributes", function() | ||||
|         it("errors on non-existent files", function() | ||||
|             expect.error(fs.attributes, "xuxu_nao_existe"):eq("/xuxu_nao_existe: No such file") | ||||
|         end) | ||||
|  | ||||
|         it("returns information about read-only mounts", function() | ||||
|             expect(fs.attributes("rom")):matches { isDir = true, size = 0, isReadOnly = true } | ||||
|         end) | ||||
|  | ||||
|         it("returns information about files", function() | ||||
|             local now = os.epoch("utc") | ||||
|  | ||||
|             fs.delete("/tmp/basic-file") | ||||
|             local h = fs.open("/tmp/basic-file", "w") | ||||
|             h.write("A reasonably sized string") | ||||
|             h.close() | ||||
|  | ||||
|             local attributes = fs.attributes("tmp/basic-file") | ||||
|             expect(attributes):matches { isDir = false, size = 25, isReadOnly = false } | ||||
|  | ||||
|             if attributes.created - now >= 1000 then | ||||
|                 fail(("Expected created time (%d) to be within 1000ms of now (%d"):format(attributes.created, now)) | ||||
|             end | ||||
|  | ||||
|             if attributes.modified - now >= 1000 then | ||||
|                 fail(("Expected modified time (%d) to be within 1000ms of now (%d"):format(attributes.modified, now)) | ||||
|             end | ||||
|  | ||||
|             expect(attributes.modification):eq(attributes.modified) | ||||
|         end) | ||||
|     end) | ||||
| end) | ||||
| @@ -1,15 +0,0 @@ | ||||
| describe("The gps library", function() | ||||
|     describe("gps.locate", function() | ||||
|         it("validates arguments", function() | ||||
|             stub(_G, "commands", { getBlockPosition = function() | ||||
|             end }) | ||||
|  | ||||
|             gps.locate() | ||||
|             gps.locate(1) | ||||
|             gps.locate(1, true) | ||||
|  | ||||
|             expect.error(gps.locate, ""):eq("bad argument #1 (expected number, got string)") | ||||
|             expect.error(gps.locate, 1, ""):eq("bad argument #2 (expected boolean, got string)") | ||||
|         end) | ||||
|     end) | ||||
| end) | ||||
| @@ -1,22 +0,0 @@ | ||||
| describe("The help library", function() | ||||
|     describe("help.setPath", function() | ||||
|         it("validates arguments", function() | ||||
|             help.setPath(help.path()) | ||||
|             expect.error(help.setPath, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("help.lookup", function() | ||||
|         it("validates arguments", function() | ||||
|             help.lookup("") | ||||
|             expect.error(help.lookup, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("help.completeTopic", function() | ||||
|         it("validates arguments", function() | ||||
|             help.completeTopic("") | ||||
|             expect.error(help.completeTopic, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|         end) | ||||
|     end) | ||||
| end) | ||||
| @@ -1,19 +0,0 @@ | ||||
| describe("The http library", function() | ||||
|     describe("http.checkURL", function() | ||||
|         it("accepts well formed domains", function() | ||||
|             expect({ http.checkURL("https://google.com")}):same({ true }) | ||||
|         end) | ||||
|  | ||||
|         it("rejects malformed URLs", function() | ||||
|             expect({ http.checkURL("google.com")}):same({ false, "Must specify http or https" }) | ||||
|             expect({ http.checkURL("https:google.com")}):same({ false, "URL malformed" }) | ||||
|             expect({ http.checkURL("https:/google.com")}):same({ false, "URL malformed" }) | ||||
|             expect({ http.checkURL("wss://google.com")}):same({ false, "Invalid protocol 'wss'" }) | ||||
|         end) | ||||
|  | ||||
|         it("rejects local domains", function() | ||||
|             expect({ http.checkURL("http://localhost")}):same({ false, "Domain not permitted" }) | ||||
|             expect({ http.checkURL("http://127.0.0.1")}):same({ false, "Domain not permitted" }) | ||||
|         end) | ||||
|     end) | ||||
| end) | ||||
| @@ -1,76 +0,0 @@ | ||||
| --- Tests the io library is (mostly) consistent with PUC Lua. | ||||
| -- | ||||
| -- These tests are based on the tests for Lua 5.1 | ||||
|  | ||||
| describe("The io library", function() | ||||
|     it("io.input on a handle returns that handle", function() | ||||
|         expect(io.input(io.stdin)):equals(io.stdin) | ||||
|     end) | ||||
|  | ||||
|     it("io.output on a handle returns that handle", function() | ||||
|         expect(io.output(io.stdout)):equals(io.stdout) | ||||
|     end) | ||||
|  | ||||
|     describe("io.type", function() | ||||
|         it("returns file on handles", function() | ||||
|             local handle = io.input() | ||||
|             expect(handle):type("table") | ||||
|             expect(io.type(handle)):equals("file") | ||||
|         end) | ||||
|  | ||||
|         it("returns nil on values", function() | ||||
|             expect(io.type(8)):equals(nil) | ||||
|         end) | ||||
|  | ||||
|         it("returns nil on tables", function() | ||||
|             expect(io.type(setmetatable({}, {}))):equals(nil) | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("io.lines", function() | ||||
|         it("validates arguments", function() | ||||
|             io.lines(nil) | ||||
|             expect.error(io.lines, ""):eq("/: No such file") | ||||
|             expect.error(io.lines, false):eq("bad argument #1 (expected string, got boolean)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("io.open", function() | ||||
|         it("validates arguments", function() | ||||
|             io.open("") | ||||
|             io.open("", "r") | ||||
|  | ||||
|             expect.error(io.open, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|             expect.error(io.open, "", false):eq("bad argument #2 (expected string, got boolean)") | ||||
|         end) | ||||
|  | ||||
|         it("returns an error message on non-existent files", function() | ||||
|             local a, b = io.open('xuxu_nao_existe') | ||||
|             expect(a):equals(nil) | ||||
|             expect(b):type("string") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     pending("io.output allows redirecting and seeking", function() | ||||
|         fs.delete("/tmp/io_spec.txt") | ||||
|  | ||||
|         io.output("/tmp/io_spec.txt") | ||||
|  | ||||
|         expect(io.output()):not_equals(io.stdout) | ||||
|  | ||||
|         expect(io.output():seek()):equal(0) | ||||
|         assert(io.write("alo alo")) | ||||
|         expect(io.output():seek()):equal(#("alo alo")) | ||||
|         expect(io.output():seek("cur", -3)):equal(#("alo alo") - 3) | ||||
|         assert(io.write("joao")) | ||||
|         expect(io.output():seek("end"):equal(#("alo joao"))) | ||||
|  | ||||
|         expect(io.output():seek("set")):equal(0) | ||||
|  | ||||
|         assert(io.write('"<22>lo"', "{a}\n", "second line\n", "third line \n")) | ||||
|         assert(io.write('<EFBFBD>fourth_line')) | ||||
|  | ||||
|         io.output(io.stdout) | ||||
|         expect(io.output()):equals(io.stdout) | ||||
|     end) | ||||
| end) | ||||
| @@ -1,8 +0,0 @@ | ||||
| describe("The keys library", function() | ||||
|     describe("keys.getName", function() | ||||
|         it("validates arguments", function() | ||||
|             keys.getName(1) | ||||
|             expect.error(keys.getName, nil):eq("bad argument #1 (expected number, got nil)") | ||||
|         end) | ||||
|     end) | ||||
| end) | ||||
| @@ -1,133 +0,0 @@ | ||||
| describe("The os library", function() | ||||
|     describe("os.date and os.time", function() | ||||
|         it("round trips correctly", function() | ||||
|             local t = math.floor(os.epoch("local") / 1000) | ||||
|             local T = os.date("*t", t) | ||||
|  | ||||
|             expect(os.time(T)):eq(t) | ||||
|         end) | ||||
|  | ||||
|         it("dst field is guessed", function() | ||||
|             local T = os.date("*t") | ||||
|             local t = os.time(T) | ||||
|             expect(T.isdst):type("boolean") | ||||
|             T.isdst = nil | ||||
|             expect(os.time(T)):eq(t) -- if isdst is absent uses correct default | ||||
|         end) | ||||
|  | ||||
|         it("has 365 days in a year", function() | ||||
|             local T = os.date("*t") | ||||
|             local t = os.time(T) | ||||
|             T.year = T.year - 1 | ||||
|             local t1 = os.time(T) | ||||
|             local delta = (t - t1) / (24 * 3600) - 365 | ||||
|             -- allow for leap years | ||||
|             assert(math.abs(delta) < 2, ("expected abs(%d )< 2"):format(delta)) | ||||
|         end) | ||||
|  | ||||
|         it("os.date uses local timezone", function() | ||||
|             local epoch = os.epoch("local") / 1000 | ||||
|             local date = os.time(os.date("*t")) | ||||
|             assert(date - epoch <= 2, ("expected %d - %d <= 2, but not the case"):format(date, epoch)) | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("os.date", function() | ||||
|         it("formats as expected", function() | ||||
|             -- From the PUC Lua tests, hence the weird style | ||||
|             local t = os.epoch("local") | ||||
|             local T = os.date("*t", t) | ||||
|  | ||||
|             _G.T = T | ||||
|             loadstring(os.date([[assert(T.year==%Y and T.month==%m and T.day==%d and | ||||
|               T.hour==%H and T.min==%M and T.sec==%S and | ||||
|               T.wday==%w+1 and T.yday==%j and type(T.isdst) == 'boolean')]], t))() | ||||
|  | ||||
|             T = os.date("!*t", t) | ||||
|             _G.T = T | ||||
|             loadstring(os.date([[!assert(T.year==%Y and T.month==%m and T.day==%d and | ||||
|               T.hour==%H and T.min==%M and T.sec==%S and | ||||
|               T.wday==%w+1 and T.yday==%j and type(T.isdst) == 'boolean')]], t))() | ||||
|         end) | ||||
|  | ||||
|         describe("produces output consistent with PUC Lua", function() | ||||
|             -- Create a separate test for each code, just so it's easier to see what's broken | ||||
|             local t1 = os.time { year = 2000, month = 10, day = 1, hour = 23, min = 12, sec = 17 } | ||||
|             local function exp_code(code, value) | ||||
|                 it(("for code '%s'"):format(code), function() | ||||
|                     expect(os.date(code, t1)):eq(value) | ||||
|                 end) | ||||
|             end | ||||
|  | ||||
|             exp_code("%a", "Sun") | ||||
|             exp_code("%A", "Sunday") | ||||
|             exp_code("%b", "Oct") | ||||
|             exp_code("%B", "October") | ||||
|             exp_code("%c", "Sun Oct  1 23:12:17 2000") | ||||
|             exp_code("%C", "20") | ||||
|             exp_code("%d", "01") | ||||
|             exp_code("%D", "10/01/00") | ||||
|             exp_code("%e", " 1") | ||||
|             exp_code("%F", "2000-10-01") | ||||
|             exp_code("%g", "00") | ||||
|             exp_code("%G", "2000") | ||||
|             exp_code("%h", "Oct") | ||||
|             exp_code("%H", "23") | ||||
|             exp_code("%I", "11") | ||||
|             exp_code("%j", "275") | ||||
|             exp_code("%m", "10") | ||||
|             exp_code("%M", "12") | ||||
|             exp_code("%n", "\n") | ||||
|             exp_code("%p", "PM") | ||||
|             exp_code("%r", "11:12:17 PM") | ||||
|             exp_code("%R", "23:12") | ||||
|             exp_code("%S", "17") | ||||
|             exp_code("%t", "\t") | ||||
|             exp_code("%T", "23:12:17") | ||||
|             exp_code("%u", "7") | ||||
|             exp_code("%U", "40") | ||||
|             exp_code("%V", "39") | ||||
|             exp_code("%w", "0") | ||||
|             exp_code("%W", "39") | ||||
|             exp_code("%x", "10/01/00") | ||||
|             exp_code("%X", "23:12:17") | ||||
|             exp_code("%y", "00") | ||||
|             exp_code("%Y", "2000") | ||||
|             exp_code("%%", "%") | ||||
|  | ||||
|             it("zones are numbers", function() | ||||
|                 local zone = os.date("%z", t1) | ||||
|                 if not zone:match("^[+-]%d%d%d%d$") then | ||||
|                     error("Invalid zone: " .. zone) | ||||
|                 end | ||||
|             end) | ||||
|  | ||||
|             it("zones id is made of letters", function() | ||||
|                 local zone = os.date("%Z", t1) | ||||
|                 if not zone:match("^%a%a+$") then | ||||
|                     error("Non letter character in zone: " .. zone) | ||||
|                 end | ||||
|             end) | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("os.time", function() | ||||
|         it("maps directly to seconds", function() | ||||
|             local t1 = os.time { year = 2000, month = 10, day = 1, hour = 23, min = 12, sec = 17 } | ||||
|             local t2 = os.time { year = 2000, month = 10, day = 1, hour = 23, min = 10, sec = 19 } | ||||
|             expect(t1 - t2):eq(60 * 2 - 2) | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("os.loadAPI", function() | ||||
|         it("validates arguments", function() | ||||
|             expect.error(os.loadAPI, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("os.unloadAPI", function() | ||||
|         it("validates arguments", function() | ||||
|             expect.error(os.loadAPI, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|         end) | ||||
|     end) | ||||
| end) | ||||
| @@ -1,176 +0,0 @@ | ||||
| local with_window = require "test_helpers".with_window | ||||
|  | ||||
| describe("The paintutils library", function() | ||||
|     -- Verifies that a window's lines are equal to the given table of blit | ||||
|     -- strings ({{"text", "fg", "bg"}, {"text", "fg", "bg"}...}) | ||||
|     local function window_eq(w, state) | ||||
|         -- Verification of the size isn't really important in the tests, but | ||||
|         -- better safe than sorry. | ||||
|         local _, height = w.getSize() | ||||
|         expect(#state):eq(height) | ||||
|  | ||||
|         for line = 1, height do | ||||
|             expect({ w.getLine(line) }):same(state[line]) | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     describe("paintutils.parseImage", function() | ||||
|         it("validates arguments", function() | ||||
|             paintutils.parseImage("") | ||||
|             expect.error(paintutils.parseImage, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("paintutils.loadImage", function() | ||||
|         it("validates arguments", function() | ||||
|             expect.error(paintutils.loadImage, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("paintutils.drawPixel", function() | ||||
|         it("validates arguments", function() | ||||
|             expect.error(paintutils.drawPixel, nil):eq("bad argument #1 (expected number, got nil)") | ||||
|             expect.error(paintutils.drawPixel, 1, nil):eq("bad argument #2 (expected number, got nil)") | ||||
|             expect.error(paintutils.drawPixel, 1, 1, false):eq("bad argument #3 (expected number, got boolean)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("paintutils.drawLine", function() | ||||
|         it("validates arguments", function() | ||||
|             expect.error(paintutils.drawLine, nil):eq("bad argument #1 (expected number, got nil)") | ||||
|             expect.error(paintutils.drawLine, 1, nil):eq("bad argument #2 (expected number, got nil)") | ||||
|             expect.error(paintutils.drawLine, 1, 1, nil):eq("bad argument #3 (expected number, got nil)") | ||||
|             expect.error(paintutils.drawLine, 1, 1, 1, nil):eq("bad argument #4 (expected number, got nil)") | ||||
|             expect.error(paintutils.drawLine, 1, 1, 1, 1, false):eq("bad argument #5 (expected number, got boolean)") | ||||
|         end) | ||||
|  | ||||
|         it("draws a line going across with custom colour", function() | ||||
|             local w = with_window(3, 2, function() | ||||
|                 paintutils.drawLine(1, 1, 3, 1, colours.red) | ||||
|             end) | ||||
|  | ||||
|             window_eq(w, { | ||||
|                 { "   ", "000", "eee" }, | ||||
|                 { "   ", "000", "fff" }, | ||||
|             }) | ||||
|         end) | ||||
|  | ||||
|         it("draws a line going diagonally with term colour", function() | ||||
|             local w = with_window(3, 3, function() | ||||
|                 term.setBackgroundColour(colours.red) | ||||
|                 paintutils.drawLine(1, 1, 3, 3) | ||||
|             end) | ||||
|  | ||||
|             window_eq(w, { | ||||
|                 { "   ", "000", "eff" }, | ||||
|                 { "   ", "000", "fef" }, | ||||
|                 { "   ", "000", "ffe" }, | ||||
|             }) | ||||
|         end) | ||||
|  | ||||
|         it("draws a line going diagonally from bottom left", function() | ||||
|             local w = with_window(3, 3, function() | ||||
|                 term.setBackgroundColour(colours.red) | ||||
|                 paintutils.drawLine(1, 3, 3, 1) | ||||
|             end) | ||||
|  | ||||
|             window_eq(w, { | ||||
|                 { "   ", "000", "ffe" }, | ||||
|                 { "   ", "000", "fef" }, | ||||
|                 { "   ", "000", "eff" }, | ||||
|             }) | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("paintutils.drawBox", function() | ||||
|         it("validates arguments", function() | ||||
|             expect.error(paintutils.drawBox, nil):eq("bad argument #1 (expected number, got nil)") | ||||
|             expect.error(paintutils.drawBox, 1, nil):eq("bad argument #2 (expected number, got nil)") | ||||
|             expect.error(paintutils.drawBox, 1, 1, nil):eq("bad argument #3 (expected number, got nil)") | ||||
|             expect.error(paintutils.drawBox, 1, 1, 1, nil):eq("bad argument #4 (expected number, got nil)") | ||||
|             expect.error(paintutils.drawBox, 1, 1, 1, 1, false):eq("bad argument #5 (expected number, got boolean)") | ||||
|         end) | ||||
|  | ||||
|         it("draws a box with term colour", function() | ||||
|             local w = with_window(3, 3, function() | ||||
|                 term.setBackgroundColour(colours.red) | ||||
|                 paintutils.drawBox(1, 1, 3, 3) | ||||
|             end) | ||||
|  | ||||
|             window_eq(w, { | ||||
|                 { "   ", "eee", "eee" }, | ||||
|                 { "   ", "e0e", "efe" }, | ||||
|                 { "   ", "eee", "eee" }, | ||||
|             }) | ||||
|         end) | ||||
|  | ||||
|         it("draws a box with custom colour", function() | ||||
|             local w = with_window(3, 3, function() | ||||
|                 paintutils.drawBox(1, 1, 3, 3, colours.red) | ||||
|             end) | ||||
|  | ||||
|             window_eq(w, { | ||||
|                 { "   ", "eee", "eee" }, | ||||
|                 { "   ", "e0e", "efe" }, | ||||
|                 { "   ", "eee", "eee" }, | ||||
|             }) | ||||
|         end) | ||||
|  | ||||
|         it("draws a box without overwriting existing content", function() | ||||
|             local w = with_window(3, 3, function() | ||||
|                 term.setCursorPos(2, 2) | ||||
|                 term.write("a") | ||||
|                 paintutils.drawBox(1, 1, 3, 3, colours.red) | ||||
|             end) | ||||
|  | ||||
|             window_eq(w, { | ||||
|                 { "   ", "eee", "eee" }, | ||||
|                 { " a ", "e0e", "efe" }, | ||||
|                 { "   ", "eee", "eee" }, | ||||
|             }) | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("paintutils.drawFilledBox", function() | ||||
|         it("validates arguments", function() | ||||
|             expect.error(paintutils.drawFilledBox, nil):eq("bad argument #1 (expected number, got nil)") | ||||
|             expect.error(paintutils.drawFilledBox, 1, nil):eq("bad argument #2 (expected number, got nil)") | ||||
|             expect.error(paintutils.drawFilledBox, 1, 1, nil):eq("bad argument #3 (expected number, got nil)") | ||||
|             expect.error(paintutils.drawFilledBox, 1, 1, 1, nil):eq("bad argument #4 (expected number, got nil)") | ||||
|             expect.error(paintutils.drawFilledBox, 1, 1, 1, 1, false):eq("bad argument #5 (expected number, got boolean)") | ||||
|         end) | ||||
|  | ||||
|         it("draws a filled box with term colour", function() | ||||
|             local w = with_window(3, 3, function() | ||||
|                 term.setBackgroundColour(colours.red) | ||||
|                 paintutils.drawFilledBox(1, 1, 3, 3) | ||||
|             end) | ||||
|  | ||||
|             window_eq(w, { | ||||
|                 { "   ", "eee", "eee" }, | ||||
|                 { "   ", "eee", "eee" }, | ||||
|                 { "   ", "eee", "eee" }, | ||||
|             }) | ||||
|         end) | ||||
|  | ||||
|         it("draws a filled box with custom colour", function() | ||||
|             local w = with_window(3, 3, function() | ||||
|                 paintutils.drawFilledBox(1, 1, 3, 3, colours.red) | ||||
|             end) | ||||
|  | ||||
|             window_eq(w, { | ||||
|                 { "   ", "eee", "eee" }, | ||||
|                 { "   ", "eee", "eee" }, | ||||
|                 { "   ", "eee", "eee" }, | ||||
|             }) | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("paintutils.drawImage", function() | ||||
|         it("validates arguments", function() | ||||
|             expect.error(paintutils.drawImage, nil):eq("bad argument #1 (expected table, got nil)") | ||||
|             expect.error(paintutils.drawImage, {}, nil):eq("bad argument #2 (expected number, got nil)") | ||||
|             expect.error(paintutils.drawImage, {}, 1, nil):eq("bad argument #3 (expected number, got nil)") | ||||
|         end) | ||||
|     end) | ||||
| end) | ||||
| @@ -1,47 +0,0 @@ | ||||
| describe("The peripheral library", function() | ||||
|     describe("peripheral.isPresent", function() | ||||
|         it("validates arguments", function() | ||||
|             peripheral.isPresent("") | ||||
|             expect.error(peripheral.isPresent, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("peripheral.getType", function() | ||||
|         it("validates arguments", function() | ||||
|             peripheral.getType("") | ||||
|             expect.error(peripheral.getType, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("peripheral.getMethods", function() | ||||
|         it("validates arguments", function() | ||||
|             peripheral.getMethods("") | ||||
|             expect.error(peripheral.getMethods, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("peripheral.call", function() | ||||
|         it("validates arguments", function() | ||||
|             peripheral.call("", "") | ||||
|             expect.error(peripheral.call, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|             expect.error(peripheral.call, "", nil):eq("bad argument #2 (expected string, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("peripheral.wrap", function() | ||||
|         it("validates arguments", function() | ||||
|             peripheral.wrap("") | ||||
|             expect.error(peripheral.wrap, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("peripheral.find", function() | ||||
|         it("validates arguments", function() | ||||
|             peripheral.find("") | ||||
|             peripheral.find("", function() | ||||
|             end) | ||||
|             expect.error(peripheral.find, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|             expect.error(peripheral.find, "", false):eq("bad argument #2 (expected function, got boolean)") | ||||
|         end) | ||||
|     end) | ||||
| end) | ||||
| @@ -1,86 +0,0 @@ | ||||
| describe("The rednet library", function() | ||||
|     describe("rednet.open", function() | ||||
|         it("validates arguments", function() | ||||
|             expect.error(rednet.open, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|         end) | ||||
|  | ||||
|         it("requires a modem to be present", function() | ||||
|             expect.error(rednet.open, "not_there"):eq("No such modem: not_there") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("rednet.close", function() | ||||
|         it("validates arguments", function() | ||||
|             rednet.close() | ||||
|             expect.error(rednet.close, 1):eq("bad argument #1 (expected string, got number)") | ||||
|             expect.error(rednet.close, false):eq("bad argument #1 (expected string, got boolean)") | ||||
|         end) | ||||
|  | ||||
|         it("requires a modem to be present", function() | ||||
|             expect.error(rednet.close, "not_there"):eq("No such modem: not_there") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("rednet.isOpen", function() | ||||
|         it("validates arguments", function() | ||||
|             rednet.isOpen() | ||||
|             rednet.isOpen("") | ||||
|             expect.error(rednet.isOpen, 1):eq("bad argument #1 (expected string, got number)") | ||||
|             expect.error(rednet.isOpen, false):eq("bad argument #1 (expected string, got boolean)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("rednet.send", function() | ||||
|         it("validates arguments", function() | ||||
|             rednet.send(1) | ||||
|             rednet.send(1, nil, "") | ||||
|             expect.error(rednet.send, nil):eq("bad argument #1 (expected number, got nil)") | ||||
|             expect.error(rednet.send, 1, nil, false):eq("bad argument #3 (expected string, got boolean)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("rednet.broadcast", function() | ||||
|         it("validates arguments", function() | ||||
|             rednet.broadcast(nil) | ||||
|             rednet.broadcast(nil, "") | ||||
|             expect.error(rednet.broadcast, nil, false):eq("bad argument #2 (expected string, got boolean)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("rednet.receive", function() | ||||
|         it("validates arguments", function() | ||||
|             expect.error(rednet.receive, false):eq("bad argument #1 (expected string, got boolean)") | ||||
|             expect.error(rednet.receive, "", false):eq("bad argument #2 (expected number, got boolean)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("rednet.host", function() | ||||
|         it("validates arguments", function() | ||||
|             expect.error(rednet.host, "", "localhost"):eq("Reserved hostname") | ||||
|             expect.error(rednet.host, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|             expect.error(rednet.host, "", nil):eq("bad argument #2 (expected string, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("rednet.unhost", function() | ||||
|         it("validates arguments", function() | ||||
|             rednet.unhost("") | ||||
|             expect.error(rednet.unhost, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("rednet.lookup", function() | ||||
|         it("validates arguments", function() | ||||
|             expect.error(rednet.lookup, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|             expect.error(rednet.lookup, "", false):eq("bad argument #2 (expected string, got boolean)") | ||||
|         end) | ||||
|  | ||||
|         it("gets a locally hosted protocol", function() | ||||
|             rednet.host("a_protocol", "a_hostname") | ||||
|  | ||||
|             expect(rednet.lookup("a_protocol")):eq(os.getComputerID()) | ||||
|             expect(rednet.lookup("a_protocol", "localhost")):eq(os.getComputerID()) | ||||
|             expect(rednet.lookup("a_protocol", "a_hostname")):eq(os.getComputerID()) | ||||
|         end) | ||||
|     end) | ||||
| end) | ||||
| @@ -1,43 +0,0 @@ | ||||
| describe("The settings library", function() | ||||
|     describe("settings.set", function() | ||||
|         it("validates arguments", function() | ||||
|             settings.set("test", 1) | ||||
|             settings.set("test", "") | ||||
|             settings.set("test", {}) | ||||
|             settings.set("test", false) | ||||
|  | ||||
|             expect.error(settings.set, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|             expect.error(settings.set, "", nil):eq("bad argument #2 (expected number, string, boolean or table, got nil)") | ||||
|         end) | ||||
|  | ||||
|         it("prevents storing unserialisable types", function() | ||||
|             expect.error(settings.set, "", { print }):eq("Cannot serialize type function") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("settings.get", function() | ||||
|         it("validates arguments", function() | ||||
|             settings.get("test") | ||||
|             expect.error(settings.get, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("settings.unset", function() | ||||
|         it("validates arguments", function() | ||||
|             settings.unset("test") | ||||
|             expect.error(settings.unset, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("settings.load", function() | ||||
|         it("validates arguments", function() | ||||
|             expect.error(settings.load, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("settings.save", function() | ||||
|         it("validates arguments", function() | ||||
|             expect.error(settings.save, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|         end) | ||||
|     end) | ||||
| end) | ||||
| @@ -1,12 +0,0 @@ | ||||
| describe("The term library", function() | ||||
|     describe("term.redirect", function() | ||||
|         it("validates arguments", function() | ||||
|             expect.error(term.redirect, nil):eq("bad argument #1 (expected table, got nil)") | ||||
|         end) | ||||
|  | ||||
|         it("prevents redirecting to term", function() | ||||
|             expect.error(term.redirect, term) | ||||
|                   :eq("term is not a recommended redirect target, try term.current() instead") | ||||
|         end) | ||||
|     end) | ||||
| end) | ||||
| @@ -1,189 +0,0 @@ | ||||
| describe("The textutils library", function() | ||||
|     describe("textutils.slowWrite", function() | ||||
|         it("validates arguments", function() | ||||
|             expect.error(textutils.slowWrite, nil, false):eq("bad argument #2 (expected number, got boolean)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("textutils.formatTime", function() | ||||
|         it("validates arguments", function() | ||||
|             textutils.formatTime(0) | ||||
|             textutils.formatTime(0, false) | ||||
|             expect.error(textutils.formatTime, nil):eq("bad argument #1 (expected number, got nil)") | ||||
|             expect.error(textutils.formatTime, 1, 1):eq("bad argument #2 (expected boolean, got number)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("textutils.pagedPrint", function() | ||||
|         it("validates arguments", function() | ||||
|             expect.error(textutils.pagedPrint, nil, false):eq("bad argument #2 (expected number, got boolean)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("textutils.tabulate", function() | ||||
|         it("validates arguments", function() | ||||
|             term.redirect(window.create(term.current(), 1, 1, 5, 5, false)) | ||||
|  | ||||
|             textutils.tabulate() | ||||
|             textutils.tabulate({ "test" }) | ||||
|             textutils.tabulate(colors.white) | ||||
|  | ||||
|             expect.error(textutils.tabulate, nil):eq("bad argument #1 (expected number or table, got nil)") | ||||
|             expect.error(textutils.tabulate, { "test" }, nil):eq("bad argument #2 (expected number or table, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("textutils.pagedTabulate", function() | ||||
|         it("validates arguments", function() | ||||
|             term.redirect(window.create(term.current(), 1, 1, 5, 5, false)) | ||||
|  | ||||
|             textutils.pagedTabulate() | ||||
|             textutils.pagedTabulate({ "test" }) | ||||
|             textutils.pagedTabulate(colors.white) | ||||
|  | ||||
|             expect.error(textutils.pagedTabulate, nil):eq("bad argument #1 (expected number or table, got nil)") | ||||
|             expect.error(textutils.pagedTabulate, { "test" }, nil):eq("bad argument #2 (expected number or table, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("textutils.empty_json_array", function() | ||||
|         it("is immutable", function() | ||||
|             expect.error(function() textutils.empty_json_array[1] = true end) | ||||
|                 :eq("textutils_spec.lua:51: attempt to mutate textutils.empty_json_array") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("textutils.unserialise", function() | ||||
|         it("validates arguments", function() | ||||
|             textutils.unserialise("") | ||||
|             expect.error(textutils.unserialise, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("textutils.serialiseJSON", function() | ||||
|         it("validates arguments", function() | ||||
|             textutils.serialiseJSON("") | ||||
|             textutils.serialiseJSON(1) | ||||
|             textutils.serialiseJSON({}) | ||||
|             textutils.serialiseJSON(false) | ||||
|             textutils.serialiseJSON("", true) | ||||
|             expect.error(textutils.serialiseJSON, nil):eq("bad argument #1 (expected table, string, number or boolean, got nil)") | ||||
|             expect.error(textutils.serialiseJSON, "", 1):eq("bad argument #2 (expected boolean, got number)") | ||||
|         end) | ||||
|  | ||||
|         it("serializes empty arrays", function() | ||||
|             expect(textutils.serializeJSON(textutils.empty_json_array)):eq("[]") | ||||
|         end) | ||||
|  | ||||
|         it("serializes null", function() | ||||
|             expect(textutils.serializeJSON(textutils.json_null)):eq("null") | ||||
|         end) | ||||
|  | ||||
|         it("serializes strings", function() | ||||
|             expect(textutils.serializeJSON('a')):eq('"a"') | ||||
|             expect(textutils.serializeJSON('"')):eq('"\\""') | ||||
|             expect(textutils.serializeJSON('\\')):eq('"\\\\"') | ||||
|             expect(textutils.serializeJSON('/')):eq('"/"') | ||||
|             expect(textutils.serializeJSON('\b')):eq('"\\b"') | ||||
|             expect(textutils.serializeJSON('\n')):eq('"\\n"') | ||||
|             expect(textutils.serializeJSON(string.char(0))):eq('"\\u0000"') | ||||
|             expect(textutils.serializeJSON(string.char(0x0A))):eq('"\\n"') | ||||
|             expect(textutils.serializeJSON(string.char(0x1D))):eq('"\\u001D"') | ||||
|             expect(textutils.serializeJSON(string.char(0x81))):eq('"\\u0081"') | ||||
|             expect(textutils.serializeJSON(string.char(0xFF))):eq('"\\u00FF"') | ||||
|         end) | ||||
|  | ||||
|         it("serializes arrays until the last index with content", function() | ||||
|             expect(textutils.serializeJSON({ 5, "test", nil, nil, 7 })):eq('[5,"test",null,null,7]') | ||||
|             expect(textutils.serializeJSON({ 5, "test", nil, nil, textutils.json_null })):eq('[5,"test",null,null,null]') | ||||
|             expect(textutils.serializeJSON({ nil, nil, nil, nil, "text" })):eq('[null,null,null,null,"text"]') | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("textutils.unserializeJSON", function() | ||||
|         describe("parses", function() | ||||
|             it("a list of primitives", function() | ||||
|                 expect(textutils.unserializeJSON('[1, true, false, "hello"]')):same { 1, true, false, "hello" } | ||||
|             end) | ||||
|  | ||||
|             it("null when parse_null is true", function() | ||||
|                 expect(textutils.unserializeJSON("null", { parse_null = true })):eq(textutils.json_null) | ||||
|             end) | ||||
|  | ||||
|             it("null when parse_null is false", function() | ||||
|                 expect(textutils.unserializeJSON("null", { parse_null = false })):eq(nil) | ||||
|             end) | ||||
|  | ||||
|             it("an empty array", function() | ||||
|                 expect(textutils.unserializeJSON("[]", { parse_null = false })):eq(textutils.empty_json_array) | ||||
|             end) | ||||
|  | ||||
|             it("basic objects", function() | ||||
|                 expect(textutils.unserializeJSON([[{ "a": 1, "b":2 }]])):same { a = 1, b = 2 } | ||||
|             end) | ||||
|         end) | ||||
|  | ||||
|         describe("parses using NBT-style syntax", function() | ||||
|             local function exp(x) | ||||
|                 local res, err = textutils.unserializeJSON(x, { nbt_style = true }) | ||||
|                 if not res then error(err, 2) end | ||||
|                 return expect(res) | ||||
|             end | ||||
|             it("basic objects", function() | ||||
|                 exp([[{ a: 1, b:2 }]]):same { a = 1, b = 2 } | ||||
|             end) | ||||
|  | ||||
|             it("suffixed numbers", function() | ||||
|                 exp("1b"):eq(1) | ||||
|                 exp("1.1d"):eq(1.1) | ||||
|             end) | ||||
|  | ||||
|             it("strings", function() | ||||
|                 exp("'123'"):eq("123") | ||||
|                 exp("\"123\""):eq("123") | ||||
|             end) | ||||
|  | ||||
|             it("typed arrays", function() | ||||
|                 exp("[B; 1, 2, 3]"):same { 1, 2, 3 } | ||||
|                 exp("[B;]"):same {} | ||||
|             end) | ||||
|         end) | ||||
|  | ||||
|         describe("passes nst/JSONTestSuite", function() | ||||
|             local search_path = "test-rom/data/json-parsing" | ||||
|             local skip = dofile(search_path .. "/skip.lua") | ||||
|             for _, file in pairs(fs.find(search_path .. "/*.json")) do | ||||
|                 local name = fs.getName(file):sub(1, -6); | ||||
|                 (skip[name] and pending or it)(name, function() | ||||
|                     local h = io.open(file, "r") | ||||
|                     local contents = h:read("*a") | ||||
|                     h:close() | ||||
|  | ||||
|                     local res, err = textutils.unserializeJSON(contents) | ||||
|                     local kind = fs.getName(file):sub(1, 1) | ||||
|                     if kind == "n" then | ||||
|                         expect(res):eq(nil) | ||||
|                     elseif kind == "y" then | ||||
|                         if err ~= nil then fail("Expected test to pass, but failed with " .. err) end | ||||
|                     end | ||||
|                 end) | ||||
|             end | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("textutils.urlEncode", function() | ||||
|         it("validates arguments", function() | ||||
|             textutils.urlEncode("") | ||||
|             expect.error(textutils.urlEncode, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("textutils.complete", function() | ||||
|         it("validates arguments", function() | ||||
|             textutils.complete("pri") | ||||
|             textutils.complete("pri", _G) | ||||
|             expect.error(textutils.complete, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|             expect.error(textutils.complete, "", false):eq("bad argument #2 (expected table, got boolean)") | ||||
|         end) | ||||
|     end) | ||||
| end) | ||||
| @@ -1,173 +0,0 @@ | ||||
| describe("The window library", function() | ||||
|     local function mk() | ||||
|         return window.create(term.current(), 1, 1, 5, 5, false) | ||||
|     end | ||||
|  | ||||
|     describe("window.create", function() | ||||
|         it("validates arguments", function() | ||||
|             local r = mk() | ||||
|             window.create(r, 1, 1, 5, 5) | ||||
|             window.create(r, 1, 1, 5, 5, false) | ||||
|  | ||||
|             expect.error(window.create, nil):eq("bad argument #1 (expected table, got nil)") | ||||
|             expect.error(window.create, r, nil):eq("bad argument #2 (expected number, got nil)") | ||||
|             expect.error(window.create, r, 1, nil):eq("bad argument #3 (expected number, got nil)") | ||||
|             expect.error(window.create, r, 1, 1, nil):eq("bad argument #4 (expected number, got nil)") | ||||
|             expect.error(window.create, r, 1, 1, 1, nil):eq("bad argument #5 (expected number, got nil)") | ||||
|             expect.error(window.create, r, 1, 1, 1, 1, ""):eq("bad argument #6 (expected boolean, got string)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("Window.blit", function() | ||||
|         it("validates arguments", function() | ||||
|             local w = mk() | ||||
|             w.blit("a", "a", "a") | ||||
|  | ||||
|             expect.error(w.blit, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|             expect.error(w.blit, "", nil):eq("bad argument #2 (expected string, got nil)") | ||||
|             expect.error(w.blit, "", "", nil):eq("bad argument #3 (expected string, got nil)") | ||||
|             expect.error(w.blit, "", "", "a"):eq("Arguments must be the same length") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("Window.setCursorPos", function() | ||||
|         it("validates arguments", function() | ||||
|             local w = mk() | ||||
|             w.setCursorPos(1, 1) | ||||
|  | ||||
|             expect.error(w.setCursorPos, nil):eq("bad argument #1 (expected number, got nil)") | ||||
|             expect.error(w.setCursorPos, 1, nil):eq("bad argument #2 (expected number, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("Window.setCursorBlink", function() | ||||
|         it("validates arguments", function() | ||||
|             local w = mk() | ||||
|             w.setCursorBlink(false) | ||||
|             expect.error(w.setCursorBlink, nil):eq("bad argument #1 (expected boolean, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("Window.setTextColour", function() | ||||
|         it("validates arguments", function() | ||||
|             local w = mk() | ||||
|             w.setTextColour(colors.white) | ||||
|  | ||||
|             expect.error(w.setTextColour, nil):eq("bad argument #1 (expected number, got nil)") | ||||
|             expect.error(w.setTextColour, -5):eq("Invalid color (got -5)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("Window.setPaletteColour", function() | ||||
|         it("validates arguments", function() | ||||
|             local w = mk() | ||||
|             w.setPaletteColour(colors.white, 0, 0, 0) | ||||
|             w.setPaletteColour(colors.white, 0x000000) | ||||
|  | ||||
|             expect.error(w.setPaletteColour, nil):eq("bad argument #1 (expected number, got nil)") | ||||
|             expect.error(w.setPaletteColour, -5):eq("Invalid color (got -5)") | ||||
|             expect.error(w.setPaletteColour, colors.white):eq("bad argument #2 (expected number, got nil)") | ||||
|             expect.error(w.setPaletteColour, colors.white, 1, false):eq("bad argument #3 (expected number, got boolean)") | ||||
|             expect.error(w.setPaletteColour, colors.white, 1, nil, 1):eq("bad argument #3 (expected number, got nil)") | ||||
|             expect.error(w.setPaletteColour, colors.white, 1, 1, nil):eq("bad argument #4 (expected number, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("Window.getPaletteColour", function() | ||||
|         it("validates arguments", function() | ||||
|             local w = mk() | ||||
|             w.getPaletteColour(colors.white) | ||||
|             expect.error(w.getPaletteColour, nil):eq("bad argument #1 (expected number, got nil)") | ||||
|             expect.error(w.getPaletteColour, -5):eq("Invalid color (got -5)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("Window.setBackgroundColour", function() | ||||
|         it("validates arguments", function() | ||||
|             local w = mk() | ||||
|             w.setBackgroundColour(colors.white) | ||||
|  | ||||
|             expect.error(w.setBackgroundColour, nil):eq("bad argument #1 (expected number, got nil)") | ||||
|             expect.error(w.setBackgroundColour, -5):eq("Invalid color (got -5)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("Window.scroll", function() | ||||
|         it("validates arguments", function() | ||||
|             local w = mk() | ||||
|             w.scroll(0) | ||||
|             expect.error(w.scroll, nil):eq("bad argument #1 (expected number, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("Window.setVisible", function() | ||||
|         it("validates arguments", function() | ||||
|             local w = mk() | ||||
|             w.setVisible(false) | ||||
|             expect.error(w.setVisible, nil):eq("bad argument #1 (expected boolean, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("Window.reposition", function() | ||||
|         it("validates arguments", function() | ||||
|             local w = mk() | ||||
|             w.reposition(1, 1) | ||||
|             w.reposition(1, 1, 5, 5) | ||||
|             expect.error(w.reposition, nil):eq("bad argument #1 (expected number, got nil)") | ||||
|             expect.error(w.reposition, 1, nil):eq("bad argument #2 (expected number, got nil)") | ||||
|             expect.error(w.reposition, 1, 1, false, 1):eq("bad argument #3 (expected number, got boolean)") | ||||
|             expect.error(w.reposition, 1, 1, nil, 1):eq("bad argument #3 (expected number, got nil)") | ||||
|             expect.error(w.reposition, 1, 1, 1, nil):eq("bad argument #4 (expected number, got nil)") | ||||
|             expect.error(w.reposition, 1, 1, 1, 1, true):eq("bad argument #5 (expected table, got boolean)") | ||||
|         end) | ||||
|  | ||||
|         it("can change the buffer", function() | ||||
|             local a, b = mk(), mk() | ||||
|             local target = window.create(a, 1, 1, a.getSize()) | ||||
|  | ||||
|             target.write("Test") | ||||
|             expect((a.getLine(1))):equal("Test ") | ||||
|             expect({ a.getCursorPos() }):same { 5, 1 } | ||||
|  | ||||
|             target.reposition(1, 1, nil, nil, b) | ||||
|  | ||||
|             target.redraw() | ||||
|             expect((a.getLine(1))):equal("Test ") | ||||
|             expect({ a.getCursorPos() }):same { 5, 1 } | ||||
|  | ||||
|             target.setCursorPos(1, 1) target.write("More") | ||||
|             expect((a.getLine(1))):equal("Test ") | ||||
|             expect((b.getLine(1))):equal("More ") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("Window.getLine", function() | ||||
|         it("validates arguments", function() | ||||
|             local w = mk() | ||||
|             w.getLine(1) | ||||
|             local _, y = w.getSize() | ||||
|             expect.error(w.getLine, nil):eq("bad argument #1 (expected number, got nil)") | ||||
|             expect.error(w.getLine, 0):eq("Line is out of range.") | ||||
|             expect.error(w.getLine, y + 1):eq("Line is out of range.") | ||||
|         end) | ||||
|  | ||||
|         it("provides a line's contents", function() | ||||
|             local w = mk() | ||||
|             w.blit("test", "aaaa", "4444") | ||||
|             expect({ w.getLine(1) }):same { "test ", "aaaa0", "4444f" } | ||||
|         end) | ||||
|     end) | ||||
|     describe("Window.setVisible", function() | ||||
|         it("validates arguments", function() | ||||
|             local w = mk() | ||||
|             expect.error(w.setVisible, nil):eq("bad argument #1 (expected boolean, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|     describe("Window.isVisible", function() | ||||
|         it("gets window visibility", function() | ||||
|             local w = mk() | ||||
|             w.setVisible(false) | ||||
|             expect(w.isVisible()):same(false) | ||||
|         end) | ||||
|     end) | ||||
| end) | ||||
| @@ -1,123 +0,0 @@ | ||||
| describe("The Lua base library", function() | ||||
|     describe("expect", function() | ||||
|         local e = _G["~expect"] | ||||
|  | ||||
|         it("checks a single type", function() | ||||
|             expect(e(1, "test", "string")):eq(true) | ||||
|             expect(e(1, 2, "number")):eq(true) | ||||
|  | ||||
|             expect.error(e, 1, nil, "string"):eq("bad argument #1 (expected string, got nil)") | ||||
|             expect.error(e, 2, 1, "nil"):eq("bad argument #2 (expected nil, got number)") | ||||
|         end) | ||||
|  | ||||
|         it("checks multiple types", function() | ||||
|             expect(e(1, "test", "string", "number")):eq(true) | ||||
|             expect(e(1, 2, "string", "number")):eq(true) | ||||
|  | ||||
|             expect.error(e, 1, nil, "string", "number"):eq("bad argument #1 (expected string or number, got nil)") | ||||
|             expect.error(e, 2, false, "string", "table", "number", "nil") | ||||
|                   :eq("bad argument #2 (expected string, table or number, got boolean)") | ||||
|         end) | ||||
|  | ||||
|         it("includes the function name", function() | ||||
|             local function worker() | ||||
|                 expect(e(1, nil, "string")):eq(true) | ||||
|             end | ||||
|             local function trampoline() | ||||
|                 worker() | ||||
|             end | ||||
|  | ||||
|             expect.error(trampoline):eq("base_spec.lua:27: bad argument #1 to 'worker' (expected string, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("sleep", function() | ||||
|         it("validates arguments", function() | ||||
|             sleep(0) | ||||
|             sleep(nil) | ||||
|  | ||||
|             expect.error(sleep, false):eq("bad argument #1 (expected number, got boolean)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("write", function() | ||||
|         it("validates arguments", function() | ||||
|             write("") | ||||
|             expect.error(write, nil):eq("bad argument #1 (expected string or number, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("loadfile", function() | ||||
|         it("validates arguments", function() | ||||
|             loadfile("") | ||||
|             loadfile("", {}) | ||||
|  | ||||
|             expect.error(loadfile, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|             expect.error(loadfile, "", false):eq("bad argument #2 (expected table, got boolean)") | ||||
|         end) | ||||
|  | ||||
|         it("prefixes the filename with @", function() | ||||
|             local info = debug.getinfo(loadfile("/rom/startup.lua"), "S") | ||||
|             expect(info):matches { short_src = "startup.lua", source = "@startup.lua" } | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("dofile", function() | ||||
|         it("validates arguments", function() | ||||
|             expect.error(dofile, ""):eq("File not found") | ||||
|             expect.error(dofile, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("loadstring", function() | ||||
|         it("prefixes the chunk name with '='", function() | ||||
|             local info = debug.getinfo(loadstring("return 1", "name"), "S") | ||||
|             expect(info):matches { short_src = "name", source = "=name" } | ||||
|         end) | ||||
|  | ||||
|         it("does not prefix for unnamed chunks", function() | ||||
|             local info = debug.getinfo(loadstring("return 1"), "S") | ||||
|             expect(info):matches { short_src = '[string "return 1"]', source = "return 1", } | ||||
|         end) | ||||
|  | ||||
|         it("does not prefix when already prefixed", function() | ||||
|             local info = debug.getinfo(loadstring("return 1", "@file.lua"), "S") | ||||
|             expect(info):matches { short_src = "file.lua", source = "@file.lua" } | ||||
|  | ||||
|             info = debug.getinfo(loadstring("return 1", "=file.lua"), "S") | ||||
|             expect(info):matches { short_src = "file.lua", source = "=file.lua" } | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("load", function() | ||||
|         it("validates arguments", function() | ||||
|             load("") | ||||
|             load(function() | ||||
|             end) | ||||
|             load("", "") | ||||
|             load("", "", "") | ||||
|             load("", "", "", _ENV) | ||||
|  | ||||
|             expect.error(load, nil):eq("bad argument #1 (expected function or string, got nil)") | ||||
|             expect.error(load, "", false):eq("bad argument #2 (expected string, got boolean)") | ||||
|             expect.error(load, "", "", false):eq("bad argument #3 (expected string, got boolean)") | ||||
|             expect.error(load, "", "", "", false):eq("bad argument #4 (expected table, got boolean)") | ||||
|         end) | ||||
|  | ||||
|         local function generator(parts) | ||||
|             return coroutine.wrap(function() | ||||
|                 for i = 1, #parts do | ||||
|                     coroutine.yield(parts[i]) | ||||
|                 end | ||||
|             end) | ||||
|         end | ||||
|  | ||||
|         it("does not prefix the chunk name with '='", function() | ||||
|             local info = debug.getinfo(load("return 1", "name"), "S") | ||||
|             expect(info):matches { short_src = "[string \"name\"]", source = "name" } | ||||
|  | ||||
|             info = debug.getinfo(load(generator { "return 1" }, "name"), "S") | ||||
|             expect(info):matches { short_src = "[string \"name\"]", source = "name" } | ||||
|         end) | ||||
|     end) | ||||
| end) | ||||
| @@ -1,41 +0,0 @@ | ||||
| describe("cc.pretty", function() | ||||
|     local str = require("cc.strings") | ||||
|  | ||||
|     describe("wrap", function() | ||||
|         it("validates arguments", function() | ||||
|             str.wrap("test string is long") | ||||
|             str.wrap("test string is long", 11) | ||||
|             expect.error(str.wrap, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|             expect.error(str.wrap, "", false):eq("bad argument #2 (expected number, got boolean)") | ||||
|         end) | ||||
|  | ||||
|         it("wraps lines", function() | ||||
|             expect(str.wrap("test string is long")[1]):eq("test string is long") | ||||
|  | ||||
|             expect(str.wrap("test string is long", 15)[1]):eq("test string is ") | ||||
|             expect(str.wrap("test string is long", 15)[2]):eq("long") | ||||
|  | ||||
|             expect(str.wrap("test string is long", 12)[1]):eq("test string ") | ||||
|             expect(str.wrap("test string is long", 12)[2]):eq("is long") | ||||
|  | ||||
|             expect(str.wrap("test string is long", 11)[1]):eq("test string") | ||||
|             expect(str.wrap("test string is long", 11)[2]):eq("is long") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("ensure_width", function() | ||||
|         it("validates arguments", function() | ||||
|             str.wrap("test string is long") | ||||
|             str.wrap("test string is long", 11) | ||||
|             expect.error(str.ensure_width, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|             expect.error(str.ensure_width, "", false):eq("bad argument #2 (expected number, got boolean)") | ||||
|         end) | ||||
|  | ||||
|         it("pads lines", function() | ||||
|             expect(str.ensure_width("test string is long", 25)):eq("test string is long      ") | ||||
|         end) | ||||
|         it("truncates lines", function() | ||||
|             expect(str.ensure_width("test string is long", 15)):eq("test string is ") | ||||
|         end) | ||||
|     end) | ||||
| end) | ||||
| @@ -1,30 +0,0 @@ | ||||
| describe("The multishell program", function() | ||||
|     describe("multishell.setFocus", function() | ||||
|         it("validates arguments", function() | ||||
|             multishell.setFocus(multishell.getFocus()) | ||||
|             expect.error(multishell.setFocus, nil):eq("bad argument #1 (expected number, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("multishell.getTitle", function() | ||||
|         it("validates arguments", function() | ||||
|             multishell.getTitle(1) | ||||
|             expect.error(multishell.getTitle, nil):eq("bad argument #1 (expected number, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("multishell.setTitle", function() | ||||
|         it("validates arguments", function() | ||||
|             multishell.setTitle(1, multishell.getTitle(1)) | ||||
|             expect.error(multishell.setTitle, nil):eq("bad argument #1 (expected number, got nil)") | ||||
|             expect.error(multishell.setTitle, 1, nil):eq("bad argument #2 (expected string, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("multishell.launch", function() | ||||
|         it("validates arguments", function() | ||||
|             expect.error(multishell.launch, nil):eq("bad argument #1 (expected table, got nil)") | ||||
|             expect.error(multishell.launch, _ENV, nil):eq("bad argument #2 (expected string, got nil)") | ||||
|         end) | ||||
|     end) | ||||
| end) | ||||
| @@ -1,33 +0,0 @@ | ||||
| local capture = require "test_helpers".capture_program | ||||
|  | ||||
| describe("The exec program", function() | ||||
|     it("displays an error without the commands api", function() | ||||
|         stub(_G, "commands", nil) | ||||
|         expect(capture(stub, "/rom/programs/command/exec.lua")) | ||||
|         :matches { ok = true, output = "", error = "Requires a Command Computer.\n" } | ||||
|     end) | ||||
|  | ||||
|     it("displays its usage when given no argument", function() | ||||
|         stub(_G, "commands", {}) | ||||
|         expect(capture(stub, "/rom/programs/command/exec.lua")) | ||||
|         :matches { ok = true, output = "", error = "Usage: /rom/programs/command/exec.lua <command>\n" } | ||||
|     end) | ||||
|  | ||||
|     it("runs a command", function() | ||||
|         stub(_G, "commands", { | ||||
|             exec = function() return true, { "Hello World!" } end, | ||||
|         }) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/command/exec.lua computercraft")) | ||||
|         :matches { ok = true, output = "Success\nHello World!\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("reports command failures", function() | ||||
|         stub(_G, "commands", { | ||||
|             exec = function() return false, { "Hello World!" } end, | ||||
|         }) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/command/exec.lua computercraft")) | ||||
|         :matches { ok = true, output = "Hello World!\n", error = "Failed\n" } | ||||
|     end) | ||||
| end) | ||||
| @@ -1,40 +0,0 @@ | ||||
| local capture = require "test_helpers".capture_program | ||||
|  | ||||
| describe("The copy program", function() | ||||
|     local function touch(file) | ||||
|         io.open(file, "w"):close() | ||||
|     end | ||||
|  | ||||
|     it("copies a file", function() | ||||
|         touch("/test-files/copy/a.txt") | ||||
|  | ||||
|         shell.run("copy /test-files/copy/a.txt /test-files/copy/b.txt") | ||||
|  | ||||
|         expect(fs.exists("/test-files/copy/a.txt")):eq(true) | ||||
|         expect(fs.exists("/test-files/copy/b.txt")):eq(true) | ||||
|     end) | ||||
|  | ||||
|     it("fails when copying a non-existent file", function() | ||||
|         expect(capture(stub, "copy nothing destination")) | ||||
|         :matches { ok = true, output = "", error = "No matching files\n" } | ||||
|     end) | ||||
|  | ||||
|     it("fails when overwriting an existing file", function() | ||||
|         touch("/test-files/copy/c.txt") | ||||
|  | ||||
|         expect(capture(stub, "copy /test-files/copy/c.txt /test-files/copy/c.txt")) | ||||
|         :matches { ok = true, output = "", error = "Destination exists\n" } | ||||
|     end) | ||||
|  | ||||
|     it("fails when copying into read-only locations", function() | ||||
|         touch("/test-files/copy/d.txt") | ||||
|  | ||||
|         expect(capture(stub, "copy /test-files/copy/d.txt /rom/test.txt")) | ||||
|         :matches { ok = true, output = "", error = "Destination is read-only\n" } | ||||
|     end) | ||||
|  | ||||
|     it("displays the usage when given no arguments", function() | ||||
|         expect(capture(stub, "copy")) | ||||
|         :matches { ok = true, output = "Usage: copy <source> <destination>\n", error = "" } | ||||
|     end) | ||||
| end) | ||||
| @@ -1,35 +0,0 @@ | ||||
| describe("The rm program", function() | ||||
|     local function touch(file) | ||||
|         io.open(file, "w"):close() | ||||
|     end | ||||
|  | ||||
|     it("deletes one file", function() | ||||
|         touch("/test-files/a.txt") | ||||
|  | ||||
|         shell.run("rm /test-files/a.txt") | ||||
|  | ||||
|         expect(fs.exists("/test-files/a.txt")):eq(false) | ||||
|     end) | ||||
|  | ||||
|     it("deletes many files", function() | ||||
|         touch("/test-files/a.txt") | ||||
|         touch("/test-files/b.txt") | ||||
|         touch("/test-files/c.txt") | ||||
|  | ||||
|         shell.run("rm /test-files/a.txt /test-files/b.txt") | ||||
|  | ||||
|         expect(fs.exists("/test-files/a.txt")):eq(false) | ||||
|         expect(fs.exists("/test-files/b.txt")):eq(false) | ||||
|         expect(fs.exists("/test-files/c.txt")):eq(true) | ||||
|     end) | ||||
|  | ||||
|     it("deletes a glob", function() | ||||
|         touch("/test-files/a.txt") | ||||
|         touch("/test-files/b.txt") | ||||
|  | ||||
|         shell.run("rm /test-files/*.txt") | ||||
|  | ||||
|         expect(fs.exists("/test-files/a.txt")):eq(false) | ||||
|         expect(fs.exists("/test-files/b.txt")):eq(false) | ||||
|     end) | ||||
| end) | ||||
| @@ -1,29 +0,0 @@ | ||||
| describe("The mkdir program", function() | ||||
|     it("creates a directory", function() | ||||
|         fs.delete("/test-files") | ||||
|  | ||||
|         shell.run("mkdir /test-files/a") | ||||
|  | ||||
|         expect(fs.isDir("/test-files/a")):eq(true) | ||||
|     end) | ||||
|  | ||||
|     it("creates many directories", function() | ||||
|         fs.delete("/test-files") | ||||
|  | ||||
|         shell.run("mkdir /test-files/a /test-files/b") | ||||
|  | ||||
|         expect(fs.isDir("/test-files/a")):eq(true) | ||||
|         expect(fs.isDir("/test-files/b")):eq(true) | ||||
|     end) | ||||
|  | ||||
|     it("can be completed", function() | ||||
|         fs.delete("/test-files") | ||||
|         fs.makeDir("/test-files/a") | ||||
|         fs.makeDir("/test-files/b") | ||||
|         io.open("/test-files.a.txt", "w"):close() | ||||
|  | ||||
|         local complete = shell.getCompletionInfo()["rom/programs/mkdir.lua"].fnComplete | ||||
|         expect(complete(shell, 1, "/test-files/", {})):same { "a/", "b/" } | ||||
|         expect(complete(shell, 2, "/test-files/", { "/" })):same { "a/", "b/" } | ||||
|     end) | ||||
| end) | ||||
| @@ -1,36 +0,0 @@ | ||||
| local capture = require "test_helpers".capture_program | ||||
|  | ||||
| describe("The motd program", function() | ||||
|     local function setup_date(month, day) | ||||
|         stub(os, "date", function() return { month = month, day = day } end) | ||||
|     end | ||||
|  | ||||
|     it("displays MOTD", function() | ||||
|         setup_date(0, 0) | ||||
|         local file = fs.open("/motd_check.txt", "w") | ||||
|         file.write("Hello World!") | ||||
|         file.close() | ||||
|         settings.set("motd.path", "/motd_check.txt") | ||||
|  | ||||
|         expect(capture(stub, "motd")) | ||||
|             :matches { ok = true, output = "Hello World!\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("displays date-specific MOTD (1/1)", function() | ||||
|         setup_date(1, 1) | ||||
|         expect(capture(stub, "motd")) | ||||
|             :matches { ok = true, output = "Happy new year!\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("displays date-specific MOTD (10/31)", function() | ||||
|         setup_date(10, 31) | ||||
|         expect(capture(stub, "motd")) | ||||
|             :matches { ok = true, output = "OOoooOOOoooo! Spooky!\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("displays date-specific MOTD (12/24)", function() | ||||
|         setup_date(12, 24) | ||||
|         expect(capture(stub, "motd")) | ||||
|             :matches { ok = true, output = "Merry X-mas!\n", error = "" } | ||||
|     end) | ||||
| end) | ||||
| @@ -1,74 +0,0 @@ | ||||
| local capture = require "test_helpers".capture_program | ||||
|  | ||||
| describe("The move program", function() | ||||
|     local function cleanup() fs.delete("/test-files/move") end | ||||
|     local function touch(file) | ||||
|         io.open(file, "w"):close() | ||||
|     end | ||||
|  | ||||
|     it("move a file", function() | ||||
|         cleanup() | ||||
|         touch("/test-files/move/a.txt") | ||||
|  | ||||
|         shell.run("move /test-files/move/a.txt /test-files/move/b.txt") | ||||
|  | ||||
|         expect(fs.exists("/test-files/move/a.txt")):eq(false) | ||||
|         expect(fs.exists("/test-files/move/b.txt")):eq(true) | ||||
|     end) | ||||
|  | ||||
|     it("moves a file to a directory", function() | ||||
|         cleanup() | ||||
|         touch("/test-files/move/a.txt") | ||||
|         fs.makeDir("/test-files/move/a") | ||||
|  | ||||
|         expect(capture(stub, "move /test-files/move/a.txt /test-files/move/a")) | ||||
|         :matches { ok = true } | ||||
|  | ||||
|         expect(fs.exists("/test-files/move/a.txt")):eq(false) | ||||
|         expect(fs.exists("/test-files/move/a/a.txt")):eq(true) | ||||
|     end) | ||||
|  | ||||
|     it("fails when moving a file which doesn't exist", function() | ||||
|         expect(capture(stub, "move nothing destination")) | ||||
|         :matches { ok = true, output = "", error = "No matching files\n" } | ||||
|     end) | ||||
|  | ||||
|     it("fails when overwriting an existing file", function() | ||||
|         cleanup() | ||||
|         touch("/test-files/move/a.txt") | ||||
|  | ||||
|         expect(capture(stub, "move /test-files/move/a.txt /test-files/move/a.txt")) | ||||
|         :matches { ok = true, output = "", error = "Destination exists\n" } | ||||
|     end) | ||||
|  | ||||
|     it("fails when moving to read-only locations", function() | ||||
|         cleanup() | ||||
|         touch("/test-files/move/a.txt") | ||||
|  | ||||
|         expect(capture(stub, "move /test-files/move/a.txt /rom/test.txt")) | ||||
|         :matches { ok = true, output = "", error = "Destination is read-only\n" } | ||||
|     end) | ||||
|  | ||||
|     it("fails when moving from read-only locations", function() | ||||
|         expect(capture(stub, "move /rom/startup.lua /test-files/move/not-exist.txt")) | ||||
|         :matches { ok = true, output = "", error = "Cannot move read-only file /rom/startup.lua\n" } | ||||
|     end) | ||||
|  | ||||
|     it("fails when moving mounts", function() | ||||
|         expect(capture(stub, "move /rom /test-files/move/rom")) | ||||
|         :matches { ok = true, output = "", error = "Cannot move mount /rom\n" } | ||||
|     end) | ||||
|  | ||||
|     it("fails when moving a file multiple times", function() | ||||
|         cleanup() | ||||
|         touch("/test-files/move/a.txt") | ||||
|         touch("/test-files/move/b.txt") | ||||
|         expect(capture(stub, "move /test-files/move/*.txt /test-files/move/c.txt")) | ||||
|         :matches { ok = true, output = "", error = "Cannot overwrite file multiple times\n" } | ||||
|     end) | ||||
|  | ||||
|     it("displays the usage with no arguments", function() | ||||
|         expect(capture(stub, "move")) | ||||
|         :matches { ok = true, output = "Usage: move <source> <destination>\n", error = "" } | ||||
|     end) | ||||
| end) | ||||
| @@ -1,94 +0,0 @@ | ||||
| describe("The shell", function() | ||||
|     describe("require", function() | ||||
|         it("validates arguments", function() | ||||
|             require("math") | ||||
|             expect.error(require, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("shell.run", function() | ||||
|         it("sets the arguments", function() | ||||
|             local handle = fs.open("test-files/out.txt", "w") | ||||
|             handle.writeLine("_G.__arg = arg") | ||||
|             handle.close() | ||||
|  | ||||
|             shell.run("/test-files/out.txt", "arg1", "arg2") | ||||
|             fs.delete("test-files/out.txt") | ||||
|  | ||||
|             local args = _G.__arg | ||||
|             _G.__arg = nil | ||||
|  | ||||
|             expect(args):same { [0] = "/test-files/out.txt", "arg1", "arg2" } | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("shell.setDir", function() | ||||
|         it("validates arguments", function() | ||||
|             shell.setDir(shell.dir()) | ||||
|             expect.error(shell.setDir, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("shell.setPath", function() | ||||
|         it("validates arguments", function() | ||||
|             shell.setPath(shell.path()) | ||||
|             expect.error(shell.setPath, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("shell.resolve", function() | ||||
|         it("validates arguments", function() | ||||
|             shell.resolve("") | ||||
|             expect.error(shell.resolve, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("shell.resolveProgram", function() | ||||
|         it("validates arguments", function() | ||||
|             shell.resolveProgram("ls") | ||||
|             expect.error(shell.resolveProgram, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("shell.complete", function() | ||||
|         it("validates arguments", function() | ||||
|             shell.complete("ls") | ||||
|             expect.error(shell.complete, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("shell.setCompletionFunction", function() | ||||
|         it("validates arguments", function() | ||||
|             expect.error(shell.setCompletionFunction, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|             expect.error(shell.setCompletionFunction, "", nil):eq("bad argument #2 (expected function, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("shell.setCompletionFunction", function() | ||||
|         it("validates arguments", function() | ||||
|             expect.error(shell.setCompletionFunction, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|             expect.error(shell.setCompletionFunction, "", nil):eq("bad argument #2 (expected function, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("shell.setAlias", function() | ||||
|         it("validates arguments", function() | ||||
|             shell.setAlias("sl", "ls") | ||||
|             expect.error(shell.setAlias, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|             expect.error(shell.setAlias, "", nil):eq("bad argument #2 (expected string, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("shell.clearAlias", function() | ||||
|         it("validates arguments", function() | ||||
|             shell.clearAlias("sl") | ||||
|             expect.error(shell.clearAlias, nil):eq("bad argument #1 (expected string, got nil)") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("shell.switchTab", function() | ||||
|         it("validates arguments", function() | ||||
|             expect.error(shell.switchTab, nil):eq("bad argument #1 (expected number, got nil)") | ||||
|         end) | ||||
|     end) | ||||
| end) | ||||
| @@ -1,69 +0,0 @@ | ||||
| local capture = require "test_helpers".capture_program | ||||
|  | ||||
| describe("The craft program", function() | ||||
|     it("errors when not a turtle", function() | ||||
|         stub(_G, "turtle", nil) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/craft.lua")) | ||||
|         :matches { ok = true, output = "", error = "Requires a Turtle\n" } | ||||
|     end) | ||||
|  | ||||
|     it("fails when turtle.craft() is unavailable", function() | ||||
|         stub(_G, "turtle", {}) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/craft.lua")) | ||||
|         :matches { ok = true, output = "Requires a Crafty Turtle\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("displays its usage when given no arguments", function() | ||||
|         stub(_G, "turtle", { craft = function() end }) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/craft.lua")) | ||||
|         :matches { ok = true, output = "Usage: /rom/programs/turtle/craft.lua [number]\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("crafts multiple items", function() | ||||
|         local item_count = 3 | ||||
|         stub(_G, "turtle", { | ||||
|             craft = function() | ||||
|                 item_count = 1 | ||||
|                 return true | ||||
|             end, | ||||
|             getItemCount = function() return item_count end, | ||||
|             getSelectedSlot = function() return 1 end, | ||||
|         }) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/craft.lua 2")) | ||||
|         :matches { ok = true, output = "2 items crafted\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("craft a single item", function() | ||||
|         local item_count = 2 | ||||
|         stub(_G, "turtle", { | ||||
|             craft = function() | ||||
|                 item_count = 1 | ||||
|                 return true | ||||
|             end, | ||||
|             getItemCount = function() return item_count end, | ||||
|             getSelectedSlot = function() return 1 end, | ||||
|         }) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/craft.lua 1")) | ||||
|         :matches { ok = true, output = "1 item crafted\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("crafts no items", function() | ||||
|         local item_count = 2 | ||||
|         stub(_G, "turtle", { | ||||
|             craft = function() | ||||
|                 item_count = 1 | ||||
|                 return false | ||||
|             end, | ||||
|             getItemCount = function() return item_count end, | ||||
|             getSelectedSlot = function() return 1 end, | ||||
|         }) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/craft.lua 1")) | ||||
|         :matches { ok = true, output = "No items crafted\n", error = "" } | ||||
|     end) | ||||
| end) | ||||
| @@ -1,89 +0,0 @@ | ||||
| local capture = require "test_helpers".capture_program | ||||
|  | ||||
| describe("The turtle equip program", function() | ||||
|     it("errors when not a turtle", function() | ||||
|         stub(_G, "turtle", nil) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/equip.lua")) | ||||
|         :matches { ok = true, output = "", error = "Requires a Turtle\n" } | ||||
|     end) | ||||
|  | ||||
|  | ||||
|     it("displays its usage when given no arguments", function() | ||||
|         stub(_G, "turtle", {}) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/equip.lua")) | ||||
|         :matches { ok = true, output = "Usage: /rom/programs/turtle/equip.lua <slot> <side>\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("equip nothing", function() | ||||
|         stub(_G, "turtle", { | ||||
|             select = function() end, | ||||
|             getItemCount = function() return 0 end, | ||||
|         }) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/equip.lua 1 left")) | ||||
|         :matches { ok = true, output = "Nothing to equip\n", error = "" } | ||||
|         expect(capture(stub, "/rom/programs/turtle/equip.lua 1 right")) | ||||
|         :matches { ok = true, output = "Nothing to equip\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("swaps existing upgrades", function() | ||||
|         stub(_G, "turtle", { | ||||
|             select = function() end, | ||||
|             getItemCount = function() return 1 end, | ||||
|             equipLeft = function() return true end, | ||||
|             equipRight = function() return true end, | ||||
|         }) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/equip.lua 1 left")) | ||||
|         :matches { ok = true, output = "Items swapped\n", error = "" } | ||||
|         expect(capture(stub, "/rom/programs/turtle/equip.lua 1 right")) | ||||
|         :matches { ok = true, output = "Items swapped\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     describe("equips a new upgrade", function() | ||||
|         local function setup() | ||||
|             local item_count = 1 | ||||
|             stub(_G, "turtle", { | ||||
|                 select = function() end, | ||||
|                 getItemCount = function() return item_count end, | ||||
|                 equipLeft = function() | ||||
|                     item_count  = 0 | ||||
|                     return true | ||||
|                 end, | ||||
|                 equipRight = function() | ||||
|                     item_count = 0 | ||||
|                     return true | ||||
|                 end, | ||||
|             }) | ||||
|         end | ||||
|  | ||||
|         it("on the left", function() | ||||
|             setup() | ||||
|             expect(capture(stub, "/rom/programs/turtle/equip.lua 1 left")) | ||||
|             :matches { ok = true, output = "Item equipped\n", error = "" } | ||||
|         end) | ||||
|  | ||||
|         it("on the right", function() | ||||
|             setup() | ||||
|             expect(capture(stub, "/rom/programs/turtle/equip.lua 1 right")) | ||||
|             :matches { ok = true, output = "Item equipped\n", error = "" } | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     it("handles when an upgrade cannot be equipped", function() | ||||
|         stub(_G, "turtle", { | ||||
|             select = function() end, | ||||
|             getItemCount = function() return 1 end, | ||||
|             equipLeft = function() return false end, | ||||
|             equipRight = function() return false end, | ||||
|         }) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/equip.lua 1 left")) | ||||
|         :matches { ok = true, output = "Item not equippable\n", error = "" } | ||||
|         expect(capture(stub, "/rom/programs/turtle/equip.lua 1 right")) | ||||
|         :matches { ok = true, output = "Item not equippable\n", error = "" } | ||||
|     end) | ||||
|  | ||||
| end) | ||||
| @@ -1,62 +0,0 @@ | ||||
| local capture = require "test_helpers".capture_program | ||||
|  | ||||
| describe("The refuel program", function() | ||||
|     local function setup_turtle(fuel_level, fuel_limit, item_count) | ||||
|         stub(_G, "turtle", { | ||||
|             getFuelLevel = function() | ||||
|                 return fuel_level | ||||
|             end, | ||||
|             getItemCount = function() | ||||
|                 return item_count | ||||
|             end, | ||||
|             refuel = function(nLimit) | ||||
|                 item_count = item_count - nLimit | ||||
|                 fuel_level = fuel_level + nLimit | ||||
|             end, | ||||
|             select = function() | ||||
|             end, | ||||
|             getFuelLimit = function() | ||||
|                 return fuel_limit | ||||
|             end, | ||||
|         }) | ||||
|     end | ||||
|  | ||||
|     it("errors when not a turtle", function() | ||||
|         stub(_G, "turtle", nil) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/refuel.lua")) | ||||
|         :matches { ok = true, output = "", error = "Requires a Turtle\n" } | ||||
|     end) | ||||
|  | ||||
|  | ||||
|     it("displays its usage when given too many argument", function() | ||||
|         setup_turtle(0, 5, 0) | ||||
|         expect(capture(stub, "/rom/programs/turtle/refuel.lua a b")) | ||||
|         :matches { ok = true, output = "Usage: /rom/programs/turtle/refuel.lua [number]\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("requires a numeric argument", function() | ||||
|         setup_turtle(0, 0, 0) | ||||
|         expect(capture(stub, "/rom/programs/turtle/refuel.lua nothing")) | ||||
|         :matches { ok = true, output = "Invalid limit, expected a number or \"all\"\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("refuels the turtle", function() | ||||
|         setup_turtle(0, 10, 5) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/refuel.lua 5")) | ||||
|         :matches { ok = true, output = "Fuel level is 5\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("reports when the fuel limit is reached", function() | ||||
|         setup_turtle(0, 5, 5) | ||||
|         expect(capture(stub, "/rom/programs/turtle/refuel.lua 5")) | ||||
|         :matches { ok = true, output = "Fuel level is 5\nFuel limit reached\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("reports when the fuel level is unlimited", function() | ||||
|         setup_turtle("unlimited", 5, 5) | ||||
|         expect(capture(stub, "/rom/programs/turtle/refuel.lua 5")) | ||||
|         :matches { ok = true, output = "Fuel level is unlimited\n", error = "" } | ||||
|     end) | ||||
| end) | ||||
| @@ -1,69 +0,0 @@ | ||||
| local capture = require "test_helpers".capture_program | ||||
|  | ||||
| describe("The turtle unequip program", function() | ||||
|     it("errors when not a turtle", function() | ||||
|         stub(_G, "turtle", nil) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/unequip.lua")) | ||||
|         :matches { ok = true, output = "", error = "Requires a Turtle\n" } | ||||
|     end) | ||||
|  | ||||
|  | ||||
|     it("displays its usage when given no arguments", function() | ||||
|         stub(_G, "turtle", {}) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/unequip.lua")) | ||||
|         :matches { ok = true, output = "Usage: /rom/programs/turtle/unequip.lua <side>\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("says when nothing was unequipped", function() | ||||
|         stub(_G, "turtle", { | ||||
|             select = function() end, | ||||
|             getItemCount = function() return 0 end, | ||||
|             equipRight = function() return true end, | ||||
|             equipLeft = function() return true end, | ||||
|         }) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/unequip.lua left")) | ||||
|         :matches { ok = true, output = "Nothing to unequip\n", error = "" } | ||||
|         expect(capture(stub, "/rom/programs/turtle/unequip.lua right")) | ||||
|         :matches { ok = true, output = "Nothing to unequip\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("unequips a upgrade", function() | ||||
|         local item_count = 0 | ||||
|         stub(_G, "turtle", { | ||||
|             select = function() end, | ||||
|             getItemCount = function() return item_count end, | ||||
|             equipRight = function() | ||||
|                 item_count = 1 | ||||
|                 return true | ||||
|             end, | ||||
|             equipLeft = function() | ||||
|                 item_count = 1 | ||||
|                 return true | ||||
|             end, | ||||
|         }) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/unequip.lua left")) | ||||
|         :matches { ok = true, output = "Item unequipped\n", error = "" } | ||||
|         item_count = 0 | ||||
|         expect(capture(stub, "/rom/programs/turtle/unequip.lua right")) | ||||
|         :matches { ok = true, output = "Item unequipped\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("fails when the turtle is full", function() | ||||
|         stub(_G, "turtle", { | ||||
|             select = function() end, | ||||
|             getItemCount = function() return 1 end, | ||||
|             equipRight = function() return true end, | ||||
|             equipLeft = function() return true end, | ||||
|         }) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/unequip.lua left")) | ||||
|         :matches { ok = true, output = "No space to unequip item\n", error = "" } | ||||
|         expect(capture(stub, "/rom/programs/turtle/unequip.lua right")) | ||||
|         :matches { ok = true, output = "No space to unequip item\n", error = "" } | ||||
|     end) | ||||
|  | ||||
| end) | ||||
		Reference in New Issue
	
	Block a user
	 Merith
					Merith