C#のテキストテンプレートを使ってみた(3)
スタブの自動生成ツールの改善に挑戦(単体テストの効率化を考える ex1)

2022年10月10日

どもです。

今回も、前回に引き続き、テキストテンプレートについて書きます。

1.今回の適用対象

今回のエントリでは、ランタイムテキストテンプレートをC言語のスタブの作成に適用してみます。
C言語のスタブについては、以下のエントリで過去に色々書いていますので、参照してください。
単体テストの効率化を考える(1) はじめに
単体テストの効率化を考える(2) スタブの戻り値
単体テストの効率化を考える(3) スタブの引数
単体テストの効率化を考える(4) スタブの引数(ダブルポインタ)
単体テストの効率化を考える(5) スタブの自動生成への入力
単体テストの効率化を考える(6) スタブの自動生成ツール
これらのエントリのうち、6回目のエントリにおいて、スタブを生成するツールと、それにより生成されるスタブについて書いています。
しかしこのツールには、ある課題があります。
この課題を、ランライムテキストテンプレートを用いて解決に挑戦します。

2.C言語のスタブの課題

ツールの課題は

出力されるコードの変更が難しい

ことです。
ツール内のスタブのコードを生成する処理は、

  1. インデント/改行/空行の挿入も含めて、全て文字列で生成する。
  2. ファイルに出力する

という順序で行っています。
この「文字列で生成する」処理の設計/実装がよろしくないため、スタブのコードの変更が難しくなってしまっています。

3.さて解決してみよう

最初のエントリで書いたように、ランタイムテキストテンプレートでは、C#の構文が使用可能です。
それを踏まえて、スタブのコードを生成するためのランタイムテキストテンプレートを作成すると、以下のようになりました。

<#@ template language="C#" inherits="StubCodeTemplateBase" #>
<#@ import namespace="CStubMKGui.Model" #>
<#@ import namespace="System.Linq" #>
<#
	PushIndent("    ");
	var indent = CurrentIndent;
	ClearIndent();
#>
#include <stdio.h>

//スタブのバッファの宣言
<# foreach (var func in Functions) { #>
<#
	foreach (var argument in func.Parameters) {
		WriteLine($"{argument.DataType}{indent}{func.Name}_{argument.Name}[100];");
	}
#>
<# } #>

//スタブの初期化関数
<# foreach (var func in Functions) { #>
<#
	WriteLine($"void {func.Name}_Stub_Init()");
	WriteLine("{");

	//呼び出し回数の初期化
	WriteLine($"{indent}{func.Name}_called_count = 0;");
	WriteLine($"{indent}for (int index = 0; index < 100; index++) {{");
	foreach (var argument in func.Parameters) {
		if (IsVariable(argument)) {
			WriteLine($"{indent}{indent}{func.Name}_{argument.Name}[index] = 0;");
		}
	}
	WriteLine($"{indent}}}");
	WriteLine($"{indent}{func.Name}_return[index] = 0;");
	WriteLine("}");
#>
<# } #>
//スタブ本体
<# foreach (var func in Functions) { #>
<#= func.ToString() #>
(
<# 
	//スタブの引数一覧
	int paramCount = func.Parameters.Count();
	for (int index = 0; index < paramCount; index++) {
		var paramItem = func.Parameters.ElementAt(index);
		Write($"{indent}{paramItem.ToString()}");
		if (index != (paramCount - 1)) {
			WriteLine(",");
		}
		else
		{
			WriteLine("");
		}
	}
#>
)
{
<#
	//スタブの戻り値の確認
	if (IsVariable(func)) {
		WriteLine($"{indent}{func.DataType} returnLatch = {func.Name}[{func.Name}_called_count];");
	}
#>
<#
	//引数の格納
	foreach (var argument in func.Parameters) {
		if (IsVariable(argument)) {
			WriteLine($"{indent}{func.Name}_{argument.Name}[{func.Name}_called_count] = {argument.Name};");
		}
	}
#>
<#
	//値の出力
	var outputArgs = func.Parameters.Where(argument => argument.Mode == Param.AccessMode.Out);
	foreach (var outputArg in outputArgs) {
		WriteLine($"{indent}*{outputArg.Name} = {func.Name}_{outputArg.Name}_value[{func.Name}_called_count];");
	}
#>
<#
	//呼び出し回数の更新
	WriteLine($"{indent}{func.Name}_called_count++;");
#>
<#
	//スタブの戻り値を返す。
	if (IsVariable(func)) {
		WriteLine($"{indent}return returnLatch;");
	}
#>
}
<# } #>

またキッタネェコードだなぁ…。

3.1.内容の解説

上述のテンプレートの内容を、少し説明します。
今回作成したテンプレートでは、その殆どを「ステートメント」で実現しています。
「ステートメント」は、「<# #>」で囲んだC#の構文です。
例えばスタブのバッファを生成する処理は、以下のようにできます。

<# foreach (var func in Functions) { #>
<#
	foreach (var argument in func.Parameters) {
		WriteLine($"{argument.DataType}{indent}{func.Name}_{argument.Name}[100];");
	}
#>
<# } #>

ココではC#の構文、特にforeach文と文字列補完を使用しています。
次に関数のエントリ部分です。

<#= func.ToString() #>
(
<# 
	//スタブの引数一覧
	int paramCount = func.Parameters.Count();
	for (int index = 0; index < paramCount; index++) {
		var paramItem = func.Parameters.ElementAt(index);
		Write($"{indent}{paramItem.ToString()}");
		if (index != (paramCount - 1)) {
			WriteLine(",");
		}
		else
		{
			WriteLine("");
		}
	}
)
#>

スタブのエントリ部分では、「<#= #>」による「式」を使用しています。
加えて、この中でメンバメソッドを適用しています。
引数の宣言処理については、for文を用いて生成しています。
この時、for文を回す回数については、Count()メソッドの戻り値を変数に格納しています。
これは、引数を複数持つ場合には、2つ目以降の引数の前に「,」を挿入する必要があります。
その判定のために、foreachを使用せずにfor文を使用するようにしています。
また「,」を挿入する/しないを判定するためにも、この変数を使用しています。
このように、「<# #>」で囲った範囲では、変数、制御文、関数呼び出し、さらにメンバ変数が使用できます!
また、ポインタ引数の実体に値を渡す処理の実装です。

<#
	//値の出力
	var outputArgs = func.Parameters.Where(argument => argument.Mode == Param.AccessMode.Out);
	foreach (var outputArg in outputArgs) {
		WriteLine($"{indent}*{outputArg.Name} = {func.Name}_{outputArg.Name}_value[{func.Name}_called_count];");
	}
#>

ポインタ引数の実体に値を格納する処理は、「出力」あるいは「入出力」に設定されている引数に対してのみ行います。
そのため、引数一覧から対象となる引数を抽出する必要があります。
for文で回して1つ1つ確認する方法もありますが、C#ではLINQを使用するのが定石です。
そして、ランタイムテキストテンプレートでもLINQが使用可能です。
これにより、対象となる引数情報を抜き出して文字列にまとめることができます。

4.まとめ

今回は、ランタイムテキストテンプレートを使用して、スタブの生成処理を課題に挑戦してみました。
結論としては、

  • 劇的な改善はできなかった
  • ランタイムテキストテンプレートを使用した方が、全てをコードで実装するよりは読み易く、また変更はし易くなる。
    (一応の課題の解決はできた!)

となりました。
これまで、ファイルに出力するテキストはコード実装一択で実現してきましたが、これからランタイムテキストテンプレートも方法の候補に含めようかと思います…。

ではっ!