blob: f5c80b068f097252d9fa1831cf0e1078682d3977 [file] [edit]
// Copyright 2021 Google LLC
//
// 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 fileset_test
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"os"
"regexp"
"sort"
"strconv"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/testing/protocmp"
"github.com/google/localtoast/scanapi"
"github.com/google/localtoast/scannerlib/fileset"
apb "github.com/google/localtoast/scannerlib/proto/api_go_proto"
ipb "github.com/google/localtoast/scannerlib/proto/scan_instructions_go_proto"
)
const (
procEnvironPath = "/proc/self/environ"
procEnvironContent = "SHELL=/bin/bash\x00PATH=/root:/root/file1.txt"
)
type fakeDirectoryReader struct{}
func (fakeDirectoryReader) FilePermissions(ctx context.Context, path string) (*apb.PosixPermissions, error) {
return nil, errors.New("Not implemented")
}
func (fakeDirectoryReader) OpenFile(ctx context.Context, path string) (io.ReadCloser, error) {
switch path {
case procEnvironPath:
return io.NopCloser(bytes.NewReader([]byte(procEnvironContent))), nil
default:
return nil, os.ErrNotExist
}
}
func (fakeDirectoryReader) OpenDir(ctx context.Context, path string) (scanapi.DirReader, error) {
// Fake dir structure:
// root---file1.txt
// \ \ \-file2.gif
// \ \--symlink
// \---subdir--file3.txt
switch path {
case "/root":
return scanapi.SliceToDirReader([]*apb.DirContent{
&apb.DirContent{Name: "file1.txt", IsDir: false},
&apb.DirContent{Name: "file2.gif", IsDir: false},
&apb.DirContent{Name: "symlink", IsDir: false, IsSymlink: true},
&apb.DirContent{Name: "subdir", IsDir: true},
}), nil
case "/root/subdir":
return scanapi.SliceToDirReader([]*apb.DirContent{
&apb.DirContent{Name: "file3.txt", IsDir: false},
}), nil
default:
return nil, os.ErrNotExist
}
}
func TestSingleFile(t *testing.T) {
expectedPath := "/path/to/file"
fileSet := &ipb.FileSet{
FilePath: &ipb.FileSet_SingleFile_{SingleFile: &ipb.FileSet_SingleFile{Path: expectedPath}},
}
err := fileset.WalkFiles(context.Background(), fileSet, &fakeDirectoryReader{}, time.Time{}, func(walkedPath string, isDir bool, traversingDir bool) error {
if expectedPath != walkedPath {
t.Errorf("fileset.WalkFiles(%v) expected to walk on path %s, got %s",
fileSet, expectedPath, walkedPath)
}
return nil
})
if err != nil {
t.Fatalf("fileset.WalkFiles(%v) returned an error: %v", fileSet, err)
}
}
type traversal struct {
Path string
IsDir bool
TraversingDir bool
}
func TestFilesInDir(t *testing.T) {
testCases := []struct {
description string
fileSet *ipb.FileSet
expectedTraversal []*traversal
}{
{
description: "visit all files recursively",
fileSet: &ipb.FileSet{
FilePath: &ipb.FileSet_FilesInDir_{FilesInDir: &ipb.FileSet_FilesInDir{
DirPath: "/root",
Recursive: true,
}},
},
expectedTraversal: []*traversal{
{Path: "/root", IsDir: true, TraversingDir: true},
{Path: "/root/file1.txt", IsDir: false, TraversingDir: true},
{Path: "/root/file2.gif", IsDir: false, TraversingDir: true},
{Path: "/root/symlink", IsDir: false, TraversingDir: true},
{Path: "/root/subdir", IsDir: true, TraversingDir: true},
{Path: "/root/subdir/file3.txt", IsDir: false, TraversingDir: true},
},
},
{
description: "visit only files",
fileSet: &ipb.FileSet{
FilePath: &ipb.FileSet_FilesInDir_{FilesInDir: &ipb.FileSet_FilesInDir{
DirPath: "/root",
Recursive: true,
FilesOnly: true,
}},
},
expectedTraversal: []*traversal{
{Path: "/root/file1.txt", IsDir: false, TraversingDir: true},
{Path: "/root/file2.gif", IsDir: false, TraversingDir: true},
{Path: "/root/symlink", IsDir: false, TraversingDir: true},
{Path: "/root/subdir/file3.txt", IsDir: false, TraversingDir: true},
},
},
{
description: "visit only directories",
fileSet: &ipb.FileSet{
FilePath: &ipb.FileSet_FilesInDir_{FilesInDir: &ipb.FileSet_FilesInDir{
DirPath: "/root",
Recursive: true,
DirsOnly: true,
}},
},
expectedTraversal: []*traversal{
{Path: "/root", IsDir: true, TraversingDir: true},
{Path: "/root/subdir", IsDir: true, TraversingDir: true},
},
},
{
description: "omit symlinks",
fileSet: &ipb.FileSet{
FilePath: &ipb.FileSet_FilesInDir_{FilesInDir: &ipb.FileSet_FilesInDir{
DirPath: "/root",
Recursive: true,
SkipSymlinks: true,
}},
},
expectedTraversal: []*traversal{
{Path: "/root", IsDir: true, TraversingDir: true},
{Path: "/root/file1.txt", IsDir: false, TraversingDir: true},
{Path: "/root/file2.gif", IsDir: false, TraversingDir: true},
{Path: "/root/subdir", IsDir: true, TraversingDir: true},
{Path: "/root/subdir/file3.txt", IsDir: false, TraversingDir: true},
},
},
{
description: "visit files in the root dir",
fileSet: &ipb.FileSet{
FilePath: &ipb.FileSet_FilesInDir_{FilesInDir: &ipb.FileSet_FilesInDir{
DirPath: "/root",
Recursive: false,
}},
},
expectedTraversal: []*traversal{
{Path: "/root", IsDir: true, TraversingDir: true},
{Path: "/root/file1.txt", IsDir: false, TraversingDir: true},
{Path: "/root/file2.gif", IsDir: false, TraversingDir: true},
{Path: "/root/symlink", IsDir: false, TraversingDir: true},
{Path: "/root/subdir", IsDir: true, TraversingDir: true},
},
},
{
description: "filter file names to visit",
fileSet: &ipb.FileSet{
FilePath: &ipb.FileSet_FilesInDir_{FilesInDir: &ipb.FileSet_FilesInDir{
DirPath: "/root",
Recursive: true,
FilenameRegex: ".*\\.txt",
}},
},
expectedTraversal: []*traversal{
{Path: "/root/file1.txt", IsDir: false, TraversingDir: true},
{Path: "/root/subdir/file3.txt", IsDir: false, TraversingDir: true},
},
},
{
description: "opt out file paths",
fileSet: &ipb.FileSet{
FilePath: &ipb.FileSet_FilesInDir_{FilesInDir: &ipb.FileSet_FilesInDir{
DirPath: "/root",
Recursive: true,
OptOutPathRegexes: []string{"/root/sub.*"},
}},
},
expectedTraversal: []*traversal{
{Path: "/root", IsDir: true, TraversingDir: true},
{Path: "/root/file1.txt", IsDir: false, TraversingDir: true},
{Path: "/root/file2.gif", IsDir: false, TraversingDir: true},
{Path: "/root/symlink", IsDir: false, TraversingDir: true},
},
},
{
description: "opt out root directory",
fileSet: &ipb.FileSet{
FilePath: &ipb.FileSet_FilesInDir_{FilesInDir: &ipb.FileSet_FilesInDir{
DirPath: "/root",
Recursive: true,
OptOutPathRegexes: []string{"/root"},
}},
},
expectedTraversal: []*traversal{},
},
{
description: "opt out root directory with regex",
fileSet: &ipb.FileSet{
FilePath: &ipb.FileSet_FilesInDir_{FilesInDir: &ipb.FileSet_FilesInDir{
DirPath: "/root",
Recursive: true,
OptOutPathRegexes: []string{"/roo.*"},
}},
},
expectedTraversal: []*traversal{},
},
{
description: "directory doesn't exist",
fileSet: &ipb.FileSet{
FilePath: &ipb.FileSet_FilesInDir_{FilesInDir: &ipb.FileSet_FilesInDir{
DirPath: "/non-existent",
Recursive: true,
}},
},
// The directory is still traversed so that the checks can report
// non-compliance.
expectedTraversal: []*traversal{
{Path: "/non-existent", IsDir: true, TraversingDir: true},
},
},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
gotTraversal := []*traversal{}
err := fileset.WalkFiles(context.Background(), tc.fileSet, &fakeDirectoryReader{}, time.Time{}, func(walkedPath string, isDir bool, traversingDir bool) error {
gotTraversal = append(gotTraversal, &traversal{walkedPath, isDir, traversingDir})
return nil
})
if err != nil {
t.Fatalf("fileset.WalkFiles(%v) returned an error: %v", tc.fileSet, err)
}
if diff := cmp.Diff(tc.expectedTraversal, gotTraversal); diff != "" {
t.Errorf("fileset.WalkFiles(%v) made an unexpected traversal diff (-want +got):\n%s",
tc.fileSet, diff)
}
})
}
}
type infiniteLoopFSReader struct{}
func (infiniteLoopFSReader) OpenFile(ctx context.Context, path string) (io.ReadCloser, error) {
return nil, errors.New("Not implemented")
}
func (infiniteLoopFSReader) FilePermissions(ctx context.Context, path string) (*apb.PosixPermissions, error) {
return nil, errors.New("Not implemented")
}
func (infiniteLoopFSReader) OpenDir(ctx context.Context, path string) (scanapi.DirReader, error) {
return scanapi.SliceToDirReader([]*apb.DirContent{
&apb.DirContent{Name: "dir", IsDir: true},
}), nil
}
func TestTraverseFilesystemWithInfiniteLoop(t *testing.T) {
files := &ipb.FileSet{FilePath: &ipb.FileSet_FilesInDir_{FilesInDir: &ipb.FileSet_FilesInDir{
DirPath: "/",
Recursive: true,
}}}
err := fileset.WalkFiles(context.Background(), files, &infiniteLoopFSReader{}, time.Time{}, func(walkedPath string, isDir bool, traversingDir bool) error { return nil })
if err == nil {
t.Fatalf("fileset.WalkFiles(%v) didn't return an error", files)
}
}
type fakeProcessPathReader struct {
pidToName map[int]string
pidToCLIArgs map[int]string
// If true, tests the race condition when the files become unavailable after
// they're listed in the /proc directory.
removeFilesAfterQuery bool
}
func (fakeProcessPathReader) FilePermissions(ctx context.Context, path string) (*apb.PosixPermissions, error) {
return nil, errors.New("Not implemented")
}
func (r fakeProcessPathReader) OpenFile(ctx context.Context, path string) (io.ReadCloser, error) {
if r.removeFilesAfterQuery {
return nil, os.ErrNotExist
}
var re *regexp.Regexp
if strings.HasSuffix(path, "cmdline") {
re = regexp.MustCompile(`^/proc/([0-9]+)/cmdline`)
} else {
re = regexp.MustCompile(`^/proc/([0-9]+)/stat$`)
}
gs := re.FindStringSubmatch(path)
if gs == nil {
return nil, os.ErrNotExist
}
pid, err := strconv.Atoi(gs[1])
if err != nil {
return nil, os.ErrNotExist
}
if strings.HasSuffix(path, "cmdline") {
return io.NopCloser(bytes.NewBufferString(r.pidToCLIArgs[pid])), nil
}
name, ok := r.pidToName[pid]
if !ok {
return nil, os.ErrNotExist
}
return io.NopCloser(bytes.NewBufferString(fmt.Sprintf(
"%d (%s) I 2 0 0 0 -1 69238880 0 0 0 0 0 0 0 0 0 -20 1 0 250 0 0 18446744073709551615 0 0 0 0 0 0 0 2147483647 0 0 0 0 17 3 0 0 0 0 0 0 0 0 0 0 0 0 0",
pid,
name,
))), nil
}
func (r fakeProcessPathReader) OpenDir(ctx context.Context, path string) (scanapi.DirReader, error) {
if path != "/proc/" {
return scanapi.SliceToDirReader([]*apb.DirContent{}), nil
}
paths := []*apb.DirContent{
&apb.DirContent{
Name: "cpuinfo",
IsDir: false,
},
&apb.DirContent{
Name: "net",
IsDir: true,
},
}
// Make order deterministic
pids := make([]int, 0, len(r.pidToName))
for pid := range r.pidToName {
pids = append(pids, pid)
}
sort.Ints(pids)
for _, pid := range pids {
paths = append(paths, &apb.DirContent{
Name: fmt.Sprintf("%d", pid),
IsDir: true,
})
}
return scanapi.SliceToDirReader(paths), nil
}
func TestProcessPath(t *testing.T) {
testCases := []struct {
name string
pidToName map[int]string
pidToCLIArgs map[int]string
fileSet *ipb.FileSet
expectedTraversal []*traversal
}{
{
name: "process name does not exist",
pidToName: map[int]string{
1: "foo",
2: "bar",
},
fileSet: &ipb.FileSet{
FilePath: &ipb.FileSet_ProcessPath_{ProcessPath: &ipb.FileSet_ProcessPath{
ProcName: "foobar",
}},
},
expectedTraversal: []*traversal{},
},
{
name: "process name exists once",
pidToName: map[int]string{
1: "foo",
2: "bar",
1337: "foobar",
},
fileSet: &ipb.FileSet{
FilePath: &ipb.FileSet_ProcessPath_{ProcessPath: &ipb.FileSet_ProcessPath{
ProcName: "foobar",
}},
},
expectedTraversal: []*traversal{
{Path: "/proc/1337", IsDir: true},
},
},
{
name: "process name exists twice",
pidToName: map[int]string{
1: "foo",
2: "bar",
42: "foobar",
1337: "foobar",
},
fileSet: &ipb.FileSet{
FilePath: &ipb.FileSet_ProcessPath_{ProcessPath: &ipb.FileSet_ProcessPath{
ProcName: "foobar",
}},
},
expectedTraversal: []*traversal{
{Path: "/proc/42", IsDir: true},
{Path: "/proc/1337", IsDir: true},
},
},
{
name: "filename is set",
pidToName: map[int]string{
1: "foo",
2: "bar",
42: "foobar",
1337: "foobar",
},
fileSet: &ipb.FileSet{
FilePath: &ipb.FileSet_ProcessPath_{ProcessPath: &ipb.FileSet_ProcessPath{
ProcName: "foobar",
FileName: "environ",
}},
},
expectedTraversal: []*traversal{
{Path: "/proc/42/environ", IsDir: false},
{Path: "/proc/1337/environ", IsDir: false},
},
},
{
name: "CLI args are set",
pidToName: map[int]string{
1: "foo",
2: "foo",
},
pidToCLIArgs: map[int]string{
1: "env vars 1",
2: "env vars 2",
},
fileSet: &ipb.FileSet{
FilePath: &ipb.FileSet_ProcessPath_{ProcessPath: &ipb.FileSet_ProcessPath{
ProcName: "foo",
CliArgRegex: ".* 2",
}},
},
expectedTraversal: []*traversal{
{Path: "/proc/2", IsDir: true},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got := make([]*traversal, 0)
err := fileset.WalkFiles(
context.Background(),
tc.fileSet,
&fakeProcessPathReader{pidToName: tc.pidToName, pidToCLIArgs: tc.pidToCLIArgs},
time.Time{},
func(path string, isDir bool, traversingDir bool) error {
got = append(got, &traversal{path, isDir, traversingDir})
return nil
},
)
if err != nil {
t.Fatalf("fileset.WalkFiles(%v) returned an error: %v", tc.fileSet, err)
}
if diff := cmp.Diff(tc.expectedTraversal, got); diff != "" {
t.Errorf(
"fileset.WalkFunc(%v) made an unexpected traversal diff (-want +got):\n%s",
tc.fileSet,
diff,
)
}
})
}
}
func TestProcessPathRemovedAfterQuerying(t *testing.T) {
fileSet := &ipb.FileSet{
FilePath: &ipb.FileSet_ProcessPath_{ProcessPath: &ipb.FileSet_ProcessPath{
ProcName: "foo",
}},
}
expectedTraversal := []*traversal{}
got := make([]*traversal, 0)
err := fileset.WalkFiles(
context.Background(),
fileSet,
&fakeProcessPathReader{pidToName: map[int]string{1: "foo"}, removeFilesAfterQuery: true},
time.Time{},
func(path string, isDir bool, traversingDir bool) error {
got = append(got, &traversal{path, isDir, traversingDir})
return nil
},
)
if err != nil {
t.Fatalf("fileset.WalkFiles(%v) returned an error: %v", fileSet, err)
}
if diff := cmp.Diff(expectedTraversal, got); diff != "" {
t.Errorf(
"fileset.WalkFunc(%v) made an unexpected traversal diff (-want +got):\n%s",
fileSet,
diff,
)
}
}
func TestUnixEnvVarPaths(t *testing.T) {
testCases := []struct {
name string
fileSet *ipb.FileSet
expectedTraversal []*traversal
expectError bool
}{
{
name: "read a valid var",
fileSet: &ipb.FileSet{
FilePath: &ipb.FileSet_UnixEnvVarPaths_{UnixEnvVarPaths: &ipb.FileSet_UnixEnvVarPaths{
VarName: "PATH",
}},
},
expectedTraversal: []*traversal{
{Path: "/root", IsDir: true},
{Path: "/root/file1.txt", IsDir: false},
},
},
{
name: "read a non-existent var",
fileSet: &ipb.FileSet{
FilePath: &ipb.FileSet_UnixEnvVarPaths_{UnixEnvVarPaths: &ipb.FileSet_UnixEnvVarPaths{
VarName: "NONEXISTENT",
}},
},
expectError: true,
},
{
name: "files only",
fileSet: &ipb.FileSet{
FilePath: &ipb.FileSet_UnixEnvVarPaths_{UnixEnvVarPaths: &ipb.FileSet_UnixEnvVarPaths{
VarName: "PATH",
FilesOnly: true,
}},
},
expectedTraversal: []*traversal{
{Path: "/root/file1.txt", IsDir: false},
},
},
{
name: "directories only",
fileSet: &ipb.FileSet{
FilePath: &ipb.FileSet_UnixEnvVarPaths_{UnixEnvVarPaths: &ipb.FileSet_UnixEnvVarPaths{
VarName: "PATH",
DirsOnly: true,
}},
},
expectedTraversal: []*traversal{
{Path: "/root", IsDir: true},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var got []*traversal
err := fileset.WalkFiles(
context.Background(),
tc.fileSet,
&fakeDirectoryReader{},
time.Time{},
func(path string, isDir bool, traversingDir bool) error {
got = append(got, &traversal{path, isDir, traversingDir})
return nil
},
)
if tc.expectError {
if err == nil {
t.Errorf("fileset.WalkFiles(%v) didn't return an error", tc.fileSet)
}
} else {
if err != nil {
t.Fatalf("fileset.WalkFiles(%v) returned an error: %v", tc.fileSet, err)
}
if diff := cmp.Diff(tc.expectedTraversal, got); diff != "" {
t.Errorf("fileset.WalkFiles(%v) made an unexpected traversal diff (-want +got):\n%s", tc.fileSet, diff)
}
}
})
}
}
func TestTimeout(t *testing.T) {
testCases := []struct {
description string
fileSet *ipb.FileSet
}{
{
description: "single file",
fileSet: &ipb.FileSet{
FilePath: &ipb.FileSet_SingleFile_{SingleFile: &ipb.FileSet_SingleFile{Path: "/path/to/file"}},
},
},
{
description: "files in directory",
fileSet: &ipb.FileSet{
FilePath: &ipb.FileSet_FilesInDir_{FilesInDir: &ipb.FileSet_FilesInDir{
DirPath: "/root",
Recursive: true,
}},
},
},
{
description: "env var paths",
fileSet: &ipb.FileSet{
FilePath: &ipb.FileSet_UnixEnvVarPaths_{UnixEnvVarPaths: &ipb.FileSet_UnixEnvVarPaths{
VarName: "PATH",
}},
},
},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
timeout := time.Now().Add(-1 * time.Second)
err := fileset.WalkFiles(context.Background(), tc.fileSet, &fakeDirectoryReader{}, timeout, func(walkedPath string, isDir bool, traversingDir bool) error { return nil })
if err == nil {
t.Fatalf("fileset.WalkFiles(%v) didn't return an error, expected one", tc.fileSet)
}
})
}
}
func TestApplyReplacementConfig(t *testing.T) {
replacements := map[string]string{"/old": "/new"}
testCases := []struct {
name string
config *apb.ReplacementConfig
fileSet *ipb.FileSet
want *ipb.FileSet
}{
{
name: "Replace prefix in SingleFile",
config: &apb.ReplacementConfig{PathPrefixReplacements: replacements},
fileSet: &ipb.FileSet{
FilePath: &ipb.FileSet_SingleFile_{SingleFile: &ipb.FileSet_SingleFile{Path: "/old/path"}},
},
want: &ipb.FileSet{
FilePath: &ipb.FileSet_SingleFile_{SingleFile: &ipb.FileSet_SingleFile{Path: "/new/path"}},
},
},
{
name: "Replace prefix in FilesInDir",
config: &apb.ReplacementConfig{PathPrefixReplacements: replacements},
fileSet: &ipb.FileSet{
FilePath: &ipb.FileSet_FilesInDir_{FilesInDir: &ipb.FileSet_FilesInDir{DirPath: "/old/path"}},
},
want: &ipb.FileSet{
FilePath: &ipb.FileSet_FilesInDir_{FilesInDir: &ipb.FileSet_FilesInDir{DirPath: "/new/path"}},
},
},
{
name: "Replacements with leading slashes",
config: &apb.ReplacementConfig{PathPrefixReplacements: map[string]string{"/old/": "/new/"}},
fileSet: &ipb.FileSet{
FilePath: &ipb.FileSet_SingleFile_{SingleFile: &ipb.FileSet_SingleFile{Path: "/old/path"}},
},
want: &ipb.FileSet{
FilePath: &ipb.FileSet_SingleFile_{SingleFile: &ipb.FileSet_SingleFile{Path: "/new/path"}},
},
},
{
name: "Don't replace if prefix not found",
config: &apb.ReplacementConfig{PathPrefixReplacements: replacements},
fileSet: &ipb.FileSet{
FilePath: &ipb.FileSet_SingleFile_{SingleFile: &ipb.FileSet_SingleFile{Path: "/some/path"}},
},
want: &ipb.FileSet{
FilePath: &ipb.FileSet_SingleFile_{SingleFile: &ipb.FileSet_SingleFile{Path: "/some/path"}},
},
},
{
name: "Don't replace if not prefix",
config: &apb.ReplacementConfig{PathPrefixReplacements: replacements},
fileSet: &ipb.FileSet{
FilePath: &ipb.FileSet_SingleFile_{SingleFile: &ipb.FileSet_SingleFile{Path: "/some/old/path"}},
},
want: &ipb.FileSet{
FilePath: &ipb.FileSet_SingleFile_{SingleFile: &ipb.FileSet_SingleFile{Path: "/some/old/path"}},
},
},
{
name: "Don't replace if only part of the file matches",
config: &apb.ReplacementConfig{PathPrefixReplacements: replacements},
fileSet: &ipb.FileSet{
FilePath: &ipb.FileSet_SingleFile_{SingleFile: &ipb.FileSet_SingleFile{Path: "/old-path/file"}},
},
want: &ipb.FileSet{
FilePath: &ipb.FileSet_SingleFile_{SingleFile: &ipb.FileSet_SingleFile{Path: "/old-path/file"}},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got := proto.Clone(tc.fileSet).(*ipb.FileSet)
fileset.ApplyReplacementConfig(got, tc.config)
if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" {
t.Errorf(
"fileset.ApplyReplacementConfig(%v, %v) unexpected results, diff (-want +got):\n%s", tc.fileSet, tc.config, diff,
)
}
})
}
}