From 3f8880ee1dcf550675c60fe6622202081ada38e8 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sat, 20 Dec 2025 03:02:43 -0600 Subject: [PATCH 01/32] Added initial project Signed-off-by: Whit Waldo --- all.sln | 9 ++++++++- src/Dapr.TestContainers/Class1.cs | 5 +++++ src/Dapr.TestContainers/Dapr.TestContainers.csproj | 14 ++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 src/Dapr.TestContainers/Class1.cs create mode 100644 src/Dapr.TestContainers/Dapr.TestContainers.csproj diff --git a/all.sln b/all.sln index f000a22c0..970eb892e 100644 --- a/all.sln +++ b/all.sln @@ -1,4 +1,4 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.3.32929.385 MinimumVisualStudioVersion = 10.0.40219.1 @@ -205,6 +205,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DistributedLock", "Distribu EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DistributedLock", "examples\DistributedLock\DistributedLock\DistributedLock.csproj", "{F2DFB0FE-DF35-4D94-9CC9-43212B1D6F93}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.TestContainers", "src\Dapr.TestContainers\Dapr.TestContainers.csproj", "{D14893E1-EF21-4EB4-9DAD-82B5127832CB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -541,6 +543,10 @@ Global {F2DFB0FE-DF35-4D94-9CC9-43212B1D6F93}.Debug|Any CPU.Build.0 = Debug|Any CPU {F2DFB0FE-DF35-4D94-9CC9-43212B1D6F93}.Release|Any CPU.ActiveCfg = Release|Any CPU {F2DFB0FE-DF35-4D94-9CC9-43212B1D6F93}.Release|Any CPU.Build.0 = Release|Any CPU + {D14893E1-EF21-4EB4-9DAD-82B5127832CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D14893E1-EF21-4EB4-9DAD-82B5127832CB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D14893E1-EF21-4EB4-9DAD-82B5127832CB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D14893E1-EF21-4EB4-9DAD-82B5127832CB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -640,6 +646,7 @@ Global {9BD12D26-AD9B-4C76-A97F-7A89B7276ABE} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {11D2CA0F-6D38-4DC7-AE06-C1DAE7FC1C20} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} {F2DFB0FE-DF35-4D94-9CC9-43212B1D6F93} = {11D2CA0F-6D38-4DC7-AE06-C1DAE7FC1C20} + {D14893E1-EF21-4EB4-9DAD-82B5127832CB} = {27C5D71D-0721-4221-9286-B94AB07B58CF} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} diff --git a/src/Dapr.TestContainers/Class1.cs b/src/Dapr.TestContainers/Class1.cs new file mode 100644 index 000000000..6f3f2fcdf --- /dev/null +++ b/src/Dapr.TestContainers/Class1.cs @@ -0,0 +1,5 @@ +namespace Dapr.TestContainers; + +public class Class1 +{ +} diff --git a/src/Dapr.TestContainers/Dapr.TestContainers.csproj b/src/Dapr.TestContainers/Dapr.TestContainers.csproj new file mode 100644 index 000000000..44b52418f --- /dev/null +++ b/src/Dapr.TestContainers/Dapr.TestContainers.csproj @@ -0,0 +1,14 @@ + + + + enable + Dapr.TestContainers + Dapr TestContainers implementation + TestContainers for validating Dapr applications + + + + + + + From 42b9ac2ad493be59bcbedb450d0e59fa665ee071 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sat, 20 Dec 2025 04:38:40 -0600 Subject: [PATCH 02/32] Initial commit of the testcontainer functionality Signed-off-by: Whit Waldo --- Directory.Packages.props | 1 + src/Dapr.TestContainers/Class1.cs | 5 - .../Common/DaprHarnessBuilder.cs | 84 +++++++++++ .../Common/HostPortPair.cs | 25 ++++ .../Common/IAsyncContainerFixture.cs | 27 ++++ .../Common/IAsyncStartable.cs | 36 +++++ .../Common/Options/DaprLogLevel.cs | 45 ++++++ .../Common/Options/DaprRuntimeOptions.cs | 56 ++++++++ .../Configuration/ConfigurationSettings.cs | 19 +++ .../OtelTracingConfigurationSettings.cs | 36 +++++ .../Configuration/SecretScope.cs | 80 +++++++++++ .../ZipkinTracingConfigurationSettings.cs | 26 ++++ .../Containers/Components/Component.cs | 44 ++++++ .../Containers/Components/MetadataEntry.cs | 19 +++ .../Containers/Dapr/DaprPlacementContainer.cs | 68 +++++++++ .../Containers/Dapr/DaprSchedulerContainer.cs | 74 ++++++++++ .../Containers/Dapr/DaprdContainer.cs | 102 ++++++++++++++ .../LocalStorageCryptographyContainer.cs | 61 ++++++++ .../Containers/OllamaContainer.cs | 96 +++++++++++++ .../Containers/RabbitMqContainer.cs | 98 +++++++++++++ .../Containers/RedisContainer.cs | 133 ++++++++++++++++++ .../Dapr.TestContainers.csproj | 2 +- .../Harnesses/ActorHarness.cs | 73 ++++++++++ .../Harnesses/BaseHarness.cs | 57 ++++++++ .../Harnesses/ConversationHarness.cs | 60 ++++++++ .../Harnesses/CryptographyHarness.cs | 55 ++++++++ .../Harnesses/DistributedLockHarness.cs | 60 ++++++++ .../Harnesses/JobsHarness.cs | 56 ++++++++ .../Harnesses/PubSubHarness.cs | 60 ++++++++ .../Harnesses/StateManagementHarness.cs | 60 ++++++++ .../Harnesses/WorkflowHarness.cs | 73 ++++++++++ 31 files changed, 1685 insertions(+), 6 deletions(-) delete mode 100644 src/Dapr.TestContainers/Class1.cs create mode 100644 src/Dapr.TestContainers/Common/DaprHarnessBuilder.cs create mode 100644 src/Dapr.TestContainers/Common/HostPortPair.cs create mode 100644 src/Dapr.TestContainers/Common/IAsyncContainerFixture.cs create mode 100644 src/Dapr.TestContainers/Common/IAsyncStartable.cs create mode 100644 src/Dapr.TestContainers/Common/Options/DaprLogLevel.cs create mode 100644 src/Dapr.TestContainers/Common/Options/DaprRuntimeOptions.cs create mode 100644 src/Dapr.TestContainers/Configuration/ConfigurationSettings.cs create mode 100644 src/Dapr.TestContainers/Configuration/OtelTracingConfigurationSettings.cs create mode 100644 src/Dapr.TestContainers/Configuration/SecretScope.cs create mode 100644 src/Dapr.TestContainers/Configuration/ZipkinTracingConfigurationSettings.cs create mode 100644 src/Dapr.TestContainers/Containers/Components/Component.cs create mode 100644 src/Dapr.TestContainers/Containers/Components/MetadataEntry.cs create mode 100644 src/Dapr.TestContainers/Containers/Dapr/DaprPlacementContainer.cs create mode 100644 src/Dapr.TestContainers/Containers/Dapr/DaprSchedulerContainer.cs create mode 100644 src/Dapr.TestContainers/Containers/Dapr/DaprdContainer.cs create mode 100644 src/Dapr.TestContainers/Containers/LocalStorageCryptographyContainer.cs create mode 100644 src/Dapr.TestContainers/Containers/OllamaContainer.cs create mode 100644 src/Dapr.TestContainers/Containers/RabbitMqContainer.cs create mode 100644 src/Dapr.TestContainers/Containers/RedisContainer.cs create mode 100644 src/Dapr.TestContainers/Harnesses/ActorHarness.cs create mode 100644 src/Dapr.TestContainers/Harnesses/BaseHarness.cs create mode 100644 src/Dapr.TestContainers/Harnesses/ConversationHarness.cs create mode 100644 src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs create mode 100644 src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs create mode 100644 src/Dapr.TestContainers/Harnesses/JobsHarness.cs create mode 100644 src/Dapr.TestContainers/Harnesses/PubSubHarness.cs create mode 100644 src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs create mode 100644 src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 6db58e8e9..79da9a732 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -52,6 +52,7 @@ + diff --git a/src/Dapr.TestContainers/Class1.cs b/src/Dapr.TestContainers/Class1.cs deleted file mode 100644 index 6f3f2fcdf..000000000 --- a/src/Dapr.TestContainers/Class1.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Dapr.TestContainers; - -public class Class1 -{ -} diff --git a/src/Dapr.TestContainers/Common/DaprHarnessBuilder.cs b/src/Dapr.TestContainers/Common/DaprHarnessBuilder.cs new file mode 100644 index 000000000..b50956b87 --- /dev/null +++ b/src/Dapr.TestContainers/Common/DaprHarnessBuilder.cs @@ -0,0 +1,84 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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. +// ------------------------------------------------------------------------ + +using System; +using System.Threading.Tasks; +using Dapr.TestContainers.Common.Options; +using Dapr.TestContainers.Harnesses; + +namespace Dapr.TestContainers.Common; + +/// +/// Builds the Dapr harnesses for different building blocks. +/// +/// The Dapr runtime options. +/// The test app to run. +public sealed class DaprHarnessBuilder(DaprRuntimeOptions options, Func startApp) +{ + /// + /// Builds a workflow harness. + /// + /// The path to the Dapr resources. + public WorkflowHarness BuildWorkflow(string componentsDir) => + new WorkflowHarness(componentsDir, startApp, options); + + /// + /// Builds a distributed lock harness. + /// + /// The path to the Dapr resources. + public DistributedLockHarness BuildDistributedLock(string componentsDir) => + new DistributedLockHarness(componentsDir, startApp, options); + + /// + /// Builds a conversation harness. + /// + /// The path to the Dapr resources. + public ConversationHarness BuildConversation(string componentsDir) => + new ConversationHarness(componentsDir, startApp, options); + + /// + /// Builds a cryptography harness. + /// + /// The path to the Dapr resources. + /// The path to the cryptography keys. + public CryptographyHarness BuildCryptography(string componentsDir, string keysDir) => + new CryptographyHarness(componentsDir, startApp, keysDir, options); + + /// + /// Builds a jobs harness. + /// + /// The path to the Dapr resources. + public JobsHarness BuildJobs(string componentsDir) => + new JobsHarness(componentsDir, startApp, options); + + /// + /// Builds a PubSub harness. + /// + /// The path to the Dapr resources. + public PubSubHarness BuildPubSub(string componentsDir) => + new PubSubHarness(componentsDir, startApp, options); + + /// + /// Builds a state management harness. + /// + /// The path to the Dapr resources. + public StateManagementHarness BuildStateManagement(string componentsDir) => + new StateManagementHarness(componentsDir, startApp, options); + + /// + /// Builds an actor harness. + /// + /// The path to the Dapr resources. + public ActorHarness BuildActors(string componentsDir) => + new ActorHarness(componentsDir, startApp, options); +} diff --git a/src/Dapr.TestContainers/Common/HostPortPair.cs b/src/Dapr.TestContainers/Common/HostPortPair.cs new file mode 100644 index 000000000..a96fe2464 --- /dev/null +++ b/src/Dapr.TestContainers/Common/HostPortPair.cs @@ -0,0 +1,25 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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 Dapr.TestContainers.Common; + +/// +/// A hostname and port value pair. +/// +public sealed record HostPortPair(string Hostname, int Port) +{ + /// + /// Provides a string version of the record. + /// + public override string ToString() => $"{Hostname}:{Port}"; +} diff --git a/src/Dapr.TestContainers/Common/IAsyncContainerFixture.cs b/src/Dapr.TestContainers/Common/IAsyncContainerFixture.cs new file mode 100644 index 000000000..a3adb135b --- /dev/null +++ b/src/Dapr.TestContainers/Common/IAsyncContainerFixture.cs @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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. +// ------------------------------------------------------------------------ + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Dapr.TestContainers.Common; + +/// +/// Represents a harness that's created for test purposes. +/// +public interface IAsyncContainerFixture : IAsyncDisposable +{ + /// + Task InitializeAsync(CancellationToken cancellationToken = default); +} diff --git a/src/Dapr.TestContainers/Common/IAsyncStartable.cs b/src/Dapr.TestContainers/Common/IAsyncStartable.cs new file mode 100644 index 000000000..f44635aa7 --- /dev/null +++ b/src/Dapr.TestContainers/Common/IAsyncStartable.cs @@ -0,0 +1,36 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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. +// ------------------------------------------------------------------------ + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Dapr.TestContainers.Common; + +/// +/// Represents a resource that can be started and stopped. +/// +public interface IAsyncStartable : IAsyncDisposable +{ + /// + /// Starts the resource. + /// + /// Cancellation token/ + Task StartAsync(CancellationToken cancellationToken = default); + + /// + /// Stops the resource. + /// + /// Cancellation token. + Task StopAsync(CancellationToken cancellationToken = default); +} diff --git a/src/Dapr.TestContainers/Common/Options/DaprLogLevel.cs b/src/Dapr.TestContainers/Common/Options/DaprLogLevel.cs new file mode 100644 index 000000000..77972bf30 --- /dev/null +++ b/src/Dapr.TestContainers/Common/Options/DaprLogLevel.cs @@ -0,0 +1,45 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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 Dapr.TestContainers.Common.Options; + +/// +/// A configurable log level. +/// +public enum DaprLogLevel +{ + /// + /// Represents a "debug" log level. + /// + Debug, + /// + /// Represents an "info" log level. + /// + Info, + /// + /// Represents a "warn" log level. + /// + Warn, + /// + /// Represents an "error" log level. + /// + Error, + /// + /// Represents a "fatal" log level. + /// + Fatal, + /// + /// Represents a "panic" log level. + /// + Panic +} diff --git a/src/Dapr.TestContainers/Common/Options/DaprRuntimeOptions.cs b/src/Dapr.TestContainers/Common/Options/DaprRuntimeOptions.cs new file mode 100644 index 000000000..138848d43 --- /dev/null +++ b/src/Dapr.TestContainers/Common/Options/DaprRuntimeOptions.cs @@ -0,0 +1,56 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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 Dapr.TestContainers.Common.Options; + +/// +/// The various options used to spin up the Dapr containers. +/// +/// The port of the test app. +/// The version of the Dapr images to use. +public sealed class DaprRuntimeOptions(int appPort, string version = "1.16.4") +{ + /// + /// The port of the test app. + /// + public int AppPort => appPort; + + /// + /// The level of Dapr logs to show. + /// + public DaprLogLevel LogLevel { get; private set; } = DaprLogLevel.Info; + + /// + /// The image tag for the Dapr runtime. + /// + public string RuntimeImageTag => $"daprio/daprd:{version}"; + /// + /// The image tag for the Dapr placement service. + /// + public string PlacementImageTag => $"daprio/placement:{version}"; + /// + /// The image tag for the Dapr scheduler service. + /// + public string SchedulerImageTag => $"daprio/scheduler:{version}"; + + /// + /// Sets the Dapr log level. + /// + /// The log level to specify. + /// + public DaprRuntimeOptions WithLogLevel(DaprLogLevel logLevel) + { + LogLevel = logLevel; + return this; + } +} diff --git a/src/Dapr.TestContainers/Configuration/ConfigurationSettings.cs b/src/Dapr.TestContainers/Configuration/ConfigurationSettings.cs new file mode 100644 index 000000000..6b333bebd --- /dev/null +++ b/src/Dapr.TestContainers/Configuration/ConfigurationSettings.cs @@ -0,0 +1,19 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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 Dapr.TestContainers.Configuration; + +/// +/// Base Dapr configuration settings. +/// +public abstract class ConfigurationSettings; diff --git a/src/Dapr.TestContainers/Configuration/OtelTracingConfigurationSettings.cs b/src/Dapr.TestContainers/Configuration/OtelTracingConfigurationSettings.cs new file mode 100644 index 000000000..856ffaec0 --- /dev/null +++ b/src/Dapr.TestContainers/Configuration/OtelTracingConfigurationSettings.cs @@ -0,0 +1,36 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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 Dapr.TestContainers.Configuration; + +/// +/// Configuration settings for OTEL tracing. +/// +/// The tracing endpoint address. +/// Indicates whether the endpoint is secure. +/// The tracing protocol. +public sealed class OtelTracingConfigurationSettings(string endpointAddress, bool isSecure, string protocol) : ConfigurationSettings +{ + /// + /// The endpoint address. + /// + public string EndpointAddress => endpointAddress; + /// + /// Whether the endpoint address is secure. + /// + public bool IsSecure => isSecure; + /// + /// The collection protocol. + /// + public string Protocol => protocol; +} diff --git a/src/Dapr.TestContainers/Configuration/SecretScope.cs b/src/Dapr.TestContainers/Configuration/SecretScope.cs new file mode 100644 index 000000000..ecfe9701f --- /dev/null +++ b/src/Dapr.TestContainers/Configuration/SecretScope.cs @@ -0,0 +1,80 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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. +// ------------------------------------------------------------------------ + +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Dapr.TestContainers.Configuration; + +/// +/// Builds out a collection of secret scopes to the Dapr configuration. +/// +/// The secret scopes to emit. +public sealed class SecretScopeConfigurationBuilder(IReadOnlyCollection scopes) +{ + /// + /// Builds the secret scope section of the Dapr configuration file. + /// + /// + public string Build() + { + var sb = new StringBuilder("secrets:"); + sb.AppendLine(" scopes:"); + foreach (var scope in scopes) + { + sb.AppendLine($" - storeName: {scope.StoreName}"); + sb.AppendLine($" defaultAccess: {scope.DefaultAccess.ToString().ToLowerInvariant()}"); + if (scope.AllowedSecrets.Count > 0) + { + sb.AppendLine( + $" allowedSecrets: [{string.Join(", ", scope.AllowedSecrets.Select(s => $"\"{s}\""))}]"); + } + if (scope.DeniedSecrets.Count > 0) + { + sb.AppendLine( + $" deniedSecrets: [{string.Join(", ", scope.DeniedSecrets.Select(s => $"\"{s}\""))}]"); + } + } + + return sb.ToString(); + } +} + +/// +/// Used to scope a named secret store component to one or more secrets for an application. +/// +/// The name of the secret store. +/// The default access to the secrets in the store. +/// The list of secret keys that can be accessed. +/// The list of secret keys that cannot be accessed. +public sealed record SecretScope( + string StoreName, + SecretStoreAccess DefaultAccess, + IReadOnlyCollection AllowedSecrets, + IReadOnlyCollection DeniedSecrets); + +/// +/// The secret store access modifier. +/// +public enum SecretStoreAccess +{ + /// + /// Indicates that secret access should be allowed by default. + /// + Allow, + /// + /// Indicates that secret access should be denied by default. + /// + Deny +} diff --git a/src/Dapr.TestContainers/Configuration/ZipkinTracingConfigurationSettings.cs b/src/Dapr.TestContainers/Configuration/ZipkinTracingConfigurationSettings.cs new file mode 100644 index 000000000..a21c9763d --- /dev/null +++ b/src/Dapr.TestContainers/Configuration/ZipkinTracingConfigurationSettings.cs @@ -0,0 +1,26 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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 Dapr.TestContainers.Configuration; + +/// +/// Configuration settings for Zipkin tracing. +/// +/// The tracing endpoint address. +public sealed class ZipkinTracingConfigurationSettings(string endpointAddress) : ConfigurationSettings +{ + /// + /// The endpoint address. + /// + public string EndpointAddress => endpointAddress; +} diff --git a/src/Dapr.TestContainers/Containers/Components/Component.cs b/src/Dapr.TestContainers/Containers/Components/Component.cs new file mode 100644 index 000000000..51dc29011 --- /dev/null +++ b/src/Dapr.TestContainers/Containers/Components/Component.cs @@ -0,0 +1,44 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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. +// ------------------------------------------------------------------------ + +using System.Collections.Generic; +using System.Linq; + +namespace Dapr.TestContainers.Containers.Components; + +/// +/// Represents a Dapr component. +/// +/// The name of the component. +/// The type of the component. +/// The component's version. +/// Metadata associated with the component. +public abstract class Component(string name, string type, string version, Dictionary metadata) +{ + /// + /// The name of the component. + /// + public string Name => name; + /// + /// The type of the component. + /// + public string Type => type; + /// + /// The version of the component. + /// + public string Version => version; + /// + /// The metadata attached to the component. + /// + public List Metadata => metadata.Select(kv => new MetadataEntry(kv.Key, kv.Value)).ToList(); +} diff --git a/src/Dapr.TestContainers/Containers/Components/MetadataEntry.cs b/src/Dapr.TestContainers/Containers/Components/MetadataEntry.cs new file mode 100644 index 000000000..fcc3c3162 --- /dev/null +++ b/src/Dapr.TestContainers/Containers/Components/MetadataEntry.cs @@ -0,0 +1,19 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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 Dapr.TestContainers.Containers.Components; + +/// +/// Provides the name and value for a metadata record in a component. +/// +public sealed record MetadataEntry(string Name, string Value); diff --git a/src/Dapr.TestContainers/Containers/Dapr/DaprPlacementContainer.cs b/src/Dapr.TestContainers/Containers/Dapr/DaprPlacementContainer.cs new file mode 100644 index 000000000..a62e05658 --- /dev/null +++ b/src/Dapr.TestContainers/Containers/Dapr/DaprPlacementContainer.cs @@ -0,0 +1,68 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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. +// ------------------------------------------------------------------------ + +using System; +using System.Threading; +using System.Threading.Tasks; +using Dapr.TestContainers.Common; +using Dapr.TestContainers.Common.Options; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; + +namespace Dapr.TestContainers.Containers.Dapr; + +/// +/// The container for the Dapr placement service. +/// +public sealed class DaprPlacementContainer : IAsyncStartable +{ + private readonly IContainer _container; + private const int InternalPort = 50006; + + /// + /// The container hostname. + /// + public string Host => _container.Hostname; + /// + /// The container's external port. + /// + public int Port { get; private set; } + + /// + /// Initializes a new instance of the . + /// + /// The Dapr runtime options. + public DaprPlacementContainer(DaprRuntimeOptions options) + { + //Placement service runs via port 50006 + _container = new ContainerBuilder() + .WithImage(options.PlacementImageTag) + .WithName($"placement-{Guid.NewGuid():N}") + .WithCommand("./placement", "-port", InternalPort.ToString()) + .WithPortBinding(InternalPort, assignRandomHostPort: true) + .WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(InternalPort)) + .Build(); + } + + /// + public async Task StartAsync(CancellationToken cancellationToken = default) + { + await _container.StartAsync(cancellationToken); + Port = _container.GetMappedPublicPort(InternalPort); + } + + /// + public Task StopAsync(CancellationToken cancellationToken = default) => _container.StopAsync(cancellationToken); + /// + public ValueTask DisposeAsync() => _container.DisposeAsync(); +} diff --git a/src/Dapr.TestContainers/Containers/Dapr/DaprSchedulerContainer.cs b/src/Dapr.TestContainers/Containers/Dapr/DaprSchedulerContainer.cs new file mode 100644 index 000000000..da5d82c2e --- /dev/null +++ b/src/Dapr.TestContainers/Containers/Dapr/DaprSchedulerContainer.cs @@ -0,0 +1,74 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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. +// ------------------------------------------------------------------------ + +using System; +using System.Threading; +using System.Threading.Tasks; +using Dapr.TestContainers.Common; +using Dapr.TestContainers.Common.Options; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; + +namespace Dapr.TestContainers.Containers.Dapr; + +/// +/// The container for the Dapr scheduler service. +/// +public sealed class DaprSchedulerContainer : IAsyncStartable +{ + private const int InternalPort = 51005; + private readonly IContainer _container; + + /// + /// The container's hostname. + /// + public string Host => _container.Hostname; + /// + /// The container's external port. + /// + public int Port { get; private set; } + + /// + /// Creates a new instance of a . + /// + public DaprSchedulerContainer(DaprRuntimeOptions options) + { + // Scheduler service runs via port 51005 + _container = new ContainerBuilder() + .WithImage(options.SchedulerImageTag) + .WithName($"scheduler-{Guid.NewGuid():N}") + .WithCommand("./scheduler", InternalPort.ToString(), "-etcd-data-dir", ".") + .WithPortBinding(InternalPort, assignRandomHostPort: true) + .WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(InternalPort)) + .Build(); + + } + + /// + public async Task StartAsync(CancellationToken cancellationToken = default) + { + await _container.StartAsync(cancellationToken); + Port = _container.GetMappedPublicPort(InternalPort); + + // Empty dirs with 0777 inside container: + await _container.ExecAsync(["sh", "-c", "mkdir -p ./default-dapr-scheduler-server-0/dapr-0.1 && chmod 0777 ./default-dapr-scheduler-server-0/dapr-0.1" + ], cancellationToken); + await _container.ExecAsync(["sh", "-c", "mkdir -p ./dapr-scheduler-existing-cluster && chmod 0777 ./dapr-scheduler-existing-cluster" + ], cancellationToken); + } + + /// + public Task StopAsync(CancellationToken cancellationToken = default) => _container.StopAsync(cancellationToken); + /// + public ValueTask DisposeAsync() => _container.DisposeAsync(); +} diff --git a/src/Dapr.TestContainers/Containers/Dapr/DaprdContainer.cs b/src/Dapr.TestContainers/Containers/Dapr/DaprdContainer.cs new file mode 100644 index 000000000..c3b72c687 --- /dev/null +++ b/src/Dapr.TestContainers/Containers/Dapr/DaprdContainer.cs @@ -0,0 +1,102 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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. +// ------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Dapr.TestContainers.Common; +using Dapr.TestContainers.Common.Options; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Configurations; +using DotNet.Testcontainers.Containers; + +namespace Dapr.TestContainers.Containers.Dapr; + +/// +/// The container for the Dapr runtime. +/// +public sealed class DaprdContainer : IAsyncStartable +{ + private const int InternalHttpPort = 3500; + private const int InternalGrpcPort = 50001; + private readonly IContainer _container; + + /// + /// The HTTP port of the Dapr runtime. + /// + public int HttpPort { get; private set; } + /// + /// The gRPC port of the Dapr runtime. + /// + public int GrpcPort { get; private set; } + + /// + /// Used to initialize a new instance of a .. + /// + /// The ID of the app to initialize daprd with. + /// The path to the Dapr resources directory. + /// The Dapr runtime options. + /// The hostname and port of the Placement service. + /// The hostname and port of the Scheduler service. + public DaprdContainer(string appId, string componentsHostFolder, DaprRuntimeOptions options, HostPortPair? placementHostAndPort = null, HostPortPair? schedulerHostAndPort = null) + { + var cmd = + new List + { + "./daprd", + "-app-id", appId, + "-app-port", options.AppPort.ToString(), + "-dapr-http-port", InternalHttpPort.ToString(), + "-dapr-grpc-port", InternalGrpcPort.ToString(), + "-log-level", options.LogLevel.ToString().ToLowerInvariant(), + "-resources-path", "/components" + }; + + if (placementHostAndPort is not null) + { + cmd.Add("-placement-host-address"); + cmd.Add(placementHostAndPort.ToString()); + } + + if (schedulerHostAndPort is not null) + { + cmd.Add("-scheduler-host-address"); + cmd.Add(schedulerHostAndPort.ToString()); + } + + _container = new ContainerBuilder() + .WithImage(options.RuntimeImageTag) + .WithName($"dapr-{Guid.NewGuid():N}") + .WithCommand(cmd.ToArray()) + .WithPortBinding(HttpPort, assignRandomHostPort: true) + .WithPortBinding(GrpcPort, assignRandomHostPort: true) + .WithBindMount(componentsHostFolder, "/components", AccessMode.ReadOnly) + .WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(InternalHttpPort) + .UntilInternalTcpPortIsAvailable(InternalGrpcPort)) + .Build(); + } + + /// + public async Task StartAsync(CancellationToken cancellationToken = default) + { + await _container.StartAsync(cancellationToken); + HttpPort = _container.GetMappedPublicPort(HttpPort); + GrpcPort = _container.GetMappedPublicPort(GrpcPort); + } + + /// + public Task StopAsync(CancellationToken cancellationToken = default) => _container.StopAsync(cancellationToken); + /// + public ValueTask DisposeAsync() => _container.DisposeAsync(); +} diff --git a/src/Dapr.TestContainers/Containers/LocalStorageCryptographyContainer.cs b/src/Dapr.TestContainers/Containers/LocalStorageCryptographyContainer.cs new file mode 100644 index 000000000..b7065b314 --- /dev/null +++ b/src/Dapr.TestContainers/Containers/LocalStorageCryptographyContainer.cs @@ -0,0 +1,61 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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. +// ------------------------------------------------------------------------ + +using System.IO; + +namespace Dapr.TestContainers.Containers; + +/// +/// This is an odd one because it's not actually a container - just the entry point for the YAML file builder. +/// +public sealed class LocalStorageCryptographyContainer +{ + /// + /// Builds out the YAML components for the local storage cryptography implementation. + /// + public static class Yaml + { + /// + /// Writes a + /// + /// + /// + /// + /// + public static string WriteCryptoYamlToFolder(string folderPath, string keyPath, string fileName = "local-crypto.yaml") + { + var yaml = GetLocalStorageYaml(keyPath); + return WriteToFolder(folderPath, fileName, yaml); + } + + private static string WriteToFolder(string folderPath, string fileName, string yaml) + { + Directory.CreateDirectory(folderPath); + var fullPath = Path.Combine(folderPath, fileName); + File.WriteAllText(fullPath, yaml); + return fullPath; + } + + private static string GetLocalStorageYaml(string keyPath) => + $@"apiVersion: dapr.io/v1alpha +kind: Component +metadata: + name: crypto +spec: + type: crypto.dapr.localstorage + version: v1 + metadata: + - name: path + value: {keyPath}"; + } +} diff --git a/src/Dapr.TestContainers/Containers/OllamaContainer.cs b/src/Dapr.TestContainers/Containers/OllamaContainer.cs new file mode 100644 index 000000000..54ff43d00 --- /dev/null +++ b/src/Dapr.TestContainers/Containers/OllamaContainer.cs @@ -0,0 +1,96 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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. +// ------------------------------------------------------------------------ + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Dapr.TestContainers.Common; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; + +namespace Dapr.TestContainers.Containers; + +/// +/// Provides an Ollama container. +/// +public sealed class OllamaContainer : IAsyncStartable +{ + private const int InternalPort = 11434; + + private readonly IContainer _container = new ContainerBuilder() + .WithImage("ollama/ollama") + .WithName($"ollama-{Guid.NewGuid():N}") + .WithPortBinding(InternalPort, assignRandomHostPort: true) + .WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(InternalPort)) + .Build(); + + /// + /// The hostname of the container. + /// + public string Host => _container.Hostname; + /// + /// The port of the container. + /// + public int Port { get; private set; } + + /// + public async Task StartAsync(CancellationToken cancellationToken = default) + { + await _container.StartAsync(cancellationToken); + Port = _container.GetMappedPublicPort(InternalPort); + } + + /// + public Task StopAsync(CancellationToken cancellationToken = default) => _container.StopAsync(cancellationToken); + /// + public ValueTask DisposeAsync() => _container.DisposeAsync(); + + /// + /// Builds out the YAML components for Ollama. + /// + public static class Yaml + { + /// + /// Writes the component YAML. + /// + public static string WriteConversationYamlToFolder(string folderPath, string fileName = "ollama-conversation.yaml", string model = "smollm:135m", string cacheTtl = "10m", string endpoint = "http://localhost:11434/v1") + { + var yaml = GetConversationYaml(model, cacheTtl, endpoint); + return WriteToFolder(folderPath, fileName, yaml); + } + + private static string WriteToFolder(string folderPath, string fileName, string yaml) + { + Directory.CreateDirectory(folderPath); + var fullPath = Path.Combine(folderPath, fileName); + File.WriteAllText(fullPath, yaml); + return fullPath; + } + + private static string GetConversationYaml(string model, string cacheTtl, string endpoint) => + $@"apiVersion: dapr.io/v1alpha +kind: Component +metadata: + name: ollama +spec: + type: conversation.ollama + metadata: + - name: model + value: {model} + - name: cacheTTL + value: {cacheTtl} + - name: endpoint + value: {endpoint}"; + } +} diff --git a/src/Dapr.TestContainers/Containers/RabbitMqContainer.cs b/src/Dapr.TestContainers/Containers/RabbitMqContainer.cs new file mode 100644 index 000000000..0785d6b0a --- /dev/null +++ b/src/Dapr.TestContainers/Containers/RabbitMqContainer.cs @@ -0,0 +1,98 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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. +// ------------------------------------------------------------------------ + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Dapr.TestContainers.Common; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; + +namespace Dapr.TestContainers.Containers; + +/// +/// Provides a RabbitMQ container. +/// +public sealed class RabbitMqContainer : IAsyncStartable +{ + private const int InternalPort = 5672; + + private readonly IContainer _container = new ContainerBuilder() + .WithImage("rabbitmq:alpine") + .WithName($"rabbitmq-{Guid.NewGuid():N}") + .WithPortBinding(InternalPort, assignRandomHostPort: true) + .WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(InternalPort)) + .Build(); + + /// + /// The hostname of the container. + /// + public string Host => _container.Hostname; + /// + /// The port of the container. + /// + public int Port { get; private set; } + + /// + public async Task StartAsync(CancellationToken cancellationToken = default) + { + await _container.StartAsync(cancellationToken); + Port = _container.GetMappedPublicPort(InternalPort); + } + + /// + public Task StopAsync(CancellationToken cancellationToken = default) => _container.StopAsync(cancellationToken); + /// + public ValueTask DisposeAsync() => _container.DisposeAsync(); + + /// + /// Builds out the YAML components for RabbitMQ. + /// + public static class Yaml + { + /// + /// Writes a PubSub YAML component. + /// + public static string WritePubSubYamlToFolder(string folderPath, string fileName = "rabbitmq-pubsub.yaml") + { + var yaml = GetPubSubYaml(); + return WriteToFolder(folderPath, fileName, yaml); + } + + private static string WriteToFolder(string folderPath, string fileName, string yaml) + { + Directory.CreateDirectory(folderPath); + var fullPath = Path.Combine(folderPath, fileName); + File.WriteAllText(fullPath, yaml); + return fullPath; + } + + private static string GetPubSubYaml() => + $@"apiVersion: dapr.io/v1alpha +kind: Component +metadata: + name: rabbitmq +spec: + type: pubsub.rabbitmq + metadata: + - name: protocol + value: amqp + - name: hostname + value: localhost + - name: username + value: default + - name: password + value: default"; + } +} diff --git a/src/Dapr.TestContainers/Containers/RedisContainer.cs b/src/Dapr.TestContainers/Containers/RedisContainer.cs new file mode 100644 index 000000000..fcf305993 --- /dev/null +++ b/src/Dapr.TestContainers/Containers/RedisContainer.cs @@ -0,0 +1,133 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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. +// ------------------------------------------------------------------------ + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Dapr.TestContainers.Common; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; + +namespace Dapr.TestContainers.Containers; + +/// +/// Provides a Redis container. +/// +public sealed class RedisContainer : IAsyncStartable +{ + private const int InternalPort = 6379; + private readonly IContainer _container = new ContainerBuilder() + .WithImage("redis:alpine") + .WithName($"redis-{Guid.NewGuid():N}") + .WithPortBinding(InternalPort, assignRandomHostPort: true) + .WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(InternalPort)) + .Build(); + + /// + /// The hostname of the container. + /// + public string Host => _container.Hostname; + /// + /// The port of the container. + /// + public int Port { get; private set; } + + /// + public async Task StartAsync(CancellationToken cancellationToken = default) + { + await _container.StartAsync(cancellationToken); + Port = _container.GetMappedPublicPort(InternalPort); + } + /// + public Task StopAsync(CancellationToken cancellationToken = default) => _container.StopAsync(cancellationToken); + /// + public ValueTask DisposeAsync() => _container.DisposeAsync(); + + /// + /// Builds out each of the YAML components for Redis + /// + public static class Yaml + { + /// + /// Writes a state store YAML component. + /// + public static string WriteStateStoreYamlToFolder(string folderPath, string fileName = "redis-state.yaml",string redisHost = "localhost:6379", + string? passwordSecretName = null) + { + var yaml = GetRedisStateStoreYaml(redisHost, passwordSecretName); + return WriteToFolder(folderPath, fileName, yaml); + } + + /// + /// Writes a distributed lock YAML component. + /// + public static string WriteDistributedLockYamlToFolder(string folderPath, string fileName = "redis-lock.yaml", + string redisHost = "localhost:6379", string? passwordSecretName = null) + { + var yaml = GetDistributedLockYaml(redisHost, passwordSecretName); + return WriteToFolder(folderPath, fileName, yaml); + } + + private static string WriteToFolder(string folderPath, string fileName, string yaml) + { + Directory.CreateDirectory(folderPath); + var fullPath = Path.Combine(folderPath, fileName); + File.WriteAllText(fullPath, yaml); + return fullPath; + } + + private static string BuildSecretBlock(string? passwordSecretName) => + passwordSecretName is null + ? string.Empty + : $" - name: redisPassword\n secretKeyRef:\n name: {passwordSecretName}\n key: redis-password\n"; + + private static string GetDistributedLockYaml(string redisHost, string? passwordSecretName) + { + var secretBlock = BuildSecretBlock(passwordSecretName); + return + $@"apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: lock + namespace: default +spec: + type: lock.redis + version: v1 + metadata: + - name: redisHost + value: {redisHost} +{secretBlock}"; + } + + private static string GetRedisStateStoreYaml(string redisHost, string? passwordSecretName) + { + var secretBlock = BuildSecretBlock(passwordSecretName); + return + $@"apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: statestore + namespace: default +spec: + type: state.redis + version: v1 + metadata: + - name: redisHost + value: {redisHost} + - name: actorStateStore + value: ""true"" +{secretBlock}"; + } + } +} diff --git a/src/Dapr.TestContainers/Dapr.TestContainers.csproj b/src/Dapr.TestContainers/Dapr.TestContainers.csproj index 44b52418f..c9adc88b3 100644 --- a/src/Dapr.TestContainers/Dapr.TestContainers.csproj +++ b/src/Dapr.TestContainers/Dapr.TestContainers.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/Dapr.TestContainers/Harnesses/ActorHarness.cs b/src/Dapr.TestContainers/Harnesses/ActorHarness.cs new file mode 100644 index 000000000..89df8c3b6 --- /dev/null +++ b/src/Dapr.TestContainers/Harnesses/ActorHarness.cs @@ -0,0 +1,73 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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. +// ------------------------------------------------------------------------ + +using System; +using System.Threading; +using System.Threading.Tasks; +using Dapr.TestContainers.Common; +using Dapr.TestContainers.Common.Options; +using Dapr.TestContainers.Containers; +using Dapr.TestContainers.Containers.Dapr; + +namespace Dapr.TestContainers.Harnesses; + +/// +/// Provides an implementation harness for Dapr's actor building block. +/// +/// The directory to Dapr components. +/// The test app to validate in the harness. +/// The dapr runtime options. +public sealed class ActorHarness(string componentsDir, Func startApp, DaprRuntimeOptions options) : BaseHarness +{ + private readonly RedisContainer _redis = new(); + private readonly DaprPlacementContainer _placement = new(options); + private readonly DaprSchedulerContainer _schedueler = new(options); + + /// + public override async Task InitializeAsync(CancellationToken cancellationToken = default) + { + // 1) Start Redis (actor state store) + await _redis.StartAsync(cancellationToken); + + // 2) Emit component YAMLs pointing to Redis + RedisContainer.Yaml.WriteStateStoreYamlToFolder(componentsDir, redisHost: $"{_redis.Host}:{_redis.Port}"); + + // 3) Start placement + await _placement.StartAsync(cancellationToken); + + // 4) Start scheduler + await _schedueler.StartAsync(cancellationToken); + + // 5) Start the test + await startApp(options.AppPort); + + // 6) Configure and start daprd, point at placement & scheduler + _daprd = new DaprdContainer( + appId: "actor-app", + componentsHostFolder: componentsDir, + options: options, + new HostPortPair(_placement.Host, _placement.Port), + new HostPortPair(_schedueler.Host, _schedueler.Port)); + await _daprd.StartAsync(cancellationToken); + } + + /// + public override async ValueTask DisposeAsync() + { + if (_daprd is not null) + await _daprd.DisposeAsync(); + await _redis.DisposeAsync(); + await _placement.DisposeAsync(); + await _schedueler.DisposeAsync(); + } +} diff --git a/src/Dapr.TestContainers/Harnesses/BaseHarness.cs b/src/Dapr.TestContainers/Harnesses/BaseHarness.cs new file mode 100644 index 000000000..4040c5e11 --- /dev/null +++ b/src/Dapr.TestContainers/Harnesses/BaseHarness.cs @@ -0,0 +1,57 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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. +// ------------------------------------------------------------------------ + +using System; +using System.Threading; +using System.Threading.Tasks; +using Dapr.TestContainers.Common; +using Dapr.TestContainers.Containers.Dapr; + +namespace Dapr.TestContainers.Harnesses; + +/// +/// Provides a base harness for building Dapr building block harnesses. +/// +public abstract class BaseHarness : IAsyncContainerFixture +{ + /// + /// The Daprd container exposed by the harness. + /// + private protected DaprdContainer? _daprd; + + /// + /// The HTTP port used by the Daprd container. + /// + public int DaprHttpPort => _daprd?.HttpPort ?? 0; + /// + /// The gRPC port used by the Daprd container. + /// + public int DaprGrpcPort => _daprd?.GrpcPort ?? 0; + + /// + /// Initializes and runs the test app with the harness. + /// + /// Cancellation token. + /// + public abstract Task InitializeAsync(CancellationToken cancellationToken = default); + + /// + /// Disposes the resources in this harness. + /// + public virtual async ValueTask DisposeAsync() + { + if (_daprd is not null) + await _daprd.DisposeAsync(); + GC.SuppressFinalize(this); + } +} diff --git a/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs b/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs new file mode 100644 index 000000000..787b3eb77 --- /dev/null +++ b/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs @@ -0,0 +1,60 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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. +// ------------------------------------------------------------------------ + +using System; +using System.Threading; +using System.Threading.Tasks; +using Dapr.TestContainers.Common.Options; +using Dapr.TestContainers.Containers; +using Dapr.TestContainers.Containers.Dapr; + +namespace Dapr.TestContainers.Harnesses; + +/// +/// Provides an implementation harness for conversation functionality. +/// +/// The directory to Dapr components. +/// The test app to validate in the harness. +/// The Dapr runtime options. +public sealed class ConversationHarness(string componentsDir, Func startApp, DaprRuntimeOptions options) : BaseHarness +{ + private readonly OllamaContainer _ollama = new(); + + /// + public override async Task InitializeAsync(CancellationToken cancellationToken = default) + { + // 1) Start Ollama (conversation) + await _ollama.StartAsync(cancellationToken); + + // 2) Emit component YAMLs for Ollama (use the default tiny model) + OllamaContainer.Yaml.WriteConversationYamlToFolder(componentsDir); + + // 3) Start the app + await startApp(options.AppPort); + + // 4) Configure & start daprd + _daprd = new DaprdContainer( + appId: "distributed-lock-app", + componentsHostFolder: componentsDir, + options: options); + await _daprd.StartAsync(cancellationToken); + } + + /// + public override async ValueTask DisposeAsync() + { + if (_daprd is not null) + await _daprd.DisposeAsync(); + await _ollama.DisposeAsync(); + } +} diff --git a/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs b/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs new file mode 100644 index 000000000..68e2ef2ba --- /dev/null +++ b/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs @@ -0,0 +1,55 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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. +// ------------------------------------------------------------------------ + +using System; +using System.Threading; +using System.Threading.Tasks; +using Dapr.TestContainers.Common.Options; +using Dapr.TestContainers.Containers; +using Dapr.TestContainers.Containers.Dapr; + +namespace Dapr.TestContainers.Harnesses; + +/// +/// Provides an implementation harness for Dapr's cryptography building block. +/// +/// The directory to Dapr components. +/// The test app to validate in the harness. +/// The Dapr runtime options. +/// The path locally to the cryptography keys to use. +public sealed class CryptographyHarness(string componentsDir, Func startApp, string keyPath, DaprRuntimeOptions options) : BaseHarness +{ + /// + public override async Task InitializeAsync(CancellationToken cancellationToken = default) + { + // 1) Emit the component YAML describing the local crypto key store + LocalStorageCryptographyContainer.Yaml.WriteCryptoYamlToFolder(componentsDir, keyPath); + + // 2) Start the app + await startApp(options.AppPort); + + // 3) Configure and start daprd + _daprd = new DaprdContainer( + appId: "crypto-app", + componentsHostFolder: componentsDir, + options: options); + await _daprd.StartAsync(cancellationToken); + } + + /// + public override async ValueTask DisposeAsync() + { + if (_daprd is not null) + await _daprd.DisposeAsync(); + } +} diff --git a/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs b/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs new file mode 100644 index 000000000..b565d4d82 --- /dev/null +++ b/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs @@ -0,0 +1,60 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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. +// ------------------------------------------------------------------------ + +using System; +using System.Threading; +using System.Threading.Tasks; +using Dapr.TestContainers.Common.Options; +using Dapr.TestContainers.Containers; +using Dapr.TestContainers.Containers.Dapr; + +namespace Dapr.TestContainers.Harnesses; + +/// +/// Provides an implementation harness for Dapr's distributed lock building block. +/// +/// The directory to Dapr components. +/// The test app to validate in the harness. +/// The Dapr runtime options. +public sealed class DistributedLockHarness(string componentsDir, Func startApp, DaprRuntimeOptions options) : BaseHarness +{ + private readonly RedisContainer _redis = new(); + + /// + public override async Task InitializeAsync(CancellationToken cancellationToken = default) + { + // 1) Start Redis (state store) + await _redis.StartAsync(cancellationToken); + + // 2) Emit component YAMLs pointing to Redis + RedisContainer.Yaml.WriteDistributedLockYamlToFolder(componentsDir, redisHost: $"{_redis.Host}:{_redis.Port}"); + + // 3) Start the app + await startApp(options.AppPort); + + // 4) Configure & start daprd + _daprd = new DaprdContainer( + appId: "distributed-lock-app", + componentsHostFolder: componentsDir, + options: options); + await _daprd.StartAsync(cancellationToken); + } + + /// + public override async ValueTask DisposeAsync() + { + if (_daprd is not null) + await _daprd.DisposeAsync(); + await _redis.DisposeAsync(); + } +} diff --git a/src/Dapr.TestContainers/Harnesses/JobsHarness.cs b/src/Dapr.TestContainers/Harnesses/JobsHarness.cs new file mode 100644 index 000000000..1b84f715e --- /dev/null +++ b/src/Dapr.TestContainers/Harnesses/JobsHarness.cs @@ -0,0 +1,56 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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. +// ------------------------------------------------------------------------ + +using System; +using System.Threading; +using System.Threading.Tasks; +using Dapr.TestContainers.Common.Options; +using Dapr.TestContainers.Containers.Dapr; + +namespace Dapr.TestContainers.Harnesses; + +/// +/// Provides an implementation harness for the Jobs building block. +/// +/// The directory to Dapr components. +/// The test app to validate in the harness. +/// The Dapr runtime options. +public sealed class JobsHarness(string componentsDir, Func startApp, DaprRuntimeOptions options) : BaseHarness +{ + private readonly DaprSchedulerContainer _scheduler = new(options); + + /// + public override async Task InitializeAsync(CancellationToken cancellationToken = default) + { + // 1) Start the Dapr Scheduler + await _scheduler.StartAsync(cancellationToken); + + // 2) Start the app + await startApp(options.AppPort); + + // 3) Configure & start daprd + _daprd = new DaprdContainer( + appId: "jobs-app", + componentsHostFolder: componentsDir, + options: options); + await _daprd.StartAsync(cancellationToken); + } + + /// + public override async ValueTask DisposeAsync() + { + if (_daprd is not null) + await _daprd.DisposeAsync(); + await _scheduler.DisposeAsync(); + } +} diff --git a/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs b/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs new file mode 100644 index 000000000..ce56c72e6 --- /dev/null +++ b/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs @@ -0,0 +1,60 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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. +// ------------------------------------------------------------------------ + +using System; +using System.Threading; +using System.Threading.Tasks; +using Dapr.TestContainers.Common.Options; +using Dapr.TestContainers.Containers; +using Dapr.TestContainers.Containers.Dapr; + +namespace Dapr.TestContainers.Harnesses; + +/// +/// Provides an implementation harness for Dapr's pub/sub building block. +/// +/// The directory to Dapr components. +/// The test app to validate in the harness. +/// The Dapr runtime options. +public sealed class PubSubHarness(string componentsDir, Func startApp, DaprRuntimeOptions options) : BaseHarness +{ + private readonly RabbitMqContainer _rabbitmq = new(); + + /// + public override async Task InitializeAsync(CancellationToken cancellationToken = default) + { + // 1) Start RabbitMq (pubsub) + await _rabbitmq.StartAsync(cancellationToken); + + // 2) Emit component YAMLs pointing to RabbitMQ + RabbitMqContainer.Yaml.WritePubSubYamlToFolder(componentsDir); + + // 3) Start the test app + await startApp(options.AppPort); + + // 4) Configure & start daprd + _daprd = new DaprdContainer( + appId: "pubsub-app", + componentsHostFolder: componentsDir, + options: options); + await _daprd.StartAsync(cancellationToken); + } + + /// + public override async ValueTask DisposeAsync() + { + if (_daprd is not null) + await _daprd.DisposeAsync(); + await _rabbitmq.DisposeAsync(); + } +} diff --git a/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs b/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs new file mode 100644 index 000000000..0fd870d06 --- /dev/null +++ b/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs @@ -0,0 +1,60 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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. +// ------------------------------------------------------------------------ + +using System; +using System.Threading; +using System.Threading.Tasks; +using Dapr.TestContainers.Common.Options; +using Dapr.TestContainers.Containers; +using Dapr.TestContainers.Containers.Dapr; + +namespace Dapr.TestContainers.Harnesses; + +/// +/// Provides an implementation harness for Dapr's state management building block. +/// +/// The directory to Dapr components. +/// The test app to validate in the harness. +/// The Dapr runtime options. +public sealed class StateManagementHarness(string componentsDir, Func startApp, DaprRuntimeOptions options) : BaseHarness +{ + private readonly RedisContainer _redis = new(); + + /// + public override async Task InitializeAsync(CancellationToken cancellationToken = default) + { + // 1) Start Redis (state store) + await _redis.StartAsync(cancellationToken); + + // 2) Emit component YAMLs pointing to Redis + RedisContainer.Yaml.WriteStateStoreYamlToFolder(componentsDir, redisHost: $"{_redis.Host}:{_redis.Port}"); + + // 3) Start the app + await startApp(options.AppPort); + + // 4) Configure & start daprd + _daprd = new DaprdContainer( + appId: "start-management-app", + componentsHostFolder: componentsDir, + options: options); + await _daprd.StartAsync(cancellationToken); + } + + /// + public override async ValueTask DisposeAsync() + { + if (_daprd is not null) + await _daprd.DisposeAsync(); + await _redis.DisposeAsync(); + } +} diff --git a/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs b/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs new file mode 100644 index 000000000..1922ceb6e --- /dev/null +++ b/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs @@ -0,0 +1,73 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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. +// ------------------------------------------------------------------------ + +using System; +using System.Threading; +using System.Threading.Tasks; +using Dapr.TestContainers.Common; +using Dapr.TestContainers.Common.Options; +using Dapr.TestContainers.Containers; +using Dapr.TestContainers.Containers.Dapr; + +namespace Dapr.TestContainers.Harnesses; + +/// +/// Provides an implementation harness for Dapr's Workflow building block. +/// +/// The directory to Dapr components. +/// The test app to validate in the harness. +/// The Dapr runtime options. +public sealed class WorkflowHarness(string componentsDir, Func startApp, DaprRuntimeOptions options) : BaseHarness +{ + private readonly RedisContainer _redis = new(); + private readonly DaprPlacementContainer _placement = new(options); + private readonly DaprSchedulerContainer _scheduler = new(options); + + /// + public override async Task InitializeAsync(CancellationToken cancellationToken = default) + { + // 1) Start Redis (actor state store) + await _redis.StartAsync(cancellationToken); + + // 2) Emit component YAMLs pointing to Redis + RedisContainer.Yaml.WriteStateStoreYamlToFolder(componentsDir, redisHost: $"{_redis.Host}:{_redis.Port}"); + + // 3) Start placement + await _placement.StartAsync(cancellationToken); + + // 4) Start scheduler + await _scheduler.StartAsync(cancellationToken); + + // 5) Start the test + await startApp(options.AppPort); + + // 6) Configure and start daprd; point at placement & scheduler + _daprd = new DaprdContainer( + appId: "workflow-app", + componentsHostFolder: componentsDir, + options: options, + new HostPortPair(_placement.Host, _placement.Port), + new HostPortPair(_scheduler.Host, _scheduler.Port)); + await _daprd.StartAsync(cancellationToken); + } + + /// + public override async ValueTask DisposeAsync() + { + if (_daprd is not null) + await _daprd.DisposeAsync(); + await _placement.DisposeAsync(); + await _scheduler.DisposeAsync(); + await _redis.DisposeAsync(); + } +} From b4623e0364cb22bb4c8301f52e4457ad8e653661 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sat, 20 Dec 2025 04:52:16 -0600 Subject: [PATCH 03/32] Updated to apply a configuration for environment variables to be set based on the container configuration Signed-off-by: Whit Waldo --- .../Harnesses/ActorHarness.cs | 2 +- .../Harnesses/BaseHarness.cs | 23 ++++++++++++++++++- .../Harnesses/ConversationHarness.cs | 2 +- .../Harnesses/CryptographyHarness.cs | 2 +- .../Harnesses/DistributedLockHarness.cs | 2 +- .../Harnesses/JobsHarness.cs | 2 +- .../Harnesses/PubSubHarness.cs | 2 +- .../Harnesses/StateManagementHarness.cs | 2 +- .../Harnesses/WorkflowHarness.cs | 2 +- 9 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/Dapr.TestContainers/Harnesses/ActorHarness.cs b/src/Dapr.TestContainers/Harnesses/ActorHarness.cs index 89df8c3b6..d86889799 100644 --- a/src/Dapr.TestContainers/Harnesses/ActorHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/ActorHarness.cs @@ -34,7 +34,7 @@ public sealed class ActorHarness(string componentsDir, Func startApp, private readonly DaprSchedulerContainer _schedueler = new(options); /// - public override async Task InitializeAsync(CancellationToken cancellationToken = default) + protected override async Task OnInitializeAsync(CancellationToken cancellationToken) { // 1) Start Redis (actor state store) await _redis.StartAsync(cancellationToken); diff --git a/src/Dapr.TestContainers/Harnesses/BaseHarness.cs b/src/Dapr.TestContainers/Harnesses/BaseHarness.cs index 4040c5e11..d84737890 100644 --- a/src/Dapr.TestContainers/Harnesses/BaseHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/BaseHarness.cs @@ -38,12 +38,33 @@ public abstract class BaseHarness : IAsyncContainerFixture /// public int DaprGrpcPort => _daprd?.GrpcPort ?? 0; + /// + /// The specific container startup logic for the harness. + /// + /// Cancellation token. + /// + protected abstract Task OnInitializeAsync(CancellationToken cancellationToken); + + protected void ConfigureSdkEnvironment() + { + if (DaprHttpPort > 0) + Environment.SetEnvironmentVariable("DAPR_HTTP_PORT", DaprHttpPort.ToString()); + if (DaprGrpcPort > 0) + Environment.SetEnvironmentVariable("DAPR_GRPC_PORT", DaprGrpcPort.ToString()); + } + /// /// Initializes and runs the test app with the harness. /// /// Cancellation token. /// - public abstract Task InitializeAsync(CancellationToken cancellationToken = default); + public async Task InitializeAsync(CancellationToken cancellationToken = default) + { + // Automatically link the Dapr .NET SDK to these containers + ConfigureSdkEnvironment(); + // Run the actual container orchestration defined in the subclass + await OnInitializeAsync(cancellationToken); + } /// /// Disposes the resources in this harness. diff --git a/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs b/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs index 787b3eb77..eeda6e5d4 100644 --- a/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs @@ -31,7 +31,7 @@ public sealed class ConversationHarness(string componentsDir, Func st private readonly OllamaContainer _ollama = new(); /// - public override async Task InitializeAsync(CancellationToken cancellationToken = default) + protected override async Task OnInitializeAsync(CancellationToken cancellationToken) { // 1) Start Ollama (conversation) await _ollama.StartAsync(cancellationToken); diff --git a/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs b/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs index 68e2ef2ba..feccdcce4 100644 --- a/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs @@ -30,7 +30,7 @@ namespace Dapr.TestContainers.Harnesses; public sealed class CryptographyHarness(string componentsDir, Func startApp, string keyPath, DaprRuntimeOptions options) : BaseHarness { /// - public override async Task InitializeAsync(CancellationToken cancellationToken = default) + protected override async Task OnInitializeAsync(CancellationToken cancellationToken) { // 1) Emit the component YAML describing the local crypto key store LocalStorageCryptographyContainer.Yaml.WriteCryptoYamlToFolder(componentsDir, keyPath); diff --git a/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs b/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs index b565d4d82..1a4e685e7 100644 --- a/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs @@ -31,7 +31,7 @@ public sealed class DistributedLockHarness(string componentsDir, Func private readonly RedisContainer _redis = new(); /// - public override async Task InitializeAsync(CancellationToken cancellationToken = default) + protected override async Task OnInitializeAsync(CancellationToken cancellationToken) { // 1) Start Redis (state store) await _redis.StartAsync(cancellationToken); diff --git a/src/Dapr.TestContainers/Harnesses/JobsHarness.cs b/src/Dapr.TestContainers/Harnesses/JobsHarness.cs index 1b84f715e..68bc0b6b4 100644 --- a/src/Dapr.TestContainers/Harnesses/JobsHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/JobsHarness.cs @@ -30,7 +30,7 @@ public sealed class JobsHarness(string componentsDir, Func startApp, private readonly DaprSchedulerContainer _scheduler = new(options); /// - public override async Task InitializeAsync(CancellationToken cancellationToken = default) + protected override async Task OnInitializeAsync(CancellationToken cancellationToken) { // 1) Start the Dapr Scheduler await _scheduler.StartAsync(cancellationToken); diff --git a/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs b/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs index ce56c72e6..d406dae51 100644 --- a/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs @@ -31,7 +31,7 @@ public sealed class PubSubHarness(string componentsDir, Func startApp private readonly RabbitMqContainer _rabbitmq = new(); /// - public override async Task InitializeAsync(CancellationToken cancellationToken = default) + protected override async Task OnInitializeAsync(CancellationToken cancellationToken) { // 1) Start RabbitMq (pubsub) await _rabbitmq.StartAsync(cancellationToken); diff --git a/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs b/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs index 0fd870d06..0226f3512 100644 --- a/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs @@ -31,7 +31,7 @@ public sealed class StateManagementHarness(string componentsDir, Func private readonly RedisContainer _redis = new(); /// - public override async Task InitializeAsync(CancellationToken cancellationToken = default) + protected override async Task OnInitializeAsync(CancellationToken cancellationToken) { // 1) Start Redis (state store) await _redis.StartAsync(cancellationToken); diff --git a/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs b/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs index 1922ceb6e..d4057cf0d 100644 --- a/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs @@ -34,7 +34,7 @@ public sealed class WorkflowHarness(string componentsDir, Func startA private readonly DaprSchedulerContainer _scheduler = new(options); /// - public override async Task InitializeAsync(CancellationToken cancellationToken = default) + protected override async Task OnInitializeAsync(CancellationToken cancellationToken) { // 1) Start Redis (actor state store) await _redis.StartAsync(cancellationToken); From d0b2e0d6dff1e106fbda76fc537ef5c05a20591d Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sat, 20 Dec 2025 05:00:36 -0600 Subject: [PATCH 04/32] Updated to use the app port reported by the test app instead of using a configuration value Signed-off-by: Whit Waldo --- .../Common/DaprHarnessBuilder.cs | 2 +- .../Common/Options/DaprRuntimeOptions.cs | 17 ++++++----------- .../Harnesses/ActorHarness.cs | 6 +++--- .../Harnesses/ConversationHarness.cs | 6 +++--- .../Harnesses/CryptographyHarness.cs | 6 +++--- .../Harnesses/DistributedLockHarness.cs | 6 +++--- .../Harnesses/JobsHarness.cs | 6 +++--- .../Harnesses/PubSubHarness.cs | 6 +++--- .../Harnesses/StateManagementHarness.cs | 6 +++--- .../Harnesses/WorkflowHarness.cs | 6 +++--- 10 files changed, 31 insertions(+), 36 deletions(-) diff --git a/src/Dapr.TestContainers/Common/DaprHarnessBuilder.cs b/src/Dapr.TestContainers/Common/DaprHarnessBuilder.cs index b50956b87..1da14b1f1 100644 --- a/src/Dapr.TestContainers/Common/DaprHarnessBuilder.cs +++ b/src/Dapr.TestContainers/Common/DaprHarnessBuilder.cs @@ -23,7 +23,7 @@ namespace Dapr.TestContainers.Common; /// /// The Dapr runtime options. /// The test app to run. -public sealed class DaprHarnessBuilder(DaprRuntimeOptions options, Func startApp) +public sealed class DaprHarnessBuilder(DaprRuntimeOptions options, Func> startApp) { /// /// Builds a workflow harness. diff --git a/src/Dapr.TestContainers/Common/Options/DaprRuntimeOptions.cs b/src/Dapr.TestContainers/Common/Options/DaprRuntimeOptions.cs index 138848d43..681b0b07b 100644 --- a/src/Dapr.TestContainers/Common/Options/DaprRuntimeOptions.cs +++ b/src/Dapr.TestContainers/Common/Options/DaprRuntimeOptions.cs @@ -16,15 +16,10 @@ namespace Dapr.TestContainers.Common.Options; /// /// The various options used to spin up the Dapr containers. /// -/// The port of the test app. -/// The version of the Dapr images to use. -public sealed class DaprRuntimeOptions(int appPort, string version = "1.16.4") +/// The port of the test app. +/// The version of the Dapr images to use. +public sealed record DaprRuntimeOptions(int AppPort, string Version = "1.16.4") { - /// - /// The port of the test app. - /// - public int AppPort => appPort; - /// /// The level of Dapr logs to show. /// @@ -33,15 +28,15 @@ public sealed class DaprRuntimeOptions(int appPort, string version = "1.16.4") /// /// The image tag for the Dapr runtime. /// - public string RuntimeImageTag => $"daprio/daprd:{version}"; + public string RuntimeImageTag => $"daprio/daprd:{Version}"; /// /// The image tag for the Dapr placement service. /// - public string PlacementImageTag => $"daprio/placement:{version}"; + public string PlacementImageTag => $"daprio/placement:{Version}"; /// /// The image tag for the Dapr scheduler service. /// - public string SchedulerImageTag => $"daprio/scheduler:{version}"; + public string SchedulerImageTag => $"daprio/scheduler:{Version}"; /// /// Sets the Dapr log level. diff --git a/src/Dapr.TestContainers/Harnesses/ActorHarness.cs b/src/Dapr.TestContainers/Harnesses/ActorHarness.cs index d86889799..38733c4cb 100644 --- a/src/Dapr.TestContainers/Harnesses/ActorHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/ActorHarness.cs @@ -27,7 +27,7 @@ namespace Dapr.TestContainers.Harnesses; /// The directory to Dapr components. /// The test app to validate in the harness. /// The dapr runtime options. -public sealed class ActorHarness(string componentsDir, Func startApp, DaprRuntimeOptions options) : BaseHarness +public sealed class ActorHarness(string componentsDir, Func> startApp, DaprRuntimeOptions options) : BaseHarness { private readonly RedisContainer _redis = new(); private readonly DaprPlacementContainer _placement = new(options); @@ -49,13 +49,13 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo await _schedueler.StartAsync(cancellationToken); // 5) Start the test - await startApp(options.AppPort); + var actualAppPort = await startApp(); // 6) Configure and start daprd, point at placement & scheduler _daprd = new DaprdContainer( appId: "actor-app", componentsHostFolder: componentsDir, - options: options, + options: options with {AppPort = actualAppPort}, new HostPortPair(_placement.Host, _placement.Port), new HostPortPair(_schedueler.Host, _schedueler.Port)); await _daprd.StartAsync(cancellationToken); diff --git a/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs b/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs index eeda6e5d4..9110b044a 100644 --- a/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs @@ -26,7 +26,7 @@ namespace Dapr.TestContainers.Harnesses; /// The directory to Dapr components. /// The test app to validate in the harness. /// The Dapr runtime options. -public sealed class ConversationHarness(string componentsDir, Func startApp, DaprRuntimeOptions options) : BaseHarness +public sealed class ConversationHarness(string componentsDir, Func> startApp, DaprRuntimeOptions options) : BaseHarness { private readonly OllamaContainer _ollama = new(); @@ -40,13 +40,13 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo OllamaContainer.Yaml.WriteConversationYamlToFolder(componentsDir); // 3) Start the app - await startApp(options.AppPort); + var actualAppPort = await startApp(); // 4) Configure & start daprd _daprd = new DaprdContainer( appId: "distributed-lock-app", componentsHostFolder: componentsDir, - options: options); + options: options with {AppPort = actualAppPort}); await _daprd.StartAsync(cancellationToken); } diff --git a/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs b/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs index feccdcce4..4083b52d0 100644 --- a/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs @@ -27,7 +27,7 @@ namespace Dapr.TestContainers.Harnesses; /// The test app to validate in the harness. /// The Dapr runtime options. /// The path locally to the cryptography keys to use. -public sealed class CryptographyHarness(string componentsDir, Func startApp, string keyPath, DaprRuntimeOptions options) : BaseHarness +public sealed class CryptographyHarness(string componentsDir, Func>startApp, string keyPath, DaprRuntimeOptions options) : BaseHarness { /// protected override async Task OnInitializeAsync(CancellationToken cancellationToken) @@ -36,13 +36,13 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo LocalStorageCryptographyContainer.Yaml.WriteCryptoYamlToFolder(componentsDir, keyPath); // 2) Start the app - await startApp(options.AppPort); + var actualAppPort = await startApp(); // 3) Configure and start daprd _daprd = new DaprdContainer( appId: "crypto-app", componentsHostFolder: componentsDir, - options: options); + options: options with {AppPort = actualAppPort}); await _daprd.StartAsync(cancellationToken); } diff --git a/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs b/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs index 1a4e685e7..940410bf1 100644 --- a/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs @@ -26,7 +26,7 @@ namespace Dapr.TestContainers.Harnesses; /// The directory to Dapr components. /// The test app to validate in the harness. /// The Dapr runtime options. -public sealed class DistributedLockHarness(string componentsDir, Func startApp, DaprRuntimeOptions options) : BaseHarness +public sealed class DistributedLockHarness(string componentsDir, Func> startApp, DaprRuntimeOptions options) : BaseHarness { private readonly RedisContainer _redis = new(); @@ -40,13 +40,13 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo RedisContainer.Yaml.WriteDistributedLockYamlToFolder(componentsDir, redisHost: $"{_redis.Host}:{_redis.Port}"); // 3) Start the app - await startApp(options.AppPort); + var actualAppPort = await startApp(); // 4) Configure & start daprd _daprd = new DaprdContainer( appId: "distributed-lock-app", componentsHostFolder: componentsDir, - options: options); + options: options with {AppPort = actualAppPort}); await _daprd.StartAsync(cancellationToken); } diff --git a/src/Dapr.TestContainers/Harnesses/JobsHarness.cs b/src/Dapr.TestContainers/Harnesses/JobsHarness.cs index 68bc0b6b4..2dd5f30e8 100644 --- a/src/Dapr.TestContainers/Harnesses/JobsHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/JobsHarness.cs @@ -25,7 +25,7 @@ namespace Dapr.TestContainers.Harnesses; /// The directory to Dapr components. /// The test app to validate in the harness. /// The Dapr runtime options. -public sealed class JobsHarness(string componentsDir, Func startApp, DaprRuntimeOptions options) : BaseHarness +public sealed class JobsHarness(string componentsDir, Func> startApp, DaprRuntimeOptions options) : BaseHarness { private readonly DaprSchedulerContainer _scheduler = new(options); @@ -36,13 +36,13 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo await _scheduler.StartAsync(cancellationToken); // 2) Start the app - await startApp(options.AppPort); + var actualAppPort = await startApp(); // 3) Configure & start daprd _daprd = new DaprdContainer( appId: "jobs-app", componentsHostFolder: componentsDir, - options: options); + options: options with {AppPort = actualAppPort}); await _daprd.StartAsync(cancellationToken); } diff --git a/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs b/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs index d406dae51..5a8db812a 100644 --- a/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs @@ -26,7 +26,7 @@ namespace Dapr.TestContainers.Harnesses; /// The directory to Dapr components. /// The test app to validate in the harness. /// The Dapr runtime options. -public sealed class PubSubHarness(string componentsDir, Func startApp, DaprRuntimeOptions options) : BaseHarness +public sealed class PubSubHarness(string componentsDir, Func> startApp, DaprRuntimeOptions options) : BaseHarness { private readonly RabbitMqContainer _rabbitmq = new(); @@ -40,13 +40,13 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo RabbitMqContainer.Yaml.WritePubSubYamlToFolder(componentsDir); // 3) Start the test app - await startApp(options.AppPort); + var actualAppPort = await startApp(); // 4) Configure & start daprd _daprd = new DaprdContainer( appId: "pubsub-app", componentsHostFolder: componentsDir, - options: options); + options: options with {AppPort = actualAppPort}); await _daprd.StartAsync(cancellationToken); } diff --git a/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs b/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs index 0226f3512..be2f07c54 100644 --- a/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs @@ -26,7 +26,7 @@ namespace Dapr.TestContainers.Harnesses; /// The directory to Dapr components. /// The test app to validate in the harness. /// The Dapr runtime options. -public sealed class StateManagementHarness(string componentsDir, Func startApp, DaprRuntimeOptions options) : BaseHarness +public sealed class StateManagementHarness(string componentsDir, Func> startApp, DaprRuntimeOptions options) : BaseHarness { private readonly RedisContainer _redis = new(); @@ -40,13 +40,13 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo RedisContainer.Yaml.WriteStateStoreYamlToFolder(componentsDir, redisHost: $"{_redis.Host}:{_redis.Port}"); // 3) Start the app - await startApp(options.AppPort); + var actualAppPort = await startApp(); // 4) Configure & start daprd _daprd = new DaprdContainer( appId: "start-management-app", componentsHostFolder: componentsDir, - options: options); + options: options with {AppPort = actualAppPort}); await _daprd.StartAsync(cancellationToken); } diff --git a/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs b/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs index d4057cf0d..621ed5b30 100644 --- a/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs @@ -27,7 +27,7 @@ namespace Dapr.TestContainers.Harnesses; /// The directory to Dapr components. /// The test app to validate in the harness. /// The Dapr runtime options. -public sealed class WorkflowHarness(string componentsDir, Func startApp, DaprRuntimeOptions options) : BaseHarness +public sealed class WorkflowHarness(string componentsDir, Func> startApp, DaprRuntimeOptions options) : BaseHarness { private readonly RedisContainer _redis = new(); private readonly DaprPlacementContainer _placement = new(options); @@ -49,13 +49,13 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo await _scheduler.StartAsync(cancellationToken); // 5) Start the test - await startApp(options.AppPort); + var actualAppPort = await startApp(); // 6) Configure and start daprd; point at placement & scheduler _daprd = new DaprdContainer( appId: "workflow-app", componentsHostFolder: componentsDir, - options: options, + options: options with {AppPort = actualAppPort}, new HostPortPair(_placement.Host, _placement.Port), new HostPortPair(_scheduler.Host, _scheduler.Port)); await _daprd.StartAsync(cancellationToken); From 7bf491cc2159be55b598e2cfb454ce96380260df Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sat, 20 Dec 2025 05:01:16 -0600 Subject: [PATCH 05/32] Minor refactoring Signed-off-by: Whit Waldo --- .../Harnesses/BaseHarness.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Dapr.TestContainers/Harnesses/BaseHarness.cs b/src/Dapr.TestContainers/Harnesses/BaseHarness.cs index d84737890..88cf91283 100644 --- a/src/Dapr.TestContainers/Harnesses/BaseHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/BaseHarness.cs @@ -45,14 +45,6 @@ public abstract class BaseHarness : IAsyncContainerFixture /// protected abstract Task OnInitializeAsync(CancellationToken cancellationToken); - protected void ConfigureSdkEnvironment() - { - if (DaprHttpPort > 0) - Environment.SetEnvironmentVariable("DAPR_HTTP_PORT", DaprHttpPort.ToString()); - if (DaprGrpcPort > 0) - Environment.SetEnvironmentVariable("DAPR_GRPC_PORT", DaprGrpcPort.ToString()); - } - /// /// Initializes and runs the test app with the harness. /// @@ -75,4 +67,13 @@ public virtual async ValueTask DisposeAsync() await _daprd.DisposeAsync(); GC.SuppressFinalize(this); } + + private void ConfigureSdkEnvironment() + { + if (DaprHttpPort > 0) + Environment.SetEnvironmentVariable("DAPR_HTTP_PORT", DaprHttpPort.ToString()); + if (DaprGrpcPort > 0) + Environment.SetEnvironmentVariable("DAPR_GRPC_PORT", DaprGrpcPort.ToString()); + } + } From b63579e09e89d56b63a2b4f9c805152947f0ecc1 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sat, 20 Dec 2025 05:34:28 -0600 Subject: [PATCH 06/32] Added support for using a shared Docker network across the containers and harnesses Signed-off-by: Whit Waldo --- .../Containers/Dapr/DaprPlacementContainer.cs | 5 ++++- .../Containers/Dapr/DaprSchedulerContainer.cs | 4 +++- src/Dapr.TestContainers/Containers/Dapr/DaprdContainer.cs | 5 ++++- src/Dapr.TestContainers/Containers/OllamaContainer.cs | 4 +++- src/Dapr.TestContainers/Containers/RabbitMqContainer.cs | 4 +++- src/Dapr.TestContainers/Containers/RedisContainer.cs | 4 +++- src/Dapr.TestContainers/Harnesses/ActorHarness.cs | 7 ++++--- src/Dapr.TestContainers/Harnesses/BaseHarness.cs | 8 ++++++++ src/Dapr.TestContainers/Harnesses/ConversationHarness.cs | 5 +++-- src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs | 3 ++- .../Harnesses/DistributedLockHarness.cs | 5 +++-- src/Dapr.TestContainers/Harnesses/JobsHarness.cs | 5 +++-- src/Dapr.TestContainers/Harnesses/PubSubHarness.cs | 5 +++-- .../Harnesses/StateManagementHarness.cs | 5 +++-- src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs | 7 ++++--- 15 files changed, 53 insertions(+), 23 deletions(-) diff --git a/src/Dapr.TestContainers/Containers/Dapr/DaprPlacementContainer.cs b/src/Dapr.TestContainers/Containers/Dapr/DaprPlacementContainer.cs index a62e05658..67f51f745 100644 --- a/src/Dapr.TestContainers/Containers/Dapr/DaprPlacementContainer.cs +++ b/src/Dapr.TestContainers/Containers/Dapr/DaprPlacementContainer.cs @@ -18,6 +18,7 @@ using Dapr.TestContainers.Common.Options; using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Containers; +using DotNet.Testcontainers.Networks; namespace Dapr.TestContainers.Containers.Dapr; @@ -42,12 +43,14 @@ public sealed class DaprPlacementContainer : IAsyncStartable /// Initializes a new instance of the . /// /// The Dapr runtime options. - public DaprPlacementContainer(DaprRuntimeOptions options) + /// The shared Docker network to connect to. + public DaprPlacementContainer(DaprRuntimeOptions options, INetwork network) { //Placement service runs via port 50006 _container = new ContainerBuilder() .WithImage(options.PlacementImageTag) .WithName($"placement-{Guid.NewGuid():N}") + .WithNetwork(network) .WithCommand("./placement", "-port", InternalPort.ToString()) .WithPortBinding(InternalPort, assignRandomHostPort: true) .WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(InternalPort)) diff --git a/src/Dapr.TestContainers/Containers/Dapr/DaprSchedulerContainer.cs b/src/Dapr.TestContainers/Containers/Dapr/DaprSchedulerContainer.cs index da5d82c2e..097469bf6 100644 --- a/src/Dapr.TestContainers/Containers/Dapr/DaprSchedulerContainer.cs +++ b/src/Dapr.TestContainers/Containers/Dapr/DaprSchedulerContainer.cs @@ -18,6 +18,7 @@ using Dapr.TestContainers.Common.Options; using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Containers; +using DotNet.Testcontainers.Networks; namespace Dapr.TestContainers.Containers.Dapr; @@ -41,12 +42,13 @@ public sealed class DaprSchedulerContainer : IAsyncStartable /// /// Creates a new instance of a . /// - public DaprSchedulerContainer(DaprRuntimeOptions options) + public DaprSchedulerContainer(DaprRuntimeOptions options, INetwork network) { // Scheduler service runs via port 51005 _container = new ContainerBuilder() .WithImage(options.SchedulerImageTag) .WithName($"scheduler-{Guid.NewGuid():N}") + .WithNetwork(network) .WithCommand("./scheduler", InternalPort.ToString(), "-etcd-data-dir", ".") .WithPortBinding(InternalPort, assignRandomHostPort: true) .WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(InternalPort)) diff --git a/src/Dapr.TestContainers/Containers/Dapr/DaprdContainer.cs b/src/Dapr.TestContainers/Containers/Dapr/DaprdContainer.cs index c3b72c687..5f19483f7 100644 --- a/src/Dapr.TestContainers/Containers/Dapr/DaprdContainer.cs +++ b/src/Dapr.TestContainers/Containers/Dapr/DaprdContainer.cs @@ -20,6 +20,7 @@ using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Configurations; using DotNet.Testcontainers.Containers; +using DotNet.Testcontainers.Networks; namespace Dapr.TestContainers.Containers.Dapr; @@ -47,9 +48,10 @@ public sealed class DaprdContainer : IAsyncStartable /// The ID of the app to initialize daprd with. /// The path to the Dapr resources directory. /// The Dapr runtime options. + /// The shared Docker network to connect to. /// The hostname and port of the Placement service. /// The hostname and port of the Scheduler service. - public DaprdContainer(string appId, string componentsHostFolder, DaprRuntimeOptions options, HostPortPair? placementHostAndPort = null, HostPortPair? schedulerHostAndPort = null) + public DaprdContainer(string appId, string componentsHostFolder, DaprRuntimeOptions options, INetwork netowrk, HostPortPair? placementHostAndPort = null, HostPortPair? schedulerHostAndPort = null) { var cmd = new List @@ -79,6 +81,7 @@ public DaprdContainer(string appId, string componentsHostFolder, DaprRuntimeOpti .WithImage(options.RuntimeImageTag) .WithName($"dapr-{Guid.NewGuid():N}") .WithCommand(cmd.ToArray()) + .WithNetwork(netowrk) .WithPortBinding(HttpPort, assignRandomHostPort: true) .WithPortBinding(GrpcPort, assignRandomHostPort: true) .WithBindMount(componentsHostFolder, "/components", AccessMode.ReadOnly) diff --git a/src/Dapr.TestContainers/Containers/OllamaContainer.cs b/src/Dapr.TestContainers/Containers/OllamaContainer.cs index 54ff43d00..4ee3b3247 100644 --- a/src/Dapr.TestContainers/Containers/OllamaContainer.cs +++ b/src/Dapr.TestContainers/Containers/OllamaContainer.cs @@ -18,19 +18,21 @@ using Dapr.TestContainers.Common; using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Containers; +using DotNet.Testcontainers.Networks; namespace Dapr.TestContainers.Containers; /// /// Provides an Ollama container. /// -public sealed class OllamaContainer : IAsyncStartable +public sealed class OllamaContainer(INetwork network) : IAsyncStartable { private const int InternalPort = 11434; private readonly IContainer _container = new ContainerBuilder() .WithImage("ollama/ollama") .WithName($"ollama-{Guid.NewGuid():N}") + .WithNetwork(network) .WithPortBinding(InternalPort, assignRandomHostPort: true) .WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(InternalPort)) .Build(); diff --git a/src/Dapr.TestContainers/Containers/RabbitMqContainer.cs b/src/Dapr.TestContainers/Containers/RabbitMqContainer.cs index 0785d6b0a..daa1aef30 100644 --- a/src/Dapr.TestContainers/Containers/RabbitMqContainer.cs +++ b/src/Dapr.TestContainers/Containers/RabbitMqContainer.cs @@ -18,19 +18,21 @@ using Dapr.TestContainers.Common; using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Containers; +using DotNet.Testcontainers.Networks; namespace Dapr.TestContainers.Containers; /// /// Provides a RabbitMQ container. /// -public sealed class RabbitMqContainer : IAsyncStartable +public sealed class RabbitMqContainer(INetwork network) : IAsyncStartable { private const int InternalPort = 5672; private readonly IContainer _container = new ContainerBuilder() .WithImage("rabbitmq:alpine") .WithName($"rabbitmq-{Guid.NewGuid():N}") + .WithNetwork(network) .WithPortBinding(InternalPort, assignRandomHostPort: true) .WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(InternalPort)) .Build(); diff --git a/src/Dapr.TestContainers/Containers/RedisContainer.cs b/src/Dapr.TestContainers/Containers/RedisContainer.cs index fcf305993..a5a8b9934 100644 --- a/src/Dapr.TestContainers/Containers/RedisContainer.cs +++ b/src/Dapr.TestContainers/Containers/RedisContainer.cs @@ -18,18 +18,20 @@ using Dapr.TestContainers.Common; using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Containers; +using DotNet.Testcontainers.Networks; namespace Dapr.TestContainers.Containers; /// /// Provides a Redis container. /// -public sealed class RedisContainer : IAsyncStartable +public sealed class RedisContainer(INetwork network) : IAsyncStartable { private const int InternalPort = 6379; private readonly IContainer _container = new ContainerBuilder() .WithImage("redis:alpine") .WithName($"redis-{Guid.NewGuid():N}") + .WithNetwork(network) .WithPortBinding(InternalPort, assignRandomHostPort: true) .WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(InternalPort)) .Build(); diff --git a/src/Dapr.TestContainers/Harnesses/ActorHarness.cs b/src/Dapr.TestContainers/Harnesses/ActorHarness.cs index 38733c4cb..d67aad7b3 100644 --- a/src/Dapr.TestContainers/Harnesses/ActorHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/ActorHarness.cs @@ -29,9 +29,9 @@ namespace Dapr.TestContainers.Harnesses; /// The dapr runtime options. public sealed class ActorHarness(string componentsDir, Func> startApp, DaprRuntimeOptions options) : BaseHarness { - private readonly RedisContainer _redis = new(); - private readonly DaprPlacementContainer _placement = new(options); - private readonly DaprSchedulerContainer _schedueler = new(options); + private readonly RedisContainer _redis = new(Network); + private readonly DaprPlacementContainer _placement = new(options, Network); + private readonly DaprSchedulerContainer _schedueler = new(options, Network); /// protected override async Task OnInitializeAsync(CancellationToken cancellationToken) @@ -56,6 +56,7 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo appId: "actor-app", componentsHostFolder: componentsDir, options: options with {AppPort = actualAppPort}, + Network, new HostPortPair(_placement.Host, _placement.Port), new HostPortPair(_schedueler.Host, _schedueler.Port)); await _daprd.StartAsync(cancellationToken); diff --git a/src/Dapr.TestContainers/Harnesses/BaseHarness.cs b/src/Dapr.TestContainers/Harnesses/BaseHarness.cs index 88cf91283..59a0a7997 100644 --- a/src/Dapr.TestContainers/Harnesses/BaseHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/BaseHarness.cs @@ -16,6 +16,8 @@ using System.Threading.Tasks; using Dapr.TestContainers.Common; using Dapr.TestContainers.Containers.Dapr; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Networks; namespace Dapr.TestContainers.Harnesses; @@ -28,6 +30,11 @@ public abstract class BaseHarness : IAsyncContainerFixture /// The Daprd container exposed by the harness. /// private protected DaprdContainer? _daprd; + + /// + /// A shared Docker network that's safer for CI environments. + /// + protected static readonly INetwork Network = new NetworkBuilder().Build(); /// /// The HTTP port used by the Daprd container. @@ -65,6 +72,7 @@ public virtual async ValueTask DisposeAsync() { if (_daprd is not null) await _daprd.DisposeAsync(); + await Network.DisposeAsync(); GC.SuppressFinalize(this); } diff --git a/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs b/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs index 9110b044a..548805890 100644 --- a/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs @@ -28,7 +28,7 @@ namespace Dapr.TestContainers.Harnesses; /// The Dapr runtime options. public sealed class ConversationHarness(string componentsDir, Func> startApp, DaprRuntimeOptions options) : BaseHarness { - private readonly OllamaContainer _ollama = new(); + private readonly OllamaContainer _ollama = new(Network); /// protected override async Task OnInitializeAsync(CancellationToken cancellationToken) @@ -46,7 +46,8 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo _daprd = new DaprdContainer( appId: "distributed-lock-app", componentsHostFolder: componentsDir, - options: options with {AppPort = actualAppPort}); + options: options with {AppPort = actualAppPort}, + Network); await _daprd.StartAsync(cancellationToken); } diff --git a/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs b/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs index 4083b52d0..1a75ae1e2 100644 --- a/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs @@ -42,7 +42,8 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo _daprd = new DaprdContainer( appId: "crypto-app", componentsHostFolder: componentsDir, - options: options with {AppPort = actualAppPort}); + options: options with {AppPort = actualAppPort}, + Network); await _daprd.StartAsync(cancellationToken); } diff --git a/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs b/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs index 940410bf1..cc58ae136 100644 --- a/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs @@ -28,7 +28,7 @@ namespace Dapr.TestContainers.Harnesses; /// The Dapr runtime options. public sealed class DistributedLockHarness(string componentsDir, Func> startApp, DaprRuntimeOptions options) : BaseHarness { - private readonly RedisContainer _redis = new(); + private readonly RedisContainer _redis = new(Network); /// protected override async Task OnInitializeAsync(CancellationToken cancellationToken) @@ -46,7 +46,8 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo _daprd = new DaprdContainer( appId: "distributed-lock-app", componentsHostFolder: componentsDir, - options: options with {AppPort = actualAppPort}); + options: options with {AppPort = actualAppPort}, + Network); await _daprd.StartAsync(cancellationToken); } diff --git a/src/Dapr.TestContainers/Harnesses/JobsHarness.cs b/src/Dapr.TestContainers/Harnesses/JobsHarness.cs index 2dd5f30e8..a1c34f04f 100644 --- a/src/Dapr.TestContainers/Harnesses/JobsHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/JobsHarness.cs @@ -27,7 +27,7 @@ namespace Dapr.TestContainers.Harnesses; /// The Dapr runtime options. public sealed class JobsHarness(string componentsDir, Func> startApp, DaprRuntimeOptions options) : BaseHarness { - private readonly DaprSchedulerContainer _scheduler = new(options); + private readonly DaprSchedulerContainer _scheduler = new(options, Network); /// protected override async Task OnInitializeAsync(CancellationToken cancellationToken) @@ -42,7 +42,8 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo _daprd = new DaprdContainer( appId: "jobs-app", componentsHostFolder: componentsDir, - options: options with {AppPort = actualAppPort}); + options: options with {AppPort = actualAppPort}, + Network); await _daprd.StartAsync(cancellationToken); } diff --git a/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs b/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs index 5a8db812a..d81d630d9 100644 --- a/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs @@ -28,7 +28,7 @@ namespace Dapr.TestContainers.Harnesses; /// The Dapr runtime options. public sealed class PubSubHarness(string componentsDir, Func> startApp, DaprRuntimeOptions options) : BaseHarness { - private readonly RabbitMqContainer _rabbitmq = new(); + private readonly RabbitMqContainer _rabbitmq = new(Network); /// protected override async Task OnInitializeAsync(CancellationToken cancellationToken) @@ -46,7 +46,8 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo _daprd = new DaprdContainer( appId: "pubsub-app", componentsHostFolder: componentsDir, - options: options with {AppPort = actualAppPort}); + options: options with {AppPort = actualAppPort}, + Network); await _daprd.StartAsync(cancellationToken); } diff --git a/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs b/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs index be2f07c54..299ddc59b 100644 --- a/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs @@ -28,7 +28,7 @@ namespace Dapr.TestContainers.Harnesses; /// The Dapr runtime options. public sealed class StateManagementHarness(string componentsDir, Func> startApp, DaprRuntimeOptions options) : BaseHarness { - private readonly RedisContainer _redis = new(); + private readonly RedisContainer _redis = new(Network); /// protected override async Task OnInitializeAsync(CancellationToken cancellationToken) @@ -46,7 +46,8 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo _daprd = new DaprdContainer( appId: "start-management-app", componentsHostFolder: componentsDir, - options: options with {AppPort = actualAppPort}); + options: options with {AppPort = actualAppPort}, + Network); await _daprd.StartAsync(cancellationToken); } diff --git a/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs b/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs index 621ed5b30..88fe4806b 100644 --- a/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs @@ -29,9 +29,9 @@ namespace Dapr.TestContainers.Harnesses; /// The Dapr runtime options. public sealed class WorkflowHarness(string componentsDir, Func> startApp, DaprRuntimeOptions options) : BaseHarness { - private readonly RedisContainer _redis = new(); - private readonly DaprPlacementContainer _placement = new(options); - private readonly DaprSchedulerContainer _scheduler = new(options); + private readonly RedisContainer _redis = new(Network); + private readonly DaprPlacementContainer _placement = new(options, Network); + private readonly DaprSchedulerContainer _scheduler = new(options, Network); /// protected override async Task OnInitializeAsync(CancellationToken cancellationToken) @@ -56,6 +56,7 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo appId: "workflow-app", componentsHostFolder: componentsDir, options: options with {AppPort = actualAppPort}, + Network, new HostPortPair(_placement.Host, _placement.Port), new HostPortPair(_scheduler.Host, _scheduler.Port)); await _daprd.StartAsync(cancellationToken); From 68f954bc17a0d2f77330e81cd5b65eefa15c9c3a Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sat, 20 Dec 2025 05:38:09 -0600 Subject: [PATCH 07/32] Added a cleanup step to remove old generated YAML files Signed-off-by: Whit Waldo --- .../Harnesses/ActorHarness.cs | 3 +++ .../Harnesses/BaseHarness.cs | 20 +++++++++++++++++++ .../Harnesses/ConversationHarness.cs | 3 +++ .../Harnesses/CryptographyHarness.cs | 3 +++ .../Harnesses/DistributedLockHarness.cs | 3 +++ .../Harnesses/JobsHarness.cs | 3 +++ .../Harnesses/PubSubHarness.cs | 3 +++ .../Harnesses/StateManagementHarness.cs | 3 +++ .../Harnesses/WorkflowHarness.cs | 3 +++ 9 files changed, 44 insertions(+) diff --git a/src/Dapr.TestContainers/Harnesses/ActorHarness.cs b/src/Dapr.TestContainers/Harnesses/ActorHarness.cs index d67aad7b3..4da6e0449 100644 --- a/src/Dapr.TestContainers/Harnesses/ActorHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/ActorHarness.cs @@ -70,5 +70,8 @@ public override async ValueTask DisposeAsync() await _redis.DisposeAsync(); await _placement.DisposeAsync(); await _schedueler.DisposeAsync(); + + // Cleanup the generated YAML files + CleanupComponents(componentsDir); } } diff --git a/src/Dapr.TestContainers/Harnesses/BaseHarness.cs b/src/Dapr.TestContainers/Harnesses/BaseHarness.cs index 59a0a7997..9cc4fe065 100644 --- a/src/Dapr.TestContainers/Harnesses/BaseHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/BaseHarness.cs @@ -12,6 +12,7 @@ // ------------------------------------------------------------------------ using System; +using System.IO; using System.Threading; using System.Threading.Tasks; using Dapr.TestContainers.Common; @@ -75,6 +76,25 @@ public virtual async ValueTask DisposeAsync() await Network.DisposeAsync(); GC.SuppressFinalize(this); } + + /// + /// Deletes the specified directory recursively as part of a clean-up operation. + /// + /// The clean to clean up. + protected virtual void CleanupComponents(string path) + { + if (Directory.Exists(path)) + { + try + { + Directory.Delete(path, true); + } + catch + { + // Ignore cleanup errors + } + } + } private void ConfigureSdkEnvironment() { diff --git a/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs b/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs index 548805890..6e9a65f11 100644 --- a/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs @@ -57,5 +57,8 @@ public override async ValueTask DisposeAsync() if (_daprd is not null) await _daprd.DisposeAsync(); await _ollama.DisposeAsync(); + + // Cleanup the generated YAML files + CleanupComponents(componentsDir); } } diff --git a/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs b/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs index 1a75ae1e2..83f2af9a7 100644 --- a/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs @@ -52,5 +52,8 @@ public override async ValueTask DisposeAsync() { if (_daprd is not null) await _daprd.DisposeAsync(); + + // Cleanup the generated YAML files + CleanupComponents(componentsDir); } } diff --git a/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs b/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs index cc58ae136..85c439136 100644 --- a/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs @@ -57,5 +57,8 @@ public override async ValueTask DisposeAsync() if (_daprd is not null) await _daprd.DisposeAsync(); await _redis.DisposeAsync(); + + // Cleanup the generated YAML files + CleanupComponents(componentsDir); } } diff --git a/src/Dapr.TestContainers/Harnesses/JobsHarness.cs b/src/Dapr.TestContainers/Harnesses/JobsHarness.cs index a1c34f04f..e16223c01 100644 --- a/src/Dapr.TestContainers/Harnesses/JobsHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/JobsHarness.cs @@ -53,5 +53,8 @@ public override async ValueTask DisposeAsync() if (_daprd is not null) await _daprd.DisposeAsync(); await _scheduler.DisposeAsync(); + + // Cleanup the generated YAML files + CleanupComponents(componentsDir); } } diff --git a/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs b/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs index d81d630d9..d55b6af5f 100644 --- a/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs @@ -57,5 +57,8 @@ public override async ValueTask DisposeAsync() if (_daprd is not null) await _daprd.DisposeAsync(); await _rabbitmq.DisposeAsync(); + + // Cleanup the generated YAML files + CleanupComponents(componentsDir); } } diff --git a/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs b/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs index 299ddc59b..0fcb8ce3e 100644 --- a/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs @@ -57,5 +57,8 @@ public override async ValueTask DisposeAsync() if (_daprd is not null) await _daprd.DisposeAsync(); await _redis.DisposeAsync(); + + // Cleanup the generated YAML files + CleanupComponents(componentsDir); } } diff --git a/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs b/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs index 88fe4806b..7391f8677 100644 --- a/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs @@ -70,5 +70,8 @@ public override async ValueTask DisposeAsync() await _placement.DisposeAsync(); await _scheduler.DisposeAsync(); await _redis.DisposeAsync(); + + // Cleanup the generated YAML files + CleanupComponents(componentsDir); } } From fb1f24beaaa932dbb8247ace8e52ad26837688a2 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sun, 21 Dec 2025 10:12:33 -0600 Subject: [PATCH 08/32] Added ability to set app ID via options, but it'll naturally pick a random app ID each invocation to avoid conflicts and facilitate parallel testing Signed-off-by: Whit Waldo --- .../Common/Options/DaprRuntimeOptions.cs | 18 +++++++++++++++++- .../Harnesses/ActorHarness.cs | 2 +- .../Harnesses/ConversationHarness.cs | 2 +- .../Harnesses/CryptographyHarness.cs | 2 +- .../Harnesses/DistributedLockHarness.cs | 2 +- .../Harnesses/JobsHarness.cs | 2 +- .../Harnesses/PubSubHarness.cs | 2 +- .../Harnesses/StateManagementHarness.cs | 2 +- .../Harnesses/WorkflowHarness.cs | 2 +- 9 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/Dapr.TestContainers/Common/Options/DaprRuntimeOptions.cs b/src/Dapr.TestContainers/Common/Options/DaprRuntimeOptions.cs index 681b0b07b..cd890228a 100644 --- a/src/Dapr.TestContainers/Common/Options/DaprRuntimeOptions.cs +++ b/src/Dapr.TestContainers/Common/Options/DaprRuntimeOptions.cs @@ -11,6 +11,8 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System; + namespace Dapr.TestContainers.Common.Options; /// @@ -20,6 +22,11 @@ namespace Dapr.TestContainers.Common.Options; /// The version of the Dapr images to use. public sealed record DaprRuntimeOptions(int AppPort, string Version = "1.16.4") { + /// + /// The ID of the test application. + /// + public string AppId { get; private set; } = $"test-app-{Guid.NewGuid():N}"; + /// /// The level of Dapr logs to show. /// @@ -42,10 +49,19 @@ public sealed record DaprRuntimeOptions(int AppPort, string Version = "1.16.4") /// Sets the Dapr log level. /// /// The log level to specify. - /// public DaprRuntimeOptions WithLogLevel(DaprLogLevel logLevel) { LogLevel = logLevel; return this; } + + /// + /// Sets the Dapr App ID. + /// + /// The App ID to use for the test application. + public DaprRuntimeOptions WithAppId(string appId) + { + AppId = appId; + return this; + } } diff --git a/src/Dapr.TestContainers/Harnesses/ActorHarness.cs b/src/Dapr.TestContainers/Harnesses/ActorHarness.cs index 4da6e0449..06cd338aa 100644 --- a/src/Dapr.TestContainers/Harnesses/ActorHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/ActorHarness.cs @@ -53,7 +53,7 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo // 6) Configure and start daprd, point at placement & scheduler _daprd = new DaprdContainer( - appId: "actor-app", + appId: options.AppId, componentsHostFolder: componentsDir, options: options with {AppPort = actualAppPort}, Network, diff --git a/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs b/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs index 6e9a65f11..fe8c12f0e 100644 --- a/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs @@ -44,7 +44,7 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo // 4) Configure & start daprd _daprd = new DaprdContainer( - appId: "distributed-lock-app", + appId: options.AppId, componentsHostFolder: componentsDir, options: options with {AppPort = actualAppPort}, Network); diff --git a/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs b/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs index 83f2af9a7..338e62694 100644 --- a/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs @@ -40,7 +40,7 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo // 3) Configure and start daprd _daprd = new DaprdContainer( - appId: "crypto-app", + appId: options.AppId, componentsHostFolder: componentsDir, options: options with {AppPort = actualAppPort}, Network); diff --git a/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs b/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs index 85c439136..9eabff34e 100644 --- a/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs @@ -44,7 +44,7 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo // 4) Configure & start daprd _daprd = new DaprdContainer( - appId: "distributed-lock-app", + appId: options.AppId, componentsHostFolder: componentsDir, options: options with {AppPort = actualAppPort}, Network); diff --git a/src/Dapr.TestContainers/Harnesses/JobsHarness.cs b/src/Dapr.TestContainers/Harnesses/JobsHarness.cs index e16223c01..8bf02874c 100644 --- a/src/Dapr.TestContainers/Harnesses/JobsHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/JobsHarness.cs @@ -40,7 +40,7 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo // 3) Configure & start daprd _daprd = new DaprdContainer( - appId: "jobs-app", + appId: options.AppId, componentsHostFolder: componentsDir, options: options with {AppPort = actualAppPort}, Network); diff --git a/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs b/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs index d55b6af5f..f908ff361 100644 --- a/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs @@ -44,7 +44,7 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo // 4) Configure & start daprd _daprd = new DaprdContainer( - appId: "pubsub-app", + appId: options.AppId, componentsHostFolder: componentsDir, options: options with {AppPort = actualAppPort}, Network); diff --git a/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs b/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs index 0fcb8ce3e..61b617207 100644 --- a/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs @@ -44,7 +44,7 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo // 4) Configure & start daprd _daprd = new DaprdContainer( - appId: "start-management-app", + appId: options.AppId, componentsHostFolder: componentsDir, options: options with {AppPort = actualAppPort}, Network); diff --git a/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs b/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs index 7391f8677..7086d4fcb 100644 --- a/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs @@ -53,7 +53,7 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo // 6) Configure and start daprd; point at placement & scheduler _daprd = new DaprdContainer( - appId: "workflow-app", + appId: options.AppId, componentsHostFolder: componentsDir, options: options with {AppPort = actualAppPort}, Network, From 668a432f45fac5887dd2da32054622bc225ca6a3 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sun, 21 Dec 2025 12:07:23 -0600 Subject: [PATCH 09/32] Standardized component named to use constants for each building block represented Signed-off-by: Whit Waldo --- src/Dapr.TestContainers/Constants.cs | 47 +++++++++++++++++++ .../LocalStorageCryptographyContainer.cs | 2 +- .../Containers/OllamaContainer.cs | 2 +- .../Containers/RabbitMqContainer.cs | 2 +- .../Containers/RedisContainer.cs | 4 +- 5 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 src/Dapr.TestContainers/Constants.cs diff --git a/src/Dapr.TestContainers/Constants.cs b/src/Dapr.TestContainers/Constants.cs new file mode 100644 index 000000000..badd283a1 --- /dev/null +++ b/src/Dapr.TestContainers/Constants.cs @@ -0,0 +1,47 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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 Dapr.TestContainers; + +/// +/// Various constants used throughout the project. +/// +public static class Constants +{ + /// + /// Various names used for the Dapr components. + /// + public static class DaprComponentNames + { + /// + /// The name of the State Management component. + /// + public const string StateManagementComponentName = "statestore"; + /// + /// The name of the PubSub component. + /// + public const string PubSubComponentName = "pubsub"; + /// + /// The name of the Conversation component. + /// + public const string ConversationComponentName = "conversation"; + /// + /// The name of the Cryptography component. + /// + public const string CryptographyComponentName = "cryptography"; + /// + /// The name of the Distributed Lock component. + /// + public const string DistributedLockComponentName = "distributed-lock"; + } +} diff --git a/src/Dapr.TestContainers/Containers/LocalStorageCryptographyContainer.cs b/src/Dapr.TestContainers/Containers/LocalStorageCryptographyContainer.cs index b7065b314..71dc85941 100644 --- a/src/Dapr.TestContainers/Containers/LocalStorageCryptographyContainer.cs +++ b/src/Dapr.TestContainers/Containers/LocalStorageCryptographyContainer.cs @@ -50,7 +50,7 @@ private static string GetLocalStorageYaml(string keyPath) => $@"apiVersion: dapr.io/v1alpha kind: Component metadata: - name: crypto + name: {Constants.DaprComponentNames.CryptographyComponentName} spec: type: crypto.dapr.localstorage version: v1 diff --git a/src/Dapr.TestContainers/Containers/OllamaContainer.cs b/src/Dapr.TestContainers/Containers/OllamaContainer.cs index 4ee3b3247..7a362124c 100644 --- a/src/Dapr.TestContainers/Containers/OllamaContainer.cs +++ b/src/Dapr.TestContainers/Containers/OllamaContainer.cs @@ -84,7 +84,7 @@ private static string GetConversationYaml(string model, string cacheTtl, string $@"apiVersion: dapr.io/v1alpha kind: Component metadata: - name: ollama + name: {Constants.DaprComponentNames.ConversationComponentName} spec: type: conversation.ollama metadata: diff --git a/src/Dapr.TestContainers/Containers/RabbitMqContainer.cs b/src/Dapr.TestContainers/Containers/RabbitMqContainer.cs index daa1aef30..178cdc095 100644 --- a/src/Dapr.TestContainers/Containers/RabbitMqContainer.cs +++ b/src/Dapr.TestContainers/Containers/RabbitMqContainer.cs @@ -84,7 +84,7 @@ private static string GetPubSubYaml() => $@"apiVersion: dapr.io/v1alpha kind: Component metadata: - name: rabbitmq + name: {Constants.DaprComponentNames.PubSubComponentName} spec: type: pubsub.rabbitmq metadata: diff --git a/src/Dapr.TestContainers/Containers/RedisContainer.cs b/src/Dapr.TestContainers/Containers/RedisContainer.cs index a5a8b9934..94de53a96 100644 --- a/src/Dapr.TestContainers/Containers/RedisContainer.cs +++ b/src/Dapr.TestContainers/Containers/RedisContainer.cs @@ -101,7 +101,7 @@ private static string GetDistributedLockYaml(string redisHost, string? passwordS $@"apiVersion: dapr.io/v1alpha1 kind: Component metadata: - name: lock + name: {Constants.DaprComponentNames.DistributedLockComponentName} namespace: default spec: type: lock.redis @@ -119,7 +119,7 @@ private static string GetRedisStateStoreYaml(string redisHost, string? passwordS $@"apiVersion: dapr.io/v1alpha1 kind: Component metadata: - name: statestore + name: {Constants.DaprComponentNames.StateManagementComponentName} namespace: default spec: type: state.redis From ec7be5bba29f1ff8fca82be3a5debf75a687a45a Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sun, 21 Dec 2025 14:19:15 -0600 Subject: [PATCH 10/32] Updated to assign a random port to the test app when starting up Signed-off-by: Whit Waldo --- .../Common/DaprHarnessBuilder.cs | 2 +- .../Common/Options/DaprRuntimeOptions.cs | 8 +++-- .../Common/PortUtilities.cs | 34 +++++++++++++++++++ .../Containers/Dapr/DaprdContainer.cs | 7 ++-- .../Containers/RabbitMqContainer.cs | 2 ++ .../Harnesses/ActorHarness.cs | 29 ++++++++-------- .../Harnesses/BaseHarness.cs | 1 - .../Harnesses/ConversationHarness.cs | 16 +++++---- .../Harnesses/CryptographyHarness.cs | 18 ++++++---- .../Harnesses/DistributedLockHarness.cs | 20 ++++++----- .../Harnesses/JobsHarness.cs | 22 +++++++----- .../Harnesses/PubSubHarness.cs | 20 ++++++----- .../Harnesses/StateManagementHarness.cs | 18 ++++++---- .../Harnesses/WorkflowHarness.cs | 29 ++++++++-------- 14 files changed, 145 insertions(+), 81 deletions(-) create mode 100644 src/Dapr.TestContainers/Common/PortUtilities.cs diff --git a/src/Dapr.TestContainers/Common/DaprHarnessBuilder.cs b/src/Dapr.TestContainers/Common/DaprHarnessBuilder.cs index 1da14b1f1..b50956b87 100644 --- a/src/Dapr.TestContainers/Common/DaprHarnessBuilder.cs +++ b/src/Dapr.TestContainers/Common/DaprHarnessBuilder.cs @@ -23,7 +23,7 @@ namespace Dapr.TestContainers.Common; /// /// The Dapr runtime options. /// The test app to run. -public sealed class DaprHarnessBuilder(DaprRuntimeOptions options, Func> startApp) +public sealed class DaprHarnessBuilder(DaprRuntimeOptions options, Func startApp) { /// /// Builds a workflow harness. diff --git a/src/Dapr.TestContainers/Common/Options/DaprRuntimeOptions.cs b/src/Dapr.TestContainers/Common/Options/DaprRuntimeOptions.cs index cd890228a..c64f0d246 100644 --- a/src/Dapr.TestContainers/Common/Options/DaprRuntimeOptions.cs +++ b/src/Dapr.TestContainers/Common/Options/DaprRuntimeOptions.cs @@ -18,10 +18,14 @@ namespace Dapr.TestContainers.Common.Options; /// /// The various options used to spin up the Dapr containers. /// -/// The port of the test app. /// The version of the Dapr images to use. -public sealed record DaprRuntimeOptions(int AppPort, string Version = "1.16.4") +public sealed record DaprRuntimeOptions(string Version = "1.16.4") { + /// + /// The application's port. + /// + public int AppPort { get; set; } = 8080; + /// /// The ID of the test application. /// diff --git a/src/Dapr.TestContainers/Common/PortUtilities.cs b/src/Dapr.TestContainers/Common/PortUtilities.cs new file mode 100644 index 000000000..1d3387f89 --- /dev/null +++ b/src/Dapr.TestContainers/Common/PortUtilities.cs @@ -0,0 +1,34 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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. +// ------------------------------------------------------------------------ + +using System.Net; +using System.Net.Sockets; + +namespace Dapr.TestContainers.Common; + +/// +/// Provides port-related utilities. +/// +internal static class PortUtilities +{ + /// + /// Finds a port that's available to use. + /// + /// The available port number. + public static int GetAvailablePort() + { + using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + socket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + return ((IPEndPoint)socket.LocalEndPoint!).Port; + } +} diff --git a/src/Dapr.TestContainers/Containers/Dapr/DaprdContainer.cs b/src/Dapr.TestContainers/Containers/Dapr/DaprdContainer.cs index 5f19483f7..0ae28393d 100644 --- a/src/Dapr.TestContainers/Containers/Dapr/DaprdContainer.cs +++ b/src/Dapr.TestContainers/Containers/Dapr/DaprdContainer.cs @@ -59,6 +59,7 @@ public DaprdContainer(string appId, string componentsHostFolder, DaprRuntimeOpti "./daprd", "-app-id", appId, "-app-port", options.AppPort.ToString(), + "-app-channel-address", "host.docker.internal", "-dapr-http-port", InternalHttpPort.ToString(), "-dapr-grpc-port", InternalGrpcPort.ToString(), "-log-level", options.LogLevel.ToString().ToLowerInvariant(), @@ -85,8 +86,10 @@ public DaprdContainer(string appId, string componentsHostFolder, DaprRuntimeOpti .WithPortBinding(HttpPort, assignRandomHostPort: true) .WithPortBinding(GrpcPort, assignRandomHostPort: true) .WithBindMount(componentsHostFolder, "/components", AccessMode.ReadOnly) - .WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(InternalHttpPort) - .UntilInternalTcpPortIsAvailable(InternalGrpcPort)) + .WithWaitStrategy(Wait.ForUnixContainer() + .UntilHttpRequestIsSucceeded(s => s.ForPath("/v1.0/healthz").ForPort(InternalHttpPort))) + // .UntilInternalTcpPortIsAvailable(InternalHttpPort) + // .UntilInternalTcpPortIsAvailable(InternalGrpcPort)) .Build(); } diff --git a/src/Dapr.TestContainers/Containers/RabbitMqContainer.cs b/src/Dapr.TestContainers/Containers/RabbitMqContainer.cs index 178cdc095..fc68ae38d 100644 --- a/src/Dapr.TestContainers/Containers/RabbitMqContainer.cs +++ b/src/Dapr.TestContainers/Containers/RabbitMqContainer.cs @@ -16,6 +16,7 @@ using System.Threading; using System.Threading.Tasks; using Dapr.TestContainers.Common; +using DotNet.Testcontainers; using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Containers; using DotNet.Testcontainers.Networks; @@ -33,6 +34,7 @@ public sealed class RabbitMqContainer(INetwork network) : IAsyncStartable .WithImage("rabbitmq:alpine") .WithName($"rabbitmq-{Guid.NewGuid():N}") .WithNetwork(network) + .WithLogger(ConsoleLogger.Instance) .WithPortBinding(InternalPort, assignRandomHostPort: true) .WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(InternalPort)) .Build(); diff --git a/src/Dapr.TestContainers/Harnesses/ActorHarness.cs b/src/Dapr.TestContainers/Harnesses/ActorHarness.cs index 06cd338aa..b7e9ca53f 100644 --- a/src/Dapr.TestContainers/Harnesses/ActorHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/ActorHarness.cs @@ -27,7 +27,7 @@ namespace Dapr.TestContainers.Harnesses; /// The directory to Dapr components. /// The test app to validate in the harness. /// The dapr runtime options. -public sealed class ActorHarness(string componentsDir, Func> startApp, DaprRuntimeOptions options) : BaseHarness +public sealed class ActorHarness(string componentsDir, Func startApp, DaprRuntimeOptions options) : BaseHarness { private readonly RedisContainer _redis = new(Network); private readonly DaprPlacementContainer _placement = new(options, Network); @@ -36,30 +36,29 @@ public sealed class ActorHarness(string componentsDir, Func> startApp, /// protected override async Task OnInitializeAsync(CancellationToken cancellationToken) { - // 1) Start Redis (actor state store) + // Start infrastructure await _redis.StartAsync(cancellationToken); - - // 2) Emit component YAMLs pointing to Redis + await _placement.StartAsync(cancellationToken); + await _schedueler.StartAsync(cancellationToken); + + // Emit component YAMLs pointing to Redis RedisContainer.Yaml.WriteStateStoreYamlToFolder(componentsDir, redisHost: $"{_redis.Host}:{_redis.Port}"); - // 3) Start placement - await _placement.StartAsync(cancellationToken); - - // 4) Start scheduler - await _schedueler.StartAsync(cancellationToken); - - // 5) Start the test - var actualAppPort = await startApp(); - - // 6) Configure and start daprd, point at placement & scheduler + // Find a random free port for the test app + var assignedAppPort = PortUtilities.GetAvailablePort(); + + // Configure and start daprd, point at placement & scheduler _daprd = new DaprdContainer( appId: options.AppId, componentsHostFolder: componentsDir, - options: options with {AppPort = actualAppPort}, + options: options with {AppPort = assignedAppPort}, Network, new HostPortPair(_placement.Host, _placement.Port), new HostPortPair(_schedueler.Host, _schedueler.Port)); await _daprd.StartAsync(cancellationToken); + + // Start the test + await startApp(assignedAppPort); } /// diff --git a/src/Dapr.TestContainers/Harnesses/BaseHarness.cs b/src/Dapr.TestContainers/Harnesses/BaseHarness.cs index 9cc4fe065..2cf94a869 100644 --- a/src/Dapr.TestContainers/Harnesses/BaseHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/BaseHarness.cs @@ -103,5 +103,4 @@ private void ConfigureSdkEnvironment() if (DaprGrpcPort > 0) Environment.SetEnvironmentVariable("DAPR_GRPC_PORT", DaprGrpcPort.ToString()); } - } diff --git a/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs b/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs index fe8c12f0e..782668767 100644 --- a/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs @@ -14,6 +14,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using Dapr.TestContainers.Common; using Dapr.TestContainers.Common.Options; using Dapr.TestContainers.Containers; using Dapr.TestContainers.Containers.Dapr; @@ -26,29 +27,32 @@ namespace Dapr.TestContainers.Harnesses; /// The directory to Dapr components. /// The test app to validate in the harness. /// The Dapr runtime options. -public sealed class ConversationHarness(string componentsDir, Func> startApp, DaprRuntimeOptions options) : BaseHarness +public sealed class ConversationHarness(string componentsDir, Func startApp, DaprRuntimeOptions options) : BaseHarness { private readonly OllamaContainer _ollama = new(Network); /// protected override async Task OnInitializeAsync(CancellationToken cancellationToken) { - // 1) Start Ollama (conversation) + // Start infrastructure await _ollama.StartAsync(cancellationToken); - // 2) Emit component YAMLs for Ollama (use the default tiny model) + // Emit component YAMLs for Ollama (use the default tiny model) OllamaContainer.Yaml.WriteConversationYamlToFolder(componentsDir); - // 3) Start the app - var actualAppPort = await startApp(); + // Find a random free port for the test app + var assignedAppPort = PortUtilities.GetAvailablePort(); // 4) Configure & start daprd _daprd = new DaprdContainer( appId: options.AppId, componentsHostFolder: componentsDir, - options: options with {AppPort = actualAppPort}, + options: options with {AppPort = assignedAppPort}, Network); await _daprd.StartAsync(cancellationToken); + + // Start the app + await startApp(assignedAppPort); } /// diff --git a/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs b/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs index 338e62694..6715a60a2 100644 --- a/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs @@ -14,6 +14,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using Dapr.TestContainers.Common; using Dapr.TestContainers.Common.Options; using Dapr.TestContainers.Containers; using Dapr.TestContainers.Containers.Dapr; @@ -27,24 +28,27 @@ namespace Dapr.TestContainers.Harnesses; /// The test app to validate in the harness. /// The Dapr runtime options. /// The path locally to the cryptography keys to use. -public sealed class CryptographyHarness(string componentsDir, Func>startApp, string keyPath, DaprRuntimeOptions options) : BaseHarness +public sealed class CryptographyHarness(string componentsDir, FuncstartApp, string keyPath, DaprRuntimeOptions options) : BaseHarness { /// protected override async Task OnInitializeAsync(CancellationToken cancellationToken) { - // 1) Emit the component YAML describing the local crypto key store + // Emit the component YAML describing the local crypto key store LocalStorageCryptographyContainer.Yaml.WriteCryptoYamlToFolder(componentsDir, keyPath); - // 2) Start the app - var actualAppPort = await startApp(); - - // 3) Configure and start daprd + // Find a random free port for the test app + var assignedAppPort = PortUtilities.GetAvailablePort(); + + // Configure and start daprd _daprd = new DaprdContainer( appId: options.AppId, componentsHostFolder: componentsDir, - options: options with {AppPort = actualAppPort}, + options: options with {AppPort = assignedAppPort}, Network); await _daprd.StartAsync(cancellationToken); + + // Start the app + await startApp(assignedAppPort); } /// diff --git a/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs b/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs index 9eabff34e..3631e7753 100644 --- a/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs @@ -14,6 +14,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using Dapr.TestContainers.Common; using Dapr.TestContainers.Common.Options; using Dapr.TestContainers.Containers; using Dapr.TestContainers.Containers.Dapr; @@ -26,29 +27,32 @@ namespace Dapr.TestContainers.Harnesses; /// The directory to Dapr components. /// The test app to validate in the harness. /// The Dapr runtime options. -public sealed class DistributedLockHarness(string componentsDir, Func> startApp, DaprRuntimeOptions options) : BaseHarness +public sealed class DistributedLockHarness(string componentsDir, Func startApp, DaprRuntimeOptions options) : BaseHarness { private readonly RedisContainer _redis = new(Network); /// protected override async Task OnInitializeAsync(CancellationToken cancellationToken) { - // 1) Start Redis (state store) + // Start infrastructure await _redis.StartAsync(cancellationToken); - // 2) Emit component YAMLs pointing to Redis + // Emit component YAMLs pointing to Redis RedisContainer.Yaml.WriteDistributedLockYamlToFolder(componentsDir, redisHost: $"{_redis.Host}:{_redis.Port}"); - // 3) Start the app - var actualAppPort = await startApp(); - - // 4) Configure & start daprd + // Find a random free port for the test app + var assignedAppPort = PortUtilities.GetAvailablePort(); + + // Configure & start daprd _daprd = new DaprdContainer( appId: options.AppId, componentsHostFolder: componentsDir, - options: options with {AppPort = actualAppPort}, + options: options with {AppPort = assignedAppPort}, Network); await _daprd.StartAsync(cancellationToken); + + // Start the app + await startApp(assignedAppPort); } /// diff --git a/src/Dapr.TestContainers/Harnesses/JobsHarness.cs b/src/Dapr.TestContainers/Harnesses/JobsHarness.cs index 8bf02874c..08eb7d34b 100644 --- a/src/Dapr.TestContainers/Harnesses/JobsHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/JobsHarness.cs @@ -14,6 +14,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using Dapr.TestContainers.Common; using Dapr.TestContainers.Common.Options; using Dapr.TestContainers.Containers.Dapr; @@ -25,26 +26,29 @@ namespace Dapr.TestContainers.Harnesses; /// The directory to Dapr components. /// The test app to validate in the harness. /// The Dapr runtime options. -public sealed class JobsHarness(string componentsDir, Func> startApp, DaprRuntimeOptions options) : BaseHarness +public sealed class JobsHarness(string componentsDir, Func startApp, DaprRuntimeOptions options) : BaseHarness { private readonly DaprSchedulerContainer _scheduler = new(options, Network); /// protected override async Task OnInitializeAsync(CancellationToken cancellationToken) { - // 1) Start the Dapr Scheduler - await _scheduler.StartAsync(cancellationToken); - - // 2) Start the app - var actualAppPort = await startApp(); - - // 3) Configure & start daprd + // Start the infrastructure + await _scheduler.StartAsync(cancellationToken); + + // Find a random port for the test app + var assignedAppPort = PortUtilities.GetAvailablePort(); + + // Configure & start daprd _daprd = new DaprdContainer( appId: options.AppId, componentsHostFolder: componentsDir, - options: options with {AppPort = actualAppPort}, + options: options with {AppPort = assignedAppPort}, Network); await _daprd.StartAsync(cancellationToken); + + // Start the app + await startApp(assignedAppPort); } /// diff --git a/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs b/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs index f908ff361..e5f51bceb 100644 --- a/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs @@ -14,6 +14,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using Dapr.TestContainers.Common; using Dapr.TestContainers.Common.Options; using Dapr.TestContainers.Containers; using Dapr.TestContainers.Containers.Dapr; @@ -26,29 +27,32 @@ namespace Dapr.TestContainers.Harnesses; /// The directory to Dapr components. /// The test app to validate in the harness. /// The Dapr runtime options. -public sealed class PubSubHarness(string componentsDir, Func> startApp, DaprRuntimeOptions options) : BaseHarness +public sealed class PubSubHarness(string componentsDir, Func startApp, DaprRuntimeOptions options) : BaseHarness { private readonly RabbitMqContainer _rabbitmq = new(Network); /// protected override async Task OnInitializeAsync(CancellationToken cancellationToken) { - // 1) Start RabbitMq (pubsub) + // Start infrastructure await _rabbitmq.StartAsync(cancellationToken); - // 2) Emit component YAMLs pointing to RabbitMQ + // Emit component YAMLs pointing to RabbitMQ RabbitMqContainer.Yaml.WritePubSubYamlToFolder(componentsDir); + + // Find a random free port for the test app + var assignedAppPort = PortUtilities.GetAvailablePort(); - // 3) Start the test app - var actualAppPort = await startApp(); - - // 4) Configure & start daprd + // Configure & start daprd _daprd = new DaprdContainer( appId: options.AppId, componentsHostFolder: componentsDir, - options: options with {AppPort = actualAppPort}, + options: options with {AppPort = assignedAppPort}, Network); await _daprd.StartAsync(cancellationToken); + + // Start the test app + await startApp(assignedAppPort); } /// diff --git a/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs b/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs index 61b617207..b5d634cf3 100644 --- a/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs @@ -14,6 +14,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using Dapr.TestContainers.Common; using Dapr.TestContainers.Common.Options; using Dapr.TestContainers.Containers; using Dapr.TestContainers.Containers.Dapr; @@ -26,29 +27,32 @@ namespace Dapr.TestContainers.Harnesses; /// The directory to Dapr components. /// The test app to validate in the harness. /// The Dapr runtime options. -public sealed class StateManagementHarness(string componentsDir, Func> startApp, DaprRuntimeOptions options) : BaseHarness +public sealed class StateManagementHarness(string componentsDir, Func startApp, DaprRuntimeOptions options) : BaseHarness { private readonly RedisContainer _redis = new(Network); /// protected override async Task OnInitializeAsync(CancellationToken cancellationToken) { - // 1) Start Redis (state store) + // Start infrastructure await _redis.StartAsync(cancellationToken); - // 2) Emit component YAMLs pointing to Redis + // Emit component YAMLs pointing to Redis RedisContainer.Yaml.WriteStateStoreYamlToFolder(componentsDir, redisHost: $"{_redis.Host}:{_redis.Port}"); - // 3) Start the app - var actualAppPort = await startApp(); + // Find a random free port for the test app + var assignedAppPort = PortUtilities.GetAvailablePort(); - // 4) Configure & start daprd + // Configure & start daprd _daprd = new DaprdContainer( appId: options.AppId, componentsHostFolder: componentsDir, - options: options with {AppPort = actualAppPort}, + options: options with {AppPort = assignedAppPort}, Network); await _daprd.StartAsync(cancellationToken); + + // Start the app + await startApp(assignedAppPort); } /// diff --git a/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs b/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs index 7086d4fcb..ec098e158 100644 --- a/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs @@ -27,7 +27,7 @@ namespace Dapr.TestContainers.Harnesses; /// The directory to Dapr components. /// The test app to validate in the harness. /// The Dapr runtime options. -public sealed class WorkflowHarness(string componentsDir, Func> startApp, DaprRuntimeOptions options) : BaseHarness +public sealed class WorkflowHarness(string componentsDir, Func startApp, DaprRuntimeOptions options) : BaseHarness { private readonly RedisContainer _redis = new(Network); private readonly DaprPlacementContainer _placement = new(options, Network); @@ -36,31 +36,30 @@ public sealed class WorkflowHarness(string componentsDir, Func> startA /// protected override async Task OnInitializeAsync(CancellationToken cancellationToken) { - // 1) Start Redis (actor state store) + // Start infrastructure await _redis.StartAsync(cancellationToken); + await _placement.StartAsync(cancellationToken); + await _scheduler.StartAsync(cancellationToken); - // 2) Emit component YAMLs pointing to Redis + // Emit component YAMLs pointing to Redis RedisContainer.Yaml.WriteStateStoreYamlToFolder(componentsDir, redisHost: $"{_redis.Host}:{_redis.Port}"); - // 3) Start placement - await _placement.StartAsync(cancellationToken); - - // 4) Start scheduler - await _scheduler.StartAsync(cancellationToken); - - // 5) Start the test - var actualAppPort = await startApp(); - - // 6) Configure and start daprd; point at placement & scheduler + // Find a random free port for the test app + var assignedAppPort = PortUtilities.GetAvailablePort(); + + // Configure and start daprd; point at placement & scheduler _daprd = new DaprdContainer( appId: options.AppId, componentsHostFolder: componentsDir, - options: options with {AppPort = actualAppPort}, + options: options with {AppPort = assignedAppPort}, Network, new HostPortPair(_placement.Host, _placement.Port), new HostPortPair(_scheduler.Host, _scheduler.Port)); await _daprd.StartAsync(cancellationToken); - } + + // Start the app + await startApp(assignedAppPort); + } /// public override async ValueTask DisposeAsync() From f0acbdd92d947afb46f339179bb571ebb8f07260 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sun, 21 Dec 2025 15:17:05 -0600 Subject: [PATCH 11/32] Changed wait condition so Dapr Placement service properly initializes Signed-off-by: Whit Waldo --- .../Containers/Dapr/DaprPlacementContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Dapr.TestContainers/Containers/Dapr/DaprPlacementContainer.cs b/src/Dapr.TestContainers/Containers/Dapr/DaprPlacementContainer.cs index 67f51f745..2cac65678 100644 --- a/src/Dapr.TestContainers/Containers/Dapr/DaprPlacementContainer.cs +++ b/src/Dapr.TestContainers/Containers/Dapr/DaprPlacementContainer.cs @@ -53,7 +53,7 @@ public DaprPlacementContainer(DaprRuntimeOptions options, INetwork network) .WithNetwork(network) .WithCommand("./placement", "-port", InternalPort.ToString()) .WithPortBinding(InternalPort, assignRandomHostPort: true) - .WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(InternalPort)) + .WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged("placement server leadership acquired")) .Build(); } From 7c54199a57fb3a1e8ff279a887a054e72852a9a3 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sun, 21 Dec 2025 15:17:30 -0600 Subject: [PATCH 12/32] Changed wait strategy and volume mount approach so Dapr Scheduler service properly initializes Signed-off-by: Whit Waldo --- .../Containers/Dapr/DaprSchedulerContainer.cs | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/src/Dapr.TestContainers/Containers/Dapr/DaprSchedulerContainer.cs b/src/Dapr.TestContainers/Containers/Dapr/DaprSchedulerContainer.cs index 097469bf6..a38e1b477 100644 --- a/src/Dapr.TestContainers/Containers/Dapr/DaprSchedulerContainer.cs +++ b/src/Dapr.TestContainers/Containers/Dapr/DaprSchedulerContainer.cs @@ -12,11 +12,14 @@ // ------------------------------------------------------------------------ using System; +using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Dapr.TestContainers.Common; using Dapr.TestContainers.Common.Options; using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Configurations; using DotNet.Testcontainers.Containers; using DotNet.Testcontainers.Networks; @@ -29,6 +32,8 @@ public sealed class DaprSchedulerContainer : IAsyncStartable { private const int InternalPort = 51005; private readonly IContainer _container; + // Contains the data directory used by this instance of the Dapr scheduler service + private string _hostDataDir = Path.Combine(Path.GetTempPath(), $"dapr-scheduler-{Guid.NewGuid():N}"); /// /// The container's hostname. @@ -45,15 +50,27 @@ public sealed class DaprSchedulerContainer : IAsyncStartable public DaprSchedulerContainer(DaprRuntimeOptions options, INetwork network) { // Scheduler service runs via port 51005 - _container = new ContainerBuilder() + const string containerDataDir = "/tmp/dapr-scheduler"; + string[] cmd = + [ + "./scheduler", + "--port", InternalPort.ToString(), + "--etcd-data-dir", containerDataDir + ]; + + //Create a unique temp directory on the host for the test run based on this instance's data directory + Directory.CreateDirectory(_hostDataDir); + + _container = new ContainerBuilder() .WithImage(options.SchedulerImageTag) .WithName($"scheduler-{Guid.NewGuid():N}") .WithNetwork(network) - .WithCommand("./scheduler", InternalPort.ToString(), "-etcd-data-dir", ".") + .WithCommand(cmd.ToArray()) .WithPortBinding(InternalPort, assignRandomHostPort: true) - .WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(InternalPort)) + // Mount an anonymous volume to /data to ensure the scheduler has write permissions + .WithBindMount(_hostDataDir, containerDataDir, AccessMode.ReadWrite) + .WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged("api is ready")) .Build(); - } /// @@ -72,5 +89,11 @@ await _container.ExecAsync(["sh", "-c", "mkdir -p ./dapr-scheduler-existing-clus /// public Task StopAsync(CancellationToken cancellationToken = default) => _container.StopAsync(cancellationToken); /// - public ValueTask DisposeAsync() => _container.DisposeAsync(); + public ValueTask DisposeAsync() + { + // Remove the data directory if it exists + if (Directory.Exists(_hostDataDir)) + Directory.Delete(_hostDataDir, true); + return _container.DisposeAsync(); + } } From 628dd49f16d5c60bfa293034b056722605ec3e2f Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 22 Dec 2025 00:44:40 -0600 Subject: [PATCH 13/32] Updated harnesses to make the start app optional Signed-off-by: Whit Waldo --- .../Common/DaprHarnessBuilder.cs | 2 +- .../Harnesses/ActorHarness.cs | 7 +++--- .../Harnesses/ConversationHarness.cs | 5 +++-- .../Harnesses/CryptographyHarness.cs | 5 +++-- .../Harnesses/DistributedLockHarness.cs | 5 +++-- .../Harnesses/JobsHarness.cs | 5 +++-- .../Harnesses/PubSubHarness.cs | 22 ++++++++++++++----- .../Harnesses/StateManagementHarness.cs | 7 +++--- .../Harnesses/WorkflowHarness.cs | 9 ++++---- 9 files changed, 43 insertions(+), 24 deletions(-) diff --git a/src/Dapr.TestContainers/Common/DaprHarnessBuilder.cs b/src/Dapr.TestContainers/Common/DaprHarnessBuilder.cs index b50956b87..c1b9a8be5 100644 --- a/src/Dapr.TestContainers/Common/DaprHarnessBuilder.cs +++ b/src/Dapr.TestContainers/Common/DaprHarnessBuilder.cs @@ -23,7 +23,7 @@ namespace Dapr.TestContainers.Common; /// /// The Dapr runtime options. /// The test app to run. -public sealed class DaprHarnessBuilder(DaprRuntimeOptions options, Func startApp) +public sealed class DaprHarnessBuilder(DaprRuntimeOptions options, Func? startApp = null) { /// /// Builds a workflow harness. diff --git a/src/Dapr.TestContainers/Harnesses/ActorHarness.cs b/src/Dapr.TestContainers/Harnesses/ActorHarness.cs index b7e9ca53f..5ee507f33 100644 --- a/src/Dapr.TestContainers/Harnesses/ActorHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/ActorHarness.cs @@ -27,7 +27,7 @@ namespace Dapr.TestContainers.Harnesses; /// The directory to Dapr components. /// The test app to validate in the harness. /// The dapr runtime options. -public sealed class ActorHarness(string componentsDir, Func startApp, DaprRuntimeOptions options) : BaseHarness +public sealed class ActorHarness(string componentsDir, Func? startApp, DaprRuntimeOptions options) : BaseHarness { private readonly RedisContainer _redis = new(Network); private readonly DaprPlacementContainer _placement = new(options, Network); @@ -58,8 +58,9 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo await _daprd.StartAsync(cancellationToken); // Start the test - await startApp(assignedAppPort); - } + if (startApp is not null) + await startApp(assignedAppPort); + } /// public override async ValueTask DisposeAsync() diff --git a/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs b/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs index 782668767..12249871c 100644 --- a/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs @@ -27,7 +27,7 @@ namespace Dapr.TestContainers.Harnesses; /// The directory to Dapr components. /// The test app to validate in the harness. /// The Dapr runtime options. -public sealed class ConversationHarness(string componentsDir, Func startApp, DaprRuntimeOptions options) : BaseHarness +public sealed class ConversationHarness(string componentsDir, Func? startApp, DaprRuntimeOptions options) : BaseHarness { private readonly OllamaContainer _ollama = new(Network); @@ -52,7 +52,8 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo await _daprd.StartAsync(cancellationToken); // Start the app - await startApp(assignedAppPort); + if (startApp is not null) + await startApp(assignedAppPort); } /// diff --git a/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs b/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs index 6715a60a2..d0f78fedc 100644 --- a/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs @@ -28,7 +28,7 @@ namespace Dapr.TestContainers.Harnesses; /// The test app to validate in the harness. /// The Dapr runtime options. /// The path locally to the cryptography keys to use. -public sealed class CryptographyHarness(string componentsDir, FuncstartApp, string keyPath, DaprRuntimeOptions options) : BaseHarness +public sealed class CryptographyHarness(string componentsDir, Func? startApp, string keyPath, DaprRuntimeOptions options) : BaseHarness { /// protected override async Task OnInitializeAsync(CancellationToken cancellationToken) @@ -48,7 +48,8 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo await _daprd.StartAsync(cancellationToken); // Start the app - await startApp(assignedAppPort); + if (startApp is not null) + await startApp(assignedAppPort); } /// diff --git a/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs b/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs index 3631e7753..1f19369b5 100644 --- a/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs @@ -27,7 +27,7 @@ namespace Dapr.TestContainers.Harnesses; /// The directory to Dapr components. /// The test app to validate in the harness. /// The Dapr runtime options. -public sealed class DistributedLockHarness(string componentsDir, Func startApp, DaprRuntimeOptions options) : BaseHarness +public sealed class DistributedLockHarness(string componentsDir, Func? startApp, DaprRuntimeOptions options) : BaseHarness { private readonly RedisContainer _redis = new(Network); @@ -52,7 +52,8 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo await _daprd.StartAsync(cancellationToken); // Start the app - await startApp(assignedAppPort); + if (startApp is not null) + await startApp(assignedAppPort); } /// diff --git a/src/Dapr.TestContainers/Harnesses/JobsHarness.cs b/src/Dapr.TestContainers/Harnesses/JobsHarness.cs index 08eb7d34b..bc4a58c98 100644 --- a/src/Dapr.TestContainers/Harnesses/JobsHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/JobsHarness.cs @@ -26,7 +26,7 @@ namespace Dapr.TestContainers.Harnesses; /// The directory to Dapr components. /// The test app to validate in the harness. /// The Dapr runtime options. -public sealed class JobsHarness(string componentsDir, Func startApp, DaprRuntimeOptions options) : BaseHarness +public sealed class JobsHarness(string componentsDir, Func? startApp, DaprRuntimeOptions options) : BaseHarness { private readonly DaprSchedulerContainer _scheduler = new(options, Network); @@ -48,7 +48,8 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo await _daprd.StartAsync(cancellationToken); // Start the app - await startApp(assignedAppPort); + if (startApp is not null) + await startApp(assignedAppPort); } /// diff --git a/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs b/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs index e5f51bceb..e91ff5126 100644 --- a/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs @@ -12,6 +12,7 @@ // ------------------------------------------------------------------------ using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Dapr.TestContainers.Common; @@ -27,7 +28,7 @@ namespace Dapr.TestContainers.Harnesses; /// The directory to Dapr components. /// The test app to validate in the harness. /// The Dapr runtime options. -public sealed class PubSubHarness(string componentsDir, Func startApp, DaprRuntimeOptions options) : BaseHarness +public sealed class PubSubHarness(string componentsDir, Func? startApp, DaprRuntimeOptions options) : BaseHarness { private readonly RabbitMqContainer _rabbitmq = new(Network); @@ -49,11 +50,22 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo componentsHostFolder: componentsDir, options: options with {AppPort = assignedAppPort}, Network); - await _daprd.StartAsync(cancellationToken); + + // Create the tasks to wait on, starting with the daprd container + var tasks = new List + { + _daprd.StartAsync(cancellationToken) + }; // Start the test app - await startApp(assignedAppPort); - } + if (startApp is not null) + { + var appTask = startApp(assignedAppPort); + tasks.Add(appTask); + } + + await Task.WhenAll(tasks); + } /// public override async ValueTask DisposeAsync() @@ -62,7 +74,7 @@ public override async ValueTask DisposeAsync() await _daprd.DisposeAsync(); await _rabbitmq.DisposeAsync(); - // Cleanup the generated YAML files + // Clean up the generated YAML files CleanupComponents(componentsDir); } } diff --git a/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs b/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs index b5d634cf3..cde2c5ad1 100644 --- a/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs @@ -27,7 +27,7 @@ namespace Dapr.TestContainers.Harnesses; /// The directory to Dapr components. /// The test app to validate in the harness. /// The Dapr runtime options. -public sealed class StateManagementHarness(string componentsDir, Func startApp, DaprRuntimeOptions options) : BaseHarness +public sealed class StateManagementHarness(string componentsDir, Func? startApp, DaprRuntimeOptions options) : BaseHarness { private readonly RedisContainer _redis = new(Network); @@ -52,8 +52,9 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo await _daprd.StartAsync(cancellationToken); // Start the app - await startApp(assignedAppPort); - } + if (startApp is not null) + await startApp(assignedAppPort); + } /// public override async ValueTask DisposeAsync() diff --git a/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs b/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs index ec098e158..d82dbf322 100644 --- a/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs @@ -27,7 +27,7 @@ namespace Dapr.TestContainers.Harnesses; /// The directory to Dapr components. /// The test app to validate in the harness. /// The Dapr runtime options. -public sealed class WorkflowHarness(string componentsDir, Func startApp, DaprRuntimeOptions options) : BaseHarness +public sealed class WorkflowHarness(string componentsDir, Func? startApp, DaprRuntimeOptions options) : BaseHarness { private readonly RedisContainer _redis = new(Network); private readonly DaprPlacementContainer _placement = new(options, Network); @@ -53,12 +53,13 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo componentsHostFolder: componentsDir, options: options with {AppPort = assignedAppPort}, Network, - new HostPortPair(_placement.Host, _placement.Port), - new HostPortPair(_scheduler.Host, _scheduler.Port)); + new HostPortPair("host.docker.internal", _placement.Port), + new HostPortPair("host.docker.internal", _scheduler.Port)); await _daprd.StartAsync(cancellationToken); // Start the app - await startApp(assignedAppPort); + if (startApp is not null) + await startApp(assignedAppPort); } /// From dfc4579246207a916b444bc672c92680e0b85679 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 22 Dec 2025 00:45:22 -0600 Subject: [PATCH 14/32] Updated the daprd container to ensure it starts up as expected Signed-off-by: Whit Waldo --- .../Containers/Dapr/DaprdContainer.cs | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/Dapr.TestContainers/Containers/Dapr/DaprdContainer.cs b/src/Dapr.TestContainers/Containers/Dapr/DaprdContainer.cs index 0ae28393d..7158c12e5 100644 --- a/src/Dapr.TestContainers/Containers/Dapr/DaprdContainer.cs +++ b/src/Dapr.TestContainers/Containers/Dapr/DaprdContainer.cs @@ -17,6 +17,7 @@ using System.Threading.Tasks; using Dapr.TestContainers.Common; using Dapr.TestContainers.Common.Options; +using DotNet.Testcontainers; using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Configurations; using DotNet.Testcontainers.Containers; @@ -52,18 +53,19 @@ public sealed class DaprdContainer : IAsyncStartable /// The hostname and port of the Placement service. /// The hostname and port of the Scheduler service. public DaprdContainer(string appId, string componentsHostFolder, DaprRuntimeOptions options, INetwork netowrk, HostPortPair? placementHostAndPort = null, HostPortPair? schedulerHostAndPort = null) - { + { + const string componentsPath = "/components"; var cmd = new List { - "./daprd", + "/daprd", "-app-id", appId, "-app-port", options.AppPort.ToString(), "-app-channel-address", "host.docker.internal", "-dapr-http-port", InternalHttpPort.ToString(), "-dapr-grpc-port", InternalGrpcPort.ToString(), "-log-level", options.LogLevel.ToString().ToLowerInvariant(), - "-resources-path", "/components" + "-resources-path", componentsPath }; if (placementHostAndPort is not null) @@ -71,6 +73,12 @@ public DaprdContainer(string appId, string componentsHostFolder, DaprRuntimeOpti cmd.Add("-placement-host-address"); cmd.Add(placementHostAndPort.ToString()); } + else + { + // Explicitly disable placement if not provided to speed up startup + cmd.Add("-placement-host-address"); + cmd.Add(""); + } if (schedulerHostAndPort is not null) { @@ -81,15 +89,15 @@ public DaprdContainer(string appId, string componentsHostFolder, DaprRuntimeOpti _container = new ContainerBuilder() .WithImage(options.RuntimeImageTag) .WithName($"dapr-{Guid.NewGuid():N}") + .WithLogger(ConsoleLogger.Instance) .WithCommand(cmd.ToArray()) .WithNetwork(netowrk) - .WithPortBinding(HttpPort, assignRandomHostPort: true) - .WithPortBinding(GrpcPort, assignRandomHostPort: true) - .WithBindMount(componentsHostFolder, "/components", AccessMode.ReadOnly) + .WithExtraHost("host.docker.internal", "host-gateway") + .WithPortBinding(InternalHttpPort, assignRandomHostPort: true) + .WithPortBinding(InternalGrpcPort, assignRandomHostPort: true) + .WithBindMount(componentsHostFolder, componentsPath, AccessMode.ReadOnly) .WithWaitStrategy(Wait.ForUnixContainer() - .UntilHttpRequestIsSucceeded(s => s.ForPath("/v1.0/healthz").ForPort(InternalHttpPort))) - // .UntilInternalTcpPortIsAvailable(InternalHttpPort) - // .UntilInternalTcpPortIsAvailable(InternalGrpcPort)) + .UntilMessageIsLogged("Dapr sidecar is up and running.")) .Build(); } @@ -97,9 +105,12 @@ public DaprdContainer(string appId, string componentsHostFolder, DaprRuntimeOpti public async Task StartAsync(CancellationToken cancellationToken = default) { await _container.StartAsync(cancellationToken); - HttpPort = _container.GetMappedPublicPort(HttpPort); - GrpcPort = _container.GetMappedPublicPort(GrpcPort); - } + HttpPort = _container.GetMappedPublicPort(InternalHttpPort); + GrpcPort = _container.GetMappedPublicPort(InternalGrpcPort); + + Environment.SetEnvironmentVariable("DAPR_HTTP_PORT", HttpPort.ToString()); + Environment.SetEnvironmentVariable("DAPR_GRPC_PORT", GrpcPort.ToString()); + } /// public Task StopAsync(CancellationToken cancellationToken = default) => _container.StopAsync(cancellationToken); From bda9f91a51494031ecdc0ced11b984cc8fdaa074 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 22 Dec 2025 01:06:55 -0600 Subject: [PATCH 15/32] Updated all the harnesses to expose the App Port so it's accessible when not using a test app Signed-off-by: Whit Waldo --- src/Dapr.TestContainers/Harnesses/ActorHarness.cs | 5 +++-- src/Dapr.TestContainers/Harnesses/BaseHarness.cs | 6 ++++++ src/Dapr.TestContainers/Harnesses/ConversationHarness.cs | 3 ++- src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs | 1 + src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs | 1 + src/Dapr.TestContainers/Harnesses/JobsHarness.cs | 1 + src/Dapr.TestContainers/Harnesses/PubSubHarness.cs | 1 + src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs | 1 + src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs | 1 + 9 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/Dapr.TestContainers/Harnesses/ActorHarness.cs b/src/Dapr.TestContainers/Harnesses/ActorHarness.cs index 5ee507f33..a1e735ac0 100644 --- a/src/Dapr.TestContainers/Harnesses/ActorHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/ActorHarness.cs @@ -32,7 +32,7 @@ public sealed class ActorHarness(string componentsDir, Func? startApp private readonly RedisContainer _redis = new(Network); private readonly DaprPlacementContainer _placement = new(options, Network); private readonly DaprSchedulerContainer _schedueler = new(options, Network); - + /// protected override async Task OnInitializeAsync(CancellationToken cancellationToken) { @@ -46,6 +46,7 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo // Find a random free port for the test app var assignedAppPort = PortUtilities.GetAvailablePort(); + AppPort = assignedAppPort; // Configure and start daprd, point at placement & scheduler _daprd = new DaprdContainer( @@ -58,7 +59,7 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo await _daprd.StartAsync(cancellationToken); // Start the test - if (startApp is not null) + if (startApp is not null) await startApp(assignedAppPort); } diff --git a/src/Dapr.TestContainers/Harnesses/BaseHarness.cs b/src/Dapr.TestContainers/Harnesses/BaseHarness.cs index 2cf94a869..74020431a 100644 --- a/src/Dapr.TestContainers/Harnesses/BaseHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/BaseHarness.cs @@ -36,11 +36,17 @@ public abstract class BaseHarness : IAsyncContainerFixture /// A shared Docker network that's safer for CI environments. /// protected static readonly INetwork Network = new NetworkBuilder().Build(); + + /// + /// Gets the port that the Dapr sidecar is configured to talk to - this is the port the test application should use. + /// + public int AppPort { get; private protected set; } /// /// The HTTP port used by the Daprd container. /// public int DaprHttpPort => _daprd?.HttpPort ?? 0; + /// /// The gRPC port used by the Daprd container. /// diff --git a/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs b/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs index 12249871c..9d470365d 100644 --- a/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs @@ -30,7 +30,7 @@ namespace Dapr.TestContainers.Harnesses; public sealed class ConversationHarness(string componentsDir, Func? startApp, DaprRuntimeOptions options) : BaseHarness { private readonly OllamaContainer _ollama = new(Network); - + /// protected override async Task OnInitializeAsync(CancellationToken cancellationToken) { @@ -42,6 +42,7 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo // Find a random free port for the test app var assignedAppPort = PortUtilities.GetAvailablePort(); + AppPort = assignedAppPort; // 4) Configure & start daprd _daprd = new DaprdContainer( diff --git a/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs b/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs index d0f78fedc..ddf61bc34 100644 --- a/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs @@ -38,6 +38,7 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo // Find a random free port for the test app var assignedAppPort = PortUtilities.GetAvailablePort(); + AppPort = assignedAppPort; // Configure and start daprd _daprd = new DaprdContainer( diff --git a/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs b/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs index 1f19369b5..1dd8c5bae 100644 --- a/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs @@ -42,6 +42,7 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo // Find a random free port for the test app var assignedAppPort = PortUtilities.GetAvailablePort(); + AppPort = assignedAppPort; // Configure & start daprd _daprd = new DaprdContainer( diff --git a/src/Dapr.TestContainers/Harnesses/JobsHarness.cs b/src/Dapr.TestContainers/Harnesses/JobsHarness.cs index bc4a58c98..eedeb7ead 100644 --- a/src/Dapr.TestContainers/Harnesses/JobsHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/JobsHarness.cs @@ -38,6 +38,7 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo // Find a random port for the test app var assignedAppPort = PortUtilities.GetAvailablePort(); + AppPort = assignedAppPort; // Configure & start daprd _daprd = new DaprdContainer( diff --git a/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs b/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs index e91ff5126..f5ad03f30 100644 --- a/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs @@ -43,6 +43,7 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo // Find a random free port for the test app var assignedAppPort = PortUtilities.GetAvailablePort(); + AppPort = assignedAppPort; // Configure & start daprd _daprd = new DaprdContainer( diff --git a/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs b/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs index cde2c5ad1..f25a5cf18 100644 --- a/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs @@ -42,6 +42,7 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo // Find a random free port for the test app var assignedAppPort = PortUtilities.GetAvailablePort(); + AppPort = assignedAppPort; // Configure & start daprd _daprd = new DaprdContainer( diff --git a/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs b/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs index d82dbf322..1884d8c49 100644 --- a/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs @@ -46,6 +46,7 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo // Find a random free port for the test app var assignedAppPort = PortUtilities.GetAvailablePort(); + AppPort = assignedAppPort; // Configure and start daprd; point at placement & scheduler _daprd = new DaprdContainer( From 295eb6786c6f8706c919eba0f14498e2a3aaddf5 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 22 Dec 2025 01:32:30 -0600 Subject: [PATCH 16/32] Updated all the containers to expose a network alias value that reflects the container name Signed-off-by: Whit Waldo --- .../Containers/Dapr/DaprPlacementContainer.cs | 9 ++++-- .../Containers/Dapr/DaprSchedulerContainer.cs | 9 ++++-- .../Containers/Dapr/DaprdContainer.cs | 10 ++++-- .../Containers/OllamaContainer.cs | 29 ++++++++++++----- .../Containers/RabbitMqContainer.cs | 31 +++++++++++++------ .../Containers/RedisContainer.cs | 30 +++++++++++++----- 6 files changed, 87 insertions(+), 31 deletions(-) diff --git a/src/Dapr.TestContainers/Containers/Dapr/DaprPlacementContainer.cs b/src/Dapr.TestContainers/Containers/Dapr/DaprPlacementContainer.cs index 2cac65678..96da7b9f7 100644 --- a/src/Dapr.TestContainers/Containers/Dapr/DaprPlacementContainer.cs +++ b/src/Dapr.TestContainers/Containers/Dapr/DaprPlacementContainer.cs @@ -29,7 +29,12 @@ public sealed class DaprPlacementContainer : IAsyncStartable { private readonly IContainer _container; private const int InternalPort = 50006; - + private string _containerName = $"placement-{Guid.NewGuid():N}"; + + /// + /// The internal network alias/name of the container. + /// + public string NetworkAlias => _containerName; /// /// The container hostname. /// @@ -49,7 +54,7 @@ public DaprPlacementContainer(DaprRuntimeOptions options, INetwork network) //Placement service runs via port 50006 _container = new ContainerBuilder() .WithImage(options.PlacementImageTag) - .WithName($"placement-{Guid.NewGuid():N}") + .WithName(_containerName) .WithNetwork(network) .WithCommand("./placement", "-port", InternalPort.ToString()) .WithPortBinding(InternalPort, assignRandomHostPort: true) diff --git a/src/Dapr.TestContainers/Containers/Dapr/DaprSchedulerContainer.cs b/src/Dapr.TestContainers/Containers/Dapr/DaprSchedulerContainer.cs index a38e1b477..af95893d8 100644 --- a/src/Dapr.TestContainers/Containers/Dapr/DaprSchedulerContainer.cs +++ b/src/Dapr.TestContainers/Containers/Dapr/DaprSchedulerContainer.cs @@ -34,7 +34,12 @@ public sealed class DaprSchedulerContainer : IAsyncStartable private readonly IContainer _container; // Contains the data directory used by this instance of the Dapr scheduler service private string _hostDataDir = Path.Combine(Path.GetTempPath(), $"dapr-scheduler-{Guid.NewGuid():N}"); - + private string _containerName = $"scheduler-{Guid.NewGuid():N}"; + + /// + /// The internal network alias/name of the container. + /// + public string NetworkAlias => _containerName; /// /// The container's hostname. /// @@ -63,7 +68,7 @@ public DaprSchedulerContainer(DaprRuntimeOptions options, INetwork network) _container = new ContainerBuilder() .WithImage(options.SchedulerImageTag) - .WithName($"scheduler-{Guid.NewGuid():N}") + .WithName(_containerName) .WithNetwork(network) .WithCommand(cmd.ToArray()) .WithPortBinding(InternalPort, assignRandomHostPort: true) diff --git a/src/Dapr.TestContainers/Containers/Dapr/DaprdContainer.cs b/src/Dapr.TestContainers/Containers/Dapr/DaprdContainer.cs index 7158c12e5..ed271112b 100644 --- a/src/Dapr.TestContainers/Containers/Dapr/DaprdContainer.cs +++ b/src/Dapr.TestContainers/Containers/Dapr/DaprdContainer.cs @@ -33,7 +33,13 @@ public sealed class DaprdContainer : IAsyncStartable private const int InternalHttpPort = 3500; private const int InternalGrpcPort = 50001; private readonly IContainer _container; - + + private string _containerName = $"dapr-{Guid.NewGuid():N}"; + + /// + /// The internal network alias/name of the container. + /// + public string NetworkAlias => _containerName; /// /// The HTTP port of the Dapr runtime. /// @@ -88,7 +94,7 @@ public DaprdContainer(string appId, string componentsHostFolder, DaprRuntimeOpti _container = new ContainerBuilder() .WithImage(options.RuntimeImageTag) - .WithName($"dapr-{Guid.NewGuid():N}") + .WithName(_containerName) .WithLogger(ConsoleLogger.Instance) .WithCommand(cmd.ToArray()) .WithNetwork(netowrk) diff --git a/src/Dapr.TestContainers/Containers/OllamaContainer.cs b/src/Dapr.TestContainers/Containers/OllamaContainer.cs index 7a362124c..10739d550 100644 --- a/src/Dapr.TestContainers/Containers/OllamaContainer.cs +++ b/src/Dapr.TestContainers/Containers/OllamaContainer.cs @@ -25,18 +25,31 @@ namespace Dapr.TestContainers.Containers; /// /// Provides an Ollama container. /// -public sealed class OllamaContainer(INetwork network) : IAsyncStartable +public sealed class OllamaContainer : IAsyncStartable { private const int InternalPort = 11434; + private string _containerName = $"ollama-{Guid.NewGuid():N}"; - private readonly IContainer _container = new ContainerBuilder() - .WithImage("ollama/ollama") - .WithName($"ollama-{Guid.NewGuid():N}") - .WithNetwork(network) - .WithPortBinding(InternalPort, assignRandomHostPort: true) - .WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(InternalPort)) - .Build(); + private readonly IContainer _container; + /// + /// Provides an Ollama container. + /// + public OllamaContainer(INetwork network) + { + _container = new ContainerBuilder() + .WithImage("ollama/ollama") + .WithName(_containerName) + .WithNetwork(network) + .WithPortBinding(InternalPort, assignRandomHostPort: true) + .WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(InternalPort)) + .Build(); + } + + /// + /// The internal network alias/name of the container. + /// + public string NetworkAlias => _containerName; /// /// The hostname of the container. /// diff --git a/src/Dapr.TestContainers/Containers/RabbitMqContainer.cs b/src/Dapr.TestContainers/Containers/RabbitMqContainer.cs index fc68ae38d..33234ffb0 100644 --- a/src/Dapr.TestContainers/Containers/RabbitMqContainer.cs +++ b/src/Dapr.TestContainers/Containers/RabbitMqContainer.cs @@ -26,19 +26,32 @@ namespace Dapr.TestContainers.Containers; /// /// Provides a RabbitMQ container. /// -public sealed class RabbitMqContainer(INetwork network) : IAsyncStartable +public sealed class RabbitMqContainer : IAsyncStartable { private const int InternalPort = 5672; - private readonly IContainer _container = new ContainerBuilder() - .WithImage("rabbitmq:alpine") - .WithName($"rabbitmq-{Guid.NewGuid():N}") - .WithNetwork(network) - .WithLogger(ConsoleLogger.Instance) - .WithPortBinding(InternalPort, assignRandomHostPort: true) - .WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(InternalPort)) - .Build(); + private readonly IContainer _container; + private string _containerName = $"rabbitmq-{Guid.NewGuid():N}"; + /// + /// Provides a RabbitMQ container. + /// + public RabbitMqContainer(INetwork network) + { + _container = new ContainerBuilder() + .WithImage("rabbitmq:alpine") + .WithName(_containerName) + .WithNetwork(network) + .WithLogger(ConsoleLogger.Instance) + .WithPortBinding(InternalPort, assignRandomHostPort: true) + .WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(InternalPort)) + .Build(); + } + + /// + /// The internal network alias/name of the container. + /// + public string NetworkAlias => _containerName; /// /// The hostname of the container. /// diff --git a/src/Dapr.TestContainers/Containers/RedisContainer.cs b/src/Dapr.TestContainers/Containers/RedisContainer.cs index 94de53a96..03925fd69 100644 --- a/src/Dapr.TestContainers/Containers/RedisContainer.cs +++ b/src/Dapr.TestContainers/Containers/RedisContainer.cs @@ -25,17 +25,31 @@ namespace Dapr.TestContainers.Containers; /// /// Provides a Redis container. /// -public sealed class RedisContainer(INetwork network) : IAsyncStartable +public sealed class RedisContainer : IAsyncStartable { private const int InternalPort = 6379; - private readonly IContainer _container = new ContainerBuilder() - .WithImage("redis:alpine") - .WithName($"redis-{Guid.NewGuid():N}") - .WithNetwork(network) - .WithPortBinding(InternalPort, assignRandomHostPort: true) - .WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(InternalPort)) - .Build(); + private readonly string _containerName = $"redis-{Guid.NewGuid():N}"; + + private readonly IContainer _container; + /// + /// Provides a Redis container. + /// + public RedisContainer(INetwork network) + { + _container = new ContainerBuilder() + .WithImage("redis:alpine") + .WithName(_containerName) + .WithNetwork(network) + .WithPortBinding(InternalPort, assignRandomHostPort: true) + .WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(InternalPort)) + .Build(); + } + + /// + /// The internal network alias/name of the container. + /// + public string NetworkAlias => _containerName; /// /// The hostname of the container. /// From c742bae361e7387b61b2a498b23c2aa1bfe91445 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 22 Dec 2025 02:08:05 -0600 Subject: [PATCH 17/32] Updating the various harnesses to use the network alias from the harness to the container Signed-off-by: Whit Waldo --- src/Dapr.TestContainers/Containers/RabbitMqContainer.cs | 8 ++++---- src/Dapr.TestContainers/Harnesses/ActorHarness.cs | 2 +- src/Dapr.TestContainers/Harnesses/ConversationHarness.cs | 2 +- .../Harnesses/DistributedLockHarness.cs | 2 +- src/Dapr.TestContainers/Harnesses/PubSubHarness.cs | 2 +- .../Harnesses/StateManagementHarness.cs | 2 +- src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Dapr.TestContainers/Containers/RabbitMqContainer.cs b/src/Dapr.TestContainers/Containers/RabbitMqContainer.cs index 33234ffb0..6763d9745 100644 --- a/src/Dapr.TestContainers/Containers/RabbitMqContainer.cs +++ b/src/Dapr.TestContainers/Containers/RabbitMqContainer.cs @@ -81,9 +81,9 @@ public static class Yaml /// /// Writes a PubSub YAML component. /// - public static string WritePubSubYamlToFolder(string folderPath, string fileName = "rabbitmq-pubsub.yaml") + public static string WritePubSubYamlToFolder(string folderPath, string fileName = "rabbitmq-pubsub.yaml", string rabbitmqHost = "localhost:5672") { - var yaml = GetPubSubYaml(); + var yaml = GetPubSubYaml(rabbitmqHost); return WriteToFolder(folderPath, fileName, yaml); } @@ -95,7 +95,7 @@ private static string WriteToFolder(string folderPath, string fileName, string y return fullPath; } - private static string GetPubSubYaml() => + private static string GetPubSubYaml(string rabbitmqHost) => $@"apiVersion: dapr.io/v1alpha kind: Component metadata: @@ -106,7 +106,7 @@ private static string GetPubSubYaml() => - name: protocol value: amqp - name: hostname - value: localhost + value: {rabbitmqHost} - name: username value: default - name: password diff --git a/src/Dapr.TestContainers/Harnesses/ActorHarness.cs b/src/Dapr.TestContainers/Harnesses/ActorHarness.cs index a1e735ac0..3e4a60490 100644 --- a/src/Dapr.TestContainers/Harnesses/ActorHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/ActorHarness.cs @@ -42,7 +42,7 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo await _schedueler.StartAsync(cancellationToken); // Emit component YAMLs pointing to Redis - RedisContainer.Yaml.WriteStateStoreYamlToFolder(componentsDir, redisHost: $"{_redis.Host}:{_redis.Port}"); + RedisContainer.Yaml.WriteStateStoreYamlToFolder(componentsDir, redisHost: $"{_redis.NetworkAlias}:{_redis.Port}"); // Find a random free port for the test app var assignedAppPort = PortUtilities.GetAvailablePort(); diff --git a/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs b/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs index 9d470365d..edee614ab 100644 --- a/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs @@ -38,7 +38,7 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo await _ollama.StartAsync(cancellationToken); // Emit component YAMLs for Ollama (use the default tiny model) - OllamaContainer.Yaml.WriteConversationYamlToFolder(componentsDir); + OllamaContainer.Yaml.WriteConversationYamlToFolder(componentsDir, endpoint: $"http://{_ollama.NetworkAlias}:{_ollama.Port}/v1"); // Find a random free port for the test app var assignedAppPort = PortUtilities.GetAvailablePort(); diff --git a/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs b/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs index 1dd8c5bae..057526936 100644 --- a/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs @@ -38,7 +38,7 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo await _redis.StartAsync(cancellationToken); // Emit component YAMLs pointing to Redis - RedisContainer.Yaml.WriteDistributedLockYamlToFolder(componentsDir, redisHost: $"{_redis.Host}:{_redis.Port}"); + RedisContainer.Yaml.WriteDistributedLockYamlToFolder(componentsDir, redisHost: $"{_redis.NetworkAlias}:{_redis.Port}"); // Find a random free port for the test app var assignedAppPort = PortUtilities.GetAvailablePort(); diff --git a/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs b/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs index f5ad03f30..bc69c820b 100644 --- a/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs @@ -39,7 +39,7 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo await _rabbitmq.StartAsync(cancellationToken); // Emit component YAMLs pointing to RabbitMQ - RabbitMqContainer.Yaml.WritePubSubYamlToFolder(componentsDir); + RabbitMqContainer.Yaml.WritePubSubYamlToFolder(componentsDir, rabbitmqHost: $"{_rabbitmq.NetworkAlias}:{_rabbitmq.Port}"); // Find a random free port for the test app var assignedAppPort = PortUtilities.GetAvailablePort(); diff --git a/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs b/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs index f25a5cf18..65cd890f9 100644 --- a/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs @@ -38,7 +38,7 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo await _redis.StartAsync(cancellationToken); // Emit component YAMLs pointing to Redis - RedisContainer.Yaml.WriteStateStoreYamlToFolder(componentsDir, redisHost: $"{_redis.Host}:{_redis.Port}"); + RedisContainer.Yaml.WriteStateStoreYamlToFolder(componentsDir, redisHost: $"{_redis.NetworkAlias}:{_redis.Port}"); // Find a random free port for the test app var assignedAppPort = PortUtilities.GetAvailablePort(); diff --git a/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs b/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs index 1884d8c49..05dfb51c1 100644 --- a/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs @@ -42,7 +42,7 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo await _scheduler.StartAsync(cancellationToken); // Emit component YAMLs pointing to Redis - RedisContainer.Yaml.WriteStateStoreYamlToFolder(componentsDir, redisHost: $"{_redis.Host}:{_redis.Port}"); + RedisContainer.Yaml.WriteStateStoreYamlToFolder(componentsDir, redisHost: $"{_redis.NetworkAlias}:{_redis.Port}"); // Find a random free port for the test app var assignedAppPort = PortUtilities.GetAvailablePort(); From 3c08d973759c66572c69347846435dd876cc7bc8 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 22 Dec 2025 20:10:56 -0600 Subject: [PATCH 18/32] Updated harnesses to make better use of the base class to avoid repeating code for starting up daprd and the start app, when specified Signed-off-by: Whit Waldo --- .../Common/DaprHarnessBuilder.cs | 25 +++---- .../Harnesses/ActorHarness.cs | 47 ++++++------ .../Harnesses/BaseHarness.cs | 72 ++++++++++++++----- .../Harnesses/ConversationHarness.cs | 42 +++++------ .../Harnesses/CryptographyHarness.cs | 46 ++++++------ .../Harnesses/DistributedLockHarness.cs | 35 ++++----- .../Harnesses/JobsHarness.cs | 39 +++++----- .../Harnesses/PubSubHarness.cs | 48 ++++--------- .../Harnesses/StateManagementHarness.cs | 35 ++++----- .../Harnesses/WorkflowHarness.cs | 56 +++++++-------- 10 files changed, 203 insertions(+), 242 deletions(-) diff --git a/src/Dapr.TestContainers/Common/DaprHarnessBuilder.cs b/src/Dapr.TestContainers/Common/DaprHarnessBuilder.cs index c1b9a8be5..b2ce94dd1 100644 --- a/src/Dapr.TestContainers/Common/DaprHarnessBuilder.cs +++ b/src/Dapr.TestContainers/Common/DaprHarnessBuilder.cs @@ -29,56 +29,49 @@ public sealed class DaprHarnessBuilder(DaprRuntimeOptions options, Func /// The path to the Dapr resources. - public WorkflowHarness BuildWorkflow(string componentsDir) => - new WorkflowHarness(componentsDir, startApp, options); + public WorkflowHarness BuildWorkflow(string componentsDir) => new(componentsDir, startApp, options); /// /// Builds a distributed lock harness. /// /// The path to the Dapr resources. - public DistributedLockHarness BuildDistributedLock(string componentsDir) => - new DistributedLockHarness(componentsDir, startApp, options); + public DistributedLockHarness BuildDistributedLock(string componentsDir) => new(componentsDir, startApp, options); /// /// Builds a conversation harness. /// /// The path to the Dapr resources. - public ConversationHarness BuildConversation(string componentsDir) => - new ConversationHarness(componentsDir, startApp, options); + public ConversationHarness BuildConversation(string componentsDir) => new(componentsDir, startApp, options); /// /// Builds a cryptography harness. /// /// The path to the Dapr resources. /// The path to the cryptography keys. - public CryptographyHarness BuildCryptography(string componentsDir, string keysDir) => - new CryptographyHarness(componentsDir, startApp, keysDir, options); + public CryptographyHarness BuildCryptography(string componentsDir, string keysDir) => + new(componentsDir, startApp, keysDir, options); /// /// Builds a jobs harness. /// /// The path to the Dapr resources. - public JobsHarness BuildJobs(string componentsDir) => - new JobsHarness(componentsDir, startApp, options); + public JobsHarness BuildJobs(string componentsDir) => new(componentsDir, startApp, options); /// /// Builds a PubSub harness. /// /// The path to the Dapr resources. - public PubSubHarness BuildPubSub(string componentsDir) => - new PubSubHarness(componentsDir, startApp, options); + public PubSubHarness BuildPubSub(string componentsDir) => new(componentsDir, startApp, options); /// /// Builds a state management harness. /// /// The path to the Dapr resources. - public StateManagementHarness BuildStateManagement(string componentsDir) => - new StateManagementHarness(componentsDir, startApp, options); + public StateManagementHarness BuildStateManagement(string componentsDir) => new(componentsDir, startApp, options); /// /// Builds an actor harness. /// /// The path to the Dapr resources. - public ActorHarness BuildActors(string componentsDir) => - new ActorHarness(componentsDir, startApp, options); + public ActorHarness BuildActors(string componentsDir) => new(componentsDir, startApp, options); } diff --git a/src/Dapr.TestContainers/Harnesses/ActorHarness.cs b/src/Dapr.TestContainers/Harnesses/ActorHarness.cs index 3e4a60490..98601de1a 100644 --- a/src/Dapr.TestContainers/Harnesses/ActorHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/ActorHarness.cs @@ -14,7 +14,6 @@ using System; using System.Threading; using System.Threading.Tasks; -using Dapr.TestContainers.Common; using Dapr.TestContainers.Common.Options; using Dapr.TestContainers.Containers; using Dapr.TestContainers.Containers.Dapr; @@ -24,15 +23,26 @@ namespace Dapr.TestContainers.Harnesses; /// /// Provides an implementation harness for Dapr's actor building block. /// -/// The directory to Dapr components. -/// The test app to validate in the harness. -/// The dapr runtime options. -public sealed class ActorHarness(string componentsDir, Func? startApp, DaprRuntimeOptions options) : BaseHarness +public sealed class ActorHarness : BaseHarness { private readonly RedisContainer _redis = new(Network); - private readonly DaprPlacementContainer _placement = new(options, Network); - private readonly DaprSchedulerContainer _schedueler = new(options, Network); - + private readonly DaprPlacementContainer _placement; + private readonly DaprSchedulerContainer _schedueler; + private readonly string componentsDir; + + /// + /// Provides an implementation harness for Dapr's actor building block. + /// + /// The directory to Dapr components. + /// The test app to validate in the harness. + /// The dapr runtime options. + public ActorHarness(string componentsDir, Func? startApp, DaprRuntimeOptions options) : base(componentsDir, startApp, options) + { + this.componentsDir = componentsDir; + _placement = new DaprPlacementContainer(options, Network); + _schedueler = new DaprSchedulerContainer(options, Network); + } + /// protected override async Task OnInitializeAsync(CancellationToken cancellationToken) { @@ -43,24 +53,9 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo // Emit component YAMLs pointing to Redis RedisContainer.Yaml.WriteStateStoreYamlToFolder(componentsDir, redisHost: $"{_redis.NetworkAlias}:{_redis.Port}"); - - // Find a random free port for the test app - var assignedAppPort = PortUtilities.GetAvailablePort(); - AppPort = assignedAppPort; - - // Configure and start daprd, point at placement & scheduler - _daprd = new DaprdContainer( - appId: options.AppId, - componentsHostFolder: componentsDir, - options: options with {AppPort = assignedAppPort}, - Network, - new HostPortPair(_placement.Host, _placement.Port), - new HostPortPair(_schedueler.Host, _schedueler.Port)); - await _daprd.StartAsync(cancellationToken); - - // Start the test - if (startApp is not null) - await startApp(assignedAppPort); + + DaprPlacementPort = _placement.Port; + DaprSchedulerPort = _schedueler.Port; } /// diff --git a/src/Dapr.TestContainers/Harnesses/BaseHarness.cs b/src/Dapr.TestContainers/Harnesses/BaseHarness.cs index 74020431a..bc4523213 100644 --- a/src/Dapr.TestContainers/Harnesses/BaseHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/BaseHarness.cs @@ -12,10 +12,12 @@ // ------------------------------------------------------------------------ using System; +using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; using Dapr.TestContainers.Common; +using Dapr.TestContainers.Common.Options; using Dapr.TestContainers.Containers.Dapr; using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Networks; @@ -25,7 +27,7 @@ namespace Dapr.TestContainers.Harnesses; /// /// Provides a base harness for building Dapr building block harnesses. /// -public abstract class BaseHarness : IAsyncContainerFixture +public abstract class BaseHarness(string componentsDirectory, Func? startApp, DaprRuntimeOptions options) : IAsyncContainerFixture { /// /// The Daprd container exposed by the harness. @@ -36,11 +38,11 @@ public abstract class BaseHarness : IAsyncContainerFixture /// A shared Docker network that's safer for CI environments. /// protected static readonly INetwork Network = new NetworkBuilder().Build(); - + /// /// Gets the port that the Dapr sidecar is configured to talk to - this is the port the test application should use. /// - public int AppPort { get; private protected set; } + public int AppPort { get; private protected set; } = PortUtilities.GetAvailablePort(); /// /// The HTTP port used by the Daprd container. @@ -52,13 +54,27 @@ public abstract class BaseHarness : IAsyncContainerFixture /// public int DaprGrpcPort => _daprd?.GrpcPort ?? 0; + /// + /// The Dapr components directory. + /// + protected string ComponentsDirectory => componentsDirectory; + + /// + /// The port of the Dapr placement service, if started. + /// + protected int? DaprPlacementPort { get; set; } + + /// + /// The port of the Dapr scheduler service, if started. + /// + protected int? DaprSchedulerPort { get; set; } + /// /// The specific container startup logic for the harness. /// /// Cancellation token. - /// protected abstract Task OnInitializeAsync(CancellationToken cancellationToken); - + /// /// Initializes and runs the test app with the harness. /// @@ -66,10 +82,36 @@ public abstract class BaseHarness : IAsyncContainerFixture /// public async Task InitializeAsync(CancellationToken cancellationToken = default) { - // Automatically link the Dapr .NET SDK to these containers - ConfigureSdkEnvironment(); - // Run the actual container orchestration defined in the subclass + // Automatically link the Dapr .NET SDK to these containers via environment variables + if (DaprHttpPort > 0) + Environment.SetEnvironmentVariable("DAPR_HTTP_PORT", DaprHttpPort.ToString()); + if (DaprGrpcPort > 0) + Environment.SetEnvironmentVariable("DAPR_GRPC_PORT", DaprGrpcPort.ToString()); + + // Run the actual container orchestration defined in the subclass to set up any pre-requisite containers before loading daprd and the start app, if specified await OnInitializeAsync(cancellationToken); + + // Configure and start daprd; point at placement & scheduler + _daprd = new DaprdContainer( + appId: options.AppId, + componentsHostFolder: ComponentsDirectory, + options: options with {AppPort = this.AppPort}, + Network, + DaprPlacementPort is null ? null : new HostPortPair("host.docker.internal", DaprPlacementPort.Value), + DaprSchedulerPort is null ? null : new HostPortPair("host.docker.internal", DaprSchedulerPort.Value)); + + // Create a list of tasks to run concurrently - namely, start daprd and the startApp, if specified + var tasks = new List + { + _daprd.StartAsync(cancellationToken) + }; + + if (startApp is not null) + { + tasks.Add(startApp(this.AppPort)); + } + + await Task.WhenAll(tasks); } /// @@ -80,6 +122,10 @@ public virtual async ValueTask DisposeAsync() if (_daprd is not null) await _daprd.DisposeAsync(); await Network.DisposeAsync(); + + // Clean up generated YAML files + CleanupComponents(ComponentsDirectory); + GC.SuppressFinalize(this); } @@ -87,7 +133,7 @@ public virtual async ValueTask DisposeAsync() /// Deletes the specified directory recursively as part of a clean-up operation. /// /// The clean to clean up. - protected virtual void CleanupComponents(string path) + protected static void CleanupComponents(string path) { if (Directory.Exists(path)) { @@ -101,12 +147,4 @@ protected virtual void CleanupComponents(string path) } } } - - private void ConfigureSdkEnvironment() - { - if (DaprHttpPort > 0) - Environment.SetEnvironmentVariable("DAPR_HTTP_PORT", DaprHttpPort.ToString()); - if (DaprGrpcPort > 0) - Environment.SetEnvironmentVariable("DAPR_GRPC_PORT", DaprGrpcPort.ToString()); - } } diff --git a/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs b/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs index edee614ab..3564542f5 100644 --- a/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs @@ -14,23 +14,30 @@ using System; using System.Threading; using System.Threading.Tasks; -using Dapr.TestContainers.Common; using Dapr.TestContainers.Common.Options; using Dapr.TestContainers.Containers; -using Dapr.TestContainers.Containers.Dapr; namespace Dapr.TestContainers.Harnesses; /// /// Provides an implementation harness for conversation functionality. /// -/// The directory to Dapr components. -/// The test app to validate in the harness. -/// The Dapr runtime options. -public sealed class ConversationHarness(string componentsDir, Func? startApp, DaprRuntimeOptions options) : BaseHarness +public sealed class ConversationHarness : BaseHarness { private readonly OllamaContainer _ollama = new(Network); - + private readonly string componentsDir; + + /// + /// Provides an implementation harness for conversation functionality. + /// + /// The directory to Dapr components. + /// The test app to validate in the harness. + /// The Dapr runtime options. + public ConversationHarness(string componentsDir, Func? startApp, DaprRuntimeOptions options) : base(componentsDir, startApp, options) + { + this.componentsDir = componentsDir; + } + /// protected override async Task OnInitializeAsync(CancellationToken cancellationToken) { @@ -38,24 +45,9 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo await _ollama.StartAsync(cancellationToken); // Emit component YAMLs for Ollama (use the default tiny model) - OllamaContainer.Yaml.WriteConversationYamlToFolder(componentsDir, endpoint: $"http://{_ollama.NetworkAlias}:{_ollama.Port}/v1"); - - // Find a random free port for the test app - var assignedAppPort = PortUtilities.GetAvailablePort(); - AppPort = assignedAppPort; - - // 4) Configure & start daprd - _daprd = new DaprdContainer( - appId: options.AppId, - componentsHostFolder: componentsDir, - options: options with {AppPort = assignedAppPort}, - Network); - await _daprd.StartAsync(cancellationToken); - - // Start the app - if (startApp is not null) - await startApp(assignedAppPort); - } + OllamaContainer.Yaml.WriteConversationYamlToFolder(componentsDir, + endpoint: $"http://{_ollama.NetworkAlias}:{_ollama.Port}/v1"); + } /// public override async ValueTask DisposeAsync() diff --git a/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs b/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs index ddf61bc34..bb6d4ee51 100644 --- a/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs @@ -14,44 +14,40 @@ using System; using System.Threading; using System.Threading.Tasks; -using Dapr.TestContainers.Common; using Dapr.TestContainers.Common.Options; using Dapr.TestContainers.Containers; -using Dapr.TestContainers.Containers.Dapr; namespace Dapr.TestContainers.Harnesses; /// /// Provides an implementation harness for Dapr's cryptography building block. /// -/// The directory to Dapr components. -/// The test app to validate in the harness. -/// The Dapr runtime options. -/// The path locally to the cryptography keys to use. -public sealed class CryptographyHarness(string componentsDir, Func? startApp, string keyPath, DaprRuntimeOptions options) : BaseHarness +public sealed class CryptographyHarness : BaseHarness { + private readonly string componentsDir; + private readonly string keyPath; + + /// + /// Provides an implementation harness for Dapr's cryptography building block. + /// + /// The directory to Dapr components. + /// The test app to validate in the harness. + /// The Dapr runtime options. + /// The path locally to the cryptography keys to use. + public CryptographyHarness(string componentsDir, Func? startApp, string keyPath, DaprRuntimeOptions options) : base(componentsDir, startApp, options) + { + this.componentsDir = componentsDir; + this.keyPath = keyPath; + } + /// - protected override async Task OnInitializeAsync(CancellationToken cancellationToken) + protected override Task OnInitializeAsync(CancellationToken cancellationToken) { // Emit the component YAML describing the local crypto key store LocalStorageCryptographyContainer.Yaml.WriteCryptoYamlToFolder(componentsDir, keyPath); - - // Find a random free port for the test app - var assignedAppPort = PortUtilities.GetAvailablePort(); - AppPort = assignedAppPort; - - // Configure and start daprd - _daprd = new DaprdContainer( - appId: options.AppId, - componentsHostFolder: componentsDir, - options: options with {AppPort = assignedAppPort}, - Network); - await _daprd.StartAsync(cancellationToken); - - // Start the app - if (startApp is not null) - await startApp(assignedAppPort); - } + + return Task.CompletedTask; + } /// public override async ValueTask DisposeAsync() diff --git a/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs b/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs index 057526936..a3010cf04 100644 --- a/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs @@ -14,22 +14,29 @@ using System; using System.Threading; using System.Threading.Tasks; -using Dapr.TestContainers.Common; using Dapr.TestContainers.Common.Options; using Dapr.TestContainers.Containers; -using Dapr.TestContainers.Containers.Dapr; namespace Dapr.TestContainers.Harnesses; /// /// Provides an implementation harness for Dapr's distributed lock building block. /// -/// The directory to Dapr components. -/// The test app to validate in the harness. -/// The Dapr runtime options. -public sealed class DistributedLockHarness(string componentsDir, Func? startApp, DaprRuntimeOptions options) : BaseHarness +public sealed class DistributedLockHarness : BaseHarness { private readonly RedisContainer _redis = new(Network); + private readonly string componentsDir; + + /// + /// Provides an implementation harness for Dapr's distributed lock building block. + /// + /// The directory to Dapr components. + /// The test app to validate in the harness. + /// The Dapr runtime options. + public DistributedLockHarness(string componentsDir, Func? startApp, DaprRuntimeOptions options) : base(componentsDir, startApp, options) + { + this.componentsDir = componentsDir; + } /// protected override async Task OnInitializeAsync(CancellationToken cancellationToken) @@ -39,22 +46,6 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo // Emit component YAMLs pointing to Redis RedisContainer.Yaml.WriteDistributedLockYamlToFolder(componentsDir, redisHost: $"{_redis.NetworkAlias}:{_redis.Port}"); - - // Find a random free port for the test app - var assignedAppPort = PortUtilities.GetAvailablePort(); - AppPort = assignedAppPort; - - // Configure & start daprd - _daprd = new DaprdContainer( - appId: options.AppId, - componentsHostFolder: componentsDir, - options: options with {AppPort = assignedAppPort}, - Network); - await _daprd.StartAsync(cancellationToken); - - // Start the app - if (startApp is not null) - await startApp(assignedAppPort); } /// diff --git a/src/Dapr.TestContainers/Harnesses/JobsHarness.cs b/src/Dapr.TestContainers/Harnesses/JobsHarness.cs index eedeb7ead..22fdaf450 100644 --- a/src/Dapr.TestContainers/Harnesses/JobsHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/JobsHarness.cs @@ -14,7 +14,6 @@ using System; using System.Threading; using System.Threading.Tasks; -using Dapr.TestContainers.Common; using Dapr.TestContainers.Common.Options; using Dapr.TestContainers.Containers.Dapr; @@ -23,34 +22,28 @@ namespace Dapr.TestContainers.Harnesses; /// /// Provides an implementation harness for the Jobs building block. /// -/// The directory to Dapr components. -/// The test app to validate in the harness. -/// The Dapr runtime options. -public sealed class JobsHarness(string componentsDir, Func? startApp, DaprRuntimeOptions options) : BaseHarness +public sealed class JobsHarness : BaseHarness { - private readonly DaprSchedulerContainer _scheduler = new(options, Network); - + private readonly DaprSchedulerContainer _scheduler; + private readonly string componentsDir; + + /// + /// Provides an implementation harness for the Jobs building block. + /// + /// The directory to Dapr components. + /// The test app to validate in the harness. + /// The Dapr runtime options. + public JobsHarness(string componentsDir, Func? startApp, DaprRuntimeOptions options) : base(componentsDir, startApp, options) + { + this.componentsDir = componentsDir; + _scheduler = new DaprSchedulerContainer(options, Network); + } + /// protected override async Task OnInitializeAsync(CancellationToken cancellationToken) { // Start the infrastructure await _scheduler.StartAsync(cancellationToken); - - // Find a random port for the test app - var assignedAppPort = PortUtilities.GetAvailablePort(); - AppPort = assignedAppPort; - - // Configure & start daprd - _daprd = new DaprdContainer( - appId: options.AppId, - componentsHostFolder: componentsDir, - options: options with {AppPort = assignedAppPort}, - Network); - await _daprd.StartAsync(cancellationToken); - - // Start the app - if (startApp is not null) - await startApp(assignedAppPort); } /// diff --git a/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs b/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs index bc69c820b..32562cb91 100644 --- a/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs @@ -12,26 +12,32 @@ // ------------------------------------------------------------------------ using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Dapr.TestContainers.Common; using Dapr.TestContainers.Common.Options; using Dapr.TestContainers.Containers; -using Dapr.TestContainers.Containers.Dapr; namespace Dapr.TestContainers.Harnesses; /// /// Provides an implementation harness for Dapr's pub/sub building block. /// -/// The directory to Dapr components. -/// The test app to validate in the harness. -/// The Dapr runtime options. -public sealed class PubSubHarness(string componentsDir, Func? startApp, DaprRuntimeOptions options) : BaseHarness +public sealed class PubSubHarness : BaseHarness { private readonly RabbitMqContainer _rabbitmq = new(Network); - + private readonly string componentsDir; + + /// + /// Provides an implementation harness for Dapr's pub/sub building block. + /// + /// The directory to Dapr components. + /// The test app to validate in the harness. + /// The Dapr runtime options. + public PubSubHarness(string componentsDir, Func? startApp, DaprRuntimeOptions options): base(componentsDir, startApp, options) + { + this.componentsDir = componentsDir; + } + /// protected override async Task OnInitializeAsync(CancellationToken cancellationToken) { @@ -40,32 +46,6 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo // Emit component YAMLs pointing to RabbitMQ RabbitMqContainer.Yaml.WritePubSubYamlToFolder(componentsDir, rabbitmqHost: $"{_rabbitmq.NetworkAlias}:{_rabbitmq.Port}"); - - // Find a random free port for the test app - var assignedAppPort = PortUtilities.GetAvailablePort(); - AppPort = assignedAppPort; - - // Configure & start daprd - _daprd = new DaprdContainer( - appId: options.AppId, - componentsHostFolder: componentsDir, - options: options with {AppPort = assignedAppPort}, - Network); - - // Create the tasks to wait on, starting with the daprd container - var tasks = new List - { - _daprd.StartAsync(cancellationToken) - }; - - // Start the test app - if (startApp is not null) - { - var appTask = startApp(assignedAppPort); - tasks.Add(appTask); - } - - await Task.WhenAll(tasks); } /// diff --git a/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs b/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs index 65cd890f9..7903e1ca1 100644 --- a/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs @@ -14,22 +14,29 @@ using System; using System.Threading; using System.Threading.Tasks; -using Dapr.TestContainers.Common; using Dapr.TestContainers.Common.Options; using Dapr.TestContainers.Containers; -using Dapr.TestContainers.Containers.Dapr; namespace Dapr.TestContainers.Harnesses; /// /// Provides an implementation harness for Dapr's state management building block. /// -/// The directory to Dapr components. -/// The test app to validate in the harness. -/// The Dapr runtime options. -public sealed class StateManagementHarness(string componentsDir, Func? startApp, DaprRuntimeOptions options) : BaseHarness +public sealed class StateManagementHarness : BaseHarness { private readonly RedisContainer _redis = new(Network); + private readonly string componentsDir; + + /// + /// Provides an implementation harness for Dapr's state management building block. + /// + /// The directory to Dapr components. + /// The test app to validate in the harness. + /// The Dapr runtime options. + public StateManagementHarness(string componentsDir, Func? startApp, DaprRuntimeOptions options) : base(componentsDir, startApp, options) + { + this.componentsDir = componentsDir; + } /// protected override async Task OnInitializeAsync(CancellationToken cancellationToken) @@ -39,22 +46,6 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo // Emit component YAMLs pointing to Redis RedisContainer.Yaml.WriteStateStoreYamlToFolder(componentsDir, redisHost: $"{_redis.NetworkAlias}:{_redis.Port}"); - - // Find a random free port for the test app - var assignedAppPort = PortUtilities.GetAvailablePort(); - AppPort = assignedAppPort; - - // Configure & start daprd - _daprd = new DaprdContainer( - appId: options.AppId, - componentsHostFolder: componentsDir, - options: options with {AppPort = assignedAppPort}, - Network); - await _daprd.StartAsync(cancellationToken); - - // Start the app - if (startApp is not null) - await startApp(assignedAppPort); } /// diff --git a/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs b/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs index 05dfb51c1..8f55ccea7 100644 --- a/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs @@ -14,7 +14,6 @@ using System; using System.Threading; using System.Threading.Tasks; -using Dapr.TestContainers.Common; using Dapr.TestContainers.Common.Options; using Dapr.TestContainers.Containers; using Dapr.TestContainers.Containers.Dapr; @@ -24,45 +23,41 @@ namespace Dapr.TestContainers.Harnesses; /// /// Provides an implementation harness for Dapr's Workflow building block. /// -/// The directory to Dapr components. -/// The test app to validate in the harness. -/// The Dapr runtime options. -public sealed class WorkflowHarness(string componentsDir, Func? startApp, DaprRuntimeOptions options) : BaseHarness +public sealed class WorkflowHarness : BaseHarness { private readonly RedisContainer _redis = new(Network); - private readonly DaprPlacementContainer _placement = new(options, Network); - private readonly DaprSchedulerContainer _scheduler = new(options, Network); + private readonly DaprPlacementContainer _placement; + private readonly DaprSchedulerContainer _scheduler; + + /// + /// Provides an implementation harness for Dapr's Workflow building block. + /// + /// The directory to Dapr components. + /// The test app to validate in the harness. + /// The Dapr runtime options. + public WorkflowHarness(string componentsDir, Func? startApp, DaprRuntimeOptions options) : base(componentsDir, startApp, options) + { + _placement = new DaprPlacementContainer(options, Network); + _scheduler = new DaprSchedulerContainer(options, Network); + } + /// protected override async Task OnInitializeAsync(CancellationToken cancellationToken) { - // Start infrastructure - await _redis.StartAsync(cancellationToken); + // Start infrastructure + await _redis.StartAsync(cancellationToken); await _placement.StartAsync(cancellationToken); await _scheduler.StartAsync(cancellationToken); - - // Emit component YAMLs pointing to Redis - RedisContainer.Yaml.WriteStateStoreYamlToFolder(componentsDir, redisHost: $"{_redis.NetworkAlias}:{_redis.Port}"); - - // Find a random free port for the test app - var assignedAppPort = PortUtilities.GetAvailablePort(); - AppPort = assignedAppPort; - // Configure and start daprd; point at placement & scheduler - _daprd = new DaprdContainer( - appId: options.AppId, - componentsHostFolder: componentsDir, - options: options with {AppPort = assignedAppPort}, - Network, - new HostPortPair("host.docker.internal", _placement.Port), - new HostPortPair("host.docker.internal", _scheduler.Port)); - await _daprd.StartAsync(cancellationToken); + // Emit component YAMLs pointing to Redis + RedisContainer.Yaml.WriteStateStoreYamlToFolder(ComponentsDirectory, redisHost: $"{_redis.NetworkAlias}:6379"); - // Start the app - if (startApp is not null) - await startApp(assignedAppPort); + // Set the service ports + this.DaprPlacementPort = _placement.Port; + this.DaprSchedulerPort = _scheduler.Port; } - + /// public override async ValueTask DisposeAsync() { @@ -71,8 +66,5 @@ public override async ValueTask DisposeAsync() await _placement.DisposeAsync(); await _scheduler.DisposeAsync(); await _redis.DisposeAsync(); - - // Cleanup the generated YAML files - CleanupComponents(componentsDir); } } From 09309d5bf0816875b120f52161400e1a9ea995f7 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 22 Dec 2025 20:57:35 -0600 Subject: [PATCH 19/32] Updated the base harness to address an out-of-order bug Signed-off-by: Whit Waldo --- src/Dapr.TestContainers/Harnesses/BaseHarness.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Dapr.TestContainers/Harnesses/BaseHarness.cs b/src/Dapr.TestContainers/Harnesses/BaseHarness.cs index bc4523213..112c11623 100644 --- a/src/Dapr.TestContainers/Harnesses/BaseHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/BaseHarness.cs @@ -82,12 +82,6 @@ public abstract class BaseHarness(string componentsDirectory, Func? s /// public async Task InitializeAsync(CancellationToken cancellationToken = default) { - // Automatically link the Dapr .NET SDK to these containers via environment variables - if (DaprHttpPort > 0) - Environment.SetEnvironmentVariable("DAPR_HTTP_PORT", DaprHttpPort.ToString()); - if (DaprGrpcPort > 0) - Environment.SetEnvironmentVariable("DAPR_GRPC_PORT", DaprGrpcPort.ToString()); - // Run the actual container orchestration defined in the subclass to set up any pre-requisite containers before loading daprd and the start app, if specified await OnInitializeAsync(cancellationToken); @@ -111,7 +105,16 @@ public async Task InitializeAsync(CancellationToken cancellationToken = default) tasks.Add(startApp(this.AppPort)); } + // Wait for both to start await Task.WhenAll(tasks); + + // Now that _daprd is populated and ports are assigned, automatically link the Dapr .NET SDK to these + // containers via environment variables. + // This ensures that when the test body creates a DaprClient, it finds the right ports. + if (DaprHttpPort > 0) + Environment.SetEnvironmentVariable("DAPR_HTTP_PORT", DaprHttpPort.ToString()); + if (DaprGrpcPort > 0) + Environment.SetEnvironmentVariable("DAPR_GRPC_PORT", DaprGrpcPort.ToString()); } /// From 0c8e33cde341ecfc2e96b9a188e19434e2968cdd Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 22 Dec 2025 21:23:19 -0600 Subject: [PATCH 20/32] Updated the default version to use the "latest" tag instead of a hardcoded version - this leaves the door open to specific versions without having to manually update it each time Signed-off-by: Whit Waldo --- src/Dapr.TestContainers/Common/Options/DaprRuntimeOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Dapr.TestContainers/Common/Options/DaprRuntimeOptions.cs b/src/Dapr.TestContainers/Common/Options/DaprRuntimeOptions.cs index c64f0d246..d5c60625a 100644 --- a/src/Dapr.TestContainers/Common/Options/DaprRuntimeOptions.cs +++ b/src/Dapr.TestContainers/Common/Options/DaprRuntimeOptions.cs @@ -19,7 +19,7 @@ namespace Dapr.TestContainers.Common.Options; /// The various options used to spin up the Dapr containers. /// /// The version of the Dapr images to use. -public sealed record DaprRuntimeOptions(string Version = "1.16.4") +public sealed record DaprRuntimeOptions(string Version = "latest") { /// /// The application's port. From 6d41ffe0c2eee6bd63bc690bcb03c23e5fb531b8 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 22 Dec 2025 23:55:35 -0600 Subject: [PATCH 21/32] Setting environment variable endpoint instead of port Signed-off-by: Whit Waldo --- src/Dapr.TestContainers/Harnesses/BaseHarness.cs | 5 +++-- src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Dapr.TestContainers/Harnesses/BaseHarness.cs b/src/Dapr.TestContainers/Harnesses/BaseHarness.cs index 112c11623..56d076b4b 100644 --- a/src/Dapr.TestContainers/Harnesses/BaseHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/BaseHarness.cs @@ -112,9 +112,10 @@ public async Task InitializeAsync(CancellationToken cancellationToken = default) // containers via environment variables. // This ensures that when the test body creates a DaprClient, it finds the right ports. if (DaprHttpPort > 0) - Environment.SetEnvironmentVariable("DAPR_HTTP_PORT", DaprHttpPort.ToString()); + Environment.SetEnvironmentVariable("DAPR_HTTP_ENDPOINT", $"http://host.docker.internal:{DaprHttpPort}"); + if (DaprGrpcPort > 0) - Environment.SetEnvironmentVariable("DAPR_GRPC_PORT", DaprGrpcPort.ToString()); + Environment.SetEnvironmentVariable("DAPR_GRPC_ENDPOINT", $"http://host.docker.internal:{DaprGrpcPort}"); } /// diff --git a/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs b/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs index 8f55ccea7..9da1e9c0f 100644 --- a/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs @@ -41,7 +41,6 @@ public WorkflowHarness(string componentsDir, Func? startApp, DaprRun _scheduler = new DaprSchedulerContainer(options, Network); } - /// protected override async Task OnInitializeAsync(CancellationToken cancellationToken) { From a2f13b727b413bd71fc8679c9a047afc09d51987 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 22 Dec 2025 23:56:01 -0600 Subject: [PATCH 22/32] Changed the logged message as the one there only shows up for CLI launches, not when using Docker Signed-off-by: Whit Waldo --- src/Dapr.TestContainers/Containers/Dapr/DaprdContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Dapr.TestContainers/Containers/Dapr/DaprdContainer.cs b/src/Dapr.TestContainers/Containers/Dapr/DaprdContainer.cs index ed271112b..690534ae1 100644 --- a/src/Dapr.TestContainers/Containers/Dapr/DaprdContainer.cs +++ b/src/Dapr.TestContainers/Containers/Dapr/DaprdContainer.cs @@ -103,7 +103,7 @@ public DaprdContainer(string appId, string componentsHostFolder, DaprRuntimeOpti .WithPortBinding(InternalGrpcPort, assignRandomHostPort: true) .WithBindMount(componentsHostFolder, componentsPath, AccessMode.ReadOnly) .WithWaitStrategy(Wait.ForUnixContainer() - .UntilMessageIsLogged("Dapr sidecar is up and running.")) + .UntilMessageIsLogged(@"^dapr initialized. Status: Running. Init Elapsed ")) .Build(); } From 1f23ab5272b4829b80243f31ec0e9340610aeca6 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 23 Dec 2025 17:56:52 -0600 Subject: [PATCH 23/32] Applied several changes to remedy errors in TestContainer implementation Signed-off-by: Whit Waldo --- .../Containers/Dapr/DaprPlacementContainer.cs | 10 ++- .../Containers/Dapr/DaprSchedulerContainer.cs | 9 ++- .../Containers/Dapr/DaprdContainer.cs | 25 +++++--- .../Harnesses/ActorHarness.cs | 4 +- .../Harnesses/BaseHarness.cs | 63 ++++++++++++------- .../Harnesses/JobsHarness.cs | 4 +- .../Harnesses/WorkflowHarness.cs | 4 +- 7 files changed, 77 insertions(+), 42 deletions(-) diff --git a/src/Dapr.TestContainers/Containers/Dapr/DaprPlacementContainer.cs b/src/Dapr.TestContainers/Containers/Dapr/DaprPlacementContainer.cs index 96da7b9f7..15447fce4 100644 --- a/src/Dapr.TestContainers/Containers/Dapr/DaprPlacementContainer.cs +++ b/src/Dapr.TestContainers/Containers/Dapr/DaprPlacementContainer.cs @@ -28,7 +28,7 @@ namespace Dapr.TestContainers.Containers.Dapr; public sealed class DaprPlacementContainer : IAsyncStartable { private readonly IContainer _container; - private const int InternalPort = 50006; + private string _containerName = $"placement-{Guid.NewGuid():N}"; /// @@ -42,7 +42,11 @@ public sealed class DaprPlacementContainer : IAsyncStartable /// /// The container's external port. /// - public int Port { get; private set; } + public int ExternalPort { get; private set; } + /// + /// THe contains' internal port. + /// + public const int InternalPort = 50006; /// /// Initializes a new instance of the . @@ -66,7 +70,7 @@ public DaprPlacementContainer(DaprRuntimeOptions options, INetwork network) public async Task StartAsync(CancellationToken cancellationToken = default) { await _container.StartAsync(cancellationToken); - Port = _container.GetMappedPublicPort(InternalPort); + ExternalPort = _container.GetMappedPublicPort(InternalPort); } /// diff --git a/src/Dapr.TestContainers/Containers/Dapr/DaprSchedulerContainer.cs b/src/Dapr.TestContainers/Containers/Dapr/DaprSchedulerContainer.cs index af95893d8..99c58fc5d 100644 --- a/src/Dapr.TestContainers/Containers/Dapr/DaprSchedulerContainer.cs +++ b/src/Dapr.TestContainers/Containers/Dapr/DaprSchedulerContainer.cs @@ -30,7 +30,6 @@ namespace Dapr.TestContainers.Containers.Dapr; /// public sealed class DaprSchedulerContainer : IAsyncStartable { - private const int InternalPort = 51005; private readonly IContainer _container; // Contains the data directory used by this instance of the Dapr scheduler service private string _hostDataDir = Path.Combine(Path.GetTempPath(), $"dapr-scheduler-{Guid.NewGuid():N}"); @@ -47,7 +46,11 @@ public sealed class DaprSchedulerContainer : IAsyncStartable /// /// The container's external port. /// - public int Port { get; private set; } + public int ExternalPort { get; private set; } + /// + /// The container's internal port. + /// + public const int InternalPort = 51005; /// /// Creates a new instance of a . @@ -82,7 +85,7 @@ public DaprSchedulerContainer(DaprRuntimeOptions options, INetwork network) public async Task StartAsync(CancellationToken cancellationToken = default) { await _container.StartAsync(cancellationToken); - Port = _container.GetMappedPublicPort(InternalPort); + ExternalPort = _container.GetMappedPublicPort(InternalPort); // Empty dirs with 0777 inside container: await _container.ExecAsync(["sh", "-c", "mkdir -p ./default-dapr-scheduler-server-0/dapr-0.1 && chmod 0777 ./default-dapr-scheduler-server-0/dapr-0.1" diff --git a/src/Dapr.TestContainers/Containers/Dapr/DaprdContainer.cs b/src/Dapr.TestContainers/Containers/Dapr/DaprdContainer.cs index 690534ae1..3ad4a4faa 100644 --- a/src/Dapr.TestContainers/Containers/Dapr/DaprdContainer.cs +++ b/src/Dapr.TestContainers/Containers/Dapr/DaprdContainer.cs @@ -49,16 +49,21 @@ public sealed class DaprdContainer : IAsyncStartable /// public int GrpcPort { get; private set; } + /// + /// The hostname to locate the Dapr runtime on in the shared Docker network. + /// + public const string ContainerHostAlias = "host.docker.internal"; + /// /// Used to initialize a new instance of a .. /// /// The ID of the app to initialize daprd with. /// The path to the Dapr resources directory. /// The Dapr runtime options. - /// The shared Docker network to connect to. + /// The shared Docker network to connect to. /// The hostname and port of the Placement service. /// The hostname and port of the Scheduler service. - public DaprdContainer(string appId, string componentsHostFolder, DaprRuntimeOptions options, INetwork netowrk, HostPortPair? placementHostAndPort = null, HostPortPair? schedulerHostAndPort = null) + public DaprdContainer(string appId, string componentsHostFolder, DaprRuntimeOptions options, INetwork network, HostPortPair? placementHostAndPort = null, HostPortPair? schedulerHostAndPort = null) { const string componentsPath = "/components"; var cmd = @@ -91,19 +96,26 @@ public DaprdContainer(string appId, string componentsHostFolder, DaprRuntimeOpti cmd.Add("-scheduler-host-address"); cmd.Add(schedulerHostAndPort.ToString()); } + else + { + // Explicitly disable scheduler if not provider + cmd.Add("-scheduler-host-address"); + cmd.Add(""); + } _container = new ContainerBuilder() .WithImage(options.RuntimeImageTag) .WithName(_containerName) .WithLogger(ConsoleLogger.Instance) .WithCommand(cmd.ToArray()) - .WithNetwork(netowrk) - .WithExtraHost("host.docker.internal", "host-gateway") + .WithNetwork(network) + .WithExtraHost(ContainerHostAlias, "host-gateway") .WithPortBinding(InternalHttpPort, assignRandomHostPort: true) .WithPortBinding(InternalGrpcPort, assignRandomHostPort: true) .WithBindMount(componentsHostFolder, componentsPath, AccessMode.ReadOnly) .WithWaitStrategy(Wait.ForUnixContainer() - .UntilMessageIsLogged(@"^dapr initialized. Status: Running. Init Elapsed ")) + .UntilMessageIsLogged("Internal gRPC server is running")) + //.UntilMessageIsLogged(@"^dapr initialized. Status: Running. Init Elapsed ")) .Build(); } @@ -113,9 +125,6 @@ public async Task StartAsync(CancellationToken cancellationToken = default) await _container.StartAsync(cancellationToken); HttpPort = _container.GetMappedPublicPort(InternalHttpPort); GrpcPort = _container.GetMappedPublicPort(InternalGrpcPort); - - Environment.SetEnvironmentVariable("DAPR_HTTP_PORT", HttpPort.ToString()); - Environment.SetEnvironmentVariable("DAPR_GRPC_PORT", GrpcPort.ToString()); } /// diff --git a/src/Dapr.TestContainers/Harnesses/ActorHarness.cs b/src/Dapr.TestContainers/Harnesses/ActorHarness.cs index 98601de1a..3b834c75b 100644 --- a/src/Dapr.TestContainers/Harnesses/ActorHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/ActorHarness.cs @@ -54,8 +54,8 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo // Emit component YAMLs pointing to Redis RedisContainer.Yaml.WriteStateStoreYamlToFolder(componentsDir, redisHost: $"{_redis.NetworkAlias}:{_redis.Port}"); - DaprPlacementPort = _placement.Port; - DaprSchedulerPort = _schedueler.Port; + DaprPlacementExternalPort = _placement.ExternalPort; + DaprSchedulerExternalPort = _schedueler.ExternalPort; } /// diff --git a/src/Dapr.TestContainers/Harnesses/BaseHarness.cs b/src/Dapr.TestContainers/Harnesses/BaseHarness.cs index 56d076b4b..0b62ebb62 100644 --- a/src/Dapr.TestContainers/Harnesses/BaseHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/BaseHarness.cs @@ -12,7 +12,6 @@ // ------------------------------------------------------------------------ using System; -using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -33,6 +32,8 @@ public abstract class BaseHarness(string componentsDirectory, Func? s /// The Daprd container exposed by the harness. /// private protected DaprdContainer? _daprd; + + private readonly TaskCompletionSource _sidecarPortsReady = new(TaskCreationOptions.RunContinuationsAsynchronously); /// /// A shared Docker network that's safer for CI environments. @@ -42,18 +43,28 @@ public abstract class BaseHarness(string componentsDirectory, Func? s /// /// Gets the port that the Dapr sidecar is configured to talk to - this is the port the test application should use. /// - public int AppPort { get; private protected set; } = PortUtilities.GetAvailablePort(); + public int AppPort { get; } = PortUtilities.GetAvailablePort(); /// /// The HTTP port used by the Daprd container. /// public int DaprHttpPort => _daprd?.HttpPort ?? 0; + + /// + /// The HTTP endpoint used by the Daprd container. + /// + public string DaprHttpEndpoint => $"http://{DaprdContainer.ContainerHostAlias}:{DaprHttpPort}"; /// /// The gRPC port used by the Daprd container. /// public int DaprGrpcPort => _daprd?.GrpcPort ?? 0; + /// + /// The gRPC endpoint used by the Daprd container. + /// + public string DaprGrpcEndpoint => $"http://{DaprdContainer.ContainerHostAlias}:{DaprGrpcPort}"; + /// /// The Dapr components directory. /// @@ -62,12 +73,22 @@ public abstract class BaseHarness(string componentsDirectory, Func? s /// /// The port of the Dapr placement service, if started. /// - protected int? DaprPlacementPort { get; set; } + protected int? DaprPlacementExternalPort { get; set; } + + /// + /// The network alias of the placement container, if started. + /// + protected string? DaprPlacementAlias { get; set; } /// /// The port of the Dapr scheduler service, if started. /// - protected int? DaprSchedulerPort { get; set; } + protected int? DaprSchedulerExternalPort { get; set; } + + /// + /// The network alias of the scheduler container, if started. + /// + protected string? DaprSchedulerAlias { get; set; } /// /// The specific container startup logic for the harness. @@ -91,31 +112,27 @@ public async Task InitializeAsync(CancellationToken cancellationToken = default) componentsHostFolder: ComponentsDirectory, options: options with {AppPort = this.AppPort}, Network, - DaprPlacementPort is null ? null : new HostPortPair("host.docker.internal", DaprPlacementPort.Value), - DaprSchedulerPort is null ? null : new HostPortPair("host.docker.internal", DaprSchedulerPort.Value)); - - // Create a list of tasks to run concurrently - namely, start daprd and the startApp, if specified - var tasks = new List + DaprPlacementExternalPort is null || DaprPlacementAlias is null ? null : new HostPortPair(DaprPlacementAlias, DaprPlacementContainer.InternalPort), + DaprSchedulerExternalPort is null || DaprSchedulerAlias is null ? null : new HostPortPair(DaprSchedulerAlias, DaprSchedulerContainer.InternalPort)); + + var daprdTask = Task.Run(async () => { - _daprd.StartAsync(cancellationToken) - }; + await _daprd!.StartAsync(cancellationToken); + _sidecarPortsReady.TrySetResult(); + }, cancellationToken); + + Task? appTask = null; if (startApp is not null) { - tasks.Add(startApp(this.AppPort)); + appTask = Task.Run(async () => + { + await _sidecarPortsReady.Task.WaitAsync(cancellationToken); + await startApp(AppPort); + }, cancellationToken); } - // Wait for both to start - await Task.WhenAll(tasks); - - // Now that _daprd is populated and ports are assigned, automatically link the Dapr .NET SDK to these - // containers via environment variables. - // This ensures that when the test body creates a DaprClient, it finds the right ports. - if (DaprHttpPort > 0) - Environment.SetEnvironmentVariable("DAPR_HTTP_ENDPOINT", $"http://host.docker.internal:{DaprHttpPort}"); - - if (DaprGrpcPort > 0) - Environment.SetEnvironmentVariable("DAPR_GRPC_ENDPOINT", $"http://host.docker.internal:{DaprGrpcPort}"); + await Task.WhenAll(daprdTask, appTask ?? Task.CompletedTask); } /// diff --git a/src/Dapr.TestContainers/Harnesses/JobsHarness.cs b/src/Dapr.TestContainers/Harnesses/JobsHarness.cs index 22fdaf450..8d1513a98 100644 --- a/src/Dapr.TestContainers/Harnesses/JobsHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/JobsHarness.cs @@ -44,7 +44,9 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo { // Start the infrastructure await _scheduler.StartAsync(cancellationToken); - } + DaprSchedulerExternalPort = _scheduler.ExternalPort; + DaprSchedulerAlias = _scheduler.NetworkAlias; + } /// public override async ValueTask DisposeAsync() diff --git a/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs b/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs index 9da1e9c0f..1d2c4a9b6 100644 --- a/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs @@ -53,8 +53,8 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo RedisContainer.Yaml.WriteStateStoreYamlToFolder(ComponentsDirectory, redisHost: $"{_redis.NetworkAlias}:6379"); // Set the service ports - this.DaprPlacementPort = _placement.Port; - this.DaprSchedulerPort = _scheduler.Port; + this.DaprPlacementExternalPort = _placement.ExternalPort; + this.DaprSchedulerExternalPort = _scheduler.ExternalPort; } /// From 5dadf4e077f8176886a294d0fc89596da8cef106 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 23 Dec 2025 17:57:32 -0600 Subject: [PATCH 24/32] Added more projects for E2E testing, including Jobs, PubSub and Workflow. Starting with Jobs tests. Signed-off-by: Whit Waldo --- all.sln | 21 ++++ .../Dapr.E2E.Test.Jobs.csproj | 27 ++++++ test/Dapr.E2E.Test.Jobs/JobsTests.cs | 96 +++++++++++++++++++ .../Dapr.E2E.Test.PubSub.csproj | 27 ++++++ .../Dapr.E2E.Test.Workflow.csproj | 27 ++++++ 5 files changed, 198 insertions(+) create mode 100644 test/Dapr.E2E.Test.Jobs/Dapr.E2E.Test.Jobs.csproj create mode 100644 test/Dapr.E2E.Test.Jobs/JobsTests.cs create mode 100644 test/Dapr.E2E.Test.PubSub/Dapr.E2E.Test.PubSub.csproj create mode 100644 test/Dapr.E2E.Test.Workflow/Dapr.E2E.Test.Workflow.csproj diff --git a/all.sln b/all.sln index 970eb892e..0da90b211 100644 --- a/all.sln +++ b/all.sln @@ -207,6 +207,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DistributedLock", "examples EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.TestContainers", "src\Dapr.TestContainers\Dapr.TestContainers.csproj", "{D14893E1-EF21-4EB4-9DAD-82B5127832CB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.E2E.Test.PubSub", "test\Dapr.E2E.Test.PubSub\Dapr.E2E.Test.PubSub.csproj", "{4997B2D1-49AF-40B7-8D84-4BD1E94D9B2E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.E2E.Test.Workflow", "test\Dapr.E2E.Test.Workflow\Dapr.E2E.Test.Workflow.csproj", "{3CE069E8-9AAB-4DE4-90AC-077C1DB4EAEB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.E2E.Test.Jobs", "test\Dapr.E2E.Test.Jobs\Dapr.E2E.Test.Jobs.csproj", "{775302E3-69CC-4FBD-98CC-0F889A5D4C63}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -547,6 +553,18 @@ Global {D14893E1-EF21-4EB4-9DAD-82B5127832CB}.Debug|Any CPU.Build.0 = Debug|Any CPU {D14893E1-EF21-4EB4-9DAD-82B5127832CB}.Release|Any CPU.ActiveCfg = Release|Any CPU {D14893E1-EF21-4EB4-9DAD-82B5127832CB}.Release|Any CPU.Build.0 = Release|Any CPU + {4997B2D1-49AF-40B7-8D84-4BD1E94D9B2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4997B2D1-49AF-40B7-8D84-4BD1E94D9B2E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4997B2D1-49AF-40B7-8D84-4BD1E94D9B2E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4997B2D1-49AF-40B7-8D84-4BD1E94D9B2E}.Release|Any CPU.Build.0 = Release|Any CPU + {3CE069E8-9AAB-4DE4-90AC-077C1DB4EAEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3CE069E8-9AAB-4DE4-90AC-077C1DB4EAEB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3CE069E8-9AAB-4DE4-90AC-077C1DB4EAEB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3CE069E8-9AAB-4DE4-90AC-077C1DB4EAEB}.Release|Any CPU.Build.0 = Release|Any CPU + {775302E3-69CC-4FBD-98CC-0F889A5D4C63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {775302E3-69CC-4FBD-98CC-0F889A5D4C63}.Debug|Any CPU.Build.0 = Debug|Any CPU + {775302E3-69CC-4FBD-98CC-0F889A5D4C63}.Release|Any CPU.ActiveCfg = Release|Any CPU + {775302E3-69CC-4FBD-98CC-0F889A5D4C63}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -647,6 +665,9 @@ Global {11D2CA0F-6D38-4DC7-AE06-C1DAE7FC1C20} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} {F2DFB0FE-DF35-4D94-9CC9-43212B1D6F93} = {11D2CA0F-6D38-4DC7-AE06-C1DAE7FC1C20} {D14893E1-EF21-4EB4-9DAD-82B5127832CB} = {27C5D71D-0721-4221-9286-B94AB07B58CF} + {4997B2D1-49AF-40B7-8D84-4BD1E94D9B2E} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {3CE069E8-9AAB-4DE4-90AC-077C1DB4EAEB} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {775302E3-69CC-4FBD-98CC-0F889A5D4C63} = {DD020B34-460F-455F-8D17-CF4A949F100B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} diff --git a/test/Dapr.E2E.Test.Jobs/Dapr.E2E.Test.Jobs.csproj b/test/Dapr.E2E.Test.Jobs/Dapr.E2E.Test.Jobs.csproj new file mode 100644 index 000000000..bcbe137eb --- /dev/null +++ b/test/Dapr.E2E.Test.Jobs/Dapr.E2E.Test.Jobs.csproj @@ -0,0 +1,27 @@ + + + + enable + enable + false + + + + + + + + + + + + + + + + + + + + + diff --git a/test/Dapr.E2E.Test.Jobs/JobsTests.cs b/test/Dapr.E2E.Test.Jobs/JobsTests.cs new file mode 100644 index 000000000..10c3723b8 --- /dev/null +++ b/test/Dapr.E2E.Test.Jobs/JobsTests.cs @@ -0,0 +1,96 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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. +// ------------------------------------------------------------------------ + +using System.Text; +using Dapr.Jobs; +using Dapr.Jobs.Extensions; +using Dapr.Jobs.Models; +using Dapr.TestContainers.Common; +using Dapr.TestContainers.Common.Options; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Dapr.E2E.Test.Jobs; + +public sealed class JobsTests +{ + [Fact] + public async Task ShouldScheduleAndExecuteJob() + { + var options = new DaprRuntimeOptions(); + var componentsDir = Path.Combine(Directory.GetCurrentDirectory(), $"jobs-components-{Guid.NewGuid():N}"); + var jobName = $"e2e-job-{Guid.NewGuid():N}"; + + WebApplication? app = null; + var invocationTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + // Build and initialize the harness + var harnessBuilder = new DaprHarnessBuilder(options); + var harness = harnessBuilder.BuildJobs(componentsDir); + + try + { + await harness.InitializeAsync(); + + Environment.SetEnvironmentVariable("DAPR_HTTP_ENDPOINT", $"http://127.0.0.1:{harness.DaprHttpPort}"); + Environment.SetEnvironmentVariable("DAPR_GRPC_ENDPOINT", $"http://127.0.0.1:{harness.DaprGrpcPort}"); + + var builder = WebApplication.CreateBuilder(); + builder.Logging.ClearProviders(); + builder.Logging.AddSimpleConsole(); + builder.WebHost.UseUrls($"http://0.0.0.0:{harness.AppPort}"); + // builder.Configuration.AddInMemoryCollection(new List> + // { + // new("DAPR_HTTP_ENDPOINT", $"http://127.0.0.1:{harness.DaprHttpPort}"), + // new("DAPR_GRPC_ENDPOINT", $"http://127.0.0.1:{harness.DaprGrpcPort}") + // }); + + builder.Services.AddDaprJobsClient(); + builder.Services.AddLogging(); + + app = builder.Build(); + + app.MapDaprScheduledJobHandler(async (string incomingJobName, ReadOnlyMemory payload, ILogger? logger, CancellationToken ct) => + { + logger?.LogInformation("Received job {Job}", incomingJobName); + invocationTcs.TrySetResult(Encoding.UTF8.GetString(payload.Span)); + await Task.CompletedTask; + }); + + await app.StartAsync(); + + await using var scope = app!.Services.CreateAsyncScope(); + var daprJobsClient = scope.ServiceProvider.GetRequiredService(); + + var payload = "Hello!"u8.ToArray(); + await daprJobsClient.ScheduleJobAsync(jobName, DaprJobSchedule.FromDuration(TimeSpan.FromSeconds(2)), + payload, repeats: 1, overwrite: true); + + // Wait for the handler to confirm execution + var received = await invocationTcs.Task.WaitAsync(TimeSpan.FromSeconds(30)); + Assert.Equal(Encoding.UTF8.GetString(payload), received); + } + finally + { + // Clean up the environment variables + Environment.SetEnvironmentVariable("DAPR_HTTP_ENDPOINT", null); + Environment.SetEnvironmentVariable("DAPR_GRPC_ENDPOINT", null); + + await harness.DisposeAsync(); + if (app is not null) + await app.DisposeAsync(); + } + } +} diff --git a/test/Dapr.E2E.Test.PubSub/Dapr.E2E.Test.PubSub.csproj b/test/Dapr.E2E.Test.PubSub/Dapr.E2E.Test.PubSub.csproj new file mode 100644 index 000000000..60d871640 --- /dev/null +++ b/test/Dapr.E2E.Test.PubSub/Dapr.E2E.Test.PubSub.csproj @@ -0,0 +1,27 @@ + + + + enable + enable + false + + + + + + + + + + + + + + + + + + + + + diff --git a/test/Dapr.E2E.Test.Workflow/Dapr.E2E.Test.Workflow.csproj b/test/Dapr.E2E.Test.Workflow/Dapr.E2E.Test.Workflow.csproj new file mode 100644 index 000000000..f03795c89 --- /dev/null +++ b/test/Dapr.E2E.Test.Workflow/Dapr.E2E.Test.Workflow.csproj @@ -0,0 +1,27 @@ + + + + net9.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + From 7cc41bd5d0c3ead638b9e3bc28dd24a77a6e8ccb Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 23 Dec 2025 18:15:30 -0600 Subject: [PATCH 25/32] Simplified harness implementation to remove some redunant cleanup Signed-off-by: Whit Waldo --- src/Dapr.TestContainers/Harnesses/ActorHarness.cs | 7 +------ src/Dapr.TestContainers/Harnesses/BaseHarness.cs | 7 +++++++ .../Harnesses/ConversationHarness.cs | 7 +------ .../Harnesses/CryptographyHarness.cs | 10 ---------- .../Harnesses/DistributedLockHarness.cs | 7 +------ src/Dapr.TestContainers/Harnesses/JobsHarness.cs | 11 +++-------- src/Dapr.TestContainers/Harnesses/PubSubHarness.cs | 7 +------ .../Harnesses/StateManagementHarness.cs | 9 ++------- src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs | 6 ++---- 9 files changed, 18 insertions(+), 53 deletions(-) diff --git a/src/Dapr.TestContainers/Harnesses/ActorHarness.cs b/src/Dapr.TestContainers/Harnesses/ActorHarness.cs index 3b834c75b..aa26efc79 100644 --- a/src/Dapr.TestContainers/Harnesses/ActorHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/ActorHarness.cs @@ -59,15 +59,10 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo } /// - public override async ValueTask DisposeAsync() + protected override async ValueTask OnDisposeAsync() { - if (_daprd is not null) - await _daprd.DisposeAsync(); await _redis.DisposeAsync(); await _placement.DisposeAsync(); await _schedueler.DisposeAsync(); - - // Cleanup the generated YAML files - CleanupComponents(componentsDir); } } diff --git a/src/Dapr.TestContainers/Harnesses/BaseHarness.cs b/src/Dapr.TestContainers/Harnesses/BaseHarness.cs index 0b62ebb62..3242500ea 100644 --- a/src/Dapr.TestContainers/Harnesses/BaseHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/BaseHarness.cs @@ -140,6 +140,8 @@ public async Task InitializeAsync(CancellationToken cancellationToken = default) /// public virtual async ValueTask DisposeAsync() { + await OnDisposeAsync(); + if (_daprd is not null) await _daprd.DisposeAsync(); await Network.DisposeAsync(); @@ -150,6 +152,11 @@ public virtual async ValueTask DisposeAsync() GC.SuppressFinalize(this); } + /// + /// Override this to dispose harness-specific resources before base cleanup. + /// + protected virtual ValueTask OnDisposeAsync() => ValueTask.CompletedTask; + /// /// Deletes the specified directory recursively as part of a clean-up operation. /// diff --git a/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs b/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs index 3564542f5..56c001991 100644 --- a/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs @@ -50,13 +50,8 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo } /// - public override async ValueTask DisposeAsync() + protected override async ValueTask OnDisposeAsync() { - if (_daprd is not null) - await _daprd.DisposeAsync(); await _ollama.DisposeAsync(); - - // Cleanup the generated YAML files - CleanupComponents(componentsDir); } } diff --git a/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs b/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs index bb6d4ee51..9bd9a78a8 100644 --- a/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs @@ -48,14 +48,4 @@ protected override Task OnInitializeAsync(CancellationToken cancellationToken) return Task.CompletedTask; } - - /// - public override async ValueTask DisposeAsync() - { - if (_daprd is not null) - await _daprd.DisposeAsync(); - - // Cleanup the generated YAML files - CleanupComponents(componentsDir); - } } diff --git a/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs b/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs index a3010cf04..14c9d1362 100644 --- a/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs @@ -49,13 +49,8 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo } /// - public override async ValueTask DisposeAsync() + protected override async ValueTask OnDisposeAsync() { - if (_daprd is not null) - await _daprd.DisposeAsync(); await _redis.DisposeAsync(); - - // Cleanup the generated YAML files - CleanupComponents(componentsDir); } } diff --git a/src/Dapr.TestContainers/Harnesses/JobsHarness.cs b/src/Dapr.TestContainers/Harnesses/JobsHarness.cs index 8d1513a98..7dbc32a7c 100644 --- a/src/Dapr.TestContainers/Harnesses/JobsHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/JobsHarness.cs @@ -49,13 +49,8 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo } /// - public override async ValueTask DisposeAsync() - { - if (_daprd is not null) - await _daprd.DisposeAsync(); - await _scheduler.DisposeAsync(); - - // Cleanup the generated YAML files - CleanupComponents(componentsDir); + protected override async ValueTask OnDisposeAsync() + { + await _scheduler.DisposeAsync(); } } diff --git a/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs b/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs index 32562cb91..8cb8ae328 100644 --- a/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs @@ -49,13 +49,8 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo } /// - public override async ValueTask DisposeAsync() + protected override async ValueTask OnDisposeAsync() { - if (_daprd is not null) - await _daprd.DisposeAsync(); await _rabbitmq.DisposeAsync(); - - // Clean up the generated YAML files - CleanupComponents(componentsDir); } } diff --git a/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs b/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs index 7903e1ca1..800ee4220 100644 --- a/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs @@ -49,13 +49,8 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo } /// - public override async ValueTask DisposeAsync() + protected override async ValueTask OnDisposeAsync() { - if (_daprd is not null) - await _daprd.DisposeAsync(); - await _redis.DisposeAsync(); - - // Cleanup the generated YAML files - CleanupComponents(componentsDir); + await _redis.DisposeAsync(); } } diff --git a/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs b/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs index 1d2c4a9b6..c023cea33 100644 --- a/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs @@ -58,12 +58,10 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo } /// - public override async ValueTask DisposeAsync() + protected override async ValueTask OnDisposeAsync() { - if (_daprd is not null) - await _daprd.DisposeAsync(); await _placement.DisposeAsync(); await _scheduler.DisposeAsync(); await _redis.DisposeAsync(); - } + } } From 666cf1dedca897888f2db5e729c39b6a4709f5a2 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 23 Dec 2025 18:47:33 -0600 Subject: [PATCH 26/32] Simplified how E2E tests are built against fluent helper for app setup and configuration Signed-off-by: Whit Waldo --- .../Common/DaprHarnessBuilder.cs | 6 ++ .../Common/Testing/DaprTestApplication.cs | 64 +++++++++++++++ .../Testing/DaprTestApplicationBuilder.cs | 80 ++++++++++++++++++ .../Dapr.TestContainers.csproj | 1 + test/Dapr.E2E.Test.Jobs/JobsTests.cs | 81 ++++++------------- 5 files changed, 176 insertions(+), 56 deletions(-) create mode 100644 src/Dapr.TestContainers/Common/Testing/DaprTestApplication.cs create mode 100644 src/Dapr.TestContainers/Common/Testing/DaprTestApplicationBuilder.cs diff --git a/src/Dapr.TestContainers/Common/DaprHarnessBuilder.cs b/src/Dapr.TestContainers/Common/DaprHarnessBuilder.cs index b2ce94dd1..70546a84f 100644 --- a/src/Dapr.TestContainers/Common/DaprHarnessBuilder.cs +++ b/src/Dapr.TestContainers/Common/DaprHarnessBuilder.cs @@ -13,6 +13,7 @@ using System; using System.Threading.Tasks; +using Dapr.E2E.Test.Common; using Dapr.TestContainers.Common.Options; using Dapr.TestContainers.Harnesses; @@ -74,4 +75,9 @@ public CryptographyHarness BuildCryptography(string componentsDir, string keysDi /// /// The path to the Dapr resources. public ActorHarness BuildActors(string componentsDir) => new(componentsDir, startApp, options); + + /// + /// Creates a test application builder for the specified harness. + /// + public static DaprTestApplicationBuilder ForHarness(BaseHarness harness) => new(harness); } diff --git a/src/Dapr.TestContainers/Common/Testing/DaprTestApplication.cs b/src/Dapr.TestContainers/Common/Testing/DaprTestApplication.cs new file mode 100644 index 000000000..9f41dac73 --- /dev/null +++ b/src/Dapr.TestContainers/Common/Testing/DaprTestApplication.cs @@ -0,0 +1,64 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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. +// ------------------------------------------------------------------------ + +using System; +using System.Threading.Tasks; +using Dapr.TestContainers.Harnesses; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Dapr.TestContainers.Common.Testing; + +/// +/// Represents a running Dapr test application. +/// +public sealed class DaprTestApplication : IAsyncDisposable +{ + private readonly BaseHarness _harness; + private readonly WebApplication? _app; + + internal DaprTestApplication(BaseHarness harness, WebApplication? app) + { + _harness = harness; + _app = app; + } + + /// + /// Gets a service from the application's DI container. + /// + public T GetRequiredService() where T : notnull + { + if (_app is null) + throw new InvalidOperationException("No app configured"); + + return _app.Services.GetRequiredService(); + } + + /// + /// Creates a service scope. + /// + public IServiceScope CreateScope() => + _app?.Services.CreateScope() ?? throw new InvalidOperationException("No app configured"); + + /// + public async ValueTask DisposeAsync() + { + Environment.SetEnvironmentVariable("DAPR_HTTP_ENDPOINT", null); + Environment.SetEnvironmentVariable("DAPR_GRPC_ENDPOINT", null); + + if (_app is not null) + await _app.DisposeAsync(); + + await _harness.DisposeAsync(); + } +} diff --git a/src/Dapr.TestContainers/Common/Testing/DaprTestApplicationBuilder.cs b/src/Dapr.TestContainers/Common/Testing/DaprTestApplicationBuilder.cs new file mode 100644 index 000000000..a176e47b5 --- /dev/null +++ b/src/Dapr.TestContainers/Common/Testing/DaprTestApplicationBuilder.cs @@ -0,0 +1,80 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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. +// ------------------------------------------------------------------------ + +using System; +using System.Threading.Tasks; +using Dapr.TestContainers.Common.Testing; +using Dapr.TestContainers.Harnesses; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Logging; + +namespace Dapr.E2E.Test.Common; + +/// +/// Fluent builder for creating test applications with Dapr harnesses. +/// +public sealed class DaprTestApplicationBuilder(BaseHarness harness) +{ + private Action? _configureServices; + private Action? _configureApp; + + /// + /// Configures services for the test application. + /// + public DaprTestApplicationBuilder ConfigureServices(Action configure) + { + _configureServices = configure; + return this; + } + + /// + /// Configures the test application pipeline. + /// + public DaprTestApplicationBuilder ConfigureApp(Action configure) + { + _configureApp = configure; + return this; + } + + /// + /// Builds and starts the test application and harness. + /// + /// + public async Task BuildAndStartAsync() + { + await harness.InitializeAsync(); + + WebApplication? app = null; + if (_configureServices is not null || _configureApp is not null) + { + // Set environment variables + Environment.SetEnvironmentVariable("DAPR_HTTP_ENDPOINT", $"http://127.0.0.1:{harness.DaprHttpPort}"); + Environment.SetEnvironmentVariable("DAPR_GRPC_ENDPOINT", $"http://127.0.0.1:{harness.DaprGrpcPort}"); + + var builder = WebApplication.CreateBuilder(); + builder.Logging.ClearProviders(); + builder.Logging.AddSimpleConsole(); + builder.WebHost.UseUrls($"http://0.0.0.0:{harness.AppPort}"); + + _configureServices?.Invoke(builder); + + app = builder.Build(); + _configureApp?.Invoke(app); + + await app.StartAsync(); + } + + return new DaprTestApplication(harness, app); + } +} diff --git a/src/Dapr.TestContainers/Dapr.TestContainers.csproj b/src/Dapr.TestContainers/Dapr.TestContainers.csproj index c9adc88b3..96485ea10 100644 --- a/src/Dapr.TestContainers/Dapr.TestContainers.csproj +++ b/src/Dapr.TestContainers/Dapr.TestContainers.csproj @@ -8,6 +8,7 @@ + diff --git a/test/Dapr.E2E.Test.Jobs/JobsTests.cs b/test/Dapr.E2E.Test.Jobs/JobsTests.cs index 10c3723b8..0f5a73ab0 100644 --- a/test/Dapr.E2E.Test.Jobs/JobsTests.cs +++ b/test/Dapr.E2E.Test.Jobs/JobsTests.cs @@ -27,70 +27,39 @@ namespace Dapr.E2E.Test.Jobs; public sealed class JobsTests { [Fact] - public async Task ShouldScheduleAndExecuteJob() + public async Task ShouldScheduleAndReceiveJob() { var options = new DaprRuntimeOptions(); var componentsDir = Path.Combine(Directory.GetCurrentDirectory(), $"jobs-components-{Guid.NewGuid():N}"); var jobName = $"e2e-job-{Guid.NewGuid():N}"; - - WebApplication? app = null; var invocationTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - // Build and initialize the harness - var harnessBuilder = new DaprHarnessBuilder(options); - var harness = harnessBuilder.BuildJobs(componentsDir); - - try - { - await harness.InitializeAsync(); - - Environment.SetEnvironmentVariable("DAPR_HTTP_ENDPOINT", $"http://127.0.0.1:{harness.DaprHttpPort}"); - Environment.SetEnvironmentVariable("DAPR_GRPC_ENDPOINT", $"http://127.0.0.1:{harness.DaprGrpcPort}"); - - var builder = WebApplication.CreateBuilder(); - builder.Logging.ClearProviders(); - builder.Logging.AddSimpleConsole(); - builder.WebHost.UseUrls($"http://0.0.0.0:{harness.AppPort}"); - // builder.Configuration.AddInMemoryCollection(new List> - // { - // new("DAPR_HTTP_ENDPOINT", $"http://127.0.0.1:{harness.DaprHttpPort}"), - // new("DAPR_GRPC_ENDPOINT", $"http://127.0.0.1:{harness.DaprGrpcPort}") - // }); - - builder.Services.AddDaprJobsClient(); - builder.Services.AddLogging(); - - app = builder.Build(); - app.MapDaprScheduledJobHandler(async (string incomingJobName, ReadOnlyMemory payload, ILogger? logger, CancellationToken ct) => + var harness = new DaprHarnessBuilder(options).BuildJobs(componentsDir); + await using var testApp = await DaprHarnessBuilder.ForHarness(harness) + .ConfigureServices(builder => { - logger?.LogInformation("Received job {Job}", incomingJobName); - invocationTcs.TrySetResult(Encoding.UTF8.GetString(payload.Span)); - await Task.CompletedTask; - }); - - await app.StartAsync(); + builder.Services.AddDaprJobsClient(); + }) + .ConfigureApp(app => + { + app.MapDaprScheduledJobHandler((string incomingJobName, ReadOnlyMemory payload, + ILogger? logger, CancellationToken _) => + { + logger?.LogInformation("Received job {Job}", incomingJobName); + invocationTcs.TrySetResult(Encoding.UTF8.GetString(payload.Span)); + }); + }) + .BuildAndStartAsync(); + + // Clean test logic + using var scope = testApp.CreateScope(); + var daprJobsClient = scope.ServiceProvider.GetRequiredService(); - await using var scope = app!.Services.CreateAsyncScope(); - var daprJobsClient = scope.ServiceProvider.GetRequiredService(); + var payload = "Hello!"u8.ToArray(); + await daprJobsClient.ScheduleJobAsync(jobName, DaprJobSchedule.FromDuration(TimeSpan.FromSeconds(2)), + payload, repeats: 1, overwrite: true); - var payload = "Hello!"u8.ToArray(); - await daprJobsClient.ScheduleJobAsync(jobName, DaprJobSchedule.FromDuration(TimeSpan.FromSeconds(2)), - payload, repeats: 1, overwrite: true); - - // Wait for the handler to confirm execution - var received = await invocationTcs.Task.WaitAsync(TimeSpan.FromSeconds(30)); - Assert.Equal(Encoding.UTF8.GetString(payload), received); - } - finally - { - // Clean up the environment variables - Environment.SetEnvironmentVariable("DAPR_HTTP_ENDPOINT", null); - Environment.SetEnvironmentVariable("DAPR_GRPC_ENDPOINT", null); - - await harness.DisposeAsync(); - if (app is not null) - await app.DisposeAsync(); - } + var received = await invocationTcs.Task.WaitAsync(TimeSpan.FromSeconds(30)); + Assert.Equal(Encoding.UTF8.GetString(payload), received); } } From e09a879dfcc341ae1f2552fc43383bed0f5d33c8 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 23 Dec 2025 19:01:18 -0600 Subject: [PATCH 27/32] Tweaks to start using separate networks for each test in preparation to run parallel E2E tests Signed-off-by: Whit Waldo --- .../Common/Testing/DaprTestApplication.cs | 3 --- .../Common/Testing/DaprTestApplicationBuilder.cs | 11 ++++++++++- src/Dapr.TestContainers/Harnesses/ActorHarness.cs | 3 ++- src/Dapr.TestContainers/Harnesses/BaseHarness.cs | 6 ++++-- .../Harnesses/ConversationHarness.cs | 3 ++- .../Harnesses/DistributedLockHarness.cs | 3 ++- src/Dapr.TestContainers/Harnesses/PubSubHarness.cs | 3 ++- .../Harnesses/StateManagementHarness.cs | 3 ++- src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs | 3 ++- 9 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/Dapr.TestContainers/Common/Testing/DaprTestApplication.cs b/src/Dapr.TestContainers/Common/Testing/DaprTestApplication.cs index 9f41dac73..c503e34df 100644 --- a/src/Dapr.TestContainers/Common/Testing/DaprTestApplication.cs +++ b/src/Dapr.TestContainers/Common/Testing/DaprTestApplication.cs @@ -53,9 +53,6 @@ public IServiceScope CreateScope() => /// public async ValueTask DisposeAsync() { - Environment.SetEnvironmentVariable("DAPR_HTTP_ENDPOINT", null); - Environment.SetEnvironmentVariable("DAPR_GRPC_ENDPOINT", null); - if (_app is not null) await _app.DisposeAsync(); diff --git a/src/Dapr.TestContainers/Common/Testing/DaprTestApplicationBuilder.cs b/src/Dapr.TestContainers/Common/Testing/DaprTestApplicationBuilder.cs index a176e47b5..43426276a 100644 --- a/src/Dapr.TestContainers/Common/Testing/DaprTestApplicationBuilder.cs +++ b/src/Dapr.TestContainers/Common/Testing/DaprTestApplicationBuilder.cs @@ -12,11 +12,13 @@ // ------------------------------------------------------------------------ using System; +using System.Collections.Generic; using System.Threading.Tasks; using Dapr.TestContainers.Common.Testing; using Dapr.TestContainers.Harnesses; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace Dapr.E2E.Test.Common; @@ -66,7 +68,14 @@ public async Task BuildAndStartAsync() builder.Logging.ClearProviders(); builder.Logging.AddSimpleConsole(); builder.WebHost.UseUrls($"http://0.0.0.0:{harness.AppPort}"); - + + // Configure Dapr endpoints via in-memory configuration instead of environment variables + builder.Configuration.AddInMemoryCollection(new Dictionary + { + { "DAPR_HTTP_ENDPOINT", $"http://127.0.0.1:{harness.DaprHttpPort}" }, + { "DAPR_GRPC_ENDPOINT", $"http://127.0.0.1:{harness.DaprGrpcPort}" } + }); + _configureServices?.Invoke(builder); app = builder.Build(); diff --git a/src/Dapr.TestContainers/Harnesses/ActorHarness.cs b/src/Dapr.TestContainers/Harnesses/ActorHarness.cs index aa26efc79..3d00f4215 100644 --- a/src/Dapr.TestContainers/Harnesses/ActorHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/ActorHarness.cs @@ -25,7 +25,7 @@ namespace Dapr.TestContainers.Harnesses; /// public sealed class ActorHarness : BaseHarness { - private readonly RedisContainer _redis = new(Network); + private readonly RedisContainer _redis; private readonly DaprPlacementContainer _placement; private readonly DaprSchedulerContainer _schedueler; private readonly string componentsDir; @@ -41,6 +41,7 @@ public ActorHarness(string componentsDir, Func? startApp, DaprRuntime this.componentsDir = componentsDir; _placement = new DaprPlacementContainer(options, Network); _schedueler = new DaprSchedulerContainer(options, Network); + _redis = new(Network); } /// diff --git a/src/Dapr.TestContainers/Harnesses/BaseHarness.cs b/src/Dapr.TestContainers/Harnesses/BaseHarness.cs index 3242500ea..b19589b90 100644 --- a/src/Dapr.TestContainers/Harnesses/BaseHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/BaseHarness.cs @@ -36,9 +36,9 @@ public abstract class BaseHarness(string componentsDirectory, Func? s private readonly TaskCompletionSource _sidecarPortsReady = new(TaskCreationOptions.RunContinuationsAsynchronously); /// - /// A shared Docker network that's safer for CI environments. + /// A shared Docker network that's safer for CI environments - each harness instance gets its own network for isolation. /// - protected static readonly INetwork Network = new NetworkBuilder().Build(); + protected readonly INetwork Network = new NetworkBuilder().Build(); /// /// Gets the port that the Dapr sidecar is configured to talk to - this is the port the test application should use. @@ -144,6 +144,8 @@ public virtual async ValueTask DisposeAsync() if (_daprd is not null) await _daprd.DisposeAsync(); + + // Clean up the per-instance network await Network.DisposeAsync(); // Clean up generated YAML files diff --git a/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs b/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs index 56c001991..d16974e7b 100644 --- a/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs @@ -24,7 +24,7 @@ namespace Dapr.TestContainers.Harnesses; /// public sealed class ConversationHarness : BaseHarness { - private readonly OllamaContainer _ollama = new(Network); + private readonly OllamaContainer _ollama; private readonly string componentsDir; /// @@ -36,6 +36,7 @@ public sealed class ConversationHarness : BaseHarness public ConversationHarness(string componentsDir, Func? startApp, DaprRuntimeOptions options) : base(componentsDir, startApp, options) { this.componentsDir = componentsDir; + _ollama = new(Network); } /// diff --git a/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs b/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs index 14c9d1362..ee4a33cf2 100644 --- a/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs @@ -24,7 +24,7 @@ namespace Dapr.TestContainers.Harnesses; /// public sealed class DistributedLockHarness : BaseHarness { - private readonly RedisContainer _redis = new(Network); + private readonly RedisContainer _redis; private readonly string componentsDir; /// @@ -36,6 +36,7 @@ public sealed class DistributedLockHarness : BaseHarness public DistributedLockHarness(string componentsDir, Func? startApp, DaprRuntimeOptions options) : base(componentsDir, startApp, options) { this.componentsDir = componentsDir; + _redis = new(Network); } /// diff --git a/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs b/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs index 8cb8ae328..9f38cd199 100644 --- a/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs @@ -24,7 +24,7 @@ namespace Dapr.TestContainers.Harnesses; /// public sealed class PubSubHarness : BaseHarness { - private readonly RabbitMqContainer _rabbitmq = new(Network); + private readonly RabbitMqContainer _rabbitmq; private readonly string componentsDir; /// @@ -36,6 +36,7 @@ public sealed class PubSubHarness : BaseHarness public PubSubHarness(string componentsDir, Func? startApp, DaprRuntimeOptions options): base(componentsDir, startApp, options) { this.componentsDir = componentsDir; + _rabbitmq = new(Network); } /// diff --git a/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs b/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs index 800ee4220..6b1dc222c 100644 --- a/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs @@ -24,7 +24,7 @@ namespace Dapr.TestContainers.Harnesses; /// public sealed class StateManagementHarness : BaseHarness { - private readonly RedisContainer _redis = new(Network); + private readonly RedisContainer _redis; private readonly string componentsDir; /// @@ -36,6 +36,7 @@ public sealed class StateManagementHarness : BaseHarness public StateManagementHarness(string componentsDir, Func? startApp, DaprRuntimeOptions options) : base(componentsDir, startApp, options) { this.componentsDir = componentsDir; + _redis = new(Network); } /// diff --git a/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs b/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs index c023cea33..1e17870d2 100644 --- a/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs @@ -25,7 +25,7 @@ namespace Dapr.TestContainers.Harnesses; /// public sealed class WorkflowHarness : BaseHarness { - private readonly RedisContainer _redis = new(Network); + private readonly RedisContainer _redis; private readonly DaprPlacementContainer _placement; private readonly DaprSchedulerContainer _scheduler; @@ -39,6 +39,7 @@ public WorkflowHarness(string componentsDir, Func? startApp, DaprRun { _placement = new DaprPlacementContainer(options, Network); _scheduler = new DaprSchedulerContainer(options, Network); + _redis = new(Network); } /// From 1f0f52a130d4abacbd3762f04f7742d9bc528585 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 23 Dec 2025 19:01:33 -0600 Subject: [PATCH 28/32] Tweaked jobs test to also validate job name Signed-off-by: Whit Waldo --- test/Dapr.E2E.Test.Jobs/JobsTests.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/test/Dapr.E2E.Test.Jobs/JobsTests.cs b/test/Dapr.E2E.Test.Jobs/JobsTests.cs index 0f5a73ab0..1cab24279 100644 --- a/test/Dapr.E2E.Test.Jobs/JobsTests.cs +++ b/test/Dapr.E2E.Test.Jobs/JobsTests.cs @@ -17,8 +17,6 @@ using Dapr.Jobs.Models; using Dapr.TestContainers.Common; using Dapr.TestContainers.Common.Options; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -32,7 +30,7 @@ public async Task ShouldScheduleAndReceiveJob() var options = new DaprRuntimeOptions(); var componentsDir = Path.Combine(Directory.GetCurrentDirectory(), $"jobs-components-{Guid.NewGuid():N}"); var jobName = $"e2e-job-{Guid.NewGuid():N}"; - var invocationTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var invocationTcs = new TaskCompletionSource<(string payload, string jobName)>(TaskCreationOptions.RunContinuationsAsynchronously); var harness = new DaprHarnessBuilder(options).BuildJobs(componentsDir); await using var testApp = await DaprHarnessBuilder.ForHarness(harness) @@ -46,7 +44,7 @@ public async Task ShouldScheduleAndReceiveJob() ILogger? logger, CancellationToken _) => { logger?.LogInformation("Received job {Job}", incomingJobName); - invocationTcs.TrySetResult(Encoding.UTF8.GetString(payload.Span)); + invocationTcs.TrySetResult((Encoding.UTF8.GetString(payload.Span), incomingJobName)); }); }) .BuildAndStartAsync(); @@ -60,6 +58,7 @@ await daprJobsClient.ScheduleJobAsync(jobName, DaprJobSchedule.FromDuration(Time payload, repeats: 1, overwrite: true); var received = await invocationTcs.Task.WaitAsync(TimeSpan.FromSeconds(30)); - Assert.Equal(Encoding.UTF8.GetString(payload), received); + Assert.Equal(Encoding.UTF8.GetString(payload), received.payload); + Assert.Equal(jobName, received.jobName); } } From d7e6f0761879bc2e1e1a8f9a6cb9593c8399bfcc Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 23 Dec 2025 19:20:59 -0600 Subject: [PATCH 29/32] Made some tweaks to ensure that the app is connecting to Dapr instances on the internal Docker network instead of through its external ports ensuring test isolation for parallel testing Signed-off-by: Whit Waldo --- .../Common/Testing/DaprTestApplicationBuilder.cs | 15 ++++++++------- .../Containers/OllamaContainer.cs | 4 ++++ .../Containers/RabbitMqContainer.cs | 4 ++++ .../Containers/RedisContainer.cs | 4 ++++ src/Dapr.TestContainers/Harnesses/ActorHarness.cs | 2 +- .../Harnesses/ConversationHarness.cs | 2 +- .../Harnesses/DistributedLockHarness.cs | 2 +- .../Harnesses/PubSubHarness.cs | 2 +- .../Harnesses/StateManagementHarness.cs | 2 +- .../Harnesses/WorkflowHarness.cs | 2 +- test/Dapr.E2E.Test.Jobs/JobsTests.cs | 14 +++++++++++++- 11 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/Dapr.TestContainers/Common/Testing/DaprTestApplicationBuilder.cs b/src/Dapr.TestContainers/Common/Testing/DaprTestApplicationBuilder.cs index 43426276a..cba540d05 100644 --- a/src/Dapr.TestContainers/Common/Testing/DaprTestApplicationBuilder.cs +++ b/src/Dapr.TestContainers/Common/Testing/DaprTestApplicationBuilder.cs @@ -19,6 +19,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace Dapr.E2E.Test.Common; @@ -60,14 +61,7 @@ public async Task BuildAndStartAsync() WebApplication? app = null; if (_configureServices is not null || _configureApp is not null) { - // Set environment variables - Environment.SetEnvironmentVariable("DAPR_HTTP_ENDPOINT", $"http://127.0.0.1:{harness.DaprHttpPort}"); - Environment.SetEnvironmentVariable("DAPR_GRPC_ENDPOINT", $"http://127.0.0.1:{harness.DaprGrpcPort}"); - var builder = WebApplication.CreateBuilder(); - builder.Logging.ClearProviders(); - builder.Logging.AddSimpleConsole(); - builder.WebHost.UseUrls($"http://0.0.0.0:{harness.AppPort}"); // Configure Dapr endpoints via in-memory configuration instead of environment variables builder.Configuration.AddInMemoryCollection(new Dictionary @@ -76,9 +70,16 @@ public async Task BuildAndStartAsync() { "DAPR_GRPC_ENDPOINT", $"http://127.0.0.1:{harness.DaprGrpcPort}" } }); + builder.Logging.ClearProviders(); + builder.Logging.AddSimpleConsole(); + builder.WebHost.UseUrls($"http://0.0.0.0:{harness.AppPort}"); + + + _configureServices?.Invoke(builder); app = builder.Build(); + _configureApp?.Invoke(app); await app.StartAsync(); diff --git a/src/Dapr.TestContainers/Containers/OllamaContainer.cs b/src/Dapr.TestContainers/Containers/OllamaContainer.cs index 10739d550..f2dd4006a 100644 --- a/src/Dapr.TestContainers/Containers/OllamaContainer.cs +++ b/src/Dapr.TestContainers/Containers/OllamaContainer.cs @@ -46,6 +46,10 @@ public OllamaContainer(INetwork network) .Build(); } + /// + /// The internal container port used by Ollama. + /// + public const int ContainerPort = InternalPort; /// /// The internal network alias/name of the container. /// diff --git a/src/Dapr.TestContainers/Containers/RabbitMqContainer.cs b/src/Dapr.TestContainers/Containers/RabbitMqContainer.cs index 6763d9745..e5c5eda94 100644 --- a/src/Dapr.TestContainers/Containers/RabbitMqContainer.cs +++ b/src/Dapr.TestContainers/Containers/RabbitMqContainer.cs @@ -48,6 +48,10 @@ public RabbitMqContainer(INetwork network) .Build(); } + /// + /// The internal container port used by RabbitMQ. + /// + public const int ContainerPort = InternalPort; /// /// The internal network alias/name of the container. /// diff --git a/src/Dapr.TestContainers/Containers/RedisContainer.cs b/src/Dapr.TestContainers/Containers/RedisContainer.cs index 03925fd69..d6daec0b9 100644 --- a/src/Dapr.TestContainers/Containers/RedisContainer.cs +++ b/src/Dapr.TestContainers/Containers/RedisContainer.cs @@ -46,6 +46,10 @@ public RedisContainer(INetwork network) .Build(); } + /// + /// The internal container port used by Redis. + /// + public const int ContainerPort = InternalPort; /// /// The internal network alias/name of the container. /// diff --git a/src/Dapr.TestContainers/Harnesses/ActorHarness.cs b/src/Dapr.TestContainers/Harnesses/ActorHarness.cs index 3d00f4215..19cd7a379 100644 --- a/src/Dapr.TestContainers/Harnesses/ActorHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/ActorHarness.cs @@ -53,7 +53,7 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo await _schedueler.StartAsync(cancellationToken); // Emit component YAMLs pointing to Redis - RedisContainer.Yaml.WriteStateStoreYamlToFolder(componentsDir, redisHost: $"{_redis.NetworkAlias}:{_redis.Port}"); + RedisContainer.Yaml.WriteStateStoreYamlToFolder(componentsDir, redisHost: $"{_redis.NetworkAlias}:{RedisContainer.ContainerPort}"); DaprPlacementExternalPort = _placement.ExternalPort; DaprSchedulerExternalPort = _schedueler.ExternalPort; diff --git a/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs b/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs index d16974e7b..ced196129 100644 --- a/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs @@ -47,7 +47,7 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo // Emit component YAMLs for Ollama (use the default tiny model) OllamaContainer.Yaml.WriteConversationYamlToFolder(componentsDir, - endpoint: $"http://{_ollama.NetworkAlias}:{_ollama.Port}/v1"); + endpoint: $"http://{_ollama.NetworkAlias}:{OllamaContainer.ContainerPort}/v1"); } /// diff --git a/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs b/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs index ee4a33cf2..237f2cb13 100644 --- a/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs @@ -46,7 +46,7 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo await _redis.StartAsync(cancellationToken); // Emit component YAMLs pointing to Redis - RedisContainer.Yaml.WriteDistributedLockYamlToFolder(componentsDir, redisHost: $"{_redis.NetworkAlias}:{_redis.Port}"); + RedisContainer.Yaml.WriteDistributedLockYamlToFolder(componentsDir, redisHost: $"{_redis.NetworkAlias}:{RedisContainer.ContainerPort}"); } /// diff --git a/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs b/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs index 9f38cd199..13931702d 100644 --- a/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs @@ -46,7 +46,7 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo await _rabbitmq.StartAsync(cancellationToken); // Emit component YAMLs pointing to RabbitMQ - RabbitMqContainer.Yaml.WritePubSubYamlToFolder(componentsDir, rabbitmqHost: $"{_rabbitmq.NetworkAlias}:{_rabbitmq.Port}"); + RabbitMqContainer.Yaml.WritePubSubYamlToFolder(componentsDir, rabbitmqHost: $"{_rabbitmq.NetworkAlias}:{RabbitMqContainer.ContainerPort}"); } /// diff --git a/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs b/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs index 6b1dc222c..703c51bd8 100644 --- a/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs @@ -46,7 +46,7 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo await _redis.StartAsync(cancellationToken); // Emit component YAMLs pointing to Redis - RedisContainer.Yaml.WriteStateStoreYamlToFolder(componentsDir, redisHost: $"{_redis.NetworkAlias}:{_redis.Port}"); + RedisContainer.Yaml.WriteStateStoreYamlToFolder(componentsDir, redisHost: $"{_redis.NetworkAlias}:{RedisContainer.ContainerPort}"); } /// diff --git a/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs b/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs index 1e17870d2..6d869af00 100644 --- a/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs +++ b/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs @@ -51,7 +51,7 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo await _scheduler.StartAsync(cancellationToken); // Emit component YAMLs pointing to Redis - RedisContainer.Yaml.WriteStateStoreYamlToFolder(ComponentsDirectory, redisHost: $"{_redis.NetworkAlias}:6379"); + RedisContainer.Yaml.WriteStateStoreYamlToFolder(ComponentsDirectory, redisHost: $"{_redis.NetworkAlias}:{RedisContainer.ContainerPort}"); // Set the service ports this.DaprPlacementExternalPort = _placement.ExternalPort; diff --git a/test/Dapr.E2E.Test.Jobs/JobsTests.cs b/test/Dapr.E2E.Test.Jobs/JobsTests.cs index 1cab24279..64f47c241 100644 --- a/test/Dapr.E2E.Test.Jobs/JobsTests.cs +++ b/test/Dapr.E2E.Test.Jobs/JobsTests.cs @@ -17,6 +17,7 @@ using Dapr.Jobs.Models; using Dapr.TestContainers.Common; using Dapr.TestContainers.Common.Options; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -36,7 +37,18 @@ public async Task ShouldScheduleAndReceiveJob() await using var testApp = await DaprHarnessBuilder.ForHarness(harness) .ConfigureServices(builder => { - builder.Services.AddDaprJobsClient(); + // Explicitly configure the Dapr client with the correct endpoints + builder.Services.AddDaprJobsClient(configure: (sp, clientBuilder) => + { + var config = sp.GetRequiredService(); + var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; + var httpEndpoint = config["DAPR_HTTP_ENDPOINT"]; + + if (!string.IsNullOrEmpty(grpcEndpoint)) + clientBuilder.UseGrpcEndpoint(grpcEndpoint); + if (!string.IsNullOrEmpty(httpEndpoint)) + clientBuilder.UseHttpEndpoint(httpEndpoint); + }); }) .ConfigureApp(app => { From a7cf2e4e62d9b51802b09ff2fddf37ad1960bd21 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 23 Dec 2025 19:29:55 -0600 Subject: [PATCH 30/32] Removed unused using statement Signed-off-by: Whit Waldo --- test/Dapr.E2E.Test.Jobs/JobsTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/Dapr.E2E.Test.Jobs/JobsTests.cs b/test/Dapr.E2E.Test.Jobs/JobsTests.cs index 64f47c241..17e4c3b64 100644 --- a/test/Dapr.E2E.Test.Jobs/JobsTests.cs +++ b/test/Dapr.E2E.Test.Jobs/JobsTests.cs @@ -15,6 +15,7 @@ using Dapr.Jobs; using Dapr.Jobs.Extensions; using Dapr.Jobs.Models; +using Dapr.Jobs.Models.Responses; using Dapr.TestContainers.Common; using Dapr.TestContainers.Common.Options; using Microsoft.Extensions.Configuration; From e682aab956777c040f5758c902fb4a60e056a341 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 23 Dec 2025 19:32:39 -0600 Subject: [PATCH 31/32] Removed invalid target framework identifier Signed-off-by: Whit Waldo --- test/Dapr.E2E.Test.Workflow/Dapr.E2E.Test.Workflow.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/test/Dapr.E2E.Test.Workflow/Dapr.E2E.Test.Workflow.csproj b/test/Dapr.E2E.Test.Workflow/Dapr.E2E.Test.Workflow.csproj index f03795c89..fc4a8b0db 100644 --- a/test/Dapr.E2E.Test.Workflow/Dapr.E2E.Test.Workflow.csproj +++ b/test/Dapr.E2E.Test.Workflow/Dapr.E2E.Test.Workflow.csproj @@ -1,7 +1,6 @@  - net9.0 enable enable false From f23f61f3f1335671ad5435856b1b68048ffe7933 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 23 Dec 2025 19:33:29 -0600 Subject: [PATCH 32/32] Added some commented out sample E2E tests from an earlier build so they're not lost locally after merging this PR. They should be replaced by real implementations against the current API when possible. Signed-off-by: Whit Waldo --- test/Dapr.E2E.Test.PubSub/PubsubTests.cs | 98 +++++++++++++++++ test/Dapr.E2E.Test.Workflow/WorkflowTests.cs | 105 +++++++++++++++++++ 2 files changed, 203 insertions(+) create mode 100644 test/Dapr.E2E.Test.PubSub/PubsubTests.cs create mode 100644 test/Dapr.E2E.Test.Workflow/WorkflowTests.cs diff --git a/test/Dapr.E2E.Test.PubSub/PubsubTests.cs b/test/Dapr.E2E.Test.PubSub/PubsubTests.cs new file mode 100644 index 000000000..920a2aa4c --- /dev/null +++ b/test/Dapr.E2E.Test.PubSub/PubsubTests.cs @@ -0,0 +1,98 @@ +// // ------------------------------------------------------------------------ +// // Copyright 2025 The Dapr Authors +// // 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. +// // ------------------------------------------------------------------------ +// +// using Dapr.Client; +// using Dapr.TestContainers; +// using Dapr.TestContainers.Common; +// using Dapr.TestContainers.Common.Options; +// using Microsoft.AspNetCore.Builder; +// using Microsoft.AspNetCore.Hosting; +// using Microsoft.AspNetCore.Http; +// using Microsoft.Extensions.DependencyInjection; +// using Microsoft.Extensions.Hosting; +// +// namespace Dapr.E2E.Test.PubSub; +// +// public class PubsubTests +// { +// [Fact] +// public async Task ShouldEnablePublishAndSubscribe() +// { +// var options = new DaprRuntimeOptions(); +// var componentsDir = Path.Combine(Directory.GetCurrentDirectory(), "pubsub-components"); +// const string topicName = "test-topic"; +// +// WebApplication? app = null; +// +// var messageReceived = new TaskCompletionSource(); +// +// // Build and initialize the test harness +// var harnessBuilder = new DaprHarnessBuilder(options, StartApp); +// var harness = harnessBuilder.BuildPubSub(componentsDir); +// +// try +// { +// await harness.InitializeAsync(); +// +// var testAppBuilder = new HostApplicationBuilder(); +// testAppBuilder.Services.AddDaprClient(); +// using var testApp = testAppBuilder.Build(); +// await using var scope = testApp.Services.CreateAsyncScope(); +// var daprClient = scope.ServiceProvider.GetRequiredService(); +// +// // Use DaprClient to publish a message +// const string testMessage = "Hello!"; +// await daprClient.PublishEventAsync(Constants.DaprComponentNames.PubSubComponentName, topicName, +// testMessage); +// +// // Wait for the app to receive the message via the sidecar +// var result = await messageReceived.Task.WaitAsync(TimeSpan.FromSeconds(10)); +// Assert.Equal(testMessage, result); +// } +// finally +// { +// await harness.DisposeAsync(); +// +// if (app != null) +// await app.DisposeAsync(); +// } +// +// return; +// +// // Define the app startup +// async Task StartApp(int port) +// { +// var builder = WebApplication.CreateBuilder(); +// builder.WebHost.UseUrls($"http://localhost:{port}"); +// builder.Services.AddControllers().AddDapr(); +// +// app = builder.Build(); +// +// // Setup the subscription endpoint +// app.UseCloudEvents(); +// app.MapSubscribeHandler(); +// +// // Endpoint that Dapr will call when a message is published +// app.MapPost("/message-handler", async (HttpContext context) => +// { +// var data = await context.Request.ReadFromJsonAsync(); +// messageReceived.TrySetResult(data?.ToString() ?? "empty"); +// return Results.Ok(); +// }) +// .WithTopic(Constants.DaprComponentNames.PubSubComponentName, topicName); +// +// await app.StartAsync(); +// } +// } +// +// } diff --git a/test/Dapr.E2E.Test.Workflow/WorkflowTests.cs b/test/Dapr.E2E.Test.Workflow/WorkflowTests.cs new file mode 100644 index 000000000..889c6d1e4 --- /dev/null +++ b/test/Dapr.E2E.Test.Workflow/WorkflowTests.cs @@ -0,0 +1,105 @@ +// // ------------------------------------------------------------------------ +// // Copyright 2025 The Dapr Authors +// // 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. +// // ------------------------------------------------------------------------ +// +// using Dapr.TestContainers.Common; +// using Dapr.TestContainers.Common.Options; +// using Dapr.Workflow; +// using Microsoft.AspNetCore.Builder; +// using Microsoft.AspNetCore.Hosting; +// using Microsoft.Extensions.DependencyInjection; +// using Microsoft.Extensions.Logging; +// +// namespace Dapr.E2E.Test.Workflow; +// +// public class WorkflowTests +// { +// [Fact] +// public async Task ShouldTestTaskChaining() +// { +// var options = new DaprRuntimeOptions(); +// var componentsDir = Path.Combine(Directory.GetCurrentDirectory(), $"test-components-{Guid.NewGuid():N}"); +// +// WebApplication? app = null; +// +// // Build and initialize the test harness +// var harnessBuilder = new DaprHarnessBuilder(options, StartApp); +// var harness = harnessBuilder.BuildWorkflow(componentsDir); +// +// try +// { +// await harness.InitializeAsync(); +// +// await using var scope = app!.Services.CreateAsyncScope(); +// var daprWorkflowClient = scope.ServiceProvider.GetRequiredService(); +// +// // Start the workflow +// var workflowId = Guid.NewGuid().ToString("N"); +// const int startingValue = 8; +// +// await daprWorkflowClient.ScheduleNewWorkflowAsync(nameof(TestWorkflow), workflowId, startingValue); +// +// var result = await daprWorkflowClient.WaitForWorkflowCompletionAsync(workflowId, true); +// +// Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus); +// var resultValue = result.ReadOutputAs(); +// +// Assert.Equal(16, resultValue); +// } +// finally +// { +// await harness.DisposeAsync(); +// if (app is not null) +// await app.DisposeAsync(); +// } +// +// return; +// +// // Define the app startup +// async Task StartApp(int port) +// { +// var builder = WebApplication.CreateBuilder(); +// builder.Logging.ClearProviders(); +// builder.Logging.AddSimpleConsole(); +// builder.WebHost.UseUrls($"http://0.0.0.0:{port}"); +// builder.Services.AddDaprWorkflow(opt => +// { +// opt.RegisterWorkflow(); +// opt.RegisterActivity(); +// }); +// +// Console.WriteLine($"HTTP: {Environment.GetEnvironmentVariable("DAPR_HTTP_ENDPOINT")}"); +// Console.WriteLine($"GRPC: {Environment.GetEnvironmentVariable("DAPR_GRPC_ENDPOINT")}"); +// +// app = builder.Build(); +// await app.StartAsync(); +// } +// } +// +// private sealed class DoublingActivity : WorkflowActivity +// { +// public override Task RunAsync(WorkflowActivityContext context, int input) +// { +// var square = input * 2; +// return Task.FromResult(square); +// } +// } +// +// private sealed class TestWorkflow : Workflow +// { +// public override async Task RunAsync(WorkflowContext context, int input) +// { +// var result = await context.CallActivityAsync(nameof(DoublingActivity), input); +// return result; +// } +// } +// }