test(design): 补「先量化再打分」的端到端反证(复审 r1 p1)
reviewer 实测变异:把 analyze_rgba 改成不透明像素算术平均,150 个测试全绿—— 因为反证只测了 quantize/score 中间层,接线没人覆盖。新增: - seed::tests::end_to_end_picks_the_five_percent_accent(95% 米白 + 5% 珊瑚 → #FF6B5A) - seed::tests::end_to_end_picks_the_three_percent_accent(97/3 → #FF8800) - resolve::tests::a_five_percent_accent_drives_level_one(一级成功、种子是珊瑚、≠ 回落色卡) 顺带把 seed 的单测拆到 seed/tests.rs(410 行 → 220 + 190,恢复 ≤400 纪律)
This commit is contained in:
1 parent
80314f4830
commit
36c1cce00d
3 files changed
+220
-133
No files matched your search
@@ -339,4 +339,33 @@ mod tests {
|
||||
Some("palette.hint.background.no-opaque-pixels")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_five_percent_accent_drives_level_one() {
|
||||
// 端到端反证(与 `palette::seed::tests::end_to_end_picks_the_five_percent_accent` 配对):
|
||||
// 95% 米白 + 5% 珊瑚的背景图**必须**在一级取色成功,且种子里就是那 5% 的珊瑚。
|
||||
// 若 `analyze_rgba` 退化成「不透明像素求算术平均」,这条会变成「回落色卡」而红。
|
||||
let beige = Color::rgb(0xF5, 0xF1, 0xEA);
|
||||
let coral = Color::rgb(0xFF, 0x6B, 0x5A);
|
||||
let mut pixels = Vec::with_capacity(40 * 40 * 4);
|
||||
for index in 0..40 * 40 {
|
||||
let color = if index < 40 * 40 - 80 { beige } else { coral };
|
||||
pixels.extend_from_slice(&[color.r, color.g, color.b, color.a]);
|
||||
}
|
||||
|
||||
let (scheme, provenance) = resolve_source(
|
||||
Some(ImageInput::pixels(&pixels, 40, 40)),
|
||||
None,
|
||||
Some(CardKey::Rose),
|
||||
SchemeFlavor::Light,
|
||||
);
|
||||
assert_eq!(provenance.level, Level::BackgroundImage, "{provenance:?}");
|
||||
assert_eq!(provenance.background(), Outcome::Used);
|
||||
let seed = provenance.seed.expect("一级成功必须带种子");
|
||||
assert_eq!(seed.color, coral, "种子应当是 5% 的珊瑚");
|
||||
assert!(provenance.hint_key().is_none());
|
||||
// 方案来自珊瑚,而不是「回落到用户选的 Rose 卡」——两者 primary 不同。
|
||||
assert_ne!(scheme, CardKey::Rose.card().scheme(SchemeFlavor::Light));
|
||||
assert!(crate::palette::wcag::check(&scheme).is_clean());
|
||||
}
|
||||
}
|
||||
@@ -217,136 +217,4 @@ fn sample_blocks(pixels: &[u8], width: usize, height: usize) -> Vec<Color> {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// 生成 `width × height` 的全 `color` 图。
|
||||
fn solid(color: Color, width: u32, height: u32) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity((width * height * 4) as usize);
|
||||
for _ in 0..(width * height) {
|
||||
out.extend_from_slice(&[color.r, color.g, color.b, color.a]);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn solid_vivid_image_yields_seed() {
|
||||
let pixels = solid(Color::rgb(0x3C, 0x8D, 0xBC), 4, 4);
|
||||
let seed = analyze_rgba(&pixels, 4, 4).expect("天蓝纯色图有种子");
|
||||
assert_eq!(seed.color, Color::from_hex("#3C8DBC").expect("合法 hex"));
|
||||
assert!(seed.chroma > MIN_SEED_CHROMA);
|
||||
assert!((seed.share - 1.0).abs() < 1.0e-6);
|
||||
assert!(!seed.is_muted());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gray_white_image_is_rejected_with_measured_chroma() {
|
||||
// D40 的反证:灰白壁纸硬取色会得到青色 #006874,这里必须拒绝并报出彩度。
|
||||
let pixels = solid(Color::rgb(0xF2, 0xF2, 0xF0), 8, 8);
|
||||
match analyze_rgba(&pixels, 8, 8) {
|
||||
Err(Failure::LowChroma { chroma, .. }) => {
|
||||
assert!(chroma < MIN_SEED_CHROMA, "彩度 {chroma} 不该过门槛");
|
||||
}
|
||||
other => panic!("灰白图必须被拒,实际 {other:?}"),
|
||||
}
|
||||
assert_eq!(seed_from_rgba(&pixels, 8, 8), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn neutral_gray_hash_rejects_instead_of_inventing_cyan() {
|
||||
// 与 M70 复审实测的坏味道对齐:#808080 的 CAM16 色相 209.5°,硬取色会得到青色。
|
||||
let pixels = solid(Color::rgb(0x80, 0x80, 0x80), 4, 4);
|
||||
assert_eq!(seed_from_rgba(&pixels, 4, 4), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiny_images_are_too_small() {
|
||||
let pixels = solid(Color::rgb(0xFF, 0x00, 0x00), 1, 3);
|
||||
assert_eq!(
|
||||
analyze_rgba(&pixels, 1, 3),
|
||||
Err(Failure::TooSmall {
|
||||
width: 1,
|
||||
height: 3
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
Failure::TooSmall {
|
||||
width: 1,
|
||||
height: 3
|
||||
}
|
||||
.code(),
|
||||
"too-small"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_buffer_length_is_reported() {
|
||||
let pixels = solid(Color::rgb(0xFF, 0x00, 0x00), 2, 2);
|
||||
match analyze_rgba(&pixels[..12], 2, 2) {
|
||||
Err(Failure::BadDimensions {
|
||||
expected, actual, ..
|
||||
}) => {
|
||||
assert_eq!((expected, actual), (16, 12));
|
||||
}
|
||||
other => panic!("应当报维度错误,实际 {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fully_transparent_image_has_no_seed() {
|
||||
let pixels = solid(Color::rgba(0x3C, 0x8D, 0xBC, 0), 4, 4);
|
||||
assert_eq!(analyze_rgba(&pixels, 4, 4), Err(Failure::NoOpaquePixels));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_image_is_downsampled_and_still_deterministic() {
|
||||
let (width, height) = (512_u32, 512_u32);
|
||||
let mut pixels = Vec::with_capacity((width * height * 4) as usize);
|
||||
for y in 0..height {
|
||||
for x in 0..width {
|
||||
// 细网格:等距抽样会走样,块平均能得到稳定的底色。
|
||||
let checker = (x + y) % 2 == 0;
|
||||
pixels.extend_from_slice(if checker {
|
||||
&[0xD2, 0x3B, 0x2A, 0xFF]
|
||||
} else {
|
||||
&[0x2A, 0x3B, 0xD2, 0xFF]
|
||||
});
|
||||
}
|
||||
}
|
||||
let first = analyze_rgba(&pixels, width, height).expect("大图有种子");
|
||||
let second = analyze_rgba(&pixels, width, height).expect("可复现");
|
||||
assert_eq!(first, second);
|
||||
let samples = sample_blocks(&pixels, width as usize, height as usize);
|
||||
assert!(
|
||||
samples.len() <= MAX_SAMPLES,
|
||||
"采样点 {} 超标",
|
||||
samples.len()
|
||||
);
|
||||
// 块平均把红蓝网格合成偏紫的中间色,而不是撞上纯红或纯蓝之一。
|
||||
assert!(first.chroma > MIN_SEED_CHROMA, "{first:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_sampling_averages_instead_of_picking() {
|
||||
// 2×2 红蓝棋盘,块边长 1(图很小)→ 4 个采样点;只验证采样点数量与顺序稳定。
|
||||
let pixels = [
|
||||
0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00,
|
||||
0x00, 0xFF,
|
||||
];
|
||||
let samples = sample_blocks(&pixels, 2, 2);
|
||||
assert_eq!(samples.len(), 4);
|
||||
assert_eq!(samples[0], Color::rgb(0xFF, 0x00, 0x00));
|
||||
assert_eq!(samples[1], Color::rgb(0x00, 0x00, 0xFF));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seed_scheme_meets_wcag_on_the_primary_pair() {
|
||||
let pixels = solid(Color::rgb(0xC0, 0x39, 0x2B), 4, 4);
|
||||
let seed = analyze_rgba(&pixels, 4, 4).expect("砖红有种子");
|
||||
for flavor in [SchemeFlavor::Light, SchemeFlavor::Dark] {
|
||||
let scheme = seed.scheme(flavor, 12.0);
|
||||
let ratio = scheme.contrast(crate::color::Role::OnPrimary, crate::color::Role::Primary);
|
||||
assert!(ratio >= 4.5, "{flavor:?} 只有 {ratio}");
|
||||
}
|
||||
}
|
||||
}
|
||||
mod tests;
|
||||
@@ -0,0 +1,190 @@
|
||||
//! `seed` 模块的单测:五类输入各自的处置 + **端到端反证**(不许退化成平均色)。
|
||||
//!
|
||||
//! 为什么端到端测试必须在这里(而不是只测 `quantize`/`score`):那两条单测手工构造
|
||||
//! `Cluster` 直接喂中间层,**绕过了 `analyze_rgba` 的接线**。把 `analyze_rgba` 换成
|
||||
//! 「不透明像素求算术平均」时,中间层单测照样绿,只有这里会红。
|
||||
|
||||
use super::*;
|
||||
|
||||
/// 生成 `width × height` 的全 `color` 图。
|
||||
fn solid(color: Color, width: u32, height: u32) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity((width * height * 4) as usize);
|
||||
for _ in 0..(width * height) {
|
||||
out.extend_from_slice(&[color.r, color.g, color.b, color.a]);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// 生成「其余是 `base`,`accent_pixels` 个像素是 `accent`」的图(强调色排在末尾)。
|
||||
///
|
||||
/// 这是**反证「不许取平均色」**的输入构造器:强调色面积小到平均色会被底色吃掉,
|
||||
/// 但它是人眼会记住、能当强调色用的那一小撮。
|
||||
fn two_tone(width: u32, height: u32, base: Color, accent: Color, accent_pixels: usize) -> Vec<u8> {
|
||||
let total = (width * height) as usize;
|
||||
assert!(accent_pixels <= total);
|
||||
let mut out = Vec::with_capacity(total * 4);
|
||||
for index in 0..total {
|
||||
let color = if index < total - accent_pixels {
|
||||
base
|
||||
} else {
|
||||
accent
|
||||
};
|
||||
out.extend_from_slice(&[color.r, color.g, color.b, color.a]);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn end_to_end_picks_the_five_percent_accent() {
|
||||
// ⚠️ 端到端反证:这条必须走 `analyze_rgba` 的**完整管线**(量化 → 打分 → 彩度门槛),
|
||||
// 不许像 `quantize`/`score` 的单测那样手工构造 `Cluster` 直接喂中间层。
|
||||
// 如果 `analyze_rgba` 被改成「不透明像素求算术平均」,平均值会是米白(chroma ≈4)→
|
||||
// 返回 `LowChroma` 而不是种子,这条立刻红。
|
||||
let beige = Color::from_hex("#F5F1EA").expect("合法 hex");
|
||||
let coral = Color::from_hex("#FF6B5A").expect("合法 hex");
|
||||
let pixels = two_tone(40, 40, beige, coral, 80); // 95% 米白 + 5% 珊瑚
|
||||
|
||||
let seed = analyze_rgba(&pixels, 40, 40).expect("5% 的珊瑚应当被选中当种子");
|
||||
assert_eq!(
|
||||
seed.color,
|
||||
coral,
|
||||
"种子必须是珊瑚(实际 {});米白占比 95% 但它不是「主色」",
|
||||
seed.color.to_hex()
|
||||
);
|
||||
assert!(seed.chroma > 40.0, "珊瑚彩度应当很高,实测 {}", seed.chroma);
|
||||
// 与「平均色」泾渭分明:原图的算术平均值是米白,色相/彩度都差一整个量级。
|
||||
assert!(!seed.is_muted());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn end_to_end_picks_the_three_percent_accent() {
|
||||
// 面积再降到 3%(48/1600):`Score` 的 1% 人气门槛仍然放行,平均色更不可能靠巧合命中。
|
||||
let beige = Color::from_hex("#F5F1EA").expect("合法 hex");
|
||||
let orange = Color::from_hex("#FF8800").expect("合法 hex");
|
||||
let pixels = two_tone(40, 40, beige, orange, 48);
|
||||
|
||||
let seed = analyze_rgba(&pixels, 40, 40).expect("3% 的橙应当被选中当种子");
|
||||
assert_eq!(seed.color, orange, "实际 {}", seed.color.to_hex());
|
||||
assert!(seed.chroma > 40.0, "彩度 {}", seed.chroma);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn solid_vivid_image_yields_seed() {
|
||||
let pixels = solid(Color::rgb(0x3C, 0x8D, 0xBC), 4, 4);
|
||||
let seed = analyze_rgba(&pixels, 4, 4).expect("天蓝纯色图有种子");
|
||||
assert_eq!(seed.color, Color::from_hex("#3C8DBC").expect("合法 hex"));
|
||||
assert!(seed.chroma > MIN_SEED_CHROMA);
|
||||
assert!((seed.share - 1.0).abs() < 1.0e-6);
|
||||
assert!(!seed.is_muted());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gray_white_image_is_rejected_with_measured_chroma() {
|
||||
// D40 的反证:灰白壁纸硬取色会得到青色 #006874,这里必须拒绝并报出彩度。
|
||||
let pixels = solid(Color::rgb(0xF2, 0xF2, 0xF0), 8, 8);
|
||||
match analyze_rgba(&pixels, 8, 8) {
|
||||
Err(Failure::LowChroma { chroma, .. }) => {
|
||||
assert!(chroma < MIN_SEED_CHROMA, "彩度 {chroma} 不该过门槛");
|
||||
}
|
||||
other => panic!("灰白图必须被拒,实际 {other:?}"),
|
||||
}
|
||||
assert_eq!(seed_from_rgba(&pixels, 8, 8), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn neutral_gray_hash_rejects_instead_of_inventing_cyan() {
|
||||
// 与 M70 复审实测的坏味道对齐:#808080 的 CAM16 色相 209.5°,硬取色会得到青色。
|
||||
let pixels = solid(Color::rgb(0x80, 0x80, 0x80), 4, 4);
|
||||
assert_eq!(seed_from_rgba(&pixels, 4, 4), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiny_images_are_too_small() {
|
||||
let pixels = solid(Color::rgb(0xFF, 0x00, 0x00), 1, 3);
|
||||
assert_eq!(
|
||||
analyze_rgba(&pixels, 1, 3),
|
||||
Err(Failure::TooSmall {
|
||||
width: 1,
|
||||
height: 3
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
Failure::TooSmall {
|
||||
width: 1,
|
||||
height: 3
|
||||
}
|
||||
.code(),
|
||||
"too-small"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_buffer_length_is_reported() {
|
||||
let pixels = solid(Color::rgb(0xFF, 0x00, 0x00), 2, 2);
|
||||
match analyze_rgba(&pixels[..12], 2, 2) {
|
||||
Err(Failure::BadDimensions {
|
||||
expected, actual, ..
|
||||
}) => {
|
||||
assert_eq!((expected, actual), (16, 12));
|
||||
}
|
||||
other => panic!("应当报维度错误,实际 {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fully_transparent_image_has_no_seed() {
|
||||
let pixels = solid(Color::rgba(0x3C, 0x8D, 0xBC, 0), 4, 4);
|
||||
assert_eq!(analyze_rgba(&pixels, 4, 4), Err(Failure::NoOpaquePixels));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_image_is_downsampled_and_still_deterministic() {
|
||||
let (width, height) = (512_u32, 512_u32);
|
||||
let mut pixels = Vec::with_capacity((width * height * 4) as usize);
|
||||
for y in 0..height {
|
||||
for x in 0..width {
|
||||
// 细网格:等距抽样会走样,块平均能得到稳定的底色。
|
||||
let checker = (x + y) % 2 == 0;
|
||||
pixels.extend_from_slice(if checker {
|
||||
&[0xD2, 0x3B, 0x2A, 0xFF]
|
||||
} else {
|
||||
&[0x2A, 0x3B, 0xD2, 0xFF]
|
||||
});
|
||||
}
|
||||
}
|
||||
let first = analyze_rgba(&pixels, width, height).expect("大图有种子");
|
||||
let second = analyze_rgba(&pixels, width, height).expect("可复现");
|
||||
assert_eq!(first, second);
|
||||
let samples = sample_blocks(&pixels, width as usize, height as usize);
|
||||
assert!(
|
||||
samples.len() <= MAX_SAMPLES,
|
||||
"采样点 {} 超标",
|
||||
samples.len()
|
||||
);
|
||||
// 块平均把红蓝网格合成偏紫的中间色,而不是撞上纯红或纯蓝之一。
|
||||
assert!(first.chroma > MIN_SEED_CHROMA, "{first:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_sampling_averages_instead_of_picking() {
|
||||
// 2×2 红蓝棋盘,块边长 1(图很小)→ 4 个采样点;只验证采样点数量与顺序稳定。
|
||||
let pixels = [
|
||||
0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0x00,
|
||||
0xFF,
|
||||
];
|
||||
let samples = sample_blocks(&pixels, 2, 2);
|
||||
assert_eq!(samples.len(), 4);
|
||||
assert_eq!(samples[0], Color::rgb(0xFF, 0x00, 0x00));
|
||||
assert_eq!(samples[1], Color::rgb(0x00, 0x00, 0xFF));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seed_scheme_meets_wcag_on_the_primary_pair() {
|
||||
let pixels = solid(Color::rgb(0xC0, 0x39, 0x2B), 4, 4);
|
||||
let seed = analyze_rgba(&pixels, 4, 4).expect("砖红有种子");
|
||||
for flavor in [SchemeFlavor::Light, SchemeFlavor::Dark] {
|
||||
let scheme = seed.scheme(flavor, 12.0);
|
||||
let ratio = scheme.contrast(crate::color::Role::OnPrimary, crate::color::Role::Primary);
|
||||
assert!(ratio >= 4.5, "{flavor:?} 只有 {ratio}");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user