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:
dev 2026-10-02 13:25:02 +08:00
parent a360238a95
commit e6065dd30e
21 changed files with 326 additions and 0 deletions

View File

@ -9,6 +9,15 @@
//! condition, which is what a caller with the input in front of them needs; //! 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 //! the handler adds the parameter name so that the form can highlight the
//! field. //! field.
//!
//! 工具处理函数。
//!
//! 每个处理函数接收已校验的参数,并调用 `apsis-rs` 的公共接口。结果由
//! [`super::out`] 塑造,使前端对所有工具都用同一批组件渲染。
//!
//! 错误通过 [`ToolError`] 报告,消息为表单的使用者而写。`apsis-rs` 底层的错误
//! 类型描述了具体状况,那正是手头有输入的调用方所需要的;处理函数补上参数名,
//! 使表单能够高亮该字段。
use apsis::apsis_coord::{Ecef, Eci, Geodetic}; use apsis::apsis_coord::{Ecef, Eci, Geodetic};
use apsis::apsis_frames::EarthRotationAngle; 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 /// The identifier is one of the identifiers in the registry; an unknown one
/// is reported as a general failure. /// is reported as a general failure.
///
/// 按标识运行一个工具。
///
/// 标识是注册表中的标识之一;未知的标识被报告为一般性失败。
pub fn run(id: &str, params: &Params<'_>) -> Result<Value, ToolError> { pub fn run(id: &str, params: &Params<'_>) -> Result<Value, ToolError> {
match id { match id {
"parse-tle" => parse_tle(params), "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. /// Parses a two-line element set and reports its fields.
/// 解析两行元素集并报告其各字段。
fn parse_tle(params: &Params<'_>) -> Result<Value, ToolError> { fn parse_tle(params: &Params<'_>) -> Result<Value, ToolError> {
let text = params.text("tle")?; let text = params.text("tle")?;
let element = TwoLineElement::parse(&text).map_err(|error| { 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. /// Propagates one instant with SGP4 and reports the state.
/// 用 SGP4 传播某一时刻并报告状态。
fn sgp4_propagate(params: &Params<'_>) -> Result<Value, ToolError> { fn sgp4_propagate(params: &Params<'_>) -> Result<Value, ToolError> {
let text = params.text("tle")?; let text = params.text("tle")?;
let model = gravity_model(params.choice("model")?); 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. /// Samples a ground track and returns the points.
/// 对地面轨迹采样并返回各点。
fn sgp4_track(params: &Params<'_>) -> Result<Value, ToolError> { fn sgp4_track(params: &Params<'_>) -> Result<Value, ToolError> {
let text = params.text("tle")?; let text = params.text("tle")?;
let model = gravity_model(params.choice("model")?); let model = gravity_model(params.choice("model")?);
@ -341,6 +357,8 @@ fn sgp4_track(params: &Params<'_>) -> Result<Value, ToolError> {
.map(|value| { .map(|value| {
// The time axis is carried separately so that the chart can label // The time axis is carried separately so that the chart can label
// the abscissa without reading the table. // the abscissa without reading the table.
// 时间轴单独携带,使图表无需读取表格即可标注横坐标。
let mut members = value.members().to_vec(); let mut members = value.members().to_vec();
members.push(( members.push((
"axis".to_string(), "axis".to_string(),
@ -355,6 +373,7 @@ fn sgp4_track(params: &Params<'_>) -> Result<Value, ToolError> {
} }
/// Compares the mean elements with the osculating elements. /// Compares the mean elements with the osculating elements.
/// 把平均根数与密切根数作比较。
fn orbit_osculating(params: &Params<'_>) -> Result<Value, ToolError> { fn orbit_osculating(params: &Params<'_>) -> Result<Value, ToolError> {
let text = params.text("tle")?; let text = params.text("tle")?;
let model = gravity_model(params.choice("model")?); 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. /// Converts Keplerian elements to a state vector.
/// 把开普勒根数转换为状态矢量。
fn kepler_state(params: &Params<'_>) -> Result<Value, ToolError> { fn kepler_state(params: &Params<'_>) -> Result<Value, ToolError> {
let parameter = gravity_parameter(params.choice("model")?); let parameter = gravity_parameter(params.choice("model")?);
let elements = KeplerianElements { let elements = KeplerianElements {
@ -594,6 +614,7 @@ fn kepler_state(params: &Params<'_>) -> Result<Value, ToolError> {
} }
/// Converts an instant between time scales. /// Converts an instant between time scales.
/// 在时间尺度之间转换一个时刻。
fn time_convert(params: &Params<'_>) -> Result<Value, ToolError> { fn time_convert(params: &Params<'_>) -> Result<Value, ToolError> {
let scale = time_scale(params.choice("scale")?); let scale = time_scale(params.choice("scale")?);
let form = params.choice("input")?; let form = params.choice("input")?;
@ -647,6 +668,9 @@ fn time_convert(params: &Params<'_>) -> Result<Value, ToolError> {
if let Some(mjd) = mjd { if let Some(mjd) = mjd {
// The label names the scale the input was given on, so it // The label names the scale the input was given on, so it
// is built rather than looked up. // is built rather than looked up.
// 标签要指出输入所用的尺度,因此它是构造出来的,而不是查表得到
// 的。
scalars.push(Scalar::with_unit( scalars.push(Scalar::with_unit(
format!("Input on {}", target.name()), format!("Input on {}", target.name()),
out::fmt(mjd, 9), out::fmt(mjd, 9),
@ -700,6 +724,7 @@ fn time_convert(params: &Params<'_>) -> Result<Value, ToolError> {
} }
/// Converts a geodetic position to Earth-fixed coordinates. /// Converts a geodetic position to Earth-fixed coordinates.
/// 把大地位置转换为地固坐标。
fn geodetic_ecef(params: &Params<'_>) -> Result<Value, ToolError> { fn geodetic_ecef(params: &Params<'_>) -> Result<Value, ToolError> {
let latitude = out::radians(params.number("latitude")?); let latitude = out::radians(params.number("latitude")?);
let longitude = out::radians(params.number("longitude")?); 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. /// Rotates a vector between the inertial and Earth-fixed frames.
/// 在惯性系与地固系之间旋转一个矢量。
fn frame_transform(params: &Params<'_>) -> Result<Value, ToolError> { fn frame_transform(params: &Params<'_>) -> Result<Value, ToolError> {
let direction = params.choice("direction")?; let direction = params.choice("direction")?;
let vector = apsis::apsis_math::Vec3::new( 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 /// 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 /// one, a coast for half the ellipse's period, and a second burn onto the
/// destination circle. /// destination circle.
///
/// 规划两个圆轨道之间的霍曼转移。
///
/// 该转移是经典的两脉冲机动:一次点火进入一个近地点触及低轨、远地点触及高轨的
/// 椭圆,滑行半个椭圆周期,再一次点火进入目标圆轨道。
fn hohmann_transfer(params: &Params<'_>) -> Result<Value, ToolError> { fn hohmann_transfer(params: &Params<'_>) -> Result<Value, ToolError> {
let from_radius = params.number("from_radius")?; let from_radius = params.number("from_radius")?;
let to_radius = params.number("to_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. /// Returns the gravity model named by a select value.
/// 返回某个选择值所命名的重力模型。
fn gravity_model(value: &str) -> GravityModel { fn gravity_model(value: &str) -> GravityModel {
match value { match value {
"wgs84" => GravityModel::WGS84, "wgs84" => GravityModel::WGS84,
@ -974,6 +1006,7 @@ fn gravity_model(value: &str) -> GravityModel {
} }
/// Returns the display name of a gravity model select value. /// Returns the display name of a gravity model select value.
/// 返回某个重力模型选择值的显示名。
fn gravity_model_name(value: &str) -> &'static str { fn gravity_model_name(value: &str) -> &'static str {
match value { match value {
"wgs84" => "WGS 84", "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. /// Returns the Chinese display name of a gravity model select value.
/// 返回某个重力模型选择值的中文显示名。
fn gravity_model_name_zh(value: &str) -> &'static str { fn gravity_model_name_zh(value: &str) -> &'static str {
match value { match value {
"wgs84" => "WGS 84", "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. /// Returns the gravitational parameter of a gravity model select value.
/// 返回某个重力模型选择值的引力参数。
fn gravity_parameter(value: &str) -> GravityParameter { fn gravity_parameter(value: &str) -> GravityParameter {
match value { match value {
"wgs84" => GravityParameter::WGS84, "wgs84" => GravityParameter::WGS84,
@ -998,6 +1033,7 @@ fn gravity_parameter(value: &str) -> GravityParameter {
} }
/// Returns the time scale named by a select value. /// Returns the time scale named by a select value.
/// 返回某个选择值所命名的时间尺度。
fn time_scale(value: &str) -> TimeScale { fn time_scale(value: &str) -> TimeScale {
match value { match value {
"tai" => TimeScale::Tai, "tai" => TimeScale::Tai,
@ -1008,6 +1044,7 @@ fn time_scale(value: &str) -> TimeScale {
} }
/// Formats an epoch as a UTC calendar string. /// Formats an epoch as a UTC calendar string.
/// 把历元格式化为 UTC 日历字符串。
fn format_calendar(epoch: Epoch) -> String { fn format_calendar(epoch: Epoch) -> String {
match epoch.calendar(TimeScale::Utc) { match epoch.calendar(TimeScale::Utc) {
Some(calendar) => format_calendar_text(calendar), Some(calendar) => format_calendar_text(calendar),
@ -1016,6 +1053,7 @@ fn format_calendar(epoch: Epoch) -> String {
} }
/// Formats a calendar value. /// Formats a calendar value.
/// 格式化一个日历值。
fn format_calendar_text(calendar: Calendar) -> String { fn format_calendar_text(calendar: Calendar) -> String {
format!( format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:09.6}", "{: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 /// An empty slice returns not-a-number for both, which encodes as `null` and
/// is rendered by the frontend as a missing value. /// is rendered by the frontend as a missing value.
///
/// 返回切片中的最小值与最大值。
///
/// 空切片对两者都返回非数,它编码为 `null`,并由前端渲染为缺失值。
fn min_max(values: &[f64]) -> (f64, f64) { fn min_max(values: &[f64]) -> (f64, f64) {
let mut minimum = f64::NAN; let mut minimum = f64::NAN;
let mut maximum = 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 /// 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. /// 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 { fn semi_major_axis_estimate(mean_motion: f64) -> f64 {
if mean_motion <= 0.0 { if mean_motion <= 0.0 {
return f64::NAN; return f64::NAN;

View File

@ -1,9 +1,15 @@
//! Tests for the router and the payloads it encodes. //! Tests for the router and the payloads it encodes.
//! 路由器及其编码的载荷的测试。
//! //!
//! They live here rather than inside the module so that `src/` holds no test //! 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 //! 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 //! reaches only what the library makes public, so the few internal items the
//! tests need are opened through `apsis_book::testkit`. //! tests need are opened through `apsis_book::testkit`.
//!
//! 它们放在这里而不是模块内部,使 `src/` 不含测试代码,与 `apsis-rs` 一致。
//! 集成测试是独立的 crate,只能触达库公开的部分,因此测试所需的那少数内部条目
//! 经 `apsis_book::testkit` 开放。
use std::sync::Arc; use std::sync::Arc;
use apsis_book::api::*; use apsis_book::api::*;
@ -25,6 +31,9 @@ fn router() -> Router {
fn get(router: &Router, path: &str) -> Response { fn get(router: &Router, path: &str) -> Response {
// The request parser splits the target, so a test that wants a query // 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. // supplies it in the path and it is split the same way here.
// 请求解析器会拆分目标,因此想要查询的测试把它写在路径里,这里也以同样的
// 方式拆分。
let (path, query) = match path.split_once('?') { let (path, query) = match path.split_once('?') {
Some((path, query)) => (path.to_string(), Some(query.to_string())), Some((path, query)) => (path.to_string(), Some(query.to_string())),
None => (path.to_string(), None), 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() { fn a_hashed_asset_is_immutable_and_a_plain_one_is_not() {
// A content hash in the name means the name changes with the // 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. // contents, so the browser can keep it for as long as it likes.
// 名字中含内容哈希意味着名字随内容变化,因此浏览器可以想存多久就存多久。
assert_eq!( assert_eq!(
cache_policy("assets/index-Vou83oaV.js"), cache_policy("assets/index-Vou83oaV.js"),
"public, max-age=31536000, immutable" "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" "public, max-age=31536000, immutable"
); );
// A name without one is reused by the next build. // A name without one is reused by the next build.
// 不含哈希的名字会被下一次构建复用。
assert_eq!(cache_policy("index.html"), UNHASHED_CACHE_POLICY); assert_eq!(cache_policy("index.html"), UNHASHED_CACHE_POLICY);
assert_eq!(cache_policy("assets/app.js"), UNHASHED_CACHE_POLICY); assert_eq!(cache_policy("assets/app.js"), UNHASHED_CACHE_POLICY);
assert_eq!(cache_policy("favicon.ico"), UNHASHED_CACHE_POLICY); assert_eq!(cache_policy("favicon.ico"), UNHASHED_CACHE_POLICY);
// A name that merely ends in a dash is not hashed. // 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/app-.js"), UNHASHED_CACHE_POLICY);
assert_eq!(cache_policy("assets/no-extension"), 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] #[test]
fn a_hash_is_recognised_by_its_shape() { fn a_hash_is_recognised_by_its_shape() {
// The bundler emits a mixed case base64 hash. // The bundler emits a mixed case base64 hash.
// 打包器输出的哈希是大小写混合的 base64。
assert!(is_content_hashed("index-Vou83oaV.js")); assert!(is_content_hashed("index-Vou83oaV.js"));
assert!(is_content_hashed("style-CEH25aoZ.css")); assert!(is_content_hashed("style-CEH25aoZ.css"));
assert!(is_content_hashed("app-deadbeef.js")); assert!(is_content_hashed("app-deadbeef.js"));
@ -270,6 +284,7 @@ fn post_to_a_get_route_is_refused() {
#[test] #[test]
fn a_crate_card_takes_the_first_sentence_only() { fn a_crate_card_takes_the_first_sentence_only() {
// The index shows what a crate is; the crate page shows the rest. // The index shows what a crate is; the crate page shows the rest.
// 索引说明一个 crate 是什么;其余内容由 crate 页面呈现。
assert_eq!( assert_eq!(
crate_summary_brief( 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}" "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] #[test]
fn a_number_is_not_a_sentence_end() { fn a_number_is_not_a_sentence_end() {
// A version or a measurement must not cut a sentence in half. // A version or a measurement must not cut a sentence in half.
// 版本号或量值不得把句子截成两半。
assert_eq!( assert_eq!(
crate_summary_brief("Reads version 1.5 of the format. Then it checksums."), crate_summary_brief("Reads version 1.5 of the format. Then it checksums."),
"Reads version 1.5 of the format." "Reads version 1.5 of the format."

View File

@ -1,10 +1,16 @@
//! Tests for the interface catalog reader. //! Tests for the interface catalog reader.
//! 接口目录读取器的测试。
//! //!
//! They live here rather than inside the module so that `src/` holds no test //! 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 //! 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 //! reaches only what the library makes public, so the few internal items the
//! tests need are opened through `apsis_book::testkit`. //! tests need are opened through `apsis_book::testkit`.
//!
//! 它们放在这里而不是模块内部,使 `src/` 不含测试代码,与 `apsis-rs` 一致。
//! 集成测试是独立的 crate,只能触达库公开的部分,因此测试所需的那少数内部条目
//! 经 `apsis_book::testkit` 开放。
use apsis_book::docs::api::*; use apsis_book::docs::api::*;
use apsis_book::testkit::parse_item; use apsis_book::testkit::parse_item;
@ -46,6 +52,8 @@ fn malformed_input_yields_an_empty_catalog() {
fn an_item_carries_both_languages() { fn an_item_carries_both_languages() {
// `apsis-rs` writes the doc comment in English and then in Chinese, and // `apsis-rs` writes the doc comment in English and then in Chinese, and
// the extractor separates the two into the catalogue. // the extractor separates the two into the catalogue.
// `apsis-rs` 把文档注释先写成英文、再写成中文,提取器把两者分开写入目录。
let item = r#"{"kind":"fn","name":"new","doc":"English.", let item = r#"{"kind":"fn","name":"new","doc":"English.",
"doc_zh":"中文。","signature":"pub fn new()"}"#; "doc_zh":"中文。","signature":"pub fn new()"}"#;
let parsed = parse_item( let parsed = parse_item(
@ -62,6 +70,8 @@ fn an_item_carries_both_languages() {
fn an_item_without_a_translation_has_none() { fn an_item_without_a_translation_has_none() {
// A comment that is English only leaves the Chinese empty, and the // A comment that is English only leaves the Chinese empty, and the
// reader is then shown the English rather than nothing. // reader is then shown the English rather than nothing.
// 只有英文的注释使中文一侧为空,此时读者看到的是英文,而不是什么都没有。
let item = r#"{"kind":"fn","name":"new","doc":"English.", let item = r#"{"kind":"fn","name":"new","doc":"English.",
"signature":"pub fn new()"}"#; "signature":"pub fn new()"}"#;
let parsed = parse_item( 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 // The translation comes from the source doc comments rather than from a
// file maintained beside them, so a crate that was never translated shows // file maintained beside them, so a crate that was never translated shows
// up here rather than silently reading as English to a Chinese reader. // up here rather than silently reading as English to a Chinese reader.
// 译文来自源码文档注释,而不是在它们旁边另行维护的文件,因此从未翻译过的
// crate 会在这里暴露出来,而不是悄悄以英文呈现给中文读者。
let catalog = apsis_book::docs::load_api_catalog(); let catalog = apsis_book::docs::load_api_catalog();
let mut missing = Vec::new(); let mut missing = Vec::new();
for krate in &catalog.crates { for krate in &catalog.crates {

View File

@ -1,10 +1,16 @@
//! Tests for the embedded content table. //! Tests for the embedded content table.
//! 内嵌内容表的测试。
//! //!
//! They live here rather than inside the module so that `src/` holds no test //! 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 //! 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 //! reaches only what the library makes public, so the few internal items the
//! tests need are opened through `apsis_book::testkit`. //! tests need are opened through `apsis_book::testkit`.
//!
//! 它们放在这里而不是模块内部,使 `src/` 不含测试代码,与 `apsis-rs` 一致。
//! 集成测试是独立的 crate,只能触达库公开的部分,因此测试所需的那少数内部条目
//! 经 `apsis_book::testkit` 开放。
use apsis_book::docs::content::*; use apsis_book::docs::content::*;
#[test] #[test]

View File

@ -1,10 +1,16 @@
//! Tests for the document model and the search tokenizer. //! Tests for the document model and the search tokenizer.
//! 文档模型与搜索分词的测试。
//! //!
//! They live here rather than inside the module so that `src/` holds no test //! 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 //! 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 //! reaches only what the library makes public, so the few internal items the
//! tests need are opened through `apsis_book::testkit`. //! tests need are opened through `apsis_book::testkit`.
//!
//! 它们放在这里而不是模块内部,使 `src/` 不含测试代码,与 `apsis-rs` 一致。
//! 集成测试是独立的 crate,只能触达库公开的部分,因此测试所需的那少数内部条目
//! 经 `apsis_book::testkit` 开放。
use apsis_book::docs::document::*; use apsis_book::docs::document::*;
use apsis_book::testkit::tokenise; use apsis_book::testkit::tokenise;
@ -43,9 +49,11 @@ BSTAR 项。
"; ";
/// The layer the sample documents declare in the tests. /// The layer the sample documents declare in the tests.
/// 测试中的样例文档所声明的层。
const LAYER: &str = "orbit"; const LAYER: &str = "orbit";
/// Parses the English sample under the given identifier. /// Parses the English sample under the given identifier.
/// 以给定标识解析英文样例。
fn sample(id: &str) -> Document { fn sample(id: &str) -> Document {
Document::parse(id, LAYER, Language::English, SAMPLE).expect("the sample must parse") 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"); let document = sample("sgp4");
// The sample body opens with `# SGP4`, which differs from the // The sample body opens with `# SGP4`, which differs from the
// metadata title `SGP4 propagation`, so it is kept. // metadata title `SGP4 propagation`, so it is kept.
// 样例正文以 `# SGP4` 开头,与元数据标题 `SGP4 propagation` 不同,
// 因此它被保留。
assert!(document.english.html.contains("<h1")); assert!(document.english.html.contains("<h1"));
assert!( assert!(
document 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. // 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 repeated = "---\ntitle: SGP4 propagation\n---\n\n# SGP4 propagation\n\nBody.\n";
let variant = DocumentVariant::parse(repeated).expect("must parse"); let variant = DocumentVariant::parse(repeated).expect("must parse");
assert!(!variant.html.contains("<h1")); assert!(!variant.html.contains("<h1"));
@ -95,18 +107,21 @@ fn a_translation_replaces_the_language_form() {
let mut document = sample("sgp4"); let mut document = sample("sgp4");
assert!(!document.has(Language::Chinese)); assert!(!document.has(Language::Chinese));
// Before the translation the Chinese reader sees the English form. // Before the translation the Chinese reader sees the English form.
// 译文之前,中文读者看到的是英文形式。
assert_eq!(document.title(Language::Chinese), "SGP4 propagation"); assert_eq!(document.title(Language::Chinese), "SGP4 propagation");
document.chinese = Some(DocumentVariant::parse(SAMPLE_ZH).expect("must parse")); document.chinese = Some(DocumentVariant::parse(SAMPLE_ZH).expect("must parse"));
assert!(document.has(Language::Chinese)); assert!(document.has(Language::Chinese));
assert_eq!(document.title(Language::Chinese), "SGP4 传播"); assert_eq!(document.title(Language::Chinese), "SGP4 传播");
assert_eq!(document.title(Language::English), "SGP4 propagation"); assert_eq!(document.title(Language::English), "SGP4 propagation");
// The references of either form are merged. // The references of either form are merged.
// 两种形式的引用被合并。
assert_eq!(document.references(), vec!["tle"]); assert_eq!(document.references(), vec!["tle"]);
} }
#[test] #[test]
fn rejects_a_document_without_a_title() { fn rejects_a_document_without_a_title() {
// Without a title the document cannot be listed. // Without a title the document cannot be listed.
// 没有标题的文档无法被列出。
assert!(Document::parse("x", LAYER, Language::English, "---\nsummary: s\n---\nbody").is_none()); assert!(Document::parse("x", LAYER, Language::English, "---\nsummary: s\n---\nbody").is_none());
assert!(DocumentVariant::parse("---\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); assert_eq!(hits[0].language, Language::Chinese);
// The English body contains "propagates", which the Chinese body // The English body contains "propagates", which the Chinese body
// does not. // does not.
// 英文正文含有 "propagates",中文正文没有。
let hits = index.search("propagates", 5); let hits = index.search("propagates", 5);
assert_eq!(hits.len(), 1); assert_eq!(hits.len(), 1);
assert_eq!(hits[0].language, Language::English); assert_eq!(hits[0].language, Language::English);
@ -163,8 +180,11 @@ fn tokenise_emits_words_and_han_pairs() {
assert_eq!(tokenise("坐标"), vec!["坐标"]); assert_eq!(tokenise("坐标"), vec!["坐标"]);
// A lone Chinese character produces no token; the search falls back // A lone Chinese character produces no token; the search falls back
// to a substring match for it. // to a substring match for it.
// 单个中文字符不产生词元;搜索对它回退到子串匹配。
assert!(tokenise("轨").is_empty()); assert!(tokenise("轨").is_empty());
// A single Latin character is dropped. // A single Latin character is dropped.
// 单个拉丁字符被丢弃。
assert!(tokenise("a").is_empty()); 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 tree = Tree::new(documents);
let index = tree.search_index(); let index = tree.search_index();
// A two character query matches the pair inside the sentence. // A two character query matches the pair inside the sentence.
// 两字查询匹配句子内部的相邻字符对。
let hits = index.search("坐标", 5); let hits = index.search("坐标", 5);
assert_eq!(hits.len(), 1); assert_eq!(hits.len(), 1);
assert_eq!(hits[0].id, "a"); assert_eq!(hits[0].id, "a");
// A one character query has no token and falls back to a substring // A one character query has no token and falls back to a substring
// match. // match.
// 单字查询没有词元,回退到子串匹配。
let hits = index.search("轨", 5); let hits = index.search("轨", 5);
assert_eq!(hits.len(), 1); assert_eq!(hits.len(), 1);
assert_eq!(hits[0].id, "a"); 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 // 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 // page would render it twice under one key and stop rendering at
// all, so no identifier may appear twice in the reading order. // all, so no identifier may appear twice in the reading order.
// 物理层声明了两个子区,因此它的文档住在其中。若某篇文档同时留在层自己的
// 列表里,页面会在同一个键下渲染它两次而完全停止渲染,因此阅读顺序中任何
// 标识都不得出现两次。
let documents = vec![ let documents = vec![
Document::parse( Document::parse(
"time-scales", "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. // Both documents are still reachable, each under exactly one layer.
// 两篇文档仍然可达,各自恰好属于一个层。
let physics = tree let physics = tree
.groups() .groups()
.iter() .iter()

View File

@ -1,10 +1,16 @@
//! Tests for the Markdown renderer. //! Tests for the Markdown renderer.
//! Markdown 渲染器的测试。
//! //!
//! They live here rather than inside the module so that `src/` holds no test //! 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 //! 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 //! reaches only what the library makes public, so the few internal items the
//! tests need are opened through `apsis_book::testkit`. //! tests need are opened through `apsis_book::testkit`.
//!
//! 它们放在这里而不是模块内部,使 `src/` 不含测试代码,与 `apsis-rs` 一致。
//! 集成测试是独立的 crate,只能触达库公开的部分,因此测试所需的那少数内部条目
//! 经 `apsis_book::testkit` 开放。
use apsis_book::docs::markdown::*; use apsis_book::docs::markdown::*;
#[test] #[test]
@ -25,6 +31,7 @@ fn a_display_formula_is_rendered_on_its_own() {
#[test] #[test]
fn a_dollar_without_a_pair_is_left_alone() { fn a_dollar_without_a_pair_is_left_alone() {
// A price must survive as a price. // A price must survive as a price.
// 价格必须作为价格保留下来。
let html = render("It costs $5 today."); let html = render("It costs $5 today.");
assert!(html.contains("$5"), "the lone dollar was consumed"); assert!(html.contains("$5"), "the lone dollar was consumed");
assert!(!html.contains("<math"), "no formula should be produced"); 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() { fn two_prices_are_not_a_formula() {
// The closing dollar has a space before it, which is what separates // The closing dollar has a space before it, which is what separates
// this from a formula. // this from a formula.
// 收尾的美元符号前有一个空格,这正是它与公式的区别所在。
let html = render("It costs $5 and then $6."); let html = render("It costs $5 and then $6.");
assert!( assert!(
!html.contains("<math"), !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("data-api=\"Epoch\""), "{html}");
assert!(html.contains("href=\"#/api/Epoch\""), "{html}"); assert!(html.contains("href=\"#/api/Epoch\""), "{html}");
// A web address is left as it is. // A web address is left as it is.
// 网址按原样保留。
assert!(html.contains("href=\"https://example.com\""), "{html}"); 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}"); assert!(html.contains("data-api=\"PropagationErrorKind\""), "{html}");
// A path whose segments are all unknown is left as written. // A path whose segments are all unknown is left as written.
// 所有片段都未知的路径按原样保留。
let html = render_with("[`Nope`](crate::Unknown::Missing).\n", Some(&known)); let html = render_with("[`Nope`](crate::Unknown::Missing).\n", Some(&known));
assert!(!html.contains("data-api"), "{html}"); assert!(!html.contains("data-api"), "{html}");
} }
/// The names of two items, for the shortcut link test. /// The names of two items, for the shortcut link test.
/// 两个条目的名字,供简写链接的测试使用。
fn items() -> std::collections::HashSet<String> { fn items() -> std::collections::HashSet<String> {
["mjd", "Epoch"] ["mjd", "Epoch"]
.iter() .iter()
@ -131,10 +143,12 @@ fn a_shortcut_link_points_at_the_interface_page() {
assert!(html.contains("href=\"#/api/mjd\""), "{html}"); assert!(html.contains("href=\"#/api/mjd\""), "{html}");
assert!(html.contains("data-api=\"Epoch\""), "{html}"); assert!(html.contains("data-api=\"Epoch\""), "{html}");
// A label that is not an item name stays as a code span. // 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)); let html = render_with("The range is [`0, 1`].\n", Some(&known));
assert!(!html.contains("data-api"), "{html}"); assert!(!html.contains("data-api"), "{html}");
assert!(html.contains("<code>0, 1</code>"), "{html}"); assert!(html.contains("<code>0, 1</code>"), "{html}");
// A name that is not in the catalog also stays as a code span. // A name that is not in the catalog also stays as a code span.
// 不在目录中的名字同样保持为代码片段。
let html = render_with("See [`NotAnItem`].\n", Some(&known)); let html = render_with("See [`NotAnItem`].\n", Some(&known));
assert!(!html.contains("data-api"), "{html}"); 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 // Only ASCII letters were kept, so every Chinese heading reduced to
// the fallback and the whole table of contents pointed at the first // the fallback and the whole table of contents pointed at the first
// section. // section.
// 只保留了 ASCII 字母,于是每个中文标题都归约到同一个回退值,整份目录都指向
// 第一节。
let html = render("# 时间尺度\n\n正文。\n\n# 单位约定\n\n更多。\n"); let html = render("# 时间尺度\n\n正文。\n\n# 单位约定\n\n更多。\n");
assert!(html.contains("id=\"时间尺度\""), "{html}"); assert!(html.contains("id=\"时间尺度\""), "{html}");
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() { fn plain_text_resolves_a_shortcut_reference() {
// A rustdoc shortcut keeps the item name and loses the brackets, // A rustdoc shortcut keeps the item name and loses the brackets,
// which otherwise appear verbatim in a one line summary. // which otherwise appear verbatim in a one line summary.
// rustdoc 简写保留条目名并去掉方括号,否则方括号会原样出现在一行摘要里。
let text = to_plain_text("See [`Vec3`] and [`Epoch::mjd`](crate::Epoch::mjd).\n"); let text = to_plain_text("See [`Vec3`] and [`Epoch::mjd`](crate::Epoch::mjd).\n");
assert!(text.contains("Vec3")); assert!(text.contains("Vec3"));
assert!(text.contains("mjd")); assert!(text.contains("mjd"));

View File

@ -1,10 +1,16 @@
//! Tests for the formula renderer. //! Tests for the formula renderer.
//! 公式渲染器的测试。
//! //!
//! They live here rather than inside the module so that `src/` holds no test //! 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 //! 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 //! reaches only what the library makes public, so the few internal items the
//! tests need are opened through `apsis_book::testkit`. //! tests need are opened through `apsis_book::testkit`.
//!
//! 它们放在这里而不是模块内部,使 `src/` 不含测试代码,与 `apsis-rs` 一致。
//! 集成测试是独立的 crate,只能触达库公开的部分,因此测试所需的那少数内部条目
//! 经 `apsis_book::testkit` 开放。
use apsis_book::docs::mathml::*; use apsis_book::docs::mathml::*;
use apsis_book::testkit::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. // 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 // Text is given an element even when it is not understood, because text
// without one is not drawn at all. // without one is not drawn at all.
// 丢掉公式的一部分比把它朴素地显示出来更糟。即使用户未被理解,文本仍会被赋予
// 元素,因为没有元素的文本根本不会被绘制。
assert_eq!(mathml("\\notacommand"), "<mtext>\\notacommand</mtext>"); 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 // 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 // symbols together. It is written as an operator so that it keeps its
// space, which is what a comma is for. // space, which is what a comma is for.
// 该命令是空格类命令,丢掉它会让一串符号挤在一起。它被写成运算符,从而保住
// 自己的空格——逗号正是为此而设。
assert_eq!( assert_eq!(
mathml("\\alpha, \\beta"), mathml("\\alpha, \\beta"),
"<mrow><mi>α</mi><mo>,</mo><mspace width=\"0.25em\"/><mi>β</mi></mrow>" "<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] #[test]
fn the_source_is_kept_in_the_output() { fn the_source_is_kept_in_the_output() {
// The page keeps the source so the formula can be copied back. // The page keeps the source so the formula can be copied back.
// 页面保留源码,使公式能被复制回去。
assert!(render("x^2", false).contains("data-tex=\"x^2\"")); 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 // The bracket branches returned before the scripts were attached, so the
// caret was set as an ordinary symbol and the exponent sat beside the // caret was set as an ordinary symbol and the exponent sat beside the
// group: `(a + b)^2` showed its own caret. // group: `(a + b)^2` showed its own caret.
// 括号分支在挂接上下标之前就返回了,于是脱字符被当作普通符号排版,指数则
// 落在括号组旁边:`(a + b)^2` 把自己的脱字符显示了出来。
for latex in [ for latex in [
r"(a + b)^2", r"(a + b)^2",
r"\left( \frac{x_{ke}}{n_0} \right)^{2/3}", 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. /// The MathML of a formula written in LaTeX.
/// 以 LaTeX 书写的公式的 MathML。
fn render_plain_of(latex: &str) -> String { fn render_plain_of(latex: &str) -> String {
apsis_book::docs::mathml::render(latex, false) apsis_book::docs::mathml::render(latex, false)
.split("display=\"inline\">") .split("display=\"inline\">")

View File

@ -1,10 +1,16 @@
//! Tests for access to the embedded frontend assets. //! Tests for access to the embedded frontend assets.
//! 对内嵌前端资源的访问的测试。
//! //!
//! They live here rather than inside the module so that `src/` holds no test //! 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 //! 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 //! reaches only what the library makes public, so the few internal items the
//! tests need are opened through `apsis_book::testkit`. //! tests need are opened through `apsis_book::testkit`.
//!
//! 它们放在这里而不是模块内部,使 `src/` 不含测试代码,与 `apsis-rs` 一致。
//! 集成测试是独立的 crate,只能触达库公开的部分,因此测试所需的那少数内部条目
//! 经 `apsis_book::testkit` 开放。
use apsis_book::embedded::*; use apsis_book::embedded::*;
#[test] #[test]

View File

@ -26,6 +26,26 @@
//! as `\ln`, is drawn as those letters, so stripping every command only //! 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 //! makes this check less strict about a function name and never hides a lost
//! number. //! number.
//!
//! 比对公式显示的与它被书写的内容。
//!
//! 公式在文档中写成纯文本,再翻译为 MathML。翻译可能丢掉一个 token 而公式仍
//! 在:结果看起来仍像公式,却是错的。这里就发生过——`year + 4800` 里的 4800
//! 被写成行内裸文本,而浏览器不绘制它,于是渲染出的页面上所有多位数都不见了,
//! 标记看起来却还算合理。
//!
//! 因此本检查针对字符而不是结构:源码中的每一个字母与数字都必须出现在页面会
//! 显示的文本里。它遍历每一篇文档而不是抽取样本,因为该缺陷是逐个公式的,样本
//! 会漏掉大部分。
//!
//! 只比对字母与数字。公式源码中的其他字符是记法而非内容:`^` 与 `_` 说明升降,
//! `*` 表示相乘,`/` 表示相除,花括号用于分组。它们都应当从页面上消失,因为
//! 排版已经表达了它们的意思。而数字或名字没有这样的借口,少一个就是缺陷。
//!
//! LaTeX 命令是同一类记法。`\sqrt` 被画成根号,`\Delta` 被画成希腊字母,
//! 因此它们的字母本就消失,不算缺失。名字即其字形的命令,例如 `\ln`,就按那些
//! 字母绘制,所以剥掉所有命令只会让本检查对函数名更宽松,而绝不会掩盖一个丢失
//! 的数字。
use apsis_book::docs::document::Language; 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 /// version counted any text inside any element, which made the check below
/// pass whatever the renderer produced: text with no element was counted as /// pass whatever the renderer produced: text with no element was counted as
/// drawn although the browser shows nothing. /// drawn although the browser shows nothing.
///
/// 返回读者看到的文本。
///
/// 浏览器绘制叶子元素内的文本,并丢弃没有元素包裹的文本,因此这里只按同样的
/// 规则统计。早先的版本统计任何元素内部的文本,使下面的检查无论在渲染器产出
/// 什么时都通过:没有元素的文本被算作已绘制,而浏览器什么也不显示。
fn visible(html: &str) -> String { fn visible(html: &str) -> String {
/// The elements whose text is drawn. /// The elements whose text is drawn.
/// 其文本会被绘制的元素。
const LEAF: [&str; 6] = ["mi", "mo", "mn", "mtext", "mspace", "ms"]; const LEAF: [&str; 6] = ["mi", "mo", "mn", "mtext", "mspace", "ms"];
let mut out = String::new(); let mut out = String::new();
// The elements currently open, innermost last. // The elements currently open, innermost last.
// 当前打开的元素,最内层在最后。
let mut open: Vec<String> = Vec::new(); let mut open: Vec<String> = Vec::new();
let mut characters = html.chars().peekable(); let mut characters = html.chars().peekable();
let mut in_tag = false; let mut in_tag = false;
@ -70,6 +98,7 @@ fn visible(html: &str) -> String {
_ if in_tag => tag.push(character), _ if in_tag => tag.push(character),
_ => { _ => {
// Drawn only when the innermost open element holds text. // Drawn only when the innermost open element holds text.
// 只有当最内层打开的元素持有文本时才绘制。
if open if open
.last() .last()
.is_some_and(|name| LEAF.contains(&name.as_str())) .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 /// 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 /// characters, so none of them is expected on the page. What remains is the
/// letters and digits, which are content and must all appear. /// letters and digits, which are content and must all appear.
///
/// 返回公式源码中的内容字符。
///
/// 被剔除的是记法:命令名、环境名、分隔矩阵单元格的 `&` 与分隔各行的 `\\`,
/// 以及用于分组的花括号。它们都不以这些字符的形式被绘制,因此都不应出现在
/// 页面上。剩下的是字母与数字,它们是内容,必须全部出现。
fn content_characters(source: &str) -> String { fn content_characters(source: &str) -> String {
// The attribute is HTML escaped, so a cell separator arrives as `&amp;` // The attribute is HTML escaped, so a cell separator arrives as `&amp;`
// and would otherwise leave the letters of `amp` behind. // and would otherwise leave the letters of `amp` behind.
// `sqrt` is written bare in the plain notation and with a backslash in // `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 // LaTeX; the bare form has to go as well, because it is drawn as a
// radical sign rather than as its letters. // radical sign rather than as its letters.
// 该属性经过 HTML 转义,因此单元格分隔符到手时是 `&amp;`,否则会留下
// `amp` 这几个字母。`sqrt` 在朴素记法中写成裸词、在 LaTeX 中带反斜杠;
// 裸词形式同样要去掉,因为它被绘制成根号而不是那些字母。
let unescaped = source let unescaped = source
.replace("sqrt", "") .replace("sqrt", "")
.replace("&amp;", "&") .replace("&amp;", "&")
@ -111,6 +150,10 @@ fn content_characters(source: &str) -> String {
// The name is captured so that `begin` and `end` can be told // The name is captured so that `begin` and `end` can be told
// apart from the rest, because those two carry the name of an // apart from the rest, because those two carry the name of an
// environment in braces after them. // environment in braces after them.
// 命令一直延伸到第一个非字母字符。名字被捕获下来,是为了把
// `begin` 与 `end` 同其余命令区分开——只有这两个会在其后用花括号
// 携带环境名。
'\\' => { '\\' => {
let mut name = String::new(); let mut name = String::new();
while let Some(next) = characters.peek().copied() { 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. /// Returns the formula sources of one document, in order.
/// 按顺序返回一篇文档中的公式源码。
fn formula_blocks(html: &str) -> Vec<String> { fn formula_blocks(html: &str) -> Vec<String> {
let mut found = Vec::new(); let mut found = Vec::new();
let mut rest = html; let mut rest = html;
@ -156,6 +200,7 @@ fn every_formula_shows_every_character_it_was_written_with() {
let mut failures = Vec::new(); let mut failures = Vec::new();
// The concept documents, in both languages. // The concept documents, in both languages.
// 概念文档,含两种语言。
let mut sources: Vec<(String, String)> = Vec::new(); let mut sources: Vec<(String, String)> = Vec::new();
for document in &apsis_book::docs::load_documents() { for document in &apsis_book::docs::load_documents() {
for language in [Language::English, Language::Chinese] { 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 // And the interface documentation, which carries formulas of its own in
// the doc comments of the crate it describes. Leaving it out would have // the doc comments of the crate it describes. Leaving it out would have
// hidden every formula there. // hidden every formula there.
// 以及接口文档,它在其所描述的 crate 的文档注释中自带公式。把它排除在外会
// 让那里的每一个公式都无人检查。
for krate in apsis_book::docs::load_api_catalog().crates { for krate in apsis_book::docs::load_api_catalog().crates {
for item in &krate.items { for item in &krate.items {
// The doc comment is Markdown, and the service renders it with // The doc comment is Markdown, and the service renders it with
// the same renderer as a document. // the same renderer as a document.
// 文档注释是 Markdown,服务用与文档相同的渲染器渲染它。
for text in [Some(&item.doc), item.doc_zh.as_ref()] for text in [Some(&item.doc), item.doc_zh.as_ref()]
.into_iter() .into_iter()
.flatten() .flatten()
@ -188,11 +238,17 @@ fn every_formula_shows_every_character_it_was_written_with() {
checked += 1; checked += 1;
// The source as written, without the spaces that only set it // The source as written, without the spaces that only set it
// out on the page. // out on the page.
// 源码按书写时的样子,去掉那些仅为页面排版而设的空格。
let written = content_characters(&source); let written = content_characters(&source);
// The part of the page that is the formula itself: the // The part of the page that is the formula itself: the
// MathML and nothing else. The source is carried beside it // MathML and nothing else. The source is carried beside it
// for a reader to copy, and including it here would compare // for a reader to copy, and including it here would compare
// the source against itself and pass whatever happened. // the source against itself and pass whatever happened.
// 页面上属于公式本身的部分:MathML,别无其他。源码被携带在其旁
// 供读者复制,若把它算进来,就成了源码与自身比对,无论发生什么
// 都会通过。
let from = html.find(&format!("data-tex=\"{source}\"")).unwrap_or(0); 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 start = html[from..].find("<math").map(|e| from + e).unwrap_or(from);
let end = html[start..] 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 // raise or lower, and neither is drawn. One that reaches the
// output is a script that was never attached, which leaves a // output is a script that was never attached, which leaves a
// formula that has every character in it and is still wrong. // formula that has every character in it and is still wrong.
// 脱字符与下划线是记法:它们说明升降,二者都不被绘制。出现在
// 输出里的那一个,说明有上下标从未被挂接,这会留下一个每个字符
// 都在、却依然是错的公式。
let body = &html[start..end]; let body = &html[start..end];
for marker in ["<mo>^</mo>", "<mo>_</mo>"] { for marker in ["<mo>^</mo>", "<mo>_</mo>"] {
assert!( assert!(

View File

@ -1,10 +1,16 @@
//! Tests for the graph model and the crate structure. //! Tests for the graph model and the crate structure.
//! 图模型与 crate 结构的测试。
//! //!
//! They live here rather than inside the module so that `src/` holds no test //! 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 //! 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 //! reaches only what the library makes public, so the few internal items the
//! tests need are opened through `apsis_book::testkit`. //! tests need are opened through `apsis_book::testkit`.
//!
//! 它们放在这里而不是模块内部,使 `src/` 不含测试代码,与 `apsis-rs` 一致。
//! 集成测试是独立的 crate,只能触达库公开的部分,因此测试所需的那少数内部条目
//! 经 `apsis_book::testkit` 开放。
use apsis_book::graph::*; use apsis_book::graph::*;
const SAMPLE: &str = r#"{ const SAMPLE: &str = r#"{

View File

@ -1,10 +1,16 @@
//! Tests for the request parser. //! Tests for the request parser.
//! 请求解析器的测试。
//! //!
//! They live here rather than inside the module so that `src/` holds no test //! 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 //! 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 //! reaches only what the library makes public, so the few internal items the
//! tests need are opened through `apsis_book::testkit`. //! tests need are opened through `apsis_book::testkit`.
//!
//! 它们放在这里而不是模块内部,使 `src/` 不含测试代码,与 `apsis-rs` 一致。
//! 集成测试是独立的 crate,只能触达库公开的部分,因此测试所需的那少数内部条目
//! 经 `apsis_book::testkit` 开放。
use apsis_book::http::request::*; use apsis_book::http::request::*;
use std::io::Cursor; use std::io::Cursor;

View File

@ -1,10 +1,16 @@
//! Tests for the response writer. //! Tests for the response writer.
//! 响应写出器的测试。
//! //!
//! They live here rather than inside the module so that `src/` holds no test //! 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 //! 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 //! reaches only what the library makes public, so the few internal items the
//! tests need are opened through `apsis_book::testkit`. //! tests need are opened through `apsis_book::testkit`.
//!
//! 它们放在这里而不是模块内部,使 `src/` 不含测试代码,与 `apsis-rs` 一致。
//! 集成测试是独立的 crate,只能触达库公开的部分,因此测试所需的那少数内部条目
//! 经 `apsis_book::testkit` 开放。
use apsis_book::http::response::*; use apsis_book::http::response::*;
#[test] #[test]
@ -33,6 +39,9 @@ fn empty_body_has_no_content_type() {
let text = String::from_utf8(response.to_bytes()).unwrap(); let text = String::from_utf8(response.to_bytes()).unwrap();
// The response carries `X-Content-Type-Options`, so the assertion is // The response carries `X-Content-Type-Options`, so the assertion is
// on the leading header line rather than on the substring. // on the leading header line rather than on the substring.
// 响应带有 `X-Content-Type-Options`,因此断言落在起始的那一行首部上,
// 而不是落在子串上。
assert!(!text.contains("\r\nContent-Type:")); assert!(!text.contains("\r\nContent-Type:"));
assert!(text.contains("Content-Length: 0\r\n")); assert!(text.contains("Content-Length: 0\r\n"));
} }

View File

@ -1,9 +1,15 @@
//! Tests for the listener and the worker pool. //! Tests for the listener and the worker pool.
//! 监听器与工作线程池的测试。
//! //!
//! They live here rather than inside the module so that `src/` holds no test //! 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 //! 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 //! reaches only what the library makes public, so the few internal items the
//! tests need are opened through `apsis_book::testkit`. //! 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::request::{Request, RequestError};
use apsis_book::http::response::{Response, Status}; use apsis_book::http::response::{Response, Status};
use std::net::SocketAddr; use std::net::SocketAddr;
@ -45,6 +51,7 @@ fn serves_a_request_over_a_real_socket() {
drop(stream); drop(stream);
// The accept loop ends when the listener is dropped with the thread. // The accept loop ends when the listener is dropped with the thread.
// 接受循环在线程随监听器一同被丢弃时结束。
let _ = running; let _ = running;
} }

View File

@ -1,10 +1,16 @@
//! Tests for the bilingual strings. //! Tests for the bilingual strings.
//! 双语字符串的测试。
//! //!
//! They live here rather than inside the module so that `src/` holds no test //! 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 //! 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 //! reaches only what the library makes public, so the few internal items the
//! tests need are opened through `apsis_book::testkit`. //! tests need are opened through `apsis_book::testkit`.
//!
//! 它们放在这里而不是模块内部,使 `src/` 不含测试代码,与 `apsis-rs` 一致。
//! 集成测试是独立的 crate,只能触达库公开的部分,因此测试所需的那少数内部条目
//! 经 `apsis_book::testkit` 开放。
use apsis_book::i18n::*; use apsis_book::i18n::*;
use apsis_book::json; use apsis_book::json;

View File

@ -1,9 +1,15 @@
//! Tests for the JSON codec. //! Tests for the JSON codec.
//! JSON 编解码器的测试。
//! //!
//! They live here rather than inside the module so that `src/` holds no test //! 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 //! 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 //! reaches only what the library makes public, so the few internal items the
//! tests need are opened through `apsis_book::testkit`. //! tests need are opened through `apsis_book::testkit`.
//!
//! 它们放在这里而不是模块内部,使 `src/` 不含测试代码,与 `apsis-rs` 一致。
//! 集成测试是独立的 crate,只能触达库公开的部分,因此测试所需的那少数内部条目
//! 经 `apsis_book::testkit` 开放。
use apsis_book::testkit::json_object; use apsis_book::testkit::json_object;
use apsis_book::json::*; use apsis_book::json::*;

View File

@ -8,6 +8,15 @@
//! The binary is located through the `CARGO_BIN_EXE_apsis-book` variable that //! 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 //! cargo sets for an integration test, so the test always runs against the
//! binary built from the same sources. //! binary built from the same sources.
//!
//! 针对服务二进制的端到端测试。
//!
//! 测试在一个临时端口上启动真实的二进制,等待它报告自己的地址,然后以普通 TCP
//! 请求与之通信。这样把请求解析器、路由器、JSON 编解码器、内嵌内容与工具处理
//! 函数放在一起检验,这是单元测试做不到的。
//!
//! 二进制的位置取自 cargo 为集成测试设置的 `CARGO_BIN_EXE_apsis-book`,因此测试
//! 总是针对由同一份源码构建出的二进制运行。
use std::io::{BufRead, BufReader, Read, Write}; use std::io::{BufRead, BufReader, Read, Write};
use std::net::TcpStream; use std::net::TcpStream;
@ -17,6 +26,7 @@ use std::thread;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
/// A running service that is stopped when it is dropped. /// A running service that is stopped when it is dropped.
/// 一个运行中的服务,被丢弃时即停止。
struct Service { struct Service {
child: Child, child: Child,
address: String, address: String,
@ -24,6 +34,7 @@ struct Service {
impl Service { impl Service {
/// Starts the binary and waits for it to report its address. /// Starts the binary and waits for it to report its address.
/// 启动二进制并等待它报告自己的地址。
fn start() -> Self { fn start() -> Self {
let mut child = Command::new(env!("CARGO_BIN_EXE_apsis-book")) let mut child = Command::new(env!("CARGO_BIN_EXE_apsis-book"))
.args(["--address", "127.0.0.1:0", "--workers", "2"]) .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. /// Sends a raw request and returns the whole response as text.
/// 发送一个原始请求,并把整个响应作为文本返回。
fn request(&self, request: &str) -> String { fn request(&self, request: &str) -> String {
let mut stream = let mut stream =
TcpStream::connect(&self.address).expect("the service must accept a connection"); TcpStream::connect(&self.address).expect("the service must accept a connection");
@ -68,11 +80,13 @@ impl Service {
} }
/// Sends a GET request. /// Sends a GET request.
/// 发送一个 GET 请求。
fn get(&self, path: &str) -> String { fn get(&self, path: &str) -> String {
self.request(&format!("GET {path} HTTP/1.1\r\nHost: localhost\r\n\r\n")) self.request(&format!("GET {path} HTTP/1.1\r\nHost: localhost\r\n\r\n"))
} }
/// Sends a POST request with a JSON body. /// Sends a POST request with a JSON body.
/// 发送一个带 JSON 请求体的 POST 请求。
fn post(&self, path: &str, body: &str) -> String { fn post(&self, path: &str, body: &str) -> String {
self.request(&format!( self.request(&format!(
"POST {path} HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{body}", "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. /// Returns the body of a response.
/// 返回某个响应的请求体。
fn body(response: &str) -> &str { fn body(response: &str) -> &str {
response response
.split_once("\r\n\r\n") .split_once("\r\n\r\n")
@ -97,6 +112,7 @@ fn body(response: &str) -> &str {
} }
/// Returns the status line of a response. /// Returns the status line of a response.
/// 返回某个响应的状态行。
fn status(response: &str) -> &str { fn status(response: &str) -> &str {
response.lines().next().unwrap_or("") 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 /// The accept loop starts a moment after the address is printed, so the
/// first connection can be refused. /// first connection can be refused.
///
/// 等待某个条件,轮询直到它成立或超过时限。
///
/// 接受循环在地址打印之后片刻才启动,因此第一次连接可能被拒绝。
fn wait_ready(service: &Service) { fn wait_ready(service: &Service) {
let deadline = Instant::now() + Duration::from_secs(10); let deadline = Instant::now() + Duration::from_secs(10);
while Instant::now() < deadline { while Instant::now() < deadline {
@ -152,6 +172,7 @@ fn serves_the_document_index_and_a_document() {
let listing = body(&index); let listing = body(&index);
assert!(listing.contains("\"groups\""), "{listing}"); assert!(listing.contains("\"groups\""), "{listing}");
// The concepts and the implementation are separate groups. // The concepts and the implementation are separate groups.
// 概念与实现是两个分开的分组。
assert!(listing.contains("\"concepts\""), "{listing}"); assert!(listing.contains("\"concepts\""), "{listing}");
assert!(listing.contains("\"implementation\""), "{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 item = service.get("/api/api/items/propagate_minutes");
let body = body(&item); let body = body(&item);
// The Chinese documentation is served and is rendered to HTML. // The Chinese documentation is served and is rendered to HTML.
// 中文文档会被提供,并渲染为 HTML。
assert!(body.contains("传播"), "the Chinese doc should be served"); assert!(body.contains("传播"), "the Chinese doc should be served");
assert!(body.contains("<p>"), "the doc should be rendered as HTML"); assert!(body.contains("<p>"), "the doc should be rendered as HTML");
// A doc comment link to another item becomes an internal route. // A doc comment link to another item becomes an internal route.
// 指向另一个条目的文档注释链接会成为一个内部路由。
assert!( assert!(
body.contains("data-api=") && body.contains("#/api/"), body.contains("data-api=") && body.contains("#/api/"),
"the doc link should be resolved: {body}" "the doc link should be resolved: {body}"
); );
// The English documentation is still present, so the language can switch // The English documentation is still present, so the language can switch
// without another request. // without another request.
// 英文文档仍然存在,因此切换语言无需再一次请求。
assert!( assert!(
body.contains("Returns the state of the satellite"), body.contains("Returns the state of the satellite"),
"the English doc should still be present" "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 response = service.get("/api/api/crates/apsis-sgp4");
let body = body(&response); let body = body(&response);
// The Chinese summary and its plain form are both served. // The Chinese summary and its plain form are both served.
// 中文概要与它的纯文本形式都会被提供。
assert!( assert!(
body.contains("传播器"), body.contains("传播器"),
"the Chinese summary should be served" "the Chinese summary should be served"
@ -301,6 +327,7 @@ fn rejects_a_malformed_body_without_failing() {
let body = body(&response); let body = body(&response);
assert!(body.contains("\"ok\":false"), "{body}"); assert!(body.contains("\"ok\":false"), "{body}");
// The service must still answer a subsequent request. // The service must still answer a subsequent request.
// 服务必须仍能回应后续请求。
assert!(status(&service.get("/health")).contains("200")); 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\":\"zh\""), "{body}");
assert!(body.contains("\"language\":\"en\""), "{body}"); assert!(body.contains("\"language\":\"en\""), "{body}");
// The Chinese form of the document carries Chinese text. // The Chinese form of the document carries Chinese text.
// 文档的中文形式携带中文文本。
assert!(body.contains("传播"), "the Chinese form should be embedded"); 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}"); assert!(body.contains("tle-parsing"), "{body}");
// The index carries a bilingual title, so the frontend needs no copy of // The index carries a bilingual title, so the frontend needs no copy of
// the flow names. // the flow names.
// 索引携带双语标题,因此前端无须另存一份流程图名。
assert!(body.contains("\"title\""), "{body}"); assert!(body.contains("\"title\""), "{body}");
assert!(body.contains("\"zh\""), "{body}"); assert!(body.contains("\"zh\""), "{body}");
} }
@ -382,6 +412,8 @@ fn static_assets_are_revalidated() {
wait_ready(&service); wait_ready(&service);
// The build writes stable file names, so an immutable policy would pin a // The build writes stable file names, so an immutable policy would pin a
// reader to the bundle current at first load. // reader to the bundle current at first load.
// 构建写出的是稳定的文件名,因此不可变策略会把读者钉在首次加载时的那个包上。
let asset = service.get("/assets/app.js"); let asset = service.get("/assets/app.js");
assert!(asset.contains("no-cache"), "{asset}"); assert!(asset.contains("no-cache"), "{asset}");
assert!(!asset.contains("immutable"), "{asset}"); assert!(!asset.contains("immutable"), "{asset}");

View File

@ -1,10 +1,16 @@
//! Tests for the parsed content shared by the workers. //! Tests for the parsed content shared by the workers.
//! 各工作线程共享的已解析内容的测试。
//! //!
//! They live here rather than inside the module so that `src/` holds no test //! 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 //! 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 //! reaches only what the library makes public, so the few internal items the
//! tests need are opened through `apsis_book::testkit`. //! tests need are opened through `apsis_book::testkit`.
//!
//! 它们放在这里而不是模块内部,使 `src/` 不含测试代码,与 `apsis-rs` 一致。
//! 集成测试是独立的 crate,只能触达库公开的部分,因此测试所需的那少数内部条目
//! 经 `apsis_book::testkit` 开放。
use apsis_book::state::*; use apsis_book::state::*;
#[test] #[test]

View File

@ -1,9 +1,15 @@
//! Tests for the tool handlers. //! Tests for the tool handlers.
//! 工具处理函数的测试。
//! //!
//! They live here rather than inside the module so that `src/` holds no test //! 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 //! 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 //! reaches only what the library makes public, so the few internal items the
//! tests need are opened through `apsis_book::testkit`. //! tests need are opened through `apsis_book::testkit`.
//!
//! 它们放在这里而不是模块内部,使 `src/` 不含测试代码,与 `apsis-rs` 一致。
//! 集成测试是独立的 crate,只能触达库公开的部分,因此测试所需的那少数内部条目
//! 经 `apsis_book::testkit` 开放。
use apsis_book::testkit::json_object; use apsis_book::testkit::json_object;
use apsis_book::tools::ToolError; use apsis_book::tools::ToolError;
@ -13,6 +19,7 @@ use apsis_book::json::Value;
use apsis_book::tools::{Params, Registry}; use apsis_book::tools::{Params, Registry};
/// Runs a tool with the given parameters. /// Runs a tool with the given parameters.
/// 以给定参数运行一个工具。
fn call(id: &str, values: Value) -> Result<Value, ToolError> { fn call(id: &str, values: Value) -> Result<Value, ToolError> {
let registry = Registry::new(); let registry = Registry::new();
let tool = registry.get(id).unwrap(); 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. /// Returns the English text of a label, which is a bilingual object.
/// 返回某个标签的英文文本,该标签是一个双语对象。
fn label_english(value: &Value) -> &str { fn label_english(value: &Value) -> &str {
value.get("en").and_then(Value::as_str).unwrap_or("") value.get("en").and_then(Value::as_str).unwrap_or("")
} }
/// Returns the numeric value of the scalar with an English label. /// Returns the numeric value of the scalar with an English label.
/// 返回英文标签为给定值的那个标量的数值。
fn scalar_number(value: &Value, label: &str) -> f64 { fn scalar_number(value: &Value, label: &str) -> f64 {
value value
.get("scalars") .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 // An eccentricity of one is a parabola; the conversion requires an
// ellipse, so the tool reports the condition without naming a // ellipse, so the tool reports the condition without naming a
// parameter. // parameter.
// 偏心率为一即抛物线;该转换要求椭圆,因此工具报告该条件而不指名某个参数。
let error = call( let error = call(
"kepler-state", "kepler-state",
json_object! { "eccentricity" => 1.0, "semi_major_axis" => 7000.0 }, json_object! { "eccentricity" => 1.0, "semi_major_axis" => 7000.0 },

View File

@ -1,10 +1,16 @@
//! Tests for the tool result label table. //! Tests for the tool result label table.
//! 工具结果标签表的测试。
//! //!
//! They live here rather than inside the module so that `src/` holds no test //! 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 //! 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 //! reaches only what the library makes public, so the few internal items the
//! tests need are opened through `apsis_book::testkit`. //! tests need are opened through `apsis_book::testkit`.
//!
//! 它们放在这里而不是模块内部,使 `src/` 不含测试代码,与 `apsis-rs` 一致。
//! 集成测试是独立的 crate,只能触达库公开的部分,因此测试所需的那少数内部条目
//! 经 `apsis_book::testkit` 开放。
use apsis_book::tools::labels::*; use apsis_book::tools::labels::*;
#[test] #[test]

View File

@ -1,15 +1,22 @@
//! Tests for the tool result shape. //! Tests for the tool result shape.
//! 工具结果形状的测试。
//! //!
//! They live here rather than inside the module so that `src/` holds no test //! 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 //! 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 //! reaches only what the library makes public, so the few internal items the
//! tests need are opened through `apsis_book::testkit`. //! tests need are opened through `apsis_book::testkit`.
//!
//! 它们放在这里而不是模块内部,使 `src/` 不含测试代码,与 `apsis-rs` 一致。
//! 集成测试是独立的 crate,只能触达库公开的部分,因此测试所需的那少数内部条目
//! 经 `apsis_book::testkit` 开放。
use apsis_book::json::Value; use apsis_book::json::Value;
use apsis_book::tools::labels; use apsis_book::tools::labels;
use apsis_book::tools::out::*; use apsis_book::tools::out::*;
/// The first cell of the first row of a shaped response. /// The first cell of the first row of a shaped response.
/// 某个已整形响应中第一行第一格的单元格。
fn first_cell(value: &Value) -> Value { fn first_cell(value: &Value) -> Value {
let rows = value let rows = value
.get("table") .get("table")
@ -26,6 +33,8 @@ fn first_cell(value: &Value) -> Value {
fn a_text_cell_is_shown_in_either_language() { fn a_text_cell_is_shown_in_either_language() {
// The meaning of a field is a fixed phrase, so it is looked up rather // The meaning of a field is a fixed phrase, so it is looked up rather
// than written twice. // than written twice.
// 字段的含义是固定短语,因此走查表而不是写两遍。
let mut table = Table::new(vec![Column::text("meaning", "Meaning")]); let mut table = Table::new(vec![Column::text("meaning", "Meaning")]);
table.push(vec![Value::from("Mean motion")]); table.push(vec![Value::from("Mean motion")]);
let value = table_response( 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() { fn an_identifier_is_not_set_as_a_formula() {
// `mean_motion` is a field name. Set as a formula it would come out as // `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. // separate letters with a subscript, which is not the field name.
// `mean_motion` 是一个字段名。当成公式排版会变成逐字母加下标,那就不是
// 字段名了。
let mut table = Table::new(vec![Column::text("field", "Field")]); let mut table = Table::new(vec![Column::text("field", "Field")]);
table.push(vec![Value::from("mean_motion")]); table.push(vec![Value::from("mean_motion")]);
let value = table_response("kind", "Title", Vec::new(), &table, Vec::new(), Vec::new()); 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] #[test]
fn a_unit_with_a_power_is_rendered() { 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` 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")]); let mut table = Table::new(vec![Column::text("unit", "Unit")]);
table.push(vec![Value::from("rev/day^2")]); table.push(vec![Value::from("rev/day^2")]);
let value = table_response("kind", "Title", Vec::new(), &table, Vec::new(), Vec::new()); let value = table_response("kind", "Title", Vec::new(), &table, Vec::new(), Vec::new());

View File

@ -1,9 +1,15 @@
//! Tests for the tool registry. //! Tests for the tool registry.
//! 工具注册表的测试。
//! //!
//! They live here rather than inside the module so that `src/` holds no test //! 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 //! 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 //! reaches only what the library makes public, so the few internal items the
//! tests need are opened through `apsis_book::testkit`. //! tests need are opened through `apsis_book::testkit`.
//!
//! 它们放在这里而不是模块内部,使 `src/` 不含测试代码,与 `apsis-rs` 一致。
//! 集成测试是独立的 crate,只能触达库公开的部分,因此测试所需的那少数内部条目
//! 经 `apsis_book::testkit` 开放。
use apsis_book::testkit::json_object; use apsis_book::testkit::json_object;
use apsis_book::tools::*; use apsis_book::tools::*;