Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion pyclean/erase.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ def delete_filesystem_objects(
all_names = sorted(directory.glob(path_glob), reverse=True)
if Runner.ignore:
all_names = [n for n in all_names if not Runner.is_ignored(n)]
if not all_names:
return
log.debug('Erase file system objects matching: %s', path_glob)
dirs = (name for name in all_names if name.is_dir() and not name.is_symlink())
files = (name for name in all_names if not name.is_dir() or name.is_symlink())

Expand Down Expand Up @@ -87,5 +90,4 @@ def remove_freeform_targets(
object is shown (unless the ``--yes`` option is used, in addition).
"""
for path_glob in glob_patterns:
log.debug('Erase file system objects matching: %s', path_glob)
delete_filesystem_objects(directory, path_glob, prompt=not yes, dry_run=dry_run)
47 changes: 21 additions & 26 deletions tests/test_erase.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

"""Tests for the erase module."""

import logging
from argparse import Namespace
from pathlib import Path
from unittest.mock import call, patch
Expand Down Expand Up @@ -219,38 +220,32 @@ def test_confirm_no(mock_input):
assert confirm('Test message') is False


def test_path_is_ignored_for_dir_itself():
def test_erase_ignored_dir_produces_no_output(tmp_path, caplog):
"""
Does Runner.is_ignored return True for an ignored directory itself?
"""
pyclean.main.Runner.ignore = ['allure-results']
assert pyclean.main.Runner.is_ignored(Path('allure-results'))

Does erase suppress all output for ignored directories?

def test_path_is_ignored_for_file_in_ignored_dir():
"""
Does Runner.is_ignored return True for a file inside an ignored directory?
When a glob pattern matches only ignored paths, the erase feature
should not produce any log output about those patterns (regression
test for the --ignore flag not fully suppressing --erase targets).
"""
pyclean.main.Runner.ignore = ['allure-results']
assert pyclean.main.Runner.is_ignored(Path('allure-results/foo.txt'))

ignored_dir = tmp_path / 'foo'
ignored_dir.mkdir()
(ignored_dir / 'file.txt').write_text('test')
(ignored_dir / 'sub').mkdir()
(ignored_dir / 'sub' / 'deep.txt').write_text('test')

def test_path_is_ignored_for_nested_path_in_ignored_dir():
"""
Does Runner.is_ignored return True for a deeply nested path inside an ignored
directory?
"""
pyclean.main.Runner.ignore = ['allure-results']
assert pyclean.main.Runner.is_ignored(Path('allure-results/sub/deep/file.txt'))
args = Namespace(dry_run=True, ignore=['foo'])
pyclean.main.Runner.configure(args)

with caplog.at_level(logging.DEBUG):
remove_freeform_targets(tmp_path, ['foo/**/*', 'foo'], yes=True, dry_run=True)

def test_path_is_not_ignored_for_unrelated_path():
"""
Does Runner.is_ignored return False for a path not matching any ignore pattern?
"""
pyclean.main.Runner.ignore = ['allure-results']
assert not pyclean.main.Runner.is_ignored(Path('keep.txt'))
assert not pyclean.main.Runner.is_ignored(Path('other/foo.txt'))
assert pyclean.main.Runner.unlink_count == 0
assert pyclean.main.Runner.rmdir_count == 0
for record in caplog.records:
assert 'foo' not in record.message, (
f'Ignored pattern should not appear in output: {record.message!r}'
)


def test_delete_filesystem_objects_skips_ignored_dirs(tmp_path):
Expand Down
96 changes: 96 additions & 0 deletions tests/test_ignore.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# SPDX-FileCopyrightText: 2020 Peter Bittner <django@bittner.it>
#
# SPDX-License-Identifier: GPL-3.0-or-later

"""Tests for the ignore module."""

import platform

import pytest

from pyclean.ignore import normalize, should_ignore


@pytest.mark.parametrize(
('path_str', 'patterns', 'expected'),
[
# Simple name matches
('foo/bar', ['bar'], True),
('foo/baz/bar', ['bar'], True),
('bar', ['bar'], True),
('foo/bar', ['baz'], False),
# Path matches
('foo/bar', ['foo/bar'], True),
('baz/foo/bar', ['foo/bar'], True),
('test/foo/bar', ['foo/bar'], True),
('foo/bar/baz', ['foo/bar'], True), # Subdirectories are also ignored
('bar/foo', ['foo/bar'], False),
('foo/baz', ['foo/bar'], False),
# Multiple patterns
('foo/bar', ['baz', 'bar'], True),
('foo/bar', ['baz', 'foo/bar'], True),
('foo/bar', ['test', 'data'], False),
# Edge cases
('bar', ['foo/bar'], False),
('foo', ['foo/bar'], False),
# Pattern longer than path
('baz', ['foo/bar/baz'], False),
('bar/baz', ['foo/bar/baz'], False),
# None/empty patterns
('foo/bar', None, False),
('foo/bar', [], False),
# Subdirectories of ignored paths
('foo/bar/baz/deep', ['foo/bar'], True),
('src/foo/bar/models', ['foo/bar'], True),
('foo/bar/baz', ['foo/bar/baz'], True),
],
)
def test_should_ignore(path_str, patterns, expected):
"""
Does should_ignore correctly match path patterns?
"""
result = should_ignore(path_str, patterns)
assert result == expected


@pytest.mark.skipif(platform.system() != 'Windows', reason='Windows-specific test')
@pytest.mark.parametrize(
('path_str', 'patterns', 'expected'),
[
# Windows-style pattern (backslash) matching filesystem paths
('foo/bar', [r'foo\bar'], True),
('foo/bar/baz', [r'foo\bar'], True),
('src/foo/bar', [r'foo\bar'], True),
],
)
def test_should_ignore_windows_paths(path_str, patterns, expected):
"""
Does should_ignore correctly handle Windows-style backslash patterns?
This test only runs on Windows where backslash is a path separator.
"""
result = should_ignore(path_str, patterns)
assert result == expected


@pytest.mark.skipif(
platform.system() not in ['Linux', 'Darwin'],
reason='Unix-specific test',
)
def test_normalize_pattern_posix():
"""
Does normalize preserve patterns on Unix?
On Unix, backslash can be part of a filename.
"""
assert normalize('foo/bar') == 'foo/bar'
assert normalize(r'foo\bar') == r'foo\bar' # Preserved on Linux and macOS
assert normalize('bar') == 'bar'


@pytest.mark.skipif(platform.system() != 'Windows', reason='Windows-specific test')
def test_normalize_pattern_windows():
"""
Does normalize convert backslashes to forward slashes on Windows?
"""
assert normalize('foo/bar') == 'foo/bar'
assert normalize(r'foo\bar') == 'foo/bar' # Normalized
assert normalize('bar') == 'bar'
34 changes: 34 additions & 0 deletions tests/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,37 @@ def test_dryrun(
assert not mock_real_rmdir.called
assert mock_dry_unlink.called
assert mock_dry_rmdir.called


def test_is_ignored_for_dir_itself():
"""
Does Runner.is_ignored return True for an ignored directory itself?
"""
pyclean.main.Runner.ignore = ['allure-results']
assert pyclean.main.Runner.is_ignored(Path('allure-results'))


def test_is_ignored_for_file_in_ignored_dir():
"""
Does Runner.is_ignored return True for a file inside an ignored directory?
"""
pyclean.main.Runner.ignore = ['allure-results']
assert pyclean.main.Runner.is_ignored(Path('allure-results/foo.txt'))


def test_is_ignored_for_nested_path_in_ignored_dir():
"""
Does Runner.is_ignored return True for a deeply nested path inside an ignored
directory?
"""
pyclean.main.Runner.ignore = ['allure-results']
assert pyclean.main.Runner.is_ignored(Path('allure-results/sub/deep/file.txt'))


def test_is_not_ignored_for_unrelated_path():
"""
Does Runner.is_ignored return False for a path not matching any ignore pattern?
"""
pyclean.main.Runner.ignore = ['allure-results']
assert not pyclean.main.Runner.is_ignored(Path('keep.txt'))
assert not pyclean.main.Runner.is_ignored(Path('other/foo.txt'))
88 changes: 0 additions & 88 deletions tests/test_traversal.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,15 @@

"""Tests for the traversal module."""

import platform
from argparse import Namespace
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest.mock import call, patch

import pytest
from conftest import SymlinkMock

import pyclean.main
from pyclean.bytecode import BYTECODE_DIRS, BYTECODE_FILES
from pyclean.ignore import normalize, should_ignore
from pyclean.traversal import descend_and_clean


Expand Down Expand Up @@ -55,91 +52,6 @@ def test_skip_ignored_directories(mock_log):
mock_log.debug.assert_called_with('Skipping %s', '.git')


@pytest.mark.parametrize(
('path_str', 'patterns', 'expected'),
[
# Simple name matches
('foo/bar', ['bar'], True),
('foo/baz/bar', ['bar'], True),
('bar', ['bar'], True),
('foo/bar', ['baz'], False),
# Path matches
('foo/bar', ['foo/bar'], True),
('baz/foo/bar', ['foo/bar'], True),
('test/foo/bar', ['foo/bar'], True),
('foo/bar/baz', ['foo/bar'], True), # Subdirectories are also ignored
('bar/foo', ['foo/bar'], False),
('foo/baz', ['foo/bar'], False),
# Multiple patterns
('foo/bar', ['baz', 'bar'], True),
('foo/bar', ['baz', 'foo/bar'], True),
('foo/bar', ['test', 'data'], False),
# Edge cases
('bar', ['foo/bar'], False),
('foo', ['foo/bar'], False),
# Pattern longer than path
('baz', ['foo/bar/baz'], False),
('bar/baz', ['foo/bar/baz'], False),
# None/empty patterns
('foo/bar', None, False),
('foo/bar', [], False),
# Subdirectories of ignored paths
('foo/bar/baz/deep', ['foo/bar'], True),
('src/foo/bar/models', ['foo/bar'], True),
('foo/bar/baz', ['foo/bar/baz'], True),
],
)
def test_should_ignore(path_str, patterns, expected):
"""
Does should_ignore correctly match path patterns?
"""
result = should_ignore(path_str, patterns)
assert result == expected


@pytest.mark.skipif(platform.system() != 'Windows', reason='Windows-specific test')
@pytest.mark.parametrize(
('path_str', 'patterns', 'expected'),
[
# Windows-style pattern (backslash) matching filesystem paths
('foo/bar', [r'foo\bar'], True),
('foo/bar/baz', [r'foo\bar'], True),
('src/foo/bar', [r'foo\bar'], True),
],
)
def test_should_ignore_windows_paths(path_str, patterns, expected):
"""
Does should_ignore correctly handle Windows-style backslash patterns?
This test only runs on Windows where backslash is a path separator.
"""
result = should_ignore(path_str, patterns)
assert result == expected


@pytest.mark.skipif(
platform.system() not in ['Linux', 'Darwin'],
reason='Unix-specific test',
)
def test_normalize_pattern_posix():
"""
Does normalize preserve patterns on Unix?
On Unix, backslash can be part of a filename.
"""
assert normalize('foo/bar') == 'foo/bar'
assert normalize(r'foo\bar') == r'foo\bar' # Preserved on Linux and macOS
assert normalize('bar') == 'bar'


@pytest.mark.skipif(platform.system() != 'Windows', reason='Windows-specific test')
def test_normalize_pattern_windows():
"""
Does normalize convert backslashes to forward slashes on Windows?
"""
assert normalize('foo/bar') == 'foo/bar'
assert normalize(r'foo\bar') == 'foo/bar' # Normalized
assert normalize('bar') == 'bar'


def test_ignore_with_simple_name():
"""
Does --ignore with a simple name match directories anywhere?
Expand Down