Skip to content

Commit 73d32a2

Browse files
committed
[LIB][BUG] (*node.Tree).Sync() is not working properly.
Summary: There was a few bugs detected by adding a simple integration test: - The scanner created from the response body was not working properly, it was never making it inside of the for loop. - The checkpoint is not necessarily present on every line, so by updating it on every line, we were ending up removing the Checkpoint completely and thus getting stuck in a sync loop. - Forcing a complete reset if the parents have changed was not optimal nor the right thing to do. fixes T14 Test Plan: Travis-CI is happy. AND STR: - Delete the current cache located at `~/Library/Caches/com.appspot.go-acd.cache` on Mac. - Run the `ls` command implemented in T5, this should create the cache - Upload a new folder using the [Amazon Cloud Drive Web UI](https://www.amazon.com/clouddrive) - Run the `ls` command to list the recently uploaded folder Expected: The files in the newly created folder are listed. Actual: The library is returning `node not found` ``` kalbasit@cratos ~/code/src/gopkg.in/acd.v0 [T5 *] ± % go run cmd/acd/main.go ls acd:///pictures panic: node not found goroutine 1 [running]: gopkg.in/acd.v0/cli.lsAction(0xc2082037a0) /Users/kalbasit/code/src/gopkg.in/acd.v0/cli/ls.go:31 +0x126 github.com/codegangsta/cli.(*App).RunAsSubcommand(0xc20806d400, 0xc208080000, 0x0, 0x0) /Users/kalbasit/code/src/github.com/codegangsta/cli/app.go:262 +0xc8c github.com/codegangsta/cli.Command.startApp(0x39c8b0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3d2590, 0x17, 0x444190, ...) /Users/kalbasit/code/src/github.com/codegangsta/cli/command.go:183 +0x505 github.com/codegangsta/cli.Command.Run(0x39c8b0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3d2590, 0x17, 0x444190, ...) /Users/kalbasit/code/src/github.com/codegangsta/cli/command.go:45 +0x14ee github.com/codegangsta/cli.(*App).Run(0xc20806c200, 0xc20800a000, 0x3, 0x3, 0x0, 0x0) /Users/kalbasit/code/src/github.com/codegangsta/cli/app.go:160 +0xd0c github.com/codegangsta/cli.(*App).RunAndExitOnError(0xc20806c200) /Users/kalbasit/code/src/github.com/codegangsta/cli/app.go:171 +0x57 main.main() /Users/kalbasit/code/src/gopkg.in/acd.v0/cmd/acd/main.go:7 +0x2c goroutine 17 [syscall, locked to thread]: runtime.goexit() /usr/local/Cellar/go/1.4.2/libexec/src/runtime/asm_amd64.s:2232 +0x1 exit status 2 ``` Reviewers: #go_amazon_cloud_drive, kalbasit Reviewed By: #go_amazon_cloud_drive, kalbasit Maniphest Tasks: T14 Differential Revision: http://phabricator.nasreddine.com/D11
1 parent 0dcfbae commit 73d32a2

File tree

8 files changed

+219
-37
lines changed

8 files changed

+219
-37
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
The Amazon Cloud Drive API lets your customers access the photos, videos, and
2+
documents they have saved in Amazon Cloud Drive, and gives you the ability to
3+
interact with millions of Amazon customers. With access to the free Amazon
4+
Cloud Drive API you can put your own creative spin on how they upload, view,
5+
edit, download, and organize their digital content using your app.
6+
7+
Built on the highly reliable and scalable AWS platform, the Amazon Cloud Drive
8+
API puts the power of a large-scale cloud services platform in your hands. You
9+
can offer confidence and peace of mind to your customer, as their content is
10+
safe and always accessible from Amazon Cloud Drive. Quit worrying about the
11+
cost of storage and unleash your creativity to build the best experience for
12+
your customers. For example, you could develop a mobile app to let customers
13+
edit all of their photos and videos into a single movie, or a web app that
14+
allows customers to organize their content around geolocation, or a desktop app
15+
that reimagines the way customers interact locally with their data in the
16+
cloud.
17+
18+
If you're new to the Amazon Cloud Drive API, see our Developer Guide and tour
19+
Getting Started.
20+
21+
https://developer.amazon.com/public/apis/experience/cloud-drive
22+
syncfolder/README.syncfolder

integrationtest/simple_upload_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,10 @@ func TestSimpleUpload(t *testing.T) {
4545
// test the NodeTree is updated
4646
node, err := c.NodeTree.FindNode(remoteReadmeFile)
4747
if err != nil {
48-
t.Errorf("c.NodeTree.FindNode(%s): got error %s", remoteReadmeFile, err)
48+
t.Errorf("c.NodeTree.FindNode(%q): got error %s", remoteReadmeFile, err)
4949
}
5050
if want, got := inmd5, node.ContentProperties.MD5; want != got {
51-
t.Errorf("c.NodeTree.FindNode(%s).ContentProperties.MD5: want %s got %s", remoteReadmeFile, want, got)
51+
t.Errorf("c.NodeTree.FindNode(%q).ContentProperties.MD5: want %s got %s", remoteReadmeFile, want, got)
5252
}
5353

5454
// test the cache is being saved updated
@@ -62,10 +62,10 @@ func TestSimpleUpload(t *testing.T) {
6262
}
6363
node, err = c.NodeTree.FindNode(remoteReadmeFile)
6464
if err != nil {
65-
t.Errorf("reloaded cache, c.NodeTree.FindNode(%s): got error %s", remoteReadmeFile, err)
65+
t.Errorf("reloaded cache, c.NodeTree.FindNode(%q): got error %s", remoteReadmeFile, err)
6666
}
6767
if want, got := inmd5, node.ContentProperties.MD5; want != got {
68-
t.Errorf("reloaded cache, c.NodeTree.FindNode(%s).ContentProperties.MD5: want %s got %s", remoteReadmeFile, want, got)
68+
t.Errorf("reloaded cache, c.NodeTree.FindNode(%q).ContentProperties.MD5: want %s got %s", remoteReadmeFile, want, got)
6969
}
7070

7171
// check the file exists on the server

integrationtest/tree_sync_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package integrationtest
2+
3+
import (
4+
"crypto/md5"
5+
"encoding/hex"
6+
"io"
7+
"os"
8+
"testing"
9+
)
10+
11+
func TestTreeSync(t *testing.T) {
12+
if testing.Short() {
13+
t.Skip("skipping integration test")
14+
}
15+
needCleaning = true
16+
17+
var (
18+
readmeFile = "fixtures/syncfolder/README.syncfolder"
19+
remoteReadmeFile = remotePath(readmeFile)
20+
)
21+
22+
// create two cached clients
23+
c1, err := newCachedClient(true)
24+
if err != nil {
25+
t.Fatal(err)
26+
}
27+
if err := c1.FetchNodeTree(); err != nil {
28+
t.Fatal(err)
29+
}
30+
c2, err := newCachedClient(true)
31+
if err != nil {
32+
t.Fatal(err)
33+
}
34+
if err := c2.FetchNodeTree(); err != nil {
35+
t.Fatal(err)
36+
}
37+
38+
// using the first client upload the README file
39+
in, err := os.Open(readmeFile)
40+
if err != nil {
41+
t.Fatal(err)
42+
}
43+
inhash := md5.New()
44+
in.Seek(0, 0)
45+
io.Copy(inhash, in)
46+
inmd5 := hex.EncodeToString(inhash.Sum(nil))
47+
in.Seek(0, 0)
48+
if err := c1.Upload(remoteReadmeFile, in); err != nil {
49+
t.Errorf("error uploading %s to %s: %s", readmeFile, remoteReadmeFile, err)
50+
}
51+
52+
// using the second client, sync and find the node
53+
if err := c2.NodeTree.Sync(); err != nil {
54+
t.Fatal(err)
55+
}
56+
readmeNode, err := c2.NodeTree.FindNode(remoteReadmeFile)
57+
if err != nil {
58+
t.Fatalf("c2.NodeTree.FindNode(%q) error: %s", remoteReadmeFile, err)
59+
}
60+
if want, got := inmd5, readmeNode.ContentProperties.MD5; want != got {
61+
t.Errorf("c.NodeTree.FindNode(%q).ContentProperties.MD5: want %s got %s", remoteReadmeFile, want, got)
62+
}
63+
}

internal/constants/errors.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ var (
2525
// ErrJSONDecodingResponseBody is returned if there was an error decoding the
2626
// response body.
2727
ErrJSONDecodingResponseBody = errors.New("error while JSON-decoding the response body")
28+
// ErrReadingResponseBody is returned if ioutil.ReadAll() has failed.
29+
ErrReadingResponseBody = errors.New("error reading the entire response body")
2830

2931
// Request errors
3032

node/sync.go

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,32 @@
11
package node
22

33
import (
4-
"bufio"
54
"bytes"
65
"encoding/json"
6+
"io/ioutil"
77
"net/http"
8-
"reflect"
98
"sort"
109

1110
"gopkg.in/acd.v0/internal/constants"
1211
"gopkg.in/acd.v0/internal/log"
1312
)
1413

14+
type (
15+
changes struct {
16+
Checkpoint string `json:"checkpoint,omitempty"`
17+
Chunksize int `json:"chunkSize,omitempty"`
18+
MaxNodes int `json:"maxNodes,omitempty"`
19+
IncludePurged string `json:"includePurged,omitempty"`
20+
}
21+
22+
changesResponse struct {
23+
Checkpoint string `json:"checkpoint,omitempty"`
24+
Nodes []*Node `json:"nodes,omitempty"`
25+
Reset bool `json:"reset,omitempty"`
26+
End bool `json:"end,omitempty"`
27+
}
28+
)
29+
1530
// Sync syncs the tree with the server.
1631
func (nt *Tree) Sync() error {
1732
postURL := nt.client.GetMetadataURL("changes")
@@ -23,17 +38,11 @@ func (nt *Tree) Sync() error {
2338
log.Errorf("%s: %s", constants.ErrJSONEncoding, err)
2439
return constants.ErrJSONEncoding
2540
}
26-
27-
// return format should be:
28-
// {"checkpoint": str, "reset": bool, "nodes": []}
29-
// {"checkpoint": str, "reset": false, "nodes": []}
30-
// {"end": true}
3141
req, err := http.NewRequest("POST", postURL, bytes.NewBuffer(jsonBytes))
3242
if err != nil {
3343
log.Errorf("%s: %s", constants.ErrCreatingHTTPRequest, err)
3444
return constants.ErrCreatingHTTPRequest
3545
}
36-
3746
req.Header.Set("Content-Type", "application/json")
3847
res, err := nt.client.Do(req)
3948
if err != nil {
@@ -44,15 +53,26 @@ func (nt *Tree) Sync() error {
4453
return err
4554
}
4655

56+
// return format should be:
57+
// {"checkpoint": str, "reset": bool, "nodes": []}
58+
// {"checkpoint": str, "reset": false, "nodes": []}
59+
// {"end": true}
4760
defer res.Body.Close()
48-
s := bufio.NewScanner(res.Body)
49-
for s.Scan() {
61+
bodyBytes, err := ioutil.ReadAll(res.Body)
62+
if err != nil {
63+
log.Errorf("%s: %s", constants.ErrReadingResponseBody, err)
64+
return constants.ErrReadingResponseBody
65+
}
66+
for _, lineBytes := range bytes.Split(bodyBytes, []byte("\n")) {
5067
var cr changesResponse
51-
if err := json.Unmarshal(s.Bytes(), &cr); err != nil {
68+
if err := json.Unmarshal(lineBytes, &cr); err != nil {
5269
log.Errorf("%s: %s", constants.ErrJSONDecodingResponseBody, err)
5370
return constants.ErrJSONDecodingResponseBody
5471
}
55-
nt.Checkpoint = cr.Checkpoint
72+
if cr.Checkpoint != "" {
73+
log.Debugf("changes returned Checkpoint: %s", cr.Checkpoint)
74+
nt.Checkpoint = cr.Checkpoint
75+
}
5676
if cr.Reset {
5777
log.Debug("reset is required")
5878
return constants.ErrMustFetchFresh
@@ -71,8 +91,14 @@ func (nt *Tree) Sync() error {
7191
func (nt *Tree) updateNodes(nodes []*Node) error {
7292
// first make sure our nodeMap is up to date
7393
for _, node := range nodes {
94+
log.Debugf("node %s ID %s has changed.", node.Name, node.ID)
7495
if _, found := nt.nodeMap[node.ID]; !found {
75-
nt.nodeMap[node.ID] = node
96+
// remove the parents from the node we are inserting so the next section
97+
// will detect the added parents and add those.
98+
newNode := &Node{}
99+
(*newNode) = *node
100+
newNode.Parents = []string{}
101+
nt.nodeMap[node.ID] = newNode
76102
}
77103
}
78104

@@ -93,7 +119,7 @@ func (nt *Tree) updateNodes(nodes []*Node) error {
93119
if err != nil {
94120
continue
95121
}
96-
parent.RemoveChild(node)
122+
parent.RemoveChild(nt.nodeMap[node.ID])
97123
}
98124

99125
// remove the node itself from the nodemap
@@ -102,12 +128,28 @@ func (nt *Tree) updateNodes(nodes []*Node) error {
102128
continue
103129
}
104130

105-
// TODO: Handle change in parent IDs gracefully!
106-
sort.Strings(node.Parents)
131+
// add/remove parents
132+
sort.Strings(nt.nodeMap[node.ID].Parents)
107133
sort.Strings(newNode.Parents)
108-
if !reflect.DeepEqual(node.Parents, newNode.Parents) {
109-
log.Debugf("The parents of the node %s have changed. %v => %v", node.ID, node.Parents, newNode.Parents)
110-
return constants.ErrMustFetchFresh
134+
if parentIDs := diffSliceStr(nt.nodeMap[node.ID].Parents, newNode.Parents); len(parentIDs) > 0 {
135+
for _, parentID := range parentIDs {
136+
log.Debugf("ParentID %s has been removed from %s ID %s", parentID, node.Name, node.ID)
137+
parent, err := nt.FindByID(parentID)
138+
if err != nil {
139+
continue
140+
}
141+
parent.RemoveChild(nt.nodeMap[node.ID])
142+
}
143+
}
144+
if parentIDs := diffSliceStr(newNode.Parents, nt.nodeMap[node.ID].Parents); len(parentIDs) > 0 {
145+
for _, parentID := range parentIDs {
146+
log.Debugf("ParentID %s has been added to %s ID %s", parentID, node.Name, node.ID)
147+
parent, err := nt.FindByID(parentID)
148+
if err != nil {
149+
continue
150+
}
151+
parent.AddChild(nt.nodeMap[node.ID])
152+
}
111153
}
112154

113155
// finally update the node itself

node/tree.go

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,6 @@ type (
3131
NextToken string `json:"nextToken,omitempty"`
3232
Nodes []*Node `json:"data,omitempty"`
3333
}
34-
35-
changes struct {
36-
Checkpoint string `json:"checkpoint,omitempty"`
37-
Chunksize int `json:"chunkSize,omitempty"`
38-
MaxNodes int `json:"maxNodes,omitempty"`
39-
IncludePurged string `json:"includePurged,omitempty"`
40-
}
41-
42-
changesResponse struct {
43-
Checkpoint string `json:"checkpoint,omitempty"`
44-
Nodes []*Node `json:"nodes,omitempty"`
45-
Reset bool `json:"reset,omitempty"`
46-
End bool `json:"end,omitempty"`
47-
}
4834
)
4935

5036
// RemoveNode removes this node from the server and from the NodeTree.

node/util.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package node
2+
3+
// diff returns all of the strings that exist in s1 but do not exist in s2
4+
func diffSliceStr(s1, s2 []string) []string {
5+
var vs []string
6+
var found bool
7+
8+
for _, v1 := range s1 {
9+
found = false
10+
for _, v2 := range s2 {
11+
if v1 == v2 {
12+
found = true
13+
break
14+
}
15+
}
16+
17+
if !found {
18+
vs = append(vs, v1)
19+
}
20+
}
21+
22+
return vs
23+
}

node/util_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package node
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
)
7+
8+
func TestDiffSliceStr(t *testing.T) {
9+
type sliceString []string
10+
slices := [][][]string{
11+
[][]string{
12+
[]string{"a", "b", "c"},
13+
[]string{"a", "b"},
14+
},
15+
[][]string{
16+
[]string{"a", "b", "c"},
17+
[]string{"a", "b", "c"},
18+
},
19+
[][]string{
20+
[]string{"b", "c"},
21+
[]string{"a", "b", "c"},
22+
},
23+
}
24+
diffs := [][]string{
25+
[]string{"c"},
26+
[]string{},
27+
[]string{},
28+
}
29+
30+
for i, ss := range slices {
31+
want, got := diffs[i], diffSliceStr(ss[0], ss[1])
32+
33+
// when we get an empty slice, we are actually getting an uninitialized one
34+
// for reflect.DeepEqual an initialized and an uninitialized slices are not
35+
// equal so we must initialize got so it reflect does not bark
36+
if len(got) == 0 {
37+
got = make([]string, 0)
38+
}
39+
40+
if !reflect.DeepEqual(want, got) {
41+
t.Errorf("diffSliceStr(%v, %v): want %v, got %v", ss[0], ss[1], want, got)
42+
}
43+
}
44+
}

0 commit comments

Comments
 (0)