use std::error::Error;
use std::fs;
use std::io;
use std::path::Path;
use prost::Message;
use chapter::*;
#[derive(Debug, PartialEq, Eq)]
pub enum PlanStep {
Line(String),
Tags(Vec<String>),
Option(String),
Select(u32),
Command(String),
Stop,
}
impl PlanStep {
fn new(line: &str) -> Self {
let mut split_line = line.splitn(2, ": ");
match split_line.next() {
Some("line") => Self::Line(split_line.next().unwrap().to_owned()),
Some("tags") => Self::Tags(
split_line
.collect::<String>()
.split(" ")
.map(|item| item.to_owned())
.collect::<Vec<String>>(),
),
Some("option") => Self::Option(split_line.next().unwrap().to_owned()),
Some("select") => {
let index: u32 = split_line.next().and_then(|s| s.parse().ok()).unwrap();
if index < 1 {
panic!("Select index must be 1 or greater.");
}
Self::Select(index - 1)
}
Some("command") => Self::Command(split_line.next().unwrap().to_owned()),
Some("stop") => Self::Stop,
Some(step) => panic!(
"Could not parse test plan step \"{}\" in line \"{}\"",
step, line
),
None => panic!("Could not parse test plan step in line \"{}\"", line),
}
}
}
pub struct TestPlan {
steps: Vec<PlanStep>,
next_step_index: usize,
options: Vec<String>,
}
impl TestPlan {
pub fn load(plan_path: &Path) -> io::Result<Self> {
let plan_text = fs::read_to_string(plan_path)?;
let steps: Vec<_> = plan_text
.lines()
.map(|line| line.trim_start())
.filter(|line| !line.is_empty() && !line.starts_with('#'))
.map(|line| PlanStep::new(line))
.collect();
Ok(Self {
steps,
next_step_index: 0,
options: Vec::new(),
})
}
pub fn next(&mut self) {
let prev_step = match self.next_step_index {
i if i > 0 && i <= self.steps.len() => Some(&self.steps[i as usize - 1]),
_ => None,
};
if let Some(PlanStep::Select(_)) = prev_step {
self.options.clear();
}
while self.next_step_index <= self.steps.len() {
let current_step = self.steps.get(self.next_step_index);
self.next_step_index += 1;
match current_step {
Some(PlanStep::Option(option)) => {
self.options.push(option.clone());
continue;
}
_ => return,
}
}
return;
}
pub fn get_current_step(&self) -> Option<&PlanStep> {
match self.next_step_index {
0 => None,
i if i <= self.steps.len() => Some(&self.steps[self.next_step_index as usize - 1]),
_ => Some(&PlanStep::Stop),
}
}
}
pub struct PlanRunner {
vm: VirtualMachine,
string_table: Vec<LineInfo>,
metadata_table: Vec<MetadataInfo>,
plan: TestPlan,
locale: String,
}
impl PlanRunner {
pub fn new(yarn_path: &str) -> Self {
let yarn_path = Path::new(yarn_path);
let proto_path = yarn_path.with_extension("yarnc");
let proto_data = fs::read(&proto_path).unwrap();
let program = Program::decode(&*proto_data).unwrap();
let mut csv_path = proto_path.clone();
csv_path.set_file_name(format!(
"{}-Lines.csv",
csv_path.file_stem().unwrap().to_str().unwrap()
));
let string_table: Vec<LineInfo> = csv::Reader::from_path(csv_path)
.unwrap()
.deserialize()
.map(|result| result.unwrap())
.collect();
let mut csv_path = proto_path;
csv_path.set_file_name(format!(
"{}-Metadata.csv",
csv_path.file_stem().unwrap().to_str().unwrap()
));
let metadata_table: Vec<MetadataInfo> = csv::ReaderBuilder::new()
.flexible(true)
.from_path(csv_path)
.unwrap()
.deserialize()
.map(|result| result.unwrap())
.collect();
let mut vm = VirtualMachine::new(program);
vm.library.insert(
"assert".to_string(),
FunctionInfo::new(1, &|_vm: &mut VirtualMachine, parameters: &[YarnValue]| {
if !parameters[0].as_bool() {
assert!(false, "Assertion failed");
}
}),
);
let plan_path = yarn_path.with_extension("testplan");
let plan = TestPlan::load(&plan_path).unwrap();
Self {
vm,
string_table,
metadata_table,
plan,
locale: "en".to_string(),
}
}
pub fn get_vm(&mut self) -> &mut VirtualMachine {
&mut self.vm
}
fn get_tags_for_line(&self, line: &Line) -> Vec<String> {
self.metadata_table
.iter()
.find(|metadata_info| metadata_info.id == line.id)
.map(|metadata_info| &metadata_info.tags)
.cloned()
.unwrap_or_else(|| Vec::new())
}
fn get_composed_text_for_line(&self, line: &Line) -> String {
let mut line_text = self
.string_table
.iter()
.find(|line_info| line_info.id == line.id)
.map(|line_info| &line_info.text)
.unwrap()
.clone();
for (i, substitution) in line.substitutions.iter().enumerate() {
line_text = line_text.replacen(&format!("{{{}}}", i), &substitution, 1);
}
expand_format_functions(&line_text, &self.locale)
}
pub fn run(&mut self) -> Result<(), Box<dyn Error>> {
self.vm.set_node("Start")?;
loop {
match self.vm.continue_dialogue()? {
SuspendReason::Nop => {}
SuspendReason::Line(line) => {
self.plan.next();
let plan_step = self.plan.get_current_step().unwrap();
let line_text = self.get_composed_text_for_line(&line);
assert!(
matches!(plan_step, PlanStep::Line(plan_text) if *plan_text == line_text),
"[{}] Expected the line {:?}, got \"{}\"",
self.plan.next_step_index,
plan_step,
line_text
);
let tags = self.get_tags_for_line(&line);
if !tags.is_empty() {
self.plan.next();
let plan_step = self.plan.get_current_step().unwrap();
if let PlanStep::Tags(found_tags) = plan_step {
assert!(
tags.iter().zip(found_tags).all(|(a, b)| a == b),
"[{}] Expected tag step \"{plan_step:?}\" but found tags: {tags:?}",
self.plan.next_step_index
)
} else {
panic!(
"[{}] Expected tag step with tags: {tags:?}, bot got \"{plan_step:?}\"",
self.plan.next_step_index
)
}
assert!(
matches!(plan_step, PlanStep::Tags(found_tags) if *found_tags == tags),
"[{}] Expected tag step {:?}, got \"{tags:?}\"",
self.plan.next_step_index,
plan_step,
);
}
}
SuspendReason::Options(options) => {
self.plan.next();
let plan_step = self.plan.get_current_step().unwrap();
for (option, plan_option) in options.into_iter().zip(&self.plan.options) {
let option_text = self.get_composed_text_for_line(&option.line);
assert_eq!(option_text, *plan_option);
}
match plan_step {
PlanStep::Select(i) => self.vm.set_selected_option(*i)?,
step => panic!("Expected PlanStep::Select, got {:?}", step),
}
}
SuspendReason::Command(command) => {
self.plan.next();
let plan_step = self.plan.get_current_step().unwrap();
assert!(
matches!(plan_step, PlanStep::Command(plan_text) if *plan_text == command),
"Expected the command {:?}, got \"{}\"",
plan_step,
command
);
}
SuspendReason::NodeChange { .. } => {}
SuspendReason::DialogueComplete(_) => {
self.plan.next();
let plan_step = self.plan.get_current_step().unwrap();
assert_eq!(*plan_step, PlanStep::Stop);
break;
}
SuspendReason::InvalidOption(option_name) => {
panic!("Unexpected option name: {}", option_name);
}
}
}
Ok(())
}
}