Initial commit

This commit is contained in:
dev
2025-02-27 21:53:53 +08:00
commit 815e55e4c0
1291 changed files with 185445 additions and 0 deletions

View File

@@ -0,0 +1,83 @@
import os
import argparse
import uuid
import time
from hf3fs_utils.fs import FileSystem
from functools import partial
from multiprocessing import Pool
from datetime import datetime
mnt = None
fs = None
repeat = 20
def task(fn):
for _ in range(repeat):
fn()
def rmrf_ln(dir):
path = os.path.join(mnt, dir, str(uuid.uuid4()))
os.makedirs(path)
os.symlink(os.path.realpath(path), os.path.realpath(os.path.join(mnt, '3fs-virt', 'rm-rf', str(uuid.uuid4()))))
def rmrf_ioctl(dir):
path = os.path.join(mnt, dir, str(uuid.uuid4()))
os.makedirs(path)
fs.remove(path, True)
def mv_ioctl(src, dst):
src = os.path.join(mnt, src, str(uuid.uuid4()))
dst = os.path.join(mnt, dst, str(uuid.uuid4()))
os.makedirs(src)
fs.rename(src, dst)
def parallel(tasks):
with Pool(processes=len(tasks)) as pool:
pool.map(task, tasks)
def main():
parser = argparse.ArgumentParser(description="Process some paths.")
parser.add_argument('--mnt_path', type=str, required=True, help='The mount path to process')
args = parser.parse_args()
global mnt, fs, repeat
mnt = args.mnt_path
fs = FileSystem(mnt)
os.makedirs(os.path.join(mnt, "test", "src"), exist_ok=True)
os.makedirs(os.path.join(mnt, "test", "dst"), exist_ok=True)
for c in [32, 64, 128]:
print(datetime.now().strftime("%Y-%m-%d %H:%M:%S"), f"3fs-virt/rm-rf {c}")
parallel([partial(rmrf_ln, "test")] * c)
print("=============")
for c in [32, 64, 128]:
print(datetime.now().strftime("%Y-%m-%d %H:%M:%S"), f"ioctl rmrf {c}")
parallel([partial(rmrf_ioctl, "test")] * c)
print("=============")
for c in [32, 64, 128]:
print(datetime.now().strftime("%Y-%m-%d %H:%M:%S"), f"ioctl mv {c}")
parallel([partial(mv_ioctl, "test/src", "test/dst")] * c)
print("=============")
for c in [32, 64, 128]:
print(datetime.now().strftime("%Y-%m-%d %H:%M:%S"), f"ioctl mv2 {c}")
parallel([partial(mv_ioctl, "test/src", "test/dst")] * c + [partial(mv_ioctl, "test/dst", "test/src")] * c)
print("=============")
for c in [32, 64, 128]:
print(datetime.now().strftime("%Y-%m-%d %H:%M:%S"), f"all {c}")
parallel(
(
[partial(mv_ioctl, "test/src", "test/dst")] * c +
[partial(mv_ioctl, "test/dst", "test/src")] * c +
[partial(rmrf_ioctl, "test")] * c +
[partial(rmrf_ioctl, "test")] * c
)
)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,79 @@
import os
import random
import time
import argparse
args = None
def gen_fixed_buffer(length, fill_char):
wbuf = bytearray([fill_char % 256] * length)
assert len(wbuf) == length
return wbuf
def write_one_file():
wpath = os.path.join(args.path1, 'data')
rpath = os.path.join(args.path2, 'data')
wfd = os.open(wpath, os.O_CREAT | os.O_WRONLY | os.O_TRUNC, 0o644)
rfd = os.open(rpath, os.O_RDONLY)
length = 0
for i in range(10):
print(".", end="", flush=True)
# write
wbuf = gen_fixed_buffer(random.randint(0, 8 << 10), i)
wsize = os.write(wfd, wbuf)
assert wsize == len(wbuf), f'{wsize} != {len(wbuf)}'
length += wsize
# note: should large than fuse cache timeout & periodic sync
for j in range(12):
time.sleep(5)
st = os.stat(rpath)
if st.st_size == length:
break
# stat
st = os.stat(rpath)
assert st.st_size == length, f'{st.st_size} != {length}'
# read
rbuf = os.read(rfd, len(wbuf))
assert rbuf == wbuf, f'{rbuf} != {wbuf}'
print('\nwrite_one_file passed')
def write_many_files():
fds = []
for i in range(100):
path = os.path.join(args.path1, f'{i}')
wbuf = gen_fixed_buffer(random.randint(0, 8 << 10), i)
fd = os.open(path, os.O_CREAT | os.O_WRONLY | os.O_TRUNC, 0o644)
wsize = os.write(fd, wbuf)
assert wsize == len(wbuf), f'{wsize} != {len(wbuf)}'
fds.append((fd, wbuf))
# note: period sync limit
print('waiting...')
time.sleep(15)
for i in range(100):
path = os.path.join(args.path2, f'{i}')
wbuf = fds[i][1]
st = os.stat(path)
assert st.st_size == len(wbuf), f'{st.st_size} != {len(wbuf)}'
fd = os.open(path, os.O_RDONLY)
rbuf = os.read(fd, st.st_size)
assert rbuf == wbuf, f'{rbuf} != {wbuf}'
for fd, wbuf in fds:
os.close(fd)
print('write_many_files passed')
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument("path1", type=str, help="")
parser.add_argument("path2", type=str, help="")
args = parser.parse_args()
write_one_file()
write_many_files()

View File

@@ -0,0 +1,15 @@
cluster_id = '${FS_CLUSTER}'
[ib_devices]
default_pkey_index = ${PKEY_INDEX}
[fdb]
clusterFile = '${FDB_UNITTEST_CLUSTER}'
[mgmtd_client]
mgmtd_server_addresses = [ 'RDMA://${ADDRESS}:${MGMTD_PORT}' ]
[user_info]
uid = 0
gid = 0
token = '${TOKEN}'

View File

@@ -0,0 +1,31 @@
attr_timeout = 2
entry_timeout = 2
fsync_length_hint = true
enable_read_cache = {{ env.READCACHE }}
chunk_size_limit = '64KB'
[[common.log.categories]]
categories = [ '.' ]
handlers = [ 'normal' ]
[[common.log.handlers]]
async = true
name = 'normal'
start_level = 'NONE'
writer_type = 'FILE'
file_path = '{{ env.LOG_FILE }}'
[mgmtd]
mgmtd_server_addresses = [ 'RDMA://${ADDRESS}:${MGMTD_PORT}' ]
[meta]
dynamic_stripe = {{ env.DYNAMIC_STRIPE }}
[periodic_sync]
enable = true
interval = '500ms'
limit = 16
[io_bufs]
write_buf_size = '{{ "1MB" if env.ENABLE_WRITE_BUFFER else "0MB" }}'
max_buf_size = '1MB'

View File

@@ -0,0 +1,9 @@
allow_other = true # echo user_allow_other | sudo tee -a /etc/fuse.conf
token_file = '${TOKEN_FILE}'
cluster_id = '${FS_CLUSTER}'
[ib_devices]
default_pkey_index = ${PKEY_INDEX}
[mgmtd_client]
mgmtd_server_addresses = [ 'RDMA://${ADDRESS}:${MGMTD_PORT}' ]

View File

@@ -0,0 +1,19 @@
shuffle = true
subdirs = 4
files_per_dir = 10000
threads = 4
coroutines = 8
cluster_id = '${FS_CLUSTER}'
[meta_client]
network_type = 'RDMA'
selection_mode = 'UniformRandom'
[mgmtd_client]
mgmtd_server_addresses = [ 'RDMA://${ADDRESS}:${MGMTD_PORT}' ]
[net_client]
default_timeout = '1s'
[[monitor.reporters]]
type = 'log'

View File

@@ -0,0 +1,44 @@
[[common.log.categories]]
categories = [ '.' ]
handlers = [ 'normal' ]
[[common.log.handlers]]
async = true
name = 'normal'
start_level = 'NONE'
writer_type = 'FILE'
file_path = '{{ env.LOG_FILE }}'
[[server.base.groups]]
[server.base.groups.listener]
listen_port = {{ env.PORT }}
[[server.base.groups]]
[server.base.groups.listener]
listen_port = 0
[server.mgmtd_client]
mgmtd_server_addresses = [ 'RDMA://${ADDRESS}:${MGMTD_PORT}' ]
auto_extend_client_session_interval = '2s'
auto_heartbeat_interval = '2s'
auto_refresh_interval = '2s'
[server.meta]
allow_owner_change_immutable = true
time_granularity = '1ms'
dynamic_stripe = true
dynamic_stripe_initial = 1
dynamic_stripe_growth = 2
iflags_chain_allocation = true
idempotent_record_clean = '1min'
idempotent_record_expire = '5min'
idempotent_remove = true
idempotent_rename = true
[server.meta.gc]
gc_file_delay = '5s'
[server.fdb]
casual_read_risky = true
clusterFile = '${FDB_UNITTEST_CLUSTER}'
externalClientPath = '/lib/libfdb_c.so'

View File

@@ -0,0 +1,7 @@
cluster_id = '${FS_CLUSTER}'
[ib_devices]
default_pkey_index = ${PKEY_INDEX}
[mgmtd_client]
mgmtd_server_addresses = [ 'RDMA://${ADDRESS}:${MGMTD_PORT}' ]

View File

@@ -0,0 +1,21 @@
[[common.log.categories]]
categories = [ '.' ]
handlers = [ 'normal' ]
[[common.log.handlers]]
async = true
name = 'normal'
start_level = 'NONE'
writer_type = 'FILE'
file_path = '{{ env.LOG_FILE }}'
[[server.base.groups]]
[server.base.groups.listener]
listen_port = {{ env.PORT }}
[[server.base.groups]]
[server.base.groups.listener]
listen_port = 0
[server.service]
allow_heartbeat_from_unregistered = true

View File

@@ -0,0 +1,7 @@
cluster_id = '${FS_CLUSTER}'
[ib_devices]
default_pkey_index = ${PKEY_INDEX}
[kv_engine.fdb]
clusterFile = '${FDB_UNITTEST_CLUSTER}'

View File

@@ -0,0 +1,4 @@
[settings]
# naptime is the duration of various short sleeps. It should be greater than
# the timestamp granularity of the file system under test.
naptime = 0.0001

View File

@@ -0,0 +1,33 @@
[[common.log.categories]]
categories = [ '.' ]
handlers = [ 'normal' ]
[[common.log.handlers]]
async = true
name = 'normal'
start_level = 'NONE'
writer_type = 'FILE'
file_path = '{{ env.LOG_FILE }}'
[[server.base.groups]]
[server.base.groups.listener]
listen_port = {{ env.PORT }}
[[server.base.groups]]
[server.base.groups.listener]
listen_port = 0
[server.allocate_worker]
min_remain_groups = 0
max_remain_groups = 1
max_reserved_chunks = '1GB'
[server.mgmtd]
mgmtd_server_addresses = [ 'RDMA://${ADDRESS}:${MGMTD_PORT}' ]
[server.targets]
target_paths = [ ${TARGETS} ]
allow_disk_without_uuid = true
[server.buffer_pool]
rdmabuf_count = 256

View File

@@ -0,0 +1,7 @@
cluster_id = '${FS_CLUSTER}'
[ib_devices]
default_pkey_index = ${PKEY_INDEX}
[mgmtd_client]
mgmtd_server_addresses = [ 'RDMA://${ADDRESS}:${MGMTD_PORT}' ]

586
tests/fuse/fuse_test_ci.py Normal file
View File

@@ -0,0 +1,586 @@
import struct
import os
import mmap
import array
import time
import threading
try:
import numpy as np
except ImportError:
np = None
file_modes = ['r', 'rb', 'r+', 'rb+', 'w', 'wb', 'w+', 'a', 'ab', 'a+', 'ab+']
path = ""
text_path = ""
binary_path = ""
def check_read(fd, path, val, desc):
if fd == None:
fd = open(path, 'r')
v = fd.read(len(val))
if v != val:
raise RuntimeError(v, val, 'Check Error: ', desc)
fd.close()
def write_file(fd, val, cls = False):
fd.write(val)
fd.flush()
if cls:
fd.close()
def print_file_data(fd, path, num):
if fd == None:
fd = open(path, 'r')
fd.read(num)
print('print_file_data', num, fd.read(num))
fd.close()
def open_test():
fd = open(text_path+'1', 'w+')
write_file(fd, 'abcdefghigk')
check_read(None, text_path+'1', 'abc', 'open w+ mode')
fd2 = open(text_path+'2', 'w+')
fd2.close()
fd = open(text_path+'1', 'r')
check_read(fd, '', 'abc', 'open r mode')
fd = open(text_path+'1', 'rb')
check_read(fd, '', b'abc', 'open rb mode')
fd = open(text_path+'1', 'r+')
check_read(fd, '', 'abc', 'open r+ mode')
fd = open(text_path+'1', 'rb+')
check_read(fd, '', b'abc', 'open rb+ mode')
#正常写文本文件
fd = open(text_path+'2_w', 'w')
write_file(fd, 'cba')
check_read(None, text_path+'2_w', 'cba', 'open w mode')
#将文本写入二进制文件
fd = open(binary_path+'2_w', 'wb')
#print_file_data(None, binary_path+'2', 6)
write_file(fd, b'efg')
check_read(None, binary_path+'2_w', 'efg', 'open wb mode')
#读写打开用同一个fd读写校验
fd = open(text_path+'2_w', 'w+')
write_file(fd, 'etc', True)
check_read(None, text_path+'2_w', 'etc', 'open w+ mode')
#追加文本文件
fd = open(text_path+'2_w', 'a')
write_file(fd, 'zzz', True)
check_read(None, text_path+'2_w', 'etczzz', 'open a mode')
#追加二进制文件
fd = open(binary_path+'2_w', 'ab')
write_file(fd, b'zzz', True)
check_read(None, binary_path+'2_w', 'efgzzz', 'open ab mode')
#读写文本文件,追加后再读取
fd = open(text_path+'2_w', 'a+')
write_file(fd, 'yyy', True)
check_read(None, text_path+'2_w', 'etczzzyyy', 'open a+ mode')
#读写二进制文件,追加后再读取
# fd = open(binary_path+'389', 'ab+')
# write_file(fd, b'xxx', True)
# check_read(None, binary_path+'389', 'xxx', 'open ab+ mode')
# fd.close()
#测试非法模式
is_err = True
try:
fd = open(binary_path+'389', 'aa')
except ValueError as err:
is_err = False
print('open_test 1: {0}'.format(err))
if is_err:
raise RuntimeError('open_test 1 Error')
os.unlink(text_path+'2_w')
print('open test over')
def seek_test():
fd = open(text_path+'1', 'r+')
#fd.seek(-1)
#获取文件长度
fd.seek(0, 2)
len = fd.tell()
print(text_path+'1', ' file_len = ', len)
#seek到文件末尾之后,是成功的
idx = fd.seek(len+2, 0)
fd.read(2)
#raise RuntimeError('seek_test len+1,', idx)
#直接seek到最后+1异常
is_err = True
try:
fd.seek(1, 2)
except ValueError as err:
is_err = False
print('seek_test 2: {0}'.format(err))
if is_err:
raise RuntimeError('seek_test 2 Error')
fd.close()
print('seek test over')
def flush_test():
fd = open(text_path+'2', 'r')
fd.flush()
fd = open(text_path+'2_w', 'wb')
fd.flush()
print('flush test over')
def next_test():
fd = open(text_path+'2_w', 'w+')
#空文件获取行
is_err = True
try:
val = next(fd)
except StopIteration as err:
is_err = False
print('next_test 1: {0}'.format(err))
if is_err:
raise RuntimeError('next_test 1 Error')
#写入不同数据,顺序读取
fd.write('abc\nttt\nbbb\nccc\nggg')
fd.flush()
fd.seek(0)
val = next(fd)
if val.strip() != 'abc':
raise RuntimeError('next_test 2 Error')
val = next(fd)
if val.strip() != 'ttt':
raise RuntimeError('next_test 3 Error')
val = next(fd)
if val.strip() != 'bbb':
raise RuntimeError('next_test 4 Error')
fd.close()
os.remove(text_path+'2_w')
print('next test over')
def read_test():
fd = open(text_path+'1', 'r+')
#读取长度为0的数据
val = fd.read(0)
if len(val) != 0:
raise RuntimeError('read_test 1,', val)
#在文件最后位置读取数据
fd.seek(0, 2)
val = fd.read(1)
if len(val) != 0:
raise RuntimeError('read_test 2 Error')
fd.close()
#在写权限的文件读取
is_err = True
fd = open(text_path+'2_w', 'w')
try:
fd.read(1)
except IOError as err:
is_err = False
print('read_test 3: {0}'.format(err))
if is_err:
raise RuntimeError('read_test 3 Error')
print('read test over')
def truncate_test():
#读权限截取
fd = open(text_path+'2_w', 'r')
is_err = True
try:
fd.truncate()
fd.close()
except IOError as err:
is_err = False
print('truncate_test 1: {0}'.format(err))
if is_err:
raise RuntimeError('truncate_test 1 Error')
#正常截取
fd = open(text_path+'2_w', 'w+')
fd.truncate()
fd.write('abcd\nefg')
#当前位置截取
fd.seek(4, 0)
fd.truncate()
fd.seek(0, 0)
val = fd.read(4)
print('val = ', val)
if val != 'abcd':
raise RuntimeError('truncate_test 2 Error')
#非法位置截取
fd.truncate(10)
fd.close()
print('truncate test over')
def tell_test():
#正常获取
fd = open(text_path+'1', 'r')
fd.seek(0, 2)
len = fd.tell()
fd.seek(1, 0)
fd.read(5)
fd.seek(0, 1)
idx = fd.tell()
if idx != 6:
raise RuntimeError('tell_test 1 Error')
#超出文件末尾获取
fd.seek(len+1, 0)
idx = fd.tell()
if idx != len+1:
raise RuntimeError('tell_test 2 Error')
print('tell test over')
def readlines_test():
fd = open(text_path+'2_w', 'w+')
#空文件获取行
val = fd.readline(-1)
fd.write('abc\nttt\nbbb\nccc\nggg')
fd.flush()
fd.seek(0, 2)
f_len = fd.tell()
fd.seek(0)
val = fd.readline(-1)
if val.strip() != 'abc':
raise RuntimeError('readlines_test 1 Error')
val = fd.readline(0)
if len(val) != 0:
raise RuntimeError('readlines_test 2 Error')
val = fd.readline(2)
if val.strip() != 'tt':
raise RuntimeError('readlines_test 3 Error')
val = fd.readline(f_len + 5)
if val.strip() != 't':
raise RuntimeError('readlines_test 4 Error')
print('readlines test over')
def mmap_test():
fd = os.open(text_path+'1', os.O_RDWR | os.O_NONBLOCK)
mm = mmap.mmap(fd, 0)
if mm[:6] != b'abcdef':
raise RuntimeError('mmap_test 1 Error')
os.close(fd)
print('mmap test over')
def os_open_test():
fd = os.open(text_path+'2_rw', os.O_RDWR | os.O_CREAT | os.O_TRUNC)
b1 = bytearray(b'123')
b2 = bytearray(b'abc')
b3 = bytearray(b'456')
bufs = []
bufs.append(b1)
bufs.append(b2)
bufs.append(b3)
os.writev(fd, bufs)
os.lseek(fd, 0, 0)
val = os.read(fd, 5)
if val != b'123ab':
raise RuntimeError('os_open_test 1 Error')
os.close(fd)
#open文件但bufsize超出长度
fd = os.open(text_path+'2_rw', os.O_RDONLY, 0)
val = os.read(fd, 20)
if val != b'123abc456':
raise RuntimeError('os_open_test 2 Error')
os.close(fd)
print('os_open test over')
def os_mkdir_test():
#创建文件夹
try:
os.mkdir(text_path+'test_file')
fd = open(text_path+'test_file/1_t', 'w')
fd.close()
#在文件夹里有文件的情况下删除
os.rmdir(text_path+'test_file')
except OSError as err:
print('os_mkdir_test 1: {0} 预期的结果'.format(err))
os.unlink(text_path+'test_file/1_t')
os.rmdir(text_path+'test_file')
if os.path.exists(text_path+'test_file2'):
os.rmdir(text_path+'test_file2')
print('os_mkdir test over')
def os_link_test():
try:
os.mkdir(text_path+'test_file')
os.mkdir(text_path+'test_file2')
fd = open(text_path+'test_file/1_t', 'w')
os.link(text_path+'test_file/1_t', text_path+'test_file2/1_t2')
except PermissionError as err:
print('os_link_test 1: {0}'.format(err))
print('os_link test over')
def os_unlink_test():
os.unlink(text_path+'test_file/1_t')
os.unlink(text_path+'test_file2/1_t2')
os.rmdir(text_path+'test_file')
os.rmdir(text_path+'test_file2')
print('os_unlink test over')
letter_ary = []
binary_ary = []
for ch in range(97, 123):
letter_ary.append(chr(ch))
binary_ary.append(ch)
def thread_check_text(start, offset, ary, text_data):
for i in range(offset):
t = (start + i) % 26
#print(t, text_data, text_data[i])
if ary[t] != text_data[i]:
raise RuntimeError('text_data check error: {0}, {1}, {2} right: {3}, real:{4}'.format( start, offset, i, ary[t], text_data[i]))
def thread_check_binary(start, offset, binary_data):
for i in range(offset):
t = (start + i + 1) % 126
if t == 0:
t = 126
if t != binary_data[i]:
raise RuntimeError('binary_data check error: start {0} offset {1} index {2} expected: {3} actual: {4}'.format( start, offset, i, t, binary_data[i]))
def thread_check_text_np(start, offset, ary, text_data):
expected = np.arange(start, start + offset, dtype=np.int32)
expected %= 26
expected += ary[0]
# actual = np.array(text_data, dtype=np.int32)
actual = np.frombuffer(text_data, dtype=np.uint8)
if not np.array_equal(expected, actual):
i = np.argmin(expected == actual)
raise RuntimeError('text_data check error: start {0} offset {1} index {2} expected: {3} actual: {4}'.format( start, offset, i, expected[i:i+16], actual[i:i+16]))
def thread_check_binary_np(start, offset, binary_data):
expected = np.arange(start + 1, start + offset + 1, dtype=np.int32)
expected %= 126
expected[expected == 0] = 126
# actual = np.array(binary_data, dtype=np.int32)
actual = np.frombuffer(binary_data, dtype=np.uint8)
if not np.array_equal(expected, actual):
i = np.argmin(expected == actual)
raise RuntimeError('binary_data check error: start {0} offset {1} index {2} expected: {3} actual: {4}'.format( start, offset, i, expected[i:i+16], actual[i:i+16]))
#文本校验
def check_text_data(start, offset, text_data):
if offset != len(text_data):
raise RuntimeError('check_text_data parameter error: offset != len(text_data), {0} {1} {2}'.format(offset, len(text_data), start))
return
if type(text_data).__name__ == 'bytes':
ary = binary_ary
else:
ary = letter_ary
if len(text_data) <= 1024:
thread_check_text(start, offset, ary, text_data)
else:
thread_check_text_np(start, offset, ary, text_data)
# p = threading.Thread(target=thread_check_text_np, args=(start, offset, ary, text_data,))
# p.start()
# for i in range(offset):
# t = (start + i) % 26
# #print(t, text_data, text_data[i])
# if ary[t] != text_data[i]:
# raise RuntimeError('text_data check error: {0}, {1}, {2} right: {3}, real:{4}'.format( start, offset, i, ary[t], text_data[i]))
#二进制校验
def check_binary_data(start, offset, binary_data):
if offset != len(binary_data):
raise RuntimeError('check_binary_data parameter error: offset != len(binary_data), {0} {1}'.format(offset, len(binary_data)))
if len(binary_data) <= 1024:
thread_check_binary(start, offset, binary_data)
else:
thread_check_binary_np(start, offset, binary_data)
# p = threading.Thread(target=thread_check_binary_np, args=(start, offset, binary_data,))
# p.start()
# for i in range(offset):
# t = (start + i + 1) % 126
# if t == 0:
# t = 126
# if t != binary_data[i]:
# raise RuntimeError('binary_data check error: {0}, {1}, {2} right: {3}, real:{4}'.format( start, offset, i, t, binary_data[i]))
def check_readv_results(file_path, read_offset, data_bufs, data_len):
index = read_offset
readbufsize = sum(len(buf) for buf in data_bufs)
filesize = os.path.getsize(file_path)
if read_offset > filesize :
assert data_len == 0, "read start exceeding file end, return size should be 0. now it's %d " % (data_len)
return
expected_return_size = readbufsize if (read_offset + readbufsize <= filesize) else (filesize - read_offset)
assert data_len == expected_return_size, "expected size %d, result return size %d" % (expected_return_size, data_len)
#print('data_bufs size = ', len(data_bufs[0]))
#print(data_bufs)
for buf in data_bufs:
l = len(buf)
if l <= data_len:
val = bytes(buf)
else:
val = bytes(buf[:data_len])
l = data_len
#print('val = ', val, data_len)
data_len -= l
#print(index, l, val[:128])
if file_path.find('binary') != -1:
check_binary_data(index, l, val)
else:
check_text_data(index, l, val)
index += l
if data_len <= 0:
break
def os_read_test_base(file_path, index, size, file_len = -1):
fd = os.open(file_path, os.O_RDONLY )
os.lseek(fd, index, 0)
val = os.read(fd, size)
os.close(fd)
readbufsize = size
filesize = os.path.getsize(file_path)
read_offset = index
data_len = len(val)
if read_offset > filesize :
assert data_len == 0, "read start exceeding file end, return size should be 0. now it's %d " % (data_len)
return
expected_return_size = readbufsize if (read_offset + readbufsize <= filesize) else (filesize - read_offset)
assert data_len == expected_return_size, "expected size %d, result return size %d" % (expected_return_size, data_len)
#print(index, l, val[:128])
if file_path.find('binary') != -1:
check_binary_data(index, data_len, val)
else:
check_text_data(index, data_len, val)
def os_readv_test_base(file_path, index, buffers_size, file_len = -1):
buffers = []
size = 0
for s in buffers_size:
buf = bytearray(s)
buffers.append(buf)
size += s
print('bufs = ', buffers_size)
fd = os.open(file_path, os.O_RDONLY ) #| os.O_DIRECT
os.lseek(fd, index, 0)
num_bytes = os.readv(fd, buffers)
os.close(fd)
check_readv_results(file_path, index, buffers, num_bytes)
def test_continuous_readv(fd, index, buffers_size):
buffers = []
size = 0
for s in buffers_size:
buf = bytearray(s)
buffers.append(buf)
size += s
print('bufs = ', buffers_size)
os.lseek(fd, index, 0)
num_bytes = os.readv(fd, buffers)
#print('buffers size = ', len(buffers), num_bytes, size, buffers)
if num_bytes == size:
for buf in buffers:
l = len(buf)
val = bytes(buf)
check_text_data(index, l, val)
index += l
else:
raise RuntimeError('os_readv_test_base error: num_bytes != size {0}, {1}'.format( num_bytes, size))
def posix_io_text():
print("=== [start test suite] posix_io_test basic funtions ===")
open_test()
seek_test()
flush_test()
next_test()
read_test()
truncate_test()
tell_test()
readlines_test()
mmap_test()
os_open_test()
os_mkdir_test()
os_link_test()
os_unlink_test()
print("=== [finish test suite] posix_io_test basic functions ===")
def os_write_run(path, open_mode, max_file_size, write_data):
fd = os.open(path, open_mode)
idx = 0
print("write started", path, max_file_size)
while idx < max_file_size:
os.write(fd, write_data)
idx += len(write_data)
os.close(fd)
print("write done", path, max_file_size)
def os_read_run(path, open_mode, max_file_size):
fd = os.open(path, open_mode)
idx = 0
data = array.array('B')
check_data = check_text_data if path.find('text') != -1 else check_binary_data
print("read started", path, max_file_size, flush=True)
while idx < max_file_size - 26:
val = os.read(fd, 26)
data.frombytes(val)
l = len(val)
idx += l
if l == 0:
print("wait for more data...")
time.sleep(0.5)
if idx % (max_file_size // 20) < 26:
print(idx * 100 // max_file_size, 'percent done', 'idx', idx, 'max file size', max_file_size, flush=True)
print("read done", path, flush=True)
os.close(fd)
print("check data", flush=True)
check_data(0, len(data), data)
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("path", help="The path argument")
args = parser.parse_args()
path = args.path
text_path = os.path.join(path, "text")
binary_path = os.path.join(path, "binary")
print('Path', path)
print('posix test')
print('text_path = ', text_path)
print('binary_path = ', binary_path)
print('letter_ary = ', letter_ary)
print('binary_ary = ', binary_ary)
posix_io_text()
#
# os_read_test()
# os_readv_test()

View File

@@ -0,0 +1,73 @@
import sys
import os
import errno
import uuid
def random_fname():
unique_id = uuid.uuid4()
return str(unique_id).replace("-", "")
def assert_errno(expected, fn, *args):
code = 0
try:
fn(*args)
except Exception as e:
code = getattr(e, "errno", errno.EINVAL)
assert code == expected, f"{code} != {expected}"
# Note:
# setxattr: hf3fs.lock -> [try_lock, unlock, preempt_lock, clear]
# getxattr: hf3fs.lock -> ENODATA if not locked else json string '{"client":{"uuid":"...","hostname":"..."}}'
# removexattr('f3fs.lock') == setxattr(path, 'hf3fs.lock', b'clear')
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Please provide exactly two mount points as arguments.")
sys.exit(-1)
# two clients mount same filesystem
mnt1 = sys.argv[1]
mnt2 = sys.argv[2]
# return ENOTSUP should affect subsequent operations.
assert_errno(errno.ENOTSUP, os.setxattr, mnt1, 'system.acl', b'soemthing')
assert_errno(errno.ENOTSUP, os.setxattr, mnt1, 'hf3fs.invalid_key', b'soemthing')
os.removexattr(mnt1, 'hf3fs.lock')
os.removexattr(mnt2, 'hf3fs.lock')
assert_errno(errno.ENODATA, os.getxattr, mnt1, 'hf3fs.lock')
assert_errno(errno.ENODATA, os.getxattr, mnt2, 'hf3fs.lock')
# directory not locked, both client1 & client2 can create
os.open(os.path.join(mnt1, random_fname()), os.O_CREAT | os.O_RDONLY, 0o644)
os.open(os.path.join(mnt2, random_fname()), os.O_CREAT | os.O_RDONLY, 0o644)
# client1 try_lock
os.setxattr(mnt1, 'hf3fs.lock', b'try_lock')
os.open(os.path.join(mnt1, 'file'), os.O_CREAT | os.O_RDONLY, 0o644)
attr = os.getxattr(mnt1, 'hf3fs.lock')
assert attr != ''
print(f'getxattr hf3fs.lock: {attr}')
attrs = os.listxattr(mnt1)
assert len(attrs) != 0
print(f'listxattr {attrs}')
# client2 can't try_lock or create
assert_errno(errno.ENOLCK, os.setxattr, mnt2, 'hf3fs.lock', b'try_lock')
assert_errno(errno.ENOLCK, os.open, os.path.join(mnt2, random_fname()), os.O_CREAT | os.O_RDONLY, 0o644)
assert_errno(errno.ENOLCK, os.link, os.path.join(mnt2, 'file'), os.path.join(mnt2, random_fname()))
assert_errno(errno.ENOLCK, os.remove, os.path.join(mnt2, 'file'))
# client2 can preempt lock
os.setxattr(mnt2, 'hf3fs.lock', b'preempt_lock')
os.open(os.path.join(mnt2, random_fname()), os.O_CREAT | os.O_RDONLY, 0o644)
os.link(os.path.join(mnt2, 'file'), os.path.join(mnt2, random_fname()))
os.remove(os.path.join(mnt2, 'file'))
assert_errno(errno.ENOLCK, os.setxattr, mnt1, 'hf3fs.lock', b'unlock')
# client2 unlock, both client1 & client2 can create again
os.setxattr(mnt2, 'hf3fs.lock', b'unlock')
os.open(os.path.join(mnt1, random_fname()), os.O_CREAT | os.O_RDONLY, 0o644)
os.open(os.path.join(mnt2, random_fname()), os.O_CREAT | os.O_RDONLY, 0o644)
print(f'test passed')

92
tests/fuse/random_rw.py Normal file
View File

@@ -0,0 +1,92 @@
import os
import random
import argparse
import subprocess
def generate_random_data(length):
data = bytearray(os.urandom(length))
return data
def write_random_blocks(file_path, data, close):
blocks = []
offset = 0
while offset < len(data):
length = min(random.randint(1, 4 << 20), len(data) - offset)
blocks.append((offset, length))
offset += length
random.shuffle(blocks)
fd = os.open(file_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o644)
for offset, length in blocks:
print("{:10d} {:10d}".format(offset, offset + length))
os.pwrite(fd, data[offset:(offset + length)], offset)
if not close:
return fd
os.close(fd)
def run_command(command):
try:
output = subprocess.check_output(command, shell=True, text=True)
print(f"{command} output:")
print(output)
except subprocess.CalledProcessError as e:
print("Error occurred while running the external program:", e)
def dump_buf(buf, data):
with open('random_rw.buf', 'wb') as f:
f.write(buf)
with open('random_rw.data', 'wb') as f:
f.write(data)
run_command("cmp -l random_rw.buf random_rw.data | head")
run_command("cmp -l random_rw.buf random_rw.data | tail")
def validate_random_access(file_path, data):
total = 0
st = os.stat(file_path)
assert st.st_size == len(data), f'{st.st_size} != {len(data)}'
fd = os.open(file_path, os.O_RDONLY)
while total < 2 * len(data):
offset = random.randint(0, len(data))
length = min(random.randint(1, 4 << 20), len(data) - offset)
buf = os.pread(fd, length, offset)
total += length
if buf != data[offset:(offset + length)]:
print(f"validation failed for {file_path}, offset {offset}, length {length}, buf {len(buf)}")
dump_buf(buf, data[offset:(offset + length)])
assert False
buf = os.pread(fd, len(data), 0)
if buf != data:
print(f"validation failed for {file_path}, buf {len(buf)}, data {len(data)}")
dump_buf(buf, data)
assert False
os.close(fd)
print(f"validation success for {file_path}")
if __name__ == '__main__':
# Use argparse to accept parameters
parser = argparse.ArgumentParser(description='Generate specified number and size of random files and validate data correctness')
parser.add_argument('--filesize', type=int, default=128 * 1024 * 1024, help='Maximum size of each file in bytes, default is 128MB')
parser.add_argument('--filenum', type=int, default=10, help='Number of files to generate, default is 1000')
parser.add_argument('--read_before_close', action='store_true', default=False, help='Enable the flag')
parser.add_argument('path', type=str, help='Path for test run')
args = parser.parse_args()
max_file_size = args.filesize
file_num = args.filenum
test_path = args.path
for i in range(file_num):
file_size = random.randint(1, max_file_size)
data = generate_random_data(file_size)
file_name = f"file_{file_size}_{i}.bin"
file_path = os.path.join(test_path, file_name)
data = generate_random_data(file_size)
fd = write_random_blocks(file_path, data, not args.read_before_close)
validate_random_access(file_path, data)
if args.read_before_close:
os.close(fd)

View File

@@ -0,0 +1,53 @@
import os
import random
import argparse
import time
def generate_random_data(length):
# Generate random data of length 'length'
data = bytearray(os.urandom(length))
return data
def write_random_data(file_path, data):
# Write data to file
with open(file_path, 'wb') as file:
file.write(data)
def validate_data(file_path, data):
st = os.stat(file_path)
assert st.st_size == len(data), f'{file_path} {st.st_size} != {len(data)}'
# Open the file and validate if the data matches the original data
with open(file_path, 'rb') as file:
read_data = file.read()
assert read_data == data, f"validation failed for {file_path}"
print(f"validation success for {file_path}")
if __name__ == '__main__':
# Use argparse to accept parameters
parser = argparse.ArgumentParser(description='Generate specified number and size of random files and validate data correctness')
parser.add_argument('--filesize', type=int, default=32 * 1024 * 1024, help='Maximum size of each file in bytes, default is 32MB')
parser.add_argument('--filenum', type=int, default=10, help='Number of files to generate, default is 1000')
parser.add_argument('--seconds', type=int, default=0, help='Run seconds')
parser.add_argument('path', type=str, help='Path for test run')
args = parser.parse_args()
max_file_size = args.filesize
file_num = args.filenum
test_path = args.path
start = time.time()
i = 0
while time.time() - start < args.seconds or i < file_num:
file_size = random.randint(1, max_file_size)
data = generate_random_data(file_size)
file_name = f"file_{file_size}_{i}.bin"
file_path = os.path.join(test_path, file_name)
write_random_data(file_path, data)
validate_data(file_path, data)
i+= 1

533
tests/fuse/run.sh Normal file
View File

@@ -0,0 +1,533 @@
#!/bin/bash
RED='\033[0;31m'
NC='\033[0m' # No Color
function check_exists {
if ! [ -e $1 ]; then
echo $1 not exists
exit 1
fi
}
function random_port {
local min_port=8000
local max_port=65535
while true; do
random_port=$((min_port + RANDOM % (max_port - min_port + 1)))
if ! lsof -i ":$random_port" >/dev/null; then
echo "$random_port"
break
fi
done
}
function start_fdb {
local PORT=$1
local DATA=$2
local LOG=$3
local CLUSTER=${DATA}/fdb.cluster
echo fdbserver: $FDB
echo fdbclient: $FDBCLI
echo start fdb @ port ${PORT}
echo test${FDB_PORT}:testdb${FDB_PORT}@127.0.0.1:${FDB_PORT} >${CLUSTER}
${FDB} -p auto:${PORT} -d ${DATA} -L ${LOG} -C ${CLUSTER} &> ${LOG}/fdbserver.log &
echo "sleep 5s then create database"
sleep 5 && $FDBCLI -C ${CLUSTER} --exec "configure new memory single"
echo "sleep 5s then check foundationdb status"
sleep 5 && $FDBCLI -C ${CLUSTER} --exec "status minimal"
}
function create_app_config {
local NODE=$1
local FILE=$2
echo "allow_empty_node_id = false" >$FILE
echo "node_id = $NODE" >>$FILE
}
function wait_path {
local file_path="$1"
local timeout=$2
local interval=1 # Check interval in seconds
local elapsed_time=0
# Wait until the file exists or timeout is reached
while [ ! -d "$file_path" ] && [ $elapsed_time -lt $timeout ]; do
sleep $interval
elapsed_time=$((elapsed_time + interval))
done
if ! [ -d "$file_path" ]; then
echo "Path '$file_path' not found within $timeout seconds."
exit 1
fi
}
function random_64 {
local random_number=$(od -An -N8 -tu8 /dev/urandom | awk '{print $1}')
echo "$random_number"
}
if (($# < 2)); then
echo "${0} <binary> <test-dir> <log-dir>"
exit 1
fi
BINARY=${1%/}
TEST_DIR=${2%/}
LOG_DIR=${3:-}
function cleanup {
rv=$?
echo exit with ${rv}
fusermount3 -u ${MOUNT1} || true
fusermount3 -u ${MOUNT2} || true
fusermount3 -u ${MOUNT3} || true
fusermount3 -u ${MOUNT4} || true
sleep 1
jobs -p | xargs -r kill || true
sleep 1
jobs -p | xargs -r kill -9 &> /dev/null || true
fusermount3 -u ${MOUNT1} &> /dev/null || true
fusermount3 -u ${MOUNT2} &> /dev/null || true
fusermount3 -u ${MOUNT3} &> /dev/null || true
fusermount3 -u ${MOUNT4} &> /dev/null || true
exit $rv
}
trap "cleanup" EXIT
set -euo pipefail
DATA=${TEST_DIR}/data
CONFIG=${TEST_DIR}/config
DEFAULT_LOG=${TEST_DIR}/log
LOG=${LOG_DIR:=${DEFAULT_LOG}}
MOUNT1=`realpath ${TEST_DIR}/mnt1`
MOUNT2=`realpath ${TEST_DIR}/mnt2`
MOUNT3=`realpath ${TEST_DIR}/mnt3`
MOUNT4=`realpath ${TEST_DIR}/mnt4`
export FUSE_TEST_TIMEOUT="${FUSE_TEST_TIMEOUT:-3600}"
SCRIPT_DIR=$(dirname "$0")
UPDATE_CONFIG="python3 ${SCRIPT_DIR}/update-config.py"
# NOTE: need regenerate chain table if change storage number or disk number
NUM_META=2
NUM_STORAGE=3
NUM_DISK=4
export TOKEN="AADHHSOs8QA92iRe2wB1fmuL"
export TOKEN_FILE="${CONFIG}/token"
export PKEY_INDEX=${PKEY_INDEX:-0}
export FS_CLUSTER="ci_test"
export FDB_UNITTEST_CLUSTER=${DATA}/foundationdb/fdb.cluster
export TARGETS="$(seq -f \'{{\ env.STORAGE_DIR\ }}/data%g\', 1 ${NUM_DISK})"
if [ -z "${FDB_PATH+x}" ]; then
FDB=$(which fdbserver)
FDBCLI=$(which fdbcli)
else
FDB=$FDB_PATH/fdbserver
FDBCLI=$FDB_PATH/fdbcli
fi
# check network
echo "### check network"
export ADDRESS=$(ip -o -4 addr show | grep -E '(en|eth|ib)' | awk '{print $4}' | head -n 1 | awk -F'[/ ]' '{print $1}')
export FDB_PORT=$(random_port)
export MGMTD_PORT=$(random_port)
if [ -n "${FUSE_TEST_FDB_PORT+x}" ]; then
echo use FUSE_TEST_FDB_PORT ${FUSE_TEST_FDB_PORT}
lsof -i ":${FUSE_TEST_FDB_PORT}" || echo port available
export FDB_PORT=${FUSE_TEST_FDB_PORT}
fi
if [ -n "${FUSE_TEST_MGMTD_PORT+x}" ]; then
echo use FUSE_TEST_MGMTD_PORT ${FUSE_TEST_MGMTD_PORT}
lsof -i ":${FUSE_TEST_MGMTD_PORT}" || echo port available
export MGMTD_PORT=${FUSE_TEST_MGMTD_PORT}
fi
echo "NIC address ${ADDRESS}"
echo "FDB port ${FDB_PORT}"
echo "MGMTD port ${MGMTD_PORT}"
# check directory
echo "### check directory and file"
check_exists ${TEST_DIR}
rm -rf ${TEST_DIR}/*
check_exists ${BINARY}
check_exists ${BINARY}/mgmtd_main
check_exists ${BINARY}/meta_main
check_exists ${BINARY}/storage_main
check_exists ${BINARY}/hf3fs_fuse_main
check_exists ${BINARY}/admin_cli
check_exists ${FDB}
check_exists ${FDBCLI}
# create subdirectory
echo "### create subdirectory"
mkdir -p ${CONFIG}
mkdir -p ${LOG}
mkdir -p ${DATA}
mkdir -p ${MOUNT1}
mkdir -p ${MOUNT2}
mkdir -p ${MOUNT3}
mkdir -p ${MOUNT4}
mkdir -p ${DATA}/foundationdb
for i in $(seq 1 ${NUM_STORAGE}); do
mkdir -p ${DATA}/storage-${i}
for j in $(seq 1 ${NUM_DISK}); do
mkdir -p ${DATA}/storage-${i}/data${j}
done
done
# generate app config
echo "generate app config"
create_app_config 1 ${CONFIG}/mgmtd_main_app.toml
for i in $(seq 1 ${NUM_META}); do
node=$(expr 50 + ${i})
file=${CONFIG}/meta_main_app_${node}.toml
create_app_config ${node} ${file}
done
create_app_config 51 ${CONFIG}/meta_main_app.toml
for i in $(seq 1 ${NUM_STORAGE}); do
node=$(expr 10000 + ${i})
file=${CONFIG}/storage_main_app_${node}.toml
create_app_config ${node} ${file}
done
# generate config
echo "generate config"
echo ${TOKEN} > ${TOKEN_FILE}
for file in ${SCRIPT_DIR}/config/*.toml; do
echo "- generate $(basename $file)"
cat ${file} | envsubst > ${CONFIG}/$(basename $file)
done
ADMIN_CLI="${BINARY}/admin_cli --cfg ${CONFIG}/admin_cli.toml --"
# start foundationdb
echo "### create foundationdb"
start_fdb $FDB_PORT ${DATA}/foundationdb ${LOG}/
# init cluster
echo "### init cluster"
echo "admin_cli: ${ADMIN_CLI}"
${ADMIN_CLI} user-add --root --admin --token ${TOKEN} 0 root
${ADMIN_CLI} user-set-token --new 0
echo ""
${ADMIN_CLI} user-list
echo ""
${ADMIN_CLI} init-cluster \
--mgmtd ${CONFIG}/mgmtd_main.toml \
--meta ${CONFIG}/meta_main.toml \
--storage ${CONFIG}/storage_main.toml \
--fuse ${CONFIG}/hf3fs_fuse_main.toml \
--skip-config-check 1 524288 8
# start mgmtd
echo "### start mgmtd"
LOG_FILE=${LOG}/mgmtd.log \
PORT=${MGMTD_PORT} \
${BINARY}/mgmtd_main \
--app_cfg ${CONFIG}/mgmtd_main_app.toml \
--launcher_cfg ${CONFIG}/mgmtd_main_launcher.toml \
--cfg ${CONFIG}/mgmtd_main.toml \
>${LOG}/mgmtd_main.stdout 2>${LOG}/mgmtd_main.stderr &
sleep 5 && echo ""
# start meta
echo "### start meta & storage"
for i in $(seq 1 ${NUM_STORAGE}); do
LOG_FILE=${LOG}/storage-${i}.log \
PORT=0 \
STORAGE_DIR=${DATA}/storage-${i}/ \
${BINARY}/storage_main \
--app_cfg ${CONFIG}/storage_main_app_$(expr 10000 + ${i}).toml \
--launcher_cfg ${CONFIG}/storage_main_launcher.toml \
--cfg ${CONFIG}/storage_main.toml \
>${LOG}/storage_main-${i}.stdout 2>${LOG}/storage_main-${i}.stderr &
done
for i in $(seq 1 ${NUM_META}); do
LOG_FILE=${LOG}/meta-${i}.log \
PORT=0 \
${BINARY}/meta_main \
--app_cfg ${CONFIG}/meta_main_app_$(expr 50 + ${i}).toml \
--launcher_cfg ${CONFIG}/meta_main_launcher.toml \
--cfg ${CONFIG}/meta_main.toml \
>${LOG}/meta_main-${i}.stdout 2>${LOG}/meta_main-${i}.stderr &
done
sleep 15 && echo ""
# list nodes
echo "### list nodes"
${ADMIN_CLI} list-nodes
# create chains
echo "### create targets and chain table"
echo "create targets"
echo "ChainId,TargetId" >${CONFIG}/chains.csv
echo "ChainId" >${CONFIG}/chain-table.csv
for storage in $(seq 1 ${NUM_STORAGE}); do
node=$(expr 10000 + ${storage})
for disk in $(seq 1 ${NUM_DISK}); do
target=$(printf "%d%02d001" $node $disk)
args="create-target --node-id ${node} --disk-index $((disk - 1)) --target-id ${target} --chain-id ${target}"
echo $args
echo "${target},${target}" >>${CONFIG}/chains.csv
echo "${target}" >>${CONFIG}/chain-table.csv
${ADMIN_CLI} ${args}
done
done
echo "update chains and chain table"
${ADMIN_CLI} upload-chains ${CONFIG}/chains.csv
${ADMIN_CLI} upload-chain-table 1 ${CONFIG}/chain-table.csv --desc replica-1
sleep 10
${ADMIN_CLI} list-chains
${ADMIN_CLI} list-chain-tables
echo "create test user"
if [ $UID -ne 0 ]; then
${ADMIN_CLI} user-add ${UID} test
fi
${ADMIN_CLI} user-list
${ADMIN_CLI} mkdir --perm 0755 test
${ADMIN_CLI} set-perm --uid ${UID} --gid ${UID} test
echo "### start fuse"
echo "redirect stdout to ${LOG}/fuse.1.stdout, stderr to ${LOG}/fuse.1.stderr"
LOG_FILE=${LOG}/fuse.1.log DYNAMIC_STRIPE=true READCACHE=false \
timeout -k 10 ${FUSE_TEST_TIMEOUT} ${BINARY}/hf3fs_fuse_main \
--launcher_cfg ${CONFIG}/hf3fs_fuse_main_launcher.toml \
--launcher_config.mountpoint=${MOUNT1} \
>${LOG}/fuse.1.stdout 2>${LOG}/fuse.1.stderr &
echo "redirect stdout to ${LOG}/fuse.2.stdout, stderr to ${LOG}/fuse.2.stderr"
LOG_FILE=${LOG}/fuse.2.log ENABLE_WRITE_BUFFER=1 DYNAMIC_STRIPE=false READCACHE=false \
timeout -k 10 ${FUSE_TEST_TIMEOUT} ${BINARY}/hf3fs_fuse_main \
--launcher_cfg ${CONFIG}/hf3fs_fuse_main_launcher.toml \
--launcher_config.mountpoint=${MOUNT2} \
>${LOG}/fuse.2.stdout 2>${LOG}/fuse.2.stderr &
echo "redirect stdout to ${LOG}/fuse.3.stdout, stderr to ${LOG}/fuse.3.stderr"
LOG_FILE=${LOG}/fuse.3.log DYNAMIC_STRIPE=false READCACHE=true \
timeout -k 10 ${FUSE_TEST_TIMEOUT} ${BINARY}/hf3fs_fuse_main \
--launcher_cfg ${CONFIG}/hf3fs_fuse_main_launcher.toml \
--launcher_config.mountpoint=${MOUNT3} \
>${LOG}/fuse.3.stdout 2>${LOG}/fuse.3.stderr &
echo "redirect stdout to ${LOG}/fuse.4.stdout, stderr to ${LOG}/fuse.4.stderr"
LOG_FILE=${LOG}/fuse.4.log ENABLE_WRITE_BUFFER=1 DYNAMIC_STRIPE=true READCACHE=true \
timeout -k 10 ${FUSE_TEST_TIMEOUT} ${BINARY}/hf3fs_fuse_main \
--launcher_cfg ${CONFIG}/hf3fs_fuse_main_launcher.toml \
--launcher_config.mountpoint=${MOUNT4} \
>${LOG}/fuse.4.stdout 2>${LOG}/fuse.4.stderr &
wait_path ${MOUNT1}/test 30
wait_path ${MOUNT2}/test 30
wait_path ${MOUNT3}/test 30
wait_path ${MOUNT4}/test 30
if [ -z "${FIO_BINARY+x}" ]; then
if [ -f ${BINARY}/fio ]; then
FIO_BINARY=${BINARY}/fio
else
FIO_BINARY=""
fi
fi
echo "Fuse mounted"
python3 ${SCRIPT_DIR}/concurrent_rmrf.py --mnt_path ${MOUNT1}
echo "### test gc permission check"
mkdir -p ${MOUNT1}/test/data/subdir{1..1024}
sudo mkdir ${MOUNT1}/test/data/subdir999/root
sudo touch ${MOUNT1}/test/data/subdir999/root/file-{1..5}
mv ${MOUNT1}/test/data ${MOUNT1}/3fs-virt/rm-rf
sleep 5
stat ${MOUNT1}/trash/gc-orphans
ls -lR ${MOUNT1}/trash/gc-orphans
echo "### test trash"
bash ${SCRIPT_DIR}/test_trash.sh ${BINARY} ${TEST_DIR}
echo "### test ioctl"
sudo rm -rf ${MOUNT1}/trash/`id -un`/*
python3 ${SCRIPT_DIR}/test_ioctl.py ${MOUNT1} ${MOUNT1}/test/ioctl
for MOUNT in ${MOUNT1} ${MOUNT2} ${MOUNT3} ${MOUNT4}; do
new_chunk_engine=$((( RANDOM % 2 == 0 )) && echo "true" || echo "false")
echo "### set new_chunk_engine to ${new_chunk_engine}"
${ADMIN_CLI} hot-update-config --node-type META --string "server.meta.enable_new_chunk_engine=${new_chunk_engine}"
echo "### run test under ${MOUNT}"
SUBDIR=${MOUNT}/test/$(random_64)
mkdir -p ${SUBDIR}
echo "use tmp dir ${SUBDIR}"
echo "#### run ci test"
mkdir ${SUBDIR}/fuse_test_ci
python3 ${SCRIPT_DIR}/fuse_test_ci.py ${SUBDIR}/fuse_test_ci
echo ""
echo "#### test read after write"
mkdir ${SUBDIR}/read_after_write
python3 ${SCRIPT_DIR}/read_after_write.py ${SUBDIR}/read_after_write
echo ""
echo "### test client io during update chain table"
# start a read/write task
mkdir ${SUBDIR}/rw_during_update_chain_table
python3 ${SCRIPT_DIR}/read_after_write.py ${SUBDIR}/rw_during_update_chain_table --seconds 60 &
PID=$!
# then update chain table in background
sleep 10
start_time=$(date +%s)
while true; do
current_time=$(date +%s)
elapsed=$(( current_time - start_time ))
if [ $elapsed -lt 30 ]; then
echo "update chain table"
(head -n 1 ${CONFIG}/chain-table.csv && tail -n +2 ${CONFIG}/chain-table.csv | shuf) > ${CONFIG}/chain-table-shuffle.csv
${ADMIN_CLI} upload-chain-table 1 ${CONFIG}/chain-table-shuffle.csv --desc replica-1-shuflle
${ADMIN_CLI} list-chain-tables
else
break
fi
sleep 10
done
wait $PID
echo ""
echo "#### test random write and read"
mkdir ${SUBDIR}/random_rw
python3 ${SCRIPT_DIR}/random_rw.py ${SUBDIR}/random_rw
echo ""
echo "#### test read before close"
mkdir ${SUBDIR}/read_before_close
python3 ${SCRIPT_DIR}/random_rw.py --read_before_close ${SUBDIR}/read_before_close
echo ""
set +e
python3 -c 'import hf3fs_fuse.io as h3io'
ret=$?
set -e
if [ ${ret} -eq 0 ]; then
# cat /proc/self/mountinfo
# echo
# grep hf3fs /proc/self/mountinfo | awk '{print $5, $(NF-2)}'
# ls $(grep hf3fs /proc/self/mountinfo | awk '{print $5}')/
# ls ${MOUNT}
# echo ${SUBDIR}/usrbio
echo '#### test usrbio'
mkdir ${SUBDIR}/usrbio
python3 ${SCRIPT_DIR}/usrbio.py ${SUBDIR}/usrbio ${MOUNT}
else
echo -e "${RED}#### can't inmport hf3fs_fuse.io, skip usrbio test!!!${NC}"
fi
if [ -f "${FIO_BINARY}" ]; then
echo "#### test fio with usrbio"
mkdir ${SUBDIR}/fio
for j in {0..3}; do
for f in {0..2}; do
truncate -s 4G ${SUBDIR}/fio/j${j}-f${f}
done
done
date
LD_PRELOAD="${BINARY}/libhf3fs_api_shared.so" ${FIO_BINARY} -name=test_write -rw=write -direct=1 -ioengine=hf3fs_ring -group_reporting -numjobs=4 -bs=4m -size=4G -directory=${SUBDIR}/fio -filename_format='j$jobnum-f$filenum' -thread=1 -mountpoint=${MOUNT} -verify=crc32c -nrfiles=2
date
ls -lh ${SUBDIR}/fio
date
LD_PRELOAD="${BINARY}/libhf3fs_api_shared.so" ${FIO_BINARY} -name=test_read -rw=randread -direct=1 -ioengine=hf3fs_ring -group_reporting -numjobs=4 -bs=4k -iodepth=100 -time_based -runtime=30 -directory=${SUBDIR}/fio -filename_format='j$jobnum-f$filenum' -thread=1 -mountpoint=${MOUNT} -iodepth_batch_submit=100 -iodepth_batch_complete_min=100 -iodepth_batch_complete_max=100 -size=4G -nrfiles=2
date
else
echo "${RED}#### fio not found, skip fio test!!!${NC}"
fi
if [ -f ${BINARY}/pjdfstest ]; then
echo "#### run pjdfstest"
${BINARY}/pjdfstest -c ${CONFIG}/pjdfstest.toml -p ${SUBDIR}
else
echo "${RED}#### pjdfstest not found, skip pjdfstest!!!${NC}"
fi
touch ${SUBDIR}/test_iflags
python3 ${SCRIPT_DIR}/test_iflags.py ${SUBDIR}/test_iflags
done
echo "### test concurrent rw"
mkdir -p ${MOUNT1}/test/concurrent_rw1 ${MOUNT2}/test/concurrent_rw1
python3 ${SCRIPT_DIR}/concurrent_rw.py ${MOUNT1}/test/concurrent_rw1 ${MOUNT2}/test/concurrent_rw1
mkdir -p ${MOUNT1}/test/concurrent_rw2 ${MOUNT2}/test/concurrent_rw2
python3 ${SCRIPT_DIR}/concurrent_rw.py ${MOUNT2}/test/concurrent_rw2 ${MOUNT1}/test/concurrent_rw2
echo "### test lock directory"
mkdir -p ${MOUNT1}/test/test_dir_lock
python3 ${SCRIPT_DIR}/lock_directory.py ${MOUNT1}/test/test_dir_lock ${MOUNT2}/test/test_dir_lock
echo "### test client io during update chain table"
(head -n 1 ${CONFIG}/chain-table.csv && tail -n +2 ${CONFIG}/chain-table.csv | shuf) > ${CONFIG}/chain-table-2.csv
${ADMIN_CLI} upload-chain-table 1 ${CONFIG}/chain-table-2.csv --desc replica-1-version-2
${ADMIN_CLI} list-chain-tables
echo "### test chattr +i"
sudo chattr +i ${MOUNT1}/test
lsattr ${MOUNT1}
sudo chattr -i ${MOUNT1}/test
echo "unmount fuse and stop all servers"
fusermount3 -u ${MOUNT1}
fusermount3 -u ${MOUNT2}
fusermount3 -u ${MOUNT3}
fusermount3 -u ${MOUNT4}
# 定义函数 kill_and_wait
kill_and_wait() {
local pid="$1"
local cmd_name="$2"
# 检查进程是否存在
if ps -p "$pid" > /dev/null; then
# 杀死进程
kill "$pid"
echo "Sent kill signal to process $cmd_name with PID $pid"
# 等待并检查进程是否退出
for i in {1..5}; do
sleep 1
if ! ps -p "$pid" > /dev/null; then
echo "Process $cmd_name with PID $pid has exited."
return 0
fi
done
# 如果5秒后进程仍然存在
echo -e "\e[31mError: Process $cmd_name with PID $pid did not exit after 5 seconds.\e[0m"
kill -9 "$pid"
else
echo "No process with PID $pid found."
return 0
fi
}
# 获取所有后台任务的PID和命令名
jobs_array=()
while read -r pid cmd; do
jobs_array+=("$pid $cmd")
done < <(jobs -l | awk '{print $2, $3, $4, $5, $6, $7, $8, $9, $10}')
# 按照启动顺序的相反顺序进行 kill
for (( idx=${#jobs_array[@]}-1 ; idx>=0 ; idx-- )); do
job_info=(${jobs_array[idx]})
pid=${job_info[0]}
cmd_name=${job_info[@]:1}
kill_and_wait "$pid" "$cmd_name"
done

46
tests/fuse/test_iflags.py Normal file
View File

@@ -0,0 +1,46 @@
import argparse
import fcntl
import os
import struct
FS_IOC_GETFLAGS = 0x80086601
FS_IOC_SETFLAGS = 0x40086602
FS_HUGE_FILE_FL = 0x00040000
FS_UNSUPPORTED_FL = 0x00800000
def get_iflags(file_path):
with open(file_path, "rb") as f:
buf = struct.pack("I", 0)
flags = fcntl.ioctl(f.fileno(), FS_IOC_GETFLAGS, buf)
iflags = struct.unpack("I", flags)[0]
return iflags
def set_iflags(file_path, iflags):
with open(file_path, "rb") as f:
buf = struct.pack("I", iflags)
fcntl.ioctl(f.fileno(), FS_IOC_SETFLAGS, buf)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("file_path")
args = parser.parse_args()
current_iflags = get_iflags(args.file_path)
print(f"Current iflags: {current_iflags:#010x}")
new_iflags = current_iflags | FS_HUGE_FILE_FL
set_iflags(args.file_path, new_iflags)
updated_iflags = get_iflags(args.file_path)
print(f"Updated iflags: {updated_iflags:#010x}")
assert updated_iflags == new_iflags
try:
set_iflags(args.file_path, updated_iflags | FS_UNSUPPORTED_FL)
assert False
except OSError as ex:
print("set FS_UNSUPPORTED_FL", ex)
pass

121
tests/fuse/test_ioctl.py Normal file
View File

@@ -0,0 +1,121 @@
import os
import argparse
import errno
import shutil
from datetime import datetime, timedelta, timezone
from hf3fs_utils.fs import FileSystem
from hf3fs_utils import trash
def remove_trailing_separator(path: str):
if path.endswith(os.path.sep):
return os.path.dirname(path)
return path
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("mount_point", type=str)
parser.add_argument("test_path", type=str)
args = parser.parse_args()
shutil.rmtree(args.test_path, ignore_errors=True)
fs = FileSystem(args.mount_point)
os.makedirs(os.path.join(args.test_path, "dir", "subdir"), exist_ok=True)
try:
fs.remove(os.path.join(args.test_path, "dir"), False)
assert False
except OSError as ex:
assert ex.errno == errno.ENOTEMPTY, ex
fs.remove(os.path.join(args.test_path, "dir"), True)
assert not os.path.exists(os.path.join(args.test_path, "dir"))
try:
fs.remove(os.path.join(args.mount_point, "../.."), True)
assert False
except RuntimeError as ex:
print(ex)
os.makedirs(os.path.join(args.test_path, "src/subdir"), exist_ok=True)
fs.rename(os.path.join(args.test_path, "src"), os.path.join(args.test_path, "dst"))
assert os.path.exists(os.path.join(args.test_path, "dst"))
os.makedirs(os.path.join(args.test_path, "src"), exist_ok=True)
try:
fs.rename(
os.path.join(args.test_path, "src"), os.path.join(args.test_path, "dst")
)
assert False
except OSError as ex:
assert ex.errno in [errno.EEXIST, errno.ENOTEMPTY], ex
fs.rename(
os.path.join(args.test_path, "src"), os.path.join(args.test_path, "dst", "src")
)
assert os.path.exists(os.path.join(args.test_path, "dst", "src"))
os.makedirs(os.path.join(args.test_path, "src/subdir"), exist_ok=True)
try:
fs.rename(os.path.join(args.test_path, "src"), os.path.expanduser("~"))
assert False
except RuntimeError as ex:
print(ex)
# test trash
# test trash timestamp
raised = False
try:
trash.format_date(datetime.now())
except:
raised = True
assert raised
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
str = trash.format_date(now)
assert trash.parse_date(str) == now
for tz in range(-12, 12):
tz_time = now.astimezone(timezone(timedelta(hours=tz)))
tz_str = trash.format_date(tz_time)
assert tz_str == str, (tz_str, str)
assert tz_time == now, (tz_time, now)
assert trash.parse_date(tz_str) == now, (trash.parse_date(tz_str), now)
try:
trash.TrashConfig("5s", timedelta(seconds=5), timedelta(seconds=5))
assert False
except AssertionError:
pass
for i in range(100):
os.makedirs(os.path.join(args.test_path, f"dir-{i}", "data", "subdir"))
trash_config = trash.TrashConfig("2m", timedelta(minutes=2), timedelta(minutes=1))
my_trash = trash.Trash(fs)
assert not os.listdir(my_trash.user_trash_path), os.listdir(
my_trash.user_trash_path
)
trash_paths = set()
for i in range(100):
trash_path = my_trash.move_to_trash(
os.path.join(args.test_path, f"dir-{i}", "data"), trash_config
)
assert trash_path not in trash_paths, trash_path
trash_paths.add(trash_path)
# time.sleep(1)
assert len(trash_paths) == 100
for t in trash_paths:
assert os.path.exists(t)
# test clean trash
my_trash.move_to_trash(
os.path.join(args.test_path, f"dir-0"),
trash.TrashConfig("1h", timedelta(hours=1), timedelta(minutes=10)),
)
# print(f"before clean", os.listdir(my_trash.user_trash_path))
# trash_cleaner = TrashCleaner(fs)
# trash_cleaner.run(True)
# print(f"after clean", os.listdir(my_trash.user_trash_path))
# time.sleep(180)
# # create some unknown type under
# trash_cleaner.run(True)
# print(f"after clean", os.listdir(my_trash.user_trash_path))
# assert os.listdir(my_trash.user_trash_path)
print("test finish")

185
tests/fuse/test_trash.sh Normal file
View File

@@ -0,0 +1,185 @@
#!/bin/bash
RED='\033[0;31m'
set -e
error_handler() {
echo "Error occurred on line $1"
}
# 设置 trap 来捕获 ERR 信号
trap 'error_handler $LINENO' ERR
check_exists() {
local path="$1"
echo check $path
if [ -e "$path" ]; then
echo "ok, 路径存在: $path"
else
echo "${RED}error, 路径不存在: $path"
exit 1
fi
}
# 检查路径是否不存在
check_not_exists() {
local path="$1"
if [ ! -e "$path" ]; then
echo "ok, 路径不存在: $path"
else
echo "${RED}error, 路径存在: $path"
exit 1
fi
}
assert_fail() {
set +e
# 执行传入的命令(包括管道)
eval "$@"
# 检查整个管道的退出状态码
if [ $? -eq 0 ]; then
echo "Assertion failed: Command '$*' succeeded, but expected to fail."
exit 1
else
echo "Assertion passed: Command '$*' failed as expected."
fi
set -e
}
if (($# < 2)); then
echo "${0} <binary> <test-dir> <log-dir>"
exit 1
fi
BINARY=${1%/}
TEST_DIR=${2%/}
LOG_DIR=${3:-}
DEFAULT_LOG=${TEST_DIR}/log
LOG=${LOG_DIR:=${DEFAULT_LOG}}
MOUNT1=${TEST_DIR}/mnt1
MOUNT2=${TEST_DIR}/mnt2
export HF3FS_CLI_MOUNTPOINT=`realpath ${MOUNT1}`
# test hf3fs trash
sudo rm -rf ${MOUNT1}/trash
# without trash directory, can't rmtree
mkdir -p ${MOUNT1}/test/subdir
assert_fail 'yes | hf3fs_cli rmtree --expire 3hours ${MOUNT1}/test/subdir'
sudo mkdir ${MOUNT1}/trash
sudo mkdir -p ${MOUNT1}/trash/`id -un`
sudo chown ${UID}:${UID} ${MOUNT1}/trash/`id -un`
mkdir -p ${MOUNT1}/trash/`id -un`/invalid-name
mkdir -p ${MOUNT1}/trash/`id -un`/1d-20240801_1200-20240801_1000 # invalid timestamp, begin < end
mkdir -p ${MOUNT1}/trash/`id -un`/1d-20240801_1200-20240801_1300 # expired
mkdir -p ${MOUNT1}/trash/`id -un`/1d-20240801_1200-20240801_1300/data-{1..200}
mkdir -p ${MOUNT1}/trash/`id -un`/1d-20300801_1200-20300801_1300 # not expired
mkdir -p ${MOUNT1}/trash/`id -un`/1d-20300801_1200-20300801_1300/data-{1..200}
# trash cleaner 在删除时使用的是普通用户的 uid/gid, 不应该有 root 权限
sudo mkdir -p ${MOUNT1}/trash/`id -un`/noperm-20240801_1200-20240801_1300/data-{1..20}
# root user's directory
sudo mkdir ${MOUNT1}/trash/user-root
sudo mkdir -p ${MOUNT1}/trash/user-root/1d-20240801_1200-20240801_1300/data
# another user's directory
sudo mkdir ${MOUNT1}/trash/user-1000
sudo chown 1000:1000 ${MOUNT1}/trash/user-1000
sudo mkdir -p ${MOUNT1}/trash/user-1000/1d-20240801_1200-20240801_1300/data
sudo mkdir -p ${MOUNT1}/trash/user-1000/1d-20240801_1200-20240801_1310/data
sudo chown -R 1000:1000 ${MOUNT1}/trash/user-1000/1d-20240801_1200-20240801_1310
sudo ${BINARY}/trash_cleaner --interval 0 --paths ${MOUNT1}/trash/
# after clean check some directory is cleaned, some still exists
echo ==== ${MOUNT1}/trash/`id -un` ====
ls ${MOUNT1}/trash/`id -un`
check_exists ${MOUNT1}/trash/`id -un`/invalid-name
check_exists ${MOUNT1}/trash/`id -un`/1d-20240801_1200-20240801_1000
check_exists ${MOUNT1}/trash/`id -un`/1d-20300801_1200-20300801_1300
check_not_exists ${MOUNT1}/trash/`id -un`/1d-20240801_1200-20240801_1300
check_exists ${MOUNT1}/trash/`id -un`/noperm-20240801_1200-20240801_1300/data-{1..20}
echo ==== ${MOUNT1}/trash/`id -un`/1d-20300801_1200-20300801_1300 ====
check_exists ${MOUNT1}/trash/`id -un`/1d-20300801_1200-20300801_1300
ls ${MOUNT1}/trash/`id -un`/1d-20300801_1200-20300801_1300
# rmtree to remove trash immediately
assert_fail 'yes | hf3fs_cli rmtree ${MOUNT1}/trash/`id -un`/'
assert_fail 'yes | hf3fs_cli rmtree ${MOUNT1}/trash/`id -un`/1d-20300801_1200-20300801_1300 --expire 3hours'
check_exists ${MOUNT1}/trash/`id -un`/1d-20300801_1200-20300801_1300
yes | hf3fs_cli rmtree ${MOUNT1}/trash/`id -un`/1d-20300801_1200-20300801_1300
check_not_exists ${MOUNT1}/trash/`id -un`/1d-20300801_1200-20300801_1300
# root user's trash directory should exists
check_exists ${MOUNT1}/trash/user-root
check_exists ${MOUNT1}/trash/user-root/1d-20240801_1200-20240801_1300/data
# another user's trash directory
check_exists ${MOUNT1}/trash/user-1000/
# no permission to remove
check_exists ${MOUNT1}/trash/user-1000/1d-20240801_1200-20240801_1300/data
check_not_exists ${MOUNT1}/trash/user-1000/1d-20240801_1200-20240801_1310/data
# test hf3fs cli
sudo rm -rf ${MOUNT1}/root
sudo mkdir ${MOUNT1}/root
sudo mkdir ${MOUNT1}/root/`id -un`
sudo chown ${UID}:${UID} ${MOUNT1}/root/`id -un`
mkdir ${MOUNT1}/root/`id -un`/subdir
assert_fail 'yes | hf3fs_cli rmtree --expire 3hours ${MOUNT1}/test/some-not-found-path'
assert_fail 'hf3fs_cli mv ${MOUNT1}/test/some-not-found-path ${MOUNT1}/'
assert_fail 'hf3fs_cli mv ${MOUNT1}/root/`id -un`/subdir ${MOUNT1}/root/`id -un`/subdir'
# don't support ..
assert_fail 'yes | hf3fs_cli rmtree --expire 1h ${MOUNT1}/root/../root/`id -un`/subdir'
assert_fail 'yes | hf3fs_cli rmtree --expire 1h ${MOUNT1}/root/`id -un`/subdir2/../subdir'
hf3fs_cli mv ${MOUNT1}/root/`id -un`/subdir ${MOUNT1}/root/`id -un`/another-dir
assert_fail 'hf3fs_cli mv ${MOUNT1}/root/`id -un`/another-dir ${MOUNT1}/root/`id -un`/another-dir/another-dir'
mkdir ${MOUNT1}/root/`id -un`/subdir
hf3fs_cli mv ${MOUNT1}/root/`id -un`/subdir ${MOUNT1}/root/`id -un`/another-dir
check_exists ${MOUNT1}/root/`id -un`/another-dir/subdir
mkdir ${MOUNT1}/root/`id -un`/subdir
assert_fail 'hf3fs_cli mv ${MOUNT1}/root/`id -un`/subdir ${MOUNT1}/root/`id -un`/another-dir'
yes | hf3fs_cli rmtree --expire 3hours ${MOUNT1}/root/`id -un`/another-dir
mkdir ${MOUNT1}/root/`id -un`/another-dir
yes | hf3fs_cli rmtree --expire 3hours ${MOUNT1}/root/`id -un`/another-dir
mkdir ${MOUNT1}/root/`id -un`/another-dir
yes | hf3fs_cli rmtree --expire 3hours ${MOUNT1}/root/`id -un`/another-dir
stat ${MOUNT1}/trash/`id -un`/*/another-dir
stat ${MOUNT1}/trash/`id -un`/*/another-dir.*
#don't allow move into trash
mkdir ${MOUNT1}/root/`id -un`/adir
assert_fail 'mv ${MOUNT1}/root/`id -un`/adir ${MOUNT1}/trash/`id -un`/'
# allow mv between trash
mkdir ${MOUNT1}/trash/`id -un`/srctrash
mv ${MOUNT1}/trash/`id -un`/srctrash ${MOUNT1}/trash/`id -un`/dsttrash
# rmtree don't support symlink
pushd ${MOUNT1}/root/`id -un`/
mkdir -p dir1/subdir
ln -s dir1 symlink-to-dir1
yes | hf3fs_cli rmtree --expire 3hours symlink-to-dir1
check_not_exists symlink-to-dir1
check_exists dir1
yes | hf3fs_cli rmtree --expire 3hours dir1
check_not_exists dir1
mkdir -p dir-otheruser/subdir
sudo chown -R 999:999 dir-otheruser
sudo chmod 0777 dir-otheruser
assert_fail 'yes | hf3fs_cli rmtree --expire 3hours dir-otheruser'
check_exists dir-otheruser
popd
# don't allow cross filesystem link
mkdir ${MOUNT1}/root/`id -un`/src
assert_fail 'hf3fs_cli mv ${MOUNT1}/root/`id -un`/src ${MOUNT2}/root/`id -un`/dest'
hf3fs_cli mv ${MOUNT1}/root/`id -un`/src ${MOUNT1}/root/`id -un`/dest
echo ======= 测试完成 ========

View File

@@ -0,0 +1,101 @@
import os
import argparse
import time
import shutil
from datetime import datetime, timedelta, timezone
UTC8_TZ = timezone(timedelta(hours=8))
DATE_FORMAT = "%Y%m%d_%H%M"
def format_date(t: datetime) -> str:
assert t.tzinfo
return t.astimezone(tz=UTC8_TZ).strftime(DATE_FORMAT)
def make_trash(trash_dir: str, mode=0o755):
tmp_path = f"{trash_dir}.tmp"
os.makedirs(tmp_path)
for i in range(0, 10):
os.makedirs(os.path.join(tmp_path, f"subdir-{i}"))
for i in range(0, 10):
with open(os.path.join(tmp_path, "file-{i}"), "w") as f:
f.write("some txt")
os.chmod(tmp_path, mode)
os.rename(tmp_path, trash_dir)
return trash_dir
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("trash", type=str, help="path to trash directory")
args = parser.parse_args()
trash = args.trash
invalid_dir = make_trash(os.path.join(trash, "invalid_name"))
nonutf8_dir = os.path.join(trash.encode('utf8'), b"\xee\x80\x81\x82")
os.mkdir(nonutf8_dir)
now = datetime.now(tz=UTC8_TZ)
befores = [
now - 2 * timedelta(minutes=10),
now - timedelta(minutes=10),
]
afters = [now + timedelta(minutes=10), now + 2 * timedelta(minutes=10)]
expired = os.path.join(
trash, f"expired-{format_date(befores[0])}-{format_date(befores[1])}"
)
make_trash(expired)
expired_no_perm = os.path.join(
trash, f"expired_no_perm-{format_date(befores[0])}-{format_date(befores[1])}"
)
make_trash(expired_no_perm, 0o000)
expired_invalid = os.path.join(
trash, f"expired-invalid-{format_date(befores[0])}-{format_date(befores[1])}"
)
make_trash(expired_invalid)
expired_file = os.path.join(
trash, f"expiredfile-{format_date(befores[0])}-{format_date(befores[1])}"
)
with open(expired_file, "w") as f:
f.write(expired_file)
expired_invalid_ts = os.path.join(
trash, f"expired-{format_date(befores[1])}-{format_date(befores[0])}"
)
make_trash(expired_invalid_ts)
notexpired = [
os.path.join(
trash, f"notexpired-{format_date(befores[1])}-{format_date(afters[0])}"
),
os.path.join(
trash, f"notexpired-{format_date(afters[0])}-{format_date(afters[1])}"
),
]
for p in notexpired:
make_trash(p)
print("sleep 180s")
time.sleep(180)
exists = [
nonutf8_dir,
invalid_dir, expired_no_perm, expired_invalid, expired_invalid_ts, expired_file
] + notexpired
notexists = [ expired ]
print("check exists")
for e in exists:
assert os.path.exists(e), f"{e} not exists"
print("check not exists")
for ne in notexists:
assert not os.path.exists(ne), f"{ne} exists"
print("test finished, clean")
for e in exists:
os.chmod(e, 0o777)
if os.path.isdir(e):
shutil.rmtree(e)
else:
os.unlink(e)
print("clean finished")

65
tests/fuse/usrbio.py Normal file
View File

@@ -0,0 +1,65 @@
import hf3fs_fuse.io as h3io
import numpy as np
import os
import sys
import multiprocessing as mp
import multiprocessing.shared_memory
import time
fn = sys.argv[1] + '/file'
fd = os.open(fn, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644)
data = np.random.randn(200 << 20).tobytes()
os.write(fd, data)
os.close(fd)
h3mp = h3io.extract_mount_point(sys.argv[1])
bs = h3io.read_file(fn, hf3fs_mount_point=h3mp, priority=h3io.IorPriority.HIGH)
assert data == bs, f'{fn} read out data not equal to those written in'
shm = mp.shared_memory.SharedMemory(size=10<<20, create=True)
iov = h3io.make_iovec(shm, h3mp, 0, -1)
shm.unlink()
ior = h3io.make_ioring(h3mp, 100, False, 0)
fn = sys.argv[1] + '/file2'
fd = os.open(fn, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644)
h3io.register_fd(fd)
shm.buf[:] = bytes(len(shm.buf))
ior.prepare(iov[:], False, fd, 0)
write_len = ior.submit().wait(min_results=1)[0].result
assert write_len == len(shm.buf)
time.sleep(6) # After linux inode cache invalidated
assert os.path.getsize(fn) == len(shm.buf)
ior.prepare(iov[:], False, fd, len(shm.buf))
write_len = ior.submit().wait(min_results=1)[0].result
assert write_len == len(shm.buf)
h3io.force_fsync(fd) # Force update length
assert os.path.getsize(fn) == 2 * len(shm.buf)
h3io.deregister_fd(fd)
os.close(fd)
print('usrbio read test succeeded', len(shm.buf))
fn = sys.argv[1] + '/file3'
fd = os.open(fn, os.O_RDONLY | os.O_CREAT | os.O_EXCL, 0o666)
os.close(fd)
fd = os.open(fn, os.O_RDONLY) # No write permission
h3io.register_fd(fd)
erred = False
try:
ior.prepare(iov[:], False, fd, 0)
except OSError as e:
erred = True
assert e.errno == 13, "Errno not correct" # Should raise EPERM
assert erred, "Didn't raise EPERM"
h3io.deregister_fd(fd)
os.close(fd)
print('usrbio permission test succeeded')