goldentests 1.4.1

A golden file testing library where tests can be configured within the same test file
Documentation
use std::path::PathBuf;

use serde::Deserialize;

const DEFAULT_ARGS_PREFIX: fn() -> String = || "args:".to_string();
const DEFAULT_ARGS_AFTER_PREFIX: fn() -> String = || "args after:".to_string();
const DEFAULT_STDOUT_PREFIX: fn() -> String = || "expected stdout:".to_string();
const DEFAULT_STDERR_PREFIX: fn() -> String = || "expected stderr:".to_string();
const DEFAULT_EXIT_STATUS_PREFIX: fn() -> String = || "expected exit status:".to_string();

#[derive(Deserialize)]
pub struct TestConfig {
    /// The binary path to your program, typically "target/debug/myprogram"
    pub binary_path: PathBuf,

    /// The path to the directory containing your tests, or a single test file.
    ///
    /// If this is a directory, it will be searched recursively for all files.
    pub test_path: PathBuf,

    /// The sequence of characters starting at the beginning of a line that
    /// all test options should be prefixed with. This is typically a comment
    /// in your language. For example, if we had a C like language we could
    /// have "// " as the test_line_prefix to allow "expected stdout:" and friends
    /// to be read inside comments at the start of a line.
    pub test_line_prefix: String,

    /// The "args:" keyword used while parsing tests. Anything after
    /// `test_line_prefix + test_args_prefix` is read in as shell arguments to
    /// the program, passed before the test file path.
    #[serde(default = "DEFAULT_ARGS_PREFIX")]
    pub test_args_prefix: String,

    /// The "args after:" keyword used while parsing tests. Anything after
    /// `test_line_prefix + test_args_after_prefix` is read in as shell
    /// arguments to the program, passed after the test file path.
    #[serde(default = "DEFAULT_ARGS_AFTER_PREFIX")]
    pub test_args_after_prefix: String,

    /// The "expected stdout:" keyword used while parsing tests. Any line starting
    /// with `test_line_prefix` after a line starting with `test_line_prefix + test_stdout_prefix`
    /// is appended to the expected stdout output. This continues until the first
    /// line that does not start with `test_line_prefix`
    ///
    /// Example with `test_line_prefix = "// "` and `test_stdout_prefix = "expected stdout:"`
    /// ```rust
    /// // expected stdout:
    /// // first line of stdout
    /// // second line of stdout
    ///
    /// // Normal comment, expected stdout is done being read.
    /// ```
    #[serde(default = "DEFAULT_STDOUT_PREFIX")]
    pub test_stdout_prefix: String,

    /// The "expected stderr:" keyword used while parsing tests. Any line starting
    /// with `test_line_prefix` after a line starting with `test_line_prefix + test_stderr_prefix`
    /// is appended to the expected stderr output. This continues until the first
    /// line that does not start with `test_line_prefix`
    ///
    /// Example with `test_line_prefix = "-- "` and `test_stderr_prefix = "expected stderr:"`
    /// ```haskell
    /// -- expected stderr:
    /// -- first line of stderr
    /// -- second line of stderr
    ///
    /// -- Normal comment, expected stderr is done being read.
    /// ```
    #[serde(default = "DEFAULT_STDERR_PREFIX")]
    pub test_stderr_prefix: String,

    /// The "expected exit status:" keyword used while parsing tests. This will expect an
    /// integer after this keyword representing the expected exit status of the given test.
    ///
    /// Example with `test_line_prefix = "; "` and `test_exit_status_prefix = "expected exit status:"`
    /// ```rust
    /// // expected exit status: 0
    /// ```
    #[serde(default = "DEFAULT_EXIT_STATUS_PREFIX")]
    pub test_exit_status_prefix: String,

    /// Flag the current output as correct and regenerate the test files. This assumes the order of
    /// the `goldenfiles` sections can be moved around.
    #[serde(skip)]
    pub overwrite_tests: bool,

    /// Arguments to always include in the command-line args for testing the program.
    /// For example, if this is `foo` and the test specifies `args: bar baz` then the
    /// binary will be invoked via `<binary> foo bar baz <filename> <args-after>`
    #[serde(default)]
    pub base_args: String,

    /// Arguments to always include in the command-line args for testing the program.
    /// For example, if this is `foo` and the test specifies `args after: bar baz` then the
    /// binary will be invoked via `<binary> <args> <filename> foo bar baz`
    #[serde(default)]
    pub base_args_after: String,
}

impl TestConfig {
    /// Creates a new TestConfig for the given binary path, test path, and prefix.
    ///
    /// If we were testing a C++-like language that uses `//` as its comment syntax, we
    /// may want our test keywords embedded in comments. Additionally, lets say our
    /// project is called "my-compiler" and our test path is "examples/goldentests".
    /// In that case we can construct a `TestConfig` like so:
    ///
    /// ```rust
    /// use goldentests::TestConfig;
    /// let config = TestConfig::new("target/debug/my-compiler", "examples/goldentests", "// ");
    /// ```
    ///
    /// This will give us the default keywords when parsing our test files which allows
    /// us to write tests such as the following:
    ///
    /// ```cpp
    /// std::cout << "Hello, World!\n";
    /// std::cerr << "Goodbye, World!\n";
    ///
    /// // These are args to your program, so this:
    /// // args: --run
    /// // Gets translated to:  target/debug/my-compiler --run testfile
    ///
    /// // The expected exit status is optional, by default it is not checked.
    /// // expected exit status: 0
    ///
    /// // The expected stdout output however is mandatory. If it is omitted, it
    /// // is assumed that stdout should be empty after invoking the program.
    /// // expected stdout:
    /// // Hello, World!
    ///
    /// // The expected stderr output is also mandatory. If it is omitted it is
    /// // likewise assumed stderr should be empty.
    /// // expected stderr:
    /// // Goodbye, World!
    /// ```
    ///
    /// Note that we can still embed normal comments in the program even though our test
    /// line prefix was "// "! Any test line that doesn't start with a keyword like "args:"
    /// or "expected stdout:" is ignored unless it is following an "expected stdout:" or
    /// "expected stderr:", in which case it is appended to the expected output.
    ///
    /// If you want to change these default keywords you can also create a TestConfig
    /// via `TestConfig::with_custom_keywords` which will allow you to specify each.
    #[allow(unused)]
    pub fn new<Binary, Tests>(binary_path: Binary, test_path: Tests, test_line_prefix: &str) -> TestConfig
    where
        Binary: Into<PathBuf>,
        Tests: Into<PathBuf>,
    {
        TestConfig::with_custom_keywords(
            binary_path,
            test_path,
            test_line_prefix,
            "args:",
            "args after:",
            "expected stdout:",
            "expected stderr:",
            "expected exit status:",
            false,
        )
    }

    /// Creates a TestConfig from reading a `goldentests.toml` configuration file.
    ///
    /// This will use the provided path to the configuration file, if it is provided.
    /// Otherwise, this will attempt to search the current directory and parent directories
    /// for a config file automatically.
    ///
    /// This function panics if the configuration file was not found or could not be read.
    #[allow(unused)]
    pub fn new_from_config_file(path: Option<PathBuf>) -> TestConfig {
        super::config_file::read_config_file(path.clone()).unwrap_or_else(|| {
            if let Some(path) = path {
                panic!("Could not read from `{:?}`", path)
            } else {
                panic!("Could not find a `goldentests.toml` in this directory or in parent directories")
            }
        })
    }

    /// This function is provided in case you want to change the default keywords used when
    /// searching through the test file. This will let you change "expected stdout:"
    /// or any other keyword to "output I want ->" or any other arbitrary string so long as it
    /// does not contain "\n".
    ///
    /// If you don't want to change any of the defaults, you can use `TestConfig::new` to construct
    /// a TestConfig with the default keywords (which are listed in its documentation).
    pub fn with_custom_keywords<Binary, Tests>(
        binary_path: Binary,
        test_path: Tests,
        test_line_prefix: &str,
        test_args_prefix: &str,
        test_args_after_prefix: &str,
        test_stdout_prefix: &str,
        test_stderr_prefix: &str,
        test_exit_status_prefix: &str,
        overwrite_tests: bool,
    ) -> TestConfig
    where
        Binary: Into<PathBuf>,
        Tests: Into<PathBuf>,
    {
        let binary_path = binary_path.into();
        let test_path = test_path.into();

        let test_line_prefix = test_line_prefix.to_string();
        let prefixed = |s| format!("{}{}", test_line_prefix, s);

        TestConfig {
            binary_path,
            test_path,
            test_args_prefix: prefixed(test_args_prefix),
            test_args_after_prefix: prefixed(test_args_after_prefix),
            test_stdout_prefix: prefixed(test_stdout_prefix),
            test_stderr_prefix: prefixed(test_stderr_prefix),
            test_exit_status_prefix: prefixed(test_exit_status_prefix),
            test_line_prefix,
            overwrite_tests,
            base_args: String::new(),
            base_args_after: String::new(),
        }
    }
}