use crate::{run, util, Config, Variables};
use std::{fmt, path::PathBuf};
use std::fmt::Write;
#[derive(Clone,Debug,PartialEq,Eq)]
pub struct Invocation
{
pub original_command: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TestFile
{
pub path: TestFilePath,
pub commands: Vec<Command>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TestFilePath {
pub absolute: PathBuf,
pub relative: PathBuf,
}
#[derive(Clone,Debug,PartialEq,Eq)]
pub struct Command
{
pub line_number: u32,
pub kind: CommandKind,
}
#[derive(Clone,Debug)]
pub enum CommandKind
{
Run(Invocation),
Check(TextPattern),
CheckNext(TextPattern),
XFail,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct TextPattern {
pub components: Vec<PatternComponent>,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum PatternComponent {
Text(String),
Variable(String),
Regex(String),
NamedRegex { name: String, regex: String },
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[must_use]
pub enum TestResultKind
{
Pass,
UnexpectedPass,
Error { message: String },
Fail {
reason: TestFailReason,
hint: Option<String>,
},
ExpectedFailure {
actual_reason: TestFailReason,
},
EmptyTest,
Skip,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum TestFailReason {
UnsuccessfulExecution {
program_command_line: String,
exit_status: i32,
},
CheckFailed(CheckFailureInfo),
}
impl TestFailReason {
pub fn human_summary(&self) -> &'static str {
match *self {
TestFailReason::UnsuccessfulExecution { .. } => {
"unsuccessful program execution whilst running test"
},
TestFailReason::CheckFailed(..) => {
"test checked for text that did not exist in the output"
},
}
}
pub fn human_detail_message(&self, config: &Config) -> String {
match *self {
TestFailReason::UnsuccessfulExecution { ref program_command_line, exit_status } => {
format!("command '{}' exited with code '{}'", program_command_line, exit_status)
},
TestFailReason::CheckFailed(ref check_failure_info) => {
let mut buf = String::new();
writeln!(&mut buf, "expected text '{}' but that was not found", check_failure_info.expected_pattern).unwrap();
writeln!(&mut buf).unwrap();
writeln!(&mut buf, "{}", format_test_output("successfully checked output",
check_failure_info.successfully_checked_text(), 1, util::TruncateDirection::Top,
config)).unwrap();
writeln!(&mut buf).unwrap();
writeln!(&mut buf, "{}", format_test_output("remaining unchecked output",
check_failure_info.remaining_text(),
check_failure_info.successfully_checked_upto_line_number(), util::TruncateDirection::Bottom,
config)).unwrap();
buf
},
}
}
}
pub(crate) fn format_test_output(
output_label: &str,
unformatted_output: &str,
output_base_line_number: usize,
truncate_direction: util::TruncateDirection,
config: &Config) -> String {
let mut formatted_output = util::decorate_with_line_numbers(unformatted_output, output_base_line_number);
if let Some(max_line_count) = config.truncate_output_context_to_number_of_lines {
formatted_output = util::truncate_to_max_lines(&formatted_output, max_line_count, truncate_direction);
}
let formatted_output = util::indent(&formatted_output, 1);
format!("<{}>:\n\n{}\n</{}>", output_label, formatted_output, output_label)
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct CheckFailureInfo {
pub complete_output_text: String,
pub successfully_checked_until_byte_index: usize,
pub expected_pattern: TextPattern,
}
#[derive(Debug)]
pub struct TestResult
{
pub path: TestFilePath,
pub overall_result: TestResultKind,
pub individual_run_results: Vec<(TestResultKind, Invocation, run::CommandLine, ProgramOutput)>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ProgramOutput {
pub stdout: String,
pub stderr: String,
}
#[derive(Debug)]
pub struct Results
{
pub test_results: Vec<TestResult>,
}
impl PartialEq for CommandKind {
fn eq(&self, other: &CommandKind) -> bool {
match *self {
CommandKind::Run(ref a) => if let CommandKind::Run(ref b) = *other { a == b } else { false },
CommandKind::Check(ref a) => if let CommandKind::Check(ref b) = *other { a.to_string() == b.to_string() } else { false },
CommandKind::CheckNext(ref a) => if let CommandKind::CheckNext(ref b) = *other { a.to_string() == b.to_string() } else { false },
CommandKind::XFail => *other == CommandKind::XFail,
}
}
}
impl Eq for CommandKind { }
impl fmt::Display for TextPattern {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
for component in self.components.iter() {
match *component {
PatternComponent::Text(ref text) => write!(fmt, "{}", text)?,
PatternComponent::Variable(ref name) => write!(fmt, "$${}", name)?,
PatternComponent::Regex(ref regex) => write!(fmt, "[[{}]]", regex)?,
PatternComponent::NamedRegex { ref name, ref regex } => write!(fmt, "[[{}:{}]]", name, regex)?,
}
}
Ok(())
}
}
impl Command
{
pub fn new(kind: CommandKind, line_number: u32) -> Self {
Command { kind, line_number }
}
}
impl TestResultKind {
pub fn is_erroneous(&self) -> bool {
use self::TestResultKind::*;
match *self {
UnexpectedPass | Error { .. } | Fail { .. } => true,
Pass | Skip | ExpectedFailure { .. } | EmptyTest => false,
}
}
pub fn unwrap(&self) {
if self.is_erroneous() {
panic!("error whilst running test: {:?}", self);
}
}
pub fn human_label_pluralized(&self) -> &'static str {
use self::TestResultKind::*;
match *self {
Pass => "Passes",
UnexpectedPass => "Unexpected passes",
Error { .. } => "Errors",
Fail { .. } => "Test failures",
ExpectedFailure { .. } => "Expected failures",
EmptyTest { .. } => "Empty tests",
Skip => "Skipped tests",
}
}
}
impl CheckFailureInfo {
pub fn successfully_checked_text(&self) -> &str {
let byte_subslice = &self.complete_output_text.as_bytes()[0..self.successfully_checked_until_byte_index];
convert_bytes_to_str(byte_subslice)
}
pub fn remaining_text(&self) -> &str {
let byte_subslice = &self.complete_output_text.as_bytes()[self.successfully_checked_until_byte_index..];
convert_bytes_to_str(byte_subslice)
}
pub fn successfully_checked_upto_line_number(&self) -> usize {
self.successfully_checked_text().lines().count() + 1
}
}
impl TestFile
{
pub fn variables(&self) -> Variables {
let mut v = Variables::new();
v.insert("file".to_owned(), self.path.absolute.to_str().unwrap().to_owned());
v
}
pub fn run_command_invocations(&self) -> impl Iterator<Item=&Invocation> {
self.commands.iter().filter_map(|c| match c.kind {
CommandKind::Run(ref invocation) => Some(invocation),
_ => None,
})
}
pub fn is_expected_failure(&self) -> bool {
self.commands.iter().any(|c| if let CommandKind::XFail = c.kind { true } else { false })
}
}
impl From<PatternComponent> for TextPattern {
fn from(component: PatternComponent) -> Self {
TextPattern { components: vec![component] }
}
}
impl std::fmt::Debug for CheckFailureInfo {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
#[derive(Debug)]
struct CheckFailureInfo<'a> {
expected_pattern: &'a TextPattern,
successfully_checked_text: PrintStrTruncate<'a>,
remaining_text: PrintStrTruncate<'a>,
}
const TRUNCATE_MIN: usize = 70;
const TRUNCATE_MARKER: &'static str = "...";
struct PrintStrTruncate<'a>(&'a str);
impl<'a> std::fmt::Debug for PrintStrTruncate<'a> {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
if self.0.len() <= TRUNCATE_MIN {
std::fmt::Debug::fmt(self.0, fmt)
} else {
let substr = &self.0[0..TRUNCATE_MIN];
substr.fmt(fmt)?;
std::fmt::Display::fmt(TRUNCATE_MARKER, fmt)
}
}
}
CheckFailureInfo {
expected_pattern: &self.expected_pattern,
remaining_text: PrintStrTruncate(self.remaining_text()),
successfully_checked_text: PrintStrTruncate(self.successfully_checked_text()),
}.fmt(fmt)
}
}
fn convert_bytes_to_str(bytes: &[u8]) -> &str {
std::str::from_utf8(bytes).expect("invalid UTF-8 in output stream")
}
impl ProgramOutput {
pub fn empty() -> Self {
ProgramOutput { stdout: String::new(), stderr: String::new() }
}
}