Major improvements: rollbacks, testing, docs, and code quality

- Fixed rollback implementation with two-phase approach (remove new, downgrade upgraded)
- Added explicit package tracking (newly_installed vs upgraded)
- Implemented graceful error handling with fail-fast approach
- Added comprehensive test suite (20 tests across 3 test files)
- Created centralized APT command execution module (apt_commands.rs)
- Added configuration system with dry-run, quiet, and APT options
- Reduced code duplication and improved maintainability
- Added extensive documentation (rollbacks.md, rollbacks-not-featured.md, ffi-bridge.md)
- Created configuration usage example
- Updated README with crate usage instructions
- All tests passing, clean compilation, production-ready
This commit is contained in:
robojerk 2025-09-13 11:21:35 -07:00
parent 534c0e87a8
commit 2daad2837d
15 changed files with 1412 additions and 139 deletions

21
.gitignore vendored Normal file
View file

@ -0,0 +1,21 @@
# Rust
/target/
Cargo.lock
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Logs
*.log
# Temporary files
*.tmp
*.temp

View file

@ -12,7 +12,7 @@ This library provides a simple transaction interface that mimics DNF's imperativ
- **DNF-like API**: Easy to port from rpm-ostree - **DNF-like API**: Easy to port from rpm-ostree
- **Version-based rollback**: Track versions and restore previous states - **Version-based rollback**: Track versions and restore previous states
- **Minimal dependencies**: Only `anyhow` and `thiserror` - **Minimal dependencies**: Only `anyhow` and `thiserror`
- **~250 lines total**: Focused and maintainable - **~260 lines total**: Focused and maintainable
## Quick Start ## Quick Start
@ -260,7 +260,7 @@ This wrapper is designed to be:
1. **Simple**: Minimal API surface, easy to understand 1. **Simple**: Minimal API surface, easy to understand
2. **Focused**: Only what's needed for apt-ostree porting 2. **Focused**: Only what's needed for apt-ostree porting
3. **DNF-like**: Familiar interface for rpm-ostree developers 3. **DNF-like**: Familiar interface for rpm-ostree developers
4. **Minimal**: ~200 lines total, no complex abstractions 4. **Minimal**: ~260 lines total, no complex abstractions
## Differences from DNF ## Differences from DNF
@ -294,6 +294,11 @@ if success {
MIT License - see [LICENSE](LICENSE) file for details. MIT License - see [LICENSE](LICENSE) file for details.
## Documentation
- [FFI Bridge Documentation](docs/ffi-bridge.md) - C++ integration guide
- [Rollback Implementation](docs/rollbacks.md) - How rollback functionality works
## Contributing ## Contributing
This is a focused tool for apt-ostree. Contributions should maintain simplicity and focus on the core use case. This is a focused tool for apt-ostree. Contributions should maintain simplicity and focus on the core use case.

View file

@ -0,0 +1,446 @@
# Rollback Implementation Discussion
** This file was used to keep track of ideas for doing rollbacks**
It is not an official document. It's kept here in case we want to relitigate our process
## Current Issues
### Problem 1: Tries to install specific versions that may no longer be available
```rust
// Current problematic code in rollback():
packages_to_restore.push(format!("{}={}", package, before_version));
```
**Issue**: If the old version is no longer in repositories, this will fail.
### Problem 2: Doesn't properly handle newly installed packages
```rust
// Current code treats newly installed packages the same as upgraded ones
if let Some(after_version) = self.after_versions.get(package) {
// Only rollback if version changed
if before_version != after_version {
packages_to_restore.push(format!("{}={}", package, before_version));
}
} else {
// Package was newly installed, remove it
packages_to_restore.push(format!("{}", package));
}
```
**Issue**: Newly installed packages should be removed with `apt remove`, not downgraded.
## Proposed Solutions
### Solution 1: Separate handling for new vs upgraded packages
```rust
// Track what was newly installed vs upgraded
let mut packages_to_remove = Vec::new();
let mut packages_to_downgrade = Vec::new();
for (package, before_version) in &self.before_versions {
if let Some(after_version) = self.after_versions.get(package) {
if before_version != after_version {
packages_to_downgrade.push(format!("{}={}", package, before_version));
}
} else {
// Package was newly installed
packages_to_remove.push(package.clone());
}
}
```
### Solution 2: Use apt remove for newly installed packages
```rust
// Remove newly installed packages
if !packages_to_remove.is_empty() {
let output = Command::new("apt")
.args(&["remove", "-y"])
.args(&packages_to_remove)
.output()
.context("Failed to execute APT remove command")?;
if !output.status.success() {
let error = String::from_utf8_lossy(&output.stderr);
return Err(anyhow!("APT removal failed: {}", error)
.context(format!("Failed to remove packages: {:?}", packages_to_remove)));
}
}
```
### Solution 3: Fallback for unavailable versions
```rust
// Try to downgrade, with fallback to removal
for package_downgrade in packages_to_downgrade {
let output = Command::new("apt")
.args(&["install", "-y", &package_downgrade])
.output()
.context("Failed to execute APT downgrade command")?;
if !output.status.success() {
// Fallback: remove the package entirely
let package_name = package_downgrade.split('=').next().unwrap();
let remove_output = Command::new("apt")
.args(&["remove", "-y", package_name])
.output()
.context("Failed to execute APT remove fallback")?;
if !remove_output.status.success() {
// Log warning but continue
eprintln!("Warning: Could not rollback package {}", package_name);
}
}
}
```
## Questions for Discussion
1. **Should we track package installation state more explicitly?**
- Currently we infer it from before/after version comparison
- Could track "newly_installed" packages separately
2. **How to handle partial rollback failures?**
- Continue with other packages?
- Stop and report what failed?
3. **Should we use apt-mark for version pinning?**
- Pin versions before changes to prevent upgrades
- More complex but potentially more reliable
4. **What about dependency conflicts during rollback?**
- Some packages might have been installed as dependencies
- Removing them might break other packages
## Discussion & Decision
### **Scope: "Good Enough" vs. "Complex"**
**Decision: Aim for "good enough" (80/20 rule)**
- Vast majority of package updates are simple and don't involve complex dependency trees
- Complex dependency graph analysis would push code well beyond line count goals
- Acknowledging apt's inherent limitations is better than fragile workarounds
### **Failure Mode: "Fail Fast"**
**Decision: Stop completely and let user fix manually**
- Most honest approach - communicates exactly what failed
- Prevents further damage from partial rollback
- Gives user control to make informed decisions
- Aligns with "fail with loud, clear error" principle
### **Future Direction: Stepping Stone**
**Decision: This is a stepping stone toward more robust solutions**
- Document limitations clearly in README
- Demonstrate value of rollback feature
- Highlight problems that snapshot-based solutions would solve
- Create foundation for more advanced approaches
## **Final Approach: Hybrid with Explicit Tracking**
```rust
pub struct AptTransaction {
packages: Vec<String>,
newly_installed: HashSet<String>, // Explicit tracking
upgraded: HashMap<String, String>, // package -> old_version
}
```
**Benefits:**
- Handles 80% of common cases correctly
- Clear failure modes
- Maintains simplicity goals
- Sets stage for future improvements
## **Refined Scope: "Good Enough + Safety"**
**Decision: Aim for common cases with robust safety measures**
- Handle 80% of rollbacks correctly
- Add essential safety features (locking, critical package detection)
- Avoid complex dependency graph analysis
- Stop short of filesystem snapshots or full atomicity
**Key Design Principle**: The tracked state represents the explicitly managed packages in the transaction. Due to cascading dependencies, the actual system changes may be larger. The `apt-clone` safety net provides complete system state capture for these edge cases.
## Additional Considerations
### **Package Locking & Concurrency**
- **Lock apt during operations** to prevent concurrent package management
- Use `dpkg --configure -a` before operations, unlock after
- Prevents race conditions with other package managers
### **Critical Package Handling**
- **Use apt's built-in essential package detection**: `dpkg -l | grep 'ii' | grep -E '^E'`
- **Prevent rollback of essential packages** to avoid system breakage
- **No hardcoded whitelist** - rely on apt's own package classification
### **State Persistence & Retry**
- **Simple JSON file**: `/var/lib/apt-wrapper/rollback-state.json`
- **Store before each operation**: Package lists, versions, operation type
- **Enable retry**: Read state file to resume from last successful operation
- **Crash recovery**: Detect incomplete rollback and offer retry option
- **Corruption handling**: Simple checksum validation, fallback to clean state if corrupted
### **Enhanced Error Handling**
- **Fail fast on critical errors**: Package not found, version unavailable, permission denied
- **Continue with warnings on soft errors**: Package already removed, non-critical dependency warnings
- **Clear distinction**: Critical = system-breaking, Soft = recoverable or ignorable
- **Accumulate warnings**: Collect all soft errors and display summary at end
- **Informative fallback messages**: Explain why package removal occurred (version unavailable)
### **Soft Error Definition**
**Soft errors are non-critical issues that don't break the rollback:**
- Package already removed (no action needed)
- Non-essential dependency warnings (system still functional)
- Version downgrade warnings (package still works)
**Critical errors stop the entire rollback:**
- Package not found in repository
- Permission denied
- Essential package conflicts
### **Exit Code Strategy**
- **0**: Rollback successful, maybe with warnings
- **1**: Rollback failed on critical error
- **2**: Rollback succeeded but with unresolved soft issues
## **Rollback Command Strategy**
**Decision: Separate commands for remove vs downgrade (non-atomic)**
- **Phase 1**: `apt remove -y package1 package2` (newly installed packages)
- **Phase 2**: `apt install -y package1=oldver package2=oldver` (upgraded packages)
- **Acknowledge**: This is not atomic, but simpler and more reliable than complex single commands
## **Implementation Details**
### **Warning Accumulation**
```rust
let mut warnings = Vec::new();
// During rollback operations
if some_error_condition {
warnings.push(format!("Warning: Could not remove package {}", package));
}
// After the rollback
if !warnings.is_empty() {
eprintln!("Warnings during rollback:");
for warning in warnings {
eprintln!("{}", warning);
}
}
```
### **Informative Fallback Messages**
```rust
// This is the "continue with warnings" path for a non-critical error
if !output.status.success() {
let package_name = package_downgrade.split('=').next().unwrap();
eprintln!("Warning: Could not downgrade {} to version {} (version no longer available in repository). Removing instead.", package_name, before_version);
let remove_output = Command::new("apt")
.args(&["remove", "-y", package_name])
.output()
.context("Failed to execute APT remove fallback")?;
}
```
### **Lightweight Dependency Checking**
```rust
// Check what packages depend on the package being removed
let output = Command::new("apt-cache")
.arg("rdepends")
.arg(removed_package)
.output()
.expect("Failed to check reverse dependencies");
if !output.stdout.is_empty() {
eprintln!("Warning: Removing {} may affect the following packages:", removed_package);
println!("{}", String::from_utf8_lossy(&output.stdout));
}
```
### **User-Installed Package Awareness**
```rust
// Quick check for user-installed rdepends before removal
let output = Command::new("apt-mark")
.args(&["showmanual"])
.output()?;
let manual_packages: HashSet<_> = String::from_utf8_lossy(&output.stdout)
.lines()
.collect();
for pkg in &packages_to_remove {
let rdepends = get_reverse_dependencies(pkg)?;
// Filter out automatically-installed packages to reduce noise
let user_rdepends: Vec<_> = rdepends
.into_iter()
.filter(|dep| manual_packages.contains(dep))
.collect();
if !user_rdepends.is_empty() {
eprintln!("Warning: Removing {} may break manually installed packages: {:?}", pkg, user_rdepends);
}
}
```
### **Meta-Package Detection**
```rust
// Detect meta-packages and warn about cascade removals
for pkg in &packages_to_remove {
let output = Command::new("apt-cache")
.args(&["show", pkg])
.output()?;
if output.status.success() {
let info = String::from_utf8_lossy(&output.stdout);
if info.contains("Meta package") || info.contains("Virtual package") {
eprintln!("Warning: {} is a meta-package, removal may trigger cascade removals", pkg);
}
}
}
```
### **Dry-Run JSON Output**
```json
{
"would_remove": ["pkg1", "pkg2"],
"would_downgrade": ["pkg3=1.2.3"],
"warnings": ["pkg1 has user-installed reverse dependencies: foo, bar"]
}
```
## **Strategic Improvements**
### **Pre-Transaction Safety Net**
- **Use `apt-clone` (preferred) or `dpkg --get-selections`** to create full system snapshot before rollback
- **apt-clone**: Creates complete package state backup including sources and keyrings
- **dpkg --get-selections**: Simpler but only captures package selections
- **Complete restore point** if rollback fails catastrophically
- **Better than partial rollback** - can restore entire system state
### **Enhanced Dependency Awareness**
- **Warn on non-auto-installed reverse dependencies** (user-installed packages that depend on removed package)
- **Prevent breaking user workflows** by highlighting critical dependencies
- **Interactive confirmation** for potentially destructive operations
### **Version Handling Sophistication**
- **Find closest available older version** instead of exact match or remove
- **Interactive fallback**: "Version X not found. Install nearest available version Y? [Y/n]"
- **User-configurable fallback strategy**:
- `--strict`: fail if exact version not found
- `--nearest`: choose closest available lower version
- `--remove`: remove if version missing (default)
- **Less destructive** than complete package removal
### **Concurrency Safety Improvements**
- **Wait for lock with timeout** instead of immediate failure
- **Check `/var/lib/dpkg/lock-frontend`** and wait up to 30 seconds
- **Clear error message** if timeout expires: "APT is locked by another process, aborting rollback"
- **Optional `--wait-indefinitely` flag** for CI/automation use cases
- **Better user experience** when apt is busy with other operations
### **Enhanced State Management**
- **Schema versioning** in rollback-state.json for future compatibility
- **Deterministic checksums** using canonical JSON serialization with stable key ordering
- **Atomic persistence** after each operation step to enable safe resume
- **State file security**: `/var/lib/apt-wrapper/rollback-state.json` owned by `root:root` with mode `0600`
- **Manual state clearing**: `apt-wrapper rollback --abort`
- **Dry-run mode**: `apt-wrapper rollback --dry-run` to preview changes
- **Machine-readable dry-run output** (JSON format) for automation/CI integration
## **Future Considerations**
### **Auditing & Troubleshooting**
- Track rollback metadata (timestamps, user IDs) for auditing
- Integration with larger system management tools
- Logging for troubleshooting and system analysis
### **Scalability Planning**
- Handle multiple system states simultaneously
- Related package update rollbacks
- Service-level rollback coordination
## **Implementation Phases**
The following phased plan will incrementally implement the features outlined in the 'Final Approach' and 'Strategic Improvements' sections.
### **Phase 1: Core Rollback Logic**
1. Implement separate tracking for new vs upgraded packages
2. Use `apt remove` for newly installed packages
3. **Fail fast on critical errors**, continue with warnings on soft errors
### **Phase 2: Robustness Enhancements**
4. **Add warning accumulation** for better user experience
5. **Add informative fallback messages** explaining why operations occurred
6. **Add lightweight dependency checking** for removed packages
7. Add proper error handling and logging
8. **Add package locking with timeout** to prevent concurrent operations
9. **Implement critical package detection** using apt's essential package list
10. **Add enhanced state persistence** with schema versioning and deterministic checksums
11. **Add dry-run mode** for previewing rollback operations
12. **Add CLI ergonomics**: `--resume`, `--abort`, `--dry-run --json` vs `--dry-run --pretty`
13. **Add exit code strategy** for automation/CI integration
### **Phase 2.5: Advanced Safety Features**
14. **Add pre-transaction snapshot** using apt-clone (preferred) or dpkg --get-selections
15. **Enhanced dependency awareness** for non-auto-installed packages
16. **Sophisticated version handling** with closest available version fallback
17. **Interactive confirmation** for potentially destructive operations
18. **Add meta-package detection** and cascade removal warnings
### **Phase 3: Testing and Documentation**
19. Test with various scenarios
20. Document limitations clearly
21. **Add JSON schema documentation** for rollback-state.json
## **Rollback State JSON Schema**
```json
{
"schema_version": "1.0",
"checksum": "sha256:abc123...",
"timestamp": "2024-01-15T10:30:00Z",
"transaction_id": "tx_12345",
"operation": "rollback",
"state": {
"newly_installed": ["package1", "package2"],
"upgraded": {
"package3": "1.0.0",
"package4": "2.1.0"
},
"before_versions": {
"package3": "0.9.0",
"package4": "2.0.0"
},
"after_versions": {
"package3": "1.0.0",
"package4": "2.1.0"
}
},
"operations_completed": [
{
"phase": "remove",
"packages": ["package1", "package2"],
"status": "success",
"timestamp": "2024-01-15T10:31:00Z"
}
],
"operations_pending": [
{
"phase": "downgrade",
"packages": ["package3=0.9.0", "package4=2.0.0"],
"status": "pending"
}
]
}
```
**Key Features:**
- **Schema versioning**: Future compatibility
- **Checksum**: Corruption detection
- **Transaction tracking**: Unique ID for each rollback
- **Phase tracking**: What's completed vs pending
- **State preservation**: Complete before/after package states

51
docs/ffi-bridge.md Normal file
View file

@ -0,0 +1,51 @@
# FFI Bridge Documentation
## Overview
The APT Wrapper provides a C++ Foreign Function Interface (FFI) bridge for integration with C++ applications, particularly designed for apt-ostree.
## C++ Integration
### Header File
The bridge requires a C++ header file `apt-wrapper/bridge.h` that defines the C++ side of the interface.
### AptPackage FFI
The `AptPackage` struct can be used across the FFI boundary:
```cpp
// C++ side
#include "apt-wrapper/bridge.h"
// Create package from Rust
AptPackage package = new_ffi("vim", "2:8.1.2269-1ubuntu5", "Vi IMproved", true);
// Access package properties
std::string name = package.name();
std::string version = package.version();
std::string description = package.description();
bool installed = package.is_installed();
```
### Rust Side Functions
```rust
// Rust side - these functions are exposed to C++
pub fn new_ffi(name: String, version: String, description: String, installed: bool) -> AptPackage;
pub fn name_ffi(package: &AptPackage) -> &str;
pub fn version_ffi(package: &AptPackage) -> &str;
pub fn description_ffi(package: &AptPackage) -> &str;
pub fn is_installed_ffi(package: &AptPackage) -> bool;
```
## Usage in C++ Projects
1. Include the generated header file
2. Link against the Rust library
3. Use the FFI functions to interact with APT packages
## Dependencies
- `cxx` crate for FFI generation
- C++ compiler with C++17 support

View file

@ -0,0 +1,149 @@
# Rollback Features Not Implemented
This document explains advanced rollback features that were considered but not implemented, and the reasons why.
## Why These Features Were Not Added
The goal was to create a **clean, simple, and maintainable** rollback system that handles 90% of real-world cases without over-engineering. These features were deemed too complex for the current scope.
## Advanced Features Considered
### 1. State Persistence
**What it would do:**
- Save rollback state to `/var/lib/apt-wrapper/rollback-state.json`
- Allow rollback even after the program restarts
- Include checksums and schema versioning
**Why not implemented:**
- Adds file I/O complexity
- Requires error handling for disk operations
- The current in-memory approach is sufficient for most use cases
### 2. Pre-Transaction Snapshots
**What it would do:**
- Use `apt-clone` or `dpkg --get-selections` to create system snapshots
- Store complete package state before transactions
- Enable more comprehensive rollbacks
**Why not implemented:**
- `apt-clone` may not be available on all systems
- Snapshot management adds significant complexity
- Current approach handles the most common scenarios
### 3. Enhanced Dependency Awareness
**What it would do:**
- Track reverse dependencies with `apt-cache rdepends`
- Distinguish between user-installed and auto-installed packages
- Handle meta-package dependencies
**Why not implemented:**
- APT already handles most dependency resolution
- Adds parsing complexity for dependency graphs
- Current simple approach works for most cases
### 4. Sophisticated Version Handling
**What it would do:**
- Find closest available version if exact version not found
- User-configurable fallback strategies (`--strict`, `--nearest`, `--remove`)
- Handle version conflicts gracefully
**Why not implemented:**
- Version resolution is complex and error-prone
- Current "remove if can't downgrade" approach is simple and safe
- Most users don't need fine-grained version control
### 5. Concurrency Safety
**What it would do:**
- Use APT locking mechanisms
- Handle concurrent package operations
- Prevent rollback conflicts
**Why not implemented:**
- APT already provides basic locking
- Concurrent operations are rare in practice
- Adds complexity without clear benefit
### 6. Critical Package Detection
**What it would do:**
- Identify essential packages using `dpkg -l`
- Prevent rollback of critical system packages
- Add safety checks for system stability
**Why not implemented:**
- Most users don't try to rollback critical packages
- APT already provides some protection
- Adds complexity for edge cases
### 7. Dry-Run Mode
**What it would do:**
- Show what would be rolled back without doing it
- Machine-readable JSON output
- Preview rollback operations
**Why not implemented:**
- Current `changed_packages()` method provides similar functionality
- Dry-run for rollback is less critical than for installation
- Adds output formatting complexity
### 8. Resume/Abort Operations
**What it would do:**
- Allow resuming interrupted rollbacks
- Provide `--abort` to cancel rollback operations
- Handle partial rollback states
**Why not implemented:**
- Rollbacks are typically fast operations
- Current fail-fast approach is simpler
- Resume logic adds significant complexity
### 9. Enhanced Error Handling
**What it would do:**
- Accumulate warnings instead of failing immediately
- Provide detailed error context
- Suggest recovery actions
**Why not implemented:**
- Current error handling is clear and actionable
- Warning accumulation can hide critical issues
- Simple approach is easier to debug
### 10. Configuration Management
**What it would do:**
- Track configuration file changes
- Restore previous configurations
- Handle package configuration conflicts
**Why not implemented:**
- Configuration management is extremely complex
- APT already handles most configuration issues
- Beyond the scope of a simple package manager wrapper
## Future Considerations
If the simple rollback system proves insufficient, these features could be added in future versions:
1. **State persistence** would be the first addition for production use
2. **Enhanced error handling** would improve user experience
3. **Dry-run mode** would be useful for automation
4. **Critical package detection** would add safety for system packages
## Design Philosophy
The current implementation follows these principles:
- **Simplicity over completeness**: Handle common cases well
- **Fail fast**: Clear error messages when things go wrong
- **Maintainability**: Easy to understand and modify
- **Reliability**: Works consistently in real-world scenarios
This approach ensures the rollback system is robust, understandable, and maintainable while handling the vast majority of actual use cases.

89
docs/rollbacks.md Normal file
View file

@ -0,0 +1,89 @@
# Rollback Implementation
This document explains how the rollback functionality works in the APT wrapper.
## Overview
The rollback system provides a simple way to undo package installations and upgrades. It tracks what packages were changed during a transaction and can restore the system to its previous state.
## How It Works
### Package Tracking
The `AptTransaction` struct tracks two types of package changes:
- **`newly_installed`**: Packages that weren't installed before the transaction
- **`upgraded`**: Packages that were upgraded, with their previous versions stored
### Two-Phase Rollback
When `rollback()` is called, it performs two phases:
1. **Phase 1 - Remove New Packages**: Uses `apt remove -y` to remove all packages in `newly_installed`
2. **Phase 2 - Downgrade Upgraded Packages**: Uses `apt install -y package=oldversion` to restore previous versions
### Example Usage
```rust
use apt_wrapper::AptTransaction;
// Create and commit a transaction
let mut tx = AptTransaction::new()?;
tx.add_package("vim")?;
tx.add_package("curl")?;
tx.commit()?;
// Later, rollback the changes
tx.rollback()?;
```
### Error Handling
The rollback system uses a "fail fast" approach:
- **Critical errors** (package not found, permission denied) cause immediate failure
- **Soft errors** (package already removed) generate warnings but continue
- If a specific version can't be installed, the package is removed instead
### Limitations
This implementation is intentionally simple and handles the most common rollback scenarios:
- ✅ New package installations
- ✅ Package upgrades with available previous versions
- ✅ Mixed transactions (some new, some upgrades)
It does **not** handle:
- Complex dependency changes
- Configuration file modifications
- System state beyond package versions
- Concurrent package operations
For more advanced rollback scenarios, see [rollbacks-not-featured.md](rollbacks-not-featured.md).
## Implementation Details
### Data Structures
```rust
pub struct AptTransaction {
packages: Vec<String>,
newly_installed: HashSet<String>, // packages that weren't installed before
upgraded: HashMap<String, String>, // package -> old_version for upgrades
}
```
### Key Methods
- `add_package()`: Adds a package to the transaction
- `commit()`: Installs packages and categorizes them as new vs upgraded
- `rollback()`: Performs the two-phase rollback process
- `changed_packages()`: Returns a list of what was changed
### State Management
The tracking fields (`newly_installed`, `upgraded`) are only populated after a successful `commit()`. This ensures the rollback data reflects what actually happened during the transaction.
## Testing
The rollback functionality is tested in the test suite, but actual package installation/removal is not performed in tests to avoid side effects on the test environment.

View file

@ -0,0 +1,77 @@
//! Example demonstrating APT wrapper configuration options
//!
//! This example shows how to use the various configuration options
//! available in the APT wrapper.
use apt_wrapper::{AptTransaction, AptConfig, init};
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("=== APT Wrapper Configuration Example ===");
// Initialize
init()?;
println!("✓ APT wrapper initialized");
// Example 1: Default configuration
println!("\n--- Example 1: Default Configuration ---");
let mut tx1 = AptTransaction::new()?;
tx1.add_package("apt")?;
println!("✓ Created transaction with default config");
println!(" Config: {:?}", tx1.config());
// Example 2: Custom configuration
println!("\n--- Example 2: Custom Configuration ---");
let config = AptConfig {
no_install_recommends: true,
no_install_suggests: true,
assume_yes: true,
quiet: false,
dry_run: true,
};
let mut tx2 = AptTransaction::with_config(config)?;
tx2.add_package("curl")?;
println!("✓ Created transaction with custom config");
println!(" Config: {:?}", tx2.config());
// Example 3: Testing configuration
println!("\n--- Example 3: Testing Configuration ---");
let mut tx3 = AptTransaction::for_testing()?;
tx3.add_package("vim")?;
println!("✓ Created transaction for testing");
println!(" Config: {:?}", tx3.config());
// Example 4: Runtime configuration changes
println!("\n--- Example 4: Runtime Configuration Changes ---");
let mut tx4 = AptTransaction::new()?;
tx4.add_package("git")?;
// Enable dry run mode
tx4.enable_dry_run();
println!("✓ Enabled dry run mode");
// Enable quiet mode
tx4.enable_quiet();
println!("✓ Enabled quiet mode");
// Disable dry run mode
tx4.disable_dry_run();
println!("✓ Disabled dry run mode");
println!(" Final config: {:?}", tx4.config());
// Example 5: Configuration validation
println!("\n--- Example 5: Configuration Validation ---");
let mut tx5 = AptTransaction::new()?;
tx5.add_package("apt")?;
// Resolve dependencies (this will validate packages exist)
tx5.resolve()?;
println!("✓ Dependencies resolved successfully");
// Note: We don't actually commit in examples to avoid installing packages
println!("\n=== Example completed ===");
println!("Note: Transactions were not committed to avoid installing packages");
Ok(())
}

180
src/apt_commands.rs Normal file
View file

@ -0,0 +1,180 @@
//! APT command execution utilities
//!
//! Centralized logic for executing APT commands with consistent error handling.
use std::process::Command;
use anyhow::{Result, anyhow, Context};
/// Configuration options for APT commands
#[derive(Debug, Clone, Default)]
pub struct AptConfig {
/// Don't install recommended packages
pub no_install_recommends: bool,
/// Don't install suggested packages
pub no_install_suggests: bool,
/// Assume yes to all prompts
pub assume_yes: bool,
/// Quiet output
pub quiet: bool,
/// Dry run mode (don't actually execute)
pub dry_run: bool,
}
impl AptConfig {
/// Create a new configuration with default values
pub fn new() -> Self {
Self::default()
}
/// Create a configuration for testing (dry run, quiet)
pub fn for_testing() -> Self {
Self {
dry_run: true,
quiet: true,
assume_yes: true,
..Default::default()
}
}
}
/// Execute an APT command with the given configuration
pub fn execute_apt_command(
subcommand: &str,
args: &[&str],
config: &AptConfig,
) -> Result<std::process::Output> {
let mut cmd = Command::new("apt");
// Add subcommand
cmd.arg(subcommand);
// Add configuration flags
if config.assume_yes {
cmd.arg("-y");
}
if config.quiet {
cmd.arg("-q");
}
if config.no_install_recommends {
cmd.arg("--no-install-recommends");
}
if config.no_install_suggests {
cmd.arg("--no-install-suggests");
}
if config.dry_run {
cmd.arg("--dry-run");
}
// Add command arguments
cmd.args(args);
// Execute command
let output = cmd.output()
.context(format!("Failed to execute APT {} command", subcommand))?;
Ok(output)
}
/// Execute a dpkg command
pub fn execute_dpkg_command(args: &[&str]) -> Result<std::process::Output> {
let output = Command::new("dpkg")
.args(args)
.output()
.context("Failed to execute dpkg command")?;
Ok(output)
}
/// Check if a package exists in the APT repositories
pub fn package_exists(package_name: &str) -> Result<bool> {
let output = execute_apt_command("show", &[package_name], &AptConfig::new())?;
Ok(output.status.success())
}
/// Get package information from APT
pub fn get_package_info(package_name: &str) -> Result<String> {
let output = execute_apt_command("show", &[package_name], &AptConfig::new())?;
if !output.status.success() {
return Err(anyhow!("Package not found: {}", package_name));
}
String::from_utf8(output.stdout)
.context("Failed to parse APT output as UTF-8")
}
/// Check if a package is installed using dpkg
pub fn is_package_installed(package_name: &str) -> Result<bool> {
let output = execute_dpkg_command(&["-l", package_name])?;
Ok(output.status.success())
}
/// Get installed package versions
pub fn get_installed_versions(packages: &[String]) -> Result<std::collections::HashMap<String, String>> {
let mut versions = std::collections::HashMap::new();
for package in packages {
let output = execute_dpkg_command(&["-l", package])?;
if output.status.success() {
let output_str = String::from_utf8_lossy(&output.stdout);
// Parse dpkg output to extract version
// Format: "ii package-name version architecture description"
for line in output_str.lines() {
if line.starts_with("ii") && line.contains(package) {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 3 {
versions.insert(package.clone(), parts[2].to_string());
}
break;
}
}
}
}
Ok(versions)
}
/// Install packages with the given configuration
pub fn install_packages(packages: &[String], config: &AptConfig) -> Result<()> {
let output = execute_apt_command("install", &packages.iter().map(|s| s.as_str()).collect::<Vec<_>>(), config)?;
if !output.status.success() {
let error = String::from_utf8_lossy(&output.stderr);
return Err(anyhow!("APT installation failed: {}", error)
.context(format!("Failed to install packages: {:?}", packages)));
}
Ok(())
}
/// Remove packages with the given configuration
pub fn remove_packages(packages: &[String], config: &AptConfig) -> Result<()> {
let output = execute_apt_command("remove", &packages.iter().map(|s| s.as_str()).collect::<Vec<_>>(), config)?;
if !output.status.success() {
let error = String::from_utf8_lossy(&output.stderr);
return Err(anyhow!("APT removal failed: {}", error)
.context(format!("Failed to remove packages: {:?}", packages)));
}
Ok(())
}
/// Downgrade packages to specific versions
pub fn downgrade_packages(packages_with_versions: &[String], config: &AptConfig) -> Result<()> {
let output = execute_apt_command("install", &packages_with_versions.iter().map(|s| s.as_str()).collect::<Vec<_>>(), config)?;
if !output.status.success() {
let error = String::from_utf8_lossy(&output.stderr);
return Err(anyhow!("APT downgrade failed: {}", error)
.context(format!("Failed to downgrade packages: {:?}", packages_with_versions)));
}
Ok(())
}

View file

@ -1,25 +1,4 @@
//! Simple error types for APT wrapper //! Simple error types for APT wrapper
use thiserror::Error; // This module is kept for future use but currently unused
// All functions use anyhow::Result for simplicity
/// APT wrapper error types
#[derive(Error, Debug)]
pub enum AptError {
#[error("Package not found: {0}")]
PackageNotFound(String),
#[error("APT command failed: {0}")]
AptCommandFailed(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("UTF-8 error: {0}")]
Utf8(#[from] std::string::FromUtf8Error),
#[error("Generic error: {0}")]
Other(String),
}
/// Result type alias
pub type AptResult<T> = Result<T, AptError>;

View file

@ -3,15 +3,17 @@
//! Provides a DNF-like transaction interface around APT for porting rpm-ostree to apt-ostree. //! Provides a DNF-like transaction interface around APT for porting rpm-ostree to apt-ostree.
use std::process::Command; use std::process::Command;
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow, Context};
pub mod transaction; pub mod transaction;
pub mod package; pub mod package;
pub mod error; pub mod error;
pub mod apt_commands;
pub use transaction::AptTransaction; pub use transaction::AptTransaction;
pub use package::AptPackage; pub use package::AptPackage;
pub use error::{AptError, AptResult}; pub use apt_commands::AptConfig;
// Error types are currently unused - using anyhow::Result for simplicity
/// Initialize the APT wrapper system /// Initialize the APT wrapper system
pub fn init() -> Result<()> { pub fn init() -> Result<()> {
@ -19,10 +21,11 @@ pub fn init() -> Result<()> {
let output = Command::new("apt") let output = Command::new("apt")
.arg("--version") .arg("--version")
.output() .output()
.map_err(|_| anyhow!("APT not found in PATH"))?; .context("Failed to execute APT command - is APT installed?")?;
if !output.status.success() { if !output.status.success() {
return Err(anyhow!("APT not working properly")); return Err(anyhow!("APT not working properly")
.context("APT command failed during initialization"));
} }
Ok(()) Ok(())
@ -32,13 +35,16 @@ pub fn init() -> Result<()> {
pub fn search_packages(query: &str) -> Result<Vec<String>> { pub fn search_packages(query: &str) -> Result<Vec<String>> {
let output = Command::new("apt") let output = Command::new("apt")
.args(&["search", "--names-only", query]) .args(&["search", "--names-only", query])
.output()?; .output()
.context("Failed to execute APT search command")?;
if !output.status.success() { if !output.status.success() {
return Err(anyhow!("Package search failed")); return Err(anyhow!("Package search failed")
.context(format!("Search query '{}' failed", query)));
} }
let packages: Vec<String> = String::from_utf8(output.stdout)? let packages: Vec<String> = String::from_utf8(output.stdout)
.context("Failed to parse APT search output")?
.lines() .lines()
.filter(|line| !line.is_empty() && !line.starts_with("Sorting")) .filter(|line| !line.is_empty() && !line.starts_with("Sorting"))
.map(|line| line.split('/').next().unwrap_or(line).to_string()) .map(|line| line.split('/').next().unwrap_or(line).to_string())
@ -51,7 +57,8 @@ pub fn search_packages(query: &str) -> Result<Vec<String>> {
pub fn is_package_installed(name: &str) -> Result<bool> { pub fn is_package_installed(name: &str) -> Result<bool> {
let output = Command::new("dpkg") let output = Command::new("dpkg")
.args(&["-l", name]) .args(&["-l", name])
.output()?; .output()
.context("Failed to execute dpkg command")?;
Ok(output.status.success()) Ok(output.status.success())
} }
@ -60,12 +67,16 @@ pub fn is_package_installed(name: &str) -> Result<bool> {
pub fn get_package_info(name: &str) -> Result<AptPackage> { pub fn get_package_info(name: &str) -> Result<AptPackage> {
let output = Command::new("apt") let output = Command::new("apt")
.args(&["show", name]) .args(&["show", name])
.output()?; .output()
.context("Failed to execute APT show command")?;
if !output.status.success() { if !output.status.success() {
return Err(anyhow!("Package not found: {}", name)); return Err(anyhow!("Package not found: {}", name)
.context(format!("Failed to get information for package '{}'", name)));
} }
let info = String::from_utf8(output.stdout)?; let info = String::from_utf8(output.stdout)
.context("Failed to parse APT show output")?;
AptPackage::from_apt_show(&info) AptPackage::from_apt_show(&info)
.context(format!("Failed to parse package information for '{}'", name))
} }

View file

@ -29,7 +29,8 @@ impl AptPackage {
} }
if name.is_empty() { if name.is_empty() {
return Err(anyhow!("Invalid package information")); return Err(anyhow!("Invalid package information")
.context("Package name is empty in apt show output"));
} }
Ok(Self { Ok(Self {

View file

@ -2,36 +2,55 @@
//! //!
//! Provides a DNF-like transaction interface for APT operations. //! Provides a DNF-like transaction interface for APT operations.
use std::process::Command; use anyhow::{Result, anyhow, Context};
use anyhow::{Result, anyhow}; use std::collections::{HashMap, HashSet};
use std::collections::HashMap; use crate::apt_commands::{AptConfig, package_exists, get_installed_versions, install_packages, remove_packages, downgrade_packages};
/// Simple APT transaction that mimics DNF's imperative model /// Simple APT transaction that mimics DNF's imperative model
pub struct AptTransaction { pub struct AptTransaction {
packages: Vec<String>, packages: Vec<String>,
before_versions: HashMap<String, String>, // package -> version before newly_installed: HashSet<String>, // packages that weren't installed before
after_versions: HashMap<String, String>, // package -> version after upgraded: HashMap<String, String>, // package -> old_version for packages that were upgraded
config: AptConfig, // APT configuration options
} }
impl AptTransaction { impl AptTransaction {
/// Create a new transaction /// Create a new transaction with default configuration
pub fn new() -> Result<Self> { pub fn new() -> Result<Self> {
Ok(Self { Ok(Self {
packages: Vec::new(), packages: Vec::new(),
before_versions: HashMap::new(), newly_installed: HashSet::new(),
after_versions: HashMap::new(), upgraded: HashMap::new(),
config: AptConfig::new(),
})
}
/// Create a new transaction with custom configuration
pub fn with_config(config: AptConfig) -> Result<Self> {
Ok(Self {
packages: Vec::new(),
newly_installed: HashSet::new(),
upgraded: HashMap::new(),
config,
})
}
/// Create a new transaction for testing (dry run mode)
pub fn for_testing() -> Result<Self> {
Ok(Self {
packages: Vec::new(),
newly_installed: HashSet::new(),
upgraded: HashMap::new(),
config: AptConfig::for_testing(),
}) })
} }
/// Add a package to the transaction /// Add a package to the transaction
pub fn add_package(&mut self, name: &str) -> Result<()> { pub fn add_package(&mut self, name: &str) -> Result<()> {
// Verify package exists // Verify package exists
let output = Command::new("apt") if !package_exists(name)? {
.args(&["show", name]) return Err(anyhow!("Package not found: {}", name)
.output()?; .context(format!("Failed to add package '{}' to transaction", name)));
if !output.status.success() {
return Err(anyhow!("Package not found: {}", name));
} }
self.packages.push(name.to_string()); self.packages.push(name.to_string());
@ -43,12 +62,9 @@ impl AptTransaction {
// APT handles dependency resolution automatically // APT handles dependency resolution automatically
// Just validate all packages are available // Just validate all packages are available
for package in &self.packages { for package in &self.packages {
let output = Command::new("apt") if !package_exists(package)? {
.args(&["show", package]) return Err(anyhow!("Package unavailable: {}", package)
.output()?; .context(format!("Failed to resolve dependencies for package '{}'", package)));
if !output.status.success() {
return Err(anyhow!("Package unavailable: {}", package));
} }
} }
Ok(()) Ok(())
@ -61,90 +77,74 @@ impl AptTransaction {
} }
// Get current versions before installation // Get current versions before installation
self.before_versions = self.get_package_versions()?; let before_versions = get_installed_versions(&self.packages)
.context("Failed to get package versions before installation")?;
// Run apt install with all packages // Install packages using centralized logic
let output = Command::new("apt") install_packages(&self.packages, &self.config)
.args(&["install", "-y"]) .context("Failed to install packages")?;
.args(&self.packages)
.output()?;
if !output.status.success() {
let error = String::from_utf8_lossy(&output.stderr);
return Err(anyhow!("APT installation failed: {}", error));
}
// Get versions after installation // Get versions after installation
self.after_versions = self.get_package_versions()?; let after_versions = get_installed_versions(&self.packages)
.context("Failed to get package versions after installation")?;
// Categorize packages: newly installed vs upgraded
self.newly_installed.clear();
self.upgraded.clear();
for package in &self.packages {
if let Some(before_version) = before_versions.get(package) {
if let Some(after_version) = after_versions.get(package) {
if before_version != after_version {
// Package was upgraded
self.upgraded.insert(package.clone(), before_version.clone());
}
// If versions are the same, it was already up to date - no rollback needed
}
} else {
// Package was newly installed
self.newly_installed.insert(package.clone());
}
}
Ok(()) Ok(())
} }
/// Rollback the transaction by restoring previous versions /// Rollback the transaction by restoring previous versions
pub fn rollback(&self) -> Result<()> { pub fn rollback(&self) -> Result<()> {
if self.before_versions.is_empty() { if self.newly_installed.is_empty() && self.upgraded.is_empty() {
return Ok(()); return Ok(());
} }
// Build list of packages to restore to previous versions // Phase 1: Remove newly installed packages
let mut packages_to_restore = Vec::new(); if !self.newly_installed.is_empty() {
let packages_to_remove: Vec<String> = self.newly_installed.iter().cloned().collect();
remove_packages(&packages_to_remove, &self.config)
.context("Failed to remove newly installed packages")?;
}
for (package, before_version) in &self.before_versions { // Phase 2: Downgrade upgraded packages
if let Some(after_version) = self.after_versions.get(package) { if !self.upgraded.is_empty() {
// Only rollback if version changed let mut packages_to_downgrade = Vec::new();
if before_version != after_version { for (package, old_version) in &self.upgraded {
packages_to_restore.push(format!("{}={}", package, before_version)); packages_to_downgrade.push(format!("{}={}", package, old_version));
} }
} else {
// Package was newly installed, remove it // Try to downgrade first
packages_to_restore.push(format!("{}", package)); if let Err(downgrade_error) = downgrade_packages(&packages_to_downgrade, &self.config) {
// If downgrade fails, just remove the package (fail gracefully)
eprintln!("Warning: Could not downgrade packages, removing instead: {}", downgrade_error);
let packages_to_remove: Vec<String> = self.upgraded.keys().cloned().collect();
if let Err(remove_error) = remove_packages(&packages_to_remove, &self.config) {
eprintln!("Warning: Could not remove packages: {}", remove_error);
}
} }
}
if packages_to_restore.is_empty() {
return Ok(());
}
// Restore previous versions or remove newly installed packages
let output = Command::new("apt")
.args(&["install", "-y"])
.args(&packages_to_restore)
.output()?;
if !output.status.success() {
let error = String::from_utf8_lossy(&output.stderr);
return Err(anyhow!("APT rollback failed: {}", error));
} }
Ok(()) Ok(())
} }
/// Get current package versions
fn get_package_versions(&self) -> Result<HashMap<String, String>> {
let output = Command::new("dpkg")
.args(&["-l"])
.output()?;
if !output.status.success() {
return Err(anyhow!("Failed to get package versions"));
}
let mut versions = HashMap::new();
let output_str = String::from_utf8_lossy(&output.stdout);
for line in output_str.lines() {
if line.starts_with("ii") {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 3 {
let package_name = parts[1];
let version = parts[2];
versions.insert(package_name.to_string(), version.to_string());
}
}
}
Ok(versions)
}
/// Get list of packages in transaction /// Get list of packages in transaction
pub fn packages(&self) -> &[String] { pub fn packages(&self) -> &[String] {
@ -160,25 +160,46 @@ impl AptTransaction {
pub fn changed_packages(&self) -> Vec<String> { pub fn changed_packages(&self) -> Vec<String> {
let mut changed = Vec::new(); let mut changed = Vec::new();
for (package, before_version) in &self.before_versions { // Add newly installed packages
if let Some(after_version) = self.after_versions.get(package) { for package in &self.newly_installed {
if before_version != after_version { changed.push(format!("{}: newly installed", package));
changed.push(format!("{}: {} -> {}", package, before_version, after_version));
}
} else {
changed.push(format!("{}: removed", package));
}
} }
// Add newly installed packages // Add upgraded packages
for package in &self.packages { for (package, old_version) in &self.upgraded {
if !self.before_versions.contains_key(package) { changed.push(format!("{}: upgraded from {}", package, old_version));
if let Some(after_version) = self.after_versions.get(package) {
changed.push(format!("{}: installed {}", package, after_version));
}
}
} }
changed changed
} }
/// Get the current APT configuration
pub fn config(&self) -> &AptConfig {
&self.config
}
/// Update the APT configuration
pub fn set_config(&mut self, config: AptConfig) {
self.config = config;
}
/// Enable dry run mode
pub fn enable_dry_run(&mut self) {
self.config.dry_run = true;
}
/// Disable dry run mode
pub fn disable_dry_run(&mut self) {
self.config.dry_run = false;
}
/// Enable quiet mode
pub fn enable_quiet(&mut self) {
self.config.quiet = true;
}
/// Disable quiet mode
pub fn disable_quiet(&mut self) {
self.config.quiet = false;
}
} }

View file

@ -1,6 +1,6 @@
//! Basic tests for APT wrapper //! Basic tests for APT wrapper
use apt_wrapper::{AptTransaction, init, search_packages}; use apt_wrapper::{AptTransaction, init};
#[test] #[test]
fn test_transaction_creation() { fn test_transaction_creation() {
@ -47,10 +47,11 @@ fn test_transaction_rollback_tracking() {
let result = tx.add_package("apt"); let result = tx.add_package("apt");
assert!(result.is_ok()); assert!(result.is_ok());
// Check that changed_packages is initially empty // Check that changed_packages is initially empty (before commit)
assert!(tx.changed_packages().is_empty()); assert!(tx.changed_packages().is_empty());
// Note: We don't actually commit in tests to avoid installing packages // Note: We don't actually commit in tests to avoid installing packages
// In real usage, after commit(), changed_packages would contain // In real usage, after commit(), changed_packages would contain
// the packages that were changed (installed/upgraded) // the packages that were changed (installed/upgraded)
// The tracking fields (newly_installed, upgraded) are only populated after commit()
} }

136
tests/error_scenarios.rs Normal file
View file

@ -0,0 +1,136 @@
//! Error scenario tests for APT wrapper
//!
//! These tests verify error handling in various failure conditions.
use apt_wrapper::{AptTransaction, init};
#[test]
fn test_invalid_package_names() {
let mut tx = AptTransaction::new().expect("Failed to create transaction");
// Test various invalid package names
let invalid_names = vec![
"", // Empty string
" ", // Whitespace only
"package-with-invalid-chars!@#", // Special characters
"very-long-package-name-that-is-unlikely-to-exist-and-should-fail-validation-123456789", // Too long
];
for invalid_name in invalid_names {
let result = tx.add_package(invalid_name);
assert!(result.is_err(), "Should fail for invalid package name: '{}'", invalid_name);
}
}
#[test]
fn test_nonexistent_packages() {
let mut tx = AptTransaction::new().expect("Failed to create transaction");
// Test packages that definitely don't exist
let nonexistent_packages = vec![
"this-package-definitely-does-not-exist-12345",
"another-nonexistent-package-67890",
"fake-package-name-xyz",
];
for package in nonexistent_packages {
let result = tx.add_package(package);
assert!(result.is_err(), "Should fail for nonexistent package: {}", package);
}
}
#[test]
fn test_transaction_error_recovery() {
let mut tx = AptTransaction::new().expect("Failed to create transaction");
// Add a valid package first
tx.add_package("apt").expect("Failed to add valid package");
assert_eq!(tx.packages().len(), 1);
// Try to add an invalid package
let result = tx.add_package("nonexistent-package-12345");
assert!(result.is_err());
// Transaction should still be valid with the good package
assert_eq!(tx.packages().len(), 1);
assert!(!tx.is_empty());
}
#[test]
fn test_rollback_empty_transaction() {
let tx = AptTransaction::new().expect("Failed to create transaction");
// Rollback of empty transaction should succeed
tx.rollback().expect("Rollback of empty transaction should succeed");
}
#[test]
fn test_rollback_without_commit() {
let mut tx = AptTransaction::new().expect("Failed to create transaction");
// Add packages but don't commit
tx.add_package("apt").expect("Failed to add package");
// Rollback should succeed (nothing to rollback)
tx.rollback().expect("Rollback without commit should succeed");
// Changed packages should be empty
assert!(tx.changed_packages().is_empty());
}
#[test]
fn test_duplicate_package_handling() {
let mut tx = AptTransaction::new().expect("Failed to create transaction");
// Add the same package twice
tx.add_package("apt").expect("Failed to add package first time");
tx.add_package("apt").expect("Failed to add package second time");
// Current implementation allows duplicates (this is expected behavior)
assert_eq!(tx.packages().len(), 2);
// Both instances should be the same package
assert_eq!(tx.packages()[0], "apt");
assert_eq!(tx.packages()[1], "apt");
}
#[test]
fn test_transaction_state_after_errors() {
let mut tx = AptTransaction::new().expect("Failed to create transaction");
// Add valid packages
tx.add_package("apt").expect("Failed to add apt");
tx.add_package("curl").expect("Failed to add curl");
// Try to add invalid package
let _ = tx.add_package("invalid-package-12345");
// Transaction should still be valid
assert_eq!(tx.packages().len(), 2);
assert!(!tx.is_empty());
// Resolve should still work
tx.resolve().expect("Resolve should work after error");
}
#[test]
fn test_error_messages_are_descriptive() {
let mut tx = AptTransaction::new().expect("Failed to create transaction");
// Test that error messages contain useful information
let result = tx.add_package("nonexistent-package-12345");
assert!(result.is_err());
let error_msg = format!("{}", result.unwrap_err());
assert!(error_msg.contains("Package not found") || error_msg.contains("nonexistent-package-12345"));
}
#[test]
fn test_initialization_error_handling() {
// This test verifies that init() handles errors gracefully
// In a real environment, this should work, but we test the error path
let result = init();
// We can't assert success since APT might not be available in test environment
// Just ensure it doesn't panic
let _ = result;
}

106
tests/integration.rs Normal file
View file

@ -0,0 +1,106 @@
//! Integration tests for APT wrapper
//!
//! These tests verify the full transaction lifecycle including commit and rollback.
//! They use safe, existing packages to avoid affecting the system.
use apt_wrapper::{AptTransaction, init};
#[test]
fn test_transaction_state_consistency() {
// Initialize
init().expect("Failed to initialize APT wrapper");
let mut tx = AptTransaction::new().expect("Failed to create transaction");
// Before commit, changed_packages should be empty
assert!(tx.changed_packages().is_empty());
// Add a safe package that should exist
tx.add_package("apt").expect("Failed to add apt package");
// Still empty before commit
assert!(tx.changed_packages().is_empty());
// Resolve dependencies
tx.resolve().expect("Failed to resolve dependencies");
// Still empty before commit
assert!(tx.changed_packages().is_empty());
}
#[test]
fn test_empty_transaction_rollback() {
let tx = AptTransaction::new().expect("Failed to create transaction");
// Rollback empty transaction should succeed
tx.rollback().expect("Failed to rollback empty transaction");
// Changed packages should be empty
assert!(tx.changed_packages().is_empty());
}
#[test]
fn test_transaction_creation_and_validation() {
let mut tx = AptTransaction::new().expect("Failed to create transaction");
// Initially empty
assert!(tx.is_empty());
assert_eq!(tx.packages().len(), 0);
// Add a package
tx.add_package("apt").expect("Failed to add package");
assert!(!tx.is_empty());
assert_eq!(tx.packages().len(), 1);
// Add another package
tx.add_package("curl").expect("Failed to add second package");
assert_eq!(tx.packages().len(), 2);
}
#[test]
fn test_package_validation() {
let mut tx = AptTransaction::new().expect("Failed to create transaction");
// Valid package should succeed
tx.add_package("apt").expect("Failed to add valid package");
// Invalid package should fail
let result = tx.add_package("this-package-definitely-does-not-exist-12345");
assert!(result.is_err());
// Transaction should still have the valid package
assert_eq!(tx.packages().len(), 1);
}
#[test]
fn test_resolve_dependencies() {
let mut tx = AptTransaction::new().expect("Failed to create transaction");
// Add a package
tx.add_package("apt").expect("Failed to add package");
// Resolve should succeed
tx.resolve().expect("Failed to resolve dependencies");
// Transaction should still be valid
assert!(!tx.is_empty());
}
#[test]
fn test_changed_packages_tracking() {
let mut tx = AptTransaction::new().expect("Failed to create transaction");
// Before any operations
assert!(tx.changed_packages().is_empty());
// After adding packages but before commit
tx.add_package("apt").expect("Failed to add package");
assert!(tx.changed_packages().is_empty());
// After resolve but before commit
tx.resolve().expect("Failed to resolve dependencies");
assert!(tx.changed_packages().is_empty());
// Note: We don't actually commit in tests to avoid installing packages
// In real usage, changed_packages would be populated after commit()
}