wdl-format 0.18.0

Formatting of WDL (Workflow Description Language) documents
Documentation
//! The format file tests.
//!
//! This test looks for directories in `tests/format`.
//!
//! Each directory is expected to contain:
//!
//! * `source.wdl` - the test input source to parse.
//! * `source.formatted.wdl` - the expected formatted output.
//!
//! The `source.formatted.wdl` file may be automatically generated or updated by
//! setting the `BLESS` environment variable when running this test.

use std::env;
use std::ffi::OsStr;
use std::fs;
use std::path::Path;

use anyhow::Context as _;
use anyhow::bail;
use codespan_reporting::files::SimpleFile;
use codespan_reporting::term;
use codespan_reporting::term::Config;
use codespan_reporting::term::termcolor::Buffer;
use libtest_mimic::Trial;
use pretty_assertions::StrComparison;
use wdl_ast::Diagnostic;
use wdl_ast::Document;
use wdl_ast::Node;
use wdl_format::Config as FormatConfig;
use wdl_format::Formatter;
use wdl_format::NewlineStyle;
use wdl_format::element::FormatElement;
use wdl_format::element::node::AstNodeFormatExt;

/// Normalizes a result.
fn normalize(s: &str) -> String {
    // Just normalize line endings
    s.replace("\r\n", "\n")
}

/// Find all the tests in the `tests/format` directory.
fn find_tests() -> Vec<Trial> {
    Path::new("tests")
        .join("format")
        .read_dir()
        .unwrap()
        .filter_map(|entry| {
            let entry = entry.expect("failed to read directory");
            let path = entry.path();
            if !path.is_dir() {
                return None;
            }

            let test_name = path
                .file_stem()
                .map(OsStr::to_string_lossy)
                .unwrap()
                .into_owned();
            Some(Trial::test(test_name, move || Ok(run_test(&path)?)))
        })
        .collect()
}

/// Format a list of diagnostics.
fn format_diagnostics(diagnostics: &[Diagnostic], path: &Path, source: &str) -> String {
    let file = SimpleFile::new(path.as_os_str().to_str().unwrap(), source);
    let mut buffer = Buffer::no_color();
    for diagnostic in diagnostics {
        term::emit_to_write_style(
            &mut buffer,
            &Config::default(),
            &file,
            &diagnostic.to_codespan(()),
        )
        .expect("should emit");
    }

    String::from_utf8(buffer.into_inner()).expect("should be UTF-8")
}

/// Compare the result of a test to the expected result.
fn compare_result(
    path: &Path,
    result: &str,
    allow_blessing: bool,
    preserve_line_endings: bool,
) -> Result<(), anyhow::Error> {
    let result = if preserve_line_endings {
        result.to_string()
    } else {
        normalize(result)
    };

    if allow_blessing && env::var_os("BLESS").is_some() {
        fs::write(path, &result).context("writing result file")?;
        return Ok(());
    }

    let expected = if preserve_line_endings {
        fs::read_to_string(path).context("reading result file")?
    } else {
        fs::read_to_string(path)
            .context("reading result file")?
            .replace("\r\n", "\n")
    };

    if expected != result {
        bail!(
            "result from `{path}` is not as expected:\n{diff}",
            path = path.display(),
            diff = StrComparison::new(&expected, &result),
        );
    }

    Ok(())
}

/// Parses source string into a document FormatElement
fn prepare_document(source: &str, path: &Path) -> Result<FormatElement, anyhow::Error> {
    let (document, diagnostics) = Document::parse(source, None);

    if !diagnostics.is_empty() {
        bail!(
            "failed to parse `{path}` {e}",
            path = path.display(),
            e = format_diagnostics(&diagnostics, path, source)
        );
    };

    Ok(Node::Ast(document.ast().into_v1().unwrap()).into_format_element())
}

/// Parses and formats source string
fn format(config: FormatConfig, source: &str, path: &Path) -> Result<String, anyhow::Error> {
    let document = prepare_document(source, path)?;
    Formatter::new(config)
        .format(&document)
        .context("formatting document")
}

/// Runs a lint test with the specified [`FormatConfig`].
fn run_test_inner(
    config: FormatConfig,
    source: &str,
    original_doc: &Path,
    formatted_doc: &Path,
    preserve_line_endings: bool,
) -> anyhow::Result<()> {
    let formatted = format(config, source, original_doc)?;
    compare_result(formatted_doc, &formatted, true, preserve_line_endings)?;

    // test idempotency by formatting the formatted document
    let twice_formatted = format(config, &formatted, formatted_doc)?;
    compare_result(
        formatted_doc,
        &twice_formatted,
        false,
        preserve_line_endings,
    )
    .context("testing idempotency")?;

    Ok(())
}

/// Run a test.
fn run_test(test: &Path) -> Result<(), anyhow::Error> {
    let path = test.join("source.wdl");
    let formatted_path = path.with_extension("formatted.wdl");
    let source = std::fs::read_to_string(&path).context("reading source file")?;

    let config_path = test.join("config.toml");
    if config_path.exists() {
        let content = std::fs::read_to_string(&config_path).context(format!(
            "failed to read config at '{}'",
            config_path.display()
        ))?;
        let config: FormatConfig = toml::from_str(&content).context(format!(
            "failed to parse config at '{}'",
            config_path.display()
        ))?;

        let preserve_line_endings = config.newline_style != NewlineStyle::Auto;

        run_test_inner(
            config,
            &source,
            &path,
            &formatted_path,
            preserve_line_endings,
        )?;
        run_test_inner(
            FormatConfig::default(),
            &source,
            &path,
            &path.with_extension("default.formatted.wdl"),
            false,
        )?;
    } else {
        run_test_inner(
            FormatConfig::default(),
            &source,
            &path,
            &formatted_path,
            false,
        )?;
    }

    Ok(())
}

/// Run all the tests.
fn main() {
    let args = libtest_mimic::Arguments::from_args();
    let tests = find_tests();
    libtest_mimic::run(&args, tests).exit();
}