git_checks/
changelog.rs

1// Copyright Kitware, Inc.
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9use std::path::Path;
10
11use derive_builder::Builder;
12use git_checks_core::impl_prelude::*;
13use itertools::Itertools;
14use rayon::prelude::*;
15
16/// The style of changelog management in use.
17#[derive(Debug, Clone)]
18pub enum ChangelogStyle {
19    /// A directory stores a file per changelog entry.
20    Directory {
21        /// The path to the directory containing changelog entry files.
22        path: String,
23        /// The extension used for changelog files.
24        extension: Option<String>,
25    },
26    /// A file contains the changelog entries for a project.
27    File {
28        /// The path to the changelog.
29        path: String,
30    },
31    /// A set of files containing the changelog entries for a project.
32    Files {
33        /// The paths to the changelog files.
34        paths: Vec<String>,
35    },
36}
37
38impl ChangelogStyle {
39    /// A changelog stored in a file at a given path.
40    pub fn file<P>(path: P) -> Self
41    where
42        P: Into<String>,
43    {
44        ChangelogStyle::File {
45            path: path.into(),
46        }
47    }
48
49    /// A directory containing many files, each with a changelog entry.
50    ///
51    /// Entries may also be required to have a given extension.
52    pub fn directory<P>(path: P, ext: Option<String>) -> Self
53    where
54        P: Into<String>,
55    {
56        ChangelogStyle::Directory {
57            path: path.into(),
58            extension: ext,
59        }
60    }
61
62    /// A changelog stored in files at the given paths.
63    pub fn files<P, I>(paths: I) -> Self
64    where
65        I: IntoIterator<Item = P>,
66        P: Into<String>,
67    {
68        ChangelogStyle::Files {
69            paths: paths.into_iter().map(Into::into).collect(),
70        }
71    }
72
73    /// A description of the changelog.
74    fn describe(&self) -> String {
75        match *self {
76            ChangelogStyle::Directory {
77                ref path,
78                ref extension,
79            } => {
80                if let Some(ext) = extension.as_ref() {
81                    format!("a file ending with `.{}` in `{}`", ext, path)
82                } else {
83                    format!("a file in `{}`", path)
84                }
85            },
86            ChangelogStyle::File {
87                ref path,
88            } => format!("the `{}` file", path),
89            ChangelogStyle::Files {
90                ref paths,
91            } => format!("one of the `{}` files", paths.iter().format("`, `")),
92        }
93    }
94
95    /// Whether the changelog style cares about the given path.
96    fn applies(&self, diff_path: &Path) -> bool {
97        match *self {
98            ChangelogStyle::Directory {
99                ref path,
100                ref extension,
101            } => {
102                let ext_ok = extension.as_ref().map_or(true, |ext| {
103                    diff_path
104                        .extension()
105                        .is_some_and(|diff_ext| diff_ext == (ext.as_ref() as &Path))
106                });
107
108                ext_ok && diff_path.starts_with(path)
109            },
110            ChangelogStyle::File {
111                ref path,
112            } => diff_path == (path.as_ref() as &Path),
113            ChangelogStyle::Files {
114                ref paths,
115            } => {
116                paths
117                    .iter()
118                    .any(|path| diff_path == (path.as_ref() as &Path))
119            },
120        }
121    }
122
123    /// Whether the path with the given status change is OK.
124    #[allow(clippy::match_like_matches_macro)]
125    fn is_ok(&self, status: StatusChange) -> bool {
126        match *self {
127            ChangelogStyle::Directory {
128                ..
129            } => {
130                match status {
131                    // Adding a new file is OK.
132                    StatusChange::Added
133                    // Modifying an existing file is OK.
134                    | StatusChange::Modified(_)
135                    // Deleting a file is OK (e.g., reverts).
136                    | StatusChange::Deleted => true,
137                    _ => false,
138                }
139            },
140            ChangelogStyle::File {
141                ..
142            }
143            | ChangelogStyle::Files {
144                ..
145            } => {
146                match status {
147                    // Adding the file is OK (initialization).
148                    StatusChange::Added
149                    // Modifying the file is OK.
150                    | StatusChange::Modified(_) => true,
151                    // Removing the file is not OK.
152                    _ => false,
153                }
154            },
155        }
156    }
157}
158
159/// Check for changelog modifications.
160///
161/// This checks to make sure that a changelog entry has been added (or modified) in every commit or
162/// topic.
163#[derive(Builder, Debug, Clone)]
164#[builder(field(private))]
165pub struct Changelog {
166    /// The changelog management style in use.
167    ///
168    /// Configuration: Required
169    style: ChangelogStyle,
170    /// Whether entries are required or not.
171    ///
172    /// Configuration: Optional
173    /// Default: `false`
174    #[builder(default = "false")]
175    required: bool,
176}
177
178impl Changelog {
179    /// Create a new builder.
180    pub fn builder() -> ChangelogBuilder {
181        Default::default()
182    }
183}
184
185impl ContentCheck for Changelog {
186    fn name(&self) -> &str {
187        "changelog"
188    }
189
190    fn check(
191        &self,
192        _: &CheckGitContext,
193        content: &dyn Content,
194    ) -> Result<CheckResult, Box<dyn Error>> {
195        let mut result = CheckResult::new();
196
197        let changelog_changes = content
198            .diffs()
199            .par_iter()
200            .filter(|diff| {
201                diff.old_blob != diff.new_blob
202                    && self.style.applies(diff.name.as_path())
203                    && self.style.is_ok(diff.status)
204            })
205            .count();
206
207        if changelog_changes == 0 {
208            if self.required {
209                result.add_error(format!(
210                    "{}missing a changelog entry in {}.",
211                    commit_prefix_str(content, "not allowed;"),
212                    self.style.describe(),
213                ));
214            } else {
215                result.add_warning(format!(
216                    "{}please consider adding a changelog entry in {}.",
217                    commit_prefix_str(content, "is missing a changelog entry;"),
218                    self.style.describe(),
219                ));
220            };
221        }
222
223        Ok(result)
224    }
225}
226
227#[cfg(feature = "config")]
228pub(crate) mod config {
229    use git_checks_config::{register_checks, CommitCheckConfig, IntoCheck, TopicCheckConfig};
230    use serde::Deserialize;
231    #[cfg(test)]
232    use serde_json::json;
233
234    #[cfg(test)]
235    use crate::test;
236    use crate::Changelog;
237    use crate::ChangelogStyle;
238
239    /// Configuration for the `Changelog` check.
240    ///
241    /// Requires the `style` key which indicates what style of changelog is used. Must be one of
242    /// `"directory"` or `"file"`.
243    ///
244    /// For both styles, the `path` key is required. This is the path to the file or directory
245    /// containing changelog information. The `required` key is a boolean that defaults to `false`.
246    /// The directory style also has an optional `extension` key which is a string that changelog
247    /// files in the directory are expected to have.
248    ///
249    /// This check is registered as a commit check with the name `"changelog"` and a topic check
250    /// with the name `"changelog/topic"`.
251    ///
252    /// # Examples
253    ///
254    /// ```json
255    /// {
256    ///     "style": "directory",
257    ///     "path": "path/to/directory",
258    ///     "extension": "md",
259    ///     "required": false
260    /// }
261    /// ```
262    ///
263    /// ```json
264    /// {
265    ///     "style": "file",
266    ///     "path": "path/to/changelog.file",
267    ///     "required": false
268    /// }
269    /// ```
270    ///
271    /// ```json
272    /// {
273    ///     "style": "files",
274    ///     "paths": [
275    ///         "path/to/first/changelog.file"
276    ///         "path/to/second/changelog.file"
277    ///     ],
278    ///     "required": false
279    /// }
280    /// ```
281    #[derive(Deserialize, Debug)]
282    #[serde(tag = "style")]
283    pub enum ChangelogConfig {
284        #[serde(rename = "directory")]
285        #[doc(hidden)]
286        Directory {
287            path: String,
288            #[serde(default)]
289            extension: Option<String>,
290
291            required: Option<bool>,
292        },
293        #[serde(rename = "file")]
294        #[doc(hidden)]
295        File {
296            path: String,
297
298            required: Option<bool>,
299        },
300        #[serde(rename = "files")]
301        #[doc(hidden)]
302        Files {
303            paths: Vec<String>,
304
305            required: Option<bool>,
306        },
307    }
308
309    impl IntoCheck for ChangelogConfig {
310        type Check = Changelog;
311
312        fn into_check(self) -> Self::Check {
313            let (style, required) = match self {
314                ChangelogConfig::Directory {
315                    path,
316                    extension,
317                    required,
318                } => (ChangelogStyle::directory(path, extension), required),
319                ChangelogConfig::File {
320                    path,
321                    required,
322                } => (ChangelogStyle::file(path), required),
323                ChangelogConfig::Files {
324                    paths,
325                    required,
326                } => (ChangelogStyle::files(paths), required),
327            };
328
329            let mut builder = Changelog::builder();
330            builder.style(style);
331
332            if let Some(required) = required {
333                builder.required(required);
334            }
335
336            builder
337                .build()
338                .expect("configuration mismatch for `Changelog`")
339        }
340    }
341
342    register_checks! {
343        ChangelogConfig {
344            "changelog" => CommitCheckConfig,
345            "changelog/topic" => TopicCheckConfig,
346        },
347    }
348
349    #[test]
350    fn test_changelog_config_empty() {
351        let json = json!({});
352        let err = serde_json::from_value::<ChangelogConfig>(json).unwrap_err();
353        test::check_missing_json_field(err, "style");
354    }
355
356    #[test]
357    fn test_changelog_config_directory_path_is_required() {
358        let json = json!({
359            "style": "directory",
360        });
361        let err = serde_json::from_value::<ChangelogConfig>(json).unwrap_err();
362        test::check_missing_json_field(err, "path");
363    }
364
365    #[test]
366    fn test_changelog_config_directory_minimum_fields() {
367        let exp_path = "path/to/directory";
368        let json = json!({
369            "style": "directory",
370            "path": exp_path,
371        });
372        let check: ChangelogConfig = serde_json::from_value(json).unwrap();
373
374        if let ChangelogConfig::Directory {
375            ref path,
376            ref extension,
377            required,
378        } = &check
379        {
380            assert_eq!(path, exp_path);
381            assert_eq!(extension, &None);
382            assert_eq!(required, &None);
383        } else {
384            panic!("did not create a directory config: {:?}", check);
385        }
386
387        let check = check.into_check();
388
389        assert!(!check.required);
390        if let ChangelogStyle::Directory {
391            path,
392            extension,
393        } = &check.style
394        {
395            assert_eq!(path, exp_path);
396            assert_eq!(extension, &None);
397        } else {
398            panic!("did not create a directory style: {:?}", check);
399        }
400    }
401
402    #[test]
403    fn test_changelog_config_directory_all_fields() {
404        let exp_path = "path/to/directory";
405        let exp_ext: String = "md".into();
406        let json = json!({
407            "style": "directory",
408            "path": exp_path,
409            "extension": exp_ext,
410            "required": true,
411        });
412        let check: ChangelogConfig = serde_json::from_value(json).unwrap();
413
414        if let ChangelogConfig::Directory {
415            ref path,
416            ref extension,
417            required,
418        } = &check
419        {
420            assert_eq!(path, exp_path);
421            assert_eq!(extension, &Some(exp_ext.clone()));
422            assert_eq!(required, &Some(true));
423        } else {
424            panic!("did not create a directory config: {:?}", check);
425        }
426
427        let check = check.into_check();
428
429        assert!(check.required);
430        if let ChangelogStyle::Directory {
431            path,
432            extension,
433        } = &check.style
434        {
435            assert_eq!(path, exp_path);
436            assert_eq!(extension, &Some(exp_ext));
437        } else {
438            panic!("did not create a directory style: {:?}", check);
439        }
440    }
441
442    #[test]
443    fn test_changelog_config_file_path_is_required() {
444        let json = json!({
445            "style": "file",
446        });
447        let err = serde_json::from_value::<ChangelogConfig>(json).unwrap_err();
448        test::check_missing_json_field(err, "path");
449    }
450
451    #[test]
452    fn test_changelog_config_file_minimum_fields() {
453        let exp_path = "path/to/changelog.file";
454        let json = json!({
455            "style": "file",
456            "path": exp_path,
457        });
458        let check: ChangelogConfig = serde_json::from_value(json).unwrap();
459
460        if let ChangelogConfig::File {
461            ref path,
462            required,
463        } = &check
464        {
465            assert_eq!(path, exp_path);
466            assert_eq!(required, &None);
467        } else {
468            panic!("did not create a file config: {:?}", check);
469        }
470
471        let check = check.into_check();
472
473        assert!(!check.required);
474        if let ChangelogStyle::File {
475            path,
476        } = &check.style
477        {
478            assert_eq!(path, exp_path);
479        } else {
480            panic!("did not create a file style: {:?}", check);
481        }
482    }
483
484    #[test]
485    fn test_changelog_config_file_all_fields() {
486        let exp_path = "path/to/changelog.file";
487        let json = json!({
488            "style": "file",
489            "path": exp_path,
490            "required": true,
491        });
492        let check: ChangelogConfig = serde_json::from_value(json).unwrap();
493
494        if let ChangelogConfig::File {
495            ref path,
496            required,
497        } = &check
498        {
499            assert_eq!(path, exp_path);
500            assert_eq!(required, &Some(true));
501        } else {
502            panic!("did not create a file config: {:?}", check);
503        }
504
505        let check = check.into_check();
506
507        assert!(check.required);
508        if let ChangelogStyle::File {
509            path,
510        } = &check.style
511        {
512            assert_eq!(path, exp_path);
513        } else {
514            panic!("did not create a file style: {:?}", check);
515        }
516    }
517
518    #[test]
519    fn test_changelog_config_files_paths_is_required() {
520        let json = json!({
521            "style": "files",
522        });
523        let err = serde_json::from_value::<ChangelogConfig>(json).unwrap_err();
524        test::check_missing_json_field(err, "paths");
525    }
526
527    #[test]
528    fn test_changelog_config_files_minimum_fields() {
529        let exp_path1 = "path/to/first/changelog.file";
530        let exp_path2 = "path/to/second/changelog.file";
531        let json = json!({
532            "style": "files",
533            "paths": &[exp_path1, exp_path2],
534        });
535        let check: ChangelogConfig = serde_json::from_value(json).unwrap();
536
537        if let ChangelogConfig::Files {
538            ref paths,
539            required,
540        } = &check
541        {
542            assert_eq!(paths, &[exp_path1, exp_path2]);
543            assert_eq!(required, &None);
544        } else {
545            panic!("did not create a files config: {:?}", check);
546        }
547
548        let check = check.into_check();
549
550        assert!(!check.required);
551        if let ChangelogStyle::Files {
552            paths,
553        } = &check.style
554        {
555            assert_eq!(paths, &[exp_path1, exp_path2]);
556        } else {
557            panic!("did not create a files style: {:?}", check);
558        }
559    }
560
561    #[test]
562    fn test_changelog_config_files_all_fields() {
563        let exp_path1 = "path/to/first/changelog.file";
564        let exp_path2 = "path/to/second/changelog.file";
565        let json = json!({
566            "style": "files",
567            "paths": &[exp_path1, exp_path2],
568            "required": true,
569        });
570        let check: ChangelogConfig = serde_json::from_value(json).unwrap();
571
572        if let ChangelogConfig::Files {
573            ref paths,
574            required,
575        } = &check
576        {
577            assert_eq!(paths, &[exp_path1, exp_path2]);
578            assert_eq!(required, &Some(true));
579        } else {
580            panic!("did not create a files config: {:?}", check);
581        }
582
583        let check = check.into_check();
584
585        assert!(check.required);
586        if let ChangelogStyle::Files {
587            paths,
588        } = &check.style
589        {
590            assert_eq!(paths, &[exp_path1, exp_path2]);
591        } else {
592            panic!("did not create a files style: {:?}", check);
593        }
594    }
595
596    #[test]
597    fn test_changelog_config_invalid() {
598        let json = json!({
599            "style": "invalid",
600        });
601        let err = serde_json::from_value::<ChangelogConfig>(json).unwrap_err();
602
603        assert!(!err.is_io());
604        assert!(!err.is_syntax());
605        assert!(err.is_data());
606        assert!(!err.is_eof());
607
608        let msg = err.to_string();
609        if msg != "unknown variant `invalid`, expected `directory` or `file`" {
610            println!(
611                "Error message doesn't match. Was a new style added? ({})",
612                msg,
613            );
614        }
615    }
616}
617
618#[cfg(test)]
619mod tests {
620    use git_checks_core::{Check, TopicCheck};
621
622    use crate::builders::ChangelogBuilder;
623    use crate::test::*;
624    use crate::Changelog;
625    use crate::ChangelogStyle;
626
627    const CHANGELOG_DELETE: &str = "e86c0859ed36311c2ebce1ff50790eb21eabba78";
628    const CHANGELOG_MISSING: &str = "66953d52f3ec6f6e4d731e7f2f70dc4000ab13ae";
629    const CHANGELOG_MISSING_FIXED: &str = "a1020529e12fab5f1f7c87c60247d0068e0c9d8c";
630    const CHANGELOG_MISSING_FIXED_BAD_EXT: &str = "72c4a5ead2fcb5ce6017a391a1767294944c3e9c";
631    const CHANGELOG_MODE_CHANGE_BASE: &str = "254882cfbc4004a678d992818b49d08dd416528e";
632    const CHANGELOG_MODE_CHANGE: &str = "98644c64aee43d327383254e36eeda477c341938";
633    const FILE_CHANGELOG_INIT: &str = "3cd51c974845ff0c120e87a8e20ad5cf44798321";
634    const FILE_CHANGELOG_ADDED: &str = "34762d3ec96e2a302a30842ccbb5765c2b4a61d5";
635    const DIRECTORY_CHANGELOG_ADD: &str = "ff67b91112f4af4861528ac11b1797490ce18fc4";
636    const DIRECTORY_CHANGELOG_DELETE: &str = "114c724c1def28ecc96f10a8dab462879c80580a";
637    const DIRECTORY_CHANGELOG_MODIFY: &str = "f2719062d6c9e7c3835b397bd9553fb7b68cce5f";
638    const DIRECTORY_CHANGELOG_PREFIX: &str = "5f5442e33b6d0dfe01a14d98476d14e54c4d590e";
639    const DIRECTORY_CHANGELOG_BAD_EXT: &str = "93e235f10a76d58581f2d6056faa9d796156c3ea";
640
641    #[test]
642    fn test_changelog_builder_default() {
643        assert!(Changelog::builder().build().is_err());
644    }
645
646    #[test]
647    fn test_changelog_builder_minimum_fields() {
648        assert!(Changelog::builder()
649            .style(ChangelogStyle::file("changelog.md"))
650            .build()
651            .is_ok());
652    }
653
654    #[test]
655    fn test_changelog_name_commit() {
656        let check = Changelog::builder()
657            .style(ChangelogStyle::file("changelog.md"))
658            .build()
659            .unwrap();
660        assert_eq!(Check::name(&check), "changelog");
661    }
662
663    #[test]
664    fn test_changelog_name_topic() {
665        let check = Changelog::builder()
666            .style(ChangelogStyle::file("changelog.md"))
667            .build()
668            .unwrap();
669        assert_eq!(TopicCheck::name(&check), "changelog");
670    }
671
672    fn file_changelog() -> ChangelogBuilder {
673        let mut builder = Changelog::builder();
674        builder.style(ChangelogStyle::file("changelog.md"));
675        builder
676    }
677
678    fn files_changelog() -> ChangelogBuilder {
679        let mut builder = Changelog::builder();
680        builder.style(ChangelogStyle::files(
681            ["changelog.md", "other.md"].iter().cloned(),
682        ));
683        builder
684    }
685
686    fn directory_changelog() -> ChangelogBuilder {
687        let mut builder = Changelog::builder();
688        builder.style(ChangelogStyle::directory("changes", None));
689        builder
690    }
691
692    fn directory_changelog_ext() -> ChangelogBuilder {
693        let mut builder = Changelog::builder();
694        builder.style(ChangelogStyle::directory("changes", Some("md".into())));
695        builder
696    }
697
698    #[test]
699    fn test_changelog_file() {
700        let check = file_changelog().required(true).build().unwrap();
701        let result = run_check("test_changelog_file", CHANGELOG_MISSING, check);
702        test_result_errors(
703            result,
704            &[
705                "commit 66953d52f3ec6f6e4d731e7f2f70dc4000ab13ae not allowed; missing a changelog \
706                 entry in the `changelog.md` file.",
707            ],
708        );
709    }
710
711    #[test]
712    fn test_changelog_file_mode_change() {
713        let check = file_changelog().required(true).build().unwrap();
714        let conf = make_check_conf(&check);
715        let result = test_check_base(
716            "test_changelog_file_mode_change",
717            CHANGELOG_MODE_CHANGE,
718            CHANGELOG_MODE_CHANGE_BASE,
719            &conf,
720        );
721        test_result_errors(
722            result,
723            &[
724                "commit 98644c64aee43d327383254e36eeda477c341938 not allowed; missing a changelog \
725                 entry in the `changelog.md` file.",
726            ],
727        );
728    }
729
730    #[test]
731    fn test_changelog_file_init() {
732        let check = file_changelog().required(true).build().unwrap();
733        run_check_ok("test_changelog_file_init", FILE_CHANGELOG_INIT, check);
734    }
735
736    #[test]
737    fn test_changelog_file_ok() {
738        let check = file_changelog().required(true).build().unwrap();
739        run_check_ok("test_changelog_file_ok", FILE_CHANGELOG_ADDED, check);
740    }
741
742    #[test]
743    fn test_changelog_file_delete() {
744        let check = file_changelog().required(true).build().unwrap();
745        let result = run_check("test_changelog_file_delete", CHANGELOG_DELETE, check);
746        test_result_errors(
747            result,
748            &[
749                "commit e86c0859ed36311c2ebce1ff50790eb21eabba78 not allowed; missing a changelog \
750                 entry in the `changelog.md` file.",
751            ],
752        );
753    }
754
755    #[test]
756    fn test_changelog_files() {
757        let check = files_changelog().required(true).build().unwrap();
758        let result = run_check("test_changelog_files", CHANGELOG_MISSING, check);
759        test_result_errors(
760            result,
761            &[
762                "commit 66953d52f3ec6f6e4d731e7f2f70dc4000ab13ae not allowed; missing a changelog \
763                 entry in one of the `changelog.md`, `other.md` files.",
764            ],
765        );
766    }
767
768    #[test]
769    fn test_changelog_files_mode_change() {
770        let check = files_changelog().required(true).build().unwrap();
771        let conf = make_check_conf(&check);
772        let result = test_check_base(
773            "test_changelog_files_mode_change",
774            CHANGELOG_MODE_CHANGE,
775            CHANGELOG_MODE_CHANGE_BASE,
776            &conf,
777        );
778        test_result_errors(
779            result,
780            &[
781                "commit 98644c64aee43d327383254e36eeda477c341938 not allowed; missing a changelog \
782                 entry in one of the `changelog.md`, `other.md` files.",
783            ],
784        );
785    }
786
787    #[test]
788    fn test_changelog_files_init() {
789        let check = files_changelog().required(true).build().unwrap();
790        run_check_ok("test_changelog_files_init", FILE_CHANGELOG_INIT, check);
791    }
792
793    #[test]
794    fn test_changelog_files_ok() {
795        let check = files_changelog().required(true).build().unwrap();
796        run_check_ok("test_changelog_files_ok", FILE_CHANGELOG_ADDED, check);
797    }
798
799    #[test]
800    fn test_changelog_files_delete() {
801        let check = files_changelog().required(true).build().unwrap();
802        let result = run_check("test_changelog_files_delete", CHANGELOG_DELETE, check);
803        test_result_errors(
804            result,
805            &[
806                "commit e86c0859ed36311c2ebce1ff50790eb21eabba78 not allowed; missing a changelog \
807                 entry in one of the `changelog.md`, `other.md` files.",
808            ],
809        );
810    }
811
812    #[test]
813    fn test_changelog_directory() {
814        let check = directory_changelog().required(true).build().unwrap();
815        let result = run_check("test_changelog_directory", CHANGELOG_MISSING, check);
816        test_result_errors(
817            result,
818            &[
819                "commit 66953d52f3ec6f6e4d731e7f2f70dc4000ab13ae not allowed; missing a changelog \
820                 entry in a file in `changes`.",
821            ],
822        );
823    }
824
825    #[test]
826    fn test_changelog_directory_mode_change() {
827        let check = directory_changelog().required(true).build().unwrap();
828        let conf = make_check_conf(&check);
829        let result = test_check_base(
830            "test_changelog_directory_mode_change",
831            CHANGELOG_MODE_CHANGE,
832            CHANGELOG_MODE_CHANGE_BASE,
833            &conf,
834        );
835        test_result_errors(
836            result,
837            &[
838                "commit 98644c64aee43d327383254e36eeda477c341938 not allowed; missing a changelog \
839                 entry in a file in `changes`.",
840            ],
841        );
842    }
843
844    #[test]
845    fn test_changelog_directory_bad_extension() {
846        let check = directory_changelog_ext().required(true).build().unwrap();
847        let result = run_check(
848            "test_changelog_directory_bad_extension",
849            DIRECTORY_CHANGELOG_BAD_EXT,
850            check,
851        );
852        test_result_errors(
853            result,
854            &[
855                "commit 93e235f10a76d58581f2d6056faa9d796156c3ea not allowed; missing a changelog \
856                 entry in a file ending with `.md` in `changes`.",
857            ],
858        );
859    }
860
861    #[test]
862    fn test_changelog_directory_delete() {
863        let check = directory_changelog().required(true).build().unwrap();
864        let conf = make_check_conf(&check);
865        let result = test_check_base(
866            "test_changelog_directory_delete",
867            DIRECTORY_CHANGELOG_DELETE,
868            CHANGELOG_MISSING_FIXED_BAD_EXT,
869            &conf,
870        );
871        test_result_ok(result);
872    }
873
874    #[test]
875    fn test_changelog_directory_modify() {
876        let check = directory_changelog().required(true).build().unwrap();
877        let conf = make_check_conf(&check);
878        let result = test_check_base(
879            "test_changelog_directory_modify",
880            DIRECTORY_CHANGELOG_MODIFY,
881            CHANGELOG_MISSING_FIXED_BAD_EXT,
882            &conf,
883        );
884        test_result_ok(result);
885    }
886
887    #[test]
888    fn test_changelog_directory_prefix() {
889        let check = directory_changelog().required(true).build().unwrap();
890        let result = run_check(
891            "test_changelog_directory_prefix",
892            DIRECTORY_CHANGELOG_PREFIX,
893            check,
894        );
895        test_result_errors(
896            result,
897            &[
898                "commit 5f5442e33b6d0dfe01a14d98476d14e54c4d590e not allowed; missing a changelog \
899                 entry in a file in `changes`.",
900            ],
901        );
902    }
903
904    #[test]
905    fn test_changelog_directory_ok() {
906        let check = directory_changelog().required(true).build().unwrap();
907        run_check_ok(
908            "test_changelog_directory_ok",
909            DIRECTORY_CHANGELOG_ADD,
910            check,
911        );
912    }
913
914    #[test]
915    fn test_changelog_directory_ok_extension() {
916        let check = directory_changelog_ext().required(true).build().unwrap();
917        run_check_ok(
918            "test_changelog_directory_ok_extension",
919            DIRECTORY_CHANGELOG_ADD,
920            check,
921        );
922    }
923
924    #[test]
925    fn test_changelog_warning_file() {
926        let check = file_changelog().build().unwrap();
927        let result = run_check("test_changelog_warning_file", CHANGELOG_MISSING, check);
928        test_result_warnings(result, &[
929            "commit 66953d52f3ec6f6e4d731e7f2f70dc4000ab13ae is missing a changelog entry; please \
930             consider adding a changelog entry in the `changelog.md` file.",
931        ]);
932    }
933
934    #[test]
935    fn test_changelog_warning_files() {
936        let check = files_changelog().build().unwrap();
937        let result = run_check("test_changelog_warning_files", CHANGELOG_MISSING, check);
938        test_result_warnings(result, &[
939            "commit 66953d52f3ec6f6e4d731e7f2f70dc4000ab13ae is missing a changelog entry; please \
940             consider adding a changelog entry in one of the `changelog.md`, `other.md` files.",
941        ]);
942    }
943
944    #[test]
945    fn test_changelog_warning_directory() {
946        let check = directory_changelog().build().unwrap();
947        let result = run_check("test_changelog_warning_directory", CHANGELOG_MISSING, check);
948        test_result_warnings(result, &[
949            "commit 66953d52f3ec6f6e4d731e7f2f70dc4000ab13ae is missing a changelog entry; please \
950             consider adding a changelog entry in a file in `changes`.",
951        ]);
952    }
953
954    #[test]
955    fn test_changelog_topic_file() {
956        let check = file_changelog().required(true).build().unwrap();
957        let result = run_topic_check("test_changelog_topic_file", CHANGELOG_MISSING, check);
958        test_result_errors(
959            result,
960            &["missing a changelog entry in the `changelog.md` file."],
961        );
962    }
963
964    #[test]
965    fn test_changelog_topic_file_warning() {
966        let check = file_changelog().build().unwrap();
967        let result = run_topic_check(
968            "test_changelog_topic_file_warning",
969            CHANGELOG_MISSING,
970            check,
971        );
972        test_result_warnings(
973            result,
974            &["please consider adding a changelog entry in the `changelog.md` file."],
975        );
976    }
977
978    #[test]
979    fn test_changelog_topic_files() {
980        let check = files_changelog().required(true).build().unwrap();
981        let result = run_topic_check("test_changelog_topic_files", CHANGELOG_MISSING, check);
982        test_result_errors(
983            result,
984            &["missing a changelog entry in one of the `changelog.md`, `other.md` files."],
985        );
986    }
987
988    #[test]
989    fn test_changelog_topic_files_warning() {
990        let check = files_changelog().build().unwrap();
991        let result = run_topic_check(
992            "test_changelog_topic_files_warning",
993            CHANGELOG_MISSING,
994            check,
995        );
996        test_result_warnings(result, &[
997            "please consider adding a changelog entry in one of the `changelog.md`, `other.md` files.",
998        ]);
999    }
1000
1001    #[test]
1002    fn test_changelog_topic_directory() {
1003        let check = directory_changelog().required(true).build().unwrap();
1004        let result = run_topic_check("test_changelog_topic_directory", CHANGELOG_MISSING, check);
1005        test_result_errors(
1006            result,
1007            &["missing a changelog entry in a file in `changes`."],
1008        );
1009    }
1010
1011    #[test]
1012    fn test_changelog_topic_directory_warning() {
1013        let check = directory_changelog().build().unwrap();
1014        let result = run_topic_check(
1015            "test_changelog_topic_directory_warning",
1016            CHANGELOG_MISSING,
1017            check,
1018        );
1019        test_result_warnings(
1020            result,
1021            &["please consider adding a changelog entry in a file in `changes`."],
1022        );
1023    }
1024
1025    #[test]
1026    fn test_changelog_topic_directory_bad_ext() {
1027        let check = directory_changelog_ext().required(true).build().unwrap();
1028        let result = run_topic_check(
1029            "test_changelog_topic_directory_bad_ext",
1030            CHANGELOG_MISSING_FIXED,
1031            check,
1032        );
1033        test_result_errors(
1034            result,
1035            &["missing a changelog entry in a file ending with `.md` in `changes`."],
1036        );
1037    }
1038
1039    #[test]
1040    fn test_changelog_topic_directory_warning_bad_ext() {
1041        let check = directory_changelog_ext().build().unwrap();
1042        let result = run_topic_check(
1043            "test_changelog_topic_directory_warning_bad_ext",
1044            CHANGELOG_MISSING_FIXED,
1045            check,
1046        );
1047        test_result_warnings(result, &[
1048            "please consider adding a changelog entry in a file ending with `.md` in `changes`.",
1049        ]);
1050    }
1051
1052    #[test]
1053    fn test_changelog_topic_fixed_file() {
1054        let check = file_changelog().required(true).build().unwrap();
1055        run_topic_check_ok(
1056            "test_changelog_topic_fixed_file",
1057            CHANGELOG_MISSING_FIXED,
1058            check,
1059        );
1060    }
1061
1062    #[test]
1063    fn test_changelog_topic_fixed_files() {
1064        let check = files_changelog().required(true).build().unwrap();
1065        run_topic_check_ok(
1066            "test_changelog_topic_fixed_files",
1067            CHANGELOG_MISSING_FIXED,
1068            check,
1069        );
1070    }
1071
1072    #[test]
1073    fn test_changelog_topic_fixed_directory() {
1074        let check = directory_changelog().required(true).build().unwrap();
1075        run_topic_check_ok(
1076            "test_changelog_topic_fixed_directory",
1077            CHANGELOG_MISSING_FIXED,
1078            check,
1079        );
1080    }
1081
1082    #[test]
1083    fn test_changelog_topic_fixed_directory_bad_ext() {
1084        let check = directory_changelog().required(true).build().unwrap();
1085        run_topic_check_ok(
1086            "test_changelog_topic_fixed_directory_bad_ext",
1087            CHANGELOG_MISSING_FIXED_BAD_EXT,
1088            check,
1089        );
1090    }
1091}