From de6ef959ce22ab8095ea6b47ba5350311adbee58 Mon Sep 17 00:00:00 2001 From: Julien THILLARD Date: Fri, 13 Mar 2026 11:15:52 +0100 Subject: [PATCH] Start stdin --- assets/cursor.png | Bin 0 -> 249 bytes crates/bytes-struct/Cargo.toml | 1 + crates/bytes-struct/src/lib.rs | 87 +++++++++++-------------- crates/kernel-macros/Cargo.toml | 1 + crates/kernel-macros/src/image.rs | 2 +- crates/kernel-macros/src/lib.rs | 4 +- crates/shared/src/syscall.rs | 9 +++ src/cursor.rs | 38 +++++++++++ src/data_structures.rs | 1 + src/data_structures/circular_buffer.rs | 55 ++++++++++++++++ src/draw.rs | 4 +- src/interrupt.rs | 10 +++ src/main.rs | 22 ++++--- src/process.rs | 8 ++- src/virtual_fs.rs | 10 ++- src/virtual_fs/keyboard.rs | 85 ++++++++++++++++++++++++ src/virtual_fs/stdin.rs | 67 +++++++++++++++++++ user/test_pic/src/main.rs | 15 +++-- 18 files changed, 347 insertions(+), 72 deletions(-) create mode 100644 assets/cursor.png create mode 100644 src/cursor.rs create mode 100644 src/data_structures.rs create mode 100644 src/data_structures/circular_buffer.rs create mode 100644 src/virtual_fs/keyboard.rs create mode 100644 src/virtual_fs/stdin.rs diff --git a/assets/cursor.png b/assets/cursor.png new file mode 100644 index 0000000000000000000000000000000000000000..ef32156713fd6982440bba378ad26fe2c1ada74d GIT binary patch literal 249 zcmeAS@N?(olHy`uVBq!ia0vp^96-#)!3HEdkIOdzDaPU;cPGZ1Cw1z99F}xPUq=Rp zjs4tz5?O(Kg=CK)Uj~LMH3o);76yi2K%s^g3=E|P3=FRl7#OT(FffQ0%-I!a1C(G( z@^*J&_z!{$_AZ|c6yYrJh%9Dc;1&j9Muu5)B!GhKC7!;n?5~)4I7GO%iCWwT3fX$P zIEF}EuDx)OkHLV2`NFoF|Nj@2-%@hA`$1_z?&Kq4OboXrRhG>1@z{G|;2;R>(qcW)Lt;GI8}3;G4Px+g^>bP0l+XkKhG0my literal 0 HcmV?d00001 diff --git a/crates/bytes-struct/Cargo.toml b/crates/bytes-struct/Cargo.toml index 4c319eb..44de9ef 100644 --- a/crates/bytes-struct/Cargo.toml +++ b/crates/bytes-struct/Cargo.toml @@ -10,3 +10,4 @@ proc-macro = true proc-macro2 = "1" quote = "1" syn = { version = "2", features = ["full"] } +zyn = "0.5" diff --git a/crates/bytes-struct/src/lib.rs b/crates/bytes-struct/src/lib.rs index 92f4aeb..41b1db9 100644 --- a/crates/bytes-struct/src/lib.rs +++ b/crates/bytes-struct/src/lib.rs @@ -1,27 +1,44 @@ use core::iter::Iterator; -use proc_macro::TokenStream; -use quote::{format_ident, quote}; -use syn::{Meta, parse_macro_input, spanned::Spanned}; +use syn::{Meta, spanned::Spanned}; +use zyn::zyn; -#[proc_macro_derive(VolatilePackedStruct, attributes(read_only))] -pub fn derive_volatile_packed_struct(item: TokenStream) -> TokenStream { - let input = parse_macro_input!(item as syn::DeriveInput); +#[zyn::element(debug)] +fn struct_getter(field: syn::Field) -> zyn::TokenStream { + let field_name = field.ident.as_ref().unwrap(); + zyn! { + pub fn {{ field_name | ident:"get_{}" }}(self: *const Self) -> {{ field.ty }} { + unsafe { + (&raw const (*self).{{ field_name }}).read_volatile() + } + } + } +} +#[zyn::element] +fn struct_setter(field: syn::Field) -> zyn::TokenStream { + let field_name = field.ident.as_ref().unwrap(); + + zyn! { + pub fn {{ field_name | ident:"set_{}" }}(self: *mut Self, value: {{ field.ty }}) { + unsafe { + (&raw mut (*self).{{ field_name }}).write_volatile(value); + } + } + } +} + +#[zyn::derive("VolatilePackedStruct", attributes(read_only), debug)] +pub fn derive_volatile_packed_struct(#[zyn(input)] input: syn::DeriveInput) -> TokenStream { if !input.attrs.iter().any(|attr| match &attr.meta { Meta::List(list) => list.path.segments.last().is_some_and(|s| s.ident == "repr"), _ => false, }) { - return syn::Error::new( - input.span(), - "Item should use a #[repr(packed)] representation", - ) - .into_compile_error() - .into(); + bail!("Item should use a #[repr(packed)] representation"); } - let fields = match input.data { - syn::Data::Struct(data_struct) => match data_struct.fields { + let fields = match &input.data { + syn::Data::Struct(data_struct) => match &data_struct.fields { syn::Fields::Named(fields_named) => fields_named, syn::Fields::Unnamed(_) | syn::Fields::Unit => unimplemented!(), }, @@ -29,42 +46,16 @@ pub fn derive_volatile_packed_struct(item: TokenStream) -> TokenStream { syn::Data::Union(_) => unimplemented!(), }; - let mut getters = Vec::new(); - let mut setters = Vec::new(); - - for field in fields.named { - let field_name = field.ident.unwrap(); - if field_name.to_string().starts_with("_") { - continue; - } - let ty = field.ty; - let getter_name = format_ident!("get_{}", field_name); - let setter_name = format_ident!("set_{}", field_name); - - getters.push(quote! { - pub fn #getter_name(self: *const Self) -> #ty { - unsafe { - (&raw const (*self).#field_name).read_volatile() - } - } - }); - setters.push(quote! { - pub fn #setter_name(self: *mut Self, value: #ty) { - unsafe { - (&raw mut (*self).#field_name).write_volatile(value); - } - } - }); - } - - let struct_path = input.ident; let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl(); - quote! { - impl #impl_generics #struct_path #type_generics #where_clause { - #(#getters)* - #(#setters)* + zyn! { + impl {{ impl_generics }} {{ input.ident }} {{ type_generics }} {{ where_clause }} { + @for (field in &fields.named) { + @if (!field.ident.as_ref().unwrap().to_string().starts_with("_")) { + @struct_getter(field = field.clone()) + @struct_setter(field = field.clone()) + } + } } } - .into() } diff --git a/crates/kernel-macros/Cargo.toml b/crates/kernel-macros/Cargo.toml index 0856fbc..cee8d5f 100644 --- a/crates/kernel-macros/Cargo.toml +++ b/crates/kernel-macros/Cargo.toml @@ -12,3 +12,4 @@ regex = "1" proc-macro2 = "1" quote = "1" syn = { version = "2", features = ["full"] } +zyn = "0.5" diff --git a/crates/kernel-macros/src/image.rs b/crates/kernel-macros/src/image.rs index 63106d2..c4a6391 100644 --- a/crates/kernel-macros/src/image.rs +++ b/crates/kernel-macros/src/image.rs @@ -96,7 +96,7 @@ pub fn parse_image( Ok((width, height, name_ident, byte_count, byte_tokens)) } -pub fn include_font_plate_impl(input: TokenStream) -> TokenStream { +pub fn include_bitmap_image_impl(input: TokenStream) -> TokenStream { let (_, _, _, _, byte_tokens) = parse_image(input).unwrap(); let output = quote! { diff --git a/crates/kernel-macros/src/lib.rs b/crates/kernel-macros/src/lib.rs index ad301a0..f0c28cd 100644 --- a/crates/kernel-macros/src/lib.rs +++ b/crates/kernel-macros/src/lib.rs @@ -3,6 +3,6 @@ mod image; use proc_macro::TokenStream; #[proc_macro] -pub fn include_font_plate(input: TokenStream) -> TokenStream { - image::include_font_plate_impl(input) +pub fn include_bitmap_image(input: TokenStream) -> TokenStream { + image::include_bitmap_image_impl(input) } diff --git a/crates/shared/src/syscall.rs b/crates/shared/src/syscall.rs index debcb12..cdbdf46 100644 --- a/crates/shared/src/syscall.rs +++ b/crates/shared/src/syscall.rs @@ -7,6 +7,7 @@ use crate::fs::File; #[repr(u64)] pub enum SysCall { + Read = 0, Write = 1, Open = 2, Seek = 8, @@ -22,6 +23,7 @@ pub enum SysCall { impl From for SysCall { fn from(value: u64) -> Self { match value { + 0 => SysCall::Read, 1 => SysCall::Write, 2 => SysCall::Open, 8 => SysCall::Seek, @@ -151,6 +153,13 @@ pub fn write(file: &mut File, buf: &[u8]) { syscall!(SysCall::Write, file.as_fd(), ptr as u64, size as u64); } } +pub fn read(file: &mut File, buf: &mut [u8]) { + unsafe { + let ptr = buf.as_ptr(); + let size = buf.len(); + syscall!(SysCall::Read, file.as_fd(), ptr as u64, size as u64); + } +} pub fn seek(file: &mut File, seek: SeekFrom) { unsafe { let (discriminant, value) = match seek { diff --git a/src/cursor.rs b/src/cursor.rs new file mode 100644 index 0000000..b11fa61 --- /dev/null +++ b/src/cursor.rs @@ -0,0 +1,38 @@ +use kernel_macros::include_bitmap_image; + +use crate::draw::{Color, Draw}; + +pub const CURSOR_WIDTH: usize = 8; +pub const CURSOR_HEIGHT: usize = 10; +pub const CURSOR_SIZE: usize = (CURSOR_HEIGHT * CURSOR_WIDTH) / 8; +pub static CURSOR: [u8; CURSOR_SIZE] = include_bitmap_image! {"assets/cursor.png"}; + +pub fn draw_cursor(drawer: &mut T, x: u16, y: u16) { + for i in 0..CURSOR_HEIGHT { + for j in 0..CURSOR_WIDTH { + let pos = i * CURSOR_WIDTH + j; + if CURSOR[pos / 8] & (1 << (pos % 8)) != 0 { + unsafe { + drawer.write_pixel_unsafe( + x.saturating_add(j as u16), + y.saturating_add(i as u16), + Color::WHITE, + ) + }; + } + } + } +} +pub fn clear_cursor(drawer: &mut T, x: u16, y: u16) { + for i in 0..CURSOR_HEIGHT { + for j in 0..CURSOR_WIDTH { + unsafe { + drawer.write_pixel_unsafe( + x.saturating_add(j as u16), + y.saturating_add(i as u16), + Color::BLACK, + ) + }; + } + } +} diff --git a/src/data_structures.rs b/src/data_structures.rs new file mode 100644 index 0000000..c04666e --- /dev/null +++ b/src/data_structures.rs @@ -0,0 +1 @@ +pub mod circular_buffer; diff --git a/src/data_structures/circular_buffer.rs b/src/data_structures/circular_buffer.rs new file mode 100644 index 0000000..3dc03f8 --- /dev/null +++ b/src/data_structures/circular_buffer.rs @@ -0,0 +1,55 @@ +use core::mem::MaybeUninit; + +#[derive(Debug)] +pub struct CircularBuffer { + buffer: [MaybeUninit; SIZE], + head: usize, + tail: usize, + len: usize, +} + +impl CircularBuffer { + pub const fn new() -> Self { + Self { + buffer: [const { MaybeUninit::uninit() }; SIZE], + head: 0, + tail: 0, + len: 0, + } + } + + pub fn len(&self) -> usize { + self.len + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn is_full(&self) -> bool { + self.len() == SIZE - 1 + } + + pub fn pop(&mut self) -> Option { + if self.is_empty() { + None + } else { + let out = core::mem::replace(&mut self.buffer[self.head], MaybeUninit::uninit()); + self.head = (self.head + 1) % SIZE; + self.len -= 1; + // # Safety + // the queue is not empty and head points to a valid value + Some(unsafe { out.assume_init() }) + } + } + + pub fn push(&mut self, value: T) { + self.buffer[self.tail] = MaybeUninit::new(value); + if self.is_full() { + self.head = (self.head + 1) % SIZE; + } else { + self.len += 1; + } + self.tail = (self.tail + 1) % SIZE; + } +} diff --git a/src/draw.rs b/src/draw.rs index b127dbd..f0e709f 100644 --- a/src/draw.rs +++ b/src/draw.rs @@ -1,4 +1,4 @@ -use kernel_macros::include_font_plate; +use kernel_macros::include_bitmap_image; /// 24-bit RGB color used by the framebuffer. #[repr(transparent)] @@ -121,4 +121,4 @@ pub const FONT_HEIGHT: usize = 13; pub const FONTPLATE_WIDTH: usize = 32 * FONT_WIDTH; pub const FONTPLATE_HEIGHT: usize = 3 * FONT_HEIGHT; pub const FONTPLATE_SIZE: usize = FONTPLATE_WIDTH * FONTPLATE_HEIGHT / 8; -pub static FONTPLATE: [u8; FONTPLATE_SIZE] = include_font_plate! {"assets/fontplate.png"}; +pub static FONTPLATE: [u8; FONTPLATE_SIZE] = include_bitmap_image! {"assets/fontplate.png"}; diff --git a/src/interrupt.rs b/src/interrupt.rs index 821994e..3d613a1 100644 --- a/src/interrupt.rs +++ b/src/interrupt.rs @@ -149,6 +149,16 @@ unsafe extern "C" fn supervisor_trap_handler( let vnode = current_process.fd_table.get_mut(&fd).unwrap(); vnode.write(buf).unwrap(); } + SysCall::Read => { + let fd = a1; + let buf = + unsafe { core::slice::from_raw_parts_mut(a2 as *mut u8, a3 as usize) }; + + let mut scheduler = SCHEDULER.lock(); + let current_process = scheduler.get_current_process(); + let vnode = current_process.fd_table.get_mut(&fd).unwrap(); + vnode.read(buf).unwrap(); + } SysCall::Seek => { let fd = a1; let seek = match a2 { diff --git a/src/main.rs b/src/main.rs index e5127bf..ebdcd91 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,7 +19,7 @@ use embedded_alloc::LlffHeap as Heap; use log::info; use crate::{ - draw::{Color, Draw}, + cursor::{clear_cursor, draw_cursor}, io::init_log, pci::{PciDeviceIterator, scan_virtio_devices}, riscv::enable_supervisor_interrupt, @@ -28,14 +28,16 @@ use crate::{ vga::Vga, virtio::{ Virtqueue, - input::{VirtioPciDriver, init_plic_pci}, + input::{EventCodeValue, VirtioPciDriver, init_plic_pci}, }, - virtual_fs::init_file_system, + virtual_fs::{FILE_SYSTEM, VirtualFileSystem, init_file_system}, }; extern crate alloc; mod boot; mod critical_section; +mod cursor; +mod data_structures; mod draw; mod fs; mod interrupt; @@ -72,11 +74,15 @@ static mut KBD_QUEUE: Virtqueue = unsafe { core::mem::zeroed() }; pub static mut KBD_DRIVER: VirtioPciDriver = unsafe { VirtioPciDriver::new( |event| { + let mut kbd_buffer = FILE_SYSTEM.open("/dev/input/keyboard".as_ref()).unwrap(); if event.is_key() { let event = event.as_key_event(); - println!("key, {:#?}", event); + if event.value == EventCodeValue::Pressed { + println!("event: {:#?}", event); + kbd_buffer.write(&[event.code as u8]).unwrap(); + } } else { - println!("key pressed, {:#?}", event); + // println!("key pressed, {:#?}", event); } }, &mut KBD_QUEUE, @@ -92,7 +98,7 @@ pub static mut MOUSE_DRIVER: VirtioPciDriver = unsafe { if event.is_relative() { let event = event.as_relative_event(); - vga::Vga.write_pixel_unsafe(MOUSE_POSITION.0, MOUSE_POSITION.1, Color::BLACK); + clear_cursor(&mut Vga, MOUSE_POSITION.0, MOUSE_POSITION.1); match event.code { virtio::input::EventCodeRelative::X => { @@ -104,7 +110,7 @@ pub static mut MOUSE_DRIVER: VirtioPciDriver = unsafe { _ => {} } - vga::Vga.write_pixel_unsafe(MOUSE_POSITION.0, MOUSE_POSITION.1, Color::RED); + draw_cursor(&mut Vga, MOUSE_POSITION.0, MOUSE_POSITION.1); // println!("mouse moved relatively, {:#?}", event); } else { @@ -122,8 +128,8 @@ pub extern "C" fn supervisor_mode_entry() { HEAP_INITIALIZED.store(true, core::sync::atomic::Ordering::Relaxed); init_log().unwrap(); Vga::init(); - SCHEDULER.lock().init(); init_file_system(); + SCHEDULER.lock().init(); } info!("Hello World !"); diff --git a/src/process.rs b/src/process.rs index 90912a6..beefbc2 100644 --- a/src/process.rs +++ b/src/process.rs @@ -13,7 +13,6 @@ use alloc::{boxed::Box, format, string::String, vec::Vec}; use bffs::path::Path; use goblin::elf::reloc::R_RISCV_RELATIVE; use hashbrown::HashMap; -use log::info; use shared::syscall::exit; use crate::{ @@ -164,9 +163,7 @@ impl Scheduler { let name = path.as_str(); // Open and read the binary file - info!("ue"); let mut bin = unsafe { FILE_SYSTEM.open(path).unwrap() }; - info!("ue"); println!("Creating process"); let mut content: Vec = Vec::new(); bin.read_to_end(&mut content).unwrap(); @@ -300,6 +297,11 @@ impl Scheduler { process.state = ProcessState::Activable; process.entry = Some(code); + process.fd_table = HashMap::new(); + process + .fd_table + .insert(0, FILE_SYSTEM.open("/dev/input/keyboard".into()).unwrap()); + // Configure execution context // a0 contains the pointer to the function to execute process.ctx.a[0] = &raw const *process.entry.as_ref().unwrap_unchecked() as u64; diff --git a/src/virtual_fs.rs b/src/virtual_fs.rs index 78f7743..06314c6 100644 --- a/src/virtual_fs.rs +++ b/src/virtual_fs.rs @@ -8,7 +8,10 @@ use bffs::{ use hashbrown::HashMap; use io::{IoBase, Read, Seek, Write}; -use crate::{fs::Disk, tty::Tty, vga::Vga}; +pub mod keyboard; +pub mod stdin; + +use crate::{fs::Disk, tty::Tty, vga::Vga, virtual_fs::keyboard::KeyboardBuffer}; pub trait VirtualNode: IoBase + Read + Write + Seek + Debug {} @@ -53,6 +56,11 @@ pub unsafe fn init_file_system() { unsafe { FILE_SYSTEM.mount("/dev/fb0".into(), Box::new(VGAFileSystem)); FILE_SYSTEM.mount("/dev/tty0".into(), Box::new(Tty::new())); + FILE_SYSTEM.mount( + "/dev/input/keyboard".into(), + Box::new(KeyboardBuffer::new()), + ); + // FILE_SYSTEM.mount("/dev/stdin".into(), Box::new(Stdin::new())); } } diff --git a/src/virtual_fs/keyboard.rs b/src/virtual_fs/keyboard.rs new file mode 100644 index 0000000..9960396 --- /dev/null +++ b/src/virtual_fs/keyboard.rs @@ -0,0 +1,85 @@ +use core::cell::RefCell; + +use alloc::boxed::Box; +use io::{IoBase, Read, Seek, Write}; + +use crate::{ + data_structures::circular_buffer::CircularBuffer, + println, + virtual_fs::{VirtualFileSystem, VirtualNode}, +}; + +pub const KEYBOARD_BUFFER_SIZE: usize = 4096; // 4Ko + +#[derive(Debug)] +pub struct KeyboardBuffer { + buffer: RefCell>, +} + +impl KeyboardBuffer { + pub fn new() -> Self { + Self { + buffer: RefCell::new(CircularBuffer::new()), + } + } +} + +#[derive(Debug)] +pub struct KeyboardBufferNode<'a> { + buffer: &'a KeyboardBuffer, +} + +impl<'a> KeyboardBufferNode<'a> { + pub fn new(stdin: &'a KeyboardBuffer) -> Self { + Self { buffer: stdin } + } +} + +impl VirtualFileSystem for KeyboardBuffer { + fn open( + &mut self, + path: &bffs::path::Path, + ) -> Result, ()> { + if !path.is_empty() { + Err(()) + } else { + Ok(Box::new(KeyboardBufferNode::new(self))) + } + } +} + +impl IoBase for KeyboardBufferNode<'_> { + type Error = (); +} + +impl Read for KeyboardBufferNode<'_> { + fn read(&mut self, buf: &mut [u8]) -> Result { + let mut buffer = self.buffer.buffer.borrow_mut(); + for i in 0..buf.len() { + buf[i] = buffer.pop().unwrap(); + } + Ok(buf.len()) + } +} + +impl Seek for KeyboardBufferNode<'_> { + fn seek(&mut self, pos: io::SeekFrom) -> Result { + todo!() + } +} + +impl Write for KeyboardBufferNode<'_> { + fn write(&mut self, buf: &[u8]) -> Result { + let mut buffer = self.buffer.buffer.borrow_mut(); + for input in buf { + buffer.push(*input); + } + Ok(buf.len()) + } + + fn flush(&mut self) -> Result<(), Self::Error> { + todo!() + } +} + +impl VirtualNode for KeyboardBufferNode<'_> {} diff --git a/src/virtual_fs/stdin.rs b/src/virtual_fs/stdin.rs new file mode 100644 index 0000000..28255da --- /dev/null +++ b/src/virtual_fs/stdin.rs @@ -0,0 +1,67 @@ +// use core::cell::RefCell; + +// use alloc::boxed::Box; +// use io::{IoBase, Read, Seek, Write}; + +// use crate::virtual_fs::{VirtualFileSystem, VirtualNode}; + +// pub const STDIN_BUFFER_SIZE: usize = 4096; // 4Ko + +// #[derive(Debug)] +// pub struct Stdin { +// buffer: RefCell<[u8; STDIN_BUFFER_SIZE]>, +// } + +// impl Stdin { +// pub fn new() -> Self { +// Self { +// buffer: RefCell::new([0; _]), +// } +// } +// } + +// #[derive(Debug)] +// pub struct StdinNode<'a> { +// stdin: &'a Stdin, +// } + +// impl VirtualFileSystem for Stdin { +// fn open( +// &mut self, +// path: &bffs::path::Path, +// ) -> Result, ()> { +// if !path.is_empty() { +// Err(()) +// } else { +// Ok(Box::new(StdinNode { stdin: self })) +// } +// } +// } + +// impl IoBase for StdinNode<'_> { +// type Error = (); +// } + +// impl Read for StdinNode<'_> { +// fn read(&mut self, buf: &mut [u8]) -> Result { +// todo!() +// } +// } + +// impl Seek for StdinNode<'_> { +// fn seek(&mut self, pos: io::SeekFrom) -> Result { +// todo!() +// } +// } + +// impl Write for StdinNode<'_> { +// fn write(&mut self, buf: &[u8]) -> Result { +// todo!() +// } + +// fn flush(&mut self) -> Result<(), Self::Error> { +// todo!() +// } +// } + +// impl VirtualNode for StdinNode<'_> {} diff --git a/user/test_pic/src/main.rs b/user/test_pic/src/main.rs index 4e6f3b0..0cefb27 100644 --- a/user/test_pic/src/main.rs +++ b/user/test_pic/src/main.rs @@ -1,23 +1,24 @@ #![no_std] #![no_main] +use core::time::Duration; + use os_std::syscall; os_std::custom_std_setup! {} fn main() { - let mut test = String::new(); - test.push('A'); - test.push('B'); - for _ in 0..100 { - test.push('C'); - } + // let mut input = String::new(); // let mut file = syscall::open("/dev/fb0"); // syscall::seek(&mut file, SeekFrom::End(-3)); // syscall::write(&mut file, &[255; 6400 * 50]); + syscall::sleep(Duration::from_secs_f64(2.0)); + let mut stdin = syscall::open("/dev/input/keyboard"); + let mut test = [0; 2]; + syscall::read(&mut stdin, &mut test); let mut file = syscall::open("/dev/tty0"); syscall::write(&mut file, b"Hi !\nnice tty\x08"); println!( - "Hello from PIC program loaded dynamically with custom std and a better justfile, and syscalls ! {}", + "Hello from PIC program loaded dynamically with custom std and a better justfile, and syscalls ! {:?}", test ); }