Hypersonic

 view release on metacpan or  search on metacpan

t/0029-sse.t  view on Meta::CPAN

{
    my $builder = XS::JIT::Builder->new;

    $builder->line('#include <string.h>')
      ->blank;

    Hypersonic::SSE->generate_c_code($builder);

    XS::JIT->compile(
        code      => $builder->code,
        name      => 'Hypersonic::SSE',
        functions => Hypersonic::SSE->get_xs_functions,
    );
}

# ============================================================
# Test 4-7: Protocol::SSE Perl-side formatting
# ============================================================
subtest 'format_event basic' => sub {
    plan tests => 4;
    
    my $event = Hypersonic::Protocol::SSE->format_event(
        type => 'message',
        data => 'Hello World',
    );
    
    like($event, qr/^event: message\n/, 'has event type');
    like($event, qr/data: Hello World\n/, 'has data');
    like($event, qr/\n\n$/, 'ends with blank line');
    unlike($event, qr/id:/, 'no id when not specified');
};

subtest 'format_event with id' => sub {
    plan tests => 2;
    
    my $event = Hypersonic::Protocol::SSE->format_event(
        type => 'update',
        data => 'test',
        id   => '123',
    );
    
    like($event, qr/id: 123\n/, 'has id');
    like($event, qr/event: update\n/, 'has event type');
};

subtest 'format_event multiline data' => sub {
    plan tests => 3;
    
    my $event = Hypersonic::Protocol::SSE->format_event(
        data => "line1\nline2\nline3",
    );
    
    like($event, qr/data: line1\n/, 'first line');
    like($event, qr/data: line2\n/, 'second line');
    like($event, qr/data: line3\n/, 'third line');
};

subtest 'format helpers' => sub {
    plan tests => 3;
    
    my $keepalive = Hypersonic::Protocol::SSE->format_keepalive();
    like($keepalive, qr/^: keepalive\n\n$/, 'keepalive format');
    
    my $retry = Hypersonic::Protocol::SSE->format_retry(3000);
    like($retry, qr/^retry: 3000\n\n$/, 'retry format');
    
    my $comment = Hypersonic::Protocol::SSE->format_comment('test');
    like($comment, qr/^: test\n\n$/, 'comment format');
};

# ============================================================
# Test 8-11: Protocol::SSE C code generation
# ============================================================
subtest 'gen_event_formatter C code' => sub {
    plan tests => 5;
    
    require XS::JIT::Builder;
    my $builder = XS::JIT::Builder->new;
    
    Hypersonic::Protocol::SSE->gen_event_formatter($builder);
    my $code = $builder->code;
    
    like($code, qr/format_sse_event/, 'function defined');
    like($code, qr/event: %s/, 'event type format');
    like($code, qr/data: /, 'data format');
    like($code, qr/id: %s/, 'id format');
    like($code, qr/while \(\*p\)/, 'multiline loop');
};

subtest 'gen_keepalive C code' => sub {
    plan tests => 2;
    
    require XS::JIT::Builder;
    my $builder = XS::JIT::Builder->new;
    
    Hypersonic::Protocol::SSE->gen_keepalive($builder);
    my $code = $builder->code;
    
    like($code, qr/format_sse_keepalive/, 'function defined');
    like($code, qr/: keepalive/, 'keepalive comment');
};

subtest 'gen_retry C code' => sub {
    plan tests => 2;
    
    require XS::JIT::Builder;
    my $builder = XS::JIT::Builder->new;
    
    Hypersonic::Protocol::SSE->gen_retry($builder);
    my $code = $builder->code;
    
    like($code, qr/format_sse_retry/, 'function defined');
    like($code, qr/retry: %d/, 'retry format');
};

subtest 'generate_c_code C code' => sub {
    plan tests => 4;
    
    require XS::JIT::Builder;
    my $builder = XS::JIT::Builder->new;
    
    Hypersonic::Protocol::SSE->generate_c_code($builder);
    my $code = $builder->code;
    
    like($code, qr/format_sse_event/, 'has event formatter');
    like($code, qr/format_sse_keepalive/, 'has keepalive');
    like($code, qr/format_sse_retry/, 'has retry');
    like($code, qr/format_sse_comment/, 'has comment');
};

# ============================================================
# Test 12-15: Hypersonic::SSE wrapper
# ============================================================
subtest 'SSE object creation' => sub {
    plan tests => 4;
    
    my $stream = Hypersonic::Stream->new(fd => 5);
    my $sse = Hypersonic::SSE->new($stream);
    
    isa_ok($sse, 'Hypersonic::SSE');
    is($sse->stream, $stream, 'stream accessor');
    ok(!$sse->is_started, 'not started initially');
    is($sse->event_count, 0, 'no events sent');
};

# Define mock stream packages to avoid "used only once" warnings
{
    package MockStream;
    sub new { bless { state => 0, writes => [] }, shift }
    sub is_finished { 0 }
    sub write { push @{$_[0]->{writes}}, $_[1]; $_[0] }
    sub headers { $_[0]->{started} = 1; $_[0] }
}
{
    package MockStream2;
    sub new { bless { state => 0, writes => [] }, shift }
    sub is_finished { 0 }
    sub write { push @{$_[0]->{writes}}, $_[1]; $_[0] }
    sub headers { $_[0] }
}
{
    package MockStream3;
    sub new { bless { state => 0, writes => [] }, shift }
    sub is_finished { 0 }
    sub write { push @{$_[0]->{writes}}, $_[1]; $_[0] }
    sub headers { $_[0] }
}
{
    package MockStream4;
    our $headers_set;
    sub new { bless { state => 0 }, shift }
    sub is_finished { 0 }
    sub write { $_[0] }
    sub headers { $headers_set = $_[2]; $_[0] }
}
{
    package MockStream5;
    sub new { bless { state => 0 }, shift }
    sub is_finished { 0 }
}

subtest 'SSE event sends correctly' => sub {
    plan tests => 2;
    
    my $mock_stream = MockStream->new;
    
    my $sse = Hypersonic::SSE->new($mock_stream);
    $sse->event(type => 'test', data => 'hello');
    
    is($sse->event_count, 1, 'event count incremented');
    like($mock_stream->{writes}[0], qr/event: test/, 'event was written');
};

subtest 'SSE data shorthand' => sub {
    plan tests => 1;
    
    my $mock_stream = MockStream2->new;
    
    my $sse = Hypersonic::SSE->new($mock_stream);
    $sse->data('simple message');
    
    like($mock_stream->{writes}[0], qr/data: simple message/, 'data was written');
};

subtest 'SSE keepalive' => sub {
    plan tests => 1;
    
    my $mock_stream = MockStream3->new;
    
    my $sse = Hypersonic::SSE->new($mock_stream);
    $sse->keepalive();
    
    like($mock_stream->{writes}[0], qr/^: keepalive\n\n$/, 'keepalive sent');
};

# ============================================================
# Test 16-17: SSE content type
# ============================================================
subtest 'SSE content type' => sub {
    plan tests => 1;
    
    is(Hypersonic::Protocol::SSE->content_type, 'text/event-stream', 'correct MIME type');
};

subtest 'SSE headers set on start' => sub {
    plan tests => 2;
    
    my $mock_stream = MockStream4->new;
    
    my $sse = Hypersonic::SSE->new($mock_stream);
    $sse->event(data => 'test');
    
    is($MockStream4::headers_set->{'Content-Type'}, 'text/event-stream', 'content-type set');
    is($MockStream4::headers_set->{'Cache-Control'}, 'no-cache', 'cache-control set');
};

# ============================================================
# Test 18-20: RFC compliance edge cases
# ============================================================
subtest 'Event with empty data' => sub {
    plan tests => 2;
    
    my $event = Hypersonic::Protocol::SSE->format_event(data => '');
    
    like($event, qr/data: \n/, 'empty data line');
    like($event, qr/\n\n$/, 'ends with blank line');
};

subtest 'Event data-only (no type)' => sub {
    plan tests => 2;
    
    my $event = Hypersonic::Protocol::SSE->format_event(data => 'test');
    
    unlike($event, qr/event:/, 'no event line when type not specified');
    like($event, qr/data: test\n/, 'has data');
};

subtest 'needs_keepalive timing' => sub {
    plan tests => 2;

    my $mock_stream = MockStream5->new;

    # Use a very short keepalive interval (1 second)
    my $sse = Hypersonic::SSE->new($mock_stream, keepalive => 1);

    ok(!$sse->needs_keepalive, 'no keepalive needed immediately');

    # Wait for the keepalive interval to elapse
    sleep(2);
    ok($sse->needs_keepalive, 'keepalive needed after interval');
};

done_testing();



( run in 1.062 second using v1.01-cache-2.11-cpan-5c0b1e786e0 )