0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C言語のmain関数に対するテストコードを書いてみた

0
Posted at

C言語のmain関数に対するテストコードを(何を間違ったか)書いてみました。


テスト対象サンプルソースコード

コマンドライン引数を、実行プログラム名を含めて改行区切りで出力するプログラムです(あえて「いかにも」な感じで書いております)。

main.c
#include <stdio.h>

int main(int argc, char *argv[]) {
	for (int i = 0; i < argc; puts(argv[i++]));
	return 0;
}

このコードに対して

  • argcは実行プログラム名を含めたコマンドライン引数argvの個数で、1以上の整数が入る
  • argv[0]はプログラム名(NULLではない)
  • $i=1,\dots,\text{argc}-1$に対してargv[i]はNULLではない(空文字""はあり得る1)
  • argv[argc]にはNULLが入る

という“決まり事”を破った入力を入れてみた~い!! とか、puts関数がEOFを返す2ことってあるの!? と思ったことはないでしょうか。


コンパイル

何も考えずにtest.cから上記のmainを呼び出そうとしても

  • test.cのmainを呼び出す(無限再帰)
  • main同士競合してコンパイルエラーになる

ので、一旦、上記ソースコードを、-Dオプションを使ってmainの名前を変更し、オブジェクトファイルを作成します。可能な環境であれば、境界外アクセスや未定義動作を検知する為、ASanとUBSanを付けておきます。

main.sh
gcc -fsanitize=address,undefined -g -Wall -Wextra -Werror -Dmain=_main -c main.c -o main.o

テストコード

テストコードを書きます。
最初に普通に渡して戻り値EXIT_SUCCESSを確認した後、今回は

  1. argcに$0$以下の整数を渡す
  2. argvの末尾以外の要素にNULLを渡す
  3. そもそもargv自体NULLを渡す
  4. argv[argc]にNULL以外を渡す
  5. 標準出力を閉じるか、読み取り専用で開き直すかして、putsを失敗させる(fcloseすると未定義動作になるので、代わりにstdoutをfreopenで「読み込み専用」にします)

の場合に、戻り値EXIT_FAILUREを確認します。(特に1., 4.のケースは、EXIT_FAILUREを必ずしも返さなければならないわけではない)

test.c
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

extern int _main(int, char*[]);

static int test0(int argc, char *argv[]) {
	return _main(argc, argv);
}

static int test1() {
	return _main(0, (char*[] ) { NULL });
}

static int test2() {
	return _main(1, (char*[] ) { NULL, NULL });
}

static int test3() {
	return _main(1, NULL);
}

static int test4(char *arg) {
	return _main(1, (char*[] ) { arg, "" });
}

static int test5(int argc, char *argv[]) {
	// fclose(stdout);
#if defined(_WIN32) ||defined(_WIN64)
	(void) freopen("nul", "r", stdout);
#else
	(void) freopen("/dev/null", "r", stdout);
#endif
	int result = _main(argc, argv);
#if defined(_WIN32) ||defined(_WIN64)
	(void) freopen("con", "w", stdout);
#else
	(void) freopen("/dev/tty", "w", stdout);
#endif
	return result;
}

int main(int argc, char *argv[]) {
	assert(test0(argc, argv) == EXIT_SUCCESS);
	assert(test1() == EXIT_FAILURE);
	assert(test2() == EXIT_FAILURE);
	assert(test3() == EXIT_FAILURE);
	assert(test4(argv[0]) == EXIT_FAILURE);
	assert(test5(argc, argv) == EXIT_FAILURE);
	puts("All tests passed.");
	return EXIT_SUCCESS;
}

テスト実行

test.cをコンパイルして、テスト実行します。
(test.cにAsanやUBSanは不要ですが、念の為つけておきます)

test.sh
gcc -fsanitize=address,undefined -g -Wall -Wextra -Werror test.c main.o -o test
./test

当然、冒頭のコードのままでは、Assertion failedが出ます(テスト失敗です)。


コード修正

テストが通る(Assertion failedが無くなる)ように、コードを修正します。
これで、“決まり事”を破った入力や、標準出力でエラーが発生した場合はEXIT_FAILUREが返ることになります。

main.c
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
	if (argc <= 0 || argv == NULL) {
		return EXIT_FAILURE;
	}
	for (int i = 0; i < argc; i++) {
		if (argv[i] == NULL) {
			return EXIT_FAILURE;
		}
	}
	if (argv[argc] != NULL) {
		return EXIT_FAILURE;
	}
	for (int i = 0; i < argc; i++) {
		if (puts(argv[i]) == EOF) {
			return EXIT_FAILURE;
		}
	}
	return EXIT_SUCCESS;
}

  1. 空文字は./main ""のように、ダブルクォーテーションマークを2つ続けることによって入力することができます。 ↩

  2. puts関数はint型の値を返し、正常終了時には非負整数値を返し、エラー発生時にはEOFを返します。 ↩

0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?