// zlib/libpng License
//
// Copyright (c) 2018 Sinoa
//
// This software is provided 'as-is', without any express or implied warranty.
// In no event will the authors be held liable for any damages arising from the use of this software.
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it freely,
// subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software.
// If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.
using System;
using IceMilkTea.Core;
using NUnit.Framework;
namespace IceMilkTeaTestStatic.Core
{
///
/// IceMilkTea の ImtStateMachine クラスのテストをするクラスです
///
public class ImtStateMachineTest
{
#region テスト用クラスの定義
///
/// 各種テスト確認用ステートの基本ステートクラスです
///
private abstract class SampleBaseState : ImtStateMachine.State
{
///
/// ステート開始時のSampleValue値
///
public abstract int SampleValueEnter { get; }
///
/// ステート更新時のSampleValue値
///
public abstract int SampleValueUpdate { get; }
///
/// ステートの開始処理を行います
///
protected internal override void Enter()
{
// コンテキストの値を設定する
Context.sampleValue = SampleValueEnter;
}
///
/// ステートの更新処理を行います
///
protected internal override void Update()
{
// コンテキストの値を設定する
Context.sampleValue = SampleValueUpdate;
}
}
///
/// テスト確認用ステートAクラスです
///
private class SampleAState : SampleBaseState
{
// 定数定義とプロパティ実装
public const int EnterValue = 100;
public const int UpdateValue = 110;
public override int SampleValueEnter => EnterValue;
public override int SampleValueUpdate => UpdateValue;
///
/// ステートの終了処理を行います
///
protected internal override void Exit()
{
// ステートAが終了した証明
Context.stateAExited = true;
}
}
///
/// テスト確認用ステートBクラスです
///
private class SampleBState : SampleBaseState
{
// 定数定義とプロパティ実装
public const int EnterValue = 200;
public const int UpdateValue = 210;
public override int SampleValueEnter => EnterValue;
public override int SampleValueUpdate => UpdateValue;
///
/// ステートの終了処理を行います
///
protected internal override void Exit()
{
// ステートBが終了した証明
Context.stateBExited = true;
}
}
///
/// テスト確認用ステートCクラスです
///
private class SampleCState : SampleBaseState
{
// 定数定義とプロパティ実装
public const int EnterValue = 300;
public const int UpdateValue = 310;
public override int SampleValueEnter => EnterValue;
public override int SampleValueUpdate => UpdateValue;
///
/// ステートの終了処理を行います
///
protected internal override void Exit()
{
// ステートCが終了した証明
Context.stateCExited = true;
}
}
///
/// Exit 時に SendEvent する許されざるステートクラスです
///
private class ExitSendEventState : ImtStateMachine.State
{
///
/// ステートの終了処理を行います
///
protected internal override void Exit()
{
// 許されざる SendEvent の呼び出しを行う(Assertは、テスト関数側のUpdateで引っ掛ける)
StateMachine.SendEvent(Context.sampleValue);
}
}
///
/// このステートに遷移したら直ちに SendEvent を行うステートクラスです
///
private class ImmediateTransitionState : ImtStateMachine.State
{
// 定数定義
public const int EventId = 12345;
///
/// ステートの開始処理を行います
///
protected internal override void Enter()
{
// 直ちにSendEventして遷移準備に入る(遷移テーブルが構築されていれば)
StateMachine.SendEvent(EventId);
}
///
/// ステートの更新処理を行います
///
protected internal override void Update()
{
// 触れてないフラグに思いっきり触れているように見えるが Enter でSendEventしているため
// 遷移に成功していれば、まず触れることはないので安心
Context.dontTouch = true;
}
///
/// ステートの終了処理を行います
///
protected internal override void Exit()
{
// 直ちに遷移した証明
Context.immediateExited = true;
}
}
///
/// このステートに遷移して更新処理された時に SendEvent を行うステートクラスです
///
private class UpdateTransitionState : ImtStateMachine.State
{
// 定数定義
public const int EvnetId = 123456;
public const int EnterValue = 300;
///
/// ステートの開始処理を行います
///
protected internal override void Enter()
{
// サンプル値を設定する
Context.sampleValue = EnterValue;
}
///
/// ステートの更新処理を行います
///
protected internal override void Update()
{
// SendEventして遷移準備に入る(遷移テーブルが構築されていれば)
StateMachine.SendEvent(EvnetId);
}
///
/// ステートの終了処理を行います
///
protected internal override void Exit()
{
// 遷移した証明
Context.updateExited = true;
}
}
///
/// コンテキストのガード有効値に基づいて、ガード制御を行うステートクラスです
///
private class ProGuardState : ImtStateMachine.State
{
///
/// ステートマシンのイベントをガードします
///
/// 送られたイベントID
/// ガードする場合は true を、ガードしない場合は false を返します
protected internal override bool GuardEvent(int eventId)
{
// 送られてきたイベントIDをサンプル値に設定して、コンテキストのガード有効値をそのまま返す
Context.sampleValue = eventId;
return Context.enableGuardEvent;
}
///
/// ステートマシンのステートスタックポップをガードします
///
/// ガードする場合は true を、ガードしない場合は false を返します
protected internal override bool GuardPop()
{
// コンテキストのガード有効値をそのまま返す
return Context.enableGuardPop;
}
}
///
/// ステート開始時にアップデートプロパティをキャプチャするステートクラスです
///
private class EnterUpdateCaptureState : ImtStateMachine.State
{
///
/// ステートの開始処理を行います
///
protected internal override void Enter()
{
// Updateプロパティを設定する
Context.updateCapture = StateMachine.Updating;
}
}
///
/// ステート更新時にアップデートプロパティをキャプチャするステートクラスです
///
private class UpdateUpdateCaptureState : ImtStateMachine.State
{
///
/// ステートの更新処理を行います
///
protected internal override void Update()
{
// Updateプロパティを設定する
Context.updateCapture = StateMachine.Updating;
}
}
///
/// ステート終了時にアップデートプロパティをキャプチャするステートクラスです
///
private class ExitUpdateCaptureState : ImtStateMachine.State
{
///
/// ステートの終了処理を行います
///
protected internal override void Exit()
{
// Updateプロパティを設定する
Context.updateCapture = StateMachine.Updating;
}
}
///
/// Enter時に sampleValue が 0 の場合は、例外を発生させるステートクラスです。
///
private class ForceEnterExceptionState : ImtStateMachine.State
{
///
/// ステートの開始処理を行います
///
protected internal override void Enter()
{
// sampleValue が 0 なら
if (Context.sampleValue == 0)
{
// 例外を即座に発生
throw new InvalidOperationException("この Enter エラーを拾ってください");
}
}
}
///
/// Update時に sampleValue が 0 の場合は、例外を発生させるステートクラスです
/// また例外モードが CatchStateException の場合は MyEventId の遷移イベントを送出します
///
private class ForceUpdateExceptionState : ImtStateMachine.State
{
///
/// CatchStateException 時に送出するイベントID
///
public const int MyEventId = -1;
///
/// ステートの更新処理を行います
///
protected internal override void Update()
{
// sampleValue が 0 なら
if (Context.sampleValue == 0)
{
// 例外を即座に発生
throw new InvalidOperationException("この Update エラーを拾ってください");
}
}
///
/// ステートのエラーハンドリングを行います
///
/// 発生した例外
/// 例外をハンドリングした場合は true を、ハンドリング出来なかった場合は false を返します
protected internal override bool Error(Exception exception)
{
// 自分の遷移IDを送出してハンドリング済みを返す
stateMachine.SendEvent(MyEventId);
return true;
}
}
///
/// Exit時に sampleValue が 0 の場合は、例外を発生させるステートクラスです
///
private class ForceExitExceptionState : ImtStateMachine.State
{
///
/// ステートの終了処理を行います
///
protected internal override void Exit()
{
// sampleValue が 0 なら
if (Context.sampleValue == 0)
{
// 例外を即座に発生
throw new InvalidOperationException("この Exit エラーを拾ってください");
}
}
}
///
/// ステートクラスのインスタンス生成に失敗するテスト用クラスです
///
private class CreateStateInstanceMissStateMachine : ImtStateMachine
{
///
/// CreateStateInstanceMissStateMachine クラスのインスタンスを初期化します
///
/// このステートマシンが持つべきコンテキスト
public CreateStateInstanceMissStateMachine(ImtStateMachineTest context) : base(context)
{
}
///
/// 必ずインスタンスの生成に失敗します
///
/// 生成するべきステートの型
/// 必ず null を返します
protected override TState CreateStateInstance()
{
// 無条件で失敗する
return null;
}
}
#endregion
// メンバ変数定義
private int sampleValue;
private bool stateAExited;
private bool stateBExited;
private bool stateCExited;
private bool dontTouch;
private bool immediateExited;
private bool updateExited;
private bool enableGuardEvent;
private bool enableGuardPop;
private bool updateCapture;
///
/// ステートマシンのコンストラクタのテストをします
///
[Test]
public void ConstructorTest()
{
// null渡しによるインスタンスの生成は許されていないのでテスト
Assert.Throws(() => new ImtStateMachine(null));
Assert.DoesNotThrow(() => new ImtStateMachine(this));
}
///
/// ステートマシンのステート遷移テーブルの構築テストをします
///
[Test]
public void StateTransitionTableBuildTest()
{
// ステートマシンのインスタンスを生成する
var stateMachine = new ImtStateMachine(this);
// イベントIDの重複しないステート遷移なら問題ない事を確認する
Assert.DoesNotThrow(() => stateMachine.AddTransition(1)); // A[1] -> B
Assert.DoesNotThrow(() => stateMachine.AddTransition(2)); // A[2] -> B
Assert.DoesNotThrow(() => stateMachine.AddTransition(3)); // A[3] -> A
Assert.DoesNotThrow(() => stateMachine.AddTransition(1)); // B[1] -> A
Assert.DoesNotThrow(() => stateMachine.AddTransition(2)); // B[2] -> A
Assert.DoesNotThrow(() => stateMachine.AddTransition(3)); // B[3] -> B
Assert.DoesNotThrow(() => stateMachine.AddAnyTransition(1)); // Any[1] -> A
Assert.DoesNotThrow(() => stateMachine.AddAnyTransition(2)); // Any[2] -> B
// 既に同じイベントIDの遷移が存在した場合、例外が吐かれる事を確認する
Assert.Throws(() => stateMachine.AddTransition(1)); // A[1] -> B
Assert.Throws(() => stateMachine.AddTransition(2)); // B[2] -> A
Assert.Throws(() => stateMachine.AddAnyTransition(1)); // Any[1] -> A
// 開始ステートの設定はまだ可能であることを確認する
Assert.DoesNotThrow(() => stateMachine.SetStartState());
Assert.DoesNotThrow(() => stateMachine.SetStartState());
// ステートマシンを起動する
stateMachine.Update();
// 起動してしまったステートマシンは、二度と遷移テーブルを編集出来ないことを確認する(また、既に登録済み遷移でも ArgumentException ではなく起動中例外が吐かれることを確認する)
Assert.Throws(() => stateMachine.AddTransition(1)); // A[1] -> B
Assert.Throws(() => stateMachine.AddTransition(4)); // A[4] -> B
Assert.Throws(() => stateMachine.AddTransition(1)); // B[1] -> A
Assert.Throws(() => stateMachine.AddTransition(4)); // B[4] -> A
Assert.Throws(() => stateMachine.AddAnyTransition(1)); // Any[1] -> A
Assert.Throws(() => stateMachine.AddAnyTransition(3)); // Any[3] -> A
// 本来登録されていないはずの遷移イベント(A[4] -> B)を送って、ステートマシンを更新後、遷移されていないことを確認する
Assert.IsTrue(stateMachine.IsCurrentState());
stateMachine.SendEvent(4);
stateMachine.Update();
Assert.IsTrue(stateMachine.IsCurrentState());
}
///
/// ステートマシンのイベント送信テストをします
///
[Test]
public void SendEventTest()
{
// ステートマシンのインスタンスを生成してサクッと遷移テーブルを構築する
var stateMachine = new ImtStateMachine(this);
stateMachine.AddTransition(1);
stateMachine.AddTransition(1);
stateMachine.AddTransition(2);
stateMachine.AddTransition(1);
stateMachine.SetStartState();
// ステートマシンが未起動状態でSendEventしたら例外が吐かれることを確認する
Assert.Throws(() => stateMachine.SendEvent(1));
// ステートマシンを起動して ExitSendEventState へ遷移するイベントを送って正しく終了することを確認する(ついでに遷移準備済みの確認もする)
stateMachine.Update(); // Wakeup
Assert.DoesNotThrow(() => stateMachine.SendEvent(1));
Assert.IsFalse(stateMachine.SendEvent(1)); // 遷移準備済みなので false が返ってくるはず
stateMachine.Update(); // A -> B
Assert.IsTrue(stateMachine.SendEvent(2)); // 初遷移なので true が返ってくるはず
stateMachine.AllowRetransition = true; // 再遷移を許可する
Assert.IsTrue(stateMachine.SendEvent(2)); // 本来なら遷移済みなので false だが、再遷移を許可しているので true になる
stateMachine.AllowRetransition = false; // 再遷移を禁止する
stateMachine.Update(); // B -> ExitSendEvent
// SendEventする前にスタックに積んで下ろした時SendEventが失敗することを確認する(PopStateが遷移状態にさせるため)
stateMachine.PushState();
stateMachine.PopState();
Assert.IsFalse(stateMachine.SendEvent(1));
// このタイミングの Update にて ExitSendEvent -> ExitSendEvent になる予定だが
// ExitSendEvent.Exit で 許されざる SendEvent をしているため例外が吐かれる
Assert.Throws(() => stateMachine.Update());
}
///
/// ステートの遷移テストをします
///
[Test]
public void StateTransitionTest()
{
// 念の為検査用メンバ変数の初期化を行う
sampleValue = 0;
stateAExited = false;
stateBExited = false;
stateCExited = false;
immediateExited = false;
updateExited = false;
dontTouch = false;
// ステートマシンのインスタンスを生成してサクッと遷移テーブルを構築する
var stateMachine = new ImtStateMachine(this);
stateMachine.AddTransition(1);
stateMachine.AddTransition(1);
stateMachine.AddTransition(1);
stateMachine.AddTransition(2);
stateMachine.AddTransition(ImmediateTransitionState.EventId);
stateMachine.AddTransition(UpdateTransitionState.EvnetId);
stateMachine.AddAnyTransition(10);
stateMachine.SetStartState();
// ステートマシンを起動する
stateMachine.Update();
// サンプル値が、起動値と更新値の想定値になっていることを確認する(念の為IsCurrentStateも見る)
Assert.IsTrue(stateMachine.IsCurrentState());
Assert.AreEqual(SampleAState.EnterValue, sampleValue);
stateMachine.Update();
Assert.IsTrue(stateMachine.IsCurrentState());
Assert.AreEqual(SampleAState.UpdateValue, sampleValue);
// ステート遷移を行って一つ前のステートが終了し、サンプル値が、起動地と更新値の想定地になっていることを確認する
stateMachine.SendEvent(1);
stateMachine.Update();
Assert.IsTrue(stateAExited);
Assert.AreEqual(SampleBState.EnterValue, sampleValue);
stateMachine.Update();
Assert.AreEqual(SampleBState.UpdateValue, sampleValue);
// 任意ステートからの遷移(イベントID10で、StateBステートからなく、任意遷移にはあるので)を行い同上の想定判断を確認する
stateMachine.SendEvent(10);
stateMachine.Update();
Assert.IsTrue(stateBExited);
Assert.AreEqual(SampleCState.EnterValue, sampleValue);
stateMachine.Update();
Assert.AreEqual(SampleCState.UpdateValue, sampleValue);
// 直ちに遷移するステートに遷移を行い、直ちに遷移が行われたかの形跡を確認をする
Assert.IsFalse(dontTouch);
Assert.IsFalse(immediateExited);
stateMachine.SendEvent(1);
stateMachine.Update();
Assert.IsTrue(stateCExited);
Assert.AreEqual(SampleAState.EnterValue, sampleValue);
Assert.IsFalse(dontTouch);
Assert.IsTrue(immediateExited);
// ひとまずステートCへ遷移する
stateMachine.SendEvent(10);
stateMachine.Update();
// 更新時に遷移するステートに遷移を行い、想定されたサンプル値になるか確認後、更新時に遷移されたかも確認する
stateMachine.SendEvent(2);
stateMachine.Update();
Assert.AreEqual(UpdateTransitionState.EnterValue, sampleValue);
Assert.IsFalse(updateExited);
stateMachine.Update();
Assert.AreEqual(SampleBState.EnterValue, sampleValue);
Assert.IsTrue(updateExited);
}
///
/// ステートによる遷移ガードのテストをします
///
[Test]
public void StateTransitionGuardTest()
{
// 念の為検査用メンバ変数の初期化を行う
sampleValue = 0;
// ステートマシンのインスタンスを生成してサクッと遷移テーブルを構築する
var stateMachine = new ImtStateMachine(this);
stateMachine.AddTransition(1);
stateMachine.AddTransition(1);
stateMachine.AddTransition(1);
stateMachine.AddTransition(2);
stateMachine.SetStartState();
// 起動して早速遷移する
stateMachine.Update();
stateMachine.SendEvent(1);
stateMachine.Update();
Assert.IsTrue(stateMachine.IsCurrentState());
// イベント、ポップともに遷移をガードする状態にする
enableGuardEvent = true;
enableGuardPop = true;
// StateAへ遷移したいがイベントがガードされて、StateBにも行こうとしてもガードされることを確認する
Assert.IsFalse(stateMachine.SendEvent(1));
Assert.AreEqual(1, sampleValue);
Assert.IsFalse(stateMachine.SendEvent(2));
Assert.AreEqual(2, sampleValue);
// ガードステートをプッシュしてポップすらもガードされることを確認する
stateMachine.PushState();
Assert.IsFalse(stateMachine.PopState());
// イベントのみガードを無効化してStateAへ遷移する準備をして成功したことを確認する
enableGuardEvent = false;
Assert.IsTrue(stateMachine.SendEvent(1));
Assert.AreEqual(1, sampleValue);
// 遷移をして希望のステートになっていることを確認
stateMachine.Update();
Assert.IsTrue(stateMachine.IsCurrentState());
// 本来はSendEventしてProGuardへ遷移するが、ステートスタックにはProGuardが積まれているので戻れる
enableGuardPop = false;
Assert.IsTrue(stateMachine.PopState());
stateMachine.Update();
// 戻ってこれた事を確認
Assert.IsTrue(stateMachine.IsCurrentState());
}
///
/// ステートマシンの現在処理中ステートのチェックテストをします
///
[Test]
public void CurrentStateCheckTest()
{
// ステートマシンのインスタンスを生成してサクッと遷移テーブルを構築する
var stateMachine = new ImtStateMachine(this);
stateMachine.AddTransition(1);
stateMachine.AddTransition(1);
stateMachine.SetStartState();
// まだ、ステートマシンが起動していないので、現在のステート確認を叩くと例外が吐かれることを確認する
Assert.Throws(() => stateMachine.IsCurrentState());
// ステートマシンを起動して、遷移をしながら想定の現在ステート確認結果が返ってくることを確認する
stateMachine.Update();
Assert.IsTrue(stateMachine.IsCurrentState());
Assert.IsFalse(stateMachine.IsCurrentState());
stateMachine.SendEvent(1);
stateMachine.Update();
Assert.IsFalse(stateMachine.IsCurrentState());
Assert.IsTrue(stateMachine.IsCurrentState());
stateMachine.SendEvent(1);
stateMachine.Update();
Assert.IsTrue(stateMachine.IsCurrentState());
Assert.IsFalse(stateMachine.IsCurrentState());
}
///
/// ステートマシンのステートスタック操作のテストをします
///
[Test]
public void StateStackTest()
{
// ステートマシンのインスタンスを生成してサクッと遷移テーブルを構築する
var stateMachine = new ImtStateMachine(this);
stateMachine.AddTransition(1);
stateMachine.AddTransition(1);
stateMachine.SetStartState();
// ステートマシンが起動していない状態で、プッシュやポップをしようとすると例外が吐かれることを確認する
Assert.Throws(() => stateMachine.PushState());
Assert.Throws(() => stateMachine.PopState());
Assert.Throws(() => stateMachine.PopAndDirectSetState());
// ステートマシンを起動すれば例外が吐かれない事を確認する
stateMachine.Update();
Assert.DoesNotThrow(() => stateMachine.PushState());
Assert.DoesNotThrow(() => stateMachine.PopAndDirectSetState());
Assert.DoesNotThrow(() => stateMachine.PushState());
Assert.DoesNotThrow(() => stateMachine.PopState());
// ポップしたことで遷移準備状態になっているのでひとまず更新
stateMachine.Update();
// ステートA、B、Cで順にプッシュしながら遷移する
Assert.IsTrue(stateMachine.IsCurrentState());
stateMachine.PushState(); // [A]
stateMachine.SendEvent(1);
stateMachine.Update();
Assert.IsTrue(stateMachine.IsCurrentState());
stateMachine.PushState(); // [A,B]
stateMachine.SendEvent(1);
stateMachine.Update();
Assert.IsTrue(stateMachine.IsCurrentState());
stateMachine.PushState(); // [A,B,C]
// この段階でスタックには3段階のステートが積まれている状態のはず
Assert.AreEqual(3, stateMachine.StackCount);
// 本来 C->B->A の遷移テーブルは構築されていないので
// このような遷移は不可能だが、今回はプッシュしながら遷移したので
// ポップしながら状態を更新すれば、実現されることを確認する
Assert.IsTrue(stateMachine.PopState()); // [A,B] -> C
stateMachine.Update();
Assert.IsTrue(stateMachine.IsCurrentState());
Assert.IsTrue(stateMachine.PopState()); // [A] -> B
stateMachine.Update();
Assert.IsTrue(stateMachine.IsCurrentState());
Assert.IsTrue(stateMachine.PopState()); // [] -> A
stateMachine.Update();
Assert.IsTrue(stateMachine.IsCurrentState());
Assert.AreEqual(0, stateMachine.StackCount);
// もう一度プッシュして遷移をする
stateMachine.PushState();
stateMachine.SendEvent(1);
stateMachine.Update();
// 遷移を準備してから、ポップをしたいが、遷移準備が完了した状態ではポップできないことを確認する
Assert.IsTrue(stateMachine.SendEvent(1));
Assert.IsFalse(stateMachine.PopState());
// ステートマシンを更新するとAではなくCへ遷移していることを確認し、ポップ出来て戻ってこれることを確認
stateMachine.Update();
Assert.IsTrue(stateMachine.IsCurrentState());
Assert.IsTrue(stateMachine.PopState());
stateMachine.Update();
Assert.IsTrue(stateMachine.IsCurrentState());
// ステートA、B、Cで順にプッシュしながら遷移する
stateMachine.PushState(); // [A]
stateMachine.SendEvent(1);
stateMachine.Update();
Assert.IsTrue(stateMachine.IsCurrentState());
stateMachine.PushState(); // [A,B]
stateMachine.SendEvent(1);
stateMachine.Update();
Assert.IsTrue(stateMachine.IsCurrentState());
stateMachine.PushState(); // [A,B,C]
// ステートスタックのトップをただただ捨てるだけして数が減っていることを確認する
Assert.AreEqual(3, stateMachine.StackCount);
stateMachine.PopAndDropState(); // [A,B] -> C
Assert.AreEqual(2, stateMachine.StackCount);
// この段階でポップ遷移をするとCは捨てられたのでBに戻ることを期待する
// (直前のポップは、ただ捨てるだけで遷移はしないので、今回のポップは成功するはず)
Assert.IsTrue(stateMachine.PopState()); // [A] -> B
stateMachine.Update();
Assert.IsTrue(stateMachine.IsCurrentState());
// ここでもう一度プッシュして、スタック段数が2段になることを確認
stateMachine.PushState();
Assert.AreEqual(2, stateMachine.StackCount);
// 今度はスタックを空にして、空になったことを確認しポップも出来ないことを確認
stateMachine.ClearStack();
Assert.AreEqual(0, stateMachine.StackCount);
Assert.IsTrue(stateMachine.IsCurrentState());
Assert.IsFalse(stateMachine.PopState());
// ステート更新をしてもスタックから復帰できるわけでも無いので、ステータスが現状維持になっていることを確認
stateMachine.Update();
Assert.IsTrue(stateMachine.IsCurrentState());
}
///
/// ステートマシンの提供するプロパティのテストをします
///
[Test]
public void StateMachinePropertyTest()
{
// 念の為検査用メンバ変数の初期化を行う
updateCapture = false;
// ステートマシンのインスタンスを生成してサクッと遷移テーブルを構築する
var stateMachine = new ImtStateMachine(this);
stateMachine.AddTransition(1);
stateMachine.AddTransition(1);
stateMachine.AddTransition(1);
stateMachine.AddAnyTransition(10); // 自爆ステート
stateMachine.SetStartState();
// 未起動状態時のプロパティ値を検証する
Assert.IsInstanceOf(stateMachine.Context);
Assert.AreEqual(this, stateMachine.Context);
Assert.IsFalse(stateMachine.Running);
Assert.IsFalse(stateMachine.Updating);
Assert.IsFalse(updateCapture);
Assert.AreEqual(0, stateMachine.StackCount);
// 起動してプロパティ値を検証する(Runningはステートマシンが起動しているかどうか、Updatingはステートマシンが Update 処理中かどうか)
stateMachine.Update();
Assert.IsInstanceOf(stateMachine.Context);
Assert.AreEqual(this, stateMachine.Context);
Assert.IsTrue(stateMachine.Running);
Assert.IsFalse(stateMachine.Updating);
Assert.IsTrue(updateCapture); // Updatingの代わりにステートで拾ってもらった
Assert.AreEqual(0, stateMachine.StackCount);
// 更新キャプチャをクリアして遷移
updateCapture = false;
stateMachine.SendEvent(1);
stateMachine.Update();
// プロパティ値を検証
Assert.IsInstanceOf(stateMachine.Context);
Assert.AreEqual(this, stateMachine.Context);
Assert.IsTrue(stateMachine.Running);
Assert.IsFalse(stateMachine.Updating);
Assert.IsFalse(updateCapture); // Updateのタイミングで拾うのでまだfalse
Assert.AreEqual(0, stateMachine.StackCount);
// 遷移せず更新してプロパティ値を検証する
stateMachine.Update();
Assert.IsInstanceOf(stateMachine.Context);
Assert.AreEqual(this, stateMachine.Context);
Assert.IsTrue(stateMachine.Running);
Assert.IsFalse(stateMachine.Updating);
Assert.IsTrue(updateCapture); // Updatingの代わりにステートで拾ってもらった
Assert.AreEqual(0, stateMachine.StackCount);
// 更新キャプチャをクリアして遷移
updateCapture = false;
stateMachine.SendEvent(1);
stateMachine.Update();
// プロパティ値を検証
Assert.IsInstanceOf(stateMachine.Context);
Assert.AreEqual(this, stateMachine.Context);
Assert.IsTrue(stateMachine.Running);
Assert.IsFalse(stateMachine.Updating);
Assert.IsFalse(updateCapture); // Exitのタイミングで拾うのでまだfalse
Assert.AreEqual(0, stateMachine.StackCount);
// 自爆ステートへ遷移
stateMachine.SendEvent(10);
stateMachine.Update();
// プロパティ値を検証
Assert.IsInstanceOf(stateMachine.Context);
Assert.AreEqual(this, stateMachine.Context);
Assert.IsTrue(stateMachine.Running);
Assert.IsFalse(stateMachine.Updating);
Assert.IsTrue(updateCapture); // Updatingの代わりにステートで拾ってもらった
Assert.AreEqual(0, stateMachine.StackCount);
// スタック操作をしまくってスタックカウント値が想定値になるか確認
stateMachine.PushState();
Assert.AreEqual(1, stateMachine.StackCount);
stateMachine.PushState();
stateMachine.PushState();
stateMachine.PushState();
Assert.AreEqual(4, stateMachine.StackCount);
stateMachine.PopAndDropState();
stateMachine.PopAndDropState();
Assert.AreEqual(2, stateMachine.StackCount);
stateMachine.ClearStack();
Assert.AreEqual(0, stateMachine.StackCount);
// キャプチャをクリアしてステートマシンを更新してまだ死なない事を確認
updateCapture = false;
Assert.DoesNotThrow(() => stateMachine.Update());
// ステートのスタック操作を行って、想定スタック値になっていることを確認
stateMachine.PushState();
Assert.AreEqual(1, stateMachine.StackCount);
stateMachine.PopState();
Assert.IsInstanceOf(stateMachine.Context);
Assert.AreEqual(this, stateMachine.Context);
Assert.IsTrue(stateMachine.Running);
Assert.IsFalse(stateMachine.Updating);
Assert.IsFalse(updateCapture);
Assert.AreEqual(0, stateMachine.StackCount);
// このステート更新は自爆ステートによって例外が吐かれるので例外チェックを行う
// また Update 中に例外により死んだステートマシンは、もうどうにもならないのでインスタンスの生成し直しが必要
// このチェックは(UnhandledExceptionModeがThrowException(既定)である場合の挙動)
Assert.Throws(() => stateMachine.Update());
// MENO : UnhandledExceptionモード周りが実装された事により、死ぬことはなくなりました。
// ステートマシンが死んだ状態プロパティ検証
// もちろんステートマシンが死んでいるのでこのタイミングでも Updating は true を示す
//Assert.IsInstanceOf(stateMachine.Context);
//Assert.AreEqual(this, stateMachine.Context);
//Assert.IsTrue(stateMachine.Running);
//Assert.IsTrue(stateMachine.Updating);
//Assert.IsFalse(updateCapture);
//Assert.AreEqual(0, stateMachine.StackCount);
}
///
/// ステートマシンのUpdateで発生した例外のハンドリングを行うテスト
///
[Test]
public void UnhandledExceptionTest()
{
// 念の為検査用メンバ変数の初期化を行う
sampleValue = 0;
// ステートマシンのインスタンスを生成して、この救いようのない例外発生しまくり遷移テーブルを構築する
var stateMachine = new ImtStateMachine(this);
stateMachine.AddTransition(1);
stateMachine.AddTransition(1);
stateMachine.AddTransition(ForceUpdateExceptionState.MyEventId);
stateMachine.AddTransition(1);
stateMachine.SetStartState();
// まずは起動するが例外が発生するのと起動が未起動かチェック(例外が発生した場合は起動は失敗する)
Assert.Throws(() => stateMachine.Update());
Assert.IsFalse(stateMachine.Running);
// 起動可能状態にして起動して確認
sampleValue = 1;
Assert.DoesNotThrow(() => stateMachine.Update());
Assert.IsTrue(stateMachine.Running);
sampleValue = 0;
// 遷移してUpdateへ
stateMachine.SendEvent(1);
stateMachine.Update();
// Updateで例外が発生するかどうか(既定は ThrowException)
Assert.Throws(() => stateMachine.Update());
// CatchExceptionモードに変えても例外が発生するかどうか(イベント未設定時は ThrowException と同等の動作)
stateMachine.UnhandledExceptionMode = ImtStateMachineUnhandledExceptionMode.CatchException;
Assert.Throws(() => stateMachine.Update());
// イベントを設定しても false を返したら例外が発生するかどうか
var returnValue = false;
stateMachine.UnhandledException += error => returnValue;
Assert.Throws(() => stateMachine.Update());
// trueを返せば例外は発生しないはず
returnValue = true;
Assert.DoesNotThrow(() => stateMachine.Update());
// 例外を発生させない様にして遷移する
sampleValue = 1;
stateMachine.SendEvent(1);
stateMachine.Update();
// Exit時に例外が発生するのを確認する
stateMachine.UnhandledExceptionMode = ImtStateMachineUnhandledExceptionMode.ThrowException;
sampleValue = 0;
stateMachine.SendEvent(1);
Assert.Throws(() => stateMachine.Update());
// 例外が出ないようにしてUpdateまで遷移する
sampleValue = 1;
stateMachine.SendEvent(1);
stateMachine.Update();
stateMachine.SendEvent(1);
stateMachine.Update();
// ステート側エラーハンドリングモードにして例外が発生してもステート内でエラーハンドリングして遷移されることを確認
stateMachine.UnhandledExceptionMode = ImtStateMachineUnhandledExceptionMode.CatchStateException;
sampleValue = 0;
stateMachine.SendEvent(1);
Assert.DoesNotThrow(() => stateMachine.Update()); // ステート側でエラーハンドリングしているためココでは例外は発生しない
stateMachine.Update();
Assert.IsTrue(stateMachine.IsCurrentState());
}
///
/// ステートのインスタンス生成ロジックが正しく動作するかのテスト
///
[Test]
public void CreateStateInstanceTest()
{
// ステートマシンを生成して簡単にステートの生成に失敗するか試みる
var stateMachine = new CreateStateInstanceMissStateMachine(this);
Assert.Throws(() => stateMachine.AddAnyTransition(0));
// レジスタとアンレジスタ関数の失敗を確認する
Assert.Throws(() => stateMachine.RegisterStateFactory(null));
Assert.Throws(() => stateMachine.UnregisterStateFactory(null));
// ステート生成関数を登録して今度は問題なく生成されるか確認をする
stateMachine.RegisterStateFactory(type =>
{
if (typeof(CreateStateInstanceMissStateMachine.AnyState) == type)
{
return new CreateStateInstanceMissStateMachine.AnyState();
}
else if (typeof(SampleAState) == type)
{
return new SampleAState();
}
return null;
});
Assert.DoesNotThrow(() => stateMachine.AddAnyTransition(0));
}
}
}