Write the comments in both languages, part two
All twenty test files and the tool handlers. The tests that share the paragraph explaining why they live in tests/ now share its translation too, so the same four lines are written once per file rather than paraphrased.
This commit is contained in:
parent
a360238a95
commit
e6065dd30e
|
|
@ -9,6 +9,15 @@
|
|||
//! condition, which is what a caller with the input in front of them needs;
|
||||
//! the handler adds the parameter name so that the form can highlight the
|
||||
//! field.
|
||||
//!
|
||||
//! 工具处理函数。
|
||||
//!
|
||||
//! 每个处理函数接收已校验的参数,并调用 `apsis-rs` 的公共接口。结果由
|
||||
//! [`super::out`] 塑造,使前端对所有工具都用同一批组件渲染。
|
||||
//!
|
||||
//! 错误通过 [`ToolError`] 报告,消息为表单的使用者而写。`apsis-rs` 底层的错误
|
||||
//! 类型描述了具体状况,那正是手头有输入的调用方所需要的;处理函数补上参数名,
|
||||
//! 使表单能够高亮该字段。
|
||||
|
||||
use apsis::apsis_coord::{Ecef, Eci, Geodetic};
|
||||
use apsis::apsis_frames::EarthRotationAngle;
|
||||
|
|
@ -27,6 +36,10 @@ use crate::tools::{Params, ToolError};
|
|||
///
|
||||
/// The identifier is one of the identifiers in the registry; an unknown one
|
||||
/// is reported as a general failure.
|
||||
///
|
||||
/// 按标识运行一个工具。
|
||||
///
|
||||
/// 标识是注册表中的标识之一;未知的标识被报告为一般性失败。
|
||||
pub fn run(id: &str, params: &Params<'_>) -> Result<Value, ToolError> {
|
||||
match id {
|
||||
"parse-tle" => parse_tle(params),
|
||||
|
|
@ -43,6 +56,7 @@ pub fn run(id: &str, params: &Params<'_>) -> Result<Value, ToolError> {
|
|||
}
|
||||
|
||||
/// Parses a two-line element set and reports its fields.
|
||||
/// 解析两行元素集并报告其各字段。
|
||||
fn parse_tle(params: &Params<'_>) -> Result<Value, ToolError> {
|
||||
let text = params.text("tle")?;
|
||||
let element = TwoLineElement::parse(&text).map_err(|error| {
|
||||
|
|
@ -156,6 +170,7 @@ fn parse_tle(params: &Params<'_>) -> Result<Value, ToolError> {
|
|||
}
|
||||
|
||||
/// Propagates one instant with SGP4 and reports the state.
|
||||
/// 用 SGP4 传播某一时刻并报告状态。
|
||||
fn sgp4_propagate(params: &Params<'_>) -> Result<Value, ToolError> {
|
||||
let text = params.text("tle")?;
|
||||
let model = gravity_model(params.choice("model")?);
|
||||
|
|
@ -245,6 +260,7 @@ fn sgp4_propagate(params: &Params<'_>) -> Result<Value, ToolError> {
|
|||
}
|
||||
|
||||
/// Samples a ground track and returns the points.
|
||||
/// 对地面轨迹采样并返回各点。
|
||||
fn sgp4_track(params: &Params<'_>) -> Result<Value, ToolError> {
|
||||
let text = params.text("tle")?;
|
||||
let model = gravity_model(params.choice("model")?);
|
||||
|
|
@ -341,6 +357,8 @@ fn sgp4_track(params: &Params<'_>) -> Result<Value, ToolError> {
|
|||
.map(|value| {
|
||||
// The time axis is carried separately so that the chart can label
|
||||
// the abscissa without reading the table.
|
||||
|
||||
// 时间轴单独携带,使图表无需读取表格即可标注横坐标。
|
||||
let mut members = value.members().to_vec();
|
||||
members.push((
|
||||
"axis".to_string(),
|
||||
|
|
@ -355,6 +373,7 @@ fn sgp4_track(params: &Params<'_>) -> Result<Value, ToolError> {
|
|||
}
|
||||
|
||||
/// Compares the mean elements with the osculating elements.
|
||||
/// 把平均根数与密切根数作比较。
|
||||
fn orbit_osculating(params: &Params<'_>) -> Result<Value, ToolError> {
|
||||
let text = params.text("tle")?;
|
||||
let model = gravity_model(params.choice("model")?);
|
||||
|
|
@ -480,6 +499,7 @@ fn orbit_osculating(params: &Params<'_>) -> Result<Value, ToolError> {
|
|||
}
|
||||
|
||||
/// Converts Keplerian elements to a state vector.
|
||||
/// 把开普勒根数转换为状态矢量。
|
||||
fn kepler_state(params: &Params<'_>) -> Result<Value, ToolError> {
|
||||
let parameter = gravity_parameter(params.choice("model")?);
|
||||
let elements = KeplerianElements {
|
||||
|
|
@ -594,6 +614,7 @@ fn kepler_state(params: &Params<'_>) -> Result<Value, ToolError> {
|
|||
}
|
||||
|
||||
/// Converts an instant between time scales.
|
||||
/// 在时间尺度之间转换一个时刻。
|
||||
fn time_convert(params: &Params<'_>) -> Result<Value, ToolError> {
|
||||
let scale = time_scale(params.choice("scale")?);
|
||||
let form = params.choice("input")?;
|
||||
|
|
@ -647,6 +668,9 @@ fn time_convert(params: &Params<'_>) -> Result<Value, ToolError> {
|
|||
if let Some(mjd) = mjd {
|
||||
// The label names the scale the input was given on, so it
|
||||
// is built rather than looked up.
|
||||
|
||||
// 标签要指出输入所用的尺度,因此它是构造出来的,而不是查表得到
|
||||
// 的。
|
||||
scalars.push(Scalar::with_unit(
|
||||
format!("Input on {}", target.name()),
|
||||
out::fmt(mjd, 9),
|
||||
|
|
@ -700,6 +724,7 @@ fn time_convert(params: &Params<'_>) -> Result<Value, ToolError> {
|
|||
}
|
||||
|
||||
/// Converts a geodetic position to Earth-fixed coordinates.
|
||||
/// 把大地位置转换为地固坐标。
|
||||
fn geodetic_ecef(params: &Params<'_>) -> Result<Value, ToolError> {
|
||||
let latitude = out::radians(params.number("latitude")?);
|
||||
let longitude = out::radians(params.number("longitude")?);
|
||||
|
|
@ -780,6 +805,7 @@ fn geodetic_ecef(params: &Params<'_>) -> Result<Value, ToolError> {
|
|||
}
|
||||
|
||||
/// Rotates a vector between the inertial and Earth-fixed frames.
|
||||
/// 在惯性系与地固系之间旋转一个矢量。
|
||||
fn frame_transform(params: &Params<'_>) -> Result<Value, ToolError> {
|
||||
let direction = params.choice("direction")?;
|
||||
let vector = apsis::apsis_math::Vec3::new(
|
||||
|
|
@ -864,6 +890,11 @@ fn frame_transform(params: &Params<'_>) -> Result<Value, ToolError> {
|
|||
/// whose perigee touches the lower orbit and whose apogee touches the upper
|
||||
/// one, a coast for half the ellipse's period, and a second burn onto the
|
||||
/// destination circle.
|
||||
///
|
||||
/// 规划两个圆轨道之间的霍曼转移。
|
||||
///
|
||||
/// 该转移是经典的两脉冲机动:一次点火进入一个近地点触及低轨、远地点触及高轨的
|
||||
/// 椭圆,滑行半个椭圆周期,再一次点火进入目标圆轨道。
|
||||
fn hohmann_transfer(params: &Params<'_>) -> Result<Value, ToolError> {
|
||||
let from_radius = params.number("from_radius")?;
|
||||
let to_radius = params.number("to_radius")?;
|
||||
|
|
@ -966,6 +997,7 @@ fn hohmann_transfer(params: &Params<'_>) -> Result<Value, ToolError> {
|
|||
}
|
||||
|
||||
/// Returns the gravity model named by a select value.
|
||||
/// 返回某个选择值所命名的重力模型。
|
||||
fn gravity_model(value: &str) -> GravityModel {
|
||||
match value {
|
||||
"wgs84" => GravityModel::WGS84,
|
||||
|
|
@ -974,6 +1006,7 @@ fn gravity_model(value: &str) -> GravityModel {
|
|||
}
|
||||
|
||||
/// Returns the display name of a gravity model select value.
|
||||
/// 返回某个重力模型选择值的显示名。
|
||||
fn gravity_model_name(value: &str) -> &'static str {
|
||||
match value {
|
||||
"wgs84" => "WGS 84",
|
||||
|
|
@ -982,6 +1015,7 @@ fn gravity_model_name(value: &str) -> &'static str {
|
|||
}
|
||||
|
||||
/// Returns the Chinese display name of a gravity model select value.
|
||||
/// 返回某个重力模型选择值的中文显示名。
|
||||
fn gravity_model_name_zh(value: &str) -> &'static str {
|
||||
match value {
|
||||
"wgs84" => "WGS 84",
|
||||
|
|
@ -990,6 +1024,7 @@ fn gravity_model_name_zh(value: &str) -> &'static str {
|
|||
}
|
||||
|
||||
/// Returns the gravitational parameter of a gravity model select value.
|
||||
/// 返回某个重力模型选择值的引力参数。
|
||||
fn gravity_parameter(value: &str) -> GravityParameter {
|
||||
match value {
|
||||
"wgs84" => GravityParameter::WGS84,
|
||||
|
|
@ -998,6 +1033,7 @@ fn gravity_parameter(value: &str) -> GravityParameter {
|
|||
}
|
||||
|
||||
/// Returns the time scale named by a select value.
|
||||
/// 返回某个选择值所命名的时间尺度。
|
||||
fn time_scale(value: &str) -> TimeScale {
|
||||
match value {
|
||||
"tai" => TimeScale::Tai,
|
||||
|
|
@ -1008,6 +1044,7 @@ fn time_scale(value: &str) -> TimeScale {
|
|||
}
|
||||
|
||||
/// Formats an epoch as a UTC calendar string.
|
||||
/// 把历元格式化为 UTC 日历字符串。
|
||||
fn format_calendar(epoch: Epoch) -> String {
|
||||
match epoch.calendar(TimeScale::Utc) {
|
||||
Some(calendar) => format_calendar_text(calendar),
|
||||
|
|
@ -1016,6 +1053,7 @@ fn format_calendar(epoch: Epoch) -> String {
|
|||
}
|
||||
|
||||
/// Formats a calendar value.
|
||||
/// 格式化一个日历值。
|
||||
fn format_calendar_text(calendar: Calendar) -> String {
|
||||
format!(
|
||||
"{:04}-{:02}-{:02}T{:02}:{:02}:{:09.6}",
|
||||
|
|
@ -1032,6 +1070,10 @@ fn format_calendar_text(calendar: Calendar) -> String {
|
|||
///
|
||||
/// An empty slice returns not-a-number for both, which encodes as `null` and
|
||||
/// is rendered by the frontend as a missing value.
|
||||
///
|
||||
/// 返回切片中的最小值与最大值。
|
||||
///
|
||||
/// 空切片对两者都返回非数,它编码为 `null`,并由前端渲染为缺失值。
|
||||
fn min_max(values: &[f64]) -> (f64, f64) {
|
||||
let mut minimum = f64::NAN;
|
||||
let mut maximum = f64::NAN;
|
||||
|
|
@ -1054,6 +1096,11 @@ fn min_max(values: &[f64]) -> (f64, f64) {
|
|||
///
|
||||
/// The estimate uses Kepler's third law with the WGS 72 gravitational
|
||||
/// parameter, so it is the size of the circular orbit with that mean motion.
|
||||
///
|
||||
/// 估计某个平均运动所隐含的半长轴。
|
||||
///
|
||||
/// 该估计使用开普勒第三定律与 WGS 72 引力参数,因此它是具有该平均运动的圆轨道
|
||||
/// 的大小。
|
||||
fn semi_major_axis_estimate(mean_motion: f64) -> f64 {
|
||||
if mean_motion <= 0.0 {
|
||||
return f64::NAN;
|
||||
|
|
|
|||
16
tests/api.rs
16
tests/api.rs
|
|
@ -1,9 +1,15 @@
|
|||
//! Tests for the router and the payloads it encodes.
|
||||
//! 路由器及其编码的载荷的测试。
|
||||
//!
|
||||
//! 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 std::sync::Arc;
|
||||
|
||||
use apsis_book::api::*;
|
||||
|
|
@ -25,6 +31,9 @@ fn router() -> Router {
|
|||
fn get(router: &Router, path: &str) -> Response {
|
||||
// The request parser splits the target, so a test that wants a query
|
||||
// supplies it in the path and it is split the same way here.
|
||||
|
||||
// 请求解析器会拆分目标,因此想要查询的测试把它写在路径里,这里也以同样的
|
||||
// 方式拆分。
|
||||
let (path, query) = match path.split_once('?') {
|
||||
Some((path, query)) => (path.to_string(), Some(query.to_string())),
|
||||
None => (path.to_string(), None),
|
||||
|
|
@ -212,6 +221,8 @@ fn malformed_bodies_are_rejected() {
|
|||
fn a_hashed_asset_is_immutable_and_a_plain_one_is_not() {
|
||||
// A content hash in the name means the name changes with the
|
||||
// contents, so the browser can keep it for as long as it likes.
|
||||
|
||||
// 名字中含内容哈希意味着名字随内容变化,因此浏览器可以想存多久就存多久。
|
||||
assert_eq!(
|
||||
cache_policy("assets/index-Vou83oaV.js"),
|
||||
"public, max-age=31536000, immutable"
|
||||
|
|
@ -221,10 +232,12 @@ fn a_hashed_asset_is_immutable_and_a_plain_one_is_not() {
|
|||
"public, max-age=31536000, immutable"
|
||||
);
|
||||
// A name without one is reused by the next build.
|
||||
// 不含哈希的名字会被下一次构建复用。
|
||||
assert_eq!(cache_policy("index.html"), UNHASHED_CACHE_POLICY);
|
||||
assert_eq!(cache_policy("assets/app.js"), UNHASHED_CACHE_POLICY);
|
||||
assert_eq!(cache_policy("favicon.ico"), UNHASHED_CACHE_POLICY);
|
||||
// A name that merely ends in a dash is not hashed.
|
||||
// 仅仅以连字符结尾的名字不算带哈希。
|
||||
assert_eq!(cache_policy("assets/app-.js"), UNHASHED_CACHE_POLICY);
|
||||
assert_eq!(cache_policy("assets/no-extension"), UNHASHED_CACHE_POLICY);
|
||||
}
|
||||
|
|
@ -232,6 +245,7 @@ fn a_hashed_asset_is_immutable_and_a_plain_one_is_not() {
|
|||
#[test]
|
||||
fn a_hash_is_recognised_by_its_shape() {
|
||||
// The bundler emits a mixed case base64 hash.
|
||||
// 打包器输出的哈希是大小写混合的 base64。
|
||||
assert!(is_content_hashed("index-Vou83oaV.js"));
|
||||
assert!(is_content_hashed("style-CEH25aoZ.css"));
|
||||
assert!(is_content_hashed("app-deadbeef.js"));
|
||||
|
|
@ -270,6 +284,7 @@ fn post_to_a_get_route_is_refused() {
|
|||
#[test]
|
||||
fn a_crate_card_takes_the_first_sentence_only() {
|
||||
// The index shows what a crate is; the crate page shows the rest.
|
||||
// 索引说明一个 crate 是什么;其余内容由 crate 页面呈现。
|
||||
assert_eq!(
|
||||
crate_summary_brief(
|
||||
"ApsisRS \u{7684}\u{5171}\u{4eab}\u{57fa}\u{7840}\u{7c7b}\u{578b}\u{3002}\u{672c} crate \u{4f4d}\u{4e8e}\u{4f9d}\u{8d56}\u{56fe}\u{7684}\u{5e95}\u{5c42}\u{3002}"
|
||||
|
|
@ -281,6 +296,7 @@ fn a_crate_card_takes_the_first_sentence_only() {
|
|||
#[test]
|
||||
fn a_number_is_not_a_sentence_end() {
|
||||
// A version or a measurement must not cut a sentence in half.
|
||||
// 版本号或量值不得把句子截成两半。
|
||||
assert_eq!(
|
||||
crate_summary_brief("Reads version 1.5 of the format. Then it checksums."),
|
||||
"Reads version 1.5 of the format."
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
//! Tests for the interface catalog reader.
|
||||
//! 接口目录读取器的测试。
|
||||
//!
|
||||
//! 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::api::*;
|
||||
use apsis_book::testkit::parse_item;
|
||||
|
||||
|
|
@ -46,6 +52,8 @@ fn malformed_input_yields_an_empty_catalog() {
|
|||
fn an_item_carries_both_languages() {
|
||||
// `apsis-rs` writes the doc comment in English and then in Chinese, and
|
||||
// the extractor separates the two into the catalogue.
|
||||
|
||||
// `apsis-rs` 把文档注释先写成英文、再写成中文,提取器把两者分开写入目录。
|
||||
let item = r#"{"kind":"fn","name":"new","doc":"English.",
|
||||
"doc_zh":"中文。","signature":"pub fn new()"}"#;
|
||||
let parsed = parse_item(
|
||||
|
|
@ -62,6 +70,8 @@ fn an_item_carries_both_languages() {
|
|||
fn an_item_without_a_translation_has_none() {
|
||||
// A comment that is English only leaves the Chinese empty, and the
|
||||
// reader is then shown the English rather than nothing.
|
||||
|
||||
// 只有英文的注释使中文一侧为空,此时读者看到的是英文,而不是什么都没有。
|
||||
let item = r#"{"kind":"fn","name":"new","doc":"English.",
|
||||
"signature":"pub fn new()"}"#;
|
||||
let parsed = parse_item(
|
||||
|
|
@ -78,6 +88,9 @@ fn the_committed_catalogue_carries_both_languages_for_every_item() {
|
|||
// The translation comes from the source doc comments rather than from a
|
||||
// file maintained beside them, so a crate that was never translated shows
|
||||
// up here rather than silently reading as English to a Chinese reader.
|
||||
|
||||
// 译文来自源码文档注释,而不是在它们旁边另行维护的文件,因此从未翻译过的
|
||||
// crate 会在这里暴露出来,而不是悄悄以英文呈现给中文读者。
|
||||
let catalog = apsis_book::docs::load_api_catalog();
|
||||
let mut missing = Vec::new();
|
||||
for krate in &catalog.crates {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
//! Tests for the embedded content table.
|
||||
//! 内嵌内容表的测试。
|
||||
//!
|
||||
//! 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::content::*;
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
//! 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;
|
||||
|
||||
|
|
@ -43,9 +49,11 @@ 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")
|
||||
}
|
||||
|
|
@ -68,6 +76,9 @@ 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.
|
||||
|
||||
// 样例正文以 `# SGP4` 开头,与元数据标题 `SGP4 propagation` 不同,
|
||||
// 因此它被保留。
|
||||
assert!(document.english.html.contains("<h1"));
|
||||
assert!(
|
||||
document
|
||||
|
|
@ -78,6 +89,7 @@ fn a_repeated_top_level_title_is_dropped_from_the_body() {
|
|||
);
|
||||
|
||||
// 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"));
|
||||
|
|
@ -95,18 +107,21 @@ 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());
|
||||
}
|
||||
|
|
@ -151,6 +166,8 @@ fn a_translated_document_is_indexed_in_both_languages() {
|
|||
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);
|
||||
|
|
@ -163,8 +180,11 @@ fn tokenise_emits_words_and_han_pairs() {
|
|||
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());
|
||||
}
|
||||
|
||||
|
|
@ -189,11 +209,14 @@ fn a_chinese_query_matches_a_word_inside_a_sentence() {
|
|||
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");
|
||||
|
|
@ -236,6 +259,10 @@ fn a_document_is_offered_once_when_its_layer_has_subjects() {
|
|||
// 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",
|
||||
|
|
@ -266,6 +293,7 @@ fn a_document_is_offered_once_when_its_layer_has_subjects() {
|
|||
);
|
||||
|
||||
// Both documents are still reachable, each under exactly one layer.
|
||||
// 两篇文档仍然可达,各自恰好属于一个层。
|
||||
let physics = tree
|
||||
.groups()
|
||||
.iter()
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
//! Tests for the Markdown renderer.
|
||||
//! Markdown 渲染器的测试。
|
||||
//!
|
||||
//! 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::markdown::*;
|
||||
|
||||
#[test]
|
||||
|
|
@ -25,6 +31,7 @@ fn a_display_formula_is_rendered_on_its_own() {
|
|||
#[test]
|
||||
fn a_dollar_without_a_pair_is_left_alone() {
|
||||
// A price must survive as a price.
|
||||
// 价格必须作为价格保留下来。
|
||||
let html = render("It costs $5 today.");
|
||||
assert!(html.contains("$5"), "the lone dollar was consumed");
|
||||
assert!(!html.contains("<math"), "no formula should be produced");
|
||||
|
|
@ -34,6 +41,8 @@ fn a_dollar_without_a_pair_is_left_alone() {
|
|||
fn two_prices_are_not_a_formula() {
|
||||
// The closing dollar has a space before it, which is what separates
|
||||
// this from a formula.
|
||||
|
||||
// 收尾的美元符号前有一个空格,这正是它与公式的区别所在。
|
||||
let html = render("It costs $5 and then $6.");
|
||||
assert!(
|
||||
!html.contains("<math"),
|
||||
|
|
@ -93,6 +102,7 @@ fn a_rust_path_link_points_at_the_interface_page() {
|
|||
assert!(html.contains("data-api=\"Epoch\""), "{html}");
|
||||
assert!(html.contains("href=\"#/api/Epoch\""), "{html}");
|
||||
// A web address is left as it is.
|
||||
// 网址按原样保留。
|
||||
assert!(html.contains("href=\"https://example.com\""), "{html}");
|
||||
}
|
||||
|
||||
|
|
@ -108,11 +118,13 @@ fn a_path_to_a_variant_resolves_to_the_declaring_item() {
|
|||
);
|
||||
assert!(html.contains("data-api=\"PropagationErrorKind\""), "{html}");
|
||||
// A path whose segments are all unknown is left as written.
|
||||
// 所有片段都未知的路径按原样保留。
|
||||
let html = render_with("[`Nope`](crate::Unknown::Missing).\n", Some(&known));
|
||||
assert!(!html.contains("data-api"), "{html}");
|
||||
}
|
||||
|
||||
/// The names of two items, for the shortcut link test.
|
||||
/// 两个条目的名字,供简写链接的测试使用。
|
||||
fn items() -> std::collections::HashSet<String> {
|
||||
["mjd", "Epoch"]
|
||||
.iter()
|
||||
|
|
@ -131,10 +143,12 @@ fn a_shortcut_link_points_at_the_interface_page() {
|
|||
assert!(html.contains("href=\"#/api/mjd\""), "{html}");
|
||||
assert!(html.contains("data-api=\"Epoch\""), "{html}");
|
||||
// A label that is not an item name stays as a code span.
|
||||
// 不是条目名的标签保持为代码片段。
|
||||
let html = render_with("The range is [`0, 1`].\n", Some(&known));
|
||||
assert!(!html.contains("data-api"), "{html}");
|
||||
assert!(html.contains("<code>0, 1</code>"), "{html}");
|
||||
// A name that is not in the catalog also stays as a code span.
|
||||
// 不在目录中的名字同样保持为代码片段。
|
||||
let html = render_with("See [`NotAnItem`].\n", Some(&known));
|
||||
assert!(!html.contains("data-api"), "{html}");
|
||||
}
|
||||
|
|
@ -144,6 +158,9 @@ fn a_chinese_heading_keeps_its_own_anchor() {
|
|||
// Only ASCII letters were kept, so every Chinese heading reduced to
|
||||
// the fallback and the whole table of contents pointed at the first
|
||||
// section.
|
||||
|
||||
// 只保留了 ASCII 字母,于是每个中文标题都归约到同一个回退值,整份目录都指向
|
||||
// 第一节。
|
||||
let html = render("# 时间尺度\n\n正文。\n\n# 单位约定\n\n更多。\n");
|
||||
assert!(html.contains("id=\"时间尺度\""), "{html}");
|
||||
assert!(html.contains("id=\"单位约定\""), "{html}");
|
||||
|
|
@ -192,6 +209,8 @@ fn plain_text_drops_the_markup() {
|
|||
fn plain_text_resolves_a_shortcut_reference() {
|
||||
// A rustdoc shortcut keeps the item name and loses the brackets,
|
||||
// which otherwise appear verbatim in a one line summary.
|
||||
|
||||
// rustdoc 简写保留条目名并去掉方括号,否则方括号会原样出现在一行摘要里。
|
||||
let text = to_plain_text("See [`Vec3`] and [`Epoch::mjd`](crate::Epoch::mjd).\n");
|
||||
assert!(text.contains("Vec3"));
|
||||
assert!(text.contains("mjd"));
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
//! Tests for the formula renderer.
|
||||
//! 公式渲染器的测试。
|
||||
//!
|
||||
//! 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::mathml::*;
|
||||
use apsis_book::testkit::mathml;
|
||||
|
||||
|
|
@ -47,6 +53,9 @@ fn an_unknown_command_is_kept_as_written() {
|
|||
// Losing part of a formula would be worse than showing it plainly.
|
||||
// Text is given an element even when it is not understood, because text
|
||||
// without one is not drawn at all.
|
||||
|
||||
// 丢掉公式的一部分比把它朴素地显示出来更糟。即使用户未被理解,文本仍会被赋予
|
||||
// 元素,因为没有元素的文本根本不会被绘制。
|
||||
assert_eq!(mathml("\\notacommand"), "<mtext>\\notacommand</mtext>");
|
||||
}
|
||||
|
||||
|
|
@ -55,6 +64,9 @@ fn a_comma_is_kept() {
|
|||
// The command is the spacing one, so dropping it would run a list of
|
||||
// symbols together. It is written as an operator so that it keeps its
|
||||
// space, which is what a comma is for.
|
||||
|
||||
// 该命令是空格类命令,丢掉它会让一串符号挤在一起。它被写成运算符,从而保住
|
||||
// 自己的空格——逗号正是为此而设。
|
||||
assert_eq!(
|
||||
mathml("\\alpha, \\beta"),
|
||||
"<mrow><mi>α</mi><mo>,</mo><mspace width=\"0.25em\"/><mi>β</mi></mrow>"
|
||||
|
|
@ -82,6 +94,7 @@ fn a_display_formula_is_marked_as_such() {
|
|||
#[test]
|
||||
fn the_source_is_kept_in_the_output() {
|
||||
// The page keeps the source so the formula can be copied back.
|
||||
// 页面保留源码,使公式能被复制回去。
|
||||
assert!(render("x^2", false).contains("data-tex=\"x^2\""));
|
||||
}
|
||||
|
||||
|
|
@ -90,6 +103,9 @@ fn a_bracketed_group_takes_the_power_that_follows_it() {
|
|||
// The bracket branches returned before the scripts were attached, so the
|
||||
// caret was set as an ordinary symbol and the exponent sat beside the
|
||||
// group: `(a + b)^2` showed its own caret.
|
||||
|
||||
// 括号分支在挂接上下标之前就返回了,于是脱字符被当作普通符号排版,指数则
|
||||
// 落在括号组旁边:`(a + b)^2` 把自己的脱字符显示了出来。
|
||||
for latex in [
|
||||
r"(a + b)^2",
|
||||
r"\left( \frac{x_{ke}}{n_0} \right)^{2/3}",
|
||||
|
|
@ -109,6 +125,7 @@ fn a_bracketed_group_takes_the_power_that_follows_it() {
|
|||
}
|
||||
|
||||
/// The MathML of a formula written in LaTeX.
|
||||
/// 以 LaTeX 书写的公式的 MathML。
|
||||
fn render_plain_of(latex: &str) -> String {
|
||||
apsis_book::docs::mathml::render(latex, false)
|
||||
.split("display=\"inline\">")
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
//! Tests for access to the embedded frontend assets.
|
||||
//! 对内嵌前端资源的访问的测试。
|
||||
//!
|
||||
//! 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::embedded::*;
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -26,6 +26,26 @@
|
|||
//! as `\ln`, is drawn as those letters, so stripping every command only
|
||||
//! makes this check less strict about a function name and never hides a lost
|
||||
//! number.
|
||||
//!
|
||||
//! 比对公式显示的与它被书写的内容。
|
||||
//!
|
||||
//! 公式在文档中写成纯文本,再翻译为 MathML。翻译可能丢掉一个 token 而公式仍
|
||||
//! 在:结果看起来仍像公式,却是错的。这里就发生过——`year + 4800` 里的 4800
|
||||
//! 被写成行内裸文本,而浏览器不绘制它,于是渲染出的页面上所有多位数都不见了,
|
||||
//! 标记看起来却还算合理。
|
||||
//!
|
||||
//! 因此本检查针对字符而不是结构:源码中的每一个字母与数字都必须出现在页面会
|
||||
//! 显示的文本里。它遍历每一篇文档而不是抽取样本,因为该缺陷是逐个公式的,样本
|
||||
//! 会漏掉大部分。
|
||||
//!
|
||||
//! 只比对字母与数字。公式源码中的其他字符是记法而非内容:`^` 与 `_` 说明升降,
|
||||
//! `*` 表示相乘,`/` 表示相除,花括号用于分组。它们都应当从页面上消失,因为
|
||||
//! 排版已经表达了它们的意思。而数字或名字没有这样的借口,少一个就是缺陷。
|
||||
//!
|
||||
//! LaTeX 命令是同一类记法。`\sqrt` 被画成根号,`\Delta` 被画成希腊字母,
|
||||
//! 因此它们的字母本就消失,不算缺失。名字即其字形的命令,例如 `\ln`,就按那些
|
||||
//! 字母绘制,所以剥掉所有命令只会让本检查对函数名更宽松,而绝不会掩盖一个丢失
|
||||
//! 的数字。
|
||||
|
||||
use apsis_book::docs::document::Language;
|
||||
|
||||
|
|
@ -36,12 +56,20 @@ use apsis_book::docs::document::Language;
|
|||
/// version counted any text inside any element, which made the check below
|
||||
/// pass whatever the renderer produced: text with no element was counted as
|
||||
/// drawn although the browser shows nothing.
|
||||
///
|
||||
/// 返回读者看到的文本。
|
||||
///
|
||||
/// 浏览器绘制叶子元素内的文本,并丢弃没有元素包裹的文本,因此这里只按同样的
|
||||
/// 规则统计。早先的版本统计任何元素内部的文本,使下面的检查无论在渲染器产出
|
||||
/// 什么时都通过:没有元素的文本被算作已绘制,而浏览器什么也不显示。
|
||||
fn visible(html: &str) -> String {
|
||||
/// The elements whose text is drawn.
|
||||
/// 其文本会被绘制的元素。
|
||||
const LEAF: [&str; 6] = ["mi", "mo", "mn", "mtext", "mspace", "ms"];
|
||||
|
||||
let mut out = String::new();
|
||||
// The elements currently open, innermost last.
|
||||
// 当前打开的元素,最内层在最后。
|
||||
let mut open: Vec<String> = Vec::new();
|
||||
let mut characters = html.chars().peekable();
|
||||
let mut in_tag = false;
|
||||
|
|
@ -70,6 +98,7 @@ fn visible(html: &str) -> String {
|
|||
_ if in_tag => tag.push(character),
|
||||
_ => {
|
||||
// Drawn only when the innermost open element holds text.
|
||||
// 只有当最内层打开的元素持有文本时才绘制。
|
||||
if open
|
||||
.last()
|
||||
.is_some_and(|name| LEAF.contains(&name.as_str()))
|
||||
|
|
@ -89,12 +118,22 @@ fn visible(html: &str) -> String {
|
|||
/// its rows, and the braces that group. None of them is drawn as those
|
||||
/// characters, so none of them is expected on the page. What remains is the
|
||||
/// letters and digits, which are content and must all appear.
|
||||
///
|
||||
/// 返回公式源码中的内容字符。
|
||||
///
|
||||
/// 被剔除的是记法:命令名、环境名、分隔矩阵单元格的 `&` 与分隔各行的 `\\`,
|
||||
/// 以及用于分组的花括号。它们都不以这些字符的形式被绘制,因此都不应出现在
|
||||
/// 页面上。剩下的是字母与数字,它们是内容,必须全部出现。
|
||||
fn content_characters(source: &str) -> String {
|
||||
// The attribute is HTML escaped, so a cell separator arrives as `&`
|
||||
// and would otherwise leave the letters of `amp` behind.
|
||||
// `sqrt` is written bare in the plain notation and with a backslash in
|
||||
// LaTeX; the bare form has to go as well, because it is drawn as a
|
||||
// radical sign rather than as its letters.
|
||||
|
||||
// 该属性经过 HTML 转义,因此单元格分隔符到手时是 `&`,否则会留下
|
||||
// `amp` 这几个字母。`sqrt` 在朴素记法中写成裸词、在 LaTeX 中带反斜杠;
|
||||
// 裸词形式同样要去掉,因为它被绘制成根号而不是那些字母。
|
||||
let unescaped = source
|
||||
.replace("sqrt", "")
|
||||
.replace("&", "&")
|
||||
|
|
@ -111,6 +150,10 @@ fn content_characters(source: &str) -> String {
|
|||
// The name is captured so that `begin` and `end` can be told
|
||||
// apart from the rest, because those two carry the name of an
|
||||
// environment in braces after them.
|
||||
|
||||
// 命令一直延伸到第一个非字母字符。名字被捕获下来,是为了把
|
||||
// `begin` 与 `end` 同其余命令区分开——只有这两个会在其后用花括号
|
||||
// 携带环境名。
|
||||
'\\' => {
|
||||
let mut name = String::new();
|
||||
while let Some(next) = characters.peek().copied() {
|
||||
|
|
@ -137,6 +180,7 @@ fn content_characters(source: &str) -> String {
|
|||
}
|
||||
|
||||
/// Returns the formula sources of one document, in order.
|
||||
/// 按顺序返回一篇文档中的公式源码。
|
||||
fn formula_blocks(html: &str) -> Vec<String> {
|
||||
let mut found = Vec::new();
|
||||
let mut rest = html;
|
||||
|
|
@ -156,6 +200,7 @@ fn every_formula_shows_every_character_it_was_written_with() {
|
|||
let mut failures = Vec::new();
|
||||
|
||||
// The concept documents, in both languages.
|
||||
// 概念文档,含两种语言。
|
||||
let mut sources: Vec<(String, String)> = Vec::new();
|
||||
for document in &apsis_book::docs::load_documents() {
|
||||
for language in [Language::English, Language::Chinese] {
|
||||
|
|
@ -165,10 +210,15 @@ fn every_formula_shows_every_character_it_was_written_with() {
|
|||
// And the interface documentation, which carries formulas of its own in
|
||||
// the doc comments of the crate it describes. Leaving it out would have
|
||||
// hidden every formula there.
|
||||
|
||||
// 以及接口文档,它在其所描述的 crate 的文档注释中自带公式。把它排除在外会
|
||||
// 让那里的每一个公式都无人检查。
|
||||
for krate in apsis_book::docs::load_api_catalog().crates {
|
||||
for item in &krate.items {
|
||||
// The doc comment is Markdown, and the service renders it with
|
||||
// the same renderer as a document.
|
||||
|
||||
// 文档注释是 Markdown,服务用与文档相同的渲染器渲染它。
|
||||
for text in [Some(&item.doc), item.doc_zh.as_ref()]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
|
|
@ -188,11 +238,17 @@ fn every_formula_shows_every_character_it_was_written_with() {
|
|||
checked += 1;
|
||||
// The source as written, without the spaces that only set it
|
||||
// out on the page.
|
||||
|
||||
// 源码按书写时的样子,去掉那些仅为页面排版而设的空格。
|
||||
let written = content_characters(&source);
|
||||
// The part of the page that is the formula itself: the
|
||||
// MathML and nothing else. The source is carried beside it
|
||||
// for a reader to copy, and including it here would compare
|
||||
// the source against itself and pass whatever happened.
|
||||
|
||||
// 页面上属于公式本身的部分:MathML,别无其他。源码被携带在其旁
|
||||
// 供读者复制,若把它算进来,就成了源码与自身比对,无论发生什么
|
||||
// 都会通过。
|
||||
let from = html.find(&format!("data-tex=\"{source}\"")).unwrap_or(0);
|
||||
let start = html[from..].find("<math").map(|e| from + e).unwrap_or(from);
|
||||
let end = html[start..]
|
||||
|
|
@ -207,6 +263,10 @@ fn every_formula_shows_every_character_it_was_written_with() {
|
|||
// raise or lower, and neither is drawn. One that reaches the
|
||||
// output is a script that was never attached, which leaves a
|
||||
// formula that has every character in it and is still wrong.
|
||||
|
||||
// 脱字符与下划线是记法:它们说明升降,二者都不被绘制。出现在
|
||||
// 输出里的那一个,说明有上下标从未被挂接,这会留下一个每个字符
|
||||
// 都在、却依然是错的公式。
|
||||
let body = &html[start..end];
|
||||
for marker in ["<mo>^</mo>", "<mo>_</mo>"] {
|
||||
assert!(
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
//! Tests for the graph model and the crate structure.
|
||||
//! 图模型与 crate 结构的测试。
|
||||
//!
|
||||
//! 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::graph::*;
|
||||
|
||||
const SAMPLE: &str = r#"{
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
//! Tests for the request parser.
|
||||
//! 请求解析器的测试。
|
||||
//!
|
||||
//! 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::http::request::*;
|
||||
|
||||
use std::io::Cursor;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
//! Tests for the response writer.
|
||||
//! 响应写出器的测试。
|
||||
//!
|
||||
//! 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::http::response::*;
|
||||
|
||||
#[test]
|
||||
|
|
@ -33,6 +39,9 @@ fn empty_body_has_no_content_type() {
|
|||
let text = String::from_utf8(response.to_bytes()).unwrap();
|
||||
// The response carries `X-Content-Type-Options`, so the assertion is
|
||||
// on the leading header line rather than on the substring.
|
||||
|
||||
// 响应带有 `X-Content-Type-Options`,因此断言落在起始的那一行首部上,
|
||||
// 而不是落在子串上。
|
||||
assert!(!text.contains("\r\nContent-Type:"));
|
||||
assert!(text.contains("Content-Length: 0\r\n"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
//! Tests for the listener and the worker pool.
|
||||
//! 监听器与工作线程池的测试。
|
||||
//!
|
||||
//! 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::http::request::{Request, RequestError};
|
||||
use apsis_book::http::response::{Response, Status};
|
||||
use std::net::SocketAddr;
|
||||
|
|
@ -45,6 +51,7 @@ fn serves_a_request_over_a_real_socket() {
|
|||
|
||||
drop(stream);
|
||||
// The accept loop ends when the listener is dropped with the thread.
|
||||
// 接受循环在线程随监听器一同被丢弃时结束。
|
||||
let _ = running;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
//! Tests for the bilingual strings.
|
||||
//! 双语字符串的测试。
|
||||
//!
|
||||
//! 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::i18n::*;
|
||||
|
||||
use apsis_book::json;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
//! Tests for the JSON codec.
|
||||
//! JSON 编解码器的测试。
|
||||
//!
|
||||
//! 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::testkit::json_object;
|
||||
|
||||
use apsis_book::json::*;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,15 @@
|
|||
//! The binary is located through the `CARGO_BIN_EXE_apsis-book` variable that
|
||||
//! cargo sets for an integration test, so the test always runs against the
|
||||
//! binary built from the same sources.
|
||||
//!
|
||||
//! 针对服务二进制的端到端测试。
|
||||
//!
|
||||
//! 测试在一个临时端口上启动真实的二进制,等待它报告自己的地址,然后以普通 TCP
|
||||
//! 请求与之通信。这样把请求解析器、路由器、JSON 编解码器、内嵌内容与工具处理
|
||||
//! 函数放在一起检验,这是单元测试做不到的。
|
||||
//!
|
||||
//! 二进制的位置取自 cargo 为集成测试设置的 `CARGO_BIN_EXE_apsis-book`,因此测试
|
||||
//! 总是针对由同一份源码构建出的二进制运行。
|
||||
|
||||
use std::io::{BufRead, BufReader, Read, Write};
|
||||
use std::net::TcpStream;
|
||||
|
|
@ -17,6 +26,7 @@ use std::thread;
|
|||
use std::time::{Duration, Instant};
|
||||
|
||||
/// A running service that is stopped when it is dropped.
|
||||
/// 一个运行中的服务,被丢弃时即停止。
|
||||
struct Service {
|
||||
child: Child,
|
||||
address: String,
|
||||
|
|
@ -24,6 +34,7 @@ struct Service {
|
|||
|
||||
impl Service {
|
||||
/// Starts the binary and waits for it to report its address.
|
||||
/// 启动二进制并等待它报告自己的地址。
|
||||
fn start() -> Self {
|
||||
let mut child = Command::new(env!("CARGO_BIN_EXE_apsis-book"))
|
||||
.args(["--address", "127.0.0.1:0", "--workers", "2"])
|
||||
|
|
@ -51,6 +62,7 @@ impl Service {
|
|||
}
|
||||
|
||||
/// Sends a raw request and returns the whole response as text.
|
||||
/// 发送一个原始请求,并把整个响应作为文本返回。
|
||||
fn request(&self, request: &str) -> String {
|
||||
let mut stream =
|
||||
TcpStream::connect(&self.address).expect("the service must accept a connection");
|
||||
|
|
@ -68,11 +80,13 @@ impl Service {
|
|||
}
|
||||
|
||||
/// Sends a GET request.
|
||||
/// 发送一个 GET 请求。
|
||||
fn get(&self, path: &str) -> String {
|
||||
self.request(&format!("GET {path} HTTP/1.1\r\nHost: localhost\r\n\r\n"))
|
||||
}
|
||||
|
||||
/// Sends a POST request with a JSON body.
|
||||
/// 发送一个带 JSON 请求体的 POST 请求。
|
||||
fn post(&self, path: &str, body: &str) -> String {
|
||||
self.request(&format!(
|
||||
"POST {path} HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{body}",
|
||||
|
|
@ -89,6 +103,7 @@ impl Drop for Service {
|
|||
}
|
||||
|
||||
/// Returns the body of a response.
|
||||
/// 返回某个响应的请求体。
|
||||
fn body(response: &str) -> &str {
|
||||
response
|
||||
.split_once("\r\n\r\n")
|
||||
|
|
@ -97,6 +112,7 @@ fn body(response: &str) -> &str {
|
|||
}
|
||||
|
||||
/// Returns the status line of a response.
|
||||
/// 返回某个响应的状态行。
|
||||
fn status(response: &str) -> &str {
|
||||
response.lines().next().unwrap_or("")
|
||||
}
|
||||
|
|
@ -105,6 +121,10 @@ fn status(response: &str) -> &str {
|
|||
///
|
||||
/// The accept loop starts a moment after the address is printed, so the
|
||||
/// first connection can be refused.
|
||||
///
|
||||
/// 等待某个条件,轮询直到它成立或超过时限。
|
||||
///
|
||||
/// 接受循环在地址打印之后片刻才启动,因此第一次连接可能被拒绝。
|
||||
fn wait_ready(service: &Service) {
|
||||
let deadline = Instant::now() + Duration::from_secs(10);
|
||||
while Instant::now() < deadline {
|
||||
|
|
@ -152,6 +172,7 @@ fn serves_the_document_index_and_a_document() {
|
|||
let listing = body(&index);
|
||||
assert!(listing.contains("\"groups\""), "{listing}");
|
||||
// The concepts and the implementation are separate groups.
|
||||
// 概念与实现是两个分开的分组。
|
||||
assert!(listing.contains("\"concepts\""), "{listing}");
|
||||
assert!(listing.contains("\"implementation\""), "{listing}");
|
||||
|
||||
|
|
@ -185,15 +206,19 @@ fn interface_documentation_is_translated() {
|
|||
let item = service.get("/api/api/items/propagate_minutes");
|
||||
let body = body(&item);
|
||||
// The Chinese documentation is served and is rendered to HTML.
|
||||
// 中文文档会被提供,并渲染为 HTML。
|
||||
assert!(body.contains("传播"), "the Chinese doc should be served");
|
||||
assert!(body.contains("<p>"), "the doc should be rendered as HTML");
|
||||
// A doc comment link to another item becomes an internal route.
|
||||
// 指向另一个条目的文档注释链接会成为一个内部路由。
|
||||
assert!(
|
||||
body.contains("data-api=") && body.contains("#/api/"),
|
||||
"the doc link should be resolved: {body}"
|
||||
);
|
||||
// The English documentation is still present, so the language can switch
|
||||
// without another request.
|
||||
|
||||
// 英文文档仍然存在,因此切换语言无需再一次请求。
|
||||
assert!(
|
||||
body.contains("Returns the state of the satellite"),
|
||||
"the English doc should still be present"
|
||||
|
|
@ -207,6 +232,7 @@ fn crate_summaries_are_translated() {
|
|||
let response = service.get("/api/api/crates/apsis-sgp4");
|
||||
let body = body(&response);
|
||||
// The Chinese summary and its plain form are both served.
|
||||
// 中文概要与它的纯文本形式都会被提供。
|
||||
assert!(
|
||||
body.contains("传播器"),
|
||||
"the Chinese summary should be served"
|
||||
|
|
@ -301,6 +327,7 @@ fn rejects_a_malformed_body_without_failing() {
|
|||
let body = body(&response);
|
||||
assert!(body.contains("\"ok\":false"), "{body}");
|
||||
// The service must still answer a subsequent request.
|
||||
// 服务必须仍能回应后续请求。
|
||||
assert!(status(&service.get("/health")).contains("200"));
|
||||
}
|
||||
|
||||
|
|
@ -321,6 +348,7 @@ fn documents_are_served_in_both_languages() {
|
|||
assert!(body.contains("\"language\":\"zh\""), "{body}");
|
||||
assert!(body.contains("\"language\":\"en\""), "{body}");
|
||||
// The Chinese form of the document carries Chinese text.
|
||||
// 文档的中文形式携带中文文本。
|
||||
assert!(body.contains("传播"), "the Chinese form should be embedded");
|
||||
}
|
||||
|
||||
|
|
@ -372,6 +400,8 @@ fn the_flow_index_lists_every_flow() {
|
|||
assert!(body.contains("tle-parsing"), "{body}");
|
||||
// The index carries a bilingual title, so the frontend needs no copy of
|
||||
// the flow names.
|
||||
|
||||
// 索引携带双语标题,因此前端无须另存一份流程图名。
|
||||
assert!(body.contains("\"title\""), "{body}");
|
||||
assert!(body.contains("\"zh\""), "{body}");
|
||||
}
|
||||
|
|
@ -382,6 +412,8 @@ fn static_assets_are_revalidated() {
|
|||
wait_ready(&service);
|
||||
// The build writes stable file names, so an immutable policy would pin a
|
||||
// reader to the bundle current at first load.
|
||||
|
||||
// 构建写出的是稳定的文件名,因此不可变策略会把读者钉在首次加载时的那个包上。
|
||||
let asset = service.get("/assets/app.js");
|
||||
assert!(asset.contains("no-cache"), "{asset}");
|
||||
assert!(!asset.contains("immutable"), "{asset}");
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
//! Tests for the parsed content shared by the workers.
|
||||
//! 各工作线程共享的已解析内容的测试。
|
||||
//!
|
||||
//! 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::state::*;
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
//! Tests for the tool handlers.
|
||||
//! 工具处理函数的测试。
|
||||
//!
|
||||
//! 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::testkit::json_object;
|
||||
|
||||
use apsis_book::tools::ToolError;
|
||||
|
|
@ -13,6 +19,7 @@ use apsis_book::json::Value;
|
|||
use apsis_book::tools::{Params, Registry};
|
||||
|
||||
/// Runs a tool with the given parameters.
|
||||
/// 以给定参数运行一个工具。
|
||||
fn call(id: &str, values: Value) -> Result<Value, ToolError> {
|
||||
let registry = Registry::new();
|
||||
let tool = registry.get(id).unwrap();
|
||||
|
|
@ -21,11 +28,13 @@ fn call(id: &str, values: Value) -> Result<Value, ToolError> {
|
|||
}
|
||||
|
||||
/// Returns the English text of a label, which is a bilingual object.
|
||||
/// 返回某个标签的英文文本,该标签是一个双语对象。
|
||||
fn label_english(value: &Value) -> &str {
|
||||
value.get("en").and_then(Value::as_str).unwrap_or("")
|
||||
}
|
||||
|
||||
/// Returns the numeric value of the scalar with an English label.
|
||||
/// 返回英文标签为给定值的那个标量的数值。
|
||||
fn scalar_number(value: &Value, label: &str) -> f64 {
|
||||
value
|
||||
.get("scalars")
|
||||
|
|
@ -111,6 +120,8 @@ fn kepler_conversion_rejects_a_non_elliptical_orbit() {
|
|||
// An eccentricity of one is a parabola; the conversion requires an
|
||||
// ellipse, so the tool reports the condition without naming a
|
||||
// parameter.
|
||||
|
||||
// 偏心率为一即抛物线;该转换要求椭圆,因此工具报告该条件而不指名某个参数。
|
||||
let error = call(
|
||||
"kepler-state",
|
||||
json_object! { "eccentricity" => 1.0, "semi_major_axis" => 7000.0 },
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
//! Tests for the tool result label table.
|
||||
//! 工具结果标签表的测试。
|
||||
//!
|
||||
//! 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::tools::labels::*;
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -1,15 +1,22 @@
|
|||
//! Tests for the tool result shape.
|
||||
//! 工具结果形状的测试。
|
||||
//!
|
||||
//! 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::json::Value;
|
||||
|
||||
use apsis_book::tools::labels;
|
||||
use apsis_book::tools::out::*;
|
||||
|
||||
/// The first cell of the first row of a shaped response.
|
||||
/// 某个已整形响应中第一行第一格的单元格。
|
||||
fn first_cell(value: &Value) -> Value {
|
||||
let rows = value
|
||||
.get("table")
|
||||
|
|
@ -26,6 +33,8 @@ fn first_cell(value: &Value) -> Value {
|
|||
fn a_text_cell_is_shown_in_either_language() {
|
||||
// The meaning of a field is a fixed phrase, so it is looked up rather
|
||||
// than written twice.
|
||||
|
||||
// 字段的含义是固定短语,因此走查表而不是写两遍。
|
||||
let mut table = Table::new(vec![Column::text("meaning", "Meaning")]);
|
||||
table.push(vec![Value::from("Mean motion")]);
|
||||
let value = table_response(
|
||||
|
|
@ -45,6 +54,9 @@ fn a_text_cell_is_shown_in_either_language() {
|
|||
fn an_identifier_is_not_set_as_a_formula() {
|
||||
// `mean_motion` is a field name. Set as a formula it would come out as
|
||||
// separate letters with a subscript, which is not the field name.
|
||||
|
||||
// `mean_motion` 是一个字段名。当成公式排版会变成逐字母加下标,那就不是
|
||||
// 字段名了。
|
||||
let mut table = Table::new(vec![Column::text("field", "Field")]);
|
||||
table.push(vec![Value::from("mean_motion")]);
|
||||
let value = table_response("kind", "Title", Vec::new(), &table, Vec::new(), Vec::new());
|
||||
|
|
@ -58,6 +70,7 @@ fn an_identifier_is_not_set_as_a_formula() {
|
|||
#[test]
|
||||
fn a_unit_with_a_power_is_rendered() {
|
||||
// `rev/day^2` is a unit, and the power is what a reader needs raised.
|
||||
// `rev/day^2` 是一个单位,读者需要看到的是被抬起的幂次。
|
||||
let mut table = Table::new(vec![Column::text("unit", "Unit")]);
|
||||
table.push(vec![Value::from("rev/day^2")]);
|
||||
let value = table_response("kind", "Title", Vec::new(), &table, Vec::new(), Vec::new());
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
//! Tests for the tool registry.
|
||||
//! 工具注册表的测试。
|
||||
//!
|
||||
//! 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::testkit::json_object;
|
||||
|
||||
use apsis_book::tools::*;
|
||||
|
|
|
|||
Loading…
Reference in New Issue