module

Krikri::ResultDisplay

ResultDisplay - Handles displaying task results and diffs

Constants

DEPRECATION_CLAIM_MUTEX = Mutex.new

Serializes "claim the dedupe key, then print" across the fibers of a --forks run: the claim is the Display-level dedupe every emitter shares, and a replayed block claims late.

TOP_LEVEL_FACT_DEPRECATION_TEXT = "INJECT_FACTS_AS_VARS default to `True` is deprecated, top-level facts will not be auto injected after the change. This feature will be removed from ansible-core version 2.24."

Class methods

adhoc_oneline=(adhoc_oneline : Bool)

Display an ad-hoc ansible command's result, matching ansible's own default ("minimal") callback: host | STATUS | rc=N >> followed by raw stdout for command-shaped modules (rc + stdout present - command/shell/script/raw), or host | STATUS => {...} pretty-printed JSON for every other module. Deliberately NOT ResultDisplay.display_result - that one renders ansible-playbook's own "ok: [host]" TASK-recap style, a different output convention ansible's ad-hoc CLI has never used. Ad-hoc-only output modifiers, set by krikri.cr (the ansible counterpart binary) from its own CLI flags:

-o/--one-line switches to Ansible's deprecated oneline callback shape - everything on ONE line: command-shaped results render as host | STATUS | rc=N | (stdout) ... with newlines escaped (verified against ansible-core 2.19.4's plugins/callback/oneline.py), everything else renders the result JSON compact (indent=0, newlines stripped) instead of pretty. There is no krikri-playbook equivalent to mirror - the playbook CLI has no -o.

Source
adhoc_oneline?

Display an ad-hoc ansible command's result, matching ansible's own default ("minimal") callback: host | STATUS | rc=N >> followed by raw stdout for command-shaped modules (rc + stdout present - command/shell/script/raw), or host | STATUS => {...} pretty-printed JSON for every other module. Deliberately NOT ResultDisplay.display_result - that one renders ansible-playbook's own "ok: [host]" TASK-recap style, a different output convention ansible's ad-hoc CLI has never used. Ad-hoc-only output modifiers, set by krikri.cr (the ansible counterpart binary) from its own CLI flags:

-o/--one-line switches to Ansible's deprecated oneline callback shape - everything on ONE line: command-shaped results render as host | STATUS | rc=N | (stdout) ... with newlines escaped (verified against ansible-core 2.19.4's plugins/callback/oneline.py), everything else renders the result JSON compact (indent=0, newlines stripped) instead of pretty. There is no krikri-playbook equivalent to mirror - the playbook CLI has no -o.

Source
adhoc_result_json(result : JSON::Any, module_name : String | Nil = nil, oneline : Bool = false) : String

The exact JSON string Ansible's ad-hoc stdout callbacks dump after the host | STATUS => prefix. The result dict the callbacks see has already been through Ansible's cleaning pipeline by the time it is dumped: executor/task_result.py's as_callback_task_result strips failed/skipped for EVERY stdout callback (a real FAILED! => dump does not contain "failed": true - live-verified against 2.19.4), and _dump_results strips the private _ansible_* keys and dumps with sort_keys=True. A debug task additionally goes through _clean_results - but ONLY in the minimal (pretty) path: oneline.py never calls it, so an -o debug dump keeps its other keys (verified live). Status derivation stays with the caller - stripping here is display-only.

Source
adhoc_state_and_color(changed : Bool, failed : Bool, unreachable : Bool) : Tuple(String, String)

Returns {state word, SGR color code} for an ad-hoc result, mirroring Ansible's minimal/oneline callbacks: unreachable wins over failed, failed over changed. Public so the state-to-color mapping (especially unreachable's distinct bright red, not plain red) can be regression-tested without a live host.

Source
adhoc_tree_dir

-t/--tree DIR: additionally log each result as pretty JSON in DIR/<hostname>, like Ansible's tree callback plugin.

Source
adhoc_tree_dir=(adhoc_tree_dir : String | Nil)

-t/--tree DIR: additionally log each result as pretty JSON in DIR/<hostname>, like Ansible's tree callback plugin.

Source
assert_that_position(lines : Array(String), start_idx : Int32, index : Int32) : Tuple(Int32, Int32) | Nil

Position (1-based line, 1-based column) of assert's that: item number index (0-based) within the source, scanned from start_idx. Public so the templating-delimiters deprecation emitter can point assert's per-item Origin without going through the ErrorBlock text form.

Source
begin_deferred_fact_deprecations

ansible-core captures every warning and deprecation raised while a task runs into that task's own result, and a DYNAMIC INCLUDE's result then goes through the strategy's "included:" path, which never displays them - so a when: clause reading a top-level fact warns when it SKIPS the include and stays silent when the include RUNS (live-verified against 2.21.6 for include_tasks and include_role, issue #974: nycrecords.nginx's passing when: ansible_os_family == 'RedHat' printed nothing while its five skipping siblings each printed a block). Hold the readings here until the verdict is known; #finish_deferred_fact_deprecations drops them when the include runs.

Source
capture_stderr

Runs block with stderr captured on the current fiber and returns what it produced; the caller decides whether/when to emit it.

Source
capture_stderr_warnings

Runs block with warning output captured and returns exactly the text that would have gone to STDERR (empty when nothing warned). A deprecation block raised in the window is dropped unclaimed - a caller that will replay the output wants #capture_stderr.

Source
conditional_value_origin(path : String | Nil, source_line : Int32, key : String, name : String | Nil = nil) : String | Nil

The Origin of a top-level fact read inside a CONDITIONAL clause (when:/until:/...). Ansible tags every YAML scalar with its own position and evaluates a conditional's list ITEM as one templating operation, so a fact read inside a block-sequence clause reports that item's scalar token (- ansible_distribution == "CentOS"), never the when: key line (live-verified vs 2.21.6 on jetune.docker's initialize.yml, which is the shape issue #990 caught at the key line). A clause written on the key line itself (when: x == 'y') keeps the plain param-VALUE position. name is the fact being read, so the item that actually named it wins; a lone item is taken as-is (a wrapped scalar can name the fact on a continuation line), and a multi-item list with no naming item falls back to the key-line value position rather than guessing which item real was evaluating.

Source
consume_pending_module_deprecations(result : JSON::Any, module_name : String | Nil) : Nil

Emits the stashed removed-param deprecations for this module unless the result says the module never ran (skip, unreachable, connection failure) or died with an uncaught exception (Ansible's crash wrapper drops the collected deprecations - live-verified vs 2.19.11 with openssl_pkcs12's maciter_size). Consumes the stash either way.

Source
deferring_fact_deprecations?

Whether a deferral scope is open on this fiber, so a caller can tell an emission that will actually reach the console from one that a running include drops (see VarSubstitutor.emit_fact_deprecation).

Source
display_adhoc_result(host : Host, result : JSON::Any, diff_mode : Bool = false, module_name : String | Nil = nil) : Nil
Source
display_attribute_diff(diff : JSON::Any) : Nil

Display attribute diff (for file attributes like mode, owner). Ansible's callback renders a DICT diff exactly like a content diff: both sides are pretty-printed as sorted 4-space-indented JSON and fed through the unified differ (live-verified vs 2.19.11 --diff: --- before / +++ after / @@ -1,5 +1,5 @@ with the JSON lines as +/-/context), not a per-key - key: value listing.

Source
display_content_diff(diff : JSON::Any) : Nil

Display content diff (for file content changes)

Source
display_diff(diff : JSON::Any) : Nil

Display diff (delegates to specific diff types). Ansible's callback prints a file diff BEFORE the task's status line - the diff block, one blank line, then changed: [host] (live-verified 2.19.11) - and starts it immediately after the TASK banner with no leading blank of its own.

Source
display_diff_entry(diff : JSON::Any) : Nil
Source
display_result(host : Host, result : JSON::Any, diff_mode : Bool, item_label : String | Nil = nil, ignore_errors : Bool = false, no_log : Bool = false, module_name : String | Nil = nil, delegate_target : String | Nil = nil, source_task : Task | Nil = nil, loop_item : JSON::Any | Nil = nil, loop_var_name : String | Nil = nil) : Nil

Display task result with appropriate formatting. item_label is set for looped tasks, rendering ok: [host] => (item=x) to match how Ansible annotates per-iteration output. source_task carries the task's parsed source position and module identity so a failed result can render ansible-core 2.19's [ERROR]: Task failed: block (see ErrorBlock) before the fatal/ failed line; nil (or a task without a parsed position) suppresses the block.

Source
dump_oneline_json(result : JSON::Any) : String

Public shape of the oneline callback's own dump (indent=0, so separators (",", ": ")): keys sorted, no space after commas, ": " between key and value. Used by Krikri::Oneline for the censored no_log shape; module results go through adhoc_result_json instead (which adds the _ansible_verbose_always indent flip on top).

Source
dump_suffix(value : JSON::Any) : String

Single-line sorted JSON dump at -v/-vv; Ansible's pretty 4-space-indent shape from -vvv up. Shared by every skip suffix helper.

Source
emit_action_warning(text : String) : Nil

An action-plugin warning raised while validating task args (the service plugin's Ignoring "X" as it is not used in "systemd" for its UNUSED_PARAMS): one console line per distinct message per run, exactly like Ansible's Display.warning dedupe - which is global across sources, so this shares the same set as every other [WARNING] emitter (see emit_display_warning).

Source
emit_core_deprecation(text : String) : Nil

One core-side param deprecation (the module bootstrap's removed_in_version warning, or a result's _ansible_core_deprecations entry) on stderr - deduped per distinct text, with the one-time "can be disabled" hint before the first one. Also called directly from the argspec validator (a removed param deprecates when the module's run returns - see validate's deprecation gate).

Source
emit_deprecation_with_origin(text : String, origin : String, payload : String | Nil = nil) : Nil

A core deprecation carrying ansible-core 2.21's Origin block: the warning line, then Origin: <origin>, then a blank line, then - only when payload is given - the payload line and another blank line. Live-captured from 2.21.6 for a deprecated callback selected from the environment: the env-origin form repeats the configured value as a payload, while a config-file origin prints no payload. Deduped on the whole emitted shape, like emit_core_deprecation.

Source
emit_display_warning(text : String) : Nil

Ansible's format_event ends a multi-line warning with a blank line (it appends "\n\n" when the message contains a newline, "\n" otherwise), so a multi-line [WARNING] block is followed by one blank stderr line - byte-verified against 2.19.11.

Source
emit_module_redirect_deprecation(module_name : String | Nil, origin : String | Nil = nil) : Nil

A collection-level plugin deprecation (a community.mysql -> ansible.mysql redirect, community.general.proxmox_kvm's moved-content notice): one [DEPRECATION WARNING] console block per distinct message per run, exactly like Ansible's Display. nil module names (pseudo-tasks) and non-deprecated modules are no-ops.

Source
emit_or_defer_deprecation(dedupe : Set(String), key : String, lines : Array(String)) : Nil

The deprecation counterpart of the funnel above: with a capture active the block is held back (see PendingDeprecation), otherwise it claims its dedupe key and prints - unchanged for every path that never batches.

Source
emit_play_hosts_deprecation(origin : String | Nil) : Nil
Source
emit_top_level_fact_deprecation(origin : String | Nil) : Nil
Source
emit_warning_stderr(text : String) : Nil

Shared stderr funnel for the warning emitters (display warnings and the jinja-constant block alike): appends the exact would-be stderr text, newlines included, to the innermost active capture instead of writing STDERR when one is active on the current fiber.

Source
error_origin_context(task : Task) : String | Nil

Public so TaskExecutor's own unreachable report (report_unreachable) can emit the same [ERROR]: Task failed: block ansible-playbook prints before an unreachable host's fatal line - that report lives outside ResultDisplay's result-dump path.

Source
failed_final_rename_split(msg : String) : Tuple(String, String) | Nil

The failed-final-rename detail's one safe split: the atoms neither the src nor the stage path can carry ("Errno" inside a bracketed errno segment text) decide where ": [Errno N] ..." begins - rindex because the errno text itself can contain further ": b'...'" colons. Returns {segment sans errno, errno text}; nil when msg is not this shape (including a detail whose errno could not be derived, which renders plain - the same split would be a no-op anyway). Both src spellings ride real's atomic_move: the copy module's crash echoes its staged source as Python bytes (b'...'), while authorized_key echoes its /tmp mkstemp source as a plain repr ('...' - issue #819). Public only so the unit suite can pin the split directly, the same reason task_error_chain is.

Source
finish_deferred_fact_deprecations(flush : Bool) : Nil
Source
include_vars_arg_cause_origin(task : Task, msg : String) : String | Nil

The caused-by Origin of an include_vars argument-validation failure. Ansible raises those with the OFFENDING VALUE as the exception's obj (include_vars.py), so its event carries that value's own source context and the chain stays two-segment instead of collapsing into the header. Two shapes pass an obj: the unknown-option loop tags the KEY (obj=arg - "<key> is not a valid option in include_vars", caret on the key) and _set_args' non-list check tags the VALUE (obj=self.valid_extensions - "The 'extensions' option must be a list.", caret on the value). The file/dir mixing error passes the whole args mapping (obj=self._task.args), which yields no usable origin, so its block stays collapsed. Live-verified against ansible-playbook 2.21.6; best-effort like the scans above, so an option written inline (include_vars: {flie: x}) keeps the collapsed shape rather than pointing at a wrong position.

Source
loop_source_origin(path : String | Nil, source_line : Int32, key : String, name : String | Nil = nil) : String | Nil

The Origin of a top-level fact read inside a LOOP SOURCE. Ansible renders a loop's items in order and pins the deprecation at the list ELEMENT that named the fact - its own scalar token (- "{{ ansible_os_family }}.yml"), never at the <key>: line - live-verified vs 2.21.6 on mullholland.ntp's with_first_found, whose candidates are block-sequence elements (issue #956). name is the fact being read, so the element that actually named it wins; nil when no element of the source names it.

Source
loop_unreachable_error_node(msg : String, origin : String | Nil, local_origin : Bool) : ErrorBlock::Node

The [ERROR] block that precedes a LOOPED task's per-item ssh-unreachable failed: line (issue #275), whose shape follows the ORIGIN host's own connection:

  • origin over ssh: real prints the collapsed single segment - just the "Task failed: Failed to connect ..." brief carrying the task's Origin, no second segment, no cause section. Observed on the #275 round's real ssh-origin repro.
  • ansible_connection=local origin: the two-segment chain - "Task failed." with the task Origin, then the bare ssh failure under "<<< caused by >>>". The bare ssh failure keeps its own empty events list in the chained form so collapse never folds the two segments back into one (it mirrors the ssh connection plugin's own exception event). Public only so the unit suite can pin both shapes directly (same reason task_error_chain is).
Source
mapping_key_origin(task : Task, key : String) : String | Nil

Origin context of a mapping key named key inside a task (set_fact's variable name, include_vars' option): the key's own line/column inside the task, found by scanning the task's source lines for the key followed by ':' (a quoted key points at the opening quote, like Ansible's per-key Origin). Public so TaskExecutor's set_fact reserved-name warning (issued from merge_ansible_facts, outside this result-dump path) can reuse the same per-key Origin scan.

Source
module_crash_result?(result : JSON::Any) : Bool

The result shape of an uncaught module exception (the plugins' module_crash_result/unhandled_error mimicry): Ansible's module wrapper builds that result WITHOUT the deprecations the module bootstrap collected, so _ansible_core_deprecations riding on it must not be displayed (live-verified vs 2.19.11 with openssl_pkcs12's maciter_size: the crash shows no [DEPRECATION WARNING] while the same params' fail_json and success results do).

Source
param_value_origin(path : String | Nil, source_line : Int32, key : String, list_entry_key : Bool = false) : String | Nil
Source
python_json_dump(result : JSON::Any) : String

Serializes result the way Ansible dumps a failed task's JSON: keys sorted alphabetically at every level, single line, Python's json.dumps default separators (", " between items, ": " after keys).

Source
python_pretty_json(value : JSON::Any) : String

Python's json.dumps(obj, sort_keys=True, indent=4) plus the trailing "\n" the callback appends when it serializes dict diffs (callback/init.py _serialize_diff). The trailing newline is what keeps the unified differ from ever emitting a "\ No newline at end of file" marker for a dict diff (issue #687).

Source
python_repr(value : JSON::Any) : String

Python's repr() of a value, matching how Ansible renders a loop item in its failed:/changed: display (True/False/None, single-quoted strings/dict-keys, insertion-order dict {k: v}, [..] lists) - distinct from the JSON dump used for the result object, which stays lower-case true/false.

Source
replay_deprecation(pending : PendingDeprecation) : Nil

Replays one held-back block when its task is finally reached: the claim happens here, so an origin a dropped capture merely prepared is still free to print for the read that really happens.

Source
show_recap(hosts : Array(Host), results : Hash(String, Hash(String, Int32))) : Nil

Show recap of all host results, matching ansible-playbook's v2_playbook_on_stats byte-for-byte: host column padded to 26 plain (37 when colorized, padding applied AROUND the ANSI-wrapped name the way Ansible's %-37s does), then " : ", then the seven counters each shaped lead=%-4s and joined with single spaces - so every counter carries trailing padding, including the last one.

Source
show_unified_diff(before : String, after : String) : Nil

Show the body of a content diff, built by the same algorithm ansible's callback uses (difflib, not the system diff command): the two pick different matching blocks for some inputs, so only difflib's hunk layout matches ansible's stdout byte for byte (issue #736 - martin-micimo.haproxy's haproxy.cfg was the first role where the same 34->35 line pair rendered differently).

Source
skip_line_suffix(when_text : String | Nil = nil, item : JSON::Any | Nil = nil, all_skipped : Bool = false) : String

ansible-playbook -v and above append a small JSON dump to every "skipping:" line. A when:-false skip carries the RAW when: condition under "false_condition" (a literal YAML false stays an unquoted false, anything written as a string keeps its quotes); a looped skip adds the item; a fully-skipped loop's trailing line carries {"msg": "All items skipped"} (live-verified vs 2.19.11). Default verbosity prints no dump at all.

Source
skip_line_suffix_all_skipped(changed : Bool = true) : String

A fully-skipped loop's trailing line dump. Real's shape carries "changed": false only when the per-item results themselves carried changed (every module EXCEPT debug does - a skipped debug item's displayed dump is just {false_condition, item}, live-verified vs 2.19.11 across debug/fail/set_fact/assert/file/command/include_*).

Source
skip_line_suffix_debug(when_text : String | Nil, item : JSON::Any, idx : Int32, loop_var : String | Nil, index_var : String | Nil) : String

The -v dump for a when:-false skipped item of a debug: task: real shows ONLY {false_condition, item} - with the item under the CUSTOM loop_var name when loop_control sets one, plus the index_var binding when loop_control sets one - not the full cleaned skip result every other module gets (live-verified vs 2.19.11 across debug/fail/set_fact/assert/file/command/include_tasks/include_role, with and without loop_control.loop_var/index_var).

Source
skip_line_suffix_empty_loop

A loop whose source resolved to ZERO items skips with its own dump shape - {"skipped_reason": "No items in the list"} - distinct from both the false_condition skip and the All items skipped aggregate (live-verified vs 2.19.11).

Source
skip_result_suffix(result : JSON::Any, item : JSON::Any | Nil = nil, loop_var_name : String | Nil = nil) : String

The -v suffix for a skipped result that carries a full module payload (check-mode skips, plugin-side skipped: results): real dumps the whole cleaned result - the "skipped"/"failed" flags and the "invocation" block are absent from the visible dump at -v/-vv (invocation only shows at -vvv; live-verified vs 2.19.11). A looped skip's dump additionally carries ansible_loop_var + the item, same as ok/changed loop dumps.

Source
skipped_result_dump(false_condition : String | Nil) : JSON::Any

The -v dump for a when:-false skipped item/task of a NON-debug module: the full cleaned skip result - changed, skip_reason, and false_condition (loop items additionally get ansible_loop_var + the item via skip_result_suffix) (live-verified vs 2.19.11).

Source
stash_pending_module_deprecations(module_name : String, texts : Array(String)) : Nil
Source
task_error_chain(module_name : String | Nil, msg : String, origin : String) : ErrorBlock::Node

The cause chain ansible-core 2.19 builds for each failure class, as an ErrorBlock event tree rooted at the task-level AnsibleTaskError ("Task failed."):

  • template (its action plugin re-raises the loader's file-not-found inside except without raise ... from): the two-segment handling chain - live-verified against 2.19.11.
  • fail/assert (action-level failures): "Action failed." + the result message.
  • copy's controller-side src miss (its action raises AnsibleActionFail(result=result) from ex with an empty message, so the type name becomes the middle segment): collapsed chain carrying Ansible's exact wording.
  • every other module-level failure: "Module failed." + the result message (the module API's own wrapper), collapsed. Public (not private) only so the unit suite can pin each module's chain shape directly, the way the other display helpers here are.
Source
task_param_value_origin(task : Task, key : String) : String | Nil

The Origin of a task param's VALUE (the position Ansible's AnsibleActionFail obj= attaches as the failing event's source context - e.g. add_host's groups: 5 points at the 5, column of the value, not the key). Same best-effort text scan as mapping_key_origin above, but the caret lands on the first non-space character after key:. Public (same reason as mapping_key_origin) so the lookup dispatch's Jinja-const embedded-template warning - emitted from inside the expression evaluator while THIS param is being finalized, and the evaluator otherwise has no route to the task (issue #259) - can produce the same value-position Origin real's warning carries. When the caller already carries just the param's source position (the evaluator, which can't name Task at all - see VarSubstitutor#current_task_param), use param_value_origin.

Source
update_stats(stats : Hash(String, Int32), result : JSON::Any, ignore_errors : Bool = false) : Nil

Update stats based on task result. A failure with ignore_errors: yes still displays as failed (see display_result) but doesn't count toward the host's failure tally - matching Ansible, where an ignored failure doesn't fail the play or the process exit code.

Source

Nested types