もちっとメモ

もちっとメモ

もぐりのエンジニアが日々の中で試してみたことを気が向いたときに書き連ねていきます

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

経緯

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

JUnitの出力するXMLの構造を見てみる

今回は、こちらを参考にさせていただきました。 JUnitの実行結果のXMLフォーマット

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

  • 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()が最終的に出力を行っているところです。

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

PythonからJenkinsのジョブを実行したい

経緯

最近ぼちぼち出張も増えてきて、出先で作業することも増えてきました。 出先ではノートPCを使って作業するのですが、手配したばかりで中は環境構築がなされていないすっからかんのPCです。 今後のためにがっつり環境を作ってもいいのですが、せっかく新品なのであまり環境を汚したくないですし、頑張ったところでまあスペックはたかが知れているようなものなので、なんとか普段使っているPCのリソースを使って重たい作業を回したいなあと考えていました。 そんな折、何をとち狂ったのかのか、「Jenkinsのジョブで実行ファイルを叩けば行けるんじゃない?ついでにFlaskでフロントを作ってWebアプリとして動かせるようにしてしまえ。」と思い立ってしまったのが事の始まりでした。 ということでなんか本来の使い方から外れているような気もしますが、タイトルの通りpython(Flask)からJenkinsのジョブを実行する環境を作ったので備忘録的に残しておきます。

Jenkins環境

サーバー環境はすでに用意してあるものとします。 まず、ユーザー名とAPIトークンをメモっておきます。 Jenkinsにログインしてユーザー名をクリックすると設定の中から確認できます。 次にリモートから実行を受け付けられるようにjobを用意します。 jobの種類は恐らくはなんでもいいのですが、今回は「フリースタイル・プロジェクトのビルド」で作ります。基本的には普通にジョブを作る時と一緒なので、リモートから実行するときに便利そうな設定だけ書いておきます。

ビルドのパラメーター化

ビルドのスクリプトを書くときにパラメーターを使いたい場合に設定します。POSTリクエストを投げた際にここで設定した名前をキーとしてパラメータを取得できます。

ビルド・トリガ

「リモートからビルド」にチェックを入れて認証トークンを設定します。 恐らくなんでもいいので任意のトークン名を入力します。 ここでJenkinsの画面に、「リモートからビルドするには次のURLを使用します~」のようなメッセージが出ていると思うので、ここにリクエストを投げればリモートからジョブを実行できます。 ビルドのパラメーター化でパラメーターを設定した場合は、「buildWithParameters?token」の方を使用します。「TOKEN_NAME」の部分には認証トークンが入ります。 ビルド Windowsバッチコマンドであれば%で囲ってあげるとビルドのパラメータを取得できます。

SET PARAM = %param% 
APP.exe %PARAM% 

シェルスクリプトの場合は$でしたっけ?ここはJenkinsではなくバッチやシェルスクリプトの書き方に沿って書いてください。 これでJenkins側の設定は終わりです。

pythonスクリプト

次にpython側のスクリプトを作ります。今回はFlask経由で実行するので若干冗長です。実行だけでよければjobの関数の部分さえあれば十分です。 render_templateredirectはWebアプリとして使いたいがためのお飾りです。 使う環境は3系を使います。

from flask import Flask ,request,redirect,render_template 
import urllib3 


app = Flask(__name__) 


@app.route('/')
def index():
 return render_template('index.html') 

@app.route('/job') 
def job():
 param = request.args.get('param')
 url = 'JENKINS_URL/job/jobname/buildWithParameters?token=TOKEN_NAME¶m='+param #¶mの部分はJenkinsのビルドのパラメーター化で設定したキーにしてください
 username = "username" #Jenskinsのユーザー名
 password = "password" #JenkinsのAPIトークン
 http = urllib3.PoolManager()
 headers = urllib3.util.make_headers(basic_auth=('%s:%s' % (username,password)))
 response = http.request('POST', url, headers=headers)
 return redirect('http://localhost:8080/') 


if __name__ == '__main__':
 app.debug = True
 app.run(host='0.0.0.0',port=8080) 

適当にhtmlも書きます。

See the Pen KxVvgQ by Sashimimochi (@sashimimochi) on CodePen.

これでlocalhost:8080にアクセスして実行ボタンを押せばJenkinsが稼働しているサーバーのリソースを使って実行ファイルを動かせるようになりました。これで出先でもある程度作業ができるようになったかな。 セキュリティー等は考慮していないガバガバ構築なのでそのあたりは悪しからず。

認証proxy環境下でpythonからはてなブログに下書き投稿する

最近、プログラムはちょこちょこいじっているのですが、はてブのサイトを開いて記事を書くのをさぼっていたので投稿間隔が空いてしまいました。 ブラウザ開かなくても作業しながら手元のコードを手軽に投稿できれば少しはさぼりが減るかもと思い調べてました。 ということで、さっそく本題へ。 今回使用したコードはこちらを利用させていただきました。 

github.com

tadaken3.hatenablog.jp

とある事情があってproxy配下から送りたかったのですが、このままでは送れないので微修正をば。 上記のコードにproxyの設定を足すだけなので変更含めて加えるのは5行です。 79行目の`requests`の部分を



    proxies = { 

    'http':'http://ユーザー名:パスワード@プロキシサーバーのアドレス:ポート番号/',

    'https':'http://ユーザー名:パスワード@プロキシサーバーのアドレス:ポート番号/' 

    } 

    r = requests.post(url, data=data, headers=headers,proxies=proxies)

に変更するだけです。 これで無事送れるようになりました。今後はもう少し投稿頻度を上げられるといいな。

ちょっと気を付けないと行けないのが、送るmarkdownに&とか<とか>とかのHTML特殊文字が混じっていると400エラーを出して送信に失敗するので&amp;などに直してから上げる必要がありそうですね。

Unityで外部テキストを読んで表示するノベルが作りたい

手っ取り早くUnityでノベルゲームを作りたい時は宴とかを使えばいいんでしょうが、お試しで軽くノベルゲーム的なのを作って見たいだけなので今回は見送り。

madnesslabo.net

じゃあデフォルトのAssetだけで作れんもんかと調べたらありました。やはり先人は偉大です。下記の記事にしたがって組んで見たら、おお!!すごい。テキストファイルに書いた内容がノベルゲームっぽく表示されてる!!しかも、ちゃんと@brでテキストが区切られていて//でコメントアウトされている。感動です。早速お借りさせていただきます。

tsubakit1.hateblo.jp

基本はこちらに書かれている通りに進めればできますが、個人的につまづいた事を備忘録的に記しておきます。

  1. Q.作ったスクリプトは何にアタッチすればいいの?
    A.スクリプトを動かしたいシーン上に適当に空のオブジェクトを作成して`ScenarioManager.cs`をアタッチします。すると自動的に`TextController.cs`も付随してアタッチされるはずです。あとはInspectorビューで`ScenarioManager.cs`のLoadFileNameにシナリオを書いたテキストのファイル名(拡張子は不要)を入力します。`TextController.cs`にはUiTextをアタッチします。これはその1と同じです。

    tsubakit1.hateblo.jp

  2. Q.`ScenarioManager.cs`のUpdate関数の`m_commandController.PreloadCommand (m_scenarios [m_currentLine])`あたりで`NullReferenceException`が出て動かないんだけど。(その2のコメントにもあったやつです)
    A.commandControllerのインスタンスが見つからない的な事を言っていると思われます。なので`commandController.cs`をシーン上のオブジェクトにアタッチして見たら動きました。アタッチするオブジェクトは何でもいいんだと思いますが私は先ほどの`ScenarioManager.cs`をアタッチしたのと同じオブジェクトにアタッチしました。

せっかくなのでもう一工夫いじらせていただきます。我流魔改造なので出来具合はご愛嬌という事で。

メッセージが表示されるとポポポポというSEを鳴らす

kan-kikuchi.hatenablog.com

オーディオ制御のコアの部分はこちらで紹介されているAudioManagerを利用させていただいてます。こちらのAudioManagerを使って`TextController.cs`のテキストが送られる部分をこんな感じに改造します。


    public void SetNextLine(string text){
		AudioManager.Instance.StopChargeSE ();//すでにポポポポがなっていたらの停止
		AudioManager.Instance.PlayChargeSE ("serifSE");//新しくポポポポを鳴らす
		currentText = text;
		timeUntilDisplay = currentText.Length * intervalForCharactorDisplay;
		timeElapsed = Time.time;
		lastUpdateCharacter = -1;
	}

上で紹介されているAudioManagerではSEの途中停止はなかったのでStopBGMを参考にStopSEを自作しています。ほぼBGMをまるっとコピーして作っています。つまりStop可能なSEは一つだけですね。以下`AudioManager.cs`に足したStop可能SE部分を抜粋(名前がChargeSEになっているのは、はじめに作った時にチャージショット様に作ったためです。stopableSEとかにしておけばよかったですね)


    //ChargeSEのフェードアウト時間	
	public const float CHARGE_SE_FADE_SPEED_RATE_MAX = 0.9f;
	public const float CHARGE_SE_FADE_SPEED_RATE_MIN = 0.3f;
	//BGM用とSE用それぞれのオーディオソース
	private AudioSource _chargeSESource;
	//全オーディオを保持
	private SortedDictionary<string, AudioClip> _bgmDic, _seDic, _chargeSEDic;
	//作成したAudioSourceを取得し、変数にセットする。ボリュームもセットする
    for (int i = 0; i < audioSourceArray.Length; i++) {
		audioSourceArray [i].playOnAwake = false;

		if (i == 0) {
			audioSourceArray [i].loop = true;
			_bgmSource = audioSourceArray [i];
			_bgmSource.volume = PlayerPrefs.GetFloat (BGM_VOLUME_KEY, BGM_VOLUME_DEFAULT);
		} else if (i == 1) {
			_chargeSESource = audioSourceArray [i];
			_chargeSESource.volume = PlayerPrefs.GetFloat (SE_VOLUME_KEY, SE_VOLUME_DEFAULT);
		} else {
			_seSourceList.Add (audioSourceArray [i]);
			audioSourceArray [i].volume = PlayerPrefs.GetFloat (SE_VOLUME_KEY, SE_VOLUME_DEFAULT);
		}
	}

	//リソースフォルダから全てのBGM,SEを読み込み、セットする
	_chargeSEDic = new SortedDictionary<string, AudioClip> ();
	object[] chargeSEList = Resources.LoadAll (SE_PATH);

	/*
	* ChargeSE
	* 指定したファイル名のChargeSEを流す
	* ただし、すでに流れている場合は前の曲をフェードアウトさせてから流す
	* 第二引数のfadeSpeedRateにフェードアウトスピードの割合を入れる
	*/ 
	public void PlayChargeSE(string chargeSEName,float fadeSpeedRate = CHARGE_SE_FADE_SPEED_RATE_MAX){
		if (!_chargeSEDic.ContainsKey (chargeSEName)) {
			Debug.Log (chargeSEName + " can not be found");
			return;
		}
		//現在、再生されていない場合はそのまま再生
		if (!_chargeSESource.isPlaying) {
			_chargeSESource.clip = _chargeSEDic [chargeSEName] as AudioClip;
			_chargeSESource.Play ();
		}
		//違うChargeSEが流れているときは、流れているChargeSEをフェードアウトさせてから次を再生
		//同じChargeSEが再生中はpass
		else if (_chargeSESource.clip.name != chargeSEName) {
			FadeOutChargeSE (fadeSpeedRate);
		}
	}

	/*
	 * ChargeSEの停止
	 */ 
	public void StopChargeSE(){
		_chargeSESource.Stop ();
	}

	/*
	 * 現在再生中のSEをフェードアウト
	 * fadeSpeedrateでスピードを調整
	 */ 
	public void FadeOutChargeSE(float fadeSpeedRate = CHARGE_SE_FADE_SPEED_RATE_MIN){
		_isFadeOut = true;
	}

	/*
	 * 音量調整
	 * BGMとSEのボリュームを別々に変更and保存
	 */ 
	public void ChangeVolume(float BGMVolume,float SEVolume){
		_chargeSESource.volume = SEVolume;
	}

	public void ChangeSEVolume(float SEVolume){
		_chargeSESource.volume = SEVolume;
	}

これでメッセージと一緒にSEが流れる様になったのですが、テキストが表示され切った時点でSEを止めたいのでもう一手間加えます。テキストが表示され切ったかは`ScenarioManager.cs`の`if(m_textController.IsCompletaDisPlayText)`の部分で判定しているのでここに先ほどのStopSEを挟んでやります。


    void Update(){
	if (m_textController.IsCompleteDisplayText) {
		AudioManager.Instance.StopChargeSE ();
		if (m_currentLine < m_scenarios.Length) {
			if (!m_isCallPreload) {
    (以下略)

これでテキストが表示され切った時点でSEが止まる様になりました。

おまけでBGMも鳴らしちゃいましょう。BGMはシーンのスタート時に鳴らします。bgmはpublicで定義しておいて指定すればいいのですがたくさんシーンを作ると毎回打つのが面倒なので中に埋め込んじゃいます。その代わり辞書型で定義しておいてシーン名に応じて切り替えられる様にしました。


    private Dictionary<string,string> bgmList = new Dictionary<string, string> () {
		{ "Story1", "BGM1" },
		{ "Story2", "BGM2" },
		{ "Story3", "BGM3" }
	};

    void PlayBGM(){
		AudioManager.Instance.StopBGM ();
		AudioManager.Instance.PlayBGM (bgmList [SceneManager.GetActiveScene ().name]);
	}

    void Start(){
		PlayBGM ();
		m_textController = GetComponent ();
		m_commandController = GetComponent ();

		UpdateLines (LoadFileName);
		RequestNextLine ();
	}

シーンの切り替えエフェクトをつける

以前紹介したこちらを使ってやります。この記事の元ネタもテラシュールブログさんでしたね。いつもお世話になっています。

t-n-clark.hatenadiary.jp

tsubakit1.hateblo.jp

以前と同じ様に`ScenarioManager.cs`に`Fadeout`用の関数を作ります。 遷移時のSEはInspectorViewから指定できる様にpublicで定義しておきます。


	[SerializeField] Fade fade = null;
    public string changeSE;

    public void Fadeout(){
		AudioManager.Instance.PlaySE (changeSE);
		fade.FadeIn (1, () => {
			Invoke ("ChangeNextScene", 0.5f);
		});
	}
	
	public string nextScene;//次のシーンの名前

	void ChangeNextScene(){
		SceneManager.LoadScene (nextScene);
	}

そして、テキストファイルの中身が全て読み終わった時点で次のシーンに飛ばします。


    void Update(){
		if (m_textController.IsCompleteDisplayText) {
			AudioManager.Instance.StopChargeSE ();
			if (m_currentLine < m_scenarios.Length) {
				if (!m_isCallPreload) {
					m_commandController.PreloadCommand (m_scenarios [m_currentLine]);
					m_isCallPreload = true;
				}
				if (Input.GetKeyDown (KeyCode.N)) {
					RequestNextLine ();
				}
			} else {//テキストの中身が全て読みこまれていて、かつNのキーが押された時
				if (Input.GetKeyDown(KeyCode.N)) {
					Fadeout ();
				}
			}
		} else {
			if(Input.GetKeyDown(KeyCode.N)){
				m_textController.ForceCompleteDisplayText ();
			}
		}
	}

これでテキストが全て読み終わった後にフェードアウトして次のシーンに遷移できる様になりました。 これでだいぶノベルゲームっぽくなりました。RPGのシナリオシーンにも使えるんじゃないかな。

C++でYAMLが読みたいんや...読めました

最近、C++のコードをちまちまいじっていたところ、設定ファイルとかの外部から渡すファイルって階層構造にしたほうが作りやすいし、読みやすいんだよな、これをそのままC++で読ませたいな...と思い立って調べました。

結論から言うと無事読めるようになったのですが、結構苦労したので備忘録を残しておきます。

今回はこちらのyaml-cppを使わせていただきました。提供方法はバイナリファイルではなくソースコードなので自分の環境でビルドしてバイナリ化してから使う必要があります。本当は最新版を使いたかったのですが、ビルドがうまくいかなかったのでver0.3.0を使いました。ver0.3とver0.6では使い方が結構違うようなのでご注意を。

github.com

基本的なインストール方法は公式の通りです。日本語の解説記事はこちらが参考になります。

www.mk-mode.com

yomi322.hateblo.jp

C++ から YAMLファイルを読み込む « Stop Making Sense

 

ついでに私が行った手順を大雑把に手順を書いておきます。

  1. GitHubリポジトリからファイルをダウンロードする。今回はver-0.3.0を使用する(ver-0.6.0はうまくビルドできなかった)
  2. CMakeをダウンロードし、インストールする。今回はver-3.0.0-rc2を使用する。

  3. ダウンロードしてきたcpp-yamlの圧縮ファイルを解凍する
  4. 解凍したフォルダ内にbuildという名前の空フォルダを作成する(この中にcamkeしたバイナリを保存します)
  5. cmakeでsourceにCMakeList.txtが入ったフォルダを指定してcmakeする。今回はGUIでcmakeした。このとき、どのコンパイラでcmakeするかによって32bitか64bitかが決まるので注意
    1. GUIを使った場合
    2. srcにcpp-yamlのルートフォルダを出力先にbuildフォルダを指定してConfigureボタンをクリックする
    3. コンパイラとして何を使うか聞かれるので選択する。今回はVisualStudio2012がインストールされている環境で64bit版で使用したいのでVisual Studio 11 2012 Win64を選択
    4. 無事完了し、Configuring doneが表示されればOKなので、次にGenerateボタンをクリックする
    5. 無事Generating doneが表示されればOK
  6. buildフォルダ内にソリューションファイル.slnが生成されてるはずなのでVSでこれを開く
  7. プラットフォームがx64になっていることを確認し、ビルドモードをRelWithDebInfoに設定してビルドする。このときビルド対象にInstallを含めると適当な位置にライブラリを配備してくれるらしい。試していないので不明。
  8. 無事ビルドが成功し、Installを指定しなかった場合は、RelWithDebInfoフォルダ内の.libファイルを取り出してC++でスタティックライブラリを読めるように設定する。わからなければ以下を参考にする

    corgi-lab.com

    qiita.com

  9. include内のyaml-cppフォルダをごそっと組み込みたいソフトで読める位置に設定する。より具体的にはVSのヘッダーファイルの設定をする
  10. あとは公式にそって書いていけばよい。

これで下ごしらえは終わりです。早速実際に使ってみようと思うのですが、使い方にも癖があってymlファイルは特定の形式で書かれていないとうまく読んでくれないみたいです。まず、このような形式のymlファイルを用意します。内容は適当で意味はありませんです。 ファイル名も適当にinfo.ymlとでもしておきます。


- enemy:
    - name: enemy0
      hp: 100
      atk: 10
    - name: enemy1
      hp: 50
      atk: 20 
    - name: enemy2
      hp: 200
      atk: 30
    world: japan
    player: sashimimochi

次にymlの形式に合わせてC++を実装します。 基本的には参考にさせていただいたページの通りですが、複数の要素を持つ場合にループで回す部分を改良しています。 インクルードはするとして、


#include "yaml-cpp\yaml.h"

まず、受け取る情報に合わせて構造体を実装します。ymlのデータが階層構造になっていれば階層分作成します。


struct Enemy{
  string name;
  int hp;
  int atk;
} 
struct Info{ 
  vector<enemy> enemy;
  string world;
  string player; 
}

これを処理するオペレーターを用意します。オペレーターも作成した構造体分作成します。


void operator (const YAML::Node& node, Enemy& enemy){ 
  node["name"] >> enemy.name;
  node["hp"] >> enemy.hp; 
  node["atk"] >> enemy.atk; 
} 
void operator >> (const YAML::Node& node, Info& info){ 
  const YAML::Node& enemies = node["enemy"];
  for(int i=0;i<enemies.size();i++){ 
    Enemy enemy; enemies[i] >> enemy;
    info.enemy.push_back(enemy); 
  }
  node["world"] >> info.world;
  node["player"] >> info.player;
} 

そしてこれを読み込む関数を実装します。


void loadYMLFile(string ymlpath){ 
  string name;
  int hp; 
  int atk; 
  string world; 
  string player; 
  try{ 
    ifstream fin(ymlpath);
    YAML::Parser parser(fin);
    YAML::Node doc; parser.GetNextDocument(doc);
    Info info;
    for(int i=0;i<doc.size();i++){ 
      Info info; doc[i] >> info;
      for(int j=0;j<info.enemy.size();j++){
        name = info.enemy[j].name;
        hp = info.enemy[j].hp;
        atk = info.enemy[j].atk;
        cout << "name:" << name << "\n" << "HP:" << hp << "\n" << "ATK:" << atk << endl;
      } 
      world = info.world; 
      player = info.player;
      cout << "world:" << world << "\n" << "player:" << player << endl;
    }
  }catch(YAML::ParserException& e){
    cerr << e.what() << endl;
  } 
} 

無事ymlファイルに入力した情報がコンソールに出力されれば成功です。 まとめて書くとこうなる。 cpp-yaml-reader.cpp


#include "yaml-cpp\yaml.h"
#include <string>
#include <fstream>


struct Enemy{ 
    string name;
    int hp;
    int atk;
}
struct Info{ 
    vector<enemy> enemy;
    string world;
    string player;
}


void operator (const YAML::Node& node, Enemy& enemy){
    node["name"] >> enemy.name;
    node["hp"] >> enemy.hp;
    node["atk"] >> enemy.atk;
}
void operator >> (const YAML::Node& node, Info& info){
    const YAML::Node& enemies = node["enemy"];
    for(int i=0;i<enemies.size();i++){
        Enemy enemy; enemies[i] >> enemy;
        info.enemy.push_back(enemy);
    } 
    node["world"] >> info.world;
    node["player"] >> info.player;
} 


void loadYMLFile(string ymlpath){ 
    string name;
    int hp; 
    int atk;
    string world;
    string player;
    try{ 
        ifstream fin(ymlpath);
        YAML::Parser parser(fin);
        YAML::Node doc; parser.GetNextDocument(doc);
        Info info;
        for(int i=0;i<doc.size();i++){
            Info info; doc[i] >> info;
            for(int j=0;j<info.enemy.size();j++){
                name = info.enemy[j].name;
                hp = info.enemy[j].hp;
                atk = info.enemy[j].atk;
                cout << "name:" << name << "\n" << "HP:" << hp << "\n" << "ATK:" << atk << endl;
            } 
            world = info.world;
            player = info.player;
            cout << "world:" << world << "\n" << "player:" << player << endl;
        }
    }catch(YAML::ParserException& e){ 
        cerr << e.what() << endl;
    }
} 

int main(int argc, char* argv[]){
    string ymlpath = argv[1];
    loadYMLFile(ymlpath);
    return 0;
} 

これをコンパイルして引数渡しで先ほどのymlファイルのパスを渡してあげると表示してくれる形にしてみました。

cpp-yaml-reader.exe info.yml

これでめでたくC++でymlファイルが扱えるようになりました。設定情報をcsvファイルとかを使って頑張って階層構造を組まなくていいようになりました。マジックナンバーではなくキーで指定できるのも地味にうれしいです。