1
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?

vscodeとgccのみでC言語のUTテストソースを自動作成してみようとするときのメモ

Posted at

chatgptに相談して

  • VSCode
  • C言語
  • gcc

のみで自動的に単体試験用コードを書いてもらえないか相談したときのメモ
※なお現時点で調整しながらやっているので精確さは低め

配布物の構成(拡張なし・ゼロ追加インストール)

your-project/
├─ .vscode/
│  ├─ tasks.json           # 生成・ビルド・実行のタスク定義(VS Code標準)
│  └─ c.code-snippets      # 任意:補助スニペット(拡張不要)
├─ tools/
│  └─ utgen.c              # C関数を解析して tests/*.c を自動生成するCプログラム
├─ ut/
│  └─ ut.h                 # 超軽量テストヘッダ(Unity不要・外部依存ゼロ)
└─ (あなたのCソースたち)
  • インストール不要:拡張・npm・make等なし。gcc と gdb だけでOK。
  • **配布は“このフォルダごと”**でOK(Zip/コピーで共有)。

セットアップ手順(配布時マニュアル)

  1. プロジェクト直下に上の構成を配置(.vscode/, tools/, ut/ を含む)。
  2. VS Code でプロジェクトを開く。
  3. 初回だけ:タスク build:utgen を実行(utgen生成)。
  4. 任意の .c ファイルを開いた状態でタスク gen:tests (current file) を実行 → tests/xxx_test.c が生成。
  5. タスク build:testsrun:tests で実行。必要なら gdb:tests でデバッグ。

.vscode/tasks.json

{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "build:utgen",
      "type": "shell",
      "command": "gcc",
      "args": [
        "tools/utgen.c",
        "-o",
        "tools/utgen"
      ],
      "problemMatcher": "$gcc",
      "group": "build"
    },
    {
      "label": "gen:tests (current file)",
      "type": "shell",
      "dependsOn": "build:utgen",
      "command": "./tools/utgen",
      "args": [
        "${relativeFile}",
        "tests"
      ],
      "problemMatcher": []
    },
    {
      "label": "build:tests",
      "type": "shell",
      "command": "bash",
      "args": [
        "-lc",
        "mkdir -p build && for f in tests/*_test.c; do gcc -Iut \"$f\" -o \"build/$(basename \"${f%.c}\").out\"; done"
      ],
      "problemMatcher": "$gcc",
      "group": "build"
    },
    {
      "label": "run:tests",
      "type": "shell",
      "command": "bash",
      "args": [
        "-lc",
        "for exe in build/*_test.out; do echo \"=== RUN $(basename \"$exe\")\"; \"$exe\"; echo; done"
      ]
    },
    {
      "label": "gdb:tests",
      "type": "shell",
      "command": "bash",
      "args": [
        "-lc",
        "gdb -q build/*_test.out"
      ],
      "problemMatcher": []
    }
  ]
}

bash が使えない場合は cmd/powershell 向けに置き換えてください(Linux前提ならこのままでOK)。


tools/utgen.c(C→UT雛形ジェネレータ)

  • 追加ライブラリ不要の超軽量パーサ(完全なCパーサではありません/実務80%想定)
  • function_definition らしきパターンを簡易抽出し、tests/xxx_test.c を生成/追記します
  • 返り値が void なら副作用チェックコメント、非 void なら簡易ASSERTを入れます
// tools/utgen.c
// 使い方: ./tools/utgen path/to/source.c tests
// 依存: なし(標準Cのみ)
// 役割: source.c から関数定義をゆるく抽出し、tests/source_test.c を生成/追記する

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

#define MAX_SRC 10485760

typedef struct {
    char ret[256];
    char name[256];
    char params[512]; // 元のパラメタ文字列(型ダミー生成に利用)
} Func;

static void trim(char *s) {
    size_t n = strlen(s);
    while (n && isspace((unsigned char)s[n-1])) s[--n] = 0;
    size_t i = 0;
    while (s[i] && isspace((unsigned char)s[i])) i++;
    if (i) memmove(s, s+i, strlen(s+i)+1);
}

static int looks_like_func_header(const char *line) {
    // ざっくり: 末尾が '{' or '){' で、'(' と ')' を含む(prototype ではなく定義)
    const char *p = line;
    int lp=0, rp=0;
    while (*p) { if (*p=='(') lp++; else if (*p==')') rp++; p++; }
    if (!(lp && rp)) return 0;
    // 末尾のブレースを確認
    p = line + strlen(line);
    while (p>line && isspace((unsigned char)p[-1])) --p;
    return (p>line && p[-1]=='{');
}

static void pick_name_ret_params(const char *line, Func *f) {
    // かなり単純化: "ret ... name(params) {" を想定
    // name: '(' の直前の識別子
    const char *lp = strchr(line, '(');
    const char *rp = lp ? strrchr(line, ')') : NULL;
    if (!lp || !rp || rp<lp) { strcpy(f->name, "func"); strcpy(f->ret, "int"); strcpy(f->params,""); return; }

    // name
    const char *q = lp-1;
    while (q>line && (isalnum((unsigned char)q[-1]) || q[-1]=='_')) q--;
    size_t nlen = (size_t)(lp - q);
    if (nlen >= sizeof(f->name)) nlen = sizeof(f->name)-1;
    strncpy(f->name, q, nlen); f->name[nlen]=0; trim(f->name);

    // params
    size_t plen = (size_t)(rp - lp - 1);
    if (plen >= sizeof(f->params)) plen = sizeof(f->params)-1;
    strncpy(f->params, lp+1, plen); f->params[plen]=0; trim(f->params);

    // ret(先頭〜name直前)
    char retbuf[512]; memset(retbuf,0,sizeof(retbuf));
    size_t rlen = (size_t)(q - line);
    if (rlen >= sizeof(retbuf)) rlen = sizeof(retbuf)-1;
    strncpy(retbuf, line, rlen); retbuf[rlen]=0; trim(retbuf);
    // 末尾のポインタ*や修飾はそのまま
    if (strlen(retbuf)==0) strcpy(retbuf, "int");
    strncpy(f->ret, retbuf, sizeof(f->ret)-1);
    f->ret[sizeof(f->ret)-1]=0;
}

static int is_void_ret(const char *ret) {
    // "void" を含むなら void とみなす(雑に対応)
    return strstr(ret, "void") != NULL;
}

static void default_args(const char *params, char *out, size_t outsz) {
    // 超簡易:引数数だけ 0 や NULL を並べる
    // 例: "int a, char* s" -> "0, NULL"
    char buf[512]; strncpy(buf, params, sizeof(buf)-1); buf[sizeof(buf)-1]=0;
    char *p = buf;
    size_t used = 0;
    int first = 1;
    while (*p) {
        // 1引数トークンを抽出(カンマ区切り)
        char *comma = strchr(p, ',');
        char tok[256];
        if (comma) {
            size_t tlen = (size_t)(comma - p);
            if (tlen >= sizeof(tok)) tlen = sizeof(tok)-1;
            strncpy(tok, p, tlen); tok[tlen]=0;
            p = comma + 1;
        } else {
            strncpy(tok, p, sizeof(tok)-1); tok[sizeof(tok)-1]=0;
            p += strlen(p);
        }
        trim(tok);
        if (strlen(tok)==0) continue;

        // 型らしき文字列
        // ポインタ or char* -> NULL / "TODO"
        const char *val = "0";
        if (strstr(tok, "*")) val = "NULL";
        else if (strstr(tok, "char") && strstr(tok, "*")) val = "\"TODO\"";
        else if (strstr(tok, "float") || strstr(tok, "double")) val = "0.0";
        else if (strstr(tok, "char")) val = "'\\0'";
        else if (strstr(tok, "void")) { continue; }

        if (!first) { if (used+2<outsz) { out[used++]=','; out[used++]=' '; } }
        size_t vl = strlen(val);
        if (used+vl < outsz) { memcpy(out+used, val, vl); used+=vl; }
        first = 0;
    }
    if (used < outsz) out[used]=0;
}

static void ensure_dir(const char *path) {
#ifdef _WIN32
    (void)path;
#else
    char cmd[512];
    snprintf(cmd, sizeof(cmd), "mkdir -p \"%s\"", path);
    system(cmd);
#endif
}

int main(int argc, char **argv) {
    if (argc < 3) {
        fprintf(stderr, "usage: %s <source.c> <tests_dir>\n", argv[0]);
        return 1;
    }
    const char *src = argv[1];
    const char *outdir = argv[2];
    ensure_dir(outdir);

    // 読み込み
    FILE *fp = fopen(src, "rb");
    if (!fp) { perror("open source"); return 1; }
    char *code = (char*)malloc(MAX_SRC);
    size_t n = fread(code, 1, MAX_SRC-1, fp);
    fclose(fp);
    code[n]=0;

    // 1行ずつ見て関数定義ヘッダを拾う(複数行宣言もつなげる簡易実装)
    char *p = code;
    char line[2048]; line[0]=0;
    Func funcs[1024]; int fcnt=0;

    while (*p) {
        // 1行
        char *nl = strchr(p, '\n');
        size_t len = nl ? (size_t)(nl - p) : strlen(p);
        char buf[1024];
        if (len >= sizeof(buf)) len = sizeof(buf)-1;
        strncpy(buf, p, len); buf[len]=0;
        p = nl ? nl + 1 : p + len;

        // コメント行・プリプロセスっぽいのは飛ばす(雑)
        char tmp[1024]; strncpy(tmp, buf, sizeof(tmp)-1); tmp[sizeof(tmp)-1]=0; trim(tmp);
        if (tmp[0]=='#' || strncmp(tmp, "//", 2)==0) continue;

        // 継続
        strncat(line, " ", sizeof(line)-strlen(line)-1);
        strncat(line, buf, sizeof(line)-strlen(line)-1);

        // ヘッダ候補?
        char cand[2048]; strncpy(cand, line, sizeof(cand)-1); cand[sizeof(cand)-1]=0; trim(cand);
        if (looks_like_func_header(cand)) {
            Func f; memset(&f,0,sizeof(f));
            pick_name_ret_params(cand, &f);
            if (fcnt < (int)(sizeof(funcs)/sizeof(funcs[0]))) {
                funcs[fcnt++] = f;
            }
            line[0]=0; // クリア
        }
        // 行が長くなりすぎたらリセット
        if (strlen(line) > 1500) line[0]=0;
    }

    if (fcnt==0) {
        fprintf(stderr, "no function definitions found.\n");
        free(code);
        return 0;
    }

    // 出力パス
    const char *slash = strrchr(src, '/');
#ifdef _WIN32
    const char *bslash = strrchr(src, '\\');
    if (!slash || (bslash && bslash>slash)) slash = bslash;
#endif
    const char *base = slash ? slash+1 : src;

    char outpath[512];
    snprintf(outpath, sizeof(outpath), "%s/%s", outdir, base);
    size_t blen = strlen(outpath);
    if (blen >= 2 && outpath[blen-2]=='.' && (outpath[blen-1]=='c' || outpath[blen-1]=='C')) {
        outpath[blen-2]=0; strcat(outpath, "_test.c");
    } else {
        strcat(outpath, "_test.c");
    }

    // 既存読み込み(追記対応)
    FILE *fo = fopen(outpath, "a+");
    if (!fo) { perror("open out"); free(code); return 1; }
    fseek(fo, 0, SEEK_END);
    long sz = ftell(fo);
    int fresh = (sz == 0);
    if (fresh) {
        fprintf(fo,
"#include \"ut/ut.h\"\n"
"#include \"%.*s.h\" // 必要に応じて修正\n\n",
            (int)(strlen(base)-2), base);
        fprintf(fo, "UT_MAIN_BEGIN();\n");
    }

    // 既存内容を重複防止のため軽く読む
    fseek(fo, 0, SEEK_SET);
    char *old = (char*)calloc(1, (size_t)sz + 1);
    fread(old, 1, (size_t)sz, fo);
    fseek(fo, 0, SEEK_END);

    for (int i=0;i<fcnt;i++) {
        // 既に test_関数があればスキップ
        char sig[256]; snprintf(sig, sizeof(sig), "void test_%s(void)", funcs[i].name);
        if (old && strstr(old, sig)) continue;

        char args[256]; args[0]=0; default_args(funcs[i].params, args, sizeof(args));
        if (is_void_ret(funcs[i].ret)) {
            fprintf(fo,
"\nvoid test_%s(void) {\n"
"  // Arrange: TODO\n"
"  // Act\n"
"  %s(%s);\n"
"  // Assert: TODO (副作用検証など)\n"
"}\n"
"UT_RUN(test_%s);\n",
                funcs[i].name, funcs[i].name, args, funcs[i].name);
        } else {
            fprintf(fo,
"\nvoid test_%s(void) {\n"
"  // Arrange: TODO\n"
"  // Act\n"
"  auto ret = %s(%s);\n"
"  // Assert\n"
"  UT_ASSERT_EQ(/* expected */, ret);\n"
"}\n"
"UT_RUN(test_%s);\n",
                funcs[i].name, funcs[i].name, args, funcs[i].name);
        }
    }

    if (fresh) {
        fprintf(fo, "\nUT_MAIN_END();\n");
    }

    fclose(fo);
    free(old);
    free(code);
    return 0;
}

ut/ut.h(超軽量テストヘッダ:依存ゼロ)

// ut/ut.h
// 超軽量テスト用マクロ群(外部依存なし)

#ifndef UT_H
#define UT_H

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

static int __ut_tests = 0;
static int __ut_fail  = 0;

#define UT_MAIN_BEGIN() \
    int main(void) { \
        __ut_tests = 0; __ut_fail = 0;

#define UT_RUN(fn) do { \
    printf("RUN %s\n", #fn); \
    __ut_tests++; \
    fn(); \
} while(0)

#define UT_ASSERT_TRUE(cond) do { \
    if (!(cond)) { \
        __ut_fail++; \
        printf("  FAIL: %s:%d: %s\n", __FILE__, __LINE__, #cond); \
    } else { \
        printf("  OK  : %s\n", #cond); \
    } \
} while(0)

#define UT_ASSERT_EQ(exp, got) do { \
    if ((exp) != (got)) { \
        __ut_fail++; \
        printf("  FAIL: %s:%d: expected=%lld got=%lld\n", __FILE__, __LINE__, (long long)(exp), (long long)(got)); \
    } else { \
        printf("  OK  : %s == %s\n", #exp, #got); \
    } \
} while(0)

#define UT_MAIN_END() \
        printf(\"\\nSUMMARY: total=%d fail=%d\\n\", __ut_tests, __ut_fail); \
        return (__ut_fail == 0) ? 0 : 1; \
    }

#endif

(任意).vscode/c.code-snippets

{
  "UT: assert eq": {
    "prefix": "utEq",
    "body": [
      "UT_ASSERT_EQ(${1:expected}, ${2:actual});"
    ],
    "description": "UT_ASSERT_EQ ショートカット"
  }
}

使い方(開発・実行)

  1. utgenビルド

    • コマンドパレット → Tasks: Run Taskbuild:utgen
  2. テスト雛形生成

    • 任意の xxx.c をエディタで開く
    • gen:tests (current file) 実行 → tests/xxx_test.c が生成/追記
  3. ビルド

    • build:tests 実行 → build/xxx_test.out が生成されます
  4. 実行

    • run:tests 実行 → 各 _test.out を順に実行して結果表示
  5. デバッグ(任意)

    • gdb:tests 実行 → gdbで最後にビルドされたテストに付与

できること / 割り切り

  • ✅ 追加インストール完全ゼロ(配布だけ)

  • ✅ Cだけでテスト自動生成(拡張なし・外部ライブラリなし)

  • ✅ gcc/gdbだけでビルド/実行/デバッグ

  • ⚠️ 関数検出は簡易(複雑なマクロ・宣言分割・関数ポインタ宣言などは対象外)

    • カバーしきれない箇所は、生成後の tests/*.c を手修正する運用でOK

配布のしかた

  • 上記フォルダごとZipにして共有(社内ポータル等)
  • 受け取った人は 解凍するだけ(“インストール”は不要)
  • 以降は VS Code のタスク操作と gcc/gdb だけで回ります
1
0
0

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
1
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?