//! 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");
}