use super::{Test, TestUniverse};
use crate::tree::{BibliographyList, SyntaxTree};
use serde::de::DeserializeOwned;
use std::collections::BTreeMap;
use std::ffi::{OsStr, OsString};
use std::fs::{self, File};
use std::io::Read;
use std::path::Path;
fn open_file(path: &Path) -> File {
match File::open(path) {
Ok(file) => file,
Err(error) => {
panic!("Unable to open file '{}': {}", path.display(), error)
}
}
}
fn read_text_file(path: &Path) -> String {
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 '{}': {}", path.display(), error);
}
process_newlines(&mut contents);
if contents.ends_with('\n') {
contents.pop();
}
contents
}
fn read_json<T: DeserializeOwned>(path: &Path) -> T {
let mut file = open_file(path);
match serde_json::from_reader(&mut file) {
Ok(object) => object,
Err(error) => {
panic!("Unable to parse JSON file '{}': {}", path.display(), error);
}
}
}
fn convert_os_string(s: OsString) -> String {
match s.into_string() {
Ok(s) => s,
Err(s) => panic!("Unable to convert OsString: {}", s.display()),
}
}
#[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");
}
}
impl TestUniverse {
#[inline]
pub fn load(test_dir: &Path) -> Self {
Self::load_inner(test_dir, false)
}
#[inline]
#[cold]
pub fn load_permissive(test_dir: &Path) -> Self {
Self::load_inner(test_dir, true)
}
fn load_inner(test_dir: &Path, permissive: bool) -> Self {
let mut tests = BTreeMap::new();
for entry in fs::read_dir(test_dir).expect("Unable to read dir") {
let entry = entry.expect("Unable to read dir entry");
let metadata = entry.metadata().expect("Unable to get dir entry metadata");
let path = entry.path();
let test_group = convert_os_string(entry.file_name());
if metadata.is_dir() {
Self::load_group(&mut tests, &test_group, &path, permissive);
} else if Self::ignore_test_file(&path) {
continue;
} else {
panic!("Unexpected file! {}", path.display());
}
}
TestUniverse { tests }
}
fn load_group(
tests: &mut BTreeMap<String, Test>,
test_group: &str,
test_dir: &Path,
permissive: bool,
) {
for entry in fs::read_dir(test_dir).expect("Unable to read dir") {
let entry = entry.expect("Unable to read dir entry");
let metadata = entry.metadata().expect("Unable to get dir entry metadata");
let path = entry.path();
let name = {
let mut test_name = convert_os_string(entry.file_name());
test_name.insert(0, '/');
test_name.insert_str(0, test_group);
test_name
};
if !metadata.is_dir() {
panic!("Found a non-directory test path: {}", path.display());
}
let test_name = name.clone();
let test = if permissive {
Test::load_permissive(test_name, &path)
} else {
Test::load(test_name, &path)
};
tests.insert(name, test);
}
}
fn ignore_test_file(path: &Path) -> bool {
const IGNORE_FILENAMES: [&str; 2] = [".gitignore", ".gitattributes"];
let filename = path.file_name();
for ignore_filename in &IGNORE_FILENAMES {
if filename == Some(OsStr::new(ignore_filename)) {
return true;
}
}
false
}
}
impl Test {
#[inline]
pub fn load(name: String, test_dir: &Path) -> Self {
Self::load_inner(name, test_dir, false)
}
#[inline]
#[cold]
pub fn load_permissive(name: String, test_dir: &Path) -> Self {
Self::load_inner(name, test_dir, true)
}
fn load_inner(name: String, test_dir: &Path, permissive: bool) -> Self {
let mut input = None;
let mut tree = None;
let mut errors = None;
let mut wikidot_output = None;
let mut html_output = None;
let mut text_output = None;
for entry in fs::read_dir(test_dir).expect("Unable to read dir") {
let entry = entry.expect("Unable to read dir entry");
let metadata = entry.metadata().expect("Unable to get dir entry metadata");
let path = entry.path();
let filename = path
.file_name()
.expect("No basename from read_dir path")
.to_str()
.expect("Encountered non-UTF-8 path");
if metadata.len() == 0 && permissive {
fn empty_syntax_tree() -> SyntaxTree<'static> {
SyntaxTree {
elements: Vec::new(),
table_of_contents: Vec::new(),
html_blocks: Vec::new(),
code_blocks: Vec::new(),
footnotes: Vec::new(),
needs_footnote_block: true,
bibliographies: BibliographyList::new(),
wikitext_len: 0,
}
}
match filename {
"input.ftml" => panic!(
"Empty wikitext inputs are not allowed!\nThe whole point of an AST test is to test it against some input, so please fill this out before attempting to update test outputs!"
),
"tree.json" => tree = Some(empty_syntax_tree()),
"errors.json" => errors = Some(Vec::new()),
"wikidot.html" => wikidot_output = Some(String::new()),
"output.html" => html_output = Some(String::new()),
"output.txt" => text_output = Some(String::new()),
_ => panic!("Unexpected empty file: {}", entry.path().display()),
}
continue;
}
match filename {
"input.ftml" => input = Some(read_text_file(&path)),
"tree.json" => tree = Some(read_json(&path)),
"errors.json" => errors = Some(read_json(&path)),
"wikidot.html" => wikidot_output = Some(read_text_file(&path)),
"output.html" => html_output = Some(read_text_file(&path)),
"output.txt" => text_output = Some(read_text_file(&path)),
_ => panic!("Unexpected file in AST test: {}", entry.path().display()),
}
}
let input = match input {
Some(input) => input,
None => panic!("No wikitext file (input.ftml) found for test '{name}'!"),
};
if errors.is_some() {
assert!(
tree.is_some(),
"No syntax tree file (tree.json) found for test '{name}' with errors.json",
);
}
let test = Test {
name,
input,
tree,
errors,
wikidot_output,
html_output,
text_output,
};
assert!(
test.has_something_to_do(),
"Test '{}' has nothing to do! Add at least one expected output file",
test.name,
);
test
}
#[inline]
pub fn has_something_to_do(&self) -> bool {
self.tree.is_some()
|| self.errors.is_some()
|| self.wikidot_output.is_some()
|| self.html_output.is_some()
|| self.text_output.is_some()
}
}