diff --git a/src/tools/handlers.rs b/src/tools/handlers.rs index 5176959..9c3e9e7 100644 --- a/src/tools/handlers.rs +++ b/src/tools/handlers.rs @@ -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 { match id { "parse-tle" => parse_tle(params), @@ -43,6 +56,7 @@ pub fn run(id: &str, params: &Params<'_>) -> Result { } /// Parses a two-line element set and reports its fields. +/// 解析两行元素集并报告其各字段。 fn parse_tle(params: &Params<'_>) -> Result { let text = params.text("tle")?; let element = TwoLineElement::parse(&text).map_err(|error| { @@ -156,6 +170,7 @@ fn parse_tle(params: &Params<'_>) -> Result { } /// Propagates one instant with SGP4 and reports the state. +/// 用 SGP4 传播某一时刻并报告状态。 fn sgp4_propagate(params: &Params<'_>) -> Result { let text = params.text("tle")?; let model = gravity_model(params.choice("model")?); @@ -245,6 +260,7 @@ fn sgp4_propagate(params: &Params<'_>) -> Result { } /// Samples a ground track and returns the points. +/// 对地面轨迹采样并返回各点。 fn sgp4_track(params: &Params<'_>) -> Result { let text = params.text("tle")?; let model = gravity_model(params.choice("model")?); @@ -341,6 +357,8 @@ fn sgp4_track(params: &Params<'_>) -> Result { .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 { } /// Compares the mean elements with the osculating elements. +/// 把平均根数与密切根数作比较。 fn orbit_osculating(params: &Params<'_>) -> Result { let text = params.text("tle")?; let model = gravity_model(params.choice("model")?); @@ -480,6 +499,7 @@ fn orbit_osculating(params: &Params<'_>) -> Result { } /// Converts Keplerian elements to a state vector. +/// 把开普勒根数转换为状态矢量。 fn kepler_state(params: &Params<'_>) -> Result { let parameter = gravity_parameter(params.choice("model")?); let elements = KeplerianElements { @@ -594,6 +614,7 @@ fn kepler_state(params: &Params<'_>) -> Result { } /// Converts an instant between time scales. +/// 在时间尺度之间转换一个时刻。 fn time_convert(params: &Params<'_>) -> Result { let scale = time_scale(params.choice("scale")?); let form = params.choice("input")?; @@ -647,6 +668,9 @@ fn time_convert(params: &Params<'_>) -> Result { 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 { } /// Converts a geodetic position to Earth-fixed coordinates. +/// 把大地位置转换为地固坐标。 fn geodetic_ecef(params: &Params<'_>) -> Result { let latitude = out::radians(params.number("latitude")?); let longitude = out::radians(params.number("longitude")?); @@ -780,6 +805,7 @@ fn geodetic_ecef(params: &Params<'_>) -> Result { } /// Rotates a vector between the inertial and Earth-fixed frames. +/// 在惯性系与地固系之间旋转一个矢量。 fn frame_transform(params: &Params<'_>) -> Result { let direction = params.choice("direction")?; let vector = apsis::apsis_math::Vec3::new( @@ -864,6 +890,11 @@ fn frame_transform(params: &Params<'_>) -> Result { /// 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 { let from_radius = params.number("from_radius")?; let to_radius = params.number("to_radius")?; @@ -966,6 +997,7 @@ fn hohmann_transfer(params: &Params<'_>) -> Result { } /// 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; diff --git a/tests/api.rs b/tests/api.rs index 3a67cb6..a475cdd 100644 --- a/tests/api.rs +++ b/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." diff --git a/tests/docs_api.rs b/tests/docs_api.rs index ff44ab0..72e73f0 100644 --- a/tests/docs_api.rs +++ b/tests/docs_api.rs @@ -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 { diff --git a/tests/docs_content.rs b/tests/docs_content.rs index 406a30a..3034458 100644 --- a/tests/docs_content.rs +++ b/tests/docs_content.rs @@ -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] diff --git a/tests/docs_document.rs b/tests/docs_document.rs index e82e088..cc406db 100644 --- a/tests/docs_document.rs +++ b/tests/docs_document.rs @@ -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(" std::collections::HashSet { ["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("0, 1"), "{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")); diff --git a/tests/docs_mathml.rs b/tests/docs_mathml.rs index 49d31be..8fd70d5 100644 --- a/tests/docs_mathml.rs +++ b/tests/docs_mathml.rs @@ -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"), "\\notacommand"); } @@ -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"), "α,β" @@ -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\">") diff --git a/tests/embedded.rs b/tests/embedded.rs index 589991a..afe6184 100644 --- a/tests/embedded.rs +++ b/tests/embedded.rs @@ -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] diff --git a/tests/formula_rendering.rs b/tests/formula_rendering.rs index 39f15cc..69ede45 100644 --- a/tests/formula_rendering.rs +++ b/tests/formula_rendering.rs @@ -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 = 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 { 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("^", "_"] { assert!( diff --git a/tests/graph.rs b/tests/graph.rs index 21dde65..54263ee 100644 --- a/tests/graph.rs +++ b/tests/graph.rs @@ -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#"{ diff --git a/tests/http_request.rs b/tests/http_request.rs index e7a52e3..a0522a9 100644 --- a/tests/http_request.rs +++ b/tests/http_request.rs @@ -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; diff --git a/tests/http_response.rs b/tests/http_response.rs index 46e6974..0ce60a2 100644 --- a/tests/http_response.rs +++ b/tests/http_response.rs @@ -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")); } diff --git a/tests/http_server.rs b/tests/http_server.rs index 82ffd09..b7f5d7d 100644 --- a/tests/http_server.rs +++ b/tests/http_server.rs @@ -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; } diff --git a/tests/i18n.rs b/tests/i18n.rs index 9b31e93..c60bc59 100644 --- a/tests/i18n.rs +++ b/tests/i18n.rs @@ -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; diff --git a/tests/json_codec.rs b/tests/json_codec.rs index 5fd0913..fae5bba 100644 --- a/tests/json_codec.rs +++ b/tests/json_codec.rs @@ -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::*; diff --git a/tests/service.rs b/tests/service.rs index 48b2b3c..dda27c2 100644 --- a/tests/service.rs +++ b/tests/service.rs @@ -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("

"), "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}"); diff --git a/tests/state.rs b/tests/state.rs index 40f1f63..5ad0820 100644 --- a/tests/state.rs +++ b/tests/state.rs @@ -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] diff --git a/tests/tools_handlers.rs b/tests/tools_handlers.rs index d683f65..af71f57 100644 --- a/tests/tools_handlers.rs +++ b/tests/tools_handlers.rs @@ -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 { let registry = Registry::new(); let tool = registry.get(id).unwrap(); @@ -21,11 +28,13 @@ fn call(id: &str, values: Value) -> Result { } /// 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 }, diff --git a/tests/tools_labels.rs b/tests/tools_labels.rs index 1c6feb8..ad1404f 100644 --- a/tests/tools_labels.rs +++ b/tests/tools_labels.rs @@ -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] diff --git a/tests/tools_out.rs b/tests/tools_out.rs index c9834f7..b51b16c 100644 --- a/tests/tools_out.rs +++ b/tests/tools_out.rs @@ -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()); diff --git a/tests/tools_registry.rs b/tests/tools_registry.rs index 15c0ce9..728ebdc 100644 --- a/tests/tools_registry.rs +++ b/tests/tools_registry.rs @@ -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::*;