#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2011 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.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using NUnit.Framework;
namespace Nuclex.Game.States {
/// Unit test for the game state manager
[TestFixture]
public class GameStateManagerTest {
#region class TestGameState
/// Game state used for unit testing
private class TestGameState : DrawableGameState, IDisposable {
/// Initializes a new test game state
public TestGameState() { }
///
/// Allows the game state to run logic such as updating the world,
/// checking for collisions, gathering input and playing audio.
///
/// Provides a snapshot of timing values
public override void Update(GameTime gameTime) {
++this.UpdateCallCount;
}
/// This is called when the game state should draw itself
/// Provides a snapshot of timing values
public override void Draw(GameTime gameTime) {
++this.DrawCallCount;
}
/// Immediately releases all resources owned by the instance
public void Dispose() {
++this.DisposeCallCount;
}
/// Called when the game state has been entered
protected override void OnEntered() {
++this.OnEnteredCallCount;
base.OnEntered();
}
/// Called when the game state is being left again
protected override void OnLeaving() {
++this.OnLeavingCallCount;
base.OnLeaving();
}
/// Called when the game state should enter pause mode
protected override void OnPause() {
++this.OnPauseCallCount;
base.OnPause();
}
/// Called when the game state should resume from pause mode
protected override void OnResume() {
++this.OnResumeCallCount;
base.OnResume();
}
/// Number of calls to the Dispose() method
public int DisposeCallCount;
/// Number of calls to the Update() method
public int UpdateCallCount;
/// Number of calls to the DraW() method
public int DrawCallCount;
/// Number of calls to the OnEntered() method
public int OnEnteredCallCount;
/// Number of calls to the OnLeaving() method
public int OnLeavingCallCount;
/// Number of calls to the OnPause() method
public int OnPauseCallCount;
/// Number of calls to the OnResume() method
public int OnResumeCallCount;
}
#endregion // class TestGameState
#region class UnenterableGameState
/// Game state the canont be entered
private class UnenterableGameState : GameState {
/// Initializes a new unenterable game state
public UnenterableGameState() { }
///
/// Allows the game state to run logic such as updating the world,
/// checking for collisions, gathering input and playing audio.
///
/// Provides a snapshot of timing values
public override void Update(GameTime gameTime) { }
/// Called when the game state has been entered
protected override void OnEntered() {
throw new InvalidOperationException("Simulated error for unit testing");
}
}
#endregion // class UnenterableGameState
#region class ReentrantGameState
/// Game state that nests another game state upon being entered
private class ReentrantGameState : GameState {
/// Initializes a new unresumable game state
///
/// Game state manager the unresumable game state belongs to
///
public ReentrantGameState(IGameStateService gameStateService) {
this.gameStateService = gameStateService;
}
///
/// Allows the game state to run logic such as updating the world,
/// checking for collisions, gathering input and playing audio.
///
/// Provides a snapshot of timing values
public override void Update(GameTime gameTime) { }
/// Called when the game state has been entered
protected override void OnEntered() {
this.gameStateService.Push(new TestGameState());
}
/// Game state service the reentrant state will use
private IGameStateService gameStateService;
}
#endregion // class ReentrantGameState
///
/// Verifies that the constructor of the game state manager is working
///
[Test]
public void TestDefaultConstructor() {
var manager = new GameStateManager();
Assert.IsNotNull(manager); // nonsense, avoids compiler warning
}
///
/// Verifies that the constructor of the game state manager accepting
/// a reference to the GameServiceCollection is working
///
[Test]
public void TestGameServiceConstructor() {
var services = new GameServiceContainer();
Assert.IsNull(services.GetService(typeof(IGameStateService)));
using (var manager = new GameStateManager(services)) {
Assert.IsNotNull(services.GetService(typeof(IGameStateService)));
}
Assert.IsNull(services.GetService(typeof(IGameStateService)));
}
///
/// Tests whether disposing the game state manager causes it to leave
/// the active game state
///
[Test]
public void TestLeaveOnDisposal() {
var test = new TestGameState();
Assert.AreEqual(0, test.OnLeavingCallCount);
using (var manager = new GameStateManager()) {
manager.Push(test);
}
Assert.AreEqual(1, test.OnLeavingCallCount);
}
///
/// Tests whether disposing the game state manager disposes the currently
/// active game states when it is disposed itself.
///
/// Whether to run the test with enabled disposal
[Test, TestCase(true), TestCase(false)]
public void TestDisposeActiveStatesOnDisposal(bool disposalEnabled) {
var test = new TestGameState();
Assert.AreEqual(0, test.DisposeCallCount);
using (var manager = new GameStateManager()) {
manager.DisposeDroppedStates = disposalEnabled;
manager.Push(test);
}
// The manager should only dispose the state if disposal was enabled
if (disposalEnabled) {
Assert.AreEqual(1, test.DisposeCallCount);
} else {
Assert.AreEqual(0, test.DisposeCallCount);
}
}
///
/// Verifies that the Pause() and Resume() methods are propagated to
/// the topmost game state on the stack
///
[Test]
public void TestPauseAndResume() {
var obscured = new TestGameState();
var active = new TestGameState();
using (var manager = new GameStateManager()) {
manager.Push(obscured);
Assert.AreEqual(0, obscured.OnPauseCallCount);
manager.Push(active);
Assert.AreEqual(1, obscured.OnPauseCallCount);
Assert.AreEqual(0, active.OnPauseCallCount);
manager.Pause();
Assert.AreEqual(1, active.OnPauseCallCount);
Assert.AreEqual(0, active.OnResumeCallCount);
manager.Resume();
Assert.AreEqual(1, active.OnResumeCallCount);
Assert.AreEqual(0, obscured.OnResumeCallCount);
manager.Pop();
Assert.AreEqual(1, obscured.OnResumeCallCount);
}
}
///
/// Verifies that the Push() method respects the modality parameter
///
/// Modality that will be tested
[Test, TestCase(GameStateModality.Exclusive), TestCase(GameStateModality.Popup)]
public void TestPushModality(GameStateModality modality) {
var alwaysObscured = new TestGameState();
var potentiallyObscured = new TestGameState();
var active = new TestGameState();
using (var manager = new GameStateManager()) {
manager.Push(alwaysObscured);
manager.Push(potentiallyObscured);
manager.Push(active, modality);
Assert.AreEqual(0, alwaysObscured.UpdateCallCount);
Assert.AreEqual(0, alwaysObscured.DrawCallCount);
Assert.AreEqual(0, potentiallyObscured.UpdateCallCount);
Assert.AreEqual(0, potentiallyObscured.DrawCallCount);
Assert.AreEqual(0, active.UpdateCallCount);
Assert.AreEqual(0, active.DrawCallCount);
manager.Update(new GameTime());
manager.Draw(new GameTime());
Assert.AreEqual(0, alwaysObscured.UpdateCallCount);
Assert.AreEqual(0, alwaysObscured.DrawCallCount);
if (modality == GameStateModality.Exclusive) {
Assert.AreEqual(0, potentiallyObscured.UpdateCallCount);
Assert.AreEqual(0, potentiallyObscured.DrawCallCount);
} else {
Assert.AreEqual(1, potentiallyObscured.UpdateCallCount);
Assert.AreEqual(1, potentiallyObscured.DrawCallCount);
}
Assert.AreEqual(1, active.UpdateCallCount);
Assert.AreEqual(1, active.DrawCallCount);
}
}
///
/// Verifies that an exception whilst pushing a state on the stack leaves the
/// game state manager unchanged
///
[Test]
public void TestPushUnenterableState() {
var test = new TestGameState();
using (var manager = new GameStateManager()) {
manager.Push(test);
Assert.AreSame(test, manager.ActiveState);
Assert.Throws(
delegate() { manager.Push(new UnenterableGameState()); }
);
Assert.AreSame(test, manager.ActiveState);
// Make sure the test state is still running. Whether pause was
// called zero times or more, we only care that it's running after
// the push has failed
Assert.AreEqual(test.OnResumeCallCount, test.OnPauseCallCount);
}
}
///
/// Tests whether the game state manager correctly handles a reentrant call
/// to Push() from a pushed game state
///
[Test]
public void TestReeantrantPush() {
using (var manager = new GameStateManager()) {
var reentrant = new ReentrantGameState(manager);
manager.Push(reentrant);
// The reentrant game state pushes another game state onto the stack in its
// OnEntered() notification. If this causes the stack to be built in the wrong
// order, the ReentrantGameState would become the new active game state instead
// of the sub-game-state it pushed onto the stack.
Assert.AreNotSame(reentrant, manager.ActiveState);
}
}
/// Verifies that the disposal of dropped states in Pop() works
/// Whether to run the test with enabled disposal
[Test, TestCase(true), TestCase(false)]
public void TestDisposalInPop(bool disposalEnabled) {
var test = new TestGameState();
using (var manager = new GameStateManager()) {
manager.DisposeDroppedStates = disposalEnabled;
manager.Push(test);
Assert.AreEqual(0, test.DisposeCallCount);
Assert.AreSame(test, manager.Pop());
if (disposalEnabled) {
Assert.AreEqual(1, test.DisposeCallCount);
} else {
Assert.AreEqual(0, test.DisposeCallCount);
}
}
}
///
/// Verifies that the game state manager correctly rolls back its update
/// and draw lists when an exclusive state is popped from the stack
///
[Test]
public void TestUpdateAndDrawListRollbackInPop() {
var obscured = new TestGameState();
var blocker1 = new TestGameState();
var popup = new TestGameState();
var blocker2 = new TestGameState();
using (var manager = new GameStateManager()) {
manager.Push(obscured);
manager.Push(blocker1);
manager.Push(popup, GameStateModality.Popup);
manager.Update(new GameTime());
Assert.AreEqual(0, obscured.UpdateCallCount);
Assert.AreEqual(1, blocker1.UpdateCallCount);
Assert.AreEqual(1, popup.UpdateCallCount);
Assert.AreEqual(0, blocker2.UpdateCallCount);
manager.Push(blocker2);
manager.Update(new GameTime());
Assert.AreEqual(0, obscured.UpdateCallCount);
Assert.AreEqual(1, blocker1.UpdateCallCount);
Assert.AreEqual(1, popup.UpdateCallCount);
Assert.AreEqual(1, blocker2.UpdateCallCount);
manager.Pop();
manager.Update(new GameTime());
Assert.AreEqual(0, obscured.UpdateCallCount);
Assert.AreEqual(2, blocker1.UpdateCallCount);
Assert.AreEqual(2, popup.UpdateCallCount);
Assert.AreEqual(1, blocker2.UpdateCallCount);
}
}
///
/// Verifies that the game state manager correctly rolls back its update
/// and draw lists when an exclusive state is popped from the stack
///
[Test]
public void TestUpdateAndDrawListRollbackInSwitch() {
var obscured = new TestGameState();
var blocker1 = new TestGameState();
var popup = new TestGameState();
var blocker2 = new TestGameState();
using (var manager = new GameStateManager()) {
manager.Push(obscured);
manager.Push(blocker1);
manager.Push(popup, GameStateModality.Popup);
manager.Update(new GameTime());
Assert.AreEqual(0, obscured.UpdateCallCount);
Assert.AreEqual(1, blocker1.UpdateCallCount);
Assert.AreEqual(1, popup.UpdateCallCount);
Assert.AreEqual(0, blocker2.UpdateCallCount);
manager.Push(blocker2);
manager.Update(new GameTime());
Assert.AreEqual(0, obscured.UpdateCallCount);
Assert.AreEqual(1, blocker1.UpdateCallCount);
Assert.AreEqual(1, popup.UpdateCallCount);
Assert.AreEqual(1, blocker2.UpdateCallCount);
manager.Switch(blocker2, GameStateModality.Popup);
manager.Update(new GameTime());
Assert.AreEqual(0, obscured.UpdateCallCount);
Assert.AreEqual(2, blocker1.UpdateCallCount);
Assert.AreEqual(2, popup.UpdateCallCount);
Assert.AreEqual(2, blocker2.UpdateCallCount);
}
}
/// Verifies that the disposal of switched out states in Switch() works
/// Whether to run the test with enabled disposal
[Test, TestCase(true), TestCase(false)]
public void TestDisposalInSwitch(bool disposalEnabled) {
var test = new TestGameState();
using (var manager = new GameStateManager()) {
manager.DisposeDroppedStates = disposalEnabled;
manager.Push(test);
Assert.AreEqual(0, test.DisposeCallCount);
Assert.AreSame(test, manager.Switch(new TestGameState()));
if (disposalEnabled) {
Assert.AreEqual(1, test.DisposeCallCount);
} else {
Assert.AreEqual(0, test.DisposeCallCount);
}
}
}
///
/// Verifies that switch only replaces the active game state,
/// not the whole stack
///
[Test]
public void TestSwitchOnlyChangesActiveState() {
var obscured = new TestGameState();
var active = new TestGameState();
using (var manager = new GameStateManager()) {
manager.Push(obscured);
manager.Push(active);
Assert.AreEqual(0, obscured.OnLeavingCallCount);
Assert.AreEqual(0, active.OnLeavingCallCount);
manager.Switch(new TestGameState());
Assert.AreEqual(0, obscured.OnLeavingCallCount);
Assert.AreEqual(1, active.OnLeavingCallCount);
}
}
///
/// Verifies that the active game state can be queried
///
[Test]
public void TestActiveState() {
var test = new TestGameState();
using (var manager = new GameStateManager()) {
Assert.IsNull(manager.ActiveState);
manager.Push(test);
Assert.AreSame(test, manager.ActiveState);
}
}
}
} // namespace Nuclex.Game.States
#endif // UNITTEST