cpp-amalgamate 1.0.1

cpp-amalgamate recursively combines C++ source files and the headers they include into a single output file.
use crate::util;

use std::path::PathBuf;

use anyhow::Result;
use assert_fs::prelude::*;
use indoc::{formatdoc, indoc};
use predicates::prelude::*;

#[test]
fn cyclic_includes() -> Result<()> {
    for handling in [None, Some("error"), Some("warn"), Some("ignore")] {
        let builder = util::builder()
            .source_file("#include <a.hpp>")?
            .search_dir(
                "-d",
                [("a.hpp", "#include <b.hpp>"), ("b.hpp", "#include <a.hpp>")],
            )?;
        let mut command = builder.command();
        if let Some(handling) = handling {
            command.args(["--cyclic-include", handling]);
        }

        let mut assert = command.assert();
        let handling = handling.unwrap_or("error");
        if handling == "error" {
            assert = assert.failure();
        } else {
            assert = assert.success().stdout("#include <a.hpp>");
        }
        if handling != "ignore" {
            assert.stderr(predicate::str::is_empty().not());
        }
    }

    Ok(())
}

#[test]
fn cyclic_include_back_to_source_file() -> Result<()> {
    let mut a_path = PathBuf::new();
    util::builder()
        .search_dir_setup("-d", |dir| {
            dir.child("a.hpp").write_str("#include <a.hpp>")?;
            dir.child("b.hpp").write_str("#include <b.hpp>")?;
            a_path = dir.child("a.hpp").to_path_buf();
            Ok(())
        })?
        .command()
        .arg(a_path)
        .assert()
        .failure();
    Ok(())
}

#[test]
fn already_included_source_file() -> Result<()> {
    let mut include_path = PathBuf::new();
    util::builder()
        .source_file("#include <a.hpp>")?
        .search_dir_setup("-d", |dir| {
            dir.child("a.hpp").write_str("arst")?;
            include_path = dir.child("a.hpp").to_path_buf();
            Ok(())
        })?
        .command()
        .arg(include_path)
        .assert()
        .success()
        .stdout("arst");
    Ok(())
}

#[test]
fn include_headers_at_most_once() -> Result<()> {
    util::builder()
        .source_file(indoc! {"
            #include <a.hpp>
            #include <b.hpp>
        "})?
        .search_dir("-d", [("a.hpp", "#include <b.hpp>"), ("b.hpp", "arst\n")])?
        .command()
        .assert()
        .success()
        .stdout("arst\n");
    Ok(())
}

#[test]
fn file_identity_considers_symlinks() -> Result<()> {
    util::builder()
        .source_file(indoc! {"
            #include <a.hpp>
            #include <b.hpp>
        "})?
        .search_dir_setup("-d", |dir| {
            dir.child("a.hpp").write_str("arst\n")?;
            dir.child("b.hpp").symlink_to_file(dir.child("a.hpp"))?;
            Ok(())
        })?
        .command()
        .assert()
        .success()
        .stdout("arst\n");
    Ok(())
}

#[test]
fn weird_include_statements() -> Result<()> {
    util::builder()
        .source_file("# \t include \t <a.hpp> \t ")?
        .search_dir("-d", [("a.hpp", "arst")])?
        .command()
        .assert()
        .success()
        .stdout("arst");
    Ok(())
}

#[test]
fn line_directives() -> Result<()> {
    let builder = util::builder()
        .source_file(indoc! {"
            arst
            #include <a.hpp>

            arst
            #include <b.hpp>
            arst
        "})?
        .search_dir("-d", [("a.hpp", "#include <b.hpp>\n"), ("b.hpp", "qwfp\n")])?;

    let src_file = builder.source_files[0].to_path_buf().canonicalize()?;
    let dir = &builder.search_dirs[0].1;
    let b_hpp = dir.child("b.hpp").to_path_buf().canonicalize()?;

    builder
        .command()
        .arg("--line-directives")
        .assert()
        .success()
        .stdout(formatdoc! {r#"
            #line 1 "{src_file}"
            arst
            #line 1 "{b_hpp}"
            qwfp
            #line 3 "{src_file}"

            arst
            #line 6 "{src_file}"
            arst
            "#,
            src_file=src_file.display(),
            b_hpp=b_hpp.display()
        });
    Ok(())
}

#[test]
fn pragma_once_removal() -> Result<()> {
    util::builder()
        .source_file(indoc! {"
            #include <a.hpp>
            #include <b.hpp>
        "})?
        .search_dir(
            "-d",
            [
                ("a.hpp", "#pragma once\n"),
                ("b.hpp", "# \tpragma\t  once  \t\n"),
            ],
        )?
        .command()
        .assert()
        .success()
        .stdout("");
    Ok(())
}