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 chieflya12b405acf
, and5502412181
. - 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:
parent
68f6fa9343
commit
9b3cadf57c
|
@ -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
|
|
@ -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
|
|
@ -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.+"
|
||||
|
|
|
@ -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__)))
|
||||
|
|
|
@ -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 );
|
||||
|
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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", {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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.
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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,
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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,
|
||||
}
|
|
@ -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
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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" },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
|
@ -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 $*&
|
||||
```
|
File diff suppressed because it is too large
Load Diff
|
@ -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.
|
||||
```
|
|
@ -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 }
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue