株式会社クリアコード > ククログ

ククログ


C言語用単体テストフレームワークCutterへのHTTPテスト機能追加

現在クリアコードでインターン中のはやみずです。クリアコードのインターンシップ制度は今年度から始まり、最初のインターン生として2週間クリアコードで働かせていただくことになりました。今回と次回の2つの記事で、現在インターンシップで取り組んでいる内容について紹介したいと思います。今回の記事では、C言語用単体テストフレームワークCutterへのHTTPテスト機能追加について紹介します。

Cutter: C言語用単体テストフレームワーク

ククログでも度々紹介されているCutterですが、このテストフレームワークを利用することでC言語での単体テストを非常に効率良く開発することができます。

Cutterはできるだけ簡潔にテストを記述できるように、基本的な assert 系関数以外にも様々なユーティリティ関数を提供しています。また、GLibを利用したプログラムのテストを開発するためのGCutterや、gdk-pixbuf(C言語で画像を扱うためのライブラリ)用の GdkCutter Pixbufなどのモジュールが含まれています。これらを利用することで、GLib や Gdk Pixbuf を使ったプログラムはよりこれらのライブラリに特化したテストを簡単に書くことが可能となっています。これからも対応ライブラリは増えてゆくのかも?

Cutterの強みの1つに、C言語でありながらメモリ管理の手間が非常に少ないということが挙げられます。ほとんどのテストフレームワークは set up(準備)→test実行→tear down(後片付け) という処理の流れを基本としているので、テスト中に利用するオブジェクトは tear down のときに解放してやればよいことがわかっています。Cutter は「このオブジェクトは tear down 時に解放しといてね」ということを Cutter に教えるための API を提供しているため、この API を利用することで解放忘れによるメモリリークを防ぐことができます。例えば文字列であれば、cut_take_string(const gchar *string) を利用することで文字列を動的にアロケートした領域にコピーしてそのポインタを得ることができ、tear down時にはこの文字列が自動的に解放されます。

今回開発しているCutterのHTTPテスト機能も、このパターンを使ってオブジェクトを簡単に生成して、しかも勝手に解放してくれるようになっています。

SoupCutter: CutterのHTTPテストモジュール

さて、本題のHTTPテスト機能を実装した SoupCutter に話を移しましょう。今回の開発では、HTTPの機能を簡単に実装するために libsoup というライブラリを利用しました。というよりも、GLibをサポートするのが GCutter、Gdk Pixbuf をサポートする GdkCutter Pixbuf などのように、libsoup をサポートする SoupCutter という位置付けのほうが正確です。しかし、簡単なテストであれば libsoup 自体には一切触れることなく作成することができるので、Cutter で HTTP サーバーや HTTP クライアントのテストを簡単にできるようにするためのモジュールだと思っていただいても大丈夫です。

SoupCutter を使って HTTP サーバープログラムが正しくレスポンスを返しているかをテストするには、次のように書くことができます。

1
2
3
4
5
6
7
8
9
SoupCutClient *client = soupcut_client_new();

/* http://localhost:8080/?key=value に HTTP Request を送信 */
soupcut_client_get(client, "http://localhost:8080/",
                   "key", "value", NULL);

soupcut_client_assert_response(client);
soupcut_client_assert_equal_content_type("text/plain", client);
soupcut_client_assert_equal_body("Hello, world", client);

SoupCutClient というのは、サーバーとやりとりしたHTTPリクエスト/レスポンスを内包しているオブジェクトです。..._assert_response では、最後に受け取ったレスポンスが 2XX (200 OK など) であるかをチェックしています。同様に、..._assert_content_type では Content-Type が text/plain であることを、..._assert_equal_body ではレスポンスの本文が Hello, world であることをチェックしています。このようにして、SoupCutter を使うと非常に簡潔な記述で HTTP サーバーが思った通りに動いているかを調べることができるようになっています。

また、SoupCutter の最初の機能としては HTTP サーバーのテスト、つまりHTTPクライアントとしての機能を実装しているのですが、このHTTPクライアント機能が正しく動作しているかをテストしなければなりません。HTTPクライアント機能をテストするためには、HTTPサーバが必要です。HTTPサーバーをテストするためにHTTPクライアントを実装し、そのHTTPクライアントをテストするためにHTTPサーバーを実装する。ややこしいですね。

というわけで、SoupCutter は簡単にHTTPサーバーを作ることもできます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void
server_callback (SoupServer *server,
                 SoupMessage *msg,
                 const gchar *path,
                 GHashTable *query,
                 SoupClientContext *client,
                 gpointer user_data)
{
    .... /* リクエストを処理して結果を返す */
}

SoupServer *server;
server = soupcut_server_take_new(NULL);
soup_server_add_handler(server, "/", server_callback,
                        NULL, NULL);
soup_server_run(server);

HTTPサーバーの作成自体はたった3行でできてしまいました。server_callback は実際にリクエストを処理してレスポンスを生成するコールバック関数です。これを SoupServer のリクエストハンドラに追加して、soup_server_run でメインループに入り、サーバーが動き始めます。

ここで注目してほしいのは、サーバーを生成するときの soupcut_server_take_new です。Cutterでは take と名前のつく関数で生成したオブジェクトは、tear down時に自動で解放されます。HTTPサーバーの場合は、ちゃんとソケットの後処理まで行い、オブジェクトを解放してくれます。つまり、HTTPサーバーを簡単に作れるだけではなく、勝手に後片付けまでしてくれます。

まとめ

今回は、現在開発中である Cutter の HTTPテストモジュール SoupCutter について簡単に紹介しました。SoupCutterを使うと簡単便利にHTTPサーバー/クライアントのテストを作成することができるようになります。SoupCutter は現在クリアコードのインターンシップで開発していて、来週末に SoupCutter を含めた Cutter をリリースすることを目標に頑張っています。もし HTTP のテストを作る必要に迫られた場合には、SoupCutter を検討してみてください。

タグ: テスト | Cutter
2009-08-21

PEから公開されている関数名を抜き出す

ELFから公開されている関数名を抜き出すMach-Oから公開されている関数名を抜き出すのPE(Portable Executable)版です。PEはWindowsの.exeや.dllなどで利用されているファイルフォーマットです。

artonさんがCodeZineでDbgHelpを利用してDLLがエクスポートしている関数を列挙するという同様の内容の記事を書いています。PEのフォーマットについても説明しているので、まず、この記事を読んでおくとよいでしょう。

ここでは、DbgHelpなどライブラリを一切使わずに自力でPEをパースし、関数名を抜き出します。そのため、MinGWでクロスコンパイルすることも簡単です。実際、CutterはMinGWを用いたクロスコンパイルに対応しています。

下準備

簡略化のためファイルの内容をすべてメモリに読み込んでから処理します。コツコツ資源を利用したい場合は少しづつ読み込みながら処理することになります。

ファイルの内容を読み込むために、便利なGLibのg_file_get_contents()を使いたいところですが、Windows環境ではGLibがインストールされていないことが多いので、ここでは自力で読み込むことにします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
char *content = NULL;
FILE *file;
char buffer[4096];
size_t bytes, total_bytes = 0;

file = fopen(argv[1], "rb");
if (!file) {
    perror("failed to fopen()");
    return -1;
}

while (!feof(file)) {
    char *original_content;

    bytes = fread(buffer, 1, sizeof(buffer), file);
    total_bytes += bytes;
    original_content = content;
    content = realloc(content, total_bytes);
    if (!content) {
        free(original_content);
        fclose(file);
        perror("failed to realloc()");
        return -1;
    }
    memcpy(content + total_bytes - bytes, buffer, bytes);
}
fclose(file);

これで、contentの中にファイルの内容が格納されました。これを使って公開されている関数名を抜き出します。

PEのフォーマットに関する情報はwinnt.hで定義されています。winnt.hwindows.hをincludeすると暗黙のうちにincludeされるので、windows.hだけincludeします。

1
#include <windows.h>

PEかどうかを判断

まず、ファイルがPEかどうかを判断します。

PEであればNTヘッダに"PE\0\0"という署名が入っているので、これを確認します。"PE\0\0"という署名はIMAGE_NT_SIGNATUREというマクロとして定義されているので、これを利用します。

1
2
3
4
5
6
7
8
9
10
11
IMAGE_DOS_HEADER *dos_header;
IMAGE_NT_HEADERS *nt_headers;

/* ファイルの先頭はDOSヘッダ */
dos_header = (IMAGE_DOS_HEADER *)content;
/* NTヘッダを見つける */
nt_headers = (IMAGE_NT_HEADERS *)(content + dos_header->e_lfanew);
/* 署名が"PE\0\0"かどうか確認 */
if (nt_headers->Signature == IMAGE_NT_SIGNATURE) {
    /* PEファイル */
}

DLLかどうかを判断

PEであることが確認できたら、DLLかどうかを確認します。

1
2
3
if (nt_headers->FileHeader.Characteristics & IMAGE_FILE_DLL) {
    /* DLL */
}

公開されているシンボルを探索し出力

公開されているシンボルはエクスポートデータセクションを見るとわかります。また、シンボルが関数かどうかは、実体がテキストセクションにあるかどうかで判断します。この方法が関数かどうかを判断する標準的な方法かはわかりませんが、実用上はこれで問題なさそうです。

よって、まず、エクスポートデータセクションヘッダとテキストセクションヘッダを見つけます。それぞれ、ヘッダの名前は以下のようになっているので、それを目印に見つけます。

エクスポートデータセクションヘッダ名
.edata
テキストセクションヘッダ名
.text

以下がソースコードです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
WORD i;
IMAGE_SECTION_HEADER *first_section;
IMAGE_SECTION_HEADER *edata_section;
IMAGE_SECTION_HEADER *text_section;

/* 最初のセクションヘッダ */
first_section = IMAGE_FIRST_SECTION(nt_headers);
for (i = 0; i < nt_headers->FileHeader.NumberOfSections; i++) {
    const char *section_name;

    section_name = (const char *)((first_section + i)->Name);
    /* 各セクションの名前を確認 */
    if (strcmp(".edata", section_name) == 0) {
        /* エクスポートデータセクションを発見 */
        edata_section = first_section + i;
    } else if (strcmp(".text", section_name) == 0) {
        /* テキストセクションを発見 */
        text_section = first_section + i;
    }
}

ヘッダが見つかったら、セクションの内容を見て、関数であるシンボル名を出力します。*1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
IMAGE_EXPORT_DIRECTORY *export_directory;
const char *base_address;
ULONG *name_addresses;
ULONG *function_addresses;
DWORD min_text_section_address, max_text_section_address;

/* エクスポートデータセクションの内容 */
export_directory =
    (IMAGE_EXPORT_DIRECTORY *)(content +
                               edata_section->PointerToRawData);

/* エクスポートデータセクション内のデータがあるアドレスを解決するための
   基準になるアドレス */
base_address =
    content +
    edata_section->PointerToRawData -
    edata_section->VirtualAddress;
/* シンボル名があるアドレス */
name_addresses =
    (ULONG *)(base_address + export_directory->AddressOfNames);
/* シンボルの実体への相対的なアドレス(RVA)があるアドレス */
function_addresses =
    (ULONG *)(base_address + export_directory->AddressOfFunctions);
/* テキストセクションのデータの相対的なアドレスの下限 */
min_text_section_address = text_section->VirtualAddress;
/* テキストセクションのデータの相対的なアドレスの上限 */
max_text_section_address =
    min_text_section_address + text_section->SizeOfRawData;

/* シンボル名毎に関数かどうか判断 */
for (i = 0; i < export_directory->NumberOfNames; i++) {
    const char *name;
    DWORD function_address;

    /* シンボル名 */
    name = base_address + name_addresses[i];
    /* シンボルの実体の相対的なアドレス */
    function_address = function_addresses[i];
    if (min_text_section_address < function_address &&
        function_address < max_text_section_address) {
        /* シンボルの実体がテキストセクションにあるなら関数 */
        printf("found: %s\n", name);
    }
}

参考

まとめ

winnt.hを使って、DbgHelpなどに依存せずに、PEから公開されている関数名を抜き出す方法を紹介しました。

サンプルプログラムはDebian GNU/Linux上でMinGWを使ってクロスコンパイルし、Wineで動作を確認しました。

今のところ、Cutterがサポートしている共有ライブラリのフォーマットはELF/Mach-O/PEです。このPE編で、公開されている関数名を抜き出す方法を紹介するシリーズは最後です。もし、今後Cutterが対応するフォーマットが増えれば、そのフォーマットから関数名を抜き出す方法を紹介するかもしれません。

*1 コメント中のRVAの説明はざっくりなので、詳しくは冒頭のartonさんの記事を参照してください。

タグ: Cutter

この記事の続き

2009-08-14

Mach-Oから公開されている関数名を抜き出す

ELFから公開されている関数名を抜き出すのMach-O版です。ただし、Universal Binaryには対応していません。

Mach-Oのフォーマットの詳細はMac OS X ABI Mach-O File Format Reference(英語)を見てください。

下準備

簡略化のためファイルの内容をすべてメモリに読み込んでから処理します。コツコツ資源を利用したい場合は少しづつ読み込みながら処理することになります。

ファイルの内容を読み込むにはGLibのg_file_get_contents()が便利です。

1
2
3
4
gchar *content;
gsize length;

g_file_get_contents(filename, &content, &length, NULL);

これで、contentの中にファイルの内容が格納されました。これを使って公開されている関数名を抜き出します。

Mach-Oのフォーマットに関する情報はmach-o/loader.hで定義されています。また、シンボルテーブルの各エントリのフォーマットに関する情報はmach-o/nlist.hで定義されています。Mach-Oをパースするときはこれらのヘッダファイルを使うと便利です。ここでもこれらのヘッダファイルを使います。

1
2
#include <mach-o/loader.h>
#include <mach-o/nlist.h>

Mach-Oかどうかを判断

まず、ファイルがMach-Oかどうかを判断します。

Mach-Oは最初にヘッダが入っていて、それを見ることでMach-Oかどうかを判断することができます。ここでは、32bit環境用のMach-Oだけを対象とします。64bit環境用のMach-Oを対象とする場合はコード中のいくつかの型の最後に「_64」を追加します。(例えば、mach_header -> mach_header_64。)どちらにも対応する場合はCutterのソースを参考にしてください。

1
2
3
4
5
6
struct mach_header *header;

header = (struct mach_header *)content;
if (header->magic == MH_MAGIC) {
    /* Mach-Oファイル */
}

共有ライブラリかどうかを判断

Mach-Oであることが確認できたら、共有ライブラリかどうかを確認します。

1
2
3
if (header->filetype == MH_DYLIB) {
    /* 共有ライブラリ */
}

バンドルからもシンボルを取り出すことができるので、バンドルにも対応するのもよいでしょう。

1
2
3
if (header->filetype == MH_DYLIB || header->filetype == MH_BUNDLE) {
    /* 共有ライブラリかバンドル */
}

公開されているシンボルを探索し出力

Mac OS X ABI Mach-O File Format Referenceの「Figure 1 Mach-O file format basic structure」にある通り、ヘッダの後にはロードコマンドと呼ばれる部分が複数続きます。いくつかあるロードコマンドのうち、興味があるのは以下の2つです。

LC_SEGMENT
セグメントに関する情報を提供する。
LC_SYMTAB
シンボルに関する情報を提供する。

シンボル名とシンボルが公開されているかはLC_SYMTABから取得できます。シンボルが関数と関連づけられているかは、シンボルが__TEXTセグメントの__textセクションで定義されているかどうかで判断できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
gsize offset;
uint32_t i, n_commands;
uint32_t section_index = 0, text_section_index = 0;

/* ファイルの先頭からコマンドの先頭までのバイト数 */
offset = sizeof(*header);
/* コマンド数 */
n_commands = header->ncmds;
for (i = 0; i < n_commands; i++) {
    struct load_command *load;

    load = (struct load_command *)(content + offset);
    switch (load->cmd) {
    case LC_SEGMENT: /* セグメント用コマンド */
    {
        struct segment_command *segment;
        struct section *section;
        gint j;

        /* セグメント */
        segment = (struct segment_command *)(content + offset);
        /* __TEXTセグメント以外は興味がない */
        if (!g_str_equal(segment->segname, "__TEXT")) {
            /* セクション数だけ数えてスキップ */
            section_index += section->nsects;
            break;
        }

        /* セクション */
        section = (struct section *)(content + offset + sizeof(*segment));
        for (j = 0; j < segment->nsects; j++, section++) {
            section_index++;
            /* __textセクションが何番目のセクションかを記録 */
            if (g_str_equal(section->sectname, "__text"))
                text_section_index = section_index;
        }
        break;
    }
    case LC_SYMTAB: /* シンボルテーブル用コマンド */
    {
        struct symtab_command *table;
        struct nlist *symbol;
        gchar *string_table;
        gint j;

        /* シンボルテーブルコマンド */
        table = (struct symtab_command *)(content + offset);
        /* シンボルリスト */
        symbol = (struct nlist *)(content + table->symoff);
        /* シンボル名が入っている文字列テーブル */
        string_table = content + table->stroff;
        for (j = 0; j < table->nsyms; j++, symbol++) {
            gboolean defined_in_section = FALSE;

            /* シンボルがセクションで定義されているか */
            if ((symbol->n_type & N_TYPE) == N_SECT)
                defined_in_section = TRUE;

                /* シンボルが__textセクションで定義されていて */
            if (defined_in_section && symbol->n_sect == text_section_index &&
                /* 公開されている */
                symbol->n_type & N_EXT) {
                gchar *name;
                int32_t string_offset;

                string_offset = symbol->n_un.n_strx;
                /* 文字列テーブルからシンボル名を取得 */
                name = string_table + string_offset;
                /* シンボル名の先頭に「_」がついているので2文字目以降を表示 */
                g_print("found: %s\n", name + 1);
            }
        }
        break;
    }
    default:
        break;
    }
    /* 次のコマンドに進む */
    offset += load->cmdsize;
}

参考

まとめ

mach-o/loader.hmach-o/nist.hを使って、BFDライブラリに依存せずに、Mach-Oから公開されている関数名を抜き出す方法を紹介しました。

関数名が取得できたら、GModuleで関数本体を取得することができます。GModuleに渡す関数名の先頭には「_」をつける必要はありません。

いずれ、PEから公開されている関数名を抜き出す方法も紹介するかもしれません。

タグ: Cutter
2009-06-25

ELFから公開されている関数名を抜き出す

先日、書きやすさとデバッグのしやすさを重視したC言語用テスティグフレームであるCutter 1.0.7がリリースされました。

Cutterでは、定義したテスト関数をフレームワークに登録する必要はありません。Cutterを用いたテストでは、共有ライブラリとしてテストを作成し、cutterコマンドでその共有ライブラリを読み込んで定義されているテスト関数を検出し実行します。

1.0.6までのCutterは、共有ライブラリから定義されているテスト関数を抽出するためにBFDライブラリを用いていました。しかし、共有ライブラリではなく静的ライブラリとしてBFDライブラリが提供されているプラットフォームがわりとあり、導入の障壁となる場合がありました。そこで、Cutter 1.0.7ではBFDライブラリに依存せず、共有ライブラリから定義されているテスト関数を抽出する機能を実装しました。

Cutter 1.0.7はELF/PE/Mach-Oに対応しているため、Linux, *BSD, Solaris, Cygwin, Mac OS Xなどの環境でもBFDライブラリなしで動作するようになりました。

ELFのフォーマットを解説しているページや、readelfなどのELF関連ツールを紹介しているページはあるのですが、ELFからシンボル名を抜き出すプログラムを紹介しているページがなかったので、Cutterで行っている、ELFから公開されている関数名を抜き出す方法を紹介します。

下準備

簡略化のためファイルの内容をすべてメモリに読み込んでから処理します。コツコツ資源を利用したい場合は少しづつ読み込みながら処理することになります。

ファイルの内容を読み込むにはGLibのg_file_get_contents()が便利です。

1
2
3
4
gchar *content;
gsize length;

g_file_get_contents(filename, &content, &length, NULL);

これで、contentの中にファイルの内容が格納されました。これを使って公開されている関数名を抜き出します。

ELFのフォーマットに関する情報はelf.hで定義されています。ELF をパースするときはelf.hを使うと便利です。ここでも、elf.hを使います。

1
#include <elf.h>

ELFかどうかを判断

まず、ファイルがELFかどうかを判断します。

ELFは最初にヘッダが入っていて、それを見ることでELFかどうかを判断することができます。ここでは、64bit環境用のELFだけを対象とします。32bit環境用のELFを対象とする場合はコード中の「64」という箇所を「32」に変更します。どちらにも対応する場合はCutterのソースを参考にしてください。

1
2
3
4
5
6
7
Elf64_Ehdr *header = NULL;

header = (Elf64_Ehdr *)content;

if (memcmp(header->e_ident, ELFMAG, SELFMAG) == 0) {
    /* ELFファイル */
}

「MAG」は「マジック」の略だと思います。

共有ライブラリかどうかを判断

ELFであることが確認できたら、共有ライブラリかどうかを確認します。

1
2
3
if (header->e_type == ET_DYN) {
    /* 共有ライブラリ */
}

.dynsym/.dynstr/.textのセクションヘッダを探索

.dynsymセクションには動的に解決されるシンボルが入っています。.dynstrセクションにはそれらのシンボルの名前が入っています。これらを見ることで共有ライブラリの中にあるシンボル名の一覧を取得することができます。

.textには関数の本体などが入っています。.dynsymにあるシンボルが.textセクションに関連していると、共有ライブラリ内で定義されているシンボルだということがわかります。

.dynsym/.dynstr/.textのセクションヘッダを探し出すコードは以下のようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
Elf64_Shdr *dynstr = NULL;
Elf64_Shdr *dynsym = NULL;
uint16_t text_section_header_index = 0;

gsize section_offset;
uint16_t section_header_size;
uint16_t i, n_headers;
Elf64_Shdr *section_name_header;
gsize section_name_header_offset;
const gchar *section_names;

/* ファイルの先頭からセクションの先頭までのバイト数 */
section_offset = header->e_shoff;
/* 1つのセクションヘッダのバイト数 */
section_header_size = header->e_shentsize;
/* セクション数 */
n_headers = header->e_shnum;

/* ファイルの先頭からセクション名があるヘッダの先頭までのバイト数 */
section_name_header_offset =
    header->e_shoff +
    (header->e_shstrndx * header->e_shentsize);
/* セクション名があるヘッダ */
section_name_header =
    (Elf64_Shdr *)(content + section_name_header_offset);
/* セクション名が格納されている位置の先頭 */
section_names = content + section_name_header->sh_offset;

for (i = 0; i < n_headers; i++) {
    Elf64_Shdr *section_header = NULL;
    gsize offset;
    const gchar *section_name;

    /* ファイルの先頭からセクションヘッダの先頭までのバイト数 */
    offset = section_offset + (section_header_size * i);
    /* セクションヘッダ */
    section_header = (Elf64_Shdr *)(content + offset);
    /* セクション名 */
    section_name = section_names + section_header->sh_name;

    if (g_str_equal(section_name, ".dynstr")) {
        /* .dynstrセクション */
        dynstr = section_header;
    } else if (g_str_equal(section_name, ".dynsym")) {
        /* .dynsymセクション */
        dynsym = section_header;
    } else if (g_str_equal(section_name, ".text")) {
        /* .textセクションが先頭から何番目のセクションか */
        text_section_header_index = i;
    }
}

公開されているシンボル名一覧

.dynsym/.dynstr/.textのセクションヘッダが見つかったら、それらのセクションにアクセスして、共有ライブラリ内に定義されているシンボル一覧を取得できます。

公開されているシンボルが関数かどうかを判断する条件は、シンボルが.textセクションに関連付けられているかどうかです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
guint i, n_entries;
gsize symbol_section_offset;
gsize symbol_entry_size;
gsize name_section_offset;

/* ファイルの先頭からシンボルが定義されているセクションまでのバイト数 */
symbol_section_offset = dynsym->sh_offset;
/* シンボル定義領域のバイト数 */
symbol_entry_size = dynsym->sh_entsize;
/* ファイルの先頭からシンボル名が定義されているセクションまでのバイト数 */
name_section_offset = dynstr->sh_offset;
/* シンボル定義領域の数 */
if (symbol_entry_size > 0)
    n_entries = dynsym->sh_size / symbol_entry_size;
else
    n_entries = 0;

for (i = 0; i < n_entries; i++) {
    Elf64_Sym *symbol;
    uint64_t name_index;
    unsigned char info;
    uint16_t section_header_index;
    gsize offset;

    /* ファイルの先頭からシンボル定義領域までのバイト数 */
    offset = symbol_section_offset + (i * symbol_entry_size);
    /* シンボル定義 */
    symbol = (Elf64_Sym *)(content + offset);
    /* シンボル名は何番目に定義されているか */
    name_index = symbol->st_name;
    /* シンボルの情報 */
    info = symbol->st_info;
    /* シンボルに関連するセクションは何番目のセクションか */
    section_header_index = symbol->st_shndx;

        /* シンボルは関数に関連付けられている */
    if ((info & STT_FUNC) &&
        /* シンボルは公開されている */
        (ELF64_ST_BIND(info) & STB_GLOBAL) &&
        /* シンボルは.textセクションに関連付けられている */
        (section_header_index == text_section_header_index)) {
        const gchar *name;

        /* シンボル名 */
        name = content + name_section_offset + name_index;
        g_print("found: %s\n", name);
    }
}

参考

まとめ

elf.hを使って、BFDライブラリに依存せずに、ELFから公開されている関数名を抜き出す方法を紹介しました。ELFを読み書きするlibelfというライブラリもあるのですが、ELFから情報を取得するだけなら、elf.hで十分でしょう。

関数名が取得できたら、GModuleで関数本体を取得することができます。GLibは便利ですね。

いずれ、PEまたはMach-Oから公開されている関数名を抜き出す方法も紹介するかもしれません。

タグ: Cutter
2009-05-22

問題解決につながるエラーメッセージ

プログラムを書いていると問題に遭遇します。問題に遭遇したときはエラーメッセージが問題解決の重要な情報になります。しかし、エラーメッセージがあるだけでは問題解決にはつながりません。問題解決に役立つエラーメッセージとそうでもないエラーメッセージがあります。

ここでは、Rubyでの例をまじえながら問題解決に有用なエラーメッセージを紹介します。ライブラリなど多くの人が使うようなプログラムを作成する場合は参考になるかもしれません。

問題解決への道

問題に遭遇してから問題を解決するまでには以下の順で作業をする必要があります。

  1. 問題の把握
  2. 問題の原因の調査
  3. 原因の解決方法の検討
  4. 解決方法の実装

役立つエラーメッセージがあると「1. 問題の把握」、「2. 問題の原因の調査」、「3. 原因の解決方法の検討」がはかどります。

問題の値を示す

エラーが発生すれば問題が起こっている事実は把握できます。次にすることは、どのような問題が起こっているかを調査することです。

String#gsubにはいくつかの使い方がありますが、その1つは以下のように正規表現と文字列を引数にする使い方です。

1
2
>> "abcde".gsub(/c/, "C")
=> "abCde"

もちろん、違うオブジェクトを渡すとエラーが発生します。

1
2
3
4
>> "abcde".gsub([:first], [:second])
TypeError: can't convert Array into String
        from (irb):2:in `gsub'
        from (irb):2

配列を文字列に変換できなかったといっています。しかし、ここでは引数に配列を2つ指定しています。このエラーメッセージでは「配列を文字列に変換できなかった」ことはわかりますが、「どの配列を文字列に変換できなかった」かはわかりません。

正規表現のリテラルでも、正規表現の構文が間違っている場合はエラーが発生します。

1
2
3
4
5
>> Regexp.new("(")
RegexpError: premature end of regular expression: /(/
        from (irb):3:in `initialize'
        from (irb):3:in `new'
        from (irb):3

この場合は「正規表現に問題がある」というだけではなく、「どの正規表現に問題がある」かも示しています。

このように、問題を起こしたオブジェクトの情報も示すことで「問題を把握」しやすくなります。エラーメッセージには、問題を起こしたオブジェクトの情報も含めるようにしましょう。

どう悪いのかを示す

問題が把握できたら、どうしてその問題が発生したのか、原因を調べます。多くの場合、エラーメッセージに問題の原因は書かれています。しかし、そうではない場合もあります。できるだけ、エラーメッセージには問題の原因も含めるようにしましょう。

Time.iso8601はISO 8601で定められた文字列のフォーマットをパースし、Timeオブジェクトにします。

1
2
3
4
>> require 'time'
=> true
>> Time.iso8601("2009-04-10T12:02:54+09:00")
=> Fri Apr 10 03:02:54 UTC 2009

不正なフォーマットの場合はエラーが発生します。

1
2
3
4
>> Time.iso8601("2009-04-10I12:02:54+09:00")
ArgumentError: invalid date: "2009-04-10I12:02:54+09:00"
        from /usr/lib/ruby/1.8/time.rb:376:in `iso8601'
        from (irb):6

この例では真ん中あたりの「T」が「I」になっているためフォーマットに適合していません。

もし、「『I』という不正な文字があります」というようなメッセージが入っていると、問題の原因を簡単に把握できるようになります。

エラーメッセージには大雑把な原因だけではなく、できるだけ詳しく原因を書くようにしましょう。

期待を示す

問題の原因がわかったら、その問題を解決する方法を検討します。期待している値がわかると、解決する方法を検討しやすくなります。

String#deleteは1つ以上の引数をとります。1つも引数を与えない場合はエラーが発生します。

1
2
3
4
5
6
>> "abcde".delete("a")
=> "bcde"
>> "abcde".delete
ArgumentError: wrong number of arguments
        from (irb):2:in `delete'
        from (irb):2

エラーメッセージを見ると「引数の数が違う」ということがわかります。これで「問題の原因」を把握することができます。

しかし、「問題の原因」はわかってもどうすればその問題を解決できるかはわかりません。引数の数を変えればよいということはわかりますが、いくつにすればよいかがわからないのです。

期待している値を示すと、問題を解決しやすくなります。

1
2
3
4
>> "abcde".gsub
ArgumentError: wrong number of arguments (0 for 2)
        from (irb):3:in `gsub'
        from (irb):3

このエラーメッセージからはString#gsubが2つの引数を期待していることがわかるので、解決案として「引数を2つ渡す」というアイディアが浮かびます。次にすることは「引数に何を2つ渡すか」を考えることです。

エラーメッセージに「期待していること」を含めると、解決案が浮かびやすくなります。できるだけ、期待していることも含めるようにしましょう。

まとめ

Rubyを例にして問題解決に役立つエラーメッセージについて紹介しました。

問題解決に役立つエラーメッセージの特長は、テストの実行結果にもあてはまります。クリアコードが開発に関わっているテスティングフレームワークではテストの実行結果にこだわっています。

  • Cutter: C言語用テスティングフレームワーク
  • UxU: Firefoxアドオン開発用テスティングフレームワーク

あなたが使っているテスティングフレームワークは問題解決に役立つような情報を提供していますか?

タグ: Ruby | Cutter | UxU
2009-04-10

Cutter 1.0.6リリース

先日、C言語用単体テストフレームワークであるCutterの新バージョン1.0.6がリリースされました。(アナウンス

ハイライト

NEWSにも書いてあるように、今回のリリースでも多くの新機能がありますが、ここではその中でも特におすすめの構造体定義なしで複雑なテストデータを使えるAPIを紹介します。

この新機能によりもっと簡単にデータ駆動テストが書けるようになります。実際、milter managerのテストが簡単に書けるようになりました。

データ駆動テスト

データ駆動テストとは同じテストケースに対して複数のテストパターンを適用するテスト手法です。これにより多くのテストパターンを簡単に書くことができるという利点があります。

例えば、入力された文字列の小文字をすべて大文字に変換するto_upper()関数のテストをするとします。テストデータは以下の3つを考えます*1

  • すべて小文字の文字列
  • 小文字と大文字が混ざった文字列
  • すべて大文字の文字列

これはこのように書けます。

1
2
3
cut_assert_equal_string("HELLO", to_upper("hello"));
cut_assert_equal_string("HELLO", to_upper("HelLo"));
cut_assert_equal_string("HELLO", to_upper("HELLO"));

データ駆動テストでは、テストケースはこのようになります。

1
cut_assert_equal_string(expected, to_upper(input));

このうち、expectedとinputを外部から与えることになります。Cutterではこのように書きます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void
data_to_upper (void)
{
#define ADD_DATUM(label, expected, input)             \
  gcut_add_datum(label,                               \
                 "expected", G_TYPE_STRING, expected, \
                 "input", G_TYPE_STRING, input,       \
                 NULL)

    ADD_DATUM("all lower", "HELLO", "hello");
    ADD_DATUM("mixed", "HELLO", "HelLo");
    ADD_DATUM("all upper", "HELLO", "HELLO");

#undef ADD_DATUM
}

void
test_to_upper (gconstpointer data)
{
    const gchar *expected, *input;

    expected = gcut_data_get_string(data, "expected");
    input = gcut_data_get_string(data, "input");
    cut_assert_equal_string(expected, to_upper(input));
}

QtのQtTestLibではこのようになります。 Chapter 2: Data Driven Testingより:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void TestQString::toUpper_data(QtTestTable &t)
{
    t.defineElement("QString", "string");
    t.defineElement("QString", "result");

    *t.newData("all lower") << "hello" << "HELLO";
    *t.newData("mixed")     << "Hello" << "HELLO";
    *t.newData("all upper") << "HELLO" << "HELLO";
}

void TestQString::toUpper()
{
    FETCH(QString, string);
    FETCH(QString, result);

    COMPARE(string.toUpper(), result);
}

ほとんど同じくらいの手間で書けているのではないでしょうか。

テストプログラムは重複があってもいいからわかりやすい方がよい、とよく言われます。しかし、そのために、重複がたくさんある中から、興味がある重複していない部分が見つけづらくなるのは問題かもしれません。データ駆動テストでは、どのようにテストしたいのかがわかりやすいテストケースになる傾向がある気がしています。

データ駆動テストのサポート状況

多くのテスティングフレームワークはデータ駆動テストをサポートしています。PerlのTest::Base、RubyのRSpec*2、C++のQtのQtTestLibやGoogle Testでもサポートされています。他にもまだまだたくさんあります。

テストデータの入力方法も様々で、テストプログラム中でテストデータを生成するものから、データベースからテストデータを取り出すもの、CSV、Excelなどから取り出すものもあります。

まとめ

動的に複雑なことができるスクリプト言語、アノテーションなどでメタデータを指定できる最近の言語、トリッキーなC++などでは便利にデータ駆動テストを実行できるテスティングフレームワークは多くあります。しかし、C言語用のフレームワークではそんなになかったのではないかと思います。*3

Cutterに興味がある方は使ってみたり、メーリングリストなどで提案、質問などしてみてください。

*1 他にも空文字列やアルファベット以外が入った文字列などが考えられます。

*2 動的にspecを定義してね、という方針のようなのでサポートしているというか、Rubyを使っているからできちゃうという感じ

*3 少なくともまだ見つけられていない。C言語用のテスティングフレームワークのAPIや機能に関してまとまっている場所はあるのだろうか。

タグ: Cutter | テスト
2009-03-02

Cutter 1.0.5リリース

ここには書いていませんでしたが、Cutter 1.0.3のリリース のリリースの約1ヶ月後に1.0.4がリリースされました。 さらにその約2ヶ月後の昨日、1.0.5がリリースされました。 CutterとはC言語用の単体テス トフレームワークです。

新機能

詳細は NEWSに 書いてありますが、1.0.5での目玉新機能は以下の3点です。

  • 画像データ入出力・操作ライブラリ gdk-pixbuf (←少し古い。最新版(英語) )をサポート
  • 多くの検証とテスト便利関数を追加
  • ユーザ定義検証作成のサポートを強化

gdk-pixbufサポート

gdk-pixbufをサポートすることにより、画像が等しいかどうかを検 証できるようになりました。また、もし、画像が異なる場合は画像 間の差分を示し、画像のどこが異なるのかをわかりやすくしていま す。

例えば、このような画像を比較したとします。

期待画像 実際の画像

これらの画像では赤い丸の部分が異なっています。

これらの画像の差分画像は以下のようになります。

差分画像

左上に期待画像、右上に実際の画像、左下に異なるピクセルを示し た画像、右下に異なるピクセルを強調表示、同じピクセルを弱めに 表示した画像を配置しています。左下の画像を見ることでどの部分 が異なるのかが具体的にわかり、右下の画像を見ることで比較画像 はどのあたりが異なるのかを相対的に確認する事ができます。

もっとよい表現方法があるかもしれませんが、しばらくはこの方法 を採用しする予定です。もしかすると、今後、よりよい表現方法に 変更されるかもしれません。

ちなみに、この機能はPDF操作・レンダリングライブラリである Popplerテスト で利用されていま す。

検証・テスト便利関数の追加

Cutterを使用していて、こんな検証・便利関数があったら便利だ、 と感じたものは積極的にCutter本体に取り込んでいます。

1.0.5では12個の検証、9個の便利関数が追加されました。追加され た検証・便利関数のリストは NEWSに 書かれています。

ユーザ定義検証作成のサポートを強化

今までもユーザが独自で検証を定義することはできたのですが、バッ クトレースを取得するためにマクロとして定義する必要がありまし た。

1.0.5では cut_trace() というマクロが追加され、検証を関数として定義してもバックトレー スを取得することができるようになりました。マクロは可変長引数 が簡単に書けるなど便利な事も多いのですが、構文エラーが見つけ にくいなどという問題もあります。1.0.5からは関数とマクロを使い 分けられるようになり、より便利にデバッグのしやすいテストが書 けるようになりました。

まとめ

Cutter 1.0.5ではテスト作成・デバッグ支援の機能が強化され、よ り便利なテスト環境を提供するようになりました。今までよりもC 言語でのテスト作成が楽しくなるかもしれません。

[チュートリアル ] [リファレンスマニュアル ] [ダウンロード ]

タグ: Cutter | テスト
2008-10-16

Cutter導入事例: Senna (2)

ちょうど1ヶ月前の話の続きです。

前回でCutterでテストを作成するための環境ができたので、実際にテストを作成していきます。と、思ったのですが、もう一点やらなければいけないことが残っていました。テスト対象のライブラリの初期化についてです。

今回はテスト対象ライブラリの初期化について説明してからテスト作成に入ります。

前回同様、コードの断片がでてきます。完全なものはSennaのリポジトリを見てください。

ライブラリの初期化

Sennaのようにライブラリ初期化・終了関数 (sen_init()/sen_fin())を用意している場合は、テストの作成に 入る前に、もう一つ用意しておかなければいけない仕組みがありま す。このような関数を持っているライブラリをテストする場合は、 テスト全体を実行する前に初期化関数を、実行した後に終了関数を 呼び出す必要があります。これを行う仕組みを用意する必要があり ます。

cutterコマンドは指定されたディレクトリ以下の共有ライブラリを かき集めて、その中からテストを検出して実行します。その時に以 下の条件にあう共有ライブラリを見つけると、テスト全体を実行す る前後に特定の関数を実行することができます。これは、今まさに 必要としている機能です。

  • ファイル名が「suite_」からはじまっている

この共有ライブラリが以下の名前のシンボルを公開している場合は、 その関数をテスト全体を実行する前後に実行します。ここでは、共 有ライブラリのファイル名はsuite_senna_test.soとします。

  • senna_test_warmup(): テスト全体を実行する前に実行
  • senna_test_cooldown(): テスト全体を実行した後に実行

「_warmup」と「_cooldown」の前の「senna_test」の部分は共有ラ イブラリのファイル名から先頭の「suite_」と拡張子を除いた部分 です。

Sennaの場合は以下のようなsuite-senna-test.cを作成します。

test/unit/suite-senna-test.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <senna.h>

void senna_test_warmup(void);
void senna_test_cooldown(void);

void
senna_test_warmup(void)
{
  sen_init();
}

void
senna_test_cooldown(void)
{
  sen_fin();
}

suite-senna-test.cをビルドするためにMakefile.amに以下を追加 します。

test/unit/Makefile.am:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if WITH_CUTTER
...

noinst_LTLIBRARIES =                                \
        suite_senna_test.la
endif

INCLUDES =                        \
        -I$(srcdir)                \
        -I$(top_srcdir)                \
        -I$(top_srcdir)/lib        \
        $(SENNA_INCLUDEDIR)

AM_CFLAGS = $(GCUTTER_CFLAGS)
AM_LDFLAGS = -module -rpath $(libdir) -avoid-version

LIBS =                                                \
        $(top_builddir)/lib/libsenna.la                \
        $(GCUTTER_LIBS)

suite_senna_test_la_SOURCES = suite-senna-test.c

よくあるMakefile.amの書き方です。noinst_LTLIBRARIESをif WITH_CUTTER ... endifの中に入れているのは、Cutterがない環境で はビルド対象からはずし、ビルドエラーにならないようにするため です。

これで、test/unit/.libs/suite_senna_test.soがビルドされるよう になり、テスト全体を実行する前後にSennaの初期化・終了処理を行 うことができます。

テストの作成

テスト実行環境が整ったのでテストを作成します。ここでは検索キー ワードの周辺テキストを取得するsnippet APIのテスト を1つ作成します。

テストの流れは以下の通りです。

  1. sen_snip_open()でsen_snipオブジェクトの生成
  2. sen_snip_add_cond()でキーワードを指定
  3. sen_snip_exec()でsnippetを生成
  4. sen_snip_get_result()で取得した結果が期待していたものか を検証
  5. sen_snip_close()で生成したsen_snipオブジェクトを開放

基本的なCutterのテスト作成方法についてはチュートリアル を参考にしてください。

sen_snip_open()のテスト

まずは、sen_snip_open()でsen_snipオブジェクトを生成する部分 と、sen_snip_close()で生成したsen_snipオブジェクトを開放する 部分を作成します。

今回はGLibサポート付きでCutterを使用するgcutterパッケージを 使っているので、テストは以下のようにgcutter.hを利用します。

test/unit/test-snip.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <senna.h>
#include <gcutter.h>

void test_simple_exec(void);

static const gchar default_open_tag[] = "[[";
static const gchar default_close_tag[] = "]]";

void
test_simple_exec(void)
{
  sen_snip *snip;

  snip = sen_snip_open(sen_enc_default, 0, 100, 10,
                       default_open_tag, strlen(default_open_tag),
                       default_close_tag, strlen(default_close_tag),
                       NULL);
  cut_assert_not_null(snip);
  sen_snip_close(snip);
}

sen_snip_open()は引数が多いですが、ここでは気にする必要はあ りません。sen_snip_open()によりsen_snip *が生成されることだけ知っ ていれば問題ありません。

cut_assert_not_null(snip) でsen_snip *が正常に生成されているかを確認します。これは、 sen_snip_open()は失敗時にはNULLを返すからです。

最後にsen_snip_close()で生成したsen_snip *を開放します。

sen_snip_add_cond()のテスト

次はsen_snip_add_cond()でキーワードを指定する処理を追加しま す。sen_snip_add_cond()の戻り値はsen_rcです。sen_rcはエラー 番号を示す数値でsen_success(0)以外はエラーになります。よって テストは以下のようになります。sen_snip_open()のときと同じく、 sen_snip_add_cond()の引数は気にしなくても構いません。

test/unit/test-snip.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
...
void
test_simple_exec(void)
{
  sen_snip *snip;
  const gchar keyword[] = "Senna";

  snip = sen_snip_open(sen_enc_default, 0, 100, 10,
                       default_open_tag, strlen(default_open_tag),
                       default_close_tag, strlen(default_close_tag),
                       NULL);
  cut_assert_not_null(snip);

  cut_assert_equal_int(sen_success,
                       sen_snip_add_cond(snip, keyword, strlen(keyword),
                                         NULL, 0, NULL, 0));

  sen_snip_close(snip);
}

sen_snip_add_cond()の結果は cut_assert_equal_int で検証しています。

ただし、ここで問題があります。cut_assert*()は検証が失敗する とその時点でテスト関数からreturnし、それ以降のコードは実行し ません。つまり、cut_assert_equal_int()が失敗した場合は、 sen_snip_open()で生成したsen_snip *が開放されないことになり ます。この問題を解決するためにsetup()/teardown()という仕組み があります。

setup()はテストが実行される前に必ず実行され、teardown()はテ ストが実行された後に成功・失敗に関わらず必ず実行されます。こ の仕組みを利用することで確実にメモリ開放処理を行うことができ ます。

test/unit/test-snip.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
...
static sen_snip *snip;
...

void
setup(void)
{
  snip = NULL;
}

void
teardown(void)
{
  if (snip) {
    sen_snip_close(snip);
  }
}

void
test_simple_exec(void)
{
  const gchar keyword[] = "Senna";

  snip = sen_snip_open(sen_enc_default, 0, 100, 10,
                       default_open_tag, strlen(default_open_tag),
                       default_close_tag, strlen(default_close_tag),
                       NULL);
  cut_assert_not_null(snip);

  cut_assert_equal_int(sen_success,
                       sen_snip_add_cond(snip, keyword, strlen(keyword),
                                         NULL, 0, NULL, 0));
}

これでcut_assert_equal_int()が成功しても失敗してもsen_snip * は開放されます。Cutterではメモリ開放処理のためにstatic変数と setup()/teardown()を使うことが定石になっています。

sen_snip_exec()のテスト

次はsen_snip_add_cond()で設定したキーワード用のsnippetを生成 するsen_snip_exec()のテストです。sen_snip_exec()もsen_rcを返 すので、それを検証します。また、引数でsnippet数とsnippet文字 列のバイト数も受けとるのでそれも検証します。特に目立った部分 はありません。

test/unit/test-snip.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
...
static const gchar text[] =
  "Senna is an embeddable fulltext search engine, which you can use in\n"
  "conjunction with various scripting languages and databases. Senna is\n"
  "an inverted index based engine, & combines the best of n-gram\n"
  "indexing and word indexing to achieve fast, precise searches. While\n"
  "senna codebase is rather compact it is scalable enough to handle large\n"
  "amounts of data and queries.";
...
void
test_simple_exec(void)
{
  ...
  unsigned int n_results;
  unsigned int max_tagged_len;

  ...

  cut_assert_equal_int(sen_success,
                       sen_snip_exec(snip, text, strlen(text),
                                     &n_results, &max_tagged_len));
  cut_assert_equal_uint(2, n_results);
  cut_assert_equal_uint(105, max_tagged_len);
}
sen_snip_get_result()のテスト

最後はsen_snip_exec()で生成したsnippetの内容が正しいかどうか のテストです。snippetはsen_snip_get_result()で取得できるので その結果を検証します。n_resultsが2なので2回 sen_snip_get_result()を呼び出す必要があります。

snippetを格納する場所のサイズは動的に決まります。そのため、 snippetを格納する領域を動的に確保する必要があります。 setup()/teardown()の仕組みを用いてメモリを開放するようにしま す。ここ以外は特に目立った部分はありません。

test/unit/test-snip.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
...
static gchar *result;

void
setup(void)
{
  ...
  result = NULL;
}

void
teardown(void)
{
  ...
  if (result) {
    g_free(result);
  }
}

void
test_simple_exec(void)
{
  ...
  unsigned int result_len;

  ...
  result = g_new(gchar, max_tagged_len);

  cut_assert_equal_int(sen_success,
                       sen_snip_get_result(snip, 0, result, &result_len));
  cut_assert_equal_string("[[Senna]] is an embeddable fulltext search engine, "
                          "which you can use in\n"
                          "conjunction with various scripti",
                          result);
  cut_assert_equal_uint(104, result_len);

  cut_assert_equal_int(sen_success,
                       sen_snip_get_result(snip, 1, result, &result_len));
  cut_assert_equal_string("ng languages and databases. [[Senna]] is\n"
                          "an inverted index based engine, & combines "
                          "the best of n-gram\ni",
                          result);
  cut_assert_equal_uint(104, result_len);
}

これで単純にsnippet APIを使った場合のテストが1つできました。 同様に、異常な場合や違ったデータを用いた場合などのテストを作 成していきます。

diff

せっかくなのでCutterのテスト結果の出力方法を紹介します。

Cutterは cut_assert_equal_string で文字列の比較が失敗したときには、どの部分が異なったかという 差分情報を表示します。

例えば、今回のテストの最後のcut_assert_equal_string()が失敗 した場合は以下のような差分情報が表示されます。

diff:
  - ng languages and DBes. [[Senna]] is
  ?                  ^^
  + ng languages and databases. [[Senna]] is
  ?                  ^^^^^^^
  - an Inverted Index Based Engine, & combines the best of n-gram
  ?    ^        ^     ^     ^
  + an inverted index based engine, & combines the best of n-gram
  ?    ^        ^     ^     ^
    i

このときの期待した結果は以下の通りです。

ng languages and DBes. [[Senna]] is
an Inverted Index Based Engine, & combines the best of n-gram
i

実際の結果は以下の通りです。

ng languages and databases. [[Senna]] is
an inverted index based engine, & combines the best of n-gram
i

差分を見てもらうと分かる通り、異なっている行を示すだけではな くて、行内で異なっている文字まで示しています。(例えば、DBの 下に^^が付いている。)

広く使われているunified diff形式では行内で異なる文字は表示し ません。テストでは1行のみの比較を行うことも多く、行単位だけ の差分よりも文字単位での差分表示も行った方がデバッグが行いや すいという判断からこのような形式になっています。

この形式はPythonのdifflibにあるndiffの形式と同じものです。

テスト全体

今回作成したテストは以下の通りです。

test/unit/test-snip.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <senna.h>
#include <gcutter.h>

void test_simple_exec(void);

static sen_snip *snip;
static const gchar default_open_tag[] = "[[";
static const gchar default_close_tag[] = "]]";
static const gchar text[] =
  "Senna is an embeddable fulltext search engine, which you can use in\n"
  "conjunction with various scripting languages and databases. Senna is\n"
  "an inverted index based engine, & combines the best of n-gram\n"
  "indexing and word indexing to achieve fast, precise searches. While\n"
  "senna codebase is rather compact it is scalable enough to handle large\n"
  "amounts of data and queries.";
static gchar *result;

void
setup(void)
{
  snip = NULL;
  result = NULL;
}

void
teardown(void)
{
  if (snip) {
    sen_snip_close(snip);
  }
  if (result) {
    g_free(result);
  }
}

void
test_simple_exec(void)
{
  const gchar keyword[] = "Senna";
  unsigned int n_results;
  unsigned int max_tagged_len;
  unsigned int result_len;

  snip = sen_snip_open(sen_enc_default, 0, 100, 10,
                       default_open_tag, strlen(default_open_tag),
                       default_close_tag, strlen(default_close_tag),
                       NULL);
  cut_assert_not_null(snip);

  cut_assert_equal_int(sen_success,
                       sen_snip_add_cond(snip, keyword, strlen(keyword),
                                         NULL, 0, NULL, 0));

  cut_assert_equal_int(sen_success,
                       sen_snip_exec(snip, text, strlen(text),
                                     &n_results, &max_tagged_len));
  cut_assert_equal_uint(2, n_results);
  cut_assert_equal_uint(105, max_tagged_len);

  result = g_new(gchar, max_tagged_len);

  cut_assert_equal_int(sen_success,
                       sen_snip_get_result(snip, 0, result, &result_len));
  cut_assert_equal_string("[[Senna]] is an embeddable fulltext search engine, "
                          "which you can use in\n"
                          "conjunction with various scripti",
                          result);
  cut_assert_equal_uint(104, result_len);

  cut_assert_equal_int(sen_success,
                       sen_snip_get_result(snip, 1, result, &result_len));
  cut_assert_equal_string("ng languages and databases. [[Senna]] is\n"
                          "an inverted index based engine, & combines "
                          "the best of n-gram\ni",
                          result);
  cut_assert_equal_uint(104, result_len);
}

問題発生時に有用なデバッグ情報を増やしたり、より読みやすいテ ストにするなど、いろいろ改良するべき点は残っていますが、今回 はこれで終了します。実際のコードはSennaのリポジトリを参照 してください。

まとめ

2回に分けて以下のことについて説明しました。

  • GNUビルドシステムを採用した既存のプロジェクトへのCutter の組み込み方法
    • cutter.m4で提供するM4マクロの使用方法
    • Cutterをインストールしていないユーザへの対応
    • Cutterをインストールしていない開発者への対応
  • Cutterを用いたテスト環境の構築方法
    • 便利なテスト起動スクリプトrun-test.shの作成方法
    • 初期化・終了関数があるライブラリのテスト方法
  • Cutterを用いたテストの作成方法
    • setup()/teardown()を用いたメモリ管理の方法
    • diffの出力

Cで書かれたプロジェクトに単体テストフレームワークを導入する 場合はCutterも検討してみてはいかがでしょうか。

タグ: Cutter | テスト
2008-08-25

Cutter導入事例: Senna (1)

Sennaの単体テストフレームワー クとしてCutterを導入したときの手順です。自分のプロジェクトに Cutterを導入するときの参考になるかもしれません。全体として そこそこ長くなってしまったので、何回かに分割して紹介することに します。

内容はSennaのリポジトリ でやったことの一部です。リポジトリは公開されているので、試行錯誤の 後などをみたい場合はコミットを追いかけるとよいでしょう。また、ここで は断片としてしか出てこないコードについても、リポジトリの中には完全な 形で入っています。

もし、まだCutterについて知らない場合は、はじめにチュートリ アル を読んでください。

はじめに

まず、Sennaについて簡単に説明します。

Sennaは組み込み型の全文検索エンジンで、その機能をライブラリ として提供します。SennaのAPIはbasic APIやadvanced APIなどい くつかのグループにわかれています。

今回はSennaの単体テストフレームワークとしてCutterを導入し、 utility APIのひとつ、snippet*1のテストを 作成するまでを示します。このためには以下の作業が必要になりま す。

  • SennaのビルドシステムにCutterを組み込む
  • Cutterでsnippet APIのテストを記述する

作業に入る前にSennaのビルドシステムについて確認します。

Sennaのビルドシステム

SennaではGNU AutomakeGNU Libtoolな どGNUビルドシステムを利用したビルドシステムを採用しています。

CutterはGNUビルドシステムサポート用の機能をいくつか提供してい ます。そのため、GNUビルドシステムを用いているプロジェクトへ はCutterを容易に導入することができます。

もし、これからプロジェクトを始める場合でGNUビルドシステムを 採用する場合はCutterのチュートリアル が参考になるでしょう。

ビルドシステムへのCutterの組み込み

Sennaの単体テストフレームワークとしてCutterを採用するにあたっ て、以下のような条件を満たすこととします。

  • Cutterがない場合でもユーザがSennaをビルドできること
  • Cutterがない場合でも開発者がSennaをビルドできること
  • configure時にCutterを使用するかどうかを指定できること
  • Cutterで作成したテストはtest/unit/以下に配置すること
  • テストではテストを簡潔・容易に記述するためにGLibを利用す る

上記の中でのユーザと開発者の違いは、autogen.shを用いて自分で configureを作成するかどうかです。ユーザは開発者が作成した configureを利用するため、自分でconfigureを作成しません。一方、 開発者はSubversionリポジトリ内にはconfigureは入っていないの でautogen.shを使ってconfigure.acからconfigureを作成し、利用 します。つまり、違いは以下の通りになります。

  • ユーザ: configureのみ実行
  • 開発者: autogen.shとconfigureを実行

それでは、まずは、開発者はすべてCutterをインストールしている ものとしてCutter対応のconfigureを生成できるようにします。

cutter.m4

Cutterはconfigure.ac内で利用できるCutter検出用のM4マクロを cutter.m4として提供しています。このファイルは ${PREFIX}/share/aclocal/cutter.m4としてインストールされます。 ${PREFIX}/share/aclocal/以下に他の.m4ファイルがインストールされ ているような環境ではおそらくそのままで大丈夫ですが、そうでな い場合はautogen.shの中でaclocalを呼び出しているところを編集 して${PREFIX}/share/aclocal/以下を.m4ファイルの検索パスに加 える必要があります。

もし、Cutterのconfigureに--prefix=/tmp/localオプションをつけ てビルド・インストールした場合はautogen.shを以下のように変更 する必要があります。

1
2
3
4
5
6
7
8
9
10
11
12
13
Index: autogen.sh
===================================================================
--- autogen.sh        (リビジョン 820)
+++ autogen.sh        (作業コピー)
@@ -105,7 +105,7 @@
 echo "Running libtoolize ..."
 $LIBTOOLIZE --force --copy
 echo "Running aclocal ..."
-$ACLOCAL ${ACLOCAL_ARGS} -I .
+$ACLOCAL ${ACLOCAL_ARGS} -I . -I /tmp/local/share/aclocal
 echo "Running autoheader..."
 $AUTOHEADER
 echo "Running automake ..."

あるいはautogen.shを実行する時に環境変数ACLOCAL_ARGSを指定し ます。

% ACLOCAL_ARGS="-I /tmp/local/share/aclocal" ./autogen.sh

これでconfigure.ac内でCutterが提供する便利M4マクロを利用する 準備が整いました。

テスト作成用パッケージ

Cutterはパッケージを pkg-configのパッ ケージとしてインストールします。パッケージをpkg-configのパッ ケージとして作成しているのは、pkg-configが広く普及していて、 GNUビルドツールなどpkg-configに対応しているビルドシステムが 多いからです。

Cutterは、テスト作成用に以下の2つのパッケージを用意しています。

  • cutter: Cutterを利用してテストを作成する場合に利用
  • gcutter: cutterパッケージにGLibサポート機能を追加したパッ ケージ。GLibを利用してもっと簡潔・容易にテストを書きたい 場合に利用

今回はGLibを利用してテストを作成するので、cutterパッケージで はなくgcutterパッケージを利用します。

Cutterはconfigure.acで簡単にcutter/gcutterパッケージの設定を 行えるように以下のM4マクロを提供しています。

AC_CHECK_CUTTER

cutterパッケージ検出マクロです。以下の変数をAC_SUBSTしま す。

  • CUTTER: cutterコマンドのパス
  • CUTTER_CFLAGS: cutterパッケージを用いたテストをビルド するためのCFLAGS
  • CUTTER_LIBS: cutterパッケージを用いたテストをビルド するためのLIBS

また、cutterパッケージが利用不可能な場合は ac_cv_use_cutterが"no"になります。

AC_CHECK_GCUTTER
gcutterパッケージ検出マクロです。上述のAC_CHECK_CUTTERマ クロがAC_SUBSTする変数に加えて、以下の変数もAC_SUBSTしま す。
  • GCUTTER_CFLAGS: gcutterパッケージを用いたテストをビル ドするためのCFLAGS
  • GCUTTER_LIBS: gcutterパッケージを用いたテストをビルド するためのLIBS

今回はGLibサポートがついたgcutterパッケージを利用するので、 AC_CHECK_GCUTTERマクロを利用します。よってconfigure.acには以 下を追加することになります。

configure.ac:

1
2
3
4
5
6
AC_CHECK_GCUTTER

AM_CONDITIONAL([WITH_CUTTER], [test "$ac_cv_use_cutter" != "no"])
if test "$ac_cv_use_cutter" != "no"; then
  AC_DEFINE(WITH_CUTTER, 1, [Define to 1 if you use Cutter])
fi

これで、Makefile.amではCutterが利用できるかどうかはif WITH_CUTTER ... endifで判断できます。Makefile.amではCutterが 利用できない場合はテストプログラムをビルドしないようにします。 こうすることにより、ユーザがCutterをインストールしていなくて も、Sennaをビルドできます。

cutter.m4がない場合への対応

cutter.m4がない場合は./autogen.shの実行が失敗します。つまり、 開発者がconfigureを正常に生成できなくなります。

残念ながら、Cutterはそれほど有名なフリーソフトウェアではない ため、開発者がCutterをインストールしていることはほとんどあり ません。そこで、開発者がCutterをインストールしていなくても configureを生成できるようにします。*2

cutter.m4がインストールされているかどうかはAC_CHECK_GCUTTER 関数が定義されているかどうかでわかります。そのため、以下のよ うに書くことにより、Cutterがインストールされてない環境でも configureを生成できます。もちろん、生成されたconfigureには Cutterの検出機能などはありません。

configure.ac:

1
2
3
4
5
6
7
8
9
m4_ifdef([AC_CHECK_GCUTTER], [
AC_CHECK_GCUTTER
],
[ac_cv_use_cutter="no"])

AM_CONDITIONAL([WITH_CUTTER], [test "$ac_cv_use_cutter" != "no"])
if test "$ac_cv_use_cutter" != "no"; then
  AC_DEFINE(WITH_CUTTER, 1, [Define to 1 if you use Cutter])
fi

このようにAC_CHECK_GCUTTERの呼び出し部分をm4_ifdefの中に入れ るだけです。AC_CHECK_GCUTTERが定義されていない場合は ac_cv_use_cutterを"no"にしているのでWITH_CUTTERが真になるこ とはありません。

ビルドシステムへtest/unit/以下を追加

Cutterを用いたテストプログラムはtest/unit/以下に配置します。 このディレクトリは新規に作成するため、以下の作業が必要になり ます。

  • test/Makefile.amのSUBDIRSにunitを追加
  • configureでtest/unit/Makefileを生成する設定を追加
  • test/unit/Makefile.amの作成

まずは、test/Makefile.amのSUBDIRSにunitを追加し、test/unit/ 以下もビルド対象とします。

test/Makefile.am:

1
SUBDIRS = unit

続いて、configure.acのAC_CONFIG_FILESにtest/unit/Makefileを 追加し、configureがtest/unit/Makefileを生成するようにします。

configure.ac:

1
AC_CONFIG_FILES([... test/unit/Makefile ...])

最後に、test/unit/Makefile.amを作成し、test/unit/以下のビル ド方法を設定します。とりあえず、今は空っぽでかまいません。

% touch test/unit/Makefile.am

これで、test/unit/以下をSennaのビルドシステムに加えることがで きました。再度./autogen.sh, ./configureを実行してからmakeす れば、test/unit/以下もビルド対象になっていることがわかります。

% ./autogen.sh
% ./configure
% make
...
make[3]: ディレクトリ `.../test/unit' に入ります
...
テスト起動コマンド

test/unit/以下がビルド対象に加わったので、test/unit/以下に作 成するテストプログラムを起動するコマンドを作成します。このテ スト起動コマンドはmake checkから呼び出されることになります。

テスト起動コマンドは伝統的にrun-test.shというシェルスクリプ トになっています。このシェルスクリプトからcutterコマンドを呼 び出してテストを実行します。

cutterを実行するときはいくつかオプションを指定する必要があり ます。例えば、テストプログラムがあるディレクトリなどがそれで す。ここでrun-test.shを作成する理由は、cutterへ渡すオプション などを指定しなくてもよいようにするなど、より簡単にテストを実 行できるようにするためです。

テストが簡単に実行できるということはとても重要なことです。テ ストを実行することが面倒だと、だんだんテストを実行しなくなっ てしまうからです。テストが実行されないと、新しくテストを作成 することも面倒になってくるでしょう。これは悪い循環といえます。 これを防ぐためにも最初のうちから簡単にテストを実行できる仕組 みを用意しておくことが重要です。

また、引数なしでも動くrun-test.shを用意することにはもう一つ理 由があります。それは、GNU Automakeが提供するテスト起動の仕組 みであるmake checkからも利用できるようにすることです。make checkでは指定されたテスト起動スクリプトが引数なしでテストを実 行できる必要があります。*3

前置きが長くなりましたがテストをもっと簡単に走らせるためのス クリプト、run-test.shは以下のようになります。

test/unit/run-test.sh:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/sh

export BASE_DIR="`dirname $0`"

if test x"$NO_MAKE" != x"yes"; then
    make -C $BASE_DIR/../../ > /dev/null || exit 1
fi

if test -z "$CUTTER"; then
    CUTTER="`make -s -C $BASE_DIR echo-cutter`"
fi

if test x"$CUTTER_DEBUG" = x"yes"; then
    CUTTER="$BASE_DIR/../../libtool --mode=execute gdb --args $CUTTER"
fi

CUTTER_ARGS="-s $BASE_DIR"
$CUTTER $CUTTER_ARGS "$@" $BASE_DIR

このスクリプトではmake check以外からも便利に利用できるように なっています。make check以外から起動された場合(つまり直接 test/unit/run-test.shを軌道した場合)は必要なビルドを行ってか らテストを起動します。つまり、run-test.shからテストを起動した 場合はビルド忘れがなくなります。

実は、上記のrun-test.shを直接起動できるようにするためには、 test/unit/Makefile.amにも一工夫する必要があります。それは、 configureで検出したcutterコマンドのパスをrun-test.shに伝える ためのターゲットを用意するということです。

test/unit/Makefile.am:

1
2
echo-cutter:
        @echo $(CUTTER)

これで、run-test.shを直接起動しても、必要に応じてビルドした り、情報を集めたりしてテストを起動してくれます。

また、make checkではテスト結果とビルド結果が混ざりそこそこの 出力になりますが、run-test.sh経由でビルド・テストを行うと必 要最小限の出力になり、問題の発見が簡単になります。実際の開発 は以下のようなサイクルになります。

  1. ソース変更
  2. test/unit/run-test.shを実行

    テスト失敗→(1)に戻る

  3. コミット→(1)に戻る

手順が少ないため開発のリズムが崩れにくくなります。このサイク ルをより簡単に行うための方法もあるのですが、それはまた別の機 会にします。

run-test.shができたので、make checkでrun-test.shを起動するよ うにMakefile.amを変更します。

test/unit/Makefile.am:

1
2
3
4
5
if WITH_CUTTER
TESTS = run-test.sh
TESTS_ENVIRONMENT = NO_MAKE=yes
...
endif

TESTS_ENVIRONMENTにNO_MAKE=yesを指定することにより、make check経由の場合はテスト実行前のmake実行を抑制します。

これでテストを実行するための環境は整いました。きりがよいので 今回はここまでにします。

まとめ

ここまでで、以下のことについて説明しました。

  • GNUビルドシステムを採用した既存のプロジェクトへのCutter の組み込み方法
    • cutter.m4で提供するM4マクロの使用方法
    • Cutterをインストールしていないユーザへの対応
    • Cutterをインストールしていない開発者への対応
  • Cutterを用いたテスト環境の構築方法
    • 便利なテスト起動スクリプトrun-test.shの作成方法

続きではテストを作成します。

*1 検索キーワードの周辺テキストの こと。ここではそれを取得するSennaの機能のこと。

*2 本当は開発者には頻繁に テストを走らせて欲しいのでCutterを必須にしたいところです。

*3 テスト起動スクリプトにオプションを 指定する場合は環境変数を利用します。

タグ: Cutter | テスト
2008-07-25

Cutter 1.0.3リリース

昨日、C言語用の単体テストフレームワークである Cutterの1.0.3がリリースされま した。

実は、Cutter-1.0リリースから3回リリースしていま す。1.0.0以降はマイクロバージョンだけを上げていますが、新しく 追加された機能はマイクロとは思えません。例えば、Windows (MinGW)でのビルド に対応、GStreamer のサポートなどといった機能が含まれていました。過去のリリースに ついてはNEWS を見てください。

Cutterとは

Cutterはテストの書きやすさ・テスト結果からのデバッグのしやす さを重視したC言語用の単体テストフレームワークです。今回のリリー スからCutterの機能を説明したページ を用意 しました。

データ駆動テスト対応

同じテストを条件を変えて実行したい時があります。例えば、以下 のような場合です。

  • 複数の入力パターンがあり、それらを網羅的にテストする場合
  • 複数のバックエンドを抽象化し、どのバックエンドを利用して いる場合でも同じインターフェイスで扱えるライブラリをテス トする場合(Cでのcairo、Perl/Ruby/GaucheなどでのDBI、 RubyでのActiveRecordなど)

このような場合、必要な分だけテストコードをコピー&ペーストして テストを作成するよりも、以下のように書けるとテスト記述・管理 のコストを下げることができます。

  • テストは1つだけ用意
  • テスト条件、つまり、入力データを複数用意
  • 各入力データに対してそれぞれテストを実行

このようなテストの方法をデータ駆動テストと呼びます。

データ駆動テストではデータの用意の仕方にはいくつかの方法があ り、それぞれ利点があります。

データベースに保存された入力データを利用
大量のデータを用意したり、データを一括変更できるなどデー タ管理機能が豊富
CSVなど表形式の入力データを利用
Excelなどを利用して入力データを用意することができる
プログラム内で入力データを生成
動的にデータを用意するので、柔軟にデータを生成すること ができる。例えば、文字'a', 'b', 'c'を使って作られる長さ が3の文字列すべて("abc", "acb", ...)、などというデータ を用意できる。

Cutterでは今回のリリースで、最後の「プログラム内で入力データ を生成」する方法をサポートしました。使い方は以下の通りです。

  • data_XXX(void)を定義
  • data_XXX()中でcut_add_data()を使ってデータを登録
  • test_XXX(const void *data)を定義
    • dataにはcut_add_data()で登録したデータの1つが渡る

今までどおり、関数を定義するだけでよく、他のC言語用の単体テ ストフレームワークにあるような「登録処理」のようなことは必要 ありません。Cutterが自動で見つけてくれます。

コードにすると以下のようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void
data_XXX(void)
{
    cut_add_data("データ1の名前", data1, data1_free_function,
                 "データ2の名前", data2, data2_free_function,
                 "データの例", strdup("test data"), free,
                 ...)
}

void
test_XXX(const void *data)
{
    /* dataはdata_XXX()で登録した「data1」か「data2」
       か「strdup("test data")」。test_XXX()はそれぞれに対
       して1回ずつ、計3回呼ばれる。
     */
    cut_assert_equal_string("test data", data);
}

具体例は cut_add_data() を見てください。

まとめ

Cutter 1.0.3ではデータ駆動テストをサポートし、より簡単にテス トがかけるようになりました。

タグ: Cutter | テスト
2008-07-16

タグ:
年・日ごとに見る
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|