github.com/slspeek/camlistore_namedsearch@v0.0.0-20140519202248-ed6f70f7721a/pkg/index/indextest/tests.go (about)

     1  /*
     2  Copyright 2011 Google Inc.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8       http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // Package indextest contains the unit tests for the indexer so they
    18  // can be re-used for each specific implementation of the index
    19  // Storage interface.
    20  package indextest
    21  
    22  import (
    23  	"bytes"
    24  	"fmt"
    25  	"io/ioutil"
    26  	"log"
    27  	"net/url"
    28  	"os"
    29  	"path/filepath"
    30  	"reflect"
    31  	"testing"
    32  	"time"
    33  
    34  	"camlistore.org/pkg/blob"
    35  	"camlistore.org/pkg/index"
    36  	"camlistore.org/pkg/jsonsign"
    37  	"camlistore.org/pkg/osutil"
    38  	"camlistore.org/pkg/schema"
    39  	"camlistore.org/pkg/test"
    40  	"camlistore.org/pkg/types/camtypes"
    41  )
    42  
    43  // An IndexDeps is a helper for populating and querying an Index for tests.
    44  type IndexDeps struct {
    45  	Index *index.Index
    46  
    47  	BlobSource *test.Fetcher
    48  
    49  	// Following three needed for signing:
    50  	PublicKeyFetcher *test.Fetcher
    51  	EntityFetcher    jsonsign.EntityFetcher // fetching decrypted openpgp entities
    52  	SignerBlobRef    blob.Ref
    53  
    54  	now time.Time // fake clock, nanos since epoch
    55  
    56  	Fataler // optional means of failing.
    57  }
    58  
    59  type Fataler interface {
    60  	Fatalf(format string, args ...interface{})
    61  }
    62  
    63  type logFataler struct{}
    64  
    65  func (logFataler) Fatalf(format string, args ...interface{}) {
    66  	log.Fatalf(format, args...)
    67  }
    68  
    69  func (id *IndexDeps) Get(key string) string {
    70  	v, _ := id.Index.Storage().Get(key)
    71  	return v
    72  }
    73  
    74  func (id *IndexDeps) Set(key, value string) error {
    75  	return id.Index.Storage().Set(key, value)
    76  }
    77  
    78  func (id *IndexDeps) DumpIndex(t *testing.T) {
    79  	t.Logf("Begin index dump:")
    80  	it := id.Index.Storage().Find("", "")
    81  	for it.Next() {
    82  		t.Logf("  %q = %q", it.Key(), it.Value())
    83  	}
    84  	if err := it.Close(); err != nil {
    85  		t.Fatalf("iterator close = %v", err)
    86  	}
    87  	t.Logf("End index dump.")
    88  }
    89  
    90  func (id *IndexDeps) uploadAndSign(m *schema.Builder) blob.Ref {
    91  	m.SetSigner(id.SignerBlobRef)
    92  	unsigned, err := m.JSON()
    93  	if err != nil {
    94  		id.Fatalf("uploadAndSignMap: " + err.Error())
    95  	}
    96  	sr := &jsonsign.SignRequest{
    97  		UnsignedJSON:  unsigned,
    98  		Fetcher:       id.PublicKeyFetcher,
    99  		EntityFetcher: id.EntityFetcher,
   100  		SignatureTime: id.now,
   101  	}
   102  	signed, err := sr.Sign()
   103  	if err != nil {
   104  		id.Fatalf("problem signing: " + err.Error())
   105  	}
   106  	tb := &test.Blob{Contents: signed}
   107  	_, err = id.BlobSource.ReceiveBlob(tb.BlobRef(), tb.Reader())
   108  	if err != nil {
   109  		id.Fatalf("public uploading signed blob to blob source, pre-indexing: %v, %v", tb.BlobRef(), err)
   110  	}
   111  	_, err = id.Index.ReceiveBlob(tb.BlobRef(), tb.Reader())
   112  	if err != nil {
   113  		id.Fatalf("problem indexing blob: %v\nblob was:\n%s", err, signed)
   114  	}
   115  	return tb.BlobRef()
   116  }
   117  
   118  // NewPermanode creates (& signs) a new permanode and adds it
   119  // to the index, returning its blobref.
   120  func (id *IndexDeps) NewPermanode() blob.Ref {
   121  	unsigned := schema.NewUnsignedPermanode()
   122  	return id.uploadAndSign(unsigned)
   123  }
   124  
   125  // NewPermanode creates (& signs) a new planned permanode and adds it
   126  // to the index, returning its blobref.
   127  func (id *IndexDeps) NewPlannedPermanode(key string) blob.Ref {
   128  	unsigned := schema.NewPlannedPermanode(key)
   129  	return id.uploadAndSign(unsigned)
   130  }
   131  
   132  func (id *IndexDeps) advanceTime() time.Time {
   133  	id.now = id.now.Add(1 * time.Second)
   134  	return id.now
   135  }
   136  
   137  // LastTime returns the time of the most recent mutation (claim).
   138  func (id *IndexDeps) LastTime() time.Time {
   139  	return id.now
   140  }
   141  
   142  func (id *IndexDeps) SetAttribute(permaNode blob.Ref, attr, value string) blob.Ref {
   143  	m := schema.NewSetAttributeClaim(permaNode, attr, value)
   144  	m.SetClaimDate(id.advanceTime())
   145  	return id.uploadAndSign(m)
   146  }
   147  
   148  func (id *IndexDeps) SetAttribute_NoTimeMove(permaNode blob.Ref, attr, value string) blob.Ref {
   149  	m := schema.NewSetAttributeClaim(permaNode, attr, value)
   150  	m.SetClaimDate(id.LastTime())
   151  	return id.uploadAndSign(m)
   152  }
   153  
   154  func (id *IndexDeps) AddAttribute(permaNode blob.Ref, attr, value string) blob.Ref {
   155  	m := schema.NewAddAttributeClaim(permaNode, attr, value)
   156  	m.SetClaimDate(id.advanceTime())
   157  	return id.uploadAndSign(m)
   158  }
   159  
   160  func (id *IndexDeps) DelAttribute(permaNode blob.Ref, attr, value string) blob.Ref {
   161  	m := schema.NewDelAttributeClaim(permaNode, attr, value)
   162  	m.SetClaimDate(id.advanceTime())
   163  	return id.uploadAndSign(m)
   164  }
   165  
   166  func (id *IndexDeps) Delete(target blob.Ref) blob.Ref {
   167  	m := schema.NewDeleteClaim(target)
   168  	m.SetClaimDate(id.advanceTime())
   169  	return id.uploadAndSign(m)
   170  }
   171  
   172  var noTime = time.Time{}
   173  
   174  func (id *IndexDeps) UploadString(v string) blob.Ref {
   175  	cb := &test.Blob{Contents: v}
   176  	id.BlobSource.AddBlob(cb)
   177  	br := cb.BlobRef()
   178  	_, err := id.Index.ReceiveBlob(br, cb.Reader())
   179  	if err != nil {
   180  		id.Fatalf("UploadString: %v", err)
   181  	}
   182  	return br
   183  }
   184  
   185  // If modTime is zero, it's not used.
   186  func (id *IndexDeps) UploadFile(fileName string, contents string, modTime time.Time) (fileRef, wholeRef blob.Ref) {
   187  	wholeRef = id.UploadString(contents)
   188  
   189  	m := schema.NewFileMap(fileName)
   190  	m.PopulateParts(int64(len(contents)), []schema.BytesPart{
   191  		schema.BytesPart{
   192  			Size:    uint64(len(contents)),
   193  			BlobRef: wholeRef,
   194  		}})
   195  	if !modTime.IsZero() {
   196  		m.SetModTime(modTime)
   197  	}
   198  	fjson, err := m.JSON()
   199  	if err != nil {
   200  		id.Fatalf("UploadFile.JSON: %v", err)
   201  	}
   202  	fb := &test.Blob{Contents: fjson}
   203  	id.BlobSource.AddBlob(fb)
   204  	fileRef = fb.BlobRef()
   205  	_, err = id.Index.ReceiveBlob(fileRef, fb.Reader())
   206  	if err != nil {
   207  		panic(err)
   208  	}
   209  	return
   210  }
   211  
   212  // If modTime is zero, it's not used.
   213  func (id *IndexDeps) UploadDir(dirName string, children []blob.Ref, modTime time.Time) blob.Ref {
   214  	// static-set entries blob
   215  	ss := new(schema.StaticSet)
   216  	for _, child := range children {
   217  		ss.Add(child)
   218  	}
   219  	ssjson := ss.Blob().JSON()
   220  	ssb := &test.Blob{Contents: ssjson}
   221  	id.BlobSource.AddBlob(ssb)
   222  	_, err := id.Index.ReceiveBlob(ssb.BlobRef(), ssb.Reader())
   223  	if err != nil {
   224  		id.Fatalf("UploadDir.ReceiveBlob: %v", err)
   225  	}
   226  
   227  	// directory blob
   228  	bb := schema.NewDirMap(dirName)
   229  	bb.PopulateDirectoryMap(ssb.BlobRef())
   230  	if !modTime.IsZero() {
   231  		bb.SetModTime(modTime)
   232  	}
   233  	dirjson, err := bb.JSON()
   234  	if err != nil {
   235  		id.Fatalf("UploadDir.JSON: %v", err)
   236  	}
   237  	dirb := &test.Blob{Contents: dirjson}
   238  	id.BlobSource.AddBlob(dirb)
   239  	_, err = id.Index.ReceiveBlob(dirb.BlobRef(), dirb.Reader())
   240  	if err != nil {
   241  		id.Fatalf("UploadDir.ReceiveBlob: %v", err)
   242  	}
   243  	return dirb.BlobRef()
   244  }
   245  
   246  // NewIndexDeps returns an IndexDeps helper for populating and working
   247  // with the provided index for tests.
   248  func NewIndexDeps(index *index.Index) *IndexDeps {
   249  	camliRootPath, err := osutil.GoPackagePath("camlistore.org")
   250  	if err != nil {
   251  		log.Fatal("Package camlistore.org no found in $GOPATH or $GOPATH not defined")
   252  	}
   253  	secretRingFile := filepath.Join(camliRootPath, "pkg", "jsonsign", "testdata", "test-secring.gpg")
   254  	pubKey := &test.Blob{Contents: `-----BEGIN PGP PUBLIC KEY BLOCK-----
   255  
   256  xsBNBEzgoVsBCAC/56aEJ9BNIGV9FVP+WzenTAkg12k86YqlwJVAB/VwdMlyXxvi
   257  bCT1RVRfnYxscs14LLfcMWF3zMucw16mLlJCBSLvbZ0jn4h+/8vK5WuAdjw2YzLs
   258  WtBcjWn3lV6tb4RJz5gtD/o1w8VWxwAnAVIWZntKAWmkcChCRgdUeWso76+plxE5
   259  aRYBJqdT1mctGqNEISd/WYPMgwnWXQsVi3x4z1dYu2tD9uO1dkAff12z1kyZQIBQ
   260  rexKYRRRh9IKAayD4kgS0wdlULjBU98aeEaMz1ckuB46DX3lAYqmmTEL/Rl9cOI0
   261  Enpn/oOOfYFa5h0AFndZd1blMvruXfdAobjVABEBAAE=
   262  =28/7
   263  -----END PGP PUBLIC KEY BLOCK-----`}
   264  
   265  	id := &IndexDeps{
   266  		Index:            index,
   267  		BlobSource:       new(test.Fetcher),
   268  		PublicKeyFetcher: new(test.Fetcher),
   269  		EntityFetcher: &jsonsign.CachingEntityFetcher{
   270  			Fetcher: &jsonsign.FileEntityFetcher{File: secretRingFile},
   271  		},
   272  		SignerBlobRef: pubKey.BlobRef(),
   273  		now:           test.ClockOrigin,
   274  		Fataler:       logFataler{},
   275  	}
   276  	// Add dev client test key public key, keyid 26F5ABDA,
   277  	// blobref sha1-ad87ca5c78bd0ce1195c46f7c98e6025abbaf007
   278  	if g, w := id.SignerBlobRef.String(), "sha1-ad87ca5c78bd0ce1195c46f7c98e6025abbaf007"; g != w {
   279  		id.Fatalf("unexpected signer blobref; got signer = %q; want %q", g, w)
   280  	}
   281  	id.PublicKeyFetcher.AddBlob(pubKey)
   282  	id.Index.KeyFetcher = id.PublicKeyFetcher
   283  	id.Index.BlobSource = id.BlobSource
   284  	return id
   285  }
   286  
   287  func Index(t *testing.T, initIdx func() *index.Index) {
   288  	id := NewIndexDeps(initIdx())
   289  	id.Fataler = t
   290  	defer id.DumpIndex(t)
   291  	pn := id.NewPermanode()
   292  	t.Logf("uploaded permanode %q", pn)
   293  	br1 := id.SetAttribute(pn, "tag", "foo1")
   294  	br1Time := id.LastTime()
   295  	t.Logf("set attribute %q", br1)
   296  	br2 := id.SetAttribute(pn, "tag", "foo2")
   297  	br2Time := id.LastTime()
   298  	t.Logf("set attribute %q", br2)
   299  	rootClaim := id.SetAttribute(pn, "camliRoot", "rootval")
   300  	rootClaimTime := id.LastTime()
   301  	t.Logf("set attribute %q", rootClaim)
   302  
   303  	pnChild := id.NewPermanode()
   304  	br3 := id.SetAttribute(pnChild, "tag", "bar")
   305  	br3Time := id.LastTime()
   306  	t.Logf("set attribute %q", br3)
   307  	memberRef := id.AddAttribute(pn, "camliMember", pnChild.String())
   308  	t.Logf("add-attribute claim %q points to member permanode %q", memberRef, pnChild)
   309  	memberRefTime := id.LastTime()
   310  
   311  	// TODO(bradfitz): add EXIF tests here, once that stuff is ready.
   312  	if false {
   313  		camliRootPath, err := osutil.GoPackagePath("camlistore.org")
   314  		if err != nil {
   315  			t.Fatal("Package camlistore.org no found in $GOPATH or $GOPATH not defined")
   316  		}
   317  		for i := 1; i <= 8; i++ {
   318  			fileBase := fmt.Sprintf("f%d-exif.jpg", i)
   319  			fileName := filepath.Join(camliRootPath, "pkg", "images", "testdata", fileBase)
   320  			contents, err := ioutil.ReadFile(fileName)
   321  			if err != nil {
   322  				t.Fatal(err)
   323  			}
   324  			id.UploadFile(fileBase, string(contents), noTime)
   325  		}
   326  	}
   327  
   328  	// Upload some files.
   329  	var jpegFileRef, exifFileRef, mediaFileRef, mediaWholeRef blob.Ref
   330  	{
   331  		camliRootPath, err := osutil.GoPackagePath("camlistore.org")
   332  		if err != nil {
   333  			t.Fatal("Package camlistore.org no found in $GOPATH or $GOPATH not defined")
   334  		}
   335  		uploadFile := func(file string, modTime time.Time) (fileRef, wholeRef blob.Ref) {
   336  			fileName := filepath.Join(camliRootPath, "pkg", "index", "indextest", "testdata", file)
   337  			contents, err := ioutil.ReadFile(fileName)
   338  			if err != nil {
   339  				t.Fatal(err)
   340  			}
   341  			fileRef, wholeRef = id.UploadFile(file, string(contents), modTime)
   342  			return
   343  		}
   344  		jpegFileRef, _ = uploadFile("dude.jpg", noTime)
   345  		exifFileRef, _ = uploadFile("dude-exif.jpg", time.Unix(1361248796, 0))
   346  		mediaFileRef, mediaWholeRef = uploadFile("0s.mp3", noTime)
   347  	}
   348  
   349  	// Upload the dir containing the previous files.
   350  	imagesDirRef := id.UploadDir(
   351  		"testdata",
   352  		[]blob.Ref{jpegFileRef, exifFileRef, mediaFileRef},
   353  		time.Now(),
   354  	)
   355  
   356  	lastPermanodeMutation := id.LastTime()
   357  
   358  	key := "signerkeyid:sha1-ad87ca5c78bd0ce1195c46f7c98e6025abbaf007"
   359  	if g, e := id.Get(key), "2931A67C26F5ABDA"; g != e {
   360  		t.Fatalf("%q = %q, want %q", key, g, e)
   361  	}
   362  
   363  	key = "imagesize|" + jpegFileRef.String()
   364  	if g, e := id.Get(key), "50|100"; g != e {
   365  		t.Errorf("JPEG dude.jpg key %q = %q; want %q", key, g, e)
   366  	}
   367  	key = "filetimes|" + jpegFileRef.String()
   368  	if g, e := id.Get(key), ""; g != e {
   369  		t.Errorf("JPEG dude.jpg key %q = %q; want %q", key, g, e)
   370  	}
   371  
   372  	key = "filetimes|" + exifFileRef.String()
   373  	if g, e := id.Get(key), "2013-02-18T01%3A11%3A20Z%2C2013-02-19T04%3A39%3A56Z"; g != e {
   374  		t.Errorf("EXIF dude-exif.jpg key %q = %q; want %q", key, g, e)
   375  	}
   376  
   377  	key = "have:" + pn.String()
   378  	pnSizeStr := id.Get(key)
   379  	if pnSizeStr == "" {
   380  		t.Fatalf("missing key %q", key)
   381  	}
   382  
   383  	key = "meta:" + pn.String()
   384  	if g, e := id.Get(key), pnSizeStr+"|application/json; camliType=permanode"; g != e {
   385  		t.Errorf("key %q = %q, want %q", key, g, e)
   386  	}
   387  
   388  	key = "recpn|2931A67C26F5ABDA|rt7988-88-71T98:67:62.999876543Z|" + br1.String()
   389  	if g, e := id.Get(key), pn.String(); g != e {
   390  		t.Fatalf("%q = %q, want %q (permanode)", key, g, e)
   391  	}
   392  
   393  	key = "recpn|2931A67C26F5ABDA|rt7988-88-71T98:67:61.999876543Z|" + br2.String()
   394  	if g, e := id.Get(key), pn.String(); g != e {
   395  		t.Fatalf("%q = %q, want %q (permanode)", key, g, e)
   396  	}
   397  
   398  	key = fmt.Sprintf("edgeback|%s|%s|%s", pnChild, pn, memberRef)
   399  	if g, e := id.Get(key), "permanode|"; g != e {
   400  		t.Fatalf("edgeback row %q = %q, want %q", key, g, e)
   401  	}
   402  
   403  	mediaTests := []struct {
   404  		prop, exp string
   405  	}{
   406  		{"title", "Zero Seconds"},
   407  		{"artist", "Test Artist"},
   408  		{"album", "Test Album"},
   409  		{"genre", "(20)Alternative"},
   410  		{"musicbrainzalbumid", "00000000-0000-0000-0000-000000000000"},
   411  		{"year", "1992"},
   412  		{"track", "1"},
   413  		{"disc", "2"},
   414  		{"mediaref", "sha1-fefac74a1d5928316d7131747107c8a61b71ffe4"},
   415  		{"durationms", "26"},
   416  	}
   417  	for _, tt := range mediaTests {
   418  		key = fmt.Sprintf("mediatag|%s|%s", mediaWholeRef.String(), tt.prop)
   419  		if g, _ := url.QueryUnescape(id.Get(key)); g != tt.exp {
   420  			t.Errorf("0s.mp3 key %q = %q; want %q", key, g, tt.exp)
   421  		}
   422  	}
   423  
   424  	// PermanodeOfSignerAttrValue
   425  	{
   426  		gotPN, err := id.Index.PermanodeOfSignerAttrValue(id.SignerBlobRef, "camliRoot", "rootval")
   427  		if err != nil {
   428  			t.Fatalf("id.Index.PermanodeOfSignerAttrValue = %v", err)
   429  		}
   430  		if gotPN.String() != pn.String() {
   431  			t.Errorf("id.Index.PermanodeOfSignerAttrValue = %q, want %q", gotPN, pn)
   432  		}
   433  		_, err = id.Index.PermanodeOfSignerAttrValue(id.SignerBlobRef, "camliRoot", "MISSING")
   434  		if err == nil {
   435  			t.Errorf("expected an error from PermanodeOfSignerAttrValue on missing value")
   436  		}
   437  	}
   438  
   439  	// SearchPermanodesWithAttr - match attr type "tag" and value "foo1"
   440  	{
   441  		ch := make(chan blob.Ref, 10)
   442  		req := &camtypes.PermanodeByAttrRequest{
   443  			Signer:    id.SignerBlobRef,
   444  			Attribute: "tag",
   445  			Query:     "foo1",
   446  		}
   447  		err := id.Index.SearchPermanodesWithAttr(ch, req)
   448  		if err != nil {
   449  			t.Fatalf("SearchPermanodesWithAttr = %v", err)
   450  		}
   451  		var got []blob.Ref
   452  		for r := range ch {
   453  			got = append(got, r)
   454  		}
   455  		want := []blob.Ref{pn}
   456  		if len(got) < 1 || got[0].String() != want[0].String() {
   457  			t.Errorf("id.Index.SearchPermanodesWithAttr gives %q, want %q", got, want)
   458  		}
   459  	}
   460  
   461  	// SearchPermanodesWithAttr - match all with attr type "tag"
   462  	{
   463  		ch := make(chan blob.Ref, 10)
   464  		req := &camtypes.PermanodeByAttrRequest{
   465  			Signer:    id.SignerBlobRef,
   466  			Attribute: "tag",
   467  		}
   468  		err := id.Index.SearchPermanodesWithAttr(ch, req)
   469  		if err != nil {
   470  			t.Fatalf("SearchPermanodesWithAttr = %v", err)
   471  		}
   472  		var got []blob.Ref
   473  		for r := range ch {
   474  			got = append(got, r)
   475  		}
   476  		want := []blob.Ref{pn, pnChild}
   477  		if len(got) != len(want) {
   478  			t.Errorf("SearchPermanodesWithAttr results differ.\n got: %q\nwant: %q",
   479  				got, want)
   480  		}
   481  		for _, w := range want {
   482  			found := false
   483  			for _, g := range got {
   484  				if g.String() == w.String() {
   485  					found = true
   486  					break
   487  				}
   488  			}
   489  			if !found {
   490  				t.Errorf("SearchPermanodesWithAttr: %v was not found.\n", w)
   491  			}
   492  		}
   493  	}
   494  
   495  	// Delete value "pony" of type "title" (which does not actually exist) for pn
   496  	br4 := id.DelAttribute(pn, "title", "pony")
   497  	br4Time := id.LastTime()
   498  	// and verify it is not found when searching by attr
   499  	{
   500  		ch := make(chan blob.Ref, 10)
   501  		req := &camtypes.PermanodeByAttrRequest{
   502  			Signer:    id.SignerBlobRef,
   503  			Attribute: "title",
   504  			Query:     "pony",
   505  		}
   506  		err := id.Index.SearchPermanodesWithAttr(ch, req)
   507  		if err != nil {
   508  			t.Fatalf("SearchPermanodesWithAttr = %v", err)
   509  		}
   510  		var got []blob.Ref
   511  		for r := range ch {
   512  			got = append(got, r)
   513  		}
   514  		want := []blob.Ref{}
   515  		if len(got) != len(want) {
   516  			t.Errorf("SearchPermanodesWithAttr results differ.\n got: %q\nwant: %q",
   517  				got, want)
   518  		}
   519  	}
   520  
   521  	// GetRecentPermanodes
   522  	{
   523  		verify := func(prefix string, want []camtypes.RecentPermanode, before time.Time) {
   524  			ch := make(chan camtypes.RecentPermanode, 10) // expect 2 results, but maybe more if buggy.
   525  			err := id.Index.GetRecentPermanodes(ch, id.SignerBlobRef, 50, before)
   526  			if err != nil {
   527  				t.Fatalf("[%s] GetRecentPermanodes = %v", prefix, err)
   528  			}
   529  			got := []camtypes.RecentPermanode{}
   530  			for r := range ch {
   531  				got = append(got, r)
   532  			}
   533  			if len(got) != len(want) {
   534  				t.Errorf("[%s] GetRecentPermanode results differ.\n got: %v\nwant: %v",
   535  					prefix, searchResults(got), searchResults(want))
   536  			}
   537  			for _, w := range want {
   538  				found := false
   539  				for _, g := range got {
   540  					if g.Equal(w) {
   541  						found = true
   542  						break
   543  					}
   544  				}
   545  				if !found {
   546  					t.Errorf("[%s] GetRecentPermanode: %v was not found.\n got: %v\nwant: %v",
   547  						prefix, w, searchResults(got), searchResults(want))
   548  				}
   549  			}
   550  		}
   551  
   552  		want := []camtypes.RecentPermanode{
   553  			{
   554  				Permanode:   pn,
   555  				Signer:      id.SignerBlobRef,
   556  				LastModTime: br4Time,
   557  			},
   558  			{
   559  				Permanode:   pnChild,
   560  				Signer:      id.SignerBlobRef,
   561  				LastModTime: br3Time,
   562  			},
   563  		}
   564  
   565  		before := time.Time{}
   566  		verify("Zero before", want, before)
   567  
   568  		before = lastPermanodeMutation
   569  		t.Log("lastPermanodeMutation", lastPermanodeMutation,
   570  			lastPermanodeMutation.Unix())
   571  		verify("Non-zero before", want[1:], before)
   572  	}
   573  	// GetDirMembers
   574  	{
   575  		ch := make(chan blob.Ref, 10) // expect 2 results
   576  		err := id.Index.GetDirMembers(imagesDirRef, ch, 50)
   577  		if err != nil {
   578  			t.Fatalf("GetDirMembers = %v", err)
   579  		}
   580  		got := []blob.Ref{}
   581  		for r := range ch {
   582  			got = append(got, r)
   583  		}
   584  		want := []blob.Ref{jpegFileRef, exifFileRef, mediaFileRef}
   585  		if len(got) != len(want) {
   586  			t.Errorf("GetDirMembers results differ.\n got: %v\nwant: %v",
   587  				got, want)
   588  		}
   589  		for _, w := range want {
   590  			found := false
   591  			for _, g := range got {
   592  				if w == g {
   593  					found = true
   594  					break
   595  				}
   596  			}
   597  			if !found {
   598  				t.Errorf("GetDirMembers: %v was not found.", w)
   599  			}
   600  		}
   601  	}
   602  
   603  	// GetBlobMeta
   604  	{
   605  		meta, err := id.Index.GetBlobMeta(pn)
   606  		if err != nil {
   607  			t.Errorf("GetBlobMeta(%q) = %v", pn, err)
   608  		} else {
   609  			if e := "permanode"; meta.CamliType != e {
   610  				t.Errorf("GetBlobMeta(%q) mime = %q, want %q", pn, meta.CamliType, e)
   611  			}
   612  			if meta.Size == 0 {
   613  				t.Errorf("GetBlobMeta(%q) size is zero", pn)
   614  			}
   615  		}
   616  		_, err = id.Index.GetBlobMeta(blob.ParseOrZero("abc-123"))
   617  		if err != os.ErrNotExist {
   618  			t.Errorf("GetBlobMeta(dummy blobref) = %v; want os.ErrNotExist", err)
   619  		}
   620  	}
   621  
   622  	// AppendClaims
   623  	{
   624  		claims, err := id.Index.AppendClaims(nil, pn, id.SignerBlobRef, "")
   625  		if err != nil {
   626  			t.Errorf("AppendClaims = %v", err)
   627  		} else {
   628  			want := []camtypes.Claim{
   629  				{
   630  					BlobRef:   br1,
   631  					Permanode: pn,
   632  					Signer:    id.SignerBlobRef,
   633  					Date:      br1Time.UTC(),
   634  					Type:      "set-attribute",
   635  					Attr:      "tag",
   636  					Value:     "foo1",
   637  				},
   638  				{
   639  					BlobRef:   br2,
   640  					Permanode: pn,
   641  					Signer:    id.SignerBlobRef,
   642  					Date:      br2Time.UTC(),
   643  					Type:      "set-attribute",
   644  					Attr:      "tag",
   645  					Value:     "foo2",
   646  				},
   647  				{
   648  					BlobRef:   rootClaim,
   649  					Permanode: pn,
   650  					Signer:    id.SignerBlobRef,
   651  					Date:      rootClaimTime.UTC(),
   652  					Type:      "set-attribute",
   653  					Attr:      "camliRoot",
   654  					Value:     "rootval",
   655  				},
   656  				{
   657  					BlobRef:   memberRef,
   658  					Permanode: pn,
   659  					Signer:    id.SignerBlobRef,
   660  					Date:      memberRefTime.UTC(),
   661  					Type:      "add-attribute",
   662  					Attr:      "camliMember",
   663  					Value:     pnChild.String(),
   664  				},
   665  				{
   666  					BlobRef:   br4,
   667  					Permanode: pn,
   668  					Signer:    id.SignerBlobRef,
   669  					Date:      br4Time.UTC(),
   670  					Type:      "del-attribute",
   671  					Attr:      "title",
   672  					Value:     "pony",
   673  				},
   674  			}
   675  			if !reflect.DeepEqual(claims, want) {
   676  				t.Errorf("AppendClaims results differ.\n got: %v\nwant: %v",
   677  					claims, want)
   678  			}
   679  		}
   680  	}
   681  }
   682  
   683  func PathsOfSignerTarget(t *testing.T, initIdx func() *index.Index) {
   684  	id := NewIndexDeps(initIdx())
   685  	id.Fataler = t
   686  	defer id.DumpIndex(t)
   687  	signer := id.SignerBlobRef
   688  	pn := id.NewPermanode()
   689  	t.Logf("uploaded permanode %q", pn)
   690  
   691  	claim1 := id.SetAttribute(pn, "camliPath:somedir", "targ-123")
   692  	claim1Time := id.LastTime().UTC()
   693  	claim2 := id.SetAttribute(pn, "camliPath:with|pipe", "targ-124")
   694  	claim2Time := id.LastTime().UTC()
   695  	t.Logf("made path claims %q and %q", claim1, claim2)
   696  
   697  	type test struct {
   698  		blobref string
   699  		want    int
   700  	}
   701  	tests := []test{
   702  		{"targ-123", 1},
   703  		{"targ-124", 1},
   704  		{"targ-125", 0},
   705  	}
   706  	for _, tt := range tests {
   707  		paths, err := id.Index.PathsOfSignerTarget(signer, blob.ParseOrZero(tt.blobref))
   708  		if err != nil {
   709  			t.Fatalf("PathsOfSignerTarget(%q): %v", tt.blobref, err)
   710  		}
   711  		if len(paths) != tt.want {
   712  			t.Fatalf("PathsOfSignerTarget(%q) got %d results; want %d",
   713  				tt.blobref, len(paths), tt.want)
   714  		}
   715  		if tt.blobref == "targ-123" {
   716  			p := paths[0]
   717  			want := fmt.Sprintf(
   718  				"Path{Claim: %s, %v; Base: %s + Suffix \"somedir\" => Target targ-123}",
   719  				claim1, claim1Time, pn)
   720  			if g := p.String(); g != want {
   721  				t.Errorf("claim wrong.\n got: %s\nwant: %s", g, want)
   722  			}
   723  		}
   724  	}
   725  	tests = []test{
   726  		{"somedir", 1},
   727  		{"with|pipe", 1},
   728  		{"void", 0},
   729  	}
   730  	for _, tt := range tests {
   731  		paths, err := id.Index.PathsLookup(id.SignerBlobRef, pn, tt.blobref)
   732  		if err != nil {
   733  			t.Fatalf("PathsLookup(%q): %v", tt.blobref, err)
   734  		}
   735  		if len(paths) != tt.want {
   736  			t.Fatalf("PathsLookup(%q) got %d results; want %d",
   737  				tt.blobref, len(paths), tt.want)
   738  		}
   739  		if tt.blobref == "with|pipe" {
   740  			p := paths[0]
   741  			want := fmt.Sprintf(
   742  				"Path{Claim: %s, %s; Base: %s + Suffix \"with|pipe\" => Target targ-124}",
   743  				claim2, claim2Time, pn)
   744  			if g := p.String(); g != want {
   745  				t.Errorf("claim wrong.\n got: %s\nwant: %s", g, want)
   746  			}
   747  		}
   748  	}
   749  
   750  	// now test deletions
   751  	// Delete an existing value
   752  	claim3 := id.Delete(claim2)
   753  	t.Logf("claim %q deletes path claim %q", claim3, claim2)
   754  	tests = []test{
   755  		{"targ-123", 1},
   756  		{"targ-124", 0},
   757  		{"targ-125", 0},
   758  	}
   759  	for _, tt := range tests {
   760  		signer := id.SignerBlobRef
   761  		paths, err := id.Index.PathsOfSignerTarget(signer, blob.ParseOrZero(tt.blobref))
   762  		if err != nil {
   763  			t.Fatalf("PathsOfSignerTarget(%q): %v", tt.blobref, err)
   764  		}
   765  		if len(paths) != tt.want {
   766  			t.Fatalf("PathsOfSignerTarget(%q) got %d results; want %d",
   767  				tt.blobref, len(paths), tt.want)
   768  		}
   769  	}
   770  	tests = []test{
   771  		{"somedir", 1},
   772  		{"with|pipe", 0},
   773  		{"void", 0},
   774  	}
   775  	for _, tt := range tests {
   776  		paths, err := id.Index.PathsLookup(id.SignerBlobRef, pn, tt.blobref)
   777  		if err != nil {
   778  			t.Fatalf("PathsLookup(%q): %v", tt.blobref, err)
   779  		}
   780  		if len(paths) != tt.want {
   781  			t.Fatalf("PathsLookup(%q) got %d results; want %d",
   782  				tt.blobref, len(paths), tt.want)
   783  		}
   784  	}
   785  
   786  	// recreate second path, and test if the previous deletion of it
   787  	// is indeed ignored.
   788  	claim4 := id.Delete(claim3)
   789  	t.Logf("delete claim %q deletes claim %q, which should undelete %q", claim4, claim3, claim2)
   790  	tests = []test{
   791  		{"targ-123", 1},
   792  		{"targ-124", 1},
   793  		{"targ-125", 0},
   794  	}
   795  	for _, tt := range tests {
   796  		signer := id.SignerBlobRef
   797  		paths, err := id.Index.PathsOfSignerTarget(signer, blob.ParseOrZero(tt.blobref))
   798  		if err != nil {
   799  			t.Fatalf("PathsOfSignerTarget(%q): %v", tt.blobref, err)
   800  		}
   801  		if len(paths) != tt.want {
   802  			t.Fatalf("PathsOfSignerTarget(%q) got %d results; want %d",
   803  				tt.blobref, len(paths), tt.want)
   804  		}
   805  		// and check the modtime too
   806  		if tt.blobref == "targ-124" {
   807  			p := paths[0]
   808  			want := fmt.Sprintf(
   809  				"Path{Claim: %s, %v; Base: %s + Suffix \"with|pipe\" => Target targ-124}",
   810  				claim2, claim2Time, pn)
   811  			if g := p.String(); g != want {
   812  				t.Errorf("claim wrong.\n got: %s\nwant: %s", g, want)
   813  			}
   814  		}
   815  	}
   816  	tests = []test{
   817  		{"somedir", 1},
   818  		{"with|pipe", 1},
   819  		{"void", 0},
   820  	}
   821  	for _, tt := range tests {
   822  		paths, err := id.Index.PathsLookup(id.SignerBlobRef, pn, tt.blobref)
   823  		if err != nil {
   824  			t.Fatalf("PathsLookup(%q): %v", tt.blobref, err)
   825  		}
   826  		if len(paths) != tt.want {
   827  			t.Fatalf("PathsLookup(%q) got %d results; want %d",
   828  				tt.blobref, len(paths), tt.want)
   829  		}
   830  		// and check that modtime is now claim4Time
   831  		if tt.blobref == "with|pipe" {
   832  			p := paths[0]
   833  			want := fmt.Sprintf(
   834  				"Path{Claim: %s, %s; Base: %s + Suffix \"with|pipe\" => Target targ-124}",
   835  				claim2, claim2Time, pn)
   836  			if g := p.String(); g != want {
   837  				t.Errorf("claim wrong.\n got: %s\nwant: %s", g, want)
   838  			}
   839  		}
   840  	}
   841  }
   842  
   843  func Files(t *testing.T, initIdx func() *index.Index) {
   844  	id := NewIndexDeps(initIdx())
   845  	id.Fataler = t
   846  	fileTime := time.Unix(1361250375, 0)
   847  	fileRef, wholeRef := id.UploadFile("foo.html", "<html>I am an html file.</html>", fileTime)
   848  	t.Logf("uploaded fileref %q, wholeRef %q", fileRef, wholeRef)
   849  	id.DumpIndex(t)
   850  
   851  	// ExistingFileSchemas
   852  	{
   853  		key := fmt.Sprintf("wholetofile|%s|%s", wholeRef, fileRef)
   854  		if g, e := id.Get(key), "1"; g != e {
   855  			t.Fatalf("%q = %q, want %q", key, g, e)
   856  		}
   857  
   858  		refs, err := id.Index.ExistingFileSchemas(wholeRef)
   859  		if err != nil {
   860  			t.Fatalf("ExistingFileSchemas = %v", err)
   861  		}
   862  		want := []blob.Ref{fileRef}
   863  		if !reflect.DeepEqual(refs, want) {
   864  			t.Errorf("ExistingFileSchemas got = %#v, want %#v", refs, want)
   865  		}
   866  	}
   867  
   868  	// FileInfo
   869  	{
   870  		key := fmt.Sprintf("fileinfo|%s", fileRef)
   871  		if g, e := id.Get(key), "31|foo.html|text%2Fhtml"; g != e {
   872  			t.Fatalf("%q = %q, want %q", key, g, e)
   873  		}
   874  
   875  		fi, err := id.Index.GetFileInfo(fileRef)
   876  		if err != nil {
   877  			t.Fatalf("GetFileInfo = %v", err)
   878  		}
   879  		if g, e := fi.Size, int64(31); g != e {
   880  			t.Errorf("Size = %d, want %d", g, e)
   881  		}
   882  		if g, e := fi.FileName, "foo.html"; g != e {
   883  			t.Errorf("FileName = %q, want %q", g, e)
   884  		}
   885  		if g, e := fi.MIMEType, "text/html"; g != e {
   886  			t.Errorf("MIMEType = %q, want %q", g, e)
   887  		}
   888  		if g, e := fi.Time, fileTime; !g.Time().Equal(e) {
   889  			t.Errorf("Time = %v; want %v", g, e)
   890  		}
   891  	}
   892  }
   893  
   894  func EdgesTo(t *testing.T, initIdx func() *index.Index) {
   895  	idx := initIdx()
   896  	id := NewIndexDeps(idx)
   897  	id.Fataler = t
   898  	defer id.DumpIndex(t)
   899  
   900  	// pn1 ---member---> pn2
   901  	pn1 := id.NewPermanode()
   902  	pn2 := id.NewPermanode()
   903  	claim1 := id.AddAttribute(pn1, "camliMember", pn2.String())
   904  
   905  	t.Logf("edge %s --> %s", pn1, pn2)
   906  
   907  	// Look for pn1
   908  	{
   909  		edges, err := idx.EdgesTo(pn2, nil)
   910  		if err != nil {
   911  			t.Fatal(err)
   912  		}
   913  		if len(edges) != 1 {
   914  			t.Fatalf("num edges = %d; want 1", len(edges))
   915  		}
   916  		wantEdge := &camtypes.Edge{
   917  			From:     pn1,
   918  			To:       pn2,
   919  			FromType: "permanode",
   920  		}
   921  		if got, want := edges[0].String(), wantEdge.String(); got != want {
   922  			t.Errorf("Wrong edge.\n GOT: %v\nWANT: %v", got, want)
   923  		}
   924  	}
   925  
   926  	// Delete claim -> break edge relationship.
   927  	del1 := id.Delete(claim1)
   928  	t.Logf("del claim %q deletes claim %q, breaks link between p1 and p2", del1, claim1)
   929  	// test that we can't find anymore pn1 from pn2
   930  	{
   931  		edges, err := idx.EdgesTo(pn2, nil)
   932  		if err != nil {
   933  			t.Fatal(err)
   934  		}
   935  		if len(edges) != 0 {
   936  			t.Fatalf("num edges = %d; want 0", len(edges))
   937  		}
   938  	}
   939  
   940  	// Undelete, should restore the link.
   941  	del2 := id.Delete(del1)
   942  	t.Logf("del claim %q deletes del claim %q, restores link between p1 and p2", del2, del1)
   943  	{
   944  		edges, err := idx.EdgesTo(pn2, nil)
   945  		if err != nil {
   946  			t.Fatal(err)
   947  		}
   948  		if len(edges) != 1 {
   949  			t.Fatalf("num edges = %d; want 1", len(edges))
   950  		}
   951  		wantEdge := &camtypes.Edge{
   952  			From:     pn1,
   953  			To:       pn2,
   954  			FromType: "permanode",
   955  		}
   956  		if got, want := edges[0].String(), wantEdge.String(); got != want {
   957  			t.Errorf("Wrong edge.\n GOT: %v\nWANT: %v", got, want)
   958  		}
   959  	}
   960  }
   961  
   962  func Delete(t *testing.T, initIdx func() *index.Index) {
   963  	idx := initIdx()
   964  	id := NewIndexDeps(idx)
   965  	id.Fataler = t
   966  	defer id.DumpIndex(t)
   967  	pn1 := id.NewPermanode()
   968  	t.Logf("uploaded permanode %q", pn1)
   969  	cl1 := id.SetAttribute(pn1, "tag", "foo1")
   970  	cl1Time := id.LastTime()
   971  	t.Logf("set attribute %q", cl1)
   972  
   973  	// delete pn1
   974  	delpn1 := id.Delete(pn1)
   975  	t.Logf("del claim %q deletes %q", delpn1, pn1)
   976  	deleted := idx.IsDeleted(pn1)
   977  	if !deleted {
   978  		t.Fatal("pn1 should be deleted")
   979  	}
   980  
   981  	// and try to find it with SearchPermanodesWithAttr (which should not work)
   982  	{
   983  		ch := make(chan blob.Ref, 10)
   984  		req := &camtypes.PermanodeByAttrRequest{
   985  			Signer:    id.SignerBlobRef,
   986  			Attribute: "tag",
   987  			Query:     "foo1"}
   988  		err := id.Index.SearchPermanodesWithAttr(ch, req)
   989  		if err != nil {
   990  			t.Fatalf("SearchPermanodesWithAttr = %v", err)
   991  		}
   992  		var got []blob.Ref
   993  		for r := range ch {
   994  			got = append(got, r)
   995  		}
   996  		want := []blob.Ref{}
   997  		if len(got) != len(want) {
   998  			t.Errorf("id.Index.SearchPermanodesWithAttr gives %q, want %q", got, want)
   999  		}
  1000  	}
  1001  
  1002  	// delete pn1 again with another claim
  1003  	delpn1bis := id.Delete(pn1)
  1004  	t.Logf("del claim %q deletes %q a second time", delpn1bis, pn1)
  1005  	deleted = idx.IsDeleted(pn1)
  1006  	if !deleted {
  1007  		t.Fatal("pn1 should be deleted")
  1008  	}
  1009  
  1010  	// verify that deleting delpn1 is not enough to make pn1 undeleted
  1011  	del2 := id.Delete(delpn1)
  1012  	t.Logf("delete claim %q deletes %q, which should not yet revive %q", del2, delpn1, pn1)
  1013  	deleted = idx.IsDeleted(pn1)
  1014  	if !deleted {
  1015  		t.Fatal("pn1 should not yet be undeleted")
  1016  	}
  1017  	// we should not yet be able to find it again with SearchPermanodesWithAttr
  1018  	{
  1019  		ch := make(chan blob.Ref, 10)
  1020  		req := &camtypes.PermanodeByAttrRequest{
  1021  			Signer:    id.SignerBlobRef,
  1022  			Attribute: "tag",
  1023  			Query:     "foo1"}
  1024  		err := id.Index.SearchPermanodesWithAttr(ch, req)
  1025  		if err != nil {
  1026  			t.Fatalf("SearchPermanodesWithAttr = %v", err)
  1027  		}
  1028  		var got []blob.Ref
  1029  		for r := range ch {
  1030  			got = append(got, r)
  1031  		}
  1032  		want := []blob.Ref{}
  1033  		if len(got) != len(want) {
  1034  			t.Errorf("id.Index.SearchPermanodesWithAttr gives %q, want %q", got, want)
  1035  		}
  1036  	}
  1037  
  1038  	// delete delpn1bis as well -> should undelete pn1
  1039  	del2bis := id.Delete(delpn1bis)
  1040  	t.Logf("delete claim %q deletes %q, which should revive %q", del2bis, delpn1bis, pn1)
  1041  	deleted = idx.IsDeleted(pn1)
  1042  	if deleted {
  1043  		t.Fatal("pn1 should be undeleted")
  1044  	}
  1045  	// we should now be able to find it again with SearchPermanodesWithAttr
  1046  	{
  1047  		ch := make(chan blob.Ref, 10)
  1048  		req := &camtypes.PermanodeByAttrRequest{
  1049  			Signer:    id.SignerBlobRef,
  1050  			Attribute: "tag",
  1051  			Query:     "foo1"}
  1052  		err := id.Index.SearchPermanodesWithAttr(ch, req)
  1053  		if err != nil {
  1054  			t.Fatalf("SearchPermanodesWithAttr = %v", err)
  1055  		}
  1056  		var got []blob.Ref
  1057  		for r := range ch {
  1058  			got = append(got, r)
  1059  		}
  1060  		want := []blob.Ref{pn1}
  1061  		if len(got) < 1 || got[0].String() != want[0].String() {
  1062  			t.Errorf("id.Index.SearchPermanodesWithAttr gives %q, want %q", got, want)
  1063  		}
  1064  	}
  1065  
  1066  	// Delete cl1
  1067  	del3 := id.Delete(cl1)
  1068  	t.Logf("del claim %q deletes claim %q", del3, cl1)
  1069  	deleted = idx.IsDeleted(cl1)
  1070  	if !deleted {
  1071  		t.Fatal("cl1 should be deleted")
  1072  	}
  1073  	// we should not find anything with SearchPermanodesWithAttr
  1074  	{
  1075  		ch := make(chan blob.Ref, 10)
  1076  		req := &camtypes.PermanodeByAttrRequest{
  1077  			Signer:    id.SignerBlobRef,
  1078  			Attribute: "tag",
  1079  			Query:     "foo1"}
  1080  		err := id.Index.SearchPermanodesWithAttr(ch, req)
  1081  		if err != nil {
  1082  			t.Fatalf("SearchPermanodesWithAttr = %v", err)
  1083  		}
  1084  		var got []blob.Ref
  1085  		for r := range ch {
  1086  			got = append(got, r)
  1087  		}
  1088  		want := []blob.Ref{}
  1089  		if len(got) != len(want) {
  1090  			t.Errorf("id.Index.SearchPermanodesWithAttr gives %q, want %q", got, want)
  1091  		}
  1092  	}
  1093  	// and now check that AppendClaims finds nothing for pn
  1094  	{
  1095  		claims, err := id.Index.AppendClaims(nil, pn1, id.SignerBlobRef, "")
  1096  		if err != nil {
  1097  			t.Errorf("AppendClaims = %v", err)
  1098  		} else {
  1099  			want := []camtypes.Claim{}
  1100  			if len(claims) != len(want) {
  1101  				t.Errorf("id.Index.AppendClaims gives %q, want %q", claims, want)
  1102  			}
  1103  		}
  1104  	}
  1105  
  1106  	// undelete cl1
  1107  	del4 := id.Delete(del3)
  1108  	t.Logf("del claim %q deletes del claim %q, which should undelete %q", del4, del3, cl1)
  1109  	// We should now be able to find it again with both methods
  1110  	{
  1111  		ch := make(chan blob.Ref, 10)
  1112  		req := &camtypes.PermanodeByAttrRequest{
  1113  			Signer:    id.SignerBlobRef,
  1114  			Attribute: "tag",
  1115  			Query:     "foo1"}
  1116  		err := id.Index.SearchPermanodesWithAttr(ch, req)
  1117  		if err != nil {
  1118  			t.Fatalf("SearchPermanodesWithAttr = %v", err)
  1119  		}
  1120  		var got []blob.Ref
  1121  		for r := range ch {
  1122  			got = append(got, r)
  1123  		}
  1124  		want := []blob.Ref{pn1}
  1125  		if len(got) < 1 || got[0].String() != want[0].String() {
  1126  			t.Errorf("id.Index.SearchPermanodesWithAttr gives %q, want %q", got, want)
  1127  		}
  1128  	}
  1129  	// and check that AppendClaims finds cl1, with the right modtime too
  1130  	{
  1131  		claims, err := id.Index.AppendClaims(nil, pn1, id.SignerBlobRef, "")
  1132  		if err != nil {
  1133  			t.Errorf("AppendClaims = %v", err)
  1134  		} else {
  1135  			want := []camtypes.Claim{
  1136  				camtypes.Claim{
  1137  					BlobRef:   cl1,
  1138  					Permanode: pn1,
  1139  					Signer:    id.SignerBlobRef,
  1140  					Date:      cl1Time.UTC(),
  1141  					Type:      "set-attribute",
  1142  					Attr:      "tag",
  1143  					Value:     "foo1",
  1144  				},
  1145  			}
  1146  			if !reflect.DeepEqual(claims, want) {
  1147  				t.Errorf("GetOwnerClaims results differ.\n got: %v\nwant: %v",
  1148  					claims, want)
  1149  			}
  1150  		}
  1151  	}
  1152  }
  1153  
  1154  type searchResults []camtypes.RecentPermanode
  1155  
  1156  func (s searchResults) String() string {
  1157  	var buf bytes.Buffer
  1158  	fmt.Fprintf(&buf, "[%d search results: ", len(s))
  1159  	for _, r := range s {
  1160  		fmt.Fprintf(&buf, "{BlobRef: %s, Signer: %s, LastModTime: %d}",
  1161  			r.Permanode, r.Signer, r.LastModTime.Unix())
  1162  	}
  1163  	buf.WriteString("]")
  1164  	return buf.String()
  1165  }