Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 14 additions & 18 deletions Assets/Input/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,20 @@ The options UI and override loader must preserve every composite path:

Test the default binding, rebind it, save and reload it, then rebind the restored composite again.

## Automated input tests

Use `Unity.InputSystem.TestFramework`'s `InputTestFixture` for action and binding tests. The fixture replaces platform input with an isolated runtime, so tests behave consistently in an interactive editor, Windows batchmode, and Linux Jenkins under Xvfb.

- Add dedicated virtual devices with `InputSystem.AddDevice<Mouse>()` and `InputSystem.AddDevice<Keyboard>()`.
- Never use `Mouse.current` or `Keyboard.current` from the host editor session.
- If a fixture already inherits another base class, compose an `InputTestFixture` and call `Setup()` and `TearDown()` explicitly.
- Create and enable the test's `CMInput` only after fixture setup.
- Dispose the test `CMInput` before fixture teardown, then restore shared application action maps after teardown restores the original Input System.
- Manual `InputSystem.Update()` is appropriate only inside `InputTestFixture` when the regression depends on multiple input reports before the next Unity frame.
- Verify that the production callback actually ran; do not accept a static field's default value as evidence that pointer or modifier input was dispatched.

See [the test harness guide](../Tests/README.md) for CLI execution and Jenkins audio constraints.

## Regression checklist

- Test both input directions where applicable.
Expand All @@ -75,21 +89,3 @@ Test the default binding, rebind it, save and reload it, then rebind the restore
- Test keybind discovery, rebinding, saved override reload, and rebinding after reload.
- Confirm generated `Master.cs` matches `Master.inputactions`.
- Run a clean Unity compile/build.




## NOTES 21f1

# m_ScrollDeltaBehavior: 0
Required for ctrl+alt+shift+scroll to change scroll precision at all. Delete this setting and some stuff stops working.

# m_InputActionPropertyDrawerMode: 0
Weirdly, without this alt+scroll on a node will still scroll the map,
shift+click will also exec left click and replace the node you're trying to select,
ctrl+shift+alt+scroll will adjust node AND precision, etc.

# m_ShortcutKeysConsumeInputs: 0
If this is set to 1, then alt+scroll wont fire on node hover, nor will ctrl+shift+scroll. Both do nothing when hovering a node.
ctrl+alt+scroll works fine, ctrl+alt+shift+scroll (hover) works fine but precision-adjustment does not.
When set to 0, shift click is also placing node instead of just selecting node. Alt+scroll is scrolling the map AND tweaking the node.
5 changes: 3 additions & 2 deletions Assets/ManualTests/InstalledMapLoadingTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ namespace ManualTests
public class InstalledMapLoadingTest : TestBase
{
private const string FailedMapSeparator = "\n\n-----------------\n\n";
// UpToNRandomMapsInDefaultSongLocationsLoadWithoutExceptions limits each seeded renderer run to five maps.
private const int MaximumRandomMapCount = 5;
// UpToNRandomMapsInDefaultSongLocationsLoadWithoutExceptions must turn a wedged scene coroutine into a
// recorded failure instead of occupying the Unity runner indefinitely after an exception aborts map loading.
private const float SceneTransitionTimeoutSeconds = 120f;
Expand All @@ -37,6 +35,9 @@ public class InstalledMapLoadingTest : TestBase
private bool sharedMapperRestored;
private bool transitionExceptionLogged;

// UpToNRandomMapsInDefaultSongLocationsLoadWithoutExceptions limits each seeded renderer run to five maps.
private const int MaximumRandomMapCount = 25;

// UpToNRandomMapsInDefaultSongLocationsLoadWithoutExceptions can exceed Unity's three-minute default while
// rendering five full maps, so grant it an hour while aggregating both configured default song locations.
[UnityTest]
Expand Down
99 changes: 95 additions & 4 deletions Assets/Tests/Editor/BasicEventChunkingTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,113 @@
using NUnit.Framework;
using Tests.Infrastructure;
using UnityEngine;
using UnityEngine.InputSystem;

namespace Tests.Editor
{
// BasicEventNodeChunkingTest and BasicEventTransitionRibbonTest must inspect the same production visual pool,
// so keep their scrub, node, and ribbon assertions here rather than letting the two fixtures drift apart.
// Shared production visual-pool helpers for node and ribbon chunking tests.
public abstract class BasicEventChunkingTestBase : TestBase
{
// Missing nodes can be active geometry rendered fully transparent, so inspect the shader value applied to them.
private static readonly int mainAlphaId = Shader.PropertyToID("_MainAlpha");

// Dense reported-map scrub matrices inspect thousands of nodes on one runner thread; reuse this diagnostic block
// so assertion allocations do not introduce GC frames that change the unload/reload sequence under test.
// Avoid test-induced GC while inspecting thousands of nodes per scrub matrix.
private static readonly MaterialPropertyBlock nodeRendererProperties = new();

// Linux Jenkins has no reliable native audio clock, so playback time is driven directly.
private static readonly System.Reflection.PropertyInfo currentSecondsProperty =
typeof(AudioTimeSyncController).GetProperty(nameof(AudioTimeSyncController.CurrentSeconds));

// Virtual devices isolate input tests from editor focus and Jenkins host state.
private InputTestFixture inputTestFixture;
private Mouse virtualMouse;
private Keyboard virtualKeyboard;

protected override EditingMode InitialEditingMode => EditingMode.BasicEvent;

protected static void StartDeterministicPlaybackAtSongBpmTime(
AudioTimeSyncController atsc,
float songBpmTime)
{
Assert.That(atsc.IsPlaying, Is.False, "Deterministic playback did not start from a paused controller.");
Assert.That(currentSecondsProperty, Is.Not.Null, "AudioTimeSyncController.CurrentSeconds was not found.");

atsc.TogglePlaying();
atsc.SongAudioSource.Stop();
atsc.StopScheduled = true;
currentSecondsProperty.SetValue(atsc, atsc.GetSecondsFromBeat(songBpmTime));

Assert.That(atsc.IsPlaying, Is.True, "Deterministic playback did not enter the production playing state.");
Assert.That(
atsc.SongAudioSource.isPlaying,
Is.False,
"Deterministic playback unexpectedly retained a native audio backend.");
}

protected static void PauseDeterministicPlayback(AudioTimeSyncController atsc)
{
Assert.That(atsc.IsPlaying, Is.True, "Deterministic playback was already paused.");
atsc.TogglePlaying();
Assert.That(atsc.IsPlaying, Is.False, "Deterministic playback did not pause.");
Assert.That(atsc.StopScheduled, Is.False, "Deterministic playback left an automatic stop scheduled.");
}

protected void InitializeVirtualInput(bool includeKeyboard)
{
Assert.That(inputTestFixture, Is.Null, "Virtual input was initialized twice in one test.");
inputTestFixture = new InputTestFixture();
inputTestFixture.Setup();
virtualMouse = InputSystem.AddDevice<Mouse>();
if (includeKeyboard)
{
virtualKeyboard = InputSystem.AddDevice<Keyboard>();
}
}

protected void SetVirtualMouseState(Vector2 position, Vector2 scroll)
{
Assert.That(inputTestFixture, Is.Not.Null, "Virtual input was not initialized before setting mouse state.");
inputTestFixture.Set(virtualMouse.position, position, queueEventOnly: true);
inputTestFixture.Set(virtualMouse.scroll, scroll, queueEventOnly: true);
InputSystem.Update();
}

// Apply each key event before constructing the next chord state.
protected void PressVirtualKeys(params Key[] keys)
{
Assert.That(virtualKeyboard, Is.Not.Null, "Virtual keyboard input was not initialized.");
foreach (var key in keys)
{
inputTestFixture.Press(virtualKeyboard[key], queueEventOnly: true);
InputSystem.Update();
Assert.That(virtualKeyboard[key].isPressed, Is.True, $"Virtual modifier {key} was not pressed.");
}
}

protected void ReleaseVirtualKeys(params Key[] keys)
{
Assert.That(virtualKeyboard, Is.Not.Null, "Virtual keyboard input was not initialized.");
foreach (var key in keys)
{
inputTestFixture.Release(virtualKeyboard[key], queueEventOnly: true);
InputSystem.Update();
Assert.That(virtualKeyboard[key].isPressed, Is.False, $"Virtual modifier {key} was not released.");
}
}

protected void TearDownVirtualInput()
{
virtualMouse = null;
virtualKeyboard = null;
if (inputTestFixture == null)
{
return;
}

inputTestFixture.TearDown();
inputTestFixture = null;
}

// Chunk regressions must use real EventPlacement-backed placement so insertion callbacks and ribbon indexes run.
protected static BaseEvent PlaceLightEvent(
float jsonTime,
Expand Down
15 changes: 5 additions & 10 deletions Assets/Tests/Editor/BasicEventDenseMapChunkingTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -353,19 +353,15 @@ private IEnumerator ScrubSameFrameAndAssertEveryNormalLaneVisual(ScrubRoute rout

private IEnumerator RunPlaybackScrubRoute(PlaybackScrubRoute route)
{
// Every route first performs uneven stopped scrubs, then lets the production AudioSource and callback
// controllers unload nodes during forward playback instead of simulating their internal cache operations.
yield return ScrubAndAssertEveryNormalLaneVisualAtSongTimes(
route.BeforePlaybackSongBpmTimes,
$"before playback route {route.Name}");

var atsc = UnityEngine.Object.FindAnyObjectByType<AudioTimeSyncController>();
Assert.That(atsc.IsPlaying, Is.False, $"Playback route {route.Name} did not start stopped.");
atsc.TogglePlaying();
atsc.SongAudioSource.time = atsc.GetSecondsFromBeat(route.PlayToSongBpmTime);
StartDeterministicPlaybackAtSongBpmTime(atsc, route.PlayToSongBpmTime);

// AudioTimeSyncController reads AudioSource.time in Update, then the callback controllers consume that new
// time on the following update. These two frames are lifecycle requirements, not visual-settling delays.
// Run both production lifecycle frames without relying on AudioSource.time.
yield return null;
yield return null;
Assert.That(atsc.IsPlaying, Is.True, $"Playback route {route.Name} stopped unexpectedly.");
Expand All @@ -374,19 +370,18 @@ private IEnumerator RunPlaybackScrubRoute(PlaybackScrubRoute route)
Is.GreaterThanOrEqualTo(route.PlayToSongBpmTime - 0.75f),
$"Playback route {route.Name} did not reach its requested callback-unload region.");

atsc.TogglePlaying();
PauseDeterministicPlayback(atsc);
Assert.That(atsc.IsPlaying, Is.False, $"Playback route {route.Name} did not pause.");

if (route.ReplayToSongBpmTime.HasValue)
{
// Replaying before a frame elapses stresses the pause RefreshPool followed immediately by another
// callback-driven unload, matching rapid Play/Pause input interspersed with scrub reversals.
atsc.TogglePlaying();
atsc.SongAudioSource.time = atsc.GetSecondsFromBeat(route.ReplayToSongBpmTime.Value);
StartDeterministicPlaybackAtSongBpmTime(atsc, route.ReplayToSongBpmTime.Value);
yield return null;
yield return null;
Assert.That(atsc.IsPlaying, Is.True, $"Replay route {route.Name} stopped unexpectedly.");
atsc.TogglePlaying();
PauseDeterministicPlayback(atsc);
Assert.That(atsc.IsPlaying, Is.False, $"Replay route {route.Name} did not pause.");
}

Expand Down
65 changes: 15 additions & 50 deletions Assets/Tests/Editor/BasicEventNodeChunkingTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ public class BasicEventNodeChunkingTest : BasicEventChunkingTestBase
private bool? invertScrollTimeBeforeTest;
private int? gridSnappingBeforeTest;
private float? songBpmBeforeTest;
private UnityEngine.InputSystem.Mouse physicalScrollMouse;
private bool addedPhysicalScrollMouse;
private Vector2 physicalScrollScreenPosition;

protected override void BeforeCleanup()
Expand Down Expand Up @@ -420,15 +418,12 @@ public IEnumerator PlaybackForwardThenImmediateBackwardWheelScrubReloadsNodesAnd
PreparePhysicalTimelineInput();
yield return null;

// Jump the playing AudioSource rather than mutating the private cursor so AudioTimeSyncController.Update and
// both BeatmapObjectCallbackControllers process the same forward playback discontinuity as production.
var atsc = Object.FindAnyObjectByType<AudioTimeSyncController>();
atsc.TogglePlaying();
atsc.SongAudioSource.time = atsc.GetSecondsFromBeat(48f);
StartDeterministicPlaybackAtSongBpmTime(atsc, 48f);
yield return null;
yield return null;
Assert.That(atsc.CurrentJsonTime, Is.GreaterThanOrEqualTo(47.5f), "Playback did not reach the unload region.");
atsc.TogglePlaying();
PauseDeterministicPlayback(atsc);
yield return null;
Assert.That(atsc.IsPlaying, Is.False, "Playback did not stop before the backward scrub.");
AssertNodeVisualUnloaded(ribbonSource, "after playback stopped ahead of the cluster");
Expand Down Expand Up @@ -782,6 +777,7 @@ private void PreparePhysicalTimelineInput()
sharedUtilsInputWasEnabled = sharedInput.Utils.enabled;
sharedInput.Utils.Disable();

InitializeVirtualInput(false);
var atsc = Object.FindAnyObjectByType<AudioTimeSyncController>();
physicalTimelineInput = new CMInput();
physicalTimelineInput.Timeline.SetCallbacks(atsc);
Expand All @@ -807,24 +803,14 @@ private void PreparePhysicalTimelineInput()
Assert.That(gameViewSize.y, Is.GreaterThan(2f), "The editor Game view had no usable height.");
physicalScrollScreenPosition = gameViewSize * 0.5f;

physicalScrollMouse = UnityEngine.InputSystem.Mouse.current;
addedPhysicalScrollMouse = physicalScrollMouse == null;
if (addedPhysicalScrollMouse)
{
physicalScrollMouse = UnityEngine.InputSystem.InputSystem.AddDevice<UnityEngine.InputSystem.Mouse>();
}

UnityEngine.InputSystem.InputSystem.QueueStateEvent(
physicalScrollMouse,
new UnityEngine.InputSystem.LowLevel.MouseState
{
position = physicalScrollScreenPosition + Vector2.one
});
UnityEngine.InputSystem.InputSystem.Update();
UnityEngine.InputSystem.InputSystem.QueueStateEvent(
physicalScrollMouse,
new UnityEngine.InputSystem.LowLevel.MouseState { position = physicalScrollScreenPosition });
UnityEngine.InputSystem.InputSystem.Update();
// Force an actual pointer transition rather than accepting cached static state.
SetVirtualMouseState(-Vector2.one, Vector2.zero);
Assert.That(
KeybindsController.IsMouseInWindow,
Is.False,
"The virtual Timeline pointer did not leave the editor window before entering it.");
SetVirtualMouseState(physicalScrollScreenPosition + Vector2.one, Vector2.zero);
SetVirtualMouseState(physicalScrollScreenPosition, Vector2.zero);
Assert.That(
KeybindsController.IsMouseInWindow,
Is.True,
Expand Down Expand Up @@ -860,25 +846,9 @@ private void SendPhysicalWheelPulse(int direction)
while (attempt < maximumAttempts && Mathf.Approximately(atsc.CurrentJsonTime, before))
{
var pointerNudge = attempt % 2 == 0 ? Vector2.one : -Vector2.one;
UnityEngine.InputSystem.InputSystem.QueueStateEvent(
physicalScrollMouse,
new UnityEngine.InputSystem.LowLevel.MouseState
{
position = physicalScrollScreenPosition + pointerNudge
});
UnityEngine.InputSystem.InputSystem.Update();
UnityEngine.InputSystem.InputSystem.QueueStateEvent(
physicalScrollMouse,
new UnityEngine.InputSystem.LowLevel.MouseState
{
position = physicalScrollScreenPosition,
scroll = new Vector2(0f, direction)
});
UnityEngine.InputSystem.InputSystem.Update();
UnityEngine.InputSystem.InputSystem.QueueStateEvent(
physicalScrollMouse,
new UnityEngine.InputSystem.LowLevel.MouseState { position = physicalScrollScreenPosition });
UnityEngine.InputSystem.InputSystem.Update();
SetVirtualMouseState(physicalScrollScreenPosition + pointerNudge, Vector2.zero);
SetVirtualMouseState(physicalScrollScreenPosition, new Vector2(0f, direction));
SetVirtualMouseState(physicalScrollScreenPosition, Vector2.zero);
attempt++;
}

Expand All @@ -901,12 +871,7 @@ private void DisposePhysicalTimelineInput()
physicalTimelineInput = null;
}

if (physicalScrollMouse != null && addedPhysicalScrollMouse)
{
UnityEngine.InputSystem.InputSystem.RemoveDevice(physicalScrollMouse);
}
physicalScrollMouse = null;
addedPhysicalScrollMouse = false;
TearDownVirtualInput();

var sharedInput = CMInputCallbackInstaller.InputInstance;
if (sharedInput != null && sharedTimelineInputWasEnabled == true)
Expand Down
Loading