// SPDX-License-Identifier: GPL-3.0-or-later package plan import ( "strings" "testing" "time" ) func facts() Facts { return Facts{ Name: "Invoice_2026-08.pdf", Captures: []string{"2026-08", "2026", "a}}b"}, ModTime: time.Date(2026, 8, 15, 14, 30, 45, 0, time.UTC), Now: time.Date(2026, 9, 12, 9, 0, 0, 0, time.UTC), } } func TestExpand(t *testing.T) { tests := []struct{ in, want string }{ {"Work/Acme", "Work/Acme"}, {"{name}", "Invoice_2026-08.pdf"}, {"{stem}", "Invoice_2026-08"}, {"{ext}", ".pdf"}, {"{stem}{ext}", "Invoice_2026-08.pdf"}, {"Work/{mtime:%Y}", "Work/2026"}, {"{mtime:%Y-%m-%d}", "2026-08-15"}, {"{mtime:%H%M%S}", "143045"}, {"{mtime:%j}", "227"}, {"{now:%Y-%m-%d}", "2026-09-12"}, {"{1}", "2026"}, // An expanded value that itself contains "}}" must reach the // output unchanged. Expand's single-pass scanner jumps past a // placeholder's closing brace, so written bytes are never re-scanned; // a refactor to scan-then-replace would silently re-collapse them. {"{2}", "a}}b"}, {"{{literal}}", "{literal}"}, {"100{{%}}", "100{%}"}, {"{mtime:%Y%%}", "2026%"}, {"Photos/{mtime:%Y}/{stem}-{1}{ext}", "Photos/2026/Invoice_2026-08-2026.pdf"}, } for _, tt := range tests { got, err := Expand(tt.in, facts()) if err != nil || got != tt.want { t.Errorf("Expand(%q) = %q, %v; want %q", tt.in, got, err, tt.want) } } } func TestExpandNoExtension(t *testing.T) { f := facts() f.Name = "README" for in, want := range map[string]string{"{stem}": "README", "{ext}": ""} { if got, err := Expand(in, f); err != nil || got != want { t.Errorf("Expand(%q) on README = %q, %v; want %q", in, got, err, want) } } f.Name = ".bashrc" if got, _ := Expand("{stem}", f); got != ".bashrc" { t.Errorf("dotfile stem = %q, want .bashrc", got) } if got, _ := Expand("{ext}", f); got != "" { t.Errorf("dotfile ext = %q, want empty", got) } f.Name = "archive.tar.gz" if got, _ := Expand("{stem}|{ext}", f); got != "archive.tar|.gz" { t.Errorf("double extension = %q, want archive.tar|.gz", got) } } func TestExpandErrors(t *testing.T) { tests := []struct{ in, want string }{ {"{7}", "no capture group 7"}, {"{0}", "capture groups are numbered from 1"}, {"{whatever}", "unknown placeholder {whatever}"}, {"{name", "unclosed placeholder"}, {"{mtime:%Q}", "unknown time format %Q in {mtime:...}"}, {"{mtime}", "unknown placeholder {mtime}"}, // The error must name the placeholder actually written ("now"), // not hardcode "mtime" - the {mtime:%Q} case above passes either // way, which is why that defect survived. {"{now:%Q}", "unknown time format %Q in {now:...}"}, // {1}...{9} is the syntax (spec ยง7.3); {10} and up must be // rejected the same way an unknown placeholder is. {"{10}", "unknown placeholder {10}"}, } for _, tt := range tests { _, err := Expand(tt.in, facts()) if err == nil || !strings.Contains(err.Error(), tt.want) { t.Errorf("Expand(%q) error = %v; want %q", tt.in, err, tt.want) } } }