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 }