use std::path::Path;
use super::RegenerationState;
use crate::input::{
LoadedSvgSource, finalize_generate_webfonts_options, resolve_generate_webfonts_options,
};
use crate::pipeline::generate_webfonts_sync;
use crate::result::GenerateWebfontsResult;
use crate::types::{FontType, GlyphChange};
use crate::{FormatOptions, GenerateWebfontsOptions, TtfFormatOptions};
mod caching;
mod output;
mod regeneration;
mod rendering;
const D1: &str = "M2 2 L22 2 L22 22 Z";
const D2: &str = "M2 2 L22 2 L12 22 Z";
const D3: &str = "M4 4 L20 4 L20 20 L4 20 Z";
const D_CHANGED: &str = "M0 0 L24 0 L24 24 Z";
const TEST_TTF_TIMESTAMP: i64 = 1_700_000_000;
#[tokio::test]
async fn single_file_helpers_recover_consumed_results_and_match_regeneration() {
let dir = temp_dir();
let first = write_icon(&dir, "first", D1);
let second = write_icon(&dir, "second", D2);
let result = generate(vec![first.clone()], true);
let result = result.add_async(second.clone(), None, None).await.unwrap();
assert_eq!(
result.ttf_bytes(),
generate(vec![first.clone(), second.clone()], false).ttf_bytes()
);
let before_rejections = result.ttf_bytes().unwrap().to_vec();
let (result, error) = result
.add_async(second.clone(), None, None)
.await
.err()
.unwrap()
.into_parts();
assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
assert_eq!(error.to_string(), "Source ID/path already exists");
let result = result.unwrap();
assert_eq!(result.ttf_bytes().unwrap(), before_rejections.as_slice());
let (result, error) = result
.update_async("missing".into(), None)
.await
.err()
.unwrap()
.into_parts();
assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
assert_eq!(error.to_string(), "Unknown source ID/path");
let result = result.unwrap();
assert_eq!(result.ttf_bytes().unwrap(), before_rejections.as_slice());
let (result, error) = result
.remove_async("missing".into())
.await
.err()
.unwrap()
.into_parts();
assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
assert_eq!(error.to_string(), "Unknown source ID/path");
let result = result.unwrap();
assert_eq!(result.ttf_bytes().unwrap(), before_rejections.as_slice());
write_icon(&dir, "second", D_CHANGED);
let result = result.update_async(second.clone(), None).await.unwrap();
assert_eq!(
result.ttf_bytes(),
generate(vec![first.clone(), second.clone()], false).ttf_bytes()
);
let result = result.remove_async(first).await.unwrap();
assert_eq!(
result.ttf_bytes(),
generate(vec![second], false).ttf_bytes()
);
std::fs::remove_dir_all(dir).unwrap();
}
fn with_regeneration_state<T>(
result: &GenerateWebfontsResult,
read: impl FnOnce(&RegenerationState) -> T,
) -> T {
let state = result.regeneration_state.lock().unwrap();
read(state.as_ref().unwrap())
}
fn stable_format_options(with_metadata: bool) -> FormatOptions {
FormatOptions {
ttf: Some(TtfFormatOptions {
copyright: with_metadata.then(|| "Copyright 2026".to_owned()),
description: with_metadata.then(|| "Incremental test font".to_owned()),
ts: Some(TEST_TTF_TIMESTAMP),
url: with_metadata.then(|| "https://example.com".to_owned()),
version: with_metadata.then(|| "1.0".to_owned()),
}),
..Default::default()
}
}
fn temp_dir() -> std::path::PathBuf {
static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let unique = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let dir = std::env::temp_dir().join(format!("recalc-ut-{}-{unique}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn write_icon(dir: &Path, name: &str, d: &str) -> String {
write_icon_with_viewbox(dir, name, 24, 24, d)
}
fn write_icon_with_viewbox(dir: &Path, name: &str, width: u32, height: u32, d: &str) -> String {
let path = dir.join(format!("{name}.svg"));
std::fs::write(
&path,
format!("<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 {width} {height}\"><path d=\"{d}\"/></svg>"),
)
.unwrap();
path.to_string_lossy().into_owned()
}
fn load(paths: &[String]) -> Vec<LoadedSvgSource> {
paths
.iter()
.map(|path| LoadedSvgSource {
contents: std::fs::read_to_string(path).unwrap().into(),
glyph_name: Path::new(path)
.file_stem()
.and_then(|stem| stem.to_str())
.unwrap_or_default()
.to_owned(),
id: path.clone(),
})
.collect()
}
fn generate(paths: Vec<String>, incremental: bool) -> GenerateWebfontsResult {
generate_with_ligatures(paths, incremental, false, false)
}
fn generate_with_ligatures(
paths: Vec<String>,
incremental: bool,
ligature: bool,
with_metadata: bool,
) -> GenerateWebfontsResult {
let mut resolved = resolve_generate_webfonts_options(GenerateWebfontsOptions {
css: Some(false),
dest: "artifacts".to_owned(),
files: paths,
html: Some(false),
font_name: Some("rc".to_owned()),
format_options: Some(stable_format_options(with_metadata)),
ligature: Some(ligature),
incremental: Some(incremental),
write_files: Some(false),
types: Some(vec![
FontType::Svg,
FontType::Ttf,
FontType::Eot,
FontType::Woff,
FontType::Woff2,
]),
..Default::default()
})
.unwrap();
let source_files = load(&resolved.files);
finalize_generate_webfonts_options(&mut resolved, &source_files).unwrap();
generate_webfonts_sync(resolved, source_files).unwrap()
}
fn generate_with_css(paths: Vec<String>, incremental: bool) -> GenerateWebfontsResult {
let mut resolved = resolve_generate_webfonts_options(GenerateWebfontsOptions {
css: Some(true),
dest: "artifacts".to_owned(),
files: paths,
html: Some(false),
font_name: Some("rc".to_owned()),
format_options: Some(stable_format_options(false)),
ligature: Some(false),
incremental: Some(incremental),
write_files: Some(false),
types: Some(vec![FontType::Woff2]),
..Default::default()
})
.unwrap();
let source_files = load(&resolved.files);
finalize_generate_webfonts_options(&mut resolved, &source_files).unwrap();
generate_webfonts_sync(resolved, source_files).unwrap()
}
fn assert_same(actual: &GenerateWebfontsResult, expected: &GenerateWebfontsResult) {
assert_eq!(actual.svg_string(), expected.svg_string(), "svg mismatch");
assert_eq!(actual.ttf_bytes(), expected.ttf_bytes(), "ttf mismatch");
assert_eq!(actual.eot_bytes(), expected.eot_bytes(), "eot mismatch");
assert_eq!(actual.woff_bytes(), expected.woff_bytes(), "woff mismatch");
assert_eq!(
actual.woff2_bytes(),
expected.woff2_bytes(),
"woff2 mismatch"
);
}