github.com/anchore/syft@v1.38.2/syft/source/filesource/file_source_test.go (about) 1 package filesource 2 3 import ( 4 "io" 5 "os" 6 "os/exec" 7 "path" 8 "path/filepath" 9 "syscall" 10 "testing" 11 12 "github.com/stretchr/testify/assert" 13 "github.com/stretchr/testify/require" 14 15 "github.com/anchore/syft/syft/artifact" 16 "github.com/anchore/syft/syft/file" 17 "github.com/anchore/syft/syft/internal/testutil" 18 "github.com/anchore/syft/syft/source" 19 ) 20 21 func TestNewFromFile(t *testing.T) { 22 testutil.Chdir(t, "..") // run with source/test-fixtures 23 24 testCases := []struct { 25 desc string 26 input string 27 expString string 28 testPathFn func(file.Resolver) ([]file.Location, error) 29 expRefs int 30 }{ 31 { 32 desc: "path detected by glob", 33 input: "test-fixtures/file-index-filter/.vimrc", 34 testPathFn: func(resolver file.Resolver) ([]file.Location, error) { 35 return resolver.FilesByGlob("**/.vimrc", "**/.2", "**/.1/*", "**/empty") 36 }, 37 expRefs: 1, 38 }, 39 { 40 desc: "path detected by abs path", 41 input: "test-fixtures/file-index-filter/.vimrc", 42 testPathFn: func(resolver file.Resolver) ([]file.Location, error) { 43 return resolver.FilesByPath("/.vimrc", "/.2", "/.1/something", "/empty") 44 }, 45 expRefs: 1, 46 }, 47 { 48 desc: "path detected by relative path", 49 input: "test-fixtures/file-index-filter/.vimrc", 50 testPathFn: func(resolver file.Resolver) ([]file.Location, error) { 51 return resolver.FilesByPath(".vimrc", "/.2", "/.1/something", "empty") 52 }, 53 expRefs: 1, 54 }, 55 { 56 desc: "normal path", 57 input: "test-fixtures/actual-path/empty", 58 testPathFn: func(resolver file.Resolver) ([]file.Location, error) { 59 return resolver.FilesByPath("empty") 60 }, 61 expRefs: 1, 62 }, 63 { 64 desc: "path containing symlink", 65 input: "test-fixtures/symlink/empty", 66 testPathFn: func(resolver file.Resolver) ([]file.Location, error) { 67 return resolver.FilesByPath("empty") 68 }, 69 expRefs: 1, 70 }, 71 } 72 for _, test := range testCases { 73 t.Run(test.desc, func(t *testing.T) { 74 src, err := New(Config{ 75 Path: test.input, 76 }) 77 require.NoError(t, err) 78 t.Cleanup(func() { 79 require.NoError(t, src.Close()) 80 }) 81 82 assert.Equal(t, test.input, src.Describe().Metadata.(source.FileMetadata).Path) 83 84 res, err := src.FileResolver(source.SquashedScope) 85 require.NoError(t, err) 86 87 refs, err := test.testPathFn(res) 88 require.NoError(t, err) 89 require.Len(t, refs, test.expRefs) 90 if test.expRefs == 1 { 91 assert.Equal(t, path.Base(test.input), path.Base(refs[0].RealPath)) 92 } 93 94 }) 95 } 96 } 97 98 func TestNewFromFile_WithArchive(t *testing.T) { 99 testutil.Chdir(t, "..") // run with source/test-fixtures 100 101 testCases := []struct { 102 desc string 103 input string 104 expString string 105 inputPaths []string 106 expRefs int 107 layer2 bool 108 contents string 109 skipExtractArchive bool 110 }{ 111 { 112 desc: "path detected", 113 input: "test-fixtures/path-detected", 114 inputPaths: []string{"/.vimrc"}, 115 expRefs: 1, 116 }, 117 { 118 desc: "use first entry for duplicate paths", 119 input: "test-fixtures/path-detected", 120 inputPaths: []string{"/.vimrc"}, 121 expRefs: 1, 122 layer2: true, 123 contents: "Another .vimrc file", 124 }, 125 { 126 desc: "skip extract archive", 127 input: "test-fixtures/path-detected", 128 inputPaths: []string{"/.vimrc"}, 129 expRefs: 0, 130 layer2: false, 131 skipExtractArchive: true, 132 }, 133 } 134 for _, test := range testCases { 135 t.Run(test.desc, func(t *testing.T) { 136 archivePath := setupArchiveTest(t, test.input, test.layer2) 137 138 cfg := Config{ 139 Path: archivePath, 140 SkipExtractArchive: test.skipExtractArchive, 141 } 142 143 src, err := New(cfg) 144 require.NoError(t, err) 145 t.Cleanup(func() { 146 require.NoError(t, src.Close()) 147 }) 148 149 assert.Equal(t, archivePath, src.Describe().Metadata.(source.FileMetadata).Path) 150 151 res, err := src.FileResolver(source.SquashedScope) 152 require.NoError(t, err) 153 154 refs, err := res.FilesByPath(test.inputPaths...) 155 require.NoError(t, err) 156 assert.Len(t, refs, test.expRefs) 157 158 if test.contents != "" { 159 reader, err := res.FileContentsByLocation(refs[0]) 160 require.NoError(t, err) 161 162 data, err := io.ReadAll(reader) 163 require.NoError(t, err) 164 165 assert.Equal(t, test.contents, string(data)) 166 } 167 168 }) 169 } 170 } 171 172 // setupArchiveTest encapsulates common test setup work for tar file tests. It returns a cleanup function, 173 // which should be called (typically deferred) by the caller, the path of the created tar archive, and an error, 174 // which should trigger a fatal test failure in the consuming test. The returned cleanup function will never be nil 175 // (even if there's an error), and it should always be called. 176 func setupArchiveTest(t testing.TB, sourceDirPath string, layer2 bool) string { 177 t.Helper() 178 179 archivePrefix, err := os.CreateTemp("", "syft-archive-TEST-") 180 require.NoError(t, err) 181 182 t.Cleanup(func() { 183 assert.NoError(t, os.Remove(archivePrefix.Name())) 184 }) 185 186 destinationArchiveFilePath := archivePrefix.Name() + ".tar" 187 t.Logf("archive path: %s", destinationArchiveFilePath) 188 createArchive(t, sourceDirPath, destinationArchiveFilePath, layer2) 189 190 t.Cleanup(func() { 191 assert.NoError(t, os.Remove(destinationArchiveFilePath)) 192 }) 193 194 cwd, err := os.Getwd() 195 require.NoError(t, err) 196 197 t.Logf("running from: %s", cwd) 198 199 return destinationArchiveFilePath 200 } 201 202 // createArchive creates a new archive file at destinationArchivePath based on the directory found at sourceDirPath. 203 func createArchive(t testing.TB, sourceDirPath, destinationArchivePath string, layer2 bool) { 204 t.Helper() 205 206 cwd, err := os.Getwd() 207 if err != nil { 208 t.Fatalf("unable to get cwd: %+v", err) 209 } 210 211 cmd := exec.Command("./generate-tar-fixture-from-source-dir.sh", destinationArchivePath, path.Base(sourceDirPath)) 212 cmd.Dir = filepath.Join(cwd, "test-fixtures") 213 214 if err := cmd.Start(); err != nil { 215 t.Fatalf("unable to start generate zip fixture script: %+v", err) 216 } 217 218 if err := cmd.Wait(); err != nil { 219 if exiterr, ok := err.(*exec.ExitError); ok { 220 // The program has exited with an exit code != 0 221 222 // This works on both Unix and Windows. Although package 223 // syscall is generally platform dependent, WaitStatus is 224 // defined for both Unix and Windows and in both cases has 225 // an ExitStatus() method with the same signature. 226 if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { 227 if status.ExitStatus() != 0 { 228 t.Fatalf("failed to generate fixture: rc=%d", status.ExitStatus()) 229 } 230 } 231 } else { 232 t.Fatalf("unable to get generate fixture script result: %+v", err) 233 } 234 } 235 236 if layer2 { 237 cmd = exec.Command("tar", "-rvf", destinationArchivePath, ".") 238 cmd.Dir = filepath.Join(cwd, "test-fixtures", path.Base(sourceDirPath+"-2")) 239 if err := cmd.Start(); err != nil { 240 t.Fatalf("unable to start tar appending fixture script: %+v", err) 241 } 242 _ = cmd.Wait() 243 } 244 } 245 246 func Test_FileSource_ID(t *testing.T) { 247 testutil.Chdir(t, "..") // run with source/test-fixtures 248 249 tests := []struct { 250 name string 251 cfg Config 252 want artifact.ID 253 wantDigest string 254 wantErr require.ErrorAssertionFunc 255 }{ 256 { 257 name: "empty", 258 cfg: Config{}, 259 wantErr: require.Error, 260 }, 261 { 262 name: "does not exist", 263 cfg: Config{ 264 Path: "./test-fixtures/does-not-exist", 265 }, 266 wantErr: require.Error, 267 }, 268 { 269 name: "to dir", 270 cfg: Config{ 271 Path: "./test-fixtures/image-simple", 272 }, 273 wantErr: require.Error, 274 }, 275 { 276 name: "with path", 277 cfg: Config{Path: "./test-fixtures/image-simple/Dockerfile"}, 278 want: artifact.ID("db7146472cf6d49b3ac01b42812fb60020b0b4898b97491b21bb690c808d5159"), 279 wantDigest: "sha256:38601c0bb4269a10ce1d00590ea7689c1117dd9274c758653934ab4f2016f80f", 280 }, 281 { 282 name: "with path and alias", 283 cfg: Config{ 284 Path: "./test-fixtures/image-simple/Dockerfile", 285 Alias: source.Alias{ 286 Name: "name-me-that!", 287 Version: "version-me-this!", 288 }, 289 }, 290 want: artifact.ID("3c713003305ac6605255cec8bf4ea649aa44b2b9a9f3a07bd683869d1363438a"), 291 wantDigest: "sha256:38601c0bb4269a10ce1d00590ea7689c1117dd9274c758653934ab4f2016f80f", 292 }, 293 { 294 name: "other fields do not affect ID", 295 cfg: Config{ 296 Path: "test-fixtures/image-simple/Dockerfile", 297 Exclude: source.ExcludeConfig{ 298 Paths: []string{"a", "b"}, 299 }, 300 }, 301 want: artifact.ID("db7146472cf6d49b3ac01b42812fb60020b0b4898b97491b21bb690c808d5159"), 302 wantDigest: "sha256:38601c0bb4269a10ce1d00590ea7689c1117dd9274c758653934ab4f2016f80f", 303 }, 304 } 305 for _, tt := range tests { 306 t.Run(tt.name, func(t *testing.T) { 307 if tt.wantErr == nil { 308 tt.wantErr = require.NoError 309 } 310 newSource, err := New(tt.cfg) 311 tt.wantErr(t, err) 312 if err != nil { 313 return 314 } 315 s := newSource.(*fileSource) 316 assert.Equalf(t, tt.want, s.ID(), "ID() mismatch") 317 assert.Equalf(t, tt.wantDigest, s.digestForVersion, "digestForVersion mismatch") 318 }) 319 } 320 }