github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/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 }{ 110 { 111 desc: "path detected", 112 input: "test-fixtures/path-detected", 113 inputPaths: []string{"/.vimrc"}, 114 expRefs: 1, 115 }, 116 { 117 desc: "use first entry for duplicate paths", 118 input: "test-fixtures/path-detected", 119 inputPaths: []string{"/.vimrc"}, 120 expRefs: 1, 121 layer2: true, 122 contents: "Another .vimrc file", 123 }, 124 } 125 for _, test := range testCases { 126 t.Run(test.desc, func(t *testing.T) { 127 archivePath := setupArchiveTest(t, test.input, test.layer2) 128 129 src, err := New(Config{ 130 Path: archivePath, 131 }) 132 require.NoError(t, err) 133 t.Cleanup(func() { 134 require.NoError(t, src.Close()) 135 }) 136 137 assert.Equal(t, archivePath, src.Describe().Metadata.(source.FileMetadata).Path) 138 139 res, err := src.FileResolver(source.SquashedScope) 140 require.NoError(t, err) 141 142 refs, err := res.FilesByPath(test.inputPaths...) 143 require.NoError(t, err) 144 assert.Len(t, refs, test.expRefs) 145 146 if test.contents != "" { 147 reader, err := res.FileContentsByLocation(refs[0]) 148 require.NoError(t, err) 149 150 data, err := io.ReadAll(reader) 151 require.NoError(t, err) 152 153 assert.Equal(t, test.contents, string(data)) 154 } 155 156 }) 157 } 158 } 159 160 // setupArchiveTest encapsulates common test setup work for tar file tests. It returns a cleanup function, 161 // which should be called (typically deferred) by the caller, the path of the created tar archive, and an error, 162 // which should trigger a fatal test failure in the consuming test. The returned cleanup function will never be nil 163 // (even if there's an error), and it should always be called. 164 func setupArchiveTest(t testing.TB, sourceDirPath string, layer2 bool) string { 165 t.Helper() 166 167 archivePrefix, err := os.CreateTemp("", "syft-archive-TEST-") 168 require.NoError(t, err) 169 170 t.Cleanup(func() { 171 assert.NoError(t, os.Remove(archivePrefix.Name())) 172 }) 173 174 destinationArchiveFilePath := archivePrefix.Name() + ".tar" 175 t.Logf("archive path: %s", destinationArchiveFilePath) 176 createArchive(t, sourceDirPath, destinationArchiveFilePath, layer2) 177 178 t.Cleanup(func() { 179 assert.NoError(t, os.Remove(destinationArchiveFilePath)) 180 }) 181 182 cwd, err := os.Getwd() 183 require.NoError(t, err) 184 185 t.Logf("running from: %s", cwd) 186 187 return destinationArchiveFilePath 188 } 189 190 // createArchive creates a new archive file at destinationArchivePath based on the directory found at sourceDirPath. 191 func createArchive(t testing.TB, sourceDirPath, destinationArchivePath string, layer2 bool) { 192 t.Helper() 193 194 cwd, err := os.Getwd() 195 if err != nil { 196 t.Fatalf("unable to get cwd: %+v", err) 197 } 198 199 cmd := exec.Command("./generate-tar-fixture-from-source-dir.sh", destinationArchivePath, path.Base(sourceDirPath)) 200 cmd.Dir = filepath.Join(cwd, "test-fixtures") 201 202 if err := cmd.Start(); err != nil { 203 t.Fatalf("unable to start generate zip fixture script: %+v", err) 204 } 205 206 if err := cmd.Wait(); err != nil { 207 if exiterr, ok := err.(*exec.ExitError); ok { 208 // The program has exited with an exit code != 0 209 210 // This works on both Unix and Windows. Although package 211 // syscall is generally platform dependent, WaitStatus is 212 // defined for both Unix and Windows and in both cases has 213 // an ExitStatus() method with the same signature. 214 if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { 215 if status.ExitStatus() != 0 { 216 t.Fatalf("failed to generate fixture: rc=%d", status.ExitStatus()) 217 } 218 } 219 } else { 220 t.Fatalf("unable to get generate fixture script result: %+v", err) 221 } 222 } 223 224 if layer2 { 225 cmd = exec.Command("tar", "-rvf", destinationArchivePath, ".") 226 cmd.Dir = filepath.Join(cwd, "test-fixtures", path.Base(sourceDirPath+"-2")) 227 if err := cmd.Start(); err != nil { 228 t.Fatalf("unable to start tar appending fixture script: %+v", err) 229 } 230 _ = cmd.Wait() 231 } 232 } 233 234 func Test_FileSource_ID(t *testing.T) { 235 testutil.Chdir(t, "..") // run with source/test-fixtures 236 237 tests := []struct { 238 name string 239 cfg Config 240 want artifact.ID 241 wantDigest string 242 wantErr require.ErrorAssertionFunc 243 }{ 244 { 245 name: "empty", 246 cfg: Config{}, 247 wantErr: require.Error, 248 }, 249 { 250 name: "does not exist", 251 cfg: Config{ 252 Path: "./test-fixtures/does-not-exist", 253 }, 254 wantErr: require.Error, 255 }, 256 { 257 name: "to dir", 258 cfg: Config{ 259 Path: "./test-fixtures/image-simple", 260 }, 261 wantErr: require.Error, 262 }, 263 { 264 name: "with path", 265 cfg: Config{Path: "./test-fixtures/image-simple/Dockerfile"}, 266 want: artifact.ID("db7146472cf6d49b3ac01b42812fb60020b0b4898b97491b21bb690c808d5159"), 267 wantDigest: "sha256:38601c0bb4269a10ce1d00590ea7689c1117dd9274c758653934ab4f2016f80f", 268 }, 269 { 270 name: "with path and alias", 271 cfg: Config{ 272 Path: "./test-fixtures/image-simple/Dockerfile", 273 Alias: source.Alias{ 274 Name: "name-me-that!", 275 Version: "version-me-this!", 276 }, 277 }, 278 want: artifact.ID("3c713003305ac6605255cec8bf4ea649aa44b2b9a9f3a07bd683869d1363438a"), 279 wantDigest: "sha256:38601c0bb4269a10ce1d00590ea7689c1117dd9274c758653934ab4f2016f80f", 280 }, 281 { 282 name: "other fields do not affect ID", 283 cfg: Config{ 284 Path: "test-fixtures/image-simple/Dockerfile", 285 Exclude: source.ExcludeConfig{ 286 Paths: []string{"a", "b"}, 287 }, 288 }, 289 want: artifact.ID("db7146472cf6d49b3ac01b42812fb60020b0b4898b97491b21bb690c808d5159"), 290 wantDigest: "sha256:38601c0bb4269a10ce1d00590ea7689c1117dd9274c758653934ab4f2016f80f", 291 }, 292 } 293 for _, tt := range tests { 294 t.Run(tt.name, func(t *testing.T) { 295 if tt.wantErr == nil { 296 tt.wantErr = require.NoError 297 } 298 newSource, err := New(tt.cfg) 299 tt.wantErr(t, err) 300 if err != nil { 301 return 302 } 303 s := newSource.(*fileSource) 304 assert.Equalf(t, tt.want, s.ID(), "ID() mismatch") 305 assert.Equalf(t, tt.wantDigest, s.digestForVersion, "digestForVersion mismatch") 306 }) 307 } 308 }