Skip to content

Commit 67cac8e

Browse files
committed
[hermes] Add UI testing
gherrit-pr-id: G1bd8ca80c7b97b4c799cec1504d281ae79f329b1
1 parent 9fb6e3f commit 67cac8e

File tree

9 files changed

+223
-3
lines changed

9 files changed

+223
-3
lines changed

tools/Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tools/hermes/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@ publish.workspace = true
1010
log = "0.4.29"
1111
miette = { version = "7.6.0", features = ["derive", "fancy"] }
1212
proc-macro2 = { version = "1.0.105", features = ["span-locations"] }
13+
serde = { version = "1.0.228", features = ["derive"] }
14+
serde_json = "1.0.149"
1315
syn = { version = "2.0.114", features = ["full", "visit", "extra-traits", "parsing"] }
1416
thiserror = "2.0.18"
1517

1618
[dev-dependencies]
1719
syn = { version = "2.0.114", features = ["printing", "full", "visit", "extra-traits", "parsing"] }
1820
proc-macro2 = { version = "1.0.105", features = ["span-locations"] }
21+
ui_test = "0.30.4"

tools/hermes/src/main.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,39 @@
11
mod errors;
22
mod parse;
3+
mod ui_test_shim;
34

4-
fn main() {}
5+
use std::{env, fs, path::PathBuf, process::exit};
6+
7+
fn main() {
8+
if env::var("HERMES_UI_TEST_MODE").is_ok() {
9+
ui_test_shim::run();
10+
return;
11+
}
12+
13+
let args: Vec<String> = env::args().collect();
14+
if args.len() < 2 {
15+
eprintln!("Usage: hermes <file.rs>");
16+
exit(1);
17+
}
18+
19+
let file_path = PathBuf::from(&args[1]);
20+
let source = match fs::read_to_string(&file_path) {
21+
Ok(s) => s,
22+
Err(e) => {
23+
eprintln!("Error reading file: {}", e);
24+
exit(1);
25+
}
26+
};
27+
28+
let mut has_errors = false;
29+
parse::visit_hermes_items_in_file(&file_path, &source, |res| {
30+
if let Err(e) = res {
31+
has_errors = true;
32+
eprint!("{:?}", miette::Report::new(e));
33+
}
34+
});
35+
36+
if has_errors {
37+
exit(1);
38+
}
39+
}

tools/hermes/src/parse.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ where
7777
/// Parses the given Rust source code from a file path and invokes the callback `f`
7878
/// for each item annotated with a `/// ```lean` block. Parsing errors and generated
7979
/// items will be associated with this file path.
80-
fn visit_hermes_items_in_file<F>(path: &Path, source: &str, f: F)
80+
pub fn visit_hermes_items_in_file<F>(path: &Path, source: &str, f: F)
8181
where
8282
F: FnMut(Result<ParsedLeanItem, HermesError>),
8383
{
@@ -94,7 +94,7 @@ where
9494
.as_ref()
9595
.map(|p| p.display().to_string())
9696
.unwrap_or_else(|| "<input>".to_string());
97-
dbg!(&f);
97+
9898
f
9999
};
100100
let _x = source_file

tools/hermes/src/ui_test_shim.rs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
use std::{env, fs, path::PathBuf, process::exit};
2+
3+
use miette::Diagnostic as _;
4+
use serde::Serialize;
5+
6+
use crate::{errors::HermesError, parse};
7+
8+
/// The entrypoint for running under the `ui_test` crate, which expects us to be
9+
/// `rustc`. This is a bit of a hack, but it works.
10+
pub fn run() {
11+
let args: Vec<String> = env::args().collect();
12+
13+
// Spoof version if requested
14+
if args.contains(&"-vV".to_string()) || args.contains(&"--version".to_string()) {
15+
println!("rustc 1.93.0-nightly (hermes-shim)");
16+
println!("binary: rustc");
17+
println!("commit-hash: 0000000000000000000000000000000000000000");
18+
println!("commit-date: 2025-01-01");
19+
println!("host: x86_64-unknown-linux-gnu");
20+
println!("release: 1.93.0-nightly");
21+
exit(0);
22+
}
23+
24+
// Find the file (ignoring rustc flags like --out-dir)
25+
let file_path = args
26+
.iter()
27+
.skip(1)
28+
.find(|arg| arg.ends_with(".rs") && !arg.starts_with("--"))
29+
.map(PathBuf::from)
30+
.unwrap_or_else(|| {
31+
// If no file found, maybe it's just a flag check. Exit successfully
32+
// to appease ui_test.
33+
exit(0);
34+
});
35+
36+
// Run logic with JSON emitter
37+
let source = fs::read_to_string(&file_path).unwrap_or_default();
38+
let mut has_errors = false;
39+
40+
parse::visit_hermes_items_in_file(&file_path, &source, |res| {
41+
if let Err(e) = res {
42+
has_errors = true;
43+
emit_rustc_json(&e, &source, file_path.to_str().unwrap());
44+
}
45+
});
46+
47+
if has_errors {
48+
exit(1);
49+
}
50+
}
51+
52+
#[derive(Serialize)]
53+
struct RustcDiagnostic {
54+
message: String,
55+
level: String,
56+
spans: Vec<RustcSpan>,
57+
children: Vec<RustcDiagnostic>,
58+
rendered: String,
59+
}
60+
61+
#[derive(Serialize)]
62+
struct RustcSpan {
63+
file_name: String,
64+
byte_start: usize,
65+
byte_end: usize,
66+
line_start: usize,
67+
line_end: usize,
68+
column_start: usize,
69+
column_end: usize,
70+
is_primary: bool,
71+
text: Vec<RustcSpanLine>, // ui_test sometimes checks the snippet context
72+
}
73+
74+
#[derive(Serialize)]
75+
struct RustcSpanLine {
76+
text: String,
77+
highlight_start: usize,
78+
highlight_end: usize,
79+
}
80+
81+
pub fn emit_rustc_json(e: &HermesError, source: &str, file: &str) {
82+
let msg = e.to_string();
83+
// Use miette's span to get byte offsets.
84+
let span = e.labels().and_then(|mut l| l.next());
85+
86+
let mut spans = Vec::new();
87+
if let Some(labeled_span) = span {
88+
let offset = labeled_span.offset();
89+
let len = labeled_span.len();
90+
91+
// Calculate lines/cols manually (miette makes this hard to extract
92+
// without a Report). This is isolated here now, so it's fine.
93+
let prefix = &source[..offset];
94+
let line_start = prefix.lines().count().max(1);
95+
let last_nl = prefix.rfind('\n').map(|i| i + 1).unwrap_or(0);
96+
let column_start = (offset - last_nl) + 1;
97+
98+
// Grab the line text for the snippet
99+
let line_end_idx = source[offset..].find('\n').map(|i| offset + i).unwrap_or(source.len());
100+
let line_text = source[last_nl..line_end_idx].to_string();
101+
102+
spans.push(RustcSpan {
103+
file_name: file.to_string(),
104+
byte_start: offset,
105+
byte_end: offset + len,
106+
line_start,
107+
line_end: line_start, // Assuming single line for simplicity
108+
column_start,
109+
column_end: column_start + len,
110+
is_primary: true,
111+
text: vec![RustcSpanLine {
112+
text: line_text,
113+
highlight_start: column_start,
114+
highlight_end: column_start + len,
115+
}],
116+
});
117+
}
118+
119+
let diag = RustcDiagnostic {
120+
message: msg.clone(),
121+
level: "error".to_string(),
122+
spans,
123+
children: vec![],
124+
rendered: format!("error: {}\n", msg),
125+
};
126+
127+
eprintln!("{}", serde_json::to_string(&diag).unwrap());
128+
}

tools/hermes/tests/ui.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
use std::{path::PathBuf, process::Command};
2+
3+
use ui_test::*;
4+
5+
#[test]
6+
fn ui() {
7+
std::env::set_var("HERMES_UI_TEST_MODE", "true");
8+
9+
let mut config = Config::rustc(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/ui"));
10+
11+
let args = Args::test().unwrap();
12+
config.with_args(&args);
13+
14+
let binary_path = compile_and_find_binary("hermes");
15+
config.program.program = binary_path;
16+
17+
run_tests(config).unwrap();
18+
}
19+
20+
fn compile_and_find_binary(name: &str) -> PathBuf {
21+
let manifest_dir = env!("CARGO_MANIFEST_DIR");
22+
let status = Command::new("cargo")
23+
.arg("build")
24+
.arg("--bin")
25+
.arg(name)
26+
.current_dir(manifest_dir)
27+
.status()
28+
.expect("Failed to execute cargo build");
29+
30+
assert!(status.success(), "Failed to build binary '{}'", name);
31+
32+
let mut path = PathBuf::from(manifest_dir);
33+
path.push("..");
34+
path.push("target");
35+
path.push("debug");
36+
path.push(name);
37+
38+
assert!(path.exists(), "Binary not found at {:?}", path);
39+
path
40+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/// ```lean
2+
//~^ ERROR: Unclosed ```lean block in documentation
3+
unsafe fn unsafe_op(x: u32) -> u32 { x }
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
error: Documentation block error: Unclosed ```lean block in documentation
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
//@ check-pass
2+
3+
/// ```lean
4+
/// ```
5+
fn safe_function(x: u32) -> u32 {
6+
x
7+
}

0 commit comments

Comments
 (0)