github.com/anchore/syft@v1.38.2/syft/internal/fileresolver/container_image_all_layers_test.go (about)

     1  package fileresolver
     2  
     3  import (
     4  	"context"
     5  	"io"
     6  	"sort"
     7  	"testing"
     8  
     9  	"github.com/google/go-cmp/cmp"
    10  	"github.com/scylladb/go-set/strset"
    11  	"github.com/stretchr/testify/assert"
    12  	"github.com/stretchr/testify/require"
    13  
    14  	"github.com/anchore/stereoscope/pkg/imagetest"
    15  	"github.com/anchore/syft/syft/file"
    16  )
    17  
    18  type resolution struct {
    19  	layer uint
    20  	path  string
    21  }
    22  
    23  func TestAllLayersResolver_FilesByPath(t *testing.T) {
    24  	cases := []struct {
    25  		name                 string
    26  		linkPath             string
    27  		resolutions          []resolution
    28  		forcePositiveHasPath bool
    29  	}{
    30  		{
    31  			name:     "link with previous data",
    32  			linkPath: "/link-1",
    33  			resolutions: []resolution{
    34  				{
    35  					layer: 1,
    36  					path:  "/file-1.txt",
    37  				},
    38  			},
    39  		},
    40  		{
    41  			name:     "link with in layer data",
    42  			linkPath: "/link-within",
    43  			resolutions: []resolution{
    44  				{
    45  					layer: 5,
    46  					path:  "/file-3.txt",
    47  				},
    48  			},
    49  		},
    50  		{
    51  			name:     "link with overridden data",
    52  			linkPath: "/link-2",
    53  			resolutions: []resolution{
    54  				{
    55  					layer: 4,
    56  					path:  "/file-2.txt",
    57  				},
    58  				{
    59  					layer: 7,
    60  					path:  "/file-2.txt",
    61  				},
    62  			},
    63  		},
    64  		{
    65  			name:     "indirect link (with overridden data)",
    66  			linkPath: "/link-indirect",
    67  			resolutions: []resolution{
    68  				{
    69  					layer: 4,
    70  					path:  "/file-2.txt",
    71  				},
    72  				{
    73  					layer: 7,
    74  					path:  "/file-2.txt",
    75  				},
    76  			},
    77  		},
    78  		{
    79  			name:                 "dead link",
    80  			linkPath:             "/link-dead",
    81  			resolutions:          []resolution{},
    82  			forcePositiveHasPath: true,
    83  		},
    84  		{
    85  			name:        "ignore directories",
    86  			linkPath:    "/bin",
    87  			resolutions: []resolution{},
    88  			// directories don't resolve BUT do exist
    89  			forcePositiveHasPath: true,
    90  		},
    91  	}
    92  	for _, c := range cases {
    93  		t.Run(c.name, func(t *testing.T) {
    94  			img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
    95  
    96  			resolver, err := NewFromContainerImageAllLayers(img)
    97  			require.NoError(t, err)
    98  
    99  			hasPath := resolver.HasPath(c.linkPath)
   100  			if !c.forcePositiveHasPath {
   101  				if len(c.resolutions) > 0 && !hasPath {
   102  					t.Errorf("expected HasPath() to indicate existance, but did not")
   103  				} else if len(c.resolutions) == 0 && hasPath {
   104  					t.Errorf("expeced HasPath() to NOT indicate existance, but does")
   105  				}
   106  			} else if !hasPath {
   107  				t.Errorf("expected HasPath() to indicate existance, but did not (force path)")
   108  			}
   109  
   110  			refs, err := resolver.FilesByPath(c.linkPath)
   111  			require.NoError(t, err)
   112  
   113  			if len(refs) != len(c.resolutions) {
   114  				t.Fatalf("unexpected number of resolutions: %d", len(refs))
   115  			}
   116  
   117  			for idx, actual := range refs {
   118  				expected := c.resolutions[idx]
   119  
   120  				if string(actual.Reference().RealPath) != expected.path {
   121  					t.Errorf("bad resolve path: '%s'!='%s'", string(actual.Reference().RealPath), expected.path)
   122  				}
   123  
   124  				if expected.path != "" && string(actual.Reference().RealPath) != actual.RealPath {
   125  					t.Errorf("we should always prefer real paths over ones with links")
   126  				}
   127  
   128  				layer := img.FileCatalog.Layer(actual.Reference())
   129  				if layer.Metadata.Index != expected.layer {
   130  					t.Errorf("bad resolve layer: '%d'!='%d'", layer.Metadata.Index, expected.layer)
   131  				}
   132  			}
   133  		})
   134  	}
   135  }
   136  
   137  func TestAllLayersResolver_FilesByGlob(t *testing.T) {
   138  	cases := []struct {
   139  		name        string
   140  		glob        string
   141  		resolutions []resolution
   142  	}{
   143  		{
   144  			name: "link with previous data",
   145  			glob: "**/*ink-1",
   146  			resolutions: []resolution{
   147  				{
   148  					layer: 1,
   149  					path:  "/file-1.txt",
   150  				},
   151  			},
   152  		},
   153  		{
   154  			name: "link with in layer data",
   155  			glob: "**/*nk-within",
   156  			resolutions: []resolution{
   157  				{
   158  					layer: 5,
   159  					path:  "/file-3.txt",
   160  				},
   161  			},
   162  		},
   163  		{
   164  			name: "link with overridden data",
   165  			glob: "**/*ink-2",
   166  			resolutions: []resolution{
   167  				{
   168  					layer: 4,
   169  					path:  "/file-2.txt",
   170  				},
   171  				{
   172  					layer: 7,
   173  					path:  "/file-2.txt",
   174  				},
   175  			},
   176  		},
   177  		{
   178  			name: "indirect link (with overridden data)",
   179  			glob: "**/*nk-indirect",
   180  			resolutions: []resolution{
   181  				{
   182  					layer: 4,
   183  					path:  "/file-2.txt",
   184  				},
   185  				{
   186  					layer: 7,
   187  					path:  "/file-2.txt",
   188  				},
   189  			},
   190  		},
   191  		{
   192  			name:        "dead link",
   193  			glob:        "**/*k-dead",
   194  			resolutions: []resolution{},
   195  		},
   196  		{
   197  			name:        "ignore directories",
   198  			glob:        "**/bin",
   199  			resolutions: []resolution{},
   200  		},
   201  	}
   202  	for _, c := range cases {
   203  		t.Run(c.name, func(t *testing.T) {
   204  			img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
   205  
   206  			resolver, err := NewFromContainerImageAllLayers(img)
   207  			require.NoError(t, err)
   208  
   209  			refs, err := resolver.FilesByGlob(c.glob)
   210  			require.NoError(t, err)
   211  
   212  			if len(refs) != len(c.resolutions) {
   213  				t.Fatalf("unexpected number of resolutions: %d", len(refs))
   214  			}
   215  
   216  			for idx, actual := range refs {
   217  				expected := c.resolutions[idx]
   218  
   219  				if string(actual.Reference().RealPath) != expected.path {
   220  					t.Errorf("bad resolve path: '%s'!='%s'", string(actual.Reference().RealPath), expected.path)
   221  				}
   222  
   223  				if expected.path != "" && string(actual.Reference().RealPath) != actual.RealPath {
   224  					t.Errorf("we should always prefer real paths over ones with links")
   225  				}
   226  
   227  				layer := img.FileCatalog.Layer(actual.Reference())
   228  
   229  				if layer.Metadata.Index != expected.layer {
   230  					t.Errorf("bad resolve layer: '%d'!='%d'", layer.Metadata.Index, expected.layer)
   231  				}
   232  			}
   233  		})
   234  	}
   235  }
   236  
   237  func Test_imageAllLayersResolver_FilesByMIMEType(t *testing.T) {
   238  
   239  	tests := []struct {
   240  		fixtureName   string
   241  		mimeType      string
   242  		expectedPaths []string
   243  	}{
   244  		{
   245  			fixtureName:   "image-duplicate-path",
   246  			mimeType:      "text/plain",
   247  			expectedPaths: []string{"/somefile-1.txt", "/somefile-1.txt"},
   248  		},
   249  	}
   250  	for _, test := range tests {
   251  		t.Run(test.fixtureName, func(t *testing.T) {
   252  			img := imagetest.GetFixtureImage(t, "docker-archive", test.fixtureName)
   253  
   254  			resolver, err := NewFromContainerImageAllLayers(img)
   255  			assert.NoError(t, err)
   256  
   257  			locations, err := resolver.FilesByMIMEType(test.mimeType)
   258  			assert.NoError(t, err)
   259  
   260  			assert.Len(t, test.expectedPaths, len(locations))
   261  			for idx, l := range locations {
   262  				assert.Equal(t, test.expectedPaths[idx], l.RealPath, "does not have path %q", l.RealPath)
   263  			}
   264  		})
   265  	}
   266  }
   267  
   268  func Test_imageAllLayersResolver_hasFilesystemIDInLocation(t *testing.T) {
   269  	img := imagetest.GetFixtureImage(t, "docker-archive", "image-duplicate-path")
   270  
   271  	resolver, err := NewFromContainerImageAllLayers(img)
   272  	assert.NoError(t, err)
   273  
   274  	locations, err := resolver.FilesByMIMEType("text/plain")
   275  	assert.NoError(t, err)
   276  	assert.NotEmpty(t, locations)
   277  	for _, location := range locations {
   278  		assert.NotEmpty(t, location.FileSystemID)
   279  	}
   280  
   281  	locations, err = resolver.FilesByGlob("*.txt")
   282  	assert.NoError(t, err)
   283  	assert.NotEmpty(t, locations)
   284  	for _, location := range locations {
   285  		assert.NotEmpty(t, location.FileSystemID)
   286  	}
   287  
   288  	locations, err = resolver.FilesByPath("/somefile-1.txt")
   289  	assert.NoError(t, err)
   290  	assert.NotEmpty(t, locations)
   291  	for _, location := range locations {
   292  		assert.NotEmpty(t, location.FileSystemID)
   293  	}
   294  
   295  }
   296  
   297  func TestAllLayersImageResolver_FilesContents(t *testing.T) {
   298  
   299  	tests := []struct {
   300  		name     string
   301  		fixture  string
   302  		contents []string
   303  	}{
   304  		{
   305  			name:    "one degree",
   306  			fixture: "link-2",
   307  			contents: []string{
   308  				"file 2!",            // from the first resolved layer's perspective
   309  				"NEW file override!", // from the second resolved layers perspective
   310  			},
   311  		},
   312  		{
   313  			name:    "two degrees",
   314  			fixture: "link-indirect",
   315  			contents: []string{
   316  				"file 2!",
   317  				"NEW file override!",
   318  			},
   319  		},
   320  		{
   321  			name:     "dead link",
   322  			fixture:  "link-dead",
   323  			contents: []string{},
   324  		},
   325  	}
   326  
   327  	for _, test := range tests {
   328  		t.Run(test.name, func(t *testing.T) {
   329  			img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
   330  
   331  			resolver, err := NewFromContainerImageAllLayers(img)
   332  			assert.NoError(t, err)
   333  
   334  			refs, err := resolver.FilesByPath(test.fixture)
   335  			require.NoError(t, err)
   336  
   337  			// the given path should have an overridden file
   338  			require.Len(t, refs, len(test.contents))
   339  
   340  			for idx, loc := range refs {
   341  				reader, err := resolver.FileContentsByLocation(loc)
   342  				require.NoError(t, err)
   343  
   344  				actual, err := io.ReadAll(reader)
   345  				require.NoError(t, err)
   346  
   347  				assert.Equal(t, test.contents[idx], string(actual))
   348  			}
   349  
   350  		})
   351  	}
   352  }
   353  
   354  func TestAllLayersImageResolver_FilesContents_errorOnDirRequest(t *testing.T) {
   355  
   356  	img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
   357  
   358  	resolver, err := NewFromContainerImageAllLayers(img)
   359  	assert.NoError(t, err)
   360  
   361  	var dirLoc *file.Location
   362  	ctx, cancel := context.WithCancel(context.Background())
   363  	defer cancel()
   364  	for loc := range resolver.AllLocations(ctx) {
   365  		entry, err := resolver.img.FileCatalog.Get(loc.Reference())
   366  		require.NoError(t, err)
   367  		if entry.Metadata.IsDir() {
   368  			dirLoc = &loc
   369  			break
   370  		}
   371  	}
   372  
   373  	require.NotNil(t, dirLoc)
   374  
   375  	reader, err := resolver.FileContentsByLocation(*dirLoc)
   376  	require.Error(t, err)
   377  	require.Nil(t, reader)
   378  }
   379  
   380  func Test_imageAllLayersResolver_resolvesLinks(t *testing.T) {
   381  	tests := []struct {
   382  		name     string
   383  		runner   func(file.Resolver) []file.Location
   384  		expected []file.Location
   385  	}{
   386  		{
   387  			name: "by mimetype",
   388  			runner: func(resolver file.Resolver) []file.Location {
   389  				// links should not show up when searching mimetype
   390  				actualLocations, err := resolver.FilesByMIMEType("text/plain")
   391  				assert.NoError(t, err)
   392  				return actualLocations
   393  			},
   394  			expected: []file.Location{
   395  				file.NewVirtualLocation("/etc/group", "/etc/group"),
   396  				file.NewVirtualLocation("/etc/passwd", "/etc/passwd"),
   397  				file.NewVirtualLocation("/etc/shadow", "/etc/shadow"),
   398  				file.NewVirtualLocation("/file-1.txt", "/file-1.txt"),
   399  				file.NewVirtualLocation("/file-2.txt", "/file-2.txt"), // copy 1
   400  				// note: we're de-duping the redundant access to file-3.txt
   401  				// ... (there would usually be two copies)
   402  				file.NewVirtualLocation("/file-3.txt", "/file-3.txt"),
   403  				file.NewVirtualLocation("/file-2.txt", "/file-2.txt"),               // copy 2
   404  				file.NewVirtualLocation("/parent/file-4.txt", "/parent/file-4.txt"), // copy 1
   405  				file.NewVirtualLocation("/parent/file-4.txt", "/parent/file-4.txt"), // copy 2
   406  			},
   407  		},
   408  		{
   409  			name: "by glob to links",
   410  			runner: func(resolver file.Resolver) []file.Location {
   411  				// links are searched, but resolve to the real files
   412  				actualLocations, err := resolver.FilesByGlob("*ink-*")
   413  				assert.NoError(t, err)
   414  				return actualLocations
   415  			},
   416  			expected: []file.Location{
   417  				file.NewVirtualLocation("/file-1.txt", "/link-1"),
   418  				file.NewVirtualLocation("/file-2.txt", "/link-2"), // copy 1
   419  				file.NewVirtualLocation("/file-2.txt", "/link-2"), // copy 2
   420  				file.NewVirtualLocation("/file-3.txt", "/link-within"),
   421  			},
   422  		},
   423  		{
   424  			name: "by basename",
   425  			runner: func(resolver file.Resolver) []file.Location {
   426  				// links are searched, but resolve to the real files
   427  				actualLocations, err := resolver.FilesByGlob("**/file-2.txt")
   428  				assert.NoError(t, err)
   429  				return actualLocations
   430  			},
   431  			expected: []file.Location{
   432  				file.NewVirtualLocation("/file-2.txt", "/file-2.txt"), // copy 1
   433  				file.NewVirtualLocation("/file-2.txt", "/file-2.txt"), // copy 2
   434  			},
   435  		},
   436  		{
   437  			name: "by basename glob",
   438  			runner: func(resolver file.Resolver) []file.Location {
   439  				// links are searched, but resolve to the real files
   440  				actualLocations, err := resolver.FilesByGlob("**/file-?.txt")
   441  				assert.NoError(t, err)
   442  				return actualLocations
   443  			},
   444  			expected: []file.Location{
   445  				file.NewVirtualLocation("/file-1.txt", "/file-1.txt"),
   446  				file.NewVirtualLocation("/file-2.txt", "/file-2.txt"), // copy 1
   447  				file.NewVirtualLocation("/file-2.txt", "/file-2.txt"), // copy 2
   448  				file.NewVirtualLocation("/file-3.txt", "/file-3.txt"),
   449  				file.NewVirtualLocation("/parent/file-4.txt", "/parent/file-4.txt"),
   450  				file.NewVirtualLocation("/parent/file-4.txt", "/parent/file-4.txt"), // when we copy into the link path, the same file-4.txt is copied
   451  			},
   452  		},
   453  		{
   454  			name: "by extension",
   455  			runner: func(resolver file.Resolver) []file.Location {
   456  				// links are searched, but resolve to the real files
   457  				actualLocations, err := resolver.FilesByGlob("**/*.txt")
   458  				assert.NoError(t, err)
   459  				return actualLocations
   460  			},
   461  			expected: []file.Location{
   462  				file.NewVirtualLocation("/file-1.txt", "/file-1.txt"),
   463  				file.NewVirtualLocation("/file-2.txt", "/file-2.txt"), // copy 1
   464  				file.NewVirtualLocation("/file-2.txt", "/file-2.txt"), // copy 2
   465  				file.NewVirtualLocation("/file-3.txt", "/file-3.txt"),
   466  				file.NewVirtualLocation("/parent/file-4.txt", "/parent/file-4.txt"),
   467  				file.NewVirtualLocation("/parent/file-4.txt", "/parent/file-4.txt"), // when we copy into the link path, the same file-4.txt is copied
   468  			},
   469  		},
   470  		{
   471  			name: "by path to degree 1 link",
   472  			runner: func(resolver file.Resolver) []file.Location {
   473  				// links resolve to the final file
   474  				actualLocations, err := resolver.FilesByPath("/link-2")
   475  				assert.NoError(t, err)
   476  				return actualLocations
   477  			},
   478  			expected: []file.Location{
   479  				// we have multiple copies across layers
   480  				file.NewVirtualLocation("/file-2.txt", "/link-2"),
   481  				file.NewVirtualLocation("/file-2.txt", "/link-2"),
   482  			},
   483  		},
   484  		{
   485  			name: "by path to degree 2 link",
   486  			runner: func(resolver file.Resolver) []file.Location {
   487  				// multiple links resolves to the final file
   488  				actualLocations, err := resolver.FilesByPath("/link-indirect")
   489  				assert.NoError(t, err)
   490  				return actualLocations
   491  			},
   492  			expected: []file.Location{
   493  				// we have multiple copies across layers
   494  				file.NewVirtualLocation("/file-2.txt", "/link-indirect"),
   495  				file.NewVirtualLocation("/file-2.txt", "/link-indirect"),
   496  			},
   497  		},
   498  	}
   499  
   500  	for _, test := range tests {
   501  		t.Run(test.name, func(t *testing.T) {
   502  
   503  			img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
   504  
   505  			resolver, err := NewFromContainerImageAllLayers(img)
   506  			assert.NoError(t, err)
   507  
   508  			actual := test.runner(resolver)
   509  
   510  			compareLocations(t, test.expected, actual)
   511  		})
   512  	}
   513  
   514  }
   515  
   516  func TestAllLayersResolver_AllLocations(t *testing.T) {
   517  	img := imagetest.GetFixtureImage(t, "docker-archive", "image-files-deleted")
   518  
   519  	resolver, err := NewFromContainerImageAllLayers(img)
   520  	assert.NoError(t, err)
   521  
   522  	paths := strset.New()
   523  	ctx, cancel := context.WithCancel(context.Background())
   524  	defer cancel()
   525  	visibleSet := strset.New()
   526  	hiddenSet := strset.New()
   527  	for loc := range resolver.AllLocations(ctx) {
   528  		paths.Add(loc.RealPath)
   529  		switch loc.Annotations[file.VisibleAnnotationKey] {
   530  		case file.VisibleAnnotation:
   531  			visibleSet.Add(loc.RealPath)
   532  		case file.HiddenAnnotation:
   533  			hiddenSet.Add(loc.RealPath)
   534  		case "":
   535  			t.Errorf("expected visibility annotation for location: %+v", loc)
   536  		}
   537  	}
   538  	visible := []string{
   539  		"/Dockerfile",
   540  		"/file-3.txt", // this is a deadlink pointing to /file-1.txt (which has been deleted)
   541  		"/target",
   542  		"/target/file-2.txt",
   543  	}
   544  	hidden := []string{
   545  		"/.wh.bin",
   546  		"/.wh.file-1.txt",
   547  		"/.wh.lib",
   548  		"/bin",
   549  		"/bin/arch",
   550  		"/bin/ash",
   551  		"/bin/base64",
   552  		"/bin/bbconfig",
   553  		"/bin/busybox",
   554  		"/bin/cat",
   555  		"/bin/chattr",
   556  		"/bin/chgrp",
   557  		"/bin/chmod",
   558  		"/bin/chown",
   559  		"/bin/cp",
   560  		"/bin/date",
   561  		"/bin/dd",
   562  		"/bin/df",
   563  		"/bin/dmesg",
   564  		"/bin/dnsdomainname",
   565  		"/bin/dumpkmap",
   566  		"/bin/echo",
   567  		"/bin/ed",
   568  		"/bin/egrep",
   569  		"/bin/false",
   570  		"/bin/fatattr",
   571  		"/bin/fdflush",
   572  		"/bin/fgrep",
   573  		"/bin/fsync",
   574  		"/bin/getopt",
   575  		"/bin/grep",
   576  		"/bin/gunzip",
   577  		"/bin/gzip",
   578  		"/bin/hostname",
   579  		"/bin/ionice",
   580  		"/bin/iostat",
   581  		"/bin/ipcalc",
   582  		"/bin/kbd_mode",
   583  		"/bin/kill",
   584  		"/bin/link",
   585  		"/bin/linux32",
   586  		"/bin/linux64",
   587  		"/bin/ln",
   588  		"/bin/login",
   589  		"/bin/ls",
   590  		"/bin/lsattr",
   591  		"/bin/lzop",
   592  		"/bin/makemime",
   593  		"/bin/mkdir",
   594  		"/bin/mknod",
   595  		"/bin/mktemp",
   596  		"/bin/more",
   597  		"/bin/mount",
   598  		"/bin/mountpoint",
   599  		"/bin/mpstat",
   600  		"/bin/mv",
   601  		"/bin/netstat",
   602  		"/bin/nice",
   603  		"/bin/pidof",
   604  		"/bin/ping",
   605  		"/bin/ping6",
   606  		"/bin/pipe_progress",
   607  		"/bin/printenv",
   608  		"/bin/ps",
   609  		"/bin/pwd",
   610  		"/bin/reformime",
   611  		"/bin/rev",
   612  		"/bin/rm",
   613  		"/bin/rmdir",
   614  		"/bin/run-parts",
   615  		"/bin/sed",
   616  		"/bin/setpriv",
   617  		"/bin/setserial",
   618  		"/bin/sh",
   619  		"/bin/sleep",
   620  		"/bin/stat",
   621  		"/bin/stty",
   622  		"/bin/su",
   623  		"/bin/sync",
   624  		"/bin/tar",
   625  		"/bin/touch",
   626  		"/bin/true",
   627  		"/bin/umount",
   628  		"/bin/uname",
   629  		"/bin/usleep",
   630  		"/bin/watch",
   631  		"/bin/zcat",
   632  		"/file-1.txt",
   633  		"/lib",
   634  		"/lib/apk",
   635  		"/lib/apk/db",
   636  		"/lib/apk/db/installed",
   637  		"/lib/apk/db/lock",
   638  		"/lib/apk/db/scripts.tar",
   639  		"/lib/apk/db/triggers",
   640  		"/lib/apk/exec",
   641  		"/lib/firmware",
   642  		"/lib/ld-musl-x86_64.so.1",
   643  		"/lib/libapk.so.3.12.0",
   644  		"/lib/libc.musl-x86_64.so.1",
   645  		"/lib/libcrypto.so.3",
   646  		"/lib/libssl.so.3",
   647  		"/lib/libz.so.1",
   648  		"/lib/libz.so.1.2.13",
   649  		"/lib/mdev",
   650  		"/lib/modules-load.d",
   651  		"/lib/sysctl.d",
   652  		"/lib/sysctl.d/00-alpine.conf",
   653  	}
   654  
   655  	var expected []string
   656  	expected = append(expected, visible...)
   657  	expected = append(expected, hidden...)
   658  	sort.Strings(expected)
   659  
   660  	cleanPaths := func(s *strset.Set) {
   661  		// depending on how the image is built (either from linux or mac), sys and proc might accidentally be added to the image.
   662  		// this isn't important for the test, so we remove them.
   663  		s.Remove("/proc", "/sys", "/dev", "/etc")
   664  
   665  		// Remove cache created by Mac Rosetta when emulating different arches
   666  		s.Remove("/.cache/rosetta", "/.cache")
   667  	}
   668  
   669  	cleanPaths(paths)
   670  	cleanPaths(visibleSet)
   671  	cleanPaths(hiddenSet)
   672  
   673  	pathsList := paths.List()
   674  	sort.Strings(pathsList)
   675  	visibleSetList := visibleSet.List()
   676  	sort.Strings(visibleSetList)
   677  	hiddenSetList := hiddenSet.List()
   678  	sort.Strings(hiddenSetList)
   679  
   680  	if d := cmp.Diff(expected, pathsList); d != "" {
   681  		t.Errorf("unexpected paths (-want +got):\n%s", d)
   682  	}
   683  
   684  	if d := cmp.Diff(visible, visibleSetList); d != "" {
   685  		t.Errorf("unexpected visible paths (-want +got):\n%s", d)
   686  	}
   687  
   688  	if d := cmp.Diff(hidden, hiddenSetList); d != "" {
   689  		t.Errorf("unexpected hidden paths (-want +got):\n%s", d)
   690  	}
   691  
   692  }