diff options
| author | tsne <tsne.dev@outlook.com> | 2026-07-06 16:52:20 +0200 |
|---|---|---|
| committer | tsne <tsne.dev@outlook.com> | 2026-07-06 19:21:15 +0200 |
| commit | 2c41acab4c97f584e8e199b31dc456194b8d7e6c (patch) | |
| tree | 42381deb6c4f6da0f32945432207a31b4d86664e /internal/cli/remote_test.go | |
| download | hopper-2c41acab4c97f584e8e199b31dc456194b8d7e6c.tar.gz | |
initial
Diffstat (limited to 'internal/cli/remote_test.go')
| -rw-r--r-- | internal/cli/remote_test.go | 141 |
1 files changed, 141 insertions, 0 deletions
diff --git a/internal/cli/remote_test.go b/internal/cli/remote_test.go new file mode 100644 index 0000000..b4e20d9 --- /dev/null +++ b/internal/cli/remote_test.go @@ -0,0 +1,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) + } + }) + } +} |