// 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"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"reflect"
	"runtime/debug"
	"sort"
	"testing"
	"time"

	"android/soong/finder/fs"
)

// 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 {
	return newFinderWithNumThreads(t, filesystem, cacheParams, 2)
}

func newFinderWithNumThreads(t *testing.T, filesystem *fs.MockFs, cacheParams CacheParams, numThreads int) *Finder {
	f, err := newFinderAndErr(t, filesystem, cacheParams, numThreads)
	if err != nil {
		fatal(t, err.Error())
	}
	return f
}

func newFinderAndErr(t *testing.T, filesystem *fs.MockFs, cacheParams CacheParams, numThreads int) (*Finder, error) {
	cachePath := "/finder/finder-db"
	cacheDir := filepath.Dir(cachePath)
	filesystem.MkDirs(cacheDir)
	if cacheParams.WorkingDirectory == "" {
		cacheParams.WorkingDirectory = "/cwd"
	}

	logger := log.New(ioutil.Discard, "", 0)
	f, err := newImpl(cacheParams, filesystem, logger, cachePath, numThreads)
	return f, err
}

func finderWithSameParams(t *testing.T, original *Finder) *Finder {
	f, err := finderAndErrorWithSameParams(t, original)
	if err != nil {
		fatal(t, err.Error())
	}
	return f
}

func finderAndErrorWithSameParams(t *testing.T, original *Finder) (*Finder, error) {
	f, err := newImpl(
		original.cacheMetadata.Config.CacheParams,
		original.filesystem,
		original.logger,
		original.DbPath,
		original.numDbLoadingThreads,
	)
	return f, err
}

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 {
		fatal(t, 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 {
		fatal(t, err.Error())
	}
}

func removeAll(t *testing.T, path string, filesystem *fs.MockFs) {
	err := filesystem.RemoveAll(path)
	if err != nil {
		fatal(t, err.Error())
	}
}

func move(t *testing.T, oldPath string, newPath string, filesystem *fs.MockFs) {
	err := filesystem.Rename(oldPath, newPath)
	if err != nil {
		fatal(t, 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 {
		fatal(t, 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 setReadErr(t *testing.T, path string, readErr error, filesystem *fs.MockFs) {
	err := filesystem.SetReadErr(path, readErr)
	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)
}

// testAgainstSeveralThreadcounts runs the given test for each threadcount that we care to test
func testAgainstSeveralThreadcounts(t *testing.T, tester func(t *testing.T, numThreads int)) {
	// test singlethreaded, multithreaded, and also using the same number of threads as
	// will be used on the current system
	threadCounts := []int{1, 2, defaultNumThreads}
	for _, numThreads := range threadCounts {
		testName := fmt.Sprintf("%v threads", numThreads)
		// store numThreads in a new variable to prevent numThreads from changing in each loop
		localNumThreads := numThreads
		t.Run(testName, func(t *testing.T) {
			tester(t, localNumThreads)
		})
	}
}

// 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) {

	testWithNumThreads := func(t *testing.T, numThreads int) {
		filesystem := newFs()
		root := "/"
		createdPath := "/findme.txt"
		create(t, createdPath, filesystem)

		finder := newFinderWithNumThreads(
			t,
			filesystem,
			CacheParams{
				RootDirs:     []string{root},
				IncludeFiles: []string{"findme.txt", "skipme.txt"},
			},
			numThreads,
		)
		defer finder.Shutdown()

		foundPaths := finder.FindNamedAt(root, "findme.txt")

		assertSameResponse(t, foundPaths, []string{createdPath})
	}

	testAgainstSeveralThreadcounts(t, testWithNumThreads)
}

func TestNonexistentDir(t *testing.T) {
	filesystem := newFs()
	create(t, "/tmp/findme.txt", filesystem)

	_, err := newFinderAndErr(
		t,
		filesystem,
		CacheParams{
			RootDirs:     []string{"/tmp/IDontExist"},
			IncludeFiles: []string{"findme.txt", "skipme.txt"},
		},
		1,
	)
	if err == nil {
		fatal(t, "Did not fail when given a nonexistent root directory")
	}
}

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"})
}

// TestRootDir tests that the value of RootDirs is used
// tests of the filesystem root are in TestFilesystemRoot
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{"/tmp/b"},
			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)
	create(t, "/rel/a/hi.txt", filesystem)

	finder := newFinder(
		t,
		filesystem,
		CacheParams{
			RootDirs:     []string{"/cwd", "../rel", "/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("/rel", "hi.txt")
	assertSameResponse(t, foundPaths,
		[]string{"/rel/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{"/", "/tmp/a/b/c", "/tmp/a/b/c/d/e/f", "/tmp/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) {

	testWithNumThreads := func(t *testing.T, numThreads int) {
		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 := newFinderWithNumThreads(
			t,
			filesystem,
			CacheParams{
				RootDirs:     []string{"/tmp"},
				IncludeFiles: []string{"findme.txt"},
			},
			numThreads,
		)
		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)
		}
	}

	testAgainstSeveralThreadcounts(t, testWithNumThreads)
}

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"})
}

func TestCacheEntryPathUnexpectedError(t *testing.T) {
	// setup filesystem
	filesystem := newFs()
	create(t, "/tmp/a/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()
	// check results
	assertSameResponse(t, foundPaths, []string{"/tmp/a/hi.txt"})

	// make the directory not readable
	setReadErr(t, "/tmp/a", os.ErrInvalid, filesystem)

	// run the second finder
	_, err := finderAndErrorWithSameParams(t, finder)
	if err == nil {
		fatal(t, "Failed to detect unexpected filesystem error")
	}
}