aboutsummaryrefslogtreecommitdiff
path: root/internal/plan/placeholder_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/plan/placeholder_test.go')
-rw-r--r--internal/plan/placeholder_test.go94
1 files changed, 94 insertions, 0 deletions
diff --git a/internal/plan/placeholder_test.go b/internal/plan/placeholder_test.go
new file mode 100644
index 0000000..e155430
--- /dev/null
+++ b/internal/plan/placeholder_test.go
@@ -0,0 +1,94 @@
+// 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"},
+ // D2: 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}"},
+ // B3: 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:...}"},
+ // D3: {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)
+ }
+ }
+}