Files
unity/Assets/Tests/EditMode/SettingsTests.cs

338 lines
16 KiB
C#

using System;
using System.IO;
using System.Linq;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.InputSystem;
namespace Ironspore.Tests
{
public sealed class SettingsTests
{
string folder, path;
InputActionAsset input;
SettingsService service;
GameSettingsData defaults;
[SetUp]
public void Setup()
{
folder = Path.Combine(Path.GetTempPath(), "IronsporeSettingsTests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(folder);
path = Path.Combine(folder, "settings.json");
input = InputActionAsset.FromJson(File.ReadAllText("Assets/Game/Input/GameInput.inputactions"));
defaults = new GameSettingsData();
}
[TearDown]
public void Cleanup()
{
service?.Dispose();
service = null;
if (input) UnityEngine.Object.DestroyImmediate(input);
string allowed = Path.GetFullPath(Path.Combine(Path.GetTempPath(), "IronsporeSettingsTests")) + Path.DirectorySeparatorChar;
if (Path.GetFullPath(folder).StartsWith(allowed, StringComparison.OrdinalIgnoreCase) && Directory.Exists(folder))
Directory.Delete(folder, true);
}
SettingsService StartService(string file = null, Func<float> clock = null) =>
service = new SettingsService(new SettingsStore(file ?? path, defaults), input, defaults, clock);
[Test]
public void CompleteSnapshotSurvivesReloadAndCallerEdits()
{
var store = new SettingsStore(path);
var candidate = new GameSettingsData { masterVolume = .35f, mouseSensitivity = 2.5f, aimSensitivity = .6f,
qualityLevel = 4, resolutionWidth = 1920, resolutionHeight = 1080, fullscreen = true, bindingOverrides = "stable input IDs" };
Assert.IsTrue(store.Save(candidate));
candidate.masterVolume = 0;
var loaded = new SettingsStore(path).Data;
Assert.AreEqual(.35f, loaded.masterVolume);
Assert.AreEqual(.35f, store.Data.masterVolume);
Assert.AreEqual(2.5f, loaded.mouseSensitivity);
Assert.AreEqual(.6f, loaded.aimSensitivity);
Assert.AreEqual(4, loaded.qualityLevel);
Assert.AreEqual(1920, loaded.resolutionWidth);
Assert.AreEqual(1080, loaded.resolutionHeight);
Assert.IsTrue(loaded.fullscreen);
Assert.AreEqual("stable input IDs", loaded.bindingOverrides);
}
[Test]
public void MissingFieldsRetainDefaults()
{
File.WriteAllText(path, "{\"version\":1,\"masterVolume\":0.4}");
var loaded = new SettingsStore(path, defaults).Data;
Assert.AreEqual(.4f, loaded.masterVolume);
Assert.AreEqual(1, loaded.mouseSensitivity);
Assert.AreEqual(720, loaded.resolutionHeight);
}
[Test]
public void CorruptPrimaryRecoversLastCompleteBackup()
{
var store = new SettingsStore(path);
Assert.IsTrue(store.Save(new GameSettingsData { masterVolume = .4f }));
Assert.IsTrue(store.Save(new GameSettingsData { masterVolume = .7f }));
File.WriteAllText(path, "{broken json");
Assert.AreEqual(.4f, new SettingsStore(path).Data.masterVolume);
}
[Test]
public void WriteFailureAndUnsupportedVersionPreservePreviousSnapshot()
{
string blocker = Path.Combine(folder, "blocked");
File.WriteAllText(blocker, "not a directory");
var store = new SettingsStore(Path.Combine(blocker, "settings.json"));
Assert.IsFalse(store.Save(new GameSettingsData { masterVolume = .25f }));
Assert.AreEqual(1, store.Data.masterVolume);
Assert.IsNotEmpty(store.Error);
Assert.IsFalse(store.Save(new GameSettingsData { version = 99 }));
Assert.AreEqual(1, store.Data.version);
}
[Test]
public void InvalidNumbersAreNormalizedWithoutMutatingSource()
{
var data = new GameSettingsData { masterVolume = float.NaN, mouseSensitivity = float.PositiveInfinity,
aimSensitivity = -5, qualityLevel = 99, resolutionWidth = -1, resolutionHeight = 0 };
var normalized = data.Normalize(6);
Assert.AreEqual(1, normalized.masterVolume);
Assert.AreEqual(1, normalized.mouseSensitivity);
Assert.AreEqual(.1f, normalized.aimSensitivity);
Assert.AreEqual(5, normalized.qualityLevel);
Assert.AreEqual(1280, normalized.resolutionWidth);
Assert.AreEqual(-1, data.resolutionWidth);
}
[Test]
public void MovementPartsAndAllButtonsCanBeReboundWithoutChangingTemplateIds()
{
var bindings = new InputBindingService(input);
Assert.AreEqual(16, bindings.Slots.Count);
Assert.AreEqual(4, bindings.Slots.Count(x => x.Action.name == "Move"));
Assert.IsFalse(bindings.Slots.Any(x => x.Action.name == "Look" || x.Action.bindings[x.Index].isComposite));
var target = bindings.Slots.First(x => x.Action.name == "Interact");
Guid original = target.Action.bindings[target.Index].id;
Assert.AreEqual(BindingChange.Success, bindings.Rebind(target, "<Keyboard>/j", out _));
string json = bindings.Save();
bindings.Reset();
Assert.AreEqual("<Keyboard>/e", target.Action.bindings[target.Index].effectivePath);
Assert.IsTrue(bindings.TryLoad(json));
Assert.AreEqual("<Keyboard>/j", target.Action.bindings[target.Index].effectivePath);
Assert.AreEqual(original, target.Action.bindings[target.Index].id);
}
[Test]
public void OlderInteractionOnRSurvivesAddingReload()
{
var bindings = new InputBindingService(input);
var interact = input.FindAction("Pilot/Interact", true);
interact.ApplyBindingOverride(0, "<Keyboard>/r");
string legacy = bindings.Save();
bindings.Reset();
Assert.IsTrue(bindings.TryLoad(legacy));
Assert.AreEqual("<Keyboard>/r", interact.bindings[0].effectivePath);
Assert.AreEqual("<Keyboard>/t", input.FindAction("Pilot/Reload", true).bindings[0].effectivePath);
}
[Test]
public void ConflictsReservedKeysAndMouseMotionDoNotReplaceBinding()
{
var bindings = new InputBindingService(input);
var target = bindings.Slots.First(x => x.Action.name == "Interact");
Assert.AreEqual(BindingChange.Conflict, bindings.Rebind(target, "<Keyboard>/w", out var conflict));
Assert.AreEqual("Move", conflict.Action.name);
Assert.AreEqual(BindingChange.Reserved, bindings.Rebind(target, "<Keyboard>/escape", out _));
Assert.AreEqual(BindingChange.Unsupported, bindings.Rebind(target, "<Mouse>/delta", out _));
Assert.AreEqual(BindingChange.Unsupported, bindings.Rebind(target, "<Gamepad>/buttonSouth", out _));
Assert.AreEqual("<Keyboard>/e", target.Action.bindings[target.Index].effectivePath);
}
[Test]
public void InvalidOverridesDoNotDestroyExistingBindings()
{
var bindings = new InputBindingService(input);
var target = bindings.Slots.First(x => x.Action.name == "Interact");
bindings.Rebind(target, "<Mouse>/middleButton", out _);
Assert.IsFalse(bindings.TryLoad("{broken json"));
Assert.AreEqual("<Mouse>/middleButton", target.Action.bindings[target.Index].effectivePath);
}
[Test]
public void DigitZeroAndPunctuationUsePhysicalKeyboardLayoutNames()
{
var bindings = new InputBindingService(input);
var target = bindings.Slots.First(x => x.Action.name == "Interact");
Assert.AreEqual(BindingChange.Success, bindings.Rebind(target, "<Keyboard>/0", out _));
Assert.AreEqual(BindingChange.Success, bindings.Rebind(target, "<Keyboard>/semicolon", out _));
Assert.AreEqual(BindingChange.Unsupported, bindings.Rebind(target, "<Keyboard>/999", out _));
Assert.AreEqual("<Keyboard>/semicolon", target.Action.bindings[target.Index].effectivePath);
}
[Test]
public void SavedOverridesCannotTurnMouseLookIntoAButton()
{
var bindings = new InputBindingService(input);
var look = input.FindAction("Pilot/Look", true);
look.ApplyBindingOverride(0, "<Mouse>/leftButton");
string invalid = bindings.Save();
bindings.Reset();
Assert.IsFalse(bindings.TryLoad(invalid));
Assert.AreEqual("<Mouse>/delta", look.bindings[0].effectivePath);
}
[Test]
public void CancellingInteractiveCaptureRestoresEnabledActions()
{
var bindings = new InputBindingService(input);
input.Enable();
var target = bindings.Slots.First(x => x.Action.name == "Interact");
BindingChange? result = null;
bindings.Begin(target, (change, _) => result = change);
Assert.IsTrue(bindings.IsRebinding);
Assert.IsFalse(target.Action.enabled);
bindings.Cancel();
Assert.IsFalse(bindings.IsRebinding);
Assert.AreEqual(BindingChange.Cancelled, result);
Assert.IsTrue(input.All(x => x.enabled));
Assert.AreEqual("<Keyboard>/e", target.Action.bindings[target.Index].effectivePath);
}
[TestCase(Key.J, BindingChange.Success)]
[TestCase(Key.W, BindingChange.Conflict)]
[TestCase(Key.Escape, BindingChange.Cancelled)]
public void NativeKeyboardCaptureCompletesAndSuppressesItsMenuInput(Key key, BindingChange expected)
{
var previous = Keyboard.current;
var keyboard = InputSystem.AddDevice<Keyboard>("SettingsCaptureTest");
var bindings = new InputBindingService(input);
try
{
input.Enable();
var target = bindings.Slots.First(x => x.Action.name == "Interact");
BindingChange? result = null;
bindings.Begin(target, (change, _) => result = change);
InputSystem.QueueStateEvent(keyboard, new UnityEngine.InputSystem.LowLevel.KeyboardState(key));
InputSystem.Update();
Assert.AreEqual(expected, result);
Assert.IsFalse(bindings.IsRebinding);
Assert.IsTrue(bindings.SuppressMenuInput);
Assert.IsTrue(target.Action.enabled);
Assert.AreEqual(key == Key.J ? "<Keyboard>/j" : "<Keyboard>/e", target.Action.bindings[target.Index].effectivePath);
}
finally
{
bindings.Dispose();
InputSystem.RemoveDevice(keyboard);
previous?.MakeCurrent();
}
}
[Test]
public void ApplySavesVolumeSensitivityAndBindingsTogether()
{
StartService();
var draft = service.Current;
draft.masterVolume = .25f;
draft.mouseSensitivity = 2;
var target = service.Bindings.Slots.First(x => x.Action.name == "Interact");
service.Bindings.Rebind(target, "<Keyboard>/j", out _);
Assert.AreEqual(SettingsApplyResult.Saved, service.Apply(draft));
Assert.AreEqual(.25f, AudioListener.volume);
Assert.AreEqual(2, service.LookSensitivity);
var reloaded = new SettingsStore(path).Data;
Assert.AreEqual(.25f, reloaded.masterVolume);
Assert.AreEqual(2, reloaded.mouseSensitivity);
StringAssert.Contains("<Keyboard>/j", reloaded.bindingOverrides);
}
[TestCase(KeyCode.J, BindingChange.Success)]
[TestCase(KeyCode.R, BindingChange.Conflict)]
[TestCase(KeyCode.W, BindingChange.Conflict)]
[TestCase(KeyCode.Alpha0, BindingChange.Success)]
public void UiKeyboardCaptureUsesTheSameValidationAndCompletesOnce(KeyCode key, BindingChange expected)
{
var bindings = new InputBindingService(input);
input.Enable();
var target = bindings.Slots.First(x => x.Action.name == "Interact");
BindingChange? result = null;
int completions = 0;
bindings.Begin(target, (change, _) => { result = change; completions++; });
Assert.IsTrue(bindings.CapturePath(SettingsInputCapture.KeyboardPath(key)));
Assert.AreEqual(expected, result);
Assert.AreEqual(1, completions);
Assert.IsFalse(bindings.IsRebinding);
Assert.IsTrue(target.Action.enabled);
Assert.IsTrue(bindings.SuppressMenuInput);
Assert.IsFalse(bindings.CapturePath("<Keyboard>/j"));
}
[Test]
public void DisplayPreviewExpiresDuringPauseWithoutWritingCandidate()
{
float time = 10;
StartService(clock: () => time);
var draft = service.Current;
draft.resolutionWidth = 1920;
draft.resolutionHeight = 1080;
draft.masterVolume = .2f;
Assert.AreEqual(SettingsApplyResult.ConfirmDisplay, service.Apply(draft));
Assert.IsFalse(File.Exists(path));
Assert.AreEqual(15, service.ConfirmationSeconds);
Assert.AreEqual(.2f, AudioListener.volume);
time += 16;
Assert.IsTrue(service.Tick());
Assert.IsFalse(service.ConfirmingDisplay);
Assert.AreEqual(1280, service.Current.resolutionWidth);
Assert.AreEqual(1, AudioListener.volume);
Assert.IsFalse(File.Exists(path));
}
[Test]
public void DisplayConfirmationPersistsTheEntireSnapshot()
{
StartService();
var draft = service.Current;
draft.resolutionWidth = 1920;
draft.resolutionHeight = 1080;
draft.masterVolume = .6f;
Assert.AreEqual(SettingsApplyResult.ConfirmDisplay, service.Apply(draft));
Assert.IsTrue(service.ConfirmDisplay());
var saved = new SettingsStore(path).Data;
Assert.AreEqual(1920, saved.resolutionWidth);
Assert.AreEqual(.6f, saved.masterVolume);
}
[Test]
public void FailedApplyDoesNotChangeCurrentOrRuntimeValues()
{
string blocker = Path.Combine(folder, "blocked");
File.WriteAllText(blocker, "not a directory");
StartService(Path.Combine(blocker, "settings.json"));
var draft = service.Current;
draft.masterVolume = .2f;
draft.mouseSensitivity = 4;
Assert.AreEqual(SettingsApplyResult.Failed, service.Apply(draft));
Assert.AreEqual(1, service.Current.masterVolume);
Assert.AreEqual(1, service.LookSensitivity);
Assert.AreEqual(1, AudioListener.volume);
Assert.IsNotEmpty(service.Error);
}
[Test]
public void DiscardRestoresSavedBindingsAndProtectsCurrentFromDraftMutation()
{
StartService();
var draft = service.Current;
draft.masterVolume = 0;
var target = service.Bindings.Slots.First(x => x.Action.name == "Interact");
service.Bindings.Rebind(target, "<Keyboard>/j", out _);
service.DiscardDraft();
Assert.AreEqual("<Keyboard>/e", target.Action.bindings[target.Index].effectivePath);
Assert.AreEqual(1, service.Current.masterVolume);
Assert.IsFalse(File.Exists(path));
}
}
}