-
Notifications
You must be signed in to change notification settings - Fork 461
Expand file tree
/
Copy pathExecuteStepInContext.cs
More file actions
293 lines (257 loc) · 11.5 KB
/
Copy pathExecuteStepInContext.cs
File metadata and controls
293 lines (257 loc) · 11.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using NUnit.Framework;
using NUnit.Framework.Interfaces;
using Unity.Netcode;
using Unity.Netcode.MultiprocessRuntimeTests;
using UnityEngine;
using UnityEngine.TestTools;
using Debug = UnityEngine.Debug;
/// <summary>
/// Allows for context based delegate execution.
/// Can specify where you want that lambda executed (client side? server side?) and it'll automatically wait for the end
/// of a clientRPC server side and vice versa.
/// todo this could be used as an in-game tool too? for protocols that require a lot of back and forth?
/// </summary>
public class ExecuteStepInContext : CustomYieldInstruction
{
public enum StepExecutionContext
{
Server,
Clients
}
[AttributeUsage(AttributeTargets.Method)]
public class MultiprocessContextBasedTestAttribute : NUnitAttribute, IOuterUnityTestAction
{
public IEnumerator BeforeTest(ITest test)
{
yield return new WaitUntil(() => TestCoordinator.Instance != null && HasRegistered);
}
public IEnumerator AfterTest(ITest test)
{
yield break;
}
}
private StepExecutionContext m_ActionContext;
private Action<byte[]> m_StepToExecute;
private string m_CurrentActionId;
// as a remote worker, I store all available actions so I can execute them when triggered from RPCs
public static Dictionary<string, ExecuteStepInContext> AllActions = new Dictionary<string, ExecuteStepInContext>();
private static Dictionary<string, int> s_MethodIdCounter = new Dictionary<string, int>();
private NetworkManager m_NetworkManager;
private bool m_IsRegistering;
private List<Func<bool>> m_ClientIsFinishedChecks = new List<Func<bool>>();
private Func<bool> m_AdditionalIsFinishedWaiter;
private bool m_WaitMultipleUpdates;
private bool m_IgnoreTimeoutException;
private float m_StartTime;
private bool isTimingOut => Time.time - m_StartTime > TestCoordinator.MaxWaitTimeoutSec;
private bool shouldExecuteLocally => (m_ActionContext == StepExecutionContext.Server && m_NetworkManager.IsServer) || (m_ActionContext == StepExecutionContext.Clients && !m_NetworkManager.IsServer);
public static bool IsRegistering;
public static bool HasRegistered;
private static List<object> s_AllClientTestInstances = new List<object>(); // to keep an instance for each tests, so captured context in each step is kept
/// <summary>
/// This MUST be called at the beginning of each test in order to use context based steps.
/// Assumes this is called from same callsite as ExecuteStepInContext (and assumes this is called from IEnumerator, the method full name is unique
/// even with the same method name and different parameters).
/// This relies on the name to be unique for each generated IEnumerator state machines
/// </summary>
public static void InitializeContextSteps()
{
var callerMethod = new StackFrame(1).GetMethod();
var methodIdentifier = GetMethodIdentifier(callerMethod); // since this is called from IEnumerator, this should be a generated class, making it unique
s_MethodIdCounter[methodIdentifier] = 0;
}
private static string GetMethodIdentifier(MethodBase callerMethod)
{
return callerMethod.DeclaringType.FullName;
}
internal static void InitializeAllSteps()
{
MultiprocessLogger.Log("InitializeAllSteps - Start");
// registering magically all context based steps
IsRegistering = true;
var registeredMethods = typeof(TestCoordinator).Assembly.GetTypes().SelectMany(t => t.GetMethods())
.Where(m => m.GetCustomAttributes(typeof(MultiprocessContextBasedTestAttribute), true).Length > 0)
.ToArray();
var typesWithContextMethods = new HashSet<Type>();
foreach (var method in registeredMethods)
{
typesWithContextMethods.Add(method.ReflectedType);
}
if (registeredMethods.Length == 0)
{
throw new Exception($"Couldn't find any registered methods for multiprocess testing. Is {nameof(TestCoordinator)} in same assembly as test methods?");
}
object[] GetParameterValuesToPassFunc(ParameterInfo[] parameterInfo)
{
object[] parametersToReturn = new object[parameterInfo.Length];
for (int i = 0; i < parameterInfo.Length; i++)
{
var paramType = parameterInfo[i].GetType();
object defaultObj = null;
if (paramType.IsValueType)
{
defaultObj = Activator.CreateInstance(paramType);
}
parametersToReturn[i] = defaultObj;
}
return parametersToReturn;
}
foreach (var contextType in typesWithContextMethods)
{
var allConstructors = contextType.GetConstructors();
if (allConstructors.Length > 1)
{
throw new NotImplementedException("Case not implemented where test has more than one constructor");
}
var instance = Activator.CreateInstance(contextType, allConstructors.Length > 0 ? GetParameterValuesToPassFunc(allConstructors[0].GetParameters()) : null);
s_AllClientTestInstances.Add(instance); // keeping that instance so tests can use captured local attributes
var typeMethodsWithContextSteps = new List<MethodInfo>();
foreach (var method in contextType.GetMethods())
{
if (method.GetCustomAttributes(typeof(MultiprocessContextBasedTestAttribute), true).Length > 0)
{
typeMethodsWithContextSteps.Add(method);
}
}
foreach (var method in typeMethodsWithContextSteps)
{
var parametersToPass = GetParameterValuesToPassFunc(method.GetParameters());
var enumerator = (IEnumerator)method.Invoke(instance, parametersToPass.ToArray());
while (enumerator.MoveNext()) { }
}
}
IsRegistering = false;
HasRegistered = true;
MultiprocessLogger.Log("InitializeAllSteps - Done");
}
/// <summary>
/// Executes an action with the specified context. This allows writing tests all in the same sequential flow,
/// making it more readable. This allows not having to jump between static client methods and test method
/// </summary>
/// <param name="actionContext">context to use. for example, should execute client side? server side?</param>
/// <param name="stepToExecute">action to execute</param>
/// <param name="ignoreTimeoutException">waits for timeout and just finishes step execution silently</param>
/// <param name="paramToPass">parameters to pass to action</param>
/// <param name="networkManager"></param>
/// <param name="waitMultipleUpdates"> waits multiple frames before allowing the execution to continue. This means ClientFinishedServerRpc must be called manually</param>
/// <param name="additionalIsFinishedWaiter"></param>
public ExecuteStepInContext(StepExecutionContext actionContext, Action<byte[]> stepToExecute, bool ignoreTimeoutException = false, byte[] paramToPass = default, NetworkManager networkManager = null, bool waitMultipleUpdates = false, Func<bool> additionalIsFinishedWaiter = null)
{
m_StartTime = Time.time;
m_IsRegistering = IsRegistering;
m_ActionContext = actionContext;
m_StepToExecute = stepToExecute;
m_WaitMultipleUpdates = waitMultipleUpdates;
m_IgnoreTimeoutException = ignoreTimeoutException;
if (additionalIsFinishedWaiter != null)
{
m_AdditionalIsFinishedWaiter = additionalIsFinishedWaiter;
}
if (networkManager == null)
{
networkManager = NetworkManager.Singleton;
}
m_NetworkManager = networkManager; // todo test using this for multiinstance tests too?
var callerMethod = new StackFrame(1).GetMethod(); // one skip frame for current method
var methodId = GetMethodIdentifier(callerMethod); // assumes called from IEnumerator MoveNext, which should be the case since we're a CustomYieldInstruction. This will return a generated class name which should be unique
if (!s_MethodIdCounter.ContainsKey(methodId))
{
s_MethodIdCounter[methodId] = 0;
}
string currentActionId = $"{methodId}-{s_MethodIdCounter[methodId]++}";
m_CurrentActionId = currentActionId;
if (m_IsRegistering)
{
Assert.That(AllActions, Does.Not.Contain(currentActionId)); // sanity check
AllActions[currentActionId] = this;
MultiprocessLogger.Log($"InitializeAllSteps - Registering {currentActionId}");
}
else
{
MultiprocessLogger.Log($"InitializeAllSteps - Not Registering {currentActionId}");
if (shouldExecuteLocally)
{
m_StepToExecute.Invoke(paramToPass);
}
else
{
if (networkManager.IsServer)
{
TestCoordinator.Instance.TriggerActionIdClientRpc(currentActionId, paramToPass, m_IgnoreTimeoutException,
clientRpcParams: new ClientRpcParams
{
Send = new ClientRpcSendParams { TargetClientIds = TestCoordinator.AllClientIdsExceptMine.ToArray() }
});
foreach (var clientId in TestCoordinator.AllClientIdsExceptMine)
{
m_ClientIsFinishedChecks.Add(TestCoordinator.ConsumeClientIsFinished(clientId));
}
}
else
{
throw new NotImplementedException();
}
}
}
}
public void Invoke(byte[] args)
{
m_StepToExecute.Invoke(args);
if (!m_WaitMultipleUpdates)
{
if (!m_NetworkManager.IsServer)
{
TestCoordinator.Instance.ClientFinishedServerRpc();
}
else
{
throw new NotImplementedException("todo implement");
}
}
}
public override bool keepWaiting
{
get
{
if (isTimingOut)
{
if (m_IgnoreTimeoutException)
{
Debug.LogWarning($"Timeout ignored for action ID {m_CurrentActionId}");
return false;
}
throw new Exception($"timeout for Context Step with action ID {m_CurrentActionId}");
}
if (m_AdditionalIsFinishedWaiter != null)
{
var isFinished = m_AdditionalIsFinishedWaiter.Invoke();
if (!isFinished)
{
return true;
}
}
if (m_IsRegistering || shouldExecuteLocally || m_ClientIsFinishedChecks == null)
{
return false;
}
for (int i = m_ClientIsFinishedChecks.Count - 1; i >= 0; i--)
{
if (m_ClientIsFinishedChecks[i].Invoke())
{
m_ClientIsFinishedChecks.RemoveAt(i);
}
else
{
return true;
}
}
return false;
}
}
}