mv internal lib
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package ignore
|
||||
|
||||
import "time"
|
||||
|
||||
type cache struct {
|
||||
patterns []Pattern
|
||||
entries map[string]cacheEntry
|
||||
}
|
||||
|
||||
type cacheEntry struct {
|
||||
value bool
|
||||
access time.Time
|
||||
}
|
||||
|
||||
func newCache(patterns []Pattern) *cache {
|
||||
return &cache{
|
||||
patterns: patterns,
|
||||
entries: make(map[string]cacheEntry),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cache) clean(d time.Duration) {
|
||||
for k, v := range c.entries {
|
||||
if time.Since(v.access) > d {
|
||||
delete(c.entries, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cache) get(key string) (result, ok bool) {
|
||||
res, ok := c.entries[key]
|
||||
if ok {
|
||||
res.access = time.Now()
|
||||
c.entries[key] = res
|
||||
}
|
||||
return res.value, ok
|
||||
}
|
||||
|
||||
func (c *cache) set(key string, val bool) {
|
||||
c.entries[key] = cacheEntry{val, time.Now()}
|
||||
}
|
||||
|
||||
func (c *cache) len() int {
|
||||
l := len(c.entries)
|
||||
return l
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package ignore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCache(t *testing.T) {
|
||||
c := newCache(nil)
|
||||
|
||||
res, ok := c.get("nonexistent")
|
||||
if res != false || ok != false {
|
||||
t.Errorf("res %v, ok %v for nonexistent item", res, ok)
|
||||
}
|
||||
|
||||
// Set and check some items
|
||||
|
||||
c.set("true", true)
|
||||
c.set("false", false)
|
||||
|
||||
res, ok = c.get("true")
|
||||
if res != true || ok != true {
|
||||
t.Errorf("res %v, ok %v for true item", res, ok)
|
||||
}
|
||||
|
||||
res, ok = c.get("false")
|
||||
if res != false || ok != true {
|
||||
t.Errorf("res %v, ok %v for false item", res, ok)
|
||||
}
|
||||
|
||||
// Don't clean anything
|
||||
|
||||
c.clean(time.Second)
|
||||
|
||||
// Same values should exist
|
||||
|
||||
res, ok = c.get("true")
|
||||
if res != true || ok != true {
|
||||
t.Errorf("res %v, ok %v for true item", res, ok)
|
||||
}
|
||||
|
||||
res, ok = c.get("false")
|
||||
if res != false || ok != true {
|
||||
t.Errorf("res %v, ok %v for false item", res, ok)
|
||||
}
|
||||
|
||||
// Sleep and access, to get some data for clean
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
c.get("true")
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// "false" was accessed ~600 ms ago, "true" was accessed ~100 ms ago.
|
||||
// This should clean out "false" but not "true"
|
||||
|
||||
c.clean(300 * time.Millisecond)
|
||||
|
||||
// Same values should exist
|
||||
|
||||
_, ok = c.get("true")
|
||||
if !ok {
|
||||
t.Error("item should still exist")
|
||||
}
|
||||
|
||||
_, ok = c.get("false")
|
||||
if ok {
|
||||
t.Errorf("item should have been cleaned")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package ignore
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/fnmatch"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
)
|
||||
|
||||
type Pattern struct {
|
||||
match *regexp.Regexp
|
||||
include bool
|
||||
}
|
||||
|
||||
func (p Pattern) String() string {
|
||||
if p.include {
|
||||
return p.match.String()
|
||||
}
|
||||
return "(?exclude)" + p.match.String()
|
||||
}
|
||||
|
||||
type Matcher struct {
|
||||
patterns []Pattern
|
||||
withCache bool
|
||||
matches *cache
|
||||
curHash string
|
||||
stop chan struct{}
|
||||
mut sync.Mutex
|
||||
}
|
||||
|
||||
func New(withCache bool) *Matcher {
|
||||
m := &Matcher{
|
||||
withCache: withCache,
|
||||
stop: make(chan struct{}),
|
||||
mut: sync.NewMutex(),
|
||||
}
|
||||
if withCache {
|
||||
go m.clean(2 * time.Hour)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Matcher) Load(file string) error {
|
||||
// No locking, Parse() does the locking
|
||||
|
||||
fd, err := os.Open(file)
|
||||
if err != nil {
|
||||
// We do a parse with empty patterns to clear out the hash, cache etc.
|
||||
m.Parse(&bytes.Buffer{}, file)
|
||||
return err
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
return m.Parse(fd, file)
|
||||
}
|
||||
|
||||
func (m *Matcher) Parse(r io.Reader, file string) error {
|
||||
m.mut.Lock()
|
||||
defer m.mut.Unlock()
|
||||
|
||||
seen := map[string]bool{file: true}
|
||||
patterns, err := parseIgnoreFile(r, file, seen)
|
||||
// Error is saved and returned at the end. We process the patterns
|
||||
// (possibly blank) anyway.
|
||||
|
||||
newHash := hashPatterns(patterns)
|
||||
if newHash == m.curHash {
|
||||
// We've already loaded exactly these patterns.
|
||||
return err
|
||||
}
|
||||
|
||||
m.curHash = newHash
|
||||
m.patterns = patterns
|
||||
if m.withCache {
|
||||
m.matches = newCache(patterns)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Matcher) Match(file string) (result bool) {
|
||||
if m == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
m.mut.Lock()
|
||||
defer m.mut.Unlock()
|
||||
|
||||
if len(m.patterns) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
if m.matches != nil {
|
||||
// Check the cache for a known result.
|
||||
res, ok := m.matches.get(file)
|
||||
if ok {
|
||||
return res
|
||||
}
|
||||
|
||||
// Update the cache with the result at return time
|
||||
defer func() {
|
||||
m.matches.set(file, result)
|
||||
}()
|
||||
}
|
||||
|
||||
// Check all the patterns for a match.
|
||||
for _, pattern := range m.patterns {
|
||||
if pattern.match.MatchString(file) {
|
||||
return pattern.include
|
||||
}
|
||||
}
|
||||
|
||||
// Default to false.
|
||||
return false
|
||||
}
|
||||
|
||||
// Patterns return a list of the loaded regexp patterns, as strings
|
||||
func (m *Matcher) Patterns() []string {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
m.mut.Lock()
|
||||
defer m.mut.Unlock()
|
||||
|
||||
patterns := make([]string, len(m.patterns))
|
||||
for i, pat := range m.patterns {
|
||||
patterns[i] = pat.String()
|
||||
}
|
||||
return patterns
|
||||
}
|
||||
|
||||
func (m *Matcher) Hash() string {
|
||||
m.mut.Lock()
|
||||
defer m.mut.Unlock()
|
||||
return m.curHash
|
||||
}
|
||||
|
||||
func (m *Matcher) Stop() {
|
||||
close(m.stop)
|
||||
}
|
||||
|
||||
func (m *Matcher) clean(d time.Duration) {
|
||||
t := time.NewTimer(d / 2)
|
||||
for {
|
||||
select {
|
||||
case <-m.stop:
|
||||
return
|
||||
case <-t.C:
|
||||
m.mut.Lock()
|
||||
if m.matches != nil {
|
||||
m.matches.clean(d)
|
||||
}
|
||||
t.Reset(d / 2)
|
||||
m.mut.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func hashPatterns(patterns []Pattern) string {
|
||||
h := md5.New()
|
||||
for _, pat := range patterns {
|
||||
h.Write([]byte(pat.String()))
|
||||
h.Write([]byte("\n"))
|
||||
}
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
func loadIgnoreFile(file string, seen map[string]bool) ([]Pattern, error) {
|
||||
if seen[file] {
|
||||
return nil, fmt.Errorf("Multiple include of ignore file %q", file)
|
||||
}
|
||||
seen[file] = true
|
||||
|
||||
fd, err := os.Open(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
return parseIgnoreFile(fd, file, seen)
|
||||
}
|
||||
|
||||
func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) ([]Pattern, error) {
|
||||
var patterns []Pattern
|
||||
|
||||
addPattern := func(line string) error {
|
||||
include := true
|
||||
if strings.HasPrefix(line, "!") {
|
||||
line = line[1:]
|
||||
include = false
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "/") {
|
||||
// Pattern is rooted in the current dir only
|
||||
exp, err := fnmatch.Convert(line[1:], fnmatch.PathName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Invalid pattern %q in ignore file", line)
|
||||
}
|
||||
patterns = append(patterns, Pattern{exp, include})
|
||||
} else if strings.HasPrefix(line, "**/") {
|
||||
// Add the pattern as is, and without **/ so it matches in current dir
|
||||
exp, err := fnmatch.Convert(line, fnmatch.PathName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Invalid pattern %q in ignore file", line)
|
||||
}
|
||||
patterns = append(patterns, Pattern{exp, include})
|
||||
|
||||
exp, err = fnmatch.Convert(line[3:], fnmatch.PathName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Invalid pattern %q in ignore file", line)
|
||||
}
|
||||
patterns = append(patterns, Pattern{exp, include})
|
||||
} else if strings.HasPrefix(line, "#include ") {
|
||||
includeFile := filepath.Join(filepath.Dir(currentFile), line[len("#include "):])
|
||||
includes, err := loadIgnoreFile(includeFile, seen)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
patterns = append(patterns, includes...)
|
||||
} else {
|
||||
// Path name or pattern, add it so it matches files both in
|
||||
// current directory and subdirs.
|
||||
exp, err := fnmatch.Convert(line, fnmatch.PathName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Invalid pattern %q in ignore file", line)
|
||||
}
|
||||
patterns = append(patterns, Pattern{exp, include})
|
||||
|
||||
exp, err = fnmatch.Convert("**/"+line, fnmatch.PathName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Invalid pattern %q in ignore file", line)
|
||||
}
|
||||
patterns = append(patterns, Pattern{exp, include})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(fd)
|
||||
var err error
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
switch {
|
||||
case line == "":
|
||||
continue
|
||||
case strings.HasPrefix(line, "//"):
|
||||
continue
|
||||
case strings.HasPrefix(line, "#"):
|
||||
err = addPattern(line)
|
||||
case strings.HasSuffix(line, "/**"):
|
||||
err = addPattern(line)
|
||||
case strings.HasSuffix(line, "/"):
|
||||
err = addPattern(line)
|
||||
if err == nil {
|
||||
err = addPattern(line + "**")
|
||||
}
|
||||
default:
|
||||
err = addPattern(line)
|
||||
if err == nil {
|
||||
err = addPattern(line + "/**")
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return patterns, nil
|
||||
}
|
||||
@@ -0,0 +1,511 @@
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package ignore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIgnore(t *testing.T) {
|
||||
pats := New(true)
|
||||
err := pats.Load("testdata/.stignore")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var tests = []struct {
|
||||
f string
|
||||
r bool
|
||||
}{
|
||||
{"afile", false},
|
||||
{"bfile", true},
|
||||
{"cfile", false},
|
||||
{"dfile", false},
|
||||
{"efile", true},
|
||||
{"ffile", true},
|
||||
|
||||
{"dir1", false},
|
||||
{filepath.Join("dir1", "cfile"), true},
|
||||
{filepath.Join("dir1", "dfile"), false},
|
||||
{filepath.Join("dir1", "efile"), true},
|
||||
{filepath.Join("dir1", "ffile"), false},
|
||||
|
||||
{"dir2", false},
|
||||
{filepath.Join("dir2", "cfile"), false},
|
||||
{filepath.Join("dir2", "dfile"), true},
|
||||
{filepath.Join("dir2", "efile"), true},
|
||||
{filepath.Join("dir2", "ffile"), false},
|
||||
|
||||
{filepath.Join("dir3"), true},
|
||||
{filepath.Join("dir3", "afile"), true},
|
||||
|
||||
{"lost+found", true},
|
||||
}
|
||||
|
||||
for i, tc := range tests {
|
||||
if r := pats.Match(tc.f); r != tc.r {
|
||||
t.Errorf("Incorrect ignoreFile() #%d (%s); E: %v, A: %v", i, tc.f, tc.r, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExcludes(t *testing.T) {
|
||||
stignore := `
|
||||
!iex2
|
||||
!ign1/ex
|
||||
ign1
|
||||
i*2
|
||||
!ign2
|
||||
`
|
||||
pats := New(true)
|
||||
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var tests = []struct {
|
||||
f string
|
||||
r bool
|
||||
}{
|
||||
{"ign1", true},
|
||||
{"ign2", true},
|
||||
{"ibla2", true},
|
||||
{"iex2", false},
|
||||
{filepath.Join("ign1", "ign"), true},
|
||||
{filepath.Join("ign1", "ex"), false},
|
||||
{filepath.Join("ign1", "iex2"), false},
|
||||
{filepath.Join("iex2", "ign"), false},
|
||||
{filepath.Join("foo", "bar", "ign1"), true},
|
||||
{filepath.Join("foo", "bar", "ign2"), true},
|
||||
{filepath.Join("foo", "bar", "iex2"), false},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
if r := pats.Match(tc.f); r != tc.r {
|
||||
t.Errorf("Incorrect match for %s: %v != %v", tc.f, r, tc.r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBadPatterns(t *testing.T) {
|
||||
var badPatterns = []string{
|
||||
"[",
|
||||
"/[",
|
||||
"**/[",
|
||||
"#include nonexistent",
|
||||
"#include .stignore",
|
||||
"!#include makesnosense",
|
||||
}
|
||||
|
||||
for _, pat := range badPatterns {
|
||||
err := New(true).Parse(bytes.NewBufferString(pat), ".stignore")
|
||||
if err == nil {
|
||||
t.Errorf("No error for pattern %q", pat)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaseSensitivity(t *testing.T) {
|
||||
ign := New(true)
|
||||
err := ign.Parse(bytes.NewBufferString("test"), ".stignore")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
match := []string{"test"}
|
||||
dontMatch := []string{"foo"}
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "darwin", "windows":
|
||||
match = append(match, "TEST", "Test", "tESt")
|
||||
default:
|
||||
dontMatch = append(dontMatch, "TEST", "Test", "tESt")
|
||||
}
|
||||
|
||||
for _, tc := range match {
|
||||
if !ign.Match(tc) {
|
||||
t.Errorf("Incorrect match for %q: should be matched", tc)
|
||||
}
|
||||
}
|
||||
|
||||
for _, tc := range dontMatch {
|
||||
if ign.Match(tc) {
|
||||
t.Errorf("Incorrect match for %q: should not be matched", tc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaching(t *testing.T) {
|
||||
fd1, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fd2, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer fd1.Close()
|
||||
defer fd2.Close()
|
||||
defer os.Remove(fd1.Name())
|
||||
defer os.Remove(fd2.Name())
|
||||
|
||||
_, err = fd1.WriteString("/x/\n#include " + filepath.Base(fd2.Name()) + "\n")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fd2.WriteString("/y/\n")
|
||||
|
||||
pats := New(true)
|
||||
err = pats.Load(fd1.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if pats.matches.len() != 0 {
|
||||
t.Fatal("Expected empty cache")
|
||||
}
|
||||
|
||||
if len(pats.patterns) != 4 {
|
||||
t.Fatal("Incorrect number of patterns loaded", len(pats.patterns), "!=", 4)
|
||||
}
|
||||
|
||||
// Cache some outcomes
|
||||
|
||||
for _, letter := range []string{"a", "b", "x", "y"} {
|
||||
pats.Match(letter)
|
||||
}
|
||||
|
||||
if pats.matches.len() != 4 {
|
||||
t.Fatal("Expected 4 cached results")
|
||||
}
|
||||
|
||||
// Reload file, expect old outcomes to be preserved
|
||||
|
||||
err = pats.Load(fd1.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pats.matches.len() != 4 {
|
||||
t.Fatal("Expected 4 cached results")
|
||||
}
|
||||
|
||||
// Modify the include file, expect empty cache
|
||||
|
||||
fd2.WriteString("/z/\n")
|
||||
|
||||
err = pats.Load(fd1.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if pats.matches.len() != 0 {
|
||||
t.Fatal("Expected 0 cached results")
|
||||
}
|
||||
|
||||
// Cache some outcomes again
|
||||
|
||||
for _, letter := range []string{"b", "x", "y"} {
|
||||
pats.Match(letter)
|
||||
}
|
||||
|
||||
// Verify that outcomes preserved on next laod
|
||||
|
||||
err = pats.Load(fd1.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pats.matches.len() != 3 {
|
||||
t.Fatal("Expected 3 cached results")
|
||||
}
|
||||
|
||||
// Modify the root file, expect cache to be invalidated
|
||||
|
||||
fd1.WriteString("/a/\n")
|
||||
|
||||
err = pats.Load(fd1.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pats.matches.len() != 0 {
|
||||
t.Fatal("Expected cache invalidation")
|
||||
}
|
||||
|
||||
// Cache some outcomes again
|
||||
|
||||
for _, letter := range []string{"b", "x", "y"} {
|
||||
pats.Match(letter)
|
||||
}
|
||||
|
||||
// Verify that outcomes provided on next laod
|
||||
|
||||
err = pats.Load(fd1.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pats.matches.len() != 3 {
|
||||
t.Fatal("Expected 3 cached results")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommentsAndBlankLines(t *testing.T) {
|
||||
stignore := `
|
||||
// foo
|
||||
//bar
|
||||
|
||||
//!baz
|
||||
//#dex
|
||||
|
||||
// ips
|
||||
|
||||
|
||||
`
|
||||
pats := New(true)
|
||||
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(pats.patterns) > 0 {
|
||||
t.Errorf("Expected no patterns")
|
||||
}
|
||||
}
|
||||
|
||||
var result bool
|
||||
|
||||
func BenchmarkMatch(b *testing.B) {
|
||||
stignore := `
|
||||
.frog
|
||||
.frog*
|
||||
.frogfox
|
||||
.whale
|
||||
.whale/*
|
||||
.dolphin
|
||||
.dolphin/*
|
||||
~ferret~.*
|
||||
.ferret.*
|
||||
flamingo.*
|
||||
flamingo
|
||||
*.crow
|
||||
*.crow
|
||||
`
|
||||
pats := New(false)
|
||||
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result = pats.Match("filename")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMatchCached(b *testing.B) {
|
||||
stignore := `
|
||||
.frog
|
||||
.frog*
|
||||
.frogfox
|
||||
.whale
|
||||
.whale/*
|
||||
.dolphin
|
||||
.dolphin/*
|
||||
~ferret~.*
|
||||
.ferret.*
|
||||
flamingo.*
|
||||
flamingo
|
||||
*.crow
|
||||
*.crow
|
||||
`
|
||||
// Caches per file, hence write the patterns to a file.
|
||||
fd, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = fd.WriteString(stignore)
|
||||
defer fd.Close()
|
||||
defer os.Remove(fd.Name())
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
// Load the patterns
|
||||
pats := New(true)
|
||||
err = pats.Load(fd.Name())
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
// Cache the outcome for "filename"
|
||||
pats.Match("filename")
|
||||
|
||||
// This load should now load the cached outcomes as the set of patterns
|
||||
// has not changed.
|
||||
err = pats.Load(fd.Name())
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result = pats.Match("filename")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheReload(t *testing.T) {
|
||||
fd, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer fd.Close()
|
||||
defer os.Remove(fd.Name())
|
||||
|
||||
// Ignore file matches f1 and f2
|
||||
|
||||
_, err = fd.WriteString("f1\nf2\n")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pats := New(true)
|
||||
err = pats.Load(fd.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Verify that both are ignored
|
||||
|
||||
if !pats.Match("f1") {
|
||||
t.Error("Unexpected non-match for f1")
|
||||
}
|
||||
if !pats.Match("f2") {
|
||||
t.Error("Unexpected non-match for f2")
|
||||
}
|
||||
if pats.Match("f3") {
|
||||
t.Error("Unexpected match for f3")
|
||||
}
|
||||
|
||||
// Rewrite file to match f1 and f3
|
||||
|
||||
err = fd.Truncate(0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = fd.Seek(0, os.SEEK_SET)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = fd.WriteString("f1\nf3\n")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = pats.Load(fd.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Verify that the new patterns are in effect
|
||||
|
||||
if !pats.Match("f1") {
|
||||
t.Error("Unexpected non-match for f1")
|
||||
}
|
||||
if pats.Match("f2") {
|
||||
t.Error("Unexpected match for f2")
|
||||
}
|
||||
if !pats.Match("f3") {
|
||||
t.Error("Unexpected non-match for f3")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHash(t *testing.T) {
|
||||
p1 := New(true)
|
||||
err := p1.Load("testdata/.stignore")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Same list of patterns as testdata/.stignore, after expansion
|
||||
stignore := `
|
||||
dir2/dfile
|
||||
dir3
|
||||
bfile
|
||||
dir1/cfile
|
||||
**/efile
|
||||
/ffile
|
||||
lost+found
|
||||
`
|
||||
p2 := New(true)
|
||||
err = p2.Parse(bytes.NewBufferString(stignore), ".stignore")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Not same list of patterns
|
||||
stignore = `
|
||||
dir2/dfile
|
||||
dir3
|
||||
bfile
|
||||
dir1/cfile
|
||||
/ffile
|
||||
lost+found
|
||||
`
|
||||
p3 := New(true)
|
||||
err = p3.Parse(bytes.NewBufferString(stignore), ".stignore")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if p1.Hash() == "" {
|
||||
t.Error("p1 hash blank")
|
||||
}
|
||||
if p2.Hash() == "" {
|
||||
t.Error("p2 hash blank")
|
||||
}
|
||||
if p3.Hash() == "" {
|
||||
t.Error("p3 hash blank")
|
||||
}
|
||||
if p1.Hash() != p2.Hash() {
|
||||
t.Error("p1-p2 hashes differ")
|
||||
}
|
||||
if p1.Hash() == p3.Hash() {
|
||||
t.Error("p1-p3 hashes same")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHashOfEmpty(t *testing.T) {
|
||||
p1 := New(true)
|
||||
err := p1.Load("testdata/.stignore")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
firstHash := p1.Hash()
|
||||
|
||||
// Reloading with a non-existent file should empty the patterns and
|
||||
// recalculate the hash. d41d8cd98f00b204e9800998ecf8427e is the md5 of
|
||||
// nothing.
|
||||
|
||||
p1.Load("file/does/not/exist")
|
||||
secondHash := p1.Hash()
|
||||
|
||||
if firstHash == secondHash {
|
||||
t.Error("hash did not change")
|
||||
}
|
||||
if secondHash != "d41d8cd98f00b204e9800998ecf8427e" {
|
||||
t.Error("second hash is not hash of empty string")
|
||||
}
|
||||
if len(p1.patterns) != 0 {
|
||||
t.Error("there are more than zero patterns")
|
||||
}
|
||||
}
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
#include excludes
|
||||
|
||||
bfile
|
||||
dir1/cfile
|
||||
**/efile
|
||||
/ffile
|
||||
lost+found
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
baz
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
quux
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
dir2/dfile
|
||||
#include further-excludes
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
dir3
|
||||
Reference in New Issue
Block a user