diff --git a/deltas.go b/deltas.go index d3036cb..b01ec78 100644 --- a/deltas.go +++ b/deltas.go @@ -35,16 +35,6 @@ func (ch *CaddyHugo) LTime() uint64 { } func (ch *CaddyHugo) ShouldApply(ltime uint64) bool { - lowest := ch.LowestPendingConfirmation() - for _, c := range ch.Confirming() { - if lowest == 0 || c < lowest { - lowest = c - } - } - - if ltime < lowest { - return false - } ch.mtx.Lock() defer ch.mtx.Unlock() @@ -56,12 +46,15 @@ func (ch *CaddyHugo) ShouldApply(ltime uint64) bool { return true } +// ConfirmLTime marks an ltime as something that should be confirmed with clients func (ch *CaddyHugo) ConfirmLTime(ltime uint64) { ch.mtx.Lock() defer ch.mtx.Unlock() ch.confirmingToClient[ltime] = struct{}{} } +// Confirming returns the current list of LTimes that need to be confirmed +// to clients func (ch *CaddyHugo) Confirming() []uint64 { var times []uint64 @@ -74,6 +67,8 @@ func (ch *CaddyHugo) Confirming() []uint64 { return times } +// LowestPendingConfirmation identifies the lowest LTime that needs to be +// confirmed to clients func (ch *CaddyHugo) LowestPendingConfirmation() uint64 { var lowest uint64 for _, c := range ch.Confirming() { @@ -181,10 +176,11 @@ func (ch *CaddyHugo) handleDeltaConn(conn DeltaConn, doc *editSession) (int, err errCh <- fmt.Errorf("error reading message from client conn: %v", err) return } - ch.ObserveLTime(message.LTime) + if message.LTime != 0 { + ch.ObserveLTime(message.LTime) + } if len(message.Deltas) == 0 { - time.Sleep(10 * time.Microsecond) continue } diff --git a/doc_test.go b/doc_test.go index fd72a33..5e34ae1 100644 --- a/doc_test.go +++ b/doc_test.go @@ -1,10 +1,6 @@ package caddyhugo import ( - "encoding/json" - "io/ioutil" - "os" - "os/exec" "path" "sync" "testing" @@ -13,136 +9,70 @@ import ( "git.stephensearles.com/stephen/acedoc" ) -type World struct { - CH *CaddyHugo - BlogFolder string -} - -func (w *World) Clean() { - if w.BlogFolder != "" { - os.RemoveAll(w.BlogFolder) - } -} - -func NewWorld(t *testing.T) *World { - dir, err := ioutil.TempDir("", "caddy-hugo2-test-") - if err != nil { - t.Fatalf("error initializing test environment: %v", err) - } - - w := &World{BlogFolder: dir} - - cmd := exec.Command("hugo", "new", "site", dir) - cmd.Dir = dir - out, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("error initializing test site: %v\n\n%v", err, string(out)) - } - - w.CH = &CaddyHugo{} - w.CH.Setup(dir) - - return w -} - func TestEdits(t *testing.T) { w := NewWorld(t) defer w.Clean() const title = "sometitle" - var contentPath = path.Join("content", title+".md") - w.CH.NewContent(title, "") + var ( + mtx sync.Mutex - send := []acedoc.Delta{ - acedoc.Insert(0, 0, "hello"), - acedoc.Insert(0, 5, " world"), - acedoc.Insert(0, 11, " world"), - } - var mtx sync.Mutex - received := []acedoc.Delta{} + contentPath = path.Join("content", title+".md") - doc, err := w.CH.editSession(contentPath) + // we will send these to the doc + send = []acedoc.Delta{ + acedoc.Insert(0, 0, "hello"), + acedoc.Insert(0, 5, " world"), + acedoc.Insert(0, 11, " world"), + } + + // we will use this to track what we get out of the doc + received = []acedoc.Delta{} + ) + + // prepare a new post + w.CH.NewContent(title, "") + + // start an edit session + es, err := w.CH.editSession(contentPath) if err != nil { t.Fatal("error creating document client:", err) } - doc.doc.Client(acedoc.DeltaHandlerFunc(func(ds []acedoc.Delta) error { - // receive some deltas... + // register a client that we will use to push the deltas. we dont need + // to actually do anything to receive deltas here, though. + c := es.doc.Client(acedoc.DeltaHandlerFunc(func(ds []acedoc.Delta) error { + return nil + })) + + // register an *extra* client that just adds to the received delta + // slice we're tracking + es.doc.Client(acedoc.DeltaHandlerFunc(func(ds []acedoc.Delta) error { mtx.Lock() defer mtx.Unlock() received = append(received, ds...) return nil })) + // make sure we have an edit session _, ok := w.CH.hasEditSession(contentPath) if !ok { t.Fatal("expected there to be an established client") } - doc.doc.Apply(send...) + // push the deltas + c.PushDeltas(send...) + // wait... <-time.After(5 * time.Second) + // be sure we got the correct number of deltas back mtx.Lock() defer mtx.Unlock() if len(received) != len(send) { t.Errorf("expected %d deltas, received %d; expected: %v, received: %v", len(send), len(received), send, received) } - -} - -type WebsocketTester struct { - receivedPointer int - received [][]byte - wroteMessages []Message - wroteDeltas []acedoc.Delta - mtx sync.Mutex -} - -func (ws *WebsocketTester) ReadJSON(v interface{}) error { - ws.mtx.Lock() - defer ws.mtx.Unlock() - - if len(ws.received) <= ws.receivedPointer { - return nil - } - - err := json.Unmarshal(ws.received[ws.receivedPointer], v) - ws.receivedPointer++ - return err -} - -func (ws *WebsocketTester) WriteJSON(v interface{}) error { - ws.mtx.Lock() - defer ws.mtx.Unlock() - - m, ok := v.(Message) - if !ok { - panic("wrong type written to WebsocketTester") - } - - if len(m.Deltas) == 0 { - return nil - } - - ws.wroteMessages = append(ws.wroteMessages, m) - ws.wroteDeltas = append(ws.wroteDeltas, m.Deltas...) - - return nil -} - -func (ws *WebsocketTester) ReceiveJSON(v interface{}) error { - ws.mtx.Lock() - defer ws.mtx.Unlock() - - out, err := json.Marshal(v) - if err != nil { - return err - } - - ws.received = append(ws.received, out) - return nil } func TestDeltasSingle(t *testing.T) { @@ -158,12 +88,12 @@ func TestDeltasSingle(t *testing.T) { client := new(WebsocketTester) - doc, err := w.CH.editSession("content/" + title + ".md") + es, err := w.CH.editSession("content/" + title + ".md") if err != nil { t.Fatal("couldn't establish docref for client 0:", err) } - go w.CH.handleDeltaConn(client, doc) + go w.CH.handleDeltaConn(client, es) a := acedoc.Insert(0, 0, "a") @@ -174,9 +104,11 @@ func TestDeltasSingle(t *testing.T) { time.Sleep(50 * time.Millisecond) // we shouldn't have written back to the client, - // so we expect to have written 0 messages - if len(client.wroteMessages) != 0 { - t.Errorf("client wrote %d messages, should have written %d", len(client.wroteMessages), 0) + // so we expect to have written 0 *deltas*. (we may have written + // empty messages without deltas because of the pings to the client) + if len(client.wroteDeltas) != 0 { + t.Errorf("client wrote %d deltas, should have written %d", len(client.wroteMessages), 0) + t.Logf("%v", client.wroteMessages) } // we received one, so make sure that's counted properly @@ -217,7 +149,7 @@ func TestDeltasDouble(t *testing.T) { // so we expect clientA to have written 0 messages, and // clientB to have written 1 - if len(clientA.wroteMessages) != 0 || len(clientB.wroteMessages) != 1 { + if len(clientA.wroteDeltas) != 0 || len(clientB.wroteDeltas) != 1 { t.Errorf("clientA wrote %d messages, should have written 0. clientB wrote %d, should have written 1", len(clientA.wroteMessages), len(clientB.wroteMessages)) } @@ -238,9 +170,9 @@ func TestDeltasDouble(t *testing.T) { clientA.mtx.Lock() clientB.mtx.Lock() - // so we expect clientA to have written 1 message this time, and + // so we expect clientA to have written 1 delta this time, and // clientB to have written nothing new, so 1 still - if len(clientA.wroteMessages) != 1 || len(clientB.wroteMessages) != 1 { + if len(clientA.wroteDeltas) != 1 || len(clientB.wroteDeltas) != 1 { t.Errorf("clientA wrote %d messages, should have written 1. clientB wrote %d, should have written 1 (just from before)", len(clientA.wroteMessages), len(clientB.wroteMessages)) } @@ -268,13 +200,15 @@ func TestDeltasMulti(t *testing.T) { doc, err := w.CH.editSession("content/" + title + ".md") if err != nil { - t.Fatal("couldn't establish docref:", err) + t.Fatal("couldn't establish edit session:", err) } go w.CH.handleDeltaConn(clients[0], doc) go w.CH.handleDeltaConn(clients[1], doc) go w.CH.handleDeltaConn(clients[2], doc) + time.Sleep(100 * time.Millisecond) + a := acedoc.Insert(0, 0, "a") b := acedoc.Insert(0, 0, "b") c := acedoc.Insert(0, 0, "c") @@ -283,12 +217,12 @@ func TestDeltasMulti(t *testing.T) { clients[1].ReceiveJSON(w.CH.Message(b)) clients[2].ReceiveJSON(w.CH.Message(c)) - time.Sleep(400 * time.Millisecond) + time.Sleep(1000 * time.Millisecond) for i, client := range clients { client.mtx.Lock() - // all clients should have "written" 2 deltas (could be the same - // message) that came from the other clients + // all clients should have "written" 2 deltas out to their "browser" + // that came from the other clients if len(client.wroteDeltas) != 2 { t.Errorf("client %d wrote %d deltas, should have written 2", i, len(client.wroteDeltas)) } diff --git a/helpers_test.go b/helpers_test.go new file mode 100644 index 0000000..d26f5a0 --- /dev/null +++ b/helpers_test.go @@ -0,0 +1,96 @@ +package caddyhugo + +import ( + "encoding/json" + "io/ioutil" + "os" + "os/exec" + "sync" + "testing" + + "git.stephensearles.com/stephen/acedoc" +) + +type World struct { + CH *CaddyHugo + BlogFolder string +} + +func (w *World) Clean() { + if w.BlogFolder != "" { + os.RemoveAll(w.BlogFolder) + } +} + +func NewWorld(t *testing.T) *World { + dir, err := ioutil.TempDir("", "caddy-hugo2-test-") + if err != nil { + t.Fatalf("error initializing test environment: %v", err) + } + + w := &World{BlogFolder: dir} + + cmd := exec.Command("hugo", "new", "site", dir) + cmd.Dir = dir + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("error initializing test site: %v\n\n%v", err, string(out)) + } + + w.CH = &CaddyHugo{} + w.CH.Setup(dir) + + return w +} + +type WebsocketTester struct { + receivedPointer int + received [][]byte + wroteMessages []Message + wroteDeltas []acedoc.Delta + mtx sync.Mutex +} + +// ReadJSON reads the next pending message from the "client" into v +func (ws *WebsocketTester) ReadJSON(v interface{}) error { + ws.mtx.Lock() + defer ws.mtx.Unlock() + + if len(ws.received) <= ws.receivedPointer { + return nil + } + + err := json.Unmarshal(ws.received[ws.receivedPointer], v) + ws.receivedPointer++ + return err +} + +// WriteJSON "sends" a message, v, to the "client" +func (ws *WebsocketTester) WriteJSON(v interface{}) error { + ws.mtx.Lock() + defer ws.mtx.Unlock() + + m, ok := v.(Message) + if !ok { + panic("wrong type written to WebsocketTester") + } + + ws.wroteMessages = append(ws.wroteMessages, m) + ws.wroteDeltas = append(ws.wroteDeltas, m.Deltas...) + + return nil +} + +// ReceiveJSON queues a message to be sent to the client +func (ws *WebsocketTester) ReceiveJSON(v interface{}) error { + ws.mtx.Lock() + defer ws.mtx.Unlock() + + out, err := json.Marshal(v) + if err != nil { + return err + } + + ws.received = append(ws.received, out) + return nil +} diff --git a/hugo.go b/hugo.go index 4d6e491..b5e6434 100644 --- a/hugo.go +++ b/hugo.go @@ -52,7 +52,6 @@ func writeThemeFiles(dir string) error { if err != nil { return err } - fmt.Println("writing", path.Join(dir, asset)) f, err := os.Create(path.Join(dir, asset)) if err != nil { return err