current commit
250514.4 · fastwalk: add example tests
package fastwalk_test

import (
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"strings"

	"github.com/charlievieth/fastwalk"
)

func CreateFiles(files map[string]string) (root string, cleanup func()) {
	tempdir, err := os.MkdirTemp("", "fastwalk-example-*")
	if err != nil {
		panic(err)
	}

	symlinks := map[string]string{}
	for path, contents := range files {
		file := filepath.Join(tempdir, "/src", path)
		if err := os.MkdirAll(filepath.Dir(file), 0755); err != nil {
			panic(err)
		}
		if strings.HasPrefix(contents, "LINK:") {
			symlinks[file] = filepath.FromSlash(strings.TrimPrefix(contents, "LINK:"))
			continue
		}
		if err := os.WriteFile(file, []byte(contents), 0644); err != nil {
			panic(err)
		}
	}

	// Create symlinks after all other files. Otherwise, directory symlinks on
	// Windows are unusable (see https://golang.org/issue/39183).
	for file, dst := range symlinks {
		if err := os.Symlink(dst, file); err != nil {
			panic(err)
		}
	}

	return filepath.Join(tempdir, "src") + "/", func() { os.RemoveAll(tempdir) }
}

func PrettyPrintEntry(root, path string, de fs.DirEntry) {
	rel, err := filepath.Rel(root, path)
	if err != nil {
		panic(err)
	}
	rel = filepath.ToSlash(rel)
	if de.Type()&fs.ModeSymlink != 0 {
		dst, err := os.Readlink(path)
		if err != nil {
			panic(err)
		}
		fmt.Printf("%s: %s -> %s\n", de.Type(), rel, filepath.ToSlash(dst))
	} else {
		fmt.Printf("%s: %s\n", de.Type(), rel)
	}
}

// This example shows how the [fastwalk.Config] Follow field can be used to
// efficiently and safely follow symlinks. The below example contains a symlink
// loop ("bar/symloop"), which fastwalk detects and does not follow.
//
// NOTE: We still call the [fs.WalkDirFunc] on the symlink that creates a loop,
// but we do not follow/traverse it.
func ExampleWalk_follow() {
	// Setup
	root, cleanup := CreateFiles(map[string]string{
		"bar/bar.go":  "one",
		"bar/symlink": "LINK:bar.go",
		"foo/foo.go":  "two",
		"foo/symdir":  "LINK:../bar/",
		"foo/broken":  "LINK:nope.txt", // Broken symlink
		"foo/foo":     "LINK:../foo/",  // Symlink loop
	})
	defer cleanup()

	conf := fastwalk.Config{
		Follow: true,
	}
	err := fastwalk.Walk(&conf, root, func(path string, de fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		PrettyPrintEntry(root, path, de)
		return nil
	})
	if err != nil {
		panic(err)
	}
	// Unordered output:
	// d---------: .
	// d---------: bar
	// ----------: bar/bar.go
	// L---------: bar/symlink -> bar.go
	// d---------: foo
	// L---------: foo/broken -> nope.txt
	// ----------: foo/foo.go
	// L---------: foo/foo -> ../foo/
	// L---------: foo/symdir -> ../bar/
	// L---------: foo/symdir/symlink -> bar.go
	// ----------: foo/symdir/bar.go
}

func ExampleWalk() {
	root, cleanup := CreateFiles(map[string]string{
		"bar/b.txt": "",
		"foo/f.txt": "",
		// Since Config.Follow is set to false, the symbolic link "link" will
		// be visited, but we will not traverse into it (since our walk func
		// does not return [fastwalk.ErrTraverseLink]).
		"link": "LINK:bar",
	})
	defer cleanup()

	err := fastwalk.Walk(nil, root, func(path string, de fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		PrettyPrintEntry(root, path, de)
		return nil
	})
	if err != nil {
		panic(err)
	}
	// Unordered output:
	// d---------: .
	// d---------: bar
	// d---------: foo
	// L---------: link -> bar
	// ----------: bar/b.txt
	// ----------: foo/f.txt
}

func ExampleWalk_traverseLink() {
	root, cleanup := CreateFiles(map[string]string{
		"bar/b.txt": "",
		"foo/f.txt": "",
		// This link is followed since our walk func returns ErrTraverseLink
		// when a symlink that resolves to a directory is encountered.
		"link": "LINK:bar",
	})
	defer cleanup()

	err := fastwalk.Walk(nil, root, func(path string, de fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		PrettyPrintEntry(root, path, de)

		// It is strongly recommended to use fastwalk.Config.Follow instead
		// of manually handling link traversal since this will fail if the
		// link points to a file (and not a directory).
		if de.Type()&fs.ModeSymlink != 0 {
			fi, err := fastwalk.StatDirEntry(path, de)
			if err != nil {
				return err
			}
			if fi.IsDir() {
				return fastwalk.ErrTraverseLink
			}
		}
		return nil
	})
	if err != nil {
		panic(err)
	}
	// Unordered output:
	// d---------: .
	// d---------: bar
	// d---------: foo
	// L---------: link -> bar
	// ----------: bar/b.txt
	// ----------: foo/f.txt
	// ----------: link/b.txt
}