github.com/Heebron/moby@v0.0.0-20221111184709-6eab4f55faf7/daemon/logger/loggerutils/sharedtemp_test.go (about)

     1  package loggerutils // import "github.com/docker/docker/daemon/logger/loggerutils"
     2  
     3  import (
     4  	"io"
     5  	"io/fs"
     6  	"os"
     7  	"path/filepath"
     8  	"runtime"
     9  	"strings"
    10  	"sync"
    11  	"sync/atomic"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/pkg/errors"
    16  	"gotest.tools/v3/assert"
    17  	is "gotest.tools/v3/assert/cmp"
    18  )
    19  
    20  func TestSharedTempFileConverter(t *testing.T) {
    21  	t.Parallel()
    22  
    23  	t.Run("OneReaderAtATime", func(t *testing.T) {
    24  		t.Parallel()
    25  		dir := t.TempDir()
    26  		name := filepath.Join(dir, "test.txt")
    27  		createFile(t, name, "hello, world!")
    28  
    29  		uut := newSharedTempFileConverter(copyTransform(strings.ToUpper))
    30  		uut.TempDir = dir
    31  
    32  		for i := 0; i < 3; i++ {
    33  			t.Logf("Iteration %v", i)
    34  
    35  			rdr := convertPath(t, uut, name)
    36  			assert.Check(t, is.Equal("HELLO, WORLD!", readAll(t, rdr)))
    37  			assert.Check(t, rdr.Close())
    38  			assert.Check(t, is.Equal(fs.ErrClosed, rdr.Close()), "closing an already-closed reader should return an error")
    39  		}
    40  
    41  		assert.NilError(t, os.Remove(name))
    42  		checkDirEmpty(t, dir)
    43  	})
    44  
    45  	t.Run("RobustToRenames", func(t *testing.T) {
    46  		t.Parallel()
    47  		dir := t.TempDir()
    48  		apath := filepath.Join(dir, "test.txt")
    49  		createFile(t, apath, "file a")
    50  
    51  		var conversions int
    52  		uut := newSharedTempFileConverter(
    53  			func(dst io.WriteSeeker, src io.ReadSeeker) error {
    54  				conversions++
    55  				return copyTransform(strings.ToUpper)(dst, src)
    56  			},
    57  		)
    58  		uut.TempDir = dir
    59  
    60  		ra1 := convertPath(t, uut, apath)
    61  
    62  		// Rotate the file to a new name and write a new file in its place.
    63  		bpath := apath
    64  		apath = filepath.Join(dir, "test2.txt")
    65  		assert.NilError(t, os.Rename(bpath, apath))
    66  		createFile(t, bpath, "file b")
    67  
    68  		rb1 := convertPath(t, uut, bpath) // Same path, different file.
    69  		ra2 := convertPath(t, uut, apath) // New path, old file.
    70  		assert.Check(t, is.Equal(2, conversions), "expected only one conversion per unique file")
    71  
    72  		// Interleave reading and closing to shake out ref-counting bugs:
    73  		// closing one reader shouldn't affect any other open readers.
    74  		assert.Check(t, is.Equal("FILE A", readAll(t, ra1)))
    75  		assert.NilError(t, ra1.Close())
    76  		assert.Check(t, is.Equal("FILE A", readAll(t, ra2)))
    77  		assert.NilError(t, ra2.Close())
    78  		assert.Check(t, is.Equal("FILE B", readAll(t, rb1)))
    79  		assert.NilError(t, rb1.Close())
    80  
    81  		assert.NilError(t, os.Remove(apath))
    82  		assert.NilError(t, os.Remove(bpath))
    83  		checkDirEmpty(t, dir)
    84  	})
    85  
    86  	t.Run("ConcurrentRequests", func(t *testing.T) {
    87  		t.Parallel()
    88  		dir := t.TempDir()
    89  		name := filepath.Join(dir, "test.txt")
    90  		createFile(t, name, "hi there")
    91  
    92  		var conversions int32
    93  		notify := make(chan chan struct{}, 1)
    94  		firstConversionStarted := make(chan struct{})
    95  		notify <- firstConversionStarted
    96  		unblock := make(chan struct{})
    97  		uut := newSharedTempFileConverter(
    98  			func(dst io.WriteSeeker, src io.ReadSeeker) error {
    99  				t.Log("Convert: enter")
   100  				defer t.Log("Convert: exit")
   101  				select {
   102  				case c := <-notify:
   103  					close(c)
   104  				default:
   105  				}
   106  				<-unblock
   107  				atomic.AddInt32(&conversions, 1)
   108  				return copyTransform(strings.ToUpper)(dst, src)
   109  			},
   110  		)
   111  		uut.TempDir = dir
   112  
   113  		closers := make(chan io.Closer, 4)
   114  		var wg sync.WaitGroup
   115  		wg.Add(3)
   116  		for i := 0; i < 3; i++ {
   117  			i := i
   118  			go func() {
   119  				defer wg.Done()
   120  				t.Logf("goroutine %v: enter", i)
   121  				defer t.Logf("goroutine %v: exit", i)
   122  				f := convertPath(t, uut, name)
   123  				assert.Check(t, is.Equal("HI THERE", readAll(t, f)), "in goroutine %v", i)
   124  				closers <- f
   125  			}()
   126  		}
   127  
   128  		select {
   129  		case <-firstConversionStarted:
   130  		case <-time.After(2 * time.Second):
   131  			t.Fatal("the first conversion should have started by now")
   132  		}
   133  		close(unblock)
   134  		t.Log("starting wait")
   135  		wg.Wait()
   136  		t.Log("wait done")
   137  
   138  		f := convertPath(t, uut, name)
   139  		closers <- f
   140  		close(closers)
   141  		assert.Check(t, is.Equal("HI THERE", readAll(t, f)), "after all goroutines returned")
   142  		for c := range closers {
   143  			assert.Check(t, c.Close())
   144  		}
   145  
   146  		assert.Check(t, is.Equal(int32(1), conversions))
   147  
   148  		assert.NilError(t, os.Remove(name))
   149  		checkDirEmpty(t, dir)
   150  	})
   151  
   152  	t.Run("ConvertError", func(t *testing.T) {
   153  		t.Parallel()
   154  		dir := t.TempDir()
   155  		name := filepath.Join(dir, "test.txt")
   156  		createFile(t, name, "hi there")
   157  		src, err := open(name)
   158  		assert.NilError(t, err)
   159  		defer src.Close()
   160  
   161  		fakeErr := errors.New("fake error")
   162  		var start sync.WaitGroup
   163  		start.Add(3)
   164  		uut := newSharedTempFileConverter(
   165  			func(dst io.WriteSeeker, src io.ReadSeeker) error {
   166  				start.Wait()
   167  				runtime.Gosched()
   168  				if fakeErr != nil {
   169  					return fakeErr
   170  				}
   171  				return copyTransform(strings.ToUpper)(dst, src)
   172  			},
   173  		)
   174  		uut.TempDir = dir
   175  
   176  		var done sync.WaitGroup
   177  		done.Add(3)
   178  		for i := 0; i < 3; i++ {
   179  			i := i
   180  			go func() {
   181  				defer done.Done()
   182  				t.Logf("goroutine %v: enter", i)
   183  				defer t.Logf("goroutine %v: exit", i)
   184  				start.Done()
   185  				_, err := uut.Do(src)
   186  				assert.Check(t, errors.Is(err, fakeErr), "in goroutine %v", i)
   187  			}()
   188  		}
   189  		done.Wait()
   190  
   191  		// Conversion errors should not be "sticky". A subsequent
   192  		// request should retry from scratch.
   193  		fakeErr = errors.New("another fake error")
   194  		_, err = uut.Do(src)
   195  		assert.Check(t, errors.Is(err, fakeErr))
   196  
   197  		fakeErr = nil
   198  		f, err := uut.Do(src)
   199  		assert.Check(t, err)
   200  		assert.Check(t, is.Equal("HI THERE", readAll(t, f)))
   201  		assert.Check(t, f.Close())
   202  
   203  		// Files pending delete continue to show up in directory
   204  		// listings on Windows RS5. Close the remaining handle before
   205  		// deleting the file to prevent spurious failures with
   206  		// checkDirEmpty.
   207  		assert.Check(t, src.Close())
   208  		assert.NilError(t, os.Remove(name))
   209  		checkDirEmpty(t, dir)
   210  	})
   211  }
   212  
   213  func createFile(t *testing.T, path string, content string) {
   214  	t.Helper()
   215  	f, err := openFile(path, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0644)
   216  	assert.NilError(t, err)
   217  	_, err = io.WriteString(f, content)
   218  	assert.NilError(t, err)
   219  	assert.NilError(t, f.Close())
   220  }
   221  
   222  func convertPath(t *testing.T, uut *sharedTempFileConverter, path string) *sharedFileReader {
   223  	t.Helper()
   224  	f, err := open(path)
   225  	assert.NilError(t, err)
   226  	defer func() { assert.NilError(t, f.Close()) }()
   227  	r, err := uut.Do(f)
   228  	assert.NilError(t, err)
   229  	return r
   230  }
   231  
   232  func readAll(t *testing.T, r io.Reader) string {
   233  	t.Helper()
   234  	v, err := io.ReadAll(r)
   235  	assert.NilError(t, err)
   236  	return string(v)
   237  }
   238  
   239  func checkDirEmpty(t *testing.T, path string) {
   240  	t.Helper()
   241  	ls, err := os.ReadDir(path)
   242  	assert.NilError(t, err)
   243  	assert.Check(t, is.Len(ls, 0), "directory should be free of temp files")
   244  }
   245  
   246  func copyTransform(f func(string) string) func(dst io.WriteSeeker, src io.ReadSeeker) error {
   247  	return func(dst io.WriteSeeker, src io.ReadSeeker) error {
   248  		s, err := io.ReadAll(src)
   249  		if err != nil {
   250  			return err
   251  		}
   252  		_, err = io.WriteString(dst, f(string(s)))
   253  		return err
   254  	}
   255  }