Windows 10でC言語開発をしよう! テストコードを追加する
●
テストコードを考える
前回は、開発しているソフトウェアにテストコードを追加する例を紹介した。テストコードは自分の身を守ることになる大切なものだ。機能の追加やバグの修正によって、バグが生まれてしまうのはソフトウェア開発にはよくあることだが、テストコードがあればこうした問題を検出し、リリース前に修正できる可能性が高まるのだ。
前回は、Visual Studio Codeを使ってテストコードを追加する方法を取り上げた。テスト用のPowerShellスクリプトを作成し、make testで実行できるようにし、最後にVisual Studio Codeからテストを実行できるようにした。テストコードを取り込む方法としてはかなりシンプルなものだ。
しかし、実際にはテストコードが単一ということはほとんどない。テストコードはすべてのケースを網羅していることが理想的だ。やり方はいろいろあるが、前回のやり方を踏襲していくなら、テストごとにスクリプトを作成して追加することになる。どれだけ網羅的なテストコードを用意できるかが、どれだけ将来の自分も助けることになるかにつながっていく。
テストコードは網羅的に
テストコードは同じようなものをたくさん用意しても意味がない。できるだけすべてのケースを網羅するようにテストコードを追加していく。どこまで用意するかはケースバイケースだが、特に見逃しがちなケースを重点的にテストコードに落とし込んでおくとよいだろう。
csv2tsvの場合、変換前のCSVファイルと変換後のTSVファイルを用意しておけばテストコードを作成できる。まず、テストコードの元となるデータを用意する。最初はよくある一般的なCSVファイルだ。
zip.csv
13101,100,1000000,トウキョウト,チヨダク,イカニケイサイガナイバアイ
13101,102,1020072,トウキョウト,チヨダク,イイダバシ
13101,102,1020082,トウキョウト,チヨダク,イチバンチョウ
13101,101,1010032,トウキョウト,チヨダク,イワモトチョウ
13101,101,1010047,トウキョウト,チヨダク,ウチカンダ
13101,100,1000011,トウキョウト,チヨダク,ウチサイワイチョウ
13101,100,1000004,トウキョウト,チヨダク,オオテマチ(ツギノビルヲノゾク)
13101,100,1006890,トウキョウト,チヨダク,オオテマチジェイエイビル(チカイ・カイソウフメイ)
13101,100,1006801,トウキョウト,チヨダク,オオテマチジェイエイビル(1カイ)
13101,100,1006802,トウキョウト,チヨダク,オオテマチジェイエイビル(2カイ)
zip.tsv
13101 100 1000000 トウキョウト チヨダク イカニケイサイガナイバアイ
13101 102 1020072 トウキョウト チヨダク イイダバシ
13101 102 1020082 トウキョウト チヨダク イチバンチョウ
13101 101 1010032 トウキョウト チヨダク イワモトチョウ
13101 101 1010047 トウキョウト チヨダク ウチカンダ
13101 100 1000011 トウキョウト チヨダク ウチサイワイチョウ
13101 100 1000004 トウキョウト チヨダク オオテマチ(ツギノビルヲノゾク)
13101 100 1006890 トウキョウト チヨダク オオテマチジェイエイビル(チカイ・カイソウフメイ)
13101 100 1006801 トウキョウト チヨダク オオテマチジェイエイビル(1カイ)
13101 100 1006802 トウキョウト チヨダク オオテマチジェイエイビル(2カイ)
次に、問題となりそうなケースを用意していく。最初はダブルクォーテーションの処理だ。CSVファイルではダブルクォーテーションがクォートの対象となるため、その扱いに注意する必要がある。そういったケースをテストデータとして先に用意しておく。仕様に則って機能した場合の適切な出力をあらかじめ用意しておくのだ。
doublequotes.csv
13101,"1""0""0",,100 0000
"13101","""102""",,102 0072
13101,"1""02",,102 0082
13101,"101""""",, 10 10032
13101,"""""101",, 101 0047
,13101,@,"1""0""0",
,"13101","""102""",@
,13101,"1""02",
@,13101,"101""""",
,13101,"""""101",
doublequotes.tsv
13101 1"0"0 100 0000
13101 "102" 102 0072
13101 1"02 102 0082
13101 101"" 10 10032
13101 ""101 101 0047
13101 @ 1"0"0
13101 "102" @
13101 1"02
@ 13101 101""
13101 ""101
ファイルが空、つまりゼロバイトだった場合の処理もレアケースになる。そのときにも適切に動作してもらう必要があるので、次のように空のファイルを用意して動作確認を行うようにする。
empty.csv
empty.tsv
フィールドが空のケースや、フィールドの前後が空白である場合なども処理を間違えがちだ。この辺りもテストデータとして用意しよう。
spaces.csv
,,,100 0000
,,,102 0072
,,,102 0082
,,, 10 10032
,,, 101 0047
,,,,
,,,
,,,
,,,
,,,
spaces.tsv
100 0000
102 0072
102 0082
10 10032
101 0047
どこまでやるかは用途にも依るが、たとえばフィールド数が1000とか10000といった極端に多いケース、1行あたりのデータサイズがきわめて大きなケース、CSVデータが適切なフォーマットではなかったときの動作など、やろうと思えばどこまでもテストケースを用意することができる。どこまで用意するかはバランス感覚が要求されるところだが、仕様の整理とテストコードの開発を行ったり来たりしながら丁寧に作業してみよう。
●
複数のテストスクリプトと、それをまとめるスクリプト
用意したテストデータを使ってテストを行うPowerShellスクリプトを用意する。先程用意した4種類のデータは次のように4つのスクリプトで動作を確認できる。
test001.ps1
$n = '001'
$d = 'zip'
$f = New-TemporaryFile
.\csv2tsv.exe .\data\${d}.csv > $f
C:\Windows\System32\fc.exe .\data\${d}.tsv $f > $null
if ($?) {
echo "テスト${n}: 成功"
}
else {
echo "テスト${n}: 失敗"
}
Remove-Item $f
test002.ps1
$n = '002'
$d = 'doublequotes'
$f = New-TemporaryFile
.\csv2tsv.exe .\data\${d}.csv > $f
C:\Windows\System32\fc.exe .\data\${d}.tsv $f > $null
if ($?) {
echo "テスト${n}: 成功"
}
else {
echo "テスト${n}: 失敗"
}
Remove-Item $f
test003.ps1
$n = '003'
$d = 'spaces'
$f = New-TemporaryFile
.\csv2tsv.exe .\data\${d}.csv > $f
C:\Windows\System32\fc.exe .\data\${d}.tsv $f > $null
if ($?) {
echo "テスト${n}: 成功"
}
else {
echo "テスト${n}: 失敗"
}
Remove-Item $f
test004.ps1
$n = '004'
$d = 'empty'
$f = New-TemporaryFile
.\csv2tsv.exe .\data\${d}.csv > $f
C:\Windows\System32\fc.exe .\data\${d}.tsv $f > $null
if ($?) {
echo "テスト${n}: 成功"
}
else {
echo "テスト${n}: 失敗"
}
Remove-Item $f
さて、問題はここからだ。Makefileから直接この4つのテストスクリプトを呼び出す仕組みにしたとしよう。そうすると、例えばテストコードが100個など大量になってきたときに、Makefileに100行のテストコード呼び出し処理が記述されることになる。あまり見通しのよい書き方とは言えない。
そこで、ほかのテストスクリプトを呼び出してすべてのテストを実施するスクリプトを用意し、Makefileからはそのスクリプトのみを実行するといった仕組みにする。次のようなスクリプトを用意して、ほかのテストスクリプトを実行させる。
test.ps1
##########################################################################
# テスト数
##########################################################################
$TESTS = 4
##########################################################################
# テスト実施
##########################################################################
$res = $true
1..$TESTS |
% {
$n = $_.ToString("000")
Invoke-Expression ./tests/test${n}.ps1
if (-Not $?) {
$res = $false
}
}
##########################################################################
# テスト結果出力
##########################################################################
if ($res) {
echo '全テストをパス'
}
else {
echo 'テスト失敗'
exit -1
}
PowerShellスクリプト自体の説明はここでは行わないが、上記のスクリプトはtestsディレクトリ以下のテストスクリプトを順番に実行し、1つでも失敗がればこのスクリプトの終了コードも失敗として処理する内容になっている。
●
ファイルとディレクトリの配置構造
CSVファイルをTSVに変換するだけのコマンドを作っているわけだが、テストスクリプトやテストデータも含めるとファイルやディレクトリの配置が少々複雑になってくる。現在は次のような状態になっている。
csv2tsv - ファイルとディレクトリの配置構造
C:.
│ LICENSE
│ main.c
│ main.h
│ Makefile
│ util_csv.c
│ util_file.c
│
├───.vscode
│ launch.json
│ tasks.json
│
├───data
│ doublequotes.csv
│ doublequotes.tsv
│ empty.csv
│ empty.tsv
│ spaces.csv
│ spaces.tsv
│ zip.csv
│ zip.tsv
│
└───tests
test.ps1
test001.ps1
test002.ps1
test003.ps1
test004.ps1
Makefileへのマージ
今回の書き換えをMakefileにも反映させると次のようになる。
今回の開発を反映させたMakefile
CMD= csv2tsv.exe
SRCS= $(wildcard *.c)
OBJS= $(SRCS:.c=.o)
CC= clang
CFLAGS+=-g
build: $(CMD)
$(CMD): $(OBJS)
$(CC) $(CFLAGS) -o $(CMD) $(OBJS)
.c.o:
$(CC) -c $< -o $@
test: $(CMD)
pwsh .\tests\test.ps1
clean:
rm -f *.exe
rm -f *.o
rm -f *.ilk
rm -f *.pdb
「make test」と実行すると次のような結果を得ることができる。
テストコードの実行
PS C:\Users\daichi\Documents\vscode-csv2tsv> make test
clang -c main.c -o main.o
clang -c util_csv.c -o util_csv.o
clang -c util_file.c -o util_file.o
clang -g -o csv2tsv.exe main.o util_csv.o util_file.o
pwsh .\tests\test.ps1
テスト001: 成功
テスト002: 成功
テスト003: 成功
テスト004: 成功
全テストをパス
PS C:\Users\daichi\Documents\vscode-csv2tsv>
開発を進めつつ、時々テストコードを実行して互換性が維持できているかを確認する。この部分を充実させておくことで、将来、楽になれる。実際に使っていて問題が見つかった場合は、それもテストコードとして作成して追加することで、将来のエンバグを減らす可能性を高めることができる。
テストフレームワークへ
このようにテストコードが増えていくと、いくつもの共通点が出てきて、整理していくことでいずれフレームワークのような状態になっていく。自分でフレームワークとして整理してもよいし、そうなってきたら似たような既存のテストフレームワークを探して置き換えるのも手ではないかと思う。
最初からテスト系のフレームワークを使ってもよいのだが、シンプルなものであれば自分で用意すればよく、そのほうが場合によっては見通しがよく移植性も高くなる。フレームワークを自作してもよいだろう。いずれテストフレームワークも取り上げるつもりだ。
●
付録:csv2tsvソースコードほか
tasks.json
{
"version": "2.0.0",
"tasks": [
{
"label": "Clang",
"type": "process",
"command": "make",
"args": [
"build"
],
"problemMatcher": [],
"group": {
"kind": "build",
"isDefault": true
}
},
{
"label": "Test",
"type": "process",
"command": "make",
"args": [
"test"
],
"problemMatcher": [],
"group": {
"kind": "build",
"isDefault": true
}
}
]
}
Makefile
CMD= csv2tsv.exe
SRCS= $(wildcard *.c)
OBJS= $(SRCS:.c=.o)
CC= clang
CFLAGS+=-g
build: $(CMD)
$(CMD): $(OBJS)
$(CC) $(CFLAGS) -o $(CMD) $(OBJS)
.c.o:
$(CC) -c $< -o $@
test: $(CMD)
pwsh .\tests\test.ps1
clean:
rm -f *.exe
rm -f *.o
rm -f *.ilk
rm -f *.pdb
main.h
int csv2tsv(const char *, int, char *, int);
char *file2str(const char *)
main.c
#include
#include
#include
#include "main.h"
int main(int argc, char *argv[]) {
char *csvdata, *tsvdata;
int csvdata_bytes, tsvdata_bytes;
csvdata = file2str(argv[1]);
csvdata_bytes = strlen(csvdata);
tsvdata_bytes = csvdata_bytes;
tsvdata = calloc(tsvdata_bytes + 1, sizeof(char));
csv2tsv(csvdata, csvdata_bytes, tsvdata, tsvdata_bytes);
printf("%s", tsvdata);
return 0;
}
util_csv.c
#include
static bool record_outputed;
static char gettsvchar(const char);
int csv2tsv(const char *ibuf, int ibufsize, char *obuf, int obufsize) {
// When the target is empty, no processing is done.
if (0 == ibufsize)
return 0;
const char *p_i, *end_i;
char *p_o;
int tsv_len = 0;
p_i = ibuf;
end_i = &ibuf[ibufsize - 1];
p_o = obuf;
// Indicates the state during parsing.
typedef enum FIELD_STATUS {
FIELD_END,
IN_FIELD,
IN_QUOTED_FIELD
} record_status;
record_status rs = FIELD_END;
record_outputed = false;
while (1) {
if ('\n' == *p_i) {
if (!record_outputed) {
// nothing
}
rs = FIELD_END;
*p_o = gettsvchar('\n');
++p_o;
++tsv_len;
} else {
switch (rs) {
case FIELD_END:
if (',' == *p_i) {
// nothing
} else if ('"' == *p_i) {
rs = IN_QUOTED_FIELD;
} else {
rs = IN_FIELD;
*p_o = gettsvchar(*p_i);
++p_o;
++tsv_len;
}
break;
case IN_FIELD:
if (',' == *p_i) {
rs = FIELD_END;
} else {
*p_o = gettsvchar(*p_i);
++p_o;
++tsv_len;
}
break;
case IN_QUOTED_FIELD:
if ('"' == *p_i) {
if (p_i == end_i) {
rs = FIELD_END;
} else if (',' == *(p_i + 1)) {
rs = FIELD_END;
++p_i;
} else if ('"' == *(p_i + 1)) {
*p_o = gettsvchar(*p_i);
++p_o;
++tsv_len;
++p_i;
}
} else {
*p_o = gettsvchar(*p_i);
++p_o;
++tsv_len;
}
break;
}
switch (rs) {
case FIELD_END:
*p_o = '\t';
++p_o;
++tsv_len;
record_outputed = false;
break;
case IN_FIELD:
case IN_QUOTED_FIELD:
break;
}
}
if (p_i == end_i || tsv_len == obufsize)
break;
else
++p_i;
}
return tsv_len;
}
static char gettsvchar(const char c) {
record_outputed = true;
if ('\t' == c) {
return ' ';
} else {
return c;
}
}
util_file.c
#include
#include
#include
char *file2str(const char *filepath) {
struct stat st;
int filesize, c;
char *buf, *p;
FILE *fp;
stat(filepath, &st);
filesize = st.st_size;
buf = calloc(filesize + 1, sizeof(char));
p = buf;
fp = fopen(filepath, "r");
for (int i = 0; i < filesize; i++) {
c = fgetc(fp);
if (EOF == c) {
break;
}
*p = (char)c;
++p;
}
return buf;
}
○参考
RFC4180 - Common Format and MIME Type for Comma-Separated Values (CSV) Files
Definition of tab-separated-values (tsv), Internet Assigned Numbers Authority
GNU make
テストコードを考える
前回は、開発しているソフトウェアにテストコードを追加する例を紹介した。テストコードは自分の身を守ることになる大切なものだ。機能の追加やバグの修正によって、バグが生まれてしまうのはソフトウェア開発にはよくあることだが、テストコードがあればこうした問題を検出し、リリース前に修正できる可能性が高まるのだ。
前回は、Visual Studio Codeを使ってテストコードを追加する方法を取り上げた。テスト用のPowerShellスクリプトを作成し、make testで実行できるようにし、最後にVisual Studio Codeからテストを実行できるようにした。テストコードを取り込む方法としてはかなりシンプルなものだ。
テストコードは網羅的に
テストコードは同じようなものをたくさん用意しても意味がない。できるだけすべてのケースを網羅するようにテストコードを追加していく。どこまで用意するかはケースバイケースだが、特に見逃しがちなケースを重点的にテストコードに落とし込んでおくとよいだろう。
csv2tsvの場合、変換前のCSVファイルと変換後のTSVファイルを用意しておけばテストコードを作成できる。まず、テストコードの元となるデータを用意する。最初はよくある一般的なCSVファイルだ。
zip.csv
13101,100,1000000,トウキョウト,チヨダク,イカニケイサイガナイバアイ
13101,102,1020072,トウキョウト,チヨダク,イイダバシ
13101,102,1020082,トウキョウト,チヨダク,イチバンチョウ
13101,101,1010032,トウキョウト,チヨダク,イワモトチョウ
13101,101,1010047,トウキョウト,チヨダク,ウチカンダ
13101,100,1000011,トウキョウト,チヨダク,ウチサイワイチョウ
13101,100,1000004,トウキョウト,チヨダク,オオテマチ(ツギノビルヲノゾク)
13101,100,1006890,トウキョウト,チヨダク,オオテマチジェイエイビル(チカイ・カイソウフメイ)
13101,100,1006801,トウキョウト,チヨダク,オオテマチジェイエイビル(1カイ)
13101,100,1006802,トウキョウト,チヨダク,オオテマチジェイエイビル(2カイ)
zip.tsv
13101 100 1000000 トウキョウト チヨダク イカニケイサイガナイバアイ
13101 102 1020072 トウキョウト チヨダク イイダバシ
13101 102 1020082 トウキョウト チヨダク イチバンチョウ
13101 101 1010032 トウキョウト チヨダク イワモトチョウ
13101 101 1010047 トウキョウト チヨダク ウチカンダ
13101 100 1000011 トウキョウト チヨダク ウチサイワイチョウ
13101 100 1000004 トウキョウト チヨダク オオテマチ(ツギノビルヲノゾク)
13101 100 1006890 トウキョウト チヨダク オオテマチジェイエイビル(チカイ・カイソウフメイ)
13101 100 1006801 トウキョウト チヨダク オオテマチジェイエイビル(1カイ)
13101 100 1006802 トウキョウト チヨダク オオテマチジェイエイビル(2カイ)
次に、問題となりそうなケースを用意していく。最初はダブルクォーテーションの処理だ。CSVファイルではダブルクォーテーションがクォートの対象となるため、その扱いに注意する必要がある。そういったケースをテストデータとして先に用意しておく。仕様に則って機能した場合の適切な出力をあらかじめ用意しておくのだ。
doublequotes.csv
13101,"1""0""0",,100 0000
"13101","""102""",,102 0072
13101,"1""02",,102 0082
13101,"101""""",, 10 10032
13101,"""""101",, 101 0047
,13101,@,"1""0""0",
,"13101","""102""",@
,13101,"1""02",
@,13101,"101""""",
,13101,"""""101",
doublequotes.tsv
13101 1"0"0 100 0000
13101 "102" 102 0072
13101 1"02 102 0082
13101 101"" 10 10032
13101 ""101 101 0047
13101 @ 1"0"0
13101 "102" @
13101 1"02
@ 13101 101""
13101 ""101
ファイルが空、つまりゼロバイトだった場合の処理もレアケースになる。そのときにも適切に動作してもらう必要があるので、次のように空のファイルを用意して動作確認を行うようにする。
empty.csv
empty.tsv
フィールドが空のケースや、フィールドの前後が空白である場合なども処理を間違えがちだ。この辺りもテストデータとして用意しよう。
spaces.csv
,,,100 0000
,,,102 0072
,,,102 0082
,,, 10 10032
,,, 101 0047
,,,,
,,,
,,,
,,,
,,,
spaces.tsv
100 0000
102 0072
102 0082
10 10032
101 0047
どこまでやるかは用途にも依るが、たとえばフィールド数が1000とか10000といった極端に多いケース、1行あたりのデータサイズがきわめて大きなケース、CSVデータが適切なフォーマットではなかったときの動作など、やろうと思えばどこまでもテストケースを用意することができる。どこまで用意するかはバランス感覚が要求されるところだが、仕様の整理とテストコードの開発を行ったり来たりしながら丁寧に作業してみよう。
●
複数のテストスクリプトと、それをまとめるスクリプト
用意したテストデータを使ってテストを行うPowerShellスクリプトを用意する。先程用意した4種類のデータは次のように4つのスクリプトで動作を確認できる。
test001.ps1
$n = '001'
$d = 'zip'
$f = New-TemporaryFile
.\csv2tsv.exe .\data\${d}.csv > $f
C:\Windows\System32\fc.exe .\data\${d}.tsv $f > $null
if ($?) {
echo "テスト${n}: 成功"
}
else {
echo "テスト${n}: 失敗"
}
Remove-Item $f
test002.ps1
$n = '002'
$d = 'doublequotes'
$f = New-TemporaryFile
.\csv2tsv.exe .\data\${d}.csv > $f
C:\Windows\System32\fc.exe .\data\${d}.tsv $f > $null
if ($?) {
echo "テスト${n}: 成功"
}
else {
echo "テスト${n}: 失敗"
}
Remove-Item $f
test003.ps1
$n = '003'
$d = 'spaces'
$f = New-TemporaryFile
.\csv2tsv.exe .\data\${d}.csv > $f
C:\Windows\System32\fc.exe .\data\${d}.tsv $f > $null
if ($?) {
echo "テスト${n}: 成功"
}
else {
echo "テスト${n}: 失敗"
}
Remove-Item $f
test004.ps1
$n = '004'
$d = 'empty'
$f = New-TemporaryFile
.\csv2tsv.exe .\data\${d}.csv > $f
C:\Windows\System32\fc.exe .\data\${d}.tsv $f > $null
if ($?) {
echo "テスト${n}: 成功"
}
else {
echo "テスト${n}: 失敗"
}
Remove-Item $f
さて、問題はここからだ。Makefileから直接この4つのテストスクリプトを呼び出す仕組みにしたとしよう。そうすると、例えばテストコードが100個など大量になってきたときに、Makefileに100行のテストコード呼び出し処理が記述されることになる。あまり見通しのよい書き方とは言えない。
そこで、ほかのテストスクリプトを呼び出してすべてのテストを実施するスクリプトを用意し、Makefileからはそのスクリプトのみを実行するといった仕組みにする。次のようなスクリプトを用意して、ほかのテストスクリプトを実行させる。
test.ps1
##########################################################################
# テスト数
##########################################################################
$TESTS = 4
##########################################################################
# テスト実施
##########################################################################
$res = $true
1..$TESTS |
% {
$n = $_.ToString("000")
Invoke-Expression ./tests/test${n}.ps1
if (-Not $?) {
$res = $false
}
}
##########################################################################
# テスト結果出力
##########################################################################
if ($res) {
echo '全テストをパス'
}
else {
echo 'テスト失敗'
exit -1
}
PowerShellスクリプト自体の説明はここでは行わないが、上記のスクリプトはtestsディレクトリ以下のテストスクリプトを順番に実行し、1つでも失敗がればこのスクリプトの終了コードも失敗として処理する内容になっている。
●
ファイルとディレクトリの配置構造
CSVファイルをTSVに変換するだけのコマンドを作っているわけだが、テストスクリプトやテストデータも含めるとファイルやディレクトリの配置が少々複雑になってくる。現在は次のような状態になっている。
csv2tsv - ファイルとディレクトリの配置構造
C:.
│ LICENSE
│ main.c
│ main.h
│ Makefile
│ util_csv.c
│ util_file.c
│
├───.vscode
│ launch.json
│ tasks.json
│
├───data
│ doublequotes.csv
│ doublequotes.tsv
│ empty.csv
│ empty.tsv
│ spaces.csv
│ spaces.tsv
│ zip.csv
│ zip.tsv
│
└───tests
test.ps1
test001.ps1
test002.ps1
test003.ps1
test004.ps1
Makefileへのマージ
今回の書き換えをMakefileにも反映させると次のようになる。
今回の開発を反映させたMakefile
CMD= csv2tsv.exe
SRCS= $(wildcard *.c)
OBJS= $(SRCS:.c=.o)
CC= clang
CFLAGS+=-g
build: $(CMD)
$(CMD): $(OBJS)
$(CC) $(CFLAGS) -o $(CMD) $(OBJS)
.c.o:
$(CC) -c $< -o $@
test: $(CMD)
pwsh .\tests\test.ps1
clean:
rm -f *.exe
rm -f *.o
rm -f *.ilk
rm -f *.pdb
「make test」と実行すると次のような結果を得ることができる。
テストコードの実行
PS C:\Users\daichi\Documents\vscode-csv2tsv> make test
clang -c main.c -o main.o
clang -c util_csv.c -o util_csv.o
clang -c util_file.c -o util_file.o
clang -g -o csv2tsv.exe main.o util_csv.o util_file.o
pwsh .\tests\test.ps1
テスト001: 成功
テスト002: 成功
テスト003: 成功
テスト004: 成功
全テストをパス
PS C:\Users\daichi\Documents\vscode-csv2tsv>
開発を進めつつ、時々テストコードを実行して互換性が維持できているかを確認する。この部分を充実させておくことで、将来、楽になれる。実際に使っていて問題が見つかった場合は、それもテストコードとして作成して追加することで、将来のエンバグを減らす可能性を高めることができる。
テストフレームワークへ
このようにテストコードが増えていくと、いくつもの共通点が出てきて、整理していくことでいずれフレームワークのような状態になっていく。自分でフレームワークとして整理してもよいし、そうなってきたら似たような既存のテストフレームワークを探して置き換えるのも手ではないかと思う。
最初からテスト系のフレームワークを使ってもよいのだが、シンプルなものであれば自分で用意すればよく、そのほうが場合によっては見通しがよく移植性も高くなる。フレームワークを自作してもよいだろう。いずれテストフレームワークも取り上げるつもりだ。
●
付録:csv2tsvソースコードほか
tasks.json
{
"version": "2.0.0",
"tasks": [
{
"label": "Clang",
"type": "process",
"command": "make",
"args": [
"build"
],
"problemMatcher": [],
"group": {
"kind": "build",
"isDefault": true
}
},
{
"label": "Test",
"type": "process",
"command": "make",
"args": [
"test"
],
"problemMatcher": [],
"group": {
"kind": "build",
"isDefault": true
}
}
]
}
Makefile
CMD= csv2tsv.exe
SRCS= $(wildcard *.c)
OBJS= $(SRCS:.c=.o)
CC= clang
CFLAGS+=-g
build: $(CMD)
$(CMD): $(OBJS)
$(CC) $(CFLAGS) -o $(CMD) $(OBJS)
.c.o:
$(CC) -c $< -o $@
test: $(CMD)
pwsh .\tests\test.ps1
clean:
rm -f *.exe
rm -f *.o
rm -f *.ilk
rm -f *.pdb
main.h
int csv2tsv(const char *, int, char *, int);
char *file2str(const char *)
main.c
#include
#include
#include
#include "main.h"
int main(int argc, char *argv[]) {
char *csvdata, *tsvdata;
int csvdata_bytes, tsvdata_bytes;
csvdata = file2str(argv[1]);
csvdata_bytes = strlen(csvdata);
tsvdata_bytes = csvdata_bytes;
tsvdata = calloc(tsvdata_bytes + 1, sizeof(char));
csv2tsv(csvdata, csvdata_bytes, tsvdata, tsvdata_bytes);
printf("%s", tsvdata);
return 0;
}
util_csv.c
#include
static bool record_outputed;
static char gettsvchar(const char);
int csv2tsv(const char *ibuf, int ibufsize, char *obuf, int obufsize) {
// When the target is empty, no processing is done.
if (0 == ibufsize)
return 0;
const char *p_i, *end_i;
char *p_o;
int tsv_len = 0;
p_i = ibuf;
end_i = &ibuf[ibufsize - 1];
p_o = obuf;
// Indicates the state during parsing.
typedef enum FIELD_STATUS {
FIELD_END,
IN_FIELD,
IN_QUOTED_FIELD
} record_status;
record_status rs = FIELD_END;
record_outputed = false;
while (1) {
if ('\n' == *p_i) {
if (!record_outputed) {
// nothing
}
rs = FIELD_END;
*p_o = gettsvchar('\n');
++p_o;
++tsv_len;
} else {
switch (rs) {
case FIELD_END:
if (',' == *p_i) {
// nothing
} else if ('"' == *p_i) {
rs = IN_QUOTED_FIELD;
} else {
rs = IN_FIELD;
*p_o = gettsvchar(*p_i);
++p_o;
++tsv_len;
}
break;
case IN_FIELD:
if (',' == *p_i) {
rs = FIELD_END;
} else {
*p_o = gettsvchar(*p_i);
++p_o;
++tsv_len;
}
break;
case IN_QUOTED_FIELD:
if ('"' == *p_i) {
if (p_i == end_i) {
rs = FIELD_END;
} else if (',' == *(p_i + 1)) {
rs = FIELD_END;
++p_i;
} else if ('"' == *(p_i + 1)) {
*p_o = gettsvchar(*p_i);
++p_o;
++tsv_len;
++p_i;
}
} else {
*p_o = gettsvchar(*p_i);
++p_o;
++tsv_len;
}
break;
}
switch (rs) {
case FIELD_END:
*p_o = '\t';
++p_o;
++tsv_len;
record_outputed = false;
break;
case IN_FIELD:
case IN_QUOTED_FIELD:
break;
}
}
if (p_i == end_i || tsv_len == obufsize)
break;
else
++p_i;
}
return tsv_len;
}
static char gettsvchar(const char c) {
record_outputed = true;
if ('\t' == c) {
return ' ';
} else {
return c;
}
}
util_file.c
#include
#include
#include
char *file2str(const char *filepath) {
struct stat st;
int filesize, c;
char *buf, *p;
FILE *fp;
stat(filepath, &st);
filesize = st.st_size;
buf = calloc(filesize + 1, sizeof(char));
p = buf;
fp = fopen(filepath, "r");
for (int i = 0; i < filesize; i++) {
c = fgetc(fp);
if (EOF == c) {
break;
}
*p = (char)c;
++p;
}
return buf;
}
○参考
RFC4180 - Common Format and MIME Type for Comma-Separated Values (CSV) Files
Definition of tab-separated-values (tsv), Internet Assigned Numbers Authority
GNU make