Selkie.git | xt/ | 04-vcl-shim-equivalence.rakutest


use Test;

# Byte-equivalence between the C shim path and the Raku per-cell
# fallback path through ViewportedCardList.!copy-cells. The shim
# is a direct port of the Raku loop, but "directly" is doing a lot
# of work — empty-cell base substitution, style/channel propagation,
# the egcpool ownership convention. If they ever drift visibly,
# users on prebuilts-without-shim would see a different chat to
# users on the fast path.
#
# Renders go through Selkie::Test::Snapshot's render-to-string from
# child subprocesses for two reasons:
#
#   1. We need to drive both code paths against the same widget
#      tree without stale notcurses state between renders.
#   2. render-to-string + destroy has a known fragility at viewport
#      dimensions roughly above 5×22 (notcurses_stop crashes at
#      process exit on some configurations); fork-per-render
#      isolates that crash to the child without affecting the
#      harness's exit code.
#
# Renders deliberately stay at 5×22 — that's the largest size with
# a green snapshot scenario today. Equivalence at this size is
# enough to verify the shim's per-cell semantics: empty-cell base
# substitution (Border interior), style/channel propagation, the
# egcpool ownership convention. It does NOT exercise wide
# multi-byte glyph runs or massive viewports — those would need
# the upstream notcurses_stop fragility resolved first.

plan 3;

sub render-in-subprocess(Bool $use-shim, Int $rows, Int $cols,
                         @labels, Int $card-h, Int $scroll
                         --> Str)
{
    my $labels-list = @labels.map({ "'$_'" }).join(', ');
    my $script = qq:to/RAKU/;
        use lib 'lib';
        use Selkie::Test::Snapshot;
        use Selkie::Widget::ViewportedCardList;
        use Selkie::Widget::Border;
        use Selkie::Widget::Text;
        use Selkie::Sizing;

        viewported-cardlist-shim-available({$use-shim ?? 'True' !! 'False'});

        my \$vcl = Selkie::Widget::ViewportedCardList.new(sizing => Sizing.flex);
        for ($labels-list) -> \$label \{
            my \$text = Selkie::Widget::Text.new(text => \$label, sizing => Sizing.flex);
            my \$border = Selkie::Widget::Border.new(sizing => Sizing.flex);
            \$border.set-content(\$text);
            \$vcl.add-item(\$text, root => \$border, height => $card-h, :border(\$border));
        \}
        \$vcl.scroll-to($scroll) if $scroll > 0;
        print render-to-string(\$vcl, rows => $rows, cols => $cols);
        RAKU

    my $tmpfile = $*TMPDIR.add("vcl-shim-eq-{$*PID}-{$use-shim}-{rand.Int}.raku");
    $tmpfile.spurt($script);
    LEAVE { $tmpfile.unlink if $tmpfile.e }

    my $proc = run 'raku', '-Ilib', $tmpfile.Str, :out, :err;
    my $out = $proc.out.slurp(:close);
    my $err = $proc.err.slurp(:close);
    if $proc.exitcode != 0 || $out eq '' {
        diag "subprocess (use-shim=$use-shim, $rows×$cols) exit { $proc.exitcode }, out len { $out.chars }";
        diag "  stderr: $err" if $err;
    }
    $out;
}

subtest 'scroll-to(2) on 3 cards × 3 rows in 5×22 viewport — byte-identical' => {
    plan 2;
    my @labels = <alpha beta gamma>;
    my $shim-out = render-in-subprocess(True,  5, 22, @labels, 3, 2);
    my $raku-out = render-in-subprocess(False, 5, 22, @labels, 3, 2);
    is $shim-out.chars, $raku-out.chars,
        "outputs equal length ({$shim-out.chars} chars)";
    ok $shim-out eq $raku-out, "byte-identical glyph output";
};

subtest 'no scroll, all visible — byte-identical' => {
    plan 2;
    my @labels = <one two>;
    my $shim-out = render-in-subprocess(True,  6, 22, @labels, 3, 0);
    my $raku-out = render-in-subprocess(False, 6, 22, @labels, 3, 0);
    is $shim-out.chars, $raku-out.chars,
        "outputs equal length ({$shim-out.chars} chars)";
    ok $shim-out eq $raku-out, "byte-identical with no scroll";
};

subtest 'mid-scroll, partial top + bottom clip — byte-identical' => {
    plan 2;
    my @labels = <a b c d>;
    # 4 cards × 3 = content 12, viewport 6, scroll-to(4) puts us
    # mid-content with both top and bottom cards partially clipped.
    my $shim-out = render-in-subprocess(True,  6, 22, @labels, 3, 4);
    my $raku-out = render-in-subprocess(False, 6, 22, @labels, 3, 4);
    is $shim-out.chars, $raku-out.chars,
        "outputs equal length under mid-scroll ({$shim-out.chars} chars)";
    ok $shim-out eq $raku-out, "byte-identical with partial clip on both ends";
};