From 7f2471d6b2953b9ec0222b4637d1f439bb5c4f27 Mon Sep 17 00:00:00 2001 From: SquidDev Date: Wed, 8 May 2019 08:00:07 +0100 Subject: [PATCH 01/30] Fix list-like config options not reloading Closes #199 --- src/main/java/dan200/computercraft/shared/Config.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/dan200/computercraft/shared/Config.java b/src/main/java/dan200/computercraft/shared/Config.java index e1051a55e..76bbe6f43 100644 --- a/src/main/java/dan200/computercraft/shared/Config.java +++ b/src/main/java/dan200/computercraft/shared/Config.java @@ -402,7 +402,7 @@ public final class Config if( oldProperty.isList() ) { - oldProperty.setValues( oldProperty.getStringList() ); + oldProperty.setValues( newProperty.getStringList() ); } else { From 0ec3884e98f19fc6845332e19cfe4b521c486f5d Mon Sep 17 00:00:00 2001 From: SquidDev Date: Wed, 8 May 2019 08:09:56 +0100 Subject: [PATCH 02/30] Handle websockets which close while receiving This changes the previous behaviour a little, but hopefully is more sane: - Only require the socket to be open when first calling receive. This means if it closes while receving, you won't get an error. This behaviour is still not perfect - the socket could have closed, but the event not reached the user yet, but it's better. - Listen to websocket_close events while receiving, and return null should it match ours. See #201 --- .../core/apis/http/websocket/WebsocketHandle.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java b/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java index d5eaf37e7..f53a2bff1 100644 --- a/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java +++ b/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java @@ -24,6 +24,7 @@ import java.io.Closeable; import java.util.Arrays; import static dan200.computercraft.core.apis.ArgumentHelper.optBoolean; +import static dan200.computercraft.core.apis.http.websocket.Websocket.CLOSE_EVENT; import static dan200.computercraft.core.apis.http.websocket.Websocket.MESSAGE_EVENT; public class WebsocketHandle implements ILuaObject, Closeable @@ -53,15 +54,18 @@ public class WebsocketHandle implements ILuaObject, Closeable switch( method ) { case 0: // receive + checkOpen(); while( true ) { - checkOpen(); - - Object[] event = context.pullEvent( MESSAGE_EVENT ); - if( event.length >= 3 && Objects.equal( event[1], websocket.address() ) ) + Object[] event = context.pullEvent( null ); + if( event.length >= 3 && Objects.equal( event[0], MESSAGE_EVENT ) && Objects.equal( event[1], websocket.address() ) ) { return Arrays.copyOfRange( event, 2, event.length ); } + else if( event.length >= 2 && Objects.equal( event[0], CLOSE_EVENT ) && Objects.equal( event[1], websocket.address() ) && closed ) + { + return null; + } } case 1: // send From ad33acd7d1a59d104f0e76ad80d0e550ae449b11 Mon Sep 17 00:00:00 2001 From: "Wilma456 (Jakob0815)" Date: Mon, 13 May 2019 17:54:19 +0200 Subject: [PATCH 03/30] Add MOTD (#175) --- .../resources/assets/computercraft/lua/bios.lua | 2 ++ .../assets/computercraft/lua/rom/motd.txt | 4 ++++ .../computercraft/lua/rom/programs/motd.lua | 15 +++++++++++++++ .../assets/computercraft/lua/rom/startup.lua | 5 +++++ 4 files changed, 26 insertions(+) create mode 100644 src/main/resources/assets/computercraft/lua/rom/motd.txt create mode 100644 src/main/resources/assets/computercraft/lua/rom/programs/motd.lua diff --git a/src/main/resources/assets/computercraft/lua/bios.lua b/src/main/resources/assets/computercraft/lua/bios.lua index 5cbc2267a..84da535d6 100644 --- a/src/main/resources/assets/computercraft/lua/bios.lua +++ b/src/main/resources/assets/computercraft/lua/bios.lua @@ -982,6 +982,8 @@ settings.set( "edit.default_extension", "lua" ) settings.set( "paint.default_extension", "nfp" ) settings.set( "lua.autocomplete", true ) settings.set( "list.show_hidden", false ) +settings.set( "motd.enable", false ) +settings.set( "motd.path", "/rom/motd.txt:/motd.txt" ) if term.isColour() then settings.set( "bios.use_multishell", true ) end diff --git a/src/main/resources/assets/computercraft/lua/rom/motd.txt b/src/main/resources/assets/computercraft/lua/rom/motd.txt new file mode 100644 index 000000000..02d3e8ecc --- /dev/null +++ b/src/main/resources/assets/computercraft/lua/rom/motd.txt @@ -0,0 +1,4 @@ +View the source code at https://github.com/SquidDev-CC/CC-Tweaked +View the documentation at https://wiki.computercraft.cc +Visit the forum at https://forums.computercraft.cc +You can disable these messages by running "set motd.enable false" diff --git a/src/main/resources/assets/computercraft/lua/rom/programs/motd.lua b/src/main/resources/assets/computercraft/lua/rom/programs/motd.lua new file mode 100644 index 000000000..317938b31 --- /dev/null +++ b/src/main/resources/assets/computercraft/lua/rom/programs/motd.lua @@ -0,0 +1,15 @@ +local tMotd = {} + +for sPath in string.gmatch(settings.get( "motd.path" ), "[^:]+") do + if fs.exists(sPath) then + for sLine in io.lines(sPath) do + table.insert(tMotd,sLine) + end + end +end + +if #tMotd == 0 then + print("missingno") +else + print(tMotd[math.random(1,#tMotd)]) +end diff --git a/src/main/resources/assets/computercraft/lua/rom/startup.lua b/src/main/resources/assets/computercraft/lua/rom/startup.lua index f29297791..9e508a952 100644 --- a/src/main/resources/assets/computercraft/lua/rom/startup.lua +++ b/src/main/resources/assets/computercraft/lua/rom/startup.lua @@ -269,6 +269,11 @@ local function findStartups( sBaseDir ) return tStartups end +-- Show MOTD +if settings.get( "motd.enable" ) then + shell.run( "motd" ) +end + -- Run the user created startup, either from disk drives or the root local tUserStartups = nil if settings.get( "shell.allow_startup" ) then From 3cdb12d293a805d8862f20b4f78d56534123e51b Mon Sep 17 00:00:00 2001 From: SquidDev Date: Tue, 14 May 2019 07:44:19 +0100 Subject: [PATCH 04/30] Stop shipping jars with GH releases The build appears to be a little bit broken, I haven't got willpower to look into it, and it's not really used anyway. --- build.gradle | 2 -- 1 file changed, 2 deletions(-) diff --git a/build.gradle b/build.gradle index 104c84dc4..aba913b74 100644 --- a/build.gradle +++ b/build.gradle @@ -302,8 +302,6 @@ githubRelease { releaseName "[${mc_version}] ${mod_version}" body '' prerelease false - - releaseAssets.from(jar.archivePath) } task uploadAll(dependsOn: [uploadArchives, "curseforge", "githubRelease"]) { From 68bf3a71dc1a68933057ae4d0b9cdaff47577a64 Mon Sep 17 00:00:00 2001 From: SquidDev Date: Fri, 24 May 2019 18:32:56 +0100 Subject: [PATCH 05/30] Lazilly instantiate the terminal packet This means we don't create an NBT tag every tick if the screen is updating, unless we actually need to. --- .../computercraft/shared/computer/core/ServerComputer.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java b/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java index 49060ef92..f5799af28 100644 --- a/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java +++ b/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java @@ -175,12 +175,13 @@ public class ServerComputer extends ServerTerminal implements IComputer, IComput FMLCommonHandler handler = FMLCommonHandler.instance(); if( handler != null ) { - IMessage packet = createTerminalPacket(); + IMessage packet = null; MinecraftServer server = handler.getMinecraftServerInstance(); for( EntityPlayerMP player : server.getPlayerList().getPlayers() ) { if( isInteracting( player ) ) { + if( packet == null ) packet = createTerminalPacket(); NetworkHandler.sendToPlayer( player, packet ); } } From d661cfa88b89320221a8077b0c0d6fa9496935e8 Mon Sep 17 00:00:00 2001 From: SquidDev Date: Fri, 24 May 2019 18:42:19 +0100 Subject: [PATCH 06/30] Set the arg table within shell.run This makes us a little more compatible with Lua --- .../computercraft/lua/rom/programs/shell.lua | 6 +++++- .../test-rom/spec/programs/shell_spec.lua | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 src/test/resources/test-rom/spec/programs/shell_spec.lua diff --git a/src/main/resources/assets/computercraft/lua/rom/programs/shell.lua b/src/main/resources/assets/computercraft/lua/rom/programs/shell.lua index 8be9cbe06..ef071be64 100644 --- a/src/main/resources/assets/computercraft/lua/rom/programs/shell.lua +++ b/src/main/resources/assets/computercraft/lua/rom/programs/shell.lua @@ -130,8 +130,12 @@ local function run( _sCommand, ... ) end multishell.setTitle( multishell.getCurrent(), sTitle ) end + local sDir = fs.getDir( sPath ) - local result = os.run( createShellEnv( sDir ), sPath, ... ) + local env = createShellEnv( sDir ) + env[ "arg" ] = { [0] = _sCommand, ... } + local result = os.run( env, sPath, ... ) + tProgramStack[#tProgramStack] = nil if multishell then if #tProgramStack > 0 then diff --git a/src/test/resources/test-rom/spec/programs/shell_spec.lua b/src/test/resources/test-rom/spec/programs/shell_spec.lua new file mode 100644 index 000000000..9d0a7d237 --- /dev/null +++ b/src/test/resources/test-rom/spec/programs/shell_spec.lua @@ -0,0 +1,17 @@ +describe("The shell", function() + describe("shell.run", function() + it("sets the arguments", function() + local handle = fs.open("test-files/out.txt", "w") + handle.writeLine("_G.__arg = arg") + handle.close() + + shell.run("/test-files/out.txt", "arg1", "arg2") + fs.delete("test-files/out.txt") + + local args = _G.__arg + _G.__arg = nil + + expect(args):same { [0] = "/test-files/out.txt", "arg1", "arg2" } + end) + end) +end) From 210f3fa9e2f942483165d83444075a8ea9fc60c8 Mon Sep 17 00:00:00 2001 From: SquidDev Date: Fri, 24 May 2019 22:33:56 +0100 Subject: [PATCH 07/30] Add mostly standard compliant os.time/os.date - os.time, when given a table, will act the same as PUC Lua - returning the seconds since the epoch. We preserve the previous string/nil behaviour though - os.epoch("local") is equivalent to PUC's os.time(). - os.date will now act accept a string and (optional) time, returning an appropriate table. Somewhat resolves the madness which was dan200/ComputerCraft#183, and hopefully (though probably not) makes @Vexatos happy. --- .../computercraft/core/apis/LuaDateTime.java | 281 ++++++++++++++++++ .../dan200/computercraft/core/apis/OSAPI.java | 48 ++- .../resources/test-rom/spec/apis/os_spec.lua | 121 ++++++++ 3 files changed, 444 insertions(+), 6 deletions(-) create mode 100644 src/main/java/dan200/computercraft/core/apis/LuaDateTime.java create mode 100644 src/test/resources/test-rom/spec/apis/os_spec.lua diff --git a/src/main/java/dan200/computercraft/core/apis/LuaDateTime.java b/src/main/java/dan200/computercraft/core/apis/LuaDateTime.java new file mode 100644 index 000000000..b4f9d41ee --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/LuaDateTime.java @@ -0,0 +1,281 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2019. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.core.apis; + +import dan200.computercraft.api.lua.LuaException; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatterBuilder; +import java.time.format.TextStyle; +import java.time.temporal.*; +import java.util.HashMap; +import java.util.Map; +import java.util.function.LongUnaryOperator; + +final class LuaDateTime +{ + private LuaDateTime() + { + } + + static void format( DateTimeFormatterBuilder formatter, String format, ZoneOffset offset ) throws LuaException + { + for( int i = 0; i < format.length(); ) + { + char c; + switch( c = format.charAt( i++ ) ) + { + case '\n': + formatter.appendLiteral( '\n' ); + break; + default: + formatter.appendLiteral( c ); + break; + case '%': + if( i >= format.length() ) break; + switch( c = format.charAt( i++ ) ) + { + default: + throw new LuaException( "bad argument #1: invalid conversion specifier '%" + (char) c + "'" ); + + case '%': + formatter.appendLiteral( '%' ); + break; + case 'a': + formatter.appendText( ChronoField.DAY_OF_WEEK, TextStyle.SHORT ); + break; + case 'A': + formatter.appendText( ChronoField.DAY_OF_WEEK, TextStyle.FULL ); + break; + case 'b': + case 'h': + formatter.appendText( ChronoField.MONTH_OF_YEAR, TextStyle.SHORT ); + break; + case 'B': + formatter.appendText( ChronoField.MONTH_OF_YEAR, TextStyle.FULL ); + break; + case 'c': + format( formatter, "%a %b %e %H:%M:%S %Y", offset ); + break; + case 'C': + formatter.appendValueReduced( CENTURY, 2, 2, 0 ); + break; + case 'd': + formatter.appendValue( ChronoField.DAY_OF_MONTH, 2 ); + break; + case 'D': + case 'x': + format( formatter, "%m/%d/%y", offset ); + break; + case 'e': + formatter.padNext( 2 ).appendValue( ChronoField.DAY_OF_MONTH ); + break; + case 'F': + format( formatter, "%Y-%m-%d", offset ); + break; + case 'g': + formatter.appendValueReduced( IsoFields.WEEK_BASED_YEAR, 2, 2, 0 ); + break; + case 'G': + formatter.appendValue( IsoFields.WEEK_BASED_YEAR ); + break; + case 'H': + formatter.appendValue( ChronoField.HOUR_OF_DAY, 2 ); + break; + case 'I': + formatter.appendValue( ChronoField.HOUR_OF_AMPM ); + break; + case 'j': + formatter.appendValue( ChronoField.DAY_OF_YEAR, 3 ); + break; + case 'm': + formatter.appendValue( ChronoField.MONTH_OF_YEAR, 2 ); + break; + case 'M': + formatter.appendValue( ChronoField.MINUTE_OF_HOUR, 2 ); + break; + case 'n': + formatter.appendLiteral( '\n' ); + break; + case 'p': + formatter.appendText( ChronoField.AMPM_OF_DAY ); + break; + case 'r': + format( formatter, "%I:%M:%S %p", offset ); + break; + case 'R': + format( formatter, "%H:%M", offset ); + break; + case 'S': + formatter.appendValue( ChronoField.SECOND_OF_MINUTE, 2 ); + break; + case 't': + formatter.appendLiteral( '\t' ); + break; + case 'T': + case 'X': + format( formatter, "%H:%M:%S", offset ); + break; + case 'u': + formatter.appendValue( ChronoField.DAY_OF_WEEK ); + break; + case 'U': + formatter.appendValue( ChronoField.ALIGNED_WEEK_OF_YEAR, 2 ); + break; + case 'V': + formatter.appendValue( IsoFields.WEEK_OF_WEEK_BASED_YEAR, 2 ); + break; + case 'w': + formatter.appendValue( ZERO_WEEK ); + break; + case 'W': + formatter.appendValue( WeekFields.ISO.weekOfYear(), 2 ); + break; + case 'y': + formatter.appendValueReduced( ChronoField.YEAR, 2, 2, 0 ); + break; + case 'Y': + formatter.appendValue( ChronoField.YEAR ); + break; + case 'z': + formatter.appendOffset( "+HHMM", "+0000" ); + break; + case 'Z': + formatter.appendChronologyId(); + break; + } + } + } + } + + static long fromTable( Map table ) throws LuaException + { + int year = getField( table, "year", -1 ); + int month = getField( table, "month", -1 ); + int day = getField( table, "day", -1 ); + int hour = getField( table, "hour", 12 ); + int minute = getField( table, "min", 12 ); + int second = getField( table, "sec", 12 ); + LocalDateTime time = LocalDateTime.of( year, month, day, hour, minute, second ); + + Boolean isDst = getBoolField( table, "isdst" ); + if( isDst != null ) + { + boolean requireDst = isDst; + for( ZoneOffset possibleOffset : ZoneOffset.systemDefault().getRules().getValidOffsets( time ) ) + { + Instant instant = time.toInstant( possibleOffset ); + if( possibleOffset.getRules().getDaylightSavings( instant ).isZero() == requireDst ) + { + return instant.getEpochSecond(); + } + } + } + + ZoneOffset offset = ZoneOffset.systemDefault().getRules().getOffset( time ); + return time.toInstant( offset ).getEpochSecond(); + } + + static Map toTable( TemporalAccessor date, ZoneId offset, Instant instant ) + { + HashMap table = new HashMap<>( 9 ); + table.put( "year", date.getLong( ChronoField.YEAR ) ); + table.put( "month", date.getLong( ChronoField.MONTH_OF_YEAR ) ); + table.put( "day", date.getLong( ChronoField.DAY_OF_MONTH ) ); + table.put( "hour", date.getLong( ChronoField.HOUR_OF_DAY ) ); + table.put( "min", date.getLong( ChronoField.MINUTE_OF_HOUR ) ); + table.put( "sec", date.getLong( ChronoField.SECOND_OF_MINUTE ) ); + table.put( "wday", date.getLong( WeekFields.SUNDAY_START.dayOfWeek() ) ); + table.put( "yday", date.getLong( ChronoField.DAY_OF_YEAR ) ); + table.put( "isdst", offset.getRules().isDaylightSavings( instant ) ); + return table; + } + + private static int getField( Map table, String field, int def ) throws LuaException + { + Object value = table.get( field ); + if( value instanceof Number ) return ((Number) value).intValue(); + if( def < 0 ) throw new LuaException( "field \"" + field + "\" missing in date table" ); + return def; + } + + private static Boolean getBoolField( Map table, String field ) throws LuaException + { + Object value = table.get( field ); + if( value instanceof Boolean || value == null ) return (Boolean) value; + throw new LuaException( "field \"" + field + "\" missing in date table" ); + } + + private static final TemporalField CENTURY = map( ChronoField.YEAR, ValueRange.of( 0, 6 ), x -> (x / 100) % 100 ); + private static final TemporalField ZERO_WEEK = map( WeekFields.SUNDAY_START.dayOfWeek(), ValueRange.of( 0, 6 ), x -> x - 1 ); + + private static TemporalField map( TemporalField field, ValueRange range, LongUnaryOperator convert ) + { + return new TemporalField() + { + private final ValueRange range = ValueRange.of( 0, 99 ); + + @Override + public TemporalUnit getBaseUnit() + { + return field.getBaseUnit(); + } + + @Override + public TemporalUnit getRangeUnit() + { + return field.getRangeUnit(); + } + + @Override + public ValueRange range() + { + return range; + } + + @Override + public boolean isDateBased() + { + return field.isDateBased(); + } + + @Override + public boolean isTimeBased() + { + return field.isTimeBased(); + } + + @Override + public boolean isSupportedBy( TemporalAccessor temporal ) + { + return field.isSupportedBy( temporal ); + } + + @Override + public ValueRange rangeRefinedBy( TemporalAccessor temporal ) + { + return range; + } + + @Override + public long getFrom( TemporalAccessor temporal ) + { + return convert.applyAsLong( temporal.getLong( field ) ); + } + + @Override + @SuppressWarnings( "unchecked" ) + public R adjustInto( R temporal, long newValue ) + { + return (R) temporal.with( field, newValue ); + } + }; + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/OSAPI.java b/src/main/java/dan200/computercraft/core/apis/OSAPI.java index c00281e55..fbbfe5e0f 100644 --- a/src/main/java/dan200/computercraft/core/apis/OSAPI.java +++ b/src/main/java/dan200/computercraft/core/apis/OSAPI.java @@ -12,6 +12,11 @@ import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.shared.util.StringUtil; import javax.annotation.Nonnull; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatterBuilder; import java.util.*; import static dan200.computercraft.core.apis.ArgumentHelper.*; @@ -184,11 +189,12 @@ public class OSAPI implements ILuaAPI "day", "cancelTimer", "cancelAlarm", - "epoch" + "epoch", + "date", }; } - private float getTimeForCalendar( Calendar c ) + private static float getTimeForCalendar( Calendar c ) { float time = c.get( Calendar.HOUR_OF_DAY ); time += c.get( Calendar.MINUTE ) / 60.0f; @@ -196,7 +202,7 @@ public class OSAPI implements ILuaAPI return time; } - private int getDayForCalendar( Calendar c ) + private static int getDayForCalendar( Calendar c ) { GregorianCalendar g = c instanceof GregorianCalendar ? (GregorianCalendar) c : new GregorianCalendar(); int year = c.get( Calendar.YEAR ); @@ -209,7 +215,7 @@ public class OSAPI implements ILuaAPI return day; } - private long getEpochForCalendar( Calendar c ) + private static long getEpochForCalendar( Calendar c ) { return c.getTime().getTime(); } @@ -282,6 +288,9 @@ public class OSAPI implements ILuaAPI case 11: { // time + Object value = args.length > 0 ? args[0] : null; + if( value instanceof Map ) return new Object[] { LuaDateTime.fromTable( (Map) value ) }; + String param = optString( args, 0, "ingame" ); switch( param.toLowerCase( Locale.ROOT ) ) { @@ -355,9 +364,8 @@ public class OSAPI implements ILuaAPI } return null; } - case 15: + case 15: // epoch { - // epoch String param = optString( args, 0, "ingame" ); switch( param.toLowerCase( Locale.ROOT ) ) { @@ -385,6 +393,34 @@ public class OSAPI implements ILuaAPI throw new LuaException( "Unsupported operation" ); } } + case 16: // date + { + String format = optString( args, 0, "%c" ); + long time = optLong( args, 1, Instant.now().getEpochSecond() ); + + Instant instant = Instant.ofEpochSecond( time ); + ZonedDateTime date; + ZoneOffset offset; + boolean isDst; + if( format.startsWith( "!" ) ) + { + offset = ZoneOffset.UTC; + date = ZonedDateTime.ofInstant( instant, offset ); + format = format.substring( 1 ); + } + else + { + ZoneId id = ZoneId.systemDefault(); + offset = id.getRules().getOffset( instant ); + date = ZonedDateTime.ofInstant( instant, id ); + } + + if( format.equals( "*t" ) ) return new Object[] { LuaDateTime.toTable( date, offset, instant ) }; + + DateTimeFormatterBuilder formatter = new DateTimeFormatterBuilder(); + LuaDateTime.format( formatter, format, offset ); + return new Object[] { formatter.toFormatter( Locale.ROOT ).format( date ) }; + } default: return null; } diff --git a/src/test/resources/test-rom/spec/apis/os_spec.lua b/src/test/resources/test-rom/spec/apis/os_spec.lua new file mode 100644 index 000000000..5c4515a46 --- /dev/null +++ b/src/test/resources/test-rom/spec/apis/os_spec.lua @@ -0,0 +1,121 @@ +describe("The os api", function() + describe("os.date and os.time", function() + it("round trips correctly", function() + local t = math.floor(os.epoch("local") / 1000) + local T = os.date("*t", t) + + expect(os.time(T)):eq(t) + end) + + it("dst field is guessed", function() + local T = os.date("*t") + local t = os.time(T) + expect(T.isdst):type("boolean") + T.isdst = nil + expect(os.time(T)):eq(t) -- if isdst is absent uses correct default + end) + + it("has 365 days in a year", function() + local T = os.date("*t") + local t = os.time(T) + T.year = T.year - 1 + local t1 = os.time(T) + local delta = (t - t1) / (24 * 3600) - 365 + -- allow for leap years + assert(math.abs(delta) < 2, ("expected abs(%d )< 2"):format(delta)) + end) + + it("os.date uses local timezone", function() + local epoch = os.epoch("local") / 1000 + local date = os.time(os.date("*t")) + assert(date - epoch <= 2, ("expected %d - %d <= 2, but not the case"):format(date, epoch)) + end) + end) + + describe("os.date", function() + it("formats as expected", function() + -- From the PUC Lua tests, hence the weird style + local t = os.epoch("local") + local T = os.date("*t", t) + + _G.T = T + loadstring(os.date([[assert(T.year==%Y and T.month==%m and T.day==%d and + T.hour==%H and T.min==%M and T.sec==%S and + T.wday==%w+1 and T.yday==%j and type(T.isdst) == 'boolean')]], t))() + + T = os.date("!*t", t) + _G.T = T + loadstring(os.date([[!assert(T.year==%Y and T.month==%m and T.day==%d and + T.hour==%H and T.min==%M and T.sec==%S and + T.wday==%w+1 and T.yday==%j and type(T.isdst) == 'boolean')]], t))() + end) + + describe("produces output consistent with PUC Lua", function() + -- Create a separate test for each code, just so it's easier to see what's broken + local t1 = os.time { year = 2000, month = 10, day = 1, hour = 23, min = 12, sec = 17 } + local function exp_code(code, value) + it(("for code '%s'"):format(code), function() + expect(os.date(code, t1)):eq(value) + end) + end + + exp_code("%a", "Sun") + exp_code("%A", "Sunday") + exp_code("%b", "Oct") + exp_code("%B", "October") + exp_code("%c", "Sun Oct 1 23:12:17 2000") + exp_code("%C", "20") + exp_code("%d", "01") + exp_code("%D", "10/01/00") + exp_code("%e", " 1") + exp_code("%F", "2000-10-01") + exp_code("%g", "00") + exp_code("%G", "2000") + exp_code("%h", "Oct") + exp_code("%H", "23") + exp_code("%I", "11") + exp_code("%j", "275") + exp_code("%m", "10") + exp_code("%M", "12") + exp_code("%n", "\n") + exp_code("%p", "PM") + exp_code("%r", "11:12:17 PM") + exp_code("%R", "23:12") + exp_code("%S", "17") + exp_code("%t", "\t") + exp_code("%T", "23:12:17") + exp_code("%u", "7") + exp_code("%U", "40") + exp_code("%V", "39") + exp_code("%w", "0") + exp_code("%W", "39") + exp_code("%x", "10/01/00") + exp_code("%X", "23:12:17") + exp_code("%y", "00") + exp_code("%Y", "2000") + exp_code("%%", "%") + + it("zones are numbers", function() + local zone = os.date("%z", t1) + if not zone:match("^[+-]%d%d%d%d$") then + error("Invalid zone: " .. zone) + end + end) + + it("zones id is made of letters", function() + local zone = os.date("%Z", t1) + if not zone:match("^%a%a+$") then + error("Non letter character in zone: " .. zone) + end + end) + end) + end) + + describe("os.time", function() + it("maps directly to seconds", function() + local t1 = os.time { year = 2000, month = 10, day = 1, hour = 23, min = 12, sec = 17 } + local t2 = os.time { year = 2000, month = 10, day = 1, hour = 23, min = 10, sec = 19 } + expect(t1 - t2):eq(60 * 2 - 2) + end) + end) +end) From d5ea22d1a0432eec1a2dc501cd63a32879f98e89 Mon Sep 17 00:00:00 2001 From: SquidDev Date: Fri, 24 May 2019 22:43:59 +0100 Subject: [PATCH 08/30] Jolly good job --- src/main/java/dan200/computercraft/core/apis/LuaDateTime.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/dan200/computercraft/core/apis/LuaDateTime.java b/src/main/java/dan200/computercraft/core/apis/LuaDateTime.java index b4f9d41ee..516f89f36 100644 --- a/src/main/java/dan200/computercraft/core/apis/LuaDateTime.java +++ b/src/main/java/dan200/computercraft/core/apis/LuaDateTime.java @@ -43,7 +43,7 @@ final class LuaDateTime switch( c = format.charAt( i++ ) ) { default: - throw new LuaException( "bad argument #1: invalid conversion specifier '%" + (char) c + "'" ); + throw new LuaException( "bad argument #1: invalid conversion specifier '%" + c + "'" ); case '%': formatter.appendLiteral( '%' ); From 5b0ce7410d72d30a639cce90b017b9b1e6ceebcb Mon Sep 17 00:00:00 2001 From: hydraz Date: Sat, 25 May 2019 05:02:42 -0300 Subject: [PATCH 09/30] Make delete delete many files (#210) Actually, many *globs*. It additionally prints the glob if no files matched it, since that's clearer. Also move the ComputerTestDelegate's filesystem to be disk-based. This is what actual computers use, and the MemoryMount is a little broken. --- .../computercraft/lua/rom/programs/delete.lua | 21 +++++------ .../assets/computercraft/lua/rom/startup.lua | 5 ++- .../core/ComputerTestDelegate.java | 29 +++++++++++---- .../core/filesystem/MemoryMount.java | 2 +- .../test-rom/spec/programs/delete_spec.lua | 35 +++++++++++++++++++ 5 files changed, 73 insertions(+), 19 deletions(-) create mode 100644 src/test/resources/test-rom/spec/programs/delete_spec.lua diff --git a/src/main/resources/assets/computercraft/lua/rom/programs/delete.lua b/src/main/resources/assets/computercraft/lua/rom/programs/delete.lua index 388491e00..51e8d288c 100644 --- a/src/main/resources/assets/computercraft/lua/rom/programs/delete.lua +++ b/src/main/resources/assets/computercraft/lua/rom/programs/delete.lua @@ -1,16 +1,17 @@ +local args = table.pack(...) -local tArgs = { ... } -if #tArgs < 1 then - print( "Usage: rm " ) +if args.n < 1 then + print("Usage: rm ") return end -local sPath = shell.resolve( tArgs[1] ) -local tFiles = fs.find( sPath ) -if #tFiles > 0 then - for n,sFile in ipairs( tFiles ) do - fs.delete( sFile ) +for i = 1, args.n do + local files = fs.find(shell.resolve(args[i])) + if #files > 0 then + for n, file in ipairs(files) do + fs.delete(file) + end + else + printError(args[i] .. ": No matching files") end -else - printError( "No matching files" ) end diff --git a/src/main/resources/assets/computercraft/lua/rom/startup.lua b/src/main/resources/assets/computercraft/lua/rom/startup.lua index 9e508a952..37e7f0e10 100644 --- a/src/main/resources/assets/computercraft/lua/rom/startup.lua +++ b/src/main/resources/assets/computercraft/lua/rom/startup.lua @@ -76,6 +76,9 @@ local function completeEither( shell, nIndex, sText, tPreviousText ) return fs.complete( sText, shell.dir(), true, true ) end end +local function completeEitherMany( shell, nIndex, sText, tPreviousText ) + return fs.complete( sText, shell.dir(), true, true ) +end local function completeEitherEither( shell, nIndex, sText, tPreviousText ) if nIndex == 1 then local tResults = fs.complete( sText, shell.dir(), true, true ) @@ -180,7 +183,7 @@ end shell.setCompletionFunction( "rom/programs/alias.lua", completeAlias ) shell.setCompletionFunction( "rom/programs/cd.lua", completeDir ) shell.setCompletionFunction( "rom/programs/copy.lua", completeEitherEither ) -shell.setCompletionFunction( "rom/programs/delete.lua", completeEither ) +shell.setCompletionFunction( "rom/programs/delete.lua", completeEitherMany ) shell.setCompletionFunction( "rom/programs/drive.lua", completeDir ) shell.setCompletionFunction( "rom/programs/edit.lua", completeFile ) shell.setCompletionFunction( "rom/programs/eject.lua", completePeripheral ) diff --git a/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java b/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java index 5b6828a3e..73d34df3e 100644 --- a/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java +++ b/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java @@ -14,8 +14,8 @@ import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.core.computer.BasicEnvironment; import dan200.computercraft.core.computer.Computer; import dan200.computercraft.core.computer.MainThread; +import dan200.computercraft.core.filesystem.FileMount; import dan200.computercraft.core.filesystem.FileSystemException; -import dan200.computercraft.core.filesystem.MemoryMount; import dan200.computercraft.core.terminal.Terminal; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -25,9 +25,13 @@ import org.opentest4j.AssertionFailedError; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; +import java.io.File; +import java.io.IOException; +import java.io.Writer; +import java.nio.channels.Channels; +import java.nio.channels.WritableByteChannel; +import java.nio.charset.StandardCharsets; +import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; @@ -70,14 +74,25 @@ public class ComputerTestDelegate private boolean finished = false; @BeforeEach - public void before() + public void before() throws IOException { ComputerCraft.logPeripheralErrors = true; ComputerCraft.log = LogManager.getLogger( ComputerCraft.MOD_ID ); Terminal term = new Terminal( 78, 20 ); - IWritableMount mount = new MemoryMount() - .addFile( "startup.lua", "loadfile('test/mcfly.lua', _ENV)('test/spec') cct_test.finish()" ); + IWritableMount mount = new FileMount( new File( "test-files/mount" ), Long.MAX_VALUE ); + + // Remove any existing files + List children = new ArrayList<>(); + mount.list( "", children ); + for( String child : children ) mount.delete( child ); + + // And add our startup file + try( WritableByteChannel channel = mount.openChannelForWrite( "startup.lua" ); + Writer writer = Channels.newWriter( channel, StandardCharsets.UTF_8.newEncoder(), -1 ) ) + { + writer.write( "loadfile('test/mcfly.lua', _ENV)('test/spec') cct_test.finish()" ); + } computer = new Computer( new BasicEnvironment( mount ), term, 0 ); computer.addApi( new ILuaAPI() diff --git a/src/test/java/dan200/computercraft/core/filesystem/MemoryMount.java b/src/test/java/dan200/computercraft/core/filesystem/MemoryMount.java index da4e733c3..60070a4d0 100644 --- a/src/test/java/dan200/computercraft/core/filesystem/MemoryMount.java +++ b/src/test/java/dan200/computercraft/core/filesystem/MemoryMount.java @@ -120,7 +120,7 @@ public class MemoryMount implements IWritableMount { for( String file : this.files.keySet() ) { - if( file.startsWith( path ) ) files.add( file ); + if( file.startsWith( path ) ) files.add( file.substring( path.length() + 1 ) ); } } diff --git a/src/test/resources/test-rom/spec/programs/delete_spec.lua b/src/test/resources/test-rom/spec/programs/delete_spec.lua new file mode 100644 index 000000000..bf2433467 --- /dev/null +++ b/src/test/resources/test-rom/spec/programs/delete_spec.lua @@ -0,0 +1,35 @@ +describe("The rm program", function() + local function touch(file) + io.open(file, "w"):close() + end + + it("deletes one file", function() + touch("/test-files/a.txt") + + shell.run("rm /test-files/a.txt") + + expect(fs.exists("/test-files/a.txt")):eq(false) + end) + + it("deletes many files", function() + touch("/test-files/a.txt") + touch("/test-files/b.txt") + touch("/test-files/c.txt") + + shell.run("rm /test-files/a.txt /test-files/b.txt") + + expect(fs.exists("/test-files/a.txt")):eq(false) + expect(fs.exists("/test-files/b.txt")):eq(false) + expect(fs.exists("/test-files/c.txt")):eq(true) + end) + + it("deletes a glob", function() + touch("/test-files/a.txt") + touch("/test-files/b.txt") + + shell.run("rm /test-files/*.txt") + + expect(fs.exists("/test-files/a.txt")):eq(false) + expect(fs.exists("/test-files/b.txt")):eq(false) + end) +end) From 99bdff0f92e4daca66c15cc421a9d1d91ecd550c Mon Sep 17 00:00:00 2001 From: SquidDev Date: Sat, 25 May 2019 14:53:36 +0100 Subject: [PATCH 10/30] Automatically label issues created with templates --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 590575c1c..064fbc389 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,7 +1,7 @@ --- name: Bug report about: Report some misbehaviour in the mod - +labels: bug ---