stories 0.1.0

Framework for running test stories.
Documentation
use rune::{
    termcolor::{ColorChoice, StandardStream},
    Diagnostics, Vm,
};
use std::{io::Read, path::Path, sync::Arc};

use crate::{
    prelude::*,
    report_generators::{Generator, StoryReportGenerator},
};

/// This function execute a single story
pub(crate) async fn execute_story(
    story: &story::Story,
    stories: &story::Stories,
    base_path: &Path,
    runtime: Arc<tokio::runtime::Runtime>,
    report_generator: &report_generators::GeneratorDispatcher,
) -> anyhow::Result<bool> {
    // Create a rune context and install the stories standard modules
    let mut rune_context = rune_modules::default_context()?;
    modules::install_standard_modules(&mut rune_context)?;

    // Get the filename
    let filename = story
        .filename
        .clone()
        .or_else(|| stories.filename.clone())
        .ok_or_else(|| {
            anyhow::Error::msg(format!(
                "No source filename was provided for test '{}'.",
                story.name
            ))
        })?;
    let filename_path = Path::new(&filename);
    let filename_resolved = if filename_path.is_absolute() {
        filename.clone().into()
    } else {
        base_path.join(&filename)
    };

    // Read the data
    let mut file = std::fs::File::open(&filename_resolved)?;
    let mut data = String::new();
    file.read_to_string(&mut data)?;

    // Load the sources
    let mut sources = rune::Sources::new();
    sources.insert(rune::Source::new(&filename, data)?)?;

    let mut diagnostics = Diagnostics::new();

    let result = rune::prepare(&mut sources)
        .with_context(&rune_context)
        .with_diagnostics(&mut diagnostics)
        .build();

    if !diagnostics.is_empty() {
        let mut writer = StandardStream::stderr(ColorChoice::Always);
        diagnostics.emit(&mut writer, &sources)?;
    }

    let unit = result?;

    let mut vm = Vm::new(Arc::new(rune_context.runtime()?), Arc::new(unit));
    let reporter = Reporter::new();
    let stories_context = Context::new(runtime, reporter);

    // Execute the story
    let start = std::time::Instant::now();
    let r = vm
        .execute([story.function.as_str()], (stories_context.clone(),))?
        .complete()
        .into_result();
    let duration = std::time::Instant::now() - start;

    if let Err(error) = r {
        report_generator.report_failure(
            story,
            duration,
            format!(
                "function {} in file {} is invalid with error '{}'.",
                story.function, filename, error
            ),
        );

        for error in error.chain().into_iter().skip(1) {
            log::error!("Caused by: {}", error);
        }
        return Ok(false);
    }

    stories_context
        .finalise_activities(std::time::Duration::from_secs(2))
        .await;

    let mut result = false;

    let mut story_generator = None;

    // Analyse the result, if the story is supposed to fail...
    if story.expect_failure {
        // it should not be successful
        if stories_context.is_successful() {
            story_generator = Some(report_generator.report_failure(
                story,
                duration,
                "story was successful, however, failure was expected.".into(),
            ));
        } else {
            // Check the messages
            let messages = stories_context.reporter_ref().clone_messages();
            if story.messages.len() == messages.len() {
                let mut missing_match = false;
                // Check that each expected messages was found
                for expected_message in &story.messages {
                    let mut found = false;
                    let expected_regex = regex::Regex::new(&expected_message.content)?;
                    for actual_message in messages.iter() {
                        if expected_message.line == actual_message.line
                            && expected_message.filename == actual_message.filename
                            && expected_regex.is_match(&actual_message.content)
                        {
                            found = true;
                            break;
                        }
                    }
                    if !found {
                        missing_match = true;
                        story_generator = Some(report_generator.report_failure(
                            story,
                            duration,
                            format!(
                                "{}: missing message '{}:{}: {}'!",
                                story.name,
                                expected_message.filename,
                                expected_message.line,
                                expected_message.content
                            ),
                        ));
                    }
                }
                // If missing a match, show the emitted messages.
                if !missing_match {
                    result = true;
                }
            } else {
                // The size of emitted and expected messages differ.
                story_generator = Some(report_generator.report_failure(
                    story,
                    duration,
                    format!(
                        "{}: failed with {} messages instead of {}.",
                        story.name,
                        messages.len(),
                        story.messages.len()
                    ),
                ));
            }
        }
    } else {
        if stories_context.is_successful() {
            result = true;
        } else {
            story_generator = Some(report_generator.report_failure(
                story,
                duration,
                format!(
                    "failed, with '{}' messages:",
                    stories_context.reporter_ref().messages_count()
                ),
            ));
            result = false;
        }
    }
    if result {
        story_generator = Some(report_generator.report_success(story, duration));
    }
    let story_generator = story_generator
        .ok_or_else(|| anyhow::Error::msg("somehow a story generator was not constructed."))?;
    stories_context
        .reporter_ref()
        .for_each_message(|message| story_generator.report_message(&message));
    Ok(result)
}