aboutsummaryrefslogtreecommitdiff
path: root/internal/plan/placeholder_test.go
blob: e155430eccea384de45f9c561b687df3a951361d (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
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)
		}
	}
}