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