mirror of
https://github.com/CompeyDev/lune-packaging.git
synced 2025-01-09 12:19:09 +00:00
Implement proper async require & error handling
This commit is contained in:
parent
7f17ab0063
commit
58ce046394
22 changed files with 412 additions and 132 deletions
|
@ -1,96 +1,107 @@
|
|||
use std::{
|
||||
env::{self, current_dir},
|
||||
io,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use mlua::prelude::*;
|
||||
use os_str_bytes::{OsStrBytes, RawOsStr};
|
||||
use tokio::fs;
|
||||
|
||||
use crate::utils::table::TableBuilder;
|
||||
|
||||
pub fn create(lua: &'static Lua) -> LuaResult<LuaTable> {
|
||||
// Preserve original require behavior if we have a special env var set
|
||||
// Preserve original require behavior if we have a special env var set,
|
||||
// returning an empty table since there are no globals to overwrite
|
||||
if env::var_os("LUAU_PWD_REQUIRE").is_some() {
|
||||
// Return an empty table since there are no globals to overwrite
|
||||
return TableBuilder::new(lua)?.build_readonly();
|
||||
}
|
||||
// Store the current pwd, and make helper functions for path conversions
|
||||
let require_pwd = current_dir()?.to_string_lossy().to_string();
|
||||
let require_info: LuaFunction = lua.named_registry_value("dbg.info")?;
|
||||
let require_error: LuaFunction = lua.named_registry_value("error")?;
|
||||
let require_get_abs_rel_paths = lua
|
||||
.create_function(
|
||||
|_, (require_pwd, require_source, require_path): (String, String, String)| {
|
||||
let mut path_relative_to_pwd = PathBuf::from(
|
||||
&require_source
|
||||
.trim_start_matches("[string \"")
|
||||
.trim_end_matches("\"]"),
|
||||
)
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join(require_path);
|
||||
// Try to normalize and resolve relative path segments such as './' and '../'
|
||||
if let Ok(canonicalized) =
|
||||
path_relative_to_pwd.with_extension("luau").canonicalize()
|
||||
{
|
||||
path_relative_to_pwd = canonicalized;
|
||||
}
|
||||
if let Ok(canonicalized) = path_relative_to_pwd.with_extension("lua").canonicalize()
|
||||
{
|
||||
path_relative_to_pwd = canonicalized;
|
||||
}
|
||||
let absolute = path_relative_to_pwd.to_string_lossy().to_string();
|
||||
let relative = absolute.trim_start_matches(&require_pwd).to_string();
|
||||
Ok((absolute, relative))
|
||||
},
|
||||
)?
|
||||
.bind(require_pwd)?;
|
||||
/*
|
||||
Store the current working directory so that we can use it later
|
||||
and remove it from require paths in error messages, showing
|
||||
absolute paths is bad ux and we should try to avoid it
|
||||
We need to get the source file where require was
|
||||
called to be able to do path-relative requires,
|
||||
so we make a small wrapper to do that here, this
|
||||
will then call our actual async require function
|
||||
|
||||
Throughout this function we also take extra care to not perform any lossy
|
||||
conversion and use os strings instead of Rust's utf-8 checked strings,
|
||||
just in case someone out there uses luau with non-utf8 string requires
|
||||
This must be done in lua because due to how our
|
||||
scheduler works mlua can not preserve debug info
|
||||
*/
|
||||
let pwd = lua.create_string(¤t_dir()?.to_raw_bytes())?;
|
||||
lua.set_named_registry_value("pwd", pwd)?;
|
||||
/*
|
||||
Create a new function that fetches the file name from the current thread,
|
||||
sets the luau module lookup path to be the exact script we are looking
|
||||
for, and then runs the original require function with the wanted path
|
||||
*/
|
||||
let new_require = lua.create_function(|lua, require_path: LuaString| {
|
||||
let require_pwd: LuaString = lua.named_registry_value("pwd")?;
|
||||
let require_fn: LuaFunction = lua.named_registry_value("require")?;
|
||||
let require_info: LuaFunction = lua.named_registry_value("dbg.info")?;
|
||||
let require_source: LuaString = require_info.call((2, "s"))?;
|
||||
/*
|
||||
Combine the require caller source with the wanted path
|
||||
string to get a final path relative to pwd - it is definitely
|
||||
relative to pwd because Lune will only load files relative to pwd
|
||||
*/
|
||||
let raw_pwd_str = RawOsStr::assert_from_raw_bytes(require_pwd.as_bytes());
|
||||
let raw_source = RawOsStr::assert_from_raw_bytes(require_source.as_bytes());
|
||||
let raw_path = RawOsStr::assert_from_raw_bytes(require_path.as_bytes());
|
||||
let mut path_relative_to_pwd = PathBuf::from(
|
||||
&raw_source
|
||||
.trim_start_matches("[string \"")
|
||||
.trim_end_matches("\"]")
|
||||
.to_os_str(),
|
||||
let require_env = TableBuilder::new(lua)?
|
||||
.with_value("loaded", lua.create_table()?)?
|
||||
.with_value("cache", lua.create_table()?)?
|
||||
.with_value("info", require_info)?
|
||||
.with_value("error", require_error)?
|
||||
.with_value("paths", require_get_abs_rel_paths)?
|
||||
.with_async_function("load", load_file)?
|
||||
.build_readonly()?;
|
||||
let require_fn_lua = lua
|
||||
.load(
|
||||
r#"
|
||||
local source = info(2, "s")
|
||||
local absolute, relative = paths(source, ...)
|
||||
if loaded[absolute] ~= true then
|
||||
local first, second = load(absolute, relative)
|
||||
if first == nil or second ~= nil then
|
||||
error("Module did not return exactly one value")
|
||||
end
|
||||
loaded[absolute] = true
|
||||
cache[absolute] = first
|
||||
return first
|
||||
else
|
||||
return cache[absolute]
|
||||
end
|
||||
"#,
|
||||
)
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join(raw_path.to_os_str());
|
||||
// Try to normalize and resolve relative path segments such as './' and '../'
|
||||
if let Ok(canonicalized) = path_relative_to_pwd.with_extension("luau").canonicalize() {
|
||||
path_relative_to_pwd = canonicalized.with_extension("");
|
||||
}
|
||||
if let Ok(canonicalized) = path_relative_to_pwd.with_extension("lua").canonicalize() {
|
||||
path_relative_to_pwd = canonicalized.with_extension("");
|
||||
}
|
||||
if let Ok(stripped) = path_relative_to_pwd.strip_prefix(&raw_pwd_str.to_os_str()) {
|
||||
path_relative_to_pwd = stripped.to_path_buf();
|
||||
}
|
||||
// Create a lossless lua string from the pathbuf and finally call require
|
||||
let raw_path_str = RawOsStr::new(path_relative_to_pwd.as_os_str());
|
||||
let lua_path_str = lua.create_string(raw_path_str.as_raw_bytes());
|
||||
// If the require call errors then we should also replace
|
||||
// the path in the error message to improve user experience
|
||||
let result: LuaResult<_> = require_fn.call::<_, LuaValue>(lua_path_str);
|
||||
match result {
|
||||
Err(LuaError::CallbackError { traceback, cause }) => {
|
||||
let before = format!(
|
||||
"runtime error: cannot find '{}'",
|
||||
path_relative_to_pwd.to_str().unwrap()
|
||||
);
|
||||
let after = format!(
|
||||
"Invalid require path '{}' ({})",
|
||||
require_path.to_str().unwrap(),
|
||||
path_relative_to_pwd.to_str().unwrap()
|
||||
);
|
||||
let cause = Arc::new(LuaError::RuntimeError(
|
||||
cause.to_string().replace(&before, &after),
|
||||
));
|
||||
Err(LuaError::CallbackError { traceback, cause })
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
Ok(result) => Ok(result),
|
||||
}
|
||||
})?;
|
||||
// Override the original require global with our monkey-patched one
|
||||
.set_name("require")?
|
||||
.set_environment(require_env)?
|
||||
.into_function()?;
|
||||
TableBuilder::new(lua)?
|
||||
.with_value("require", new_require)?
|
||||
.with_value("require", require_fn_lua)?
|
||||
.build_readonly()
|
||||
}
|
||||
|
||||
async fn load_file(
|
||||
lua: &Lua,
|
||||
(path_absolute, path_relative): (String, String),
|
||||
) -> LuaResult<LuaValue> {
|
||||
// Try to read the wanted file, note that we use bytes instead of reading
|
||||
// to a string since lua scripts are not necessarily valid utf-8 strings
|
||||
match fs::read(&path_absolute).await {
|
||||
Ok(contents) => lua.load(&contents).set_name(path_relative)?.eval(),
|
||||
Err(e) => match e.kind() {
|
||||
io::ErrorKind::NotFound => Err(LuaError::RuntimeError(format!(
|
||||
"No lua module exists at the path '{path_relative}'"
|
||||
))),
|
||||
_ => Err(LuaError::external(e)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,16 +37,21 @@ pub fn create(lua: &'static Lua) -> LuaResult<LuaTable> {
|
|||
})?
|
||||
.with_function("error", |lua, (arg, level): (LuaValue, Option<u32>)| {
|
||||
let error: LuaFunction = lua.named_registry_value("error")?;
|
||||
let multi = arg.to_lua_multi(lua)?;
|
||||
let trace: LuaFunction = lua.named_registry_value("dbg.trace")?;
|
||||
error.call((
|
||||
format!(
|
||||
"{}\n{}",
|
||||
format_label("error"),
|
||||
pretty_format_multi_value(&multi)?
|
||||
),
|
||||
LuaError::CallbackError {
|
||||
traceback: format!("override traceback:{}", trace.call::<_, String>(())?),
|
||||
cause: LuaError::external(format!(
|
||||
"{}\n{}",
|
||||
format_label("error"),
|
||||
pretty_format_multi_value(&arg.to_lua_multi(lua)?)?
|
||||
))
|
||||
.into(),
|
||||
},
|
||||
level,
|
||||
))?;
|
||||
Ok(())
|
||||
})?
|
||||
// TODO: Add an override for tostring that formats errors in a nicer way
|
||||
.build_readonly()
|
||||
}
|
||||
|
|
|
@ -118,9 +118,8 @@ impl Lune {
|
|||
let mut got_error = false;
|
||||
loop {
|
||||
let result = sched.resume_queue().await;
|
||||
// println!("{result}");
|
||||
if let Some(err) = result.get_lua_error() {
|
||||
eprintln!("{}", pretty_format_luau_error(&err));
|
||||
eprintln!("{}", pretty_format_luau_error(&err, true));
|
||||
got_error = true;
|
||||
}
|
||||
if result.is_done() {
|
||||
|
|
|
@ -3,29 +3,38 @@ use mlua::prelude::*;
|
|||
/*
|
||||
- Level 0 is the call to info
|
||||
- Level 1 is the load call in create() below where we load this into a function
|
||||
- Level 2 is the call to the scheduler, probably, but we can't know for sure so we start at 2
|
||||
- Level 2 is the call to the trace, which we also want to skip, so start at 3
|
||||
|
||||
Also note that we must match the mlua traceback format here so that we
|
||||
can pattern match and beautify it properly later on when outputting it
|
||||
*/
|
||||
const TRACE_IMPL_LUA: &str = r#"
|
||||
local lines = {}
|
||||
for level = 2, 2^8 do
|
||||
for level = 3, 16 do
|
||||
local parts = {}
|
||||
local source, line, name = info(level, "sln")
|
||||
if source then
|
||||
if line then
|
||||
if name and #name > 0 then
|
||||
push(lines, format(" Script '%s', Line %d - function %s", source, line, name))
|
||||
else
|
||||
push(lines, format(" Script '%s', Line %d", source, line))
|
||||
end
|
||||
elseif name and #name > 0 then
|
||||
push(lines, format(" Script '%s' - function %s", source, name))
|
||||
else
|
||||
push(lines, format(" Script '%s'", source))
|
||||
end
|
||||
elseif name then
|
||||
push(lines, format("[Lune] - function %s", source, name))
|
||||
push(parts, source)
|
||||
else
|
||||
break
|
||||
end
|
||||
if line == -1 then
|
||||
line = nil
|
||||
end
|
||||
if name and #name <= 0 then
|
||||
name = nil
|
||||
end
|
||||
if line then
|
||||
push(parts, format("%d", line))
|
||||
end
|
||||
if name and #parts > 1 then
|
||||
push(parts, format(" in function '%s'", name))
|
||||
elseif name then
|
||||
push(parts, format("in function '%s'", name))
|
||||
end
|
||||
if #parts > 0 then
|
||||
push(lines, concat(parts, ":"))
|
||||
end
|
||||
end
|
||||
if #lines > 0 then
|
||||
return concat(lines, "\n")
|
||||
|
@ -49,12 +58,20 @@ end
|
|||
* `"type"` -> `type`
|
||||
* `"typeof"` -> `typeof`
|
||||
---
|
||||
* `"pcall"` -> `pcall`
|
||||
* `"xpcall"` -> `xpcall`
|
||||
---
|
||||
* `"tostring"` -> `tostring`
|
||||
* `"tonumber"` -> `tonumber`
|
||||
---
|
||||
* `"co.thread"` -> `coroutine.running`
|
||||
* `"co.yield"` -> `coroutine.yield`
|
||||
* `"co.close"` -> `coroutine.close`
|
||||
---
|
||||
* `"dbg.info"` -> `debug.info`
|
||||
* `"dbg.trace"` -> `debug.traceback`
|
||||
* `"dbg.iserr"` -> `<custom function>`
|
||||
* `"dbg.makeerr"` -> `<custom function>`
|
||||
---
|
||||
*/
|
||||
pub fn create() -> LuaResult<&'static Lua> {
|
||||
|
@ -72,23 +89,43 @@ pub fn create() -> LuaResult<&'static Lua> {
|
|||
lua.set_named_registry_value("error", globals.get::<_, LuaFunction>("error")?)?;
|
||||
lua.set_named_registry_value("type", globals.get::<_, LuaFunction>("type")?)?;
|
||||
lua.set_named_registry_value("typeof", globals.get::<_, LuaFunction>("typeof")?)?;
|
||||
lua.set_named_registry_value("xpcall", globals.get::<_, LuaFunction>("xpcall")?)?;
|
||||
lua.set_named_registry_value("pcall", globals.get::<_, LuaFunction>("pcall")?)?;
|
||||
lua.set_named_registry_value("tostring", globals.get::<_, LuaFunction>("tostring")?)?;
|
||||
lua.set_named_registry_value("tonumber", globals.get::<_, LuaFunction>("tonumber")?)?;
|
||||
lua.set_named_registry_value("co.thread", coroutine.get::<_, LuaFunction>("running")?)?;
|
||||
lua.set_named_registry_value("co.yield", coroutine.get::<_, LuaFunction>("yield")?)?;
|
||||
lua.set_named_registry_value("co.close", coroutine.get::<_, LuaFunction>("close")?)?;
|
||||
lua.set_named_registry_value("dbg.info", debug.get::<_, LuaFunction>("info")?)?;
|
||||
lua.set_named_registry_value("tab.pack", table.get::<_, LuaFunction>("pack")?)?;
|
||||
lua.set_named_registry_value("tab.unpack", table.get::<_, LuaFunction>("unpack")?)?;
|
||||
// Create a function that can be called from lua to check if a value is a mlua error,
|
||||
// this will be used in async environments for proper error handling and throwing, as
|
||||
// well as a function that can be called to make a callback error with a traceback from lua
|
||||
let dbg_is_err_fn =
|
||||
lua.create_function(move |_, value: LuaValue| Ok(matches!(value, LuaValue::Error(_))))?;
|
||||
|
||||
let dbg_make_err_fn = lua.create_function(|_, (cause, traceback): (LuaError, String)| {
|
||||
Ok(LuaError::CallbackError {
|
||||
traceback,
|
||||
cause: cause.into(),
|
||||
})
|
||||
})?;
|
||||
// Create a trace function that can be called to obtain a full stack trace from
|
||||
// lua, this is not possible to do from rust when using our manual scheduler
|
||||
let trace_env = lua.create_table_with_capacity(0, 1)?;
|
||||
trace_env.set("info", debug.get::<_, LuaFunction>("info")?)?;
|
||||
trace_env.set("push", table.get::<_, LuaFunction>("insert")?)?;
|
||||
trace_env.set("concat", table.get::<_, LuaFunction>("concat")?)?;
|
||||
trace_env.set("format", string.get::<_, LuaFunction>("format")?)?;
|
||||
let trace_fn = lua
|
||||
let dbg_trace_env = lua.create_table_with_capacity(0, 1)?;
|
||||
dbg_trace_env.set("info", debug.get::<_, LuaFunction>("info")?)?;
|
||||
dbg_trace_env.set("push", table.get::<_, LuaFunction>("insert")?)?;
|
||||
dbg_trace_env.set("concat", table.get::<_, LuaFunction>("concat")?)?;
|
||||
dbg_trace_env.set("format", string.get::<_, LuaFunction>("format")?)?;
|
||||
let dbg_trace_fn = lua
|
||||
.load(TRACE_IMPL_LUA)
|
||||
.set_name("=dbg.trace")?
|
||||
.set_environment(trace_env)?
|
||||
.set_environment(dbg_trace_env)?
|
||||
.into_function()?;
|
||||
lua.set_named_registry_value("dbg.trace", trace_fn)?;
|
||||
lua.set_named_registry_value("dbg.trace", dbg_trace_fn)?;
|
||||
lua.set_named_registry_value("dbg.iserr", dbg_is_err_fn)?;
|
||||
lua.set_named_registry_value("dbg.makeerr", dbg_make_err_fn)?;
|
||||
// All done
|
||||
Ok(lua)
|
||||
}
|
||||
|
|
|
@ -26,9 +26,19 @@ impl LuaAsyncExt for &'static Lua {
|
|||
F: 'static + Fn(&'static Lua, A) -> FR,
|
||||
FR: 'static + Future<Output = LuaResult<R>>,
|
||||
{
|
||||
let async_env_make_err: LuaFunction = self.named_registry_value("dbg.makeerr")?;
|
||||
let async_env_is_err: LuaFunction = self.named_registry_value("dbg.iserr")?;
|
||||
let async_env_trace: LuaFunction = self.named_registry_value("dbg.trace")?;
|
||||
let async_env_error: LuaFunction = self.named_registry_value("error")?;
|
||||
let async_env_unpack: LuaFunction = self.named_registry_value("tab.unpack")?;
|
||||
let async_env_thread: LuaFunction = self.named_registry_value("co.thread")?;
|
||||
let async_env_yield: LuaFunction = self.named_registry_value("co.yield")?;
|
||||
let async_env = TableBuilder::new(self)?
|
||||
.with_value("makeError", async_env_make_err)?
|
||||
.with_value("isError", async_env_is_err)?
|
||||
.with_value("trace", async_env_trace)?
|
||||
.with_value("error", async_env_error)?
|
||||
.with_value("unpack", async_env_unpack)?
|
||||
.with_value("thread", async_env_thread)?
|
||||
.with_value("yield", async_env_yield)?
|
||||
.with_function(
|
||||
|
@ -50,7 +60,12 @@ impl LuaAsyncExt for &'static Lua {
|
|||
.load(
|
||||
"
|
||||
resumeAsync(thread(), ...)
|
||||
return yield()
|
||||
local results = { yield() }
|
||||
if isError(results[1]) then
|
||||
error(makeError(results[1], trace()))
|
||||
else
|
||||
return unpack(results)
|
||||
end
|
||||
",
|
||||
)
|
||||
.set_name("asyncWrapper")?
|
||||
|
|
|
@ -225,8 +225,18 @@ impl<'fut> TaskScheduler<'fut> {
|
|||
self.guid_running.set(Some(reference.id()));
|
||||
let rets = match args_opt_res {
|
||||
Some(args_res) => match args_res {
|
||||
Err(err) => Err(err), // FIXME: We need to throw this error in lua to let pcall & friends handle it properly
|
||||
Ok(args) => thread.resume::<_, LuaMultiValue>(args),
|
||||
/*
|
||||
HACK: Resuming with an error here only works because the Rust
|
||||
functions that we register and that may return lua errors are
|
||||
also error-aware and wrapped in a special wrapper that checks
|
||||
if the returned value is a lua error userdata, then throws it
|
||||
|
||||
Also note that this only happens for our custom async functions
|
||||
that may pass errors as arguments when resuming tasks, other
|
||||
native mlua functions will handle this and dont need wrapping
|
||||
*/
|
||||
Err(err) => thread.resume(err),
|
||||
Ok(args) => thread.resume(args),
|
||||
},
|
||||
None => thread.resume(()),
|
||||
};
|
||||
|
|
|
@ -55,11 +55,14 @@ create_tests! {
|
|||
process_env: "process/env",
|
||||
process_exit: "process/exit",
|
||||
process_spawn: "process/spawn",
|
||||
require_children: "require/tests/children",
|
||||
require_invalid: "require/tests/invalid",
|
||||
require_nested: "require/tests/nested",
|
||||
require_parents: "require/tests/parents",
|
||||
require_siblings: "require/tests/siblings",
|
||||
require_children: "globals/require/tests/children",
|
||||
require_invalid: "globals/require/tests/invalid",
|
||||
require_nested: "globals/require/tests/nested",
|
||||
require_parents: "globals/require/tests/parents",
|
||||
require_siblings: "globals/require/tests/siblings",
|
||||
global_pcall: "globals/pcall",
|
||||
global_type: "globals/type",
|
||||
global_typeof: "globals/typeof",
|
||||
stdio_format: "stdio/format",
|
||||
stdio_color: "stdio/color",
|
||||
stdio_style: "stdio/style",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::fmt::Write;
|
||||
|
||||
use console::{style, Style};
|
||||
use console::{colors_enabled, set_colors_enabled, style, Style};
|
||||
use lazy_static::lazy_static;
|
||||
use mlua::prelude::*;
|
||||
|
||||
|
@ -178,7 +178,7 @@ pub fn pretty_format_value(
|
|||
}
|
||||
}
|
||||
LuaValue::LightUserData(_) => write!(buffer, "{}", COLOR_PURPLE.apply_to("<userdata>"))?,
|
||||
_ => write!(buffer, "{}", STYLE_DIM.apply_to("?"))?,
|
||||
LuaValue::Error(e) => write!(buffer, "{}", pretty_format_luau_error(e, false),)?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -200,7 +200,13 @@ pub fn pretty_format_multi_value(multi: &LuaMultiValue) -> LuaResult<String> {
|
|||
Ok(buffer)
|
||||
}
|
||||
|
||||
pub fn pretty_format_luau_error(e: &LuaError) -> String {
|
||||
pub fn pretty_format_luau_error(e: &LuaError, colorized: bool) -> String {
|
||||
let previous_colors_enabled = if !colorized {
|
||||
set_colors_enabled(false);
|
||||
Some(colors_enabled())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let stack_begin = format!("[{}]", COLOR_BLUE.apply_to("Stack Begin"));
|
||||
let stack_end = format!("[{}]", COLOR_BLUE.apply_to("Stack End"));
|
||||
let err_string = match e {
|
||||
|
@ -218,23 +224,33 @@ pub fn pretty_format_luau_error(e: &LuaError) -> String {
|
|||
let mut found_stack_begin = false;
|
||||
for (index, line) in err_lines.clone().iter().enumerate().rev() {
|
||||
if *line == "stack traceback:" {
|
||||
err_lines[index] = stack_begin;
|
||||
err_lines[index] = stack_begin.clone();
|
||||
found_stack_begin = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Add "Stack End" to the very end of the stack trace for symmetry
|
||||
if found_stack_begin {
|
||||
err_lines.push(stack_end);
|
||||
err_lines.push(stack_end.clone());
|
||||
}
|
||||
err_lines.join("\n")
|
||||
}
|
||||
LuaError::CallbackError { traceback, cause } => {
|
||||
// Find the best traceback (most lines) and the root error message
|
||||
let mut best_trace = traceback;
|
||||
// The traceback may also start with "override traceback:" which
|
||||
// means it was passed from somewhere that wants a custom trace,
|
||||
// so we should then respect that and get the best override instead
|
||||
let mut best_trace: &str = traceback;
|
||||
let mut root_cause = cause.as_ref();
|
||||
let mut trace_override = false;
|
||||
while let LuaError::CallbackError { cause, traceback } = root_cause {
|
||||
if traceback.lines().count() > best_trace.len() {
|
||||
let is_override = traceback.starts_with("override traceback:");
|
||||
if is_override {
|
||||
if !trace_override || traceback.lines().count() > best_trace.len() {
|
||||
best_trace = traceback.strip_prefix("override traceback:").unwrap();
|
||||
trace_override = true;
|
||||
}
|
||||
} else if !trace_override && traceback.lines().count() > best_trace.len() {
|
||||
best_trace = traceback;
|
||||
}
|
||||
root_cause = cause;
|
||||
|
@ -242,15 +258,19 @@ pub fn pretty_format_luau_error(e: &LuaError) -> String {
|
|||
// If we got a runtime error with an embedded traceback, we should
|
||||
// use that instead since it generally contains more information
|
||||
if matches!(root_cause, LuaError::RuntimeError(e) if e.contains("stack traceback:")) {
|
||||
pretty_format_luau_error(root_cause)
|
||||
pretty_format_luau_error(root_cause, colorized)
|
||||
} else {
|
||||
// Otherwise we format whatever root error we got using
|
||||
// the same error formatting as for above runtime errors
|
||||
format!(
|
||||
"{}\n{}\n{}\n{}",
|
||||
pretty_format_luau_error(root_cause),
|
||||
pretty_format_luau_error(root_cause, colorized),
|
||||
stack_begin,
|
||||
best_trace.strip_prefix("stack traceback:\n").unwrap(),
|
||||
if best_trace.starts_with("stack traceback:") {
|
||||
best_trace.strip_prefix("stack traceback:\n").unwrap()
|
||||
} else {
|
||||
best_trace
|
||||
},
|
||||
stack_end
|
||||
)
|
||||
}
|
||||
|
@ -269,11 +289,13 @@ pub fn pretty_format_luau_error(e: &LuaError) -> String {
|
|||
}
|
||||
e => format!("{e}"),
|
||||
};
|
||||
let mut err_lines = err_string.lines().collect::<Vec<_>>();
|
||||
// Re-enable colors if they were previously enabled
|
||||
if let Some(true) = previous_colors_enabled {
|
||||
set_colors_enabled(true)
|
||||
}
|
||||
// Remove the script path from the error message
|
||||
// itself, it can be found in the stack trace
|
||||
// FIXME: This no longer works now that we use
|
||||
// an exact name when our lune script is loaded
|
||||
let mut err_lines = err_string.lines().collect::<Vec<_>>();
|
||||
if let Some(first_line) = err_lines.first() {
|
||||
if first_line.starts_with("[string \"") {
|
||||
if let Some(closing_bracket) = first_line.find("]:") {
|
||||
|
@ -287,6 +309,120 @@ pub fn pretty_format_luau_error(e: &LuaError) -> String {
|
|||
}
|
||||
}
|
||||
}
|
||||
// Merge all lines back together into one string
|
||||
err_lines.join("\n")
|
||||
// Find where the stack trace stars and ends
|
||||
let stack_begin_idx =
|
||||
err_lines.iter().enumerate().find_map(
|
||||
|(i, line)| {
|
||||
if *line == stack_begin {
|
||||
Some(i)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
);
|
||||
let stack_end_idx =
|
||||
err_lines.iter().enumerate().find_map(
|
||||
|(i, line)| {
|
||||
if *line == stack_end {
|
||||
Some(i)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
);
|
||||
// If we have a stack trace, we should transform the formatting from the
|
||||
// default mlua formatting into something more friendly, similar to Roblox
|
||||
if let (Some(idx_start), Some(idx_end)) = (stack_begin_idx, stack_end_idx) {
|
||||
let stack_lines = err_lines
|
||||
.iter()
|
||||
.enumerate()
|
||||
// Filter out stack lines
|
||||
.filter_map(|(idx, line)| {
|
||||
if idx > idx_start && idx < idx_end {
|
||||
Some(*line)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
// Transform from mlua format into friendly format, while also
|
||||
// ensuring that leading whitespace / indentation is consistent
|
||||
.map(transform_stack_line)
|
||||
.collect::<Vec<_>>();
|
||||
fix_error_nitpicks(format!(
|
||||
"{}\n{}\n{}\n{}",
|
||||
err_lines
|
||||
.iter()
|
||||
.take(idx_start)
|
||||
.copied()
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"),
|
||||
stack_begin,
|
||||
stack_lines.join("\n"),
|
||||
stack_end,
|
||||
))
|
||||
} else {
|
||||
fix_error_nitpicks(err_string)
|
||||
}
|
||||
}
|
||||
|
||||
fn transform_stack_line(line: &str) -> String {
|
||||
match (line.find('['), line.find(']')) {
|
||||
(Some(idx_start), Some(idx_end)) => {
|
||||
let name = line[idx_start..idx_end + 1]
|
||||
.trim_start_matches('[')
|
||||
.trim_start_matches("string ")
|
||||
.trim_start_matches('"')
|
||||
.trim_end_matches(']')
|
||||
.trim_end_matches('"');
|
||||
let after_name = &line[idx_end + 1..];
|
||||
let line_num = match after_name.find(':') {
|
||||
Some(lineno_start) => match after_name[lineno_start + 1..].find(':') {
|
||||
Some(lineno_end) => &after_name[lineno_start + 1..lineno_end + 1],
|
||||
None => match after_name.contains("in function") {
|
||||
false => &after_name[lineno_start + 1..],
|
||||
true => "",
|
||||
},
|
||||
},
|
||||
None => "",
|
||||
};
|
||||
let func_name = match after_name.find("in function ") {
|
||||
Some(func_start) => after_name[func_start + 12..]
|
||||
.trim()
|
||||
.trim_end_matches('\'')
|
||||
.trim_start_matches('\'')
|
||||
.trim_start_matches("_G."),
|
||||
None => "",
|
||||
};
|
||||
let mut result = String::new();
|
||||
write!(
|
||||
result,
|
||||
" Script '{}'",
|
||||
match name {
|
||||
"C" => "[C]",
|
||||
name => name,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
if !line_num.is_empty() {
|
||||
write!(result, ", Line {line_num}").unwrap();
|
||||
}
|
||||
if !func_name.is_empty() {
|
||||
write!(result, " - function {func_name}").unwrap();
|
||||
}
|
||||
result
|
||||
}
|
||||
(_, _) => line.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn fix_error_nitpicks(full_message: String) -> String {
|
||||
full_message
|
||||
// Hacky fix for our custom require appearing as a normal script
|
||||
.replace("'require', Line 5", "'[C]' - function require")
|
||||
.replace("'require', Line 7", "'[C]' - function require")
|
||||
// Fix error calls in custom script chunks coming through
|
||||
.replace(
|
||||
"'[C]' - function error\n Script '[C]' - function require",
|
||||
"'[C]' - function require",
|
||||
)
|
||||
}
|
||||
|
|
39
tests/globals/pcall.luau
Normal file
39
tests/globals/pcall.luau
Normal file
|
@ -0,0 +1,39 @@
|
|||
local function test(f, ...)
|
||||
local success, message = pcall(f, ...)
|
||||
assert(not success, "Function did not throw an error")
|
||||
assert(type(message) == "userdata", "Pcall did not return a proper error")
|
||||
end
|
||||
|
||||
-- These are not async but should be pcallable
|
||||
|
||||
test(error, "Test error", 2)
|
||||
|
||||
-- Net request is async and will throw a DNS error here for the weird address
|
||||
|
||||
test(net.request, "https://wxyz.google.com")
|
||||
|
||||
-- Net serve is async and will throw an OS error when trying to serve twice on the same port
|
||||
|
||||
local handle = net.serve(8080, function()
|
||||
return ""
|
||||
end)
|
||||
|
||||
task.delay(0, function()
|
||||
handle.stop()
|
||||
end)
|
||||
|
||||
test(net.serve, 8080, function() end)
|
||||
|
||||
local function e()
|
||||
task.spawn(function()
|
||||
task.defer(function()
|
||||
task.delay(0, function()
|
||||
error({
|
||||
Hello = "World",
|
||||
})
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
task.defer(e)
|
0
tests/globals/require/tests/foo.lua
Normal file
0
tests/globals/require/tests/foo.lua
Normal file
|
@ -5,8 +5,8 @@ local function test(path: string)
|
|||
if success then
|
||||
error(string.format("Invalid require at path '%s' succeeded", path))
|
||||
else
|
||||
message = tostring(message)
|
||||
if string.find(message, string.format("'%s'", path)) == nil then
|
||||
print(message)
|
||||
if string.find(message, string.format("%s'", path)) == nil then
|
||||
error(
|
||||
string.format(
|
||||
"Invalid require did not mention path '%s' in its error message!\nMessage: %s",
|
11
tests/globals/type.luau
Normal file
11
tests/globals/type.luau
Normal file
|
@ -0,0 +1,11 @@
|
|||
local function f() end
|
||||
|
||||
local thread1 = coroutine.create(f)
|
||||
local thread2 = task.spawn(f)
|
||||
local thread3 = task.defer(f)
|
||||
local thread4 = task.delay(0, f)
|
||||
|
||||
assert(type(thread1) == "thread", "Calling type() did not return 'thread' after coroutine.create")
|
||||
assert(type(thread2) == "thread", "Calling type() did not return 'thread' after task.spawn")
|
||||
assert(type(thread3) == "thread", "Calling type() did not return 'thread' after task.defer")
|
||||
assert(type(thread4) == "thread", "Calling type() did not return 'thread' after delay")
|
14
tests/globals/typeof.luau
Normal file
14
tests/globals/typeof.luau
Normal file
|
@ -0,0 +1,14 @@
|
|||
local function f() end
|
||||
|
||||
local thread1 = coroutine.create(f)
|
||||
local thread2 = task.spawn(f)
|
||||
local thread3 = task.defer(f)
|
||||
local thread4 = task.delay(0, f)
|
||||
|
||||
assert(
|
||||
typeof(thread1) == "thread",
|
||||
"Calling typeof() did not return 'thread' after coroutine.create"
|
||||
)
|
||||
assert(typeof(thread2) == "thread", "Calling typeof() did not return 'thread' after task.spawn")
|
||||
assert(typeof(thread3) == "thread", "Calling typeof() did not return 'thread' after task.defer")
|
||||
assert(typeof(thread4) == "thread", "Calling typeof() did not return 'thread' after delay")
|
Loading…
Reference in a new issue