From 28310b0dd6263b724f2141d0186c2638677fa411 Mon Sep 17 00:00:00 2001 From: Bel LaPointe Date: Thu, 28 Feb 2019 13:55:20 -0700 Subject: [PATCH] this is fun --- mytinytodo/buffer.go | 122 +++++++++++++++++++++++++++--- mytinytodo/buffer_test.go | 59 +++++++++++++++ mytinytodo/config.go | 11 ++- mytinytodo/packable.go | 56 ++++++++++++++ mytinytodo/packable_test.go | 62 ++++++++++++++++ mytinytodo/remote/client.go | 35 +++++---- mytinytodo/remote/client_test.go | 123 +++++++++++++++++++++++++++++++ mytinytodo/remote/config_test.go | 25 +++++++ mytinytodo/remote/http_test.go | 95 ++++++++++++++++++++++++ mytinytodo/remote/list.go | 1 - mytinytodo/remote/op.go | 36 +++++++++ mytinytodo/remote/task.go | 1 - 12 files changed, 593 insertions(+), 33 deletions(-) create mode 100644 mytinytodo/buffer_test.go create mode 100644 mytinytodo/packable.go create mode 100644 mytinytodo/packable_test.go create mode 100644 mytinytodo/remote/client_test.go create mode 100644 mytinytodo/remote/config_test.go create mode 100644 mytinytodo/remote/http_test.go create mode 100644 mytinytodo/remote/op.go diff --git a/mytinytodo/buffer.go b/mytinytodo/buffer.go index 09918ee..39e26c6 100644 --- a/mytinytodo/buffer.go +++ b/mytinytodo/buffer.go @@ -3,21 +3,121 @@ package mytinytodo import ( "local/mytinytodoclient/mytinytodo/remote" "local/rproxy3/storage" + "log" + "sync" + "time" + + "github.com/google/uuid" ) +const nsQueue = "delta" +const keyQueue = "queue" +const nsTasks = "delta" + type Buffer struct { - remote *remote.Client - db storage.DB + config *Config + db storage.DB + dbLock *sync.RWMutex + done chan struct{} + interval time.Duration } -func NewBuffer(config *remote.Config) (*Buffer, error) { - remote, err := remote.NewClient(config) - if err != nil { - return nil, err - } +func NewBuffer(config *Config) (*Buffer, error) { db := storage.NewMap() - return &Buffer{ - remote: remote, - db: db, - }, nil + b := &Buffer{ + config: config, + db: db, + done: make(chan struct{}), + dbLock: &sync.RWMutex{}, + interval: time.Second * 10, + } + go b.Dequeue() + go b.RefreshLocal() + return b, nil +} + +func (buffer *Buffer) Close() { + close(buffer.done) +} + +func (buffer *Buffer) Enqueue(op remote.Op, listID, taskName string, taskTags ...string) error { + buffer.dbLock.Lock() + defer buffer.dbLock.Unlock() + qop := &QueuedOp{ + Op: op, + ListID: listID, + TaskName: taskName, + TaskTags: taskTags, + } + uuid, _ := uuid.NewRandom() + key := uuid.String() + todo := NewStringArray() + if err := buffer.db.Get(nsQueue, keyQueue, todo); err != nil { + return err + } + sa := todo.StringArray() + sa = append(sa, key) + todo = NewStringArray(sa...) + if err := buffer.db.Set(nsTasks, key, qop); err != nil { + return err + } + if err := buffer.db.Set(nsQueue, keyQueue, todo); err != nil { + return err + } + log.Printf("enqueued task %v as %v, %vth in line", qop, key, len(sa)) + return nil +} + +func (buffer *Buffer) Dequeue() { + buffer.notDoneCallback(func() { + client, err := remote.NewClient(buffer.config.Config) + if err != nil { + log.Printf("cannot create client: %v", err) + return + } + if _, err := client.Lists(); err != nil { + log.Printf("cannot client.lists: %v", err) + return + } + buffer.dbLock.Lock() + defer buffer.dbLock.Unlock() + todo := NewStringArray() + if err := buffer.db.Get(nsQueue, keyQueue, todo); err != nil { + log.Printf("cannot get %v.%v: %v", nsQueue, keyQueue, err) + return + } + sa := todo.StringArray() + nsa := []string{} + for i := range sa { + qop := &QueuedOp{} + if err := buffer.db.Get(nsTasks, sa[i], qop); err != nil { + log.Printf("cannot get %v.%v: %v", nsTasks, sa[i], err) + nsa = append(nsa, sa[i]) + continue + } + log.Printf("UPSERT %v", qop) + } + if err := buffer.db.Set(nsQueue, keyQueue, NewStringArray(nsa...)); err != nil { + log.Printf("cannot update queue: %v", err) + return + } + }) +} + +func (buffer *Buffer) RefreshLocal() { + buffer.notDoneCallback(func() { + buffer.dbLock.Lock() + defer buffer.dbLock.Unlock() + }) +} + +func (buffer *Buffer) notDoneCallback(foo func()) { + for { + select { + case <-buffer.done: + return + case <-time.After(buffer.interval): + foo() + } + } } diff --git a/mytinytodo/buffer_test.go b/mytinytodo/buffer_test.go new file mode 100644 index 0000000..e711fb0 --- /dev/null +++ b/mytinytodo/buffer_test.go @@ -0,0 +1,59 @@ +package mytinytodo + +import ( + "net/http" + "net/http/httptest" + "os" + "testing" + "time" +) + +func TestNewBuffer(t *testing.T) { + b, s := mockBuffer(t) + defer b.Close() + defer s.Close() +} + +func TestBufferEnqueue(t *testing.T) { + t.Fatal("not implemented") +} + +func TestBufferDequeue(t *testing.T) { + t.Fatal("not implemented") +} + +func TestBufferRefreshLocal(t *testing.T) { + t.Fatal("not implemented") +} + +func TestBufferNotDoneCallback(t *testing.T) { + b := &Buffer{ + done: make(chan struct{}), + interval: time.Millisecond * 70, + } + called := make(chan struct{}) + go b.notDoneCallback(func() { + called <- struct{}{} + }) + <-called + close(b.done) +} + +func mockBuffer(t *testing.T) (*Buffer, *httptest.Server) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + })) + osArgsWas := os.Args[:] + os.Args = []string{"skip", "-remote", srv.URL, "-p", ""} + defer func() { + os.Args = osArgsWas[:] + }() + config, err := NewConfig() + if err != nil { + t.Fatalf("cannot get new config: %v", err) + } + buff, err := NewBuffer(config) + if err != nil { + t.Fatalf("cannot get new buffer: %v", err) + } + return buff, srv +} diff --git a/mytinytodo/config.go b/mytinytodo/config.go index e7d45f8..3b254c3 100644 --- a/mytinytodo/config.go +++ b/mytinytodo/config.go @@ -2,6 +2,13 @@ package mytinytodo import "local/mytinytodoclient/mytinytodo/remote" -func NewConfig() (*remote.Config, error) { - return remote.NewConfig() +type Config struct { + *remote.Config +} + +func NewConfig() (*Config, error) { + c, err := remote.NewConfig() + return &Config{ + Config: c, + }, err } diff --git a/mytinytodo/packable.go b/mytinytodo/packable.go new file mode 100644 index 0000000..1429b15 --- /dev/null +++ b/mytinytodo/packable.go @@ -0,0 +1,56 @@ +package mytinytodo + +import ( + "bytes" + "encoding/gob" + "local/mytinytodoclient/mytinytodo/remote" +) + +type StringArray []string + +func NewStringArray(s ...string) *StringArray { + arr := s[:] + var sa StringArray = arr + return &sa +} + +func (sa *StringArray) StringArray() []string { + return []string(*sa) +} + +func (sa *StringArray) Encode() ([]byte, error) { + buf := bytes.NewBuffer(nil) + enc := gob.NewEncoder(buf) + err := enc.Encode(sa.StringArray()) + return buf.Bytes(), err +} + +func (sa *StringArray) Decode(b []byte) error { + buf := bytes.NewBuffer(b) + enc := gob.NewDecoder(buf) + var s []string + err := enc.Decode(&s) + *sa = StringArray(s) + return err +} + +type QueuedOp struct { + Op remote.Op + ListID string + TaskName string + TaskTags []string +} + +func (qop *QueuedOp) Encode() ([]byte, error) { + buf := bytes.NewBuffer(nil) + enc := gob.NewEncoder(buf) + err := enc.Encode(*qop) + return buf.Bytes(), err +} + +func (qop *QueuedOp) Decode(b []byte) error { + buf := bytes.NewBuffer(b) + enc := gob.NewDecoder(buf) + err := enc.Decode(qop) + return err +} diff --git a/mytinytodo/packable_test.go b/mytinytodo/packable_test.go new file mode 100644 index 0000000..b6f91a7 --- /dev/null +++ b/mytinytodo/packable_test.go @@ -0,0 +1,62 @@ +package mytinytodo + +import ( + "fmt" + "local/mytinytodoclient/mytinytodo/remote" + "testing" +) + +func TestPackableStringArray(t *testing.T) { + cases := [][]string{ + nil, + []string{}, + []string{"a"}, + []string{"a", "b"}, + } + + for i, c := range cases { + sa := NewStringArray(c...) + if fmt.Sprintf("%v", sa.StringArray()) != fmt.Sprintf("%v", c) { + t.Errorf("[%d]: NewStringArray() != input: got %v, want %v", i, sa, c) + } + encoded, err := sa.Encode() + if err != nil { + t.Errorf("[%d]: sa.Encode() error: %v", i, err) + } + sa2 := NewStringArray() + if err := sa2.Decode(encoded); err != nil { + t.Errorf("[%d]: sa2.Decode() error: %v", i, err) + } + if fmt.Sprintf("%v", sa2.StringArray()) != fmt.Sprintf("%v", c) { + t.Errorf("[%d]: Decoded != input: got %v, want %v", i, sa2, c) + } + t.Logf("[%d] sa: %v vs %v", i, *sa, *sa2) + } +} + +func TestPackableQueuedOp(t *testing.T) { + cases := []*QueuedOp{ + &QueuedOp{ + Op: remote.NEW, + ListID: "1", + TaskName: "name", + TaskTags: []string{"some", "tags"}, + }, + &QueuedOp{}, + } + + for i, c := range cases { + encoded, err := c.Encode() + if err != nil { + t.Errorf("[%d]: qop.Encode() error: %v", i, err) + } + qop := &QueuedOp{} + if err := qop.Decode(encoded); err != nil { + t.Errorf("[%d]: qop.Decode() error: %v", i, err) + } + if fmt.Sprintf("%v", *qop) != fmt.Sprintf("%v", *c) { + t.Errorf("[%d]: Decoded != input: got %v, want %v", i, *qop, *c) + } + t.Logf("[%d] qop: %v vs %v", i, *qop, *c) + } +} diff --git a/mytinytodo/remote/client.go b/mytinytodo/remote/client.go index a27d9f1..20619cd 100644 --- a/mytinytodo/remote/client.go +++ b/mytinytodo/remote/client.go @@ -9,9 +9,8 @@ import ( ) type Client struct { - config *Config - http *http.Client - session *http.Cookie + config *Config + http *http.Client } func NewClient(config *Config) (*Client, error) { @@ -23,14 +22,14 @@ func NewClient(config *Config) (*Client, error) { func (c *Client) ParseArgs() error { for i := 0; i < len(c.config.args); i++ { arg := c.config.args[i] - switch arg { - case "list": + switch fromString(arg) { + case LISTS: log.Printf("lists: %v", fmt.Sprint(c.Lists())) - case "tasks": + case TASKS: listID := c.config.args[i+1] i += 1 log.Printf("tasks: %v", fmt.Sprint(c.Tasks(List{ID: listID}))) - case "new": + case NEW: listID := c.config.args[i+1] i += 1 taskTitle := c.config.args[i+1] @@ -38,14 +37,14 @@ func (c *Client) ParseArgs() error { tagsCSV := c.config.args[i+1] i += 1 log.Printf("new: %v", fmt.Sprint(c.NewTask(List{ID: listID}, Task{Title: taskTitle}, tagsCSV))) - case "close": - taskID := c.config.args[i+1] - i += 1 - log.Printf("close: %v", fmt.Sprint(c.CloseTask(Task{ID: taskID}))) - case "open": + case OPEN: taskID := c.config.args[i+1] i += 1 log.Printf("open: %v", fmt.Sprint(c.OpenTask(Task{ID: taskID}))) + case CLOSE: + taskID := c.config.args[i+1] + i += 1 + log.Printf("close: %v", fmt.Sprint(c.CloseTask(Task{ID: taskID}))) default: log.Printf("unknown arg %q", arg) } @@ -82,7 +81,6 @@ func (c *Client) Tasks(list List) ([]Task, error) { } func (c *Client) NewTask(list List, task Task, tags string) error { - log.Printf("new: %v < %v", list, task.Title) client, err := NewHTTP(c.config.remote, c.config.password) if err != nil { return err @@ -96,13 +94,13 @@ func (c *Client) NewTask(list List, task Task, tags string) error { return err } else if err := json.NewDecoder(resp.Body).Decode(&lists); err != nil { return fmt.Errorf("cannot make task: %v", err) + } else if resp.StatusCode != http.StatusOK { + return fmt.Errorf("non 200 status on new task: %v", resp.StatusCode) } - log.Print(lists) return nil } func (c *Client) CloseTask(task Task) error { - log.Printf("close: %v", task.ID) client, err := NewHTTP(c.config.remote, c.config.password) if err != nil { return err @@ -115,13 +113,13 @@ func (c *Client) CloseTask(task Task) error { return err } else if err := json.NewDecoder(resp.Body).Decode(&lists); err != nil { return fmt.Errorf("cannot close task: %v", err) + } else if resp.StatusCode != http.StatusOK { + return fmt.Errorf("non 200 status on close task: %v", resp.StatusCode) } - log.Print(lists) return nil } func (c *Client) OpenTask(task Task) error { - log.Printf("open: %v", task.ID) client, err := NewHTTP(c.config.remote, c.config.password) if err != nil { return err @@ -134,7 +132,8 @@ func (c *Client) OpenTask(task Task) error { return err } else if err := json.NewDecoder(resp.Body).Decode(&lists); err != nil { return fmt.Errorf("cannot close task: %v", err) + } else if resp.StatusCode != http.StatusOK { + return fmt.Errorf("non 200 status on open task: %v", resp.StatusCode) } - log.Print(lists) return nil } diff --git a/mytinytodo/remote/client_test.go b/mytinytodo/remote/client_test.go new file mode 100644 index 0000000..12dcf40 --- /dev/null +++ b/mytinytodo/remote/client_test.go @@ -0,0 +1,123 @@ +package remote + +import ( + "errors" + "net/http" + "net/http/httptest" + "testing" +) + +func mockClient(code int, body string) (*Client, *httptest.Server) { + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(code) + w.Write([]byte(body)) + })) + return &Client{ + config: &Config{ + remote: s.URL, + password: "", + args: []string{}, + }, + http: &http.Client{}, + }, s +} + +func TestClientLists(t *testing.T) { + c, srv := mockClient(200, `{"list":[{"id":"1", "name":"name"}]}`) + defer srv.Close() + + if l, err := c.Lists(); err != nil { + t.Fatalf("cannot lists(): %v", err) + } else if l[0] != (List{ID: "1", Name: "name"}) { + t.Fatalf("cannot parse lists(): %v", l[0]) + } +} + +func TestClientTasks(t *testing.T) { + c, srv := mockClient(200, `{"list":[{"id":"1", "title":"title", "compl": 1}]}`) + defer srv.Close() + + if l, err := c.Tasks(List{}); err != nil { + t.Fatalf("cannot tasks(): %v", err) + } else if l[0] != (Task{ID: "1", Title: "title", Complete: 1}) { + t.Fatalf("cannot parse tasks(): %v", l[0]) + } +} + +func TestClientNewTask(t *testing.T) { + cases := []struct { + status int + err error + }{ + { + status: http.StatusOK, + err: nil, + }, + { + status: http.StatusNotFound, + err: errors.New("non 200 status on new task: 404"), + }, + } + + for i, c := range cases { + cli, srv := mockClient(c.status, `{}`) + defer srv.Close() + + err := cli.NewTask(List{ID: "1"}, Task{Title: "a"}, "a,tag") + if (err != nil && c.err == nil) || (err == nil && c.err != nil) { + t.Errorf("[%d] new task unexpected error: got %v, want %v", i, err, c.err) + } + } +} + +func TestClientCloseTask(t *testing.T) { + cases := []struct { + status int + err error + }{ + { + status: http.StatusOK, + err: nil, + }, + { + status: http.StatusNotFound, + err: errors.New("non 200 status on close task: 404"), + }, + } + + for i, c := range cases { + cli, srv := mockClient(c.status, `{}`) + defer srv.Close() + + err := cli.CloseTask(Task{ID: "1"}) + if (err != nil && c.err == nil) || (err == nil && c.err != nil) { + t.Errorf("[%d] new task unexpected error: got %v, want %v", i, err, c.err) + } + } +} + +func TestClientOpenTask(t *testing.T) { + cases := []struct { + status int + err error + }{ + { + status: http.StatusOK, + err: nil, + }, + { + status: http.StatusNotFound, + err: errors.New("non 200 status on open task: 404"), + }, + } + + for i, c := range cases { + cli, srv := mockClient(c.status, `{}`) + defer srv.Close() + + err := cli.OpenTask(Task{ID: "1"}) + if (err != nil && c.err == nil) || (err == nil && c.err != nil) { + t.Errorf("[%d] new task unexpected error: got %v, want %v", i, err, c.err) + } + } +} diff --git a/mytinytodo/remote/config_test.go b/mytinytodo/remote/config_test.go new file mode 100644 index 0000000..113912f --- /dev/null +++ b/mytinytodo/remote/config_test.go @@ -0,0 +1,25 @@ +package remote + +import ( + "os" + "testing" +) + +func TestNewConfig(t *testing.T) { + osArgsWas := os.Args[:] + defer func() { + os.Args = osArgsWas[:] + }() + + os.Args = []string{"ignore", "-remote", "remotename", "-p", "password", "other"} + + if conf, err := NewConfig(); err != nil { + t.Errorf("new config failed: %v", err) + } else if conf.remote != "remotename" { + t.Errorf("new config no remote flag: %v", conf.remote) + } else if conf.password != "password" { + t.Errorf("new config no password flag: %v", conf.password) + } else if conf.args[0] != "other" { + t.Errorf("new config no other args: %v", conf.args) + } +} diff --git a/mytinytodo/remote/http_test.go b/mytinytodo/remote/http_test.go new file mode 100644 index 0000000..adf143f --- /dev/null +++ b/mytinytodo/remote/http_test.go @@ -0,0 +1,95 @@ +package remote + +import ( + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" +) + +func mockHTTP(status int, response string) (*HTTP, *httptest.Server) { + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(status) + w.Write([]byte(response)) + })) + return &HTTP{ + password: "", + domain: s.URL, + client: &http.Client{}, + }, s +} + +func TestNewHTTP(t *testing.T) { + h, srv := mockHTTP(http.StatusOK, "") + defer srv.Close() + if _, err := NewHTTP(h.domain, ""); err != nil { + t.Fatalf("cannot make new HTTP: %v", err) + } + + h, srv = mockHTTP(http.StatusNotFound, "") + defer srv.Close() + if _, err := NewHTTP(h.domain, ""); err == nil { + t.Fatalf("can make new HTTP on bad status: %v", err) + } +} + +func TestHTTPGet(t *testing.T) { + validBody := "body" + h, srv := mockHTTP(http.StatusOK, validBody) + defer srv.Close() + resp, err := h.Get("/anything") + if err != nil { + t.Fatalf("cannot use HTTP GET: %v", err) + } + if resp.StatusCode != http.StatusOK { + t.Errorf("cannot use HTTP GET: %v", resp.StatusCode) + } +} + +func TestHTTPPost(t *testing.T) { + validBody := "body" + h, srv := mockHTTP(http.StatusOK, validBody) + defer srv.Close() + resp, err := h.Post("/anything", validBody) + if err != nil { + t.Fatalf("cannot use HTTP GET: %v", err) + } + if resp.StatusCode != http.StatusOK { + t.Errorf("cannot use HTTP GET: %v", resp.StatusCode) + } +} + +func TestHTTPNewReq(t *testing.T) { + cases := []struct { + method string + path string + body string + }{ + { + method: "GET", + path: "/", + body: "", + }, + { + method: "POST", + path: "/asdf/asdf", + body: "bod", + }, + } + + h := &HTTP{} + for i, c := range cases { + r, _ := h.NewReq(c.method, c.path, c.body) + if r.Method != c.method { + t.Errorf("[%d] wrong method on newreq: got %v, want %v", i, r.Method, c.method) + } + if r.URL.Path != c.path { + t.Errorf("[%d] wrong path on newreq: got %v, want %v", i, r.URL.Path, c.path) + } + if b, err := ioutil.ReadAll(r.Body); err != nil { + t.Errorf("[%d] bad body on newreq: %v", i, err) + } else if string(b) != c.body { + t.Errorf("[%d] wrong body on newreq: got %v, want %v", i, string(b), c.body) + } + } +} diff --git a/mytinytodo/remote/list.go b/mytinytodo/remote/list.go index 8969930..2bb9077 100644 --- a/mytinytodo/remote/list.go +++ b/mytinytodo/remote/list.go @@ -1,7 +1,6 @@ package remote type loadListsResponse struct { - Total int `json:"total"` Lists []List `json:"list"` } diff --git a/mytinytodo/remote/op.go b/mytinytodo/remote/op.go new file mode 100644 index 0000000..e03856f --- /dev/null +++ b/mytinytodo/remote/op.go @@ -0,0 +1,36 @@ +package remote + +type Op int + +const ( + NEW Op = iota + CLOSE Op = iota + OPEN Op = iota + LISTS Op = iota + TASKS Op = iota +) + +func (op Op) String() string { + switch op { + case LISTS: + return "lists" + case TASKS: + return "tasks" + case NEW: + return "new" + case CLOSE: + return "close" + case OPEN: + return "open" + } + return "" +} + +func fromString(s string) Op { + for i := 0; i < 20; i++ { + if Op(i).String() == s { + return Op(i) + } + } + return Op(-1) +} diff --git a/mytinytodo/remote/task.go b/mytinytodo/remote/task.go index 63bbc33..06fe6f7 100644 --- a/mytinytodo/remote/task.go +++ b/mytinytodo/remote/task.go @@ -1,7 +1,6 @@ package remote type loadTasksResponse struct { - Total int `json:"total"` Tasks []Task `json:"list"` }