Patch 3/8: Implement stat and dir creation for FSTestOSWrapper

Implements Stat() and Mkdir(), and adds testing for Stat(), Mkdir(),
and MkdirAll().

Bug: 436025865
Change-Id: I0c9ece64a46bdb271cfca23e08d6a28f9df63995
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/256456
Commit-Queue: Brian Sheedy <bsheedy@google.com>
Auto-Submit: Ryan Harrison <rharrison@chromium.org>
Commit-Queue: Ryan Harrison <rharrison@chromium.org>
Reviewed-by: Brian Sheedy <bsheedy@google.com>
diff --git a/tools/src/oswrapper/fstestoswrapper.go b/tools/src/oswrapper/fstestoswrapper.go
index aebf559..9f00d4f 100644
--- a/tools/src/oswrapper/fstestoswrapper.go
+++ b/tools/src/oswrapper/fstestoswrapper.go
@@ -145,7 +145,7 @@
 }
 
 func (w FSTestFilesystemReaderWriter) Stat(name string) (os.FileInfo, error) {
-	panic("Stat() is not currently implemented in fstest wrapper")
+	return fs.Stat(w.fs(), w.CleanPath(name))
 }
 
 func (w FSTestFilesystemReaderWriter) Walk(root string, fn filepath.WalkFunc) error {
@@ -158,37 +158,68 @@
 	panic("Create() is not currently implemented in fstest wrapper")
 }
 
-func (w FSTestFilesystemReaderWriter) Mkdir(path string, perm os.FileMode) error {
-	panic("Mkdir() is not currently implemented in fstest wrapper")
-}
-
-func (w FSTestFilesystemReaderWriter) MkdirAll(path string, perm os.FileMode) error {
-	p := w.CleanPath(path)
+func (w FSTestFilesystemReaderWriter) Mkdir(dir string, perm os.FileMode) error {
+	p := w.CleanPath(dir)
 
 	if p == "." {
+		// The root directory always exists in a fs.FS.
+		// os.Mkdir returns EEXIST in this case.
+		return &os.PathError{Op: "mkdir", Path: dir, Err: os.ErrExist}
+	}
+
+	if _, exists := w.FS[p]; exists {
+		return &os.PathError{Op: "mkdir", Path: dir, Err: os.ErrExist}
+	}
+
+	parent := filepath.Dir(p)
+	if parent != "." {
+		parentInfo, parentExists := w.FS[parent]
+		if !parentExists {
+			return &os.PathError{Op: "mkdir", Path: dir, Err: os.ErrNotExist}
+		}
+		if !parentInfo.Mode.IsDir() {
+			return &os.PathError{Op: "mkdir", Path: dir, Err: fmt.Errorf("not a directory")}
+		}
+	}
+	w.FS[p] = &fstest.MapFile{Mode: os.ModeDir | perm, ModTime: time.Now()}
+	return nil
+}
+
+func (w FSTestFilesystemReaderWriter) MkdirAll(dir string, perm os.FileMode) error {
+	err := w.Mkdir(dir, perm)
+	if err == nil {
 		return nil
 	}
 
-	parts := strings.Split(p, "/")
-	current := ""
-	for i, part := range parts {
-		if current == "" {
-			current = part
-		} else {
-			current = current + "/" + part // Internally FSTestOSWrapper uses '/' for separators
+	if os.IsExist(err) {
+		info, statErr := w.Stat(dir)
+		if statErr == nil && info.IsDir() {
+			// p already exists and is a directory, so return success
+			return nil
 		}
-		if file, exists := w.FS[current]; !exists {
-			w.FS[current] = &fstest.MapFile{Mode: os.ModeDir | perm, ModTime: time.Now()}
-		} else if !file.Mode.IsDir() {
-			// If this is the last part of the path, the error is that the file exists.
-			// Otherwise, an intermediate path component is not a directory.
-			if i < len(parts)-1 {
-				return &os.PathError{Op: "mkdir", Path: path, Err: fmt.Errorf("not a directory")}
-			}
-			return &os.PathError{Op: "mkdir", Path: path, Err: os.ErrExist}
-		}
+		// p already exists and is not a directory, so propagate the error
+		return err
 	}
-	return nil
+
+	if !os.IsNotExist(err) {
+		// Unexpected failure, probably indicating a bad path, i.e. parent is a file, so propagate the error
+		return err
+	}
+
+	// At this point, it is known the problem was the parent didn't exist, so need to recursively create it.
+	p := w.CleanPath(dir)
+	parent := filepath.Dir(p)
+	if parent == "." || parent == p {
+		// The parent is the root of the filesystem, so Mkdir should have succeeded, so propagating the error.
+		return err
+	}
+
+	if err := w.MkdirAll(parent, perm); err != nil {
+		return err
+	}
+
+	// Creating the directory now that the parent exists.
+	return w.Mkdir(p, perm)
 }
 
 func (w FSTestFilesystemReaderWriter) MkdirTemp(dir, pattern string) (string, error) {
diff --git a/tools/src/oswrapper/fstestoswrapper_test.go b/tools/src/oswrapper/fstestoswrapper_test.go
index 2bf1a20..f9c960d 100644
--- a/tools/src/oswrapper/fstestoswrapper_test.go
+++ b/tools/src/oswrapper/fstestoswrapper_test.go
@@ -273,7 +273,7 @@
 			name: "Read a directory",
 			path: filepath.Join(root, "mydir"),
 			setup: unittestSetup{
-				initialDirs: []string{"/mydir"},
+				initialDirs: []string{filepath.Join(root, "mydir")},
 			},
 			expectedError: expectedError{
 				wantErrMsg: "is a directory",
@@ -342,8 +342,478 @@
 	}
 }
 
+func TestFSTestOSWrapper_Stat(t *testing.T) {
+	root := getTestRoot()
+	tests := []struct {
+		name   string
+		setup  unittestSetup
+		path   string
+		verify func(t *testing.T, info os.FileInfo)
+		expectedError
+	}{
+		{
+			name: "Stat a file",
+			path: filepath.Join(root, "file.txt"),
+			setup: unittestSetup{
+				initialFiles: map[string]string{filepath.Join(root, "file.txt"): "content"},
+			},
+			verify: func(t *testing.T, info os.FileInfo) {
+				require.False(t, info.IsDir())
+				require.Equal(t, "file.txt", info.Name())
+				require.Equal(t, int64(7), info.Size())
+			},
+		},
+		{
+			name: "Stat a directory",
+			path: filepath.Join(root, "dir"),
+			setup: unittestSetup{
+				initialDirs: []string{filepath.Join(root, "dir")},
+			},
+			verify: func(t *testing.T, info os.FileInfo) {
+				require.True(t, info.IsDir())
+				require.Equal(t, "dir", info.Name())
+			},
+		},
+		{
+			name: "Stat non-existent path",
+			path: filepath.Join(root, "nonexistent"),
+			expectedError: expectedError{
+				wantErrIs: os.ErrNotExist,
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			wrapper := tc.setup.setup(t)
+			info, err := wrapper.Stat(tc.path)
+
+			if tc.expectedError.Check(t, err) {
+				return
+			}
+
+			if tc.verify != nil {
+				tc.verify(t, info)
+			}
+		})
+	}
+}
+
+func TestFSTestOSWrapper_Stat_MatchesReal(t *testing.T) {
+	tests := []struct {
+		name  string
+		setup matchesRealSetup
+		path  string
+	}{
+		{
+			name: "Stat a file",
+			setup: matchesRealSetup{unittestSetup{
+				initialFiles: map[string]string{
+					"file.txt": "content",
+				},
+			}},
+			path: "file.txt",
+		},
+		{
+			name: "Stat a directory",
+			setup: matchesRealSetup{unittestSetup{
+				initialDirs: []string{
+					"dir",
+				},
+			}},
+			path: "dir",
+		},
+		{
+			name: "Stat non-existent path",
+			path: "nonexistent",
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			realRoot, realFS, testFS := tc.setup.setup(t)
+			defer os.RemoveAll(realRoot)
+
+			// Execute
+			realInfo, realErr := realFS.Stat(filepath.Join(realRoot, tc.path))
+			testInfo, testErr := testFS.Stat(tc.path)
+
+			requireErrorsMatch(t, realErr, testErr)
+			if realErr == nil {
+				require.Equal(t, realInfo.Name(), testInfo.Name())
+				require.Equal(t, realInfo.IsDir(), testInfo.IsDir())
+				// The size of a directory is system-dependent, so only compare sizes for files.
+				if !realInfo.IsDir() {
+					require.Equal(t, realInfo.Size(), testInfo.Size())
+				}
+			}
+		})
+	}
+}
+
 // --- FilesystemWriter tests ---
 
+func TestFSTestOSWrapper_Mkdir(t *testing.T) {
+	root := getTestRoot()
+	tests := []struct {
+		name         string
+		setup        unittestSetup
+		path         string
+		mode         os.FileMode
+		expectedMode os.FileMode
+		verify       func(t *testing.T, fs oswrapper.FSTestOSWrapper)
+		expectedError
+	}{
+		{
+			name:         "Create new directory",
+			path:         filepath.Join(root, "newdir"),
+			mode:         0755,
+			expectedMode: 0755,
+			verify: func(t *testing.T, fs oswrapper.FSTestOSWrapper) {
+				info, err := fs.Stat(filepath.Join(root, "newdir"))
+				require.NoError(t, err)
+				require.True(t, info.IsDir())
+			},
+		},
+		{
+			name:         "Create in existing subdirectory",
+			path:         filepath.Join(root, "existing", "newdir"),
+			mode:         0755,
+			expectedMode: 0755,
+			setup: unittestSetup{
+				initialDirs: []string{filepath.Join(root, "existing")},
+			},
+			verify: func(t *testing.T, fs oswrapper.FSTestOSWrapper) {
+				info, err := fs.Stat(filepath.Join(root, "existing", "newdir"))
+				require.NoError(t, err)
+				require.True(t, info.IsDir())
+			},
+		},
+		{
+			name:         "Create new directory with specific mode",
+			path:         filepath.Join(root, "newdir_mode"),
+			mode:         0700,
+			expectedMode: 0700,
+		},
+		{
+			name: "Directory already exists",
+			path: filepath.Join(root, "mydir"),
+			setup: unittestSetup{
+				initialDirs: []string{filepath.Join(root, "mydir")},
+			},
+			expectedError: expectedError{
+				wantErrIs: os.ErrExist,
+			},
+		},
+		{
+			name: "Path is a file",
+			path: filepath.Join(root, "myfile.txt"),
+			setup: unittestSetup{
+				initialFiles: map[string]string{filepath.Join(root, "myfile.txt"): ""},
+			},
+			expectedError: expectedError{
+				wantErrIs: os.ErrExist,
+			},
+		},
+		{
+			name: "Parent directory does not exist",
+			path: filepath.Join(root, "nonexistent", "newdir"),
+			expectedError: expectedError{
+				wantErrIs: os.ErrNotExist,
+			},
+		},
+		{
+			name: "Parent path is a file",
+			path: filepath.Join(root, "file.txt", "newdir"),
+			setup: unittestSetup{
+				initialFiles: map[string]string{filepath.Join(root, "file.txt"): ""},
+			},
+			expectedError: expectedError{
+				wantErrMsg: "not a directory",
+			},
+		},
+		{
+			name: "Path is dot",
+			path: ".",
+			expectedError: expectedError{
+				wantErrIs: os.ErrExist,
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			wrapper := tc.setup.setup(t)
+			err := wrapper.Mkdir(tc.path, tc.mode)
+
+			if tc.expectedError.Check(t, err) {
+				return
+			}
+
+			if tc.verify != nil {
+				tc.verify(t, wrapper)
+			}
+
+			if tc.expectedMode != 0 {
+				cleanedPath := wrapper.CleanPath(tc.path)
+				file, ok := wrapper.FS[cleanedPath]
+				require.True(t, ok, "file %v not found in test fs", cleanedPath)
+				require.Equal(t, tc.expectedMode, file.Mode.Perm())
+			}
+		})
+	}
+}
+
+func TestFSTestOSWrapper_Mkdir_MatchesReal(t *testing.T) {
+	tests := []struct {
+		name  string
+		setup matchesRealSetup
+		path  string // path to Mkdir
+	}{
+		{
+			name: "Create new directory",
+			path: "newdir",
+		},
+		{
+			name: "Create in existing subdirectory",
+			setup: matchesRealSetup{unittestSetup{
+				initialDirs: []string{"foo"},
+			}},
+			path: filepath.Join("foo", "newdir"),
+		},
+		{
+			name: "Create in existing nested subdirectory",
+			setup: matchesRealSetup{unittestSetup{
+				initialDirs: []string{filepath.Join("foo", "bar")},
+			}},
+			path: filepath.Join("foo", "bar", "newdir"),
+		},
+		{
+			name: "Error on existing directory",
+			setup: matchesRealSetup{unittestSetup{
+				initialDirs: []string{"mydir"},
+			}},
+			path: "mydir",
+		},
+		{
+			name: "Error on existing file",
+			setup: matchesRealSetup{unittestSetup{
+				initialFiles: map[string]string{"myfile": "content"},
+			}},
+			path: "myfile",
+		},
+		{
+			name: "Error on non-existent parent",
+			path: filepath.Join("nonexistent", "newdir"),
+		},
+		{
+			name: "Error on parent is file",
+			setup: matchesRealSetup{unittestSetup{
+				initialFiles: map[string]string{"file.txt": "content"},
+			}},
+			path: filepath.Join("file.txt", "newdir"),
+		},
+		{
+			name: "Error on dot",
+			path: ".",
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			realRoot, realFS, testFS := tc.setup.setup(t)
+			defer os.RemoveAll(realRoot)
+
+			// Execute
+			realErr := realFS.Mkdir(filepath.Join(realRoot, tc.path), 0755)
+			testErr := testFS.Mkdir(tc.path, 0755)
+
+			requireErrorsMatch(t, realErr, testErr)
+			if realErr == nil {
+				requireFileSystemsMatch(t, realRoot, testFS)
+			}
+		})
+	}
+}
+
+func TestFSTestOSWrapper_MkdirAll(t *testing.T) {
+	root := getTestRoot()
+	tests := []struct {
+		name         string
+		setup        unittestSetup
+		path         string
+		mode         os.FileMode
+		expectedMode os.FileMode
+		verify       func(t *testing.T, fs oswrapper.FSTestOSWrapper)
+		expectedError
+	}{
+		{
+			name:         "Create new nested directory",
+			path:         filepath.Join(root, "a", "b", "c"),
+			mode:         0755,
+			expectedMode: 0755,
+			verify: func(t *testing.T, fs oswrapper.FSTestOSWrapper) {
+				for _, p := range []string{
+					filepath.Join(root, "a"),
+					filepath.Join(root, "a", "b"),
+					filepath.Join(root, "a", "b", "c"),
+				} {
+					info, err := fs.Stat(p)
+					require.NoError(t, err)
+					require.True(t, info.IsDir())
+					file, ok := fs.FS[fs.CleanPath(p)]
+					require.True(t, ok)
+					require.Equal(t, os.FileMode(0755), file.Mode.Perm())
+				}
+			},
+		},
+		{
+			name:         "Create in existing subdirectory",
+			path:         filepath.Join(root, "a", "b"),
+			mode:         0755,
+			expectedMode: 0755,
+			setup: unittestSetup{
+				initialDirs: []string{filepath.Join(root, "a")},
+			},
+			verify: func(t *testing.T, fs oswrapper.FSTestOSWrapper) {
+				info, err := fs.Stat(filepath.Join(root, "a", "b"))
+				require.NoError(t, err)
+				require.True(t, info.IsDir())
+			},
+		},
+		{
+			name:         "Create new nested directory with specific mode",
+			path:         filepath.Join(root, "d", "e", "f"),
+			mode:         0700,
+			expectedMode: 0700,
+			verify: func(t *testing.T, fs oswrapper.FSTestOSWrapper) {
+				for _, p := range []string{
+					filepath.Join(root, "d"),
+					filepath.Join(root, "d", "e"),
+					filepath.Join(root, "d", "e", "f"),
+				} {
+					info, err := fs.Stat(p)
+					require.NoError(t, err)
+					require.True(t, info.IsDir())
+					file, ok := fs.FS[fs.CleanPath(p)]
+					require.True(t, ok)
+					require.Equal(t, os.FileMode(0700), file.Mode.Perm())
+				}
+			},
+		},
+		{
+			name: "Path already exists as directory",
+			path: filepath.Join(root, "a", "b"),
+			setup: unittestSetup{
+				initialDirs: []string{filepath.Join(root, "a", "b")},
+			},
+		},
+		{
+			name: "Part of path is a file",
+			path: filepath.Join(root, "a", "b", "c"),
+			setup: unittestSetup{
+				initialFiles: map[string]string{filepath.Join(root, "a", "b"): "i am a file"},
+				initialDirs:  []string{filepath.Join(root, "a")},
+			},
+			expectedError: expectedError{
+				wantErrMsg: "not a directory",
+			},
+		},
+		{
+			name: "Destination path is a file",
+			path: filepath.Join(root, "a", "b"),
+			setup: unittestSetup{
+				initialFiles: map[string]string{filepath.Join(root, "a", "b"): "i am a file"},
+				initialDirs:  []string{filepath.Join(root, "a")},
+			},
+			expectedError: expectedError{
+				wantErrIs: os.ErrExist,
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			wrapper := tc.setup.setup(t)
+			err := wrapper.MkdirAll(tc.path, tc.mode)
+
+			if tc.expectedError.Check(t, err) {
+				return
+			}
+
+			if tc.verify != nil {
+				tc.verify(t, wrapper)
+			}
+
+			if tc.expectedMode != 0 {
+				cleanedPath := wrapper.CleanPath(tc.path)
+				file, ok := wrapper.FS[cleanedPath]
+				require.True(t, ok, "file %v not found in test fs", cleanedPath)
+				require.Equal(t, tc.expectedMode, file.Mode.Perm())
+			}
+		})
+	}
+}
+
+func TestFSTestOSWrapper_MkdirAll_MatchesReal(t *testing.T) {
+	tests := []struct {
+		name  string
+		setup matchesRealSetup
+		path  string // path to MkdirAll
+	}{
+		{
+			name: "Create new nested directory",
+			path: filepath.Join("a", "b", "c"),
+		},
+		{
+			name: "Create in existing subdirectory",
+			setup: matchesRealSetup{unittestSetup{
+				initialDirs: []string{"a"},
+			}},
+			path: filepath.Join("a", "b"),
+		},
+		{
+			name: "Path already exists as directory",
+			setup: matchesRealSetup{unittestSetup{
+				initialDirs: []string{filepath.Join("a", "b", "c")},
+			}},
+			path: filepath.Join("a", "b", "c"),
+		},
+		{
+			name: "Part of path is a file",
+			setup: matchesRealSetup{unittestSetup{
+				initialFiles: map[string]string{filepath.Join("a", "b"): "i am a file"},
+				initialDirs:  []string{"a"},
+			}},
+			path: filepath.Join("a", "b", "c"),
+		},
+		{
+			name: "Destination is a file",
+			setup: matchesRealSetup{unittestSetup{
+				initialFiles: map[string]string{filepath.Join("a", "b"): "i am a file"},
+				initialDirs:  []string{"a"},
+			}},
+			path: filepath.Join("a", "b"),
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			realRoot, realFS, testFS := tc.setup.setup(t)
+			defer os.RemoveAll(realRoot)
+
+			// Execute
+			realErr := realFS.MkdirAll(filepath.Join(realRoot, tc.path), 0755)
+			testErr := testFS.MkdirAll(tc.path, 0755)
+
+			requireErrorsMatch(t, realErr, testErr)
+			if realErr == nil {
+				requireFileSystemsMatch(t, realRoot, testFS)
+			}
+		})
+	}
+}
+
 func TestFSTestOSWrapper_WriteFile(t *testing.T) {
 	root := getTestRoot()
 	tests := []struct {