From f9755b0b7a99dfc0dd5eb7d9f020ab2673f5b87b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 15 Aug 2025 21:22:41 +0200 Subject: [PATCH] Introduce `Ice` format and save test metadata --- devtools/src/tester.rs | 59 +++++----- devtools/src/tester/null.rs | 10 -- examples/todos/tests/carl_sagan.ice | 8 +- test/src/ice.rs | 169 ++++++++++++++++++++++++++++ test/src/instruction.rs | 10 +- test/src/lib.rs | 2 + 6 files changed, 215 insertions(+), 43 deletions(-) create mode 100644 test/src/ice.rs diff --git a/devtools/src/tester.rs b/devtools/src/tester.rs index 8c7ac9ce..13b6e9e2 100644 --- a/devtools/src/tester.rs +++ b/devtools/src/tester.rs @@ -15,8 +15,9 @@ use crate::icon; use crate::program; use crate::runtime::Task; use crate::test::emulator; +use crate::test::ice; use crate::test::instruction; -use crate::test::{Emulator, Instruction}; +use crate::test::{Emulator, Ice, Instruction}; use crate::widget::{ button, center, column, combo_box, container, horizontal_space, monospace, pick_list, row, scrollable, text, text_editor, text_input, themer, @@ -64,7 +65,7 @@ pub enum Message { Play, Import, Export, - Imported(Option), + Imported(Result), Edit, Edited(text_editor::Action), Confirm, @@ -188,8 +189,10 @@ impl Tester

{ Task::future(import) .and_then(|file| { executor::spawn_blocking(move |mut sender| { - let _ = sender - .try_send(fs::read_to_string(file.path()).ok()); + let _ = sender.try_send(Ice::parse( + &fs::read_to_string(file.path()) + .unwrap_or_default(), + )); }) }) .map(Message::Imported) @@ -201,11 +204,15 @@ impl Tester

{ self.confirm(); - let test: Vec<_> = self - .instructions - .iter() - .map(Instruction::to_string) - .collect(); + let ice = Ice { + viewport: Size::new( + self.viewport.width as u32, + self.viewport.height as u32, + ), + mode: self.mode, + preset: self.preset.clone(), + instructions: self.instructions.clone(), + }; let export = rfd::AsyncFileDialog::new() .add_filter("ice", &["ice"]) @@ -217,28 +224,21 @@ impl Tester

{ }; let _ = thread::spawn(move || { - fs::write(file.path(), test.join("\n")) + fs::write(file.path(), ice.to_string()) }); }) .discard() } - Message::Imported(instructions) => { - let Some(instructions) = instructions else { - return Task::none(); - }; - - let instructions: Result, _> = - instructions.lines().map(Instruction::parse).collect(); - - match instructions { - Ok(instructions) => { - self.instructions = instructions; - self.edit = None; - } - Err(error) => { - log::error!("{error}"); - } - } + Message::Imported(Ok(ice)) => { + self.viewport = Size::new( + ice.viewport.width as f32, + ice.viewport.height as f32, + ); + self.mode = ice.mode; + self.preset = ice.preset; + self.instructions = ice.instructions; + self.edit = None; + self.state = State::Idle; Task::none() } @@ -268,6 +268,11 @@ impl Tester

{ Message::Confirm => { self.confirm(); + Task::none() + } + Message::Imported(Err(error)) => { + log::error!("{error}"); + Task::none() } } diff --git a/devtools/src/tester/null.rs b/devtools/src/tester/null.rs index a2b7be47..96b1dbfa 100644 --- a/devtools/src/tester/null.rs +++ b/devtools/src/tester/null.rs @@ -1,7 +1,5 @@ use crate::Program; -use crate::core::window; use crate::core::{Element, Theme}; -use crate::futures::Subscription; use crate::runtime::Task; use crate::widget::horizontal_space; @@ -24,10 +22,6 @@ impl Tester

{ Self { _type: PhantomData } } - pub fn is_idle(&self) -> bool { - true - } - pub fn is_busy(&self) -> bool { false } @@ -40,10 +34,6 @@ impl Tester

{ Task::none() } - pub fn subscription(&self, _program: &P) -> Subscription> { - Subscription::none() - } - pub fn view<'a, T: 'static>( &'a self, _program: &P, diff --git a/examples/todos/tests/carl_sagan.ice b/examples/todos/tests/carl_sagan.ice index d536b9f1..860f5d9e 100644 --- a/examples/todos/tests/carl_sagan.ice +++ b/examples/todos/tests/carl_sagan.ice @@ -1,3 +1,7 @@ +viewport: 512x768 +mode: impatient +preset: Empty +----- click left at (377.80, 236.50) type "Create the universe" type enter @@ -5,4 +9,6 @@ type "Make an apple pie" type enter click left at (135.40, 351.70) click left at (153.80, 398.10) -move cursor to (511.40, 448.50) \ No newline at end of file +move cursor to (511.40, 448.50) +expect text "Create the universe" +expect text "Make an apple pie" diff --git a/test/src/ice.rs b/test/src/ice.rs new file mode 100644 index 00000000..67037c14 --- /dev/null +++ b/test/src/ice.rs @@ -0,0 +1,169 @@ +use crate::Instruction; +use crate::core::Size; +use crate::emulator; +use crate::instruction; + +#[derive(Debug, Clone, PartialEq)] +pub struct Ice { + pub viewport: Size, + pub mode: emulator::Mode, + pub preset: Option, + pub instructions: Vec, +} + +impl Ice { + pub fn parse(content: &str) -> Result { + let Some((metadata, rest)) = content.split_once("-") else { + return Err(ParseError::NoMetadata); + }; + + let mut viewport = None; + let mut mode = None; + let mut preset = None; + + for (i, line) in metadata.lines().enumerate() { + if line.trim().is_empty() { + continue; + } + + let Some((field, value)) = line.split_once(':') else { + return Err(ParseError::InvalidMetadata { + line: i, + content: line.to_owned(), + }); + }; + + match field.trim() { + "viewport" => { + viewport = Some( + if let Some((width, height)) = + value.trim().split_once('x') + && let Ok(width) = width.parse() + && let Ok(height) = height.parse() + { + Size::new(width, height) + } else { + return Err(ParseError::InvalidViewport { + line: i, + value: value.to_owned(), + }); + }, + ); + } + "mode" => { + mode = Some(match value.trim() { + "patient" => emulator::Mode::Patient, + "impatient" => emulator::Mode::Impatient, + _ => { + return Err(ParseError::InvalidMode { + line: i, + value: value.to_owned(), + }); + } + }); + } + "preset" => { + preset = Some(value.trim().to_owned()); + } + field => { + return Err(ParseError::UnknownField { + line: i, + field: field.to_owned(), + }); + } + } + } + + let Some(viewport) = viewport else { + return Err(ParseError::MissingViewport); + }; + + let Some(mode) = mode else { + return Err(ParseError::MissingMode); + }; + + let instructions = rest + .lines() + .skip(1) + .enumerate() + .map(|(i, line)| { + Instruction::parse(line).map_err(|error| { + ParseError::InvalidInstruction { + line: metadata.lines().count() + 1 + i, + error, + } + }) + }) + .collect::, _>>()?; + + Ok(Self { + viewport, + mode, + preset, + instructions, + }) + } +} + +impl std::fmt::Display for Ice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!( + f, + "viewport: {width}x{height}", + width = self.viewport.width, + height = self.viewport.height + )?; + + writeln!( + f, + "mode: {}", + match self.mode { + emulator::Mode::Patient => "patient", + emulator::Mode::Impatient => "impatient", + } + )?; + + if let Some(preset) = &self.preset { + writeln!(f, "preset: {preset}")?; + } + + f.write_str("-----\n")?; + + for instruction in &self.instructions { + instruction.fmt(f)?; + f.write_str("\n")?; + } + + Ok(()) + } +} + +#[derive(Debug, Clone, thiserror::Error)] +pub enum ParseError { + #[error("the ice test has no metadata")] + NoMetadata, + + #[error("invalid metadata in line {line}: \"{content}\"")] + InvalidMetadata { line: usize, content: String }, + + #[error("invalid viewport in line {line}: \"{value}\"")] + InvalidViewport { line: usize, value: String }, + + #[error("invalid mode in line {line}: \"{value}\"")] + InvalidMode { line: usize, value: String }, + + #[error("unknown metadata field in line {line}: \"{field}\"")] + UnknownField { line: usize, field: String }, + + #[error("metadata is missing the viewport field")] + MissingViewport, + + #[error("metadata is missing the mode field")] + MissingMode, + + #[error("invalid instruction in line {line}: {error}")] + InvalidInstruction { + line: usize, + error: instruction::ParseError, + }, +} diff --git a/test/src/instruction.rs b/test/src/instruction.rs index ba4867ba..ba987072 100644 --- a/test/src/instruction.rs +++ b/test/src/instruction.rs @@ -7,7 +7,7 @@ use crate::simulator; use std::fmt; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum Instruction { Interact(Interaction), Expect(Expectation), @@ -28,7 +28,7 @@ impl fmt::Display for Instruction { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum Interaction { Mouse(Mouse), Keyboard(Keyboard), @@ -239,7 +239,7 @@ impl fmt::Display for Interaction { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum Mouse { Move(Point), Press { @@ -275,7 +275,7 @@ impl fmt::Display for Mouse { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum Keyboard { Press(Key), Release(Key), @@ -357,7 +357,7 @@ mod format { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum Expectation { Presence(Selector), } diff --git a/test/src/lib.rs b/test/src/lib.rs index bdf06df1..150521f0 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -90,6 +90,7 @@ use iced_runtime as runtime; use iced_runtime::core; pub mod emulator; +pub mod ice; pub mod instruction; pub mod selector; pub mod simulator; @@ -98,6 +99,7 @@ mod error; pub use emulator::Emulator; pub use error::Error; +pub use ice::Ice; pub use instruction::Instruction; pub use selector::Selector; pub use simulator::{Simulator, simulator};