aboutsummaryrefslogtreecommitdiff
path: root/internal/cli/remote_test.go
blob: b4e20d9d68859490fc0da8b3fc4753766c4de4a1 (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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
package cli

import (
	"sort"
	"strings"
	"testing"
)

func dir(path string) remoteEntry {
	return remoteEntry{path: path, isDir: true, orderKey: pathOrderKey(path)}
}
func file(path string) remoteEntry {
	return remoteEntry{path: path, orderKey: pathOrderKey(path)}
}

func deleted(e remoteEntry) remoteEntry {
	e.deleted = true
	return e
}

func emptyDirPaths(entries []remoteEntry) []string {
	// Reproduce the production ordering (see listRemoteEntries) so the sort rule
	// and the emptyDirs scan rule are always exercised together, rather than
	// trusting hand-ordered literals.
	sort.Slice(entries, func(i, j int) bool {
		return entries[i].orderKey < entries[j].orderKey
	})
	got := emptyDirs(entries)
	paths := make([]string, len(got))
	for i, e := range got {
		paths[i] = e.path
	}
	sort.Strings(paths)
	return paths
}

func TestEmptyDirs(t *testing.T) {
	tests := []struct {
		name    string
		entries []remoteEntry
		want    []string
	}{
		{
			name: "dir with only a deleted file is empty",
			entries: []remoteEntry{
				dir("a"),
				deleted(file("a/gone.js")),
			},
			want: []string{"a"},
		},
		{
			name: "dir with a surviving file is kept",
			entries: []remoteEntry{
				dir("a"),
				file("a/live.js"),
			},
			want: []string{},
		},
		{
			name: "bottom-up cascade removes parent and child",
			entries: []remoteEntry{
				dir("a"),
				dir("a/b"),
				deleted(file("a/b/gone.js")),
			},
			want: []string{"a"},
		},
		{
			name: "deep file deleted but sibling file alive keeps parent",
			entries: []remoteEntry{
				dir("a"),
				dir("a/b"),
				deleted(file("a/b/gone.js")),
				file("a/c.js"),
			},
			want: []string{"a/b"},
		},
		{
			name: "sibling with shared name prefix is not swallowed",
			entries: []remoteEntry{
				dir("a"),
				deleted(file("a/gone.js")),
				file("ab/live.js"),
			},
			want: []string{"a"},
		},
		{
			name: "sibling colliding on base name does not hide live children",
			entries: []remoteEntry{
				dir("a"),
				file("a.js"),
				file("a/live.js"),
			},
			want: []string{},
		},
		{
			name: "empty dir with no children is removed",
			entries: []remoteEntry{
				dir("a"),
			},
			want: []string{"a"},
		},
		{
			name: "nested dirs with no files at all are all removed",
			entries: []remoteEntry{
				dir("a"),
				dir("a/b"),
				dir("a/c"),
			},
			want: []string{"a"},
		},
		{
			name: "empty dir nested under a dir holding a live file keeps outer, removes inner",
			entries: []remoteEntry{
				dir("a"),
				dir("a/b"),
				dir("a/b/c"),
				file("a/live.js"),
			},
			want: []string{"a/b"},
		},
		{
			name: "live file under a nested dir keeps the whole chain",
			entries: []remoteEntry{
				dir("a"),
				dir("a/b"),
				file("a/b/live.js"),
			},
			want: []string{},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := emptyDirPaths(tt.entries)
			if strings.Join(got, ",") != strings.Join(tt.want, ",") {
				t.Fatalf("emptyDirs = %v, want %v", got, tt.want)
			}
		})
	}
}