282 lines
8.6 KiB
Rust
282 lines
8.6 KiB
Rust
//! 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`.
|
|
|
|
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("<h1"));
|
|
}
|
|
|
|
#[test]
|
|
fn a_repeated_top_level_title_is_dropped_from_the_body() {
|
|
let document = sample("sgp4");
|
|
// The sample body opens with `# SGP4`, which differs from the
|
|
// metadata title `SGP4 propagation`, so it is kept.
|
|
assert!(document.english.html.contains("<h1"));
|
|
assert!(
|
|
document
|
|
.english
|
|
.headings
|
|
.iter()
|
|
.any(|heading| heading.text == "SGP4")
|
|
);
|
|
|
|
// A body that repeats the metadata title loses the repeated heading.
|
|
let repeated = "---\ntitle: SGP4 propagation\n---\n\n# SGP4 propagation\n\nBody.\n";
|
|
let variant = DocumentVariant::parse(repeated).expect("must parse");
|
|
assert!(!variant.html.contains("<h1"));
|
|
assert!(
|
|
!variant
|
|
.headings
|
|
.iter()
|
|
.any(|heading| heading.text == "SGP4 propagation")
|
|
);
|
|
assert!(variant.html.contains("Body."));
|
|
}
|
|
|
|
#[test]
|
|
fn a_translation_replaces_the_language_form() {
|
|
let mut document = sample("sgp4");
|
|
assert!(!document.has(Language::Chinese));
|
|
// Before the translation the Chinese reader sees the English form.
|
|
assert_eq!(document.title(Language::Chinese), "SGP4 propagation");
|
|
document.chinese = Some(DocumentVariant::parse(SAMPLE_ZH).expect("must parse"));
|
|
assert!(document.has(Language::Chinese));
|
|
assert_eq!(document.title(Language::Chinese), "SGP4 传播");
|
|
assert_eq!(document.title(Language::English), "SGP4 propagation");
|
|
// The references of either form are merged.
|
|
assert_eq!(document.references(), vec!["tle"]);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_a_document_without_a_title() {
|
|
// Without a title the document cannot be listed.
|
|
assert!(Document::parse("x", LAYER, Language::English, "---\nsummary: s\n---\nbody").is_none());
|
|
assert!(DocumentVariant::parse("---\nsummary: s\n---\nbody").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn language_of_file_uses_the_suffix() {
|
|
assert_eq!(Language::of_file("sgp4"), Language::English);
|
|
assert_eq!(Language::of_file("sgp4.zh"), Language::Chinese);
|
|
}
|
|
|
|
#[test]
|
|
fn tree_orders_documents_by_chapter() {
|
|
let documents = vec![
|
|
sample("sgp4"),
|
|
Document::parse(
|
|
"tle",
|
|
LAYER,
|
|
Language::English,
|
|
"---\ntitle: Two line elements\nchapter: orbit\n---\n\nElement sets.\n",
|
|
)
|
|
.expect("must parse"),
|
|
];
|
|
let tree = Tree::new(documents);
|
|
assert_eq!(tree.len(), 2);
|
|
assert!(tree.document("sgp4").is_some());
|
|
let ids: Vec<&str> = 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.
|
|
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");
|
|
}
|