[infra] Add dependency injection support to more fileutils API Adds the ability to pass in a oswrapper.* to IsDir(), IsExe(), IsFile(), CopyFile(), and CopyDir() to allow better testing of code that uses these functions. Testing is added for these functions. CopyFile & CopyDir were significantly rewritten due to issues found when testing. Updates all of the callsites to use the new API. Some places where I was making these changes I also changed calls to os.* to be to oswrapper.*. Bug: 344014313 Change-Id: I418672fdbcb1f5492d7fd796ec70e9dd2f3a7cc9 Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/255514 Commit-Queue: Ryan Harrison <rharrison@chromium.org> Reviewed-by: Brian Sheedy <bsheedy@google.com>
diff --git a/tools/src/cmd/cts/roll/roll.go b/tools/src/cmd/cts/roll/roll.go index d319908..b5d54d9 100644 --- a/tools/src/cmd/cts/roll/roll.go +++ b/tools/src/cmd/cts/roll/roll.go
@@ -810,7 +810,7 @@ ctx context.Context, fsReader oswrapper.FilesystemReader) (string, error) { tscPath := filepath.Join(r.ctsDir, "node_modules/.bin/tsc") - if !fileutils.IsExe(tscPath) { + if !fileutils.IsExe(tscPath, fsReader) { return "", fmt.Errorf("tsc not found at '%v'", tscPath) }
diff --git a/tools/src/cmd/fuzz/main.go b/tools/src/cmd/fuzz/main.go index a96cca7..f7b64b5 100644 --- a/tools/src/cmd/fuzz/main.go +++ b/tools/src/cmd/fuzz/main.go
@@ -164,24 +164,24 @@ dictionary string // path to dictionary to use for tint_wgsl_fuzzer } -func run(c *cmdConfig, osWrapper oswrapper.OSWrapper) error { - if !fileutils.IsDir(c.build) { +func run(c *cmdConfig, fsReaderWriter oswrapper.FilesystemReaderWriter) error { + if !fileutils.IsDir(c.build, fsReaderWriter) { return fmt.Errorf("build directory '%v' does not exist", c.build) } // Verify / create the output directory if c.out == "" || c.out == "<tmp>" { - if tmp, err := osWrapper.MkdirTemp("", "tint_fuzz"); err == nil { - defer func(osWrapper oswrapper.OSWrapper, path string) { - _ = osWrapper.RemoveAll(path) - }(osWrapper, tmp) + if tmp, err := fsReaderWriter.MkdirTemp("", "tint_fuzz"); err == nil { + defer func(fsWriter oswrapper.FilesystemWriter, path string) { + _ = fsWriter.RemoveAll(path) + }(fsReaderWriter, tmp) c.out = tmp } else { return err } } - if !fileutils.IsDir(c.out) { + if !fileutils.IsDir(c.out, fsReaderWriter) { return fmt.Errorf("output directory '%v' does not exist", c.out) } @@ -190,18 +190,18 @@ if c.fuzzMode == FuzzModeIr && (c.cmdMode == TaskModeRun || c.cmdMode == TaskModeCheck) { // The default input files are .wgsl files and tint_ir_fuzzer runs on .tirb files, so need // to convert them before running/checking - if c.inputs == defaultWgslCorpusDir(osWrapper) { + if c.inputs == defaultWgslCorpusDir(fsReaderWriter) { origOut := c.out - tmp, err := osWrapper.MkdirTemp("", "ir_corpus") + tmp, err := fsReaderWriter.MkdirTemp("", "ir_corpus") if err != nil { return err } - defer func(osWrapper oswrapper.OSWrapper, path string) { - _ = osWrapper.RemoveAll(path) - }(osWrapper, tmp) + defer func(fsWriter oswrapper.FilesystemWriter, path string) { + _ = fsWriter.RemoveAll(path) + }(fsReaderWriter, tmp) c.out = tmp - t, err := generateTaskConfig(TaskModeGenerate, c, osWrapper) + t, err := generateTaskConfig(TaskModeGenerate, c, fsReaderWriter) if err != nil { return err } @@ -212,7 +212,7 @@ } } - t, err := generateTaskConfig(c.cmdMode, c, osWrapper) + t, err := generateTaskConfig(c.cmdMode, c, fsReaderWriter) if err != nil { return err } @@ -222,11 +222,11 @@ var err error switch t.taskMode { case TaskModeRun: - err = runFuzzer(t, osWrapper) + err = runFuzzer(t) case TaskModeCheck: - err = checkFuzzer(t, osWrapper) + err = checkFuzzer(t, fsReaderWriter) case TaskModeGenerate: - err = runCorpusGenerator(t, osWrapper) + err = runCorpusGenerator(t, fsReaderWriter) default: err = fmt.Errorf("unknown task mode %d", t.taskMode) } @@ -238,7 +238,7 @@ } // generateTaskConfig produces a taskConfig based off the supplied cmdConfig and specified TaskMode. -func generateTaskConfig(tm TaskMode, c *cmdConfig, osWrapper oswrapper.OSWrapper) (*taskConfig, error) { +func generateTaskConfig(tm TaskMode, c *cmdConfig, fsReader oswrapper.FilesystemReader) (*taskConfig, error) { t := taskConfig{ cmdConfig: *c, taskMode: tm, @@ -272,18 +272,18 @@ for _, config := range dependencies { switch { case filepath.Ext(config.name) == ".py": - *config.path = filepath.Join(filepath.Join(fileutils.DawnRoot(osWrapper), "src", "tint", "cmd", "fuzz"), config.name) - if !fileutils.IsFile(*config.path) { + *config.path = filepath.Join(filepath.Join(fileutils.DawnRoot(fsReader), "src", "tint", "cmd", "fuzz"), config.name) + if !fileutils.IsFile(*config.path, fsReader) { return nil, fmt.Errorf("script '%v' not found at '%v'", config.name, *config.path) } case filepath.Ext(config.name) == ".txt": - *config.path = filepath.Join(filepath.Join(fileutils.DawnRoot(osWrapper), "src", "tint", "cmd", "fuzz", "wgsl"), config.name) - if !fileutils.IsFile(*config.path) { + *config.path = filepath.Join(filepath.Join(fileutils.DawnRoot(fsReader), "src", "tint", "cmd", "fuzz", "wgsl"), config.name) + if !fileutils.IsFile(*config.path, fsReader) { return nil, fmt.Errorf("resource '%v' not found at '%v'", config.name, *config.path) } default: *config.path = filepath.Join(t.build, config.name+fileutils.ExeExt) - if !fileutils.IsExe(*config.path) { + if !fileutils.IsExe(*config.path, fsReader) { return nil, fmt.Errorf("binary '%v' not found at '%v'", config.name, *config.path) } } @@ -294,14 +294,14 @@ // checkFuzzer runs the fuzzer against all the test files the inputs directory, // ensuring that the fuzzers do not error for the given file. -func checkFuzzer(t *taskConfig, osWrapper oswrapper.OSWrapper) error { +func checkFuzzer(t *taskConfig, fsReader oswrapper.FilesystemReader) error { var files []string var err error switch t.fuzzMode { case FuzzModeIr: - files, err = glob.Glob(filepath.Join(t.inputs, "**.tirb"), osWrapper) + files, err = glob.Glob(filepath.Join(t.inputs, "**.tirb"), fsReader) case FuzzModeWgsl: - files, err = glob.Glob(filepath.Join(t.inputs, "**.wgsl"), osWrapper) + files, err = glob.Glob(filepath.Join(t.inputs, "**.wgsl"), fsReader) default: err = fmt.Errorf("unknown fuzzer mode %d", t.fuzzMode) } @@ -355,7 +355,7 @@ // The fuzzer will use t.inputs as the seed directory. // New cases are written to t.out. // Blocks until a fuzzer errors, or the process is interrupted. -func runFuzzer(t *taskConfig, fsReader oswrapper.FilesystemReader) error { +func runFuzzer(t *taskConfig) error { ctx := utils.CancelOnInterruptContext(context.Background()) ctx, cancel := context.WithCancel(ctx) defer cancel() @@ -423,35 +423,35 @@ // runCorpusGenerator converts a set of input test files into a fuzzer corpus // The generator will use t.inputs as the source directory. // The corpus will be written to t.out. -func runCorpusGenerator(t *taskConfig, osWrapper oswrapper.OSWrapper) error { +func runCorpusGenerator(t *taskConfig, fsReaderWriter oswrapper.FilesystemReaderWriter) error { switch t.fuzzMode { case FuzzModeWgsl: - return runCorpusGeneratorWgsl(t, osWrapper) + return runCorpusGeneratorWgsl(t, fsReaderWriter) case FuzzModeIr: - return runCorpusGeneratorIr(t, osWrapper) + return runCorpusGeneratorIr(t, fsReaderWriter) default: return fmt.Errorf("unknown fuzzer mode %d", t.fuzzMode) } } // runCorpusGeneratorWgsl converts a set of input test .wgsl files into a WGSL fuzzer corpus. -func runCorpusGeneratorWgsl(t *taskConfig, osWrapper oswrapper.OSWrapper) error { - return gatherWgslFiles(t.inputs, t.out, osWrapper) +func runCorpusGeneratorWgsl(t *taskConfig, fsReaderWriter oswrapper.FilesystemReaderWriter) error { + return gatherWgslFiles(t.inputs, t.out, fsReaderWriter) } // runCorpusGeneratorWgsl converts a set of input test .wgsl files into an IR fuzzer corpus // Forks out to an external binary, t.assembler, to perform the operation. -func runCorpusGeneratorIr(t *taskConfig, osWrapper oswrapper.OSWrapper) error { - tmp, err := osWrapper.MkdirTemp("", "wgsl_corpus") +func runCorpusGeneratorIr(t *taskConfig, fsReaderWriter oswrapper.FilesystemReaderWriter) error { + tmp, err := fsReaderWriter.MkdirTemp("", "wgsl_corpus") if err == nil { - defer func(osWrapper oswrapper.OSWrapper, path string) { - _ = osWrapper.RemoveAll(path) - }(osWrapper, tmp) + defer func(fsWriter oswrapper.FilesystemWriter, path string) { + _ = fsWriter.RemoveAll(path) + }(fsReaderWriter, tmp) } else { return err } - err = gatherWgslFiles(t.inputs, tmp, osWrapper) + err = gatherWgslFiles(t.inputs, tmp, fsReaderWriter) if err != nil { return err } @@ -486,9 +486,9 @@ // gatherWgslFiles copies all the .wgsl files in a directory structure over to a flat directory // structure, via replacing the path separators for the origins with underscores in the destination // file names. It also filters out any '*.expected.*' files -func gatherWgslFiles(inputs string, out string, osWrapper oswrapper.OSWrapper) error { +func gatherWgslFiles(inputs string, out string, fsReaderWriter oswrapper.FilesystemReaderWriter) error { fmt.Println("gathering and filtering .wgsl files") - files, err := glob.Glob(filepath.Join(inputs, "**.wgsl"), osWrapper) + files, err := glob.Glob(filepath.Join(inputs, "**.wgsl"), fsReaderWriter) if err != nil { return err } @@ -506,7 +506,7 @@ } for src, dest := range mapping { - if err := fileutils.CopyFile(filepath.Join(out, dest), src); err != nil { + if err := fileutils.CopyFile(filepath.Join(out, dest), src, fsReaderWriter); err != nil { return err } }
diff --git a/tools/src/cmd/gen/common/clang_format.go b/tools/src/cmd/gen/common/clang_format.go index f21f9bc..ba717e8 100644 --- a/tools/src/cmd/gen/common/clang_format.go +++ b/tools/src/cmd/gen/common/clang_format.go
@@ -75,7 +75,7 @@ case "windows": path = filepath.Join(dawnRoot, "buildtools/win/clang-format.exe") } - if fileutils.IsExe(path) { + if fileutils.IsExe(path, fsReader) { return path, nil } var err error
diff --git a/tools/src/cmd/node/main.go b/tools/src/cmd/node/main.go index 6ebe65c..5443b34 100644 --- a/tools/src/cmd/node/main.go +++ b/tools/src/cmd/node/main.go
@@ -79,14 +79,14 @@ debugger = "lldb" } - if err := run(opts.BinDir, nodePath, nodeFlags, flag.Args(), debugger); err != nil { + if err := run(opts.BinDir, nodePath, nodeFlags, flag.Args(), debugger, wrapper); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } // run starts the -func run(binPath, nodePath string, flags node.Flags, args []string, debugger string) error { +func run(binPath, nodePath string, flags node.Flags, args []string, debugger string, fsReader oswrapper.FilesystemReader) error { if len(args) == 0 { return fmt.Errorf("missing path to .js file") } @@ -102,7 +102,7 @@ } for _, file := range []string{"cts.js", "dawn.node"} { - if !fileutils.IsFile(filepath.Join(binPath, file)) { + if !fileutils.IsFile(filepath.Join(binPath, file), fsReader) { return fmt.Errorf("'%v' does not contain '%v'", binPath, file) } }
diff --git a/tools/src/cmd/run-cts/chrome/cmd.go b/tools/src/cmd/run-cts/chrome/cmd.go index 5a6bd53..ee34531 100644 --- a/tools/src/cmd/run-cts/chrome/cmd.go +++ b/tools/src/cmd/run-cts/chrome/cmd.go
@@ -80,7 +80,7 @@ // TODO(crbug.com/416755658): Add unittest coverage once there is a way to fake // the Chrome instance. func (c *cmd) Run(ctx context.Context, cfg common.Config) error { - state, err := c.flags.Process() + state, err := c.flags.Process(cfg.OsWrapper) if err != nil { return err } @@ -96,10 +96,10 @@ return fmt.Errorf("only a single query can be provided") } - if err := c.state.CTS.Node.BuildIfRequired(c.flags.Verbose); err != nil { + if err := c.state.CTS.Node.BuildIfRequired(c.flags.Verbose, cfg.OsWrapper); err != nil { return err } - if err := c.state.CTS.Standalone.BuildIfRequired(c.flags.Verbose); err != nil { + if err := c.state.CTS.Standalone.BuildIfRequired(c.flags.Verbose, cfg.OsWrapper); err != nil { return err } @@ -206,9 +206,9 @@ handler.HandleFunc("/test_page.html", serveFile("webgpu-cts/test_page.html", fsReader)) handler.HandleFunc("/test_runner.js", serveFile("webgpu-cts/test_runner.js", fsReader)) handler.HandleFunc("/third_party/webgpu-cts/resources/", - serveDir("/third_party/webgpu-cts/resources/", c.flags.CTS+"/out/resources/")) + serveDir("/third_party/webgpu-cts/resources/", c.flags.CTS+"/out/resources/", fsReader)) handler.HandleFunc("/third_party/webgpu-cts/src/", - serveDir("/third_party/webgpu-cts/src/", c.flags.CTS+"/out/")) + serveDir("/third_party/webgpu-cts/src/", c.flags.CTS+"/out/", fsReader)) handler.HandleFunc("/", websocket.Handler(func(ws *websocket.Conn) { go func() { d := json.NewDecoder(ws) @@ -337,17 +337,17 @@ dawnRoot := fileutils.DawnRoot(fsReader) return func(w http.ResponseWriter, r *http.Request) { fullPath := filepath.Join(dawnRoot, relPath) - if !fileutils.IsFile(fullPath) { + if !fileutils.IsFile(fullPath, fsReader) { log.Printf("'%v' file does not exist", fullPath) } http.ServeFile(w, r, filepath.Join(dawnRoot, relPath)) } } -func serveDir(remote, local string) func(http.ResponseWriter, *http.Request) { +func serveDir(remote, local string, fsReader oswrapper.FilesystemReader) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { fullPath := filepath.Join(local, strings.TrimPrefix(r.URL.Path, remote)) - if !fileutils.IsFile(fullPath) { + if !fileutils.IsFile(fullPath, fsReader) { log.Printf("'%v' file does not exist", fullPath) } http.ServeFile(w, r, fullPath)
diff --git a/tools/src/cmd/run-cts/common/builder.go b/tools/src/cmd/run-cts/common/builder.go index 14bc5b9..b58b8ee 100644 --- a/tools/src/cmd/run-cts/common/builder.go +++ b/tools/src/cmd/run-cts/common/builder.go
@@ -28,9 +28,9 @@ package common import ( + "dawn.googlesource.com/dawn/tools/src/oswrapper" "encoding/json" "fmt" - "os" "os/exec" "path/filepath" "time" @@ -46,7 +46,7 @@ } // BuildIfRequired calls Build() if the CTS sources have been modified since the last build. -func (b *Builder) BuildIfRequired(verbose bool) error { +func (b *Builder) BuildIfRequired(verbose bool, osWrapper oswrapper.OSWrapper) error { name := fmt.Sprintf("cts %v", b.Name) // Scan the CTS source to determine the most recent change to the CTS source @@ -61,17 +61,17 @@ cache := Cache{BuildTimestamp: map[string]time.Time{}} cachePath := "" - if home, err := os.UserHomeDir(); err == nil { + if home, err := osWrapper.UserHomeDir(); err == nil { cacheDir := filepath.Join(home, ".cache/webgpu") cachePath = filepath.Join(cacheDir, "run-cts.json") - os.MkdirAll(cacheDir, 0777) + osWrapper.MkdirAll(cacheDir, 0777) } needsRebuild := true if cachePath != "" { // consult the cache to see if we need to rebuild - if cacheFile, err := os.Open(cachePath); err == nil { + if cacheFile, err := osWrapper.Open(cachePath); err == nil { if err := json.NewDecoder(cacheFile).Decode(&cache); err == nil { - if fileutils.IsDir(b.Out) { + if fileutils.IsDir(b.Out, osWrapper) { needsRebuild = mostRecentSourceChange.After(cache.BuildTimestamp[b.Name]) } } @@ -84,14 +84,14 @@ } if needsRebuild { - if err := b.Build(verbose); err != nil { + if err := b.Build(verbose, osWrapper); err != nil { return fmt.Errorf("failed to build %v: %w", name, err) } } if cachePath != "" { // Update the cache timestamp - if cacheFile, err := os.Create(cachePath); err == nil { + if cacheFile, err := osWrapper.Create(cachePath); err == nil { cache.BuildTimestamp[b.Name] = mostRecentSourceChange json.NewEncoder(cacheFile).Encode(&cache) cacheFile.Close() @@ -104,7 +104,7 @@ // Build executes the necessary build commands to build the CTS, including // copying the cache files from gen to the out directory and compiling the // TypeScript files down to JavaScript. -func (b *Builder) Build(verbose bool) error { +func (b *Builder) Build(verbose bool, fsReaderWrapper oswrapper.FilesystemReaderWriter) error { if verbose { start := time.Now() fmt.Printf("Building CTS %v...\n", b.Name) @@ -113,11 +113,11 @@ }() } - if err := os.MkdirAll(b.Out, 0777); err != nil { + if err := fsReaderWrapper.MkdirAll(b.Out, 0777); err != nil { return err } - if !fileutils.IsExe(b.npx) { + if !fileutils.IsExe(b.npx, fsReaderWrapper) { return fmt.Errorf("cannot find npx at '%v'", b.npx) }
diff --git a/tools/src/cmd/run-cts/common/flags.go b/tools/src/cmd/run-cts/common/flags.go index caa5561..f0a9e81 100644 --- a/tools/src/cmd/run-cts/common/flags.go +++ b/tools/src/cmd/run-cts/common/flags.go
@@ -31,7 +31,6 @@ "flag" "fmt" "io" - "os" "os/exec" "path/filepath" "runtime" @@ -69,7 +68,7 @@ // Process processes the flags, returning a State. // Note: Ensure you call Close() on the returned State -func (f *Flags) Process() (*State, error) { +func (f *Flags) Process(fsReaderWriter oswrapper.FilesystemReaderWriter) (*State, error) { s := &State{ resultsPath: f.ResultsPath, } @@ -81,7 +80,7 @@ if f.CTS == "" { return nil, subcmd.InvalidCLA() } - if !fileutils.IsDir(f.CTS) { + if !fileutils.IsDir(f.CTS, fsReaderWriter) { return nil, fmt.Errorf("'%v' is not a directory", f.CTS) } absCTS, err := filepath.Abs(f.CTS) @@ -92,7 +91,7 @@ // Build the logger, if needed if f.Log != "" { - writer, err := os.Create(f.Log) + writer, err := fsReaderWriter.Create(f.Log) if err != nil { return nil, fmt.Errorf("failed to open log '%v': %w", f.Log, err) }
diff --git a/tools/src/cmd/run-cts/node/cmd.go b/tools/src/cmd/run-cts/node/cmd.go index d9b5757..4de3457 100644 --- a/tools/src/cmd/run-cts/node/cmd.go +++ b/tools/src/cmd/run-cts/node/cmd.go
@@ -126,12 +126,12 @@ return err } - if err := c.maybeInitCoverage(); err != nil { + if err := c.maybeInitCoverage(cfg.OsWrapper); err != nil { return err } if c.flags.build { - if err := c.state.CTS.Node.BuildIfRequired(c.flags.Verbose); err != nil { + if err := c.state.CTS.Node.BuildIfRequired(c.flags.Verbose, cfg.OsWrapper); err != nil { return err } } @@ -178,17 +178,17 @@ } // TODO(crbug.com/344014313): Add unittest coverage. -func (c *cmd) processFlags(fsReader oswrapper.FilesystemReader) error { +func (c *cmd) processFlags(fsReaderWriter oswrapper.FilesystemReaderWriter) error { // Check mandatory arguments if c.flags.bin == "" { return fmt.Errorf("-bin is not set. It defaults to <dawn>/out/active (%v) which does not exist", - filepath.Join(fileutils.DawnRoot(fsReader), "out/active")) + filepath.Join(fileutils.DawnRoot(fsReaderWriter), "out/active")) } - if !fileutils.IsDir(c.flags.bin) { + if !fileutils.IsDir(c.flags.bin, fsReaderWriter) { return fmt.Errorf("'%v' is not a directory", c.flags.bin) } for _, file := range []string{"cts.js", "dawn.node"} { - if !fileutils.IsFile(filepath.Join(c.flags.bin, file)) { + if !fileutils.IsFile(filepath.Join(c.flags.bin, file), fsReaderWriter) { return fmt.Errorf("'%v' does not contain '%v'", c.flags.bin, file) } } @@ -228,7 +228,7 @@ c.flags.Verbose = true } - state, err := c.flags.Process() + state, err := c.flags.Process(fsReaderWriter) if err != nil { return err } @@ -239,7 +239,7 @@ // TODO(crbug.com/416755658): Add unittest coverage when exec is handled via // dependency injection. -func (c *cmd) maybeInitCoverage() error { +func (c *cmd) maybeInitCoverage(fsReader oswrapper.FilesystemReader) error { if !c.flags.genCoverage && c.flags.coverageFile == "" { return nil } @@ -249,7 +249,7 @@ profdata = "" if runtime.GOOS == "darwin" { profdata = "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/llvm-profdata" - if !fileutils.IsExe(profdata) { + if !fileutils.IsExe(profdata, fsReader) { profdata = "" } } @@ -260,7 +260,7 @@ llvmCov := "" turboCov := filepath.Join(c.flags.bin, "turbo-cov"+fileutils.ExeExt) - if !fileutils.IsExe(turboCov) { + if !fileutils.IsExe(turboCov, fsReader) { turboCov = "" if path, err := exec.LookPath("llvm-cov"); err == nil { llvmCov = path
diff --git a/tools/src/cmd/tests/main.go b/tools/src/cmd/tests/main.go index b2aa927..4328861 100644 --- a/tools/src/cmd/tests/main.go +++ b/tools/src/cmd/tests/main.go
@@ -159,7 +159,7 @@ flag.Parse() // Check the executable can be found and actually is executable - if !fileutils.IsExe(tintPath) { + if !fileutils.IsExe(tintPath, fsReaderWriter) { fmt.Fprintln(os.Stderr, "tint executable not found, please specify with --tint") showUsage() } @@ -192,12 +192,12 @@ } switch { - case fileutils.IsDir(arg): + case fileutils.IsDir(arg, fsReaderWriter): // Argument is to a directory, expand out to N globs for _, glob := range directoryGlobs { globs = append(globs, path.Join(arg, glob)) } - case fileutils.IsFile(arg): + case fileutils.IsFile(arg, fsReaderWriter): // Argument is a file, append to absFiles absFiles = append(absFiles, arg) default: @@ -270,16 +270,16 @@ if *tool.path == "" { // Look first in the directory of the tint executable p, err := exec.LookPath(filepath.Join(filepath.Dir(tintPath), tool.name)) - if err == nil && fileutils.IsExe(p) { + if err == nil && fileutils.IsExe(p, fsReaderWriter) { *tool.path = p } else { // Look in PATH p, err := exec.LookPath(tool.name) - if err == nil && fileutils.IsExe(p) { + if err == nil && fileutils.IsExe(p, fsReaderWriter) { *tool.path = p } } - } else if !fileutils.IsExe(*tool.path) { + } else if !fileutils.IsExe(*tool.path, fsReaderWriter) { return fmt.Errorf("%v not found at '%v'", tool.name, *tool.path) }
diff --git a/tools/src/cmd/tintd/install/install.go b/tools/src/cmd/tintd/install/install.go index 541d566..e55d78f 100644 --- a/tools/src/cmd/tintd/install/install.go +++ b/tools/src/cmd/tintd/install/install.go
@@ -31,6 +31,7 @@ import ( "bytes" "context" + "dawn.googlesource.com/dawn/tools/src/oswrapper" "encoding/json" "flag" "fmt" @@ -74,12 +75,12 @@ // TODO(crbug.com/416755658): Add unittest coverage once exec is handled via // dependency injection. func (c Cmd) Run(ctx context.Context, cfg *common.Config) error { - pkgDir := c.findPackage() + pkgDir := c.findPackage(cfg.OsWrapper) if pkgDir == "" { return fmt.Errorf("could not find extension package directory at '%v'", c.flags.buildDir) } - if !fileutils.IsExe(c.flags.npmPath) { + if !fileutils.IsExe(c.flags.npmPath, cfg.OsWrapper) { return fmt.Errorf("could not find npm") } @@ -109,7 +110,7 @@ return fmt.Errorf("failed to obtain home directory: %w", err) } vscodeBaseExtsDir := filepath.Join(home, ".vscode", "extensions") - if !fileutils.IsDir(vscodeBaseExtsDir) { + if !fileutils.IsDir(vscodeBaseExtsDir, cfg.OsWrapper) { return fmt.Errorf("vscode extensions directory not found at '%v'", vscodeBaseExtsDir) } @@ -123,7 +124,7 @@ } } else { // Copy the build directory to vscode extensions directory - if err := fileutils.CopyDir(vscodeTintdDir, pkgDir); err != nil { + if err := fileutils.CopyDir(vscodeTintdDir, pkgDir, cfg.OsWrapper); err != nil { return fmt.Errorf("failed to copy '%v' to '%v': %w", pkgDir, vscodeTintdDir, err) } } @@ -133,7 +134,7 @@ // TODO(crbug.com/344014313): Add unittest coverage. // findPackage looks for and returns the tintd package directory. Returns an empty string if not found. -func (c Cmd) findPackage() string { +func (c Cmd) findPackage(fsReader oswrapper.FilesystemReader) string { searchPaths := []string{ filepath.Join(c.flags.buildDir, "gen/vscode"), c.flags.buildDir, @@ -143,7 +144,7 @@ nextDir: for _, dir := range searchPaths { for _, file := range files { - if !fileutils.IsFile(filepath.Join(dir, file)) { + if !fileutils.IsFile(filepath.Join(dir, file), fsReader) { continue nextDir } }
diff --git a/tools/src/fileutils/copy.go b/tools/src/fileutils/copy.go index 0170c4f..d314e16 100644 --- a/tools/src/fileutils/copy.go +++ b/tools/src/fileutils/copy.go
@@ -32,75 +32,102 @@ "io" "os" "path/filepath" + "strings" + + "dawn.googlesource.com/dawn/tools/src/oswrapper" ) -// CopyFile copies the file from 'src' to 'dst' replacing the existing file at 'dst' if it already -// exists. -func CopyFile(dst, src string) error { - if !IsFile(src) { - return fmt.Errorf("'%v' is not a file", src) - } - - dstDir := filepath.Dir(dst) - if !IsDir(dstDir) { - if err := os.MkdirAll(dstDir, 0777); err != nil { - return err - } - } - - s, err := os.Open(src) +// CopyFile copies the file from 'src' to 'dst', creating the destination directory +// if needed and overwriting the destination file if it already exists. +// It preserves the file mode from the source. +func CopyFile(dst, src string, fsReaderWriter oswrapper.FilesystemReaderWriter) error { + srcInfo, err := fsReaderWriter.Stat(src) if err != nil { - return err + return fmt.Errorf("cannot stat source '%v': %w", src, err) + } + if srcInfo.IsDir() { + return fmt.Errorf("source '%v' is a directory, not a file", src) + } + + s, err := fsReaderWriter.Open(src) + if err != nil { + return fmt.Errorf("failed to open source file '%v': %w", src, err) } defer s.Close() - info, err := s.Stat() - if err != nil { - return err + if err := fsReaderWriter.MkdirAll(filepath.Dir(dst), 0755); err != nil { + return fmt.Errorf("failed to create destination directory '%v': %w", filepath.Dir(dst), err) } - d, err := os.OpenFile(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666|info.Mode()&0777) + d, err := fsReaderWriter.OpenFile(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, srcInfo.Mode()) if err != nil { - return err + return fmt.Errorf("failed to open destination file '%v': %w", dst, err) } defer d.Close() - _, err = io.Copy(d, s) - return err + if _, err = io.Copy(d, s); err != nil { + return fmt.Errorf("failed to copy data from '%v' to '%v': %w", src, dst, err) + } + + return nil } -// CopyDir copies the directory and all its content from 'src' to 'dst' replacing the existing -// directory at 'dst' if it already exists. -func CopyDir(dst, src string) error { - if !IsDir(src) { - return fmt.Errorf("'%v' is not a directory", src) +// CopyDir recursively copies the content of the 'src' directory to 'dst'. +// If 'dst' exists, it will be completely overwritten with the content of 'src'. +// If 'dst' does not exist, it will be created. +func CopyDir(dst, src string, fsReaderWriter oswrapper.FilesystemReaderWriter) error { + srcInfo, err := fsReaderWriter.Stat(src) + if err != nil { + return fmt.Errorf("cannot stat source '%v': %w", src, err) } - if IsFile(dst) { - return fmt.Errorf("'%v' is a file", dst) + if !srcInfo.IsDir() { + return fmt.Errorf("source '%v' is not a directory", src) } - if IsDir(dst) { - if err := os.RemoveAll(dst); err != nil { - return err + var dstMode = srcInfo.Mode() + if dstInfo, err := fsReaderWriter.Stat(dst); err == nil { + if !dstInfo.IsDir() { + return fmt.Errorf("destination '%v' is a file, not a directory", dst) } + dstMode = dstInfo.Mode() // Preserve original dst permissions + } else if !os.IsNotExist(err) { // Some other error + return fmt.Errorf("cannot stat destination '%v': %w", dst, err) } - return filepath.Walk(src, func(path string, info os.FileInfo, err error) error { + // To prevent recursion, ensure dst is not a subdirectory of src. + cleanSrc := filepath.Clean(src) + cleanDst := filepath.Clean(dst) + if strings.HasPrefix(cleanDst, cleanSrc) && (cleanDst == cleanSrc || cleanDst[len(cleanSrc)] == filepath.Separator) { + return fmt.Errorf("cannot copy directory '%s' into itself '%s'", src, dst) + } + + if err := fsReaderWriter.RemoveAll(dst); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to remove destination directory '%v': %w", dst, err) + } + if err := fsReaderWriter.MkdirAll(dst, dstMode); err != nil { + return fmt.Errorf("failed to create destination directory '%v': %w", dst, err) + } + + return fsReaderWriter.Walk(src, func(path string, info os.FileInfo, err error) error { if err != nil { return err } - rel, err := filepath.Rel(src, path) + relPath, err := filepath.Rel(src, path) if err != nil { - return err + return fmt.Errorf("failed to calculate relative path for '%s': %w", path, err) } - if !info.IsDir() { - if err := CopyFile(filepath.Join(dst, rel), path); err != nil { - return err - } + if relPath == "." { // Skip the root directory itself. + return nil } - return nil + dstPath := filepath.Join(dst, relPath) + + if info.IsDir() { + return fsReaderWriter.MkdirAll(dstPath, info.Mode()) + } + + return CopyFile(dstPath, path, fsReaderWriter) }) }
diff --git a/tools/src/fileutils/copy_test.go b/tools/src/fileutils/copy_test.go new file mode 100644 index 0000000..f05f644 --- /dev/null +++ b/tools/src/fileutils/copy_test.go
@@ -0,0 +1,302 @@ +// 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 fileutils_test + +import ( + "os" + "path/filepath" + "testing" + + "dawn.googlesource.com/dawn/tools/src/fileutils" + "dawn.googlesource.com/dawn/tools/src/oswrapper" + "github.com/stretchr/testify/require" +) + +func TestCopyFile(t *testing.T) { + tests := []struct { + name string + srcPath string + dstPath string + setupFS func(t *testing.T, fs oswrapper.MemMapOSWrapper) + wantErr bool + verify func(t *testing.T, fs oswrapper.MemMapOSWrapper) + }{ + { + name: "Simple copy", + srcPath: "/src/file.txt", + dstPath: "/dst/file.txt", + setupFS: func(t *testing.T, fs oswrapper.MemMapOSWrapper) { + require.NoError(t, fs.MkdirAll("/src", 0777)) + require.NoError(t, fs.WriteFile("/src/file.txt", []byte("hello world"), 0666)) + require.NoError(t, fs.MkdirAll("/dst", 0777)) + }, + wantErr: false, + verify: func(t *testing.T, fs oswrapper.MemMapOSWrapper) { + content, err := fs.ReadFile("/dst/file.txt") + require.NoError(t, err) + require.Equal(t, "hello world", string(content)) + }, + }, + { + name: "Overwrite existing file", + srcPath: "/src/file.txt", + dstPath: "/dst/file.txt", + setupFS: func(t *testing.T, fs oswrapper.MemMapOSWrapper) { + require.NoError(t, fs.MkdirAll("/src", 0777)) + require.NoError(t, fs.WriteFile("/src/file.txt", []byte("new content"), 0666)) + require.NoError(t, fs.MkdirAll("/dst", 0777)) + require.NoError(t, fs.WriteFile("/dst/file.txt", []byte("old content"), 0666)) + }, + wantErr: false, + verify: func(t *testing.T, fs oswrapper.MemMapOSWrapper) { + content, err := fs.ReadFile("/dst/file.txt") + require.NoError(t, err) + require.Equal(t, "new content", string(content)) + }, + }, + { + name: "Source does not exist", + srcPath: "/src/nonexistent.txt", + dstPath: "/dst/file.txt", + setupFS: func(t *testing.T, fs oswrapper.MemMapOSWrapper) { + require.NoError(t, fs.MkdirAll("/dst", 0777)) + }, + wantErr: true, + }, + { + name: "Source is a directory", + srcPath: "/src/dir", + dstPath: "/dst/file.txt", + setupFS: func(t *testing.T, fs oswrapper.MemMapOSWrapper) { + require.NoError(t, fs.MkdirAll("/src/dir", 0777)) + require.NoError(t, fs.MkdirAll("/dst", 0777)) + }, + wantErr: true, + }, + { + name: "Destination directory does not exist", + srcPath: "/src/file.txt", + dstPath: "/nonexistent/dst/file.txt", + setupFS: func(t *testing.T, fs oswrapper.MemMapOSWrapper) { + require.NoError(t, fs.MkdirAll("/src", 0777)) + require.NoError(t, fs.WriteFile("/src/file.txt", []byte("hello"), 0666)) + }, + wantErr: false, // CopyFile creates the destination directory + verify: func(t *testing.T, fs oswrapper.MemMapOSWrapper) { + content, err := fs.ReadFile("/nonexistent/dst/file.txt") + require.NoError(t, err) + require.Equal(t, "hello", string(content)) + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + wrapper := oswrapper.CreateMemMapOSWrapper() + if tc.setupFS != nil { + tc.setupFS(t, wrapper) + } + + err := fileutils.CopyFile(tc.dstPath, tc.srcPath, wrapper) + + if tc.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + + if tc.verify != nil { + tc.verify(t, wrapper) + } + }) + } +} + +func TestCopyDir(t *testing.T) { + tests := []struct { + name string + srcPath string + dstPath string + setupFS func(t *testing.T, fs oswrapper.MemMapOSWrapper) + wantErr bool + verify func(t *testing.T, fs oswrapper.MemMapOSWrapper) + }{ + { + name: "Copy to non-existent destination", + srcPath: "/src/data", + dstPath: "/dst", + setupFS: func(t *testing.T, fs oswrapper.MemMapOSWrapper) { + require.NoError(t, fs.MkdirAll("/src/data/subdir", 0777)) + require.NoError(t, fs.WriteFile("/src/data/file1.txt", []byte("file1"), 0666)) + }, + wantErr: false, + verify: func(t *testing.T, fs oswrapper.MemMapOSWrapper) { + require.True(t, fileutils.IsDir("/dst", fs)) + content, err := fs.ReadFile(filepath.Join("/dst", "file1.txt")) + require.NoError(t, err) + require.Equal(t, "file1", string(content)) + require.True(t, fileutils.IsDir(filepath.Join("/dst", "subdir"), fs)) + }, + }, + { + name: "Overwrite existing destination", + srcPath: "/src/new_data", + dstPath: "/dst", + setupFS: func(t *testing.T, fs oswrapper.MemMapOSWrapper) { + // Source + require.NoError(t, fs.MkdirAll("/src/new_data", 0777)) + require.NoError(t, fs.WriteFile("/src/new_data/new_file.txt", []byte("new"), 0666)) + // Destination with old content + require.NoError(t, fs.MkdirAll("/dst/old_subdir", 0777)) + require.NoError(t, fs.WriteFile("/dst/old_file.txt", []byte("old"), 0666)) + }, + wantErr: false, + verify: func(t *testing.T, fs oswrapper.MemMapOSWrapper) { + // Check new file exists + content, err := fs.ReadFile(filepath.Join("/dst", "new_file.txt")) + require.NoError(t, err) + require.Equal(t, "new", string(content)) + + // Check old files are gone + require.False(t, fileutils.IsFile(filepath.Join("/dst", "old_file.txt"), fs)) + require.False(t, fileutils.IsDir(filepath.Join("/dst", "old_subdir"), fs)) + }, + }, + { + name: "Copy complex directory structure", + srcPath: "/src/complex", + dstPath: "/dst", + setupFS: func(t *testing.T, fs oswrapper.MemMapOSWrapper) { + // Source with complex structure + require.NoError(t, fs.MkdirAll("/src/complex/a", 0777)) + require.NoError(t, fs.MkdirAll("/src/complex/b/c", 0777)) + require.NoError(t, fs.WriteFile("/src/complex/root.txt", []byte("root"), 0666)) + require.NoError(t, fs.WriteFile("/src/complex/a/a.txt", []byte("a"), 0666)) + require.NoError(t, fs.WriteFile("/src/complex/b/b.txt", []byte("b"), 0666)) + require.NoError(t, fs.WriteFile("/src/complex/b/c/c.txt", []byte("c"), 0666)) + // Empty destination + require.NoError(t, fs.MkdirAll("/dst", 0777)) + }, + wantErr: false, + verify: func(t *testing.T, fs oswrapper.MemMapOSWrapper) { + content, err := fs.ReadFile(filepath.Join("/dst", "root.txt")) + require.NoError(t, err) + require.Equal(t, "root", string(content)) + content, err = fs.ReadFile(filepath.Join("/dst", "a", "a.txt")) + require.NoError(t, err) + require.Equal(t, "a", string(content)) + content, err = fs.ReadFile(filepath.Join("/dst", "b", "b.txt")) + require.NoError(t, err) + require.Equal(t, "b", string(content)) + content, err = fs.ReadFile(filepath.Join("/dst", "b", "c", "c.txt")) + require.NoError(t, err) + require.Equal(t, "c", string(content)) + }, + }, + { + name: "Copy empty directory to existing destination", + srcPath: "/src/empty", + dstPath: "/dst", + setupFS: func(t *testing.T, fs oswrapper.MemMapOSWrapper) { + require.NoError(t, fs.MkdirAll("/src/empty", 0777)) + require.NoError(t, fs.MkdirAll("/dst", 0777)) + require.NoError(t, fs.WriteFile("/dst/old_file.txt", []byte("old"), 0666)) + }, + 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 + }) + require.NoError(t, err) + require.Zero(t, fileCount, "directory should be empty") + }, + }, + { + name: "Source does not exist", + srcPath: "/src/nonexistent", + dstPath: "/dst", + setupFS: func(t *testing.T, fs oswrapper.MemMapOSWrapper) { + require.NoError(t, fs.MkdirAll("/dst", 0777)) + }, + wantErr: true, + }, + { + name: "Source is a file", + srcPath: "/src/file.txt", + dstPath: "/dst", + setupFS: func(t *testing.T, fs oswrapper.MemMapOSWrapper) { + require.NoError(t, fs.WriteFile("/src/file.txt", []byte("i am a file"), 0666)) + require.NoError(t, fs.MkdirAll("/dst", 0777)) + }, + wantErr: true, + }, + { + name: "Destination is a file", + srcPath: "/src/data", + dstPath: "/dst_is_a_file", + setupFS: func(t *testing.T, fs oswrapper.MemMapOSWrapper) { + require.NoError(t, fs.MkdirAll("/src/data", 0777)) + require.NoError(t, fs.WriteFile("/dst_is_a_file", []byte("i am a file"), 0666)) + }, + wantErr: true, + }, + { + name: "Destination is a subdirectory of source", + srcPath: "/src", + dstPath: "/src/sub", + setupFS: func(t *testing.T, fs oswrapper.MemMapOSWrapper) { + require.NoError(t, fs.MkdirAll("/src/sub", 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 := fileutils.CopyDir(tc.dstPath, tc.srcPath, wrapper) + + require.Equal(t, tc.wantErr, err != nil, "CopyDir() error = %v, wantErr %v", err, tc.wantErr) + + if tc.verify != nil { + tc.verify(t, wrapper) + } + }) + } +}
diff --git a/tools/src/fileutils/fileutils_other.go b/tools/src/fileutils/fileutils_other.go index 2742b74..13bb23f 100644 --- a/tools/src/fileutils/fileutils_other.go +++ b/tools/src/fileutils/fileutils_other.go
@@ -32,14 +32,14 @@ package fileutils import ( - "os" + "dawn.googlesource.com/dawn/tools/src/oswrapper" ) const ExeExt = "" // IsExe returns true if the file at path is an executable -func IsExe(path string) bool { - s, err := os.Stat(path) +func IsExe(path string, fsReader oswrapper.FilesystemReader) bool { + s, err := fsReader.Stat(path) if err != nil { return false }
diff --git a/tools/src/fileutils/fileutils_windows.go b/tools/src/fileutils/fileutils_windows.go index 7e897e9..5a6e2cd 100644 --- a/tools/src/fileutils/fileutils_windows.go +++ b/tools/src/fileutils/fileutils_windows.go
@@ -28,13 +28,15 @@ // Package fileutils contains utility functions for files package fileutils -import "os" +import ( + "dawn.googlesource.com/dawn/tools/src/oswrapper" +) const ExeExt = ".exe" // IsExe returns true if the file at path is an executable -func IsExe(path string) bool { - if _, err := os.Stat(path); err != nil { +func IsExe(path string, fsReader oswrapper.FilesystemReader) bool { + if _, err := fsReader.Stat(path); err != nil { return false } return true
diff --git a/tools/src/fileutils/paths.go b/tools/src/fileutils/paths.go index a3513a7..e0abd1d 100644 --- a/tools/src/fileutils/paths.go +++ b/tools/src/fileutils/paths.go
@@ -29,7 +29,6 @@ import ( "fmt" - "os" "os/exec" "path/filepath" "runtime" @@ -137,8 +136,8 @@ } // IsDir returns true if the path resolves to a directory -func IsDir(path string) bool { - s, err := os.Stat(path) +func IsDir(path string, fsReader oswrapper.FilesystemReader) bool { + s, err := fsReader.Stat(path) if err != nil { return false } @@ -146,8 +145,8 @@ } // IsFile returns true if the path resolves to a file -func IsFile(path string) bool { - s, err := os.Stat(path) +func IsFile(path string, fsReader oswrapper.FilesystemReader) bool { + s, err := fsReader.Stat(path) if err != nil { return false }
diff --git a/tools/src/fileutils/paths_test.go b/tools/src/fileutils/paths_test.go index c8bd072..f3df804 100644 --- a/tools/src/fileutils/paths_test.go +++ b/tools/src/fileutils/paths_test.go
@@ -180,3 +180,105 @@ } } } + +func TestIsDir(t *testing.T) { + tests := []struct { + name string + path string + setupFS func(fs oswrapper.MemMapOSWrapper) // Sets up the filesystem + want bool + }{ + { + name: "Is a directory", + path: "/a/b/c", + setupFS: func(fs oswrapper.MemMapOSWrapper) { + require.NoError(t, fs.MkdirAll("/a/b/c", 0777)) + }, + want: true, + }, + { + name: "Is a file", + path: "/a/b/file.txt", + setupFS: func(fs oswrapper.MemMapOSWrapper) { + require.NoError(t, fs.MkdirAll("/a/b", 0777)) + require.NoError(t, fs.WriteFile("/a/b/file.txt", []byte("hello"), 0666)) + }, + want: false, + }, + { + name: "Does not exist", + path: "/a/b/c", + setupFS: nil, + want: false, + }, + { + name: "Empty path", + path: "", + setupFS: nil, + want: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + wrapper := oswrapper.CreateMemMapOSWrapper() + if tc.setupFS != nil { + tc.setupFS(wrapper) + } + + got := fileutils.IsDir(tc.path, wrapper) + require.Equal(t, tc.want, got) + }) + } +} + +func TestIsFile(t *testing.T) { + tests := []struct { + name string + path string + setupFS func(fs oswrapper.MemMapOSWrapper) // Sets up the filesystem + want bool + }{ + { + name: "Is a file", + path: "/a/b/file.txt", + setupFS: func(fs oswrapper.MemMapOSWrapper) { + require.NoError(t, fs.MkdirAll("/a/b", 0777)) + require.NoError(t, fs.WriteFile("/a/b/file.txt", []byte("hello"), 0666)) + }, + want: true, + }, + { + name: "Is a directory", + path: "/a/b/c", + setupFS: func(fs oswrapper.MemMapOSWrapper) { + require.NoError(t, fs.MkdirAll("/a/b/c", 0777)) + }, + want: false, + }, + { + name: "Does not exist", + path: "/a/b/c", + setupFS: nil, + want: false, + }, + { + name: "Empty path", + path: "", + setupFS: nil, + want: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + wrapper := oswrapper.CreateMemMapOSWrapper() + if tc.setupFS != nil { + tc.setupFS(wrapper) + } + + got := fileutils.IsFile(tc.path, wrapper) + require.Equal(t, tc.want, got) + }) + } +}
diff --git a/tools/src/oswrapper/memmaposwrapper.go b/tools/src/oswrapper/memmaposwrapper.go index dc01f4e..69c7593 100644 --- a/tools/src/oswrapper/memmaposwrapper.go +++ b/tools/src/oswrapper/memmaposwrapper.go
@@ -123,6 +123,11 @@ } func (m MemMapFilesystemReader) Stat(name string) (os.FileInfo, error) { + // KI with afero, https://github.com/spf13/afero/issues/522 + if name == "" { + return nil, fmt.Errorf("no such file or directory") + } + return m.fs.Stat(name) }
diff --git a/tools/src/oswrapper/memmaposwrapper_test.go b/tools/src/oswrapper/memmaposwrapper_test.go index 936a96f..a97baa5 100644 --- a/tools/src/oswrapper/memmaposwrapper_test.go +++ b/tools/src/oswrapper/memmaposwrapper_test.go
@@ -146,6 +146,12 @@ require.ErrorContains(t, err, "open /foo.txt: file does not exist") } +func TestStat_EmptyString(t *testing.T) { + wrapper := CreateMemMapOSWrapper() + _, err := wrapper.Stat("") + require.ErrorContains(t, err, "no such file or directory") +} + func TestWalk_Nonexistent(t *testing.T) { wrapper := CreateMemMapOSWrapper()