From 1f910ee2bac4eee99509138f7204a417349c2dec Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Fri, 28 Oct 2022 23:40:55 +0100 Subject: [PATCH 1/7] Use a separate object for tracking TickScheduler state This allows us to use non-TileGeneric block entities. This is a clever trick which will help us later! --- .../shared/common/TileGeneric.java | 8 ---- .../peripheral/modem/wired/TileCable.java | 7 +-- .../modem/wired/TileWiredModemFull.java | 7 +-- .../modem/wireless/TileWirelessModem.java | 5 ++- .../peripheral/monitor/ServerMonitor.java | 2 +- .../peripheral/monitor/TileMonitor.java | 4 +- .../shared/util/TickScheduler.java | 45 +++++++++++++------ 7 files changed, 47 insertions(+), 31 deletions(-) diff --git a/src/main/java/dan200/computercraft/shared/common/TileGeneric.java b/src/main/java/dan200/computercraft/shared/common/TileGeneric.java index cc24dd1c6..9ade77ceb 100644 --- a/src/main/java/dan200/computercraft/shared/common/TileGeneric.java +++ b/src/main/java/dan200/computercraft/shared/common/TileGeneric.java @@ -19,17 +19,9 @@ import net.minecraft.util.math.BlockRayTraceResult; import net.minecraftforge.common.util.Constants; import javax.annotation.Nonnull; -import java.util.concurrent.atomic.AtomicBoolean; public abstract class TileGeneric extends TileEntity { - /** - * Is this block enqueued to be updated next tick? This should only be read/written by the tick scheduler. - * - * @see dan200.computercraft.shared.util.TickScheduler - */ - public final AtomicBoolean scheduled = new AtomicBoolean(); - public TileGeneric( TileEntityType type ) { super( type ); diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/TileCable.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/TileCable.java index e7d34fbb1..8a6e46a8a 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/TileCable.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/TileCable.java @@ -89,8 +89,9 @@ public class TileCable extends TileGeneric private final WiredModemElement cable = new CableElement(); private LazyOptional elementCap; private final IWiredNode node = cable.getNode(); + private final TickScheduler.Token tickToken = new TickScheduler.Token( this ); private final WiredModemPeripheral modem = new WiredModemPeripheral( - new ModemState( () -> TickScheduler.schedule( this ) ), + new ModemState( () -> TickScheduler.schedule( tickToken ) ), cable ) { @@ -170,7 +171,7 @@ public class TileCable extends TileGeneric public void onLoad() { super.onLoad(); - TickScheduler.schedule( this ); + TickScheduler.schedule( tickToken ); } @Override @@ -246,7 +247,7 @@ public class TileCable extends TileGeneric { if( invalidPeripheral ) return; invalidPeripheral = true; - TickScheduler.schedule( this ); + TickScheduler.schedule( tickToken ); } private void refreshPeripheral() diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/TileWiredModemFull.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/TileWiredModemFull.java index 34963cc1b..d5fb986d3 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/TileWiredModemFull.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/TileWiredModemFull.java @@ -100,7 +100,8 @@ public class TileWiredModemFull extends TileGeneric private boolean destroyed = false; private boolean connectionsFormed = false; - private final ModemState modemState = new ModemState( () -> TickScheduler.schedule( this ) ); + private final TickScheduler.Token tickToken = new TickScheduler.Token( this ); + private final ModemState modemState = new ModemState( () -> TickScheduler.schedule( tickToken ) ); private final WiredModemElement element = new FullElement( this ); private LazyOptional elementCap; private final IWiredNode node = element.getNode(); @@ -181,7 +182,7 @@ public class TileWiredModemFull extends TileGeneric private void queueRefreshPeripheral( @Nonnull Direction facing ) { - if( invalidSides == 0 ) TickScheduler.schedule( this ); + if( invalidSides == 0 ) TickScheduler.schedule( tickToken ); invalidSides |= 1 << facing.ordinal(); } @@ -263,7 +264,7 @@ public class TileWiredModemFull extends TileGeneric public void onLoad() { super.onLoad(); - TickScheduler.schedule( this ); + TickScheduler.schedule( tickToken ); } @Override diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/TileWirelessModem.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/TileWirelessModem.java index f5b35696f..e4bf0827b 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/TileWirelessModem.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/TileWirelessModem.java @@ -32,7 +32,7 @@ public class TileWirelessModem extends TileGeneric Peripheral( TileWirelessModem entity ) { - super( new ModemState( () -> TickScheduler.schedule( entity ) ), entity.advanced ); + super( new ModemState( () -> TickScheduler.schedule( entity.tickToken ) ), entity.advanced ); this.entity = entity; } @@ -71,6 +71,7 @@ public class TileWirelessModem extends TileGeneric private final ModemPeripheral modem; private boolean destroyed = false; private LazyOptional modemCap; + private final TickScheduler.Token tickToken = new TickScheduler.Token( this ); public TileWirelessModem( TileEntityType type, boolean advanced ) { @@ -83,7 +84,7 @@ public class TileWirelessModem extends TileGeneric public void onLoad() { super.onLoad(); - TickScheduler.schedule( this ); + TickScheduler.schedule( tickToken ); } @Override diff --git a/src/main/java/dan200/computercraft/shared/peripheral/monitor/ServerMonitor.java b/src/main/java/dan200/computercraft/shared/peripheral/monitor/ServerMonitor.java index 056786755..2e7efc0de 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/monitor/ServerMonitor.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/monitor/ServerMonitor.java @@ -64,7 +64,7 @@ public class ServerMonitor private void markChanged() { - if( !changed.getAndSet( true ) ) TickScheduler.schedule( origin ); + if( !changed.getAndSet( true ) ) TickScheduler.schedule( origin.tickToken ); } int getTextScale() diff --git a/src/main/java/dan200/computercraft/shared/peripheral/monitor/TileMonitor.java b/src/main/java/dan200/computercraft/shared/peripheral/monitor/TileMonitor.java index 307ad4a8b..a2f63da69 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/monitor/TileMonitor.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/monitor/TileMonitor.java @@ -75,6 +75,8 @@ public class TileMonitor extends TileGeneric private int bbX, bbY, bbWidth, bbHeight; private AxisAlignedBB boundingBox; + TickScheduler.Token tickToken = new TickScheduler.Token( this ); + public TileMonitor( TileEntityType type, boolean advanced ) { super( type ); @@ -86,7 +88,7 @@ public class TileMonitor extends TileGeneric { super.onLoad(); needsValidating = true; // Same, tbh - TickScheduler.schedule( this ); + TickScheduler.schedule( tickToken ); } @Override diff --git a/src/main/java/dan200/computercraft/shared/util/TickScheduler.java b/src/main/java/dan200/computercraft/shared/util/TickScheduler.java index 84d88e1a8..491453abb 100644 --- a/src/main/java/dan200/computercraft/shared/util/TickScheduler.java +++ b/src/main/java/dan200/computercraft/shared/util/TickScheduler.java @@ -6,7 +6,7 @@ package dan200.computercraft.shared.util; import dan200.computercraft.ComputerCraft; -import dan200.computercraft.shared.common.TileGeneric; +import net.minecraft.tileentity.TileEntity; import net.minecraft.util.math.BlockPos; import net.minecraft.world.ITickList; import net.minecraft.world.World; @@ -16,6 +16,7 @@ import net.minecraftforge.fml.common.Mod; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.atomic.AtomicBoolean; /** * A thread-safe version of {@link ITickList#scheduleTick(BlockPos, Object, int)}. @@ -29,12 +30,12 @@ public final class TickScheduler { } - private static final Queue toTick = new ConcurrentLinkedDeque<>(); + private static final Queue toTick = new ConcurrentLinkedDeque<>(); - public static void schedule( TileGeneric tile ) + public static void schedule( Token token ) { - World world = tile.getLevel(); - if( world != null && !world.isClientSide && !tile.scheduled.getAndSet( true ) ) toTick.add( tile ); + World world = token.owner.getLevel(); + if( world != null && !world.isClientSide && !token.scheduled.getAndSet( true ) ) toTick.add( token ); } @SubscribeEvent @@ -42,19 +43,37 @@ public final class TickScheduler { if( event.phase != TickEvent.Phase.START ) return; - TileGeneric tile; - while( (tile = toTick.poll()) != null ) + Token token; + while( (token = toTick.poll()) != null ) { - tile.scheduled.set( false ); - if( tile.isRemoved() ) continue; + token.scheduled.set( false ); + TileEntity blockEntity = token.owner; + if( blockEntity.isRemoved() ) continue; - World world = tile.getLevel(); - BlockPos pos = tile.getBlockPos(); + World world = blockEntity.getLevel(); + BlockPos pos = blockEntity.getBlockPos(); - if( world != null && pos != null && world.isAreaLoaded( pos, 0 ) && world.getBlockEntity( pos ) == tile ) + if( world != null && world.isAreaLoaded( pos, 0 ) && world.getBlockEntity( pos ) == blockEntity ) { - world.getBlockTicks().scheduleTick( pos, tile.getBlockState().getBlock(), 0 ); + world.getBlockTicks().scheduleTick( pos, blockEntity.getBlockState().getBlock(), 0 ); } } } + + /** + * An item which can be scheduled for future ticking. + *

+ * This tracks whether the {@link TileEntity} is queued or not, as this is more efficient than maintaining a set. + * As such, it should be unique per {@link TileEntity} instance to avoid it being queued multiple times. + */ + public static class Token + { + final TileEntity owner; + final AtomicBoolean scheduled = new AtomicBoolean(); + + public Token( TileEntity owner ) + { + this.owner = owner; + } + } } From 97387556fe1a7f57cdc6a43608d55835158e2983 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sat, 29 Oct 2022 12:01:23 +0100 Subject: [PATCH 2/7] Handle file transfers inside CraftOS (#1190) - Add a new file_transfer event. This has the signature "file_transfer", TransferredFiles. TransferredFiles has a single method getFiles(), which returns a list of all transferred files. - Add a new "import" program which waits for a file_transfer event and writes files to the current directory. - If a file_transfer event is not handled (i.e. its getFiles() method is not called) within 5 seconds on the client, we display a toast informing the user on how to upload a file. --- doc/events/file_transfer.md | 41 +++++ gradle/libs.versions.toml | 4 +- .../dan200/computercraft/ComputerCraft.java | 2 + .../client/gui/ComputerScreenBase.java | 62 ++++---- .../computercraft/client/gui/ItemToast.java | 150 ++++++++++++++++++ .../core/apis/handles/ByteBufferChannel.java | 99 ++++++++++++ .../core/filesystem/ResourceMount.java | 1 + .../dan200/computercraft/shared/Config.java | 11 +- .../shared/command/CommandComputerCraft.java | 3 +- .../computer/blocks/TileComputerBase.java | 6 +- .../inventory/ContainerComputerBase.java | 22 ++- .../computer/menu/ServerInputHandler.java | 8 - .../computer/menu/ServerInputState.java | 106 +++---------- .../computer/upload/TransferredFile.java | 53 +++++++ .../computer/upload/TransferredFiles.java | 58 +++++++ .../shared/computer/upload/UploadResult.java | 9 +- .../shared/network/NetworkHandler.java | 1 - .../network/client/UploadResultMessage.java | 38 +++-- .../container/ComputerContainerData.java | 20 ++- .../network/server/ContinueUploadMessage.java | 45 ------ .../pocket/items/ItemPocketComputer.java | 2 +- .../assets/computercraft/lang/en_us.json | 7 +- .../computercraft/lua/rom/apis/textutils.lua | 3 + .../lua/rom/modules/internal/cc/import.lua | 70 ++++++++ .../lua/rom/programs/advanced/multishell.lua | 5 +- .../computercraft/lua/rom/programs/import.lua | 24 +++ .../computercraft/lua/rom/programs/shell.lua | 68 ++++++-- .../computercraft/support/CustomMatchers.java | 6 +- .../test-rom/spec/programs/shell_spec.lua | 46 ++++++ 29 files changed, 748 insertions(+), 222 deletions(-) create mode 100644 doc/events/file_transfer.md create mode 100644 src/main/java/dan200/computercraft/client/gui/ItemToast.java create mode 100644 src/main/java/dan200/computercraft/core/apis/handles/ByteBufferChannel.java create mode 100644 src/main/java/dan200/computercraft/shared/computer/upload/TransferredFile.java create mode 100644 src/main/java/dan200/computercraft/shared/computer/upload/TransferredFiles.java delete mode 100644 src/main/java/dan200/computercraft/shared/network/server/ContinueUploadMessage.java create mode 100644 src/main/resources/data/computercraft/lua/rom/modules/internal/cc/import.lua create mode 100644 src/main/resources/data/computercraft/lua/rom/programs/import.lua diff --git a/doc/events/file_transfer.md b/doc/events/file_transfer.md new file mode 100644 index 000000000..f68289c34 --- /dev/null +++ b/doc/events/file_transfer.md @@ -0,0 +1,41 @@ +--- +module: [kind=event] file_transfer +--- + +The @{file_transfer} event is queued when a user drags-and-drops a file on an open computer. + +This event contains a single argument, that in turn has a single method @{TransferredFiles.getFiles|getFiles}. This +returns the list of files that are being transferred. Each file is a @{fs.BinaryReadHandle|binary file handle} with an +additional @{TransferredFile.getName|getName} method. + +## Return values +1. @{string}: The event name +2. @{TransferredFiles}: The list of transferred files. + +## Example +Waits for a user to drop files on top of the computer, then prints the list of files and the size of each file. + +```lua +local _, files = os.pullEvent("file_transfer") +for _, file in ipairs(files.getFiles()) do + -- Seek to the end of the file to get its size, then go back to the beginning. + local size = file.seek("end") + file.seek("set", 0) + + print(file.getName() .. " " .. file.getSize()) +end +``` + +## Example +Save each transferred file to the computer's storage. + +```lua +local _, files = os.pullEvent("file_transfer") +for _, file in ipairs(files.getFiles()) do + local handle = fs.open(file.getName(), "wb") + handle.write(file.readAll()) + + handle.close() + file.close() +end +``` diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f693343ad..3d7dcd42b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,12 +18,12 @@ jqwik = "1.7.0" junit = "5.9.1" # Build tools -cctJavadoc = "1.5.1" +cctJavadoc = "1.5.2" checkstyle = "8.25" # There's a reason we're pinned on an ancient version, but I can't remember what it is. curseForgeGradle = "1.0.11" forgeGradle = "5.1.+" githubRelease = "2.2.12" -illuaminate = "0.1.0-3-g0f40379" +illuaminate = "0.1.0-7-g2a5a89c" librarian = "1.+" minotaur = "2.+" mixinGradle = "0.7.+" diff --git a/src/main/java/dan200/computercraft/ComputerCraft.java b/src/main/java/dan200/computercraft/ComputerCraft.java index c6417240b..ae597d531 100644 --- a/src/main/java/dan200/computercraft/ComputerCraft.java +++ b/src/main/java/dan200/computercraft/ComputerCraft.java @@ -82,6 +82,8 @@ public final class ComputerCraft public static int monitorWidth = 8; public static int monitorHeight = 6; + public static int uploadNagDelay = 5; + public static final class TurtleUpgrades { public static TurtleModem wirelessModemNormal; diff --git a/src/main/java/dan200/computercraft/client/gui/ComputerScreenBase.java b/src/main/java/dan200/computercraft/client/gui/ComputerScreenBase.java index a4e5ac313..6082771ee 100644 --- a/src/main/java/dan200/computercraft/client/gui/ComputerScreenBase.java +++ b/src/main/java/dan200/computercraft/client/gui/ComputerScreenBase.java @@ -17,30 +17,35 @@ import dan200.computercraft.shared.computer.inventory.ContainerComputerBase; import dan200.computercraft.shared.computer.upload.FileUpload; import dan200.computercraft.shared.computer.upload.UploadResult; import dan200.computercraft.shared.network.NetworkHandler; -import dan200.computercraft.shared.network.server.ContinueUploadMessage; import dan200.computercraft.shared.network.server.UploadFileMessage; import net.minecraft.client.gui.screen.inventory.ContainerScreen; import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Util; import net.minecraft.util.text.ITextComponent; +import net.minecraft.util.text.StringTextComponent; +import net.minecraft.util.text.TextFormatting; import net.minecraft.util.text.TranslationTextComponent; import org.lwjgl.glfw.GLFW; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.SeekableByteChannel; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.concurrent.TimeUnit; public abstract class ComputerScreenBase extends ContainerScreen { private static final ITextComponent OK = new TranslationTextComponent( "gui.ok" ); - private static final ITextComponent CANCEL = new TranslationTextComponent( "gui.cancel" ); - private static final ITextComponent OVERWRITE = new TranslationTextComponent( "gui.computercraft.upload.overwrite_button" ); + private static final ITextComponent NO_RESPONSE_TITLE = new TranslationTextComponent( "gui.computercraft.upload.no_response" ); + private static final ITextComponent NO_RESPONSE_MSG = new TranslationTextComponent( "gui.computercraft.upload.no_response.msg", + new StringTextComponent( "import" ).withStyle( TextFormatting.DARK_GRAY ) ); protected WidgetTerminal terminal; protected Terminal terminalData; @@ -49,11 +54,15 @@ public abstract class ComputerScreenBase extend protected final int sidebarYOffset; + private long uploadNagDeadline = Long.MAX_VALUE; + private final ItemStack displayStack; + public ComputerScreenBase( T container, PlayerInventory player, ITextComponent title, int sidebarYOffset ) { super( container, player, title ); terminalData = container.getTerminal(); family = container.getFamily(); + displayStack = container.getDisplayStack(); input = new ClientInputHandler( menu ); this.sidebarYOffset = sidebarYOffset; } @@ -83,6 +92,13 @@ public abstract class ComputerScreenBase extend { super.tick(); terminal.update(); + + if( uploadNagDeadline != Long.MAX_VALUE && Util.getNanos() >= uploadNagDeadline ) + { + new ItemToast( minecraft, displayStack, NO_RESPONSE_TITLE, NO_RESPONSE_MSG, ItemToast.TRANSFER_NO_RESPONSE_TOKEN ) + .showOrReplace( minecraft.getToasts() ); + uploadNagDeadline = Long.MAX_VALUE; + } } @Override @@ -194,41 +210,29 @@ public abstract class ComputerScreenBase extend if( toUpload.size() > 0 ) UploadFileMessage.send( menu, toUpload, NetworkHandler::sendToServer ); } - public void uploadResult( UploadResult result, ITextComponent message ) + public void uploadResult( UploadResult result, @Nullable ITextComponent message ) { switch( result ) { - case SUCCESS: - alert( UploadResult.SUCCESS_TITLE, message ); + case QUEUED: + { + if( ComputerCraft.uploadNagDelay > 0 ) + { + uploadNagDeadline = Util.getNanos() + TimeUnit.SECONDS.toNanos( ComputerCraft.uploadNagDelay ); + } break; + } + case CONSUMED: + { + uploadNagDeadline = Long.MAX_VALUE; + break; + } case ERROR: alert( UploadResult.FAILED_TITLE, message ); break; - case CONFIRM_OVERWRITE: - OptionScreen.show( - minecraft, UploadResult.UPLOAD_OVERWRITE, message, - Arrays.asList( - OptionScreen.newButton( CANCEL, b -> cancelUpload() ), - OptionScreen.newButton( OVERWRITE, b -> continueUpload() ) - ), - this::cancelUpload - ); - break; } } - private void continueUpload() - { - if( minecraft.screen instanceof OptionScreen ) ((OptionScreen) minecraft.screen).disable(); - NetworkHandler.sendToServer( new ContinueUploadMessage( menu, true ) ); - } - - private void cancelUpload() - { - minecraft.setScreen( this ); - NetworkHandler.sendToServer( new ContinueUploadMessage( menu, false ) ); - } - private void alert( ITextComponent title, ITextComponent message ) { OptionScreen.show( minecraft, title, message, diff --git a/src/main/java/dan200/computercraft/client/gui/ItemToast.java b/src/main/java/dan200/computercraft/client/gui/ItemToast.java new file mode 100644 index 000000000..59474a40b --- /dev/null +++ b/src/main/java/dan200/computercraft/client/gui/ItemToast.java @@ -0,0 +1,150 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.client.gui; + +import com.mojang.blaze3d.matrix.MatrixStack; +import com.mojang.blaze3d.systems.RenderSystem; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.toasts.IToast; +import net.minecraft.client.gui.toasts.ToastGui; +import net.minecraft.item.ItemStack; +import net.minecraft.util.IReorderingProcessor; +import net.minecraft.util.text.ITextComponent; + +import javax.annotation.Nonnull; +import java.util.List; + +/** + * A {@link IToast} implementation which displays an arbitrary message along with an optional {@link ItemStack}. + */ +public class ItemToast implements IToast +{ + public static final Object TRANSFER_NO_RESPONSE_TOKEN = new Object(); + + private static final long DISPLAY_TIME = 7000L; + private static final int MAX_LINE_SIZE = 200; + + private static final int IMAGE_SIZE = 16; + private static final int LINE_SPACING = 10; + private static final int MARGIN = 8; + + private final ItemStack stack; + private final ITextComponent title; + private final List message; + private final Object token; + private final int width; + + private boolean isNew = true; + private long firstDisplay; + + public ItemToast( Minecraft minecraft, ItemStack stack, ITextComponent title, ITextComponent message, Object token ) + { + this.stack = stack; + this.title = title; + this.token = token; + + FontRenderer font = minecraft.font; + this.message = font.split( message, MAX_LINE_SIZE ); + width = Math.max( MAX_LINE_SIZE, this.message.stream().mapToInt( font::width ).max().orElse( MAX_LINE_SIZE ) ) + MARGIN * 3 + IMAGE_SIZE; + } + + public void showOrReplace( ToastGui toasts ) + { + ItemToast existing = toasts.getToast( ItemToast.class, getToken() ); + if( existing != null ) + { + existing.isNew = true; + } + else + { + toasts.addToast( this ); + } + } + + @Override + public int width() + { + return width; + } + + @Override + public int height() + { + return MARGIN * 2 + LINE_SPACING + message.size() * LINE_SPACING; + } + + @Nonnull + @Override + public Object getToken() + { + return token; + } + + @Nonnull + @Override + public Visibility render( @Nonnull MatrixStack transform, @Nonnull ToastGui component, long time ) + { + if( isNew ) + { + + firstDisplay = time; + isNew = false; + } + + component.getMinecraft().getTextureManager().bind( TEXTURE ); + RenderSystem.color3f( 1.0F, 1.0F, 1.0F ); + + if( width == 160 && message.size() <= 1 ) + { + component.blit( transform, 0, 0, 0, 64, width, height() ); + } + else + { + + int height = height(); + + int bottom = Math.min( 4, height - 28 ); + renderBackgroundRow( transform, component, width, 0, 0, 28 ); + + for( int i = 28; i < height - bottom; i += 10 ) + { + renderBackgroundRow( transform, component, width, 16, i, Math.min( 16, height - i - bottom ) ); + } + + renderBackgroundRow( transform, component, width, 32 - bottom, height - bottom, bottom ); + } + + int textX = MARGIN; + if( !stack.isEmpty() ) + { + textX += MARGIN + IMAGE_SIZE; + component.getMinecraft().getItemRenderer().renderAndDecorateFakeItem( stack, MARGIN, MARGIN + height() / 2 - IMAGE_SIZE ); + } + + component.getMinecraft().font.draw( transform, title, textX, MARGIN, 0xff500050 ); + for( int i = 0; i < message.size(); ++i ) + { + component.getMinecraft().font.draw( transform, message.get( i ), textX, (float) (LINE_SPACING + (i + 1) * LINE_SPACING), 0xff000000 ); + } + + return time - firstDisplay < DISPLAY_TIME ? Visibility.SHOW : Visibility.HIDE; + } + + private static void renderBackgroundRow( MatrixStack transform, ToastGui component, int x, int u, int y, int height ) + { + int leftOffset = 5; + int rightOffset = Math.min( 60, x - leftOffset ); + + component.blit( transform, 0, y, 0, 32 + u, leftOffset, height ); + for( int k = leftOffset; k < x - rightOffset; k += 64 ) + { + component.blit( transform, k, y, 32, 32 + u, Math.min( 64, x - k - rightOffset ), height ); + } + + component.blit( transform, x - rightOffset, y, 160 - rightOffset, 32 + u, rightOffset, height ); + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/handles/ByteBufferChannel.java b/src/main/java/dan200/computercraft/core/apis/handles/ByteBufferChannel.java new file mode 100644 index 000000000..b7568e8da --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/handles/ByteBufferChannel.java @@ -0,0 +1,99 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.core.apis.handles; + +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.NonWritableChannelException; +import java.nio.channels.SeekableByteChannel; +import java.util.Objects; + +/** + * A seekable, readable byte channel which is backed by a {@link ByteBuffer}. + */ +public class ByteBufferChannel implements SeekableByteChannel +{ + private boolean closed = false; + private int position = 0; + + private final ByteBuffer backing; + + public ByteBufferChannel( ByteBuffer backing ) + { + this.backing = backing; + } + + @Override + public int read( ByteBuffer destination ) throws ClosedChannelException + { + if( closed ) throw new ClosedChannelException(); + Objects.requireNonNull( destination, "destination" ); + + if( position >= backing.limit() ) return -1; + + int remaining = Math.min( backing.limit() - position, destination.remaining() ); + + // TODO: Switch to Java 17 methods on 1.18.x + ByteBuffer slice = backing.slice(); + slice.position( position ); + slice.limit( position + remaining ); + destination.put( slice ); + position += remaining; + return remaining; + } + + @Override + public int write( ByteBuffer src ) throws ClosedChannelException + { + if( closed ) throw new ClosedChannelException(); + throw new NonWritableChannelException(); + } + + @Override + public long position() throws ClosedChannelException + { + if( closed ) throw new ClosedChannelException(); + return position; + } + + @Override + public SeekableByteChannel position( long newPosition ) throws ClosedChannelException + { + if( closed ) throw new ClosedChannelException(); + if( newPosition < 0 || newPosition > Integer.MAX_VALUE ) + { + throw new IllegalArgumentException( "Position out of bounds" ); + } + position = (int) newPosition; + return this; + } + + @Override + public long size() throws ClosedChannelException + { + if( closed ) throw new ClosedChannelException(); + return backing.limit(); + } + + @Override + public SeekableByteChannel truncate( long size ) throws ClosedChannelException + { + if( closed ) throw new ClosedChannelException(); + throw new NonWritableChannelException(); + } + + @Override + public boolean isOpen() + { + return !closed; + } + + @Override + public void close() + { + closed = true; + } +} diff --git a/src/main/java/dan200/computercraft/core/filesystem/ResourceMount.java b/src/main/java/dan200/computercraft/core/filesystem/ResourceMount.java index 131bbb551..3e46fb1b9 100644 --- a/src/main/java/dan200/computercraft/core/filesystem/ResourceMount.java +++ b/src/main/java/dan200/computercraft/core/filesystem/ResourceMount.java @@ -293,6 +293,7 @@ public final class ResourceMount implements IMount try { for( ResourceMount mount : MOUNT_CACHE.values() ) mount.load( manager ); + CONTENTS_CACHE.invalidateAll(); } finally { diff --git a/src/main/java/dan200/computercraft/shared/Config.java b/src/main/java/dan200/computercraft/shared/Config.java index 882c4849f..9b2e3a832 100644 --- a/src/main/java/dan200/computercraft/shared/Config.java +++ b/src/main/java/dan200/computercraft/shared/Config.java @@ -17,7 +17,6 @@ import dan200.computercraft.core.apis.http.options.Action; import dan200.computercraft.core.apis.http.options.AddressRuleConfig; import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer; import net.minecraftforge.common.ForgeConfigSpec; -import net.minecraftforge.common.ForgeConfigSpec.Builder; import net.minecraftforge.common.ForgeConfigSpec.ConfigValue; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.fml.ModLoadingContext; @@ -86,6 +85,7 @@ public final class Config private static final ConfigValue monitorRenderer; private static final ConfigValue monitorDistance; + private static final ConfigValue uploadNagDelay; private static final ForgeConfigSpec serverSpec; private static final ForgeConfigSpec clientSpec; @@ -94,7 +94,7 @@ public final class Config static { - Builder builder = new Builder(); + ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder(); { // General computers computerSpaceLimit = builder @@ -282,13 +282,17 @@ public final class Config serverSpec = builder.build(); - Builder clientBuilder = new Builder(); + ForgeConfigSpec.Builder clientBuilder = new ForgeConfigSpec.Builder(); monitorRenderer = clientBuilder .comment( "The renderer to use for monitors. Generally this should be kept at \"best\" - if\nmonitors have performance issues, you may wish to experiment with alternative\nrenderers." ) .defineEnum( "monitor_renderer", MonitorRenderer.BEST ); monitorDistance = clientBuilder .comment( "The maximum distance monitors will render at. This defaults to the standard tile\nentity limit, but may be extended if you wish to build larger monitors." ) .defineInRange( "monitor_distance", 64, 16, 1024 ); + uploadNagDelay = clientBuilder + .comment( "The delay in seconds after which we'll notify about unhandled imports. Set to 0 to disable." ) + .defineInRange( "upload_nag_delay", ComputerCraft.uploadNagDelay, 0, 60 ); + clientSpec = clientBuilder.build(); } @@ -357,6 +361,7 @@ public final class Config // Client ComputerCraft.monitorRenderer = monitorRenderer.get(); ComputerCraft.monitorDistanceSq = monitorDistance.get() * monitorDistance.get(); + ComputerCraft.uploadNagDelay = uploadNagDelay.get(); } @SubscribeEvent diff --git a/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java b/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java index c25a99152..a71235e74 100644 --- a/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java +++ b/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java @@ -28,6 +28,7 @@ import net.minecraft.entity.player.PlayerInventory; import net.minecraft.entity.player.ServerPlayerEntity; import net.minecraft.inventory.container.Container; import net.minecraft.inventory.container.INamedContainerProvider; +import net.minecraft.item.ItemStack; import net.minecraft.network.play.server.SPlayerPositionLookPacket; import net.minecraft.util.math.BlockPos; import net.minecraft.util.text.IFormattableTextComponent; @@ -225,7 +226,7 @@ public final class CommandComputerCraft .executes( context -> { ServerPlayerEntity player = context.getSource().getPlayerOrException(); ServerComputer computer = getComputerArgument( context, "computer" ); - new ComputerContainerData( computer ).open( player, new INamedContainerProvider() + new ComputerContainerData( computer, ItemStack.EMPTY ).open( player, new INamedContainerProvider() { @Nonnull @Override diff --git a/src/main/java/dan200/computercraft/shared/computer/blocks/TileComputerBase.java b/src/main/java/dan200/computercraft/shared/computer/blocks/TileComputerBase.java index d19a74440..01b87baa3 100644 --- a/src/main/java/dan200/computercraft/shared/computer/blocks/TileComputerBase.java +++ b/src/main/java/dan200/computercraft/shared/computer/blocks/TileComputerBase.java @@ -146,7 +146,11 @@ public abstract class TileComputerBase extends TileGeneric implements IComputerT { ServerComputer computer = createServerComputer(); computer.turnOn(); - new ComputerContainerData( computer ).open( player, this ); + + ItemStack stack = getBlockState().getBlock() instanceof BlockComputerBase + ? ((BlockComputerBase) getBlockState().getBlock()).getItem( this ) + : ItemStack.EMPTY; + new ComputerContainerData( computer, stack ).open( player, this ); } return ActionResultType.SUCCESS; } diff --git a/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerComputerBase.java b/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerComputerBase.java index 67bba7602..08109cf69 100644 --- a/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerComputerBase.java +++ b/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerComputerBase.java @@ -9,6 +9,7 @@ import dan200.computercraft.core.terminal.Terminal; import dan200.computercraft.shared.computer.core.ComputerFamily; import dan200.computercraft.shared.computer.core.ServerComputer; import dan200.computercraft.shared.computer.menu.ComputerMenu; +import dan200.computercraft.shared.computer.menu.ServerInputHandler; import dan200.computercraft.shared.computer.menu.ServerInputState; import dan200.computercraft.shared.network.client.TerminalState; import dan200.computercraft.shared.network.container.ComputerContainerData; @@ -16,6 +17,7 @@ import dan200.computercraft.shared.util.SingleIntArray; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.inventory.container.Container; import net.minecraft.inventory.container.ContainerType; +import net.minecraft.item.ItemStack; import net.minecraft.util.IIntArray; import net.minecraft.util.IntArray; @@ -30,10 +32,12 @@ public abstract class ContainerComputerBase extends Container implements Compute private final IIntArray data; private final @Nullable ServerComputer computer; - private final @Nullable ServerInputState input; + private final @Nullable ServerInputState input; private final @Nullable Terminal terminal; + private final ItemStack displayStack; + public ContainerComputerBase( ContainerType type, int id, Predicate canUse, ComputerFamily family, @Nullable ServerComputer computer, @Nullable ComputerContainerData containerData @@ -46,8 +50,9 @@ public abstract class ContainerComputerBase extends Container implements Compute addDataSlots( data ); this.computer = computer; - input = computer == null ? null : new ServerInputState( this ); + input = computer == null ? null : new ServerInputState<>( this ); terminal = containerData == null ? null : containerData.terminal().create(); + displayStack = containerData == null ? null : containerData.displayStack(); } @Override @@ -75,7 +80,7 @@ public abstract class ContainerComputerBase extends Container implements Compute } @Override - public ServerInputState getInput() + public ServerInputHandler getInput() { if( input == null ) throw new UnsupportedOperationException( "Cannot access server computer on the client" ); return input; @@ -106,4 +111,15 @@ public abstract class ContainerComputerBase extends Container implements Compute super.removed( player ); if( input != null ) input.close(); } + + /** + * Get the stack associated with this container. + * + * @return The current stack. + */ + @Nonnull + public ItemStack getDisplayStack() + { + return displayStack; + } } diff --git a/src/main/java/dan200/computercraft/shared/computer/menu/ServerInputHandler.java b/src/main/java/dan200/computercraft/shared/computer/menu/ServerInputHandler.java index e6f6bf8f2..815e21c6c 100644 --- a/src/main/java/dan200/computercraft/shared/computer/menu/ServerInputHandler.java +++ b/src/main/java/dan200/computercraft/shared/computer/menu/ServerInputHandler.java @@ -46,12 +46,4 @@ public interface ServerInputHandler extends InputHandler * @param uploadId The unique ID of this upload. */ void finishUpload( ServerPlayerEntity uploader, UUID uploadId ); - - /** - * Continue an upload. - * - * @param uploader The player uploading files. - * @param overwrite Whether the files should be overwritten or not. - */ - void confirmUpload( ServerPlayerEntity uploader, boolean overwrite ); } diff --git a/src/main/java/dan200/computercraft/shared/computer/menu/ServerInputState.java b/src/main/java/dan200/computercraft/shared/computer/menu/ServerInputState.java index d8ee57b9f..9a05f0e11 100644 --- a/src/main/java/dan200/computercraft/shared/computer/menu/ServerInputState.java +++ b/src/main/java/dan200/computercraft/shared/computer/menu/ServerInputState.java @@ -6,13 +6,8 @@ package dan200.computercraft.shared.computer.menu; import dan200.computercraft.ComputerCraft; -import dan200.computercraft.core.filesystem.FileSystem; -import dan200.computercraft.core.filesystem.FileSystemException; -import dan200.computercraft.core.filesystem.FileSystemWrapper; import dan200.computercraft.shared.computer.core.ServerComputer; -import dan200.computercraft.shared.computer.upload.FileSlice; -import dan200.computercraft.shared.computer.upload.FileUpload; -import dan200.computercraft.shared.computer.upload.UploadResult; +import dan200.computercraft.shared.computer.upload.*; import dan200.computercraft.shared.network.NetworkHandler; import dan200.computercraft.shared.network.NetworkMessage; import dan200.computercraft.shared.network.client.UploadResultMessage; @@ -20,27 +15,24 @@ import it.unimi.dsi.fastutil.ints.IntIterator; import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.ints.IntSet; import net.minecraft.entity.player.ServerPlayerEntity; +import net.minecraft.inventory.container.Container; import net.minecraft.util.text.TranslationTextComponent; import javax.annotation.Nullable; -import java.io.IOException; -import java.nio.channels.WritableByteChannel; -import java.util.ArrayList; import java.util.List; -import java.util.StringJoiner; import java.util.UUID; -import java.util.function.Function; +import java.util.stream.Collectors; /** * The default concrete implementation of {@link ServerInputHandler}. *

* This keeps track of the current key and mouse state, and releases them when the container is closed. + * + * @param The type of container this server input belongs to. */ -public class ServerInputState implements ServerInputHandler +public class ServerInputState implements ServerInputHandler { - private static final String LIST_PREFIX = "\n \u2022 "; - - private final ComputerMenu owner; + private final T owner; private final IntSet keysDown = new IntOpenHashSet( 4 ); private int lastMouseX; @@ -50,7 +42,7 @@ public class ServerInputState implements ServerInputHandler private @Nullable UUID toUploadId; private @Nullable List toUpload; - public ServerInputState( ComputerMenu owner ) + public ServerInputState( T owner ) { this.owner = owner; } @@ -160,91 +152,31 @@ public class ServerInputState implements ServerInputHandler return; } - NetworkMessage message = finishUpload( false ); + NetworkMessage message = finishUpload( uploader ); NetworkHandler.sendToPlayer( uploader, message ); } - @Override - public void confirmUpload( ServerPlayerEntity uploader, boolean overwrite ) - { - if( toUploadId == null || toUpload == null || toUpload.isEmpty() ) - { - ComputerCraft.log.warn( "Invalid finishUpload call, skipping." ); - return; - } - - NetworkMessage message = finishUpload( true ); - NetworkHandler.sendToPlayer( uploader, message ); - } - - private UploadResultMessage finishUpload( boolean forceOverwrite ) + private UploadResultMessage finishUpload( ServerPlayerEntity player ) { ServerComputer computer = owner.getComputer(); - if( toUpload == null ) return UploadResultMessage.COMPUTER_OFF; - - FileSystem fs = computer.getComputer().getAPIEnvironment().getFileSystem(); + if( toUpload == null ) + { + return UploadResultMessage.error( owner, UploadResult.COMPUTER_OFF_MSG ); + } for( FileUpload upload : toUpload ) { if( !upload.checksumMatches() ) { ComputerCraft.log.warn( "Checksum failed to match for {}.", upload.getName() ); - return new UploadResultMessage( UploadResult.ERROR, new TranslationTextComponent( "gui.computercraft.upload.failed.corrupted" ) ); + return UploadResultMessage.error( owner, new TranslationTextComponent( "gui.computercraft.upload.failed.corrupted" ) ); } } - try - { - List overwrite = new ArrayList<>(); - List files = toUpload; - toUpload = null; - for( FileUpload upload : files ) - { - if( !fs.exists( upload.getName() ) ) continue; - if( fs.isDir( upload.getName() ) ) - { - return new UploadResultMessage( - UploadResult.ERROR, - new TranslationTextComponent( "gui.computercraft.upload.failed.overwrite_dir", upload.getName() ) - ); - } - - overwrite.add( upload.getName() ); - } - - if( !overwrite.isEmpty() && !forceOverwrite ) - { - StringJoiner joiner = new StringJoiner( LIST_PREFIX, LIST_PREFIX, "" ); - for( String value : overwrite ) joiner.add( value ); - toUpload = files; - return new UploadResultMessage( - UploadResult.CONFIRM_OVERWRITE, - new TranslationTextComponent( "gui.computercraft.upload.overwrite.detail", joiner.toString() ) - ); - } - - long availableSpace = fs.getFreeSpace( "/" ); - long neededSpace = 0; - for( FileUpload upload : files ) neededSpace += Math.max( 512, upload.getBytes().remaining() ); - if( neededSpace > availableSpace ) return UploadResultMessage.OUT_OF_SPACE; - - for( FileUpload file : files ) - { - try( FileSystemWrapper channel = fs.openForWrite( file.getName(), false, Function.identity() ) ) - { - channel.get().write( file.getBytes() ); - } - } - - return new UploadResultMessage( - UploadResult.SUCCESS, new TranslationTextComponent( "gui.computercraft.upload.success.msg", files.size() ) - ); - } - catch( FileSystemException | IOException e ) - { - ComputerCraft.log.error( "Error uploading files", e ); - return new UploadResultMessage( UploadResult.ERROR, new TranslationTextComponent( "gui.computercraft.upload.failed.generic", e.getMessage() ) ); - } + computer.queueEvent( "file_transfer", new Object[] { + new TransferredFiles( player, owner, toUpload.stream().map( x -> new TransferredFile( x.getName(), x.getBytes() ) ).collect( Collectors.toList() ) ), + } ); + return UploadResultMessage.queued( owner ); } public void close() diff --git a/src/main/java/dan200/computercraft/shared/computer/upload/TransferredFile.java b/src/main/java/dan200/computercraft/shared/computer/upload/TransferredFile.java new file mode 100644 index 000000000..51c32494e --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/upload/TransferredFile.java @@ -0,0 +1,53 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.computer.upload; + +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.core.apis.handles.BinaryReadableHandle; +import dan200.computercraft.core.apis.handles.ByteBufferChannel; +import dan200.computercraft.core.asm.ObjectSource; + +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.Optional; + +/** + * A binary file handle that has been transferred to this computer. + *

+ * This inherits all methods of {@link BinaryReadableHandle binary file handles}, meaning you can use the standard + * {@link BinaryReadableHandle#read(Optional) read functions} to access the contents of the file. + * + * @cc.module [kind=event] file_transfer.TransferredFile + * @see BinaryReadableHandle + */ +public class TransferredFile implements ObjectSource +{ + private final String name; + private final BinaryReadableHandle handle; + + public TransferredFile( String name, ByteBuffer contents ) + { + this.name = name; + handle = BinaryReadableHandle.of( new ByteBufferChannel( contents ) ); + } + + /** + * Get the name of this file being transferred. + * + * @return The file's name. + */ + @LuaFunction + public final String getName() + { + return name; + } + + @Override + public Iterable getExtra() + { + return Collections.singleton( handle ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/upload/TransferredFiles.java b/src/main/java/dan200/computercraft/shared/computer/upload/TransferredFiles.java new file mode 100644 index 000000000..c4ea7f2a8 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/upload/TransferredFiles.java @@ -0,0 +1,58 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.computer.upload; + +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.shared.network.NetworkHandler; +import dan200.computercraft.shared.network.client.UploadResultMessage; +import net.minecraft.entity.player.ServerPlayerEntity; +import net.minecraft.inventory.container.Container; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A list of files that have been transferred to this computer. + * + * @cc.module [kind=event] file_transfer.TransferredFiles + */ +public class TransferredFiles +{ + private final ServerPlayerEntity player; + private final Container container; + private final AtomicBoolean consumed = new AtomicBoolean( false ); + + private final List files; + + public TransferredFiles( ServerPlayerEntity player, Container container, List files ) + { + this.player = player; + this.container = container; + this.files = files; + } + + /** + * All the files that are being transferred to this computer. + * + * @return The list of files. + */ + @LuaFunction + public final List getFiles() + { + consumed(); + return files; + } + + private void consumed() + { + if( consumed.getAndSet( true ) ) return; + + if( player.isAlive() && player.containerMenu == container ) + { + NetworkHandler.sendToPlayer( player, UploadResultMessage.consumed( container ) ); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/upload/UploadResult.java b/src/main/java/dan200/computercraft/shared/computer/upload/UploadResult.java index 622fe8ad6..d785080cd 100644 --- a/src/main/java/dan200/computercraft/shared/computer/upload/UploadResult.java +++ b/src/main/java/dan200/computercraft/shared/computer/upload/UploadResult.java @@ -10,16 +10,13 @@ import net.minecraft.util.text.TranslationTextComponent; public enum UploadResult { - SUCCESS, - ERROR, - CONFIRM_OVERWRITE; + QUEUED, + CONSUMED, + ERROR; public static final ITextComponent SUCCESS_TITLE = new TranslationTextComponent( "gui.computercraft.upload.success" ); public static final ITextComponent FAILED_TITLE = new TranslationTextComponent( "gui.computercraft.upload.failed" ); public static final ITextComponent COMPUTER_OFF_MSG = new TranslationTextComponent( "gui.computercraft.upload.failed.computer_off" ); - public static final ITextComponent OUT_OF_SPACE_MSG = new TranslationTextComponent( "gui.computercraft.upload.failed.out_of_space" ); public static final ITextComponent TOO_MUCH_MSG = new TranslationTextComponent( "gui.computercraft.upload.failed.too_much" ); - - public static final ITextComponent UPLOAD_OVERWRITE = new TranslationTextComponent( "gui.computercraft.upload.overwrite" ); } diff --git a/src/main/java/dan200/computercraft/shared/network/NetworkHandler.java b/src/main/java/dan200/computercraft/shared/network/NetworkHandler.java index 9dc79a00b..663f38caf 100644 --- a/src/main/java/dan200/computercraft/shared/network/NetworkHandler.java +++ b/src/main/java/dan200/computercraft/shared/network/NetworkHandler.java @@ -50,7 +50,6 @@ public final class NetworkHandler registerMainThread( 2, NetworkDirection.PLAY_TO_SERVER, KeyEventServerMessage.class, KeyEventServerMessage::new ); registerMainThread( 3, NetworkDirection.PLAY_TO_SERVER, MouseEventServerMessage.class, MouseEventServerMessage::new ); registerMainThread( 4, NetworkDirection.PLAY_TO_SERVER, UploadFileMessage.class, UploadFileMessage::new ); - registerMainThread( 5, NetworkDirection.PLAY_TO_SERVER, ContinueUploadMessage.class, ContinueUploadMessage::new ); // Client messages registerMainThread( 10, NetworkDirection.PLAY_TO_CLIENT, ChatTableClientMessage.class, ChatTableClientMessage::new ); diff --git a/src/main/java/dan200/computercraft/shared/network/client/UploadResultMessage.java b/src/main/java/dan200/computercraft/shared/network/client/UploadResultMessage.java index 877d02de7..1cccc2014 100644 --- a/src/main/java/dan200/computercraft/shared/network/client/UploadResultMessage.java +++ b/src/main/java/dan200/computercraft/shared/network/client/UploadResultMessage.java @@ -11,37 +11,55 @@ import dan200.computercraft.shared.computer.upload.UploadResult; import dan200.computercraft.shared.network.NetworkMessage; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.screen.Screen; +import net.minecraft.inventory.container.Container; import net.minecraft.network.PacketBuffer; import net.minecraft.util.text.ITextComponent; import net.minecraftforge.fml.network.NetworkEvent; import javax.annotation.Nonnull; +import javax.annotation.Nullable; public class UploadResultMessage implements NetworkMessage { - public static final UploadResultMessage COMPUTER_OFF = new UploadResultMessage( UploadResult.ERROR, UploadResult.COMPUTER_OFF_MSG ); - public static final UploadResultMessage OUT_OF_SPACE = new UploadResultMessage( UploadResult.ERROR, UploadResult.OUT_OF_SPACE_MSG ); - + private final int containerId; private final UploadResult result; - private final ITextComponent message; + private final ITextComponent errorMessage; - public UploadResultMessage( UploadResult result, ITextComponent message ) + private UploadResultMessage( Container container, UploadResult result, @Nullable ITextComponent errorMessage ) { + containerId = container.containerId; this.result = result; - this.message = message; + this.errorMessage = errorMessage; + } + + public static UploadResultMessage queued( Container container ) + { + return new UploadResultMessage( container, UploadResult.QUEUED, null ); + } + + public static UploadResultMessage consumed( Container container ) + { + return new UploadResultMessage( container, UploadResult.CONSUMED, null ); + } + + public static UploadResultMessage error( Container container, ITextComponent errorMessage ) + { + return new UploadResultMessage( container, UploadResult.ERROR, errorMessage ); } public UploadResultMessage( @Nonnull PacketBuffer buf ) { + containerId = buf.readVarInt(); result = buf.readEnum( UploadResult.class ); - message = buf.readComponent(); + errorMessage = result == UploadResult.ERROR ? buf.readComponent() : null; } @Override public void toBytes( @Nonnull PacketBuffer buf ) { + buf.writeVarInt( containerId ); buf.writeEnum( result ); - buf.writeComponent( message ); + if( result == UploadResult.ERROR ) buf.writeComponent( errorMessage ); } @Override @@ -50,9 +68,9 @@ public class UploadResultMessage implements NetworkMessage Minecraft minecraft = Minecraft.getInstance(); Screen screen = OptionScreen.unwrap( minecraft.screen ); - if( screen instanceof ComputerScreenBase ) + if( screen instanceof ComputerScreenBase && ((ComputerScreenBase) screen).getMenu().containerId == containerId ) { - ((ComputerScreenBase) screen).uploadResult( result, message ); + ((ComputerScreenBase) screen).uploadResult( result, errorMessage ); } } } diff --git a/src/main/java/dan200/computercraft/shared/network/container/ComputerContainerData.java b/src/main/java/dan200/computercraft/shared/network/container/ComputerContainerData.java index 926029ec2..162268dc3 100644 --- a/src/main/java/dan200/computercraft/shared/network/container/ComputerContainerData.java +++ b/src/main/java/dan200/computercraft/shared/network/container/ComputerContainerData.java @@ -8,23 +8,29 @@ package dan200.computercraft.shared.network.container; import dan200.computercraft.shared.computer.core.ComputerFamily; import dan200.computercraft.shared.computer.core.ServerComputer; import dan200.computercraft.shared.network.client.TerminalState; +import net.minecraft.item.ItemStack; import net.minecraft.network.PacketBuffer; +import javax.annotation.Nonnull; + public class ComputerContainerData implements ContainerData { private final ComputerFamily family; private final TerminalState terminal; + private final ItemStack displayStack; - public ComputerContainerData( ServerComputer computer ) + public ComputerContainerData( ServerComputer computer, @Nonnull ItemStack displayStack ) { family = computer.getFamily(); terminal = computer.getTerminalState(); + this.displayStack = displayStack; } public ComputerContainerData( PacketBuffer buf ) { family = buf.readEnum( ComputerFamily.class ); terminal = new TerminalState( buf ); + displayStack = buf.readItem(); } @Override @@ -32,6 +38,7 @@ public class ComputerContainerData implements ContainerData { buf.writeEnum( family ); terminal.write( buf ); + buf.writeItemStack( displayStack, true ); } public ComputerFamily family() @@ -43,4 +50,15 @@ public class ComputerContainerData implements ContainerData { return terminal; } + + /** + * Get a stack associated with this menu. This may be displayed on the client. + * + * @return The stack associated with this menu. + */ + @Nonnull + public ItemStack displayStack() + { + return displayStack; + } } diff --git a/src/main/java/dan200/computercraft/shared/network/server/ContinueUploadMessage.java b/src/main/java/dan200/computercraft/shared/network/server/ContinueUploadMessage.java deleted file mode 100644 index 4b4973643..000000000 --- a/src/main/java/dan200/computercraft/shared/network/server/ContinueUploadMessage.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * This file is part of ComputerCraft - http://www.computercraft.info - * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. - * Send enquiries to dratcliffe@gmail.com - */ -package dan200.computercraft.shared.network.server; - -import dan200.computercraft.shared.computer.menu.ComputerMenu; -import net.minecraft.entity.player.ServerPlayerEntity; -import net.minecraft.inventory.container.Container; -import net.minecraft.network.PacketBuffer; -import net.minecraftforge.fml.network.NetworkEvent; - -import javax.annotation.Nonnull; - -public class ContinueUploadMessage extends ComputerServerMessage -{ - private final boolean overwrite; - - public ContinueUploadMessage( Container menu, boolean overwrite ) - { - super( menu ); - this.overwrite = overwrite; - } - - public ContinueUploadMessage( @Nonnull PacketBuffer buf ) - { - super( buf ); - overwrite = buf.readBoolean(); - } - - @Override - public void toBytes( @Nonnull PacketBuffer buf ) - { - super.toBytes( buf ); - buf.writeBoolean( overwrite ); - } - - @Override - protected void handle( NetworkEvent.Context context, @Nonnull ComputerMenu container ) - { - ServerPlayerEntity player = context.getSender(); - if( player != null ) container.getInput().confirmUpload( player, overwrite ); - } -} diff --git a/src/main/java/dan200/computercraft/shared/pocket/items/ItemPocketComputer.java b/src/main/java/dan200/computercraft/shared/pocket/items/ItemPocketComputer.java index a067cb8c2..e1c3987a3 100644 --- a/src/main/java/dan200/computercraft/shared/pocket/items/ItemPocketComputer.java +++ b/src/main/java/dan200/computercraft/shared/pocket/items/ItemPocketComputer.java @@ -168,7 +168,7 @@ public class ItemPocketComputer extends Item implements IComputerItem, IMedia, I if( !stop ) { boolean isTypingOnly = hand == Hand.OFF_HAND; - new ComputerContainerData( computer ).open( player, new PocketComputerMenuProvider( computer, stack, this, hand, isTypingOnly ) ); + new ComputerContainerData( computer, stack ).open( player, new PocketComputerMenuProvider( computer, stack, this, hand, isTypingOnly ) ); } } return new ActionResult<>( ActionResultType.SUCCESS, stack ); diff --git a/src/main/resources/assets/computercraft/lang/en_us.json b/src/main/resources/assets/computercraft/lang/en_us.json index f40d1d927..33de587af 100644 --- a/src/main/resources/assets/computercraft/lang/en_us.json +++ b/src/main/resources/assets/computercraft/lang/en_us.json @@ -119,16 +119,13 @@ "gui.computercraft.upload.success": "Upload Succeeded", "gui.computercraft.upload.success.msg": "%d files uploaded.", "gui.computercraft.upload.failed": "Upload Failed", - "gui.computercraft.upload.failed.out_of_space": "Not enough space on the computer for these files.", "gui.computercraft.upload.failed.computer_off": "You must turn the computer on before uploading files.", "gui.computercraft.upload.failed.too_much": "Your files are too large to be uploaded.", "gui.computercraft.upload.failed.name_too_long": "File names are too long to be uploaded.", "gui.computercraft.upload.failed.too_many_files": "Cannot upload this many files.", - "gui.computercraft.upload.failed.overwrite_dir": "Cannot upload %s, as there is already a directory with the same name.", "gui.computercraft.upload.failed.generic": "Uploading files failed (%s)", "gui.computercraft.upload.failed.corrupted": "Files corrupted when uploading. Please try again.", - "gui.computercraft.upload.overwrite": "Files would be overwritten", - "gui.computercraft.upload.overwrite.detail": "The following files will be overwritten when uploading. Continue?%s", - "gui.computercraft.upload.overwrite_button": "Overwrite", + "gui.computercraft.upload.no_response": "Transferring Files", + "gui.computercraft.upload.no_response.msg": "Your computer has not used your transferred files. You may need to run the %s program and try again.", "gui.computercraft.pocket_computer_overlay": "Pocket computer open. Press ESC to close." } 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 6daab3f8b..4e59e1ad9 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua @@ -211,6 +211,8 @@ local function tabulateCommon(bPaged, ...) end print() end + + local previous_colour = term.getTextColour() for _, t in ipairs(tAll) do if type(t) == "table" then if #t > 0 then @@ -220,6 +222,7 @@ local function tabulateCommon(bPaged, ...) term.setTextColor(t) end end + term.setTextColor(previous_colour) end --[[- Prints tables in a structured form. diff --git a/src/main/resources/data/computercraft/lua/rom/modules/internal/cc/import.lua b/src/main/resources/data/computercraft/lua/rom/modules/internal/cc/import.lua new file mode 100644 index 000000000..2b8f26db0 --- /dev/null +++ b/src/main/resources/data/computercraft/lua/rom/modules/internal/cc/import.lua @@ -0,0 +1,70 @@ +-- Internal module for handling file uploads. This has NO stability guarantees, +-- and so SHOULD NOT be relyed on in user code. + +local completion = require "cc.completion" + +return function(files) + local overwrite = {} + for _, file in pairs(files) do + local filename = file.getName() + local path = shell.resolve(filename) + if fs.exists(path) then + if fs.isDir(path) then + return nil, filename .. " is already a directory." + end + + overwrite[#overwrite + 1] = filename + end + end + + if #overwrite > 0 then + table.sort(overwrite) + printError("The following files will be overwritten:") + textutils.pagedTabulate(colours.cyan, overwrite) + + while true do + io.write("Overwrite? (yes/no) ") + local input = read(nil, nil, function(t) + return completion.choice(t, { "yes", "no" }) + end) + if not input then return end + + input = input:lower() + if input == "" or input == "yes" or input == "y" then + break + elseif input == "no" or input == "n" then + return + end + end + end + + for _, file in pairs(files) do + local filename = file.getName() + print("Transferring " .. filename) + + local path = shell.resolve(filename) + local handle, err = fs.open(path, "wb") + if not handle then return nil, err end + + -- Write the file without loading it all into memory. This uses the same buffer size + -- as BinaryReadHandle. It would be really nice to have a way to do this without + -- multiple copies. + while true do + local chunk = file.read(8192) + if not chunk then break end + + local ok, err = pcall(handle.write, chunk) + if not ok then + handle.close() + + -- Probably an out-of-space issue, just bail. + if err:sub(1, 7) == "pcall: " then err = err:sub(8) end + return nil, "Failed to write file (" .. err .. "). File may be corrupted" + end + end + + handle.close() + end + + return true +end 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 59de7153d..51a5b4fd5 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 @@ -331,9 +331,8 @@ while #tProcesses > 0 do resizeWindows() redrawMenu() - elseif sEvent == "char" or sEvent == "key" or sEvent == "key_up" or sEvent == "paste" or sEvent == "terminate" then - -- Keyboard event - -- Passthrough to current process + elseif sEvent == "char" or sEvent == "key" or sEvent == "key_up" or sEvent == "paste" or sEvent == "terminate" or sEvent == "file_transfer" then + -- Basic input, just passthrough to current process resumeProcess(nCurrentProcess, table.unpack(tEventData, 1, tEventData.n)) if cullProcess(nCurrentProcess) then setMenuVisible(#tProcesses >= 2) diff --git a/src/main/resources/data/computercraft/lua/rom/programs/import.lua b/src/main/resources/data/computercraft/lua/rom/programs/import.lua new file mode 100644 index 000000000..54c073864 --- /dev/null +++ b/src/main/resources/data/computercraft/lua/rom/programs/import.lua @@ -0,0 +1,24 @@ +require "cc.completion" + +print("Drop files to transfer them to this computer") + +local files +while true do + local event, arg = os.pullEvent() + if event == "file_transfer" then + files = arg.getFiles() + break + elseif event == "key" and arg == keys.q then + return + end +end + +if #files == 0 then + printError("No files to transfer") + return +end + +package.path = package.path .. "/rom/modules/internal/?.lua" + +local ok, err = require("cc.import")(files) +if not ok and err then printError(err) 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 19c1bb02d..d4e150790 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/shell.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/shell.lua @@ -10,7 +10,6 @@ -- -- @module[module] shell -local expect = dofile("rom/modules/main/cc/expect.lua").expect local make_package = dofile("rom/modules/main/cc/require.lua").make local multishell = multishell @@ -35,6 +34,14 @@ local function createShellEnv(dir) return env end +-- Set up a dummy require based on the current shell, for loading some of our internal dependencies. +local require +do + local env = setmetatable(createShellEnv("/rom/modules/internal"), { __index = _ENV }) + require = env.require +end +local expect = require("cc.expect").expect + -- Colours local promptColour, textColour, bgColour if term.isColour() then @@ -591,6 +598,13 @@ if #tArgs > 0 then shell.run(...) else + local function show_prompt() + term.setBackgroundColor(bgColour) + term.setTextColour(promptColour) + write(shell.dir() .. "> ") + term.setTextColour(textColour) + end + -- "shell" -- Print the header term.setBackgroundColor(bgColour) @@ -607,21 +621,49 @@ else local tCommandHistory = {} while not bExit do term.redirect(parentTerm) - term.setBackgroundColor(bgColour) - term.setTextColour(promptColour) - write(shell.dir() .. "> ") - term.setTextColour(textColour) + show_prompt() - local sLine - if settings.get("shell.autocomplete") then - sLine = read(nil, tCommandHistory, shell.complete) - else - sLine = read(nil, tCommandHistory) + local complete + if settings.get("shell.autocomplete") then complete = shell.complete end + + local ok, result + local co = coroutine.create(read) + assert(coroutine.resume(co, nil, tCommandHistory, complete)) + + while coroutine.status(co) ~= "dead" do + local event = table.pack(os.pullEvent()) + if event[1] == "file_transfer" then + -- Abandon the current prompt + local _, h = term.getSize() + local _, y = term.getCursorPos() + if y == h then + term.scroll(1) + term.setCursorPos(1, y) + else + term.setCursorPos(1, y + 1) + end + term.setCursorBlink(false) + + -- Run the import script with the provided files + local ok, err = require("cc.import")(event[2].getFiles()) + if not ok and err then printError(err) end + + -- And attempt to restore the prompt. + show_prompt() + term.setCursorBlink(true) + event = { "term_resize", n = 1 } -- Nasty hack to force read() to redraw. + end + + if result == nil or event[1] == result or event[1] == "terminate" then + ok, result = coroutine.resume(co, table.unpack(event, 1, event.n)) + if not ok then error(result, 0) end + end end - if sLine:match("%S") and tCommandHistory[#tCommandHistory] ~= sLine then - table.insert(tCommandHistory, sLine) + + if result:match("%S") and tCommandHistory[#tCommandHistory] ~= result then + table.insert(tCommandHistory, result) end - shell.run(sLine) + shell.run(result) end end diff --git a/src/test/java/dan200/computercraft/support/CustomMatchers.java b/src/test/java/dan200/computercraft/support/CustomMatchers.java index 2388fe060..7a1d4e71a 100644 --- a/src/test/java/dan200/computercraft/support/CustomMatchers.java +++ b/src/test/java/dan200/computercraft/support/CustomMatchers.java @@ -6,13 +6,12 @@ package dan200.computercraft.support; import org.hamcrest.Matcher; +import org.hamcrest.Matchers; import java.util.List; import java.util.function.Function; import java.util.stream.Collectors; -import static org.hamcrest.Matchers.contains; - public class CustomMatchers { /** @@ -27,6 +26,7 @@ public class CustomMatchers */ public static Matcher> containsWith( List items, Function> matcher ) { - return contains( items.stream().map( matcher ).collect( Collectors.toList() ) ); + // The explicit type argument should be redundant, but it appears some Java compilers require it. + return Matchers.contains( items.stream().map( matcher ).collect( Collectors.toList() ) ); } } diff --git a/src/test/resources/test-rom/spec/programs/shell_spec.lua b/src/test/resources/test-rom/spec/programs/shell_spec.lua index fae7b85d1..0e88983ae 100644 --- a/src/test/resources/test-rom/spec/programs/shell_spec.lua +++ b/src/test/resources/test-rom/spec/programs/shell_spec.lua @@ -1,3 +1,5 @@ +local with_window = require "test_helpers".with_window + describe("The shell", function() describe("require", function() it("validates arguments", function() @@ -101,4 +103,48 @@ describe("The shell", function() expect.error(shell.switchTab, nil):eq("bad argument #1 (expected number, got nil)") end) end) + + describe("file uploads", function() + local function create_file(name, contents) + local did_read = false + return { + getName = function() return name end, + read = function() + if did_read then return end + did_read = true + return contents + end, + close = function() end, + } + end + local function create_files(files) return { getFiles = function() return files end } end + + it("suspends the read prompt", function() + fs.delete("tmp.txt") + + local win = with_window(32, 5, function() + local queue = { + { "shell" }, + { "paste", "xyz" }, + { "file_transfer", create_files { create_file("transfer.txt", "empty file") } }, + } + local co = coroutine.create(shell.run) + for _, event in pairs(queue) do assert(coroutine.resume(co, table.unpack(event))) end + end) + + expect(win.getCursorBlink()):eq(true) + + local lines = {} + for i = 1, 5 do lines[i] = win.getLine(i):gsub(" +$", "") end + expect(lines):same { + "CraftOS 1.8", + "> xyz", + "Transferring transfer.txt", + "> xyz", + "", + } + + expect({ win.getCursorPos() }):same { 6, 4 } + end) + end) end) From 1e88d3700490022571c36bd8969771ee0d971292 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sat, 29 Oct 2022 16:03:05 +0100 Subject: [PATCH 3/7] Add peripheral_hub type for wired-modem-like peripherals (#1193) This allows other mods to create wired-modem alike blocks, which expose peripherals on the wired network, without having to reimplement the main modem interface. This is not currently documented, but a peripheral_hub should provide the following methods: - isPresentRemote - getTypeRemote - hasTypeRemote - getMethodsRemote - callRemote --- .../modem/wired/WiredModemPeripheral.java | 7 ++++++ .../computercraft/lua/rom/apis/peripheral.lua | 22 +++++-------------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemPeripheral.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemPeripheral.java index 50f9efa34..503cc0ebc 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemPeripheral.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemPeripheral.java @@ -72,6 +72,13 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements IW protected abstract WiredModemLocalPeripheral getLocalPeripheral(); //endregion + @Nonnull + @Override + public Set getAdditionalTypes() + { + return Collections.singleton( "peripheral_hub" ); + } + //region Peripheral methods /** 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 ae4063195..f5e8fcc8b 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/peripheral.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/peripheral.lua @@ -109,7 +109,7 @@ function getNames() local side = sides[n] if native.isPresent(side) then table.insert(results, side) - if native.hasType(side, "modem") and not native.call(side, "isWireless") then + if native.hasType(side, "peripheral_hub") then local remote = native.call(side, "getNamesRemote") for _, name in ipairs(remote) do table.insert(results, name) @@ -134,9 +134,7 @@ function isPresent(name) for n = 1, #sides do local side = sides[n] - if native.hasType(side, "modem") and not native.call(side, "isWireless") and - native.call(side, "isPresentRemote", name) - then + if native.hasType(side, "peripheral_hub") and native.call(side, "isPresentRemote", name) then return true end end @@ -162,9 +160,7 @@ function getType(peripheral) end for n = 1, #sides do local side = sides[n] - if native.hasType(side, "modem") and not native.call(side, "isWireless") and - native.call(side, "isPresentRemote", peripheral) - then + if native.hasType(side, "peripheral_hub") and native.call(side, "isPresentRemote", peripheral) then return native.call(side, "getTypeRemote", peripheral) end end @@ -195,9 +191,7 @@ function hasType(peripheral, peripheral_type) end for n = 1, #sides do local side = sides[n] - if native.hasType(side, "modem") and not native.call(side, "isWireless") and - native.call(side, "isPresentRemote", peripheral) - then + if native.hasType(side, "peripheral_hub") and native.call(side, "isPresentRemote", peripheral) then return native.call(side, "hasTypeRemote", peripheral, peripheral_type) end end @@ -223,9 +217,7 @@ function getMethods(name) end for n = 1, #sides do local side = sides[n] - if native.hasType(side, "modem") and not native.call(side, "isWireless") and - native.call(side, "isPresentRemote", name) - then + if native.hasType(side, "peripheral_hub") and native.call(side, "isPresentRemote", peripheral) then return native.call(side, "getMethodsRemote", name) end end @@ -265,9 +257,7 @@ function call(name, method, ...) for n = 1, #sides do local side = sides[n] - if native.hasType(side, "modem") and not native.call(side, "isWireless") and - native.call(side, "isPresentRemote", name) - then + if native.hasType(side, "peripheral_hub") and native.call(side, "isPresentRemote", peripheral) then return native.call(side, "callRemote", name, method, ...) end end From 71f81e12013edf82e8a815c0938cbbb3a3c8c2e5 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sat, 29 Oct 2022 18:17:02 +0100 Subject: [PATCH 4/7] Move some test support code into testFixtues This offers very few advantages now, but helps support the following in the future: - Reuse test support code across multiple projects (useful for multi-loader). - Allow using test fixture code in testMod. We've got a version of our gametest which use Kotlin instead of Lua for asserting computer behaviour. We can't use java-test-fixtures here for Forge reasons, so have to roll our own version. Alas. - Add an ILuaMachine implementation which runs Kotlin coroutines instead. We can use this for testing asynchronous APIs. This also replaces the FakeComputerManager. - Move most things in the .support module to .test.core. We need to use a separate package in order to cope with Java 9 modules (again, thanks Forge). --- build.gradle.kts | 28 +- buildSrc/build.gradle.kts | 1 + .../kotlin/cc-tweaked.gametest.gradle.kts | 53 ++++ .../cc-tweaked.java-convention.gradle.kts | 3 +- .../cc-tweaked.kotlin-convention.gradle.kts | 21 ++ .../cc/tweaked/gradle/CCTweakedPlugin.kt | 5 + gradle/libs.versions.toml | 1 + .../core/computer/ComputerThread.java | 4 +- .../core/ComputerTestDelegate.java | 4 +- .../computercraft/core/apis/AsyncRunner.kt | 123 --------- .../computercraft/core/asm/GeneratorTest.java | 2 +- .../core/computer/ComputerBootstrap.java | 3 +- .../core/computer/ComputerThreadTest.java | 5 +- .../core/computer/FakeComputerManager.java | 259 ------------------ .../core/terminal/TerminalTest.java | 5 +- .../network/server/UploadFileMessageTest.java | 8 +- .../computercraft/core/http}/TestHttpApi.kt | 14 +- .../test/core}/ArbitraryByteBuffer.java | 2 +- .../test/core}/ByteBufferMatcher.java | 2 +- .../computercraft/test/core}/CallCounter.java | 2 +- .../test/core}/ContramapMatcher.java | 2 +- .../test/core}/CustomMatchers.java | 2 +- .../test/core/apis/BasicApiEnvironment.java | 161 +++++++++++ .../test}/core/computer/BasicEnvironment.java | 9 +- .../computer/FakeMainThreadScheduler.java | 2 +- .../test}/core/filesystem/MemoryMount.java | 2 +- .../test}/core/terminal/TerminalMatchers.java | 6 +- .../computercraft/test/core/Assertions.kt | 63 +++++ .../core/computer/KotlinComputerManager.kt | 188 +++++++++++++ .../test/core/computer/KotlinLuaMachine.kt | 41 +++ .../test/core/computer/LuaTaskContext.kt | 87 ++++++ .../test/core/computer/LuaTaskRunner.kt | 64 +++++ 32 files changed, 745 insertions(+), 427 deletions(-) create mode 100644 buildSrc/src/main/kotlin/cc-tweaked.gametest.gradle.kts create mode 100644 buildSrc/src/main/kotlin/cc-tweaked.kotlin-convention.gradle.kts delete mode 100644 src/test/java/dan200/computercraft/core/apis/AsyncRunner.kt delete mode 100644 src/test/java/dan200/computercraft/core/computer/FakeComputerManager.java rename src/test/{java/dan200/computercraft/core/apis/http/options => kotlin/dan200/computercraft/core/http}/TestHttpApi.kt (75%) rename src/{test/java/dan200/computercraft/support => testFixtures/java/dan200/computercraft/test/core}/ArbitraryByteBuffer.java (99%) rename src/{test/java/dan200/computercraft/support => testFixtures/java/dan200/computercraft/test/core}/ByteBufferMatcher.java (98%) rename src/{test/java/dan200/computercraft/support => testFixtures/java/dan200/computercraft/test/core}/CallCounter.java (95%) rename src/{test/java/dan200/computercraft/support => testFixtures/java/dan200/computercraft/test/core}/ContramapMatcher.java (97%) rename src/{test/java/dan200/computercraft/support => testFixtures/java/dan200/computercraft/test/core}/CustomMatchers.java (96%) create mode 100644 src/testFixtures/java/dan200/computercraft/test/core/apis/BasicApiEnvironment.java rename src/{test/java/dan200/computercraft => testFixtures/java/dan200/computercraft/test}/core/computer/BasicEnvironment.java (91%) rename src/{test/java/dan200/computercraft => testFixtures/java/dan200/computercraft/test}/core/computer/FakeMainThreadScheduler.java (96%) rename src/{test/java/dan200/computercraft => testFixtures/java/dan200/computercraft/test}/core/filesystem/MemoryMount.java (98%) rename src/{test/java/dan200/computercraft => testFixtures/java/dan200/computercraft/test}/core/terminal/TerminalMatchers.java (88%) create mode 100644 src/testFixtures/kotlin/dan200/computercraft/test/core/Assertions.kt create mode 100644 src/testFixtures/kotlin/dan200/computercraft/test/core/computer/KotlinComputerManager.kt create mode 100644 src/testFixtures/kotlin/dan200/computercraft/test/core/computer/KotlinLuaMachine.kt create mode 100644 src/testFixtures/kotlin/dan200/computercraft/test/core/computer/LuaTaskContext.kt create mode 100644 src/testFixtures/kotlin/dan200/computercraft/test/core/computer/LuaTaskRunner.kt diff --git a/build.gradle.kts b/build.gradle.kts index 20fdf53e9..7a5c1f845 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,9 +1,9 @@ import cc.tweaked.gradle.* import net.darkhax.curseforgegradle.TaskPublishCurseForge +import net.minecraftforge.gradle.common.util.RunConfig plugins { // Build - alias(libs.plugins.kotlin) alias(libs.plugins.forgeGradle) alias(libs.plugins.mixinGradle) alias(libs.plugins.librarian) @@ -18,7 +18,7 @@ plugins { id("cc-tweaked.illuaminate") id("cc-tweaked.node") - id("cc-tweaked.java-convention") + id("cc-tweaked.gametest") id("cc-tweaked") } @@ -36,8 +36,6 @@ sourceSets { main { resources.srcDir("src/generated/resources") } - - register("testMod") } minecraft { @@ -76,21 +74,26 @@ minecraft { property("cct.pretty-json", "true") } + fun RunConfig.configureForGameTest() { + mods.register("cctest") { + source(sourceSets["testMod"]) + source(sourceSets["testFixtures"]) + } + } + val testClient by registering { workingDirectory(file("run/testClient")) parent(client.get()) - - mods.register("cctest") { source(sourceSets["testMod"]) } + configureForGameTest() } val testServer by registering { workingDirectory(file("run/testServer")) parent(server.get()) + configureForGameTest() property("cctest.run", "true") property("forge.logging.console.level", "info") - - mods.register("cctest") { source(sourceSets["testMod"]) } } } @@ -113,8 +116,6 @@ configurations { val shade by registering { isTransitive = false } implementation { extendsFrom(shade.get()) } register("cctJavadoc") - - named("testModImplementation") { extendsFrom(implementation.get(), testImplementation.get()) } } dependencies { @@ -132,12 +133,13 @@ dependencies { "shade"(libs.cobalt) + testFixturesApi(libs.bundles.test) + testFixturesApi(libs.bundles.kotlin) + testImplementation(libs.bundles.test) testImplementation(libs.bundles.kotlin) testRuntimeOnly(libs.bundles.testRuntime) - "testModImplementation"(sourceSets.main.get().output) - "cctJavadoc"(libs.cctJavadoc) } @@ -205,7 +207,7 @@ tasks.jar { "Specification-Title" to "computercraft", "Specification-Vendor" to "SquidDev", "Specification-Version" to "1", - "specificationVersion" to "cctweaked", + "Implementation-Title" to "cctweaked", "Implementation-Version" to modVersion, "Implementation-Vendor" to "SquidDev", ) diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 6ba68e361..7e14fdeac 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -9,6 +9,7 @@ repositories { } dependencies { + implementation(libs.kotlin.plugin) implementation(libs.spotless) } diff --git a/buildSrc/src/main/kotlin/cc-tweaked.gametest.gradle.kts b/buildSrc/src/main/kotlin/cc-tweaked.gametest.gradle.kts new file mode 100644 index 000000000..bc8a0e2ba --- /dev/null +++ b/buildSrc/src/main/kotlin/cc-tweaked.gametest.gradle.kts @@ -0,0 +1,53 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +/** + * Sets up the configurations for writing game tests. + * + * See notes in [cc.tweaked.gradle.MinecraftConfigurations] for the general design behind these cursed ideas. + */ + +plugins { + id("cc-tweaked.kotlin-convention") + id("cc-tweaked.java-convention") +} + +val main = sourceSets.main.get() + +// Both testMod and testFixtures inherit from the main classpath, just so we have access to Minecraft classes. +val testMod by sourceSets.creating { + compileClasspath += main.compileClasspath + runtimeClasspath += main.runtimeClasspath +} + +configurations { + named(testMod.compileClasspathConfigurationName) { + shouldResolveConsistentlyWith(compileClasspath.get()) + } + + named(testMod.runtimeClasspathConfigurationName) { + shouldResolveConsistentlyWith(runtimeClasspath.get()) + } +} + +// Like the main test configurations, we're safe to depend on source set outputs. +dependencies { + add(testMod.implementationConfigurationName, main.output) +} + +// Similar to java-test-fixtures, but tries to avoid putting the obfuscated jar on the classpath. + +val testFixtures by sourceSets.creating { + compileClasspath += main.compileClasspath +} + +java.registerFeature("testFixtures") { + usingSourceSet(testFixtures) + disablePublication() +} + +dependencies { + add(testFixtures.implementationConfigurationName, main.output) + + testImplementation(testFixtures(project)) + add(testMod.implementationConfigurationName, testFixtures(project)) +} diff --git a/buildSrc/src/main/kotlin/cc-tweaked.java-convention.gradle.kts b/buildSrc/src/main/kotlin/cc-tweaked.java-convention.gradle.kts index 0580ad5b3..e52bbf060 100644 --- a/buildSrc/src/main/kotlin/cc-tweaked.java-convention.gradle.kts +++ b/buildSrc/src/main/kotlin/cc-tweaked.java-convention.gradle.kts @@ -1,3 +1,4 @@ +import cc.tweaked.gradle.CCTweakedPlugin import cc.tweaked.gradle.LicenseHeader import com.diffplug.gradle.spotless.FormatExtension import com.diffplug.spotless.LineEnding @@ -12,7 +13,7 @@ plugins { java { toolchain { - languageVersion.set(JavaLanguageVersion.of(8)) + languageVersion.set(CCTweakedPlugin.JAVA_VERSION) } withSourcesJar() diff --git a/buildSrc/src/main/kotlin/cc-tweaked.kotlin-convention.gradle.kts b/buildSrc/src/main/kotlin/cc-tweaked.kotlin-convention.gradle.kts new file mode 100644 index 000000000..040204d0c --- /dev/null +++ b/buildSrc/src/main/kotlin/cc-tweaked.kotlin-convention.gradle.kts @@ -0,0 +1,21 @@ +import cc.tweaked.gradle.CCTweakedPlugin +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + kotlin("jvm") +} + +kotlin { + jvmToolchain { + languageVersion.set(CCTweakedPlugin.JAVA_VERSION) + } +} + +tasks.withType(KotlinCompile::class.java).configureEach { + // So technically we shouldn't need to do this as the toolchain sets it above. However, the option only appears + // to be set when the task executes, so doesn't get picked up by IDEs. + kotlinOptions.jvmTarget = when { + CCTweakedPlugin.JAVA_VERSION.asInt() > 8 -> CCTweakedPlugin.JAVA_VERSION.toString() + else -> "1.${CCTweakedPlugin.JAVA_VERSION.asInt()}" + } +} diff --git a/buildSrc/src/main/kotlin/cc/tweaked/gradle/CCTweakedPlugin.kt b/buildSrc/src/main/kotlin/cc/tweaked/gradle/CCTweakedPlugin.kt index c2bd83b49..ca453a841 100644 --- a/buildSrc/src/main/kotlin/cc/tweaked/gradle/CCTweakedPlugin.kt +++ b/buildSrc/src/main/kotlin/cc/tweaked/gradle/CCTweakedPlugin.kt @@ -2,6 +2,7 @@ package cc.tweaked.gradle import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.jvm.toolchain.JavaLanguageVersion /** * Configures projects to match a shared configuration. @@ -10,4 +11,8 @@ class CCTweakedPlugin : Plugin { override fun apply(project: Project) { project.extensions.create("cct", CCTweakedExtension::class.java) } + + companion object { + val JAVA_VERSION = JavaLanguageVersion.of(8) + } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3d7dcd42b..35e818d5c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -49,6 +49,7 @@ junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", vers # Build tools cctJavadoc = { module = "cc.tweaked:cct-javadoc", version.ref = "cctJavadoc" } checkstyle = { module = "com.puppycrawl.tools:checkstyle", version.ref = "checkstyle" } +kotlin-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless" } [plugins] diff --git a/src/main/java/dan200/computercraft/core/computer/ComputerThread.java b/src/main/java/dan200/computercraft/core/computer/ComputerThread.java index c891ed72e..e3afd0e48 100644 --- a/src/main/java/dan200/computercraft/core/computer/ComputerThread.java +++ b/src/main/java/dan200/computercraft/core/computer/ComputerThread.java @@ -5,6 +5,7 @@ */ package dan200.computercraft.core.computer; +import com.google.common.annotations.VisibleForTesting; import com.google.errorprone.annotations.concurrent.GuardedBy; import dan200.computercraft.ComputerCraft; import dan200.computercraft.core.ComputerContext; @@ -443,7 +444,8 @@ public final class ComputerThread * * @return If we have work queued up. */ - boolean hasPendingWork() + @VisibleForTesting + public boolean hasPendingWork() { // FIXME: See comment in scaledPeriod. Again, we access this in multiple threads but not clear if it matters! return !computerQueue.isEmpty(); diff --git a/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java b/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java index 7a97d55a7..6ac766bc7 100644 --- a/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java +++ b/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java @@ -11,16 +11,16 @@ import dan200.computercraft.api.lua.ILuaAPI; import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.api.peripheral.IPeripheral; -import dan200.computercraft.core.computer.BasicEnvironment; import dan200.computercraft.core.computer.Computer; import dan200.computercraft.core.computer.ComputerSide; -import dan200.computercraft.core.computer.FakeMainThreadScheduler; import dan200.computercraft.core.filesystem.FileMount; import dan200.computercraft.core.filesystem.FileSystemException; import dan200.computercraft.core.terminal.Terminal; import dan200.computercraft.shared.peripheral.modem.ModemState; import dan200.computercraft.shared.peripheral.modem.wireless.WirelessModemPeripheral; import dan200.computercraft.support.TestFiles; +import dan200.computercraft.test.core.computer.BasicEnvironment; +import dan200.computercraft.test.core.computer.FakeMainThreadScheduler; import net.minecraft.util.math.vector.Vector3d; import net.minecraft.world.World; import org.apache.logging.log4j.LogManager; diff --git a/src/test/java/dan200/computercraft/core/apis/AsyncRunner.kt b/src/test/java/dan200/computercraft/core/apis/AsyncRunner.kt deleted file mode 100644 index 1839a8efb..000000000 --- a/src/test/java/dan200/computercraft/core/apis/AsyncRunner.kt +++ /dev/null @@ -1,123 +0,0 @@ -package dan200.computercraft.core.apis - -import dan200.computercraft.ComputerCraft -import dan200.computercraft.api.lua.ILuaAPI -import dan200.computercraft.api.lua.MethodResult -import dan200.computercraft.api.peripheral.IPeripheral -import dan200.computercraft.api.peripheral.IWorkMonitor -import dan200.computercraft.core.computer.BasicEnvironment -import dan200.computercraft.core.computer.ComputerEnvironment -import dan200.computercraft.core.computer.ComputerSide -import dan200.computercraft.core.computer.GlobalEnvironment -import dan200.computercraft.core.filesystem.FileSystem -import dan200.computercraft.core.metrics.Metric -import dan200.computercraft.core.terminal.Terminal -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withTimeout -import kotlin.time.Duration -import kotlin.time.Duration.Companion.seconds -import kotlin.time.ExperimentalTime - -abstract class NullApiEnvironment : IAPIEnvironment { - private val computerEnv = BasicEnvironment() - - override fun getComputerID(): Int = 0 - override fun getComputerEnvironment(): ComputerEnvironment = computerEnv - override fun getGlobalEnvironment(): GlobalEnvironment = computerEnv - override fun getMainThreadMonitor(): IWorkMonitor = throw IllegalStateException("Work monitor not available") - override fun getTerminal(): Terminal = throw IllegalStateException("Terminal not available") - override fun getFileSystem(): FileSystem = throw IllegalStateException("Terminal not available") - override fun shutdown() {} - override fun reboot() {} - override fun setOutput(side: ComputerSide?, output: Int) {} - override fun getOutput(side: ComputerSide?): Int = 0 - override fun getInput(side: ComputerSide?): Int = 0 - override fun setBundledOutput(side: ComputerSide?, output: Int) {} - override fun getBundledOutput(side: ComputerSide?): Int = 0 - override fun getBundledInput(side: ComputerSide?): Int = 0 - override fun setPeripheralChangeListener(listener: IAPIEnvironment.IPeripheralChangeListener?) {} - override fun getPeripheral(side: ComputerSide?): IPeripheral? = null - override fun getLabel(): String? = null - override fun setLabel(label: String?) {} - override fun startTimer(ticks: Long): Int = 0 - override fun cancelTimer(id: Int) {} - override fun observe(field: Metric.Counter) {} - override fun observe(field: Metric.Event, change: Long) {} -} - -class EventResult(val name: String, val args: Array) - -class AsyncRunner : NullApiEnvironment() { - private val eventStream: Channel> = Channel(Int.MAX_VALUE) - private val apis: MutableList = mutableListOf() - - override fun queueEvent(event: String?, vararg args: Any?) { - ComputerCraft.log.debug("Queue event $event ${args.contentToString()}") - if (!eventStream.trySend(arrayOf(event, *args)).isSuccess) { - throw IllegalStateException("Queue is full") - } - } - - override fun shutdown() { - super.shutdown() - eventStream.close() - apis.forEach { it.shutdown() } - } - - fun addApi(api: T): T { - apis.add(api) - api.startup() - return api - } - - suspend fun resultOf(toRun: MethodResult): Array { - var running = toRun - while (running.callback != null) running = runOnce(running) - return running.result ?: empty - } - - private suspend fun runOnce(obj: MethodResult): MethodResult { - val callback = obj.callback ?: throw NullPointerException("Callback cannot be null") - - val result = obj.result - val filter: String? = if (result.isNullOrEmpty() || result[0] !is String) { - null - } else { - result[0] as String - } - - return callback.resume(pullEventImpl(filter)) - } - - private suspend fun pullEventImpl(filter: String?): Array { - for (event in eventStream) { - ComputerCraft.log.debug("Pulled event ${event.contentToString()}") - val eventName = event[0] as String - if (filter == null || eventName == filter || eventName == "terminate") return event - } - - throw IllegalStateException("No more events") - } - - suspend fun pullEvent(filter: String? = null): EventResult { - val result = pullEventImpl(filter) - return EventResult(result[0] as String, result.copyOfRange(1, result.size)) - } - - companion object { - private val empty: Array = arrayOf() - - @OptIn(ExperimentalTime::class) - fun runTest(timeout: Duration = 5.seconds, fn: suspend AsyncRunner.() -> Unit) { - runBlocking { - val runner = AsyncRunner() - try { - withTimeout(timeout) { fn(runner) } - } finally { - runner.shutdown() - } - } - } - } -} diff --git a/src/test/java/dan200/computercraft/core/asm/GeneratorTest.java b/src/test/java/dan200/computercraft/core/asm/GeneratorTest.java index 4ebe80167..0fc8e7607 100644 --- a/src/test/java/dan200/computercraft/core/asm/GeneratorTest.java +++ b/src/test/java/dan200/computercraft/core/asm/GeneratorTest.java @@ -18,7 +18,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import static dan200.computercraft.support.ContramapMatcher.contramap; +import static dan200.computercraft.test.core.ContramapMatcher.contramap; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; import static org.junit.jupiter.api.Assertions.assertThrows; diff --git a/src/test/java/dan200/computercraft/core/computer/ComputerBootstrap.java b/src/test/java/dan200/computercraft/core/computer/ComputerBootstrap.java index f1e86f464..25f7eeff1 100644 --- a/src/test/java/dan200/computercraft/core/computer/ComputerBootstrap.java +++ b/src/test/java/dan200/computercraft/core/computer/ComputerBootstrap.java @@ -13,8 +13,9 @@ import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.core.ComputerContext; import dan200.computercraft.core.computer.mainthread.MainThread; -import dan200.computercraft.core.filesystem.MemoryMount; import dan200.computercraft.core.terminal.Terminal; +import dan200.computercraft.test.core.computer.BasicEnvironment; +import dan200.computercraft.test.core.filesystem.MemoryMount; import org.junit.jupiter.api.Assertions; import java.util.Arrays; diff --git a/src/test/java/dan200/computercraft/core/computer/ComputerThreadTest.java b/src/test/java/dan200/computercraft/core/computer/ComputerThreadTest.java index e3597b6dc..108d69274 100644 --- a/src/test/java/dan200/computercraft/core/computer/ComputerThreadTest.java +++ b/src/test/java/dan200/computercraft/core/computer/ComputerThreadTest.java @@ -8,6 +8,7 @@ package dan200.computercraft.core.computer; import dan200.computercraft.ComputerCraft; import dan200.computercraft.core.lua.MachineResult; import dan200.computercraft.support.ConcurrentHelpers; +import dan200.computercraft.test.core.computer.KotlinComputerManager; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -25,12 +26,12 @@ import static org.junit.jupiter.api.Assertions.*; @Execution( ExecutionMode.CONCURRENT ) public class ComputerThreadTest { - private FakeComputerManager manager; + private KotlinComputerManager manager; @BeforeEach public void before() { - manager = new FakeComputerManager(); + manager = new KotlinComputerManager(); } @AfterEach diff --git a/src/test/java/dan200/computercraft/core/computer/FakeComputerManager.java b/src/test/java/dan200/computercraft/core/computer/FakeComputerManager.java deleted file mode 100644 index e2a9e7fb6..000000000 --- a/src/test/java/dan200/computercraft/core/computer/FakeComputerManager.java +++ /dev/null @@ -1,259 +0,0 @@ -/* - * This file is part of ComputerCraft - http://www.computercraft.info - * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. - * Send enquiries to dratcliffe@gmail.com - */ -package dan200.computercraft.core.computer; - -import dan200.computercraft.api.lua.ILuaAPI; -import dan200.computercraft.core.ComputerContext; -import dan200.computercraft.core.lua.ILuaMachine; -import dan200.computercraft.core.lua.MachineResult; -import dan200.computercraft.core.terminal.Terminal; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.io.InputStream; -import java.util.HashMap; -import java.util.Map; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.Condition; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; - -/** - * Creates "fake" computers, which just run user-defined tasks rather than Lua code. - */ -public class FakeComputerManager implements AutoCloseable -{ - interface Task - { - MachineResult run( TimeoutState state ) throws Exception; - } - - private final Map> machines = new HashMap<>(); - private final ComputerContext context = new ComputerContext( - new BasicEnvironment(), - new ComputerThread( 1 ), - new FakeMainThreadScheduler(), - args -> new DummyLuaMachine( args.timeout ) - ); - - private final Lock errorLock = new ReentrantLock(); - private final Condition hasError = errorLock.newCondition(); - private volatile @Nullable Throwable error; - - @Override - public void close() - { - try - { - context.ensureClosed( 1, TimeUnit.SECONDS ); - } - catch( InterruptedException e ) - { - throw new IllegalStateException( "Runtime thread was interrupted", e ); - } - } - - public ComputerContext context() - { - return context; - } - - /** - * Create a new computer which pulls from our task queue. - * - * @return The computer. This will not be started yet, you must call {@link Computer#turnOn()} and - * {@link Computer#tick()} to do so. - */ - public Computer create() - { - Queue queue = new ConcurrentLinkedQueue<>(); - Computer computer = new Computer( context, new BasicEnvironment(), new Terminal( 51, 19, true ), 0 ); - computer.addApi( new QueuePassingAPI( queue ) ); // Inject an extra API to pass the queue to the machine. - machines.put( computer, queue ); - return computer; - } - - /** - * Create and start a new computer which loops forever. - */ - public void createLoopingComputer() - { - Computer computer = create(); - enqueueForever( computer, t -> { - Thread.sleep( 100 ); - return MachineResult.OK; - } ); - computer.turnOn(); - computer.tick(); - } - - /** - * Enqueue a task on a computer. - * - * @param computer The computer to enqueue the work on. - * @param task The task to run. - */ - public void enqueue( Computer computer, Task task ) - { - machines.get( computer ).offer( task ); - } - - /** - * Enqueue a repeated task on a computer. This is automatically requeued when the task finishes, meaning the task - * queue is never empty. - * - * @param computer The computer to enqueue the work on. - * @param task The task to run. - */ - private void enqueueForever( Computer computer, Task task ) - { - machines.get( computer ).offer( t -> { - MachineResult result = task.run( t ); - - enqueueForever( computer, task ); - computer.queueEvent( "some_event", null ); - return result; - } ); - } - - /** - * Sleep for a given period, immediately propagating any exceptions thrown by a computer. - * - * @param delay The duration to sleep for. - * @param unit The time unit the duration is measured in. - * @throws Exception An exception thrown by a running computer. - */ - public void sleep( long delay, TimeUnit unit ) throws Exception - { - errorLock.lock(); - try - { - rethrowIfNeeded(); - if( hasError.await( delay, unit ) ) rethrowIfNeeded(); - } - finally - { - errorLock.unlock(); - } - } - - /** - * Start a computer and wait for it to finish. - * - * @param computer The computer to wait for. - * @throws Exception An exception thrown by a running computer. - */ - public void startAndWait( Computer computer ) throws Exception - { - computer.turnOn(); - computer.tick(); - - do - { - sleep( 100, TimeUnit.MILLISECONDS ); - } while( context.computerScheduler().hasPendingWork() || computer.isOn() ); - - rethrowIfNeeded(); - } - - private void rethrowIfNeeded() throws Exception - { - Throwable error = this.error; - if( error == null ) return; - if( error instanceof Exception ) throw (Exception) error; - rethrow( error ); - } - - @SuppressWarnings( "unchecked" ) - private static void rethrow( Throwable e ) throws T - { - throw (T) e; - } - - private static final class QueuePassingAPI implements ILuaAPI - { - final Queue tasks; - - private QueuePassingAPI( Queue tasks ) - { - this.tasks = tasks; - } - - @Override - public String[] getNames() - { - return new String[0]; - } - } - - private final class DummyLuaMachine implements ILuaMachine - { - private final TimeoutState state; - private @Nullable Queue tasks; - - DummyLuaMachine( TimeoutState state ) - { - this.state = state; - } - - @Override - public void addAPI( @Nonnull ILuaAPI api ) - { - if( api instanceof QueuePassingAPI ) tasks = ((QueuePassingAPI) api).tasks; - } - - @Override - public MachineResult loadBios( @Nonnull InputStream bios ) - { - return MachineResult.OK; - } - - @Override - public MachineResult handleEvent( @Nullable String eventName, @Nullable Object[] arguments ) - { - try - { - if( tasks == null ) throw new IllegalStateException( "Not received tasks yet" ); - return tasks.remove().run( state ); - } - catch( Throwable e ) - { - errorLock.lock(); - try - { - if( error == null ) - { - error = e; - hasError.signal(); - } - else - { - error.addSuppressed( e ); - } - } - finally - { - errorLock.unlock(); - } - - if( !(e instanceof Exception) && !(e instanceof AssertionError) ) rethrow( e ); - return MachineResult.error( e.getMessage() ); - } - } - - @Override - public void printExecutionState( StringBuilder out ) - { - } - - @Override - public void close() - { - } - } -} diff --git a/src/test/java/dan200/computercraft/core/terminal/TerminalTest.java b/src/test/java/dan200/computercraft/core/terminal/TerminalTest.java index c60574dd7..c9674af08 100644 --- a/src/test/java/dan200/computercraft/core/terminal/TerminalTest.java +++ b/src/test/java/dan200/computercraft/core/terminal/TerminalTest.java @@ -7,7 +7,8 @@ package dan200.computercraft.core.terminal; import dan200.computercraft.api.lua.LuaValues; import dan200.computercraft.shared.util.Colour; -import dan200.computercraft.support.CallCounter; +import dan200.computercraft.test.core.CallCounter; +import dan200.computercraft.test.core.terminal.TerminalMatchers; import io.netty.buffer.Unpooled; import net.minecraft.nbt.CompoundNBT; import net.minecraft.network.PacketBuffer; @@ -16,7 +17,7 @@ import org.junit.jupiter.api.Test; import java.nio.ByteBuffer; -import static dan200.computercraft.core.terminal.TerminalMatchers.*; +import static dan200.computercraft.test.core.terminal.TerminalMatchers.*; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.equalTo; diff --git a/src/test/java/dan200/computercraft/shared/network/server/UploadFileMessageTest.java b/src/test/java/dan200/computercraft/shared/network/server/UploadFileMessageTest.java index 21f012cbe..3441e02d2 100644 --- a/src/test/java/dan200/computercraft/shared/network/server/UploadFileMessageTest.java +++ b/src/test/java/dan200/computercraft/shared/network/server/UploadFileMessageTest.java @@ -7,7 +7,7 @@ package dan200.computercraft.shared.network.server; import dan200.computercraft.shared.computer.upload.FileSlice; import dan200.computercraft.shared.computer.upload.FileUpload; -import dan200.computercraft.support.ArbitraryByteBuffer; +import dan200.computercraft.test.core.ArbitraryByteBuffer; import dan200.computercraft.support.FakeContainer; import io.netty.buffer.Unpooled; import net.jqwik.api.*; @@ -21,9 +21,9 @@ import java.util.List; import java.util.stream.Collectors; import static dan200.computercraft.shared.network.server.UploadFileMessage.*; -import static dan200.computercraft.support.ByteBufferMatcher.bufferEqual; -import static dan200.computercraft.support.ContramapMatcher.contramap; -import static dan200.computercraft.support.CustomMatchers.containsWith; +import static dan200.computercraft.test.core.ByteBufferMatcher.bufferEqual; +import static dan200.computercraft.test.core.ContramapMatcher.contramap; +import static dan200.computercraft.test.core.CustomMatchers.containsWith; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/src/test/java/dan200/computercraft/core/apis/http/options/TestHttpApi.kt b/src/test/kotlin/dan200/computercraft/core/http/TestHttpApi.kt similarity index 75% rename from src/test/java/dan200/computercraft/core/apis/http/options/TestHttpApi.kt rename to src/test/kotlin/dan200/computercraft/core/http/TestHttpApi.kt index 334af4b5e..718e393fa 100644 --- a/src/test/java/dan200/computercraft/core/apis/http/options/TestHttpApi.kt +++ b/src/test/kotlin/dan200/computercraft/core/http/TestHttpApi.kt @@ -1,8 +1,10 @@ -package dan200.computercraft.core.apis.http.options +package dan200.computercraft.core.http import dan200.computercraft.ComputerCraft -import dan200.computercraft.core.apis.AsyncRunner import dan200.computercraft.core.apis.HTTPAPI +import dan200.computercraft.core.apis.http.options.Action +import dan200.computercraft.core.apis.http.options.AddressRule +import dan200.computercraft.test.core.computer.LuaTaskRunner import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.Assertions.assertArrayEquals import org.junit.jupiter.api.Assertions.assertEquals @@ -36,15 +38,15 @@ class TestHttpApi { @Test fun `Connects to websocket`() { - AsyncRunner.runTest { - val httpApi = addApi(HTTPAPI(this)) + LuaTaskRunner.runTest { + val httpApi = addApi(HTTPAPI(environment)) val result = httpApi.websocket(WS_ADDRESS, Optional.empty()) assertArrayEquals(arrayOf(true), result, "Should have created websocket") val event = pullEvent() - assertEquals("websocket_success", event.name) { - "Websocket failed to connect: ${event.args.contentToString()}" + assertEquals("websocket_success", event[0]) { + "Websocket failed to connect: ${event.contentToString()}" } } } diff --git a/src/test/java/dan200/computercraft/support/ArbitraryByteBuffer.java b/src/testFixtures/java/dan200/computercraft/test/core/ArbitraryByteBuffer.java similarity index 99% rename from src/test/java/dan200/computercraft/support/ArbitraryByteBuffer.java rename to src/testFixtures/java/dan200/computercraft/test/core/ArbitraryByteBuffer.java index de60f2f3b..ea5585e3b 100644 --- a/src/test/java/dan200/computercraft/support/ArbitraryByteBuffer.java +++ b/src/testFixtures/java/dan200/computercraft/test/core/ArbitraryByteBuffer.java @@ -3,7 +3,7 @@ * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ -package dan200.computercraft.support; +package dan200.computercraft.test.core; import net.jqwik.api.*; import net.jqwik.api.arbitraries.SizableArbitrary; diff --git a/src/test/java/dan200/computercraft/support/ByteBufferMatcher.java b/src/testFixtures/java/dan200/computercraft/test/core/ByteBufferMatcher.java similarity index 98% rename from src/test/java/dan200/computercraft/support/ByteBufferMatcher.java rename to src/testFixtures/java/dan200/computercraft/test/core/ByteBufferMatcher.java index b94bf96b4..c0f801b4c 100644 --- a/src/test/java/dan200/computercraft/support/ByteBufferMatcher.java +++ b/src/testFixtures/java/dan200/computercraft/test/core/ByteBufferMatcher.java @@ -3,7 +3,7 @@ * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ -package dan200.computercraft.support; +package dan200.computercraft.test.core; import org.hamcrest.Description; import org.hamcrest.Matcher; diff --git a/src/test/java/dan200/computercraft/support/CallCounter.java b/src/testFixtures/java/dan200/computercraft/test/core/CallCounter.java similarity index 95% rename from src/test/java/dan200/computercraft/support/CallCounter.java rename to src/testFixtures/java/dan200/computercraft/test/core/CallCounter.java index 213ad13e0..5916f865b 100644 --- a/src/test/java/dan200/computercraft/support/CallCounter.java +++ b/src/testFixtures/java/dan200/computercraft/test/core/CallCounter.java @@ -3,7 +3,7 @@ * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ -package dan200.computercraft.support; +package dan200.computercraft.test.core; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/src/test/java/dan200/computercraft/support/ContramapMatcher.java b/src/testFixtures/java/dan200/computercraft/test/core/ContramapMatcher.java similarity index 97% rename from src/test/java/dan200/computercraft/support/ContramapMatcher.java rename to src/testFixtures/java/dan200/computercraft/test/core/ContramapMatcher.java index 6f09aab96..4a982d558 100644 --- a/src/test/java/dan200/computercraft/support/ContramapMatcher.java +++ b/src/testFixtures/java/dan200/computercraft/test/core/ContramapMatcher.java @@ -3,7 +3,7 @@ * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ -package dan200.computercraft.support; +package dan200.computercraft.test.core; import org.hamcrest.FeatureMatcher; import org.hamcrest.Matcher; diff --git a/src/test/java/dan200/computercraft/support/CustomMatchers.java b/src/testFixtures/java/dan200/computercraft/test/core/CustomMatchers.java similarity index 96% rename from src/test/java/dan200/computercraft/support/CustomMatchers.java rename to src/testFixtures/java/dan200/computercraft/test/core/CustomMatchers.java index 7a1d4e71a..f3335efd7 100644 --- a/src/test/java/dan200/computercraft/support/CustomMatchers.java +++ b/src/testFixtures/java/dan200/computercraft/test/core/CustomMatchers.java @@ -3,7 +3,7 @@ * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ -package dan200.computercraft.support; +package dan200.computercraft.test.core; import org.hamcrest.Matcher; import org.hamcrest.Matchers; diff --git a/src/testFixtures/java/dan200/computercraft/test/core/apis/BasicApiEnvironment.java b/src/testFixtures/java/dan200/computercraft/test/core/apis/BasicApiEnvironment.java new file mode 100644 index 000000000..28ecb964d --- /dev/null +++ b/src/testFixtures/java/dan200/computercraft/test/core/apis/BasicApiEnvironment.java @@ -0,0 +1,161 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.test.core.apis; + +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.api.peripheral.IWorkMonitor; +import dan200.computercraft.core.apis.IAPIEnvironment; +import dan200.computercraft.core.computer.ComputerEnvironment; +import dan200.computercraft.core.computer.ComputerSide; +import dan200.computercraft.core.computer.GlobalEnvironment; +import dan200.computercraft.core.filesystem.FileSystem; +import dan200.computercraft.core.metrics.Metric; +import dan200.computercraft.core.terminal.Terminal; +import dan200.computercraft.test.core.computer.BasicEnvironment; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public abstract class BasicApiEnvironment implements IAPIEnvironment +{ + private final BasicEnvironment environment; + private @Nullable String label; + + public BasicApiEnvironment( BasicEnvironment environment ) + { + this.environment = environment; + } + + @Override + public int getComputerID() + { + return 0; + } + + @Nonnull + @Override + public ComputerEnvironment getComputerEnvironment() + { + return environment; + } + + @Nonnull + @Override + public GlobalEnvironment getGlobalEnvironment() + { + return environment; + } + + @Nonnull + @Override + public IWorkMonitor getMainThreadMonitor() + { + throw new IllegalStateException( "Main thread monitor not available" ); + } + + @Nonnull + @Override + public Terminal getTerminal() + { + throw new IllegalStateException( "Terminal not available" ); + } + + @Override + public FileSystem getFileSystem() + { + throw new IllegalStateException( "Filesystem not available" ); + } + + @Override + public void shutdown() + { + } + + @Override + public void reboot() + { + } + + @Override + public void setOutput( ComputerSide side, int output ) + { + } + + @Override + public int getOutput( ComputerSide side ) + { + return 0; + } + + @Override + public int getInput( ComputerSide side ) + { + return 0; + } + + @Override + public void setBundledOutput( ComputerSide side, int output ) + { + } + + @Override + public int getBundledOutput( ComputerSide side ) + { + return 0; + } + + @Override + public int getBundledInput( ComputerSide side ) + { + return 0; + } + + @Override + public void setPeripheralChangeListener( @Nullable IPeripheralChangeListener listener ) + { + } + + @Nullable + @Override + public IPeripheral getPeripheral( ComputerSide side ) + { + return null; + } + + @Nullable + @Override + public String getLabel() + { + return label; + } + + @Override + public void setLabel( @Nullable String label ) + { + this.label = label; + } + + @Override + public int startTimer( long ticks ) + { + throw new IllegalStateException( "Cannot start timers" ); + } + + @Override + public void cancelTimer( int id ) + { + } + + @Override + public void observe( @Nonnull Metric.Event summary, long value ) + { + } + + @Override + public void observe( @Nonnull Metric.Counter counter ) + { + } +} diff --git a/src/test/java/dan200/computercraft/core/computer/BasicEnvironment.java b/src/testFixtures/java/dan200/computercraft/test/core/computer/BasicEnvironment.java similarity index 91% rename from src/test/java/dan200/computercraft/core/computer/BasicEnvironment.java rename to src/testFixtures/java/dan200/computercraft/test/core/computer/BasicEnvironment.java index e763f44b8..8d48343a9 100644 --- a/src/test/java/dan200/computercraft/core/computer/BasicEnvironment.java +++ b/src/testFixtures/java/dan200/computercraft/test/core/computer/BasicEnvironment.java @@ -3,16 +3,18 @@ * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ -package dan200.computercraft.core.computer; +package dan200.computercraft.test.core.computer; import dan200.computercraft.ComputerCraft; import dan200.computercraft.api.filesystem.IMount; import dan200.computercraft.api.filesystem.IWritableMount; +import dan200.computercraft.core.computer.ComputerEnvironment; +import dan200.computercraft.core.computer.GlobalEnvironment; import dan200.computercraft.core.filesystem.FileMount; import dan200.computercraft.core.filesystem.JarMount; -import dan200.computercraft.core.filesystem.MemoryMount; import dan200.computercraft.core.metrics.Metric; import dan200.computercraft.core.metrics.MetricsObserver; +import dan200.computercraft.test.core.filesystem.MemoryMount; import javax.annotation.Nonnull; import java.io.File; @@ -24,7 +26,8 @@ import java.net.URISyntaxException; import java.net.URL; /** - * A very basic environment. + * A basic implementation of {@link ComputerEnvironment} and {@link GlobalEnvironment}, suitable for a context which + * will only run a single computer. */ public class BasicEnvironment implements ComputerEnvironment, GlobalEnvironment, MetricsObserver { diff --git a/src/test/java/dan200/computercraft/core/computer/FakeMainThreadScheduler.java b/src/testFixtures/java/dan200/computercraft/test/core/computer/FakeMainThreadScheduler.java similarity index 96% rename from src/test/java/dan200/computercraft/core/computer/FakeMainThreadScheduler.java rename to src/testFixtures/java/dan200/computercraft/test/core/computer/FakeMainThreadScheduler.java index fbb758ab4..5735146d0 100644 --- a/src/test/java/dan200/computercraft/core/computer/FakeMainThreadScheduler.java +++ b/src/testFixtures/java/dan200/computercraft/test/core/computer/FakeMainThreadScheduler.java @@ -3,7 +3,7 @@ * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ -package dan200.computercraft.core.computer; +package dan200.computercraft.test.core.computer; import dan200.computercraft.core.computer.mainthread.MainThreadScheduler; import dan200.computercraft.core.metrics.MetricsObserver; diff --git a/src/test/java/dan200/computercraft/core/filesystem/MemoryMount.java b/src/testFixtures/java/dan200/computercraft/test/core/filesystem/MemoryMount.java similarity index 98% rename from src/test/java/dan200/computercraft/core/filesystem/MemoryMount.java rename to src/testFixtures/java/dan200/computercraft/test/core/filesystem/MemoryMount.java index dc785de3a..8219d75db 100644 --- a/src/test/java/dan200/computercraft/core/filesystem/MemoryMount.java +++ b/src/testFixtures/java/dan200/computercraft/test/core/filesystem/MemoryMount.java @@ -3,7 +3,7 @@ * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ -package dan200.computercraft.core.filesystem; +package dan200.computercraft.test.core.filesystem; import dan200.computercraft.api.filesystem.IWritableMount; import dan200.computercraft.core.apis.handles.ArrayByteChannel; diff --git a/src/test/java/dan200/computercraft/core/terminal/TerminalMatchers.java b/src/testFixtures/java/dan200/computercraft/test/core/terminal/TerminalMatchers.java similarity index 88% rename from src/test/java/dan200/computercraft/core/terminal/TerminalMatchers.java rename to src/testFixtures/java/dan200/computercraft/test/core/terminal/TerminalMatchers.java index 3359f5c16..248883339 100644 --- a/src/test/java/dan200/computercraft/core/terminal/TerminalMatchers.java +++ b/src/testFixtures/java/dan200/computercraft/test/core/terminal/TerminalMatchers.java @@ -3,9 +3,11 @@ * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ -package dan200.computercraft.core.terminal; +package dan200.computercraft.test.core.terminal; -import dan200.computercraft.support.ContramapMatcher; +import dan200.computercraft.core.terminal.Terminal; +import dan200.computercraft.core.terminal.TextBuffer; +import dan200.computercraft.test.core.ContramapMatcher; import org.hamcrest.Matcher; import org.hamcrest.Matchers; diff --git a/src/testFixtures/kotlin/dan200/computercraft/test/core/Assertions.kt b/src/testFixtures/kotlin/dan200/computercraft/test/core/Assertions.kt new file mode 100644 index 000000000..e83e80381 --- /dev/null +++ b/src/testFixtures/kotlin/dan200/computercraft/test/core/Assertions.kt @@ -0,0 +1,63 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.test.core + +import org.hamcrest.BaseMatcher +import org.hamcrest.Description +import org.hamcrest.Matcher +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.collection.IsArray +import org.junit.jupiter.api.Assertions + +/** Postfix version of [Assertions.assertArrayEquals] */ +fun Array?.assertArrayEquals(vararg expected: Any?, message: String? = null) { + assertThat( + message ?: "", + this, + IsArrayVerbose(expected.map { FuzzyEqualTo(it) }.toTypedArray()), + ) +} + +/** + * Extension of [IsArray] which always prints the array, not just when the items are mismatched. + */ +internal class IsArrayVerbose(private val elementMatchers: Array>) : IsArray(elementMatchers) { + override fun describeMismatchSafely(actual: Array, description: Description) { + description.appendText("array was ").appendValue(actual) + if (actual.size != elementMatchers.size) { + description.appendText(" with length ").appendValue(actual.size) + return + } + + for (i in actual.indices) { + if (!elementMatchers[i].matches(actual[i])) { + description.appendText("with element ").appendValue(i).appendText(" ") + elementMatchers[i].describeMismatch(actual[i], description) + return + } + } + } +} + +/** + * An equality matcher which is slightly more relaxed on comparing some values. + */ +internal class FuzzyEqualTo(private val expected: Any?) : BaseMatcher() { + override fun describeTo(description: Description) { + description.appendValue(expected) + } + + override fun matches(actual: Any?): Boolean { + if (actual == null) return false + + if (actual is Number && expected is Number && actual.javaClass != expected.javaClass) { + // Allow equating integers and floats. + return actual.toDouble() == expected.toDouble() + } + + return actual == expected + } +} diff --git a/src/testFixtures/kotlin/dan200/computercraft/test/core/computer/KotlinComputerManager.kt b/src/testFixtures/kotlin/dan200/computercraft/test/core/computer/KotlinComputerManager.kt new file mode 100644 index 000000000..f9964da9c --- /dev/null +++ b/src/testFixtures/kotlin/dan200/computercraft/test/core/computer/KotlinComputerManager.kt @@ -0,0 +1,188 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.test.core.computer + +import dan200.computercraft.api.lua.ILuaAPI +import dan200.computercraft.core.ComputerContext +import dan200.computercraft.core.computer.Computer +import dan200.computercraft.core.computer.ComputerThread +import dan200.computercraft.core.computer.TimeoutState +import dan200.computercraft.core.lua.MachineEnvironment +import dan200.computercraft.core.lua.MachineResult +import dan200.computercraft.core.terminal.Terminal +import java.util.* +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.TimeUnit +import java.util.concurrent.locks.Lock +import java.util.concurrent.locks.ReentrantLock + +typealias FakeComputerTask = (state: TimeoutState) -> MachineResult + +/** + * Creates "fake" computers, which just run user-defined tasks rather than Lua code. + */ +class KotlinComputerManager : AutoCloseable { + + private val machines: MutableMap> = HashMap() + private val context = ComputerContext(BasicEnvironment(), ComputerThread(1), FakeMainThreadScheduler()) { DummyLuaMachine(it) } + private val errorLock: Lock = ReentrantLock() + private val hasError = errorLock.newCondition() + + @Volatile + private var error: Throwable? = null + override fun close() { + try { + context.ensureClosed(1, TimeUnit.SECONDS) + } catch (e: InterruptedException) { + throw IllegalStateException("Runtime thread was interrupted", e) + } + } + + fun context(): ComputerContext { + return context + } + + /** + * Create a new computer which pulls from our task queue. + * + * @return The computer. This will not be started yet, you must call [Computer.turnOn] and + * [Computer.tick] to do so. + */ + fun create(): Computer { + val queue: Queue = ConcurrentLinkedQueue() + val computer = Computer(context, BasicEnvironment(), Terminal(51, 19, true), 0) + computer.addApi(QueuePassingAPI(queue)) // Inject an extra API to pass the queue to the machine. + machines[computer] = queue + return computer + } + + /** + * Create and start a new computer which loops forever. + */ + fun createLoopingComputer() { + val computer = create() + enqueueForever(computer) { + Thread.sleep(100) + MachineResult.OK + } + computer.turnOn() + computer.tick() + } + + /** + * Enqueue a task on a computer. + * + * @param computer The computer to enqueue the work on. + * @param task The task to run. + */ + fun enqueue(computer: Computer, task: FakeComputerTask) { + machines[computer]!!.offer(task) + } + + /** + * Enqueue a repeated task on a computer. This is automatically requeued when the task finishes, meaning the task + * queue is never empty. + * + * @param computer The computer to enqueue the work on. + * @param task The task to run. + */ + private fun enqueueForever(computer: Computer, task: FakeComputerTask) { + machines[computer]!!.offer { + val result = task(it) + enqueueForever(computer, task) + computer.queueEvent("some_event", null) + result + } + } + + /** + * Sleep for a given period, immediately propagating any exceptions thrown by a computer. + * + * @param delay The duration to sleep for. + * @param unit The time unit the duration is measured in. + * @throws Exception An exception thrown by a running computer. + */ + @Throws(Exception::class) + fun sleep(delay: Long, unit: TimeUnit?) { + errorLock.lock() + try { + rethrowIfNeeded() + if (hasError.await(delay, unit)) rethrowIfNeeded() + } finally { + errorLock.unlock() + } + } + + /** + * Start a computer and wait for it to finish. + * + * @param computer The computer to wait for. + * @throws Exception An exception thrown by a running computer. + */ + @Throws(Exception::class) + fun startAndWait(computer: Computer) { + computer.turnOn() + computer.tick() + do { + sleep(100, TimeUnit.MILLISECONDS) + } while (context.computerScheduler().hasPendingWork() || computer.isOn) + + rethrowIfNeeded() + } + + @Throws(Exception::class) + private fun rethrowIfNeeded() { + val error = error ?: return + throw error + } + + private class QueuePassingAPI constructor(val tasks: Queue) : ILuaAPI { + override fun getNames(): Array = arrayOf() + } + + private inner class DummyLuaMachine(private val environment: MachineEnvironment) : KotlinLuaMachine(environment) { + private var tasks: Queue? = null + override fun addAPI(api: ILuaAPI) { + super.addAPI(api) + if (api is QueuePassingAPI) tasks = api.tasks + } + + override fun getTask(): (suspend KotlinLuaMachine.() -> Unit)? { + try { + val tasks = this.tasks ?: throw NullPointerException("Not received tasks yet") + val task = tasks.remove() + return { + try { + task(environment.timeout) + } catch (e: Throwable) { + reportError(e) + } + } + } catch (e: Throwable) { + reportError(e) + return null + } + } + + override fun close() {} + + private fun reportError(e: Throwable) { + errorLock.lock() + try { + if (error == null) { + error = e + hasError.signal() + } else { + error!!.addSuppressed(e) + } + } finally { + errorLock.unlock() + } + + if (e is Exception || e is AssertionError) return else throw e + } + } +} diff --git a/src/testFixtures/kotlin/dan200/computercraft/test/core/computer/KotlinLuaMachine.kt b/src/testFixtures/kotlin/dan200/computercraft/test/core/computer/KotlinLuaMachine.kt new file mode 100644 index 000000000..ad36ae258 --- /dev/null +++ b/src/testFixtures/kotlin/dan200/computercraft/test/core/computer/KotlinLuaMachine.kt @@ -0,0 +1,41 @@ +package dan200.computercraft.test.core.computer + +import dan200.computercraft.api.lua.ILuaAPI +import dan200.computercraft.api.lua.ILuaContext +import dan200.computercraft.core.lua.ILuaMachine +import dan200.computercraft.core.lua.MachineEnvironment +import dan200.computercraft.core.lua.MachineResult +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.io.InputStream + +/** + * An [ILuaMachine] which runs Kotlin functions instead. + */ +abstract class KotlinLuaMachine(environment: MachineEnvironment) : ILuaMachine, AbstractLuaTaskContext() { + override val context: ILuaContext = environment.context + + override fun addAPI(api: ILuaAPI) = addApi(api) + + override fun loadBios(bios: InputStream): MachineResult = MachineResult.OK + + override fun handleEvent(eventName: String?, arguments: Array?): MachineResult { + if (hasEventListeners) { + queueEvent(eventName, arguments) + } else { + val task = getTask() + if (task != null) CoroutineScope(Dispatchers.Unconfined + CoroutineName("Computer")).launch { task() } + } + + return MachineResult.OK + } + + override fun printExecutionState(out: StringBuilder) {} + + /** + * Get the next task to execute on this computer. + */ + protected abstract fun getTask(): (suspend KotlinLuaMachine.() -> Unit)? +} diff --git a/src/testFixtures/kotlin/dan200/computercraft/test/core/computer/LuaTaskContext.kt b/src/testFixtures/kotlin/dan200/computercraft/test/core/computer/LuaTaskContext.kt new file mode 100644 index 000000000..28ab11c3a --- /dev/null +++ b/src/testFixtures/kotlin/dan200/computercraft/test/core/computer/LuaTaskContext.kt @@ -0,0 +1,87 @@ +package dan200.computercraft.test.core.computer + +import dan200.computercraft.api.lua.ILuaAPI +import dan200.computercraft.api.lua.ILuaContext +import dan200.computercraft.api.lua.MethodResult +import dan200.computercraft.api.lua.ObjectArguments +import dan200.computercraft.core.apis.PeripheralAPI +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.suspendCancellableCoroutine + +/** + * The context for tasks which consume Lua objects. + * + * This provides helpers for converting CC's callback-based code into a more direct style based on Kotlin coroutines. + */ +interface LuaTaskContext { + /** The current Lua context, to be passed to method calls. */ + val context: ILuaContext + + /** Get a registered API. */ + fun getApi(api: Class): T + + /** Pull a Lua event */ + suspend fun pullEvent(event: String? = null): Array + + /** Resolve a [MethodResult] until completion, returning the resulting values. */ + suspend fun MethodResult.await(): Array? { + var result = this + while (true) { + val callback = result.callback + val values = result.result + + if (callback == null) return values + + val filter = if (values == null) null else values[0] as String? + result = callback.resume(pullEvent(filter)) + } + } + + /** Call a peripheral method. */ + suspend fun LuaTaskContext.callPeripheral(name: String, method: String, vararg args: Any?): Array? = + getApi().call(context, ObjectArguments(name, method, *args)).await() +} + +/** Get a registered API. */ +inline fun LuaTaskContext.getApi(): T = getApi(T::class.java) + +abstract class AbstractLuaTaskContext : LuaTaskContext, AutoCloseable { + private val pullEvents = mutableListOf() + private val apis = mutableMapOf, ILuaAPI>() + + protected fun addApi(api: ILuaAPI) { + apis[api.javaClass] = api + } + + protected val hasEventListeners + get() = pullEvents.isNotEmpty() + + protected fun queueEvent(eventName: String?, arguments: Array?) { + val fullEvent: Array = when { + eventName == null && arguments == null -> arrayOf() + eventName != null && arguments == null -> arrayOf(eventName) + eventName == null && arguments != null -> arguments + else -> arrayOf(eventName, *arguments!!) + } + for (i in pullEvents.size - 1 downTo 0) { + val puller = pullEvents[i] + if (puller.name == null || puller.name == eventName || eventName == "terminate") { + pullEvents.removeAt(i) + puller.cont.resumeWith(Result.success(fullEvent)) + } + } + } + + override fun close() { + for (pullEvent in pullEvents) pullEvent.cont.cancel() + pullEvents.clear() + } + + final override fun getApi(api: Class): T = + api.cast(apis[api] ?: throw IllegalStateException("No API of type ${api.name}")) + + final override suspend fun pullEvent(event: String?): Array = + suspendCancellableCoroutine { cont -> pullEvents.add(PullEvent(event, cont)) } + + private class PullEvent(val name: String?, val cont: CancellableContinuation>) +} diff --git a/src/testFixtures/kotlin/dan200/computercraft/test/core/computer/LuaTaskRunner.kt b/src/testFixtures/kotlin/dan200/computercraft/test/core/computer/LuaTaskRunner.kt new file mode 100644 index 000000000..4ac65d1cb --- /dev/null +++ b/src/testFixtures/kotlin/dan200/computercraft/test/core/computer/LuaTaskRunner.kt @@ -0,0 +1,64 @@ +package dan200.computercraft.test.core.computer + +import dan200.computercraft.api.lua.ILuaAPI +import dan200.computercraft.api.lua.ILuaContext +import dan200.computercraft.api.lua.LuaException +import dan200.computercraft.core.apis.IAPIEnvironment +import dan200.computercraft.test.core.apis.BasicApiEnvironment +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +class LuaTaskRunner : AbstractLuaTaskContext() { + private val eventStream: Channel = Channel(Channel.UNLIMITED) + private val apis = mutableListOf() + + val environment: IAPIEnvironment = object : BasicApiEnvironment(BasicEnvironment()) { + override fun queueEvent(event: String?, vararg args: Any?) { + if (eventStream.trySend(Event(event, args)).isFailure) { + throw IllegalStateException("Queue is full") + } + } + + override fun shutdown() { + super.shutdown() + eventStream.close() + } + } + override val context = + ILuaContext { throw LuaException("Cannot queue main thread task") } + + fun addApi(api: T): T { + super.addApi(api) + apis.add(api) + api.startup() + return api + } + + override fun close() { + environment.shutdown() + } + + private suspend fun run() { + for (event in eventStream) { + queueEvent(event.name, event.args) + } + } + + private class Event(val name: String?, val args: Array) + + companion object { + fun runTest(timeout: Duration = 5.seconds, fn: suspend LuaTaskRunner.() -> Unit) { + runBlocking { + withTimeout(timeout) { + val runner = LuaTaskRunner() + launch { runner.run() } + runner.use { fn(runner) } + } + } + } + } +} From 3d6ef0cf96d4cf1075ab6819734aadf57577dcb5 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sat, 29 Oct 2022 22:47:31 +0100 Subject: [PATCH 5/7] Fix peripheral API using the wrong methods --- build.gradle.kts | 2 +- .../computercraft/lua/rom/apis/peripheral.lua | 4 +- .../core/ComputerTestDelegate.java | 76 ++++++++++++++----- .../test-rom/spec/apis/peripheral_spec.lua | 55 +++++++++++++- 4 files changed, 111 insertions(+), 26 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 7a5c1f845..5691dc3c0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -300,7 +300,7 @@ val docWebsite by tasks.registering(Copy::class) { // Check tasks tasks.test { - systemProperty("cct.test-files", buildDir.resolve("tmp/test-files").absolutePath) + systemProperty("cct.test-files", buildDir.resolve("tmp/testFiles").absolutePath) } val lintLua by tasks.registering(IlluaminateExec::class) { 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 f5e8fcc8b..b10530232 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/peripheral.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/peripheral.lua @@ -217,7 +217,7 @@ function getMethods(name) end for n = 1, #sides do local side = sides[n] - if native.hasType(side, "peripheral_hub") and native.call(side, "isPresentRemote", peripheral) then + if native.hasType(side, "peripheral_hub") and native.call(side, "isPresentRemote", name) then return native.call(side, "getMethodsRemote", name) end end @@ -257,7 +257,7 @@ function call(name, method, ...) for n = 1, #sides do local side = sides[n] - if native.hasType(side, "peripheral_hub") and native.call(side, "isPresentRemote", peripheral) then + if native.hasType(side, "peripheral_hub") and native.call(side, "isPresentRemote", name) then return native.call(side, "callRemote", name, method, ...) end end diff --git a/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java b/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java index 6ac766bc7..095ede05f 100644 --- a/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java +++ b/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java @@ -16,13 +16,9 @@ import dan200.computercraft.core.computer.ComputerSide; import dan200.computercraft.core.filesystem.FileMount; import dan200.computercraft.core.filesystem.FileSystemException; import dan200.computercraft.core.terminal.Terminal; -import dan200.computercraft.shared.peripheral.modem.ModemState; -import dan200.computercraft.shared.peripheral.modem.wireless.WirelessModemPeripheral; import dan200.computercraft.support.TestFiles; import dan200.computercraft.test.core.computer.BasicEnvironment; import dan200.computercraft.test.core.computer.FakeMainThreadScheduler; -import net.minecraft.util.math.vector.Vector3d; -import net.minecraft.world.World; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.jupiter.api.*; @@ -118,6 +114,7 @@ public class ComputerTestDelegate context = new ComputerContext( environment, 1, new FakeMainThreadScheduler() ); computer = new Computer( context, environment, term, 0 ); computer.getEnvironment().setPeripheral( ComputerSide.TOP, new FakeModem() ); + computer.getEnvironment().setPeripheral( ComputerSide.BOTTOM, new FakePeripheralHub() ); computer.addApi( new CctTestAPI() ); computer.turnOn(); @@ -282,26 +279,13 @@ public class ComputerTestDelegate return name.replace( "\0", " -> " ); } - private static class FakeModem extends WirelessModemPeripheral + public static class FakeModem implements IPeripheral { - FakeModem() - { - super( new ModemState(), true ); - } - @Nonnull @Override - @SuppressWarnings( "ConstantConditions" ) - public World getWorld() + public String getType() { - return null; - } - - @Nonnull - @Override - public Vector3d getPosition() - { - return Vector3d.ZERO; + return "modem"; } @Override @@ -309,6 +293,58 @@ public class ComputerTestDelegate { return this == other; } + + @LuaFunction + public final boolean isOpen( int channel ) + { + return false; + } + } + + public static class FakePeripheralHub implements IPeripheral + { + @Nonnull + @Override + public String getType() + { + return "peripheral_hub"; + } + + @Override + public boolean equals( @Nullable IPeripheral other ) + { + return this == other; + } + + @LuaFunction + public final Collection getNamesRemote() + { + return Collections.singleton( "remote_1" ); + } + + @LuaFunction + public final boolean isPresentRemote( String name ) + { + return name.equals( "remote_1" ); + } + + @LuaFunction + public final Object[] getTypeRemote( String name ) + { + return name.equals( "remote_1" ) ? new Object[] { "remote", "other_type" } : null; + } + + @LuaFunction + public final Object[] hasTypeRemote( String name, String type ) + { + return name.equals( "remote_1" ) ? new Object[] { type.equals( "remote" ) || type.equals( "other_type" ) } : null; + } + + @LuaFunction + public final Object[] getMethodsRemote( String name ) + { + return name.equals( "remote_1" ) ? new Object[] { Collections.singletonList( "func" ) } : null; + } } public class CctTestAPI implements ILuaAPI diff --git a/src/test/resources/test-rom/spec/apis/peripheral_spec.lua b/src/test/resources/test-rom/spec/apis/peripheral_spec.lua index c64c042ce..36d507a5e 100644 --- a/src/test/resources/test-rom/spec/apis/peripheral_spec.lua +++ b/src/test/resources/test-rom/spec/apis/peripheral_spec.lua @@ -1,5 +1,6 @@ describe("The peripheral library", function() local it_modem = peripheral.getType("top") == "modem" and it or pending + local it_remote = peripheral.getType("bottom") == "peripheral_hub" and it or pending local multitype_peripheral = setmetatable({}, { __name = "peripheral", @@ -13,6 +14,16 @@ describe("The peripheral library", function() peripheral.isPresent("") expect.error(peripheral.isPresent, nil):eq("bad argument #1 (expected string, got nil)") end) + + it_modem("asserts the presence of local peripherals", function() + expect(peripheral.isPresent("top")):eq(true) + expect(peripheral.isPresent("left")):eq(false) + end) + + it_remote("asserts the presence of remote peripherals", function() + expect(peripheral.isPresent("remote_1")):eq(true) + expect(peripheral.isPresent("remote_2")):eq(false) + end) end) describe("peripheral.getName", function() @@ -24,6 +35,10 @@ describe("The peripheral library", function() it_modem("can get the name of a wrapped peripheral", function() expect(peripheral.getName(peripheral.wrap("top"))):eq("top") end) + + it("can get the name of a fake peripheral", function() + expect(peripheral.getName(multitype_peripheral)):eq("top") + end) end) describe("peripheral.getType", function() @@ -34,13 +49,18 @@ describe("The peripheral library", function() end) it("returns nil when no peripheral is present", function() - expect(peripheral.getType("bottom")):eq(nil) + expect(peripheral.getType("left")):eq(nil) + expect(peripheral.getType("remote_2")):eq(nil) end) - it_modem("can get the type of a peripheral by side", function() + it_modem("can get the type of a local peripheral", function() expect(peripheral.getType("top")):eq("modem") end) + it_remote("can get the type of a remote peripheral", function() + expect(peripheral.getType("remote_1")):eq("remote") + end) + it_modem("can get the type of a wrapped peripheral", function() expect(peripheral.getType(peripheral.wrap("top"))):eq("modem") end) @@ -59,7 +79,8 @@ describe("The peripheral library", function() end) it("returns nil when no peripherals are present", function() - expect(peripheral.hasType("bottom", "modem")):eq(nil) + expect(peripheral.hasType("left", "modem")):eq(nil) + expect(peripheral.hasType("remote_2", "remote")):eq(nil) end) it_modem("can check type of a peripheral by side", function() @@ -76,6 +97,10 @@ describe("The peripheral library", function() expect(peripheral.hasType(multitype_peripheral, "inventory")):eq(true) expect(peripheral.hasType(multitype_peripheral, "something else")):eq(false) end) + + it_remote("can check type of a remote peripheral", function() + expect(peripheral.hasType("remote_1", "remote")):eq(true) + end) end) describe("peripheral.getMethods", function() @@ -103,6 +128,18 @@ describe("The peripheral library", function() peripheral.wrap("") expect.error(peripheral.wrap, nil):eq("bad argument #1 (expected string, got nil)") end) + + it_modem("wraps a local peripheral", function() + local p = peripheral.wrap("top") + expect(type(p)):eq("table") + expect(type(next(p))):eq("string") + end) + + it_remote("wraps a remote peripheral", function() + local p = peripheral.wrap("remote_1") + expect(type(p)):eq("table") + expect(next(p)):eq("func") + end) end) describe("peripheral.find", function() @@ -113,5 +150,17 @@ describe("The peripheral library", function() expect.error(peripheral.find, nil):eq("bad argument #1 (expected string, got nil)") expect.error(peripheral.find, "", false):eq("bad argument #2 (expected function, got boolean)") end) + + it_modem("finds a local peripheral", function() + local p = peripheral.find("modem") + expect(type(p)):eq("table") + expect(peripheral.getName(p)):eq("top") + end) + + it_modem("finds a local peripheral", function() + local p = peripheral.find("remote") + expect(type(p)):eq("table") + expect(peripheral.getName(p)):eq("remote_1") + end) end) end) From b2d215325889bb866fe1cb4bda87b57bbb6314d5 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sat, 29 Oct 2022 22:49:45 +0100 Subject: [PATCH 6/7] Add several missing version annotations I probably need to add this to the pre-release checklist. Don't think there's a good way to automate this :( --- doc/events/file_transfer.md | 1 + src/main/resources/data/computercraft/lua/rom/apis/textutils.lua | 1 + .../data/computercraft/lua/rom/modules/main/cc/audio/dfpwm.lua | 1 + 3 files changed, 3 insertions(+) diff --git a/doc/events/file_transfer.md b/doc/events/file_transfer.md index f68289c34..4eac223fa 100644 --- a/doc/events/file_transfer.md +++ b/doc/events/file_transfer.md @@ -1,5 +1,6 @@ --- module: [kind=event] file_transfer +since: 1.101.0 --- The @{file_transfer} event is queued when a user drags-and-drops a file on an open computer. 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 4e59e1ad9..e88548fa2 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua @@ -688,6 +688,7 @@ do @treturn[2] nil If the object could not be deserialised. @treturn string A message describing why the JSON string is invalid. @since 1.87.0 + @changed 1.100.6 Added `parse_empty_array` option @see textutils.json_null Use to serialize a JSON `null` value. @see textutils.empty_json_array Use to serialize a JSON empty array. @usage Unserialise a basic JSON object diff --git a/src/main/resources/data/computercraft/lua/rom/modules/main/cc/audio/dfpwm.lua b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/audio/dfpwm.lua index 2b5e2d183..fd8fa2686 100644 --- a/src/main/resources/data/computercraft/lua/rom/modules/main/cc/audio/dfpwm.lua +++ b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/audio/dfpwm.lua @@ -29,6 +29,7 @@ application or development builds of [FFmpeg]. @see speaker.playAudio To play the decoded audio data. @usage Reads "data/example.dfpwm" in chunks, decodes them and then doubles the speed of the audio. The resulting audio is then re-encoded and saved to "speedy.dfpwm". This processed audio can then be played with the `speaker` program. +@since 1.100.0 ```lua local dfpwm = require("cc.audio.dfpwm") From 5ee5b119953a37a9ec64c4ed52e17a6aa9852c60 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sat, 29 Oct 2022 23:54:35 +0100 Subject: [PATCH 7/7] Fix month and day names under Java 17 I still don't really understand why the ROOT locale is wrong here, but there we go. We'll need to remember to uncomment the tests on the 1.18 branch! Also add some code to map tests back to their definition side. Alas, this only links to the file right now, not the correct line :/. --- .../dan200/computercraft/core/apis/OSAPI.java | 4 +- .../core/ComputerTestDelegate.java | 38 ++++++++++++------- src/test/resources/test-rom/mcfly.lua | 15 ++++---- 3 files changed, 35 insertions(+), 22 deletions(-) diff --git a/src/main/java/dan200/computercraft/core/apis/OSAPI.java b/src/main/java/dan200/computercraft/core/apis/OSAPI.java index 9b7b0c943..0ada26bb2 100644 --- a/src/main/java/dan200/computercraft/core/apis/OSAPI.java +++ b/src/main/java/dan200/computercraft/core/apis/OSAPI.java @@ -494,7 +494,9 @@ public class OSAPI implements ILuaAPI DateTimeFormatterBuilder formatter = new DateTimeFormatterBuilder(); LuaDateTime.format( formatter, format ); - return formatter.toFormatter( Locale.ROOT ).format( date ); + // ROOT would be more sensible, but US appears more consistent with the default C locale + // on Linux. + return formatter.toFormatter( Locale.US ).format( date ); } } diff --git a/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java b/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java index 095ede05f..b9991d028 100644 --- a/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java +++ b/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java @@ -28,8 +28,10 @@ import org.opentest4j.AssertionFailedError; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.io.BufferedWriter; +import java.io.File; import java.io.IOException; import java.io.Writer; +import java.net.URI; import java.nio.channels.Channels; import java.nio.channels.WritableByteChannel; import java.nio.charset.StandardCharsets; @@ -43,8 +45,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; -import static dan200.computercraft.api.lua.LuaValues.getType; - /** * Loads tests from {@code test-rom/spec} and executes them. *

@@ -195,36 +195,45 @@ public class ComputerTestDelegate private static class DynamicNodeBuilder { private final String name; + private final URI uri; private final Map children; private final Executable executor; - DynamicNodeBuilder( String name ) + DynamicNodeBuilder( String name, String path ) { this.name = name; + this.uri = getUri( path ); this.children = new HashMap<>(); this.executor = null; } - DynamicNodeBuilder( String name, Executable executor ) + DynamicNodeBuilder( String name, String path, Executable executor ) { this.name = name; + this.uri = getUri( path ); this.children = Collections.emptyMap(); this.executor = executor; } + private static URI getUri( String path ) + { + // Unfortunately ?line=xxx doesn't appear to work with IntelliJ, so don't worry about getting it working. + return path == null ? null : new File( "src/test/resources" + path.substring( 0, path.indexOf( ':' ) ) ).toURI(); + } + DynamicNodeBuilder get( String name ) { DynamicNodeBuilder child = children.get( name ); - if( child == null ) children.put( name, child = new DynamicNodeBuilder( name ) ); + if( child == null ) children.put( name, child = new DynamicNodeBuilder( name, null ) ); return child; } - void runs( String name, Executable executor ) + void runs( String name, String uri, Executable executor ) { if( this.executor != null ) throw new IllegalStateException( name + " is leaf node" ); if( children.containsKey( name ) ) throw new IllegalStateException( "Duplicate key for " + name ); - children.put( name, new DynamicNodeBuilder( name, executor ) ); + children.put( name, new DynamicNodeBuilder( name, uri, executor ) ); } boolean isActive() @@ -241,8 +250,8 @@ public class ComputerTestDelegate DynamicNode build() { return executor == null - ? DynamicContainer.dynamicContainer( name, buildChildren() ) - : DynamicTest.dynamicTest( name, executor ); + ? DynamicContainer.dynamicContainer( name, uri, buildChildren() ) + : DynamicTest.dynamicTest( name, uri, executor ); } Stream buildChildren() @@ -376,16 +385,17 @@ public class ComputerTestDelegate { // Submit several tests and signal for #get to run LOG.info( "Received tests from computer" ); - DynamicNodeBuilder root = new DynamicNodeBuilder( "" ); - for( Object key : tests.keySet() ) + DynamicNodeBuilder root = new DynamicNodeBuilder( "", null ); + for( Map.Entry entry : tests.entrySet() ) { - if( !(key instanceof String) ) throw new LuaException( "Non-key string " + getType( key ) ); + String name = (String) entry.getKey(); + Map details = (Map) entry.getValue(); + String def = (String) details.get( "definition" ); - String name = (String) key; String[] parts = name.split( "\0" ); DynamicNodeBuilder builder = root; for( int i = 0; i < parts.length - 1; i++ ) builder = builder.get( parts[i] ); - builder.runs( parts[parts.length - 1], () -> { + builder.runs( parts[parts.length - 1], def, () -> { // Run it lock.lockInterruptibly(); try diff --git a/src/test/resources/test-rom/mcfly.lua b/src/test/resources/test-rom/mcfly.lua index a5caec7a8..258a51236 100644 --- a/src/test/resources/test-rom/mcfly.lua +++ b/src/test/resources/test-rom/mcfly.lua @@ -424,6 +424,8 @@ local tests_locked = false local test_list = {} local test_map, test_count = {}, 0 +local function format_loc(info) return ("%s:%d"):format(info.short_src, info.currentline) end + --- Add a new test to our queue. -- -- @param test The descriptor of this test @@ -432,7 +434,7 @@ local function do_test(test) if not test.name then test.name = table.concat(test_stack, "\0", 1, test_stack.n) end test_count = test_count + 1 test_list[test_count] = test - test_map[test.name] = test_count + test_map[test.name] = { idx = test_count, definition = test.definition } end --- Get the "friendly" name of this test. @@ -456,7 +458,7 @@ local function describe(name, body) local ok, err = try(body) -- We count errors as a (failing) test. - if not ok then do_test { error = err } end + if not ok then do_test { error = err, definition = format_loc(debug.getinfo(2, "Sl")) } end test_stack.n = n - 1 end @@ -475,7 +477,7 @@ local function it(name, body) local n = test_stack.n + 1 test_stack[n], test_stack.n, tests_locked = name, n, true - do_test { action = body } + do_test { action = body, definition = format_loc(debug.getinfo(2, "Sl")) } -- Pop the test from the stack test_stack.n, tests_locked = n - 1, false @@ -488,12 +490,11 @@ local function pending(name) check('it', 1, 'string', name) if tests_locked then error("Cannot create test while running tests", 2) end - local _, loc = pcall(error, "", 3) - loc = loc:gsub(":%s*$", "") + local trace = format_loc(debug.getinfo(2, "Sl")) local n = test_stack.n + 1 test_stack[n], test_stack.n = name, n - do_test { pending = true, trace = loc } + do_test { pending = true, trace = trace, definition = trace } test_stack.n = n - 1 end @@ -667,7 +668,7 @@ if cct_test then while true do local _, name = os.pullEvent("cct_test_run") if not name then break end - do_run(test_list[test_map[name]]) + do_run(test_list[test_map[name].idx]) end else for _, test in pairs(test_list) do do_run(test) end