syncthing/lib/ignore/ignore_test.go
Eric P 9f131eee6b
lib/ignore: Properly handle non-existing included ignore-files (fixes #8764) (#8874)
In the sequence of loading ignores, the error File Does Not Exist is not being considered a fatal  error, since the .stignore file is allowed to not exist. However, included ignore files also tossed that same error in case those do not exist while in those cases it's considered an error and it should lead to the folder stopping. Changing the error when opening an included ignore file to something other than the regular does fix this issue, as in it now works again as described in the Documentation.
2023-04-20 15:00:55 +02:00

1245 lines
25 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"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/syncthing/syncthing/lib/build"
"github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/osutil"
)
func TestIgnore(t *testing.T) {
pats := New(fs.NewFilesystem(fs.FilesystemTypeBasic, "testdata"), WithCache(true))
err := pats.Load(".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.IsIgnored() != 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(fs.NewFilesystem(fs.FilesystemTypeBasic, "."), WithCache(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.IsIgnored() != tc.r {
t.Errorf("Incorrect match for %s: %v != %v", tc.f, r, tc.r)
}
}
}
func TestFlagOrder(t *testing.T) {
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(fs.NewFilesystem(fs.FilesystemTypeBasic, "."), 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) {
stignore := `
(?d)ign1
(?d)(?i)ign2
(?i)(?d)ign3
!(?d)ign4
!ign5
!(?i)(?d)ign6
ign7
(?i)ign8
`
pats := New(fs.NewFilesystem(fs.FilesystemTypeBasic, "."), WithCache(true))
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
if err != nil {
t.Fatal(err)
}
var 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) {
var badPatterns = []string{
"[",
"/[",
"**/[",
"#include nonexistent",
"#include .stignore",
}
for _, pat := range badPatterns {
err := New(fs.NewFilesystem(fs.FilesystemTypeBasic, "."), 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) {
ign := New(fs.NewFilesystem(fs.FilesystemTypeBasic, "."), 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) {
dir := t.TempDir()
fs := fs.NewFilesystem(fs.FilesystemTypeBasic, dir)
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) {
stignore := `
// foo
//bar
//!baz
//#dex
// ips
`
pats := New(fs.NewFilesystem(fs.FilesystemTypeBasic, "."), 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 Result
func BenchmarkMatch(b *testing.B) {
stignore := `
.frog
.frog*
.frogfox
.whale
.whale/*
.dolphin
.dolphin/*
~ferret~.*
.ferret.*
flamingo.*
flamingo
*.crow
*.crow
`
pats := New(fs.NewFilesystem(fs.FilesystemTypeBasic, "."))
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.
dir := b.TempDir()
fs := fs.NewFilesystem(fs.FilesystemTypeBasic, dir)
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) {
dir := t.TempDir()
fs := fs.NewFilesystem(fs.FilesystemTypeBasic, dir)
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) {
p1 := New(fs.NewFilesystem(fs.FilesystemTypeBasic, "."), WithCache(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(fs.NewFilesystem(fs.FilesystemTypeBasic, "."), 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(fs.NewFilesystem(fs.FilesystemTypeBasic, "."), 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) {
p1 := New(fs.NewFilesystem(fs.FilesystemTypeBasic, "."), WithCache(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.
// 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) {
// 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(fs.NewFilesystem(fs.FilesystemTypeBasic, "."), 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) {
// 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(fs.NewFilesystem(fs.FilesystemTypeBasic, "."), 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) {
stignore := `
foo,bar.txt
{baz,quux}.txt
`
pats := New(fs.NewFilesystem(fs.FilesystemTypeBasic, "."), 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) {
stignore := `
(?d)(?i)*.part
(?d)(?i)/foo
(?d)(?i)**/bar
`
pats := New(fs.NewFilesystem(fs.FilesystemTypeBasic, "."), 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) {
stignore := `
*ä*
`
pats := New(fs.NewFilesystem(fs.FilesystemTypeBasic, "."), WithCache(true))
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
if err != nil {
t.Fatal(err)
}
if !pats.Match("åäö").IsIgnored() {
t.Error("Should match")
}
}
func TestIssue3639(t *testing.T) {
stignore := `
foo/
`
pats := New(fs.NewFilesystem(fs.FilesystemTypeBasic, "."), 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) {
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(fs.NewFilesystem(fs.FilesystemTypeBasic, "."), 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) {
stignore := `
a?b
bb?
`
testcases := []struct {
file string
matches bool
}{
{"ab", false},
{"acb", true},
{"asdb", false},
{"bb", false},
{"bba", true},
{"bbaa", false},
}
pats := New(fs.NewFilesystem(fs.FilesystemTypeBasic, "."), 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) {
stignore := `
!/a
/*
`
testcases := []struct {
file string
matches bool
}{
{".", false},
{"a", false},
{"b", true},
}
pats := New(fs.NewFilesystem(fs.FilesystemTypeBasic, "."), 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) {
stignore := `
#include testdata/excludes
!/a
/*
!/a
`
pats := New(fs.NewFilesystem(fs.FilesystemTypeBasic, "."), WithCache(true))
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
if err != nil {
t.Fatal(err)
}
expectedLines := []string{
"",
"#include testdata/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) {
stignore := `
!/a
/*
!/a
`
stignoreFiltered := `
!/a
/*
`
pats := New(fs.NewFilesystem(fs.FilesystemTypeBasic, "testdata"), 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) {
stignore := `
#snapshot
`
testcases := []struct {
file string
matches bool
}{
{"#snapshot", true},
{"#snapshot/foo", true},
}
pats := New(fs.NewFilesystem(fs.FilesystemTypeBasic, "."), 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) {
stignore := `// orig`
pats := New(fs.NewFilesystem(fs.FilesystemTypeBasic, "."), 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) {
dir := t.TempDir()
stignore := `
#include unicorn-lazor-death
puppy
`
if err := os.WriteFile(filepath.Join(dir, ".stignore"), []byte(stignore), 0777); err != nil {
t.Fatalf(err.Error())
}
pats := New(fs.NewFilesystem(fs.FilesystemTypeBasic, dir), WithCache(true))
// 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")
}
}
if err := os.WriteFile(filepath.Join(dir, "unicorn-lazor-death"), []byte(" "), 0777); err != nil {
t.Fatalf(err.Error())
}
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) {
pats := New(fs.NewFilesystem(fs.FilesystemTypeBasic, "."), WithCache(true))
stignore := `
ign1
i*2
`
if err := pats.Parse(bytes.NewBufferString(stignore), ".stignore"); err != nil {
t.Fatal(err)
}
if !pats.skipIgnoredDirs {
t.Error("skipIgnoredDirs should be true without includes")
}
stignore = `
!iex2
!ign1/ex
ign1
i*2
!ign2
`
if err := pats.Parse(bytes.NewBufferString(stignore), ".stignore"); err != nil {
t.Fatal(err)
}
if pats.skipIgnoredDirs {
t.Error("skipIgnoredDirs should not be true with includes")
}
}
func TestSpecialChars(t *testing.T) {
pats := New(fs.NewFilesystem(fs.FilesystemTypeBasic, "."), 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) {
pats := New(fs.NewFilesystem(fs.FilesystemTypeBasic, "."), 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) {
// Loading a partial #include line (no file mentioned) should error but not crash.
pats := New(fs.NewFilesystem(fs.FilesystemTypeBasic, "."), 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) {
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(fs.NewFilesystem(fs.FilesystemTypeBasic, "testdata"), WithCache(true))
stignore := `
/foo/ign*
!/f*
!/bar
*
`
if err := pats.Parse(bytes.NewBufferString(stignore), ".stignore"); err != nil {
t.Fatal(err)
}
if !pats.SkipIgnoredDirs() {
t.Error("SkipIgnoredDirs should be true")
}
stignore = `
!/foo/ign*
*
`
if err := pats.Parse(bytes.NewBufferString(stignore), ".stignore"); err != nil {
t.Fatal(err)
}
if pats.SkipIgnoredDirs() {
t.Error("SkipIgnoredDirs should be false")
}
}
func TestEmptyPatterns(t *testing.T) {
// These patterns are all invalid and should be rejected as such (without panicking...)
tcs := []string{
"!",
"(?d)",
"(?i)",
}
for _, tc := range tcs {
m := New(fs.NewFilesystem(fs.FilesystemTypeFake, ""))
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) {
if !build.IsWindows {
t.Skip("Windows specific")
}
lines := "foo\nbar\nbaz\n"
dir := t.TempDir()
ffs := fs.NewFilesystem(fs.FilesystemTypeBasic, dir)
m := New(ffs)
if err := m.Parse(strings.NewReader(lines), ".stignore"); err != nil {
t.Fatal(err)
}
if err := WriteIgnores(ffs, ".stignore", m.Lines()); err != nil {
t.Fatal(err)
}
fd, err := ffs.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")
}
}