From 6a6a2ac8c5d868893c8c2362502987d95c41aaa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 27 Aug 2025 06:02:02 +0200 Subject: [PATCH] Implement `iced_test::run` entrypoint for ice testing --- devtools/src/tester.rs | 12 ++--- examples/todos/src/main.rs | 23 ++++++--- test/src/emulator.rs | 8 +-- test/src/error.rs | 21 ++++++++ test/src/ice.rs | 6 +-- test/src/lib.rs | 101 +++++++++++++++++++++++++++++++++++++ 6 files changed, 149 insertions(+), 22 deletions(-) diff --git a/devtools/src/tester.rs b/devtools/src/tester.rs index de1d3e25..1f5397c0 100644 --- a/devtools/src/tester.rs +++ b/devtools/src/tester.rs @@ -221,10 +221,7 @@ impl Tester

{ self.confirm(); let ice = Ice { - viewport: Size::new( - self.viewport.width as u32, - self.viewport.height as u32, - ), + viewport: self.viewport, mode: self.mode, preset: self.preset.clone(), instructions: self.instructions.clone(), @@ -246,10 +243,7 @@ impl Tester

{ .discard() } Message::Imported(Ok(ice)) => { - self.viewport = Size::new( - ice.viewport.width as f32, - ice.viewport.height as f32, - ); + self.viewport = ice.viewport; self.mode = ice.mode; self.preset = ice.preset; self.instructions = ice.instructions; @@ -347,7 +341,7 @@ impl Tester

{ emulator::Event::Action(action) => { emulator.perform(program, action); } - emulator::Event::Failed => { + emulator::Event::Failed(_instruction) => { *outcome = Outcome::Failed; } emulator::Event::Ready => { diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index ecaa99d4..0f7c1009 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -5,8 +5,8 @@ use iced::widget::{ }; use iced::window; use iced::{ - Center, Element, Fill, Font, Function, Preset, Subscription, - Task as Command, + Application, Center, Element, Fill, Font, Function, Preset, Program, + Subscription, Task as Command, }; use serde::{Deserialize, Serialize}; @@ -16,14 +16,16 @@ pub fn main() -> iced::Result { #[cfg(not(target_arch = "wasm32"))] tracing_subscriber::fmt::init(); - let todos = iced::application(Todos::new, Todos::update, Todos::view) + application().run() +} + +fn application() -> Application { + iced::application(Todos::new, Todos::update, Todos::view) .subscription(Todos::subscription) .title(Todos::title) .font(Todos::ICON_FONT) .window_size((500.0, 800.0)) - .presets(presets()); - - todos.run() + .presets(presets()) } #[derive(Debug)] @@ -648,4 +650,13 @@ mod tests { Ok(()) } + + #[test] + #[ignore] + fn it_passes_the_ice_tests() -> Result<(), Error> { + iced_test::run( + application(), + format!("{}/tests", env!("CARGO_MANIFEST_DIR")), + ) + } } diff --git a/test/src/emulator.rs b/test/src/emulator.rs index b51d4a5c..ef358d7a 100644 --- a/test/src/emulator.rs +++ b/test/src/emulator.rs @@ -36,7 +36,7 @@ pub struct Emulator { #[allow(missing_debug_implementations)] pub enum Event { Action(Action), - Failed, + Failed(Instruction), Ready, } @@ -211,7 +211,7 @@ impl Emulator

{ let mut messages = Vec::new(); - match instruction { + match &instruction { Instruction::Interact(interaction) => { let Some(events) = interaction.events(|target| match target { instruction::Target::Point(position) => Some(*position), @@ -234,7 +234,7 @@ impl Emulator

{ } } }) else { - self.runtime.send(Event::Failed); + self.runtime.send(Event::Failed(instruction)); self.cache = Some(user_interface.into_cache()); return; }; @@ -282,7 +282,7 @@ impl Emulator

{ self.runtime.send(Event::Ready); } _ => { - self.runtime.send(Event::Failed); + self.runtime.send(Event::Failed(instruction)); } } diff --git a/test/src/error.rs b/test/src/error.rs index 1e1de331..4b898e95 100644 --- a/test/src/error.rs +++ b/test/src/error.rs @@ -1,4 +1,8 @@ +use crate::Instruction; +use crate::ice; + use std::io; +use std::path::PathBuf; use std::sync::Arc; /// A test error. @@ -20,6 +24,23 @@ pub enum Error { /// The encoding of some PNG image failed. #[error("the encoding of some PNG image failed: {0}")] PngEncodingFailed(Arc), + #[error("the ice test ({file}) is invalid: {error}")] + IceParsingFailed { + file: PathBuf, + error: ice::ParseError, + }, + #[error("the ice test ({file}) failed")] + IceFailed { + file: PathBuf, + instruction: Instruction, + }, + #[error( + "the preset \"{name}\" does not exist (available presets: {available:?})" + )] + PresetNotFound { + name: String, + available: Vec, + }, } impl From for Error { diff --git a/test/src/ice.rs b/test/src/ice.rs index ab1a9c8f..a8e44a93 100644 --- a/test/src/ice.rs +++ b/test/src/ice.rs @@ -5,7 +5,7 @@ use crate::instruction; #[derive(Debug, Clone, PartialEq)] pub struct Ice { - pub viewport: Size, + pub viewport: Size, pub mode: emulator::Mode, pub preset: Option, pub instructions: Vec, @@ -110,8 +110,8 @@ impl std::fmt::Display for Ice { writeln!( f, "viewport: {width}x{height}", - width = self.viewport.width, - height = self.viewport.height + width = self.viewport.width as u32, // TODO + height = self.viewport.height as u32, // TODO )?; writeln!( diff --git a/test/src/lib.rs b/test/src/lib.rs index baccc5e3..82f5db92 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -104,3 +104,104 @@ pub use ice::Ice; pub use instruction::Instruction; pub use selector::Selector; pub use simulator::{Simulator, simulator}; + +use std::path::Path; + +pub fn run( + program: impl program::Program + 'static, + tests_dir: impl AsRef, +) -> Result<(), Error> { + use crate::runtime::futures::futures::StreamExt; + use crate::runtime::futures::futures::channel::mpsc; + use crate::runtime::futures::futures::executor; + + use std::ffi::OsStr; + use std::fs; + + let tests = fs::read_dir(tests_dir)?; + + // TODO: Concurrent runtimes + for file in tests { + let file = file?; + + if file.path().extension().and_then(OsStr::to_str) != Some("ice") { + continue; + } + + let ice = { + let content = fs::read_to_string(file.path())?; + + match Ice::parse(&content) { + Ok(ice) => ice, + Err(error) => { + return Err(Error::IceParsingFailed { + file: file.path().to_path_buf(), + error, + }); + } + } + }; + + let (sender, mut receiver) = mpsc::channel(1); + + let preset = if let Some(preset) = ice.preset { + let Some(preset) = program + .presets() + .iter() + .find(|candidate| candidate.name() == preset) + else { + return Err(Error::PresetNotFound { + name: preset.to_owned(), + available: program + .presets() + .iter() + .map(program::Preset::name) + .map(str::to_owned) + .collect(), + }); + }; + + Some(preset) + } else { + None + }; + + let mut emulator = Emulator::with_preset( + sender, + &program, + ice.mode, + ice.viewport, + preset, + ); + + let mut instructions: Vec<_> = + ice.instructions.into_iter().rev().collect(); + + loop { + let Some(event) = executor::block_on(receiver.next()) else { + panic!("emulator runtime stopped unexpectedly"); + }; + + match event { + emulator::Event::Action(action) => { + emulator.perform(&program, action); + } + emulator::Event::Failed(instruction) => { + return Err(Error::IceFailed { + file: file.path().to_path_buf(), + instruction, + }); + } + emulator::Event::Ready => { + let Some(instruction) = instructions.pop() else { + break; + }; + + emulator.run(&program, instruction); + } + } + } + } + + Ok(()) +}