Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 57.426 phút đọc

Độ phủ và tích hợp liên tục

Sau bài này bạn sẽ làm được

  • Đo độ phủ dòng và độ phủ nhánh
  • Đọc báo cáo lcov và tìm chỗ chưa kiểm thử
  • Biết vì sao độ phủ cao không đồng nghĩa kiểm thử tốt
  • Viết cấu hình tích hợp liên tục cho một dự án C

Độ phủ trả lời đúng một câu hỏi: dòng nào chưa bao giờ chạy. Đó là một câu hỏi hữu ích. Nhưng nó không trả lời câu hỏi bạn thật sự quan tâm, và bài này chứng minh điều đó bằng một bộ kiểm thử đạt một trăm phần trăm trên mã có lỗi.

#gcov

bien.c, mã cần đo
#include "bien.h"
#include <string.h>

size_t chep_an_toan(char *dich, size_t dich_co, const char *nguon) {
    if (dich == NULL || nguon == NULL || dich_co == 0) return (size_t)-1;
    size_t n = strlen(nguon);
    if (n > dich_co - 1) n = dich_co - 1;
    memcpy(dich, nguon, n);
    dich[n] = '\0';
    return n;
}

char xep_loai(int diem) {
    if (diem < 0 || diem > 100) return '?';
    if (diem >= 90) return 'A';
    if (diem >= 80) return 'B';
    if (diem >= 70) return 'C';
    if (diem >= 60) return 'D';
    return 'F';
}
kt_it.c, bộ kiểm thử yếu
#include "kt.h"
#include "bien.h"
KT_DINH_NGHIA();

int main(void) {
    KT_NHOM("bo kiem thu yeu");
    KT_BANG_INT(xep_loai(95), 'A');
    char d[8];
    KT_BANG_INT(chep_an_toan(d, sizeof d, "abc"), 3);
    return KT_TONG_KET();
}
terminal
# --coverage bat ca -fprofile-arcs va -ftest-coverage
gcc -std=c11 -D__USE_MINGW_ANSI_STDIO=1 -Wall -Wextra -g --coverage kt_it.c bien.c -o kt_it.exe
# Chay: sinh ra cac tep .gcda ben canh .gcno
./kt_it.exe > /dev/null
gcov -b bien.c
File 'bien.c'
Lines executed:71.43% of 14
Branches executed:70.00% of 20
Taken at least once:35.00% of 20
No calls
Creating 'bien.c.gcov'

#Đọc báo cáo

bien.c.gcov, với bộ kiểm thử yếu
        -:    0:Source:bien.c
        -:    0:Graph:bien.gcno
        -:    0:Data:bien.gcda
        -:    0:Runs:1
        -:    1:#include "bien.h"
        -:    2:#include <string.h>
        -:    3:
function chep_an_toan called 1 returned 100% blocks executed 75%
        1:    4:size_t chep_an_toan(char *dich, size_t dich_co, const char *nguon) {
       1*:    5:    if (dich == NULL || nguon == NULL || dich_co == 0) return (size_t)-1;
branch  0 taken 100% (fallthrough)
branch  1 taken 0%
branch  2 taken 100% (fallthrough)
branch  3 taken 0%
branch  4 taken 0% (fallthrough)
branch  5 taken 100%
        1:    6:    size_t n = strlen(nguon);
       1*:    7:    if (n > dich_co - 1) n = dich_co - 1;
branch  0 taken 0% (fallthrough)
branch  1 taken 100%
        1:    8:    memcpy(dich, nguon, n);
        1:    9:    dich[n] = '\0';
        1:   10:    return n;
        -:   11:}
        -:   12:
function xep_loai called 1 returned 100% blocks executed 38%
        1:   13:char xep_loai(int diem) {
       1*:   14:    if (diem < 0 || diem > 100) return '?';
branch  0 taken 100% (fallthrough)
branch  1 taken 0%
branch  2 taken 0% (fallthrough)
branch  3 taken 100%
        1:   15:    if (diem >= 90) return 'A';
branch  0 taken 100% (fallthrough)
branch  1 taken 0%
    #####:   16:    if (diem >= 80) return 'B';
branch  0 never executed
branch  1 never executed
    #####:   17:    if (diem >= 70) return 'C';
branch  0 never executed
branch  1 never executed
    #####:   18:    if (diem >= 60) return 'D';
branch  0 never executed
branch  1 never executed
Ký hiệu ở cột đầuNghĩa
-Dòng không sinh mã: khai báo, bình luận, dấu ngoặc
1Dòng chạy đúng một lần
1*Dòng chạy một lần, nhưng CÓ nhánh trong dòng chưa chạy hết
#####Dòng KHÔNG BAO GIỜ chạy. Đây là thứ bạn tìm
=====Dòng không chạy, và nằm trong hàm không bao giờ được gọi

#Độ phủ nhánh

Độ phủ dòng và độ phủ nhánh
Độ phủ dòng đếm xem bao nhiêu dòng đã chạy. Độ phủ nhánh đếm xem với mỗi điều kiện, cả hai kết quả đúng và sai đã xảy ra chưa. Độ phủ nhánh luôn khắt khe hơn, và là con số bạn nên theo dõi.
/* Vi sao do phu dong khong du: */

if (p != NULL) {
    dung(p);
}

/* Mot bai kiem thu voi p khac NULL:
     do phu DONG   = 100%   (ca hai dong deu chay)
     do phu NHANH  = 50%    (nhanh p == NULL chua chay)

   Va neu ma dung la: */

if (p == NULL) return -1;
dung(p);

/* Mot bai kiem thu voi p khac NULL:
     do phu DONG   = 50%    (dong return chua chay)
     do phu NHANH  = 50%

   Cung mot thieu sot, nhung do phu dong bao 100% trong truong hop
   thu nhat va 50% trong truong hop thu hai. No phu thuoc vao
   CACH VIET chu khong phai vao chat luong kiem thu. */

Bây giờ chạy lại với bộ kiểm thử đầy đủ từ Bài 57.2.

terminal
rm -f *.gcda *.gcno *.gcov
gcc -std=c11 -D__USE_MINGW_ANSI_STDIO=1 -Wall -Wextra -g --coverage kt_bien.c bien.c -o kt_du.exe
./kt_du.exe > /dev/null
gcov -b bien.c
File 'bien.c'
Lines executed:100.00% of 14
Branches executed:100.00% of 20
Taken at least once:100.00% of 20
No calls
Creating 'bien.c.gcov'
grep -n "#####\|never executed\|taken 0%" bien.c.gcov

#Khi độ phủ nói dối

kt_rong.c, bộ kiểm thử không khẳng định gì
/* Bo kiem thu dat 100% do phu DONG nhung khong khang dinh gi that. */
#include "kt.h"
#include "bien.h"
KT_DINH_NGHIA();

int main(void) {
    KT_NHOM("goi het moi dong, khong kiem chung ket qua");
    for (int d = -1; d <= 101; ++d) (void)xep_loai(d);
    char b[8];
    (void)chep_an_toan(b, sizeof b, "abc");
    (void)chep_an_toan(b, sizeof b, "12345678");
    (void)chep_an_toan(NULL, 8, "x");
    (void)chep_an_toan(b, 0, "x");
    (void)chep_an_toan(b, 8, NULL);
    KT_DUNG(1);                     /* mot khang dinh vo nghia */
    return KT_TONG_KET();
}
terminal
# Chu y: dich voi bien_loi.c, ban CO HAI LOI lech mot
gcc -std=c11 -D__USE_MINGW_ANSI_STDIO=1 -Wall -Wextra -g --coverage kt_rong.c bien_loi.c -o kt_rong.exe
./kt_rong.exe

-- goi het moi dong, khong kiem chung ket qua
  ok   1

1 kiem tra, 0 hong
echo $?
0
gcov -b bien_loi.c
File 'bien_loi.c'
Lines executed:100.00% of 14
Branches executed:100.00% of 20
Taken at least once:95.00% of 20
No calls
Creating 'bien_loi.c.gcov'
Dùng độ phủ sai
# CI bat buoc mot con so tuyet doi - name: kiem tra do phu run: | ti_le=$(gcov -b src/*.c | grep 'Lines executed' | ...) if [ "$ti_le" -lt 90 ]; then exit 1; fi # Nguoi ta se viet kiem thu rong de vuot qua.
Dùng độ phủ đúng
# CI bao cao, va chan viec TUT LUI - name: do do phu run: make do-phu - name: so voi nhanh chinh run: | moi=$(cat do-phu.txt) cu=$(git show main:do-phu.txt 2>/dev/null || echo 0) echo "do phu: $cu -> $moi" awk "BEGIN{exit !($moi < $cu - 1)}" && { echo "do phu giam qua 1 diem"; exit 1; } # Va quan trong hon con so: dang bao cao HTML len, # de nguoi review NHIN duoc dong nao moi ma chua co kiem thu.

#lcov và báo cáo HTML

terminal
# Thu thap so lieu tu moi tep .gcda
lcov --capture --directory . --output-file phu.info
Capturing coverage data from .
Found 3 data files in .
Processing bien.gcda
Processing kt_bien.gcda
Finished .info-file creation
# Bo thu vien he thong va chinh bo kiem thu ra khoi bao cao
lcov --remove phu.info '/usr/*' '*/kiem-thu/*' --output-file phu.info
genhtml phu.info --output-directory bao-cao
Reading data file phu.info
Found 2 entries.
Writing .css and .png files.
Generating output.
Overall coverage rate:
  lines......: 100.0% (14 of 14 lines)
  functions..: 100.0% (2 of 2 functions)
start bao-cao/index.html

#Tích hợp liên tục

Makefile hoàn chỉnh
CC      := gcc
CFLAGS  := -std=c11 -Wall -Wextra -Wpedantic -g -Iinclude
SRC     := $(wildcard src/*.c)
KT_SRC  := $(wildcard kiem-thu/kt_*.c)
KT_BIN  := $(KT_SRC:.c=.exe)

.PHONY: tat-ca kiem-thu do-phu kiem-dinh-dang ve-sinh sach

tat-ca: kiem-dinh-dang kiem-thu do-phu ve-sinh

kiem-dinh-dang:
	clang-format --dry-run -Werror $(SRC) $(wildcard include/*.h)

kiem-thu: $(KT_BIN)
	@hong=0; for t in $(KT_BIN); do ./$$t || hong=1; done; exit $$hong

kiem-thu/%.exe: kiem-thu/%.c $(SRC)
	$(CC) $(CFLAGS) -Ikiem-thu $^ -o $@

do-phu:
	rm -f *.gcda *.gcno *.gcov
	$(CC) $(CFLAGS) -O0 --coverage -Ikiem-thu $(KT_SRC) $(SRC) -o kt-phu.exe
	./kt-phu.exe > /dev/null
	gcov -b $(SRC) | tee do-phu.txt

ve-sinh:
	$(CC) $(CFLAGS) -fsanitize=address,undefined -Ikiem-thu \
	      $(KT_SRC) $(SRC) -o kt-ve-sinh.exe
	./kt-ve-sinh.exe

sach:
	rm -f $(KT_BIN) kt-phu.exe kt-ve-sinh.exe *.gcda *.gcno *.gcov
.github/workflows/kiem-tra.yml
name: kiem tra
on: [push, pull_request]

jobs:
  dinh-dang:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: sudo apt-get update && sudo apt-get install -y clang-format
      - run: clang-format --dry-run -Werror $(git ls-files '*.c' '*.h')

  kiem-thu:
    strategy:
      fail-fast: false          # chay HET moi to hop du mot cai do
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        cc: [gcc, clang]
        exclude:
          - os: windows-latest
            cc: clang
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - run: make kiem-thu CC=${{ matrix.cc }}

  ve-sinh:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: make ve-sinh

  do-phu:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: sudo apt-get update && sudo apt-get install -y lcov
      - run: make do-phu
      - run: |
          lcov --capture --directory . --output-file phu.info
          lcov --remove phu.info '/usr/*' '*/kiem-thu/*' -o phu.info
          genhtml phu.info -o bao-cao
      - uses: actions/upload-artifact@v4
        with:
          name: bao-cao-do-phu
          path: bao-cao/

Tự làm thử

  1. Đo độ phủ dòng và nhánh cho một module trong dự án của bạn.
  2. Mở tệp .gcov và tìm mọi dòng có dấu ##### hoặc dấu sao.
  3. Viết bài kiểm thử cho ba nhánh chưa phủ và đo lại.
  4. Chạy kt_rong.c và giải thích cho một đồng nghiệp vì sao 100% độ phủ ở đó là vô nghĩa.
  5. Cài gcovr bằng pip và sinh báo cáo HTML cho dự án của bạn.
  6. Viết tệp cấu hình CI chạy đủ bốn bước, và cho nó chạy trên hai hệ điều hành.

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • --coverage -O0 -g, chạy chương trình, rồi gcov -b; xoá .gcda trước mỗi lần đo.
  • Con số quan trọng là Taken at least once, không phải Lines executed; dấu sao chỉ ra nhánh còn thiếu trong một dòng đã chạy.
  • Đã chứng minh: một bộ kiểm thử không khẳng định gì đạt 100% độ phủ trên mã có lỗi tràn bộ đệm và báo xanh.
  • Dùng độ phủ để tìm chỗ thiếu, không dùng làm chỉ tiêu; chặn tụt lùi thay vì đặt ngưỡng tuyệt đối.
  • CI cần ma trận nền tảng, fail-fast: false, thứ tự bước theo chi phí, và kỷ luật không hợp nhất khi đỏ.