返回 DeepSeek-TUI-2026
palette_audit.rs
根目录 / crates / tui / tests / palette_audit.rs
1 //! Palette audit tests to prevent color drift.
2 //!
3 //! These tests ensure that deprecated colors (like DEEPSEEK_AQUA) are not used
4 //! directly in user-visible code. The palette should only use DeepSeek brand
5 //! colors: blue, sky, red (plus neutral shades).
6
7 use std::fs;
8 use std::path::Path;
9
10 use ratatui::style::Color;
11
12 #[path = "../src/palette.rs"]
13 #[allow(dead_code)]
14 mod palette;
15
16 const DEPRECATED_DIRECT_COLORS: &[&str] = &["DEEPSEEK_AQUA"];
17 const ALLOWED_PATTERNS: &[&str] = &["pub const DEEPSEEK_AQUA", "DEEPSEEK_AQUA_RGB"];
18
19 fn color_to_rgb(color: Color) -> (u8, u8, u8) {
20 match color {
21 Color::Rgb(r, g, b) => (r, g, b),
22 Color::Black => (0, 0, 0),
23 Color::White => (255, 255, 255),
24 Color::Gray => (128, 128, 128),
25 Color::DarkGray => (169, 169, 169),
26 Color::Red => (255, 0, 0),
27 Color::LightRed => (255, 102, 102),
28 Color::Green => (0, 255, 0),
29 Color::LightGreen => (102, 255, 102),
30 Color::Yellow => (255, 255, 0),
31 Color::LightYellow => (255, 255, 153),
32 Color::Blue => (0, 0, 255),
33 Color::LightBlue => (102, 153, 255),
34 Color::Magenta => (255, 0, 255),
35 Color::LightMagenta => (255, 153, 255),
36 Color::Cyan => (0, 255, 255),
37 Color::LightCyan => (153, 255, 255),
38 _ => panic!("unsupported color variant for contrast test: {:?}", color),
39 }
40 }
41
42 fn linearize_srgb(component: u8) -> f64 {
43 let srgb = f64::from(component) / 255.0;
44 if srgb <= 0.04045 {
45 srgb / 12.92
46 } else {
47 ((srgb + 0.055) / 1.055).powf(2.4)
48 }
49 }
50
51 fn relative_luminance(color: Color) -> f64 {
52 let (r, g, b) = color_to_rgb(color);
53 0.2126 * linearize_srgb(r) + 0.7152 * linearize_srgb(g) + 0.0722 * linearize_srgb(b)
54 }
55
56 fn contrast_ratio(foreground: Color, background: Color) -> f64 {
57 let fg = relative_luminance(foreground);
58 let bg = relative_luminance(background);
59 if fg >= bg {
60 (fg + 0.05) / (bg + 0.05)
61 } else {
62 (bg + 0.05) / (fg + 0.05)
63 }
64 }
65
66 fn assert_min_contrast(label: &str, foreground: Color, background: Color, min_ratio: f64) {
67 let ratio = contrast_ratio(foreground, background);
68 assert!(
69 ratio >= min_ratio,
70 "{label} contrast {ratio:.2} is below minimum {min_ratio:.2}"
71 );
72 }
73
74 fn audit_file(path: &Path, violations: &mut Vec<String>) {
75 let content = match fs::read_to_string(path) {
76 Ok(c) => c,
77 Err(_) => return,
78 };
79
80 for (line_num, line) in content.lines().enumerate() {
81 for deprecated in DEPRECATED_DIRECT_COLORS {
82 let pattern = format!("palette::{}", deprecated);
83 if line.contains(&pattern) {
84 let is_allowed = ALLOWED_PATTERNS.iter().any(|p| line.contains(p));
85 if !is_allowed {
86 violations.push(format!(
87 "{}:{}: direct use of {} (use semantic alias instead)",
88 path.display(),
89 line_num + 1,
90 deprecated
91 ));
92 }
93 }
94 }
95 }
96 }
97
98 fn audit_directory(dir: &Path, violations: &mut Vec<String>) {
99 let entries = match fs::read_dir(dir) {
100 Ok(e) => e,
101 Err(_) => return,
102 };
103
104 for entry in entries.flatten() {
105 let path = entry.path();
106 if path.is_dir() {
107 audit_directory(&path, violations);
108 } else if path.extension().is_some_and(|e| e == "rs") {
109 if path.file_name().is_some_and(|n| n == "palette.rs") {
110 continue;
111 }
112 audit_file(&path, violations);
113 }
114 }
115 }
116
117 #[test]
118 fn audit_no_direct_aqua_usage() {
119 let manifest_dir = env!("CARGO_MANIFEST_DIR");
120 let src_dir = Path::new(manifest_dir).join("src");
121 let mut violations = Vec::new();
122
123 audit_directory(&src_dir, &mut violations);
124
125 if !violations.is_empty() {
126 let report = violations.join("\n");
127 panic!(
128 "Palette audit failed! Found {} direct uses of deprecated colors:\n{}",
129 violations.len(),
130 report
131 );
132 }
133 }
134
135 #[test]
136 fn verify_status_success_uses_sky() {
137 let manifest_dir = env!("CARGO_MANIFEST_DIR");
138 let palette_path = Path::new(manifest_dir).join("src/palette.rs");
139 let content = fs::read_to_string(&palette_path).expect("Failed to read palette.rs");
140
141 assert!(
142 content.contains("pub const STATUS_SUCCESS: Color = DEEPSEEK_SKY;"),
143 "STATUS_SUCCESS should use DEEPSEEK_SKY, not DEEPSEEK_AQUA"
144 );
145 }
146
147 #[test]
148 fn verify_brand_colors_defined() {
149 let manifest_dir = env!("CARGO_MANIFEST_DIR");
150 let palette_path = Path::new(manifest_dir).join("src/palette.rs");
151 let content = fs::read_to_string(&palette_path).expect("Failed to read palette.rs");
152
153 assert!(
154 content.contains("DEEPSEEK_BLUE_RGB: (u8, u8, u8) = (53, 120, 229);"),
155 "DEEPSEEK_BLUE should be #3578E5"
156 );
157 assert!(
158 content.contains("DEEPSEEK_SKY_RGB: (u8, u8, u8) = (106, 174, 242);"),
159 "DEEPSEEK_SKY should be #6AAEF2"
160 );
161 assert!(
162 content.contains("DEEPSEEK_RED_RGB: (u8, u8, u8) = (226, 80, 96);"),
163 "DEEPSEEK_RED should be #E25060"
164 );
165 }
166
167 #[test]
168 fn contrast_guardrails_for_key_ui_pairs() {
169 let min_readable = 4.5;
170
171 assert_min_contrast(
172 "TEXT_BODY on DEEPSEEK_INK",
173 palette::TEXT_BODY,
174 palette::DEEPSEEK_INK,
175 min_readable,
176 );
177 assert_min_contrast(
178 "TEXT_SECONDARY on DEEPSEEK_INK",
179 palette::TEXT_SECONDARY,
180 palette::DEEPSEEK_INK,
181 min_readable,
182 );
183 assert_min_contrast(
184 "TEXT_HINT on DEEPSEEK_INK",
185 palette::TEXT_HINT,
186 palette::DEEPSEEK_INK,
187 min_readable,
188 );
189 assert_min_contrast(
190 "STATUS_WARNING on DEEPSEEK_INK",
191 palette::STATUS_WARNING,
192 palette::DEEPSEEK_INK,
193 min_readable,
194 );
195 assert_min_contrast(
196 "STATUS_ERROR on DEEPSEEK_INK",
197 palette::STATUS_ERROR,
198 palette::DEEPSEEK_INK,
199 min_readable,
200 );
201 }
202
202 lines RUST