From c4cd83313b361fc580da0484e10951ca8260944a Mon Sep 17 00:00:00 2001 From: Roger Johansson Date: Wed, 6 Aug 2025 22:08:11 +0200 Subject: [PATCH] test: add stashing tests --- tests/Proto.Actor.Tests/StashingTests.cs | 141 +++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 tests/Proto.Actor.Tests/StashingTests.cs diff --git a/tests/Proto.Actor.Tests/StashingTests.cs b/tests/Proto.Actor.Tests/StashingTests.cs new file mode 100644 index 0000000000..dd56c6adaa --- /dev/null +++ b/tests/Proto.Actor.Tests/StashingTests.cs @@ -0,0 +1,141 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2015-2025 Asynkron AB All rights reserved +// +// ----------------------------------------------------------------------- + +using System.Collections.Generic; +using System.Threading.Tasks; +using FluentAssertions; +using Xunit; + +namespace Proto.Tests; + +// actor that stashes first string message and processes it when Receive is called +public class StashingActor : IActor +{ + private readonly List _processed; + private CapturedContext? _captured; + private bool _stashed; + + public StashingActor(List processed) + { + _processed = processed; + } + + public Task ReceiveAsync(IContext context) + { + return context.Message switch + { + string msg when msg == "unstash" => HandleUnstash(), + string msg => HandleString(msg, context), + _ => Task.CompletedTask + }; + + Task HandleUnstash() + { + return _captured?.Receive() ?? Task.CompletedTask; + } + + Task HandleString(string msg, IContext ctx) + { + if (!_stashed) + { + _captured = ctx.Capture(); + _stashed = true; + return Task.CompletedTask; // do not process yet + } + + _processed.Add(msg); // processed on replay + return Task.CompletedTask; + } + } +} + +// actor used to test Apply() +public class ApplyActor : IActor +{ + private readonly List _observed; + private CapturedContext? _captured; + + public bool ContextCorrupted { get; private set; } + + public ApplyActor(List observed) => _observed = observed; + + public Task ReceiveAsync(IContext context) + { + return context.Message switch + { + string msg when msg == "stash" => HandleStash(context), + string msg when msg == "apply" => HandleApply(context), + _ => Task.CompletedTask + }; + + Task HandleStash(IContext ctx) + { + _captured = ctx.Capture(); + return Task.CompletedTask; + } + + Task HandleApply(IContext ctx) + { + var current = ctx.Capture(); // capture current context + + _captured?.Apply(); // restore stashed envelope + _observed.Add(ctx.Message?.ToString() ?? string.Empty); // record restored message + + current.Apply(); // restore current message + if (!Equals(ctx.Message, current.MessageEnvelope.Message)) + { + ContextCorrupted = true; + } + + _observed.Add(ctx.Message?.ToString() ?? string.Empty); // record current message after restore + + return Task.CompletedTask; + } + } +} + +public class StashingTests +{ + [Fact] + public async Task Replayed_message_is_processed_only_once() + { + var system = new ActorSystem(); + await using var _ = system; + var processed = new List(); + + var props = Props.FromProducer(() => new StashingActor(processed)); + var pid = system.Root.Spawn(props); + + system.Root.Send(pid, "hello"); // stashed + processed.Should().BeEmpty(); + + system.Root.Send(pid, "unstash"); + await system.Root.PoisonAsync(pid); + + processed.Should().BeEquivalentTo(new[] { "hello" }); + } + + [Fact] + public async Task Apply_restores_message_without_corrupting_context() + { + var system = new ActorSystem(); + await using var _ = system; + var processed = new List(); + ApplyActor? actor = null; + + var props = Props.FromProducer(() => actor = new ApplyActor(processed)); + var pid = system.Root.Spawn(props); + + system.Root.Send(pid, "stash"); + system.Root.Send(pid, "apply"); + + await system.Root.PoisonAsync(pid); + + processed.Should().BeEquivalentTo(new[] { "stash", "apply" }); + actor!.ContextCorrupted.Should().BeFalse(); + } +} +