clawless_core/prompt/
scripted_user.rs1use std::collections::VecDeque;
2
3use super::{AnswerPromptError, ScriptedAnswer};
4use crate::event::prompt::PromptRequest;
5use crate::event::{Event, EventReceiver};
6
7#[derive(Clone, Eq, PartialEq, Hash, Debug, Default)]
47pub struct ScriptedUser {
48 answers: VecDeque<ScriptedAnswer>,
50}
51
52impl ScriptedUser {
53 pub fn new(answers: impl IntoIterator<Item = ScriptedAnswer>) -> Self {
66 Self {
67 answers: answers.into_iter().collect(),
68 }
69 }
70
71 pub async fn attend(mut self, mut receiver: EventReceiver) -> Vec<Event> {
78 let mut events = Vec::new();
79
80 while let Some(event) = receiver.recv().await {
81 match event {
82 Event::Prompt(request) => self.answer(*request),
83 Event::Message(_) | Event::Detail(_) | Event::Artifact(_) | Event::Process(_) => {
84 events.push(event);
85 }
86 }
87 }
88
89 events
90 }
91
92 fn answer(&mut self, request: PromptRequest) {
94 match (request, self.answers.pop_front()) {
95 (PromptRequest::Confirm { reply, .. }, Some(ScriptedAnswer::Confirm(answer))) => {
96 reply.answer(answer);
97 }
98 (PromptRequest::Select { reply, .. }, Some(ScriptedAnswer::Select(answer))) => {
99 reply.answer(answer);
100 }
101 (PromptRequest::Text { reply, .. }, Some(ScriptedAnswer::Text(answer))) => {
102 reply.answer(answer);
103 }
104 (
105 request @ PromptRequest::Confirm { .. },
106 Some(ScriptedAnswer::Select(_) | ScriptedAnswer::Text(_)) | None,
107 ) => request.fail(AnswerPromptError::UnscriptedPrompt),
108 (
109 request @ PromptRequest::Select { .. },
110 Some(ScriptedAnswer::Confirm(_) | ScriptedAnswer::Text(_)) | None,
111 ) => request.fail(AnswerPromptError::UnscriptedPrompt),
112 (
113 request @ PromptRequest::Text { .. },
114 Some(ScriptedAnswer::Confirm(_) | ScriptedAnswer::Select(_)) | None,
115 ) => request.fail(AnswerPromptError::UnscriptedPrompt),
116 }
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 #![allow(clippy::missing_panics_doc)]
125
126 use super::*;
127 use crate::context::Interactivity;
128 use crate::event::event_channel;
129 use crate::output::Output;
130 use crate::prompt::{Confirmation, Prompt, PromptUserError};
131
132 fn is_unscripted(error: &PromptUserError) -> bool {
134 match error {
135 PromptUserError::UnansweredPrompt { source, .. } => match source {
136 AnswerPromptError::UnscriptedPrompt => true,
137 AnswerPromptError::ClosedInput => false,
138 AnswerPromptError::DroppedRequest => false,
139 AnswerPromptError::UnusableTerminal { .. } => false,
140 },
141 PromptUserError::AbsentUser { .. } => false,
142 PromptUserError::CancelledPrompt { .. } => false,
143 PromptUserError::UndeliverablePrompt { .. } => false,
144 PromptUserError::MissingOptions { .. } => false,
145 PromptUserError::UnknownOption { .. } => false,
146 }
147 }
148
149 fn scripted(
151 answers: impl IntoIterator<Item = ScriptedAnswer>,
152 ) -> (Prompt, tokio::task::JoinHandle<Vec<Event>>) {
153 let (sender, receiver) = event_channel();
154 let user = tokio::spawn(ScriptedUser::new(answers).attend(receiver));
155 let prompt = Prompt::builder()
156 .output(Output::new(sender))
157 .interactivity(Interactivity::Interactive)
158 .build();
159
160 (prompt, user)
161 }
162
163 #[tokio::test]
164 async fn attend_answers_a_confirmation_from_the_script() {
165 let (prompt, _user) = scripted([ScriptedAnswer::Confirm(Confirmation::Yes)]);
166
167 let answer = prompt.confirm("Release?").await.expect("should answer");
168
169 assert_eq!(answer, Confirmation::Yes);
170 }
171
172 #[tokio::test]
173 async fn attend_answers_in_the_order_of_the_script() {
174 let (prompt, _user) = scripted([
175 ScriptedAnswer::Confirm(Confirmation::Yes),
176 ScriptedAnswer::Confirm(Confirmation::No),
177 ]);
178 drop(prompt.confirm("Release?").await);
179
180 let answer = prompt.confirm("Publish?").await.expect("should answer");
181
182 assert_eq!(answer, Confirmation::No);
183 }
184
185 #[tokio::test]
186 async fn attend_answers_a_selection_from_the_script() {
187 let (prompt, _user) = scripted([ScriptedAnswer::Select(1)]);
188
189 let answer = prompt
190 .select("Port", [80, 443])
191 .await
192 .expect("should answer");
193
194 assert_eq!(answer, 443);
195 }
196
197 #[tokio::test]
198 async fn attend_answers_a_text_prompt_from_the_script() {
199 let (prompt, _user) = scripted([ScriptedAnswer::Text("Fix the race".to_owned())]);
200
201 let answer = prompt.text("Title").await.expect("should answer");
202
203 assert_eq!(answer, "Fix the race");
204 }
205
206 #[tokio::test]
207 async fn attend_returns_the_events_that_are_no_prompts() {
208 let (sender, receiver) = event_channel();
209 let user = tokio::spawn(ScriptedUser::default().attend(receiver));
210 let output = Output::new(sender);
211 output.message("released").await.expect("should send");
212 drop(output);
213
214 let events = user.await.expect("should join");
215
216 assert_eq!(
217 events
218 .into_iter()
219 .map(|event| match event {
220 Event::Message(text) => text,
221 Event::Detail(text) => text,
222 Event::Artifact(artifact) => artifact.to_string(),
223 Event::Process(event) => event.to_string(),
224 Event::Prompt(request) => format!("{request:?}"),
225 })
226 .collect::<Vec<_>>(),
227 vec!["released".to_owned()]
228 );
229 }
230
231 #[tokio::test]
232 async fn attend_with_an_answer_for_another_kind_fails_the_prompt() {
233 let (prompt, _user) = scripted([ScriptedAnswer::Confirm(Confirmation::Yes)]);
234
235 let error = prompt.text("Title").await.expect_err("should fail");
236
237 assert!(is_unscripted(&error));
238 }
239
240 #[tokio::test]
241 async fn attend_with_an_empty_script_fails_the_prompt() {
242 let (prompt, _user) = scripted([]);
243
244 let error = prompt.confirm("Release?").await.expect_err("should fail");
245
246 assert!(is_unscripted(&error));
247 }
248
249 #[test]
250 fn trait_send() {
251 fn assert_send<T: Send>() {}
252 assert_send::<ScriptedUser>();
253 }
254
255 #[test]
256 fn trait_sync() {
257 fn assert_sync<T: Sync>() {}
258 assert_sync::<ScriptedUser>();
259 }
260
261 #[test]
262 fn trait_unpin() {
263 fn assert_unpin<T: Unpin>() {}
264 assert_unpin::<ScriptedUser>();
265 }
266}