Skip to content

Commit

Permalink
Add depchecks to doctor, also split checks into production and non …
Browse files Browse the repository at this point in the history
…production (#931)

* impl dep checks, add --production flag to doctor
  • Loading branch information
jondot authored Oct 30, 2024
1 parent 550dc6d commit 322b86a
Show file tree
Hide file tree
Showing 11 changed files with 355 additions and 18 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## Unreleased

* `loco doctor` now checks for app-specific minimum dependency versions. This should help in upgrades. `doctor` also supports "production only" checks which you can run in production with `loco doctor --production`. This, for example, will check your connections but will not check dependencies.
*

## v0.12.0

This release have been primarily about cleanups and simplification.
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ serde = { workspace = true }
serde_json = { workspace = true }
serde_yaml = "0.9"
serde_variant = "0.1.2"
toml = "0.8"


async-trait = { workspace = true }
Expand Down
18 changes: 18 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
## Blessed depdenencies maintenance and `loco doctor`

Loco contain a few major and "blessed" dependencies, these appear **both** in an app that was generated at the surface level in their `Cargo.toml` and in the core Loco framework.

If stale, may require an upgrade as a must.

Example for such dependencies:

* The `sea-orm-cli` - while Loco uses `SeaORM`, it uses the `SeaORM` CLI to generate entities, and so there may be an incompatibility if `SeaORM` has a too large breaking change between their CLI (which ships separately) and their framework.
* `axum`
* etc.

This is why we are checking these automatically as part of `loco doctor`.

We keep minimal version requirements for these. As a maintainer, you can update these **minimal** versions, only if required in [`doctor.rs`](src/doctor.rs).



## Running Tests

Before running tests make sure that:
Expand Down
7 changes: 7 additions & 0 deletions docs-site/content/docs/infrastructure/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,13 @@ auth:
```
<!-- </snip>-->
## Running `loco doctor`

You can run `loco doctor` in your server to check the connection health of your environment.

```sh
$ myapp doctor --production
```

## Generate

Expand Down
23 changes: 18 additions & 5 deletions examples/demo/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions examples/demo/tests/cmd/cli.trycmd
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ $ demo_app-cli doctor
✅ SeaORM CLI is installed
✅ DB connection: success
✅ redis queue: queue connection: success
✅ Dependencies


```

Expand Down
9 changes: 7 additions & 2 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ enum Commands {
/// print out the current configurations.
#[arg(short, long, action)]
config: bool,
#[arg(short, long, action)]
production: bool,
},
/// Display the app version
Version {},
Expand Down Expand Up @@ -540,13 +542,16 @@ pub async fn main<H: Hooks, M: MigratorTrait>() -> crate::Result<()> {
},
)?;
}
Commands::Doctor { config: config_arg } => {
Commands::Doctor {
config: config_arg,
production,
} => {
if config_arg {
println!("{}", &config);
println!("Environment: {}", &environment);
} else {
let mut should_exit = false;
for (_, check) in doctor::run_all(&config).await? {
for (_, check) in doctor::run_all(&config, production).await? {
if !should_exit && !check.valid() {
should_exit = true;
}
Expand Down
221 changes: 221 additions & 0 deletions src/depcheck.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
use std::collections::HashMap;

use semver::{Version, VersionReq};
use thiserror::Error;
use toml::Value;

#[derive(Debug, PartialEq, Eq, Ord, PartialOrd)]
pub enum VersionStatus {
NotFound,
Invalid {
version: String,
min_version: String,
},
Ok(String),
}

#[derive(Debug, PartialEq, Eq, Ord, PartialOrd)]
pub struct CrateStatus {
pub crate_name: String,
pub status: VersionStatus,
}

#[derive(Error, Debug)]
pub enum VersionCheckError {
#[error("Failed to parse Cargo.lock: {0}")]
ParseError(#[from] toml::de::Error),

#[error("Error with crate {crate_name}: {msg}")]
CrateError { crate_name: String, msg: String },
}

pub type Result<T> = std::result::Result<T, VersionCheckError>;

pub fn check_crate_versions(
cargo_lock_content: &str,
min_versions: HashMap<&str, &str>,
) -> Result<Vec<CrateStatus>> {
let lock_file: Value = cargo_lock_content.parse()?;

let packages = lock_file
.get("package")
.and_then(|v| v.as_array())
.ok_or_else(|| {
VersionCheckError::ParseError(serde::de::Error::custom(
"Missing package array in Cargo.lock",
))
})?;

let mut results = Vec::new();

for (crate_name, min_version) in min_versions {
let min_version_req =
VersionReq::parse(min_version).map_err(|_| VersionCheckError::CrateError {
crate_name: crate_name.to_string(),
msg: format!("Invalid minimum version format: {min_version}"),
})?;

let mut found = false;
for package in packages {
if let Some(name) = package.get("name").and_then(|v| v.as_str()) {
if name == crate_name {
found = true;
let version_str =
package
.get("version")
.and_then(|v| v.as_str())
.ok_or_else(|| VersionCheckError::CrateError {
crate_name: crate_name.to_string(),
msg: "Invalid version format in Cargo.lock".to_string(),
})?;

let version =
Version::parse(version_str).map_err(|_| VersionCheckError::CrateError {
crate_name: crate_name.to_string(),
msg: format!("Invalid version format in Cargo.lock: {version_str}"),
})?;

let status = if min_version_req.matches(&version) {
VersionStatus::Ok(version.to_string())
} else {
VersionStatus::Invalid {
version: version.to_string(),
min_version: min_version.to_string(),
}
};
results.push(CrateStatus {
crate_name: crate_name.to_string(),
status,
});
break;
}
}
}

if !found {
results.push(CrateStatus {
crate_name: crate_name.to_string(),
status: VersionStatus::NotFound,
});
}
}

Ok(results)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_multiple_crates_mixed_results() {
let cargo_lock_content = r#"
[[package]]
name = "serde"
version = "1.0.130"
[[package]]
name = "tokio"
version = "0.3.0"
[[package]]
name = "rand"
version = "0.8.4"
"#;

let mut min_versions = HashMap::new();
min_versions.insert("serde", "1.0.130");
min_versions.insert("tokio", "1.0");
min_versions.insert("rand", "0.8.0");

let mut result = check_crate_versions(cargo_lock_content, min_versions).unwrap();
result.sort();
assert_eq!(
result,
vec![
CrateStatus {
crate_name: "rand".to_string(),
status: VersionStatus::Ok("0.8.4".to_string())
},
CrateStatus {
crate_name: "serde".to_string(),
status: VersionStatus::Ok("1.0.130".to_string())
},
CrateStatus {
crate_name: "tokio".to_string(),
status: VersionStatus::Invalid {
version: "0.3.0".to_string(),
min_version: "1.0".to_string()
}
}
]
);
}

#[test]
fn test_invalid_version_format_in_cargo_lock() {
let cargo_lock_content = r#"
[[package]]
name = "serde"
version = "1.0.x"
"#;

let mut min_versions = HashMap::new();
min_versions.insert("serde", "1.0.0");

let result = check_crate_versions(cargo_lock_content, min_versions);
assert!(matches!(
result,
Err(VersionCheckError::CrateError { crate_name, msg }) if crate_name == "serde" && msg.contains("Invalid version format")
));
}

#[test]
fn test_no_package_section_in_cargo_lock() {
let cargo_lock_content = r"
# No packages listed in this Cargo.lock
";

let mut min_versions = HashMap::new();
min_versions.insert("serde", "1.0.130");

let result = check_crate_versions(cargo_lock_content, min_versions);
assert!(matches!(result, Err(VersionCheckError::ParseError(_))));
}

#[test]
fn test_exact_version_match_for_minimum_requirement() {
let cargo_lock_content = r#"
[[package]]
name = "serde"
version = "1.0.130"
"#;

let mut min_versions = HashMap::new();
min_versions.insert("serde", "1.0.130");

let mut result = check_crate_versions(cargo_lock_content, min_versions).unwrap();
result.sort();
assert_eq!(
result,
vec![CrateStatus {
crate_name: "serde".to_string(),
status: VersionStatus::Ok("1.0.130".to_string()),
}]
);
}

#[test]
fn test_no_crates_in_min_versions_map() {
let cargo_lock_content = r#"
[[package]]
name = "serde"
version = "1.0.130"
"#;

let min_versions = HashMap::new(); // Empty map

let result = check_crate_versions(cargo_lock_content, min_versions).unwrap();
assert!(result.is_empty());
}
}
Loading

0 comments on commit 322b86a

Please sign in to comment.