tests/TestCLI.py
Rama master · réplica de lectura del repositorio original.
# SPDX-License-Identifier: GPL-3.0-or-later
# Copyright (c) 2008-2026 Andrew Ziem.
#
# This work is licensed under the terms of the GNU GPL, version 3 or
# later. See the COPYING file in the top-level directory.
"""
Test case for module CLI
"""
# standard imports
import copy
import errno
import io
import locale
import os
import random
import runpy
import shutil
import signal
import sys
import tempfile
from unittest.mock import MagicMock, patch
from tests import common
from tests.common import pytest
# first party imports
import bleachbit
from bleachbit.CLI import (
CliCallback,
args_to_operations,
args_to_operations_list,
cleaners_list,
parse_cmd_line,
preview_or_clean,
process_cmd_line)
from bleachbit.General import get_executable, run_external
from bleachbit.GtkShim import gtk_may_be_available
from bleachbit import FileUtilities, Options, IS_WINDOWS, IS_POSIX
RUN_EXTERNAL_TIMEOUT = 30
class CLITestCase(common.BleachbitTestCase):
"""Test case for module CLI"""
def setUp(self):
super().setUp()
Options.options.reset_overrides()
Options.options.set_override("first_start", False)
def _test_preview(self, args, redirect_stdout=True, env=None):
"""Helper to test preview"""
# Use devnull because in some cases the buffer will be too large,
# and the other alternative, the screen, is not desirable.
with open(os.devnull, 'w', encoding='utf-8') as stdout:
if not redirect_stdout:
stdout = None
output = run_external(args, stdout=stdout,
env=env, timeout=RUN_EXTERNAL_TIMEOUT)
self.assertEqual(output[0], 0, "Return code = %d, stderr='%s'"
% (output[0], output[2]))
pos = output[2].find('Traceback (most recent call last)')
if pos > -1:
print("Saw the following error when using args '%s':\n %s" %
(args, output[2]))
self.assertEqual(
pos, -1, f"Traceback found in stderr when using args {args}:\n{output[2][pos:]}")
def test_args_to_operations_list(self):
"""Unit test for args_to_operations_list()"""
# --preset
Options.init_configuration()
o = args_to_operations_list(True, False)
self.assertEqual(o, [])
Options.options.set_tree('system', 'tmp', True)
o = args_to_operations_list(True, False)
self.assertEqual(o, ['system.tmp'])
# --all-but-warning
o = args_to_operations_list(False, True)
self.assertIsInstance(o, list)
self.assertIn('google_chrome.cache', o)
self.assertIn('system.tmp', o)
gui_available = IS_WINDOWS or gtk_may_be_available()
self.assertEqual(gui_available, 'system.clipboard' in o)
self.assertNotIn('system.empty_space', o)
self.assertNotIn('system.memory', o)
def test_args_to_operations(self):
"""Unit test for args_to_operations()"""
# test explicit cleaners (without --preset or --all-but-warning)
# adobe_reader is used here only as a stable example cleaner for
# testing wildcard/exclusion parsing. Its 'logs' option is macOS-only
# and its 'mru'/'tmp' options are excluded on macOS
# (cleaners/adobe_reader.xml), so the expected option set differs by
# platform.
if bleachbit.IS_MAC:
adobe_reader_options = ['cache', 'logs']
else:
adobe_reader_options = ['cache', 'mru', 'tmp']
adobe_reader_options_no_cache = [
o for o in adobe_reader_options if o != 'cache']
tests = (
(['adobe_reader.*'], [],
{'adobe_reader': adobe_reader_options}),
(['adobe_reader.cache'], [], {'adobe_reader': ['cache']}),
(['adobe_reader.*'], ['adobe_reader.cache'],
{'adobe_reader': adobe_reader_options_no_cache}))
for test_args, excludes, expected in tests:
o = args_to_operations(test_args, False, False, excludes)
self.assertIsInstance(o, dict)
self.assertEqual(o, expected)
# Unknown cleaners and options are skipped with a warning
with self.assertLogs('bleachbit.CLI', level='WARNING') as log_context:
o = args_to_operations(
['adobe_reader.bogus', 'adobe_reader.cache', 'no_such_cleaner.cache'],
False, False)
self.assertEqual(o, {'adobe_reader': ['cache']})
self.assertEqual(len(log_context.output), 2)
# An unknown option in --except drops the whole cleaner
with self.assertLogs('bleachbit.CLI', level='WARNING') as log_context:
o = args_to_operations(
['adobe_reader.*', 'google_chrome.cache'], False, False,
['adobe_reader.bogus'])
self.assertEqual(o, {'google_chrome': ['cache']})
self.assertEqual(len(log_context.output), 2)
# An unknown option for a cleaner that is not selected only warns
with self.assertLogs('bleachbit.CLI', level='WARNING') as log_context:
o = args_to_operations(
['google_chrome.cache'], False, False, ['adobe_reader.bogus'])
self.assertEqual(o, {'google_chrome': ['cache']})
self.assertEqual(len(log_context.output), 1)
# An unknown cleaner in --except cleans nothing
with self.assertRaises(SystemExit) as cm:
args_to_operations(
['adobe_reader.cache'], False, False, ['no_such_cleaner.cache'])
self.assertEqual(cm.exception.code, 1)
# Test failure on wildcard
with self.assertRaises(SystemExit) as cm:
args_to_operations(
['adobe_reader.mru'], False, False, ['adobe_reader.*'])
self.assertEqual(cm.exception.code, 1)
def test_parse_cmd_line_except(self):
"""Unit test for parse_cmd_line() --except handling"""
tests = (
(['--clean', 'firefox.*', 'chromium.*',
'--except', 'firefox.passwords,chromium.passwords'],
['firefox.*', 'chromium.*'],
['firefox.passwords', 'chromium.passwords']),
(['--clean', 'firefox.*', 'chromium.*',
'--except', 'firefox.passwords',
'--except', 'chromium.passwords'],
['firefox.*', 'chromium.*'],
['firefox.passwords', 'chromium.passwords']),
(['--clean', 'chromium.*',
'--except', 'firefox.passwords',
'firefox.*',
'--except', 'chromium.passwords'],
['chromium.*', 'firefox.*'],
['firefox.passwords', 'chromium.passwords']))
for argv, expected_args, expected_excludes in tests:
_parser, options, args, excludes = parse_cmd_line(argv)
self.assertTrue(options.clean)
self.assertEqual(expected_args, args)
self.assertEqual(expected_excludes, excludes)
def test_cleaners_list(self):
"""Unit test for cleaners_list()"""
for cleaner in cleaners_list():
self.assertIsString(cleaner)
def test_debug_log(self):
"""Unit test for --debug-log option"""
# These texts are required in the log file.
file_required_texts = [
'DEBUG - Debug log file initialized',
# the unknown cleaner, then no operations left
'WARNING - ',
'doesnot.exist',
'ERROR - ',
]
# These texts are forbidden in stderr.
stderror_forbidden_texts = [
'bleachbit.CLI',
'bleachbit.CleanerML',
'ERROR',
'DEBUG'
]
for delimiter in (' ', '='):
# delete_on_close=False is helpful but requires Python 3.12.
with tempfile.NamedTemporaryFile(delete=False, dir=self.tempdir) as f:
f.close()
log_path = f.name
# 'bleachbit.py' is used instead of '-m bleachbit.CLI'
args = [get_executable(), 'bleachbit.py', '--preview', 'doesnot.exist',
'--debug'] + f'--debug-log{delimiter}{log_path}'.split(delimiter)
(rc, _stdout, stderr) = run_external(
args, stdout=None, timeout=RUN_EXTERNAL_TIMEOUT)
self.assertEqual(1, rc, f"rc={rc}, stderr={stderr}")
self.assertExists(log_path)
with open(log_path, 'r', encoding='utf-8') as log_file:
log_content = log_file.read()
self.assertNotIn('Traceback', stderr)
self.assertNotIn('Traceback', log_content)
for stderr_forbidden_text in stderror_forbidden_texts:
self.assertNotIn(stderr_forbidden_text, stderr,
f"The text '{stderr_forbidden_text}' was found in stderr")
# The file handler stamps each record with a timestamp
# such as "2026-10-30 18:57:15,591", which must not leak
# into stderr. Match the timestamp rather than the bare
# date because the console output may legitimately
# contain a date (e.g. "Build date = 2026-10-30").
self.assertNotRegex(
stderr, r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}')
for required_text in file_required_texts:
self.assertIn(required_text, log_content,
f"The text '{required_text}' was not found in the log file")
os.remove(log_path)
self.assertNotExists(log_path)
@common.skipIfWindows
def test_encoding(self):
"""Unit test for encoding"""
filename = self.write_file(
'/tmp/bleachbit-test-cli-encoding-\xe4\xf6\xfc~')
# not assertExists because it doesn't cope with invalid encodings
self.assertTrue(os.path.exists(filename))
env = copy.deepcopy(os.environ)
env['LANG'] = 'en_US' # not UTF-8
args = [get_executable(), '-m', 'bleachbit.CLI', '-p', 'system.tmp']
# If Python pipes stdout to file or devnull, the test may give
# a false negative. It must print stdout to terminal.
self._test_preview(args, redirect_stdout=False, env=env)
os.remove(filename)
self.assertNotExists(filename)
def test_invalid_locale(self):
"""Unit test for invalid locales"""
original_locale = locale.getlocale(locale.LC_NUMERIC)
old_lang = common.get_env('LANG')
with common.set_temporary_env('LANG', 'blahfoo'):
# tests are run from the parent directory
args = [get_executable(), '-m', 'bleachbit.CLI', '--version']
output = run_external(args, timeout=RUN_EXTERNAL_TIMEOUT)
self.assertNotEqual(output[1].find('Copyright'), -1, str(output))
self.assertEqual(common.get_env('LANG'), old_lang)
self.assertEqual(locale.getlocale(locale.LC_NUMERIC), original_locale)
def test_preview(self):
"""Unit test for --preview option"""
env = copy.deepcopy(os.environ)
# the language and encoding affect the test results
env['LANG'] = 'en_US.UTF-8'
env['PYTHONIOENCODING'] = 'utf-8:backslashreplace'
args_list = []
module = 'bleachbit.CLI'
big_args = [get_executable(), '-m', module, '--preview', ]
# The full list can take a long time and generally does not improve the testing,
# so test a subset.
full_cleaners_list = list(cleaners_list())
system_cleaners = [
c for c in full_cleaners_list if c.startswith('system.')]
if 'system.clipboard' in system_cleaners:
# Fix error on GitHub Actions
# FIXME: do conditional removal
system_cleaners.remove('system.clipboard')
non_system_cleaners = [
c for c in full_cleaners_list
# deepscan.* can be too slow
if not c.startswith('system.') and not c.startswith('deepscan.')]
sample_cleaners = random.sample(non_system_cleaners, 5)
for cleaner in (system_cleaners + sample_cleaners):
args_list.append(
[get_executable(), '-m', module, '--preview', cleaner])
big_args.append(cleaner)
args_list.append(big_args)
for args in args_list:
self._test_preview(args, env=env)
def test_delete(self):
"""Unit test for --delete option"""
prefixes = [
'bleachbit-test-cli-delete',
'\x8b\x8b-bad-encoding'
]
for prefix in prefixes:
with self.subTest(prefix=prefix):
filename = self.mkstemp(prefix=prefix)
filename = os.path.normcase(filename)
deleted_paths = []
crash = [False]
# the defaults bind this iteration's values into the closure
# pylint: disable-next=unused-argument, dangerous-default-value
def dummy_delete(path, shred=False, top=None, crash=crash,
deleted_paths=deleted_paths, filename=filename):
try:
self.assertLExists(path)
except AssertionError:
# On busy systems, other temp files may disappear
# between scan and delete, so flag a crash only for the
# test file itself.
if os.path.normcase(path) == os.path.normcase(filename):
crash[0] = True
deleted_paths.append(os.path.normcase(path))
with patch.object(FileUtilities, 'delete', side_effect=dummy_delete):
FileUtilities.delete(filename)
# File exists because delete() is mocked, so real
# delete() was not called.
self.assertExists(filename)
operations = args_to_operations(
['system.tmp'], False, False)
preview_or_clean(operations, True, quiet=True)
self.assertIn(filename, deleted_paths,
f"{filename} not found deleted")
os.remove(filename)
self.assertNotExists(filename)
self.assertFalse(crash[0], "Crash detected during deletion")
def test_preview_or_clean_sigterm(self):
"""SIGTERM stops preview_or_clean() like Ctrl+C so cleanup code runs"""
cleaned_up = []
def fake_run():
try:
signal.getsignal(signal.SIGTERM)(signal.SIGTERM, None)
yield True
finally:
cleaned_up.append(True)
old_handler = signal.getsignal(signal.SIGTERM)
with patch('bleachbit.Worker.Worker') as mock_worker:
mock_worker.return_value.run.side_effect = fake_run
with self.assertRaises(KeyboardInterrupt):
preview_or_clean({'test': ['option1']}, True, quiet=True)
self.assertEqual(cleaned_up, [True])
self.assertEqual(signal.getsignal(signal.SIGTERM), old_handler)
def test_preview_or_clean_ignored_signal(self):
"""preview_or_clean() leaves ignored signals alone, e.g. under nohup"""
handlers = []
def fake_run():
handlers.append(signal.getsignal(signal.SIGTERM))
yield True
old_handler = signal.signal(signal.SIGTERM, signal.SIG_IGN)
self.addCleanup(signal.signal, signal.SIGTERM, old_handler)
with patch('bleachbit.Worker.Worker') as mock_worker:
mock_worker.return_value.run.side_effect = fake_run
preview_or_clean({'test': ['option1']}, True, quiet=True)
self.assertEqual(handlers, [signal.SIG_IGN])
self.assertIs(signal.getsignal(signal.SIGTERM), signal.SIG_IGN)
@pytest.mark.no_xdist
def test_append_text(self):
"""Unit test for CliCallback.append_text() with special strings"""
cb = CliCallback(quiet=False)
for test_str in common.SPECIAL_TEST_STRINGS:
# Test that append_text handles special strings without crashing
cb.append_text(test_str + "\n")
# Test with newlines stripped (as implementation does)
cb.append_text(f"prefix{test_str}suffix\n")
# Test tag parameter (ignored but should not crash)
cb.append_text(test_str + "\n", _tag="test_tag")
def test_append_text_writes_stdout(self):
"""Unit test for CliCallback.append_text() normal output"""
cb = CliCallback(quiet=False)
with patch('sys.stdout', new_callable=io.StringIO) as mock_stdout:
cb.append_text("hello\n")
self.assertEqual("hello\n", mock_stdout.getvalue())
def _assert_append_text_closes_output(self, exception):
"""Verify append_text ignores interrupted stdout."""
class ClosedStdout:
def __init__(self, write_exception):
self.write_exception = write_exception
self.write_calls = 0
def write(self, _text):
self.write_calls += 1
raise self.write_exception
def flush(self):
return None
stdout = ClosedStdout(exception)
cb = CliCallback(quiet=False)
with patch('sys.stdout', stdout):
cb.append_text("hello\n")
cb.append_text("again\n")
self.assertEqual(1, stdout.write_calls)
def test_append_text_ignores_broken_pipe(self):
"""Unit test for CliCallback.append_text() with a closed pipe"""
self._assert_append_text_closes_output(BrokenPipeError())
def test_append_text_ignores_windows_invalid_argument(self):
"""Unit test for CliCallback.append_text() after more.com exits"""
self._assert_append_text_closes_output(
OSError(errno.EINVAL, "Invalid argument"))
def test_append_text_reraises_unrelated_oserror(self):
"""Unit test for preserving unrelated stdout errors"""
cb = CliCallback(quiet=False)
with patch('sys.stdout') as mock_stdout:
mock_stdout.write.side_effect = OSError(errno.EIO, "I/O error")
with self.assertRaises(OSError) as cm:
cb.append_text("hello\n")
self.assertEqual(errno.EIO, cm.exception.errno)
def test_return_text(self):
"""Check for correct text in output"""
# format of tests is: args, expected_output, clean_env, env
tests = [
[['--help'], 'Usage: ', False, None],
[['--version'], 'BleachBit version', True, None],
[['--sysinfo'], 'sys.version', False, None]
]
if IS_POSIX:
# GUI is not available with clean environment on POSIX.
tests.append([['--help'], 'Usage: ', True, None])
# Force detection of Wayland
wayland_env = os.environ.copy()
wayland_env['XDG_SESSION_TYPE'] = 'wayland'
tests.append([['--sysinfo'], 'sys.version', False, wayland_env])
for args, expected_output, clean_env, env in tests:
launcher_output = {}
for i, launcher in enumerate([['bleachbit.py'], ['-m', 'bleachbit.CLI']]):
direct_args = [get_executable(),] + launcher + args
output = run_external(
direct_args, env=env, clean_env=clean_env, timeout=RUN_EXTERNAL_TIMEOUT)
self.assertEqual(output[0], 0, output)
self.assertIn(expected_output, output[1])
modified_output_str = output[1].replace(
'CLI.py', 'bleachbit.py')
launcher_output[i] = [line for line in modified_output_str.split(
'\n') if not line.startswith("sys.argv")]
self.assertEqual(launcher_output[0], launcher_output[1])
def test_process_cmd_line_mutually_exclusive(self):
"""Unit test for process_cmd_line() with mutually exclusive commands"""
with patch('sys.argv', ['bleachbit', '--preview', '--clean']):
with self.assertRaises(SystemExit) as cm:
process_cmd_line()
self.assertEqual(cm.exception.code, 1)
def test_process_cmd_line_version(self):
"""Unit test for process_cmd_line() with --version"""
with patch('sys.argv', ['bleachbit', '--version']):
with patch('sys.stdout', new_callable=io.StringIO) as mock_stdout:
with self.assertRaises(SystemExit) as cm:
process_cmd_line()
self.assertEqual(cm.exception.code, 0)
self.assertIn('BleachBit version', mock_stdout.getvalue())
def test_process_cmd_line_list_cleaners(self):
"""Unit test for process_cmd_line() with --list-cleaners"""
with patch('bleachbit.CLI.list_cleaners') as mock_list:
with patch('sys.argv', ['bleachbit', '--list-cleaners']):
with self.assertRaises(SystemExit) as cm:
process_cmd_line()
self.assertEqual(cm.exception.code, 0)
mock_list.assert_called_once()
def test_process_cmd_line_pot(self):
"""Unit test for process_cmd_line() with --pot"""
with patch('bleachbit.CleanerML.create_pot') as mock_create_pot:
with patch('sys.argv', ['bleachbit', '--pot']):
with self.assertRaises(SystemExit) as cm:
process_cmd_line()
self.assertEqual(cm.exception.code, 0)
mock_create_pot.assert_called_once()
def test_process_cmd_line_wipe_empty_space_no_args(self):
"""Unit test for process_cmd_line() --wipe-empty-space with no args"""
with patch('sys.argv', ['bleachbit', '--wipe-empty-space']):
with self.assertRaises(SystemExit) as cm:
process_cmd_line()
self.assertEqual(cm.exception.code, 1)
def test_process_cmd_line_wipe_empty_space_missing_path(self):
"""Unit test for process_cmd_line() --wipe-empty-space, missing path"""
missing = os.path.join(self.tempdir, 'does-not-exist')
with patch('bleachbit.Wipe.wipe_path') as mock_wipe_path:
with patch('sys.argv', ['bleachbit', '--wipe-empty-space', missing]):
with self.assertRaises(SystemExit) as cm:
process_cmd_line()
self.assertEqual(cm.exception.code, 1)
mock_wipe_path.assert_not_called()
def test_process_cmd_line_preview_failed(self):
"""Unit test for process_cmd_line() --preview when the run fails"""
with patch('bleachbit.CLI.preview_or_clean', return_value=False):
with patch('sys.argv', ['bleachbit', '--preview', 'system.tmp']):
with self.assertRaises(SystemExit) as cm:
process_cmd_line()
self.assertEqual(cm.exception.code, 1)
def test_preview_or_clean_status(self):
"""preview_or_clean() returns False when the run fails"""
def fake_run():
yield False
def failing_run():
yield True
raise KeyError('test')
with patch('bleachbit.Worker.Worker') as mock_worker:
mock_worker.return_value.run.side_effect = fake_run
self.assertTrue(preview_or_clean(
{'test': ['option1']}, False, quiet=True))
mock_worker.return_value.run.side_effect = failing_run
with self.assertLogs('bleachbit.CLI', level='ERROR'):
self.assertFalse(preview_or_clean(
{'test': ['option1']}, False, quiet=True))
def test_process_cmd_line_preview_no_operations(self):
"""Unit test for process_cmd_line() --preview with no operations"""
with patch('sys.argv', ['bleachbit', '--preview']):
with self.assertRaises(SystemExit) as cm:
process_cmd_line()
self.assertEqual(cm.exception.code, 1)
def test_process_cmd_line_overwrite_warning(self):
"""Unit test for process_cmd_line() --overwrite warning with --preview"""
with patch('bleachbit.CLI.preview_or_clean'):
with patch('bleachbit.CLI.logger.warning') as mock_warning:
with patch('sys.argv', ['bleachbit', '--preview', 'system.tmp', '--overwrite']):
with self.assertRaises(SystemExit) as cm:
process_cmd_line()
self.assertEqual(cm.exception.code, 0)
mock_warning.assert_called_once()
def test_process_cmd_line_overwrite_no_warning_with_clean(self):
"""Unit test for process_cmd_line() --overwrite without warning when used with --clean"""
with patch('bleachbit.CLI.preview_or_clean'):
with patch('bleachbit.CLI.logger.warning') as mock_warning:
with patch('sys.argv', ['bleachbit', '--clean', 'system.tmp', '--overwrite']):
with self.assertRaises(SystemExit) as cm:
process_cmd_line()
self.assertEqual(cm.exception.code, 0)
mock_warning.assert_not_called()
def test_process_cmd_line_sysinfo(self):
"""Unit test for process_cmd_line() with --sysinfo"""
with patch('bleachbit.CLI.SystemInformation.get_system_information',
return_value='test sysinfo'):
with patch('sys.stdout', new_callable=io.StringIO) as mock_stdout:
with patch('sys.argv', ['bleachbit', '--sysinfo']):
with self.assertRaises(SystemExit) as cm:
process_cmd_line()
self.assertEqual(cm.exception.code, 0)
self.assertIn('test sysinfo', mock_stdout.getvalue())
def test_process_cmd_line_shred(self):
"""Unit test for process_cmd_line() with --shred"""
with patch.dict('bleachbit.CLI.backends', clear=True):
with patch('bleachbit.CLI.create_simple_cleaner', return_value=MagicMock()):
with patch('bleachbit.CLI.preview_or_clean') as mock_preview:
with patch('sys.argv', ['bleachbit', '--shred', '/tmp/test-shred']):
with self.assertRaises(SystemExit) as cm:
process_cmd_line()
self.assertEqual(cm.exception.code, 0)
mock_preview.assert_called_once()
def test_process_cmd_line_gui(self):
"""Unit test for process_cmd_line() with --gui"""
mock_gui_module = MagicMock()
mock_app = MagicMock()
mock_app.run.return_value = 0
mock_gui_module.Bleachbit.return_value = mock_app
with patch.dict('sys.modules', {'bleachbit.GuiApplication': mock_gui_module}):
with patch.object(bleachbit, 'GuiApplication', mock_gui_module, create=True):
with patch('bleachbit.Bootstrap.check_wayland_and_root', return_value=False):
with patch('bleachbit.CLI.IS_WINDOWS', False):
with patch('sys.argv', ['bleachbit', '--gui']):
with self.assertRaises(SystemExit) as cm:
process_cmd_line()
self.assertEqual(cm.exception.code, 0)
mock_gui_module.Bleachbit.assert_called_once_with(
uac=False, shred_paths=[], auto_exit=None)
mock_app.run.assert_called_once()
def test_process_cmd_line_no_command(self):
"""Unit test for process_cmd_line() with no command"""
# Reminder: When GUI is available and there are no arguments, then
# process_cmd_line is not called.
# process_cmd_line() is called when either GUI is not available or
# there are arguments.
with patch('sys.argv', ['bleachbit']):
with patch('sys.stdout', new_callable=io.StringIO) as mock_stdout:
process_cmd_line()
self.assertIn('bleachbit', mock_stdout.getvalue())
def test_process_cmd_line_debug(self):
"""Unit test for process_cmd_line() with --debug"""
with patch('bleachbit.CLI.preview_or_clean'):
with patch('sys.argv', ['bleachbit', '--debug', '--preview', 'system.tmp']):
with self.assertRaises(SystemExit) as cm:
process_cmd_line()
self.assertEqual(cm.exception.code, 0)
self.assertTrue(Options.options.has_override('debug'))
def test_process_cmd_line_preset(self):
"""Unit test for process_cmd_line() with --preset"""
# Preset branch only calls set_root_log_level when debug is enabled.
Options.options.set_override('debug', True)
with patch('bleachbit.CLI.preview_or_clean'):
with patch('bleachbit.CLI.set_root_log_level') as mock_set_level:
with patch('sys.argv', ['bleachbit', '--preview', '--preset', 'system.tmp']):
with self.assertRaises(SystemExit) as cm:
process_cmd_line()
self.assertEqual(cm.exception.code, 0)
mock_set_level.assert_called_once()
def test_process_cmd_line_debug_log(self):
"""Unit test for process_cmd_line() with --debug-log"""
with patch('bleachbit.CLI.preview_or_clean'):
with patch('bleachbit.CLI.SystemInformation.get_system_information',
return_value='test sysinfo'):
with patch('bleachbit.CLI.logger.info') as mock_log_info:
with patch('sys.argv', ['bleachbit', '--debug-log', '/tmp/test.log',
'--preview', 'system.tmp']):
with self.assertRaises(SystemExit) as cm:
process_cmd_line()
self.assertEqual(cm.exception.code, 0)
mock_log_info.assert_any_call('test sysinfo')
def test_process_cmd_line_option_overrides(self):
"""Unit test for process_cmd_line() option overrides"""
with patch('sys.argv', ['bleachbit', '--no-load-cleaners',
'--no-delete-confirmation']):
with patch('sys.stdout', new_callable=io.StringIO):
process_cmd_line()
self.assertFalse(Options.options.get('load_cleaners'))
self.assertFalse(Options.options.get('delete_confirmation'))
def test_shred(self):
"""Unit test for --shred"""
suffixes = ['', '.', '.txt']
dirs = ['.', self.tempdir]
for dir_ in dirs:
for suffix in suffixes:
(fd, filename) = tempfile.mkstemp(
prefix='bleachbit-test-cli-shred', suffix=suffix, dir=dir_)
os.close(fd)
if '.' == dir_:
filename = os.path.basename(filename)
# The CWD case is not covered by tearDownClass, so register
# this cleanup.
self.addCleanup(
lambda f=filename: os.path.exists(f) and os.remove(f))
# not assertExists because something strange happens on Windows
self.assertTrue(os.path.exists(filename))
args = [get_executable(), '-m',
'bleachbit.CLI', '--shred', filename]
output = run_external(args, timeout=RUN_EXTERNAL_TIMEOUT)
self.assertEqual(output[0], 0)
self.assertNotExists(filename)
@common.also_with_sudo
def test_sysinfo(self):
"""Unit test for --sysinfo
By not using `-m bleachbit.CLI`, this exercises `./bleachbit.py`.
"""
env_configs = [[]]
if not IS_WINDOWS:
env_configs.extend([
['env', '-i'], # No environment variables
['env', '-i', 'XDG_SESSION_TYPE=wayland'] # Mimic Wayland
])
for env_prefix in env_configs:
args = env_prefix + [get_executable(), 'bleachbit.py', '--sysinfo']
output = run_external(args, timeout=RUN_EXTERNAL_TIMEOUT)
if IS_POSIX and os.environ.get('USER') == 'root' and \
output[0] == 1:
continue
self.assertEqual(output[0], 0, output)
self.assertIn('sys.version', output[1])
# FIXME: verify that there is not a message like
# (bleachbit.py:1234): Gdk-CRITICAL **: 23:05:08.581: gdk_screen_get_root_window: assertion 'GDK_IS_SCREEN (screen)' failed
@common.skipIfWindows
def test_launcher_prefix_share(self):
"""The launcher finds the package `make install` put in <prefix>/share"""
prefix = self.mkdtemp()
common.touch_file(os.path.join(
prefix, 'share', 'bleachbit', '__init__.py'))
launcher = os.path.join(prefix, 'bin', 'bleachbit')
os.mkdir(os.path.dirname(launcher))
shutil.copy('bleachbit.py', launcher)
launcher_globals = runpy.run_path(launcher, run_name='launcher')
with patch.object(sys, 'path', []), patch.dict(os.environ):
os.environ.pop('APPDIR', None)
launcher_globals['_add_posix_share_to_path']()
self.assertEqual(sys.path, [os.path.join(prefix, 'share')])
@pytest.mark.xdist_group('gui')
@common.skipUnlessWindows
def test_gui_exit(self):
"""Unit test for --gui --exit, only for Windows"""
# When running tests without elevated privileges, then --no-uac shows
# the UAC dialog, requiring user interaction. Also, the parent
# closes immediately, but the child continues, causing test to fail.
args = (get_executable(), '-m',
'bleachbit.CLI', '--gui', '--exit', '--no-uac')
(rc, _stdout, stderr) = run_external(
args, timeout=RUN_EXTERNAL_TIMEOUT)
self.assertNotIn('no such option', stderr)
self.assertNotIn('Usage: CLI.py', stderr)
self.assertEqual(rc, 0)
# Is the application still running?
opened_windows_titles = common.get_opened_windows_titles()
self.assertNotIn('BleachBit', opened_windows_titles)