use madi::{Engine, EngineConfig, Skip};
use std::path::PathBuf;
const DOCS: [&str; 4] = [
"정부는 내년 경제 성장률이 2.5%에 이를 것으로 내다봤다. 그러나 수출 여건은 여전히 불투명하다.\n김철수 기자 kim@example.com",
"서울 시내 버스 요금이 다음 달부터 오른다. 시민들은 \"부담이 커졌다\"고 말했다.",
"그는 어제 도서관에 가서 책을 빌렸다. 날씨가 더워서 일찍 돌아왔다.\n▶ 관련 기사 보기",
"회의는 오후 3시에 열린다. 참석자는 모두 12명이다. 끝.",
];
fn root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..")
}
fn config() -> Option<EngineConfig> {
let r = root();
let dict = r.join("dict/madi.dict");
if !dict.exists() {
eprintln!("skip: {} 없음", dict.display());
return None;
}
let knlm = r.join("dict/knlm.bin");
Some(EngineConfig { knlm: knlm.exists().then_some(knlm), ..EngineConfig::new(dict) })
}
fn flat(e: &Engine, doc: &str) -> Vec<String> {
e.worker().analyze_doc(doc).iter().map(|s| format!("{}\t{}", s.text, madi::format_tokens(&s.tokens))).collect()
}
#[test]
fn builtin_rules_parse() {
let r = madi::text::SkipRules::builtin();
assert!(r.len() > 0);
}
#[test]
fn deterministic_across_threads_and_rule_source() {
let Some(cfg) = config() else { return };
let e = Engine::load(&cfg).unwrap();
assert!(e.version().starts_with(env!("CARGO_PKG_VERSION")));
let one: Vec<Vec<String>> = DOCS.iter().map(|d| flat(&e, d)).collect();
assert!(one.iter().all(|v| !v.is_empty()));
let many: Vec<Vec<String>> = std::thread::scope(|s| {
let hs: Vec<_> = DOCS.iter().rev().map(|d| s.spawn(|| flat(&e, d))).collect();
let mut v: Vec<Vec<String>> = hs.into_iter().map(|h| h.join().unwrap()).collect();
v.reverse();
v
});
assert_eq!(one, many);
let cfg2 = EngineConfig { skip: Skip::File(root().join("tools/boilerplate.txt")), ..cfg.clone() };
let e2 = Engine::load(&cfg2).unwrap();
assert_eq!(e.version(), e2.version());
for (d, o) in DOCS.iter().zip(&one) {
assert_eq!(&flat(&e2, d), o);
}
let crlf = std::env::temp_dir().join("madi_boilerplate_crlf.txt");
std::fs::write(&crlf, madi::text::BUILTIN_SKIP.replace("\r\n", "\n").replace('\n', "\r\n")).unwrap();
let e4 = Engine::load(&EngineConfig { skip: Skip::File(crlf), ..cfg.clone() }).unwrap();
assert_eq!(e.version(), e4.version());
for (d, o) in DOCS.iter().zip(&one) {
assert_eq!(&flat(&e4, d), o);
}
let e3 = Engine::load(&EngineConfig { skip: Skip::Off, ..cfg }).unwrap();
assert_ne!(e.version(), e3.version());
}
#[test]
fn query_and_eojeol_start() {
let Some(cfg) = config() else { return };
let e = Engine::load(&cfg).unwrap();
let mut w = e.worker();
let q = w.analyze_query(" 경제 성장률이 ");
assert!(!q.is_empty());
assert!(!q[0].space_before);
assert!(q.iter().skip(1).any(|t| t.space_before));
let nb = w.analyze_query_nbest("경제 성장률이", 3);
assert!(!nb.is_empty() && nb.len() <= 3);
assert_eq!(nb[0].1.iter().map(|t| (&t.form, t.tag)).collect::<Vec<_>>(), q.iter().map(|t| (&t.form, t.tag)).collect::<Vec<_>>());
}
#[test]
fn from_memory_matches_load() {
let Some(cfg) = config() else { return };
let words = |p: &std::path::Path| -> (Vec<u64>, usize) {
let b = std::fs::read(p).unwrap();
let mut w = vec![0u64; b.len().div_ceil(8)];
for (i, c) in b.chunks(8).enumerate() {
let mut x = [0u8; 8];
x[..c.len()].copy_from_slice(c);
w[i] = u64::from_le_bytes(x);
}
(w, b.len())
};
let (dw, dn) = words(&cfg.dict);
let km = cfg.knlm.as_ref().map(|p| words(p));
let e1 = Engine::load(&cfg).unwrap();
let e2 = Engine::from_memory(dw, dn, km, &cfg).unwrap();
assert_eq!(e1.version(), e2.version());
for d in DOCS {
assert_eq!(flat(&e1, d), flat(&e2, d));
}
}