2017年9月24日日曜日

コードカバレッジツール kcovを使ってみた

Softiesプロジェクトは 2006年から gcovツールを使っているが、シェルスクリプトのカバレッジを計測することができる kcovというのがあるのを最近になって知ったので、早速使ってみた。


結論から言えば、『なんとか使える』レベル。「ないよりはマシ」。


bashスクリプトのカバレッジを計測するのた目的で、今回使用した環境は 64bit版 Ubuntu 17.04。

aptで簡単にインストールできたが、実は 2日前に別の Ubuntu 17.04環境にインストールしたときはインストールは成功したものの、実行すると C++のライブラリで異常な配列インデクスが原因でコアダンプしてしまう現象が発生した。ソースからダウンロードして cmakeからやってみたが解決しなかった。そのときの詳しい経過やエラーメッセージは持ち出しできないので再現できず原因は不明だが、依存するライブラリとの相性問題があるかも知れない。(訂正:その動かなかった環境のものも動いた。エラーが発生したのは kcovと一緒にインストールしたテストフレームワークのものだった。)

機能の簡単な紹介

  • ELF, Mach-Oの両形式でコンパイルされたバイナリ、Python、bash、shプログラムのカバレッジを計測するテスタで、Linux/OSXで動作
  • オプションなしにコンパイルされたバイナリから直接カバレッジを取得
  • 特別な工程なしに HTML, Cobertura XMLを出力
詳しい情報は https://simonkagstrom.github.io/kcov/ を参照

実行状況

とりあえず今日動かした状況は下記のとおり。
$ kcov d ~/bin/bu
  • dは出力先のディレクトリ、その次に計測対象のプログラムを指定する。
  • 出力先ディレクトリは予め作っておく必要はない。
  • 逆に出力先ディレクトリに以前の計測結果があるときはマージされるので、まっさらな状況から計測したいときは空にしておかなければならない。
  • プログラムを指定するときは、絶対パスまたは相対パスを指定する必要があり、環境変数に指定したパスにあるものではプログラムは usageを表示してしまい計測できない。
  • コンパイル時にオプションが不要とのことだが、ビルドID ファイルがない場合は -gまたは -ggdbオプションでビルドする必要がある。(ビルドIDファイルって何だ?)
  • プログラムの後にプログラムに与えるオプションやオプション引数を指定できる。

レポート

前述のコマンドを実行するとつぎのような HTMLのレポートが得られた。
gcovでもよく使われれ、見慣れた lcov風の書式。いい感じ。

しかし、コードを表示させると妙な行間があって見にくい。

charsetの指定がないため、文字化けも発生。 手動で UTF-8を選択すれば正しく表示されるが、ページを移動するたびにやらなければならない。

doneや fiのカウントが可笑しい。(他のカバレッジツールでも起こる問題だが)



いつくか難点はあるものの、ここまで作ってくれた作者に感謝するとともに、今後の改善を祈りながら使っていきたいと思う。
冒頭で「ないよりマシ」とか酷い書き方をしたが、実際には「使えねぇ」ものも数ある中では、使い続けようかという気にさせたツールの1つである。

2017年5月19日金曜日

Clangでユニバーサルキャラクタ識別子を使う

Softiesプロジェクトとは直接は関係ない話題。

1年前から Softiesプロジェクトがこれまで使用してきた gccを clangに置き換える方向で準備を進めている。
と言っても gccを廃すかどうかまでは決まって折らず、平行運用することになるかも知れない。

そんな中、gccでは使えないユニバーサルキャラクタ識別子が clangでも使えることが判った。
ちょっと、悪乗りした感じのプログラムを紹介する。
つぎのプログラムが Cで書いた物。実際には C99の仕様である。

main.c
#include "C文法定義"
#include "ライブラリ定義"

型定義 構造体 自動車* 自動車;

構造体 自動車 {
 文字列 車種;
 整数 価格;
};

整数 メイン関数(整数 argc, 文字列* argv) {
 自動車 俺の車 = (自動車)メモリ割り当て(サイズ(構造体 自動車));
 もし (俺の車 != ぬるぽ) のとき {
  俺の車->車種 = "プリウス";
  俺の車->価格 = 1234567;
 }
 表示("[INFO] %s, %d円\n", 俺の車->車種, 俺の車->価格);
 戻る 0;
}

ヘッダファイルはつぎの2つ


C文法定義
#ifndef _Included_C文法定義
#define _Included_C文法定義

#define 文字列  char*
#define でないとき else
#define もし  if
#define 整数  int
#define 戻る  return
#define サイズ  sizeof 
#define 構造体  struct
#define 型定義  typedef
#define メイン関数 main
#define のとき

#endif /* _Included_C文法定義 */

ライブラリ定義
#ifndef _Included_ライブラリ定義
#define _Included_ライブラリ定義

#include 
#include 

#define ぬるぽ  NULL
#define 表示  printf
#define メモリ割り当て malloc

#endif /* _Included_ライブラリ定義 */

ついでに必要最小限の Makefile
CC = clang

つぎのようにビルドして
make Main

実行する。
$ ./Main
[INFO] プリウス, 1234567円

Javaは初期の言語仕様から識別子に日本語が使えた。
しかし、#define機能がないのでキーワードまでは置き換えられない。

2016年10月21日金曜日

変数の値が非数 NaNかどうか調べるために、定数 NANと比較してはならない。

次に示すのは CUnitを用いたテストケースである。
/**
 * 定数 NANと比較すると偽を返すこと。
 *
 * NaNかどうか判定するために、定数 NANと比較してはならない。
 */
public void testCompareNanAndNan() {
        /* 事前条件の準備 */
        double value = NAN;

        /* 実行 */
        boolean result = (value == NAN);

        /* 事後条件の検証 */
        Assert_false("結果は偽であること。", result);
}
これを実行すると、テストは成功する。
変数 valueが定数 NANで初期化されているが、その変数を NANと比較しても真にならない。
比較演算子で NANと比較しても NaNかどうか判断できないということで、これは正しい挙動である。
なお、このテストケースは gccを使用してコンパイルしたが、定数 NANを使用するためには math.hをインクルードする必要がある。
ISOの C99よりも前のバージョンでは、処理系によっては NANが使えない。
NaNであるかどうかの正しい判定方法を次のテストケースに示す。
/**
 * NaNであることを判定できること。
 *
 * NaNかどうかの判定には isnan()を使用すること。
 */
public void testIsNan() {
        /* 事前条件の準備 */
        double value = NAN;

        /* 実行 */
        boolean result = isnan(value);

        /* 事後条件の検証 */
        Assert_true("結果は真であること。", result);
}
isnan()も math.hが必要。
==比較演算子を使用して「強引に」判定する例を次に示す。
/**
 * ポインタを使用して NANと比較することで、NaNかどうかを判定できること。
 *
 * この使い方を推奨しているわけではないが、定数を代入しただけのものは
 * ビットパターンの比較により判断ができる。
 * この例ではポインタを使用したが、unionを用いても同様のことができる。
 *
 * NaNの値は1通りではないため、計算結果や外部から読み込んだ値を正しく
 * 判断するには == は使えない。
 */
public void testCompareNanAsPointer() {
        /* 事前条件の準備 */
        double value = NAN;
        double nan = NAN;

        /* 実行 */
        boolean result = (*(long long*)&value == *(long long*)&nan);

        /* 事後条件の検証 */
        Assert_true("結果は真であること。", result);
}
ドキュメントコメントにも書いてあるよう、この方法では特定の条件でした判断できない。
非数 NaNを理解するための例をもう1つ示しておこう。
/**
 * NaNは 指数部の全ビットが 1で、仮数部が 0でないこと。
 *
 * これは例を示しただけで、実際にやるのは無駄。 isnan()を使うべき。
 */
public void testNanAsPattern() {
        /* 事前条件の準備 */
        double value = NAN;
        long long exponentMask = 0x7ff0000000000000;
        long long mantissaMask = 0x000fffffffffffff;

        /* 実行 */
        long long longValue = *(long long*)&value;
        boolean result = ((longValue & exponentMask) == exponentMask)
                && ((longValue & mantissaMask) != 0);

        /* 事後条件の検証 */
        Assert_true("結果は真であること。", result);
}
valueを定数 NANで初期化したものだけでなく、0.0 / 0.0した結果も正しく判断できる。
printfを使って調べると、gccの場合、NANのパターンは 0x7ff8000000000000、0.0 / 0.0の結果は 0xfff8000000000000で 1ビット異なるが、いづれも NaNと判定できた。
注:value = 0.0 / 0.0; というコードはコンパイル時点でエラーになる。 分母を変数にしてその変数を 0.0で初期化するようにすれば、コンパイルが通るようになる。

普段の判定は isnan()を使用すればよいが、isnan()が使えない場合や NaNを使ったテストケースを記述するとき、この知識は役にたつだろう。
なお、上記の例で使った CUnitテストフレームワークについてはこのブログで紹介してきたが、はじめてみる人のために簡単に説明しておく。
これらのテスト関数はつぎのようなコードを含むソースコードとして記述され、softiesの CUnitライブラリとリンクされる。
#define import_lwd_unit_Assert
#define import_lwd_unit_CUnit

#include <lwd/lwd.h>
#include <lwd/unit/Assert.h>
#include <lwd/unit/CUnit.h>
#include <math.h>

/**
 * @file
 *
 * NaNの振る舞いのテスト。
 */

/****************************************************************
        test cases
****************************************************************/

// ここに、テスト関数を記述する。
// テスト関数名が異なれば複数個記述可能。

/****************************************************************
        main
****************************************************************/

public int main(int argc, char** argv) {
        return CUnit_test(argc, argv);
}
main関数のなかで CUnit_test()を呼び出すことにより、自動的にテスト関数を認識して実行する。
なおテスト関数の名前は textXXXのように先頭4文字は testでなければならない。

2016年10月16日日曜日

単体テストフレームワーク CUnit(その2)

テスト仕様をテストレポートに表示できるようになったのは、つい最近の 2016年 6月。それまではテストケースのクラス名や、テストメソッドの名称でしか表現できなかった。
レポートにテスト仕様の表示が必要なことは最初のころから感じていたが、うまい実現方法が思いつかなかった。
仕様を記録するために、専用の関数を定義しようかと考えたことがある。
たとえば、事前条件や事後条件のテスト仕様を示すために、テストコード中につぎのような関数呼び出しを記述するのである。

 Specification_postcondition("valueは 100になっていること。");

技術的には何の問題も無いが、テストコードの作りやすさや保守性を考えると冗長すぎないかと思い、まだ実現していない。
テストコードそのものがテスト仕様になるはずなので、文字列で記述せずにテストコードから仕様の記述を生成できればそのほうがよいと考えている。しかしこちらの方は技術的に難しい。
いよいよ方法が見つからないようなら上記のような関数を提供することになる。

そこで当面の間、もう少し気楽に使えるできるよう、テストケースとテストメソッドに対してドキュメントコメントで概要だけを記述できるようにした。
例)

/**
 * 2つの値を加算できること。
 */
public void testAdd() {

この程度の記述でテストレポートの内容がわかり易くなるなら、書くようになるでしょう。


過去に記述したテストケースなど、ドキュメントコメントの記述が無いものを最近のフレームワークで処理すると、テストレポートに警告文が表示されるようになっている。

折角テストがOKになっても警告文はカッコ悪いですよね。これには既存のテストコードにもドキュメントを書いてくださいという思いが込められている。


将来的には、事前条件や事後条件、実行内容もレポートできるようにして、さらには Excelや Wordのテスト仕様書、テスト報告書が自動作成できるようにしたい。

2016年10月14日金曜日

単体テストフレームワーク CUnit


Javaでコードをテストするときには JUnitを使用しているが、C/C++でもそのようなのがあったら便利だと思った。
2006年ごろ、自社製(まだ会社設立前だったが)の Java VM(TCK通してないので、公式には“もどき”)の品質が悪く、ちゃんと細部までテストをしたいなというのが始まりだった。
ネット上で検索すると沢山の CUnitが見つかり、そのうちのいくつかを試してみた。
しかし、どれもあまりパッとしなかった。 使いたくないと感じたのは、
  1. プロジェクトが放置されて久しいもの
  2. 完成度があまりにも低いもの
  3. 手続きが多くて使いにくいもの
  4. ドキュメントが全く無いもの
  5. エラーが発生したときの処理が貧弱なもの
  6. レポートを見て「ワクワク」しないもの

3の使いにくいとは、リポジトリとかテストスイートなどのオブジェクトを作り、テストケースを1個ずつ追加しなければならないのは、無精者の自分には向かないと思った。
テストケースを追加したのに、テストスイートに追加を忘れると、エラーにもならずに黙ってテスト漏れになってしまう。

しばらく我慢して使っていたがあまりにも作業効率が悪いので、自分も CUnitを作ろうということになった。(VM作りに集中せず、横道に逸れてしまうのだが)

今なら Google Testを使うでしょう。 Google Testレポートはワクワクしないけど、Softiesの CUnitの目標と共通している点も多い。 ただ当時は日文のドキュメントが多くなかったので Google Testは不採用とした。(なんと無謀な…。 皆さんは真似してはいけません。時間の無駄です。 Google Testを使いましょう。)

CUnitの目標は
  1. JUnitライクとする。(Cではアノテーションができないので 3.8ベース)
  2. リポジトリやスイートはデフォルトで不要または自動とする。
  3. JUnit同様、テスト関数の結果が他のテスト関数に影響しない。(ファイルや DBを除く)
  4. テスト中にエラーが発生してプロセスが停止しても、次にテスト関数は実行できること。
  5. 赤と緑を使った刺激的でモチベーションを維持できるレポートを出力できること。

では細かな機能紹介は別にするとして、テストケースの記述例とテストレポートのスナップショットをご覧頂きましょう。
(バージョンは違うが、以前、パッケージ一覧でも例示してある。)

テストケースのサンプル

テストプログラムの冒頭


テストケースの仕様をドキュメントコメント @fileタグで記述する。

スタブ(必要なときのみ)


フィクスチャ


テストケース


テスト関数の仕様をドキュメントコメントで記述する。
Assertクラスのコメントもテストレポートに引用される。

main関数


main関数はスタティックリンク(デフォルト)する場合に必要。
CUnitクラスの testメソッドを呼ぶだけのため、覚えるのは簡単。
main関数の中で argc, argvの内容を testメソッドを呼ぶ前に書き換えることも可能。

テストレポートのスナップショット

テストケース一覧


テスト関数一覧


テスト関数の実行結果(1)


テストレポートに事前条件の表示が無いため、事後条件の検証結果に唐突感がある。
事前条件の表示は今後の課題。
printf等で標準出力に出力した結果は、"stdout"に表示される。(この例では空)
標準エラー出力に出力した結果は、"stderr"に表示される。
malloc, free, reallocは追跡され、"heap"に表示される。 この例ではメモリリークが起きているがこれはテストケースでおきたものではなく、softiesが使用している libbfdのバグによるもの。
なお、softiesが未公開の理由の1つはこの libbfdが GPLライセンスであるため。 これを排除して softiesを公開する予定であるが、まだ時間を要する。

テスト関数の実行結果(2)


テストによりテキストファイルのほか、イメージや動画、オーディオファイルが出力されると、自動的にテストレポート中に表示される。