diff --git a/context/stoppable/cleanup.go b/context/stoppable/cleanup.go index 6c84deba1a01afbb48ed98947fa0ecfb5cbdb1ee..a733d74bc175d09403d2a681c8108b528a31d9f2 100644 --- a/context/stoppable/cleanup.go +++ b/context/stoppable/cleanup.go @@ -7,10 +7,10 @@ import ( "time" ) -// Wraps any stoppable and runs a callback after to stop for cleanup behavior -// the cleanup is run under the remainder of the timeout but will not be canceled -// if the timeout runs out -// the cleanup function does not run if the thread does not stop +// Cleanup wraps any stoppable and runs a callback after to stop for cleanup +// behavior. The cleanup is run under the remainder of the timeout but will not +// be canceled if the timeout runs out. The cleanup function does not run if the +// thread does not stop. type Cleanup struct { stop Stoppable // the clean function receives how long it has to run before the timeout, @@ -20,7 +20,7 @@ type Cleanup struct { once sync.Once } -// Creates a new cleanup from the passed stoppable and the function +// NewCleanup creates a new Cleanup from the passed stoppable and function. func NewCleanup(stop Stoppable, clean func(duration time.Duration) error) *Cleanup { return &Cleanup{ stop: stop, @@ -29,18 +29,19 @@ func NewCleanup(stop Stoppable, clean func(duration time.Duration) error) *Clean } } -// returns true if the thread is still running and its cleanup has completed +// IsRunning returns true if the thread is still running and its cleanup has +// completed. func (c *Cleanup) IsRunning() bool { return atomic.LoadUint32(&c.running) == 1 } -// returns the name of the stoppable denoting it has cleanup +// Name returns the name of the stoppable denoting it has cleanup. func (c *Cleanup) Name() string { return c.stop.Name() + " with cleanup" } -// stops the contained stoppable and runs the cleanup function after. -// the cleanup function does not run if the thread does not stop +// Close stops the contained stoppable and runs the cleanup function after. The +// cleanup function does not run if the thread does not stop. func (c *Cleanup) Close(timeout time.Duration) error { var err error @@ -49,14 +50,14 @@ func (c *Cleanup) Close(timeout time.Duration) error { defer atomic.StoreUint32(&c.running, 0) start := time.Now() - //run the stopable + // Run the stoppable if err := c.stop.Close(timeout); err != nil { err = errors.WithMessagef(err, "Cleanup for %s not executed", c.stop.Name()) return } - //run the cleanup function with the remaining time as a timeout + // Run the cleanup function with the remaining time as a timeout elapsed := time.Since(start) complete := make(chan error, 1) @@ -69,11 +70,11 @@ func (c *Cleanup) Close(timeout time.Duration) error { select { case err := <-complete: if err != nil { - err = errors.WithMessagef(err, "Cleanup for %s "+ - "failed", c.stop.Name()) + err = errors.WithMessagef(err, "Cleanup for %s failed", + c.stop.Name()) } case <-timer.C: - err = errors.Errorf("Clean up for %s timedout", c.stop.Name()) + err = errors.Errorf("Clean up for %s timeout", c.stop.Name()) } }) diff --git a/context/stoppable/cleanup_test.go b/context/stoppable/cleanup_test.go new file mode 100644 index 0000000000000000000000000000000000000000..3d808212a09754692d8c573d74034610f88bcb45 --- /dev/null +++ b/context/stoppable/cleanup_test.go @@ -0,0 +1,55 @@ +package stoppable + +import ( + "testing" +) + +// Tests happy path of NewCleanup(). +func TestNewCleanup(t *testing.T) { + single := NewSingle("test name") + cleanup := NewCleanup(single, single.Close) + + if cleanup.stop != single || cleanup.running != 0 { + t.Errorf("NewCleanup() returned Single with incorrect values."+ + "\n\texpected: stop: %v running: %d\n\treceived: stop: %v running: %d", + single, cleanup.stop, 0, cleanup.running) + } +} + +// Tests happy path of Cleanup.IsRunning(). +func TestCleanup_IsRunning(t *testing.T) { + single := NewSingle("test name") + cleanup := NewCleanup(single, single.Close) + + if cleanup.IsRunning() { + t.Errorf("IsRunning() returned false when it should be running.") + } + + cleanup.running = 1 + if !cleanup.IsRunning() { + t.Errorf("IsRunning() returned true when it should not be running.") + } +} + +// Tests happy path of Cleanup.Name(). +func TestCleanup_Name(t *testing.T) { + name := "test name" + single := NewSingle(name) + cleanup := NewCleanup(single, single.Close) + + if name+" with cleanup" != cleanup.Name() { + t.Errorf("Name() returned the incorrect string."+ + "\n\texpected: %s\n\treceived: %s", name+" with cleanup", cleanup.Name()) + } +} + +// Tests happy path of Cleanup.Close(). +func TestCleanup_Close(t *testing.T) { + single := NewSingle("test name") + cleanup := NewCleanup(single, single.Close) + + err := cleanup.Close(0) + if err != nil { + t.Errorf("Close() returned an error: %v", err) + } +} diff --git a/context/stoppable/multi.go b/context/stoppable/multi.go index c9ee1ff61fc79a76ed9f268be0fe009a597a0e81..061be492dd4efd2d6c618d7f9772a18fb8cc7ab7 100644 --- a/context/stoppable/multi.go +++ b/context/stoppable/multi.go @@ -17,7 +17,7 @@ type Multi struct { once sync.Once } -//returns a new multi stoppable +// NewMulti returns a new multi stoppable. func NewMulti(name string) *Multi { return &Multi{ name: name, @@ -25,20 +25,20 @@ func NewMulti(name string) *Multi { } } -// returns true if the thread is still running +// IsRunning returns true if the thread is still running. func (m *Multi) IsRunning() bool { return atomic.LoadUint32(&m.running) == 1 } -// adds the given stoppable to the list of stoppables +// Add adds the given stoppable to the list of stoppables. func (m *Multi) Add(stoppable Stoppable) { m.mux.Lock() m.stoppables = append(m.stoppables, stoppable) m.mux.Unlock() } -// returns the name of the multi stoppable and the names of all stoppables it -// contains +// Name returns the name of the multi stoppable and the names of all stoppables +// it contains. func (m *Multi) Name() string { m.mux.RLock() names := m.name + ": {" @@ -54,8 +54,8 @@ func (m *Multi) Name() string { return names } -// closes all child stoppers. It does not return their errors and assumes they -// print them to the log +// Close closes all child stoppers. It does not return their errors and assumes +// they print them to the log. func (m *Multi) Close(timeout time.Duration) error { var err error m.once.Do( @@ -63,18 +63,17 @@ func (m *Multi) Close(timeout time.Duration) error { atomic.StoreUint32(&m.running, 0) numErrors := uint32(0) - wg := &sync.WaitGroup{} m.mux.Lock() for _, stoppable := range m.stoppables { wg.Add(1) - go func() { + go func(stoppable Stoppable) { if stoppable.Close(timeout) != nil { atomic.AddUint32(&numErrors, 1) } wg.Done() - }() + }(stoppable) } m.mux.Unlock() @@ -89,5 +88,4 @@ func (m *Multi) Close(timeout time.Duration) error { }) return err - } diff --git a/context/stoppable/multi_test.go b/context/stoppable/multi_test.go new file mode 100644 index 0000000000000000000000000000000000000000..70f90e286da556ab736308f490edce3b8f620d3e --- /dev/null +++ b/context/stoppable/multi_test.go @@ -0,0 +1,115 @@ +package stoppable + +import ( + "reflect" + "testing" + "time" +) + +// Tests happy path of NewMulti(). +func TestNewMulti(t *testing.T) { + name := "test name" + multi := NewMulti(name) + + if multi.name != name || multi.running != 1 { + t.Errorf("NewMulti() returned Multi with incorrect values."+ + "\n\texpected: name: %s running: %d\n\treceived: name: %s running: %d", + name, 1, multi.name, multi.running) + } +} + +// Tests happy path of Multi.IsRunning(). +func TestMulti_IsRunning(t *testing.T) { + multi := NewMulti("name") + + if !multi.IsRunning() { + t.Errorf("IsRunning() returned false when it should be running.") + } + + multi.running = 0 + if multi.IsRunning() { + t.Errorf("IsRunning() returned true when it should not be running.") + } +} + +// Tests happy path of Multi.Add(). +func TestMulti_Add(t *testing.T) { + multi := NewMulti("multi name") + singles := []*Single{ + NewSingle("single name 1"), + NewSingle("single name 2"), + NewSingle("single name 3"), + } + + for _, single := range singles { + multi.Add(single) + } + + for i, single := range singles { + if !reflect.DeepEqual(single, multi.stoppables[i]) { + t.Errorf("Add() did not add the correct Stoppables."+ + "\n\texpected: %#v\n\treceived: %#v", single, multi.stoppables[i]) + } + } +} + +// Tests happy path of Multi.Name(). +func TestMulti_Name(t *testing.T) { + name := "test name" + multi := NewMulti(name) + singles := []*Single{ + NewSingle("single name 1"), + NewSingle("single name 2"), + NewSingle("single name 3"), + } + expectedNames := []string{ + name + ": {}", + name + ": {" + singles[0].name + "}", + name + ": {" + singles[0].name + ", " + singles[1].name + "}", + name + ": {" + singles[0].name + ", " + singles[1].name + ", " + singles[2].name + "}", + } + + for i, single := range singles { + if expectedNames[i] != multi.Name() { + t.Errorf("Name() returned the incorrect string."+ + "\n\texpected: %s\n\treceived: %s", expectedNames[0], multi.Name()) + } + multi.Add(single) + } +} + +// Tests happy path of Multi.Close(). +func TestMulti_Close(t *testing.T) { + // Create new Multi and add Singles to it + multi := NewMulti("name") + singles := []*Single{ + NewSingle("single name 1"), + NewSingle("single name 2"), + NewSingle("single name 3"), + } + for _, single := range singles { + multi.Add(single) + } + + go func() { + select { + case <-singles[0].quit: + } + select { + case <-singles[1].quit: + } + select { + case <-singles[2].quit: + } + }() + + err := multi.Close(5 * time.Millisecond) + if err != nil { + t.Errorf("Close() returned an error: %v", err) + } + + err = multi.Close(0) + if err != nil { + t.Errorf("Close() returned an error: %v", err) + } +} diff --git a/context/stoppable/single.go b/context/stoppable/single.go index 47a2259b068f731a09d01efda4f1322a9cccc182..acd8da0077427215934dae20b68ff9346396492c 100644 --- a/context/stoppable/single.go +++ b/context/stoppable/single.go @@ -8,8 +8,8 @@ import ( "time" ) -// Single allows stopping a single goroutine using a channel -// adheres to the stoppable interface +// Single allows stopping a single goroutine using a channel. +// It adheres to the stoppable interface. type Single struct { name string quit chan struct{} @@ -17,7 +17,7 @@ type Single struct { once sync.Once } -//returns a new single stoppable +// NewSingle returns a new single stoppable. func NewSingle(name string) *Single { return &Single{ name: name, @@ -26,22 +26,22 @@ func NewSingle(name string) *Single { } } -// returns true if the thread is still running +// IsRunning returns true if the thread is still running. func (s *Single) IsRunning() bool { return atomic.LoadUint32(&s.running) == 1 } -// returns the read only channel it will send the stop signal on -func (s *Single) Quit() <-chan struct{} { +// Quit returns the read only channel it will send the stop signal on. +func (s *Single) Quit() chan<- struct{} { return s.quit } -// returns the name of the thread. This is designed to be +// Name returns the name of the thread. This is designed to be func (s *Single) Name() string { return s.name } -// Close signals thread to time out and closes if it is still running. +// Close signals the thread to time out and closes if it is still running. func (s *Single) Close(timeout time.Duration) error { var err error s.once.Do(func() { diff --git a/context/stoppable/single_test.go b/context/stoppable/single_test.go new file mode 100644 index 0000000000000000000000000000000000000000..2343379ca3a21efc384ac47b503c5896ee0ae862 --- /dev/null +++ b/context/stoppable/single_test.go @@ -0,0 +1,96 @@ +package stoppable + +import ( + "testing" + "time" +) + +// Tests happy path of NewSingle(). +func TestNewSingle(t *testing.T) { + name := "test name" + single := NewSingle(name) + + if single.name != name || single.running != 1 { + t.Errorf("NewSingle() returned Single with incorrect values."+ + "\n\texpected: name: %s running: %d\n\treceived: name: %s running: %d", + name, 1, single.name, single.running) + } +} + +// Tests happy path of Single.IsRunning(). +func TestSingle_IsRunning(t *testing.T) { + single := NewSingle("name") + + if !single.IsRunning() { + t.Errorf("IsRunning() returned false when it should be running.") + } + + single.running = 0 + if single.IsRunning() { + t.Errorf("IsRunning() returned true when it should not be running.") + } +} + +// Tests happy path of Single.Quit(). +func TestSingle_Quit(t *testing.T) { + single := NewSingle("name") + + go func() { + time.Sleep(150 * time.Nanosecond) + single.Quit() <- struct{}{} + }() + + timer := time.NewTimer(2 * time.Millisecond) + select { + case <-timer.C: + t.Errorf("Quit signal not received.") + case <-single.quit: + } +} + +// Tests happy path of Single.Name(). +func TestSingle_Name(t *testing.T) { + name := "test name" + single := NewSingle(name) + + if name != single.Name() { + t.Errorf("Name() returned the incorrect string."+ + "\n\texpected: %s\n\treceived: %s", name, single.Name()) + } +} + +// Test happy path of Single.Close(). +func TestSingle_Close(t *testing.T) { + single := NewSingle("name") + + go func() { + time.Sleep(150 * time.Nanosecond) + select { + case <-single.quit: + } + }() + + err := single.Close(5 * time.Millisecond) + if err != nil { + t.Errorf("Close() returned an error: %v", err) + } +} + +// Tests that Single.Close() returns an error when the timeout is reached. +func TestSingle_Close_Error(t *testing.T) { + single := NewSingle("name") + expectedErr := single.name + " failed to close" + + go func() { + time.Sleep(3 * time.Millisecond) + select { + case <-single.quit: + } + }() + + err := single.Close(2 * time.Millisecond) + if err == nil { + t.Errorf("Close() did not return the expected error."+ + "\n\texpected: %v\n\treceived: %v", expectedErr, err) + } +} diff --git a/context/stoppable/stoppable.go b/context/stoppable/stoppable.go index 6f76d7348ed4a8fddb35f893e2be413b0f4a502e..e4436d5a6f971ae0f648e0fe7e16c0472cd8a2ab 100644 --- a/context/stoppable/stoppable.go +++ b/context/stoppable/stoppable.go @@ -2,7 +2,7 @@ package stoppable import "time" -// Interface for stopping a goroutine +// Interface for stopping a goroutine. type Stoppable interface { Close(timeout time.Duration) error IsRunning() bool diff --git a/storage/conversation/partner.go b/storage/conversation/partner.go index 2d3de1b6a3dd7578f7280dbd0f2087c20d323691..3533f65fd6ce51382fb0ec77507da6b92583aa14 100644 --- a/storage/conversation/partner.go +++ b/storage/conversation/partner.go @@ -12,11 +12,13 @@ import ( "time" ) -const conversationKeyPrefix = "conversation" -const currentConversationVersion = 0 -const maxTruncatedID = math.MaxUint32 -const bottomRegion = maxTruncatedID / 4 -const topRegion = bottomRegion * 3 +const ( + conversationKeyPrefix = "conversation" + currentConversationVersion = 0 + maxTruncatedID = math.MaxUint32 + bottomRegion = maxTruncatedID / 4 + topRegion = bottomRegion * 3 +) type Conversation struct { // Public & stored data @@ -37,7 +39,8 @@ type conversationDisk struct { NextSendID uint64 } -// Returns the Conversation if it can be found, otherwise returns a new partner +// LoadOrMakeConversation returns the Conversation if it can be found, otherwise +// returns a new partner. func LoadOrMakeConversation(kv *versioned.KV, partner *id.ID) *Conversation { c, err := loadConversation(kv, partner) @@ -61,11 +64,12 @@ func LoadOrMakeConversation(kv *versioned.KV, partner *id.ID) *Conversation { return c } -// Finds the full 64 bit message ID and updates the internal last message ID if -// the new ID is newer +// ProcessReceivedMessageID finds the full 64-bit message ID and updates the +// internal last message ID if the new ID is newer. func (c *Conversation) ProcessReceivedMessageID(mid uint32) uint64 { c.mux.Lock() defer c.mux.Unlock() + var high uint32 switch cmp(c.lastReceivedID, mid) { case 1: @@ -101,14 +105,14 @@ func cmp(a, b uint32) int { return 0 } -//returns the next sendID in both full and truncated formats +// GetNextSendID returns the next sendID in both full and truncated formats. func (c *Conversation) GetNextSendID() (uint64, uint32) { c.mux.Lock() old := c.nextSentID c.nextSentID++ if err := c.save(); err != nil { - jww.FATAL.Panicf("Failed to save after incrementing the sendID: "+ - "%s", err) + jww.FATAL.Panicf("Failed to save after incrementing the sendID: %s", + err) } c.mux.Unlock() return old, uint32(old & 0x00000000FFFFFFFF) diff --git a/storage/conversation/partner_test.go b/storage/conversation/partner_test.go new file mode 100644 index 0000000000000000000000000000000000000000..0c6091abc3aee7ff1801d7dc3c648879aa878607 --- /dev/null +++ b/storage/conversation/partner_test.go @@ -0,0 +1,207 @@ +package conversation + +import ( + "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/elixxir/ekv" + "gitlab.com/xx_network/primitives/id" + "math/rand" + "reflect" + "testing" +) + +// Tests happy path of LoadOrMakeConversation() when making a new Conversation. +func TestLoadOrMakeConversation_Make(t *testing.T) { + // Set up test values + kv := versioned.NewKV(make(ekv.Memstore)) + partner := id.NewIdFromString("partner ID", id.User, t) + expectedConv := &Conversation{ + lastReceivedID: 0, + numReceivedRevolutions: 0, + nextSentID: 0, + partner: partner, + kv: kv, + } + + // Create new Conversation + conv := LoadOrMakeConversation(kv, partner) + + // Check that the result matches the expected Conversation + if !reflect.DeepEqual(expectedConv, conv) { + t.Errorf("LoadOrMakeConversation() made unexpected Conversation."+ + "\n\texpected: %+v\n\treceived: %+v", expectedConv, conv) + } +} + +// Tests happy path of LoadOrMakeConversation() when loading a Conversation. +func TestLoadOrMakeConversation_Load(t *testing.T) { + // Set up test values + kv := versioned.NewKV(make(ekv.Memstore)) + partner := id.NewIdFromString("partner ID", id.User, t) + expectedConv := LoadOrMakeConversation(kv, partner) + + // Load Conversation + conv := LoadOrMakeConversation(kv, partner) + + // Check that the result matches the expected Conversation + if !reflect.DeepEqual(expectedConv, conv) { + t.Errorf("LoadOrMakeConversation() made unexpected Conversation."+ + "\n\texpected: %+v\n\treceived: %+v", expectedConv, conv) + } +} + +// Tests case 1 of Conversation.ProcessReceivedMessageID(). +func TestConversation_ProcessReceivedMessageID_Case_1(t *testing.T) { + // Set up test values + mid := uint32(5) + kv := versioned.NewKV(make(ekv.Memstore)) + partner := id.NewIdFromString("partner ID", id.User, t) + expectedConv := LoadOrMakeConversation(kv, partner) + expectedConv.lastReceivedID = mid + expectedConv.numReceivedRevolutions = 1 + conv := LoadOrMakeConversation(kv, partner) + conv.lastReceivedID = topRegion + 5 + expectedResult := uint64(expectedConv.numReceivedRevolutions)<<32 | uint64(mid) + + result := conv.ProcessReceivedMessageID(mid) + if result != expectedResult { + t.Errorf("ProcessReceivedMessageID() did not product the expected "+ + "result.\n\texpected: %+v\n\trecieved: %+v", + expectedResult, result) + } + if !reflect.DeepEqual(expectedConv, conv) { + t.Errorf("ProcessReceivedMessageID() did not product the expected "+ + "Conversation.\n\texpected: %+v\n\trecieved: %+v", + expectedConv, conv) + } +} + +// Tests case 0 of Conversation.ProcessReceivedMessageID(). +func TestConversation_ProcessReceivedMessageID_Case_0(t *testing.T) { + // Set up test values + mid := uint32(5) + kv := versioned.NewKV(make(ekv.Memstore)) + partner := id.NewIdFromString("partner ID", id.User, t) + expectedConv := LoadOrMakeConversation(kv, partner) + expectedConv.lastReceivedID = mid + conv := LoadOrMakeConversation(kv, partner) + expectedResult := uint64(expectedConv.numReceivedRevolutions)<<32 | uint64(mid) + + result := conv.ProcessReceivedMessageID(mid) + if result != expectedResult { + t.Errorf("ProcessReceivedMessageID() did not product the expected "+ + "result.\n\texpected: %+v\n\trecieved: %+v", + expectedResult, result) + } + if !reflect.DeepEqual(expectedConv, conv) { + t.Errorf("ProcessReceivedMessageID() did not product the expected "+ + "Conversation.\n\texpected: %+v\n\trecieved: %+v", + expectedConv, conv) + } +} + +// Tests case -1 of Conversation.ProcessReceivedMessageID(). +func TestConversation_ProcessReceivedMessageID_Case_Neg1(t *testing.T) { + // Set up test values + mid := uint32(topRegion + 5) + kv := versioned.NewKV(make(ekv.Memstore)) + partner := id.NewIdFromString("partner ID", id.User, t) + expectedConv := LoadOrMakeConversation(kv, partner) + expectedConv.lastReceivedID = bottomRegion - 5 + conv := LoadOrMakeConversation(kv, partner) + conv.lastReceivedID = bottomRegion - 5 + expectedResult := uint64(expectedConv.numReceivedRevolutions-1)<<32 | uint64(mid) + + result := conv.ProcessReceivedMessageID(mid) + if result != expectedResult { + t.Errorf("ProcessReceivedMessageID() did not product the expected "+ + "result.\n\texpected: %+v\n\trecieved: %+v", + expectedResult, result) + } + if !reflect.DeepEqual(expectedConv, conv) { + t.Errorf("ProcessReceivedMessageID() did not product the expected "+ + "Conversation.\n\texpected: %+v\n\trecieved: %+v", + expectedConv, conv) + } +} + +// Tests happy path of Conversation.GetNextSendID(). +func TestConversation_GetNextSendID(t *testing.T) { + // Set up test values + kv := versioned.NewKV(make(ekv.Memstore)) + partner := id.NewIdFromString("partner ID", id.User, t) + conv := LoadOrMakeConversation(kv, partner) + conv.nextSentID = maxTruncatedID - 100 + + for i := uint64(maxTruncatedID - 100); i < maxTruncatedID+100; i++ { + fullID, truncID := conv.GetNextSendID() + if fullID != i { + t.Errorf("Returned incorrect full sendID."+ + "\n\texpected: %d\n\treceived: %d", i, fullID) + } + if truncID != uint32(i) { + t.Errorf("Returned incorrect truncated sendID."+ + "\n\texpected: %d\n\treceived: %d", uint32(i), truncID) + } + } +} + +// Tests the happy path of save() and loadConversation(). +func TestConversation_save_load(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + partner := id.NewIdFromString("partner ID", id.User, t) + expectedConv := makeRandomConv(kv, partner) + expectedErr := "loadConversation() produced an error: Failed to Load " + + "conversation: object not found" + + err := expectedConv.save() + if err != nil { + t.Errorf("save() produced an error: %v", err) + } + + testConv, err := loadConversation(kv, partner) + if err != nil { + t.Errorf("loadConversation() produced an error: %v", err) + } + + if !reflect.DeepEqual(expectedConv, testConv) { + t.Errorf("saving and loading Conversation failed."+ + "\n\texpected: %+v\n\treceived: %+v", expectedConv, testConv) + } + + _, err = loadConversation(versioned.NewKV(make(ekv.Memstore)), partner) + if err == nil { + t.Errorf("loadConversation() failed to produce an error."+ + "\n\texpected: %s\n\treceived: %v", expectedErr, nil) + } +} + +// Tests the happy path of marshal() and unmarshal(). +func TestConversation_marshal_unmarshal(t *testing.T) { + expectedConv := makeRandomConv(versioned.NewKV(make(ekv.Memstore)), + id.NewIdFromString("partner ID", id.User, t)) + testConv := LoadOrMakeConversation(expectedConv.kv, expectedConv.partner) + + data, err := expectedConv.marshal() + if err != nil { + t.Errorf("marshal() returned an error: %v", err) + } + + err = testConv.unmarshal(data) + if err != nil { + t.Errorf("unmarshal() returned an error: %v", err) + } + + if !reflect.DeepEqual(expectedConv, testConv) { + t.Errorf("marshaling and unmarshaling Conversation failed."+ + "\n\texpected: %+v\n\treceived: %+v", expectedConv, testConv) + } +} + +func makeRandomConv(kv *versioned.KV, partner *id.ID) *Conversation { + c := LoadOrMakeConversation(kv, partner) + c.lastReceivedID = rand.Uint32() + c.numReceivedRevolutions = rand.Uint32() + c.nextSentID = rand.Uint64() + + return c +}