//! 门禁:**组件层零硬编码**(色值 / 尺寸)与「不依赖 slint」。 //! //! 这三条测试会扫本 crate 的 `src/**` 与 `Cargo.toml`,故意写得「宁可错杀」: //! 将来 reviewer 复跑 `cargo test -p unlucky-app-iced` 就能挡住「顺手写个 #RRGGBB」的提交。 //! //! 判定口径(与 `component-fidelity-rubric.md` T1/T2、`visual-acceptance-protocol.md` §1 对齐): //! //! - **色值字面量**:`#` + 6 位十六进制(且**至少含一个数字**,这样文档里的 `#RRGGBB` 占位不会被误杀) //! 或 `Color::from_rgb(0.1, …)` / `color!(0x…)` 这类带数值参数的构造。 //! - **令牌优先**:唯一允许的是 `theme.rs`(适配层),且它也只做 `token → iced` 的搬运。 //! - **尺寸字面量**:`shell.rs` 里 `.width(12.0)` / `.height(34)` / `.size(22)` 一律不许出现 //! (必须走 `LayoutMetrics` / `TypeStyle` / `Space`)。 use std::path::{Path, PathBuf}; /// 本 crate 根目录(编译期注入,换机器也成立)。 fn manifest_dir() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) } /// 递归收集 `dir` 下的 `.rs` 文件。 fn rust_files(dir: &Path, collected: &mut Vec) { let entries = std::fs::read_dir(dir).unwrap_or_else(|error| panic!("读不动 {}:{error}", dir.display())); for entry in entries { let path = entry.expect("目录项读失败").path(); if path.is_dir() { rust_files(&path, collected); } else if path.extension().is_some_and(|ext| ext == "rs") { collected.push(path); } } } fn sources() -> Vec<(PathBuf, String)> { let mut files = Vec::new(); rust_files(&manifest_dir().join("src"), &mut files); assert!( files.len() >= 8, "只扫到 {} 个源文件:扫描逻辑坏了,这条门禁等于没跑", files.len() ); files .into_iter() .map(|path| { let text = std::fs::read_to_string(&path) .unwrap_or_else(|error| panic!("读不动 {}:{error}", path.display())); (path, text) }) .collect() } /// `#RRGGBB` / `#AARRGGBB` 形式的色值字面量(至少含一个数字,避开 `#RRGGBB` 这种占位写法)。 fn hex_color_at(bytes: &[u8], start: usize) -> Option<&str> { let digits = bytes.get(start + 1..start + 7)?; let all_hex = digits .iter() .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_whitespace()); let has_number = digits.iter().any(u8::is_ascii_digit); if all_hex && has_number { // 后面不能再跟着十六进制字符(否则是更长的 token,不是 6 位色值) let next = bytes.get(start + 7).copied(); if next.is_none_or(|byte| !byte.is_ascii_hexdigit()) { return Some("hex 色值字面量(#RRGGBB)"); } } None } /// 带数值参数的取色构造(`Color::from_rgb(0.1, …)`、`color!(0x12, …)`)。 const NUMERIC_COLOR_CALLS: [&str; 5] = [ "Color::from_rgb(", "Color::from_rgba(", "Color::from_rgba8(", "Color::from_hex(", "color!(", ]; fn numeric_color_call(text: &str) -> Option<&'static str> { for pattern in NUMERIC_COLOR_CALLS { let mut from = 0; while let Some(found) = text[from..].find(pattern) { let after = from + found + pattern.len(); let next = text[after..].trim_start().chars().next(); // 参数以数字或 `0x`/`#` 开头才算字面量;`Color::from_rgba8(color.r, …)` 是搬运,放行。 if next.is_some_and(|ch| ch.is_ascii_digit() || ch == '#') { return Some(pattern); } from = after; } } None } #[test] fn no_hardcoded_colors_in_sources() { let mut offenders = Vec::new(); for (path, text) in sources() { for (index, line) in text.lines().enumerate() { let bytes = line.as_bytes(); let mut position = 0; while let Some(found) = line[position..].find('#') { let at = position + found; if let Some(kind) = hex_color_at(bytes, at) { offenders.push(format!( "{}:{} {line:?} -> {kind}", path.display(), index + 1 )); } position = at + 1; } if let Some(pattern) = numeric_color_call(line) { offenders.push(format!( "{}:{} {line:?} -> {pattern}", path.display(), index + 1 )); } } } assert!( offenders.is_empty(), "组件层不许出现色值字面量(一律走 unlucky_design 的 Role):\n{}", offenders.join("\n") ); } /// 判断某个布局调用后面是不是直接跟了尺寸字面量;`0`(= 无内边距)放行。 fn size_literal(argument: &str) -> Option { let number: String = argument .trim_start() .chars() .take_while(|ch| ch.is_ascii_digit() || *ch == '.') .collect(); if number.is_empty() || number.trim_end_matches('.').parse::() == Ok(0.0) { return None; } Some(number) } #[test] fn shell_takes_every_size_from_tokens() { let mut files = Vec::new(); rust_files(&manifest_dir().join("src").join("shell"), &mut files); assert!( files.len() >= 2, "shell 模块至少有两个文件(mod.rs + nav.rs),实际扫到 {}", files.len() ); let mut offenders = Vec::new(); for path in files { let text = std::fs::read_to_string(&path).expect("读 shell 源文件"); for (index, line) in text.lines().enumerate() { for pattern in [".width(", ".height(", ".size(", ".spacing(", ".padding("] { let mut from = 0; while let Some(found) = line[from..].find(pattern) { let after = from + found + pattern.len(); if let Some(number) = size_literal(&line[after..]) { offenders.push(format!( "{}:{} {line:?} -> {pattern} 后面直接写了数字 {number}", path.display(), index + 1 )); } from = after; } } } } assert!( offenders.is_empty(), "骨架尺寸必须来自 LayoutMetrics / TypeStyle / Space:\n{}", offenders.join("\n") ); } /// 依赖行里出现 `slint` 就算违规(注释行放过:说明并存期要提到 slint)。 fn manifest_has_slint_dependency(manifest: &str) -> Vec { manifest .lines() .enumerate() .filter(|(_, line)| { let trimmed = line.trim_start(); !trimmed.starts_with('#') && trimmed.contains("slint") }) .map(|(index, line)| format!("Cargo.toml:{} {line}", index + 1)) .collect() } #[test] fn this_crate_never_depends_on_slint() { let manifest = std::fs::read_to_string(manifest_dir().join("Cargo.toml")).expect("读 Cargo.toml"); let offenders = manifest_has_slint_dependency(&manifest); assert!( offenders.is_empty(), "P4 的 iced 版 crate 不许依赖 slint(D26):\n{}", offenders.join("\n") ); for (path, text) in sources() { for (index, line) in text.lines().enumerate() { // 只查真代码里的 use/路径,文档里提 slint 是允许的(要解释并存期) let code = line.trim_start(); let is_import = code.starts_with("use ") || code.starts_with("extern crate"); if is_import && code.contains("slint") { panic!("{}:{} 引入了 slint:{line}", path.display(), index + 1); } } } } #[test] fn the_gate_itself_catches_a_planted_violation() { // 自检:把违规样本喂给同一套判定,必须被抓住(否则门禁是空转) assert!(hex_color_at(b" color: #6750A4,", 11).is_some()); assert!(hex_color_at(b"// placeholder #RRGGBB here", 15).is_none()); assert!(numeric_color_call("Color::from_rgb(0.1, 0.2, 0.3)").is_some()); assert!(numeric_color_call("Color::from_rgba8(color.r, color.g, color.b, color.a)").is_none()); assert!(numeric_color_call("color!(0x5865F2)").is_some()); assert!(size_literal("40.0)").is_some()); assert!(size_literal("0)").is_none()); assert!(size_literal("metrics.titlebar_height)").is_none()); assert!(!manifest_has_slint_dependency("slint = \"1.18\"").is_empty()); assert!(manifest_has_slint_dependency("# 不得依赖 slint").is_empty()); }