// Copyright 2019 The Kubernetes Authors. // SPDX-License-Identifier: Apache-2.0 // +build !windows package filesys import ( "fmt" "io" "io/ioutil" "math/rand" "os" "path/filepath" "sort" "strings" "testing" ) const content = ` Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ` const shortContent = "hi" var topCases = []pathCase{ { what: "dotdot", arg: ParentDir, errStr: "illegal name '..' in file creation", }, { what: "empty", arg: "", name: "", errStr: "illegal name '.' in file creation", }, { what: "simple", arg: "bob", name: "bob", path: "bob", }, { what: "longer", arg: filepath.Join("longer", "bob"), name: "bob", path: filepath.Join("longer", "bob"), }, { what: "longer yet", arg: filepath.Join("longer", "foo", "bar", "beans", "bob"), name: "bob", path: filepath.Join("longer", "foo", "bar", "beans", "bob"), }, { what: "tricky", arg: filepath.Join("bob", ParentDir, "sally"), name: "sally", path: "sally", }, { what: "trickier", arg: filepath.Join("bob", "sally", ParentDir, ParentDir, "jean"), name: "jean", path: "jean", }, } func TestMakeEmptyDirInMemory(t *testing.T) { n := MakeEmptyDirInMemory() if !n.isNodeADir() { t.Fatalf("not a directory") } if n.Size() != 0 { t.Fatalf("unexpected size %d", n.Size()) } if n.Name() != "" { t.Fatalf("unexpected name '%s'", n.Name()) } if n.Path() != "" { t.Fatalf("unexpected path '%s'", n.Path()) } runBasicOperations( t, "MakeEmptyDirInMemory", false, topCases, n) } func TestMakeFsInMemory(t *testing.T) { runBasicOperations( t, "MakeFsInMemory", true, topCases, MakeFsInMemory()) } //nolint:gocyclo func runBasicOperations( t *testing.T, tName string, isFSysRooted bool, cases []pathCase, fSys FileSystem) { for _, c := range cases { err := fSys.WriteFile(c.arg, []byte(content)) if c.errStr != "" { if err == nil { t.Fatalf("%s; expected error writing to '%s'!", c.what, c.arg) } if !strings.Contains(err.Error(), c.errStr) { t.Fatalf("%s; expected err containing '%s', got '%v'", c.what, c.errStr, err) } continue } if err != nil { t.Fatalf("%s; unexpected error: %v", c.what, err) } if !fSys.Exists(c.path) { t.Fatalf("%s; expect existence of '%s'", c.what, c.path) } stuff, err := fSys.ReadFile(c.path) if err != nil { t.Fatalf("%s; unexpected error: %v", c.what, err) } if string(stuff) != content { t.Fatalf("%s; unexpected content '%s'", c.what, stuff) } f, err := fSys.Open(c.arg) if err != nil { t.Fatalf("%s; unexpected error: %v", c.what, err) } fi, err := f.Stat() if err != nil { t.Fatalf("%s; unexpected error: %v", c.what, err) } if fi.Name() != c.name { t.Fatalf("%s; expected name '%s', got '%s'", c.what, c.name, fi.Name()) } buff, err := ioutil.ReadAll(f) if err != nil { t.Fatalf("%s; unexpected error: %v", c.what, err) } if string(buff) != content { t.Fatalf("%s; unexpected buff '%s'", c.what, buff) } count, err := f.Write([]byte(shortContent)) if err != nil { t.Fatalf("%s; unexpected error: %v", c.what, err) } if count != len(shortContent) { t.Fatalf("%s; unexpected count: %d", c.what, len(shortContent)) } if err := f.Close(); err != nil { t.Fatalf("%s; unexpected error: %v", c.what, err) } stuff, err = fSys.ReadFile(c.path) if err != nil { t.Fatalf("%s; unexpected error: %v", c.what, err) } both := content + shortContent if string(stuff) != both { t.Fatalf("%s; unexpected content '%s', expected '%s'", c.what, stuff, both) } content := []byte(shortContent) if err := fSys.WriteFile(c.path, content); err != nil { t.Fatalf("%s; unexpected error: %v", c.what, err) } // This ensures that modifying the original slice does not change the contents of the file. content[0] = '@' stuff, err = fSys.ReadFile(c.path) if err != nil { t.Fatalf("%s; unexpected error: %v", c.what, err) } if string(stuff) != shortContent { t.Fatalf("%s; unexpected content '%s', expected '%s'", c.what, stuff, shortContent) } } var actualPaths []string var err error prefix := "" { root := SelfDir if isFSysRooted { root = Separator prefix = Separator } err = fSys.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil { fmt.Printf("err '%v' at path %q\n", err, path) return nil } if !info.IsDir() { actualPaths = append(actualPaths, path) } return nil }) } if err != nil { t.Fatalf("unexpected error %v", err) } var expectedPaths []string for _, c := range cases { if c.errStr == "" { expectedPaths = append(expectedPaths, prefix+c.path) } } sort.Strings(expectedPaths) assertEqualStringSlices(t, expectedPaths, actualPaths, tName) } type pathCase struct { what string arg string name string path string errStr string } func TestAddDir(t *testing.T) { cases := []pathCase{ { what: "dotdot", arg: ParentDir, errStr: "cannot add a directory above ''", }, { what: "empty", arg: "", name: "", path: "", }, { what: "simple", arg: "bob", name: "bob", path: "bob", }, { what: "longer", arg: filepath.Join("longer", "bob"), name: "bob", path: filepath.Join("longer", "bob"), }, { what: "longer yet", arg: filepath.Join("longer", "foo", "bar", "beans", "bob"), name: "bob", path: filepath.Join("longer", "foo", "bar", "beans", "bob"), }, { what: "tricky", arg: filepath.Join("bob", ParentDir, "sally"), name: "sally", path: "sally", }, { what: "trickier", arg: filepath.Join("bob", "sally", ParentDir, ParentDir, "jean"), name: "jean", path: "jean", }, } for _, c := range cases { n := MakeEmptyDirInMemory() f, err := n.AddDir(c.arg) if c.errStr != "" { if err == nil { t.Fatalf("%s; expected error!", c.what) } if !strings.Contains(err.Error(), c.errStr) { t.Fatalf( "%s; expected error with '%s', got '%v'", c.what, c.errStr, err) } continue } if err != nil { t.Fatalf("%s; unexpected error: %v", c.what, err) } checkNode(t, c.what, f, c.name, 0, true, c.path) checkOsStat(t, c.what, f, f.Name(), 0, true) } } var bagOfCases = []pathCase{ { what: "empty", arg: "", errStr: "illegal name '.' in file creation", }, { what: "simple", arg: "bob", name: "bob", path: "bob", }, { what: "longer", arg: filepath.Join("longer", "bob"), name: "bob", path: filepath.Join("longer", "bob"), }, { what: "longer", arg: filepath.Join("longer", "sally"), name: "sally", path: filepath.Join("longer", "sally"), }, { what: "even longer", arg: filepath.Join("longer", "than", "the", "other", "bob"), name: "bob", path: filepath.Join("longer", "than", "the", "other", "bob"), }, { what: "even longer", arg: filepath.Join("even", "much", "longer", "than", "the", "other", "bob"), name: "bob", path: filepath.Join("even", "much", "longer", "than", "the", "other", "bob"), }, } func TestAddFile(t *testing.T) { n := MakeEmptyDirInMemory() if n.FileCount() != 0 { t.Fatalf("expected no files, got %d", n.FileCount()) } expectedFileCount := 0 for _, c := range bagOfCases { f, err := n.AddFile(c.arg, []byte(content)) if c.errStr != "" { if err == nil { t.Fatalf("%s; expected error!", c.what) } if !strings.Contains(err.Error(), c.errStr) { t.Fatalf("%s; expected err containing '%s', got '%v'", c.what, c.errStr, err) } continue } if err != nil { t.Fatalf("%s; unexpected error %v", c.what, err) } checkNode(t, c.what, f, c.name, len(content), false, c.path) checkOsStat(t, c.what, f, f.Name(), len(content), false) result, err := n.Find(c.arg) if err != nil { t.Fatalf("%s; unexpected find error %v", c.what, err) } if result != f { t.Fatalf("%s; unexpected find result %v", c.what, result) } result, err = n.Find(filepath.Join("longer", "bogus")) if err != nil { t.Fatalf("%s; unexpected find error %v", c.what, err) } if result != nil { t.Fatalf("%s; unexpected find result %v", c.what, result) } expectedFileCount++ fc := n.FileCount() if fc != expectedFileCount { t.Fatalf("expected file count %d, got %d", expectedFileCount, fc) } } } func checkNode( t *testing.T, what string, f *fsNode, name string, size int, isDir bool, path string) { if f.isNodeADir() != isDir { t.Fatalf("%s; unexpected isNodeADir = %v", what, f.isNodeADir()) } if f.Size() != int64(size) { t.Fatalf("%s; unexpected size %d", what, f.Size()) } if name != f.Name() { t.Fatalf("%s; expected name '%s', got '%s'", what, name, f.Name()) } if path != f.Path() { t.Fatalf("%s; expected path '%s', got '%s'", what, path, f.Path()) } } func checkOsStat( t *testing.T, what string, f File, name string, size int, isDir bool) { info, err := f.Stat() if err != nil { t.Fatalf("%s; unexpected stat error %v", what, err) } if info.IsDir() != isDir { t.Fatalf("%s; unexpected info.isNodeADir = %v", what, info.IsDir()) } if info.Size() != int64(size) { t.Fatalf("%s; unexpected info.size %d", what, info.Size()) } if info.Name() != name { t.Fatalf("%s; expected name '%s', got info.Name '%s'", what, name, info.Name()) } } var bunchOfFiles = []struct { path string addAsDir bool }{ { path: filepath.Join("b", "e", "a", "c", "g"), }, { path: filepath.Join("z", "r", "a", "b", "g"), }, { path: filepath.Join("b", "q", "a", "c", "g"), }, { path: filepath.Join("b", "a", "a", "m", "g"), addAsDir: true, }, { path: filepath.Join("b", "w"), }, { path: filepath.Join("b", "d", "a", "c", "m"), }, { path: filepath.Join("b", "d", "z"), }, { path: filepath.Join("b", "d", "y"), }, { path: filepath.Join("b", "d", "ignore", "c", "n"), }, { path: filepath.Join("b", "d", "x"), }, { path: filepath.Join("b", "d", "ignore", "c", "o"), }, { path: filepath.Join("b", "d", "ignore", "c", "m"), }, { path: filepath.Join("b", "d", "a", "c", "i"), addAsDir: true, }, { path: filepath.Join("x"), }, { path: filepath.Join("y"), }, { path: filepath.Join("b", "d", "a", "c", "i", "beans"), }, { path: filepath.Join("b", "d", "a", "c", "r", "w"), addAsDir: true, }, { path: filepath.Join("b", "d", "a", "c", "u"), }, } func makeLoadedFileTree(t *testing.T) *fsNode { n := MakeEmptyDirInMemory() var err error expectedFileCount := 0 for _, item := range bunchOfFiles { if item.addAsDir { _, err = n.AddDir(item.path) } else { _, err = n.AddFile(item.path, []byte(content)) expectedFileCount++ } if err != nil { t.Fatalf("unexpected error %v", err) } } fc := n.FileCount() if fc != expectedFileCount { t.Fatalf("expected file count %d, got %d", expectedFileCount, fc) } return n } func TestWalkMe(t *testing.T) { n := makeLoadedFileTree(t) var actualPaths []string err := n.WalkMe(func(path string, info os.FileInfo, err error) error { if err != nil { fmt.Printf("err '%v' at path %q\n", err, path) return nil } if info.IsDir() { if info.Name() == "ignore" { return filepath.SkipDir } } else { actualPaths = append(actualPaths, path) } return nil }) if err != nil { t.Fatalf("unexpected error %v", err) } var expectedPaths []string for _, c := range bunchOfFiles { if !c.addAsDir && !strings.Contains(c.path, "ignore") { expectedPaths = append(expectedPaths, c.path) } } sort.Strings(expectedPaths) assertEqualStringSlices(t, expectedPaths, actualPaths, "testWalkMe") } func TestRemove(t *testing.T) { n := makeLoadedFileTree(t) orgCount := n.FileCount() // Remove the "ignore" directory and everything below it. path := filepath.Join("b", "d", "ignore") result, err := n.Find(path) if err != nil { t.Fatalf("%s; unexpected error %v", path, err) } if result == nil { t.Fatalf("%s; expected to find '%s'", path, path) } if !result.isNodeADir() { t.Fatalf("%s; expected to find a directory", path) } err = result.Remove() if err != nil { t.Fatalf("%s; unable to remove: %v", path, err) } result, err = n.Find(path) if err != nil { // Just because it's gone doesn't mean error. t.Fatalf("%s; unexpected error %v", path, err) } if result != nil { t.Fatalf("%s; should not have been able to find '%s'", path, path) } // There were three files below "ignore". orgCount -= 3 // Now drop one more for a total of four dropped. result, _ = n.Find(filepath.Join("y")) err = result.Remove() if err != nil { t.Fatalf("%s; unable to remove: %v", path, err) } orgCount -= 1 fc := n.FileCount() if fc != orgCount { t.Fatalf("expected file count %d, got %d", orgCount, fc) } } func TestExists(t *testing.T) { n := makeLoadedFileTree(t) path := filepath.Join("b", "d", "a") if !n.Exists(path) { t.Fatalf("expected existence at %s", path) } if !n.IsDir(path) { t.Fatalf("expected directory at %s", path) } } func TestRegExpGlob(t *testing.T) { n := makeLoadedFileTree(t) expected := []string{ filepath.Join("b", "d", "a", "c", "i", "beans"), filepath.Join("b", "d", "a", "c", "m"), filepath.Join("b", "d", "a", "c", "u"), filepath.Join("b", "d", "ignore", "c", "m"), filepath.Join("b", "d", "ignore", "c", "n"), filepath.Join("b", "d", "ignore", "c", "o"), filepath.Join("b", "d", "x"), filepath.Join("b", "d", "y"), filepath.Join("b", "d", "z"), } paths, err := n.RegExpGlob("b/d/*") if err != nil { t.Fatalf("glob error: %v", err) } assertEqualStringSlices(t, expected, paths, "glob test") } func TestGlob(t *testing.T) { n := makeLoadedFileTree(t) expected := []string{ filepath.Join("b", "d", "x"), filepath.Join("b", "d", "y"), filepath.Join("b", "d", "z"), } paths, err := n.Glob("b/d/*") if err != nil { t.Fatalf("glob error: %v", err) } assertEqualStringSlices(t, expected, paths, "glob test") } func assertEqualStringSlices(t *testing.T, expected, actual []string, message string) { t.Helper() if len(expected) != len(actual) { t.Fatalf( "%s; unequal sizes; len(expected)=%d, len(actual)=%d\n%+v\n%+v\n", message, len(expected), len(actual), expected, actual) } for i := range expected { if expected[i] != actual[i] { t.Fatalf( "%s; unequal entries; expected=%s, actual=%s", message, expected[i], actual[i]) } } } func TestFind(t *testing.T) { cases := []struct { what string arg string expectDir bool expectFile bool errStr string }{ { what: "garbage", arg: "///1(*&SA", }, { what: "simple", arg: "bob", }, { what: "no directory", arg: filepath.Join("b", "rrrrrr"), }, { what: "is a directory", arg: filepath.Join("b", "d", "ignore"), expectDir: true, }, { what: "longer, ending in file", arg: filepath.Join("b", "d", "x"), expectFile: true, }, { what: "moar longer, ending in file", arg: filepath.Join("b", "d", "a", "c", "u"), expectFile: true, }, { what: "directory", arg: filepath.Join("b"), expectDir: true, }, { // Querying for the empty string could // 1) be an error, // 2) return no result (and no error) as with // any illegal and therefore non-existent // file name, // 3) return the node itself, like running // 'ls' with no argument. // Going with option 2 (no result, no error), // since at this low level it makes more sense // if the results for the empty string query // differ from the results for the "." query. what: "empty name", arg: "", }, { what: "self dir", arg: SelfDir, expectDir: true, }, { what: "parent dir - doesn't exist", arg: ParentDir, }, { what: "many parents - doesn't exist", arg: filepath.Join(ParentDir, ParentDir, ParentDir), }, } n := makeLoadedFileTree(t) for _, item := range cases { result, err := n.Find(item.arg) if item.errStr != "" { if err == nil { t.Fatalf("%s; expected error", item.what) } if !strings.Contains(err.Error(), item.errStr) { t.Fatalf("%s; expected err containing '%s', got '%v'", item.what, item.errStr, err) } continue } if err != nil { t.Fatalf("%s; unexpected error: %v", item.what, err) } if result == nil { if item.expectDir { t.Fatalf( "%s; expected to find directory '%s'", item.what, item.arg) } if item.expectFile { t.Fatalf( "%s; expected to find file '%s'", item.what, item.arg) } continue } if item.expectDir { if !result.isNodeADir() { t.Fatalf( "%s; expected '%s' to be a directory", item.what, item.arg) } continue } if item.expectFile { if result.isNodeADir() { t.Fatalf("%s; expected '%s' to be a file", item.what, item.arg) } continue } t.Fatalf( "%s; expected nothing for '%s', but got '%s'", item.what, item.arg, result.Path()) } } func TestCleanedAbs(t *testing.T) { cases := []struct { what string full string cDir string name string errStr string }{ { what: "empty", full: "", errStr: "doesn't exist", }, { what: "simple", full: "bob", errStr: "'bob' doesn't exist", }, { what: "no directory", full: filepath.Join("b", "rrrrrr"), errStr: "'b/rrrrrr' doesn't exist", }, { what: "longer, ending in file", full: filepath.Join("b", "d", "x"), cDir: filepath.Join("b", "d"), name: "x", }, { what: "moar longer, ending in file", full: filepath.Join("b", "d", "a", "c", "u"), cDir: filepath.Join("b", "d", "a", "c"), name: "u", }, { what: "directory", full: filepath.Join("b", "d"), cDir: filepath.Join("b", "d"), name: "", }, } n := makeLoadedFileTree(t) for _, item := range cases { cDir, name, err := n.CleanedAbs(item.full) if item.errStr != "" { if err == nil { t.Fatalf("%s; expected error", item.what) } if !strings.Contains(err.Error(), item.errStr) { t.Fatalf("%s; expected err containing '%s', got '%v'", item.what, item.errStr, err) } continue } if err != nil { t.Fatalf("%s; unexpected error: %v", item.what, err) } if cDir != ConfirmedDir(item.cDir) { t.Fatalf("%s; expected cDir=%s, got '%s'", item.what, item.cDir, cDir) } if name != item.name { t.Fatalf("%s; expected name=%s, got '%s'", item.what, item.name, name) } } } func TestFileOps(t *testing.T) { const path = "foo.txt" content := strings.Repeat("longest content", 100) fs := MakeFsInMemory() f, err := fs.Create(path) if err != nil { t.Fatalf("unexpected error: %v", err) } if _, err := fs.Open(path); err == nil { t.Fatalf("expected already opened error, got nil") } if _, err := fmt.Fprint(f, content); err != nil { t.Fatalf("unexpected error: %v", err) } if err := f.Close(); err != nil { t.Fatalf("unexpected error: %v", err) } if err := f.Close(); err == nil { t.Fatalf("expected already closed error, got nil") } f, err = fs.Open(path) if err != nil { t.Fatalf("unexpected error: %v", err) } defer f.Close() for { buf := make([]byte, rand.Intn(10)) n, err := f.Read(buf) if err != nil && err != io.EOF { t.Fatalf("unexpected error: %v", err) } if content[:n] != string(buf[:n]) { t.Fatalf("unexpected read: expected %q got %q", content[:n], buf[:n]) } content = content[n:] if err != io.EOF { continue } if len(content) == 0 { break } t.Fatalf("unexpected EOF: remaining %d bytes", len(content)) } }