Revert "Revert "Cacheable, multithreaded finder.""

Bug: 62455338
Test: m -j

This reverts commit d1abeb9d982b11fdf4047176d213acc8197c375f.

Change-Id: I9f73031636157511b5f1c6ce8a205e9bc91669ff
diff --git a/finder/finder_test.go b/finder/finder_test.go
new file mode 100644
index 0000000..60e5eb2
--- /dev/null
+++ b/finder/finder_test.go
@@ -0,0 +1,1573 @@
+// Copyright 2017 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 finder
+
+import (
+	"fmt"
+	"log"
+	"path/filepath"
+	"reflect"
+	"testing"
+
+	"sort"
+
+	"io/ioutil"
+
+	"android/soong/fs"
+	"runtime/debug"
+	"time"
+)
+
+// some utils for tests to use
+func newFs() *fs.MockFs {
+	return fs.NewMockFs(map[string][]byte{})
+}
+
+func newFinder(t *testing.T, filesystem *fs.MockFs, cacheParams CacheParams) *Finder {
+	cachePath := "/finder/finder-db"
+	cacheDir := filepath.Dir(cachePath)
+	filesystem.MkDirs(cacheDir)
+	if cacheParams.WorkingDirectory == "" {
+		cacheParams.WorkingDirectory = "/cwd"
+	}
+
+	logger := log.New(ioutil.Discard, "", 0)
+	finder := New(cacheParams, filesystem, logger, cachePath)
+	return finder
+}
+
+func finderWithSameParams(t *testing.T, original *Finder) *Finder {
+	return New(
+		original.cacheMetadata.Config.CacheParams,
+		original.filesystem,
+		original.logger,
+		original.DbPath)
+}
+
+func write(t *testing.T, path string, content string, filesystem *fs.MockFs) {
+	parent := filepath.Dir(path)
+	filesystem.MkDirs(parent)
+	err := filesystem.WriteFile(path, []byte(content), 0777)
+	if err != nil {
+		t.Fatal(err.Error())
+	}
+}
+
+func create(t *testing.T, path string, filesystem *fs.MockFs) {
+	write(t, path, "hi", filesystem)
+}
+
+func delete(t *testing.T, path string, filesystem *fs.MockFs) {
+	err := filesystem.Remove(path)
+	if err != nil {
+		t.Fatal(err.Error())
+	}
+}
+
+func removeAll(t *testing.T, path string, filesystem *fs.MockFs) {
+	err := filesystem.RemoveAll(path)
+	if err != nil {
+		t.Fatal(err.Error())
+	}
+}
+
+func move(t *testing.T, oldPath string, newPath string, filesystem *fs.MockFs) {
+	err := filesystem.Rename(oldPath, newPath)
+	if err != nil {
+		t.Fatal(err.Error())
+	}
+}
+
+func link(t *testing.T, newPath string, oldPath string, filesystem *fs.MockFs) {
+	parentPath := filepath.Dir(newPath)
+	err := filesystem.MkDirs(parentPath)
+	if err != nil {
+		t.Fatal(err.Error())
+	}
+	err = filesystem.Symlink(oldPath, newPath)
+	if err != nil {
+		t.Fatal(err.Error())
+	}
+}
+func read(t *testing.T, path string, filesystem *fs.MockFs) string {
+	reader, err := filesystem.Open(path)
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+	bytes, err := ioutil.ReadAll(reader)
+	if err != nil {
+		t.Fatal(err.Error())
+	}
+	return string(bytes)
+}
+func modTime(t *testing.T, path string, filesystem *fs.MockFs) time.Time {
+	stats, err := filesystem.Lstat(path)
+	if err != nil {
+		t.Fatal(err.Error())
+	}
+	return stats.ModTime()
+}
+func setReadable(t *testing.T, path string, readable bool, filesystem *fs.MockFs) {
+	err := filesystem.SetReadable(path, readable)
+	if err != nil {
+		t.Fatal(err.Error())
+	}
+}
+func fatal(t *testing.T, message string) {
+	t.Error(message)
+	debug.PrintStack()
+	t.FailNow()
+}
+func assertSameResponse(t *testing.T, actual []string, expected []string) {
+	sort.Strings(actual)
+	sort.Strings(expected)
+	if !reflect.DeepEqual(actual, expected) {
+		fatal(
+			t,
+			fmt.Sprintf(
+				"Expected Finder to return these %v paths:\n  %v,\ninstead returned these %v paths:  %v\n",
+				len(expected), expected, len(actual), actual),
+		)
+	}
+}
+
+func assertSameStatCalls(t *testing.T, actual []string, expected []string) {
+	sort.Strings(actual)
+	sort.Strings(expected)
+
+	if !reflect.DeepEqual(actual, expected) {
+		fatal(
+			t,
+			fmt.Sprintf(
+				"Finder made incorrect Stat calls.\n"+
+					"Actual:\n"+
+					"%v\n"+
+					"Expected:\n"+
+					"%v\n"+
+					"\n",
+				actual, expected),
+		)
+	}
+}
+func assertSameReadDirCalls(t *testing.T, actual []string, expected []string) {
+	sort.Strings(actual)
+	sort.Strings(expected)
+
+	if !reflect.DeepEqual(actual, expected) {
+		fatal(
+			t,
+			fmt.Sprintf(
+				"Finder made incorrect ReadDir calls.\n"+
+					"Actual:\n"+
+					"%v\n"+
+					"Expected:\n"+
+					"%v\n"+
+					"\n",
+				actual, expected),
+		)
+	}
+}
+
+// runSimpleTests creates a few files, searches for findme.txt, and checks for the expected matches
+func runSimpleTest(t *testing.T, existentPaths []string, expectedMatches []string) {
+	filesystem := newFs()
+	root := "/tmp"
+	filesystem.MkDirs(root)
+	for _, path := range existentPaths {
+		create(t, filepath.Join(root, path), filesystem)
+	}
+
+	finder := newFinder(t,
+		filesystem,
+		CacheParams{
+			"/cwd",
+			[]string{root},
+			nil,
+			nil,
+			[]string{"findme.txt", "skipme.txt"},
+		},
+	)
+	defer finder.Shutdown()
+
+	foundPaths := finder.FindNamedAt(root, "findme.txt")
+	absoluteMatches := []string{}
+	for i := range expectedMatches {
+		absoluteMatches = append(absoluteMatches, filepath.Join(root, expectedMatches[i]))
+	}
+	assertSameResponse(t, foundPaths, absoluteMatches)
+}
+
+// end of utils, start of individual tests
+
+func TestSingleFile(t *testing.T) {
+	runSimpleTest(t,
+		[]string{"findme.txt"},
+		[]string{"findme.txt"},
+	)
+}
+
+func TestIncludeFiles(t *testing.T) {
+	runSimpleTest(t,
+		[]string{"findme.txt", "skipme.txt"},
+		[]string{"findme.txt"},
+	)
+}
+
+func TestNestedDirectories(t *testing.T) {
+	runSimpleTest(t,
+		[]string{"findme.txt", "skipme.txt", "subdir/findme.txt", "subdir/skipme.txt"},
+		[]string{"findme.txt", "subdir/findme.txt"},
+	)
+}
+
+func TestEmptyDirectory(t *testing.T) {
+	runSimpleTest(t,
+		[]string{},
+		[]string{},
+	)
+}
+
+func TestEmptyPath(t *testing.T) {
+	filesystem := newFs()
+	root := "/tmp"
+	create(t, filepath.Join(root, "findme.txt"), filesystem)
+
+	finder := newFinder(
+		t,
+		filesystem,
+		CacheParams{
+			RootDirs:     []string{root},
+			IncludeFiles: []string{"findme.txt", "skipme.txt"},
+		},
+	)
+	defer finder.Shutdown()
+
+	foundPaths := finder.FindNamedAt("", "findme.txt")
+
+	assertSameResponse(t, foundPaths, []string{})
+}
+
+func TestFilesystemRoot(t *testing.T) {
+	filesystem := newFs()
+	root := "/"
+	createdPath := "/findme.txt"
+	create(t, createdPath, filesystem)
+
+	finder := newFinder(
+		t,
+		filesystem,
+		CacheParams{
+			RootDirs:     []string{root},
+			IncludeFiles: []string{"findme.txt", "skipme.txt"},
+		},
+	)
+	defer finder.Shutdown()
+
+	foundPaths := finder.FindNamedAt(root, "findme.txt")
+
+	assertSameResponse(t, foundPaths, []string{createdPath})
+}
+
+func TestNonexistentPath(t *testing.T) {
+	filesystem := newFs()
+	create(t, "/tmp/findme.txt", filesystem)
+
+	finder := newFinder(
+		t,
+		filesystem,
+		CacheParams{
+			RootDirs:     []string{"/tmp/IDontExist"},
+			IncludeFiles: []string{"findme.txt", "skipme.txt"},
+		},
+	)
+	defer finder.Shutdown()
+
+	foundPaths := finder.FindNamedAt("/tmp/IAlsoDontExist", "findme.txt")
+
+	assertSameResponse(t, foundPaths, []string{})
+}
+
+func TestExcludeDirs(t *testing.T) {
+	filesystem := newFs()
+	create(t, "/tmp/exclude/findme.txt", filesystem)
+	create(t, "/tmp/exclude/subdir/findme.txt", filesystem)
+	create(t, "/tmp/subdir/exclude/findme.txt", filesystem)
+	create(t, "/tmp/subdir/subdir/findme.txt", filesystem)
+	create(t, "/tmp/subdir/findme.txt", filesystem)
+	create(t, "/tmp/findme.txt", filesystem)
+
+	finder := newFinder(
+		t,
+		filesystem,
+		CacheParams{
+			RootDirs:     []string{"/tmp"},
+			ExcludeDirs:  []string{"exclude"},
+			IncludeFiles: []string{"findme.txt", "skipme.txt"},
+		},
+	)
+	defer finder.Shutdown()
+
+	foundPaths := finder.FindNamedAt("/tmp", "findme.txt")
+
+	assertSameResponse(t, foundPaths,
+		[]string{"/tmp/findme.txt",
+			"/tmp/subdir/findme.txt",
+			"/tmp/subdir/subdir/findme.txt"})
+}
+
+func TestPruneFiles(t *testing.T) {
+	filesystem := newFs()
+	create(t, "/tmp/out/findme.txt", filesystem)
+	create(t, "/tmp/out/.ignore-out-dir", filesystem)
+	create(t, "/tmp/out/child/findme.txt", filesystem)
+
+	create(t, "/tmp/out2/.ignore-out-dir", filesystem)
+	create(t, "/tmp/out2/sub/findme.txt", filesystem)
+
+	create(t, "/tmp/findme.txt", filesystem)
+	create(t, "/tmp/include/findme.txt", filesystem)
+
+	finder := newFinder(
+		t,
+		filesystem,
+		CacheParams{
+			RootDirs:     []string{"/tmp"},
+			PruneFiles:   []string{".ignore-out-dir"},
+			IncludeFiles: []string{"findme.txt"},
+		},
+	)
+	defer finder.Shutdown()
+
+	foundPaths := finder.FindNamedAt("/tmp", "findme.txt")
+
+	assertSameResponse(t, foundPaths,
+		[]string{"/tmp/findme.txt",
+			"/tmp/include/findme.txt"})
+}
+
+func TestRootDir(t *testing.T) {
+	filesystem := newFs()
+	create(t, "/tmp/a/findme.txt", filesystem)
+	create(t, "/tmp/a/subdir/findme.txt", filesystem)
+	create(t, "/tmp/b/findme.txt", filesystem)
+	create(t, "/tmp/b/subdir/findme.txt", filesystem)
+
+	finder := newFinder(
+		t,
+		filesystem,
+		CacheParams{
+			RootDirs:     []string{"/tmp/a"},
+			IncludeFiles: []string{"findme.txt"},
+		},
+	)
+	defer finder.Shutdown()
+
+	foundPaths := finder.FindNamedAt("/tmp/a", "findme.txt")
+
+	assertSameResponse(t, foundPaths,
+		[]string{"/tmp/a/findme.txt",
+			"/tmp/a/subdir/findme.txt"})
+}
+
+func TestUncachedDir(t *testing.T) {
+	filesystem := newFs()
+	create(t, "/tmp/a/findme.txt", filesystem)
+	create(t, "/tmp/a/subdir/findme.txt", filesystem)
+	create(t, "/tmp/b/findme.txt", filesystem)
+	create(t, "/tmp/b/subdir/findme.txt", filesystem)
+
+	finder := newFinder(
+		t,
+		filesystem,
+		CacheParams{
+			RootDirs:     []string{"/IDoNotExist"},
+			IncludeFiles: []string{"findme.txt"},
+		},
+	)
+
+	foundPaths := finder.FindNamedAt("/tmp/a", "findme.txt")
+	// If the caller queries for a file that is in the cache, then computing the
+	// correct answer won't be fast, and it would be easy for the caller to
+	// fail to notice its slowness. Instead, we only ever search the cache for files
+	// to return, which enforces that we can determine which files will be
+	// interesting upfront.
+	assertSameResponse(t, foundPaths, []string{})
+
+	finder.Shutdown()
+}
+
+func TestSearchingForFilesExcludedFromCache(t *testing.T) {
+	// setup filesystem
+	filesystem := newFs()
+	create(t, "/tmp/findme.txt", filesystem)
+	create(t, "/tmp/a/findme.txt", filesystem)
+	create(t, "/tmp/a/misc.txt", filesystem)
+
+	// set up the finder and run it
+	finder := newFinder(
+		t,
+		filesystem,
+		CacheParams{
+			RootDirs:     []string{"/tmp"},
+			IncludeFiles: []string{"findme.txt"},
+		},
+	)
+	foundPaths := finder.FindNamedAt("/tmp", "misc.txt")
+	// If the caller queries for a file that is in the cache, then computing the
+	// correct answer won't be fast, and it would be easy for the caller to
+	// fail to notice its slowness. Instead, we only ever search the cache for files
+	// to return, which enforces that we can determine which files will be
+	// interesting upfront.
+	assertSameResponse(t, foundPaths, []string{})
+
+	finder.Shutdown()
+}
+
+func TestRelativeFilePaths(t *testing.T) {
+	filesystem := newFs()
+
+	create(t, "/tmp/ignore/hi.txt", filesystem)
+	create(t, "/tmp/include/hi.txt", filesystem)
+	create(t, "/cwd/hi.txt", filesystem)
+	create(t, "/cwd/a/hi.txt", filesystem)
+	create(t, "/cwd/a/a/hi.txt", filesystem)
+
+	finder := newFinder(
+		t,
+		filesystem,
+		CacheParams{
+			RootDirs:     []string{"/cwd", "/tmp/include"},
+			IncludeFiles: []string{"hi.txt"},
+		},
+	)
+	defer finder.Shutdown()
+
+	foundPaths := finder.FindNamedAt("a", "hi.txt")
+	assertSameResponse(t, foundPaths,
+		[]string{"a/hi.txt",
+			"a/a/hi.txt"})
+
+	foundPaths = finder.FindNamedAt("/tmp/include", "hi.txt")
+	assertSameResponse(t, foundPaths, []string{"/tmp/include/hi.txt"})
+
+	foundPaths = finder.FindNamedAt(".", "hi.txt")
+	assertSameResponse(t, foundPaths,
+		[]string{"hi.txt",
+			"a/hi.txt",
+			"a/a/hi.txt"})
+
+	foundPaths = finder.FindNamedAt("/tmp/include", "hi.txt")
+	assertSameResponse(t, foundPaths, []string{"/tmp/include/hi.txt"})
+}
+
+// have to run this test with the race-detector (`go test -race src/android/soong/finder/*.go`)
+// for there to be much chance of the test actually detecting any error that may be present
+func TestRootDirsContainedInOtherRootDirs(t *testing.T) {
+	filesystem := newFs()
+
+	create(t, "/tmp/a/b/c/d/e/f/g/h/i/j/findme.txt", filesystem)
+
+	finder := newFinder(
+		t,
+		filesystem,
+		CacheParams{
+			RootDirs:     []string{"/", "/a/b/c", "/a/b/c/d/e/f", "/a/b/c/d/e/f/g/h/i"},
+			IncludeFiles: []string{"findme.txt"},
+		},
+	)
+	defer finder.Shutdown()
+
+	foundPaths := finder.FindNamedAt("/tmp/a", "findme.txt")
+
+	assertSameResponse(t, foundPaths,
+		[]string{"/tmp/a/b/c/d/e/f/g/h/i/j/findme.txt"})
+}
+
+func TestFindFirst(t *testing.T) {
+	filesystem := newFs()
+	create(t, "/tmp/a/hi.txt", filesystem)
+	create(t, "/tmp/b/hi.txt", filesystem)
+	create(t, "/tmp/b/a/hi.txt", filesystem)
+
+	finder := newFinder(
+		t,
+		filesystem,
+		CacheParams{
+			RootDirs:     []string{"/tmp"},
+			IncludeFiles: []string{"hi.txt"},
+		},
+	)
+	defer finder.Shutdown()
+
+	foundPaths := finder.FindFirstNamed("hi.txt")
+
+	assertSameResponse(t, foundPaths,
+		[]string{"/tmp/a/hi.txt",
+			"/tmp/b/hi.txt"},
+	)
+}
+
+func TestConcurrentFindSameDirectory(t *testing.T) {
+	filesystem := newFs()
+
+	// create a bunch of files and directories
+	paths := []string{}
+	for i := 0; i < 10; i++ {
+		parentDir := fmt.Sprintf("/tmp/%v", i)
+		for j := 0; j < 10; j++ {
+			filePath := filepath.Join(parentDir, fmt.Sprintf("%v/findme.txt", j))
+			paths = append(paths, filePath)
+		}
+	}
+	sort.Strings(paths)
+	for _, path := range paths {
+		create(t, path, filesystem)
+	}
+
+	// set up a finder
+	finder := newFinder(
+		t,
+		filesystem,
+		CacheParams{
+			RootDirs:     []string{"/tmp"},
+			IncludeFiles: []string{"findme.txt"},
+		},
+	)
+	defer finder.Shutdown()
+
+	numTests := 20
+	results := make(chan []string, numTests)
+	// make several parallel calls to the finder
+	for i := 0; i < numTests; i++ {
+		go func() {
+			foundPaths := finder.FindNamedAt("/tmp", "findme.txt")
+			results <- foundPaths
+		}()
+	}
+
+	// check that each response was correct
+	for i := 0; i < numTests; i++ {
+		foundPaths := <-results
+		assertSameResponse(t, foundPaths, paths)
+	}
+}
+
+func TestConcurrentFindDifferentDirectories(t *testing.T) {
+	filesystem := newFs()
+
+	// create a bunch of files and directories
+	allFiles := []string{}
+	numSubdirs := 10
+	rootPaths := []string{}
+	queryAnswers := [][]string{}
+	for i := 0; i < numSubdirs; i++ {
+		parentDir := fmt.Sprintf("/tmp/%v", i)
+		rootPaths = append(rootPaths, parentDir)
+		queryAnswers = append(queryAnswers, []string{})
+		for j := 0; j < 10; j++ {
+			filePath := filepath.Join(parentDir, fmt.Sprintf("%v/findme.txt", j))
+			queryAnswers[i] = append(queryAnswers[i], filePath)
+			allFiles = append(allFiles, filePath)
+		}
+		sort.Strings(queryAnswers[i])
+	}
+	sort.Strings(allFiles)
+	for _, path := range allFiles {
+		create(t, path, filesystem)
+	}
+
+	// set up a finder
+	finder := newFinder(
+		t,
+		filesystem,
+
+		CacheParams{
+			RootDirs:     []string{"/tmp"},
+			IncludeFiles: []string{"findme.txt"},
+		},
+	)
+	defer finder.Shutdown()
+
+	type testRun struct {
+		path           string
+		foundMatches   []string
+		correctMatches []string
+	}
+
+	numTests := numSubdirs + 1
+	testRuns := make(chan testRun, numTests)
+
+	searchAt := func(path string, correctMatches []string) {
+		foundPaths := finder.FindNamedAt(path, "findme.txt")
+		testRuns <- testRun{path, foundPaths, correctMatches}
+	}
+
+	// make several parallel calls to the finder
+	go searchAt("/tmp", allFiles)
+	for i := 0; i < len(rootPaths); i++ {
+		go searchAt(rootPaths[i], queryAnswers[i])
+	}
+
+	// check that each response was correct
+	for i := 0; i < numTests; i++ {
+		testRun := <-testRuns
+		assertSameResponse(t, testRun.foundMatches, testRun.correctMatches)
+	}
+}
+
+func TestStrangelyFormattedPaths(t *testing.T) {
+	filesystem := newFs()
+
+	create(t, "/tmp/findme.txt", filesystem)
+	create(t, "/tmp/a/findme.txt", filesystem)
+	create(t, "/tmp/b/findme.txt", filesystem)
+
+	finder := newFinder(
+		t,
+		filesystem,
+		CacheParams{
+			RootDirs:     []string{"//tmp//a//.."},
+			IncludeFiles: []string{"findme.txt"},
+		},
+	)
+	defer finder.Shutdown()
+
+	foundPaths := finder.FindNamedAt("//tmp//a//..", "findme.txt")
+
+	assertSameResponse(t, foundPaths,
+		[]string{"/tmp/a/findme.txt",
+			"/tmp/b/findme.txt",
+			"/tmp/findme.txt"})
+}
+
+func TestCorruptedCacheHeader(t *testing.T) {
+	filesystem := newFs()
+
+	create(t, "/tmp/findme.txt", filesystem)
+	create(t, "/tmp/a/findme.txt", filesystem)
+	write(t, "/finder/finder-db", "sample header", filesystem)
+
+	finder := newFinder(
+		t,
+		filesystem,
+		CacheParams{
+			RootDirs:     []string{"/tmp"},
+			IncludeFiles: []string{"findme.txt"},
+		},
+	)
+	defer finder.Shutdown()
+
+	foundPaths := finder.FindNamedAt("/tmp", "findme.txt")
+
+	assertSameResponse(t, foundPaths,
+		[]string{"/tmp/a/findme.txt",
+			"/tmp/findme.txt"})
+}
+
+func TestCanUseCache(t *testing.T) {
+	// setup filesystem
+	filesystem := newFs()
+	create(t, "/tmp/findme.txt", filesystem)
+	create(t, "/tmp/a/findme.txt", filesystem)
+
+	// run the first finder
+	finder := newFinder(
+		t,
+		filesystem,
+		CacheParams{
+			RootDirs:     []string{"/tmp"},
+			IncludeFiles: []string{"findme.txt"},
+		},
+	)
+	foundPaths := finder.FindNamedAt("/tmp", "findme.txt")
+	// check the response of the first finder
+	correctResponse := []string{"/tmp/a/findme.txt",
+		"/tmp/findme.txt"}
+	assertSameResponse(t, foundPaths, correctResponse)
+	finder.Shutdown()
+
+	// check results
+	cacheText := read(t, finder.DbPath, filesystem)
+	if len(cacheText) < 1 {
+		t.Fatalf("saved cache db is empty\n")
+	}
+	if len(filesystem.StatCalls) == 0 {
+		t.Fatal("No Stat calls recorded by mock filesystem")
+	}
+	if len(filesystem.ReadDirCalls) == 0 {
+		t.Fatal("No ReadDir calls recorded by filesystem")
+	}
+	statCalls := filesystem.StatCalls
+	filesystem.ClearMetrics()
+
+	// run the second finder
+	finder2 := finderWithSameParams(t, finder)
+	foundPaths = finder2.FindNamedAt("/tmp", "findme.txt")
+	// check results
+	assertSameReadDirCalls(t, filesystem.ReadDirCalls, []string{})
+	assertSameReadDirCalls(t, filesystem.StatCalls, statCalls)
+
+	finder2.Shutdown()
+}
+
+func TestCorruptedCacheBody(t *testing.T) {
+	// setup filesystem
+	filesystem := newFs()
+	create(t, "/tmp/findme.txt", filesystem)
+	create(t, "/tmp/a/findme.txt", filesystem)
+
+	// run the first finder
+	finder := newFinder(
+		t,
+		filesystem,
+		CacheParams{
+			RootDirs:     []string{"/tmp"},
+			IncludeFiles: []string{"findme.txt"},
+		},
+	)
+	foundPaths := finder.FindNamedAt("/tmp", "findme.txt")
+	finder.Shutdown()
+
+	// check the response of the first finder
+	correctResponse := []string{"/tmp/a/findme.txt",
+		"/tmp/findme.txt"}
+	assertSameResponse(t, foundPaths, correctResponse)
+	numStatCalls := len(filesystem.StatCalls)
+	numReadDirCalls := len(filesystem.ReadDirCalls)
+
+	// load the cache file, corrupt it, and save it
+	cacheReader, err := filesystem.Open(finder.DbPath)
+	if err != nil {
+		t.Fatal(err)
+	}
+	cacheData, err := ioutil.ReadAll(cacheReader)
+	if err != nil {
+		t.Fatal(err)
+	}
+	cacheData = append(cacheData, []byte("DontMindMe")...)
+	filesystem.WriteFile(finder.DbPath, cacheData, 0777)
+	filesystem.ClearMetrics()
+
+	// run the second finder
+	finder2 := finderWithSameParams(t, finder)
+	foundPaths = finder2.FindNamedAt("/tmp", "findme.txt")
+	// check results
+	assertSameResponse(t, foundPaths, correctResponse)
+	numNewStatCalls := len(filesystem.StatCalls)
+	numNewReadDirCalls := len(filesystem.ReadDirCalls)
+	// It's permissable to make more Stat calls with a corrupted cache because
+	// the Finder may restart once it detects corruption.
+	// However, it may have already issued many Stat calls.
+	// Because a corrupted db is not expected to be a common (or even a supported case),
+	// we don't care to optimize it and don't cache the already-issued Stat calls
+	if numNewReadDirCalls < numReadDirCalls {
+		t.Fatalf(
+			"Finder made fewer ReadDir calls with a corrupted cache (%v calls) than with no cache"+
+				" (%v calls)",
+			numNewReadDirCalls, numReadDirCalls)
+	}
+	if numNewStatCalls < numStatCalls {
+		t.Fatalf(
+			"Finder made fewer Stat calls with a corrupted cache (%v calls) than with no cache (%v calls)",
+			numNewStatCalls, numStatCalls)
+	}
+	finder2.Shutdown()
+}
+
+func TestStatCalls(t *testing.T) {
+	// setup filesystem
+	filesystem := newFs()
+	create(t, "/tmp/a/findme.txt", filesystem)
+
+	// run finder
+	finder := newFinder(
+		t,
+		filesystem,
+		CacheParams{
+			RootDirs:     []string{"/tmp"},
+			IncludeFiles: []string{"findme.txt"},
+		},
+	)
+	foundPaths := finder.FindNamedAt("/tmp", "findme.txt")
+	finder.Shutdown()
+
+	// check response
+	assertSameResponse(t, foundPaths, []string{"/tmp/a/findme.txt"})
+	assertSameStatCalls(t, filesystem.StatCalls, []string{"/tmp", "/tmp/a"})
+	assertSameReadDirCalls(t, filesystem.ReadDirCalls, []string{"/tmp", "/tmp/a"})
+}
+
+func TestFileAdded(t *testing.T) {
+	// setup filesystem
+	filesystem := newFs()
+	create(t, "/tmp/ignoreme.txt", filesystem)
+	create(t, "/tmp/a/findme.txt", filesystem)
+	create(t, "/tmp/b/ignore.txt", filesystem)
+	create(t, "/tmp/b/c/nope.txt", filesystem)
+	create(t, "/tmp/b/c/d/irrelevant.txt", filesystem)
+
+	// run the first finder
+	finder := newFinder(
+		t,
+		filesystem,
+		CacheParams{
+			RootDirs:     []string{"/tmp"},
+			IncludeFiles: []string{"findme.txt"},
+		},
+	)
+	foundPaths := finder.FindNamedAt("/tmp", "findme.txt")
+	filesystem.Clock.Tick()
+	finder.Shutdown()
+	// check the response of the first finder
+	assertSameResponse(t, foundPaths, []string{"/tmp/a/findme.txt"})
+
+	// modify the filesystem
+	filesystem.Clock.Tick()
+	create(t, "/tmp/b/c/findme.txt", filesystem)
+	filesystem.Clock.Tick()
+	filesystem.ClearMetrics()
+
+	// run the second finder
+	finder2 := finderWithSameParams(t, finder)
+	foundPaths = finder2.FindNamedAt("/tmp", "findme.txt")
+
+	// check results
+	assertSameResponse(t, foundPaths, []string{"/tmp/a/findme.txt", "/tmp/b/c/findme.txt"})
+	assertSameStatCalls(t, filesystem.StatCalls, []string{"/tmp", "/tmp/a", "/tmp/b", "/tmp/b/c", "/tmp/b/c/d"})
+	assertSameReadDirCalls(t, filesystem.ReadDirCalls, []string{"/tmp/b/c"})
+	finder2.Shutdown()
+
+}
+
+func TestDirectoriesAdded(t *testing.T) {
+	// setup filesystem
+	filesystem := newFs()
+	create(t, "/tmp/ignoreme.txt", filesystem)
+	create(t, "/tmp/a/findme.txt", filesystem)
+	create(t, "/tmp/b/ignore.txt", filesystem)
+	create(t, "/tmp/b/c/nope.txt", filesystem)
+	create(t, "/tmp/b/c/d/irrelevant.txt", filesystem)
+
+	// run the first finder
+	finder := newFinder(
+		t,
+		filesystem,
+		CacheParams{
+			RootDirs:     []string{"/tmp"},
+			IncludeFiles: []string{"findme.txt"},
+		},
+	)
+	foundPaths := finder.FindNamedAt("/tmp", "findme.txt")
+	finder.Shutdown()
+	// check the response of the first finder
+	assertSameResponse(t, foundPaths, []string{"/tmp/a/findme.txt"})
+
+	// modify the filesystem
+	filesystem.Clock.Tick()
+	create(t, "/tmp/b/c/new/findme.txt", filesystem)
+	create(t, "/tmp/b/c/new/new2/findme.txt", filesystem)
+	create(t, "/tmp/b/c/new/new2/ignoreme.txt", filesystem)
+	filesystem.ClearMetrics()
+
+	// run the second finder
+	finder2 := finderWithSameParams(t, finder)
+	foundPaths = finder2.FindNamedAt("/tmp", "findme.txt")
+
+	// check results
+	assertSameResponse(t, foundPaths,
+		[]string{"/tmp/a/findme.txt", "/tmp/b/c/new/findme.txt", "/tmp/b/c/new/new2/findme.txt"})
+	assertSameStatCalls(t, filesystem.StatCalls,
+		[]string{"/tmp", "/tmp/a", "/tmp/b", "/tmp/b/c", "/tmp/b/c/d", "/tmp/b/c/new", "/tmp/b/c/new/new2"})
+	assertSameReadDirCalls(t, filesystem.ReadDirCalls, []string{"/tmp/b/c", "/tmp/b/c/new", "/tmp/b/c/new/new2"})
+
+	finder2.Shutdown()
+}
+
+func TestDirectoryAndSubdirectoryBothUpdated(t *testing.T) {
+	// setup filesystem
+	filesystem := newFs()
+	create(t, "/tmp/hi1.txt", filesystem)
+	create(t, "/tmp/a/hi1.txt", filesystem)
+
+	// run the first finder
+	finder := newFinder(
+		t,
+		filesystem,
+		CacheParams{
+			RootDirs:     []string{"/tmp"},
+			IncludeFiles: []string{"hi1.txt", "hi2.txt"},
+		},
+	)
+	foundPaths := finder.FindNamedAt("/tmp", "hi1.txt")
+	finder.Shutdown()
+	// check the response of the first finder
+	assertSameResponse(t, foundPaths, []string{"/tmp/hi1.txt", "/tmp/a/hi1.txt"})
+
+	// modify the filesystem
+	filesystem.Clock.Tick()
+	create(t, "/tmp/hi2.txt", filesystem)
+	create(t, "/tmp/a/hi2.txt", filesystem)
+	filesystem.ClearMetrics()
+
+	// run the second finder
+	finder2 := finderWithSameParams(t, finder)
+	foundPaths = finder2.FindAll()
+
+	// check results
+	assertSameResponse(t, foundPaths,
+		[]string{"/tmp/hi1.txt", "/tmp/hi2.txt", "/tmp/a/hi1.txt", "/tmp/a/hi2.txt"})
+	assertSameStatCalls(t, filesystem.StatCalls,
+		[]string{"/tmp", "/tmp/a"})
+	assertSameReadDirCalls(t, filesystem.ReadDirCalls, []string{"/tmp", "/tmp/a"})
+
+	finder2.Shutdown()
+}
+
+func TestFileDeleted(t *testing.T) {
+	// setup filesystem
+	filesystem := newFs()
+	create(t, "/tmp/ignoreme.txt", filesystem)
+	create(t, "/tmp/a/findme.txt", filesystem)
+	create(t, "/tmp/b/findme.txt", filesystem)
+	create(t, "/tmp/b/c/nope.txt", filesystem)
+	create(t, "/tmp/b/c/d/irrelevant.txt", filesystem)
+
+	// run the first finder
+	finder := newFinder(
+		t,
+		filesystem,
+		CacheParams{
+			RootDirs:     []string{"/tmp"},
+			IncludeFiles: []string{"findme.txt"},
+		},
+	)
+	foundPaths := finder.FindNamedAt("/tmp", "findme.txt")
+	finder.Shutdown()
+	// check the response of the first finder
+	assertSameResponse(t, foundPaths, []string{"/tmp/a/findme.txt", "/tmp/b/findme.txt"})
+
+	// modify the filesystem
+	filesystem.Clock.Tick()
+	delete(t, "/tmp/b/findme.txt", filesystem)
+	filesystem.ClearMetrics()
+
+	// run the second finder
+	finder2 := finderWithSameParams(t, finder)
+	foundPaths = finder2.FindNamedAt("/tmp", "findme.txt")
+
+	// check results
+	assertSameResponse(t, foundPaths, []string{"/tmp/a/findme.txt"})
+	assertSameStatCalls(t, filesystem.StatCalls, []string{"/tmp", "/tmp/a", "/tmp/b", "/tmp/b/c", "/tmp/b/c/d"})
+	assertSameReadDirCalls(t, filesystem.ReadDirCalls, []string{"/tmp/b"})
+
+	finder2.Shutdown()
+}
+
+func TestDirectoriesDeleted(t *testing.T) {
+	// setup filesystem
+	filesystem := newFs()
+	create(t, "/tmp/findme.txt", filesystem)
+	create(t, "/tmp/a/findme.txt", filesystem)
+	create(t, "/tmp/a/1/findme.txt", filesystem)
+	create(t, "/tmp/a/1/2/findme.txt", filesystem)
+	create(t, "/tmp/b/findme.txt", filesystem)
+
+	// run the first finder
+	finder := newFinder(
+		t,
+		filesystem,
+		CacheParams{
+			RootDirs:     []string{"/tmp"},
+			IncludeFiles: []string{"findme.txt"},
+		},
+	)
+	foundPaths := finder.FindNamedAt("/tmp", "findme.txt")
+	finder.Shutdown()
+	// check the response of the first finder
+	assertSameResponse(t, foundPaths,
+		[]string{"/tmp/findme.txt",
+			"/tmp/a/findme.txt",
+			"/tmp/a/1/findme.txt",
+			"/tmp/a/1/2/findme.txt",
+			"/tmp/b/findme.txt"})
+
+	// modify the filesystem
+	filesystem.Clock.Tick()
+	removeAll(t, "/tmp/a/1", filesystem)
+	filesystem.ClearMetrics()
+
+	// run the second finder
+	finder2 := finderWithSameParams(t, finder)
+	foundPaths = finder2.FindNamedAt("/tmp", "findme.txt")
+
+	// check results
+	assertSameResponse(t, foundPaths,
+		[]string{"/tmp/findme.txt", "/tmp/a/findme.txt", "/tmp/b/findme.txt"})
+	// Technically, we don't care whether /tmp/a/1/2 gets Statted or gets skipped
+	// if the Finder detects the nonexistence of /tmp/a/1
+	// However, when resuming from cache, we don't want the Finder to necessarily wait
+	// to stat a directory until after statting its parent.
+	// So here we just include /tmp/a/1/2 in the list.
+	// The Finder is currently implemented to always restat every dir and
+	// to not short-circuit due to nonexistence of parents (but it will remove
+	// missing dirs from the cache for next time)
+	assertSameStatCalls(t, filesystem.StatCalls,
+		[]string{"/tmp", "/tmp/a", "/tmp/a/1", "/tmp/a/1/2", "/tmp/b"})
+	assertSameReadDirCalls(t, filesystem.ReadDirCalls, []string{"/tmp/a"})
+
+	finder2.Shutdown()
+}
+
+func TestDirectoriesMoved(t *testing.T) {
+	// setup filesystem
+	filesystem := newFs()
+	create(t, "/tmp/findme.txt", filesystem)
+	create(t, "/tmp/a/findme.txt", filesystem)
+	create(t, "/tmp/a/1/findme.txt", filesystem)
+	create(t, "/tmp/a/1/2/findme.txt", filesystem)
+	create(t, "/tmp/b/findme.txt", filesystem)
+
+	// run the first finder
+	finder := newFinder(
+		t,
+		filesystem,
+		CacheParams{
+			RootDirs:     []string{"/tmp"},
+			IncludeFiles: []string{"findme.txt"},
+		},
+	)
+	foundPaths := finder.FindNamedAt("/tmp", "findme.txt")
+	finder.Shutdown()
+	// check the response of the first finder
+	assertSameResponse(t, foundPaths,
+		[]string{"/tmp/findme.txt",
+			"/tmp/a/findme.txt",
+			"/tmp/a/1/findme.txt",
+			"/tmp/a/1/2/findme.txt",
+			"/tmp/b/findme.txt"})
+
+	// modify the filesystem
+	filesystem.Clock.Tick()
+	move(t, "/tmp/a", "/tmp/c", filesystem)
+	filesystem.ClearMetrics()
+
+	// run the second finder
+	finder2 := finderWithSameParams(t, finder)
+	foundPaths = finder2.FindNamedAt("/tmp", "findme.txt")
+
+	// check results
+	assertSameResponse(t, foundPaths,
+		[]string{"/tmp/findme.txt",
+			"/tmp/b/findme.txt",
+			"/tmp/c/findme.txt",
+			"/tmp/c/1/findme.txt",
+			"/tmp/c/1/2/findme.txt"})
+	// Technically, we don't care whether /tmp/a/1/2 gets Statted or gets skipped
+	// if the Finder detects the nonexistence of /tmp/a/1
+	// However, when resuming from cache, we don't want the Finder to necessarily wait
+	// to stat a directory until after statting its parent.
+	// So here we just include /tmp/a/1/2 in the list.
+	// The Finder is currently implemented to always restat every dir and
+	// to not short-circuit due to nonexistence of parents (but it will remove
+	// missing dirs from the cache for next time)
+	assertSameStatCalls(t, filesystem.StatCalls,
+		[]string{"/tmp", "/tmp/a", "/tmp/a/1", "/tmp/a/1/2", "/tmp/b", "/tmp/c", "/tmp/c/1", "/tmp/c/1/2"})
+	assertSameReadDirCalls(t, filesystem.ReadDirCalls, []string{"/tmp", "/tmp/c", "/tmp/c/1", "/tmp/c/1/2"})
+	finder2.Shutdown()
+}
+
+func TestDirectoriesSwapped(t *testing.T) {
+	// setup filesystem
+	filesystem := newFs()
+	create(t, "/tmp/findme.txt", filesystem)
+	create(t, "/tmp/a/findme.txt", filesystem)
+	create(t, "/tmp/a/1/findme.txt", filesystem)
+	create(t, "/tmp/a/1/2/findme.txt", filesystem)
+	create(t, "/tmp/b/findme.txt", filesystem)
+
+	// run the first finder
+	finder := newFinder(
+		t,
+		filesystem,
+		CacheParams{
+			RootDirs:     []string{"/tmp"},
+			IncludeFiles: []string{"findme.txt"},
+		},
+	)
+	foundPaths := finder.FindNamedAt("/tmp", "findme.txt")
+	finder.Shutdown()
+	// check the response of the first finder
+	assertSameResponse(t, foundPaths,
+		[]string{"/tmp/findme.txt",
+			"/tmp/a/findme.txt",
+			"/tmp/a/1/findme.txt",
+			"/tmp/a/1/2/findme.txt",
+			"/tmp/b/findme.txt"})
+
+	// modify the filesystem
+	filesystem.Clock.Tick()
+	move(t, "/tmp/a", "/tmp/temp", filesystem)
+	move(t, "/tmp/b", "/tmp/a", filesystem)
+	move(t, "/tmp/temp", "/tmp/b", filesystem)
+	filesystem.ClearMetrics()
+
+	// run the second finder
+	finder2 := finderWithSameParams(t, finder)
+	foundPaths = finder2.FindNamedAt("/tmp", "findme.txt")
+
+	// check results
+	assertSameResponse(t, foundPaths,
+		[]string{"/tmp/findme.txt",
+			"/tmp/a/findme.txt",
+			"/tmp/b/findme.txt",
+			"/tmp/b/1/findme.txt",
+			"/tmp/b/1/2/findme.txt"})
+	// Technically, we don't care whether /tmp/a/1/2 gets Statted or gets skipped
+	// if the Finder detects the nonexistence of /tmp/a/1
+	// However, when resuming from cache, we don't want the Finder to necessarily wait
+	// to stat a directory until after statting its parent.
+	// So here we just include /tmp/a/1/2 in the list.
+	// The Finder is currently implemented to always restat every dir and
+	// to not short-circuit due to nonexistence of parents (but it will remove
+	// missing dirs from the cache for next time)
+	assertSameStatCalls(t, filesystem.StatCalls,
+		[]string{"/tmp", "/tmp/a", "/tmp/a/1", "/tmp/a/1/2", "/tmp/b", "/tmp/b/1", "/tmp/b/1/2"})
+	assertSameReadDirCalls(t, filesystem.ReadDirCalls, []string{"/tmp", "/tmp/a", "/tmp/b", "/tmp/b/1", "/tmp/b/1/2"})
+	finder2.Shutdown()
+}
+
+// runFsReplacementTest tests a change modifying properties of the filesystem itself:
+// runFsReplacementTest tests changing the user, the hostname, or the device number
+// runFsReplacementTest is a helper method called by other tests
+func runFsReplacementTest(t *testing.T, fs1 *fs.MockFs, fs2 *fs.MockFs) {
+	// setup fs1
+	create(t, "/tmp/findme.txt", fs1)
+	create(t, "/tmp/a/findme.txt", fs1)
+	create(t, "/tmp/a/a/findme.txt", fs1)
+
+	// setup fs2 to have the same directories but different files
+	create(t, "/tmp/findme.txt", fs2)
+	create(t, "/tmp/a/findme.txt", fs2)
+	create(t, "/tmp/a/a/ignoreme.txt", fs2)
+	create(t, "/tmp/a/b/findme.txt", fs2)
+
+	// run the first finder
+	finder := newFinder(
+		t,
+		fs1,
+		CacheParams{
+			RootDirs:     []string{"/tmp"},
+			IncludeFiles: []string{"findme.txt"},
+		},
+	)
+	foundPaths := finder.FindNamedAt("/tmp", "findme.txt")
+	finder.Shutdown()
+	// check the response of the first finder
+	assertSameResponse(t, foundPaths,
+		[]string{"/tmp/findme.txt", "/tmp/a/findme.txt", "/tmp/a/a/findme.txt"})
+
+	// copy the cache data from the first filesystem to the second
+	cacheContent := read(t, finder.DbPath, fs1)
+	write(t, finder.DbPath, cacheContent, fs2)
+
+	// run the second finder, with the same config and same cache contents but a different filesystem
+	finder2 := newFinder(
+		t,
+		fs2,
+		CacheParams{
+			RootDirs:     []string{"/tmp"},
+			IncludeFiles: []string{"findme.txt"},
+		},
+	)
+	foundPaths = finder2.FindNamedAt("/tmp", "findme.txt")
+
+	// check results
+	assertSameResponse(t, foundPaths,
+		[]string{"/tmp/findme.txt", "/tmp/a/findme.txt", "/tmp/a/b/findme.txt"})
+	assertSameStatCalls(t, fs2.StatCalls,
+		[]string{"/tmp", "/tmp/a", "/tmp/a/a", "/tmp/a/b"})
+	assertSameReadDirCalls(t, fs2.ReadDirCalls,
+		[]string{"/tmp", "/tmp/a", "/tmp/a/a", "/tmp/a/b"})
+	finder2.Shutdown()
+}
+
+func TestChangeOfDevice(t *testing.T) {
+	fs1 := newFs()
+	// not as fine-grained mounting controls as a real filesystem, but should be adequate
+	fs1.SetDeviceNumber(0)
+
+	fs2 := newFs()
+	fs2.SetDeviceNumber(1)
+
+	runFsReplacementTest(t, fs1, fs2)
+}
+
+func TestChangeOfUserOrHost(t *testing.T) {
+	fs1 := newFs()
+	fs1.SetViewId("me@here")
+
+	fs2 := newFs()
+	fs2.SetViewId("you@there")
+
+	runFsReplacementTest(t, fs1, fs2)
+}
+
+func TestConsistentCacheOrdering(t *testing.T) {
+	// setup filesystem
+	filesystem := newFs()
+	for i := 0; i < 5; i++ {
+		create(t, fmt.Sprintf("/tmp/%v/findme.txt", i), filesystem)
+	}
+
+	// run the first finder
+	finder := newFinder(
+		t,
+		filesystem,
+		CacheParams{
+			RootDirs:     []string{"/tmp"},
+			IncludeFiles: []string{"findme.txt"},
+		},
+	)
+	finder.FindNamedAt("/tmp", "findme.txt")
+	finder.Shutdown()
+
+	// read db file
+	string1 := read(t, finder.DbPath, filesystem)
+
+	err := filesystem.Remove(finder.DbPath)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// run another finder
+	finder2 := finderWithSameParams(t, finder)
+	finder2.FindNamedAt("/tmp", "findme.txt")
+	finder2.Shutdown()
+
+	string2 := read(t, finder.DbPath, filesystem)
+
+	if string1 != string2 {
+		t.Errorf("Running Finder twice generated two dbs not having identical contents.\n"+
+			"Content of first file:\n"+
+			"\n"+
+			"%v"+
+			"\n"+
+			"\n"+
+			"Content of second file:\n"+
+			"\n"+
+			"%v\n"+
+			"\n",
+			string1,
+			string2,
+		)
+	}
+
+}
+
+func TestNumSyscallsOfSecondFind(t *testing.T) {
+	// setup filesystem
+	filesystem := newFs()
+	create(t, "/tmp/findme.txt", filesystem)
+	create(t, "/tmp/a/findme.txt", filesystem)
+	create(t, "/tmp/a/misc.txt", filesystem)
+
+	// set up the finder and run it once
+	finder := newFinder(
+		t,
+		filesystem,
+		CacheParams{
+			RootDirs:     []string{"/tmp"},
+			IncludeFiles: []string{"findme.txt"},
+		},
+	)
+	foundPaths := finder.FindNamedAt("/tmp", "findme.txt")
+	assertSameResponse(t, foundPaths, []string{"/tmp/findme.txt", "/tmp/a/findme.txt"})
+
+	filesystem.ClearMetrics()
+
+	// run the finder again and confirm it doesn't check the filesystem
+	refoundPaths := finder.FindNamedAt("/tmp", "findme.txt")
+	assertSameResponse(t, refoundPaths, foundPaths)
+	assertSameStatCalls(t, filesystem.StatCalls, []string{})
+	assertSameReadDirCalls(t, filesystem.ReadDirCalls, []string{})
+
+	finder.Shutdown()
+}
+
+func TestChangingParamsOfSecondFind(t *testing.T) {
+	// setup filesystem
+	filesystem := newFs()
+	create(t, "/tmp/findme.txt", filesystem)
+	create(t, "/tmp/a/findme.txt", filesystem)
+	create(t, "/tmp/a/metoo.txt", filesystem)
+
+	// set up the finder and run it once
+	finder := newFinder(
+		t,
+		filesystem,
+		CacheParams{
+			RootDirs:     []string{"/tmp"},
+			IncludeFiles: []string{"findme.txt", "metoo.txt"},
+		},
+	)
+	foundPaths := finder.FindNamedAt("/tmp", "findme.txt")
+	assertSameResponse(t, foundPaths, []string{"/tmp/findme.txt", "/tmp/a/findme.txt"})
+
+	filesystem.ClearMetrics()
+
+	// run the finder again and confirm it gets the right answer without asking the filesystem
+	refoundPaths := finder.FindNamedAt("/tmp", "metoo.txt")
+	assertSameResponse(t, refoundPaths, []string{"/tmp/a/metoo.txt"})
+	assertSameStatCalls(t, filesystem.StatCalls, []string{})
+	assertSameReadDirCalls(t, filesystem.ReadDirCalls, []string{})
+
+	finder.Shutdown()
+}
+
+func TestSymlinkPointingToFile(t *testing.T) {
+	// setup filesystem
+	filesystem := newFs()
+	create(t, "/tmp/a/hi.txt", filesystem)
+	create(t, "/tmp/a/ignoreme.txt", filesystem)
+	link(t, "/tmp/hi.txt", "a/hi.txt", filesystem)
+	link(t, "/tmp/b/hi.txt", "../a/hi.txt", filesystem)
+	link(t, "/tmp/c/hi.txt", "/tmp/hi.txt", filesystem)
+	link(t, "/tmp/d/hi.txt", "../a/bye.txt", filesystem)
+	link(t, "/tmp/d/bye.txt", "../a/hi.txt", filesystem)
+	link(t, "/tmp/e/bye.txt", "../a/bye.txt", filesystem)
+	link(t, "/tmp/f/hi.txt", "somethingThatDoesntExist", filesystem)
+
+	// set up the finder and run it once
+	finder := newFinder(
+		t,
+		filesystem,
+		CacheParams{
+			RootDirs:     []string{"/tmp"},
+			IncludeFiles: []string{"hi.txt"},
+		},
+	)
+	foundPaths := finder.FindNamedAt("/tmp", "hi.txt")
+	// should search based on the name of the link rather than the destination or validity of the link
+	correctResponse := []string{
+		"/tmp/a/hi.txt",
+		"/tmp/hi.txt",
+		"/tmp/b/hi.txt",
+		"/tmp/c/hi.txt",
+		"/tmp/d/hi.txt",
+		"/tmp/f/hi.txt",
+	}
+	assertSameResponse(t, foundPaths, correctResponse)
+
+}
+
+func TestSymlinkPointingToDirectory(t *testing.T) {
+	// setup filesystem
+	filesystem := newFs()
+	create(t, "/tmp/dir/hi.txt", filesystem)
+	create(t, "/tmp/dir/ignoreme.txt", filesystem)
+
+	link(t, "/tmp/links/dir", "../dir", filesystem)
+	link(t, "/tmp/links/link", "../dir", filesystem)
+	link(t, "/tmp/links/broken", "nothingHere", filesystem)
+	link(t, "/tmp/links/recursive", "recursive", filesystem)
+
+	// set up the finder and run it once
+	finder := newFinder(
+		t,
+		filesystem,
+		CacheParams{
+			RootDirs:     []string{"/tmp"},
+			IncludeFiles: []string{"hi.txt"},
+		},
+	)
+
+	foundPaths := finder.FindNamedAt("/tmp", "hi.txt")
+
+	// should completely ignore symlinks that point to directories
+	correctResponse := []string{
+		"/tmp/dir/hi.txt",
+	}
+	assertSameResponse(t, foundPaths, correctResponse)
+
+}
+
+// TestAddPruneFile confirms that adding a prune-file (into a directory for which we
+// already had a cache) causes the directory to be ignored
+func TestAddPruneFile(t *testing.T) {
+	// setup filesystem
+	filesystem := newFs()
+	create(t, "/tmp/out/hi.txt", filesystem)
+	create(t, "/tmp/out/a/hi.txt", filesystem)
+	create(t, "/tmp/hi.txt", filesystem)
+
+	// do find
+	finder := newFinder(
+		t,
+		filesystem,
+		CacheParams{
+			RootDirs:     []string{"/tmp"},
+			PruneFiles:   []string{".ignore-out-dir"},
+			IncludeFiles: []string{"hi.txt"},
+		},
+	)
+
+	foundPaths := finder.FindNamedAt("/tmp", "hi.txt")
+
+	// check result
+	assertSameResponse(t, foundPaths,
+		[]string{"/tmp/hi.txt",
+			"/tmp/out/hi.txt",
+			"/tmp/out/a/hi.txt"},
+	)
+	finder.Shutdown()
+
+	// modify filesystem
+	filesystem.Clock.Tick()
+	create(t, "/tmp/out/.ignore-out-dir", filesystem)
+	// run another find and check its result
+	finder2 := finderWithSameParams(t, finder)
+	foundPaths = finder2.FindNamedAt("/tmp", "hi.txt")
+	assertSameResponse(t, foundPaths, []string{"/tmp/hi.txt"})
+	finder2.Shutdown()
+}
+
+func TestUpdatingDbIffChanged(t *testing.T) {
+	// setup filesystem
+	filesystem := newFs()
+	create(t, "/tmp/a/hi.txt", filesystem)
+	create(t, "/tmp/b/bye.txt", filesystem)
+
+	// run the first finder
+	finder := newFinder(
+		t,
+		filesystem,
+		CacheParams{
+			RootDirs:     []string{"/tmp"},
+			IncludeFiles: []string{"hi.txt"},
+		},
+	)
+	foundPaths := finder.FindAll()
+	filesystem.Clock.Tick()
+	finder.Shutdown()
+	// check results
+	assertSameResponse(t, foundPaths, []string{"/tmp/a/hi.txt"})
+
+	// modify the filesystem
+	filesystem.Clock.Tick()
+	create(t, "/tmp/b/hi.txt", filesystem)
+	filesystem.Clock.Tick()
+	filesystem.ClearMetrics()
+
+	// run the second finder
+	finder2 := finderWithSameParams(t, finder)
+	foundPaths = finder2.FindAll()
+	finder2.Shutdown()
+	// check results
+	assertSameResponse(t, foundPaths, []string{"/tmp/a/hi.txt", "/tmp/b/hi.txt"})
+	assertSameReadDirCalls(t, filesystem.ReadDirCalls, []string{"/tmp/b"})
+	expectedDbWriteTime := filesystem.Clock.Time()
+	actualDbWriteTime := modTime(t, finder2.DbPath, filesystem)
+	if actualDbWriteTime != expectedDbWriteTime {
+		t.Fatalf("Expected to write db at %v, actually wrote db at %v\n",
+			expectedDbWriteTime, actualDbWriteTime)
+	}
+
+	// reset metrics
+	filesystem.ClearMetrics()
+
+	// run the third finder
+	finder3 := finderWithSameParams(t, finder2)
+	foundPaths = finder3.FindAll()
+
+	// check results
+	assertSameResponse(t, foundPaths, []string{"/tmp/a/hi.txt", "/tmp/b/hi.txt"})
+	assertSameReadDirCalls(t, filesystem.ReadDirCalls, []string{})
+	finder3.Shutdown()
+	actualDbWriteTime = modTime(t, finder3.DbPath, filesystem)
+	if actualDbWriteTime != expectedDbWriteTime {
+		t.Fatalf("Re-wrote db even when contents did not change")
+	}
+
+}
+
+func TestDirectoryNotPermitted(t *testing.T) {
+	// setup filesystem
+	filesystem := newFs()
+	create(t, "/tmp/hi.txt", filesystem)
+	create(t, "/tmp/a/hi.txt", filesystem)
+	create(t, "/tmp/a/a/hi.txt", filesystem)
+	create(t, "/tmp/b/hi.txt", filesystem)
+
+	// run the first finder
+	finder := newFinder(
+		t,
+		filesystem,
+		CacheParams{
+			RootDirs:     []string{"/tmp"},
+			IncludeFiles: []string{"hi.txt"},
+		},
+	)
+	foundPaths := finder.FindAll()
+	filesystem.Clock.Tick()
+	finder.Shutdown()
+	allPaths := []string{"/tmp/hi.txt", "/tmp/a/hi.txt", "/tmp/a/a/hi.txt", "/tmp/b/hi.txt"}
+	// check results
+	assertSameResponse(t, foundPaths, allPaths)
+
+	// modify the filesystem
+	filesystem.Clock.Tick()
+
+	setReadable(t, "/tmp/a", false, filesystem)
+	filesystem.Clock.Tick()
+
+	// run the second finder
+	finder2 := finderWithSameParams(t, finder)
+	foundPaths = finder2.FindAll()
+	finder2.Shutdown()
+	// check results
+	assertSameResponse(t, foundPaths, []string{"/tmp/hi.txt", "/tmp/b/hi.txt"})
+
+	// modify the filesystem back
+	setReadable(t, "/tmp/a", true, filesystem)
+
+	// run the third finder
+	finder3 := finderWithSameParams(t, finder2)
+	foundPaths = finder3.FindAll()
+	finder3.Shutdown()
+	// check results
+	assertSameResponse(t, foundPaths, allPaths)
+}
+
+func TestFileNotPermitted(t *testing.T) {
+	// setup filesystem
+	filesystem := newFs()
+	create(t, "/tmp/hi.txt", filesystem)
+	setReadable(t, "/tmp/hi.txt", false, filesystem)
+
+	// run the first finder
+	finder := newFinder(
+		t,
+		filesystem,
+		CacheParams{
+			RootDirs:     []string{"/tmp"},
+			IncludeFiles: []string{"hi.txt"},
+		},
+	)
+	foundPaths := finder.FindAll()
+	filesystem.Clock.Tick()
+	finder.Shutdown()
+	// check results
+	assertSameResponse(t, foundPaths, []string{"/tmp/hi.txt"})
+}