Skip to main content

tag_test/
tag-test.rs

1//! `tag-test`: set the RTC and execute tag self-tests.
2//!
3//! Port of `host/commandline/test.cc`. Prints the tag's information, its
4//! clock drift before and after an RTC sync, its state and state log, then,
5//! if the tag is IDLE, runs the requested self-test (`--test`, by TestReq
6//! name or number) and polls until the tag returns to IDLE or
7//! `--test-timeout` expires, and prints the test result. The RTC is synced
8//! again afterwards, since a test may alter it.
9//!
10//! Usage: `tag-test [-d] [-b BUS:DEVICE] [-t|--test NAME|NUMBER]
11//! [--test-timeout S]` (defaults `RUN_ALL` and 30 s). Status is polled once
12//! a second while the test runs. Clock drift prints in seconds (tag minus
13//! host), voltage in volts.
14//!
15//! # Exit status
16//!
17//! 0 when the test finished with `ALL_PASSED` and the clock was re-synced.
18//! 1 for a test that finished with any other `TestResult` (after printing it
19//! and re-syncing the clock), an unknown test, a failed attach, info or
20//! status read or clock sync, a tag that is not IDLE, a refused test request,
21//! or a test that does not finish within `--test-timeout`. Option errors
22//! exit 2 (clap).
23//!
24//! # Hardware side effects
25//!
26//! The attach resets the tag, its clock is set from the host twice, and the
27//! self-test exercises the hardware. On a tag with external NAND, the
28//! external-flash test can write the NAND bad-block map. Run it after every
29//! flash, before measuring anything (step 4 of the flashing protocol in
30//! `AGENTS.md`).
31
32use std::process::ExitCode;
33use std::thread::sleep;
34use std::time::{Duration, SystemTime, UNIX_EPOCH};
35
36use clap::Parser;
37use tag_tools::{BaseArgs, enum_name, exit_on_interrupt, init_logging, select_base, state_name};
38use tagcore::Tag;
39use tagcore::proto::{TagState, TestReq, TestResult, state};
40
41/// sets the RTC and executes tag self-tests
42#[derive(Parser)]
43#[command(name = "tag-test")]
44struct Cli {
45    /// The shared `--debug` and `--base` options.
46    #[command(flatten)]
47    base: BaseArgs,
48
49    /// Self-test to run by TestReq name or number
50    #[arg(short, long, default_value = "RUN_ALL")]
51    test: String,
52
53    /// Seconds to wait for the tag to return to IDLE before giving up. A test
54    /// that has not finished has no result to report, so this is a give-up
55    /// time, not a pass/fail threshold. Raise it for slower tests or tags
56    #[arg(long, default_value_t = 30, value_name = "SECONDS")]
57    test_timeout: i32,
58}
59
60/// Parses an integer as `std::stoi(s, &parsed, 0)` does (leading
61/// whitespace, optional sign, `0x` hex or leading-`0` octal), requiring the
62/// whole string to be consumed and the value to fit an `int`.
63fn parse_c_int(text: &str) -> Option<i32> {
64    let s = text.trim_start();
65    let (negative, s) = match s.as_bytes().first() {
66        Some(b'-') => (true, &s[1..]),
67        Some(b'+') => (false, &s[1..]),
68        _ => (false, s),
69    };
70    let (radix, digits) = if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
71        (16, hex)
72    } else if s.len() > 1 && s.starts_with('0') {
73        (8, &s[1..])
74    } else {
75        (10, s)
76    };
77    if digits.is_empty() || !digits.chars().all(|c| c.is_digit(radix)) {
78        return None;
79    }
80    let magnitude = i64::from_str_radix(digits, radix).ok()?;
81    i32::try_from(if negative { -magnitude } else { magnitude }).ok()
82}
83
84/// A TestReq by protobuf name, or by a number that names a valid value.
85fn parse_test_req(name: &str) -> Option<TestReq> {
86    TestReq::from_str_name(name)
87        .or_else(|| parse_c_int(name).and_then(|v| TestReq::try_from(v).ok()))
88}
89
90/// The tag clock's offset from the host clock in seconds (tag minus host),
91/// echoing any firmware debug message to stderr. `None` if status failed.
92fn rtc_drift(tag: &Tag) -> Option<f32> {
93    let status = tag.status().ok()?;
94    if !status.debug_message.is_empty() {
95        eprint!("{}", status.debug_message);
96    }
97    let now_ms = SystemTime::now()
98        .duration_since(UNIX_EPOCH)
99        .unwrap_or_default()
100        .as_millis() as i64;
101    Some(((status.millis - now_ms) as f64 / 1000.0) as f32)
102}
103
104/// Attaches, prints the tag's information and clock drift, sets the clock,
105/// prints the state log, runs the self-test and reports its result.
106fn main() -> ExitCode {
107    let cli = Cli::parse();
108    init_logging(&cli.base);
109
110    let tag = Tag::new();
111    let dev = select_base::<Cli>(&tag, &cli.base);
112    let Some(requested_test) = parse_test_req(&cli.test) else {
113        eprintln!("Invalid test: {}", cli.test);
114        return ExitCode::FAILURE;
115    };
116    if !tag.attach(&dev) {
117        eprintln!("Attach failed");
118        return ExitCode::FAILURE;
119    }
120    exit_on_interrupt();
121
122    let info = match tag.tag_info() {
123        Ok(info) => info,
124        Err(_) => {
125            eprintln!("GetTagInfo failed: {}", tag.debug_message());
126            return ExitCode::FAILURE;
127        }
128    };
129
130    // The C++ sets std::fixed with precision 2 for floats; tag_type prints
131    // as its number.
132    println!("Tag Type: {}", info.tag_type);
133    println!("Board name: {}", info.board_desc);
134    println!("Build time and date: {}", info.build_time);
135    println!("Internal Flash size: {}kb", info.intflashsz);
136    println!("External Flash size: {}kb", info.extflashsz);
137    println!("Repo: {}", info.gitrepo);
138    println!("Repo hash: {}", info.githash);
139    println!("UUID: {}", info.uuid);
140    if let Some(v) = tag.voltage() {
141        println!("Voltage: {v:.2}");
142    }
143
144    if let Some(f) = rtc_drift(&tag) {
145        println!("Initial Clock drift: {f:.2}");
146    }
147    println!("#  Checking RTC (2 second delay)");
148    if tag.set_rtc().is_err() {
149        eprintln!("SetRtc failed: {}", tag.debug_message());
150        return ExitCode::FAILURE;
151    }
152    sleep(Duration::from_millis(2000));
153    if let Some(f) = rtc_drift(&tag) {
154        println!("Clock drift after setting: {f:.2}");
155    }
156
157    let mut status = match tag.status() {
158        Ok(status) => status,
159        Err(_) => {
160            eprintln!("GetStatus failed: {}", tag.debug_message());
161            return ExitCode::FAILURE;
162        }
163    };
164    if !status.debug_message.is_empty() {
165        eprint!("{}", status.debug_message);
166    }
167    println!("State: {}", state_name(status.state));
168
169    let mut next = 0;
170    while let Ok(log) = tag.state_log(next) {
171        if log.states.is_empty() {
172            break;
173        }
174        next += log.states.len() as i32;
175        for marker in &log.states {
176            let s = marker.status.clone().unwrap_or_default();
177            println!("State: {}", state_name(s.state));
178            println!(
179                "{}",
180                enum_name(marker.transition_reason, state::Event::as_str_name)
181            );
182        }
183    }
184
185    if status.state != TagState::Idle as i32 {
186        // No test ran, so there is no result.
187        println!("#  State not idle ! Exiting");
188        return ExitCode::FAILURE;
189    }
190
191    println!("#  Running test {}", requested_test.as_str_name());
192    // A result is only meaningful if the request was accepted and the
193    // status reporting it was actually read.
194    if tag.test(requested_test).is_err() {
195        eprintln!("Test request failed: {}", tag.debug_message());
196        return ExitCode::FAILURE;
197    }
198    // The tag returns to IDLE when the test is finished; until then
199    // test_status is TEST_RUNNING, a state rather than a verdict.
200    let test_poll_s = cli.test_timeout;
201    let mut status_read = false;
202    let mut completed = false;
203    for _ in 0..test_poll_s.max(0) {
204        sleep(Duration::from_millis(1000));
205        match tag.status() {
206            Ok(s) => status = s,
207            Err(_) => {
208                eprintln!("GetStatus failed: {}", tag.debug_message());
209                continue;
210            }
211        }
212        status_read = true;
213        if !status.debug_message.is_empty() {
214            eprint!("{}", status.debug_message);
215        }
216        if status.state == TagState::Idle as i32 {
217            completed = true;
218            break;
219        }
220    }
221    if !status_read {
222        eprintln!("No status was read after the test; not reporting a result");
223        return ExitCode::FAILURE;
224    }
225    if !completed {
226        eprintln!(
227            "Test did not finish within {test_poll_s} s; last state {}, no result to report. Use \
228             --test-timeout to allow longer.",
229            state_name(status.state)
230        );
231        return ExitCode::FAILURE;
232    }
233    println!(
234        "Test Result: {}",
235        enum_name(status.test_status, TestResult::as_str_name)
236    );
237    if tag.set_rtc().is_err() {
238        // reset rtc since test may alter
239        eprintln!("SetRtc after test failed: {}", tag.debug_message());
240        return ExitCode::FAILURE;
241    }
242    // A test that ran and failed is a failure: scripts read the exit status.
243    if status.test_status != TestResult::AllPassed as i32 {
244        eprintln!(
245            "Self-test failed: {}",
246            enum_name(status.test_status, TestResult::as_str_name)
247        );
248        return ExitCode::FAILURE;
249    }
250    ExitCode::SUCCESS
251}
252
253#[cfg(test)]
254/// Unit tests for self-test selection.
255mod tests {
256    use super::*;
257
258    #[test]
259    /// Checks names, decimal, hex and octal numbers, and rejects numbers that
260    /// name no test or carry trailing text.
261    fn test_req_parses_names_and_numbers() {
262        assert_eq!(parse_test_req("RUN_ALL"), Some(TestReq::RunAll));
263        assert_eq!(parse_test_req("2"), Some(TestReq::RunRtc));
264        assert_eq!(parse_test_req("0x0e"), Some(TestReq::RunMx25u12843));
265        assert_eq!(parse_test_req("016"), Some(TestReq::RunMx25u12843));
266        assert_eq!(parse_test_req("9"), None);
267        assert_eq!(parse_test_req("2x"), None);
268        assert_eq!(parse_test_req("bogus"), None);
269        assert_eq!(parse_test_req("0x-2"), None);
270    }
271}