Mutare.Selector (mutare v0.4.1)

Copy Markdown View Source

Runtime selection of the active mutant.

The active mutant is constant for an entire suite run, so it is read once from the environment in the sandbox mix.exs prefix, before target project code, and stashed in :persistent_term (O(1) reads, built for write-once/read-many). Every selector site in the metamutant reads this key, normally hoisted to one read per function activation.

Eligible functions run their uninstrumented source when the active mutation belongs elsewhere: a lifted function through a copy of its original clauses, a function that stays in place through a second body chosen after its one read. Pure direct self-recursion may stay in a lifted function's clean clauses without rereading the selector; it relies on selection remaining stable during the computation. Ordinary entries and effectful recursion still read afresh, so put/1 can select another mutant between in-process calls.

A schema mutant is stored as {root_relative_file, local_id} in that single slot, the file as an atom (namespace_key/1): every instrumented function compares it on entry, and an atom compares in one instruction where a path string of equal length is read byte by byte. Mutare.Metamutant projects the slot to the local integer for the active file, 0 at baseline, or :inactive for other files. Thus only one file can be active and inactive files still short-circuit coverage before its tracking-flag read. put/1 and active/0 speak the string form. Standalone transforms continue to select by plain integer, including :start_id. The bootstrap combines MUTARE_ACTIVE_MUTANT (the local integer) with MUTARE_MUTANT_NAMESPACE (the file); the runner translates report ids before setting these. An absent namespace retains standalone integer selection.

This module defines both sides of that contract: Mutare.Metamutant uses its key and baseline when building selectors, while Mutare.Sandbox renders bootstrap_ast/0 into the target project's dependency-free project prefix, with an idempotent fallback in the test bootstrap.

Self-hosting: a private key for the suite-under-test

When the target is Mutare, the suite-under-test contains Mutare's own tests, many of which drive selection directly (put/1, building fixtures via Mutare.Transform) to exercise the machinery. If those tests wrote the same :persistent_term slot the harness uses to hold the mutant-under-test, they would clobber it mid-run — the active mutant would silently revert to baseline and every mutant whose killing test ran afterwards would register a false survivor (see NOTES.md, "Self-hosting").

So the runtime key is configurable: key/0 returns default_key/0 (:mutare_active, the harness key) unless override_env/0 names another, in which case the suite plays in that private slot. Mutare.Sandbox.Command sets that env var (to suite_key/0) on every sandbox mix it spawns, so the suite-under-test reads/writes :mutare_active__suite while the real metamutant — whose sites and bootstrap bake default_key/0 as literals at transform time, in the harness process where the override is unset — keeps reading :mutare_active. The two never collide. On a normal target there is no Mutare.Selector compiled in, so the env is inert.

A private key per process

Ahead of both, a process may hold a private key in its dictionary (process_key/0). key/0 returns it there, so a metamutant transformed in that process bakes it into its selector sites and put/1/active/0 read and write it: two processes holding different private keys select independently. Mutare.Test.isolate_selector/0 gives each execution of an ExUnit test module one, which is what lets tests that drive selection run async: true.

Summary

Functions

The active mutant id for in-process execution (0 if unset).

The baseline id (no mutant active).

Dependency-free code that reads the selector environment variables and stores the active runtime identity. Initialization precedes target project code; repeated project prefixes and umbrella helpers preserve the first value, even if target code later changes the environment.

The harness selection key (:mutare_active) — the default key/0, env-independent.

The environment variable a runner sets to pick the active mutant.

Selection environment, explicitly clearing an inherited namespace for integer ids.

The :persistent_term key the metamutant reads at runtime.

Environment variable carrying a schema mutant's root-relative file namespace.

The form a file namespace takes in the stored selection and in the projection generated code matches it against (Mutare.Metamutant.subject_ast/1): an atom, or the string itself when it is too long to be one. bootstrap_ast/0 applies the same rule inside the target.

Env var a sandbox run sets to give the suite-under-test a private selection key.

The process-dictionary key under which a process holds a private selection key, read by key/0 ahead of the environment. Set it before transforming a metamutant the process will select in, and in every process that selects (Mutare.Test.isolate_selector/0).

Set the active mutant id directly for in-process execution.

The private key (override_env/0's value) the suite-under-test selects on under dogfooding.

Functions

active()

@spec active() :: Mutare.RuntimeId.t()

The active mutant id for in-process execution (0 if unset).

baseline()

@spec baseline() :: non_neg_integer()

The baseline id (no mutant active).

bootstrap_ast()

@spec bootstrap_ast() :: Macro.t()

Dependency-free code that reads the selector environment variables and stores the active runtime identity. Initialization precedes target project code; repeated project prefixes and umbrella helpers preserve the first value, even if target code later changes the environment.

Mutare.Sandbox renders this AST directly into project prefixes and the test bootstrap fallback, so the target does not need Mutare as a dependency.

default_key()

@spec default_key() :: atom()

The harness selection key (:mutare_active) — the default key/0, env-independent.

env_var()

@spec env_var() :: String.t()

The environment variable a runner sets to pick the active mutant.

environment(id)

@spec environment(Mutare.RuntimeId.t()) :: [{String.t(), String.t() | nil}]

Selection environment, explicitly clearing an inherited namespace for integer ids.

key()

@spec key() :: atom()

The :persistent_term key the metamutant reads at runtime.

The calling process's private key (process_key/0) where it holds one; else the key override_env/0 names — the knob self-hosting needs so the suite-under-test does not clobber the harness's active-mutant slot (see the moduledoc); else default_key/0 (:mutare_active).

namespace_env()

@spec namespace_env() :: String.t()

Environment variable carrying a schema mutant's root-relative file namespace.

namespace_key(namespace)

@spec namespace_key(String.t()) :: atom() | String.t()

The form a file namespace takes in the stored selection and in the projection generated code matches it against (Mutare.Metamutant.subject_ast/1): an atom, or the string itself when it is too long to be one. bootstrap_ast/0 applies the same rule inside the target.

override_env()

@spec override_env() :: String.t()

Env var a sandbox run sets to give the suite-under-test a private selection key.

process_key()

@spec process_key() :: atom()

The process-dictionary key under which a process holds a private selection key, read by key/0 ahead of the environment. Set it before transforming a metamutant the process will select in, and in every process that selects (Mutare.Test.isolate_selector/0).

put(id)

@spec put(Mutare.RuntimeId.t()) :: :ok

Set the active mutant id directly for in-process execution.

suite_key()

@spec suite_key() :: String.t()

The private key (override_env/0's value) the suite-under-test selects on under dogfooding.