#region CPL License /* Nuclex Framework Copyright (C) 2002-2009 Nuclex Development Labs This library is free software; you can redistribute it and/or modify it under the terms of the IBM Common Public License as published by the IBM Corporation; either version 1.0 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the IBM Common Public License for more details. You should have received a copy of the IBM Common Public License along with this library */ #endregion #if UNITTEST using System; using System.Collections; using System.Collections.Generic; using System.Threading; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using NUnit.Framework; namespace Nuclex.Graphics.SpecialEffects.Particles { /// Unit tests for the particle system [TestFixture] internal class ParticleSystemTest { #region class ThrowingAffector /// Dummy particle affector that throws an exception private class ThrowingAffector : IParticleAffector { /// /// Whether the affector can do multiple updates in a single step without /// changing the outcome of the simulation /// public bool IsCoalescable { get { return false; } } /// Applies the affector's effect to a series of particles /// Particles the affector will be applied to /// Index of the first particle that will be affected /// Number of particles that will be affected /// Number of updates to perform in the affector public void Affect(SimpleParticle[] particles, int start, int count, int updates) { throw new KeyNotFoundException("Simulated error"); } } #endregion // class ThrowingAffector #region class TestAffector /// Dummy particle affector for unit testing private class TestAffector : IParticleAffector { /// Initializes a new test affector /// Whether the test affector is coalescable public TestAffector(bool coalescable) { this.coalescable = coalescable; } /// /// Whether the affector can do multiple updates in a single step without /// changing the outcome of the simulation /// public bool IsCoalescable { get { return this.coalescable; } } /// Applies the affector's effect to a series of particles /// Particles the affector will be applied to /// Index of the first particle that will be affected /// Number of particles that will be affected /// Number of updates to perform in the affector public void Affect(SimpleParticle[] particles, int start, int count, int updates) { this.LastUpdateCount = updates; } /// Number of updates the last Affect() call requested public int LastUpdateCount; /// Whether this affector is coalescable private bool coalescable; } #endregion // class TestAffector #region class WaitAffector /// Dummy particle affector for unit testing that holds execution private class WaitAffector : IParticleAffector, IDisposable { /// Initializes a new wait affector public WaitAffector() { this.waitEvent = new ManualResetEvent(false); } /// Immediately releases all resources used by the instance public void Dispose() { if(this.waitEvent != null) { this.waitEvent.Close(); this.waitEvent = null; } } /// /// Whether the affector can do multiple updates in a single step without /// changing the outcome of the simulation /// public bool IsCoalescable { get { return false; } } /// Applies the affector's effect to a series of particles /// Particles the affector will be applied to /// Index of the first particle that will be affected /// Number of particles that will be affected /// Number of updates to perform in the affector public void Affect(SimpleParticle[] particles, int start, int count, int updates) { this.waitEvent.WaitOne(); } /// Lets the wait affector stop execution public void Halt() { this.waitEvent.Reset(); } /// Lets the wait affector continue execution public void Continue() { this.waitEvent.Set(); } /// Event the wait affector will wait for private ManualResetEvent waitEvent; } #endregion // class TestAffector #region class DummyAsyncResult /// Dummy asynchronous result private class DummyAsyncResult : IAsyncResult { /// User defined state from the Begin() method public object AsyncState { get { throw new NotImplementedException(); } } /// Wait handle that can be used to wait for the process public WaitHandle AsyncWaitHandle { get { throw new NotImplementedException(); } } /// Whether the process has finished synchronously public bool CompletedSynchronously { get { throw new NotImplementedException(); } } /// True if the process has finished public bool IsCompleted { get { throw new NotImplementedException(); } } } #endregion // class DummyAsyncResult #region class DummyCallbackReceiver /// Dummy receiver that count the number of callbacks private class DummyCallbackReceiver { /// Initializes a new dummy callback receiver public DummyCallbackReceiver() { this.callbackEvent = new AutoResetEvent(false); } /// Callback which counts its number of invocations /// User defined state from the Begin() method public void Callback(object state) { Interlocked.Increment(ref this.callbackCallCount); this.callbackEvent.Set(); } /// Number of calls to the Callback() method public int CallbackCallCount { get { return Thread.VolatileRead(ref this.callbackCallCount); } } /// Event that will be triggered when the callback executes public AutoResetEvent CallbackEvent { get { return this.callbackEvent; } } /// Number of calls to the Callback() method private int callbackCallCount; /// Event that will be triggered when the callback executes private AutoResetEvent callbackEvent; } #endregion // class DummyCallbackReceiver /// Verifies that the particle system's constructor is working [Test] public void TestConstructor() { using(ParticleSystem system = new ParticleSystem(100)) { Assert.AreEqual(100, system.Capacity); } } /// Verifies that the AddParticles() method is working [Test] public void TestAddParticles() { using(ParticleSystem system = new ParticleSystem(10)) { for(int index = 0; index < 12; ++index) { system.AddParticle(index); } Assert.AreEqual(10, system.Particles.Count); for(int index = 0; index < 10; ++index) { Assert.Contains(index, system.Particles.Array); } } } /// /// Validates that the Update() method can run a simulation with multiple affectors /// [Test] public void TestUpdateWithMultipleAffectors() { using( ParticleSystem system = new ParticleSystem(10) ) { for(int index = 0; index < 10; ++index) { system.AddParticle(new SimpleParticle()); } system.Affectors.Add( new GravityAffector(SimpleParticleModifier.Default) ); system.Affectors.Add( new MovementAffector(SimpleParticleModifier.Default) ); system.Update(2); float newY = -GravityAffector.StandardEarthGravity * 3.0f / 60.0f; for(int index = 0; index < 10; ++index) { Assert.That( system.Particles.Array[0].Position.Y, Is.EqualTo(newY).Within(4).Ulps ); } } } /// /// Verifies that the particle system's Add() and Remove() methods are working /// [Test] public void TestAddAndRemove() { using(ParticleSystem system = new ParticleSystem(10)) { for(int index = 0; index < 10; ++index) { system.AddParticle(index); } system.RemoveParticle(8); system.RemoveParticle(6); system.RemoveParticle(4); system.RemoveParticle(2); CollectionAssert.AreEquivalent( new int[] { 0, 1, 3, 5, 7, 9 }, toArray(system.Particles) ); } } /// /// Verifies that the particle system can prune dead particles /// [Test] public void TestPrune() { using( ParticleSystem system = new ParticleSystem(10) ) { for(int index = 0; index < 10; ++index) { SimpleParticle particle = new SimpleParticle(); particle.Position.Y = (float)index; system.AddParticle(particle); } system.Prune( delegate(ref SimpleParticle particle) { return (particle.Position.Y < 5.0f); } ); Assert.AreEqual(5, system.Particles.Count); for(int index = 0; index < system.Particles.Count; ++index) { Assert.Less(system.Particles.Array[index].Position.Y, 5.0f); } } } /// /// Verifies that the particle system coalesces even a non-coalescable update if /// there is only one non-coalescable update. /// [Test] public void TestCoalesceSingleNoncoalescableUpdate() { using( ParticleSystem system = new ParticleSystem(10) ) { for(int index = 0; index < 10; ++index) { system.AddParticle(new SimpleParticle()); } TestAffector noncoalescable = new TestAffector(false); TestAffector coalescable = new TestAffector(true); system.Affectors.Add(noncoalescable); system.Affectors.Add(coalescable); system.Update(2); Assert.AreEqual(2, coalescable.LastUpdateCount); Assert.AreEqual(2, noncoalescable.LastUpdateCount); } } /// /// Verifies that the particle system can prune dead particles asynchronously /// [Test] public void TestAsynchronousPrune() { using( ParticleSystem system = new ParticleSystem(10) ) { for(int index = 0; index < 15; ++index) { SimpleParticle particle = new SimpleParticle(); particle.Position.Y = (float)index; system.AddParticle(particle); } for(int repetition = 0; repetition < 3; ++repetition) { IAsyncResult asyncResult = system.BeginPrune( delegate(ref SimpleParticle particle) { Thread.Sleep(0); // yield time slice return (particle.Position.Y < 5.0f); }, null, null ); system.EndPrune(asyncResult); } Assert.AreEqual(5, system.Particles.Count); for(int index = 0; index < system.Particles.Count; ++index) { Assert.Less(system.Particles.Array[index].Position.Y, 5.0f); } } } /// /// Tests whether an exception happening during the asynchronous prune process is /// caught and delivered to the caller of the EndPrune() method /// [Test] public void TestThrowOnExceptionDuringAsynchronousPrune() { using( ParticleSystem system = new ParticleSystem(10) ) { system.AddParticle(new SimpleParticle()); IAsyncResult asyncResult = system.BeginPrune( delegate(ref SimpleParticle particle) { throw new KeyNotFoundException(); }, null, null ); Assert.Throws( delegate() { system.EndPrune(asyncResult); } ); } } /// /// Tests whether an exception is thrown when EndPrune() is called with /// an async result that was not returned by BeginPrune() /// [Test] public void TestThrowOnEndPruneWithWrongAsyncResult() { using( ParticleSystem system = new ParticleSystem(10) ) { IAsyncResult asyncResult = system.BeginPrune( delegate(ref SimpleParticle particle) { return false; }, null, null ); try { Assert.Throws( delegate() { system.EndPrune(new DummyAsyncResult()); } ); } finally { system.EndPrune(asyncResult); } } } /// /// Verifies that the particle system can handle multiple asynchronous prunes /// [Test] public void TestMultipleAsynchronousPrunes() { using( ParticleSystem system = new ParticleSystem(10) ) { DummyCallbackReceiver receiver = new DummyCallbackReceiver(); for(int run = 0; run < 3; ++run) { IAsyncResult asyncResult = system.BeginPrune( delegate(ref SimpleParticle particle) { return false; }, receiver.Callback, receiver ); try { Assert.IsNotNull(asyncResult.AsyncWaitHandle); Assert.AreSame(asyncResult.AsyncState, receiver); Assert.IsFalse(asyncResult.CompletedSynchronously); } finally { system.EndPrune(asyncResult); } Assert.IsTrue(receiver.CallbackEvent.WaitOne(1000)); Assert.AreEqual(run + 1, receiver.CallbackCallCount); } } } /// /// Verifies that the particle system can update its particle asynchronously /// [Test] public void TestAsynchronousUpdate() { using( ParticleSystem system = new ParticleSystem(40) ) { system.Affectors.Add( new MovementAffector(SimpleParticleModifier.Default) ); for(int index = 0; index < 40; ++index) { SimpleParticle particle = new SimpleParticle(); particle.Position.Y = (float)index; particle.Velocity.X = 1.2f; system.AddParticle(particle); } IAsyncResult asyncResult = system.BeginUpdate(2, 4, null, null); system.EndUpdate(asyncResult); for(int index = 0; index < system.Particles.Count; ++index) { Assert.AreEqual(2.4f, system.Particles.Array[index].Position.X); } } } /// /// Tests whether an exception happening during the asynchronous prune process is /// caught and delivered to the caller of the EndPrune() method /// [Test] public void TestThrowOnExceptionDuringAsynchronousUpdate() { using( ParticleSystem system = new ParticleSystem(10) ) { system.Affectors.Add(new ThrowingAffector()); system.AddParticle(new SimpleParticle()); IAsyncResult asyncResult = system.BeginUpdate(2, 4, null, null); Assert.Throws( delegate() { system.EndUpdate(asyncResult); } ); } } /// /// Tests whether an exception is thrown when EndPrune() is called with /// an async result that was not returned by BeginPrune() /// [Test] public void TestThrowOnEndUpdateWithWrongAsyncResult() { using( ParticleSystem system = new ParticleSystem(10) ) { IAsyncResult asyncResult = system.BeginUpdate(2, 4, null, null); try { Assert.Throws( delegate() { system.EndUpdate(new DummyAsyncResult()); } ); } finally { system.EndUpdate(asyncResult); } } } /// /// Verifies that the particle system can handle multiple asynchronous prunes /// [Test] public void TestMultipleAsynchronousUpdates() { using( ParticleSystem system = new ParticleSystem(10) ) { using(WaitAffector waitAffector = new WaitAffector()) { for(int index = 0; index < 40; ++index) { system.AddParticle(new SimpleParticle()); } system.Affectors.Add(waitAffector); DummyCallbackReceiver receiver = new DummyCallbackReceiver(); for(int run = 0; run < 3; ++run) { waitAffector.Halt(); IAsyncResult asyncResult = system.BeginUpdate( 2, 4, receiver.Callback, receiver ); try { Assert.IsNotNull(asyncResult.AsyncWaitHandle); Assert.AreSame(asyncResult.AsyncState, receiver); Assert.IsFalse(asyncResult.CompletedSynchronously); } finally { waitAffector.Continue(); system.EndUpdate(asyncResult); } Assert.IsTrue(receiver.CallbackEvent.WaitOne(1000)); Assert.AreEqual(run + 1, receiver.CallbackCallCount); } } } } /// Returns a subsection of an array as its own array /// Type of the items stored in the array /// Array from which to create a subsection /// Start of the subsection in the array /// Number of items that will be copied /// A subsection of the provided array private static ItemType[] subArray(ItemType[] array, int start, int count) { ItemType[] segment = new ItemType[count]; Array.Copy(array, start, segment, 0, count); return segment; } /// Turns an array segment into a new array /// Type of the items stored in the array /// Array segment from which a new array will be created /// A new array with all the items from the array segment private static ItemType[] toArray(ArraySegment segment) { return subArray(segment.Array, segment.Offset, segment.Count); } } } // namespace Nuclex.Graphics.SpecialEffects.Particles #endif // UNITTEST