[tint][fuzz] Add testing for gatherWgslFiles
Adding this test took me slightly down the rabbit hole, since I wanted
to simply check if a dir is empty without needing to walk the
directory structure, which should be doable using os.ReadDir. This led
me to discovering that our OS wrapper did not implement ReadDir(),
which inturn led me to discovering afero's implementation of ReadDir
is not API compatible with os's, it actually implements the older
`Readdir()` (yes with a lowercase d) from File.
So I have added `Readdir` to the wrapper API to match closer the
behaviour that is currently going on/what afero acutally
implements. And added related testing and updated existing tests I was
walking the directory struct manually.
Change-Id: I3ca3087db36445ec9c27c95ba361f94a94d4f384
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/255594
Commit-Queue: Ryan Harrison <rharrison@chromium.org>
Reviewed-by: Brian Sheedy <bsheedy@google.com>
diff --git a/tools/src/cmd/fuzz/main_test.go b/tools/src/cmd/fuzz/main_test.go
new file mode 100644
index 0000000..5740035
--- /dev/null
+++ b/tools/src/cmd/fuzz/main_test.go
@@ -0,0 +1,161 @@
+// Copyright 2025 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+// list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation
+// and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package main
+
+import (
+ "testing"
+
+ "dawn.googlesource.com/dawn/tools/src/fileutils"
+ "dawn.googlesource.com/dawn/tools/src/oswrapper"
+ "github.com/stretchr/testify/require"
+)
+
+func TestGatherWgslFiles(t *testing.T) {
+ tests := []struct {
+ name string
+ inputsDir string
+ outDir string
+ setupFS func(t *testing.T, fs oswrapper.MemMapOSWrapper)
+ wantErr bool
+ verify func(t *testing.T, fs oswrapper.MemMapOSWrapper)
+ }{
+ {
+ name: "Basic copy",
+ inputsDir: "/in",
+ outDir: "/out",
+ setupFS: func(t *testing.T, fs oswrapper.MemMapOSWrapper) {
+ require.NoError(t, fs.MkdirAll("/in/subdir", 0777))
+ require.NoError(t, fs.MkdirAll("/out", 0777))
+ require.NoError(t, fs.WriteFile("/in/a.wgsl", []byte("shader a"), 0666))
+ require.NoError(t, fs.WriteFile("/in/b.txt", []byte("not a shader"), 0666))
+ require.NoError(t, fs.WriteFile("/in/c.expected.wgsl", []byte("should be ignored"), 0666))
+ require.NoError(t, fs.WriteFile("/in/subdir/d.wgsl", []byte("shader d"), 0666))
+ },
+ wantErr: false,
+ verify: func(t *testing.T, fs oswrapper.MemMapOSWrapper) {
+ // Check that expected files were created with correct content
+ contentA, err := fs.ReadFile("/out/a.wgsl")
+ require.NoError(t, err)
+ require.Equal(t, "shader a", string(contentA))
+
+ contentD, err := fs.ReadFile("/out/subdir_d.wgsl")
+ require.NoError(t, err)
+ require.Equal(t, "shader d", string(contentD))
+
+ // Check that other files were not copied
+ _, err = fs.Stat("/out/b.txt")
+ require.Error(t, err, "b.txt should not have been copied")
+
+ _, err = fs.Stat("/out/c.expected.wgsl")
+ require.Error(t, err, "c.expected.wgsl should not have been copied")
+ },
+ },
+ {
+ name: "Complex subdirectories",
+ inputsDir: "/in",
+ outDir: "/out",
+ setupFS: func(t *testing.T, fs oswrapper.MemMapOSWrapper) {
+ require.NoError(t, fs.MkdirAll("/in/a/b", 0777))
+ require.NoError(t, fs.MkdirAll("/out", 0777))
+ require.NoError(t, fs.WriteFile("/in/a/b/c.wgsl", []byte("shader c"), 0666))
+ },
+ wantErr: false,
+ verify: func(t *testing.T, fs oswrapper.MemMapOSWrapper) {
+ content, err := fs.ReadFile("/out/a_b_c.wgsl")
+ require.NoError(t, err)
+ require.Equal(t, "shader c", string(content))
+ },
+ },
+ {
+ name: "Empty input directory",
+ inputsDir: "/in",
+ outDir: "/out",
+ setupFS: func(t *testing.T, fs oswrapper.MemMapOSWrapper) {
+ require.NoError(t, fs.MkdirAll("/in", 0777))
+ require.NoError(t, fs.MkdirAll("/out", 0777))
+ },
+ wantErr: false,
+ verify: func(t *testing.T, fs oswrapper.MemMapOSWrapper) {
+ isEmpty, err := fileutils.IsEmptyDir("/out", fs)
+ require.NoError(t, err)
+ require.True(t, isEmpty)
+ },
+ },
+ {
+ name: "Output directory does not exist",
+ inputsDir: "/in",
+ outDir: "/out",
+ setupFS: func(t *testing.T, fs oswrapper.MemMapOSWrapper) {
+ require.NoError(t, fs.MkdirAll("/in", 0777))
+ require.NoError(t, fs.WriteFile("/in/a.wgsl", []byte("shader a"), 0666))
+ },
+ wantErr: false,
+ verify: func(t *testing.T, fs oswrapper.MemMapOSWrapper) {
+ // Check that output directory was created
+ info, err := fs.Stat("/out")
+ require.NoError(t, err)
+ require.True(t, info.IsDir())
+
+ // Check that file was copied
+ content, err := fs.ReadFile("/out/a.wgsl")
+ require.NoError(t, err)
+ require.Equal(t, "shader a", string(content))
+ },
+ },
+ {
+ name: "Input directory does not exist",
+ inputsDir: "/nonexistent",
+ outDir: "/out",
+ setupFS: func(t *testing.T, fs oswrapper.MemMapOSWrapper) {
+ require.NoError(t, fs.MkdirAll("/out", 0777))
+ },
+ wantErr: true,
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ wrapper := oswrapper.CreateMemMapOSWrapper()
+ if tc.setupFS != nil {
+ tc.setupFS(t, wrapper)
+ }
+
+ err := gatherWgslFiles(tc.inputsDir, tc.outDir, wrapper)
+
+ if tc.wantErr {
+ require.Error(t, err)
+ } else {
+ require.NoError(t, err)
+ }
+
+ if tc.verify != nil {
+ tc.verify(t, wrapper)
+ }
+ })
+ }
+}
diff --git a/tools/src/fileutils/copy_test.go b/tools/src/fileutils/copy_test.go
index f05f644..3a454a2 100644
--- a/tools/src/fileutils/copy_test.go
+++ b/tools/src/fileutils/copy_test.go
@@ -28,7 +28,6 @@
package fileutils_test
import (
- "os"
"path/filepath"
"testing"
@@ -229,18 +228,9 @@
},
wantErr: false,
verify: func(t *testing.T, fs oswrapper.MemMapOSWrapper) {
- require.True(t, fileutils.IsDir("/dst", fs))
- // Check it's empty by walking the directory.
- var fileCount int
- err := fs.Walk("/dst", func(path string, info os.FileInfo, err error) error {
- require.NoError(t, err)
- if path != "/dst" {
- fileCount++
- }
- return nil
- })
+ isEmpty, err := fileutils.IsEmptyDir("/dst", fs)
require.NoError(t, err)
- require.Zero(t, fileCount, "directory should be empty")
+ require.True(t, isEmpty)
},
},
{
diff --git a/tools/src/fileutils/paths.go b/tools/src/fileutils/paths.go
index e0abd1d..d6a103a 100644
--- a/tools/src/fileutils/paths.go
+++ b/tools/src/fileutils/paths.go
@@ -80,7 +80,7 @@
}
// ExpandHome returns the string with all occurrences of '~' replaced with the
-// user's home directory. The the user's home directory cannot be found, then
+// user's home directory. If the user's home directory cannot be found, then
// the input string is returned.
func ExpandHome(path string, environProvider oswrapper.EnvironProvider) string {
if strings.ContainsRune(path, '~') {
@@ -178,3 +178,25 @@
}
return common
}
+
+// IsEmptyDir returns true if the directory at 'dir' contains no files or
+// subdirectories. Returns an error if the path does not exist or is not a
+// directory.
+func IsEmptyDir(dir string, fsReader oswrapper.FilesystemReader) (bool, error) {
+ // First, check if the path exists and is a directory.
+ info, err := fsReader.Stat(dir)
+ if err != nil {
+ return false, fmt.Errorf("failed to stat '%s': %w", dir, err)
+ }
+ if !info.IsDir() {
+ return false, fmt.Errorf("path is not a directory: %s", dir)
+ }
+
+ // Now, read the directory's contents.
+ entries, err := fsReader.Readdir(dir)
+ if err != nil {
+ return false, fmt.Errorf("failed to read directory '%s': %w", dir, err)
+ }
+
+ return len(entries) == 0, nil
+}
diff --git a/tools/src/fileutils/paths_test.go b/tools/src/fileutils/paths_test.go
index f3df804..0070969 100644
--- a/tools/src/fileutils/paths_test.go
+++ b/tools/src/fileutils/paths_test.go
@@ -282,3 +282,74 @@
})
}
}
+
+func TestIsEmptyDir(t *testing.T) {
+ tests := []struct {
+ name string
+ path string
+ setupFS func(fs oswrapper.MemMapOSWrapper)
+ want bool
+ wantErr bool
+ }{
+ {
+ name: "Path does not exist",
+ path: "/nonexistent",
+ setupFS: nil,
+ wantErr: true,
+ },
+ {
+ name: "Path is a file",
+ path: "/myfile.txt",
+ setupFS: func(fs oswrapper.MemMapOSWrapper) {
+ require.NoError(t, fs.WriteFile("/myfile.txt", []byte("content"), 0666))
+ },
+ wantErr: true,
+ },
+ {
+ name: "Directory is empty",
+ path: "/mydir",
+ setupFS: func(fs oswrapper.MemMapOSWrapper) {
+ require.NoError(t, fs.MkdirAll("/mydir", 0777))
+ },
+ want: true,
+ wantErr: false,
+ },
+ {
+ name: "Directory with a file",
+ path: "/mydir",
+ setupFS: func(fs oswrapper.MemMapOSWrapper) {
+ require.NoError(t, fs.MkdirAll("/mydir", 0777))
+ require.NoError(t, fs.WriteFile(filepath.Join("/mydir", "file.txt"), []byte("content"), 0666))
+ },
+ want: false,
+ wantErr: false,
+ },
+ {
+ name: "Directory with a subdirectory",
+ path: "/mydir",
+ setupFS: func(fs oswrapper.MemMapOSWrapper) {
+ require.NoError(t, fs.MkdirAll(filepath.Join("/mydir", "subdir"), 0777))
+ },
+ want: false,
+ wantErr: false,
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ wrapper := oswrapper.CreateMemMapOSWrapper()
+ if tc.setupFS != nil {
+ tc.setupFS(wrapper)
+ }
+
+ got, err := fileutils.IsEmptyDir(tc.path, wrapper)
+
+ if tc.wantErr {
+ require.Error(t, err)
+ } else {
+ require.NoError(t, err)
+ require.Equal(t, tc.want, got)
+ }
+ })
+ }
+}
diff --git a/tools/src/oswrapper/memmaposwrapper.go b/tools/src/oswrapper/memmaposwrapper.go
index 69c7593..340210c 100644
--- a/tools/src/oswrapper/memmaposwrapper.go
+++ b/tools/src/oswrapper/memmaposwrapper.go
@@ -122,6 +122,19 @@
return afero.ReadFile(m.fs, name)
}
+func (m MemMapFilesystemReader) Readdir(name string) ([]os.FileInfo, error) {
+ // afero does technically implement Readdir behaviour as afero.ReadDir, but
+ // that its interface does not match os.ReadDir, so that may change in the
+ // future, so implementing this here using only interfaces that match os.*
+ // behaviour.
+ f, err := m.fs.Open(name)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+ return f.Readdir(-1)
+}
+
func (m MemMapFilesystemReader) Stat(name string) (os.FileInfo, error) {
// KI with afero, https://github.com/spf13/afero/issues/522
if name == "" {
diff --git a/tools/src/oswrapper/memmaposwrapper_test.go b/tools/src/oswrapper/memmaposwrapper_test.go
index a97baa5..c69f3e6 100644
--- a/tools/src/oswrapper/memmaposwrapper_test.go
+++ b/tools/src/oswrapper/memmaposwrapper_test.go
@@ -32,6 +32,7 @@
"io/fs"
"os"
"regexp"
+ "sort"
"testing"
"time"
@@ -223,6 +224,47 @@
require.Equal(t, []byte("asdf"), contents)
}
+func TestReaddir_NonExistent(t *testing.T) {
+ wrapper := CreateMemMapOSWrapper()
+ _, err := wrapper.Readdir("/nonexistent")
+ require.Error(t, err)
+ require.ErrorContains(t, err, "open /nonexistent: file does not exist")
+}
+
+func TestReaddir_PathIsFile(t *testing.T) {
+ wrapper := CreateMemMapOSWrapper()
+ require.NoError(t, wrapper.WriteFile("/myfile", []byte("content"), 0644))
+ _, err := wrapper.Readdir("/myfile")
+ require.Error(t, err)
+ require.ErrorContains(t, err, "not a dir")
+}
+
+func TestReaddir_EmptyDir(t *testing.T) {
+ wrapper := CreateMemMapOSWrapper()
+ require.NoError(t, wrapper.Mkdir("/mydir", 0755))
+ infos, err := wrapper.Readdir("/mydir")
+ require.NoError(t, err)
+ require.Empty(t, infos)
+}
+
+func TestReaddir_MixedContent(t *testing.T) {
+ wrapper := CreateMemMapOSWrapper()
+ require.NoError(t, wrapper.MkdirAll("/mydir/b_subdir", 0755))
+ require.NoError(t, wrapper.WriteFile("/mydir/z_file.txt", nil, 0644))
+ require.NoError(t, wrapper.WriteFile("/mydir/a_file.txt", nil, 0644))
+
+ infos, err := wrapper.Readdir("/mydir")
+ require.NoError(t, err)
+
+ gotNames := []string{}
+ for _, info := range infos {
+ gotNames = append(gotNames, info.Name())
+ }
+ sort.Strings(gotNames)
+ expectedNames := []string{"a_file.txt", "b_subdir", "z_file.txt"}
+ require.Equal(t, expectedNames, gotNames, "directory entries do not match")
+}
+
func TestMkdir_Exists(t *testing.T) {
wrapper := CreateMemMapOSWrapper()
err := wrapper.Mkdir("/parent", 0o700)
diff --git a/tools/src/oswrapper/oswrapper.go b/tools/src/oswrapper/oswrapper.go
index 46f442a..cef545f 100644
--- a/tools/src/oswrapper/oswrapper.go
+++ b/tools/src/oswrapper/oswrapper.go
@@ -56,10 +56,18 @@
FilesystemWriter
}
+// Note: afero's implementation of ReadDir/Readdir is somewhat non-compliant.
+// It's ReadDir doesn't implement the os.ReadDir interface, but instead is a
+// wrapper that calls its implementations of File.Readdir which has a subtly
+// different interface, i.e. []DirEntry vs []FileInfo.
+// So FilesystemReader has a Readdir that returns []FileInfo, and does not have
+// a ReadDir that returns []DirEntry
+
// FilesystemReader is a wrapper around the read-related filesystem os functions.
type FilesystemReader interface {
Open(name string) (afero.File, error)
OpenFile(name string, flag int, perm os.FileMode) (afero.File, error)
+ Readdir(name string) ([]os.FileInfo, error) // See note above for why Readdir and not ReadDir
ReadFile(name string) ([]byte, error)
Stat(name string) (os.FileInfo, error)
Walk(root string, fn filepath.WalkFunc) error
diff --git a/tools/src/oswrapper/realoswrapper.go b/tools/src/oswrapper/realoswrapper.go
index 9794488..6d6d733 100644
--- a/tools/src/oswrapper/realoswrapper.go
+++ b/tools/src/oswrapper/realoswrapper.go
@@ -29,10 +29,9 @@
package oswrapper
import (
+ "github.com/spf13/afero"
"os"
"path/filepath"
-
- "github.com/spf13/afero"
)
type RealOSWrapper struct {
@@ -104,6 +103,15 @@
return os.ReadFile(name)
}
+func (rfsr RealFilesystemReader) Readdir(name string) ([]os.FileInfo, error) {
+ f, err := rfsr.Open(name)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+ return f.Readdir(-1)
+}
+
func (RealFilesystemReader) Stat(name string) (os.FileInfo, error) {
return os.Stat(name)
}