git/intake/main
internal/sheet/sheet_test.go
161 lines5 KB last change raw · history
package sheet

import (
	"strings"
	"testing"
)

func sample() Sheet {
	return Sheet{
		Album:       "The Perfect Crime",
		AlbumArtist: "Straylight Run",
		ArtistDir:   "straylight-run",
		AlbumDir:    "the-perfect-crime",
		Year:        "2004",
		Cover:       "",
		Tracks: []Track{
			{ID: 1, Title: "The Perfect Crime #2", Artist: "", Disc: 1, Number: 1, Source: "01 one.flac", Derived: "1-01_the-perfect-crime-2.flac"},
			{ID: 2, Title: "Handcuffs", Artist: "Someone Else", Disc: 1, Number: 2, Source: "02 two.flac", Derived: "1-02_handcuffs.flac"},
			{ID: 3, Title: "A/B", Artist: "", Disc: 2, Number: 1, Source: "03 three.flac", Derived: "2-01_a_b.flac"},
		},
	}
}

func TestRenderParseRoundTrip(t *testing.T) {
	before := sample()
	got, err := Parse(Render(before), before)
	if err != nil {
		t.Fatal(err)
	}
	if got.Album != before.Album || got.AlbumArtist != before.AlbumArtist {
		t.Errorf("album fields changed: %+v", got)
	}
	if got.ArtistDir != before.ArtistDir || got.AlbumDir != before.AlbumDir || got.Year != before.Year {
		t.Errorf("folder fields changed: %+v", got)
	}
	if len(got.Tracks) != len(before.Tracks) {
		t.Fatalf("got %d tracks, want %d", len(got.Tracks), len(before.Tracks))
	}
	for i := range before.Tracks {
		if got.Tracks[i] != before.Tracks[i] {
			t.Errorf("track %d: got %+v, want %+v", i, got.Tracks[i], before.Tracks[i])
		}
	}
	if c := got.ChangedFrom(before); c.Any() {
		t.Errorf("a round trip reported changes: %+v", c)
	}
}

// A '#' is ordinary text in a title, and a ':' is ordinary text in a title. The
// old editor stripped an inline ' #' comment, which quietly truncated any title
// that contained one.
func TestHashesAndColonsSurviveInValues(t *testing.T) {
	before := sample()
	text := Render(before)
	text = strings.Replace(text, "title:        The Perfect Crime #2", "title:        Track #4: Reprise #2", 1)

	got, err := Parse(text, before)
	if err != nil {
		t.Fatal(err)
	}
	if got.Tracks[0].Title != "Track #4: Reprise #2" {
		t.Errorf("title = %q", got.Tracks[0].Title)
	}
}

// Deleting a block is how a file is left out of the album. The old editor told
// people a deleted line skipped the file and then filed it anyway, under a name
// it had already chosen for it.
func TestDeletingABlockDropsThatTrack(t *testing.T) {
	before := sample()
	text := Render(before)
	lines := strings.Split(text, "\n")
	var kept []string
	skip := false
	for _, l := range lines {
		if strings.HasPrefix(l, "## 2") {
			skip = true
			continue
		}
		if skip && strings.HasPrefix(l, "## 3") {
			skip = false
		}
		if !skip {
			kept = append(kept, l)
		}
	}

	got, err := Parse(strings.Join(kept, "\n"), before)
	if err != nil {
		t.Fatal(err)
	}
	if len(got.Tracks) != 2 {
		t.Fatalf("got %d tracks, want 2", len(got.Tracks))
	}
	if got.Tracks[0].ID != 1 || got.Tracks[1].ID != 3 {
		t.Errorf("the wrong tracks survived: %+v", got.Tracks)
	}
}

func TestParsingRefusesDamage(t *testing.T) {
	before := sample()
	cases := []struct {
		name string
		edit func(string) string
		want string
	}{
		{"empty", func(string) string { return "" }, "no tracks"},
		{"unknown id", func(s string) string { return strings.Replace(s, "## 3", "## 9", 1) }, "no track 9"},
		{"duplicate id", func(s string) string { return strings.Replace(s, "## 3", "## 1", 1) }, "appears twice"},
		{"unknown album field", func(s string) string { return "genre: metal\n" + s }, "unknown field"},
		{"unknown track field", func(s string) string { return strings.Replace(s, "## 1\n", "## 1\nlabel: x\n", 1) }, "unknown track field"},
		{"track field at the top", func(s string) string { return "title: x\n" + s }, "belongs inside a track block"},
		{"album field in a block", func(s string) string { return strings.Replace(s, "## 1\n", "## 1\nalbum: x\n", 1) }, "belongs at the top"},
		{"junk line", func(s string) string { return "nonsense\n" + s }, "not a 'field: value' line"},
		{"bad disc", func(s string) string { return strings.Replace(s, "disc:         1", "disc:         0", 1) }, "disc"},
		{"bad track", func(s string) string { return strings.Replace(s, "track:        1", "track:        x", 1) }, "track"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			_, err := Parse(tc.edit(Render(before)), before)
			if err == nil {
				t.Fatal("parsing should have failed")
			}
			if !strings.Contains(err.Error(), tc.want) {
				t.Errorf("error %q does not mention %q", err, tc.want)
			}
		})
	}
}

func TestChangesAreReported(t *testing.T) {
	before := sample()
	after := before
	after.Year = "2005"
	after.Cover = "none"

	got := after.ChangedFrom(before)
	if !got.Year || !got.Cover || got.Album || got.ArtistDir || got.AlbumArtist || got.AlbumDir {
		t.Errorf("changes = %+v", got)
	}
	if !got.Any() {
		t.Error("Any should be true")
	}
	same := before
	if same.ChangedFrom(before).Any() {
		t.Error("nothing changed, but Any is true")
	}
}

// The rendered file has to say what intake will name each file, because the name
// is not obvious from the title once normalisation has had it.
func TestRenderShowsTheDerivedName(t *testing.T) {
	text := Render(sample())
	if !strings.Contains(text, "1-01_the-perfect-crime-2.flac") {
		t.Errorf("the derived name is missing:\n%s", text)
	}
	if !strings.Contains(text, "# from:     01 one.flac") {
		t.Errorf("the source file is missing:\n%s", text)
	}
}