#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