mirror of https://github.com/perkeep/perkeep.git
1166 lines
32 KiB
Go
1166 lines
32 KiB
Go
/*
|
|
Copyright 2011 Google Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
// Package indextest contains the unit tests for the indexer so they
|
|
// can be re-used for each specific implementation of the index
|
|
// Storage interface.
|
|
package indextest
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"camlistore.org/pkg/blob"
|
|
"camlistore.org/pkg/index"
|
|
"camlistore.org/pkg/jsonsign"
|
|
"camlistore.org/pkg/osutil"
|
|
"camlistore.org/pkg/schema"
|
|
"camlistore.org/pkg/test"
|
|
"camlistore.org/pkg/types/camtypes"
|
|
)
|
|
|
|
// An IndexDeps is a helper for populating and querying an Index for tests.
|
|
type IndexDeps struct {
|
|
Index *index.Index
|
|
|
|
BlobSource *test.Fetcher
|
|
|
|
// Following three needed for signing:
|
|
PublicKeyFetcher *test.Fetcher
|
|
EntityFetcher jsonsign.EntityFetcher // fetching decrypted openpgp entities
|
|
SignerBlobRef blob.Ref
|
|
|
|
now time.Time // fake clock, nanos since epoch
|
|
|
|
Fataler // optional means of failing.
|
|
}
|
|
|
|
type Fataler interface {
|
|
Fatalf(format string, args ...interface{})
|
|
}
|
|
|
|
type logFataler struct{}
|
|
|
|
func (logFataler) Fatalf(format string, args ...interface{}) {
|
|
log.Fatalf(format, args...)
|
|
}
|
|
|
|
func (id *IndexDeps) Get(key string) string {
|
|
v, _ := id.Index.Storage().Get(key)
|
|
return v
|
|
}
|
|
|
|
func (id *IndexDeps) Set(key, value string) error {
|
|
return id.Index.Storage().Set(key, value)
|
|
}
|
|
|
|
func (id *IndexDeps) DumpIndex(t *testing.T) {
|
|
t.Logf("Begin index dump:")
|
|
it := id.Index.Storage().Find("", "")
|
|
for it.Next() {
|
|
t.Logf(" %q = %q", it.Key(), it.Value())
|
|
}
|
|
if err := it.Close(); err != nil {
|
|
t.Fatalf("iterator close = %v", err)
|
|
}
|
|
t.Logf("End index dump.")
|
|
}
|
|
|
|
func (id *IndexDeps) uploadAndSign(m *schema.Builder) blob.Ref {
|
|
m.SetSigner(id.SignerBlobRef)
|
|
unsigned, err := m.JSON()
|
|
if err != nil {
|
|
id.Fatalf("uploadAndSignMap: " + err.Error())
|
|
}
|
|
sr := &jsonsign.SignRequest{
|
|
UnsignedJSON: unsigned,
|
|
Fetcher: id.PublicKeyFetcher,
|
|
EntityFetcher: id.EntityFetcher,
|
|
SignatureTime: id.now,
|
|
}
|
|
signed, err := sr.Sign()
|
|
if err != nil {
|
|
id.Fatalf("problem signing: " + err.Error())
|
|
}
|
|
tb := &test.Blob{Contents: signed}
|
|
_, err = id.BlobSource.ReceiveBlob(tb.BlobRef(), tb.Reader())
|
|
if err != nil {
|
|
id.Fatalf("public uploading signed blob to blob source, pre-indexing: %v, %v", tb.BlobRef(), err)
|
|
}
|
|
_, err = id.Index.ReceiveBlob(tb.BlobRef(), tb.Reader())
|
|
if err != nil {
|
|
id.Fatalf("problem indexing blob: %v\nblob was:\n%s", err, signed)
|
|
}
|
|
return tb.BlobRef()
|
|
}
|
|
|
|
// NewPermanode creates (& signs) a new permanode and adds it
|
|
// to the index, returning its blobref.
|
|
func (id *IndexDeps) NewPermanode() blob.Ref {
|
|
unsigned := schema.NewUnsignedPermanode()
|
|
return id.uploadAndSign(unsigned)
|
|
}
|
|
|
|
// NewPermanode creates (& signs) a new planned permanode and adds it
|
|
// to the index, returning its blobref.
|
|
func (id *IndexDeps) NewPlannedPermanode(key string) blob.Ref {
|
|
unsigned := schema.NewPlannedPermanode(key)
|
|
return id.uploadAndSign(unsigned)
|
|
}
|
|
|
|
func (id *IndexDeps) advanceTime() time.Time {
|
|
id.now = id.now.Add(1 * time.Second)
|
|
return id.now
|
|
}
|
|
|
|
// LastTime returns the time of the most recent mutation (claim).
|
|
func (id *IndexDeps) LastTime() time.Time {
|
|
return id.now
|
|
}
|
|
|
|
func (id *IndexDeps) SetAttribute(permaNode blob.Ref, attr, value string) blob.Ref {
|
|
m := schema.NewSetAttributeClaim(permaNode, attr, value)
|
|
m.SetClaimDate(id.advanceTime())
|
|
return id.uploadAndSign(m)
|
|
}
|
|
|
|
func (id *IndexDeps) SetAttribute_NoTimeMove(permaNode blob.Ref, attr, value string) blob.Ref {
|
|
m := schema.NewSetAttributeClaim(permaNode, attr, value)
|
|
m.SetClaimDate(id.LastTime())
|
|
return id.uploadAndSign(m)
|
|
}
|
|
|
|
func (id *IndexDeps) AddAttribute(permaNode blob.Ref, attr, value string) blob.Ref {
|
|
m := schema.NewAddAttributeClaim(permaNode, attr, value)
|
|
m.SetClaimDate(id.advanceTime())
|
|
return id.uploadAndSign(m)
|
|
}
|
|
|
|
func (id *IndexDeps) DelAttribute(permaNode blob.Ref, attr, value string) blob.Ref {
|
|
m := schema.NewDelAttributeClaim(permaNode, attr, value)
|
|
m.SetClaimDate(id.advanceTime())
|
|
return id.uploadAndSign(m)
|
|
}
|
|
|
|
func (id *IndexDeps) Delete(target blob.Ref) blob.Ref {
|
|
m := schema.NewDeleteClaim(target)
|
|
m.SetClaimDate(id.advanceTime())
|
|
return id.uploadAndSign(m)
|
|
}
|
|
|
|
var noTime = time.Time{}
|
|
|
|
func (id *IndexDeps) UploadString(v string) blob.Ref {
|
|
cb := &test.Blob{Contents: v}
|
|
id.BlobSource.AddBlob(cb)
|
|
br := cb.BlobRef()
|
|
_, err := id.Index.ReceiveBlob(br, cb.Reader())
|
|
if err != nil {
|
|
id.Fatalf("UploadString: %v", err)
|
|
}
|
|
return br
|
|
}
|
|
|
|
// If modTime is zero, it's not used.
|
|
func (id *IndexDeps) UploadFile(fileName string, contents string, modTime time.Time) (fileRef, wholeRef blob.Ref) {
|
|
wholeRef = id.UploadString(contents)
|
|
|
|
m := schema.NewFileMap(fileName)
|
|
m.PopulateParts(int64(len(contents)), []schema.BytesPart{
|
|
schema.BytesPart{
|
|
Size: uint64(len(contents)),
|
|
BlobRef: wholeRef,
|
|
}})
|
|
if !modTime.IsZero() {
|
|
m.SetModTime(modTime)
|
|
}
|
|
fjson, err := m.JSON()
|
|
if err != nil {
|
|
id.Fatalf("UploadFile.JSON: %v", err)
|
|
}
|
|
fb := &test.Blob{Contents: fjson}
|
|
id.BlobSource.AddBlob(fb)
|
|
fileRef = fb.BlobRef()
|
|
_, err = id.Index.ReceiveBlob(fileRef, fb.Reader())
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return
|
|
}
|
|
|
|
// If modTime is zero, it's not used.
|
|
func (id *IndexDeps) UploadDir(dirName string, children []blob.Ref, modTime time.Time) blob.Ref {
|
|
// static-set entries blob
|
|
ss := new(schema.StaticSet)
|
|
for _, child := range children {
|
|
ss.Add(child)
|
|
}
|
|
ssjson := ss.Blob().JSON()
|
|
ssb := &test.Blob{Contents: ssjson}
|
|
id.BlobSource.AddBlob(ssb)
|
|
_, err := id.Index.ReceiveBlob(ssb.BlobRef(), ssb.Reader())
|
|
if err != nil {
|
|
id.Fatalf("UploadDir.ReceiveBlob: %v", err)
|
|
}
|
|
|
|
// directory blob
|
|
bb := schema.NewDirMap(dirName)
|
|
bb.PopulateDirectoryMap(ssb.BlobRef())
|
|
if !modTime.IsZero() {
|
|
bb.SetModTime(modTime)
|
|
}
|
|
dirjson, err := bb.JSON()
|
|
if err != nil {
|
|
id.Fatalf("UploadDir.JSON: %v", err)
|
|
}
|
|
dirb := &test.Blob{Contents: dirjson}
|
|
id.BlobSource.AddBlob(dirb)
|
|
_, err = id.Index.ReceiveBlob(dirb.BlobRef(), dirb.Reader())
|
|
if err != nil {
|
|
id.Fatalf("UploadDir.ReceiveBlob: %v", err)
|
|
}
|
|
return dirb.BlobRef()
|
|
}
|
|
|
|
// NewIndexDeps returns an IndexDeps helper for populating and working
|
|
// with the provided index for tests.
|
|
func NewIndexDeps(index *index.Index) *IndexDeps {
|
|
camliRootPath, err := osutil.GoPackagePath("camlistore.org")
|
|
if err != nil {
|
|
log.Fatal("Package camlistore.org no found in $GOPATH or $GOPATH not defined")
|
|
}
|
|
secretRingFile := filepath.Join(camliRootPath, "pkg", "jsonsign", "testdata", "test-secring.gpg")
|
|
pubKey := &test.Blob{Contents: `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
|
|
|
xsBNBEzgoVsBCAC/56aEJ9BNIGV9FVP+WzenTAkg12k86YqlwJVAB/VwdMlyXxvi
|
|
bCT1RVRfnYxscs14LLfcMWF3zMucw16mLlJCBSLvbZ0jn4h+/8vK5WuAdjw2YzLs
|
|
WtBcjWn3lV6tb4RJz5gtD/o1w8VWxwAnAVIWZntKAWmkcChCRgdUeWso76+plxE5
|
|
aRYBJqdT1mctGqNEISd/WYPMgwnWXQsVi3x4z1dYu2tD9uO1dkAff12z1kyZQIBQ
|
|
rexKYRRRh9IKAayD4kgS0wdlULjBU98aeEaMz1ckuB46DX3lAYqmmTEL/Rl9cOI0
|
|
Enpn/oOOfYFa5h0AFndZd1blMvruXfdAobjVABEBAAE=
|
|
=28/7
|
|
-----END PGP PUBLIC KEY BLOCK-----`}
|
|
|
|
id := &IndexDeps{
|
|
Index: index,
|
|
BlobSource: new(test.Fetcher),
|
|
PublicKeyFetcher: new(test.Fetcher),
|
|
EntityFetcher: &jsonsign.CachingEntityFetcher{
|
|
Fetcher: &jsonsign.FileEntityFetcher{File: secretRingFile},
|
|
},
|
|
SignerBlobRef: pubKey.BlobRef(),
|
|
now: test.ClockOrigin,
|
|
Fataler: logFataler{},
|
|
}
|
|
// Add dev client test key public key, keyid 26F5ABDA,
|
|
// blobref sha1-ad87ca5c78bd0ce1195c46f7c98e6025abbaf007
|
|
if g, w := id.SignerBlobRef.String(), "sha1-ad87ca5c78bd0ce1195c46f7c98e6025abbaf007"; g != w {
|
|
id.Fatalf("unexpected signer blobref; got signer = %q; want %q", g, w)
|
|
}
|
|
id.PublicKeyFetcher.AddBlob(pubKey)
|
|
id.Index.KeyFetcher = id.PublicKeyFetcher
|
|
id.Index.BlobSource = id.BlobSource
|
|
return id
|
|
}
|
|
|
|
func Index(t *testing.T, initIdx func() *index.Index) {
|
|
id := NewIndexDeps(initIdx())
|
|
id.Fataler = t
|
|
defer id.DumpIndex(t)
|
|
pn := id.NewPermanode()
|
|
t.Logf("uploaded permanode %q", pn)
|
|
br1 := id.SetAttribute(pn, "tag", "foo1")
|
|
br1Time := id.LastTime()
|
|
t.Logf("set attribute %q", br1)
|
|
br2 := id.SetAttribute(pn, "tag", "foo2")
|
|
br2Time := id.LastTime()
|
|
t.Logf("set attribute %q", br2)
|
|
rootClaim := id.SetAttribute(pn, "camliRoot", "rootval")
|
|
rootClaimTime := id.LastTime()
|
|
t.Logf("set attribute %q", rootClaim)
|
|
|
|
pnChild := id.NewPermanode()
|
|
br3 := id.SetAttribute(pnChild, "tag", "bar")
|
|
br3Time := id.LastTime()
|
|
t.Logf("set attribute %q", br3)
|
|
memberRef := id.AddAttribute(pn, "camliMember", pnChild.String())
|
|
t.Logf("add-attribute claim %q points to member permanode %q", memberRef, pnChild)
|
|
memberRefTime := id.LastTime()
|
|
|
|
// TODO(bradfitz): add EXIF tests here, once that stuff is ready.
|
|
if false {
|
|
camliRootPath, err := osutil.GoPackagePath("camlistore.org")
|
|
if err != nil {
|
|
t.Fatal("Package camlistore.org no found in $GOPATH or $GOPATH not defined")
|
|
}
|
|
for i := 1; i <= 8; i++ {
|
|
fileBase := fmt.Sprintf("f%d-exif.jpg", i)
|
|
fileName := filepath.Join(camliRootPath, "pkg", "images", "testdata", fileBase)
|
|
contents, err := ioutil.ReadFile(fileName)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
id.UploadFile(fileBase, string(contents), noTime)
|
|
}
|
|
}
|
|
|
|
// Upload some files.
|
|
var jpegFileRef, exifFileRef, mediaFileRef, mediaWholeRef blob.Ref
|
|
{
|
|
camliRootPath, err := osutil.GoPackagePath("camlistore.org")
|
|
if err != nil {
|
|
t.Fatal("Package camlistore.org no found in $GOPATH or $GOPATH not defined")
|
|
}
|
|
uploadFile := func(file string, modTime time.Time) (fileRef, wholeRef blob.Ref) {
|
|
fileName := filepath.Join(camliRootPath, "pkg", "index", "indextest", "testdata", file)
|
|
contents, err := ioutil.ReadFile(fileName)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fileRef, wholeRef = id.UploadFile(file, string(contents), modTime)
|
|
return
|
|
}
|
|
jpegFileRef, _ = uploadFile("dude.jpg", noTime)
|
|
exifFileRef, _ = uploadFile("dude-exif.jpg", time.Unix(1361248796, 0))
|
|
mediaFileRef, mediaWholeRef = uploadFile("0s.mp3", noTime)
|
|
}
|
|
|
|
// Upload the dir containing the previous files.
|
|
imagesDirRef := id.UploadDir(
|
|
"testdata",
|
|
[]blob.Ref{jpegFileRef, exifFileRef, mediaFileRef},
|
|
time.Now(),
|
|
)
|
|
|
|
lastPermanodeMutation := id.LastTime()
|
|
|
|
key := "signerkeyid:sha1-ad87ca5c78bd0ce1195c46f7c98e6025abbaf007"
|
|
if g, e := id.Get(key), "2931A67C26F5ABDA"; g != e {
|
|
t.Fatalf("%q = %q, want %q", key, g, e)
|
|
}
|
|
|
|
key = "imagesize|" + jpegFileRef.String()
|
|
if g, e := id.Get(key), "50|100"; g != e {
|
|
t.Errorf("JPEG dude.jpg key %q = %q; want %q", key, g, e)
|
|
}
|
|
key = "filetimes|" + jpegFileRef.String()
|
|
if g, e := id.Get(key), ""; g != e {
|
|
t.Errorf("JPEG dude.jpg key %q = %q; want %q", key, g, e)
|
|
}
|
|
|
|
key = "filetimes|" + exifFileRef.String()
|
|
if g, e := id.Get(key), "2013-02-18T01%3A11%3A20Z%2C2013-02-19T04%3A39%3A56Z"; g != e {
|
|
t.Errorf("EXIF dude-exif.jpg key %q = %q; want %q", key, g, e)
|
|
}
|
|
|
|
key = "have:" + pn.String()
|
|
pnSizeStr := id.Get(key)
|
|
if pnSizeStr == "" {
|
|
t.Fatalf("missing key %q", key)
|
|
}
|
|
|
|
key = "meta:" + pn.String()
|
|
if g, e := id.Get(key), pnSizeStr+"|application/json; camliType=permanode"; g != e {
|
|
t.Errorf("key %q = %q, want %q", key, g, e)
|
|
}
|
|
|
|
key = "recpn|2931A67C26F5ABDA|rt7988-88-71T98:67:62.999876543Z|" + br1.String()
|
|
if g, e := id.Get(key), pn.String(); g != e {
|
|
t.Fatalf("%q = %q, want %q (permanode)", key, g, e)
|
|
}
|
|
|
|
key = "recpn|2931A67C26F5ABDA|rt7988-88-71T98:67:61.999876543Z|" + br2.String()
|
|
if g, e := id.Get(key), pn.String(); g != e {
|
|
t.Fatalf("%q = %q, want %q (permanode)", key, g, e)
|
|
}
|
|
|
|
key = fmt.Sprintf("edgeback|%s|%s|%s", pnChild, pn, memberRef)
|
|
if g, e := id.Get(key), "permanode|"; g != e {
|
|
t.Fatalf("edgeback row %q = %q, want %q", key, g, e)
|
|
}
|
|
|
|
mediaTests := []struct {
|
|
prop, exp string
|
|
}{
|
|
{"title", "Zero Seconds"},
|
|
{"artist", "Test Artist"},
|
|
{"album", "Test Album"},
|
|
{"genre", "(20)Alternative"},
|
|
{"musicbrainzalbumid", "00000000-0000-0000-0000-000000000000"},
|
|
{"year", "1992"},
|
|
{"track", "1"},
|
|
{"disc", "2"},
|
|
{"mediaref", "sha1-fefac74a1d5928316d7131747107c8a61b71ffe4"},
|
|
{"durationms", "26"},
|
|
}
|
|
for _, tt := range mediaTests {
|
|
key = fmt.Sprintf("mediatag|%s|%s", mediaWholeRef.String(), tt.prop)
|
|
if g, _ := url.QueryUnescape(id.Get(key)); g != tt.exp {
|
|
t.Errorf("0s.mp3 key %q = %q; want %q", key, g, tt.exp)
|
|
}
|
|
}
|
|
|
|
// PermanodeOfSignerAttrValue
|
|
{
|
|
gotPN, err := id.Index.PermanodeOfSignerAttrValue(id.SignerBlobRef, "camliRoot", "rootval")
|
|
if err != nil {
|
|
t.Fatalf("id.Index.PermanodeOfSignerAttrValue = %v", err)
|
|
}
|
|
if gotPN.String() != pn.String() {
|
|
t.Errorf("id.Index.PermanodeOfSignerAttrValue = %q, want %q", gotPN, pn)
|
|
}
|
|
_, err = id.Index.PermanodeOfSignerAttrValue(id.SignerBlobRef, "camliRoot", "MISSING")
|
|
if err == nil {
|
|
t.Errorf("expected an error from PermanodeOfSignerAttrValue on missing value")
|
|
}
|
|
}
|
|
|
|
// SearchPermanodesWithAttr - match attr type "tag" and value "foo1"
|
|
{
|
|
ch := make(chan blob.Ref, 10)
|
|
req := &camtypes.PermanodeByAttrRequest{
|
|
Signer: id.SignerBlobRef,
|
|
Attribute: "tag",
|
|
Query: "foo1",
|
|
}
|
|
err := id.Index.SearchPermanodesWithAttr(ch, req)
|
|
if err != nil {
|
|
t.Fatalf("SearchPermanodesWithAttr = %v", err)
|
|
}
|
|
var got []blob.Ref
|
|
for r := range ch {
|
|
got = append(got, r)
|
|
}
|
|
want := []blob.Ref{pn}
|
|
if len(got) < 1 || got[0].String() != want[0].String() {
|
|
t.Errorf("id.Index.SearchPermanodesWithAttr gives %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
// SearchPermanodesWithAttr - match all with attr type "tag"
|
|
{
|
|
ch := make(chan blob.Ref, 10)
|
|
req := &camtypes.PermanodeByAttrRequest{
|
|
Signer: id.SignerBlobRef,
|
|
Attribute: "tag",
|
|
}
|
|
err := id.Index.SearchPermanodesWithAttr(ch, req)
|
|
if err != nil {
|
|
t.Fatalf("SearchPermanodesWithAttr = %v", err)
|
|
}
|
|
var got []blob.Ref
|
|
for r := range ch {
|
|
got = append(got, r)
|
|
}
|
|
want := []blob.Ref{pn, pnChild}
|
|
if len(got) != len(want) {
|
|
t.Errorf("SearchPermanodesWithAttr results differ.\n got: %q\nwant: %q",
|
|
got, want)
|
|
}
|
|
for _, w := range want {
|
|
found := false
|
|
for _, g := range got {
|
|
if g.String() == w.String() {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("SearchPermanodesWithAttr: %v was not found.\n", w)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Delete value "pony" of type "title" (which does not actually exist) for pn
|
|
br4 := id.DelAttribute(pn, "title", "pony")
|
|
br4Time := id.LastTime()
|
|
// and verify it is not found when searching by attr
|
|
{
|
|
ch := make(chan blob.Ref, 10)
|
|
req := &camtypes.PermanodeByAttrRequest{
|
|
Signer: id.SignerBlobRef,
|
|
Attribute: "title",
|
|
Query: "pony",
|
|
}
|
|
err := id.Index.SearchPermanodesWithAttr(ch, req)
|
|
if err != nil {
|
|
t.Fatalf("SearchPermanodesWithAttr = %v", err)
|
|
}
|
|
var got []blob.Ref
|
|
for r := range ch {
|
|
got = append(got, r)
|
|
}
|
|
want := []blob.Ref{}
|
|
if len(got) != len(want) {
|
|
t.Errorf("SearchPermanodesWithAttr results differ.\n got: %q\nwant: %q",
|
|
got, want)
|
|
}
|
|
}
|
|
|
|
// GetRecentPermanodes
|
|
{
|
|
verify := func(prefix string, want []camtypes.RecentPermanode, before time.Time) {
|
|
ch := make(chan camtypes.RecentPermanode, 10) // expect 2 results, but maybe more if buggy.
|
|
err := id.Index.GetRecentPermanodes(ch, id.SignerBlobRef, 50, before)
|
|
if err != nil {
|
|
t.Fatalf("[%s] GetRecentPermanodes = %v", prefix, err)
|
|
}
|
|
got := []camtypes.RecentPermanode{}
|
|
for r := range ch {
|
|
got = append(got, r)
|
|
}
|
|
if len(got) != len(want) {
|
|
t.Errorf("[%s] GetRecentPermanode results differ.\n got: %v\nwant: %v",
|
|
prefix, searchResults(got), searchResults(want))
|
|
}
|
|
for _, w := range want {
|
|
found := false
|
|
for _, g := range got {
|
|
if g.Equal(w) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("[%s] GetRecentPermanode: %v was not found.\n got: %v\nwant: %v",
|
|
prefix, w, searchResults(got), searchResults(want))
|
|
}
|
|
}
|
|
}
|
|
|
|
want := []camtypes.RecentPermanode{
|
|
{
|
|
Permanode: pn,
|
|
Signer: id.SignerBlobRef,
|
|
LastModTime: br4Time,
|
|
},
|
|
{
|
|
Permanode: pnChild,
|
|
Signer: id.SignerBlobRef,
|
|
LastModTime: br3Time,
|
|
},
|
|
}
|
|
|
|
before := time.Time{}
|
|
verify("Zero before", want, before)
|
|
|
|
before = lastPermanodeMutation
|
|
t.Log("lastPermanodeMutation", lastPermanodeMutation,
|
|
lastPermanodeMutation.Unix())
|
|
verify("Non-zero before", want[1:], before)
|
|
}
|
|
// GetDirMembers
|
|
{
|
|
ch := make(chan blob.Ref, 10) // expect 2 results
|
|
err := id.Index.GetDirMembers(imagesDirRef, ch, 50)
|
|
if err != nil {
|
|
t.Fatalf("GetDirMembers = %v", err)
|
|
}
|
|
got := []blob.Ref{}
|
|
for r := range ch {
|
|
got = append(got, r)
|
|
}
|
|
want := []blob.Ref{jpegFileRef, exifFileRef, mediaFileRef}
|
|
if len(got) != len(want) {
|
|
t.Errorf("GetDirMembers results differ.\n got: %v\nwant: %v",
|
|
got, want)
|
|
}
|
|
for _, w := range want {
|
|
found := false
|
|
for _, g := range got {
|
|
if w == g {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("GetDirMembers: %v was not found.", w)
|
|
}
|
|
}
|
|
}
|
|
|
|
// GetBlobMeta
|
|
{
|
|
meta, err := id.Index.GetBlobMeta(pn)
|
|
if err != nil {
|
|
t.Errorf("GetBlobMeta(%q) = %v", pn, err)
|
|
} else {
|
|
if e := "permanode"; meta.CamliType != e {
|
|
t.Errorf("GetBlobMeta(%q) mime = %q, want %q", pn, meta.CamliType, e)
|
|
}
|
|
if meta.Size == 0 {
|
|
t.Errorf("GetBlobMeta(%q) size is zero", pn)
|
|
}
|
|
}
|
|
_, err = id.Index.GetBlobMeta(blob.ParseOrZero("abc-123"))
|
|
if err != os.ErrNotExist {
|
|
t.Errorf("GetBlobMeta(dummy blobref) = %v; want os.ErrNotExist", err)
|
|
}
|
|
}
|
|
|
|
// AppendClaims
|
|
{
|
|
claims, err := id.Index.AppendClaims(nil, pn, id.SignerBlobRef, "")
|
|
if err != nil {
|
|
t.Errorf("AppendClaims = %v", err)
|
|
} else {
|
|
want := []camtypes.Claim{
|
|
{
|
|
BlobRef: br1,
|
|
Permanode: pn,
|
|
Signer: id.SignerBlobRef,
|
|
Date: br1Time.UTC(),
|
|
Type: "set-attribute",
|
|
Attr: "tag",
|
|
Value: "foo1",
|
|
},
|
|
{
|
|
BlobRef: br2,
|
|
Permanode: pn,
|
|
Signer: id.SignerBlobRef,
|
|
Date: br2Time.UTC(),
|
|
Type: "set-attribute",
|
|
Attr: "tag",
|
|
Value: "foo2",
|
|
},
|
|
{
|
|
BlobRef: rootClaim,
|
|
Permanode: pn,
|
|
Signer: id.SignerBlobRef,
|
|
Date: rootClaimTime.UTC(),
|
|
Type: "set-attribute",
|
|
Attr: "camliRoot",
|
|
Value: "rootval",
|
|
},
|
|
{
|
|
BlobRef: memberRef,
|
|
Permanode: pn,
|
|
Signer: id.SignerBlobRef,
|
|
Date: memberRefTime.UTC(),
|
|
Type: "add-attribute",
|
|
Attr: "camliMember",
|
|
Value: pnChild.String(),
|
|
},
|
|
{
|
|
BlobRef: br4,
|
|
Permanode: pn,
|
|
Signer: id.SignerBlobRef,
|
|
Date: br4Time.UTC(),
|
|
Type: "del-attribute",
|
|
Attr: "title",
|
|
Value: "pony",
|
|
},
|
|
}
|
|
if !reflect.DeepEqual(claims, want) {
|
|
t.Errorf("AppendClaims results differ.\n got: %v\nwant: %v",
|
|
claims, want)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func PathsOfSignerTarget(t *testing.T, initIdx func() *index.Index) {
|
|
id := NewIndexDeps(initIdx())
|
|
id.Fataler = t
|
|
defer id.DumpIndex(t)
|
|
signer := id.SignerBlobRef
|
|
pn := id.NewPermanode()
|
|
t.Logf("uploaded permanode %q", pn)
|
|
|
|
claim1 := id.SetAttribute(pn, "camliPath:somedir", "targ-123")
|
|
claim1Time := id.LastTime().UTC()
|
|
claim2 := id.SetAttribute(pn, "camliPath:with|pipe", "targ-124")
|
|
claim2Time := id.LastTime().UTC()
|
|
t.Logf("made path claims %q and %q", claim1, claim2)
|
|
|
|
type test struct {
|
|
blobref string
|
|
want int
|
|
}
|
|
tests := []test{
|
|
{"targ-123", 1},
|
|
{"targ-124", 1},
|
|
{"targ-125", 0},
|
|
}
|
|
for _, tt := range tests {
|
|
paths, err := id.Index.PathsOfSignerTarget(signer, blob.ParseOrZero(tt.blobref))
|
|
if err != nil {
|
|
t.Fatalf("PathsOfSignerTarget(%q): %v", tt.blobref, err)
|
|
}
|
|
if len(paths) != tt.want {
|
|
t.Fatalf("PathsOfSignerTarget(%q) got %d results; want %d",
|
|
tt.blobref, len(paths), tt.want)
|
|
}
|
|
if tt.blobref == "targ-123" {
|
|
p := paths[0]
|
|
want := fmt.Sprintf(
|
|
"Path{Claim: %s, %v; Base: %s + Suffix \"somedir\" => Target targ-123}",
|
|
claim1, claim1Time, pn)
|
|
if g := p.String(); g != want {
|
|
t.Errorf("claim wrong.\n got: %s\nwant: %s", g, want)
|
|
}
|
|
}
|
|
}
|
|
tests = []test{
|
|
{"somedir", 1},
|
|
{"with|pipe", 1},
|
|
{"void", 0},
|
|
}
|
|
for _, tt := range tests {
|
|
paths, err := id.Index.PathsLookup(id.SignerBlobRef, pn, tt.blobref)
|
|
if err != nil {
|
|
t.Fatalf("PathsLookup(%q): %v", tt.blobref, err)
|
|
}
|
|
if len(paths) != tt.want {
|
|
t.Fatalf("PathsLookup(%q) got %d results; want %d",
|
|
tt.blobref, len(paths), tt.want)
|
|
}
|
|
if tt.blobref == "with|pipe" {
|
|
p := paths[0]
|
|
want := fmt.Sprintf(
|
|
"Path{Claim: %s, %s; Base: %s + Suffix \"with|pipe\" => Target targ-124}",
|
|
claim2, claim2Time, pn)
|
|
if g := p.String(); g != want {
|
|
t.Errorf("claim wrong.\n got: %s\nwant: %s", g, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// now test deletions
|
|
// Delete an existing value
|
|
claim3 := id.Delete(claim2)
|
|
t.Logf("claim %q deletes path claim %q", claim3, claim2)
|
|
tests = []test{
|
|
{"targ-123", 1},
|
|
{"targ-124", 0},
|
|
{"targ-125", 0},
|
|
}
|
|
for _, tt := range tests {
|
|
signer := id.SignerBlobRef
|
|
paths, err := id.Index.PathsOfSignerTarget(signer, blob.ParseOrZero(tt.blobref))
|
|
if err != nil {
|
|
t.Fatalf("PathsOfSignerTarget(%q): %v", tt.blobref, err)
|
|
}
|
|
if len(paths) != tt.want {
|
|
t.Fatalf("PathsOfSignerTarget(%q) got %d results; want %d",
|
|
tt.blobref, len(paths), tt.want)
|
|
}
|
|
}
|
|
tests = []test{
|
|
{"somedir", 1},
|
|
{"with|pipe", 0},
|
|
{"void", 0},
|
|
}
|
|
for _, tt := range tests {
|
|
paths, err := id.Index.PathsLookup(id.SignerBlobRef, pn, tt.blobref)
|
|
if err != nil {
|
|
t.Fatalf("PathsLookup(%q): %v", tt.blobref, err)
|
|
}
|
|
if len(paths) != tt.want {
|
|
t.Fatalf("PathsLookup(%q) got %d results; want %d",
|
|
tt.blobref, len(paths), tt.want)
|
|
}
|
|
}
|
|
|
|
// recreate second path, and test if the previous deletion of it
|
|
// is indeed ignored.
|
|
claim4 := id.Delete(claim3)
|
|
t.Logf("delete claim %q deletes claim %q, which should undelete %q", claim4, claim3, claim2)
|
|
tests = []test{
|
|
{"targ-123", 1},
|
|
{"targ-124", 1},
|
|
{"targ-125", 0},
|
|
}
|
|
for _, tt := range tests {
|
|
signer := id.SignerBlobRef
|
|
paths, err := id.Index.PathsOfSignerTarget(signer, blob.ParseOrZero(tt.blobref))
|
|
if err != nil {
|
|
t.Fatalf("PathsOfSignerTarget(%q): %v", tt.blobref, err)
|
|
}
|
|
if len(paths) != tt.want {
|
|
t.Fatalf("PathsOfSignerTarget(%q) got %d results; want %d",
|
|
tt.blobref, len(paths), tt.want)
|
|
}
|
|
// and check the modtime too
|
|
if tt.blobref == "targ-124" {
|
|
p := paths[0]
|
|
want := fmt.Sprintf(
|
|
"Path{Claim: %s, %v; Base: %s + Suffix \"with|pipe\" => Target targ-124}",
|
|
claim2, claim2Time, pn)
|
|
if g := p.String(); g != want {
|
|
t.Errorf("claim wrong.\n got: %s\nwant: %s", g, want)
|
|
}
|
|
}
|
|
}
|
|
tests = []test{
|
|
{"somedir", 1},
|
|
{"with|pipe", 1},
|
|
{"void", 0},
|
|
}
|
|
for _, tt := range tests {
|
|
paths, err := id.Index.PathsLookup(id.SignerBlobRef, pn, tt.blobref)
|
|
if err != nil {
|
|
t.Fatalf("PathsLookup(%q): %v", tt.blobref, err)
|
|
}
|
|
if len(paths) != tt.want {
|
|
t.Fatalf("PathsLookup(%q) got %d results; want %d",
|
|
tt.blobref, len(paths), tt.want)
|
|
}
|
|
// and check that modtime is now claim4Time
|
|
if tt.blobref == "with|pipe" {
|
|
p := paths[0]
|
|
want := fmt.Sprintf(
|
|
"Path{Claim: %s, %s; Base: %s + Suffix \"with|pipe\" => Target targ-124}",
|
|
claim2, claim2Time, pn)
|
|
if g := p.String(); g != want {
|
|
t.Errorf("claim wrong.\n got: %s\nwant: %s", g, want)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func Files(t *testing.T, initIdx func() *index.Index) {
|
|
id := NewIndexDeps(initIdx())
|
|
id.Fataler = t
|
|
fileTime := time.Unix(1361250375, 0)
|
|
fileRef, wholeRef := id.UploadFile("foo.html", "<html>I am an html file.</html>", fileTime)
|
|
t.Logf("uploaded fileref %q, wholeRef %q", fileRef, wholeRef)
|
|
id.DumpIndex(t)
|
|
|
|
// ExistingFileSchemas
|
|
{
|
|
key := fmt.Sprintf("wholetofile|%s|%s", wholeRef, fileRef)
|
|
if g, e := id.Get(key), "1"; g != e {
|
|
t.Fatalf("%q = %q, want %q", key, g, e)
|
|
}
|
|
|
|
refs, err := id.Index.ExistingFileSchemas(wholeRef)
|
|
if err != nil {
|
|
t.Fatalf("ExistingFileSchemas = %v", err)
|
|
}
|
|
want := []blob.Ref{fileRef}
|
|
if !reflect.DeepEqual(refs, want) {
|
|
t.Errorf("ExistingFileSchemas got = %#v, want %#v", refs, want)
|
|
}
|
|
}
|
|
|
|
// FileInfo
|
|
{
|
|
key := fmt.Sprintf("fileinfo|%s", fileRef)
|
|
if g, e := id.Get(key), "31|foo.html|text%2Fhtml"; g != e {
|
|
t.Fatalf("%q = %q, want %q", key, g, e)
|
|
}
|
|
|
|
fi, err := id.Index.GetFileInfo(fileRef)
|
|
if err != nil {
|
|
t.Fatalf("GetFileInfo = %v", err)
|
|
}
|
|
if g, e := fi.Size, int64(31); g != e {
|
|
t.Errorf("Size = %d, want %d", g, e)
|
|
}
|
|
if g, e := fi.FileName, "foo.html"; g != e {
|
|
t.Errorf("FileName = %q, want %q", g, e)
|
|
}
|
|
if g, e := fi.MIMEType, "text/html"; g != e {
|
|
t.Errorf("MIMEType = %q, want %q", g, e)
|
|
}
|
|
if g, e := fi.Time, fileTime; !g.Time().Equal(e) {
|
|
t.Errorf("Time = %v; want %v", g, e)
|
|
}
|
|
}
|
|
}
|
|
|
|
func EdgesTo(t *testing.T, initIdx func() *index.Index) {
|
|
idx := initIdx()
|
|
id := NewIndexDeps(idx)
|
|
id.Fataler = t
|
|
defer id.DumpIndex(t)
|
|
|
|
// pn1 ---member---> pn2
|
|
pn1 := id.NewPermanode()
|
|
pn2 := id.NewPermanode()
|
|
claim1 := id.AddAttribute(pn1, "camliMember", pn2.String())
|
|
|
|
t.Logf("edge %s --> %s", pn1, pn2)
|
|
|
|
// Look for pn1
|
|
{
|
|
edges, err := idx.EdgesTo(pn2, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(edges) != 1 {
|
|
t.Fatalf("num edges = %d; want 1", len(edges))
|
|
}
|
|
wantEdge := &camtypes.Edge{
|
|
From: pn1,
|
|
To: pn2,
|
|
FromType: "permanode",
|
|
}
|
|
if got, want := edges[0].String(), wantEdge.String(); got != want {
|
|
t.Errorf("Wrong edge.\n GOT: %v\nWANT: %v", got, want)
|
|
}
|
|
}
|
|
|
|
// Delete claim -> break edge relationship.
|
|
del1 := id.Delete(claim1)
|
|
t.Logf("del claim %q deletes claim %q, breaks link between p1 and p2", del1, claim1)
|
|
// test that we can't find anymore pn1 from pn2
|
|
{
|
|
edges, err := idx.EdgesTo(pn2, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(edges) != 0 {
|
|
t.Fatalf("num edges = %d; want 0", len(edges))
|
|
}
|
|
}
|
|
|
|
// Undelete, should restore the link.
|
|
del2 := id.Delete(del1)
|
|
t.Logf("del claim %q deletes del claim %q, restores link between p1 and p2", del2, del1)
|
|
{
|
|
edges, err := idx.EdgesTo(pn2, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(edges) != 1 {
|
|
t.Fatalf("num edges = %d; want 1", len(edges))
|
|
}
|
|
wantEdge := &camtypes.Edge{
|
|
From: pn1,
|
|
To: pn2,
|
|
FromType: "permanode",
|
|
}
|
|
if got, want := edges[0].String(), wantEdge.String(); got != want {
|
|
t.Errorf("Wrong edge.\n GOT: %v\nWANT: %v", got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func Delete(t *testing.T, initIdx func() *index.Index) {
|
|
idx := initIdx()
|
|
id := NewIndexDeps(idx)
|
|
id.Fataler = t
|
|
defer id.DumpIndex(t)
|
|
pn1 := id.NewPermanode()
|
|
t.Logf("uploaded permanode %q", pn1)
|
|
cl1 := id.SetAttribute(pn1, "tag", "foo1")
|
|
cl1Time := id.LastTime()
|
|
t.Logf("set attribute %q", cl1)
|
|
|
|
// delete pn1
|
|
delpn1 := id.Delete(pn1)
|
|
t.Logf("del claim %q deletes %q", delpn1, pn1)
|
|
deleted := idx.IsDeleted(pn1)
|
|
if !deleted {
|
|
t.Fatal("pn1 should be deleted")
|
|
}
|
|
|
|
// and try to find it with SearchPermanodesWithAttr (which should not work)
|
|
{
|
|
ch := make(chan blob.Ref, 10)
|
|
req := &camtypes.PermanodeByAttrRequest{
|
|
Signer: id.SignerBlobRef,
|
|
Attribute: "tag",
|
|
Query: "foo1"}
|
|
err := id.Index.SearchPermanodesWithAttr(ch, req)
|
|
if err != nil {
|
|
t.Fatalf("SearchPermanodesWithAttr = %v", err)
|
|
}
|
|
var got []blob.Ref
|
|
for r := range ch {
|
|
got = append(got, r)
|
|
}
|
|
want := []blob.Ref{}
|
|
if len(got) != len(want) {
|
|
t.Errorf("id.Index.SearchPermanodesWithAttr gives %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
// delete pn1 again with another claim
|
|
delpn1bis := id.Delete(pn1)
|
|
t.Logf("del claim %q deletes %q a second time", delpn1bis, pn1)
|
|
deleted = idx.IsDeleted(pn1)
|
|
if !deleted {
|
|
t.Fatal("pn1 should be deleted")
|
|
}
|
|
|
|
// verify that deleting delpn1 is not enough to make pn1 undeleted
|
|
del2 := id.Delete(delpn1)
|
|
t.Logf("delete claim %q deletes %q, which should not yet revive %q", del2, delpn1, pn1)
|
|
deleted = idx.IsDeleted(pn1)
|
|
if !deleted {
|
|
t.Fatal("pn1 should not yet be undeleted")
|
|
}
|
|
// we should not yet be able to find it again with SearchPermanodesWithAttr
|
|
{
|
|
ch := make(chan blob.Ref, 10)
|
|
req := &camtypes.PermanodeByAttrRequest{
|
|
Signer: id.SignerBlobRef,
|
|
Attribute: "tag",
|
|
Query: "foo1"}
|
|
err := id.Index.SearchPermanodesWithAttr(ch, req)
|
|
if err != nil {
|
|
t.Fatalf("SearchPermanodesWithAttr = %v", err)
|
|
}
|
|
var got []blob.Ref
|
|
for r := range ch {
|
|
got = append(got, r)
|
|
}
|
|
want := []blob.Ref{}
|
|
if len(got) != len(want) {
|
|
t.Errorf("id.Index.SearchPermanodesWithAttr gives %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
// delete delpn1bis as well -> should undelete pn1
|
|
del2bis := id.Delete(delpn1bis)
|
|
t.Logf("delete claim %q deletes %q, which should revive %q", del2bis, delpn1bis, pn1)
|
|
deleted = idx.IsDeleted(pn1)
|
|
if deleted {
|
|
t.Fatal("pn1 should be undeleted")
|
|
}
|
|
// we should now be able to find it again with SearchPermanodesWithAttr
|
|
{
|
|
ch := make(chan blob.Ref, 10)
|
|
req := &camtypes.PermanodeByAttrRequest{
|
|
Signer: id.SignerBlobRef,
|
|
Attribute: "tag",
|
|
Query: "foo1"}
|
|
err := id.Index.SearchPermanodesWithAttr(ch, req)
|
|
if err != nil {
|
|
t.Fatalf("SearchPermanodesWithAttr = %v", err)
|
|
}
|
|
var got []blob.Ref
|
|
for r := range ch {
|
|
got = append(got, r)
|
|
}
|
|
want := []blob.Ref{pn1}
|
|
if len(got) < 1 || got[0].String() != want[0].String() {
|
|
t.Errorf("id.Index.SearchPermanodesWithAttr gives %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
// Delete cl1
|
|
del3 := id.Delete(cl1)
|
|
t.Logf("del claim %q deletes claim %q", del3, cl1)
|
|
deleted = idx.IsDeleted(cl1)
|
|
if !deleted {
|
|
t.Fatal("cl1 should be deleted")
|
|
}
|
|
// we should not find anything with SearchPermanodesWithAttr
|
|
{
|
|
ch := make(chan blob.Ref, 10)
|
|
req := &camtypes.PermanodeByAttrRequest{
|
|
Signer: id.SignerBlobRef,
|
|
Attribute: "tag",
|
|
Query: "foo1"}
|
|
err := id.Index.SearchPermanodesWithAttr(ch, req)
|
|
if err != nil {
|
|
t.Fatalf("SearchPermanodesWithAttr = %v", err)
|
|
}
|
|
var got []blob.Ref
|
|
for r := range ch {
|
|
got = append(got, r)
|
|
}
|
|
want := []blob.Ref{}
|
|
if len(got) != len(want) {
|
|
t.Errorf("id.Index.SearchPermanodesWithAttr gives %q, want %q", got, want)
|
|
}
|
|
}
|
|
// and now check that AppendClaims finds nothing for pn
|
|
{
|
|
claims, err := id.Index.AppendClaims(nil, pn1, id.SignerBlobRef, "")
|
|
if err != nil {
|
|
t.Errorf("AppendClaims = %v", err)
|
|
} else {
|
|
want := []camtypes.Claim{}
|
|
if len(claims) != len(want) {
|
|
t.Errorf("id.Index.AppendClaims gives %q, want %q", claims, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// undelete cl1
|
|
del4 := id.Delete(del3)
|
|
t.Logf("del claim %q deletes del claim %q, which should undelete %q", del4, del3, cl1)
|
|
// We should now be able to find it again with both methods
|
|
{
|
|
ch := make(chan blob.Ref, 10)
|
|
req := &camtypes.PermanodeByAttrRequest{
|
|
Signer: id.SignerBlobRef,
|
|
Attribute: "tag",
|
|
Query: "foo1"}
|
|
err := id.Index.SearchPermanodesWithAttr(ch, req)
|
|
if err != nil {
|
|
t.Fatalf("SearchPermanodesWithAttr = %v", err)
|
|
}
|
|
var got []blob.Ref
|
|
for r := range ch {
|
|
got = append(got, r)
|
|
}
|
|
want := []blob.Ref{pn1}
|
|
if len(got) < 1 || got[0].String() != want[0].String() {
|
|
t.Errorf("id.Index.SearchPermanodesWithAttr gives %q, want %q", got, want)
|
|
}
|
|
}
|
|
// and check that AppendClaims finds cl1, with the right modtime too
|
|
{
|
|
claims, err := id.Index.AppendClaims(nil, pn1, id.SignerBlobRef, "")
|
|
if err != nil {
|
|
t.Errorf("AppendClaims = %v", err)
|
|
} else {
|
|
want := []camtypes.Claim{
|
|
camtypes.Claim{
|
|
BlobRef: cl1,
|
|
Permanode: pn1,
|
|
Signer: id.SignerBlobRef,
|
|
Date: cl1Time.UTC(),
|
|
Type: "set-attribute",
|
|
Attr: "tag",
|
|
Value: "foo1",
|
|
},
|
|
}
|
|
if !reflect.DeepEqual(claims, want) {
|
|
t.Errorf("GetOwnerClaims results differ.\n got: %v\nwant: %v",
|
|
claims, want)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
type searchResults []camtypes.RecentPermanode
|
|
|
|
func (s searchResults) String() string {
|
|
var buf bytes.Buffer
|
|
fmt.Fprintf(&buf, "[%d search results: ", len(s))
|
|
for _, r := range s {
|
|
fmt.Fprintf(&buf, "{BlobRef: %s, Signer: %s, LastModTime: %d}",
|
|
r.Permanode, r.Signer, r.LastModTime.Unix())
|
|
}
|
|
buf.WriteString("]")
|
|
return buf.String()
|
|
}
|