WACATE2013冬にてリスクベースドテストについて講演

先週末、WACATE2013冬というソフトウェアテストの合宿勉強会にて「リスクベースドテストを活用しよう」というセッションを担当させていただきました。

リスクベースドテストを活用しよう/Risk Based Testing In Action - Speaker Deck

内容はリスクベースドテストの概要や一連の進め方の解説となっています。
今回はリスクベースドテストの運用上の問題がよく見られる現状もあり、特に「4. リスクベースドテストの限界・注意点」の解説を行えればという思いで機会を頂きました。
内容を詰め込みすぎてしまい駆け足気味のセッションとなってしまいましたが、適切なリスクベースドテストの活用の一助になればと考えています。

Ultimate Agile Stories Iteration 3に寄稿

 アジャイルの同人誌Ultimate Agile Storiesですが、前号に続いてIteration 3でも「アジャイルでのシステムテストはテスト戦略ありき」という記事を寄稿させていただきました。
http://ultimateagilestories.web.fc2.com/bookcontents3.html
 今回は規模が大きかったり複雑だったりするソフトウェアを相手にした場合の、アジャイルでのシステムテストについて書かせて頂いています。開発ライフサイクルの中で、どのようなテストをどのように配置していくか、というテスト戦略が内容の中心となっています。
 今年の夏コミで頒布される予定ですので、気になった方はぜひ。なお恐らくその他アジャイル系のイベントでも販売されると思います。

CCCCについて

 サイクロマティック数などソースコードのメトリクスは、レビューやリファクタリングのとっかかりとしてしばしば有用になるけれど、それらをシンプルかつ手軽に解析する手段の1つとして、前々からCCCC(C and C++ Code Counter)を使っている。最近身近なところで需要がちらほらあったので、今回はCCCCについて簡単にまとめたいと思う。

CCCCの概要

 CCCCはC、C++等を対象としたオープンソースの静的解析ツールで、特にソースコードの複雑度(主体はサイクロマティック数)と規模(主体はLOC)の解析に特化している。

 CCCCのメリットとしては、設定や処理が手軽というものがある。
 というのも、自分の業界でメトリクス解析の定番となっている、QACやPGRelief、AdLintといった高機能な静的解析ツールだと、高機能な半面、外部リンクの設定・参照構造の設定・各種処理系に合わせたカスタマイズなど色々設定の手間がかかる上に、設定ミスがあると解析エラーが発生してメトリクスが得られない、なんて煩わしさがよくある。
 一方でCCCCはファイル単体ごとにテキスト処理でメトリクスを解析していくので、そうした煩わしい設定がなくとも結果を取得できる手軽さがある。
 ただその代償として、プリプロセッサ処理の解析ができない、外部参照を扱えないといった制約も持っている。そのあたりは解析の目的に応じた使い分けが必要になると思う。

実行

 引数に解析対象のパスを指定して実行すると解析が行われる。
 なお「---outdir」のオプションで解析結果の出力場所を指定できる。それを指定しないと「.cccc」という隠しディレクトリに結果が格納されるので注意。

 例えば以下のシェルスクリプトを実行すれば、「./source」に格納された*.h、*.cppファイルを解析し、「./cccc_out」に解析結果を出力する。

#!/bin/sh
find ./source -name "*.h" -o -name "*.cpp" | cccc - --outdir="./cccc_out"

 解析結果については、「cccc.html」に、全体のメトリクスや、各関数ごとのメトリクスを記述したページへのリンクが記述される。あと「cccc.xml」に、解析情報がXML形式で全部まとめて出力される。こちらはJenkinsなど他ツールと連携させる場合に有用になる。

 ユーザガイドとしては以下が詳しい。
CCCC User Guide

ユニットテスト関連の入門記事執筆

年度末辺りからですが、「Developers AppKit Box」というサイトでユニットテストに関する記事を3つほど書かせて頂きました。

ユニットテストの基礎_Developers AppKitBox | AppTest | Developers AppKitBox

テスト駆動開発の基礎_Developers AppKitBox | AppTest | Developers AppKitBox

ユニットテストを学ぶための推薦図書 | AppTest | Developers AppKitBox

いずれも入門テキストですが(本当はこれから中級・上級の記事に取り掛かる予定でしたが、転職で時間が取れなくなり中断中です・・)、何かの参考になれば幸いです。


(記事で挙げさせていただいた推薦書籍達です。この内TDD for Embedded Cは翻訳本が最近出ています)

ソフトウェアが止まったり落ちたりした状態をgdbで解析する

 ソフトウェアがデッドロックや無限ループ等で応答しなくなったり、Segmentation Fault等で強制終了したりする場合での、gdbを使った解析手法について簡単なまとめ。
 なお今回の内容はunixlinux開発での基本知識となっていると思うけれど、例えば組み込みlinux開発などでは活用できるのを知らずに、printfデバッグで頑張っている所が結構あるようだ。開発が楽になるので基礎として知っておいて損はないと思う。

ソフトウェアが落ちる状態の解析

 まず例外発生などでソフトウェアが異常終了する場合の解析について。
 例えば以下のコードを実行すると、メモリアクセスエラーで異常終了する。

//main.c
void hoge1(void)
{
        int *hoge = (int *)0xDEADBEEF;
        hoge[0] = 100;
}

void hoge2(void)
{
        hoge1();
}

int main(void)
{
        hoge2();
        return 0;
}

 このようにコアダンプが出力されるような異常終了の場合は、コアダンプをgdbで解析させて発生箇所を追跡できる。
 まずコアダンプ出力が無効化されているなら「ulimit -c unlimited」などで有効化しておく。
 そして実行ファイルを実行し、異常終了させてコアダンプ出力。

sh-3.2# ulimit -c unlimited
sh-3.2# gcc -g -o main main.c
sh-3.2# ./main
Segmentation fault: 11 (core dumped)

 異常終了したら、コアファイルと実行ファイルをgdbに読み込ませる。

sh-3.2# gdb main /cores/core.1015
(中略)
#0  0x000000010b675ea6 in hoge1 () at main.c:7
7		hoge[0] = 100;

 するとgdbはコアダンプのデータをメモリ展開して強制終了時の状態を再現し、各種解析コマンドを使用可能にする。ここで例えば「bt」コマンドを使えばどの関数で異常終了したか解析できる。

(gdb) bt
#0  0x000000010b675ea6 in hoge1 () at main.c:7
#1  0x000000010b675eb9 in hoge2 () at main.c:12
#2  0x000000010b675ecd in main () at main.c:17

 なお例外発生でソフトウェアが落ちる場合は、gdb上で実行ファイルを実行しつつ、「catch throw」「catch catch」などのコマンドで、例外発生時にブレークさせるようにすると、ソフトが異常終了する直前で解析ができる。

ソフトウェアが止まったり応答不能になる状態の解析

 デッドロックや無限ループなどで実行ファイルが停止し、応答不能になる場合の解析について。例えば単純だけれど以下のコードを扱う。

//main2.c
void hoge1(void)
{
        while (1) {}
}

int main(void)
{
        hoge1();
        return 0;
}

 これを実行すると無限ループでソフトウェアが停止する。

sh-3.2# gcc -g -o loop main2.c 
sh-3.2# ./loop &
(無限ループに)

 
 こういった場合は「ps」「ps x | grep 実行ファイル名」などでプロセスIDを取得し、gdbでアタッチする。アタッチには「gdb -p プロセスID」などを使用。

sh-3.2# ps x|grep loop
 1148 s000  R      2:30.53 ./loop
 1157 s000  R+     0:00.00 grep loop
sh-3.2# gdb -p 1148

 すると各種gdbの解析が有効になる。「where」や「bt」で、どこで止まっているか場所を把握できる。

(gdb) where
#0  hoge1 () at main2.c:3
#1  0x000000010de66efd in main () at main2.c:9

 なおこれは無限ループ以外にも、外部要因でポーリング状態が終わらなくなっている、デッドロックが発生しているといった問題の解析にも使える。ただメモリ不足といったリソース不足でソフトウェアが実行停止状態になる場合は、別の解析手段が必要になる。

gdbでのマルチスレッド処理のデバッグや制御について

 マルチスレッド処理のデバッグや解析において、gdbで各スレッドの実行・停止を制御する操作についてメモ。
 なお今回は解説で以下のサンプルコードを使用する。ここでは3つのスレッドがそれぞれ「m_count 」「t1_count 」「t2_count 」の3つの変数をインクリメントしている。

//main.c
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

static unsigned int m_count = 0, t1_count = 0, t2_count = 0;

void *thread1(void *args)
{
    while (1) {
        t1_count++;
    }
    return NULL;
}

void *thread2(void *args)
{
    while (1) {
        t2_count++;
    }
    return NULL;
}

int main(void)
{
    int i;
    pthread_t t1, t2;
    
    pthread_create(&t1, NULL, thread1, (void *)NULL);
    pthread_create(&t2, NULL, thread2, (void *)NULL);
    while (1) {
        m_count++;
    }
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    printf("exit\n");
    return 0;
}

 gdbの基本操作は今回は省略する。一応、マルチスレッド処理でスレッド単位で各種制御を行う場合の主要操作は以下の通り。

  • 「break [linespec] thread [threadno]」で特定のスレッドのみに有効なbreakpointを設定
  • 「thread apply [threadno] [command]」で特定スレッドに対しcommand実行。
  • 「thread [threadno]」でカレントスレッド切り替え。

 上記のうち「threadno」は「info threads」で取得できる。また[threadno]に「all」を指定すると全スレッドに対する一括操作ができる。

全スレッドの停止・実行をgdbで一括制御する

 まずスレッドを区別せず、全スレッドに対して一括してbreak処理、実行処理を行いたい場合だけれど、これはデフォルト設定(scheduler-locking off設定)でできる。
 上記サンプルコードをgdbで実行させた際の結果をまとめる。

続きを読む

実行中でのメモリ使用情報の取得

 前回のエントリに続いて、今回は実行中に自分自身のメモリ使用情報を取得する方法についてメモ。

 検証、プロファイリング、メモリ容量に合わせたアルゴリズムの選択処理などを行うために、実行中に自分自身が使用しているメモリサイズやそのピークサイズを取得したい場合がある。
 その実装手段はいくつかあるのだけれど、まず各OSごとのシステムコールを使うと取得できる場合がある。例えばlinuxだと、getrusage()を使用すると各種メモリ情報を取得できる。サンプルコードは以下の通り。

/** getrusage()を使った最大駐在ページサイズの取得と表示 */
void use_getrusage(void)
{
	struct rusage r;
	if (getrusage(RUSAGE_SELF, &r) != 0) {
		/*Failure*/
	}
	printf("maxrss=%ld\n", r.ru_maxrss);
}


 また、linuxunixなら「/proc/[PID]/status」を参照すれば各種メモリ情報を取得できる。そのため単純に情報を標準出力に出力したい場合は、「/proc/[PID]/status」からgrepで必要な情報を取得するコマンドをsystem()で実行すれば良い。なお[PID]はgetpid()で取得できる。

#define MAX_STRING 128

/** system()で/proc/[PID]/statusから物理メモリサイズのピーク値を検索し出力するコマンド実行 */
void use_system(void)
{
	char command[MAX_STRING];

	sprintf(command, "grep VmHWM /proc/%d/status", getpid());
	system(command);
}


 なお結果を標準出力に出力するのでなく、アプリケーション内で取得したいならば、popen()で先ほどのコマンドを実行する。出力を文字列として受け取れるので、その文字列からsscanf()やsstreamで中の数値を読みだせば、メモリ情報を取得できる。

#define MAX_STRING 128

/** popen()で/proc/[PID]/statusから使用仮想メモリサイズを検索・出力するコマンドを実行し、結果習得 */
void use_popen(void)
{
	FILE *fp;
	char command[MAX_STRING];
	char output[MAX_STRING];
	sprintf(command, "grep VmSize /proc/%d/status", getpid());
	if ((fp = popen(command, "r")) == NULL) {
		/*Failure*/
		return;
	}

	while (fgets(output, MAX_STRING, fp) != NULL) {
		//具体的な数値を取得する場合は、sscanf等で読み出し
		printf("%s", output);
	}
	
	if (pclose(fp) == -1) {
		/*Failure*/
	}
}