オプション沼(13) コードカバレッジを計測する準備、gcc ‐‐coverageオプション

Joseph Halfmoon

今回はソースコードのどこを何回実行したのだか測定できる、コードカバレッジ計測のためのgccのオプション、‐‐coverage を使ってみます。コードカバレッジ用のオプションは複数あるようなのですが、‐‐coverageオプションを一つ使えばこれ一個で良きにはからってくれるみたいっす。知らんけど。

※『オプション沼』投稿順indexはこちら

※動作確認に Windows11 WSL2上のUbuntu 20.04 LTS を使用しています。gccのバージョンは9.4.0です

カバレッジ

最初から腰を折るようなことを書いてしまうと、コードカバレッジは「そこを通過(実行した)」ことは確認してくれても、そこで処理した結果がプログラムの外に影響を与えたのか否かは教えてくれないです。通過しても結果が外へ出てこないのであれば意図せぬエラーがあっても分かりませぬ。よってカバレッジが100%であっても信用はおけないっと。逆に言えばカバレッジが100%でない、ということは通過していない(テストしていない)ところがあるということであります。ダメじゃん。

ま、とりあえず100%にするか、100%でないのならなぜそこを触らないのか明らかにしておくってもんかと。

今回のカバレッジ測定のターゲット

今回、カバレッジを観察するのは以下の別シリーズ記事でやったユニットテスト(Google-test利用)のコードです。

IoT何をいまさら(119) ESP32C3用のソースをWSL2上でGoogle-test

もとは、Arduino環境用の「スケッチ」であったものを C++のフツーのソース化した上で、Google-testでユニットテストかけているというもの。

カバレッジ測定はC++でも当然できるのですが、本シリーズは「C」範囲と決めているのでズレとります。また上記はCMake使ったGoogle-testなので「手作業で全オプションを出し入れ」している本シリーズとは違うっと。そこで被テスト関数ファイルはほぼほぼそのまま、テストフィクスチャとヘッダのみ細かい修正を施して「C」かつ「CMakeヌキ、Google-testヌキ」としてみました。

カバレッジの測定手順

手順をかいつまむと以下です。

    1. ‐‐coverage オプションつけてターゲットプログラムをビルド(‐は半角にしてくだされ)
    2. ビルドしたプログラムを実行
    3. カバレッジを測定したいソースファイル名を引数にしてgcovを実行

gnu gcovの使い方については以下のページにマニュアルがありです。

gcov—a Test Coverage Program

カバレッジの結果(例)

今回カバレッジ測定した結果ファイルが以下に。ソースファイルの各行の横(左端のフィールド)にそこを実行した回数が記されてます。

    -:    0:Source:undertest.c
    -:    0:Graph:undertest.gcno
    -:    0:Data:undertest.gcda
    -:    0:Runs:1
    -:    1:#include "undertest.h"
    -:    2:
    -:    3:extern char buf[];
    -:    4://--- Under test -------------------
    -:    5:
    -:    6:int currentIndex;
    -:    7:int lastIndex;
    -:    8:bool errFlag;
    -:    9:bool errNumber;
    -:   10:
   19:   11:bool nextCh() {
   23:   12:  while (isSpace(buf[currentIndex])) {
    4:   13:    currentIndex++;
    -:   14:  }
~~~途中略~~~
    3:   49:  if (! isdigit(buf[currentIndex])) { //first character must be 0-9
#####:   50:    errFlag = true;
#####:   51:    errNumber = 1;
#####:   52:    return 0;
    -:   53:  }
    3:   54:  sum = (uint32_t)(buf[currentIndex++] - '0');
~~~以下略~~~

上記の中で ##### となっているところが「触れてない」ところね。ダメじゃん、テスト漏れぞな。

ソース類

まずは被テスト関数を収めた undertest.c ファイルです。元はArduinoの.ino形式のソースから持ってきたもの。ArduinoはキホンC++なので bool とかCにはないキーワードが出現。

#include "undertest.h"

extern char buf[];
//--- Under test -------------------

int currentIndex;
int lastIndex;
bool errFlag;
bool errNumber;

bool nextCh() {
  while (isSpace(buf[currentIndex])) {
    currentIndex++;
  }
  if (currentIndex >= lastIndex) {
    return false;
  }
  return true;
}

char toUpper(char arg) {
  if ((arg >= 'a') && (arg <= 'z')) {
    return arg - 32;
  }
  return arg;
}

bool toHex(char arg, uint32_t *res) {
  char work = toUpper(arg);
  if ((work >= '0') && (work <= '9')) {
    *res = (uint32_t)(work - '0');
    return true;
  }
  if ((work >= 'A') && (work <= 'F')) {
    *res = (uint32_t)(work - 'A' + 10);
    return true;
  }
  return false;
}

uint32_t numberU32(char *buf) {
  uint32_t sum, temp;
  bool bitNotFlag = false;
  if (buf[currentIndex] == '~') {
    bitNotFlag = true;
    currentIndex++;
    nextCh();
  }
  if (! isdigit(buf[currentIndex])) { //first character must be 0-9
    errFlag = true;
    errNumber = 1;
    return 0;
  }
  sum = (uint32_t)(buf[currentIndex++] - '0');
  while (nextCh() && toHex(buf[currentIndex], &temp)) {
    sum = (sum << 4) + temp;
    currentIndex++;
  }
  return bitNotFlag ? ~sum : sum;
}

上記のファイルと標準的なCとの差分を埋めるためのヘッダファイル undertest.h が以下に。Arduino式のC++との差分だけでなく、ついでにGoogle-testのTEST関数の代替品(手抜きなやつ)も定義。

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

#define isSpace(arg)    isspace(arg)
typedef int bool;
#define true    (1)
#define false   (0)

#define  EXPECT_TRUE(fnarg)     if (fnarg) printf("OK\n"); else printf("NG\n")
#define  EXPECT_FALSE(fnarg)    if (fnarg) printf("NG\n"); else printf("OK\n")
#define  EXPECT_EQ(arg1, arg2)  if (arg1 == arg2) printf("OK\n"); else printf("NG\n")

extern int currentIndex;
extern int lastIndex;
extern bool errFlag;
extern bool errNumber;

bool nextCh();
char toUpper(char arg);
bool toHex(char arg, uint32_t *res);
uint32_t numberU32(char *buf)

Google-testのテストランナーのソースを若干修正してGoogle-testに依存しないように修正したものが以下に。これがmain.c

#include "undertest.h"

char buf[256];

void TEST_nextCh() {
    strcpy(buf, "  1234\t 567");
    currentIndex = 0;
    lastIndex = strlen(buf);
    errFlag = false;
    errNumber = 0;
    EXPECT_TRUE(nextCh());
    EXPECT_EQ(currentIndex, 2);
    currentIndex += 4;
    EXPECT_TRUE(nextCh());
    EXPECT_EQ(currentIndex, 8);
}

void TEST_toUpper() {
    EXPECT_EQ(toUpper('a'), 'A');
    EXPECT_EQ(toUpper('z'), 'Z');
    EXPECT_EQ(toUpper('1'), '1');
    EXPECT_EQ(toUpper('X'), 'X');
    EXPECT_EQ(toUpper('/'), '/');
}

void TEST_toHex() {
    uint32_t work;
    EXPECT_TRUE(toHex('a', &work));
    EXPECT_EQ(work, 10);
    EXPECT_TRUE(toHex('0', &work));
    EXPECT_EQ(work, 0);
    EXPECT_TRUE(toHex('9', &work));
    EXPECT_EQ(work, 9);
    EXPECT_TRUE(toHex('F', &work));
    EXPECT_EQ(work, 15);
    EXPECT_FALSE(toHex('.', &work));
    EXPECT_FALSE(toHex('G', &work));
    EXPECT_FALSE(toHex('x', &work));
    EXPECT_FALSE(toHex('%', &work));
}

void TEST_numberU32() {
    strcpy(buf, "1234");
    currentIndex = 0;
    lastIndex = strlen(buf);
    errFlag = false;
    errNumber = 0;
    EXPECT_EQ(numberU32(buf), 0x1234);
    EXPECT_EQ(currentIndex, 4);
    strcpy(buf, "0FF");
    currentIndex = 0;
    lastIndex = strlen(buf);
    errFlag = false;
    errNumber = 0;
    EXPECT_EQ(numberU32(buf), 0xFF);
    EXPECT_EQ(currentIndex, 3);
    strcpy(buf, "~0FFFFFF80");
    currentIndex = 0;
    lastIndex = strlen(buf);
    errFlag = false;
    errNumber = 0;
    EXPECT_EQ(numberU32(buf), 0x7F);
    EXPECT_EQ(currentIndex, 10);
}

int main(void) {
    TEST_nextCh();
    TEST_toUpper();
    TEST_toHex();
    TEST_numberU32();
    return 0;
}
ビルドして実行

上記のソースをビルドするときは以下で。

$ gcc --coverage main.c undertest.c

–coverageつけても何のスペクタクルもなく、a.outができるだけ。

実行したところが以下に。偽物のEXPECT_EQなどがOK, OK言ってきます。当然、Google-testが通っているのでNGは無いわな。coverageRun

さて、実行後、gcovを起動。ソースファイル undertest.c の横に実行回数を書き込んだ、undertest.c.gcovファイルを生成してもらいます。coverageGcov

赤線部分に注目。100%じゃないですな。先ほどの ##### で示されていた行デス。次回テスト追加しておかないと。

オプション沼(12)  外部からヘッダ・ファイルを与えられる -include オプション へ戻る

オプション沼(14) 最適化で以下省略なコード書いてみました。gcc -O3オプション へ進む