detached 90c6b9a

test(phases): add comprehensive unit tests for base install phase

commit
90c6b9aaab1bb0dc896fef5b50e3690358332f64
author
bnema · · 2025-10-27 19:05 UTC
committer
bnema · · 2025-10-27 21:51 UTC
parent
ed51bf5
signature
unsigned
1 file · +469 -0
A internal/phases/base_test.go +469 -0
@@ -0,0 +1,469 @@
1+package phases
2+
3+import (
4+ "bytes"
5+ "fmt"
6+ "io"
7+ "path/filepath"
8+ "testing"
9+
10+ "github.com/bnema/archup/internal/config"
11+ "github.com/bnema/archup/internal/interfaces/mocks"
12+ "github.com/bnema/archup/internal/logger"
13+ "github.com/bnema/archup/internal/system"
14+ "go.uber.org/mock/gomock"
15+)
16+
17+// TestBaseInstallPhasePreCheck tests /mnt mount validation
18+func TestBaseInstallPhasePreCheck(t *testing.T) {
19+ tests := []struct {
20+ name string
21+ mountResult system.CommandResult
22+ wantErr bool
23+ errContains string
24+ }{
25+ {
26+ name: "/mnt is mounted",
27+ mountResult: system.CommandResult{ExitCode: 0},
28+ wantErr: false,
29+ },
30+ {
31+ name: "/mnt is not mounted",
32+ mountResult: system.CommandResult{ExitCode: 32},
33+ wantErr: true,
34+ errContains: "/mnt is not mounted",
35+ },
36+ }
37+
38+ for _, tt := range tests {
39+ t.Run(tt.name, func(t *testing.T) {
40+ ctrl := gomock.NewController(t)
41+ defer ctrl.Finish()
42+
43+ mockFS := mocks.NewMockFileSystem(ctrl)
44+ mockSysExec := mocks.NewMockSystemExecutor(ctrl)
45+
46+ mockSysExec.EXPECT().RunSimple("mountpoint", "-q", "/mnt").Return(tt.mountResult).Times(1)
47+
48+ tmpDir := t.TempDir()
49+ logPath := filepath.Join(tmpDir, "test.log")
50+ log, err := logger.New(logPath, false)
51+ if err != nil {
52+ t.Fatalf("Failed to create logger: %v", err)
53+ }
54+ defer log.Close()
55+
56+ cfg := config.NewConfig("test")
57+ phase := NewBaseInstallPhase(cfg, log, mockFS, mockSysExec)
58+
59+ err = phase.PreCheck()
60+ if (err != nil) != tt.wantErr {
61+ t.Errorf("PreCheck() error = %v, wantErr %v", err, tt.wantErr)
62+ return
63+ }
64+ if tt.wantErr && err != nil {
65+ if errMsg := err.Error(); tt.errContains != "" && !containsSubstringBaseTest(errMsg, tt.errContains) {
66+ t.Errorf("PreCheck() error = %q, should contain %q", errMsg, tt.errContains)
67+ }
68+ }
69+ })
70+ }
71+}
72+
73+// TestBaseInstallPhaseDetectCPU tests CPU vendor detection
74+func TestBaseInstallPhaseDetectCPU(t *testing.T) {
75+ tests := []struct {
76+ name string
77+ cpuinfoContent string
78+ expectedVendor string
79+ expectedMicrocode string
80+ wantErr bool
81+ }{
82+ {
83+ name: "Intel CPU detected",
84+ cpuinfoContent: "processor\t: 0\nvendor_id\t: GenuineIntel\nflags\t: fpu\n",
85+ expectedVendor: "Intel",
86+ expectedMicrocode: "intel-ucode",
87+ wantErr: false,
88+ },
89+ {
90+ name: "AMD CPU detected",
91+ cpuinfoContent: "processor\t: 0\nvendor_id\t: AuthenticAMD\nflags\t: fpu\n",
92+ expectedVendor: "AMD",
93+ expectedMicrocode: "amd-ucode",
94+ wantErr: false,
95+ },
96+ {
97+ name: "Unknown CPU vendor",
98+ cpuinfoContent: "processor\t: 0\nvendor_id\t: UnknownVendor\nflags\t: fpu\n",
99+ expectedVendor: "Unknown",
100+ expectedMicrocode: "",
101+ wantErr: false,
102+ },
103+ {
104+ name: "Empty cpuinfo",
105+ cpuinfoContent: "",
106+ expectedVendor: "Unknown",
107+ expectedMicrocode: "",
108+ wantErr: false,
109+ },
110+ }
111+
112+ for _, tt := range tests {
113+ t.Run(tt.name, func(t *testing.T) {
114+ ctrl := gomock.NewController(t)
115+ defer ctrl.Finish()
116+
117+ mockFS := mocks.NewMockFileSystem(ctrl)
118+ mockSysExec := mocks.NewMockSystemExecutor(ctrl)
119+
120+ reader := io.NopCloser(bytes.NewBufferString(tt.cpuinfoContent))
121+ mockFS.EXPECT().Open("/proc/cpuinfo").Return(reader, nil).Times(1)
122+
123+ tmpDir := t.TempDir()
124+ logPath := filepath.Join(tmpDir, "test.log")
125+ log, err := logger.New(logPath, false)
126+ if err != nil {
127+ t.Fatalf("Failed to create logger: %v", err)
128+ }
129+ defer log.Close()
130+
131+ cfg := config.NewConfig("test")
132+ phase := NewBaseInstallPhase(cfg, log, mockFS, mockSysExec)
133+
134+ progressChan := make(chan ProgressUpdate, 10)
135+ err = phase.detectCPU(progressChan)
136+
137+ if (err != nil) != tt.wantErr {
138+ t.Errorf("detectCPU() error = %v, wantErr %v", err, tt.wantErr)
139+ return
140+ }
141+
142+ if cfg.CPUVendor != tt.expectedVendor {
143+ t.Errorf("CPUVendor = %q, want %q", cfg.CPUVendor, tt.expectedVendor)
144+ }
145+ if cfg.Microcode != tt.expectedMicrocode {
146+ t.Errorf("Microcode = %q, want %q", cfg.Microcode, tt.expectedMicrocode)
147+ }
148+ })
149+ }
150+}
151+
152+// TestBaseInstallPhaseLoadBasePackages tests package file parsing
153+func TestBaseInstallPhaseLoadBasePackages(t *testing.T) {
154+ tests := []struct {
155+ name string
156+ fileContent string
157+ expectedPackages []string
158+ wantErr bool
159+ errContains string
160+ }{
161+ {
162+ name: "Valid package list",
163+ fileContent: "base\nlinux\nlinux-firmware\ngit\n",
164+ expectedPackages: []string{"base", "linux", "linux-firmware", "git"},
165+ wantErr: false,
166+ },
167+ {
168+ name: "Package list with comments and empty lines",
169+ fileContent: "# Core packages\nbase\n\n# Additional\nlinux\n# Another comment\n\ngit\n",
170+ expectedPackages: []string{"base", "linux", "git"},
171+ wantErr: false,
172+ },
173+ {
174+ name: "Empty file",
175+ fileContent: "",
176+ expectedPackages: []string{},
177+ wantErr: false,
178+ },
179+ {
180+ name: "Only comments",
181+ fileContent: "# Comment 1\n# Comment 2\n",
182+ expectedPackages: []string{},
183+ wantErr: false,
184+ },
185+ {
186+ name: "Only whitespace",
187+ fileContent: " \n\n \n",
188+ expectedPackages: []string{},
189+ wantErr: false,
190+ },
191+ }
192+
193+ for _, tt := range tests {
194+ t.Run(tt.name, func(t *testing.T) {
195+ ctrl := gomock.NewController(t)
196+ defer ctrl.Finish()
197+
198+ mockFS := mocks.NewMockFileSystem(ctrl)
199+ mockSysExec := mocks.NewMockSystemExecutor(ctrl)
200+
201+ reader := io.NopCloser(bytes.NewBufferString(tt.fileContent))
202+ packageFile := config.DefaultInstallDir + "/" + config.BasePackagesFile
203+ mockFS.EXPECT().Open(packageFile).Return(reader, nil).Times(1)
204+
205+ tmpDir := t.TempDir()
206+ logPath := filepath.Join(tmpDir, "test.log")
207+ log, err := logger.New(logPath, false)
208+ if err != nil {
209+ t.Fatalf("Failed to create logger: %v", err)
210+ }
211+ defer log.Close()
212+
213+ cfg := config.NewConfig("test")
214+ phase := NewBaseInstallPhase(cfg, log, mockFS, mockSysExec)
215+
216+ packages, err := phase.loadBasePackages()
217+
218+ if (err != nil) != tt.wantErr {
219+ t.Errorf("loadBasePackages() error = %v, wantErr %v", err, tt.wantErr)
220+ return
221+ }
222+
223+ if len(packages) != len(tt.expectedPackages) {
224+ t.Errorf("loadBasePackages() returned %d packages, want %d", len(packages), len(tt.expectedPackages))
225+ return
226+ }
227+
228+ for i, pkg := range packages {
229+ if pkg != tt.expectedPackages[i] {
230+ t.Errorf("Package %d = %q, want %q", i, pkg, tt.expectedPackages[i])
231+ }
232+ }
233+ })
234+ }
235+}
236+
237+// TestBaseInstallPhaseGenerateFstab tests fstab generation
238+func TestBaseInstallPhaseGenerateFstab(t *testing.T) {
239+ tests := []struct {
240+ name string
241+ genfstabResult system.CommandResult
242+ wantErr bool
243+ errContains string
244+ }{
245+ {
246+ name: "Fstab generation success",
247+ genfstabResult: system.CommandResult{ExitCode: 0},
248+ wantErr: false,
249+ },
250+ {
251+ name: "Genfstab command failed",
252+ genfstabResult: system.CommandResult{ExitCode: 1, Error: fmt.Errorf("permission denied")},
253+ wantErr: true,
254+ errContains: "genfstab failed",
255+ },
256+ }
257+
258+ for _, tt := range tests {
259+ t.Run(tt.name, func(t *testing.T) {
260+ ctrl := gomock.NewController(t)
261+ defer ctrl.Finish()
262+
263+ mockFS := mocks.NewMockFileSystem(ctrl)
264+ mockSysExec := mocks.NewMockSystemExecutor(ctrl)
265+
266+ mockSysExec.EXPECT().RunSimple("sh", "-c", "genfstab -U /mnt >> /mnt/etc/fstab").Return(tt.genfstabResult).Times(1)
267+
268+ tmpDir := t.TempDir()
269+ logPath := filepath.Join(tmpDir, "test.log")
270+ log, err := logger.New(logPath, false)
271+ if err != nil {
272+ t.Fatalf("Failed to create logger: %v", err)
273+ }
274+ defer log.Close()
275+
276+ cfg := config.NewConfig("test")
277+ phase := NewBaseInstallPhase(cfg, log, mockFS, mockSysExec)
278+
279+ progressChan := make(chan ProgressUpdate, 10)
280+ err = phase.generateFstab(progressChan)
281+
282+ if (err != nil) != tt.wantErr {
283+ t.Errorf("generateFstab() error = %v, wantErr %v", err, tt.wantErr)
284+ return
285+ }
286+ if tt.wantErr && err != nil {
287+ if errMsg := err.Error(); tt.errContains != "" && !containsSubstringBaseTest(errMsg, tt.errContains) {
288+ t.Errorf("generateFstab() error = %q, should contain %q", errMsg, tt.errContains)
289+ }
290+ }
291+ })
292+ }
293+}
294+
295+// TestBaseInstallPhasePostCheck tests installation validation
296+func TestBaseInstallPhasePostCheck(t *testing.T) {
297+ tests := []struct {
298+ name string
299+ usrDirExists bool
300+ fstabFileExists bool
301+ wantErr bool
302+ errContains string
303+ }{
304+ {
305+ name: "Both /mnt/usr and /mnt/etc/fstab exist",
306+ usrDirExists: true,
307+ fstabFileExists: true,
308+ wantErr: false,
309+ },
310+ {
311+ name: "/mnt/usr does not exist",
312+ usrDirExists: false,
313+ fstabFileExists: true,
314+ wantErr: true,
315+ errContains: "/mnt/usr does not exist",
316+ },
317+ {
318+ name: "/mnt/etc/fstab does not exist",
319+ usrDirExists: true,
320+ fstabFileExists: false,
321+ wantErr: true,
322+ errContains: "fstab was not created",
323+ },
324+ }
325+
326+ for _, tt := range tests {
327+ t.Run(tt.name, func(t *testing.T) {
328+ ctrl := gomock.NewController(t)
329+ defer ctrl.Finish()
330+
331+ mockFS := mocks.NewMockFileSystem(ctrl)
332+ mockSysExec := mocks.NewMockSystemExecutor(ctrl)
333+
334+ usrResult := system.CommandResult{ExitCode: 1}
335+ if tt.usrDirExists {
336+ usrResult.ExitCode = 0
337+ }
338+ mockSysExec.EXPECT().RunSimple("test", "-d", "/mnt/usr").Return(usrResult).Times(1)
339+
340+ // Only expect fstab check if usr dir exists
341+ if tt.usrDirExists {
342+ fstabResult := system.CommandResult{ExitCode: 1}
343+ if tt.fstabFileExists {
344+ fstabResult.ExitCode = 0
345+ }
346+ mockSysExec.EXPECT().RunSimple("test", "-f", "/mnt/etc/fstab").Return(fstabResult).Times(1)
347+ }
348+
349+ tmpDir := t.TempDir()
350+ logPath := filepath.Join(tmpDir, "test.log")
351+ log, err := logger.New(logPath, false)
352+ if err != nil {
353+ t.Fatalf("Failed to create logger: %v", err)
354+ }
355+ defer log.Close()
356+
357+ cfg := config.NewConfig("test")
358+ cfg.ConfigPath = filepath.Join(tmpDir, "config")
359+
360+ phase := NewBaseInstallPhase(cfg, log, mockFS, mockSysExec)
361+
362+ err = phase.PostCheck()
363+
364+ if (err != nil) != tt.wantErr {
365+ t.Errorf("PostCheck() error = %v, wantErr %v", err, tt.wantErr)
366+ return
367+ }
368+ if tt.wantErr && err != nil {
369+ if errMsg := err.Error(); tt.errContains != "" && !containsSubstringBaseTest(errMsg, tt.errContains) {
370+ t.Errorf("PostCheck() error = %q, should contain %q", errMsg, tt.errContains)
371+ }
372+ }
373+ })
374+ }
375+}
376+
377+// TestBaseInstallPhaseExecute tests Execute calls correct steps
378+func TestBaseInstallPhaseExecute(t *testing.T) {
379+ // Note: Full Execute testing is complex due to logger.ExecCommand() calls
380+ // This test verifies the phase structure and progress updates
381+ tmpDir := t.TempDir()
382+ logPath := filepath.Join(tmpDir, "test.log")
383+ log, err := logger.New(logPath, false)
384+ if err != nil {
385+ t.Fatalf("Failed to create logger: %v", err)
386+ }
387+ defer log.Close()
388+
389+ ctrl := gomock.NewController(t)
390+ defer ctrl.Finish()
391+
392+ mockFS := mocks.NewMockFileSystem(ctrl)
393+ mockSysExec := mocks.NewMockSystemExecutor(ctrl)
394+
395+ cfg := config.NewConfig("test")
396+ phase := NewBaseInstallPhase(cfg, log, mockFS, mockSysExec)
397+
398+ progressChan := make(chan ProgressUpdate, 100)
399+
400+ // Just verify that Execute sends progress updates (higher-level behavior)
401+ // since testing the full Execute requires mocking logger.ExecCommand
402+ result := phase.Execute(progressChan)
403+
404+ // Result may be error due to logger.ExecCommand, but we should get a PhaseResult
405+ if result.Message == "" && !result.Success {
406+ // This is expected when logger.ExecCommand is called
407+ t.Logf("Execute correctly attempted to run - logger.ExecCommand not mocked")
408+ }
409+}
410+
411+// TestBaseInstallPhaseRollback tests rollback
412+func TestBaseInstallPhaseRollback(t *testing.T) {
413+ ctrl := gomock.NewController(t)
414+ defer ctrl.Finish()
415+
416+ mockFS := mocks.NewMockFileSystem(ctrl)
417+ mockSysExec := mocks.NewMockSystemExecutor(ctrl)
418+
419+ tmpDir := t.TempDir()
420+ logPath := filepath.Join(tmpDir, "test.log")
421+ log, err := logger.New(logPath, false)
422+ if err != nil {
423+ t.Fatalf("Failed to create logger: %v", err)
424+ }
425+ defer log.Close()
426+
427+ cfg := config.NewConfig("test")
428+ phase := NewBaseInstallPhase(cfg, log, mockFS, mockSysExec)
429+
430+ // BaseInstallPhase Rollback should be no-op
431+ err = phase.Rollback()
432+ if err != nil {
433+ t.Errorf("Rollback() error = %v, want nil", err)
434+ }
435+}
436+
437+// TestBaseInstallPhaseCanSkip tests CanSkip
438+func TestBaseInstallPhaseCanSkip(t *testing.T) {
439+ ctrl := gomock.NewController(t)
440+ defer ctrl.Finish()
441+
442+ mockFS := mocks.NewMockFileSystem(ctrl)
443+ mockSysExec := mocks.NewMockSystemExecutor(ctrl)
444+
445+ tmpDir := t.TempDir()
446+ logPath := filepath.Join(tmpDir, "test.log")
447+ log, err := logger.New(logPath, false)
448+ if err != nil {
449+ t.Fatalf("Failed to create logger: %v", err)
450+ }
451+ defer log.Close()
452+
453+ cfg := config.NewConfig("test")
454+ phase := NewBaseInstallPhase(cfg, log, mockFS, mockSysExec)
455+
456+ if phase.CanSkip() {
457+ t.Error("CanSkip() = true, want false")
458+ }
459+}
460+
461+// Helper function for substring matching (avoid duplication across test files)
462+func containsSubstringBaseTest(s, substr string) bool {
463+ for i := 0; i < len(s)-len(substr)+1; i++ {
464+ if s[i:i+len(substr)] == substr {
465+ return true
466+ }
467+ }
468+ return false
469+}
@@ -0,0 +1,469 @@
1+package phases
2+
3+import (
4+ "bytes"
5+ "fmt"
6+ "io"
7+ "path/filepath"
8+ "testing"
9+
10+ "github.com/bnema/archup/internal/config"
11+ "github.com/bnema/archup/internal/interfaces/mocks"
12+ "github.com/bnema/archup/internal/logger"
13+ "github.com/bnema/archup/internal/system"
14+ "go.uber.org/mock/gomock"
15+)
16+
17+// TestBaseInstallPhasePreCheck tests /mnt mount validation
18+func TestBaseInstallPhasePreCheck(t *testing.T) {
19+ tests := []struct {
20+ name string
21+ mountResult system.CommandResult
22+ wantErr bool
23+ errContains string
24+ }{
25+ {
26+ name: "/mnt is mounted",
27+ mountResult: system.CommandResult{ExitCode: 0},
28+ wantErr: false,
29+ },
30+ {
31+ name: "/mnt is not mounted",
32+ mountResult: system.CommandResult{ExitCode: 32},
33+ wantErr: true,
34+ errContains: "/mnt is not mounted",
35+ },
36+ }
37+
38+ for _, tt := range tests {
39+ t.Run(tt.name, func(t *testing.T) {
40+ ctrl := gomock.NewController(t)
41+ defer ctrl.Finish()
42+
43+ mockFS := mocks.NewMockFileSystem(ctrl)
44+ mockSysExec := mocks.NewMockSystemExecutor(ctrl)
45+
46+ mockSysExec.EXPECT().RunSimple("mountpoint", "-q", "/mnt").Return(tt.mountResult).Times(1)
47+
48+ tmpDir := t.TempDir()
49+ logPath := filepath.Join(tmpDir, "test.log")
50+ log, err := logger.New(logPath, false)
51+ if err != nil {
52+ t.Fatalf("Failed to create logger: %v", err)
53+ }
54+ defer log.Close()
55+
56+ cfg := config.NewConfig("test")
57+ phase := NewBaseInstallPhase(cfg, log, mockFS, mockSysExec)
58+
59+ err = phase.PreCheck()
60+ if (err != nil) != tt.wantErr {
61+ t.Errorf("PreCheck() error = %v, wantErr %v", err, tt.wantErr)
62+ return
63+ }
64+ if tt.wantErr && err != nil {
65+ if errMsg := err.Error(); tt.errContains != "" && !containsSubstringBaseTest(errMsg, tt.errContains) {
66+ t.Errorf("PreCheck() error = %q, should contain %q", errMsg, tt.errContains)
67+ }
68+ }
69+ })
70+ }
71+}
72+
73+// TestBaseInstallPhaseDetectCPU tests CPU vendor detection
74+func TestBaseInstallPhaseDetectCPU(t *testing.T) {
75+ tests := []struct {
76+ name string
77+ cpuinfoContent string
78+ expectedVendor string
79+ expectedMicrocode string
80+ wantErr bool
81+ }{
82+ {
83+ name: "Intel CPU detected",
84+ cpuinfoContent: "processor\t: 0\nvendor_id\t: GenuineIntel\nflags\t: fpu\n",
85+ expectedVendor: "Intel",
86+ expectedMicrocode: "intel-ucode",
87+ wantErr: false,
88+ },
89+ {
90+ name: "AMD CPU detected",
91+ cpuinfoContent: "processor\t: 0\nvendor_id\t: AuthenticAMD\nflags\t: fpu\n",
92+ expectedVendor: "AMD",
93+ expectedMicrocode: "amd-ucode",
94+ wantErr: false,
95+ },
96+ {
97+ name: "Unknown CPU vendor",
98+ cpuinfoContent: "processor\t: 0\nvendor_id\t: UnknownVendor\nflags\t: fpu\n",
99+ expectedVendor: "Unknown",
100+ expectedMicrocode: "",
101+ wantErr: false,
102+ },
103+ {
104+ name: "Empty cpuinfo",
105+ cpuinfoContent: "",
106+ expectedVendor: "Unknown",
107+ expectedMicrocode: "",
108+ wantErr: false,
109+ },
110+ }
111+
112+ for _, tt := range tests {
113+ t.Run(tt.name, func(t *testing.T) {
114+ ctrl := gomock.NewController(t)
115+ defer ctrl.Finish()
116+
117+ mockFS := mocks.NewMockFileSystem(ctrl)
118+ mockSysExec := mocks.NewMockSystemExecutor(ctrl)
119+
120+ reader := io.NopCloser(bytes.NewBufferString(tt.cpuinfoContent))
121+ mockFS.EXPECT().Open("/proc/cpuinfo").Return(reader, nil).Times(1)
122+
123+ tmpDir := t.TempDir()
124+ logPath := filepath.Join(tmpDir, "test.log")
125+ log, err := logger.New(logPath, false)
126+ if err != nil {
127+ t.Fatalf("Failed to create logger: %v", err)
128+ }
129+ defer log.Close()
130+
131+ cfg := config.NewConfig("test")
132+ phase := NewBaseInstallPhase(cfg, log, mockFS, mockSysExec)
133+
134+ progressChan := make(chan ProgressUpdate, 10)
135+ err = phase.detectCPU(progressChan)
136+
137+ if (err != nil) != tt.wantErr {
138+ t.Errorf("detectCPU() error = %v, wantErr %v", err, tt.wantErr)
139+ return
140+ }
141+
142+ if cfg.CPUVendor != tt.expectedVendor {
143+ t.Errorf("CPUVendor = %q, want %q", cfg.CPUVendor, tt.expectedVendor)
144+ }
145+ if cfg.Microcode != tt.expectedMicrocode {
146+ t.Errorf("Microcode = %q, want %q", cfg.Microcode, tt.expectedMicrocode)
147+ }
148+ })
149+ }
150+}
151+
152+// TestBaseInstallPhaseLoadBasePackages tests package file parsing
153+func TestBaseInstallPhaseLoadBasePackages(t *testing.T) {
154+ tests := []struct {
155+ name string
156+ fileContent string
157+ expectedPackages []string
158+ wantErr bool
159+ errContains string
160+ }{
161+ {
162+ name: "Valid package list",
163+ fileContent: "base\nlinux\nlinux-firmware\ngit\n",
164+ expectedPackages: []string{"base", "linux", "linux-firmware", "git"},
165+ wantErr: false,
166+ },
167+ {
168+ name: "Package list with comments and empty lines",
169+ fileContent: "# Core packages\nbase\n\n# Additional\nlinux\n# Another comment\n\ngit\n",
170+ expectedPackages: []string{"base", "linux", "git"},
171+ wantErr: false,
172+ },
173+ {
174+ name: "Empty file",
175+ fileContent: "",
176+ expectedPackages: []string{},
177+ wantErr: false,
178+ },
179+ {
180+ name: "Only comments",
181+ fileContent: "# Comment 1\n# Comment 2\n",
182+ expectedPackages: []string{},
183+ wantErr: false,
184+ },
185+ {
186+ name: "Only whitespace",
187+ fileContent: " \n\n \n",
188+ expectedPackages: []string{},
189+ wantErr: false,
190+ },
191+ }
192+
193+ for _, tt := range tests {
194+ t.Run(tt.name, func(t *testing.T) {
195+ ctrl := gomock.NewController(t)
196+ defer ctrl.Finish()
197+
198+ mockFS := mocks.NewMockFileSystem(ctrl)
199+ mockSysExec := mocks.NewMockSystemExecutor(ctrl)
200+
201+ reader := io.NopCloser(bytes.NewBufferString(tt.fileContent))
202+ packageFile := config.DefaultInstallDir + "/" + config.BasePackagesFile
203+ mockFS.EXPECT().Open(packageFile).Return(reader, nil).Times(1)
204+
205+ tmpDir := t.TempDir()
206+ logPath := filepath.Join(tmpDir, "test.log")
207+ log, err := logger.New(logPath, false)
208+ if err != nil {
209+ t.Fatalf("Failed to create logger: %v", err)
210+ }
211+ defer log.Close()
212+
213+ cfg := config.NewConfig("test")
214+ phase := NewBaseInstallPhase(cfg, log, mockFS, mockSysExec)
215+
216+ packages, err := phase.loadBasePackages()
217+
218+ if (err != nil) != tt.wantErr {
219+ t.Errorf("loadBasePackages() error = %v, wantErr %v", err, tt.wantErr)
220+ return
221+ }
222+
223+ if len(packages) != len(tt.expectedPackages) {
224+ t.Errorf("loadBasePackages() returned %d packages, want %d", len(packages), len(tt.expectedPackages))
225+ return
226+ }
227+
228+ for i, pkg := range packages {
229+ if pkg != tt.expectedPackages[i] {
230+ t.Errorf("Package %d = %q, want %q", i, pkg, tt.expectedPackages[i])
231+ }
232+ }
233+ })
234+ }
235+}
236+
237+// TestBaseInstallPhaseGenerateFstab tests fstab generation
238+func TestBaseInstallPhaseGenerateFstab(t *testing.T) {
239+ tests := []struct {
240+ name string
241+ genfstabResult system.CommandResult
242+ wantErr bool
243+ errContains string
244+ }{
245+ {
246+ name: "Fstab generation success",
247+ genfstabResult: system.CommandResult{ExitCode: 0},
248+ wantErr: false,
249+ },
250+ {
251+ name: "Genfstab command failed",
252+ genfstabResult: system.CommandResult{ExitCode: 1, Error: fmt.Errorf("permission denied")},
253+ wantErr: true,
254+ errContains: "genfstab failed",
255+ },
256+ }
257+
258+ for _, tt := range tests {
259+ t.Run(tt.name, func(t *testing.T) {
260+ ctrl := gomock.NewController(t)
261+ defer ctrl.Finish()
262+
263+ mockFS := mocks.NewMockFileSystem(ctrl)
264+ mockSysExec := mocks.NewMockSystemExecutor(ctrl)
265+
266+ mockSysExec.EXPECT().RunSimple("sh", "-c", "genfstab -U /mnt >> /mnt/etc/fstab").Return(tt.genfstabResult).Times(1)
267+
268+ tmpDir := t.TempDir()
269+ logPath := filepath.Join(tmpDir, "test.log")
270+ log, err := logger.New(logPath, false)
271+ if err != nil {
272+ t.Fatalf("Failed to create logger: %v", err)
273+ }
274+ defer log.Close()
275+
276+ cfg := config.NewConfig("test")
277+ phase := NewBaseInstallPhase(cfg, log, mockFS, mockSysExec)
278+
279+ progressChan := make(chan ProgressUpdate, 10)
280+ err = phase.generateFstab(progressChan)
281+
282+ if (err != nil) != tt.wantErr {
283+ t.Errorf("generateFstab() error = %v, wantErr %v", err, tt.wantErr)
284+ return
285+ }
286+ if tt.wantErr && err != nil {
287+ if errMsg := err.Error(); tt.errContains != "" && !containsSubstringBaseTest(errMsg, tt.errContains) {
288+ t.Errorf("generateFstab() error = %q, should contain %q", errMsg, tt.errContains)
289+ }
290+ }
291+ })
292+ }
293+}
294+
295+// TestBaseInstallPhasePostCheck tests installation validation
296+func TestBaseInstallPhasePostCheck(t *testing.T) {
297+ tests := []struct {
298+ name string
299+ usrDirExists bool
300+ fstabFileExists bool
301+ wantErr bool
302+ errContains string
303+ }{
304+ {
305+ name: "Both /mnt/usr and /mnt/etc/fstab exist",
306+ usrDirExists: true,
307+ fstabFileExists: true,
308+ wantErr: false,
309+ },
310+ {
311+ name: "/mnt/usr does not exist",
312+ usrDirExists: false,
313+ fstabFileExists: true,
314+ wantErr: true,
315+ errContains: "/mnt/usr does not exist",
316+ },
317+ {
318+ name: "/mnt/etc/fstab does not exist",
319+ usrDirExists: true,
320+ fstabFileExists: false,
321+ wantErr: true,
322+ errContains: "fstab was not created",
323+ },
324+ }
325+
326+ for _, tt := range tests {
327+ t.Run(tt.name, func(t *testing.T) {
328+ ctrl := gomock.NewController(t)
329+ defer ctrl.Finish()
330+
331+ mockFS := mocks.NewMockFileSystem(ctrl)
332+ mockSysExec := mocks.NewMockSystemExecutor(ctrl)
333+
334+ usrResult := system.CommandResult{ExitCode: 1}
335+ if tt.usrDirExists {
336+ usrResult.ExitCode = 0
337+ }
338+ mockSysExec.EXPECT().RunSimple("test", "-d", "/mnt/usr").Return(usrResult).Times(1)
339+
340+ // Only expect fstab check if usr dir exists
341+ if tt.usrDirExists {
342+ fstabResult := system.CommandResult{ExitCode: 1}
343+ if tt.fstabFileExists {
344+ fstabResult.ExitCode = 0
345+ }
346+ mockSysExec.EXPECT().RunSimple("test", "-f", "/mnt/etc/fstab").Return(fstabResult).Times(1)
347+ }
348+
349+ tmpDir := t.TempDir()
350+ logPath := filepath.Join(tmpDir, "test.log")
351+ log, err := logger.New(logPath, false)
352+ if err != nil {
353+ t.Fatalf("Failed to create logger: %v", err)
354+ }
355+ defer log.Close()
356+
357+ cfg := config.NewConfig("test")
358+ cfg.ConfigPath = filepath.Join(tmpDir, "config")
359+
360+ phase := NewBaseInstallPhase(cfg, log, mockFS, mockSysExec)
361+
362+ err = phase.PostCheck()
363+
364+ if (err != nil) != tt.wantErr {
365+ t.Errorf("PostCheck() error = %v, wantErr %v", err, tt.wantErr)
366+ return
367+ }
368+ if tt.wantErr && err != nil {
369+ if errMsg := err.Error(); tt.errContains != "" && !containsSubstringBaseTest(errMsg, tt.errContains) {
370+ t.Errorf("PostCheck() error = %q, should contain %q", errMsg, tt.errContains)
371+ }
372+ }
373+ })
374+ }
375+}
376+
377+// TestBaseInstallPhaseExecute tests Execute calls correct steps
378+func TestBaseInstallPhaseExecute(t *testing.T) {
379+ // Note: Full Execute testing is complex due to logger.ExecCommand() calls
380+ // This test verifies the phase structure and progress updates
381+ tmpDir := t.TempDir()
382+ logPath := filepath.Join(tmpDir, "test.log")
383+ log, err := logger.New(logPath, false)
384+ if err != nil {
385+ t.Fatalf("Failed to create logger: %v", err)
386+ }
387+ defer log.Close()
388+
389+ ctrl := gomock.NewController(t)
390+ defer ctrl.Finish()
391+
392+ mockFS := mocks.NewMockFileSystem(ctrl)
393+ mockSysExec := mocks.NewMockSystemExecutor(ctrl)
394+
395+ cfg := config.NewConfig("test")
396+ phase := NewBaseInstallPhase(cfg, log, mockFS, mockSysExec)
397+
398+ progressChan := make(chan ProgressUpdate, 100)
399+
400+ // Just verify that Execute sends progress updates (higher-level behavior)
401+ // since testing the full Execute requires mocking logger.ExecCommand
402+ result := phase.Execute(progressChan)
403+
404+ // Result may be error due to logger.ExecCommand, but we should get a PhaseResult
405+ if result.Message == "" && !result.Success {
406+ // This is expected when logger.ExecCommand is called
407+ t.Logf("Execute correctly attempted to run - logger.ExecCommand not mocked")
408+ }
409+}
410+
411+// TestBaseInstallPhaseRollback tests rollback
412+func TestBaseInstallPhaseRollback(t *testing.T) {
413+ ctrl := gomock.NewController(t)
414+ defer ctrl.Finish()
415+
416+ mockFS := mocks.NewMockFileSystem(ctrl)
417+ mockSysExec := mocks.NewMockSystemExecutor(ctrl)
418+
419+ tmpDir := t.TempDir()
420+ logPath := filepath.Join(tmpDir, "test.log")
421+ log, err := logger.New(logPath, false)
422+ if err != nil {
423+ t.Fatalf("Failed to create logger: %v", err)
424+ }
425+ defer log.Close()
426+
427+ cfg := config.NewConfig("test")
428+ phase := NewBaseInstallPhase(cfg, log, mockFS, mockSysExec)
429+
430+ // BaseInstallPhase Rollback should be no-op
431+ err = phase.Rollback()
432+ if err != nil {
433+ t.Errorf("Rollback() error = %v, want nil", err)
434+ }
435+}
436+
437+// TestBaseInstallPhaseCanSkip tests CanSkip
438+func TestBaseInstallPhaseCanSkip(t *testing.T) {
439+ ctrl := gomock.NewController(t)
440+ defer ctrl.Finish()
441+
442+ mockFS := mocks.NewMockFileSystem(ctrl)
443+ mockSysExec := mocks.NewMockSystemExecutor(ctrl)
444+
445+ tmpDir := t.TempDir()
446+ logPath := filepath.Join(tmpDir, "test.log")
447+ log, err := logger.New(logPath, false)
448+ if err != nil {
449+ t.Fatalf("Failed to create logger: %v", err)
450+ }
451+ defer log.Close()
452+
453+ cfg := config.NewConfig("test")
454+ phase := NewBaseInstallPhase(cfg, log, mockFS, mockSysExec)
455+
456+ if phase.CanSkip() {
457+ t.Error("CanSkip() = true, want false")
458+ }
459+}
460+
461+// Helper function for substring matching (avoid duplication across test files)
462+func containsSubstringBaseTest(s, substr string) bool {
463+ for i := 0; i < len(s)-len(substr)+1; i++ {
464+ if s[i:i+len(substr)] == substr {
465+ return true
466+ }
467+ }
468+ return false
469+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 0

    ↑↓ moveenter openesc close