Purpose

In a hexagonal-architecture project the tests tree is expected to shadow the src tree: a test for src/contexts/orders/domain/entities/order.py lives at tests/contexts/orders/domain/entities/test_order.py. This rule enforces that every directory under tests has a counterpart under src, so the test suite can never invent a layout of its own or leave a stale directory behind after the source it covered was moved.

Configuration

rules:
  - name: tests-mirror-src
    type: mirror
    description: The tests tree mirrors the src tree
    source: src
    mirror: tests
    direction: mirror-to-source
    mirror_extra_allowed: [doubles, fixtures, conftest]

source and mirror name the two trees. direction: mirror-to-source requires every directory under tests to have a matching directory under src — the source drives the layout, and the tests tree adds no structure the source does not already have.

The direction runs this way on purpose: a test's location is determined by the source module it covers, so the mirror side must not diverge. The reverse is not required — not every source directory needs a test, so a source module without a test is fine. mirror_extra_allowed exempts test-only scaffolding directories (doubles, fixtures, conftest) from needing a source counterpart.

Both examples below share this source tree:

src/
└── contexts/
    └── orders/
        └── domain/
            ├── entities/
            │   └── order.py
            └── errors/
                └── order_not_found_error.py

Violation Example

The tests tree mirrors entities and errors, but adds a services directory the source tree never defined — an orphan with no source counterpart:

tests/
└── contexts/
    └── orders/
        └── domain/
            ├── entities/
            │   └── test_order.py
            ├── errors/
            │   └── test_order_not_found_error.py
            └── services/                    # no src/contexts/orders/domain/services
                └── test_pricing.py

Passing Example

Drop the orphan services directory. A test-only doubles directory may stay even though src has no such directory, because doubles is listed in mirror_extra_allowed:

tests/
└── contexts/
    └── orders/
        └── domain/
            ├── doubles/                     # exempt via mirror_extra_allowed
            │   └── fake_order_repository.py
            ├── entities/
            │   └── test_order.py
            └── errors/
                └── test_order_not_found_error.py

Output

$ psl check
tests/contexts/orders/domain/services
    [tests-mirror-src] The tests tree mirrors the src tree
    no source counterpart 'src/contexts/orders/domain/services'

Found 1 violation(s).