From 4e89412cde7facd5a0e9cb4f9dd673b7061e40a5 Mon Sep 17 00:00:00 2001 From: xfy Date: Mon, 15 Jun 2026 10:57:51 +0800 Subject: [PATCH 1/7] =?UTF-8?q?test(theme):=20=E6=89=A9=E5=85=85=E4=B8=BB?= =?UTF-8?q?=E9=A2=98=E6=A8=A1=E5=9D=97=E6=B5=8B=E8=AF=95=E8=A6=86=E7=9B=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原仅 2 个 toggle 测试,补充后覆盖: - toggle 的对合性(连续切换两次回到原值) - Theme 的 PartialEq / Copy trait 行为 - 首屏预加载脚本的关键行为契约:读取 localStorage、 回退 prefers-color-scheme、添加 dark class、 包裹 try/catch 防止禁用 localStorage 时抛错 共 9 个测试,全部通过。 --- src/theme.rs | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/theme.rs b/src/theme.rs index b1654a2..c34e40b 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -210,4 +210,55 @@ mod tests { fn toggle_switches_dark_to_light() { assert_eq!(Theme::Dark.toggle(), Theme::Light); } + + #[test] + fn toggle_is_an_involution() { + // 连续切换两次应当回到原始主题。 + assert_eq!(Theme::Light.toggle().toggle(), Theme::Light); + assert_eq!(Theme::Dark.toggle().toggle(), Theme::Dark); + } + + #[test] + fn theme_derives_equality() { + // Theme 派生了 PartialEq,相同变体必须相等。 + assert_eq!(Theme::Light, Theme::Light); + assert_eq!(Theme::Dark, Theme::Dark); + assert_ne!(Theme::Light, Theme::Dark); + } + + #[test] + fn theme_is_copy() { + // 确认 Theme 实现了 Copy,赋值后原值仍可用。 + let a = Theme::Light; + let b = a; + assert_eq!(a, b); + // 再次使用 a,若未实现 Copy 则编译失败。 + let _ = a.toggle(); + } + + #[test] + fn theme_preload_script_adds_dark_class() { + // 预加载脚本必须包含给 documentElement 添加 dark class 的逻辑。 + assert!(THEME_PRELOAD_SCRIPT.contains("classList.add('dark')")); + } + + #[test] + fn theme_preload_script_reads_local_storage() { + // 预加载脚本必须读取 yggdrasil-theme 键,与 THEME_KEY 保持一致。 + assert!(THEME_PRELOAD_SCRIPT.contains("localStorage.getItem('yggdrasil-theme')")); + assert_eq!(THEME_KEY, "yggdrasil-theme"); + } + + #[test] + fn theme_preload_script_falls_back_to_prefers_color_scheme() { + // 当 localStorage 中无主题时,脚本应回退到系统颜色偏好。 + assert!(THEME_PRELOAD_SCRIPT.contains("prefers-color-scheme: dark")); + } + + #[test] + fn theme_preload_script_swallows_errors() { + // 预加载脚本必须包裹在 try/catch 中,避免禁用 localStorage 时抛错。 + assert!(THEME_PRELOAD_SCRIPT.contains("try")); + assert!(THEME_PRELOAD_SCRIPT.contains("catch")); + } } From e9aac7d6b4d51db757bad9ab2e7f6e41627db4ef Mon Sep 17 00:00:00 2001 From: xfy Date: Mon, 15 Jun 2026 11:17:53 +0800 Subject: [PATCH 2/7] =?UTF-8?q?fix(highlight):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=A4=A7=E5=86=99=E8=AF=AD=E8=A8=80=E6=A0=87=E8=AF=86=E6=97=A0?= =?UTF-8?q?=E6=B3=95=E8=B5=B0=E5=88=AB=E5=90=8D=E6=98=A0=E5=B0=84=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 别名表此前用 lang == from 做严格相等比较,导致 'RUST' 这类 大写标识无法命中 ('rust' → 'rs') 这类别名,最终回退到纯文本。 改用 eq_ignore_ascii_case 比较,使别名解析与扩展名/名称的小写 回退路径保持一致。 test(highlight): 补充 find_syntax 分支覆盖 新增 8 个测试覆盖此前未测的路径: - 大写语言标识的小写回退(同时验证了上述修复) - 别名表:golang/bash/yml 等映射 - 未知语言与空字符串的纯文本回退 - 代码首尾空白被 trim - 多行代码逐行高亮 通过等价性断言(而非脆弱的 span class 字面量)校验别名解析, 避免 syntect 内部 class 命名变化导致测试漂移。 共 15 个测试,全部通过。 --- src/highlight.rs | 70 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/src/highlight.rs b/src/highlight.rs index 872bbc7..7947e0d 100644 --- a/src/highlight.rs +++ b/src/highlight.rs @@ -69,7 +69,8 @@ pub mod server { ("golang", "go"), ]; for &(from, to) in aliases { - if lang == from { + // 别名比较同样不区分大小写,保证 "RUST" 与 "rust" 等价。 + if lang.eq_ignore_ascii_case(from) { if let Some(s) = ss.find_syntax_by_extension(to) { return s; } @@ -151,4 +152,71 @@ mod tests { assert!(result.contains(r#"let"#)); assert!(result.contains(r#"1"#)); } + + #[test] + fn highlight_code_uppercase_language_falls_back_via_lowercase() { + // 大写语言标识应通过小写回退路径匹配到对应语法。 + let lower = highlight_code("fn main() {}", Some("rust")); + let upper = highlight_code("fn main() {}", Some("RUST")); + // 大写标识的输出必须与小写标识完全一致,证明回退路径生效。 + assert_eq!(lower, upper); + assert!(lower.contains(r#"fn"#)); + } + + #[test] + fn highlight_code_resolves_golang_alias() { + // 别名表中 "golang" 映射到 "go" 扩展名,输出应与直接用 "go" 一致。 + let by_alias = highlight_code("package main", Some("golang")); + let by_ext = highlight_code("package main", Some("go")); + assert_eq!(by_alias, by_ext); + // 别名解析必须产出带 span 的高亮输出,而非纯文本。 + assert!(by_alias.contains("span")); + } + + #[test] + fn highlight_code_resolves_bash_alias() { + // 别名表中 "bash" 映射到 "sh" 扩展名。 + let result = highlight_code("echo hello", Some("bash")); + assert!(result.contains("span")); + } + + #[test] + fn highlight_code_resolves_yml_alias() { + // 别名表中 "yml" 映射到 "yaml" 扩展名。 + let result = highlight_code("key: value", Some("yml")); + assert!(!result.is_empty()); + } + + #[test] + fn highlight_code_unknown_language_falls_back_to_plain_text() { + // 无法识别的语言应回退到纯文本语法,仍能输出内容。 + let result = highlight_code("hello world", Some("totally-not-a-language-xyz")); + assert!(result.contains("hello world")); + } + + #[test] + fn highlight_code_empty_language_string_falls_back_to_plain_text() { + // 空字符串语言标识应走纯文本回退路径。 + let result = highlight_code("just text", Some("")); + assert!(result.contains("just text")); + } + + #[test] + fn highlight_code_trims_surrounding_whitespace() { + // 代码首尾的空白会被 trim 掉再高亮。 + let result = highlight_code(" \nfn main() {}\n ", Some("rust")); + assert!(result.contains(r#"fn"#)); + } + + #[test] + fn highlight_code_multiline_output_spans_all_lines() { + // 多行代码每一行都应被解析为带 span 的输出。 + let code = "fn a() {}\nfn b() {}"; + let result = highlight_code(code, Some("rust")); + // 两处 fn 关键字都应出现 + assert_eq!( + result.matches(r#"fn"#).count(), + 2 + ); + } } From a9cde41de82e47156ddb9e3adfa10073c462acc2 Mon Sep 17 00:00:00 2001 From: xfy Date: Mon, 15 Jun 2026 11:18:55 +0800 Subject: [PATCH 3/7] =?UTF-8?q?test(webp):=20=E6=89=A9=E5=85=85=20WebP=20?= =?UTF-8?q?=E7=BC=96=E8=A7=A3=E7=A0=81=E6=B5=8B=E8=AF=95=E8=A6=86=E7=9B=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原仅 6 个测试,补充后覆盖: - WebpError 的 Display 格式与 std::error::Error trait 约束 - encode 对非快速路径像素格式(Luma8 / LumaA8)的 RGBA 转换 - encode 质量参数对体积的单调性(低质量 <= 高质量) - decode 对非法字节流与空输入的错误处理(不 panic) - decode 错误信息带 'WebP decode error' 前缀便于排查 - 编解码往返保持图像尺寸不变 共 17 个测试,全部通过。 --- src/webp.rs | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/src/webp.rs b/src/webp.rs index 1056587..ac2aacc 100644 --- a/src/webp.rs +++ b/src/webp.rs @@ -237,4 +237,101 @@ mod tests { assert_eq!(0u8.clamp(0, 6), 0); assert_eq!(6u8.clamp(0, 6), 6); } + + #[test] + fn webp_error_encode_display() { + let err = WebpError::Encode("boom".to_string()); + assert_eq!(err.to_string(), "WebP encode error: boom"); + } + + #[test] + fn webp_error_decode_display() { + let err = WebpError::Decode("busted".to_string()); + assert_eq!(err.to_string(), "WebP decode error: busted"); + } + + #[test] + fn webp_error_implements_std_error() { + // WebpError 必须实现 std::error::Error,才能在 ? 传播链中使用。 + fn assert_error() {} + assert_error::(); + } + + #[test] + fn encode_converts_luma8_to_rgba() { + // Luma8(灰度)图像不在 encode 的快速路径中,应被转换为 RGBA8 后编码。 + let img = image::DynamicImage::new_luma8(4, 4); + let result = encode(&img, 80.0, 2); + assert!(result.is_ok()); + assert!(!result.unwrap().is_empty()); + } + + #[test] + fn encode_converts_luma_a8_to_rgba() { + // LumaA8(带 alpha 的灰度)同样走转换路径。 + let img = image::DynamicImage::new_luma_a8(4, 4); + let result = encode(&img, 80.0, 2); + assert!(result.is_ok()); + assert!(!result.unwrap().is_empty()); + } + + #[test] + fn encode_lower_quality_produces_smaller_or_equal_bytes() { + // 对同一张随机内容图,更低质量通常不产生更大的输出。 + // 使用固定尺寸的纯色图保证可复现:低质量下体积应 <= 高质量体积。 + let img = image::DynamicImage::new_rgb8(64, 64); + let high = encode(&img, 95.0, 4).unwrap(); + let low = encode(&img, 10.0, 4).unwrap(); + assert!( + low.len() <= high.len(), + "lower quality ({}) should not exceed higher quality ({}); got low={} high={}", + 10.0, + 95.0, + low.len(), + high.len() + ); + } + + #[test] + fn decode_invalid_bytes_returns_error() { + // 非 WebP 字节流应返回解码错误而非 panic。 + let junk = b"this is definitely not a webp image"; + let result = decode(junk); + assert!(result.is_err()); + } + + #[test] + fn decode_empty_bytes_returns_error() { + // 空字节流应返回解码错误而非 panic。 + let result = decode(&[]); + assert!(result.is_err()); + } + + #[test] + fn decode_error_message_is_descriptive() { + // 解码错误的 Display 应包含 'WebP decode error' 前缀,便于日志排查。 + let err = decode(b"not webp").unwrap_err(); + assert!(err.to_string().starts_with("WebP decode error")); + } + + #[test] + fn encode_decode_preserves_dimensions() { + // 编码再解码后,图像宽高应保持一致。 + let original = image::DynamicImage::new_rgb8(16, 9); + let encoded = encode(&original, 85.0, 4).unwrap(); + let decoded = decode(&encoded).unwrap(); + assert_eq!(decoded.width(), 16); + assert_eq!(decoded.height(), 9); + } + + #[test] + fn webp_config_default_quality_and_method() { + // 未设置环境变量时,默认 quality=85.0、method=2(见 WEBP_CONFIG 文档)。 + let config = WebpConfig { + quality: 85.0, + method: 2, + }; + assert_eq!(config.quality, 85.0); + assert_eq!(config.method, 2); + } } From 28f0117f069ee334c5c5a3a6fc1bb6a2988c4e4c Mon Sep 17 00:00:00 2001 From: xfy Date: Mon, 15 Jun 2026 11:19:46 +0800 Subject: [PATCH 4/7] =?UTF-8?q?test(sanitizer):=20=E8=A1=A5=E5=85=85=20is?= =?UTF-8?q?=5Fsafe=5Furl=20=E5=88=86=E6=94=AF=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit is_safe_url 是 HTML 消毒器的安全核心,原仅通过 clean_html 间接覆盖。 新增 9 个直接单元测试,锁定各分支契约: - https/http 白名单通过 - javascript / vbscript scheme 拒绝(含大小写混淆) - data URI 按 allow_data_uri 标志决定(文章正文 vs 评论) - 相对路径与锚点片段通过 - 空字符串/纯空白视为安全 - mailto / tel / ftp 等其它白名单 scheme 通过 - 含空白的 scheme 名(混淆手法)被拒绝 共 17 个测试,全部通过。 --- src/api/sanitizer.rs | 72 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/src/api/sanitizer.rs b/src/api/sanitizer.rs index 9a5b30d..4b2b24a 100644 --- a/src/api/sanitizer.rs +++ b/src/api/sanitizer.rs @@ -435,4 +435,76 @@ mod tests { "x" ); } + + // ---- is_safe_url 直接分支测试 ---- + // is_safe_url 是安全敏感的内部函数,以下测试锁定其各分支的行为契约。 + + #[test] + fn is_safe_url_allows_https() { + let schemes = default_allowed_schemes(); + assert!(is_safe_url("https://example.com", &schemes, false)); + assert!(is_safe_url("http://example.com", &schemes, false)); + } + + #[test] + fn is_safe_url_rejects_javascript() { + let schemes = default_allowed_schemes(); + assert!(!is_safe_url("javascript:alert(1)", &schemes, false)); + } + + #[test] + fn is_safe_url_rejects_vbscript() { + let schemes = default_allowed_schemes(); + assert!(!is_safe_url("vbscript:msgbox", &schemes, false)); + } + + #[test] + fn is_safe_url_data_uri_respects_flag() { + let schemes = default_allowed_schemes(); + // 文章正文允许 data URI + assert!(is_safe_url("data:image/png;base64,iVBOR", &schemes, true)); + // 评论禁用 data URI + assert!(!is_safe_url("data:image/png;base64,iVBOR", &schemes, false)); + } + + #[test] + fn is_safe_url_allows_relative_and_fragment() { + let schemes = default_allowed_schemes(); + // 绝对路径 + assert!(is_safe_url("/path/to/page", &schemes, false)); + // 锚点 + assert!(is_safe_url("#section", &schemes, false)); + } + + #[test] + fn is_safe_url_empty_is_safe() { + let schemes = default_allowed_schemes(); + // 空 URL(如 img 无 src)视为安全。 + assert!(is_safe_url("", &schemes, false)); + assert!(is_safe_url(" ", &schemes, false)); + } + + #[test] + fn is_safe_url_allows_other_whitelisted_schemes() { + let schemes = default_allowed_schemes(); + // mailto / tel / ftp 等均在默认白名单中。 + assert!(is_safe_url("mailto:user@example.com", &schemes, false)); + assert!(is_safe_url("tel:+8613800138000", &schemes, false)); + assert!(is_safe_url("ftp://example.com/file", &schemes, false)); + } + + #[test] + fn is_safe_url_rejects_scheme_with_whitespace() { + let schemes = default_allowed_schemes(); + // 含空格的 scheme 名是已知的混淆手法,应被拒绝。 + assert!(!is_safe_url("java\tscript:alert(1)", &schemes, false)); + } + + #[test] + fn is_safe_url_scheme_matching_is_case_insensitive() { + let schemes = default_allowed_schemes(); + // scheme 大小写不敏感:HTTPS 与 https 等价。 + assert!(is_safe_url("HTTPS://example.com", &schemes, false)); + assert!(!is_safe_url("JAVASCRIPT:alert(1)", &schemes, false)); + } } From 9fc626232cd416f7a47cea8023ba0535e535dc53 Mon Sep 17 00:00:00 2001 From: xfy Date: Mon, 15 Jun 2026 11:20:33 +0800 Subject: [PATCH 5/7] =?UTF-8?q?test(error):=20=E8=A1=A5=E5=85=A8=20AppErro?= =?UTF-8?q?r=20=E5=90=84=E5=8F=98=E4=BD=93=E7=9A=84=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E8=BD=AC=E6=8D=A2=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原仅覆盖 Unauthorized/Forbidden/NotFound/DbConn/Query 五个变体, 补充: - Internal 变体的消息透传 - Transaction 变体的 SQL 细节隐藏(与 Query 行为一致) - 三类数据库错误统一返回中文通用提示的聚合断言 共 8 个测试,全部通过。 --- src/api/error.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/api/error.rs b/src/api/error.rs index e69e1cc..412fcd3 100644 --- a/src/api/error.rs +++ b/src/api/error.rs @@ -106,4 +106,34 @@ mod tests { let msg = err.to_string(); assert!(msg.contains("文章不存在"), "expected passthrough: {msg}"); } + + #[test] + fn internal_message_passthrough() { + // Internal 错误的消息原样透传,便于向用户展示可读的内部错误描述。 + let err: ServerFnError = AppError::Internal("内部错误").into(); + let msg = err.to_string(); + assert!(msg.contains("内部错误"), "expected passthrough: {msg}"); + } + + #[test] + fn transaction_hides_sql_details() { + // 事务错误同样返回通用提示,不泄露 SQL 细节。 + let err: ServerFnError = AppError::tx("deadlock detected on UPDATE posts").into(); + let msg = err.to_string(); + assert!(!msg.contains("UPDATE"), "should not leak SQL: {msg}"); + assert!(!msg.contains("deadlock"), "should not leak error detail: {msg}"); + assert!(msg.contains("操作失败"), "expected generic message: {msg}"); + } + + #[test] + fn db_conn_query_transaction_all_return_generic_message() { + // 三类数据库错误对外均返回固定中文提示,避免泄露实现细节。 + let db_conn: ServerFnError = AppError::DbConn("x".into()).into(); + let query: ServerFnError = AppError::Query("x".into()).into(); + let tx: ServerFnError = AppError::Transaction("x".into()).into(); + + assert!(db_conn.to_string().contains("服务暂时不可用")); + assert!(query.to_string().contains("操作失败")); + assert!(tx.to_string().contains("操作失败")); + } } From 263208b9305f614a6c111555cf9c3d6186bbc356 Mon Sep 17 00:00:00 2001 From: xfy Date: Mon, 15 Jun 2026 11:22:35 +0800 Subject: [PATCH 6/7] =?UTF-8?q?style:=20=E4=BF=AE=E6=AD=A3=E6=96=B0?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E7=9A=84=E6=A0=BC=E5=BC=8F=E4=BB=A5=E7=AC=A6?= =?UTF-8?q?=E5=90=88=20rustfmt=20=E5=AE=BD=E5=BA=A6=E9=99=90=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cargo fmt 自动调整了 highlight.rs 与 error.rs 中新增测试的 assert! / assert_eq! 长行折行。 --- src/api/error.rs | 5 ++++- src/highlight.rs | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/api/error.rs b/src/api/error.rs index 412fcd3..bc63ee5 100644 --- a/src/api/error.rs +++ b/src/api/error.rs @@ -121,7 +121,10 @@ mod tests { let err: ServerFnError = AppError::tx("deadlock detected on UPDATE posts").into(); let msg = err.to_string(); assert!(!msg.contains("UPDATE"), "should not leak SQL: {msg}"); - assert!(!msg.contains("deadlock"), "should not leak error detail: {msg}"); + assert!( + !msg.contains("deadlock"), + "should not leak error detail: {msg}" + ); assert!(msg.contains("操作失败"), "expected generic message: {msg}"); } diff --git a/src/highlight.rs b/src/highlight.rs index 7947e0d..dcc56c6 100644 --- a/src/highlight.rs +++ b/src/highlight.rs @@ -215,7 +215,9 @@ mod tests { let result = highlight_code(code, Some("rust")); // 两处 fn 关键字都应出现 assert_eq!( - result.matches(r#"fn"#).count(), + result + .matches(r#"fn"#) + .count(), 2 ); } From 19ffcada4af7290776027f1180b6bd14b9aa32e1 Mon Sep 17 00:00:00 2001 From: xfy Date: Mon, 15 Jun 2026 11:26:24 +0800 Subject: [PATCH 7/7] =?UTF-8?q?test:=20=E7=A7=BB=E9=99=A4=E8=84=86?= =?UTF-8?q?=E5=BC=B1=E6=96=AD=E8=A8=80=E4=B8=8E=E5=87=91=E6=95=B0=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=EF=BC=88review=20=E4=BF=AE=E6=AD=A3=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对本次新增测试自审后的清理: 1. webp: encode_lower_quality_produces_smaller_or_equal_bytes 原断言“低质量体积 <= 高质量体积”是 WebP 的统计趋势而非 确定性不变量,依赖底层 libwebp 量化策略,纯色图上高低差异 接近 0,未来库升级易变为 flaky。改为验证两个质量档位都能 成功编码并产生可被本模块解码的合法 WebP。 2. theme: theme_is_copy Copy 语义由编译器在编译期保证,运行期断言永真,不测任何 运行逻辑,属凑数。 3. webp: webp_config_default_quality_and_method 构造 struct 再断言字段等于构造时填入的值,是同义反复。 净减 2 个测试(292 → 290),全部通过。 --- src/theme.rs | 10 ---------- src/webp.rs | 31 +++++++++---------------------- 2 files changed, 9 insertions(+), 32 deletions(-) diff --git a/src/theme.rs b/src/theme.rs index c34e40b..54d2e9a 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -226,16 +226,6 @@ mod tests { assert_ne!(Theme::Light, Theme::Dark); } - #[test] - fn theme_is_copy() { - // 确认 Theme 实现了 Copy,赋值后原值仍可用。 - let a = Theme::Light; - let b = a; - assert_eq!(a, b); - // 再次使用 a,若未实现 Copy 则编译失败。 - let _ = a.toggle(); - } - #[test] fn theme_preload_script_adds_dark_class() { // 预加载脚本必须包含给 documentElement 添加 dark class 的逻辑。 diff --git a/src/webp.rs b/src/webp.rs index ac2aacc..b1987a2 100644 --- a/src/webp.rs +++ b/src/webp.rs @@ -276,20 +276,18 @@ mod tests { } #[test] - fn encode_lower_quality_produces_smaller_or_equal_bytes() { - // 对同一张随机内容图,更低质量通常不产生更大的输出。 - // 使用固定尺寸的纯色图保证可复现:低质量下体积应 <= 高质量体积。 + fn encode_lower_quality_does_not_explode_on_solid_color() { + // 纯色图是 WebP 的极端情况(信息熵接近 0),确保高低质量都能编码成功 + // 而非 panic,且产物是合法非空字节流。不假设低质量体积一定更小, + // 因为这依赖底层 libwebp 的量化策略,非确定性不变量。 let img = image::DynamicImage::new_rgb8(64, 64); let high = encode(&img, 95.0, 4).unwrap(); let low = encode(&img, 10.0, 4).unwrap(); - assert!( - low.len() <= high.len(), - "lower quality ({}) should not exceed higher quality ({}); got low={} high={}", - 10.0, - 95.0, - low.len(), - high.len() - ); + assert!(!high.is_empty()); + assert!(!low.is_empty()); + // 两者都应是合法的 WebP(能被本模块解码回来)。 + assert!(decode(&high).is_ok()); + assert!(decode(&low).is_ok()); } #[test] @@ -323,15 +321,4 @@ mod tests { assert_eq!(decoded.width(), 16); assert_eq!(decoded.height(), 9); } - - #[test] - fn webp_config_default_quality_and_method() { - // 未设置环境变量时,默认 quality=85.0、method=2(见 WEBP_CONFIG 文档)。 - let config = WebpConfig { - quality: 85.0, - method: 2, - }; - assert_eq!(config.quality, 85.0); - assert_eq!(config.method, 2); - } }