Merge pull request #24 from colincross/glob

pathtools.Glob bugfixes and features
diff --git a/bootstrap/command.go b/bootstrap/command.go
index 16a240e..05a3e9d 100644
--- a/bootstrap/command.go
+++ b/bootstrap/command.go
@@ -21,8 +21,8 @@
 	"io/ioutil"
 	"os"
 	"path/filepath"
-	"runtime/pprof"
 	"runtime"
+	"runtime/pprof"
 
 	"github.com/google/blueprint"
 	"github.com/google/blueprint/deptools"
diff --git a/ninja_strings.go b/ninja_strings.go
index d640c87..3492da2 100644
--- a/ninja_strings.go
+++ b/ninja_strings.go
@@ -132,7 +132,7 @@
 		r >= '0' && r <= '9', r == '_', r == '-':
 		// The beginning of a of the variable name.  Output the string and
 		// keep going.
-		state.pushString(state.str[state.stringStart:i-1])
+		state.pushString(state.str[state.stringStart : i-1])
 		return parseDollarState, nil
 
 	case r == '$':
@@ -143,7 +143,7 @@
 	case r == '{':
 		// This is a bracketted variable name (e.g. "${blah.blah}").  Output
 		// the string and keep going.
-		state.pushString(state.str[state.stringStart:i-1])
+		state.pushString(state.str[state.stringStart : i-1])
 		state.varStart = i + 1
 		return parseBracketsState, nil
 
diff --git a/ninja_strings_test.go b/ninja_strings_test.go
index 695f14c..b417335 100644
--- a/ninja_strings_test.go
+++ b/ninja_strings_test.go
@@ -58,12 +58,12 @@
 	{
 		input: "$foo${bar}",
 		vars:  []string{"foo", "bar"},
-		strs:  []string{"","", ""},
+		strs:  []string{"", "", ""},
 	},
 	{
 		input: "$foo$$",
 		vars:  []string{"foo"},
-		strs:  []string{"","$$"},
+		strs:  []string{"", "$$"},
 	},
 	{
 		input: "foo bar",
diff --git a/package_ctx.go b/package_ctx.go
index f160eb4..0d3f0ae 100644
--- a/package_ctx.go
+++ b/package_ctx.go
@@ -563,7 +563,7 @@
 }
 
 type builtinPool struct {
-	name_  string
+	name_ string
 }
 
 func (p *builtinPool) packageContext() *PackageContext {
diff --git a/pathtools/glob.go b/pathtools/glob.go
index 507983e..239a3c7 100644
--- a/pathtools/glob.go
+++ b/pathtools/glob.go
@@ -15,55 +15,247 @@
 package pathtools
 
 import (
+	"errors"
+	"os"
 	"path/filepath"
 	"strings"
 )
 
+var GlobMultipleRecursiveErr = errors.New("pattern contains multiple **")
+var GlobLastRecursiveErr = errors.New("pattern ** as last path element")
+
 // Glob returns the list of files that match the given pattern along with the
 // list of directories that were searched to construct the file list.
+// The supported glob patterns are equivalent to filepath.Glob, with an
+// extension that recursive glob (** matching zero or more complete path
+// entries) is supported.
 func Glob(pattern string) (matches, dirs []string, err error) {
-	matches, err = filepath.Glob(pattern)
+	return GlobWithExcludes(pattern, nil)
+}
+
+// GlobWithExcludes returns the list of files that match the given pattern but
+// do not match the given exclude patterns, along with the list of directories
+// that were searched to construct the file list.  The supported glob and
+// exclude patterns are equivalent to filepath.Glob, with an extension that
+// recursive glob (** matching zero or more complete path entries) is supported.
+func GlobWithExcludes(pattern string, excludes []string) (matches, dirs []string, err error) {
+	if !isWild(pattern) {
+		// If there are no wilds in the pattern, just return whether the file at the pattern
+		// exists or not.  Uses filepath.Glob instead of manually statting to get consistent
+		// results.
+		matches, err = filepath.Glob(filepath.Clean(pattern))
+	} else if filepath.Base(pattern) == "**" {
+		return nil, nil, GlobLastRecursiveErr
+	} else {
+		matches, dirs, err = glob(pattern, false)
+	}
+
 	if err != nil {
 		return nil, nil, err
 	}
 
-	wildIndices := wildElements(pattern)
+	matches, err = filterExcludes(matches, excludes)
+	if err != nil {
+		return nil, nil, err
+	}
 
-	if len(wildIndices) > 0 {
-		for _, match := range matches {
-			dir := filepath.Dir(match)
-			dirElems := strings.Split(dir, string(filepath.Separator))
+	return matches, dirs, nil
+}
 
-			for _, index := range wildIndices {
-				dirs = append(dirs, strings.Join(dirElems[:index],
-					string(filepath.Separator)))
+// glob is a recursive helper function to handle globbing each level of the pattern individually,
+// allowing searched directories to be tracked.  Also handles the recursive glob pattern, **.
+func glob(pattern string, hasRecursive bool) (matches, dirs []string, err error) {
+	if !isWild(pattern) {
+		// If there are no wilds in the pattern, just return whether the file at the pattern
+		// exists or not.  Uses filepath.Glob instead of manually statting to get consistent
+		// results.
+		matches, err = filepath.Glob(filepath.Clean(pattern))
+		return matches, dirs, err
+	}
+
+	dir, file := saneSplit(pattern)
+
+	if file == "**" {
+		if hasRecursive {
+			return matches, dirs, GlobMultipleRecursiveErr
+		}
+		hasRecursive = true
+	}
+
+	dirMatches, dirs, err := glob(dir, hasRecursive)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	for _, m := range dirMatches {
+		if info, _ := os.Stat(m); info.IsDir() {
+			if file == "**" {
+				recurseDirs, err := walkAllDirs(m)
+				if err != nil {
+					return nil, nil, err
+				}
+				matches = append(matches, recurseDirs...)
+			} else {
+				dirs = append(dirs, m)
+				newMatches, err := filepath.Glob(filepath.Join(m, file))
+				if err != nil {
+					return nil, nil, err
+				}
+				matches = append(matches, newMatches...)
 			}
 		}
 	}
 
-	return
+	return matches, dirs, nil
 }
 
-func wildElements(pattern string) []int {
-	elems := strings.Split(pattern, string(filepath.Separator))
-
-	var result []int
-	for i, elem := range elems {
-		if isWild(elem) {
-			result = append(result, i)
-		}
+// Faster version of dir, file := filepath.Dir(path), filepath.File(path) with no allocations
+// Similar to filepath.Split, but returns "." if dir is empty and trims trailing slash if dir is
+// not "/".  Returns ".", "" if path is "."
+func saneSplit(path string) (dir, file string) {
+	if path == "." {
+		return ".", ""
 	}
-	return result
+	dir, file = filepath.Split(path)
+	switch dir {
+	case "":
+		dir = "."
+	case "/":
+		// Nothing
+	default:
+		dir = dir[:len(dir)-1]
+	}
+	return dir, file
 }
 
 func isWild(pattern string) bool {
 	return strings.ContainsAny(pattern, "*?[")
 }
 
+// Returns a list of all directories under dir
+func walkAllDirs(dir string) (dirs []string, err error) {
+	err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
+		if err != nil {
+			return err
+		}
+
+		if info.Mode().IsDir() {
+			dirs = append(dirs, path)
+		}
+		return nil
+	})
+
+	return dirs, err
+}
+
+// Filters the strings in matches based on the glob patterns in excludes.  Hierarchical (a/*) and
+// recursive (**) glob patterns are supported.
+func filterExcludes(matches []string, excludes []string) ([]string, error) {
+	if len(excludes) == 0 {
+		return matches, nil
+	}
+
+	var ret []string
+matchLoop:
+	for _, m := range matches {
+		for _, e := range excludes {
+			exclude, err := match(e, m)
+			if err != nil {
+				return nil, err
+			}
+			if exclude {
+				continue matchLoop
+			}
+		}
+		ret = append(ret, m)
+	}
+
+	return ret, nil
+}
+
+// match returns true if name matches pattern using the same rules as filepath.Match, but supporting
+// hierarchical patterns (a/*) and recursive globs (**).
+func match(pattern, name string) (bool, error) {
+	if filepath.Base(pattern) == "**" {
+		return false, GlobLastRecursiveErr
+	}
+
+	for {
+		var patternFile, nameFile string
+		pattern, patternFile = saneSplit(pattern)
+		name, nameFile = saneSplit(name)
+
+		if patternFile == "**" {
+			return matchPrefix(pattern, filepath.Join(name, nameFile))
+		}
+
+		if nameFile == "" && patternFile == "" {
+			return true, nil
+		} else if nameFile == "" || patternFile == "" {
+			return false, nil
+		}
+
+		match, err := filepath.Match(patternFile, nameFile)
+		if err != nil || !match {
+			return match, err
+		}
+	}
+}
+
+// matchPrefix returns true if the beginning of name matches pattern using the same rules as
+// filepath.Match, but supporting hierarchical patterns (a/*).  Recursive globs (**) are not
+// supported, they should have been handled in match().
+func matchPrefix(pattern, name string) (bool, error) {
+	if len(pattern) > 0 && pattern[0] == '/' {
+		if len(name) > 0 && name[0] == '/' {
+			pattern = pattern[1:]
+			name = name[1:]
+		} else {
+			return false, nil
+		}
+	}
+
+	for {
+		var patternElem, nameElem string
+		patternElem, pattern = saneSplitFirst(pattern)
+		nameElem, name = saneSplitFirst(name)
+
+		if patternElem == "." {
+			patternElem = ""
+		}
+		if nameElem == "." {
+			nameElem = ""
+		}
+
+		if patternElem == "**" {
+			return false, GlobMultipleRecursiveErr
+		}
+
+		if patternElem == "" {
+			return true, nil
+		} else if nameElem == "" {
+			return false, nil
+		}
+
+		match, err := filepath.Match(patternElem, nameElem)
+		if err != nil || !match {
+			return match, err
+		}
+	}
+}
+
+func saneSplitFirst(path string) (string, string) {
+	i := strings.IndexRune(path, filepath.Separator)
+	if i < 0 {
+		return path, ""
+	}
+	return path[:i], path[i+1:]
+}
+
 func GlobPatternList(patterns []string, prefix string) (globedList []string, depDirs []string, err error) {
 	var (
 		matches []string
-		deps         []string
+		deps    []string
 	)
 
 	globedList = make([]string, 0)
diff --git a/pathtools/glob_test.go b/pathtools/glob_test.go
new file mode 100644
index 0000000..efd63b0
--- /dev/null
+++ b/pathtools/glob_test.go
@@ -0,0 +1,403 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package pathtools
+
+import (
+	"os"
+	"path/filepath"
+	"reflect"
+	"testing"
+)
+
+var pwd, _ = os.Getwd()
+
+var globTestCases = []struct {
+	pattern  string
+	matches  []string
+	excludes []string
+	dirs     []string
+	err      error
+}{
+	// Current directory tests
+	{
+		pattern: "*",
+		matches: []string{"a", "b", "c", "d.ext", "e.ext"},
+		dirs:    []string{"."},
+	},
+	{
+		pattern: "*.ext",
+		matches: []string{"d.ext", "e.ext"},
+		dirs:    []string{"."},
+	},
+	{
+		pattern: "*/a",
+		matches: []string{"a/a", "b/a"},
+		dirs:    []string{".", "a", "b", "c"},
+	},
+	{
+		pattern: "*/*/a",
+		matches: []string{"a/a/a"},
+		dirs:    []string{".", "a", "b", "c", "a/a", "a/b", "c/f", "c/g", "c/h"},
+	},
+	{
+		pattern: "*/a/a",
+		matches: []string{"a/a/a"},
+		dirs:    []string{".", "a", "b", "c", "a/a"},
+	},
+
+	// ./ directory tests
+	{
+		pattern: "./*",
+		matches: []string{"a", "b", "c", "d.ext", "e.ext"},
+		dirs:    []string{"."},
+	},
+	{
+		pattern: "./*.ext",
+		matches: []string{"d.ext", "e.ext"},
+		dirs:    []string{"."},
+	},
+	{
+		pattern: "./*/a",
+		matches: []string{"a/a", "b/a"},
+		dirs:    []string{".", "a", "b", "c"},
+	},
+	{
+		pattern: "./[ac]/a",
+		matches: []string{"a/a"},
+		dirs:    []string{".", "a", "c"},
+	},
+
+	// subdirectory tests
+	{
+		pattern: "c/*/*.ext",
+		matches: []string{"c/f/f.ext", "c/g/g.ext"},
+		dirs:    []string{"c", "c/f", "c/g", "c/h"},
+	},
+	{
+		pattern: "a/*/a",
+		matches: []string{"a/a/a"},
+		dirs:    []string{"a", "a/a", "a/b"},
+	},
+
+	// absolute tests
+	{
+		pattern: filepath.Join(pwd, "testdata/c/*/*.ext"),
+		matches: []string{
+			filepath.Join(pwd, "testdata/c/f/f.ext"),
+			filepath.Join(pwd, "testdata/c/g/g.ext"),
+		},
+		dirs: []string{
+			filepath.Join(pwd, "testdata/c"),
+			filepath.Join(pwd, "testdata/c/f"),
+			filepath.Join(pwd, "testdata/c/g"),
+			filepath.Join(pwd, "testdata/c/h"),
+		},
+	},
+
+	// no-wild tests
+	{
+		pattern: "a",
+		matches: []string{"a"},
+		dirs:    nil,
+	},
+	{
+		pattern: "a/a",
+		matches: []string{"a/a"},
+		dirs:    nil,
+	},
+
+	// clean tests
+	{
+		pattern: "./c/*/*.ext",
+		matches: []string{"c/f/f.ext", "c/g/g.ext"},
+		dirs:    []string{"c", "c/f", "c/g", "c/h"},
+	},
+	{
+		pattern: "c/../c/*/*.ext",
+		matches: []string{"c/f/f.ext", "c/g/g.ext"},
+		dirs:    []string{"c", "c/f", "c/g", "c/h"},
+	},
+
+	// recursive tests
+	{
+		pattern: "**/a",
+		matches: []string{"a", "a/a", "a/a/a", "b/a"},
+		dirs:    []string{".", "a", "a/a", "a/b", "b", "c", "c/f", "c/g", "c/h"},
+	},
+	{
+		pattern: "a/**/a",
+		matches: []string{"a/a", "a/a/a"},
+		dirs:    []string{"a", "a/a", "a/b"},
+	},
+	{
+		pattern: "a/**/*",
+		matches: []string{"a/a", "a/b", "a/a/a", "a/b/b"},
+		dirs:    []string{"a", "a/a", "a/b"},
+	},
+
+	// absolute recursive tests
+	{
+		pattern: filepath.Join(pwd, "testdata/**/*.ext"),
+		matches: []string{
+			filepath.Join(pwd, "testdata/d.ext"),
+			filepath.Join(pwd, "testdata/e.ext"),
+			filepath.Join(pwd, "testdata/c/f/f.ext"),
+			filepath.Join(pwd, "testdata/c/g/g.ext"),
+		},
+		dirs: []string{
+			filepath.Join(pwd, "testdata"),
+			filepath.Join(pwd, "testdata/a"),
+			filepath.Join(pwd, "testdata/a/a"),
+			filepath.Join(pwd, "testdata/a/b"),
+			filepath.Join(pwd, "testdata/b"),
+			filepath.Join(pwd, "testdata/c"),
+			filepath.Join(pwd, "testdata/c/f"),
+			filepath.Join(pwd, "testdata/c/g"),
+			filepath.Join(pwd, "testdata/c/h"),
+		},
+	},
+
+	// recursive error tests
+	{
+		pattern: "**/**/*",
+		err:     GlobMultipleRecursiveErr,
+	},
+	{
+		pattern: "a/**/**/*",
+		err:     GlobMultipleRecursiveErr,
+	},
+	{
+		pattern: "**/a/**/*",
+		err:     GlobMultipleRecursiveErr,
+	},
+	{
+		pattern: "**/**/a/*",
+		err:     GlobMultipleRecursiveErr,
+	},
+	{
+		pattern: "a/**",
+		err:     GlobLastRecursiveErr,
+	},
+	{
+		pattern: "**/**",
+		err:     GlobLastRecursiveErr,
+	},
+
+	// exclude tests
+	{
+		pattern:  "*.ext",
+		excludes: []string{"d.ext"},
+		matches:  []string{"e.ext"},
+		dirs:     []string{"."},
+	},
+	{
+		pattern:  "*/*",
+		excludes: []string{"a/b"},
+		matches:  []string{"a/a", "b/a", "c/c", "c/f", "c/g", "c/h"},
+		dirs:     []string{".", "a", "b", "c"},
+	},
+	{
+		pattern:  "*/*",
+		excludes: []string{"a/b", "c/c"},
+		matches:  []string{"a/a", "b/a", "c/f", "c/g", "c/h"},
+		dirs:     []string{".", "a", "b", "c"},
+	},
+	{
+		pattern:  "*/*",
+		excludes: []string{"c/*", "*/a"},
+		matches:  []string{"a/b"},
+		dirs:     []string{".", "a", "b", "c"},
+	},
+	{
+		pattern:  "*/*",
+		excludes: []string{"*/*"},
+		matches:  nil,
+		dirs:     []string{".", "a", "b", "c"},
+	},
+
+	// absolute exclude tests
+	{
+		pattern:  filepath.Join(pwd, "testdata/c/*/*.ext"),
+		excludes: []string{filepath.Join(pwd, "testdata/c/*/f.ext")},
+		matches: []string{
+			filepath.Join(pwd, "testdata/c/g/g.ext"),
+		},
+		dirs: []string{
+			filepath.Join(pwd, "testdata/c"),
+			filepath.Join(pwd, "testdata/c/f"),
+			filepath.Join(pwd, "testdata/c/g"),
+			filepath.Join(pwd, "testdata/c/h"),
+		},
+	},
+	{
+		pattern:  filepath.Join(pwd, "testdata/c/*/*.ext"),
+		excludes: []string{filepath.Join(pwd, "testdata/c/f/*.ext")},
+		matches: []string{
+			filepath.Join(pwd, "testdata/c/g/g.ext"),
+		},
+		dirs: []string{
+			filepath.Join(pwd, "testdata/c"),
+			filepath.Join(pwd, "testdata/c/f"),
+			filepath.Join(pwd, "testdata/c/g"),
+			filepath.Join(pwd, "testdata/c/h"),
+		},
+	},
+
+	// recursive exclude tests
+	{
+		pattern:  "*.ext",
+		excludes: []string{"**/*.ext"},
+		matches:  nil,
+		dirs:     []string{"."},
+	},
+	{
+		pattern:  "*/*",
+		excludes: []string{"**/b"},
+		matches:  []string{"a/a", "b/a", "c/c", "c/f", "c/g", "c/h"},
+		dirs:     []string{".", "a", "b", "c"},
+	},
+	{
+		pattern:  "*/*",
+		excludes: []string{"a/**/*"},
+		matches:  []string{"b/a", "c/c", "c/f", "c/g", "c/h"},
+		dirs:     []string{".", "a", "b", "c"},
+	},
+	{
+		pattern:  "**/*",
+		excludes: []string{"**/*"},
+		matches:  nil,
+		dirs:     []string{".", "a", "a/a", "a/b", "b", "c", "c/f", "c/g", "c/h"},
+	},
+	{
+		pattern:  "*/*/*",
+		excludes: []string{"a/**/a"},
+		matches:  []string{"a/b/b", "c/f/f.ext", "c/g/g.ext", "c/h/h"},
+		dirs:     []string{".", "a", "b", "c", "a/a", "a/b", "c/f", "c/g", "c/h"},
+	},
+	{
+		pattern:  "*/*/*",
+		excludes: []string{"**/a"},
+		matches:  []string{"a/b/b", "c/f/f.ext", "c/g/g.ext", "c/h/h"},
+		dirs:     []string{".", "a", "b", "c", "a/a", "a/b", "c/f", "c/g", "c/h"},
+	},
+	{
+		pattern:  "c/*/*.ext",
+		excludes: []string{"c/**/f.ext"},
+		matches:  []string{"c/g/g.ext"},
+		dirs:     []string{"c", "c/f", "c/g", "c/h"},
+	},
+
+	// absoulte recursive exclude tests
+	{
+		pattern:  filepath.Join(pwd, "testdata/c/*/*.ext"),
+		excludes: []string{filepath.Join(pwd, "testdata/**/f.ext")},
+		matches: []string{
+			filepath.Join(pwd, "testdata/c/g/g.ext"),
+		},
+		dirs: []string{
+			filepath.Join(pwd, "testdata/c"),
+			filepath.Join(pwd, "testdata/c/f"),
+			filepath.Join(pwd, "testdata/c/g"),
+			filepath.Join(pwd, "testdata/c/h"),
+		},
+	},
+
+	// clean exclude tests
+	{
+		pattern:  "./c/*/*.ext",
+		excludes: []string{"./c/*/f.ext"},
+		matches:  []string{"c/g/g.ext"},
+		dirs:     []string{"c", "c/f", "c/g", "c/h"},
+	},
+	{
+		pattern:  "c/*/*.ext",
+		excludes: []string{"./c/*/f.ext"},
+		matches:  []string{"c/g/g.ext"},
+		dirs:     []string{"c", "c/f", "c/g", "c/h"},
+	},
+	{
+		pattern:  "./c/*/*.ext",
+		excludes: []string{"c/*/f.ext"},
+		matches:  []string{"c/g/g.ext"},
+		dirs:     []string{"c", "c/f", "c/g", "c/h"},
+	},
+
+	// recursive exclude error tests
+	{
+		pattern:  "**/*",
+		excludes: []string{"**/**/*"},
+		err:      GlobMultipleRecursiveErr,
+	},
+	{
+		pattern:  "**/*",
+		excludes: []string{"a/**/**/*"},
+		err:      GlobMultipleRecursiveErr,
+	},
+	{
+		pattern:  "**/*",
+		excludes: []string{"**/a/**/*"},
+		err:      GlobMultipleRecursiveErr,
+	},
+	{
+		pattern:  "**/*",
+		excludes: []string{"**/**/a/*"},
+		err:      GlobMultipleRecursiveErr,
+	},
+	{
+		pattern:  "**/*",
+		excludes: []string{"a/**"},
+		err:      GlobLastRecursiveErr,
+	},
+	{
+		pattern:  "**/*",
+		excludes: []string{"**/**"},
+		err:      GlobLastRecursiveErr,
+	},
+}
+
+func TestGlob(t *testing.T) {
+	os.Chdir("testdata")
+	defer os.Chdir("..")
+	for _, testCase := range globTestCases {
+		matches, dirs, err := GlobWithExcludes(testCase.pattern, testCase.excludes)
+		if err != testCase.err {
+			t.Errorf(" pattern: %q", testCase.pattern)
+			if testCase.excludes != nil {
+				t.Errorf("excludes: %q", testCase.excludes)
+			}
+			t.Errorf("   error: %s", err)
+			continue
+		}
+
+		if !reflect.DeepEqual(matches, testCase.matches) {
+			t.Errorf("incorrect matches list:")
+			t.Errorf(" pattern: %q", testCase.pattern)
+			if testCase.excludes != nil {
+				t.Errorf("excludes: %q", testCase.excludes)
+			}
+			t.Errorf("     got: %#v", matches)
+			t.Errorf("expected: %#v", testCase.matches)
+		}
+		if !reflect.DeepEqual(dirs, testCase.dirs) {
+			t.Errorf("incorrect dirs list:")
+			t.Errorf(" pattern: %q", testCase.pattern)
+			if testCase.excludes != nil {
+				t.Errorf("excludes: %q", testCase.excludes)
+			}
+			t.Errorf("     got: %#v", dirs)
+			t.Errorf("expected: %#v", testCase.dirs)
+		}
+	}
+}
diff --git a/pathtools/testdata/a/a/a b/pathtools/testdata/a/a/a
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pathtools/testdata/a/a/a
diff --git a/pathtools/testdata/a/b/b b/pathtools/testdata/a/b/b
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pathtools/testdata/a/b/b
diff --git a/pathtools/testdata/b/a b/pathtools/testdata/b/a
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pathtools/testdata/b/a
diff --git a/pathtools/testdata/c/c b/pathtools/testdata/c/c
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pathtools/testdata/c/c
diff --git a/pathtools/testdata/c/f/f.ext b/pathtools/testdata/c/f/f.ext
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pathtools/testdata/c/f/f.ext
diff --git a/pathtools/testdata/c/g/g.ext b/pathtools/testdata/c/g/g.ext
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pathtools/testdata/c/g/g.ext
diff --git a/pathtools/testdata/c/h/h b/pathtools/testdata/c/h/h
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pathtools/testdata/c/h/h
diff --git a/pathtools/testdata/d.ext b/pathtools/testdata/d.ext
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pathtools/testdata/d.ext
diff --git a/pathtools/testdata/e.ext b/pathtools/testdata/e.ext
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pathtools/testdata/e.ext