ExQuality.Report (ExQuality v0.13.0)

View Source

Builds the machine-readable form of a run.

mix quality exits 0 or 1 for the whole run, which tells a caller that something failed but not what. A script that wants to route on the result - hand the credo findings to one fixer, the test failures to another - would otherwise have to scrape the console or re-run the tools.

A report answers "which stages ran, which failed, and what did they find" from the same results the human output is rendered from, so the two streams can never disagree.

Shape

{
  "version": "0.6.0",
  "status": "error",
  "duration_ms": 48213,
  "profile": "loop",
  "scope": "changed",
  "base_ref": "origin/main",
  "stages": [
    {
      "name": "Credo",
      "status": "error",
      "summary": "5 issues (2 readability, 3 design)",
      "duration_ms": 412,
      "stats": {"issue_count": 5},
      "findings": [
        {
          "file": "lib/user.ex", "line": 42, "column": 3,
          "app": "web", "severity": "info",
          "check": "Credo.Check.Readability.ModuleDoc",
          "message": "Modules should have a @moduledoc tag."
        }
      ]
    },
    {
      "name": "Dialyzer", "status": "skipped",
      "summary": "--quick", "duration_ms": 0,
      "stats": {}, "findings": []
    }
  ]
}

Every stage carries the same keys whatever its status, so a consumer reads one field for the explanation rather than branching: a skipped stage puts its reason in summary, exactly as ExQuality.Stage.skipped/2 records it.

A stage that failed without producing findings carries its tool's full output under output instead, mirroring the human renderer. Findings are the parsed form; output is the fallback, and one of the two is always present for a failure.

How much the run covered

profile, scope and base_ref are always present at the root, null when they do not apply. They are what makes "status": "ok" interpretable: a green run scoped to three test files is not the same claim as a green full run, and a caller that lowers a recorded coverage figure or moves a baseline on a green run has to be able to refuse the narrow one. The test stage repeats them, plus the files it ran, in its own object. See ExQuality.Scope.

Summary

Functions

Builds a report from the results of a run.

Encodes a report as pretty-printed JSON, newline terminated.

Types

t()

@type t() :: %{
  version: String.t(),
  status: String.t(),
  duration_ms: non_neg_integer(),
  profile: String.t() | nil,
  scope: String.t() | nil,
  base_ref: String.t() | nil,
  stages: [map()]
}

Functions

build(results, duration_ms, config \\ [])

@spec build([ExQuality.Stage.result()], non_neg_integer(), keyword()) :: t()

Builds a report from the results of a run.

duration_ms is the wall clock time of the whole run, which is not the sum of the stage durations because the analysis stages run in parallel.

config is the run's loaded config, used for the root profile and for the scope when the test stage did not run to report one of its own.

iex> alias ExQuality.{Report, Stage}
iex> report = Report.build([Stage.skipped("Dialyzer", "--quick")], 12)
iex> {report.status, report.duration_ms, length(report.stages)}
{"ok", 12, 1}

iex> alias ExQuality.{Report, Stage}
iex> report = Report.build([Stage.skipped("Tests", "--skip test")], 12, profile: :loop)
iex> {report.profile, report.scope}
{"loop", "all"}

encode!(report)

@spec encode!(map()) :: String.t()

Encodes a report as pretty-printed JSON, newline terminated.

iex> ExQuality.Report.encode!(%{status: "ok"})
"{\n  \"status\": \"ok\"\n}\n"