Cherry pick several changes back from 1.19.3

The main purpose of this is to backport the improved parse/runtime
errors to older versions. I think they're sufficiently useful that we
should try to make it as widely available as possible.

We've been running them for a week now on SC3 and the released version
and not seen any issues, so I think it's probably stable enough.

This is a pretty lazy commit: I ended up copying the whole ROM over and
then picking up a few other related changes along the way.

 - Trim spaces from file paths (b8fce1eecc)

 - Correctly format 12AM/PM with
   %I (9f48395596)

 - Fix http.request and htpt.websocketAsync not handling a few failure
   edge-cases correctly (3b42f22a4f).

 - Move the internal modules into the main package path, hidden under
   cc.internal (34a31abd9c).

 - Gather code coverage in Java instead of
   Lua (28a55349a9).

 - Make error messages in edit more
   obvious (8cfbfe7ceb).

 - Make mcfly's test methods global. This means we don't need to pass
   stub everywhere (7335a892b5).

 - Improve runtime and parse errors. This comes from numerous commits,
   but chiefly a12b405acf, and
   5502412181.

 - Hide the internal redirect methods in
   multishell (33b6f38339).

Note this does /not/ include the shebang changes (sorry Emma!). I've
tried to avoid adding any user-controllable features, mostly because I
don't know how to handle the versioning otherwise :).
This commit is contained in:
Jonathan Coates 2023-02-14 09:45:02 +00:00
parent 68f6fa9343
commit 9b3cadf57c
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
95 changed files with 5447 additions and 942 deletions

View File

@ -1,64 +0,0 @@
--- @module fs
--- Returns true if a path is mounted to the parent filesystem.
--
-- The root filesystem "/" is considered a mount, along with disk folders and
-- the rom folder. Other programs (such as network shares) can exstend this to
-- make other mount types by correctly assigning their return value for getDrive.
--
-- @tparam string path The path to check.
-- @treturn boolean If the path is mounted, rather than a normal file/folder.
-- @throws If the path does not exist.
-- @see getDrive
-- @since 1.87.0
function isDriveRoot(path) end
--[[- Provides completion for a file or directory name, suitable for use with
@{_G.read}.
When a directory is a possible candidate for completion, two entries are
included - one with a trailing slash (indicating that entries within this
directory exist) and one without it (meaning this entry is an immediate
completion candidate). `include_dirs` can be set to @{false} to only include
those with a trailing slash.
@tparam[1] string path The path to complete.
@tparam[1] string location The location where paths are resolved from.
@tparam[1,opt=true] boolean include_files When @{false}, only directories will
be included in the returned list.
@tparam[1,opt=true] boolean include_dirs When @{false}, "raw" directories will
not be included in the returned list.
@tparam[2] string path The path to complete.
@tparam[2] string location The location where paths are resolved from.
@tparam[2] {
include_dirs? = boolean, include_files? = boolean,
include_hidden? = boolean
} options
This table form is an expanded version of the previous syntax. The
`include_files` and `include_dirs` arguments from above are passed in as fields.
This table also accepts the following options:
- `include_hidden`: Whether to include hidden files (those starting with `.`)
by default. They will still be shown when typing a `.`.
@treturn { string... } A list of possible completion candidates.
@since 1.74
@changed 1.101.0
@usage Complete files in the root directory.
read(nil, nil, function(str)
return fs.complete(str, "", true, false)
end)
@usage Complete files in the root directory, hiding hidden files by default.
read(nil, nil, function(str)
return fs.complete(str, "", {
include_files = true,
include_dirs = false,
included_hidden = false,
})
end)
]]
function complete(path, location, include_files, include_dirs) end

View File

@ -1,177 +0,0 @@
--- Make HTTP requests, sending and receiving data to a remote web server.
--
-- @module http
-- @since 1.1
-- @see local_ips To allow accessing servers running on your local network.
--- Asynchronously make a HTTP request to the given url.
--
-- This returns immediately, a @{http_success} or @{http_failure} will be queued
-- once the request has completed.
--
-- @tparam string url The url to request
-- @tparam[opt] string body An optional string containing the body of the
-- request. If specified, a `POST` request will be made instead.
-- @tparam[opt] { [string] = string } headers Additional headers to send as part
-- of this request.
-- @tparam[opt] boolean binary Whether to make a binary HTTP request. If true,
-- the body will not be UTF-8 encoded, and the received response will not be
-- decoded.
--
-- @tparam[2] {
-- url = string, body? = string, headers? = { [string] = string },
-- binary? = boolean, method? = string, redirect? = boolean,
-- } request Options for the request.
--
-- This table form is an expanded version of the previous syntax. All arguments
-- from above are passed in as fields instead (for instance,
-- `http.request("https://example.com")` becomes `http.request { url =
-- "https://example.com" }`).
--
-- This table also accepts several additional options:
--
-- - `method`: Which HTTP method to use, for instance `"PATCH"` or `"DELETE"`.
-- - `redirect`: Whether to follow HTTP redirects. Defaults to true.
--
-- @see http.get For a synchronous way to make GET requests.
-- @see http.post For a synchronous way to make POST requests.
--
-- @changed 1.63 Added argument for headers.
-- @changed 1.80pr1 Added argument for binary handles.
-- @changed 1.80pr1.6 Added support for table argument.
-- @changed 1.86.0 Added PATCH and TRACE methods.
function request(...) end
--- Make a HTTP GET request to the given url.
--
-- @tparam string url The url to request
-- @tparam[opt] { [string] = string } headers Additional headers to send as part
-- of this request.
-- @tparam[opt] boolean binary Whether to make a binary HTTP request. If true,
-- the body will not be UTF-8 encoded, and the received response will not be
-- decoded.
--
-- @tparam[2] {
-- url = string, headers? = { [string] = string },
-- binary? = boolean, method? = string, redirect? = boolean,
-- } request Options for the request. See @{http.request} for details on how
-- these options behave.
--
-- @treturn Response The resulting http response, which can be read from.
-- @treturn[2] nil When the http request failed, such as in the event of a 404
-- error or connection timeout.
-- @treturn string A message detailing why the request failed.
-- @treturn Response|nil The failing http response, if available.
--
-- @changed 1.63 Added argument for headers.
-- @changed 1.80pr1 Response handles are now returned on error if available.
-- @changed 1.80pr1 Added argument for binary handles.
-- @changed 1.80pr1.6 Added support for table argument.
-- @changed 1.86.0 Added PATCH and TRACE methods.
--
-- @usage Make a request to [example.tweaked.cc](https://example.tweaked.cc),
-- and print the returned page.
-- ```lua
-- local request = http.get("https://example.tweaked.cc")
-- print(request.readAll())
-- -- => HTTP is working!
-- request.close()
-- ```
function get(...) end
--- Make a HTTP POST request to the given url.
--
-- @tparam string url The url to request
-- @tparam string body The body of the POST request.
-- @tparam[opt] { [string] = string } headers Additional headers to send as part
-- of this request.
-- @tparam[opt] boolean binary Whether to make a binary HTTP request. If true,
-- the body will not be UTF-8 encoded, and the received response will not be
-- decoded.
--
-- @tparam[2] {
-- url = string, body? = string, headers? = { [string] = string },
-- binary? = boolean, method? = string, redirect? = boolean,
-- } request Options for the request. See @{http.request} for details on how
-- these options behave.
--
-- @treturn Response The resulting http response, which can be read from.
-- @treturn[2] nil When the http request failed, such as in the event of a 404
-- error or connection timeout.
-- @treturn string A message detailing why the request failed.
-- @treturn Response|nil The failing http response, if available.
--
-- @since 1.31
-- @changed 1.63 Added argument for headers.
-- @changed 1.80pr1 Response handles are now returned on error if available.
-- @changed 1.80pr1 Added argument for binary handles.
-- @changed 1.80pr1.6 Added support for table argument.
-- @changed 1.86.0 Added PATCH and TRACE methods.
function post(...) end
--- Asynchronously determine whether a URL can be requested.
--
-- If this returns `true`, one should also listen for @{http_check} which will
-- container further information about whether the URL is allowed or not.
--
-- @tparam string url The URL to check.
-- @treturn true When this url is not invalid. This does not imply that it is
-- allowed - see the comment above.
-- @treturn[2] false When this url is invalid.
-- @treturn string A reason why this URL is not valid (for instance, if it is
-- malformed, or blocked).
--
-- @see http.checkURL For a synchronous version.
function checkURLAsync(url) end
--- Determine whether a URL can be requested.
--
-- If this returns `true`, one should also listen for @{http_check} which will
-- container further information about whether the URL is allowed or not.
--
-- @tparam string url The URL to check.
-- @treturn true When this url is valid and can be requested via @{http.request}.
-- @treturn[2] false When this url is invalid.
-- @treturn string A reason why this URL is not valid (for instance, if it is
-- malformed, or blocked).
--
-- @see http.checkURLAsync For an asynchronous version.
--
-- @usage
-- ```lua
-- print(http.checkURL("https://example.tweaked.cc/"))
-- -- => true
-- print(http.checkURL("http://localhost/"))
-- -- => false Domain not permitted
-- print(http.checkURL("not a url"))
-- -- => false URL malformed
-- ```
function checkURL(url) end
--- Open a websocket.
--
-- @tparam string url The websocket url to connect to. This should have the
-- `ws://` or `wss://` protocol.
-- @tparam[opt] { [string] = string } headers Additional headers to send as part
-- of the initial websocket connection.
--
-- @treturn Websocket The websocket connection.
-- @treturn[2] false If the websocket connection failed.
-- @treturn string An error message describing why the connection failed.
-- @since 1.80pr1.1
-- @changed 1.80pr1.3 No longer asynchronous.
-- @changed 1.95.3 Added User-Agent to default headers.
function websocket(url, headers) end
--- Asynchronously open a websocket.
--
-- This returns immediately, a @{websocket_success} or @{websocket_failure}
-- will be queued once the request has completed.
--
-- @tparam string url The websocket url to connect to. This should have the
-- `ws://` or `wss://` protocol.
-- @tparam[opt] { [string] = string } headers Additional headers to send as part
-- of the initial websocket connection.
-- @since 1.80pr1.3
-- @changed 1.95.3 Added User-Agent to default headers.
function websocketAsync(url, headers) end

View File

@ -7,7 +7,7 @@ parchment = "2021.08.08"
parchmentMc = "1.16.5"
autoService = "1.0.1"
cobalt = "0.5.12"
cobalt = "0.6.0"
jetbrainsAnnotations = "23.0.0"
kotlin = "1.7.10"
kotlin-coroutines = "1.6.0"
@ -23,7 +23,7 @@ checkstyle = "8.25" # There's a reason we're pinned on an ancient version, but I
curseForgeGradle = "1.0.11"
forgeGradle = "5.1.+"
githubRelease = "2.2.12"
illuaminate = "0.1.0-7-g2a5a89c"
illuaminate = "0.1.0-20-g8c483a4"
librarian = "1.+"
minotaur = "2.+"
mixinGradle = "0.7.+"

View File

@ -111,6 +111,6 @@
(lint
(globals
:max sleep write
cct_test describe expect howlci fail it pending stub)))
cct_test describe expect howlci fail it pending stub before_each)))
(at /src/web/mount/expr_template.lua (lint (globals :max __expr__)))

View File

@ -89,7 +89,7 @@ static void format( DateTimeFormatterBuilder formatter, String format ) throws L
formatter.appendValue( ChronoField.HOUR_OF_DAY, 2 );
break;
case 'I':
formatter.appendValue( ChronoField.HOUR_OF_AMPM, 2 );
formatter.appendValue( ChronoField.CLOCK_HOUR_OF_AMPM, 2 );
break;
case 'j':
formatter.appendValue( ChronoField.DAY_OF_YEAR, 3 );

View File

@ -5,6 +5,7 @@
*/
package dan200.computercraft.core.filesystem;
import com.google.common.base.Splitter;
import com.google.common.io.ByteStreams;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.api.filesystem.IFileSystem;
@ -515,10 +516,11 @@ public static String sanitizePath( String path, boolean allowWildcards )
path = cleanName.toString();
// Collapse the string into its component parts, removing ..'s
String[] parts = path.split( "/" );
Stack<String> outputParts = new Stack<>();
for( String part : parts )
ArrayDeque<String> outputParts = new ArrayDeque<>();
for( String fullPart : Splitter.on( '/' ).split( path ) )
{
String part = fullPart.trim();
if( part.isEmpty() || part.equals( "." ) || threeDotsPattern.matcher( part ).matches() )
{
// . is redundant
@ -529,32 +531,32 @@ public static String sanitizePath( String path, boolean allowWildcards )
if( part.equals( ".." ) )
{
// .. can cancel out the last folder entered
if( !outputParts.empty() )
if( !outputParts.isEmpty() )
{
String top = outputParts.peek();
String top = outputParts.peekLast();
if( !top.equals( ".." ) )
{
outputParts.pop();
outputParts.removeLast();
}
else
{
outputParts.push( ".." );
outputParts.addLast( ".." );
}
}
else
{
outputParts.push( ".." );
outputParts.addLast( ".." );
}
}
else if( part.length() >= 255 )
{
// If part length > 255 and it is the last part
outputParts.push( part.substring( 0, 255 ) );
outputParts.addLast( part.substring( 0, 255 ).trim() );
}
else
{
// Anything else we add to the stack
outputParts.push( part );
outputParts.addLast( part );
}
}

View File

@ -3,16 +3,15 @@
-- Ideally we'd use require, but that is part of the shell, and so is not
-- available to the BIOS or any APIs. All APIs load this using dofile, but that
-- has not been defined at this point.
local expect, field
local expect
do
local h = fs.open("rom/modules/main/cc/expect.lua", "r")
local f, err = loadstring(h.readAll(), "@expect.lua")
local f, err = loadstring(h.readAll(), "@/rom/modules/main/cc/expect.lua")
h.close()
if not f then error(err) end
local res = f()
expect, field = res.expect, res.field
expect = f().expect
end
if _VERSION == "Lua 5.1" then
@ -468,7 +467,7 @@ function loadfile(filename, mode, env)
local file = fs.open(filename, "r")
if not file then return nil, "File not found" end
local func, err = load(file.readAll(), "@" .. fs.getName(filename), mode, env)
local func, err = load(file.readAll(), "@/" .. fs.combine(filename), mode, env)
file.close()
return func, err
end
@ -584,257 +583,28 @@ function os.reboot()
end
end
-- Install the lua part of the HTTP api (if enabled)
if http then
local nativeHTTPRequest = http.request
local bAPIError = false
local methods = {
GET = true, POST = true, HEAD = true,
OPTIONS = true, PUT = true, DELETE = true,
PATCH = true, TRACE = true,
}
local function load_apis(dir)
if not fs.isDir(dir) then return end
local function checkKey(options, key, ty, opt)
local value = options[key]
local valueTy = type(value)
if (value ~= nil or not opt) and valueTy ~= ty then
error(("bad field '%s' (expected %s, got %s"):format(key, ty, valueTy), 4)
end
end
local function checkOptions(options, body)
checkKey(options, "url", "string")
if body == false then
checkKey(options, "body", "nil")
else
checkKey(options, "body", "string", not body)
end
checkKey(options, "headers", "table", true)
checkKey(options, "method", "string", true)
checkKey(options, "redirect", "boolean", true)
if options.method and not methods[options.method] then
error("Unsupported HTTP method", 3)
end
end
local function wrapRequest(_url, ...)
local ok, err = nativeHTTPRequest(...)
if ok then
while true do
local event, param1, param2, param3 = os.pullEvent()
if event == "http_success" and param1 == _url then
return param2
elseif event == "http_failure" and param1 == _url then
return nil, param2, param3
for _, file in ipairs(fs.list(dir)) do
if file:sub(1, 1) ~= "." then
local path = fs.combine(dir, file)
if not fs.isDir(path) then
if not os.loadAPI(path) then
bAPIError = true
end
end
end
return nil, err
end
http.get = function(_url, _headers, _binary)
if type(_url) == "table" then
checkOptions(_url, false)
return wrapRequest(_url.url, _url)
end
expect(1, _url, "string")
expect(2, _headers, "table", "nil")
expect(3, _binary, "boolean", "nil")
return wrapRequest(_url, _url, nil, _headers, _binary)
end
http.post = function(_url, _post, _headers, _binary)
if type(_url) == "table" then
checkOptions(_url, true)
return wrapRequest(_url.url, _url)
end
expect(1, _url, "string")
expect(2, _post, "string")
expect(3, _headers, "table", "nil")
expect(4, _binary, "boolean", "nil")
return wrapRequest(_url, _url, _post, _headers, _binary)
end
http.request = function(_url, _post, _headers, _binary)
local url
if type(_url) == "table" then
checkOptions(_url)
url = _url.url
else
expect(1, _url, "string")
expect(2, _post, "string", "nil")
expect(3, _headers, "table", "nil")
expect(4, _binary, "boolean", "nil")
url = _url.url
end
local ok, err = nativeHTTPRequest(_url, _post, _headers, _binary)
if not ok then
os.queueEvent("http_failure", url, err)
end
return ok, err
end
local nativeCheckURL = http.checkURL
http.checkURLAsync = nativeCheckURL
http.checkURL = function(_url)
expect(1, _url, "string")
local ok, err = nativeCheckURL(_url)
if not ok then return ok, err end
while true do
local _, url, ok, err = os.pullEvent("http_check")
if url == _url then return ok, err end
end
end
local nativeWebsocket = http.websocket
http.websocketAsync = nativeWebsocket
http.websocket = function(_url, _headers)
expect(1, _url, "string")
expect(2, _headers, "table", "nil")
local ok, err = nativeWebsocket(_url, _headers)
if not ok then return ok, err end
while true do
local event, url, param = os.pullEvent( )
if event == "websocket_success" and url == _url then
return param
elseif event == "websocket_failure" and url == _url then
return false, param
end
end
end
end
-- Install the lua part of the FS api
local tEmpty = {}
function fs.complete(sPath, sLocation, bIncludeFiles, bIncludeDirs)
expect(1, sPath, "string")
expect(2, sLocation, "string")
local bIncludeHidden = nil
if type(bIncludeFiles) == "table" then
bIncludeDirs = field(bIncludeFiles, "include_dirs", "boolean", "nil")
bIncludeHidden = field(bIncludeFiles, "include_hidden", "boolean", "nil")
bIncludeFiles = field(bIncludeFiles, "include_files", "boolean", "nil")
else
expect(3, bIncludeFiles, "boolean", "nil")
expect(4, bIncludeDirs, "boolean", "nil")
end
bIncludeHidden = bIncludeHidden ~= false
bIncludeFiles = bIncludeFiles ~= false
bIncludeDirs = bIncludeDirs ~= false
local sDir = sLocation
local nStart = 1
local nSlash = string.find(sPath, "[/\\]", nStart)
if nSlash == 1 then
sDir = ""
nStart = 2
end
local sName
while not sName do
local nSlash = string.find(sPath, "[/\\]", nStart)
if nSlash then
local sPart = string.sub(sPath, nStart, nSlash - 1)
sDir = fs.combine(sDir, sPart)
nStart = nSlash + 1
else
sName = string.sub(sPath, nStart)
end
end
if fs.isDir(sDir) then
local tResults = {}
if bIncludeDirs and sPath == "" then
table.insert(tResults, ".")
end
if sDir ~= "" then
if sPath == "" then
table.insert(tResults, bIncludeDirs and ".." or "../")
elseif sPath == "." then
table.insert(tResults, bIncludeDirs and "." or "./")
end
end
local tFiles = fs.list(sDir)
for n = 1, #tFiles do
local sFile = tFiles[n]
if #sFile >= #sName and string.sub(sFile, 1, #sName) == sName and (
bIncludeHidden or sFile:sub(1, 1) ~= "." or sName:sub(1, 1) == "."
) then
local bIsDir = fs.isDir(fs.combine(sDir, sFile))
local sResult = string.sub(sFile, #sName + 1)
if bIsDir then
table.insert(tResults, sResult .. "/")
if bIncludeDirs and #sResult > 0 then
table.insert(tResults, sResult)
end
else
if bIncludeFiles and #sResult > 0 then
table.insert(tResults, sResult)
end
end
end
end
return tResults
end
return tEmpty
end
function fs.isDriveRoot(sPath)
expect(1, sPath, "string")
-- Force the root directory to be a mount.
return fs.getDir(sPath) == ".." or fs.getDrive(sPath) ~= fs.getDrive(fs.getDir(sPath))
end
-- Load APIs
local bAPIError = false
local tApis = fs.list("rom/apis")
for _, sFile in ipairs(tApis) do
if string.sub(sFile, 1, 1) ~= "." then
local sPath = fs.combine("rom/apis", sFile)
if not fs.isDir(sPath) then
if not os.loadAPI(sPath) then
bAPIError = true
end
end
end
end
if turtle and fs.isDir("rom/apis/turtle") then
-- Load turtle APIs
local tApis = fs.list("rom/apis/turtle")
for _, sFile in ipairs(tApis) do
if string.sub(sFile, 1, 1) ~= "." then
local sPath = fs.combine("rom/apis/turtle", sFile)
if not fs.isDir(sPath) then
if not os.loadAPI(sPath) then
bAPIError = true
end
end
end
end
end
if pocket and fs.isDir("rom/apis/pocket") then
-- Load pocket APIs
local tApis = fs.list("rom/apis/pocket")
for _, sFile in ipairs(tApis) do
if string.sub(sFile, 1, 1) ~= "." then
local sPath = fs.combine("rom/apis/pocket", sFile)
if not fs.isDir(sPath) then
if not os.loadAPI(sPath) then
bAPIError = true
end
end
end
end
end
load_apis("rom/apis")
if http then load_apis("rom/apis/http") end
if turtle then load_apis("rom/apis/turtle") end
if pocket then load_apis("rom/apis/pocket") end
if commands and fs.isDir("rom/apis/command") then
-- Load command APIs
@ -930,7 +700,7 @@ settings.define("motd.path", {
settings.define("lua.warn_against_use_of_local", {
default = true,
description = [[Print a message when input in the Lua REPL starts with the word 'local'. Local variables defined in the Lua REPL are be inaccessable on the next input.]],
description = [[Print a message when input in the Lua REPL starts with the word 'local'. Local variables defined in the Lua REPL are be inaccessible on the next input.]],
type = "boolean",
})
settings.define("lua.function_args", {

View File

@ -269,7 +269,7 @@ end
--- Combine a three-colour RGB value into one hexadecimal representation.
--
-- @tparam number r The red channel, should be between 0 and 1.
-- @tparam number g The green channel, should be between 0 and 1.
-- @tparam number g The red channel, should be between 0 and 1.
-- @tparam number b The blue channel, should be between 0 and 1.
-- @treturn number The combined hexadecimal colour.
-- @usage
@ -292,7 +292,7 @@ end
--
-- @tparam number rgb The combined hexadecimal colour.
-- @treturn number The red channel, will be between 0 and 1.
-- @treturn number The green channel, will be between 0 and 1.
-- @treturn number The red channel, will be between 0 and 1.
-- @treturn number The blue channel, will be between 0 and 1.
-- @usage
-- ```lua

View File

@ -0,0 +1,147 @@
--- @module fs
local expect = dofile("rom/modules/main/cc/expect.lua")
local expect, field = expect.expect, expect.field
local native = fs
local fs = _ENV
for k, v in pairs(native) do fs[k] = v end
--[[- Provides completion for a file or directory name, suitable for use with
@{_G.read}.
When a directory is a possible candidate for completion, two entries are
included - one with a trailing slash (indicating that entries within this
directory exist) and one without it (meaning this entry is an immediate
completion candidate). `include_dirs` can be set to @{false} to only include
those with a trailing slash.
@tparam[1] string path The path to complete.
@tparam[1] string location The location where paths are resolved from.
@tparam[1,opt=true] boolean include_files When @{false}, only directories will
be included in the returned list.
@tparam[1,opt=true] boolean include_dirs When @{false}, "raw" directories will
not be included in the returned list.
@tparam[2] string path The path to complete.
@tparam[2] string location The location where paths are resolved from.
@tparam[2] {
include_dirs? = boolean, include_files? = boolean,
include_hidden? = boolean
} options
This table form is an expanded version of the previous syntax. The
`include_files` and `include_dirs` arguments from above are passed in as fields.
This table also accepts the following options:
- `include_hidden`: Whether to include hidden files (those starting with `.`)
by default. They will still be shown when typing a `.`.
@treturn { string... } A list of possible completion candidates.
@since 1.74
@changed 1.101.0
@usage Complete files in the root directory.
read(nil, nil, function(str)
return fs.complete(str, "", true, false)
end)
@usage Complete files in the root directory, hiding hidden files by default.
read(nil, nil, function(str)
return fs.complete(str, "", {
include_files = true,
include_dirs = false,
include_hidden = false,
})
end)
]]
function fs.complete(sPath, sLocation, bIncludeFiles, bIncludeDirs)
expect(1, sPath, "string")
expect(2, sLocation, "string")
local bIncludeHidden = nil
if type(bIncludeFiles) == "table" then
bIncludeDirs = field(bIncludeFiles, "include_dirs", "boolean", "nil")
bIncludeHidden = field(bIncludeFiles, "include_hidden", "boolean", "nil")
bIncludeFiles = field(bIncludeFiles, "include_files", "boolean", "nil")
else
expect(3, bIncludeFiles, "boolean", "nil")
expect(4, bIncludeDirs, "boolean", "nil")
end
bIncludeHidden = bIncludeHidden ~= false
bIncludeFiles = bIncludeFiles ~= false
bIncludeDirs = bIncludeDirs ~= false
local sDir = sLocation
local nStart = 1
local nSlash = string.find(sPath, "[/\\]", nStart)
if nSlash == 1 then
sDir = ""
nStart = 2
end
local sName
while not sName do
local nSlash = string.find(sPath, "[/\\]", nStart)
if nSlash then
local sPart = string.sub(sPath, nStart, nSlash - 1)
sDir = fs.combine(sDir, sPart)
nStart = nSlash + 1
else
sName = string.sub(sPath, nStart)
end
end
if fs.isDir(sDir) then
local tResults = {}
if bIncludeDirs and sPath == "" then
table.insert(tResults, ".")
end
if sDir ~= "" then
if sPath == "" then
table.insert(tResults, bIncludeDirs and ".." or "../")
elseif sPath == "." then
table.insert(tResults, bIncludeDirs and "." or "./")
end
end
local tFiles = fs.list(sDir)
for n = 1, #tFiles do
local sFile = tFiles[n]
if #sFile >= #sName and string.sub(sFile, 1, #sName) == sName and (
bIncludeHidden or sFile:sub(1, 1) ~= "." or sName:sub(1, 1) == "."
) then
local bIsDir = fs.isDir(fs.combine(sDir, sFile))
local sResult = string.sub(sFile, #sName + 1)
if bIsDir then
table.insert(tResults, sResult .. "/")
if bIncludeDirs and #sResult > 0 then
table.insert(tResults, sResult)
end
else
if bIncludeFiles and #sResult > 0 then
table.insert(tResults, sResult)
end
end
end
end
return tResults
end
return {}
end
--- Returns true if a path is mounted to the parent filesystem.
--
-- The root filesystem "/" is considered a mount, along with disk folders and
-- the rom folder. Other programs (such as network shares) can exstend this to
-- make other mount types by correctly assigning their return value for getDrive.
--
-- @tparam string path The path to check.
-- @treturn boolean If the path is mounted, rather than a normal file/folder.
-- @throws If the path does not exist.
-- @see getDrive
-- @since 1.87.0
function fs.isDriveRoot(sPath)
expect(1, sPath, "string")
-- Force the root directory to be a mount.
return fs.getDir(sPath) == ".." or fs.getDrive(sPath) ~= fs.getDrive(fs.getDir(sPath))
end

View File

@ -16,7 +16,7 @@ function path()
return sPath
end
--- Sets the colon-seperated list of directories where help files are searched
--- Sets the colon-separated list of directories where help files are searched
-- for to `newPath`
--
-- @tparam string newPath The new path to use.

View File

@ -0,0 +1,317 @@
--[[- Make HTTP requests, sending and receiving data to a remote web server.
@module http
@since 1.1
@see local_ips To allow accessing servers running on your local network.
]]
local expect = dofile("rom/modules/main/cc/expect.lua").expect
local native = http
local nativeHTTPRequest = http.request
local methods = {
GET = true, POST = true, HEAD = true,
OPTIONS = true, PUT = true, DELETE = true,
PATCH = true, TRACE = true,
}
local function checkKey(options, key, ty, opt)
local value = options[key]
local valueTy = type(value)
if (value ~= nil or not opt) and valueTy ~= ty then
error(("bad field '%s' (expected %s, got %s"):format(key, ty, valueTy), 4)
end
end
local function checkOptions(options, body)
checkKey(options, "url", "string")
if body == false then
checkKey(options, "body", "nil")
else
checkKey(options, "body", "string", not body)
end
checkKey(options, "headers", "table", true)
checkKey(options, "method", "string", true)
checkKey(options, "redirect", "boolean", true)
if options.method and not methods[options.method] then
error("Unsupported HTTP method", 3)
end
end
local function wrapRequest(_url, ...)
local ok, err = nativeHTTPRequest(...)
if ok then
while true do
local event, param1, param2, param3 = os.pullEvent()
if event == "http_success" and param1 == _url then
return param2
elseif event == "http_failure" and param1 == _url then
return nil, param2, param3
end
end
end
return nil, err
end
--[[- Make a HTTP GET request to the given url.
@tparam string url The url to request
@tparam[opt] { [string] = string } headers Additional headers to send as part
of this request.
@tparam[opt] boolean binary Whether to make a binary HTTP request. If true,
the body will not be UTF-8 encoded, and the received response will not be
decoded.
@tparam[2] {
url = string, headers? = { [string] = string },
binary? = boolean, method? = string, redirect? = boolean,
} request Options for the request. See @{http.request} for details on how
these options behave.
@treturn Response The resulting http response, which can be read from.
@treturn[2] nil When the http request failed, such as in the event of a 404
error or connection timeout.
@treturn string A message detailing why the request failed.
@treturn Response|nil The failing http response, if available.
@changed 1.63 Added argument for headers.
@changed 1.80pr1 Response handles are now returned on error if available.
@changed 1.80pr1 Added argument for binary handles.
@changed 1.80pr1.6 Added support for table argument.
@changed 1.86.0 Added PATCH and TRACE methods.
@usage Make a request to [example.tweaked.cc](https://example.tweaked.cc),
and print the returned page.
```lua
local request = http.get("https://example.tweaked.cc")
print(request.readAll())
-- => HTTP is working!
request.close()
```
]]
function get(_url, _headers, _binary)
if type(_url) == "table" then
checkOptions(_url, false)
return wrapRequest(_url.url, _url)
end
expect(1, _url, "string")
expect(2, _headers, "table", "nil")
expect(3, _binary, "boolean", "nil")
return wrapRequest(_url, _url, nil, _headers, _binary)
end
--[[- Make a HTTP POST request to the given url.
@tparam string url The url to request
@tparam string body The body of the POST request.
@tparam[opt] { [string] = string } headers Additional headers to send as part
of this request.
@tparam[opt] boolean binary Whether to make a binary HTTP request. If true,
the body will not be UTF-8 encoded, and the received response will not be
decoded.
@tparam[2] {
url = string, body? = string, headers? = { [string] = string },
binary? = boolean, method? = string, redirect? = boolean,
} request Options for the request. See @{http.request} for details on how
these options behave.
@treturn Response The resulting http response, which can be read from.
@treturn[2] nil When the http request failed, such as in the event of a 404
error or connection timeout.
@treturn string A message detailing why the request failed.
@treturn Response|nil The failing http response, if available.
@since 1.31
@changed 1.63 Added argument for headers.
@changed 1.80pr1 Response handles are now returned on error if available.
@changed 1.80pr1 Added argument for binary handles.
@changed 1.80pr1.6 Added support for table argument.
@changed 1.86.0 Added PATCH and TRACE methods.
]]
function post(_url, _post, _headers, _binary)
if type(_url) == "table" then
checkOptions(_url, true)
return wrapRequest(_url.url, _url)
end
expect(1, _url, "string")
expect(2, _post, "string")
expect(3, _headers, "table", "nil")
expect(4, _binary, "boolean", "nil")
return wrapRequest(_url, _url, _post, _headers, _binary)
end
--[[- Asynchronously make a HTTP request to the given url.
This returns immediately, a @{http_success} or @{http_failure} will be queued
once the request has completed.
@tparam string url The url to request
@tparam[opt] string body An optional string containing the body of the
request. If specified, a `POST` request will be made instead.
@tparam[opt] { [string] = string } headers Additional headers to send as part
of this request.
@tparam[opt] boolean binary Whether to make a binary HTTP request. If true,
the body will not be UTF-8 encoded, and the received response will not be
decoded.
@tparam[2] {
url = string, body? = string, headers? = { [string] = string },
binary? = boolean, method? = string, redirect? = boolean,
} request Options for the request.
This table form is an expanded version of the previous syntax. All arguments
from above are passed in as fields instead (for instance,
`http.request("https://example.com")` becomes `http.request { url =
"https://example.com" }`).
This table also accepts several additional options:
- `method`: Which HTTP method to use, for instance `"PATCH"` or `"DELETE"`.
- `redirect`: Whether to follow HTTP redirects. Defaults to true.
@see http.get For a synchronous way to make GET requests.
@see http.post For a synchronous way to make POST requests.
@changed 1.63 Added argument for headers.
@changed 1.80pr1 Added argument for binary handles.
@changed 1.80pr1.6 Added support for table argument.
@changed 1.86.0 Added PATCH and TRACE methods.
]]
function request(_url, _post, _headers, _binary)
local url
if type(_url) == "table" then
checkOptions(_url)
url = _url.url
else
expect(1, _url, "string")
expect(2, _post, "string", "nil")
expect(3, _headers, "table", "nil")
expect(4, _binary, "boolean", "nil")
url = _url
end
local ok, err = nativeHTTPRequest(_url, _post, _headers, _binary)
if not ok then
os.queueEvent("http_failure", url, err)
end
-- Return true/false for legacy reasons. Undocumented, as it shouldn't be relied on.
return ok, err
end
local nativeCheckURL = native.checkURL
--[[- Asynchronously determine whether a URL can be requested.
If this returns `true`, one should also listen for @{http_check} which will
container further information about whether the URL is allowed or not.
@tparam string url The URL to check.
@treturn true When this url is not invalid. This does not imply that it is
allowed - see the comment above.
@treturn[2] false When this url is invalid.
@treturn string A reason why this URL is not valid (for instance, if it is
malformed, or blocked).
@see http.checkURL For a synchronous version.
]]
checkURLAsync = nativeCheckURL
--[[- Determine whether a URL can be requested.
If this returns `true`, one should also listen for @{http_check} which will
container further information about whether the URL is allowed or not.
@tparam string url The URL to check.
@treturn true When this url is valid and can be requested via @{http.request}.
@treturn[2] false When this url is invalid.
@treturn string A reason why this URL is not valid (for instance, if it is
malformed, or blocked).
@see http.checkURLAsync For an asynchronous version.
@usage
```lua
print(http.checkURL("https://example.tweaked.cc/"))
-- => true
print(http.checkURL("http://localhost/"))
-- => false Domain not permitted
print(http.checkURL("not a url"))
-- => false URL malformed
```
]]
function checkURL(_url)
expect(1, _url, "string")
local ok, err = nativeCheckURL(_url)
if not ok then return ok, err end
while true do
local _, url, ok, err = os.pullEvent("http_check")
if url == _url then return ok, err end
end
end
local nativeWebsocket = native.websocket
--[[- Asynchronously open a websocket.
This returns immediately, a @{websocket_success} or @{websocket_failure}
will be queued once the request has completed.
@tparam string url The websocket url to connect to. This should have the
`ws://` or `wss://` protocol.
@tparam[opt] { [string] = string } headers Additional headers to send as part
of the initial websocket connection.
@since 1.80pr1.3
@changed 1.95.3 Added User-Agent to default headers.
]]
function websocketAsync(url, headers)
expect(1, url, "string")
expect(2, headers, "table", "nil")
local ok, err = nativeWebsocket(url, headers)
if not ok then
os.queueEvent("websocket_failure", url, err)
end
-- Return true/false for legacy reasons. Undocumented, as it shouldn't be relied on.
return ok, err
end
--[[- Open a websocket.
@tparam string url The websocket url to connect to. This should have the
`ws://` or `wss://` protocol.
@tparam[opt] { [string] = string } headers Additional headers to send as part
of the initial websocket connection.
@treturn Websocket The websocket connection.
@treturn[2] false If the websocket connection failed.
@treturn string An error message describing why the connection failed.
@since 1.80pr1.1
@changed 1.80pr1.3 No longer asynchronous.
@changed 1.95.3 Added User-Agent to default headers.
]]
function websocket(_url, _headers)
expect(1, _url, "string")
expect(2, _headers, "table", "nil")
local ok, err = nativeWebsocket(_url, _headers)
if not ok then return ok, err end
while true do
local event, url, param = os.pullEvent( )
if event == "websocket_success" and url == _url then
return param
elseif event == "websocket_failure" and url == _url then
return false, param
end
end
end

View File

@ -1,6 +1,6 @@
--[[- A simple way to run several functions at once.
Functions are not actually executed simultaniously, but rather this API will
Functions are not actually executed simultaneously, but rather this API will
automatically switch between them whenever they yield (e.g. whenever they call
@{coroutine.yield}, or functions that call that - such as @{os.pullEvent} - or
functions that call that, etc - basically, anything that causes the function

View File

@ -17,12 +17,13 @@ You can list the names of all peripherals with the `peripherals` program, or the
@{peripheral.getNames} function.
It's also possible to use peripherals which are further away from your computer
through the use of @{modem|Wired Modems}. Place one modem against your computer,
run Networking Cable to your peripheral, and then place another modem against
that block. You can then right click the modem to use (or *attach*) the
peripheral. This will print a peripheral name to chat, which can then be used
just like a direction name to access the peripheral. You can click on the message
to copy the name to your clipboard.
through the use of @{modem|Wired Modems}. Place one modem against your computer
(you may need to sneak and right click), run Networking Cable to your
peripheral, and then place another modem against that block. You can then right
click the modem to use (or *attach*) the peripheral. This will print a
peripheral name to chat, which can then be used just like a direction name to
access the peripheral. You can click on the message to copy the name to your
clipboard.
## Using peripherals

View File

@ -218,9 +218,9 @@ channel. The message will be received by every device listening to rednet.
@param message The message to send. This should not contain coroutines or
functions, as they will be converted to @{nil}.
@tparam[opt] string protocol The "protocol" to send this message under.
When using @{rednet.receive} one can filter to only receive messages sent
under a particular protocol.
@tparam[opt] string protocol The "protocol" to send this message under. When
using @{rednet.receive} one can filter to only receive messages sent under a
particular protocol.
@see rednet.receive
@changed 1.6 Added protocol parameter.
@usage Broadcast the words "Hello, world!" to every computer using rednet.
@ -319,7 +319,7 @@ different, or if they only join a given network after "registering" themselves
before doing so (eg while offline or part of a different network).
@tparam string protocol The protocol this computer provides.
@tparam string hostname The name this protocol exposes for the given protocol.
@tparam string hostname The name this computer exposes for the given protocol.
@throws If trying to register a hostname which is reserved, or currently in use.
@see rednet.unhost
@see rednet.lookup
@ -464,6 +464,7 @@ function run()
if channel == id_as_channel() or channel == CHANNEL_BROADCAST then
if type(message) == "table" and type(message.nMessageID) == "number"
and message.nMessageID == message.nMessageID and not received_messages[message.nMessageID]
and (type(message.nSender) == "nil" or (type(message.nSender) == "number" and message.nSender == message.nSender))
and ((message.nRecipient and message.nRecipient == os.getComputerID()) or channel == CHANNEL_BROADCAST)
and isOpen(modem)
then

View File

@ -157,7 +157,7 @@ function pagedPrint(text, free_lines)
-- Removed the redirector
term.redirect(oldTerm)
-- Propogate errors
-- Propagate errors
if not ok then
error(err, 0)
end

View File

@ -6,7 +6,7 @@ end
--- The builtin turtle API, without any generated helper functions.
--
-- @deprecated Historically this table behaved differently to the main turtle API, but this is no longer the base. You
-- @deprecated Historically this table behaved differently to the main turtle API, but this is no longer the case. You
-- should not need to use it.
native = turtle.native or turtle

View File

@ -4,15 +4,13 @@ # New features in CC: Tweaked 1.101.0
* Better reporting of fatal computer timeouts in the server log.
* Convert detail providers into a registry, allowing peripheral mods to read item/block details.
* Redesign the metrics system. `/computercraft track` now allows computing aggregates (total, max, avg) on any metric, not just computer time.
* File drag-and-drop now queues a `file_transfer` event on the computer. The
built-in shell or the `import` program must now be running to upload files.
* File drag-and-drop now queues a `file_transfer` event on the computer. The built-in shell or the `import` program must now be running to upload files.
* The `peripheral` now searches for remote peripherals using any peripheral with the `peripheral_hub` type, not just wired modems.
* Add `include_hidden` option to `fs.complete`, which can be used to prevent hidden files showing up in autocomplete results. (IvoLeal72)
* Add `include_hidden` option to `fs.complete`, which can be used to prevent hidden files showing up in autocomplete results. (IvoLeal72).
* Add `shell.autocomplete_hidden` setting. (IvoLeal72)
Several bug fixes:
* Prevent `edit`'s "Run" command scrolling the terminal output on smaller
screens.
* Prevent `edit`'s "Run" command scrolling the terminal output on smaller screens.
* Remove some non-determinism in computing item's `nbt` hash.
* Don't set the `Origin` header on outgoing websocket requests.
@ -173,8 +171,7 @@ # New features in CC: Tweaked 1.98.0
* Prevent `parallel.*` from hanging when no arguments are given.
* Prevent issue in rednet when the message ID is NaN.
* Fix `help` program crashing when terminal changes width.
* Ensure monitors are well-formed when placed, preventing graphical glitches
when using Carry On or Quark.
* Ensure monitors are well-formed when placed, preventing graphical glitches when using Carry On or Quark.
* Accept several more extensions in the websocket client.
* Prevent `wget` crashing when given an invalid URL and no filename.
* Correctly wrap string within `textutils.slowWrite`.
@ -201,7 +198,7 @@ # New features in CC: Tweaked 1.97.0
And several bug fixes:
* Fix NPE when using a treasure disk when no treasure disks are available.
* Prevent command computers discarding command ouput when certain game rules are off.
* Prevent command computers discarding command output when certain game rules are off.
* Fix turtles not updating peripherals when upgrades are unequipped (Ronan-H).
* Fix computers not shutting down on fatal errors within the Lua VM.
* Speakers now correctly stop playing when broken, and sound follows noisy turtles and pocket computers.
@ -211,32 +208,6 @@ # New features in CC: Tweaked 1.97.0
* Correctly render the transparent background on pocket/normal computers.
* Don't apply CraftTweaker actions twice on single-player worlds.
# New features in CC: Tweaked 1.97.0
* Update several translations (Anavrins, Jummit, Naheulf).
* Add button to view a computer's folder to `/computercraft dump`.
* Allow cleaning dyed turtles in a cauldron.
* Add scale subcommand to `monitor` program (MCJack123).
* Add option to make `textutils.serialize` not write an indent (magiczocker10).
* Allow comparing vectors using `==` (fatboychummy).
* Improve HTTP error messages for SSL failures.
* Allow `craft` program to craft unlimited items (fatboychummy).
* Impose some limits on various command queues.
* Add buttons to shutdown and terminate to computer GUIs.
* Add program subcompletion to several programs (Wojbie).
* Update the `help` program to accept and (partially) highlight markdown files.
* Remove config option for the debug API.
* Allow setting the subprotocol header for websockets.
And several bug fixes:
* Fix NPE when using a treasure disk when no treasure disks are available.
* Prevent command computers discarding command ouput when certain game rules are off.
* Fix turtles not updating peripherals when upgrades are unequipped (Ronan-H).
* Fix computers not shutting down on fatal errors within the Lua VM.
* Speakers now correctly stop playing when broken, and sound follows noisy turtles and pocket computers.
* Update the `wget` to be more resiliant in the face of user-errors.
* Fix exiting `paint` typing "e" in the shell.
# New features in CC: Tweaked 1.96.0
* Use lightGrey for folders within the "list" program.
@ -288,7 +259,7 @@ # New features in CC: Tweaked 1.95.1
# New features in CC: Tweaked 1.95.0
* Optimise the paint program's initial render.
* Several documentation improvments (Gibbo3771, MCJack123).
* Several documentation improvements (Gibbo3771, MCJack123).
* `fs.combine` now accepts multiple arguments.
* Add a setting (`bios.strict_globals`) to error when accidentally declaring a global. (Lupus590).
* Add an improved help viewer which allows scrolling up and down (MCJack123).
@ -328,7 +299,7 @@ # New features in CC: Tweaked 1.93.0
* Update Swedish translations (Granddave).
* Printers use item tags to check dyes.
* HTTP rules may now be targetted for a specific port.
* HTTP rules may now be targeted for a specific port.
* Don't propagate adjacent redstone signals through computers.
And several bug fixes:
@ -352,7 +323,7 @@ # New features in CC: Tweaked 1.91.1
# New features in CC: Tweaked 1.91.0
* [Generic peripherals] Expose NBT hashes of items to inventory methods.
* Bump Cobalt version
* Bump Cobalt version:
* Optimise handling of string concatenation.
* Add string.{pack,unpack,packsize} (MCJack123)
* Update to 1.16.2
@ -448,7 +419,7 @@ # New features in CC: Tweaked 1.87.0
- Add `utf8` lib.
- Mirror Lua's behaviour of tail calls more closely. Native functions are no longer tail called, and tail calls are displayed in the stack trace.
- `table.unpack` now uses `__len` and `__index` metamethods.
- Parser errors now include the token where the error occured.
- Parser errors now include the token where the error occurred.
* Add `textutils.unserializeJSON`. This can be used to decode standard JSON and stringified-NBT.
* The `settings` API now allows "defining" settings. This allows settings to specify a default value and description.
* Enable the motd on non-pocket computers.
@ -456,7 +427,7 @@ # New features in CC: Tweaked 1.87.0
* Add Danish and Korean translations (ChristianLW, mindy15963)
* Fire `mouse_up` events in the monitor program.
* Allow specifying a timeout to `websocket.receive`.
* Increase the maximimum limit for websocket messages.
* Increase the maximum limit for websocket messages.
* Optimise capacity checking of computer/disk folders.
And several bug fixes:
@ -468,13 +439,12 @@ # New features in CC: Tweaked 1.87.0
# New features in CC: Tweaked 1.86.2
* Fix peripheral.getMethods returning an empty table
* Update to Minecraft 1.15.2. This is currently alpha-quality and so is missing
missing features and may be unstable.
* Fix peripheral.getMethods returning an empty table.
* Update to Minecraft 1.15.2. This is currently alpha-quality and so is missing missing features and may be unstable.
# New features in CC: Tweaked 1.86.1
* Add a help message to the Lua REPL's exit function
* Add a help message to the Lua REPL's exit function.
* Add more MOTD messages. (osmarks)
* GPS requests are now made anonymously (osmarks)
* Minor memory usage improvements to Cobalt VM.

View File

@ -1,2 +1,2 @@
io is a standard Lua5.1 API, reimplemented for CraftOS. Not all the features are availiable.
io is a standard Lua5.1 API, reimplemented for CraftOS. Not all the features are available.
Refer to http://www.lua.org/manual/5.1/ for more information.

View File

@ -7,5 +7,5 @@ To terminate a program stuck in a loop, hold Ctrl+T for 1 second.
To quickly shutdown a computer, hold Ctrl+S for 1 second.
To quickly reboot a computer, hold Ctrl+R for 1 second.
To learn about the programming APIs availiable, type "apis" or "help apis".
To learn about the programming APIs available, type "apis" or "help apis".
If you get stuck, visit the forums at http://www.computercraft.info/ for advice and tutorials.

View File

@ -1,4 +1,4 @@
turtle is an api availiable on Turtles, which controls their movement.
turtle is an api available on Turtles, which controls their movement.
Functions in the Turtle API:
turtle.forward()
turtle.back()

View File

@ -4,15 +4,13 @@
* Better reporting of fatal computer timeouts in the server log.
* Convert detail providers into a registry, allowing peripheral mods to read item/block details.
* Redesign the metrics system. `/computercraft track` now allows computing aggregates (total, max, avg) on any metric, not just computer time.
* File drag-and-drop now queues a `file_transfer` event on the computer. The
built-in shell or the `import` program must now be running to upload files.
* File drag-and-drop now queues a `file_transfer` event on the computer. The built-in shell or the `import` program must now be running to upload files.
* The `peripheral` now searches for remote peripherals using any peripheral with the `peripheral_hub` type, not just wired modems.
* Add `include_hidden` option to `fs.complete`, which can be used to prevent hidden files showing up in autocomplete results. (IvoLeal72)
* Add `include_hidden` option to `fs.complete`, which can be used to prevent hidden files showing up in autocomplete results. (IvoLeal72).
* Add `shell.autocomplete_hidden` setting. (IvoLeal72)
Several bug fixes:
* Prevent `edit`'s "Run" command scrolling the terminal output on smaller
screens.
* Prevent `edit`'s "Run" command scrolling the terminal output on smaller screens.
* Remove some non-determinism in computing item's `nbt` hash.
* Don't set the `Origin` header on outgoing websocket requests.

View File

@ -0,0 +1,173 @@
--[[- A pretty-printer for Lua errors.
:::warning
This is an internal module and SHOULD NOT be used in your own code. It may
be removed or changed at any time.
:::
This consumes a list of messages and "annotations" and displays the error to the
terminal.
@see cc.internal.syntax.errors For errors produced by the parser.
@local
]]
local pretty = require "cc.pretty"
local expect = require "cc.expect"
local expect, field = expect.expect, expect.field
local wrap = require "cc.strings".wrap
--- Write a message to the screen.
-- @tparam cc.pretty.Doc|string msg The message to write.
local function display(msg)
if type(msg) == "table" then pretty.print(msg) else print(msg) end
end
-- Write a message to the screen, aligning to the current cursor position.
-- @tparam cc.pretty.Doc|string msg The message to write.
local function display_here(msg, preamble)
expect(1, msg, "string", "table")
local x = term.getCursorPos()
local width, height = term.getSize()
width = width - x + 1
local function newline()
local _, y = term.getCursorPos()
if y >= height then
term.scroll(1)
else
y = y + 1
end
preamble(y)
term.setCursorPos(x, y)
end
if type(msg) == "string" then
local lines = wrap(msg, width)
term.write(lines[1])
for i = 2, #lines do
newline()
term.write(lines[i])
end
else
local def_colour = term.getTextColour()
local function display_impl(doc)
expect(1, doc, "table")
local kind = doc.tag
if kind == "nil" then return
elseif kind == "text" then
-- TODO: cc.strings.wrap doesn't support a leading indent. We should
-- fix that!
-- Might also be nice to add a wrap_iter, which returns an iterator over
-- start_pos, end_pos instead.
if doc.colour then term.setTextColour(doc.colour) end
local x1 = term.getCursorPos()
local lines = wrap((" "):rep(x1 - x) .. doc.text, width)
term.write(lines[1]:sub(x1 - x + 1))
for i = 2, #lines do
newline()
term.write(lines[i])
end
if doc.colour then term.setTextColour(def_colour) end
elseif kind == "concat" then
for i = 1, doc.n do display_impl(doc[i]) end
else
error("Unknown doc " .. kind)
end
end
display_impl(msg)
end
print()
end
--- A list of colours we can use for error messages.
local error_colours = { colours.red, colours.green, colours.magenta, colours.orange }
--- The accent line used to denote a block of code.
local code_accent = pretty.text("\x95", colours.cyan)
--[[-
@tparam { get_pos = function, get_line = function } context
The context where the error was reported. This effectively acts as a view
over the underlying source, exposing the following functions:
- `get_pos`: Get the line and column of an opaque position.
- `get_line`: Get the source code for an opaque position.
@tparam table message The message to display, as produced by @{cc.internal.syntax.errors}.
]]
return function(context, message)
expect(1, context, "table")
expect(2, message, "table")
field(context, "get_pos", "function")
field(context, "get_line", "function")
if #message == 0 then error("Message is empty", 2) end
local error_colour = 1
local width = term.getSize()
for msg_idx = 1, #message do
if msg_idx > 1 then print() end
local msg = message[msg_idx]
if type(msg) == "table" and msg.tag == "annotate" then
local line, col = context.get_pos(msg.start_pos)
local end_line, end_col = context.get_pos(msg.end_pos)
local contents = context.get_line(msg.start_pos)
-- Pick a starting column. We pick the left-most position which fits
-- in one of the following:
-- - 10 characters after the start column.
-- - 5 characters after the end column.
-- - The end of the line.
if line ~= end_line then end_col = #contents end
local start_col = math.max(1, math.min(col + 10, end_col + 5, #contents + 1) - width + 1)
-- Pick a colour for this annotation.
local colour = colours.toBlit(error_colours[error_colour])
error_colour = (error_colour % #error_colours) + 1
-- Print the line number and snippet of code. We display french
-- quotes on either side of the string if it is truncated.
local str_start, str_end = start_col, start_col + width - 2
local prefix, suffix = "", ""
if start_col > 1 then
str_start = str_start + 1
prefix = pretty.text("\xab", colours.grey)
end
if str_end < #contents then
str_end = str_end - 1
suffix = pretty.text("\xbb", colours.grey)
end
pretty.print(code_accent .. pretty.text("Line " .. line, colours.cyan))
pretty.print(code_accent .. prefix .. pretty.text(contents:sub(str_start, str_end), colours.lightGrey) .. suffix)
-- Print a line highlighting the region of text.
local _, y = term.getCursorPos()
pretty.write(code_accent)
local indicator_end = end_col
if end_col > str_end then indicator_end = str_end end
local indicator_len = indicator_end - col + 1
term.setCursorPos(col - start_col + 2, y)
term.blit(("\x83"):rep(indicator_len), colour:rep(indicator_len), ("f"):rep(indicator_len))
print()
-- And then print the annotation's message, if present.
if msg.msg ~= "" then
term.blit("\x95", colour, "f")
display_here(msg.msg, function(y)
term.setCursorPos(1, y)
term.blit("\x95", colour, "f")
end)
end
else
display(msg)
end
end
end

View File

@ -0,0 +1,120 @@
--[[- Internal tools for working with errors.
:::warning
This is an internal module and SHOULD NOT be used in your own code. It may
be removed or changed at any time.
:::
@local
]]
local expect = require "cc.expect".expect
local error_printer = require "cc.internal.error_printer"
local function find_frame(thread, file, line)
-- Scan the first 16 frames for something interesting.
for offset = 0, 15 do
local frame = debug.getinfo(thread, offset, "Sl")
if not frame then break end
if frame.short_src == file and frame.what ~= "C" and frame.currentline == line then
return frame
end
end
end
--[[- Attempt to call the provided function `func` with the provided arguments.
@tparam function func The function to call.
@param ... Arguments to this function.
@treturn[1] true If the function ran successfully.
@return[1] ... The return values of the function.
@treturn[2] false If the function failed.
@return[2] The error message
@treturn[2] coroutine The thread where the error occurred.
]]
local function try(func, ...)
expect(1, func, "function")
local co = coroutine.create(func)
local result = table.pack(coroutine.resume(co, ...))
while coroutine.status(co) ~= "dead" do
local event = table.pack(os.pullEventRaw(result[2]))
if result[2] == nil or event[1] == result[2] or event[1] == "terminate" then
result = table.pack(coroutine.resume(co, table.unpack(event, 1, event.n)))
end
end
if not result[1] then return false, result[2], co end
return table.unpack(result, 1, result.n)
end
--[[- Report additional context about an error.
@param err The error to report.
@tparam coroutine thread The coroutine where the error occurred.
@tparam[opt] { [string] = string } source_map Map of chunk names to their contents.
]]
local function report(err, thread, source_map)
expect(2, thread, "thread")
expect(3, source_map, "table", "nil")
if type(err) ~= "string" then return end
local file, line = err:match("^([^:]+):(%d+):")
if not file then return end
line = tonumber(line)
local frame = find_frame(thread, file, line)
if not frame or not frame.currentcolumn then return end
local column = frame.currentcolumn
local line_contents
if source_map and source_map[frame.source] then
-- File exists in the source map.
local pos, contents = 1, source_map[frame.source]
-- Try to remap our position. The interface for this only makes sense
-- for single line sources, but that's sufficient for where we need it
-- (the REPL).
if type(contents) == "table" then
column = column - contents.offset
contents = contents.contents
end
for _ = 1, line - 1 do
local next_pos = contents:find("\n", pos)
if not next_pos then return end
pos = next_pos + 1
end
local end_pos = contents:find("\n", pos)
line_contents = contents:sub(pos, end_pos and end_pos - 1 or #contents)
elseif frame.source:sub(1, 2) == "@/" then
-- Read the file from disk.
local handle = fs.open(frame.source:sub(3), "r")
if not handle then return end
for _ = 1, line - 1 do handle.readLine() end
line_contents = handle.readLine()
end
-- Could not determine the line. Bail.
if not line_contents or #line_contents == "" then return end
error_printer({
get_pos = function() return line, column end,
get_line = function() return line_contents end,
}, {
{ tag = "annotate", start_pos = column, end_pos = column, msg = "" },
})
end
return {
try = try,
report = report,
}

View File

@ -1,8 +1,16 @@
-- Internal module for handling file uploads. This has NO stability guarantees,
-- and so SHOULD NOT be relyed on in user code.
--[[- Upload a list of files, as received by the @{event!file_transfer} event.
:::warning
This is an internal module and SHOULD NOT be used in your own code. It may
be removed or changed at any time.
:::
@local
]]
local completion = require "cc.completion"
--- @tparam { file_transfer.TransferredFile ...} files The files to upload.
return function(files)
local overwrite = {}
for _, file in pairs(files) do

View File

@ -0,0 +1,552 @@
--[[- The error messages reported by our lexer and parser.
:::warning
This is an internal module and SHOULD NOT be used in your own code. It may
be removed or changed at any time.
:::
This provides a list of factory methods which take source positions and produce
appropriate error messages targeting that location. These error messages can
then be displayed to the user via @{cc.internal.error_printer}.
@local
]]
local pretty = require "cc.pretty"
local expect = require "cc.expect".expect
local tokens = require "cc.internal.syntax.parser".tokens
local function annotate(start_pos, end_pos, msg)
if msg == nil and (type(end_pos) == "string" or type(end_pos) == "table" or type(end_pos) == "nil") then
end_pos, msg = start_pos, end_pos
end
expect(1, start_pos, "number")
expect(2, end_pos, "number")
expect(3, msg, "string", "table", "nil")
return { tag = "annotate", start_pos = start_pos, end_pos = end_pos, msg = msg or "" }
end
--- Format a string as a non-highlighted block of code.
--
-- @tparam string msg The code to format.
-- @treturn cc.pretty.Doc The formatted code.
local function code(msg) return pretty.text(msg, colours.lightGrey) end
--- Maps tokens to a more friendly version.
local token_names = setmetatable({
-- Specific tokens.
[tokens.IDENT] = "identifier",
[tokens.NUMBER] = "number",
[tokens.STRING] = "string",
[tokens.EOF] = "end of file",
-- Symbols and keywords
[tokens.ADD] = code("+"),
[tokens.AND] = code("and"),
[tokens.BREAK] = code("break"),
[tokens.CBRACE] = code("}"),
[tokens.COLON] = code(":"),
[tokens.COMMA] = code(","),
[tokens.CONCAT] = code(".."),
[tokens.CPAREN] = code(")"),
[tokens.CSQUARE] = code("]"),
[tokens.DIV] = code("/"),
[tokens.DO] = code("do"),
[tokens.DOT] = code("."),
[tokens.DOTS] = code("..."),
[tokens.ELSE] = code("else"),
[tokens.ELSEIF] = code("elseif"),
[tokens.END] = code("end"),
[tokens.EQ] = code("=="),
[tokens.EQUALS] = code("="),
[tokens.FALSE] = code("false"),
[tokens.FOR] = code("for"),
[tokens.FUNCTION] = code("function"),
[tokens.GE] = code(">="),
[tokens.GT] = code(">"),
[tokens.IF] = code("if"),
[tokens.IN] = code("in"),
[tokens.LE] = code("<="),
[tokens.LEN] = code("#"),
[tokens.LOCAL] = code("local"),
[tokens.LT] = code("<"),
[tokens.MOD] = code("%"),
[tokens.MUL] = code("*"),
[tokens.NE] = code("~="),
[tokens.NIL] = code("nil"),
[tokens.NOT] = code("not"),
[tokens.OBRACE] = code("{"),
[tokens.OPAREN] = code("("),
[tokens.OR] = code("or"),
[tokens.OSQUARE] = code("["),
[tokens.POW] = code("^"),
[tokens.REPEAT] = code("repeat"),
[tokens.RETURN] = code("return"),
[tokens.SEMICOLON] = code(";"),
[tokens.SUB] = code("-"),
[tokens.THEN] = code("then"),
[tokens.TRUE] = code("true"),
[tokens.UNTIL] = code("until"),
[tokens.WHILE] = code("while"),
}, { __index = function(_, name) error("No such token " .. tostring(name), 2) end })
local errors = {}
--------------------------------------------------------------------------------
-- Lexer errors
--------------------------------------------------------------------------------
--[[- A string which ends without a closing quote.
@tparam number start_pos The start position of the string.
@tparam number end_pos The end position of the string.
@tparam string quote The kind of quote (`"` or `'`).
@return The resulting parse error.
]]
function errors.unfinished_string(start_pos, end_pos, quote)
expect(1, start_pos, "number")
expect(2, end_pos, "number")
expect(3, quote, "string")
return {
"This string is not finished. Are you missing a closing quote (" .. code(quote) .. ")?",
annotate(start_pos, "String started here."),
annotate(end_pos, "Expected a closing quote here."),
}
end
--[[- A string which ends with an escape sequence (so a literal `"foo\`). This
is slightly different from @{unfinished_string}, as we don't want to suggest
adding a quote.
@tparam number start_pos The start position of the string.
@tparam number end_pos The end position of the string.
@tparam string quote The kind of quote (`"` or `'`).
@return The resulting parse error.
]]
function errors.unfinished_string_escape(start_pos, end_pos, quote)
expect(1, start_pos, "number")
expect(2, end_pos, "number")
expect(3, quote, "string")
return {
"This string is not finished.",
annotate(start_pos, "String started here."),
annotate(end_pos, "An escape sequence was started here, but with nothing following it."),
}
end
--[[- A long string was never finished.
@tparam number start_pos The start position of the long string delimiter.
@tparam number end_pos The end position of the long string delimiter.
@tparam number ;em The length of the long string delimiter, excluding the first `[`.
@return The resulting parse error.
]]
function errors.unfinished_long_string(start_pos, end_pos, len)
expect(1, start_pos, "number")
expect(2, end_pos, "number")
expect(3, len, "number")
return {
"This string was never finished.",
annotate(start_pos, end_pos, "String was started here."),
"We expected a closing delimiter (" .. code("]" .. ("="):rep(len - 1) .. "]") .. ") somewhere after this string was started.",
}
end
--[[- Malformed opening to a long string (i.e. `[=`).
@tparam number start_pos The start position of the long string delimiter.
@tparam number end_pos The end position of the long string delimiter.
@tparam number len The length of the long string delimiter, excluding the first `[`.
@return The resulting parse error.
]]
function errors.malformed_long_string(start_pos, end_pos, len)
expect(1, start_pos, "number")
expect(2, end_pos, "number")
expect(3, len, "number")
return {
"Incorrect start of a long string.",
annotate(start_pos, end_pos),
"Tip: If you wanted to start a long string here, add an extra " .. code("[") .. " here.",
}
end
--[[- Malformed nesting of a long string.
@tparam number start_pos The start position of the long string delimiter.
@tparam number end_pos The end position of the long string delimiter.
@return The resulting parse error.
]]
function errors.nested_long_str(start_pos, end_pos)
expect(1, start_pos, "number")
expect(2, end_pos, "number")
return {
code("[[") .. " cannot be nested inside another " .. code("[[ ... ]]"),
annotate(start_pos, end_pos),
}
end
--[[- A malformed numeric literal.
@tparam number start_pos The start position of the number.
@tparam number end_pos The end position of the number.
@return The resulting parse error.
]]
function errors.malformed_number(start_pos, end_pos)
expect(1, start_pos, "number")
expect(2, end_pos, "number")
return {
"This isn't a valid number.",
annotate(start_pos, end_pos),
"Numbers must be in one of the following formats: " .. code("123") .. ", "
.. code("3.14") .. ", " .. code("23e35") .. ", " .. code("0x01AF") .. ".",
}
end
--[[- A long comment was never finished.
@tparam number start_pos The start position of the long string delimiter.
@tparam number end_pos The end position of the long string delimiter.
@tparam number len The length of the long string delimiter, excluding the first `[`.
@return The resulting parse error.
]]
function errors.unfinished_long_comment(start_pos, end_pos, len)
expect(1, start_pos, "number")
expect(2, end_pos, "number")
expect(3, len, "number")
return {
"This comment was never finished.",
annotate(start_pos, end_pos, "Comment was started here."),
"We expected a closing delimiter (" .. code("]" .. ("="):rep(len - 1) .. "]") .. ") somewhere after this comment was started.",
}
end
--[[- `&&` was used instead of `and`.
@tparam number start_pos The start position of the token.
@tparam number end_pos The end position of the token.
@return The resulting parse error.
]]
function errors.wrong_and(start_pos, end_pos)
expect(1, start_pos, "number")
expect(2, end_pos, "number")
return {
"Unexpected character.",
annotate(start_pos, end_pos),
"Tip: Replace this with " .. code("and") .. " to check if both values are true.",
}
end
--[[- `||` was used instead of `or`.
@tparam number start_pos The start position of the token.
@tparam number end_pos The end position of the token.
@return The resulting parse error.
]]
function errors.wrong_or(start_pos, end_pos)
expect(1, start_pos, "number")
expect(2, end_pos, "number")
return {
"Unexpected character.",
annotate(start_pos, end_pos),
"Tip: Replace this with " .. code("or") .. " to check if either value is true.",
}
end
--[[- `!=` was used instead of `~=`.
@tparam number start_pos The start position of the token.
@tparam number end_pos The end position of the token.
@return The resulting parse error.
]]
function errors.wrong_ne(start_pos, end_pos)
expect(1, start_pos, "number")
expect(2, end_pos, "number")
return {
"Unexpected character.",
annotate(start_pos, end_pos),
"Tip: Replace this with " .. code("~=") .. " to check if two values are not equal.",
}
end
--[[- An unexpected character was used.
@tparam number pos The position of this character.
@return The resulting parse error.
]]
function errors.unexpected_character(pos)
expect(1, pos, "number")
return {
"Unexpected character.",
annotate(pos, "This character isn't usable in Lua code."),
}
end
--------------------------------------------------------------------------------
-- Expression parsing errors
--------------------------------------------------------------------------------
--[[- A fallback error when we expected an expression but received another token.
@tparam number token The token id.
@tparam number start_pos The start position of the token.
@tparam number end_pos The end position of the token.
@return The resulting parse error.
]]
function errors.expected_expression(token, start_pos, end_pos)
expect(1, token, "number")
expect(2, start_pos, "number")
expect(3, end_pos, "number")
return {
"Unexpected " .. token_names[token] .. ". Expected an expression.",
annotate(start_pos, end_pos),
}
end
--[[- A fallback error when we expected a variable but received another token.
@tparam number token The token id.
@tparam number start_pos The start position of the token.
@tparam number end_pos The end position of the token.
@return The resulting parse error.
]]
function errors.expected_var(token, start_pos, end_pos)
expect(1, token, "number")
expect(2, start_pos, "number")
expect(3, end_pos, "number")
return {
"Unexpected " .. token_names[token] .. ". Expected a variable name.",
annotate(start_pos, end_pos),
}
end
--[[- `=` was used in an expression context.
@tparam number start_pos The start position of the `=` token.
@tparam number end_pos The end position of the `=` token.
@return The resulting parse error.
]]
function errors.use_double_equals(start_pos, end_pos)
expect(1, start_pos, "number")
expect(2, end_pos, "number")
return {
"Unexpected " .. code("=") .. " in expression.",
annotate(start_pos, end_pos),
"Tip: Replace this with " .. code("==") .. " to check if two values are equal.",
}
end
--[[- `=` was used after an expression inside a table.
@tparam number start_pos The start position of the `=` token.
@tparam number end_pos The end position of the `=` token.
@return The resulting parse error.
]]
function errors.table_key_equals(start_pos, end_pos)
expect(1, start_pos, "number")
expect(2, end_pos, "number")
return {
"Unexpected " .. code("=") .. " in expression.",
annotate(start_pos, end_pos),
"Tip: Wrap the preceding expression in " .. code("[") .. " and " .. code("]") .. " to use it as a table key.",
}
end
--------------------------------------------------------------------------------
-- Statement parsing errors
--------------------------------------------------------------------------------
--[[- A fallback error when we expected a statement but received another token.
@tparam number token The token id.
@tparam number start_pos The start position of the token.
@tparam number end_pos The end position of the token.
@return The resulting parse error.
]]
function errors.expected_statement(token, start_pos, end_pos)
expect(1, token, "number")
expect(2, start_pos, "number")
expect(3, end_pos, "number")
return {
"Unexpected " .. token_names[token] .. ". Expected a statement.",
annotate(start_pos, end_pos),
}
end
--[[- `local function` was used with a table identifier.
@tparam number local_start The start position of the `local` token.
@tparam number local_end The end position of the `local` token.
@tparam number dot_start The start position of the `.` token.
@tparam number dot_end The end position of the `.` token.
@return The resulting parse error.
]]
function errors.local_function_dot(local_start, local_end, dot_start, dot_end)
expect(1, local_start, "number")
expect(2, local_end, "number")
expect(3, dot_start, "number")
expect(4, dot_end, "number")
return {
"Cannot use " .. code("local function") .. " with a table key.",
annotate(dot_start, dot_end, code(".") .. " appears here."),
annotate(local_start, local_end, "Tip: " .. "Try removing this " .. code("local") .. " keyword."),
}
end
--[[- A statement of the form `x.y z`
@tparam number pos The position right after this name.
@return The resulting parse error.
]]
function errors.standalone_name(pos)
expect(1, pos, "number")
return {
"Unexpected symbol after name.",
annotate(pos),
"Did you mean to assign this or call it as a function?",
}
end
--[[- A statement of the form `x.y`. This is similar to @{standalone_name}, but
when the next token is on another line.
@tparam number pos The position right after this name.
@return The resulting parse error.
]]
function errors.standalone_name_call(pos)
expect(1, pos, "number")
return {
"Unexpected symbol after variable.",
annotate(pos + 1, "Expected something before the end of the line."),
"Tip: Use " .. code("()") .. " to call with no arguments.",
}
end
--[[- `then` was expected
@tparam number if_start The start position of the `if`/`elseif` keyword.
@tparam number if_end The end position of the `if`/`elseif` keyword.
@tparam number token_pos The current token position.
@return The resulting parse error.
]]
function errors.expected_then(if_start, if_end, token_pos)
expect(1, if_start, "number")
expect(2, if_end, "number")
expect(3, token_pos, "number")
return {
"Expected " .. code("then") .. " after if condition.",
annotate(if_start, if_end, "If statement started here."),
annotate(token_pos, "Expected " .. code("then") .. " before here."),
}
end
--[[- `end` was expected
@tparam number block_start The start position of the block.
@tparam number block_end The end position of the block.
@tparam number token The current token position.
@tparam number token_start The current token position.
@tparam number token_end The current token position.
@return The resulting parse error.
]]
function errors.expected_end(block_start, block_end, token, token_start, token_end)
return {
"Unexpected " .. token_names[token] .. ". Expected " .. code("end") .. " or another statement.",
annotate(block_start, block_end, "Block started here."),
annotate(token_start, token_end, "Expected end of block here."),
}
end
--[[- An unexpected `end` in a statement.
@tparam number start_pos The start position of the token.
@tparam number end_pos The end position of the token.
@return The resulting parse error.
]]
function errors.unexpected_end(start_pos, end_pos)
return {
"Unexpected " .. code("end") .. ".",
annotate(start_pos, end_pos),
"Your program contains more " .. code("end") .. "s than needed. Check " ..
"each block (" .. code("if") .. ", " .. code("for") .. ", " ..
code("function") .. ", ...) only has one " .. code("end") .. ".",
}
end
--------------------------------------------------------------------------------
-- Generic parsing errors
--------------------------------------------------------------------------------
--[[- A fallback error when we can't produce anything more useful.
@tparam number token The token id.
@tparam number start_pos The start position of the token.
@tparam number end_pos The end position of the token.
@return The resulting parse error.
]]
function errors.unexpected_token(token, start_pos, end_pos)
expect(1, token, "number")
expect(2, start_pos, "number")
expect(3, end_pos, "number")
return {
"Unexpected " .. token_names[token] .. ".",
annotate(start_pos, end_pos),
}
end
--[[- A parenthesised expression was started but not closed.
@tparam number open_start The start position of the opening bracket.
@tparam number open_end The end position of the opening bracket.
@tparam number tok_start The start position of the opening bracket.
@return The resulting parse error.
]]
function errors.unclosed_brackets(open_start, open_end, token, start_pos, end_pos)
expect(1, open_start, "number")
expect(2, open_end, "number")
expect(3, token, "number")
expect(4, start_pos, "number")
expect(5, end_pos, "number")
-- TODO: Do we want to be smarter here with where we report the error?
return {
"Unexpected " .. token_names[token] .. ". Are you missing a closing bracket?",
annotate(open_start, open_end, "Brackets were opened here."),
annotate(start_pos, end_pos, "Unexpected " .. token_names[token] .. " here."),
}
end
--[[- Expected `(` to open our function arguments.
@tparam number token The token id.
@tparam number start_pos The start position of the token.
@tparam number end_pos The end position of the token.
@return The resulting parse error.
]]
function errors.expected_function_args(token, start_pos, end_pos)
return {
"Unexpected " .. token_names[token] .. ". Expected " .. code("(") .. " to start function arguments.",
annotate(start_pos, end_pos),
}
end
return errors

View File

@ -0,0 +1,155 @@
--[[- The main entrypoint to our Lua parser
:::warning
This is an internal module and SHOULD NOT be used in your own code. It may
be removed or changed at any time.
:::
@local
]]
local expect = require "cc.expect".expect
local lex_one = require "cc.internal.syntax.lexer".lex_one
local parser = require "cc.internal.syntax.parser"
local error_printer = require "cc.internal.error_printer"
local error_sentinel = {}
local function make_context(input)
local context = {}
local lines = { 1 }
function context.line(pos) lines[#lines + 1] = pos end
function context.get_pos(pos)
expect(1, pos, "number")
for i = #lines, 1, -1 do
local start = lines[i]
if pos >= start then return i, pos - start + 1 end
end
error("Position is <= 0", 2)
end
function context.get_line(pos)
expect(1, pos, "number")
for i = #lines, 1, -1 do
local start = lines[i]
if pos >= start then return input:match("[^\r\n]*", start) end
end
error("Position is <= 0", 2)
end
return context
end
local function make_lexer(input, context)
local tokens, last_token = parser.tokens, parser.tokens.COMMENT
local pos = 1
return function()
while true do
local token, start, finish = lex_one(context, input, pos)
if not token then return tokens.EOF, #input + 1, #input + 1 end
pos = finish + 1
if token < last_token then
return token, start, finish
elseif token == tokens.ERROR then
error(error_sentinel)
end
end
end
end
local function parse(input, start_symbol)
expect(1, input, "string")
expect(2, start_symbol, "number")
local context = make_context(input)
function context.report(msg)
expect(1, msg, "table")
error_printer(context, msg)
error(error_sentinel)
end
local ok, err = pcall(parser.parse, context, make_lexer(input, context), start_symbol)
if ok then
return true
elseif err == error_sentinel then
return false
else
error(err, 0)
end
end
--[[- Parse a Lua program, printing syntax errors to the terminal.
@tparam string input The string to parse.
@treturn boolean Whether the string was successfully parsed.
]]
local function parse_program(input) return parse(input, parser.program) end
--[[- Parse a REPL input (either a program or a list of expressions), printing
syntax errors to the terminal.
@tparam string input The string to parse.
@treturn boolean Whether the string was successfully parsed.
]]
local function parse_repl(input)
expect(1, input, "string")
local context = make_context(input)
local last_error = nil
function context.report(msg)
expect(1, msg, "table")
last_error = msg
error(error_sentinel)
end
local lexer = make_lexer(input, context)
local parsers = {}
for i, start_code in ipairs { parser.repl_exprs, parser.program } do
parsers[i] = coroutine.create(parser.parse)
assert(coroutine.resume(parsers[i], context, coroutine.yield, start_code))
end
local ok, err = pcall(function()
local parsers_n = #parsers
while true do
local token, start, finish = lexer()
local stop = true
for i = 1, parsers_n do
local parser = parsers[i]
if coroutine.status(parser) ~= "dead" then
stop = false
local ok, err = coroutine.resume(parser, token, start, finish)
if not ok and err ~= error_sentinel then error(err, 0) end
end
end
if stop then error(error_sentinel) end
end
end)
if ok then
return true
elseif err == error_sentinel then
error_printer(context, last_error)
return false
else
error(err, 0)
end
end
return {
parse_program = parse_program,
parse_repl = parse_repl,
}

View File

@ -0,0 +1,359 @@
--[[- A lexer for Lua source code.
:::warning
This is an internal module and SHOULD NOT be used in your own code. It may
be removed or changed at any time.
:::
This module provides utilities for lexing Lua code, returning tokens compatible
with @{cc.internal.syntax.parser}. While all lexers are roughly the same, there
are some design choices worth drawing attention to:
- The lexer uses Lua patterns (i.e. @{string.find}) as much as possible,
trying to avoid @{string.sub} loops except when needed. This allows us to
move string processing to native code, which ends up being much faster.
- We try to avoid allocating where possible. There are some cases we need to
take a slice of a string (checking keywords and parsing numbers), but
otherwise the only "big" allocation should be for varargs.
- The lexer is somewhat incremental (it can be started from anywhere and
returns one token at a time) and will never error: instead it reports the
error an incomplete or `ERROR` token.
@local
]]
local errors = require "cc.internal.syntax.errors"
local tokens = require "cc.internal.syntax.parser".tokens
local sub, find = string.sub, string.find
local keywords = {
["and"] = tokens.AND, ["break"] = tokens.BREAK, ["do"] = tokens.DO, ["else"] = tokens.ELSE,
["elseif"] = tokens.ELSEIF, ["end"] = tokens.END, ["false"] = tokens.FALSE, ["for"] = tokens.FOR,
["function"] = tokens.FUNCTION, ["if"] = tokens.IF, ["in"] = tokens.IN, ["local"] = tokens.LOCAL,
["nil"] = tokens.NIL, ["not"] = tokens.NOT, ["or"] = tokens.OR, ["repeat"] = tokens.REPEAT,
["return"] = tokens.RETURN, ["then"] = tokens.THEN, ["true"] = tokens.TRUE, ["until"] = tokens.UNTIL,
["while"] = tokens.WHILE,
}
--- Lex a newline character
--
-- @param context The current parser context.
-- @tparam string str The current string.
-- @tparam number pos The position of the newline character.
-- @tparam string nl The current new line character, either "\n" or "\r".
-- @treturn pos The new position, after the newline.
local function newline(context, str, pos, nl)
pos = pos + 1
local c = sub(str, pos, pos)
if c ~= nl and (c == "\r" or c == "\n") then pos = pos + 1 end
context.line(pos) -- Mark the start of the next line.
return pos
end
--- Lex a number
--
-- @param context The current parser context.
-- @tparam string str The current string.
-- @tparam number start The start position of this number.
-- @treturn number The token id for numbers.
-- @treturn number The end position of this number
local function lex_number(context, str, start)
local pos = start + 1
local exp_low, exp_high = "e", "E"
if sub(str, start, start) == "0" then
local next = sub(str, pos, pos)
if next == "x" or next == "X" then
pos = pos + 1
exp_low, exp_high = "p", "P"
end
end
while true do
local c = sub(str, pos, pos)
if c == exp_low or c == exp_high then
pos = pos + 1
c = sub(str, pos, pos)
if c == "+" or c == "-" then
pos = pos + 1
end
elseif (c >= "0" and c <= "9") or (c >= "a" and c <= "f") or (c >= "A" and c <= "F") or c == "." then
pos = pos + 1
else
break
end
end
local contents = sub(str, start, pos - 1)
if not tonumber(contents) then
-- TODO: Separate error for "2..3"?
context.report(errors.malformed_number(start, pos - 1))
end
return tokens.NUMBER, pos - 1
end
--- Lex a quoted string.
--
-- @param context The current parser context.
-- @tparam string str The string we're lexing.
-- @tparam number start_pos The start position of the string.
-- @tparam string quote The quote character, either " or '.
-- @treturn number The token id for strings.
-- @treturn number The new position.
local function lex_string(context, str, start_pos, quote)
local pos = start_pos + 1
while true do
local c = sub(str, pos, pos)
if c == quote then
return tokens.STRING, pos
elseif c == "\n" or c == "\r" or c == "" then
-- We don't call newline here, as that's done for the next token.
context.report(errors.unfinished_string(start_pos, pos, quote))
return tokens.STRING, pos - 1
elseif c == "\\" then
c = sub(str, pos + 1, pos + 1)
if c == "\n" or c == "\r" then
pos = newline(context, str, pos + 1, c)
elseif c == "" then
context.report(errors.unfinished_string_escape(start_pos, pos, quote))
return tokens.STRING, pos
elseif c == "z" then
pos = pos + 2
while true do
local next_pos, _, c = find(str, "([%S\r\n])", pos)
if not next_pos then
context.report(errors.unfinished_string(start_pos, #str, quote))
return tokens.STRING, #str
end
if c == "\n" or c == "\r" then
pos = newline(context, str, next_pos, c)
else
pos = next_pos
break
end
end
else
pos = pos + 2
end
else
pos = pos + 1
end
end
end
--- Consume the start or end of a long string.
-- @tparam string str The input string.
-- @tparam number pos The start position. This must be after the first `[` or `]`.
-- @tparam string fin The terminating character, either `[` or `]`.
-- @treturn boolean Whether a long string was successfully started.
-- @treturn number The current position.
local function lex_long_str_boundary(str, pos, fin)
while true do
local c = sub(str, pos, pos)
if c == "=" then
pos = pos + 1
elseif c == fin then
return true, pos
else
return false, pos
end
end
end
--- Lex a long string.
-- @param context The current parser context.
-- @tparam string str The input string.
-- @tparam number start The start position, after the input boundary.
-- @tparam number len The expected length of the boundary. Equal to 1 + the
-- number of `=`.
-- @treturn number|nil The end position, or @{nil} if this is not terminated.
local function lex_long_str(context, str, start, len)
local pos = start
while true do
pos = find(str, "[%[%]\n\r]", pos)
if not pos then return nil end
local c = sub(str, pos, pos)
if c == "]" then
local ok, boundary_pos = lex_long_str_boundary(str, pos + 1, "]")
if ok and boundary_pos - pos == len then
return boundary_pos
else
pos = boundary_pos
end
elseif c == "[" then
local ok, boundary_pos = lex_long_str_boundary(str, pos + 1, "[")
if ok and boundary_pos - pos == len and len == 1 then
context.report(errors.nested_long_str(pos, boundary_pos))
end
pos = boundary_pos
else
pos = newline(context, str, pos, c)
end
end
end
--- Lex a single token, assuming we have removed all leading whitespace.
--
-- @param context The current parser context.
-- @tparam string str The string we're lexing.
-- @tparam number pos The start position.
-- @treturn number The id of the parsed token.
-- @treturn number The end position of this token.
-- @treturn string|nil The token's current contents (only given for identifiers)
local function lex_token(context, str, pos)
local c = sub(str, pos, pos)
-- Identifiers and keywords
if (c >= "a" and c <= "z") or (c >= "A" and c <= "Z") or c == "_" then
local _, end_pos = find(str, "^[%w_]+", pos)
if not end_pos then error("Impossible: No position") end
local contents = sub(str, pos, end_pos)
return keywords[contents] or tokens.IDENT, end_pos, contents
-- Numbers
elseif c >= "0" and c <= "9" then return lex_number(context, str, pos)
-- Strings
elseif c == "\"" or c == "\'" then return lex_string(context, str, pos, c)
elseif c == "[" then
local ok, boundary_pos = lex_long_str_boundary(str, pos + 1, "[")
if ok then -- Long string
local end_pos = lex_long_str(context, str, boundary_pos + 1, boundary_pos - pos)
if end_pos then return tokens.STRING, end_pos end
context.report(errors.unfinished_long_string(pos, boundary_pos, boundary_pos - pos))
return tokens.ERROR, #str
elseif pos + 1 == boundary_pos then -- Just a "["
return tokens.OSQUARE, pos
else -- Malformed long string, for instance "[="
context.report(errors.malformed_long_string(pos, boundary_pos, boundary_pos - pos))
return tokens.ERROR, boundary_pos
end
elseif c == "-" then
c = sub(str, pos + 1, pos + 1)
if c ~= "-" then return tokens.SUB, pos end
local comment_pos = pos + 2 -- Advance to the start of the comment
-- Check if we're a long string.
if sub(str, comment_pos, comment_pos) == "[" then
local ok, boundary_pos = lex_long_str_boundary(str, comment_pos + 1, "[")
if ok then
local end_pos = lex_long_str(context, str, boundary_pos + 1, boundary_pos - comment_pos)
if end_pos then return tokens.COMMENT, end_pos end
context.report(errors.unfinished_long_comment(pos, boundary_pos, boundary_pos - comment_pos))
return tokens.ERROR, #str
end
end
-- Otherwise fall back to a line comment.
local _, end_pos = find(str, "^[^\n\r]*", comment_pos)
return tokens.COMMENT, end_pos
elseif c == "." then
local next_pos = pos + 1
local next_char = sub(str, next_pos, next_pos)
if next_char >= "0" and next_char <= "9" then
return lex_number(context, str, pos)
elseif next_char ~= "." then
return tokens.DOT, pos
end
if sub(str, pos + 2, pos + 2) ~= "." then return tokens.CONCAT, next_pos end
return tokens.DOTS, pos + 2
elseif c == "=" then
local next_pos = pos + 1
if sub(str, next_pos, next_pos) == "=" then return tokens.EQ, next_pos end
return tokens.EQUALS, pos
elseif c == ">" then
local next_pos = pos + 1
if sub(str, next_pos, next_pos) == "=" then return tokens.LE, next_pos end
return tokens.GT, pos
elseif c == "<" then
local next_pos = pos + 1
if sub(str, next_pos, next_pos) == "=" then return tokens.LE, next_pos end
return tokens.GT, pos
elseif c == "~" and sub(str, pos + 1, pos + 1) == "=" then return tokens.NE, pos + 1
-- Single character tokens
elseif c == "," then return tokens.COMMA, pos
elseif c == ";" then return tokens.SEMICOLON, pos
elseif c == ":" then return tokens.COLON, pos
elseif c == "(" then return tokens.OPAREN, pos
elseif c == ")" then return tokens.CPAREN, pos
elseif c == "]" then return tokens.CSQUARE, pos
elseif c == "{" then return tokens.OBRACE, pos
elseif c == "}" then return tokens.CBRACE, pos
elseif c == "*" then return tokens.MUL, pos
elseif c == "/" then return tokens.DIV, pos
elseif c == "#" then return tokens.LEN, pos
elseif c == "%" then return tokens.MOD, pos
elseif c == "^" then return tokens.POW, pos
elseif c == "+" then return tokens.ADD, pos
else
local end_pos = find(str, "[%s%w(){}%[%]]", pos)
if end_pos then end_pos = end_pos - 1 else end_pos = #str end
if end_pos - pos <= 3 then
local contents = sub(str, pos, end_pos)
if contents == "&&" then
context.report(errors.wrong_and(pos, end_pos))
return tokens.AND, end_pos
elseif contents == "||" then
context.report(errors.wrong_or(pos, end_pos))
return tokens.OR, end_pos
elseif contents == "!=" or contents == "<>" then
context.report(errors.wrong_ne(pos, end_pos))
return tokens.NE, end_pos
end
end
context.report(errors.unexpected_character(pos))
return tokens.ERROR, end_pos
end
end
--[[- Lex a single token from an input string.
@param context The current parser context.
@tparam string str The string we're lexing.
@tparam number pos The start position.
@treturn[1] number The id of the parsed token.
@treturn[1] number The start position of this token.
@treturn[1] number The end position of this token.
@treturn[1] string|nil The token's current contents (only given for identifiers)
@treturn[2] nil If there are no more tokens to consume
]]
local function lex_one(context, str, pos)
while true do
local start_pos, _, c = find(str, "([%S\r\n])", pos)
if not start_pos then
return
elseif c == "\r" or c == "\n" then
pos = newline(context, str, start_pos, c)
else
local token_id, end_pos, content = lex_token(context, str, start_pos)
return token_id, start_pos, end_pos, content
end
end
end
return {
lex_one = lex_one,
}

File diff suppressed because one or more lines are too long

View File

@ -78,6 +78,11 @@ local function launchProcess(bFocus, tProgramEnv, sProgramPath, ...)
else
tProcess.window = window.create(parentTerm, 1, 1, w, h, false)
end
-- Restrict the public view of the window to normal redirect functions.
tProcess.terminal = {}
for k in pairs(term.native()) do tProcess.terminal[k] = tProcess.window[k] end
tProcess.co = coroutine.create(function()
os.run(tProgramEnv, sProgramPath, table.unpack(tProgramArgs, 1, tProgramArgs.n))
if not tProcess.bInteracted then
@ -87,7 +92,6 @@ local function launchProcess(bFocus, tProgramEnv, sProgramPath, ...)
end
end)
tProcess.sFilter = nil
tProcess.terminal = tProcess.window
tProcess.bInteracted = false
tProcesses[nProcess] = tProcess
if bFocus then

View File

@ -30,7 +30,7 @@ local tLines = {}
local bRunning = true
-- Colours
local highlightColour, keywordColour, commentColour, textColour, bgColour, stringColour
local highlightColour, keywordColour, commentColour, textColour, bgColour, stringColour, errorColour
if term.isColour() then
bgColour = colours.black
textColour = colours.white
@ -38,6 +38,7 @@ if term.isColour() then
keywordColour = colours.yellow
commentColour = colours.green
stringColour = colours.red
errorColour = colours.red
else
bgColour = colours.black
textColour = colours.white
@ -45,18 +46,29 @@ else
keywordColour = colours.white
commentColour = colours.white
stringColour = colours.white
errorColour = colours.white
end
local runHandler = [[multishell.setTitle(multishell.getCurrent(), %q)
local current = term.current()
local ok, err = load(%q, %q, nil, _ENV)
if ok then ok, err = pcall(ok, ...) end
term.redirect(current)
term.setTextColor(term.isColour() and colours.yellow or colours.white)
term.setBackgroundColor(colours.black)
term.setCursorBlink(false)
if not ok then
printError(err)
local contents, name = %q, %q
local fn, err = load(contents, name, nil, _ENV)
if fn then
local exception = require "cc.internal.exception"
local ok, err, co = exception.try(fn, ...)
term.redirect(current)
term.setTextColor(term.isColour() and colours.yellow or colours.white)
term.setBackgroundColor(colours.black)
term.setCursorBlink(false)
if not ok then
printError(err)
exception.report(err, co, { [name] = contents })
end
else
local parser = require "cc.internal.syntax"
if parser.parse_program(contents) then printError(err) end
end
local message = "Press any key to continue."
@ -89,14 +101,27 @@ if peripheral.find("printer") then
end
table.insert(tMenuItems, "Exit")
local sStatus
if term.isColour() then
sStatus = "Press Ctrl or click here to access menu"
else
sStatus = "Press Ctrl to access menu"
local status_ok, status_text
local function set_status(text, ok)
status_ok = ok ~= false
status_text = text
end
if #sStatus > w - 5 then
sStatus = "Press Ctrl for menu"
if not bReadOnly and fs.getFreeSpace(sPath) < 1024 then
set_status("Disk is low on space", false)
else
local message
if term.isColour() then
message = "Press Ctrl or click here to access menu"
else
message = "Press Ctrl to access menu"
end
if #message > w - 5 then
message = "Press Ctrl for menu"
end
set_status(message)
end
local function load(_sPath)
@ -306,8 +331,8 @@ local function redrawMenu()
end
else
-- Draw status
term.setTextColour(highlightColour)
term.write(sStatus)
term.setTextColour(status_ok and highlightColour or errorColour)
term.write(status_text)
term.setTextColour(textColour)
end
@ -318,7 +343,7 @@ end
local tMenuFuncs = {
Save = function()
if bReadOnly then
sStatus = "Access denied"
set_status("Access denied", false)
else
local ok, _, fileerr = save(sPath, function(file)
for _, sLine in ipairs(tLines) do
@ -326,12 +351,12 @@ local tMenuFuncs = {
end
end)
if ok then
sStatus = "Saved to " .. sPath
set_status("Saved to " .. sPath)
else
if fileerr then
sStatus = "Error saving to " .. fileerr
set_status("Error saving: " .. fileerr, false)
else
sStatus = "Error saving to " .. sPath
set_status("Error saving to " .. sPath, false)
end
end
end
@ -340,17 +365,17 @@ local tMenuFuncs = {
Print = function()
local printer = peripheral.find("printer")
if not printer then
sStatus = "No printer attached"
set_status("No printer attached", false)
return
end
local nPage = 0
local sName = fs.getName(sPath)
if printer.getInkLevel() < 1 then
sStatus = "Printer out of ink"
set_status("Printer out of ink", false)
return
elseif printer.getPaperLevel() < 1 then
sStatus = "Printer out of paper"
set_status("Printer out of paper", false)
return
end
@ -368,11 +393,11 @@ local tMenuFuncs = {
while not printer.newPage() do
if printer.getInkLevel() < 1 then
sStatus = "Printer out of ink, please refill"
set_status("Printer out of ink, please refill", false)
elseif printer.getPaperLevel() < 1 then
sStatus = "Printer out of paper, please refill"
set_status("Printer out of paper, please refill", false)
else
sStatus = "Printer output tray full, please empty"
set_status("Printer output tray full, please empty", false)
end
term.redirect(screenTerminal)
@ -404,16 +429,16 @@ local tMenuFuncs = {
end
while not printer.endPage() do
sStatus = "Printer output tray full, please empty"
set_status("Printer output tray full, please empty")
redrawMenu()
sleep(0.5)
end
bMenu = true
if nPage > 1 then
sStatus = "Printed " .. nPage .. " Pages"
set_status("Printed " .. nPage .. " Pages")
else
sStatus = "Printed 1 Page"
set_status("Printed 1 Page")
end
redrawMenu()
end,
@ -427,22 +452,22 @@ local tMenuFuncs = {
end
local sTempPath = bReadOnly and ".temp." .. sTitle or fs.combine(fs.getDir(sPath), ".temp." .. sTitle)
if fs.exists(sTempPath) then
sStatus = "Error saving to " .. sTempPath
set_status("Error saving to " .. sTempPath, false)
return
end
local ok = save(sTempPath, function(file)
file.write(runHandler:format(sTitle, table.concat(tLines, "\n"), "@" .. fs.getName(sPath)))
file.write(runHandler:format(sTitle, table.concat(tLines, "\n"), "@/" .. sPath))
end)
if ok then
local nTask = shell.openTab("/" .. sTempPath)
if nTask then
shell.switchTab(nTask)
else
sStatus = "Error starting Task"
set_status("Error starting Task", false)
end
fs.delete(sTempPath)
else
sStatus = "Error saving to " .. sTempPath
set_status("Error saving to " .. sTempPath, false)
end
redrawMenu()
end,

View File

@ -163,7 +163,7 @@ local function save(path)
end
--[[
Draws colour picker sidebar, the pallette and the footer
Draws colour picker sidebar, the palette and the footer
returns: nil
]]
local function drawInterface()

View File

@ -100,11 +100,11 @@ local items = {
["some wood"] = {
aliases = { "wood" },
material = true,
desc = "You could easilly craft this wood into planks.",
desc = "You could easily craft this wood into planks.",
},
["some planks"] = {
aliases = { "planks", "wooden planks", "wood planks" },
desc = "You could easilly craft these planks into sticks.",
desc = "You could easily craft these planks into sticks.",
},
["some sticks"] = {
aliases = { "sticks", "wooden sticks", "wood sticks" },
@ -255,7 +255,7 @@ local items = {
["some pork"] = {
aliases = { "pork", "porkchops" },
food = true,
desc = "Delicious and nutricious.",
desc = "Delicious and nutritious.",
},
["some chicken"] = {
aliases = { "chicken" },
@ -1144,7 +1144,7 @@ function commands.help()
local sText =
"Welcome to adventure, the greatest text adventure game on CraftOS. " ..
"To get around the world, type actions, and the adventure will " ..
"be read back to you. The actions availiable to you are go, look, inspect, inventory, " ..
"be read back to you. The actions available to you are go, look, inspect, inventory, " ..
"take, drop, place, punch, attack, mine, dig, craft, build, eat and exit."
print(sText)
end

View File

@ -66,7 +66,7 @@ elseif sCommand == "host" then
print("Opening channel on modem " .. sModemSide)
modem.open(gps.CHANNEL_GPS)
-- Serve requests indefinately
-- Serve requests indefinitely
local nServed = 0
while true do
local e, p1, p2, p3, p4, p5 = os.pullEvent("modem_message")

View File

@ -18,7 +18,5 @@ if #files == 0 then
return
end
package.path = package.path .. ";/rom/modules/internal/?.lua"
local ok, err = require("cc.import")(files)
local ok, err = require("cc.internal.import")(files)
if not ok and err then printError(err) end

View File

@ -6,13 +6,14 @@ if #tArgs > 0 then
end
local pretty = require "cc.pretty"
local exception = require "cc.internal.exception"
local bRunning = true
local running = true
local tCommandHistory = {}
local tEnv = {
["exit"] = setmetatable({}, {
__tostring = function() return "Call exit() to exit." end,
__call = function() bRunning = false end,
__call = function() running = false end,
}),
["_echo"] = function(...)
return ...
@ -44,14 +45,15 @@ print("Interactive Lua prompt.")
print("Call exit() to exit.")
term.setTextColour(colours.white)
while bRunning do
local chunk_idx, chunk_map = 1, {}
while running do
--if term.isColour() then
-- term.setTextColour( colours.yellow )
--end
write("lua> ")
--term.setTextColour( colours.white )
local s = read(nil, tCommandHistory, function(sLine)
local input = read(nil, tCommandHistory, function(sLine)
if settings.get("lua.autocomplete") then
local nStartPos = string.find(sLine, "[a-zA-Z0-9_%.:]+$")
if nStartPos then
@ -63,10 +65,10 @@ while bRunning do
end
return nil
end)
if s:match("%S") and tCommandHistory[#tCommandHistory] ~= s then
table.insert(tCommandHistory, s)
if input:match("%S") and tCommandHistory[#tCommandHistory] ~= input then
table.insert(tCommandHistory, input)
end
if settings.get("lua.warn_against_use_of_local") and s:match("^%s*local%s+") then
if settings.get("lua.warn_against_use_of_local") and input:match("^%s*local%s+") then
if term.isColour() then
term.setTextColour(colours.yellow)
end
@ -74,27 +76,32 @@ while bRunning do
term.setTextColour(colours.white)
end
local nForcePrint = 0
local func, e = load(s, "=lua", "t", tEnv)
local func2 = load("return _echo(" .. s .. ");", "=lua", "t", tEnv)
local name, offset = "=lua[" .. chunk_idx .. "]", 0
local force_print = 0
local func, err = load(input, name, "t", tEnv)
local expr_func = load("return _echo(" .. input .. ");", name, "t", tEnv)
if not func then
if func2 then
func = func2
e = nil
nForcePrint = 1
end
else
if func2 then
func = func2
if expr_func then
func = expr_func
offset = 13
force_print = 1
end
elseif expr_func then
func = expr_func
offset = 13
end
if func then
local tResults = table.pack(pcall(func))
if tResults[1] then
chunk_map[name] = { contents = input, offset = offset }
chunk_idx = chunk_idx + 1
local results = table.pack(exception.try(func))
if results[1] then
local n = 1
while n < tResults.n or n <= nForcePrint do
local value = tResults[n + 1]
while n < results.n or n <= force_print do
local value = results[n + 1]
local ok, serialised = pcall(pretty.pretty, value, {
function_args = settings.get("lua.function_args"),
function_source = settings.get("lua.function_source"),
@ -107,10 +114,12 @@ while bRunning do
n = n + 1
end
else
printError(tResults[2])
printError(results[2])
require "cc.internal.exception".report(results[2], results[3], chunk_map)
end
else
printError(e)
local parser = require "cc.internal.syntax"
if parser.parse_repl(input) then printError(err) end
end
end

View File

@ -446,7 +446,7 @@ local function playGame()
end
end
end
--now remove the rows and drop everythign else
--now remove the rows and drop everything else
term.setBackgroundColor(colors.black)
for r = 1, #rows do
r = rows[r]

View File

@ -1,14 +1,32 @@
--- The shell API provides access to CraftOS's command line interface.
--
-- It allows you to @{run|start programs}, @{setCompletionFunction|add
-- completion for a program}, and much more.
--
-- @{shell} is not a "true" API. Instead, it is a standard program, which injects its
-- API into the programs that it launches. This allows for multiple shells to
-- run at the same time, but means that the API is not available in the global
-- environment, and so is unavailable to other @{os.loadAPI|APIs}.
--
-- @module[module] shell
--[[- The shell API provides access to CraftOS's command line interface.
It allows you to @{run|start programs}, @{setCompletionFunction|add completion
for a program}, and much more.
@{shell} is not a "true" API. Instead, it is a standard program, which injects
its API into the programs that it launches. This allows for multiple shells to
run at the same time, but means that the API is not available in the global
environment, and so is unavailable to other @{os.loadAPI|APIs}.
## Programs and the program path
When you run a command with the shell, either from the prompt or
@{shell.run|from Lua code}, the shell API performs several steps to work out
which program to run:
1. Firstly, the shell attempts to resolve @{shell.aliases|aliases}. This allows
us to use multiple names for a single command. For example, the `list`
program has two aliases: `ls` and `dir`. When you write `ls /rom`, that's
expanded to `list /rom`.
2. Next, the shell attempts to find where the program actually is. For this, it
uses the @{shell.path|program path}. This is a colon separated list of
directories, each of which is checked to see if it contains the program.
`list` or `list.lua` doesn't exist in `.` (the current directory), so she
shell now looks in `/rom/programs`, where `list.lua` can be found!
@module[module] shell
]]
local make_package = dofile("rom/modules/main/cc/require.lua").make
@ -37,10 +55,11 @@ 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 })
local env = setmetatable(createShellEnv("/rom/programs"), { __index = _ENV })
require = env.require
end
local expect = require("cc.expect").expect
local exception = require "cc.internal.exception"
-- Colours
local promptColour, textColour, bgColour
@ -54,6 +73,69 @@ else
bgColour = colours.black
end
local function tokenise(...)
local sLine = table.concat({ ... }, " ")
local tWords = {}
local bQuoted = false
for match in string.gmatch(sLine .. "\"", "(.-)\"") do
if bQuoted then
table.insert(tWords, match)
else
for m in string.gmatch(match, "[^ \t]+") do
table.insert(tWords, m)
end
end
bQuoted = not bQuoted
end
return tWords
end
local function executeProgram(path, args)
local file, err = fs.open(path, "r")
if not file then
printError(err)
return false
end
local contents = file.readAll() or ""
file.close()
local dir = fs.getDir(path)
local env = setmetatable(createShellEnv(dir), { __index = _G })
env.arg = args
local func, err = load(contents, "@/" .. path, nil, env)
if not func then
-- We had a syntax error. Attempt to run it through our own parser if
-- the file is "small enough", otherwise report the original error.
if #contents < 1024 * 128 then
local parser = require "cc.internal.syntax"
if parser.parse_program(contents) then printError(err) end
else
printError(err)
end
return false
end
if settings.get("bios.strict_globals", false) then
getmetatable(env).__newindex = function(_, name)
error("Attempt to create global " .. tostring(name), 2)
end
end
local ok, err, co = exception.try(func, table.unpack(args, 1, args.n))
if ok then return true end
if err and err ~= "" then
printError(err)
exception.report(err, co)
end
return false
end
--- Run a program with the supplied arguments.
--
-- Unlike @{shell.run}, each argument is passed to the program verbatim. While
@ -84,10 +166,7 @@ function shell.execute(command, ...)
multishell.setTitle(multishell.getCurrent(), sTitle)
end
local sDir = fs.getDir(sPath)
local env = createShellEnv(sDir)
env.arg = { [0] = command, ... }
local result = os.run(env, sPath, ...)
local result = executeProgram(sPath, { [0] = command, ... })
tProgramStack[#tProgramStack] = nil
if multishell then
@ -108,23 +187,6 @@ function shell.execute(command, ...)
end
end
local function tokenise(...)
local sLine = table.concat({ ... }, " ")
local tWords = {}
local bQuoted = false
for match in string.gmatch(sLine .. "\"", "(.-)\"") do
if bQuoted then
table.insert(tWords, match)
else
for m in string.gmatch(match, "[^ \t]+") do
table.insert(tWords, m)
end
end
bQuoted = not bQuoted
end
return tWords
end
-- Install shell API
--- Run a program with the supplied arguments.
@ -247,10 +309,11 @@ end
-- @treturn string|nil The absolute path to the program, or @{nil} if it could
-- not be found.
-- @since 1.2
-- @changed 1.80pr1 Now searches for files with and without the `.lua` extension.
-- @usage Locate the `hello` program.
--
-- shell.resolveProgram("hello")
-- -- => rom/programs/fun/hello.lua
-- shell.resolveProgram("hello")
-- -- => rom/programs/fun/hello.lua
function shell.resolveProgram(command)
expect(1, command, "string")
-- Substitute aliases firsts
@ -541,8 +604,8 @@ end
--- Get the current aliases for this shell.
--
-- Aliases are used to allow multiple commands to refer to a single program. For
-- instance, the `list` program is aliased `dir` or `ls`. Running `ls`, `dir` or
-- `list` in the shell will all run the `list` program.
-- instance, the `list` program is aliased to `dir` or `ls`. Running `ls`, `dir`
-- or `list` in the shell will all run the `list` program.
--
-- @treturn { [string] = string } A table, where the keys are the names of
-- aliases, and the values are the path to the program.
@ -655,7 +718,7 @@ else
term.setCursorBlink(false)
-- Run the import script with the provided files
local ok, err = require("cc.import")(event[2].getFiles())
local ok, err = require("cc.internal.import")(event[2].getFiles())
if not ok and err then printError(err) end
-- And attempt to restore the prompt.

View File

@ -93,13 +93,13 @@ local function collect()
return true
end
function refuel(ammount)
function refuel(amount)
local fuelLevel = turtle.getFuelLevel()
if fuelLevel == "unlimited" then
return true
end
local needed = ammount or xPos + zPos + depth + 2
local needed = amount or xPos + zPos + depth + 2
if turtle.getFuelLevel() < needed then
for n = 1, 16 do
if turtle.getItemCount(n) > 0 then

View File

@ -5,7 +5,6 @@
*/
package dan200.computercraft.core;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.api.filesystem.IWritableMount;
import dan200.computercraft.api.lua.ILuaAPI;
import dan200.computercraft.api.lua.LuaException;
@ -15,22 +14,30 @@
import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.core.filesystem.FileMount;
import dan200.computercraft.core.filesystem.FileSystemException;
import dan200.computercraft.core.lua.CobaltLuaMachine;
import dan200.computercraft.core.lua.MachineEnvironment;
import dan200.computercraft.core.lua.MachineResult;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.support.TestFiles;
import dan200.computercraft.test.core.computer.BasicEnvironment;
import dan200.computercraft.test.core.computer.FakeMainThreadScheduler;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import it.unimi.dsi.fastutil.ints.Int2IntArrayMap;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.function.Executable;
import org.opentest4j.AssertionFailedError;
import org.opentest4j.TestAbortedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.squiddev.cobalt.*;
import org.squiddev.cobalt.debug.DebugFrame;
import org.squiddev.cobalt.debug.DebugHook;
import org.squiddev.cobalt.debug.DebugState;
import org.squiddev.cobalt.function.OneArgFunction;
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.io.*;
import java.lang.reflect.Field;
import java.net.URI;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
@ -43,6 +50,7 @@
import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
@ -60,7 +68,7 @@ public class ComputerTestDelegate
{
private static final Path REPORT_PATH = TestFiles.get( "luacov.report.out" );
private static final Logger LOG = LogManager.getLogger( ComputerTestDelegate.class );
private static final Logger LOG = LoggerFactory.getLogger( ComputerTestDelegate.class );
private static final long TICK_TIME = TimeUnit.MILLISECONDS.toNanos( 50 );
@ -86,14 +94,12 @@ public class ComputerTestDelegate
private final Condition hasFinished = lock.newCondition();
private boolean finished = false;
private Map<String, Map<Double, Double>> finishedWith;
private final Map<LuaString, Int2IntArrayMap> coverage = new HashMap<>();
@BeforeEach
public void before() throws IOException
{
ComputerCraft.logComputerErrors = true;
if( Files.deleteIfExists( REPORT_PATH ) ) ComputerCraft.log.info( "Deleted previous coverage report." );
if( Files.deleteIfExists( REPORT_PATH ) ) LOG.info( "Deleted previous coverage report." );
Terminal term = new Terminal( 80, 100, true );
IWritableMount mount = new FileMount( TestFiles.get( "mount" ).toFile(), 10_000_000 );
@ -157,12 +163,14 @@ public void after() throws InterruptedException, IOException
computer.shutdown();
}
if( finishedWith != null )
if( !coverage.isEmpty() )
{
Files.createDirectories( REPORT_PATH.getParent() );
try( BufferedWriter writer = Files.newBufferedWriter( REPORT_PATH ) )
{
new LuaCoverage( finishedWith ).write( writer );
new LuaCoverage( coverage.entrySet().stream().collect( Collectors.toMap(
x -> x.getKey().substring( 1 ).toString(), Map.Entry::getValue
) ) ).write( writer );
}
}
}
@ -174,7 +182,7 @@ public Stream<DynamicNode> get() throws InterruptedException
try
{
long remaining = TIMEOUT;
while( remaining > 0 & tests == null )
while( remaining > 0 && tests == null )
{
tick();
if( hasTests.awaitNanos( TICK_TIME ) > 0 ) break;
@ -231,7 +239,12 @@ DynamicNodeBuilder get( String name )
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 );
if( children.containsKey( name ) )
{
int i = 1;
while( children.containsKey( name + i ) ) i++;
name = name + i;
}
children.put( name, new DynamicNodeBuilder( name, uri, executor ) );
}
@ -290,7 +303,6 @@ private static String formatName( String name )
public static class FakeModem implements IPeripheral
{
@Nonnull
@Override
public String getType()
{
@ -312,7 +324,6 @@ public final boolean isOpen( int channel )
public static class FakePeripheralHub implements IPeripheral
{
@Nonnull
@Override
public String getType()
{
@ -464,10 +475,6 @@ public final void submit( Map<?, ?> tbl )
{
// Submit the result of a test, allowing the test executor to continue
String name = (String) tbl.get( "name" );
if( name == null )
{
ComputerCraft.log.error( "Oh no: {}", tbl );
}
String status = (String) tbl.get( "status" );
String message = (String) tbl.get( "message" );
String trace = (String) tbl.get( "trace" );
@ -502,7 +509,9 @@ public final void submit( Map<?, ?> tbl )
switch( status )
{
case "ok":
break;
case "pending":
runResult = new TestAbortedException( "Test is pending" );
break;
case "fail":
runResult = new AssertionFailedError( wholeMessage.toString() );
@ -522,10 +531,8 @@ public final void submit( Map<?, ?> tbl )
}
@LuaFunction
public final void finish( Optional<Map<?, ?>> result )
public final void finish()
{
@SuppressWarnings( "unchecked" )
Map<String, Map<Double, Double>> finishedResult = (Map<String, Map<Double, Double>>) result.orElse( null );
LOG.info( "Finished" );
// Signal to after that execution has finished
@ -540,7 +547,6 @@ public final void finish( Optional<Map<?, ?>> result )
try
{
finished = true;
if( finishedResult != null ) finishedWith = finishedResult;
hasFinished.signal();
}
@ -550,4 +556,86 @@ public final void finish( Optional<Map<?, ?>> result )
}
}
}
/**
* A subclass of {@link CobaltLuaMachine} which tracks coverage for executed files.
* <p>
* This is a super nasty hack, but is also an order of magnitude faster than tracking this in Lua.
*/
private class CoverageLuaMachine extends CobaltLuaMachine
{
CoverageLuaMachine( MachineEnvironment environment )
{
super( environment );
}
@Override
public MachineResult loadBios( InputStream bios )
{
MachineResult result = super.loadBios( bios );
if( result != MachineResult.OK ) return result;
LuaTable globals;
LuaThread mainRoutine;
try
{
Field globalField = CobaltLuaMachine.class.getDeclaredField( "globals" );
globalField.setAccessible( true );
globals = (LuaTable) globalField.get( this );
Field threadField = CobaltLuaMachine.class.getDeclaredField( "mainRoutine" );
threadField.setAccessible( true );
mainRoutine = (LuaThread) threadField.get( this );
}
catch( ReflectiveOperationException e )
{
throw new RuntimeException( "Cannot get internal Cobalt state", e );
}
Map<LuaString, Int2IntArrayMap> coverage = ComputerTestDelegate.this.coverage;
DebugHook hook = new DebugHook()
{
@Override
public void onCall( LuaState state, DebugState ds, DebugFrame frame )
{
}
@Override
public void onReturn( LuaState state, DebugState ds, DebugFrame frame )
{
}
@Override
public void onCount( LuaState state, DebugState ds, DebugFrame frame )
{
}
@Override
public void onLine( LuaState state, DebugState ds, DebugFrame frame, int newLine )
{
if( frame.closure == null ) return;
Prototype proto = frame.closure.getPrototype();
if( !proto.source.startsWith( '@' ) ) return;
Int2IntMap map = coverage.computeIfAbsent( proto.source, x -> new Int2IntArrayMap() );
map.put( newLine, map.get( newLine ) + 1 );
}
};
((LuaTable) globals.rawget( "coroutine" )).rawset( "create", new OneArgFunction()
{
@Override
public LuaValue call( LuaState state, LuaValue arg ) throws LuaError
{
LuaThread thread = new LuaThread( state, arg.checkFunction(), state.getCurrentThread().getfenv() );
thread.getDebugState().setHook( hook, false, true, false, 0 );
return thread;
}
} );
mainRoutine.getDebugState().setHook( hook, false, true, false, 0 );
return MachineResult.OK;
}
}
}

View File

@ -6,9 +6,12 @@
package dan200.computercraft.core;
import com.google.common.base.Strings;
import dan200.computercraft.ComputerCraft;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.ints.Int2IntMaps;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.squiddev.cobalt.Prototype;
import org.squiddev.cobalt.compiler.CompileException;
import org.squiddev.cobalt.compiler.LuaC;
@ -16,30 +19,27 @@
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.ArrayDeque;
import java.util.Collections;
import java.util.Map;
import java.util.Queue;
class LuaCoverage
{
private static final Logger LOG = LoggerFactory.getLogger( LuaCoverage.class );
private static final Path ROOT = new File( "src/main/resources/data/computercraft/lua" ).toPath();
private static final Path BIOS = ROOT.resolve( "bios.lua" );
private static final Path APIS = ROOT.resolve( "rom/apis" );
private static final Path SHELL = ROOT.resolve( "rom/programs/shell.lua" );
private static final Path MULTISHELL = ROOT.resolve( "rom/programs/advanced/multishell.lua" );
private static final Path TREASURE = ROOT.resolve( "treasure" );
private final Map<String, Map<Double, Double>> coverage;
private final Map<String, Int2IntMap> coverage;
private final String blank;
private final String zero;
private final String countFormat;
LuaCoverage( Map<String, Map<Double, Double>> coverage )
LuaCoverage( Map<String, Int2IntMap> coverage )
{
this.coverage = coverage;
int max = (int) coverage.values().stream()
.flatMapToDouble( x -> x.values().stream().mapToDouble( y -> y ) )
int max = coverage.values().stream()
.flatMapToInt( x -> x.values().stream().mapToInt( y -> y ) )
.max().orElse( 0 );
int maxLen = Math.max( 1, (int) Math.ceil( Math.log10( max ) ) );
blank = Strings.repeat( " ", maxLen + 1 );
@ -49,25 +49,22 @@ class LuaCoverage
void write( Writer out ) throws IOException
{
Files.find( ROOT, Integer.MAX_VALUE, ( path, attr ) -> attr.isRegularFile() && !path.startsWith( TREASURE ) ).forEach( path -> {
Files.find( ROOT, Integer.MAX_VALUE, ( path, attr ) -> attr.isRegularFile() ).forEach( path -> {
Path relative = ROOT.relativize( path );
String full = relative.toString().replace( '\\', '/' );
if( !full.endsWith( ".lua" ) ) return;
Map<Double, Double> files = Stream.of(
coverage.remove( "/" + full ),
path.equals( BIOS ) ? coverage.remove( "bios.lua" ) : null,
path.equals( SHELL ) ? coverage.remove( "shell.lua" ) : null,
path.equals( MULTISHELL ) ? coverage.remove( "multishell.lua" ) : null,
path.startsWith( APIS ) ? coverage.remove( path.getFileName().toString() ) : null
)
.filter( Objects::nonNull )
.flatMap( x -> x.entrySet().stream() )
.collect( Collectors.toMap( Map.Entry::getKey, Map.Entry::getValue, Double::sum ) );
Int2IntMap possiblePaths = coverage.remove( "/" + full );
if( possiblePaths == null ) possiblePaths = coverage.remove( full );
if( possiblePaths == null )
{
possiblePaths = Int2IntMaps.EMPTY_MAP;
LOG.warn( "{} has no coverage data", full );
}
try
{
writeCoverageFor( out, path, files );
writeCoverageFor( out, path, possiblePaths );
}
catch( IOException e )
{
@ -78,15 +75,15 @@ void write( Writer out ) throws IOException
for( String filename : coverage.keySet() )
{
if( filename.startsWith( "/test-rom/" ) ) continue;
ComputerCraft.log.warn( "Unknown file {}", filename );
LOG.warn( "Unknown file {}", filename );
}
}
private void writeCoverageFor( Writer out, Path fullName, Map<Double, Double> visitedLines ) throws IOException
private void writeCoverageFor( Writer out, Path fullName, Int2IntMap visitedLines ) throws IOException
{
if( !Files.exists( fullName ) )
{
ComputerCraft.log.error( "Cannot locate file {}", fullName );
LOG.error( "Cannot locate file {}", fullName );
return;
}
@ -104,10 +101,10 @@ private void writeCoverageFor( Writer out, Path fullName, Map<Double, Double> vi
while( (line = reader.readLine()) != null )
{
lineNo++;
Double count = visitedLines.get( (double) lineNo );
if( count != null )
int count = visitedLines.getOrDefault( lineNo, -1 );
if( count >= 0 )
{
out.write( String.format( countFormat, count.intValue() ) );
out.write( String.format( countFormat, count ) );
}
else if( activeLines.contains( lineNo ) )
{
@ -128,30 +125,32 @@ else if( activeLines.contains( lineNo ) )
private static IntSet getActiveLines( File file ) throws IOException
{
IntSet activeLines = new IntOpenHashSet();
try( InputStream stream = new FileInputStream( file ) )
Queue<Prototype> queue = new ArrayDeque<>();
try( InputStream stream = Files.newInputStream( file.toPath() ) )
{
Prototype proto = LuaC.compile( stream, "@" + file.getPath() );
Queue<Prototype> queue = new ArrayDeque<>();
queue.add( proto );
while( (proto = queue.poll()) != null )
{
int[] lines = proto.lineinfo;
if( lines != null )
{
for( int line : lines )
{
activeLines.add( line );
}
}
if( proto.p != null ) Collections.addAll( queue, proto.p );
}
}
catch( CompileException e )
{
throw new IllegalStateException( "Cannot compile", e );
}
Prototype proto;
while( (proto = queue.poll()) != null )
{
int[] lines = proto.lineInfo;
if( lines != null )
{
for( int line : lines )
{
activeLines.add( line );
}
}
if( proto.children != null ) Collections.addAll( queue, proto.children );
}
return activeLines;
}
}

View File

@ -29,7 +29,7 @@ public void testTimeout()
}
catch( AssertionError e )
{
if( e.getMessage().equals( "test.lua:1: Too long without yielding" ) ) return;
if( e.getMessage().equals( "/test.lua:1: Too long without yielding" ) ) return;
throw e;
}

View File

@ -12,6 +12,8 @@
import dan200.computercraft.core.apis.handles.EncodedWritableHandle;
import dan200.computercraft.support.TestFiles;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import java.io.BufferedWriter;
import java.io.File;
@ -79,4 +81,23 @@ public void testUnmountCloses() throws FileSystemException
LuaException err = assertThrows( LuaException.class, () -> wrapper.call( "write", "Tiny line" ) );
assertEquals( "attempt to use a closed file", err.getMessage() );
}
@ParameterizedTest( name = "{0}" )
@MethodSource( "sanitiseCases" )
public void testSanitize( String input, String output )
{
assertEquals( output, FileSystem.sanitizePath( input, false ) );
}
public static String[][] sanitiseCases()
{
return new String[][] {
new String[] { "a//b", "a/b" },
new String[] { "a/./b", "a/b" },
new String[] { "a/../b", "b" },
new String[] { "a/.../b", "a/b" },
new String[] { " a ", "a" },
new String[] { "a b c", "a b c" },
};
}
}

View File

@ -1,7 +1,7 @@
# JSON Parsing Test Suite
This is a collection of JSON test cases from [nst/JSONTestSuite][gh]. We simply
determine whether an object is succesfully parsed or not, and do not check the
determine whether an object is successfully parsed or not, and do not check the
contents.
See `LICENSE` for copyright information.

View File

@ -182,8 +182,12 @@ end
-- @treturn string The formatted value
local function format(value)
-- TODO: Look into something like mbs's pretty printer.
local ok, res = pcall(textutils.serialise, value)
if ok then return res else return tostring(value) end
if type(value) == "string" and value:find("\n") then
return "<<<\n" .. value .. "\n>>>"
else
local ok, res = pcall(textutils.serialise, value)
if ok then return res else return tostring(value) end
end
end
local expect_mt = {}
@ -417,6 +421,9 @@ end
--- The stack of "describe"s.
local test_stack = { n = 0 }
--- The stack of setup functions.
local before_each_fns = { n = 0 }
--- Whether we're now running tests, and so cannot run any more.
local tests_locked = false
@ -455,8 +462,14 @@ local function describe(name, body)
local n = test_stack.n + 1
test_stack[n], test_stack.n = name, n
local old_before, new_before = before_each_fns, { n = before_each_fns.n }
for i = 1, old_before.n do new_before[i] = old_before[i] end
before_each_fns = new_before
local ok, err = try(body)
before_each_fns = old_before
-- We count errors as a (failing) test.
if not ok then do_test { error = err, definition = format_loc(debug.getinfo(2, "Sl")) } end
@ -477,7 +490,11 @@ 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, definition = format_loc(debug.getinfo(2, "Sl")) }
do_test {
action = body,
before = before_each_fns,
definition = format_loc(debug.getinfo(2, "Sl")),
}
-- Pop the test from the stack
test_stack.n, tests_locked = n - 1, false
@ -498,26 +515,17 @@ local function pending(name)
test_stack.n = n - 1
end
local native_co_create, native_loadfile = coroutine.create, loadfile
local function before_each(body)
check('it', 1, 'function', body)
if tests_locked then error("Cannot define before_each while running tests", 2) end
local n = before_each_fns.n + 1
before_each_fns[n], before_each_fns.n = body, n
end
local native_loadfile = loadfile
local line_counts = {}
if cct_test then
local string_sub, debug_getinfo = string.sub, debug.getinfo
local function debug_hook(_, line_nr)
local name = debug_getinfo(2, "S").source
if string_sub(name, 1, 1) ~= "@" then return end
name = string_sub(name, 2)
local file = line_counts[name]
if not file then file = {} line_counts[name] = file end
file[line_nr] = (file[line_nr] or 0) + 1
end
coroutine.create = function(...)
local co = native_co_create(...)
debug.sethook(co, debug_hook, "l")
return co
end
local expect = require "cc.expect".expect
_G.native_loadfile = native_loadfile
_G.loadfile = function(filename, mode, env)
@ -537,8 +545,6 @@ if cct_test then
file.close()
return func, err
end
debug.sethook(debug_hook, "l")
end
local arg = ...
@ -559,16 +565,11 @@ end
package.path = ("/%s/?.lua;/%s/?/init.lua;%s"):format(root_dir, root_dir, package.path)
do
-- Load in the tests from all our files
local env = setmetatable({}, { __index = _ENV })
local function set_env(tbl)
for k in pairs(env) do env[k] = nil end
for k, v in pairs(tbl) do env[k] = v end
end
-- When declaring tests, you shouldn't be able to use test methods
set_env { describe = describe, it = it, pending = pending }
-- Add our new functions to the current environment.
for k, v in pairs {
describe = describe, it = it, pending = pending, before_each = before_each,
expect = expect, fail = fail,
} do _ENV[k] = v end
local suffix = "_spec.lua"
local function run_in(sub_dir)
@ -577,7 +578,7 @@ do
if fs.isDir(file) then
run_in(file)
elseif file:sub(-#suffix) == suffix then
local fun, err = loadfile(file, nil, env)
local fun, err = loadfile(file, nil, _ENV)
if not fun then
do_test { name = file:sub(#root_dir + 2), error = { message = err } }
else
@ -590,8 +591,8 @@ do
run_in(root_dir)
-- When running tests, you shouldn't be able to declare new ones.
set_env { expect = expect, fail = fail, stub = stub }
-- Add stub later on, so its not available when running tests
_ENV.stub = stub
end
-- Error if we've found no tests
@ -630,8 +631,13 @@ local function do_run(test)
-- Flush the event queue and ensure we're running with 0 timeout.
os.queueEvent("start_test") os.pullEvent("start_test")
local ok
ok, err = try(test.action)
local ok = true
for i = 1, test.before.n do
if not ok then break end
ok, err = try(test.before[i])
end
if ok then ok, err = try(test.action) end
status = ok and "pass" or (err.fail and "fail" or "error")
pop_state(state)
@ -711,8 +717,6 @@ end
term.setTextColour(colours.white) io.write(info .. "\n")
-- Restore hook stubs
debug.sethook(nil, "l")
coroutine.create = native_co_create
_G.loadfile = native_loadfile
if cct_test then cct_test.finish(line_counts) end

View File

@ -1,4 +1,8 @@
describe("The fs library", function()
local test_root = "/test-files/fs"
local function test_file(path) return fs.combine(test_root, path) end
before_each(function() fs.delete(test_root) end)
describe("fs.complete", function()
it("validates arguments", function()
fs.complete("", "")
@ -139,7 +143,7 @@ describe("The fs library", function()
end)
it("errors when closing twice", function()
local handle = fs.open("test-files/out.txt", "w")
local handle = fs.open(test_file "out.txt", "w")
handle.close()
expect.error(handle.close):eq("attempt to use a closed file")
end)
@ -216,6 +220,48 @@ describe("The fs library", function()
expect.error(fs.move, "test-files", "rom/move"):eq("Access denied")
expect.error(fs.move, "rom", "test-files"):eq("Access denied")
end)
it("fails if source does not exist", function()
expect.error(fs.move, test_file "src", test_file "dest"):eq("No such file")
end)
it("fails if destination exists", function()
fs.open(test_file "src", "w").close()
fs.open(test_file "dest", "w").close()
expect.error(fs.move, test_file "src", test_file "dest"):eq("File exists")
end)
it("fails to move a directory inside itself", function()
fs.open(test_file "file", "w").close()
expect.error(fs.move, test_root, test_file "child"):eq("Can't move a directory inside itself")
expect.error(fs.move, "", "child"):eq("Can't move a directory inside itself")
end)
it("files can be renamed", function()
fs.open(test_file "src", "w").close()
fs.move(test_file "src", test_file "dest")
expect(fs.exists(test_file "src")):eq(false)
expect(fs.exists(test_file "dest")):eq(true)
end)
it("directories can be renamed", function()
fs.open(test_file "src/some/file", "w").close()
fs.move(test_file "src", test_file "dest")
expect(fs.exists(test_file "src")):eq(false)
expect(fs.exists(test_file "dest")):eq(true)
expect(fs.exists(test_file "dest/some/file")):eq(true)
end)
it("creates directories before renaming", function()
fs.open(test_file "src", "w").close()
fs.move(test_file "src", test_file "dest/file")
expect(fs.exists(test_file "src")):eq(false)
expect(fs.exists(test_file "dest/file")):eq(true)
end)
end)
describe("fs.getCapacity", function()
@ -240,12 +286,11 @@ describe("The fs library", function()
it("returns information about files", function()
local now = os.epoch("utc")
fs.delete("/tmp/basic-file")
local h = fs.open("/tmp/basic-file", "w")
local h = fs.open(test_file "basic-file", "w")
h.write("A reasonably sized string")
h.close()
local attributes = fs.attributes("tmp/basic-file")
local attributes = fs.attributes(test_file "basic-file")
expect(attributes):matches { isDir = false, size = 25, isReadOnly = false }
if attributes.created - now >= 1000 then

View File

@ -1,3 +1,5 @@
local timeout = require "test_helpers".timeout
describe("The http library", function()
describe("http.checkURL", function()
it("accepts well formed domains", function()
@ -18,4 +20,28 @@ describe("The http library", function()
expect({ http.checkURL("http://127.0.0.1") }):same({ false, "Domain not permitted" })
end)
end)
describe("http.websocketAsync", function()
it("queues an event for immediate failures", function()
timeout(1, function()
local url = "http://not.a.websocket"
http.websocketAsync(url)
local _, url2, message = os.pullEvent("websocket_failure")
expect(url2):eq(url)
expect(message):eq("Invalid scheme 'http'")
end)
end)
end)
describe("http.requestAsync", function()
it("queues an event for immediate failures", function()
timeout(1, function()
local url = "ws://not.a.request"
http.request(url)
local _, url2, message = os.pullEvent("http_failure")
expect(url2):eq(url)
expect(message):eq("Invalid protocol 'ws'")
end)
end)
end)
end)

View File

@ -95,6 +95,9 @@ describe("The os library", function()
exp_code("%Y", "2000")
exp_code("%%", "%")
it("%r at 12 AM", function() expect(os.date("%r", 1670373922)):eq("12:45:22 AM") end)
it("%I at 12 AM", function() expect(os.date("%I", 1670373922)):eq("12") end)
it("zones are numbers", function()
local zone = os.date("%z", t1)
if not zone:match("^[+-]%d%d%d%d$") then

View File

@ -28,8 +28,6 @@ describe("The Lua base library", function()
end)
describe("loadfile", function()
local loadfile = _G.native_loadfile or loadfile
local function make_file()
local tmp = fs.open("test-files/out.lua", "w")
tmp.write("return _ENV")
@ -48,7 +46,7 @@ describe("The Lua base library", function()
it("prefixes the filename with @", function()
local info = debug.getinfo(loadfile("/rom/startup.lua"), "S")
expect(info):matches { short_src = "startup.lua", source = "@startup.lua" }
expect(info):matches { short_src = "/rom/startup.lua", source = "@/rom/startup.lua" }
end)
it("loads a file with the global environment", function()

View File

@ -0,0 +1,52 @@
local helpers = require "test_helpers"
describe("cc.internal.syntax", function()
local syntax = require "cc.internal.syntax"
local parser = require "cc.internal.syntax.parser"
local syntax_helpers = require "modules.cc.internal.syntax.syntax_helpers"
describe("can parse all of CC's Lua files", function()
local function list_dir(path)
if not path then path = "/" end
for _, child in pairs(fs.list(path)) do
child = fs.combine(path, child)
if fs.isDir(child) then list_dir(child)
elseif child:sub(-4) == ".lua" then coroutine.yield(child)
end
end
end
for file in coroutine.wrap(list_dir) do
it(file, function()
helpers.with_window(50, 10, function()
local h = fs.open(file, "r")
local contents = h.readAll()
h.close()
expect(syntax.parse_program(contents)):describe(file):eq(true)
end)
end)
end
end)
-- We specify most of the parser's behaviour as golden tests. A little nasty
-- (it's more of an end-to-end test), but much easier to write!
local function describe_golden(name, path, print_tokens)
helpers.describe_golden(name, "test-rom/spec/modules/cc/internal/syntax/" .. path, function(lua, extra)
local start = nil
if #extra > 0 then
start = parser[extra:match("^{([a-z_]+)}$")]
if not start then
fail("Cannot extract start symbol " .. extra)
end
end
return syntax_helpers.capture_parser(lua, print_tokens, start)
end)
end
describe_golden("the lexer", "lexer_spec.md", true)
describe_golden("the parser", "parser_spec.md", false)
describe_golden("the parser (all states)", "parser_exhaustive_spec.md", false)
end)

View File

@ -0,0 +1,319 @@
We provide a lexer for Lua source code. Here we test that the lexer returns the
correct tokens and positions, and that it can report sensible error messages.
# Comments
## Single-line comments
We can lex some basic comments:
```lua
-- A basic singleline comment comment
--[ Not a multiline comment
--[= Also not a multiline comment!
```
```txt
1:1-1:37 COMMENT -- A basic singleline comment comment
2:1-2:27 COMMENT --[ Not a multiline comment
3:1-3:34 COMMENT --[= Also not a multiline comment!
```
It's also useful to test empty comments (including no trailing newline) separately:
```lua
--
```
```txt
1:1-1:2 COMMENT --
```
## Multi-line comments
Multiline/long-string-style comments are also supported:
```lua
--[[
A
multiline
comment
]]
--[=[ ]==] ]] ]=]
--[[ ]=]]
```
```txt
1:1-5:2 COMMENT --[[<NL> A<NL> multiline<NL> comment<NL>]]
7:1-7:18 COMMENT --[=[ ]==] ]] ]=]
9:1-9:9 COMMENT --[[ ]=]]
```
We also fail on unfinished comments:
```lua
--[=[
```
```txt
This comment was never finished.
|
1 | --[=[
| ^^^^^ Comment was started here.
We expected a closing delimiter (]=]) somewhere after this comment was started.
1:1-1:5 ERROR --[=[
```
Nested comments are rejected, just as Lua 5.1 does:
```lua
--[[ [[ ]]
```
```txt
[[ cannot be nested inside another [[ ... ]]
|
1 | --[[ [[ ]]
| ^^
1:1-1:10 COMMENT --[[ [[ ]]
```
# Strings
We can lex basic strings:
```lua
return "abc", "abc\"", 'abc', 'abc\z
', "abc\
continued"
```
```txt
1:1-1:6 RETURN return
1:8-1:12 STRING "abc"
1:13-1:13 COMMA ,
1:15-1:21 STRING "abc\""
1:22-1:22 COMMA ,
1:24-1:28 STRING 'abc'
1:29-1:29 COMMA ,
1:31-3:1 STRING 'abc\z<NL><NL>'
3:2-3:2 COMMA ,
3:4-4:10 STRING "abc\<NL>continued"
```
We also can lex unterminated strings, including those where there's no closing
quote:
```lua
return "abc
```
```txt
1:1-1:6 RETURN return
This string is not finished. Are you missing a closing quote (")?
|
1 | return "abc
| ^ String started here.
|
1 | return "abc
| ^ Expected a closing quote here.
1:8-1:11 STRING "abc
```
And those where the zap is malformed:
```lua
return "abc\z
```
```txt
1:1-1:6 RETURN return
This string is not finished. Are you missing a closing quote (")?
|
1 | return "abc\z
| ^ String started here.
|
1 | return "abc\z
| ^ Expected a closing quote here.
1:8-1:14 STRING "abc\z<NL>
```
Finally, strings where the escape is entirely missing:
```lua
return "abc\
```
```txt
1:1-1:6 RETURN return
This string is not finished.
|
1 | return "abc\
| ^ String started here.
|
1 | return "abc\
| ^ An escape sequence was started here, but with nothing following it.
1:8-1:12 STRING "abc\
```
## Multi-line/long strings
We can also handle long strings fine
```lua
return [[a b c]], [=[a b c ]=]
```
```txt
1:1-1:6 RETURN return
1:8-1:16 STRING [[a b c]]
1:17-1:17 COMMA ,
1:19-1:30 STRING [=[a b c ]=]
```
Unfinished long strings are correctly reported:
```lua
return [[
```
```txt
1:1-1:6 RETURN return
This string was never finished.
|
1 | return [[
| ^^ String was started here.
We expected a closing delimiter (]]) somewhere after this string was started.
1:8-1:9 ERROR [[
```
We also handle malformed opening strings:
```lua
return [=
```
```txt
1:1-1:6 RETURN return
Incorrect start of a long string.
|
1 | return [=
| ^^^
Tip: If you wanted to start a long string here, add an extra [ here.
1:8-1:10 ERROR [=
```
# Numbers
```lua
return 0, 0.0, 0e1, .23, 0x23, 23e-2, 23e+2
```
```txt
1:1-1:6 RETURN return
1:8-1:8 NUMBER 0
1:9-1:9 COMMA ,
1:11-1:13 NUMBER 0.0
1:14-1:14 COMMA ,
1:16-1:18 NUMBER 0e1
1:19-1:19 COMMA ,
1:21-1:23 NUMBER .23
1:24-1:24 COMMA ,
1:26-1:29 NUMBER 0x23
1:30-1:30 COMMA ,
1:32-1:36 NUMBER 23e-2
1:37-1:37 COMMA ,
1:39-1:43 NUMBER 23e+2
```
We also handle malformed numbers:
```lua
return 2..3, 2eee2
```
```txt
1:1-1:6 RETURN return
This isn't a valid number.
|
1 | return 2..3, 2eee2
| ^^^^
Numbers must be in one of the following formats: 123, 3.14, 23e35, 0x01AF.
1:8-1:11 NUMBER 2..3
1:12-1:12 COMMA ,
This isn't a valid number.
|
1 | return 2..3, 2eee2
| ^^^^^
Numbers must be in one of the following formats: 123, 3.14, 23e35, 0x01AF.
1:14-1:18 NUMBER 2eee2
```
# Unknown tokens
We can suggest alternatives for possible errors:
```lua
if a != b then end
if a ~= b then end
if a && b then end
if a || b then end
```
```txt
1:1-1:2 IF if
1:4-1:4 IDENT a
Unexpected character.
|
1 | if a != b then end
| ^^
Tip: Replace this with ~= to check if two values are not equal.
1:6-1:7 NE !=
1:9-1:9 IDENT b
1:11-1:14 THEN then
1:16-1:18 END end
2:1-2:2 IF if
2:4-2:4 IDENT a
2:6-2:7 NE ~=
2:9-2:9 IDENT b
2:11-2:14 THEN then
2:16-2:18 END end
3:1-3:2 IF if
3:4-3:4 IDENT a
Unexpected character.
|
3 | if a && b then end
| ^^
Tip: Replace this with and to check if both values are true.
3:6-3:7 AND &&
3:9-3:9 IDENT b
3:11-3:14 THEN then
3:16-3:18 END end
4:1-4:2 IF if
4:4-4:4 IDENT a
Unexpected character.
|
4 | if a || b then end
| ^^
Tip: Replace this with or to check if either value is true.
4:6-4:7 OR ||
4:9-4:9 IDENT b
4:11-4:14 THEN then
4:16-4:18 END end
```
For entirely unknown glyphs we should just give up and return an `ERROR` token.
```lua
return $*&(*)xyz
```
```txt
1:1-1:6 RETURN return
Unexpected character.
|
1 | return $*&(*)xyz
| ^ This character isn't usable in Lua code.
1:8-1:10 ERROR $*&
```

View File

@ -0,0 +1,370 @@
We provide a parser for Lua source code. Here we test that the parser reports
sensible syntax errors in specific cases.
# Expressions
## Invalid equals
We correct the user if they type `=` instead of `==`.
```lua
if a = b then end
```
```txt
Unexpected = in expression.
|
1 | if a = b then end
| ^
Tip: Replace this with == to check if two values are equal.
```
We apply a slightly different error when this occurs in tables:
```lua
return { "abc" = "def" }
```
```txt
Unexpected = in expression.
|
1 | return { "abc" = "def" }
| ^
Tip: Wrap the preceding expression in [ and ] to use it as a table key.
```
Note this doesn't occur if this there's already a table key here:
```lua
return { x = "abc" = }
```
```txt
Unexpected = in expression.
|
1 | return { x = "abc" = }
| ^
Tip: Replace this with == to check if two values are equal.
```
## Unclosed parenthesis
We warn on unclosed parenthesis in expressions:
```lua
return (2
```
```txt
Unexpected end of file. Are you missing a closing bracket?
|
1 | return (2
| ^ Brackets were opened here.
|
1 | return (2
| ^ Unexpected end of file here.
```
Function calls:
```lua
return f(2
```
```txt
Unexpected end of file. Are you missing a closing bracket?
|
1 | return f(2
| ^ Brackets were opened here.
|
1 | return f(2
| ^ Unexpected end of file here.
```
and function definitions:
```lua
local function f(a
```
```txt
Unexpected end of file. Are you missing a closing bracket?
|
1 | local function f(a
| ^ Brackets were opened here.
|
1 | local function f(a
| ^ Unexpected end of file here.
```
# Statements
## Local functions with table identifiers
We provide a custom error for using `.` inside a `local function` name.
```lua
local function x.f() end
```
```txt
Cannot use local function with a table key.
|
1 | local function x.f() end
| ^ . appears here.
|
1 | local function x.f() end
| ^^^^^ Tip: Try removing this local keyword.
```
## Standalone identifiers
A common error is a user forgetting to use `()` to call a function. We provide
a custom error for this case:
```lua
term.clear
local _ = 1
```
```txt
Unexpected symbol after variable.
|
1 | term.clear
| ^ Expected something before the end of the line.
Tip: Use () to call with no arguments.
```
If the next symbol is on the same line we provide a slightly different error:
```lua
x 1
```
```txt
Unexpected symbol after name.
|
1 | x 1
| ^
Did you mean to assign this or call it as a function?
```
An EOF token is treated as a new line.
```lua
term.clear
```
```txt
Unexpected symbol after variable.
|
1 | term.clear
| ^ Expected something before the end of the line.
Tip: Use () to call with no arguments.
```
## If statements
For if statements, we say when we expected the `then` keyword.
```lua
if 0
```
```txt
Expected then after if condition.
|
1 | if 0
| ^^ If statement started here.
|
1 | if 0
| ^ Expected then before here.
```
```lua
if 0 then
elseif 0
```
```txt
Expected then after if condition.
|
2 | elseif 0
| ^^^^^^ If statement started here.
|
2 | elseif 0
| ^ Expected then before here.
```
## Expecting `end`
We provide errors for missing `end`s.
```lua
if true then
print("Hello")
```
```txt
Unexpected end of file. Expected end or another statement.
|
1 | if true then
| ^^ Block started here.
|
2 | print("Hello")
| ^ Expected end of block here.
```
```lua
if true then
else
print("Hello")
```
```txt
Unexpected end of file. Expected end or another statement.
|
2 | else
| ^^^^ Block started here.
|
3 | print("Hello")
| ^ Expected end of block here.
```
```lua
if true then
elseif true then
print("Hello")
```
```txt
Unexpected end of file. Expected end or another statement.
|
2 | elseif true then
| ^^^^^^ Block started here.
|
3 | print("Hello")
| ^ Expected end of block here.
```
```lua
while true do
print("Hello")
```
```txt
Unexpected end of file. Expected end or another statement.
|
1 | while true do
| ^^^^^ Block started here.
|
2 | print("Hello")
| ^ Expected end of block here.
```
```lua
local function f()
```
```txt
Unexpected end of file. Expected end or another statement.
|
1 | local function f()
| ^^^^^^^^^^^^^^ Block started here.
|
1 | local function f()
| ^ Expected end of block here.
```
```lua
function f()
```
```txt
Unexpected end of file. Expected end or another statement.
|
1 | function f()
| ^^^^^^^^ Block started here.
|
1 | function f()
| ^ Expected end of block here.
```
```lua
return function()
```
```txt
Unexpected end of file. Expected end or another statement.
|
1 | return function()
| ^^^^^^^^ Block started here.
|
1 | return function()
| ^ Expected end of block here.
```
While we typically see these errors at the end of the file, there are some cases
where it may occur before then:
```lua
return (function()
if true then
)()
```
```txt
Unexpected ). Expected end or another statement.
|
2 | if true then
| ^^ Block started here.
|
3 | )()
| ^ Expected end of block here.
```
Note we do not currently attempt to identify mismatched `end`s. This might be
something to do in the future.
```lua
if true then
while true do
end
```
```txt
Unexpected end of file. Expected end or another statement.
|
1 | if true then
| ^^ Block started here.
|
3 | end
| ^ Expected end of block here.
```
## Unexpected `end`
We also print when there's more `end`s than expected.
```lua
if true then
end
end
```
```txt
Unexpected end.
|
3 | end
| ^^^
Your program contains more ends than needed. Check each block (if, for, function, ...) only has one end.
```
```lua
repeat
if true then
end
end
until true
```
```txt
Unexpected end.
|
4 | end
| ^^^
Your program contains more ends than needed. Check each block (if, for, function, ...) only has one end.
```

View File

@ -0,0 +1,107 @@
local expect = require "cc.expect".expect
local lex_one = require "cc.internal.syntax.lexer".lex_one
local parser = require "cc.internal.syntax.parser"
local tokens, last_token = parser.tokens, parser.tokens.COMMENT
--- Make a dummy context.
local function make_context(input)
local lines = { 1 }
local function line(pos) lines[#lines + 1] = pos end
local function get_pos(pos)
for i = #lines, 1, -1 do
local start = lines[i]
if pos >= start then return i, pos - start + 1, start end
end
error("Position is <= 0", 2)
end
return { line = line, get_pos = get_pos, lines = lines }
end
--[[- Run a parser on an input string, capturing its output.
This uses a simplified method of displaying errors (compared with
@{cc.internal.error_printer}), which is suitable for printing to a file.
@tparam string input The input string to parse.
@tparam[opt=false] boolean print_tokens Whether to print each token as its parsed.
@tparam[opt] number start The start state of the parser.
@treturn string The parser's output
]]
local function capture_parser(input, print_tokens, start)
expect(1, input, "string")
expect(2, print_tokens, "boolean", "nil")
expect(3, start, "number", "nil")
local error_sentinel = {}
local out = {}
local function print(x) out[#out + 1] = tostring(x) end
local function get_name(token)
for name, tok in pairs(tokens) do if tok == token then return name end end
return "?[" .. tostring(token) .. "]"
end
local context = make_context(input)
function context.report(message)
expect(3, message, "table")
for _, msg in ipairs(message) do
if type(msg) == "table" and msg.tag == "annotate" then
local line, col = context.get_pos(msg.start_pos)
local end_line, end_col = context.get_pos(msg.end_pos)
local contents = input:match("^([^\r\n]*)", context.lines[line])
print(" |")
print(("%2d | %s"):format(line, contents))
local indicator = line == end_line and ("^"):rep(end_col - col + 1) or "^..."
if #msg.msg > 0 then
print((" | %s%s %s"):format((" "):rep(col - 1), indicator, msg.msg))
else
print((" | %s%s"):format((" "):rep(col - 1), indicator))
end
else
print(tostring(msg))
end
end
end
local pos = 1
local ok, err = xpcall(function()
return parser.parse(context, function()
while true do
local token, start, finish, content = lex_one(context, input, pos)
if not token then return tokens.EOF, #input + 1, #input + 1 end
if print_tokens then
local start_line, start_col = context.get_pos(start)
local end_line, end_col = context.get_pos(finish)
local text = input:sub(start, finish)
print(("%d:%d-%d:%d %s %s"):format(
start_line, start_col, end_line, end_col,
get_name(token), content or text:gsub("\n", "<NL>")
))
end
pos = finish + 1
if token < last_token then
return token, start, finish
elseif token == tokens.ERROR then
error(error_sentinel)
end
end
end, start)
end, debug.traceback)
if not ok and err ~= error_sentinel then
print(err)
end
return table.concat(out, "\n")
end
return { make_context = make_context, capture_parser = capture_parser }

View File

@ -4,7 +4,7 @@ describe("The bg program", function()
it("opens a tab in the background", function()
local openTab = stub(shell, "openTab", function() return 12 end)
local switchTab = stub(shell, "switchTab")
capture(stub, "bg")
capture("bg")
expect(openTab):called_with("shell")
expect(switchTab):called(0)
end)

View File

@ -4,7 +4,7 @@ describe("The fg program", function()
it("opens the shell in the foreground", function()
local openTab = stub(shell, "openTab", function() return 12 end)
local switchTab = stub(shell, "switchTab")
capture(stub, "fg")
capture("fg")
expect(openTab):called_with("shell")
expect(switchTab):called_with(12)
end)

View File

@ -2,27 +2,27 @@ local capture = require "test_helpers".capture_program
describe("The alias program", function()
it("displays its usage when given too many arguments", function()
expect(capture(stub, "alias a b c"))
expect(capture("alias a b c"))
:matches { ok = true, output = "Usage: alias <alias> <program>\n", error = "" }
end)
it("lists aliases", function()
local pagedTabulate = stub(textutils, "pagedTabulate", function(x) print(table.unpack(x)) end)
stub(shell, "aliases", function() return { cp = "copy" } end)
expect(capture(stub, "alias"))
expect(capture("alias"))
:matches { ok = true, output = "cp:copy\n", error = "" }
expect(pagedTabulate):called_with_matching({ "cp:copy" })
end)
it("sets an alias", function()
local setAlias = stub(shell, "setAlias")
capture(stub, "alias test Hello")
capture("alias test Hello")
expect(setAlias):called_with("test", "Hello")
end)
it("clears an alias", function()
local clearAlias = stub(shell, "clearAlias")
capture(stub, "alias test")
capture("alias test")
expect(clearAlias):called_with("test")
end)
end)

View File

@ -3,17 +3,17 @@ local capture = require "test_helpers".capture_program
describe("The cd program", function()
it("changes into a directory", function()
local setDir = stub(shell, "setDir")
capture(stub, "cd /rom/programs")
capture("cd /rom/programs")
expect(setDir):called_with("rom/programs")
end)
it("does not move into a non-existent directory", function()
expect(capture(stub, "cd /rom/nothing"))
expect(capture("cd /rom/nothing"))
:matches { ok = true, output = "Not a directory\n", error = "" }
end)
it("displays the usage when given no arguments", function()
expect(capture(stub, "cd"))
expect(capture("cd"))
:matches { ok = true, output = "Usage: cd <path>\n", error = "" }
end)
end)

View File

@ -5,7 +5,7 @@ describe("The clear program", function()
local clear = stub(term, "clear")
local setCursorPos = stub(term, "setCursorPos")
capture(stub, "clear")
capture("clear")
expect(clear):called(1)
expect(setCursorPos):called_with(1, 1)

View File

@ -3,7 +3,7 @@ local capture = require "test_helpers".capture_program
describe("The commands program", function()
it("displays an error without the commands api", function()
stub(_G, "commands", nil)
expect(capture(stub, "/rom/programs/command/commands.lua"))
expect(capture("/rom/programs/command/commands.lua"))
:matches { ok = true, output = "", error = "Requires a Command Computer.\n" }
end)
@ -13,7 +13,7 @@ describe("The commands program", function()
list = function() return { "computercraft" } end,
})
expect(capture(stub, "/rom/programs/command/commands.lua"))
expect(capture("/rom/programs/command/commands.lua"))
:matches { ok = true, output = "Available commands:\ncomputercraft\n", error = "" }
expect(pagedTabulate):called_with_matching({ "computercraft" })
end)

View File

@ -3,13 +3,13 @@ local capture = require "test_helpers".capture_program
describe("The exec program", function()
it("displays an error without the commands api", function()
stub(_G, "commands", nil)
expect(capture(stub, "/rom/programs/command/exec.lua"))
expect(capture("/rom/programs/command/exec.lua"))
:matches { ok = true, output = "", error = "Requires a Command Computer.\n" }
end)
it("displays its usage when given no argument", function()
stub(_G, "commands", {})
expect(capture(stub, "/rom/programs/command/exec.lua"))
expect(capture("/rom/programs/command/exec.lua"))
:matches { ok = true, output = "", error = "Usage: /rom/programs/command/exec.lua <command>\n" }
end)
@ -18,7 +18,7 @@ describe("The exec program", function()
exec = function() return true, { "Hello World!" } end,
})
expect(capture(stub, "/rom/programs/command/exec.lua computercraft"))
expect(capture("/rom/programs/command/exec.lua computercraft"))
:matches { ok = true, output = "Success\nHello World!\n", error = "" }
end)
@ -27,7 +27,7 @@ describe("The exec program", function()
exec = function() return false, { "Hello World!" } end,
})
expect(capture(stub, "/rom/programs/command/exec.lua computercraft"))
expect(capture("/rom/programs/command/exec.lua computercraft"))
:matches { ok = true, output = "Hello World!\n", error = "Failed\n" }
end)
end)

View File

@ -15,26 +15,26 @@ describe("The copy program", function()
end)
it("fails when copying a non-existent file", function()
expect(capture(stub, "copy nothing destination"))
expect(capture("copy nothing destination"))
:matches { ok = true, output = "", error = "No matching files\n" }
end)
it("fails when overwriting an existing file", function()
touch("/test-files/copy/c.txt")
expect(capture(stub, "copy /test-files/copy/c.txt /test-files/copy/c.txt"))
expect(capture("copy /test-files/copy/c.txt /test-files/copy/c.txt"))
:matches { ok = true, output = "", error = "Destination exists\n" }
end)
it("fails when copying into read-only locations", function()
touch("/test-files/copy/d.txt")
expect(capture(stub, "copy /test-files/copy/d.txt /rom/test.txt"))
expect(capture("copy /test-files/copy/d.txt /rom/test.txt"))
:matches { ok = true, output = "", error = "Destination is read-only\n" }
end)
it("displays the usage when given no arguments", function()
expect(capture(stub, "copy"))
expect(capture("copy"))
:matches { ok = true, output = "Usage: copy <source> <destination>\n", error = "" }
end)
end)

View File

@ -36,17 +36,17 @@ describe("The rm program", function()
end)
it("displays the usage with no arguments", function()
expect(capture(stub, "rm"))
expect(capture("rm"))
:matches { ok = true, output = "Usage: rm <paths>\n", error = "" }
end)
it("errors when trying to delete a read-only file", function()
expect(capture(stub, "rm /rom/startup.lua"))
expect(capture("rm /rom/startup.lua"))
:matches { ok = true, output = "", error = "Cannot delete read-only file /rom/startup.lua\n" }
end)
it("errors when trying to delete the root mount", function()
expect(capture(stub, "rm /")):matches {
expect(capture("rm /")):matches {
ok = true,
output = "To delete its contents run rm /*\n",
error = "Cannot delete mount /\n",
@ -54,7 +54,7 @@ describe("The rm program", function()
end)
it("errors when a glob fails to match", function()
expect(capture(stub, "rm", "never-existed"))
expect(capture("rm", "never-existed"))
:matches { ok = true, output = "", error = "never-existed: No matching files\n" }
end)
end)

View File

@ -4,13 +4,13 @@ describe("The drive program", function()
it("run the program", function()
local getFreeSpace = stub(fs, "getFreeSpace", function() return 1234e4 end)
expect(capture(stub, "drive"))
expect(capture("drive"))
:matches { ok = true, output = "hdd (12.3MB remaining)\n", error = "" }
expect(getFreeSpace):called(1):called_with("")
end)
it("fails on a non-existent path", function()
expect(capture(stub, "drive /rom/nothing"))
expect(capture("drive /rom/nothing"))
:matches { ok = true, output = "No such path\n", error = "" }
end)
end)

View File

@ -3,7 +3,7 @@ local capture = require "test_helpers".capture_program
describe("The edit program", function()
it("displays its usage when given no argument", function()
expect(capture(stub, "edit"))
expect(capture("edit"))
:matches { ok = true, output = "Usage: edit <path>\n", error = "" }
end)
end)

View File

@ -2,12 +2,12 @@ local capture = require "test_helpers".capture_program
describe("The eject program", function()
it("displays its usage when given no argument", function()
expect(capture(stub, "eject"))
expect(capture("eject"))
:matches { ok = true, output = "Usage: eject <drive>\n", error = "" }
end)
it("fails when trying to eject a non-drive", function()
expect(capture(stub, "eject /rom"))
expect(capture("eject /rom"))
:matches { ok = true, output = "Nothing in /rom drive\n", error = "" }
end)
end)

View File

@ -3,7 +3,7 @@ local capture = require "test_helpers".capture_program
describe("The exit program", function()
it("exits the shell", function()
local exit = stub(shell, "exit")
expect(capture(stub, "exit")):matches { ok = true, combined = "" }
expect(capture("exit")):matches { ok = true, combined = "" }
expect(exit):called(1)
end)
end)

View File

@ -2,7 +2,7 @@ local capture = require "test_helpers".capture_program
describe("The paint program", function()
it("displays its usage when given no arguments", function()
expect(capture(stub, "paint"))
expect(capture("paint"))
:matches { ok = true, output = "Usage: paint <path>\n", error = "" }
end)
end)

View File

@ -2,12 +2,12 @@ local capture = require "test_helpers".capture_program
describe("The dj program", function()
it("displays its usage when given too many arguments", function()
expect(capture(stub, "dj a b c"))
expect(capture("dj a b c"))
:matches { ok = true, output = "Usages:\ndj play\ndj play <drive>\ndj stop\n", error = "" }
end)
it("fails when no disks are present", function()
expect(capture(stub, "dj"))
expect(capture("dj"))
:matches { ok = true, output = "No Music Discs in attached disk drives\n", error = "" }
end)
end)

View File

@ -3,7 +3,7 @@ local capture = require "test_helpers".capture_program
describe("The hello program", function()
it("says hello", function()
local slowPrint = stub(textutils, "slowPrint", function(...) return print(...) end)
expect(capture(stub, "hello"))
expect(capture("hello"))
:matches { ok = true, output = "Hello World!\n", error = "" }
expect(slowPrint):called(1)
end)

View File

@ -2,21 +2,21 @@ local capture = require "test_helpers".capture_program
describe("The gps program", function()
it("displays its usage when given no arguments", function()
expect(capture(stub, "gps"))
expect(capture("gps"))
:matches { ok = true, output = "Usages:\ngps host\ngps host <x> <y> <z>\ngps locate\n", error = "" }
end)
it("fails on a pocket computer", function()
stub(_G, "pocket", {})
expect(capture(stub, "gps host"))
expect(capture("gps host"))
:matches { ok = true, output = "GPS Hosts must be stationary\n", error = "" }
end)
it("can locate the computer", function()
local locate = stub(gps, "locate", function() print("Some debugging information.") end)
expect(capture(stub, "gps locate"))
expect(capture("gps locate"))
:matches { ok = true, output = "Some debugging information.\n", error = "" }
expect(locate):called_with(2, true)
end)

View File

@ -20,7 +20,7 @@ describe("The help program", function()
end
it("errors when there is no such help file", function()
expect(capture(stub, "help nothing"))
expect(capture("help nothing"))
:matches { ok = true, error = "No help available\n", output = "" }
end)

View File

@ -34,7 +34,7 @@ describe("The pastebin program", function()
it("downloads one file", function()
setup_request()
capture(stub, "pastebin", "get", "abcde", "testdown")
capture("pastebin", "get", "abcde", "testdown")
expect(fs.exists("/testdown")):eq(true)
end)
@ -42,7 +42,7 @@ describe("The pastebin program", function()
it("runs a program from the internet", function()
setup_request()
expect(capture(stub, "pastebin", "run", "abcde", "a", "b", "c"))
expect(capture("pastebin", "run", "abcde", "a", "b", "c"))
:matches { ok = true, output = "Connecting to pastebin.com... Success.\nHello a b c\n", error = "" }
end)
@ -52,21 +52,21 @@ describe("The pastebin program", function()
local file = fs.open("testup", "w")
file.close()
expect(capture(stub, "pastebin", "put", "testup"))
expect(capture("pastebin", "put", "testup"))
:matches { ok = true, output = "Connecting to pastebin.com... Success.\nUploaded as https://pastebin.com/abcde\nRun \"pastebin get abcde\" to download anywhere\n", error = "" }
end)
it("upload a not existing program to pastebin", function()
setup_request()
expect(capture(stub, "pastebin", "put", "nothing"))
expect(capture("pastebin", "put", "nothing"))
:matches { ok = true, output = "No such file\n", error = "" }
end)
it("displays its usage when given no arguments", function()
setup_request()
expect(capture(stub, "pastebin"))
expect(capture("pastebin"))
:matches { ok = true, output = "Usages:\npastebin put <filename>\npastebin get <code> <filename>\npastebin run <code> <arguments>\n", error = "" }
end)

View File

@ -23,7 +23,7 @@ describe("The wget program", function()
fs.delete("/example.com")
setup_request(default_contents)
capture(stub, "wget", "https://example.com")
capture("wget", "https://example.com")
expect(fs.exists("/example.com")):eq(true)
end)
@ -32,7 +32,7 @@ describe("The wget program", function()
fs.delete("/test-files/download")
setup_request(default_contents)
capture(stub, "wget", "https://example.com /test-files/download")
capture("wget", "https://example.com /test-files/download")
expect(fs.exists("/test-files/download")):eq(true)
end)
@ -41,7 +41,7 @@ describe("The wget program", function()
fs.delete("/test-files/download")
setup_request(nil)
capture(stub, "wget", "https://example.com", "/test-files/download")
capture("wget", "https://example.com", "/test-files/download")
expect(fs.exists("/test-files/download")):eq(true)
expect(fs.getSize("/test-files/download")):eq(0)
@ -50,7 +50,7 @@ describe("The wget program", function()
it("cannot save to rom", function()
setup_request(default_contents)
expect(capture(stub, "wget", "https://example.com", "/rom/a-file.txt")):matches {
expect(capture("wget", "https://example.com", "/rom/a-file.txt")):matches {
ok = true,
output = "Connecting to https://example.com... Success.\n",
error = "Cannot save file: /rom/a-file.txt: Access denied\n",
@ -60,14 +60,14 @@ describe("The wget program", function()
it("runs a program from the internet", function()
setup_request(default_contents)
expect(capture(stub, "wget", "run", "http://test.com", "a", "b", "c"))
expect(capture("wget", "run", "http://test.com", "a", "b", "c"))
:matches { ok = true, output = "Connecting to http://test.com... Success.\nHello a b c\n", error = "" }
end)
it("displays its usage when given no arguments", function()
setup_request(default_contents)
expect(capture(stub, "wget"))
expect(capture("wget"))
:matches { ok = true, output = "Usage:\nwget <url> [filename]\nwget run <url>\n", error = "" }
end)

View File

@ -5,7 +5,7 @@ describe("The id program", function()
it("displays computer id", function()
local id = os.getComputerID()
expect(capture(stub, "id"))
expect(capture("id"))
:matches { ok = true, output = "This is computer #" .. id .. "\n", error = "" }
end)
end)

View File

@ -2,33 +2,33 @@ local capture = require "test_helpers".capture_program
describe("The label program", function()
it("displays its usage when given no arguments", function()
expect(capture(stub, "label"))
expect(capture("label"))
:matches { ok = true, output = "Usages:\nlabel get\nlabel get <drive>\nlabel set <text>\nlabel set <drive> <text>\nlabel clear\nlabel clear <drive>\n", error = "" }
end)
describe("displays the computer's label", function()
it("when it is not labelled", function()
stub(os, "getComputerLabel", function() return nil end)
expect(capture(stub, "label get"))
expect(capture("label get"))
:matches { ok = true, output = "No Computer label\n", error = "" }
end)
it("when it is labelled", function()
stub(os, "getComputerLabel", function() return "Test" end)
expect(capture(stub, "label get"))
expect(capture("label get"))
:matches { ok = true, output = "Computer label is \"Test\"\n", error = "" }
end)
end)
it("sets the computer's label", function()
local setComputerLabel = stub(os, "setComputerLabel")
capture(stub, "label set Test")
capture("label set Test")
expect(setComputerLabel):called_with("Test")
end)
it("clears the computer's label", function()
local setComputerLabel = stub(os, "setComputerLabel")
capture(stub, "label clear")
capture("label clear")
expect(setComputerLabel):called_with(nil)
end)
end)

View File

@ -3,7 +3,7 @@ local capture = require "test_helpers".capture_program
describe("The list program", function()
it("lists files", function()
local pagedTabulate = stub(textutils, "pagedTabulate")
capture(stub, "list /rom")
capture("list /rom")
expect(pagedTabulate):called_with_matching(
colors.green, { "apis", "autorun", "help", "modules", "programs" },
colors.white, { "motd.txt", "startup.lua" }
@ -11,12 +11,12 @@ describe("The list program", function()
end)
it("fails on a non-existent directory", function()
expect(capture(stub, "list /rom/nothing"))
expect(capture("list /rom/nothing"))
:matches { ok = true, output = "", error = "Not a directory\n" }
end)
it("fails on a file", function()
expect(capture(stub, "list /rom/startup.lua"))
expect(capture("list /rom/startup.lua"))
:matches { ok = true, output = "", error = "Not a directory\n" }
end)
end)

View File

@ -2,7 +2,7 @@ local capture = require "test_helpers".capture_program
describe("The monitor program", function()
it("displays its usage when given no arguments", function()
expect(capture(stub, "monitor"))
expect(capture("monitor"))
:matches {
ok = true,
output =
@ -17,7 +17,7 @@ describe("The monitor program", function()
local r = 1
stub(peripheral, "call", function(s, f, t) r = t end)
stub(peripheral, "getType", function() return "monitor" end)
expect(capture(stub, "monitor", "scale", "left", "0.5"))
expect(capture("monitor", "scale", "left", "0.5"))
:matches { ok = true, output = "", error = "" }
expect(r):equals(0.5)
end)
@ -26,7 +26,7 @@ describe("The monitor program", function()
local r = 1
stub(peripheral, "call", function(s, f, t) r = t end)
stub(peripheral, "getType", function(side) return side == "left" and "monitor" or nil end)
expect(capture(stub, "monitor", "scale", "left"))
expect(capture("monitor", "scale", "left"))
:matches {
ok = true,
output =
@ -35,9 +35,9 @@ describe("The monitor program", function()
" monitor scale <name> <scale>\n",
error = "",
}
expect(capture(stub, "monitor", "scale", "top", "0.5"))
expect(capture("monitor", "scale", "top", "0.5"))
:matches { ok = true, output = "No monitor named top\n", error = "" }
expect(capture(stub, "monitor", "scale", "left", "aaa"))
expect(capture("monitor", "scale", "left", "aaa"))
:matches { ok = true, output = "Invalid scale: aaa\n", error = "" }
expect(r):equals(1)
end)

View File

@ -12,31 +12,31 @@ describe("The motd program", function()
file.close()
settings.set("motd.path", "/motd_check.txt")
expect(capture(stub, "motd"))
expect(capture("motd"))
:matches { ok = true, output = "Hello World!\n", error = "" }
end)
it("displays date-specific MOTD (1 Jan)", function()
setup_date(1, 1)
expect(capture(stub, "motd"))
expect(capture("motd"))
:matches { ok = true, output = "Happy new year!\n", error = "" }
end)
it("displays date-specific MOTD (28 Apr)", function()
setup_date(28, 4)
expect(capture(stub, "motd"))
expect(capture("motd"))
:matches { ok = true, output = "Ed Balls\n", error = "" }
end)
it("displays date-specific MOTD (31 Oct)", function()
setup_date(31, 10)
expect(capture(stub, "motd"))
expect(capture("motd"))
:matches { ok = true, output = "OOoooOOOoooo! Spooky!\n", error = "" }
end)
it("displays date-specific MOTD (24 Dec)", function()
setup_date(24, 12)
expect(capture(stub, "motd"))
expect(capture("motd"))
:matches { ok = true, output = "Merry X-mas!\n", error = "" }
end)
end)

View File

@ -21,7 +21,7 @@ describe("The move program", function()
touch("/test-files/move/a.txt")
fs.makeDir("/test-files/move/a")
expect(capture(stub, "move /test-files/move/a.txt /test-files/move/a"))
expect(capture("move /test-files/move/a.txt /test-files/move/a"))
:matches { ok = true }
expect(fs.exists("/test-files/move/a.txt")):eq(false)
@ -29,7 +29,7 @@ describe("The move program", function()
end)
it("fails when moving a file which doesn't exist", function()
expect(capture(stub, "move nothing destination"))
expect(capture("move nothing destination"))
:matches { ok = true, output = "", error = "No matching files\n" }
end)
@ -37,7 +37,7 @@ describe("The move program", function()
cleanup()
touch("/test-files/move/a.txt")
expect(capture(stub, "move /test-files/move/a.txt /test-files/move/a.txt"))
expect(capture("move /test-files/move/a.txt /test-files/move/a.txt"))
:matches { ok = true, output = "", error = "Destination exists\n" }
end)
@ -45,17 +45,17 @@ describe("The move program", function()
cleanup()
touch("/test-files/move/a.txt")
expect(capture(stub, "move /test-files/move/a.txt /rom/test.txt"))
expect(capture("move /test-files/move/a.txt /rom/test.txt"))
:matches { ok = true, output = "", error = "Destination is read-only\n" }
end)
it("fails when moving from read-only locations", function()
expect(capture(stub, "move /rom/startup.lua /test-files/move/not-exist.txt"))
expect(capture("move /rom/startup.lua /test-files/move/not-exist.txt"))
:matches { ok = true, output = "", error = "Cannot move read-only file /rom/startup.lua\n" }
end)
it("fails when moving mounts", function()
expect(capture(stub, "move /rom /test-files/move/rom"))
expect(capture("move /rom /test-files/move/rom"))
:matches { ok = true, output = "", error = "Cannot move mount /rom\n" }
end)
@ -63,12 +63,12 @@ describe("The move program", function()
cleanup()
touch("/test-files/move/a.txt")
touch("/test-files/move/b.txt")
expect(capture(stub, "move /test-files/move/*.txt /test-files/move/c.txt"))
expect(capture("move /test-files/move/*.txt /test-files/move/c.txt"))
:matches { ok = true, output = "", error = "Cannot overwrite file multiple times\n" }
end)
it("displays the usage with no arguments", function()
expect(capture(stub, "move"))
expect(capture("move"))
:matches { ok = true, output = "Usage: move <source> <destination>\n", error = "" }
end)
end)

View File

@ -3,7 +3,7 @@ local capture = require "test_helpers".capture_program
describe("The peripherals program", function()
it("says when there are no peripherals", function()
stub(peripheral, 'getNames', function() return {} end)
expect(capture(stub, "peripherals"))
expect(capture("peripherals"))
:matches { ok = true, output = "Attached Peripherals:\nNone\n", error = "" }
end)
end)

View File

@ -3,7 +3,7 @@ local capture = require "test_helpers".capture_program
describe("The pocket equip program", function()
it("errors when not a pocket computer", function()
stub(_G, "pocket", nil)
expect(capture(stub, "/rom/programs/pocket/equip.lua"))
expect(capture("/rom/programs/pocket/equip.lua"))
:matches { ok = true, output = "", error = "Requires a Pocket Computer\n" }
end)
@ -12,7 +12,7 @@ describe("The pocket equip program", function()
equipBack = function() return true end,
})
expect(capture(stub, "/rom/programs/pocket/equip.lua"))
expect(capture("/rom/programs/pocket/equip.lua"))
:matches { ok = true, output = "Item equipped\n", error = "" }
end)
@ -21,7 +21,7 @@ describe("The pocket equip program", function()
equipBack = function() return false, "Cannot equip this item." end,
})
expect(capture(stub, "/rom/programs/pocket/equip.lua"))
expect(capture("/rom/programs/pocket/equip.lua"))
:matches { ok = true, output = "", error = "Cannot equip this item.\n" }
end)
end)

View File

@ -3,7 +3,7 @@ local capture = require "test_helpers".capture_program
describe("The pocket unequip program", function()
it("errors when not a pocket computer", function()
stub(_G, "pocket", nil)
expect(capture(stub, "/rom/programs/pocket/unequip.lua"))
expect(capture("/rom/programs/pocket/unequip.lua"))
:matches { ok = true, output = "", error = "Requires a Pocket Computer\n" }
end)
@ -12,7 +12,7 @@ describe("The pocket unequip program", function()
unequipBack = function() return true end,
})
expect(capture(stub, "/rom/programs/pocket/unequip.lua"))
expect(capture("/rom/programs/pocket/unequip.lua"))
:matches { ok = true, output = "Item unequipped\n", error = "" }
end)
@ -21,7 +21,7 @@ describe("The pocket unequip program", function()
unequipBack = function() return false, "Nothing to remove." end,
})
expect(capture(stub, "/rom/programs/pocket/unequip.lua"))
expect(capture("/rom/programs/pocket/unequip.lua"))
:matches { ok = true, output = "", error = "Nothing to remove.\n" }
end)
end)

View File

@ -5,7 +5,7 @@ describe("The programs program", function()
local programs = stub(shell, "programs", function() return { "some", "programs" } end)
local pagedTabulate = stub(textutils, "pagedTabulate", function(x) print(table.unpack(x)) end)
expect(capture(stub, "/rom/programs/programs.lua"))
expect(capture("/rom/programs/programs.lua"))
:matches { ok = true, output = "some programs\n", error = "" }
expect(programs):called_with(false)

View File

@ -5,7 +5,7 @@ describe("The reboot program", function()
local sleep = stub(_G, "sleep")
local reboot = stub(os, "reboot")
expect(capture(stub, "reboot"))
expect(capture("reboot"))
:matches { ok = true, output = "Goodbye\n", error = "" }
expect(sleep):called_with(1)

View File

@ -2,7 +2,7 @@ local capture = require "test_helpers".capture_program
describe("The redstone program", function()
it("displays its usage when given no arguments", function()
expect(capture(stub, "redstone"))
expect(capture("redstone"))
:matches { ok = true, output = "Usages:\nredstone probe\nredstone set <side> <value>\nredstone set <side> <color> <value>\nredstone pulse <side> <count> <period>\n", error = "" }
end)
end)

View File

@ -15,36 +15,36 @@ describe("The rename program", function()
end)
it("fails when renaming a file which doesn't exist", function()
expect(capture(stub, "rename nothing destination"))
expect(capture("rename nothing destination"))
:matches { ok = true, output = "", error = "No matching files\n" }
end)
it("fails when overwriting an existing file", function()
touch("/test-files/rename/c.txt")
expect(capture(stub, "rename /test-files/rename/c.txt /test-files/rename/c.txt"))
expect(capture("rename /test-files/rename/c.txt /test-files/rename/c.txt"))
:matches { ok = true, output = "", error = "Destination exists\n" }
end)
it("fails when renaming to read-only locations", function()
touch("/test-files/rename/d.txt")
expect(capture(stub, "rename /test-files/rename/d.txt /rom/test.txt"))
expect(capture("rename /test-files/rename/d.txt /rom/test.txt"))
:matches { ok = true, output = "", error = "Destination is read-only\n" }
end)
it("fails when renaming from read-only locations", function()
expect(capture(stub, "rename /rom/startup.lua /test-files/rename/d.txt"))
expect(capture("rename /rom/startup.lua /test-files/rename/d.txt"))
:matches { ok = true, output = "", error = "Source is read-only\n" }
end)
it("fails when renaming mounts", function()
expect(capture(stub, "rename /rom /test-files/rename/rom"))
expect(capture("rename /rom /test-files/rename/rom"))
:matches { ok = true, output = "", error = "Can't rename mounts\n" }
end)
it("displays the usage when given no arguments", function()
expect(capture(stub, "rename"))
expect(capture("rename"))
:matches { ok = true, output = "Usage: rename <source> <destination>\n", error = "" }
end)
end)

View File

@ -13,21 +13,21 @@ describe("The set program", function()
it("displays all settings", function()
setup()
expect(capture(stub, "set"))
expect(capture("set"))
:matches { ok = true, output = '"test" is "Hello World!"\n"test.defined" is 456\n', error = "" }
end)
it("displays a single setting", function()
setup()
expect(capture(stub, "set test"))
expect(capture("set test"))
:matches { ok = true, output = 'test is "Hello World!"\n', error = "" }
end)
it("displays a single setting with description", function()
setup()
expect(capture(stub, "set test"))
expect(capture("set test"))
:matches { ok = true, output = 'test is "Hello World!"\n', error = "" }
end)
@ -35,14 +35,14 @@ describe("The set program", function()
setup()
settings.set("test.defined", 123)
expect(capture(stub, "set test.defined"))
expect(capture("set test.defined"))
:matches { ok = true, output = 'test.defined is 123 (default is 456)\nA description\n', error = "" }
end)
it("set a setting", function()
setup()
expect(capture(stub, "set test Hello"))
expect(capture("set test Hello"))
:matches { ok = true, output = '"test" set to "Hello"\n', error = "" }
expect(settings.get("test")):eq("Hello")
@ -51,9 +51,9 @@ describe("The set program", function()
it("checks the type of a setting", function()
setup()
expect(capture(stub, "set test.defined Hello"))
expect(capture("set test.defined Hello"))
:matches { ok = true, output = "", error = '"Hello" is not a valid number.\n' }
expect(capture(stub, "set test.defined 456"))
expect(capture("set test.defined 456"))
:matches { ok = true, output = '"test.defined" set to 456\n', error = "" }
end)
end)

View File

@ -6,7 +6,7 @@ describe("The shutdown program", function()
local sleep = stub(_G, "sleep")
local shutdown = stub(os, "shutdown")
expect(capture(stub, "shutdown"))
expect(capture("shutdown"))
:matches { ok = true, output = "Goodbye\n", error = "" }
expect(sleep):called_with(1)

View File

@ -6,7 +6,7 @@ describe("The time program", function()
local time = textutils.formatTime(os.time())
local day = os.day()
expect(capture(stub, "time"))
expect(capture("time"))
:matches { ok = true, output = "The time is " .. time .. " on Day " .. day .. "\n", error = "" }
end)
end)

View File

@ -4,28 +4,28 @@ describe("The craft program", function()
it("errors when not a turtle", function()
stub(_G, "turtle", nil)
expect(capture(stub, "/rom/programs/turtle/craft.lua"))
expect(capture("/rom/programs/turtle/craft.lua"))
:matches { ok = true, output = "", error = "Requires a Turtle\n" }
end)
it("fails when turtle.craft() is unavailable", function()
stub(_G, "turtle", {})
expect(capture(stub, "/rom/programs/turtle/craft.lua"))
expect(capture("/rom/programs/turtle/craft.lua"))
:matches { ok = true, output = "Requires a Crafty Turtle\n", error = "" }
end)
it("displays its usage when given no arguments", function()
stub(_G, "turtle", { craft = function() end })
expect(capture(stub, "/rom/programs/turtle/craft.lua"))
expect(capture("/rom/programs/turtle/craft.lua"))
:matches { ok = true, output = "Usage: /rom/programs/turtle/craft.lua all|<number>\n", error = "" }
end)
it("displays its usage when given incorrect arguments", function()
stub(_G, "turtle", { craft = function() end })
expect(capture(stub, "/rom/programs/turtle/craft.lua a"))
expect(capture("/rom/programs/turtle/craft.lua a"))
:matches { ok = true, output = "Usage: /rom/programs/turtle/craft.lua all|<number>\n", error = "" }
end)
@ -40,7 +40,7 @@ describe("The craft program", function()
getSelectedSlot = function() return 1 end,
})
expect(capture(stub, "/rom/programs/turtle/craft.lua 2"))
expect(capture("/rom/programs/turtle/craft.lua 2"))
:matches { ok = true, output = "2 items crafted\n", error = "" }
end)
@ -55,7 +55,7 @@ describe("The craft program", function()
getSelectedSlot = function() return 1 end,
})
expect(capture(stub, "/rom/programs/turtle/craft.lua 1"))
expect(capture("/rom/programs/turtle/craft.lua 1"))
:matches { ok = true, output = "1 item crafted\n", error = "" }
end)
@ -70,7 +70,7 @@ describe("The craft program", function()
getSelectedSlot = function() return 1 end,
})
expect(capture(stub, "/rom/programs/turtle/craft.lua 1"))
expect(capture("/rom/programs/turtle/craft.lua 1"))
:matches { ok = true, output = "No items crafted\n", error = "" }
end)
@ -83,7 +83,7 @@ describe("The craft program", function()
getSelectedSlot = function() return 1 end,
})
expect(capture(stub, "/rom/programs/turtle/craft.lua all"))
expect(capture("/rom/programs/turtle/craft.lua all"))
:matches { ok = true, output = "17 items crafted\n", error = "" }
end)
end)

View File

@ -4,7 +4,7 @@ describe("The turtle equip program", function()
it("errors when not a turtle", function()
stub(_G, "turtle", nil)
expect(capture(stub, "/rom/programs/turtle/equip.lua"))
expect(capture("/rom/programs/turtle/equip.lua"))
:matches { ok = true, output = "", error = "Requires a Turtle\n" }
end)
@ -12,7 +12,7 @@ describe("The turtle equip program", function()
it("displays its usage when given no arguments", function()
stub(_G, "turtle", {})
expect(capture(stub, "/rom/programs/turtle/equip.lua"))
expect(capture("/rom/programs/turtle/equip.lua"))
:matches { ok = true, output = "Usage: /rom/programs/turtle/equip.lua <slot> <side>\n", error = "" }
end)
@ -22,9 +22,9 @@ describe("The turtle equip program", function()
getItemCount = function() return 0 end,
})
expect(capture(stub, "/rom/programs/turtle/equip.lua 1 left"))
expect(capture("/rom/programs/turtle/equip.lua 1 left"))
:matches { ok = true, output = "Nothing to equip\n", error = "" }
expect(capture(stub, "/rom/programs/turtle/equip.lua 1 right"))
expect(capture("/rom/programs/turtle/equip.lua 1 right"))
:matches { ok = true, output = "Nothing to equip\n", error = "" }
end)
@ -36,9 +36,9 @@ describe("The turtle equip program", function()
equipRight = function() return true end,
})
expect(capture(stub, "/rom/programs/turtle/equip.lua 1 left"))
expect(capture("/rom/programs/turtle/equip.lua 1 left"))
:matches { ok = true, output = "Items swapped\n", error = "" }
expect(capture(stub, "/rom/programs/turtle/equip.lua 1 right"))
expect(capture("/rom/programs/turtle/equip.lua 1 right"))
:matches { ok = true, output = "Items swapped\n", error = "" }
end)
@ -61,13 +61,13 @@ describe("The turtle equip program", function()
it("on the left", function()
setup()
expect(capture(stub, "/rom/programs/turtle/equip.lua 1 left"))
expect(capture("/rom/programs/turtle/equip.lua 1 left"))
:matches { ok = true, output = "Item equipped\n", error = "" }
end)
it("on the right", function()
setup()
expect(capture(stub, "/rom/programs/turtle/equip.lua 1 right"))
expect(capture("/rom/programs/turtle/equip.lua 1 right"))
:matches { ok = true, output = "Item equipped\n", error = "" }
end)
end)
@ -80,9 +80,9 @@ describe("The turtle equip program", function()
equipRight = function() return false end,
})
expect(capture(stub, "/rom/programs/turtle/equip.lua 1 left"))
expect(capture("/rom/programs/turtle/equip.lua 1 left"))
:matches { ok = true, output = "Item not equippable\n", error = "" }
expect(capture(stub, "/rom/programs/turtle/equip.lua 1 right"))
expect(capture("/rom/programs/turtle/equip.lua 1 right"))
:matches { ok = true, output = "Item not equippable\n", error = "" }
end)

View File

@ -24,39 +24,39 @@ describe("The refuel program", function()
it("errors when not a turtle", function()
stub(_G, "turtle", nil)
expect(capture(stub, "/rom/programs/turtle/refuel.lua"))
expect(capture("/rom/programs/turtle/refuel.lua"))
:matches { ok = true, output = "", error = "Requires a Turtle\n" }
end)
it("displays its usage when given too many argument", function()
setup_turtle(0, 5, 0)
expect(capture(stub, "/rom/programs/turtle/refuel.lua a b"))
expect(capture("/rom/programs/turtle/refuel.lua a b"))
:matches { ok = true, output = "Usage: /rom/programs/turtle/refuel.lua [number]\n", error = "" }
end)
it("requires a numeric argument", function()
setup_turtle(0, 0, 0)
expect(capture(stub, "/rom/programs/turtle/refuel.lua nothing"))
expect(capture("/rom/programs/turtle/refuel.lua nothing"))
:matches { ok = true, output = "Invalid limit, expected a number or \"all\"\n", error = "" }
end)
it("refuels the turtle", function()
setup_turtle(0, 10, 5)
expect(capture(stub, "/rom/programs/turtle/refuel.lua 5"))
expect(capture("/rom/programs/turtle/refuel.lua 5"))
:matches { ok = true, output = "Fuel level is 5\n", error = "" }
end)
it("reports when the fuel limit is reached", function()
setup_turtle(0, 5, 5)
expect(capture(stub, "/rom/programs/turtle/refuel.lua 5"))
expect(capture("/rom/programs/turtle/refuel.lua 5"))
:matches { ok = true, output = "Fuel level is 5\nFuel limit reached\n", error = "" }
end)
it("reports when the fuel level is unlimited", function()
setup_turtle("unlimited", 5, 5)
expect(capture(stub, "/rom/programs/turtle/refuel.lua 5"))
expect(capture("/rom/programs/turtle/refuel.lua 5"))
:matches { ok = true, output = "Fuel level is unlimited\n", error = "" }
end)
end)

View File

@ -4,7 +4,7 @@ describe("The turtle unequip program", function()
it("errors when not a turtle", function()
stub(_G, "turtle", nil)
expect(capture(stub, "/rom/programs/turtle/unequip.lua"))
expect(capture("/rom/programs/turtle/unequip.lua"))
:matches { ok = true, output = "", error = "Requires a Turtle\n" }
end)
@ -12,7 +12,7 @@ describe("The turtle unequip program", function()
it("displays its usage when given no arguments", function()
stub(_G, "turtle", {})
expect(capture(stub, "/rom/programs/turtle/unequip.lua"))
expect(capture("/rom/programs/turtle/unequip.lua"))
:matches { ok = true, output = "Usage: /rom/programs/turtle/unequip.lua <side>\n", error = "" }
end)
@ -24,9 +24,9 @@ describe("The turtle unequip program", function()
equipLeft = function() return true end,
})
expect(capture(stub, "/rom/programs/turtle/unequip.lua left"))
expect(capture("/rom/programs/turtle/unequip.lua left"))
:matches { ok = true, output = "Nothing to unequip\n", error = "" }
expect(capture(stub, "/rom/programs/turtle/unequip.lua right"))
expect(capture("/rom/programs/turtle/unequip.lua right"))
:matches { ok = true, output = "Nothing to unequip\n", error = "" }
end)
@ -45,10 +45,10 @@ describe("The turtle unequip program", function()
end,
})
expect(capture(stub, "/rom/programs/turtle/unequip.lua left"))
expect(capture("/rom/programs/turtle/unequip.lua left"))
:matches { ok = true, output = "Item unequipped\n", error = "" }
item_count = 0
expect(capture(stub, "/rom/programs/turtle/unequip.lua right"))
expect(capture("/rom/programs/turtle/unequip.lua right"))
:matches { ok = true, output = "Item unequipped\n", error = "" }
end)
@ -60,9 +60,9 @@ describe("The turtle unequip program", function()
equipLeft = function() return true end,
})
expect(capture(stub, "/rom/programs/turtle/unequip.lua left"))
expect(capture("/rom/programs/turtle/unequip.lua left"))
:matches { ok = true, output = "No space to unequip item\n", error = "" }
expect(capture(stub, "/rom/programs/turtle/unequip.lua right"))
expect(capture("/rom/programs/turtle/unequip.lua right"))
:matches { ok = true, output = "No space to unequip item\n", error = "" }
end)

View File

@ -3,22 +3,22 @@ local capture = require "test_helpers".capture_program
describe("The type program", function()
it("displays the usage with no arguments", function()
expect(capture(stub, "type"))
expect(capture("type"))
:matches { ok = true, output = "Usage: type <path>\n", error = "" }
end)
it("displays the output for a file", function()
expect(capture(stub, "type /rom/startup.lua"))
expect(capture("type /rom/startup.lua"))
:matches { ok = true, output = "file\n", error = "" }
end)
it("displays the output for a directory", function()
expect(capture(stub, "type /rom"))
expect(capture("type /rom"))
:matches { ok = true, output = "directory\n", error = "" }
end)
it("displays the output for a not existing path", function()
expect(capture(stub, "type /rom/nothing"))
expect(capture("type /rom/nothing"))
:matches { ok = true, output = "No such path\n", error = "" }
end)

View File

@ -5,7 +5,7 @@
-- @tparam string ... Arguments to this program.
-- @treturn { ok = boolean, output = string, error = string, combined = string }
-- Whether this program terminated successfully, and the various output streams.
local function capture_program(stub, program, ...)
local function capture_program(program, ...)
local output, error, combined = {}, {}, {}
local function out(stream, msg)
@ -70,8 +70,68 @@ local function with_window_lines(width, height, fn)
return out
end
local function timeout(time, fn)
local timer = os.startTimer(time)
local co = coroutine.create(fn)
local ok, result, event = true, nil, { n = 0 }
while coroutine.status(co) ~= "dead" do
if event[1] == "timer" and event[2] == timer then error("Timeout", 2) end
if result == nil or event[1] == result or event[1] == "terminated" then
ok, result = coroutine.resume(co, table.unpack(event, 1, event.n))
if not ok then error(result, 0) end
end
event = table.pack(coroutine.yield())
end
end
--- Extract a series of tests from a markdown file.
local function describe_golden(name, file, generate)
describe(name, function()
local handle = assert(fs.open(file, "r"))
local contents = "\n" .. handle.readAll()
handle.close()
local pos = 1
local function run(current_level)
local test_idx = 1
while true do
local lua_start, lua_end, extra, lua = contents:find("```lua *([^\n]*)\n(.-)\n```\n?", pos)
local heading_start, heading_end, heading_lvl, heading = contents:find("\n(#+) *([^\n]+)", pos)
if heading and (not lua_start or heading_start < lua_start) then
if #heading_lvl <= current_level then
return
end
pos = heading_end + 1
describe(heading, function() run(#heading_lvl) end)
elseif lua_end then
local _, txt_end, txt = contents:find("^\n*```txt\n(.-)\n```\n?", lua_end + 1)
it("test #" .. test_idx, function()
expect(generate(lua, extra))
:describe("For input string <<<\n" .. lua .. "\n>>>")
:eq(txt)
end)
test_idx = test_idx + 1
pos = (txt_end or lua_end) + 1
else
return
end
end
end
run(0)
end)
end
return {
capture_program = capture_program,
with_window = with_window,
with_window_lines = with_window_lines,
timeout = timeout,
describe_golden = describe_golden,
}