Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
4db41e7
Add Dapr.IntegrationTest.Actors project and fix ActorHarness
Copilot Apr 12, 2026
08fddef
Fix ActorHarness Redis disposal and TimerActor timer name tracking
Copilot Apr 12, 2026
39f1e5c
Address review feedback: 2026 copyright, primary constructors, switch…
Copilot Apr 13, 2026
bfcd903
test: add comprehensive ActorStateManager, DaprStateProvider, ActorRu…
Copilot Apr 13, 2026
458f903
test: remove unused System.Net using from ActivationTests.cs
Copilot Apr 13, 2026
80f9701
fix: pass IConfiguration to GetDefaultHttpEndpoint in ActorsServiceCo…
Copilot Apr 13, 2026
3ad5781
style: fix spelling normalises -> normalizes in test comment
Copilot Apr 13, 2026
79b4c15
Merge branch 'master' into copilot/add-dapr-integrationtest-actors
WhitWaldo Apr 14, 2026
7e36f56
Merge branch 'master' into copilot/add-dapr-integrationtest-actors
WhitWaldo Apr 14, 2026
dd0d3aa
Merge branch 'master' into copilot/add-dapr-integrationtest-actors
WhitWaldo Apr 14, 2026
209a127
Added missing dispose statement
WhitWaldo Apr 15, 2026
9ef7a54
Simplified actor harness setup
WhitWaldo Apr 15, 2026
375dfeb
Removed extraneous arguments from harness builder
WhitWaldo Apr 15, 2026
e9fea7f
Fixing integration test approach in light of removed unused argument
WhitWaldo Apr 15, 2026
410434f
fix: bound WaitForActorRuntimeAsync per-attempt to 5s to avoid 100s H…
Copilot Apr 15, 2026
3c48728
style: restore 250ms retry delay in WaitForActorRuntimeAsync (reverts…
Copilot Apr 15, 2026
036a216
fix: environment lifecycle bug in CreateTestAppAsync — placement/sche…
Copilot Apr 15, 2026
fa92455
style: US English spelling in ActorTestContext XML doc (Initializes)
Copilot Apr 15, 2026
6b882d6
fix: actor integration test failures — TTL feature flag, serializatio…
Copilot Apr 15, 2026
1e4e1b6
style: add System.IO using to ActorHarness, narrow DaprApiException c…
Copilot Apr 15, 2026
8c7a6f3
Merge branch 'master' into copilot/add-dapr-integrationtest-actors
WhitWaldo Apr 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions all.sln
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.IntegrationTest.Messag
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.IntegrationTest.Cryptography", "test\Dapr.IntegrationTest.Cryptography\Dapr.IntegrationTest.Cryptography.csproj", "{7B14879F-156B-417E-ACA3-0B5A69CC2F39}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.IntegrationTest.Actors", "test\Dapr.IntegrationTest.Actors\Dapr.IntegrationTest.Actors.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -669,6 +671,10 @@ Global
{7B14879F-156B-417E-ACA3-0B5A69CC2F39}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7B14879F-156B-417E-ACA3-0B5A69CC2F39}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7B14879F-156B-417E-ACA3-0B5A69CC2F39}.Release|Any CPU.Build.0 = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -791,6 +797,7 @@ Global
{97CAEE0B-4020-4A86-97DA-9900FDF4DFC6} = {8462B106-175A-423A-BA94-BE0D39D0BD8E}
{01A20A89-53A1-4D5B-B563-89E157718474} = {8462B106-175A-423A-BA94-BE0D39D0BD8E}
{7B14879F-156B-417E-ACA3-0B5A69CC2F39} = {8462B106-175A-423A-BA94-BE0D39D0BD8E}
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890} = {8462B106-175A-423A-BA94-BE0D39D0BD8E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40}
Expand Down
15 changes: 4 additions & 11 deletions src/Dapr.Testcontainers/Common/DaprHarnessBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,17 +116,10 @@ public CryptographyHarness BuildCryptography(string keysDir) =>
/// <param name="componentsDir">The path to the Dapr resources.</param>
public PubSubHarness BuildPubSub(string componentsDir) => new(_componentsDirectory, _startApp, _options, _environment);

// /// <summary>
// /// Builds a state management harness.
// /// </summary>
// /// <param name="componentsDir">The path to the Dapr resources.</param>
// public StateManagementHarness BuildStateManagement(string componentsDir) => new(_componentsDirectory, _startApp, _options, _environment);
//
// /// <summary>
// /// Builds an actor harness.
// /// </summary>
// /// <param name="componentsDir">The path to the Dapr resources.</param>
// public ActorHarness BuildActors(string componentsDir) => new(_componentsDirectory, _startApp, _options, _environment);
/// <summary>
/// Builds an actor harness.
/// </summary>
public ActorHarness BuildActors() => new(_componentsDirectory, _startApp, _options, _environment);

/// <summary>
/// Creates a test application builder for the specified harness.
Expand Down
66 changes: 37 additions & 29 deletions src/Dapr.Testcontainers/Harnesses/ActorHarness.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
using System.Threading.Tasks;
using Dapr.Testcontainers.Common.Options;
using Dapr.Testcontainers.Containers;
using Dapr.Testcontainers.Containers.Dapr;

namespace Dapr.Testcontainers.Harnesses;

Expand All @@ -26,45 +25,54 @@ namespace Dapr.Testcontainers.Harnesses;
public sealed class ActorHarness : BaseHarness
{
private readonly RedisContainer _redis;
private readonly DaprPlacementContainer _placement;
private readonly DaprSchedulerContainer _schedueler;
private readonly string componentsDir;
private readonly bool _isSelfHostedRedis;
private readonly string _componentsDir;

/// <summary>
/// Provides an implementation harness for Dapr's actor building block.
/// </summary>
/// <param name="componentsDir">The directory to Dapr components.</param>
/// <param name="startApp">The test app to validate in the harness.</param>
/// <param name="options">The dapr runtime options.</param>
/// <param name="environment">The isolated environment instance.</param>
public ActorHarness(string componentsDir, Func<int, Task>? startApp, DaprRuntimeOptions options, DaprTestEnvironment? environment = null) : base(componentsDir, startApp, options, environment)
/// <param name="options">The Dapr runtime options.</param>
/// <param name="environment">
/// An optional shared <see cref="DaprTestEnvironment"/>. When provided the harness reuses
/// its Redis, Placement, and Scheduler services instead of starting its own.
/// </param>
public ActorHarness(string componentsDir, Func<int, Task>? startApp, DaprRuntimeOptions options, DaprTestEnvironment? environment = null)
: base(componentsDir, startApp, options, environment)
{
this.componentsDir = componentsDir;
_placement = new DaprPlacementContainer(options, Network, ContainerLogsDirectory);
_schedueler = new DaprSchedulerContainer(options, Network, ContainerLogsDirectory);
_redis = new RedisContainer(Network, ContainerLogsDirectory);
_componentsDir = componentsDir;
_redis = environment?.RedisContainer ?? new RedisContainer(Network, ContainerLogsDirectory);
_isSelfHostedRedis = environment?.RedisContainer is null;
}

/// <inheritdoc />
protected override async Task OnInitializeAsync(CancellationToken cancellationToken)
{
// Start infrastructure
await _redis.StartAsync(cancellationToken);
await _placement.StartAsync(cancellationToken);
await _schedueler.StartAsync(cancellationToken);

// Emit component YAMLs pointing to Redis
RedisContainer.Yaml.WriteStateStoreYamlToFolder(componentsDir, redisHost: $"{_redis.NetworkAlias}:{RedisContainer.ContainerPort}");
protected override async Task OnInitializeAsync(CancellationToken cancellationToken)
{
// Only start Redis if it is not provided by a shared environment.
if (_isSelfHostedRedis)
{
await _redis.StartAsync(cancellationToken);
}

// Write the state-store component YAML that points to the Redis instance.
RedisContainer.Yaml.WriteStateStoreYamlToFolder(
_componentsDir,
redisHost: $"{_redis.NetworkAlias}:{RedisContainer.ContainerPort}");

DaprPlacementExternalPort = _placement.ExternalPort;
DaprSchedulerExternalPort = _schedueler.ExternalPort;
// Forward placement and scheduler coordinates from the environment.
DaprPlacementExternalPort = Environment.PlacementExternalPort;
DaprPlacementAlias = Environment.PlacementAlias;
DaprSchedulerExternalPort = Environment.SchedulerExternalPort;
DaprSchedulerAlias = Environment.SchedulerAlias;
}

/// <inheritdoc />
protected override async ValueTask OnDisposeAsync()
{
await _redis.DisposeAsync();
await _placement.DisposeAsync();
await _schedueler.DisposeAsync();
}
protected override async ValueTask OnDisposeAsync()
{
if (_isSelfHostedRedis)
{
await _redis.DisposeAsync();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// ------------------------------------------------------------------------
// 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.Actors.Runtime;

namespace Dapr.IntegrationTest.Actors.ExceptionTesting;

/// <summary>
/// Implementation of <see cref="IExceptionActor"/> that unconditionally throws to
/// validate that the Dapr runtime correctly propagates remote exceptions to callers.
/// </summary>
public class ExceptionActor : Actor, IExceptionActor
{
/// <summary>
/// Initializes a new instance of <see cref="ExceptionActor"/>.
/// </summary>
/// <param name="host">The actor host provided by the Dapr runtime.</param>
public ExceptionActor(ActorHost host) : base(host)
{
}

/// <inheritdoc />
public Task Ping() => Task.CompletedTask;

/// <inheritdoc />
public Task ExceptionExample() =>
throw new InvalidOperationException("This exception is intentional.");
}
Original file line number Diff line number Diff line change
@@ -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.
// ------------------------------------------------------------------------

using System.Threading.Tasks;
using Dapr.Actors;

namespace Dapr.IntegrationTest.Actors.ExceptionTesting;

/// <summary>
/// Actor interface that deliberately throws an exception to exercise error propagation.
/// </summary>
public interface IExceptionActor : IPingActor, IActor
{
/// <summary>Always throws an <see cref="System.Exception"/> to validate remote exception handling.</summary>
Task ExceptionExample();
}
26 changes: 26 additions & 0 deletions test/Dapr.IntegrationTest.Actors/Actors/IPingActor.cs
Original file line number Diff line number Diff line change
@@ -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.
// ------------------------------------------------------------------------

using System.Threading.Tasks;
using Dapr.Actors;

namespace Dapr.IntegrationTest.Actors;

/// <summary>
/// Minimal actor interface used as a readiness probe.
/// </summary>
public interface IPingActor : IActor
{
/// <summary>Pings the actor to verify that the runtime is available.</summary>
Task Ping();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// ------------------------------------------------------------------------
// 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.Tasks;
using Dapr.Actors;

namespace Dapr.IntegrationTest.Actors.Reentrancy;

/// <summary>
/// Options controlling how a reentrant call chain is executed.
/// </summary>
public sealed class ReentrantCallOptions
{
/// <summary>Gets or sets the number of additional reentrant calls remaining to make.</summary>
public int CallsRemaining { get; set; }

/// <summary>Gets or sets the zero-based sequence number of the current call.</summary>
public int CallNumber { get; set; }
}

/// <summary>
/// Records a single enter or exit event within a reentrant call chain.
/// </summary>
public sealed class CallRecord
{
/// <summary>Gets or sets a value indicating whether this record represents an entry (<see langword="true"/>) or exit (<see langword="false"/>).</summary>
public bool IsEnter { get; set; }

/// <summary>Gets or sets the wall-clock time of this event.</summary>
public DateTime Timestamp { get; set; }

/// <summary>Gets or sets the sequence number of the call that produced this record.</summary>
public int CallNumber { get; set; }
}

/// <summary>
/// Per-call state kept by <see cref="IReentrantActor"/>.
/// </summary>
public sealed class ReentrantCallState
{
/// <summary>Gets the ordered list of enter/exit records for a single call number.</summary>
public List<CallRecord> Records { get; init; } = [];
}

/// <summary>
/// Actor interface that exercises Dapr actor reentrancy.
/// </summary>
public interface IReentrantActor : IPingActor, IActor
{
/// <summary>Initiates a reentrant call chain as described by <paramref name="callOptions"/>.</summary>
Comment thread
WhitWaldo marked this conversation as resolved.
Outdated
/// <param name="callOptions">Controls the depth and sequence number of the reentrant chain.</param>
Task ReentrantCall(ReentrantCallOptions callOptions);

/// <summary>Returns the enter/exit records accumulated for the given <paramref name="callNumber"/>.</summary>
/// <param name="callNumber">The zero-based call number to retrieve state for.</param>
Task<ReentrantCallState> GetState(int callNumber);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// ------------------------------------------------------------------------
// Copyright 2025 The Dapr Authors
Comment thread
WhitWaldo marked this conversation as resolved.
Outdated
// 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.Actors.Runtime;

namespace Dapr.IntegrationTest.Actors.Reentrancy;

/// <summary>
/// Implementation of <see cref="IReentrantActor"/> that recursively calls itself
/// to produce a chain of reentrant invocations.
/// </summary>
public class ReentrantActor : Actor, IReentrantActor
{
/// <summary>
/// Initializes a new instance of <see cref="ReentrantActor"/>.
/// </summary>
/// <param name="host">The actor host provided by the Dapr runtime.</param>
public ReentrantActor(ActorHost host) : base(host)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer to use primary constructors where possible

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Converted all actor implementations (ReentrantActor, RegressionActor, ReminderActor, StateActor, AdvancedStateActor, ExceptionActor, SerializationActor, WeaklyTypedTestingActor, TimerActor) to use C# 12 primary constructors in commit 39f1e5c.

{
}

/// <inheritdoc />
public Task Ping() => Task.CompletedTask;

/// <inheritdoc />
public async Task ReentrantCall(ReentrantCallOptions callOptions)
{
await UpdateState(isEnter: true, callOptions.CallNumber);

var self = ProxyFactory.CreateActorProxy<IReentrantActor>(Id, "ReentrantActor");
if (callOptions.CallsRemaining <= 1)
{
await self.Ping();
}
else
{
await self.ReentrantCall(new ReentrantCallOptions
{
CallsRemaining = callOptions.CallsRemaining - 1,
CallNumber = callOptions.CallNumber + 1,
});
}

await UpdateState(isEnter: false, callOptions.CallNumber);
}

/// <inheritdoc />
public Task<ReentrantCallState> GetState(int callNumber) =>
StateManager.GetOrAddStateAsync($"reentrant-record{callNumber}", new ReentrantCallState());

private async Task UpdateState(bool isEnter, int callNumber)
{
var stateKey = $"reentrant-record{callNumber}";
var state = await StateManager.GetOrAddStateAsync(stateKey, new ReentrantCallState());
state.Records.Add(new CallRecord
{
IsEnter = isEnter,
Timestamp = DateTime.Now,
CallNumber = callNumber,
});
await StateManager.SetStateAsync(stateKey, state);

if (!isEnter)
{
await StateManager.SaveStateAsync();
}
}
}
Loading
Loading