ferro_cli/doctor/checks/
migrations.rs1use crate::doctor::check::{CheckResult, DoctorCheck};
7use crate::doctor::checks::run_cargo_subcommand;
8use std::path::Path;
9
10pub struct MigrationsCheck;
11
12const NAME: &str = "migrations_pending";
13
14impl DoctorCheck for MigrationsCheck {
15 fn name(&self) -> &'static str {
16 NAME
17 }
18 fn run(&self, root: &Path) -> CheckResult {
19 check_impl(root)
20 }
21}
22
23pub(crate) fn check_impl(root: &Path) -> CheckResult {
24 if !root.join("src/migrations").exists() {
25 return CheckResult::warn(NAME, "no migrations directory");
26 }
27
28 let output = match run_cargo_subcommand(root, &["db:status"]) {
29 Ok(o) => o,
30 Err(e) => return CheckResult::error(NAME, format!("could not invoke cargo: {e}")),
31 };
32
33 if !output.status.success() {
34 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
35 return CheckResult::error(NAME, "db:status failed").with_details(stderr);
36 }
37
38 let stdout = String::from_utf8_lossy(&output.stdout);
39 let pending = count_lines_containing(&stdout, "pending");
40 let applied = count_lines_containing(&stdout, "applied");
41
42 if pending == 0 {
43 CheckResult::ok(NAME, format!("{applied} applied, 0 pending"))
44 } else {
45 CheckResult::warn(NAME, format!("{applied} applied, {pending} pending"))
46 }
47}
48
49fn count_lines_containing(text: &str, needle: &str) -> usize {
50 text.lines()
51 .filter(|l| l.to_ascii_lowercase().contains(needle))
52 .count()
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58
59 #[test]
60 fn name_is_migrations() {
61 assert_eq!(MigrationsCheck.name(), "migrations_pending");
62 }
63
64 #[test]
65 fn count_lines_containing_is_case_insensitive() {
66 let text = "Pending: m1\nApplied: m0\nPending: m2";
67 assert_eq!(count_lines_containing(text, "pending"), 2);
68 assert_eq!(count_lines_containing(text, "applied"), 1);
69 }
70}