355 lines
17 KiB
Rust
355 lines
17 KiB
Rust
//! 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",
|
||
};
|
||
// 字节有效不能补救缺版本身份,空白版本标签应单独报告 MissingRevision。
|
||
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",
|
||
] {
|
||
// 每个非法定位符都必须先被基础 URI 校验拒绝,后面的计数零验证未访问宿主。
|
||
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);
|
||
}
|