github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/fs/sync/pipe_test.go (about) 1 package sync 2 3 import ( 4 "container/heap" 5 "context" 6 "sync" 7 "sync/atomic" 8 "testing" 9 10 "github.com/rclone/rclone/fs" 11 "github.com/rclone/rclone/fstest/mockobject" 12 "github.com/stretchr/testify/assert" 13 "github.com/stretchr/testify/require" 14 ) 15 16 // Check interface satisfied 17 var _ heap.Interface = (*pipe)(nil) 18 19 func TestPipe(t *testing.T) { 20 var queueLength int 21 var queueSize int64 22 stats := func(n int, size int64) { 23 queueLength, queueSize = n, size 24 } 25 26 // Make a new pipe 27 p, err := newPipe("", stats, 10) 28 require.NoError(t, err) 29 30 checkStats := func(expectedN int, expectedSize int64) { 31 n, size := p.Stats() 32 assert.Equal(t, expectedN, n) 33 assert.Equal(t, expectedSize, size) 34 assert.Equal(t, expectedN, queueLength) 35 assert.Equal(t, expectedSize, queueSize) 36 } 37 38 checkStats(0, 0) 39 40 ctx := context.Background() 41 42 obj1 := mockobject.New("potato").WithContent([]byte("hello"), mockobject.SeekModeNone) 43 44 pair1 := fs.ObjectPair{Src: obj1, Dst: nil} 45 pairD := fs.ObjectPair{Src: obj1, Dst: obj1} // this object should not count to the stats 46 47 // Put an object 48 ok := p.Put(ctx, pair1) 49 assert.Equal(t, true, ok) 50 checkStats(1, 5) 51 52 // Put an object to be deleted 53 ok = p.Put(ctx, pairD) 54 assert.Equal(t, true, ok) 55 checkStats(2, 5) 56 57 // Close the pipe showing reading on closed pipe is OK 58 p.Close() 59 60 // Read from pipe 61 pair2, ok := p.Get(ctx) 62 assert.Equal(t, pair1, pair2) 63 assert.Equal(t, true, ok) 64 checkStats(1, 0) 65 66 // Read from pipe 67 pair2, ok = p.Get(ctx) 68 assert.Equal(t, pairD, pair2) 69 assert.Equal(t, true, ok) 70 checkStats(0, 0) 71 72 // Check read on closed pipe 73 pair2, ok = p.Get(ctx) 74 assert.Equal(t, fs.ObjectPair{}, pair2) 75 assert.Equal(t, false, ok) 76 77 // Check panic on write to closed pipe 78 assert.Panics(t, func() { p.Put(ctx, pair1) }) 79 80 // Make a new pipe 81 p, err = newPipe("", stats, 10) 82 require.NoError(t, err) 83 ctx2, cancel := context.WithCancel(ctx) 84 85 // cancel it in the background - check read ceases 86 go cancel() 87 pair2, ok = p.Get(ctx2) 88 assert.Equal(t, fs.ObjectPair{}, pair2) 89 assert.Equal(t, false, ok) 90 91 // check we can't write 92 ok = p.Put(ctx2, pair1) 93 assert.Equal(t, false, ok) 94 95 } 96 97 // TestPipeConcurrent runs concurrent Get and Put to flush out any 98 // race conditions and concurrency problems. 99 func TestPipeConcurrent(t *testing.T) { 100 const ( 101 N = 1000 102 readWriters = 10 103 ) 104 105 stats := func(n int, size int64) {} 106 107 // Make a new pipe 108 p, err := newPipe("", stats, 10) 109 require.NoError(t, err) 110 111 var wg sync.WaitGroup 112 obj1 := mockobject.New("potato").WithContent([]byte("hello"), mockobject.SeekModeNone) 113 pair1 := fs.ObjectPair{Src: obj1, Dst: nil} 114 ctx := context.Background() 115 var count atomic.Int64 116 117 for j := 0; j < readWriters; j++ { 118 wg.Add(2) 119 go func() { 120 defer wg.Done() 121 for i := 0; i < N; i++ { 122 // Read from pipe 123 pair2, ok := p.Get(ctx) 124 assert.Equal(t, pair1, pair2) 125 assert.Equal(t, true, ok) 126 count.Add(-1) 127 } 128 }() 129 go func() { 130 defer wg.Done() 131 for i := 0; i < N; i++ { 132 // Put an object 133 ok := p.Put(ctx, pair1) 134 assert.Equal(t, true, ok) 135 count.Add(1) 136 } 137 }() 138 } 139 wg.Wait() 140 141 assert.Equal(t, int64(0), count.Load()) 142 } 143 144 func TestPipeOrderBy(t *testing.T) { 145 var ( 146 stats = func(n int, size int64) {} 147 ctx = context.Background() 148 obj1 = mockobject.New("b").WithContent([]byte("1"), mockobject.SeekModeNone) 149 obj2 = mockobject.New("a").WithContent([]byte("22"), mockobject.SeekModeNone) 150 pair1 = fs.ObjectPair{Src: obj1} 151 pair2 = fs.ObjectPair{Src: obj2} 152 ) 153 154 for _, test := range []struct { 155 orderBy string 156 swapped1 bool 157 swapped2 bool 158 fraction int 159 }{ 160 {"", false, true, -1}, 161 {"size", false, false, -1}, 162 {"name", true, true, -1}, 163 {"modtime", false, true, -1}, 164 {"size,ascending", false, false, -1}, 165 {"name,asc", true, true, -1}, 166 {"modtime,ascending", false, true, -1}, 167 {"size,descending", true, true, -1}, 168 {"name,desc", false, false, -1}, 169 {"modtime,descending", true, false, -1}, 170 {"size,mixed,50", false, false, 25}, 171 {"size,mixed,51", true, true, 75}, 172 } { 173 t.Run(test.orderBy, func(t *testing.T) { 174 p, err := newPipe(test.orderBy, stats, 10) 175 require.NoError(t, err) 176 177 readAndCheck := func(swapped bool) { 178 var readFirst, readSecond fs.ObjectPair 179 var ok1, ok2 bool 180 if test.fraction < 0 { 181 readFirst, ok1 = p.Get(ctx) 182 readSecond, ok2 = p.Get(ctx) 183 } else { 184 readFirst, ok1 = p.GetMax(ctx, test.fraction) 185 readSecond, ok2 = p.GetMax(ctx, test.fraction) 186 } 187 assert.True(t, ok1) 188 assert.True(t, ok2) 189 190 if swapped { 191 assert.True(t, readFirst == pair2 && readSecond == pair1) 192 } else { 193 assert.True(t, readFirst == pair1 && readSecond == pair2) 194 } 195 } 196 197 ok := p.Put(ctx, pair1) 198 assert.True(t, ok) 199 ok = p.Put(ctx, pair2) 200 assert.True(t, ok) 201 202 readAndCheck(test.swapped1) 203 204 // insert other way round 205 206 ok = p.Put(ctx, pair2) 207 assert.True(t, ok) 208 ok = p.Put(ctx, pair1) 209 assert.True(t, ok) 210 211 readAndCheck(test.swapped2) 212 }) 213 } 214 } 215 216 func TestNewLess(t *testing.T) { 217 t.Run("blankOK", func(t *testing.T) { 218 less, _, err := newLess("") 219 require.NoError(t, err) 220 assert.Nil(t, less) 221 }) 222 223 t.Run("tooManyParts", func(t *testing.T) { 224 _, _, err := newLess("size,asc,toomanyparts") 225 require.Error(t, err) 226 assert.Contains(t, err.Error(), "bad --order-by string") 227 }) 228 229 t.Run("tooManyParts2", func(t *testing.T) { 230 _, _, err := newLess("size,mixed,50,toomanyparts") 231 require.Error(t, err) 232 assert.Contains(t, err.Error(), "bad --order-by string") 233 }) 234 235 t.Run("badMixed", func(t *testing.T) { 236 _, _, err := newLess("size,mixed,32.7") 237 require.Error(t, err) 238 assert.Contains(t, err.Error(), "bad mixed fraction") 239 }) 240 241 t.Run("unknownComparison", func(t *testing.T) { 242 _, _, err := newLess("potato") 243 require.Error(t, err) 244 assert.Contains(t, err.Error(), "unknown --order-by comparison") 245 }) 246 247 t.Run("unknownSortDirection", func(t *testing.T) { 248 _, _, err := newLess("name,sideways") 249 require.Error(t, err) 250 assert.Contains(t, err.Error(), "unknown --order-by sort direction") 251 }) 252 253 var ( 254 obj1 = mockobject.New("b").WithContent([]byte("1"), mockobject.SeekModeNone) 255 obj2 = mockobject.New("a").WithContent([]byte("22"), mockobject.SeekModeNone) 256 pair1 = fs.ObjectPair{Src: obj1} 257 pair2 = fs.ObjectPair{Src: obj2} 258 ) 259 260 for _, test := range []struct { 261 orderBy string 262 pair1LessPair2 bool 263 pair2LessPair1 bool 264 wantFraction int 265 }{ 266 {"size", true, false, -1}, 267 {"name", false, true, -1}, 268 {"modtime", false, false, -1}, 269 {"size,ascending", true, false, -1}, 270 {"name,asc", false, true, -1}, 271 {"modtime,ascending", false, false, -1}, 272 {"size,descending", false, true, -1}, 273 {"name,desc", true, false, -1}, 274 {"modtime,descending", true, true, -1}, 275 {"modtime,mixed", false, false, 50}, 276 {"modtime,mixed,30", false, false, 30}, 277 } { 278 t.Run(test.orderBy, func(t *testing.T) { 279 less, gotFraction, err := newLess(test.orderBy) 280 assert.Equal(t, test.wantFraction, gotFraction) 281 require.NoError(t, err) 282 require.NotNil(t, less) 283 pair1LessPair2 := less(pair1, pair2) 284 assert.Equal(t, test.pair1LessPair2, pair1LessPair2) 285 pair2LessPair1 := less(pair2, pair1) 286 assert.Equal(t, test.pair2LessPair1, pair2LessPair1) 287 }) 288 } 289 290 }