using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine.InputSystem.Controls;
using NUnit.Framework;
using NUnit.Framework.Constraints;
using Unity.Collections;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Utilities;
using UnityEngine.SceneManagement;
using UnityEngine.TestTools.Utils;
#if UNITY_EDITOR
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;
// 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.
InputSystem.SaveAndReset(enableRemoting: false, runtime: runtime);
#if UNITY_EDITOR
// Make sure we're not affected by the user giving focus away from the
// game view.
InputEditorUserSettings.lockInputToGameView = true;
#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;
}
catch (Exception exception)
{
Debug.LogError("Failed to set up input system for test " + TestContext.CurrentContext.Test.Name);
Debug.LogException(exception);
throw;
}
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()
{
try
{
// Destroy any GameObject in the current scene that isn't hidden and isn't the
// test runner object. Do this first so that any cleanup finds the system in the
// state it expects.
var scene = SceneManager.GetActiveScene();
foreach (var go in scene.GetRootGameObjects())
{
if (go.hideFlags != 0 || go.name.Contains("tests runner"))
continue;
Object.DestroyImmediate(go);
}
InputSystem.Restore();
runtime.Dispose();
// Re-enable input debugger.
#if UNITY_EDITOR
InputDebuggerWindow.Enable();
#endif
}
catch (Exception exception)
{
Debug.LogError("Failed to shut down and restore input system after test " + TestContext.CurrentContext.Test.Name);
Debug.LogException(exception);
throw;
}
}
// 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");
}
}
private Dictionary> m_KeyInfos;
///
/// 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)
{
return new ActionConstraint(InputActionPhase.Started, action, control, time: time, duration: 0);
}
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)
{
return new ActionConstraint(InputActionPhase.Performed, action, control, time: time, duration: duration);
}
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)
{
return new ActionConstraint(InputActionPhase.Canceled, action, control, time: time, duration: duration);
}
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);
}
// 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.
/// 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.
/// 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));
void SetUpAndQueueEvent(InputEventPtr eventPtr)
{
////REVIEW: should we by default take the time from the device here?
if (time >= 0)
eventPtr.time = time;
eventPtr.time += 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();
}
public void BeginTouch(int touchId, Vector2 position, bool queueEventOnly = false, Touchscreen screen = null,
double time = -1, double timeOffset = 0)
{
SetTouch(touchId, TouchPhase.Began, position, queueEventOnly: queueEventOnly, screen: screen, time: time, timeOffset: timeOffset);
}
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, delta, queueEventOnly, screen: screen, time: time, timeOffset: timeOffset);
}
public void EndTouch(int touchId, Vector2 position, Vector2 delta = default, bool queueEventOnly = false,
Touchscreen screen = null, double time = -1, double timeOffset = 0)
{
SetTouch(touchId, TouchPhase.Ended, position, delta, queueEventOnly, screen: screen, time: time, timeOffset: timeOffset);
}
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, screen: screen, time: time, timeOffset: timeOffset);
}
public void SetTouch(int touchId, TouchPhase phase, Vector2 position, Vector2 delta = default, bool queueEventOnly = true,
Touchscreen screen = null, double time = -1, double timeOffset = 0)
{
if (screen == null)
{
screen = Touchscreen.current;
if (screen == null)
throw new InvalidOperationException("No touchscreen has been added");
}
InputSystem.QueueStateEvent(screen, new TouchState
{
touchId = touchId,
phase = phase,
position = position,
delta = delta,
}, (time >= 0 ? time : InputRuntime.s_Instance.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;
set
{
runtime.currentTime = value;
runtime.dontAdvanceTimeNextDynamicUpdate = 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 (value is float f)
{
if (!Mathf.Approximately(f, Convert.ToSingle(val)))
return false;
}
else if (value is double d)
{
if (!Mathf.Approximately((float)d, (float)Convert.ToDouble(val)))
return false;
}
else if (value is Vector2 v2)
{
if (!Vector2EqualityComparer.Instance.Equals(v2, (Vector2)val))
return false;
}
else if (value is Vector3 v3)
{
if (!Vector3EqualityComparer.Instance.Equals(v3, (Vector3)val))
return false;
}
else if (!value.Equals(val))
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
}
}