Introduce Ice format and save test metadata
This commit is contained in:
parent
28a4c53f43
commit
f9755b0b7a
6 changed files with 215 additions and 43 deletions
|
|
@ -15,8 +15,9 @@ use crate::icon;
|
||||||
use crate::program;
|
use crate::program;
|
||||||
use crate::runtime::Task;
|
use crate::runtime::Task;
|
||||||
use crate::test::emulator;
|
use crate::test::emulator;
|
||||||
|
use crate::test::ice;
|
||||||
use crate::test::instruction;
|
use crate::test::instruction;
|
||||||
use crate::test::{Emulator, Instruction};
|
use crate::test::{Emulator, Ice, Instruction};
|
||||||
use crate::widget::{
|
use crate::widget::{
|
||||||
button, center, column, combo_box, container, horizontal_space, monospace,
|
button, center, column, combo_box, container, horizontal_space, monospace,
|
||||||
pick_list, row, scrollable, text, text_editor, text_input, themer,
|
pick_list, row, scrollable, text, text_editor, text_input, themer,
|
||||||
|
|
@ -64,7 +65,7 @@ pub enum Message {
|
||||||
Play,
|
Play,
|
||||||
Import,
|
Import,
|
||||||
Export,
|
Export,
|
||||||
Imported(Option<String>),
|
Imported(Result<Ice, ice::ParseError>),
|
||||||
Edit,
|
Edit,
|
||||||
Edited(text_editor::Action),
|
Edited(text_editor::Action),
|
||||||
Confirm,
|
Confirm,
|
||||||
|
|
@ -188,8 +189,10 @@ impl<P: Program + 'static> Tester<P> {
|
||||||
Task::future(import)
|
Task::future(import)
|
||||||
.and_then(|file| {
|
.and_then(|file| {
|
||||||
executor::spawn_blocking(move |mut sender| {
|
executor::spawn_blocking(move |mut sender| {
|
||||||
let _ = sender
|
let _ = sender.try_send(Ice::parse(
|
||||||
.try_send(fs::read_to_string(file.path()).ok());
|
&fs::read_to_string(file.path())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
));
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.map(Message::Imported)
|
.map(Message::Imported)
|
||||||
|
|
@ -201,11 +204,15 @@ impl<P: Program + 'static> Tester<P> {
|
||||||
|
|
||||||
self.confirm();
|
self.confirm();
|
||||||
|
|
||||||
let test: Vec<_> = self
|
let ice = Ice {
|
||||||
.instructions
|
viewport: Size::new(
|
||||||
.iter()
|
self.viewport.width as u32,
|
||||||
.map(Instruction::to_string)
|
self.viewport.height as u32,
|
||||||
.collect();
|
),
|
||||||
|
mode: self.mode,
|
||||||
|
preset: self.preset.clone(),
|
||||||
|
instructions: self.instructions.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
let export = rfd::AsyncFileDialog::new()
|
let export = rfd::AsyncFileDialog::new()
|
||||||
.add_filter("ice", &["ice"])
|
.add_filter("ice", &["ice"])
|
||||||
|
|
@ -217,28 +224,21 @@ impl<P: Program + 'static> Tester<P> {
|
||||||
};
|
};
|
||||||
|
|
||||||
let _ = thread::spawn(move || {
|
let _ = thread::spawn(move || {
|
||||||
fs::write(file.path(), test.join("\n"))
|
fs::write(file.path(), ice.to_string())
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.discard()
|
.discard()
|
||||||
}
|
}
|
||||||
Message::Imported(instructions) => {
|
Message::Imported(Ok(ice)) => {
|
||||||
let Some(instructions) = instructions else {
|
self.viewport = Size::new(
|
||||||
return Task::none();
|
ice.viewport.width as f32,
|
||||||
};
|
ice.viewport.height as f32,
|
||||||
|
);
|
||||||
let instructions: Result<Vec<_>, _> =
|
self.mode = ice.mode;
|
||||||
instructions.lines().map(Instruction::parse).collect();
|
self.preset = ice.preset;
|
||||||
|
self.instructions = ice.instructions;
|
||||||
match instructions {
|
self.edit = None;
|
||||||
Ok(instructions) => {
|
self.state = State::Idle;
|
||||||
self.instructions = instructions;
|
|
||||||
self.edit = None;
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
log::error!("{error}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
@ -268,6 +268,11 @@ impl<P: Program + 'static> Tester<P> {
|
||||||
Message::Confirm => {
|
Message::Confirm => {
|
||||||
self.confirm();
|
self.confirm();
|
||||||
|
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::Imported(Err(error)) => {
|
||||||
|
log::error!("{error}");
|
||||||
|
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
use crate::Program;
|
use crate::Program;
|
||||||
use crate::core::window;
|
|
||||||
use crate::core::{Element, Theme};
|
use crate::core::{Element, Theme};
|
||||||
use crate::futures::Subscription;
|
|
||||||
use crate::runtime::Task;
|
use crate::runtime::Task;
|
||||||
use crate::widget::horizontal_space;
|
use crate::widget::horizontal_space;
|
||||||
|
|
||||||
|
|
@ -24,10 +22,6 @@ impl<P: Program> Tester<P> {
|
||||||
Self { _type: PhantomData }
|
Self { _type: PhantomData }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_idle(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_busy(&self) -> bool {
|
pub fn is_busy(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
@ -40,10 +34,6 @@ impl<P: Program> Tester<P> {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn subscription(&self, _program: &P) -> Subscription<Tick<P>> {
|
|
||||||
Subscription::none()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn view<'a, T: 'static>(
|
pub fn view<'a, T: 'static>(
|
||||||
&'a self,
|
&'a self,
|
||||||
_program: &P,
|
_program: &P,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
|
viewport: 512x768
|
||||||
|
mode: impatient
|
||||||
|
preset: Empty
|
||||||
|
-----
|
||||||
click left at (377.80, 236.50)
|
click left at (377.80, 236.50)
|
||||||
type "Create the universe"
|
type "Create the universe"
|
||||||
type enter
|
type enter
|
||||||
|
|
@ -5,4 +9,6 @@ type "Make an apple pie"
|
||||||
type enter
|
type enter
|
||||||
click left at (135.40, 351.70)
|
click left at (135.40, 351.70)
|
||||||
click left at (153.80, 398.10)
|
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
169
test/src/ice.rs
Normal 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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,7 @@ use crate::simulator;
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum Instruction {
|
pub enum Instruction {
|
||||||
Interact(Interaction),
|
Interact(Interaction),
|
||||||
Expect(Expectation),
|
Expect(Expectation),
|
||||||
|
|
@ -28,7 +28,7 @@ impl fmt::Display for Instruction {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum Interaction {
|
pub enum Interaction {
|
||||||
Mouse(Mouse),
|
Mouse(Mouse),
|
||||||
Keyboard(Keyboard),
|
Keyboard(Keyboard),
|
||||||
|
|
@ -239,7 +239,7 @@ impl fmt::Display for Interaction {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum Mouse {
|
pub enum Mouse {
|
||||||
Move(Point),
|
Move(Point),
|
||||||
Press {
|
Press {
|
||||||
|
|
@ -275,7 +275,7 @@ impl fmt::Display for Mouse {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum Keyboard {
|
pub enum Keyboard {
|
||||||
Press(Key),
|
Press(Key),
|
||||||
Release(Key),
|
Release(Key),
|
||||||
|
|
@ -357,7 +357,7 @@ mod format {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum Expectation {
|
pub enum Expectation {
|
||||||
Presence(Selector),
|
Presence(Selector),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,7 @@ use iced_runtime as runtime;
|
||||||
use iced_runtime::core;
|
use iced_runtime::core;
|
||||||
|
|
||||||
pub mod emulator;
|
pub mod emulator;
|
||||||
|
pub mod ice;
|
||||||
pub mod instruction;
|
pub mod instruction;
|
||||||
pub mod selector;
|
pub mod selector;
|
||||||
pub mod simulator;
|
pub mod simulator;
|
||||||
|
|
@ -98,6 +99,7 @@ mod error;
|
||||||
|
|
||||||
pub use emulator::Emulator;
|
pub use emulator::Emulator;
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
|
pub use ice::Ice;
|
||||||
pub use instruction::Instruction;
|
pub use instruction::Instruction;
|
||||||
pub use selector::Selector;
|
pub use selector::Selector;
|
||||||
pub use simulator::{Simulator, simulator};
|
pub use simulator::{Simulator, simulator};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue