diff --git a/CLAUDE.md b/CLAUDE.md index 260b053..2ebfb5f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,7 +23,7 @@ pre-commit run --files $(git diff --name-only --diff-filter=ACMR HEAD) ## Tech Stack -Python with venv, pip, and pytest (Unix-only) +Python 3.10 with venv, pip, and pytest (Unix-only) ## Code Principles diff --git a/README.md b/README.md index ff459c5..fa803f3 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,9 @@ python3 -m dependency_resolver --pretty-print ### Programmatic Interface -The dependency-resolver can also be used as a Python library in other projects. Here are some examples: +The dependency-resolver can be used as a Python library in other projects with both functional and class-based interfaces: + +#### Functional Interface (Simple Usage) ```python import dependency_resolver @@ -75,8 +77,89 @@ docker_deps = dependency_resolver.resolve_docker_dependencies( container_identifier="nginx", working_dir="/app" ) +``` + +#### Class-Based Interface (Advanced Usage) + +For complex scenarios and when you need environment-specific configuration: + +```python +from dependency_resolver import DependencyResolver, ResolveRequest + +# Create resolver for specific environment with configuration +host_resolver = DependencyResolver( + environment_type="host", + debug=True, + skip_system_scope=False, + max_workers=4 # Parallel processing +) + +# Docker resolver with container-specific options +docker_resolver = DependencyResolver( + environment_type="docker", + only_container_info=True, # Only valid for docker environment + debug=True +) + +# Single environment analysis +host_result = host_resolver.resolve( + working_dir="/path/to/project", + venv_path="/opt/venv" +) + +docker_result = docker_resolver.resolve( + environment_identifier="nginx", + working_dir="/app" +) + +# Parallel multi-environment analysis (same environment type) +# For host environments +host_requests = [ + ResolveRequest(working_dir="/path/to/project1"), + ResolveRequest(working_dir="/path/to/project2", venv_path="/opt/venv"), + ResolveRequest(working_dir="/path/to/project3") +] + +# For docker containers +docker_requests = [ + ResolveRequest(environment_identifier="nginx", working_dir="/app"), + ResolveRequest(environment_identifier="redis", working_dir="/data"), + ResolveRequest(environment_identifier="postgres") +] + +def progress_callback(completed, total, result): + print(f"Progress: {completed}/{total} ({'✓' if result.success else '✗'})") + +# Execute in parallel with progress tracking +host_results = host_resolver.resolve_batch( + host_requests, + progress_callback=progress_callback, + fail_fast=False # Continue processing even if some fail +) + +docker_results = docker_resolver.resolve_batch( + docker_requests, + progress_callback=progress_callback, + fail_fast=False +) + +# Process results +for result in host_results: + if result.success: + print(f"Found {len(result.dependencies)} detectors") + else: + print(f"Error: {result.error}") -# Advanced usage with direct access to core classes +# Get results as dictionary format +host_dict_results = host_resolver.resolve_batch_as_dict(host_requests) +docker_dict_results = docker_resolver.resolve_batch_as_dict(docker_requests) +``` + +#### Low-Level Interface + +For maximum control, access core classes directly: + +```python from dependency_resolver import Orchestrator, HostExecutor, OutputFormatter executor = HostExecutor() @@ -92,12 +175,21 @@ json_output = formatter.format_json(dependencies, pretty_print=True) - `resolve_docker_dependencies()` - Analyze Docker container, returns JSON string - `resolve_dependencies_as_dict()` - Generic analysis, returns Python dictionary -**Available classes for advanced usage:** +**Available classes:** -- `Orchestrator` - Main dependency resolution coordinator +- `DependencyResolver` - Main class for single and batch operations with parallel processing +- `ResolveRequest` - Configuration for individual resolution requests +- `ResolveResult` - Result object containing dependencies, errors, and execution metadata +- `Orchestrator` - Core dependency resolution coordinator - `HostExecutor`, `DockerExecutor`, `DockerComposeExecutor` - Environment adapters - `OutputFormatter` - JSON formatting utilities +**When to use each interface:** + +- **Functional Interface**: Simple one-off dependency resolution +- **Class-Based Interface**: Multiple environments, parallel processing, shared configuration, progress tracking +- **Low-Level Interface**: Custom orchestration, advanced error handling, integration with existing frameworks + ### Supported Environments - **host** - Host system analysis (default) diff --git a/dependency_resolver/__init__.py b/dependency_resolver/__init__.py index 5642f58..8a85dc1 100644 --- a/dependency_resolver/__init__.py +++ b/dependency_resolver/__init__.py @@ -3,6 +3,7 @@ from .core.interfaces import EnvironmentExecutor from .core.orchestrator import Orchestrator from .core.output_formatter import OutputFormatter +from .core.resolver import DependencyResolver, ResolveRequest, ResolveResult from .executors import HostExecutor, DockerExecutor, DockerComposeExecutor from typing import Optional, Any @@ -90,22 +91,13 @@ def resolve_dependencies_as_dict( Returns: Dictionary containing all discovered dependencies """ - executor: EnvironmentExecutor - if environment_type == "host": - executor = HostExecutor() - elif environment_type == "docker": - if not environment_identifier: - raise ValueError("Docker environment requires container identifier") - executor = DockerExecutor(environment_identifier) - elif environment_type == "docker_compose": - if not environment_identifier: - raise ValueError("Docker Compose environment requires service identifier") - executor = DockerComposeExecutor(environment_identifier) - else: - raise ValueError(f"Unsupported environment type: {environment_type}") - - orchestrator = Orchestrator(debug=debug, skip_system_scope=skip_system_scope, venv_path=venv_path) - return orchestrator.resolve_dependencies(executor, working_dir, only_container_info) + resolver = DependencyResolver( + environment_type=environment_type, + only_container_info=only_container_info, + debug=debug, + skip_system_scope=skip_system_scope, + ) + return resolver.resolve(environment_identifier=environment_identifier, working_dir=working_dir, venv_path=venv_path) def main() -> None: diff --git a/dependency_resolver/__main__.py b/dependency_resolver/__main__.py index 623d24d..c96870c 100644 --- a/dependency_resolver/__main__.py +++ b/dependency_resolver/__main__.py @@ -7,9 +7,7 @@ import sys import argparse -from .executors import HostExecutor, DockerExecutor, DockerComposeExecutor -from .core.interfaces import EnvironmentExecutor -from .core.orchestrator import Orchestrator +from .core.resolver import DependencyResolver from .core.output_formatter import OutputFormatter @@ -95,25 +93,6 @@ def validate_arguments( sys.exit(1) -def create_executor(environment_type: str, environment_identifier: str | None) -> EnvironmentExecutor: - """Create executor based on environment type.""" - if environment_type == "host": - return HostExecutor() - elif environment_type == "docker": - if environment_identifier is None: - print("Error: Docker environment requires container identifier", file=sys.stderr) - sys.exit(1) - return DockerExecutor(environment_identifier) - elif environment_type == "docker_compose": - if environment_identifier is None: - print("Error: Docker Compose environment requires service identifier", file=sys.stderr) - sys.exit(1) - return DockerComposeExecutor(environment_identifier) - else: - print(f"Error: Unsupported environment type: {environment_type}", file=sys.stderr) - sys.exit(1) - - def main() -> None: """Main entry point.""" args = parse_arguments() @@ -121,11 +100,15 @@ def main() -> None: validate_arguments(args.environment_type, args.environment_identifier, args.only_container_info) try: - executor = create_executor(args.environment_type, args.environment_identifier) - orchestrator = Orchestrator( - debug=args.debug, skip_system_scope=args.skip_system_scope, venv_path=args.venv_path + resolver = DependencyResolver( + environment_type=args.environment_type, + only_container_info=args.only_container_info, + debug=args.debug, + skip_system_scope=args.skip_system_scope, + ) + dependencies = resolver.resolve( + environment_identifier=args.environment_identifier, working_dir=args.working_dir, venv_path=args.venv_path ) - dependencies = orchestrator.resolve_dependencies(executor, args.working_dir, args.only_container_info) formatter = OutputFormatter(debug=args.debug) result = formatter.format_json(dependencies, pretty_print=args.pretty_print) print(result) diff --git a/dependency_resolver/core/resolver.py b/dependency_resolver/core/resolver.py new file mode 100644 index 0000000..bf5efe9 --- /dev/null +++ b/dependency_resolver/core/resolver.py @@ -0,0 +1,258 @@ +"""DependencyResolver class for managing dependency resolution operations.""" + +from typing import Optional, Any, Callable, Dict, List +from concurrent.futures import ThreadPoolExecutor, as_completed +from dataclasses import dataclass, field +import time + +from .interfaces import EnvironmentExecutor +from .orchestrator import Orchestrator +from .output_formatter import OutputFormatter +from ..executors import HostExecutor, DockerExecutor, DockerComposeExecutor + + +@dataclass +class ResolveRequest: + """Configuration for a single dependency resolution request.""" + + environment_identifier: Optional[str] = None + working_dir: Optional[str] = None + venv_path: Optional[str] = None + # Optional metadata for tracking + metadata: Dict[str, Any] = field(default_factory=dict) + + +@dataclass +class ResolveResult: + """Result of a dependency resolution operation.""" + + request: ResolveRequest + dependencies: Optional[Dict[str, Any]] = None + error: Optional[str] = None + execution_time: float = 0.0 + success: bool = False + + +class DependencyResolver: + """ + Main class for dependency resolution operations. + + Supports both single and batch resolution with parallel processing. + """ + + def __init__( + self, + environment_type: str = "host", + only_container_info: bool = False, + debug: bool = False, + skip_system_scope: bool = False, + max_workers: Optional[int] = None, + ): + """ + Initialize the dependency resolver. + + Args: + environment_type: Type of environment ("host", "docker", "docker_compose") + only_container_info: Only analyze container metadata (only valid for docker) + debug: Enable debug output + skip_system_scope: Skip system-scope package managers + max_workers: Maximum number of worker threads for parallel operations + """ + # Validate inputs early to provide clear error messages + if environment_type not in ["host", "docker", "docker_compose"]: + raise ValueError(f"Unsupported environment type: {environment_type}") + + if only_container_info and environment_type != "docker": + raise ValueError("only_container_info flag is only valid for docker environment") + + self.environment_type = environment_type + self.only_container_info = only_container_info + self.debug = debug + self.skip_system_scope = skip_system_scope + self.max_workers = max_workers + self.formatter = OutputFormatter(debug=debug) + + def resolve( + self, + environment_identifier: Optional[str] = None, + working_dir: Optional[str] = None, + venv_path: Optional[str] = None, + ) -> Dict[str, Any]: + """ + Resolve dependencies for the configured environment. + + Args: + environment_identifier: Environment identifier (required for docker/docker_compose) + working_dir: Working directory to analyze + venv_path: Explicit virtual environment path for pip detector + + Returns: + Dictionary containing all discovered dependencies + """ + # Validate environment_identifier requirements + if self.environment_type == "docker" and not environment_identifier: + raise ValueError("Docker environment requires container identifier") + + if self.environment_type == "docker_compose" and not environment_identifier: + raise ValueError("Docker Compose environment requires service identifier") + + request = ResolveRequest( + environment_identifier=environment_identifier, + working_dir=working_dir, + venv_path=venv_path, + ) + + result = self._resolve_single(request) + if not result.success: + raise RuntimeError(f"Resolution failed: {result.error}") + + return result.dependencies or {} + + def resolve_batch( + self, + requests: List[ResolveRequest], + progress_callback: Optional[Callable[[int, int, ResolveResult], None]] = None, + fail_fast: bool = False, + ) -> List[ResolveResult]: + """ + Resolve dependencies for multiple environments in parallel. + + Args: + requests: List of resolution requests + progress_callback: Optional callback for progress updates (completed, total, result) + fail_fast: Stop processing on first error + + Returns: + List of results in the same order as requests + """ + if not requests: + return [] + + results: List[Optional[ResolveResult]] = [None] * len(requests) + completed_count = 0 + + with ThreadPoolExecutor(max_workers=self.max_workers) as executor: + # Submit all tasks + future_to_index = {executor.submit(self._resolve_single, request): i for i, request in enumerate(requests)} + + # Process completed futures + for future in as_completed(future_to_index): + index = future_to_index[future] + result = future.result() + results[index] = result + completed_count += 1 + + # Call progress callback if provided + if progress_callback: + progress_callback(completed_count, len(requests), result) + + # Handle fail_fast mode + if fail_fast and not result.success: + # Cancel remaining futures + for remaining_future in future_to_index: + if not remaining_future.done(): + remaining_future.cancel() + break + + return [r for r in results if r is not None] + + def resolve_batch_as_dict( + self, + requests: List[ResolveRequest], + include_errors: bool = True, + progress_callback: Optional[Callable[[int, int, ResolveResult], None]] = None, + ) -> Dict[str, Any]: + """ + Resolve dependencies in batch and return as a single dictionary. + + Args: + requests: List of resolution requests + include_errors: Include error information in results + progress_callback: Optional callback for progress updates + + Returns: + Dictionary with results keyed by environment identifier or index + """ + results = self.resolve_batch(requests, progress_callback=progress_callback) + + output = {} + for i, result in enumerate(results): + # Generate a key for this result + key = result.request.environment_identifier or f"request_{i}" + + if result.success: + output[key] = result.dependencies + elif include_errors: + output[key] = { + "error": result.error, + "execution_time": result.execution_time, + "request": { + "environment_type": self.environment_type, + "environment_identifier": result.request.environment_identifier, + "working_dir": result.request.working_dir, + }, + } + + return output + + def _resolve_single(self, request: ResolveRequest) -> ResolveResult: + """ + Internal method to resolve dependencies for a single request. + + Args: + request: The resolution request + + Returns: + ResolveResult containing the outcome + """ + start_time = time.time() + + try: + # Create appropriate executor + executor = self._create_executor(request) + + # Create orchestrator with request-specific configuration + orchestrator = Orchestrator( + debug=self.debug, skip_system_scope=self.skip_system_scope, venv_path=request.venv_path + ) + + # Resolve dependencies + dependencies = orchestrator.resolve_dependencies(executor, request.working_dir, self.only_container_info) + + execution_time = time.time() - start_time + + return ResolveResult( + request=request, dependencies=dependencies, execution_time=execution_time, success=True + ) + + except (RuntimeError, OSError, ValueError) as e: + execution_time = time.time() - start_time + error_msg = str(e) + + if self.debug: + print(f"Error resolving {self.environment_type}: {error_msg}") + + return ResolveResult(request=request, error=error_msg, execution_time=execution_time, success=False) + + def _create_executor(self, request: ResolveRequest) -> EnvironmentExecutor: + """ + Create the appropriate executor for the configured environment. + + Args: + request: The resolution request containing environment_identifier + + Returns: + Configured executor instance + """ + if self.environment_type == "host": + return HostExecutor() + elif self.environment_type == "docker": + if not request.environment_identifier: + raise ValueError("Docker environment requires container identifier") + return DockerExecutor(request.environment_identifier) + elif self.environment_type == "docker_compose": + if not request.environment_identifier: + raise ValueError("Docker Compose environment requires service identifier") + return DockerComposeExecutor(request.environment_identifier) + else: + raise ValueError(f"Unsupported environment type: {self.environment_type}") diff --git a/tests/core/test_dependency_resolver.py b/tests/core/test_dependency_resolver.py new file mode 100644 index 0000000..617f813 --- /dev/null +++ b/tests/core/test_dependency_resolver.py @@ -0,0 +1,271 @@ +"""Tests for the DependencyResolver class.""" + +import pytest +from typing import Any +from unittest.mock import patch, MagicMock + +from dependency_resolver import DependencyResolver, ResolveRequest, ResolveResult + + +class TestDependencyResolver: + """Tests for DependencyResolver class.""" + + def test_init_default_args(self) -> None: + """Test DependencyResolver initialization with default arguments.""" + resolver = DependencyResolver() + + assert resolver.environment_type == "host" + assert resolver.only_container_info is False + assert resolver.debug is False + assert resolver.skip_system_scope is False + assert resolver.max_workers is None + + def test_init_custom_args(self) -> None: + """Test DependencyResolver initialization with custom arguments.""" + resolver = DependencyResolver( + environment_type="docker", + only_container_info=True, + debug=True, + skip_system_scope=True, + max_workers=4, + ) + + assert resolver.environment_type == "docker" + assert resolver.only_container_info is True + assert resolver.debug is True + assert resolver.skip_system_scope is True + assert resolver.max_workers == 4 + + @patch("dependency_resolver.core.resolver.Orchestrator") + def test_resolve_host_success(self, mock_orchestrator: Any) -> None: + """Test single host environment resolution.""" + # Setup mocks + mock_orchestrator_instance = MagicMock() + mock_orchestrator.return_value = mock_orchestrator_instance + mock_dependencies: dict[str, Any] = {"pip": {"dependencies": {"requests": {"version": "2.28.0"}}}} + mock_orchestrator_instance.resolve_dependencies.return_value = mock_dependencies + + # Create resolver and resolve + resolver = DependencyResolver(environment_type="host", debug=True) + result = resolver.resolve(working_dir="/tmp") + + # Verify + assert result == mock_dependencies + mock_orchestrator.assert_called_once_with(debug=True, skip_system_scope=False, venv_path=None) + + def test_resolve_invalid_environment_type(self) -> None: + """Test resolver initialization with invalid environment type.""" + with pytest.raises(ValueError, match="Unsupported environment type: invalid"): + DependencyResolver(environment_type="invalid") + + def test_resolve_docker_missing_identifier(self) -> None: + """Test resolve with docker but missing identifier.""" + resolver = DependencyResolver(environment_type="docker") + with pytest.raises(ValueError, match="Docker environment requires container identifier"): + resolver.resolve() + + def test_only_container_info_invalid_environment(self) -> None: + """Test resolver initialization with only_container_info for non-docker environment.""" + with pytest.raises(ValueError, match="only_container_info flag is only valid for docker environment"): + DependencyResolver(environment_type="host", only_container_info=True) + + @patch("dependency_resolver.core.resolver.Orchestrator") + @patch("dependency_resolver.core.resolver.DockerExecutor") + def test_resolve_docker_success(self, mock_docker_executor: Any, mock_orchestrator: Any) -> None: + """Test single docker environment resolution.""" + # Setup mocks + mock_executor_instance = MagicMock() + mock_docker_executor.return_value = mock_executor_instance + + mock_orchestrator_instance = MagicMock() + mock_orchestrator.return_value = mock_orchestrator_instance + mock_dependencies: dict[str, Any] = {"docker-info": {"name": "nginx", "image": "nginx:latest"}} + mock_orchestrator_instance.resolve_dependencies.return_value = mock_dependencies + + # Create resolver and resolve + resolver = DependencyResolver(environment_type="docker", debug=True) + result = resolver.resolve(environment_identifier="nginx", working_dir="/app") + + # Verify + assert result == mock_dependencies + mock_docker_executor.assert_called_once_with("nginx") + mock_orchestrator.assert_called_once_with(debug=True, skip_system_scope=False, venv_path=None) + + @patch("dependency_resolver.core.resolver.Orchestrator") + def test_resolve_batch_single_request(self, mock_orchestrator: Any) -> None: + """Test batch resolution with single request.""" + # Setup mocks + mock_orchestrator_instance = MagicMock() + mock_orchestrator.return_value = mock_orchestrator_instance + mock_dependencies: dict[str, Any] = {"pip": {"dependencies": {}}} + mock_orchestrator_instance.resolve_dependencies.return_value = mock_dependencies + + # Create resolver and resolve batch + resolver = DependencyResolver(environment_type="host") + requests = [ResolveRequest(working_dir="/tmp")] + results = resolver.resolve_batch(requests) + + # Verify + assert len(results) == 1 + assert results[0].success is True + assert results[0].dependencies == mock_dependencies + assert results[0].request.working_dir == "/tmp" + + @patch("dependency_resolver.core.resolver.Orchestrator") + def test_resolve_batch_multiple_requests(self, mock_orchestrator: Any) -> None: + """Test batch resolution with multiple requests.""" + # Setup mocks + mock_orchestrator_instance = MagicMock() + mock_orchestrator.return_value = mock_orchestrator_instance + mock_dependencies: dict[str, Any] = {"pip": {"dependencies": {}}} + mock_orchestrator_instance.resolve_dependencies.return_value = mock_dependencies + + # Create resolver and resolve batch + resolver = DependencyResolver(environment_type="host", max_workers=2) + requests = [ResolveRequest(working_dir="/tmp"), ResolveRequest(working_dir="/home")] + results = resolver.resolve_batch(requests) + + # Verify + assert len(results) == 2 + assert all(r.success for r in results) + assert all(r.dependencies == mock_dependencies for r in results) + + def test_resolve_batch_empty_requests(self) -> None: + """Test batch resolution with empty request list.""" + resolver = DependencyResolver() + results = resolver.resolve_batch([]) + + assert results == [] + + @patch("dependency_resolver.core.resolver.Orchestrator") + def test_resolve_batch_with_progress_callback(self, mock_orchestrator: Any) -> None: + """Test batch resolution with progress callback.""" + # Setup mocks + mock_orchestrator_instance = MagicMock() + mock_orchestrator.return_value = mock_orchestrator_instance + mock_dependencies: dict[str, Any] = {"pip": {"dependencies": {}}} + mock_orchestrator_instance.resolve_dependencies.return_value = mock_dependencies + + # Track progress calls + progress_calls = [] + + def progress_callback(completed: int, total: int, result: ResolveResult) -> None: + progress_calls.append((completed, total, result.success)) + + # Create resolver and resolve batch + resolver = DependencyResolver(environment_type="host") + requests = [ResolveRequest(venv_path="/test/venv")] + results = resolver.resolve_batch(requests, progress_callback=progress_callback) + + # Verify progress was called + assert len(progress_calls) == 1 + assert progress_calls[0] == (1, 1, True) + assert len(results) == 1 + + @patch("dependency_resolver.core.resolver.Orchestrator") + def test_resolve_batch_as_dict(self, mock_orchestrator: Any) -> None: + """Test batch resolution returning dictionary format.""" + # Setup mocks + mock_orchestrator_instance = MagicMock() + mock_orchestrator.return_value = mock_orchestrator_instance + mock_dependencies: dict[str, Any] = {"pip": {"dependencies": {"requests": {"version": "2.28.0"}}}} + mock_orchestrator_instance.resolve_dependencies.return_value = mock_dependencies + + # Create resolver and resolve batch + resolver = DependencyResolver(environment_type="host") + requests = [ + ResolveRequest(metadata={"name": "test"}), + ResolveRequest(metadata={"name": "container"}), + ] + result_dict = resolver.resolve_batch_as_dict(requests) + + # Verify + assert "request_0" in result_dict + # Second request key depends on execution order, could be "nginx" or "request_1" + assert len(result_dict) == 2 + assert result_dict["request_0"] == mock_dependencies + + def test_resolve_batch_error_handling(self) -> None: + """Test batch resolution handles errors gracefully.""" + # Mock an error in orchestrator to test error handling + with patch("dependency_resolver.core.resolver.Orchestrator") as mock_orchestrator: + mock_orchestrator_instance = MagicMock() + mock_orchestrator.return_value = mock_orchestrator_instance + mock_orchestrator_instance.resolve_dependencies.side_effect = RuntimeError("Test error") + + resolver = DependencyResolver(environment_type="host") + requests = [ResolveRequest()] + results = resolver.resolve_batch(requests) + + # Verify error is captured + assert len(results) == 1 + assert results[0].success is False + assert results[0].error is not None + assert "Test error" in results[0].error + assert results[0].dependencies is None + + +class TestResolveRequest: + """Tests for ResolveRequest dataclass.""" + + def test_resolve_request_defaults(self) -> None: + """Test ResolveRequest with default values.""" + request = ResolveRequest() + + assert request.environment_identifier is None + assert request.working_dir is None + assert request.venv_path is None + assert request.metadata == {} + + def test_resolve_request_full(self) -> None: + """Test ResolveRequest with all values.""" + metadata = {"custom": "value"} + request = ResolveRequest( + environment_identifier="nginx", + working_dir="/app", + venv_path="/opt/venv", + metadata=metadata, + ) + + assert request.environment_identifier == "nginx" + assert request.working_dir == "/app" + assert request.venv_path == "/opt/venv" + assert request.metadata == metadata + + +class TestResolveResult: + """Tests for ResolveResult dataclass.""" + + def test_resolve_result_defaults(self) -> None: + """Test ResolveResult with default values.""" + request = ResolveRequest() + result = ResolveResult(request) + + assert result.request == request + assert result.dependencies is None + assert result.error is None + assert result.execution_time == 0.0 + assert result.success is False + + def test_resolve_result_success(self) -> None: + """Test ResolveResult for successful resolution.""" + request = ResolveRequest() + dependencies: dict[str, Any] = {"pip": {"dependencies": {}}} + result = ResolveResult(request=request, dependencies=dependencies, execution_time=1.5, success=True) + + assert result.request == request + assert result.dependencies == dependencies + assert result.error is None + assert result.execution_time == 1.5 + assert result.success is True + + def test_resolve_result_error(self) -> None: + """Test ResolveResult for failed resolution.""" + request = ResolveRequest() + result = ResolveResult(request=request, error="Container not found", execution_time=0.1, success=False) + + assert result.request == request + assert result.dependencies is None + assert result.error == "Container not found" + assert result.execution_time == 0.1 + assert result.success is False