From 168cfc2f6d1bb4df773b491ac0aa6dd275007171 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 10 Apr 2016 16:52:15 +0200 Subject: [PATCH] Add testing helper functions --- src/restic/repository/testing.go | 62 +++++++++++ src/restic/testing.go | 174 +++++++++++++++++++++++++++++++ src/restic/testing_test.go | 60 +++++++++++ 3 files changed, 296 insertions(+) create mode 100644 src/restic/repository/testing.go create mode 100644 src/restic/testing.go create mode 100644 src/restic/testing_test.go diff --git a/src/restic/repository/testing.go b/src/restic/repository/testing.go new file mode 100644 index 000000000..f35954efa --- /dev/null +++ b/src/restic/repository/testing.go @@ -0,0 +1,62 @@ +package repository + +import ( + "os" + "restic/backend" + "restic/backend/local" + "restic/backend/mem" + "testing" +) + +// TestBackend returns a fully configured in-memory backend. +func TestBackend(t testing.TB) (be backend.Backend, cleanup func()) { + return mem.New(), func() {} +} + +const TestPassword = "geheim" + +// TestRepositoryWithBackend returns a repository initialized with a test +// password. If be is nil, an in-memory backend is used. +func TestRepositoryWithBackend(t testing.TB, be backend.Backend) (r *Repository, cleanup func()) { + var beCleanup func() + if be == nil { + be, beCleanup = TestBackend(t) + } + + r = New(be) + + err := r.Init(TestPassword) + if err != nil { + t.Fatalf("TestRepopository(): initialize repo failed: %v", err) + } + + return r, func() { + if beCleanup != nil { + beCleanup() + } + } +} + +// TestRepository returns a repository initialized with a test password on an +// in-memory backend. When the environment variable RESTIC_TEST_REPO is set to +// a non-existing directory, a local backend is created there and this is used +// instead. The directory is not removed. +func TestRepository(t testing.TB) (r *Repository, cleanup func()) { + dir := os.Getenv("RESTIC_TEST_REPO") + if dir != "" { + _, err := os.Stat(dir) + if err != nil { + be, err := local.Create(dir) + if err != nil { + t.Fatalf("error creating local backend at %v: %v", dir, err) + } + return TestRepositoryWithBackend(t, be) + } + + if err == nil { + t.Logf("directory at %v already exists, using mem backend", dir) + } + } + + return TestRepositoryWithBackend(t, nil) +} diff --git a/src/restic/testing.go b/src/restic/testing.go new file mode 100644 index 000000000..1b9e1a7fb --- /dev/null +++ b/src/restic/testing.go @@ -0,0 +1,174 @@ +package restic + +import ( + "bytes" + "fmt" + "io" + "math/rand" + "restic/backend" + "restic/pack" + "restic/repository" + "testing" + "time" + + "github.com/restic/chunker" +) + +type randReader struct { + rnd *rand.Rand + buf []byte +} + +func newRandReader(rnd *rand.Rand) io.Reader { + return &randReader{rnd: rnd, buf: make([]byte, 0, 7)} +} + +func (rd *randReader) read(p []byte) (n int, err error) { + if len(p)%7 != 0 { + panic("invalid buffer length, not multiple of 7") + } + + rnd := rd.rnd + for i := 0; i < len(p); i += 7 { + val := rnd.Int63() + + p[i+0] = byte(val >> 0) + p[i+1] = byte(val >> 8) + p[i+2] = byte(val >> 16) + p[i+3] = byte(val >> 24) + p[i+4] = byte(val >> 32) + p[i+5] = byte(val >> 40) + p[i+6] = byte(val >> 48) + } + + return len(p), nil +} + +func (rd *randReader) Read(p []byte) (int, error) { + // first, copy buffer to p + pos := copy(p, rd.buf) + copy(rd.buf, rd.buf[pos:]) + + // shorten buf and p accordingly + rd.buf = rd.buf[:len(rd.buf)-pos] + p = p[pos:] + + // if this is enough to fill p, return + if len(p) == 0 { + return pos, nil + } + + // load multiple of 7 byte + l := (len(p) / 7) * 7 + n, err := rd.read(p[:l]) + pos += n + if err != nil { + return pos, err + } + p = p[n:] + + // load 7 byte to temp buffer + rd.buf = rd.buf[:7] + n, err = rd.read(rd.buf) + if err != nil { + return pos, err + } + + // copy the remaining bytes from the buffer to p + n = copy(p, rd.buf) + pos += n + + // save the remaining bytes in rd.buf + n = copy(rd.buf, rd.buf[n:]) + rd.buf = rd.buf[:n] + + return pos, nil +} + +// fakeFile returns a reader which yields deterministic pseudo-random data. +func fakeFile(t testing.TB, seed, size int64) io.Reader { + return io.LimitReader(newRandReader(rand.New(rand.NewSource(seed))), size) +} + +// saveFile reads from rd and saves the blobs in the repository. The list of +// IDs is returned. +func saveFile(t testing.TB, repo *repository.Repository, rd io.Reader) (blobs backend.IDs) { + ch := chunker.New(rd, repo.Config.ChunkerPolynomial) + + for { + chunk, err := ch.Next(getBuf()) + if err == io.EOF { + break + } + + if err != nil { + t.Fatalf("unabel to save chunk in repo: %v", err) + } + + id := backend.Hash(chunk.Data) + err = repo.SaveFrom(pack.Data, &id, uint(len(chunk.Data)), bytes.NewReader(chunk.Data)) + if err != nil { + t.Fatalf("error saving chunk: %v", err) + } + blobs = append(blobs, id) + } + + return blobs +} + +// saveTree saves a tree of fake files in the repo and returns the ID. +func saveTree(t testing.TB, repo *repository.Repository, seed int64) backend.ID { + rnd := rand.NewSource(seed) + numNodes := int(rnd.Int63() % 64) + t.Logf("create %v nodes", numNodes) + + var tree Tree + for i := 0; i < numNodes; i++ { + t.Logf("create node %v", i) + + node := &Node{} + + tree.Nodes = append(tree.Nodes, node) + } + + id, err := repo.SaveJSON(pack.Tree, tree) + if err != nil { + t.Fatal(err) + } + + return id +} + +// TestRepositoryCreateSnapshot creates a snapshot filled with fake data. The +// fake data is generated deterministically from the timestamp `at`, which is +// also used as the snapshot's timestamp. +func TestCreateSnapshot(t testing.TB, repo *repository.Repository, at time.Time) backend.ID { + fakedir := fmt.Sprintf("fakedir-at-%v", at.Format("2006-01-02 15:04:05")) + snapshot, err := NewSnapshot([]string{fakedir}) + if err != nil { + t.Fatal(err) + } + snapshot.Time = at + + treeID := saveTree(t, repo, at.UnixNano()) + snapshot.Tree = &treeID + + id, err := repo.SaveJSONUnpacked(backend.Snapshot, snapshot) + if err != nil { + t.Fatal(err) + } + + t.Logf("saved snapshot %v", id.Str()) + + err = repo.Flush() + if err != nil { + t.Fatal(err) + } + + err = repo.SaveIndex() + if err != nil { + t.Fatal(err) + } + + return id +} diff --git a/src/restic/testing_test.go b/src/restic/testing_test.go new file mode 100644 index 000000000..bf1733f51 --- /dev/null +++ b/src/restic/testing_test.go @@ -0,0 +1,60 @@ +package restic_test + +import ( + "restic" + "restic/checker" + "restic/repository" + "testing" + "time" +) + +var testSnapshotTime = time.Unix(1460289341, 207401672) + +func TestCreateSnapshot(t *testing.T) { + repo, cleanup := repository.TestRepository(t) + defer cleanup() + + restic.TestCreateSnapshot(t, repo, testSnapshotTime) + + snapshots, err := restic.LoadAllSnapshots(repo) + if err != nil { + t.Fatal(err) + } + + if len(snapshots) != 1 { + t.Fatalf("got %d snapshots, expected %d", len(snapshots), 1) + } + + sn := snapshots[0] + if sn.Time != testSnapshotTime { + t.Fatalf("got timestamp %v, expected %v", sn.Time, testSnapshotTime) + } + + if sn.Tree == nil { + t.Fatalf("tree id is nil") + } + + if sn.Tree.IsNull() { + t.Fatalf("snapshot has zero tree ID") + } + + chkr := checker.New(repo) + + hints, errs := chkr.LoadIndex() + if len(errs) != 0 { + t.Fatalf("errors loading index: %v", errs) + } + + if len(hints) != 0 { + t.Fatalf("errors loading index: %v", hints) + } + + done := make(chan struct{}) + defer close(done) + errChan := make(chan error) + go chkr.Structure(errChan, done) + + for err := range errChan { + t.Error(err) + } +}