Introduce Ice format and save test metadata

This commit is contained in:
Héctor Ramón Jiménez 2025-08-15 21:22:41 +02:00
parent 28a4c53f43
commit f9755b0b7a
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
6 changed files with 215 additions and 43 deletions

View file

@ -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<String>),
Imported(Result<Ice, ice::ParseError>),
Edit,
Edited(text_editor::Action),
Confirm,
@ -188,8 +189,10 @@ impl<P: Program + 'static> Tester<P> {
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<P: Program + 'static> Tester<P> {
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<P: Program + 'static> Tester<P> {
};
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<Vec<_>, _> =
instructions.lines().map(Instruction::parse).collect();
match instructions {
Ok(instructions) => {
self.instructions = instructions;
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;
}
Err(error) => {
log::error!("{error}");
}
}
self.state = State::Idle;
Task::none()
}
@ -268,6 +268,11 @@ impl<P: Program + 'static> Tester<P> {
Message::Confirm => {
self.confirm();
Task::none()
}
Message::Imported(Err(error)) => {
log::error!("{error}");
Task::none()
}
}

View file

@ -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<P: Program> Tester<P> {
Self { _type: PhantomData }
}
pub fn is_idle(&self) -> bool {
true
}
pub fn is_busy(&self) -> bool {
false
}
@ -40,10 +34,6 @@ impl<P: Program> Tester<P> {
Task::none()
}
pub fn subscription(&self, _program: &P) -> Subscription<Tick<P>> {
Subscription::none()
}
pub fn view<'a, T: 'static>(
&'a self,
_program: &P,

View file

@ -1,3 +1,7 @@
viewport: 512x768
mode: impatient
preset: Empty
-----
click left at (377.80, 236.50)
type "Create the universe"
type enter
@ -6,3 +10,5 @@ type enter
click left at (135.40, 351.70)
click left at (153.80, 398.10)
move cursor to (511.40, 448.50)
expect text "Create the universe"
expect text "Make an apple pie"

169
test/src/ice.rs Normal file
View file

@ -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<u32>,
pub mode: emulator::Mode,
pub preset: Option<String>,
pub instructions: Vec<Instruction>,
}
impl Ice {
pub fn parse(content: &str) -> Result<Self, ParseError> {
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::<Result<Vec<_>, _>>()?;
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,
},
}

View file

@ -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),
}

View file

@ -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};