diff --git a/Makefile b/Makefile index 0b87688a0..99c8a9d7f 100644 --- a/Makefile +++ b/Makefile @@ -56,6 +56,9 @@ dev-deps: test-with-coverage: install go test -count=1 -coverprofile=tmp.cov --coverpkg $(COVERAGE_PACKAGES) -timeout 90s github.com/kopia/kopia/... +test-with-coverage-pkgonly: install + go test -count=1 -coverprofile=tmp.cov -timeout 90s github.com/kopia/kopia/... + test: install go test -count=1 -timeout 90s github.com/kopia/kopia/... diff --git a/internal/repotesting/repotesting.go b/internal/repotesting/repotesting.go new file mode 100644 index 000000000..9f10e7f8d --- /dev/null +++ b/internal/repotesting/repotesting.go @@ -0,0 +1,123 @@ +// Package repotesting contains test utilities for working with repositories. +package repotesting + +import ( + "context" + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/kopia/kopia/repo" + "github.com/kopia/kopia/repo/storage" + "github.com/kopia/kopia/repo/storage/filesystem" +) + +const masterPassword = "foobarbazfoobarbaz" + +// Environment encapsulates details of a test environment. +type Environment struct { + Repository *repo.Repository + + configDir string + storageDir string +} + +// Setup sets up a test environment. +func (e *Environment) Setup(t *testing.T, opts ...func(*repo.NewRepositoryOptions)) *Environment { + var err error + + ctx := context.Background() + + e.configDir, err = ioutil.TempDir("", "") + if err != nil { + t.Fatalf("err: %v", err) + } + e.storageDir, err = ioutil.TempDir("", "") + if err != nil { + t.Fatalf("err: %v", err) + } + + opt := &repo.NewRepositoryOptions{ + MaxBlockSize: 400, + Splitter: "FIXED", + BlockFormat: "UNENCRYPTED_HMAC_SHA256", + MetadataEncryptionAlgorithm: "NONE", + ObjectHMACSecret: []byte{}, + } + + for _, mod := range opts { + mod(opt) + } + + st, err := filesystem.New(ctx, &filesystem.Options{ + Path: e.storageDir, + }) + if err != nil { + t.Fatalf("err: %v", err) + } + + if err = repo.Initialize(ctx, st, opt, masterPassword); err != nil { + t.Fatalf("err: %v", err) + } + + connOpts := repo.ConnectOptions{ + //TraceStorage: log.Printf, + } + + if err = repo.Connect(ctx, e.configFile(), st, masterPassword, connOpts); err != nil { + t.Fatalf("can't connect: %v", err) + } + + e.Repository, err = repo.Open(ctx, e.configFile(), masterPassword, &repo.Options{}) + if err != nil { + t.Fatalf("can't open: %v", err) + } + + return e +} + +// Close closes testing environment +func (e *Environment) Close(t *testing.T) { + if err := e.Repository.Close(context.Background()); err != nil { + t.Fatalf("unable to close: %v", err) + } + + if err := os.RemoveAll(e.configDir); err != nil { + t.Errorf("error removing config directory: %v", err) + } + if err := os.RemoveAll(e.storageDir); err != nil { + t.Errorf("error removing storage directory: %v", err) + } +} + +func (e *Environment) configFile() string { + return filepath.Join(e.configDir, "kopia.config") +} + +// MustReopen closes and reopens the repository. +func (e *Environment) MustReopen(t *testing.T) { + err := e.Repository.Close(context.Background()) + if err != nil { + t.Fatalf("close error: %v", err) + } + + e.Repository, err = repo.Open(context.Background(), e.configFile(), masterPassword, &repo.Options{}) + if err != nil { + t.Fatalf("err: %v", err) + } +} + +// VerifyStorageBlockCount verifies that the underlying storage contains the specified number of blocks. +func (e *Environment) VerifyStorageBlockCount(t *testing.T, want int) { + var got int + + _ = e.Repository.Storage.ListBlocks(context.Background(), "", func(_ storage.BlockMetadata) error { + got++ + return nil + }) + + if got != want { + t.Errorf("got unexpected number of storage blocks: %v, wanted %v", got, want) + } +} diff --git a/repo/initialize.go b/repo/initialize.go index f0bb5204e..93b9ef0c7 100644 --- a/repo/initialize.go +++ b/repo/initialize.go @@ -33,6 +33,7 @@ type NewRepositoryOptions struct { BlockFormat string // identifier of object format ObjectHMACSecret []byte // force the use of particular object HMAC secret + DisableHMAC bool ObjectEncryptionKey []byte // force the use of particular object encryption key Splitter string // splitter used to break objects into storage blocks @@ -40,8 +41,6 @@ type NewRepositoryOptions struct { AvgBlockSize int // approximate size of storage block (used with dynamic splitter) MaxBlockSize int // maximum size of storage block - // test-only - noHMAC bool // disable HMAC } // Initialize creates initial repository data structures in the specified storage with given credentials. @@ -102,7 +101,7 @@ func repositoryObjectFormatFromOptions(opt *NewRepositoryOptions) *config.Reposi AvgBlockSize: applyDefaultInt(opt.AvgBlockSize, 16<<20), // 16MiB } - if opt.noHMAC { + if opt.DisableHMAC { f.HMACSecret = nil } diff --git a/repo/tests/repository_test/repository.go b/repo/tests/repository_test/repository.go new file mode 100644 index 000000000..8fb601007 --- /dev/null +++ b/repo/tests/repository_test/repository.go @@ -0,0 +1,3 @@ +package repository + +// dummy package diff --git a/repo/repository_test.go b/repo/tests/repository_test/repository_test.go similarity index 51% rename from repo/repository_test.go rename to repo/tests/repository_test/repository_test.go index 7ad7e1c71..2406ec58f 100644 --- a/repo/repository_test.go +++ b/repo/tests/repository_test/repository_test.go @@ -1,60 +1,23 @@ -package repo +package repository_test import ( "bytes" - "compress/gzip" "context" cryptorand "crypto/rand" - "encoding/json" "fmt" "io/ioutil" "math/rand" "reflect" "runtime/debug" "testing" - "time" - "github.com/kopia/kopia/internal/config" + "github.com/kopia/kopia/internal/repotesting" + "github.com/kopia/kopia/repo" "github.com/kopia/kopia/repo/block" - "github.com/kopia/kopia/repo/internal/storagetesting" "github.com/kopia/kopia/repo/object" "github.com/kopia/kopia/repo/storage" ) -const masterPassword = "foobarbazfoobarbaz" - -func setupTest(t *testing.T, mods ...func(o *NewRepositoryOptions)) (map[string][]byte, map[string]time.Time, *Repository) { - return setupTestWithData(t, map[string][]byte{}, map[string]time.Time{}, mods...) -} - -func setupTestWithData(t *testing.T, data map[string][]byte, keyTime map[string]time.Time, mods ...func(o *NewRepositoryOptions)) (map[string][]byte, map[string]time.Time, *Repository) { - st := storagetesting.NewMapStorage(data, keyTime, nil) - - opt := &NewRepositoryOptions{ - MaxBlockSize: 400, - Splitter: "FIXED", - BlockFormat: "UNENCRYPTED_HMAC_SHA256", - MetadataEncryptionAlgorithm: "NONE", - noHMAC: true, - } - - for _, m := range mods { - m(opt) - } - - ctx := context.Background() - Initialize(ctx, st, opt, masterPassword) - - r, err := connect(ctx, st, &config.LocalConfig{}, masterPassword, &Options{ - //TraceStorage: log.Printf, - }, block.CachingOptions{}) - if err != nil { - t.Fatalf("can't connect: %v", err) - } - - return data, keyTime, r -} - func TestWriters(t *testing.T) { cases := []struct { data []byte @@ -70,10 +33,10 @@ func TestWriters(t *testing.T) { ctx := context.Background() for _, c := range cases { - data, _, repo := setupTest(t) - - writer := repo.Objects.NewWriter(ctx, object.WriterOptions{}) + var env repotesting.Environment + defer env.Setup(t).Close(t) + writer := env.Repository.Objects.NewWriter(ctx, object.WriterOptions{}) writer.Write(c.data) result, err := writer.Result() @@ -82,48 +45,27 @@ func TestWriters(t *testing.T) { continue } - repo.Objects.Flush(ctx) + env.Repository.Objects.Flush(ctx) if !objectIDsEqual(result, c.objectID) { t.Errorf("incorrect result for %v, expected: %v got: %v", c.data, c.objectID.String(), result.String()) } - repo.Blocks.Flush(ctx) - - if got, want := len(data), 3; got != want { - // 1 format block + 1 data block + 1 pack index block - t.Errorf("unexpected data written to the storage (%v), wanted %v", got, want) - dumpBlockManagerData(data) - } + env.Repository.Blocks.Flush(ctx) } } -func dumpBlockManagerData(data map[string][]byte) { - for k, v := range data { - if k[0] == 'P' { - gz, _ := gzip.NewReader(bytes.NewReader(v)) - var buf bytes.Buffer - buf.ReadFrom(gz) - - var dst bytes.Buffer - json.Indent(&dst, buf.Bytes(), "", " ") - - log.Debugf("data[%v] = %v", k, dst.String()) - } else { - log.Debugf("data[%v] = %x", k, v) - } - } -} func objectIDsEqual(o1 object.ID, o2 object.ID) bool { return reflect.DeepEqual(o1, o2) } func TestWriterCompleteChunkInTwoWrites(t *testing.T) { + var env repotesting.Environment + defer env.Setup(t).Close(t) ctx := context.Background() - _, _, repo := setupTest(t) bytes := make([]byte, 100) - writer := repo.Objects.NewWriter(ctx, object.WriterOptions{}) + writer := env.Repository.Objects.NewWriter(ctx, object.WriterOptions{}) writer.Write(bytes[0:50]) writer.Write(bytes[0:50]) result, err := writer.Result() @@ -133,28 +75,29 @@ func TestWriterCompleteChunkInTwoWrites(t *testing.T) { } func TestPackingSimple(t *testing.T) { - data, keyTime, repo := setupTest(t, func(n *NewRepositoryOptions) { - }) + var env repotesting.Environment + defer env.Setup(t).Close(t) + ctx := context.Background() content1 := "hello, how do you do?" content2 := "hi, how are you?" content3 := "thank you!" - oid1a := writeObject(ctx, t, repo, []byte(content1), "packed-object-1a") - oid1b := writeObject(ctx, t, repo, []byte(content1), "packed-object-1b") - oid2a := writeObject(ctx, t, repo, []byte(content2), "packed-object-2a") - oid2b := writeObject(ctx, t, repo, []byte(content2), "packed-object-2b") + oid1a := writeObject(ctx, t, env.Repository, []byte(content1), "packed-object-1a") + oid1b := writeObject(ctx, t, env.Repository, []byte(content1), "packed-object-1b") + oid2a := writeObject(ctx, t, env.Repository, []byte(content2), "packed-object-2a") + oid2b := writeObject(ctx, t, env.Repository, []byte(content2), "packed-object-2b") - oid3a := writeObject(ctx, t, repo, []byte(content3), "packed-object-3a") - oid3b := writeObject(ctx, t, repo, []byte(content3), "packed-object-3b") - verify(ctx, t, repo, oid1a, []byte(content1), "packed-object-1") - verify(ctx, t, repo, oid2a, []byte(content2), "packed-object-2") - oid2c := writeObject(ctx, t, repo, []byte(content2), "packed-object-2c") - oid1c := writeObject(ctx, t, repo, []byte(content1), "packed-object-1c") + oid3a := writeObject(ctx, t, env.Repository, []byte(content3), "packed-object-3a") + oid3b := writeObject(ctx, t, env.Repository, []byte(content3), "packed-object-3b") + verify(ctx, t, env.Repository, oid1a, []byte(content1), "packed-object-1") + verify(ctx, t, env.Repository, oid2a, []byte(content2), "packed-object-2") + oid2c := writeObject(ctx, t, env.Repository, []byte(content2), "packed-object-2c") + oid1c := writeObject(ctx, t, env.Repository, []byte(content1), "packed-object-1c") - repo.Objects.Flush(ctx) - repo.Blocks.Flush(ctx) + env.Repository.Objects.Flush(ctx) + env.Repository.Blocks.Flush(ctx) if got, want := oid1a.String(), oid1b.String(); got != want { t.Errorf("oid1a(%q) != oid1b(%q)", got, want) @@ -172,94 +115,68 @@ func TestPackingSimple(t *testing.T) { t.Errorf("oid3a(%q) != oid3b(%q)", got, want) } - // format + index + data - if got, want := len(data), 3; got != want { - t.Errorf("got unexpected repository contents %v items, wanted %v", got, want) - } - repo.Close(ctx) + env.VerifyStorageBlockCount(t, 3) - data, _, repo = setupTestWithData(t, data, keyTime, func(n *NewRepositoryOptions) { - }) + env.MustReopen(t) - verify(ctx, t, repo, oid1a, []byte(content1), "packed-object-1") - verify(ctx, t, repo, oid2a, []byte(content2), "packed-object-2") - verify(ctx, t, repo, oid3a, []byte(content3), "packed-object-3") + verify(ctx, t, env.Repository, oid1a, []byte(content1), "packed-object-1") + verify(ctx, t, env.Repository, oid2a, []byte(content2), "packed-object-2") + verify(ctx, t, env.Repository, oid3a, []byte(content3), "packed-object-3") - if err := repo.Blocks.CompactIndexes(ctx, block.CompactOptions{MinSmallBlocks: 1, MaxSmallBlocks: 1}); err != nil { + if err := env.Repository.Blocks.CompactIndexes(ctx, block.CompactOptions{MinSmallBlocks: 1, MaxSmallBlocks: 1}); err != nil { t.Errorf("optimize error: %v", err) } - data, _, repo = setupTestWithData(t, data, keyTime, func(n *NewRepositoryOptions) { - }) - verify(ctx, t, repo, oid1a, []byte(content1), "packed-object-1") - verify(ctx, t, repo, oid2a, []byte(content2), "packed-object-2") - verify(ctx, t, repo, oid3a, []byte(content3), "packed-object-3") + env.MustReopen(t) - if err := repo.Blocks.CompactIndexes(ctx, block.CompactOptions{MinSmallBlocks: 1, MaxSmallBlocks: 1}); err != nil { + verify(ctx, t, env.Repository, oid1a, []byte(content1), "packed-object-1") + verify(ctx, t, env.Repository, oid2a, []byte(content2), "packed-object-2") + verify(ctx, t, env.Repository, oid3a, []byte(content3), "packed-object-3") + + if err := env.Repository.Blocks.CompactIndexes(ctx, block.CompactOptions{MinSmallBlocks: 1, MaxSmallBlocks: 1}); err != nil { t.Errorf("optimize error: %v", err) } - _, _, repo = setupTestWithData(t, data, keyTime, func(n *NewRepositoryOptions) { - }) - verify(ctx, t, repo, oid1a, []byte(content1), "packed-object-1") - verify(ctx, t, repo, oid2a, []byte(content2), "packed-object-2") - verify(ctx, t, repo, oid3a, []byte(content3), "packed-object-3") + env.MustReopen(t) + + verify(ctx, t, env.Repository, oid1a, []byte(content1), "packed-object-1") + verify(ctx, t, env.Repository, oid2a, []byte(content2), "packed-object-2") + verify(ctx, t, env.Repository, oid3a, []byte(content3), "packed-object-3") } func TestHMAC(t *testing.T) { - content := bytes.Repeat([]byte{0xcd}, 50) - - _, _, repo := setupTest(t) - + var env repotesting.Environment + defer env.Setup(t).Close(t) ctx := context.Background() - w := repo.Objects.NewWriter(ctx, object.WriterOptions{}) + content := bytes.Repeat([]byte{0xcd}, 50) + + w := env.Repository.Objects.NewWriter(ctx, object.WriterOptions{}) w.Write(content) result, err := w.Result() if result.String() != "367352007ee6ca9fa755ce8352347d092c17a24077fd33c62f655574a8cf906d" { t.Errorf("unexpected result: %v err: %v", result.String(), err) } } -func TestMalformedStoredData(t *testing.T) { - data, _, repo := setupTest(t) - ctx := context.Background() - - cases := [][]byte{ - []byte("foo\nba"), - []byte("foo\nbar1"), - } - - for _, c := range cases { - data["a76999788386641a3ec798554f1fe7e6"] = c - objectID, err := object.ParseID("Da76999788386641a3ec798554f1fe7e6") - if err != nil { - t.Errorf("cannot parse object ID: %v", err) - continue - } - - reader, err := repo.Objects.Open(ctx, objectID) - if err == nil || reader != nil { - t.Errorf("expected error for %x", c) - } - } -} func TestReaderStoredBlockNotFound(t *testing.T) { - _, _, repo := setupTest(t) + var env repotesting.Environment + defer env.Setup(t).Close(t) ctx := context.Background() objectID, err := object.ParseID("Ddeadbeef") if err != nil { t.Errorf("cannot parse object ID: %v", err) } - reader, err := repo.Objects.Open(ctx, objectID) + reader, err := env.Repository.Objects.Open(ctx, objectID) if err != storage.ErrBlockNotFound || reader != nil { t.Errorf("unexpected result: reader: %v err: %v", reader, err) } } func TestEndToEndReadAndSeek(t *testing.T) { - _, _, repo := setupTest(t) + var env repotesting.Environment + defer env.Setup(t).Close(t) ctx := context.Background() for _, size := range []int{1, 199, 200, 201, 9999, 512434} { @@ -267,7 +184,7 @@ func TestEndToEndReadAndSeek(t *testing.T) { randomData := make([]byte, size) cryptorand.Read(randomData) - writer := repo.Objects.NewWriter(ctx, object.WriterOptions{}) + writer := env.Repository.Objects.NewWriter(ctx, object.WriterOptions{}) writer.Write(randomData) objectID, err := writer.Result() writer.Close() @@ -276,12 +193,12 @@ func TestEndToEndReadAndSeek(t *testing.T) { continue } - verify(ctx, t, repo, objectID, randomData, fmt.Sprintf("%v %v", objectID, size)) + verify(ctx, t, env.Repository, objectID, randomData, fmt.Sprintf("%v %v", objectID, size)) } } -func writeObject(ctx context.Context, t *testing.T, repo *Repository, data []byte, testCaseID string) object.ID { - w := repo.Objects.NewWriter(ctx, object.WriterOptions{}) +func writeObject(ctx context.Context, t *testing.T, rep *repo.Repository, data []byte, testCaseID string) object.ID { + w := rep.Objects.NewWriter(ctx, object.WriterOptions{}) if _, err := w.Write(data); err != nil { t.Fatalf("can't write object %q - write failed: %v", testCaseID, err) @@ -294,9 +211,9 @@ func writeObject(ctx context.Context, t *testing.T, repo *Repository, data []byt return oid } -func verify(ctx context.Context, t *testing.T, repo *Repository, objectID object.ID, expectedData []byte, testCaseID string) { +func verify(ctx context.Context, t *testing.T, rep *repo.Repository, objectID object.ID, expectedData []byte, testCaseID string) { t.Helper() - reader, err := repo.Objects.Open(ctx, objectID) + reader, err := rep.Objects.Open(ctx, objectID) if err != nil { t.Errorf("cannot get reader for %v (%v): %v %v", testCaseID, objectID, err, string(debug.Stack())) return @@ -328,24 +245,22 @@ func verify(ctx context.Context, t *testing.T, repo *Repository, objectID object func TestFormats(t *testing.T) { ctx := context.Background() - makeFormat := func(objectFormat string) func(*NewRepositoryOptions) { - return func(n *NewRepositoryOptions) { + makeFormat := func(objectFormat string) func(*repo.NewRepositoryOptions) { + return func(n *repo.NewRepositoryOptions) { n.BlockFormat = objectFormat n.ObjectHMACSecret = []byte("key") n.MaxBlockSize = 10000 n.Splitter = "FIXED" - n.noHMAC = false } } cases := []struct { - format func(*NewRepositoryOptions) + format func(*repo.NewRepositoryOptions) oids map[string]object.ID }{ { - format: func(n *NewRepositoryOptions) { + format: func(n *repo.NewRepositoryOptions) { n.MaxBlockSize = 10000 - n.noHMAC = true }, oids: map[string]object.ID{ "": "b613679a0814d9ec772f95d778c35fc5ff1697c493715653c6c712144292c5ad", @@ -367,11 +282,12 @@ func TestFormats(t *testing.T) { } for caseIndex, c := range cases { - _, _, repo := setupTest(t, c.format) + var env repotesting.Environment + defer env.Setup(t, c.format).Close(t) for k, v := range c.oids { bytesToWrite := []byte(k) - w := repo.Objects.NewWriter(ctx, object.WriterOptions{}) + w := env.Repository.Objects.NewWriter(ctx, object.WriterOptions{}) w.Write(bytesToWrite) oid, err := w.Result() if err != nil { @@ -381,7 +297,7 @@ func TestFormats(t *testing.T) { t.Errorf("invalid oid for #%v\ngot:\n%#v\nexpected:\n%#v", caseIndex, oid.String(), v.String()) } - rc, err := repo.Objects.Open(ctx, oid) + rc, err := env.Repository.Objects.Open(ctx, oid) if err != nil { t.Errorf("open failed: %v", err) continue diff --git a/snapshot/manager.go b/snapshot/manager.go index 45df05955..121756491 100644 --- a/snapshot/manager.go +++ b/snapshot/manager.go @@ -56,21 +56,35 @@ func ListSnapshots(ctx context.Context, rep *repo.Repository, si SourceInfo) ([] return LoadSnapshots(ctx, rep, manifest.EntryIDs(entries)) } -// LoadSnapshot loads and parses a snapshot with a given ID. -func LoadSnapshot(ctx context.Context, rep *repo.Repository, manifestID string) (*Manifest, error) { +// loadSnapshot loads and parses a snapshot with a given ID. +func loadSnapshot(ctx context.Context, rep *repo.Repository, manifestID string) (*Manifest, error) { sm := &Manifest{} if err := rep.Manifests.Get(ctx, manifestID, sm); err != nil { return nil, fmt.Errorf("unable to find manifest entries: %v", err) } sm.ID = manifestID - return sm, nil } // SaveSnapshot persists given snapshot manifest and returns manifest ID. func SaveSnapshot(ctx context.Context, rep *repo.Repository, manifest *Manifest) (string, error) { - return rep.Manifests.Put(ctx, sourceInfoToLabels(manifest.Source), manifest) + if manifest.Source.Host == "" { + return "", fmt.Errorf("missing host") + } + if manifest.Source.UserName == "" { + return "", fmt.Errorf("missing username") + } + if manifest.Source.Path == "" { + return "", fmt.Errorf("missing path") + } + + id, err := rep.Manifests.Put(ctx, sourceInfoToLabels(manifest.Source), manifest) + if err != nil { + return "", err + } + manifest.ID = id + return id, nil } // LoadSnapshots efficiently loads and parses a given list of snapshot IDs. @@ -83,7 +97,7 @@ func LoadSnapshots(ctx context.Context, rep *repo.Repository, names []string) ([ go func(i int, n string) { defer func() { <-sem }() - m, err := LoadSnapshot(ctx, rep, n) + m, err := loadSnapshot(ctx, rep, n) if err != nil { log.Warningf("unable to parse snapshot manifest %v: %v", n, err) return diff --git a/snapshot/snapshot_test.go b/snapshot/snapshot_test.go new file mode 100644 index 000000000..d97ddfc9e --- /dev/null +++ b/snapshot/snapshot_test.go @@ -0,0 +1,210 @@ +package snapshot_test + +import ( + "context" + "os" + "path/filepath" + "reflect" + "sort" + "testing" + + "github.com/kopia/kopia/internal/repotesting" + "github.com/kopia/kopia/repo" + "github.com/kopia/kopia/snapshot" +) + +func TestSnapshotsAPI(t *testing.T) { + var env repotesting.Environment + defer env.Setup(t).Close(t) + + src1 := snapshot.SourceInfo{ + Host: "host-1", + UserName: "user-1", + Path: "/some/path", + } + + src2 := snapshot.SourceInfo{ + Host: "host-1", + UserName: "user-1", + Path: "/some/other/path", + } + + verifySnapshotManifestIDs(t, env.Repository, nil, nil) + verifySnapshotManifestIDs(t, env.Repository, &src1, nil) + verifySnapshotManifestIDs(t, env.Repository, &src2, nil) + verifyListSnapshots(t, env.Repository, src1, []*snapshot.Manifest{}) + verifyListSnapshots(t, env.Repository, src2, []*snapshot.Manifest{}) + manifest1 := &snapshot.Manifest{ + Source: src1, + Description: "some-description", + } + id1 := mustSaveSnapshot(t, env.Repository, manifest1) + verifySnapshotManifestIDs(t, env.Repository, nil, []string{id1}) + verifySnapshotManifestIDs(t, env.Repository, &src1, []string{id1}) + verifySnapshotManifestIDs(t, env.Repository, &src2, nil) + verifyListSnapshots(t, env.Repository, src1, []*snapshot.Manifest{manifest1}) + + manifest2 := &snapshot.Manifest{ + Source: src1, + Description: "some-other-description", + } + id2 := mustSaveSnapshot(t, env.Repository, manifest2) + if id1 == id2 { + t.Errorf("expected different manifest IDs, got same: %v", id1) + } + verifySnapshotManifestIDs(t, env.Repository, nil, []string{id1, id2}) + verifySnapshotManifestIDs(t, env.Repository, &src1, []string{id1, id2}) + verifySnapshotManifestIDs(t, env.Repository, &src2, nil) + + manifest3 := &snapshot.Manifest{ + Source: src2, + Description: "some-other-description", + } + + id3 := mustSaveSnapshot(t, env.Repository, manifest3) + verifySnapshotManifestIDs(t, env.Repository, nil, []string{id1, id2, id3}) + verifySnapshotManifestIDs(t, env.Repository, &src1, []string{id1, id2}) + verifySnapshotManifestIDs(t, env.Repository, &src2, []string{id3}) + verifySources(t, env.Repository, src1, src2) + verifyLoadSnapshots(t, env.Repository, []string{id1, id2, id3}, []*snapshot.Manifest{manifest1, manifest2, manifest3}) +} + +func verifySnapshotManifestIDs(t *testing.T, rep *repo.Repository, src *snapshot.SourceInfo, expected []string) []string { + t.Helper() + res, err := snapshot.ListSnapshotManifests(context.Background(), rep, src) + if err != nil { + t.Errorf("error listing snapshot manifests: %v", err) + } + sort.Strings(res) + sort.Strings(expected) + if !reflect.DeepEqual(res, expected) { + t.Errorf("unexpected manifests: %v, wanted %v", res, expected) + return expected + } + return res +} + +func mustSaveSnapshot(t *testing.T, rep *repo.Repository, man *snapshot.Manifest) string { + t.Helper() + id, err := snapshot.SaveSnapshot(context.Background(), rep, man) + if err != nil { + t.Fatalf("error saving snapshot: %v", err) + } + return id +} + +func verifySources(t *testing.T, rep *repo.Repository, sources ...snapshot.SourceInfo) { + sources, err := snapshot.ListSources(context.Background(), rep) + if err != nil { + t.Errorf("error listing sources: %v", err) + } + + if got, want := sorted(sourcesToStrings(sources...)), sorted(sourcesToStrings(sources...)); !reflect.DeepEqual(got, want) { + t.Errorf("unexpected sources: %v want %v", got, want) + } +} + +func verifyListSnapshots(t *testing.T, rep *repo.Repository, src snapshot.SourceInfo, expected []*snapshot.Manifest) { + t.Helper() + got, err := snapshot.ListSnapshots(context.Background(), rep, src) + if err != nil { + t.Errorf("error loading manifests: %v", err) + return + } + + if !reflect.DeepEqual(got, expected) { + t.Errorf("unexpected manifests: %v, wanted %v", got, expected) + for i, m := range got { + t.Logf("got[%v]=%#v", i, m) + } + for i, m := range expected { + t.Logf("want[%v]=%#v", i, m) + } + } +} + +func verifyLoadSnapshots(t *testing.T, rep *repo.Repository, ids []string, expected []*snapshot.Manifest) { + got, err := snapshot.LoadSnapshots(context.Background(), rep, ids) + if err != nil { + t.Errorf("error loading manifests: %v", err) + return + } + + if !reflect.DeepEqual(got, expected) { + t.Errorf("unexpected manifests: %v, wanted %v", got, expected) + for i, m := range got { + t.Logf("got[%v]=%#v", i, m) + } + for i, m := range expected { + t.Logf("want[%v]=%#v", i, m) + } + } +} + +func sorted(s []string) []string { + res := append([]string(nil), s...) + sort.Strings(res) + return res +} + +func sourcesToStrings(sources ...snapshot.SourceInfo) []string { + var res []string + + for _, src := range sources { + res = append(res, src.String()) + } + + return res +} + +func TestParseSourceInfo(t *testing.T) { + cwd, _ := os.Getwd() + + cases := []struct { + path string + want snapshot.SourceInfo + }{ + {"(global)", snapshot.SourceInfo{}}, + {"", snapshot.SourceInfo{UserName: "default-user", Host: "default-host", Path: cwd}}, + {".", snapshot.SourceInfo{UserName: "default-user", Host: "default-host", Path: cwd}}, + {"..", snapshot.SourceInfo{UserName: "default-user", Host: "default-host", Path: filepath.Clean(filepath.Join(cwd, ".."))}}, + {"foo@bar:/some/path", snapshot.SourceInfo{UserName: "foo", Host: "bar", Path: "/some/path"}}, + {"/some/path", snapshot.SourceInfo{UserName: "default-user", Host: "default-host", Path: "/some/path"}}, + {"/some/path/../other-path", snapshot.SourceInfo{UserName: "default-user", Host: "default-host", Path: "/some/other-path"}}, + {"@some-host", snapshot.SourceInfo{Host: "some-host"}}, + {"some-user@some-host", snapshot.SourceInfo{UserName: "some-user", Host: "some-host"}}, + } + + for _, tc := range cases { + got, err := snapshot.ParseSourceInfo(tc.path, "default-host", "default-user") + if err != nil { + t.Errorf("error parsing %q: %v", tc.path, err) + continue + } + if got != tc.want { + t.Errorf("unexpected parsed value of %q: %v, wanted %v", tc.path, got, tc.want) + } + got2, err := snapshot.ParseSourceInfo(got.String(), "default-host", "default-user") + if err != nil { + t.Errorf("error parsing %q: %v", tc.path, err) + continue + } + if got != got2 { + t.Errorf("unexpected parsed value of %q: %v, wanted %v", got.String(), got2, got) + } + } + +} + +func TestParseInvalidSourceInfo(t *testing.T) { + cases := []string{ + "@", + } + + for _, tc := range cases { + si, err := snapshot.ParseSourceInfo(tc, "default-host", "default-user") + if err == nil { + t.Errorf("unexpected success when parsing %v: %v", tc, si) + } + } +}