Skip to content

Commit d8972e1

Browse files
ekcohCopilot
andauthored
FIX: ISXB-1584, ISXB-1688, ISXB-1692, ISXB-1693, ISXB-1694, ISXB-1701 InputActionReference bug fixes and improved test coverage. (#2248)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent b5a5349 commit d8972e1

27 files changed

+1145
-209
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using System.Collections;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
using UnityEngine;
5+
using UnityEngine.InputSystem;
6+
7+
internal static class DumpInputActionReferences
8+
{
9+
private static void DumpReferences(StringBuilder sb, string prefix, InputActionReference[] references)
10+
{
11+
sb.Append(prefix + ":\n");
12+
foreach (var reference in references)
13+
{
14+
var s = reference.action != null ? "Yes" : "No";
15+
sb.Append($"- {reference.name} (Resolved: {s}, Asset: {reference.asset})\n");
16+
}
17+
}
18+
19+
private static void DumpReferences()
20+
{
21+
var sb = new StringBuilder();
22+
DumpReferences(sb, "Loaded objects", Object.FindObjectsByType<InputActionReference>(
23+
FindObjectsInactive.Include, FindObjectsSortMode.InstanceID));
24+
DumpReferences(sb, "All objects:", Resources.FindObjectsOfTypeAll<InputActionReference>());
25+
Debug.Log(sb.ToString());
26+
}
27+
28+
[UnityEditor.MenuItem("QA Tools/Dump Input Action References to Console", false, 100)]
29+
private static void Dump()
30+
{
31+
DumpReferences();
32+
}
33+
}

Assets/Editor/DumpInputActionReferences.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Assets/Samples/RebindingUI/RebindActionParameterUI.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public string bindingId
3232
/// <summary>
3333
/// The preference key to be used for persistence.
3434
/// </summary>
35-
public string mPreferenceKey
35+
public string preferenceKey
3636
{
3737
get => m_PreferenceKey;
3838
set => m_PreferenceKey = value;
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
using UnityEditor;
2+
3+
namespace Tests.InputSystem.Editor
4+
{
5+
/// <summary>
6+
/// Utility to simplify editor tests with respect to editor preferences.
7+
/// </summary>
8+
internal static class EditorPrefsTestUtils
9+
{
10+
private const string EnterPlayModeOptionsEnabledKey = "EnterPlayModeOptionsEnabled";
11+
private const string EnterPlayModeOptionsKey = "EnterPlayModeOptions";
12+
13+
private static bool _savedEnterPlayModeOptionsEnabled;
14+
private static int _savedEnterPlayModeOptions;
15+
16+
/// <summary>
17+
/// Call this from a tests SetUp routine to save editor preferences so they can be restored after the test.
18+
/// </summary>
19+
public static void SaveEditorPrefs()
20+
{
21+
_savedEnterPlayModeOptionsEnabled = EditorPrefs.GetBool(EnterPlayModeOptionsEnabledKey, false);
22+
_savedEnterPlayModeOptions = EditorPrefs.GetInt(EnterPlayModeOptionsKey, (int)EnterPlayModeOptions.None);
23+
}
24+
25+
/// <summary>
26+
/// Call this from a tests TearDown routine to restore editor preferences to the state it had before the test.
27+
/// </summary>
28+
/// <remarks>Note that if domain reloads have not been disabled and you have a domain reload mid-test,
29+
/// this utility will fail to restore editor preferences since the saved data will be lost.</remarks>
30+
public static void RestoreEditorPrefs()
31+
{
32+
EditorPrefs.SetBool(EnterPlayModeOptionsEnabledKey, _savedEnterPlayModeOptionsEnabled);
33+
EditorPrefs.SetInt(EnterPlayModeOptionsKey, _savedEnterPlayModeOptions);
34+
}
35+
36+
/// <summary>
37+
/// Returns whether domain reloads are disabled.
38+
/// </summary>
39+
/// <returns>true if domain reloads have been disabled, else false.</returns>
40+
public static bool IsDomainReloadsDisabled()
41+
{
42+
return EditorPrefs.GetBool(EnterPlayModeOptionsEnabledKey, false) &&
43+
(EditorPrefs.GetInt(EnterPlayModeOptionsKey, (int)EnterPlayModeOptions.None) &
44+
(int)EnterPlayModeOptions.DisableDomainReload) != 0;
45+
}
46+
47+
/// <summary>
48+
/// Returns whether scene reloads are disabled.
49+
/// </summary>
50+
/// <returns>true if scene reloads have been disabled, else false.</returns>
51+
public static bool IsSceneReloadsDisabled()
52+
{
53+
return EditorPrefs.GetBool(EnterPlayModeOptionsEnabledKey, false) &&
54+
(EditorPrefs.GetInt(EnterPlayModeOptionsKey, (int)EnterPlayModeOptions.None) &
55+
(int)EnterPlayModeOptions.DisableSceneReload) != 0;
56+
}
57+
58+
/// <summary>
59+
/// Call this from within a test to temporarily enable domain reload.
60+
/// </summary>
61+
public static void EnableDomainReload()
62+
{
63+
EditorPrefs.SetBool(EnterPlayModeOptionsEnabledKey, false);
64+
}
65+
66+
/// <summary>
67+
/// Call this from within a test to temporarily disable domain reload (and scene reloads).
68+
/// </summary>
69+
public static void DisableDomainReload()
70+
{
71+
EditorPrefs.SetInt(EnterPlayModeOptionsKey, (int)(EnterPlayModeOptions.DisableDomainReload |
72+
EnterPlayModeOptions.DisableSceneReload));
73+
EditorPrefs.SetBool(EnterPlayModeOptionsEnabledKey, true);
74+
}
75+
}
76+
}

Assets/Tests/InputSystem.Editor/EditorPrefsTestUtils.cs.meta

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
using System;
2+
using System.Collections;
3+
using System.Linq;
4+
using NUnit.Framework;
5+
using Tests.InputSystem.Editor;
6+
using UnityEditor;
7+
using UnityEditor.SceneManagement;
8+
using UnityEngine;
9+
using UnityEngine.InputSystem;
10+
using UnityEngine.InputSystem.Editor;
11+
using UnityEngine.SceneManagement;
12+
using UnityEngine.TestTools;
13+
using Object = UnityEngine.Object;
14+
15+
/// <summary>
16+
/// Editor tests for <see cref="UnityEngine.InputSystem.InputActionReference"/>.
17+
/// </summary>
18+
/// <remarks>
19+
/// This test need fixed asset paths since mid-test domain reloads would otherwise discard data.
20+
///
21+
/// Be aware that if you get failures in editor tests that switch between play mode and edit mode via coroutines
22+
/// you might get misleading stack traces that indicate errors in different places than they actually happen.
23+
/// At least this have been observered for exception stack traces.
24+
/// </remarks>
25+
internal class InputActionReferenceEditorTestsWithScene
26+
{
27+
private const string TestCategory = "Editor";
28+
29+
private Scene m_Scene;
30+
31+
private const string assetPath = "Assets/__InputActionReferenceEditorTests.inputactions";
32+
private const string dummyPath = "Assets/__InputActionReferenceEditorTestsDummy.asset";
33+
private const string scenePath = "Assets/__InputActionReferenceEditorTestsScene.unity";
34+
35+
private void CreateAsset()
36+
{
37+
var asset = ScriptableObject.CreateInstance<InputActionAsset>();
38+
39+
var map1 = new InputActionMap("map1");
40+
map1.AddAction("action1");
41+
map1.AddAction("action2");
42+
asset.AddActionMap(map1);
43+
44+
System.IO.File.WriteAllText(assetPath, asset.ToJson());
45+
Object.DestroyImmediate(asset);
46+
AssetDatabase.ImportAsset(assetPath);
47+
}
48+
49+
[SetUp]
50+
public void SetUp()
51+
{
52+
// This looks odd, but when we yield into play mode from our test coroutine we may get a domain reload
53+
// (depending on editor preferences) which will trigger another SetUp() mid-test.
54+
if (Application.isPlaying)
55+
return;
56+
57+
EditorPrefsTestUtils.SaveEditorPrefs();
58+
59+
m_Scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene);
60+
CreateAsset();
61+
62+
var go = new GameObject("Root");
63+
var behaviour = go.AddComponent<InputActionBehaviour>();
64+
var reference = InputActionImporter.LoadInputActionReferencesFromAsset(assetPath).First(
65+
r => "action1".Equals(r.action.name));
66+
behaviour.referenceAsField = reference;
67+
behaviour.referenceAsReference = reference;
68+
69+
TestUtils.SaveScene(m_Scene, scenePath);
70+
}
71+
72+
[TearDown]
73+
public void TearDown()
74+
{
75+
// This looks odd, but when we yield into play mode from our test coroutine we may get a domain reload
76+
// (depending on editor preferences) which will trigger another TearDown() mid-test.
77+
if (Application.isPlaying)
78+
return;
79+
80+
EditorPrefsTestUtils.RestoreEditorPrefs();
81+
82+
// Close scene
83+
EditorSceneManager.CloseScene(m_Scene, true);
84+
85+
// Clean-up
86+
AssetDatabase.DeleteAsset(dummyPath);
87+
AssetDatabase.DeleteAsset(assetPath);
88+
AssetDatabase.DeleteAsset(scenePath);
89+
}
90+
91+
private void DisableDomainReloads()
92+
{
93+
// Assumes off before running tests.
94+
Debug.Assert(!EditorPrefsTestUtils.IsDomainReloadsDisabled());
95+
Debug.Assert(!EditorPrefsTestUtils.IsSceneReloadsDisabled());
96+
97+
// Safe to store since state wouldn't be reset by domain reload.
98+
EditorPrefsTestUtils.DisableDomainReload();
99+
}
100+
101+
private static InputActionBehaviour GetBehaviour() => Object.FindFirstObjectByType<InputActionBehaviour>();
102+
private static InputActionAsset GetAsset() => AssetDatabase.LoadAssetAtPath<InputActionAsset>(assetPath);
103+
104+
// For unclear reason, NUnit fails to assert throwing exceptions after transition into play-mode.
105+
// So until that can be sorted out, we do it manually (in the same way) ourselves.
106+
private static void AssertThrows<T>(Action action) where T : Exception
107+
{
108+
var exceptionThrown = false;
109+
try
110+
{
111+
action();
112+
}
113+
catch (InvalidOperationException)
114+
{
115+
exceptionThrown = true;
116+
}
117+
Assert.IsTrue(exceptionThrown, $"Expected exception of type {typeof(T)} to be thrown but it was not.");
118+
}
119+
120+
private static bool[] _disableDomainReloadsValues = new bool[] { false, true };
121+
122+
[UnityTest]
123+
[Category(TestCategory)]
124+
[Description("https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-1584")]
125+
public IEnumerator ReferenceSetInPlaymodeShouldBeRestored_WhenExitingPlaymode(
126+
[ValueSource(nameof(_disableDomainReloadsValues))] bool disableDomainReloads)
127+
{
128+
if (disableDomainReloads)
129+
DisableDomainReloads();
130+
131+
// Edit-mode section
132+
{
133+
// Sanity check our initial edit-mode state
134+
var obj = GetBehaviour();
135+
Assert.That(obj.referenceAsField.action, Is.SameAs(GetAsset().FindAction("map1/action1")));
136+
Assert.That(obj.referenceAsReference.action, Is.SameAs(GetAsset().FindAction("map1/action1")));
137+
138+
// Enter play-mode (This will lead to domain reload by default).
139+
yield return new EnterPlayMode();
140+
}
141+
142+
// Play-mode section
143+
{
144+
var obj = GetBehaviour();
145+
var editModeAction = GetAsset().FindAction("map1/action1");
146+
var playModeAction = GetAsset().FindAction("map1/action2");
147+
148+
// Make sure our action reference is consistent in play-mode
149+
Assert.That(obj.referenceAsField.action, Is.SameAs(editModeAction));
150+
151+
// ISXB-1584: Attempting assignment of persisted input action reference in play-mode in editor.
152+
// Rationale: We cannot allow this since it would corrupt the source asset since changes applied to SO
153+
// mapped to an asset isn't reverted when exiting play-mode.
154+
//
155+
// Here we would like to do:
156+
// Assert.Throws<InvalidOperationException>(() => obj.reference.Set(null));
157+
//
158+
// But we can't since it would fail with NullReferenceException.
159+
// It turns out that because of the domain reload / Unity’s internal serialization quirks, the obj is
160+
// sometimes null inside the lambda when NUnit captures it for execution. So to work around this we
161+
// instead do the same kind of check manually for now which doesn't seem to have this problem.
162+
//
163+
// It is odd since NUnit does basically does the same thing (apart from wrapping the lambda as a
164+
// TestDelegate). So the WHY for this problem remains unclear for now.
165+
AssertThrows<InvalidOperationException>(() => obj.referenceAsField.Set(playModeAction));
166+
AssertThrows<InvalidOperationException>(() => obj.referenceAsReference.Set(editModeAction));
167+
168+
// Make sure there were no side-effects.
169+
Assert.That(obj.referenceAsField.action, Is.SameAs(editModeAction));
170+
Assert.That(obj.referenceAsReference.action, Is.SameAs(editModeAction));
171+
172+
// Correct usage is to use a run-time assigned input action reference instead. It is up to the user
173+
// to decide whether this reference should additionally be persisted (which is possible by saving it to
174+
// and asset, or by using SerializeReference).
175+
obj.referenceAsField = InputActionReference.Create(playModeAction);
176+
obj.referenceAsReference = InputActionReference.Create(playModeAction);
177+
178+
// Makes sure we have the expected reference.
179+
Assert.That(obj.referenceAsField.action, Is.SameAs(playModeAction));
180+
Assert.That(obj.referenceAsReference.action, Is.SameAs(playModeAction));
181+
182+
// Exit play-mode (This will lead to domain reload by default).
183+
yield return new ExitPlayMode();
184+
}
185+
186+
// Edit-mode section
187+
{
188+
// Make sure our reference is back to its initial edit mode state
189+
var obj = GetBehaviour();
190+
var editModeAction = GetAsset().FindAction("map1/action1");
191+
Assert.That(obj.referenceAsField.action, Is.SameAs(editModeAction));
192+
Assert.That(obj.referenceAsReference.action, Is.SameAs(editModeAction));
193+
}
194+
195+
yield return null;
196+
}
197+
}

Assets/Tests/InputSystem.Editor/InputActionReferenceEditorTests.cs.meta

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)