1use crate::{help_text, lint, output};
11use kenwood_thd75::types::{Band, BatteryLevel, PowerLevel};
12
13type Generator = fn() -> Vec<String>;
17
18const COVERAGE: &[(&str, Generator)] = &[
19 ("output::frequency", gen_frequency),
20 ("output::tuned_to", gen_tuned_to),
21 ("output::stepped_up", gen_stepped_up),
22 ("output::stepped_down", gen_stepped_down),
23 ("output::mode_read", gen_mode_read),
24 ("output::mode_set", gen_mode_set),
25 ("output::power_read", gen_power_read),
26 ("output::power_set", gen_power_set),
27 ("output::squelch_read", gen_squelch_read),
28 ("output::squelch_set", gen_squelch_set),
29 ("output::smeter", gen_smeter),
30 ("output::battery", gen_battery),
31 ("output::radio_model", gen_radio_model),
32 ("output::firmware_version", gen_firmware_version),
33 ("output::clock", gen_clock),
34 ("output::key_lock", gen_key_lock),
35 ("output::bluetooth", gen_bluetooth),
36 ("output::dual_band", gen_dual_band),
37 ("output::attenuator", gen_attenuator),
38 ("output::vox", gen_vox),
39 ("output::fm_radio", gen_fm_radio),
40 ("output::channel_read", gen_channel_read),
41 ("output::channels_summary", gen_channels_summary),
42 ("output::gps_config", gen_gps_config),
43 ("output::urcall_read", gen_urcall_read),
44 ("output::urcall_set", gen_urcall_set),
45 ("output::reflector_connected", gen_reflector_connected),
46 ("output::error", gen_error),
47 ("output::warning", gen_warning),
48 ("output::aprs_events", gen_aprs_events),
49 ("output::dstar_events", gen_dstar_events),
50 ("help_text::for_command", gen_help_text),
51];
52
53fn gen_frequency() -> Vec<String> {
54 let mut v = Vec::new();
55 for band in [Band::A, Band::B] {
56 for hz in [0u32, 146_520_000, 446_000_000, 1_200_000_000, 1_300_000_000] {
57 v.push(output::frequency(band, hz));
58 }
59 }
60 v
61}
62
63fn gen_tuned_to() -> Vec<String> {
64 vec![
65 output::tuned_to(Band::A, 146_520_000),
66 output::tuned_to(Band::B, 446_000_000),
67 ]
68}
69
70fn gen_stepped_up() -> Vec<String> {
71 vec![output::stepped_up(Band::A, 146_525_000)]
72}
73
74fn gen_stepped_down() -> Vec<String> {
75 vec![output::stepped_down(Band::A, 146_515_000)]
76}
77
78fn gen_mode_read() -> Vec<String> {
79 ["FM", "NFM", "AM", "DV", "LSB", "USB", "CW"]
80 .iter()
81 .map(|m| output::mode_read(Band::A, m))
82 .collect()
83}
84
85fn gen_mode_set() -> Vec<String> {
86 ["FM", "DV"]
87 .iter()
88 .map(|m| output::mode_set(Band::A, m))
89 .collect()
90}
91
92fn gen_power_read() -> Vec<String> {
93 [
94 PowerLevel::High,
95 PowerLevel::Medium,
96 PowerLevel::Low,
97 PowerLevel::ExtraLow,
98 ]
99 .iter()
100 .map(|p| output::power_read(Band::A, *p))
101 .collect()
102}
103
104fn gen_power_set() -> Vec<String> {
105 [PowerLevel::High, PowerLevel::Medium]
106 .iter()
107 .map(|p| output::power_set(Band::B, *p))
108 .collect()
109}
110
111fn gen_squelch_read() -> Vec<String> {
112 (0u8..=5)
113 .map(|l| output::squelch_read(Band::A, l))
114 .collect()
115}
116
117fn gen_squelch_set() -> Vec<String> {
118 (0u8..=5).map(|l| output::squelch_set(Band::A, l)).collect()
119}
120
121fn gen_smeter() -> Vec<String> {
122 ["S0", "S1", "S3", "S5", "S7", "S9"]
123 .iter()
124 .map(|r| output::smeter(Band::A, r))
125 .collect()
126}
127
128fn gen_battery() -> Vec<String> {
129 [
130 BatteryLevel::Empty,
131 BatteryLevel::OneThird,
132 BatteryLevel::TwoThirds,
133 BatteryLevel::Full,
134 BatteryLevel::Charging,
135 ]
136 .iter()
137 .map(|l| output::battery(*l))
138 .collect()
139}
140
141fn gen_radio_model() -> Vec<String> {
142 vec![output::radio_model("TH-D75")]
143}
144
145fn gen_firmware_version() -> Vec<String> {
146 vec![output::firmware_version("1.03")]
147}
148
149fn gen_clock() -> Vec<String> {
150 vec![output::clock("2026-04-10 14:32:07")]
151}
152
153fn gen_key_lock() -> Vec<String> {
154 vec![output::key_lock(true), output::key_lock(false)]
155}
156
157fn gen_bluetooth() -> Vec<String> {
158 vec![output::bluetooth(true), output::bluetooth(false)]
159}
160
161fn gen_dual_band() -> Vec<String> {
162 vec![output::dual_band(true), output::dual_band(false)]
163}
164
165fn gen_attenuator() -> Vec<String> {
166 vec![
167 output::attenuator(Band::A, true),
168 output::attenuator(Band::B, false),
169 ]
170}
171
172fn gen_vox() -> Vec<String> {
173 vec![
174 output::vox(true),
175 output::vox(false),
176 output::vox_set(true),
177 output::vox_gain_read(5),
178 output::vox_gain_set(7),
179 output::vox_delay_read(3),
180 output::vox_delay_set(4),
181 ]
182}
183
184fn gen_fm_radio() -> Vec<String> {
185 vec![
186 output::fm_radio(true),
187 output::fm_radio(false),
188 output::fm_radio_set(true),
189 ]
190}
191
192fn gen_channel_read() -> Vec<String> {
193 vec![
194 output::channel_read(0, 146_520_000),
195 output::channel_read(999, 446_000_000),
196 ]
197}
198
199fn gen_channels_summary() -> Vec<String> {
200 vec![
201 output::channels_summary(0),
202 output::channels_summary(1),
203 output::channels_summary(42),
204 ]
205}
206
207fn gen_gps_config() -> Vec<String> {
208 vec![
209 output::gps_config(true, true),
210 output::gps_config(false, false),
211 output::gps_config(true, false),
212 output::gps_config(false, true),
213 ]
214}
215
216fn gen_urcall_read() -> Vec<String> {
217 vec![
218 output::urcall_read("W1AW", ""),
219 output::urcall_read("W1AW", "P"),
220 ]
221}
222
223fn gen_urcall_set() -> Vec<String> {
224 vec![output::urcall_set("W1AW")]
225}
226
227fn gen_reflector_connected() -> Vec<String> {
228 vec![
229 output::reflector_connected("REF030", 'C'),
230 output::reflector_disconnected().to_string(),
231 ]
232}
233
234fn gen_error() -> Vec<String> {
235 vec![
236 output::error("invalid frequency"),
237 output::error("connection lost"),
238 ]
239}
240
241fn gen_warning() -> Vec<String> {
242 vec![output::warning("could not detect local time zone")]
243}
244
245fn gen_aprs_events() -> Vec<String> {
246 vec![
247 output::aprs_station_heard("W1AW"),
248 output::aprs_message_received("W1AW", "hi"),
249 output::aprs_message_delivered("1"),
250 output::aprs_message_rejected("2"),
251 output::aprs_message_expired("3"),
252 output::aprs_position("W1AW", 35.3, -82.46),
253 output::aprs_weather("W1AW"),
254 output::aprs_digipeated("W1AW"),
255 output::aprs_query_responded("W1AW"),
256 output::aprs_raw_packet("W1AW"),
257 output::aprs_mode_active().to_string(),
258 output::aprs_is_connected().to_string(),
259 output::aprs_is_incoming("W1AW>APRS:hello"),
260 output::aprs_stations_summary(5),
261 ]
262}
263
264fn gen_dstar_events() -> Vec<String> {
265 vec![
266 output::dstar_voice_start("W1AW", "P", "CQCQCQ"),
267 output::dstar_voice_start("W1AW", "", "W9ABC"),
268 output::dstar_voice_end().to_string(),
269 output::dstar_voice_lost().to_string(),
270 output::dstar_text_message("hello"),
271 output::dstar_gps(""),
272 output::dstar_station_heard("W1AW"),
273 output::dstar_command_cq().to_string(),
274 output::dstar_command_echo().to_string(),
275 output::dstar_command_unlink().to_string(),
276 output::dstar_command_info().to_string(),
277 output::dstar_command_link("REF030", 'C'),
278 output::dstar_command_callsign("W1AW"),
279 output::dstar_modem_status(5, false),
280 output::reflector_event_connected().to_string(),
281 output::reflector_event_voice_start("W1AW", "", "CQCQCQ"),
282 output::reflector_event_voice_end().to_string(),
283 ]
284}
285
286fn gen_help_text() -> Vec<String> {
287 let mut v = Vec::new();
288 for cmd in help_text::ALL_COMMANDS {
289 if let Some(text) = help_text::for_command(cmd) {
290 for line in text.lines() {
291 v.push(line.to_string());
292 }
293 }
294 }
295 v
296}
297
298#[must_use]
303#[allow(clippy::too_many_lines)]
304pub fn run() -> i32 {
305 println!(
306 "Accessibility compliance check, thd75-repl version {}.",
307 env!("CARGO_PKG_VERSION")
308 );
309 println!("Standard: WCAG 2.1 Level AA.");
310 println!("Standard: Section 508 of the US Rehabilitation Act.");
311 println!("Standard: CHI 2021 CLI accessibility paper.");
312 println!("Standard: EN 301 549 version 3.2.1.");
313 println!("Standard: ISO 9241-171.");
314 println!("Standard: ITU-T Recommendation F.790.");
315 println!("Standard: BRLTTY compatibility.");
316 println!("Standard: Handi-Ham Program recommendations.");
317 println!("Standard: ARRL accessibility resources.");
318 println!();
319
320 let mut rule_violations: std::collections::BTreeMap<lint::Rule, Vec<(String, String)>> =
322 std::collections::BTreeMap::new();
323 let mut rule_string_counts: std::collections::BTreeMap<lint::Rule, usize> =
324 std::collections::BTreeMap::new();
325 let mut total_strings: usize = 0;
326
327 for (source, generator) in COVERAGE {
328 let outputs = generator();
329 total_strings += outputs.len();
330 for s in &outputs {
331 if let Err(violations) = lint::check_output(s) {
332 for v in violations {
333 rule_violations
334 .entry(v.rule)
335 .or_default()
336 .push(((*source).to_string(), v.message));
337 }
338 }
339 }
340 }
341
342 let all_rules = [
343 lint::Rule::AsciiOnly,
344 lint::Rule::LineLength,
345 lint::Rule::NoAnsi,
346 lint::Rule::ErrorPrefix,
347 lint::Rule::WarningPrefix,
348 lint::Rule::ConfirmOnSet,
349 lint::Rule::ListCountSummary,
350 lint::Rule::LabelColonValue,
351 lint::Rule::BooleanWords,
352 lint::Rule::NoNakedPrint,
353 lint::Rule::NoCursorMoves,
354 lint::Rule::UnitsSpelledOut,
355 lint::Rule::NoAdHocTimestamps,
356 lint::Rule::StdoutStderrSeparation,
357 ];
358 for rule in all_rules {
359 let _ = rule_string_counts.insert(rule, total_strings);
360 }
361
362 let mut failed = 0usize;
363 for rule in all_rules {
364 let count = rule_string_counts.get(&rule).copied().unwrap_or(0);
365 if let Some(vs) = rule_violations.get(&rule) {
366 failed += 1;
367 println!(
368 "Rule {} {}: FAILED, {} violations, {count} strings checked.",
369 rule.id(),
370 rule.description(),
371 vs.len()
372 );
373 for (source, message) in vs.iter().take(5) {
374 println!(" Source: {source}. {message}");
375 }
376 } else {
377 println!(
378 "Rule {} {}: passed, {count} strings checked.",
379 rule.id(),
380 rule.description()
381 );
382 }
383 }
384
385 println!();
386 if failed == 0 {
387 println!(
388 "All 14 rules passed. {total_strings} strings checked from {} sources.",
389 COVERAGE.len()
390 );
391 0
392 } else {
393 println!(
394 "{failed} of 14 rules failed. {total_strings} strings checked from {} sources.",
395 COVERAGE.len()
396 );
397 1
398 }
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404
405 #[test]
406 fn run_exits_zero_on_clean_build() {
407 let code = run();
410 assert_eq!(code, 0, "check command reported violations");
411 }
412
413 #[test]
414 fn every_generator_produces_at_least_one_string() {
415 for (name, generator) in COVERAGE {
416 let outputs = generator();
417 assert!(!outputs.is_empty(), "generator {name} produced no outputs");
418 }
419 }
420}