Merge "veyron/services/wspr: Setting NoTimeout for WSPR RPC calls. But we need to provide an API in JS for developers to set proper timeout. See https://code.google.com/p/envyor/issues/detail?id=48 for details."
diff --git a/services/store/memstore/blackbox/team_player_test.go b/services/store/memstore/blackbox/team_player_test.go
index fc9c3d0..61a1255 100644
--- a/services/store/memstore/blackbox/team_player_test.go
+++ b/services/store/memstore/blackbox/team_player_test.go
@@ -122,7 +122,9 @@
// Iterate over the rockets.
players := make(map[storage.ID]*Player)
name := storage.ParsePath("/teamsapp/players")
- for it := st.Snapshot().NewIterator(rootPublicID, name, nil); it.IsValid(); it.Next() {
+ for it := st.Snapshot().NewIterator(rootPublicID, name,
+ state.ListPaths, nil); it.IsValid(); it.Next() {
+
e := it.Get()
if p, ok := e.Value.(*Player); ok {
if _, ok := players[e.Stat.ID]; ok {
@@ -148,7 +150,9 @@
// Iterate over all teams, nonrecursively.
teams := make(map[storage.ID]*Team)
name = storage.ParsePath("/teamsapp/teams")
- for it := st.Snapshot().NewIterator(rootPublicID, name, state.ImmediateFilter); it.IsValid(); it.Next() {
+ for it := st.Snapshot().NewIterator(rootPublicID, name,
+ state.ListPaths, state.ImmediateFilter); it.IsValid(); it.Next() {
+
e := it.Get()
v := e.Value
if _, ok := v.(*Player); ok {
@@ -173,17 +177,19 @@
}
// Iterate over all teams, recursively.
- playerCount := 0
+ contractCount := 0
teamCount := 0
players = make(map[storage.ID]*Player)
teams = make(map[storage.ID]*Team)
name = storage.ParsePath("/teamsapp/teams")
- for it := st.Snapshot().NewIterator(rootPublicID, name, nil); it.IsValid(); it.Next() {
+ for it := st.Snapshot().NewIterator(rootPublicID, name,
+ state.ListPaths, nil); it.IsValid(); it.Next() {
+
e := it.Get()
v := e.Value
if p, ok := v.(*Player); ok {
players[e.Stat.ID] = p
- playerCount++
+ contractCount++
}
if team, ok := v.(*Team); ok {
teams[e.Stat.ID] = team
@@ -202,8 +208,8 @@
if team, ok := teams[hornetsID]; !ok || team.FullName != "Hornets" {
t.Errorf("Should have Hornets, have %v", team)
}
- if playerCount != 3 {
- t.Errorf("Should have 3 players: have %d", playerCount)
+ if contractCount != 4 {
+ t.Errorf("Should have 4 contracts: have %d", contractCount)
}
if len(players) != 3 {
t.Errorf("Should have 3 players: have %v", players)
diff --git a/services/store/memstore/query/eval.go b/services/store/memstore/query/eval.go
index cd89615..48e991a 100644
--- a/services/store/memstore/query/eval.go
+++ b/services/store/memstore/query/eval.go
@@ -357,7 +357,9 @@
path := storage.ParsePath(naming.Join(c.suffix, basepath))
vlog.VI(2).Infof("nameEvaluator suffix: %s, result.Name: %s, VName: %s",
c.suffix, result.Name, e.wildcardName.VName)
- for it := c.sn.NewIterator(c.clientID, path, state.ImmediateFilter); it.IsValid(); it.Next() {
+ for it := c.sn.NewIterator(c.clientID, path,
+ state.ListObjects, state.ImmediateFilter); it.IsValid(); it.Next() {
+
entry := it.Get()
result := &store.QueryResult{
Name: naming.Join(basepath, it.Name()),
diff --git a/services/store/memstore/query/eval_test.go b/services/store/memstore/query/eval_test.go
index 282eaa0..2985265 100644
--- a/services/store/memstore/query/eval_test.go
+++ b/services/store/memstore/query/eval_test.go
@@ -492,7 +492,7 @@
it state.Iterator
}
-func (m *mockSnapshot) NewIterator(pid security.PublicID, path storage.PathName, filter state.IterFilter) state.Iterator {
+func (m *mockSnapshot) NewIterator(pid security.PublicID, path storage.PathName, pathFilter state.PathFilter, filter state.IterFilter) state.Iterator {
return m.it
}
diff --git a/services/store/memstore/query/glob.go b/services/store/memstore/query/glob.go
index 3614af3..8b1722e 100644
--- a/services/store/memstore/query/glob.go
+++ b/services/store/memstore/query/glob.go
@@ -32,7 +32,7 @@
pathLen: len(path),
glob: parsed,
}
- g.Iterator = sn.NewIterator(clientID, path, state.IterFilter(g.filter))
+ g.Iterator = sn.NewIterator(clientID, path, state.ListPaths, state.IterFilter(g.filter))
return g, nil
}
diff --git a/services/store/memstore/query/glob_test.go b/services/store/memstore/query/glob_test.go
index 70ff580..6690d98 100644
--- a/services/store/memstore/query/glob_test.go
+++ b/services/store/memstore/query/glob_test.go
@@ -9,51 +9,64 @@
"veyron2/storage"
)
-type nameOptions []string
-
type globTest struct {
path string
pattern string
- expected []nameOptions
+ expected []string
}
var globTests = []globTest{
- {"", "mvps/...", []nameOptions{
- {"mvps"},
- {"mvps/Links/0"},
- {"mvps/Links/1"},
+ {"", "...", []string{
+ "",
+ "mvps",
+ "mvps/Links/0",
+ "mvps/Links/1",
+ "players",
+ "players/alfred",
+ "players/alice",
+ "players/betty",
+ "players/bob",
+ "teams",
+ "teams/bears",
+ "teams/cardinals",
+ "teams/sharks",
}},
- {"", "players/...", []nameOptions{
- {"players"},
- {"players/alfred"},
- {"players/alice"},
- {"players/betty"},
- {"players/bob"},
+ {"", "mvps/...", []string{
+ "mvps",
+ "mvps/Links/0",
+ "mvps/Links/1",
+ }},
+ {"", "players/...", []string{
+ "players",
+ "players/alfred",
+ "players/alice",
+ "players/betty",
+ "players/bob",
}},
// Note(mattr): This test case shows that Glob does not return
// subfield nodes.
- {"", "mvps/*", []nameOptions{}},
- {"", "mvps/Links/*", []nameOptions{
- {"mvps/Links/0"},
- {"mvps/Links/1"},
+ {"", "mvps/*", []string{}},
+ {"", "mvps/Links/*", []string{
+ "mvps/Links/0",
+ "mvps/Links/1",
}},
- {"", "players/alfred", []nameOptions{
- {"players/alfred"},
+ {"", "players/alfred", []string{
+ "players/alfred",
}},
- {"", "mvps/Links/0", []nameOptions{
- {"mvps/Links/0"},
+ {"", "mvps/Links/0", []string{
+ "mvps/Links/0",
}},
// An empty pattern returns the element referred to by the path.
- {"/mvps/Links/0", "", []nameOptions{
- {""},
+ {"/mvps/Links/0", "", []string{
+ "",
}},
- {"mvps", "Links/*", []nameOptions{
- {"Links/0"},
- {"Links/1"},
+ {"mvps", "Links/*", []string{
+ "Links/0",
+ "Links/1",
}},
- {"mvps/Links", "*", []nameOptions{
- {"0"},
- {"1"},
+ {"mvps/Links", "*", []string{
+ "0",
+ "1",
}},
}
@@ -102,16 +115,9 @@
t.Errorf("Wrong number of names for %s. got %v, wanted %v",
gt.pattern, names, gt.expected)
}
- for _, options := range gt.expected {
- found := false
- for _, name := range options {
- if names[name] {
- found = true
- break
- }
- }
- if !found {
- t.Errorf("Expected to find one of %v in %v", options, names)
+ for _, name := range gt.expected {
+ if !names[name] {
+ t.Errorf("Expected to find %v in %v", name, names)
}
}
}
diff --git a/services/store/memstore/state/iterator.go b/services/store/memstore/state/iterator.go
index 92614be..70eac5c 100644
--- a/services/store/memstore/state/iterator.go
+++ b/services/store/memstore/state/iterator.go
@@ -37,10 +37,13 @@
type iterator struct {
snapshot Snapshot
- // Set of IDs already visited.
+ // Set of IDs already visited on this path.
visited map[storage.ID]struct{}
- // Stack of IDs to visit next. Some of these may already have been visited.
+ // Stack of actions to consider next. Actions are one of:
+ // - visit a node accessible from the current path (the node may already
+ // have been visited on the current path).
+ // - unvisit a node (backtrack the current path).
next []next
// Depth of starting path.
@@ -50,6 +53,8 @@
entry *storage.Entry
path *refs.FullPath
+ pathFilter PathFilter
+
filter IterFilter
}
@@ -59,13 +64,32 @@
parent *refs.FullPath
path *refs.Path
id storage.ID
+ action action
}
+type action int
+
+const (
+ visit = action(iota)
+ unvisit
+)
+
var (
_ Iterator = (*iterator)(nil)
_ Iterator = (*errorIterator)(nil)
)
+// A PathFilter automatically limits the traversal of certain paths,
+type PathFilter int
+
+const (
+ // ListPaths permits any path that does not visit the same object twice.
+ ListPaths = PathFilter(iota)
+ // ListObjects permits any path that does not revisit any object on a
+ // previously traversed path 'Q', even if Q did not satisfy it.filter.
+ ListObjects
+)
+
// An IterFilter examines entries as they are considered by the
// iterator and allows it to give two boolean inputs to the process:
// ret: True if the iterator should return this value in its iteration.
@@ -84,11 +108,14 @@
return true, path == nil
}
-// NewIterator returns an Iterator that starts with the value at
-// <path>. If filter is given it is used to limit traversal beneath
-// certain paths. filter can be specified to limit the results of the iteration.
-// If filter is nil, all decendents of the specified path are returned.
-func (sn *snapshot) NewIterator(pid security.PublicID, path storage.PathName, filter IterFilter) Iterator {
+// NewIterator returns an Iterator that starts with the value at <path>.
+// pathFilter is used to automatically limit traversal of certain paths.
+// If filter is given, it is used to limit traversal beneath certain paths and
+// limit the results of the iteration. If filter is nil, all decendents of the
+// specified path are returned.
+func (sn *snapshot) NewIterator(pid security.PublicID, path storage.PathName,
+ pathFilter PathFilter, filter IterFilter) Iterator {
+
checker := sn.newPermChecker(pid)
cell, suffix, v := sn.resolveCell(checker, path, nil)
if cell == nil {
@@ -104,6 +131,7 @@
visited: make(map[storage.ID]struct{}),
initialDepth: len(path),
path: refs.NewFullPathFromName(path),
+ pathFilter: pathFilter,
filter: filter,
}
@@ -120,11 +148,12 @@
} else {
it.entry = cell.GetEntry()
it.visited[cell.ID] = struct{}{}
+ it.pushUnvisit(nil, cell.ID)
set = cell.refs
}
if expand {
- it.pushAll(checker, it.path, set)
+ it.pushVisitAll(checker, it.path, set)
}
if !ret {
it.Next()
@@ -133,11 +162,25 @@
return it
}
-func (it *iterator) pushAll(checker *acl.Checker, parentPath *refs.FullPath, set refs.Set) {
+func (it *iterator) pushUnvisit(path *refs.Path, id storage.ID) {
+ switch it.pathFilter {
+ case ListPaths:
+ it.next = append(it.next, next{nil, nil, path, id, unvisit})
+ case ListObjects:
+ // Do not unvisit the object, as it is on a path already seen by
+ // it.filter.
+ default:
+ panic("unknown PathFilter")
+ }
+}
+
+func (it *iterator) pushVisitAll(checker *acl.Checker,
+ parentPath *refs.FullPath, set refs.Set) {
+
set.Iter(func(x interface{}) bool {
ref := x.(*refs.Ref)
if checker.IsAllowed(ref.Label) {
- it.next = append(it.next, next{checker, parentPath, ref.Path, ref.ID})
+ it.next = append(it.next, next{checker, parentPath, ref.Path, ref.ID, visit})
}
return true
})
@@ -171,10 +214,20 @@
return
}
n, it.next = it.next[topIndex], it.next[:topIndex]
+
+ if n.action == unvisit {
+ delete(it.visited, n.id)
+ continue
+ }
+
if _, ok := it.visited[n.id]; ok {
continue
}
+ // Mark as visited.
+ it.visited[n.id] = struct{}{}
+ it.pushUnvisit(n.path, n.id)
+
// Fetch the cell.
c = it.snapshot.Find(n.id)
if c == nil {
@@ -193,7 +246,7 @@
ret, expand := it.filter(n.parent, n.path)
fullPath = n.parent.AppendPath(n.path)
if expand {
- it.pushAll(checker, fullPath, c.refs)
+ it.pushVisitAll(checker, fullPath, c.refs)
}
if ret {
// Found a value.
@@ -201,8 +254,6 @@
}
}
- // Mark as visited.
- it.visited[n.id] = struct{}{}
it.entry, it.path = c.GetEntry(), fullPath
}
diff --git a/services/store/memstore/state/iterator_test.go b/services/store/memstore/state/iterator_test.go
index 344f69b..a9f3b2b 100644
--- a/services/store/memstore/state/iterator_test.go
+++ b/services/store/memstore/state/iterator_test.go
@@ -4,16 +4,48 @@
"runtime"
"testing"
+ "veyron/services/store/memstore/refs"
"veyron/services/store/memstore/state"
"veyron2/security"
"veyron2/storage"
)
+// check that the iterator produces a set of names.
+func checkAcyclicIterator(t *testing.T, sn *state.MutableSnapshot, id security.PublicID, filter state.IterFilter, names []string) {
+ _, file, line, _ := runtime.Caller(1)
+
+ // Construct an index of names.
+ index := map[string]bool{}
+ for _, name := range names {
+ index[name] = false
+ }
+
+ // Compute the found names.
+ for it := sn.NewIterator(id, storage.ParsePath("/"), state.ListPaths, filter); it.IsValid(); it.Next() {
+ name := it.Name()
+ if found, ok := index[name]; ok {
+ if found {
+ t.Errorf("%s(%d): duplicate name %q", file, line, name)
+ }
+ index[name] = true
+ } else {
+ t.Errorf("%s(%d): unexpected name %q", file, line, name)
+ }
+ }
+
+ // Print the not found names.
+ for name, found := range index {
+ if !found {
+ t.Errorf("%s(%d): expected: %v", file, line, name)
+ }
+ }
+}
+
// check that the iterator produces a set of names. Since entries in the store
// can have multiple names, the names are provided using a set of equivalence
// classes. The requirement is that the iterator produces exactly one name from
-// each equivalence class. Order doesn't matter.
-func checkIterator(t *testing.T, sn *state.MutableSnapshot, id security.PublicID, names [][]string) {
+// each equivalence class. Order doesn't matter.
+func checkUniqueObjectsIterator(t *testing.T, sn *state.MutableSnapshot, id security.PublicID, filter state.IterFilter, names [][]string) {
_, file, line, _ := runtime.Caller(1)
// Construct an index of name to equivalence class.
@@ -26,7 +58,7 @@
// Compute the found set of equivalence classes.
found := map[int]bool{}
- for it := sn.NewIterator(id, storage.ParsePath("/"), nil); it.IsValid(); it.Next() {
+ for it := sn.NewIterator(id, storage.ParsePath("/"), state.ListObjects, filter); it.IsValid(); it.Next() {
name := it.Name()
if i, ok := index[name]; ok {
if _, ok := found[i]; ok {
@@ -46,6 +78,55 @@
}
}
+// Tests that an iterator returns all non-cyclic paths that reach an object.
+func TestDuplicatePaths(t *testing.T) {
+ st := state.New(rootPublicID)
+ sn := st.MutableSnapshot()
+
+ // Add some objects
+ put(t, sn, rootPublicID, "/", "")
+ put(t, sn, rootPublicID, "/teams", "")
+ put(t, sn, rootPublicID, "/teams/cardinals", "")
+ put(t, sn, rootPublicID, "/players", "")
+ mattID := put(t, sn, rootPublicID, "/players/matt", "")
+
+ // Add some hard links
+ put(t, sn, rootPublicID, "/teams/cardinals/mvp", mattID)
+
+ checkAcyclicIterator(t, sn, rootPublicID, nil, []string{
+ "",
+ "teams",
+ "players",
+ "teams/cardinals",
+ "players/matt",
+ "teams/cardinals/mvp",
+ })
+ checkUniqueObjectsIterator(t, sn, rootPublicID, nil, [][]string{
+ {""},
+ {"teams"},
+ {"players"},
+ {"teams/cardinals"},
+ {"players/matt", "teams/cardinals/mvp"},
+ })
+
+ // Test that the iterator does not revisit objects on previously rejected paths.
+ rejected := false
+ rejectMatt := func(fullPath *refs.FullPath, path *refs.Path) (bool, bool) {
+ name := fullPath.Append(path.Suffix(1)).Name().String()
+ if !rejected && (name == "players/matt" || name == "teams/cardinals/mvp") {
+ rejected = true
+ return false, true
+ }
+ return true, true
+ }
+ checkUniqueObjectsIterator(t, sn, rootPublicID, rejectMatt, [][]string{
+ {""},
+ {"teams"},
+ {"players"},
+ {"teams/cardinals"},
+ })
+}
+
// Test that an iterator doesn't get stuck in cycles.
func TestCyclicStructure(t *testing.T) {
st := state.New(rootPublicID)
@@ -63,7 +144,17 @@
put(t, sn, rootPublicID, "/players/matt/team", cardinalsID)
put(t, sn, rootPublicID, "/teams/cardinals/mvp", mattID)
- checkIterator(t, sn, rootPublicID, [][]string{
+ checkAcyclicIterator(t, sn, rootPublicID, nil, []string{
+ "",
+ "teams",
+ "players",
+ "players/joe",
+ "players/matt",
+ "teams/cardinals/mvp",
+ "teams/cardinals",
+ "players/matt/team",
+ })
+ checkUniqueObjectsIterator(t, sn, rootPublicID, nil, [][]string{
{""},
{"teams"},
{"players"},
@@ -108,7 +199,21 @@
put(t, sn, rootPublicID, "/Users/john/shared", sharedID)
// Root gets everything.
- checkIterator(t, sn, rootPublicID, [][]string{
+ checkAcyclicIterator(t, sn, rootPublicID, nil, []string{
+ "",
+ "Users",
+ "Users/jane",
+ "Users/jane/acls",
+ "Users/jane/acls/janeRWA",
+ "Users/jane/aaa",
+ "Users/john",
+ "Users/john/acls",
+ "Users/john/acls/johnRWA",
+ "Users/john/aaa",
+ "Users/jane/shared",
+ "Users/john/shared",
+ })
+ checkUniqueObjectsIterator(t, sn, rootPublicID, nil, [][]string{
{""},
{"Users"},
{"Users/jane"},
@@ -123,7 +228,16 @@
})
// Jane sees only her names.
- checkIterator(t, sn, janePublicID, [][]string{
+ checkAcyclicIterator(t, sn, janePublicID, nil, []string{
+ "",
+ "Users",
+ "Users/jane",
+ "Users/jane/acls",
+ "Users/jane/acls/janeRWA",
+ "Users/jane/aaa",
+ "Users/jane/shared",
+ })
+ checkUniqueObjectsIterator(t, sn, janePublicID, nil, [][]string{
{""},
{"Users"},
{"Users/jane"},
@@ -134,7 +248,16 @@
})
// John sees only his names.
- checkIterator(t, sn, johnPublicID, [][]string{
+ checkAcyclicIterator(t, sn, johnPublicID, nil, []string{
+ "",
+ "Users",
+ "Users/john",
+ "Users/john/acls",
+ "Users/john/acls/johnRWA",
+ "Users/john/aaa",
+ "Users/john/shared",
+ })
+ checkUniqueObjectsIterator(t, sn, johnPublicID, nil, [][]string{
{""},
{"Users"},
{"Users/john"},
diff --git a/services/store/memstore/state/snapshot.go b/services/store/memstore/state/snapshot.go
index fd3a032..4213a55 100644
--- a/services/store/memstore/state/snapshot.go
+++ b/services/store/memstore/state/snapshot.go
@@ -12,11 +12,12 @@
)
type Snapshot interface {
- // NewIterator returns an Iterator that starts with the value at <path>. If
- // filter is given it is used to limit traversal beneath certain paths.
- // filter can be specified to limit the results of the iteration. If filter
- // is nil, all decendents of the specified path are returned.
- NewIterator(pid security.PublicID, path storage.PathName, filter IterFilter) Iterator
+ // NewIterator returns an Iterator that starts with the value at <path>.
+ // pathFilter is used to automatically limit traversal of certain paths.
+ // If filter is given, it is used to limit traversal beneath certain paths
+ // and limit the results of the iteration. If filter is nil, all decendents
+ // of the specified path are returned.
+ NewIterator(pid security.PublicID, path storage.PathName, pathFilter PathFilter, filter IterFilter) Iterator
// PathMatch returns true iff there is a name for the store value that
// matches the pathRegex.
diff --git a/services/store/memstore/watch/glob_processor_test.go b/services/store/memstore/watch/glob_processor_test.go
index 699d5f5..02f73c7 100644
--- a/services/store/memstore/watch/glob_processor_test.go
+++ b/services/store/memstore/watch/glob_processor_test.go
@@ -19,6 +19,9 @@
id1 := put(t, st, tr, "/", "val1")
id2 := put(t, st, tr, "/a", "val2")
put(t, st, tr, "/a/b", "val3")
+ id4 := put(t, st, tr, "/a/c", "val4")
+ // Test duplicate paths to the same object.
+ put(t, st, tr, "/a/d", id4)
commit(t, tr)
// Remove /a/b.
@@ -44,20 +47,26 @@
aRecursiveProcessor := createGlobProcessor(t, storage.ParsePath("/a"), "...")
aListProcessor := createGlobProcessor(t, storage.ParsePath("/a"), "*")
- // Expect initial state that contains / and /a.
+ // Expect initial state that contains /, /a, /a/c and /a/d.
logst := readState(t, log)
- changes := processState(t, rootRecursiveProcessor, logst, 2)
+ changes := processState(t, rootRecursiveProcessor, logst, 4)
watchtesting.ExpectEntryExists(t, changes, "", id1, "val1")
watchtesting.ExpectEntryExists(t, changes, "a", id2, "val2")
+ watchtesting.ExpectEntryExists(t, changes, "a/c", id4, "val4")
+ watchtesting.ExpectEntryExists(t, changes, "a/d", id4, "val4")
changes = processState(t, rootListProcessor, logst, 1)
watchtesting.ExpectEntryExists(t, changes, "a", id2, "val2")
- changes = processState(t, aRecursiveProcessor, logst, 1)
+ changes = processState(t, aRecursiveProcessor, logst, 3)
watchtesting.ExpectEntryExists(t, changes, "a", id2, "val2")
+ watchtesting.ExpectEntryExists(t, changes, "a/c", id4, "val4")
+ watchtesting.ExpectEntryExists(t, changes, "a/d", id4, "val4")
- processState(t, aListProcessor, logst, 0)
+ processState(t, aListProcessor, logst, 2)
+ watchtesting.ExpectEntryExists(t, changes, "a/c", id4, "val4")
+ watchtesting.ExpectEntryExists(t, changes, "a/d", id4, "val4")
}
func TestGlobProcessTransactionAdd(t *testing.T) {
@@ -85,25 +94,34 @@
id1 := put(t, st, tr, "/", "val1")
id2 := put(t, st, tr, "/a", "val2")
id3 := put(t, st, tr, "/a/b", "val3")
+ id4 := put(t, st, tr, "/a/c", "val4")
+ // Test duplicate paths to the same object.
+ put(t, st, tr, "/a/d", id4)
commit(t, tr)
// Expect transaction that adds /, /a and /a/b.
mus := readTransaction(t, log)
- changes := processTransaction(t, rootRecursiveProcessor, mus, 3)
+ changes := processTransaction(t, rootRecursiveProcessor, mus, 5)
watchtesting.ExpectEntryExists(t, changes, "", id1, "val1")
watchtesting.ExpectEntryExists(t, changes, "a", id2, "val2")
watchtesting.ExpectEntryExists(t, changes, "a/b", id3, "val3")
+ watchtesting.ExpectEntryExists(t, changes, "a/c", id4, "val4")
+ watchtesting.ExpectEntryExists(t, changes, "a/d", id4, "val4")
changes = processTransaction(t, rootListProcessor, mus, 1)
watchtesting.ExpectEntryExists(t, changes, "a", id2, "val2")
- changes = processTransaction(t, aRecursiveProcessor, mus, 2)
+ changes = processTransaction(t, aRecursiveProcessor, mus, 4)
watchtesting.ExpectEntryExists(t, changes, "a", id2, "val2")
watchtesting.ExpectEntryExists(t, changes, "a/b", id3, "val3")
+ watchtesting.ExpectEntryExists(t, changes, "a/c", id4, "val4")
+ watchtesting.ExpectEntryExists(t, changes, "a/d", id4, "val4")
- changes = processTransaction(t, aListProcessor, mus, 1)
+ changes = processTransaction(t, aListProcessor, mus, 3)
watchtesting.ExpectEntryExists(t, changes, "a/b", id3, "val3")
+ watchtesting.ExpectEntryExists(t, changes, "a/c", id4, "val4")
+ watchtesting.ExpectEntryExists(t, changes, "a/d", id4, "val4")
changes = processTransaction(t, bRecursiveProcessor, mus, 1)
watchtesting.ExpectEntryExists(t, changes, "a/b", id3, "val3")
diff --git a/services/store/memstore/watch/raw_processor.go b/services/store/memstore/watch/raw_processor.go
index a783caf..5c8d76e 100644
--- a/services/store/memstore/watch/raw_processor.go
+++ b/services/store/memstore/watch/raw_processor.go
@@ -67,7 +67,9 @@
// Create a change for each id in the state. In each change, the object
// exists, has no PriorVersion, has the Version of the new cell, and
// has the Value, Tags and Dir of the new cell.
- for it := sn.NewIterator(p.pid, nil, state.RecursiveFilter); it.IsValid(); it.Next() {
+ for it := sn.NewIterator(p.pid, nil,
+ state.ListObjects, state.RecursiveFilter); it.IsValid(); it.Next() {
+
entry := it.Get()
id := entry.Stat.ID
// Retrieve Value, Tags and Dir from the corresponding cell.