// Copyright 2022 syzkaller project authors. All rights reserved. // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. package asset import ( "bytes" "compress/gzip" "errors" "fmt" "io" "reflect" "strings" "testing" "time" "github.com/google/syzkaller/dashboard/dashapi" "github.com/google/syzkaller/pkg/debugtracer" "github.com/stretchr/testify/assert" "github.com/ulikunitz/xz" ) type addBuildAssetCallback func(obj dashapi.NewAsset) error type dashMock struct { downloadURLs map[string]bool addBuildAsset addBuildAssetCallback } func newDashMock() *dashMock { return &dashMock{downloadURLs: map[string]bool{}} } func (dm *dashMock) AddBuildAssets(req *dashapi.AddBuildAssetsReq) error { for _, obj := range req.Assets { if dm.addBuildAsset != nil { if err := dm.addBuildAsset(obj); err != nil { return err } } dm.downloadURLs[obj.DownloadURL] = true } return nil } func (dm *dashMock) NeededAssetsList() (*dashapi.NeededAssetsResp, error) { resp := &dashapi.NeededAssetsResp{} for url := range dm.downloadURLs { resp.DownloadURLs = append(resp.DownloadURLs, url) } return resp, nil } func makeStorage(t *testing.T, dash Dashboard) (*Storage, *dummyStorageBackend) { be := makeDummyStorageBackend() cfg := &Config{ UploadTo: "dummy://test", } return &Storage{ dash: dash, cfg: cfg, backend: be, tracer: &debugtracer.TestTracer{T: t}, }, be } func validateGzip(res *uploadedFile, expected []byte) error { if res == nil { return fmt.Errorf("no file was uploaded") } reader, err := gzip.NewReader(bytes.NewReader(res.bytes)) if err != nil { return fmt.Errorf("gzip.NewReader failed: %w", err) } defer reader.Close() body, err := io.ReadAll(reader) if err != nil { return fmt.Errorf("read of ungzipped content failed: %w", err) } if !reflect.DeepEqual(body, expected) { return fmt.Errorf("decompressed: %#v, expected: %#v", body, expected) } return nil } func validateXz(res *uploadedFile, expected []byte) error { if res == nil { return fmt.Errorf("no file was uploaded") } xzUsed := strings.HasSuffix(res.req.savePath, ".xz") if !xzUsed { return fmt.Errorf("xz expected to be used") } xzReader, err := xz.NewReader(bytes.NewReader(res.bytes)) if err != nil { return fmt.Errorf("xz reader failed: %w", err) } out, err := io.ReadAll(xzReader) if err != nil { return fmt.Errorf("xz decompression failed: %w", err) } if !reflect.DeepEqual(out, expected) { return fmt.Errorf("decompressed: %#v, expected: %#v", out, expected) } return nil } func (storage *Storage) sendBuildAsset(reader io.Reader, fileName string, assetType dashapi.AssetType, build *dashapi.Build) error { asset, err := storage.UploadBuildAsset(reader, fileName, assetType, build, nil) if err != nil { return err } return storage.ReportBuildAssets(build, asset) } func TestUploadBuildAsset(t *testing.T) { dashMock := newDashMock() storage, be := makeStorage(t, dashMock) be.currentTime = time.Now().Add(-2 * deletionEmbargo) build := &dashapi.Build{ID: "1234", KernelCommit: "abcdef2134"} // Upload two assets using different means. vmLinuxContent := []byte{0xDE, 0xAD, 0xBE, 0xEF} dashMock.addBuildAsset = func(newAsset dashapi.NewAsset) error { if newAsset.Type != dashapi.KernelObject { t.Fatalf("expected KernelObject, got %v", newAsset.Type) } if !strings.Contains(newAsset.DownloadURL, "vmlinux") { t.Fatalf("%#v was expected to mention vmlinux", newAsset.DownloadURL) } return nil } var file *uploadedFile be.objectUpload = collectBytes(&file) err := storage.sendBuildAsset(bytes.NewReader(vmLinuxContent), "vmlinux", dashapi.KernelObject, build) if err != nil { t.Fatalf("file upload failed: %s", err) } if err := validateXz(file, vmLinuxContent); err != nil { t.Fatalf("vmlinux validation failed: %s", err) } // Upload the same file the second time. storage.sendBuildAsset(bytes.NewReader(vmLinuxContent), "vmlinux", dashapi.KernelObject, build) // The currently expected behavior is that it will be uploaded twice and will have // different names. if len(dashMock.downloadURLs) < 2 { t.Fatalf("same-file upload was expected to succeed, but it didn't; %#v", dashMock.downloadURLs) } diskImageContent := []byte{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8} dashMock.addBuildAsset = func(newAsset dashapi.NewAsset) error { if newAsset.Type != dashapi.KernelImage { t.Fatalf("expected KernelImage, got %v", newAsset.Type) } if !strings.Contains(newAsset.DownloadURL, "disk") || !strings.Contains(newAsset.DownloadURL, ".img") { t.Fatalf("%#v was expected to mention disk.img", newAsset.DownloadURL) } if !strings.Contains(newAsset.DownloadURL, build.KernelCommit[:6]) { t.Fatalf("%#v was expected to mention build commit", newAsset.DownloadURL) } return nil } file = nil be.objectUpload = collectBytes(&file) storage.sendBuildAsset(bytes.NewReader(diskImageContent), "disk.img", dashapi.KernelImage, build) if err := validateXz(file, diskImageContent); err != nil { t.Fatalf("disk.img validation failed: %s", err) } allUrls := []string{} for url := range dashMock.downloadURLs { allUrls = append(allUrls, url) } if len(allUrls) != 3 { t.Fatalf("invalid dashMock state: expected 3 assets, got %d", len(allUrls)) } // First try to remove two assets. dashMock.downloadURLs = map[string]bool{allUrls[2]: true, "http://download/unrelated.txt": true} // Pretend there's an asset deletion error. be.objectRemove = func(string) error { return fmt.Errorf("not now") } _, err = storage.DeprecateAssets() if err == nil { t.Fatalf("DeprecateAssets should have failed") } // Let the deletion be successful. be.objectRemove = nil _, err = storage.DeprecateAssets() if err != nil { t.Fatalf("DeprecateAssets was expected to be successful, got %s", err) } path, err := be.getPath(allUrls[2]) if err != nil { t.Fatalf("getPath failed: %s", err) } err = be.hasOnly([]string{path}) if err != nil { t.Fatalf("after first DeprecateAssets: %s", err) } // Delete the rest. dashMock.downloadURLs = map[string]bool{} _, err = storage.DeprecateAssets() if err != nil || len(be.objects) != 0 { t.Fatalf("second DeprecateAssets failed: %s, len %d", err, len(be.objects)) } } type uploadedFile struct { req uploadRequest bytes []byte } func collectBytes(saveTo **uploadedFile) objectUploadCallback { return func(req *uploadRequest) (*uploadResponse, error) { buf := &bytes.Buffer{} wwc := &wrappedWriteCloser{ writer: buf, closeCallback: func() error { *saveTo = &uploadedFile{req: *req, bytes: buf.Bytes()} return nil }, } return &uploadResponse{path: req.savePath, writer: wwc}, nil } } func TestUploadHtmlAsset(t *testing.T) { dashMock := newDashMock() storage, be := makeStorage(t, dashMock) build := &dashapi.Build{ID: "1234", KernelCommit: "abcdef2134"} htmlContent := []byte("