KCL tests: take fancier snapshots of KCL errors (#4574)

Right now our KCL tests output a debug representation of the KCLError
value. This works OK, but it's difficult to read an error like
"runtime error: SourceRange([44, 48])" because I don't fucking know what
the 44th character in my KCL program is.

In the modeling app, source ranges are turned into nice red squiggly
underlines in the editor. I want nice squiggly underline when I run the
Rust unit tests too, damnit. The JS world should NEVER have fancy toys
that I, a Rust programmer, cannot access. I deserve this. I need this.

So anyway instead of snapshotting debug repr, snapshot a fancy error
via the miette library.
This commit is contained in:
Adam Chalmers
2024-11-25 17:28:57 -06:00
committed by GitHub
parent 99dd8b87dc
commit 23a3e330f6
20 changed files with 320 additions and 22 deletions

107
src/wasm-lib/Cargo.lock generated
View File

@ -228,6 +228,15 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "backtrace-ext"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50"
dependencies = [
"backtrace",
]
[[package]]
name = "base64"
version = "0.13.1"
@ -1625,6 +1634,12 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "is_ci"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
@ -1708,6 +1723,7 @@ dependencies = [
"kittycad-modeling-cmds",
"lazy_static",
"measurements",
"miette",
"mime_guess",
"parse-display 0.9.1",
"pretty_assertions",
@ -1971,6 +1987,37 @@ dependencies = [
"autocfg",
]
[[package]]
name = "miette"
version = "7.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4edc8853320c2a0dab800fbda86253c8938f6ea88510dc92c5f1ed20e794afc1"
dependencies = [
"backtrace",
"backtrace-ext",
"cfg-if",
"miette-derive",
"owo-colors",
"supports-color",
"supports-hyperlinks",
"supports-unicode",
"terminal_size",
"textwrap",
"thiserror 1.0.68",
"unicode-width 0.1.14",
]
[[package]]
name = "miette-derive"
version = "7.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf09caffaac8068c346b6df2a7fc27a177fd20b39421a39ce0a211bde679a6c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
]
[[package]]
name = "mime"
version = "0.3.17"
@ -2163,6 +2210,12 @@ dependencies = [
"thiserror 1.0.68",
]
[[package]]
name = "owo-colors"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb37767f6569cd834a413442455e0f066d0d522de8630436e2a1761d9726ba56"
[[package]]
name = "papergrid"
version = "0.11.0"
@ -3311,6 +3364,12 @@ version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "smawk"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
[[package]]
name = "socket2"
version = "0.5.7"
@ -3396,6 +3455,27 @@ version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "supports-color"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9829b314621dfc575df4e409e79f9d6a66a3bd707ab73f23cb4aa3a854ac854f"
dependencies = [
"is_ci",
]
[[package]]
name = "supports-hyperlinks"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c0a1e5168041f5f3ff68ff7d95dcb9c8749df29f6e7e89ada40dd4c9de404ee"
[[package]]
name = "supports-unicode"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2"
[[package]]
name = "syn"
version = "1.0.109"
@ -3496,6 +3576,27 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "terminal_size"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7"
dependencies = [
"rustix",
"windows-sys 0.48.0",
]
[[package]]
name = "textwrap"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9"
dependencies = [
"smawk",
"unicode-linebreak",
"unicode-width 0.1.14",
]
[[package]]
name = "thiserror"
version = "1.0.68"
@ -3955,6 +4056,12 @@ version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[package]]
name = "unicode-linebreak"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"

View File

@ -33,6 +33,7 @@ kittycad = { workspace = true }
kittycad-modeling-cmds = { workspace = true }
lazy_static = "1.5.0"
measurements = "0.11.0"
miette = "7.2.0"
mime_guess = "2.0.5"
parse-display = "0.9.1"
pyo3 = { version = "0.22.6", optional = true }
@ -89,6 +90,7 @@ iai = "0.1"
image = { version = "0.25.5", default-features = false, features = ["png"] }
insta = { version = "1.41.1", features = ["json", "filters", "redactions"] }
itertools = "0.13.0"
miette = { version = "7.2.0", features = ["fancy"] }
pretty_assertions = "1.4.1"
tokio = { version = "1.40.0", features = ["rt-multi-thread", "macros", "time"] }
twenty-twenty = "0.8.0"

View File

@ -54,10 +54,56 @@ pub enum KclError {
Internal(KclErrorDetails),
}
#[derive(Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq)]
#[derive(thiserror::Error, Debug)]
#[error("{}", self.error.get_message())]
pub struct Report {
pub error: KclError,
pub kcl_source: String,
pub filename: String,
}
impl miette::Diagnostic for Report {
fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
let family = match self.error {
KclError::Lexical(_) => "Lexical",
KclError::Syntax(_) => "Syntax",
KclError::Semantic(_) => "Semantic",
KclError::ImportCycle(_) => "ImportCycle",
KclError::Type(_) => "Type",
KclError::Unimplemented(_) => "Unimplemented",
KclError::Unexpected(_) => "Unexpected",
KclError::ValueAlreadyDefined(_) => "ValueAlreadyDefined",
KclError::UndefinedValue(_) => "UndefinedValue",
KclError::InvalidExpression(_) => "InvalidExpression",
KclError::Engine(_) => "Engine",
KclError::Internal(_) => "Internal",
};
let error_string = format!("KCL {family} error");
Some(Box::new(error_string))
}
fn source_code(&self) -> Option<&dyn miette::SourceCode> {
Some(&self.kcl_source)
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
let iter = self
.error
.source_ranges()
.clone()
.into_iter()
.map(miette::SourceSpan::from)
.map(|span| miette::LabeledSpan::new_with_span(None, span));
Some(Box::new(iter))
}
}
#[derive(Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq, thiserror::Error, miette::Diagnostic)]
#[error("{message}")]
#[ts(export)]
pub struct KclErrorDetails {
#[serde(rename = "sourceRanges")]
#[label(collection, "Errors")]
pub source_ranges: Vec<SourceRange>,
#[serde(rename = "msg")]
pub message: String,

View File

@ -1020,6 +1020,20 @@ impl From<[usize; 3]> for SourceRange {
}
}
impl From<&SourceRange> for miette::SourceSpan {
fn from(source_range: &SourceRange) -> Self {
let length = source_range.end() - source_range.start();
let start = miette::SourceOffset::from(source_range.start());
Self::new(start, length)
}
}
impl From<SourceRange> for miette::SourceSpan {
fn from(source_range: SourceRange) -> Self {
Self::from(&source_range)
}
}
impl SourceRange {
/// Create a new source range.
pub fn new(start: usize, end: usize, module_id: ModuleId) -> Self {

View File

@ -99,9 +99,30 @@ async fn execute(test_name: &str, render_to_png: bool) {
});
}
Err(e) => {
assert_snapshot(test_name, "Error from executing", || {
insta::assert_snapshot!("execution_error", e);
});
match e {
crate::errors::ExecError::Kcl(error) => {
// Snapshot the KCL error with a fancy graphical report.
// This looks like a Cargo compile error, with arrows pointing
// to source code, underlines, etc.
let report = crate::errors::Report {
error,
filename: format!("{test_name}.kcl"),
kcl_source: read("input.kcl", test_name),
};
let report = miette::Report::new(report);
let report = format!("{:?}", report);
assert_snapshot(test_name, "Error from executing", || {
insta::assert_snapshot!("execution_error", report);
});
}
e => {
// These kinds of errors aren't expected to occur. We don't
// snapshot them because they indicate there's something wrong
// with the Rust test, not with the KCL code being tested.
panic!("{e}")
}
};
}
}
}

View File

@ -1,5 +1,13 @@
---
source: kcl/src/simulation_tests.rs
description: Error from executing argument_error.kcl
snapshot_kind: text
---
type: KclErrorDetails { source_ranges: [SourceRange([34, 35, 0])], message: "Expected an array but found Function" }
KCL Type error
× type: Expected an array but found Function
╭─[5:5]
4 │
5 │ map(f, [0, 1])
· ─
╰────

View File

@ -3,4 +3,11 @@ source: kcl/src/simulation_tests.rs
description: Error from executing array_elem_push_fail.kcl
snapshot_kind: text
---
undefined value: KclErrorDetails { source_ranges: [SourceRange([48, 54, 0])], message: "The array doesn't have any item at index 3" }
KCL UndefinedValue error
× undefined value: The array doesn't have any item at index 3
╭─[3:8]
2 │ pushedArr = push(arr, 4)
3 │ fail = arr[3]
· ──────
╰────

View File

@ -3,4 +3,11 @@ source: kcl/src/simulation_tests.rs
description: Error from executing array_index_oob.kcl
snapshot_kind: text
---
undefined value: KclErrorDetails { source_ranges: [SourceRange([13, 19, 0])], message: "The array doesn't have any item at index 0" }
KCL UndefinedValue error
× undefined value: The array doesn't have any item at index 0
╭─[2:5]
1 │ arr = []
2 │ x = arr[0]
· ──────
╰────

View File

@ -3,4 +3,10 @@ source: kcl/src/simulation_tests.rs
description: Error from executing comparisons_multiple.kcl
snapshot_kind: text
---
semantic: KclErrorDetails { source_ranges: [SourceRange([7, 13, 0])], message: "Expected a number, but found a boolean (true/false value)" }
KCL Semantic error
× semantic: Expected a number, but found a boolean (true/false value)
╭────
1 │ assert(3 == 3 == 3, "this should not compile")
· ──────
╰────

View File

@ -3,4 +3,11 @@ source: kcl/src/simulation_tests.rs
description: Error from executing import_constant.kcl
snapshot_kind: text
---
engine: KclErrorDetails { source_ranges: [SourceRange([0, 39, 0])], message: "Failed to read file `export_constant.kcl`: No such file or directory (os error 2)" }
KCL Engine error
× engine: Failed to read file `export_constant.kcl`: No such file or
│ directory (os error 2)
╭────
1 │ import three from "export_constant.kcl"
· ───────────────────────────────────────
╰────

View File

@ -3,4 +3,12 @@ source: kcl/src/simulation_tests.rs
description: Error from executing import_cycle1.kcl
snapshot_kind: text
---
engine: KclErrorDetails { source_ranges: [SourceRange([0, 35, 0])], message: "Failed to read file `import_cycle2.kcl`: No such file or directory (os error 2)" }
KCL Engine error
× engine: Failed to read file `import_cycle2.kcl`: No such file or directory
│ (os error 2)
╭─[1:1]
1 │ import two from "import_cycle2.kcl"
· ───────────────────────────────────
2 │
╰────

View File

@ -3,4 +3,11 @@ source: kcl/src/simulation_tests.rs
description: Error from executing import_side_effect.kcl
snapshot_kind: text
---
engine: KclErrorDetails { source_ranges: [SourceRange([0, 40, 0])], message: "Failed to read file `export_side_effect.kcl`: No such file or directory (os error 2)" }
KCL Engine error
× engine: Failed to read file `export_side_effect.kcl`: No such file or
│ directory (os error 2)
╭────
1 │ import foo from "export_side_effect.kcl"
· ────────────────────────────────────────
╰────

View File

@ -1,7 +1,13 @@
---
source: kcl/src/simulation_tests.rs
assertion_line: 103
description: Error from executing invalid_index_fractional.kcl
snapshot_kind: text
---
semantic: KclErrorDetails { source_ranges: [SourceRange([20, 28, 0])], message: "1.2 is not a valid index, indices must be whole numbers >= 0" }
KCL Semantic error
× semantic: 1.2 is not a valid index, indices must be whole numbers >= 0
╭─[2:5]
1 │ arr = [1, 2, 3]
2 │ x = arr[1.2]
· ────────
╰────

View File

@ -3,4 +3,11 @@ source: kcl/src/simulation_tests.rs
description: Error from executing invalid_index_negative.kcl
snapshot_kind: text
---
semantic: KclErrorDetails { source_ranges: [SourceRange([27, 33, 0])], message: "'-1' is negative, so you can't index an array with it" }
KCL Semantic error
× semantic: '-1' is negative, so you can't index an array with it
╭─[3:5]
2 │ i = -1
3 │ x = arr[i]
· ──────
╰────

View File

@ -3,4 +3,12 @@ source: kcl/src/simulation_tests.rs
description: Error from executing invalid_index_str.kcl
snapshot_kind: text
---
semantic: KclErrorDetails { source_ranges: [SourceRange([20, 28, 0])], message: "Only integers >= 0 can be used as the index of an array, but you're using a string" }
KCL Semantic error
× semantic: Only integers >= 0 can be used as the index of an array, but
│ you're using a string
╭─[2:5]
1 │ arr = [1, 2, 3]
2 │ x = arr["s"]
· ────────
╰────

View File

@ -1,7 +1,14 @@
---
source: kcl/src/simulation_tests.rs
assertion_line: 103
description: Error from executing invalid_member_object.kcl
snapshot_kind: text
---
semantic: KclErrorDetails { source_ranges: [SourceRange([14, 20, 0])], message: "Only arrays and objects can be indexed, but you're trying to index a number" }
KCL Semantic error
× semantic: Only arrays and objects can be indexed, but you're trying to
│ index a number
╭─[2:5]
1 │ num = 999
2 │ x = num[3]
· ──────
╰────

View File

@ -3,4 +3,12 @@ source: kcl/src/simulation_tests.rs
description: Error from executing invalid_member_object_prop.kcl
snapshot_kind: text
---
semantic: KclErrorDetails { source_ranges: [SourceRange([13, 26, 0])], message: "Only arrays and objects can be indexed, but you're trying to index a boolean (true/false value)" }
KCL Semantic error
× semantic: Only arrays and objects can be indexed, but you're trying to
│ index a boolean (true/false value)
╭─[2:5]
1 │ b = true
2 │ x = b["property"]
· ─────────────
╰────

View File

@ -1,7 +1,14 @@
---
source: kcl/src/simulation_tests.rs
assertion_line: 116
description: Error from executing non_string_key_of_object.kcl
snapshot_kind: text
---
semantic: KclErrorDetails { source_ranges: [SourceRange([26, 32, 0])], message: "Only strings can be used as the property of an object, but you're using a number" }
KCL Semantic error
× semantic: Only strings can be used as the property of an object, but
│ you're using a number
╭─[2:7]
1 │ obj = { key = 123 }
2 │ num = obj[3]
· ──────
╰────

View File

@ -3,4 +3,11 @@ source: kcl/src/simulation_tests.rs
description: Error from executing object_prop_not_found.kcl
snapshot_kind: text
---
undefined value: KclErrorDetails { source_ranges: [SourceRange([15, 25, 0])], message: "Property 'age' not found in object" }
KCL UndefinedValue error
× undefined value: Property 'age' not found in object
╭─[2:5]
1 │ obj = { }
2 │ k = obj["age"]
· ──────────
╰────

View File

@ -3,4 +3,12 @@ source: kcl/src/simulation_tests.rs
description: Error from executing pipe_substitution_inside_function_called_from_pipeline.kcl
snapshot_kind: text
---
semantic: KclErrorDetails { source_ranges: [SourceRange([106, 107, 0])], message: "cannot use % outside a pipe expression" }
KCL Semantic error
× semantic: cannot use % outside a pipe expression
╭─[6:10]
5 │
6 │ answer = %
· ─
7 │ |> f(%)
╰────