go.mway.dev/x@v0.0.0-20240520034138-950aede9a3fb/archive/extract/extract_test.go (about) 1 // Copyright (c) 2024 Matt Way 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to 5 // deal in the Software without restriction, including without limitation the 6 // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 // sell copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 // IN THE THE SOFTWARE. 20 21 package extract 22 23 import ( 24 "bytes" 25 "context" 26 "errors" 27 "io/fs" 28 "os" 29 "path/filepath" 30 "strings" 31 "testing" 32 33 "github.com/stretchr/testify/require" 34 ) 35 36 var _fullTree = map[string]string{ 37 "foo/bar/bat/testfile": "foo/bar/bat/", 38 "foo/bar/qux/testfile": "foo/bar/qux/", 39 "foo/bar/testfile": "foo/bar/", 40 "foo/baz/bat/testfile": "foo/baz/bat/", 41 "foo/baz/qux/testfile": "foo/baz/qux/", 42 "foo/baz/testfile": "foo/baz/", 43 "foo/testfile": "foo/", 44 } 45 46 func TestExtract(t *testing.T) { 47 cases := map[string]struct { //nolint:govet 48 giveArchives []string 49 giveOptions []Option 50 wantTree map[string]string 51 wantMissing []string 52 wantErr error 53 }{ 54 "gzip": { 55 giveArchives: []string{ 56 "testdata/foo.tar.gz", 57 "testdata/foo.tgz", 58 }, 59 giveOptions: nil, 60 wantTree: _fullTree, 61 wantMissing: nil, 62 wantErr: nil, 63 }, 64 "bzip2": { 65 giveArchives: []string{ 66 "testdata/foo.tar.bz", 67 "testdata/foo.tar.bz2", 68 "testdata/foo.tbz", 69 "testdata/foo.tbz2", 70 }, 71 giveOptions: nil, 72 wantTree: _fullTree, 73 wantMissing: nil, 74 wantErr: nil, 75 }, 76 "xz": { 77 giveArchives: []string{ 78 "testdata/foo.tar.xz", 79 }, 80 giveOptions: nil, 81 wantTree: _fullTree, 82 wantMissing: nil, 83 wantErr: nil, 84 }, 85 "zip": { 86 giveArchives: []string{ 87 "testdata/foo.zip", 88 }, 89 giveOptions: nil, 90 wantTree: _fullTree, 91 wantMissing: nil, 92 wantErr: nil, 93 }, 94 "include paths": { 95 giveArchives: []string{ 96 "testdata/foo.tar.bz", 97 "testdata/foo.tar.bz2", 98 "testdata/foo.tar.gz", 99 "testdata/foo.tar.xz", 100 "testdata/foo.tbz", 101 "testdata/foo.tbz2", 102 "testdata/foo.tgz", 103 "testdata/foo.zip", 104 }, 105 giveOptions: []Option{ 106 IncludePaths(map[string]string{ 107 "foo/*ar/bat/testfile": "overridden-bat-dst", 108 "foo/baz/*/testfile": "", 109 "foo/testfile": "", 110 }), 111 }, 112 wantTree: map[string]string{ 113 "overridden-bat-dst": _fullTree["foo/bar/bat/testfile"], 114 "foo/baz/bat/testfile": _fullTree["foo/baz/bat/testfile"], 115 "foo/baz/qux/testfile": _fullTree["foo/baz/qux/testfile"], 116 "foo/testfile": _fullTree["foo/testfile"], 117 }, 118 wantMissing: []string{ 119 "foo/bar/bat/testfile", 120 "foo/bar/qux/testfile", 121 "foo/bar/testfile", 122 "foo/baz/testfile", 123 }, 124 wantErr: nil, 125 }, 126 "exclude paths": { 127 giveArchives: []string{ 128 "testdata/foo.tar.bz", 129 "testdata/foo.tar.bz2", 130 "testdata/foo.tar.gz", 131 "testdata/foo.tar.xz", 132 "testdata/foo.tbz", 133 "testdata/foo.tbz2", 134 "testdata/foo.tgz", 135 "testdata/foo.zip", 136 }, 137 giveOptions: []Option{ 138 ExcludePaths([]string{ 139 "foo/bar/*", 140 "foo/bar/*/*", 141 "foo/baz/*", 142 }), 143 }, 144 wantTree: map[string]string{ 145 "foo/testfile": _fullTree["foo/testfile"], 146 "foo/baz/bat/testfile": _fullTree["foo/baz/bat/testfile"], 147 "foo/baz/qux/testfile": _fullTree["foo/baz/qux/testfile"], 148 }, 149 wantMissing: []string{ 150 "foo/bar/bat/testfile", 151 "foo/bar/qux/testfile", 152 "foo/bar/testfile", 153 "foo/baz/testfile", 154 }, 155 wantErr: nil, 156 }, 157 "strip prefix": { 158 giveArchives: []string{ 159 "testdata/foo.tar.bz", 160 "testdata/foo.tar.bz2", 161 "testdata/foo.tar.gz", 162 "testdata/foo.tar.xz", 163 "testdata/foo.tbz", 164 "testdata/foo.tbz2", 165 "testdata/foo.tgz", 166 "testdata/foo.zip", 167 }, 168 giveOptions: []Option{ 169 StripPrefix("foo"), 170 IncludePaths(map[string]string{ 171 "testfile": "", 172 }), 173 }, 174 wantTree: map[string]string{ 175 "testfile": _fullTree["foo/testfile"], 176 }, 177 wantMissing: []string{ 178 "foo/bar/bat/testfile", 179 "foo/bar/bat/testfile", 180 "foo/bar/qux/testfile", 181 "foo/bar/testfile", 182 "foo/baz/bat/testfile", 183 "foo/baz/qux/testfile", 184 "foo/baz/testfile", 185 }, 186 wantErr: nil, 187 }, 188 } 189 190 for name, tt := range cases { 191 t.Run(name, func(t *testing.T) { 192 for _, archive := range tt.giveArchives { 193 t.Run(filepath.Base(archive), func(t *testing.T) { 194 var ( 195 dst = t.TempDir() 196 opts = Options{}.With(tt.giveOptions...) 197 ) 198 199 err := Extract(context.Background(), dst, archive, opts) 200 require.ErrorIs(t, err, tt.wantErr) 201 202 for relpath, contents := range tt.wantTree { 203 path := filepath.Join(dst, relpath) 204 205 stat, statErr := os.Stat(path) 206 require.NoError(t, statErr) 207 require.False(t, stat.IsDir()) 208 209 raw, readErr := os.ReadFile(path) 210 require.NoError(t, readErr) 211 require.Equal(t, contents, string(bytes.TrimSpace(raw))) 212 } 213 214 for _, relpath := range tt.wantMissing { 215 _, statErr := os.Stat(filepath.Join(dst, relpath)) 216 require.ErrorIs(t, statErr, os.ErrNotExist, relpath) 217 } 218 }) 219 } 220 }) 221 } 222 } 223 224 func TestExtractOutput(t *testing.T) { 225 archives := []string{ 226 "testdata/foo.tar.bz", 227 "testdata/foo.tar.bz2", 228 "testdata/foo.tar.gz", 229 "testdata/foo.tar.xz", 230 "testdata/foo.tbz", 231 "testdata/foo.tbz2", 232 "testdata/foo.tgz", 233 "testdata/foo.zip", 234 } 235 236 for _, archive := range archives { 237 t.Run(filepath.Base(archive), func(t *testing.T) { 238 buf := bytes.NewBuffer(nil) 239 require.NoError( 240 t, 241 Extract( 242 context.Background(), 243 ExtractToTempDir, 244 archive, 245 Output(buf), 246 ), 247 ) 248 249 var ( 250 lines = strings.Split(strings.TrimSpace(buf.String()), "\n") 251 count int 252 ) 253 254 // Ignore ._* files. 255 for _, line := range lines { 256 if !strings.Contains(line, "/._") { 257 count++ 258 } 259 } 260 261 require.Equal(t, len(_fullTree), count) 262 }) 263 } 264 } 265 266 func TestExtractTempDirWithCallback(t *testing.T) { 267 archives := []string{ 268 "testdata/foo.tar.bz", 269 "testdata/foo.tar.bz2", 270 "testdata/foo.tar.gz", 271 "testdata/foo.tar.xz", 272 "testdata/foo.tbz", 273 "testdata/foo.tbz2", 274 "testdata/foo.tgz", 275 "testdata/foo.zip", 276 } 277 278 for _, archive := range archives { 279 t.Run(filepath.Base(archive), func(t *testing.T) { 280 var ( 281 wantErr = errors.New("done") 282 err = Extract( 283 context.Background(), 284 ExtractToTempDir, 285 archive, 286 Callback(func(_ context.Context, dir string) error { 287 err := filepath.WalkDir( 288 dir, 289 func(path string, _ fs.DirEntry, err error) error { 290 require.NoError(t, err) 291 292 contents, exists := _fullTree[path] 293 if !exists { 294 return nil 295 } 296 297 raw, err := os.ReadFile(path) 298 require.NoError(t, err) 299 require.Equal(t, contents, string(raw)) 300 return nil 301 }, 302 ) 303 require.NoError(t, err) 304 return wantErr 305 }), 306 ) 307 ) 308 require.ErrorIs(t, err, wantErr) 309 }) 310 } 311 } 312 313 func TestStripPrefix(t *testing.T) { 314 cases := map[string]struct { //nolint:govet 315 givePath string 316 givePrefix string 317 wantPath string 318 wantStripped bool 319 wantErr error 320 }{ 321 "matched exact": { 322 givePath: "foo/bar/baz/bat", 323 givePrefix: "foo/bar", 324 wantPath: "baz/bat", 325 wantStripped: true, 326 wantErr: nil, 327 }, 328 "matched exact with trailing slash": { 329 givePath: "foo/bar/baz/bat", 330 givePrefix: "foo/bar/", 331 wantPath: "baz/bat", 332 wantStripped: true, 333 wantErr: nil, 334 }, 335 "matched glob": { 336 givePath: "foo/bar/baz/bat", 337 givePrefix: "f*/*", 338 wantPath: "baz/bat", 339 wantStripped: true, 340 wantErr: nil, 341 }, 342 "not matched exact": { 343 givePath: "foo/bar", 344 givePrefix: "bar", 345 wantPath: "foo/bar", 346 wantStripped: false, 347 wantErr: nil, 348 }, 349 "not matched glob": { 350 givePath: "foo/bar", 351 givePrefix: "o*", 352 wantPath: "foo/bar", 353 wantStripped: false, 354 wantErr: nil, 355 }, 356 "empty prefix": { 357 givePath: "foo/bar", 358 givePrefix: "", 359 wantPath: "foo/bar", 360 wantStripped: true, 361 wantErr: nil, 362 }, 363 "empty strings": { 364 givePath: "", 365 givePrefix: "", 366 wantPath: "", 367 wantStripped: true, 368 wantErr: nil, 369 }, 370 } 371 372 for name, tt := range cases { 373 t.Run(name, func(t *testing.T) { 374 path, stripped, err := stripPrefix(tt.givePath, tt.givePrefix) 375 require.Equal(t, tt.wantPath, path) 376 require.Equal(t, tt.wantStripped, stripped) 377 require.ErrorIs(t, err, tt.wantErr) 378 }) 379 } 380 }