Skip to content

Commit d749988

Browse files
authored
Merge pull request #348 from abhinavagarwal07/add-tests-round-2
add tests for error paths, option coverage, and fix existing test bugs
2 parents ea60e34 + 7921230 commit d749988

1 file changed

Lines changed: 260 additions & 5 deletions

File tree

test/test_sshfs.py

Lines changed: 260 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,11 @@ def test_sshfs(
163163
tst_truncate_path(mnt_dir)
164164
tst_truncate_fd(mnt_dir)
165165
tst_open_unlink(mnt_dir)
166+
tst_open_writeonly_read(mnt_dir)
167+
tst_access(mnt_dir)
168+
tst_mkdir_exist(mnt_dir)
169+
tst_readdir_repeated(mnt_dir)
170+
tst_rename_sibling(mnt_dir)
166171
except Exception as exc:
167172
cleanup(mount_process, mnt_dir)
168173
raise exc
@@ -224,6 +229,8 @@ def tst_rename(mnt_dir):
224229
os.rename(src_name, dst_name)
225230

226231
assert not os.path.exists(src_name)
232+
assert os.path.basename(src_name) not in os.listdir(mnt_dir)
233+
assert os.path.basename(dst_name) in os.listdir(mnt_dir)
227234
with open(dst_name, "rb") as fh:
228235
assert fh.read() == data
229236

@@ -244,6 +251,7 @@ def tst_rename_over(mnt_dir):
244251
os.rename(src_name, dst_name)
245252

246253
assert not os.path.exists(src_name)
254+
assert os.path.basename(src_name) not in os.listdir(mnt_dir)
247255
with open(dst_name, "rb") as fh:
248256
assert fh.read() == src_data
249257

@@ -296,6 +304,9 @@ def tst_symlink(mnt_dir):
296304
assert fstat.st_nlink == 1
297305
assert linkname in os.listdir(mnt_dir)
298306

307+
os.unlink(fullname)
308+
assert linkname not in os.listdir(mnt_dir)
309+
299310

300311
def tst_create(mnt_dir):
301312
name = name_generator()
@@ -392,7 +403,7 @@ def tst_open_unlink(mnt_dir):
392403
os.unlink(fullname)
393404
with pytest.raises(OSError) as exc_info:
394405
os.stat(fullname)
395-
assert exc_info.value.errno == errno.ENOENT
406+
assert exc_info.value.errno == errno.ENOENT
396407
assert name not in os.listdir(mnt_dir)
397408
fh.write(data2)
398409
fh.seek(0)
@@ -415,6 +426,83 @@ def tst_statvfs(src_dir, mnt_dir):
415426
assert vfs.f_namemax > 0
416427

417428

429+
def tst_open_writeonly_read(mnt_dir):
430+
name = pjoin(mnt_dir, name_generator())
431+
fd = os.open(name, os.O_CREAT | os.O_WRONLY)
432+
try:
433+
os.write(fd, b"hello")
434+
with pytest.raises(OSError) as exc_info:
435+
os.read(fd, 10)
436+
assert exc_info.value.errno == errno.EBADF
437+
finally:
438+
os.close(fd)
439+
os.unlink(name)
440+
441+
442+
def tst_access(mnt_dir):
443+
filename = pjoin(mnt_dir, name_generator())
444+
with open(filename, "wb") as fh:
445+
fh.write(b"test")
446+
os.chmod(filename, 0o644)
447+
assert os.access(filename, os.R_OK)
448+
if os.getuid() != 0:
449+
assert not os.access(filename, os.X_OK)
450+
os.unlink(filename)
451+
452+
453+
def tst_mkdir_exist(mnt_dir):
454+
name = name_generator()
455+
fullname = pjoin(mnt_dir, name)
456+
os.mkdir(fullname)
457+
with pytest.raises(OSError) as exc_info:
458+
os.mkdir(fullname)
459+
assert exc_info.value.errno == errno.EEXIST
460+
os.rmdir(fullname)
461+
462+
463+
def tst_readdir_repeated(mnt_dir):
464+
dirname = pjoin(mnt_dir, name_generator())
465+
os.mkdir(dirname)
466+
names = []
467+
for i in range(5):
468+
n = name_generator()
469+
names.append(n)
470+
with open(pjoin(dirname, n), "wb") as fh:
471+
fh.write(b"x")
472+
473+
# Verify repeated directory listings return consistent results
474+
listing1 = sorted(os.listdir(dirname))
475+
listing2 = sorted(os.listdir(dirname))
476+
assert listing1 == sorted(names)
477+
assert listing1 == listing2
478+
479+
for n in names:
480+
os.unlink(pjoin(dirname, n))
481+
os.rmdir(dirname)
482+
483+
484+
def tst_rename_sibling(mnt_dir):
485+
# Verify renaming one file doesn't break access to a sibling
486+
name_a = pjoin(mnt_dir, name_generator())
487+
name_b = pjoin(mnt_dir, name_generator())
488+
name_c = pjoin(mnt_dir, name_generator())
489+
490+
with open(name_a, "wb") as fh:
491+
fh.write(b"aaa")
492+
with open(name_b, "wb") as fh:
493+
fh.write(b"bbb")
494+
495+
os.rename(name_a, name_c)
496+
497+
assert not os.path.exists(name_a)
498+
assert os.path.exists(name_b)
499+
with open(name_b, "rb") as fh:
500+
assert fh.read() == b"bbb"
501+
502+
os.unlink(name_b)
503+
os.unlink(name_c)
504+
505+
418506
def tst_link(mnt_dir, cache_timeout):
419507
name1 = pjoin(mnt_dir, name_generator())
420508
name2 = pjoin(mnt_dir, name_generator())
@@ -455,6 +543,10 @@ def tst_link(mnt_dir, cache_timeout):
455543
assert os.path.basename(name2) not in os.listdir(mnt_dir)
456544
with pytest.raises(FileNotFoundError):
457545
os.lstat(name2)
546+
if cache_timeout:
547+
safe_sleep(cache_timeout + 1)
548+
fstat1 = os.lstat(name1)
549+
assert fstat1.st_nlink == 1
458550

459551
os.unlink(name1)
460552

@@ -508,6 +600,10 @@ def tst_truncate_path(mnt_dir):
508600
with open(filename, "rb") as fh:
509601
assert fh.read(size) == TEST_DATA[: size - 1024]
510602

603+
# Truncate to zero
604+
os.truncate(filename, 0)
605+
assert os.stat(filename).st_size == 0
606+
511607
os.unlink(filename)
512608

513609

@@ -533,6 +629,10 @@ def tst_truncate_fd(mnt_dir):
533629
fh.seek(0)
534630
assert fh.read(size) == TEST_DATA[: size - 1024]
535631

632+
# Truncate to zero via fd
633+
os.ftruncate(fd, 0)
634+
assert os.fstat(fd).st_size == 0
635+
536636

537637
def tst_utimens(mnt_dir, tol=0):
538638
filename = pjoin(mnt_dir, name_generator())
@@ -573,7 +673,7 @@ def tst_utimens_now(mnt_dir):
573673
def tst_passthrough(src_dir, mnt_dir, cache_timeout):
574674
name = name_generator()
575675
src_name = pjoin(src_dir, name)
576-
mnt_name = pjoin(src_dir, name)
676+
mnt_name = pjoin(mnt_dir, name)
577677
assert name not in os.listdir(src_dir)
578678
assert name not in os.listdir(mnt_dir)
579679
with open(src_name, "w") as fh:
@@ -582,11 +682,16 @@ def tst_passthrough(src_dir, mnt_dir, cache_timeout):
582682
if cache_timeout:
583683
safe_sleep(cache_timeout + 1)
584684
assert name in os.listdir(mnt_dir)
585-
assert os.stat(src_name) == os.stat(mnt_name)
685+
src_st = os.stat(src_name)
686+
mnt_st = os.stat(mnt_name)
687+
assert src_st.st_size == mnt_st.st_size
688+
assert src_st.st_uid == mnt_st.st_uid
689+
assert src_st.st_gid == mnt_st.st_gid
690+
assert abs(src_st.st_mtime - mnt_st.st_mtime) <= 1
586691

587692
name = name_generator()
588693
src_name = pjoin(src_dir, name)
589-
mnt_name = pjoin(src_dir, name)
694+
mnt_name = pjoin(mnt_dir, name)
590695
assert name not in os.listdir(src_dir)
591696
assert name not in os.listdir(mnt_dir)
592697
with open(mnt_name, "w") as fh:
@@ -595,4 +700,154 @@ def tst_passthrough(src_dir, mnt_dir, cache_timeout):
595700
if cache_timeout:
596701
safe_sleep(cache_timeout + 1)
597702
assert name in os.listdir(mnt_dir)
598-
assert os.stat(src_name) == os.stat(mnt_name)
703+
src_st = os.stat(src_name)
704+
mnt_st = os.stat(mnt_name)
705+
assert src_st.st_size == mnt_st.st_size
706+
assert src_st.st_uid == mnt_st.st_uid
707+
assert src_st.st_gid == mnt_st.st_gid
708+
assert abs(src_st.st_mtime - mnt_st.st_mtime) <= 1
709+
710+
711+
def _check_ssh_localhost():
712+
try:
713+
res = subprocess.call(
714+
["ssh", "-o", "StrictHostKeyChecking=no",
715+
"-o", "KbdInteractiveAuthentication=no",
716+
"-o", "ChallengeResponseAuthentication=no",
717+
"-o", "PasswordAuthentication=no",
718+
"localhost", "--", "true"],
719+
stdin=subprocess.DEVNULL, timeout=10,
720+
)
721+
except subprocess.TimeoutExpired:
722+
res = 1
723+
if res != 0:
724+
pytest.fail("Unable to ssh into localhost without password prompt.")
725+
726+
727+
_mount_ctr = [0]
728+
729+
730+
def _mount_sshfs(tmpdir, extra_opts=None):
731+
"""Helper to mount sshfs with custom options. Returns (mount_process, mnt_dir, src_dir)."""
732+
_check_ssh_localhost()
733+
_mount_ctr[0] += 1
734+
mnt_dir = str(tmpdir.mkdir(f"mnt{_mount_ctr[0]}"))
735+
src_dir = str(tmpdir.mkdir(f"src{_mount_ctr[0]}"))
736+
737+
cmdline = base_cmdline + [
738+
pjoin(basename, "sshfs"),
739+
"-f",
740+
f"localhost:{src_dir}",
741+
mnt_dir,
742+
"-o", "entry_timeout=0",
743+
"-o", "attr_timeout=0",
744+
]
745+
if extra_opts:
746+
for opt in extra_opts:
747+
cmdline += ["-o", opt]
748+
749+
new_env = dict(os.environ)
750+
new_env["G_DEBUG"] = "fatal-warnings"
751+
752+
mount_process = subprocess.Popen(cmdline, env=new_env)
753+
try:
754+
wait_for_mount(mount_process, mnt_dir)
755+
except:
756+
cleanup(mount_process, mnt_dir)
757+
raise
758+
return mount_process, mnt_dir, src_dir
759+
760+
761+
def test_disable_hardlink(tmpdir, capfd):
762+
capfd.register_output(r"^Warning: Permanently added 'localhost' .+", count=0)
763+
764+
# Control: verify hardlinks work without disable_hardlink.
765+
# If the server lacks the extension, skip this test entirely.
766+
mount_process, mnt_dir, src_dir = _mount_sshfs(tmpdir, [])
767+
try:
768+
name1 = pjoin(mnt_dir, name_generator())
769+
name2 = pjoin(mnt_dir, name_generator())
770+
with open(name1, "wb") as fh:
771+
fh.write(b"test")
772+
try:
773+
os.link(name1, name2)
774+
except OSError:
775+
os.unlink(name1)
776+
pytest.skip("server does not support hardlink extension")
777+
os.unlink(name2)
778+
os.unlink(name1)
779+
except Exception:
780+
cleanup(mount_process, mnt_dir)
781+
raise
782+
else:
783+
umount(mount_process, mnt_dir)
784+
785+
# Now test with disable_hardlink — links should fail
786+
mount_process, mnt_dir, src_dir = _mount_sshfs(tmpdir, ["disable_hardlink"])
787+
try:
788+
name1 = pjoin(mnt_dir, name_generator())
789+
name2 = pjoin(mnt_dir, name_generator())
790+
with open(name1, "wb") as fh:
791+
fh.write(b"test")
792+
with pytest.raises(OSError) as exc_info:
793+
os.link(name1, name2)
794+
assert exc_info.value.errno in (errno.ENOSYS, errno.EPERM)
795+
os.unlink(name1)
796+
except Exception:
797+
cleanup(mount_process, mnt_dir)
798+
raise
799+
else:
800+
umount(mount_process, mnt_dir)
801+
802+
803+
def test_follow_symlinks(tmpdir, capfd):
804+
capfd.register_output(r"^Warning: Permanently added 'localhost' .+", count=0)
805+
mount_process, mnt_dir, src_dir = _mount_sshfs(tmpdir, ["follow_symlinks"])
806+
try:
807+
target_name = name_generator()
808+
target = pjoin(src_dir, target_name)
809+
with open(target, "wb") as fh:
810+
fh.write(b"symlink target data")
811+
812+
link = pjoin(src_dir, name_generator())
813+
os.symlink(target_name, link)
814+
815+
mnt_link = pjoin(mnt_dir, os.path.basename(link))
816+
# With follow_symlinks, stat should return the target's attributes
817+
# and the entry should appear as a regular file, not a symlink
818+
fstat = os.lstat(mnt_link)
819+
assert stat.S_ISREG(fstat.st_mode)
820+
with open(mnt_link, "rb") as fh:
821+
assert fh.read() == b"symlink target data"
822+
823+
os.unlink(link)
824+
os.unlink(target)
825+
except Exception:
826+
cleanup(mount_process, mnt_dir)
827+
raise
828+
else:
829+
umount(mount_process, mnt_dir)
830+
831+
832+
def test_direct_io(tmpdir, capfd):
833+
capfd.register_output(r"^Warning: Permanently added 'localhost' .+", count=0)
834+
mount_process, mnt_dir, src_dir = _mount_sshfs(tmpdir, ["direct_io"])
835+
try:
836+
name = name_generator()
837+
mnt_name = pjoin(mnt_dir, name)
838+
src_name = pjoin(src_dir, name)
839+
data = b"direct io test data\n" * 100
840+
841+
with open(mnt_name, "wb") as fh:
842+
fh.write(data)
843+
with open(mnt_name, "rb") as fh:
844+
assert fh.read() == data
845+
with open(src_name, "rb") as fh:
846+
assert fh.read() == data
847+
848+
os.unlink(mnt_name)
849+
except Exception:
850+
cleanup(mount_process, mnt_dir)
851+
raise
852+
else:
853+
umount(mount_process, mnt_dir)

0 commit comments

Comments
 (0)