#![cfg(feature = "native")]
use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use surf_parse::render_native::{self, BlockTier, NativeBlock};
fn corpus_files() -> Vec<PathBuf> {
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/corpus");
let mut files: Vec<PathBuf> = fs::read_dir(&dir)
.expect("tests/corpus exists")
.map(|e| e.unwrap().path())
.filter(|p| p.extension().map(|e| e == "surf").unwrap_or(false))
.collect();
files.sort();
assert!(!files.is_empty(), "corpus must not be empty");
files
}
fn snapshot_path(fixture: &Path, target: &str) -> PathBuf {
let stem = fixture.file_stem().unwrap().to_str().unwrap();
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/snapshots")
.join(format!("{stem}.{target}.snap"))
}
fn assert_snapshot(fixture: &Path, target: &str, actual: &str) {
let path = snapshot_path(fixture, target);
if std::env::var("UPDATE_SNAPSHOTS").is_ok() {
fs::write(&path, actual).expect("write snapshot");
return;
}
let expected = fs::read_to_string(&path).unwrap_or_else(|_| {
panic!(
"missing snapshot {} — run UPDATE_SNAPSHOTS=1 cargo test --test corpus --features native",
path.display()
)
});
assert_eq!(
expected, actual,
"snapshot drift for {} ({target}) — if intentional, regenerate with UPDATE_SNAPSHOTS=1",
fixture.display()
);
}
#[test]
fn corpus_render_html_pinned() {
for fixture in corpus_files() {
let src = fs::read_to_string(&fixture).unwrap();
let doc = surf_parse::parse(&src).doc;
assert_snapshot(&fixture, "html", &doc.to_html());
}
}
#[test]
fn corpus_to_native_pinned() {
for fixture in corpus_files() {
let src = fs::read_to_string(&fixture).unwrap();
let doc = surf_parse::parse(&src).doc;
let native = render_native::to_native_blocks(&doc);
let json = serde_json::to_string_pretty(&native).unwrap();
assert_snapshot(&fixture, "native", &json);
}
}
#[cfg(feature = "uniffi")]
#[test]
fn corpus_styled_themes_pinned() {
let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/corpus/tier2-site.surf");
let src = fs::read_to_string(&fixture).unwrap();
let mut out = String::new();
for pack in ["surf", "comic"] {
let doc = surf_parse::ffi::parse_to_native_styled(
src.clone(),
Some(pack.to_string()),
None,
None,
)
.expect("styled parse");
out.push_str(&format!(
"=== pack: {pack} ===\n{}\n",
serde_json::to_string_pretty(&doc.theme).unwrap()
));
}
assert_snapshot(&fixture, "themes", &out);
}
fn kind_of(block: &surf_parse::Block) -> String {
serde_json::to_value(block).unwrap()["kind"]
.as_str()
.unwrap()
.to_string()
}
fn walk<'a>(blocks: &'a [surf_parse::Block], out: &mut Vec<&'a surf_parse::Block>) {
for b in blocks {
out.push(b);
}
}
#[test]
fn corpus_tier_manifest_consistent() {
let mut seen: BTreeMap<String, BlockTier> = BTreeMap::new();
let mut tier_counts: BTreeMap<&'static str, usize> = BTreeMap::new();
for fixture in corpus_files() {
let src = fs::read_to_string(&fixture).unwrap();
let doc = surf_parse::parse(&src).doc;
let mut all: Vec<&surf_parse::Block> = Vec::new();
walk(&doc.blocks, &mut all);
for block in all {
let tier = render_native::block_tier(block);
let kind = kind_of(block);
if let Some(prev) = seen.insert(kind.clone(), tier) {
assert_eq!(prev, tier, "tier classification for {kind} must be stable");
}
*tier_counts
.entry(match tier {
BlockTier::Content => "content",
BlockTier::Site => "site",
BlockTier::Chrome => "chrome",
BlockTier::Degraded => "degraded",
})
.or_default() += 1;
let native = render_native::to_native_blocks(&surf_parse::SurfDoc {
blocks: vec![block.clone()],
..doc.clone()
});
match tier {
BlockTier::Degraded => {
assert!(
matches!(native[0], NativeBlock::Markdown { .. }),
"Degraded-tier {kind} must convert to Markdown"
);
}
_ => {
if kind != "Markdown" {
assert!(
!matches!(native[0], NativeBlock::Markdown { .. }),
"structured-tier {kind} must not silently degrade"
);
}
}
}
}
}
for tier in ["content", "site", "chrome", "degraded"] {
assert!(
tier_counts.get(tier).copied().unwrap_or(0) > 0,
"corpus must cover the {tier} tier (counts: {tier_counts:?})"
);
}
assert!(
seen.len() >= 45,
"corpus covers {} block kinds — expected ≥ 45: {:?}",
seen.len(),
seen.keys().collect::<Vec<_>>()
);
}