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:
Chris Lovett 2020-03-11 20:48:21 +00:00
Родитель 605bf0349e
Коммит 5b662e28b2
6 изменённых файлов: 178 добавлений и 66 удалений

Просмотреть файл

@ -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.