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)) = value.trim().split_once('x')
82 && let Ok(width) = width.parse()
83 && let Ok(height) = height.parse()
84 {
85 Size::new(width, height)
86 } else {
87 return Err(ParseError::InvalidViewport {
88 line: i,
89 value: value.to_owned(),
90 });
91 },
92 );
93 }
94 "mode" => {
95 mode = Some(match value.trim().to_lowercase().as_str() {
96 "zen" => emulator::Mode::Zen,
97 "patient" => emulator::Mode::Patient,
98 "immediate" => emulator::Mode::Immediate,
99 _ => {
100 return Err(ParseError::InvalidMode {
101 line: i,
102 value: value.to_owned(),
103 });
104 }
105 });
106 }
107 "preset" => {
108 preset = Some(value.trim().to_owned());
109 }
110 field => {
111 return Err(ParseError::UnknownField {
112 line: i,
113 field: field.to_owned(),
114 });
115 }
116 }
117 }
118
119 let Some(viewport) = viewport else {
120 return Err(ParseError::MissingViewport);
121 };
122
123 let Some(mode) = mode else {
124 return Err(ParseError::MissingMode);
125 };
126
127 let instructions = rest
128 .lines()
129 .skip(1)
130 .enumerate()
131 .map(|(i, line)| {
132 Instruction::parse(line).map_err(|error| ParseError::InvalidInstruction {
133 line: metadata.lines().count() + 1 + i,
134 error,
135 })
136 })
137 .collect::<Result<Vec<_>, _>>()?;
138
139 Ok(Self {
140 viewport,
141 mode,
142 preset,
143 instructions,
144 })
145 }
146}
147
148impl std::fmt::Display for Ice {
149 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150 writeln!(
151 f,
152 "viewport: {width}x{height}",
153 width = self.viewport.width as u32, // TODO
154 height = self.viewport.height as u32, // TODO
155 )?;
156
157 writeln!(f, "mode: {}", self.mode)?;
158
159 if let Some(preset) = &self.preset {
160 writeln!(f, "preset: {preset}")?;
161 }
162
163 f.write_str("-----\n")?;
164
165 for instruction in &self.instructions {
166 instruction.fmt(f)?;
167 f.write_str("\n")?;
168 }
169
170 Ok(())
171 }
172}
173
174/// An error produced during [`Ice::parse`].
175#[derive(Debug, Clone, thiserror::Error)]
176pub enum ParseError {
177 /// No metadata is present.
178 #[error("the ice test has no metadata")]
179 NoMetadata,
180
181 /// The metadata is invalid.
182 #[error("invalid metadata in line {line}: \"{content}\"")]
183 InvalidMetadata {
184 /// The number of the invalid line.
185 line: usize,
186 /// The content of the invalid line.
187 content: String,
188 },
189
190 /// The viewport is invalid.
191 #[error("invalid viewport in line {line}: \"{value}\"")]
192 InvalidViewport {
193 /// The number of the invalid line.
194 line: usize,
195
196 /// The invalid value.
197 value: String,
198 },
199
200 /// The [`emulator::Mode`] is invalid.
201 #[error("invalid mode in line {line}: \"{value}\"")]
202 InvalidMode {
203 /// The number of the invalid line.
204 line: usize,
205 /// The invalid value.
206 value: String,
207 },
208
209 /// A metadata field is unknown.
210 #[error("unknown metadata field in line {line}: \"{field}\"")]
211 UnknownField {
212 /// The number of the invalid line.
213 line: usize,
214 /// The name of the unknown field.
215 field: String,
216 },
217
218 /// Viewport metadata is missing.
219 #[error("metadata is missing the viewport field")]
220 MissingViewport,
221
222 /// [`emulator::Mode`] metadata is missing.
223 #[error("metadata is missing the mode field")]
224 MissingMode,
225
226 /// An [`Instruction`] failed to parse.
227 #[error("invalid instruction in line {line}: {error}")]
228 InvalidInstruction {
229 /// The number of the invalid line.
230 line: usize,
231 /// The parse error.
232 error: instruction::ParseError,
233 },
234}