git-checks 4.2.1

Checks to run against a topic in git to enforce coding standards.
Documentation
// Copyright Kitware, Inc.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

use std::path::Path;

use derive_builder::Builder;
use git_checks_core::impl_prelude::*;
use itertools::Itertools;
use rayon::prelude::*;

/// The style of changelog management in use.
#[derive(Debug, Clone)]
pub enum ChangelogStyle {
    /// A directory stores a file per changelog entry.
    Directory {
        /// The path to the directory containing changelog entry files.
        path: String,
        /// The extension used for changelog files.
        extension: Option<String>,
    },
    /// A file contains the changelog entries for a project.
    File {
        /// The path to the changelog.
        path: String,
    },
    /// A set of files containing the changelog entries for a project.
    Files {
        /// The paths to the changelog files.
        paths: Vec<String>,
    },
}

impl ChangelogStyle {
    /// A changelog stored in a file at a given path.
    pub fn file<P>(path: P) -> Self
    where
        P: Into<String>,
    {
        ChangelogStyle::File {
            path: path.into(),
        }
    }

    /// A directory containing many files, each with a changelog entry.
    ///
    /// Entries may also be required to have a given extension.
    pub fn directory<P>(path: P, ext: Option<String>) -> Self
    where
        P: Into<String>,
    {
        ChangelogStyle::Directory {
            path: path.into(),
            extension: ext,
        }
    }

    /// A changelog stored in files at the given paths.
    pub fn files<P, I>(paths: I) -> Self
    where
        I: IntoIterator<Item = P>,
        P: Into<String>,
    {
        ChangelogStyle::Files {
            paths: paths.into_iter().map(Into::into).collect(),
        }
    }

    /// A description of the changelog.
    fn describe(&self) -> String {
        match *self {
            ChangelogStyle::Directory {
                ref path,
                ref extension,
            } => {
                if let Some(ext) = extension.as_ref() {
                    format!("a file ending with `.{}` in `{}`", ext, path)
                } else {
                    format!("a file in `{}`", path)
                }
            },
            ChangelogStyle::File {
                ref path,
            } => format!("the `{}` file", path),
            ChangelogStyle::Files {
                ref paths,
            } => format!("one of the `{}` files", paths.iter().format("`, `")),
        }
    }

    /// Whether the changelog style cares about the given path.
    fn applies(&self, diff_path: &Path) -> bool {
        match *self {
            ChangelogStyle::Directory {
                ref path,
                ref extension,
            } => {
                let ext_ok = extension.as_ref().map_or(true, |ext| {
                    diff_path
                        .extension()
                        .map_or(false, |diff_ext| diff_ext == (ext.as_ref() as &Path))
                });

                ext_ok && diff_path.starts_with(path)
            },
            ChangelogStyle::File {
                ref path,
            } => diff_path == (path.as_ref() as &Path),
            ChangelogStyle::Files {
                ref paths,
            } => {
                paths
                    .iter()
                    .any(|path| diff_path == (path.as_ref() as &Path))
            },
        }
    }

    /// Whether the path with the given status change is OK.
    #[allow(clippy::match_like_matches_macro)]
    fn is_ok(&self, status: StatusChange) -> bool {
        match *self {
            ChangelogStyle::Directory {
                ..
            } => {
                match status {
                    // Adding a new file is OK.
                    StatusChange::Added
                    // Modifying an existing file is OK.
                    | StatusChange::Modified(_)
                    // Deleting a file is OK (e.g., reverts).
                    | StatusChange::Deleted => true,
                    _ => false,
                }
            },
            ChangelogStyle::File {
                ..
            }
            | ChangelogStyle::Files {
                ..
            } => {
                match status {
                    // Adding the file is OK (initialization).
                    StatusChange::Added
                    // Modifying the file is OK.
                    | StatusChange::Modified(_) => true,
                    // Removing the file is not OK.
                    _ => false,
                }
            },
        }
    }
}

/// Check for changelog modifications.
///
/// This checks to make sure that a changelog entry has been added (or modified) in every commit or
/// topic.
#[derive(Builder, Debug, Clone)]
#[builder(field(private))]
pub struct Changelog {
    /// The changelog management style in use.
    ///
    /// Configuration: Required
    style: ChangelogStyle,
    /// Whether entries are required or not.
    ///
    /// Configuration: Optional
    /// Default: `false`
    #[builder(default = "false")]
    required: bool,
}

impl Changelog {
    /// Create a new builder.
    pub fn builder() -> ChangelogBuilder {
        ChangelogBuilder::default()
    }
}

impl ContentCheck for Changelog {
    fn name(&self) -> &str {
        "changelog"
    }

    fn check(
        &self,
        _: &CheckGitContext,
        content: &dyn Content,
    ) -> Result<CheckResult, Box<dyn Error>> {
        let mut result = CheckResult::new();

        let changelog_changes = content
            .diffs()
            .par_iter()
            .filter(|diff| {
                diff.old_blob != diff.new_blob
                    && self.style.applies(diff.name.as_path())
                    && self.style.is_ok(diff.status)
            })
            .count();

        if changelog_changes == 0 {
            if self.required {
                result.add_error(format!(
                    "{}missing a changelog entry in {}.",
                    commit_prefix_str(content, "not allowed;"),
                    self.style.describe(),
                ));
            } else {
                result.add_warning(format!(
                    "{}please consider adding a changelog entry in {}.",
                    commit_prefix_str(content, "is missing a changelog entry;"),
                    self.style.describe(),
                ));
            };
        }

        Ok(result)
    }
}

#[cfg(feature = "config")]
pub(crate) mod config {
    use git_checks_config::{register_checks, CommitCheckConfig, IntoCheck, TopicCheckConfig};
    use serde::Deserialize;
    #[cfg(test)]
    use serde_json::json;

    #[cfg(test)]
    use crate::test;
    use crate::Changelog;
    use crate::ChangelogStyle;

    /// Configuration for the `Changelog` check.
    ///
    /// Requires the `style` key which indicates what style of changelog is used. Must be one of
    /// `"directory"` or `"file"`.
    ///
    /// For both styles, the `path` key is required. This is the path to the file or directory
    /// containing changelog information. The `required` key is a boolean that defaults to `false`.
    /// The directory style also has an optional `extension` key which is a string that changelog
    /// files in the directory are expected to have.
    ///
    /// This check is registered as a commit check with the name `"changelog"` and a topic check
    /// with the name `"changelog/topic"`.
    ///
    /// # Examples
    ///
    /// ```json
    /// {
    ///     "style": "directory",
    ///     "path": "path/to/directory",
    ///     "extension": "md",
    ///     "required": false
    /// }
    /// ```
    ///
    /// ```json
    /// {
    ///     "style": "file",
    ///     "path": "path/to/changelog.file",
    ///     "required": false
    /// }
    /// ```
    ///
    /// ```json
    /// {
    ///     "style": "files",
    ///     "paths": [
    ///         "path/to/first/changelog.file"
    ///         "path/to/second/changelog.file"
    ///     ],
    ///     "required": false
    /// }
    /// ```
    #[derive(Deserialize, Debug)]
    #[serde(tag = "style")]
    pub enum ChangelogConfig {
        #[serde(rename = "directory")]
        #[doc(hidden)]
        Directory {
            path: String,
            #[serde(default)]
            extension: Option<String>,

            required: Option<bool>,
        },
        #[serde(rename = "file")]
        #[doc(hidden)]
        File {
            path: String,

            required: Option<bool>,
        },
        #[serde(rename = "files")]
        #[doc(hidden)]
        Files {
            paths: Vec<String>,

            required: Option<bool>,
        },
    }

    impl IntoCheck for ChangelogConfig {
        type Check = Changelog;

        fn into_check(self) -> Self::Check {
            let (style, required) = match self {
                ChangelogConfig::Directory {
                    path,
                    extension,
                    required,
                } => (ChangelogStyle::directory(path, extension), required),
                ChangelogConfig::File {
                    path,
                    required,
                } => (ChangelogStyle::file(path), required),
                ChangelogConfig::Files {
                    paths,
                    required,
                } => (ChangelogStyle::files(paths), required),
            };

            let mut builder = Changelog::builder();
            builder.style(style);

            if let Some(required) = required {
                builder.required(required);
            }

            builder
                .build()
                .expect("configuration mismatch for `Changelog`")
        }
    }

    register_checks! {
        ChangelogConfig {
            "changelog" => CommitCheckConfig,
            "changelog/topic" => TopicCheckConfig,
        },
    }

    #[test]
    fn test_changelog_config_empty() {
        let json = json!({});
        let err = serde_json::from_value::<ChangelogConfig>(json).unwrap_err();
        test::check_missing_json_field(err, "style");
    }

    #[test]
    fn test_changelog_config_directory_path_is_required() {
        let json = json!({
            "style": "directory",
        });
        let err = serde_json::from_value::<ChangelogConfig>(json).unwrap_err();
        test::check_missing_json_field(err, "path");
    }

    #[test]
    fn test_changelog_config_directory_minimum_fields() {
        let exp_path = "path/to/directory";
        let json = json!({
            "style": "directory",
            "path": exp_path,
        });
        let check: ChangelogConfig = serde_json::from_value(json).unwrap();

        if let ChangelogConfig::Directory {
            ref path,
            ref extension,
            required,
        } = &check
        {
            assert_eq!(path, exp_path);
            assert_eq!(extension, &None);
            assert_eq!(required, &None);
        } else {
            panic!("did not create a directory config: {:?}", check);
        }

        let check = check.into_check();

        assert!(!check.required);
        if let ChangelogStyle::Directory {
            path,
            extension,
        } = &check.style
        {
            assert_eq!(path, exp_path);
            assert_eq!(extension, &None);
        } else {
            panic!("did not create a directory style: {:?}", check);
        }
    }

    #[test]
    fn test_changelog_config_directory_all_fields() {
        let exp_path = "path/to/directory";
        let exp_ext: String = "md".into();
        let json = json!({
            "style": "directory",
            "path": exp_path,
            "extension": exp_ext,
            "required": true,
        });
        let check: ChangelogConfig = serde_json::from_value(json).unwrap();

        if let ChangelogConfig::Directory {
            ref path,
            ref extension,
            required,
        } = &check
        {
            assert_eq!(path, exp_path);
            assert_eq!(extension, &Some(exp_ext.clone()));
            assert_eq!(required, &Some(true));
        } else {
            panic!("did not create a directory config: {:?}", check);
        }

        let check = check.into_check();

        assert!(check.required);
        if let ChangelogStyle::Directory {
            path,
            extension,
        } = &check.style
        {
            assert_eq!(path, exp_path);
            assert_eq!(extension, &Some(exp_ext));
        } else {
            panic!("did not create a directory style: {:?}", check);
        }
    }

    #[test]
    fn test_changelog_config_file_path_is_required() {
        let json = json!({
            "style": "file",
        });
        let err = serde_json::from_value::<ChangelogConfig>(json).unwrap_err();
        test::check_missing_json_field(err, "path");
    }

    #[test]
    fn test_changelog_config_file_minimum_fields() {
        let exp_path = "path/to/changelog.file";
        let json = json!({
            "style": "file",
            "path": exp_path,
        });
        let check: ChangelogConfig = serde_json::from_value(json).unwrap();

        if let ChangelogConfig::File {
            ref path,
            required,
        } = &check
        {
            assert_eq!(path, exp_path);
            assert_eq!(required, &None);
        } else {
            panic!("did not create a file config: {:?}", check);
        }

        let check = check.into_check();

        assert!(!check.required);
        if let ChangelogStyle::File {
            path,
        } = &check.style
        {
            assert_eq!(path, exp_path);
        } else {
            panic!("did not create a file style: {:?}", check);
        }
    }

    #[test]
    fn test_changelog_config_file_all_fields() {
        let exp_path = "path/to/changelog.file";
        let json = json!({
            "style": "file",
            "path": exp_path,
            "required": true,
        });
        let check: ChangelogConfig = serde_json::from_value(json).unwrap();

        if let ChangelogConfig::File {
            ref path,
            required,
        } = &check
        {
            assert_eq!(path, exp_path);
            assert_eq!(required, &Some(true));
        } else {
            panic!("did not create a file config: {:?}", check);
        }

        let check = check.into_check();

        assert!(check.required);
        if let ChangelogStyle::File {
            path,
        } = &check.style
        {
            assert_eq!(path, exp_path);
        } else {
            panic!("did not create a file style: {:?}", check);
        }
    }

    #[test]
    fn test_changelog_config_files_paths_is_required() {
        let json = json!({
            "style": "files",
        });
        let err = serde_json::from_value::<ChangelogConfig>(json).unwrap_err();
        test::check_missing_json_field(err, "paths");
    }

    #[test]
    fn test_changelog_config_files_minimum_fields() {
        let exp_path1 = "path/to/first/changelog.file";
        let exp_path2 = "path/to/second/changelog.file";
        let json = json!({
            "style": "files",
            "paths": &[exp_path1, exp_path2],
        });
        let check: ChangelogConfig = serde_json::from_value(json).unwrap();

        if let ChangelogConfig::Files {
            ref paths,
            required,
        } = &check
        {
            assert_eq!(paths, &[exp_path1, exp_path2]);
            assert_eq!(required, &None);
        } else {
            panic!("did not create a files config: {:?}", check);
        }

        let check = check.into_check();

        assert!(!check.required);
        if let ChangelogStyle::Files {
            paths,
        } = &check.style
        {
            assert_eq!(paths, &[exp_path1, exp_path2]);
        } else {
            panic!("did not create a files style: {:?}", check);
        }
    }

    #[test]
    fn test_changelog_config_files_all_fields() {
        let exp_path1 = "path/to/first/changelog.file";
        let exp_path2 = "path/to/second/changelog.file";
        let json = json!({
            "style": "files",
            "paths": &[exp_path1, exp_path2],
            "required": true,
        });
        let check: ChangelogConfig = serde_json::from_value(json).unwrap();

        if let ChangelogConfig::Files {
            ref paths,
            required,
        } = &check
        {
            assert_eq!(paths, &[exp_path1, exp_path2]);
            assert_eq!(required, &Some(true));
        } else {
            panic!("did not create a files config: {:?}", check);
        }

        let check = check.into_check();

        assert!(check.required);
        if let ChangelogStyle::Files {
            paths,
        } = &check.style
        {
            assert_eq!(paths, &[exp_path1, exp_path2]);
        } else {
            panic!("did not create a files style: {:?}", check);
        }
    }

    #[test]
    fn test_changelog_config_invalid() {
        let json = json!({
            "style": "invalid",
        });
        let err = serde_json::from_value::<ChangelogConfig>(json).unwrap_err();

        assert!(!err.is_io());
        assert!(!err.is_syntax());
        assert!(err.is_data());
        assert!(!err.is_eof());

        let msg = format!("{}", err);
        if msg != "unknown variant `invalid`, expected `directory` or `file`" {
            println!(
                "Error message doesn't match. Was a new style added? ({})",
                msg,
            );
        }
    }
}

#[cfg(test)]
mod tests {
    use git_checks_core::{Check, TopicCheck};

    use crate::builders::ChangelogBuilder;
    use crate::test::*;
    use crate::Changelog;
    use crate::ChangelogStyle;

    const CHANGELOG_DELETE: &str = "e86c0859ed36311c2ebce1ff50790eb21eabba78";
    const CHANGELOG_MISSING: &str = "66953d52f3ec6f6e4d731e7f2f70dc4000ab13ae";
    const CHANGELOG_MISSING_FIXED: &str = "a1020529e12fab5f1f7c87c60247d0068e0c9d8c";
    const CHANGELOG_MISSING_FIXED_BAD_EXT: &str = "72c4a5ead2fcb5ce6017a391a1767294944c3e9c";
    const CHANGELOG_MODE_CHANGE_BASE: &str = "254882cfbc4004a678d992818b49d08dd416528e";
    const CHANGELOG_MODE_CHANGE: &str = "98644c64aee43d327383254e36eeda477c341938";
    const FILE_CHANGELOG_INIT: &str = "3cd51c974845ff0c120e87a8e20ad5cf44798321";
    const FILE_CHANGELOG_ADDED: &str = "34762d3ec96e2a302a30842ccbb5765c2b4a61d5";
    const DIRECTORY_CHANGELOG_ADD: &str = "ff67b91112f4af4861528ac11b1797490ce18fc4";
    const DIRECTORY_CHANGELOG_DELETE: &str = "114c724c1def28ecc96f10a8dab462879c80580a";
    const DIRECTORY_CHANGELOG_MODIFY: &str = "f2719062d6c9e7c3835b397bd9553fb7b68cce5f";
    const DIRECTORY_CHANGELOG_PREFIX: &str = "5f5442e33b6d0dfe01a14d98476d14e54c4d590e";
    const DIRECTORY_CHANGELOG_BAD_EXT: &str = "93e235f10a76d58581f2d6056faa9d796156c3ea";

    #[test]
    fn test_changelog_builder_default() {
        assert!(Changelog::builder().build().is_err());
    }

    #[test]
    fn test_changelog_builder_minimum_fields() {
        assert!(Changelog::builder()
            .style(ChangelogStyle::file("changelog.md"))
            .build()
            .is_ok());
    }

    #[test]
    fn test_changelog_name_commit() {
        let check = Changelog::builder()
            .style(ChangelogStyle::file("changelog.md"))
            .build()
            .unwrap();
        assert_eq!(Check::name(&check), "changelog");
    }

    #[test]
    fn test_changelog_name_topic() {
        let check = Changelog::builder()
            .style(ChangelogStyle::file("changelog.md"))
            .build()
            .unwrap();
        assert_eq!(TopicCheck::name(&check), "changelog");
    }

    fn file_changelog() -> ChangelogBuilder {
        let mut builder = Changelog::builder();
        builder.style(ChangelogStyle::file("changelog.md"));
        builder
    }

    fn files_changelog() -> ChangelogBuilder {
        let mut builder = Changelog::builder();
        builder.style(ChangelogStyle::files(
            ["changelog.md", "other.md"].iter().cloned(),
        ));
        builder
    }

    fn directory_changelog() -> ChangelogBuilder {
        let mut builder = Changelog::builder();
        builder.style(ChangelogStyle::directory("changes", None));
        builder
    }

    fn directory_changelog_ext() -> ChangelogBuilder {
        let mut builder = Changelog::builder();
        builder.style(ChangelogStyle::directory("changes", Some("md".into())));
        builder
    }

    #[test]
    fn test_changelog_file() {
        let check = file_changelog().required(true).build().unwrap();
        let result = run_check("test_changelog_file", CHANGELOG_MISSING, check);
        test_result_errors(
            result,
            &[
                "commit 66953d52f3ec6f6e4d731e7f2f70dc4000ab13ae not allowed; missing a changelog \
                 entry in the `changelog.md` file.",
            ],
        );
    }

    #[test]
    fn test_changelog_file_mode_change() {
        let check = file_changelog().required(true).build().unwrap();
        let conf = make_check_conf(&check);
        let result = test_check_base(
            "test_changelog_file_mode_change",
            CHANGELOG_MODE_CHANGE,
            CHANGELOG_MODE_CHANGE_BASE,
            &conf,
        );
        test_result_errors(
            result,
            &[
                "commit 98644c64aee43d327383254e36eeda477c341938 not allowed; missing a changelog \
                 entry in the `changelog.md` file.",
            ],
        );
    }

    #[test]
    fn test_changelog_file_init() {
        let check = file_changelog().required(true).build().unwrap();
        run_check_ok("test_changelog_file_init", FILE_CHANGELOG_INIT, check);
    }

    #[test]
    fn test_changelog_file_ok() {
        let check = file_changelog().required(true).build().unwrap();
        run_check_ok("test_changelog_file_ok", FILE_CHANGELOG_ADDED, check);
    }

    #[test]
    fn test_changelog_file_delete() {
        let check = file_changelog().required(true).build().unwrap();
        let result = run_check("test_changelog_file_delete", CHANGELOG_DELETE, check);
        test_result_errors(
            result,
            &[
                "commit e86c0859ed36311c2ebce1ff50790eb21eabba78 not allowed; missing a changelog \
                 entry in the `changelog.md` file.",
            ],
        );
    }

    #[test]
    fn test_changelog_files() {
        let check = files_changelog().required(true).build().unwrap();
        let result = run_check("test_changelog_files", CHANGELOG_MISSING, check);
        test_result_errors(
            result,
            &[
                "commit 66953d52f3ec6f6e4d731e7f2f70dc4000ab13ae not allowed; missing a changelog \
                 entry in one of the `changelog.md`, `other.md` files.",
            ],
        );
    }

    #[test]
    fn test_changelog_files_mode_change() {
        let check = files_changelog().required(true).build().unwrap();
        let conf = make_check_conf(&check);
        let result = test_check_base(
            "test_changelog_files_mode_change",
            CHANGELOG_MODE_CHANGE,
            CHANGELOG_MODE_CHANGE_BASE,
            &conf,
        );
        test_result_errors(
            result,
            &[
                "commit 98644c64aee43d327383254e36eeda477c341938 not allowed; missing a changelog \
                 entry in one of the `changelog.md`, `other.md` files.",
            ],
        );
    }

    #[test]
    fn test_changelog_files_init() {
        let check = files_changelog().required(true).build().unwrap();
        run_check_ok("test_changelog_files_init", FILE_CHANGELOG_INIT, check);
    }

    #[test]
    fn test_changelog_files_ok() {
        let check = files_changelog().required(true).build().unwrap();
        run_check_ok("test_changelog_files_ok", FILE_CHANGELOG_ADDED, check);
    }

    #[test]
    fn test_changelog_files_delete() {
        let check = files_changelog().required(true).build().unwrap();
        let result = run_check("test_changelog_files_delete", CHANGELOG_DELETE, check);
        test_result_errors(
            result,
            &[
                "commit e86c0859ed36311c2ebce1ff50790eb21eabba78 not allowed; missing a changelog \
                 entry in one of the `changelog.md`, `other.md` files.",
            ],
        );
    }

    #[test]
    fn test_changelog_directory() {
        let check = directory_changelog().required(true).build().unwrap();
        let result = run_check("test_changelog_directory", CHANGELOG_MISSING, check);
        test_result_errors(
            result,
            &[
                "commit 66953d52f3ec6f6e4d731e7f2f70dc4000ab13ae not allowed; missing a changelog \
                 entry in a file in `changes`.",
            ],
        );
    }

    #[test]
    fn test_changelog_directory_mode_change() {
        let check = directory_changelog().required(true).build().unwrap();
        let conf = make_check_conf(&check);
        let result = test_check_base(
            "test_changelog_directory_mode_change",
            CHANGELOG_MODE_CHANGE,
            CHANGELOG_MODE_CHANGE_BASE,
            &conf,
        );
        test_result_errors(
            result,
            &[
                "commit 98644c64aee43d327383254e36eeda477c341938 not allowed; missing a changelog \
                 entry in a file in `changes`.",
            ],
        );
    }

    #[test]
    fn test_changelog_directory_bad_extension() {
        let check = directory_changelog_ext().required(true).build().unwrap();
        let result = run_check(
            "test_changelog_directory_bad_extension",
            DIRECTORY_CHANGELOG_BAD_EXT,
            check,
        );
        test_result_errors(
            result,
            &[
                "commit 93e235f10a76d58581f2d6056faa9d796156c3ea not allowed; missing a changelog \
                 entry in a file ending with `.md` in `changes`.",
            ],
        );
    }

    #[test]
    fn test_changelog_directory_delete() {
        let check = directory_changelog().required(true).build().unwrap();
        let conf = make_check_conf(&check);
        let result = test_check_base(
            "test_changelog_directory_delete",
            DIRECTORY_CHANGELOG_DELETE,
            CHANGELOG_MISSING_FIXED_BAD_EXT,
            &conf,
        );
        test_result_ok(result);
    }

    #[test]
    fn test_changelog_directory_modify() {
        let check = directory_changelog().required(true).build().unwrap();
        let conf = make_check_conf(&check);
        let result = test_check_base(
            "test_changelog_directory_modify",
            DIRECTORY_CHANGELOG_MODIFY,
            CHANGELOG_MISSING_FIXED_BAD_EXT,
            &conf,
        );
        test_result_ok(result);
    }

    #[test]
    fn test_changelog_directory_prefix() {
        let check = directory_changelog().required(true).build().unwrap();
        let result = run_check(
            "test_changelog_directory_prefix",
            DIRECTORY_CHANGELOG_PREFIX,
            check,
        );
        test_result_errors(
            result,
            &[
                "commit 5f5442e33b6d0dfe01a14d98476d14e54c4d590e not allowed; missing a changelog \
                 entry in a file in `changes`.",
            ],
        );
    }

    #[test]
    fn test_changelog_directory_ok() {
        let check = directory_changelog().required(true).build().unwrap();
        run_check_ok(
            "test_changelog_directory_ok",
            DIRECTORY_CHANGELOG_ADD,
            check,
        );
    }

    #[test]
    fn test_changelog_directory_ok_extension() {
        let check = directory_changelog_ext().required(true).build().unwrap();
        run_check_ok(
            "test_changelog_directory_ok_extension",
            DIRECTORY_CHANGELOG_ADD,
            check,
        );
    }

    #[test]
    fn test_changelog_warning_file() {
        let check = file_changelog().build().unwrap();
        let result = run_check("test_changelog_warning_file", CHANGELOG_MISSING, check);
        test_result_warnings(result, &[
            "commit 66953d52f3ec6f6e4d731e7f2f70dc4000ab13ae is missing a changelog entry; please \
             consider adding a changelog entry in the `changelog.md` file.",
        ]);
    }

    #[test]
    fn test_changelog_warning_files() {
        let check = files_changelog().build().unwrap();
        let result = run_check("test_changelog_warning_files", CHANGELOG_MISSING, check);
        test_result_warnings(result, &[
            "commit 66953d52f3ec6f6e4d731e7f2f70dc4000ab13ae is missing a changelog entry; please \
             consider adding a changelog entry in one of the `changelog.md`, `other.md` files.",
        ]);
    }

    #[test]
    fn test_changelog_warning_directory() {
        let check = directory_changelog().build().unwrap();
        let result = run_check("test_changelog_warning_directory", CHANGELOG_MISSING, check);
        test_result_warnings(result, &[
            "commit 66953d52f3ec6f6e4d731e7f2f70dc4000ab13ae is missing a changelog entry; please \
             consider adding a changelog entry in a file in `changes`.",
        ]);
    }

    #[test]
    fn test_changelog_topic_file() {
        let check = file_changelog().required(true).build().unwrap();
        let result = run_topic_check("test_changelog_topic_file", CHANGELOG_MISSING, check);
        test_result_errors(
            result,
            &["missing a changelog entry in the `changelog.md` file."],
        );
    }

    #[test]
    fn test_changelog_topic_file_warning() {
        let check = file_changelog().build().unwrap();
        let result = run_topic_check(
            "test_changelog_topic_file_warning",
            CHANGELOG_MISSING,
            check,
        );
        test_result_warnings(
            result,
            &["please consider adding a changelog entry in the `changelog.md` file."],
        );
    }

    #[test]
    fn test_changelog_topic_files() {
        let check = files_changelog().required(true).build().unwrap();
        let result = run_topic_check("test_changelog_topic_files", CHANGELOG_MISSING, check);
        test_result_errors(
            result,
            &["missing a changelog entry in one of the `changelog.md`, `other.md` files."],
        );
    }

    #[test]
    fn test_changelog_topic_files_warning() {
        let check = files_changelog().build().unwrap();
        let result = run_topic_check(
            "test_changelog_topic_files_warning",
            CHANGELOG_MISSING,
            check,
        );
        test_result_warnings(result, &[
            "please consider adding a changelog entry in one of the `changelog.md`, `other.md` files.",
        ]);
    }

    #[test]
    fn test_changelog_topic_directory() {
        let check = directory_changelog().required(true).build().unwrap();
        let result = run_topic_check("test_changelog_topic_directory", CHANGELOG_MISSING, check);
        test_result_errors(
            result,
            &["missing a changelog entry in a file in `changes`."],
        );
    }

    #[test]
    fn test_changelog_topic_directory_warning() {
        let check = directory_changelog().build().unwrap();
        let result = run_topic_check(
            "test_changelog_topic_directory_warning",
            CHANGELOG_MISSING,
            check,
        );
        test_result_warnings(
            result,
            &["please consider adding a changelog entry in a file in `changes`."],
        );
    }

    #[test]
    fn test_changelog_topic_directory_bad_ext() {
        let check = directory_changelog_ext().required(true).build().unwrap();
        let result = run_topic_check(
            "test_changelog_topic_directory_bad_ext",
            CHANGELOG_MISSING_FIXED,
            check,
        );
        test_result_errors(
            result,
            &["missing a changelog entry in a file ending with `.md` in `changes`."],
        );
    }

    #[test]
    fn test_changelog_topic_directory_warning_bad_ext() {
        let check = directory_changelog_ext().build().unwrap();
        let result = run_topic_check(
            "test_changelog_topic_directory_warning_bad_ext",
            CHANGELOG_MISSING_FIXED,
            check,
        );
        test_result_warnings(result, &[
            "please consider adding a changelog entry in a file ending with `.md` in `changes`.",
        ]);
    }

    #[test]
    fn test_changelog_topic_fixed_file() {
        let check = file_changelog().required(true).build().unwrap();
        run_topic_check_ok(
            "test_changelog_topic_fixed_file",
            CHANGELOG_MISSING_FIXED,
            check,
        );
    }

    #[test]
    fn test_changelog_topic_fixed_files() {
        let check = files_changelog().required(true).build().unwrap();
        run_topic_check_ok(
            "test_changelog_topic_fixed_files",
            CHANGELOG_MISSING_FIXED,
            check,
        );
    }

    #[test]
    fn test_changelog_topic_fixed_directory() {
        let check = directory_changelog().required(true).build().unwrap();
        run_topic_check_ok(
            "test_changelog_topic_fixed_directory",
            CHANGELOG_MISSING_FIXED,
            check,
        );
    }

    #[test]
    fn test_changelog_topic_fixed_directory_bad_ext() {
        let check = directory_changelog().required(true).build().unwrap();
        run_topic_check_ok(
            "test_changelog_topic_fixed_directory_bad_ext",
            CHANGELOG_MISSING_FIXED_BAD_EXT,
            check,
        );
    }
}