From 4bf9b4d41b24e22a03bc77fcc042d3ffbb1bff99 Mon Sep 17 00:00:00 2001 From: James Shubin Date: Fri, 26 Jul 2019 03:28:29 -0400 Subject: [PATCH] util: Add some path helper functions In the end, I'm not sure how useful these will be, but we'll keep them in for now. --- util/util.go | 54 +++++++++++ util/util_test.go | 227 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 281 insertions(+) diff --git a/util/util.go b/util/util.go index 9bbc04cf..1ccb54cc 100644 --- a/util/util.go +++ b/util/util.go @@ -421,6 +421,60 @@ func RemovePathSuffix(s string) (string, error) { return strings.Join(x, "/") + "/", nil } +// DirParents returns a list of the parent directories in a given path. If you +// pass it an empty string, or a single slash, then you will get an empty list. +// If you pass it a malformed path, then you might get unexpected results. +func DirParents(p string) []string { + if p == "" { + return nil // TODO: should we error? + } + if p == "/" { + return []string{} + } + d := Dirname(p) + x := DirParents(d) + x = append(x, d) + return x +} + +// MissingMkdirs takes a list of paths, and returns a list of any missing paths +// that would be needed to avoid having to `mkdir -p` to prevent missing parent +// directory errors from happening. This adds paths all the way up to the root, +// but without including it, because it's implied. +// TODO: do we want to include the root? +// TODO: this could probably be implemented in a more efficient way... +func MissingMkdirs(input []string) ([]string, error) { + dirs := []string{} + for _, p := range input { + if p == "/" { + continue + } + d := Dirname(p) + dirs = append(dirs, d) + if strings.HasSuffix(p, "/") { // it's a dir + dirs = append(dirs, p) + } + } + // TODO: remove duplicates for efficiency? + + result := []string{} + for _, d := range dirs { + p := DirParents(d) // TODO: memoize + p = append(p, d) // include self + for _, x := range p { + if StrInList(x, input) { + continue + } + result = append(result, x) + } + } + + out := StrRemoveDuplicatesInList(result) // avoid duplicates + sort.Sort(PathSlice(out)) + + return out, nil +} + // TimeAfterOrBlock is aspecial version of time.After that blocks when given a // negative integer. When used in a case statement, the timer restarts on each // select call to it. diff --git a/util/util_test.go b/util/util_test.go index 0eddb9a1..b5f3c1a2 100644 --- a/util/util_test.go +++ b/util/util_test.go @@ -1055,6 +1055,233 @@ func TestRemovePathSuffix0(t *testing.T) { } } +func TestDirParents0(t *testing.T) { + tests := []struct { + in string + out []string + }{ + { + in: "", + out: nil, + }, + { + in: "/", + out: []string{}, + }, + { + in: "/tmp/x1/mod1/files/", + out: []string{ + "/", + "/tmp/", + "/tmp/x1/", + "/tmp/x1/mod1/", + }, + }, + { + in: "/tmp/x1/mod1/files/foo", + out: []string{ + "/", + "/tmp/", + "/tmp/x1/", + "/tmp/x1/mod1/", + "/tmp/x1/mod1/files/", + }, + }, + } + for index, tt := range tests { + result := DirParents(tt.in) + if a, b := len(result), len(tt.out); a != b { + t.Errorf("test #%d: expected length differs (%d != %d)", index, a, b) + t.Errorf("test #%d: actual: %+v", index, result) + t.Errorf("test #%d: expected: %+v", index, tt.out) + break + } + for i := range result { + if result[i] != tt.out[i] { + t.Errorf("test #%d: parents diff: wanted: %s got: %s", index, tt.out[i], result[i]) + } + } + } +} + +func TestMissingMkdirs0(t *testing.T) { + tests := []struct { + in []string + out []string + fail bool + }{ + { + in: []string{}, + out: []string{}, + }, + { + in: []string{ + "/", + }, + out: []string{}, + }, + { + in: []string{ + "/tmp/x1/metadata.yaml", + "/tmp/x1/main.mcl", + "/tmp/x1/files/", + "/tmp/x1/second.mcl", + "/tmp/x1/mod1/metadata.yaml", + "/tmp/x1/mod1/main.mcl", + "/tmp/x1/mod1/files/", + }, + out: []string{ + "/", + "/tmp/", + "/tmp/x1/", + "/tmp/x1/mod1/", + }, + }, + { + in: []string{ + "/tmp/x1/files/", + "/tmp/x1/main.mcl", + "/tmp/x1/metadata.yaml", + "/tmp/x1/mod1/files/", + "/tmp/x1/mod1/main.mcl", + "/tmp/x1/mod1/metadata.yaml", + "/tmp/x1/second.mcl", + }, + out: []string{ + "/", + "/tmp/", + "/tmp/x1/", + "/tmp/x1/mod1/", + }, + }, + { + in: []string{ + "/tmp/x1/files/", + "/tmp/x1/files/a/b/c/", + }, + out: []string{ + "/", + "/tmp/", + "/tmp/x1/", + //"/tmp/x1/files/", // already exists! + "/tmp/x1/files/a/", + "/tmp/x1/files/a/b/", + }, + }, + { + in: []string{ + "/tmp/x1/files/", + "/tmp/x1/files/a/b/c/", + "/tmp/x1/files/a/b/c/", + "/tmp/x1/files/a/b/c/", + "/tmp/x1/files/a/b/c/", // duplicates + }, + out: []string{ + "/", + "/tmp/", + "/tmp/x1/", + //"/tmp/x1/files/", // already exists! + "/tmp/x1/files/a/", + "/tmp/x1/files/a/b/", + }, + }, + { + in: []string{ + "/tmp/x1/files/", + "/tmp/x1/files/a/b/c/d1", + "/tmp/x1/files/a/b/c/d2", + }, + out: []string{ + "/", + "/tmp/", + "/tmp/x1/", + //"/tmp/x1/files/", // already exists! + "/tmp/x1/files/a/", + "/tmp/x1/files/a/b/", + "/tmp/x1/files/a/b/c/", + }, + }, + { + in: []string{ + "/tmp/x1/files/", + "/tmp/x1/files/a/b/c/d1", + "/tmp/x1/files/a/b/c/d1", + "/tmp/x1/files/a/b/c/d1", + "/tmp/x1/files/a/b/c/d1", // duplicates! + "/tmp/x1/files/a/b/c/d2", + }, + out: []string{ + "/", + "/tmp/", + "/tmp/x1/", + //"/tmp/x1/files/", // already exists! + "/tmp/x1/files/a/", + "/tmp/x1/files/a/b/", + "/tmp/x1/files/a/b/c/", + }, + }, + { + in: []string{ + "/tmp/x1/files/", + "/tmp/x1/files/a/b/c/d1", + "/tmp/x1/files/a/b/c/d1", + "/tmp/x1/files/a/b/c/d1", + "/tmp/x1/files/a/b/c/d1", // duplicates! + "/tmp/x1/files/a/b/", + "/tmp/x1/files/a/b/c/d2", + }, + out: []string{ + "/", + "/tmp/", + "/tmp/x1/", + //"/tmp/x1/files/", // already exists! + "/tmp/x1/files/a/", + "/tmp/x1/files/a/b/c/", + }, + }, + // invalid path list, so undefined results + //{ + // in: []string{ + // "/tmp/x1/files/", + // "/tmp/x1/files/a/b/c/d", + // "/tmp/x1/files/a/b/c/d/", // error: same name as file + // "/tmp/x1/files/a/b/c/d1", + // }, + // out: []string{}, + // fail: true, // TODO: put a specific error? + //}, + // TODO: add more tests + } + for index, tt := range tests { + result, err := MissingMkdirs(tt.in) + + if !tt.fail && err != nil { + t.Errorf("test #%d: failed with: %+v", index, err) + break + } + if tt.fail && err == nil { + t.Errorf("test #%d: passed, expected fail", index) + break + } + if !tt.fail && result == nil { + t.Errorf("test #%d: output was nil", index) + break + } + + if a, b := len(result), len(tt.out); a != b { + t.Errorf("test #%d: expected length differs (%d != %d)", index, a, b) + t.Errorf("test #%d: actual: %+v", index, result) + t.Errorf("test #%d: expected: %+v", index, tt.out) + break + } + for i := range result { + if result[i] != tt.out[i] { + t.Errorf("test #%d: missing mkdirs diff: wanted: %s got: %s", index, tt.out[i], result[i]) + } + } + } +} + func TestPriorityStrSliceSort0(t *testing.T) { in := []string{"foo", "bar", "baz"} ex := []string{"bar", "baz", "foo"}