use rust_fontconfig::*;
#[test]
fn test_operating_system_font_expansion() {
let windows_os = OperatingSystem::Windows;
let no_ranges: &[UnicodeRange] = &[];
assert_eq!(windows_os.get_serif_fonts(no_ranges), vec!["Times New Roman".to_string()]);
assert_eq!(
windows_os.get_sans_serif_fonts(no_ranges),
vec!["Segoe UI", "Tahoma", "Microsoft Sans Serif", "MS Sans Serif", "Helv"]
.iter().map(|s| s.to_string()).collect::<Vec<_>>()
);
assert_eq!(
windows_os.get_monospace_fonts(no_ranges),
vec!["Segoe UI Mono", "Courier New", "Cascadia Code", "Cascadia Mono", "Consolas"]
.iter().map(|s| s.to_string()).collect::<Vec<_>>()
);
let macos_os = OperatingSystem::MacOS;
assert_eq!(
macos_os.get_serif_fonts(no_ranges),
vec!["Times", "New York", "Palatino"].iter().map(|s| s.to_string()).collect::<Vec<_>>()
);
assert_eq!(
macos_os.get_sans_serif_fonts(no_ranges),
vec!["San Francisco", "Helvetica Neue", "Lucida Grande"]
.iter().map(|s| s.to_string()).collect::<Vec<_>>()
);
assert_eq!(
macos_os.get_monospace_fonts(no_ranges),
vec!["SF Mono", "Menlo", "Monaco", "Courier", "Oxygen Mono", "Source Code Pro", "Fira Mono"]
.iter().map(|s| s.to_string()).collect::<Vec<_>>()
);
let linux_os = OperatingSystem::Linux;
assert_eq!(
linux_os.get_serif_fonts(no_ranges).len(),
8,
"Linux should have 8 serif fonts"
);
assert_eq!(
linux_os.get_sans_serif_fonts(no_ranges),
vec!["Ubuntu", "Arial", "DejaVu Sans", "Noto Sans", "Liberation Sans"]
.iter().map(|s| s.to_string()).collect::<Vec<_>>()
);
let families = vec!["Arial".to_string(), "sans-serif".to_string()];
let expanded = expand_font_families(&families, OperatingSystem::MacOS, no_ranges);
assert_eq!(expanded[0], "Arial");
assert_eq!(expanded[1], "San Francisco");
assert_eq!(expanded[2], "Helvetica Neue");
assert_eq!(expanded[3], "Lucida Grande");
let specific = vec!["MyCustomFont".to_string()];
let expanded = expand_font_families(&specific, OperatingSystem::Windows, no_ranges);
assert_eq!(expanded, vec!["MyCustomFont".to_string()]);
}
#[test]
fn test_unicode_range_matching() {
let latin_font = FcFont {
bytes: vec![0, 1, 2, 3], font_index: 0,
id: "latin-font".to_string(),
};
let cyrillic_font = FcFont {
bytes: vec![4, 5, 6, 7], font_index: 0,
id: "cyrillic-font".to_string(),
};
let cjk_font = FcFont {
bytes: vec![8, 9, 10, 11], font_index: 0,
id: "cjk-font".to_string(),
};
let latin_pattern = FcPattern {
name: Some("Latin Font".to_string()),
family: Some("Latin Family".to_string()),
unicode_ranges: vec![
UnicodeRange {
start: 0x0000,
end: 0x007F,
}, UnicodeRange {
start: 0x0080,
end: 0x00FF,
}, ],
..Default::default()
};
let cyrillic_pattern = FcPattern {
name: Some("Cyrillic Font".to_string()),
family: Some("Cyrillic Family".to_string()),
unicode_ranges: vec![
UnicodeRange {
start: 0x0400,
end: 0x04FF,
}, ],
..Default::default()
};
let cjk_pattern = FcPattern {
name: Some("CJK Font".to_string()),
family: Some("CJK Family".to_string()),
unicode_ranges: vec![
UnicodeRange {
start: 0x4E00,
end: 0x9FFF,
}, ],
..Default::default()
};
let mut cache = FcFontCache::default();
cache.with_memory_fonts(vec![
(latin_pattern.clone(), latin_font),
(cyrillic_pattern.clone(), cyrillic_font),
(cjk_pattern.clone(), cjk_font),
]);
let font_list = cache.list();
let latin_id = font_list
.iter()
.find(|(pattern, _)| pattern.name == Some("Latin Font".to_string()))
.map(|(_, id)| *id)
.expect("Latin font not found");
let cyrillic_id = font_list
.iter()
.find(|(pattern, _)| pattern.name == Some("Cyrillic Font".to_string()))
.map(|(_, id)| *id)
.expect("Cyrillic font not found");
let mut trace: Vec<TraceMsg> = Vec::new();
let latin_query = FcPattern {
unicode_ranges: vec![UnicodeRange {
start: 0x0041,
end: 0x005A,
}], ..Default::default()
};
let matches: Vec<_> = cache.list().into_iter()
.filter(|(pattern, _)| {
if pattern.unicode_ranges.is_empty() { return false; }
pattern.unicode_ranges.iter().any(|r| {
latin_query.unicode_ranges.iter().any(|q| {
r.start <= q.end && q.start <= r.end
})
})
})
.collect();
assert_eq!(matches.len(), 1);
assert_eq!(cache.get_memory_font(&latin_id).is_some(), true);
trace.clear();
let cyrillic_query = FcPattern {
unicode_ranges: vec![UnicodeRange {
start: 0x0410,
end: 0x044F,
}], ..Default::default()
};
let matches: Vec<_> = cache.list().into_iter()
.filter(|(pattern, _)| {
if pattern.unicode_ranges.is_empty() { return false; }
pattern.unicode_ranges.iter().any(|r| {
cyrillic_query.unicode_ranges.iter().any(|q| {
r.start <= q.end && q.start <= r.end
})
})
})
.collect();
assert_eq!(matches.len(), 1);
assert_eq!(cache.get_memory_font(&cyrillic_id).is_some(), true);
#[cfg(feature = "std")]
{
let text = "Hello Привет 你好";
let families: Vec<String> = cache.list().iter()
.filter_map(|(pattern, _)| pattern.family.clone())
.collect();
let chain = cache.resolve_font_chain(
&families,
FcWeight::Normal,
PatternMatch::DontCare,
PatternMatch::DontCare,
&mut trace,
);
let runs = chain.query_for_text(&cache, text);
let unique_fonts: std::collections::HashSet<_> = runs.iter()
.filter_map(|r| r.font_id)
.collect();
assert!(
unique_fonts.len() >= 2,
"Should use multiple fonts for multilingual text"
);
}
}
#[test]
fn test_weight_matching() {
let normal_font = FcFont {
bytes: vec![0, 1, 2, 3],
font_index: 0,
id: "normal-font".to_string(),
};
let bold_font = FcFont {
bytes: vec![4, 5, 6, 7],
font_index: 0,
id: "bold-font".to_string(),
};
let normal_pattern = FcPattern {
name: Some("Normal Font".to_string()),
family: Some("Test Family".to_string()),
weight: FcWeight::Normal,
..Default::default()
};
let bold_pattern = FcPattern {
name: Some("Bold Font".to_string()),
family: Some("Test Family".to_string()),
weight: FcWeight::Bold,
bold: PatternMatch::True,
..Default::default()
};
let mut cache = FcFontCache::default();
cache.with_memory_fonts(vec![
(normal_pattern.clone(), normal_font),
(bold_pattern.clone(), bold_font),
]);
let mut trace = Vec::new();
let normal_query = FcPattern {
family: Some("Test Family".to_string()),
weight: FcWeight::Normal,
..Default::default()
};
let matches = cache.query(&normal_query, &mut trace);
assert!(matches.is_some(), "Should match normal weight font");
let bold_query = FcPattern {
family: Some("Test Family".to_string()),
weight: FcWeight::Bold,
..Default::default()
};
let matches = cache.query(&bold_query, &mut trace);
assert!(matches.is_some(), "Should match bold weight font");
trace.clear();
let wrong_family_query = FcPattern {
family: Some("Wrong Family".to_string()),
weight: FcWeight::Normal,
..Default::default()
};
let matches = cache.query(&wrong_family_query, &mut trace);
assert!(matches.is_none(), "Should not match with wrong family");
let family_mismatch_traces = trace
.iter()
.filter(|msg| matches!(msg.reason, MatchReason::FamilyMismatch { .. }))
.count();
assert!(
family_mismatch_traces > 0,
"Expected family mismatch trace messages"
);
trace.clear();
let light_query = FcPattern {
family: Some("Test Family".to_string()),
weight: FcWeight::Light,
..Default::default()
};
let matches = cache.query(&light_query, &mut trace);
assert!(matches.is_none(), "Should not match with weight mismatch");
let weight_mismatch_traces = trace
.iter()
.filter(|msg| matches!(msg.reason, MatchReason::WeightMismatch { .. }))
.count();
assert!(
weight_mismatch_traces > 0,
"Expected weight mismatch trace messages"
);
let available_weights = [FcWeight::Light, FcWeight::Normal, FcWeight::Bold];
assert_eq!(
FcWeight::Normal.find_best_match(&available_weights),
Some(FcWeight::Normal),
"Should find exact match when available"
);
assert_eq!(
FcWeight::ExtraLight.find_best_match(&available_weights),
Some(FcWeight::Light),
"Should find closest lighter weight for weights < 400"
);
assert_eq!(
FcWeight::ExtraBold.find_best_match(&available_weights),
Some(FcWeight::Bold),
"Should find closest heavier weight for weights > 500"
);
let available = [FcWeight::Light, FcWeight::Bold];
assert_eq!(
FcWeight::Normal.find_best_match(&available),
Some(FcWeight::Light),
"For weight 400, should prefer lightest weight when 500 unavailable"
);
let available = [FcWeight::Light, FcWeight::SemiBold];
assert_eq!(
FcWeight::Medium.find_best_match(&available),
Some(FcWeight::Light),
"For weight 500, should prefer 400 first"
);
}
#[test]
fn test_trace_messages() {
let test_font = FcFont {
bytes: vec![0, 1, 2, 3],
font_index: 0,
id: "test-font".to_string(),
};
let test_pattern = FcPattern {
name: Some("Test Font".to_string()),
family: Some("Test Family".to_string()),
italic: PatternMatch::False,
monospace: PatternMatch::True,
weight: FcWeight::Normal,
stretch: FcStretch::Normal,
unicode_ranges: vec![UnicodeRange {
start: 0x0000,
end: 0x007F,
}],
..Default::default()
};
let mut cache = FcFontCache::default();
cache.with_memory_fonts(vec![(test_pattern.clone(), test_font)]);
let mut trace = Vec::new();
let name_query = FcPattern {
name: Some("Wrong Name".to_string()),
..Default::default()
};
let matches = cache.query(&name_query, &mut trace);
assert!(matches.is_none(), "Should not match with wrong name");
assert!(!trace.is_empty(), "Trace should not be empty");
let name_mismatch = trace.iter().any(|msg| {
if let MatchReason::NameMismatch { requested, found } = &msg.reason {
requested.as_ref() == Some(&"Wrong Name".to_string())
&& found.as_ref() == Some(&"Test Font".to_string())
} else {
false
}
});
assert!(name_mismatch, "Name mismatch trace message not found");
trace.clear();
let style_query = FcPattern {
name: Some("Test Font".to_string()),
italic: PatternMatch::True,
..Default::default()
};
let matches = cache.query(&style_query, &mut trace);
assert!(matches.is_none(), "Should not match with style mismatch");
let style_mismatch = trace.iter().any(|msg| {
if let MatchReason::StyleMismatch { property, .. } = &msg.reason {
property == &"italic"
} else {
false
}
});
assert!(style_mismatch, "Style mismatch trace message not found");
trace.clear();
let stretch_query = FcPattern {
name: Some("Test Font".to_string()),
stretch: FcStretch::Condensed,
..Default::default()
};
let matches = cache.query(&stretch_query, &mut trace);
assert!(matches.is_none(), "Should not match with stretch mismatch");
let stretch_mismatch = trace
.iter()
.any(|msg| matches!(msg.reason, MatchReason::StretchMismatch { .. }));
assert!(stretch_mismatch, "Stretch mismatch trace message not found");
trace.clear();
let range_query = FcPattern {
name: Some("Test Font".to_string()),
unicode_ranges: vec![UnicodeRange {
start: 0x0370,
end: 0x03FF,
}], ..Default::default()
};
let matches = cache.query(&range_query, &mut trace);
assert!(
matches.is_none(),
"Should not match with Unicode range mismatch"
);
let range_mismatch = trace
.iter()
.any(|msg| matches!(msg.reason, MatchReason::UnicodeRangeMismatch { .. }));
assert!(
range_mismatch,
"Unicode range mismatch trace message not found"
);
}
fn getfonts(
arial_id: FontId,
arial_bold_id: FontId,
courier_id: FontId,
fira_id: FontId,
noto_cjk_id: FontId,
) -> Vec<(FontId, FcPattern, FcFont)> {
return vec![
(
arial_id,
FcPattern {
name: Some("Arial".to_string()),
family: Some("Arial".to_string()),
weight: FcWeight::Normal,
bold: PatternMatch::False,
monospace: PatternMatch::False,
unicode_ranges: vec![UnicodeRange {
start: 0x0000,
end: 0x007F,
}],
..Default::default()
},
FcFont {
bytes: vec![1, 2, 3, 4],
font_index: 0,
id: "arial-regular".to_string(),
},
),
(
arial_bold_id,
FcPattern {
name: Some("Arial Bold".to_string()),
family: Some("Arial".to_string()),
weight: FcWeight::Bold,
bold: PatternMatch::True,
monospace: PatternMatch::False,
unicode_ranges: vec![UnicodeRange {
start: 0x0000,
end: 0x007F,
}],
..Default::default()
},
FcFont {
bytes: vec![5, 6, 7, 8],
font_index: 0,
id: "arial-bold".to_string(),
},
),
(
courier_id,
FcPattern {
name: Some("Courier New".to_string()),
family: Some("Courier New".to_string()),
weight: FcWeight::Normal,
monospace: PatternMatch::True,
unicode_ranges: vec![UnicodeRange {
start: 0x0000,
end: 0x007F,
}],
..Default::default()
},
FcFont {
bytes: vec![9, 10, 11, 12],
font_index: 0,
id: "courier-new".to_string(),
},
),
(
fira_id,
FcPattern {
name: Some("Fira Code".to_string()),
family: Some("Fira Code".to_string()),
weight: FcWeight::Normal,
monospace: PatternMatch::True,
unicode_ranges: vec![UnicodeRange {
start: 0x0000,
end: 0x007F,
}],
..Default::default()
},
FcFont {
bytes: vec![13, 14, 15, 16],
font_index: 0,
id: "fira-code".to_string(),
},
),
(
noto_cjk_id,
FcPattern {
name: Some("Noto Sans CJK".to_string()),
family: Some("Noto Sans CJK".to_string()),
weight: FcWeight::Normal,
monospace: PatternMatch::False,
unicode_ranges: vec![
UnicodeRange {
start: 0x0000,
end: 0x007F,
}, UnicodeRange {
start: 0x4E00,
end: 0x9FFF,
}, ],
..Default::default()
},
FcFont {
bytes: vec![17, 18, 19, 20],
font_index: 0,
id: "noto-sans-cjk".to_string(),
},
),
];
}
#[test]
fn test_font_search() {
let arial_id = FontId(1);
let arial_bold_id = FontId(2);
let courier_id = FontId(3);
let fira_id = FontId(4);
let noto_cjk_id = FontId(5);
let fonts = getfonts(arial_id, arial_bold_id, courier_id, fira_id, noto_cjk_id);
let mut cache = FcFontCache::default();
for (id, pattern, font) in fonts {
cache.with_memory_font_with_id(id, pattern, font);
}
let mut trace: Vec<TraceMsg> = Vec::new();
let results: Vec<_> = cache.list().into_iter()
.filter(|(pattern, _)| pattern.monospace == PatternMatch::True)
.collect();
assert_eq!(results.len(), 2, "Should find two monospace fonts");
let result_ids: Vec<FontId> = results.into_iter().map(|(_, id)| id).collect();
assert!(
result_ids.contains(&courier_id),
"Should include Courier New"
);
assert!(result_ids.contains(&fira_id), "Should include Fira Code");
#[cfg(feature = "std")]
{
let cjk_text = "你好";
let families: Vec<String> = cache.list().iter()
.filter_map(|(pattern, _)| pattern.family.clone())
.collect();
let chain = cache.resolve_font_chain(
&families,
FcWeight::Normal,
PatternMatch::DontCare,
PatternMatch::DontCare,
&mut trace,
);
let runs = chain.query_for_text(&cache, cjk_text);
assert!(!runs.is_empty(), "Should find fonts for CJK text");
let result_ids: Vec<FontId> = runs.iter()
.filter_map(|r| r.font_id)
.collect();
assert!(
result_ids.contains(¬o_cjk_id),
"Should include Noto Sans CJK"
);
trace.clear();
let mixed_text = "Hello 你好";
let runs = chain.query_for_text(&cache, mixed_text);
let unique_fonts: std::collections::HashSet<_> = runs.iter()
.filter_map(|r| r.font_id)
.collect();
assert!(
unique_fonts.len() >= 1,
"Should find at least one font for mixed text"
);
let cjk_found = unique_fonts.contains(¬o_cjk_id);
assert!(cjk_found, "Should find a CJK-capable font");
}
}
#[test]
fn test_failing_isolated() {
let arial_id = FontId(1);
let arial_bold_id = FontId(2);
let courier_id = FontId(3);
let fira_id = FontId(4);
let noto_cjk_id = FontId(5);
let fonts = getfonts(arial_id, arial_bold_id, courier_id, fira_id, noto_cjk_id);
let mut cache = FcFontCache::default();
for (id, pattern, font) in fonts {
cache.with_memory_font_with_id(id, pattern, font);
}
let mut trace = Vec::new();
let arial_query = FcPattern {
name: Some("Arial".to_string()),
..Default::default()
};
let result = cache.query(&arial_query, &mut trace);
assert!(result.is_some(), "Should find Arial font");
assert_eq!(result.unwrap().id, arial_id, "Should match Arial font ID");
}
#[test]
fn test_failing_isolated_2() {
let arial_id = FontId(1);
let arial_bold_id = FontId(2);
let courier_id = FontId(3);
let fira_id = FontId(4);
let noto_cjk_id = FontId(5);
let fonts = getfonts(arial_id, arial_bold_id, courier_id, fira_id, noto_cjk_id);
let mut cache = FcFontCache::default();
for (id, pattern, font) in fonts {
cache.with_memory_font_with_id(id, pattern, font);
}
let mut trace = Vec::new();
let arial_bold_query = FcPattern {
family: Some("Arial".to_string()),
bold: PatternMatch::True,
..Default::default()
};
let result = cache.query(&arial_bold_query, &mut trace);
assert!(result.is_some(), "Should find Arial Bold font");
assert_eq!(
result.unwrap().id,
arial_bold_id,
"Should match Arial Bold font ID"
);
}