1
0
mirror of https://github.com/arsenetar/send2trash.git synced 2025-08-30 12:39:43 +00:00

Compare commits

...

10 Commits

Author SHA1 Message Date
10c7693d11
Minor fixes to tests 2021-03-17 21:51:51 -05:00
356509120b
Add some checks to catch test failure
Really just checking that the setup is able to create test files so it
is known they were there then removed.
Windows tests really need verification of
recycle, which is not present.
2021-03-17 20:52:16 -05:00
f9fcdb8d8c
Fix legacy windows platform for multibyte unicode
- Add handling to create correctly sized buffer even with multibyte
characters as len() in python does not line up with what
create_unicode_buffer() needs for length.
- Add test for single and multiple files
2021-03-10 21:41:30 -06:00
af0c1ba704
More test fixes
- Fix travis windows env
- Fix exception type in test_plat_win
- Finish converting tests in test_plat_other, fix exception type
2021-03-10 18:57:35 -06:00
37be84d46e
Update .travis.yml 2021-03-02 22:48:48 -06:00
9f76fbf036
Updates for tests on windows
- Other platform tests WIP
- Windows long directory tests WIP
2021-03-02 19:33:44 -06:00
a324923ffa
Add IFileOperationProgressSink
- Add sink to allow detection of times when the file would be deleted
- Currently can detect, but not stop operations, more work needed
2021-03-02 19:23:43 -06:00
dbdcce8b04
First batch of updates to unit tests
- Remove content from __init__.py
- Change test_script_main to use pytest
- Update test_script_main to run on windows as well as linux
2021-03-02 00:26:29 -06:00
054d56c564
Update Tox and Travis configurations 2021-03-02 00:24:59 -06:00
33ed07811b
Cleanup flake8 issues 2021-03-01 23:44:03 -06:00
11 changed files with 508 additions and 375 deletions

View File

@ -3,16 +3,14 @@ matrix:
include: include:
- os: windows - os: windows
language: sh language: sh
python: 3 python: "3.8"
env: "PATH=/c/Python38:/c/Python38/Scripts:$PATH"
# Perform the manual steps on windows to install python3 # Perform the manual steps on windows to install python3
before_install: before_install:
- choco install python3 --params "/InstallDir:C:\Python" - choco install python --version=3.8.6
- export PATH="/c/Python:/c/Python/Scripts:$PATH"
- python -m pip install --upgrade pip - python -m pip install --upgrade pip
- python -m pip install pywin32
before_script: before_script:
- export TOXENV=py3-win - export TOXENV=py38
- python: "2.7" - python: "2.7"
- python: "3.4" - python: "3.4"
- python: "3.5" - python: "3.5"
@ -27,7 +25,6 @@ matrix:
arch: ppc64le arch: ppc64le
- python: "3.6" - python: "3.6"
arch: ppc64le arch: ppc64le
install: install:
- python -m pip install tox - python -m pip install tox
before_script: before_script:

View File

@ -0,0 +1,105 @@
# Sample implementation of IFileOperationProgressSink that just prints
# some basic info
import pythoncom
from win32com.shell import shell, shellcon
from win32com.server.policy import DesignatedWrapPolicy
class FileOperationProgressSink(DesignatedWrapPolicy):
_com_interfaces_ = [shell.IID_IFileOperationProgressSink]
_public_methods_ = [
"StartOperations",
"FinishOperations",
"PreRenameItem",
"PostRenameItem",
"PreMoveItem",
"PostMoveItem",
"PreCopyItem",
"PostCopyItem",
"PreDeleteItem",
"PostDeleteItem",
"PreNewItem",
"PostNewItem",
"UpdateProgress",
"ResetTimer",
"PauseTimer",
"ResumeTimer",
]
def __init__(self):
self._wrap_(self)
self.newItem = None
def PreDeleteItem(self, flags, item):
# Can detect cases where to stop via flags and condition below, however the operation
# does not actual stop, we can resort to raising an exception as that does stop things
# but that may need some additional considerations before implementing.
return (
0 if flags & shellcon.TSF_DELETE_RECYCLE_IF_POSSIBLE else 0x80004005
) # S_OK, or E_FAIL
def PostDeleteItem(self, flags, item, hrDelete, newlyCreated):
if newlyCreated:
self.newItem = newlyCreated.GetDisplayName(shellcon.SHGDN_FORPARSING)
def StartOperations(self):
pass
def FinishOperations(self, Result):
pass
def PreRenameItem(self, Flags, Item, NewName):
pass
def PostRenameItem(self, Flags, Item, NewName, hrRename, NewlyCreated):
pass
def PreMoveItem(self, Flags, Item, DestinationFolder, NewName):
pass
def PostMoveItem(
self, Flags, Item, DestinationFolder, NewName, hrMove, NewlyCreated
):
pass
def PreCopyItem(self, Flags, Item, DestinationFolder, NewName):
pass
def PostCopyItem(
self, Flags, Item, DestinationFolder, NewName, hrCopy, NewlyCreated
):
pass
def PreNewItem(self, Flags, DestinationFolder, NewName):
pass
def PostNewItem(
self,
Flags,
DestinationFolder,
NewName,
TemplateName,
FileAttributes,
hrNew,
NewItem,
):
pass
def UpdateProgress(self, WorkTotal, WorkSoFar):
pass
def ResetTimer(self):
pass
def PauseTimer(self):
pass
def ResumeTimer(self):
pass
def CreateSink():
return pythoncom.WrapObject(
FileOperationProgressSink(), shell.IID_IFileOperationProgressSink
)

View File

@ -2,7 +2,7 @@ import errno
from .compat import PY3 from .compat import PY3
if PY3: if PY3:
_permission_error = PermissionError _permission_error = PermissionError # noqa: F821
else: else:
_permission_error = OSError _permission_error = OSError

View File

@ -77,8 +77,6 @@ def send2trash(paths):
paths = [op.abspath(path) if not op.isabs(path) else path for path in paths] paths = [op.abspath(path) if not op.isabs(path) else path for path in paths]
# get short path to handle path length issues # get short path to handle path length issues
paths = [get_short_path_name(path) for path in paths] paths = [get_short_path_name(path) for path in paths]
# convert to a single string of null terminated paths
paths = "\0".join(paths)
fileop = SHFILEOPSTRUCTW() fileop = SHFILEOPSTRUCTW()
fileop.hwnd = 0 fileop.hwnd = 0
fileop.wFunc = FO_DELETE fileop.wFunc = FO_DELETE
@ -93,7 +91,15 @@ def send2trash(paths):
# NOTE: based on how python allocates memory for these types they should # NOTE: based on how python allocates memory for these types they should
# always be zero, if this is ever not true we can go back to explicitly # always be zero, if this is ever not true we can go back to explicitly
# setting the last two characters to null using buffer[index] = '\0'. # setting the last two characters to null using buffer[index] = '\0'.
buffer = create_unicode_buffer(paths, len(paths) + 2) # Additional note on another issue here, unicode_buffer expects length in
# bytes essentially, so having multi-byte characters causes issues if just
# passing pythons string length. Instead of dealing with this difference we
# just create a buffer then a new one with an extra null. Since the non-length
# specified version apparently stops after the first null, join with a space first.
buffer = create_unicode_buffer(" ".join(paths))
# convert to a single string of null terminated paths
path_string = "\0".join(paths)
buffer = create_unicode_buffer(path_string, len(buffer) + 1)
fileop.pFrom = LPCWSTR(addressof(buffer)) fileop.pFrom = LPCWSTR(addressof(buffer))
fileop.pTo = None fileop.pTo = None
fileop.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT fileop.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT

View File

@ -11,6 +11,7 @@ from platform import version
import pythoncom import pythoncom
import pywintypes import pywintypes
from win32com.shell import shell, shellcon from win32com.shell import shell, shellcon
from .IFileOperationProgressSink import CreateSink
def send2trash(paths): def send2trash(paths):
@ -50,10 +51,11 @@ def send2trash(paths):
# actually try to perform the operation, this section may throw a # actually try to perform the operation, this section may throw a
# pywintypes.com_error which does not seem to create as nice of an # pywintypes.com_error which does not seem to create as nice of an
# error as OSError so wrapping with try to convert # error as OSError so wrapping with try to convert
sink = CreateSink()
try: try:
for path in paths: for path in paths:
item = shell.SHCreateItemFromParsingName(path, None, shell.IID_IShellItem) item = shell.SHCreateItemFromParsingName(path, None, shell.IID_IShellItem)
fileop.DeleteItem(item) fileop.DeleteItem(item, sink)
result = fileop.PerformOperations() result = fileop.PerformOperations()
aborted = fileop.GetAnyOperationsAborted() aborted = fileop.GetAnyOperationsAborted()
# if non-zero result or aborted throw an exception # if non-zero result or aborted throw an exception

View File

@ -1,18 +0,0 @@
import sys
import unittest
def TestSuite():
suite = unittest.TestSuite()
loader = unittest.TestLoader()
if sys.platform == "win32":
from . import test_plat_win
suite.addTests(loader.loadTestsFromModule(test_plat_win))
else:
from . import test_script_main
from . import test_plat_other
suite.addTests(loader.loadTestsFromModule(test_script_main))
suite.addTests(loader.loadTestsFromModule(test_plat_other))
return suite

View File

@ -1,51 +1,53 @@
# encoding: utf-8 # encoding: utf-8
import pytest
import codecs import codecs
import unittest
import os import os
import sys
from os import path as op from os import path as op
import send2trash.plat_other
from send2trash.plat_other import send2trash as s2t
from send2trash.compat import PY3 from send2trash.compat import PY3
from send2trash import TrashPermissionError
try: try:
from configparser import ConfigParser from configparser import ConfigParser
except ImportError: except ImportError:
# py2 # py2
from ConfigParser import ConfigParser from ConfigParser import ConfigParser # noqa: F401
from tempfile import mkdtemp, NamedTemporaryFile, mktemp from tempfile import mkdtemp, NamedTemporaryFile, mktemp
import shutil import shutil
import stat import stat
import sys
# Could still use cleaning up. But no longer relies on ramfs. if sys.platform != "win32":
import send2trash.plat_other
from send2trash.plat_other import send2trash as s2t
HOMETRASH = send2trash.plat_other.HOMETRASH HOMETRASH = send2trash.plat_other.HOMETRASH
else:
pytest.skip("Skipping non-windows tests", allow_module_level=True)
def touch(path): @pytest.fixture
with open(path, "a"): def testfile():
os.utime(path, None) file = NamedTemporaryFile(
class TestHomeTrash(unittest.TestCase):
def setUp(self):
self.file = NamedTemporaryFile(
dir=op.expanduser("~"), prefix="send2trash_test", delete=False dir=op.expanduser("~"), prefix="send2trash_test", delete=False
) )
file.close()
def test_trash(self): assert op.exists(file.name) is True
s2t(self.file.name) yield file
self.assertFalse(op.exists(self.file.name)) # Cleanup trash files on supported platforms
if sys.platform != "win32":
def tearDown(self): name = op.basename(file.name)
name = op.basename(self.file.name) # Remove trash files if they exist
if op.exists(op.join(HOMETRASH, "files", name)):
os.remove(op.join(HOMETRASH, "files", name)) os.remove(op.join(HOMETRASH, "files", name))
os.remove(op.join(HOMETRASH, "info", name + ".trashinfo")) os.remove(op.join(HOMETRASH, "info", name + ".trashinfo"))
if op.exists(file.name):
os.remove(file.name)
class TestHomeMultiTrash(unittest.TestCase): @pytest.fixture
def setUp(self): def testfiles():
self.files = list( files = list(
map( map(
lambda index: NamedTemporaryFile( lambda index: NamedTemporaryFile(
dir=op.expanduser("~"), dir=op.expanduser("~"),
@ -55,14 +57,10 @@ class TestHomeMultiTrash(unittest.TestCase):
range(10), range(10),
) )
) )
[file.close() for file in files]
def test_multitrash(self): assert all([op.exists(file.name) for file in files]) is True
filenames = [file.name for file in self.files] yield files
s2t(filenames) filenames = [op.basename(file.name) for file in files]
self.assertFalse(any([op.exists(filename) for filename in filenames]))
def tearDown(self):
filenames = [op.basename(file.name) for file in self.files]
[os.remove(op.join(HOMETRASH, "files", filename)) for filename in filenames] [os.remove(op.join(HOMETRASH, "files", filename)) for filename in filenames]
[ [
os.remove(op.join(HOMETRASH, "info", filename + ".trashinfo")) os.remove(op.join(HOMETRASH, "info", filename + ".trashinfo"))
@ -70,57 +68,64 @@ class TestHomeMultiTrash(unittest.TestCase):
] ]
def test_trash(testfile):
s2t(testfile.name)
assert op.exists(testfile.name) is False
def test_multitrash(testfiles):
filenames = [file.name for file in testfiles]
s2t(filenames)
assert any([op.exists(filename) for filename in filenames]) is False
def touch(path):
with open(path, "a"):
os.utime(path, None)
def _filesys_enc(): def _filesys_enc():
enc = sys.getfilesystemencoding() enc = sys.getfilesystemencoding()
# Get canonical name of codec # Get canonical name of codec
return codecs.lookup(enc).name return codecs.lookup(enc).name
@unittest.skipIf(_filesys_enc() == "ascii", "ASCII filesystem") @pytest.fixture
class TestUnicodeTrash(unittest.TestCase): def testUnicodefile():
def setUp(self): name = u"send2trash_tést1"
self.name = u"send2trash_tést1" file = op.join(op.expanduser(b"~"), name.encode("utf-8"))
self.file = op.join(op.expanduser(b"~"), self.name.encode("utf-8")) touch(file)
touch(self.file) assert op.exists(file) is True
yield file
def test_trash_bytes(self): # Cleanup trash files on supported platforms
s2t(self.file) if sys.platform != "win32":
assert not op.exists(self.file) # Remove trash files if they exist
if op.exists(op.join(HOMETRASH, "files", name)):
def test_trash_unicode(self): os.remove(op.join(HOMETRASH, "files", name))
s2t(self.file.decode(sys.getfilesystemencoding())) os.remove(op.join(HOMETRASH, "info", name + ".trashinfo"))
assert not op.exists(self.file) if op.exists(file):
os.remove(file)
def tearDown(self):
if op.exists(self.file):
os.remove(self.file)
trash_file = op.join(HOMETRASH, "files", self.name)
if op.exists(trash_file):
os.remove(trash_file)
os.remove(op.join(HOMETRASH, "info", self.name + ".trashinfo"))
# @pytest.mark.skipif(_filesys_enc() == "ascii", reason="Requires Unicode filesystem")
# Tests for files on some other volume than the user's home directory. def test_trash_bytes(testUnicodefile):
# s2t(testUnicodefile)
# What we need to stub: assert not op.exists(testUnicodefile)
# * plat_other.get_dev (to make sure the file will not be on the home dir dev)
# * os.path.ismount (to make our topdir look like a top dir)
# @pytest.mark.skipif(_filesys_enc() == "ascii", reason="Requires Unicode filesystem")
class TestExtVol(unittest.TestCase): def test_trash_unicode(testUnicodefile):
def setUp(self): s2t(testUnicodefile.decode(sys.getfilesystemencoding()))
self.trashTopdir = mkdtemp(prefix="s2t") assert not op.exists(testUnicodefile)
class ExtVol:
def __init__(self, path):
self.trashTopdir = path
if PY3: if PY3:
trashTopdir_b = os.fsencode(self.trashTopdir) self.trashTopdir_b = os.fsencode(self.trashTopdir)
else: else:
trashTopdir_b = self.trashTopdir self.trashTopdir_b = self.trashTopdir
self.fileName = "test.txt"
self.filePath = op.join(self.trashTopdir, self.fileName)
touch(self.filePath)
self.old_ismount = old_ismount = op.ismount
self.old_getdev = send2trash.plat_other.get_dev
def s_getdev(path): def s_getdev(path):
from send2trash.plat_other import is_parent from send2trash.plat_other import is_parent
@ -133,120 +138,98 @@ class TestExtVol(unittest.TestCase):
def s_ismount(path): def s_ismount(path):
if op.realpath(path) in ( if op.realpath(path) in (
op.realpath(self.trashTopdir), op.realpath(self.trashTopdir),
op.realpath(trashTopdir_b), op.realpath(self.trashTopdir_b),
): ):
return True return True
return old_ismount(path) return old_ismount(path)
self.old_ismount = old_ismount = op.ismount
self.old_getdev = send2trash.plat_other.get_dev
send2trash.plat_other.os.path.ismount = s_ismount send2trash.plat_other.os.path.ismount = s_ismount
send2trash.plat_other.get_dev = s_getdev send2trash.plat_other.get_dev = s_getdev
def tearDown(self): def cleanup(self):
send2trash.plat_other.get_dev = self.old_getdev send2trash.plat_other.get_dev = self.old_getdev
send2trash.plat_other.os.path.ismount = self.old_ismount send2trash.plat_other.os.path.ismount = self.old_ismount
shutil.rmtree(self.trashTopdir) shutil.rmtree(self.trashTopdir)
class TestTopdirTrash(TestExtVol): @pytest.fixture
def setUp(self): def testExtVol():
TestExtVol.setUp(self) trashTopdir = mkdtemp(prefix="s2t")
# Create a .Trash dir w/ a sticky bit volume = ExtVol(trashTopdir)
self.trashDir = op.join(self.trashTopdir, ".Trash") fileName = "test.txt"
os.mkdir(self.trashDir, 0o777 | stat.S_ISVTX) filePath = op.join(volume.trashTopdir, fileName)
touch(filePath)
assert op.exists(filePath) is True
yield volume, fileName, filePath
volume.cleanup()
def test_trash(self):
s2t(self.filePath) def test_trash_topdir(testExtVol):
self.assertFalse(op.exists(self.filePath)) trashDir = op.join(testExtVol[0].trashTopdir, ".Trash")
self.assertTrue( os.mkdir(trashDir, 0o777 | stat.S_ISVTX)
op.exists(op.join(self.trashDir, str(os.getuid()), "files", self.fileName))
s2t(testExtVol[2])
assert op.exists(testExtVol[2]) is False
assert (
op.exists(op.join(trashDir, str(os.getuid()), "files", testExtVol[1])) is True
) )
self.assertTrue( assert (
op.exists( op.exists(
op.join( op.join(trashDir, str(os.getuid()), "info", testExtVol[1] + ".trashinfo",)
self.trashDir,
str(os.getuid()),
"info",
self.fileName + ".trashinfo",
)
) )
is True
) )
# info relative path (if another test is added, with the same fileName/Path, # info relative path (if another test is added, with the same fileName/Path,
# then it gets renamed etc.) # then it gets renamed etc.)
cfg = ConfigParser() cfg = ConfigParser()
cfg.read( cfg.read(op.join(trashDir, str(os.getuid()), "info", testExtVol[1] + ".trashinfo"))
op.join( assert (testExtVol[1] == cfg.get("Trash Info", "Path", raw=True)) is True
self.trashDir, str(os.getuid()), "info", self.fileName + ".trashinfo"
)
)
self.assertEqual(self.fileName, cfg.get("Trash Info", "Path", raw=True))
# Test .Trash-UID def test_trash_topdir_fallback(testExtVol):
class TestTopdirTrashFallback(TestExtVol): s2t(testExtVol[2])
def test_trash(self): assert op.exists(testExtVol[2]) is False
touch(self.filePath) assert (
s2t(self.filePath)
self.assertFalse(op.exists(self.filePath))
self.assertTrue(
op.exists( op.exists(
op.join( op.join(
self.trashTopdir, testExtVol[0].trashTopdir,
".Trash-" + str(os.getuid()), ".Trash-" + str(os.getuid()),
"files", "files",
self.fileName, testExtVol[1],
) )
) )
is True
) )
# Test failure def test_trash_topdir_failure(testExtVol):
class TestTopdirFailure(TestExtVol): os.chmod(testExtVol[0].trashTopdir, 0o500) # not writable to induce the exception
def setUp(self): pytest.raises(TrashPermissionError, s2t, [testExtVol[2]])
TestExtVol.setUp(self) os.chmod(testExtVol[0].trashTopdir, 0o700) # writable to allow deletion
os.chmod(self.trashTopdir, 0o500) # not writable to induce the exception
def test_trash(self):
with self.assertRaises(OSError):
s2t(self.filePath)
self.assertTrue(op.exists(self.filePath))
def tearDown(self):
os.chmod(self.trashTopdir, 0o700) # writable to allow deletion
TestExtVol.tearDown(self)
# Make sure it will find the mount point properly for a file in a symlinked path def test_trash_symlink(testExtVol):
class TestSymlink(TestExtVol):
def setUp(self):
TestExtVol.setUp(self)
# Use mktemp (race conditioney but no symlink equivalent) # Use mktemp (race conditioney but no symlink equivalent)
# Since is_parent uses realpath(), and our getdev uses is_parent, # Since is_parent uses realpath(), and our getdev uses is_parent,
# this should work # this should work
self.slDir = mktemp(prefix="s2t", dir=op.expanduser("~")) slDir = mktemp(prefix="s2t", dir=op.expanduser("~"))
os.mkdir(op.join(testExtVol[0].trashTopdir, "subdir"), 0o700)
os.mkdir(op.join(self.trashTopdir, "subdir"), 0o700) filePath = op.join(testExtVol[0].trashTopdir, "subdir", testExtVol[1])
self.filePath = op.join(self.trashTopdir, "subdir", self.fileName) touch(filePath)
touch(self.filePath) os.symlink(op.join(testExtVol[0].trashTopdir, "subdir"), slDir)
os.symlink(op.join(self.trashTopdir, "subdir"), self.slDir) s2t(op.join(slDir, testExtVol[1]))
assert op.exists(filePath) is False
def test_trash(self): assert (
s2t(op.join(self.slDir, self.fileName))
self.assertFalse(op.exists(self.filePath))
self.assertTrue(
op.exists( op.exists(
op.join( op.join(
self.trashTopdir, testExtVol[0].trashTopdir,
".Trash-" + str(os.getuid()), ".Trash-" + str(os.getuid()),
"files", "files",
self.fileName, testExtVol[1],
) )
) )
is True
) )
os.remove(slDir)
def tearDown(self):
os.remove(self.slDir)
TestExtVol.tearDown(self)
if __name__ == "__main__":
unittest.main()

View File

@ -1,10 +1,9 @@
# coding: utf-8 # encoding: utf-8
import os import os
import shutil import shutil
import sys import sys
import unittest import pytest
from os import path as op from os import path as op
from tempfile import gettempdir
from send2trash import send2trash as s2t from send2trash import send2trash as s2t
@ -12,144 +11,193 @@ from send2trash import send2trash as s2t
if sys.platform == "win32": if sys.platform == "win32":
from send2trash.plat_win_modern import send2trash as s2t_modern from send2trash.plat_win_modern import send2trash as s2t_modern
from send2trash.plat_win_legacy import send2trash as s2t_legacy from send2trash.plat_win_legacy import send2trash as s2t_legacy
else:
pytest.skip("Skipping windows-only tests", allow_module_level=True)
@unittest.skipIf(sys.platform != "win32", "Windows only") def _create_tree(path):
class TestNormal(unittest.TestCase):
def setUp(self):
self.dirname = "\\\\?\\" + op.join(gettempdir(), "python.send2trash")
self.file = op.join(self.dirname, "testfile.txt")
self._create_tree(self.file)
self.files = [
op.join(self.dirname, "testfile{}.txt".format(index)) for index in range(10)
]
[self._create_tree(file) for file in self.files]
def tearDown(self):
shutil.rmtree(self.dirname, ignore_errors=True)
def _create_tree(self, path):
dirname = op.dirname(path) dirname = op.dirname(path)
if not op.isdir(dirname): if not op.isdir(dirname):
os.makedirs(dirname) os.makedirs(dirname)
with open(path, "w") as writer: with open(path, "w") as writer:
writer.write("send2trash test") writer.write("send2trash test")
def _trash_file(self, fcn):
fcn(self.file)
self.assertFalse(op.exists(self.file))
def _trash_multifile(self, fcn): @pytest.fixture
fcn(self.files) def testdir(tmp_path):
self.assertFalse(any([op.exists(file) for file in self.files])) dirname = "\\\\?\\" + str(tmp_path)
assert op.exists(dirname) is True
def _file_not_found(self, fcn): yield dirname
file = op.join(self.dirname, "otherfile.txt") shutil.rmtree(dirname, ignore_errors=True)
self.assertRaises(WindowsError, fcn, file)
def test_trash_file(self):
self._trash_file(s2t)
def test_trash_multifile(self):
self._trash_multifile(s2t)
def test_file_not_found(self):
self._file_not_found(s2t)
def test_trash_file_modern(self):
self._trash_file(s2t_modern)
def test_trash_multifile_modern(self):
self._trash_multifile(s2t_modern)
def test_file_not_found_modern(self):
self._file_not_found(s2t_modern)
def test_trash_file_legacy(self):
self._trash_file(s2t_legacy)
def test_trash_multifile_legacy(self):
self._trash_multifile(s2t_legacy)
def test_file_not_found_legacy(self):
self._file_not_found(s2t_legacy)
@unittest.skipIf(sys.platform != "win32", "Windows only") @pytest.fixture
class TestLongPath(unittest.TestCase): def testfile(testdir):
def setUp(self): file = op.join(testdir, "testfile.txt")
self.functions = {s2t: "auto", s2t_legacy: "legacy", s2t_modern: "modern"} _create_tree(file)
filename = "A" * 100 assert op.exists(file) is True
self.dirname = "\\\\?\\" + op.join(gettempdir(), filename) yield file
path = op.join( # Note dir will cleanup the file
self.dirname,
filename,
filename, # From there, the path is not trashable from Explorer
filename,
filename + "{}.txt",
)
self.file = path.format("")
self._create_tree(self.file)
self.files = [path.format(index) for index in range(10)]
[self._create_tree(file) for file in self.files]
def tearDown(self):
shutil.rmtree(self.dirname, ignore_errors=True)
def _create_tree(self, path): @pytest.fixture
dirname = op.dirname(path) def testfiles(testdir):
if not op.isdir(dirname): files = [op.join(testdir, "testfile{}.txt".format(index)) for index in range(10)]
os.makedirs(dirname) [_create_tree(file) for file in files]
with open(path, "w") as writer: assert all([op.exists(file) for file in files]) is True
writer.write("Looong filename!") yield files
# Note dir will cleanup the files
def _trash_file(self, fcn):
fcn(self.file)
self.assertFalse(op.exists(self.file))
def _trash_multifile(self, fcn): def _trash_folder(dir, fcn):
fcn(self.files) fcn(dir)
self.assertFalse(any([op.exists(file) for file in self.files])) assert op.exists(dir) is False
def _trash_folder(self, fcn):
fcn(self.dirname)
self.assertFalse(op.exists(self.dirname))
def test_trash_file(self): def _trash_file(file, fcn):
self._trash_file(s2t) fcn(file)
assert op.exists(file) is False
def test_trash_multifile(self):
self._trash_multifile(s2t)
@unittest.skipIf( def _trash_multifile(files, fcn):
op.splitdrive(os.getcwd())[0] != op.splitdrive(gettempdir())[0], fcn(files)
"Cannot trash long path from other drive", assert any([op.exists(file) for file in files]) is False
)
def test_trash_folder(self):
self._trash_folder(s2t)
def test_trash_file_modern(self):
self._trash_file(s2t_modern)
def test_trash_multifile_modern(self): def _file_not_found(dir, fcn):
self._trash_multifile(s2t_modern) file = op.join(dir, "otherfile.txt")
pytest.raises(OSError, fcn, file)
@unittest.skipIf(
op.splitdrive(os.getcwd())[0] != op.splitdrive(gettempdir())[0],
"Cannot trash long path from other drive",
)
def test_trash_folder_modern(self):
self._trash_folder(s2t_modern)
def test_trash_file_legacy(self): def _multi_byte_unicode(dir, fcn):
self._trash_file(s2t_legacy) single_file = op.join(dir, "😇.txt")
_create_tree(single_file)
assert op.exists(single_file) is True
fcn(single_file)
assert op.exists(single_file) is False
files = [op.join(dir, "😇{}.txt".format(index)) for index in range(10)]
[_create_tree(file) for file in files]
assert all([op.exists(file) for file in files]) is True
fcn(files)
assert any([op.exists(file) for file in files]) is False
def test_trash_multifile_legacy(self):
self._trash_multifile(s2t_legacy)
@unittest.skipIf( def test_trash_folder(testdir):
op.splitdrive(os.getcwd())[0] != op.splitdrive(gettempdir())[0], _trash_folder(testdir, s2t)
"Cannot trash long path from other drive",
)
def test_trash_folder_legacy(self): def test_trash_file(testfile):
self._trash_folder(s2t_legacy) _trash_file(testfile, s2t)
def test_trash_multifile(testfiles):
_trash_multifile(testfiles, s2t)
def test_file_not_found(testdir):
_file_not_found(testdir, s2t)
def test_trash_folder_modern(testdir):
_trash_folder(testdir, s2t_modern)
def test_trash_file_modern(testfile):
_trash_file(testfile, s2t_modern)
def test_trash_multifile_modern(testfiles):
_trash_multifile(testfiles, s2t_modern)
def test_file_not_found_modern(testdir):
_file_not_found(testdir, s2t_modern)
def test_multi_byte_unicode_modern(testdir):
_multi_byte_unicode(testdir, s2t_modern)
def test_trash_folder_legacy(testdir):
_trash_folder(testdir, s2t_legacy)
def test_trash_file_legacy(testfile):
_trash_file(testfile, s2t_legacy)
def test_trash_multifile_legacy(testfiles):
_trash_multifile(testfiles, s2t_legacy)
def test_file_not_found_legacy(testdir):
_file_not_found(testdir, s2t_legacy)
def test_multi_byte_unicode_legacy(testdir):
_multi_byte_unicode(testdir, s2t_legacy)
# Long path tests
@pytest.fixture
def longdir(tmp_path):
dirname = "\\\\?\\" + str(tmp_path)
name = "A" * 100
yield op.join(dirname, name, name, name)
shutil.rmtree(dirname, ignore_errors=True)
@pytest.fixture
def longfile(longdir):
name = "A" * 100
path = op.join(longdir, name + "{}.txt")
file = path.format("")
_create_tree(file)
assert op.exists(file) is True
yield file
@pytest.fixture
def longfiles(longdir):
name = "A" * 100
path = op.join(longdir, name + "{}.txt")
files = [path.format(index) for index in range(10)]
[_create_tree(file) for file in files]
assert all([op.exists(file) for file in files]) is True
yield files
# NOTE: both legacy and modern test "pass" on windows, however sometimes with the same path
# they do not actually recycle files but delete them. Noticed this when testing with the
# recycle bin open, noticed later tests actually worked, modern version can actually detect
# when this happens but not stop it at this moment, and we need a way to verify it when testing.
def test_trash_long_file_modern(longfile):
_trash_file(longfile, s2t_modern)
def test_trash_long_multifile_modern(longfiles):
_trash_multifile(longfiles, s2t_modern)
# @pytest.skipif(
# op.splitdrive(os.getcwd())[0] != op.splitdrive(gettempdir())[0],
# "Cannot trash long path from other drive",
# )
# def test_trash_long_folder_modern(self):
# self._trash_folder(s2t_modern)
def test_trash_long_file_legacy(longfile):
_trash_file(longfile, s2t_legacy)
def test_trash_long_multifile_legacy(longfiles):
_trash_multifile(longfiles, s2t_legacy)
# @pytest.skipif(
# op.splitdrive(os.getcwd())[0] != op.splitdrive(gettempdir())[0],
# "Cannot trash long path from other drive",
# )
# def test_trash_long_folder_legacy(self):
# self._trash_folder(s2t_legacy)

View File

@ -1,32 +1,43 @@
# encoding: utf-8 # encoding: utf-8
import os import os
import unittest import sys
import pytest
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from os import path as op from os import path as op
from send2trash.__main__ import main as trash_main from send2trash.__main__ import main as trash_main
from tests.test_plat_other import HOMETRASH
# Only import HOMETRASH on supported platforms
if sys.platform != "win32":
from send2trash.plat_other import HOMETRASH
class TestMainTrash(unittest.TestCase): @pytest.fixture
def setUp(self): def file():
self.file = NamedTemporaryFile(dir=op.expanduser('~'), prefix='send2trash_test', delete=False) file = NamedTemporaryFile(
dir=op.expanduser("~"), prefix="send2trash_test", delete=False
def test_trash(self): )
trash_main(['-v', self.file.name]) file.close()
self.assertFalse(op.exists(self.file.name)) # Verify file was actually created
assert op.exists(file.name) is True
def test_no_args(self): yield file.name
self.assertRaises(SystemExit, trash_main, []) # Cleanup trash files on supported platforms
self.assertRaises(SystemExit, trash_main, ['-v']) if sys.platform != "win32":
self.assertTrue(op.exists(self.file.name)) name = op.basename(file.name)
trash_main([self.file.name]) # Trash the file so tearDown runs properly # Remove trash files if they exist
if op.exists(op.join(HOMETRASH, "files", name)):
def tearDown(self): os.remove(op.join(HOMETRASH, "files", name))
name = op.basename(self.file.name) os.remove(op.join(HOMETRASH, "info", name + ".trashinfo"))
os.remove(op.join(HOMETRASH, 'files', name)) if op.exists(file.name):
os.remove(op.join(HOMETRASH, 'info', name + '.trashinfo')) os.remove(file.name)
if __name__ == '__main__': def test_trash(file):
unittest.main() trash_main(["-v", file])
assert op.exists(file) is False
def test_no_args(file):
pytest.raises(SystemExit, trash_main, [])
pytest.raises(SystemExit, trash_main, ["-v"])
assert op.exists(file) is True

17
tox.ini
View File

@ -1,21 +1,20 @@
[tox] [tox]
envlist = py{27,34,35,36,37,38,39,310,3-win} envlist = py{27,34,35,36,37,38,39,310}
skip_missing_interpreters = True skip_missing_interpreters = True
[testenv] [testenv]
platform = linux deps =
flake8
pytest
pywin32; sys_platform == 'win32'
commands = commands =
python setup.py test --test-suite tests.TestSuite flake8
pytest
[testenv:py3-win]
platform = win
commands =
python -m pip install pywin32
python setup.py test --test-suite tests.TestSuite
[testenv:py27] [testenv:py27]
deps = deps =
configparser configparser
{[testenv]deps}
[flake8] [flake8]
exclude = .tox,env,build exclude = .tox,env,build