blob: 5a8e561f0624ad94ecfc90f123cfb86d0f904a61 [file]
<?php
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
declare(strict_types=1);
use Iggy\AutoCommit;
use Iggy\Client as IggyClient;
use Iggy\PollingStrategy;
use Iggy\ReceiveMessage;
use Iggy\SendMessage;
use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\TestCase;
final class IggySdkTest extends TestCase
{
#[TestDox('A connected client can ping the server')]
public function testPing(): void
{
new_client()->ping();
assert_true(true, 'ping completed without throwing');
}
#[TestDox('The TCP constructor returns an IggyClient instance')]
public function testClientNotNull(): void
{
assert_true(new_client() instanceof IggyClient);
}
#[TestDox('The TCP constructor treats an empty address as the default server address')]
public function testEmptyClientAddressUsesDefault(): void
{
if (!in_array(server_host(), ['127.0.0.1', 'localhost'], true)) {
$this->markTestSkipped('empty address defaults to localhost, but this run uses a remote test server');
}
$client = new IggyClient('');
$client->connect();
$client->loginUser(env_or_default('IGGY_USERNAME', 'iggy'), env_or_default('IGGY_PASSWORD', 'iggy'));
$client->ping();
assert_true($client instanceof IggyClient);
}
#[TestDox('A client created from a connection string can connect and ping')]
public function testClientFromConnectionString(): void
{
$client = new_connection_string_client();
$client->ping();
assert_true($client instanceof IggyClient);
}
#[TestDox('A stream can be created and fetched by name')]
public function testCreateAndGetStream(): void
{
$client = new_client();
$streamName = unique_name('test-stream');
try {
$client->createStream($streamName);
$stream = $client->getStream($streamName);
assert_not_null($stream);
assert_same($streamName, $stream->name);
assert_true($stream->id >= 0, 'expected non-negative stream id');
} finally {
cleanup_stream($client, $streamName);
}
}
#[TestDox('A newly created stream reports no topics')]
public function testNewStreamHasNoTopics(): void
{
$client = new_client();
$streamName = unique_name('test-stream');
try {
$client->createStream($streamName);
$stream = $client->getStream($streamName);
assert_not_null($stream);
assert_same($streamName, $stream->name);
assert_true($stream->id >= 0, 'expected non-negative stream id');
assert_same(0, $stream->topics_count);
} finally {
cleanup_stream($client, $streamName);
}
}
#[TestDox('A topic can be created and fetched by name')]
public function testCreateAndGetTopic(): void
{
$client = new_client();
$streamName = unique_name('test-stream');
$topicName = unique_name('test-topic');
try {
$client->createStream($streamName);
$client->createTopic($streamName, $topicName, 2, null, null, null, null);
$topic = $client->getTopic($streamName, $topicName);
assert_not_null($topic);
assert_same($topicName, $topic->name);
assert_true($topic->id >= 0, 'expected non-negative topic id');
assert_same(2, $topic->partitions_count);
} finally {
cleanup_stream_with_topics($client, $streamName, [$topicName]);
}
}
#[TestDox('Streams and topics can be fetched with numeric PHP identifiers')]
public function testNumericIdentifiers(): void
{
$client = new_client();
$streamName = unique_name('numeric-stream');
$topicName = unique_name('numeric-topic');
try {
create_stream_and_topic($client, $streamName, $topicName);
$streamByName = $client->getStream($streamName);
assert_not_null($streamByName);
$streamById = $client->getStream($streamByName->id);
assert_not_null($streamById);
assert_same($streamName, $streamById->name);
$topicByName = $client->getTopic($streamByName->id, $topicName);
assert_not_null($topicByName);
$topicById = $client->getTopic($streamByName->id, $topicByName->id);
assert_not_null($topicById);
assert_same($topicName, $topicById->name);
} finally {
cleanup_stream_with_topics($client, $streamName, [$topicName]);
}
}
#[TestDox('Digit-only PHP string identifiers are fetched by name')]
public function testDigitOnlyStringIdentifiersAreNamed(): void
{
$client = new_client();
$streamName = (string) random_int(2_000_000_000, 4_294_967_295);
$topicName = (string) random_int(2_000_000_000, 4_294_967_295);
try {
create_stream_and_topic($client, $streamName, $topicName);
$stream = $client->getStream($streamName);
assert_not_null($stream);
assert_same($streamName, $stream->name);
$topic = $client->getTopic($streamName, $topicName);
assert_not_null($topic);
assert_same($topicName, $topic->name);
} finally {
cleanup_stream_with_topics($client, $streamName, [$topicName]);
}
}
#[TestDox('A topic created through the helper can be fetched')]
public function testListTopicsViaGetTopic(): void
{
$client = new_client();
$streamName = unique_name('test-stream');
$topicName = unique_name('test-topic');
try {
create_stream_and_topic($client, $streamName, $topicName);
$topic = $client->getTopic($streamName, $topicName);
assert_not_null($topic);
assert_same($topicName, $topic->name);
assert_true($topic->id >= 0, 'expected non-negative topic id');
assert_same(1, $topic->partitions_count);
} finally {
cleanup_stream_with_topics($client, $streamName, [$topicName]);
}
}
#[TestDox('Binary payloads round-trip through sendMessages and pollMessages')]
public function testSendAndPollBinaryMessages(): void
{
$client = new_client();
$streamName = unique_name('msg-stream');
$topicName = unique_name('msg-topic');
$partitionId = 0;
$messages = array_map(
static fn (int $i): string => random_bytes(16) . pack('C*', 0, $i, 255),
range(1, 3),
);
try {
create_stream_and_topic($client, $streamName, $topicName);
$client->sendMessages(
$streamName,
$topicName,
$partitionId,
array_map(static fn (string $payload): SendMessage => new SendMessage($payload), $messages),
);
$polled = $client->pollMessages($streamName, $topicName, $partitionId, PollingStrategy::first(), 10, true);
assert_count(count($messages), $polled);
assert_same($messages, collect_payloads($polled));
} finally {
cleanup_stream_with_topics($client, $streamName, [$topicName]);
}
}
#[TestDox('Received messages expose payload and metadata')]
public function testMessageProperties(): void
{
$client = new_client();
$streamName = unique_name('msg-stream');
$topicName = unique_name('msg-topic');
$partitionId = 0;
$payload = unique_name('Property test');
try {
create_stream_and_topic($client, $streamName, $topicName);
$client->sendMessages($streamName, $topicName, $partitionId, [new SendMessage($payload)]);
$polled = $client->pollMessages($streamName, $topicName, $partitionId, PollingStrategy::last(), 1, true);
assert_count(1, $polled);
$message = $polled[0];
assert_same($payload, $message->payload());
assert_true($message->offset() >= 0, 'expected non-negative offset');
assert_true($message->id() !== '', 'expected message id');
assert_true($message->timestamp() > 0, 'expected positive timestamp');
assert_true(ctype_digit($message->checksum()), 'expected numeric checksum');
assert_true($message->length() > 0, 'expected positive length');
assert_same($partitionId, $message->partitionId());
} finally {
cleanup_stream_with_topics($client, $streamName, [$topicName]);
}
}
#[TestDox('Polling strategies return the expected payloads and next advances stored offsets')]
public function testPollingStrategies(): void
{
$client = new_client();
$streamName = unique_name('poll-stream');
$topicName = unique_name('poll-topic');
$partitionId = 0;
$messages = array_map(
static fn (int $i): string => "Polling test {$i} - {$streamName}",
range(0, 4),
);
try {
create_stream_and_topic($client, $streamName, $topicName);
$client->sendMessages(
$streamName,
$topicName,
$partitionId,
array_map(static fn (string $payload): SendMessage => new SendMessage($payload), $messages),
);
$firstMessages = $client->pollMessages($streamName, $topicName, $partitionId, PollingStrategy::first(), 1, false);
assert_count(1, $firstMessages);
assert_same([$messages[0]], collect_payloads($firstMessages));
$lastMessages = $client->pollMessages($streamName, $topicName, $partitionId, PollingStrategy::last(), 1, false);
assert_count(1, $lastMessages);
assert_same([$messages[4]], collect_payloads($lastMessages));
$offset = $firstMessages[0]->offset() + 2;
$offsetMessages = $client->pollMessages(
$streamName,
$topicName,
$partitionId,
PollingStrategy::offset($offset),
2,
false,
);
assert_count(2, $offsetMessages);
assert_same([$messages[2], $messages[3]], collect_payloads($offsetMessages));
assert_same([$offset, $offset + 1], collect_offsets($offsetMessages));
$nextFirstBatch = $client->pollMessages($streamName, $topicName, $partitionId, PollingStrategy::next(), 2, true);
assert_count(2, $nextFirstBatch);
assert_same([$messages[0], $messages[1]], collect_payloads($nextFirstBatch));
$nextSecondBatch = $client->pollMessages($streamName, $topicName, $partitionId, PollingStrategy::next(), 2, true);
assert_count(2, $nextSecondBatch);
assert_same([$messages[2], $messages[3]], collect_payloads($nextSecondBatch));
$nextThirdBatch = $client->pollMessages($streamName, $topicName, $partitionId, PollingStrategy::next(), 2, true);
assert_count(1, $nextThirdBatch);
assert_same([$messages[4]], collect_payloads($nextThirdBatch));
} finally {
cleanup_stream_with_topics($client, $streamName, [$topicName]);
}
}
#[TestDox('Creating the same stream twice raises an error')]
public function testDuplicateStreamCreation(): void
{
$client = new_client();
$streamName = unique_name('duplicate-test');
try {
$client->createStream($streamName);
assert_throws(static fn () => $client->createStream($streamName), 'already exists');
} finally {
cleanup_stream($client, $streamName);
}
}
#[TestDox('Fetching a missing stream returns null')]
public function testGetNonexistentStream(): void
{
$stream = new_client()->getStream(unique_name('nonexistent'));
assert_null($stream);
}
#[TestDox('Creating a topic in a missing stream raises an error')]
public function testCreateTopicInNonexistentStream(): void
{
$client = new_client();
assert_throws(
static fn () => $client->createTopic(unique_name('nonexistent'), 'test-topic', 1, null, null, null, null),
);
}
#[TestDox('Consumer group metadata exposes name, stream, topic, partition, and empty offsets')]
public function testConsumerGroupMeta(): void
{
$client = new_client();
$consumerName = unique_name('consumer-group-consumer');
$streamName = unique_name('consumer-group-stream');
$topicName = unique_name('consumer-group-topic');
$partitionId = 0;
try {
create_stream_and_topic($client, $streamName, $topicName);
$consumer = $client->consumerGroup(
$consumerName,
$streamName,
$topicName,
$partitionId,
PollingStrategy::next(),
10,
AutoCommit::interval(micros(5)),
true,
true,
micros(1),
null,
null,
null,
false,
);
assert_same($consumerName, $consumer->name());
assert_same($streamName, $consumer->stream());
assert_same($topicName, $consumer->topic());
assert_same(0, $consumer->partitionId());
assert_null($consumer->getLastConsumedOffset($partitionId));
assert_null($consumer->getLastStoredOffset($partitionId));
} finally {
cleanup_stream_with_topics($client, $streamName, [$topicName]);
}
}
#[TestDox('Consumer group rejects zero duration options before initialization')]
public function testConsumerGroupRejectsZeroDurationOptions(): void
{
$client = new_client();
$consumerName = unique_name('consumer-group-consumer');
$streamName = unique_name('consumer-group-stream');
$topicName = unique_name('consumer-group-topic');
try {
create_stream_and_topic($client, $streamName, $topicName);
assert_throws(
static fn () => $client->consumerGroup(
$consumerName,
$streamName,
$topicName,
0,
PollingStrategy::next(),
10,
AutoCommit::disabled(),
true,
true,
0,
null,
null,
null,
false,
),
'poll_interval_micros',
);
assert_throws(
static fn () => $client->consumerGroup(
$consumerName,
$streamName,
$topicName,
0,
PollingStrategy::next(),
10,
AutoCommit::disabled(),
true,
true,
micros(1),
0,
null,
null,
false,
),
'polling_retry_interval_micros',
);
assert_throws(
static fn () => $client->consumerGroup(
$consumerName,
$streamName,
$topicName,
0,
PollingStrategy::next(),
10,
AutoCommit::disabled(),
true,
true,
micros(1),
null,
0,
0,
false,
),
'init_retry_interval_micros',
);
} finally {
cleanup_stream_with_topics($client, $streamName, [$topicName]);
}
}
#[TestDox('Consumer offsets require an explicit partition before any message is polled')]
public function testConsumerOffsetCurrentPartitionRequiresPriorPoll(): void
{
$client = new_client();
$consumerName = unique_name('consumer-group-consumer');
$streamName = unique_name('consumer-group-stream');
$topicName = unique_name('consumer-group-topic');
try {
create_stream_and_topic($client, $streamName, $topicName);
$consumer = $client->consumerGroup(
$consumerName,
$streamName,
$topicName,
0,
PollingStrategy::next(),
10,
AutoCommit::disabled(),
true,
true,
micros(1),
null,
null,
null,
false,
);
assert_throws(static fn () => $consumer->storeOffset(0, null), 'partition_id');
assert_throws(static fn () => $consumer->deleteOffset(null), 'partition_id');
} finally {
cleanup_stream_with_topics($client, $streamName, [$topicName]);
}
}
#[TestDox('A consumer group callback receives messages in order')]
public function testConsumeMessages(): void
{
$client = new_client();
$consumerName = unique_name('consumer-group-consumer');
$streamName = unique_name('consumer-group-stream');
$topicName = unique_name('consumer-group-topic');
$partitionId = 0;
$messages = array_map(
static fn (int $i): string => "Consumer group test {$i} - {$streamName}",
range(0, 4),
);
try {
create_stream_and_topic($client, $streamName, $topicName);
$client->sendMessages(
$streamName,
$topicName,
$partitionId,
array_map(static fn (string $payload): SendMessage => new SendMessage($payload), $messages),
);
$consumer = $client->consumerGroup(
$consumerName,
$streamName,
$topicName,
$partitionId,
PollingStrategy::next(),
10,
AutoCommit::interval(micros(5)),
true,
true,
micros(1),
null,
null,
null,
false,
);
$received = [];
$count = $consumer->consumeMessages(
static function (ReceiveMessage $message) use (&$received): void {
$received[] = $message->payload();
},
count($messages),
);
assert_same(count($messages), $count);
assert_same($messages, $received);
} finally {
cleanup_stream_with_topics($client, $streamName, [$topicName]);
}
}
#[TestDox('A consumer group iterator yields messages in order')]
public function testIterMessages(): void
{
$client = new_client();
$consumerName = unique_name('consumer-group-iterator');
$streamName = unique_name('consumer-group-stream');
$topicName = unique_name('consumer-group-topic');
$partitionId = 0;
$messages = array_map(
static fn (int $i): string => "Consumer iterator test {$i} - {$streamName}",
range(0, 4),
);
try {
create_stream_and_topic($client, $streamName, $topicName);
$client->sendMessages(
$streamName,
$topicName,
$partitionId,
array_map(static fn (string $payload): SendMessage => new SendMessage($payload), $messages),
);
$consumer = $client->consumerGroup(
$consumerName,
$streamName,
$topicName,
$partitionId,
PollingStrategy::next(),
10,
AutoCommit::interval(micros(5)),
true,
true,
micros(1),
null,
null,
null,
false,
);
$iterator = $consumer->iterMessages();
$received = [];
$keys = [];
assert_instance_of(Iterator::class, $iterator);
foreach ($iterator as $key => $message) {
$keys[] = $key;
$received[] = $message->payload();
if (count($received) === count($messages)) {
break;
}
}
assert_same(range(0, count($messages) - 1), $keys);
assert_same($messages, $received);
} finally {
cleanup_stream_with_topics($client, $streamName, [$topicName]);
}
}
#[TestDox('Consumer group consumeMessages requires a finite limit')]
public function testConsumeMessagesRequiresLimit(): void
{
$client = new_client();
$consumerName = unique_name('consumer-group-consumer');
$streamName = unique_name('consumer-group-stream');
$topicName = unique_name('consumer-group-topic');
try {
create_stream_and_topic($client, $streamName, $topicName);
$consumer = $client->consumerGroup(
$consumerName,
$streamName,
$topicName,
0,
PollingStrategy::next(),
10,
AutoCommit::disabled(),
true,
true,
micros(1),
null,
null,
null,
false,
);
assert_throws(static fn () => $consumer->consumeMessages(static fn (ReceiveMessage $message): null => null, null));
} finally {
cleanup_stream_with_topics($client, $streamName, [$topicName]);
}
}
}