#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.Content; using Microsoft.Xna.Framework.Graphics; using NUnit.Framework; using Nuclex.Graphics.Batching; using Nuclex.Testing.Xna; namespace Nuclex.Graphics.SpecialEffects.Particles.HighLevel { /// Unit tests for the particle system manager [TestFixture] internal class ParticleSystemManagerTest { #region class DummyDrawContext /// Dummy drawing context that does nothing private class DummyDrawContext : DrawContext { /// Number of passes this draw context requires for rendering public override int Passes { get { return 0; } } /// Prepares the graphics device for drawing /// Index of the pass to begin rendering public override void Apply(int pass) { } /// Tests whether another draw context is identical to this one /// Other context to check for equality /// True if the other context is identical to this one public override bool Equals(DrawContext otherContext) { return false; } } #endregion // class DummyDrawContext #region class CallbackReceiver /// Helper class used to test callbacks private class CallbackReceiver { /// Callback method that records the state /// Asynchronous result handle of the operation public void Callback(IAsyncResult asyncResult) { this.State = asyncResult.AsyncState; } /// State that has been passed to the callback method public object State; } #endregion // class CallbackReceiver #region class ExceptionThrowingAffector /// Particle affector which throws an exception private class ExceptionThrowingAffector : 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 ArithmeticException(); // some unlikely exception easy to recognize } } #endregion // class ExceptionThrowingAffector #region class ExceptionThrowingAffector /// Particle affector which takes a long time private class SlowAffector : 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) { Thread.Sleep(0); } } #endregion // class ExceptionThrowingAffector #region class DummyRenderer /// Dummy particle renderer for the unit test private class DummyRenderer : IParticleRenderer where ParticleType : struct, IVertexType { /// Renders a series of particles /// Particles that will be rendered /// /// Primitive batch that will receive the vertices generated by the particles /// public void Render( ArraySegment particles, PrimitiveBatch primitiveBatch ) { } } #endregion // class DummyRenderer /// Called before each test is run [SetUp] public void Setup() { this.mockedGraphicsDeviceService = new MockedGraphicsDeviceService(DeviceType.Reference); this.mockedGraphicsDeviceService.CreateDevice(); this.contentManager = new ResourceContentManager( GraphicsDeviceServiceHelper.MakePrivateServiceProvider( this.mockedGraphicsDeviceService ), Resources.ScreenMaskResources.ResourceManager ); this.effect = this.contentManager.Load("ScreenMaskEffect"); } /// Called after each test has run [TearDown] public void Teardown() { if(this.contentManager != null) { this.contentManager.Dispose(); this.effect = null; this.contentManager = null; } if(this.mockedGraphicsDeviceService != null) { this.mockedGraphicsDeviceService.DestroyDevice(); this.mockedGraphicsDeviceService = null; } } /// /// Verifies that the particle system manager's constructor is working /// [Test] public void TestConstructor() { using( ParticleSystemManager manager = new ParticleSystemManager( this.mockedGraphicsDeviceService ) ) { Assert.IsNotNull(manager); // nonsense; prevents compiler warning } } /// /// Tests whether particle systems can be added and removed from the manager /// [Test] public void TestAddParticleSystem() { using( ParticleSystemManager manager = new ParticleSystemManager( this.mockedGraphicsDeviceService ) ) { ParticleSystem test = new ParticleSystem(100); // Add and remove the particle system using a user-defined renderer manager.AddParticleSystem(test, dontPrune, new DummyRenderer()); manager.RemoveParticleSystem(test); } } /// /// Verifies that an exception during primitive batch creation is handled /// [Test] public void TestThrowDuringPrimitiveBatchCreation() { using( ParticleSystemManager manager = new ParticleSystemManager( this.mockedGraphicsDeviceService ) ) { manager.InducePrimitiveBatchErrorDelegate = delegate() { throw new ArithmeticException("Simulated error"); }; Assert.Throws( delegate() { manager.AddParticleSystem( new ParticleSystem(100), dontPrune, new DummyRenderer() ); } ); } } /// /// Verifies that an exception is thrown that a particle system is removed from /// the manager that has not been added /// [Test] public void TestThrowOnRemoveNotAddedParticleSystem() { using( ParticleSystemManager manager = new ParticleSystemManager( this.mockedGraphicsDeviceService ) ) { Assert.Throws( delegate() { manager.RemoveParticleSystem(new ParticleSystem(100)); } ); } } /// /// Verifies that an exception is thrown if a particle system is added to /// the manager with null specified for the renderer /// [Test] public void TestThrowOnAddParticleSystemWithNullRenderer() { using( ParticleSystemManager manager = new ParticleSystemManager( this.mockedGraphicsDeviceService ) ) { IParticleRenderer nullRenderer = null; Assert.Throws( delegate() { manager.AddParticleSystem( new ParticleSystem(100), dontPrune, nullRenderer ); } ); } } /// /// Verifies that the particle system manager can update its particle systems /// [Test] public void TestUpdate() { using( ParticleSystemManager manager = new ParticleSystemManager( this.mockedGraphicsDeviceService ) ) { for(int index = 0; index < 2; ++index) { manager.AddParticleSystem( new ParticleSystem(100), dontPrune, new DummyRenderer() ); } manager.Update(2); } } /// /// Verifies that the particle system manager can update its particle systems /// asynchronously /// [Test] public void TestAsynchronousUpdate() { using( ParticleSystemManager manager = new ParticleSystemManager( this.mockedGraphicsDeviceService ) ) { // Add one particle system where there's stuff to do ParticleSystem test = new ParticleSystem(16); for(int index = 0; index < test.Capacity; ++index) { test.AddParticle(new SimpleParticle()); } test.Affectors.Add(new SlowAffector()); manager.AddParticleSystem(test, dontPrune, new DummyRenderer()); // Add 16 other particle systems for(int index = 0; index < 16; ++index) { manager.AddParticleSystem( new ParticleSystem(100), dontPrune, new DummyRenderer() ); } // Now update everything for(int repetition = 0; repetition < 2; ++repetition) { IAsyncResult asyncResult = manager.BeginUpdate(2, 4, null, null); manager.EndUpdate(asyncResult); } } } /// /// Verifies that an exception is thrown if a wrong async result is specified /// when calling the EndUpdate() method /// [Test] public void TestThrowOnWrongAsyncResultInEndUpdate() { using( ParticleSystemManager manager = new ParticleSystemManager( this.mockedGraphicsDeviceService ) ) { IAsyncResult asyncResult = manager.BeginUpdate(1, 1, null, null); try { Assert.Throws( delegate() { manager.EndUpdate(null); } ); } finally { manager.EndUpdate(asyncResult); } } } /// /// Verifies that the user-defined callback is invoked when an asynchronous /// update completes /// [Test] public void TestAsynchronousUpdateCallback() { using( ParticleSystemManager manager = new ParticleSystemManager( this.mockedGraphicsDeviceService ) ) { CallbackReceiver receiver = new CallbackReceiver(); object state = new object(); IAsyncResult asyncResult = manager.BeginUpdate( 1, 1, receiver.Callback, state ); manager.EndUpdate(asyncResult); Assert.AreSame(state, receiver.State); Assert.IsFalse(asyncResult.CompletedSynchronously); } } /// /// Tests whether exceptions during asynchronous updating are handles /// [Test] public void TestThrowDuringAsynchronousUpdate() { using( ParticleSystemManager manager = new ParticleSystemManager( this.mockedGraphicsDeviceService ) ) { ParticleSystem test = new ParticleSystem(4096); for(int index = 0; index < test.Capacity; ++index) { test.AddParticle(new SimpleParticle()); } test.Affectors.Add(new ExceptionThrowingAffector()); manager.AddParticleSystem(test, dontPrune, new DummyRenderer()); IAsyncResult asyncResult = manager.BeginUpdate(1, 1, null, null); Assert.Throws( delegate() { manager.EndUpdate(asyncResult); } ); } } /// /// Verifies that the particle system manager can prune its particle systems /// [Test] public void TestPrune() { using( ParticleSystemManager manager = new ParticleSystemManager( this.mockedGraphicsDeviceService ) ) { for(int index = 0; index < 2; ++index) { manager.AddParticleSystem( new ParticleSystem(100), dontPrune, new DummyRenderer() ); } manager.Prune(); } } /// /// Verifies that the particle system manager can prune its particle systems /// asynchronously /// [Test] public void TestAsynchronousPrune() { using( ParticleSystemManager manager = new ParticleSystemManager( this.mockedGraphicsDeviceService ) ) { // Add one particle system where there's stuff to do ParticleSystem test = new ParticleSystem(16); for(int index = 0; index < test.Capacity; ++index) { test.AddParticle(new SimpleParticle()); } manager.AddParticleSystem(test, slowPrune, new DummyRenderer()); // Add 16 other particle systems for(int index = 0; index < 16; ++index) { manager.AddParticleSystem( new ParticleSystem(100), dontPrune, new DummyRenderer() ); } // Now update everything for(int repetition = 0; repetition < 2; ++repetition) { IAsyncResult asyncResult = manager.BeginPrune(null, null); manager.EndPrune(asyncResult); } } } /// /// Verifies that an exception is thrown if a wrong async result is specified /// when calling the EndPrune() method /// [Test] public void TestThrowOnWrongAsyncResultInEndPrune() { using( ParticleSystemManager manager = new ParticleSystemManager( this.mockedGraphicsDeviceService ) ) { IAsyncResult asyncResult = manager.BeginPrune(null, null); try { Assert.Throws( delegate() { manager.EndPrune(null); } ); } finally { manager.EndPrune(asyncResult); } } } /// /// Verifies that the user-defined callback is invoked when an asynchronous /// pruning process completes /// [Test] public void TestAsynchronousPruneCallback() { using( ParticleSystemManager manager = new ParticleSystemManager( this.mockedGraphicsDeviceService ) ) { CallbackReceiver receiver = new CallbackReceiver(); object state = new object(); IAsyncResult asyncResult = manager.BeginPrune(receiver.Callback, state); manager.EndPrune(asyncResult); Assert.AreSame(state, receiver.State); Assert.IsFalse(asyncResult.CompletedSynchronously); } } /// /// Tests whether exceptions during asynchronous updating are handles /// [Test] public void TestThrowDuringAsynchronousPrune() { using( ParticleSystemManager manager = new ParticleSystemManager( this.mockedGraphicsDeviceService ) ) { ParticleSystem test = new ParticleSystem(4096); for(int index = 0; index < test.Capacity; ++index) { test.AddParticle(new SimpleParticle()); } manager.AddParticleSystem( test, delegate(ref SimpleParticle particle) { throw new ArithmeticException(); }, new DummyRenderer() ); IAsyncResult asyncResult = manager.BeginPrune(null, null); Assert.Throws( delegate() { manager.EndPrune(asyncResult); } ); } } /// /// Tests whether the particle manager can draw its particle systems /// [Test] public void TestDraw() { using( ParticleSystemManager manager = new ParticleSystemManager( this.mockedGraphicsDeviceService ) ) { // Add one particle system where there's stuff to do ParticleSystem test = new ParticleSystem(4096); for(int index = 0; index < test.Capacity; ++index) { test.AddParticle(new SimpleParticle()); } manager.AddParticleSystem(test, dontPrune, new DummyRenderer()); manager.Draw(new GameTime()); } } /// /// Tests whether the particle manager can handle a large number of vertex types /// [Test] public void TestManyVertexTypes() { using( ParticleSystemManager manager = new ParticleSystemManager( this.mockedGraphicsDeviceService ) ) { manager.AddParticleSystem( new ParticleSystem(100), dontPrune, new DummyRenderer() ); manager.AddParticleSystem( new ParticleSystem(100), dontPrune, new DummyRenderer() ); manager.AddParticleSystem( new ParticleSystem(100), dontPrune, new DummyRenderer() ); manager.AddParticleSystem( new ParticleSystem(100), dontPrune, new DummyRenderer() ); manager.AddParticleSystem( new ParticleSystem(100), dontPrune, new DummyRenderer() ); manager.Draw(new GameTime()); } } /// Prune method that always returns false /// Type of particles to process /// Not used /// False. Always. private static bool dontPrune(ref ParticleType particle) { return false; } /// Prune method that is very slow /// Type of particles to process /// Not used /// False. Always. private static bool slowPrune(ref ParticleType particle) { Thread.Sleep(0); return false; } /// Mocked graphics device service used to run the unit tests private MockedGraphicsDeviceService mockedGraphicsDeviceService; /// /// Content manager used to load the effect by which the particles are drawn /// private ResourceContentManager contentManager; /// Effect used to draw the particles private Effect effect; } } // namespace Nuclex.Graphics.SpecialEffects.Particles.HighLevel #endif // UNITTEST