Files
endorphin-aster-vm/crates/frontend/tests/reader.rs
T

352 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Rust 测试框架验证 Reader 适配器:完整 UTF-8 快照、来源/版本身份、错误阶段和宿主 URI 授权边界。
use std::cell::Cell;
use std::error::Error;
use std::io::{self, Cursor, Read};
use std::path::PathBuf;
use std::sync::atomic::{AtomicUsize, Ordering};
use frontend::{
DiagnosticKind, EndorReader, EndorSourceProvider, ResolvedEndorSource, SourceOrigin,
SourceReadErrorKind, lex_source, parse_source, parse_source_expression,
};
/// 流测试包装器,让 read_to_end 每次最多取得一个字节,强制 UTF-8 字符跨多次底层读取。
struct OneByte<R>(R);
impl<R: Read> Read for OneByte<R> {
/// Reader 的 read_to_end 调用此适配器,把一次底层读取限制为至多一个字节。
fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
// 空输出缓冲保持零长度,其余读取都限制为一个字节,不能按单次读取独立解码。
let count = buffer.len().min(1);
self.0.read(&mut buffer[..count])
}
}
#[test]
/// 由 Rust 测试框架调用:文本与单字节流保留相同 CRLF/Unicode 原文并解析成相同 AST/Token;空流可读取但空模块无效,BOM 保留到词法阶段拒绝。
fn strings_and_split_utf8_streams_produce_the_same_ast_and_exact_text() {
// 保留注释、CRLF、日文和四字节 emoji,使原文精确性及跨读取解码同时受检。
let text = "// 日本語\r\nfunction 表示(): String { return \"こんにちは🌸\" }\r\n";
// String 文本已是 UTF-8,Reader 只包装拥有快照和显示名。
let memory = EndorReader::from_text("<editor>", text);
// 同一字节序列经 OneByte 拆分读取,Reader 应先完整聚合再验证 UTF-8。
let stream = EndorReader::from_stream("<pipe>", OneByte(Cursor::new(text.as_bytes()))).unwrap();
assert_eq!(memory.text(), text);
assert_eq!(stream.text(), text);
assert_eq!(
parse_source(&memory).unwrap(),
parse_source(&stream).unwrap()
);
assert_eq!(
memory.origin(),
&SourceOrigin::Text {
name: "<editor>".into()
}
);
assert_eq!(
stream.origin(),
&SourceOrigin::Stream {
name: "<pipe>".into()
}
);
assert_eq!(lex_source(&memory).unwrap(), lex_source(&stream).unwrap());
// 空字节流是合法 UTF-8 快照,是否为有效模块由后续语法阶段决定。
let empty = EndorReader::from_stream("<empty>", Cursor::new(b"")).unwrap();
assert!(empty.text().is_empty());
assert_eq!(
parse_source(&empty).unwrap_err().diagnostic.kind,
DiagnosticKind::Syntax
);
// Reader 不剥除 BOM,词法器看到原始开头字符后才决定是否接受。
let bom = EndorReader::from_stream("<bom>", Cursor::new("\u{feff}1".as_bytes())).unwrap();
assert_eq!(bom.text(), "\u{feff}1");
// 当前用例要求此阶段失败,随后检查具体类别、来源或底层原因,避免仅接受任意错误。
let error = lex_source(&bom).unwrap_err();
assert_eq!(error.diagnostic.kind, DiagnosticKind::Lexical);
assert_eq!(error.diagnostic.span.end, 3);
}
#[test]
/// 由 Rust 测试框架调用:语法与词法错误保留原始字节跨度、会话名及 Unicode 行列,源快照销毁后诊断仍可显示并提供错误链。
fn source_diagnostics_keep_session_names_unicode_locations_and_original_offsets() {
// 建立带来源名的原文快照,后续错误必须定位这份文本而非临时转换后的内容。
let source = EndorReader::from_text("<session:one/input:2>", "\"日\" + )");
// 当前用例要求此阶段失败,随后检查具体类别、来源或底层原因,避免仅接受任意错误。
let error = parse_source_expression(&source).unwrap_err();
assert_eq!(error.location.line, 1);
assert_eq!(error.location.column, 7);
assert_eq!(
&source.text()[error.diagnostic.span.start..error.diagnostic.span.end],
")"
);
assert_eq!(error.origin, source.origin().clone());
// 丢弃文本快照后仍应能显示已固化的来源和行列,不依赖借用的源码缓冲。
drop(source);
assert!(error.to_string().starts_with("<session:one/input:2>:1:7:"));
assert!(error.source().is_some());
// 建立带来源名的原文快照,后续错误必须定位这份文本而非临时转换后的内容。
let source = EndorReader::from_text("<editor>", "\r\n\"日\\q\"");
// 当前用例要求此阶段失败,随后检查具体类别、来源或底层原因,避免仅接受任意错误。
let error = lex_source(&source).unwrap_err();
assert_eq!(error.diagnostic.kind, DiagnosticKind::Lexical);
assert_eq!(error.location.line, 2);
assert_eq!(error.location.column, 3);
assert!(error.to_string().contains("unsupported string escape"));
}
#[test]
/// 由 Rust 测试框架调用:非法字节、截断多字节字符及合法前缀后非法字节都在读取解码阶段拒绝,不作有损替换。
fn incomplete_or_invalid_utf8_is_rejected_without_replacement_characters() {
// 覆盖无效起始字节、不完整 UTF-8 序列,以及一个合法 ASCII 前缀后的错误。
for bytes in [vec![0xff], vec![0xe6, 0x97], vec![b'a', 0xff]] {
// 当前用例要求此阶段失败,随后检查具体类别、来源或底层原因,避免仅接受任意错误。
let error = EndorReader::from_stream("<bytes>", OneByte(Cursor::new(bytes))).unwrap_err();
assert!(matches!(error.kind, SourceReadErrorKind::InvalidUtf8(_)));
assert!(error.to_string().contains("<bytes>: invalid UTF-8 at byte"));
assert!(error.source().is_some());
}
}
/// 模拟中途失败的流;布尔值记录是否已返回过第一个合法字节。
struct FailingStream(bool);
impl Read for FailingStream {
/// Reader 的 read_to_end 调用;先成功一次再失败,检验中途错误不会变成部分成功。
fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
// 空缓冲不消耗模拟状态,符合 Read 的零长度读取约定。
if buffer.is_empty() {
return Ok(0);
}
// 第一次非空读取给出 x,后续非空读取才返回 IO 错误。
if !self.0 {
self.0 = true;
buffer[0] = b'x';
return Ok(1);
}
Err(io::Error::other("stream failure"))
}
}
#[test]
/// 由 Rust 测试框架调用:读到一个有效字节后发生 IO 失败时仍整体失败,不向解析器提供半份快照。
fn a_failed_stream_never_returns_partially_read_source() {
// 当前用例要求此阶段失败,随后检查具体类别、来源或底层原因,避免仅接受任意错误。
let error = EndorReader::from_stream("<failed>", FailingStream(false)).unwrap_err();
assert!(matches!(error.kind, SourceReadErrorKind::Io(_)));
assert!(error.to_string().contains("stream failure"));
}
// 并行文件测试使用进程内递增编号避免重名;这里只需要原子唯一性,不发布共享数据。
static NEXT_FILE: AtomicUsize = AtomicUsize::new(0);
/// 本文件测试用的临时文件拥有者;构造写入字节,Drop 尽力删除,不留测试文件。
struct SourceFile(PathBuf);
impl SourceFile {
/// 文件测试创建临时输入;路径组合进程 ID 和原子编号,写入失败直接使测试失败。
fn new(bytes: &[u8]) -> Self {
// 放在系统临时目录,不依赖当前仓库布局,同一进程的不同测试实例用不同编号。
let path = std::env::temp_dir().join(format!(
"aster-reader-{}-{}.endor",
std::process::id(),
NEXT_FILE.fetch_add(1, Ordering::Relaxed)
));
std::fs::write(&path, bytes).unwrap();
Self(path)
}
}
impl Drop for SourceFile {
/// 退出文件测试作用域时清理文件,忽略已不存在等删除错误,避免 Drop 再次失败。
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
#[test]
/// 由 Rust 测试框架调用:文件保留原路径及 CRLF,坏编码与文件不存在分别产生读取编码错误和 NotFound IO 错误。
fn file_sources_preserve_the_path_and_read_errors_are_not_lexical_errors() {
// 建立合法模块临时文件,用作成功读取及路径来源的基线。
let file = SourceFile::new(b"function f(): Number { return 1 }\r\n");
// 建立带来源名的原文快照,后续错误必须定位这份文本而非临时转换后的内容。
let source = EndorReader::from_file(&file.0).unwrap();
assert_eq!(
source.origin(),
&SourceOrigin::File {
path: file.0.clone()
}
);
assert!(source.text().ends_with("\r\n"));
assert!(parse_source(&source).is_ok());
// 单字节 0xff 不是 UTF-8,打开文件成功后应在解码阶段拒绝。
let invalid = SourceFile::new(&[0xff]);
// 当前用例要求此阶段失败,随后检查具体类别、来源或底层原因,避免仅接受任意错误。
let error = EndorReader::from_file(&invalid.0).unwrap_err();
assert!(matches!(error.kind, SourceReadErrorKind::InvalidUtf8(_)));
assert!(error.to_string().contains(&invalid.0.display().to_string()));
// 从基线路径换一个未创建的扩展名,验证文件不存在作为 IO 错误暴露。
let missing = file.0.with_extension("missing");
// 当前用例要求此阶段失败,随后检查具体类别、来源或底层原因,避免仅接受任意错误。
let error = EndorReader::from_file(&missing).unwrap_err();
// 必须能解构出原始 IO 错误,之后检查 NotFound 或 PermissionDenied 的具体种类。
let SourceReadErrorKind::Io(error) = error.kind else {
panic!("expected I/O error");
};
assert_eq!(error.kind(), io::ErrorKind::NotFound);
}
/// URI 测试的可计数宿主,每次 load 返回不同版本和源码,用来检测隐式重复解析。
struct Provider {
// &self 的 load 通过 Cell 累计调用次数,不要求多线程共享。
calls: Cell<usize>,
}
impl EndorSourceProvider for Provider {
/// from_endor 调用,检查定位符并累计解析次数,返回新版本拥有字节。
fn load(&self, uri: &str) -> io::Result<ResolvedEndorSource> {
assert_eq!(uri, "endor:workspace/namespace/function");
// 每次解析选择新的版本编号,同时反映在标签与源码返回值中。
let version = self.calls.get() + 1;
self.calls.set(version);
Ok(ResolvedEndorSource {
revision: format!("revision-{version}"),
bytes: format!("function f(): Number {{ return {version} }}").into_bytes(),
})
}
}
#[test]
/// 由 Rust 测试框架调用:同一 URI 每次读取只调用一次宿主,后续解析不再解析 URI,旧快照不被新版本改写。
fn each_uri_read_resolves_once_and_old_snapshots_keep_their_original_revision() {
// 为本场景新建计数宿主,不与其他 URI 测试共享解析次数。
let provider = Provider {
calls: Cell::new(0),
};
// URI 作为宿主可解释的定位符,Reader 不把 namespace/function 猜成磁盘路径。
let uri = "endor:workspace/namespace/function";
// 第一份快照保存第一次解析的原文及 revision-1。
let first = EndorReader::from_endor(uri, &provider).unwrap();
// 同一 URI 再读取得第二份独立快照,不能回写第一份文本或来源。
let second = EndorReader::from_endor(uri, &provider).unwrap();
assert_eq!(provider.calls.get(), 2);
assert!(first.text().contains("return 1"));
assert!(second.text().contains("return 2"));
assert_eq!(
first.origin(),
&SourceOrigin::Endor {
uri: uri.into(),
revision: "revision-1".into()
}
);
assert_eq!(
second.origin(),
&SourceOrigin::Endor {
uri: uri.into(),
revision: "revision-2".into()
}
);
assert!(parse_source(&first).is_ok());
assert!(parse_source(&second).is_ok());
assert_eq!(provider.calls.get(), 2); // Parsing never resolves the URI again.
}
/// 错误测试的固定宿主,按指定版本标签和字节返回,Reader 仍须自行校验协议及编码。
struct StaticProvider {
// 借用测试静态版本标签,load 时复制为拥有字符串。
revision: &'static str,
// 借用测试静态字节,load 时复制给 Reader 解码。
bytes: &'static [u8],
}
impl EndorSourceProvider for StaticProvider {
/// from_endor 调用,把测试指定标签与字节复制到结果,不提前替 Reader 做编码/版本验证。
fn load(&self, _uri: &str) -> io::Result<ResolvedEndorSource> {
Ok(ResolvedEndorSource {
revision: self.revision.into(),
bytes: self.bytes.into(),
})
}
}
#[test]
/// 由 Rust 测试框架调用:URI 结果必须有非空版本身份;语法错误与编码错误都显示实际解析版本,空白标签被拒绝。
fn uri_snapshots_require_a_revision_and_preserve_it_in_source_diagnostics() {
// URI 作为宿主可解释的定位符,Reader 不把 namespace/function 猜成磁盘路径。
let uri = "endor:workspace/namespace/function";
// 使用合法版本身份但语法错误的源码,错误显示应继续包含该版本。
let provider = StaticProvider {
revision: "rev-one",
bytes: b"function f(): Number { return ) }",
};
// 建立带来源名的原文快照,后续错误必须定位这份文本而非临时转换后的内容。
let source = EndorReader::from_endor(uri, &provider).unwrap();
// 当前用例要求此阶段失败,随后检查具体类别、来源或底层原因,避免仅接受任意错误。
let error = parse_source(&source).unwrap_err();
assert_eq!(error.origin, source.origin().clone());
assert!(error.to_string().contains("[revision rev-one]"));
// 版本全为空白,即使字节是有效源码也必须在创建 URI 来源前拒绝。
let missing = StaticProvider {
revision: " \t",
bytes: b"1",
};
assert!(matches!(
EndorReader::from_endor(uri, &missing).unwrap_err().kind,
SourceReadErrorKind::MissingRevision
));
// 版本有效但字节非法,编码失败说明中也应保留已解析版本身份。
let invalid = StaticProvider {
revision: "rev-two",
// 借用测试静态字节,load 时复制给 Reader 解码。
bytes: &[0xff],
};
// 当前用例要求此阶段失败,随后检查具体类别、来源或底层原因,避免仅接受任意错误。
let error = EndorReader::from_endor(uri, &invalid).unwrap_err();
assert!(matches!(error.kind, SourceReadErrorKind::InvalidUtf8(_)));
assert!(error.to_string().contains("[revision rev-two]"));
}
/// 宿主拒绝授权的模拟,不提供任何替代源码或路径。
struct DeniedProvider;
impl EndorSourceProvider for DeniedProvider {
/// from_endor 调用,始终返回宿主 PermissionDenied,禁止任何本地文件回退。
fn load(&self, _uri: &str) -> io::Result<ResolvedEndorSource> {
Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"host denied source access",
))
}
}
#[test]
/// 由 Rust 测试框架调用:基础非法 URI 不触发宿主,合法 URI 被宿主拒绝时保留 PermissionDenied,不改走文件读取。
fn invalid_uris_do_not_call_the_provider_and_denials_do_not_fall_back_to_files() {
// 为本场景新建计数宿主,不与其他 URI 测试共享解析次数。
let provider = Provider {
calls: Cell::new(0),
};
// 覆盖空定位符、空地址、错误 scheme、空白和控制字符,均应在访问 provider 前被拒绝。
for uri in [
"",
"endor:",
"file:source",
"https://example.com",
"endor:space here",
"endor:bad\nname",
] {
assert!(matches!(
EndorReader::from_endor(uri, &provider).unwrap_err().kind,
SourceReadErrorKind::InvalidEndorUri
));
}
assert_eq!(provider.calls.get(), 0);
// 当前用例要求此阶段失败,随后检查具体类别、来源或底层原因,避免仅接受任意错误。
let error = EndorReader::from_endor("endor:anywhere", &DeniedProvider).unwrap_err();
// 必须能解构出原始 IO 错误,之后检查 NotFound 或 PermissionDenied 的具体种类。
let SourceReadErrorKind::Io(error) = error.kind else {
panic!("expected provider error");
};
assert_eq!(error.kind(), io::ErrorKind::PermissionDenied);
}