938b43c26c
Now that the minimum version is go1.20 we can stop seeding the random number generator.
308 lines
5.5 KiB
Go
308 lines
5.5 KiB
Go
// Test the VFS to exhaustion, specifically looking for deadlocks
|
|
//
|
|
// Run on a mounted filesystem
|
|
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"math"
|
|
"math/rand"
|
|
"os"
|
|
"path"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/rclone/rclone/lib/file"
|
|
"github.com/rclone/rclone/lib/random"
|
|
)
|
|
|
|
var (
|
|
nameLength = flag.Int("name-length", 10, "Length of names to create")
|
|
verbose = flag.Bool("v", false, "Set to show more info")
|
|
number = flag.Int("n", 4, "Number of tests to run simultaneously")
|
|
iterations = flag.Int("i", 100, "Iterations of the test")
|
|
timeout = flag.Duration("timeout", 10*time.Second, "Inactivity time to detect a deadlock")
|
|
testNumber atomic.Int32
|
|
)
|
|
|
|
// Test contains stats about the running test which work for files or
|
|
// directories
|
|
type Test struct {
|
|
dir string
|
|
name string
|
|
created bool
|
|
handle *os.File
|
|
tests []func()
|
|
isDir bool
|
|
number int32
|
|
prefix string
|
|
timer *time.Timer
|
|
}
|
|
|
|
// NewTest creates a new test and fills in the Tests
|
|
func NewTest(Dir string) *Test {
|
|
t := &Test{
|
|
dir: Dir,
|
|
name: random.String(*nameLength),
|
|
isDir: rand.Intn(2) == 0,
|
|
number: testNumber.Add(1),
|
|
timer: time.NewTimer(*timeout),
|
|
}
|
|
width := int(math.Floor(math.Log10(float64(*number)))) + 1
|
|
t.prefix = fmt.Sprintf("%*d: %s: ", width, t.number, t.path())
|
|
if t.isDir {
|
|
t.tests = []func(){
|
|
t.list,
|
|
t.rename,
|
|
t.mkdir,
|
|
t.rmdir,
|
|
}
|
|
} else {
|
|
t.tests = []func(){
|
|
t.list,
|
|
t.rename,
|
|
t.open,
|
|
t.close,
|
|
t.remove,
|
|
t.read,
|
|
t.write,
|
|
}
|
|
}
|
|
return t
|
|
}
|
|
|
|
// kick the deadlock timeout
|
|
func (t *Test) kick() {
|
|
if !t.timer.Stop() {
|
|
<-t.timer.C
|
|
}
|
|
t.timer.Reset(*timeout)
|
|
}
|
|
|
|
// randomTest runs a random test
|
|
func (t *Test) randomTest() {
|
|
t.kick()
|
|
i := rand.Intn(len(t.tests))
|
|
t.tests[i]()
|
|
}
|
|
|
|
// logf logs things - not shown unless -v
|
|
func (t *Test) logf(format string, a ...interface{}) {
|
|
if *verbose {
|
|
log.Printf(t.prefix+format, a...)
|
|
}
|
|
}
|
|
|
|
// errorf logs errors
|
|
func (t *Test) errorf(format string, a ...interface{}) {
|
|
log.Printf(t.prefix+"ERROR: "+format, a...)
|
|
}
|
|
|
|
// list test
|
|
func (t *Test) list() {
|
|
t.logf("list")
|
|
fis, err := os.ReadDir(t.dir)
|
|
if err != nil {
|
|
t.errorf("%s: failed to read directory: %v", t.dir, err)
|
|
return
|
|
}
|
|
if t.created && len(fis) == 0 {
|
|
t.errorf("%s: expecting entries in directory, got none", t.dir)
|
|
return
|
|
}
|
|
found := false
|
|
for _, fi := range fis {
|
|
if fi.Name() == t.name {
|
|
found = true
|
|
}
|
|
}
|
|
if t.created {
|
|
if !found {
|
|
t.errorf("%s: expecting to find %q in directory, got none", t.dir, t.name)
|
|
return
|
|
}
|
|
} else {
|
|
if found {
|
|
t.errorf("%s: not expecting to find %q in directory, got none", t.dir, t.name)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// path returns the current path to the item
|
|
func (t *Test) path() string {
|
|
return path.Join(t.dir, t.name)
|
|
}
|
|
|
|
// rename test
|
|
func (t *Test) rename() {
|
|
if !t.created {
|
|
return
|
|
}
|
|
t.logf("rename")
|
|
NewName := random.String(*nameLength)
|
|
newPath := path.Join(t.dir, NewName)
|
|
err := os.Rename(t.path(), newPath)
|
|
if err != nil {
|
|
t.errorf("failed to rename to %q: %v", newPath, err)
|
|
return
|
|
}
|
|
t.name = NewName
|
|
}
|
|
|
|
// close test
|
|
func (t *Test) close() {
|
|
if t.handle == nil {
|
|
return
|
|
}
|
|
t.logf("close")
|
|
err := t.handle.Close()
|
|
t.handle = nil
|
|
if err != nil {
|
|
t.errorf("failed to close: %v", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// open test
|
|
func (t *Test) open() {
|
|
t.close()
|
|
t.logf("open")
|
|
handle, err := file.OpenFile(t.path(), os.O_RDWR|os.O_CREATE, 0666)
|
|
if err != nil {
|
|
t.errorf("failed to open: %v", err)
|
|
return
|
|
}
|
|
t.handle = handle
|
|
t.created = true
|
|
}
|
|
|
|
// read test
|
|
func (t *Test) read() {
|
|
if t.handle == nil {
|
|
return
|
|
}
|
|
t.logf("read")
|
|
bytes := make([]byte, 10)
|
|
_, err := t.handle.Read(bytes)
|
|
if err != nil && err != io.EOF {
|
|
t.errorf("failed to read: %v", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// write test
|
|
func (t *Test) write() {
|
|
if t.handle == nil {
|
|
return
|
|
}
|
|
t.logf("write")
|
|
bytes := make([]byte, 10)
|
|
_, err := t.handle.Write(bytes)
|
|
if err != nil {
|
|
t.errorf("failed to write: %v", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// remove test
|
|
func (t *Test) remove() {
|
|
if !t.created {
|
|
return
|
|
}
|
|
t.logf("remove")
|
|
err := os.Remove(t.path())
|
|
if err != nil {
|
|
t.errorf("failed to remove: %v", err)
|
|
return
|
|
}
|
|
t.created = false
|
|
}
|
|
|
|
// mkdir test
|
|
func (t *Test) mkdir() {
|
|
if t.created {
|
|
return
|
|
}
|
|
t.logf("mkdir")
|
|
err := os.Mkdir(t.path(), 0777)
|
|
if err != nil {
|
|
t.errorf("failed to mkdir %q", t.path())
|
|
return
|
|
}
|
|
t.created = true
|
|
}
|
|
|
|
// rmdir test
|
|
func (t *Test) rmdir() {
|
|
if !t.created {
|
|
return
|
|
}
|
|
t.logf("rmdir")
|
|
err := os.Remove(t.path())
|
|
if err != nil {
|
|
t.errorf("failed to rmdir %q", t.path())
|
|
return
|
|
}
|
|
t.created = false
|
|
}
|
|
|
|
// Tidy removes any stray files and stops the deadlock timer
|
|
func (t *Test) Tidy() {
|
|
t.timer.Stop()
|
|
if !t.isDir {
|
|
t.close()
|
|
t.remove()
|
|
} else {
|
|
t.rmdir()
|
|
}
|
|
t.logf("finished")
|
|
}
|
|
|
|
// RandomTests runs random tests with deadlock detection
|
|
func (t *Test) RandomTests(iterations int, quit chan struct{}) {
|
|
var finished = make(chan struct{})
|
|
go func() {
|
|
for i := 0; i < iterations; i++ {
|
|
t.randomTest()
|
|
}
|
|
close(finished)
|
|
}()
|
|
select {
|
|
case <-finished:
|
|
case <-quit:
|
|
quit <- struct{}{}
|
|
case <-t.timer.C:
|
|
t.errorf("deadlock detected")
|
|
quit <- struct{}{}
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
args := flag.Args()
|
|
if len(args) != 1 {
|
|
log.Fatalf("%s: Syntax [opts] <directory>", os.Args[0])
|
|
}
|
|
dir := args[0]
|
|
_ = file.MkdirAll(dir, 0777)
|
|
|
|
var (
|
|
wg sync.WaitGroup
|
|
quit = make(chan struct{}, *iterations)
|
|
)
|
|
for i := 0; i < *number; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
t := NewTest(dir)
|
|
defer t.Tidy()
|
|
t.RandomTests(*iterations, quit)
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
}
|