//! Tests for the document model and the search tokenizer. //! 文档模型与搜索分词的测试。 //! //! They live here rather than inside the module so that `src/` holds no test //! code, as it does in `apsis-rs`. An integration test is a separate crate and //! reaches only what the library makes public, so the few internal items the //! tests need are opened through `apsis_book::testkit`. //! //! //! 它们放在这里而不是模块内部,使 `src/` 不含测试代码,与 `apsis-rs` 一致。 //! 集成测试是独立的 crate,只能触达库公开的部分,因此测试所需的那少数内部条目 //! 经 `apsis_book::testkit` 开放。 use apsis_book::docs::document::*; use apsis_book::testkit::tokenise; const SAMPLE: &str = "\ --- id: sgp4 title: SGP4 propagation chapter: orbit summary: Analytical propagation of mean elements. tags: propagation, tle prerequisites: tle --- # SGP4 The model propagates [[tle|mean elements]]. ## Drag The BSTAR term. "; const SAMPLE_ZH: &str = "\ --- title: SGP4 传播 summary: 平均元素的解析传播。 --- # SGP4 该模型传播 [[tle|平均元素]]。 ## 阻力 BSTAR 项。 "; /// The layer the sample documents declare in the tests. /// 测试中的样例文档所声明的层。 const LAYER: &str = "orbit"; /// Parses the English sample under the given identifier. /// 以给定标识解析英文样例。 fn sample(id: &str) -> Document { Document::parse(id, LAYER, Language::English, SAMPLE).expect("the sample must parse") } #[test] fn parses_metadata_and_body() { let document = sample("sgp4"); assert_eq!(document.id, "sgp4"); assert_eq!(document.title(Language::English), "SGP4 propagation"); assert_eq!(document.tags, vec!["propagation", "tle"]); assert_eq!(document.prerequisites, vec!["tle"]); assert_eq!(document.references(), vec!["tle"]); assert_eq!(document.english.headings.len(), 2); assert_eq!(document.english.headings[1].anchor, "drag"); assert!(document.english.html.contains(" = tree .ordered() .map(|document| document.id.as_str()) .collect(); assert_eq!(ids.len(), 2); } #[test] fn a_translated_document_is_indexed_in_both_languages() { let mut document = sample("sgp4"); document.chinese = Some(DocumentVariant::parse(SAMPLE_ZH).expect("must parse")); let tree = Tree::new(vec![document]); let index = tree.search_index(); assert_eq!(index.len(), 2); let hits = index.search("平均元素", 5); assert_eq!(hits.len(), 1); assert_eq!(hits[0].language, Language::Chinese); // The English body contains "propagates", which the Chinese body // does not. // 英文正文含有 "propagates",中文正文没有。 let hits = index.search("propagates", 5); assert_eq!(hits.len(), 1); assert_eq!(hits[0].language, Language::English); } #[test] fn tokenise_emits_words_and_han_pairs() { assert_eq!(tokenise("mean_motion"), vec!["mean", "motion"]); assert_eq!(tokenise("轨道传播"), vec!["轨道", "道传", "传播"]); assert_eq!(tokenise("坐标"), vec!["坐标"]); // A lone Chinese character produces no token; the search falls back // to a substring match for it. // 单个中文字符不产生词元;搜索对它回退到子串匹配。 assert!(tokenise("轨").is_empty()); // A single Latin character is dropped. // 单个拉丁字符被丢弃。 assert!(tokenise("a").is_empty()); } #[test] fn a_chinese_query_matches_a_word_inside_a_sentence() { let documents = vec![ Document::parse( "a", LAYER, Language::English, "---\ntitle: Orbits\nsummary: s\n---\n\n本条论述轨道坐标的转换。\n", ) .expect("must parse"), Document::parse( "b", LAYER, Language::English, "---\ntitle: Time\nsummary: s\n---\n\n这里讨论时间尺度。\n", ) .expect("must parse"), ]; let tree = Tree::new(documents); let index = tree.search_index(); // A two character query matches the pair inside the sentence. // 两字查询匹配句子内部的相邻字符对。 let hits = index.search("坐标", 5); assert_eq!(hits.len(), 1); assert_eq!(hits[0].id, "a"); // A one character query has no token and falls back to a substring // match. // 单字查询没有词元,回退到子串匹配。 let hits = index.search("轨", 5); assert_eq!(hits.len(), 1); assert_eq!(hits[0].id, "a"); } #[test] fn search_ranks_title_matches_first() { let documents = vec![ Document::parse( "a", LAYER, Language::English, "---\ntitle: Redshift\nsummary: x\n---\n\ntext about orbits\n", ) .expect("must parse"), Document::parse( "b", LAYER, Language::English, "---\ntitle: Orbits\nsummary: y\n---\n\ntext about redshift\n", ) .expect("must parse"), ]; let tree = Tree::new(documents); let index = tree.search_index(); let hits = index.search("redshift orbits", 5); assert_eq!(hits.len(), 2); } #[test] fn missing_links_are_reported() { let tree = Tree::new(vec![sample("sgp4")]); let missing = tree.missing_links(); assert_eq!(missing, vec![("sgp4".to_string(), "tle".to_string())]); } #[test] fn a_document_is_offered_once_when_its_layer_has_subjects() { // The physics layer declares two subjects, so its documents live in // them. If a document were also left in the layer's own list, the // page would render it twice under one key and stop rendering at // all, so no identifier may appear twice in the reading order. // 物理层声明了两个子区,因此它的文档住在其中。若某篇文档同时留在层自己的 // 列表里,页面会在同一个键下渲染它两次而完全停止渲染,因此阅读顺序中任何 // 标识都不得出现两次。 let documents = vec![ Document::parse( "time-scales", "physics", Language::English, "---\ntitle: Time scales\nsummary: s\n---\n\nScales.\n", ) .expect("must parse"), Document::parse( "geodesy-wgs84", "physics", Language::English, "---\ntitle: WGS84\nsummary: s\n---\n\nThe ellipsoid.\n", ) .expect("must parse"), ]; let tree = Tree::new(documents); let mut seen: Vec<&String> = tree.reading_order().iter().collect(); let before = seen.len(); seen.sort(); seen.dedup(); assert_eq!( before, seen.len(), "a document is offered twice: {:?}", tree.reading_order() ); // Both documents are still reachable, each under exactly one layer. // 两篇文档仍然可达,各自恰好属于一个层。 let physics = tree .groups() .iter() .flat_map(|group| group.sections.iter()) .find(|section| section.id == "physics") .expect("the physics layer is declared"); assert!( physics.documents.is_empty(), "the layer still lists its own documents: {:?}", physics.documents ); assert!(physics.subsections.len() >= 2, "the subjects are missing"); }