// created by momotaro
/*------------------------------------------------------------------------------
= VVSUnit ver.0.03
ViViScriptでUnitTestを行うためのクラス。

== 使い方
このファイルは他のスクリプトに include されて使われることを想定しています。

VVSUnitクラスをextendsしたクラスを作成し、testで始まるメソッドを定義します。
そのメソッドにテストケースを記述します。
テストを行うには後述のassertionメソッドを使用します。

  class TestClass extends VVSUnit {
    function testSample1() {
      this.assertEqual(1, 1);
      this.assertNotEqual(1, 2);
    }
  }

そして、そのクラスのインスタンスを作成し、runメソッドを呼び出します。
runメソッドではtestで始まるメソッドを探して実行していきます。
実行順序は不定です。

  new TestClass().run();

JUnit等他のUnitTestと同じく、setUp/tearDownメソッドを定義することで、
各テストメソッドの前後に初期化処理や終了処理を挟むことができます。

  class TestClass extends VVSUnit {
    function setUp() {
      thisView = newDocument().getView();
      thisView.viCommand("iabc def\x1b1G0");
    }

    function tearDown() {
      thisView.close();
    }

    function testGetCursorPos() {
      thisView.viCommand("w");
      this.assertCursorPos(1, 4);
    }
  }

また、setUp/tearDownに引数を付けると、そこに現在実行する/されたメソッド名が代入されます。
特定のメソッドのみに初期化処理/終了処理をさせたい場合に有効です。

  class TestClass extends VVSUnit {
    function setUp(funcName) {
      // testViewで始まる場合のみviewを作成する。
      if (funcName =~ /^testView/) {
        thisView = newDocument().getView();
        thisView.viCommand("iabc def\x1b1G0");
      }
    }

    function tearDown(funcName) {
      // testViewで始まる場合のみviewの後始末をする。
      if (funcName =~ /^testView/) {
        thisView.close();
      }
    }

    // viewを使うテストなので testView で始める。
    function testViewCursorPos() {
      thisView.viCommand("w");
      this.assertCursorPos(1, 4);
    }

    // viewを使わないテストなので test で始める。
    function testRegexp() {
      this.assertMatch(/^abc/, "abcde");
    }
  }

テストクラス全体に対する、初期化処理/終了処理を定義する方法は提供していませんが、
以下のようにしてあげることで実現できます。

  class TestClass extends VVSUnit {
    function initialize() {
      thisView = newDocument().getView();
      thisView.viCommand("1G0dGiabc def\x1b");
    }

    function terminate() {
      thisView.close();
    }

    function testCursorPos() {
      thisView.viCommand("\x1b1G0");
      thisView.viCommand("w");
      this.assertCursorPos(1, 4);
    }

    function testSelectedRange() {
      thisView.viCommand("\x1b1G0");
      thisView.viCommand("wvv");
      this.assertSelectedRange(1, 4, 1, 7);
    }
  }
  var test = new TestClass();
  test.initialize();
  test.run();
  test.terminate();


== 定義されている assertion
全てのassertionには最後にmessage引数がありますが、省略可能です。
省略した場合、vvsunitで定義されたデフォルトのメッセージが出力されます。

: assert(boolean, message)
引数が true(not 0の数値) であること(!!boolean)を表明します。

: assertFalse(boolean, mesasge)
引数が true(not 0の数値) ではないこと(!boolean)を表明します。
assertの逆です。

: assertEqual(expected, actual, message)
: assertEquals(expected, actual, message)
2つの引数が等価であること(expected == actual)を表明します。

: assertNotEqual(expected, actual, message)
: assertNotEquals(expected, actual, message)
2つの引数が等価でないこと(expected != actual)を表明します。
assertEqualの逆です。

: assertKindOf(clazz, object, message)
引数のオブジェクトが指定したクラスのインスタンスであること(object.isKindOf(clazz.id))を表明します。
clazzには、VSTYPE_〜〜で定義された、いずれかの定数を指定します。

: assertMatch(regexp, string, message)
文字列が正規表現にマッチすること(sring =~ regexp)を表明します。

正規表現オブジェクトから正規表現文字列を取得する方法が無いため、
デフォルトのfailureメッセージが不適切です。
必要に応じてmessage引数に適切なメッセージをセットしてください。

: assertNoMatch(regexp, string, message)
文字列が正規表現にマッチしないこと(!(string =~ regexp))を表明します。
assertMatchの逆です。

: assertCursorPos(line, offset, view, message)
ビューの現在カーソル位置の位置が指定された位置であること
(view.getCursorPos() == {line: line, offset: offset})を表明します。

viewを省略した場合、thisViewを使用します。

: assertSelected(view, message)
ビューが選択状態であること(view.isSelected())を表明します。

viewを省略した場合、thisViewを使用します。

: assertNoSelected(view, message)
ビューが選択状態ではないこと(!view.isSelected())を表明します。
assertSelectedの逆です。

viewを省略した場合、thisViewを使用します。

: assertBoxSelected(view, message)
ビューがBOX選択状態であること(view.isBoxSelected())を表明します。

viewを省略した場合、thisViewを使用します。

: assertNoBoxSelected(view, message)
ビューがBOX選択状態ではないこと(!view.isBoxSelected())を表明します。
assertBoxSelectedの逆です。

viewを省略した場合、thisViewを使用します。

: assertSelectedRange(line1, offset1, line2, offset2, view, message)
ビューの選択箇所が指定された箇所と一致すること
(view.getSelectedRange() == {line1: line1, offset1: offset1, line2: line2, offset2: offset2})
を表明します。

viewを省略した場合、thisViewを使用します。

== これまでの歴史
=== Ver.0.03 (2008/03/12)
* 型を表す定数から「VSTYPE_OUTLINEBAR」「VSTYPE_POINTER」「VSTYPE_CLASS」
  を削除した。

=== Ver.0.02 (2008/03/10)
* ドキュメントを整えた。
  変更履歴も覚えている限り記述した。
* ViViのバグによる暫定対処部分を削除。
  (これによりViVi 2.02.09(RC09)以降でなければ動作しなくなりました)
* assertEquals, assertNotEqualsメソッドを追加。
* 定数名を津田さんのコメントに合わせた。
* ドキュメントを整えたので、サンプルとしてのmain関数を削除。
  呼び出し元でmain関数が使われない場合でも動くようにした。
* 極力定義するメソッドを減らすようにリファクタリングした。
  (サブクラスを作ってテストをするのに、制約は少ない方がよいので)

=== Ver.0.01 (2008/03/06)
最初にニコライに投稿したバージョン。
* 「class」が予約語になってしまったせいで動作しなくなったのを修正した。
* クラス定義ができるようになったので、JUnit等の一般的UnitTestのインターフェースに似せた。
* クラス名をスクリプトファイル名に合わせて、VVSUnitに変更した。
* setUp/tearDownによる初期化処理/終了処理をできるようにした。

=== 昔々 (2004/08/30)
確か、テキストオブジェクトもどきを作るときにテストケースを作りたくて作成。
当時IRCとかで細々と公開していた気がする。

*/

//定数定義
var VSTYPE_NULL				= {id: 0, display: "null"};								//タイプ無し(__argvなど)
var VSTYPE_INTEGER			= {id: 1, display: "number"};							//整数
var VSTYPE_DOUBLE			= {id: 2, display: "flort"};							//浮動小数点
var VSTYPE_STRING			= {id: 3, display: "string"};							//文字列
var VSTYPE_ARRAY			= {id: 4, display: "object"};							//オブジェクト
var VSTYPE_FUNCTION			= {id: 5, display: "function"};							//関数
var VSTYPE_BLTINFUNC		= {id: 6, display: "build in function"};				//組み込み関数
var VSTYPE_TIME				= {id: 7, display: "date"};								//日付・時刻
var VSTYPE_VIEW				= {id: 8, display: "view"};								//ビュー
var VSTYPE_DOC				= {id: 9, display: "document"};							//ドキュメント
var VSTYPE_TYPESTG			= {id: 11, display: "type settings"};					//タイプ別設定
var VSTYPE_FILE				= {id: 12, display: "file"};							//ファイル
var VSTYPE_REGEXP			= {id: 13, display: "regexp"};							//正規表現
var VSTYPE_MATH				= {id: 14, display: "Math"};							//Math
var VSTYPE_EXTVVS_FUNCTION	= {id: 16, display: "ViViScript extention function"};	//ViViScript拡張関数

class VVSUnit {
	/**
	 * ?を、argsで置き換えたメッセージを生成する。
	 * @param message メッセージ内容
	 * @param args ? を置き換えるための内容
	 * @return 生成されたメッセージ
	 */
	function _buildMessage(message, args) {
		var pos;
		for (var i = 0; i < args.getCount(); i++) {
			pos = message.find("?");
			if (pos < 0) break;
			message = message.left(pos) + args[i] + message.mid(pos + 1);
		}
		return message;
	}

	/**
	 * 引数が true(not 0の数値) であること(!!boolean)を表明します。
	 *
	 * @param boolean チェック内容の真偽値
	 * @param message 出力されるメッセージ。省略可
	 */
	function assert(boolean, message) {
		if (message == 0 || message == "") {
			message = this._buildMessage("<?> is not true(1).\n", [boolean]);
		}

		this._count++;
		this._funcCount++;
		if (!boolean) {
			this._assertCount++;
			this._message += format("%s(%d): %s", this._funcName, this._funcCount, message);
		}
	}

	/**
	 * 引数が true(not 0の数値) ではないこと(!boolean)を表明します。
	 * assertの逆です。
	 *
	 * @param boolean チェック内容の真偽値
	 * @param message 出力されるメッセージ。省略可
	 */
	function assertFalse(boolean, message) {
		if (message == 0 || message == "") {
			message = this._buildMessage("<?> is not false(0).\n", [boolean]);
		}

		this.assert(!boolean, message);
	}

	/**
	 * 2つの引数が等価であること(expected == actual)を表明します。
	 * 
	 * @param expected 期待する値
	 * @param actual テストされる値
	 * @param message 出力されるメッセージ。省略可
	 */
	function assertEqual(expected, actual, message) {
		if (message == 0 || message == "") {
			message = this._buildMessage("<?> expected but was <?>.\n", [expected, actual]);
		}

		this.assert((expected == actual), message);
	}

	/** 
	 * 2つの引数が等価であること(expected == actual)を表明します。
	 * assertEqualの別名です。
	 * 
	 * @param expected 期待する値
	 * @param actual テストされる値
	 * @param message 出力されるメッセージ。省略可
	 */
	function assertEquals(expected, actual, message) {
		this.assertEqual(expected, actual, message);
	}

	/**
	 * 2つの引数が等価でないこと(expected != actual)を表明します。
	 * assertEqualの逆です。
	 *
	 * @param expected 期待する値
	 * @param actual テストされる値
	 * @param message 出力されるメッセージ。省略可
	 */
	function assertNotEqual(expected, actual, message) {
		if (message == 0 || message == "") {
			message = this._buildMessage("<?> expected to be != to <?>.\n", [expected, actual]);
		}

		this.assert((expected != actual), message);
	}

	/**
	 * 2つの引数が等価でないこと(expected != actual)を表明します。
	 * assertNotEqualの別名です。
	 *
	 * @param expected 期待する値
	 * @param actual テストされる値
	 * @param message 出力されるメッセージ。省略可
	 */
	function assertNotEquals(expected, actual, message) {
		this.assertNotEqual(expected, actual, message);
	}

	/**
	 * 引数のオブジェクトが指定したクラスのインスタンスであること(object.isKindOf(clazz.id))を表明します。
	 * clazzには、VSTYPE_〜〜で定義された、いずれかの定数を指定します。
	 *
	 * @param clazz 期待するクラス。VSTYPE〜〜 系定数で指定
	 * @param object テストされるオブジェクト
	 * @param message 出力されるメッセージ。省略可
	 */
	function assertKindOf(clazz, object, message) {
		if (message == 0 || message == "") {
			message = this._buildMessage("<?> expected to kind of ?.\n", [object, clazz.display]);
		}

		this.assert((object.isKindOf(clazz.id)), message);
	}

	/**
	 * 文字列が正規表現にマッチすること(sring =~ regexp)を表明します。
	 *
	 * 正規表現オブジェクトから正規表現文字列を取得する方法が無いため、
	 * デフォルトのfailureメッセージが不適切です。
	 * 必要に応じてmessage引数に適切なメッセージをセットしてください。
	 *
	 * @param regexp チェックする正規表現
	 * @param string テストされる文字列
	 * @param message 出力されるメッセージ。省略可
	 */
	function assertMatch(regexp, string, message) {
		if (message == 0 || message == "") {
			message = this._buildMessage("<?> expected to match <?>.\n", [string, regexp]);
		}

		this.assert((string =~ regexp), message);
	}

	/**
	 * 文字列が正規表現にマッチしないこと(!(string =~ regexp))を表明します。
	 * assertMatchの逆です。
	 *
	 * @param regexp チェックする正規表現
	 * @param string テストされる文字列
	 * @param message 出力されるメッセージ。省略可
	 */
	function assertNoMatch(regexp, string, message) {
		if (message == 0 || message == "") {
			message = this._buildMessage("<?> expected to not match <?>.\n", [string, regexp]);
		}

		this.assert(!(string =~ regexp), message);
	}

	/**
	 * ビューの現在カーソル位置の位置が指定された位置であること
	 * (view.getCursorPos() == {line: line, offset: offset})を表明します。
	 *
	 * @param line 行
	 * @param offset オフセット
	 * @param view テストされるビューオブジェクト。省略時は thisView
	 * @param message 出力されるメッセージ。省略可
	 */
	function assertCursorPos(line, offset, view, message) {
		if (!view) {
			view = thisView;
		}
		var pos = view.getCursorPos();

		if (message == 0 || message == "") {
			message = this._buildMessage("<(?, ?)> expected cursor position but was <(?, ?)>.\n",
										[line, offset, pos.line, pos.offset]);
		}

		this.assert((pos == {line: line, offset: offset}), message);
	}

	/**
	 * ビューが選択状態であること(view.isSelected())を表明します。
	 *
	 * @param view テストされるビューオブジェクト。省略時は thisView
	 * @param message 出力されるメッセージ。省略可
	 */
	function assertSelected(view, message) {
		if (!view) {
			view = thisView;
		}

		if (message == 0 || message == "") {
			message = this._buildMessage("<?> expected to selected.\n", [view]);
		}

		this.assert(view.isSelected(), message);
	}

	/**
	 * ビューが選択状態ではないこと(!view.isSelected())を表明します。
	 * assertSelectedの逆です。
	 *
	 * @param view テストされるビューオブジェクト。省略時は thisView
	 * @param message 出力されるメッセージ。省略可
	 */
	function assertNoSelected(view, message) {
		if (!view) {
			view = thisView;
		}

		if (message == 0 || message == "") {
			message = this._buildMessage("<?> expected to not selected.\n", [view]);
		}

		this.assert(!view.isSelected(), message);
	}

	/**
	 * ビューがBOX選択状態であること(view.isBoxSelected())を表明します。
	 *
	 * @param view テストされるビューオブジェクト。省略時は thisView
	 * @param message 出力されるメッセージ。省略可
	 */
	function assertBoxSelected(view, message) {
		if (!view) {
			view = thisView;
		}

		if (message == 0 || message == "") {
			message = this._buildMessage("<?> expected to box selected.\n", [view]);
		}

		this.assert(view.isBoxSelected(), message);
	}

	/**
	 * ビューがBOX選択状態ではないこと(!view.isBoxSelected())を表明します。
	 * assertBoxSelectedの逆です。
	 *
	 * @param view テストされるビューオブジェクト。省略時は thisView
	 * @param message 出力されるメッセージ。省略可
	 */
	function assertNoBoxSelected(view, message) {
		if (!view) {
			view = thisView;
		}

		if (message == 0 || message == "") {
			message = this._buildMessage("<?> expected to not box selected.\n", [view]);
		}

		this.assert(!view.isBoxSelected(), message);
	}

	/**
	 * ビューの選択箇所が指定された箇所と一致すること
	 * (view.getSelectedRange() == {line1: line1, offset1: offset1, line2: line2, offset2: offset2})
	 * を表明します。
	 *
	 * @param line1 開始行
	 * @param offset1 開始オフセット
	 * @param line2 終了行
	 * @param offset2 終了オフセット
	 * @param view テストされるビューオブジェクト。省略時は thisView
	 * @param message 出力されるメッセージ。省略可
	 */
	function assertSelectedRange(line1, offset1, line2, offset2, view, message) {
		if (!view) {
			view = thisView;
		}
		var range = view.getSelectedRange();

		if (message == 0 || message == "") {
			message = this._buildMessage("<(?, ?)-(?, ?)> expected to selected range but was <(?, ?)-(?, ?)>.\n",
										[line1, offset1, line2, offset2,
										range.line1, range.offset1, range.line2, range.offset2]);
		}

		this.assert((range == {line1: line1, offset1: offset1, line2: line2, offset2: offset2}), message);
	}

	/**
	 * テストを実行する。
	 */
	function run() {
		//変数初期化
		this._message = "";
		this._funcName = "";
		this._count = 0;
		this._funcCount = 0;
		this._assertCount = 0;

		//testで始まる関数を探して実行。
		var funcName;
		for(funcName in this) {
			if (funcName =~ /^test/) {
				this._funcName = funcName;
				this._funcCount = 0;

				if (this.setUp.isKindOf(VSTYPE_FUNCTION.id)) this.setUp(funcName);
				this[funcName]();
				if (this.tearDown.isKindOf(VSTYPE_FUNCTION.id)) this.tearDown(funcName);
			}
		}

		//メッセージ出力
		this._message += "\n";
		this._message += this._count + " tests, " + this._assertCount + " failures.";
		console.writeln(this._message);
	}
}