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