EV-Kafka
view release on metacpan or search on metacpan
t/18_response_parsers.t view on Meta::CPAN
use strict;
use warnings;
use Test::More;
use EV::Kafka;
# These goldens are hand-built per the Kafka protocol spec rather than
# captured against a real broker â that way the test has no external
# dependency. Lengths are big-endian INT32; strings are length-prefixed
# with INT16. See https://kafka.apache.org/protocol.html.
plan tests => 29;
sub i32 { pack 'N', $_[0] }
sub i16 { pack 'n', $_[0] }
sub i64 { pack 'q>', $_[0] }
sub kstr { my $s = shift; i16(length $s) . $s }
# --- Produce response v0: throttle absent, response-time absent ---
{
my $body =
i32(1) # topics array len
. kstr('mytopic')
. i32(1) # partitions array len
. i32(0) # partition
. i16(0) # error_code
. i64(42); # base_offset
my $r = EV::Kafka::_test_parse_response('produce', 0, $body);
ok ref $r eq 'HASH', 'produce v0 response parses to hashref';
is $r->{topics}[0]{topic}, 'mytopic',
'produce v0 topic name decoded';
is $r->{topics}[0]{partitions}[0]{base_offset}, 42,
'produce v0 base_offset decoded';
}
# --- Metadata response v0: brokers + topics ---
{
my $body =
i32(1) # brokers array len
. i32(0) # node_id
. kstr('host1')
. i32(9092) # port
. i32(1) # topics array len
. i16(0) # topic error_code
. kstr('mytopic')
. i32(1) # partitions array len
. i16(0) # partition error_code
. i32(0) # partition id
. i32(0) # leader id
. i32(0) # replicas array len
. i32(0); # isr array len
my $r = EV::Kafka::_test_parse_response('metadata', 0, $body);
ok ref $r eq 'HASH', 'metadata v0 parses';
is scalar @{$r->{brokers}}, 1, 'one broker';
is $r->{brokers}[0]{host}, 'host1', 'broker host decoded';
is $r->{brokers}[0]{port}, 9092, 'broker port decoded';
is $r->{topics}[0]{name}, 'mytopic', 'topic name decoded';
is $r->{topics}[0]{partitions}[0]{leader}, 0, 'partition leader decoded';
}
# --- Truncated metadata response: should not crash, returns partial data ---
{
my $body = i32(1); # claims one broker but no broker bytes follow
my $r = EV::Kafka::_test_parse_response('metadata', 0, $body);
ok ref $r eq 'HASH', 'truncated metadata returns parsed-so-far hash';
is scalar @{$r->{brokers} // []}, 0,
'truncated metadata yields empty brokers list (parser bails on bounds check)';
}
# --- Heartbeat response v0: just throttle(?) + error_code ---
{
my $body = i16(0); # error_code = 0, no throttle in v0
my $r = EV::Kafka::_test_parse_response('heartbeat', 0, $body);
ok ref $r eq 'HASH', 'heartbeat v0 parses';
is $r->{error_code}, 0, 'heartbeat error_code decoded';
}
# --- LeaveGroup response v0: error_code only ---
{
my $body = i16(0);
my $r = EV::Kafka::_test_parse_response('leave_group', 0, $body);
ok ref $r eq 'HASH', 'leave_group v0 parses';
}
# --- EndTxn response v0: throttle_ms(i32) + error_code ---
{
my $body = i32(0) . i16(0);
my $r = EV::Kafka::_test_parse_response('end_txn', 0, $body);
ok ref $r eq 'HASH', 'end_txn v0 parses';
}
# --- Find_coordinator response v0 ---
{
my $body =
i16(0) # error_code
. i32(7) # node_id
. kstr('coord-host')
. i32(9092); # port
my $r = EV::Kafka::_test_parse_response('find_coordinator', 0, $body);
is $r->{node_id}, 7, 'find_coordinator node_id';
is $r->{host}, 'coord-host', 'find_coordinator host';
}
# --- SyncGroup response v0: error_code + assignment(BYTES) ---
{
my $assignment = "\x00\x01" . # version
i32(0); # zero topics
my $body = i16(0) . i32(length $assignment) . $assignment;
my $r = EV::Kafka::_test_parse_response('sync_group', 0, $body);
ok ref $r eq 'HASH', 'sync_group v0 parses';
is $r->{error_code}, 0, 'sync_group error_code decoded';
}
# --- OffsetFetch response v0: topics array with partitions ---
{
my $body =
i32(1) # topics
. kstr('mytopic')
. i32(1) # partitions
. i32(0) # partition
. pack('q>', 100) # offset
. i16(-1) # metadata len = -1 (null)
. i16(0); # error_code
my $r = EV::Kafka::_test_parse_response('offset_fetch', 0, $body);
is $r->{topics}[0]{partitions}[0]{offset}, 100,
'offset_fetch decodes committed offset';
}
# --- JoinGroup response v0 (leader case) ---
{
my $body =
i16(0) # error_code
. i32(7) # generation_id
. kstr('range') # protocol_name
. kstr('me') # leader
. kstr('me') # member_id
. i32(1) # members array
. kstr('me') # member_id
. i32(0); # metadata len = 0
my $r = EV::Kafka::_test_parse_response('join_group', 0, $body);
is $r->{generation_id}, 7, 'join_group generation_id';
is $r->{leader}, 'me', 'join_group leader';
is $r->{member_id}, 'me', 'join_group member_id';
}
# --- AddPartitionsToTxn response v0 ---
{
my $body =
i32(0) # throttle_time_ms
. i32(1) # topics
. kstr('mytopic')
. i32(1) # partitions
. i32(0) # partition
. i16(0); # error_code
my $r = EV::Kafka::_test_parse_response('add_partitions_to_txn', 0, $body);
ok ref $r eq 'HASH', 'add_partitions_to_txn v0 parses';
}
# --- TxnOffsetCommit response v0 ---
{
my $body =
i32(0) # throttle_time_ms
. i32(1) # topics
. kstr('mytopic')
. i32(1) # partitions
. i32(0) # partition
. i16(0); # error_code
my $r = EV::Kafka::_test_parse_response('txn_offset_commit', 0, $body);
ok ref $r eq 'HASH', 'txn_offset_commit v0 parses';
}
# --- CreateTopics response v0: topics array of (name, error) ---
{
my $body =
i32(1) # topics
. kstr('newtopic')
. i16(36); # error_code = TOPIC_ALREADY_EXISTS
my $r = EV::Kafka::_test_parse_response('create_topics', 0, $body);
ok ref $r eq 'HASH', 'create_topics v0 parses';
}
# --- DeleteTopics response v0 ---
{
my $body =
i32(1) # topics
. kstr('oldtopic')
. i16(0); # error_code = NONE
my $r = EV::Kafka::_test_parse_response('delete_topics', 0, $body);
ok ref $r eq 'HASH', 'delete_topics v0 parses';
}
# --- InitProducerId response v1: throttle prefix is added at v1+ ---
{
my $body =
i32(0) # throttle_time_ms (v1+)
. i16(0) # error_code
. pack('q>', 12345) # producer_id
. i16(0); # producer_epoch
my $r = EV::Kafka::_test_parse_response('init_producer_id', 1, $body);
is $r->{producer_id}, 12345, 'init_producer_id pid decoded';
is $r->{producer_epoch}, 0, 'init_producer_id epoch decoded';
}
( run in 1.210 second using v1.01-cache-2.11-cpan-84e82930d8c )