1use std::fmt;
10use std::io::{self, Read};
11use std::iter;
12use std::os::unix::process::ExitStatusExt;
13use std::path::PathBuf;
14use std::process::{Command, Stdio};
15use std::time::Duration;
16
17use derive_builder::Builder;
18use git_checks_core::impl_prelude::*;
19use git_checks_core::AttributeError;
20use git_workarea::{GitContext, GitWorkArea};
21use itertools::Itertools;
22use log::{info, warn};
23use rayon::prelude::*;
24use thiserror::Error;
25use wait_timeout::ChildExt;
26
27#[derive(Debug, Clone, Copy)]
28enum FormattingExecStage {
29 Run,
30 Wait,
31 Kill,
32 TimeoutWait,
33}
34
35impl fmt::Display for FormattingExecStage {
36 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
37 let what = match self {
38 FormattingExecStage::Run => "execute",
39 FormattingExecStage::Wait => "wait on",
40 FormattingExecStage::Kill => "kill (timed out)",
41 FormattingExecStage::TimeoutWait => "wait on (timed out)",
42 };
43
44 write!(f, "{}", what)
45 }
46}
47
48#[derive(Debug, Clone, Copy)]
49enum ListFilesReason {
50 Modified,
51 Untracked,
52}
53
54impl fmt::Display for ListFilesReason {
55 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
56 let what = match self {
57 ListFilesReason::Modified => "modified",
58 ListFilesReason::Untracked => "untracked",
59 };
60
61 write!(f, "{}", what)
62 }
63}
64
65#[derive(Debug, Error)]
66enum FormattingError {
67 #[error("failed to {} the {} formatter: {}", stage, command.display(), source)]
68 ExecFormatter {
69 command: PathBuf,
70 stage: FormattingExecStage,
71 #[source]
72 source: io::Error,
73 },
74 #[error("failed to collect stderr from the {} formatter: {}", command.display(), source)]
75 CollectStderr {
76 command: PathBuf,
77 #[source]
78 source: io::Error,
79 },
80 #[error("failed to list {} file in the work area: {}", reason, output)]
81 ListFiles {
82 reason: ListFilesReason,
83 output: String,
84 },
85 #[error("attribute extraction error: {}", source)]
86 Attribute {
87 #[from]
88 source: AttributeError,
89 },
90}
91
92impl FormattingError {
93 fn exec_formatter(command: PathBuf, stage: FormattingExecStage, source: io::Error) -> Self {
94 FormattingError::ExecFormatter {
95 command,
96 stage,
97 source,
98 }
99 }
100
101 fn collect_stderr(command: PathBuf, source: io::Error) -> Self {
102 FormattingError::CollectStderr {
103 command,
104 source,
105 }
106 }
107
108 fn list_files(reason: ListFilesReason, output: &[u8]) -> Self {
109 FormattingError::ListFiles {
110 reason,
111 output: String::from_utf8_lossy(output).into(),
112 }
113 }
114}
115
116#[derive(Builder, Debug, Clone)]
132#[builder(field(private))]
133pub struct Formatting {
134 #[builder(setter(into, strip_option), default)]
141 name: Option<String>,
142 #[builder(setter(into))]
148 kind: String,
149 #[builder(setter(into))]
155 formatter: PathBuf,
156 #[builder(private)]
157 #[builder(setter(name = "_config_files"))]
158 #[builder(default)]
159 config_files: Vec<String>,
160 #[builder(setter(into, strip_option), default)]
168 fix_message: Option<String>,
169 #[builder(setter(into, strip_option), default)]
176 timeout: Option<Duration>,
177}
178
179const MAX_EXPLICIT_FILE_LIST: usize = 5;
182const ZOMBIE_TIMEOUT: Duration = Duration::from_secs(1);
185
186impl FormattingBuilder {
187 pub fn config_files<I, F>(&mut self, files: I) -> &mut Self
192 where
193 I: IntoIterator<Item = F>,
194 F: Into<String>,
195 {
196 self.config_files = Some(files.into_iter().map(Into::into).collect());
197 self
198 }
199}
200
201impl Formatting {
202 pub fn builder() -> FormattingBuilder {
204 Default::default()
205 }
206
207 fn check_path<'a>(
209 &self,
210 ctx: &GitWorkArea,
211 path: &'a FileName,
212 attr_value: Option<String>,
213 ) -> Result<Option<&'a FileName>, FormattingError> {
214 let mut cmd = Command::new(&self.formatter);
215 ctx.cd_to_work_tree(&mut cmd);
216 cmd.arg(path.as_path());
217 if let Some(attr_value) = attr_value {
218 cmd.arg(attr_value);
219 }
220
221 let (success, output) = if let Some(timeout) = self.timeout {
222 let mut child = cmd
223 .stdin(Stdio::null())
225 .stdout(Stdio::null())
227 .stderr(Stdio::piped())
231 .spawn()
232 .map_err(|err| {
233 FormattingError::exec_formatter(
234 self.formatter.clone(),
235 FormattingExecStage::Run,
236 err,
237 )
238 })?;
239 let check = child.wait_timeout(timeout).map_err(|err| {
240 FormattingError::exec_formatter(
241 self.formatter.clone(),
242 FormattingExecStage::Wait,
243 err,
244 )
245 })?;
246
247 if let Some(status) = check {
248 let stderr = child.stderr.expect("spawned with stderr");
249 let bytes_output = stderr
250 .bytes()
251 .collect::<Result<Vec<u8>, _>>()
252 .map_err(|err| FormattingError::collect_stderr(self.formatter.clone(), err))?;
253 (
254 status.success(),
255 format!(
256 "failed with exit code {:?}, signal {:?}, output: {:?}",
257 status.code(),
258 status.signal(),
259 String::from_utf8_lossy(&bytes_output),
260 ),
261 )
262 } else {
263 child.kill().map_err(|err| {
264 FormattingError::exec_formatter(
265 self.formatter.clone(),
266 FormattingExecStage::Kill,
267 err,
268 )
269 })?;
270 let timed_out_status = child.wait_timeout(ZOMBIE_TIMEOUT).map_err(|err| {
271 FormattingError::exec_formatter(
272 self.formatter.clone(),
273 FormattingExecStage::TimeoutWait,
274 err,
275 )
276 })?;
277 if timed_out_status.is_none() {
278 warn!(
279 target: "git-checks/formatting",
280 "leaving a zombie '{}' process; it did not respond to kill",
281 self.kind,
282 );
283 }
284 (false, "timeout reached".into())
285 }
286 } else {
287 let check = cmd.output().map_err(|err| {
288 FormattingError::exec_formatter(
289 self.formatter.clone(),
290 FormattingExecStage::Run,
291 err,
292 )
293 })?;
294 (
295 check.status.success(),
296 String::from_utf8_lossy(&check.stderr).into_owned(),
297 )
298 };
299
300 Ok(if success {
301 None
302 } else {
303 info!(
304 target: "git-checks/formatting",
305 "failed to run the {} formatting command: {}",
306 self.kind,
307 output,
308 );
309 Some(path)
310 })
311 }
312
313 #[allow(clippy::needless_collect)]
315 fn message_for_paths<P>(
316 &self,
317 results: &mut CheckResult,
318 content: &dyn Content,
319 paths: Vec<P>,
320 description: &str,
321 ) where
322 P: fmt::Display,
323 {
324 if !paths.is_empty() {
325 let mut all_paths = paths.into_iter();
326 let explicit_paths = all_paths
328 .by_ref()
329 .take(MAX_EXPLICIT_FILE_LIST)
330 .map(|path| format!("`{}`", path))
331 .collect::<Vec<_>>();
332 let next_path = all_paths.next();
334 let tail_paths = if let Some(next_path) = next_path {
335 let remaining_paths = all_paths.count();
336 if remaining_paths == 0 {
337 iter::once(format!("`{}`", next_path)).collect::<Vec<_>>()
339 } else {
340 iter::once(format!("and {} others", remaining_paths + 1)).collect::<Vec<_>>()
341 }
342 } else {
343 iter::empty().collect::<Vec<_>>()
344 }
345 .into_iter();
346 let paths = explicit_paths.into_iter().chain(tail_paths).join(", ");
347 let fix = self
348 .fix_message
349 .as_ref()
350 .map_or_else(String::new, |fix_message| format!(" {}", fix_message));
351 results.add_error(format!(
352 "{}the following files {} the '{}' check: {}.{}",
353 commit_prefix_str(content, "is not allowed because"),
354 description,
355 self.name.as_ref().unwrap_or(&self.kind),
356 paths,
357 fix,
358 ));
359 }
360 }
361}
362
363impl ContentCheck for Formatting {
364 fn name(&self) -> &str {
365 "formatting"
366 }
367
368 fn check(
369 &self,
370 ctx: &CheckGitContext,
371 content: &dyn Content,
372 ) -> Result<CheckResult, Box<dyn Error>> {
373 let changed_paths = content.modified_files();
374
375 let gitctx = GitContext::new(ctx.gitdir());
376 let mut workarea = content.workarea(&gitctx)?;
377
378 let files_to_checkout = changed_paths
380 .iter()
381 .map(|path| path.as_path())
382 .chain(self.config_files.iter().map(AsRef::as_ref))
383 .collect::<Vec<_>>();
384 workarea.checkout(&files_to_checkout)?;
385
386 let attr = format!("format.{}", self.kind);
387 let failed_paths = changed_paths
388 .par_iter()
389 .map(|path| {
390 match ctx.check_attr(&attr, path.as_path())? {
391 AttributeState::Set => self.check_path(&workarea, path, None),
392 AttributeState::Value(v) => self.check_path(&workarea, path, Some(v)),
393 _ => Ok(None),
394 }
395 })
396 .collect::<Vec<Result<_, _>>>()
397 .into_iter()
398 .collect::<Result<Vec<_>, _>>()?
399 .into_iter()
400 .flatten()
401 .collect::<Vec<_>>();
402
403 let ls_files_m = workarea
404 .git()
405 .arg("ls-files")
406 .arg("-m")
407 .output()
408 .map_err(|err| GitError::subcommand("ls-files -m", err))?;
409 if !ls_files_m.status.success() {
410 return Err(
411 FormattingError::list_files(ListFilesReason::Modified, &ls_files_m.stderr).into(),
412 );
413 }
414 let modified_paths = String::from_utf8_lossy(&ls_files_m.stdout);
415
416 let modified_paths_in_commit = modified_paths
420 .lines()
421 .filter(|&path| {
422 changed_paths
423 .iter()
424 .any(|diff_path| diff_path.as_str() == path)
425 })
426 .collect();
427
428 let ls_files_o = workarea
429 .git()
430 .arg("ls-files")
431 .arg("-o")
432 .output()
433 .map_err(|err| GitError::subcommand("ls-files -o", err))?;
434 if !ls_files_o.status.success() {
435 return Err(FormattingError::list_files(
436 ListFilesReason::Untracked,
437 &ls_files_o.stderr,
438 )
439 .into());
440 }
441 let untracked_paths = String::from_utf8_lossy(&ls_files_o.stdout);
442
443 let mut results = CheckResult::new();
444
445 self.message_for_paths(
446 &mut results,
447 content,
448 failed_paths,
449 "could not be formatted by",
450 );
451 self.message_for_paths(
452 &mut results,
453 content,
454 modified_paths_in_commit,
455 "are not formatted according to",
456 );
457 self.message_for_paths(
458 &mut results,
459 content,
460 untracked_paths.lines().collect(),
461 "were created by",
462 );
463
464 Ok(results)
465 }
466}
467
468#[cfg(feature = "config")]
469pub(crate) mod config {
470 #[cfg(test)]
471 use std::path::Path;
472 use std::time::Duration;
473
474 use git_checks_config::{register_checks, CommitCheckConfig, IntoCheck, TopicCheckConfig};
475 use serde::Deserialize;
476 #[cfg(test)]
477 use serde_json::json;
478
479 #[cfg(test)]
480 use crate::test;
481 use crate::Formatting;
482
483 #[derive(Deserialize, Debug)]
515 pub struct FormattingConfig {
516 #[serde(default)]
517 name: Option<String>,
518 kind: String,
519 formatter: String,
520 #[serde(default)]
521 config_files: Option<Vec<String>>,
522 #[serde(default)]
523 fix_message: Option<String>,
524 #[serde(default)]
525 timeout: Option<u64>,
526 }
527
528 impl IntoCheck for FormattingConfig {
529 type Check = Formatting;
530
531 fn into_check(self) -> Self::Check {
532 let mut builder = Formatting::builder();
533
534 builder.kind(self.kind).formatter(self.formatter);
535
536 if let Some(name) = self.name {
537 builder.name(name);
538 }
539
540 if let Some(config_files) = self.config_files {
541 builder.config_files(config_files);
542 }
543
544 if let Some(fix_message) = self.fix_message {
545 builder.fix_message(fix_message);
546 }
547
548 if let Some(timeout) = self.timeout {
549 builder.timeout(Duration::from_secs(timeout));
550 }
551
552 builder
553 .build()
554 .expect("configuration mismatch for `Formatting`")
555 }
556 }
557
558 register_checks! {
559 FormattingConfig {
560 "formatting" => CommitCheckConfig,
561 "formatting/topic" => TopicCheckConfig,
562 },
563 }
564
565 #[test]
566 fn test_formatting_config_empty() {
567 let json = json!({});
568 let err = serde_json::from_value::<FormattingConfig>(json).unwrap_err();
569 test::check_missing_json_field(err, "kind");
570 }
571
572 #[test]
573 fn test_formatting_config_kind_is_required() {
574 let exp_formatter = "/path/to/formatter";
575 let json = json!({
576 "formatter": exp_formatter,
577 });
578 let err = serde_json::from_value::<FormattingConfig>(json).unwrap_err();
579 test::check_missing_json_field(err, "kind");
580 }
581
582 #[test]
583 fn test_formatting_config_formatter_is_required() {
584 let exp_kind = "kind";
585 let json = json!({
586 "kind": exp_kind,
587 });
588 let err = serde_json::from_value::<FormattingConfig>(json).unwrap_err();
589 test::check_missing_json_field(err, "formatter");
590 }
591
592 #[test]
593 fn test_formatting_config_minimum_fields() {
594 let exp_kind = "kind";
595 let exp_formatter = "/path/to/formatter";
596 let json = json!({
597 "kind": exp_kind,
598 "formatter": exp_formatter,
599 });
600 let check: FormattingConfig = serde_json::from_value(json).unwrap();
601
602 assert_eq!(check.name, None);
603 assert_eq!(check.kind, exp_kind);
604 assert_eq!(check.formatter, exp_formatter);
605 assert_eq!(check.config_files, None);
606 assert_eq!(check.fix_message, None);
607 assert_eq!(check.timeout, None);
608
609 let check = check.into_check();
610
611 assert_eq!(check.name, None);
612 assert_eq!(check.kind, exp_kind);
613 assert_eq!(check.formatter, Path::new(exp_formatter));
614 itertools::assert_equal(&check.config_files, &[] as &[&str]);
615 assert_eq!(check.fix_message, None);
616 assert_eq!(check.timeout, None);
617 }
618
619 #[test]
620 fn test_formatting_config_all_fields() {
621 let exp_name: String = "formatter name".into();
622 let exp_kind = "kind";
623 let exp_formatter = "/path/to/formatter";
624 let exp_config: String = "path/to/config/file".into();
625 let exp_fix_message: String = "instructions for fixing".into();
626 let exp_timeout = 10;
627 let json = json!({
628 "name": exp_name,
629 "kind": exp_kind,
630 "formatter": exp_formatter,
631 "config_files": [exp_config],
632 "fix_message": exp_fix_message,
633 "timeout": exp_timeout,
634 });
635 let check: FormattingConfig = serde_json::from_value(json).unwrap();
636
637 assert_eq!(check.name, Some(exp_name.clone()));
638 assert_eq!(check.kind, exp_kind);
639 assert_eq!(check.formatter, exp_formatter);
640 itertools::assert_equal(check.config_files.as_ref().unwrap(), &[exp_config.clone()]);
641 assert_eq!(check.fix_message, Some(exp_fix_message.clone()));
642 assert_eq!(check.timeout, Some(exp_timeout));
643
644 let check = check.into_check();
645
646 assert_eq!(check.name, Some(exp_name));
647 assert_eq!(check.kind, exp_kind);
648 assert_eq!(check.formatter, Path::new(exp_formatter));
649 itertools::assert_equal(&check.config_files, &[exp_config]);
650 assert_eq!(check.fix_message, Some(exp_fix_message));
651 assert_eq!(check.timeout, Some(Duration::from_secs(exp_timeout)));
652 }
653}
654
655#[cfg(test)]
656mod tests {
657 use std::time::Duration;
658
659 use git_checks_core::{Check, TopicCheck};
660
661 use crate::builders::FormattingBuilder;
662 use crate::test::*;
663 use crate::Formatting;
664
665 const MISSING_CONFIG_COMMIT: &str = "220efbb4d0380fe932b70444fe15e787506080b0";
666 const ADD_CONFIG_COMMIT: &str = "e08e9ac1c5b6a0a67e0b2715cb0dbf99935d9cbf";
667 const BAD_FORMAT_COMMIT: &str = "e9a08d956553f94e9c8a0a02b11ca60f62de3c2b";
668 const FIX_BAD_FORMAT_COMMIT: &str = "7fe590bdb883e195812cae7602ce9115cbd269ee";
669 const OK_FORMAT_COMMIT: &str = "b77d2a5d63cd6afa599d0896dafff95f1ace50b6";
670 const IGNORE_UNTRACKED_COMMIT: &str = "c0154d1087906d50c5551ff8f60e544e9a492a48";
671 const DELETE_FORMAT_COMMIT: &str = "31446c81184df35498814d6aa3c7f933dddf91c2";
672 const MANY_BAD_FORMAT_COMMIT: &str = "f0d10d9385ef697175c48fa72324c33d5e973f4b";
673 const MANY_MORE_BAD_FORMAT_COMMIT: &str = "0e80ff6dd2495571b7d255e39fb3e7cc9f487fb7";
674 const TIMEOUT_CONFIG_COMMIT: &str = "62f5eac20c5021cf323c757a4d24234c81c9c7ad";
675 const WITH_ARG_COMMIT: &str = "dfa4c021e96f1e94634ad1787f31c2abdbeaff99";
676 const NOEXEC_CONFIG_COMMIT: &str = "1b5be48f8ce45b8fec155a1787cabb6995194ce5";
677
678 #[test]
679 fn test_exec_stage_display() {
680 assert_eq!(format!("{}", super::FormattingExecStage::Run), "execute");
681 assert_eq!(format!("{}", super::FormattingExecStage::Wait), "wait on");
682 assert_eq!(
683 format!("{}", super::FormattingExecStage::Kill),
684 "kill (timed out)"
685 );
686 assert_eq!(
687 format!("{}", super::FormattingExecStage::TimeoutWait),
688 "wait on (timed out)"
689 );
690 }
691
692 #[test]
693 fn test_list_files_reason_display() {
694 assert_eq!(format!("{}", super::ListFilesReason::Modified), "modified");
695 assert_eq!(
696 format!("{}", super::ListFilesReason::Untracked),
697 "untracked"
698 );
699 }
700
701 #[test]
702 fn test_formatting_builder_default() {
703 assert!(Formatting::builder().build().is_err());
704 }
705
706 #[test]
707 fn test_formatting_builder_kind_is_required() {
708 assert!(Formatting::builder()
709 .formatter("path/to/formatter")
710 .build()
711 .is_err());
712 }
713
714 #[test]
715 fn test_formatting_builder_formatter_is_required() {
716 assert!(Formatting::builder().kind("kind").build().is_err());
717 }
718
719 #[test]
720 fn test_formatting_builder_minimum_fields() {
721 assert!(Formatting::builder()
722 .formatter("path/to/formatter")
723 .kind("kind")
724 .build()
725 .is_ok());
726 }
727
728 #[test]
729 fn test_formatting_name_commit() {
730 let check = Formatting::builder()
731 .formatter("path/to/formatter")
732 .kind("kind")
733 .build()
734 .unwrap();
735 assert_eq!(Check::name(&check), "formatting");
736 }
737
738 #[test]
739 fn test_formatting_name_topic() {
740 let check = Formatting::builder()
741 .formatter("path/to/formatter")
742 .kind("kind")
743 .build()
744 .unwrap();
745 assert_eq!(TopicCheck::name(&check), "formatting");
746 }
747
748 fn formatting_check(kind: &str) -> FormattingBuilder {
749 let formatter = format!("{}/test/format.{}", env!("CARGO_MANIFEST_DIR"), kind);
750 let mut builder = Formatting::builder();
751 builder
752 .kind(kind)
753 .formatter(formatter)
754 .config_files(["format-config"].iter().cloned());
755 builder
756 }
757
758 #[test]
759 fn test_formatting_pass() {
760 let check = formatting_check("simple").build().unwrap();
761 let conf = make_check_conf(&check);
762
763 let result = test_check_base(
764 "test_formatting_pass",
765 OK_FORMAT_COMMIT,
766 BAD_FORMAT_COMMIT,
767 &conf,
768 );
769 test_result_ok(result);
770 }
771
772 #[test]
773 fn test_formatting_pass_with_arg() {
774 let check = formatting_check("with_arg").build().unwrap();
775 let conf = make_check_conf(&check);
776
777 let result = test_check("test_formatting_pass_with_arg", WITH_ARG_COMMIT, &conf);
778 test_result_errors(result, &[
779 "commit dfa4c021e96f1e94634ad1787f31c2abdbeaff99 is not allowed because the following \
780 files are not formatted according to the 'with_arg' check: `with-arg.txt`.",
781 ]);
782 }
783
784 #[test]
785 fn test_formatting_formatter_fail() {
786 let check = formatting_check("simple").build().unwrap();
787 let result = run_check(
788 "test_formatting_formatter_fail",
789 MISSING_CONFIG_COMMIT,
790 check,
791 );
792 test_result_errors(result, &[
793 "commit 220efbb4d0380fe932b70444fe15e787506080b0 is not allowed because the following \
794 files could not be formatted by the 'simple' check: `empty.txt`.",
795 ]);
796 }
797
798 #[test]
799 fn test_formatting_formatter_fail_named() {
800 let check = formatting_check("simple").name("renamed").build().unwrap();
801 let result = run_check(
802 "test_formatting_formatter_fail_named",
803 MISSING_CONFIG_COMMIT,
804 check,
805 );
806 test_result_errors(result, &[
807 "commit 220efbb4d0380fe932b70444fe15e787506080b0 is not allowed because the following \
808 files could not be formatted by the 'renamed' check: `empty.txt`.",
809 ]);
810 }
811
812 #[test]
813 fn test_formatting_formatter_fail_fix_message() {
814 let check = formatting_check("simple")
815 .fix_message("These may be fixed by magic.")
816 .build()
817 .unwrap();
818 let result = run_check(
819 "test_formatting_formatter_fail_fix_message",
820 MISSING_CONFIG_COMMIT,
821 check,
822 );
823 test_result_errors(result, &[
824 "commit 220efbb4d0380fe932b70444fe15e787506080b0 is not allowed because the following \
825 files could not be formatted by the 'simple' check: `empty.txt`. These may be fixed \
826 by magic.",
827 ]);
828 }
829
830 #[test]
831 fn test_formatting_formatter_untracked_files() {
832 let check = formatting_check("untracked").build().unwrap();
833 let result = run_check(
834 "test_formatting_formatter_untracked_files",
835 MISSING_CONFIG_COMMIT,
836 check,
837 );
838 test_result_errors(result, &[
839 "commit 220efbb4d0380fe932b70444fe15e787506080b0 is not allowed because the following \
840 files were created by the 'untracked' check: `untracked`.",
841 ]);
842 }
843
844 #[test]
845 fn test_formatting_formatter_timeout() {
846 let check = formatting_check("timeout")
847 .timeout(Duration::from_secs(1))
848 .build()
849 .unwrap();
850 let result = run_check(
851 "test_formatting_formatter_timeout",
852 TIMEOUT_CONFIG_COMMIT,
853 check,
854 );
855 test_result_errors(result, &[
856 "commit 62f5eac20c5021cf323c757a4d24234c81c9c7ad is not allowed because the following \
857 files could not be formatted by the 'timeout' check: `empty.txt`.",
858 ]);
859 }
860
861 #[test]
862 fn test_formatting_formatter_untracked_files_ignored() {
863 let check = formatting_check("untracked").build().unwrap();
864 let conf = make_check_conf(&check);
865
866 let result = test_check_base(
867 "test_formatting_formatter_untracked_files_ignored",
868 IGNORE_UNTRACKED_COMMIT,
869 OK_FORMAT_COMMIT,
870 &conf,
871 );
872 test_result_ok(result);
873 }
874
875 #[test]
876 fn test_formatting_formatter_modified_files() {
877 let check = formatting_check("simple").build().unwrap();
878 let conf = make_check_conf(&check);
879
880 let result = test_check_base(
881 "test_formatting_formatter_modified_files",
882 BAD_FORMAT_COMMIT,
883 ADD_CONFIG_COMMIT,
884 &conf,
885 );
886 test_result_errors(result, &[
887 "commit e9a08d956553f94e9c8a0a02b11ca60f62de3c2b is not allowed because the following \
888 files are not formatted according to the 'simple' check: `bad.txt`.",
889 ]);
890 }
891
892 #[test]
893 fn test_formatting_formatter_modified_files_topic() {
894 let check = formatting_check("simple").build().unwrap();
895 let conf = make_topic_check_conf(&check);
896
897 let result = test_check_base(
898 "test_formatting_formatter_modified_files_topic",
899 BAD_FORMAT_COMMIT,
900 ADD_CONFIG_COMMIT,
901 &conf,
902 );
903 test_result_errors(
904 result,
905 &["the following files are not formatted according to the 'simple' check: `bad.txt`."],
906 );
907 }
908
909 #[test]
910 fn test_formatting_formatter_modified_files_topic_fixed() {
911 let check = formatting_check("simple").build().unwrap();
912 run_topic_check_ok(
913 "test_formatting_formatter_modified_files_topic_fixed",
914 FIX_BAD_FORMAT_COMMIT,
915 check,
916 );
917 }
918
919 #[test]
920 fn test_formatting_formatter_many_modified_files() {
921 let check = formatting_check("simple").build().unwrap();
922 let conf = make_check_conf(&check);
923
924 let result = test_check_base(
925 "test_formatting_formatter_many_modified_files",
926 MANY_BAD_FORMAT_COMMIT,
927 ADD_CONFIG_COMMIT,
928 &conf,
929 );
930 test_result_errors(result, &[
931 "commit f0d10d9385ef697175c48fa72324c33d5e973f4b is not allowed because the following \
932 files are not formatted according to the 'simple' check: `1.bad.txt`, `2.bad.txt`, \
933 `3.bad.txt`, `4.bad.txt`, `5.bad.txt`, `6.bad.txt`.",
934 ]);
935 }
936
937 #[test]
938 fn test_formatting_formatter_many_more_modified_files() {
939 let check = formatting_check("simple").build().unwrap();
940 let conf = make_check_conf(&check);
941
942 let result = test_check_base(
943 "test_formatting_formatter_many_more_modified_files",
944 MANY_MORE_BAD_FORMAT_COMMIT,
945 ADD_CONFIG_COMMIT,
946 &conf,
947 );
948 test_result_errors(result, &[
949 "commit 0e80ff6dd2495571b7d255e39fb3e7cc9f487fb7 is not allowed because the following \
950 files are not formatted according to the 'simple' check: `1.bad.txt`, `2.bad.txt`, \
951 `3.bad.txt`, `4.bad.txt`, `5.bad.txt`, and 2 others.",
952 ]);
953 }
954
955 #[test]
956 fn test_formatting_script_deleted_files() {
957 let check = formatting_check("delete").build().unwrap();
958 let result = run_check(
959 "test_formatting_script_deleted_files",
960 DELETE_FORMAT_COMMIT,
961 check,
962 );
963 test_result_errors(result, &[
964 "commit 31446c81184df35498814d6aa3c7f933dddf91c2 is not allowed because the following \
965 files are not formatted according to the 'delete' check: `remove.txt`.",
966 ]);
967 }
968
969 #[test]
970 fn test_formatting_formatter_noexec() {
971 let check = formatting_check("noexec").build().unwrap();
972 let result = run_check(
973 "test_formatting_formatter_noexec",
974 NOEXEC_CONFIG_COMMIT,
975 check,
976 );
977
978 assert_eq!(result.warnings().len(), 0);
979 assert_eq!(result.alerts().len(), 1);
980 assert_eq!(
981 result.alerts()[0],
982 "failed to run the formatting check on commit 1b5be48f8ce45b8fec155a1787cabb6995194ce5",
983 );
984 assert_eq!(result.errors().len(), 0);
985 assert!(!result.temporary());
986 assert!(!result.allowed());
987 assert!(!result.pass());
988 }
989}