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  }