Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 18 additions & 11 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
use anyhow::{Context, Result};

#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Config {
Expand Down Expand Up @@ -47,25 +47,32 @@ fn default_true() -> bool {

impl Config {
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
let content = fs::read_to_string(path.as_ref())
.context("Failed to read config file")?;

let config: Config = serde_yaml::from_str(&content)
.context("Failed to parse config file")?;

let content = fs::read_to_string(path.as_ref()).context("Failed to read config file")?;

let config: Config =
serde_yaml::from_str(&content).context("Failed to parse config file")?;

Ok(config)
}

pub fn find_config() -> Result<Self> {
let config_names = [".envcheck.yaml", ".envcheck.yml", "envcheck.yaml", "envcheck.yml"];

let config_names = [
".envcheck.yaml",
".envcheck.yml",
"envcheck.yaml",
"envcheck.yml",
];

for name in &config_names {
if Path::new(name).exists() {
return Self::load(name);
}
}

anyhow::bail!("No config file found. Looking for: {}", config_names.join(", "))

anyhow::bail!(
"No config file found. Looking for: {}",
config_names.join(", ")
)
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
pub mod config;
pub mod validators;
pub mod reporter;
pub mod validators;

pub use config::Config;
pub use validators::{ValidationResult, ValidationStatus, Validator};
pub use reporter::Reporter;
pub use validators::{ValidationResult, ValidationStatus, Validator};
4 changes: 2 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use clap::Parser;
use anyhow::Result;
use clap::Parser;
use std::process;

mod config;
mod validators;
mod reporter;
mod validators;

use config::Config;
use reporter::Reporter;
Expand Down
8 changes: 5 additions & 3 deletions src/reporter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ impl Reporter {
}

println!();

if error_count > 0 {
println!(
"{} {} issue(s) found. Fix them to continue.",
Expand All @@ -56,12 +56,14 @@ impl Reporter {
} else {
println!("{} All checks passed!", "✓".green().bold());
}

println!();
}

pub fn has_errors(&self) -> bool {
self.results.iter().any(|r| matches!(r.status, ValidationStatus::Error))
self.results
.iter()
.any(|r| matches!(r.status, ValidationStatus::Error))
}

pub fn exit_code(&self) -> i32 {
Expand Down
19 changes: 12 additions & 7 deletions src/validators/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,24 @@ impl Validator for EnvValidator {
if let Some(pattern) = &self.check.pattern {
// TODO: Add regex pattern matching
if value.contains(pattern) {
results.push(ValidationResult::success(
format!("{} is set and matches pattern", self.check.name),
));
results.push(ValidationResult::success(format!(
"{} is set and matches pattern",
self.check.name
)));
} else {
results.push(ValidationResult::error(
format!("{} is set but does not match pattern", self.check.name),
Some(format!("Ensure {} matches pattern: {}", self.check.name, pattern)),
Some(format!(
"Ensure {} matches pattern: {}",
self.check.name, pattern
)),
));
}
} else {
results.push(ValidationResult::success(
format!("{} is set", self.check.name),
));
results.push(ValidationResult::success(format!(
"{} is set",
self.check.name
)));
}
}
Err(_) => {
Expand Down
25 changes: 12 additions & 13 deletions src/validators/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,20 @@ impl Validator for FileValidator {
let path = Path::new(&self.check.path);

if path.exists() {
results.push(ValidationResult::success(
format!("{} exists", self.check.path),
results.push(ValidationResult::success(format!(
"{} exists",
self.check.path
)));
} else if self.check.required {
results.push(ValidationResult::error(
format!("{} does not exist", self.check.path),
Some(format!("Create {} file", self.check.path)),
));
} else {
if self.check.required {
results.push(ValidationResult::error(
format!("{} does not exist", self.check.path),
Some(format!("Create {} file", self.check.path)),
));
} else {
results.push(ValidationResult::warning(
format!("{} does not exist (optional)", self.check.path),
None,
));
}
results.push(ValidationResult::warning(
format!("{} does not exist (optional)", self.check.path),
None,
));
}

Ok(results)
Expand Down
4 changes: 2 additions & 2 deletions src/validators/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use crate::config::Config;
use anyhow::Result;

pub mod tool;
pub mod env;
pub mod port;
pub mod file;
pub mod port;
pub mod tool;

#[derive(Debug, Clone)]
pub enum ValidationStatus {
Expand Down
12 changes: 8 additions & 4 deletions src/validators/port.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,18 @@ impl Validator for PortValidator {

match TcpListener::bind(format!("127.0.0.1:{}", self.port)) {
Ok(_) => {
results.push(ValidationResult::success(
format!("Port {} is available", self.port),
));
results.push(ValidationResult::success(format!(
"Port {} is available",
self.port
)));
}
Err(_) => {
results.push(ValidationResult::error(
format!("Port {} is already in use", self.port),
Some(format!("Free up port {} or change the port in your config", self.port)),
Some(format!(
"Free up port {} or change the port in your config",
self.port
)),
));
}
}
Expand Down
59 changes: 32 additions & 27 deletions src/validators/tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,22 @@ impl ToolValidator {
}

fn get_version_command<'a>(&self, tool: &'a str) -> Option<(&'a str, Vec<&'static str>)> {
match tool {
"node" => Some(("node", vec!["--version"])),
"npm" => Some(("npm", vec!["--version"])),
"go" => Some(("go", vec!["version"])),
"rust" | "rustc" => Some(("rustc", vec!["--version"])),
"cargo" => Some(("cargo", vec!["--version"])),
"python" | "python3" => Some(("python3", vec!["--version"])),
"docker" => Some(("docker", vec!["--version"])),
"git" => Some(("git", vec!["--version"])),
"java" => Some(("java", vec!["--version"])),
"ruby" => Some(("ruby", vec!["--version"])),
_ => Some((tool, vec!["--version"])),
}
let tool = match tool {
"rust" => "rustc",
"python" => "python3",
_ => tool,
};

Some((tool, vec!["--version"]))
}

fn parse_version(&self, output: &str, _tool: &str) -> Option<String> {
let output = output.trim();

// Simple version extraction - find first occurrence of X.Y.Z or X.Y pattern
for word in output.split_whitespace() {
let cleaned = word.trim_start_matches('v').trim_start_matches('V');

// Check if it looks like a version number
let parts: Vec<&str> = cleaned.split('.').collect();
if parts.len() >= 2 && parts.iter().all(|p| p.chars().all(|c| c.is_numeric())) {
Expand Down Expand Up @@ -70,7 +64,7 @@ impl ToolValidator {
let req_ver = requirement.trim_start_matches('=').trim();
return version == req_ver;
}

// Default: exact match
version.contains(requirement)
}
Expand Down Expand Up @@ -102,21 +96,31 @@ impl Validator for ToolValidator {
match Command::new(cmd).args(&args).output() {
Ok(output) => {
let version_output = String::from_utf8_lossy(&output.stdout);
if let Some(version) = self.parse_version(&version_output, &self.check.name) {
if let Some(version) = self.parse_version(&version_output, &self.check.name)
{
if self.check_version_requirement(&version, version_req) {
results.push(ValidationResult::success(
format!("{} {} found", self.check.name, version),
));
results.push(ValidationResult::success(format!(
"{} {} found",
self.check.name, version
)));
} else {
results.push(ValidationResult::error(
format!("{} version {} does not meet requirement {}",
self.check.name, version, version_req),
Some(format!("Update {} to version {}", self.check.name, version_req)),
format!(
"{} version {} does not meet requirement {}",
self.check.name, version, version_req
),
Some(format!(
"Update {} to version {}",
self.check.name, version_req
)),
));
}
} else {
results.push(ValidationResult::warning(
format!("{} found but version could not be determined", self.check.name),
format!(
"{} found but version could not be determined",
self.check.name
),
None,
));
}
Expand All @@ -130,9 +134,10 @@ impl Validator for ToolValidator {
}
}
} else {
results.push(ValidationResult::success(
format!("{} found", self.check.name),
));
results.push(ValidationResult::success(format!(
"{} found",
self.check.name
)));
}

Ok(results)
Expand Down
16 changes: 9 additions & 7 deletions tests/integration_tests.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
use assert_cmd::Command;
use predicates::prelude::*;
use tempfile::NamedTempFile;
use std::io::Write;
use tempfile::NamedTempFile;

#[test]
fn test_cli_help() {
let mut cmd = Command::cargo_bin("envcheck").unwrap();
cmd.arg("--help");
cmd.assert()
.success()
.stdout(predicate::str::contains("Validate your development environment"));
cmd.assert().success().stdout(predicate::str::contains(
"Validate your development environment",
));
}

#[test]
Expand Down Expand Up @@ -39,11 +39,12 @@ files:
required: true
"#,
path_str
).unwrap();
)
.unwrap();

let mut cmd = Command::cargo_bin("envcheck").unwrap();
cmd.arg("--config").arg(file.path());

// Node check might fail if not installed in CI environment, but PATH and file should pass
// We check for "Running environment checks" to ensure it started
cmd.assert()
Expand All @@ -59,7 +60,8 @@ fn test_cli_port_validation() {
ports:
- 9999
"#
).unwrap();
)
.unwrap();

let mut cmd = Command::cargo_bin("envcheck").unwrap();
cmd.arg("--config").arg(file.path());
Expand Down