Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Now trash-list checks for sticky bit and symlinks.

  • Loading branch information...
commit c9aa7cfac61c3018acf745a8e05dcbcfa0909e0e 1 parent cd6b9ef
@andreafrancia authored
View
5 DONE.txt
@@ -0,0 +1,5 @@
+Features:
+ x trash-list should check that the $topdir/.Trash is a sticky dir
+ x trash-list should report when $topdir/.Trash is not sticky
+ x trash-list should check that the $topdir/.Trash is not a symbolic link
+ x and should warn when it is
View
5 TODO.txt
@@ -1,7 +1,6 @@
Features backlog:
- - trash-list should check that the $topdir/.Trash is not a symbolic link
- - trash-list should handle exception when a .trashinfo from home trashdir
- contains a path that is not absolute
+ - trash-list should handle .trashinfo from home trashdir that contains
+ relative path
- trash-list should print also the orphan files
- support alias rmdir=trash-put
- don't use world writable trash
View
4 integration_tests/assert_equals_with_unidiff.py
@@ -6,7 +6,9 @@ def unidiff(expected, actual):
expected=expected.splitlines(1)
actual=actual.splitlines(1)
- diff=difflib.unified_diff(expected, actual)
+ diff=difflib.unified_diff(expected, actual,
+ fromfile='Expected', tofile='Actual',
+ lineterm='\n', n=10)
return ''.join(diff)
from nose.tools import assert_equals
View
81 integration_tests/test_trash_list.py → integration_tests/describe_trash_list.py
@@ -1,5 +1,6 @@
# Copyright (C) 2011 Andrea Francia Trivolzio(PV) Italy
+import os
from trashcli.trash import ListCmd
from files import (write_file, require_empty_dir, make_sticky_dir,
ensure_non_sticky_dir, make_unsticky_dir,
@@ -23,9 +24,15 @@ def setUp(self):
self.home_trashcan = FakeTrashDir('XDG_DATA_HOME/Trash')
self.add_trashinfo = self.home_trashcan.add_trashinfo
+ def when_dir_is_sticky(self, path):
+ make_sticky_dir(path)
+ def when_dir_exists_unsticky(self, path):
+ make_unsticky_dir(path)
+
@istest
class describe_trash_list(Setup):
+
@istest
def should_output_the_help_message(self):
@@ -118,7 +125,7 @@ def should_warn_about_unexistent_path_entry(self):
self.user.should_read_output('')
@istest
-class describe_TrashList_WhenAFileIsTrashedInTopTrashDir(Setup):
+class with_a_top_trash_dir(Setup):
def setUp(self):
super(type(self),self).setUp()
self.top_trashdir1 = FakeTrashDir('topdir/.Trash/123')
@@ -126,26 +133,36 @@ def setUp(self):
self.user.add_volume('topdir')
@istest
- def should_listed_when_the_dir_is_sticky(self):
- make_sticky_dir('topdir/.Trash')
- self.top_trashdir1.add_trashinfo('file1', '2000-01-01T00:00:00')
+ def should_list_its_contents_if_parent_is_sticky(self):
+ self.when_dir_is_sticky('topdir/.Trash')
+ self.and_contains_a_valid_trashinfo()
self.user.run_trash_list()
self.user.should_read_output("2000-01-01 00:00:00 topdir/file1\n")
-
+
@istest
- def should_report_errors_when_the_dir_is_not_sticky(self):
- make_unsticky_dir('topdir/.Trash')
+ def and_should_warn_if_parent_is_not_sticky(self):
+ self.when_dir_exists_unsticky('topdir/.Trash')
+ self.and_dir_exists('topdir/.Trash/123')
self.user.run_trash_list()
self.user.should_read_error("TrashDir skipped because parent not sticky: topdir/.Trash/123\n")
@istest
- def should_ignored_when_the_dir_is_not_sticky(self):
- self.top_trashdir1.add_trashinfo('file1', '2000-01-01T00:00:00')
- ensure_non_sticky_dir('topdir/.Trash')
+ def but_it_should_not_warn_when_the_parent_is_unsticky_but_there_is_no_trashdir(self):
+ self.when_dir_exists_unsticky('topdir/.Trash')
+ self.but_does_not_exists_any('topdir/.Trash/123')
+
+ self.user.run_trash_list()
+
+ self.user.should_read_error("")
+
+ @istest
+ def should_ignore_trash_from_a_unsticky_topdir(self):
+ self.when_dir_exists_unsticky('topdir/.Trash')
+ self.and_contains_a_valid_trashinfo()
self.user.run_trash_list()
@@ -153,21 +170,33 @@ def should_ignored_when_the_dir_is_not_sticky(self):
@istest
def it_should_ignore_Trash_is_a_symlink(self):
- make_a_symlink_to_a_dir('topdir/.Trash')
- self.top_trashdir1.add_trashinfo('file1', '2000-01-01T00:00:00')
+ self.when_is_a_symlink_to_a_dir('topdir/.Trash')
+ self.and_contains_a_valid_trashinfo()
self.user.run_trash_list()
self.user.should_read_output('')
@istest
- def it_should_notify_when_Trash_is_a_symlink(self):
- make_a_symlink_to_a_dir('topdir/.Trash')
+ def and_should_warn_about_it(self):
+ self.when_is_a_symlink_to_a_dir('topdir/.Trash')
+ self.and_contains_a_valid_trashinfo()
self.user.run_trash_list()
- with assert_raises(AssertionError):
- self.user.should_read_error('Skipping topdir/.Trash: is a symlink ')
+ self.user.should_read_error('TrashDir skipped because parent not sticky: topdir/.Trash/123\n')
+ def but_does_not_exists_any(self, path):
+ assert not os.path.exists(path)
+ def and_dir_exists(self, path):
+ os.mkdir(path)
+ assert os.path.isdir(path)
+ def and_contains_a_valid_trashinfo(self):
+ self.top_trashdir1.add_trashinfo('file1', '2000-01-01T00:00:00')
+ def when_is_a_symlink_to_a_dir(self, path):
+ dest = "%s-dest" % path
+ os.mkdir(dest)
+ rel_dest = os.path.basename(dest)
+ os.symlink(rel_dest, path)
@istest
class describe_when_a_file_is_in_alternate_top_trashdir(Setup):
@@ -184,13 +213,6 @@ def should_list_contents_of_alternate_trashdir(self):
from nose.tools import assert_raises
-def make_a_symlink_to_a_dir(path):
- import os
- dest = "%s-dest" % path
- os.mkdir(dest)
- rel_dest = os.path.basename(dest)
- os.symlink(rel_dest, path)
-
@istest
class describe_trash_list_with_raw_option:
def setup(self):
@@ -255,12 +277,15 @@ def __init__(self, environ={}):
def run_trash_list(self):
self.run('trash-list')
def run(self,*argv):
+ from trashcli.trash import FileSystemReader
+ file_reader = FileSystemReader()
+ file_reader.list_volumes = lambda: self.volumes
ListCmd(
- out = self.stdout,
- err = self.stderr,
- environ = self.environ,
- getuid = self.fake_getuid,
- list_volumes = lambda: self.volumes
+ out = self.stdout,
+ err = self.stderr,
+ environ = self.environ,
+ getuid = self.fake_getuid,
+ file_reader = file_reader,
).run(*argv)
def set_fake_uid(self, uid):
self.fake_getuid = lambda: uid
View
14 integration_tests/files.py
@@ -39,12 +39,24 @@ def make_sticky_dir(path):
def make_unsticky_dir(path):
os.mkdir(path)
- ensure_non_sticky_dir(path)
+ unset_sticky_bit(path)
+
+def make_dir_unsticky(path):
+ assert_is_dir(path)
+ unset_sticky_bit(path)
+
+def assert_is_dir(path):
+ assert os.path.isdir(path)
def set_sticky_bit(path):
import stat
os.chmod(path, os.stat(path).st_mode | stat.S_ISVTX)
+def unset_sticky_bit(path):
+ import stat
+ os.chmod(path, os.stat(path).st_mode & ~ stat.S_ISVTX)
+ assert not has_sticky_bit(path)
+
def touch(path):
open(path,'a+').close()
View
3  integration_tests/test_filesystem.py
@@ -1,6 +1,6 @@
# Copyright (C) 2008-2011 Andrea Francia Trivolzio(PV) Italy
-from trashcli.trash import has_sticky_bit, mkdirs, is_sticky_dir
+from trashcli.trash import has_sticky_bit, mkdirs, FileSystemReader
from .files import require_empty_dir, having_file, set_sticky_bit
import os
@@ -29,6 +29,7 @@ def test_has_sticky_bit_returns_false(self):
def setUp(self):
require_empty_dir('sandbox')
+is_sticky_dir=FileSystemReader().is_sticky_dir
class Test_is_sticky_dir:
def test_dir_non_sticky(self):
View
168 integration_tests/test_trash_empty.py
@@ -7,23 +7,40 @@
import os
from files import write_file, require_empty_dir, make_dirs, set_sticky_bit
from files import having_file
+from mock import MagicMock
@istest
class describe_trash_empty:
+
+ def setUp(self):
+ require_empty_dir('XDG_DATA_HOME')
+ self.info_dir_path = 'XDG_DATA_HOME/Trash/info'
+ self.files_dir_path = 'XDG_DATA_HOME/Trash/files'
+ self.environ = {'XDG_DATA_HOME':'XDG_DATA_HOME'}
+ now = MagicMock(side_effect=RuntimeError)
+ self.empty_cmd = EmptyCmd(
+ out = StringIO(),
+ err = StringIO(),
+ environ = self.environ,
+ now = now
+ )
+
+ def user_run_trash_empty(self):
+ self.empty_cmd.run('trash-empty')
+
@istest
def it_should_remove_an_info_file(self):
self.having_a_trashinfo_in_trashcan('foo.trashinfo')
- self.run_trash_empty()
+ self.user_run_trash_empty()
self.assert_dir_empty(self.info_dir_path)
@istest
def it_should_remove_all_the_infofiles(self):
-
self.having_three_trashinfo_in_trashcan()
- self.run_trash_empty()
+ self.user_run_trash_empty()
self.assert_dir_empty(self.info_dir_path)
@@ -31,7 +48,7 @@ def it_should_remove_all_the_infofiles(self):
def it_should_remove_the_backup_files(self):
self.having_one_trashed_file()
- self.run_trash_empty()
+ self.user_run_trash_empty()
self.assert_dir_empty(self.files_dir_path)
@@ -39,7 +56,7 @@ def it_should_remove_the_backup_files(self):
def it_should_keep_unknown_files_found_in_infodir(self):
self.having_file_in_info_dir('not-a-trashinfo')
- self.run_trash_empty()
+ self.user_run_trash_empty()
self.assert_dir_contains(self.info_dir_path, 'not-a-trashinfo')
@@ -47,7 +64,7 @@ def it_should_keep_unknown_files_found_in_infodir(self):
def but_it_should_remove_orphan_files_from_the_files_dir(self):
self.having_orphan_file_in_files_dir()
- self.run_trash_empty()
+ self.user_run_trash_empty()
self.assert_dir_empty(self.files_dir_path)
@@ -55,19 +72,17 @@ def but_it_should_remove_orphan_files_from_the_files_dir(self):
def it_should_purge_also_directories(self):
os.makedirs("XDG_DATA_HOME/Trash/files/a-dir")
- self.run_trash_empty()
+ self.user_run_trash_empty()
- def setUp(self):
- require_empty_dir('XDG_DATA_HOME')
- self.info_dir_path = 'XDG_DATA_HOME/Trash/info'
- self.files_dir_path = 'XDG_DATA_HOME/Trash/files'
- self.run_trash_empty = TrashEmptyRunner('XDG_DATA_HOME')
def assert_dir_empty(self, path):
assert len(os.listdir(path)) == 0
+
def assert_dir_contains(self, path, filename):
assert os.path.exists(os.path.join(path, filename))
+
def having_a_trashinfo_in_trashcan(self, basename_of_trashinfo):
having_file(os.path.join(self.info_dir_path, basename_of_trashinfo))
+
def having_three_trashinfo_in_trashcan(self):
self.having_a_trashinfo_in_trashcan('foo.trashinfo')
self.having_a_trashinfo_in_trashcan('bar.trashinfo')
@@ -75,14 +90,18 @@ def having_three_trashinfo_in_trashcan(self):
assert_items_equal(['foo.trashinfo',
'bar.trashinfo',
'baz.trashinfo'], os.listdir(self.info_dir_path))
+
def having_one_trashed_file(self):
self.having_a_trashinfo_in_trashcan('foo.trashinfo')
having_file(self.files_dir_path +'/foo')
self.files_dir_should_not_be_empty()
+
def files_dir_should_not_be_empty(self):
assert len(os.listdir(self.files_dir_path)) != 0
+
def having_file_in_info_dir(self, filename):
having_file(os.path.join(self.info_dir_path, filename))
+
def having_orphan_file_in_files_dir(self):
complete_path = os.path.join(self.files_dir_path,
'a-file-without-any-associated-trashinfo')
@@ -91,42 +110,55 @@ def having_orphan_file_in_files_dir(self):
@istest
class describe_trash_empty_invoked_with_N_days_as_argument:
+ def setUp(self):
+ require_empty_dir('XDG_DATA_HOME')
+ self.xdg_data_home = 'XDG_DATA_HOME'
+ self.environ = {'XDG_DATA_HOME':'XDG_DATA_HOME'}
+ self.now = MagicMock(side_effect=RuntimeError)
+ self.empty_cmd=EmptyCmd(
+ out = StringIO(),
+ err = StringIO(),
+ environ = self.environ,
+ now = self.now
+ )
+
+ def user_run_trash_empty(self, *args):
+ self.empty_cmd.run('trash-empty', *args)
+
+ def set_clock_at(self, yyyy_mm_dd):
+ self.now.side_effect = lambda:date(yyyy_mm_dd)
+
+ def date(yyyy_mm_dd):
+ from datetime import datetime
+ return datetime.strptime(yyyy_mm_dd, '%Y-%m-%d')
@istest
def it_should_keep_files_newer_than_N_days(self):
-
self.having_a_trashed_file('foo', '2000-01-01')
- self.having_now_is('2000-01-01')
+ self.set_clock_at('2000-01-01')
- self.run_trash_empty('2')
+ self.user_run_trash_empty('2')
self.file_should_have_been_kept_in_trashcan('foo')
@istest
def it_should_remove_files_older_than_N_days(self):
-
self.having_a_trashed_file('foo', '1999-01-01')
- self.having_now_is('2000-01-01')
+ self.set_clock_at('2000-01-01')
- self.run_trash_empty('2')
+ self.user_run_trash_empty('2')
self.file_should_have_been_removed_from_trashcan('foo')
@istest
def it_should_kept_files_with_invalid_deletion_date(self):
self.having_a_trashed_file('foo', 'Invalid Date')
- self.having_now_is('2000-01-01')
+ self.set_clock_at('2000-01-01')
- self.run_trash_empty('2')
+ self.user_run_trash_empty('2')
self.file_should_have_been_kept_in_trashcan('foo')
- def setUp(self):
- self.xdg_data_home = 'XDG_DATA_HOME'
- self.run_trash_empty = TrashEmptyRunner(self.xdg_data_home)
- self.having_now_is = self.run_trash_empty.set_now
- require_empty_dir(self.xdg_data_home)
-
def having_a_trashed_file(self, name, date):
contents = "DeletionDate=%sT00:00:00\n" % date
write_file(self.trashinfo(name), contents)
@@ -141,49 +173,35 @@ def file_should_have_been_kept_in_trashcan(self, trashinfo_name):
def file_should_have_been_removed_from_trashcan(self, trashinfo_name):
assert not os.path.exists(self.trashinfo(trashinfo_name))
-class TrashEmptyRunner:
- def __init__(self, XDG_DATA_HOME):
- self.XDG_DATA_HOME = XDG_DATA_HOME
- self.now = now_not_set
- def __call__(self, *args):
- EmptyCmd(
- out = StringIO(),
- err = StringIO(),
- environ = { 'XDG_DATA_HOME': self.XDG_DATA_HOME },
- now = self.now
- ).run('trash-empty', *args)
- def set_now(self, yyyy_mm_dd):
- self.now = lambda: date(yyyy_mm_dd)
-def now_not_set(): raise RuntimeError('now_not_set')
-
class TestEmptyCmdWithMultipleVolumes:
- def test_it_removes_trashinfos_from_method_1_dir(self):
- require_empty_dir('.fake_root')
- make_dirs('.fake_root/media/external-disk/.Trash/')
- set_sticky_bit('.fake_root/media/external-disk/.Trash/')
- having_file('.fake_root/media/external-disk/.Trash/123/info/foo.trashinfo')
- empty=EmptyCmd(
+ def setUp(self):
+ require_empty_dir('topdir')
+ self.empty=EmptyCmd(
out = StringIO(),
err = StringIO(),
environ = {},
getuid = lambda: 123,
- list_volumes = lambda: ['.fake_root/media/external-disk'],
- )
- empty.run('trash-empty')
- assert not os.path.exists('.fake_root/media/external-disk/.Trash/123/info/foo.trashinfo')
+ list_volumes = lambda: ['topdir'],)
+
+ def test_it_removes_trashinfos_from_method_1_dir(self):
+ self.make_proper_top_trash_dir('topdir/.Trash')
+ having_file('topdir/.Trash/123/info/foo.trashinfo')
+
+ self.empty.run('trash-empty')
+
+ assert not os.path.exists('topdir/.Trash/123/info/foo.trashinfo')
def test_it_removes_trashinfos_from_method_2_dir(self):
- require_empty_dir('.fake_root')
- having_file('.fake_root/media/external-disk/.Trash-123/info/foo.trashinfo')
- empty=EmptyCmd(
- out = StringIO(),
- err = StringIO(),
- environ = {},
- getuid = lambda: 123,
- list_volumes = lambda: ['.fake_root/media/external-disk'],
- )
- empty.run('trash-empty')
- assert not os.path.exists('.fake_root/media/external-disk/.Trash-123/info/foo.trashinfo')
+ having_file('topdir/.Trash-123/info/foo.trashinfo')
+
+ self.empty.run('trash-empty')
+ assert not os.path.exists('topdir/.Trash-123/info/foo.trashinfo')
+
+ def make_proper_top_trash_dir(self, path):
+ make_dirs(path)
+ set_sticky_bit(path)
+
+from textwrap import dedent
class TestTrashEmpty_on_help:
def test_help_output(self):
err, out = StringIO(), StringIO()
@@ -191,17 +209,17 @@ def test_help_output(self):
out = out,
environ = {},)
cmd.run('trash-empty', '--help')
- assert_equals(out.getvalue(), """\
-Usage: trash-empty [days]
+ assert_equals(out.getvalue(), dedent("""\
+ Usage: trash-empty [days]
-Purge trashed files.
+ Purge trashed files.
-Options:
- --version show program's version number and exit
- -h, --help show this help message and exit
+ Options:
+ --version show program's version number and exit
+ -h, --help show this help message and exit
-Report bugs to http://code.google.com/p/trash-cli/issues
-""")
+ Report bugs to http://code.google.com/p/trash-cli/issues
+ """))
class TestTrashEmpty_on_version():
def test_it_print_version(self):
@@ -211,11 +229,7 @@ def test_it_print_version(self):
environ = {},
version = '1.2.3')
cmd.run('trash-empty', '--version')
- assert_equals(out.getvalue(), """\
-trash-empty 1.2.3
-""")
-
-def date(yyyy_mm_dd):
- from datetime import datetime
- return datetime.strptime(yyyy_mm_dd, '%Y-%m-%d')
+ assert_equals(out.getvalue(), dedent("""\
+ trash-empty 1.2.3
+ """))
View
1  setup.cfg
@@ -12,3 +12,4 @@ release = egg_info -RDb ''
[nosetests]
nocapture=1
+testmatch=((?:^|[b_.-])(:?[Tt]est|describe_|it_should))
View
96 trashcli/trash.py
@@ -10,7 +10,7 @@
logger.addHandler(logging.StreamHandler())
class TrashDirectory:
- def __init__(self, path, volume) :
+ def __init__(self, path, volume) : # TODO: contents_of should be injected
self.path = os.path.normpath(path)
self.volume = volume
@@ -86,7 +86,7 @@ def all_info_files(self) :
def trashed_files(self) :
for info_file in self.all_info_files():
try:
- yield self._create_trashed_file_from_info_file(info_file)
+ yield self._create_trashed_file_from_info_file(info_file)
except ValueError:
logger.warning("Non parsable trashinfo file: %s" % info_file)
except IOError as e:
@@ -761,22 +761,25 @@ def write_file(path, contents):
f.close()
def do_nothing(*argv, **argvk): pass
-class _FileSystemReader:
- def __init__(self):
- self.contents_of = contents_of
- self.exists = os.path.exists
- self.is_sticky_dir = is_sticky_dir
+class FileSystemReader:
def entries_if_dir_exists(self, path):
if os.path.exists(path):
for entry in os.listdir(path):
yield entry
-
-def is_sticky_dir(path):
- import os
- return os.path.isdir(path) and has_sticky_bit(path)
-
-def contents_of(path):
- return file(path).read()
+ def is_sticky_dir(self, path):
+ import os
+ return os.path.isdir(path) and has_sticky_bit(path)
+ def list_volumes(self):
+ return mount_points()
+ def exists(self, path):
+ return os.path.exists(path)
+ def is_symlink(self, path):
+ return os.path.islink(path)
+ def contents_of(self, path):
+ return file(path).read()
+
+def contents_of(path): # TODO remove
+ return FileSystemReader().contents_of(path)
class _FileRemover:
def remove_file(self, path):
@@ -791,12 +794,11 @@ def remove_file_if_exists(self,path):
from .list_mount_points import mount_points
from datetime import datetime
-class ListCmd():
+class ListCmd:
def __init__(self, out, err, environ,
getuid = os.getuid,
list_volumes = mount_points,
- is_sticky_dir = is_sticky_dir,
- file_reader = _FileSystemReader(),
+ file_reader = FileSystemReader(),
version = version):
self.output = self.Output(out, err)
@@ -805,8 +807,7 @@ def __init__(self, out, err, environ,
self.contents_of = file_reader.contents_of
self.trashdirs = AvailableTrashDirs(environ,
getuid,
- list_volumes,
- is_sticky_dir)
+ fs = file_reader)
self.version = version
def run(self, *argv):
@@ -816,8 +817,7 @@ def run(self, *argv):
parse.as_default(self.list_trash)
parse(argv)
def list_trash(self):
- self.trashdirs.for_each_trashdir_and_volume(self.list_contents,
- self.output)
+ self.trashdirs.list_trashdirs(self.list_contents, self.output)
def list_contents(self, trash_dir, volume_path):
self.output.set_volume_path(volume_path)
trashdir = TrashDir(self.file_reader, trash_dir, volume_path)
@@ -860,6 +860,9 @@ def print_parse_path_error(self, offending_file):
def top_trashdir_skipped_because_parent_not_sticky(self, trashdir):
self.error("TrashDir skipped because parent not sticky: %s"
% trashdir)
+ def top_trashdir_skipped_because_parent_is_symlink(self, trashdir):
+ self.error("TrashDir skipped because parent is symlink: %s"
+ % trashdir)
def set_volume_path(self, volume_path):
self.volume_path = volume_path
def print_entry(self, maybe_deletion_date, relative_location):
@@ -912,10 +915,9 @@ def as_default(self, default_action):
class EmptyCmd():
def __init__(self, out, err, environ,
now = datetime.now,
- file_reader = _FileSystemReader(),
+ file_reader = FileSystemReader(),
list_volumes = mount_points,
getuid = os.getuid,
- is_sticky_dir = is_sticky_dir,
file_remover = _FileRemover(),
version = version):
@@ -923,10 +925,15 @@ def __init__(self, out, err, environ,
self.err = err
self.file_reader = file_reader
self.contents_of = file_reader.contents_of
+ class Fs: #TODO remove the need of this class
+ def __init__(self):
+ self.list_volumes = list_volumes
+ self.is_sticky_dir = file_reader.is_sticky_dir
+ self.exists = file_reader.exists
+ self.is_symlink = file_reader.is_symlink
self.trashdirs = AvailableTrashDirs(environ,
getuid,
- list_volumes,
- is_sticky_dir)
+ fs = Fs())
self.now = now
self.file_remover = file_remover
@@ -957,7 +964,7 @@ def is_int(self, text):
except ValueError:
return False
def _empty_all_trashdirs(self):
- self.trashdirs.for_each_trashdir_and_volume(self._empty_trashdir)
+ self.trashdirs.list_trashdirs(self._empty_trashdir)
def _empty_trashdir(self, trash_dir, volume_path):
trashdir = TrashDir(self.file_reader, trash_dir, volume_path)
@@ -1044,18 +1051,20 @@ def __call__(self, program_name):
self.println("%s %s" % (program_name, self.version))
class AvailableTrashDirs:
- def __init__(self, environ, getuid, list_volumes, is_sticky_dir):
+ def __init__(self, environ, getuid, fs=None):
self.environ = environ
self.getuid = getuid
- self.list_volumes = list_volumes
- self.is_sticky_dir = is_sticky_dir
+ self.fs = fs
+ self.list_volumes = fs.list_volumes
+ self.is_sticky_dir = fs.is_sticky_dir
+ self.exists = fs.exists
def do_nothing(trash_dir, volume): pass
class NullLog:
def top_trashdir_skipped_because_parent_not_sticky(self, trashdir): pass
- def for_each_trashdir_and_volume(self, action = do_nothing,
- error_log = NullLog()):
- self._for_home_trashcan(action)
- self._for_each_volume_trashcan(action, error_log)
+ def top_trashdir_skipped_because_parent_is_symlink(self, trashdir): pass
+ def list_trashdirs(self, out = do_nothing, error_log = NullLog()):
+ self._for_home_trashcan(out)
+ self._for_each_volume_trashcan(out, error_log)
def _for_home_trashcan(self, out):
def return_result_with_volume(trashcan_path):
out(trashcan_path, '/')
@@ -1063,14 +1072,19 @@ def return_result_with_volume(trashcan_path):
def _for_each_volume_trashcan(self, action, error_log):
from os.path import join
for volume in self.list_volumes():
- top_trash_dir = join(volume, '.Trash')
- if self.is_sticky_dir(top_trash_dir):
- action(join(top_trash_dir, str(self.getuid())), volume)
- else:
- error_log.top_trashdir_skipped_because_parent_not_sticky(
- join(top_trash_dir, str(self.getuid())))
+ parent_trashdir = join(volume, '.Trash')
+ top_trashdir = join(parent_trashdir, str(self.getuid()))
+ alt_top_trashdir = join(volume, '.Trash-%s' % self.getuid())
+ if self.exists(top_trashdir):
+ if self.is_sticky_dir(parent_trashdir):
+ if not self.fs.is_symlink(parent_trashdir):
+ action(top_trashdir, volume)
+ else:
+ error_log.top_trashdir_skipped_because_parent_is_symlink(top_trashdir)
+ else:
+ error_log.top_trashdir_skipped_because_parent_not_sticky(top_trashdir)
- action(join(volume, '.Trash-%s' % self.getuid()), volume)
+ action(alt_top_trashdir, volume)
def home_trashcan_if_possible(environ, out):
if 'XDG_DATA_HOME' in environ:
@@ -1211,7 +1225,7 @@ def parse_path(contents):
return urllib.unquote(line[len('Path='):])
raise ParseError('Unable to parse Path')
-def has_sticky_bit(path):
+def has_sticky_bit(path): # TODO move to FileSystemReader
import os
import stat
return (os.stat(path).st_mode & stat.S_ISVTX) == stat.S_ISVTX
View
70 unit_tests/test_available_trash_dirs.py
@@ -36,12 +36,20 @@ def should_return_home_trashcan_when_XDG_DATA_HOME_is_defined(self):
def trashdirs(self):
result = collector()
+ class FileReader:
+ def list_volumes(_):
+ return self.volumes
+ def is_sticky_dir(_, path):
+ return self.Trash_dir_is_sticky
+ def exists(_, path):
+ return True
+ def is_symlink(_, path):
+ return False
AvailableTrashDirs(
environ=self.environ,
getuid=lambda:self.uid,
- list_volumes=lambda:self.volumes,
- is_sticky_dir=lambda path: self.Trash_dir_is_sticky
- ).for_each_trashdir_and_volume(result)
+ fs = FileReader(),
+ ).list_trashdirs(result)
return result.trash_dirs
def setUp(self):
@@ -64,16 +72,54 @@ def __init__(self):
def __call__(self, trash_dir, volume):
self.trash_dirs.append(trash_dir)
+from nose.tools import assert_equals
from mock import MagicMock
@istest
-class Describe_AvailableTrashDirs:
- def test_it_should_report_when_topdir_is_a_symlink(self):
- error_log = MagicMock()
- dirs = AvailableTrashDirs(environ = {},
- getuid = lambda:123,
- list_volumes = lambda: ['/topdir'],
- is_sticky_dir = lambda dir: False)
+class Describe_AvailableTrashDirs_when_parent_is_unsticky:
+ def setUp(self):
+ self.error_log = MagicMock()
+ self.fs = MagicMock()
+ self.dirs = AvailableTrashDirs(environ = {}, getuid = lambda:123,
+ fs = self.fs)
+ self.fs.list_volumes.return_value = ['/topdir']
+ self.fs.is_sticky_dir.side_effect = (
+ lambda path: {'/topdir/.Trash':False}[path])
+
+ def test_it_should_report_skipped_dir_non_sticky(self):
+ self.fs.exists.side_effect = (
+ lambda path: {'/topdir/.Trash/123':True}[path])
+
+ self.dirs.list_trashdirs(error_log = self.error_log)
+
+ (self.error_log.top_trashdir_skipped_because_parent_not_sticky.
+ assert_called_with('/topdir/.Trash/123'))
+
+ def test_it_shouldnot_care_about_non_existent(self):
+ self.fs.exists.side_effect = (
+ lambda path: {'/topdir/.Trash/123':False}[path])
+
+ self.dirs.list_trashdirs(error_log = self.error_log)
+
+ assert_equals([], self.error_log.
+ top_trashdir_skipped_because_parent_not_sticky.mock_calls)
+
+@istest
+class Describe_AvailableTrashDirs_when_parent_is_symlink:
+ def setUp(self):
+ self.error_log = MagicMock()
+ self.fs = MagicMock()
+ self.dirs = AvailableTrashDirs(environ = {}, getuid = lambda:123,
+ fs = self.fs)
+ self.fs.list_volumes.return_value = ['/topdir']
+ self.fs.exists.side_effect = (lambda path: {'/topdir/.Trash/123':True}[path])
+
+
+ def test_it_should_skip_symlink(self):
+ self.fs.is_sticky_dir.return_value = True
+ self.fs.is_symlink.return_value = True
+
+ self.dirs.list_trashdirs(error_log = self.error_log)
- dirs.for_each_trashdir_and_volume(error_log = error_log)
+ (self.error_log.top_trashdir_skipped_because_parent_is_symlink.
+ assert_called_with('/topdir/.Trash/123'))
- error_log.top_trashdir_skipped_because_parent_not_sticky.assert_called_with('/topdir/.Trash/123')
View
11 unit_tests/test_list_cmd.py
@@ -1,11 +0,0 @@
-from trashcli.trash import ListCmd
-from StringIO import StringIO
-
-class TestListCmd:
- def test_something(self):
- out=StringIO()
- err=StringIO()
- trash_list=ListCmd(out, err, {})
-
- trash_list.list_trash()
-
Please sign in to comment.
Something went wrong with that request. Please try again.