- SQLite รักษาความน่าเชื่อถือและความทนทานในระดับสูงด้วย ระบบทดสอบอัตโนมัติที่เข้มงวด โดยมี โค้ดทดสอบมากกว่าโค้ดจริง 590 เท่า
- มี ชุดทดสอบอิสระ (test harness) 4 แบบ ได้แก่ TCL, TH3, SQL Logic Test และ dbsqlfuzz ที่ใช้ตรวจสอบไลบรารีแกนหลัก พร้อมรัน การทดสอบหลายร้อยล้านครั้ง
- ใช้ การทดสอบสถานการณ์ผิดปกติ (OOM, ข้อผิดพลาด I/O, การจำลองแครช) และ fuzz testing เพื่อตรวจสอบว่ายังทำงานได้เสถียรแม้เจออินพุตผิดปกติหรือระบบขัดข้อง
- คงไว้ซึ่งกระบวนการตรวจสอบหลายชั้น เช่น branch coverage และ MC/DC 100%, การตรวจจับ resource leak, Valgrind, static analysis และ checklist
- ด้วยระบบทดสอบที่เป็นระบบเช่นนี้ SQLite จึงได้รับการประเมินว่าเป็นฐานข้อมูลโอเพนซอร์สที่มี ความน่าเชื่อถือและคุณภาพในระดับฐานข้อมูลเชิงพาณิชย์
1. ภาพรวม
- ความน่าเชื่อถือและความทนทาน ของ SQLite มาจากกระบวนการทดสอบที่ละเอียดถี่ถ้วน
- ณ เวอร์ชัน 3.42.0 SQLite ประกอบด้วยโค้ด C ประมาณ 155.8 KSLOC และโค้ดทดสอบ 92053.1 KSLOC
- ระบบทดสอบประกอบด้วย harness อิสระ 4 ชุด, branch coverage 100% และ กรณีทดสอบหลายล้านรายการ
- ครอบคลุมหัวข้อต่าง ๆ เช่น OOM, ข้อผิดพลาด I/O, การแครช, fuzz, ค่าขอบเขต, regression, ไฟล์ DB ที่ผิดปกติ, การทดสอบแบบปิด optimization เป็นต้น
2. Test Harness
- TCL Tests
- เป็นชุดทดสอบสาธารณะที่ใช้หลัก ๆ ระหว่างการพัฒนา SQLite
- ประกอบด้วยโค้ด C 27.2 KSLOC และไฟล์สคริปต์ 1,390 ไฟล์ (23.2MB)
- มี กรณีทดสอบมากกว่า 50,000 รายการ และเมื่อรันทั้งหมดแบบ parameterized จะมีการทดสอบหลายล้านครั้ง
- TH3
- เป็นชุดทดสอบเชิงพาณิชย์ที่พัฒนาด้วย C และทำให้บรรลุ branch coverage และ MC/DC 100%
- ทำงานได้แม้ในสภาพแวดล้อมแบบ embedded และมี 1055.4 KSLOC พร้อมเคสมากกว่า 50,000 รายการ
- เมื่อรันการทดสอบ coverage แบบเต็มจะมีประมาณ 2.4 ล้านครั้ง และก่อนออกรีลีสจะทำ soak test 248 ล้านครั้ง
- SQL Logic Test (SLT)
- เปรียบเทียบผลลัพธ์ของ SQLite กับ PostgreSQL, MySQL, SQL Server และ Oracle 10g
- ประกอบด้วย 7.2 ล้านคิวรี และข้อมูลขนาด 1.12GB
- dbsqlfuzz
- เป็น fuzzer ที่ใช้ libFuzzer ซึ่งกลายพันธุ์ทั้ง SQL และไฟล์ฐานข้อมูลพร้อมกัน
- รัน การทดสอบกลายพันธุ์ราว 1 พันล้านครั้งต่อวัน เพื่อตรวจสอบความทนทานต่ออินพุตที่เป็นอันตราย
- เครื่องมือเพิ่มเติม
- speedtest1.c, mptester.c, threadtest3.c, fuzzershell.c, jfuzz เป็นต้น
- ทุกการทดสอบต้องผ่านบน หลายแพลตฟอร์มและหลายการตั้งค่าคอมไพล์ จึงจะออกรีลีสได้
3. การทดสอบสถานการณ์ผิดปกติ
- การทดสอบ OOM
- จำลองการล้มเหลวของ
malloc() เพื่อตรวจสอบว่าสามารถกู้คืนได้ถูกต้องเมื่อหน่วยความจำไม่พอ
- รันซ้ำโดยเพิ่มตัวนับตำแหน่งที่ทำให้ล้มเหลวไปเรื่อย ๆ
- การทดสอบข้อผิดพลาด I/O
- ใช้ virtual file system (VFS) เพื่อจำลองข้อผิดพลาดของดิสก์
- หลังเกิดข้อผิดพลาดจะใช้
PRAGMA integrity_check ตรวจสอบว่าข้อมูลเสียหายหรือไม่
- การทดสอบการแครช
- จำลองสถานการณ์ไฟดับหรือ OS แครช
- TCL harness ใช้ child process เป็นฐาน ส่วน TH3 ใช้ VFS แบบอิงหน่วยความจำ
- ตรวจสอบว่าธุรกรรมถูก rollback ทั้งหมดหรือเสร็จสมบูรณ์ทั้งหมด
- การทดสอบความล้มเหลวแบบผสม
- ตรวจสอบไปถึงกรณีที่หลังแครชแล้วเกิด OOM หรือข้อผิดพลาด I/O ต่อเนื่องกัน
4. Fuzz Testing
- SQL Fuzz
- สร้าง SQL ที่ถูกต้องตามไวยากรณ์แต่ผิดปกติ เพื่อตรวจสอบการตอบสนองของ SQLite
- American Fuzzy Lop (AFL)
- เป็น fuzzer แบบอาศัยโปรไฟล์ที่นำมาใช้ในปี 2014 เพื่อสำรวจเส้นทางควบคุมใหม่ ๆ
- พบ assert failure, การแครช และผลลัพธ์ที่ผิดพลาดของ SQLite ได้จำนวนมาก
- Google OSS Fuzz
- ตั้งแต่ปี 2016 มีการทำ fuzzing อัตโนมัติบนโครงสร้างพื้นฐานของ Google
- ใช้ตรวจจับปัญหาที่เกิดเป็นครั้งคราวใน commit ใหม่
- dbsqlfuzz / jfuzz
- ถูกนำมาใช้เป็น fuzzer ภายในตั้งแต่ปี 2018 โดยกลายพันธุ์ทั้ง SQL และไฟล์ DB พร้อมกัน
- ทดสอบมากกว่า 500 ล้านครั้งต่อวัน จนรายงานบั๊กจาก fuzzer ภายนอกแทบหายไป
- ตั้งแต่ปี 2024 เป็นต้นมา jfuzz ได้เพิ่มการตรวจสอบอินพุต JSONB
- fuzzer ของบุคคลที่สามและ fuzzcheck
- นักวิจัยภายนอก (เช่น Manuel Rigger) พบกรณีการคำนวณผลลัพธ์ผิดจำนวนมาก
- ยูทิลิตี fuzzcheck ใช้ตรวจสอบซ้ำเคส fuzz ในอดีตที่ “น่าสนใจ” หลายพันกรณี
- ความตึงเครียดระหว่าง MC/DC กับ fuzz testing
- MC/DC มุ่งลดโค้ดเชิงป้องกันให้น้อยที่สุด ขณะที่ fuzz ต้องการโค้ดเชิงป้องกัน
- SQLite ใช้ทั้งสองแนวทางควบคู่กันเพื่อคงไว้ซึ่ง โค้ดที่ทนทานทั้งต่ออินพุตปกติและอินพุตประสงค์ร้าย
5. Regression Testing
- บั๊กที่มีการรายงานเข้ามา เมื่อแก้แล้วจะต้องเพิ่มเป็น กรณีทดสอบใหม่ เสมอ
- เพื่อป้องกันไม่ให้บั๊กเดิมกลับมาอีก
6. การตรวจจับ Resource Leak อัตโนมัติ
- harness ของ TCL และ TH3 เฝ้าตรวจ memory leak, file leak, thread leak และ mutex leak แบบอัตโนมัติ
- แม้หลัง OOM หรือข้อผิดพลาด I/O ก็ต้องไม่มี memory leak
7. Test Coverage
- SQLite core บรรลุ branch coverage 100% ตามเกณฑ์ของ TH3
- ไม่รวมส่วนขยายอย่าง FTS3 และ RTree
- Statement vs Branch Coverage
- branch coverage เข้มงวดกว่า statement coverage และตรวจสอบทุกเงื่อนไขแตกแขนงทั้งสองทาง
- การครอบคลุมโค้ดเชิงป้องกัน
- ใช้แมโคร ALWAYS() และ NEVER() เพื่อระบุเงื่อนไขเชิงป้องกันอย่างชัดเจน
- ทดสอบซ้ำด้วยนิยาม 3 รูปแบบเพื่อยืนยันความสอดคล้อง
- การทดสอบค่าขอบเขตและ boolean vector
- ใช้แมโคร testcase() เพื่อตรวจสอบทั้งผลลัพธ์จริงและเท็จของเงื่อนไข
- มีการใช้ testcase() จำนวน 1184 จุด
- การบรรลุ MC/DC
- ใช้แมโคร testcase() เพื่อตรวจสอบผลกระทบอย่างเป็นอิสระของทุกเงื่อนไข
- การวัดผลด้วย gcov
- วัด coverage ด้วยออปชัน
-fprofile-arcs -ftest-coverage
- ใช้การเปรียบเทียบผลลัพธ์เพื่อตรวจจับ compiler bug หรือ undefined behavior
- Mutation Testing
- เปลี่ยนคำสั่ง branch เพื่อดูว่าชุดทดสอบสามารถตรวจจับได้หรือไม่
- branch สำหรับ optimization (
/*OPTIMIZATION-IF-TRUE*/) ถูกจัดเป็นข้อยกเว้น
- ประสบการณ์จากการมี coverage ครบถ้วน
- การทดสอบทุก branch ช่วยลดผลข้างเคียงเมื่อมีการแก้โค้ด
- แม้ต้นทุนการดูแลสูง แต่ถือว่าสมเหตุสมผลสำหรับไลบรารีโครงสร้างพื้นฐานที่ถูกใช้งานอย่างกว้างขวาง
8. Dynamic Analysis
- Assert()
- มีคำสั่ง assert 6754 จุดสำหรับตรวจสอบ precondition, postcondition และ loop invariant
- เปิดใช้งานเฉพาะในบิลด์
SQLITE_DEBUG
- Valgrind
- ตรวจจับข้อผิดพลาดหน่วยความจำ, stack overflow และการเข้าถึงหน่วยความจำที่ยังไม่ได้กำหนดค่าเริ่มต้น
- ก่อนออกรีลีสจะรัน veryquick และ TH3 tests ผ่าน Valgrind
- Memsys2
- ในบิลด์
SQLITE_MEMDEBUG จะใส่ wrapper สำหรับเฝ้าระวังข้อผิดพลาดหน่วยความจำ
- สามารถตรวจสอบซ้ำได้เร็วกว่า Valgrind
- Mutex Asserts
- ใช้
sqlite3_mutex_held() เป็นต้น เพื่อตรวจสอบการซิงโครไนซ์ในงานหลายเธรด
- Journal Tests
- ตรวจสอบว่า rollback journal ถูกเขียนก่อน DB เพื่อรับประกัน atomicity ของธุรกรรม
- Undefined Behavior Checks
- ใช้
-ftrapv, -fsanitize=undefined, /RTC1 เป็นต้น เพื่อตรวจจับ undefined behavior
- รันซ้ำบน 32/64 บิต, endian ที่ต่างกัน และ CPU architecture หลายแบบ
9. การทดสอบแบบปิด Optimization
- ใช้
sqlite3_test_control(SQLITE_TESTCTRL_OPTIMIZATIONS) เพื่อปิด optimization
- ไม่ว่าจะเปิดหรือปิด optimization ก็ต้องให้ผลลัพธ์เหมือนกัน
- ยกเว้นการทดสอบบางรายการที่ใช้วัดประสิทธิภาพ
10. Checklist
- ก่อนออกรีลีสจะตรวจสอบ checklist แบบ manual ราว 200 รายการ
- บางข้อใช้เวลาไม่กี่วินาที บางข้อใช้เวลาหลายชั่วโมง
- หากพบปัญหาจะเพิ่มรายการทันทีและปรับปรุงอย่างต่อเนื่อง
11. Static Analysis
- คอมไพล์ผ่านโดย ไม่มี warning บน GCC, Clang และ MSVC
- และไม่มี warning ที่มีนัยสำคัญจาก Clang Static Analyzer
- static analysis มีประสิทธิภาพจำกัดในการค้นหาบั๊กจริง
12. สรุป
- แม้ SQLite จะเป็นโอเพนซอร์ส แต่ก็ยังคงไว้ซึ่ง คุณภาพระดับเชิงพาณิชย์และอัตราข้อบกพร่องต่ำ
- การทดสอบที่เข้มงวดและการออกแบบโค้ดคือปัจจัยสำคัญ
- ทุกรีลีสผ่านกระบวนการข้างต้น จึงถูกส่งมอบในฐานะ DB engine ที่เชื่อถือได้แม้ในสภาพแวดล้อม mission-critical
4 ความคิดเห็น
บทความที่น่าอ่านร่วมกัน: เรื่องราวที่ไม่ค่อยมีใครรู้ของ SQLite
นี่คือบทความสรุปบทสัมภาษณ์ของ Richard Hipp ผู้พัฒนา SQLite
ผู้พัฒนา SQLite บอกว่าได้รู้จัก Do-178 ระหว่างที่ทำงานกับ Rockwell Collins และเริ่มปฏิบัติตามกระบวนการนี้ หนึ่งในนั้นคือการบรรลุ MC/DC ให้ได้ 100%
Do-178 เป็นแนวทางที่ใช้งานได้ดีมาก จึงอยากแนะนำให้นักพัฒนาทุกคนลองอ่านดู
อันนี้หรือเปล่า? https://alm.parasoft.com/hubfs/…
ลิงก์ที่คุณแชร์น่าจะเป็นเอกสารการอบรม Do-178 ครับ
เอกสารต้นฉบับดูได้ที่ลิงก์นี้ครับ
https://studylib.net/doc/27132454/rtca-do-178b
ความเห็นบน Hacker News
เมื่อราว 10 กว่าปีก่อน ผู้ดูแล SQLite เคยพูดที่ OSCON เกี่ยวกับ แนวปฏิบัติด้านการทดสอบ
สิ่งที่น่าประทับใจเป็นพิเศษคือพลังของ เช็กลิสต์ (checklist) ซึ่งก็คือเครื่องมือแบบเดียวกับที่นักบินใช้ก่อนขึ้นบินทุกครั้ง
เขายังพูดถึงกรณีของ Doctors Without Borders ด้วย โดยบอกว่าบุคลากรทางการแพทย์ไม่รู้จักชื่อกันและพูดกันคนละภาษา ทำให้ผลลัพธ์ของการผ่าตัดไม่ดีนัก
วิธีแก้นั้นง่ายมาก — สร้างเช็กลิสต์ก่อนผ่าตัดให้แต่ละคนพูดชื่อและบทบาทของตัวเอง พิธีกรรมเล็ก ๆ นี้ช่วยเพิ่มอัตราการรอดชีวิตผ่านการ ปรับปรุงการสื่อสาร ไม่ใช่เทคโนโลยี
เนื้อหาที่เกี่ยวข้อง: ตัวอย่างเช็กลิสต์ของ SQLite
น่าจะต้องมีการถกกันมากกว่านี้เรื่องความต่างระหว่างเช็กลิสต์ที่ดีกับเช็กลิสต์ที่แย่ มันดูเรียบง่ายเหมือน สูตรคณิตศาสตร์ที่งดงาม แต่กลับค้นพบได้ยาก
โดยเฉพาะเอกสาร FM22-100 ของกองทัพบกสหรัฐฯ ที่อ่านมาหลายครั้งแล้ว ซึ่งทันสมัยและสร้างแรงบันดาลใจอย่างน่าประหลาด
ดูเอกสาร FM22-100
ลิงก์หนังสือ
นอกจากการทดสอบและ CI แล้ว ฉันยังทำตาม เช็กลิสต์การ deploy ที่เขียนเป็น Markdown ด้วย แม้จะไม่ได้เก็บผลลัพธ์ไว้ แต่ก็ทำตามทีละขั้น
ไม่เข้าใจว่าทำไมคนอื่นถึงไม่ทำเรื่องง่าย ๆ แบบนี้
ถ้ามีหน้าอย่างเป็นทางการที่พูดถึงกรณีของ MSF ก็อยากอ่านมาก หาใน Google แล้วไม่เจอ
รวบรวมการพูดคุยในอดีตเกี่ยวกับการทดสอบของ SQLite ไว้
รายชื่อเธรด HN ปี 2009~2024
ดูเหมือนการรีโพสต์จะวนกลับมาทุก ๆ 1 ปี
อิจฉาและทึ่งกับกระบวนการขัดเกลาซอฟต์แวร์อย่าง SQLite ให้สมบูรณ์แบบขนาดนี้
เป็นงานที่ให้ความรู้สึกถึงความเป็นช่างฝีมือ
เมื่อเวลาผ่านไป มาตรฐานคุณภาพจะสูงขึ้น และคุณก็จะได้ผลตอบแทนที่มากขึ้นจากความพยายามเท่าเดิม
ไม่มีใครเกลียดคนที่ทิ้งส่วนที่ตัวเองแตะต้องไว้ให้สะอาดขึ้นอีกนิด
SQLite เป็นซอฟต์แวร์ที่ยอดเยี่ยมจริง ๆ เว็บไซต์ทางการก็ดีตรงที่ เน้นข้อมูลมากกว่าการตลาด
แต่ก็แปลกดีที่ช่วงหลัง ๆ มีหน้าในเว็บทางการถูกโพสต์ขึ้น HN ทีละหน้า
ถ้ารวบรวมลิงก์พวกนี้ไว้ก็น่าจะสนุกดี
น่าสนใจที่ SQLite เป็น ซอฟต์แวร์เปิดเผยต่อสาธารณะ แต่กลับใช้ การทดสอบแบบปิด
เพิ่งตระหนักได้ว่าโปรเจกต์โอเพนซอร์สก็สามารถมีการทดสอบแบบปิดได้
โมเดลแบบนี้อาจกลายเป็นโมเดลธุรกิจใหม่ที่คล้าย open-core ก็ได้
ตัวอย่าง: ไลเซนส์ของ railgunlabs/unicorn
Branch coverage 100% ของ SQLite น่าประทับใจพอ ๆ กับตัวโปรเจกต์เอง
โดยเฉพาะการรักษาระดับนั้นไว้ได้อย่างต่อเนื่อง
น่าสนใจที่การทดสอบเป็นแบบปิด ในยุคที่การเขียนโค้ดด้วย LLM กำลังพัฒนา การทดสอบกำลังสำคัญกว่าการ implement มากขึ้นเรื่อย ๆ
ช่วงหลังเห็นกรณีที่ simonw แปลงเอนจิน justHTML จาก Python เป็น JS ได้แทบจะอัตโนมัติ เลยนึกถึงกลยุทธ์การทดสอบของ SQLite ขึ้นมา
ช่วงหลังได้คุยกับ LLM เรื่องความเข้ากันได้ระหว่าง SQLite กับ DuckDB และได้ข้อสรุปว่าในแง่ของ การจัดการ concurrency นั้น SQLite ดีกว่า
แปลกใจที่ในเอกสารการทดสอบของ SQLite แทบไม่พูดถึง performance regression เลย
ความถูกต้องสำคัญก็จริง แต่การถดถอยของประสิทธิภาพในเส้นทางของ query บางแบบอาจร้ายแรงถึงขั้นวิกฤตได้
เลยสงสัยว่ามีโปรเจกต์ไหนที่ยึดเป้าหมายนี้เป็นภารกิจหลักบ้างไหม
เมื่อดูความเสถียรของ SQLite ก็เลยอยากรู้เพิ่มว่าเขาทดสอบ anomaly กันอย่างไร
แต่ในบทความแทบไม่ได้พูดถึงเลย ถึงอย่างนั้น SQLite ก็ยังเป็นหนึ่งใน ซอฟต์แวร์ที่แข็งแกร่งที่สุด ที่ถูกใช้งานอยู่บนอุปกรณ์แทบทุกชนิด