test
This commit is contained in:
@@ -0,0 +1,489 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine.InputSystem.LowLevel;
|
||||
using Unity.Collections;
|
||||
using Unity.Collections.LowLevel.Unsafe;
|
||||
using UnityEngine.Analytics;
|
||||
using UnityEngine.InputSystem.Layouts;
|
||||
using UnityEngine.InputSystem.Utilities;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace UnityEngine.InputSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// An implementation of <see cref="IInputRuntime"/> for use during tests.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class is only available in the editor and in development players.
|
||||
///
|
||||
/// The test runtime replaces the services usually supplied by <see cref="UnityEngineInternal.Input.NativeInputSystem"/>.
|
||||
/// </remarks>
|
||||
/// <seealso cref="InputTestFixture.runtime"/>
|
||||
internal class InputTestRuntime : IInputRuntime, IDisposable
|
||||
{
|
||||
public unsafe delegate long DeviceCommandCallback(int deviceId, InputDeviceCommand* command);
|
||||
|
||||
~InputTestRuntime()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
|
||||
public int AllocateDeviceId()
|
||||
{
|
||||
var result = m_NextDeviceId;
|
||||
++m_NextDeviceId;
|
||||
return result;
|
||||
}
|
||||
|
||||
public unsafe void Update(InputUpdateType type)
|
||||
{
|
||||
if (!onShouldRunUpdate.Invoke(type))
|
||||
return;
|
||||
|
||||
lock (m_Lock)
|
||||
{
|
||||
if (type == InputUpdateType.Dynamic && !dontAdvanceUnscaledGameTimeNextDynamicUpdate)
|
||||
{
|
||||
unscaledGameTime += 1 / 30f;
|
||||
dontAdvanceUnscaledGameTimeNextDynamicUpdate = false;
|
||||
}
|
||||
|
||||
if (m_NewDeviceDiscoveries != null && m_NewDeviceDiscoveries.Count > 0)
|
||||
{
|
||||
if (onDeviceDiscovered != null)
|
||||
foreach (var entry in m_NewDeviceDiscoveries)
|
||||
onDeviceDiscovered(entry.Key, entry.Value);
|
||||
m_NewDeviceDiscoveries.Clear();
|
||||
}
|
||||
|
||||
onBeforeUpdate?.Invoke(type);
|
||||
|
||||
// Advance time *after* onBeforeUpdate so that events generated from onBeforeUpdate
|
||||
// don't get bumped into the following update.
|
||||
if (type == InputUpdateType.Dynamic && !dontAdvanceTimeNextDynamicUpdate)
|
||||
{
|
||||
currentTime += advanceTimeEachDynamicUpdate;
|
||||
dontAdvanceTimeNextDynamicUpdate = false;
|
||||
}
|
||||
|
||||
if (onUpdate != null)
|
||||
{
|
||||
var buffer = new InputEventBuffer(
|
||||
(InputEvent*)NativeArrayUnsafeUtility.GetUnsafeBufferPointerWithoutChecks(m_EventBuffer),
|
||||
m_EventCount, m_EventWritePosition, m_EventBuffer.Length);
|
||||
|
||||
try
|
||||
{
|
||||
onUpdate(type, ref buffer);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Same order as in NativeInputRuntime
|
||||
Debug.LogException(e);
|
||||
Debug.LogError($"{e.GetType().Name} during event processing of {type} update; resetting event buffer");
|
||||
|
||||
// Rethrow exception for test runtime to enable us to assert against it in tests.
|
||||
m_EventCount = 0;
|
||||
m_EventWritePosition = 0;
|
||||
throw;
|
||||
}
|
||||
|
||||
m_EventCount = buffer.eventCount;
|
||||
m_EventWritePosition = (int)buffer.sizeInBytes;
|
||||
|
||||
if (NativeArrayUnsafeUtility.GetUnsafeBufferPointerWithoutChecks(buffer.data) !=
|
||||
NativeArrayUnsafeUtility.GetUnsafeBufferPointerWithoutChecks(m_EventBuffer))
|
||||
m_EventBuffer = buffer.data;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_EventCount = 0;
|
||||
m_EventWritePosition = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe void QueueEvent(InputEvent* eventPtr)
|
||||
{
|
||||
var eventSize = eventPtr->sizeInBytes;
|
||||
var alignedEventSize = eventSize.AlignToMultipleOf(4);
|
||||
|
||||
lock (m_Lock)
|
||||
{
|
||||
eventPtr->eventId = m_NextEventId;
|
||||
eventPtr->handled = false;
|
||||
++m_NextEventId;
|
||||
|
||||
// Enlarge buffer, if we have to.
|
||||
if ((m_EventWritePosition + alignedEventSize) > m_EventBuffer.Length)
|
||||
{
|
||||
var newBufferSize = m_EventBuffer.Length + Mathf.Max((int)alignedEventSize, 1024);
|
||||
var newBuffer = new NativeArray<byte>(newBufferSize, Allocator.Persistent);
|
||||
UnsafeUtility.MemCpy(newBuffer.GetUnsafePtr(), m_EventBuffer.GetUnsafePtr(), m_EventWritePosition);
|
||||
m_EventBuffer.Dispose();
|
||||
m_EventBuffer = newBuffer;
|
||||
}
|
||||
|
||||
// Copy event.
|
||||
UnsafeUtility.MemCpy((byte*)m_EventBuffer.GetUnsafePtr() + m_EventWritePosition, eventPtr, eventSize);
|
||||
m_EventWritePosition += (int)alignedEventSize;
|
||||
++m_EventCount;
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe void SetCanRunInBackground(int deviceId)
|
||||
{
|
||||
SetDeviceCommandCallback(deviceId,
|
||||
(id, command) =>
|
||||
{
|
||||
if (command->type == QueryCanRunInBackground.Type)
|
||||
{
|
||||
((QueryCanRunInBackground*)command)->canRunInBackground = true;
|
||||
return InputDeviceCommand.GenericSuccess;
|
||||
}
|
||||
return InputDeviceCommand.GenericFailure;
|
||||
});
|
||||
}
|
||||
|
||||
public void SetDeviceCommandCallback(InputDevice device, DeviceCommandCallback callback)
|
||||
{
|
||||
SetDeviceCommandCallback(device.deviceId, callback);
|
||||
}
|
||||
|
||||
public void SetDeviceCommandCallback(int deviceId, DeviceCommandCallback callback)
|
||||
{
|
||||
lock (m_Lock)
|
||||
{
|
||||
if (m_DeviceCommandCallbacks == null)
|
||||
m_DeviceCommandCallbacks = new List<KeyValuePair<int, DeviceCommandCallback>>();
|
||||
else
|
||||
{
|
||||
for (var i = 0; i < m_DeviceCommandCallbacks.Count; ++i)
|
||||
{
|
||||
if (m_DeviceCommandCallbacks[i].Key == deviceId)
|
||||
{
|
||||
m_DeviceCommandCallbacks[i] = new KeyValuePair<int, DeviceCommandCallback>(deviceId, callback);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
m_DeviceCommandCallbacks.Add(new KeyValuePair<int, DeviceCommandCallback>(deviceId, callback));
|
||||
}
|
||||
}
|
||||
|
||||
public void SetDeviceCommandCallback<TCommand>(int deviceId, TCommand result)
|
||||
where TCommand : struct, IInputDeviceCommandInfo
|
||||
{
|
||||
bool? receivedCommand = null;
|
||||
unsafe
|
||||
{
|
||||
SetDeviceCommandCallback(deviceId,
|
||||
(id, commandPtr) =>
|
||||
{
|
||||
if (commandPtr->type == result.typeStatic)
|
||||
{
|
||||
Assert.That(receivedCommand.HasValue, Is.False);
|
||||
receivedCommand = true;
|
||||
UnsafeUtility.MemCpy(commandPtr, UnsafeUtility.AddressOf(ref result),
|
||||
UnsafeUtility.SizeOf<TCommand>());
|
||||
return InputDeviceCommand.GenericSuccess;
|
||||
}
|
||||
|
||||
return InputDeviceCommand.GenericFailure;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe long DeviceCommand(int deviceId, InputDeviceCommand* commandPtr)
|
||||
{
|
||||
lock (m_Lock)
|
||||
{
|
||||
if (commandPtr->type == QueryPairedUserAccountCommand.Type)
|
||||
{
|
||||
foreach (var pairing in userAccountPairings)
|
||||
{
|
||||
if (pairing.deviceId != deviceId)
|
||||
continue;
|
||||
|
||||
var queryPairedUser = (QueryPairedUserAccountCommand*)commandPtr;
|
||||
queryPairedUser->handle = pairing.userHandle;
|
||||
queryPairedUser->name = pairing.userName;
|
||||
queryPairedUser->id = pairing.userId;
|
||||
return (long)QueryPairedUserAccountCommand.Result.DevicePairedToUserAccount;
|
||||
}
|
||||
}
|
||||
|
||||
var result = InputDeviceCommand.GenericFailure;
|
||||
if (m_DeviceCommandCallbacks != null)
|
||||
foreach (var entry in m_DeviceCommandCallbacks)
|
||||
{
|
||||
if (entry.Key == deviceId)
|
||||
{
|
||||
result = entry.Value(deviceId, commandPtr);
|
||||
if (result >= 0)
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public void InvokePlayerFocusChanged(bool newFocusState)
|
||||
{
|
||||
m_HasFocus = newFocusState;
|
||||
onPlayerFocusChanged?.Invoke(newFocusState);
|
||||
}
|
||||
|
||||
public void PlayerFocusLost()
|
||||
{
|
||||
InvokePlayerFocusChanged(false);
|
||||
}
|
||||
|
||||
public void PlayerFocusGained()
|
||||
{
|
||||
InvokePlayerFocusChanged(true);
|
||||
}
|
||||
|
||||
public int ReportNewInputDevice(string deviceDescriptor, int deviceId = InputDevice.InvalidDeviceId)
|
||||
{
|
||||
lock (m_Lock)
|
||||
{
|
||||
if (deviceId == InputDevice.InvalidDeviceId)
|
||||
deviceId = AllocateDeviceId();
|
||||
if (m_NewDeviceDiscoveries == null)
|
||||
m_NewDeviceDiscoveries = new List<KeyValuePair<int, string>>();
|
||||
m_NewDeviceDiscoveries.Add(new KeyValuePair<int, string>(deviceId, deviceDescriptor));
|
||||
return deviceId;
|
||||
}
|
||||
}
|
||||
|
||||
public int ReportNewInputDevice(InputDeviceDescription description, int deviceId = InputDevice.InvalidDeviceId,
|
||||
ulong userHandle = 0, string userName = null, string userId = null)
|
||||
{
|
||||
deviceId = ReportNewInputDevice(description.ToJson(), deviceId);
|
||||
|
||||
// If we have user information, automatically set up
|
||||
if (userHandle != 0)
|
||||
AssociateInputDeviceWithUser(deviceId, userHandle, userName, userId);
|
||||
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
public int ReportNewInputDevice<TDevice>(int deviceId = InputDevice.InvalidDeviceId,
|
||||
ulong userHandle = 0, string userName = null, string userId = null)
|
||||
where TDevice : InputDevice
|
||||
{
|
||||
return ReportNewInputDevice(
|
||||
new InputDeviceDescription {deviceClass = typeof(TDevice).Name, interfaceName = "Test"}, deviceId,
|
||||
userHandle, userName, userId);
|
||||
}
|
||||
|
||||
public unsafe void ReportInputDeviceRemoved(int deviceId)
|
||||
{
|
||||
var removeEvent = DeviceRemoveEvent.Create(deviceId);
|
||||
var removeEventPtr = UnsafeUtility.AddressOf(ref removeEvent);
|
||||
QueueEvent((InputEvent*)removeEventPtr);
|
||||
}
|
||||
|
||||
public void ReportInputDeviceRemoved(InputDevice device)
|
||||
{
|
||||
if (device == null)
|
||||
throw new ArgumentNullException(nameof(device));
|
||||
ReportInputDeviceRemoved(device.deviceId);
|
||||
}
|
||||
|
||||
public void AssociateInputDeviceWithUser(int deviceId, ulong userHandle, string userName = null, string userId = null)
|
||||
{
|
||||
var existingIndex = -1;
|
||||
for (var i = 0; i < userAccountPairings.Count; ++i)
|
||||
if (userAccountPairings[i].deviceId == deviceId)
|
||||
{
|
||||
existingIndex = i;
|
||||
break;
|
||||
}
|
||||
|
||||
if (userHandle == 0)
|
||||
{
|
||||
if (existingIndex != -1)
|
||||
userAccountPairings.RemoveAt(existingIndex);
|
||||
}
|
||||
else if (existingIndex != -1)
|
||||
{
|
||||
userAccountPairings[existingIndex] =
|
||||
new PairedUser
|
||||
{
|
||||
deviceId = deviceId,
|
||||
userHandle = userHandle,
|
||||
userName = userName,
|
||||
userId = userId,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
userAccountPairings.Add(
|
||||
new PairedUser
|
||||
{
|
||||
deviceId = deviceId,
|
||||
userHandle = userHandle,
|
||||
userName = userName,
|
||||
userId = userId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void AssociateInputDeviceWithUser(InputDevice device, ulong userHandle, string userName = null, string userId = null)
|
||||
{
|
||||
AssociateInputDeviceWithUser(device.deviceId, userHandle, userName, userId);
|
||||
}
|
||||
|
||||
public struct PairedUser
|
||||
{
|
||||
public int deviceId;
|
||||
public ulong userHandle;
|
||||
public string userName;
|
||||
public string userId;
|
||||
}
|
||||
|
||||
public InputUpdateDelegate onUpdate { get; set; }
|
||||
public Action<InputUpdateType> onBeforeUpdate { get; set; }
|
||||
public Func<InputUpdateType, bool> onShouldRunUpdate { get; set; }
|
||||
#if UNITY_EDITOR
|
||||
public Action onPlayerLoopInitialization { get; set; }
|
||||
#endif
|
||||
public Action<int, string> onDeviceDiscovered { get; set; }
|
||||
public Action onShutdown { get; set; }
|
||||
public Action<bool> onPlayerFocusChanged { get; set; }
|
||||
public bool isPlayerFocused => m_HasFocus;
|
||||
public float pollingFrequency { get; set; }
|
||||
public double currentTime { get; set; }
|
||||
public double currentTimeForFixedUpdate { get; set; }
|
||||
public float unscaledGameTime { get; set; } = 1;
|
||||
public bool dontAdvanceUnscaledGameTimeNextDynamicUpdate { get; set; }
|
||||
|
||||
public double advanceTimeEachDynamicUpdate { get; set; } = 1.0 / 60;
|
||||
|
||||
public bool dontAdvanceTimeNextDynamicUpdate { get; set; }
|
||||
|
||||
public bool runInBackground { get; set; } = false;
|
||||
|
||||
public Vector2 screenSize { get; set; } = new Vector2(1024, 768);
|
||||
public ScreenOrientation screenOrientation { set; get; } = ScreenOrientation.Portrait;
|
||||
public bool normalizeScrollWheelDelta { get; set; } = true;
|
||||
public float scrollWheelDeltaPerTick { get; set; } = 1.0f;
|
||||
|
||||
public List<PairedUser> userAccountPairings
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_UserPairings == null)
|
||||
m_UserPairings = new List<PairedUser>();
|
||||
return m_UserPairings;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
m_EventBuffer.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public double currentTimeOffsetToRealtimeSinceStartup
|
||||
{
|
||||
get => m_CurrentTimeOffsetToRealtimeSinceStartup;
|
||||
set
|
||||
{
|
||||
m_CurrentTimeOffsetToRealtimeSinceStartup = value;
|
||||
InputRuntime.s_CurrentTimeOffsetToRealtimeSinceStartup = value;
|
||||
}
|
||||
}
|
||||
|
||||
public bool isInBatchMode { get; set; }
|
||||
|
||||
#if UNITY_EDITOR
|
||||
public bool isInPlayMode { get; set; } = true;
|
||||
public bool isPaused { get; set; }
|
||||
public bool isEditorActive { get; set; } = true;
|
||||
public Func<IntPtr, bool> onUnityRemoteMessage
|
||||
{
|
||||
get => m_UnityRemoteMessageHandler;
|
||||
set => m_UnityRemoteMessageHandler = value;
|
||||
}
|
||||
|
||||
public bool? unityRemoteGyroEnabled;
|
||||
public float? unityRemoteGyroUpdateInterval;
|
||||
|
||||
public void SetUnityRemoteGyroEnabled(bool value)
|
||||
{
|
||||
unityRemoteGyroEnabled = value;
|
||||
}
|
||||
|
||||
public void SetUnityRemoteGyroUpdateInterval(float interval)
|
||||
{
|
||||
unityRemoteGyroUpdateInterval = interval;
|
||||
}
|
||||
|
||||
public Action<PlayModeStateChange> onPlayModeChanged { get; set; }
|
||||
public Action onProjectChange { get; set; }
|
||||
#endif
|
||||
|
||||
public int eventCount => m_EventCount;
|
||||
|
||||
internal const int kDefaultEventBufferSize = 1024 * 512;
|
||||
|
||||
private bool m_HasFocus = true;
|
||||
private int m_NextDeviceId = 1;
|
||||
private int m_NextEventId = 1;
|
||||
internal int m_EventCount;
|
||||
private int m_EventWritePosition;
|
||||
private NativeArray<byte> m_EventBuffer = new NativeArray<byte>(kDefaultEventBufferSize, Allocator.Persistent);
|
||||
private List<PairedUser> m_UserPairings;
|
||||
private List<KeyValuePair<int, string>> m_NewDeviceDiscoveries;
|
||||
private List<KeyValuePair<int, DeviceCommandCallback>> m_DeviceCommandCallbacks;
|
||||
private object m_Lock = new object();
|
||||
private double m_CurrentTimeOffsetToRealtimeSinceStartup;
|
||||
private Func<IntPtr, bool> m_UnityRemoteMessageHandler;
|
||||
|
||||
#if UNITY_ANALYTICS || UNITY_EDITOR
|
||||
|
||||
public Action<string, int, int> onRegisterAnalyticsEvent { get; set; }
|
||||
public Action<string, object> onSendAnalyticsEvent { get; set; }
|
||||
|
||||
public void SendAnalytic(InputAnalytics.IInputAnalytic analytic)
|
||||
{
|
||||
#if UNITY_2023_2_OR_NEWER
|
||||
|
||||
// Mimic editor analytics for Unity 2023.2+ invoking TryGatherData to send
|
||||
var analyticInfoAttribute = analytic.GetType().GetCustomAttributes(
|
||||
typeof(AnalyticInfoAttribute), true).FirstOrDefault() as AnalyticInfoAttribute;
|
||||
var info = analytic.info;
|
||||
#if UNITY_EDITOR
|
||||
// Registration handled by framework
|
||||
#else
|
||||
onRegisterAnalyticsEvent?.Invoke(info.Name, info.MaxEventsPerHour, info.MaxNumberOfElements); // only to avoid writing two tests per Unity version (registration handled by framework)
|
||||
#endif
|
||||
if (analytic.TryGatherData(out var data, out var ex) && data != null && analyticInfoAttribute != null)
|
||||
onSendAnalyticsEvent?.Invoke(analyticInfoAttribute.eventName, data);
|
||||
else if (ex != null)
|
||||
throw ex; // rethrow for visibility in test scope
|
||||
|
||||
#else
|
||||
|
||||
var info = analytic.info;
|
||||
onRegisterAnalyticsEvent?.Invoke(info.Name, info.MaxEventsPerHour, info.MaxNumberOfElements);
|
||||
|
||||
if (analytic.TryGatherData(out var data, out var error))
|
||||
onSendAnalyticsEvent?.Invoke(info.Name, data);
|
||||
else
|
||||
throw error; // For visibility in tests
|
||||
|
||||
#endif // UNITY_2023_2_OR_NEWER
|
||||
}
|
||||
|
||||
#endif // UNITY_ANALYTICS || UNITY_EDITOR
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user