use super::includer::TestIncluder;
use crate::data::{PageInfo, ScoreValue};
use crate::layout::Layout;
use crate::parsing::ParseError;
use crate::render::html::HtmlRender;
use crate::render::Render;
use crate::settings::{WikitextMode, WikitextSettings};
use crate::tree::SyntaxTree;
use once_cell::sync::Lazy;
use std::borrow::Cow;
use std::fs::{self, File};
use std::io::Read;
use std::path::{Path, PathBuf};
use std::process;
const SKIP_TESTS: &[&str] = &[];
const ONLY_TESTS: &[&str] = &[];
static TEST_DIRECTORY: Lazy<PathBuf> = Lazy::new(|| {
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("test");
path
});
macro_rules! cow {
($text:expr) => {
Cow::Borrowed(&$text)
};
}
macro_rules! file_name {
($entry:expr) => {
$entry.file_name().to_string_lossy()
};
}
#[derive(Debug, Copy, Clone)]
pub enum TestResult {
Pass,
Fail,
Skip,
}
fn only_test_should_skip(name: &str) -> bool {
assert!(!ONLY_TESTS.is_empty());
for pattern in ONLY_TESTS.iter() {
if pattern == &name {
return false;
}
if pattern.ends_with('-') {
if name.starts_with(pattern) {
return false;
}
}
}
true
}
#[cfg(not(target_os = "windows"))]
fn process_newlines(_: &mut String) {}
#[cfg(target_os = "windows")]
fn process_newlines(text: &mut String) {
while let Some(idx) = text.find("\r\n") {
let range = idx..idx + 2;
text.replace_range(range, "\n");
}
}
#[derive(Serialize, Deserialize, Debug)]
struct Test<'a> {
#[serde(skip)]
name: String,
input: String,
tree: SyntaxTree<'a>,
errors: Vec<ParseError>,
#[serde(skip)]
html: String,
}
impl Test<'_> {
pub fn load(path: &Path, name: &str) -> Self {
assert!(path.is_absolute());
macro_rules! open_file {
($path:expr) => {
match File::open(&$path) {
Ok(file) => file,
Err(error) => {
panic!("Unable to open file '{}': {}", $path.display(), error)
}
}
};
}
macro_rules! load_output {
($name:expr, $extension:expr) => {{
let mut path = PathBuf::from(path);
path.set_extension($extension);
let mut file = open_file!(path);
let mut contents = String::new();
if let Err(error) = file.read_to_string(&mut contents) {
panic!(
"Unable to read {} file '{}': {}",
$name,
path.display(),
error,
);
}
process_newlines(&mut contents);
if contents.ends_with('\n') {
contents.pop();
}
contents
}};
}
let mut file = open_file!(path);
let mut test: Self = match serde_json::from_reader(&mut file) {
Ok(test) => test,
Err(error) => {
panic!("Unable to parse JSON file '{}': {}", path.display(), error)
}
};
test.name = str!(name);
test.html = load_output!("HTML", "html");
test
}
pub fn run(&self) -> TestResult {
if SKIP_TESTS.contains(&&*self.name) {
println!("+ {} [SKIPPED]", self.name);
return TestResult::Skip;
}
if !ONLY_TESTS.is_empty() && only_test_should_skip(&&*self.name) {
println!("+ {} [SKIPPED]", self.name);
return TestResult::Skip;
}
debug!(
"Running syntax tree test case {} on {}",
&self.name, &self.input,
);
println!("+ {}", self.name);
let page_info = PageInfo {
page: Cow::Owned(format!("page-{}", self.name)),
category: None,
site: cow!("test"),
title: cow!(self.name),
alt_title: None,
score: ScoreValue::Integer(0),
tags: vec![cow!("fruit"), cow!("component")],
language: cow!("default"),
};
let settings = WikitextSettings::from_mode(WikitextMode::Page, Layout::Wikidot);
let (mut text, _pages) =
crate::include(&self.input, &settings, TestIncluder, || unreachable!())
.unwrap_or_else(|x| match x {});
crate::preprocess(&mut text);
let tokens = crate::tokenize(&text);
let result = crate::parse(&tokens, &page_info, &settings);
let (mut tree, errors) = result.into();
tree.wikitext_len = self.tree.wikitext_len; let html_output = HtmlRender.render(&tree, &page_info, &settings);
fn json<T>(object: &T) -> String
where
T: serde::Serialize,
{
let mut output = serde_json::to_string_pretty(object)
.expect("Unable to serialize JSON to stdout");
output.insert_str(0, "Generated JSON: ");
output
}
let mut result = TestResult::Pass;
if tree != self.tree {
result = TestResult::Fail;
eprintln!(
"AST did not match:\nExpected: {:#?}\nActual: {:#?}\n{}\nErrors: {:#?}",
self.tree,
tree,
json(&tree),
&errors,
);
}
if errors != self.errors {
result = TestResult::Fail;
eprintln!(
"Errors did not match:\nExpected: {:#?}\nActual: {:#?}\n{}\nTree (for reference): {:#?}",
self.errors,
errors,
json(&errors),
&tree,
);
}
if html_output.body != self.html {
result = TestResult::Fail;
eprintln!(
"HTML does not match:\nExpected: {:?}\nActual: {:?}\n\n{}\n\nTree (for reference): {:#?}",
self.html,
html_output.body,
html_output.body,
&tree,
);
}
result
}
}
#[test]
fn ast_and_html() {
if !SKIP_TESTS.is_empty() {
println!("=========");
println!(" WARNING ");
println!("=========");
println!();
println!("The following tests are being SKIPPED:");
for test in SKIP_TESTS {
println!("- {}", test);
}
println!();
}
if !ONLY_TESTS.is_empty() {
println!("=========");
println!(" WARNING ");
println!("=========");
println!();
println!("Only the following tests are being run.");
println!("All others are being SKIPPED!");
for test in ONLY_TESTS {
println!("- {}", test);
}
println!();
}
let entries = fs::read_dir(&*TEST_DIRECTORY) .expect("Unable to read directory");
let tests_iter = entries.filter_map(|entry| {
let entry = entry.expect("Unable to read directory entry");
let ftype = entry.file_type().expect("Unable to get file type");
if !ftype.is_file() {
println!("Skipping non-file {}", file_name!(entry));
return None;
}
let path = entry.path();
let stem = path
.file_stem()
.expect("Unable to get file stem")
.to_string_lossy();
let extension = path.extension().map(|s| s.to_str()).flatten();
match extension {
Some("json") => Some(Test::load(&path, &stem)),
Some("html") => None,
_ => {
println!("Skipping non-JSON file {}", file_name!(entry));
None
}
}
});
let mut tests: Vec<Test> = tests_iter.collect();
tests.sort_by(|a, b| (a.name).cmp(&b.name));
let mut failed = 0;
let mut skipped = 0;
println!("Running {} syntax tree tests:", tests.len());
for test in &tests {
match test.run() {
TestResult::Pass => (),
TestResult::Fail => failed += 1,
TestResult::Skip => skipped += 1,
}
}
println!();
println!("Ran a total of {} tests", tests.len());
if failed > 0 {
println!("Of these, {} failed", failed);
}
if skipped > 0 {
println!("Additionally, {} tests are being skipped", skipped);
println!("Remember to re-enable all tests before committing!");
}
process::exit(failed + skipped);
}