C言語のmain関数に対するテストコードを(何を間違ったか)書いてみました。
テスト対象サンプルソースコード
コマンドライン引数を、実行プログラム名を含めて改行区切りで出力するプログラムです(あえて「いかにも」な感じで書いております)。
#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を付けておきます。
gcc -fsanitize=address,undefined -g -Wall -Wextra -Werror -Dmain=_main -c main.c -o main.o
テストコード
テストコードを書きます。
最初に普通に渡して戻り値EXIT_SUCCESSを確認した後、今回は
-
argcに$0$以下の整数を渡す -
argvの末尾以外の要素にNULLを渡す - そもそも
argv自体NULLを渡す -
argv[argc]にNULL以外を渡す - 標準出力を閉じるか、読み取り専用で開き直すかして、
putsを失敗させる(fcloseすると未定義動作になるので、代わりにstdoutをfreopenで「読み込み専用」にします)
の場合に、戻り値EXIT_FAILUREを確認します。(特に1., 4.のケースは、EXIT_FAILUREを必ずしも返さなければならないわけではない)
#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は不要ですが、念の為つけておきます)
gcc -fsanitize=address,undefined -g -Wall -Wextra -Werror test.c main.o -o test
./test
当然、冒頭のコードのままでは、Assertion failedが出ます(テスト失敗です)。
コード修正
テストが通る(Assertion failedが無くなる)ように、コードを修正します。
これで、“決まり事”を破った入力や、標準出力でエラーが発生した場合はEXIT_FAILUREが返ることになります。
#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;
}