-
Notifications
You must be signed in to change notification settings - Fork 2
fix: error stack traces in transpiled TypeScript #740
Changes from 5 commits
fe71fc0
24155c1
eb991a2
ff644be
822d7b7
f19cff8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
/runtime/vendored | ||
/runtime/js/vendored | ||
target | ||
/runtime/tests/js/typescript_fixtures/typescript_stack_trace.ts | ||
|
||
# Let's keep LICENSE.md in the same formatting as we use in other PL repositories | ||
LICENSE.md |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
// This TypeScript-only block of code is removed during transpilation. A naive solution that removes | ||
// the TypeScript code instead of replacing it with whitespace and does not apply source maps to error | ||
// stack traces will lead to incorrect line numbers in error stack traces. | ||
interface User { | ||
name: string; | ||
email: string; | ||
} | ||
|
||
// The part `: Error` changes the source column number | ||
// between the TypeScript original and the transpiled code. | ||
// | ||
// Throw the error so that the test can verify source code line & column numbers | ||
// in the stack trace frames but also the source code of the line throwing the exception. | ||
const error: Error = new Error(); throw error; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ use std::rc::Rc; | |
|
||
use anyhow::{anyhow, Context}; | ||
use deno_core::ModuleSpecifier; | ||
use zinnia_runtime::fmt_errors::format_js_error; | ||
use zinnia_runtime::{any_and_jserrorbox_downcast_ref, CoreError, RecordingReporter}; | ||
use zinnia_runtime::{anyhow, deno_core, run_js_module, AnyError, BootstrapOptions}; | ||
|
||
|
@@ -94,6 +95,36 @@ js_tests!(ipfs_retrieval_tests); | |
test_runner_tests!(passing_tests); | ||
test_runner_tests!(failing_tests expect_failure); | ||
|
||
#[tokio::test] | ||
async fn typescript_stack_trace_test() -> Result<(), AnyError> { | ||
let (_, run_error) = run_js_test_file("typescript_fixtures/typescript_stack_trace.ts").await?; | ||
let error = run_error.ok_or_else(|| { | ||
anyhow!("The script was expected to throw an error. Success was reported instead.") | ||
})?; | ||
|
||
if let Some(CoreError::Js(e)) = any_and_jserrorbox_downcast_ref::<CoreError>(&error) { | ||
let actual_error = format_js_error(e); | ||
// Strip ANSI codes (colors, styles) | ||
let actual_error = console_static_text::ansi::strip_ansi_codes(&actual_error); | ||
// Replace current working directory in stack trace file paths with a fixed placeholder | ||
let cwd_url = ModuleSpecifier::from_file_path(std::env::current_dir().unwrap()).unwrap(); | ||
let actual_error = actual_error.replace(cwd_url.as_str(), "file:///project-root"); | ||
// Normalize line endings to Unix style (LF only) | ||
let actual_error = actual_error.replace("\r\n", "\n"); | ||
|
||
let expected_error = r#" | ||
Uncaught (in promise) Error | ||
const error: Error = new Error(); throw error; | ||
^ | ||
at file:///project-root/tests/js/typescript_fixtures/typescript_stack_trace.ts:14:22 | ||
"#; | ||
assert_eq!(actual_error.trim(), expected_error.trim()); | ||
} else { | ||
panic!("The script threw unexpected error: {}", error); | ||
} | ||
Ok(()) | ||
} | ||
|
||
// Run all tests in a single JS file | ||
async fn run_js_test_file(name: &str) -> Result<(Vec<String>, Option<AnyError>), AnyError> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In retrospect, it was a mistake to use @NikolasHaimerl, do you have a suggestion on how to best represent a type with the following variants in Rust?
Variants:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about we wrap the logs inside the Anyhow context string? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see what you mean. We need to extract the logs from the results, so that we can assert what exactly was logged by the test runner on failure. Do you know how to extract one piece of a context from the Anyhow error? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am leaning towards using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it help to create a named struct Alternatively, maybe we can treat the case "we cannot execute the script" the same way as "the script threw an error". That will give us
WDYT? I'd also like to make this refactoring as a follow-up, after I land the remaining two pull requests. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see what you mean. Ideally I would like to avoid having a result inside a result. We could use enums as error types to give us specific information about which error was caused by the test runner. Why do we not unwrap an error once it occurs? What is the benefit of propagating it upwards if it is only used in testing? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense.
In some tests, we want to assert that execution of the JS code threw an error and that the JS code produced the expected logs.
I find it easier when all test helpers use error propagation instead of unwrapping, because that way I don't have to decide in each test helper which error-handling strategy is appropriate. Let's continue this discussion in #752 |
||
let _ = env_logger::builder().is_test(true).try_init(); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the module loader exclusively single threaded?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great question! I am not sure, TBH.
We are running the Deno runtime on the current thread, see here:
zinnia/daemon/main.rs
Line 21 in 2d40376
zinnia/cli/main.rs
Line 17 in 2d40376
I think that means all async tasks will be executed on the current thread too, including the asynchronous work of the module loader.
Deno uses
Rc<RefCell<...>>
in their example:https://github.com/denoland/deno_core/blob/0.343.0/core/examples/ts_module_loader.rs
My knowledge of async Rust is very limited, I don't understand this deeply enough. What would you suggest to use?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I see, well
Rc<RefCell<...>>
is used in a single-threaded environment, since Deno is using that as an example, I would assume it is single threaded. If we were to use multithreaded environments, I'd rather go for aArc<Mutex<...>>
type.