Skip to content

Narrow a mutation's snippet to the function it names #276

Description

@fniessink

A Mutation names a module, and its snippet must occur exactly once in the whole file. That forces snippets to be
long enough to be unique across the file rather than clear about what they change, and it leaves a registration's
target saying less than a reader expects. Letting a mutation name a function, and searching that function's source
instead of the file, fixes both.

What happens today

Mutation.check reads the file the module was loaded from and requires the snippet to be there exactly once:

        if (occurrences := source.count(self.old)) != 1:
            return Result(Outcome.STALE, f"the snippet occurs {occurrences} times rather than once")

That rule is what stops a mutation breaking a line it was not aimed at, and it is worth keeping. What it costs is
visible in this repository: if metadata is None: occurs twice in src/update_time/sources/pypi.py, so the
registration on test_new_version_of_a_release_without_project_urls has to quote two lines to be unique, though
only the first is the line it means.

The target is a module even where the test invokes a function. Of the four registrations of the two pypi.py
mutations, three name a module whose snippet sits in a function the test never calls, so naming the function the
test does call would be misleading rather than helpful — unless the snippet is required to be inside it.

What would change

A mutation may name a callable as well as a module. The module is derived from the callable, which is reliable
through decorators: get_latest_version carries three and release_metadata is cached, and __module__ reports
update_time.sources.pypi for both. Where a callable is named, the snippet is searched in its source rather than in
the file, and a snippet absent from it is stale, exactly as a snippet absent from the file is today.

just mutate gains the same narrowing. Without it the two tools disagree: a snippet unique within a function but
repeated in the file is stale to the probe and fine to the registration, which breaks the loop this repository works
by — probe with just mutate, read what it killed, paste it into a registration.

What to settle

  1. Which targets. A function and a method, certainly. A class? A property is not a function, so a mutation
    naming one would have to reach its fget.
  2. What the window holds. inspect.getsourcelines returns the decorator lines along with the function:
    get_latest_version's source starts at @publication_date_reporting. So a mutation could aim at a decorator,
    which may be wanted or may be a surprise worth excluding.
  3. How the narrowed replacement is spliced back, since the module executed for the run is still the whole file.
  4. How just mutate names the function on the command line, and what it reports when the snippet is in the file
    but outside the function named.
  5. Whether module targets stay. They should: a snippet in a private helper that no test names has no callable to
    point at.

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions