test(config-settings): add byte-exact golden-output render harness
The three config-settings draw functions are hand-tuned layout code whose field widths, dash counts and pad targets are arithmetic on the width tier. Any refactor of them has to be byte-identical, and nothing until now could prove that. Renders all 356 page/width/scope/cursor-row combinations -- every page at COLUMNS 100/88/84/70, both scopes, cursor on every row, plus the inline editor -- and byte-compares against a committed baseline. The golden holds raw output: set_color escapes and box drawing exactly as emitted, plus the wrap-aware \e[<N>A\e[J erase config-settings.fish would emit for each panel, and per case the declared panel height against the measured line count. Nothing is normalized; the gate is cmp(1) and a one-space change anywhere fails it. Proven by four deliberate mutations, one per draw function plus a panel-height change, each caught and reverted. Runs entirely inside a throwaway HOME/XDG_CONFIG_HOME sandbox: the fixtures must be real universal variables, and this repo doubles as a live ~/.config/fish. fish --no-config cannot be used, as -N silently degrades set -U to global scope. Every utility is called through `command`, since the config itself shadows rm, cat and mkdir and aliases cp -i.
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
#!/usr/bin/env fish
|
||||
# Copyright (C) 2026 Rootiest
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
#
|
||||
# Golden-output harness for the config-settings TUI renderer.
|
||||
#
|
||||
# fish tests/config-settings-render.fish compare against the golden
|
||||
# fish tests/config-settings-render.fish --update rewrite the golden
|
||||
#
|
||||
# __config_settings_draw, __config_settings_draw_subcat and
|
||||
# __config_settings_draw_value are hand-tuned layout code: field widths, dash
|
||||
# counts and pad targets are arithmetic on the width tier, verified by eye once
|
||||
# and never since. Any refactor of them has to be byte-identical, so this
|
||||
# renders every page at every width tier, in both scopes, with the cursor on
|
||||
# every row, and byte-compares the result against a committed baseline.
|
||||
#
|
||||
# The golden holds RAW output: the set_color escapes and box drawing exactly as
|
||||
# the draw functions emit them, plus the \e[<N>A\e[J erase config-settings.fish
|
||||
# would emit for that panel. Nothing is normalized, folded or pretty-printed --
|
||||
# the gate is cmp(1) over the bytes, and a one-space change anywhere fails it.
|
||||
# `diff` is only used to *display* a failure, through cat -v.
|
||||
#
|
||||
# Everything runs inside a throwaway HOME/XDG_CONFIG_HOME sandbox. The fixtures
|
||||
# have to be real universal variables (the Universal page reads universal scope
|
||||
# through `set --show`), and this repo doubles as a live ~/.config/fish whose
|
||||
# fish_variables must never be touched by a test run. `fish --no-config` is not
|
||||
# an option here: under -N, `set -U` silently degrades to global scope, which
|
||||
# would render the Universal page as all-DEFAULT and prove nothing.
|
||||
#
|
||||
# Every external utility below is called through `command`. The outer pass runs
|
||||
# under the user's real config, which shadows rm (-> trash), cat (-> bat) and
|
||||
# mkdir, and aliases cp to `cp -i` -- a bare `cp` over an existing golden would
|
||||
# sit waiting for a confirmation that never comes.
|
||||
|
||||
set -l self (command realpath (status filename))
|
||||
set -l repo (command realpath (command dirname $self)/..)
|
||||
set -l golden $repo/tests/golden/config-settings-render.txt
|
||||
|
||||
# ╭──────────────────────────────────────────────────────────────────────────╮
|
||||
# │ Outer pass: sandbox, run the render, compare or update │
|
||||
# ╰──────────────────────────────────────────────────────────────────────────╯
|
||||
if not set -q CS_RENDER_OUT
|
||||
set -l sandbox (command mktemp -d)
|
||||
command mkdir -p $sandbox/home $sandbox/xdg/fish
|
||||
set -l out (command mktemp)
|
||||
set -l errf (command mktemp)
|
||||
|
||||
# TERM is pinned so set_color emits a fixed sequence set regardless of the
|
||||
# terminal this runs from; COLUMNS is set per case inside the render pass.
|
||||
command env -i \
|
||||
HOME=$sandbox/home \
|
||||
XDG_CONFIG_HOME=$sandbox/xdg \
|
||||
PATH="$PATH" \
|
||||
TERM=xterm-256color \
|
||||
CS_RENDER_OUT=$out \
|
||||
fish $self >/dev/null 2>$errf
|
||||
set -l render_status $status
|
||||
command rm -rf $sandbox
|
||||
|
||||
if test $render_status -ne 0
|
||||
echo " FAIL render pass exited $render_status"
|
||||
command cat $errf >&2
|
||||
command rm -f $out $errf
|
||||
exit 1
|
||||
end
|
||||
|
||||
set -l cases (command grep -c '^### ' $out)
|
||||
if contains -- --update $argv
|
||||
command mkdir -p (command dirname $golden)
|
||||
command cp $out $golden
|
||||
echo "golden updated: $cases cases, "(command wc -l <$golden | string trim)" lines, "(command wc -c <$golden | string trim)" bytes"
|
||||
command rm -f $out $errf
|
||||
exit 0
|
||||
end
|
||||
|
||||
if not test -f $golden
|
||||
echo " FAIL no golden at $golden -- run with --update to create it"
|
||||
command rm -f $out $errf
|
||||
exit 1
|
||||
end
|
||||
|
||||
if command cmp -s $out $golden
|
||||
echo "$cases/$cases config-settings render cases byte-identical"
|
||||
command rm -f $out $errf
|
||||
exit 0
|
||||
end
|
||||
|
||||
echo " FAIL rendering differs from $golden"
|
||||
# cat -v only to make the escapes readable in the report; the gate above is
|
||||
# a raw byte compare, never this.
|
||||
command diff (command cat -v $golden | psub) (command cat -v $out | psub) | command head -40
|
||||
command rm -f $out $errf
|
||||
exit 1
|
||||
end
|
||||
|
||||
# ╭──────────────────────────────────────────────────────────────────────────╮
|
||||
# │ Render pass (inside the sandbox) │
|
||||
# ╰──────────────────────────────────────────────────────────────────────────╯
|
||||
# Sourced, not autoloaded: XDG_CONFIG_HOME points at the empty sandbox. Only
|
||||
# the draw path is ever called -- __config_settings_apply and
|
||||
# __config_settings_set_value are defined here and never invoked.
|
||||
for f in $repo/functions/__config_settings_*.fish
|
||||
source $f
|
||||
end
|
||||
|
||||
set -l toggle_vars \
|
||||
__fish_config_op_aliases \
|
||||
__fish_config_op_autoexec \
|
||||
__fish_config_op_overrides \
|
||||
__fish_config_op_integrations \
|
||||
__fish_config_op_logging \
|
||||
__fish_config_op_greeting \
|
||||
__fish_config_opinionated
|
||||
|
||||
set -l categories $toggle_vars[1..6]
|
||||
|
||||
# ── Fixtures ──────────────────────────────────────────────────────────────
|
||||
# Chosen so every badge branch is live somewhere in the golden: an explicit
|
||||
# truthy (ON), an explicit falsy (OFF), and an unset variable (DEFAULT). The
|
||||
# session values deliberately differ from the universal ones so the two pages
|
||||
# cannot render identically by accident.
|
||||
set -U __fish_config_op_aliases on
|
||||
set -U __fish_config_op_autoexec off
|
||||
set -U __fish_config_op_integrations on
|
||||
set -U __fish_config_op_logging off
|
||||
set -U __fish_config_opinionated off
|
||||
# __fish_config_op_overrides, __fish_config_op_greeting: unset -> DEFAULT
|
||||
|
||||
set -g __fish_config_op_aliases off
|
||||
set -g __fish_config_op_overrides on
|
||||
set -g __fish_config_op_greeting off
|
||||
|
||||
# Sub-category fixtures. "Notifications" is the 13-char label the subcat page's
|
||||
# label field was widened for; multiplexer-capture exercises the slug's '-'->'_'
|
||||
# rewrite into a variable name.
|
||||
set -U __fish_config_op_aliases_filesystem on
|
||||
set -U __fish_config_op_aliases_search off
|
||||
set -U __fish_config_op_integrations_notifications on
|
||||
set -U __fish_config_op_logging_multiplexer_capture off
|
||||
set -g __fish_config_op_aliases_search on
|
||||
|
||||
# Value-page fixtures: one per type badge (INT / LIST / PATH) plus unset rows
|
||||
# for DEFAULT, plus a value long enough to force `string shorten`'s ellipsis at
|
||||
# every tier.
|
||||
set -U sponge_delay 5
|
||||
set -U sponge_purge_only_on_exit true
|
||||
set -U sponge_allow_previously_successful false
|
||||
set -U __fish_sponge_extra_sensitive KOPIA_PASSWORD MY_CORP_AUTH
|
||||
# sponge_successful_exit_codes: unset -> DEFAULT
|
||||
set -U __fish_scrollback_history_dir /home/tester/very/long/scrollback/history/directory
|
||||
set -U __fish_user_dots_path /home/tester/.config/.user-dots/fish
|
||||
set -U __fish_user_dots_symlink false
|
||||
# __fish_scrollback_history_max_files: unset -> DEFAULT
|
||||
|
||||
# ── Case emitter ──────────────────────────────────────────────────────────
|
||||
# panel_h is passed in by the caller, mirroring __cs_dispatch_draw in
|
||||
# config-settings.fish: the category list and both value pages are a fixed 16
|
||||
# lines, a sub-category page is 7 + <sub-category count>. The golden records
|
||||
# the declared height, the measured line count, and the erase sequence derived
|
||||
# from the declared height -- so flattening the fixed/dynamic divergence, or
|
||||
# changing a page's height at all, breaks the compare three ways.
|
||||
function _cs_render_case --argument-names label panel_h
|
||||
set -l cmd $argv[3..]
|
||||
set -l t (command mktemp)
|
||||
$cmd >$t
|
||||
set -l lines (command wc -l <$t | string trim)
|
||||
|
||||
# Wrap-aware erase, byte-for-byte what config-settings.fish emits for a
|
||||
# panel of this height at this width in the steady state (last_cols ==
|
||||
# COLUMNS). 78 is the widest box (IW=76 + 2 borders).
|
||||
set -l pml (math --scale=0 "($COLUMNS + 78) / 2")
|
||||
set -l eh (math --scale=0 "$panel_h * max(1, ceil($pml / $COLUMNS))")
|
||||
|
||||
printf '### %s COLUMNS=%d PANEL_H=%d LINES=%d ERASE=' $label $COLUMNS $panel_h $lines
|
||||
printf '\e[%dA\e[J' $eh
|
||||
printf '\n'
|
||||
command cat $t
|
||||
command rm -f $t
|
||||
end
|
||||
|
||||
# ── Cases ─────────────────────────────────────────────────────────────────
|
||||
# 100 -> IW 76, 88 -> IW 72, 84 -> IW 68, 70 -> IW 50: one COLUMNS value per
|
||||
# width tier, each a few columns above its threshold.
|
||||
begin
|
||||
for cols in 100 88 84 70
|
||||
set -g COLUMNS $cols
|
||||
|
||||
for scope in universal session
|
||||
for row in (seq 0 6)
|
||||
_cs_render_case "toggle scope=$scope row=$row" 16 \
|
||||
__config_settings_draw $row $scope $toggle_vars
|
||||
end
|
||||
end
|
||||
|
||||
for category in $categories
|
||||
set -l n (count (__config_settings_subcats $category))
|
||||
for scope in universal session
|
||||
for row in (seq 0 $n)
|
||||
_cs_render_case "subcat cat=$category scope=$scope row=$row" (math 7 + $n) \
|
||||
__config_settings_draw_subcat $row $scope $category
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for row in (seq 0 4)
|
||||
_cs_render_case "value page=sponge row=$row" 16 \
|
||||
__config_settings_draw_value $row sponge
|
||||
end
|
||||
for row in (seq 0 3)
|
||||
_cs_render_case "value page=paths row=$row" 16 \
|
||||
__config_settings_draw_value $row paths
|
||||
end
|
||||
|
||||
# Inline editor: only reachable on non-bool rows. Short buffer, a
|
||||
# buffer long enough to tail-anchor against the caret, and an empty one.
|
||||
_cs_render_case "edit page=sponge row=0 buf=short" 16 \
|
||||
__config_settings_draw_value 0 sponge edit 12
|
||||
_cs_render_case "edit page=sponge row=4 buf=long" 16 \
|
||||
__config_settings_draw_value 4 sponge edit "KOPIA_PASSWORD MY_CORP_AUTH ANOTHER_SECRET_NAME"
|
||||
_cs_render_case "edit page=paths row=0 buf=long" 16 \
|
||||
__config_settings_draw_value 0 paths edit /home/tester/very/long/scrollback/history/directory
|
||||
_cs_render_case "edit page=paths row=2 buf=empty" 16 \
|
||||
__config_settings_draw_value 2 paths edit ""
|
||||
end
|
||||
end >$CS_RENDER_OUT
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user