Skip to content

Commit 46c8d57

Browse files
committed
Merge #122: Create detection logic package for development dependencies
eaa0acc refactor: [#114] rename tests to follow it_should_ convention (Jose Celano) 91d2eb3 fix: [#114] use inline format args in example (copilot-swe-agent[bot]) 04754eb feat: [#114] add example demonstrating dependency checking (copilot-swe-agent[bot]) 07cc42a docs: [#114] add documentation for platform support and detector creation (copilot-swe-agent[bot]) 48b5c0f feat: [#114] create detection logic package (copilot-swe-agent[bot]) 848b331 Initial plan (copilot-swe-agent[bot]) Pull request description: ## Create Detection Logic Package Implementing detection logic for development dependencies (cargo-machete, OpenTofu, Ansible, LXD) following the linting package pattern. ### Implementation Plan - [x] Create package structure at `packages/dependency-installer/` - [x] Create directory structure - [x] Create Cargo.toml with dependencies (tracing, thiserror) - [x] Add to workspace Cargo.toml - [x] Create README.md - [x] Define core abstractions - [x] ToolDetector trait in `src/detector/mod.rs` - [x] DetectionError enum in `src/errors.rs` - [x] Dependency enum for tool types - [x] CheckResult struct for detection results - [x] Implement command utilities in `src/command.rs` - [x] command_exists() function - [x] execute_command() function - [x] CommandError type - [x] Implement 4 detector structs - [x] CargoMacheteDetector in `src/detector/cargo_machete.rs` - [x] OpenTofuDetector in `src/detector/opentofu.rs` - [x] AnsibleDetector in `src/detector/ansible.rs` - [x] LxdDetector in `src/detector/lxd.rs` - [x] Create DependencyManager in `src/manager.rs` - [x] new() to create with all detectors - [x] check_all() to check all dependencies - [x] get_detector() to get specific detector - [x] Add library exports in `src/lib.rs` - [x] Write comprehensive unit tests - [x] Test each detector implementation - [x] Test DependencyManager functionality - [x] Test error handling - [x] Add working example demonstrating usage - [x] Run linting, build, and verify - [x] cargo build ✓ - [x] cargo test ✓ (22 tests + 2 doctests passing) - [x] cargo run --bin linter all ✓ - [x] Address code review feedback - [x] Document Unix-specific `which` command usage - [x] Clarify detector creation pattern in DependencyManager - [x] Fix clippy linting issue - [x] Use inline format args in example ### Summary Successfully created the `packages/dependency-installer` package with: - **ToolDetector trait**: Abstract interface for tool detection with `name()` and `is_installed()` methods - **4 Detector implementations**: CargoMacheteDetector, OpenTofuDetector, AnsibleDetector, LxdDetector - **DependencyManager**: Orchestrates all detectors with `check_all()` and `get_detector()` methods - **Command utilities**: Helper functions for command execution (`command_exists`, `execute_command`) - **Comprehensive testing**: 22 unit tests + 2 doc tests covering all functionality - **Error handling**: Clear error types (DetectionError, CommandError) with context - **Logging**: Structured logging with tracing for all operations - **Documentation**: Platform support notes and implementation clarifications - **Example**: Working example demonstrating dependency checking All linters pass, all tests pass, workspace builds successfully, and the package follows the linting package pattern. ### Security Summary No security vulnerabilities identified. The package performs read-only operations: - Executes `which` command to check for tool existence - No file system modifications - No network operations - No user input processing - No credential handling ### Next Steps This completes Phase 1 of Issue #114. The next phases will add: - **Phase 2 (Issue #115)**: CLI binary with check command - **Phase 3 (Issue #116)**: Docker test infrastructure - **Phase 4 (Issue #117)**: Installation logic <!-- START COPILOT CODING AGENT SUFFIX --> <details> <summary>Original prompt</summary> > > ---- > > *This section details on the original issue you should resolve* > > <issue_title>Create Detection Logic Package</issue_title> > <issue_description>**Parent Issue**: #113 - Create Dependency Installation Package for E2E Tests > **Depends On**: None (first phase) > **Epic**: #112 - Refactor and Improve E2E Test Execution > > ## Overview > > Create the package structure and implement detection logic to check if development dependencies (cargo-machete, OpenTofu, Ansible, LXD) are installed. > > This is **Phase 1 of 4** for building the dependency installation package. > > ## Objectives > > - Create packages/dependency-installer/ structure following packages/linting/ pattern > - Define ToolDetector trait for detection abstraction > - Implement 4 detector structs (CargoMachete, OpenTofu, Ansible, LXD) > - Create DependencyManager to coordinate detectors > - Add comprehensive unit tests with mocked commands > - Set up tracing for observability > > ## Key Components > > **ToolDetector Trait**: > ```rust > trait ToolDetector { > fn name(&self) -> &str; > fn is_installed(&self) -> Result<bool, DetectionError>; > } > ``` > > **4 Detector Implementations**: > - CargoMacheteDetector - checks `cargo machete --version` > - OpenTofuDetector - checks `tofu --version` > - AnsibleDetector - checks `ansible --version` > - LxdDetector - checks `lxc --version` > > **DependencyManager**: > - Coordinates all detectors > - Provides `check_all()` method returning status for all tools > > ## Acceptance Criteria > > - Pre-commit checks pass > - Package structure follows linting package pattern > - ToolDetector trait is well-defined > - All 4 detectors are implemented with proper error handling > - DependencyManager works correctly > - Unit tests cover all detectors with mocked commands > - Logging provides clear visibility > > ## Time Estimate > > 2-3 hours > > ## Related Documentation > > - Full specification: [docs/issues/114-1-1-1-create-detection-logic-package.md](https://github.com/torrust/torrust-tracker-deployer/blob/main/docs/issues/114-1-1-1-create-detection-logic-package.md) > - Linting package reference: packages/linting/ > - Error handling guide: docs/contributing/error-handling.md</issue_description> > > ## Comments on the Issue (you are @copilot in this section) > > <comments> > </comments> > </details> - Fixes #114 <!-- START COPILOT CODING AGENT TIPS --> --- 💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey). ACKs for top commit: josecelano: ACK eaa0acc Tree-SHA512: 10f9a1163fa41a5690af3eb6c93fcba05e41103dcb475b83421dd16c3459f9cb427c6b6ceb99f5edc65d793063981b402c11fec9a4696bd4e11df41aca5a33ee
2 parents 0d50005 + eaa0acc commit 46c8d57

File tree

14 files changed

+736
-0
lines changed

14 files changed

+736
-0
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
[workspace]
22
members = [
33
"packages/linting",
4+
"packages/dependency-installer",
45
]
56
resolver = "2"
67

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[package]
2+
name = "torrust-dependency-installer"
3+
version = "0.1.0"
4+
edition = "2021"
5+
description = "Dependency detection and installation utilities for the Torrust Tracker Deployer project"
6+
license = "MIT"
7+
8+
[lib]
9+
name = "torrust_dependency_installer"
10+
path = "src/lib.rs"
11+
12+
[dependencies]
13+
thiserror = "1.0"
14+
tracing = "0.1"
15+
tracing-subscriber = { version = "0.3", features = [ "env-filter" ] }
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Torrust Dependency Installer Package
2+
3+
This package provides dependency detection and installation utilities for the Torrust Tracker Deployer project.
4+
5+
## Features
6+
7+
- **Tool Detection**: Check if required development tools are installed
8+
- **Extensible**: Easy to add new tool detectors
9+
- **Logging**: Built-in tracing support for observability
10+
- **Error Handling**: Clear, actionable error messages
11+
12+
## Required Tools
13+
14+
This package can detect the following development dependencies:
15+
16+
- **cargo-machete** - Detects unused Rust dependencies
17+
- **OpenTofu** - Infrastructure provisioning tool
18+
- **Ansible** - Configuration management tool
19+
- **LXD** - VM-based testing infrastructure
20+
21+
## Usage
22+
23+
### Checking Dependencies
24+
25+
```rust
26+
use torrust_dependency_installer::DependencyManager;
27+
28+
fn main() -> Result<(), Box<dyn std::error::Error>> {
29+
let manager = DependencyManager::new();
30+
31+
// Check all dependencies
32+
let results = manager.check_all()?;
33+
34+
for result in results {
35+
println!("{}: {}", result.tool, if result.installed { "" } else { "" });
36+
}
37+
38+
Ok(())
39+
}
40+
```
41+
42+
### Using Individual Detectors
43+
44+
```rust
45+
use torrust_dependency_installer::{ToolDetector, OpenTofuDetector};
46+
47+
fn main() -> Result<(), Box<dyn std::error::Error>> {
48+
let detector = OpenTofuDetector;
49+
50+
if detector.is_installed()? {
51+
println!("{} is installed", detector.name());
52+
} else {
53+
println!("{} is not installed", detector.name());
54+
}
55+
56+
Ok(())
57+
}
58+
```
59+
60+
## Adding to Your Project
61+
62+
Add to your `Cargo.toml`:
63+
64+
```toml
65+
[dependencies]
66+
torrust-dependency-installer = { path = "path/to/torrust-dependency-installer" }
67+
```
68+
69+
Or if using in a workspace:
70+
71+
```toml
72+
[workspace]
73+
members = ["packages/torrust-dependency-installer"]
74+
75+
[dependencies]
76+
torrust-dependency-installer = { path = "packages/torrust-dependency-installer" }
77+
```
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
//! Example: Check all development dependencies
2+
//!
3+
//! This example demonstrates how to use the dependency installer package
4+
//! to check if all required development dependencies are installed.
5+
//!
6+
//! Run with: `cargo run --example check_dependencies`
7+
8+
use torrust_dependency_installer::{init_tracing, DependencyManager};
9+
10+
fn main() {
11+
// Initialize tracing for structured logging
12+
init_tracing();
13+
14+
println!("Checking development dependencies...\n");
15+
16+
// Create dependency manager
17+
let manager = DependencyManager::new();
18+
19+
// Check all dependencies
20+
match manager.check_all() {
21+
Ok(results) => {
22+
println!("Dependency Status:");
23+
println!("{}", "=".repeat(40));
24+
25+
for result in &results {
26+
let status = if result.installed { "✓" } else { "✗" };
27+
let status_text = if result.installed {
28+
"Installed"
29+
} else {
30+
"Not Installed"
31+
};
32+
33+
println!("{} {:20} {}", status, result.tool, status_text);
34+
}
35+
36+
println!("\n{} dependencies checked", results.len());
37+
}
38+
Err(e) => {
39+
eprintln!("Error checking dependencies: {e}");
40+
std::process::exit(1);
41+
}
42+
}
43+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
use std::process::Command;
2+
3+
use crate::errors::CommandError;
4+
5+
/// Check if a command exists in the system PATH
6+
///
7+
/// # Platform Support
8+
///
9+
/// Currently uses the `which` command on Unix-like systems. Windows support
10+
/// would require using `where` command or a different approach.
11+
///
12+
/// # Examples
13+
///
14+
/// ```rust
15+
/// use torrust_dependency_installer::command::command_exists;
16+
///
17+
/// // Check if 'cargo' is installed
18+
/// let exists = command_exists("cargo").unwrap();
19+
/// assert!(exists);
20+
/// ```
21+
///
22+
/// # Errors
23+
///
24+
/// Returns an error if the 'which' command fails to execute
25+
pub fn command_exists(command: &str) -> Result<bool, CommandError> {
26+
// Use 'which' on Unix-like systems to check if command exists
27+
// Note: This is Unix-specific. For Windows support, use 'where' command.
28+
let output =
29+
Command::new("which")
30+
.arg(command)
31+
.output()
32+
.map_err(|e| CommandError::ExecutionFailed {
33+
command: format!("which {command}"),
34+
source: e,
35+
})?;
36+
37+
Ok(output.status.success())
38+
}
39+
40+
/// Execute a command and return its stdout as a string
41+
///
42+
/// # Examples
43+
///
44+
/// ```rust,no_run
45+
/// use torrust_dependency_installer::command::execute_command;
46+
///
47+
/// // Get cargo version
48+
/// let version = execute_command("cargo", &["--version"]).unwrap();
49+
/// println!("Cargo version: {}", version);
50+
/// ```
51+
///
52+
/// # Errors
53+
///
54+
/// Returns an error if the command is not found or fails to execute
55+
pub fn execute_command(command: &str, args: &[&str]) -> Result<String, CommandError> {
56+
let output = Command::new(command).args(args).output().map_err(|e| {
57+
if e.kind() == std::io::ErrorKind::NotFound {
58+
CommandError::CommandNotFound {
59+
command: command.to_string(),
60+
}
61+
} else {
62+
CommandError::ExecutionFailed {
63+
command: format!("{command} {}", args.join(" ")),
64+
source: e,
65+
}
66+
}
67+
})?;
68+
69+
if !output.status.success() {
70+
return Err(CommandError::ExecutionFailed {
71+
command: format!("{command} {}", args.join(" ")),
72+
source: std::io::Error::other(format!("Command exited with status: {}", output.status)),
73+
});
74+
}
75+
76+
String::from_utf8(output.stdout)
77+
.map(|s| s.trim().to_string())
78+
.map_err(|e| CommandError::ExecutionFailed {
79+
command: format!("{command} {}", args.join(" ")),
80+
source: std::io::Error::new(std::io::ErrorKind::InvalidData, e),
81+
})
82+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
use tracing::info;
2+
3+
use crate::command::command_exists;
4+
use crate::detector::ToolDetector;
5+
use crate::errors::DetectionError;
6+
7+
/// Detector for `Ansible` tool
8+
pub struct AnsibleDetector;
9+
10+
impl ToolDetector for AnsibleDetector {
11+
fn name(&self) -> &'static str {
12+
"Ansible"
13+
}
14+
15+
fn is_installed(&self) -> Result<bool, DetectionError> {
16+
info!(tool = "ansible", "Checking if Ansible is installed");
17+
18+
let installed = command_exists("ansible").map_err(|e| DetectionError::DetectionFailed {
19+
tool: self.name().to_string(),
20+
source: std::io::Error::other(e.to_string()),
21+
})?;
22+
23+
if installed {
24+
info!(tool = "ansible", "Ansible is installed");
25+
} else {
26+
info!(tool = "ansible", "Ansible is not installed");
27+
}
28+
29+
Ok(installed)
30+
}
31+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
use tracing::info;
2+
3+
use crate::command::command_exists;
4+
use crate::detector::ToolDetector;
5+
use crate::errors::DetectionError;
6+
7+
/// Detector for `cargo-machete` tool
8+
pub struct CargoMacheteDetector;
9+
10+
impl ToolDetector for CargoMacheteDetector {
11+
fn name(&self) -> &'static str {
12+
"cargo-machete"
13+
}
14+
15+
fn is_installed(&self) -> Result<bool, DetectionError> {
16+
info!(
17+
tool = "cargo-machete",
18+
"Checking if cargo-machete is installed"
19+
);
20+
21+
let installed =
22+
command_exists("cargo-machete").map_err(|e| DetectionError::DetectionFailed {
23+
tool: self.name().to_string(),
24+
source: std::io::Error::other(e.to_string()),
25+
})?;
26+
27+
if installed {
28+
info!(tool = "cargo-machete", "cargo-machete is installed");
29+
} else {
30+
info!(tool = "cargo-machete", "cargo-machete is not installed");
31+
}
32+
33+
Ok(installed)
34+
}
35+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
use tracing::info;
2+
3+
use crate::command::command_exists;
4+
use crate::detector::ToolDetector;
5+
use crate::errors::DetectionError;
6+
7+
/// Detector for `LXD` tool
8+
pub struct LxdDetector;
9+
10+
impl ToolDetector for LxdDetector {
11+
fn name(&self) -> &'static str {
12+
"LXD"
13+
}
14+
15+
fn is_installed(&self) -> Result<bool, DetectionError> {
16+
info!(tool = "lxd", "Checking if LXD is installed");
17+
18+
// Check for 'lxc' command (LXD client)
19+
let installed = command_exists("lxc").map_err(|e| DetectionError::DetectionFailed {
20+
tool: self.name().to_string(),
21+
source: std::io::Error::other(e.to_string()),
22+
})?;
23+
24+
if installed {
25+
info!(tool = "lxd", "LXD is installed");
26+
} else {
27+
info!(tool = "lxd", "LXD is not installed");
28+
}
29+
30+
Ok(installed)
31+
}
32+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
pub mod ansible;
2+
pub mod cargo_machete;
3+
pub mod lxd;
4+
pub mod opentofu;
5+
6+
use crate::errors::DetectionError;
7+
8+
pub use ansible::AnsibleDetector;
9+
pub use cargo_machete::CargoMacheteDetector;
10+
pub use lxd::LxdDetector;
11+
pub use opentofu::OpenTofuDetector;
12+
13+
/// Trait for detecting if a tool is installed
14+
pub trait ToolDetector {
15+
/// Get the tool name for display purposes
16+
fn name(&self) -> &'static str;
17+
18+
/// Check if the tool is already installed
19+
///
20+
/// # Errors
21+
///
22+
/// Returns an error if the detection process fails
23+
fn is_installed(&self) -> Result<bool, DetectionError>;
24+
25+
/// Get the required version (if applicable)
26+
fn required_version(&self) -> Option<&str> {
27+
None // Default implementation
28+
}
29+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
use tracing::info;
2+
3+
use crate::command::command_exists;
4+
use crate::detector::ToolDetector;
5+
use crate::errors::DetectionError;
6+
7+
/// Detector for `OpenTofu` tool
8+
pub struct OpenTofuDetector;
9+
10+
impl ToolDetector for OpenTofuDetector {
11+
fn name(&self) -> &'static str {
12+
"OpenTofu"
13+
}
14+
15+
fn is_installed(&self) -> Result<bool, DetectionError> {
16+
info!(tool = "opentofu", "Checking if OpenTofu is installed");
17+
18+
let installed = command_exists("tofu").map_err(|e| DetectionError::DetectionFailed {
19+
tool: self.name().to_string(),
20+
source: std::io::Error::other(e.to_string()),
21+
})?;
22+
23+
if installed {
24+
info!(tool = "opentofu", "OpenTofu is installed");
25+
} else {
26+
info!(tool = "opentofu", "OpenTofu is not installed");
27+
}
28+
29+
Ok(installed)
30+
}
31+
}

0 commit comments

Comments
 (0)