iced_test/ice.rs
1//! A shareable, simple format of end-to-end tests.
2use crate::Instruction;
3use crate::core::Size;
4use crate::emulator;
5use crate::instruction;
6
7/// An end-to-end test for iced applications.
8///
9/// Ice tests encode a certain configuration together with a sequence of instructions.
10/// An ice test passes if all the instructions can be executed successfully.
11///
12/// Normally, ice tests are run by an [`Emulator`](crate::Emulator) in continuous
13/// integration pipelines.
14///
15/// Ice tests can be easily run by saving them as `.ice` files in a folder and simply
16/// calling [`run`](crate::run). These test files can be recorded by enabling the `tester`
17/// feature flag in the root crate.
18#[derive(Debug, Clone, PartialEq)]
19pub struct Ice {
20 /// The viewport [`Size`] that must be used for the test.
21 pub viewport: Size,
22 /// The [`emulator::Mode`] that must be used for the test.
23 pub mode: emulator::Mode,
24 /// The name of the [`Preset`](crate::program::Preset) that must be used for the test.
25 pub preset: Option<String>,
26 /// The sequence of instructions of the test.
27 pub instructions: Vec<Instruction>,
28}
29
30impl Ice {
31 /// Parses an [`Ice`] test from its textual representation.
32 ///
33 /// Here is an example of the [`Ice`] test syntax:
34 ///
35 /// ```text
36 /// viewport: 500x800
37 /// mode: Immediate
38 /// preset: Empty
39 /// -----
40 /// click "What needs to be done?"
41 /// type "Create the universe"
42 /// type enter
43 /// type "Make an apple pie"
44 /// type enter
45 /// expect "2 tasks left"
46 /// click "Create the universe"
47 /// expect "1 task left"
48 /// click "Make an apple pie"
49 /// expect "0 tasks left"
50 /// ```
51 ///
52 /// This syntax is _very_ experimental and extremely likely to change often.
53 /// For this reason, it is reserved for advanced users that want to early test it.
54 ///
55 /// Currently, in order to use it, you will need to earn the right and prove you understand
56 /// its experimental nature by reading the code!
57 pub fn parse(content: &str) -> Result<Self, ParseError> {
58 let Some((metadata, rest)) = content.split_once("-") else {
59 return Err(ParseError::NoMetadata);
60 };
61
62 let mut viewport = None;
63 let mut mode = None;
64 let mut preset = None;
65
66 for (i, line) in metadata.lines().enumerate() {
67 if line.trim().is_empty() {
68 continue;
69 }
70
71 let Some((field, value)) = line.split_once(':') else {
72 return Err(ParseError::InvalidMetadata {
73 line: i,
74 content: line.to_owned(),
75 });
76 };
77
78 match field.trim() {
79 "viewport" => {
80 viewport = Some(
81 if let Some((width, height)) =
82 value.trim().split_once('x')
83 && let Ok(width) = width.parse()
84 && let Ok(height) = height.parse()
85 {
86 Size::new(width, height)
87 } else {
88 return Err(ParseError::InvalidViewport {
89 line: i,
90 value: value.to_owned(),
91 });
92 },
93 );
94 }
95 "mode" => {
96 mode = Some(match value.trim().to_lowercase().as_str() {
97 "zen" => emulator::Mode::Zen,
98 "patient" => emulator::Mode::Patient,
99 "immediate" => emulator::Mode::Immediate,
100 _ => {
101 return Err(ParseError::InvalidMode {
102 line: i,
103 value: value.to_owned(),
104 });
105 }
106 });
107 }
108 "preset" => {
109 preset = Some(value.trim().to_owned());
110 }
111 field => {
112 return Err(ParseError::UnknownField {
113 line: i,
114 field: field.to_owned(),
115 });
116 }
117 }
118 }
119
120 let Some(viewport) = viewport else {
121 return Err(ParseError::MissingViewport);
122 };
123
124 let Some(mode) = mode else {
125 return Err(ParseError::MissingMode);
126 };
127
128 let instructions = rest
129 .lines()
130 .skip(1)
131 .enumerate()
132 .map(|(i, line)| {
133 Instruction::parse(line).map_err(|error| {
134 ParseError::InvalidInstruction {
135 line: metadata.lines().count() + 1 + i,
136 error,
137 }
138 })
139 })
140 .collect::<Result<Vec<_>, _>>()?;
141
142 Ok(Self {
143 viewport,
144 mode,
145 preset,
146 instructions,
147 })
148 }
149}
150
151impl std::fmt::Display for Ice {
152 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153 writeln!(
154 f,
155 "viewport: {width}x{height}",
156 width = self.viewport.width as u32, // TODO
157 height = self.viewport.height as u32, // TODO
158 )?;
159
160 writeln!(f, "mode: {}", self.mode)?;
161
162 if let Some(preset) = &self.preset {
163 writeln!(f, "preset: {preset}")?;
164 }
165
166 f.write_str("-----\n")?;
167
168 for instruction in &self.instructions {
169 instruction.fmt(f)?;
170 f.write_str("\n")?;
171 }
172
173 Ok(())
174 }
175}
176
177/// An error produced during [`Ice::parse`].
178#[derive(Debug, Clone, thiserror::Error)]
179pub enum ParseError {
180 /// No metadata is present.
181 #[error("the ice test has no metadata")]
182 NoMetadata,
183
184 /// The metadata is invalid.
185 #[error("invalid metadata in line {line}: \"{content}\"")]
186 InvalidMetadata {
187 /// The number of the invalid line.
188 line: usize,
189 /// The content of the invalid line.
190 content: String,
191 },
192
193 /// The viewport is invalid.
194 #[error("invalid viewport in line {line}: \"{value}\"")]
195 InvalidViewport {
196 /// The number of the invalid line.
197 line: usize,
198
199 /// The invalid value.
200 value: String,
201 },
202
203 /// The [`emulator::Mode`] is invalid.
204 #[error("invalid mode in line {line}: \"{value}\"")]
205 InvalidMode {
206 /// The number of the invalid line.
207 line: usize,
208 /// The invalid value.
209 value: String,
210 },
211
212 /// A metadata field is unknown.
213 #[error("unknown metadata field in line {line}: \"{field}\"")]
214 UnknownField {
215 /// The number of the invalid line.
216 line: usize,
217 /// The name of the unknown field.
218 field: String,
219 },
220
221 /// Viewport metadata is missing.
222 #[error("metadata is missing the viewport field")]
223 MissingViewport,
224
225 /// [`emulator::Mode`] metadata is missing.
226 #[error("metadata is missing the mode field")]
227 MissingMode,
228
229 /// An [`Instruction`] failed to parse.
230 #[error("invalid instruction in line {line}: {error}")]
231 InvalidInstruction {
232 /// The number of the invalid line.
233 line: usize,
234 /// The parse error.
235 error: instruction::ParseError,
236 },
237}