ExQuality.Custom (ExQuality v0.13.0)

View Source

Project-defined stages, declared in .quality.exs under custom:.

A project with a house check, a schema linter, a custom mix task or a shell script gate could have ExQuality's parallelism, timing, report and printer, or it could have its own check, but not both. Custom stages are the way to have both, and a check that runs inside mix quality is a check the JSON report can route on.

Two layers, one mechanism:

custom: [
  # A command. This is the case most projects want.
  [
    key: :nullability,
    name: "Nullability",
    command: "mix",
    args: ["schema.nullability", "--format", "json"],
    env: [{"MIX_ENV", "test"}],
    kind: :reader
  ],

  # A module, for anything the command form cannot express.
  [key: :house_rules, name: "House rules", module: MyApp.Quality.HouseRules]
]

Every entry is a keyword list with key and name, plus either module or command. Registration data lives in the entry rather than in callbacks on a module so that the config file alone says what a run will contain: a stage that only announces itself once it has run cannot be reported as skipped, and a run that says nothing about a stage reads as a run where the stage passed.

Module entries

module: names a module exporting run/1 and returning an ExQuality.Stage.result(), which is the contract every built-in stage already satisfies. It may also export stage_kind/1; a module that does not is a :reader. See ExQuality.Stage.

Command entries

command: is run by ExQuality.Stages.Command, which documents the options and the JSON finding contract.

What custom stages are not for

Filling gaps in built-in stages. Routing a second mix credo config through a custom command would work and would be a mistake: the output comes back as text instead of per-check findings, and per-check routing is the reason the JSON report exists. If a built-in stage cannot express something its tool supports, that is a bug in the stage.

Summary

Functions

Returns whether an entry reads the build or writes to it.

Returns the function that runs an entry, given the run's config.

Returns why a custom stage will not run, or nil when it will.

Returns the custom stage entries in a loaded config, in declaration order.

Raises unless every custom entry in config is well formed.

Functions

kind(entry, config)

@spec kind(keyword(), keyword()) :: ExQuality.Stage.kind()

Returns whether an entry reads the build or writes to it.

An entry that declares kind: is taken at its word. Otherwise a module entry is asked, via ExQuality.Stage.kind/2, and a command entry is a :reader.

iex> ExQuality.Custom.kind([key: :a, name: "A", command: "true"], [])
:reader

runner(entry)

@spec runner(keyword()) :: (keyword() -> ExQuality.Stage.result())

Returns the function that runs an entry, given the run's config.

skip_reason(config, entry)

@spec skip_reason(keyword(), keyword()) :: String.t() | nil

Returns why a custom stage will not run, or nil when it will.

--skip <key> is read through ExQuality.Config.skip_reason/2, exactly as a built-in stage's switch is. enabled: false in the entry itself is the config-file spelling.

stages(config)

@spec stages(keyword()) :: [keyword()]

Returns the custom stage entries in a loaded config, in declaration order.

iex> ExQuality.Custom.stages([])
[]

validate!(config)

@spec validate!(keyword()) :: :ok

Raises unless every custom entry in config is well formed.

Custom stages are the place a config file can be wrong in ways that silently weaken a run - a stage that never registers is a check nobody is told is not running - so this fails the run at load time and names the offending entry rather than letting it go quiet.