Async-Redis

 view release on metacpan or  search on metacpan

t/lib/Test/Async/Redis.pm  view on Meta::CPAN

    }
    skip_all("Redis not available at " . redis_host() . ":" . redis_port() . ": $@");
}

# Skip if no Redis available - returns connected Redis object
sub skip_without_redis {
    return $test_redis if $test_redis;

    my $redis = eval {
        my $r = Async::Redis->new(
            host => redis_host(),
            port => redis_port(),
            connect_timeout => 2,
        );
        run { $r->connect };
        $r;
    };
    if ($redis) {
        $test_redis = $redis;
        return $redis;
    }
    skip_all("Redis not available at " . redis_host() . ":" . redis_port() . ": $@");
}

# Clean up test keys
async sub cleanup_keys {
    my ($redis, $pattern) = @_;
    my $keys = await $redis->keys($pattern);
    return unless @$keys;
    await $redis->del(@$keys);
}

# Test with timeout wrapper
async sub with_timeout {
    my ($timeout, $future) = @_;
    my $timeout_f = Future::IO->sleep($timeout)->then(sub {
        Future->fail("Test timeout after ${timeout}s");
    });
    return await Future->wait_any($future, $timeout_f);
}

# Assert Future fails with specific error type
sub fails_with {
    my ($future, $error_class, $message) = @_;
    my $error;
    eval { run { $future } } or $error = $@;
    ok($error && ref($error) && $error->isa($error_class), $message)
        or diag("Expected $error_class, got: " . (ref($error) || $error // 'undef'));
}

# Async delay
async sub delay {
    my ($seconds) = @_;
    await Future::IO->sleep($seconds);
}

# Force EOF on the socket under the reader. Next read returns 0 bytes,
# triggering the reader's EOF handling path. Uses shutdown() rather
# than close() so the file descriptor stays valid for Future::IO's
# select loop — close() on an fh that Future::IO has an active poller
# on leaves a stale watcher whose fileno is undef, which taints select()
# with uninit warnings.
sub inject_eof {
    my ($redis) = @_;
    shutdown($redis->{socket}, 2) if $redis->{socket};
}

# Feed bytes directly into the parser, bypassing the socket. Useful
# for exercising the reader's decode/dispatch path with a crafted frame.
sub inject_unexpected_frame {
    my ($redis, $raw_bytes) = @_;
    $redis->{parser}->parse($raw_bytes) if $redis->{parser};
}

# Synthesize a fatal timeout directly. Routes through the detach-first
# _reader_fatal path so the typed Async::Redis::Error::Timeout is
# propagated to all inflight futures (not a generic cancellation).
sub force_read_timeout {
    my ($redis) = @_;
    require Async::Redis::Error::Timeout;
    $redis->_reader_fatal(Async::Redis::Error::Timeout->new(
        message => "synthetic timeout for test",
        timeout => 0,
    ));
}

1;

__END__

=head1 NAME

Test::Async::Redis - Test utilities for Async::Redis

=head1 SYNOPSIS

    use Test::Lib;
    use Test::Async::Redis ':redis';
    use Future::AsyncAwait;

    # Tests auto-skip if Redis unavailable
    # Use run {} to execute async code in tests:

    my $result = run {
        my $redis = Async::Redis->new;
        await $redis->connect;
        await $redis->set('key', 'value');
        await $redis->get('key');
    };
    is($result, 'value', 'got value');

    # Or get Redis from skip_without_redis:
    my $redis = skip_without_redis();
    my $pong = run { await $redis->ping };
    is($pong, 'PONG', 'ping works');

=head1 DESCRIPTION

Test utilities for Async::Redis using async/await. Uses Future::IO's
built-in default implementation (IO::Poll based) for event loop management.



( run in 3.667 seconds using v1.01-cache-2.11-cpan-7fcb06a456a )