package grubenv import ( "os" "path/filepath" "strings" "testing" ) // createTestGrubenv writes a properly formatted grubenv file for testing. // GRUB requires the file to be exactly 1024 bytes, padded with '#'. func createTestGrubenv(t *testing.T, dir string, vars map[string]string) string { t.Helper() path := filepath.Join(dir, "grubenv") var sb strings.Builder sb.WriteString("# GRUB Environment Block\n") for k, v := range vars { sb.WriteString(k + "=" + v + "\n") } content := sb.String() padding := 1024 - len(content) if padding > 0 { content += strings.Repeat("#", padding) } if err := os.WriteFile(path, []byte(content), 0o644); err != nil { t.Fatal(err) } return path } func TestNew(t *testing.T) { env := New("") if env.path != DefaultGrubenvPath { t.Errorf("expected default path %s, got %s", DefaultGrubenvPath, env.path) } env = New("/custom/path/grubenv") if env.path != "/custom/path/grubenv" { t.Errorf("expected custom path, got %s", env.path) } } func TestReadAll(t *testing.T) { dir := t.TempDir() path := createTestGrubenv(t, dir, map[string]string{ "active_slot": "A", "boot_counter": "3", "boot_success": "1", }) env := New(path) vars, err := env.ReadAll() if err != nil { t.Fatal(err) } if vars["active_slot"] != "A" { t.Errorf("active_slot: expected A, got %s", vars["active_slot"]) } if vars["boot_counter"] != "3" { t.Errorf("boot_counter: expected 3, got %s", vars["boot_counter"]) } if vars["boot_success"] != "1" { t.Errorf("boot_success: expected 1, got %s", vars["boot_success"]) } } func TestGet(t *testing.T) { dir := t.TempDir() path := createTestGrubenv(t, dir, map[string]string{ "active_slot": "B", }) env := New(path) val, err := env.Get("active_slot") if err != nil { t.Fatal(err) } if val != "B" { t.Errorf("expected B, got %s", val) } _, err = env.Get("nonexistent") if err == nil { t.Fatal("expected error for nonexistent key") } } func TestSet(t *testing.T) { dir := t.TempDir() path := createTestGrubenv(t, dir, map[string]string{ "active_slot": "A", "boot_counter": "3", }) env := New(path) if err := env.Set("boot_counter", "2"); err != nil { t.Fatal(err) } val, err := env.Get("boot_counter") if err != nil { t.Fatal(err) } if val != "2" { t.Errorf("expected 2 after set, got %s", val) } // Verify file is still 1024 bytes data, err := os.ReadFile(path) if err != nil { t.Fatal(err) } if len(data) != 1024 { t.Errorf("grubenv should be 1024 bytes, got %d", len(data)) } } func TestActiveSlot(t *testing.T) { dir := t.TempDir() path := createTestGrubenv(t, dir, map[string]string{ "active_slot": "A", "boot_counter": "3", "boot_success": "1", }) env := New(path) slot, err := env.ActiveSlot() if err != nil { t.Fatal(err) } if slot != "A" { t.Errorf("expected A, got %s", slot) } } func TestPassiveSlot(t *testing.T) { tests := []struct { active string passive string }{ {"A", "B"}, {"B", "A"}, } for _, tt := range tests { t.Run("active_"+tt.active, func(t *testing.T) { dir := t.TempDir() path := createTestGrubenv(t, dir, map[string]string{ "active_slot": tt.active, }) env := New(path) passive, err := env.PassiveSlot() if err != nil { t.Fatal(err) } if passive != tt.passive { t.Errorf("expected passive %s, got %s", tt.passive, passive) } }) } } func TestBootCounter(t *testing.T) { tests := []struct { value string expect int wantErr bool }{ {"0", 0, false}, {"1", 1, false}, {"2", 2, false}, {"3", 3, false}, {"invalid", -1, true}, {"99", -1, true}, } for _, tt := range tests { t.Run("counter_"+tt.value, func(t *testing.T) { dir := t.TempDir() path := createTestGrubenv(t, dir, map[string]string{ "boot_counter": tt.value, }) env := New(path) counter, err := env.BootCounter() if tt.wantErr { if err == nil { t.Fatal("expected error") } return } if err != nil { t.Fatal(err) } if counter != tt.expect { t.Errorf("expected %d, got %d", tt.expect, counter) } }) } } func TestBootSuccess(t *testing.T) { tests := []struct { value string expect bool }{ {"0", false}, {"1", true}, } for _, tt := range tests { t.Run("success_"+tt.value, func(t *testing.T) { dir := t.TempDir() path := createTestGrubenv(t, dir, map[string]string{ "boot_success": tt.value, }) env := New(path) success, err := env.BootSuccess() if err != nil { t.Fatal(err) } if success != tt.expect { t.Errorf("expected %v, got %v", tt.expect, success) } }) } } func TestMarkBootSuccess(t *testing.T) { dir := t.TempDir() path := createTestGrubenv(t, dir, map[string]string{ "active_slot": "B", "boot_counter": "1", "boot_success": "0", }) env := New(path) if err := env.MarkBootSuccess(); err != nil { t.Fatal(err) } success, err := env.BootSuccess() if err != nil { t.Fatal(err) } if !success { t.Error("expected boot_success=1 after MarkBootSuccess") } counter, err := env.BootCounter() if err != nil { t.Fatal(err) } if counter != 3 { t.Errorf("expected boot_counter=3 after MarkBootSuccess, got %d", counter) } } func TestActivateSlot(t *testing.T) { dir := t.TempDir() path := createTestGrubenv(t, dir, map[string]string{ "active_slot": "A", "boot_counter": "3", "boot_success": "1", }) env := New(path) if err := env.ActivateSlot("B"); err != nil { t.Fatal(err) } slot, _ := env.ActiveSlot() if slot != "B" { t.Errorf("expected active_slot=B, got %s", slot) } counter, _ := env.BootCounter() if counter != 3 { t.Errorf("expected boot_counter=3, got %d", counter) } success, _ := env.BootSuccess() if success { t.Error("expected boot_success=0 after ActivateSlot") } } func TestActivateSlotInvalid(t *testing.T) { dir := t.TempDir() path := createTestGrubenv(t, dir, map[string]string{ "active_slot": "A", }) env := New(path) err := env.ActivateSlot("C") if err == nil { t.Fatal("expected error for invalid slot") } } func TestForceRollback(t *testing.T) { dir := t.TempDir() path := createTestGrubenv(t, dir, map[string]string{ "active_slot": "A", "boot_counter": "3", "boot_success": "1", }) env := New(path) if err := env.ForceRollback(); err != nil { t.Fatal(err) } slot, _ := env.ActiveSlot() if slot != "B" { t.Errorf("expected active_slot=B after rollback from A, got %s", slot) } } func TestParseEnvOutput(t *testing.T) { input := `# GRUB Environment Block active_slot=A boot_counter=3 boot_success=1 ` vars := parseEnvOutput(input) if len(vars) != 3 { t.Errorf("expected 3 variables, got %d", len(vars)) } if vars["active_slot"] != "A" { t.Errorf("active_slot: expected A, got %s", vars["active_slot"]) } if vars["boot_counter"] != "3" { t.Errorf("boot_counter: expected 3, got %s", vars["boot_counter"]) } } func TestWriteManualFormat(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "grubenv") env := New(path) // Use setManual directly since grub-editenv may not be available err := env.setManual("test_key", "test_value") if err != nil { t.Fatal(err) } data, err := os.ReadFile(path) if err != nil { t.Fatal(err) } if len(data) != 1024 { t.Errorf("grubenv should be exactly 1024 bytes, got %d", len(data)) } if !strings.HasPrefix(string(data), "# GRUB Environment Block\n") { t.Error("grubenv should start with '# GRUB Environment Block'") } if !strings.Contains(string(data), "test_key=test_value\n") { t.Error("grubenv should contain test_key=test_value") } } func TestReadNonexistentFile(t *testing.T) { env := New("/nonexistent/path/grubenv") _, err := env.ReadAll() if err == nil { t.Fatal("expected error reading nonexistent file") } } func TestMultipleSetOperations(t *testing.T) { dir := t.TempDir() path := createTestGrubenv(t, dir, map[string]string{ "active_slot": "A", "boot_counter": "3", "boot_success": "1", }) env := New(path) // Simulate a boot cycle: decrement counter, then mark success if err := env.Set("boot_counter", "2"); err != nil { t.Fatal(err) } if err := env.Set("boot_success", "0"); err != nil { t.Fatal(err) } // Now mark boot success if err := env.MarkBootSuccess(); err != nil { t.Fatal(err) } // Verify final state vars, err := env.ReadAll() if err != nil { t.Fatal(err) } if vars["active_slot"] != "A" { t.Errorf("active_slot should still be A, got %s", vars["active_slot"]) } if vars["boot_counter"] != "3" { t.Errorf("boot_counter should be 3 after mark success, got %s", vars["boot_counter"]) } if vars["boot_success"] != "1" { t.Errorf("boot_success should be 1, got %s", vars["boot_success"]) } }