fixing tests and some subtle issues with saving contents #16
@@ -35,16 +35,6 @@ func (ch *CaddyHugo) LTime() uint64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ch *CaddyHugo) ShouldApply(ltime uint64) bool {
|
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()
|
ch.mtx.Lock()
|
||||||
defer ch.mtx.Unlock()
|
defer ch.mtx.Unlock()
|
||||||
@@ -56,12 +46,15 @@ func (ch *CaddyHugo) ShouldApply(ltime uint64) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ConfirmLTime marks an ltime as something that should be confirmed with clients
|
||||||
func (ch *CaddyHugo) ConfirmLTime(ltime uint64) {
|
func (ch *CaddyHugo) ConfirmLTime(ltime uint64) {
|
||||||
ch.mtx.Lock()
|
ch.mtx.Lock()
|
||||||
defer ch.mtx.Unlock()
|
defer ch.mtx.Unlock()
|
||||||
ch.confirmingToClient[ltime] = struct{}{}
|
ch.confirmingToClient[ltime] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Confirming returns the current list of LTimes that need to be confirmed
|
||||||
|
// to clients
|
||||||
func (ch *CaddyHugo) Confirming() []uint64 {
|
func (ch *CaddyHugo) Confirming() []uint64 {
|
||||||
var times []uint64
|
var times []uint64
|
||||||
|
|
||||||
@@ -74,6 +67,8 @@ func (ch *CaddyHugo) Confirming() []uint64 {
|
|||||||
return times
|
return times
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LowestPendingConfirmation identifies the lowest LTime that needs to be
|
||||||
|
// confirmed to clients
|
||||||
func (ch *CaddyHugo) LowestPendingConfirmation() uint64 {
|
func (ch *CaddyHugo) LowestPendingConfirmation() uint64 {
|
||||||
var lowest uint64
|
var lowest uint64
|
||||||
for _, c := range ch.Confirming() {
|
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)
|
errCh <- fmt.Errorf("error reading message from client conn: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ch.ObserveLTime(message.LTime)
|
if message.LTime != 0 {
|
||||||
|
ch.ObserveLTime(message.LTime)
|
||||||
|
}
|
||||||
|
|
||||||
if len(message.Deltas) == 0 {
|
if len(message.Deltas) == 0 {
|
||||||
time.Sleep(10 * time.Microsecond)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+49
-115
@@ -1,10 +1,6 @@
|
|||||||
package caddyhugo
|
package caddyhugo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path"
|
"path"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -13,136 +9,70 @@ import (
|
|||||||
"git.stephensearles.com/stephen/acedoc"
|
"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) {
|
func TestEdits(t *testing.T) {
|
||||||
w := NewWorld(t)
|
w := NewWorld(t)
|
||||||
defer w.Clean()
|
defer w.Clean()
|
||||||
|
|
||||||
const title = "sometitle"
|
const title = "sometitle"
|
||||||
var contentPath = path.Join("content", title+".md")
|
|
||||||
|
|
||||||
|
var (
|
||||||
|
mtx sync.Mutex
|
||||||
|
|
||||||
|
contentPath = path.Join("content", title+".md")
|
||||||
|
|
||||||
|
// 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, "")
|
w.CH.NewContent(title, "")
|
||||||
|
|
||||||
send := []acedoc.Delta{
|
// start an edit session
|
||||||
acedoc.Insert(0, 0, "hello"),
|
es, err := w.CH.editSession(contentPath)
|
||||||
acedoc.Insert(0, 5, " world"),
|
|
||||||
acedoc.Insert(0, 11, " world"),
|
|
||||||
}
|
|
||||||
var mtx sync.Mutex
|
|
||||||
received := []acedoc.Delta{}
|
|
||||||
|
|
||||||
doc, err := w.CH.editSession(contentPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("error creating document client:", err)
|
t.Fatal("error creating document client:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
doc.doc.Client(acedoc.DeltaHandlerFunc(func(ds []acedoc.Delta) error {
|
// register a client that we will use to push the deltas. we dont need
|
||||||
// receive some deltas...
|
// 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()
|
mtx.Lock()
|
||||||
defer mtx.Unlock()
|
defer mtx.Unlock()
|
||||||
received = append(received, ds...)
|
received = append(received, ds...)
|
||||||
return nil
|
return nil
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// make sure we have an edit session
|
||||||
_, ok := w.CH.hasEditSession(contentPath)
|
_, ok := w.CH.hasEditSession(contentPath)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("expected there to be an established client")
|
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)
|
<-time.After(5 * time.Second)
|
||||||
|
|
||||||
|
// be sure we got the correct number of deltas back
|
||||||
mtx.Lock()
|
mtx.Lock()
|
||||||
defer mtx.Unlock()
|
defer mtx.Unlock()
|
||||||
if len(received) != len(send) {
|
if len(received) != len(send) {
|
||||||
t.Errorf("expected %d deltas, received %d; expected: %v, received: %v", len(send), len(received), send, received)
|
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) {
|
func TestDeltasSingle(t *testing.T) {
|
||||||
@@ -158,12 +88,12 @@ func TestDeltasSingle(t *testing.T) {
|
|||||||
|
|
||||||
client := new(WebsocketTester)
|
client := new(WebsocketTester)
|
||||||
|
|
||||||
doc, err := w.CH.editSession("content/" + title + ".md")
|
es, err := w.CH.editSession("content/" + title + ".md")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("couldn't establish docref for client 0:", err)
|
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")
|
a := acedoc.Insert(0, 0, "a")
|
||||||
|
|
||||||
@@ -174,9 +104,11 @@ func TestDeltasSingle(t *testing.T) {
|
|||||||
time.Sleep(50 * time.Millisecond)
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
// we shouldn't have written back to the client,
|
// we shouldn't have written back to the client,
|
||||||
// so we expect to have written 0 messages
|
// so we expect to have written 0 *deltas*. (we may have written
|
||||||
if len(client.wroteMessages) != 0 {
|
// empty messages without deltas because of the pings to the client)
|
||||||
t.Errorf("client wrote %d messages, should have written %d", len(client.wroteMessages), 0)
|
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
|
// 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
|
// so we expect clientA to have written 0 messages, and
|
||||||
// clientB to have written 1
|
// 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))
|
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()
|
clientA.mtx.Lock()
|
||||||
clientB.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
|
// 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))
|
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")
|
doc, err := w.CH.editSession("content/" + title + ".md")
|
||||||
if err != nil {
|
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[0], doc)
|
||||||
go w.CH.handleDeltaConn(clients[1], doc)
|
go w.CH.handleDeltaConn(clients[1], doc)
|
||||||
go w.CH.handleDeltaConn(clients[2], doc)
|
go w.CH.handleDeltaConn(clients[2], doc)
|
||||||
|
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
a := acedoc.Insert(0, 0, "a")
|
a := acedoc.Insert(0, 0, "a")
|
||||||
b := acedoc.Insert(0, 0, "b")
|
b := acedoc.Insert(0, 0, "b")
|
||||||
c := acedoc.Insert(0, 0, "c")
|
c := acedoc.Insert(0, 0, "c")
|
||||||
@@ -283,12 +217,12 @@ func TestDeltasMulti(t *testing.T) {
|
|||||||
clients[1].ReceiveJSON(w.CH.Message(b))
|
clients[1].ReceiveJSON(w.CH.Message(b))
|
||||||
clients[2].ReceiveJSON(w.CH.Message(c))
|
clients[2].ReceiveJSON(w.CH.Message(c))
|
||||||
|
|
||||||
time.Sleep(400 * time.Millisecond)
|
time.Sleep(1000 * time.Millisecond)
|
||||||
|
|
||||||
for i, client := range clients {
|
for i, client := range clients {
|
||||||
client.mtx.Lock()
|
client.mtx.Lock()
|
||||||
// all clients should have "written" 2 deltas (could be the same
|
// all clients should have "written" 2 deltas out to their "browser"
|
||||||
// message) that came from the other clients
|
// that came from the other clients
|
||||||
if len(client.wroteDeltas) != 2 {
|
if len(client.wroteDeltas) != 2 {
|
||||||
t.Errorf("client %d wrote %d deltas, should have written 2", i, len(client.wroteDeltas))
|
t.Errorf("client %d wrote %d deltas, should have written 2", i, len(client.wroteDeltas))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -52,7 +52,6 @@ func writeThemeFiles(dir string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Println("writing", path.Join(dir, asset))
|
|
||||||
f, err := os.Create(path.Join(dir, asset))
|
f, err := os.Create(path.Join(dir, asset))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
Reference in New Issue
Block a user