Skip to content

Commit 27ae3b1

Browse files
authored
FIX: InputTestFixture not resetting all global state on start (#1395, case 1329015).
1 parent b8d5716 commit 27ae3b1

File tree

12 files changed

+543
-387
lines changed

12 files changed

+543
-387
lines changed

Assets/Tests/InputSystem/CoreTests_Editor.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2618,15 +2618,15 @@ public void Editor_LeavingPlayMode_DestroysAllActionStates()
26182618
var action = new InputAction(binding: "<Gamepad>/buttonSouth");
26192619
action.Enable();
26202620

2621-
Assert.That(InputActionState.s_GlobalList.length, Is.EqualTo(1));
2621+
Assert.That(InputActionState.s_GlobalState.globalList.length, Is.EqualTo(1));
26222622
Assert.That(InputSystem.s_Manager.m_StateChangeMonitors.Length, Is.GreaterThan(0));
26232623
Assert.That(InputSystem.s_Manager.m_StateChangeMonitors[0].count, Is.EqualTo(1));
26242624

26252625
// Exit play mode.
26262626
InputSystem.OnPlayModeChange(PlayModeStateChange.ExitingPlayMode);
26272627
InputSystem.OnPlayModeChange(PlayModeStateChange.EnteredEditMode);
26282628

2629-
Assert.That(InputActionState.s_GlobalList.length, Is.Zero);
2629+
Assert.That(InputActionState.s_GlobalState.globalList.length, Is.Zero);
26302630
Assert.That(InputSystem.s_Manager.m_StateChangeMonitors[0].listeners[0].control, Is.Null); // Won't get removed, just cleared.
26312631
}
26322632

Assets/Tests/InputSystem/Plugins/EnhancedTouchTests.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public override void Setup()
2626
base.Setup();
2727

2828
// Disable() will not reset this so default initialize it here.
29-
Touch.s_HistoryLengthPerFinger = 64;
29+
Touch.s_GlobalState.historyLengthPerFinger = 64;
3030

3131
if (!TestContext.CurrentContext.Test.Properties.ContainsKey("EnhancedTouchDisabled"))
3232
{
@@ -45,16 +45,16 @@ public override void TearDown()
4545
EnhancedTouchSupport.Disable();
4646

4747
// Make sure cleanup really did clean up.
48-
Assert.That(Touch.s_Touchscreens.length, Is.EqualTo(0));
49-
Assert.That(Touch.s_PlayerState, Is.EqualTo(default(Touch.FingerAndTouchState)));
48+
Assert.That(Touch.s_GlobalState.touchscreens.length, Is.EqualTo(0));
49+
Assert.That(Touch.s_GlobalState.playerState, Is.EqualTo(default(Touch.FingerAndTouchState)));
5050
#if UNITY_EDITOR
51-
Assert.That(Touch.s_EditorState, Is.EqualTo(default(Touch.FingerAndTouchState)));
51+
Assert.That(Touch.s_GlobalState.editorState, Is.EqualTo(default(Touch.FingerAndTouchState)));
5252
#endif
5353

5454
// Some state is kept alive in-between Disable/Enable. Manually clean it out.
55-
Touch.s_OnFingerDown = default;
56-
Touch.s_OnFingerUp = default;
57-
Touch.s_OnFingerMove = default;
55+
Touch.s_GlobalState.onFingerDown = default;
56+
Touch.s_GlobalState.onFingerUp = default;
57+
Touch.s_GlobalState.onFingerMove = default;
5858

5959
TouchSimulation.Destroy();
6060
TouchSimulation.s_Instance = m_OldTouchSimulationInstance;

Packages/com.unity.inputsystem/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ however, it has to be formatted properly to pass verification tests.
1717
* Internally, a control is now again allowed to feed into the same action through more than one binding.
1818
* However, externally the control will be mentioned on the action's `InputAction.controls` list only once.
1919

20+
### Fixed
21+
22+
- Fixed an issue where mixing test cases based on `InputTestFixture` (using mocked `InputSystem`) and regular test cases (using real `InputSystem`) would lead to static state leaking between test cases causing random failures and unexpected/undefined behavior ([case 1329015](https://issuetracker.unity3d.com/product/unity/issues/guid/1329015)).
23+
2024
## [1.1.0-pre.6] - 2021-08-23
2125

2226
### Fixed

Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs

Lines changed: 66 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ internal unsafe class InputActionState : IInputStateChangeMonitor, ICloneable, I
127127
public void Initialize(InputBindingResolver resolver)
128128
{
129129
ClaimDataFrom(resolver);
130-
AddToGlobaList();
130+
AddToGlobalList();
131131
}
132132

133133
internal void ClaimDataFrom(InputBindingResolver resolver)
@@ -390,7 +390,7 @@ public void RestoreActionStates(UnmanagedMemory oldState)
390390
HookOnBeforeUpdate();
391391

392392
// Fire notifications.
393-
if (s_OnActionChange.length > 0)
393+
if (s_GlobalState.onActionChange.length > 0)
394394
{
395395
for (var i = 0; i < totalMapCount; ++i)
396396
{
@@ -1955,7 +1955,7 @@ private void CallActionListeners(int actionIndex, InputActionMap actionMap, Inpu
19551955
{
19561956
// If there's no listeners, don't bother with anything else.
19571957
var callbacksOnMap = actionMap.m_ActionCallbacks;
1958-
if (listeners.length == 0 && callbacksOnMap.length == 0 && s_OnActionChange.length == 0)
1958+
if (listeners.length == 0 && callbacksOnMap.length == 0 && s_GlobalState.onActionChange.length == 0)
19591959
return;
19601960

19611961
var context = new InputAction.CallbackContext
@@ -1968,7 +1968,7 @@ private void CallActionListeners(int actionIndex, InputActionMap actionMap, Inpu
19681968

19691969
// Global callback goes first.
19701970
var action = context.action;
1971-
if (s_OnActionChange.length > 0)
1971+
if (s_GlobalState.onActionChange.length > 0)
19721972
{
19731973
InputActionChange change;
19741974
switch (phase)
@@ -1987,7 +1987,7 @@ private void CallActionListeners(int actionIndex, InputActionMap actionMap, Inpu
19871987
return;
19881988
}
19891989

1990-
DelegateHelpers.InvokeCallbacksSafe(ref s_OnActionChange, action, change, "InputSystem.onActionChange");
1990+
DelegateHelpers.InvokeCallbacksSafe(ref s_GlobalState.onActionChange, action, change, "InputSystem.onActionChange");
19911991
}
19921992

19931993
// Run callbacks (if any) directly on action.
@@ -3607,7 +3607,7 @@ public UnmanagedMemory Clone()
36073607
#region Global State
36083608

36093609
/// <summary>
3610-
/// List of weak references to all action map states currently in the system.
3610+
/// Global state containing a list of weak references to all action map states currently in the system.
36113611
/// </summary>
36123612
/// <remarks>
36133613
/// When the control setup in the system changes, we need a way for control resolution that
@@ -3616,25 +3616,44 @@ public UnmanagedMemory Clone()
36163616
///
36173617
/// Both of these needs are served by this global list.
36183618
/// </remarks>
3619-
internal static InlinedArray<GCHandle> s_GlobalList;
3620-
internal static CallbackArray<Action<object, InputActionChange>> s_OnActionChange;
3621-
internal static CallbackArray<Action<object>> s_OnActionControlsChanged;
3619+
internal struct GlobalState
3620+
{
3621+
internal InlinedArray<GCHandle> globalList;
3622+
internal CallbackArray<Action<object, InputActionChange>> onActionChange;
3623+
internal CallbackArray<Action<object>> onActionControlsChanged;
3624+
}
3625+
3626+
internal static GlobalState s_GlobalState;
3627+
3628+
internal static ISavedState SaveAndResetState()
3629+
{
3630+
// Save current state
3631+
var savedState = new SavedStructState<GlobalState>(
3632+
ref s_GlobalState,
3633+
(ref GlobalState state) => s_GlobalState = state, // restore
3634+
() => ResetGlobals()); // static dispose
3635+
3636+
// Reset global state
3637+
s_GlobalState = default;
3638+
3639+
return savedState;
3640+
}
36223641

3623-
private void AddToGlobaList()
3642+
private void AddToGlobalList()
36243643
{
36253644
CompactGlobalList();
36263645
var handle = GCHandle.Alloc(this, GCHandleType.Weak);
3627-
s_GlobalList.AppendWithCapacity(handle);
3646+
s_GlobalState.globalList.AppendWithCapacity(handle);
36283647
}
36293648

36303649
private void RemoveMapFromGlobalList()
36313650
{
3632-
var count = s_GlobalList.length;
3651+
var count = s_GlobalState.globalList.length;
36333652
for (var i = 0; i < count; ++i)
3634-
if (s_GlobalList[i].Target == this)
3653+
if (s_GlobalState.globalList[i].Target == this)
36353654
{
3636-
s_GlobalList[i].Free();
3637-
s_GlobalList.RemoveAtByMovingTailWithCapacity(i);
3655+
s_GlobalState.globalList[i].Free();
3656+
s_GlobalState.globalList.RemoveAtByMovingTailWithCapacity(i);
36383657
break;
36393658
}
36403659
}
@@ -3644,25 +3663,25 @@ private void RemoveMapFromGlobalList()
36443663
/// </summary>
36453664
private static void CompactGlobalList()
36463665
{
3647-
var length = s_GlobalList.length;
3666+
var length = s_GlobalState.globalList.length;
36483667
var head = 0;
36493668
for (var i = 0; i < length; ++i)
36503669
{
3651-
var handle = s_GlobalList[i];
3670+
var handle = s_GlobalState.globalList[i];
36523671
if (handle.IsAllocated && handle.Target != null)
36533672
{
36543673
if (head != i)
3655-
s_GlobalList[head] = handle;
3674+
s_GlobalState.globalList[head] = handle;
36563675
++head;
36573676
}
36583677
else
36593678
{
36603679
if (handle.IsAllocated)
3661-
s_GlobalList[i].Free();
3662-
s_GlobalList[i] = default;
3680+
s_GlobalState.globalList[i].Free();
3681+
s_GlobalState.globalList[i] = default;
36633682
}
36643683
}
3665-
s_GlobalList.length = head;
3684+
s_GlobalState.globalList.length = head;
36663685
}
36673686

36683687
internal static void NotifyListenersOfActionChange(InputActionChange change, object actionOrMapOrAsset)
@@ -3671,33 +3690,33 @@ internal static void NotifyListenersOfActionChange(InputActionChange change, obj
36713690
Debug.Assert(actionOrMapOrAsset is InputAction || (actionOrMapOrAsset as InputActionMap)?.m_SingletonAction == null,
36723691
"Must not send notifications for changes made to hidden action maps of singleton actions");
36733692

3674-
DelegateHelpers.InvokeCallbacksSafe(ref s_OnActionChange, actionOrMapOrAsset, change, "onActionChange");
3693+
DelegateHelpers.InvokeCallbacksSafe(ref s_GlobalState.onActionChange, actionOrMapOrAsset, change, "onActionChange");
36753694
if (change == InputActionChange.BoundControlsChanged)
3676-
DelegateHelpers.InvokeCallbacksSafe(ref s_OnActionControlsChanged, actionOrMapOrAsset, "onActionControlsChange");
3695+
DelegateHelpers.InvokeCallbacksSafe(ref s_GlobalState.onActionControlsChanged, actionOrMapOrAsset, "onActionControlsChange");
36773696
}
36783697

36793698
/// <summary>
36803699
/// Nuke global state we have to keep track of action map states.
36813700
/// </summary>
3682-
internal static void ResetGlobals()
3701+
private static void ResetGlobals()
36833702
{
36843703
DestroyAllActionMapStates();
3685-
for (var i = 0; i < s_GlobalList.length; ++i)
3686-
if (s_GlobalList[i].IsAllocated)
3687-
s_GlobalList[i].Free();
3688-
s_GlobalList.length = 0;
3689-
s_OnActionChange.Clear();
3690-
s_OnActionControlsChanged.Clear();
3704+
for (var i = 0; i < s_GlobalState.globalList.length; ++i)
3705+
if (s_GlobalState.globalList[i].IsAllocated)
3706+
s_GlobalState.globalList[i].Free();
3707+
s_GlobalState.globalList.length = 0;
3708+
s_GlobalState.onActionChange.Clear();
3709+
s_GlobalState.onActionControlsChanged.Clear();
36913710
}
36923711

36933712
// Walk all maps with enabled actions and add all enabled actions to the given list.
36943713
internal static int FindAllEnabledActions(List<InputAction> result)
36953714
{
36963715
var numFound = 0;
3697-
var stateCount = s_GlobalList.length;
3716+
var stateCount = s_GlobalState.globalList.length;
36983717
for (var i = 0; i < stateCount; ++i)
36993718
{
3700-
var handle = s_GlobalList[i];
3719+
var handle = s_GlobalState.globalList[i];
37013720
if (!handle.IsAllocated)
37023721
continue;
37033722
var state = (InputActionState)handle.Target;
@@ -3761,15 +3780,15 @@ internal static void OnDeviceChange(InputDevice device, InputDeviceChange change
37613780
change == InputDeviceChange.SoftReset || change == InputDeviceChange.HardReset,
37623781
"Should only be called for relevant changes");
37633782

3764-
for (var i = 0; i < s_GlobalList.length; ++i)
3783+
for (var i = 0; i < s_GlobalState.globalList.length; ++i)
37653784
{
3766-
var handle = s_GlobalList[i];
3785+
var handle = s_GlobalState.globalList[i];
37673786
if (!handle.IsAllocated || handle.Target == null)
37683787
{
37693788
// Stale entry in the list. State has already been reclaimed by GC. Remove it.
37703789
if (handle.IsAllocated)
3771-
s_GlobalList[i].Free();
3772-
s_GlobalList.RemoveAtWithCapacity(i);
3790+
s_GlobalState.globalList[i].Free();
3791+
s_GlobalState.globalList.RemoveAtWithCapacity(i);
37733792
--i;
37743793
continue;
37753794
}
@@ -3833,15 +3852,15 @@ internal static void DeferredResolutionOfBindings()
38333852
++InputActionMap.s_DeferBindingResolution;
38343853
try
38353854
{
3836-
for (var i = 0; i < s_GlobalList.length; ++i)
3855+
for (var i = 0; i < s_GlobalState.globalList.length; ++i)
38373856
{
3838-
var handle = s_GlobalList[i];
3857+
var handle = s_GlobalState.globalList[i];
38393858
if (!handle.IsAllocated || handle.Target == null)
38403859
{
38413860
// Stale entry in the list. State has already been reclaimed by GC. Remove it.
38423861
if (handle.IsAllocated)
3843-
s_GlobalList[i].Free();
3844-
s_GlobalList.RemoveAtWithCapacity(i);
3862+
s_GlobalState.globalList[i].Free();
3863+
s_GlobalState.globalList.RemoveAtWithCapacity(i);
38453864
--i;
38463865
continue;
38473866
}
@@ -3859,9 +3878,9 @@ internal static void DeferredResolutionOfBindings()
38593878

38603879
internal static void DisableAllActions()
38613880
{
3862-
for (var i = 0; i < s_GlobalList.length; ++i)
3881+
for (var i = 0; i < s_GlobalState.globalList.length; ++i)
38633882
{
3864-
var handle = s_GlobalList[i];
3883+
var handle = s_GlobalState.globalList[i];
38653884
if (!handle.IsAllocated || handle.Target == null)
38663885
continue;
38673886
var state = (InputActionState)handle.Target;
@@ -3885,16 +3904,16 @@ internal static void DisableAllActions()
38853904
/// </remarks>
38863905
internal static void DestroyAllActionMapStates()
38873906
{
3888-
while (s_GlobalList.length > 0)
3907+
while (s_GlobalState.globalList.length > 0)
38893908
{
3890-
var index = s_GlobalList.length - 1;
3891-
var handle = s_GlobalList[index];
3909+
var index = s_GlobalState.globalList.length - 1;
3910+
var handle = s_GlobalState.globalList[index];
38923911
if (!handle.IsAllocated || handle.Target == null)
38933912
{
38943913
// Already destroyed.
38953914
if (handle.IsAllocated)
3896-
s_GlobalList[index].Free();
3897-
s_GlobalList.RemoveAtWithCapacity(index);
3915+
s_GlobalState.globalList[index].Free();
3916+
s_GlobalState.globalList.RemoveAtWithCapacity(index);
38983917
continue;
38993918
}
39003919

Packages/com.unity.inputsystem/InputSystem/InputSystem.cs

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using UnityEngine.InputSystem.XInput;
1515
using UnityEngine.InputSystem.Utilities;
1616
using UnityEngine.Profiling;
17+
1718
#if UNITY_EDITOR
1819
using UnityEditor;
1920
using UnityEngine.InputSystem.Editor;
@@ -2917,13 +2918,13 @@ public static event Action<object, InputActionChange> onActionChange
29172918
{
29182919
if (value == null)
29192920
throw new ArgumentNullException(nameof(value));
2920-
InputActionState.s_OnActionChange.AddCallback(value);
2921+
InputActionState.s_GlobalState.onActionChange.AddCallback(value);
29212922
}
29222923
remove
29232924
{
29242925
if (value == null)
29252926
throw new ArgumentNullException(nameof(value));
2926-
InputActionState.s_OnActionChange.RemoveCallback(value);
2927+
InputActionState.s_GlobalState.onActionChange.RemoveCallback(value);
29272928
}
29282929
}
29292930

@@ -3519,7 +3520,6 @@ private static void Destroy()
35193520
// state repeatedly during tests but we want to not create InputSystemObject
35203521
// over and over.
35213522

3522-
InputActionState.ResetGlobals();
35233523
s_Manager.Destroy();
35243524
if (s_RemoteConnection != null)
35253525
Object.DestroyImmediate(s_RemoteConnection);
@@ -3552,7 +3552,10 @@ internal struct State
35523552
[SerializeField] public InputEditorUserSettings.SerializedState userSettings;
35533553
[SerializeField] public string systemObject;
35543554
#endif
3555-
////REVIEW: preserve InputUser state? (if even possible)
3555+
////TODO: make these saved states capable of surviving domain reloads
3556+
[NonSerialized] public ISavedState inputActionState;
3557+
[NonSerialized] public ISavedState touchState;
3558+
[NonSerialized] public ISavedState inputUserState;
35563559
}
35573560

35583561
private static Stack<State> s_SavedStateStack;
@@ -3590,6 +3593,9 @@ internal static void SaveAndReset(bool enableRemoting = false, IInputRuntime run
35903593
userSettings = InputEditorUserSettings.s_Settings,
35913594
systemObject = JsonUtility.ToJson(s_SystemObject),
35923595
#endif
3596+
inputActionState = InputActionState.SaveAndResetState(),
3597+
touchState = EnhancedTouch.Touch.SaveAndResetState(),
3598+
inputUserState = InputUser.SaveAndResetState()
35933599
});
35943600

35953601
Reset(enableRemoting, runtime ?? InputRuntime.s_Instance); // Keep current runtime.
@@ -3603,11 +3609,20 @@ internal static void Restore()
36033609
{
36043610
Debug.Assert(s_SavedStateStack != null && s_SavedStateStack.Count > 0);
36053611

3612+
// Load back previous state.
3613+
var state = s_SavedStateStack.Pop();
3614+
3615+
state.inputUserState.StaticDisposeCurrentState();
3616+
state.touchState.StaticDisposeCurrentState();
3617+
state.inputActionState.StaticDisposeCurrentState();
3618+
36063619
// Nuke what we have.
36073620
Destroy();
36083621

3609-
// Load back previous state.
3610-
var state = s_SavedStateStack.Pop();
3622+
state.inputUserState.RestoreSavedState();
3623+
state.touchState.RestoreSavedState();
3624+
state.inputActionState.RestoreSavedState();
3625+
36113626
s_Manager = state.manager;
36123627
s_Remote = state.remote;
36133628
s_RemoteConnection = state.remoteConnection;

0 commit comments

Comments
 (0)