Skip to content

Commit 0dbc8f5

Browse files
committed
Additional simplification, fixes and test cases. Added test assembly to support serialization within editor test. Added test utilities.
1 parent b02a52e commit 0dbc8f5

16 files changed

+467
-24
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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+
public static void RestoreEditorPrefs()
29+
{
30+
EditorPrefs.SetBool(EnterPlayModeOptionsEnabledKey, _savedEnterPlayModeOptionsEnabled);
31+
EditorPrefs.SetInt(EnterPlayModeOptionsKey, _savedEnterPlayModeOptions);
32+
}
33+
34+
/// <summary>
35+
/// Call this from within a test to temporarily enable domain reload.
36+
/// </summary>
37+
public static void EnableDomainReload()
38+
{
39+
EditorPrefs.SetBool(EnterPlayModeOptionsEnabledKey, false);
40+
}
41+
42+
/// <summary>
43+
/// Call this from within a test to temporarily disable domain reload (and scene reloads).
44+
/// </summary>
45+
public static void DisableDomainReload()
46+
{
47+
EditorPrefs.SetBool(EnterPlayModeOptionsEnabledKey, true);
48+
EditorPrefs.SetInt(EnterPlayModeOptionsKey, (int)(EnterPlayModeOptions.DisableDomainReload |
49+
EnterPlayModeOptions.DisableSceneReload));
50+
}
51+
}
52+
}

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

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.

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

Lines changed: 10 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Assets/Tests/InputSystem.Editor/TestUtils.cs

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1+
using System;
2+
using System.Linq;
3+
using UnityEditor;
4+
using UnityEditor.SceneManagement;
15
using UnityEngine.InputSystem;
26
using UnityEngine.InputSystem.Editor;
7+
using UnityEngine.SceneManagement;
38

4-
// Replicated in this test assembly to avoid building public API picked up by PackageValidator
9+
// Replicated in this test assembly to avoid building public API picked up by PackageValidator.
510
internal class TestUtils
611
{
712
#if UNITY_EDITOR
@@ -24,5 +29,85 @@ public static void RestoreDialogs()
2429
Dialog.ControlScheme.SetDeleteControlScheme(null);
2530
}
2631

32+
/// <summary>
33+
/// Returns the (default) name of a scene asset created at the given path.
34+
/// </summary>
35+
/// <param name="scenePath">The path to the asset.</param>
36+
/// <returns>Scene name.</returns>
37+
public static string GetSceneName(string scenePath)
38+
{
39+
return System.IO.Path.GetFileNameWithoutExtension(scenePath);
40+
}
41+
42+
/// <summary>
43+
/// Saves a scene to the given path.
44+
/// </summary>
45+
/// <param name="scene">The scene to be saved.</param>
46+
/// <param name="scenePath">The target scene path.</param>
47+
/// <param name="makeDirty">If true, force saves the scene by making it explicitly dirty.</param>
48+
/// <exception cref="Exception">Throws exception if failed to save scene
49+
/// (if dirty or <paramref name="makeDirty"/> was true)</exception>
50+
public static void SaveScene(Scene scene, string scenePath, bool makeDirty = true)
51+
{
52+
var wasDirty = scene.isDirty;
53+
if (makeDirty)
54+
EditorSceneManager.MarkSceneDirty(scene);
55+
var wasSaved = EditorSceneManager.SaveScene(scene, scenePath);
56+
if (!wasSaved && (wasDirty || makeDirty))
57+
throw new Exception($"Failed to save scene to {scenePath}");
58+
}
59+
60+
/// <summary>
61+
/// Adds a scene asset to build settings unless it is already present.
62+
/// </summary>
63+
/// <param name="scenePath">The scene path to the scene asset.</param>
64+
/// <returns>Value tuple where first argument (int) specifies the build index of the scene and the second
65+
/// argument (bool) specifies whether the scene was added (true) or already present (false).</returns>
66+
public static ValueTuple<int, bool> AddSceneToEditorBuildSettings(string scenePath)
67+
{
68+
// Determine if scene is already present or not.
69+
var existingScenes = EditorBuildSettings.scenes;
70+
var index = FindSceneIndexByPath(existingScenes, scenePath);
71+
if (index >= 0)
72+
return new ValueTuple<int, bool>(index, false);
73+
74+
// Add the new scene.
75+
var newScenes = new EditorBuildSettingsScene[existingScenes.Length + 1];
76+
Array.Copy(existingScenes, newScenes, existingScenes.Length);
77+
newScenes[existingScenes.Length] = new EditorBuildSettingsScene(scenePath, true);
78+
EditorBuildSettings.scenes = newScenes;
79+
80+
return new ValueTuple<int, bool>(existingScenes.Length, true);
81+
}
82+
83+
/// <summary>
84+
/// Removes a scene asset from editor build settings given its path.
85+
/// </summary>
86+
/// <param name="scenePath">The asset path of the scene to be removed.</param>
87+
/// <returns>true if the scene was removed, false if it do not exist within editor build settings scenes.</returns>
88+
public static bool RemoveSceneFromEditorBuildSettings(string scenePath)
89+
{
90+
var existingScenes = EditorBuildSettings.scenes;
91+
var index = FindSceneIndexByPath(existingScenes, scenePath);
92+
if (index < 0)
93+
return false;
94+
95+
var newScenes = new EditorBuildSettingsScene[existingScenes.Length - 1];
96+
Array.Copy(existingScenes, newScenes, index);
97+
Array.Copy(existingScenes, index + 1, newScenes, index, existingScenes.Length - index - 1);
98+
EditorBuildSettings.scenes = newScenes;
99+
return true;
100+
}
101+
102+
private static int FindSceneIndexByPath(EditorBuildSettingsScene[] scenes, string scenePath)
103+
{
104+
for (var i = 0; i < scenes.Length; ++i)
105+
{
106+
if (scenes[i].path == scenePath)
107+
return i;
108+
}
109+
return -1;
110+
}
111+
27112
#endif // UNITY_EDITOR
28113
}

Assets/Tests/InputSystem.Editor/Unity.InputSystem.Tests.Editor.asmdef

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"UnityEngine.TestRunner",
66
"UnityEditor.TestRunner",
77
"Unity.InputSystem",
8-
"Unity.InputSystem.TestFramework"
8+
"Unity.InputSystem.TestFramework",
9+
"Unity.InputSystem.TestSupport"
910
],
1011
"includePlatforms": [
1112
"Editor"

Assets/Tests/InputSystem.TestSupport.meta

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using UnityEngine;
2+
using UnityEngine.InputSystem;
3+
4+
// This class and this assembly should NOT be considered API, it is only public and its own assembly for the following
5+
// reasons:
6+
// 1) Unity only supports serialization of public types stored in a file with the same name.
7+
// If this wasn't the case, this type would be internal to the test assembly.
8+
// 2) Editor test assemblies cannot contain MonoBehaviour that should be added to scene game objects.
9+
// Hence, this assembly needs to be a regular assembly and referenced by editor test assembly to use this
10+
// MonoBehaviour in a scene.
11+
//
12+
// Note that we serialize both as field and reference. Note that this should not make any difference for Unity.Object
13+
// types, but is included for completeness.
14+
public sealed class InputActionBehaviour : MonoBehaviour
15+
{
16+
[SerializeField] public InputActionReference referenceAsField;
17+
[SerializeReference] public InputActionReference referenceAsReference;
18+
}

Assets/Tests/InputSystem.TestSupport/InputActionBehaviour.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)