using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using UnityEngine.InputSystem.Controls; using NUnit.Framework; using NUnit.Framework.Constraints; using NUnit.Framework.Internal; using Unity.Collections; using UnityEngine.InputSystem.LowLevel; using UnityEngine.InputSystem.Utilities; using UnityEngine.TestTools; using UnityEngine.TestTools.Utils; #if UNITY_EDITOR using UnityEditor; using UnityEngine.InputSystem.Editor; #endif ////TODO: must allow running UnityTests which means we have to be able to get per-frame updates yet not receive input from native ////TODO: when running tests in players, make sure that remoting is turned off ////REVIEW: always enable event diagnostics in InputTestFixture? namespace UnityEngine.InputSystem { /// /// A test fixture for writing tests that use the input system. Can be derived from /// or simply instantiated from another test fixture. /// /// /// The fixture will put the input system into a known state where it has only the /// built-in set of basic layouts and no devices. The state of the system before /// starting a test is recorded and restored when the test finishes. /// /// /// /// public class MyInputTests : InputTestFixture /// { /// public override void Setup() /// { /// base.Setup(); /// /// InputSystem.RegisterLayout<MyDevice>(); /// } /// /// [Test] /// public void CanCreateMyDevice() /// { /// InputSystem.AddDevice<MyDevice>(); /// Assert.That(InputSystem.devices, Has.Exactly(1).TypeOf<MyDevice>()); /// } /// } /// /// /// /// The test fixture will also sever the tie of the input system to the Unity runtime. /// This means that while the test fixture is active, the input system will not receive /// input and device discovery or removal notifications from platform code. This ensures /// that while the test is running, input that may be generated on the machine running /// the test will not infer with it. /// public class InputTestFixture { /// /// Put into a known state where it only has a basic set of /// layouts and does not have any input devices. /// /// /// If you derive your own test fixture directly from InputTestFixture, this /// method will automatically be called. If you embed InputTestFixture into /// your fixture, you have to explicitly call this method yourself. /// /// [SetUp] public virtual void Setup() { try { // Apparently, NUnit is reusing instances :( m_KeyInfos = default; m_IsUnityTest = default; m_CurrentTest = default; // Disable input debugger so we don't waste time responding to all the // input system activity from the tests. #if UNITY_EDITOR InputDebuggerWindow.Disable(); #endif runtime = new InputTestRuntime(); // Push current input system state on stack. #if DEVELOPMENT_BUILD || UNITY_EDITOR InputSystem.SaveAndReset(enableRemoting: false, runtime: runtime); #endif // Override the editor messing with logic like canRunInBackground and focus and // make it behave like in the player. #if UNITY_EDITOR InputSystem.settings.editorInputBehaviorInPlayMode = InputSettings.EditorInputBehaviorInPlayMode.AllDeviceInputAlwaysGoesToGameView; #endif // For a [UnityTest] play mode test, we don't want editor updates interfering with the test, // so turn them off. #if UNITY_EDITOR if (Application.isPlaying && IsUnityTest()) InputSystem.s_Manager.m_UpdateMask &= ~InputUpdateType.Editor; #endif // We use native collections in a couple places. We when leak them, we want to know where exactly // the allocation came from so enable full leak detection in tests. NativeLeakDetection.Mode = NativeLeakDetectionMode.EnabledWithStackTrace; // For [UnityTest]s, we need to process input in sync with the player loop. However, InputTestRuntime // is divorced from the player loop by virtue of not being tied into NativeInputSystem. Listen // for NativeInputSystem.Update here and trigger input processing in our isolated InputSystem. // This is irrelevant for normal [Test]s but for [UnityTest]s that run over several frames, it's crucial. // NOTE: We're severing the tie the previous InputManager had to NativeInputRuntime here. This means that // device removal events that happen to occur while tests are running will get lost. NativeInputRuntime.instance.onUpdate = (InputUpdateType updateType, ref InputEventBuffer buffer) => { if (InputSystem.s_Manager.ShouldRunUpdate(updateType)) InputSystem.Update(updateType); // We ignore any input coming from native. buffer.Reset(); }; NativeInputRuntime.instance.onShouldRunUpdate = updateType => true; #if UNITY_EDITOR m_OnPlayModeStateChange = OnPlayModeStateChange; EditorApplication.playModeStateChanged += m_OnPlayModeStateChange; #endif // Always want to merge by default InputSystem.settings.disableRedundantEventsMerging = false; // Turn on all optimizations and checks InputSystem.settings.SetInternalFeatureFlag(InputFeatureNames.kUseOptimizedControls, true); InputSystem.settings.SetInternalFeatureFlag(InputFeatureNames.kUseReadValueCaching, true); InputSystem.settings.SetInternalFeatureFlag(InputFeatureNames.kParanoidReadValueCachingChecks, true); #if UNITY_EDITOR // Default mock dialogs to avoid unexpected cancellation of standard flows Dialog.InputActionAsset.SetSaveChanges((_) => Dialog.Result.Discard); Dialog.InputActionAsset.SetDiscardUnsavedChanges((_) => Dialog.Result.Discard); Dialog.InputActionAsset.SetCreateAndOverwriteExistingAsset((_) => Dialog.Result.Discard); Dialog.ControlScheme.SetDeleteControlScheme((_) => Dialog.Result.Delete); #endif } catch (Exception exception) { Debug.LogError("Failed to set up input system for test " + TestContext.CurrentContext.Test.Name); Debug.LogException(exception); throw; } m_Initialized = true; if (InputSystem.devices.Count > 0) Assert.Fail("Input system should not have devices after reset"); } /// /// Restore the state of the input system it had when the test was started. /// /// [TearDown] public virtual void TearDown() { if (!m_Initialized) return; try { #if DEVELOPMENT_BUILD || UNITY_EDITOR InputSystem.Restore(); #endif runtime.Dispose(); // Unhook from play mode state changes. #if UNITY_EDITOR if (m_OnPlayModeStateChange != null) EditorApplication.playModeStateChanged -= m_OnPlayModeStateChange; #endif // Re-enable input debugger. #if UNITY_EDITOR InputDebuggerWindow.Enable(); #endif #if UNITY_EDITOR // Re-enable dialogs. Dialog.InputActionAsset.SetSaveChanges(null); Dialog.InputActionAsset.SetDiscardUnsavedChanges(null); Dialog.InputActionAsset.SetCreateAndOverwriteExistingAsset(null); Dialog.ControlScheme.SetDeleteControlScheme(null); #endif } catch (Exception exception) { Debug.LogError("Failed to shut down and restore input system after test " + TestContext.CurrentContext.Test.Name); Debug.LogException(exception); throw; } m_Initialized = false; } private bool? m_IsUnityTest; private Test m_CurrentTest; // True if the current test is a [UnityTest]. private bool IsUnityTest() { // We cache this value so that any call after the first in a test no // longer allocates GC memory. Otherwise we'll run into trouble with // DoesNotAllocate tests. var test = TestContext.CurrentTestExecutionContext.CurrentTest; if (m_IsUnityTest.HasValue && m_CurrentTest == test) return m_IsUnityTest.Value; var className = test.ClassName; var methodName = test.MethodName; // Doesn't seem like there's a proper way to get the current test method based on // the information provided by NUnit (see https://github.com/nunit/nunit/issues/3354). var type = Type.GetType(className); if (type == null) { foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) { type = assembly.GetType(className); if (type != null) break; } } if (type == null) { m_IsUnityTest = false; } else { var method = type.GetMethod(methodName); m_IsUnityTest = method?.GetCustomAttribute() != null; } m_CurrentTest = test; return m_IsUnityTest.Value; } #if UNITY_EDITOR private Action m_OnPlayModeStateChange; private void OnPlayModeStateChange(PlayModeStateChange change) { if (change == PlayModeStateChange.ExitingPlayMode && m_Initialized) TearDown(); } #endif // ReSharper disable once MemberCanBeProtected.Global public static void AssertButtonPress(InputDevice device, TState state, params ButtonControl[] buttons) where TState : struct, IInputStateTypeInfo { // Update state. InputSystem.QueueStateEvent(device, state); InputSystem.Update(); // Now verify that only the buttons we expect to be pressed are pressed. foreach (var control in device.allControls) { if (!(control is ButtonControl controlAsButton)) continue; var isInList = buttons.Contains(controlAsButton); if (!isInList) Assert.That(controlAsButton.isPressed, Is.False, $"Expected button {controlAsButton} to NOT be pressed"); else Assert.That(controlAsButton.isPressed, Is.True, $"Expected button {controlAsButton} to be pressed"); } } public static void AssertStickValues(StickControl stick, Vector2 stickValue, float up, float down, float left, float right) { Assert.That(stick.ReadUnprocessedValue(), Is.EqualTo(stickValue)); Assert.That(stick.up.ReadUnprocessedValue(), Is.EqualTo(up).Within(0.0001), "Incorrect 'up' value"); Assert.That(stick.down.ReadUnprocessedValue(), Is.EqualTo(down).Within(0.0001), "Incorrect 'down' value"); Assert.That(stick.left.ReadUnprocessedValue(), Is.EqualTo(left).Within(0.0001), "Incorrect 'left' value"); Assert.That(stick.right.ReadUnprocessedValue(), Is.EqualTo(right).Within(0.0001), "Incorrect 'right' value"); } private Dictionary> m_KeyInfos; private bool m_Initialized; /// /// Set of the given keyboard. /// /// Name of the keyboard layout to switch to. /// Keyboard to switch layout on. If null, is used. /// and are both null. /// /// Also queues and immediately processes an for the keyboard. /// public unsafe void SetKeyboardLayout(string name, Keyboard keyboard = null) { if (keyboard == null) { keyboard = Keyboard.current; if (keyboard == null) throw new ArgumentException("No keyboard has been created and no keyboard has been given", nameof(keyboard)); } runtime.SetDeviceCommandCallback(keyboard, (id, command) => { if (id == QueryKeyboardLayoutCommand.Type) { var commandPtr = (QueryKeyboardLayoutCommand*)command; commandPtr->WriteLayoutName(name); return InputDeviceCommand.GenericSuccess; } return InputDeviceCommand.GenericFailure; }); // Make sure caches on keys are flushed. InputSystem.QueueConfigChangeEvent(Keyboard.current); InputSystem.Update(); } /// /// Set the of on the current /// to be . /// /// Key to set the display name for. /// Display name for the key. /// Optional to report for the key. /// /// Automatically adds a if none has been added yet. /// public unsafe void SetKeyInfo(Key key, string displayName, int scanCode = 0) { if (Keyboard.current == null) InputSystem.AddDevice(); if (m_KeyInfos == null) { m_KeyInfos = new Dictionary>(); runtime.SetDeviceCommandCallback(Keyboard.current, (id, commandPtr) => { if (commandPtr->type == QueryKeyNameCommand.Type) { var keyNameCommand = (QueryKeyNameCommand*)commandPtr; if (m_KeyInfos.TryGetValue((Key)keyNameCommand->scanOrKeyCode, out var info)) { keyNameCommand->scanOrKeyCode = info.Item2; StringHelpers.WriteStringToBuffer(info.Item1, (IntPtr)keyNameCommand->nameBuffer, QueryKeyNameCommand.kMaxNameLength); } return QueryKeyNameCommand.kSize; } return InputDeviceCommand.GenericFailure; }); } m_KeyInfos[key] = new Tuple(displayName, scanCode); // Make sure caches on keys are flushed. InputSystem.QueueConfigChangeEvent(Keyboard.current); InputSystem.Update(); } /// /// Add support for to and return /// as . /// /// internal unsafe void SetCanRunInBackground(InputDevice device, bool canRunInBackground = true) { runtime.SetDeviceCommandCallback(device, (id, command) => { if (command->type == QueryCanRunInBackground.Type) { ((QueryCanRunInBackground*)command)->canRunInBackground = canRunInBackground; return InputDeviceCommand.GenericSuccess; } return InputDeviceCommand.GenericFailure; }); } public ActionConstraint Started(InputAction action, InputControl control = null, double? time = null, object value = null) { return new ActionConstraint(InputActionPhase.Started, action, control, time: time, duration: 0, value: value); } public ActionConstraint Started(InputAction action, InputControl control, TValue value, double? time = null) where TValue : struct { return new ActionConstraint(InputActionPhase.Started, action, control, value, time: time, duration: 0); } public ActionConstraint Performed(InputAction action, InputControl control = null, double? time = null, double? duration = null, object value = null) { return new ActionConstraint(InputActionPhase.Performed, action, control, time: time, duration: duration, value: value); } public ActionConstraint Performed(InputAction action, InputControl control, TValue value, double? time = null, double? duration = null) where TValue : struct { return new ActionConstraint(InputActionPhase.Performed, action, control, value, time: time, duration: duration); } public ActionConstraint Canceled(InputAction action, InputControl control = null, double? time = null, double? duration = null, object value = null) { return new ActionConstraint(InputActionPhase.Canceled, action, control, time: time, duration: duration, value: value); } public ActionConstraint Canceled(InputAction action, InputControl control, TValue value, double? time = null, double? duration = null) where TValue : struct { return new ActionConstraint(InputActionPhase.Canceled, action, control, value, time: time, duration: duration); } public ActionConstraint Started(InputAction action, InputControl control = null, object value = null, double? time = null) where TInteraction : IInputInteraction { return new ActionConstraint(InputActionPhase.Started, action, control, interaction: typeof(TInteraction), time: time, duration: 0, value: value); } public ActionConstraint Performed(InputAction action, InputControl control = null, object value = null, double? time = null, double? duration = null) where TInteraction : IInputInteraction { return new ActionConstraint(InputActionPhase.Performed, action, control, interaction: typeof(TInteraction), time: time, duration: duration, value: value); } public ActionConstraint Canceled(InputAction action, InputControl control = null, object value = null, double? time = null, double? duration = null) where TInteraction : IInputInteraction { return new ActionConstraint(InputActionPhase.Canceled, action, control, interaction: typeof(TInteraction), time: time, duration: duration, value: value); } ////REVIEW: Should we determine queueEventOnly automatically from whether we're in a UnityTest? // ReSharper disable once MemberCanBeProtected.Global public void Press(ButtonControl button, double time = -1, double timeOffset = 0, bool queueEventOnly = false) { Set(button, 1, time, timeOffset, queueEventOnly: queueEventOnly); } // ReSharper disable once MemberCanBeProtected.Global public void Release(ButtonControl button, double time = -1, double timeOffset = 0, bool queueEventOnly = false) { Set(button, 0, time, timeOffset, queueEventOnly: queueEventOnly); } // ReSharper disable once MemberCanBePrivate.Global public void PressAndRelease(ButtonControl button, double time = -1, double timeOffset = 0, bool queueEventOnly = false) { Press(button, time, timeOffset, queueEventOnly: true); // This one is always just a queue. Release(button, time, timeOffset, queueEventOnly: queueEventOnly); } // ReSharper disable once MemberCanBeProtected.Global public void Click(ButtonControl button, double time = -1, double timeOffset = 0, bool queueEventOnly = false) { PressAndRelease(button, time, timeOffset, queueEventOnly: queueEventOnly); } /// /// Set the control with the given on to the given /// by sending a state event with the value to the device. /// /// Device on which to find a control. /// Path of the control on the device. /// New state for the control. /// Timestamp to use for the state event. If -1 (default), current time is used (see ). /// Offset to apply to the current time. This is an alternative to . By default, no offset is applied. /// If true, no will be performed after queueing the event. This will only put /// the state event on the event queue and not do anything else. The default is to call after queuing the event. /// Note that not issuing an update means the state of the device will not change yet. This may affect subsequent Set/Press/Release/etc calls /// as they will not yet see the state change. /// /// Note that this parameter will be ignored if the test is a [UnityTest]. Multi-frame /// playmode tests will automatically process input as part of the Unity player loop. /// Value type of the control. /// /// /// var device = InputSystem.AddDevice("TestDevice"); /// Set<ButtonControl>(device, "button", 1); /// Set<AxisControl>(device, "{Primary2DMotion}/x", 123.456f); /// /// public void Set(InputDevice device, string path, TValue state, double time = -1, double timeOffset = 0, bool queueEventOnly = false) where TValue : struct { if (device == null) throw new ArgumentNullException(nameof(device)); if (string.IsNullOrEmpty(path)) throw new ArgumentNullException(nameof(path)); var control = (InputControl)device[path]; Set(control, state, time, timeOffset, queueEventOnly); } /// /// Set the control to the given value by sending a state event with the value to the /// control's device. /// /// An input control on a device that has been added to the system. /// New value for the input control. /// Timestamp to use for the state event. If -1 (default), current time is used (see ). /// Offset to apply to the current time. This is an alternative to . By default, no offset is applied. /// If true, no will be performed after queueing the event. This will only put /// the state event on the event queue and not do anything else. The default is to call after queuing the event. /// Note that not issuing an update means the state of the device will not change yet. This may affect subsequent Set/Press/Release/etc calls /// as they will not yet see the state change. /// /// Note that this parameter will be ignored if the test is a [UnityTest]. Multi-frame /// playmode tests will automatically process input as part of the Unity player loop. /// Value type of the given control. /// /// /// var gamepad = InputSystem.AddDevice<Gamepad>(); /// Set(gamepad.leftButton, 1); /// /// public void Set(InputControl control, TValue state, double time = -1, double timeOffset = 0, bool queueEventOnly = false) where TValue : struct { if (control == null) throw new ArgumentNullException(nameof(control)); if (!control.device.added) throw new ArgumentException( $"Device of control '{control}' has not been added to the system", nameof(control)); if (IsUnityTest()) queueEventOnly = true; void SetUpAndQueueEvent(InputEventPtr eventPtr) { eventPtr.time = (time >= 0 ? time : InputState.currentTime) + timeOffset; control.WriteValueIntoEvent(state, eventPtr); InputSystem.QueueEvent(eventPtr); } // Touchscreen does not support delta events involving TouchState. if (control is TouchControl) { using (StateEvent.From(control.device, out var eventPtr)) SetUpAndQueueEvent(eventPtr); } else { // We use delta state events rather than full state events here to mitigate the following problem: // Grabbing state from the device will preserve the current values of controls covered in the state. // However, running an update may alter the value of one or more of those controls. So with a full // state event, we may be writing outdated data back into the device. For example, in the case of delta // controls which will reset in OnBeforeUpdate(). // // Using delta events, we may still grab state outside of just the one control in case we're looking at // bit-addressed controls but at least we can avoid the problem for the majority of controls. using (DeltaStateEvent.From(control, out var eventPtr)) SetUpAndQueueEvent(eventPtr); } if (!queueEventOnly) InputSystem.Update(); } public void Move(InputControl positionControl, Vector2 position, Vector2? delta = null, double time = -1, double timeOffset = 0, bool queueEventOnly = false) { Set(positionControl, position, time: time, timeOffset: timeOffset, queueEventOnly: true); var deltaControl = (Vector2Control)positionControl.device.TryGetChildControl("delta"); if (deltaControl != null) Set(deltaControl, delta ?? position - positionControl.ReadValue(), time: time, timeOffset: timeOffset, queueEventOnly: true); if (!queueEventOnly) InputSystem.Update(); } ////TODO: obsolete this one in 2.0 and use pressure=1 default value public void BeginTouch(int touchId, Vector2 position, bool queueEventOnly = false, Touchscreen screen = null, double time = -1, double timeOffset = 0, byte displayIndex = 0) { SetTouch(touchId, TouchPhase.Began, position, 1, queueEventOnly: queueEventOnly, screen: screen, time: time, timeOffset: timeOffset, displayIndex: displayIndex); } public void BeginTouch(int touchId, Vector2 position, float pressure, bool queueEventOnly = false, Touchscreen screen = null, double time = -1, double timeOffset = 0) { SetTouch(touchId, TouchPhase.Began, position, pressure, queueEventOnly: queueEventOnly, screen: screen, time: time, timeOffset: timeOffset); } ////TODO: obsolete this one in 2.0 and use pressure=1 default value public void MoveTouch(int touchId, Vector2 position, Vector2 delta = default, bool queueEventOnly = false, Touchscreen screen = null, double time = -1, double timeOffset = 0) { SetTouch(touchId, TouchPhase.Moved, position, 1, delta, queueEventOnly: queueEventOnly, screen: screen, time: time, timeOffset: timeOffset); } public void MoveTouch(int touchId, Vector2 position, float pressure, Vector2 delta = default, bool queueEventOnly = false, Touchscreen screen = null, double time = -1, double timeOffset = 0) { SetTouch(touchId, TouchPhase.Moved, position, pressure, delta, queueEventOnly, screen: screen, time: time, timeOffset: timeOffset); } ////TODO: obsolete this one in 2.0 and use pressure=1 default value public void EndTouch(int touchId, Vector2 position, Vector2 delta = default, bool queueEventOnly = false, Touchscreen screen = null, double time = -1, double timeOffset = 0, byte displayIndex = 0) { SetTouch(touchId, TouchPhase.Ended, position, 1, delta, queueEventOnly: queueEventOnly, screen: screen, time: time, timeOffset: timeOffset, displayIndex: displayIndex); } public void EndTouch(int touchId, Vector2 position, float pressure, Vector2 delta = default, bool queueEventOnly = false, Touchscreen screen = null, double time = -1, double timeOffset = 0) { SetTouch(touchId, TouchPhase.Ended, position, pressure, delta, queueEventOnly, screen: screen, time: time, timeOffset: timeOffset); } ////TODO: obsolete this one in 2.0 and use pressure=1 default value public void CancelTouch(int touchId, Vector2 position, Vector2 delta = default, bool queueEventOnly = false, Touchscreen screen = null, double time = -1, double timeOffset = 0) { SetTouch(touchId, TouchPhase.Canceled, position, delta, queueEventOnly: queueEventOnly, screen: screen, time: time, timeOffset: timeOffset); } public void CancelTouch(int touchId, Vector2 position, float pressure, Vector2 delta = default, bool queueEventOnly = false, Touchscreen screen = null, double time = -1, double timeOffset = 0) { SetTouch(touchId, TouchPhase.Canceled, position, pressure, delta, queueEventOnly, screen: screen, time: time, timeOffset: timeOffset); } ////TODO: obsolete this one in 2.0 and use pressure=1 default value public void SetTouch(int touchId, TouchPhase phase, Vector2 position, Vector2 delta = default, bool queueEventOnly = true, Touchscreen screen = null, double time = -1, double timeOffset = 0) { SetTouch(touchId, phase, position, 1, delta: delta, queueEventOnly: queueEventOnly, screen: screen, time: time, timeOffset: timeOffset); } public void SetTouch(int touchId, TouchPhase phase, Vector2 position, float pressure, Vector2 delta = default, bool queueEventOnly = true, Touchscreen screen = null, double time = -1, double timeOffset = 0, byte displayIndex = 0) { if (screen == null) { screen = Touchscreen.current; if (screen == null) screen = InputSystem.AddDevice(); } InputSystem.QueueStateEvent(screen, new TouchState { touchId = touchId, phase = phase, position = position, delta = delta, pressure = pressure, displayIndex = displayIndex, }, (time >= 0 ? time : InputState.currentTime) + timeOffset); if (!queueEventOnly) InputSystem.Update(); } public void Trigger(InputAction action, InputControl control, TValue value) where TValue : struct { throw new NotImplementedException(); } /// /// Perform the input action without having to know what it is bound to. /// /// An input action that is currently enabled and has controls it is bound to. /// /// Blindly triggering an action requires making a few assumptions. Actions are not built to be able to trigger /// without any input. This means that this method has to generate input on a control that the action is bound to. /// /// Note that this method has no understanding of the interactions that may be present on the action and thus /// does not know how they may affect the triggering of the action. /// public void Trigger(InputAction action) { if (action == null) throw new ArgumentNullException(nameof(action)); if (!action.enabled) throw new ArgumentException( $"Action '{action}' must be enabled in order to be able to trigger it", nameof(action)); var controls = action.controls; if (controls.Count == 0) throw new ArgumentException( $"Action '{action}' must be bound to controls in order to be able to trigger it", nameof(action)); // See if we have a button we can trigger. for (var i = 0; i < controls.Count; ++i) { if (!(controls[i] is ButtonControl button)) continue; // Press and release button. Set(button, 1); Set(button, 0); return; } // See if we have an axis we can slide a bit. for (var i = 0; i < controls.Count; ++i) { if (!(controls[i] is AxisControl axis)) continue; // We do, so nudge its value a bit. Set(axis, axis.ReadValue() + 0.01f); return; } ////TODO: support a wider range of controls throw new NotImplementedException(); } /// /// The input runtime used during testing. /// internal InputTestRuntime runtime { get; private set; } /// /// Get or set the current time used by the input system. /// /// Current time used by the input system. public double currentTime { get => runtime.currentTime - runtime.currentTimeOffsetToRealtimeSinceStartup; set { runtime.currentTime = value + runtime.currentTimeOffsetToRealtimeSinceStartup; runtime.dontAdvanceTimeNextDynamicUpdate = true; } } internal float unscaledGameTime { get => runtime.unscaledGameTime; set { runtime.unscaledGameTime = value; runtime.dontAdvanceUnscaledGameTimeNextDynamicUpdate = true; } } public class ActionConstraint : Constraint { public InputActionPhase phase { get; set; } public double? time { get; set; } public double? duration { get; set; } public InputAction action { get; set; } public InputControl control { get; set; } public object value { get; set; } public Type interaction { get; set; } private readonly List m_AndThen = new List(); public ActionConstraint(InputActionPhase phase, InputAction action, InputControl control, object value = null, Type interaction = null, double? time = null, double? duration = null) { this.phase = phase; this.time = time; this.duration = duration; this.action = action; this.control = control; this.value = value; this.interaction = interaction; var interactionText = string.Empty; if (interaction != null) interactionText = InputInteraction.GetDisplayName(interaction); var actionName = action.actionMap != null ? $"{action.actionMap}/{action.name}" : action.name; // Use same text format as InputActionTrace for easier comparison. var description = $"{{ action={actionName} phase={phase}"; if (time != null) description += $" time={time}"; if (control != null) description += $" control={control}"; if (value != null) description += $" value={value}"; if (interaction != null) description += $" interaction={interactionText}"; if (duration != null) description += $" duration={duration}"; description += " }"; Description = description; } public override ConstraintResult ApplyTo(object actual) { var trace = (InputActionTrace)actual; var actions = trace.ToArray(); if (actions.Length == 0) return new ConstraintResult(this, actual, false); if (!Verify(actions[0])) return new ConstraintResult(this, actual, false); var i = 1; foreach (var constraint in m_AndThen) { if (i >= actions.Length || !constraint.Verify(actions[i])) return new ConstraintResult(this, actual, false); ++i; } if (i != actions.Length) return new ConstraintResult(this, actual, false); return new ConstraintResult(this, actual, true); } private bool Verify(InputActionTrace.ActionEventPtr eventPtr) { // NOTE: Using explicit "return false" branches everywhere for easier setting of breakpoints. if (eventPtr.action != action || eventPtr.phase != phase) return false; // Check time. if (time != null && !Mathf.Approximately((float)time.Value, (float)eventPtr.time)) return false; // Check duration. if (duration != null && !Mathf.Approximately((float)duration.Value, (float)eventPtr.duration)) return false; // Check control. if (control != null && eventPtr.control != control) return false; // Check interaction. if (interaction != null && (eventPtr.interaction == null || !interaction.IsInstanceOfType(eventPtr.interaction))) return false; // Check value. if (value != null) { var val = eventPtr.ReadValueAsObject(); if (val is float f) { if (!Mathf.Approximately(f, Convert.ToSingle(value))) return false; } else if (val is double d) { if (!Mathf.Approximately((float)d, (float)Convert.ToDouble(value))) return false; } else if (val is Vector2 v2) { if (!Vector2EqualityComparer.Instance.Equals(v2, value.As())) return false; } else if (val is Vector3 v3) { if (!Vector3EqualityComparer.Instance.Equals(v3, value.As())) return false; } else if (!val.Equals(value)) return false; } return true; } public ActionConstraint AndThen(ActionConstraint constraint) { m_AndThen.Add(constraint); Description += " and\n"; Description += constraint.Description; return this; } } #if UNITY_EDITOR internal void SimulateDomainReload() { // This quite invasively goes into InputSystem internals. Unfortunately, we // have no proper way of simulating domain reloads ATM. So we directly call various // internal methods here in a sequence similar to what we'd get during a domain reload. InputSystem.s_SystemObject.OnBeforeSerialize(); InputSystem.s_SystemObject = null; InputSystem.InitializeInEditor(runtime); } #endif #if UNITY_EDITOR /// /// Represents an analytics registration event captured by test harness. /// protected struct AnalyticsRegistrationEventData { public AnalyticsRegistrationEventData(string name, int maxPerHour, int maxPropertiesPerEvent) { this.name = name; this.maxPerHour = maxPerHour; this.maxPropertiesPerEvent = maxPropertiesPerEvent; } public readonly string name; public readonly int maxPerHour; public readonly int maxPropertiesPerEvent; } /// /// Represents an analytics data event captured by test harness. /// protected struct AnalyticsEventData { public AnalyticsEventData(string name, object data) { this.name = name; this.data = data; } public readonly string name; public readonly object data; } private List m_RegisteredAnalytics; private List m_SentAnalyticsEvents; /// /// Returns a read-only list of all analytics events registred by enabling capture via . /// protected IReadOnlyList registeredAnalytics => m_RegisteredAnalytics; /// /// Returns a read-only list of all analytics events captured by enabling capture via . /// protected IReadOnlyList sentAnalyticsEvents => m_SentAnalyticsEvents; /// /// Set up the test fixture to collect analytics registrations and events /// /// A filter predicate evaluating whether the given analytics name should be accepted to be stored in test fixture. protected void CollectAnalytics(Predicate analyticsNameFilter) { // Make sure containers are initialized and create them if not. Otherwise just clear to avoid allocation. if (m_RegisteredAnalytics == null) m_RegisteredAnalytics = new List(); else m_RegisteredAnalytics.Clear(); if (m_SentAnalyticsEvents == null) m_SentAnalyticsEvents = new List(); else m_SentAnalyticsEvents.Clear(); // Store registered analytics when called if filter applies runtime.onRegisterAnalyticsEvent = (name, maxPerHour, maxPropertiesPerEvent) => { if (analyticsNameFilter(name)) m_RegisteredAnalytics.Add(new AnalyticsRegistrationEventData(name: name, maxPerHour: maxPerHour, maxPropertiesPerEvent: maxPropertiesPerEvent)); }; // Store sent analytic events when called if filter applies runtime.onSendAnalyticsEvent = (name, data) => { if (analyticsNameFilter(name)) m_SentAnalyticsEvents.Add(new AnalyticsEventData(name: name, data: data)); }; } /// /// Set up the test fixture to collect filtered analytics registrations and events. /// /// The analytics name to be accepted, all other registrations and data /// will be discarded. protected void CollectAnalytics(string acceptedName) { CollectAnalytics((name) => name.Equals(acceptedName)); } /// /// Set up the test fixture to collect ALL analytics registrations and events. /// protected void CollectAnalytics() { CollectAnalytics((_) => true); } #endif } }