use std::collections::VecDeque;
use super::{AnswerPromptError, ScriptedAnswer};
use crate::event::prompt::PromptRequest;
use crate::event::{Event, EventReceiver};
#[derive(Clone, Eq, PartialEq, Hash, Debug, Default)]
pub struct ScriptedUser {
answers: VecDeque<ScriptedAnswer>,
}
impl ScriptedUser {
pub fn new(answers: impl IntoIterator<Item = ScriptedAnswer>) -> Self {
Self {
answers: answers.into_iter().collect(),
}
}
pub async fn attend(mut self, mut receiver: EventReceiver) -> Vec<Event> {
let mut events = Vec::new();
while let Some(event) = receiver.recv().await {
match event {
Event::Prompt(request) => self.answer(*request),
Event::Message(_) | Event::Detail(_) | Event::Artifact(_) | Event::Process(_) => {
events.push(event);
}
}
}
events
}
fn answer(&mut self, request: PromptRequest) {
match (request, self.answers.pop_front()) {
(PromptRequest::Confirm { reply, .. }, Some(ScriptedAnswer::Confirm(answer))) => {
reply.answer(answer);
}
(PromptRequest::Select { reply, .. }, Some(ScriptedAnswer::Select(answer))) => {
reply.answer(answer);
}
(PromptRequest::Text { reply, .. }, Some(ScriptedAnswer::Text(answer))) => {
reply.answer(answer);
}
(
request @ PromptRequest::Confirm { .. },
Some(ScriptedAnswer::Select(_) | ScriptedAnswer::Text(_)) | None,
) => request.fail(AnswerPromptError::UnscriptedPrompt),
(
request @ PromptRequest::Select { .. },
Some(ScriptedAnswer::Confirm(_) | ScriptedAnswer::Text(_)) | None,
) => request.fail(AnswerPromptError::UnscriptedPrompt),
(
request @ PromptRequest::Text { .. },
Some(ScriptedAnswer::Confirm(_) | ScriptedAnswer::Select(_)) | None,
) => request.fail(AnswerPromptError::UnscriptedPrompt),
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::missing_panics_doc)]
use super::*;
use crate::context::Interactivity;
use crate::event::event_channel;
use crate::output::Output;
use crate::prompt::{Confirmation, Prompt, PromptUserError};
fn is_unscripted(error: &PromptUserError) -> bool {
match error {
PromptUserError::UnansweredPrompt { source, .. } => match source {
AnswerPromptError::UnscriptedPrompt => true,
AnswerPromptError::ClosedInput => false,
AnswerPromptError::DroppedRequest => false,
AnswerPromptError::UnusableTerminal { .. } => false,
},
PromptUserError::AbsentUser { .. } => false,
PromptUserError::CancelledPrompt { .. } => false,
PromptUserError::UndeliverablePrompt { .. } => false,
PromptUserError::MissingOptions { .. } => false,
PromptUserError::UnknownOption { .. } => false,
}
}
fn scripted(
answers: impl IntoIterator<Item = ScriptedAnswer>,
) -> (Prompt, tokio::task::JoinHandle<Vec<Event>>) {
let (sender, receiver) = event_channel();
let user = tokio::spawn(ScriptedUser::new(answers).attend(receiver));
let prompt = Prompt::builder()
.output(Output::new(sender))
.interactivity(Interactivity::Interactive)
.build();
(prompt, user)
}
#[tokio::test]
async fn attend_answers_a_confirmation_from_the_script() {
let (prompt, _user) = scripted([ScriptedAnswer::Confirm(Confirmation::Yes)]);
let answer = prompt.confirm("Release?").await.expect("should answer");
assert_eq!(answer, Confirmation::Yes);
}
#[tokio::test]
async fn attend_answers_in_the_order_of_the_script() {
let (prompt, _user) = scripted([
ScriptedAnswer::Confirm(Confirmation::Yes),
ScriptedAnswer::Confirm(Confirmation::No),
]);
drop(prompt.confirm("Release?").await);
let answer = prompt.confirm("Publish?").await.expect("should answer");
assert_eq!(answer, Confirmation::No);
}
#[tokio::test]
async fn attend_answers_a_selection_from_the_script() {
let (prompt, _user) = scripted([ScriptedAnswer::Select(1)]);
let answer = prompt
.select("Port", [80, 443])
.await
.expect("should answer");
assert_eq!(answer, 443);
}
#[tokio::test]
async fn attend_answers_a_text_prompt_from_the_script() {
let (prompt, _user) = scripted([ScriptedAnswer::Text("Fix the race".to_owned())]);
let answer = prompt.text("Title").await.expect("should answer");
assert_eq!(answer, "Fix the race");
}
#[tokio::test]
async fn attend_returns_the_events_that_are_no_prompts() {
let (sender, receiver) = event_channel();
let user = tokio::spawn(ScriptedUser::default().attend(receiver));
let output = Output::new(sender);
output.message("released").await.expect("should send");
drop(output);
let events = user.await.expect("should join");
assert_eq!(
events
.into_iter()
.map(|event| match event {
Event::Message(text) => text,
Event::Detail(text) => text,
Event::Artifact(artifact) => artifact.to_string(),
Event::Process(event) => event.to_string(),
Event::Prompt(request) => format!("{request:?}"),
})
.collect::<Vec<_>>(),
vec!["released".to_owned()]
);
}
#[tokio::test]
async fn attend_with_an_answer_for_another_kind_fails_the_prompt() {
let (prompt, _user) = scripted([ScriptedAnswer::Confirm(Confirmation::Yes)]);
let error = prompt.text("Title").await.expect_err("should fail");
assert!(is_unscripted(&error));
}
#[tokio::test]
async fn attend_with_an_empty_script_fails_the_prompt() {
let (prompt, _user) = scripted([]);
let error = prompt.confirm("Release?").await.expect_err("should fail");
assert!(is_unscripted(&error));
}
#[test]
fn trait_send() {
fn assert_send<T: Send>() {}
assert_send::<ScriptedUser>();
}
#[test]
fn trait_sync() {
fn assert_sync<T: Sync>() {}
assert_sync::<ScriptedUser>();
}
#[test]
fn trait_unpin() {
fn assert_unpin<T: Unpin>() {}
assert_unpin::<ScriptedUser>();
}
}