github.com/nextlinux/gosbom@v0.81.1-0.20230627115839-1ff50c281391/gosbom/internal/fileresolver/directory_indexer_test.go (about)

     1  package fileresolver
     2  
     3  import (
     4  	"io/fs"
     5  	"os"
     6  	"path"
     7  	"sort"
     8  	"strings"
     9  	"testing"
    10  
    11  	"github.com/google/go-cmp/cmp"
    12  	"github.com/scylladb/go-set/strset"
    13  	"github.com/stretchr/testify/assert"
    14  	"github.com/stretchr/testify/require"
    15  	"github.com/wagoodman/go-progress"
    16  
    17  	"github.com/anchore/stereoscope/pkg/file"
    18  )
    19  
    20  type indexerMock struct {
    21  	observedRoots   []string
    22  	additionalRoots map[string][]string
    23  }
    24  
    25  func (m *indexerMock) indexer(s string, _ *progress.Stage) ([]string, error) {
    26  	m.observedRoots = append(m.observedRoots, s)
    27  	return m.additionalRoots[s], nil
    28  }
    29  
    30  func Test_indexAllRoots(t *testing.T) {
    31  	tests := []struct {
    32  		name          string
    33  		root          string
    34  		mock          indexerMock
    35  		expectedRoots []string
    36  	}{
    37  		{
    38  			name: "no additional roots",
    39  			root: "a/place",
    40  			mock: indexerMock{
    41  				additionalRoots: make(map[string][]string),
    42  			},
    43  			expectedRoots: []string{
    44  				"a/place",
    45  			},
    46  		},
    47  		{
    48  			name: "additional roots from a single call",
    49  			root: "a/place",
    50  			mock: indexerMock{
    51  				additionalRoots: map[string][]string{
    52  					"a/place": {
    53  						"another/place",
    54  						"yet-another/place",
    55  					},
    56  				},
    57  			},
    58  			expectedRoots: []string{
    59  				"a/place",
    60  				"another/place",
    61  				"yet-another/place",
    62  			},
    63  		},
    64  		{
    65  			name: "additional roots from a multiple calls",
    66  			root: "a/place",
    67  			mock: indexerMock{
    68  				additionalRoots: map[string][]string{
    69  					"a/place": {
    70  						"another/place",
    71  						"yet-another/place",
    72  					},
    73  					"yet-another/place": {
    74  						"a-quiet-place-2",
    75  						"a-final/place",
    76  					},
    77  				},
    78  			},
    79  			expectedRoots: []string{
    80  				"a/place",
    81  				"another/place",
    82  				"yet-another/place",
    83  				"a-quiet-place-2",
    84  				"a-final/place",
    85  			},
    86  		},
    87  	}
    88  
    89  	for _, test := range tests {
    90  		t.Run(test.name, func(t *testing.T) {
    91  			assert.NoError(t, indexAllRoots(test.root, test.mock.indexer))
    92  		})
    93  	}
    94  }
    95  
    96  func TestDirectoryIndexer_handleFileAccessErr(t *testing.T) {
    97  	tests := []struct {
    98  		name                string
    99  		input               error
   100  		expectedPathTracked bool
   101  	}{
   102  		{
   103  			name:                "permission error does not propagate",
   104  			input:               os.ErrPermission,
   105  			expectedPathTracked: true,
   106  		},
   107  		{
   108  			name:                "file does not exist error does not propagate",
   109  			input:               os.ErrNotExist,
   110  			expectedPathTracked: true,
   111  		},
   112  		{
   113  			name:                "non-permission errors are tracked",
   114  			input:               os.ErrInvalid,
   115  			expectedPathTracked: true,
   116  		},
   117  		{
   118  			name:                "non-errors ignored",
   119  			input:               nil,
   120  			expectedPathTracked: false,
   121  		},
   122  	}
   123  
   124  	for _, test := range tests {
   125  		t.Run(test.name, func(t *testing.T) {
   126  			r := directoryIndexer{
   127  				errPaths: make(map[string]error),
   128  			}
   129  			p := "a/place"
   130  			assert.Equal(t, r.isFileAccessErr(p, test.input), test.expectedPathTracked)
   131  			_, exists := r.errPaths[p]
   132  			assert.Equal(t, test.expectedPathTracked, exists)
   133  		})
   134  	}
   135  }
   136  
   137  func TestDirectoryIndexer_IncludeRootPathInIndex(t *testing.T) {
   138  	filterFn := func(path string, _ os.FileInfo, _ error) error {
   139  		if path != "/" {
   140  			return fs.SkipDir
   141  		}
   142  		return nil
   143  	}
   144  
   145  	indexer := newDirectoryIndexer("/", "", filterFn)
   146  	tree, index, err := indexer.build()
   147  	require.NoError(t, err)
   148  
   149  	exists, ref, err := tree.File(file.Path("/"))
   150  	require.NoError(t, err)
   151  	require.NotNil(t, ref)
   152  	assert.True(t, exists)
   153  
   154  	_, err = index.Get(*ref.Reference)
   155  	require.NoError(t, err)
   156  }
   157  
   158  func TestDirectoryIndexer_indexPath_skipsNilFileInfo(t *testing.T) {
   159  	// TODO: Ideally we can use an OS abstraction, which would obviate the need for real FS setup.
   160  	tempFile, err := os.CreateTemp("", "")
   161  	require.NoError(t, err)
   162  
   163  	indexer := newDirectoryIndexer(tempFile.Name(), "")
   164  
   165  	t.Run("filtering path with nil os.FileInfo", func(t *testing.T) {
   166  		assert.NotPanics(t, func() {
   167  			_, err := indexer.indexPath("/dont-care", nil, nil)
   168  			assert.NoError(t, err)
   169  			assert.False(t, indexer.tree.HasPath("/dont-care"))
   170  		})
   171  	})
   172  }
   173  
   174  func TestDirectoryIndexer_index(t *testing.T) {
   175  	// note: this test is testing the effects from NewFromDirectory, indexTree, and addPathToIndex
   176  	indexer := newDirectoryIndexer("test-fixtures/system_paths/target", "")
   177  	tree, index, err := indexer.build()
   178  	require.NoError(t, err)
   179  
   180  	tests := []struct {
   181  		name string
   182  		path string
   183  	}{
   184  		{
   185  			name: "has dir",
   186  			path: "test-fixtures/system_paths/target/home",
   187  		},
   188  		{
   189  			name: "has path",
   190  			path: "test-fixtures/system_paths/target/home/place",
   191  		},
   192  		{
   193  			name: "has symlink",
   194  			path: "test-fixtures/system_paths/target/link/a-symlink",
   195  		},
   196  		{
   197  			name: "has symlink target",
   198  			path: "test-fixtures/system_paths/outside_root/link_target/place",
   199  		},
   200  	}
   201  	for _, test := range tests {
   202  		t.Run(test.name, func(t *testing.T) {
   203  			info, err := os.Stat(test.path)
   204  			assert.NoError(t, err)
   205  
   206  			// note: the index uses absolute paths, so assertions MUST keep this in mind
   207  			cwd, err := os.Getwd()
   208  			require.NoError(t, err)
   209  
   210  			p := file.Path(path.Join(cwd, test.path))
   211  			assert.Equal(t, true, tree.HasPath(p))
   212  			exists, ref, err := tree.File(p)
   213  			assert.Equal(t, true, exists)
   214  			if assert.NoError(t, err) {
   215  				return
   216  			}
   217  
   218  			entry, err := index.Get(*ref.Reference)
   219  			require.NoError(t, err)
   220  			assert.Equal(t, info.Mode(), entry.Mode)
   221  		})
   222  	}
   223  }
   224  
   225  func TestDirectoryIndexer_SkipsAlreadyVisitedLinkDestinations(t *testing.T) {
   226  	var observedPaths []string
   227  	pathObserver := func(p string, _ os.FileInfo, _ error) error {
   228  		fields := strings.Split(p, "test-fixtures/symlinks-prune-indexing")
   229  		if len(fields) < 2 {
   230  			return nil
   231  		}
   232  		clean := strings.TrimLeft(fields[1], "/")
   233  		if clean != "" {
   234  			observedPaths = append(observedPaths, clean)
   235  		}
   236  		return nil
   237  	}
   238  	resolver := newDirectoryIndexer("./test-fixtures/symlinks-prune-indexing", "")
   239  	// we want to cut ahead of any possible filters to see what paths are considered for indexing (closest to walking)
   240  	resolver.pathIndexVisitors = append([]PathIndexVisitor{pathObserver}, resolver.pathIndexVisitors...)
   241  
   242  	// note: this test is NOT about the effects left on the tree or the index, but rather the WHICH paths that are
   243  	// considered for indexing and HOW traversal prunes paths that have already been visited
   244  	_, _, err := resolver.build()
   245  	require.NoError(t, err)
   246  
   247  	expected := []string{
   248  		"before-path",
   249  		"c-file.txt",
   250  		"c-path",
   251  		"path",
   252  		"path/1",
   253  		"path/1/2",
   254  		"path/1/2/3",
   255  		"path/1/2/3/4",
   256  		"path/1/2/3/4/dont-index-me-twice.txt",
   257  		"path/5",
   258  		"path/5/6",
   259  		"path/5/6/7",
   260  		"path/5/6/7/8",
   261  		"path/5/6/7/8/dont-index-me-twice-either.txt",
   262  		"path/file.txt",
   263  		// everything below is after the original tree is indexed, and we are now indexing additional roots from symlinks
   264  		"path",          // considered from symlink before-path, but pruned
   265  		"path/file.txt", // leaf
   266  		"before-path",   // considered from symlink c-path, but pruned
   267  		"path/file.txt", // leaf
   268  		"before-path",   // considered from symlink c-path, but pruned
   269  	}
   270  
   271  	assert.Equal(t, expected, observedPaths, "visited paths differ \n %s", cmp.Diff(expected, observedPaths))
   272  
   273  }
   274  
   275  func TestDirectoryIndexer_IndexesAllTypes(t *testing.T) {
   276  	indexer := newDirectoryIndexer("./test-fixtures/symlinks-prune-indexing", "")
   277  
   278  	tree, index, err := indexer.build()
   279  	require.NoError(t, err)
   280  
   281  	allRefs := tree.AllFiles(file.AllTypes()...)
   282  	var pathRefs []file.Reference
   283  	paths := strset.New()
   284  	for _, ref := range allRefs {
   285  		fields := strings.Split(string(ref.RealPath), "test-fixtures/symlinks-prune-indexing")
   286  		if len(fields) != 2 {
   287  			continue
   288  		}
   289  		clean := strings.TrimLeft(fields[1], "/")
   290  		if clean == "" {
   291  			continue
   292  		}
   293  		paths.Add(clean)
   294  		pathRefs = append(pathRefs, ref)
   295  	}
   296  
   297  	pathsList := paths.List()
   298  	sort.Strings(pathsList)
   299  
   300  	expected := []string{
   301  		"before-path",                          // link
   302  		"c-file.txt",                           // link
   303  		"c-path",                               // link
   304  		"path",                                 // dir
   305  		"path/1",                               // dir
   306  		"path/1/2",                             // dir
   307  		"path/1/2/3",                           // dir
   308  		"path/1/2/3/4",                         // dir
   309  		"path/1/2/3/4/dont-index-me-twice.txt", // file
   310  		"path/5",                               // dir
   311  		"path/5/6",                             // dir
   312  		"path/5/6/7",                           // dir
   313  		"path/5/6/7/8",                         // dir
   314  		"path/5/6/7/8/dont-index-me-twice-either.txt", // file
   315  		"path/file.txt", // file
   316  	}
   317  	expectedSet := strset.New(expected...)
   318  
   319  	// make certain all expected paths are in the tree (and no extra ones are their either)
   320  
   321  	assert.True(t, paths.IsEqual(expectedSet), "expected all paths to be indexed, but found different paths: \n%s", cmp.Diff(expected, pathsList))
   322  
   323  	// make certain that the paths are also in the file index
   324  
   325  	for _, ref := range pathRefs {
   326  		_, err := index.Get(ref)
   327  		require.NoError(t, err)
   328  	}
   329  
   330  }
   331  
   332  func Test_allContainedPaths(t *testing.T) {
   333  
   334  	tests := []struct {
   335  		name string
   336  		path string
   337  		want []string
   338  	}{
   339  		{
   340  			name: "empty",
   341  			path: "",
   342  			want: nil,
   343  		},
   344  		{
   345  			name: "single relative",
   346  			path: "a",
   347  			want: []string{"a"},
   348  		},
   349  		{
   350  			name: "single absolute",
   351  			path: "/a",
   352  			want: []string{"/a"},
   353  		},
   354  		{
   355  			name: "multiple relative",
   356  			path: "a/b/c",
   357  			want: []string{"a", "a/b", "a/b/c"},
   358  		},
   359  		{
   360  			name: "multiple absolute",
   361  			path: "/a/b/c",
   362  			want: []string{"/a", "/a/b", "/a/b/c"},
   363  		},
   364  		{
   365  			name: "multiple absolute with extra slashs",
   366  			path: "///a/b//c/",
   367  			want: []string{"/a", "/a/b", "/a/b/c"},
   368  		},
   369  		{
   370  			name: "relative with single dot",
   371  			path: "a/./b",
   372  			want: []string{"a", "a/b"},
   373  		},
   374  		{
   375  			name: "relative with double single dot",
   376  			path: "a/../b",
   377  			want: []string{"b"},
   378  		},
   379  	}
   380  	for _, tt := range tests {
   381  		t.Run(tt.name, func(t *testing.T) {
   382  			assert.Equal(t, tt.want, allContainedPaths(tt.path))
   383  		})
   384  	}
   385  }