mirror of
https://github.com/rclone/rclone.git
synced 2024-12-23 15:38:57 +01:00
71e98ea584
Before this change, renaming and deleting of open files (which can easily happen due to the asynchronous nature of file systems) would produce an error, for example saving files with Firefox. After this change we open files with the flags necessary for open files to be renamed or deleted. Fixes #2730
333 lines
6.0 KiB
Go
333 lines
6.0 KiB
Go
// Test the VFS to exhaustion, specifically looking for deadlocks
|
|
//
|
|
// Run on a mounted filesystem
|
|
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"math"
|
|
"math/rand"
|
|
"os"
|
|
"path"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/ncw/rclone/lib/file"
|
|
)
|
|
|
|
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 int32
|
|
)
|
|
|
|
// Seed the random number generator
|
|
func init() {
|
|
rand.Seed(time.Now().UnixNano())
|
|
|
|
}
|
|
|
|
// RandomString create a random string for test purposes
|
|
func RandomString(n int) string {
|
|
const (
|
|
vowel = "aeiou"
|
|
consonant = "bcdfghjklmnpqrstvwxyz"
|
|
digit = "0123456789"
|
|
)
|
|
pattern := []string{consonant, vowel, consonant, vowel, consonant, vowel, consonant, digit}
|
|
out := make([]byte, n)
|
|
p := 0
|
|
for i := range out {
|
|
source := pattern[p]
|
|
p = (p + 1) % len(pattern)
|
|
out[i] = source[rand.Intn(len(source))]
|
|
}
|
|
return string(out)
|
|
}
|
|
|
|
// 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: RandomString(*nameLength),
|
|
isDir: rand.Intn(2) == 0,
|
|
number: atomic.AddInt32(&testNumber, 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 := ioutil.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 := RandomString(*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]
|
|
_ = os.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()
|
|
}
|