extern crate twig; extern crate serde_json; extern crate difference; extern crate term; #[macro_use] extern crate log; extern crate env_logger; use std::collections::HashMap; use std::io::{self, Read, BufReader, BufRead}; use std::env; use std::io::Write; use difference::Difference; use std::fs::{self, DirEntry, File}; use std::path::Path; use twig::environment::{ Environment, Config }; use twig::loader::ArrayLoader; use twig::Engine; #[test] fn fixtures() { env_logger::init().unwrap(); let errors = visit_fixtures(&env::current_dir().unwrap().join("tests").join("fixtures"), &|entry| { let f = match File::open(entry.path()) { Ok(f) => f, Err(e) => panic!("error opening fixture file {:?}, {:?}", entry.path(), e), }; let fixture = match Fixture::new(f) { Err(e) => panic!("invalid test {:?}", e), Ok(f) => f, }; let message = match fixture.message.clone() { Some(m) => m, None => panic!("fixture {:?} must have a message", entry.path()), }; print_fixture_start(&message).unwrap(); let mut twig = Engine::new(ArrayLoader::new( vec![("index.twig".into(), fixture.template.expect("fixture must contain main template"))] .into_iter() .chain(fixture.templates.into_iter()) .collect() ), match fixture.config { Some(config) => Environment::new(Config::from_hashmap( match serde_json::from_str(&config) { Ok(map) => map, Err(e) => panic!("failed to deserialize template config: {:#?}", e), } )), _ => Environment::default(), }); let data = match fixture.data { Some(data) => match serde_json::from_str::>(&data) { Ok(map) => map, Err(e) => panic!("failed to deserialize template data: {:#?}", e), }, None => HashMap::new(), }; let res = match twig.get("index.twig", data) { Ok(res) => res, Err(e) => panic!("\nerror executing template:\n {:#?}\n", e), }; let expected = fixture.expect.expect("fixture must have expect block"); if res != expected { print_fixture_result(false).unwrap(); let (_, changeset) = difference::diff( &res, &expected, "\n" ); print_diff(&changeset).unwrap(); Some(( entry.path().to_string_lossy().into_owned(), message, changeset )) //assert_eq!(res, expected); } else { print_fixture_result(true).unwrap(); None } }).unwrap(); let num_errors = errors.len(); if num_errors > 0 { for (file, name, changeset) in errors { println!("in {}", file); println!("testing {}", name); print_uncolored(&changeset); } //panic!("{} fixtures produced errors", num_errors); } } fn visit_fixtures(dir: &Path, cb: &Fn(&DirEntry) -> Option<(String, String, Vec)>) -> io::Result)>> { let mut errors = Vec::new(); if try!(fs::metadata(dir)).is_dir() { for entry in try!(fs::read_dir(dir)) { let entry = try!(entry); if try!(fs::metadata(entry.path())).is_dir() { for e in try!(visit_fixtures(&entry.path(), cb)) { errors.push(e); } } else { if let Some(Some("test")) = entry.path().extension().map(|v| v.to_str()) { if let Some(err) = cb(&entry) { errors.push(err); } } } } } Ok(errors) } #[derive(Debug)] enum FixtureError { ExpectedBlockStart, IoError, } #[derive(Debug)] enum TemplateName { Main, Other(String), } #[derive(Debug)] enum ReadState { Message(String), Template((TemplateName, String)), Data(String), Config(String), Expect(String) } #[derive(Debug)] struct Fixture { message: Option, template: Option, templates: Vec<(String, String)>, data: Option, config: Option, expect: Option, } const TEMPLATE_NAME_START: &'static str = "--TEMPLATE("; const TEMPLATE_NAME_END: &'static str = ")--"; fn check_for_new_state(line: &str) -> Option { match &line[..] { "--TEST--" => Some(ReadState::Message(String::new())), "--DATA--" => Some(ReadState::Data(String::new())), "--CONFIG--" => Some(ReadState::Config(String::new())), "--EXPECT--" => Some(ReadState::Expect(String::new())), other => if other.starts_with("--TEMPLATE(") { let name = other[TEMPLATE_NAME_START.len()..other.len()-TEMPLATE_NAME_END.len()].to_string(); Some(ReadState::Template((TemplateName::Other(name), String::new()))) } else if other.starts_with("--TEMPLATE--") { Some(ReadState::Template((TemplateName::Main, String::new()))) } else { None }, } } impl Fixture { pub fn new(input: R) -> Result { let mut state = None; let mut fixture = Fixture { message: None, template: None, templates: Vec::new(), data: None, config: None, expect: None, }; for maybe_line in BufReader::new(input).lines() { let line = match maybe_line { Ok(l) => l, Err(_) => return Err(FixtureError::IoError), }; state = match state { None => { Some(try!(check_for_new_state(&line).ok_or(FixtureError::ExpectedBlockStart))) }, Some(mut old) => match check_for_new_state(&line) { Some(new_state) => { fixture.collect(old); Some(new_state) }, None => { match old { ReadState::Message(ref mut m) => { if m.len() > 0 { m.push_str("\n"); } m.push_str(&line); }, ReadState::Template((_, ref mut m)) => { if m.len() > 0 { m.push_str("\n"); } m.push_str(&line); }, ReadState::Data(ref mut m) => { if m.len() > 0 { m.push_str("\n"); } m.push_str(&line); }, ReadState::Config(ref mut m) => { if m.len() > 0 { m.push_str("\n"); } m.push_str(&line); }, ReadState::Expect(ref mut m) => { if m.len() > 0 { m.push_str("\n"); } m.push_str(&line); }, }; Some(old) } }, } } if let Some(leftover_state) = state { fixture.collect(leftover_state); } Ok(fixture) } fn collect(&mut self, state: ReadState) { match state { ReadState::Message(m) => self.message = Some(m), ReadState::Template((TemplateName::Main, m)) => self.template = Some(m), ReadState::Template((TemplateName::Other(name), m)) => { self.templates.push((name, m)); }, ReadState::Data(m) => self.data = Some(m), ReadState::Config(m) => self.config = Some(m), ReadState::Expect(m) => self.expect = Some(m), } } } fn print_fixture_start(message: &str) -> io::Result<()> { let mut t = term::stdout().unwrap(); try!(write!(t, "fixture ")); try!(t.attr(term::Attr::Bold)); try!(write!(t, "{}", message)); try!(t.reset()); try!(write!(t, " ... ")); t.flush() } fn print_fixture_result(ok: bool) -> io::Result<()> { let mut t = term::stdout().unwrap(); if ok { try!(t.fg(term::color::GREEN)); try!(writeln!(t, "ok")); } else { try!(t.fg(term::color::RED)); try!(writeln!(t, "ERROR!")); } try!(t.reset()); t.flush() } fn print_diff(changeset: &Vec) -> io::Result<()> { let mut t = term::stdout().unwrap(); for i in 0..changeset.len() { match changeset[i] { Difference::Same(ref x) => { try!(t.reset()); try!(writeln!(t, " {}", x)); }, Difference::Add(ref x) => { for line in x.lines() { try!(t.fg(term::color::GREEN)); try!(writeln!(t, "+ {}", line)); } }, Difference::Rem(ref x) => { for line in x.lines() { try!(t.fg(term::color::RED)); try!(writeln!(t, "- {}", line)); } } } } try!(t.reset()); try!(writeln!(t, "")); t.flush() } fn print_uncolored(changeset: &Vec) { for i in 0..changeset.len() { match changeset[i] { Difference::Same(ref x) => { println!(" {}", x); }, Difference::Add(ref x) => { for line in x.lines() { println!("+ {}", line); } }, Difference::Rem(ref x) => { for line in x.lines() { println!("- {}", line); } } } } println!(""); }