package lockfile import ( "os" "path/filepath" "testing" ) func readFile(t *testing.T, path string) string { t.Helper() data, err := os.ReadFile(path) if err != nil { t.Fatal(err) } return string(data) } func TestMarshalDeterministic(t *testing.T) { l1 := New() l1.Sources["beta"] = &Source{ URL: "https://example.com/beta.git", Ref: "main", Rev: "cafe123", Selection: Selection{Mode: ModeAll}, Renames: map[string]string{"zeta": "beta-zeta"}, Inventory: map[string][]string{TypeSkill: {"a", "zeta"}}, Warnings: []string{"a-warning", "b-warning"}, } l1.Sources["alpha"] = &Source{ URL: "https://example.com/alpha.git", Ref: "v1.0", Rev: "abcd00", Selection: Selection{Mode: ModeInclude, Include: []string{"a", "x"}}, } l2 := New() l2.Sources["alpha"] = l1.Sources["alpha"] l2.Sources["beta"] = l1.Sources["beta"] b1, err := Marshal(l1) if err != nil { t.Fatal(err) } b2, err := Marshal(l2) if err != nil { t.Fatal(err) } if string(b1) != string(b2) { t.Fatalf("marshal not deterministic:\n%s\n--- vs ---\n%s", b1, b2) } if b1[len(b1)-1] != '\n' { t.Error("missing trailing newline") } l1.Save(filepath.Join(t.TempDir(), FileName)) b3, err := Marshal(l1) if err != nil { t.Fatal(err) } if string(b1) != string(b3) { t.Fatalf("save mutated already-canonical bytes:\n%s\n--- vs ---\n%s", b1, b3) } } func TestSaveCanonicalizesUnsortedInput(t *testing.T) { l := New() l.Sources["s"] = &Source{ URL: "u", Ref: "r", Rev: "rev", Selection: Selection{Mode: ModeInclude, Include: []string{"z", "a"}}, Warnings: []string{"w2", "w1"}, Inventory: map[string][]string{TypeSkill: {"z", "a"}}, } path := filepath.Join(t.TempDir(), FileName) if err := l.Save(path); err != nil { t.Fatal(err) } got, err := Load(path) if err != nil { t.Fatal(err) } src := got.Sources["s"] wantInclude := []string{"a", "z"} if !slicesEqual(src.Selection.Include, wantInclude) { t.Errorf("include = %v, want %v", src.Selection.Include, wantInclude) } wantWarnings := []string{"w1", "w2"} if !slicesEqual(src.Warnings, wantWarnings) { t.Errorf("warnings = %v, want %v", src.Warnings, wantWarnings) } wantInv := []string{"a", "z"} if !slicesEqual(src.Inventory[TypeSkill], wantInv) { t.Errorf("inventory = %v, want %v", src.Inventory[TypeSkill], wantInv) } } func slicesEqual(a, b []string) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true } func TestLoadRoundTrip(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, FileName) l := New() l.Sources["s"] = &Source{URL: "u", Ref: "r", Rev: "rev1", Selection: Selection{Mode: ModeAll}} if err := l.Save(path); err != nil { t.Fatal(err) } got, err := Load(path) if err != nil { t.Fatal(err) } if got.Version != Version || got.Sources["s"].Rev != "rev1" { t.Fatalf("round trip lost data: %+v", got) } if _, err := Load(filepath.Join(dir, "missing.json")); err != ErrNotExist { t.Fatalf("want ErrNotExist, got %v", err) } os.WriteFile(path, []byte(`{"version": 1, "sources": {}}`), 0o644) if _, err := Load(path); err == nil { t.Fatal("version 1 lockfile must be rejected") } }