github.com/anchore/syft@v1.38.2/syft/internal/unionreader/union_reader_test.go (about) 1 package unionreader 2 3 import ( 4 "bytes" 5 "io" 6 "strings" 7 "sync" 8 "testing" 9 10 "github.com/stretchr/testify/assert" 11 "github.com/stretchr/testify/require" 12 13 "github.com/anchore/syft/syft/file" 14 ) 15 16 func Test_getUnionReader_notUnionReader(t *testing.T) { 17 expectedContents := "this is a test" 18 reader := io.NopCloser(strings.NewReader(expectedContents)) 19 20 // make certain that the test fixture does not implement the union reader 21 _, ok := reader.(UnionReader) 22 require.False(t, ok) 23 24 actual, err := GetUnionReader(reader) 25 require.NoError(t, err) 26 27 _, ok = actual.(UnionReader) 28 require.True(t, ok) 29 30 b, err := io.ReadAll(actual) 31 require.NoError(t, err) 32 33 assert.Equal(t, expectedContents, string(b)) 34 } 35 36 type panickingUnionReader struct{} 37 38 func (p2 *panickingUnionReader) ReadAt(p []byte, off int64) (n int, err error) { 39 panic("don't call this in your unit test!") 40 } 41 42 func (p2 *panickingUnionReader) Seek(offset int64, whence int) (int64, error) { 43 panic("don't call this in your unit test!") 44 } 45 46 func (p2 *panickingUnionReader) Read(p []byte) (n int, err error) { 47 panic("don't call this in your unit test!") 48 } 49 50 func (p2 *panickingUnionReader) Close() error { 51 panic("don't call this in your unit test!") 52 } 53 54 var _ UnionReader = (*panickingUnionReader)(nil) 55 56 func Test_getUnionReader_fileLocationReadCloser(t *testing.T) { 57 // panickingUnionReader is a UnionReader 58 p := &panickingUnionReader{} 59 embedsUnionReader := file.NewLocationReadCloser(file.Location{}, p) 60 61 // embedded union reader is returned without "ReadAll" invocation 62 ur, err := GetUnionReader(embedsUnionReader) 63 require.NoError(t, err) 64 require.Equal(t, p, ur) 65 } 66 67 func TestReaderAtAdapter_ReadAt(t *testing.T) { 68 testData := "Hello, World! This is a test string for ReadAt." 69 70 t.Run("basic functionality", func(t *testing.T) { 71 reader := newReadSeekCloser(strings.NewReader(testData)) 72 adapter := newReaderAtAdapter(reader) 73 74 tests := []struct { 75 name string 76 offset int64 77 length int 78 expected string 79 }{ 80 {name: "read from beginning", offset: 0, length: 5, expected: "Hello"}, 81 {name: "read from middle", offset: 7, length: 5, expected: "World"}, 82 {name: "read from end", offset: int64(len(testData) - 4), length: 4, expected: "dAt."}, 83 {name: "read single character", offset: 12, length: 1, expected: "!"}, 84 } 85 86 for _, tt := range tests { 87 t.Run(tt.name, func(t *testing.T) { 88 buf := make([]byte, tt.length) 89 n, err := adapter.ReadAt(buf, tt.offset) 90 91 if err != nil && err != io.EOF { 92 t.Fatalf("Unexpected error: %v", err) 93 } 94 95 result := string(buf[:n]) 96 if result != tt.expected { 97 t.Errorf("Expected %q, got %q", tt.expected, result) 98 } 99 }) 100 } 101 }) 102 103 t.Run("edge cases", func(t *testing.T) { 104 tests := []struct { 105 name string 106 data string 107 offset int64 108 bufSize int 109 expectedN int 110 expectedErr error 111 expectedStr string 112 }{ 113 { 114 name: "beyond EOF", 115 data: "Hello", 116 offset: 10, 117 bufSize: 5, 118 expectedN: 0, 119 expectedErr: io.EOF, 120 expectedStr: "", 121 }, 122 { 123 name: "partial read", 124 data: "Hello", 125 offset: 2, 126 bufSize: 10, 127 expectedN: 3, 128 expectedErr: nil, 129 expectedStr: "llo", 130 }, 131 { 132 name: "empty buffer", 133 data: "Hello", 134 offset: 0, 135 bufSize: 0, 136 expectedN: 0, 137 expectedErr: nil, 138 expectedStr: "", 139 }, 140 } 141 142 for _, tt := range tests { 143 t.Run(tt.name, func(t *testing.T) { 144 reader := newReadSeekCloser(strings.NewReader(tt.data)) 145 adapter := newReaderAtAdapter(reader) 146 147 buf := make([]byte, tt.bufSize) 148 n, err := adapter.ReadAt(buf, tt.offset) 149 150 if err != tt.expectedErr { 151 t.Errorf("Expected error %v, got %v", tt.expectedErr, err) 152 } 153 154 if n != tt.expectedN { 155 t.Errorf("Expected %d bytes read, got %d", tt.expectedN, n) 156 } 157 158 result := string(buf[:n]) 159 if result != tt.expectedStr { 160 t.Errorf("Expected %q, got %q", tt.expectedStr, result) 161 } 162 }) 163 } 164 }) 165 166 t.Run("multiple reads from same position", func(t *testing.T) { 167 reader := newReadSeekCloser(strings.NewReader(testData)) 168 adapter := newReaderAtAdapter(reader) 169 170 // read the same data multiple times 171 for i := 0; i < 3; i++ { 172 buf := make([]byte, 5) 173 n, err := adapter.ReadAt(buf, 7) 174 175 if err != nil && err != io.EOF { 176 t.Fatalf("ReadAt %d failed: %v", i, err) 177 } 178 179 result := string(buf[:n]) 180 if result != "World" { 181 t.Errorf("ReadAt %d: expected 'World', got %q", i, result) 182 } 183 } 184 }) 185 186 t.Run("concurrent access", func(t *testing.T) { 187 td := "0123456789abcdefghijklmnopqrstuvwxyz" 188 reader := newReadSeekCloser(strings.NewReader(td)) 189 adapter := newReaderAtAdapter(reader) 190 191 const numGoroutines = 10 192 const numReads = 100 193 194 var wg sync.WaitGroup 195 results := make(chan bool, numGoroutines*numReads) 196 197 for i := 0; i < numGoroutines; i++ { 198 wg.Add(1) 199 go func(goroutineID int) { 200 defer wg.Done() 201 202 for j := 0; j < numReads; j++ { 203 offset := int64(goroutineID % len(td)) 204 buf := make([]byte, 1) 205 206 n, err := adapter.ReadAt(buf, offset) 207 if err != nil && err != io.EOF { 208 results <- false 209 return 210 } 211 212 if n > 0 { 213 expected := td[offset] 214 if buf[0] != expected { 215 results <- false 216 return 217 } 218 } 219 results <- true 220 } 221 }(i) 222 } 223 224 wg.Wait() 225 close(results) 226 227 successCount := 0 228 totalCount := 0 229 for success := range results { 230 totalCount++ 231 if success { 232 successCount++ 233 } 234 } 235 236 if successCount != totalCount { 237 t.Errorf("Concurrent reads failed: %d/%d successful", successCount, totalCount) 238 } 239 }) 240 } 241 242 func TestReaderAtAdapter_PositionHandling(t *testing.T) { 243 testData := "Hello, World!" 244 245 t.Run("preserves position after ReadAt", func(t *testing.T) { 246 reader := newReadSeekCloser(strings.NewReader(testData)) 247 adapter := newReaderAtAdapter(reader) 248 249 // move to a specific position 250 initialPos := int64(7) 251 _, err := adapter.Seek(initialPos, io.SeekStart) 252 if err != nil { 253 t.Fatalf("Failed to seek: %v", err) 254 } 255 256 // read using ReadAt 257 buf := make([]byte, 5) 258 _, err = adapter.ReadAt(buf, 0) 259 if err != nil && err != io.EOF { 260 t.Fatalf("ReadAt failed: %v", err) 261 } 262 263 // verify position is preserved 264 currentPos, err := adapter.Seek(0, io.SeekCurrent) 265 if err != nil { 266 t.Fatalf("Failed to get current position: %v", err) 267 } 268 269 if currentPos != initialPos { 270 t.Errorf("Position not preserved. Expected %d, got %d", initialPos, currentPos) 271 } 272 }) 273 274 t.Run("does not affect regular reads", func(t *testing.T) { 275 reader := newReadSeekCloser(strings.NewReader(testData)) 276 adapter := newReaderAtAdapter(reader) 277 278 // read first few bytes normally 279 normalBuf := make([]byte, 5) 280 n, err := adapter.Read(normalBuf) 281 if err != nil { 282 t.Fatalf("Normal read failed: %v", err) 283 } 284 if string(normalBuf[:n]) != "Hello" { 285 t.Errorf("Expected 'Hello', got %q", string(normalBuf[:n])) 286 } 287 288 // use ReadAt to read from a different position 289 readAtBuf := make([]byte, 5) 290 n, err = adapter.ReadAt(readAtBuf, 7) 291 if err != nil && err != io.EOF { 292 t.Fatalf("ReadAt failed: %v", err) 293 } 294 if string(readAtBuf[:n]) != "World" { 295 t.Errorf("Expected 'World', got %q", string(readAtBuf[:n])) 296 } 297 298 // continue normal reading - should pick up where we left off 299 continueBuf := make([]byte, 2) 300 n, err = adapter.Read(continueBuf) 301 if err != nil { 302 t.Fatalf("Continue read failed: %v", err) 303 } 304 if string(continueBuf[:n]) != ", " { 305 t.Errorf("Expected ', ', got %q", string(continueBuf[:n])) 306 } 307 }) 308 } 309 310 func TestReaderAtAdapter_Close(t *testing.T) { 311 reader := newReadSeekCloser(bytes.NewReader([]byte("test data"))) 312 adapter := newReaderAtAdapter(reader) 313 314 // test that adapter can be closed 315 err := adapter.Close() 316 if err != nil { 317 t.Errorf("Close failed: %v", err) 318 } 319 320 if !reader.closed { 321 t.Error("Underlying reader was not closed") 322 } 323 } 324 325 type readSeekCloser struct { 326 io.ReadSeeker 327 closed bool 328 } 329 330 func newReadSeekCloser(rs io.ReadSeeker) *readSeekCloser { 331 return &readSeekCloser{ReadSeeker: rs} 332 } 333 334 func (r *readSeekCloser) Close() error { 335 r.closed = true 336 return nil 337 }