use std::path::PathBuf;
use readsight::language::JsonLanguageRepository;
use readsight::{Config, Engine, Error, ReadSight, Script};
const SAMPLE: &str = "The quick brown fox jumps over the lazy dog. It was a sunny day.";
fn data_dir(sub: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("data")
.join(sub)
}
#[test]
fn supported_languages_sorted_86() {
let langs = ReadSight::supported_languages(None);
assert_eq!(langs.len(), 86);
let mut sorted = langs.clone();
sorted.sort();
assert_eq!(langs, sorted);
}
#[test]
fn language_accessor() {
let rs = ReadSight::new("en-us").unwrap();
assert_eq!(rs.language().code, "en-us");
assert_eq!(rs.language().name, "English (US)");
assert_eq!(rs.language().script, Script::Latin);
assert_eq!(rs.language().script.as_str(), "Latin");
}
#[test]
fn code_is_normalized() {
let rs = ReadSight::new(" EN-US ").unwrap();
assert_eq!(rs.language().code, "en-us");
}
#[test]
fn engine_alias_works() {
let rs: Engine = Engine::new("fr").unwrap();
assert_eq!(rs.language().code, "fr");
}
#[test]
fn unsupported_language_errors() {
match ReadSight::new("zz-zz") {
Err(Error::UnsupportedLanguage(code)) => assert_eq!(code, "zz-zz"),
Err(other) => panic!("expected UnsupportedLanguage, got {other:?}"),
Ok(_) => panic!("expected UnsupportedLanguage, got Ok"),
}
}
#[test]
fn unsupported_formula_errors() {
let rs = ReadSight::new("en-us").unwrap();
match rs.score("gulpease", SAMPLE) {
Err(Error::UnsupportedFormula { formula, language }) => {
assert_eq!(formula, "gulpease");
assert_eq!(language, "en-us");
}
other => panic!("expected UnsupportedFormula, got {other:?}"),
}
assert!(rs.gulpease(SAMPLE).is_err());
assert!(rs.osman(SAMPLE).is_err());
}
#[test]
fn empty_text_errors() {
let rs = ReadSight::new("en-us").unwrap();
assert!(matches!(rs.analyze(" "), Err(Error::EmptyText)));
assert!(matches!(rs.gunning_fog(""), Err(Error::EmptyText)));
}
#[test]
fn convenience_methods_ok() {
let rs = ReadSight::new("en-us").unwrap();
assert!(rs.flesch_reading_ease(SAMPLE).is_ok());
assert!(rs.flesch_kincaid_grade_level(SAMPLE).is_ok());
assert!(rs.gunning_fog(SAMPLE).is_ok());
assert!(rs.smog_index(SAMPLE).is_ok());
assert!(rs.coleman_liau(SAMPLE).is_ok());
assert!(rs.automated_readability_index(SAMPLE).is_ok());
assert!(rs.lix(SAMPLE).is_ok());
assert!(rs.dale_chall(SAMPLE).is_ok());
assert!(rs.spache(SAMPLE).is_ok());
let es = ReadSight::new("es").unwrap();
assert!(es.fernandez_huerta(SAMPLE).is_ok());
assert!(es.szigriszt_pazos(SAMPLE).is_ok());
assert!(es.gutierrez_polini(SAMPLE).is_ok());
assert!(es.crawford(SAMPLE).is_ok());
assert!(ReadSight::new("it").unwrap().gulpease(SAMPLE).is_ok());
assert!(ReadSight::new("pl").unwrap().fog_pl(SAMPLE).is_ok());
assert!(ReadSight::new("ar").unwrap().osman(SAMPLE).is_ok());
}
#[test]
fn wiener_variants_and_invalid() {
let rs = ReadSight::new("de-1996").unwrap();
for v in 1..=4 {
let r = rs.wiener_sachtextformel(SAMPLE, v).unwrap();
assert_eq!(r.formula_name, format!("wiener_sachtextformel_{v}"));
}
match rs.wiener_sachtextformel(SAMPLE, 5) {
Err(Error::InvalidVariant(5)) => {}
other => panic!("expected InvalidVariant(5), got {other:?}"),
}
}
#[test]
fn add_hyphenations_overrides() {
let rs = ReadSight::new("en-us").unwrap();
rs.add_hyphenations([("readsight".to_string(), "read-sight".to_string())]);
assert_eq!(rs.split_word("readsight"), vec!["read", "sight"]);
}
#[test]
fn text_metric_helpers() {
let rs = ReadSight::new("en-us").unwrap();
assert_eq!(rs.word_count(SAMPLE), 14);
assert_eq!(rs.sentence_count(SAMPLE), 2);
assert!(rs.letter_count(SAMPLE) > 0);
assert!(rs.average_syllables_per_word(SAMPLE) >= 1.0);
assert!(rs.average_words_per_sentence(SAMPLE) > 0.0);
assert!(!rs.histogram_syllables(SAMPLE).is_empty());
}
#[test]
fn total_syllables_equals_manual_sum() {
let rs = ReadSight::new("en-us").unwrap();
let text = "The quick brown fox jumps over the lazy dog";
let manual: i64 = [
"The", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog",
]
.iter()
.map(|w| rs.syllable_count(w))
.sum();
assert_eq!(rs.total_syllables(text), manual);
}
#[test]
fn histogram_sums_to_word_count() {
let rs = ReadSight::new("en-us").unwrap();
let stats = rs.analyze(SAMPLE).unwrap();
let sum: i64 = stats.syllable_histogram.values().sum();
assert_eq!(sum, stats.word_count);
}
#[test]
fn proper_noun_filtering() {
let rs = ReadSight::new("en-us").unwrap();
let text = "The beautiful Elephant.";
assert_eq!(rs.words_with_more_than_n_syllables(text, 2, true), 2);
assert_eq!(rs.words_with_more_than_n_syllables(text, 2, false), 1);
assert_eq!(rs.polysyllable_count(text, true), 2);
}
#[test]
fn split_reconstructs_and_bounds_multilingual() {
let cases: &[(&str, &[&str])] = &[
("en-us", &["banana", "computer", "beautiful", "a", "the"]),
("ru", &["молоко", "привет", "я"]),
("de-1996", &["Schreiben", "Deutsch"]),
("fr", &["bonjour"]),
("es", &["hola", "computadora"]),
("it", &["ciao"]),
];
for (lang, words) in cases {
let rs = ReadSight::new(lang).unwrap();
for word in *words {
let parts = rs.split_word(word);
assert_eq!(parts.concat(), *word, "{lang}: reconstruct {word}");
let count = rs.syllable_count(word);
let len = word.chars().count() as i64;
assert!(
count >= 1 && count <= len,
"{lang}: {word} count {count} len {len}"
);
assert_eq!(rs.split_word(word), parts, "{lang}: {word} deterministic");
}
}
}
#[test]
fn empty_and_single_char_words() {
let rs = ReadSight::new("en-us").unwrap();
assert_eq!(rs.syllable_count(""), 0);
assert_eq!(rs.split_word(""), Vec::<String>::new());
for c in ["a", "b", "z", "A", "Z"] {
assert_eq!(rs.syllable_count(c), 1);
assert_eq!(rs.split_word(c), vec![c]);
}
}
#[test]
fn filesystem_data_source_matches_embedded() {
let config = Config::from_dirs(data_dir("patterns"), data_dir("languages"));
let fs = ReadSight::with_config("en-us", config).unwrap();
let embedded = ReadSight::new("en-us").unwrap();
assert_eq!(
fs.syllable_count("hyphenation"),
embedded.syllable_count("hyphenation")
);
assert_eq!(
fs.flesch_reading_ease(SAMPLE).unwrap().score,
embedded.flesch_reading_ease(SAMPLE).unwrap().score
);
assert_eq!(
ReadSight::supported_languages(Some(&Config::from_dirs(
data_dir("patterns"),
data_dir("languages")
)))
.len(),
86
);
}
#[test]
fn language_layer_methods() {
let rs = ReadSight::new("en-us").unwrap();
let lang = rs.language();
assert!(lang.supports_formula("flesch_reading_ease"));
assert!(!lang.supports_formula("nonexistent"));
assert!(lang.get_formula_config("lix").is_some());
assert_eq!(
lang.formula_number("flesch_reading_ease", "base"),
Some(206.835)
);
assert_eq!(lang.formula_number("lix", "longWordThreshold"), Some(6.0));
assert!(lang.get_supported_formulas().contains(&"lix".to_string()));
}
#[test]
fn repository_operations() {
let mut repo = JsonLanguageRepository::new(Config::default_embedded());
assert!(repo.exists("en-us"));
assert!(repo.exists(" RU "));
assert!(!repo.exists("zz"));
assert_eq!(repo.find("en-us").unwrap().code, "en-us");
assert_eq!(repo.find("en-us").unwrap().name, "English (US)");
assert!(repo.exists("en-us"));
assert!(matches!(
repo.find("zz"),
Err(Error::UnsupportedLanguage(_))
));
assert_eq!(repo.list_codes().unwrap().len(), 86);
}
#[test]
fn filesystem_repository_lists_codes() {
let mut repo = JsonLanguageRepository::new(Config::from_dirs(
data_dir("patterns"),
data_dir("languages"),
));
assert_eq!(repo.list_codes().unwrap().len(), 86);
assert!(repo.exists("en-us"));
assert!(!repo.exists("zz-nonexistent"));
assert_eq!(repo.find("de-1996").unwrap().code, "de-1996");
}
#[test]
fn registry_accessor() {
let rs = ReadSight::new("en-us").unwrap();
assert!(rs.registry().has("smog"));
assert_eq!(rs.registry().list_names().len(), 17);
}
#[test]
fn filesystem_list_skips_non_json_and_handles_bad_dir() {
let base = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("temp/cov_fs");
let langs = base.join("languages");
let pats = base.join("patterns");
std::fs::create_dir_all(&langs).unwrap();
std::fs::create_dir_all(&pats).unwrap();
std::fs::copy(
data_dir("languages").join("en-us.json"),
langs.join("en-us.json"),
)
.unwrap();
std::fs::write(langs.join("notes.txt"), "ignore me").unwrap();
let codes = ReadSight::supported_languages(Some(&Config::from_dirs(&pats, &langs)));
assert_eq!(codes, vec!["en-us".to_string()]);
let missing = ReadSight::supported_languages(Some(&Config::from_dirs(
base.join("no-patterns"),
base.join("no-languages"),
)));
assert!(missing.is_empty());
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn malformed_language_json_errors() {
let base = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("temp/cov_badjson");
let langs = base.join("languages");
let pats = base.join("patterns");
std::fs::create_dir_all(&langs).unwrap();
std::fs::create_dir_all(&pats).unwrap();
std::fs::write(langs.join("en-us.json"), "{ this is not valid json").unwrap();
match ReadSight::with_config("en-us", Config::from_dirs(&pats, &langs)) {
Err(Error::Json(_)) => {}
Err(other) => panic!("expected Json error, got {other:?}"),
Ok(_) => panic!("expected Json error, got Ok"),
}
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn error_display_messages() {
assert_eq!(
Error::UnsupportedLanguage("xx".into()).to_string(),
"Language \"xx\" is not supported."
);
assert_eq!(
Error::UnsupportedFormula {
formula: "smog".into(),
language: "xx".into()
}
.to_string(),
"Formula \"smog\" is not supported for language \"xx\"."
);
assert_eq!(Error::EmptyText.to_string(), "Text cannot be empty.");
assert_eq!(
Error::InvalidVariant(9).to_string(),
"Wiener Sachtextformel variant must be 1-4, got 9."
);
assert!(Error::PatternFileNotFound("f.tex".into())
.to_string()
.contains("f.tex"));
assert!(Error::InvalidPattern("bad".into())
.to_string()
.contains("bad"));
}
#[test]
fn pattern_file_not_found_on_bad_dirs() {
let config = Config::from_dirs("no-such-patterns", data_dir("languages"));
match ReadSight::with_config("en-us", config) {
Err(Error::PatternFileNotFound(_)) => {}
Err(other) => panic!("expected PatternFileNotFound, got {other:?}"),
Ok(_) => panic!("expected PatternFileNotFound, got Ok"),
}
}