Skip to main content

console_tests_rust/
lib.rs

1// Copyright 2026 The Fuchsia Authors
2//
3// Use of this source code is governed by a MIT-style
4// license that can be found in the LICENSE file or at
5// https://opensource.org/licenses/MIT
6
7#![no_std]
8// Allow missing safety doc comments for public unsafe extern FFI test helper functions.
9#![allow(clippy::missing_safety_doc)]
10#![allow(unused_crate_dependencies)]
11#![cfg(console_enabled)]
12
13use console_rust::{CMD_AVAIL_ALWAYS, CMD_AVAIL_NORMAL, CmdArgs, static_command};
14use core::ffi::c_int;
15use core::sync::atomic::{AtomicI32, Ordering};
16use zx_status::Status;
17
18macro_rules! zx_status {
19    ($status:expr) => {
20        $status.into_raw()
21    };
22}
23
24// FFI imports.
25unsafe extern "C" {
26    static __start_commands: console_rust::Cmd;
27    static __stop_commands: console_rust::Cmd;
28    fn rust_console_match_command(
29        name: *const core::ffi::c_char,
30        mask: u8,
31    ) -> *const console_rust::Cmd;
32    fn rust_console_get_echo() -> bool;
33    fn rust_console_get_exit() -> bool;
34    fn rust_console_set_exit(val: bool);
35    fn console_run_script_locked(string: *const core::ffi::c_char) -> c_int;
36}
37
38#[repr(C)]
39#[derive(Clone, Copy)]
40struct FILE {
41    write: unsafe extern "C" fn(
42        *mut core::ffi::c_void,
43        *const core::ffi::c_char,
44        usize,
45    ) -> core::ffi::c_int,
46    ptr: *mut core::ffi::c_void,
47}
48
49unsafe extern "C" {
50    static mut gStdout: FILE;
51}
52
53struct CapturerState {
54    buf: *mut u8,
55    buf_size: usize,
56    len: usize,
57    original_stdout: Option<FILE>,
58}
59
60struct SyncUnsafeCell<T>(core::cell::UnsafeCell<T>);
61
62// SAFETY: This is safe because we only run tests sequentially in the kernel console test thread.
63unsafe impl<T> Sync for SyncUnsafeCell<T> {}
64
65static CAPTURER_STATE: SyncUnsafeCell<CapturerState> =
66    SyncUnsafeCell(core::cell::UnsafeCell::new(CapturerState {
67        buf: core::ptr::null_mut(),
68        buf_size: 0,
69        len: 0,
70        original_stdout: None,
71    }));
72
73unsafe extern "C" fn write_callback(
74    _ptr: *mut core::ffi::c_void,
75    str_ptr: *const core::ffi::c_char,
76    len: usize,
77) -> core::ffi::c_int {
78    let state = unsafe { &mut *CAPTURER_STATE.0.get() };
79    if state.buf.is_null() || state.buf_size == 0 {
80        return 0;
81    }
82    let to_copy = core::cmp::min(len, state.buf_size - 1 - state.len);
83    if to_copy > 0 {
84        unsafe {
85            core::ptr::copy_nonoverlapping(str_ptr as *const u8, state.buf.add(state.len), to_copy);
86        }
87        state.len += to_copy;
88        unsafe { *state.buf.add(state.len) = 0 };
89    }
90    len as core::ffi::c_int
91}
92
93unsafe fn test_capture_stdout_start(buf: *mut u8, size: usize) {
94    let state = unsafe { &mut *CAPTURER_STATE.0.get() };
95    state.buf = buf;
96    state.buf_size = size;
97    state.len = 0;
98    unsafe { *state.buf = 0 };
99    unsafe {
100        state.original_stdout = Some(gStdout);
101        gStdout = FILE { write: write_callback, ptr: core::ptr::null_mut() };
102    }
103}
104
105unsafe fn test_capture_stdout_stop() {
106    let state = unsafe { &mut *CAPTURER_STATE.0.get() };
107    if let Some(orig) = state.original_stdout {
108        unsafe {
109            gStdout = orig;
110        }
111        state.original_stdout = None;
112    }
113    state.buf = core::ptr::null_mut();
114    state.buf_size = 0;
115    state.len = 0;
116}
117
118// Statically registered commands.
119static_command!(
120    TEST_CMD,
121    c"mock_success".as_ptr(),
122    c"mock_success help".as_ptr(),
123    mock_success_callback,
124    CMD_AVAIL_NORMAL
125);
126
127static_command!(
128    MOCK_FAILURE,
129    c"mock_failure".as_ptr(),
130    core::ptr::null(),
131    mock_failure_callback,
132    CMD_AVAIL_NORMAL
133);
134
135static_command!(
136    CMD_MOCK_ALWAYS,
137    c"mock_avail_always".as_ptr(),
138    c"mock_avail_always help".as_ptr(),
139    mock_success_callback,
140    CMD_AVAIL_ALWAYS
141);
142
143static MOCK_CALL_COUNT: AtomicI32 = AtomicI32::new(0);
144
145unsafe extern "C" fn mock_success_callback(
146    _argc: c_int,
147    _argv: *const CmdArgs,
148    _flags: u32,
149) -> c_int {
150    MOCK_CALL_COUNT.fetch_add(1, Ordering::Relaxed);
151    zx_status!(Status::OK)
152}
153
154unsafe extern "C" fn mock_failure_callback(
155    _argc: c_int,
156    _argv: *const CmdArgs,
157    _flags: u32,
158) -> c_int {
159    MOCK_CALL_COUNT.fetch_add(1, Ordering::Relaxed);
160    zx_status!(Status::INVALID_ARGS)
161}
162
163fn contains_command_help(output: &[u8], name: &[u8], help: &[u8]) -> bool {
164    output
165        .split(|&b| b == b'\n')
166        .any(|line| line.starts_with(b"\t") && line[1..].starts_with(name) && line.ends_with(help))
167}
168
169fn find_command_index(output: &[u8], name: &[u8]) -> Option<usize> {
170    for (idx, line) in output.split(|&b| b == b'\n').enumerate() {
171        if line.starts_with(b"\t") && line[1..].starts_with(name) {
172            return Some(idx);
173        }
174    }
175    None
176}
177
178/// Tests for the kernel console.
179#[cfg(ktest)]
180#[unittest::test_suite(name = "console_rust")]
181mod console_tests {
182    use unittest::{
183        assert_eq, assert_nonnull, expect_eq, expect_false, expect_lt, expect_ne, expect_true,
184    };
185
186    /// Verify size and alignment compatibility with C++ structs.
187    #[test]
188    fn console_abi_test() {
189        assert_eq!(core::mem::size_of::<console_rust::CmdArgs>(), 40);
190        assert_eq!(core::mem::align_of::<console_rust::CmdArgs>(), 8);
191        assert_eq!(core::mem::size_of::<console_rust::Cmd>(), 32);
192        assert_eq!(core::mem::align_of::<console_rust::Cmd>(), 8);
193    }
194
195    /// Test echo command callback in Rust
196    #[test]
197    fn command_echo_test() {
198        let original_echo = unsafe { rust_console_get_echo() };
199
200        // Set echo setting to false.
201        let res = unsafe { console_run_script_locked(c"echo false".as_ptr()) };
202        expect_eq!(res, zx_status!(Status::OK));
203        expect_false!(unsafe { rust_console_get_echo() });
204
205        // Set echo setting to true.
206        let res = unsafe { console_run_script_locked(c"echo true".as_ptr()) };
207        expect_eq!(res, zx_status!(Status::OK));
208        expect_true!(unsafe { rust_console_get_echo() });
209
210        // Restore original.
211        let cmd = if original_echo { c"echo true" } else { c"echo false" };
212        unsafe {
213            console_run_script_locked(cmd.as_ptr());
214        }
215    }
216
217    /// Test exit command callback in Rust
218    #[test]
219    fn command_exit_test() {
220        let original_exit = unsafe { rust_console_get_exit() };
221
222        unsafe {
223            rust_console_set_exit(false);
224        }
225        let res = unsafe { console_run_script_locked(c"exit".as_ptr()) };
226        expect_eq!(res, zx_status!(Status::OK));
227        expect_false!(unsafe { rust_console_get_exit() });
228
229        // Restore.
230        unsafe {
231            rust_console_set_exit(original_exit);
232        }
233    }
234
235    /// Test boot-test-success command callback in Rust
236    #[test]
237    fn command_boot_test_success_test() {
238        // Test success.
239        unsafe {
240            console_run_script_locked(c"mock_success".as_ptr());
241        }
242        let res = unsafe { console_run_script_locked(c"boot-test-success".as_ptr()) };
243        expect_eq!(res, zx_status!(Status::OK));
244
245        // Test failure.
246        let some_failure = unsafe { console_run_script_locked(c"mock_failure".as_ptr()) };
247        let res = unsafe { console_run_script_locked(c"boot-test-success".as_ptr()) };
248        expect_eq!(res, some_failure);
249
250        // Restore to success state.
251        unsafe {
252            console_run_script_locked(c"mock_success".as_ptr());
253        }
254    }
255
256    /// Test and command callback in Rust
257    #[test]
258    fn command_and_test() {
259        // If lastresult != zx_status!(Status::OK), it should return lastresult immediately.
260        let some_failure = unsafe { console_run_script_locked(c"mock_failure".as_ptr()) };
261        let res = unsafe { console_run_script_locked(c"and mock_success".as_ptr()) };
262        expect_eq!(res, some_failure);
263
264        // If lastresult == zx_status!(Status::OK), it should execute the command.
265        unsafe {
266            console_run_script_locked(c"mock_success".as_ptr());
267        }
268        let res = unsafe { console_run_script_locked(c"and mock_success".as_ptr()) };
269        expect_eq!(res, zx_status!(Status::OK));
270    }
271
272    /// Test repeat command callback in Rust
273    #[test]
274    fn command_repeat_test() {
275        MOCK_CALL_COUNT.store(0, Ordering::Relaxed);
276
277        // Repeat mock_success.
278        let res = unsafe { console_run_script_locked(c"repeat 3 mock_success".as_ptr()) };
279        expect_eq!(res, zx_status!(Status::OK));
280        expect_eq!(MOCK_CALL_COUNT.load(Ordering::Relaxed), 3);
281
282        MOCK_CALL_COUNT.store(0, Ordering::Relaxed);
283
284        // Repeat with early failure.
285        let res = unsafe { console_run_script_locked(c"repeat 3 mock_failure".as_ptr()) };
286        expect_ne!(res, zx_status!(Status::OK));
287        expect_eq!(MOCK_CALL_COUNT.load(Ordering::Relaxed), 1);
288    }
289
290    /// Test help command callback in Rust (normal)
291    #[test]
292    fn command_help_normal_test() {
293        let cmd_ptr = unsafe { rust_console_match_command(c"help".as_ptr(), 0xff) };
294        assert_nonnull!(cmd_ptr);
295        let cb = unsafe { (*cmd_ptr).cmd_callback };
296
297        // Statically allocate the capture buffer to avoid kernel stack overflows.
298        static BUF: SyncUnsafeCell<[u8; 4096]> =
299            SyncUnsafeCell(core::cell::UnsafeCell::new([0; 4096]));
300        let buf_ptr = BUF.0.get() as *mut u8;
301
302        // Reset buffer and start capturing stdout.
303        unsafe {
304            core::ptr::write_bytes(buf_ptr, 0, 4096);
305            test_capture_stdout_start(buf_ptr, 4096);
306        }
307        // Execute command and stop capturing stdout.
308        let res = unsafe { cb(1, core::ptr::null(), 0) };
309        unsafe {
310            test_capture_stdout_stop();
311        }
312
313        expect_eq!(res, zx_status!(Status::OK));
314
315        // Slice the buffer up to the null terminator.
316        let output = unsafe { core::slice::from_raw_parts(buf_ptr, 4096) };
317        let len = output.iter().position(|&b| b == b'\0').unwrap_or(output.len());
318        let output = &output[..len];
319
320        expect_true!(contains_command_help(
321            output,
322            b"mock_avail_always",
323            b"mock_avail_always help"
324        ));
325        expect_true!(contains_command_help(output, b"mock_success", b"mock_success help"));
326
327        // Verify alphabetical sorting: mock_avail_always (a) comes before mock_success (s).
328        let always_idx = find_command_index(output, b"mock_avail_always");
329        let success_idx = find_command_index(output, b"mock_success");
330
331        match (always_idx, success_idx) {
332            (Some(a), Some(s)) => {
333                expect_lt!(a, s);
334            }
335            _ => {
336                record_failure!();
337            }
338        }
339    }
340
341    /// Test help command callback in Rust (panic)
342    #[test]
343    fn command_help_panic_test() {
344        let cmd_ptr = unsafe { rust_console_match_command(c"help".as_ptr(), 0xff) };
345        assert_nonnull!(cmd_ptr);
346        let cb = unsafe { (*cmd_ptr).cmd_callback };
347
348        // Statically allocate the capture buffer to avoid kernel stack overflows.
349        static BUF: SyncUnsafeCell<[u8; 4096]> =
350            SyncUnsafeCell(core::cell::UnsafeCell::new([0; 4096]));
351        let buf_ptr = BUF.0.get() as *mut u8;
352
353        // Reset buffer and start capturing stdout.
354        unsafe {
355            core::ptr::write_bytes(buf_ptr, 0, 4096);
356            test_capture_stdout_start(buf_ptr, 4096);
357        }
358        // Execute command and stop capturing stdout.
359        let res = unsafe { cb(1, core::ptr::null(), console_rust::CMD_FLAG_PANIC) };
360        unsafe {
361            test_capture_stdout_stop();
362        }
363
364        expect_eq!(res, zx_status!(Status::OK));
365
366        // Slice the buffer up to the null terminator.
367        let output = unsafe { core::slice::from_raw_parts(buf_ptr, 4096) };
368        let len = output.iter().position(|&b| b == b'\0').unwrap_or(output.len());
369        let output = &output[..len];
370
371        // mock_avail_always is still printed.
372        expect_true!(contains_command_help(
373            output,
374            b"mock_avail_always",
375            b"mock_avail_always help"
376        ));
377
378        // mock_success (CMD_AVAIL_NORMAL) should NOT be printed in panic mode.
379        expect_false!(contains_command_help(output, b"mock_success", b"mock_success help"));
380    }
381}
382
383// Verifies that the test command is registered correctly from the Rust side.
384// We must expose this via FFI to ensure the C++ tests runner links this file.
385// The alternative would be to move these tests directly into
386// `zircon/kernel/lib/console/rust/src/lib.rs`, but keeping them in a separate
387// test crate avoids cluttering the main library with mocks and helpers.
388#[unsafe(no_mangle)]
389pub unsafe extern "C" fn command_visibility_from_rust_test() -> bool {
390    unsafe {
391        let start = &__start_commands as *const console_rust::Cmd;
392        let stop = &__stop_commands as *const console_rust::Cmd;
393        let len = stop.offset_from(start) as usize;
394        let commands = core::slice::from_raw_parts(start, len);
395
396        for cmd in commands {
397            if core::ffi::CStr::from_ptr(cmd.cmd_str) == c"mock_success" {
398                if core::ffi::CStr::from_ptr(cmd.help_str) != c"mock_success help" {
399                    return false;
400                }
401                if cmd.availability_mask != CMD_AVAIL_NORMAL {
402                    return false;
403                }
404                return true;
405            }
406        }
407    }
408    false
409}