Merged PR 1452: Support inheritance of action handlers defined on Actor subclasses.
Support inheritance of action handlers defined on Actor subclasses. Related work items: #1871
This commit is contained in:
Родитель
605bf0349e
Коммит
5b662e28b2
|
@ -755,7 +755,41 @@ namespace Microsoft.Coyote.Actors
|
|||
/// </summary>
|
||||
internal virtual void SetupEventHandlers()
|
||||
{
|
||||
Type actorType = this.GetType();
|
||||
if (!ActionCache.ContainsKey(this.GetType()))
|
||||
{
|
||||
Stack<Type> actorTypes = new Stack<Type>();
|
||||
for (var actorType = this.GetType(); typeof(Actor).IsAssignableFrom(actorType); actorType = actorType.BaseType)
|
||||
{
|
||||
actorTypes.Push(actorType);
|
||||
}
|
||||
|
||||
// process base types in reverse order, so mosts derrived type is cached first.
|
||||
while (actorTypes.Count > 0)
|
||||
{
|
||||
this.SetupEventHandlers(actorTypes.Pop());
|
||||
}
|
||||
}
|
||||
|
||||
// Now we have all derrived types cached, we can build the combined action map for this type.
|
||||
for (var actorType = this.GetType(); typeof(Actor).IsAssignableFrom(actorType); actorType = actorType.BaseType)
|
||||
{
|
||||
// Populates the map of event handlers for this actor instance.
|
||||
foreach (var kvp in ActionCache[actorType])
|
||||
{
|
||||
// use the most derrived action handler for a given event (ignoring any base handlers defined for the same event).
|
||||
if (!this.ActionMap.ContainsKey(kvp.Key))
|
||||
{
|
||||
// MethodInfo.Invoke catches the exception to wrap it in a TargetInvocationException.
|
||||
// This unwinds the stack before the ExecuteAction exception filter is invoked, so
|
||||
// call through a delegate instead (which is also much faster than Invoke).
|
||||
this.ActionMap.Add(kvp.Key, new CachedDelegate(kvp.Value, this));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupEventHandlers(Type actorType)
|
||||
{
|
||||
if (!ActionCache.ContainsKey(actorType))
|
||||
{
|
||||
// If this type has not already been setup in the ActionCache, then we need to try and grab the ActionCacheLock
|
||||
|
@ -779,14 +813,14 @@ namespace Microsoft.Coyote.Actors
|
|||
|
||||
// Map containing all action bindings.
|
||||
var actionBindings = new Dictionary<Type, ActionEventHandlerDeclaration>();
|
||||
var doAttributes = this.GetType().GetCustomAttributes(typeof(OnEventDoActionAttribute), false)
|
||||
var doAttributes = actorType.GetCustomAttributes(typeof(OnEventDoActionAttribute), false)
|
||||
as OnEventDoActionAttribute[];
|
||||
|
||||
foreach (var attr in doAttributes)
|
||||
{
|
||||
this.Assert(!handledEvents.Contains(attr.Event),
|
||||
"{0} declared multiple handlers for event '{1}'.",
|
||||
this.Id, attr.Event);
|
||||
actorType.FullName, attr.Event);
|
||||
actionBindings.Add(attr.Event, new ActionEventHandlerDeclaration(attr.Action));
|
||||
handledEvents.Add(attr.Event);
|
||||
}
|
||||
|
@ -805,15 +839,6 @@ namespace Microsoft.Coyote.Actors
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Populates the map of event handlers for this actor instance.
|
||||
foreach (var kvp in ActionCache[actorType])
|
||||
{
|
||||
// MethodInfo.Invoke catches the exception to wrap it in a TargetInvocationException.
|
||||
// This unwinds the stack before the ExecuteAction exception filter is invoked, so
|
||||
// call through a delegate instead (which is also much faster than Invoke).
|
||||
this.ActionMap.Add(kvp.Key, new CachedDelegate(kvp.Value, this));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -855,26 +880,27 @@ namespace Microsoft.Coyote.Actors
|
|||
/// <summary>
|
||||
/// Checks the validity of the specified action.
|
||||
/// </summary>
|
||||
private protected virtual void AssertActionValidity(MethodInfo action)
|
||||
private void AssertActionValidity(MethodInfo action)
|
||||
{
|
||||
Type actionType = action.DeclaringType;
|
||||
ParameterInfo[] parameters = action.GetParameters();
|
||||
this.Assert(parameters.Length is 0 ||
|
||||
(parameters.Length is 1 && parameters[0].ParameterType == typeof(Event)),
|
||||
"Action '{0}' in '{1}' must either accept no parameters or a single parameter of type 'Event'.",
|
||||
action.Name, this.GetType().Name);
|
||||
action.Name, actionType.Name);
|
||||
|
||||
// Check if the action is an 'async' method.
|
||||
if (action.GetCustomAttribute(typeof(AsyncStateMachineAttribute)) != null)
|
||||
{
|
||||
this.Assert(action.ReturnType == typeof(Task),
|
||||
"Async action '{0}' in '{1}' must have 'Task' return type.",
|
||||
action.Name, this.GetType().Name);
|
||||
action.Name, actionType.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Assert(action.ReturnType == typeof(void),
|
||||
"Action '{0}' in '{1}' must have 'void' return type.",
|
||||
action.Name, this.GetType().Name);
|
||||
action.Name, actionType.Name);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -29,16 +29,6 @@ namespace Microsoft.Coyote.Actors
|
|||
this.Handler = Delegate.CreateDelegate(typeof(Action), caller, method);
|
||||
this.IsAsync = false;
|
||||
}
|
||||
else if (parameters.Length == 1 && method.ReturnType == typeof(StateMachine.Transition))
|
||||
{
|
||||
this.Handler = Delegate.CreateDelegate(typeof(Func<Event, StateMachine.Transition>), caller, method);
|
||||
this.IsAsync = false;
|
||||
}
|
||||
else if (method.ReturnType == typeof(StateMachine.Transition))
|
||||
{
|
||||
this.Handler = Delegate.CreateDelegate(typeof(Func<StateMachine.Transition>), caller, method);
|
||||
this.IsAsync = false;
|
||||
}
|
||||
else if (parameters.Length == 1 && method.ReturnType == typeof(Task))
|
||||
{
|
||||
this.Handler = Delegate.CreateDelegate(typeof(Func<Event, Task>), caller, method);
|
||||
|
@ -49,16 +39,6 @@ namespace Microsoft.Coyote.Actors
|
|||
this.Handler = Delegate.CreateDelegate(typeof(Func<Task>), caller, method);
|
||||
this.IsAsync = true;
|
||||
}
|
||||
else if (parameters.Length == 1 && method.ReturnType == typeof(Task<StateMachine.Transition>))
|
||||
{
|
||||
this.Handler = Delegate.CreateDelegate(typeof(Func<Event, Task<StateMachine.Transition>>), caller, method);
|
||||
this.IsAsync = true;
|
||||
}
|
||||
else if (method.ReturnType == typeof(Task<StateMachine.Transition>))
|
||||
{
|
||||
this.Handler = Delegate.CreateDelegate(typeof(Func<Task<StateMachine.Transition>>), caller, method);
|
||||
this.IsAsync = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Trying to cache invalid action delegate '{method.Name}'.");
|
||||
|
|
|
@ -1042,32 +1042,6 @@ namespace Microsoft.Coyote.Actors
|
|||
this.Assert(this.StateStack.Peek() != null, "{0} must not have a null current state.", this.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks the validity of the specified action.
|
||||
/// </summary>
|
||||
private protected override void AssertActionValidity(MethodInfo action)
|
||||
{
|
||||
ParameterInfo[] parameters = action.GetParameters();
|
||||
this.Assert(parameters.Length is 0 ||
|
||||
(parameters.Length is 1 && parameters[0].ParameterType == typeof(Event)),
|
||||
"Action '{0}' in '{1}' must either accept no parameters or a single parameter of type 'Event'.",
|
||||
action.Name, this.GetType().Name);
|
||||
|
||||
// Check if the action is an 'async' method.
|
||||
if (action.GetCustomAttribute(typeof(AsyncStateMachineAttribute)) != null)
|
||||
{
|
||||
this.Assert(action.ReturnType == typeof(Task) || action.ReturnType == typeof(Task<Transition>),
|
||||
"Async action '{0}' in '{1}' must have 'Task' or 'Task<Transition>' return type.",
|
||||
action.Name, this.GetType().Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Assert(action.ReturnType == typeof(void) || action.ReturnType == typeof(Transition),
|
||||
"Action '{0}' in '{1}' must have 'void' or 'Transition' return type.",
|
||||
action.Name, this.GetType().Name);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the formatted strint to be used with a fair nondeterministic boolean choice.
|
||||
/// </summary>
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Coyote.Actors;
|
||||
using Microsoft.Coyote.Runtime;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.Coyote.Core.Tests.Actors
|
||||
{
|
||||
public class ActorInheritanceTests : BaseTest
|
||||
{
|
||||
public ActorInheritanceTests(ITestOutputHelper output)
|
||||
: base(output)
|
||||
{
|
||||
}
|
||||
|
||||
private class E1 : Event
|
||||
{
|
||||
}
|
||||
|
||||
private class E2 : Event
|
||||
{
|
||||
}
|
||||
|
||||
private class E3 : Event
|
||||
{
|
||||
}
|
||||
|
||||
private class E4 : Event
|
||||
{
|
||||
}
|
||||
|
||||
private class CompletedEvent : Event
|
||||
{
|
||||
}
|
||||
|
||||
private class ConfigEvent : Event
|
||||
{
|
||||
public StringWriter Log = new StringWriter();
|
||||
public TaskCompletionSource<bool> Completed = new TaskCompletionSource<bool>();
|
||||
}
|
||||
|
||||
[OnEventDoAction(typeof(E1), nameof(HandleE1))]
|
||||
[OnEventDoAction(typeof(E3), nameof(HandleE3))]
|
||||
[OnEventDoAction(typeof(CompletedEvent), nameof(HandleCompleted))]
|
||||
private class BaseActor : Actor
|
||||
{
|
||||
public StringWriter Log;
|
||||
private TaskCompletionSource<bool> Completed;
|
||||
|
||||
protected override Task OnInitializeAsync(Event initialEvent)
|
||||
{
|
||||
if (initialEvent is ConfigEvent config)
|
||||
{
|
||||
this.Log = config.Log;
|
||||
this.Completed = config.Completed;
|
||||
}
|
||||
|
||||
return base.OnInitializeAsync(initialEvent);
|
||||
}
|
||||
|
||||
private void HandleE1()
|
||||
{
|
||||
this.Log.WriteLine("BaseActor handling E1");
|
||||
}
|
||||
|
||||
private void HandleE3()
|
||||
{
|
||||
this.Log.WriteLine("BaseActor handling E3");
|
||||
}
|
||||
|
||||
protected void HandleE4()
|
||||
{
|
||||
this.Log.WriteLine("Inherited handling of E4");
|
||||
}
|
||||
|
||||
private void HandleCompleted()
|
||||
{
|
||||
this.Completed.SetResult(true);
|
||||
}
|
||||
}
|
||||
|
||||
[OnEventDoAction(typeof(E1), nameof(HandleE1))]
|
||||
[OnEventDoAction(typeof(E2), nameof(HandleE2))]
|
||||
[OnEventDoAction(typeof(E4), nameof(HandleE4))]
|
||||
private class ActorSubclass : BaseActor
|
||||
{
|
||||
private void HandleE1()
|
||||
{
|
||||
this.Log.WriteLine("ActorSubclass handling E1");
|
||||
}
|
||||
|
||||
private void HandleE2()
|
||||
{
|
||||
this.Log.WriteLine("ActorSubclass handling E2");
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeNewLines(string text)
|
||||
{
|
||||
return text.Replace("\r\n", "\n");
|
||||
}
|
||||
|
||||
[Fact(Timeout = 5000)]
|
||||
public async Task TestActorInheritance()
|
||||
{
|
||||
var runtime = ActorRuntimeFactory.Create();
|
||||
var config = new ConfigEvent();
|
||||
var actor = runtime.CreateActor(typeof(ActorSubclass), config);
|
||||
runtime.SendEvent(actor, new E1());
|
||||
runtime.SendEvent(actor, new E2());
|
||||
runtime.SendEvent(actor, new E3());
|
||||
runtime.SendEvent(actor, new E4());
|
||||
runtime.SendEvent(actor, new CompletedEvent());
|
||||
await config.Completed.Task;
|
||||
|
||||
var actual = NormalizeNewLines(config.Log.ToString());
|
||||
var expected = NormalizeNewLines(@"ActorSubclass handling E1
|
||||
ActorSubclass handling E2
|
||||
BaseActor handling E3
|
||||
Inherited handling of E4
|
||||
");
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -35,7 +35,7 @@ namespace Microsoft.Coyote.TestingServices.Tests.Actors
|
|||
{
|
||||
r.CreateActor(typeof(A1));
|
||||
},
|
||||
expectedError: "A1() declared multiple handlers for event 'Actors.UnitEvent'.");
|
||||
expectedError: "A1 declared multiple handlers for event 'Actors.UnitEvent'.");
|
||||
}
|
||||
|
||||
private class M1 : StateMachine
|
||||
|
|
|
@ -61,11 +61,11 @@ At the moment, the APIs used to implement `SharedObjects` are internal to Coyote
|
|||
|
||||
Conceptually one should think of a Coyote SharedObject as a wrapper around a Coyote actor. Thus, one
|
||||
needs to be careful about stashing references inside a SharedObject and treat it in the same manner as
|
||||
sharing references between actors. In the case of a `SharedDictionary` both key and value objects
|
||||
sharing references between actors. In the case of a `SharedDictionary` both the key and the value
|
||||
(which are passed by reference into the dictionary) should not be mutated unless first removed from the
|
||||
dictionary because this can lead to a data race. Consider two actors that share a `SharedDictionary`
|
||||
object `D`. If both actors grab the value `D[k]` at the same key `k` they will each have a reference
|
||||
to the same value object, creating the potential for a data race (unless the intention is to only do
|
||||
read operations on the value object).
|
||||
to the same object, creating the potential for a data race (unless the intention is to only do
|
||||
read operations on that object).
|
||||
|
||||
The same note holds for `SharedRegister<T>` when `T` is a `struct` type with reference fields inside it.
|
||||
|
|
Загрузка…
Ссылка в новой задаче