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) } }) } }