# UNDF: UNDF-2026-000000039
# UNDF: (pending)
# cpython-0001: unittest.mock.Mock.reset_mock — O(N²) visited list in diamond mock tree traversal

## CWE-407 — Algorithmic Complexity

| Field | Value |
|-------|-------|
| ID | cpython-0001 |
| Severity | MEDIUM |
| Ecosystem | cpython |
| Package | unittest.mock |
| File | `Lib/unittest/mock.py` |
| Lines | 638–667 |
| Complexity | O(N²) list membership, O(N) with set |
| Hot path | `mock.reset_mock()` on large/deeply-nested MagicMock trees |

## Defect

`reset_mock` uses a `list` for cycle detection when traversing the mock object
tree. On each recursive call, `id(self) in visited` performs an O(N) linear
scan of all previously visited mock IDs. For a mock tree with N nodes (children
plus return_value chains), the total membership-check cost is O(1+2+…+N) =
O(N²).

MagicMock auto-creates child mocks on attribute access, so test suites that
build large spec-based or deeply-patched mocks can trigger this path. A mock
with 1,000 children (e.g. a module-level patch with many methods) causes ~500k
comparisons instead of ~1k.

```python
# BEFORE — O(N²): list membership O(N) per recursive call
def reset_mock(self, visited=None, *, return_value=False, side_effect=False):
    if visited is None:
        visited = []          # list, not set
    if id(self) in visited:   # O(N) linear scan
        return
    visited.append(id(self))  # O(1) append but scan above is O(N)

    for child in self._mock_children.values():
        if isinstance(child, _SpecState) or child is _deleted:
            continue
        child.reset_mock(visited, ...)   # recurse, visited grows

    ret = self._mock_return_value
    if _is_instance_mock(ret) and ret is not self:
        ret.reset_mock(visited)
```

## Fix

Replace the `list` with a `set`. Integer `id` values hash in O(1) and set
membership is O(1) average.

```python
# AFTER — O(N): set membership O(1) per call
def reset_mock(self, visited=None, *, return_value=False, side_effect=False):
    if visited is None:
        visited = set()        # set, not list
    if id(self) in visited:    # O(1) hash lookup
        return
    visited.add(id(self))      # O(1) insert
    # ... rest unchanged
```

## Speedup

| Mock nodes (N) | Before (ops) | After (ops) | Speedup |
|----------------|--------------|-------------|---------|
| 100 | 5,050 | 100 | 50× |
| 500 | 125,250 | 500 | 250× |
| 1,000 | 500,500 | 1,000 | 500× |
| 2,000 | 2,001,000 | 2,000 | 1,000× |

## Notes

- The same pattern appears in `_mock_check_sig` / other recursive helpers; only
  `reset_mock` is flagged here as the primary confirmed path.
- `id` values are unique integers per live object — set storage is safe.
- No semantic change: cycle detection behaviour is identical.
- Fix applies to CPython main as of depth-1 clone (HEAD ~May 2025).
