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/コピーで共有)。
セットアップ手順(配布時マニュアル)
- プロジェクト直下に上の構成を配置(
.vscode/
,tools/
,ut/
を含む)。 - VS Code でプロジェクトを開く。
-
初回だけ:タスク
build:utgen
を実行(utgen生成)。 - 任意の
.c
ファイルを開いた状態でタスクgen:tests (current file)
を実行 →tests/xxx_test.c
が生成。 - タスク
build:tests
→run: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 ショートカット"
}
}
使い方(開発・実行)
-
utgenビルド
- コマンドパレット →
Tasks: Run Task
→build:utgen
- コマンドパレット →
-
テスト雛形生成
- 任意の
xxx.c
をエディタで開く -
gen:tests (current file)
実行 →tests/xxx_test.c
が生成/追記
- 任意の
-
ビルド
-
build:tests
実行 →build/xxx_test.out
が生成されます
-
-
実行
-
run:tests
実行 → 各_test.out
を順に実行して結果表示
-
-
デバッグ(任意)
-
gdb:tests
実行 → gdbで最後にビルドされたテストに付与
-
できること / 割り切り
-
✅ 追加インストール完全ゼロ(配布だけ)
-
✅ Cだけでテスト自動生成(拡張なし・外部ライブラリなし)
-
✅ gcc/gdbだけでビルド/実行/デバッグ
-
⚠️ 関数検出は簡易(複雑なマクロ・宣言分割・関数ポインタ宣言などは対象外)
- カバーしきれない箇所は、生成後の
tests/*.c
を手修正する運用でOK
- カバーしきれない箇所は、生成後の
配布のしかた
- 上記フォルダごとZipにして共有(社内ポータル等)
- 受け取った人は 解凍するだけ(“インストール”は不要)
- 以降は VS Code のタスク操作と gcc/gdb だけで回ります