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