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ệ
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:
clang-format --dry-run --Werror $(git ls-files '*.c' '*.h') cmake -S . -B build -DCMAKE_C_FLAGS="-Werror" && cmake --build build clang-tidy -p build $(git ls-files 'src/*.c') ctest --test-dir build --output-on-failure cmake -B build-san -DCMAKE_C_FLAGS="-fsanitize=address,undefined" && ctest --test-dir build-san .github/workflows/ci.yml - ▸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 1Cổ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 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
Hết khoá Toolchain 🎉 - bạn đã biết LÀM RA phần mềm
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õ.
Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.
Trả lời vài câu để chắc rằng em đã nắm bài.
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
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
-Werrorsạch; clang-tidy 0 cảnh báo;ctest100% pass; bản ASan/UBSan cũng pass. - 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 --Werrorbáo lỗi đúng dòng; chạyclang-format -isửa xong; cổng xanh lại. - 3
Bỏ một dấu ngoặc
Xoá cặp
{ }quanh một câu lệnhiftrongstore.crồ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
Thêm một test
Trong
tests/test_store.c, thêmassertkiểmstore_removemột phần tử Ở GIỮA hoạt động đúng.Hoàn thành khi:
ctestvẫn 100% pass; nếu logic sai,assertmới bắt được. - 5
Gài một rò rỉ
Tạm bỏ
free(s->items)trongstore_destroyrồ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ạifree→ sạch. - 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.