func TestResendChunks(t *testing.T) { req := core.ResendRequest{ 10: []core.SequenceId{5, 6, 7, 8, 9}, 100: []core.SequenceId{1}, 2500: []core.SequenceId{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18}, } Convey("The data that comes out of a resend chunk is the same as the data that went into it.", t, func() { var config core.Config config.MaxChunkDataSize = 10000 datas := core.MakeResendChunkDatas(&config, req) So(len(datas), ShouldEqual, 1) parsed, err := core.ParseResendChunkData(datas[0]) So(err, ShouldBeNil) So(parsed, ShouldResemble, req) }) Convey("Resend data can be split across multiple chunks.", t, func() { var config core.Config config.MaxChunkDataSize = 20 datas := core.MakeResendChunkDatas(&config, req) So(len(datas), ShouldBeGreaterThan, 1) merged := make(core.ResendRequest) for _, data := range datas { parsed, err := core.ParseResendChunkData(data) So(err, ShouldBeNil) mergeResendData(merged, parsed) } So(merged, ShouldResemble, req) }) Convey("Malformed resend chunks return errors.", t, func() { _, err := core.ParseResendChunkData([]byte{1}) So(err, ShouldNotBeNil) }) }
func TestClientSendChunks(t *testing.T) { Convey("ClientSendChunksHandler", t, func() { config := &core.Config{ Node: 5, Logger: log.New(os.Stdout, "", log.Lshortfile|log.Ltime), GlobalConfig: core.GlobalConfig{ Streams: map[core.StreamId]core.StreamConfig{ 7: core.StreamConfig{ Name: "UU", Id: 7, Mode: core.ModeUnreliableUnordered, }, 8: core.StreamConfig{ Name: "UO", Id: 8, Mode: core.ModeUnreliableOrdered, }, 9: core.StreamConfig{ Name: "RU", Id: 9, Mode: core.ModeReliableUnordered, }, 10: core.StreamConfig{ Name: "RO", Id: 10, Mode: core.ModeReliableOrdered, }, }, MaxChunkDataSize: 50, PositionChunkMin: 20 * time.Millisecond, PositionChunkMax: 50 * time.Millisecond, Clock: &clock.RealClock{}, }, } fromCore := make(chan core.Chunk) reserved := make(chan core.Chunk) toHost := make(chan core.Chunk) handlerIsDone := make(chan struct{}) defer func() { close(fromCore) close(reserved) for { select { case <-handlerIsDone: return case <-toHost: } } }() go func() { core.ClientSendChunksHandler(config, fromCore, reserved, toHost) close(handlerIsDone) }() Convey("Copies all fromCore chunks to toHost.", func() { go func() { fromCore <- makeSimpleChunk(config.GetIdFromName("UU"), config.Node, 10) fromCore <- makeSimpleChunk(config.GetIdFromName("UU"), config.Node, 11) fromCore <- makeSimpleChunk(config.GetIdFromName("UU"), config.Node, 12) fromCore <- makeSimpleChunk(config.GetIdFromName("UO"), config.Node, 20) fromCore <- makeSimpleChunk(config.GetIdFromName("UO"), config.Node, 21) fromCore <- makeSimpleChunk(config.GetIdFromName("UO"), config.Node, 22) fromCore <- makeSimpleChunk(config.GetIdFromName("RU"), config.Node, 30) fromCore <- makeSimpleChunk(config.GetIdFromName("RU"), config.Node, 31) fromCore <- makeSimpleChunk(config.GetIdFromName("RU"), config.Node, 32) fromCore <- makeSimpleChunk(config.GetIdFromName("RO"), config.Node, 40) fromCore <- makeSimpleChunk(config.GetIdFromName("RO"), config.Node, 41) fromCore <- makeSimpleChunk(config.GetIdFromName("RO"), config.Node, 42) }() pt := make(core.PacketTracker) for i := 0; i < 12; i++ { pt.Add(<-toHost) } So(verifySimpleChunk(pt.Get(config.GetIdFromName("UU"), config.Node, 10)), ShouldBeTrue) So(verifySimpleChunk(pt.Get(config.GetIdFromName("UU"), config.Node, 11)), ShouldBeTrue) So(verifySimpleChunk(pt.Get(config.GetIdFromName("UU"), config.Node, 12)), ShouldBeTrue) So(verifySimpleChunk(pt.Get(config.GetIdFromName("UO"), config.Node, 20)), ShouldBeTrue) So(verifySimpleChunk(pt.Get(config.GetIdFromName("UO"), config.Node, 21)), ShouldBeTrue) So(verifySimpleChunk(pt.Get(config.GetIdFromName("UO"), config.Node, 22)), ShouldBeTrue) So(verifySimpleChunk(pt.Get(config.GetIdFromName("RU"), config.Node, 30)), ShouldBeTrue) So(verifySimpleChunk(pt.Get(config.GetIdFromName("RU"), config.Node, 31)), ShouldBeTrue) So(verifySimpleChunk(pt.Get(config.GetIdFromName("RU"), config.Node, 32)), ShouldBeTrue) So(verifySimpleChunk(pt.Get(config.GetIdFromName("RO"), config.Node, 40)), ShouldBeTrue) So(verifySimpleChunk(pt.Get(config.GetIdFromName("RO"), config.Node, 41)), ShouldBeTrue) So(verifySimpleChunk(pt.Get(config.GetIdFromName("RO"), config.Node, 42)), ShouldBeTrue) }) Convey("After it gets a bunch of chunks on reliable streams.", func() { N := 100 go func() { for i := 0; i < N; i++ { fromCore <- makeSimpleChunk(config.GetIdFromName("RU"), config.Node, core.SequenceId(i)) fromCore <- makeSimpleChunk(config.GetIdFromName("RO"), config.Node, core.SequenceId(i)) } }() for i := 0; i < N; i++ { <-toHost <-toHost } Convey("Sends position chunks periodically.", func() { done := make(chan struct{}) go func() { time.Sleep(200 * time.Millisecond) close(done) }() count := 0 getPositions: for { select { case <-done: So(count, ShouldBeGreaterThan, 2) So(count, ShouldBeLessThan, 10) break getPositions case chunk := <-toHost: count++ positions, err := core.ParsePositionChunkData(chunk.Data) So(err, ShouldBeNil) So(len(positions), ShouldEqual, 2) So(positions[config.GetIdFromName("RU")], ShouldEqual, core.SequenceId(N-1)) So(positions[config.GetIdFromName("RO")], ShouldEqual, core.SequenceId(N-1)) } } Convey("Unless it is sending chunks anyway.", func() { status := make(chan struct{}) // Defer the close because if a check fails in here we'll get a confusing panic // otherwise. defer close(status) // Send chunks periodically on one of the reliable streams, that one should not // cause any position chunks to be sent. After a while, stop sending those // chunks, this will cause the positions chunks to start sending again. go func() { next := core.SequenceId(N) timeout := time.After(100 * time.Millisecond) for { select { case <-status: return case <-timeout: status <- struct{}{} time.Sleep(100 * time.Millisecond) status <- struct{}{} return case <-time.After(10 * time.Millisecond): fromCore <- makeSimpleChunk(config.GetIdFromName("RU"), config.Node, next) next++ } } }() var countRO, countRU int getOnePosition: for { select { case <-status: // Might get 1 chunk because we don't start this phase of the test // immediately. So(countRU, ShouldBeLessThanOrEqualTo, 1) So(countRO, ShouldBeGreaterThan, 1) So(countRO, ShouldBeLessThan, 5) break getOnePosition case chunk := <-toHost: if chunk.Stream != core.StreamPosition { // The chunks we're sending will come through here, but we don't // want to parse them like a position packet. break } positions, err := core.ParsePositionChunkData(chunk.Data) So(err, ShouldBeNil) So(len(positions), ShouldBeGreaterThanOrEqualTo, 1) So(len(positions), ShouldBeLessThanOrEqualTo, 2) if position, ok := positions[config.GetIdFromName("RU")]; ok { countRU++ So(position, ShouldBeGreaterThanOrEqualTo, core.SequenceId(N-1)) } if position, ok := positions[config.GetIdFromName("RO")]; ok { countRO++ So(position, ShouldEqual, core.SequenceId(N-1)) } } } countRO, countRU = 0, 0 getBothPosition: for { select { case <-status: // Should be getting chunks for both of the reliable streams. So(countRU, ShouldBeGreaterThan, 1) So(countRU, ShouldBeLessThan, 5) So(countRO, ShouldBeGreaterThan, 1) So(countRO, ShouldBeLessThan, 5) break getBothPosition case chunk := <-toHost: if chunk.Stream != core.StreamPosition { // The chunks we're sending will come through here, but we don't // want to parse them like a position packet. break } positions, err := core.ParsePositionChunkData(chunk.Data) So(err, ShouldBeNil) So(len(positions), ShouldBeGreaterThanOrEqualTo, 1) So(len(positions), ShouldBeLessThanOrEqualTo, 2) if position, ok := positions[config.GetIdFromName("RU")]; ok { countRU++ So(position, ShouldBeGreaterThanOrEqualTo, core.SequenceId(N-1)) } if position, ok := positions[config.GetIdFromName("RO")]; ok { countRO++ So(position, ShouldEqual, core.SequenceId(N-1)) } } } }) Convey("Until the streams are truncated.", func() { // Truncate one stream. go func() { req := core.TruncateRequest{ config.GetIdFromName("RU"): core.SequenceId(N - 1), } for _, data := range core.MakeTruncateChunkDatas(config, req) { reserved <- core.Chunk{ Stream: core.StreamTruncate, Source: 1, Data: data, } } }() // Wait long enough to get some position packets. done := make(chan struct{}) go func() { time.Sleep(200 * time.Millisecond) close(done) }() count := 0 for { select { case <-done: So(count, ShouldBeGreaterThan, 1) return case chunk := <-toHost: if chunk.Stream != core.StreamPosition { // The chunks we're sending will come through here, but we don't // want to parse them like a position packet. break } positions, err := core.ParsePositionChunkData(chunk.Data) So(err, ShouldBeNil) So(len(positions), ShouldBeGreaterThanOrEqualTo, 1) So(len(positions), ShouldBeLessThanOrEqualTo, 2) if len(positions) == 1 { count++ So(positions[config.GetIdFromName("RO")], ShouldEqual, core.SequenceId(N-1)) } } } }) }) Convey("Resends chunks when requested.", func() { go func() { // Request a resend of all even chunks on RU and all odd chunks on RO. req := make(core.ResendRequest) for i := 0; i < N; i++ { var id core.StreamId if i%2 == 0 { id = config.GetIdFromName("RU") } else { id = config.GetIdFromName("RO") } req[id] = append(req[id], core.SequenceId(i)) } for _, data := range core.MakeResendChunkDatas(config, req) { reserved <- core.Chunk{ Stream: core.StreamResend, Source: 1, Data: data, } } }() pt := make(core.PacketTracker) for i := 0; i < N; i++ { pt.Add(<-toHost) } for i := 0; i < N; i++ { if i%2 == 0 { So(verifySimpleChunk(pt.Get(config.GetIdFromName("RU"), config.Node, core.SequenceId(i))), ShouldBeTrue) So(pt.Get(config.GetIdFromName("RO"), config.Node, core.SequenceId(i)), ShouldBeNil) } else { So(verifySimpleChunk(pt.Get(config.GetIdFromName("RO"), config.Node, core.SequenceId(i))), ShouldBeTrue) So(pt.Get(config.GetIdFromName("RU"), config.Node, core.SequenceId(i)), ShouldBeNil) } } }) Convey("Then gets a truncate request.", func() { go func() { req := core.TruncateRequest{ config.GetIdFromName("RU"): 30, config.GetIdFromName("RO"): 90, } for _, data := range core.MakeTruncateChunkDatas(config, req) { reserved <- core.Chunk{ Stream: core.StreamTruncate, Source: 1, Data: data, } } }() // This is a strange test, since this shouldn't happen in practice, but I don't // know any other reasonable way to check if data has actually been truncated, // other than checking memory usage. Convey("Can only respond to resend requests after the truncation point.", func() { go func() { req := core.ResendRequest{ config.GetIdFromName("RU"): []core.SequenceId{20, 30, 40}, config.GetIdFromName("RO"): []core.SequenceId{60, 70, 80}, } for _, data := range core.MakeResendChunkDatas(config, req) { reserved <- core.Chunk{ Stream: core.StreamResend, Source: 1, Data: data, } } }() pt := make(core.PacketTracker) for i := 0; i < 1; i++ { pt.Add(<-toHost) } So(pt.Get(config.GetIdFromName("RU"), config.Node, 20), ShouldBeNil) So(pt.Get(config.GetIdFromName("RU"), config.Node, 30), ShouldBeNil) So(verifySimpleChunk(pt.Get(config.GetIdFromName("RU"), config.Node, 40)), ShouldBeTrue) So(pt.Get(config.GetIdFromName("RO"), config.Node, 60), ShouldBeNil) So(pt.Get(config.GetIdFromName("RO"), config.Node, 70), ShouldBeNil) So(pt.Get(config.GetIdFromName("RO"), config.Node, 80), ShouldBeNil) }) }) }) }) }