#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