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