Skip to content

Commit 8350402

Browse files
committed
Handle test end and switch to msgpack
1 parent e3a7cb5 commit 8350402

6 files changed

Lines changed: 128 additions & 50 deletions

File tree

Cargo.lock

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -529,16 +529,6 @@ dependencies = [
529529
"serde",
530530
]
531531

532-
[[package]]
533-
name = "bincode"
534-
version = "2.0.1"
535-
source = "registry+https://github.com/rust-lang/crates.io-index"
536-
checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740"
537-
dependencies = [
538-
"serde",
539-
"unty",
540-
]
541-
542532
[[package]]
543533
name = "bit-set"
544534
version = "0.8.0"
@@ -1392,7 +1382,6 @@ dependencies = [
13921382
name = "egui_kittest"
13931383
version = "0.34.1"
13941384
dependencies = [
1395-
"bincode 2.0.1",
13961385
"dify",
13971386
"document-features",
13981387
"eframe",
@@ -1403,6 +1392,7 @@ dependencies = [
14031392
"kittest",
14041393
"open",
14051394
"pollster",
1395+
"rmp-serde",
14061396
"serde",
14071397
"tempfile",
14081398
"toml",
@@ -3587,7 +3577,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
35873577
checksum = "fa9dae7b05c02ec1a6bc9bcf20d8bc64a7dcbf57934107902a872014899b741f"
35883578
dependencies = [
35893579
"anyhow",
3590-
"bincode 1.3.3",
3580+
"bincode",
35913581
"byteorder",
35923582
"cfg-if",
35933583
"itertools 0.10.5",
@@ -3895,6 +3885,25 @@ dependencies = [
38953885
"windows-sys 0.52.0",
38963886
]
38973887

3888+
[[package]]
3889+
name = "rmp"
3890+
version = "0.8.15"
3891+
source = "registry+https://github.com/rust-lang/crates.io-index"
3892+
checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c"
3893+
dependencies = [
3894+
"num-traits",
3895+
]
3896+
3897+
[[package]]
3898+
name = "rmp-serde"
3899+
version = "1.3.1"
3900+
source = "registry+https://github.com/rust-lang/crates.io-index"
3901+
checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155"
3902+
dependencies = [
3903+
"rmp",
3904+
"serde",
3905+
]
3906+
38983907
[[package]]
38993908
name = "ron"
39003909
version = "0.12.0"
@@ -4389,7 +4398,7 @@ version = "5.3.0"
43894398
source = "registry+https://github.com/rust-lang/crates.io-index"
43904399
checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925"
43914400
dependencies = [
4392-
"bincode 1.3.3",
4401+
"bincode",
43934402
"fancy-regex",
43944403
"flate2",
43954404
"fnv",
@@ -4832,12 +4841,6 @@ version = "0.9.0"
48324841
source = "registry+https://github.com/rust-lang/crates.io-index"
48334842
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
48344843

4835-
[[package]]
4836-
name = "unty"
4837-
version = "0.0.4"
4838-
source = "registry+https://github.com/rust-lang/crates.io-index"
4839-
checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
4840-
48414844
[[package]]
48424845
name = "ureq"
48434846
version = "3.3.0"

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ ahash = { version = "0.8.12", default-features = false, features = [
7878
android_logger = "0.15.1"
7979
arboard = { version = "3.6.1", default-features = false }
8080
backtrace = "0.3.76"
81-
bincode = { version = "2.0.1", default-features = false, features = ["std", "serde"] }
81+
rmp-serde = "1.3.1"
8282
bitflags = "2.9.4"
8383
bytemuck = "1.24.0"
8484
cint = "0.3.1"

crates/egui_kittest/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ snapshot = ["dep:dify", "dep:image", "dep:open", "dep:tempfile", "image/png"]
2929
## Expose the [`inspector_api`] wire protocol used to talk to the external
3030
## `kittest_inspector` binary. Pull this in if you're building a tool that consumes the
3131
## same stream — the binary itself enables this transitively.
32-
inspector_api = ["dep:bincode", "egui/serde"]
32+
inspector_api = ["dep:rmp-serde", "egui/serde"]
3333

3434
## Stream frames + accesskit tree to a `kittest_inspector` window for live debugging.
3535
## Auto-launches when the `KITTEST_INSPECTOR` env var is truthy.
@@ -60,7 +60,7 @@ wgpu = { workspace = true, features = ["metal", "dx12", "vulkan", "gles"], optio
6060
dify = { workspace = true, optional = true }
6161

6262
# inspector dependencies
63-
bincode = { workspace = true, optional = true }
63+
rmp-serde = { workspace = true, optional = true }
6464

6565
# Enable this when generating docs.
6666
document-features = { workspace = true, optional = true }

crates/egui_kittest/src/inspector.rs

Lines changed: 80 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use egui::mutex::Mutex;
2424
use crate::inspector_api::{
2525
Frame, HarnessMessage, InspectorCommand, SourceView, read_message, write_message,
2626
};
27-
use crate::{Harness, Plugin};
27+
use crate::{Harness, Plugin, TestResult};
2828

2929
/// Environment variable: when set to a truthy value, every harness auto-launches an inspector.
3030
pub const INSPECTOR_ENV_VAR: &str = "KITTEST_INSPECTOR";
@@ -115,16 +115,48 @@ impl<S> Plugin<S> for InspectorPlugin {
115115
self.block_until_resume(harness);
116116
}
117117
}
118+
119+
/// Test ended — send `Finished` (carrying the panic location in its `SourceView` when
120+
/// the panic's file matches the test entry), then block until the user dismisses with a
121+
/// Step / Run / Play command. The dismiss unblocks us; the harness finishes dropping on
122+
/// the way out.
123+
fn on_test_result(&mut self, harness: &mut Harness<'_, S>, result: TestResult<'_>) {
124+
if self.conn.broken {
125+
return;
126+
}
127+
128+
let (ok, message, panic_loc) = match result {
129+
TestResult::Pass => (true, None, None),
130+
TestResult::Fail { message, location } => (
131+
false,
132+
message.map(str::to_owned),
133+
location.map(|loc| (loc.file.clone(), loc.line)),
134+
),
135+
};
136+
137+
let source = build_source_view(
138+
harness.entry_location(),
139+
harness.consumed_event_locations(),
140+
panic_loc.as_ref(),
141+
);
142+
self.conn.write(&HarnessMessage::Finished {
143+
ok,
144+
message,
145+
source,
146+
});
147+
// Park here until the user dismisses with Step/Run/Play. `block_until_resume` exits
148+
// on any of those (they all transition out of `Paused`); `Pause` is a no-op; `Handle`
149+
// still works so the user can poke at the final UI on failure. The mode mutation it
150+
// leaves behind is harmless — the plugin is about to drop.
151+
self.mode = Mode::Paused;
152+
self.block_until_resume(harness);
153+
}
118154
}
119155

120156
impl InspectorPlugin {
121157
/// Send a frame for this step and apply the current mode's blocking / draining policy.
122158
/// `after_run` is handled separately — it only transitions `RunOnce → Paused`.
123-
fn handle_after_step<S>(
124-
&mut self,
125-
harness: &mut Harness<'_, S>,
126-
tree: &accesskit::TreeUpdate,
127-
) {
159+
fn handle_after_step<S>(&mut self, harness: &mut Harness<'_, S>, tree: &accesskit::TreeUpdate) {
128160
if self.conn.broken {
129161
return;
130162
}
@@ -209,7 +241,7 @@ impl InspectorPlugin {
209241
harness.input_mut().events.push(event);
210242
}
211243
// `step_no_side_effects` returns the tree directly — we can't receive it via
212-
// `on_accesskit_update` because we're inside an outer `plugin_dispatch`.
244+
// `after_step` because nested plugin dispatches are suppressed.
213245
let tree = harness.step_no_side_effects();
214246
self.send_frame(harness, Some(tree));
215247
}
@@ -231,10 +263,12 @@ impl InspectorPlugin {
231263
}
232264
};
233265
let ppp = harness.ctx.pixels_per_point();
234-
let call_site = harness.entry_location();
235-
let event_sites = harness.consumed_event_locations().to_vec();
236-
self.conn
237-
.send_frame(&image, ppp, tree, call_site, &event_sites);
266+
let source = build_source_view(
267+
harness.entry_location(),
268+
harness.consumed_event_locations(),
269+
None,
270+
);
271+
self.conn.send_frame(&image, ppp, tree, source);
238272
}
239273
}
240274

@@ -298,8 +332,7 @@ impl Connection {
298332
image: &image::RgbaImage,
299333
pixels_per_point: f32,
300334
accesskit: Option<accesskit::TreeUpdate>,
301-
call_site: Option<&'static Location<'static>>,
302-
event_sites: &[&'static Location<'static>],
335+
source: Option<SourceView>,
303336
) {
304337
if self.broken {
305338
return;
@@ -313,7 +346,7 @@ impl Connection {
313346
rgba: image.as_raw().clone(),
314347
accesskit,
315348
label: self.label.clone(),
316-
source: build_source_view(call_site, event_sites),
349+
source,
317350
};
318351
self.write(&HarnessMessage::Frame(Box::new(frame)));
319352
}
@@ -353,13 +386,16 @@ fn run_reader(mut reader: BufReader<ChildStdout>, tx: &mpsc::Sender<InspectorCom
353386
}
354387

355388
/// Build the [`SourceView`] payload for a frame: pick the `.run()`/`.step()` caller's file
356-
/// as the anchor, and record each event's line within that same file.
389+
/// as the anchor, and record each event's line within that same file. `panic_loc` is set
390+
/// only on the final frame after a failed test — and only included in the output if the
391+
/// panic's file matches the anchor (otherwise there's no highlight to attach).
357392
///
358393
/// `#[track_caller]` chains through the entire event-queuing API, so each `Location` points
359394
/// directly at the user's test source — no backtrace walking needed.
360395
fn build_source_view(
361396
call_site: Option<&'static Location<'static>>,
362397
event_sites: &[&'static Location<'static>],
398+
panic_loc: Option<&(String, u32)>,
363399
) -> Option<SourceView> {
364400
let call = call_site?;
365401
let path = call.file().to_owned();
@@ -368,25 +404,53 @@ fn build_source_view(
368404
.filter(|loc| loc.file() == path)
369405
.map(|loc| loc.line())
370406
.collect();
407+
let panic_line = panic_loc
408+
.filter(|(file, _)| file == &path)
409+
.map(|(_, line)| *line);
371410
Some(SourceView {
372411
path: path.clone(),
373412
contents: read_source_file(&path),
374413
call_site_line: Some(call.line()),
375414
event_lines,
415+
panic_line,
376416
})
377417
}
378418

379419
/// Read the full contents of a source file, cached per path (including negative results).
420+
///
421+
/// `path` comes from `std::panic::Location::file()`, which the compiler reports relative to
422+
/// the *workspace* root. Cargo runs tests with CWD set to the *crate* root, so for a
423+
/// workspace crate at `<workspace>/crates/foo/` the compiler-reported path is
424+
/// `crates/foo/src/…` but CWD is `<workspace>/crates/foo/`. We try as-is first (handles
425+
/// absolute paths and single-crate layouts), then walk up from CWD looking for an ancestor
426+
/// where `ancestor.join(path)` resolves.
380427
fn read_source_file(path: &str) -> Option<String> {
381428
static CACHE: LazyLock<Mutex<HashMap<String, Option<String>>>> =
382429
LazyLock::new(|| Mutex::new(HashMap::new()));
383430
let mut cache = CACHE.lock();
384431
cache
385432
.entry(path.to_owned())
386-
.or_insert_with(|| std::fs::read_to_string(path).ok())
433+
.or_insert_with(|| resolve_and_read(path))
387434
.clone()
388435
}
389436

437+
fn resolve_and_read(path: &str) -> Option<String> {
438+
if let Ok(contents) = std::fs::read_to_string(path) {
439+
return Some(contents);
440+
}
441+
if std::path::Path::new(path).is_absolute() {
442+
return None;
443+
}
444+
let mut cursor = std::env::current_dir().ok()?;
445+
// `pop()` returns false once we've hit the root, which terminates the search.
446+
while cursor.pop() {
447+
if let Ok(contents) = std::fs::read_to_string(cursor.join(path)) {
448+
return Some(contents);
449+
}
450+
}
451+
None
452+
}
453+
390454
/// Read [`INSPECTOR_ENV_VAR`] once and cache. Exposed to [`crate::Harness::from_builder`]
391455
/// so it can auto-register an [`InspectorPlugin`].
392456
pub(crate) fn env_enabled() -> bool {

crates/egui_kittest/src/inspector_api.rs

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
//! [`InspectorCommand`]s to the harness's stdout whenever the user drives the UI. Shutdown
88
//! is detected on either side via EOF — no explicit goodbye message.
99
//!
10-
//! Messages are framed as a 4-byte big-endian length followed by a bincode-encoded body.
11-
//! Anything the inspector wants to log goes to a file on disk (see the inspector crate's
12-
//! `log_diag`), keeping stdout reserved for protocol traffic.
10+
//! Messages are framed as a 4-byte big-endian length followed by a MessagePack-encoded body
11+
//! (`rmp-serde`). Anything the inspector wants to log goes to a file on disk (see the
12+
//! inspector crate's `log_diag`), keeping stdout reserved for protocol traffic.
1313
//!
1414
//! Living inside `egui_kittest` (rather than the inspector crate) lets the inspector be
1515
//! released independently — it just consumes whichever protocol version ships with the
@@ -34,6 +34,9 @@ pub struct SourceView {
3434
pub call_site_line: Option<u32>,
3535
/// Line numbers of events consumed by this frame's step, in queue order.
3636
pub event_lines: Vec<u32>,
37+
/// Line number of a panic captured in this file. The inspector highlights this line in
38+
/// red. Set on the [`HarnessMessage::Finished`] source view when a panic was captured.
39+
pub panic_line: Option<u32>,
3740
}
3841

3942
/// A single rendered frame plus the accesskit tree update produced by the harness step.
@@ -68,6 +71,18 @@ pub enum HarnessMessage {
6871
/// The harness is now either blocked (`true`) waiting for an [`InspectorCommand`], or
6972
/// running freely (`false`).
7073
Blocked(bool),
74+
/// The test has ended. Implies [`Self::Blocked`]`(true)`: the harness blocks after
75+
/// sending this, and any subsequent `Step` / `Run` / `Play` command dismisses the result
76+
/// and lets the harness drop.
77+
Finished {
78+
/// `true` on pass; `false` if a panic was in progress when the harness dropped.
79+
ok: bool,
80+
/// Panic message, if captured (requires `egui_kittest::install_panic_hook()`).
81+
message: Option<String>,
82+
/// Final-frame source context: the test entry point's file, with the panic line (if
83+
/// any and if it matches that file) recorded in [`SourceView::panic_line`].
84+
source: Option<SourceView>,
85+
},
7186
}
7287

7388
/// Sent inspector → harness at any time to drive test execution.
@@ -95,7 +110,7 @@ pub enum InspectorCommand {
95110

96111
const MAX_MESSAGE_BYTES: usize = 256 * 1024 * 1024; // 256 MiB sanity cap
97112

98-
/// Read a length-prefixed bincode message.
113+
/// Read a length-prefixed MessagePack message.
99114
///
100115
/// # Errors
101116
/// I/O or decode failures.
@@ -115,13 +130,11 @@ where
115130
}
116131
let mut buf = vec![0u8; len];
117132
reader.read_exact(&mut buf)?;
118-
let config = bincode::config::standard();
119-
let (value, _) = bincode::serde::decode_from_slice(&buf, config)
120-
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err.to_string()))?;
121-
Ok(value)
133+
rmp_serde::from_slice(&buf)
134+
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err.to_string()))
122135
}
123136

124-
/// Write a length-prefixed bincode message.
137+
/// Write a length-prefixed MessagePack message.
125138
///
126139
/// # Errors
127140
/// I/O or encode failures.
@@ -130,8 +143,7 @@ where
130143
W: Write,
131144
T: serde::Serialize,
132145
{
133-
let config = bincode::config::standard();
134-
let bytes = bincode::serde::encode_to_vec(value, config)
146+
let bytes = rmp_serde::to_vec(value)
135147
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err.to_string()))?;
136148
let len = u32::try_from(bytes.len())
137149
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;

deny.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ version = 2
3333
ignore = [
3434
"RUSTSEC-2024-0320", # unmaintained yaml-rust pulled in by syntect
3535
"RUSTSEC-2024-0436", # unmaintained paste pulled via metal/wgpu, see https://github.com/gfx-rs/metal-rs/issues/349
36-
"RUSTSEC-2025-0141", # https://rustsec.org/advisories/RUSTSEC-2025-0141 - bincode is unmaintained - https://git.sr.ht/~stygianentity/bincode/tree/v3.0/item/README.md
3736
]
3837

3938
[bans]

0 commit comments

Comments
 (0)