macrotest 0.1.1

Test harness for macro expansion
Documentation
//! ####   Test harness for macro expansion.
//!
//! Similar to [trybuild], but allows you to write tests on how macros are expanded.
//!
//! <br>
//!
//! # Macro expansion tests
//!
//! A minimal `macrotest` setup looks like this:
//!
//! ```rust
//! #[test]
//! pub fn pass() {
//!     let t = macrotest::TestCases::new();
//!     t.pass("tests/expand/*.rs");
//! }
//! ```
//!
//! The test can be run with `cargo test`. It will individually extract each of
//! the source files matches the glob pattern as `main.rs` in a separate `cargo` crate in
//! temporary folder and will invoke `cargo expand` to expand macro invocations.
//!
//! Project's crate will listed under `[dependencies]` section of temporary crates and will be
//! available from the test cases.
//!
//! Expansion result is compared with the corresponding `.expanded.rs` file (same file name as
//! the test except with a different extension). If file doesn't exists, it will create a new one
//! (this is how you update your tests).
//!
//! Possible test outcomes are:
//! - **Pass**: expansion succeeded and result is the same as in `.expanded.rs` file
//! - **Fail**: expansion is different from the `.expanded.rs` file content. This will print a diff
//! - **Refresh**: `.expanded.rs` didn't exist and has been created
//!
//! *NB*: after execution of each test, a temporary folder with the crate is removed automatically.
//!
//! # Workflow
//!
//! First of all, the `cargo-expand` tool must be present. You can install it via cargo:
//!
//! ```bash
//! cargo install cargo-expand
//! ```
//!
//! A **nigthly** compiler is required for this tool to operate, so it must be installed as well.
//!
//! ## Setting up a test project
//!
//! Inside your crate that provides procedural or declarative macros, create a test case
//! under `tests` directory.
//!
//! Under the `tests` directory create an `expand` directory and populate it with
//! different expansion test cases as Rust source files.
//!
//! Then, udner the `tests` directory, create `tests.rs` file that will run the tests:
//!
//! ```rust
//! #[test]
//! pub fn pass() {
//!     let t = macrotest::TestCases::new();
//!     t.pass("tests/expand/*.rs");
//! }
//! ```
//!
//! And then you can run `cargo test` to
//!
//! 1. For the first time, generate the `.expanded.rs` files for each of the test cases under
//! the `expand` directory
//! 1. After that, test cases' expansion result will be compared with the
//! content of `.expanded.rs` files
//!
//! ## Updating `.expanded.rs`
//!
//! Just remove the `.expanded.rs` files and re-run the corresponding tests. Files will be created
//! automatically; hand-writing them is not recommended.
//!
//! [trybuild]: https://github.com/dtolnay/trybuild

#![crate_type = "lib"]

use derive_more::From;
use failure::Fail;

use std::cell::RefCell;
use std::path::{Path, PathBuf};
use std::thread;

pub mod common;
mod expand;
mod message;

#[derive(Debug, Fail, From)]
pub enum Error {
    #[fail(display = "Failed to execute `cargo expand`: {}", _0)]
    CargoExpandExecutionError(String),

    #[fail(display = "I/O error: {}", _0)]
    IoError(#[cause] std::io::Error),

    #[fail(display = "TOML serialization error: {}", _0)]
    TomlSerError(#[cause] toml::ser::Error),

    #[fail(display = "TOML deserialization error: {}", _0)]
    TomlDeError(#[cause] toml::de::Error),

    #[fail(display = "Glob error: {}", _0)]
    GlobError(#[cause] glob::GlobError),

    #[fail(display = "Glob pattern error: {}", _0)]
    GlobPatternError(#[cause] glob::PatternError),

    #[fail(display = "No CARGO_MANIFEST_DIR env var")]
    ManifestDirError,

    #[fail(display = "No CARGO_PKG_NAME env var")]
    PkgName,
}

type Result<T> = std::result::Result<T, Error>;

#[derive(Debug)]
enum ExpansionOutcome {
    Same,
    Different(Vec<u8>, Vec<u8>),
    New(Vec<u8>),
    ExpandError(Vec<u8>),
}

#[derive(Debug)]
pub struct TestCases {
    inner: RefCell<Expander>,
}

#[derive(Debug)]
struct Expander {
    tests: Vec<Test>,
}

#[derive(Debug, Copy, Clone)]
enum Expected {
    Pass,

    #[allow(dead_code)]
    CompileFail,
}

#[derive(Clone, Debug)]
struct Test {
    path: PathBuf,
    expected: Expected,
}

impl TestCases {
    pub fn new() -> Self {
        TestCases {
            inner: RefCell::new(Expander { tests: Vec::new() }),
        }
    }

    pub fn pass<P: AsRef<Path>>(&self, path: P) {
        self.inner.borrow_mut().tests.push(Test {
            path: path.as_ref().to_owned(),
            expected: Expected::Pass,
        });
    }
}

#[doc(hidden)]
impl Drop for TestCases {
    fn drop(&mut self) {
        if !thread::panicking() {
            self.inner.borrow_mut().expand();
        }
    }
}