Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@andre-vauvelle
Copy link

Summary

Fixes a bug where only the first test from each unaffected file was skipped, instead of all tests in that file.

The Bug

When running tach test --base HEAD (no changes), only 1 test per file was being skipped:

collected 8 items
[Tach] Skipped 1 test file (1 tests) since they were unaffected by current changes.
============================== 7 passed ===============================

Root Cause

In pytest_collection_modifyitems, after processing the first test from a file:

  1. The path was added to seen
  2. Subsequent tests from the same file hit if item.path in seen: continue
  3. This skipped the removal logic, leaving those tests in the collection

The Fix

Use a two-pass approach:

  1. First pass: Identify all paths that should be removed (without modifying the list)
  2. Second pass: Filter out all items from those paths at once using list comprehension

After Fix

collected 8 items
[Tach] Skipped 1 test file (8 tests) since they were unaffected by current changes.
============================ no tests ran =============================

Testing

Tested with a minimal example project:

  • No changes: All 8 tests correctly skipped ✓
  • With changes to module: All 8 tests correctly run ✓

Previously, only the first test from each unaffected file was skipped.
This was due to the `if item.path in seen: continue` check skipping
the removal logic for subsequent tests in the same file.

The fix uses a two-pass approach:
1. First pass identifies all paths that should be removed
2. Second pass filters out all items from those paths at once

Before: 8 tests collected, 1 skipped, 7 passed (bug)
After:  8 tests collected, 8 skipped, no tests ran (correct)
Copy link
Collaborator

@DetachHead DetachHead left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the contribution!

Comment on lines +61 to +91
def pytest_collection_modifyitems(
session: pytest.Session,
config: TachConfig,
items: list[pytest.Item],
):
handler = config.tach_handler
seen: set[Path] = set()
for item in copy(items):
if not item.path:
continue
if str(item.path) in handler.removed_test_paths:
handler.num_removed_items += 1
items.remove(item)
continue
if item.path in seen:
continue
paths_to_remove: set[Path] = set()

if str(item.path) in handler.all_affected_modules:
# If this test file was changed,
# then we know we need to rerun it
seen.add(item.path)
# First pass: determine which paths should be removed
for item in items:
if not item.path or item.path in seen:
continue

if handler.should_remove_items(file_path=item.path.resolve()):
handler.num_removed_items += 1
items.remove(item)
handler.remove_test_path(item.path)

seen.add(item.path)

if str(item.path) in handler.removed_test_paths:
paths_to_remove.add(item.path)
elif str(item.path) not in handler.all_affected_modules:
# If this test file was not changed, check if it should be removed
if handler.should_remove_items(file_path=item.path.resolve()):
paths_to_remove.add(item.path)
handler.remove_test_path(item.path)

# Second pass: remove all items from paths marked for removal
original_count = len(items)
items[:] = [
item for item in items
if not item.path or item.path not in paths_to_remove
]
handler.num_removed_items = original_count - len(items)

Copy link
Collaborator

@DetachHead DetachHead Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we can simplify this by using a pytest_collect_file hook instead:

class HasTachConfig(Protocol):
    config: TachConfig

@pytest.hookimpl(wrapper=True)
def pytest_collect_file(
    file_path: Path, parent: HasTachConfig
) -> Generator[None, list[pytest.Collector], list[pytest.Collector]]:
    handler = parent.config.tach_handler
    # skip any paths that already get filtered out by other hook impls
    result = yield
    if result and handler.should_remove_items(file_path=file_path.resolve()):
        handler.num_removed_items += 1
        handler.remove_test_path(file_path)
        return []
    return result

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants