← C Thực Chiến · Toolchain & Kiểm soát chất lượng

Bài 9 · Nâng cao · 30 phút· Cập nhật 11/06/2026

Capstone: dự án xuất xưởng

Biên soạn bởi Nguyễn Anh Tuấn

Áp trọn quy trình chất lượng lên một dự án thật: CMake + clang-format + clang-tidy + sanitizers + unit test + CI - tới khi cả 6 cổng đều xanh.

Suốt khoá này bạn học từng công cụ riêng lẻ: trình biên dịch, Make/CMake, clang-format, clang-tidy, sanitizers, gdb, test, CI. Bài cuối ghép tất cả lại trên một dự án thật - app Todo module-hoá từ khoá C nâng cao (task.h + store.c + main.c) - và lùa nó qua mọi cổng chất lượng cho tới khi tất cả xanh.

  • Đầu vào: một dự án C đa file có sẵn (không viết lại logic - đây là khoá kỹ thuật, không phải ngôn ngữ).
  • Đầu ra: cùng dự án đó nhưng “xuất xưởng được” - định dạng, sạch cảnh báo, test & sanitizer xanh, CI tự động.
  • Đây là thứ đặt vào portfolio: nhà tuyển dụng nhìn quy trình, không chỉ nhìn code.

Trước hết, sắp xếp như một dự án thật: tách header công khai, implementation, test, và đặt các file cấu hình công cụ ở gốc:

cấu trúc thư mục

du-an-xuat-xuong/
├── include/task.h            # interface cong khai (Model + Store opaque)
├── src/store.c               # implementation (mang dong) + storage
├── src/main.c                # CLI
├── tests/test_store.c        # unit test (assert) -> chay bang CTest
├── CMakeLists.txt            # build + bat test
├── .clang-format             # phong cach thong nhat
├── .clang-tidy               # phan tich tinh
└── .github/workflows/ci.yml  # chay het cac cong moi push

Liên hệ

Phần thiết kế bố cục này (opaque type, include/ vs src/) đã học ở khoá C nâng cao. Ở đây ta lo phần thi công: biến nó thành dự án build/test/CI được.

Một dự án “xuất xưởng được” phải qua sáu cổng, mỗi cổng bắt một lớp lỗi khác nhau. Bật thử từng cổng - để ý chỉ một cổng đỏ là chưa thể ship:

Bật từng cổng chất lượng đã đạt - chỉ tất cả xanh mới xuất xưởng:
Định dạng clang-format
clang-format --dry-run --Werror $(git ls-files '*.c' '*.h')
Mọi file đúng một phong cách thống nhất (thụt lề, đặt ngoặc, thứ tự include).
Đỏ khi: Có chỗ format khác cấu hình .clang-format. · Code dễ đọc, diff sạch, hết tranh cãi về style.
Cảnh báo = lỗi trình biên dịch
cmake -S . -B build -DCMAKE_C_FLAGS="-Werror" && cmake --build build
Biên dịch sạch với -Wall -Wextra -Werror, không một cảnh báo nào.
Đỏ khi: Biến không dùng, so sánh kiểu lệch, thiếu return, ép kiểu mất dữ liệu… · Phần lớn cảnh báo là một bug đang chờ phát tác.
Phân tích tĩnh clang-tidy
clang-tidy -p build $(git ls-files 'src/*.c')
Mẫu dễ lỗi & lỗi luồng: thiếu { }, null deref, rò rỉ, dùng-sau-free.
Đỏ khi: Phát hiện một mẫu nguy hiểm (vd câu lệnh không bọc { } kiểu “goto fail”). · Bắt bug mà trình biên dịch bỏ qua - không cần chạy chương trình.
Unit test CTest
ctest --test-dir build --output-on-failure
Mọi unit test đều xanh (các assert đúng).
Đỏ khi: Một assert sai → test đỏ. · Sửa/đổi code mà không làm hỏng hành vi đã đúng (chống hồi quy).
Sanitizer ASan + UBSan
cmake -B build-san -DCMAKE_C_FLAGS="-fsanitize=address,undefined" && ctest --test-dir build-san
Chạy test dưới ASan/UBSan: không lỗi bộ nhớ hay hành vi không xác định.
Đỏ khi: Tràn bộ đệm, dùng-sau-free, rò rỉ, tràn số có dấu… · Lộ bug ẩn lúc CHẠY mà bản build thường im lặng bỏ qua.
CI tự động GitHub Actions
.github/workflows/ci.yml
Năm cổng trên chạy lại tự động trên mỗi push / pull request.
Đỏ khi: Bất kỳ cổng nào đỏ → CI đỏ, chặn merge. · Không ai quên chạy; code lỗi không lọt vào nhánh chính.
0/6 cổng đạt - chưa thể ship. Chỉ một cổng đỏ là chặn: chất lượng là điều kiện AND, không phải điểm trung bình.
  • Chất lượng là điều kiện AND: ship = TẤT CẢ cổng xanh, không phải điểm trung bình.
  • Mỗi cổng một lớp lỗi: format · cảnh báo · phân tích tĩnh · test · sanitizer · CI.
  • CI buộc cả sáu chạy tự động mỗi push - không ai “quên” kiểm.

Một CMakeLists.txt gói lõi thành thư viện todocore, dựng chương trình chính, và đăng ký test cho ctest:

CMakeLists.txt

cmake_minimum_required(VERSION 3.16)
project(todo C)

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)  # cho clang-tidy doc
add_compile_options(-Wall -Wextra)

# Loi tai dung: module store. include/ la PUBLIC.
add_library(todocore src/store.c)
target_include_directories(todocore PUBLIC include)

add_executable(todo src/main.c)
target_link_libraries(todo PRIVATE todocore)

enable_testing()                       # bat ctest
add_executable(test_store tests/test_store.c)
target_link_libraries(test_store PRIVATE todocore)
add_test(NAME store COMMAND test_store)

tests/test_store.c - unit test bằng assert

#include "task.h"
#include <assert.h>
#include <stdio.h>

int main(void) {
    TaskStore *s = store_create();
    assert(s != NULL);
    assert(store_count(s) == 0);

    int id1 = store_add(s, "viec A");
    assert(store_count(s) == 1);
    assert(store_done(s, id1) == true);
    assert(store_done(s, 999) == false);   /* id khong ton tai */
    assert(store_at(s, 9) == NULL);        /* ngoai pham vi    */

    store_destroy(s);
    printf("OK: tat ca assert deu dung\n");
    return 0;
}

Chạy lần lượt sáu cổng. Cổng nào đỏ thì sửa rồi chạy lại, tới khi xanh hết:

chạy các cổng trên máy

# Cong 1 - dinh dang
clang-format --dry-run --Werror $(git ls-files '*.c' '*.h')

# Cong 2 - build voi canh bao = loi
cmake -S . -B build -DCMAKE_C_FLAGS="-Werror" && cmake --build build

# Cong 3 - phan tich tinh
clang-tidy -p build $(git ls-files 'src/*.c')

# Cong 4 - test
ctest --test-dir build --output-on-failure

# Cong 5 - test duoi ASan + UBSan
cmake -S . -B build-san -DCMAKE_C_FLAGS="-fsanitize=address,undefined -g"
cmake --build build-san && ctest --test-dir build-san --output-on-failure

Kết quả khi chạy

Test project .../build
    Start 1: store
1/1 Test #1: store ............   Passed    0.01 sec
100% tests passed, 0 tests failed out of 1

Cổng 6 gói năm cổng trên vào một workflow chạy tự động mỗi lần push:

.github/workflows/ci.yml (rút gọn)

name: CI
on: [push, pull_request]
jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - run: sudo apt-get update && sudo apt-get install -y cmake clang-format clang-tidy
      - run: clang-format --dry-run --Werror $(git ls-files '*.c' '*.h')
      - run: cmake -S . -B build -DCMAKE_C_FLAGS="-Werror -fsanitize=address,undefined -g" -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
      - run: cmake --build build
      - run: clang-tidy -p build $(git ls-files 'src/*.c')
      - run: ctest --test-dir build --output-on-failure
Chạy trên máy bạn - kỹ năng thật cần gõ lệnh thật

Tải dự án đầy đủ (include/ src/ tests/ + CMake + .clang-format + .clang-tidy + CI). Chạy đủ 6 cổng cho tới khi tất cả xanh, rồi đẩy lên GitHub để CI chạy.

$ cmake -S . -B build -DCMAKE_C_FLAGS="-Werror"
$ cmake --build build
$ ctest --test-dir build --output-on-failure
Tải mã nguồn (.zip) Xem trên GitHub bài lab: 09-du-an-xuat-xuong
Xem từng file (9) ↡

Hết khoá Toolchain 🎉 - bạn đã biết LÀM RA phần mềm

Bạn đã đi trọn: hành trình biên dịch → thư viện & liên kết → Make → CMake → clang-format → clang-tidy → sanitizers → gỡ lỗi/test/CI → và giờ ghép tất cả thành một dự án xuất xưởng được. Đây là khác biệt giữa “viết được code” và “giao được phần mềm”: một quy trình mà mỗi thay đổi đều được định dạng, phân tích, kiểm thử và giám sát tự động. Đi tiếp với các track C Thực Chiến còn lại (Hệ thống · Nhúng · Dữ liệu/Bảo mật) để xây những phần mềm lớn hơn.

Câu hỏi thường gặp

“Chạy được” mới chỉ là một tiêu chí - và là tiêu chí dễ nhất. Mỗi cổng bắt một lớp lỗi khác nhau: format giữ code đọc được; -Werror bắt cảnh báo; clang-tidy bắt mẫu nguy hiểm không cần chạy; test chống hồi quy; sanitizer lộ bug bộ nhớ lúc chạy; CI đảm bảo không ai quên chạy. Bỏ cổng nào là để lọt lớp lỗi đó.

Vì một lỗi dùng-sau-free hay một test đỏ là đủ làm hỏng phần mềm - không “bù” lại được bằng việc các cổng khác xanh. Nên điều kiện ship là TẤT CẢ cổng xanh; CI thường được cấu hình để chặn merge nếu bất kỳ cổng nào đỏ.

Năm 2014, một dòng goto fail; bị lặp hai lần trong code TLS của Apple, và vì câu lệnh if KHÔNG bọc { }, dòng thứ hai luôn chạy → bỏ qua kiểm tra chữ ký, một lỗ bảo mật nghiêm trọng. Kiểm tra readability-braces-around-statements của clang-tidy bắt buộc { } chính là để chặn lớp lỗi đó.

Một .clang-tidy tốt là cấu hình vừa đủ nghiêm cho đội của bạn, không phải bật tất cả. Lab tắt insecureAPI.DeprecatedOrUnsafeBufferHandling (đòi hàm _s của Annex K, không khả chuyển) và readability-magic-numbers (quá ồn cho kích thước buffer) - đây là lựa chọn phổ biến ở dự án thật, miễn là CÓ CHỦ ĐÍCH và ghi rõ.

Là “bản đồ” mỗi file .c được biên dịch bằng cờ gì - clang-tidy (và nhiều công cụ) đọc nó để phân tích đúng ngữ cảnh. Bật bằng set(CMAKE_EXPORT_COMPILE_COMMANDS ON); rồi clang-tidy -p build đọc từ thư mục build.

Test kiểm KẾT QUẢ đúng (assert giá trị mong đợi). Sanitizer kiểm CÁCH chạy có an toàn không (bộ nhớ, UB) - kể cả khi kết quả “tình cờ” đúng. Mạnh nhất là chạy CHÍNH các test của bạn DƯỚI sanitizer: vừa đúng kết quả, vừa sạch bộ nhớ.

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

Vì sao điều kiện “xuất xưởng” là AND (tất cả cổng xanh) chứ không phải điểm trung bình?

  1. 1

    Chạy đủ 6 cổng

    Tải lab, chạy lần lượt 6 cổng trên máy bạn cho tới khi tất cả xanh.

    Hoàn thành khi: format im lặng; build -Werror sạch; clang-tidy 0 cảnh báo; ctest 100% pass; bản ASan/UBSan cũng pass.

  2. 2

    Làm một cổng đỏ rồi chữa

    Cố tình thụt lề lệch một dòng trong src/store.c. Cổng nào bắt được? Sửa lại cho xanh.

    Hoàn thành khi: clang-format --dry-run --Werror báo lỗi đúng dòng; chạy clang-format -i sửa xong; cổng xanh lại.

  3. 3

    Bỏ một dấu ngoặc

    Xoá cặp { } quanh một câu lệnh if trong store.c rồi chạy clang-tidy. Nó nói gì?

    Hoàn thành khi: clang-tidy báo readability-braces-around-statements (lớp lỗi “goto fail”). Thêm lại { } để xanh.

  4. 4

    Thêm một test

    Trong tests/test_store.c, thêm assert kiểm store_remove một phần tử Ở GIỮA hoạt động đúng.

    Hoàn thành khi: ctest vẫn 100% pass; nếu logic sai, assert mới bắt được.

  5. 5

    Gài một rò rỉ

    Tạm bỏ free(s->items) trong store_destroy rồi chạy test dưới ASan. Báo gì? Khôi phục lại.

    Hoàn thành khi: ASan báo “detected memory leaks” kèm stack cấp phát. Trả lại free → sạch.

  6. 6

    Bật CI

    Đẩy dự án lên GitHub với .github/workflows/ci.yml. Mở tab Actions xem CI chạy.

    Hoàn thành khi: CI chạy 6 cổng và báo xanh; huy hiệu xanh trên repo - sẵn sàng khoe nhà tuyển dụng.