tests/TestCommand.py
Rama master · réplica de lectura del repositorio original.
# vim: ts=4:sw=4:expandtab
# BleachBit
# Copyright (C) 2008-2025 Andrew Ziem
# https://www.bleachbit.org
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
Test case for Command
"""
import errno
import os
import sqlite3
import warnings
from unittest import mock
from tests import common
import bleachbit.Command
from bleachbit import FileUtilities, IS_WINDOWS
from bleachbit.Command import Delete, DeleteRecycleBinMetadata, Fail, Function, Shred, Truncate
from bleachbit.Language import get_text as _
from bleachbit.Options import options
if not IS_WINDOWS:
# pylint: disable-next=redefined-builtin
from bleachbit.General import WindowsError
class CommandTestCase(common.BleachbitTestCase):
"""Test case for Command"""
def test_Delete(self, cls=Delete):
"""Unit test for Delete"""
path = self.write_file('test_Delete', b'foo')
cmd = cls(path)
self.assertExists(path)
# preview
ret = next(cmd.execute(really_delete=False))
self.assertGreater(ret['size'], 0)
self.assertEqual(ret['path'], path)
self.assertExists(path)
# delete
ret = next(cmd.execute(really_delete=True))
self.assertGreater(ret['size'], 0)
self.assertEqual(ret['path'], path)
self.assertNotExists(path)
def test_delete_scan_size(self):
"""Delete reports the size a directory scan read"""
path = self.write_file('test_delete_scan_size', b'foo')
scanned = FileUtilities.ScannedPath(path)
scanned.scan_size = 1234
with mock.patch('bleachbit.FileUtilities.getsize') as mock_getsize:
ret = next(Delete(scanned).execute(really_delete=False))
mock_getsize.assert_not_called()
self.assertEqual(ret['size'], 1234)
next(Delete(path).execute(really_delete=False))
mock_getsize.assert_called_once_with(path)
self.assertExists(path)
def test_delete_permission(self):
"""Test delete with permission denied for getsize()"""
path = self.write_file('test_delete_permission', b'foo')
cmd = Delete(path)
self.assertExists(path)
with mock.patch('bleachbit.FileUtilities.getsize') as mock_getsize:
mock_getsize.side_effect = PermissionError('Permission denied')
# preview
ret = next(cmd.execute(really_delete=False))
self.assertIsNone(ret['size'])
self.assertEqual(ret['path'], path)
self.assertExists(path)
def test_recycle_bin_metadata(self):
"""Metadata is previewed, but removed only after its payload is gone."""
payload = self.write_file('payload')
metadata = self.write_file('metadata')
cmd = DeleteRecycleBinMetadata(metadata, payload)
preview = list(cmd.execute(False))
self.assertEqual([metadata], [ret['path'] for ret in preview])
self.assertExists(payload)
self.assertExists(metadata)
self.assertEqual([], list(cmd.execute(True)))
os.remove(payload)
results = list(cmd.execute(True))
self.assertEqual([metadata], [ret['path'] for ret in results])
self.assertNotExists(metadata)
self.assertEqual([], list(cmd.execute(True)))
def test_recycle_bin_metadata_kept_payload(self):
"""An explicit keep entry also protects an orphaned restore record."""
payload = os.path.join(self.tempdir, 'missing payload')
metadata = self.write_file('kept metadata')
for kept_path in (payload, metadata):
options.set_whitelist_paths([('file', kept_path)])
results = list(DeleteRecycleBinMetadata(
metadata, payload).execute(True))
self.assertEqual(0, results[0]['n_deleted'])
self.assertExists(metadata)
def test_recycle_bin_metadata_reappearing_payload(self):
"""Recheck payload presence at execution, not just at enumeration."""
payload = os.path.join(self.tempdir, 'reappearing payload')
metadata = self.write_file('reappearing metadata')
cmd = DeleteRecycleBinMetadata(metadata, payload)
self.write_file(payload)
self.assertEqual([], list(cmd.execute(True)))
self.assertExists(payload)
self.assertExists(metadata)
def test_recycle_bin_metadata_payload_stat_error(self):
"""A failed existence check must not be mistaken for a missing payload."""
payload = self.write_file('inaccessible payload')
metadata = self.write_file('inaccessible metadata')
with mock.patch('bleachbit.Command.os.lstat',
side_effect=PermissionError('Access denied')):
with self.assertRaises(PermissionError):
list(DeleteRecycleBinMetadata(metadata, payload).execute(True))
self.assertExists(payload)
self.assertExists(metadata)
def test_recycle_bin_metadata_nonempty_folder(self):
"""A folder left behind by a kept child retains restore information."""
payload = self.mkdir('payload folder')
child = self.write_file(os.path.join(payload, 'kept child'))
metadata = self.write_file('folder metadata')
options.set_whitelist_paths([('file', child)])
list(Delete(payload).execute(True))
self.assertEqual([], list(
DeleteRecycleBinMetadata(metadata, payload).execute(True)))
self.assertExists(child)
self.assertExists(metadata)
def test_recycle_bin_metadata_shred(self):
"""Restore records obey the global shred preference."""
payload = os.path.join(self.tempdir, 'shredded payload')
metadata = self.write_file('shredded metadata')
options.set('shred', True)
with mock.patch('bleachbit.FileUtilities.wipe_contents',
wraps=FileUtilities.wipe_contents) as wipe:
list(DeleteRecycleBinMetadata(metadata, payload).execute(True))
self.assertTrue(wipe.called)
self.assertNotExists(metadata)
def test_delete_dir_left_renamed(self):
"""Delete raises for a directory it could not rename back"""
dirname = self.mkdir('renamed_dir')
self.write_file(os.path.join(dirname, 'a_file'), b'content')
real_rename = os.rename
def rename(src, dst, **kwargs):
if os.path.basename(dst) == 'renamed_dir':
# A sharing violation must not mark the old name for reboot
raise OSError(errno.EACCES, 'In use', src, 32)
return real_rename(src, dst, **kwargs)
cmd = Delete(dirname, shred=True)
with mock.patch('bleachbit.FileUtilities.is_dir_empty',
return_value=True), \
mock.patch('os.rename', side_effect=rename):
with self.assertRaises(OSError) as cm:
next(cmd.execute(really_delete=True))
self.assertExists(os.path.join(cm.exception.filename, 'a_file'))
def test_Delete_locked_with_shred_option(self):
"""A locked file is reported as not overwritten when the global
shred option is on, as with the Shred command"""
path = self.write_file('test_Delete_locked', b'foo')
# The overwrite and the delete both failed on a sharing violation
if IS_WINDOWS:
locked = PermissionError(errno.EACCES, 'locked', path)
locked.winerror = 32
else:
# pylint: disable-next=possibly-used-before-assignment
locked = WindowsError(32, 'locked')
options.set('shred', True)
with mock.patch('bleachbit.FileUtilities.delete', side_effect=locked), \
mock.patch.object(bleachbit.Command, 'bleachbit', mock.Mock(),
create=True) as fake_bleachbit, \
warnings.catch_warnings(record=True) as caught:
warnings.simplefilter('always')
ret = next(Delete(path).execute(really_delete=True))
fake_bleachbit.Windows.delete_locked_file.assert_called_once_with(
FileUtilities.extended_path(path))
self.assertEqual(ret['label'], _('Mark for deletion'))
self.assertIn(UserWarning, [w.category for w in caught])
def test_Delete_locked_extended_path(self):
"""A locked file is flagged for deletion by the same extended
path that delete() uses, so a long path is found"""
path = self.write_file('test_Delete_locked_extended', b'foo')
if IS_WINDOWS:
locked = PermissionError(errno.EACCES, 'locked', path)
locked.winerror = 32
else:
# pylint: disable-next=possibly-used-before-assignment
locked = WindowsError(32, 'locked')
with mock.patch('bleachbit.FileUtilities.delete', side_effect=locked), \
mock.patch('bleachbit.FileUtilities.extended_path',
side_effect=lambda p: '\\\\?\\' + p), \
mock.patch.object(bleachbit.Command, 'bleachbit', mock.Mock(),
create=True) as fake_bleachbit:
next(Delete(path).execute(really_delete=True))
fake_bleachbit.Windows.delete_locked_file.assert_called_once_with(
'\\\\?\\' + path)
def test_Function(self):
"""Unit test for Function"""
path = self.write_file('test_Function', b'foo')
cmd = Function(path, FileUtilities.delete, 'bar')
self.assertExists(path)
self.assertGreater(os.path.getsize(path), 0)
# preview
next(cmd.execute(False))
self.assertExists(path)
self.assertGreater(os.path.getsize(path), 0)
# delete
ret = next(cmd.execute(True))
self.assertGreater(ret['size'], 0)
self.assertEqual(ret['path'], path)
self.assertNotExists(path)
def test_Function_no_collation(self):
"""Unit test for Function with no collation
See https://github.com/bleachbit/bleachbit/issues/1866
"""
path = self.write_file('test_Function_no_collation', b'')
cmd = Function(path,
lambda p: FileUtilities.execute_sqlite3(
p, 'CREATE TABLE test (name TEXT COLLATE foo);'),
'test_no_collation')
with mock.patch('bleachbit.Command.logger.debug') as mock_debug:
with self.assertRaises(StopIteration):
next(cmd.execute(True))
mock_debug.assert_called_with(mock.ANY)
def test_Function_sqlite_error_propagates(self):
"""Unit test for Function with sqlite error that is not collation
Non-collation sqlite errors must propagate to the Worker instead of
being silently swallowed by Command.execute().
"""
path = self.write_file('test_Function_sqlite_error', b'')
cmd = Function(path,
lambda p: FileUtilities.execute_sqlite3(
p, 'SELECT * FROM nonexistent_table;'),
'test_sqlite_error')
with self.assertRaises(sqlite3.OperationalError):
next(cmd.execute(True))
def test_Fail(self):
"""Unit test Fail"""
cmd = Fail('cannot clean')
self.assertEqual(str(cmd), 'cannot clean')
for really_delete in (False, True):
with self.assertRaises(RuntimeError) as cm:
list(cmd.execute(really_delete))
self.assertEqual(str(cm.exception), 'cannot clean')
def test_Shred(self):
"""Unit test for Shred"""
self.test_Delete(Shred)
def test_Truncate(self):
"""Unit test for Truncate"""
path = self.write_file('test_Truncate', b'foo')
cmd = Truncate(path)
# preview leaves the file intact
ret = next(cmd.execute(really_delete=False))
self.assertEqual(ret['path'], path)
self.assertGreater(os.path.getsize(path), 0)
# truncate empties the file but keeps it
ret = next(cmd.execute(really_delete=True))
self.assertEqual(ret['path'], path)
self.assertExists(path)
self.assertEqual(os.path.getsize(path), 0)