mirror of
https://github.com/octoleo/syncthing.git
synced 2024-12-22 19:08:58 +00:00
605fd6d726
In ignores, normalize the input when parsing it. When scanning, normalize earlier such that the path is already normalized when checking ignores. This requires splitting normalization of the string from normalization of the file, as we don't want to attempt the latter if the file is ignored. Closes #9598 --------- Co-authored-by: Jakob Borg <jakob@kastelo.net>
1348 lines
26 KiB
Go
1348 lines
26 KiB
Go
// 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 https://mozilla.org/MPL/2.0/.
|
|
|
|
package ignore
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/syncthing/syncthing/lib/build"
|
|
"github.com/syncthing/syncthing/lib/fs"
|
|
"github.com/syncthing/syncthing/lib/ignore/ignoreresult"
|
|
"github.com/syncthing/syncthing/lib/osutil"
|
|
"github.com/syncthing/syncthing/lib/rand"
|
|
)
|
|
|
|
var testFiles = map[string]string{
|
|
".stignore": `#include excludes
|
|
bfile
|
|
dir1/cfile
|
|
**/efile
|
|
/ffile
|
|
lost+found
|
|
`,
|
|
"excludes": "dir2/dfile\n#include further-excludes\n",
|
|
"further-excludes": "dir3\n",
|
|
}
|
|
|
|
func newTestFS() fs.Filesystem {
|
|
testFS := fs.NewFilesystem(fs.FilesystemTypeFake, rand.String(32)+"?content=true&nostfolder=true")
|
|
|
|
// Add some data expected by the tests, previously existing on disk.
|
|
testFS.Mkdir("dir3", 0o777)
|
|
for name, content := range testFiles {
|
|
fs.WriteFile(testFS, name, []byte(content), 0o666)
|
|
}
|
|
|
|
return testFS
|
|
}
|
|
|
|
func TestIgnore(t *testing.T) {
|
|
testFs := newTestFS()
|
|
|
|
pats := New(testFs, WithCache(true))
|
|
err := pats.Load(".stignore")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
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.IsIgnored() != tc.r {
|
|
t.Errorf("Incorrect ignoreFile() #%d (%s); E: %v, A: %v", i, tc.f, tc.r, r)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExcludes(t *testing.T) {
|
|
testFs := newTestFS()
|
|
|
|
stignore := `
|
|
!iex2
|
|
!ign1/ex
|
|
ign1
|
|
i*2
|
|
!ign2
|
|
`
|
|
pats := New(testFs, WithCache(true))
|
|
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
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.IsIgnored() != tc.r {
|
|
t.Errorf("Incorrect match for %s: %v != %v", tc.f, r, tc.r)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFlagOrder(t *testing.T) {
|
|
testFs := newTestFS()
|
|
|
|
stignore := `
|
|
## Ok cases
|
|
(?i)(?d)!ign1
|
|
(?d)(?i)!ign2
|
|
(?i)!(?d)ign3
|
|
(?d)!(?i)ign4
|
|
!(?i)(?d)ign5
|
|
!(?d)(?i)ign6
|
|
## Bad cases
|
|
!!(?i)(?d)ign7
|
|
(?i)(?i)(?d)ign8
|
|
(?i)(?d)(?d)!ign9
|
|
(?d)(?d)!ign10
|
|
`
|
|
pats := New(testFs, WithCache(true))
|
|
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for i := 1; i < 7; i++ {
|
|
pat := fmt.Sprintf("ign%d", i)
|
|
if r := pats.Match(pat); r.IsIgnored() || r.IsDeletable() {
|
|
t.Errorf("incorrect %s", pat)
|
|
}
|
|
}
|
|
for i := 7; i < 10; i++ {
|
|
pat := fmt.Sprintf("ign%d", i)
|
|
if r := pats.Match(pat); r.IsDeletable() {
|
|
t.Errorf("incorrect %s", pat)
|
|
}
|
|
}
|
|
|
|
if r := pats.Match("(?d)!ign10"); !r.IsIgnored() {
|
|
t.Errorf("incorrect")
|
|
}
|
|
}
|
|
|
|
func TestDeletables(t *testing.T) {
|
|
testFs := newTestFS()
|
|
|
|
stignore := `
|
|
(?d)ign1
|
|
(?d)(?i)ign2
|
|
(?i)(?d)ign3
|
|
!(?d)ign4
|
|
!ign5
|
|
!(?i)(?d)ign6
|
|
ign7
|
|
(?i)ign8
|
|
`
|
|
pats := New(testFs, WithCache(true))
|
|
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
tests := []struct {
|
|
f string
|
|
i bool
|
|
d bool
|
|
}{
|
|
{"ign1", true, true},
|
|
{"ign2", true, true},
|
|
{"ign3", true, true},
|
|
{"ign4", false, false},
|
|
{"ign5", false, false},
|
|
{"ign6", false, false},
|
|
{"ign7", true, false},
|
|
{"ign8", true, false},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
if r := pats.Match(tc.f); r.IsIgnored() != tc.i || r.IsDeletable() != tc.d {
|
|
t.Errorf("Incorrect match for %s: %v != Result{%t, %t}", tc.f, r, tc.i, tc.d)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBadPatterns(t *testing.T) {
|
|
testFs := newTestFS()
|
|
|
|
t.Skip("to fix: bad pattern not happening")
|
|
badPatterns := []string{
|
|
"[",
|
|
"/[",
|
|
"**/[",
|
|
"#include nonexistent",
|
|
"#include .stignore",
|
|
}
|
|
|
|
for _, pat := range badPatterns {
|
|
err := New(testFs, WithCache(true)).Parse(bytes.NewBufferString(pat), ".stignore")
|
|
if err == nil {
|
|
t.Errorf("No error for pattern %q", pat)
|
|
}
|
|
if !IsParseError(err) {
|
|
t.Error("Should have been a parse error:", err)
|
|
}
|
|
if strings.HasPrefix(pat, "#include") {
|
|
if fs.IsNotExist(err) {
|
|
t.Error("Includes should not toss a regular isNotExist error")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCaseSensitivity(t *testing.T) {
|
|
testFs := newTestFS()
|
|
|
|
ign := New(testFs, WithCache(true))
|
|
err := ign.Parse(bytes.NewBufferString("test"), ".stignore")
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
match := []string{"test"}
|
|
dontMatch := []string{"foo"}
|
|
|
|
if build.IsDarwin || build.IsWindows {
|
|
match = append(match, "TEST", "Test", "tESt")
|
|
} else {
|
|
dontMatch = append(dontMatch, "TEST", "Test", "tESt")
|
|
}
|
|
|
|
for _, tc := range match {
|
|
if !ign.Match(tc).IsIgnored() {
|
|
t.Errorf("Incorrect match for %q: should be matched", tc)
|
|
}
|
|
}
|
|
|
|
for _, tc := range dontMatch {
|
|
if ign.Match(tc).IsIgnored() {
|
|
t.Errorf("Incorrect match for %q: should not be matched", tc)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCaching(t *testing.T) {
|
|
fs := fs.NewFilesystem(fs.FilesystemTypeFake, rand.String(32)+"?content=true")
|
|
|
|
fd1, err := osutil.TempFile(fs, "", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
fd2, err := osutil.TempFile(fs, "", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
defer fd1.Close()
|
|
defer fd2.Close()
|
|
defer fs.Remove(fd1.Name())
|
|
defer fs.Remove(fd2.Name())
|
|
|
|
_, err = fd1.Write([]byte("/x/\n#include " + filepath.Base(fd2.Name()) + "\n"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
fd2.Write([]byte("/y/\n"))
|
|
|
|
pats := New(fs, WithCache(true))
|
|
err = pats.Load(fd1.Name())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if pats.matches.len() != 0 {
|
|
t.Fatal("Expected empty cache")
|
|
}
|
|
|
|
// 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. Ensure the timestamp on
|
|
// the file changes.
|
|
|
|
fd2.Write([]byte("/z/\n"))
|
|
fd2.Sync()
|
|
fakeTime := time.Now().Add(5 * time.Second)
|
|
fs.Chtimes(fd2.Name(), fakeTime, fakeTime)
|
|
|
|
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 load
|
|
|
|
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.Write([]byte("/a/\n"))
|
|
fd1.Sync()
|
|
fakeTime = time.Now().Add(5 * time.Second)
|
|
fs.Chtimes(fd1.Name(), fakeTime, fakeTime)
|
|
|
|
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 load
|
|
|
|
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) {
|
|
testFs := newTestFS()
|
|
|
|
stignore := `
|
|
// foo
|
|
//bar
|
|
|
|
//!baz
|
|
//#dex
|
|
|
|
// ips
|
|
|
|
|
|
`
|
|
pats := New(testFs, WithCache(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 ignoreresult.R
|
|
|
|
func BenchmarkMatch(b *testing.B) {
|
|
testFs := newTestFS()
|
|
|
|
stignore := `
|
|
.frog
|
|
.frog*
|
|
.frogfox
|
|
.whale
|
|
.whale/*
|
|
.dolphin
|
|
.dolphin/*
|
|
~ferret~.*
|
|
.ferret.*
|
|
flamingo.*
|
|
flamingo
|
|
*.crow
|
|
*.crow
|
|
`
|
|
pats := New(testFs)
|
|
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.
|
|
|
|
fs := fs.NewFilesystem(fs.FilesystemTypeFake, rand.String(32)+"?content=true")
|
|
|
|
fd, err := osutil.TempFile(fs, "", "")
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
|
|
_, err = fd.Write([]byte(stignore))
|
|
defer fd.Close()
|
|
defer fs.Remove(fd.Name())
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
|
|
// Load the patterns
|
|
pats := New(fs, WithCache(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) {
|
|
fs := fs.NewFilesystem(fs.FilesystemTypeFake, rand.String(32)+"?content=true")
|
|
|
|
fd, err := osutil.TempFile(fs, "", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
defer fd.Close()
|
|
defer fs.Remove(fd.Name())
|
|
|
|
// Ignore file matches f1 and f2
|
|
|
|
_, err = fd.Write([]byte("f1\nf2\n"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
pats := New(fs, WithCache(true))
|
|
err = pats.Load(fd.Name())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Verify that both are ignored
|
|
|
|
if !pats.Match("f1").IsIgnored() {
|
|
t.Error("Unexpected non-match for f1")
|
|
}
|
|
if !pats.Match("f2").IsIgnored() {
|
|
t.Error("Unexpected non-match for f2")
|
|
}
|
|
if pats.Match("f3").IsIgnored() {
|
|
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, io.SeekStart)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err = fd.Write([]byte("f1\nf3\n"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fd.Sync()
|
|
fakeTime := time.Now().Add(5 * time.Second)
|
|
fs.Chtimes(fd.Name(), fakeTime, fakeTime)
|
|
|
|
err = pats.Load(fd.Name())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Verify that the new patterns are in effect
|
|
|
|
if !pats.Match("f1").IsIgnored() {
|
|
t.Error("Unexpected non-match for f1")
|
|
}
|
|
if pats.Match("f2").IsIgnored() {
|
|
t.Error("Unexpected match for f2")
|
|
}
|
|
if !pats.Match("f3").IsIgnored() {
|
|
t.Error("Unexpected non-match for f3")
|
|
}
|
|
}
|
|
|
|
func TestHash(t *testing.T) {
|
|
testFs := newTestFS()
|
|
|
|
p1 := New(testFs, WithCache(true))
|
|
err := p1.Load(".stignore")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Same list of patterns as .stignore, after expansion
|
|
stignore := `
|
|
dir2/dfile
|
|
dir3
|
|
bfile
|
|
dir1/cfile
|
|
**/efile
|
|
/ffile
|
|
lost+found
|
|
`
|
|
p2 := New(testFs, WithCache(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(testFs, WithCache(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) {
|
|
testFs := newTestFS()
|
|
|
|
p1 := New(testFs, WithCache(true))
|
|
|
|
err := p1.Load(".stignore")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
firstHash := p1.Hash()
|
|
|
|
// Reloading with a non-existent file should empty the patterns and
|
|
// recalculate the hash.
|
|
// e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 is
|
|
// the sah256 of nothing.
|
|
|
|
p1.Load("file/does/not/exist")
|
|
secondHash := p1.Hash()
|
|
|
|
if firstHash == secondHash {
|
|
t.Error("hash did not change")
|
|
}
|
|
if secondHash != "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" {
|
|
t.Error("second hash is not hash of empty string")
|
|
}
|
|
if len(p1.patterns) != 0 {
|
|
t.Error("there are more than zero patterns")
|
|
}
|
|
}
|
|
|
|
func TestWindowsPatterns(t *testing.T) {
|
|
testFs := newTestFS()
|
|
|
|
// We should accept patterns as both a/b and a\b and match that against
|
|
// both kinds of slash as well.
|
|
if !build.IsWindows {
|
|
t.Skip("Windows specific test")
|
|
return
|
|
}
|
|
|
|
stignore := `
|
|
a/b
|
|
c\d
|
|
`
|
|
pats := New(testFs, WithCache(true))
|
|
|
|
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
tests := []string{`a\b`, `c\d`}
|
|
for _, pat := range tests {
|
|
if !pats.Match(pat).IsIgnored() {
|
|
t.Errorf("Should match %s", pat)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAutomaticCaseInsensitivity(t *testing.T) {
|
|
testFs := newTestFS()
|
|
|
|
// We should do case insensitive matching by default on some platforms.
|
|
if !build.IsWindows && !build.IsDarwin {
|
|
t.Skip("Windows/Mac specific test")
|
|
return
|
|
}
|
|
|
|
stignore := `
|
|
A/B
|
|
c/d
|
|
`
|
|
pats := New(testFs, WithCache(true))
|
|
|
|
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
tests := []string{`a/B`, `C/d`}
|
|
for _, pat := range tests {
|
|
if !pats.Match(pat).IsIgnored() {
|
|
t.Errorf("Should match %s", pat)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCommas(t *testing.T) {
|
|
testFs := newTestFS()
|
|
|
|
stignore := `
|
|
foo,bar.txt
|
|
{baz,quux}.txt
|
|
`
|
|
pats := New(testFs, WithCache(true))
|
|
|
|
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
match bool
|
|
}{
|
|
{"foo.txt", false},
|
|
{"bar.txt", false},
|
|
{"foo,bar.txt", true},
|
|
{"baz.txt", true},
|
|
{"quux.txt", true},
|
|
{"baz,quux.txt", false},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
if pats.Match(tc.name).IsIgnored() != tc.match {
|
|
t.Errorf("Match of %s was %v, should be %v", tc.name, !tc.match, tc.match)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIssue3164(t *testing.T) {
|
|
testFs := newTestFS()
|
|
|
|
stignore := `
|
|
(?d)(?i)*.part
|
|
(?d)(?i)/foo
|
|
(?d)(?i)**/bar
|
|
`
|
|
pats := New(testFs, WithCache(true))
|
|
|
|
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
expanded := pats.Patterns()
|
|
t.Log(expanded)
|
|
expected := []string{
|
|
"(?d)(?i)*.part",
|
|
"(?d)(?i)**/*.part",
|
|
"(?d)(?i)*.part/**",
|
|
"(?d)(?i)**/*.part/**",
|
|
"(?d)(?i)/foo",
|
|
"(?d)(?i)/foo/**",
|
|
"(?d)(?i)**/bar",
|
|
"(?d)(?i)bar",
|
|
"(?d)(?i)**/bar/**",
|
|
"(?d)(?i)bar/**",
|
|
}
|
|
|
|
if len(expanded) != len(expected) {
|
|
t.Errorf("Unmatched count: %d != %d", len(expanded), len(expected))
|
|
}
|
|
|
|
for i := range expanded {
|
|
if expanded[i] != expected[i] {
|
|
t.Errorf("Pattern %d does not match: %s != %s", i, expanded[i], expected[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIssue3174(t *testing.T) {
|
|
testFs := newTestFS()
|
|
|
|
stignore := `
|
|
*ä*
|
|
`
|
|
pats := New(testFs, WithCache(true))
|
|
|
|
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// The pattern above is normalized when parsing, and in order for this
|
|
// string to match the pattern, it needs to use the same normalization. And
|
|
// Go always uses NFC regardless of OS, while we use NFD on macos.
|
|
if !pats.Match(nativeUnicodeNorm("åäö")).IsIgnored() {
|
|
t.Error("Should match")
|
|
}
|
|
}
|
|
|
|
func TestIssue3639(t *testing.T) {
|
|
testFs := newTestFS()
|
|
|
|
stignore := `
|
|
foo/
|
|
`
|
|
pats := New(testFs, WithCache(true))
|
|
|
|
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if !pats.Match("foo/bar").IsIgnored() {
|
|
t.Error("Should match 'foo/bar'")
|
|
}
|
|
|
|
if pats.Match("foo").IsIgnored() {
|
|
t.Error("Should not match 'foo'")
|
|
}
|
|
}
|
|
|
|
func TestIssue3674(t *testing.T) {
|
|
testFs := newTestFS()
|
|
|
|
stignore := `
|
|
a*b
|
|
a**c
|
|
`
|
|
|
|
testcases := []struct {
|
|
file string
|
|
matches bool
|
|
}{
|
|
{"ab", true},
|
|
{"asdfb", true},
|
|
{"ac", true},
|
|
{"asdfc", true},
|
|
{"as/db", false},
|
|
{"as/dc", true},
|
|
}
|
|
|
|
pats := New(testFs, WithCache(true))
|
|
|
|
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, tc := range testcases {
|
|
res := pats.Match(tc.file).IsIgnored()
|
|
if res != tc.matches {
|
|
t.Errorf("Matches(%q) == %v, expected %v", tc.file, res, tc.matches)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGobwasGlobIssue18(t *testing.T) {
|
|
testFs := newTestFS()
|
|
|
|
stignore := `
|
|
a?b
|
|
bb?
|
|
`
|
|
|
|
testcases := []struct {
|
|
file string
|
|
matches bool
|
|
}{
|
|
{"ab", false},
|
|
{"acb", true},
|
|
{"asdb", false},
|
|
{"bb", false},
|
|
{"bba", true},
|
|
{"bbaa", false},
|
|
}
|
|
|
|
pats := New(testFs, WithCache(true))
|
|
|
|
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, tc := range testcases {
|
|
res := pats.Match(tc.file).IsIgnored()
|
|
if res != tc.matches {
|
|
t.Errorf("Matches(%q) == %v, expected %v", tc.file, res, tc.matches)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRoot(t *testing.T) {
|
|
testFs := newTestFS()
|
|
|
|
stignore := `
|
|
!/a
|
|
/*
|
|
`
|
|
|
|
testcases := []struct {
|
|
file string
|
|
matches bool
|
|
}{
|
|
{".", false},
|
|
{"a", false},
|
|
{"b", true},
|
|
}
|
|
|
|
pats := New(testFs, WithCache(true))
|
|
|
|
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, tc := range testcases {
|
|
res := pats.Match(tc.file).IsIgnored()
|
|
if res != tc.matches {
|
|
t.Errorf("Matches(%q) == %v, expected %v", tc.file, res, tc.matches)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLines(t *testing.T) {
|
|
testFs := newTestFS()
|
|
|
|
stignore := `
|
|
#include excludes
|
|
|
|
!/a
|
|
/*
|
|
!/a
|
|
`
|
|
|
|
pats := New(testFs, WithCache(true))
|
|
|
|
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
expectedLines := []string{
|
|
"",
|
|
"#include excludes",
|
|
"",
|
|
"!/a",
|
|
"/*",
|
|
"!/a",
|
|
"",
|
|
}
|
|
|
|
lines := pats.Lines()
|
|
if len(lines) != len(expectedLines) {
|
|
t.Fatalf("len(Lines()) == %d, expected %d", len(lines), len(expectedLines))
|
|
}
|
|
for i := range lines {
|
|
if lines[i] != expectedLines[i] {
|
|
t.Fatalf("Lines()[%d] == %s, expected %s", i, lines[i], expectedLines[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDuplicateLines(t *testing.T) {
|
|
testFs := newTestFS()
|
|
|
|
stignore := `
|
|
!/a
|
|
/*
|
|
!/a
|
|
`
|
|
stignoreFiltered := `
|
|
!/a
|
|
/*
|
|
`
|
|
|
|
pats := New(testFs, WithCache(true))
|
|
|
|
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
patsLen := len(pats.patterns)
|
|
|
|
err = pats.Parse(bytes.NewBufferString(stignoreFiltered), ".stignore")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if patsLen != len(pats.patterns) {
|
|
t.Fatalf("Parsed patterns differ when manually removing duplicate lines")
|
|
}
|
|
}
|
|
|
|
func TestIssue4680(t *testing.T) {
|
|
testFs := newTestFS()
|
|
|
|
stignore := `
|
|
#snapshot
|
|
`
|
|
|
|
testcases := []struct {
|
|
file string
|
|
matches bool
|
|
}{
|
|
{"#snapshot", true},
|
|
{"#snapshot/foo", true},
|
|
}
|
|
|
|
pats := New(testFs, WithCache(true))
|
|
|
|
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, tc := range testcases {
|
|
res := pats.Match(tc.file).IsIgnored()
|
|
if res != tc.matches {
|
|
t.Errorf("Matches(%q) == %v, expected %v", tc.file, res, tc.matches)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIssue4689(t *testing.T) {
|
|
testFs := newTestFS()
|
|
|
|
stignore := `// orig`
|
|
|
|
pats := New(testFs, WithCache(true))
|
|
|
|
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if lines := pats.Lines(); len(lines) != 1 || lines[0] != "// orig" {
|
|
t.Fatalf("wrong lines parsing original comment:\n%q", lines)
|
|
}
|
|
|
|
stignore = `// new`
|
|
|
|
err = pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if lines := pats.Lines(); len(lines) != 1 || lines[0] != "// new" {
|
|
t.Fatalf("wrong lines parsing changed comment:\n%v", lines)
|
|
}
|
|
}
|
|
|
|
func TestIssue4901(t *testing.T) {
|
|
testFs := newTestFS()
|
|
|
|
stignore := `
|
|
#include unicorn-lazor-death
|
|
puppy
|
|
`
|
|
|
|
pats := New(testFs, WithCache(true))
|
|
|
|
fd, err := pats.fs.Create(".stignore")
|
|
if err != nil {
|
|
t.Fatalf(err.Error())
|
|
}
|
|
if _, err := fd.Write([]byte(stignore)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Cache does not suddenly make the load succeed.
|
|
for i := 0; i < 2; i++ {
|
|
err := pats.Load(".stignore")
|
|
if err == nil {
|
|
t.Fatal("expected an error")
|
|
}
|
|
if err == fs.ErrNotExist {
|
|
t.Fatalf("unexpected error type: %T", err)
|
|
}
|
|
if !IsParseError(err) {
|
|
t.Fatal("failure to load included file should be a parse error")
|
|
}
|
|
}
|
|
|
|
fd, err = pats.fs.Create("unicorn-lazor-death")
|
|
if err != nil {
|
|
t.Fatalf(err.Error())
|
|
}
|
|
if _, err := fd.Write([]byte(" ")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = pats.Load(".stignore")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
// TestIssue5009 checks that ignored dirs are only skipped if there are no include patterns.
|
|
// https://github.com/syncthing/syncthing/issues/5009 (rc-only bug)
|
|
func TestIssue5009(t *testing.T) {
|
|
testFs := newTestFS()
|
|
|
|
pats := New(testFs, WithCache(true))
|
|
|
|
stignore := `
|
|
ign1
|
|
i*2
|
|
`
|
|
if err := pats.Parse(bytes.NewBufferString(stignore), ".stignore"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if m := pats.Match("ign2"); !m.CanSkipDir() {
|
|
t.Error("CanSkipDir should be true without excludes")
|
|
}
|
|
|
|
stignore = `
|
|
!iex2
|
|
!ign1/ex
|
|
ign1
|
|
i*2
|
|
!ign2
|
|
`
|
|
|
|
if err := pats.Parse(bytes.NewBufferString(stignore), ".stignore"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if m := pats.Match("ign2"); m.CanSkipDir() {
|
|
t.Error("CanSkipDir should not be true with excludes")
|
|
}
|
|
}
|
|
|
|
func TestSpecialChars(t *testing.T) {
|
|
testFs := newTestFS()
|
|
|
|
pats := New(testFs, WithCache(true))
|
|
|
|
stignore := `(?i)/#recycle
|
|
(?i)/#nosync
|
|
(?i)/$Recycle.bin
|
|
(?i)/$RECYCLE.BIN
|
|
(?i)/System Volume Information`
|
|
if err := pats.Parse(bytes.NewBufferString(stignore), ".stignore"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cases := []string{
|
|
"#nosync",
|
|
"$RECYCLE.BIN",
|
|
filepath.FromSlash("$RECYCLE.BIN/S-1-5-18/desktop.ini"),
|
|
}
|
|
|
|
for _, c := range cases {
|
|
if !pats.Match(c).IsIgnored() {
|
|
t.Errorf("%q should be ignored", c)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIntlWildcards(t *testing.T) {
|
|
testFs := newTestFS()
|
|
|
|
pats := New(testFs, WithCache(true))
|
|
|
|
stignore := `1000春
|
|
200?春
|
|
300[0-9]春
|
|
400[0-9]?`
|
|
if err := pats.Parse(bytes.NewBufferString(stignore), ".stignore"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cases := []string{
|
|
"1000春",
|
|
"2002春",
|
|
"3003春",
|
|
"4004春",
|
|
}
|
|
|
|
for _, c := range cases {
|
|
if !pats.Match(c).IsIgnored() {
|
|
t.Errorf("%q should be ignored", c)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPartialIncludeLine(t *testing.T) {
|
|
testFs := newTestFS()
|
|
|
|
// Loading a partial #include line (no file mentioned) should error but not crash.
|
|
|
|
pats := New(testFs, WithCache(true))
|
|
|
|
cases := []string{
|
|
"#include",
|
|
"#include\n",
|
|
"#include ",
|
|
"#include \n",
|
|
"#include \n\n\n",
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
err := pats.Parse(bytes.NewBufferString(tc), ".stignore")
|
|
if err == nil {
|
|
t.Fatal("should error out")
|
|
}
|
|
if !IsParseError(err) {
|
|
t.Fatal("failure to load included file should be a parse error")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSkipIgnoredDirs(t *testing.T) {
|
|
testFs := newTestFS()
|
|
|
|
tcs := []struct {
|
|
pattern string
|
|
expected bool
|
|
}{
|
|
{`!/test`, true},
|
|
{`!/t[eih]t`, true},
|
|
{`!/t*t`, true},
|
|
{`!/t?t`, true},
|
|
{`!/**`, true},
|
|
{`!/parent/test`, false},
|
|
{`!/parent/t[eih]t`, false},
|
|
{`!/parent/t*t`, false},
|
|
{`!/parent/t?t`, false},
|
|
{`!/**.mp3`, false},
|
|
{`!/pa*nt/test`, false},
|
|
{`!/pa[sdf]nt/t[eih]t`, false},
|
|
{`!/lowest/pa[sdf]nt/test`, false},
|
|
{`!/lo*st/parent/test`, false},
|
|
{`/pa*nt/test`, true},
|
|
{`test`, true},
|
|
{`*`, true},
|
|
}
|
|
|
|
for _, tc := range tcs {
|
|
pats, err := parseLine(tc.pattern)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
for _, pat := range pats {
|
|
if got := pat.allowsSkippingIgnoredDirs(); got != tc.expected {
|
|
t.Errorf(`Pattern "%v": got %v, expected %v`, pat, got, tc.expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
pats := New(testFs, WithCache(true))
|
|
|
|
stignore := `
|
|
/foo/ign*
|
|
!/f*
|
|
!/bar
|
|
*
|
|
`
|
|
if err := pats.Parse(bytes.NewBufferString(stignore), ".stignore"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if m := pats.Match("whatever"); !m.CanSkipDir() {
|
|
t.Error("CanSkipDir should be true")
|
|
}
|
|
|
|
stignore = `
|
|
!/foo/ign*
|
|
*
|
|
`
|
|
if err := pats.Parse(bytes.NewBufferString(stignore), ".stignore"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if m := pats.Match("whatever"); m.CanSkipDir() {
|
|
t.Error("CanSkipDir should be false")
|
|
}
|
|
}
|
|
|
|
func TestEmptyPatterns(t *testing.T) {
|
|
testFs := newTestFS()
|
|
|
|
// These patterns are all invalid and should be rejected as such (without panicking...)
|
|
tcs := []string{
|
|
"!",
|
|
"(?d)",
|
|
"(?i)",
|
|
}
|
|
|
|
for _, tc := range tcs {
|
|
m := New(testFs)
|
|
err := m.Parse(strings.NewReader(tc), ".stignore")
|
|
if err == nil {
|
|
t.Error("Should reject invalid pattern", tc)
|
|
}
|
|
if !IsParseError(err) {
|
|
t.Fatal("bad pattern should be a parse error")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWindowsLineEndings(t *testing.T) {
|
|
testFs := newTestFS()
|
|
|
|
if !build.IsWindows {
|
|
t.Skip("Windows specific")
|
|
}
|
|
lines := "foo\nbar\nbaz\n"
|
|
|
|
m := New(testFs)
|
|
if err := m.Parse(strings.NewReader(lines), ".stignore"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := WriteIgnores(testFs, ".stignore", m.Lines()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
fd, err := testFs.Open(".stignore")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bs, err := io.ReadAll(fd)
|
|
fd.Close()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
unixLineEndings := bytes.Count(bs, []byte("\n"))
|
|
windowsLineEndings := bytes.Count(bs, []byte("\r\n"))
|
|
if unixLineEndings == 0 || windowsLineEndings != unixLineEndings {
|
|
t.Error("expected there to be a non-zero number of Windows line endings")
|
|
}
|
|
}
|