// Copyright 2024 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 testfiles provides utilities for writing Go tests with files // in testdata. package testfiles import ( "io" "io/fs" "os" "path/filepath" "strings" "testing" "golang.org/x/tools/txtar" ) // CopyDirToTmp copies dir to a temporary test directory using // CopyTestFiles and returns the path to the test directory. func CopyDirToTmp(t testing.TB, srcdir string) string { dst := t.TempDir() if err := CopyFS(dst, os.DirFS(srcdir)); err != nil { t.Fatal(err) } return dst } // CopyFS copies the files and directories in src to a // destination directory dst. Paths to files and directories // ending in a ".test" extension have the ".test" extension // removed. This allows tests to hide files whose names have // special meaning, such as "go.mod" files or "testdata" directories // from the go command, or ill-formed Go source files from gofmt. // // For example if we copy the directory testdata: // // testdata/ // go.mod.test // a/a.go // b/b.go // // The resulting files will be: // // dst/ // go.mod // a/a.go // b/b.go func CopyFS(dstdir string, src fs.FS) error { if err := copyFS(dstdir, src); err != nil { return err } // Collect ".test" paths in lexical order. var rename []string err := fs.WalkDir(os.DirFS(dstdir), ".", func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if strings.HasSuffix(path, ".test") { rename = append(rename, path) } return nil }) if err != nil { return err } // Rename the .test paths in reverse lexical order, e.g. // in d.test/a.test renames a.test to d.test/a then d.test to d. for i := len(rename) - 1; i >= 0; i-- { oldpath := filepath.Join(dstdir, rename[i]) newpath := strings.TrimSuffix(oldpath, ".test") if err != os.Rename(oldpath, newpath) { return err } } return nil } // Copy the files in src to dst. // Use os.CopyFS when 1.23 can be used in x/tools. func copyFS(dstdir string, src fs.FS) error { return fs.WalkDir(src, ".", func(path string, d fs.DirEntry, err error) error { if err != nil { return err } newpath := filepath.Join(dstdir, path) if d.IsDir() { return os.MkdirAll(newpath, 0777) } r, err := src.Open(path) if err != nil { return err } defer r.Close() w, err := os.Create(newpath) if err != nil { return err } defer w.Close() _, err = io.Copy(w, r) return err }) } // ExtractTxtar writes each archive file to the corresponding location beneath dir. // // TODO(adonovan): move this to txtar package, we need it all the time (#61386). func ExtractTxtar(dstdir string, ar *txtar.Archive) error { for _, file := range ar.Files { name := filepath.Join(dstdir, file.Name) if err := os.MkdirAll(filepath.Dir(name), 0777); err != nil { return err } if err := os.WriteFile(name, file.Data, 0666); err != nil { return err } } return nil } // ExtractTxtarToTmp read a txtar archive on a given path, // extracts it to a temporary directory, and returns the // temporary directory. func ExtractTxtarToTmp(t testing.TB, archive string) string { ar, err := txtar.ParseFile(archive) if err != nil { t.Fatal(err) } dir := t.TempDir() err = ExtractTxtar(dir, ar) if err != nil { t.Fatal(err) } return dir }