[Test] Add integration test (#76)
* Add PartitionedProducerProcess.
* Remove PartitionedProducerState.
* Add UpdatePartitions Event.
* Implement PartitionedProducer.
* Fix some problems
* Remove PartitionedProducerProcess.
* Make some classes sealed.
* Use await in PartitionedProducer.Send
* Fix test method name.
* Make CreateSubProducers sync.
* Fix some comments.
* Subproducers
* Change PartitionedProducer to Producer and add some comments.
* Add UpdatePartitions to connect logic and fix some other problems.
* Add integration test.
* Improve integration framework
* Update and add fixture.
* Add integration test CI.
* fix docker compose file not exist in stress test
* fix
* remove testcontainer
* fix ci job name.
* Fix lookup exception not retry.
* Update integration test
* Use async Task instead of async void.
diff --git a/.github/workflows/ci-integration-test.yaml b/.github/workflows/ci-integration-test.yaml
new file mode 100644
index 0000000..7fd8cda
--- /dev/null
+++ b/.github/workflows/ci-integration-test.yaml
@@ -0,0 +1,60 @@
+#
+# 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.
+#
+
+name: CI - Integration Test
+on:
+ pull_request:
+ branches:
+ - master
+ push:
+ branches:
+ - master
+
+jobs:
+ integration-tests:
+ runs-on: ubuntu-latest
+ timeout-minutes: 120
+ steps:
+ - name: checkout
+ uses: actions/checkout@main
+
+ - name: Setup dotnet
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: '5.0.x'
+
+ - name: run integration tests
+ run: |
+ export PULSAR_DEPLOYMENT_TYPE=container
+ dotnet test ./tests/DotPulsar.IntegrationTests/DotPulsar.IntegrationTests.csproj --logger "trx;verbosity=detailed"
+
+ - name: package artifacts
+ if: failure()
+ run: |
+ rm -rf artifacts
+ mkdir artifacts
+ find . -type d -name "TestResults" -exec cp --parents -R {} artifacts/ \;
+ zip -r artifacts.zip artifacts
+
+ - name: upload artifacts
+ uses: actions/upload-artifact@master
+ if: failure()
+ with:
+ name: artifacts
+ path: artifacts.zip
diff --git a/DotPulsar.sln b/DotPulsar.sln
index 5300e55..9c4a0e3 100644
--- a/DotPulsar.sln
+++ b/DotPulsar.sln
@@ -26,6 +26,8 @@
README.md = README.md
EndProjectSection
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotPulsar.IntegrationTests", "tests\DotPulsar.IntegrationTests\DotPulsar.IntegrationTests.csproj", "{B44E52DB-DB45-4E31-AA2C-68E5C52AFDEB}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -56,6 +58,10 @@
{6D44683B-865C-4D15-9F0A-1A8441354589}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6D44683B-865C-4D15-9F0A-1A8441354589}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6D44683B-865C-4D15-9F0A-1A8441354589}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B44E52DB-DB45-4E31-AA2C-68E5C52AFDEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B44E52DB-DB45-4E31-AA2C-68E5C52AFDEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B44E52DB-DB45-4E31-AA2C-68E5C52AFDEB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B44E52DB-DB45-4E31-AA2C-68E5C52AFDEB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -66,6 +72,7 @@
{2A810EB9-45CE-4593-8E4C-026E0CBB3C42} = {E7106D0F-B255-4631-9FB8-734FC5748FA9}
{14934BED-A222-47B2-A58A-CFC4AAB89B49} = {E7106D0F-B255-4631-9FB8-734FC5748FA9}
{6D44683B-865C-4D15-9F0A-1A8441354589} = {E7106D0F-B255-4631-9FB8-734FC5748FA9}
+ {B44E52DB-DB45-4E31-AA2C-68E5C52AFDEB} = {E1C932A9-6D4C-4DDF-8922-BE7B71F12F1C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {88355922-E70A-4B73-B7F8-ABF8F2B59789}
diff --git a/src/DotPulsar/Internal/DefaultExceptionHandler.cs b/src/DotPulsar/Internal/DefaultExceptionHandler.cs
index a783827..f29d269 100644
--- a/src/DotPulsar/Internal/DefaultExceptionHandler.cs
+++ b/src/DotPulsar/Internal/DefaultExceptionHandler.cs
@@ -51,6 +51,7 @@
AsyncLockDisposedException _ => FaultAction.Retry,
PulsarStreamDisposedException _ => FaultAction.Retry,
AsyncQueueDisposedException _ => FaultAction.Retry,
+ LookupNotReadyException _ => FaultAction.Retry,
OperationCanceledException _ => cancellationToken.IsCancellationRequested ? FaultAction.Rethrow : FaultAction.Retry,
DotPulsarException _ => FaultAction.Rethrow,
SocketException socketException => socketException.SocketErrorCode switch
diff --git a/tests/DotPulsar.IntegrationTests/Abstraction/IPulsarService.cs b/tests/DotPulsar.IntegrationTests/Abstraction/IPulsarService.cs
new file mode 100644
index 0000000..84dfec9
--- /dev/null
+++ b/tests/DotPulsar.IntegrationTests/Abstraction/IPulsarService.cs
@@ -0,0 +1,44 @@
+/*
+ * Licensed 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.
+ */
+
+namespace DotPulsar.IntegrationTests.Abstraction
+{
+ using System;
+ using System.Net.Http;
+ using System.Threading.Tasks;
+ using Xunit;
+
+ /// <summary>
+ /// Pulsar Service interface
+ /// </summary>
+ public interface IPulsarService : IAsyncLifetime
+ {
+ /// <summary>
+ /// Get broker binary protocol uri
+ /// </summary>
+ Uri GetBrokerUri();
+
+ /// <summary>
+ /// Get broker rest uri
+ /// </summary>
+ Uri GetWebServiceUri();
+
+ /// <summary>
+ /// Create a partitioned topic
+ /// The format of the restTopic must be `{schema}/{tenant}/{namespace}/{topicName}`
+ /// For example, `persistent/public/default/test-topic`
+ /// </summary>
+ Task<HttpResponseMessage?> CreatePartitionedTopic(string restTopic, int numPartitions);
+ }
+}
diff --git a/tests/DotPulsar.IntegrationTests/DotPulsar.IntegrationTests.csproj b/tests/DotPulsar.IntegrationTests/DotPulsar.IntegrationTests.csproj
new file mode 100644
index 0000000..3f569d1
--- /dev/null
+++ b/tests/DotPulsar.IntegrationTests/DotPulsar.IntegrationTests.csproj
@@ -0,0 +1,33 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net5.0</TargetFramework>
+
+ <IsPackable>false</IsPackable>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="FluentAssertions" Version="5.10.3" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
+ <PackageReference Include="xunit" Version="2.4.1" />
+ <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ <PrivateAssets>all</PrivateAssets>
+ </PackageReference>
+ <PackageReference Include="coverlet.collector" Version="1.3.0">
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ <PrivateAssets>all</PrivateAssets>
+ </PackageReference>
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\DotPulsar\DotPulsar.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <None Update="docker-compose-standalone-tests.yml">
+ <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+ </None>
+ </ItemGroup>
+
+</Project>
diff --git a/tests/DotPulsar.IntegrationTests/Fixtures/StandaloneClusterFixture.cs b/tests/DotPulsar.IntegrationTests/Fixtures/StandaloneClusterFixture.cs
new file mode 100644
index 0000000..eb2d072
--- /dev/null
+++ b/tests/DotPulsar.IntegrationTests/Fixtures/StandaloneClusterFixture.cs
@@ -0,0 +1,38 @@
+/*
+ * Licensed 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.
+ */
+
+namespace DotPulsar.IntegrationTests.Fixtures
+{
+ using Abstraction;
+ using Services;
+ using System.Threading.Tasks;
+ using Xunit;
+
+ public class StandaloneClusterFixture : IAsyncLifetime
+ {
+ public IPulsarService? PulsarService { private set; get; }
+
+ public async Task InitializeAsync()
+ {
+ PulsarService = ServiceFactory.CreatePulsarService();
+ await PulsarService.InitializeAsync();
+ }
+
+ public async Task DisposeAsync()
+ {
+ if (PulsarService != null)
+ await PulsarService.DisposeAsync();
+ }
+ }
+}
diff --git a/tests/DotPulsar.IntegrationTests/Fixtures/StandaloneClusterTests.cs b/tests/DotPulsar.IntegrationTests/Fixtures/StandaloneClusterTests.cs
new file mode 100644
index 0000000..51ba95d
--- /dev/null
+++ b/tests/DotPulsar.IntegrationTests/Fixtures/StandaloneClusterTests.cs
@@ -0,0 +1,21 @@
+/*
+ * Licensed 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.
+ */
+
+namespace DotPulsar.IntegrationTests.Fixtures
+{
+ using Xunit;
+
+ [CollectionDefinition(nameof(StandaloneClusterTest))]
+ public class StandaloneClusterTest : ICollectionFixture<StandaloneClusterFixture> { }
+}
diff --git a/tests/DotPulsar.IntegrationTests/ProducerTests.cs b/tests/DotPulsar.IntegrationTests/ProducerTests.cs
new file mode 100644
index 0000000..bcbaf69
--- /dev/null
+++ b/tests/DotPulsar.IntegrationTests/ProducerTests.cs
@@ -0,0 +1,138 @@
+/*
+ * Licensed 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.
+ */
+
+namespace DotPulsar.IntegrationTests
+{
+ using Abstraction;
+ using Abstractions;
+ using Extensions;
+ using Fixtures;
+ using FluentAssertions;
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.Linq;
+ using System.Threading.Tasks;
+ using Xunit;
+ using Xunit.Abstractions;
+
+ [Collection(nameof(StandaloneClusterTest))]
+ public class ProducerTests
+ {
+ private readonly ITestOutputHelper _testOutputHelper;
+ private readonly IPulsarService _pulsarService;
+
+ public ProducerTests(ITestOutputHelper outputHelper, StandaloneClusterFixture fixture)
+ {
+ _testOutputHelper = outputHelper;
+ Debug.Assert(fixture.PulsarService != null, "fixture.PulsarService != null");
+ _pulsarService = fixture.PulsarService;
+ }
+
+ [Fact]
+ public async Task SimpleProduceConsume_WhenSendingMessagesToProducer_ThenReceiveMessagesFromConsumer()
+ {
+ //Arrange
+ await using var client = PulsarClient.Builder().ServiceUrl(_pulsarService.GetBrokerUri()).Build();
+ string topicName = $"simple-produce-consume{Guid.NewGuid():N}";
+ const string content = "test-message";
+
+ //Act
+ await using var producer = client.NewProducer(Schema.String)
+ .Topic(topicName)
+ .Create();
+
+ await using var consumer = client.NewConsumer(Schema.String)
+ .Topic(topicName)
+ .SubscriptionName("test-sub")
+ .InitialPosition(SubscriptionInitialPosition.Earliest)
+ .Create();
+
+ await producer.Send(content);
+ _testOutputHelper.WriteLine($"Sent a message: {content}");
+
+ //Assert
+ (await consumer.Receive()).Value().Should().Be(content);
+ }
+
+ [Fact]
+ public async Task SinglePartition_WhenSendMessages_ThenGetMessagesFromSinglePartition()
+ {
+ //Arrange
+ await using var client = PulsarClient.Builder().ServiceUrl(_pulsarService.GetBrokerUri()).Build();
+ string topicName = $"single-partitioned-{Guid.NewGuid():N}";
+ const string content = "test-message";
+ const int partitions = 3;
+ var consumers = new List<IConsumer<string>>();
+
+ await _pulsarService.CreatePartitionedTopic($"persistent/public/default/{topicName}", partitions);
+
+ //Act
+ await using var producer = client.NewProducer(Schema.String)
+ .Topic(topicName)
+ .Create();
+
+ for (var i = 0; i < partitions; ++i)
+ {
+ consumers.Add(client.NewConsumer(Schema.String)
+ .Topic($"{topicName}-partition-{i}")
+ .SubscriptionName("test-sub")
+ .InitialPosition(SubscriptionInitialPosition.Earliest)
+ .Create());
+ }
+
+ await producer.Send(content);
+ _testOutputHelper.WriteLine($"Sent a message: {content}");
+
+ //Assert
+ (await Task.WhenAny(consumers.Select(c => c.Receive().AsTask()).ToList())).Result.Value().Should().Be(content);
+ }
+
+ [Fact]
+ public async Task RoundRobinPartition_WhenSendMessages_ThenGetMessagesFromPartitionsInOrder()
+ {
+ //Arrange
+ await using var client = PulsarClient.Builder().ServiceUrl(_pulsarService.GetBrokerUri()).Build();
+ string topicName = $"round-robin-partitioned-{Guid.NewGuid():N}";
+ const string content = "test-message";
+ const int partitions = 3;
+ var consumers = new List<IConsumer<string>>();
+
+ await _pulsarService.CreatePartitionedTopic($"persistent/public/default/{topicName}", partitions);
+
+ //Act
+ await using var producer = client.NewProducer(Schema.String)
+ .Topic(topicName)
+ .Create();
+ await producer.StateChangedTo(ProducerState.Connected);
+
+ for (var i = 0; i < partitions; ++i)
+ {
+ consumers.Add(client.NewConsumer(Schema.String)
+ .Topic($"{topicName}-partition-{i}")
+ .SubscriptionName("test-sub")
+ .InitialPosition(SubscriptionInitialPosition.Earliest)
+ .Create());
+ await producer.Send($"{content}-{i}");
+ _testOutputHelper.WriteLine($"Sent a message to consumer [{i}]");
+ }
+
+ //Assert
+ for (var i = 0; i < partitions; ++i)
+ {
+ (await consumers[i].Receive()).Value().Should().Be($"{content}-{i}");
+ }
+ }
+ }
+}
diff --git a/tests/DotPulsar.IntegrationTests/Services/PulsarServiceBase.cs b/tests/DotPulsar.IntegrationTests/Services/PulsarServiceBase.cs
new file mode 100644
index 0000000..bc52005
--- /dev/null
+++ b/tests/DotPulsar.IntegrationTests/Services/PulsarServiceBase.cs
@@ -0,0 +1,57 @@
+/*
+ * Licensed 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.
+ */
+
+namespace DotPulsar.IntegrationTests.Services
+{
+ using Abstraction;
+ using System;
+ using System.Net.Http;
+ using System.Text;
+ using System.Threading;
+ using System.Threading.Tasks;
+
+ public class PulsarServiceBase : IPulsarService
+ {
+ private readonly CancellationTokenSource _cts;
+ private readonly HttpClient _adminClient;
+
+ protected PulsarServiceBase()
+ {
+ _cts = new CancellationTokenSource();
+ _adminClient = new HttpClient();
+ }
+
+ public virtual Task InitializeAsync()
+ => Task.CompletedTask;
+
+ public virtual Task DisposeAsync()
+ {
+ _adminClient.Dispose();
+ _cts.Dispose();
+ return Task.CompletedTask;
+ }
+
+ public virtual Uri GetBrokerUri()
+ => throw new NotImplementedException();
+
+ public virtual Uri GetWebServiceUri()
+ => throw new NotImplementedException();
+
+ public async Task<HttpResponseMessage?> CreatePartitionedTopic(string restTopic, int numPartitions)
+ {
+ var content = new StringContent(numPartitions.ToString(), Encoding.UTF8, "application/json");
+ return await _adminClient.PutAsync($"{GetWebServiceUri()}admin/v2/{restTopic}/partitions", content, _cts.Token).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/tests/DotPulsar.IntegrationTests/Services/ServiceFactory.cs b/tests/DotPulsar.IntegrationTests/Services/ServiceFactory.cs
new file mode 100644
index 0000000..01b3c1c
--- /dev/null
+++ b/tests/DotPulsar.IntegrationTests/Services/ServiceFactory.cs
@@ -0,0 +1,36 @@
+/*
+ * Licensed 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.
+ */
+
+namespace DotPulsar.IntegrationTests.Services
+{
+ using Abstraction;
+
+ public static class ServiceFactory
+ {
+ private const string PulsarDeploymentType = "PULSAR_DEPLOYMENT_TYPE";
+ private const string ContainerDeployment = "container";
+
+ public static IPulsarService CreatePulsarService()
+ {
+ var deploymentType = System.Environment.GetEnvironmentVariable(PulsarDeploymentType);
+
+ if (deploymentType == ContainerDeployment)
+ {
+ return new StandaloneContainerService();
+ }
+
+ return new StandaloneExternalService();
+ }
+ }
+}
diff --git a/tests/DotPulsar.IntegrationTests/Services/StandaloneContainerService.cs b/tests/DotPulsar.IntegrationTests/Services/StandaloneContainerService.cs
new file mode 100644
index 0000000..e711d0c
--- /dev/null
+++ b/tests/DotPulsar.IntegrationTests/Services/StandaloneContainerService.cs
@@ -0,0 +1,88 @@
+/*
+ * Licensed 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.
+ */
+
+namespace DotPulsar.IntegrationTests.Services
+{
+ using System;
+ using System.Diagnostics;
+ using System.Net.Http;
+ using System.Threading.Tasks;
+
+ public sealed class StandaloneContainerService : PulsarServiceBase
+ {
+ public override async Task InitializeAsync()
+ {
+ await base.InitializeAsync().ConfigureAwait(false);
+ TakeDownPulsar(); // clean-up if anything was left running from previous run
+
+ RunProcess("docker-compose", "-f docker-compose-standalone-tests.yml up -d");
+
+ var waitTries = 10;
+
+ using var handler = new HttpClientHandler { AllowAutoRedirect = true };
+
+ using var client = new HttpClient(handler);
+
+ while (waitTries > 0)
+ {
+ try
+ {
+ await client.GetAsync("http://localhost:54546/metrics/").ConfigureAwait(false);
+ return;
+ }
+ catch
+ {
+ waitTries--;
+ await Task.Delay(5000).ConfigureAwait(false);
+ }
+ }
+
+ throw new Exception("Unable to confirm Pulsar has initialized");
+ }
+
+ public override async Task DisposeAsync()
+ {
+ await base.DisposeAsync().ConfigureAwait(false);
+ TakeDownPulsar();
+ }
+
+ private static void TakeDownPulsar()
+ => RunProcess("docker-compose", "-f docker-compose-standalone-tests.yml down");
+
+ private static void RunProcess(string name, string arguments)
+ {
+ var processStartInfo = new ProcessStartInfo { FileName = name, Arguments = arguments };
+
+ processStartInfo.Environment["TAG"] = "test";
+ processStartInfo.Environment["CONFIGURATION"] = "Debug";
+ processStartInfo.Environment["COMPUTERNAME"] = Environment.MachineName;
+
+ var process = Process.Start(processStartInfo);
+
+ if (process is null)
+ throw new Exception("Process.Start returned null");
+
+ process.WaitForExit();
+
+ if (process.ExitCode != 0)
+ throw new Exception($"Exit code {process.ExitCode} when running process {name} with arguments {arguments}");
+ }
+
+ public override Uri GetBrokerUri()
+ => new("pulsar://localhost:54545");
+
+ public override Uri GetWebServiceUri()
+ => new("http://localhost:54546");
+ }
+}
diff --git a/tests/DotPulsar.IntegrationTests/Services/StandaloneExternalService.cs b/tests/DotPulsar.IntegrationTests/Services/StandaloneExternalService.cs
new file mode 100644
index 0000000..820cecd
--- /dev/null
+++ b/tests/DotPulsar.IntegrationTests/Services/StandaloneExternalService.cs
@@ -0,0 +1,27 @@
+/*
+ * Licensed 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.
+ */
+
+namespace DotPulsar.IntegrationTests.Services
+{
+ using System;
+
+ public sealed class StandaloneExternalService : PulsarServiceBase
+ {
+ public override Uri GetBrokerUri()
+ => new ("pulsar://localhost:6650");
+
+ public override Uri GetWebServiceUri()
+ => new("http://localhost:8080");
+ }
+}
diff --git a/tests/DotPulsar.IntegrationTests/docker-compose-standalone-tests.yml b/tests/DotPulsar.IntegrationTests/docker-compose-standalone-tests.yml
new file mode 100644
index 0000000..a3642ec
--- /dev/null
+++ b/tests/DotPulsar.IntegrationTests/docker-compose-standalone-tests.yml
@@ -0,0 +1,14 @@
+version: '3.5'
+
+services:
+
+ pulsar:
+ container_name: pulsar-standalone
+ image: 'apachepulsar/pulsar:2.7.0'
+ ports:
+ - '54546:8080'
+ - '54545:6650'
+ environment:
+ PULSAR_MEM: " -Xms1g -Xmx1g -XX:MaxDirectMemorySize=2g"
+ command: |
+ /bin/bash -c "bin/apply-config-from-env.py conf/standalone.conf && bin/pulsar standalone --no-functions-worker"