App-karr

 view release on metacpan or  search on metacpan

t/167-env-octet-crossing.t  view on Meta::CPAN

use strict;
use warnings;
use Test::More;
use Encode qw( encode_utf8 decode FB_CROAK LEAVE_SRC );
use Path::Tiny qw( path );

use App::karr::Encoding qw(
    to_octets to_octets_for_env
    from_octets from_octets_from_env
);

# Ticket #167: %ENV is a byte boundary karr did not own. A non-ASCII prompt
# assigned at Foundation/Runner.pm:63 emitted "Wide character in setenv" on
# foundation's stderr — the bytes the child received were correct UTF-8
# either way, so this is a warning, not a data bug. The fix routes both
# sides of the crossing through App::karr::Encoding: to_octets_for_env on
# the way out, from_octets_from_env on the way back in.

my $NON_ASCII = "Caf\x{e9} \x{2014} na\x{ef}ve prompt \x{1f914}";

subtest 'to_octets_for_env is the character-to-octet edge for %ENV' => sub {
    # The function is documented as the ENV-crossing edge, not a new codec.
    # Behavioural equivalence to to_octets is the property — name carries the
    # intent at the call site.
    is( to_octets_for_env($NON_ASCII), to_octets($NON_ASCII),
        'to_octets_for_env and to_octets agree on a non-ASCII input' );
    is( to_octets_for_encoded($NON_ASCII), encode_utf8($NON_ASCII),
        'to_octets_for_env returns the canonical UTF-8 octets' );
    is( to_octets_for_env(undef), undef, 'undef passes through' );
    is( to_octets_for_env(''), '', 'empty string passes through' );
};

subtest 'from_octets_from_env is the octet-to-character edge for %ENV' => sub {
    is( from_octets_from_env(encode_utf8($NON_ASCII)), $NON_ASCII,
        'from_octets_from_env decodes the same UTF-8 octets back to characters' );
    is( from_octets_from_env(undef), undef, 'undef passes through' );

    # A non-UTF-8 byte string: from_octets' "return unchanged" rule applies
    # here too — passing the bytes through silently turns a wrong-bytes-in,
    # same-wrong-bytes-out into a wrong-bytes-in, mojibake-out.
    my $raw = "\xff\xfe\xfd";
    is( from_octets_from_env($raw), $raw,
        'non-UTF-8 bytes are returned unchanged' );
};

# The symptom the ticket describes: assigning a character string to %ENV
# emits "Wide character in setenv" on STDERR. Wrap the assignment in a
# $SIG{__WARN__} trap and assert that the helper's call site stays silent.
subtest 'assigning through to_octets_for_env does not warn' => sub {
    my @warnings;
    local $SIG{__WARN__} = sub { push @warnings, $_[0] };

    local $ENV{KARR_TEST_PROMPT} = to_octets_for_env($NON_ASCII);

    my @wide = grep { /Wide character/ } @warnings;
    is( scalar @wide, 0, 'no "Wide character in setenv" warning' )
        or diag "warnings emitted: @warnings";

    # The bytes stored in %ENV are the encoded form, not the characters —
    # that is what the child process receives.
    is( $ENV{KARR_TEST_PROMPT}, encode_utf8($NON_ASCII),
        '%ENV holds the singly-encoded UTF-8 octets' );
    ok( defined eval { decode( 'UTF-8', $ENV{KARR_TEST_PROMPT}, FB_CROAK | LEAVE_SRC ) },
        'the stored bytes are valid UTF-8' );
    isnt( $ENV{KARR_TEST_PROMPT}, encode_utf8(encode_utf8($NON_ASCII)),
        'no double encoding' );
};

# Round-trip the whole crossing: characters → encode → store → read → decode →
# characters. This is the read side the ticket asked to settle. In karr's
# own code no Perl reader of KARR_REPO / KARR_ROLE / PROMPT exists today
# (the shell expands them), so a read site has to be fabricated to exercise
# the helper; the helper itself is what the rule gap asked for.
subtest 'round-trip through %ENV yields the original characters' => sub {
    my @warnings;
    local $SIG{__WARN__} = sub { push @warnings, $_[0] };

    # Write side, the way Foundation/Runner.pm now does it.
    local $ENV{KARR_TEST_ROUNDTRIP} = to_octets_for_env($NON_ASCII);



( run in 1.751 second using v1.01-cache-2.11-cpan-364913b4093 )