package xfer // import "github.com/docker/docker/distribution/xfer" import ( "sync/atomic" "testing" "time" "github.com/docker/docker/pkg/progress" ) func TestTransfer(t *testing.T) { makeXferFunc := func(id string) doFunc { return func(progressChan chan<- progress.Progress, start <-chan struct{}, _ chan<- struct{}) transfer { select { case <-start: default: t.Errorf("%s: transfer function not started even though concurrency limit not reached", id) } xfer := newTransfer() go func() { for i := 0; i <= 10; i++ { progressChan <- progress.Progress{ID: id, Action: "testing", Current: int64(i), Total: 10} time.Sleep(10 * time.Millisecond) } close(progressChan) }() return xfer } } tm := newTransferManager(5) progressChan := make(chan progress.Progress) progressDone := make(chan struct{}) receivedProgress := make(map[string]int64) go func() { for p := range progressChan { val, present := receivedProgress[p.ID] if present && p.Current <= val { t.Errorf("%s: got unexpected progress value: %d (expected <= %d)", p.ID, p.Current, val) } receivedProgress[p.ID] = p.Current } close(progressDone) }() // Start a few transfers ids := []string{"id1", "id2", "id3"} xfers := make([]transfer, len(ids)) watchers := make([]*watcher, len(ids)) for i, id := range ids { xfers[i], watchers[i] = tm.transfer(id, makeXferFunc(id), progress.ChanOutput(progressChan)) } for i, xfer := range xfers { <-xfer.done() xfer.release(watchers[i]) } close(progressChan) <-progressDone for _, id := range ids { if receivedProgress[id] != 10 { t.Fatalf("final progress value %d instead of 10", receivedProgress[id]) } } } func TestConcurrencyLimit(t *testing.T) { const concurrencyLimit = 3 var runningJobs int32 makeXferFunc := func(id string) doFunc { return func(progressChan chan<- progress.Progress, start <-chan struct{}, _ chan<- struct{}) transfer { xfer := newTransfer() go func() { <-start totalJobs := atomic.AddInt32(&runningJobs, 1) if int(totalJobs) > concurrencyLimit { t.Errorf("%s: too many jobs running (%d > %d)", id, totalJobs, concurrencyLimit) } for i := 0; i <= 10; i++ { progressChan <- progress.Progress{ID: id, Action: "testing", Current: int64(i), Total: 10} time.Sleep(10 * time.Millisecond) } atomic.AddInt32(&runningJobs, -1) close(progressChan) }() return xfer } } tm := newTransferManager(concurrencyLimit) progressChan := make(chan progress.Progress) progressDone := make(chan struct{}) receivedProgress := make(map[string]int64) go func() { for p := range progressChan { receivedProgress[p.ID] = p.Current } close(progressDone) }() // Start more transfers than the concurrency limit ids := []string{"id1", "id2", "id3", "id4", "id5", "id6", "id7", "id8"} xfers := make([]transfer, len(ids)) watchers := make([]*watcher, len(ids)) for i, id := range ids { xfers[i], watchers[i] = tm.transfer(id, makeXferFunc(id), progress.ChanOutput(progressChan)) } for i, xfer := range xfers { <-xfer.done() xfer.release(watchers[i]) } close(progressChan) <-progressDone for _, id := range ids { if receivedProgress[id] != 10 { t.Fatalf("final progress value %d instead of 10", receivedProgress[id]) } } } func TestInactiveJobs(t *testing.T) { const concurrencyLimit = 3 var runningJobs int32 testDone := make(chan struct{}) makeXferFunc := func(id string) doFunc { return func(progressChan chan<- progress.Progress, start <-chan struct{}, inactive chan<- struct{}) transfer { xfer := newTransfer() go func() { <-start totalJobs := atomic.AddInt32(&runningJobs, 1) if int(totalJobs) > concurrencyLimit { t.Errorf("%s: too many jobs running (%d > %d)", id, totalJobs, concurrencyLimit) } for i := 0; i <= 10; i++ { progressChan <- progress.Progress{ID: id, Action: "testing", Current: int64(i), Total: 10} time.Sleep(10 * time.Millisecond) } atomic.AddInt32(&runningJobs, -1) close(inactive) <-testDone close(progressChan) }() return xfer } } tm := newTransferManager(concurrencyLimit) progressChan := make(chan progress.Progress) progressDone := make(chan struct{}) receivedProgress := make(map[string]int64) go func() { for p := range progressChan { receivedProgress[p.ID] = p.Current } close(progressDone) }() // Start more transfers than the concurrency limit ids := []string{"id1", "id2", "id3", "id4", "id5", "id6", "id7", "id8"} xfers := make([]transfer, len(ids)) watchers := make([]*watcher, len(ids)) for i, id := range ids { xfers[i], watchers[i] = tm.transfer(id, makeXferFunc(id), progress.ChanOutput(progressChan)) } close(testDone) for i, xfer := range xfers { <-xfer.done() xfer.release(watchers[i]) } close(progressChan) <-progressDone for _, id := range ids { if receivedProgress[id] != 10 { t.Fatalf("final progress value %d instead of 10", receivedProgress[id]) } } } func TestWatchRelease(t *testing.T) { ready := make(chan struct{}) makeXferFunc := func(id string) doFunc { return func(progressChan chan<- progress.Progress, start <-chan struct{}, _ chan<- struct{}) transfer { xfer := newTransfer() go func() { defer func() { close(progressChan) }() <-ready for i := int64(0); ; i++ { select { case <-time.After(10 * time.Millisecond): case <-xfer.context().Done(): return } progressChan <- progress.Progress{ID: id, Action: "testing", Current: i, Total: 10} } }() return xfer } } tm := newTransferManager(5) type watcherInfo struct { watcher *watcher progressChan chan progress.Progress progressDone chan struct{} receivedFirstProgress chan struct{} } progressConsumer := func(w watcherInfo) { first := true for range w.progressChan { if first { close(w.receivedFirstProgress) } first = false } close(w.progressDone) } // Start a transfer watchers := make([]watcherInfo, 5) var xfer transfer watchers[0].progressChan = make(chan progress.Progress) watchers[0].progressDone = make(chan struct{}) watchers[0].receivedFirstProgress = make(chan struct{}) xfer, watchers[0].watcher = tm.transfer("id1", makeXferFunc("id1"), progress.ChanOutput(watchers[0].progressChan)) go progressConsumer(watchers[0]) // Give it multiple watchers for i := 1; i != len(watchers); i++ { watchers[i].progressChan = make(chan progress.Progress) watchers[i].progressDone = make(chan struct{}) watchers[i].receivedFirstProgress = make(chan struct{}) watchers[i].watcher = xfer.watch(progress.ChanOutput(watchers[i].progressChan)) go progressConsumer(watchers[i]) } // Now that the watchers are set up, allow the transfer goroutine to // proceed. close(ready) // Confirm that each watcher gets progress output. for _, w := range watchers { <-w.receivedFirstProgress } // Release one watcher every 5ms for _, w := range watchers { xfer.release(w.watcher) <-time.After(5 * time.Millisecond) } // Now that all watchers have been released, Released() should // return a closed channel. <-xfer.released() // Done() should return a closed channel because the xfer func returned // due to cancellation. <-xfer.done() for _, w := range watchers { close(w.progressChan) <-w.progressDone } } func TestWatchFinishedTransfer(t *testing.T) { makeXferFunc := func(id string) doFunc { return func(progressChan chan<- progress.Progress, _ <-chan struct{}, _ chan<- struct{}) transfer { xfer := newTransfer() go func() { // Finish immediately close(progressChan) }() return xfer } } tm := newTransferManager(5) // Start a transfer watchers := make([]*watcher, 3) var xfer transfer xfer, watchers[0] = tm.transfer("id1", makeXferFunc("id1"), progress.ChanOutput(make(chan progress.Progress))) // Give it a watcher immediately watchers[1] = xfer.watch(progress.ChanOutput(make(chan progress.Progress))) // Wait for the transfer to complete <-xfer.done() // Set up another watcher watchers[2] = xfer.watch(progress.ChanOutput(make(chan progress.Progress))) // Release the watchers for _, w := range watchers { xfer.release(w) } // Now that all watchers have been released, Released() should // return a closed channel. <-xfer.released() } func TestDuplicateTransfer(t *testing.T) { ready := make(chan struct{}) var xferFuncCalls int32 makeXferFunc := func(id string) doFunc { return func(progressChan chan<- progress.Progress, _ <-chan struct{}, _ chan<- struct{}) transfer { atomic.AddInt32(&xferFuncCalls, 1) xfer := newTransfer() go func() { defer func() { close(progressChan) }() <-ready for i := int64(0); ; i++ { select { case <-time.After(10 * time.Millisecond): case <-xfer.context().Done(): return } progressChan <- progress.Progress{ID: id, Action: "testing", Current: i, Total: 10} } }() return xfer } } tm := newTransferManager(5) type transferInfo struct { xfer transfer watcher *watcher progressChan chan progress.Progress progressDone chan struct{} receivedFirstProgress chan struct{} } progressConsumer := func(t transferInfo) { first := true for range t.progressChan { if first { close(t.receivedFirstProgress) } first = false } close(t.progressDone) } // Try to start multiple transfers with the same ID transfers := make([]transferInfo, 5) for i := range transfers { t := &transfers[i] t.progressChan = make(chan progress.Progress) t.progressDone = make(chan struct{}) t.receivedFirstProgress = make(chan struct{}) t.xfer, t.watcher = tm.transfer("id1", makeXferFunc("id1"), progress.ChanOutput(t.progressChan)) go progressConsumer(*t) } // Allow the transfer goroutine to proceed. close(ready) // Confirm that each watcher gets progress output. for _, t := range transfers { <-t.receivedFirstProgress } // Confirm that the transfer function was called exactly once. if xferFuncCalls != 1 { t.Fatal("transfer function wasn't called exactly once") } // Release one watcher every 5ms for _, t := range transfers { t.xfer.release(t.watcher) <-time.After(5 * time.Millisecond) } for _, t := range transfers { // Now that all watchers have been released, Released() should // return a closed channel. <-t.xfer.released() // Done() should return a closed channel because the xfer func returned // due to cancellation. <-t.xfer.done() } for _, t := range transfers { close(t.progressChan) <-t.progressDone } }