| <?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\Client as IggyClient; |
| use Iggy\PollingStrategy; |
| use Iggy\SendMessage; |
| use PHPUnit\Framework\Attributes\DataProvider; |
| use PHPUnit\Framework\Attributes\TestDox; |
| use PHPUnit\Framework\TestCase; |
| |
| final class BasicMessagingFeatureTest extends TestCase |
| { |
| private ?IggyClient $client = null; |
| private ?int $lastStreamId = null; |
| private ?string $lastStreamName = null; |
| private ?string $lastTopicName = null; |
| private array $lastPolledMessages = []; |
| private array $sentPayloads = []; |
| private ?int $lastSentMessageCount = null; |
| |
| #[DataProvider('scenarioCases')] |
| #[TestDox('Basic messaging shared BDD scenario passes for the PHP SDK')] |
| public function testBasicMessagingScenario(string $scenarioName, array $steps): void |
| { |
| assert_true($scenarioName !== '', 'scenario name must not be empty'); |
| |
| foreach ($steps as $step) { |
| $this->runStep($step); |
| } |
| } |
| |
| protected function tearDown(): void |
| { |
| if ($this->client !== null && $this->lastStreamName !== null) { |
| cleanup_stream_with_topics( |
| $this->client, |
| $this->lastStreamName, |
| $this->lastTopicName !== null ? [$this->lastTopicName] : [], |
| ); |
| } |
| |
| parent::tearDown(); |
| } |
| |
| public static function scenarioCases(): array |
| { |
| $featureFile = getenv('BDD_FEATURE_FILE') ?: __DIR__ . '/../../scenarios/basic_messaging.feature'; |
| if (!is_file($featureFile)) { |
| throw new RuntimeException("feature file not found at {$featureFile}"); |
| } |
| |
| $lines = file($featureFile, FILE_IGNORE_NEW_LINES); |
| if ($lines === false) { |
| throw new RuntimeException("failed to read feature file at {$featureFile}"); |
| } |
| |
| $backgroundSteps = []; |
| $scenarios = []; |
| $currentScenario = null; |
| $currentSteps = []; |
| $section = null; |
| $stepPattern = '/^(Given|When|Then|And|But|\*) (.+)$/'; |
| $unsupportedStructurePattern = '/^(Scenario Outline|Rule|Examples):|^\|/'; |
| foreach ($lines as $line) { |
| $line = trim($line); |
| if ($line === '' || str_starts_with($line, '#') || str_starts_with($line, '@')) { |
| continue; |
| } |
| |
| if ($line === 'Background:') { |
| $section = 'background'; |
| continue; |
| } |
| |
| if (str_starts_with($line, 'Scenario:')) { |
| if ($currentScenario !== null) { |
| $scenarios[$currentScenario] = $currentSteps; |
| } |
| |
| $currentScenario = trim(substr($line, strlen('Scenario:'))); |
| $currentSteps = []; |
| $section = 'scenario'; |
| continue; |
| } |
| |
| if (preg_match($unsupportedStructurePattern, $line) === 1) { |
| throw new RuntimeException("Unsupported BDD structure: {$line}"); |
| } |
| |
| if (preg_match($stepPattern, $line, $matches) !== 1) { |
| continue; |
| } |
| |
| if ($section === null) { |
| throw new RuntimeException("BDD step appears before Background or Scenario: {$line}"); |
| } |
| |
| if ($section === 'background') { |
| $backgroundSteps[] = $matches[2]; |
| continue; |
| } |
| |
| if ($section === 'scenario' && $currentScenario !== null) { |
| $currentSteps[] = $matches[2]; |
| } |
| } |
| |
| if ($currentScenario !== null) { |
| $scenarios[$currentScenario] = $currentSteps; |
| } |
| |
| if ($backgroundSteps === []) { |
| throw new RuntimeException('no BDD background steps were loaded from the feature'); |
| } |
| |
| if ($scenarios === []) { |
| throw new RuntimeException('no BDD scenarios were loaded from the feature'); |
| } |
| |
| $cases = []; |
| foreach ($scenarios as $scenarioName => $scenarioSteps) { |
| if ($scenarioSteps === []) { |
| throw new RuntimeException("scenario has no steps: {$scenarioName}"); |
| } |
| |
| $cases[$scenarioName] = [$scenarioName, [...$backgroundSteps, ...$scenarioSteps]]; |
| } |
| |
| return $cases; |
| } |
| |
| private function runStep(string $step): void |
| { |
| if ($step === 'I have a running Iggy server') { |
| $this->client = new IggyClient(server_host() . ':' . server_port()); |
| $this->client->connect(); |
| $this->client->ping(); |
| |
| return; |
| } |
| |
| if ($step === 'I am authenticated as the root user') { |
| $this->requireClient()->loginUser(env_or_default('IGGY_USERNAME', 'iggy'), env_or_default('IGGY_PASSWORD', 'iggy')); |
| |
| return; |
| } |
| |
| if ($step === 'I have no streams in the system') { |
| return; |
| } |
| |
| if (preg_match('/^I create a stream with name "([^"]+)"$/', $step, $matches) === 1) { |
| $streamName = $matches[1]; |
| $this->requireClient()->createStream($streamName); |
| $stream = $this->requireClient()->getStream($streamName); |
| assert_not_null($stream); |
| $this->lastStreamId = $stream->id; |
| $this->lastStreamName = $streamName; |
| |
| return; |
| } |
| |
| if ($step === 'the stream should be created successfully') { |
| assert_not_null($this->requireClient()->getStream($this->requireStreamName())); |
| |
| return; |
| } |
| |
| if (preg_match('/^the stream should have name "([^"]+)"$/', $step, $matches) === 1) { |
| $stream = $this->requireClient()->getStream($matches[1]); |
| assert_not_null($stream); |
| assert_same($matches[1], $stream->name); |
| |
| return; |
| } |
| |
| if (preg_match('/^I create a topic with name "([^"]+)" in stream (\d+) with (\d+) partitions$/', $step, $matches) === 1) { |
| $topicName = $matches[1]; |
| $streamId = (int) $matches[2]; |
| $partitions = (int) $matches[3]; |
| |
| $this->requireClient()->createTopic($streamId, $topicName, $partitions, null, null, null, null); |
| $topic = $this->requireClient()->getTopic($streamId, $topicName); |
| assert_not_null($topic); |
| $this->lastTopicName = $topicName; |
| |
| return; |
| } |
| |
| if ($step === 'the topic should be created successfully') { |
| assert_not_null($this->requireClient()->getTopic($this->requireStreamId(), $this->requireTopicName())); |
| |
| return; |
| } |
| |
| if (preg_match('/^the topic should have name "([^"]+)"$/', $step, $matches) === 1) { |
| $topic = $this->requireClient()->getTopic($this->requireStreamId(), $matches[1]); |
| assert_not_null($topic); |
| assert_same($matches[1], $topic->name); |
| |
| return; |
| } |
| |
| if (preg_match('/^the topic should have (\d+) partitions$/', $step, $matches) === 1) { |
| $topic = $this->requireClient()->getTopic($this->requireStreamId(), $this->requireTopicName()); |
| assert_not_null($topic); |
| assert_same((int) $matches[1], $topic->partitions_count); |
| |
| return; |
| } |
| |
| if (preg_match('/^I send (\d+) messages to stream (\d+), topic (\d+), partition (\d+)$/', $step, $matches) === 1) { |
| $messageCount = (int) $matches[1]; |
| $this->lastSentMessageCount = $messageCount; |
| $this->sentPayloads = array_map( |
| static fn (int $index): string => "bdd-php-message-{$index}", |
| range(0, $messageCount - 1), |
| ); |
| |
| $this->requireClient()->sendMessages( |
| (int) $matches[2], |
| (int) $matches[3], |
| (int) $matches[4], |
| array_map(static fn (string $payload): SendMessage => new SendMessage($payload), $this->sentPayloads), |
| ); |
| |
| return; |
| } |
| |
| if ($step === 'all messages should be sent successfully') { |
| assert_not_null($this->lastSentMessageCount, 'sent message count has not been captured'); |
| assert_count($this->lastSentMessageCount, $this->sentPayloads); |
| |
| return; |
| } |
| |
| if (preg_match('/^I poll messages from stream (\d+), topic (\d+), partition (\d+) starting from offset (\d+)$/', $step, $matches) === 1) { |
| $this->lastPolledMessages = $this->requireClient()->pollMessages( |
| (int) $matches[1], |
| (int) $matches[2], |
| (int) $matches[3], |
| PollingStrategy::offset((int) $matches[4]), |
| $this->requireSentMessageCount(), |
| true, |
| ); |
| |
| return; |
| } |
| |
| if (preg_match('/^I should receive (\d+) messages$/', $step, $matches) === 1) { |
| assert_count((int) $matches[1], $this->lastPolledMessages); |
| |
| return; |
| } |
| |
| if (preg_match('/^the messages should have sequential offsets from (\d+) to (\d+)$/', $step, $matches) === 1) { |
| assert_same(range((int) $matches[1], (int) $matches[2]), collect_offsets($this->lastPolledMessages)); |
| |
| return; |
| } |
| |
| if ($step === 'each message should have the expected payload content') { |
| assert_same($this->sentPayloads, collect_payloads($this->lastPolledMessages)); |
| |
| return; |
| } |
| |
| if ($step === 'the last polled message should match the last sent message') { |
| $lastSentPayload = $this->sentPayloads[array_key_last($this->sentPayloads)]; |
| $lastPolledMessage = $this->lastPolledMessages[array_key_last($this->lastPolledMessages)]; |
| |
| assert_same($lastSentPayload, $lastPolledMessage->payload()); |
| |
| return; |
| } |
| |
| self::fail("Unsupported BDD step: {$step}"); |
| } |
| |
| private function requireClient(): IggyClient |
| { |
| assert_not_null($this->client, 'BDD client has not been initialized'); |
| |
| return $this->client; |
| } |
| |
| private function requireStreamId(): int |
| { |
| assert_not_null($this->lastStreamId, 'stream id has not been captured'); |
| |
| return $this->lastStreamId; |
| } |
| |
| private function requireStreamName(): string |
| { |
| assert_not_null($this->lastStreamName, 'stream name has not been captured'); |
| |
| return $this->lastStreamName; |
| } |
| |
| private function requireTopicName(): string |
| { |
| assert_not_null($this->lastTopicName, 'topic name has not been captured'); |
| |
| return $this->lastTopicName; |
| } |
| |
| private function requireSentMessageCount(): int |
| { |
| assert_not_null($this->lastSentMessageCount, 'sent message count has not been captured'); |
| |
| return $this->lastSentMessageCount; |
| } |
| } |