// Copyright 2020 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package fsys import ( "errors" "internal/testenv" "internal/txtar" "io" "io/fs" "os" "path/filepath" "reflect" "runtime" "slices" "strings" "sync" "testing" ) func resetForTesting() { cwd = sync.OnceValue(cwdOnce) overlay = nil binds = nil } // initOverlay resets the overlay state to reflect the config. // config should be a text archive string. The comment is the overlay config // json, and the files, in the archive are laid out in a temp directory // that cwd is set to. func initOverlay(t *testing.T, config string) { t.Helper() t.Chdir(t.TempDir()) resetForTesting() t.Cleanup(resetForTesting) cwd := cwd() a := txtar.Parse([]byte(config)) for _, f := range a.Files { name := filepath.Join(cwd, f.Name) if err := os.MkdirAll(filepath.Dir(name), 0777); err != nil { t.Fatal(err) } if err := os.WriteFile(name, f.Data, 0666); err != nil { t.Fatal(err) } } if err := initFromJSON(a.Comment); err != nil { t.Fatal(err) } } var statInfoOverlay = `{"Replace": { "x": "replace/x", "a/b/c": "replace/c", "d/e": "" }}` var statInfoTests = []struct { path string info info }{ {"foo", info{abs: "/tmp/foo", actual: "foo"}}, {"foo/bar/baz/quux", info{abs: "/tmp/foo/bar/baz/quux", actual: "foo/bar/baz/quux"}}, {"x", info{abs: "/tmp/x", replaced: true, file: true, actual: "/tmp/replace/x"}}, {"/tmp/x", info{abs: "/tmp/x", replaced: true, file: true, actual: "/tmp/replace/x"}}, {"x/y", info{abs: "/tmp/x/y", deleted: true}}, {"a", info{abs: "/tmp/a", replaced: true, dir: true, actual: "a"}}, {"a/b", info{abs: "/tmp/a/b", replaced: true, dir: true, actual: "a/b"}}, {"a/b/c", info{abs: "/tmp/a/b/c", replaced: true, file: true, actual: "/tmp/replace/c"}}, {"d/e", info{abs: "/tmp/d/e", deleted: true}}, {"d", info{abs: "/tmp/d", replaced: true, dir: true, actual: "d"}}, } var statInfoChildrenTests = []struct { path string children []info }{ {"foo", nil}, {"foo/bar", nil}, {"foo/bar/baz", nil}, {"x", nil}, {"x/y", nil}, {"a", []info{{abs: "/tmp/a/b", replaced: true, dir: true, actual: ""}}}, {"a/b", []info{{abs: "/tmp/a/b/c", replaced: true, actual: "/tmp/replace/c"}}}, {"d", []info{{abs: "/tmp/d/e", deleted: true}}}, {"d/e", nil}, {".", []info{ {abs: "/tmp/a", replaced: true, dir: true, actual: ""}, // {abs: "/tmp/d", replaced: true, dir: true, actual: ""}, {abs: "/tmp/x", replaced: true, actual: "/tmp/replace/x"}, }}, } func TestStatInfo(t *testing.T) { tmp := "/tmp" if runtime.GOOS == "windows" { tmp = `C:\tmp` } cwd = sync.OnceValue(func() string { return tmp }) winFix := func(s string) string { if runtime.GOOS == "windows" { s = strings.ReplaceAll(s, `/tmp`, tmp) // fix tmp s = strings.ReplaceAll(s, `/`, `\`) // use backslashes } return s } overlay := statInfoOverlay overlay = winFix(overlay) overlay = strings.ReplaceAll(overlay, `\`, `\\`) // JSON escaping if err := initFromJSON([]byte(overlay)); err != nil { t.Fatal(err) } for _, tt := range statInfoTests { tt.path = winFix(tt.path) tt.info.abs = winFix(tt.info.abs) tt.info.actual = winFix(tt.info.actual) info := stat(tt.path) if info != tt.info { t.Errorf("stat(%#q):\nhave %+v\nwant %+v", tt.path, info, tt.info) } } for _, tt := range statInfoChildrenTests { tt.path = winFix(tt.path) for i, info := range tt.children { info.abs = winFix(info.abs) info.actual = winFix(info.actual) tt.children[i] = info } parent := stat(winFix(tt.path)) var children []info for name, child := range parent.children() { if name != filepath.Base(child.abs) { t.Errorf("stat(%#q): child %#q has inconsistent abs %#q", tt.path, name, child.abs) } children = append(children, child) } slices.SortFunc(children, func(x, y info) int { return cmp(x.abs, y.abs) }) if !slices.Equal(children, tt.children) { t.Errorf("stat(%#q) children:\nhave %+v\nwant %+v", tt.path, children, tt.children) } } } func TestIsDir(t *testing.T) { initOverlay(t, ` { "Replace": { "subdir2/file2.txt": "overlayfiles/subdir2_file2.txt", "subdir4": "overlayfiles/subdir4", "subdir3/file3b.txt": "overlayfiles/subdir3_file3b.txt", "subdir5": "", "subdir6": "" } } -- subdir1/file1.txt -- -- subdir3/file3a.txt -- 33 -- subdir4/file4.txt -- 444 -- overlayfiles/subdir2_file2.txt -- 2 -- overlayfiles/subdir3_file3b.txt -- 66666 -- overlayfiles/subdir4 -- x -- subdir6/file6.txt -- six `) cwd := cwd() testCases := []struct { path string want, wantErr bool }{ {"", true, true}, {".", true, false}, {cwd, true, false}, {cwd + string(filepath.Separator), true, false}, // subdir1 is only on disk {filepath.Join(cwd, "subdir1"), true, false}, {"subdir1", true, false}, {"subdir1" + string(filepath.Separator), true, false}, {"subdir1/file1.txt", false, false}, {"subdir1/doesntexist.txt", false, true}, {"doesntexist", false, true}, // subdir2 is only in overlay {filepath.Join(cwd, "subdir2"), true, false}, {"subdir2", true, false}, {"subdir2" + string(filepath.Separator), true, false}, {"subdir2/file2.txt", false, false}, {"subdir2/doesntexist.txt", false, true}, // subdir3 has files on disk and in overlay {filepath.Join(cwd, "subdir3"), true, false}, {"subdir3", true, false}, {"subdir3" + string(filepath.Separator), true, false}, {"subdir3/file3a.txt", false, false}, {"subdir3/file3b.txt", false, false}, {"subdir3/doesntexist.txt", false, true}, // subdir4 is overlaid with a file {filepath.Join(cwd, "subdir4"), false, false}, {"subdir4", false, false}, {"subdir4" + string(filepath.Separator), false, false}, {"subdir4/file4.txt", false, false}, {"subdir4/doesntexist.txt", false, false}, // subdir5 doesn't exist, and is overlaid with a "delete" entry {filepath.Join(cwd, "subdir5"), false, false}, {"subdir5", false, false}, {"subdir5" + string(filepath.Separator), false, false}, {"subdir5/file5.txt", false, false}, {"subdir5/doesntexist.txt", false, false}, // subdir6 does exist, and is overlaid with a "delete" entry {filepath.Join(cwd, "subdir6"), false, false}, {"subdir6", false, false}, {"subdir6" + string(filepath.Separator), false, false}, {"subdir6/file6.txt", false, false}, {"subdir6/doesntexist.txt", false, false}, } for _, tc := range testCases { got, err := IsDir(tc.path) if err != nil { if !tc.wantErr { t.Errorf("IsDir(%q): got error with string %q, want no error", tc.path, err.Error()) } continue } if tc.wantErr { t.Errorf("IsDir(%q): got no error, want error", tc.path) } if tc.want != got { t.Errorf("IsDir(%q) = %v, want %v", tc.path, got, tc.want) } } } const readDirOverlay = ` { "Replace": { "subdir2/file2.txt": "overlayfiles/subdir2_file2.txt", "subdir4": "overlayfiles/subdir4", "subdir3/file3b.txt": "overlayfiles/subdir3_file3b.txt", "subdir5": "", "subdir6/asubsubdir/afile.txt": "overlayfiles/subdir6_asubsubdir_afile.txt", "subdir6/asubsubdir/zfile.txt": "overlayfiles/subdir6_asubsubdir_zfile.txt", "subdir6/zsubsubdir/file.txt": "overlayfiles/subdir6_zsubsubdir_file.txt", "subdir7/asubsubdir/file.txt": "overlayfiles/subdir7_asubsubdir_file.txt", "subdir7/zsubsubdir/file.txt": "overlayfiles/subdir7_zsubsubdir_file.txt", "subdir8/doesntexist": "this_file_doesnt_exist_anywhere", "other/pointstodir": "overlayfiles/this_is_a_directory", "parentoverwritten/subdir1": "overlayfiles/parentoverwritten_subdir1", "subdir9/this_file_is_overlaid.txt": "overlayfiles/subdir9_this_file_is_overlaid.txt", "subdir10/only_deleted_file.txt": "", "subdir11/deleted.txt": "", "subdir11": "overlayfiles/subdir11", "textfile.txt/file.go": "overlayfiles/textfile_txt_file.go" } } -- subdir1/file1.txt -- -- subdir3/file3a.txt -- 33 -- subdir4/file4.txt -- 444 -- subdir6/file.txt -- -- subdir6/asubsubdir/file.txt -- -- subdir6/anothersubsubdir/file.txt -- -- subdir9/this_file_is_overlaid.txt -- -- subdir10/only_deleted_file.txt -- this will be deleted in overlay -- subdir11/deleted.txt -- -- parentoverwritten/subdir1/subdir2/subdir3/file.txt -- -- textfile.txt -- this will be overridden by textfile.txt/file.go -- overlayfiles/subdir2_file2.txt -- 2 -- overlayfiles/subdir3_file3b.txt -- 66666 -- overlayfiles/subdir4 -- x -- overlayfiles/subdir6_asubsubdir_afile.txt -- -- overlayfiles/subdir6_asubsubdir_zfile.txt -- -- overlayfiles/subdir6_zsubsubdir_file.txt -- -- overlayfiles/subdir7_asubsubdir_file.txt -- -- overlayfiles/subdir7_zsubsubdir_file.txt -- -- overlayfiles/parentoverwritten_subdir1 -- x -- overlayfiles/subdir9_this_file_is_overlaid.txt -- 99999999 -- overlayfiles/subdir11 -- -- overlayfiles/this_is_a_directory/file.txt -- -- overlayfiles/textfile_txt_file.go -- x ` func TestReadDir(t *testing.T) { initOverlay(t, readDirOverlay) type entry struct { name string size int64 isDir bool } testCases := []struct { dir string want []entry }{ { ".", []entry{ {"other", 0, true}, {"overlayfiles", 0, true}, {"parentoverwritten", 0, true}, {"subdir1", 0, true}, {"subdir10", 0, true}, {"subdir11", 0, false}, {"subdir2", 0, true}, {"subdir3", 0, true}, {"subdir4", 2, false}, // no subdir5. {"subdir6", 0, true}, {"subdir7", 0, true}, {"subdir8", 0, true}, {"subdir9", 0, true}, {"textfile.txt", 0, true}, }, }, { "subdir1", []entry{ {"file1.txt", 1, false}, }, }, { "subdir2", []entry{ {"file2.txt", 2, false}, }, }, { "subdir3", []entry{ {"file3a.txt", 3, false}, {"file3b.txt", 6, false}, }, }, { "subdir6", []entry{ {"anothersubsubdir", 0, true}, {"asubsubdir", 0, true}, {"file.txt", 0, false}, {"zsubsubdir", 0, true}, }, }, { "subdir6/asubsubdir", []entry{ {"afile.txt", 0, false}, {"file.txt", 0, false}, {"zfile.txt", 0, false}, }, }, { "subdir8", []entry{ {"doesntexist", 0, false}, // entry is returned even if destination file doesn't exist }, }, { // check that read dir actually redirects files that already exist // the original this_file_is_overlaid.txt is empty "subdir9", []entry{ {"this_file_is_overlaid.txt", 9, false}, }, }, { "subdir10", []entry{}, }, { "parentoverwritten", []entry{ {"subdir1", 2, false}, }, }, { "textfile.txt", []entry{ {"file.go", 2, false}, }, }, } for _, tc := range testCases { dir, want := tc.dir, tc.want infos, err := ReadDir(dir) if err != nil { t.Errorf("ReadDir(%q): %v", dir, err) continue } // Sorted diff of want and infos. for len(infos) > 0 || len(want) > 0 { switch { case len(want) == 0 || len(infos) > 0 && infos[0].Name() < want[0].name: t.Errorf("ReadDir(%q): unexpected entry: %s IsDir=%v", dir, infos[0].Name(), infos[0].IsDir()) infos = infos[1:] case len(infos) == 0 || len(want) > 0 && want[0].name < infos[0].Name(): t.Errorf("ReadDir(%q): missing entry: %s IsDir=%v", dir, want[0].name, want[0].isDir) want = want[1:] default: if infos[0].IsDir() != want[0].isDir { t.Errorf("ReadDir(%q): %s: IsDir=%v, want IsDir=%v", dir, want[0].name, infos[0].IsDir(), want[0].isDir) } infos = infos[1:] want = want[1:] } } } errCases := []string{ "subdir1/file1.txt", // regular file on disk "subdir2/file2.txt", // regular file in overlay "subdir4", // directory overlaid with regular file "subdir5", // directory deleted in overlay "parentoverwritten/subdir1/subdir2/subdir3", // parentoverwritten/subdir1 overlaid with regular file "parentoverwritten/subdir1/subdir2", // parentoverwritten/subdir1 overlaid with regular file "subdir11", // directory with deleted child, overlaid with regular file "other/pointstodir", } for _, dir := range errCases { _, err := ReadDir(dir) if _, ok := err.(*fs.PathError); !ok { t.Errorf("ReadDir(%q): err = %T (%v), want fs.PathError", dir, err, err) } } } func TestGlob(t *testing.T) { initOverlay(t, readDirOverlay) testCases := []struct { pattern string match []string }{ { "*o*", []string{ "other", "overlayfiles", "parentoverwritten", }, }, { "subdir2/file2.txt", []string{ "subdir2/file2.txt", }, }, { "*/*.txt", []string{ "overlayfiles/subdir2_file2.txt", "overlayfiles/subdir3_file3b.txt", "overlayfiles/subdir6_asubsubdir_afile.txt", "overlayfiles/subdir6_asubsubdir_zfile.txt", "overlayfiles/subdir6_zsubsubdir_file.txt", "overlayfiles/subdir7_asubsubdir_file.txt", "overlayfiles/subdir7_zsubsubdir_file.txt", "overlayfiles/subdir9_this_file_is_overlaid.txt", "subdir1/file1.txt", "subdir2/file2.txt", "subdir3/file3a.txt", "subdir3/file3b.txt", "subdir6/file.txt", "subdir9/this_file_is_overlaid.txt", }, }, } for _, tc := range testCases { pattern := tc.pattern match, err := Glob(pattern) if err != nil { t.Errorf("Glob(%q): %v", pattern, err) continue } want := tc.match for i, name := range want { if name != tc.pattern { want[i] = filepath.FromSlash(name) } } for len(match) > 0 || len(want) > 0 { switch { case len(match) == 0 || len(want) > 0 && want[0] < match[0]: t.Errorf("Glob(%q): missing match: %s", pattern, want[0]) want = want[1:] case len(want) == 0 || len(match) > 0 && match[0] < want[0]: t.Errorf("Glob(%q): extra match: %s", pattern, match[0]) match = match[1:] default: want = want[1:] match = match[1:] } } } } func TestActual(t *testing.T) { initOverlay(t, ` { "Replace": { "subdir2/file2.txt": "overlayfiles/subdir2_file2.txt", "subdir3/doesntexist": "this_file_doesnt_exist_anywhere", "subdir4/this_file_is_overlaid.txt": "overlayfiles/subdir4_this_file_is_overlaid.txt", "subdir5/deleted.txt": "", "parentoverwritten/subdir1": "" } } -- subdir1/file1.txt -- file 1 -- subdir4/this_file_is_overlaid.txt -- these contents are replaced by the overlay -- parentoverwritten/subdir1/subdir2/subdir3/file.txt -- -- subdir5/deleted.txt -- deleted -- overlayfiles/subdir2_file2.txt -- file 2 -- overlayfiles/subdir4_this_file_is_overlaid.txt -- 99999999 `) cwd := cwd() testCases := []struct { path string wantPath string wantOK bool }{ {"subdir1/file1.txt", "subdir1/file1.txt", false}, // Actual returns false for directories {"subdir2", "subdir2", false}, {"subdir2/file2.txt", filepath.Join(cwd, "overlayfiles/subdir2_file2.txt"), true}, // Actual doesn't stat a file to see if it exists, so it happily returns // the 'to' path and true even if the 'to' path doesn't exist on disk. {"subdir3/doesntexist", filepath.Join(cwd, "this_file_doesnt_exist_anywhere"), true}, // Like the subdir2/file2.txt case above, but subdir4 exists on disk, but subdir2 does not. {"subdir4/this_file_is_overlaid.txt", filepath.Join(cwd, "overlayfiles/subdir4_this_file_is_overlaid.txt"), true}, {"subdir5", "subdir5", false}, {"subdir5/deleted.txt", "", true}, } for _, tc := range testCases { path := Actual(tc.path) ok := Replaced(tc.path) if path != tc.wantPath { t.Errorf("Actual(%q) = %q, want %q", tc.path, path, tc.wantPath) } if ok != tc.wantOK { t.Errorf("Replaced(%q) = %v, want %v", tc.path, ok, tc.wantOK) } } } func TestOpen(t *testing.T) { initOverlay(t, ` { "Replace": { "subdir2/file2.txt": "overlayfiles/subdir2_file2.txt", "subdir3/doesntexist": "this_file_doesnt_exist_anywhere", "subdir4/this_file_is_overlaid.txt": "overlayfiles/subdir4_this_file_is_overlaid.txt", "subdir5/deleted.txt": "", "parentoverwritten/subdir1": "", "childoverlay/subdir1.txt/child.txt": "overlayfiles/child.txt", "subdir11/deleted.txt": "", "subdir11": "overlayfiles/subdir11", "parentdeleted": "", "parentdeleted/file.txt": "overlayfiles/parentdeleted_file.txt" } } -- subdir11/deleted.txt -- -- subdir1/file1.txt -- file 1 -- subdir4/this_file_is_overlaid.txt -- these contents are replaced by the overlay -- parentoverwritten/subdir1/subdir2/subdir3/file.txt -- -- childoverlay/subdir1.txt -- this file doesn't exist because the path childoverlay/subdir1.txt/child.txt is in the overlay -- subdir5/deleted.txt -- deleted -- parentdeleted -- this will be deleted so that parentdeleted/file.txt can exist -- overlayfiles/subdir2_file2.txt -- file 2 -- overlayfiles/subdir4_this_file_is_overlaid.txt -- 99999999 -- overlayfiles/child.txt -- -- overlayfiles/subdir11 -- 11 -- overlayfiles/parentdeleted_file.txt -- this can exist because the parent directory is deleted `) testCases := []struct { path string wantContents string isErr bool }{ {"subdir1/file1.txt", "file 1\n", false}, {"subdir2/file2.txt", "file 2\n", false}, {"subdir3/doesntexist", "", true}, {"subdir4/this_file_is_overlaid.txt", "99999999\n", false}, {"subdir5/deleted.txt", "", true}, {"parentoverwritten/subdir1/subdir2/subdir3/file.txt", "", true}, {"childoverlay/subdir1.txt", "", true}, {"subdir11", "11\n", false}, {"parentdeleted/file.txt", "this can exist because the parent directory is deleted\n", false}, } for _, tc := range testCases { f, err := Open(tc.path) if tc.isErr { if err == nil { f.Close() t.Errorf("Open(%q): got no error, but want error", tc.path) } continue } if err != nil { t.Errorf("Open(%q): got error %v, want nil", tc.path, err) continue } contents, err := io.ReadAll(f) if err != nil { t.Errorf("unexpected error reading contents of file: %v", err) } if string(contents) != tc.wantContents { t.Errorf("contents of file opened with Open(%q): got %q, want %q", tc.path, contents, tc.wantContents) } f.Close() } } func TestIsGoDir(t *testing.T) { initOverlay(t, ` { "Replace": { "goinoverlay/file.go": "dummy", "directory/removed/by/file": "dummy", "directory_with_go_dir/dir.go/file.txt": "dummy", "otherdirectory/deleted.go": "", "nonexistentdirectory/deleted.go": "", "textfile.txt/file.go": "dummy" } } -- dummy -- a destination file for the overlay entries to point to contents don't matter for this test -- nogo/file.txt -- -- goondisk/file.go -- -- goinoverlay/file.txt -- -- directory/removed/by/file/in/overlay/file.go -- -- otherdirectory/deleted.go -- -- textfile.txt -- `) testCases := []struct { dir string want bool wantErr bool }{ {"nogo", false, false}, {"goondisk", true, false}, {"goinoverlay", true, false}, {"directory/removed/by/file/in/overlay", false, false}, {"directory_with_go_dir", false, false}, {"otherdirectory", false, false}, {"nonexistentdirectory", false, false}, {"textfile.txt", true, false}, } for _, tc := range testCases { got, gotErr := IsGoDir(tc.dir) if tc.wantErr { if gotErr == nil { t.Errorf("IsGoDir(%q): got %v, %v; want non-nil error", tc.dir, got, gotErr) } continue } if gotErr != nil { t.Errorf("IsGoDir(%q): got %v, %v; want nil error", tc.dir, got, gotErr) } if got != tc.want { t.Errorf("IsGoDir(%q) = %v; want %v", tc.dir, got, tc.want) } } } func TestWalk(t *testing.T) { // The root of the walk must be a name with an actual basename, not just ".". // Walk uses Lstat to obtain the name of the root, and Lstat on platforms // other than Plan 9 reports the name "." instead of the actual base name of // the directory. (See https://golang.org/issue/42115.) type file struct { path string name string size int64 mode fs.FileMode isDir bool } testCases := []struct { name string overlay string root string wantFiles []file }{ {"no overlay", ` {} -- dir/file.txt -- `, "dir", []file{ {"dir", "dir", 0, fs.ModeDir | 0700, true}, {"dir/file.txt", "file.txt", 0, 0600, false}, }, }, {"overlay with different file", ` { "Replace": { "dir/file.txt": "dir/other.txt" } } -- dir/file.txt -- -- dir/other.txt -- contents of other file `, "dir", []file{ {"dir", "dir", 0, fs.ModeDir | 0500, true}, {"dir/file.txt", "file.txt", 23, 0600, false}, {"dir/other.txt", "other.txt", 23, 0600, false}, }, }, {"overlay with new file", ` { "Replace": { "dir/file.txt": "dir/other.txt" } } -- dir/other.txt -- contents of other file `, "dir", []file{ {"dir", "dir", 0, fs.ModeDir | 0500, true}, {"dir/file.txt", "file.txt", 23, 0600, false}, {"dir/other.txt", "other.txt", 23, 0600, false}, }, }, {"overlay with new directory", ` { "Replace": { "dir/subdir/file.txt": "dir/other.txt" } } -- dir/other.txt -- contents of other file `, "dir", []file{ {"dir", "dir", 0, fs.ModeDir | 0500, true}, {"dir/other.txt", "other.txt", 23, 0600, false}, {"dir/subdir", "subdir", 0, fs.ModeDir | 0500, true}, {"dir/subdir/file.txt", "file.txt", 23, 0600, false}, }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { initOverlay(t, tc.overlay) var got []file WalkDir(tc.root, func(path string, d fs.DirEntry, err error) error { info, err := d.Info() if err != nil { t.Fatal(err) } if info.Name() != d.Name() { t.Errorf("walk %s: d.Name() = %q, but info.Name() = %q", path, d.Name(), info.Name()) } if info.IsDir() != d.IsDir() { t.Errorf("walk %s: d.IsDir() = %v, but info.IsDir() = %v", path, d.IsDir(), info.IsDir()) } if info.Mode().Type() != d.Type() { t.Errorf("walk %s: d.Type() = %v, but info.Mode().Type() = %v", path, d.Type(), info.Mode().Type()) } got = append(got, file{path, d.Name(), info.Size(), info.Mode(), d.IsDir()}) return nil }) if len(got) != len(tc.wantFiles) { t.Errorf("Walk: saw %#v in walk; want %#v", got, tc.wantFiles) } for i := 0; i < len(got) && i < len(tc.wantFiles); i++ { wantPath := filepath.FromSlash(tc.wantFiles[i].path) if got[i].path != wantPath { t.Errorf("walk #%d: path = %q, want %q", i, got[i].path, wantPath) } if got[i].name != tc.wantFiles[i].name { t.Errorf("walk %s: Name = %q, want %q", got[i].path, got[i].name, tc.wantFiles[i].name) } if got[i].mode&(fs.ModeDir|0700) != tc.wantFiles[i].mode { t.Errorf("walk %s: Mode = %q, want %q", got[i].path, got[i].mode&(fs.ModeDir|0700), tc.wantFiles[i].mode) } if got[i].isDir != tc.wantFiles[i].isDir { t.Errorf("walk %s: IsDir = %v, want %v", got[i].path, got[i].isDir, tc.wantFiles[i].isDir) } } }) } } func TestWalkSkipDir(t *testing.T) { initOverlay(t, ` { "Replace": { "dir/skip/file.go": "dummy.txt", "dir/dontskip/file.go": "dummy.txt", "dir/dontskip/skip/file.go": "dummy.txt" } } -- dummy.txt -- `) var seen []string WalkDir("dir", func(path string, d fs.DirEntry, err error) error { seen = append(seen, filepath.ToSlash(path)) if d.Name() == "skip" { return filepath.SkipDir } return nil }) wantSeen := []string{"dir", "dir/dontskip", "dir/dontskip/file.go", "dir/dontskip/skip", "dir/skip"} if len(seen) != len(wantSeen) { t.Errorf("paths seen in walk: got %v entries; want %v entries", len(seen), len(wantSeen)) } for i := 0; i < len(seen) && i < len(wantSeen); i++ { if seen[i] != wantSeen[i] { t.Errorf("path #%v seen walking tree: want %q, got %q", i, seen[i], wantSeen[i]) } } } func TestWalkSkipAll(t *testing.T) { initOverlay(t, ` { "Replace": { "dir/subdir1/foo1": "dummy.txt", "dir/subdir1/foo2": "dummy.txt", "dir/subdir1/foo3": "dummy.txt", "dir/subdir2/foo4": "dummy.txt", "dir/zzlast": "dummy.txt" } } -- dummy.txt -- `) var seen []string WalkDir("dir", func(path string, d fs.DirEntry, err error) error { seen = append(seen, filepath.ToSlash(path)) if d.Name() == "foo2" { return filepath.SkipAll } return nil }) wantSeen := []string{"dir", "dir/subdir1", "dir/subdir1/foo1", "dir/subdir1/foo2"} if len(seen) != len(wantSeen) { t.Errorf("paths seen in walk: got %v entries; want %v entries", len(seen), len(wantSeen)) } for i := 0; i < len(seen) && i < len(wantSeen); i++ { if seen[i] != wantSeen[i] { t.Errorf("path %#v seen walking tree: got %q, want %q", i, seen[i], wantSeen[i]) } } } func TestWalkError(t *testing.T) { initOverlay(t, "{}") alreadyCalled := false err := WalkDir("foo", func(path string, d fs.DirEntry, err error) error { if alreadyCalled { t.Fatal("expected walk function to be called exactly once, but it was called more than once") } alreadyCalled = true return errors.New("returned from function") }) if !alreadyCalled { t.Fatal("expected walk function to be called exactly once, but it was never called") } if err == nil { t.Fatalf("Walk: got no error, want error") } if err.Error() != "returned from function" { t.Fatalf("Walk: got error %v, want \"returned from function\" error", err) } } func TestWalkSymlink(t *testing.T) { testenv.MustHaveSymlink(t) initOverlay(t, `{ "Replace": {"overlay_symlink/file": "symlink/file"} } -- dir/file --`) // Create symlink if err := os.Symlink("dir", "symlink"); err != nil { t.Error(err) } testCases := []struct { name string dir string wantFiles []string }{ {"control", "dir", []string{"dir", filepath.Join("dir", "file")}}, // ensure Walk doesn't walk into the directory pointed to by the symlink // (because it's supposed to use Lstat instead of Stat). {"symlink_to_dir", "symlink", []string{"symlink"}}, {"overlay_to_symlink_to_dir", "overlay_symlink", []string{"overlay_symlink", filepath.Join("overlay_symlink", "file")}}, // However, adding filepath.Separator should cause the link to be resolved. {"symlink_with_slash", "symlink" + string(filepath.Separator), []string{"symlink" + string(filepath.Separator), filepath.Join("symlink", "file")}}, {"overlay_to_symlink_to_dir", "overlay_symlink" + string(filepath.Separator), []string{"overlay_symlink" + string(filepath.Separator), filepath.Join("overlay_symlink", "file")}}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { var got []string err := WalkDir(tc.dir, func(path string, d fs.DirEntry, err error) error { t.Logf("walk %q", path) got = append(got, path) if err != nil { t.Errorf("walkfn: got non nil err argument: %v, want nil err argument", err) } return nil }) if err != nil { t.Errorf("Walk: got error %q, want nil", err) } if !reflect.DeepEqual(got, tc.wantFiles) { t.Errorf("files examined by walk: got %v, want %v", got, tc.wantFiles) } }) } } func TestLstat(t *testing.T) { type file struct { name string size int64 mode fs.FileMode // mode & (fs.ModeDir|0x700): only check 'user' permissions isDir bool } testCases := []struct { name string overlay string path string want file wantErr bool }{ { "regular_file", `{} -- file.txt -- contents`, "file.txt", file{"file.txt", 9, 0600, false}, false, }, { "new_file_in_overlay", `{"Replace": {"file.txt": "dummy.txt"}} -- dummy.txt -- contents`, "file.txt", file{"file.txt", 9, 0600, false}, false, }, { "file_replaced_in_overlay", `{"Replace": {"file.txt": "dummy.txt"}} -- file.txt -- -- dummy.txt -- contents`, "file.txt", file{"file.txt", 9, 0600, false}, false, }, { "file_cant_exist", `{"Replace": {"deleted": "dummy.txt"}} -- deleted/file.txt -- -- dummy.txt -- `, "deleted/file.txt", file{}, true, }, { "deleted", `{"Replace": {"deleted": ""}} -- deleted -- `, "deleted", file{}, true, }, { "dir_on_disk", `{} -- dir/foo.txt -- `, "dir", file{"dir", 0, 0700 | fs.ModeDir, true}, false, }, { "dir_in_overlay", `{"Replace": {"dir/file.txt": "dummy.txt"}} -- dummy.txt -- `, "dir", file{"dir", 0, 0500 | fs.ModeDir, true}, false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { initOverlay(t, tc.overlay) got, err := Lstat(tc.path) if tc.wantErr { if err == nil { t.Errorf("lstat(%q): got no error, want error", tc.path) } return } if err != nil { t.Fatalf("lstat(%q): got error %v, want no error", tc.path, err) } if got.Name() != tc.want.name { t.Errorf("lstat(%q).Name(): got %q, want %q", tc.path, got.Name(), tc.want.name) } if got.Mode()&(fs.ModeDir|0700) != tc.want.mode { t.Errorf("lstat(%q).Mode()&(fs.ModeDir|0700): got %v, want %v", tc.path, got.Mode()&(fs.ModeDir|0700), tc.want.mode) } if got.IsDir() != tc.want.isDir { t.Errorf("lstat(%q).IsDir(): got %v, want %v", tc.path, got.IsDir(), tc.want.isDir) } if tc.want.isDir { return // don't check size for directories } if got.Size() != tc.want.size { t.Errorf("lstat(%q).Size(): got %v, want %v", tc.path, got.Size(), tc.want.size) } }) } } func TestStat(t *testing.T) { testenv.MustHaveSymlink(t) type file struct { name string size int64 mode os.FileMode // mode & (os.ModeDir|0x700): only check 'user' permissions isDir bool } testCases := []struct { name string overlay string path string want file wantErr bool }{ { "regular_file", `{} -- file.txt -- contents`, "file.txt", file{"file.txt", 9, 0600, false}, false, }, { "new_file_in_overlay", `{"Replace": {"file.txt": "dummy.txt"}} -- dummy.txt -- contents`, "file.txt", file{"file.txt", 9, 0600, false}, false, }, { "file_replaced_in_overlay", `{"Replace": {"file.txt": "dummy.txt"}} -- file.txt -- -- dummy.txt -- contents`, "file.txt", file{"file.txt", 9, 0600, false}, false, }, { "file_cant_exist", `{"Replace": {"deleted": "dummy.txt"}} -- deleted/file.txt -- -- dummy.txt -- `, "deleted/file.txt", file{}, true, }, { "deleted", `{"Replace": {"deleted": ""}} -- deleted -- `, "deleted", file{}, true, }, { "dir_on_disk", `{} -- dir/foo.txt -- `, "dir", file{"dir", 0, 0700 | os.ModeDir, true}, false, }, { "dir_in_overlay", `{"Replace": {"dir/file.txt": "dummy.txt"}} -- dummy.txt -- `, "dir", file{"dir", 0, 0500 | os.ModeDir, true}, false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { initOverlay(t, tc.overlay) got, err := Stat(tc.path) if tc.wantErr { if err == nil { t.Errorf("Stat(%q): got no error, want error", tc.path) } return } if err != nil { t.Fatalf("Stat(%q): got error %v, want no error", tc.path, err) } if got.Name() != tc.want.name { t.Errorf("Stat(%q).Name(): got %q, want %q", tc.path, got.Name(), tc.want.name) } if got.Mode()&(os.ModeDir|0700) != tc.want.mode { t.Errorf("Stat(%q).Mode()&(os.ModeDir|0700): got %v, want %v", tc.path, got.Mode()&(os.ModeDir|0700), tc.want.mode) } if got.IsDir() != tc.want.isDir { t.Errorf("Stat(%q).IsDir(): got %v, want %v", tc.path, got.IsDir(), tc.want.isDir) } if tc.want.isDir { return // don't check size for directories } if got.Size() != tc.want.size { t.Errorf("Stat(%q).Size(): got %v, want %v", tc.path, got.Size(), tc.want.size) } }) } } func TestStatSymlink(t *testing.T) { testenv.MustHaveSymlink(t) initOverlay(t, `{ "Replace": {"file.go": "symlink"} } -- to.go -- 0123456789 `) // Create symlink if err := os.Symlink("to.go", "symlink"); err != nil { t.Error(err) } f := "file.go" fi, err := Stat(f) if err != nil { t.Errorf("Stat(%q): got error %q, want nil error", f, err) } if !fi.Mode().IsRegular() { t.Errorf("Stat(%q).Mode(): got %v, want regular mode", f, fi.Mode()) } if fi.Size() != 11 { t.Errorf("Stat(%q).Size(): got %v, want 11", f, fi.Size()) } } func TestBindOverlay(t *testing.T) { initOverlay(t, `{"Replace": {"mtpt/x.go": "xx.go"}} -- mtpt/x.go -- mtpt/x.go -- mtpt/y.go -- mtpt/y.go -- mtpt2/x.go -- mtpt/x.go -- replaced/x.go -- replaced/x.go -- replaced/x/y/z.go -- replaced/x/y/z.go -- xx.go -- xx.go `) testReadFile(t, "mtpt/x.go", "xx.go\n") Bind("replaced", "mtpt") testReadFile(t, "mtpt/x.go", "replaced/x.go\n") testReadDir(t, "mtpt/x", "y/") testReadDir(t, "mtpt/x/y", "z.go") testReadFile(t, "mtpt/x/y/z.go", "replaced/x/y/z.go\n") testReadFile(t, "mtpt/y.go", "ERROR") Bind("replaced", "mtpt2/a/b") testReadDir(t, "mtpt2", "a/", "x.go") testReadDir(t, "mtpt2/a", "b/") testReadDir(t, "mtpt2/a/b", "x/", "x.go") testReadFile(t, "mtpt2/a/b/x.go", "replaced/x.go\n") } var badOverlayTests = []struct { json string err string }{ {`{`, "parsing overlay JSON: unexpected end of JSON input"}, {`{"Replace": {"":"a"}}`, "empty string key in overlay map"}, {`{"Replace": {"/tmp/x": "y", "x": "y"}}`, `duplicate paths /tmp/x and x in overlay map`}, {`{"Replace": {"/tmp/x/z": "z", "x":"y"}}`, `inconsistent files /tmp/x and /tmp/x/z in overlay map`}, {`{"Replace": {"/tmp/x/z/z2": "z", "x":"y"}}`, `inconsistent files /tmp/x and /tmp/x/z/z2 in overlay map`}, {`{"Replace": {"/tmp/x": "y", "x/z/z2": "z"}}`, `inconsistent files /tmp/x and /tmp/x/z/z2 in overlay map`}, } func TestBadOverlay(t *testing.T) { tmp := "/tmp" if runtime.GOOS == "windows" { tmp = `C:\tmp` } cwd = sync.OnceValue(func() string { return tmp }) defer resetForTesting() for i, tt := range badOverlayTests { if runtime.GOOS == "windows" { tt.json = strings.ReplaceAll(tt.json, `/tmp`, tmp) // fix tmp tt.json = strings.ReplaceAll(tt.json, `/`, `\`) // use backslashes tt.json = strings.ReplaceAll(tt.json, `\`, `\\`) // JSON escaping tt.err = strings.ReplaceAll(tt.err, `/tmp`, tmp) // fix tmp tt.err = strings.ReplaceAll(tt.err, `/`, `\`) // use backslashes } err := initFromJSON([]byte(tt.json)) if err == nil || err.Error() != tt.err { t.Errorf("#%d: err=%v, want %q", i, err, tt.err) } } } func testReadFile(t *testing.T, name string, want string) { t.Helper() data, err := ReadFile(name) if want == "ERROR" { if data != nil || err == nil { t.Errorf("ReadFile(%q) = %q, %v, want nil, error", name, data, err) } return } if string(data) != want || err != nil { t.Errorf("ReadFile(%q) = %q, %v, want %q, nil", name, data, err, want) } } func testReadDir(t *testing.T, name string, want ...string) { t.Helper() dirs, err := ReadDir(name) var names []string for _, d := range dirs { name := d.Name() if d.IsDir() { name += "/" } names = append(names, name) } if !slices.Equal(names, want) || err != nil { t.Errorf("ReadDir(%q) = %q, %v, want %q, nil", name, names, err, want) } }