writer_test.go 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. /*
  2. Copyright 2023 Docker Compose CLI authors
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package sync
  14. import (
  15. "context"
  16. "errors"
  17. "io"
  18. "sync"
  19. "testing"
  20. "time"
  21. "github.com/stretchr/testify/require"
  22. )
  23. func TestLossyMultiWriter(t *testing.T) {
  24. ctx, cancel := context.WithCancel(context.Background())
  25. t.Cleanup(cancel)
  26. const count = 5
  27. readers := make([]*bufReader, count)
  28. writers := make([]*io.PipeWriter, count)
  29. for i := 0; i < count; i++ {
  30. r, w := io.Pipe()
  31. readers[i] = newBufReader(ctx, r)
  32. writers[i] = w
  33. }
  34. w := newLossyMultiWriter(writers...)
  35. t.Cleanup(w.Close)
  36. n, err := w.Write([]byte("hello world"))
  37. require.Equal(t, 11, n)
  38. require.NoError(t, err)
  39. for i := range readers {
  40. readers[i].waitForWrite(t)
  41. require.Equal(t, "hello world", string(readers[i].contents()))
  42. readers[i].reset()
  43. }
  44. // even if a writer fails (in this case simulated by closing the receiving end of the pipe),
  45. // write operations should continue to return nil error but the writer should be closed
  46. // with an error
  47. const failIndex = 3
  48. require.NoError(t, readers[failIndex].r.CloseWithError(errors.New("oh no")))
  49. n, err = w.Write([]byte("hello"))
  50. require.Equal(t, 5, n)
  51. require.NoError(t, err)
  52. for i := range readers {
  53. readers[i].waitForWrite(t)
  54. if i == failIndex {
  55. err := readers[i].error()
  56. require.EqualError(t, err, "io: read/write on closed pipe")
  57. require.Empty(t, readers[i].contents())
  58. } else {
  59. require.Equal(t, "hello", string(readers[i].contents()))
  60. }
  61. }
  62. // perform another write, verify there's still no errors
  63. n, err = w.Write([]byte(" world"))
  64. require.Equal(t, 6, n)
  65. require.NoError(t, err)
  66. }
  67. type bufReader struct {
  68. ctx context.Context
  69. r *io.PipeReader
  70. mu sync.Mutex
  71. err error
  72. data []byte
  73. writeSync chan struct{}
  74. }
  75. func newBufReader(ctx context.Context, r *io.PipeReader) *bufReader {
  76. b := &bufReader{
  77. ctx: ctx,
  78. r: r,
  79. writeSync: make(chan struct{}),
  80. }
  81. go b.consume()
  82. return b
  83. }
  84. func (b *bufReader) waitForWrite(t testing.TB) {
  85. t.Helper()
  86. select {
  87. case <-b.writeSync:
  88. return
  89. case <-time.After(50 * time.Millisecond):
  90. t.Fatal("timed out waiting for write")
  91. }
  92. }
  93. func (b *bufReader) consume() {
  94. defer close(b.writeSync)
  95. for {
  96. buf := make([]byte, 512)
  97. n, err := b.r.Read(buf)
  98. if n != 0 {
  99. b.mu.Lock()
  100. b.data = append(b.data, buf[:n]...)
  101. b.mu.Unlock()
  102. }
  103. if errors.Is(err, io.EOF) {
  104. return
  105. }
  106. if err != nil {
  107. b.mu.Lock()
  108. b.err = err
  109. b.mu.Unlock()
  110. return
  111. }
  112. // prevent goroutine leak, tie lifetime to the test
  113. select {
  114. case b.writeSync <- struct{}{}:
  115. case <-b.ctx.Done():
  116. return
  117. }
  118. }
  119. }
  120. func (b *bufReader) contents() []byte {
  121. b.mu.Lock()
  122. defer b.mu.Unlock()
  123. return b.data
  124. }
  125. func (b *bufReader) reset() {
  126. b.mu.Lock()
  127. defer b.mu.Unlock()
  128. b.data = nil
  129. }
  130. func (b *bufReader) error() error {
  131. b.mu.Lock()
  132. defer b.mu.Unlock()
  133. return b.err
  134. }