增加运行测试的命令;修复一些测试问题
This commit is contained in:
parent
67c29cbf1c
commit
ee5deffc82
|
@ -0,0 +1,231 @@
|
|||
package cmdline
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"gitlink.org.cn/cloudream/common/pkgs/logger"
|
||||
"gitlink.org.cn/cloudream/storage2/client/internal/accessstat"
|
||||
"gitlink.org.cn/cloudream/storage2/client/internal/config"
|
||||
"gitlink.org.cn/cloudream/storage2/client/internal/db"
|
||||
"gitlink.org.cn/cloudream/storage2/client/internal/downloader"
|
||||
"gitlink.org.cn/cloudream/storage2/client/internal/downloader/strategy"
|
||||
"gitlink.org.cn/cloudream/storage2/client/internal/http"
|
||||
"gitlink.org.cn/cloudream/storage2/client/internal/metacache"
|
||||
"gitlink.org.cn/cloudream/storage2/client/internal/mount"
|
||||
"gitlink.org.cn/cloudream/storage2/client/internal/mount/vfstest"
|
||||
"gitlink.org.cn/cloudream/storage2/client/internal/services"
|
||||
"gitlink.org.cn/cloudream/storage2/client/internal/uploader"
|
||||
stgglb "gitlink.org.cn/cloudream/storage2/common/globals"
|
||||
"gitlink.org.cn/cloudream/storage2/common/models/datamap"
|
||||
"gitlink.org.cn/cloudream/storage2/common/pkgs/connectivity"
|
||||
"gitlink.org.cn/cloudream/storage2/common/pkgs/distlock"
|
||||
"gitlink.org.cn/cloudream/storage2/common/pkgs/storage/pool"
|
||||
"gitlink.org.cn/cloudream/storage2/common/pkgs/sysevent"
|
||||
)
|
||||
|
||||
// 初始化函数,将ServeHTTP命令注册到命令列表中。
|
||||
func init() {
|
||||
var configPath string
|
||||
var opt serveHTTPOptions
|
||||
cmd := cobra.Command{
|
||||
Use: "vfstest",
|
||||
Short: "test vfs",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
vfsTest(configPath, opt)
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&configPath, "config", "c", "", "config file path")
|
||||
cmd.Flags().BoolVarP(&opt.DisableHTTP, "no-http", "", false, "disable http server")
|
||||
cmd.Flags().StringVarP(&opt.HTTPListenAddr, "listen", "", "", "http listen address, will override config file")
|
||||
cmd.Flags().BoolVarP(&opt.DisableMount, "no-mount", "", false, "disable mount")
|
||||
cmd.Flags().StringVarP(&opt.MountPoint, "mount", "", "", "mount point, will override config file")
|
||||
RootCmd.AddCommand(&cmd)
|
||||
}
|
||||
|
||||
func vfsTest(configPath string, opts serveHTTPOptions) {
|
||||
err := config.Init(configPath)
|
||||
if err != nil {
|
||||
fmt.Printf("init config failed, err: %s", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = logger.Init(&config.Cfg().Logger)
|
||||
if err != nil {
|
||||
fmt.Printf("init logger failed, err: %s", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
stgglb.InitLocal(config.Cfg().Local)
|
||||
stgglb.InitMQPool(config.Cfg().RabbitMQ)
|
||||
stgglb.InitHubRPCPool(&config.Cfg().HubGRPC)
|
||||
|
||||
// 数据库
|
||||
db, err := db.NewDB(&config.Cfg().DB)
|
||||
if err != nil {
|
||||
logger.Fatalf("new db failed, err: %s", err.Error())
|
||||
}
|
||||
|
||||
// 初始化系统事件发布器
|
||||
evtPub, err := sysevent.NewPublisher(sysevent.ConfigFromMQConfig(config.Cfg().RabbitMQ), &datamap.SourceClient{
|
||||
UserID: config.Cfg().Local.UserID,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Errorf("new sysevent publisher: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
evtPubChan := evtPub.Start()
|
||||
defer evtPub.Stop()
|
||||
|
||||
// 连接性信息收集
|
||||
conCol := connectivity.NewCollector(&config.Cfg().Connectivity, nil)
|
||||
conCol.CollectInPlace()
|
||||
|
||||
// 元数据缓存
|
||||
metaCacheHost := metacache.NewHost(db)
|
||||
go metaCacheHost.Serve()
|
||||
stgMeta := metaCacheHost.AddStorageMeta()
|
||||
hubMeta := metaCacheHost.AddHubMeta()
|
||||
conMeta := metaCacheHost.AddConnectivity()
|
||||
|
||||
// 分布式锁
|
||||
distlockSvc, err := distlock.NewService(&config.Cfg().DistLock)
|
||||
if err != nil {
|
||||
logger.Warnf("new distlock service failed, err: %s", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
go serveDistLock(distlockSvc)
|
||||
|
||||
// 访问统计
|
||||
acStat := accessstat.NewAccessStat(accessstat.Config{
|
||||
// TODO 考虑放到配置里
|
||||
ReportInterval: time.Second * 10,
|
||||
}, db)
|
||||
acStatChan := acStat.Start()
|
||||
defer acStat.Stop()
|
||||
|
||||
// 存储管理器
|
||||
stgPool := pool.NewPool()
|
||||
|
||||
// 下载策略
|
||||
strgSel := strategy.NewSelector(config.Cfg().DownloadStrategy, stgMeta, hubMeta, conMeta)
|
||||
|
||||
// 下载器
|
||||
dlder := downloader.NewDownloader(config.Cfg().Downloader, &conCol, stgPool, strgSel, db)
|
||||
|
||||
// 上传器
|
||||
uploader := uploader.NewUploader(distlockSvc, &conCol, stgPool, stgMeta, db)
|
||||
|
||||
// 挂载
|
||||
mntCfg := config.Cfg().Mount
|
||||
if !opts.DisableMount && mntCfg != nil && mntCfg.Enabled {
|
||||
if opts.MountPoint != "" {
|
||||
mntCfg.MountPoint = opts.MountPoint
|
||||
}
|
||||
} else {
|
||||
mntCfg = nil
|
||||
}
|
||||
mnt := mount.NewMount(mntCfg, db, uploader, dlder)
|
||||
mntChan := mnt.Start()
|
||||
defer mnt.Stop()
|
||||
|
||||
svc := services.NewService(distlockSvc, dlder, acStat, uploader, strgSel, stgMeta, db, evtPub, mnt)
|
||||
|
||||
// HTTP接口
|
||||
httpCfg := config.Cfg().HTTP
|
||||
if !opts.DisableHTTP && httpCfg != nil && httpCfg.Enabled {
|
||||
if opts.HTTPListenAddr != "" {
|
||||
httpCfg.Listen = opts.HTTPListenAddr
|
||||
}
|
||||
} else {
|
||||
httpCfg = nil
|
||||
}
|
||||
httpSvr := http.NewServer(httpCfg, svc)
|
||||
httpChan := httpSvr.Start()
|
||||
defer httpSvr.Stop()
|
||||
|
||||
go func() {
|
||||
<-time.After(5 * time.Second)
|
||||
testing.Main(nil, []testing.InternalTest{
|
||||
{"VFS", func(t *testing.T) {
|
||||
vfstest.RunTests(t, mnt)
|
||||
}},
|
||||
}, nil, nil)
|
||||
}()
|
||||
|
||||
/// 开始监听各个模块的事件
|
||||
|
||||
evtPubEvt := evtPubChan.Receive()
|
||||
acStatEvt := acStatChan.Receive()
|
||||
httpEvt := httpChan.Receive()
|
||||
mntEvt := mntChan.Receive()
|
||||
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case e := <-evtPubEvt.Chan():
|
||||
if e.Err != nil {
|
||||
logger.Errorf("receive publisher event: %v", err)
|
||||
break loop
|
||||
}
|
||||
|
||||
switch val := e.Value.(type) {
|
||||
case sysevent.PublishError:
|
||||
logger.Errorf("publishing event: %v", val)
|
||||
|
||||
case sysevent.PublisherExited:
|
||||
if val.Err != nil {
|
||||
logger.Errorf("publisher exited with error: %v", val.Err)
|
||||
} else {
|
||||
logger.Info("publisher exited")
|
||||
}
|
||||
break loop
|
||||
|
||||
case sysevent.OtherError:
|
||||
logger.Errorf("sysevent: %v", val)
|
||||
}
|
||||
evtPubEvt = evtPubChan.Receive()
|
||||
|
||||
case e := <-acStatEvt.Chan():
|
||||
if e.Err != nil {
|
||||
logger.Errorf("receive access stat event: %v", err)
|
||||
break loop
|
||||
}
|
||||
switch e := e.Value.(type) {
|
||||
case accessstat.ExitEvent:
|
||||
logger.Infof("access stat exited, err: %v", e.Err)
|
||||
break loop
|
||||
}
|
||||
acStatEvt = acStatChan.Receive()
|
||||
|
||||
case e := <-httpEvt.Chan():
|
||||
if e.Err != nil {
|
||||
logger.Errorf("receive http event: %v", err)
|
||||
break loop
|
||||
}
|
||||
|
||||
switch e := e.Value.(type) {
|
||||
case http.ExitEvent:
|
||||
logger.Infof("http server exited, err: %v", e.Err)
|
||||
break loop
|
||||
}
|
||||
httpEvt = httpChan.Receive()
|
||||
|
||||
case e := <-mntEvt.Chan():
|
||||
if e.Err != nil {
|
||||
logger.Errorf("receive mount event: %v", e.Err)
|
||||
break loop
|
||||
}
|
||||
|
||||
switch e := e.Value.(type) {
|
||||
case mount.ExitEvent:
|
||||
logger.Infof("mount exited, err: %v", e.Err)
|
||||
break loop
|
||||
}
|
||||
mntEvt = mntChan.Receive()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -566,10 +566,6 @@ func (db *ObjectDB) BatchUpdateRedundancy(ctx SQLContext, updates []UpdatingObje
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *ObjectDB) DeleteByPath(ctx SQLContext, packageID types.PackageID, path string) error {
|
||||
return ctx.Table("Object").Where("PackageID = ? AND Path = ?", packageID, path).Delete(&types.Object{}).Error
|
||||
}
|
||||
|
||||
func (db *ObjectDB) MoveByPrefix(ctx SQLContext, oldPkgID types.PackageID, oldPrefix string, newPkgID types.PackageID, newPrefix string) error {
|
||||
return ctx.Table("Object").Where("PackageID = ? AND Path LIKE ?", oldPkgID, escapeLike("", "%", oldPrefix)).
|
||||
Updates(map[string]any{
|
||||
|
@ -628,3 +624,36 @@ func (db *ObjectDB) AppendPart(tx SQLContext, block types.ObjectBlock) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *ObjectDB) BatchDeleteComplete(ctx SQLContext, objectIDs []types.ObjectID) error {
|
||||
err := db.Object().BatchDelete(ctx, objectIDs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("batch deleting objects: %w", err)
|
||||
}
|
||||
|
||||
err = db.ObjectBlock().BatchDeleteByObjectID(ctx, objectIDs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("batch deleting object blocks: %w", err)
|
||||
}
|
||||
|
||||
err = db.PinnedObject().BatchDeleteByObjectID(ctx, objectIDs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("batch deleting pinned objects: %w", err)
|
||||
}
|
||||
|
||||
err = db.ObjectAccessStat().BatchDeleteByObjectID(ctx, objectIDs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("batch deleting object access stats: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *ObjectDB) DeleteCompleteByPath(ctx SQLContext, packageID types.PackageID, path string) error {
|
||||
obj, err := db.Object().GetByPath(ctx, packageID, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return db.BatchDeleteComplete(ctx, []types.ObjectID{obj.ObjectID})
|
||||
}
|
||||
|
|
|
@ -77,6 +77,14 @@ func (m *Mount) Stop() {
|
|||
m.fuseServer.Unmount()
|
||||
}
|
||||
|
||||
func (m *Mount) MountPoint() string {
|
||||
if m.cfg == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return m.cfg.MountPoint
|
||||
}
|
||||
|
||||
func (m *Mount) Dump() MountStatus {
|
||||
if m.vfs == nil {
|
||||
return MountStatus{}
|
||||
|
|
|
@ -27,6 +27,10 @@ func (m *Mount) Start() *MountEventChan {
|
|||
return m.eventChan
|
||||
}
|
||||
|
||||
func (m *Mount) MountPoint() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Mount) Stop() {
|
||||
m.eventChan.Send(ExitEvent{})
|
||||
}
|
||||
|
|
|
@ -52,6 +52,7 @@ func loadCacheDir(c *Cache, pathComps []string) (*CacheDir, error) {
|
|||
}
|
||||
|
||||
return &CacheDir{
|
||||
cache: c,
|
||||
pathComps: pathComps,
|
||||
name: stat.Name(),
|
||||
modTime: stat.ModTime(),
|
||||
|
@ -75,6 +76,7 @@ func makeCacheDirFromOption(c *Cache, pathComps []string, opt CreateDirOption) (
|
|||
|
||||
os.Chtimes(dataPath, opt.ModTime, opt.ModTime)
|
||||
return &CacheDir{
|
||||
cache: c,
|
||||
pathComps: pathComps,
|
||||
name: pathComps[len(pathComps)-1],
|
||||
modTime: opt.ModTime,
|
||||
|
|
|
@ -99,8 +99,12 @@ func listChildren(vfs *Vfs, ctx context.Context, parent FuseNode) ([]fuse.FsEntr
|
|||
}
|
||||
|
||||
objPath := clitypes.JoinObjectPath(myPathComps[2:]...)
|
||||
objPrefix := objPath
|
||||
if objPath != "" {
|
||||
objPrefix += clitypes.ObjectPathSeparator
|
||||
}
|
||||
|
||||
objs, coms, err := d.Object().GetByPrefixGrouped(tx, pkg.PackageID, objPath+clitypes.ObjectPathSeparator)
|
||||
objs, coms, err := d.Object().GetByPrefixGrouped(tx, pkg.PackageID, objPrefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -186,8 +190,8 @@ func removeChild(vfs *Vfs, ctx context.Context, name string, parent FuseNode) er
|
|||
}
|
||||
|
||||
// 存储系统不会保存目录结构,所以这里是尝试删除同名文件
|
||||
err = d.Object().DeleteByPath(tx, pkg.PackageID, joinedPath)
|
||||
if err != nil {
|
||||
err = d.Object().DeleteCompleteByPath(tx, pkg.PackageID, joinedPath)
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -223,8 +227,11 @@ func moveChild(vfs *Vfs, ctx context.Context, oldName string, oldParent FuseNode
|
|||
}
|
||||
|
||||
err2 := vfs.cache.Move(oldChildPath, newChildPath)
|
||||
if err == fuse.ErrNotExists && err2 == fuse.ErrNotExists {
|
||||
return fuse.ErrNotExists
|
||||
if err2 == fuse.ErrNotExists {
|
||||
if err == fuse.ErrNotExists {
|
||||
return fuse.ErrNotExists
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return err2
|
||||
|
@ -273,11 +280,11 @@ func moveRemote(vfs *Vfs, tx db.SQLContext, oldChildPath []string, newParentPath
|
|||
|
||||
return d.Object().BatchUpdate(tx, []clitypes.Object{oldObj})
|
||||
}
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return fuse.ErrNotExists
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
err = d.Object().HasObjectWithPrefix(tx, oldObj.PackageID, oldChildPathJoined+clitypes.ObjectPathSeparator)
|
||||
err = d.Object().HasObjectWithPrefix(tx, oldPkg.PackageID, oldChildPathJoined+clitypes.ObjectPathSeparator)
|
||||
if err == nil {
|
||||
return d.Object().MoveByPrefix(tx,
|
||||
oldPkg.PackageID, oldChildPathJoined+clitypes.ObjectPathSeparator,
|
||||
|
|
|
@ -0,0 +1,225 @@
|
|||
package vfstest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestDirLs checks out listing
|
||||
func TestDirLs(t *testing.T) {
|
||||
run.skipIfNoFUSE(t)
|
||||
|
||||
run.checkDir(t, "")
|
||||
|
||||
run.mkdir(t, "a directory")
|
||||
run.createFile(t, "a file", "hello")
|
||||
|
||||
run.checkDir(t, "a directory/|a file 5")
|
||||
|
||||
run.rmdir(t, "a directory")
|
||||
run.rm(t, "a file")
|
||||
|
||||
run.checkDir(t, "")
|
||||
}
|
||||
|
||||
// TestDirCreateAndRemoveDir tests creating and removing a directory
|
||||
func TestDirCreateAndRemoveDir(t *testing.T) {
|
||||
run.skipIfNoFUSE(t)
|
||||
|
||||
run.mkdir(t, "dir")
|
||||
run.mkdir(t, "dir/subdir")
|
||||
run.checkDir(t, "dir/|dir/subdir/")
|
||||
|
||||
// Check we can't delete a directory with stuff in
|
||||
err := run.os.Remove(run.path("dir"))
|
||||
assert.Error(t, err, "file exists")
|
||||
|
||||
// Now delete subdir then dir - should produce no errors
|
||||
run.rmdir(t, "dir/subdir")
|
||||
run.checkDir(t, "dir/")
|
||||
run.rmdir(t, "dir")
|
||||
run.checkDir(t, "")
|
||||
}
|
||||
|
||||
// TestDirCreateAndRemoveFile tests creating and removing a file
|
||||
func TestDirCreateAndRemoveFile(t *testing.T) {
|
||||
run.skipIfNoFUSE(t)
|
||||
|
||||
run.mkdir(t, "dir")
|
||||
run.createFile(t, "dir/file", "potato")
|
||||
run.checkDir(t, "dir/|dir/file 6")
|
||||
|
||||
// Check we can't delete a directory with stuff in
|
||||
err := run.os.Remove(run.path("dir"))
|
||||
assert.Error(t, err, "file exists")
|
||||
|
||||
// Now delete file
|
||||
run.rm(t, "dir/file")
|
||||
|
||||
run.checkDir(t, "dir/")
|
||||
run.rmdir(t, "dir")
|
||||
run.checkDir(t, "")
|
||||
}
|
||||
|
||||
// TestDirRenameFile tests renaming a file
|
||||
func TestDirRenameFile(t *testing.T) {
|
||||
run.skipIfNoFUSE(t)
|
||||
|
||||
run.mkdir(t, "dir")
|
||||
run.createFile(t, "file", "potato")
|
||||
run.checkDir(t, "dir/|file 6")
|
||||
|
||||
err := run.os.Rename(run.path("file"), run.path("file2"))
|
||||
require.NoError(t, err)
|
||||
run.checkDir(t, "dir/|file2 6")
|
||||
|
||||
data := run.readFile(t, "file2")
|
||||
assert.Equal(t, "potato", data)
|
||||
|
||||
err = run.os.Rename(run.path("file2"), run.path("dir/file3"))
|
||||
require.NoError(t, err)
|
||||
run.checkDir(t, "dir/|dir/file3 6")
|
||||
|
||||
data = run.readFile(t, "dir/file3")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "potato", data)
|
||||
|
||||
run.rm(t, "dir/file3")
|
||||
run.rmdir(t, "dir")
|
||||
run.checkDir(t, "")
|
||||
}
|
||||
|
||||
// TestDirRenameEmptyDir tests renaming and empty directory
|
||||
func TestDirRenameEmptyDir(t *testing.T) {
|
||||
run.skipIfNoFUSE(t)
|
||||
|
||||
run.mkdir(t, "dir")
|
||||
run.mkdir(t, "dir1")
|
||||
run.checkDir(t, "dir/|dir1/")
|
||||
|
||||
err := run.os.Rename(run.path("dir1"), run.path("dir/dir2"))
|
||||
require.NoError(t, err)
|
||||
run.checkDir(t, "dir/|dir/dir2/")
|
||||
|
||||
err = run.os.Rename(run.path("dir/dir2"), run.path("dir/dir3"))
|
||||
require.NoError(t, err)
|
||||
run.checkDir(t, "dir/|dir/dir3/")
|
||||
|
||||
run.rmdir(t, "dir/dir3")
|
||||
run.rmdir(t, "dir")
|
||||
run.checkDir(t, "")
|
||||
}
|
||||
|
||||
// TestDirRenameFullDir tests renaming a full directory
|
||||
func TestDirRenameFullDir(t *testing.T) {
|
||||
run.skipIfNoFUSE(t)
|
||||
|
||||
run.mkdir(t, "dir")
|
||||
run.mkdir(t, "dir1")
|
||||
run.createFile(t, "dir1/potato.txt", "maris piper")
|
||||
run.checkDir(t, "dir/|dir1/|dir1/potato.txt 11")
|
||||
|
||||
err := run.os.Rename(run.path("dir1"), run.path("dir/dir2"))
|
||||
require.NoError(t, err)
|
||||
run.checkDir(t, "dir/|dir/dir2/|dir/dir2/potato.txt 11")
|
||||
|
||||
err = run.os.Rename(run.path("dir/dir2"), run.path("dir/dir3"))
|
||||
require.NoError(t, err)
|
||||
run.checkDir(t, "dir/|dir/dir3/|dir/dir3/potato.txt 11")
|
||||
|
||||
run.rm(t, "dir/dir3/potato.txt")
|
||||
run.rmdir(t, "dir/dir3")
|
||||
run.rmdir(t, "dir")
|
||||
run.checkDir(t, "")
|
||||
}
|
||||
|
||||
// TestDirModTime tests mod times
|
||||
func TestDirModTime(t *testing.T) {
|
||||
run.skipIfNoFUSE(t)
|
||||
|
||||
run.mkdir(t, "dir")
|
||||
mtime := time.Date(2012, time.November, 18, 17, 32, 31, 0, time.UTC)
|
||||
err := run.os.Chtimes(run.path("dir"), mtime, mtime)
|
||||
require.NoError(t, err)
|
||||
|
||||
info, err := run.os.Stat(run.path("dir"))
|
||||
require.NoError(t, err)
|
||||
|
||||
// avoid errors because of timezone differences
|
||||
assert.Equal(t, info.ModTime().Unix(), mtime.Unix())
|
||||
|
||||
run.rmdir(t, "dir")
|
||||
}
|
||||
|
||||
/*
|
||||
// TestDirCacheFlush tests flushing the dir cache
|
||||
func TestDirCacheFlush(t *testing.T) {
|
||||
run.skipIfNoFUSE(t)
|
||||
|
||||
run.checkDir(t, "")
|
||||
|
||||
run.mkdir(t, "dir")
|
||||
run.mkdir(t, "otherdir")
|
||||
run.createFile(t, "dir/file", "1")
|
||||
run.createFile(t, "otherdir/file", "1")
|
||||
|
||||
dm := newDirMap("otherdir/|otherdir/file 1|dir/|dir/file 1")
|
||||
localDm := make(dirMap)
|
||||
run.readLocal(t, localDm, "")
|
||||
assert.Equal(t, dm, localDm, "expected vs fuse mount")
|
||||
|
||||
err := run.fremote.Mkdir(context.Background(), "dir/subdir")
|
||||
require.NoError(t, err)
|
||||
|
||||
// expect newly created "subdir" on remote to not show up
|
||||
run.forget("otherdir")
|
||||
run.readLocal(t, localDm, "")
|
||||
assert.Equal(t, dm, localDm, "expected vs fuse mount")
|
||||
|
||||
run.forget("dir")
|
||||
dm = newDirMap("otherdir/|otherdir/file 1|dir/|dir/file 1|dir/subdir/")
|
||||
run.readLocal(t, localDm, "")
|
||||
assert.Equal(t, dm, localDm, "expected vs fuse mount")
|
||||
|
||||
run.rm(t, "otherdir/file")
|
||||
run.rmdir(t, "otherdir")
|
||||
run.rm(t, "dir/file")
|
||||
run.rmdir(t, "dir/subdir")
|
||||
run.rmdir(t, "dir")
|
||||
run.checkDir(t, "")
|
||||
}
|
||||
|
||||
// TestDirCacheFlushOnDirRename tests flushing the dir cache on rename
|
||||
func TestDirCacheFlushOnDirRename(t *testing.T) {
|
||||
run.skipIfNoFUSE(t)
|
||||
run.mkdir(t, "dir")
|
||||
run.createFile(t, "dir/file", "1")
|
||||
|
||||
dm := newDirMap("dir/|dir/file 1")
|
||||
localDm := make(dirMap)
|
||||
run.readLocal(t, localDm, "")
|
||||
assert.Equal(t, dm, localDm, "expected vs fuse mount")
|
||||
|
||||
// expect remotely created directory to not show up
|
||||
err := run.fremote.Mkdir(context.Background(), "dir/subdir")
|
||||
require.NoError(t, err)
|
||||
run.readLocal(t, localDm, "")
|
||||
assert.Equal(t, dm, localDm, "expected vs fuse mount")
|
||||
|
||||
err = run.os.Rename(run.path("dir"), run.path("rid"))
|
||||
require.NoError(t, err)
|
||||
|
||||
dm = newDirMap("rid/|rid/subdir/|rid/file 1")
|
||||
localDm = make(dirMap)
|
||||
run.readLocal(t, localDm, "")
|
||||
assert.Equal(t, dm, localDm, "expected vs fuse mount")
|
||||
|
||||
run.rm(t, "rid/file")
|
||||
run.rmdir(t, "rid/subdir")
|
||||
run.rmdir(t, "rid")
|
||||
run.checkDir(t, "")
|
||||
}
|
||||
*/
|
|
@ -0,0 +1,60 @@
|
|||
package vfstest
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestTouchAndDelete checks that writing a zero byte file and immediately
|
||||
// deleting it is not racy. See https://github.com/rclone/rclone/issues/1181
|
||||
func TestTouchAndDelete(t *testing.T) {
|
||||
run.skipIfNoFUSE(t)
|
||||
run.checkDir(t, "")
|
||||
|
||||
run.createFile(t, "touched", "")
|
||||
run.rm(t, "touched")
|
||||
|
||||
run.checkDir(t, "")
|
||||
}
|
||||
|
||||
// TestRenameOpenHandle checks that a file with open writers is successfully
|
||||
// renamed after all writers close. See https://github.com/rclone/rclone/issues/2130
|
||||
func TestRenameOpenHandle(t *testing.T) {
|
||||
run.skipIfNoFUSE(t)
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("Skipping test on Windows")
|
||||
}
|
||||
|
||||
run.checkDir(t, "")
|
||||
|
||||
// create file
|
||||
example := []byte("Some Data")
|
||||
path := run.path("rename")
|
||||
file, err := osCreate(path)
|
||||
require.NoError(t, err)
|
||||
|
||||
// write some data
|
||||
_, err = file.Write(example)
|
||||
require.NoError(t, err)
|
||||
err = file.Sync()
|
||||
require.NoError(t, err)
|
||||
|
||||
// attempt to rename open file
|
||||
err = run.os.Rename(path, path+"bla")
|
||||
require.NoError(t, err)
|
||||
|
||||
// close open writers to allow rename on remote to go through
|
||||
err = file.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
run.waitForWriters()
|
||||
|
||||
// verify file was renamed properly
|
||||
run.checkDir(t, "renamebla 9")
|
||||
|
||||
// cleanup
|
||||
run.rm(t, "renamebla")
|
||||
run.checkDir(t, "")
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
package vfstest
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestFileModTime tests mod times on files
|
||||
func TestFileModTime(t *testing.T) {
|
||||
run.skipIfNoFUSE(t)
|
||||
|
||||
run.createFile(t, "file", "123")
|
||||
|
||||
mtime := time.Date(2012, time.November, 18, 17, 32, 31, 0, time.UTC)
|
||||
err := run.os.Chtimes(run.path("file"), mtime, mtime)
|
||||
require.NoError(t, err)
|
||||
|
||||
info, err := run.os.Stat(run.path("file"))
|
||||
require.NoError(t, err)
|
||||
|
||||
// avoid errors because of timezone differences
|
||||
assert.Equal(t, info.ModTime().Unix(), mtime.Unix())
|
||||
|
||||
run.rm(t, "file")
|
||||
}
|
||||
|
||||
// run.os.Create without opening for write too
|
||||
func osCreate(name string) (OsFiler, error) {
|
||||
return run.os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
}
|
||||
|
||||
// run.os.Create with append
|
||||
func osAppend(name string) (OsFiler, error) {
|
||||
return run.os.OpenFile(name, os.O_WRONLY|os.O_APPEND, 0666)
|
||||
}
|
||||
|
||||
// TestFileModTimeWithOpenWriters tests mod time on open files
|
||||
func TestFileModTimeWithOpenWriters(t *testing.T) {
|
||||
run.skipIfNoFUSE(t)
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("Skipping test on Windows")
|
||||
}
|
||||
|
||||
mtime := time.Date(2012, time.November, 18, 17, 32, 31, 0, time.UTC)
|
||||
filepath := run.path("cp-archive-test")
|
||||
|
||||
f, err := osCreate(filepath)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = f.Write([]byte{104, 105})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = run.os.Chtimes(filepath, mtime, mtime)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = f.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
run.waitForWriters()
|
||||
|
||||
info, err := run.os.Stat(filepath)
|
||||
require.NoError(t, err)
|
||||
|
||||
// avoid errors because of timezone differences
|
||||
assert.Equal(t, info.ModTime().Unix(), mtime.Unix())
|
||||
|
||||
run.rm(t, "cp-archive-test")
|
||||
}
|
|
@ -0,0 +1,338 @@
|
|||
// Test suite for rclonefs
|
||||
|
||||
package vfstest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gitlink.org.cn/cloudream/common/pkgs/logger"
|
||||
"gitlink.org.cn/cloudream/storage2/client/internal/mount"
|
||||
)
|
||||
|
||||
const (
|
||||
waitForWritersDelay = 30 * time.Second // time to wait for existing writers
|
||||
testRoot = "b1/p1"
|
||||
)
|
||||
|
||||
// RunTests runs all the tests against all the VFS cache modes
|
||||
//
|
||||
// If useVFS is set then it runs the tests against a VFS rather than a
|
||||
// mount
|
||||
//
|
||||
// If useVFS is not set then it runs the mount in a subprocess in
|
||||
// order to avoid kernel deadlocks.
|
||||
func RunTests(t *testing.T, mnt *mount.Mount) {
|
||||
run = &Run{
|
||||
os: realOs{},
|
||||
mountPath: mnt.MountPoint(),
|
||||
mnt: mnt,
|
||||
}
|
||||
|
||||
run.Init()
|
||||
logger.Infof("Starting test run")
|
||||
ok := t.Run("", func(t *testing.T) {
|
||||
// t.Run("TestTouchAndDelete", TestTouchAndDelete)
|
||||
// t.Run("TestRenameOpenHandle", TestRenameOpenHandle)
|
||||
// t.Run("TestDirLs", TestDirLs)
|
||||
// t.Run("TestDirCreateAndRemoveDir", TestDirCreateAndRemoveDir)
|
||||
// t.Run("TestDirCreateAndRemoveFile", TestDirCreateAndRemoveFile)
|
||||
// t.Run("TestDirRenameFile", TestDirRenameFile)
|
||||
// t.Run("TestDirRenameEmptyDir", TestDirRenameEmptyDir)
|
||||
// t.Run("TestDirRenameFullDir", TestDirRenameFullDir)
|
||||
// t.Run("TestDirModTime", TestDirModTime)
|
||||
// if enableCacheTests {
|
||||
// t.Run("TestDirCacheFlush", TestDirCacheFlush)
|
||||
// }
|
||||
// t.Run("TestDirCacheFlushOnDirRename", TestDirCacheFlushOnDirRename)
|
||||
// t.Run("TestFileModTime", TestFileModTime)
|
||||
// t.Run("TestFileModTimeWithOpenWriters", TestFileModTimeWithOpenWriters)
|
||||
// t.Run("TestMount", TestMount)
|
||||
// t.Run("TestRoot", TestRoot)
|
||||
// t.Run("TestReadByByte", TestReadByByte)
|
||||
// t.Run("TestReadChecksum", TestReadChecksum)
|
||||
// t.Run("TestReadFileDoubleClose", TestReadFileDoubleClose)
|
||||
// t.Run("TestReadSeek", TestReadSeek)
|
||||
// t.Run("TestWriteFileNoWrite", TestWriteFileNoWrite)
|
||||
// t.Run("TestWriteFileWrite", TestWriteFileWrite)
|
||||
// t.Run("TestWriteFileOverwrite", TestWriteFileOverwrite)
|
||||
// t.Run("TestWriteFileDoubleClose", TestWriteFileDoubleClose)
|
||||
// t.Run("TestWriteFileFsync", TestWriteFileFsync)
|
||||
// t.Run("TestWriteFileDup", TestWriteFileDup)
|
||||
t.Run("TestWriteFileAppend", TestWriteFileAppend)
|
||||
})
|
||||
logger.Infof("Finished test run (ok=%v)", ok)
|
||||
run.Finalise()
|
||||
}
|
||||
|
||||
// Run holds the remotes for a test run
|
||||
type Run struct {
|
||||
os Oser
|
||||
mountPath string
|
||||
skip bool
|
||||
mnt *mount.Mount
|
||||
}
|
||||
|
||||
// run holds the master Run data
|
||||
var run *Run
|
||||
|
||||
func (r *Run) skipIfNoFUSE(t *testing.T) {
|
||||
if r.skip {
|
||||
t.Skip("FUSE not found so skipping test")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Run) skipIfVFS(t *testing.T) {
|
||||
if r.skip {
|
||||
t.Skip("Not running under VFS")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Run) Init() {
|
||||
testRootDir := filepath.Join(r.mountPath, testRoot)
|
||||
err := os.MkdirAll(testRootDir, 0644)
|
||||
if err != nil {
|
||||
logger.Infof("Failed to make test root dir %q: %v", testRootDir, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Finalise cleans the remote and unmounts
|
||||
func (r *Run) Finalise() {
|
||||
r.mnt.Stop()
|
||||
|
||||
err := os.RemoveAll(r.mountPath)
|
||||
if err != nil {
|
||||
logger.Infof("Failed to clean mountPath %q: %v", r.mountPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
// path returns an OS local path for filepath
|
||||
func (r *Run) path(filePath string) string {
|
||||
// return windows drive letter root as E:\
|
||||
if filePath == "" && runtime.GOOS == "windows" {
|
||||
return r.mountPath + `\`
|
||||
}
|
||||
return filepath.Join(r.mountPath, testRoot, filepath.FromSlash(filePath))
|
||||
}
|
||||
|
||||
type dirMap map[string]struct{}
|
||||
|
||||
// Create a dirMap from a string
|
||||
func newDirMap(dirString string) (dm dirMap) {
|
||||
dm = make(dirMap)
|
||||
for _, entry := range strings.Split(dirString, "|") {
|
||||
if entry != "" {
|
||||
dm[entry] = struct{}{}
|
||||
}
|
||||
}
|
||||
return dm
|
||||
}
|
||||
|
||||
// Returns a dirmap with only the files in
|
||||
func (dm dirMap) filesOnly() dirMap {
|
||||
newDm := make(dirMap)
|
||||
for name := range dm {
|
||||
if !strings.HasSuffix(name, "/") {
|
||||
newDm[name] = struct{}{}
|
||||
}
|
||||
}
|
||||
return newDm
|
||||
}
|
||||
|
||||
// reads the local tree into dir
|
||||
func (r *Run) readLocal(t *testing.T, dir dirMap, filePath string) {
|
||||
realPath := r.path(filePath)
|
||||
files, err := r.os.ReadDir(realPath)
|
||||
require.NoError(t, err)
|
||||
for _, fi := range files {
|
||||
name := path.Join(filePath, fi.Name())
|
||||
if fi.IsDir() {
|
||||
dir[name+"/"] = struct{}{}
|
||||
r.readLocal(t, dir, name)
|
||||
// assert.Equal(t, os.FileMode(r.vfsOpt.DirPerms)&os.ModePerm, fi.Mode().Perm())
|
||||
} else {
|
||||
dir[fmt.Sprintf("%s %d", name, fi.Size())] = struct{}{}
|
||||
// assert.Equal(t, os.FileMode(r.vfsOpt.FilePerms)&os.ModePerm, fi.Mode().Perm())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// reads the remote tree into dir
|
||||
// func (r *Run) readRemote(t *testing.T, dir dirMap, filepath string) {
|
||||
// // objs, dirs, err := walk.GetAll(context.Background(), r.fremote, filepath, true, 1)
|
||||
// // if err == fs.ErrorDirNotFound {
|
||||
// // return
|
||||
// // }
|
||||
// // require.NoError(t, err)
|
||||
// // for _, obj := range objs {
|
||||
// // dir[fmt.Sprintf("%s %d", obj.Remote(), obj.Size())] = struct{}{}
|
||||
// // }
|
||||
// // for _, d := range dirs {
|
||||
// // name := d.Remote()
|
||||
// // dir[name+"/"] = struct{}{}
|
||||
// // r.readRemote(t, dir, name)
|
||||
// // }
|
||||
// }
|
||||
|
||||
// checkDir checks the local and remote against the string passed in
|
||||
func (r *Run) checkDir(t *testing.T, dirString string) {
|
||||
var retries = 3
|
||||
sleep := time.Second / 5
|
||||
var fuseOK bool
|
||||
var dm, localDm dirMap
|
||||
for i := 1; i <= retries; i++ {
|
||||
dm = newDirMap(dirString)
|
||||
localDm = make(dirMap)
|
||||
r.readLocal(t, localDm, "")
|
||||
fuseOK = reflect.DeepEqual(dm, localDm)
|
||||
if fuseOK {
|
||||
return
|
||||
}
|
||||
sleep *= 2
|
||||
t.Logf("Sleeping for %v for list eventual consistency: %d/%d", sleep, i, retries)
|
||||
time.Sleep(sleep)
|
||||
}
|
||||
assert.Equal(t, dm, localDm, "expected vs fuse mount")
|
||||
}
|
||||
|
||||
// writeFile writes data to a file named by filename.
|
||||
// If the file does not exist, WriteFile creates it with permissions perm;
|
||||
// otherwise writeFile truncates it before writing.
|
||||
// If there is an error writing then writeFile
|
||||
// deletes it an existing file and tries again.
|
||||
func writeFile(filename string, data []byte, perm os.FileMode) error {
|
||||
f, err := run.os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
|
||||
if err != nil {
|
||||
err = run.os.Remove(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f, err = run.os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, perm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
n, err := f.Write(data)
|
||||
if err == nil && n < len(data) {
|
||||
err = io.ErrShortWrite
|
||||
}
|
||||
if err1 := f.Close(); err == nil {
|
||||
err = err1
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Run) createFile(t *testing.T, filepath string, contents string) {
|
||||
filepath = r.path(filepath)
|
||||
err := writeFile(filepath, []byte(contents), 0644)
|
||||
require.NoError(t, err)
|
||||
r.waitForWriters()
|
||||
}
|
||||
|
||||
func (r *Run) readFile(t *testing.T, filepath string) string {
|
||||
filepath = r.path(filepath)
|
||||
result, err := r.os.ReadFile(filepath)
|
||||
require.NoError(t, err)
|
||||
return string(result)
|
||||
}
|
||||
|
||||
func (r *Run) mkdir(t *testing.T, filepath string) {
|
||||
filepath = r.path(filepath)
|
||||
err := r.os.Mkdir(filepath, 0755)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func (r *Run) rm(t *testing.T, filepath string) {
|
||||
filepath = r.path(filepath)
|
||||
err := r.os.Remove(filepath)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for file to disappear from listing
|
||||
for i := 0; i < 100; i++ {
|
||||
_, err := r.os.Stat(filepath)
|
||||
if os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
assert.Fail(t, "failed to delete file", filepath)
|
||||
}
|
||||
|
||||
func (r *Run) rmdir(t *testing.T, filepath string) {
|
||||
filepath = r.path(filepath)
|
||||
err := r.os.Remove(filepath)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func (r *Run) waitForWriters() {
|
||||
timeout := waitForWritersDelay
|
||||
tickTime := 10 * time.Millisecond
|
||||
deadline := time.NewTimer(timeout)
|
||||
defer deadline.Stop()
|
||||
tick := time.NewTimer(tickTime)
|
||||
defer tick.Stop()
|
||||
tick.Stop()
|
||||
for {
|
||||
writers := 0
|
||||
cacheInUse := 0
|
||||
status := r.mnt.Dump()
|
||||
for _, f := range status.Cache.ActiveFiles {
|
||||
if f.RefCount > 0 {
|
||||
writers++
|
||||
}
|
||||
}
|
||||
cacheInUse = len(status.Cache.ActiveFiles)
|
||||
|
||||
if writers == 0 && cacheInUse == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debugf("Still %d writers active and %d cache items in use, waiting %v", writers, cacheInUse, tickTime)
|
||||
tick.Reset(tickTime)
|
||||
select {
|
||||
case <-tick.C:
|
||||
case <-deadline.C:
|
||||
logger.Errorf("Exiting even though %d writers active and %d cache items in use after %v\n%v", writers, cacheInUse, timeout, status)
|
||||
return
|
||||
}
|
||||
tickTime *= 2
|
||||
if tickTime > time.Second {
|
||||
tickTime = time.Second
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestMount checks that the Fs is mounted by seeing if the mountpoint
|
||||
// is in the mount output
|
||||
// func TestMount(t *testing.T) {
|
||||
// run.skipIfVFS(t)
|
||||
// run.skipIfNoFUSE(t)
|
||||
// if runtime.GOOS == "windows" {
|
||||
// t.Skip("not running on windows")
|
||||
// }
|
||||
|
||||
// out, err := exec.Command("mount").Output()
|
||||
// require.NoError(t, err)
|
||||
// assert.Contains(t, string(out), run.mountPath)
|
||||
// }
|
||||
|
||||
// TestRoot checks root directory is present and correct
|
||||
func TestRoot(t *testing.T) {
|
||||
run.skipIfVFS(t)
|
||||
run.skipIfNoFUSE(t)
|
||||
|
||||
fi, err := os.Lstat(run.mountPath)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, fi.IsDir())
|
||||
// assert.Equal(t, os.FileMode(run.vfsOpt.DirPerms)&os.ModePerm, fi.Mode().Perm())
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
package vfstest
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Oser defines the things that the "os" package can do
|
||||
//
|
||||
// This covers what the VFS can do also
|
||||
type Oser interface {
|
||||
Chtimes(name string, atime time.Time, mtime time.Time) error
|
||||
Create(name string) (OsFiler, error)
|
||||
Mkdir(name string, perm os.FileMode) error
|
||||
Open(name string) (OsFiler, error)
|
||||
OpenFile(name string, flags int, perm os.FileMode) (fd OsFiler, err error)
|
||||
ReadDir(dirname string) ([]os.FileInfo, error)
|
||||
ReadFile(filename string) (b []byte, err error)
|
||||
Remove(name string) error
|
||||
Rename(oldName, newName string) error
|
||||
Stat(path string) (os.FileInfo, error)
|
||||
}
|
||||
|
||||
// realOs is an implementation of Oser backed by the "os" package
|
||||
type realOs struct {
|
||||
}
|
||||
|
||||
// realOsFile is an implementation of vfs.Handle
|
||||
type realOsFile struct {
|
||||
*os.File
|
||||
}
|
||||
|
||||
// Flush
|
||||
func (f realOsFile) Flush() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Release
|
||||
func (f realOsFile) Release() error {
|
||||
return f.File.Close()
|
||||
}
|
||||
|
||||
// Chtimes
|
||||
func (r realOs) Chtimes(name string, atime time.Time, mtime time.Time) error {
|
||||
return os.Chtimes(name, atime, mtime)
|
||||
}
|
||||
|
||||
// Create
|
||||
func (r realOs) Create(name string) (OsFiler, error) {
|
||||
fd, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return realOsFile{File: fd}, err
|
||||
}
|
||||
|
||||
// Mkdir
|
||||
func (r realOs) Mkdir(name string, perm os.FileMode) error {
|
||||
return os.Mkdir(name, perm)
|
||||
}
|
||||
|
||||
// Open
|
||||
func (r realOs) Open(name string) (OsFiler, error) {
|
||||
fd, err := os.Open(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return realOsFile{File: fd}, err
|
||||
}
|
||||
|
||||
// OpenFile
|
||||
func (r realOs) OpenFile(name string, flags int, perm os.FileMode) (OsFiler, error) {
|
||||
fd, err := os.OpenFile(name, flags, perm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return realOsFile{File: fd}, err
|
||||
}
|
||||
|
||||
// ReadDir
|
||||
func (r realOs) ReadDir(dirname string) ([]os.FileInfo, error) {
|
||||
entries, err := os.ReadDir(dirname)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
infos := make([]os.FileInfo, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
infos = append(infos, info)
|
||||
}
|
||||
return infos, nil
|
||||
}
|
||||
|
||||
// ReadFile
|
||||
func (r realOs) ReadFile(filename string) (b []byte, err error) {
|
||||
return os.ReadFile(filename)
|
||||
}
|
||||
|
||||
// Remove
|
||||
func (r realOs) Remove(name string) error {
|
||||
return os.Remove(name)
|
||||
}
|
||||
|
||||
// Rename
|
||||
func (r realOs) Rename(oldName, newName string) error {
|
||||
return os.Rename(oldName, newName)
|
||||
}
|
||||
|
||||
// Stat
|
||||
func (r realOs) Stat(path string) (os.FileInfo, error) {
|
||||
return os.Stat(path)
|
||||
}
|
||||
|
||||
// Check interfaces
|
||||
var _ Oser = &realOs{}
|
||||
|
||||
// OsFiler is the methods on *os.File
|
||||
type OsFiler interface {
|
||||
Chdir() error
|
||||
Chmod(mode os.FileMode) error
|
||||
Chown(uid, gid int) error
|
||||
Close() error
|
||||
Fd() uintptr
|
||||
Name() string
|
||||
Read(b []byte) (n int, err error)
|
||||
ReadAt(b []byte, off int64) (n int, err error)
|
||||
Readdir(n int) ([]os.FileInfo, error)
|
||||
Readdirnames(n int) (names []string, err error)
|
||||
Seek(offset int64, whence int) (ret int64, err error)
|
||||
Stat() (os.FileInfo, error)
|
||||
Sync() error
|
||||
Truncate(size int64) error
|
||||
Write(b []byte) (n int, err error)
|
||||
WriteAt(b []byte, off int64) (n int, err error)
|
||||
WriteString(s string) (n int, err error)
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
package vfstest
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestReadByByte reads by byte including don't read any bytes
|
||||
func TestReadByByte(t *testing.T) {
|
||||
run.skipIfNoFUSE(t)
|
||||
|
||||
var data = []byte("hellohello")
|
||||
run.createFile(t, "testfile", string(data))
|
||||
run.checkDir(t, "testfile 10")
|
||||
|
||||
for i := 0; i < len(data); i++ {
|
||||
fd, err := run.os.Open(run.path("testfile"))
|
||||
assert.NoError(t, err)
|
||||
for j := 0; j < i; j++ {
|
||||
buf := make([]byte, 1)
|
||||
n, err := io.ReadFull(fd, buf)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, n)
|
||||
assert.Equal(t, buf[0], data[j])
|
||||
}
|
||||
err = fd.Close()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
run.rm(t, "testfile")
|
||||
}
|
||||
|
||||
// TestReadChecksum checks the checksum reading is working
|
||||
func TestReadChecksum(t *testing.T) {
|
||||
run.skipIfNoFUSE(t)
|
||||
|
||||
// create file big enough so we exceed any single FUSE read
|
||||
// request
|
||||
b := make([]rune, 3*128*1024)
|
||||
for i := range b {
|
||||
b[i] = 'r'
|
||||
}
|
||||
run.createFile(t, "bigfile", string(b))
|
||||
|
||||
// The hash comparison would fail in Flush, if we did not
|
||||
// ensure we read the whole file
|
||||
fd, err := run.os.Open(run.path("bigfile"))
|
||||
assert.NoError(t, err)
|
||||
buf := make([]byte, 10)
|
||||
_, err = io.ReadFull(fd, buf)
|
||||
assert.NoError(t, err)
|
||||
err = fd.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// The hash comparison would fail, because we only read parts
|
||||
// of the file
|
||||
fd, err = run.os.Open(run.path("bigfile"))
|
||||
assert.NoError(t, err)
|
||||
// read at start
|
||||
_, err = io.ReadFull(fd, buf)
|
||||
assert.NoError(t, err)
|
||||
// read at end
|
||||
_, err = fd.Seek(int64(len(b)-len(buf)), io.SeekStart)
|
||||
assert.NoError(t, err)
|
||||
_, err = io.ReadFull(fd, buf)
|
||||
assert.NoError(t, err)
|
||||
// ensure we don't compare hashes
|
||||
err = fd.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
run.rm(t, "bigfile")
|
||||
}
|
||||
|
||||
// TestReadSeek test seeking
|
||||
func TestReadSeek(t *testing.T) {
|
||||
run.skipIfNoFUSE(t)
|
||||
|
||||
var data = []byte("helloHELLO")
|
||||
run.createFile(t, "testfile", string(data))
|
||||
run.checkDir(t, "testfile 10")
|
||||
|
||||
fd, err := run.os.Open(run.path("testfile"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Seek to half way
|
||||
_, err = fd.Seek(5, io.SeekStart)
|
||||
assert.NoError(t, err)
|
||||
|
||||
buf, err := io.ReadAll(fd)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, buf, []byte("HELLO"))
|
||||
|
||||
// Test seeking to the end
|
||||
_, err = fd.Seek(10, io.SeekStart)
|
||||
assert.NoError(t, err)
|
||||
|
||||
buf, err = io.ReadAll(fd)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, buf, []byte(""))
|
||||
|
||||
// Test seeking beyond the end
|
||||
_, err = fd.Seek(1000000, io.SeekStart)
|
||||
assert.NoError(t, err)
|
||||
|
||||
buf, err = io.ReadAll(fd)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, buf, []byte(""))
|
||||
|
||||
// Now back to the start
|
||||
_, err = fd.Seek(0, io.SeekStart)
|
||||
assert.NoError(t, err)
|
||||
|
||||
buf, err = io.ReadAll(fd)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, buf, []byte("helloHELLO"))
|
||||
|
||||
err = fd.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
run.rm(t, "testfile")
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
//go:build !linux && !darwin && !freebsd
|
||||
|
||||
package vfstest
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestReadFileDoubleClose tests double close on read
|
||||
func TestReadFileDoubleClose(t *testing.T) {
|
||||
t.Skip("not supported on " + runtime.GOOS)
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
//go:build linux || darwin || freebsd
|
||||
|
||||
package vfstest
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestReadFileDoubleClose tests double close on read
|
||||
func TestReadFileDoubleClose(t *testing.T) {
|
||||
run.skipIfVFS(t)
|
||||
run.skipIfNoFUSE(t)
|
||||
|
||||
run.createFile(t, "testdoubleclose", "hello")
|
||||
|
||||
in, err := run.os.Open(run.path("testdoubleclose"))
|
||||
assert.NoError(t, err)
|
||||
fd := in.Fd()
|
||||
|
||||
fd1, err := syscall.Dup(int(fd))
|
||||
assert.NoError(t, err)
|
||||
|
||||
fd2, err := syscall.Dup(int(fd))
|
||||
assert.NoError(t, err)
|
||||
|
||||
// close one of the dups - should produce no error
|
||||
err = syscall.Close(fd1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// read from the file
|
||||
buf := make([]byte, 1)
|
||||
_, err = in.Read(buf)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// close it
|
||||
err = in.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// read from the other dup - should produce no error as this
|
||||
// file is now buffered
|
||||
n, err := syscall.Read(fd2, buf)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, n)
|
||||
|
||||
// close the dup - should not produce an error
|
||||
err = syscall.Close(fd2)
|
||||
assert.NoError(t, err, "input/output error")
|
||||
|
||||
run.rm(t, "testdoubleclose")
|
||||
}
|
|
@ -0,0 +1,170 @@
|
|||
package vfstest
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestWriteFileNoWrite tests writing a file with no write()'s to it
|
||||
func TestWriteFileNoWrite(t *testing.T) {
|
||||
run.skipIfNoFUSE(t)
|
||||
|
||||
fd, err := osCreate(run.path("testnowrite"))
|
||||
require.NoError(t, err)
|
||||
|
||||
err = fd.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
run.waitForWriters()
|
||||
|
||||
run.checkDir(t, "testnowrite 0")
|
||||
|
||||
run.rm(t, "testnowrite")
|
||||
}
|
||||
|
||||
// FIXMETestWriteOpenFileInDirListing tests open file in directory listing
|
||||
func FIXMETestWriteOpenFileInDirListing(t *testing.T) {
|
||||
run.skipIfNoFUSE(t)
|
||||
|
||||
fd, err := osCreate(run.path("testnowrite"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
run.checkDir(t, "testnowrite 0")
|
||||
|
||||
err = fd.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
run.waitForWriters()
|
||||
|
||||
run.rm(t, "testnowrite")
|
||||
}
|
||||
|
||||
// TestWriteFileWrite tests writing a file and reading it back
|
||||
func TestWriteFileWrite(t *testing.T) {
|
||||
run.skipIfNoFUSE(t)
|
||||
|
||||
run.createFile(t, "testwrite", "data")
|
||||
run.checkDir(t, "testwrite 4")
|
||||
contents := run.readFile(t, "testwrite")
|
||||
assert.Equal(t, "data", contents)
|
||||
run.rm(t, "testwrite")
|
||||
}
|
||||
|
||||
// TestWriteFileOverwrite tests overwriting a file
|
||||
func TestWriteFileOverwrite(t *testing.T) {
|
||||
run.skipIfNoFUSE(t)
|
||||
|
||||
run.createFile(t, "testwrite", "data")
|
||||
run.checkDir(t, "testwrite 4")
|
||||
run.createFile(t, "testwrite", "potato")
|
||||
contents := run.readFile(t, "testwrite")
|
||||
assert.Equal(t, "potato", contents)
|
||||
run.rm(t, "testwrite")
|
||||
}
|
||||
|
||||
// TestWriteFileFsync tests Fsync
|
||||
//
|
||||
// NB the code for this is in file.go rather than write.go
|
||||
func TestWriteFileFsync(t *testing.T) {
|
||||
run.skipIfNoFUSE(t)
|
||||
|
||||
filepath := run.path("to be synced")
|
||||
fd, err := osCreate(filepath)
|
||||
require.NoError(t, err)
|
||||
_, err = fd.Write([]byte("hello"))
|
||||
require.NoError(t, err)
|
||||
err = fd.Sync()
|
||||
require.NoError(t, err)
|
||||
err = fd.Close()
|
||||
require.NoError(t, err)
|
||||
run.waitForWriters()
|
||||
run.rm(t, "to be synced")
|
||||
}
|
||||
|
||||
// TestWriteFileDup tests behavior of mmap() in Python by using dup() on a file handle
|
||||
func TestWriteFileDup(t *testing.T) {
|
||||
run.skipIfVFS(t)
|
||||
run.skipIfNoFUSE(t)
|
||||
|
||||
filepath := run.path("to be synced")
|
||||
fh, err := osCreate(filepath)
|
||||
require.NoError(t, err)
|
||||
|
||||
testData := []byte("0123456789")
|
||||
|
||||
err = fh.Truncate(int64(len(testData) + 2))
|
||||
require.NoError(t, err)
|
||||
|
||||
err = fh.Sync()
|
||||
require.NoError(t, err)
|
||||
|
||||
var dupFd uintptr
|
||||
dupFd, err = writeTestDup(fh.Fd())
|
||||
require.NoError(t, err)
|
||||
|
||||
dupFile := os.NewFile(dupFd, fh.Name())
|
||||
_, err = dupFile.Write(testData)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = dupFile.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = fh.Seek(int64(len(testData)), 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = fh.Write([]byte("10"))
|
||||
require.NoError(t, err)
|
||||
|
||||
err = fh.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
run.waitForWriters()
|
||||
run.rm(t, "to be synced")
|
||||
}
|
||||
|
||||
// TestWriteFileAppend tests that O_APPEND works on cache backends >= writes
|
||||
func TestWriteFileAppend(t *testing.T) {
|
||||
run.skipIfNoFUSE(t)
|
||||
|
||||
// TODO: Windows needs the v1.5 release of WinFsp to handle O_APPEND properly.
|
||||
// Until it gets released, skip this test on Windows.
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("currently unsupported on Windows")
|
||||
}
|
||||
|
||||
filepath := run.path("to be synced")
|
||||
fh, err := osCreate(filepath)
|
||||
require.NoError(t, err)
|
||||
|
||||
testData := []byte("0123456789")
|
||||
appendData := []byte("10")
|
||||
|
||||
_, err = fh.Write(testData)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = fh.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
fh, err = osAppend(filepath)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = fh.Write(appendData)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = fh.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
<-time.After(time.Second * 10)
|
||||
|
||||
info, err := run.os.Stat(filepath)
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, len(testData)+len(appendData), info.Size())
|
||||
|
||||
run.waitForWriters()
|
||||
run.rm(t, "to be synced")
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
//go:build !linux && !darwin && !freebsd && !windows
|
||||
// +build !linux,!darwin,!freebsd,!windows
|
||||
|
||||
package vfstest
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestWriteFileDoubleClose tests double close on write
|
||||
func TestWriteFileDoubleClose(t *testing.T) {
|
||||
t.Skip("not supported on " + runtime.GOOS)
|
||||
}
|
||||
|
||||
// writeTestDup performs the platform-specific implementation of the dup() unix
|
||||
func writeTestDup(oldfd uintptr) (uintptr, error) {
|
||||
return 0, errors.New("not supported on " + runtime.GOOS)
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
//go:build linux || darwin || freebsd
|
||||
|
||||
package vfstest
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// TestWriteFileDoubleClose tests double close on write
|
||||
func TestWriteFileDoubleClose(t *testing.T) {
|
||||
run.skipIfVFS(t)
|
||||
run.skipIfNoFUSE(t)
|
||||
if runtime.GOOS == "darwin" {
|
||||
t.Skip("Skipping test on OSX")
|
||||
}
|
||||
|
||||
out, err := osCreate(run.path("testdoubleclose"))
|
||||
require.NoError(t, err)
|
||||
fd := out.Fd()
|
||||
|
||||
fd1, err := unix.Dup(int(fd))
|
||||
assert.NoError(t, err)
|
||||
|
||||
fd2, err := unix.Dup(int(fd))
|
||||
assert.NoError(t, err)
|
||||
|
||||
// close one of the dups - should produce no error
|
||||
err = unix.Close(fd1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// write to the file
|
||||
buf := []byte("hello")
|
||||
n, err := out.Write(buf)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 5, n)
|
||||
|
||||
// close it
|
||||
err = out.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// write to the other dup
|
||||
_, err = unix.Write(fd2, buf)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// close the dup - should not produce an error
|
||||
err = unix.Close(fd2)
|
||||
assert.NoError(t, err)
|
||||
|
||||
run.waitForWriters()
|
||||
run.rm(t, "testdoubleclose")
|
||||
}
|
||||
|
||||
// writeTestDup performs the platform-specific implementation of the dup() unix
|
||||
func writeTestDup(oldfd uintptr) (uintptr, error) {
|
||||
newfd, err := unix.Dup(int(oldfd))
|
||||
return uintptr(newfd), err
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
//go:build windows
|
||||
|
||||
package vfstest
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// TestWriteFileDoubleClose tests double close on write
|
||||
func TestWriteFileDoubleClose(t *testing.T) {
|
||||
t.Skip("not supported on " + runtime.GOOS)
|
||||
}
|
||||
|
||||
// writeTestDup performs the platform-specific implementation of the dup() syscall
|
||||
func writeTestDup(oldfd uintptr) (uintptr, error) {
|
||||
p := windows.CurrentProcess()
|
||||
var h windows.Handle
|
||||
return uintptr(h), windows.DuplicateHandle(p, windows.Handle(oldfd), p, &h, 0, true, windows.DUPLICATE_SAME_ACCESS)
|
||||
}
|
|
@ -382,27 +382,7 @@ func (svc *ObjectService) Delete(objectIDs []types.ObjectID) error {
|
|||
}
|
||||
sucs = lo.Keys(avaiIDs)
|
||||
|
||||
err = svc.DB.Object().BatchDelete(tx, objectIDs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("batch deleting objects: %w", err)
|
||||
}
|
||||
|
||||
err = svc.DB.ObjectBlock().BatchDeleteByObjectID(tx, objectIDs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("batch deleting object blocks: %w", err)
|
||||
}
|
||||
|
||||
err = svc.DB.PinnedObject().BatchDeleteByObjectID(tx, objectIDs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("batch deleting pinned objects: %w", err)
|
||||
}
|
||||
|
||||
err = svc.DB.ObjectAccessStat().BatchDeleteByObjectID(tx, objectIDs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("batch deleting object access stats: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return svc.DB.Object().BatchDeleteComplete(tx, sucs)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
Loading…
Reference in New Issue