using System.Collections.Generic; using System.Linq; using jeanf.audiosystems; using NUnit.Framework; using UnityEditor; using UnityEngine; namespace AutomaticDoorSystem.Tests { /// /// Locks the edit-time validation contract for door audio: DoorAudioConfigurationValidator /// must flag every authoring state that plays wrong at runtime (half-authored open/close /// pairs, empty random-pick slots, SamplerData loop windows that pin the playhead) and stay /// quiet on healthy assets. The project sweep tests at the bottom then hold every serialized /// config in this project to that standard - no playmode needed to find a broken one. /// public class DoorAudioConfigValidationTests { private readonly List _cleanup = new(); private T Track(T obj) where T : Object { _cleanup.Add(obj); return obj; } private DoorAudioConfiguration NewConfig() => Track(ScriptableObject.CreateInstance()); private AudioClip NewClip(string name) => Track(AudioClip.Create(name, 44100, 1, 44100, false)); private SamplerData NewSamplerData(string name, AudioClip clip, bool oneShot = true, float loopFrom = 0f, float loopTo = 0f) { var data = Track(ScriptableObject.CreateInstance()); data.name = name; data.audioClip = clip; data.volume = 1f; data.isPlayOneShot = oneShot; data.loopFrom = loopFrom; data.loopTo = loopTo; if (!oneShot) data.playOut = loopTo; return data; } [TearDown] public void TearDown() { foreach (var obj in _cleanup) { if (obj != null) Object.DestroyImmediate(obj); } _cleanup.Clear(); } private static List Errors(List issues) => issues.Where(i => i.IsError).ToList(); [Test] public void FullyAuthoredConfig_HasNoIssues() { var config = NewConfig(); config.openSoundClips = new[] { NewClip("open") }; config.closeSoundClips = new[] { NewClip("close") }; Assert.IsEmpty(DoorAudioConfigurationValidator.Validate(config)); } [Test] public void OpenAuthoredButCloseEmpty_FlagsTheAsymmetry() { var config = NewConfig(); config.openSoundClips = new[] { NewClip("open") }; var issues = DoorAudioConfigurationValidator.Validate(config); Assert.That(issues.Any(i => i.Message.Contains("stay silent when closing")), "a config that only sounds on open must be flagged - this is the 'plays on open but not on close' bug class"); } [Test] public void NullClipSlot_IsAnError() { var config = NewConfig(); config.openSoundClips = new[] { NewClip("open"), null }; config.closeSoundClips = new[] { NewClip("close") }; var issues = DoorAudioConfigurationValidator.Validate(config); Assert.That(Errors(issues).Any(i => i.Message.Contains("open sound clip slot 1")), "an empty slot in a random-pick list plays nothing when picked and must be an error"); } [Test] public void NullSamplerSlot_IsAnError() { var config = NewConfig(); config.openSamplerData = new SamplerData[] { null }; config.closeSoundClips = new[] { NewClip("close") }; var issues = DoorAudioConfigurationValidator.Validate(config); Assert.That(Errors(issues).Any(i => i.Message.Contains("open SamplerData slot 0"))); } [Test] public void LoopingSamplerDataWithEmptyWindow_IsAnError() { var config = NewConfig(); config.openSamplerData = new[] { NewSamplerData("openLoop", NewClip("open"), oneShot: false, loopFrom: 0f, loopTo: 0f) }; config.closeSoundClips = new[] { NewClip("close") }; var issues = DoorAudioConfigurationValidator.Validate(config); Assert.That(Errors(issues).Any(i => i.Message.Contains("loop window is empty")), "an empty loop window pins the playhead to loopFrom forever - the clip never audibly plays " + "and the AudioSource never stops, so this must be an error"); } [Test] public void SamplerDataWithoutClip_FallsBackToClips_SoEventStillCounts() { var config = NewConfig(); config.openSamplerData = new[] { NewSamplerData("openNoClip", null) }; config.openSoundClips = new[] { NewClip("open") }; config.closeSoundClips = new[] { NewClip("close") }; var issues = DoorAudioConfigurationValidator.Validate(config); Assert.That(issues.Any(i => i.IsError), "the clipless SamplerData itself is an error"); Assert.That(issues.All(i => !i.Message.Contains("stay silent when closing") && !i.Message.Contains("stay silent when opening")), "the event still reaches the legacy clip fallback, so no asymmetry warning applies"); } [Test] public void SilentConfig_IsFlagged() { var issues = DoorAudioConfigurationValidator.Validate(NewConfig()); Assert.That(issues.Any(i => i.Message.Contains("neither open nor close"))); } [Test] public void ZeroVolumeSamplerData_IsFlagged() { var data = NewSamplerData("quiet", NewClip("clip")); data.volume = 0f; var issues = SamplerDataValidation.Validate(data); Assert.That(issues.Any(i => !i.IsError && i.Message.Contains("volume is 0")), "volume defaults to 0 on a fresh SamplerData asset, so this trap needs a warning"); } // ---- project sweeps: every serialized asset in this project must hold the line ---- [Test] public void EveryDoorAudioConfigurationAssetInProject_HasNoErrors() { var failures = new List(); foreach (var guid in AssetDatabase.FindAssets("t:DoorAudioConfiguration")) { var path = AssetDatabase.GUIDToAssetPath(guid); var config = AssetDatabase.LoadAssetAtPath(path); if (config == null) continue; foreach (var issue in DoorAudioConfigurationValidator.Validate(config).Where(i => i.IsError)) { failures.Add($"{path}: {issue.Message}"); } } Assert.IsEmpty(failures, "DoorAudioConfiguration assets with errors (doors using them will misplay in playmode):\n " + string.Join("\n ", failures)); } [Test] public void EverySamplerDataAssetInProject_HasNoErrors() { var failures = new List(); foreach (var guid in AssetDatabase.FindAssets("t:SamplerData")) { var path = AssetDatabase.GUIDToAssetPath(guid); var data = AssetDatabase.LoadAssetAtPath(path); if (data == null) continue; foreach (var issue in SamplerDataValidation.Validate(data).Where(i => i.IsError)) { failures.Add($"{path}: {issue.Message}"); } } Assert.IsEmpty(failures, "SamplerData assets with errors (they play wrong or wedge the AudioSource at runtime):\n " + string.Join("\n ", failures)); } } }