github.com/wtsi-ssg/wrstat@v1.1.4-0.20221008232152-3030622a8cf8/stat/paths_test.go (about) 1 /******************************************************************************* 2 * Copyright (c) 2021 Genome Research Ltd. 3 * 4 * Author: Sendu Bala <sb10@sanger.ac.uk> 5 * 6 * Permission is hereby granted, free of charge, to any person obtaining 7 * a copy of this software and associated documentation files (the 8 * "Software"), to deal in the Software without restriction, including 9 * without limitation the rights to use, copy, modify, merge, publish, 10 * distribute, sublicense, and/or sell copies of the Software, and to 11 * permit persons to whom the Software is furnished to do so, subject to 12 * the following conditions: 13 * 14 * The above copyright notice and this permission notice shall be included 15 * in all copies or substantial portions of the Software. 16 * 17 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 22 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 ******************************************************************************/ 25 26 package stat 27 28 import ( 29 "io" 30 "io/fs" 31 "os" 32 "path/filepath" 33 "strings" 34 "sync/atomic" 35 "testing" 36 "time" 37 38 . "github.com/smartystreets/goconvey/convey" 39 ) 40 41 const errTestFail = Error("test fail") 42 const errTestFileDetails = Error("file details wrong") 43 44 func TestPaths(t *testing.T) { 45 statterTimeout := 1 * time.Second 46 statterRetries := 2 47 48 Convey("Given a Paths with a report frequency", t, func() { 49 buff, l := newLogger() 50 s := WithTimeout(statterTimeout, statterRetries, l) 51 p := NewPaths(s, l, 15*time.Millisecond) 52 So(p, ShouldNotBeNil) 53 54 Convey("You can't add an operation with the reserved name", func() { 55 err := p.AddOperation("lstat", func(string, fs.FileInfo) error { 56 return nil 57 }) 58 So(err, ShouldNotBeNil) 59 So(err, ShouldEqual, errReservedOpName) 60 }) 61 62 Convey("You can add operations", func() { 63 sleepN, failN := addTestOperations(p) 64 65 Convey("Which get called when you Scan, providing timing reports", func() { 66 r := createScanInput(t) 67 err := p.Scan(r) 68 So(err, ShouldBeNil) 69 70 So(*sleepN, ShouldEqual, 2) 71 So(buff.String(), ShouldContainSubstring, `lvl=info msg="report since last" op=check count=`) 72 So(buff.String(), ShouldContainSubstring, `lvl=info msg="report overall" op=check count=2`) 73 So(buff.String(), ShouldNotContainSubstring, `file details wrong`) 74 75 So(*failN, ShouldEqual, 2) 76 So(buff.String(), ShouldContainSubstring, `lvl=warn msg="operation error" op=fail err="test fail"`) 77 So(buff.String(), ShouldContainSubstring, `lvl=info msg="report since last" op=fail count=0 time=0s ops/s=n/a`) 78 So(buff.String(), ShouldContainSubstring, `lvl=info msg="report overall" op=fail count=0 time=0s ops/s=n/a`) 79 So(buff.String(), ShouldContainSubstring, `lvl=warn msg="report failed" op=fail count=2`) 80 81 So(buff.String(), ShouldContainSubstring, `lvl=info msg="report since last" op=lstat count=`) 82 So(buff.String(), ShouldContainSubstring, `lvl=info msg="report overall" op=lstat count=2`) 83 So(buff.String(), ShouldContainSubstring, `lvl=warn msg="report failed" op=lstat count=1`) 84 }) 85 }) 86 }) 87 88 Convey("Given a Paths with 0 report frequency", t, func() { 89 buff, l := newLogger() 90 s := WithTimeout(statterTimeout, statterRetries, l) 91 p := NewPaths(s, l, 0) 92 So(p, ShouldNotBeNil) 93 94 r := createScanInput(t) 95 96 Convey("You can add operations and scan with no timing reports", func() { 97 checkN, failN := addTestOperations(p) 98 err := p.Scan(r) 99 So(err, ShouldBeNil) 100 101 So(*checkN, ShouldEqual, 2) 102 So(*failN, ShouldEqual, 2) 103 So(buff.String(), ShouldNotContainSubstring, `lvl=info msg="report`) 104 So(buff.String(), ShouldContainSubstring, `lvl=warn msg="operation error" op=fail err="test fail"`) 105 }) 106 107 Convey("Operations you add run concurrently during a scan", func() { 108 addTestablyConcurrentOperations(p) 109 err := p.Scan(r) 110 So(err, ShouldBeNil) 111 So(buff.String(), ShouldNotContainSubstring, `lvl=warn msg="operation error" op=first err="test fail"`) 112 So(buff.String(), ShouldNotContainSubstring, `lvl=warn msg="operation error" op=second err="test fail"`) 113 }) 114 115 Convey("Operations you add run concurrently with the next lstat", func() { 116 addOperationConcurrentWithLstat(p) 117 err := p.Scan(r) 118 So(err, ShouldBeNil) 119 So(buff.String(), ShouldNotContainSubstring, `lvl=warn msg="operation error" op=withlstat err="test fail"`) 120 }) 121 122 Convey("FileOperation works as expected", func() { 123 dir := t.TempDir() 124 outPath := filepath.Join(dir, "out") 125 out, err := os.Create(outPath) 126 So(err, ShouldBeNil) 127 128 err = p.AddOperation("file", FileOperation(out)) 129 So(err, ShouldBeNil) 130 131 err = p.Scan(r) 132 So(err, ShouldBeNil) 133 134 err = out.Close() 135 So(err, ShouldBeNil) 136 output, err := os.ReadFile(outPath) 137 So(err, ShouldBeNil) 138 139 So(string(output), ShouldContainSubstring, "\t0\t") 140 So(string(output), ShouldContainSubstring, "\t1\t") 141 So(string(output), ShouldContainSubstring, "\tf\t") 142 }) 143 144 Convey("SizeOperation works as expected", func() { 145 dir := t.TempDir() 146 outPath := filepath.Join(dir, "out") 147 out, err := os.Create(outPath) 148 So(err, ShouldBeNil) 149 150 err = p.AddOperation("szie", SizeOperation(out)) 151 So(err, ShouldBeNil) 152 153 err = p.Scan(r) 154 So(err, ShouldBeNil) 155 156 err = out.Close() 157 So(err, ShouldBeNil) 158 output, err := os.ReadFile(outPath) 159 So(err, ShouldBeNil) 160 161 So(string(output), ShouldContainSubstring, "empty\t0\n") 162 So(string(output), ShouldContainSubstring, "content\t1\n") 163 }) 164 }) 165 } 166 167 // addTestOperations adds a "check" and a "fail" operation to the given Paths, 168 // and returns counters that tell you how many times each was called. 169 func addTestOperations(p *Paths) (*int, *int) { 170 checkN := 0 171 err := p.AddOperation("check", func(absPath string, info fs.FileInfo) error { 172 <-time.After(5 * time.Millisecond) 173 checkN++ 174 175 return checkFileDetails(absPath, info) 176 }) 177 So(err, ShouldBeNil) 178 179 failN := 0 180 err = p.AddOperation("fail", func(string, fs.FileInfo) error { 181 failN++ 182 183 return errTestFail 184 }) 185 So(err, ShouldBeNil) 186 187 So(len(p.ops), ShouldEqual, 2) 188 So(len(p.reporters), ShouldEqual, 2) 189 190 return &checkN, &failN 191 } 192 193 // checkFileDetails checks that we actually get expected file paths and info 194 // in our Operation callbacks. 195 func checkFileDetails(absPath string, info fs.FileInfo) error { 196 switch { 197 case strings.HasSuffix(absPath, "empty"): 198 if info.Size() != 0 { 199 return errTestFileDetails 200 } 201 case strings.HasSuffix(absPath, "content"): 202 if info.Size() != 1 { 203 return errTestFileDetails 204 } 205 default: 206 return errTestFileDetails 207 } 208 209 return nil 210 } 211 212 // createScanInput creates 2 test files and an io.Reader of their paths, with 213 // a non-existent path in between. 214 func createScanInput(t *testing.T) io.Reader { 215 t.Helper() 216 217 pathEmpty, pathContent := createTestFiles(t) 218 r := strings.NewReader(pathEmpty + "\n/foo/bar\n" + pathContent) 219 220 return r 221 } 222 223 // addTestablyConcurrentOperations adds 2 operations "first" and "second" to p 224 // that would only work if they were running concurrently, not one after the 225 // other. 226 func addTestablyConcurrentOperations(p *Paths) { 227 testCh := make(chan bool) 228 err := p.AddOperation("first", func(string, fs.FileInfo) error { 229 select { 230 case <-time.After(1 * time.Second): 231 return errTestFail 232 case testCh <- true: 233 return nil 234 } 235 }) 236 So(err, ShouldBeNil) 237 238 err = p.AddOperation("second", func(string, fs.FileInfo) error { 239 select { 240 case <-time.After(1 * time.Second): 241 return errTestFail 242 case <-testCh: 243 return nil 244 } 245 }) 246 So(err, ShouldBeNil) 247 } 248 249 // addOperationConcurrentWithLstat adds 1 operation "withlstat" that would only 250 // work if run concurrently with the following Lstat call. It also would fail if 251 // run concurrently with itself (ie. this tests that after the Lstat call, we 252 // wait for previous Operations to complete). 253 func addOperationConcurrentWithLstat(p *Paths) { 254 testCh := make(chan bool) 255 p.statter = &statterWithConcurrentTest{ch: testCh} 256 257 var started, ended int32 258 259 err := p.AddOperation("withlstat", func(absPath string, info fs.FileInfo) error { 260 atomic.AddInt32(&started, 1) 261 262 if atomic.LoadInt32(&started) != atomic.LoadInt32(&ended)+1 { 263 return errTestFail 264 } 265 266 if atomic.LoadInt32(&started) == 3 { 267 return nil 268 } 269 270 select { 271 case <-time.After(1 * time.Second): 272 return errTestFail 273 case testCh <- true: 274 } 275 276 <-time.After(50 * time.Millisecond) 277 atomic.AddInt32(&ended, 1) 278 279 return nil 280 }) 281 So(err, ShouldBeNil) 282 } 283 284 // statterWithConcurrentTest is used in addOperationConcurrentWithLstat() to 285 // enable that test. 286 type statterWithConcurrentTest struct { 287 ch chan bool 288 i int 289 } 290 291 func (s *statterWithConcurrentTest) Lstat(path string) (info fs.FileInfo, err error) { 292 s.i++ 293 if s.i == 1 { 294 return 295 } 296 297 select { 298 case <-time.After(1 * time.Second): 299 err = errTestFail 300 case <-s.ch: 301 } 302 303 return 304 }