Merge branch 'master' into mc-1.14.x

This commit is contained in:
SquidDev 2020-05-13 14:04:28 +01:00
commit 4be0b15afa
69 changed files with 1195 additions and 141 deletions

View File

@ -32,6 +32,9 @@ jobs:
name: CC-Tweaked name: CC-Tweaked
path: build/libs path: build/libs
- name: Upload Coverage
run: bash <(curl -s https://codecov.io/bash)
lint-lua: lint-lua:
name: Lint Lua name: Lint Lua
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@ -17,6 +17,7 @@
plugins { plugins {
id "checkstyle" id "checkstyle"
id "jacoco"
id "com.github.hierynomus.license" version "0.15.0" id "com.github.hierynomus.license" version "0.15.0"
id "com.matthewprenger.cursegradle" version "1.3.0" id "com.matthewprenger.cursegradle" version "1.3.0"
id "com.github.breadmoirai.github-release" version "2.2.4" id "com.github.breadmoirai.github-release" version "2.2.4"
@ -288,6 +289,15 @@ task compressJson(dependsOn: jar) {
} }
} }
jacocoTestReport {
reports {
xml.enabled true
html.enabled true
}
}
check.dependsOn jacocoTestReport
license { license {
mapping("java", "SLASHSTAR_STYLE") mapping("java", "SLASHSTAR_STYLE")
strictCheck true strictCheck true

View File

@ -1,6 +1,77 @@
--- Execute a specific command.
--
-- @tparam string command The command to execute.
-- @treturn boolean Whether the command executed successfully.
-- @treturn { string... } The output of this command, as a list of lines.
-- @treturn number|nil The number of "affected" objects, or `nil` if the command
-- failed. The definition of this varies from command to command.
-- @usage Set the block above the command computer to stone.
--
-- commands.exec("setblock ~ ~1 ~ minecraft:stone")
function exec(command) end function exec(command) end
--- Asynchronously execute a command.
--
-- Unlike @{exec}, this will immediately return, instead of waiting for the
-- command to execute. This allows you to run multiple commands at the same
-- time.
--
-- When this command has finished executing, it will queue a `task_complete`
-- event containing the result of executing this command (what @{exec} would
-- return).
--
-- @tparam string command The command to execute.
-- @treturn number The "task id". When this command has been executed, it will
-- queue a `task_complete` event with a matching id.
-- @usage Asynchronously sets the block above the computer to stone.
--
-- commands.execAsync("~ ~1 ~ minecraft:stone")
-- @see parallel One may also use the parallel API to run multiple commands at
-- once.
function execAsync(commad) end function execAsync(commad) end
--- List all available commands which the computer has permission to execute.
--
-- @treturn { string... } A list of all available commands
function list() end function list() end
--- Get the position of the current command computer.
--
-- @treturn number This computer's x position.
-- @treturn number This computer's y position.
-- @treturn number This computer's z position.
-- @see gps.locate To get the position of a non-command computer.
function getBlockPosition() end function getBlockPosition() end
function getBlockInfos(min_x, min_y, min_z, max_x, max_y, max_z) end
--- Get some basic information about a block.
--
-- The returned table contains the current name, metadata and block state (as
-- with @{turtle.inspect}). If there is a tile entity for that block, its NBT
-- will also be returned.
--
-- @tparam number x The x position of the block to query.
-- @tparam number y The y position of the block to query.
-- @tparam number z The z position of the block to query.
-- @treturn table The given block's information.
-- @throws If the coordinates are not within the world, or are not currently
-- loaded.
function getBlockInfo(x, y, z) end function getBlockInfo(x, y, z) end
--- Get information about a range of blocks.
--
-- This returns the same information as @{getBlockInfo}, just for multiple
-- blocks at once.
--
-- Blocks are traversed by ascending y level, followed by z and x - the returned
-- table may be indexed using `x + z*width + y*depth*depth`.
--
-- @tparam number min_x The start x coordinate of the range to query.
-- @tparam number min_y The start y coordinate of the range to query.
-- @tparam number min_z The start z coordinate of the range to query.
-- @tparam number max_x The end x coordinate of the range to query.
-- @tparam number max_y The end y coordinate of the range to query.
-- @tparam number max_z The end z coordinate of the range to query.
-- @treturn { table... } A list of information about each block.
-- @throws If the coordinates are not within the world.
-- @throws If trying to get information about more than 4096 blocks.
function getBlockInfos(min_x, min_y, min_z, max_x, max_y, max_z) end

View File

@ -19,6 +19,18 @@ function getFreeSpace(path) end
function find(pattern) end function find(pattern) end
function getDir(path) end function getDir(path) end
--- Returns true if a path is mounted to the parent filesystem.
--
-- The root filesystem "/" is considered a mount, along with disk folders and
-- the rom folder. Other programs (such as network shares) can exstend this to
-- make other mount types by correctly assigning their return value for getDrive.
--
-- @tparam string path The path to check.
-- @treturn boolean If the path is mounted, rather than a normal file/folder.
-- @throws If the path does not exist.
-- @see getDrive
function isDriveRoot(path) end
--- Get the capacity of the drive at the given path. --- Get the capacity of the drive at the given path.
-- --
-- This may be used in conjunction with @{getFreeSpace} to determine what -- This may be used in conjunction with @{getFreeSpace} to determine what

View File

@ -1,5 +1,5 @@
# Mod properties # Mod properties
mod_version=1.87.1 mod_version=1.88.0
# Minecraft properties (update mods.toml when changing) # Minecraft properties (update mods.toml when changing)
mc_version=1.14.4 mc_version=1.14.4

View File

@ -70,14 +70,12 @@
;; Suppress warnings for currently undocumented modules. ;; Suppress warnings for currently undocumented modules.
(at (at
(/doc/stub/commands.lua (/doc/stub/fs.lua
/doc/stub/fs.lua
/doc/stub/http.lua /doc/stub/http.lua
/doc/stub/os.lua /doc/stub/os.lua
/doc/stub/redstone.lua /doc/stub/redstone.lua
/doc/stub/term.lua /doc/stub/term.lua
/doc/stub/turtle.lua /doc/stub/turtle.lua
/src/main/resources/*/computercraft/lua/rom/apis/command/commands.lua
/src/main/resources/*/computercraft/lua/rom/apis/io.lua /src/main/resources/*/computercraft/lua/rom/apis/io.lua
/src/main/resources/*/computercraft/lua/rom/apis/window.lua /src/main/resources/*/computercraft/lua/rom/apis/window.lua
/src/main/resources/*/computercraft/lua/rom/modules/main/cc/shell/completion.lua) /src/main/resources/*/computercraft/lua/rom/modules/main/cc/shell/completion.lua)

View File

@ -65,7 +65,8 @@ public final class ComputerCraft
public static boolean disable_lua51_features = false; public static boolean disable_lua51_features = false;
public static String default_computer_settings = ""; public static String default_computer_settings = "";
public static boolean debug_enable = true; public static boolean debug_enable = true;
public static boolean logPeripheralErrors = false; public static boolean logPeripheralErrors = true;
public static boolean commandRequireCreative = true;
public static int computer_threads = 1; public static int computer_threads = 1;
public static long maxMainGlobalTime = TimeUnit.MILLISECONDS.toNanos( 10 ); public static long maxMainGlobalTime = TimeUnit.MILLISECONDS.toNanos( 10 );

View File

@ -50,12 +50,12 @@ private FixedWidthFontRenderer()
{ {
} }
private static float toGreyscale( double[] rgb ) public static float toGreyscale( double[] rgb )
{ {
return (float) ((rgb[0] + rgb[1] + rgb[2]) / 3); return (float) ((rgb[0] + rgb[1] + rgb[2]) / 3);
} }
private static int getColour( char c, Colour def ) public static int getColour( char c, Colour def )
{ {
return 15 - Terminal.getColour( c, def ); return 15 - Terminal.getColour( c, def );
} }

View File

@ -0,0 +1,174 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.client.render;
import com.google.common.base.Strings;
import com.mojang.blaze3d.platform.GLX;
import com.mojang.blaze3d.platform.TextureUtil;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.client.gui.FixedWidthFontRenderer;
import dan200.computercraft.shared.util.Palette;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL20;
import java.io.IOException;
import java.io.InputStream;
import java.nio.FloatBuffer;
class MonitorTextureBufferShader
{
static final int TEXTURE_INDEX = GL13.GL_TEXTURE3;
private static final FloatBuffer MATRIX_BUFFER = BufferUtils.createFloatBuffer( 16 );
private static final FloatBuffer PALETTE_BUFFER = BufferUtils.createFloatBuffer( 16 * 3 );
private static int uniformMv;
private static int uniformP;
private static int uniformFont;
private static int uniformWidth;
private static int uniformHeight;
private static int uniformTbo;
private static int uniformPalette;
private static boolean initialised;
private static boolean ok;
private static int program;
static void setupUniform( int width, int height, Palette palette, boolean greyscale )
{
MATRIX_BUFFER.rewind();
GL11.glGetFloatv( GL11.GL_MODELVIEW_MATRIX, MATRIX_BUFFER );
MATRIX_BUFFER.rewind();
GLX.glUniformMatrix4( uniformMv, false, MATRIX_BUFFER );
MATRIX_BUFFER.rewind();
GL11.glGetFloatv( GL11.GL_PROJECTION_MATRIX, MATRIX_BUFFER );
MATRIX_BUFFER.rewind();
GLX.glUniformMatrix4( uniformP, false, MATRIX_BUFFER );
GLX.glUniform1i( uniformWidth, width );
GLX.glUniform1i( uniformHeight, height );
PALETTE_BUFFER.rewind();
for( int i = 0; i < 16; i++ )
{
double[] colour = palette.getColour( i );
if( greyscale )
{
float f = FixedWidthFontRenderer.toGreyscale( colour );
PALETTE_BUFFER.put( f ).put( f ).put( f );
}
else
{
PALETTE_BUFFER.put( (float) colour[0] ).put( (float) colour[1] ).put( (float) colour[2] );
}
}
PALETTE_BUFFER.flip();
GLX.glUniform3( uniformPalette, PALETTE_BUFFER );
}
static boolean use()
{
if( initialised )
{
if( ok ) GLX.glUseProgram( program );
return ok;
}
if( ok = load() )
{
GL20.glUseProgram( program );
GLX.glUniform1i( uniformFont, 0 );
GLX.glUniform1i( uniformTbo, TEXTURE_INDEX - GL13.GL_TEXTURE0 );
}
return ok;
}
private static boolean load()
{
initialised = true;
try
{
int vertexShader = loadShader( GL20.GL_VERTEX_SHADER, "assets/computercraft/shaders/monitor.vert" );
int fragmentShader = loadShader( GL20.GL_FRAGMENT_SHADER, "assets/computercraft/shaders/monitor.frag" );
program = GLX.glCreateProgram();
GLX.glAttachShader( program, vertexShader );
GLX.glAttachShader( program, fragmentShader );
GL20.glBindAttribLocation( program, 0, "v_pos" );
GLX.glLinkProgram( program );
boolean ok = GLX.glGetProgrami( program, GL20.GL_LINK_STATUS ) != 0;
String log = GLX.glGetProgramInfoLog( program, Short.MAX_VALUE ).trim();
if( !Strings.isNullOrEmpty( log ) )
{
ComputerCraft.log.warn( "Problems when linking monitor shader: {}", log );
}
GL20.glDetachShader( program, vertexShader );
GL20.glDetachShader( program, fragmentShader );
GLX.glDeleteShader( vertexShader );
GLX.glDeleteShader( fragmentShader );
if( !ok ) return false;
uniformMv = getUniformLocation( program, "u_mv" );
uniformP = getUniformLocation( program, "u_p" );
uniformFont = getUniformLocation( program, "u_font" );
uniformWidth = getUniformLocation( program, "u_width" );
uniformHeight = getUniformLocation( program, "u_height" );
uniformTbo = getUniformLocation( program, "u_tbo" );
uniformPalette = getUniformLocation( program, "u_palette" );
ComputerCraft.log.info( "Loaded monitor shader." );
return true;
}
catch( Exception e )
{
ComputerCraft.log.error( "Cannot load monitor shaders", e );
return false;
}
}
private static int loadShader( int kind, String path ) throws IOException
{
String contents;
try( InputStream stream = TileEntityMonitorRenderer.class.getClassLoader().getResourceAsStream( path ) )
{
if( stream == null ) throw new IllegalArgumentException( "Cannot find " + path );
contents = TextureUtil.readResourceAsString( stream );
}
int shader = GLX.glCreateShader( kind );
GLX.glShaderSource( shader, contents );
GLX.glCompileShader( shader );
boolean ok = GLX.glGetShaderi( shader, GL20.GL_COMPILE_STATUS ) != 0;
String log = GLX.glGetShaderInfoLog( shader, Short.MAX_VALUE ).trim();
if( !Strings.isNullOrEmpty( log ) )
{
ComputerCraft.log.warn( "Problems when loading monitor shader {}: {}", path, log );
}
if( !ok ) throw new IllegalStateException( "Cannot compile shader " + path );
return shader;
}
private static int getUniformLocation( int program, String name )
{
int uniform = GLX.glGetUniformLocation( program, name );
if( uniform == -1 ) throw new IllegalStateException( "Cannot find uniform " + name );
return uniform;
}
}

View File

@ -10,21 +10,30 @@
import dan200.computercraft.client.FrameInfo; import dan200.computercraft.client.FrameInfo;
import dan200.computercraft.client.gui.FixedWidthFontRenderer; import dan200.computercraft.client.gui.FixedWidthFontRenderer;
import dan200.computercraft.core.terminal.Terminal; import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.core.terminal.TextBuffer;
import dan200.computercraft.shared.peripheral.monitor.ClientMonitor; import dan200.computercraft.shared.peripheral.monitor.ClientMonitor;
import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer; import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer;
import dan200.computercraft.shared.peripheral.monitor.TileMonitor; import dan200.computercraft.shared.peripheral.monitor.TileMonitor;
import dan200.computercraft.shared.util.Colour;
import dan200.computercraft.shared.util.DirectionUtil; import dan200.computercraft.shared.util.DirectionUtil;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.BufferBuilder; import net.minecraft.client.renderer.BufferBuilder;
import net.minecraft.client.renderer.GLAllocation;
import net.minecraft.client.renderer.Tessellator; import net.minecraft.client.renderer.Tessellator;
import net.minecraft.client.renderer.tileentity.TileEntityRenderer; import net.minecraft.client.renderer.tileentity.TileEntityRenderer;
import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
import net.minecraft.client.renderer.vertex.VertexBuffer; import net.minecraft.client.renderer.vertex.VertexBuffer;
import net.minecraft.util.Direction; import net.minecraft.util.Direction;
import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.BlockPos;
import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL31;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import java.nio.ByteBuffer;
import static dan200.computercraft.client.gui.FixedWidthFontRenderer.*;
import static dan200.computercraft.shared.peripheral.monitor.TileMonitor.RENDER_MARGIN; import static dan200.computercraft.shared.peripheral.monitor.TileMonitor.RENDER_MARGIN;
public class TileEntityMonitorRenderer extends TileEntityRenderer<TileMonitor> public class TileEntityMonitorRenderer extends TileEntityRenderer<TileMonitor>
@ -92,8 +101,8 @@ public void render( @Nonnull TileMonitor monitor, double posX, double posY, doub
if( terminal != null ) if( terminal != null )
{ {
// Draw a terminal // Draw a terminal
double xScale = xSize / (terminal.getWidth() * FixedWidthFontRenderer.FONT_WIDTH); double xScale = xSize / (terminal.getWidth() * FONT_WIDTH);
double yScale = ySize / (terminal.getHeight() * FixedWidthFontRenderer.FONT_HEIGHT); double yScale = ySize / (terminal.getHeight() * FONT_HEIGHT);
GlStateManager.pushMatrix(); GlStateManager.pushMatrix();
GlStateManager.scaled( (float) xScale, (float) -yScale, 1.0f ); GlStateManager.scaled( (float) xScale, (float) -yScale, 1.0f );
@ -142,6 +151,52 @@ private static void renderTerminal( ClientMonitor monitor, float xMargin, float
switch( renderer ) switch( renderer )
{ {
case TBO:
{
if( !MonitorTextureBufferShader.use() ) return;
Terminal terminal = monitor.getTerminal();
int width = terminal.getWidth(), height = terminal.getHeight();
int pixelWidth = width * FONT_WIDTH, pixelHeight = height * FONT_HEIGHT;
if( redraw )
{
ByteBuffer monitorBuffer = GLAllocation.createDirectByteBuffer( width * height * 3 );
for( int y = 0; y < height; y++ )
{
TextBuffer text = terminal.getLine( y ), textColour = terminal.getTextColourLine( y ), background = terminal.getBackgroundColourLine( y );
for( int x = 0; x < width; x++ )
{
monitorBuffer.put( (byte) (text.charAt( x ) & 0xFF) );
monitorBuffer.put( (byte) getColour( textColour.charAt( x ), Colour.White ) );
monitorBuffer.put( (byte) getColour( background.charAt( x ), Colour.Black ) );
}
}
monitorBuffer.flip();
GLX.glBindBuffer( GL31.GL_TEXTURE_BUFFER, monitor.tboBuffer );
GLX.glBufferData( GL31.GL_TEXTURE_BUFFER, monitorBuffer, GL15.GL_STATIC_DRAW );
GLX.glBindBuffer( GL31.GL_TEXTURE_BUFFER, 0 );
}
// Bind TBO texture and set up the uniforms. We've already set up the main font above.
GlStateManager.activeTexture( MonitorTextureBufferShader.TEXTURE_INDEX );
GL11.glBindTexture( GL31.GL_TEXTURE_BUFFER, monitor.tboTexture );
GlStateManager.activeTexture( GL13.GL_TEXTURE0 );
MonitorTextureBufferShader.setupUniform( width, height, terminal.getPalette(), !monitor.isColour() );
buffer.begin( GL11.GL_TRIANGLE_STRIP, DefaultVertexFormats.POSITION );
buffer.pos( -xMargin, -yMargin, 0 ).endVertex();
buffer.pos( -xMargin, pixelHeight + yMargin, 0 ).endVertex();
buffer.pos( pixelWidth + xMargin, -yMargin, 0 ).endVertex();
buffer.pos( pixelWidth + xMargin, pixelHeight + yMargin, 0 ).endVertex();
tessellator.draw();
GLX.glUseProgram( 0 );
break;
}
case VBO: case VBO:
{ {
VertexBuffer vbo = monitor.buffer; VertexBuffer vbo = monitor.buffer;

View File

@ -13,6 +13,7 @@
import dan200.computercraft.api.turtle.event.TurtleAction; import dan200.computercraft.api.turtle.event.TurtleAction;
import dan200.computercraft.core.apis.AddressPredicate; import dan200.computercraft.core.apis.AddressPredicate;
import dan200.computercraft.core.apis.http.websocket.Websocket; import dan200.computercraft.core.apis.http.websocket.Websocket;
import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer;
import net.minecraftforge.common.ForgeConfigSpec; import net.minecraftforge.common.ForgeConfigSpec;
import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.ModLoadingContext; import net.minecraftforge.fml.ModLoadingContext;
@ -74,7 +75,10 @@ public final class Config
private static final ConfigValue<Boolean> turtlesCanPush; private static final ConfigValue<Boolean> turtlesCanPush;
private static final ConfigValue<List<? extends String>> turtleDisabledActions; private static final ConfigValue<List<? extends String>> turtleDisabledActions;
private static final ForgeConfigSpec spec; private static final ConfigValue<MonitorRenderer> monitorRenderer;
private static final ForgeConfigSpec commonSpec;
private static final ForgeConfigSpec clientSpec;
private Config() {} private Config() {}
@ -260,12 +264,20 @@ private Config() {}
builder.pop(); builder.pop();
} }
spec = builder.build(); commonSpec = builder.build();
Builder clientBuilder = new Builder();
monitorRenderer = clientBuilder
.comment( "The renderer to use for monitors. Generally this should be kept at \"best\" - if " +
"monitors have performance issues, you may wish to experiment with alternative renderers." )
.defineEnum( "monitor_renderer", MonitorRenderer.BEST );
clientSpec = clientBuilder.build();
} }
public static void load() public static void load()
{ {
ModLoadingContext.get().registerConfig( ModConfig.Type.COMMON, spec ); ModLoadingContext.get().registerConfig( ModConfig.Type.COMMON, commonSpec );
ModLoadingContext.get().registerConfig( ModConfig.Type.CLIENT, clientSpec );
} }
public static void sync() public static void sync()
@ -315,6 +327,9 @@ public static void sync()
ComputerCraft.turtleDisabledActions.clear(); ComputerCraft.turtleDisabledActions.clear();
for( String value : turtleDisabledActions.get() ) ComputerCraft.turtleDisabledActions.add( getAction( value ) ); for( String value : turtleDisabledActions.get() ) ComputerCraft.turtleDisabledActions.add( getAction( value ) );
// Client
ComputerCraft.monitorRenderer = monitorRenderer.get();
} }
@SubscribeEvent @SubscribeEvent

View File

@ -120,6 +120,11 @@ protected ServerComputer createComputer( int instanceID, int id )
@Override @Override
public boolean isUsable( PlayerEntity player, boolean ignoreRange ) public boolean isUsable( PlayerEntity player, boolean ignoreRange )
{
return isUsable( player ) && super.isUsable( player, ignoreRange );
}
public static boolean isUsable( PlayerEntity player )
{ {
MinecraftServer server = player.getServer(); MinecraftServer server = player.getServer();
if( server == null || !server.isCommandBlockEnabled() ) if( server == null || !server.isCommandBlockEnabled() )
@ -127,14 +132,12 @@ public boolean isUsable( PlayerEntity player, boolean ignoreRange )
player.sendStatusMessage( new TranslationTextComponent( "advMode.notEnabled" ), true ); player.sendStatusMessage( new TranslationTextComponent( "advMode.notEnabled" ), true );
return false; return false;
} }
else if( !player.canUseCommandBlock() ) else if( ComputerCraft.commandRequireCreative ? !player.canUseCommandBlock() : !server.getPlayerList().canSendCommands( player.getGameProfile() ) )
{ {
player.sendStatusMessage( new TranslationTextComponent( "advMode.notAllowed" ), true ); player.sendStatusMessage( new TranslationTextComponent( "advMode.notAllowed" ), true );
return false; return false;
} }
else
{ return true;
return super.isUsable( player, ignoreRange );
}
} }
} }

View File

@ -35,4 +35,3 @@ public String toString()
return name; return name;
} }
} }

View File

@ -6,6 +6,7 @@
package dan200.computercraft.shared.computer.inventory; package dan200.computercraft.shared.computer.inventory;
import dan200.computercraft.ComputerCraft; import dan200.computercraft.ComputerCraft;
import dan200.computercraft.shared.computer.blocks.TileCommandComputer;
import dan200.computercraft.shared.computer.core.ComputerFamily; import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.IContainerComputer; import dan200.computercraft.shared.computer.core.IContainerComputer;
import dan200.computercraft.shared.computer.core.ServerComputer; import dan200.computercraft.shared.computer.core.ServerComputer;
@ -14,8 +15,6 @@
import net.minecraft.entity.player.PlayerEntity; import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.entity.player.PlayerInventory; import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.inventory.container.ContainerType; import net.minecraft.inventory.container.ContainerType;
import net.minecraft.server.MinecraftServer;
import net.minecraft.util.text.TranslationTextComponent;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
@ -48,18 +47,9 @@ private static boolean canInteractWith( @Nonnull ServerComputer computer, @Nonnu
} }
// If we're a command computer then ensure we're in creative // If we're a command computer then ensure we're in creative
if( computer.getFamily() == ComputerFamily.Command ) if( computer.getFamily() == ComputerFamily.Command && !TileCommandComputer.isUsable( player ) )
{ {
MinecraftServer server = player.getServer(); return false;
if( server == null || !server.isCommandBlockEnabled() )
{
return false;
}
else if( !player.canUseCommandBlock() )
{
player.sendStatusMessage( new TranslationTextComponent( "advMode.notAllowed" ), false );
return false;
}
} }
return true; return true;

View File

@ -5,12 +5,18 @@
*/ */
package dan200.computercraft.shared.peripheral.monitor; package dan200.computercraft.shared.peripheral.monitor;
import com.mojang.blaze3d.platform.GLX;
import com.mojang.blaze3d.platform.GlStateManager;
import dan200.computercraft.client.gui.FixedWidthFontRenderer; import dan200.computercraft.client.gui.FixedWidthFontRenderer;
import dan200.computercraft.shared.common.ClientTerminal; import dan200.computercraft.shared.common.ClientTerminal;
import net.minecraft.client.renderer.vertex.VertexBuffer; import net.minecraft.client.renderer.vertex.VertexBuffer;
import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.BlockPos;
import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn; import net.minecraftforge.api.distmarker.OnlyIn;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.GL31;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
@ -25,6 +31,8 @@ public final class ClientMonitor extends ClientTerminal
public long lastRenderFrame = -1; public long lastRenderFrame = -1;
public BlockPos lastRenderPos = null; public BlockPos lastRenderPos = null;
public int tboBuffer;
public int tboTexture;
public VertexBuffer buffer; public VertexBuffer buffer;
public ClientMonitor( boolean colour, TileMonitor origin ) public ClientMonitor( boolean colour, TileMonitor origin )
@ -50,6 +58,26 @@ public boolean createBuffer( MonitorRenderer renderer )
{ {
switch( renderer ) switch( renderer )
{ {
case TBO:
{
if( tboBuffer != 0 ) return false;
deleteBuffers();
tboBuffer = GLX.glGenBuffers();
GLX.glBindBuffer( GL31.GL_TEXTURE_BUFFER, tboBuffer );
GL15.glBufferData( GL31.GL_TEXTURE_BUFFER, 0, GL15.GL_STATIC_DRAW );
tboTexture = GlStateManager.genTexture();
GL11.glBindTexture( GL31.GL_TEXTURE_BUFFER, tboTexture );
GL31.glTexBuffer( GL31.GL_TEXTURE_BUFFER, GL30.GL_R8, tboBuffer );
GL11.glBindTexture( GL31.GL_TEXTURE_BUFFER, 0 );
GLX.glBindBuffer( GL31.GL_TEXTURE_BUFFER, 0 );
addMonitor();
return true;
}
case VBO: case VBO:
if( buffer != null ) return false; if( buffer != null ) return false;
@ -73,6 +101,19 @@ private void addMonitor()
private void deleteBuffers() private void deleteBuffers()
{ {
if( tboBuffer != 0 )
{
GLX.glDeleteBuffers( tboBuffer );
tboBuffer = 0;
}
if( tboTexture != 0 )
{
GlStateManager.deleteTexture( tboTexture );
tboTexture = 0;
}
if( buffer != null ) if( buffer != null )
{ {
buffer.deleteGlBuffers(); buffer.deleteGlBuffers();
@ -83,7 +124,7 @@ private void deleteBuffers()
@OnlyIn( Dist.CLIENT ) @OnlyIn( Dist.CLIENT )
public void destroy() public void destroy()
{ {
if( buffer != null ) if( tboBuffer != 0 || buffer != null )
{ {
synchronized( allMonitors ) synchronized( allMonitors )
{ {

View File

@ -6,12 +6,11 @@
package dan200.computercraft.shared.peripheral.monitor; package dan200.computercraft.shared.peripheral.monitor;
import com.mojang.blaze3d.platform.GLX;
import dan200.computercraft.ComputerCraft; import dan200.computercraft.ComputerCraft;
import dan200.computercraft.client.render.TileEntityMonitorRenderer; import dan200.computercraft.client.render.TileEntityMonitorRenderer;
import org.lwjgl.opengl.GL;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import java.util.Locale;
/** /**
* The render type to use for monitors. * The render type to use for monitors.
@ -26,6 +25,13 @@ public enum MonitorRenderer
*/ */
BEST, BEST,
/**
* Render using texture buffer objects.
*
* @see org.lwjgl.opengl.GL31#glTexBuffer(int, int, int)
*/
TBO,
/** /**
* Render using VBOs. * Render using VBOs.
* *
@ -33,37 +39,6 @@ public enum MonitorRenderer
*/ */
VBO; VBO;
private static final MonitorRenderer[] VALUES = values();
public static final String[] NAMES;
private final String displayName = "gui.computercraft:config.peripheral.monitor_renderer." + name().toLowerCase( Locale.ROOT );
static
{
NAMES = new String[VALUES.length];
for( int i = 0; i < VALUES.length; i++ ) NAMES[i] = VALUES[i].displayName();
}
public String displayName()
{
return displayName;
}
@Nonnull
public static MonitorRenderer ofString( String name )
{
for( MonitorRenderer backend : VALUES )
{
if( backend.displayName.equalsIgnoreCase( name ) || backend.name().equalsIgnoreCase( name ) )
{
return backend;
}
}
ComputerCraft.log.warn( "Unknown monitor renderer {}. Falling back to default.", name );
return BEST;
}
/** /**
* Get the current renderer to use. * Get the current renderer to use.
* *
@ -77,15 +52,16 @@ public static MonitorRenderer current()
{ {
case BEST: case BEST:
return best(); return best();
case VBO: case TBO:
if( !GLX.useVbo() ) checkCapabilities();
if( !textureBuffer )
{ {
ComputerCraft.log.warn( "VBOs are not supported on your graphics card. Falling back to default." ); ComputerCraft.log.warn( "Texture buffers are not supported on your graphics card. Falling back to default." );
ComputerCraft.monitorRenderer = BEST; ComputerCraft.monitorRenderer = BEST;
return best(); return best();
} }
return VBO; return TBO;
default: default:
return current; return current;
} }
@ -93,6 +69,18 @@ public static MonitorRenderer current()
private static MonitorRenderer best() private static MonitorRenderer best()
{ {
return VBO; checkCapabilities();
return textureBuffer ? TBO : VBO;
}
private static boolean initialised = false;
private static boolean textureBuffer = false;
private static void checkCapabilities()
{
if( initialised ) return;
textureBuffer = GL.getCapabilities().OpenGL31;
initialised = true;
} }
} }

View File

@ -154,4 +154,3 @@ private synchronized boolean playSound( ILuaContext context, ResourceLocation na
return true; return true;
} }
} }

View File

@ -78,4 +78,3 @@ public boolean equals( @Nullable IPeripheral other )
} }
} }
} }

View File

@ -6,4 +6,3 @@
"modem=true,peripheral=true": { "model": "computercraft:block/wired_modem_full_on_peripheral" } "modem=true,peripheral=true": { "model": "computercraft:block/wired_modem_full_on_peripheral" }
} }
} }

View File

@ -0,0 +1,40 @@
#version 140
#define FONT_WIDTH 6.0
#define FONT_HEIGHT 9.0
uniform sampler2D u_font;
uniform int u_width;
uniform int u_height;
uniform samplerBuffer u_tbo;
uniform vec3 u_palette[16];
in vec2 f_pos;
out vec4 colour;
vec2 texture_corner(int index) {
float x = 1.0 + float(index % 16) * (FONT_WIDTH + 2.0);
float y = 1.0 + float(index / 16) * (FONT_HEIGHT + 2.0);
return vec2(x, y);
}
void main() {
vec2 term_pos = vec2(f_pos.x / FONT_WIDTH, f_pos.y / FONT_HEIGHT);
vec2 corner = floor(term_pos);
ivec2 cell = ivec2(corner);
int index = 3 * (clamp(cell.x, 0, u_width - 1) + clamp(cell.y, 0, u_height - 1) * u_width);
// 1 if 0 <= x, y < width, height, 0 otherwise
vec2 outside = step(vec2(0.0, 0.0), vec2(cell)) * step(vec2(cell), vec2(float(u_width) - 1.0, float(u_height) - 1.0));
float mult = outside.x * outside.y;
int character = int(texelFetch(u_tbo, index).r * 255.0);
int fg = int(texelFetch(u_tbo, index + 1).r * 255.0);
int bg = int(texelFetch(u_tbo, index + 2).r * 255.0);
vec2 pos = (term_pos - corner) * vec2(FONT_WIDTH, FONT_HEIGHT);
vec4 img = texture2D(u_font, (texture_corner(character) + pos) / 256.0);
colour = vec4(mix(u_palette[bg], img.rgb * u_palette[fg], img.a * mult), 1.0);
}

View File

@ -0,0 +1,13 @@
#version 140
uniform mat4 u_mv;
uniform mat4 u_p;
in vec3 v_pos;
out vec2 f_pos;
void main() {
gl_Position = u_p * u_mv * vec4(v_pos.x, v_pos.y, 0, 1);
f_pos = v_pos.xy;
}

View File

@ -797,6 +797,12 @@ function fs.complete(sPath, sLocation, bIncludeFiles, bIncludeDirs)
return tEmpty return tEmpty
end end
function fs.isDriveRoot(sPath)
expect(1, sPath, "string")
-- Force the root directory to be a mount.
return fs.getDir(sPath) == ".." or fs.getDrive(sPath) ~= fs.getDrive(fs.getDir(sPath))
end
-- Load APIs -- Load APIs
local bAPIError = false local bAPIError = false
local tApis = fs.list("rom/apis") local tApis = fs.list("rom/apis")
@ -932,6 +938,11 @@ settings.define("motd.path", {
description = [[The path to load random messages from. Should be a colon (":") separated string of file paths.]], description = [[The path to load random messages from. Should be a colon (":") separated string of file paths.]],
type = "string", type = "string",
}) })
settings.define("lua.warn_against_use_of_local", {
default = true,
description = [[Print a message when input in the Lua REPL starts with the word 'local'. Local variables defined in the Lua REPL are be inaccessable on the next input.]],
type = "boolean",
})
if term.isColour() then if term.isColour() then
settings.define("bios.use_multishell", { settings.define("bios.use_multishell", {
default = true, default = true,

View File

@ -12,6 +12,9 @@
-- [mc]: https://minecraft.gamepedia.com/Commands -- [mc]: https://minecraft.gamepedia.com/Commands
-- --
-- @module commands -- @module commands
-- @usage Set the block above this computer to stone:
--
-- commands.setblock("~", "~1", "~", "minecraft:stone")
if not commands then if not commands then
error("Cannot load command API on normal computer", 2) error("Cannot load command API on normal computer", 2)
@ -97,4 +100,13 @@ for _, sCommandName in ipairs(native.list()) do
tAsync[sCommandName] = mk_command({ sCommandName }, bJSONIsNBT, native.execAsync) tAsync[sCommandName] = mk_command({ sCommandName }, bJSONIsNBT, native.execAsync)
end end
end end
--- A table containing asynchronous wrappers for all commands.
--
-- As with @{commands.execAsync}, this returns the "task id" of the enqueued
-- command.
-- @see execAsync
-- @usage Asynchronously sets the block above the computer to stone.
--
-- commands.async.setblock("~", "~1", "~", "minecraft:stone")
env.async = tAsync env.async = tAsync

View File

@ -75,7 +75,10 @@ handleMetatable = {
if not handle.read then return nil, "file is not readable" end if not handle.read then return nil, "file is not readable" end
local args = table.pack(...) local args = table.pack(...)
return function() return checkResult(self, self:read(table.unpack(args, 1, args.n))) end return function()
if self._closed then error("file is already closed", 2) end
return checkResult(self, self:read(table.unpack(args, 1, args.n)))
end
end, end,
read = function(self, ...) read = function(self, ...)
@ -259,12 +262,13 @@ end
-- instead. In this case, the handle is not used. -- instead. In this case, the handle is not used.
-- --
-- @tparam[opt] string filename The name of the file to extract lines from -- @tparam[opt] string filename The name of the file to extract lines from
-- @param ... The argument to pass to @{Handle:read} for each line.
-- @treturn function():string|nil The line iterator. -- @treturn function():string|nil The line iterator.
-- @throws If the file cannot be opened for reading -- @throws If the file cannot be opened for reading
-- --
-- @see Handle:lines -- @see Handle:lines
-- @see io.input -- @see io.input
function lines(filename) function lines(filename, ...)
expect(1, filename, "string", "nil") expect(1, filename, "string", "nil")
if filename then if filename then
local ok, err = open(filename, "rb") local ok, err = open(filename, "rb")
@ -273,9 +277,9 @@ function lines(filename)
-- We set this magic flag to mark this file as being opened by io.lines and so should be -- We set this magic flag to mark this file as being opened by io.lines and so should be
-- closed automatically -- closed automatically
ok._autoclose = true ok._autoclose = true
return ok:lines() return ok:lines(...)
else else
return currentInput:lines() return currentInput:lines(...)
end end
end end
@ -313,7 +317,7 @@ end
-- @throws If the provided filename cannot be opened for writing. -- @throws If the provided filename cannot be opened for writing.
function output(file) function output(file)
if type_of(file) == "string" then if type_of(file) == "string" then
local res, err = open(file, "w") local res, err = open(file, "wb")
if not res then error(err, 2) end if not res then error(err, 2) end
currentOutput = res currentOutput = res
elseif type_of(file) == "table" and getmetatable(file) == handleMetatable then elseif type_of(file) == "table" and getmetatable(file) == handleMetatable then

View File

@ -1,3 +1,17 @@
# New features in CC: Tweaked 1.88.0
* Computers and turtles now preserve their ID when broken.
* Add `peripheral.getName` - returns the name of a wrapped peripheral.
* Reduce network overhead of monitors and terminals.
* Add a TBO backend for monitors, with a significant performance boost.
* The Lua REPL warns when declaring locals (lupus590, exerro)
* Add config to allow using command computers in survival.
* Add fs.isDriveRoot - checks if a path is the root of a drive.
And several bug fixes:
* Fix io.lines not accepting arguments.
* Fix settings.load using an unknown global (MCJack123).
# New features in CC: Tweaked 1.87.1 # New features in CC: Tweaked 1.87.1
* Fix blocks not dropping items in survival. * Fix blocks not dropping items in survival.

View File

@ -1,4 +1,3 @@
ComputerCraft was created by Daniel "dan200" Ratcliffe, with additional code by Aaron "Cloudy" Mills. ComputerCraft was created by Daniel "dan200" Ratcliffe, with additional code by Aaron "Cloudy" Mills.
Thanks to nitrogenfingers, GopherATL and RamiLego for program contributions. Thanks to nitrogenfingers, GopherATL and RamiLego for program contributions.
Thanks to Mojang, the Forge team, and the MCP team. Thanks to Mojang, the Forge team, and the MCP team.

View File

@ -1,5 +1,15 @@
New features in CC: Tweaked 1.87.1 New features in CC: Tweaked 1.88.0
* Fix blocks not dropping items in survival. * Computers and turtles now preserve their ID when broken.
* Add `peripheral.getName` - returns the name of a wrapped peripheral.
* Reduce network overhead of monitors and terminals.
* Add a TBO backend for monitors, with a significant performance boost.
* The Lua REPL warns when declaring locals (lupus590, exerro)
* Add config to allow using command computers in survival.
* Add fs.isDriveRoot - checks if a path is the root of a drive.
And several bug fixes:
* Fix io.lines not accepting arguments.
* Fix settings.load using an unknown global (MCJack123).
Type "help changelog" to see the full version history. Type "help changelog" to see the full version history.

View File

@ -1,4 +1,3 @@
if not shell.openTab then if not shell.openTab then
printError("Requires multishell") printError("Requires multishell")
return return

View File

@ -1,4 +1,3 @@
if not shell.openTab then if not shell.openTab then
printError("Requires multishell") printError("Requires multishell")
return return

View File

@ -1,4 +1,3 @@
local tArgs = { ... } local tArgs = { ... }
if #tArgs > 2 then if #tArgs > 2 then
print("Usage: alias <alias> <program>") print("Usage: alias <alias> <program>")

View File

@ -1,4 +1,3 @@
local tApis = {} local tApis = {}
for k, v in pairs(_G) do for k, v in pairs(_G) do
if type(k) == "string" and type(v) == "table" and k ~= "_G" then if type(k) == "string" and type(v) == "table" and k ~= "_G" then

View File

@ -1,4 +1,3 @@
local tArgs = { ... } local tArgs = { ... }
if #tArgs < 1 then if #tArgs < 1 then
print("Usage: cd <path>") print("Usage: cd <path>")

View File

@ -1,4 +1,3 @@
if not commands then if not commands then
printError("Requires a Command Computer.") printError("Requires a Command Computer.")
return return

View File

@ -1,4 +1,3 @@
local tArgs = { ... } local tArgs = { ... }
if not commands then if not commands then
printError("Requires a Command Computer.") printError("Requires a Command Computer.")

View File

@ -1,4 +1,3 @@
local tArgs = { ... } local tArgs = { ... }
if #tArgs < 2 then if #tArgs < 2 then
print("Usage: cp <source> <destination>") print("Usage: cp <source> <destination>")

View File

@ -9,9 +9,18 @@ for i = 1, args.n do
local files = fs.find(shell.resolve(args[i])) local files = fs.find(shell.resolve(args[i]))
if #files > 0 then if #files > 0 then
for _, file in ipairs(files) do for _, file in ipairs(files) do
local ok, err = pcall(fs.delete, file) if fs.isReadOnly(file) then
if not ok then printError("Cannot delete read-only file /" .. file)
printError((err:gsub("^pcall: ", ""))) elseif fs.isDriveRoot(file) then
printError("Cannot delete mount /" .. file)
if fs.isDir(file) then
print("To delete its contents run rm /" .. fs.combine(file, "*"))
end
else
local ok, err = pcall(fs.delete, file)
if not ok then
printError((err:gsub("^pcall: ", "")))
end
end end
end end
else else

View File

@ -1,4 +1,3 @@
-- Get arguments -- Get arguments
local tArgs = { ... } local tArgs = { ... }
if #tArgs == 0 then if #tArgs == 0 then

View File

@ -1,4 +1,3 @@
local tBiomes = { local tBiomes = {
"in a forest", "in a forest",
"in a pine forest", "in a pine forest",

View File

@ -1,4 +1,3 @@
-- Display the start screen -- Display the start screen
local w, h = term.getSize() local w, h = term.getSize()

View File

@ -1,4 +1,3 @@
local function printUsage() local function printUsage()
print("Usages:") print("Usages:")
print("gps host") print("gps host")

View File

@ -1,4 +1,3 @@
local function printUsage() local function printUsage()
print("Usages:") print("Usages:")
print("pastebin put <filename>") print("pastebin put <filename>")

View File

@ -1,4 +1,3 @@
local function printUsage() local function printUsage()
print("Usage:") print("Usage:")
print("wget <url> [filename]") print("wget <url> [filename]")

View File

@ -1,4 +1,3 @@
local sDrive = nil local sDrive = nil
local tArgs = { ... } local tArgs = { ... }
if #tArgs > 0 then if #tArgs > 0 then

View File

@ -1,4 +1,3 @@
local function printUsage() local function printUsage()
print("Usages:") print("Usages:")
print("label get") print("label get")

View File

@ -1,4 +1,3 @@
local tArgs = { ... } local tArgs = { ... }
-- Get all the files in the directory -- Get all the files in the directory

View File

@ -1,4 +1,3 @@
local tArgs = { ... } local tArgs = { ... }
if #tArgs > 0 then if #tArgs > 0 then
print("This is an interactive Lua prompt.") print("This is an interactive Lua prompt.")
@ -67,6 +66,13 @@ while bRunning do
if s:match("%S") and tCommandHistory[#tCommandHistory] ~= s then if s:match("%S") and tCommandHistory[#tCommandHistory] ~= s then
table.insert(tCommandHistory, s) table.insert(tCommandHistory, s)
end end
if settings.get("lua.warn_against_use_of_local") and s:match("^%s*local%s+") then
if term.isColour() then
term.setTextColour(colours.yellow)
end
print("To access local variables in later inputs, remove the local keyword.")
term.setTextColour(colours.white)
end
local nForcePrint = 0 local nForcePrint = 0
local func, e = load(s, "=lua", "t", tEnv) local func, e = load(s, "=lua", "t", tEnv)

View File

@ -1,4 +1,3 @@
local tArgs = { ... } local tArgs = { ... }
if #tArgs < 2 then if #tArgs < 2 then
print("Usage: mv <source> <destination>") print("Usage: mv <source> <destination>")
@ -8,12 +7,35 @@ end
local sSource = shell.resolve(tArgs[1]) local sSource = shell.resolve(tArgs[1])
local sDest = shell.resolve(tArgs[2]) local sDest = shell.resolve(tArgs[2])
local tFiles = fs.find(sSource) local tFiles = fs.find(sSource)
local function sanity_checks(source, dest)
if fs.exists(dest) then
printError("Destination exists")
return false
elseif fs.isReadOnly(dest) then
printError("Destination is read-only")
return false
elseif fs.isDriveRoot(source) then
printError("Cannot move mount /" .. source)
return false
elseif fs.isReadOnly(source) then
printError("Cannot move read-only file /" .. source)
return false
end
return true
end
if #tFiles > 0 then if #tFiles > 0 then
for _, sFile in ipairs(tFiles) do for _, sFile in ipairs(tFiles) do
if fs.isDir(sDest) then if fs.isDir(sDest) then
fs.move(sFile, fs.combine(sDest, fs.getName(sFile))) local dest = fs.combine(sDest, fs.getName(sFile))
if sanity_checks(sFile, dest) then
fs.move(sFile, dest)
end
elseif #tFiles == 1 then elseif #tFiles == 1 then
fs.move(sFile, sDest) if sanity_checks(sFile, sDest) then
fs.move(sFile, sDest)
end
else else
printError("Cannot overwrite file multiple times") printError("Cannot overwrite file multiple times")
return return

View File

@ -1,4 +1,3 @@
local bAll = false local bAll = false
local tArgs = { ... } local tArgs = { ... }
if #tArgs > 0 and tArgs[1] == "all" then if #tArgs > 0 and tArgs[1] == "all" then

View File

@ -1,4 +1,3 @@
local tArgs = { ... } local tArgs = { ... }
local function printUsage() local function printUsage()

View File

@ -1,4 +1,3 @@
-- Find modems -- Find modems
local tModems = {} local tModems = {}
for _, sModem in ipairs(peripheral.getNames()) do for _, sModem in ipairs(peripheral.getNames()) do

View File

@ -1,4 +1,3 @@
local tArgs = { ... } local tArgs = { ... }
local function printUsage() local function printUsage()

View File

@ -10,6 +10,12 @@ local sDest = shell.resolve(tArgs[2])
if not fs.exists(sSource) then if not fs.exists(sSource) then
printError("No matching files") printError("No matching files")
return return
elseif fs.isDriveRoot(sSource) then
printError("Can't rename mounts")
return
elseif fs.isReadOnly(sSource) then
printError("Source is read-only")
return
elseif fs.exists(sDest) then elseif fs.exists(sDest) then
printError("Destination exists") printError("Destination exists")
return return

View File

@ -1,4 +1,3 @@
local tArgs = { ... } local tArgs = { ... }
if #tArgs < 1 then if #tArgs < 1 then
print("Usage: type <path>") print("Usage: type <path>")
@ -15,4 +14,3 @@ if fs.exists(sPath) then
else else
print("No such path") print("No such path")
end end

View File

@ -1,4 +1,3 @@
-- --
-- Lua IDE -- Lua IDE
-- Made by GravityScore -- Made by GravityScore

File diff suppressed because one or more lines are too long

View File

@ -30,12 +30,14 @@
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.io.BufferedWriter;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.Writer; import java.io.Writer;
import java.nio.channels.Channels; import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel; import java.nio.channels.WritableByteChannel;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.*; import java.util.*;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Condition;
@ -58,6 +60,8 @@
*/ */
public class ComputerTestDelegate public class ComputerTestDelegate
{ {
private static final File REPORT_PATH = new File( "test-files/luacov.report.out" );
private static final Logger LOG = LogManager.getLogger( ComputerTestDelegate.class ); private static final Logger LOG = LogManager.getLogger( ComputerTestDelegate.class );
private static final long TICK_TIME = TimeUnit.MILLISECONDS.toNanos( 50 ); private static final long TICK_TIME = TimeUnit.MILLISECONDS.toNanos( 50 );
@ -77,12 +81,15 @@ public class ComputerTestDelegate
private final Condition hasFinished = lock.newCondition(); private final Condition hasFinished = lock.newCondition();
private boolean finished = false; private boolean finished = false;
private Map<String, Map<Double, Double>> finishedWith;
@BeforeEach @BeforeEach
public void before() throws IOException public void before() throws IOException
{ {
ComputerCraft.logPeripheralErrors = true; ComputerCraft.logPeripheralErrors = true;
if( REPORT_PATH.delete() ) ComputerCraft.log.info( "Deleted previous coverage report." );
Terminal term = new Terminal( 78, 20 ); Terminal term = new Terminal( 78, 20 );
IWritableMount mount = new FileMount( new File( "test-files/mount" ), 10_000_000 ); IWritableMount mount = new FileMount( new File( "test-files/mount" ), 10_000_000 );
@ -264,6 +271,13 @@ else if( !computer.isOn() )
try try
{ {
finished = true; finished = true;
if( arguments.length > 0 )
{
@SuppressWarnings( "unchecked" )
Map<String, Map<Double, Double>> finished = (Map<String, Map<Double, Double>>) arguments[0];
finishedWith = finished;
}
hasFinished.signal(); hasFinished.signal();
} }
finally finally
@ -281,7 +295,7 @@ else if( !computer.isOn() )
} }
@AfterEach @AfterEach
public void after() throws InterruptedException public void after() throws InterruptedException, IOException
{ {
try try
{ {
@ -316,6 +330,14 @@ public void after() throws InterruptedException
// And shutdown // And shutdown
computer.shutdown(); computer.shutdown();
} }
if( finishedWith != null )
{
try( BufferedWriter writer = Files.newBufferedWriter( REPORT_PATH.toPath() ) )
{
new LuaCoverage( finishedWith ).write( writer );
}
}
} }
@TestFactory @TestFactory

View File

@ -0,0 +1,158 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core;
import com.google.common.base.Strings;
import dan200.computercraft.ComputerCraft;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import org.squiddev.cobalt.Prototype;
import org.squiddev.cobalt.compiler.CompileException;
import org.squiddev.cobalt.compiler.LuaC;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
class LuaCoverage
{
private static final Path ROOT = new File( "src/main/resources/assets/computercraft/lua" ).toPath();
private static final Path BIOS = ROOT.resolve( "bios.lua" );
private static final Path APIS = ROOT.resolve( "rom/apis" );
private static final Path SHELL = ROOT.resolve( "rom/programs/shell.lua" );
private static final Path MULTISHELL = ROOT.resolve( "rom/programs/advanced/multishell.lua" );
private static final Path TREASURE = ROOT.resolve( "treasure" );
private final Map<String, Map<Double, Double>> coverage;
private final String blank;
private final String zero;
private final String countFormat;
LuaCoverage( Map<String, Map<Double, Double>> coverage )
{
this.coverage = coverage;
int max = (int) coverage.values().stream()
.flatMapToDouble( x -> x.values().stream().mapToDouble( y -> y ) )
.max().orElse( 0 );
int maxLen = Math.max( 1, (int) Math.ceil( Math.log10( max ) ) );
blank = Strings.repeat( " ", maxLen + 1 );
zero = Strings.repeat( "*", maxLen ) + "0";
countFormat = "%" + (maxLen + 1) + "d";
}
void write( Writer out ) throws IOException
{
Files.find( ROOT, Integer.MAX_VALUE, ( path, attr ) -> attr.isRegularFile() && !path.startsWith( TREASURE ) ).forEach( path -> {
Path relative = ROOT.relativize( path );
String full = relative.toString().replace( '\\', '/' );
if( !full.endsWith( ".lua" ) ) return;
Map<Double, Double> files = Stream.of(
coverage.remove( "/" + full ),
path.equals( BIOS ) ? coverage.remove( "bios.lua" ) : null,
path.equals( SHELL ) ? coverage.remove( "shell.lua" ) : null,
path.equals( MULTISHELL ) ? coverage.remove( "multishell.lua" ) : null,
path.startsWith( APIS ) ? coverage.remove( path.getFileName().toString() ) : null
)
.filter( Objects::nonNull )
.flatMap( x -> x.entrySet().stream() )
.collect( Collectors.toMap( Map.Entry::getKey, Map.Entry::getValue, Double::sum ) );
try
{
writeCoverageFor( out, path, files );
}
catch( IOException e )
{
throw new UncheckedIOException( e );
}
} );
for( String filename : coverage.keySet() )
{
if( filename.startsWith( "/test-rom/" ) ) continue;
ComputerCraft.log.warn( "Unknown file {}", filename );
}
}
private void writeCoverageFor( Writer out, Path fullName, Map<Double, Double> visitedLines ) throws IOException
{
if( !Files.exists( fullName ) )
{
ComputerCraft.log.error( "Cannot locate file {}", fullName );
return;
}
IntSet activeLines = getActiveLines( fullName.toFile() );
out.write( "==============================================================================\n" );
out.write( fullName.toString().replace( '\\', '/' ) );
out.write( "\n" );
out.write( "==============================================================================\n" );
try( BufferedReader reader = Files.newBufferedReader( fullName ) )
{
String line;
int lineNo = 0;
while( (line = reader.readLine()) != null )
{
lineNo++;
Double count = visitedLines.get( (double) lineNo );
if( count != null )
{
out.write( String.format( countFormat, count.intValue() ) );
}
else if( activeLines.contains( lineNo ) )
{
out.write( zero );
}
else
{
out.write( blank );
}
out.write( ' ' );
out.write( line );
out.write( "\n" );
}
}
}
private static IntSet getActiveLines( File file ) throws IOException
{
IntSet activeLines = new IntOpenHashSet();
try( InputStream stream = new FileInputStream( file ) )
{
Prototype proto = LuaC.compile( stream, "@" + file.getPath() );
Queue<Prototype> queue = new ArrayDeque<>();
queue.add( proto );
while( (proto = queue.poll()) != null )
{
int[] lines = proto.lineinfo;
if( lines != null )
{
for( int line : lines )
{
activeLines.add( line );
}
}
if( proto.p != null ) Collections.addAll( queue, proto.p );
}
}
catch( CompileException e )
{
throw new IllegalStateException( "Cannot compile", e );
}
return activeLines;
}
}

View File

@ -483,6 +483,49 @@ local function pending(name)
test_stack.n = n - 1 test_stack.n = n - 1
end end
local native_co_create, native_loadfile = coroutine.create, loadfile
local line_counts = {}
if cct_test then
local string_sub, debug_getinfo = string.sub, debug.getinfo
local function debug_hook(_, line_nr)
local name = debug_getinfo(2, "S").source
if string_sub(name, 1, 1) ~= "@" then return end
name = string_sub(name, 2)
local file = line_counts[name]
if not file then file = {} line_counts[name] = file end
file[line_nr] = (file[line_nr] or 0) + 1
end
coroutine.create = function(...)
local co = native_co_create(...)
debug.sethook(co, debug_hook, "l")
return co
end
local expect = require "cc.expect".expect
_G.native_loadfile = native_loadfile
_G.loadfile = function(filename, mode, env)
-- Support the previous `loadfile(filename, env)` form instead.
if type(mode) == "table" and env == nil then
mode, env = nil, mode
end
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.combine(filename, ""), mode, env)
file.close()
return func, err
end
debug.sethook(debug_hook, "l")
end
local arg = ... local arg = ...
if arg == "--help" or arg == "-h" then if arg == "--help" or arg == "-h" then
io.write("Usage: mcfly [DIR]\n") io.write("Usage: mcfly [DIR]\n")
@ -648,4 +691,11 @@ if test_status.pending > 0 then
end end
term.setTextColour(colours.white) io.write(info .. "\n") term.setTextColour(colours.white) io.write(info .. "\n")
-- Restore hook stubs
debug.sethook(nil, "l")
coroutine.create = native_co_create
_G.loadfile = native_loadfile
if cct_test then cct_test.finish(line_counts) end
if howlci then howlci.log("debug", info) sleep(3) end if howlci then howlci.log("debug", info) sleep(3) end

View File

@ -12,6 +12,21 @@ describe("The fs library", function()
end) end)
end) end)
describe("fs.isDriveRoot", function()
it("validates arguments", function()
fs.isDriveRoot("")
expect.error(fs.isDriveRoot, nil):eq("bad argument #1 (expected string, got nil)")
end)
it("correctly identifies drive roots", function()
expect(fs.isDriveRoot("/rom")):eq(true)
expect(fs.isDriveRoot("/")):eq(true)
expect(fs.isDriveRoot("/rom/startup.lua")):eq(false)
expect(fs.isDriveRoot("/rom/programs/delete.lua")):eq(false)
end)
end)
describe("fs.list", function() describe("fs.list", function()
it("fails on files", function() it("fails on files", function()
expect.error(fs.list, "rom/startup.lua"):eq("/rom/startup.lua: Not a directory") expect.error(fs.list, "rom/startup.lua"):eq("/rom/startup.lua: Not a directory")

View File

@ -1,8 +1,44 @@
--- Tests the io library is (mostly) consistent with PUC Lua. --- Tests the io library is (mostly) consistent with PUC Lua.
-- --
-- These tests are based on the tests for Lua 5.1 -- These tests are based on the tests for Lua 5.1 and 5.3
describe("The io library", function() describe("The io library", function()
local file = "/test-files/tmp.txt"
local otherfile = "/test-files/tmp2.txt"
local t = '0123456789'
for _ = 1, 12 do t = t .. t end
assert(#t == 10 * 2 ^ 12)
local function read_all(f)
local h = fs.open(f, "rb")
local contents = h.readAll()
h.close()
return contents
end
local function write_file(f, contents)
local h = fs.open(f, "wb")
h.write(contents)
h.close()
end
local function setup()
write_file(file, "\"<EFBFBD>lo\"{a}\nsecond line\nthird line \n<EFBFBD>fourth_line\n\n\9\9 3450\n")
end
describe("io.close", function()
it("cannot close stdin", function()
expect{ io.stdin:close() }:same { nil, "attempt to close standard stream" }
end)
it("cannot close stdout", function()
expect{ io.stdout:close() }:same { nil, "attempt to close standard stream" }
end)
it("cannot close stdout", function()
expect{ io.stdout:close() }:same { nil, "attempt to close standard stream" }
end)
end)
it("io.input on a handle returns that handle", function() it("io.input on a handle returns that handle", function()
expect(io.input(io.stdin)):equals(io.stdin) expect(io.input(io.stdin)):equals(io.stdin)
end) end)
@ -11,11 +47,16 @@ describe("The io library", function()
expect(io.output(io.stdout)):equals(io.stdout) expect(io.output(io.stdout)):equals(io.stdout)
end) end)
it("defines a __name field", function()
expect(getmetatable(io.input()).__name):eq("FILE*")
end)
describe("io.type", function() describe("io.type", function()
it("returns file on handles", function() it("returns file on handles", function()
local handle = io.input() local handle = io.input()
expect(handle):type("table") expect(handle):type("table")
expect(io.type(handle)):equals("file") expect(io.type(handle)):equals("file")
expect(io.type(io.stdin)):equals("file")
end) end)
it("returns nil on values", function() it("returns nil on values", function()
@ -33,6 +74,88 @@ describe("The io library", function()
expect.error(io.lines, ""):eq("/: No such file") expect.error(io.lines, ""):eq("/: No such file")
expect.error(io.lines, false):eq("bad argument #1 (expected string, got boolean)") expect.error(io.lines, false):eq("bad argument #1 (expected string, got boolean)")
end) end)
it("closes the file", function()
setup()
local n = 0
local f = io.lines(file)
while f() do n = n + 1 end
expect(n):eq(6)
expect.error(f):eq("file is already closed")
expect.error(f):eq("file is already closed")
end)
it("can copy a file", function()
setup()
local n = 0
io.output(otherfile)
for l in io.lines(file) do
io.write(l, "\n")
n = n + 1
end
io.close()
expect(n):eq(6)
io.input(file)
local f = io.open(otherfile):lines()
local n = 0
for l in io.lines() do
expect(l):eq(f())
n = n + 1
end
expect(n):eq(6)
end)
it("does not close on a normal file handle", function()
setup()
local f = assert(io.open(file))
local n = 0
for _ in f:lines() do n = n + 1 end
expect(n):eq(6)
expect(tostring(f):sub(1, 5)):eq("file ")
assert(f:close())
expect(tostring(f)):eq("file (closed)")
expect(io.type(f)):eq("closed file")
end)
it("accepts multiple arguments", function()
write_file(file, "0123456789\n")
for a, b in io.lines(file, 1, 1) do
if a == "\n" then
expect(b):eq(nil)
else
expect(tonumber(a)):eq(tonumber(b) - 1)
end
end
for a, b, c in io.lines(file, 1, 2, "a") do
expect(a):eq("0")
expect(b):eq("12")
expect(c):eq("3456789\n")
end
for a, b, c in io.lines(file, "a", 0, 1) do
if a == "" then break end
expect(a):eq("0123456789\n")
expect(b):eq(nil)
expect(c):eq(nil)
end
write_file(file, "00\n10\n20\n30\n40\n")
for a, b in io.lines(file, "n", "n") do
if a == 40 then
expect(b):eq(nil)
else
expect(a):eq(b - 10)
end
end
end)
end) end)
describe("io.open", function() describe("io.open", function()
@ -44,6 +167,22 @@ describe("The io library", function()
expect.error(io.open, "", false):eq("bad argument #2 (expected string, got boolean)") expect.error(io.open, "", false):eq("bad argument #2 (expected string, got boolean)")
end) end)
it("checks the mode", function()
io.open(file, "w"):close()
-- This really should be invalid mode, but I'll live.
expect.error(io.open, file, "rw"):str_match("Unsupported mode")
-- TODO: expect.error(io.open, file, "rb+"):str_match("Unsupported mode")
expect.error(io.open, file, "r+bk"):str_match("Unsupported mode")
expect.error(io.open, file, ""):str_match("Unsupported mode")
expect.error(io.open, file, "+"):str_match("Unsupported mode")
expect.error(io.open, file, "b"):str_match("Unsupported mode")
assert(io.open(file, "r+b")):close()
assert(io.open(file, "r+")):close()
assert(io.open(file, "rb")):close()
end)
it("returns an error message on non-existent files", function() it("returns an error message on non-existent files", function()
local a, b = io.open('xuxu_nao_existe') local a, b = io.open('xuxu_nao_existe')
expect(a):equals(nil) expect(a):equals(nil)
@ -51,26 +190,139 @@ describe("The io library", function()
end) end)
end) end)
pending("io.output allows redirecting and seeking", function() describe("a readable handle", function()
fs.delete("/tmp/io_spec.txt") it("cannot be written to", function()
write_file(file, "")
io.input(file)
expect { io.input():write("xuxu") }:same { nil, "file is not writable" }
io.input(io.stdin)
end)
io.output("/tmp/io_spec.txt") it("supports various modes", function()
write_file(file, "alo\n " .. t .. " ;end of file\n")
expect(io.output()):not_equals(io.stdout) io.input(file)
expect(io.read()):eq("alo")
expect(io.read(1)):eq(' ')
expect(io.read(#t)):eq(t)
expect(io.read(1)):eq(' ')
expect(io.read(0))
expect(io.read('*a')):eq(';end of file\n')
expect(io.read(0)):eq(nil)
expect(io.close(io.input())):eq(true)
expect(io.output():seek()):equal(0) fs.delete(file)
assert(io.write("alo alo")) end)
expect(io.output():seek()):equal(#"alo alo")
expect(io.output():seek("cur", -3)):equal(#"alo alo" - 3)
assert(io.write("joao"))
expect(io.output():seek("end"):equal(#"alo joao"))
expect(io.output():seek("set")):equal(0) it("support seeking", function()
setup()
io.input(file)
assert(io.write('"<22>lo"', "{a}\n", "second line\n", "third line \n")) expect(io.read(0)):eq("") -- not eof
assert(io.write('<EFBFBD>fourth_line')) expect(io.read(5, '*l')):eq('"<22>lo"')
expect(io.read(0)):eq("")
expect(io.read()):eq("second line")
local x = io.input():seek()
expect(io.read()):eq("third line ")
assert(io.input():seek("set", x))
expect(io.read('*l')):eq("third line ")
expect(io.read(1)):eq("<EFBFBD>")
expect(io.read(#"fourth_line")):eq("fourth_line")
assert(io.input():seek("cur", -#"fourth_line"))
expect(io.read()):eq("fourth_line")
expect(io.read()):eq("") -- empty line
expect(io.read(8)):eq('\9\9 3450') -- FIXME: Not actually supported
expect(io.read(1)):eq('\n')
expect(io.read(0)):eq(nil) -- end of file
expect(io.read(1)):eq(nil) -- end of file
expect(({ io.read(1) })[2]):eq(nil)
expect(io.read()):eq(nil) -- end of file
expect(({ io.read() })[2]):eq(nil)
expect(io.read('*n')):eq(nil) -- end of file
expect(({ io.read('*n') })[2]):eq(nil)
expect(io.read('*a')):eq('') -- end of file (OK for `*a')
expect(io.read('*a')):eq('') -- end of file (OK for `*a')
io.output(io.stdout) io.close(io.input())
expect(io.output()):equals(io.stdout) end)
it("supports the 'L' mode", function()
write_file(file, "\n\nline\nother")
io.input(file)
expect(io.read"L"):eq("\n")
expect(io.read"L"):eq("\n")
expect(io.read"L"):eq("line\n")
expect(io.read"L"):eq("other")
expect(io.read"L"):eq(nil)
io.input():close()
local f = assert(io.open(file))
local s = ""
for l in f:lines("L") do s = s .. l end
expect(s):eq("\n\nline\nother")
f:close()
io.input(file)
s = ""
for l in io.lines(nil, "L") do s = s .. l end
expect(s):eq("\n\nline\nother")
io.input():close()
s = ""
for l in io.lines(file, "L") do s = s .. l end
expect(s):eq("\n\nline\nother")
s = ""
for l in io.lines(file, "l") do s = s .. l end
expect(s):eq("lineother")
write_file(file, "a = 10 + 34\na = 2*a\na = -a\n")
local t = {}
load(io.lines(file, "L"), nil, nil, t)()
expect(t.a):eq(-((10 + 34) * 2))
end)
end)
describe("a writable handle", function()
it("supports seeking", function()
fs.delete(file)
io.output(file)
expect(io.output()):not_equals(io.stdout)
expect(io.output():seek()):equal(0)
assert(io.write("alo alo"))
expect(io.output():seek()):equal(#"alo alo")
expect(io.output():seek("cur", -3)):equal(#"alo alo" - 3)
assert(io.write("joao"))
expect(io.output():seek("end")):equal(#"alo joao")
expect(io.output():seek("set")):equal(0)
assert(io.write('"<22>lo"', "{a}\n", "second line\n", "third line \n"))
assert(io.write('<EFBFBD>fourth_line'))
io.output(io.stdout)
expect(io.output()):equals(io.stdout)
end)
it("supports appending", function()
io.output(file)
io.write("alo\n")
io.close()
expect.error(io.write)
local f = io.open(file, "a")
io.output(f)
assert(io.write(' ' .. t .. ' '))
assert(io.write(';', 'end of file\n'))
f:flush()
io.flush()
f:close()
expect(read_all(file)):eq("alo\n " .. t .. " ;end of file\n")
end)
end) end)
end) end)

View File

@ -51,7 +51,7 @@ describe("The peripheral library", function()
it_modem("has the correct error location", function() it_modem("has the correct error location", function()
expect.error(function() peripheral.call("top", "isOpen", false) end) expect.error(function() peripheral.call("top", "isOpen", false) end)
:str_match("^peripheral_spec.lua:%d+: bad argument #1 %(number expected, got boolean%)$") :str_match("^[^:]+:%d+: bad argument #1 %(number expected, got boolean%)$")
end) end)
end) end)

View File

@ -49,7 +49,7 @@ describe("The textutils library", function()
describe("textutils.empty_json_array", function() describe("textutils.empty_json_array", function()
it("is immutable", function() it("is immutable", function()
expect.error(function() textutils.empty_json_array[1] = true end) expect.error(function() textutils.empty_json_array[1] = true end)
:eq("textutils_spec.lua:51: attempt to mutate textutils.empty_json_array") :str_match("^[^:]+:51: attempt to mutate textutils.empty_json_array$")
end) end)
end) end)

View File

@ -28,6 +28,8 @@ describe("The Lua base library", function()
end) end)
describe("loadfile", function() describe("loadfile", function()
local loadfile = _G.native_loadfile or loadfile
local function make_file() local function make_file()
local tmp = fs.open("test-files/out.lua", "w") local tmp = fs.open("test-files/out.lua", "w")
tmp.write("return _ENV") tmp.write("return _ENV")

View File

@ -27,7 +27,7 @@ describe("cc.expect", function()
worker() worker()
end end
expect.error(trampoline):eq("expect_spec.lua:27: bad argument #1 to 'worker' (expected string, got nil)") expect.error(trampoline):str_match("^[^:]*expect_spec.lua:27: bad argument #1 to 'worker' %(expected string, got nil%)$")
end) end)
end) end)

View File

@ -42,7 +42,15 @@ describe("The rm program", function()
it("errors when trying to delete a read-only file", function() it("errors when trying to delete a read-only file", function()
expect(capture(stub, "rm /rom/startup.lua")) expect(capture(stub, "rm /rom/startup.lua"))
:matches { ok = true, output = "", error = "/rom/startup.lua: Access denied\n" } :matches { ok = true, output = "", error = "Cannot delete read-only file /rom/startup.lua\n" }
end)
it("errors when trying to delete the root mount", function()
expect(capture(stub, "rm /")):matches {
ok = true,
output = "To delete its contents run rm /*\n",
error = "Cannot delete mount /\n",
}
end) end)
it("errors when a glob fails to match", function() it("errors when a glob fails to match", function()

View File

@ -1,11 +1,13 @@
local capture = require "test_helpers".capture_program local capture = require "test_helpers".capture_program
describe("The move program", function() describe("The move program", function()
local function cleanup() fs.delete("/test-files/move") end
local function touch(file) local function touch(file)
io.open(file, "w"):close() io.open(file, "w"):close()
end end
it("move a file", function() it("move a file", function()
cleanup()
touch("/test-files/move/a.txt") touch("/test-files/move/a.txt")
shell.run("move /test-files/move/a.txt /test-files/move/b.txt") shell.run("move /test-files/move/a.txt /test-files/move/b.txt")
@ -14,11 +16,57 @@ describe("The move program", function()
expect(fs.exists("/test-files/move/b.txt")):eq(true) expect(fs.exists("/test-files/move/b.txt")):eq(true)
end) end)
it("try to move a not existing file", function() it("moves a file to a directory", function()
cleanup()
touch("/test-files/move/a.txt")
fs.makeDir("/test-files/move/a")
expect(capture(stub, "move /test-files/move/a.txt /test-files/move/a"))
:matches { ok = true }
expect(fs.exists("/test-files/move/a.txt")):eq(false)
expect(fs.exists("/test-files/move/a/a.txt")):eq(true)
end)
it("fails when moving a file which doesn't exist", function()
expect(capture(stub, "move nothing destination")) expect(capture(stub, "move nothing destination"))
:matches { ok = true, output = "", error = "No matching files\n" } :matches { ok = true, output = "", error = "No matching files\n" }
end) end)
it("fails when overwriting an existing file", function()
cleanup()
touch("/test-files/move/a.txt")
expect(capture(stub, "move /test-files/move/a.txt /test-files/move/a.txt"))
:matches { ok = true, output = "", error = "Destination exists\n" }
end)
it("fails when moving to read-only locations", function()
cleanup()
touch("/test-files/move/a.txt")
expect(capture(stub, "move /test-files/move/a.txt /rom/test.txt"))
:matches { ok = true, output = "", error = "Destination is read-only\n" }
end)
it("fails when moving from read-only locations", function()
expect(capture(stub, "move /rom/startup.lua /test-files/move/not-exist.txt"))
:matches { ok = true, output = "", error = "Cannot move read-only file /rom/startup.lua\n" }
end)
it("fails when moving mounts", function()
expect(capture(stub, "move /rom /test-files/move/rom"))
:matches { ok = true, output = "", error = "Cannot move mount /rom\n" }
end)
it("fails when moving a file multiple times", function()
cleanup()
touch("/test-files/move/a.txt")
touch("/test-files/move/b.txt")
expect(capture(stub, "move /test-files/move/*.txt /test-files/move/c.txt"))
:matches { ok = true, output = "", error = "Cannot overwrite file multiple times\n" }
end)
it("displays the usage with no arguments", function() it("displays the usage with no arguments", function()
expect(capture(stub, "move")) expect(capture(stub, "move"))
:matches { ok = true, output = "Usage: mv <source> <destination>\n", error = "" } :matches { ok = true, output = "Usage: mv <source> <destination>\n", error = "" }

View File

@ -26,13 +26,23 @@ describe("The rename program", function()
:matches { ok = true, output = "", error = "Destination exists\n" } :matches { ok = true, output = "", error = "Destination exists\n" }
end) end)
it("fails when copying to read-only locations", function() it("fails when renaming to read-only locations", function()
touch("/test-files/rename/d.txt") touch("/test-files/rename/d.txt")
expect(capture(stub, "rename /test-files/rename/d.txt /rom/test.txt")) expect(capture(stub, "rename /test-files/rename/d.txt /rom/test.txt"))
:matches { ok = true, output = "", error = "Destination is read-only\n" } :matches { ok = true, output = "", error = "Destination is read-only\n" }
end) end)
it("fails when renaming from read-only locations", function()
expect(capture(stub, "rename /rom/startup.lua /test-files/rename/d.txt"))
:matches { ok = true, output = "", error = "Source is read-only\n" }
end)
it("fails when renaming mounts", function()
expect(capture(stub, "rename /rom /test-files/rename/rom"))
:matches { ok = true, output = "", error = "Can't rename mounts\n" }
end)
it("displays the usage when given no arguments", function() it("displays the usage when given no arguments", function()
expect(capture(stub, "rename")) expect(capture(stub, "rename"))
:matches { ok = true, output = "Usage: rename <source> <destination>\n", error = "" } :matches { ok = true, output = "Usage: rename <source> <destination>\n", error = "" }

View File

@ -23,4 +23,3 @@ describe("The type program", function()
end) end)
end) end)

View File

@ -11,8 +11,13 @@ for path in pathlib.Path("src").glob("**/*"):
continue continue
with path.open(encoding="utf-8") as file: with path.open(encoding="utf-8") as file:
has_dos, has_trailing, needs_final = False, False, False has_dos, has_trailing, first, count = False, False, 0, True
for i, line in enumerate(file): for i, line in enumerate(file):
if first:
first = False
if line.strip() == "":
print("%s has empty first line" % path)
if len(line) >= 2 and line[-2] == "\r" and line[-1] == "\n" and not has_line: if len(line) >= 2 and line[-2] == "\r" and line[-1] == "\n" and not has_line:
print("%s has contains '\\r\\n' on line %d" % (path, i + 1)) print("%s has contains '\\r\\n' on line %d" % (path, i + 1))
problems = has_dos = True problems = has_dos = True
@ -21,8 +26,15 @@ for path in pathlib.Path("src").glob("**/*"):
print("%s has trailing whitespace on line %d" % (path, i + 1)) print("%s has trailing whitespace on line %d" % (path, i + 1))
problems = has_trailing = True problems = has_trailing = True
if line is not None and len(line) >= 1 and line[-1] != "\n": if len(line) == 0 or line[-1] != "\n":
print("%s should end with '\\n'" % path) count = 0
elif line.strip() == "":
count += 1
else:
count = 1
if count != 1:
print("%s should have 1 trailing lines, but has %d" % (path, count))
problems = True problems = True
if problems: if problems: