package meta import ( "strings" "testing" ) const docTemplate = "{albumartist}/{year} - {album}/{disc:1-}{track:02} - {title}.{ext}" func in(f Fields, discs int, ext string) TemplateInput { return TemplateInput{Fields: f, DiscCount: discs, Ext: ext} } func TestRenderTemplate(t *testing.T) { tpl, err := ParseTemplate(docTemplate) if err != nil { t.Fatal(err) } f := Fields{Artist: "Кино", Album: "Группа крови", Year: "1988-01-05", Track: "3/11", Disc: "1", Title: "Война"} cases := []struct { name string input TemplateInput want string }{ {"single disc", in(f, 1, "FLAC"), "Кино/1988 - Группа крови/03 - Война.flac"}, {"multi disc", in(f, 2, "mp3"), "Кино/1988 - Группа крови/1-03 - Война.mp3"}, {"forbidden chars and trailing dots", in(Fields{Artist: "AC/DC", Album: "What?...", Year: "1980", Track: "1", Title: ".hidden: x"}, 1, "mp3"), "AC_DC/1980 - What_/01 - .hidden_ x.mp3"}, } for _, c := range cases { got, err := tpl.Render(c.input) if err != nil || got != c.want { t.Errorf("%s: got %q, %v; want %q", c.name, got, err, c.want) } } if _, err := tpl.Render(in(Fields{Artist: "A", Album: "B", Track: "1", Title: "C"}, 1, "mp3")); err == nil { t.Error("missing year outside [ ] must fail") } opt, _ := ParseTemplate("{albumartist}/[{year} - ]{album}/{track:02} - {title}.{ext}") if got, _ := opt.Render(in(Fields{Artist: "A", Album: "B", Track: "1", Title: "C"}, 1, "mp3")); got != "A/B/01 - C.mp3" { t.Errorf("optional part: %q", got) } long, _ := opt.Render(in(Fields{Artist: "A", Album: "B", Track: "1", Title: strings.Repeat("я", 300)}, 1, "flac")) if seg := long[strings.LastIndex(long, "/")+1:]; len(seg) > MaxSegmentBytes || !strings.HasSuffix(seg, ".flac") { t.Errorf("long title segment: %d bytes %q", len(seg), seg[len(seg)-10:]) } } func TestParseTemplateErrors(t *testing.T) { for _, bad := range []string{"", "/{artist}/{title}.{ext}", "{artist}/{title}.mp3", "{artist}/{nope}/{title}.{ext}", "{artist}/../{title}.{ext}", "[{artist}/{title}.{ext}", "{artist}/{year:02}/{title}.{ext}", "{artist}//{title}.{ext}"} { if _, err := ParseTemplate(bad); err == nil { t.Errorf("%q must be rejected", bad) } } } // Шаблоны из docs/library-and-storage.md превращаются в шаблоны записи и проходят проверку обратимости. func TestSuggestAndCheck(t *testing.T) { exprs := []string{ `^(?P_incoming)/`, `^(?P[^/]+)/(?P\d{4}) - (?P[^/]+)/(?:(?P\d)-)?(?P\d{1,3}) - (?P[^/]+)\.\w+$`, `^(?P<genre>[^/]+)/(?P<artist>[^/]+)/(?P<album>.+?) \((?P<year>\d{4})\)/(?P<track>\d+)\. (?P<title>[^/]+)\.\w+$`, } paths := []string{"Кино/1988 - Группа крови/01 - Группа крови.flac", "Кино/1988 - Группа крови/02 - Закрой за мной дверь.flac", "Rock/Queen/Jazz (1978)/3. Jealousy.mp3"} got, err := SuggestTemplate(exprs, paths) if err != nil { t.Fatal(err) } if got != "{artist}/{year} - {album}/{disc:1-}{track:02} - {title}.{ext}" { t.Fatalf("suggested %q", got) } tpl, _ := ParseTemplate(got) patterns, _ := CompilePatterns(exprs) samples := []TemplateSample{ {RelPath: paths[0], Input: in(Fields{Artist: "Кино", Year: "1988", Album: "Группа крови", Track: "1", Title: "Группа крови"}, 1, "flac")}, {RelPath: paths[1], Input: in(Fields{Artist: "Кино", Year: "1988", Album: "Группа крови", Track: "2", Title: "Закрой за мной дверь"}, 1, "flac")}, {RelPath: "Pink Floyd/1979 - The Wall/2-01 - Hey You.flac", Input: in(Fields{Artist: "Pink Floyd", Year: "1979", Album: "The Wall", Disc: "2", Track: "1", Title: "Hey You"}, 2, "flac")}, } res := CheckTemplate(tpl, patterns, samples) if !res.OK || res.RoundTrip != 3 || res.SamePath != 3 { t.Fatalf("check: %+v", res) } // Шаблон, который регэкспы источника не разберут обратно. bad, _ := ParseTemplate("{artist} - {album}/{title}.{ext}") res = CheckTemplate(bad, patterns, samples) if res.OK || res.RoundTrip != 0 || len(res.Mismatches) != 3 || res.Mismatches[0].Fields[0] != "pattern" { t.Fatalf("bad template check: %+v", res) } }