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  }