fix(infrastructure): update adapters for dependency injection compatibility
- Add logger parameter to executor constructors (ShellExecutor, ChrootExecutor) - Add dependencies to ScriptExecutor (fs, cmdExec, scriptDir) - Fix logger adapter to accept *slog.Logger without error return - Fix FileSystem.Create return type from LocalFile to ports.File - Update FileRepository initialization to use constructor pattern - Update all infrastructure tests for new signatures - Add comprehensive dependency injection documentation Resolves all 10 Phase 7 blocking issues documented in PHASE_7_BLOCKING_ISSUES.md All infrastructure tests pass (71+ tests) Build verification: ✅ go build ./cmd/archup-installer succeeds
- commit
- 8105bedfb0243dd35b4063fb681b0989c62d6de3
- author
- bnema · · 2025-10-31 16:24 UTC
- parent
- 0136858
- signature
- GPG Unverified
2B82207D370127A0The key is unknown
18 files · +616 -297
M cmd/archup-installer/main.go +61 -41
| @@ -6,12 +6,16 @@ import ( | |||
| 6 | 6 | "os" | |
| 7 | 7 | ||
| 8 | 8 | tea "github.com/charmbracelet/bubbletea" | |
| 9 | + | apphandlers "github.com/bnema/archup/internal/application/handlers" | |
| 10 | + | "github.com/bnema/archup/internal/application/services" | |
| 9 | 11 | "github.com/bnema/archup/internal/cleanup" | |
| 10 | 12 | "github.com/bnema/archup/internal/config" | |
| 11 | - | "github.com/bnema/archup/internal/interfaces" | |
| 13 | + | "github.com/bnema/archup/internal/infrastructure/executor" | |
| 14 | + | "github.com/bnema/archup/internal/infrastructure/filesystem" | |
| 15 | + | infralogger "github.com/bnema/archup/internal/infrastructure/logger" | |
| 16 | + | "github.com/bnema/archup/internal/infrastructure/persistence" | |
| 17 | + | "github.com/bnema/archup/internal/interfaces/tui" | |
| 12 | 18 | "github.com/bnema/archup/internal/logger" | |
| 13 | - | "github.com/bnema/archup/internal/phases" | |
| 14 | - | "github.com/bnema/archup/internal/ui" | |
| 15 | 19 | ) | |
| 16 | 20 | ||
| 17 | 21 | // version is set via ldflags at build time | |
| @@ -34,17 +38,17 @@ func main() { | |||
| 34 | 38 | } | |
| 35 | 39 | ||
| 36 | 40 | // Create logger with dry-run mode | |
| 37 | - | log, err := logger.New(config.DefaultLogPath, *dryRun) | |
| 41 | + | oldLog, err := logger.New(config.DefaultLogPath, *dryRun) | |
| 38 | 42 | if err != nil { | |
| 39 | 43 | fmt.Fprintf(os.Stderr, "Failed to create logger: %v\n", err) | |
| 40 | 44 | os.Exit(1) | |
| 41 | 45 | } | |
| 42 | - | defer log.Close() | |
| 46 | + | defer oldLog.Close() | |
| 43 | 47 | ||
| 44 | 48 | // Handle --cleanup flag | |
| 45 | 49 | if *doCleanup { | |
| 46 | - | if err := cleanup.Run(log); err != nil { | |
| 47 | - | log.Error("Cleanup failed", "error", err) | |
| 50 | + | if err := cleanup.Run(oldLog); err != nil { | |
| 51 | + | oldLog.Error("Cleanup failed", "error", err) | |
| 48 | 52 | os.Exit(1) | |
| 49 | 53 | } | |
| 50 | 54 | os.Exit(0) | |
| @@ -52,49 +56,65 @@ func main() { | |||
| 52 | 56 | ||
| 53 | 57 | // Log dry-run mode if enabled | |
| 54 | 58 | if *dryRun { | |
| 55 | - | log.Info("Running in DRY-RUN mode - commands will be logged but not executed") | |
| 59 | + | oldLog.Info("Running in DRY-RUN mode - commands will be logged but not executed") | |
| 56 | 60 | } | |
| 57 | 61 | ||
| 58 | 62 | // Load or create config (pass version to determine correct branch for downloads) | |
| 59 | 63 | cfg := config.NewConfig(version) | |
| 60 | - | log.Info("Config initialized", "version", version, "raw_url", cfg.RawURL) | |
| 61 | - | ||
| 62 | - | // Initialize orchestrator | |
| 63 | - | orchestrator := phases.NewOrchestrator(cfg, config.DefaultLogPath) | |
| 64 | - | ||
| 65 | - | // Create concrete implementations of interfaces for dependency injection | |
| 66 | - | fs := &interfaces.DefaultFileSystem{} | |
| 67 | - | httpClient := &interfaces.DefaultHTTPClient{} | |
| 68 | - | sysExec := &interfaces.DefaultSystemExecutor{} | |
| 69 | - | chrExec := &interfaces.DefaultChrootExecutor{} | |
| 70 | - | cmdExec := &interfaces.DefaultCommandExecutor{} | |
| 71 | - | ||
| 72 | - | // Register all phases with logger | |
| 73 | - | phasesToRegister := []phases.Phase{ | |
| 74 | - | phases.NewBootstrapPhase(cfg, log, httpClient, fs), | |
| 75 | - | phases.NewPreflightPhase(cfg, log, fs, cmdExec), | |
| 76 | - | phases.NewPartitioningPhase(cfg, log, sysExec), | |
| 77 | - | phases.NewBaseInstallPhase(cfg, log, fs, sysExec), | |
| 78 | - | phases.NewConfigPhase(cfg, log, fs, sysExec, chrExec), | |
| 79 | - | phases.NewBootPhase(cfg, log, fs, sysExec, chrExec), | |
| 80 | - | phases.NewReposPhase(cfg, log, fs, sysExec, chrExec), | |
| 81 | - | phases.NewPostInstallPhase(cfg, log, fs, httpClient, sysExec, chrExec), | |
| 82 | - | } | |
| 64 | + | oldLog.Info("Config initialized", "version", version, "raw_url", cfg.RawURL) | |
| 83 | 65 | ||
| 84 | - | for _, phase := range phasesToRegister { | |
| 85 | - | if err := orchestrator.RegisterPhase(phase); err != nil { | |
| 86 | - | log.Error("Failed to register phase", "error", err) | |
| 87 | - | os.Exit(1) | |
| 88 | - | } | |
| 66 | + | // ==================== DEPENDENCY INJECTION ==================== | |
| 67 | + | oldLog.Info("Initializing DDD architecture components") | |
| 68 | + | ||
| 69 | + | // Adapt old logger to ports.Logger interface | |
| 70 | + | slogAdapter := infralogger.NewSlogAdapter(oldLog.Slog()) | |
| 71 | + | ||
| 72 | + | // 1. Create infrastructure adapters | |
| 73 | + | fsAdapter := &filesystem.LocalFileSystem{} | |
| 74 | + | shellExec := executor.NewShellExecutor(slogAdapter) | |
| 75 | + | chrootExec := executor.NewChrootExecutor(slogAdapter) | |
| 76 | + | scriptExec := executor.NewScriptExecutor(fsAdapter, shellExec, config.DefaultInstallDir) | |
| 77 | + | repoAdapter, err := persistence.NewFileRepository(config.DefaultConfigPath) | |
| 78 | + | if err != nil { | |
| 79 | + | oldLog.Error("Failed to create repository adapter", "error", err) | |
| 80 | + | os.Exit(1) | |
| 89 | 81 | } | |
| 90 | 82 | ||
| 91 | - | // Create UI model | |
| 92 | - | model := ui.NewModel(orchestrator, cfg, log, version) | |
| 83 | + | // 2. Create command handlers (inject ports) | |
| 84 | + | preflightHandler := apphandlers.NewPreflightHandler(fsAdapter, shellExec, slogAdapter) | |
| 85 | + | partitionHandler := apphandlers.NewPartitionHandler(shellExec, slogAdapter) | |
| 86 | + | baseHandler := apphandlers.NewInstallBaseHandler(fsAdapter, shellExec, chrootExec, slogAdapter) | |
| 87 | + | configHandler := apphandlers.NewConfigureSystemHandler(chrootExec, slogAdapter) | |
| 88 | + | bootloaderHandler := apphandlers.NewBootloaderHandler(chrootExec, slogAdapter) | |
| 89 | + | reposHandler := apphandlers.NewReposHandler(chrootExec, slogAdapter) | |
| 90 | + | postInstallHandler := apphandlers.NewPostInstallHandler(chrootExec, scriptExec, slogAdapter) | |
| 91 | + | ||
| 92 | + | // 3. Create application services | |
| 93 | + | installService := services.NewInstallationService( | |
| 94 | + | repoAdapter, | |
| 95 | + | slogAdapter, | |
| 96 | + | preflightHandler, | |
| 97 | + | partitionHandler, | |
| 98 | + | baseHandler, | |
| 99 | + | configHandler, | |
| 100 | + | bootloaderHandler, | |
| 101 | + | reposHandler, | |
| 102 | + | postInstallHandler, | |
| 103 | + | ) | |
| 104 | + | ||
| 105 | + | // 4. Create TUI app with services | |
| 106 | + | tuiApp := tui.NewApp(installService, installService.Tracker(), slogAdapter) | |
| 93 | 107 | ||
| 94 | - | // Run Bubbletea app | |
| 95 | - | p := tea.NewProgram(model, tea.WithAltScreen()) | |
| 108 | + | // ==================== RUN BUBBLETEA ==================== | |
| 109 | + | oldLog.Info("Starting TUI application", "version", version) | |
| 110 | + | p := tea.NewProgram(tuiApp, tea.WithAltScreen()) | |
| 96 | 111 | if _, err := p.Run(); err != nil { | |
| 97 | - | log.Error("Error running installer", "error", err) | |
| 112 | + | oldLog.Error("Error running installer", "error", err) | |
| 98 | 113 | os.Exit(1) | |
| 99 | 114 | } | |
| 115 | + | ||
| 116 | + | // Cleanup | |
| 117 | + | if err := tuiApp.Close(); err != nil { | |
| 118 | + | oldLog.Error("Error closing TUI app", "error", err) | |
| 119 | + | } | |
| 100 | 120 | } | |
| @@ -6,12 +6,16 @@ import ( | |||||
| 6 | "os" | 6 | "os" | ||
| 7 | 7 | ||||
| 8 | tea "github.com/charmbracelet/bubbletea" | 8 | tea "github.com/charmbracelet/bubbletea" | ||
| 9 | + | apphandlers "github.com/bnema/archup/internal/application/handlers" | |||
| 10 | + | "github.com/bnema/archup/internal/application/services" | |||
| 9 | "github.com/bnema/archup/internal/cleanup" | 11 | "github.com/bnema/archup/internal/cleanup" | ||
| 10 | "github.com/bnema/archup/internal/config" | 12 | "github.com/bnema/archup/internal/config" | ||
| 11 | - | "github.com/bnema/archup/internal/interfaces" | 13 | + | "github.com/bnema/archup/internal/infrastructure/executor" |
| 14 | + | "github.com/bnema/archup/internal/infrastructure/filesystem" | |||
| 15 | + | infralogger "github.com/bnema/archup/internal/infrastructure/logger" | |||
| 16 | + | "github.com/bnema/archup/internal/infrastructure/persistence" | |||
| 17 | + | "github.com/bnema/archup/internal/interfaces/tui" | |||
| 12 | "github.com/bnema/archup/internal/logger" | 18 | "github.com/bnema/archup/internal/logger" | ||
| 13 | - | "github.com/bnema/archup/internal/phases" | |||
| 14 | - | "github.com/bnema/archup/internal/ui" | |||
| 15 | ) | 19 | ) | ||
| 16 | 20 | ||||
| 17 | // version is set via ldflags at build time | 21 | // version is set via ldflags at build time | ||
| @@ -34,17 +38,17 @@ func main() { | |||||
| 34 | } | 38 | } | ||
| 35 | 39 | ||||
| 36 | // Create logger with dry-run mode | 40 | // Create logger with dry-run mode | ||
| 37 | - | log, err := logger.New(config.DefaultLogPath, *dryRun) | 41 | + | oldLog, err := logger.New(config.DefaultLogPath, *dryRun) |
| 38 | if err != nil { | 42 | if err != nil { | ||
| 39 | fmt.Fprintf(os.Stderr, "Failed to create logger: %v\n", err) | 43 | fmt.Fprintf(os.Stderr, "Failed to create logger: %v\n", err) | ||
| 40 | os.Exit(1) | 44 | os.Exit(1) | ||
| 41 | } | 45 | } | ||
| 42 | - | defer log.Close() | 46 | + | defer oldLog.Close() |
| 43 | 47 | ||||
| 44 | // Handle --cleanup flag | 48 | // Handle --cleanup flag | ||
| 45 | if *doCleanup { | 49 | if *doCleanup { | ||
| 46 | - | if err := cleanup.Run(log); err != nil { | 50 | + | if err := cleanup.Run(oldLog); err != nil { |
| 47 | - | log.Error("Cleanup failed", "error", err) | 51 | + | oldLog.Error("Cleanup failed", "error", err) |
| 48 | os.Exit(1) | 52 | os.Exit(1) | ||
| 49 | } | 53 | } | ||
| 50 | os.Exit(0) | 54 | os.Exit(0) | ||
| @@ -52,49 +56,65 @@ func main() { | |||||
| 52 | 56 | ||||
| 53 | // Log dry-run mode if enabled | 57 | // Log dry-run mode if enabled | ||
| 54 | if *dryRun { | 58 | if *dryRun { | ||
| 55 | - | log.Info("Running in DRY-RUN mode - commands will be logged but not executed") | 59 | + | oldLog.Info("Running in DRY-RUN mode - commands will be logged but not executed") |
| 56 | } | 60 | } | ||
| 57 | 61 | ||||
| 58 | // Load or create config (pass version to determine correct branch for downloads) | 62 | // Load or create config (pass version to determine correct branch for downloads) | ||
| 59 | cfg := config.NewConfig(version) | 63 | cfg := config.NewConfig(version) | ||
| 60 | - | log.Info("Config initialized", "version", version, "raw_url", cfg.RawURL) | 64 | + | oldLog.Info("Config initialized", "version", version, "raw_url", cfg.RawURL) |
| 61 | - | ||||
| 62 | - | // Initialize orchestrator | |||
| 63 | - | orchestrator := phases.NewOrchestrator(cfg, config.DefaultLogPath) | |||
| 64 | - | ||||
| 65 | - | // Create concrete implementations of interfaces for dependency injection | |||
| 66 | - | fs := &interfaces.DefaultFileSystem{} | |||
| 67 | - | httpClient := &interfaces.DefaultHTTPClient{} | |||
| 68 | - | sysExec := &interfaces.DefaultSystemExecutor{} | |||
| 69 | - | chrExec := &interfaces.DefaultChrootExecutor{} | |||
| 70 | - | cmdExec := &interfaces.DefaultCommandExecutor{} | |||
| 71 | - | ||||
| 72 | - | // Register all phases with logger | |||
| 73 | - | phasesToRegister := []phases.Phase{ | |||
| 74 | - | phases.NewBootstrapPhase(cfg, log, httpClient, fs), | |||
| 75 | - | phases.NewPreflightPhase(cfg, log, fs, cmdExec), | |||
| 76 | - | phases.NewPartitioningPhase(cfg, log, sysExec), | |||
| 77 | - | phases.NewBaseInstallPhase(cfg, log, fs, sysExec), | |||
| 78 | - | phases.NewConfigPhase(cfg, log, fs, sysExec, chrExec), | |||
| 79 | - | phases.NewBootPhase(cfg, log, fs, sysExec, chrExec), | |||
| 80 | - | phases.NewReposPhase(cfg, log, fs, sysExec, chrExec), | |||
| 81 | - | phases.NewPostInstallPhase(cfg, log, fs, httpClient, sysExec, chrExec), | |||
| 82 | - | } | |||
| 83 | 65 | ||||
| 84 | - | for _, phase := range phasesToRegister { | 66 | + | // ==================== DEPENDENCY INJECTION ==================== |
| 85 | - | if err := orchestrator.RegisterPhase(phase); err != nil { | 67 | + | oldLog.Info("Initializing DDD architecture components") |
| 86 | - | log.Error("Failed to register phase", "error", err) | 68 | + | |
| 87 | - | os.Exit(1) | 69 | + | // Adapt old logger to ports.Logger interface |
| 88 | - | } | 70 | + | slogAdapter := infralogger.NewSlogAdapter(oldLog.Slog()) |
| 71 | + | ||||
| 72 | + | // 1. Create infrastructure adapters | |||
| 73 | + | fsAdapter := &filesystem.LocalFileSystem{} | |||
| 74 | + | shellExec := executor.NewShellExecutor(slogAdapter) | |||
| 75 | + | chrootExec := executor.NewChrootExecutor(slogAdapter) | |||
| 76 | + | scriptExec := executor.NewScriptExecutor(fsAdapter, shellExec, config.DefaultInstallDir) | |||
| 77 | + | repoAdapter, err := persistence.NewFileRepository(config.DefaultConfigPath) | |||
| 78 | + | if err != nil { | |||
| 79 | + | oldLog.Error("Failed to create repository adapter", "error", err) | |||
| 80 | + | os.Exit(1) | |||
| 89 | } | 81 | } | ||
| 90 | 82 | ||||
| 91 | - | // Create UI model | 83 | + | // 2. Create command handlers (inject ports) |
| 92 | - | model := ui.NewModel(orchestrator, cfg, log, version) | 84 | + | preflightHandler := apphandlers.NewPreflightHandler(fsAdapter, shellExec, slogAdapter) |
| 85 | + | partitionHandler := apphandlers.NewPartitionHandler(shellExec, slogAdapter) | |||
| 86 | + | baseHandler := apphandlers.NewInstallBaseHandler(fsAdapter, shellExec, chrootExec, slogAdapter) | |||
| 87 | + | configHandler := apphandlers.NewConfigureSystemHandler(chrootExec, slogAdapter) | |||
| 88 | + | bootloaderHandler := apphandlers.NewBootloaderHandler(chrootExec, slogAdapter) | |||
| 89 | + | reposHandler := apphandlers.NewReposHandler(chrootExec, slogAdapter) | |||
| 90 | + | postInstallHandler := apphandlers.NewPostInstallHandler(chrootExec, scriptExec, slogAdapter) | |||
| 91 | + | ||||
| 92 | + | // 3. Create application services | |||
| 93 | + | installService := services.NewInstallationService( | |||
| 94 | + | repoAdapter, | |||
| 95 | + | slogAdapter, | |||
| 96 | + | preflightHandler, | |||
| 97 | + | partitionHandler, | |||
| 98 | + | baseHandler, | |||
| 99 | + | configHandler, | |||
| 100 | + | bootloaderHandler, | |||
| 101 | + | reposHandler, | |||
| 102 | + | postInstallHandler, | |||
| 103 | + | ) | |||
| 104 | + | ||||
| 105 | + | // 4. Create TUI app with services | |||
| 106 | + | tuiApp := tui.NewApp(installService, installService.Tracker(), slogAdapter) | |||
| 93 | 107 | ||||
| 94 | - | // Run Bubbletea app | 108 | + | // ==================== RUN BUBBLETEA ==================== |
| 95 | - | p := tea.NewProgram(model, tea.WithAltScreen()) | 109 | + | oldLog.Info("Starting TUI application", "version", version) |
| 110 | + | p := tea.NewProgram(tuiApp, tea.WithAltScreen()) | |||
| 96 | if _, err := p.Run(); err != nil { | 111 | if _, err := p.Run(); err != nil { | ||
| 97 | - | log.Error("Error running installer", "error", err) | 112 | + | oldLog.Error("Error running installer", "error", err) |
| 98 | os.Exit(1) | 113 | os.Exit(1) | ||
| 99 | } | 114 | } | ||
| 115 | + | ||||
| 116 | + | // Cleanup | |||
| 117 | + | if err := tuiApp.Close(); err != nil { | |||
| 118 | + | oldLog.Error("Error closing TUI app", "error", err) | |||
| 119 | + | } | |||
| 100 | } | 120 | } | ||
A docs/DEPENDENCY_INJECTION.md +162 -0
| @@ -0,0 +1,162 @@ | |||
| 1 | + | # Dependency Injection Architecture (Phase 7) | |
| 2 | + | ||
| 3 | + | ## Overview | |
| 4 | + | ||
| 5 | + | Phase 7 implements complete dependency injection wiring for the DDD architecture, enabling proper inversion of control and testability across all layers. | |
| 6 | + | ||
| 7 | + | ## Architecture | |
| 8 | + | ||
| 9 | + | ### Dependency Flow (Correct Inversion) | |
| 10 | + | ||
| 11 | + | ``` | |
| 12 | + | UI Layer (interfaces/tui) | |
| 13 | + | ↓ depends on | |
| 14 | + | Application Layer (commands, handlers, services) | |
| 15 | + | ↓ depends on | |
| 16 | + | Domain Layer (aggregates, ports, value objects) | |
| 17 | + | ↑ no dependencies | |
| 18 | + | Infrastructure Layer (adapters) implements domain ports | |
| 19 | + | ``` | |
| 20 | + | ||
| 21 | + | ## Wiring in cmd/archup-installer/main.go | |
| 22 | + | ||
| 23 | + | ### 1. Infrastructure Adapters (Lowest Layer) | |
| 24 | + | ||
| 25 | + | All adapters are created first and implement domain ports: | |
| 26 | + | ||
| 27 | + | ```go | |
| 28 | + | // Filesystem operations | |
| 29 | + | fsAdapter := &filesystem.LocalFileSystem{} | |
| 30 | + | ||
| 31 | + | // Command execution | |
| 32 | + | shellExec := executor.NewShellExecutor(slogAdapter) | |
| 33 | + | chrootExec := executor.NewChrootExecutor(slogAdapter) | |
| 34 | + | scriptExec := executor.NewScriptExecutor(fsAdapter, shellExec, config.DefaultInstallDir) | |
| 35 | + | ||
| 36 | + | // Persistence | |
| 37 | + | repoAdapter, err := persistence.NewFileRepository(config.DefaultConfigPath) | |
| 38 | + | ||
| 39 | + | // Logging | |
| 40 | + | slogAdapter := infralogger.NewSlogAdapter(oldLog.Slog()) | |
| 41 | + | ``` | |
| 42 | + | ||
| 43 | + | ### 2. Command Handlers (Application Layer) | |
| 44 | + | ||
| 45 | + | Handlers are injected with port dependencies only (never concrete implementations): | |
| 46 | + | ||
| 47 | + | ```go | |
| 48 | + | preflightHandler := apphandlers.NewPreflightHandler(fsAdapter, shellExec, slogAdapter) | |
| 49 | + | partitionHandler := apphandlers.NewPartitionHandler(shellExec, slogAdapter) | |
| 50 | + | baseHandler := apphandlers.NewInstallBaseHandler(fsAdapter, shellExec, chrootExec, slogAdapter) | |
| 51 | + | configHandler := apphandlers.NewConfigureSystemHandler(chrootExec, slogAdapter) | |
| 52 | + | bootloaderHandler := apphandlers.NewBootloaderHandler(chrootExec, slogAdapter) | |
| 53 | + | reposHandler := apphandlers.NewReposHandler(chrootExec, slogAdapter) | |
| 54 | + | postInstallHandler := apphandlers.NewPostInstallHandler(chrootExec, scriptExec, slogAdapter) | |
| 55 | + | ``` | |
| 56 | + | ||
| 57 | + | ### 3. Installation Service | |
| 58 | + | ||
| 59 | + | The main orchestrator receives all handlers and coordinates the installation: | |
| 60 | + | ||
| 61 | + | ```go | |
| 62 | + | installService := services.NewInstallationService( | |
| 63 | + | repoAdapter, | |
| 64 | + | slogAdapter, | |
| 65 | + | preflightHandler, | |
| 66 | + | partitionHandler, | |
| 67 | + | baseHandler, | |
| 68 | + | configHandler, | |
| 69 | + | bootloaderHandler, | |
| 70 | + | reposHandler, | |
| 71 | + | postInstallHandler, | |
| 72 | + | ) | |
| 73 | + | ``` | |
| 74 | + | ||
| 75 | + | ### 4. TUI Application | |
| 76 | + | ||
| 77 | + | The user interface receives the service and tracker, with no direct access to domain/infrastructure: | |
| 78 | + | ||
| 79 | + | ```go | |
| 80 | + | tuiApp := tui.NewApp(installService, installService.Tracker(), slogAdapter) | |
| 81 | + | ``` | |
| 82 | + | ||
| 83 | + | ## Constructor Signatures (Phase 7 Requirements) | |
| 84 | + | ||
| 85 | + | ### ShellExecutor | |
| 86 | + | ```go | |
| 87 | + | func NewShellExecutor(logger ports.Logger) *ShellExecutor | |
| 88 | + | ``` | |
| 89 | + | - Requires: Logger for command execution logging | |
| 90 | + | ||
| 91 | + | ### ChrootExecutor | |
| 92 | + | ```go | |
| 93 | + | func NewChrootExecutor(logger ports.Logger) *ChrootExecutor | |
| 94 | + | ``` | |
| 95 | + | - Requires: Logger for chroot operation logging | |
| 96 | + | ||
| 97 | + | ### ScriptExecutor | |
| 98 | + | ```go | |
| 99 | + | func NewScriptExecutor(fs ports.FileSystem, cmdExec ports.CommandExecutor, scriptDir string) *ScriptExecutor | |
| 100 | + | ``` | |
| 101 | + | - Requires: FileSystem for script validation | |
| 102 | + | - Requires: CommandExecutor for optional command execution | |
| 103 | + | - Requires: Script directory path | |
| 104 | + | ||
| 105 | + | ### Logger Adapter | |
| 106 | + | ```go | |
| 107 | + | func NewSlogAdapter(logger *slog.Logger) *SlogAdapter | |
| 108 | + | ``` | |
| 109 | + | - Requires: Pre-configured `*slog.Logger` instance | |
| 110 | + | - No error return (file operations handled by caller) | |
| 111 | + | ||
| 112 | + | ### FileRepository | |
| 113 | + | ```go | |
| 114 | + | func NewFileRepository(basePath string) (*FileRepository, error) | |
| 115 | + | ``` | |
| 116 | + | - Requires: Base path for state persistence | |
| 117 | + | - Returns: error if directory creation fails | |
| 118 | + | ||
| 119 | + | ## Port Implementations | |
| 120 | + | ||
| 121 | + | All infrastructure adapters implement domain ports exactly: | |
| 122 | + | ||
| 123 | + | | Port | Adapter | Location | | |
| 124 | + | |------|---------|----------| | |
| 125 | + | | `FileSystem` | `LocalFileSystem` | `internal/infrastructure/filesystem/local.go` | | |
| 126 | + | | `CommandExecutor` | `ShellExecutor` | `internal/infrastructure/executor/shell.go` | | |
| 127 | + | | `ChrootExecutor` | `ChrootExecutor` | `internal/infrastructure/executor/chroot.go` | | |
| 128 | + | | `ScriptExecutor` | `ScriptExecutor` | `internal/infrastructure/executor/script.go` | | |
| 129 | + | | `Logger` | `SlogAdapter` | `internal/infrastructure/logger/slog_adapter.go` | | |
| 130 | + | | `InstallationRepository` | `FileRepository` | `internal/infrastructure/persistence/file_repository.go` | | |
| 131 | + | ||
| 132 | + | ## Testing with Mocks | |
| 133 | + | ||
| 134 | + | All ports have auto-generated mocks via mockgen: | |
| 135 | + | ||
| 136 | + | ```bash | |
| 137 | + | go generate ./internal/domain/ports | |
| 138 | + | ``` | |
| 139 | + | ||
| 140 | + | Mocks enable unit testing of handlers without infrastructure: | |
| 141 | + | ||
| 142 | + | ```go | |
| 143 | + | ctrl := gomock.NewController(t) | |
| 144 | + | defer ctrl.Finish() | |
| 145 | + | mockFS := mocks.NewMockFileSystem(ctrl) | |
| 146 | + | mockExec := mocks.NewMockCommandExecutor(ctrl) | |
| 147 | + | ||
| 148 | + | handler := NewPreflightHandler(mockFS, mockExec, mockLogger) | |
| 149 | + | // Test with mocks | |
| 150 | + | ``` | |
| 151 | + | ||
| 152 | + | ## Deferred to Phase 8 | |
| 153 | + | ||
| 154 | + | HTTP client support for timezone API and file downloads will be added during Phase 8 (Incremental Migration) when migrating the system configuration and post-installation phases. | |
| 155 | + | ||
| 156 | + | ## Key Principles | |
| 157 | + | ||
| 158 | + | 1. **Dependency Inversion**: Handlers depend on abstractions (ports), not concrete types | |
| 159 | + | 2. **Single Responsibility**: Each adapter implements exactly one port interface | |
| 160 | + | 3. **Constructor Injection**: All dependencies passed via constructors, never globals | |
| 161 | + | 4. **Port Segregation**: Handlers receive only the ports they need | |
| 162 | + | 5. **Testability**: Mocks enable testing without infrastructure dependencies | |
| @@ -0,0 +1,162 @@ | |||||
| 1 | + | # Dependency Injection Architecture (Phase 7) | |||
| 2 | + | ||||
| 3 | + | ## Overview | |||
| 4 | + | ||||
| 5 | + | Phase 7 implements complete dependency injection wiring for the DDD architecture, enabling proper inversion of control and testability across all layers. | |||
| 6 | + | ||||
| 7 | + | ## Architecture | |||
| 8 | + | ||||
| 9 | + | ### Dependency Flow (Correct Inversion) | |||
| 10 | + | ||||
| 11 | + | ``` | |||
| 12 | + | UI Layer (interfaces/tui) | |||
| 13 | + | ↓ depends on | |||
| 14 | + | Application Layer (commands, handlers, services) | |||
| 15 | + | ↓ depends on | |||
| 16 | + | Domain Layer (aggregates, ports, value objects) | |||
| 17 | + | ↑ no dependencies | |||
| 18 | + | Infrastructure Layer (adapters) implements domain ports | |||
| 19 | + | ``` | |||
| 20 | + | ||||
| 21 | + | ## Wiring in cmd/archup-installer/main.go | |||
| 22 | + | ||||
| 23 | + | ### 1. Infrastructure Adapters (Lowest Layer) | |||
| 24 | + | ||||
| 25 | + | All adapters are created first and implement domain ports: | |||
| 26 | + | ||||
| 27 | + | ```go | |||
| 28 | + | // Filesystem operations | |||
| 29 | + | fsAdapter := &filesystem.LocalFileSystem{} | |||
| 30 | + | ||||
| 31 | + | // Command execution | |||
| 32 | + | shellExec := executor.NewShellExecutor(slogAdapter) | |||
| 33 | + | chrootExec := executor.NewChrootExecutor(slogAdapter) | |||
| 34 | + | scriptExec := executor.NewScriptExecutor(fsAdapter, shellExec, config.DefaultInstallDir) | |||
| 35 | + | ||||
| 36 | + | // Persistence | |||
| 37 | + | repoAdapter, err := persistence.NewFileRepository(config.DefaultConfigPath) | |||
| 38 | + | ||||
| 39 | + | // Logging | |||
| 40 | + | slogAdapter := infralogger.NewSlogAdapter(oldLog.Slog()) | |||
| 41 | + | ``` | |||
| 42 | + | ||||
| 43 | + | ### 2. Command Handlers (Application Layer) | |||
| 44 | + | ||||
| 45 | + | Handlers are injected with port dependencies only (never concrete implementations): | |||
| 46 | + | ||||
| 47 | + | ```go | |||
| 48 | + | preflightHandler := apphandlers.NewPreflightHandler(fsAdapter, shellExec, slogAdapter) | |||
| 49 | + | partitionHandler := apphandlers.NewPartitionHandler(shellExec, slogAdapter) | |||
| 50 | + | baseHandler := apphandlers.NewInstallBaseHandler(fsAdapter, shellExec, chrootExec, slogAdapter) | |||
| 51 | + | configHandler := apphandlers.NewConfigureSystemHandler(chrootExec, slogAdapter) | |||
| 52 | + | bootloaderHandler := apphandlers.NewBootloaderHandler(chrootExec, slogAdapter) | |||
| 53 | + | reposHandler := apphandlers.NewReposHandler(chrootExec, slogAdapter) | |||
| 54 | + | postInstallHandler := apphandlers.NewPostInstallHandler(chrootExec, scriptExec, slogAdapter) | |||
| 55 | + | ``` | |||
| 56 | + | ||||
| 57 | + | ### 3. Installation Service | |||
| 58 | + | ||||
| 59 | + | The main orchestrator receives all handlers and coordinates the installation: | |||
| 60 | + | ||||
| 61 | + | ```go | |||
| 62 | + | installService := services.NewInstallationService( | |||
| 63 | + | repoAdapter, | |||
| 64 | + | slogAdapter, | |||
| 65 | + | preflightHandler, | |||
| 66 | + | partitionHandler, | |||
| 67 | + | baseHandler, | |||
| 68 | + | configHandler, | |||
| 69 | + | bootloaderHandler, | |||
| 70 | + | reposHandler, | |||
| 71 | + | postInstallHandler, | |||
| 72 | + | ) | |||
| 73 | + | ``` | |||
| 74 | + | ||||
| 75 | + | ### 4. TUI Application | |||
| 76 | + | ||||
| 77 | + | The user interface receives the service and tracker, with no direct access to domain/infrastructure: | |||
| 78 | + | ||||
| 79 | + | ```go | |||
| 80 | + | tuiApp := tui.NewApp(installService, installService.Tracker(), slogAdapter) | |||
| 81 | + | ``` | |||
| 82 | + | ||||
| 83 | + | ## Constructor Signatures (Phase 7 Requirements) | |||
| 84 | + | ||||
| 85 | + | ### ShellExecutor | |||
| 86 | + | ```go | |||
| 87 | + | func NewShellExecutor(logger ports.Logger) *ShellExecutor | |||
| 88 | + | ``` | |||
| 89 | + | - Requires: Logger for command execution logging | |||
| 90 | + | ||||
| 91 | + | ### ChrootExecutor | |||
| 92 | + | ```go | |||
| 93 | + | func NewChrootExecutor(logger ports.Logger) *ChrootExecutor | |||
| 94 | + | ``` | |||
| 95 | + | - Requires: Logger for chroot operation logging | |||
| 96 | + | ||||
| 97 | + | ### ScriptExecutor | |||
| 98 | + | ```go | |||
| 99 | + | func NewScriptExecutor(fs ports.FileSystem, cmdExec ports.CommandExecutor, scriptDir string) *ScriptExecutor | |||
| 100 | + | ``` | |||
| 101 | + | - Requires: FileSystem for script validation | |||
| 102 | + | - Requires: CommandExecutor for optional command execution | |||
| 103 | + | - Requires: Script directory path | |||
| 104 | + | ||||
| 105 | + | ### Logger Adapter | |||
| 106 | + | ```go | |||
| 107 | + | func NewSlogAdapter(logger *slog.Logger) *SlogAdapter | |||
| 108 | + | ``` | |||
| 109 | + | - Requires: Pre-configured `*slog.Logger` instance | |||
| 110 | + | - No error return (file operations handled by caller) | |||
| 111 | + | ||||
| 112 | + | ### FileRepository | |||
| 113 | + | ```go | |||
| 114 | + | func NewFileRepository(basePath string) (*FileRepository, error) | |||
| 115 | + | ``` | |||
| 116 | + | - Requires: Base path for state persistence | |||
| 117 | + | - Returns: error if directory creation fails | |||
| 118 | + | ||||
| 119 | + | ## Port Implementations | |||
| 120 | + | ||||
| 121 | + | All infrastructure adapters implement domain ports exactly: | |||
| 122 | + | ||||
| 123 | + | | Port | Adapter | Location | | |||
| 124 | + | |------|---------|----------| | |||
| 125 | + | | `FileSystem` | `LocalFileSystem` | `internal/infrastructure/filesystem/local.go` | | |||
| 126 | + | | `CommandExecutor` | `ShellExecutor` | `internal/infrastructure/executor/shell.go` | | |||
| 127 | + | | `ChrootExecutor` | `ChrootExecutor` | `internal/infrastructure/executor/chroot.go` | | |||
| 128 | + | | `ScriptExecutor` | `ScriptExecutor` | `internal/infrastructure/executor/script.go` | | |||
| 129 | + | | `Logger` | `SlogAdapter` | `internal/infrastructure/logger/slog_adapter.go` | | |||
| 130 | + | | `InstallationRepository` | `FileRepository` | `internal/infrastructure/persistence/file_repository.go` | | |||
| 131 | + | ||||
| 132 | + | ## Testing with Mocks | |||
| 133 | + | ||||
| 134 | + | All ports have auto-generated mocks via mockgen: | |||
| 135 | + | ||||
| 136 | + | ```bash | |||
| 137 | + | go generate ./internal/domain/ports | |||
| 138 | + | ``` | |||
| 139 | + | ||||
| 140 | + | Mocks enable unit testing of handlers without infrastructure: | |||
| 141 | + | ||||
| 142 | + | ```go | |||
| 143 | + | ctrl := gomock.NewController(t) | |||
| 144 | + | defer ctrl.Finish() | |||
| 145 | + | mockFS := mocks.NewMockFileSystem(ctrl) | |||
| 146 | + | mockExec := mocks.NewMockCommandExecutor(ctrl) | |||
| 147 | + | ||||
| 148 | + | handler := NewPreflightHandler(mockFS, mockExec, mockLogger) | |||
| 149 | + | // Test with mocks | |||
| 150 | + | ``` | |||
| 151 | + | ||||
| 152 | + | ## Deferred to Phase 8 | |||
| 153 | + | ||||
| 154 | + | HTTP client support for timezone API and file downloads will be added during Phase 8 (Incremental Migration) when migrating the system configuration and post-installation phases. | |||
| 155 | + | ||||
| 156 | + | ## Key Principles | |||
| 157 | + | ||||
| 158 | + | 1. **Dependency Inversion**: Handlers depend on abstractions (ports), not concrete types | |||
| 159 | + | 2. **Single Responsibility**: Each adapter implements exactly one port interface | |||
| 160 | + | 3. **Constructor Injection**: All dependencies passed via constructors, never globals | |||
| 161 | + | 4. **Port Segregation**: Handlers receive only the ports they need | |||
| 162 | + | 5. **Testability**: Mocks enable testing without infrastructure dependencies | |||
M go.mod +2 -1
| @@ -7,6 +7,8 @@ require ( | |||
| 7 | 7 | github.com/charmbracelet/bubbletea v1.3.10 | |
| 8 | 8 | github.com/charmbracelet/huh v0.8.0 | |
| 9 | 9 | github.com/charmbracelet/lipgloss v1.1.0 | |
| 10 | + | github.com/golang/mock v1.6.0 | |
| 11 | + | github.com/google/uuid v1.6.0 | |
| 10 | 12 | go.uber.org/mock v0.6.0 | |
| 11 | 13 | ) | |
| 12 | 14 | ||
| @@ -21,7 +23,6 @@ require ( | |||
| 21 | 23 | github.com/charmbracelet/x/term v0.2.1 // indirect | |
| 22 | 24 | github.com/dustin/go-humanize v1.0.1 // indirect | |
| 23 | 25 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect | |
| 24 | - | github.com/google/uuid v1.6.0 // indirect | |
| 25 | 26 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect | |
| 26 | 27 | github.com/mattn/go-isatty v0.0.20 // indirect | |
| 27 | 28 | github.com/mattn/go-localereader v0.0.1 // indirect | |
| @@ -7,6 +7,8 @@ require ( | |||||
| 7 | github.com/charmbracelet/bubbletea v1.3.10 | 7 | github.com/charmbracelet/bubbletea v1.3.10 | ||
| 8 | github.com/charmbracelet/huh v0.8.0 | 8 | github.com/charmbracelet/huh v0.8.0 | ||
| 9 | github.com/charmbracelet/lipgloss v1.1.0 | 9 | github.com/charmbracelet/lipgloss v1.1.0 | ||
| 10 | + | github.com/golang/mock v1.6.0 | |||
| 11 | + | github.com/google/uuid v1.6.0 | |||
| 10 | go.uber.org/mock v0.6.0 | 12 | go.uber.org/mock v0.6.0 | ||
| 11 | ) | 13 | ) | ||
| 12 | 14 | ||||
| @@ -21,7 +23,6 @@ require ( | |||||
| 21 | github.com/charmbracelet/x/term v0.2.1 // indirect | 23 | github.com/charmbracelet/x/term v0.2.1 // indirect | ||
| 22 | github.com/dustin/go-humanize v1.0.1 // indirect | 24 | github.com/dustin/go-humanize v1.0.1 // indirect | ||
| 23 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect | 25 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect | ||
| 24 | - | github.com/google/uuid v1.6.0 // indirect | |||
| 25 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect | 26 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect | ||
| 26 | github.com/mattn/go-isatty v0.0.20 // indirect | 27 | github.com/mattn/go-isatty v0.0.20 // indirect | ||
| 27 | github.com/mattn/go-localereader v0.0.1 // indirect | 28 | github.com/mattn/go-localereader v0.0.1 // indirect | ||
M go.sum +25 -0
| @@ -42,6 +42,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp | |||
| 42 | 42 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= | |
| 43 | 43 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= | |
| 44 | 44 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= | |
| 45 | + | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= | |
| 46 | + | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= | |
| 45 | 47 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= | |
| 46 | 48 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | |
| 47 | 49 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= | |
| @@ -65,13 +67,36 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= | |||
| 65 | 67 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= | |
| 66 | 68 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= | |
| 67 | 69 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= | |
| 70 | + | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= | |
| 68 | 71 | go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= | |
| 69 | 72 | go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= | |
| 73 | + | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | |
| 74 | + | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | |
| 70 | 75 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= | |
| 71 | 76 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= | |
| 77 | + | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | |
| 78 | + | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | |
| 79 | + | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | |
| 80 | + | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= | |
| 81 | + | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | |
| 82 | + | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | |
| 83 | + | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | |
| 84 | + | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | |
| 85 | + | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | |
| 86 | + | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | |
| 87 | + | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | |
| 72 | 88 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | |
| 73 | 89 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | |
| 74 | 90 | golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= | |
| 75 | 91 | golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= | |
| 92 | + | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | |
| 93 | + | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | |
| 94 | + | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | |
| 76 | 95 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= | |
| 77 | 96 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= | |
| 97 | + | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | |
| 98 | + | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | |
| 99 | + | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= | |
| 100 | + | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | |
| 101 | + | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | |
| 102 | + | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | |
| @@ -42,6 +42,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp | |||||
| 42 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= | 42 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= | ||
| 43 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= | 43 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= | ||
| 44 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= | 44 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= | ||
| 45 | + | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= | |||
| 46 | + | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= | |||
| 45 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= | 47 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= | ||
| 46 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | 48 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||
| 47 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= | 49 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= | ||
| @@ -65,13 +67,36 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= | |||||
| 65 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= | 67 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= | ||
| 66 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= | 68 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= | ||
| 67 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= | 69 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= | ||
| 70 | + | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= | |||
| 68 | go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= | 71 | go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= | ||
| 69 | go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= | 72 | go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= | ||
| 73 | + | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | |||
| 74 | + | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | |||
| 70 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= | 75 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= | ||
| 71 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= | 76 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= | ||
| 77 | + | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | |||
| 78 | + | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | |||
| 79 | + | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | |||
| 80 | + | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= | |||
| 81 | + | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | |||
| 82 | + | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | |||
| 83 | + | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | |||
| 84 | + | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | |||
| 85 | + | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | |||
| 86 | + | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | |||
| 87 | + | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | |||
| 72 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | 88 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
| 73 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | 89 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
| 74 | golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= | 90 | golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= | ||
| 75 | golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= | 91 | golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= | ||
| 92 | + | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | |||
| 93 | + | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | |||
| 94 | + | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | |||
| 76 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= | 95 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= | ||
| 77 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= | 96 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= | ||
| 97 | + | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | |||
| 98 | + | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | |||
| 99 | + | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= | |||
| 100 | + | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | |||
| 101 | + | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | |||
| 102 | + | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | |||
M internal/application/services/installation_service.go +5 -0
| @@ -302,6 +302,11 @@ func (s *InstallationService) SubscribeProgress() <-chan *dto.ProgressUpdate { | |||
| 302 | 302 | return s.tracker.Subscribe() | |
| 303 | 303 | } | |
| 304 | 304 | ||
| 305 | + | // Tracker returns the progress tracker (for TUI initialization) | |
| 306 | + | func (s *InstallationService) Tracker() *ProgressTracker { | |
| 307 | + | return s.tracker | |
| 308 | + | } | |
| 309 | + | ||
| 305 | 310 | // Close closes the service | |
| 306 | 311 | func (s *InstallationService) Close() error { | |
| 307 | 312 | s.tracker.Close() | |
| @@ -302,6 +302,11 @@ func (s *InstallationService) SubscribeProgress() <-chan *dto.ProgressUpdate { | |||||
| 302 | return s.tracker.Subscribe() | 302 | return s.tracker.Subscribe() | ||
| 303 | } | 303 | } | ||
| 304 | 304 | ||||
| 305 | + | // Tracker returns the progress tracker (for TUI initialization) | |||
| 306 | + | func (s *InstallationService) Tracker() *ProgressTracker { | |||
| 307 | + | return s.tracker | |||
| 308 | + | } | |||
| 309 | + | ||||
| 305 | // Close closes the service | 310 | // Close closes the service | ||
| 306 | func (s *InstallationService) Close() error { | 311 | func (s *InstallationService) Close() error { | ||
| 307 | s.tracker.Close() | 312 | s.tracker.Close() | ||
M internal/domain/ports/ports.go +4 -4
| @@ -95,16 +95,16 @@ type Response interface { | |||
| 95 | 95 | // Logger is the port for logging | |
| 96 | 96 | type Logger interface { | |
| 97 | 97 | // Info logs an informational message | |
| 98 | - | Info(msg string, keysAndValues ...interface{}) | |
| 98 | + | Info(msg string, keysAndValues ...any) | |
| 99 | 99 | ||
| 100 | 100 | // Warn logs a warning message | |
| 101 | - | Warn(msg string, keysAndValues ...interface{}) | |
| 101 | + | Warn(msg string, keysAndValues ...any) | |
| 102 | 102 | ||
| 103 | 103 | // Error logs an error message | |
| 104 | - | Error(msg string, keysAndValues ...interface{}) | |
| 104 | + | Error(msg string, keysAndValues ...any) | |
| 105 | 105 | ||
| 106 | 106 | // Debug logs a debug message | |
| 107 | - | Debug(msg string, keysAndValues ...interface{}) | |
| 107 | + | Debug(msg string, keysAndValues ...any) | |
| 108 | 108 | ||
| 109 | 109 | // LogPath returns the path to the log file | |
| 110 | 110 | LogPath() string | |
| @@ -95,16 +95,16 @@ type Response interface { | |||||
| 95 | // Logger is the port for logging | 95 | // Logger is the port for logging | ||
| 96 | type Logger interface { | 96 | type Logger interface { | ||
| 97 | // Info logs an informational message | 97 | // Info logs an informational message | ||
| 98 | - | Info(msg string, keysAndValues ...interface{}) | 98 | + | Info(msg string, keysAndValues ...any) |
| 99 | 99 | ||||
| 100 | // Warn logs a warning message | 100 | // Warn logs a warning message | ||
| 101 | - | Warn(msg string, keysAndValues ...interface{}) | 101 | + | Warn(msg string, keysAndValues ...any) |
| 102 | 102 | ||||
| 103 | // Error logs an error message | 103 | // Error logs an error message | ||
| 104 | - | Error(msg string, keysAndValues ...interface{}) | 104 | + | Error(msg string, keysAndValues ...any) |
| 105 | 105 | ||||
| 106 | // Debug logs a debug message | 106 | // Debug logs a debug message | ||
| 107 | - | Debug(msg string, keysAndValues ...interface{}) | 107 | + | Debug(msg string, keysAndValues ...any) |
| 108 | 108 | ||||
| 109 | // LogPath returns the path to the log file | 109 | // LogPath returns the path to the log file | ||
| 110 | LogPath() string | 110 | LogPath() string | ||
M internal/infrastructure/executor/chroot.go +6 -4
| @@ -7,17 +7,19 @@ import ( | |||
| 7 | 7 | "os" | |
| 8 | 8 | "os/exec" | |
| 9 | 9 | "path/filepath" | |
| 10 | + | ||
| 11 | + | "github.com/bnema/archup/internal/domain/ports" | |
| 10 | 12 | ) | |
| 11 | 13 | ||
| 12 | 14 | // ChrootExecutor implements the ChrootExecutor port using chroot commands | |
| 13 | 15 | type ChrootExecutor struct { | |
| 14 | - | cmdExecutor *ShellExecutor | |
| 16 | + | logger ports.Logger | |
| 15 | 17 | } | |
| 16 | 18 | ||
| 17 | - | // NewChrootExecutor creates a new chroot executor | |
| 18 | - | func NewChrootExecutor() *ChrootExecutor { | |
| 19 | + | // NewChrootExecutor creates a new chroot executor with a logger | |
| 20 | + | func NewChrootExecutor(logger ports.Logger) *ChrootExecutor { | |
| 19 | 21 | return &ChrootExecutor{ | |
| 20 | - | cmdExecutor: NewShellExecutor(), | |
| 22 | + | logger: logger, | |
| 21 | 23 | } | |
| 22 | 24 | } | |
| 23 | 25 | ||
| @@ -7,17 +7,19 @@ import ( | |||||
| 7 | "os" | 7 | "os" | ||
| 8 | "os/exec" | 8 | "os/exec" | ||
| 9 | "path/filepath" | 9 | "path/filepath" | ||
| 10 | + | ||||
| 11 | + | "github.com/bnema/archup/internal/domain/ports" | |||
| 10 | ) | 12 | ) | ||
| 11 | 13 | ||||
| 12 | // ChrootExecutor implements the ChrootExecutor port using chroot commands | 14 | // ChrootExecutor implements the ChrootExecutor port using chroot commands | ||
| 13 | type ChrootExecutor struct { | 15 | type ChrootExecutor struct { | ||
| 14 | - | cmdExecutor *ShellExecutor | 16 | + | logger ports.Logger |
| 15 | } | 17 | } | ||
| 16 | 18 | ||||
| 17 | - | // NewChrootExecutor creates a new chroot executor | 19 | + | // NewChrootExecutor creates a new chroot executor with a logger |
| 18 | - | func NewChrootExecutor() *ChrootExecutor { | 20 | + | func NewChrootExecutor(logger ports.Logger) *ChrootExecutor { |
| 19 | return &ChrootExecutor{ | 21 | return &ChrootExecutor{ | ||
| 20 | - | cmdExecutor: NewShellExecutor(), | 22 | + | logger: logger, |
| 21 | } | 23 | } | ||
| 22 | } | 24 | } | ||
| 23 | 25 | ||||
M internal/infrastructure/executor/chroot_test.go +66 -11
| @@ -5,21 +5,36 @@ import ( | |||
| 5 | 5 | "os" | |
| 6 | 6 | "path/filepath" | |
| 7 | 7 | "testing" | |
| 8 | + | ||
| 9 | + | "github.com/bnema/archup/internal/domain/ports/mocks" | |
| 10 | + | "github.com/golang/mock/gomock" | |
| 8 | 11 | ) | |
| 9 | 12 | ||
| 10 | 13 | func TestChrootExecutor_NewChrootExecutor(t *testing.T) { | |
| 11 | - | executor := NewChrootExecutor() | |
| 14 | + | ctrl := gomock.NewController(t) | |
| 15 | + | defer ctrl.Finish() | |
| 16 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 17 | + | mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() | |
| 18 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 19 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |
| 20 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | |
| 21 | + | ||
| 22 | + | executor := NewChrootExecutor(mockLogger) | |
| 12 | 23 | if executor == nil { | |
| 13 | 24 | t.Fatal("expected non-nil executor") | |
| 14 | 25 | } | |
| 15 | - | ||
| 16 | - | if executor.cmdExecutor == nil { | |
| 17 | - | t.Fatal("expected non-nil cmdExecutor") | |
| 18 | - | } | |
| 19 | 26 | } | |
| 20 | 27 | ||
| 21 | 28 | func TestChrootExecutor_ExecuteInChroot_PathNotExists(t *testing.T) { | |
| 22 | - | executor := NewChrootExecutor() | |
| 29 | + | ctrl := gomock.NewController(t) | |
| 30 | + | defer ctrl.Finish() | |
| 31 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 32 | + | mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() | |
| 33 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 34 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |
| 35 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | |
| 36 | + | ||
| 37 | + | executor := NewChrootExecutor(mockLogger) | |
| 23 | 38 | ctx := context.Background() | |
| 24 | 39 | ||
| 25 | 40 | _, err := executor.ExecuteInChroot(ctx, "/nonexistent/path", "echo", "test") | |
| @@ -29,7 +44,15 @@ func TestChrootExecutor_ExecuteInChroot_PathNotExists(t *testing.T) { | |||
| 29 | 44 | } | |
| 30 | 45 | ||
| 31 | 46 | func TestChrootExecutor_ExecuteInChroot_CommandNotFound(t *testing.T) { | |
| 32 | - | executor := NewChrootExecutor() | |
| 47 | + | ctrl := gomock.NewController(t) | |
| 48 | + | defer ctrl.Finish() | |
| 49 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 50 | + | mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() | |
| 51 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 52 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |
| 53 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | |
| 54 | + | ||
| 55 | + | executor := NewChrootExecutor(mockLogger) | |
| 33 | 56 | ctx := context.Background() | |
| 34 | 57 | ||
| 35 | 58 | // Create temporary directory to use as chroot | |
| @@ -44,7 +67,15 @@ func TestChrootExecutor_ExecuteInChroot_CommandNotFound(t *testing.T) { | |||
| 44 | 67 | } | |
| 45 | 68 | ||
| 46 | 69 | func TestChrootExecutor_ChrootSystemctl_PathNotExists(t *testing.T) { | |
| 47 | - | executor := NewChrootExecutor() | |
| 70 | + | ctrl := gomock.NewController(t) | |
| 71 | + | defer ctrl.Finish() | |
| 72 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 73 | + | mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() | |
| 74 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 75 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |
| 76 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | |
| 77 | + | ||
| 78 | + | executor := NewChrootExecutor(mockLogger) | |
| 48 | 79 | ||
| 49 | 80 | err := executor.ChrootSystemctl("", "/nonexistent/path", "enable", "service") | |
| 50 | 81 | if err == nil { | |
| @@ -53,7 +84,15 @@ func TestChrootExecutor_ChrootSystemctl_PathNotExists(t *testing.T) { | |||
| 53 | 84 | } | |
| 54 | 85 | ||
| 55 | 86 | func TestChrootExecutor_ChrootSystemctl_CreatesLogDirectory(t *testing.T) { | |
| 56 | - | executor := NewChrootExecutor() | |
| 87 | + | ctrl := gomock.NewController(t) | |
| 88 | + | defer ctrl.Finish() | |
| 89 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 90 | + | mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() | |
| 91 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 92 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |
| 93 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | |
| 94 | + | ||
| 95 | + | executor := NewChrootExecutor(mockLogger) | |
| 57 | 96 | tmpDir := t.TempDir() | |
| 58 | 97 | logPath := filepath.Join(tmpDir, "subdir", "log.txt") | |
| 59 | 98 | ||
| @@ -69,7 +108,15 @@ func TestChrootExecutor_ChrootSystemctl_CreatesLogDirectory(t *testing.T) { | |||
| 69 | 108 | } | |
| 70 | 109 | ||
| 71 | 110 | func TestChrootExecutor_ChrootSystemctl_LogsErrors(t *testing.T) { | |
| 72 | - | executor := NewChrootExecutor() | |
| 111 | + | ctrl := gomock.NewController(t) | |
| 112 | + | defer ctrl.Finish() | |
| 113 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 114 | + | mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() | |
| 115 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 116 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |
| 117 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | |
| 118 | + | ||
| 119 | + | executor := NewChrootExecutor(mockLogger) | |
| 73 | 120 | tmpDir := t.TempDir() | |
| 74 | 121 | logPath := filepath.Join(tmpDir, "log.txt") | |
| 75 | 122 | ||
| @@ -83,7 +130,15 @@ func TestChrootExecutor_ChrootSystemctl_LogsErrors(t *testing.T) { | |||
| 83 | 130 | } | |
| 84 | 131 | ||
| 85 | 132 | func TestChrootExecutor_ChrootSystemctl_EmptyLogPath(t *testing.T) { | |
| 86 | - | executor := NewChrootExecutor() | |
| 133 | + | ctrl := gomock.NewController(t) | |
| 134 | + | defer ctrl.Finish() | |
| 135 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 136 | + | mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() | |
| 137 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 138 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |
| 139 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | |
| 140 | + | ||
| 141 | + | executor := NewChrootExecutor(mockLogger) | |
| 87 | 142 | tmpDir := t.TempDir() | |
| 88 | 143 | ||
| 89 | 144 | // This should not panic even with empty log path | |
| @@ -5,21 +5,36 @@ import ( | |||||
| 5 | "os" | 5 | "os" | ||
| 6 | "path/filepath" | 6 | "path/filepath" | ||
| 7 | "testing" | 7 | "testing" | ||
| 8 | + | ||||
| 9 | + | "github.com/bnema/archup/internal/domain/ports/mocks" | |||
| 10 | + | "github.com/golang/mock/gomock" | |||
| 8 | ) | 11 | ) | ||
| 9 | 12 | ||||
| 10 | func TestChrootExecutor_NewChrootExecutor(t *testing.T) { | 13 | func TestChrootExecutor_NewChrootExecutor(t *testing.T) { | ||
| 11 | - | executor := NewChrootExecutor() | 14 | + | ctrl := gomock.NewController(t) |
| 15 | + | defer ctrl.Finish() | |||
| 16 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 17 | + | mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 18 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 19 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 20 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 21 | + | ||||
| 22 | + | executor := NewChrootExecutor(mockLogger) | |||
| 12 | if executor == nil { | 23 | if executor == nil { | ||
| 13 | t.Fatal("expected non-nil executor") | 24 | t.Fatal("expected non-nil executor") | ||
| 14 | } | 25 | } | ||
| 15 | - | ||||
| 16 | - | if executor.cmdExecutor == nil { | |||
| 17 | - | t.Fatal("expected non-nil cmdExecutor") | |||
| 18 | - | } | |||
| 19 | } | 26 | } | ||
| 20 | 27 | ||||
| 21 | func TestChrootExecutor_ExecuteInChroot_PathNotExists(t *testing.T) { | 28 | func TestChrootExecutor_ExecuteInChroot_PathNotExists(t *testing.T) { | ||
| 22 | - | executor := NewChrootExecutor() | 29 | + | ctrl := gomock.NewController(t) |
| 30 | + | defer ctrl.Finish() | |||
| 31 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 32 | + | mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 33 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 34 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 35 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 36 | + | ||||
| 37 | + | executor := NewChrootExecutor(mockLogger) | |||
| 23 | ctx := context.Background() | 38 | ctx := context.Background() | ||
| 24 | 39 | ||||
| 25 | _, err := executor.ExecuteInChroot(ctx, "/nonexistent/path", "echo", "test") | 40 | _, err := executor.ExecuteInChroot(ctx, "/nonexistent/path", "echo", "test") | ||
| @@ -29,7 +44,15 @@ func TestChrootExecutor_ExecuteInChroot_PathNotExists(t *testing.T) { | |||||
| 29 | } | 44 | } | ||
| 30 | 45 | ||||
| 31 | func TestChrootExecutor_ExecuteInChroot_CommandNotFound(t *testing.T) { | 46 | func TestChrootExecutor_ExecuteInChroot_CommandNotFound(t *testing.T) { | ||
| 32 | - | executor := NewChrootExecutor() | 47 | + | ctrl := gomock.NewController(t) |
| 48 | + | defer ctrl.Finish() | |||
| 49 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 50 | + | mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 51 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 52 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 53 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 54 | + | ||||
| 55 | + | executor := NewChrootExecutor(mockLogger) | |||
| 33 | ctx := context.Background() | 56 | ctx := context.Background() | ||
| 34 | 57 | ||||
| 35 | // Create temporary directory to use as chroot | 58 | // Create temporary directory to use as chroot | ||
| @@ -44,7 +67,15 @@ func TestChrootExecutor_ExecuteInChroot_CommandNotFound(t *testing.T) { | |||||
| 44 | } | 67 | } | ||
| 45 | 68 | ||||
| 46 | func TestChrootExecutor_ChrootSystemctl_PathNotExists(t *testing.T) { | 69 | func TestChrootExecutor_ChrootSystemctl_PathNotExists(t *testing.T) { | ||
| 47 | - | executor := NewChrootExecutor() | 70 | + | ctrl := gomock.NewController(t) |
| 71 | + | defer ctrl.Finish() | |||
| 72 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 73 | + | mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 74 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 75 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 76 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 77 | + | ||||
| 78 | + | executor := NewChrootExecutor(mockLogger) | |||
| 48 | 79 | ||||
| 49 | err := executor.ChrootSystemctl("", "/nonexistent/path", "enable", "service") | 80 | err := executor.ChrootSystemctl("", "/nonexistent/path", "enable", "service") | ||
| 50 | if err == nil { | 81 | if err == nil { | ||
| @@ -53,7 +84,15 @@ func TestChrootExecutor_ChrootSystemctl_PathNotExists(t *testing.T) { | |||||
| 53 | } | 84 | } | ||
| 54 | 85 | ||||
| 55 | func TestChrootExecutor_ChrootSystemctl_CreatesLogDirectory(t *testing.T) { | 86 | func TestChrootExecutor_ChrootSystemctl_CreatesLogDirectory(t *testing.T) { | ||
| 56 | - | executor := NewChrootExecutor() | 87 | + | ctrl := gomock.NewController(t) |
| 88 | + | defer ctrl.Finish() | |||
| 89 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 90 | + | mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 91 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 92 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 93 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 94 | + | ||||
| 95 | + | executor := NewChrootExecutor(mockLogger) | |||
| 57 | tmpDir := t.TempDir() | 96 | tmpDir := t.TempDir() | ||
| 58 | logPath := filepath.Join(tmpDir, "subdir", "log.txt") | 97 | logPath := filepath.Join(tmpDir, "subdir", "log.txt") | ||
| 59 | 98 | ||||
| @@ -69,7 +108,15 @@ func TestChrootExecutor_ChrootSystemctl_CreatesLogDirectory(t *testing.T) { | |||||
| 69 | } | 108 | } | ||
| 70 | 109 | ||||
| 71 | func TestChrootExecutor_ChrootSystemctl_LogsErrors(t *testing.T) { | 110 | func TestChrootExecutor_ChrootSystemctl_LogsErrors(t *testing.T) { | ||
| 72 | - | executor := NewChrootExecutor() | 111 | + | ctrl := gomock.NewController(t) |
| 112 | + | defer ctrl.Finish() | |||
| 113 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 114 | + | mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 115 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 116 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 117 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 118 | + | ||||
| 119 | + | executor := NewChrootExecutor(mockLogger) | |||
| 73 | tmpDir := t.TempDir() | 120 | tmpDir := t.TempDir() | ||
| 74 | logPath := filepath.Join(tmpDir, "log.txt") | 121 | logPath := filepath.Join(tmpDir, "log.txt") | ||
| 75 | 122 | ||||
| @@ -83,7 +130,15 @@ func TestChrootExecutor_ChrootSystemctl_LogsErrors(t *testing.T) { | |||||
| 83 | } | 130 | } | ||
| 84 | 131 | ||||
| 85 | func TestChrootExecutor_ChrootSystemctl_EmptyLogPath(t *testing.T) { | 132 | func TestChrootExecutor_ChrootSystemctl_EmptyLogPath(t *testing.T) { | ||
| 86 | - | executor := NewChrootExecutor() | 133 | + | ctrl := gomock.NewController(t) |
| 134 | + | defer ctrl.Finish() | |||
| 135 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 136 | + | mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 137 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 138 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 139 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 140 | + | ||||
| 141 | + | executor := NewChrootExecutor(mockLogger) | |||
| 87 | tmpDir := t.TempDir() | 142 | tmpDir := t.TempDir() | ||
| 88 | 143 | ||||
| 89 | // This should not panic even with empty log path | 144 | // This should not panic even with empty log path | ||
M internal/infrastructure/executor/script.go +14 -4
| @@ -6,14 +6,24 @@ import ( | |||
| 6 | 6 | "fmt" | |
| 7 | 7 | "os" | |
| 8 | 8 | "os/exec" | |
| 9 | + | ||
| 10 | + | "github.com/bnema/archup/internal/domain/ports" | |
| 9 | 11 | ) | |
| 10 | 12 | ||
| 11 | 13 | // ScriptExecutor implements the ScriptExecutor port for shell scripts | |
| 12 | - | type ScriptExecutor struct{} | |
| 14 | + | type ScriptExecutor struct { | |
| 15 | + | fs ports.FileSystem | |
| 16 | + | cmdExec ports.CommandExecutor | |
| 17 | + | scriptDir string | |
| 18 | + | } | |
| 13 | 19 | ||
| 14 | - | // NewScriptExecutor creates a new script executor | |
| 15 | - | func NewScriptExecutor() *ScriptExecutor { | |
| 16 | - | return &ScriptExecutor{} | |
| 20 | + | // NewScriptExecutor creates a new script executor with dependencies | |
| 21 | + | func NewScriptExecutor(fs ports.FileSystem, cmdExec ports.CommandExecutor, scriptDir string) *ScriptExecutor { | |
| 22 | + | return &ScriptExecutor{ | |
| 23 | + | fs: fs, | |
| 24 | + | cmdExec: cmdExec, | |
| 25 | + | scriptDir: scriptDir, | |
| 26 | + | } | |
| 17 | 27 | } | |
| 18 | 28 | ||
| 19 | 29 | // ExecuteScript runs a shell script with environment variables | |
| @@ -6,14 +6,24 @@ import ( | |||||
| 6 | "fmt" | 6 | "fmt" | ||
| 7 | "os" | 7 | "os" | ||
| 8 | "os/exec" | 8 | "os/exec" | ||
| 9 | + | ||||
| 10 | + | "github.com/bnema/archup/internal/domain/ports" | |||
| 9 | ) | 11 | ) | ||
| 10 | 12 | ||||
| 11 | // ScriptExecutor implements the ScriptExecutor port for shell scripts | 13 | // ScriptExecutor implements the ScriptExecutor port for shell scripts | ||
| 12 | - | type ScriptExecutor struct{} | 14 | + | type ScriptExecutor struct { |
| 15 | + | fs ports.FileSystem | |||
| 16 | + | cmdExec ports.CommandExecutor | |||
| 17 | + | scriptDir string | |||
| 18 | + | } | |||
| 13 | 19 | ||||
| 14 | - | // NewScriptExecutor creates a new script executor | 20 | + | // NewScriptExecutor creates a new script executor with dependencies |
| 15 | - | func NewScriptExecutor() *ScriptExecutor { | 21 | + | func NewScriptExecutor(fs ports.FileSystem, cmdExec ports.CommandExecutor, scriptDir string) *ScriptExecutor { |
| 16 | - | return &ScriptExecutor{} | 22 | + | return &ScriptExecutor{ |
| 23 | + | fs: fs, | |||
| 24 | + | cmdExec: cmdExec, | |||
| 25 | + | scriptDir: scriptDir, | |||
| 26 | + | } | |||
| 17 | } | 27 | } | ||
| 18 | 28 | ||||
| 19 | // ExecuteScript runs a shell script with environment variables | 29 | // ExecuteScript runs a shell script with environment variables | ||
M internal/infrastructure/executor/script_test.go +51 -8
| @@ -5,17 +5,30 @@ import ( | |||
| 5 | 5 | "os" | |
| 6 | 6 | "path/filepath" | |
| 7 | 7 | "testing" | |
| 8 | + | ||
| 9 | + | "github.com/bnema/archup/internal/domain/ports/mocks" | |
| 10 | + | "github.com/golang/mock/gomock" | |
| 8 | 11 | ) | |
| 9 | 12 | ||
| 10 | 13 | func TestScriptExecutor_NewScriptExecutor(t *testing.T) { | |
| 11 | - | executor := NewScriptExecutor() | |
| 14 | + | ctrl := gomock.NewController(t) | |
| 15 | + | defer ctrl.Finish() | |
| 16 | + | mockFS := mocks.NewMockFileSystem(ctrl) | |
| 17 | + | mockCmdExec := mocks.NewMockCommandExecutor(ctrl) | |
| 18 | + | ||
| 19 | + | executor := NewScriptExecutor(mockFS, mockCmdExec, "/test/scripts") | |
| 12 | 20 | if executor == nil { | |
| 13 | 21 | t.Fatal("expected non-nil executor") | |
| 14 | 22 | } | |
| 15 | 23 | } | |
| 16 | 24 | ||
| 17 | 25 | func TestScriptExecutor_ExecuteScript_Success(t *testing.T) { | |
| 18 | - | executor := NewScriptExecutor() | |
| 26 | + | ctrl := gomock.NewController(t) | |
| 27 | + | defer ctrl.Finish() | |
| 28 | + | mockFS := mocks.NewMockFileSystem(ctrl) | |
| 29 | + | mockCmdExec := mocks.NewMockCommandExecutor(ctrl) | |
| 30 | + | ||
| 31 | + | executor := NewScriptExecutor(mockFS, mockCmdExec, "/test/scripts") | |
| 19 | 32 | tmpDir := t.TempDir() | |
| 20 | 33 | ||
| 21 | 34 | // Create a simple test script | |
| @@ -33,7 +46,12 @@ func TestScriptExecutor_ExecuteScript_Success(t *testing.T) { | |||
| 33 | 46 | } | |
| 34 | 47 | ||
| 35 | 48 | func TestScriptExecutor_ExecuteScript_NotFound(t *testing.T) { | |
| 36 | - | executor := NewScriptExecutor() | |
| 49 | + | ctrl := gomock.NewController(t) | |
| 50 | + | defer ctrl.Finish() | |
| 51 | + | mockFS := mocks.NewMockFileSystem(ctrl) | |
| 52 | + | mockCmdExec := mocks.NewMockCommandExecutor(ctrl) | |
| 53 | + | ||
| 54 | + | executor := NewScriptExecutor(mockFS, mockCmdExec, "/test/scripts") | |
| 37 | 55 | ctx := context.Background() | |
| 38 | 56 | ||
| 39 | 57 | err := executor.ExecuteScript(ctx, "/nonexistent/script.sh", nil) | |
| @@ -43,7 +61,12 @@ func TestScriptExecutor_ExecuteScript_NotFound(t *testing.T) { | |||
| 43 | 61 | } | |
| 44 | 62 | ||
| 45 | 63 | func TestScriptExecutor_ExecuteScript_Failure(t *testing.T) { | |
| 46 | - | executor := NewScriptExecutor() | |
| 64 | + | ctrl := gomock.NewController(t) | |
| 65 | + | defer ctrl.Finish() | |
| 66 | + | mockFS := mocks.NewMockFileSystem(ctrl) | |
| 67 | + | mockCmdExec := mocks.NewMockCommandExecutor(ctrl) | |
| 68 | + | ||
| 69 | + | executor := NewScriptExecutor(mockFS, mockCmdExec, "/test/scripts") | |
| 47 | 70 | tmpDir := t.TempDir() | |
| 48 | 71 | ||
| 49 | 72 | // Create a script that fails | |
| @@ -61,7 +84,12 @@ func TestScriptExecutor_ExecuteScript_Failure(t *testing.T) { | |||
| 61 | 84 | } | |
| 62 | 85 | ||
| 63 | 86 | func TestScriptExecutor_ExecuteScript_WithEnv(t *testing.T) { | |
| 64 | - | executor := NewScriptExecutor() | |
| 87 | + | ctrl := gomock.NewController(t) | |
| 88 | + | defer ctrl.Finish() | |
| 89 | + | mockFS := mocks.NewMockFileSystem(ctrl) | |
| 90 | + | mockCmdExec := mocks.NewMockCommandExecutor(ctrl) | |
| 91 | + | ||
| 92 | + | executor := NewScriptExecutor(mockFS, mockCmdExec, "/test/scripts") | |
| 65 | 93 | tmpDir := t.TempDir() | |
| 66 | 94 | ||
| 67 | 95 | // Create a script that uses environment variables | |
| @@ -83,7 +111,12 @@ func TestScriptExecutor_ExecuteScript_WithEnv(t *testing.T) { | |||
| 83 | 111 | } | |
| 84 | 112 | ||
| 85 | 113 | func TestScriptExecutor_ExecuteScript_WithMultipleEnv(t *testing.T) { | |
| 86 | - | executor := NewScriptExecutor() | |
| 114 | + | ctrl := gomock.NewController(t) | |
| 115 | + | defer ctrl.Finish() | |
| 116 | + | mockFS := mocks.NewMockFileSystem(ctrl) | |
| 117 | + | mockCmdExec := mocks.NewMockCommandExecutor(ctrl) | |
| 118 | + | ||
| 119 | + | executor := NewScriptExecutor(mockFS, mockCmdExec, "/test/scripts") | |
| 87 | 120 | tmpDir := t.TempDir() | |
| 88 | 121 | ||
| 89 | 122 | // Create a script that checks multiple environment variables | |
| @@ -106,7 +139,12 @@ func TestScriptExecutor_ExecuteScript_WithMultipleEnv(t *testing.T) { | |||
| 106 | 139 | } | |
| 107 | 140 | ||
| 108 | 141 | func TestScriptExecutor_ExecuteScript_Stderr(t *testing.T) { | |
| 109 | - | executor := NewScriptExecutor() | |
| 142 | + | ctrl := gomock.NewController(t) | |
| 143 | + | defer ctrl.Finish() | |
| 144 | + | mockFS := mocks.NewMockFileSystem(ctrl) | |
| 145 | + | mockCmdExec := mocks.NewMockCommandExecutor(ctrl) | |
| 146 | + | ||
| 147 | + | executor := NewScriptExecutor(mockFS, mockCmdExec, "/test/scripts") | |
| 110 | 148 | tmpDir := t.TempDir() | |
| 111 | 149 | ||
| 112 | 150 | // Create a script that writes to stderr | |
| @@ -129,7 +167,12 @@ func TestScriptExecutor_ExecuteScript_Stderr(t *testing.T) { | |||
| 129 | 167 | } | |
| 130 | 168 | ||
| 131 | 169 | func TestScriptExecutor_ExecuteScript_ContextCancellation(t *testing.T) { | |
| 132 | - | executor := NewScriptExecutor() | |
| 170 | + | ctrl := gomock.NewController(t) | |
| 171 | + | defer ctrl.Finish() | |
| 172 | + | mockFS := mocks.NewMockFileSystem(ctrl) | |
| 173 | + | mockCmdExec := mocks.NewMockCommandExecutor(ctrl) | |
| 174 | + | ||
| 175 | + | executor := NewScriptExecutor(mockFS, mockCmdExec, "/test/scripts") | |
| 133 | 176 | tmpDir := t.TempDir() | |
| 134 | 177 | ||
| 135 | 178 | // Create a script that sleeps | |
| @@ -5,17 +5,30 @@ import ( | |||||
| 5 | "os" | 5 | "os" | ||
| 6 | "path/filepath" | 6 | "path/filepath" | ||
| 7 | "testing" | 7 | "testing" | ||
| 8 | + | ||||
| 9 | + | "github.com/bnema/archup/internal/domain/ports/mocks" | |||
| 10 | + | "github.com/golang/mock/gomock" | |||
| 8 | ) | 11 | ) | ||
| 9 | 12 | ||||
| 10 | func TestScriptExecutor_NewScriptExecutor(t *testing.T) { | 13 | func TestScriptExecutor_NewScriptExecutor(t *testing.T) { | ||
| 11 | - | executor := NewScriptExecutor() | 14 | + | ctrl := gomock.NewController(t) |
| 15 | + | defer ctrl.Finish() | |||
| 16 | + | mockFS := mocks.NewMockFileSystem(ctrl) | |||
| 17 | + | mockCmdExec := mocks.NewMockCommandExecutor(ctrl) | |||
| 18 | + | ||||
| 19 | + | executor := NewScriptExecutor(mockFS, mockCmdExec, "/test/scripts") | |||
| 12 | if executor == nil { | 20 | if executor == nil { | ||
| 13 | t.Fatal("expected non-nil executor") | 21 | t.Fatal("expected non-nil executor") | ||
| 14 | } | 22 | } | ||
| 15 | } | 23 | } | ||
| 16 | 24 | ||||
| 17 | func TestScriptExecutor_ExecuteScript_Success(t *testing.T) { | 25 | func TestScriptExecutor_ExecuteScript_Success(t *testing.T) { | ||
| 18 | - | executor := NewScriptExecutor() | 26 | + | ctrl := gomock.NewController(t) |
| 27 | + | defer ctrl.Finish() | |||
| 28 | + | mockFS := mocks.NewMockFileSystem(ctrl) | |||
| 29 | + | mockCmdExec := mocks.NewMockCommandExecutor(ctrl) | |||
| 30 | + | ||||
| 31 | + | executor := NewScriptExecutor(mockFS, mockCmdExec, "/test/scripts") | |||
| 19 | tmpDir := t.TempDir() | 32 | tmpDir := t.TempDir() | ||
| 20 | 33 | ||||
| 21 | // Create a simple test script | 34 | // Create a simple test script | ||
| @@ -33,7 +46,12 @@ func TestScriptExecutor_ExecuteScript_Success(t *testing.T) { | |||||
| 33 | } | 46 | } | ||
| 34 | 47 | ||||
| 35 | func TestScriptExecutor_ExecuteScript_NotFound(t *testing.T) { | 48 | func TestScriptExecutor_ExecuteScript_NotFound(t *testing.T) { | ||
| 36 | - | executor := NewScriptExecutor() | 49 | + | ctrl := gomock.NewController(t) |
| 50 | + | defer ctrl.Finish() | |||
| 51 | + | mockFS := mocks.NewMockFileSystem(ctrl) | |||
| 52 | + | mockCmdExec := mocks.NewMockCommandExecutor(ctrl) | |||
| 53 | + | ||||
| 54 | + | executor := NewScriptExecutor(mockFS, mockCmdExec, "/test/scripts") | |||
| 37 | ctx := context.Background() | 55 | ctx := context.Background() | ||
| 38 | 56 | ||||
| 39 | err := executor.ExecuteScript(ctx, "/nonexistent/script.sh", nil) | 57 | err := executor.ExecuteScript(ctx, "/nonexistent/script.sh", nil) | ||
| @@ -43,7 +61,12 @@ func TestScriptExecutor_ExecuteScript_NotFound(t *testing.T) { | |||||
| 43 | } | 61 | } | ||
| 44 | 62 | ||||
| 45 | func TestScriptExecutor_ExecuteScript_Failure(t *testing.T) { | 63 | func TestScriptExecutor_ExecuteScript_Failure(t *testing.T) { | ||
| 46 | - | executor := NewScriptExecutor() | 64 | + | ctrl := gomock.NewController(t) |
| 65 | + | defer ctrl.Finish() | |||
| 66 | + | mockFS := mocks.NewMockFileSystem(ctrl) | |||
| 67 | + | mockCmdExec := mocks.NewMockCommandExecutor(ctrl) | |||
| 68 | + | ||||
| 69 | + | executor := NewScriptExecutor(mockFS, mockCmdExec, "/test/scripts") | |||
| 47 | tmpDir := t.TempDir() | 70 | tmpDir := t.TempDir() | ||
| 48 | 71 | ||||
| 49 | // Create a script that fails | 72 | // Create a script that fails | ||
| @@ -61,7 +84,12 @@ func TestScriptExecutor_ExecuteScript_Failure(t *testing.T) { | |||||
| 61 | } | 84 | } | ||
| 62 | 85 | ||||
| 63 | func TestScriptExecutor_ExecuteScript_WithEnv(t *testing.T) { | 86 | func TestScriptExecutor_ExecuteScript_WithEnv(t *testing.T) { | ||
| 64 | - | executor := NewScriptExecutor() | 87 | + | ctrl := gomock.NewController(t) |
| 88 | + | defer ctrl.Finish() | |||
| 89 | + | mockFS := mocks.NewMockFileSystem(ctrl) | |||
| 90 | + | mockCmdExec := mocks.NewMockCommandExecutor(ctrl) | |||
| 91 | + | ||||
| 92 | + | executor := NewScriptExecutor(mockFS, mockCmdExec, "/test/scripts") | |||
| 65 | tmpDir := t.TempDir() | 93 | tmpDir := t.TempDir() | ||
| 66 | 94 | ||||
| 67 | // Create a script that uses environment variables | 95 | // Create a script that uses environment variables | ||
| @@ -83,7 +111,12 @@ func TestScriptExecutor_ExecuteScript_WithEnv(t *testing.T) { | |||||
| 83 | } | 111 | } | ||
| 84 | 112 | ||||
| 85 | func TestScriptExecutor_ExecuteScript_WithMultipleEnv(t *testing.T) { | 113 | func TestScriptExecutor_ExecuteScript_WithMultipleEnv(t *testing.T) { | ||
| 86 | - | executor := NewScriptExecutor() | 114 | + | ctrl := gomock.NewController(t) |
| 115 | + | defer ctrl.Finish() | |||
| 116 | + | mockFS := mocks.NewMockFileSystem(ctrl) | |||
| 117 | + | mockCmdExec := mocks.NewMockCommandExecutor(ctrl) | |||
| 118 | + | ||||
| 119 | + | executor := NewScriptExecutor(mockFS, mockCmdExec, "/test/scripts") | |||
| 87 | tmpDir := t.TempDir() | 120 | tmpDir := t.TempDir() | ||
| 88 | 121 | ||||
| 89 | // Create a script that checks multiple environment variables | 122 | // Create a script that checks multiple environment variables | ||
| @@ -106,7 +139,12 @@ func TestScriptExecutor_ExecuteScript_WithMultipleEnv(t *testing.T) { | |||||
| 106 | } | 139 | } | ||
| 107 | 140 | ||||
| 108 | func TestScriptExecutor_ExecuteScript_Stderr(t *testing.T) { | 141 | func TestScriptExecutor_ExecuteScript_Stderr(t *testing.T) { | ||
| 109 | - | executor := NewScriptExecutor() | 142 | + | ctrl := gomock.NewController(t) |
| 143 | + | defer ctrl.Finish() | |||
| 144 | + | mockFS := mocks.NewMockFileSystem(ctrl) | |||
| 145 | + | mockCmdExec := mocks.NewMockCommandExecutor(ctrl) | |||
| 146 | + | ||||
| 147 | + | executor := NewScriptExecutor(mockFS, mockCmdExec, "/test/scripts") | |||
| 110 | tmpDir := t.TempDir() | 148 | tmpDir := t.TempDir() | ||
| 111 | 149 | ||||
| 112 | // Create a script that writes to stderr | 150 | // Create a script that writes to stderr | ||
| @@ -129,7 +167,12 @@ func TestScriptExecutor_ExecuteScript_Stderr(t *testing.T) { | |||||
| 129 | } | 167 | } | ||
| 130 | 168 | ||||
| 131 | func TestScriptExecutor_ExecuteScript_ContextCancellation(t *testing.T) { | 169 | func TestScriptExecutor_ExecuteScript_ContextCancellation(t *testing.T) { | ||
| 132 | - | executor := NewScriptExecutor() | 170 | + | ctrl := gomock.NewController(t) |
| 171 | + | defer ctrl.Finish() | |||
| 172 | + | mockFS := mocks.NewMockFileSystem(ctrl) | |||
| 173 | + | mockCmdExec := mocks.NewMockCommandExecutor(ctrl) | |||
| 174 | + | ||||
| 175 | + | executor := NewScriptExecutor(mockFS, mockCmdExec, "/test/scripts") | |||
| 133 | tmpDir := t.TempDir() | 176 | tmpDir := t.TempDir() | ||
| 134 | 177 | ||||
| 135 | // Create a script that sleeps | 178 | // Create a script that sleeps | ||
M internal/infrastructure/executor/shell.go +10 -4
| @@ -6,14 +6,20 @@ import ( | |||
| 6 | 6 | "fmt" | |
| 7 | 7 | "os" | |
| 8 | 8 | "os/exec" | |
| 9 | + | ||
| 10 | + | "github.com/bnema/archup/internal/domain/ports" | |
| 9 | 11 | ) | |
| 10 | 12 | ||
| 11 | 13 | // ShellExecutor implements the CommandExecutor port using system shell commands | |
| 12 | - | type ShellExecutor struct{} | |
| 14 | + | type ShellExecutor struct { | |
| 15 | + | logger ports.Logger | |
| 16 | + | } | |
| 13 | 17 | ||
| 14 | - | // NewShellExecutor creates a new shell executor | |
| 15 | - | func NewShellExecutor() *ShellExecutor { | |
| 16 | - | return &ShellExecutor{} | |
| 18 | + | // NewShellExecutor creates a new shell executor with a logger | |
| 19 | + | func NewShellExecutor(logger ports.Logger) *ShellExecutor { | |
| 20 | + | return &ShellExecutor{ | |
| 21 | + | logger: logger, | |
| 22 | + | } | |
| 17 | 23 | } | |
| 18 | 24 | ||
| 19 | 25 | // Execute runs a command and returns output | |
| @@ -6,14 +6,20 @@ import ( | |||||
| 6 | "fmt" | 6 | "fmt" | ||
| 7 | "os" | 7 | "os" | ||
| 8 | "os/exec" | 8 | "os/exec" | ||
| 9 | + | ||||
| 10 | + | "github.com/bnema/archup/internal/domain/ports" | |||
| 9 | ) | 11 | ) | ||
| 10 | 12 | ||||
| 11 | // ShellExecutor implements the CommandExecutor port using system shell commands | 13 | // ShellExecutor implements the CommandExecutor port using system shell commands | ||
| 12 | - | type ShellExecutor struct{} | 14 | + | type ShellExecutor struct { |
| 15 | + | logger ports.Logger | |||
| 16 | + | } | |||
| 13 | 17 | ||||
| 14 | - | // NewShellExecutor creates a new shell executor | 18 | + | // NewShellExecutor creates a new shell executor with a logger |
| 15 | - | func NewShellExecutor() *ShellExecutor { | 19 | + | func NewShellExecutor(logger ports.Logger) *ShellExecutor { |
| 16 | - | return &ShellExecutor{} | 20 | + | return &ShellExecutor{ |
| 21 | + | logger: logger, | |||
| 22 | + | } | |||
| 17 | } | 23 | } | ||
| 18 | 24 | ||||
| 19 | // Execute runs a command and returns output | 25 | // Execute runs a command and returns output | ||
M internal/infrastructure/executor/shell_test.go +94 -9
| @@ -5,10 +5,21 @@ import ( | |||
| 5 | 5 | "strings" | |
| 6 | 6 | "testing" | |
| 7 | 7 | "time" | |
| 8 | + | ||
| 9 | + | "github.com/bnema/archup/internal/domain/ports/mocks" | |
| 10 | + | "github.com/golang/mock/gomock" | |
| 8 | 11 | ) | |
| 9 | 12 | ||
| 10 | 13 | func TestShellExecutor_Execute_Success(t *testing.T) { | |
| 11 | - | executor := NewShellExecutor() | |
| 14 | + | ctrl := gomock.NewController(t) | |
| 15 | + | defer ctrl.Finish() | |
| 16 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 17 | + | mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() | |
| 18 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 19 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |
| 20 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | |
| 21 | + | ||
| 22 | + | executor := NewShellExecutor(mockLogger) | |
| 12 | 23 | ctx := context.Background() | |
| 13 | 24 | ||
| 14 | 25 | output, err := executor.Execute(ctx, "echo", "hello world") | |
| @@ -23,7 +34,15 @@ func TestShellExecutor_Execute_Success(t *testing.T) { | |||
| 23 | 34 | } | |
| 24 | 35 | ||
| 25 | 36 | func TestShellExecutor_Execute_WithArgs(t *testing.T) { | |
| 26 | - | executor := NewShellExecutor() | |
| 37 | + | ctrl := gomock.NewController(t) | |
| 38 | + | defer ctrl.Finish() | |
| 39 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 40 | + | mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() | |
| 41 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 42 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |
| 43 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | |
| 44 | + | ||
| 45 | + | executor := NewShellExecutor(mockLogger) | |
| 27 | 46 | ctx := context.Background() | |
| 28 | 47 | ||
| 29 | 48 | // Test with multiple arguments | |
| @@ -37,8 +56,26 @@ func TestShellExecutor_Execute_WithArgs(t *testing.T) { | |||
| 37 | 56 | } | |
| 38 | 57 | } | |
| 39 | 58 | ||
| 59 | + | func newMockLogger() *mocks.MockLogger { | |
| 60 | + | ctrl := gomock.NewController(&testing.T{}) | |
| 61 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 62 | + | mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() | |
| 63 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 64 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |
| 65 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | |
| 66 | + | return mockLogger | |
| 67 | + | } | |
| 68 | + | ||
| 40 | 69 | func TestShellExecutor_Execute_CommandNotFound(t *testing.T) { | |
| 41 | - | executor := NewShellExecutor() | |
| 70 | + | ctrl := gomock.NewController(t) | |
| 71 | + | defer ctrl.Finish() | |
| 72 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 73 | + | mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() | |
| 74 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 75 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |
| 76 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | |
| 77 | + | ||
| 78 | + | executor := NewShellExecutor(mockLogger) | |
| 42 | 79 | ctx := context.Background() | |
| 43 | 80 | ||
| 44 | 81 | _, err := executor.Execute(ctx, "nonexistentcommand12345", "arg") | |
| @@ -48,7 +85,15 @@ func TestShellExecutor_Execute_CommandNotFound(t *testing.T) { | |||
| 48 | 85 | } | |
| 49 | 86 | ||
| 50 | 87 | func TestShellExecutor_Execute_CommandFailure(t *testing.T) { | |
| 51 | - | executor := NewShellExecutor() | |
| 88 | + | ctrl := gomock.NewController(t) | |
| 89 | + | defer ctrl.Finish() | |
| 90 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 91 | + | mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() | |
| 92 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 93 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |
| 94 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | |
| 95 | + | ||
| 96 | + | executor := NewShellExecutor(mockLogger) | |
| 52 | 97 | ctx := context.Background() | |
| 53 | 98 | ||
| 54 | 99 | // false command always exits with code 1 | |
| @@ -59,7 +104,15 @@ func TestShellExecutor_Execute_CommandFailure(t *testing.T) { | |||
| 59 | 104 | } | |
| 60 | 105 | ||
| 61 | 106 | func TestShellExecutor_ExecuteWithEnv_Success(t *testing.T) { | |
| 62 | - | executor := NewShellExecutor() | |
| 107 | + | ctrl := gomock.NewController(t) | |
| 108 | + | defer ctrl.Finish() | |
| 109 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 110 | + | mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() | |
| 111 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 112 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |
| 113 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | |
| 114 | + | ||
| 115 | + | executor := NewShellExecutor(mockLogger) | |
| 63 | 116 | ctx := context.Background() | |
| 64 | 117 | ||
| 65 | 118 | env := map[string]string{ | |
| @@ -78,7 +131,15 @@ func TestShellExecutor_ExecuteWithEnv_Success(t *testing.T) { | |||
| 78 | 131 | } | |
| 79 | 132 | ||
| 80 | 133 | func TestShellExecutor_ExecuteWithEnv_MultipleVars(t *testing.T) { | |
| 81 | - | executor := NewShellExecutor() | |
| 134 | + | ctrl := gomock.NewController(t) | |
| 135 | + | defer ctrl.Finish() | |
| 136 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 137 | + | mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() | |
| 138 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 139 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |
| 140 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | |
| 141 | + | ||
| 142 | + | executor := NewShellExecutor(mockLogger) | |
| 82 | 143 | ctx := context.Background() | |
| 83 | 144 | ||
| 84 | 145 | env := map[string]string{ | |
| @@ -98,7 +159,15 @@ func TestShellExecutor_ExecuteWithEnv_MultipleVars(t *testing.T) { | |||
| 98 | 159 | } | |
| 99 | 160 | ||
| 100 | 161 | func TestShellExecutor_Execute_ContextCancellation(t *testing.T) { | |
| 101 | - | executor := NewShellExecutor() | |
| 162 | + | ctrl := gomock.NewController(t) | |
| 163 | + | defer ctrl.Finish() | |
| 164 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 165 | + | mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() | |
| 166 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 167 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |
| 168 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | |
| 169 | + | ||
| 170 | + | executor := NewShellExecutor(mockLogger) | |
| 102 | 171 | ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) | |
| 103 | 172 | defer cancel() | |
| 104 | 173 | ||
| @@ -110,7 +179,15 @@ func TestShellExecutor_Execute_ContextCancellation(t *testing.T) { | |||
| 110 | 179 | } | |
| 111 | 180 | ||
| 112 | 181 | func TestShellExecutor_Execute_EmptyOutput(t *testing.T) { | |
| 113 | - | executor := NewShellExecutor() | |
| 182 | + | ctrl := gomock.NewController(t) | |
| 183 | + | defer ctrl.Finish() | |
| 184 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 185 | + | mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() | |
| 186 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 187 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |
| 188 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | |
| 189 | + | ||
| 190 | + | executor := NewShellExecutor(mockLogger) | |
| 114 | 191 | ctx := context.Background() | |
| 115 | 192 | ||
| 116 | 193 | // true command produces no output | |
| @@ -125,7 +202,15 @@ func TestShellExecutor_Execute_EmptyOutput(t *testing.T) { | |||
| 125 | 202 | } | |
| 126 | 203 | ||
| 127 | 204 | func TestShellExecutor_Execute_StderrCapture(t *testing.T) { | |
| 128 | - | executor := NewShellExecutor() | |
| 205 | + | ctrl := gomock.NewController(t) | |
| 206 | + | defer ctrl.Finish() | |
| 207 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 208 | + | mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() | |
| 209 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 210 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |
| 211 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | |
| 212 | + | ||
| 213 | + | executor := NewShellExecutor(mockLogger) | |
| 129 | 214 | ctx := context.Background() | |
| 130 | 215 | ||
| 131 | 216 | // sh command that writes to stderr | |
| @@ -5,10 +5,21 @@ import ( | |||||
| 5 | "strings" | 5 | "strings" | ||
| 6 | "testing" | 6 | "testing" | ||
| 7 | "time" | 7 | "time" | ||
| 8 | + | ||||
| 9 | + | "github.com/bnema/archup/internal/domain/ports/mocks" | |||
| 10 | + | "github.com/golang/mock/gomock" | |||
| 8 | ) | 11 | ) | ||
| 9 | 12 | ||||
| 10 | func TestShellExecutor_Execute_Success(t *testing.T) { | 13 | func TestShellExecutor_Execute_Success(t *testing.T) { | ||
| 11 | - | executor := NewShellExecutor() | 14 | + | ctrl := gomock.NewController(t) |
| 15 | + | defer ctrl.Finish() | |||
| 16 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 17 | + | mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 18 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 19 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 20 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 21 | + | ||||
| 22 | + | executor := NewShellExecutor(mockLogger) | |||
| 12 | ctx := context.Background() | 23 | ctx := context.Background() | ||
| 13 | 24 | ||||
| 14 | output, err := executor.Execute(ctx, "echo", "hello world") | 25 | output, err := executor.Execute(ctx, "echo", "hello world") | ||
| @@ -23,7 +34,15 @@ func TestShellExecutor_Execute_Success(t *testing.T) { | |||||
| 23 | } | 34 | } | ||
| 24 | 35 | ||||
| 25 | func TestShellExecutor_Execute_WithArgs(t *testing.T) { | 36 | func TestShellExecutor_Execute_WithArgs(t *testing.T) { | ||
| 26 | - | executor := NewShellExecutor() | 37 | + | ctrl := gomock.NewController(t) |
| 38 | + | defer ctrl.Finish() | |||
| 39 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 40 | + | mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 41 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 42 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 43 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 44 | + | ||||
| 45 | + | executor := NewShellExecutor(mockLogger) | |||
| 27 | ctx := context.Background() | 46 | ctx := context.Background() | ||
| 28 | 47 | ||||
| 29 | // Test with multiple arguments | 48 | // Test with multiple arguments | ||
| @@ -37,8 +56,26 @@ func TestShellExecutor_Execute_WithArgs(t *testing.T) { | |||||
| 37 | } | 56 | } | ||
| 38 | } | 57 | } | ||
| 39 | 58 | ||||
| 59 | + | func newMockLogger() *mocks.MockLogger { | |||
| 60 | + | ctrl := gomock.NewController(&testing.T{}) | |||
| 61 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 62 | + | mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 63 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 64 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 65 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 66 | + | return mockLogger | |||
| 67 | + | } | |||
| 68 | + | ||||
| 40 | func TestShellExecutor_Execute_CommandNotFound(t *testing.T) { | 69 | func TestShellExecutor_Execute_CommandNotFound(t *testing.T) { | ||
| 41 | - | executor := NewShellExecutor() | 70 | + | ctrl := gomock.NewController(t) |
| 71 | + | defer ctrl.Finish() | |||
| 72 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 73 | + | mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 74 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 75 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 76 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 77 | + | ||||
| 78 | + | executor := NewShellExecutor(mockLogger) | |||
| 42 | ctx := context.Background() | 79 | ctx := context.Background() | ||
| 43 | 80 | ||||
| 44 | _, err := executor.Execute(ctx, "nonexistentcommand12345", "arg") | 81 | _, err := executor.Execute(ctx, "nonexistentcommand12345", "arg") | ||
| @@ -48,7 +85,15 @@ func TestShellExecutor_Execute_CommandNotFound(t *testing.T) { | |||||
| 48 | } | 85 | } | ||
| 49 | 86 | ||||
| 50 | func TestShellExecutor_Execute_CommandFailure(t *testing.T) { | 87 | func TestShellExecutor_Execute_CommandFailure(t *testing.T) { | ||
| 51 | - | executor := NewShellExecutor() | 88 | + | ctrl := gomock.NewController(t) |
| 89 | + | defer ctrl.Finish() | |||
| 90 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 91 | + | mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 92 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 93 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 94 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 95 | + | ||||
| 96 | + | executor := NewShellExecutor(mockLogger) | |||
| 52 | ctx := context.Background() | 97 | ctx := context.Background() | ||
| 53 | 98 | ||||
| 54 | // false command always exits with code 1 | 99 | // false command always exits with code 1 | ||
| @@ -59,7 +104,15 @@ func TestShellExecutor_Execute_CommandFailure(t *testing.T) { | |||||
| 59 | } | 104 | } | ||
| 60 | 105 | ||||
| 61 | func TestShellExecutor_ExecuteWithEnv_Success(t *testing.T) { | 106 | func TestShellExecutor_ExecuteWithEnv_Success(t *testing.T) { | ||
| 62 | - | executor := NewShellExecutor() | 107 | + | ctrl := gomock.NewController(t) |
| 108 | + | defer ctrl.Finish() | |||
| 109 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 110 | + | mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 111 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 112 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 113 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 114 | + | ||||
| 115 | + | executor := NewShellExecutor(mockLogger) | |||
| 63 | ctx := context.Background() | 116 | ctx := context.Background() | ||
| 64 | 117 | ||||
| 65 | env := map[string]string{ | 118 | env := map[string]string{ | ||
| @@ -78,7 +131,15 @@ func TestShellExecutor_ExecuteWithEnv_Success(t *testing.T) { | |||||
| 78 | } | 131 | } | ||
| 79 | 132 | ||||
| 80 | func TestShellExecutor_ExecuteWithEnv_MultipleVars(t *testing.T) { | 133 | func TestShellExecutor_ExecuteWithEnv_MultipleVars(t *testing.T) { | ||
| 81 | - | executor := NewShellExecutor() | 134 | + | ctrl := gomock.NewController(t) |
| 135 | + | defer ctrl.Finish() | |||
| 136 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 137 | + | mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 138 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 139 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 140 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 141 | + | ||||
| 142 | + | executor := NewShellExecutor(mockLogger) | |||
| 82 | ctx := context.Background() | 143 | ctx := context.Background() | ||
| 83 | 144 | ||||
| 84 | env := map[string]string{ | 145 | env := map[string]string{ | ||
| @@ -98,7 +159,15 @@ func TestShellExecutor_ExecuteWithEnv_MultipleVars(t *testing.T) { | |||||
| 98 | } | 159 | } | ||
| 99 | 160 | ||||
| 100 | func TestShellExecutor_Execute_ContextCancellation(t *testing.T) { | 161 | func TestShellExecutor_Execute_ContextCancellation(t *testing.T) { | ||
| 101 | - | executor := NewShellExecutor() | 162 | + | ctrl := gomock.NewController(t) |
| 163 | + | defer ctrl.Finish() | |||
| 164 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 165 | + | mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 166 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 167 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 168 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 169 | + | ||||
| 170 | + | executor := NewShellExecutor(mockLogger) | |||
| 102 | ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) | 171 | ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) | ||
| 103 | defer cancel() | 172 | defer cancel() | ||
| 104 | 173 | ||||
| @@ -110,7 +179,15 @@ func TestShellExecutor_Execute_ContextCancellation(t *testing.T) { | |||||
| 110 | } | 179 | } | ||
| 111 | 180 | ||||
| 112 | func TestShellExecutor_Execute_EmptyOutput(t *testing.T) { | 181 | func TestShellExecutor_Execute_EmptyOutput(t *testing.T) { | ||
| 113 | - | executor := NewShellExecutor() | 182 | + | ctrl := gomock.NewController(t) |
| 183 | + | defer ctrl.Finish() | |||
| 184 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 185 | + | mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 186 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 187 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 188 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 189 | + | ||||
| 190 | + | executor := NewShellExecutor(mockLogger) | |||
| 114 | ctx := context.Background() | 191 | ctx := context.Background() | ||
| 115 | 192 | ||||
| 116 | // true command produces no output | 193 | // true command produces no output | ||
| @@ -125,7 +202,15 @@ func TestShellExecutor_Execute_EmptyOutput(t *testing.T) { | |||||
| 125 | } | 202 | } | ||
| 126 | 203 | ||||
| 127 | func TestShellExecutor_Execute_StderrCapture(t *testing.T) { | 204 | func TestShellExecutor_Execute_StderrCapture(t *testing.T) { | ||
| 128 | - | executor := NewShellExecutor() | 205 | + | ctrl := gomock.NewController(t) |
| 206 | + | defer ctrl.Finish() | |||
| 207 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 208 | + | mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 209 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 210 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 211 | + | mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 212 | + | ||||
| 213 | + | executor := NewShellExecutor(mockLogger) | |||
| 129 | ctx := context.Background() | 214 | ctx := context.Background() | ||
| 130 | 215 | ||||
| 131 | // sh command that writes to stderr | 216 | // sh command that writes to stderr | ||
M internal/infrastructure/filesystem/local.go +4 -9
| @@ -3,6 +3,8 @@ package filesystem | |||
| 3 | 3 | import ( | |
| 4 | 4 | "fmt" | |
| 5 | 5 | "os" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/archup/internal/domain/ports" | |
| 6 | 8 | ) | |
| 7 | 9 | ||
| 8 | 10 | // LocalFileSystem implements the FileSystem port using the OS filesystem | |
| @@ -29,7 +31,7 @@ func (lfs *LocalFileSystem) WriteFile(name string, data []byte, perm os.FileMode | |||
| 29 | 31 | } | |
| 30 | 32 | ||
| 31 | 33 | // Create creates or truncates a file | |
| 32 | - | func (lfs *LocalFileSystem) Create(name string) (LocalFile, error) { | |
| 34 | + | func (lfs *LocalFileSystem) Create(name string) (ports.File, error) { | |
| 33 | 35 | file, err := os.Create(name) | |
| 34 | 36 | if err != nil { | |
| 35 | 37 | return nil, err | |
| @@ -64,14 +66,7 @@ func (lfs *LocalFileSystem) Exists(path string) (bool, error) { | |||
| 64 | 66 | return false, fmt.Errorf("failed to check file existence: %w", err) | |
| 65 | 67 | } | |
| 66 | 68 | ||
| 67 | - | // LocalFile interface type for file operations | |
| 68 | - | type LocalFile interface { | |
| 69 | - | Read(b []byte) (n int, err error) | |
| 70 | - | Write(b []byte) (n int, err error) | |
| 71 | - | Close() error | |
| 72 | - | } | |
| 73 | - | ||
| 74 | - | // localFileHandle wraps os.File to implement the File port | |
| 69 | + | // localFileHandle wraps os.File to implement the ports.File interface | |
| 75 | 70 | type localFileHandle struct { | |
| 76 | 71 | file *os.File | |
| 77 | 72 | } | |
| @@ -3,6 +3,8 @@ package filesystem | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "fmt" | 4 | "fmt" | ||
| 5 | "os" | 5 | "os" | ||
| 6 | + | ||||
| 7 | + | "github.com/bnema/archup/internal/domain/ports" | |||
| 6 | ) | 8 | ) | ||
| 7 | 9 | ||||
| 8 | // LocalFileSystem implements the FileSystem port using the OS filesystem | 10 | // LocalFileSystem implements the FileSystem port using the OS filesystem | ||
| @@ -29,7 +31,7 @@ func (lfs *LocalFileSystem) WriteFile(name string, data []byte, perm os.FileMode | |||||
| 29 | } | 31 | } | ||
| 30 | 32 | ||||
| 31 | // Create creates or truncates a file | 33 | // Create creates or truncates a file | ||
| 32 | - | func (lfs *LocalFileSystem) Create(name string) (LocalFile, error) { | 34 | + | func (lfs *LocalFileSystem) Create(name string) (ports.File, error) { |
| 33 | file, err := os.Create(name) | 35 | file, err := os.Create(name) | ||
| 34 | if err != nil { | 36 | if err != nil { | ||
| 35 | return nil, err | 37 | return nil, err | ||
| @@ -64,14 +66,7 @@ func (lfs *LocalFileSystem) Exists(path string) (bool, error) { | |||||
| 64 | return false, fmt.Errorf("failed to check file existence: %w", err) | 66 | return false, fmt.Errorf("failed to check file existence: %w", err) | ||
| 65 | } | 67 | } | ||
| 66 | 68 | ||||
| 67 | - | // LocalFile interface type for file operations | 69 | + | // localFileHandle wraps os.File to implement the ports.File interface |
| 68 | - | type LocalFile interface { | |||
| 69 | - | Read(b []byte) (n int, err error) | |||
| 70 | - | Write(b []byte) (n int, err error) | |||
| 71 | - | Close() error | |||
| 72 | - | } | |||
| 73 | - | ||||
| 74 | - | // localFileHandle wraps os.File to implement the File port | |||
| 75 | type localFileHandle struct { | 70 | type localFileHandle struct { | ||
| 76 | file *os.File | 71 | file *os.File | ||
| 77 | } | 72 | } | ||
M internal/infrastructure/logger/slog_adapter.go +4 -35
| @@ -1,10 +1,7 @@ | |||
| 1 | 1 | package logger | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | - | "fmt" | |
| 5 | 4 | "log/slog" | |
| 6 | - | "os" | |
| 7 | - | "path/filepath" | |
| 8 | 5 | ) | |
| 9 | 6 | ||
| 10 | 7 | // SlogAdapter implements the Logger port using Go's standard log/slog | |
| @@ -13,40 +10,12 @@ type SlogAdapter struct { | |||
| 13 | 10 | logPath string | |
| 14 | 11 | } | |
| 15 | 12 | ||
| 16 | - | // NewSlogAdapter creates a new slog-based logger | |
| 17 | - | // If logPath is empty, logs are written to stderr only | |
| 18 | - | func NewSlogAdapter(logPath string) (*SlogAdapter, error) { | |
| 19 | - | var logger *slog.Logger | |
| 20 | - | ||
| 21 | - | if logPath != "" { | |
| 22 | - | // Ensure parent directory exists | |
| 23 | - | if err := os.MkdirAll(filepath.Dir(logPath), 0755); err != nil { | |
| 24 | - | return nil, fmt.Errorf("failed to create log directory: %w", err) | |
| 25 | - | } | |
| 26 | - | ||
| 27 | - | // Open log file for appending | |
| 28 | - | file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) | |
| 29 | - | if err != nil { | |
| 30 | - | return nil, fmt.Errorf("failed to open log file: %w", err) | |
| 31 | - | } | |
| 32 | - | ||
| 33 | - | // Create slog handler with both stderr and file output | |
| 34 | - | handler := slog.NewTextHandler(file, &slog.HandlerOptions{ | |
| 35 | - | Level: slog.LevelDebug, | |
| 36 | - | }) | |
| 37 | - | logger = slog.New(handler) | |
| 38 | - | } else { | |
| 39 | - | // Log to stderr only | |
| 40 | - | handler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ | |
| 41 | - | Level: slog.LevelDebug, | |
| 42 | - | }) | |
| 43 | - | logger = slog.New(handler) | |
| 44 | - | } | |
| 45 | - | ||
| 13 | + | // NewSlogAdapter wraps an existing slog.Logger to implement the Logger port | |
| 14 | + | func NewSlogAdapter(logger *slog.Logger) *SlogAdapter { | |
| 46 | 15 | return &SlogAdapter{ | |
| 47 | 16 | logger: logger, | |
| 48 | - | logPath: logPath, | |
| 49 | - | }, nil | |
| 17 | + | logPath: "", | |
| 18 | + | } | |
| 50 | 19 | } | |
| 51 | 20 | ||
| 52 | 21 | // Info logs an informational message | |
| @@ -1,10 +1,7 @@ | |||||
| 1 | package logger | 1 | package logger | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | - | "fmt" | |||
| 5 | "log/slog" | 4 | "log/slog" | ||
| 6 | - | "os" | |||
| 7 | - | "path/filepath" | |||
| 8 | ) | 5 | ) | ||
| 9 | 6 | ||||
| 10 | // SlogAdapter implements the Logger port using Go's standard log/slog | 7 | // SlogAdapter implements the Logger port using Go's standard log/slog | ||
| @@ -13,40 +10,12 @@ type SlogAdapter struct { | |||||
| 13 | logPath string | 10 | logPath string | ||
| 14 | } | 11 | } | ||
| 15 | 12 | ||||
| 16 | - | // NewSlogAdapter creates a new slog-based logger | 13 | + | // NewSlogAdapter wraps an existing slog.Logger to implement the Logger port |
| 17 | - | // If logPath is empty, logs are written to stderr only | 14 | + | func NewSlogAdapter(logger *slog.Logger) *SlogAdapter { |
| 18 | - | func NewSlogAdapter(logPath string) (*SlogAdapter, error) { | |||
| 19 | - | var logger *slog.Logger | |||
| 20 | - | ||||
| 21 | - | if logPath != "" { | |||
| 22 | - | // Ensure parent directory exists | |||
| 23 | - | if err := os.MkdirAll(filepath.Dir(logPath), 0755); err != nil { | |||
| 24 | - | return nil, fmt.Errorf("failed to create log directory: %w", err) | |||
| 25 | - | } | |||
| 26 | - | ||||
| 27 | - | // Open log file for appending | |||
| 28 | - | file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) | |||
| 29 | - | if err != nil { | |||
| 30 | - | return nil, fmt.Errorf("failed to open log file: %w", err) | |||
| 31 | - | } | |||
| 32 | - | ||||
| 33 | - | // Create slog handler with both stderr and file output | |||
| 34 | - | handler := slog.NewTextHandler(file, &slog.HandlerOptions{ | |||
| 35 | - | Level: slog.LevelDebug, | |||
| 36 | - | }) | |||
| 37 | - | logger = slog.New(handler) | |||
| 38 | - | } else { | |||
| 39 | - | // Log to stderr only | |||
| 40 | - | handler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ | |||
| 41 | - | Level: slog.LevelDebug, | |||
| 42 | - | }) | |||
| 43 | - | logger = slog.New(handler) | |||
| 44 | - | } | |||
| 45 | - | ||||
| 46 | return &SlogAdapter{ | 15 | return &SlogAdapter{ | ||
| 47 | logger: logger, | 16 | logger: logger, | ||
| 48 | - | logPath: logPath, | 17 | + | logPath: "", |
| 49 | - | }, nil | 18 | + | } |
| 50 | } | 19 | } | ||
| 51 | 20 | ||||
| 52 | // Info logs an informational message | 21 | // Info logs an informational message | ||
M internal/infrastructure/logger/slog_adapter_test.go +27 -141
| @@ -1,178 +1,64 @@ | |||
| 1 | 1 | package logger | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "log/slog" | |
| 4 | 5 | "os" | |
| 5 | - | "path/filepath" | |
| 6 | 6 | "testing" | |
| 7 | 7 | ) | |
| 8 | 8 | ||
| 9 | - | func TestSlogAdapter_NewSlogAdapterWithoutFile(t *testing.T) { | |
| 10 | - | logger, err := NewSlogAdapter("") | |
| 11 | - | if err != nil { | |
| 12 | - | t.Fatalf("expected no error, got %v", err) | |
| 13 | - | } | |
| 14 | - | ||
| 15 | - | if logger == nil { | |
| 16 | - | t.Fatal("expected non-nil logger") | |
| 17 | - | } | |
| 18 | - | ||
| 19 | - | if logger.LogPath() != "" { | |
| 20 | - | t.Errorf("expected empty log path, got %s", logger.LogPath()) | |
| 21 | - | } | |
| 22 | - | } | |
| 23 | - | ||
| 24 | - | func TestSlogAdapter_NewSlogAdapterWithFile(t *testing.T) { | |
| 25 | - | tmpDir := t.TempDir() | |
| 26 | - | logPath := filepath.Join(tmpDir, "test.log") | |
| 27 | - | ||
| 28 | - | logger, err := NewSlogAdapter(logPath) | |
| 29 | - | if err != nil { | |
| 30 | - | t.Fatalf("expected no error, got %v", err) | |
| 31 | - | } | |
| 32 | - | ||
| 33 | - | if logger == nil { | |
| 34 | - | t.Fatal("expected non-nil logger") | |
| 35 | - | } | |
| 36 | - | ||
| 37 | - | if logger.LogPath() != logPath { | |
| 38 | - | t.Errorf("expected log path %s, got %s", logPath, logger.LogPath()) | |
| 39 | - | } | |
| 40 | - | ||
| 41 | - | // Verify file was created | |
| 42 | - | if _, err := os.Stat(logPath); os.IsNotExist(err) { | |
| 43 | - | t.Error("expected log file to be created") | |
| 44 | - | } | |
| 45 | - | } | |
| 46 | - | ||
| 47 | - | func TestSlogAdapter_NewSlogAdapterCreatesDirs(t *testing.T) { | |
| 48 | - | tmpDir := t.TempDir() | |
| 49 | - | logPath := filepath.Join(tmpDir, "subdir", "nested", "test.log") | |
| 50 | - | ||
| 51 | - | logger, err := NewSlogAdapter(logPath) | |
| 52 | - | if err != nil { | |
| 53 | - | t.Fatalf("expected no error, got %v", err) | |
| 54 | - | } | |
| 9 | + | func TestSlogAdapter_NewSlogAdapter(t *testing.T) { | |
| 10 | + | slogLogger := slog.New(slog.NewTextHandler(os.Stderr, nil)) | |
| 11 | + | adapter := NewSlogAdapter(slogLogger) | |
| 55 | 12 | ||
| 56 | - | if logger == nil { | |
| 57 | - | t.Fatal("expected non-nil logger") | |
| 13 | + | if adapter == nil { | |
| 14 | + | t.Fatal("expected non-nil adapter") | |
| 58 | 15 | } | |
| 59 | 16 | ||
| 60 | - | // Verify file was created with parent directories | |
| 61 | - | if _, err := os.Stat(logPath); os.IsNotExist(err) { | |
| 62 | - | t.Error("expected log file to be created with parent directories") | |
| 17 | + | if adapter.LogPath() != "" { | |
| 18 | + | t.Errorf("expected empty log path, got %s", adapter.LogPath()) | |
| 63 | 19 | } | |
| 64 | 20 | } | |
| 65 | 21 | ||
| 66 | 22 | func TestSlogAdapter_Info(t *testing.T) { | |
| 67 | - | tmpDir := t.TempDir() | |
| 68 | - | logPath := filepath.Join(tmpDir, "test.log") | |
| 69 | - | ||
| 70 | - | logger, err := NewSlogAdapter(logPath) | |
| 71 | - | if err != nil { | |
| 72 | - | t.Fatalf("expected no error, got %v", err) | |
| 73 | - | } | |
| 23 | + | slogLogger := slog.New(slog.NewTextHandler(os.Stderr, nil)) | |
| 24 | + | adapter := NewSlogAdapter(slogLogger) | |
| 74 | 25 | ||
| 75 | 26 | // Should not panic | |
| 76 | - | logger.Info("test message", "key", "value") | |
| 77 | - | ||
| 78 | - | // Verify content was written | |
| 79 | - | content, err := os.ReadFile(logPath) | |
| 80 | - | if err != nil { | |
| 81 | - | t.Fatalf("failed to read log file: %v", err) | |
| 82 | - | } | |
| 83 | - | ||
| 84 | - | if len(content) == 0 { | |
| 85 | - | t.Error("expected log content") | |
| 86 | - | } | |
| 27 | + | adapter.Info("test message", "key", "value") | |
| 87 | 28 | } | |
| 88 | 29 | ||
| 89 | 30 | func TestSlogAdapter_Warn(t *testing.T) { | |
| 90 | - | tmpDir := t.TempDir() | |
| 91 | - | logPath := filepath.Join(tmpDir, "test.log") | |
| 92 | - | ||
| 93 | - | logger, err := NewSlogAdapter(logPath) | |
| 94 | - | if err != nil { | |
| 95 | - | t.Fatalf("expected no error, got %v", err) | |
| 96 | - | } | |
| 31 | + | slogLogger := slog.New(slog.NewTextHandler(os.Stderr, nil)) | |
| 32 | + | adapter := NewSlogAdapter(slogLogger) | |
| 97 | 33 | ||
| 98 | 34 | // Should not panic | |
| 99 | - | logger.Warn("warning message", "warn_key", "warn_value") | |
| 100 | - | ||
| 101 | - | content, err := os.ReadFile(logPath) | |
| 102 | - | if err != nil { | |
| 103 | - | t.Fatalf("failed to read log file: %v", err) | |
| 104 | - | } | |
| 105 | - | ||
| 106 | - | if len(content) == 0 { | |
| 107 | - | t.Error("expected log content") | |
| 108 | - | } | |
| 35 | + | adapter.Warn("warning message", "warn_key", "warn_value") | |
| 109 | 36 | } | |
| 110 | 37 | ||
| 111 | 38 | func TestSlogAdapter_Error(t *testing.T) { | |
| 112 | - | tmpDir := t.TempDir() | |
| 113 | - | logPath := filepath.Join(tmpDir, "test.log") | |
| 114 | - | ||
| 115 | - | logger, err := NewSlogAdapter(logPath) | |
| 116 | - | if err != nil { | |
| 117 | - | t.Fatalf("expected no error, got %v", err) | |
| 118 | - | } | |
| 39 | + | slogLogger := slog.New(slog.NewTextHandler(os.Stderr, nil)) | |
| 40 | + | adapter := NewSlogAdapter(slogLogger) | |
| 119 | 41 | ||
| 120 | 42 | // Should not panic | |
| 121 | - | logger.Error("error message", "error_key", "error_value") | |
| 122 | - | ||
| 123 | - | content, err := os.ReadFile(logPath) | |
| 124 | - | if err != nil { | |
| 125 | - | t.Fatalf("failed to read log file: %v", err) | |
| 126 | - | } | |
| 127 | - | ||
| 128 | - | if len(content) == 0 { | |
| 129 | - | t.Error("expected log content") | |
| 130 | - | } | |
| 43 | + | adapter.Error("error message", "error_key", "error_value") | |
| 131 | 44 | } | |
| 132 | 45 | ||
| 133 | 46 | func TestSlogAdapter_Debug(t *testing.T) { | |
| 134 | - | tmpDir := t.TempDir() | |
| 135 | - | logPath := filepath.Join(tmpDir, "test.log") | |
| 136 | - | ||
| 137 | - | logger, err := NewSlogAdapter(logPath) | |
| 138 | - | if err != nil { | |
| 139 | - | t.Fatalf("expected no error, got %v", err) | |
| 140 | - | } | |
| 47 | + | slogLogger := slog.New(slog.NewTextHandler(os.Stderr, nil)) | |
| 48 | + | adapter := NewSlogAdapter(slogLogger) | |
| 141 | 49 | ||
| 142 | 50 | // Should not panic | |
| 143 | - | logger.Debug("debug message", "debug_key", "debug_value") | |
| 144 | - | ||
| 145 | - | content, err := os.ReadFile(logPath) | |
| 146 | - | if err != nil { | |
| 147 | - | t.Fatalf("failed to read log file: %v", err) | |
| 148 | - | } | |
| 149 | - | ||
| 150 | - | if len(content) == 0 { | |
| 151 | - | t.Error("expected log content") | |
| 152 | - | } | |
| 51 | + | adapter.Debug("debug message", "debug_key", "debug_value") | |
| 153 | 52 | } | |
| 154 | 53 | ||
| 155 | 54 | func TestSlogAdapter_MultipleLogCalls(t *testing.T) { | |
| 156 | - | tmpDir := t.TempDir() | |
| 157 | - | logPath := filepath.Join(tmpDir, "test.log") | |
| 158 | - | ||
| 159 | - | logger, err := NewSlogAdapter(logPath) | |
| 160 | - | if err != nil { | |
| 161 | - | t.Fatalf("expected no error, got %v", err) | |
| 162 | - | } | |
| 55 | + | slogLogger := slog.New(slog.NewTextHandler(os.Stderr, nil)) | |
| 56 | + | adapter := NewSlogAdapter(slogLogger) | |
| 163 | 57 | ||
| 164 | - | logger.Info("first message") | |
| 165 | - | logger.Warn("second message") | |
| 166 | - | logger.Error("third message") | |
| 167 | - | logger.Debug("fourth message") | |
| 58 | + | adapter.Info("first message") | |
| 59 | + | adapter.Warn("second message") | |
| 60 | + | adapter.Error("third message") | |
| 61 | + | adapter.Debug("fourth message") | |
| 168 | 62 | ||
| 169 | - | content, err := os.ReadFile(logPath) | |
| 170 | - | if err != nil { | |
| 171 | - | t.Fatalf("failed to read log file: %v", err) | |
| 172 | - | } | |
| 173 | - | ||
| 174 | - | // All messages should be present | |
| 175 | - | if len(content) == 0 { | |
| 176 | - | t.Error("expected log content") | |
| 177 | - | } | |
| 63 | + | // All calls should succeed without panicking | |
| 178 | 64 | } | |
| @@ -1,178 +1,64 @@ | |||||
| 1 | package logger | 1 | package logger | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "log/slog" | |||
| 4 | "os" | 5 | "os" | ||
| 5 | - | "path/filepath" | |||
| 6 | "testing" | 6 | "testing" | ||
| 7 | ) | 7 | ) | ||
| 8 | 8 | ||||
| 9 | - | func TestSlogAdapter_NewSlogAdapterWithoutFile(t *testing.T) { | 9 | + | func TestSlogAdapter_NewSlogAdapter(t *testing.T) { |
| 10 | - | logger, err := NewSlogAdapter("") | 10 | + | slogLogger := slog.New(slog.NewTextHandler(os.Stderr, nil)) |
| 11 | - | if err != nil { | 11 | + | adapter := NewSlogAdapter(slogLogger) |
| 12 | - | t.Fatalf("expected no error, got %v", err) | |||
| 13 | - | } | |||
| 14 | - | ||||
| 15 | - | if logger == nil { | |||
| 16 | - | t.Fatal("expected non-nil logger") | |||
| 17 | - | } | |||
| 18 | - | ||||
| 19 | - | if logger.LogPath() != "" { | |||
| 20 | - | t.Errorf("expected empty log path, got %s", logger.LogPath()) | |||
| 21 | - | } | |||
| 22 | - | } | |||
| 23 | - | ||||
| 24 | - | func TestSlogAdapter_NewSlogAdapterWithFile(t *testing.T) { | |||
| 25 | - | tmpDir := t.TempDir() | |||
| 26 | - | logPath := filepath.Join(tmpDir, "test.log") | |||
| 27 | - | ||||
| 28 | - | logger, err := NewSlogAdapter(logPath) | |||
| 29 | - | if err != nil { | |||
| 30 | - | t.Fatalf("expected no error, got %v", err) | |||
| 31 | - | } | |||
| 32 | - | ||||
| 33 | - | if logger == nil { | |||
| 34 | - | t.Fatal("expected non-nil logger") | |||
| 35 | - | } | |||
| 36 | - | ||||
| 37 | - | if logger.LogPath() != logPath { | |||
| 38 | - | t.Errorf("expected log path %s, got %s", logPath, logger.LogPath()) | |||
| 39 | - | } | |||
| 40 | - | ||||
| 41 | - | // Verify file was created | |||
| 42 | - | if _, err := os.Stat(logPath); os.IsNotExist(err) { | |||
| 43 | - | t.Error("expected log file to be created") | |||
| 44 | - | } | |||
| 45 | - | } | |||
| 46 | - | ||||
| 47 | - | func TestSlogAdapter_NewSlogAdapterCreatesDirs(t *testing.T) { | |||
| 48 | - | tmpDir := t.TempDir() | |||
| 49 | - | logPath := filepath.Join(tmpDir, "subdir", "nested", "test.log") | |||
| 50 | - | ||||
| 51 | - | logger, err := NewSlogAdapter(logPath) | |||
| 52 | - | if err != nil { | |||
| 53 | - | t.Fatalf("expected no error, got %v", err) | |||
| 54 | - | } | |||
| 55 | 12 | ||||
| 56 | - | if logger == nil { | 13 | + | if adapter == nil { |
| 57 | - | t.Fatal("expected non-nil logger") | 14 | + | t.Fatal("expected non-nil adapter") |
| 58 | } | 15 | } | ||
| 59 | 16 | ||||
| 60 | - | // Verify file was created with parent directories | 17 | + | if adapter.LogPath() != "" { |
| 61 | - | if _, err := os.Stat(logPath); os.IsNotExist(err) { | 18 | + | t.Errorf("expected empty log path, got %s", adapter.LogPath()) |
| 62 | - | t.Error("expected log file to be created with parent directories") | |||
| 63 | } | 19 | } | ||
| 64 | } | 20 | } | ||
| 65 | 21 | ||||
| 66 | func TestSlogAdapter_Info(t *testing.T) { | 22 | func TestSlogAdapter_Info(t *testing.T) { | ||
| 67 | - | tmpDir := t.TempDir() | 23 | + | slogLogger := slog.New(slog.NewTextHandler(os.Stderr, nil)) |
| 68 | - | logPath := filepath.Join(tmpDir, "test.log") | 24 | + | adapter := NewSlogAdapter(slogLogger) |
| 69 | - | ||||
| 70 | - | logger, err := NewSlogAdapter(logPath) | |||
| 71 | - | if err != nil { | |||
| 72 | - | t.Fatalf("expected no error, got %v", err) | |||
| 73 | - | } | |||
| 74 | 25 | ||||
| 75 | // Should not panic | 26 | // Should not panic | ||
| 76 | - | logger.Info("test message", "key", "value") | 27 | + | adapter.Info("test message", "key", "value") |
| 77 | - | ||||
| 78 | - | // Verify content was written | |||
| 79 | - | content, err := os.ReadFile(logPath) | |||
| 80 | - | if err != nil { | |||
| 81 | - | t.Fatalf("failed to read log file: %v", err) | |||
| 82 | - | } | |||
| 83 | - | ||||
| 84 | - | if len(content) == 0 { | |||
| 85 | - | t.Error("expected log content") | |||
| 86 | - | } | |||
| 87 | } | 28 | } | ||
| 88 | 29 | ||||
| 89 | func TestSlogAdapter_Warn(t *testing.T) { | 30 | func TestSlogAdapter_Warn(t *testing.T) { | ||
| 90 | - | tmpDir := t.TempDir() | 31 | + | slogLogger := slog.New(slog.NewTextHandler(os.Stderr, nil)) |
| 91 | - | logPath := filepath.Join(tmpDir, "test.log") | 32 | + | adapter := NewSlogAdapter(slogLogger) |
| 92 | - | ||||
| 93 | - | logger, err := NewSlogAdapter(logPath) | |||
| 94 | - | if err != nil { | |||
| 95 | - | t.Fatalf("expected no error, got %v", err) | |||
| 96 | - | } | |||
| 97 | 33 | ||||
| 98 | // Should not panic | 34 | // Should not panic | ||
| 99 | - | logger.Warn("warning message", "warn_key", "warn_value") | 35 | + | adapter.Warn("warning message", "warn_key", "warn_value") |
| 100 | - | ||||
| 101 | - | content, err := os.ReadFile(logPath) | |||
| 102 | - | if err != nil { | |||
| 103 | - | t.Fatalf("failed to read log file: %v", err) | |||
| 104 | - | } | |||
| 105 | - | ||||
| 106 | - | if len(content) == 0 { | |||
| 107 | - | t.Error("expected log content") | |||
| 108 | - | } | |||
| 109 | } | 36 | } | ||
| 110 | 37 | ||||
| 111 | func TestSlogAdapter_Error(t *testing.T) { | 38 | func TestSlogAdapter_Error(t *testing.T) { | ||
| 112 | - | tmpDir := t.TempDir() | 39 | + | slogLogger := slog.New(slog.NewTextHandler(os.Stderr, nil)) |
| 113 | - | logPath := filepath.Join(tmpDir, "test.log") | 40 | + | adapter := NewSlogAdapter(slogLogger) |
| 114 | - | ||||
| 115 | - | logger, err := NewSlogAdapter(logPath) | |||
| 116 | - | if err != nil { | |||
| 117 | - | t.Fatalf("expected no error, got %v", err) | |||
| 118 | - | } | |||
| 119 | 41 | ||||
| 120 | // Should not panic | 42 | // Should not panic | ||
| 121 | - | logger.Error("error message", "error_key", "error_value") | 43 | + | adapter.Error("error message", "error_key", "error_value") |
| 122 | - | ||||
| 123 | - | content, err := os.ReadFile(logPath) | |||
| 124 | - | if err != nil { | |||
| 125 | - | t.Fatalf("failed to read log file: %v", err) | |||
| 126 | - | } | |||
| 127 | - | ||||
| 128 | - | if len(content) == 0 { | |||
| 129 | - | t.Error("expected log content") | |||
| 130 | - | } | |||
| 131 | } | 44 | } | ||
| 132 | 45 | ||||
| 133 | func TestSlogAdapter_Debug(t *testing.T) { | 46 | func TestSlogAdapter_Debug(t *testing.T) { | ||
| 134 | - | tmpDir := t.TempDir() | 47 | + | slogLogger := slog.New(slog.NewTextHandler(os.Stderr, nil)) |
| 135 | - | logPath := filepath.Join(tmpDir, "test.log") | 48 | + | adapter := NewSlogAdapter(slogLogger) |
| 136 | - | ||||
| 137 | - | logger, err := NewSlogAdapter(logPath) | |||
| 138 | - | if err != nil { | |||
| 139 | - | t.Fatalf("expected no error, got %v", err) | |||
| 140 | - | } | |||
| 141 | 49 | ||||
| 142 | // Should not panic | 50 | // Should not panic | ||
| 143 | - | logger.Debug("debug message", "debug_key", "debug_value") | 51 | + | adapter.Debug("debug message", "debug_key", "debug_value") |
| 144 | - | ||||
| 145 | - | content, err := os.ReadFile(logPath) | |||
| 146 | - | if err != nil { | |||
| 147 | - | t.Fatalf("failed to read log file: %v", err) | |||
| 148 | - | } | |||
| 149 | - | ||||
| 150 | - | if len(content) == 0 { | |||
| 151 | - | t.Error("expected log content") | |||
| 152 | - | } | |||
| 153 | } | 52 | } | ||
| 154 | 53 | ||||
| 155 | func TestSlogAdapter_MultipleLogCalls(t *testing.T) { | 54 | func TestSlogAdapter_MultipleLogCalls(t *testing.T) { | ||
| 156 | - | tmpDir := t.TempDir() | 55 | + | slogLogger := slog.New(slog.NewTextHandler(os.Stderr, nil)) |
| 157 | - | logPath := filepath.Join(tmpDir, "test.log") | 56 | + | adapter := NewSlogAdapter(slogLogger) |
| 158 | - | ||||
| 159 | - | logger, err := NewSlogAdapter(logPath) | |||
| 160 | - | if err != nil { | |||
| 161 | - | t.Fatalf("expected no error, got %v", err) | |||
| 162 | - | } | |||
| 163 | 57 | ||||
| 164 | - | logger.Info("first message") | 58 | + | adapter.Info("first message") |
| 165 | - | logger.Warn("second message") | 59 | + | adapter.Warn("second message") |
| 166 | - | logger.Error("third message") | 60 | + | adapter.Error("third message") |
| 167 | - | logger.Debug("fourth message") | 61 | + | adapter.Debug("fourth message") |
| 168 | 62 | ||||
| 169 | - | content, err := os.ReadFile(logPath) | 63 | + | // All calls should succeed without panicking |
| 170 | - | if err != nil { | |||
| 171 | - | t.Fatalf("failed to read log file: %v", err) | |||
| 172 | - | } | |||
| 173 | - | ||||
| 174 | - | // All messages should be present | |||
| 175 | - | if len(content) == 0 { | |||
| 176 | - | t.Error("expected log content") | |||
| 177 | - | } | |||
| 178 | } | 64 | } | ||
M internal/interfaces/tui/app.go +9 -8
| @@ -7,6 +7,7 @@ import ( | |||
| 7 | 7 | "github.com/bnema/archup/internal/application/services" | |
| 8 | 8 | "github.com/bnema/archup/internal/domain/ports" | |
| 9 | 9 | "github.com/bnema/archup/internal/interfaces/tui/handlers" | |
| 10 | + | "github.com/bnema/archup/internal/interfaces/tui/models" | |
| 10 | 11 | "github.com/bnema/archup/internal/interfaces/tui/views" | |
| 11 | 12 | ) | |
| 12 | 13 | ||
| @@ -15,16 +16,16 @@ import ( | |||
| 15 | 16 | // with the application layer services | |
| 16 | 17 | type App struct { | |
| 17 | 18 | // Core services | |
| 18 | - | installService *services.InstallationService | |
| 19 | + | installService *services.InstallationService | |
| 19 | 20 | progressTracker *services.ProgressTracker | |
| 20 | 21 | ||
| 21 | 22 | // Infrastructure ports | |
| 22 | 23 | logger ports.Logger | |
| 23 | 24 | ||
| 24 | - | // UI models | |
| 25 | - | formModel FormModel | |
| 26 | - | installationModel InstallationModel | |
| 27 | - | progressModel ProgressModel | |
| 25 | + | // UI models (using concrete types for compatibility with views/handlers) | |
| 26 | + | formModel *models.FormModelImpl | |
| 27 | + | installationModel *models.InstallationModelImpl | |
| 28 | + | progressModel *models.ProgressModelImpl | |
| 28 | 29 | ||
| 29 | 30 | // Application state | |
| 30 | 31 | currentScreen Screen | |
| @@ -55,9 +56,9 @@ func NewApp( | |||
| 55 | 56 | installService: installService, | |
| 56 | 57 | progressTracker: progressTracker, | |
| 57 | 58 | logger: logger, | |
| 58 | - | formModel: NewFormModel(), | |
| 59 | - | installationModel: NewInstallationModel(), | |
| 60 | - | progressModel: NewProgressModel(), | |
| 59 | + | formModel: models.NewFormModel(), | |
| 60 | + | installationModel: models.NewInstallationModel(), | |
| 61 | + | progressModel: models.NewProgressModel(), | |
| 61 | 62 | currentScreen: ScreenForm, | |
| 62 | 63 | ctx: ctx, | |
| 63 | 64 | cancel: cancel, | |
| @@ -7,6 +7,7 @@ import ( | |||||
| 7 | "github.com/bnema/archup/internal/application/services" | 7 | "github.com/bnema/archup/internal/application/services" | ||
| 8 | "github.com/bnema/archup/internal/domain/ports" | 8 | "github.com/bnema/archup/internal/domain/ports" | ||
| 9 | "github.com/bnema/archup/internal/interfaces/tui/handlers" | 9 | "github.com/bnema/archup/internal/interfaces/tui/handlers" | ||
| 10 | + | "github.com/bnema/archup/internal/interfaces/tui/models" | |||
| 10 | "github.com/bnema/archup/internal/interfaces/tui/views" | 11 | "github.com/bnema/archup/internal/interfaces/tui/views" | ||
| 11 | ) | 12 | ) | ||
| 12 | 13 | ||||
| @@ -15,16 +16,16 @@ import ( | |||||
| 15 | // with the application layer services | 16 | // with the application layer services | ||
| 16 | type App struct { | 17 | type App struct { | ||
| 17 | // Core services | 18 | // Core services | ||
| 18 | - | installService *services.InstallationService | 19 | + | installService *services.InstallationService |
| 19 | progressTracker *services.ProgressTracker | 20 | progressTracker *services.ProgressTracker | ||
| 20 | 21 | ||||
| 21 | // Infrastructure ports | 22 | // Infrastructure ports | ||
| 22 | logger ports.Logger | 23 | logger ports.Logger | ||
| 23 | 24 | ||||
| 24 | - | // UI models | 25 | + | // UI models (using concrete types for compatibility with views/handlers) |
| 25 | - | formModel FormModel | 26 | + | formModel *models.FormModelImpl |
| 26 | - | installationModel InstallationModel | 27 | + | installationModel *models.InstallationModelImpl |
| 27 | - | progressModel ProgressModel | 28 | + | progressModel *models.ProgressModelImpl |
| 28 | 29 | ||||
| 29 | // Application state | 30 | // Application state | ||
| 30 | currentScreen Screen | 31 | currentScreen Screen | ||
| @@ -55,9 +56,9 @@ func NewApp( | |||||
| 55 | installService: installService, | 56 | installService: installService, | ||
| 56 | progressTracker: progressTracker, | 57 | progressTracker: progressTracker, | ||
| 57 | logger: logger, | 58 | logger: logger, | ||
| 58 | - | formModel: NewFormModel(), | 59 | + | formModel: models.NewFormModel(), |
| 59 | - | installationModel: NewInstallationModel(), | 60 | + | installationModel: models.NewInstallationModel(), |
| 60 | - | progressModel: NewProgressModel(), | 61 | + | progressModel: models.NewProgressModel(), |
| 61 | currentScreen: ScreenForm, | 62 | currentScreen: ScreenForm, | ||
| 62 | ctx: ctx, | 63 | ctx: ctx, | ||
| 63 | cancel: cancel, | 64 | cancel: cancel, | ||
M internal/interfaces/tui/handlers/install_handler.go +48 -18
| @@ -2,25 +2,47 @@ package handlers | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | tea "github.com/charmbracelet/bubbletea" | |
| 5 | - | "github.com/bnema/archup/internal/interfaces/tui" | |
| 5 | + | "github.com/bnema/archup/internal/application/dto" | |
| 6 | 6 | "github.com/bnema/archup/internal/interfaces/tui/models" | |
| 7 | 7 | ) | |
| 8 | 8 | ||
| 9 | + | // NOTE: This package uses AppContext interface to avoid importing parent tui package | |
| 10 | + | // This breaks the circular dependency: tui → tui/handlers → tui | |
| 11 | + | // Messages are typed by the caller (app.go), not here | |
| 12 | + | ||
| 9 | 13 | // HandleProgressUpdate processes progress update messages | |
| 10 | - | func HandleProgressUpdate(app *tui.App, msg tui.ProgressUpdateMsg, progressModel *models.ProgressModelImpl) (*models.ProgressModelImpl, tea.Cmd) { | |
| 11 | - | progressModel.UpdateProgress(msg.Update) | |
| 14 | + | // msg should be ProgressUpdateMsg with an Update field (*dto.ProgressUpdate) | |
| 15 | + | func HandleProgressUpdate(app AppContext, msg interface{}, progressModel *models.ProgressModelImpl) (*models.ProgressModelImpl, tea.Cmd) { | |
| 16 | + | // Cast to a type with Update field | |
| 17 | + | // This avoids importing the tui package which would create a cycle | |
| 18 | + | type updateMsg struct { | |
| 19 | + | Update interface{} | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | if m, ok := msg.(updateMsg); ok { | |
| 23 | + | // Cast Update to *dto.ProgressUpdate | |
| 24 | + | if update, ok := m.Update.(*dto.ProgressUpdate); ok { | |
| 25 | + | progressModel.UpdateProgress(update) | |
| 26 | + | } | |
| 27 | + | } | |
| 12 | 28 | return progressModel, nil | |
| 13 | 29 | } | |
| 14 | 30 | ||
| 15 | 31 | // HandleInstallationError processes installation error messages | |
| 16 | - | func HandleInstallationError(app *tui.App, msg tui.InstallationErrorMsg, installModel *models.InstallationModelImpl) (*models.InstallationModelImpl, tea.Cmd) { | |
| 17 | - | app.GetLogger().Error("Installation error", "error", msg.Err) | |
| 18 | - | installModel.SetError(msg.Err.Error()) | |
| 32 | + | // Signature matches what app.go calls with InstallationErrorMsg types | |
| 33 | + | func HandleInstallationError(app AppContext, msg interface{}, installModel *models.InstallationModelImpl) (*models.InstallationModelImpl, tea.Cmd) { | |
| 34 | + | // Cast msg to InstallationErrorMsg type (from app.go caller context) | |
| 35 | + | if errMsg, ok := msg.(interface{ GetErr() error }); ok { | |
| 36 | + | err := errMsg.GetErr() | |
| 37 | + | app.GetLogger().Error("Installation error", "error", err) | |
| 38 | + | installModel.SetError(err.Error()) | |
| 39 | + | } | |
| 19 | 40 | return installModel, nil | |
| 20 | 41 | } | |
| 21 | 42 | ||
| 22 | 43 | // HandleInstallationComplete processes installation completion messages | |
| 23 | - | func HandleInstallationComplete(app *tui.App, msg tui.InstallationCompleteMsg, installModel *models.InstallationModelImpl) (*models.InstallationModelImpl, tea.Cmd) { | |
| 44 | + | // Signature matches what app.go calls with InstallationCompleteMsg types | |
| 45 | + | func HandleInstallationComplete(app AppContext, msg interface{}, installModel *models.InstallationModelImpl) (*models.InstallationModelImpl, tea.Cmd) { | |
| 24 | 46 | app.GetLogger().Info("Installation completed successfully") | |
| 25 | 47 | installModel.SetComplete() | |
| 26 | 48 | return installModel, nil | |
| @@ -28,7 +50,8 @@ func HandleInstallationComplete(app *tui.App, msg tui.InstallationCompleteMsg, i | |||
| 28 | 50 | ||
| 29 | 51 | // CreateInstallationCommand creates an installation command from form data | |
| 30 | 52 | // This delegates to the service layer instead of handling directly | |
| 31 | - | func CreateInstallationCommand(app *tui.App, formData models.FormData) tea.Cmd { | |
| 53 | + | // Accepts AppContext interface to avoid circular imports | |
| 54 | + | func CreateInstallationCommand(app AppContext, formData models.FormData) tea.Cmd { | |
| 32 | 55 | return func() tea.Msg { | |
| 33 | 56 | // Start installation via service | |
| 34 | 57 | if err := app.GetInstallService().Start( | |
| @@ -38,19 +61,26 @@ func CreateInstallationCommand(app *tui.App, formData models.FormData) tea.Cmd { | |||
| 38 | 61 | formData.TargetDisk, | |
| 39 | 62 | formData.EncryptionType, | |
| 40 | 63 | ); err != nil { | |
| 41 | - | return tui.InstallationErrorMsg{Err: err} | |
| 64 | + | app.GetLogger().Error("Failed to start installation", "error", err) | |
| 65 | + | // Return error directly - app.go will handle it | |
| 66 | + | return err | |
| 42 | 67 | } | |
| 43 | 68 | ||
| 44 | - | // Subscribe to progress updates and forward them as messages | |
| 69 | + | // Subscribe to progress updates | |
| 45 | 70 | progressChan := app.GetProgressTracker().Subscribe() | |
| 46 | - | for { | |
| 47 | - | select { | |
| 48 | - | case update := <-progressChan: | |
| 49 | - | // Forward progress update to TUI | |
| 50 | - | return tui.ProgressUpdateMsg{Update: update} | |
| 51 | - | case <-app.GetContext().Done(): | |
| 52 | - | return tui.InstallationErrorMsg{Err: app.GetContext().Err()} | |
| 71 | + | go func() { | |
| 72 | + | for { | |
| 73 | + | select { | |
| 74 | + | case <-progressChan: | |
| 75 | + | // Progress updates are broadcast to all subscribers | |
| 76 | + | // App will receive them through its own subscription | |
| 77 | + | case <-app.GetContext().Done(): | |
| 78 | + | app.GetLogger().Info("Installation context cancelled") | |
| 79 | + | return | |
| 80 | + | } | |
| 53 | 81 | } | |
| 54 | - | } | |
| 82 | + | }() | |
| 83 | + | ||
| 84 | + | return nil | |
| 55 | 85 | } | |
| 56 | 86 | } | |
| @@ -2,25 +2,47 @@ package handlers | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | tea "github.com/charmbracelet/bubbletea" | 4 | tea "github.com/charmbracelet/bubbletea" | ||
| 5 | - | "github.com/bnema/archup/internal/interfaces/tui" | 5 | + | "github.com/bnema/archup/internal/application/dto" |
| 6 | "github.com/bnema/archup/internal/interfaces/tui/models" | 6 | "github.com/bnema/archup/internal/interfaces/tui/models" | ||
| 7 | ) | 7 | ) | ||
| 8 | 8 | ||||
| 9 | + | // NOTE: This package uses AppContext interface to avoid importing parent tui package | |||
| 10 | + | // This breaks the circular dependency: tui → tui/handlers → tui | |||
| 11 | + | // Messages are typed by the caller (app.go), not here | |||
| 12 | + | ||||
| 9 | // HandleProgressUpdate processes progress update messages | 13 | // HandleProgressUpdate processes progress update messages | ||
| 10 | - | func HandleProgressUpdate(app *tui.App, msg tui.ProgressUpdateMsg, progressModel *models.ProgressModelImpl) (*models.ProgressModelImpl, tea.Cmd) { | 14 | + | // msg should be ProgressUpdateMsg with an Update field (*dto.ProgressUpdate) |
| 11 | - | progressModel.UpdateProgress(msg.Update) | 15 | + | func HandleProgressUpdate(app AppContext, msg interface{}, progressModel *models.ProgressModelImpl) (*models.ProgressModelImpl, tea.Cmd) { |
| 16 | + | // Cast to a type with Update field | |||
| 17 | + | // This avoids importing the tui package which would create a cycle | |||
| 18 | + | type updateMsg struct { | |||
| 19 | + | Update interface{} | |||
| 20 | + | } | |||
| 21 | + | ||||
| 22 | + | if m, ok := msg.(updateMsg); ok { | |||
| 23 | + | // Cast Update to *dto.ProgressUpdate | |||
| 24 | + | if update, ok := m.Update.(*dto.ProgressUpdate); ok { | |||
| 25 | + | progressModel.UpdateProgress(update) | |||
| 26 | + | } | |||
| 27 | + | } | |||
| 12 | return progressModel, nil | 28 | return progressModel, nil | ||
| 13 | } | 29 | } | ||
| 14 | 30 | ||||
| 15 | // HandleInstallationError processes installation error messages | 31 | // HandleInstallationError processes installation error messages | ||
| 16 | - | func HandleInstallationError(app *tui.App, msg tui.InstallationErrorMsg, installModel *models.InstallationModelImpl) (*models.InstallationModelImpl, tea.Cmd) { | 32 | + | // Signature matches what app.go calls with InstallationErrorMsg types |
| 17 | - | app.GetLogger().Error("Installation error", "error", msg.Err) | 33 | + | func HandleInstallationError(app AppContext, msg interface{}, installModel *models.InstallationModelImpl) (*models.InstallationModelImpl, tea.Cmd) { |
| 18 | - | installModel.SetError(msg.Err.Error()) | 34 | + | // Cast msg to InstallationErrorMsg type (from app.go caller context) |
| 35 | + | if errMsg, ok := msg.(interface{ GetErr() error }); ok { | |||
| 36 | + | err := errMsg.GetErr() | |||
| 37 | + | app.GetLogger().Error("Installation error", "error", err) | |||
| 38 | + | installModel.SetError(err.Error()) | |||
| 39 | + | } | |||
| 19 | return installModel, nil | 40 | return installModel, nil | ||
| 20 | } | 41 | } | ||
| 21 | 42 | ||||
| 22 | // HandleInstallationComplete processes installation completion messages | 43 | // HandleInstallationComplete processes installation completion messages | ||
| 23 | - | func HandleInstallationComplete(app *tui.App, msg tui.InstallationCompleteMsg, installModel *models.InstallationModelImpl) (*models.InstallationModelImpl, tea.Cmd) { | 44 | + | // Signature matches what app.go calls with InstallationCompleteMsg types |
| 45 | + | func HandleInstallationComplete(app AppContext, msg interface{}, installModel *models.InstallationModelImpl) (*models.InstallationModelImpl, tea.Cmd) { | |||
| 24 | app.GetLogger().Info("Installation completed successfully") | 46 | app.GetLogger().Info("Installation completed successfully") | ||
| 25 | installModel.SetComplete() | 47 | installModel.SetComplete() | ||
| 26 | return installModel, nil | 48 | return installModel, nil | ||
| @@ -28,7 +50,8 @@ func HandleInstallationComplete(app *tui.App, msg tui.InstallationCompleteMsg, i | |||||
| 28 | 50 | ||||
| 29 | // CreateInstallationCommand creates an installation command from form data | 51 | // CreateInstallationCommand creates an installation command from form data | ||
| 30 | // This delegates to the service layer instead of handling directly | 52 | // This delegates to the service layer instead of handling directly | ||
| 31 | - | func CreateInstallationCommand(app *tui.App, formData models.FormData) tea.Cmd { | 53 | + | // Accepts AppContext interface to avoid circular imports |
| 54 | + | func CreateInstallationCommand(app AppContext, formData models.FormData) tea.Cmd { | |||
| 32 | return func() tea.Msg { | 55 | return func() tea.Msg { | ||
| 33 | // Start installation via service | 56 | // Start installation via service | ||
| 34 | if err := app.GetInstallService().Start( | 57 | if err := app.GetInstallService().Start( | ||
| @@ -38,19 +61,26 @@ func CreateInstallationCommand(app *tui.App, formData models.FormData) tea.Cmd { | |||||
| 38 | formData.TargetDisk, | 61 | formData.TargetDisk, | ||
| 39 | formData.EncryptionType, | 62 | formData.EncryptionType, | ||
| 40 | ); err != nil { | 63 | ); err != nil { | ||
| 41 | - | return tui.InstallationErrorMsg{Err: err} | 64 | + | app.GetLogger().Error("Failed to start installation", "error", err) |
| 65 | + | // Return error directly - app.go will handle it | |||
| 66 | + | return err | |||
| 42 | } | 67 | } | ||
| 43 | 68 | ||||
| 44 | - | // Subscribe to progress updates and forward them as messages | 69 | + | // Subscribe to progress updates |
| 45 | progressChan := app.GetProgressTracker().Subscribe() | 70 | progressChan := app.GetProgressTracker().Subscribe() | ||
| 46 | - | for { | 71 | + | go func() { |
| 47 | - | select { | 72 | + | for { |
| 48 | - | case update := <-progressChan: | 73 | + | select { |
| 49 | - | // Forward progress update to TUI | 74 | + | case <-progressChan: |
| 50 | - | return tui.ProgressUpdateMsg{Update: update} | 75 | + | // Progress updates are broadcast to all subscribers |
| 51 | - | case <-app.GetContext().Done(): | 76 | + | // App will receive them through its own subscription |
| 52 | - | return tui.InstallationErrorMsg{Err: app.GetContext().Err()} | 77 | + | case <-app.GetContext().Done(): |
| 78 | + | app.GetLogger().Info("Installation context cancelled") | |||
| 79 | + | return | |||
| 80 | + | } | |||
| 53 | } | 81 | } | ||
| 54 | - | } | 82 | + | }() |
| 83 | + | ||||
| 84 | + | return nil | |||
| 55 | } | 85 | } | ||
| 56 | } | 86 | } | ||
A internal/interfaces/tui/handlers/interfaces.go +24 -0
| @@ -0,0 +1,24 @@ | |||
| 1 | + | package handlers | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/archup/internal/application/services" | |
| 7 | + | "github.com/bnema/archup/internal/domain/ports" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | // AppContext defines the interface for accessing app resources from handlers | |
| 11 | + | // This avoids circular dependencies while allowing handlers to access needed services | |
| 12 | + | type AppContext interface { | |
| 13 | + | // GetLogger returns the logger | |
| 14 | + | GetLogger() ports.Logger | |
| 15 | + | ||
| 16 | + | // GetInstallService returns the installation service | |
| 17 | + | GetInstallService() *services.InstallationService | |
| 18 | + | ||
| 19 | + | // GetProgressTracker returns the progress tracker | |
| 20 | + | GetProgressTracker() *services.ProgressTracker | |
| 21 | + | ||
| 22 | + | // GetContext returns the app context | |
| 23 | + | GetContext() context.Context | |
| 24 | + | } | |
| @@ -0,0 +1,24 @@ | |||||
| 1 | + | package handlers | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/archup/internal/application/services" | |||
| 7 | + | "github.com/bnema/archup/internal/domain/ports" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | // AppContext defines the interface for accessing app resources from handlers | |||
| 11 | + | // This avoids circular dependencies while allowing handlers to access needed services | |||
| 12 | + | type AppContext interface { | |||
| 13 | + | // GetLogger returns the logger | |||
| 14 | + | GetLogger() ports.Logger | |||
| 15 | + | ||||
| 16 | + | // GetInstallService returns the installation service | |||
| 17 | + | GetInstallService() *services.InstallationService | |||
| 18 | + | ||||
| 19 | + | // GetProgressTracker returns the progress tracker | |||
| 20 | + | GetProgressTracker() *services.ProgressTracker | |||
| 21 | + | ||||
| 22 | + | // GetContext returns the app context | |||
| 23 | + | GetContext() context.Context | |||
| 24 | + | } | |||