自作テストツールでもJenkinsで結果を可視化したい

## 経緯
C++で書いたコードのテストをしてテスト結果をJenkinsで可視化したい。だが、いかんせん低スペックな私にはその辺りの環境設定がわからん。周りからはとりあえず早くと急かされる。Jenkinsが読み込めるのはJUnitが出力する形式で書かれたXMLファイル。つまり、(わから)ないなら力業でその形式のXMLを書けばよかろうなのだ。
という頭の悪い発想に思い至ってしまったのが事の発端です。

## JUnitの出力するXMLの構造を見てみる
今回は、こちらを参考にさせていただきました。
[JUnitの実行結果のXMLフォーマット](https://dev.classmethod.jp/testing/unittesting/junit-xml-format/)

私の稚拙な説明よりよっぽど明快に解説してくださっているので上のページを読んでいただければ十分かとは思いますが、概説すると

- testsuite
	- name:テストスイート名(テストクラス名,テストのグループ名)
	- tests:testsuiteに含まれるテストの総件数
	- failures:testsuiteに含まれる失敗(AssertionError)となったテストの総件数
	- errors:testsuiteに含まれるエラー(AssertionError以外の例外)となったテストの総件数
	- time:testsuiteに含まれるテストの総実行時間(秒)
- testcase:testsuite要素の子要素
	- classname:テストケースの属するクラス名
	- name:テストケースの名前(半角数字で指定する?)
	- time:テストケースの実行時間(秒)
- failure:テストが失敗した場合に追加するtestcase要素の子要素
	- type:失敗の種類(例外クラス名など)
	- message:例外に含まれるメッセージ
- error:テストが失敗でなくてエラーだった場合に利用。基本failure要素と同じ
- skipped:テストがスキップされた時に追加されるtestcase要素の子要素
- system-out:テスト実行時に標準出力に出力された情報をCDATAとして記述
- system-err:標準エラー出力について記述する以外はsystem-out要素と同じ

といった要素(スキーマ)を記述すればよさそうです。
恐らく日本語が混じっていてはダメです。

## 実装
ものすごく雑に組んでみます。実際は、テスト条件や期待値を外部ファイルから設定してテスト関数を動かすなど、もう少し工夫しています。
`MakeXMLResult()`が最終的に出力を行っているところです。

```cpp
#include <string>
#include <vector>
#include <fstream>
#include <iostream>

using namespace std;

#define SUCCESS 0
#define FAILURE 1
#define EXCEPTION 2

struct SingleResult {
	int no;
	double time;
	int result;
	string name;
	string type;
	string message;
};

struct TotalResult{
	int test_tot_num;
	int ok_num;
	int error_num;
	int failure_num;
	double test_tot_time;
};


class Test{
private:
public:
	string OutputStdMessage();
	string ErrorMessage();
	void MakeXMLResult(string outputpath,vector<SingleResult> results,TotalResult tot_result);
	SingleResult DoTest();
	TotalResult CalcTotalTest(vector<SingleResult> results);
};

string Test::OutputStdMessage(){
	return "std message";
}

string Test::ErrorMessage(){
	if(){//1件でもエラーか失敗があれば
		return "Some Failure or Error Tests Caused";
		}
	else{
		return "All Test Success";
		}
}

void Test::MakeXMLResult(string outputpath,vector<SingleResult> results,TotalResult tot_result){
	char c_filepath[_MAX_PATH];
	sprintf_s(c_filepath,"%s",outputpath.c_str());
	ofstream output(c_filepath);

	output << "<?xml version=\"1.0\" ?>" << endl;
	output << "<testsuite name=\"" << tot_result.testname << "\" " 
		<< "tests=\"" << tot_result.test_tot_num << "\" "
		<< "errors=\"" << tot_result.error_num << "\" "
		<< "failures=\"" << tot_result.failure_num << "\" "
		<< "time=\"" << tot_result.test_tot_time 
		<< "\">" << endl;

	for(SingleResult result : results){
		if(result.result == SUCCESS){
			output << "<testcase classname=\"" << tot_result.testname << "\" "
				<< "name=\"" << result.name << "\" "
				<< "time=\"" << result.time << "\" />" << endl;
		}
		else if(result.result == FAILURE){
			output << "<testcase classname=\"" << tot_result.testname << "\" "
				<< "name=\"" << result.name << "\" "
				<< "time=\"" << result.time << "\" >" << endl;
			output << "<failure type=\"" << result.type << "\" "
				<< "message=\"" << result.message << "\" >"
				<< endl;
			output << "</failure>" << endl;
			output << "</testcase>" << endl;
			}
		else if(result.result == EXCEPTION){
			output << "<testcase classname=\"" << tot_result.testname << "\" "
				<< "name=\"" << result.name << "\" "
				<< "time=\"" << result.time << "\" >" << endl;
				output << "<error type=\"" << result.type << "\" "
				<< "message=\"" << result.message << "\" >" << endl;

			output << "</error>" << endl;
			output << "</testcase>" << endl;
			}
	}

	output << "<system-out>" << "<![CDATA[" << OutputStdMessage() << "]]>" << "</system-out>" << endl;
	output << "<system-err>" << "<![CDATA[" << ErrorMessage() << "]]>" << "</system-err>" << endl;
	output << "</testsuite>" << endl;
}

SingleResult Test::DoTest(){
	///テストコードを書く
	///例えば結果を構造体に書き込んでいく
}

TotalResult Test::CalcTotalTest(vector<SingleResult> results){
	///全体の集計結果を出しておく
}

int main(int argc, char* argv[]){
	Test* test = new Test;

	string outputpath(argc<=1? ".": argv[1]);

	SingleResult result;
	vector<SingleResult> results;
	TotalResult tot_result;

	result = test->DoTest();
	results.push_back(result); ///きっとテストは二つ以上あるだろうから

	tot_result = test->CalcTotalTest(results)
	test->MakeXMLResult(outputpath,results,tot_result);

	delete test;

	return 0;
}
```

テストコードも書いてやって、実際に出力されたXMLはこんな感じです。各々の数字に深い意味はありません。

```xml
<?xml version="1.0" ?>
<testsuite name="Sample" tests="10" errors="2" failures="7" time="10">
<testcase classname="Sample" name="101" time="1" >
<error type="1" message="Error" >
</error>
</testcase>
<testcase classname="Sample" name="201" time="1" >
<failure type="2" message="Failure" >
</failure>
</testcase>
<testcase classname="Sample" name="301" time="1" >
<error type="3" message="Error" >
</error>
</testcase>
<testcase classname="Sample" name="401" time="1" >
<failure type="4" message="failure" >
</failure>
</testcase>
<testcase classname="Sample" name="501" time="1" >
<failure type="5" message="failure" >
</failure>
</testcase>
<testcase classname="Sample" name="601" time="1" >
<failure type="6" message="Execute" >
</failure>
</testcase>
<testcase classname="Sample" name="701" time="1" >
<failure type="7" message="Execute" >
</failure>
</testcase>
<testcase classname="Sample" name="801" time="1" >
<failure type="8" message="Execute" >
</failure>
</testcase>
<testcase classname="Sample" name="802" time="1" />
<testcase classname="Sample" name="910" time="1" >
<failure type="9" message="Failure" >
</failure>
</testcase>
<system-out><![CDATA[std message]]></system-out>
<system-err><![CDATA[Some Failure or Error Tests Caused]]></system-err>
</testsuite>
```

これをJenkinsのビルド後の処理で「JUnitテスト結果の集計/テスト結果XML」で指定したディレクトリに出力されるようにしておけばJenkinsで見れるようになります。
注意点としては

- xmlまでのパスはワークスペースからの相対パスで書く(私はワークスペースの直下に出力するかコピーされるようにしています)
- ジョブ終了直前のものだけが認識される(ファイルのタイムスタンプを見ているのか、生成時刻が古いと「このファイル今のジョブのじゃないんじゃないの???」的なメッセージが出て認識されません)