diff --git a/.luacheckrc b/.luacheckrc index 71b0f872c..3bf2c2c41 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -17,7 +17,8 @@ ignore = { -- are largely unsupported. include_files = { 'src/main/resources/assets/computercraft/lua/rom', - 'src/main/resources/assets/computercraft/lua/bios.lua' + 'src/main/resources/assets/computercraft/lua/bios.lua', + 'src/test/resources/test-rom', } files['src/main/resources/assets/computercraft/lua/bios.lua'] = { diff --git a/.travis.yml b/.travis.yml index 327098845..0e4be822e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,4 +11,4 @@ cache: - $HOME/.gradle/wrapper/s jdk: - - oraclejdk8 + - openjdk8 diff --git a/README.md b/README.md index 283cd4591..f353fe778 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # ![CC: Tweaked](logo.png) -[![Current build status](https://travis-ci.org/SquidDev-CC/CC-Tweaked.svg?branch=master)](https://travis-ci.org/SquidDev-CC/CC-Tweaked "Current build status") [![Download CC: Tweaked on CurseForge](https://cf.way2muchnoise.eu/title/cc-tweaked.svg)](https://minecraft.curseforge.com/projects/cc-tweaked "Download CC: Tweaked on CurseForge") +[![Current build status](https://travis-ci.org/SquidDev-CC/CC-Tweaked.svg?branch=master)](https://travis-ci.org/SquidDev-CC/CC-Tweaked "Current build status") [![Download CC: Tweaked on CurseForge](http://cf.way2muchnoise.eu/title/cc-tweaked.svg)](https://minecraft.curseforge.com/projects/cc-tweaked "Download CC: Tweaked on CurseForge") CC: Tweaked is a fork of [ComputerCraft](https://github.com/dan200/ComputerCraft), adding programmable computers, turtles and more to Minecraft. diff --git a/build.gradle b/build.gradle index b31811486..94f7d11c3 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { mavenCentral() maven { name = "forge" - url = "http://files.minecraftforge.net/maven" + url = "https://files.minecraftforge.net/maven" } } dependencies { @@ -67,7 +67,7 @@ minecraft { repositories { maven { name "JEI" - url "http://dvs1.progwml6.com/files/maven" + url "https://dvs1.progwml6.com/files/maven" } maven { name "SquidDev" @@ -79,7 +79,7 @@ repositories { } maven { name "Amadornes" - url "http://maven.amadornes.com/" + url "https://maven.amadornes.com/" } } @@ -102,8 +102,8 @@ dependencies { shade 'org.squiddev:Cobalt:0.5.0-SNAPSHOT' - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.1.0' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.1.0' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.4.2' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.4.2' deployerJars "org.apache.maven.wagon:wagon-ssh:3.0.0" } @@ -117,6 +117,8 @@ sourceSets { } } +// Compile tasks + javadoc { include "dan200/computercraft/api/**/*.java" } @@ -141,6 +143,14 @@ jar { from configurations.shade.collect { it.isDirectory() ? it : zipTree(it) } } +[compileJava, compileTestJava].forEach { + it.configure { + options.compilerArgs << "-Xlint" << "-Xlint:-processing" << "-Werror" + } +} + + + import java.nio.charset.StandardCharsets import java.nio.file.* import java.util.zip.* @@ -276,7 +286,14 @@ task compressJson(dependsOn: jar) { assemble.dependsOn compressJson -/* Check tasks */ +// Check tasks + +test { + useJUnitPlatform() + testLogging { + events "skipped", "failed" + } +} license { mapping("java", "SLASHSTAR_STYLE") @@ -300,6 +317,13 @@ license { } } +gradle.projectsEvaluated { + tasks.withType(LicenseFormat) { + outputs.upToDateWhen { false } + } +} + + task licenseAPI(type: LicenseCheck); task licenseFormatAPI(type: LicenseFormat); [licenseAPI, licenseFormatAPI].forEach { @@ -310,7 +334,7 @@ task licenseFormatAPI(type: LicenseFormat); } } -/* Upload tasks */ +// Upload tasks task checkRelease { group "upload" @@ -441,23 +465,3 @@ task uploadAll(dependsOn: uploadTasks) { group "upload" description "Uploads to all repositories (Maven, Curse, GitHub release)" } - -test { - useJUnitPlatform() - testLogging { - events "passed", "skipped", "failed" - } -} - -gradle.projectsEvaluated { - reobfJar.dependsOn proguardMove - - tasks.withType(JavaCompile) { - options.compilerArgs << "-Xlint" << "-Xlint:-processing" // Causes Forge build to fail << "-Werror" - } - - tasks.withType(LicenseFormat) { - outputs.upToDateWhen { false } - } -} - diff --git a/gradle.properties b/gradle.properties index c0f4eb432..44330812b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ # Mod properties -mod_version=1.83.1 +mod_version=1.84.0 # Minecraft properties mc_version=1.14.4 diff --git a/src/main/java/dan200/computercraft/api/turtle/ITurtleAccess.java b/src/main/java/dan200/computercraft/api/turtle/ITurtleAccess.java index 7d9b80721..9b1563329 100644 --- a/src/main/java/dan200/computercraft/api/turtle/ITurtleAccess.java +++ b/src/main/java/dan200/computercraft/api/turtle/ITurtleAccess.java @@ -144,7 +144,9 @@ public interface ITurtleAccess GameProfile getOwningPlayer(); /** - * Get the inventory of this turtle + * Get the inventory of this turtle. + * + * Note: this inventory should only be accessed and modified on the server thread. * * @return This turtle's inventory * @see #getItemHandler() @@ -155,6 +157,8 @@ public interface ITurtleAccess /** * Get the inventory of this turtle as an {@link IItemHandlerModifiable}. * + * Note: this inventory should only be accessed and modified on the server thread. + * * @return This turtle's inventory * @see #getInventory() * @see IItemHandlerModifiable diff --git a/src/main/java/dan200/computercraft/api/turtle/ITurtleUpgrade.java b/src/main/java/dan200/computercraft/api/turtle/ITurtleUpgrade.java index e36f9230a..1f4b96154 100644 --- a/src/main/java/dan200/computercraft/api/turtle/ITurtleUpgrade.java +++ b/src/main/java/dan200/computercraft/api/turtle/ITurtleUpgrade.java @@ -98,8 +98,8 @@ public interface ITurtleUpgrade * Will only be called for Tool turtle. Called when turtle.dig() or turtle.attack() is called * by the turtle, and the tool is required to do some work. * - * Conforming implementations should fire {@link BlockEvent.BreakEvent} and {@link TurtleBlockEvent.Dig}for digging, - * {@link AttackEntityEvent} and {@link TurtleAttackEvent} for attacking. + * Conforming implementations should fire {@link BlockEvent.BreakEvent} and {@link TurtleBlockEvent.Dig} for + * digging, {@link AttackEntityEvent} and {@link TurtleAttackEvent} for attacking. * * @param turtle Access to the turtle that the tool resides on. * @param side Which side of the turtle (left or right) the tool resides on. diff --git a/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/DiskDrivePeripheral.java b/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/DiskDrivePeripheral.java index 855486cbd..aadb9f15d 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/DiskDrivePeripheral.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/DiskDrivePeripheral.java @@ -11,6 +11,7 @@ import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.api.media.IMedia; import dan200.computercraft.api.peripheral.IComputerAccess; import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.shared.MediaProviders; import dan200.computercraft.shared.media.items.ItemDisk; import dan200.computercraft.shared.util.StringUtil; import net.minecraft.item.ItemStack; @@ -19,11 +20,11 @@ import javax.annotation.Nonnull; import static dan200.computercraft.core.apis.ArgumentHelper.optString; -public class DiskDrivePeripheral implements IPeripheral +class DiskDrivePeripheral implements IPeripheral { private final TileDiskDrive m_diskDrive; - public DiskDrivePeripheral( TileDiskDrive diskDrive ) + DiskDrivePeripheral( TileDiskDrive diskDrive ) { m_diskDrive = diskDrive; } @@ -55,7 +56,7 @@ public class DiskDrivePeripheral implements IPeripheral } @Override - public Object[] callMethod( @Nonnull IComputerAccess computer, @Nonnull ILuaContext context, int method, @Nonnull Object[] arguments ) throws LuaException + public Object[] callMethod( @Nonnull IComputerAccess computer, @Nonnull ILuaContext context, int method, @Nonnull Object[] arguments ) throws LuaException, InterruptedException { switch( method ) { @@ -63,21 +64,26 @@ public class DiskDrivePeripheral implements IPeripheral return new Object[] { !m_diskDrive.getDiskStack().isEmpty() }; case 1: // getDiskLabel { - IMedia media = m_diskDrive.getDiskMedia(); - return media == null ? null : new Object[] { media.getLabel( m_diskDrive.getDiskStack() ) }; + ItemStack stack = m_diskDrive.getDiskStack(); + IMedia media = MediaProviders.get( stack ); + return media == null ? null : new Object[] { media.getLabel( stack ) }; } case 2: // setDiskLabel { String label = optString( arguments, 0, null ); - IMedia media = m_diskDrive.getDiskMedia(); - if( media == null ) return null; + return context.executeMainThreadTask( () -> { + ItemStack stack = m_diskDrive.getDiskStack(); + IMedia media = MediaProviders.get( stack ); + if( media == null ) return null; - ItemStack disk = m_diskDrive.getDiskStack(); - label = StringUtil.normaliseLabel( label ); - if( !media.setLabel( disk, label ) ) throw new LuaException( "Disk label cannot be changed" ); - m_diskDrive.setDiskStack( disk ); - return null; + if( !media.setLabel( stack, StringUtil.normaliseLabel( label ) ) ) + { + throw new LuaException( "Disk label cannot be changed" ); + } + m_diskDrive.setDiskStack( stack ); + return null; + } ); } case 3: // hasData return new Object[] { m_diskDrive.getDiskMountPath( computer ) != null }; @@ -86,14 +92,16 @@ public class DiskDrivePeripheral implements IPeripheral case 5: { // hasAudio - IMedia media = m_diskDrive.getDiskMedia(); - return new Object[] { media != null && media.getAudio( m_diskDrive.getDiskStack() ) != null }; + ItemStack stack = m_diskDrive.getDiskStack(); + IMedia media = MediaProviders.get( stack ); + return new Object[] { media != null && media.getAudio( stack ) != null }; } case 6: { // getAudioTitle - IMedia media = m_diskDrive.getDiskMedia(); - return new Object[] { media != null ? media.getAudioTitle( m_diskDrive.getDiskStack() ) : false }; + ItemStack stack = m_diskDrive.getDiskStack(); + IMedia media = MediaProviders.get( stack ); + return new Object[] { media != null ? media.getAudioTitle( stack ) : false }; } case 7: // playAudio m_diskDrive.playDiskAudio(); @@ -129,8 +137,7 @@ public class DiskDrivePeripheral implements IPeripheral @Override public boolean equals( IPeripheral other ) { - if( this == other ) return true; - return other instanceof DiskDrivePeripheral && ((DiskDrivePeripheral) other).m_diskDrive == m_diskDrive; + return this == other || other instanceof DiskDrivePeripheral && ((DiskDrivePeripheral) other).m_diskDrive == m_diskDrive; } @Nonnull diff --git a/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/TileDiskDrive.java b/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/TileDiskDrive.java index d424fbde4..5f6c35203 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/TileDiskDrive.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/TileDiskDrive.java @@ -320,35 +320,31 @@ public final class TileDiskDrive extends TileGeneric implements DefaultInventory } @Nonnull - public ItemStack getDiskStack() + ItemStack getDiskStack() { return getStackInSlot( 0 ); } - public void setDiskStack( @Nonnull ItemStack stack ) + void setDiskStack( @Nonnull ItemStack stack ) { setInventorySlotContents( 0, stack ); } - public IMedia getDiskMedia() + private IMedia getDiskMedia() { return MediaProviders.get( getDiskStack() ); } - public String getDiskMountPath( IComputerAccess computer ) + String getDiskMountPath( IComputerAccess computer ) { synchronized( this ) { - if( m_computers.containsKey( computer ) ) - { - MountInfo info = m_computers.get( computer ); - return info.mountPath; - } + MountInfo info = m_computers.get( computer ); + return info != null ? info.mountPath : null; } - return null; } - public void mount( IComputerAccess computer ) + void mount( IComputerAccess computer ) { synchronized( this ) { @@ -357,7 +353,7 @@ public final class TileDiskDrive extends TileGeneric implements DefaultInventory } } - public void unmount( IComputerAccess computer ) + void unmount( IComputerAccess computer ) { synchronized( this ) { @@ -366,7 +362,7 @@ public final class TileDiskDrive extends TileGeneric implements DefaultInventory } } - public void playDiskAudio() + void playDiskAudio() { synchronized( this ) { @@ -379,7 +375,7 @@ public final class TileDiskDrive extends TileGeneric implements DefaultInventory } } - public void stopDiskAudio() + void stopDiskAudio() { synchronized( this ) { @@ -388,7 +384,7 @@ public final class TileDiskDrive extends TileGeneric implements DefaultInventory } } - public void ejectDisk() + void ejectDisk() { synchronized( this ) { diff --git a/src/main/java/dan200/computercraft/shared/peripheral/printer/PrinterPeripheral.java b/src/main/java/dan200/computercraft/shared/peripheral/printer/PrinterPeripheral.java index 0a786b1e2..91ea62895 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/printer/PrinterPeripheral.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/printer/PrinterPeripheral.java @@ -11,6 +11,7 @@ import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.api.peripheral.IComputerAccess; import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.core.terminal.Terminal; +import dan200.computercraft.shared.util.StringUtil; import javax.annotation.Nonnull; @@ -51,8 +52,13 @@ public class PrinterPeripheral implements IPeripheral } @Override - public Object[] callMethod( @Nonnull IComputerAccess computer, @Nonnull ILuaContext context, int method, @Nonnull Object[] args ) throws LuaException + public Object[] callMethod( @Nonnull IComputerAccess computer, @Nonnull ILuaContext context, int method, @Nonnull Object[] args ) throws LuaException, InterruptedException { + // FIXME: There's a theoretical race condition here between getCurrentPage and then using the page. Ideally + // we'd lock on the page, consume it, and unlock. + + // FIXME: None of our page modification functions actually mark the tile as dirty, so the page may not be + // persisted correctly. switch( method ) { case 0: // write @@ -89,10 +95,13 @@ public class PrinterPeripheral implements IPeripheral return new Object[] { width, height }; } case 4: // newPage - return new Object[] { m_printer.startNewPage() }; + return context.executeMainThreadTask( () -> new Object[] { m_printer.startNewPage() } ); case 5: // endPage getCurrentPage(); - return new Object[] { m_printer.endCurrentPage() }; + return context.executeMainThreadTask( () -> { + getCurrentPage(); + return new Object[] { m_printer.endCurrentPage() }; + } ); case 6: // getInkLevel return new Object[] { m_printer.getInkLevel() }; case 7: @@ -100,7 +109,7 @@ public class PrinterPeripheral implements IPeripheral // setPageTitle String title = optString( args, 0, "" ); getCurrentPage(); - m_printer.setPageTitle( title ); + m_printer.setPageTitle( StringUtil.normaliseLabel( title ) ); return null; } case 8: // getPaperLevel @@ -123,13 +132,11 @@ public class PrinterPeripheral implements IPeripheral return m_printer; } + @Nonnull private Terminal getCurrentPage() throws LuaException { Terminal currentPage = m_printer.getCurrentPage(); - if( currentPage == null ) - { - throw new LuaException( "Page not started" ); - } + if( currentPage == null ) throw new LuaException( "Page not started" ); return currentPage; } } diff --git a/src/main/java/dan200/computercraft/shared/peripheral/printer/TilePrinter.java b/src/main/java/dan200/computercraft/shared/peripheral/printer/TilePrinter.java index b525dfbfd..4c6f422ce 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/printer/TilePrinter.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/printer/TilePrinter.java @@ -120,10 +120,7 @@ public final class TilePrinter extends TileGeneric implements DefaultSidedInvent } // Read inventory - synchronized( m_inventory ) - { - ItemStackHelper.loadAllItems( nbt, m_inventory ); - } + ItemStackHelper.loadAllItems( nbt, m_inventory ); } @Nonnull @@ -141,15 +138,12 @@ public final class TilePrinter extends TileGeneric implements DefaultSidedInvent } // Write inventory - synchronized( m_inventory ) - { - ItemStackHelper.saveAllItems( nbt, m_inventory ); - } + ItemStackHelper.saveAllItems( nbt, m_inventory ); return super.write( nbt ); } - public boolean isPrinting() + boolean isPrinting() { return m_printing; } @@ -173,73 +167,59 @@ public final class TilePrinter extends TileGeneric implements DefaultSidedInvent @Nonnull @Override - public ItemStack getStackInSlot( int i ) + public ItemStack getStackInSlot( int slot ) { - return m_inventory.get( i ); + return m_inventory.get( slot ); } @Nonnull @Override - public ItemStack removeStackFromSlot( int i ) + public ItemStack removeStackFromSlot( int slot ) { - synchronized( m_inventory ) - { - ItemStack result = m_inventory.get( i ); - m_inventory.set( i, ItemStack.EMPTY ); - markDirty(); - updateBlockState(); - return result; - } + ItemStack result = m_inventory.get( slot ); + m_inventory.set( slot, ItemStack.EMPTY ); + markDirty(); + updateBlockState(); + return result; } @Nonnull @Override - public ItemStack decrStackSize( int i, int j ) + public ItemStack decrStackSize( int slot, int count ) { - synchronized( m_inventory ) + ItemStack stack = m_inventory.get( slot ); + if( stack.isEmpty() ) return ItemStack.EMPTY; + + if( stack.getCount() <= count ) { - if( m_inventory.get( i ).isEmpty() ) return ItemStack.EMPTY; - - if( m_inventory.get( i ).getCount() <= j ) - { - ItemStack itemstack = m_inventory.get( i ); - m_inventory.set( i, ItemStack.EMPTY ); - markDirty(); - updateBlockState(); - return itemstack; - } - - ItemStack part = m_inventory.get( i ).split( j ); - if( m_inventory.get( i ).isEmpty() ) - { - m_inventory.set( i, ItemStack.EMPTY ); - updateBlockState(); - } - markDirty(); - return part; + setInventorySlotContents( slot, ItemStack.EMPTY ); + return stack; } + + ItemStack part = stack.split( count ); + if( m_inventory.get( slot ).isEmpty() ) + { + m_inventory.set( slot, ItemStack.EMPTY ); + updateBlockState(); + } + markDirty(); + return part; } @Override - public void setInventorySlotContents( int i, @Nonnull ItemStack stack ) + public void setInventorySlotContents( int slot, @Nonnull ItemStack stack ) { - synchronized( m_inventory ) - { - m_inventory.set( i, stack ); - markDirty(); - updateBlockState(); - } + m_inventory.set( slot, stack ); + markDirty(); + updateBlockState(); } @Override public void clear() { - synchronized( m_inventory ) - { - for( int i = 0; i < m_inventory.size(); i++ ) m_inventory.set( i, ItemStack.EMPTY ); - markDirty(); - updateBlockState(); - } + for( int i = 0; i < m_inventory.size(); i++ ) m_inventory.set( i, ItemStack.EMPTY ); + markDirty(); + updateBlockState(); } @Override @@ -290,14 +270,18 @@ public final class TilePrinter extends TileGeneric implements DefaultSidedInvent return new PrinterPeripheral( this ); } - public Terminal getCurrentPage() + @Nullable + Terminal getCurrentPage() { - return m_printing ? m_page : null; + synchronized( m_page ) + { + return m_printing ? m_page : null; + } } - public boolean startNewPage() + boolean startNewPage() { - synchronized( m_inventory ) + synchronized( m_page ) { if( !canInputPage() ) return false; if( m_printing && !outputPage() ) return false; @@ -305,49 +289,36 @@ public final class TilePrinter extends TileGeneric implements DefaultSidedInvent } } - public boolean endCurrentPage() + boolean endCurrentPage() { - synchronized( m_inventory ) + synchronized( m_page ) { - if( m_printing && outputPage() ) - { - return true; - } - } - return false; - } - - public int getInkLevel() - { - synchronized( m_inventory ) - { - ItemStack inkStack = m_inventory.get( 0 ); - return isInk( inkStack ) ? inkStack.getCount() : 0; + return m_printing && outputPage(); } } - public int getPaperLevel() + int getInkLevel() + { + ItemStack inkStack = m_inventory.get( 0 ); + return isInk( inkStack ) ? inkStack.getCount() : 0; + } + + int getPaperLevel() { int count = 0; - synchronized( m_inventory ) + for( int i = 1; i < 7; i++ ) { - for( int i = 1; i < 7; i++ ) - { - ItemStack paperStack = m_inventory.get( i ); - if( !paperStack.isEmpty() && isPaper( paperStack ) ) - { - count += paperStack.getCount(); - } - } + ItemStack paperStack = m_inventory.get( i ); + if( isPaper( paperStack ) ) count += paperStack.getCount(); } return count; } - public void setPageTitle( String title ) + void setPageTitle( String title ) { - if( m_printing ) + synchronized( m_page ) { - m_pageTitle = title; + if( m_printing ) m_pageTitle = title; } } @@ -365,116 +336,100 @@ public final class TilePrinter extends TileGeneric implements DefaultSidedInvent private boolean canInputPage() { - synchronized( m_inventory ) - { - ItemStack inkStack = m_inventory.get( 0 ); - return !inkStack.isEmpty() && isInk( inkStack ) && getPaperLevel() > 0; - } + ItemStack inkStack = m_inventory.get( 0 ); + return !inkStack.isEmpty() && isInk( inkStack ) && getPaperLevel() > 0; } private boolean inputPage() { - synchronized( m_inventory ) + ItemStack inkStack = m_inventory.get( 0 ); + if( !isInk( inkStack ) ) return false; + + for( int i = 1; i < 7; i++ ) { - ItemStack inkStack = m_inventory.get( 0 ); - if( !isInk( inkStack ) ) return false; + ItemStack paperStack = m_inventory.get( i ); + if( paperStack.isEmpty() || !isPaper( paperStack ) ) continue; - for( int i = 1; i < 7; i++ ) + // Setup the new page + DyeColor dye = ColourUtils.getStackColour( inkStack ); + m_page.setTextColour( dye != null ? dye.getId() : 15 ); + + m_page.clear(); + if( paperStack.getItem() instanceof ItemPrintout ) { - ItemStack paperStack = m_inventory.get( i ); - if( !paperStack.isEmpty() && isPaper( paperStack ) ) + m_pageTitle = ItemPrintout.getTitle( paperStack ); + String[] text = ItemPrintout.getText( paperStack ); + String[] textColour = ItemPrintout.getColours( paperStack ); + for( int y = 0; y < m_page.getHeight(); y++ ) { - // Setup the new page - DyeColor dye = ColourUtils.getStackColour( inkStack ); - m_page.setTextColour( dye != null ? dye.getId() : 15 ); - - m_page.clear(); - if( paperStack.getItem() instanceof ItemPrintout ) - { - m_pageTitle = ItemPrintout.getTitle( paperStack ); - String[] text = ItemPrintout.getText( paperStack ); - String[] textColour = ItemPrintout.getColours( paperStack ); - for( int y = 0; y < m_page.getHeight(); y++ ) - { - m_page.setLine( y, text[y], textColour[y], "" ); - } - } - else - { - m_pageTitle = ""; - } - m_page.setCursorPos( 0, 0 ); - - // Decrement ink - inkStack.shrink( 1 ); - if( inkStack.isEmpty() ) m_inventory.set( 0, ItemStack.EMPTY ); - - // Decrement paper - paperStack.shrink( 1 ); - if( paperStack.isEmpty() ) - { - m_inventory.set( i, ItemStack.EMPTY ); - updateBlockState(); - } - - markDirty(); - m_printing = true; - return true; + m_page.setLine( y, text[y], textColour[y], "" ); } } - return false; + else + { + m_pageTitle = ""; + } + m_page.setCursorPos( 0, 0 ); + + // Decrement ink + inkStack.shrink( 1 ); + if( inkStack.isEmpty() ) m_inventory.set( 0, ItemStack.EMPTY ); + + // Decrement paper + paperStack.shrink( 1 ); + if( paperStack.isEmpty() ) + { + m_inventory.set( i, ItemStack.EMPTY ); + updateBlockState(); + } + + markDirty(); + m_printing = true; + return true; } + return false; } private boolean outputPage() { - synchronized( m_page ) + int height = m_page.getHeight(); + String[] lines = new String[height]; + String[] colours = new String[height]; + for( int i = 0; i < height; i++ ) { - int height = m_page.getHeight(); - String[] lines = new String[height]; - String[] colours = new String[height]; - for( int i = 0; i < height; i++ ) - { - lines[i] = m_page.getLine( i ).toString(); - colours[i] = m_page.getTextColourLine( i ).toString(); - } - - ItemStack stack = ItemPrintout.createSingleFromTitleAndText( m_pageTitle, lines, colours ); - synchronized( m_inventory ) - { - for( int slot : BOTTOM_SLOTS ) - { - if( m_inventory.get( slot ).isEmpty() ) - { - setInventorySlotContents( slot, stack ); - m_printing = false; - return true; - } - } - } - return false; + lines[i] = m_page.getLine( i ).toString(); + colours[i] = m_page.getTextColourLine( i ).toString(); } + + ItemStack stack = ItemPrintout.createSingleFromTitleAndText( m_pageTitle, lines, colours ); + for( int slot : BOTTOM_SLOTS ) + { + if( m_inventory.get( slot ).isEmpty() ) + { + setInventorySlotContents( slot, stack ); + m_printing = false; + return true; + } + } + return false; } private void ejectContents() { - synchronized( m_inventory ) + for( int i = 0; i < 13; i++ ) { - for( int i = 0; i < 13; i++ ) + ItemStack stack = m_inventory.get( i ); + if( !stack.isEmpty() ) { - ItemStack stack = m_inventory.get( i ); - if( !stack.isEmpty() ) - { - // Remove the stack from the inventory - setInventorySlotContents( i, ItemStack.EMPTY ); + // Remove the stack from the inventory + setInventorySlotContents( i, ItemStack.EMPTY ); - // Spawn the item in the world - BlockPos pos = getPos(); - double x = pos.getX() + 0.5; - double y = pos.getY() + 0.75; - double z = pos.getZ() + 0.5; - WorldUtil.dropItemStack( stack, getWorld(), x, y, z ); - } + // Spawn the item in the world + BlockPos pos = getPos(); + double x = pos.getX() + 0.5; + double y = pos.getY() + 0.75; + double z = pos.getZ() + 0.5; + WorldUtil.dropItemStack( stack, getWorld(), x, y, z ); } } } @@ -482,25 +437,22 @@ public final class TilePrinter extends TileGeneric implements DefaultSidedInvent private void updateBlockState() { boolean top = false, bottom = false; - synchronized( m_inventory ) + for( int i = 1; i < 7; i++ ) { - for( int i = 1; i < 7; i++ ) + ItemStack stack = m_inventory.get( i ); + if( !stack.isEmpty() && isPaper( stack ) ) { - ItemStack stack = m_inventory.get( i ); - if( !stack.isEmpty() && isPaper( stack ) ) - { - top = true; - break; - } + top = true; + break; } - for( int i = 7; i < 13; i++ ) + } + for( int i = 7; i < 13; i++ ) + { + ItemStack stack = m_inventory.get( i ); + if( !stack.isEmpty() && isPaper( stack ) ) { - ItemStack stack = m_inventory.get( i ); - if( !stack.isEmpty() && isPaper( stack ) ) - { - bottom = true; - break; - } + bottom = true; + break; } } diff --git a/src/main/java/dan200/computercraft/shared/turtle/FurnaceRefuelHandler.java b/src/main/java/dan200/computercraft/shared/turtle/FurnaceRefuelHandler.java index 714eb4e69..9c407c9b5 100644 --- a/src/main/java/dan200/computercraft/shared/turtle/FurnaceRefuelHandler.java +++ b/src/main/java/dan200/computercraft/shared/turtle/FurnaceRefuelHandler.java @@ -31,9 +31,13 @@ public final class FurnaceRefuelHandler implements TurtleRefuelEvent.Handler @Override public int refuel( @Nonnull ITurtleAccess turtle, @Nonnull ItemStack currentStack, int slot, int limit ) { - ItemStack stack = turtle.getItemHandler().extractItem( slot, limit, false ); - int fuelToGive = getFuelPerItem( stack ) * stack.getCount(); + int fuelSpaceLeft = turtle.getFuelLimit() - turtle.getFuelLevel(); + int fuelPerItem = getFuelPerItem( turtle.getItemHandler().getStackInSlot( slot ) ); + int fuelItemLimit = (int) Math.ceil( fuelSpaceLeft / (double) fuelPerItem ); + if( limit > fuelItemLimit ) limit = fuelItemLimit; + ItemStack stack = turtle.getItemHandler().extractItem( slot, limit, false ); + int fuelToGive = fuelPerItem * stack.getCount(); // Store the replacement item in the inventory ItemStack replacementStack = stack.getItem().getContainerItem( stack ); if( !replacementStack.isEmpty() ) diff --git a/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java b/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java index 45ddd84ba..1cb008de4 100644 --- a/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java +++ b/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java @@ -335,9 +335,11 @@ public class TurtleAPI implements ILuaAPI return tryCommand( context, new TurtleInspectCommand( InteractDirection.Up ) ); case 40: // inspectDown return tryCommand( context, new TurtleInspectCommand( InteractDirection.Down ) ); - case 41: + case 41: // getItemDetail { - // getItemDetail + // FIXME: There's a race condition here if the stack is being modified (mutating NBT, etc...) + // on another thread. The obvious solution is to move this into a command, but some programs rely + // on this having a 0-tick delay. int slot = parseOptionalSlotNumber( args, 0, m_turtle.getSelectedSlot() ); ItemStack stack = m_turtle.getInventory().getStackInSlot( slot ); if( stack.isEmpty() ) return new Object[] { null }; diff --git a/src/main/java/dan200/computercraft/shared/turtle/blocks/BlockTurtle.java b/src/main/java/dan200/computercraft/shared/turtle/blocks/BlockTurtle.java index b5572b80c..0e16ef997 100644 --- a/src/main/java/dan200/computercraft/shared/turtle/blocks/BlockTurtle.java +++ b/src/main/java/dan200/computercraft/shared/turtle/blocks/BlockTurtle.java @@ -153,7 +153,7 @@ public class BlockTurtle extends BlockComputerBase implements IWater @Override public float getExplosionResistance( BlockState state, IWorldReader world, BlockPos pos, @Nullable Entity exploder, Explosion explosion ) { - if( getFamily() == ComputerFamily.Advanced && (exploder instanceof LivingEntity || exploder instanceof DamagingProjectileEntity) ) + if( getFamily() == ComputerFamily.Advanced || exploder instanceof LivingEntity || exploder instanceof DamagingProjectileEntity ) { return 2000; } diff --git a/src/main/java/dan200/computercraft/shared/turtle/blocks/TileTurtle.java b/src/main/java/dan200/computercraft/shared/turtle/blocks/TileTurtle.java index 982697aa5..6a6b72d40 100644 --- a/src/main/java/dan200/computercraft/shared/turtle/blocks/TileTurtle.java +++ b/src/main/java/dan200/computercraft/shared/turtle/blocks/TileTurtle.java @@ -49,13 +49,12 @@ import net.minecraftforge.items.wrapper.InvWrapper; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.util.Collections; import static net.minecraftforge.items.CapabilityItemHandler.ITEM_HANDLER_CAPABILITY; public class TileTurtle extends TileComputerBase implements ITurtleTile, DefaultInventory { - // Statics - public static final int INVENTORY_SIZE = 16; public static final int INVENTORY_WIDTH = 4; public static final int INVENTORY_HEIGHT = 4; @@ -70,8 +69,6 @@ public class TileTurtle extends TileComputerBase implements ITurtleTile, Default type -> new TileTurtle( type, ComputerFamily.Advanced ) ); - // Members - enum MoveState { NOT_MOVED, @@ -79,25 +76,20 @@ public class TileTurtle extends TileComputerBase implements ITurtleTile, Default MOVED } - private NonNullList m_inventory; - private NonNullList m_previousInventory; + private final NonNullList m_inventory = NonNullList.withSize( INVENTORY_SIZE, ItemStack.EMPTY ); + private final NonNullList m_previousInventory = NonNullList.withSize( INVENTORY_SIZE, ItemStack.EMPTY ); private final IItemHandlerModifiable m_itemHandler = new InvWrapper( this ); private LazyOptional itemHandlerCap; - private boolean m_inventoryChanged; - private TurtleBrain m_brain; - private MoveState m_moveState; + private boolean m_inventoryChanged = false; + private TurtleBrain m_brain = new TurtleBrain( this ); + private MoveState m_moveState = MoveState.NOT_MOVED; public TileTurtle( TileEntityType type, ComputerFamily family ) { super( type, family ); - m_inventory = NonNullList.withSize( INVENTORY_SIZE, ItemStack.EMPTY ); - m_previousInventory = NonNullList.withSize( INVENTORY_SIZE, ItemStack.EMPTY ); - m_inventoryChanged = false; - m_brain = new TurtleBrain( this ); - m_moveState = MoveState.NOT_MOVED; } - public boolean hasMoved() + private boolean hasMoved() { return m_moveState == MoveState.MOVED; } @@ -237,18 +229,15 @@ public class TileTurtle extends TileComputerBase implements ITurtleTile, Default { super.tick(); m_brain.update(); - synchronized( m_inventory ) + if( !getWorld().isRemote && m_inventoryChanged ) { - if( !getWorld().isRemote && m_inventoryChanged ) - { - ServerComputer computer = getServerComputer(); - if( computer != null ) computer.queueEvent( "turtle_inventory" ); + ServerComputer computer = getServerComputer(); + if( computer != null ) computer.queueEvent( "turtle_inventory" ); - m_inventoryChanged = false; - for( int n = 0; n < getSizeInventory(); n++ ) - { - m_previousInventory.set( n, InventoryUtil.copyItem( getStackInSlot( n ) ) ); - } + m_inventoryChanged = false; + for( int n = 0; n < getSizeInventory(); n++ ) + { + m_previousInventory.set( n, InventoryUtil.copyItem( getStackInSlot( n ) ) ); } } } @@ -288,8 +277,8 @@ public class TileTurtle extends TileComputerBase implements ITurtleTile, Default // Read inventory ListNBT nbttaglist = nbt.getList( "Items", Constants.NBT.TAG_COMPOUND ); - m_inventory = NonNullList.withSize( INVENTORY_SIZE, ItemStack.EMPTY ); - m_previousInventory = NonNullList.withSize( INVENTORY_SIZE, ItemStack.EMPTY ); + m_inventory.clear(); + m_previousInventory.clear(); for( int i = 0; i < nbttaglist.size(); i++ ) { CompoundNBT tag = nbttaglist.getCompound( i ); @@ -396,7 +385,7 @@ public class TileTurtle extends TileComputerBase implements ITurtleTile, Default return m_brain.getToolRenderAngle( side, f ); } - public void setOwningPlayer( GameProfile player ) + void setOwningPlayer( GameProfile player ) { m_brain.setOwningPlayer( player ); markDirty(); @@ -424,109 +413,76 @@ public class TileTurtle extends TileComputerBase implements ITurtleTile, Default @Override public ItemStack getStackInSlot( int slot ) { - if( slot >= 0 && slot < INVENTORY_SIZE ) - { - synchronized( m_inventory ) - { - return m_inventory.get( slot ); - } - } - return ItemStack.EMPTY; + return slot >= 0 && slot < INVENTORY_SIZE ? m_inventory.get( slot ) : ItemStack.EMPTY; } @Nonnull @Override public ItemStack removeStackFromSlot( int slot ) { - synchronized( m_inventory ) - { - ItemStack result = getStackInSlot( slot ); - setInventorySlotContents( slot, ItemStack.EMPTY ); - return result; - } + ItemStack result = getStackInSlot( slot ); + setInventorySlotContents( slot, ItemStack.EMPTY ); + return result; } @Nonnull @Override public ItemStack decrStackSize( int slot, int count ) { - if( count == 0 ) + if( count == 0 ) return ItemStack.EMPTY; + + ItemStack stack = getStackInSlot( slot ); + if( stack.isEmpty() ) return ItemStack.EMPTY; + + if( stack.getCount() <= count ) { - return ItemStack.EMPTY; + setInventorySlotContents( slot, ItemStack.EMPTY ); + return stack; } - synchronized( m_inventory ) - { - ItemStack stack = getStackInSlot( slot ); - if( stack.isEmpty() ) - { - return ItemStack.EMPTY; - } - - if( stack.getCount() <= count ) - { - setInventorySlotContents( slot, ItemStack.EMPTY ); - return stack; - } - - ItemStack part = stack.split( count ); - onInventoryDefinitelyChanged(); - return part; - } + ItemStack part = stack.split( count ); + onInventoryDefinitelyChanged(); + return part; } @Override public void setInventorySlotContents( int i, @Nonnull ItemStack stack ) { - if( i >= 0 && i < INVENTORY_SIZE ) + if( i >= 0 && i < INVENTORY_SIZE && !InventoryUtil.areItemsEqual( stack, m_inventory.get( i ) ) ) { - synchronized( m_inventory ) - { - if( !InventoryUtil.areItemsEqual( stack, m_inventory.get( i ) ) ) - { - m_inventory.set( i, stack ); - onInventoryDefinitelyChanged(); - } - } + m_inventory.set( i, stack ); + onInventoryDefinitelyChanged(); } } @Override public void clear() { - synchronized( m_inventory ) + boolean changed = false; + for( int i = 0; i < INVENTORY_SIZE; i++ ) { - boolean changed = false; - for( int i = 0; i < INVENTORY_SIZE; i++ ) + if( !m_inventory.get( i ).isEmpty() ) { - if( !m_inventory.get( i ).isEmpty() ) - { - m_inventory.set( i, ItemStack.EMPTY ); - changed = true; - } - } - if( changed ) - { - onInventoryDefinitelyChanged(); + m_inventory.set( i, ItemStack.EMPTY ); + changed = true; } } + + if( changed ) onInventoryDefinitelyChanged(); } @Override public void markDirty() { super.markDirty(); - synchronized( m_inventory ) + if( !m_inventoryChanged ) { - if( !m_inventoryChanged ) + for( int n = 0; n < getSizeInventory(); n++ ) { - for( int n = 0; n < getSizeInventory(); n++ ) + if( !ItemStack.areItemStacksEqual( getStackInSlot( n ), m_previousInventory.get( n ) ) ) { - if( !ItemStack.areItemStacksEqual( getStackInSlot( n ), m_previousInventory.get( n ) ) ) - { - m_inventoryChanged = true; - break; - } + m_inventoryChanged = true; + break; } } } @@ -587,8 +543,8 @@ public class TileTurtle extends TileComputerBase implements ITurtleTile, Default public void transferStateFrom( TileTurtle copy ) { super.transferStateFrom( copy ); - m_inventory = copy.m_inventory; - m_previousInventory = copy.m_previousInventory; + Collections.copy( m_inventory, copy.m_inventory ); + Collections.copy( m_previousInventory, copy.m_previousInventory ); m_inventoryChanged = copy.m_inventoryChanged; m_brain = copy.m_brain; m_brain.setOwner( this ); diff --git a/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleTool.java b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleTool.java index 5f82731b9..153954e56 100644 --- a/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleTool.java +++ b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleTool.java @@ -248,7 +248,7 @@ public class TurtleTool extends AbstractTurtleUpgrade boolean canHarvest = state.canHarvestBlock( world, blockPosition, turtlePlayer ); boolean canBreak = state.removedByPlayer( world, blockPosition, turtlePlayer, canHarvest, fluidState ); if( canBreak ) state.getBlock().onPlayerDestroy( world, blockPosition, state ); - if( canHarvest ) + if( canHarvest && canBreak ) { state.getBlock().harvestBlock( world, turtlePlayer, blockPosition, state, tile, turtlePlayer.getHeldItemMainhand() ); } diff --git a/src/main/resources/assets/computercraft/lua/rom/modules/main/cc/expect.lua b/src/main/resources/assets/computercraft/lua/rom/modules/main/cc/expect.lua new file mode 100644 index 000000000..14db7231c --- /dev/null +++ b/src/main/resources/assets/computercraft/lua/rom/modules/main/cc/expect.lua @@ -0,0 +1,46 @@ +--- The @{craftos.expect} library provides helper functions for verifying that +-- function arguments are well-formed and of the correct type. +-- +-- @module craftos.expect + +local native_select, native_type = select, type + +--- Expect an argument to have a specific type. +-- +-- @tparam int index The 1-based argument index. +-- @param value The argument's value. +-- @tparam string ... The allowed types of the argument. +-- @throws If the value is not one of the allowed types. +local function expect(index, value, ...) + local t = native_type(value) + for i = 1, native_select("#", ...) do + if t == native_select(i, ...) then return true end + end + + local types = table.pack(...) + for i = types.n, 1, -1 do + if types[i] == "nil" then table.remove(types, i) end + end + + local type_names + if #types <= 1 then + type_names = tostring(...) + else + type_names = table.concat(types, ", ", 1, #types - 1) .. " or " .. types[#types] + end + + -- If we can determine the function name with a high level of confidence, try to include it. + local name + if native_type(debug) == "table" and native_type(debug.getinfo) == "function" then + local ok, info = pcall(debug.getinfo, 3, "nS") + if ok and info.name and #info.name ~= "" and info.what ~= "C" then name = info.name end + end + + if name then + error( ("bad argument #%d to '%s' (expected %s, got %s)"):format(index, name, type_names, t), 3 ) + else + error( ("bad argument #%d (expected %s, got %s)"):format(index, type_names, t), 3 ) + end +end + +return { expect = expect } diff --git a/src/main/resources/data/computercraft/lua/bios.lua b/src/main/resources/data/computercraft/lua/bios.lua index 5f24d94cb..417b55bc9 100644 --- a/src/main/resources/data/computercraft/lua/bios.lua +++ b/src/main/resources/data/computercraft/lua/bios.lua @@ -1,48 +1,19 @@ -local native_select, native_type = select, type - ---- Expect an argument to have a specific type. +-- Load in expect from the module path. -- --- @tparam int index The 1-based argument index. --- @param value The argument's value. --- @tparam string ... The allowed types of the argument. --- @throws If the value is not one of the allowed types. -local function expect(index, value, ...) - local t = native_type(value) - for i = 1, native_select("#", ...) do - if t == native_select(i, ...) then return true end - end +-- Ideally we'd use require, but that is part of the shell, and so is not +-- available to the BIOS or any APIs. All APIs load this using dofile, but that +-- has not been defined at this point. +local expect - local types = table.pack(...) - for i = types.n, 1, -1 do - if types[i] == "nil" then table.remove(types, i) end - end +do + local h = fs.open("rom/modules/main/cc/expect.lua", "r") + local f, err = loadstring(h.readAll(), "@expect.lua") + h.close() - local type_names - if #types <= 1 then - type_names = tostring(...) - else - type_names = table.concat(types, ", ", 1, #types - 1) .. " or " .. types[#types] - end - - -- If we can determine the function name with a high level of confidence, try to include it. - local name - if native_type(debug) == "table" and native_type(debug.getinfo) == "function" then - local ok, info = pcall(debug.getinfo, 3, "nS") - if ok and info.name and #info.name ~= "" and info.what ~= "C" then name = info.name end - end - - if name then - error( ("bad argument #%d to '%s' (expected %s, got %s)"):format(index, name, type_names, t), 3 ) - else - error( ("bad argument #%d (expected %s, got %s)"):format(index, type_names, t), 3 ) - end + if not f then error(err) end + expect = f().expect end --- We expose expect in the global table as APIs need to access it, but give it --- a non-identifier name - meaning it does not show up in auto-completion. --- expect is an internal function, and should not be used by users. -_G["~expect"] = expect - if _VERSION == "Lua 5.1" then -- If we're on Lua 5.1, install parts of the Lua 5.2/5.3 API so that programs can be written against it local type = type @@ -568,23 +539,28 @@ function read( _sReplaceChar, _tHistory, _fnComplete, _sDefault ) return sLine end -function loadfile( _sFile, _tEnv ) - expect(1, _sFile, "string") - expect(2, _tEnv, "table", "nil") - - local file = fs.open( _sFile, "r" ) - if file then - local func, err = load( file.readAll(), "@" .. fs.getName( _sFile ), "t", _tEnv ) - file.close() - return func, err +function loadfile( filename, mode, env ) + -- Support the previous `loadfile(filename, env)` form instead. + if type(mode) == "table" and env == nil then + mode, env = nil, mode end - return nil, "File not found" + + expect(1, filename, "string") + expect(2, mode, "string", "nil") + expect(3, env, "table", "nil") + + local file = fs.open( filename, "r" ) + if not file then return nil, "File not found" end + + local func, err = load( file.readAll(), "@" .. fs.getName( filename ), mode, env ) + file.close() + return func, err end function dofile( _sFile ) expect(1, _sFile, "string") - local fnFile, e = loadfile( _sFile, _G ) + local fnFile, e = loadfile( _sFile, nil, _G ) if fnFile then return fnFile() else @@ -600,7 +576,7 @@ function os.run( _tEnv, _sPath, ... ) local tArgs = table.pack( ... ) local tEnv = _tEnv setmetatable( tEnv, { __index = _G } ) - local fnFile, err = loadfile( _sPath, tEnv ) + local fnFile, err = loadfile( _sPath, nil, tEnv ) if fnFile then local ok, err = pcall( function() fnFile( table.unpack( tArgs, 1, tArgs.n ) ) @@ -634,7 +610,7 @@ function os.loadAPI( _sPath ) local tEnv = {} setmetatable( tEnv, { __index = _G } ) - local fnAPI, err = loadfile( _sPath, tEnv ) + local fnAPI, err = loadfile( _sPath, nil, tEnv ) if fnAPI then local ok, err = pcall( fnAPI ) if not ok then diff --git a/src/main/resources/data/computercraft/lua/rom/apis/colors.lua b/src/main/resources/data/computercraft/lua/rom/apis/colors.lua index c113d324a..969f984e5 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/colors.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/colors.lua @@ -1,4 +1,4 @@ -local expect = _G["~expect"] +local expect = dofile("rom/modules/main/cc/expect.lua").expect -- Colors white = 1 diff --git a/src/main/resources/data/computercraft/lua/rom/apis/gps.lua b/src/main/resources/data/computercraft/lua/rom/apis/gps.lua index d27ba1beb..08f698eb2 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/gps.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/gps.lua @@ -1,4 +1,4 @@ -local expect = _G["~expect"] +local expect = dofile("rom/modules/main/cc/expect.lua").expect CHANNEL_GPS = 65534 diff --git a/src/main/resources/data/computercraft/lua/rom/apis/help.lua b/src/main/resources/data/computercraft/lua/rom/apis/help.lua index 63ca4d1d6..b4e69921f 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/help.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/help.lua @@ -1,4 +1,4 @@ -local expect = _G["~expect"] +local expect = dofile("rom/modules/main/cc/expect.lua").expect local sPath = "/rom/help" diff --git a/src/main/resources/data/computercraft/lua/rom/apis/io.lua b/src/main/resources/data/computercraft/lua/rom/apis/io.lua index cd8a02acb..efbc13b40 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/io.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/io.lua @@ -1,6 +1,6 @@ -- Definition for the IO API -local expect, typeOf = _G["~expect"], _G.type +local expect, typeOf = dofile("rom/modules/main/cc/expect.lua").expect, _G.type --- If we return nil then close the file, as we've reached the end. -- We use this weird wrapper function as we wish to preserve the varargs diff --git a/src/main/resources/data/computercraft/lua/rom/apis/keys.lua b/src/main/resources/data/computercraft/lua/rom/apis/keys.lua index ffe2ddcf7..06dc1c6d1 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/keys.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/keys.lua @@ -8,7 +8,7 @@ -- taught me anything, it's that emulating LWJGL's weird key handling is nigh-on -- impossible. -local expect = _G["~expect"] +local expect = dofile("rom/modules/main/cc/expect.lua").expect local tKeys = {} tKeys[32] = 'space' diff --git a/src/main/resources/data/computercraft/lua/rom/apis/paintutils.lua b/src/main/resources/data/computercraft/lua/rom/apis/paintutils.lua index fe475a6fa..0988945e0 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/paintutils.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/paintutils.lua @@ -1,4 +1,4 @@ -local expect = _G["~expect"] +local expect = dofile("rom/modules/main/cc/expect.lua").expect local function drawPixelInternal( xPos, yPos ) term.setCursorPos( xPos, yPos ) diff --git a/src/main/resources/data/computercraft/lua/rom/apis/peripheral.lua b/src/main/resources/data/computercraft/lua/rom/apis/peripheral.lua index c34a59eb8..3f4fdc150 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/peripheral.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/peripheral.lua @@ -1,4 +1,4 @@ -local expect = _G["~expect"] +local expect = dofile("rom/modules/main/cc/expect.lua").expect local native = peripheral diff --git a/src/main/resources/data/computercraft/lua/rom/apis/rednet.lua b/src/main/resources/data/computercraft/lua/rom/apis/rednet.lua index 8867e4caf..5af3a4706 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/rednet.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/rednet.lua @@ -1,4 +1,4 @@ -local expect = _G["~expect"] +local expect = dofile("rom/modules/main/cc/expect.lua").expect CHANNEL_BROADCAST = 65535 CHANNEL_REPEAT = 65533 diff --git a/src/main/resources/data/computercraft/lua/rom/apis/settings.lua b/src/main/resources/data/computercraft/lua/rom/apis/settings.lua index 3290a03d3..d60510937 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/settings.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/settings.lua @@ -1,4 +1,4 @@ -local expect = _G["~expect"] +local expect = dofile("rom/modules/main/cc/expect.lua").expect local tSettings = {} diff --git a/src/main/resources/data/computercraft/lua/rom/apis/term.lua b/src/main/resources/data/computercraft/lua/rom/apis/term.lua index d93c3f367..e9ad00408 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/term.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/term.lua @@ -1,4 +1,4 @@ -local expect = _G["~expect"] +local expect = dofile("rom/modules/main/cc/expect.lua").expect local native = (term.native and term.native()) or term local redirectTarget = native diff --git a/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua b/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua index ba9f5e033..447babb44 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua @@ -1,4 +1,4 @@ -local expect = _G["~expect"] +local expect = dofile("rom/modules/main/cc/expect.lua").expect function slowWrite( sText, nRate ) expect(2, nRate, "number", "nil") diff --git a/src/main/resources/data/computercraft/lua/rom/apis/window.lua b/src/main/resources/data/computercraft/lua/rom/apis/window.lua index 6d9ab38ec..b1e1b3db4 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/window.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/window.lua @@ -1,4 +1,4 @@ -local expect = _G["~expect"] +local expect = dofile("rom/modules/main/cc/expect.lua").expect local tHex = { [ colors.white ] = "0", @@ -388,6 +388,16 @@ function create( parent, nX, nY, nWidth, nHeight, bStartVisible ) return nBackgroundColor end + function window.getLine(y) + if type(y) ~= "number" then expect(1, y, "number") end + + if y < 1 or y > nHeight then + error("Line is out of range.", 2) + end + + return tLines[y].text, tLines[y].textColor, tLines[y].backgroundColor + end + -- Other functions function window.setVisible( bVis ) if type(bVis) ~= "boolean" then expect(1, bVis, "boolean") end diff --git a/src/main/resources/data/computercraft/lua/rom/help/changelog.txt b/src/main/resources/data/computercraft/lua/rom/help/changelog.txt index a9e543539..c9fb3ad1b 100644 --- a/src/main/resources/data/computercraft/lua/rom/help/changelog.txt +++ b/src/main/resources/data/computercraft/lua/rom/help/changelog.txt @@ -1,3 +1,20 @@ +New features in CC: Tweaked 1.84.0 + +* Improve validation in rename, copy and delete programs +* Add window.getLine - the inverse of blit +* turtle.refuel no longer consumes more fuel than needed +* Add "cc.expect" module, for improved argument type checks +* Mount the ROM from all mod jars, not just CC's + +And several bug fixes: +* Ensure file error messages use the absolute correct path +* Fix NPE when closing a file multiple times. +* Do not load chunks when calling writeDescription. +* Fix the signature of loadfile +* Fix turtles harvesting blocks multiple times +* Improve thread-safety of various peripherals +* Prevent printed pages having massive/malformed titles + # New features in CC: Tweaked 1.83.1 * Add several new MOTD messages (JakobDev) diff --git a/src/main/resources/data/computercraft/lua/rom/help/whatsnew.txt b/src/main/resources/data/computercraft/lua/rom/help/whatsnew.txt index 55e2ac007..20957867e 100644 --- a/src/main/resources/data/computercraft/lua/rom/help/whatsnew.txt +++ b/src/main/resources/data/computercraft/lua/rom/help/whatsnew.txt @@ -1,10 +1,18 @@ -New features in CC: Tweaked 1.83.1 +New features in CC: Tweaked 1.84.0 -* Add several new MOTD messages (JakobDev) +* Improve validation in rename, copy and delete programs +* Add window.getLine - the inverse of blit +* turtle.refuel no longer consumes more fuel than needed +* Add "cc.expect" module, for improved argument type checks +* Mount the ROM from all mod jars, not just CC's And several bug fixes: -* Fix type check in `rednet.lookup` -* Error if turtle and pocket computer programs are run on the wrong system (JakobDev) -* Do not discard varargs after a nil. +* Ensure file error messages use the absolute correct path +* Fix NPE when closing a file multiple times. +* Do not load chunks when calling writeDescription. +* Fix the signature of loadfile +* Fix turtles harvesting blocks multiple times +* Improve thread-safety of various peripherals +* Prevent printed pages having massive/malformed titles Type "help changelog" to see the full version history. diff --git a/src/main/resources/data/computercraft/lua/rom/help/window.txt b/src/main/resources/data/computercraft/lua/rom/help/window.txt index 7fcf5d43e..ea3ea3d78 100644 --- a/src/main/resources/data/computercraft/lua/rom/help/window.txt +++ b/src/main/resources/data/computercraft/lua/rom/help/window.txt @@ -23,3 +23,4 @@ getPosition() reposition( x, y, width, height ) getPaletteColor( color ) setPaletteColor( color, r, g, b ) +getLine() diff --git a/src/main/resources/data/computercraft/lua/rom/programs/advanced/multishell.lua b/src/main/resources/data/computercraft/lua/rom/programs/advanced/multishell.lua index 00988eb27..51c1c1753 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/advanced/multishell.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/advanced/multishell.lua @@ -1,4 +1,4 @@ -local expect = _G["~expect"] +local expect = dofile("rom/modules/main/cc/expect.lua").expect -- Setup process switching local parentTerm = term.current() diff --git a/src/main/resources/data/computercraft/lua/rom/programs/edit.lua b/src/main/resources/data/computercraft/lua/rom/programs/edit.lua index 43910d56a..e481dd9cd 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/edit.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/edit.lua @@ -329,7 +329,7 @@ local tMenuFuncs = { printer.setPageTitle( sName.." (page "..nPage..")" ) end - while not printer.newPage() do + while not printer.newPage() do if printer.getInkLevel() < 1 then sStatus = "Printer out of ink, please refill" elseif printer.getPaperLevel() < 1 then @@ -342,7 +342,6 @@ local tMenuFuncs = { redrawMenu() term.redirect( printerTerminal ) - local timer = os.startTimer(0.5) sleep(0.5) end diff --git a/src/main/resources/data/computercraft/lua/rom/programs/shell.lua b/src/main/resources/data/computercraft/lua/rom/programs/shell.lua index bc95dbf2e..3c15e04b3 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/shell.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/shell.lua @@ -1,4 +1,4 @@ -local expect = _G["~expect"] +local expect = dofile("rom/modules/main/cc/expect.lua").expect local multishell = multishell local parentShell = shell @@ -56,7 +56,7 @@ local function createShellEnv( sDir ) sPath = fs.combine(sDir, sPath) end if fs.exists(sPath) and not fs.isDir(sPath) then - local fnFile, sError = loadfile( sPath, tEnv ) + local fnFile, sError = loadfile( sPath, nil, tEnv ) if fnFile then return fnFile, sPath else diff --git a/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java b/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java index 86cbd6c03..7f083c7ee 100644 --- a/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java +++ b/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java @@ -90,7 +90,7 @@ public class ComputerTestDelegate try( WritableByteChannel channel = mount.openChannelForWrite( "startup.lua" ); Writer writer = Channels.newWriter( channel, StandardCharsets.UTF_8.newEncoder(), -1 ) ) { - writer.write( "loadfile('test/mcfly.lua', _ENV)('test/spec') cct_test.finish()" ); + writer.write( "loadfile('test/mcfly.lua', nil, _ENV)('test/spec') cct_test.finish()" ); } computer = new Computer( new BasicEnvironment( mount ), term, 0 ); diff --git a/src/test/java/dan200/computercraft/core/computer/ComputerBootstrap.java b/src/test/java/dan200/computercraft/core/computer/ComputerBootstrap.java index 79b67eba2..a0e044bb2 100644 --- a/src/test/java/dan200/computercraft/core/computer/ComputerBootstrap.java +++ b/src/test/java/dan200/computercraft/core/computer/ComputerBootstrap.java @@ -33,7 +33,7 @@ public class ComputerBootstrap { MemoryMount mount = new MemoryMount() .addFile( "test.lua", program ) - .addFile( "startup", "assertion.assert(pcall(loadfile('test.lua', _ENV))) os.shutdown()" ); + .addFile( "startup", "assertion.assert(pcall(loadfile('test.lua', nil, _ENV))) os.shutdown()" ); run( mount, x -> { } ); } diff --git a/src/test/resources/test-rom/mcfly.lua b/src/test/resources/test-rom/mcfly.lua index 38ebf9e83..e46d2dc4e 100644 --- a/src/test/resources/test-rom/mcfly.lua +++ b/src/test/resources/test-rom/mcfly.lua @@ -27,18 +27,58 @@ local function check(func, arg, ty, val) end end +--- A stub - wraps a value within a a table, +local stub_mt = {} +stub_mt.__index = stub_mt + +--- Revert this stub, restoring the previous value. +-- +-- Note, a stub can only be reverted once. +function stub_mt:revert() + if not self.active then return end + + self.active = false + rawset(self.stubbed_in, self.key, self.original) +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) - check('stub', 1, 'table', tbl) - check('stub', 2, 'string', var) +local function default_stub() end - table.insert(active_stubs, { tbl = tbl, var = var, value = tbl[var] }) - rawset(tbl, var, value) +--- Stub a table entry with a new value. +-- +-- @tparam table +-- @tparam string key The variable to stub +-- @param[opt] value The value to stub it with. If this is a function, one can +-- use the various stub expectation methods to determine what it was called +-- with. Defaults to an empty function - pass @{nil} in explicitly to set the +-- value to nil. +-- @treturn Stub The resulting stub +local function stub(tbl, key, ...) + check('stub', 1, 'table', tbl) + check('stub', 2, 'string', key) + + local stub = setmetatable({ + active = true, + stubbed_in = tbl, + key = key, + original = rawget(tbl, key), + }, stub_mt) + + local value = ... + if select('#', ...) == 0 then value = default_stub end + if type(value) == "function" then + local arguments, delegate = {}, value + stub.arguments = arguments + value = function(...) + arguments[#arguments + 1] = table.pack(...) + return delegate(...) + end + end + + table.insert(active_stubs, stub) + rawset(tbl, key, value) + return stub end --- Capture the current global state of the computer @@ -51,16 +91,14 @@ local function push_state() output = io.output(), dir = shell.dir(), path = shell.path(), + aliases = shell.aliases(), 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] - rawset(stub.tbl, stub.var, stub.value) - end + for i = #active_stubs, 1, -1 do active_stubs[i]:revert() end active_stubs = state.stubs @@ -69,6 +107,14 @@ local function pop_state(state) io.output(state.output) shell.setDir(state.dir) shell.setPath(state.path) + + local aliases = shell.aliases() + for k in pairs(aliases) do + if not state.aliases[k] then shell.clearAlias(k) end + end + for k, v in pairs(state.aliases) do + if aliases[k] ~= v then shell.setAlias(k, v) end + end end local error_mt = { __tostring = function(self) return self.message end } @@ -210,6 +256,16 @@ local function matches(eq, exact, left, right) return true end +local function pairwise_equal(left, right) + if left.n ~= right.n then return false end + + for i = 1, left.n do + if left[i] ~= right[i] then return false end + end + + return true +end + --- Assert that this expectation is structurally equivalent to -- the provided object. -- @@ -236,6 +292,70 @@ function expect_mt:matches(value) return self end +--- Assert that this stub was called a specific number of times. +-- +-- @tparam[opt] number The exact number of times the function must be called. +-- If not given just require the function to be called at least once. +-- @raises If this function was not called the expected number of times. +function expect_mt:called(times) + if getmetatable(self.value) ~= stub_mt or self.value.arguments == nil then + fail(("Expected stubbed function, got %s"):format(type(self.value))) + end + + local called = #self.value.arguments + + if times == nil then + if called == 0 then + fail("Expected stub to be called\nbut it was not.") + end + else + check('stub', 1, 'number', times) + if called ~= times then + fail(("Expected stub to be called %d times\nbut was called %d times."):format(times, called)) + end + end + + return self +end + +local function called_with_check(eq, self, ...) + if getmetatable(self.value) ~= stub_mt or self.value.arguments == nil then + fail(("Expected stubbed function, got %s"):format(type(self.value))) + end + + local exp_args = table.pack(...) + local actual_args = self.value.arguments + for i = 1, #actual_args do + if eq(actual_args[i], exp_args) then return self end + end + + local head = ("Expected stub to be called with %s\nbut was"):format(format(exp_args)) + if #actual_args == 0 then + fail(head .. " not called at all") + elseif #actual_args == 1 then + fail(("%s called with %s."):format(head, format(actual_args[1]))) + else + local lines = { head .. " called with:" } + for i = 1, #actual_args do lines[i + 1] = " - " .. format(actual_args[i]) end + + fail(table.concat(lines, "\n")) + end +end + +--- Assert that this stub was called with a set of arguments +-- +-- Arguments are compared using exact equality. +function expect_mt:called_with(...) + return called_with_check(pairwise_equal, self, ...) +end + +--- Assert that this stub was called with a set of arguments +-- +-- Arguments are compared using matching. +function expect_mt:called_with_matching(...) + return called_with_check(matches, self, ...) +end + local expect = setmetatable( { --- Construct an expectation on the error message calling this function -- produces @@ -381,7 +501,7 @@ do if fs.isDir(file) then run_in(file) elseif file:sub(-#suffix) == suffix then - local fun, err = loadfile(file, env) + local fun, err = loadfile(file, nil, env) if not fun then do_test { name = file:sub(#root_dir + 2), error = { message = err } } else diff --git a/src/test/resources/test-rom/spec/apis/window_spec.lua b/src/test/resources/test-rom/spec/apis/window_spec.lua index 16fe229e1..2fb6d119d 100644 --- a/src/test/resources/test-rom/spec/apis/window_spec.lua +++ b/src/test/resources/test-rom/spec/apis/window_spec.lua @@ -120,4 +120,21 @@ describe("The window library", function() expect.error(w.reposition, 1, 1, 1, nil):eq("bad argument #4 (expected number, got nil)") 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) end) diff --git a/src/test/resources/test-rom/spec/base_spec.lua b/src/test/resources/test-rom/spec/base_spec.lua index 191d87eab..43de8239d 100644 --- a/src/test/resources/test-rom/spec/base_spec.lua +++ b/src/test/resources/test-rom/spec/base_spec.lua @@ -1,36 +1,4 @@ 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) @@ -48,18 +16,43 @@ describe("The Lua base library", function() end) describe("loadfile", function() + local function make_file() + local tmp = fs.open("test-files/out.lua", "w") + tmp.write("return _ENV") + tmp.close() + end + it("validates arguments", function() loadfile("") - loadfile("", {}) + 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)") + expect.error(loadfile, "", false):eq("bad argument #2 (expected string, got boolean)") + expect.error(loadfile, "", "", false):eq("bad argument #3 (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) + + it("loads a file with the global environment", function() + make_file() + expect(loadfile("test-files/out.lua")()):eq(_G) + end) + + it("loads a file with a specific environment", function() + make_file() + local env = {} + expect(loadfile("test-files/out.lua", nil, env)()):eq(env) + end) + + it("supports the old-style argument form", function() + make_file() + local env = {} + expect(loadfile("test-files/out.lua", env)()):eq(env) + end) end) describe("dofile", function() diff --git a/src/test/resources/test-rom/spec/modules/cc/expect_spec.lua b/src/test/resources/test-rom/spec/modules/cc/expect_spec.lua new file mode 100644 index 000000000..36f076f60 --- /dev/null +++ b/src/test/resources/test-rom/spec/modules/cc/expect_spec.lua @@ -0,0 +1,31 @@ +describe("cc.expect", function() + local e = require("cc.expect") + + it("checks a single type", function() + expect(e.expect(1, "test", "string")):eq(true) + expect(e.expect(1, 2, "number")):eq(true) + + expect.error(e.expect, 1, nil, "string"):eq("bad argument #1 (expected string, got nil)") + expect.error(e.expect, 2, 1, "nil"):eq("bad argument #2 (expected nil, got number)") + end) + + it("checks multiple types", function() + expect(e.expect(1, "test", "string", "number")):eq(true) + expect(e.expect(1, 2, "string", "number")):eq(true) + + expect.error(e.expect, 1, nil, "string", "number"):eq("bad argument #1 (expected string or number, got nil)") + expect.error(e.expect, 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.expect(1, nil, "string")):eq(true) + end + local function trampoline() + worker() + end + + expect.error(trampoline):eq("expect_spec.lua:26: bad argument #1 to 'worker' (expected string, got nil)") + end) +end) diff --git a/src/test/resources/test-rom/spec/programs/advanced/bg_spec.lua b/src/test/resources/test-rom/spec/programs/advanced/bg_spec.lua new file mode 100644 index 000000000..a53dc2f2d --- /dev/null +++ b/src/test/resources/test-rom/spec/programs/advanced/bg_spec.lua @@ -0,0 +1,11 @@ +local capture = require "test_helpers".capture_program + +describe("The bg program", function() + it("opens a tab in the background", function() + local openTab = stub(shell, "openTab", function() return 12 end) + local switchTab = stub(shell, "switchTab") + capture(stub, "bg") + expect(openTab):called_with("shell") + expect(switchTab):called(0) + end) +end) diff --git a/src/test/resources/test-rom/spec/programs/advanced/fg_spec.lua b/src/test/resources/test-rom/spec/programs/advanced/fg_spec.lua new file mode 100644 index 000000000..8e5cc4f26 --- /dev/null +++ b/src/test/resources/test-rom/spec/programs/advanced/fg_spec.lua @@ -0,0 +1,11 @@ +local capture = require "test_helpers".capture_program + +describe("The fg program", function() + it("opens the shell in the foreground", function() + local openTab = stub(shell, "openTab", function() return 12 end) + local switchTab = stub(shell, "switchTab") + capture(stub, "fg") + expect(openTab):called_with("shell") + expect(switchTab):called_with(12) + end) +end) diff --git a/src/test/resources/test-rom/spec/programs/alias_spec.lua b/src/test/resources/test-rom/spec/programs/alias_spec.lua new file mode 100644 index 000000000..95a2c2556 --- /dev/null +++ b/src/test/resources/test-rom/spec/programs/alias_spec.lua @@ -0,0 +1,28 @@ +local capture = require "test_helpers".capture_program + +describe("The alias program", function() + it("displays its usage when given too many arguments", function() + expect(capture(stub, "alias a b c")) + :matches { ok = true, output = "Usage: alias \n", error = "" } + end) + + it("lists aliases", function() + local pagedTabulate = stub(textutils, "pagedTabulate", function(x) print(table.unpack(x)) end) + stub(shell, "aliases", function() return { cp = "copy" } end) + expect(capture(stub, "alias")) + :matches { ok = true, output = "cp:copy\n", error = "" } + expect(pagedTabulate):called_with_matching({ "cp:copy" }) + end) + + it("sets an alias", function() + local setAlias = stub(shell, "setAlias") + capture(stub, "alias test Hello") + expect(setAlias):called_with("test", "Hello") + end) + + it("clears an alias", function() + local clearAlias = stub(shell, "clearAlias") + capture(stub, "alias test") + expect(clearAlias):called_with("test") + end) +end) diff --git a/src/test/resources/test-rom/spec/programs/cd_spec.lua b/src/test/resources/test-rom/spec/programs/cd_spec.lua index 2826b8007..348d62944 100644 --- a/src/test/resources/test-rom/spec/programs/cd_spec.lua +++ b/src/test/resources/test-rom/spec/programs/cd_spec.lua @@ -1,19 +1,18 @@ local capture = require "test_helpers".capture_program describe("The cd program", function() - - it("cd into a directory", function() - shell.run("cd /rom/programs") - - expect(shell.dir()):eq("rom/programs") + it("changes into a directory", function() + local setDir = stub(shell, "setDir") + capture(stub, "cd /rom/programs") + expect(setDir):called_with("rom/programs") end) - it("cd into a not existing directory", function() + it("does not move into a non-existent directory", function() expect(capture(stub, "cd /rom/nothing")) :matches { ok = true, output = "Not a directory\n", error = "" } end) - - it("displays the usage with no arguments", function() + + it("displays the usage when given no arguments", function() expect(capture(stub, "cd")) :matches { ok = true, output = "Usage: cd \n", error = "" } end) diff --git a/src/test/resources/test-rom/spec/programs/clear_spec.lua b/src/test/resources/test-rom/spec/programs/clear_spec.lua new file mode 100644 index 000000000..f339c83ce --- /dev/null +++ b/src/test/resources/test-rom/spec/programs/clear_spec.lua @@ -0,0 +1,13 @@ +local capture = require "test_helpers".capture_program + +describe("The clear program", function() + it("clears the screen", function() + local clear = stub(term, "clear") + local setCursorPos = stub(term, "setCursorPos") + + capture(stub, "clear") + + expect(clear):called(1) + expect(setCursorPos):called_with(1, 1) + end) +end) diff --git a/src/test/resources/test-rom/spec/programs/command/commands_spec.lua b/src/test/resources/test-rom/spec/programs/command/commands_spec.lua new file mode 100644 index 000000000..d7261b9d1 --- /dev/null +++ b/src/test/resources/test-rom/spec/programs/command/commands_spec.lua @@ -0,0 +1,20 @@ +local capture = require "test_helpers".capture_program + +describe("The commands program", function() + it("displays an error without the commands api", function() + stub(_G, "commands", nil) + expect(capture(stub, "/rom/programs/command/commands.lua")) + :matches { ok = true, output = "", error = "Requires a Command Computer.\n" } + end) + + it("lists commands", function() + local pagedTabulate = stub(textutils, "pagedTabulate", function(x) print(table.unpack(x)) end) + stub(_G, "commands", { + list = function() return { "computercraft" } end + }) + + expect(capture(stub, "/rom/programs/command/commands.lua")) + :matches { ok = true, output = "Available commands:\ncomputercraft\n", error = "" } + expect(pagedTabulate):called_with_matching({ "computercraft" }) + end) +end) diff --git a/src/test/resources/test-rom/spec/programs/command/exec_spec.lua b/src/test/resources/test-rom/spec/programs/command/exec_spec.lua new file mode 100644 index 000000000..869222b2c --- /dev/null +++ b/src/test/resources/test-rom/spec/programs/command/exec_spec.lua @@ -0,0 +1,33 @@ +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: exec \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) diff --git a/src/test/resources/test-rom/spec/programs/drive_spec.lua b/src/test/resources/test-rom/spec/programs/drive_spec.lua new file mode 100644 index 000000000..d175a60f7 --- /dev/null +++ b/src/test/resources/test-rom/spec/programs/drive_spec.lua @@ -0,0 +1,16 @@ +local capture = require "test_helpers".capture_program + +describe("The drive program", function() + it("run the program", function() + local getFreeSpace = stub(fs, "getFreeSpace", function() return 1234e4 end) + + expect(capture(stub, "drive")) + :matches { ok = true, output = "hdd (12.3MB remaining)\n", error = "" } + expect(getFreeSpace):called(1):called_with("") + end) + + it("fails on a non-existent path", function() + expect(capture(stub, "drive /rom/nothing")) + :matches { ok = true, output = "No such path\n", error = "" } + end) +end) diff --git a/src/test/resources/test-rom/spec/programs/edit_spec.lua b/src/test/resources/test-rom/spec/programs/edit_spec.lua index dc8bcf9d0..137c39c83 100644 --- a/src/test/resources/test-rom/spec/programs/edit_spec.lua +++ b/src/test/resources/test-rom/spec/programs/edit_spec.lua @@ -1,10 +1,9 @@ local capture = require "test_helpers".capture_program +local testFile = require "test_helpers".testFile describe("The edit program", function() - it("displays its usage when given no argument", function() - multishell = nil - + it("displays its usage when given no argument", function() expect(capture(stub, "edit")) :matches { ok = true, output = "Usage: edit \n", error = "" } end) diff --git a/src/test/resources/test-rom/spec/programs/eject_spec.lua b/src/test/resources/test-rom/spec/programs/eject_spec.lua new file mode 100644 index 000000000..e524a8625 --- /dev/null +++ b/src/test/resources/test-rom/spec/programs/eject_spec.lua @@ -0,0 +1,13 @@ +local capture = require "test_helpers".capture_program + +describe("The eject program", function() + it("displays its usage when given no argument", function() + expect(capture(stub, "eject")) + :matches { ok = true, output = "Usage: eject \n", error = "" } + end) + + it("fails when trying to eject a non-drive", function() + expect(capture(stub, "eject /rom")) + :matches { ok = true, output = "Nothing in /rom drive\n", error = "" } + end) +end) diff --git a/src/test/resources/test-rom/spec/programs/exit_spec.lua b/src/test/resources/test-rom/spec/programs/exit_spec.lua new file mode 100644 index 000000000..301461087 --- /dev/null +++ b/src/test/resources/test-rom/spec/programs/exit_spec.lua @@ -0,0 +1,9 @@ +local capture = require "test_helpers".capture_program + +describe("The exit program", function() + it("exits the shell", function() + local exit = stub(shell, "exit") + expect(capture(stub, "exit")):matches { ok = true, combined = "" } + expect(exit):called(1) + end) +end) diff --git a/src/test/resources/test-rom/spec/programs/fun/advanced/paint_spec.lua b/src/test/resources/test-rom/spec/programs/fun/advanced/paint_spec.lua new file mode 100644 index 000000000..086404add --- /dev/null +++ b/src/test/resources/test-rom/spec/programs/fun/advanced/paint_spec.lua @@ -0,0 +1,8 @@ +local capture = require "test_helpers".capture_program + +describe("The paint program", function() + it("displays its usage when given no arguments", function() + expect(capture(stub, "paint")) + :matches { ok = true, output = "Usage: paint \n", error = "" } + end) +end) diff --git a/src/test/resources/test-rom/spec/programs/fun/dj_spec.lua b/src/test/resources/test-rom/spec/programs/fun/dj_spec.lua new file mode 100644 index 000000000..4464d4e8c --- /dev/null +++ b/src/test/resources/test-rom/spec/programs/fun/dj_spec.lua @@ -0,0 +1,13 @@ +local capture = require "test_helpers".capture_program + +describe("The dj program", function() + it("displays its usage when given too many arguments", function() + expect(capture(stub, "dj a b c")) + :matches { ok = true, output = "Usages:\ndj play\ndj play \ndj stop\n", error = "" } + end) + + it("fails when no disks are present", function() + expect(capture(stub, "dj")) + :matches { ok = true, output = "No Music Discs in attached disk drives\n", error = "" } + end) +end) diff --git a/src/test/resources/test-rom/spec/programs/fun/hello_spec.lua b/src/test/resources/test-rom/spec/programs/fun/hello_spec.lua new file mode 100644 index 000000000..25db085bc --- /dev/null +++ b/src/test/resources/test-rom/spec/programs/fun/hello_spec.lua @@ -0,0 +1,10 @@ +local capture = require "test_helpers".capture_program + +describe("The hello program", function() + it("says hello", function() + local slowPrint = stub(textutils, "slowPrint", function(...) return print(...) end) + expect(capture(stub, "hello")) + :matches { ok = true, output = "Hello World!\n", error = "" } + expect(slowPrint):called(1) + end) +end) diff --git a/src/test/resources/test-rom/spec/programs/gps_spec.lua b/src/test/resources/test-rom/spec/programs/gps_spec.lua new file mode 100644 index 000000000..245f259c9 --- /dev/null +++ b/src/test/resources/test-rom/spec/programs/gps_spec.lua @@ -0,0 +1,23 @@ +local capture = require "test_helpers".capture_program + +describe("The gps program", function() + it("displays its usage when given no arguments", function() + expect(capture(stub, "gps")) + :matches { ok = true, output = "Usages:\ngps host\ngps host \ngps locate\n", error = "" } + end) + + it("fails on a pocket computer", function() + stub(_G, "pocket", {}) + + expect(capture(stub, "gps host")) + :matches { ok = true, output = "GPS Hosts must be stationary\n", error = "" } + end) + + it("can locate the computer", function() + local locate = stub(gps, "locate", function() print("Some debugging information.") end) + + expect(capture(stub, "gps locate")) + :matches { ok = true, output = "Some debugging information.\n", error = "" } + expect(locate):called_with(2, true) + end) +end) diff --git a/src/test/resources/test-rom/spec/programs/help_spec.lua b/src/test/resources/test-rom/spec/programs/help_spec.lua new file mode 100644 index 000000000..79e4e4742 --- /dev/null +++ b/src/test/resources/test-rom/spec/programs/help_spec.lua @@ -0,0 +1,8 @@ +local capture = require "test_helpers".capture_program + +describe("The help program", function() + it("errors when there is no such help file", function() + expect(capture(stub, "help nothing")) + :matches { ok = true, output = "No help available\n", error = "" } + end) +end) diff --git a/src/test/resources/test-rom/spec/programs/label_spec.lua b/src/test/resources/test-rom/spec/programs/label_spec.lua new file mode 100644 index 000000000..5762f8fbe --- /dev/null +++ b/src/test/resources/test-rom/spec/programs/label_spec.lua @@ -0,0 +1,34 @@ +local capture = require "test_helpers".capture_program + +describe("The label program", function() + it("displays its usage when given no arguments", function() + expect(capture(stub, "label")) + :matches { ok = true, output = "Usages:\nlabel get\nlabel get \nlabel set \nlabel set \nlabel clear\nlabel clear \n", error = "" } + end) + + describe("displays the computer's label", function() + it("when it is not labelled", function() + stub(os, "getComputerLabel", function() return nil end) + expect(capture(stub, "label get")) + :matches { ok = true, output = "No Computer label\n", error = "" } + end) + + it("when it is labelled", function() + stub(os, "getComputerLabel", function() return "Test" end) + expect(capture(stub, "label get")) + :matches { ok = true, output = "Computer label is \"Test\"\n", error = "" } + end) + end) + + it("sets the computer's label", function() + local setComputerLabel = stub(os, "setComputerLabel") + capture(stub, "label set Test") + expect(setComputerLabel):called_with("Test") + end) + + it("clears the computer's label", function() + local setComputerLabel = stub(os, "setComputerLabel") + capture(stub, "label clear") + expect(setComputerLabel):called_with(nil) + end) +end) diff --git a/src/test/resources/test-rom/spec/programs/list_spec.lua b/src/test/resources/test-rom/spec/programs/list_spec.lua new file mode 100644 index 000000000..f9f5f0b0b --- /dev/null +++ b/src/test/resources/test-rom/spec/programs/list_spec.lua @@ -0,0 +1,22 @@ +local capture = require "test_helpers".capture_program + +describe("The list program", function() + it("lists files", function() + local pagedTabulate = stub(textutils, "pagedTabulate") + capture(stub, "list /rom") + expect(pagedTabulate):called_with_matching( + colors.green, { "apis", "autorun", "help", "modules", "programs" }, + colors.white, { "motd.txt", "startup.lua" } + ) + end) + + it("fails on a non-existent directory", function() + expect(capture(stub, "list /rom/nothing")) + :matches { ok = true, output = "", error = "Not a directory\n" } + end) + + it("fails on a file", function() + expect(capture(stub, "list /rom/startup.lua")) + :matches { ok = true, output = "", error = "Not a directory\n" } + end) +end) diff --git a/src/test/resources/test-rom/spec/programs/monitor_spec.lua b/src/test/resources/test-rom/spec/programs/monitor_spec.lua new file mode 100644 index 000000000..04fbb613a --- /dev/null +++ b/src/test/resources/test-rom/spec/programs/monitor_spec.lua @@ -0,0 +1,8 @@ +local capture = require "test_helpers".capture_program + +describe("The monitor program", function() + it("displays its usage when given no arguments", function() + expect(capture(stub, "monitor")) + :matches { ok = true, output = "Usage: monitor \n", error = "" } + end) +end) diff --git a/src/test/resources/test-rom/spec/programs/peripherals_spec.lua b/src/test/resources/test-rom/spec/programs/peripherals_spec.lua new file mode 100644 index 000000000..f52961a13 --- /dev/null +++ b/src/test/resources/test-rom/spec/programs/peripherals_spec.lua @@ -0,0 +1,8 @@ +local capture = require "test_helpers".capture_program + +describe("The peripherals program", function() + it("says when there are no peripherals", function() + expect(capture(stub, "peripherals" )) + :matches { ok = true, output = "Attached Peripherals:\nNone\n", error = "" } + end) +end) diff --git a/src/test/resources/test-rom/spec/programs/pocket/equip_spec.lua b/src/test/resources/test-rom/spec/programs/pocket/equip_spec.lua new file mode 100644 index 000000000..0c3fb3183 --- /dev/null +++ b/src/test/resources/test-rom/spec/programs/pocket/equip_spec.lua @@ -0,0 +1,27 @@ +local capture = require "test_helpers".capture_program + +describe("The pocket equip program", function() + it("errors when not a pocket computer", function() + stub(_G, "pocket", nil) + expect(capture(stub, "/rom/programs/pocket/equip.lua")) + :matches { ok = true, output = "", error = "Requires a Pocket Computer\n" } + end) + + it("can equip an upgrade", function() + stub(_G, "pocket", { + equipBack = function() return true end + }) + + expect(capture(stub, "/rom/programs/pocket/equip.lua")) + :matches { ok = true, output = "Item equipped\n", error = "" } + end) + + it("handles when an upgrade cannot be equipped", function() + stub(_G, "pocket", { + equipBack = function() return false, "Cannot equip this item." end + }) + + expect(capture(stub, "/rom/programs/pocket/equip.lua")) + :matches { ok = true, output = "", error = "Cannot equip this item.\n" } + end) +end) diff --git a/src/test/resources/test-rom/spec/programs/pocket/unequip_spec.lua b/src/test/resources/test-rom/spec/programs/pocket/unequip_spec.lua new file mode 100644 index 000000000..85a1e640f --- /dev/null +++ b/src/test/resources/test-rom/spec/programs/pocket/unequip_spec.lua @@ -0,0 +1,27 @@ +local capture = require "test_helpers".capture_program + +describe("The pocket unequip program", function() + it("errors when not a pocket computer", function() + stub(_G, "pocket", nil) + expect(capture(stub, "/rom/programs/pocket/unequip.lua")) + :matches { ok = true, output = "", error = "Requires a Pocket Computer\n" } + end) + + it("unequips an upgrade", function() + stub(_G, "pocket", { + unequipBack = function() return true end + }) + + expect(capture(stub, "/rom/programs/pocket/unequip.lua")) + :matches { ok = true, output = "Item unequipped\n", error = "" } + end) + + it("handles when an upgrade cannot be equipped", function() + stub(_G, "pocket", { + unequipBack = function() return false, "Nothing to remove." end + }) + + expect(capture(stub, "/rom/programs/pocket/unequip.lua")) + :matches { ok = true, output = "", error = "Nothing to remove.\n" } + end) +end) diff --git a/src/test/resources/test-rom/spec/programs/programs_spec.lua b/src/test/resources/test-rom/spec/programs/programs_spec.lua new file mode 100644 index 000000000..8f19bc83e --- /dev/null +++ b/src/test/resources/test-rom/spec/programs/programs_spec.lua @@ -0,0 +1,14 @@ +local capture = require "test_helpers".capture_program + +describe("The programs program", function() + it("list programs", function() + local programs = stub(shell, "programs", function() return { "some", "programs" } end) + local pagedTabulate = stub(textutils, "pagedTabulate", function(x) print(table.unpack(x)) end) + + expect(capture(stub, "/rom/programs/programs.lua")) + :matches { ok = true, output = "some programs\n", error = "" } + + expect(programs):called_with(false) + expect(pagedTabulate):called_with_matching({ "some", "programs" }) + end) +end) diff --git a/src/test/resources/test-rom/spec/programs/reboot_spec.lua b/src/test/resources/test-rom/spec/programs/reboot_spec.lua new file mode 100644 index 000000000..d2a877d9d --- /dev/null +++ b/src/test/resources/test-rom/spec/programs/reboot_spec.lua @@ -0,0 +1,14 @@ +local capture = require "test_helpers".capture_program + +describe("The reboot program", function() + it("sleeps and then reboots", function() + local sleep = stub(_G, "sleep") + local reboot = stub(os, "reboot") + + expect(capture(stub, "reboot")) + :matches { ok = true, output = "Goodbye\n", error = "" } + + expect(sleep):called_with(1) + expect(reboot):called() + end) +end) diff --git a/src/test/resources/test-rom/spec/programs/redstone_spec.lua b/src/test/resources/test-rom/spec/programs/redstone_spec.lua new file mode 100644 index 000000000..fc687ac7e --- /dev/null +++ b/src/test/resources/test-rom/spec/programs/redstone_spec.lua @@ -0,0 +1,8 @@ +local capture = require "test_helpers".capture_program + +describe("The redstone program", function() + it("displays its usage when given no arguments", function() + expect(capture(stub, "redstone")) + :matches { ok = true, output = "Usages:\nredstone probe\nredstone set \nredstone set \nredstone pulse \n", error = "" } + end) +end) diff --git a/src/test/resources/test-rom/spec/programs/shutdown_spec.lua b/src/test/resources/test-rom/spec/programs/shutdown_spec.lua new file mode 100644 index 000000000..4cfa37ba1 --- /dev/null +++ b/src/test/resources/test-rom/spec/programs/shutdown_spec.lua @@ -0,0 +1,15 @@ +local capture = require "test_helpers".capture_program + +describe("The shutdown program", function() + + it("run the program", function() + local sleep = stub(_G, "sleep") + local shutdown = stub(os, "shutdown") + + expect(capture(stub, "shutdown")) + :matches { ok = true, output = "Goodbye\n", error = "" } + + expect(sleep):called_with(1) + expect(shutdown):called() + end) +end) diff --git a/src/test/resources/test-rom/spec/programs/turtle/craft_spec.lua b/src/test/resources/test-rom/spec/programs/turtle/craft_spec.lua new file mode 100644 index 000000000..43b7ea982 --- /dev/null +++ b/src/test/resources/test-rom/spec/programs/turtle/craft_spec.lua @@ -0,0 +1,69 @@ +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: craft [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) diff --git a/src/test/resources/test-rom/spec/programs/turtle/equip_spec.lua b/src/test/resources/test-rom/spec/programs/turtle/equip_spec.lua new file mode 100644 index 000000000..2e637fc82 --- /dev/null +++ b/src/test/resources/test-rom/spec/programs/turtle/equip_spec.lua @@ -0,0 +1,89 @@ +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: equip \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) diff --git a/src/test/resources/test-rom/spec/programs/turtle/refuel_spec.lua b/src/test/resources/test-rom/spec/programs/turtle/refuel_spec.lua new file mode 100644 index 000000000..3b6283260 --- /dev/null +++ b/src/test/resources/test-rom/spec/programs/turtle/refuel_spec.lua @@ -0,0 +1,62 @@ +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: refuel [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) diff --git a/src/test/resources/test-rom/spec/programs/turtle/unequip_spec.lua b/src/test/resources/test-rom/spec/programs/turtle/unequip_spec.lua new file mode 100644 index 000000000..1cb97b440 --- /dev/null +++ b/src/test/resources/test-rom/spec/programs/turtle/unequip_spec.lua @@ -0,0 +1,69 @@ +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: unequip \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)