ข้ามไปที่เนื้อหาหลัก

ใช้ Watchdog ช่วยทำ TDD สำหรับ C++ ด้วย Gtest และ CMake

เห็นคนรู้จักหลายๆ คนทำ TDD กัน เลยอยากทำบาง โดยมากมักใช้ Watchr เพื่อช่วนในการรัน Test Case ทันทีที่บันทึกไฟล์ เห็นแล้วสะดวกดี แต่พอดีเขียน Ruby ไม่เป็นเลยมากหาไลบราลีภาษา Python แทน ก็ไปเจอ Watchdog อาศัยแรงของตั้วเลยได้ Python script อย่างง่ายมาเล่น TDD สำหรับ Python กัน

วันนี้อยากเอามันมาใช้กับ C++ บ้างเล่นเอาเหนื่อยเหมือนกันกว่าจะเอา Python script ของตั้วมาใช้กับ C++ ซึ่งจากที่ดูๆ มา Google Test กับ CMake น่าจะตอบโจทย์ TDD ได้ระดับนึ่ง แต่ต้องอาศัยการจัดรูปแบบของไดเร็กทอรีเข้าร่วมด้วย ตอนนี้สคริปต์ยังมีการกำหนดหลายๆ อย่างตายตัวอยู่มาก อาจจะยังไม่เรียบร้อยดีเท่าที่ควร

หน้าตาไดเร็กทอรีประมาณนี้

.
├── src
│   └── xxx
│       └── xxx.cpp
├── tests
│   ├── CMakeLists.txt
│   ├── external
│   │   └── gtest
│   │       └── CMakeLists.txt
│   └── units
│       └── xxx
│           └── test_xxx.cpp
├── cpp-testrunner
│
└── CMakeLists.txt

สำหรับไดเร็กทอรีโปรเจคจะประกอบไปด้วย src tests CmakeLists.txt และ cpp-testrunner
  • src ใช้เก็บซอร์ซโค้ดของโปรแกรม
  • tests ใช้เก็บ Test Case ต่างๆ
  • CMakeLists.txt เป็น CMake หลักของโปรเจค
  • cpp_testrunner คือสคริปต์ที่เอามาช่วยรัน Test Case
นอกจากนี้ยังมี tests/CMakeLists.txt และ tests/external/gtest/CMakeLists.txt สองไฟล์นี้ทำหน้าที่สำหรับคอมไฟล์ Test ให้กับโปรแกรม โดยที่ tests/CMakeLists.txt จะเป็นส่วนหลักในการคอมไพล์และจะเรียก tests/external/gtest/CMakeLists.txt เพื่อคอมไพล์ไลบรารี Gtest อีกทีนึง นั้นก็หมายความว่า เครื่องที่จะรันจะต้องติดตั้งเพกเกจ libgtest-dev ไว้แล้ว โดยตัวอย่างการเขียน CMake ทั้งหมดกับ Gtest ได้มาจาก google-test-examples

สำหรับส่วนของการเขียน Test จะต้องสร้าง Unit Test ไฟล์ไว้ใน tests/units และมีได้เร็กทอรีที่ล้อกับ src ดังตัวอย่าง src/xxx/xxx.cpp จะต้องสร้าง Unit Test ไฟล์ไว้ที่ tests/units/xxx/test_xxx.cpp ใช้ test_ นำหน้าที่ไฟล์ สำหรับ cpp-testrunner หน้าตาจะเหมือนด้านล่าง ใช้ Python3 นะครับ


#!/usr/bin/env python
import os
import sys
import time
import re
from watchdog.observers import Observer
from watchdog.events import PatternMatchingEventHandler
 


def when_file_changed(filename):
    def projectname():
        """
            Projectname from name of current directory ('.')
            if there are dash '-' in its name replace to underscore '_'
        """
        return os.path.abspath(".").rsplit("/", 1)[1]
 
    def get_test_cases(filename):     
        test_case = []
        try:
            with open(test_file(filename), 'r') as f:
                src = f.read()
                test_case = list(set(re.findall('TEST\s*\((.*),', src)))
        except:
            pass

        return test_case
    
    def test_file(filename):
        basename = os.path.basename(filename)
        if not basename.startswith("test_"):
            filename = filename.replace("src", "tests/units")
            filename = filename.replace(basename, "test_" + basename)
        return filename

    def make_test_runner():
        build_test_path = os.path.abspath(".")+'/build-test'
        test_path = os.path.abspath(".")+'/tests'
        os.system("cmake -B{build_test_path} -H{test_path}".format(build_test_path=build_test_path, test_path=test_path))
        os.system("make -C {build_test_path}".format(build_test_path=build_test_path))
            
 
    cls()
    print(os.path.abspath(filename))
    make_test_runner()

    # extract testcase

    test_cases = get_test_cases(test_file(filename))
    if len(test_cases) == 0:
        print('There are no unit tests, skip test running')
        return

    cmd = 'build-test/'+projectname()+'-test'\
            + ' --gtest_filter='+'.*:'.join(test_cases)+'.*'
    
    print(cmd)
    os.system(cmd)
 
 
def cls():
    os.system('cls' if os.name == 'nt' else 'clear')
 
 
class ModifiedHandler(PatternMatchingEventHandler):
    patterns = ['*.cpp', '*.hpp', '*cc', '*.h']  # Monitor only matched patterns
    def on_created(self, event):
        """" For Vim :w - not modify that deleted and created file instead. """       
        if '/build-test/' in event.src_path:
            return

        when_file_changed(event.src_path)
 
 
if __name__ == '__main__':
    args = sys.argv[1:]

    path = args[1] if args else '.'
    build_path = path + '/build-test'
    if not os.path.exists(build_path):
        os.mkdir(build_path)

    event_handler = ModifiedHandler()
    observer = Observer()
    observer.schedule(event_handler,
                      path=args[1] if args else '.', recursive=True)
    observer.start()
 
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        observer.stop()
    observer.join()

ไฟล์ tests/CMakeLists.txt
cmake_minimum_required(VERSION 2.8.8)

set(PROJECT_NAME HelloGtest)
set(PROJECT_TEST_NAME ${PROJECT_NAME}-test)
project(${PROJECT_NAME} C CXX)


set(EXT_PROJECTS_DIR ${PROJECT_SOURCE_DIR}/external)
add_subdirectory(${EXT_PROJECTS_DIR}/gtest)

link_libraries(
)

file(GLOB_RECURSE ${PROJECT_NAME}_CXX_SRC RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "../src/*.cpp")
file(GLOB_RECURSE ${PROJECT_NAME}_CXX_TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test*.cpp")

# remove main function file from test
list(REMOVE_ITEM ${PROJECT_NAME}_CXX_SRC ${CMAKE_CURRENT_SOURCE_DIR} "../src/main.cpp")

# complier
set (CMAKE_CXX_COMPILER g++)
add_definitions(-std=c++1y -pthread)
set (CMAKE_VERBOSE_MAKEFILE TRUE)

set (CMAKE_CXX_FLAGS_DEBUG, "-g -Wall")
set (CMAKE_CXX_FLAGS "-Wall")

include_directories ("${PROJECT_BINARY_DIR}" "${PROJECT_SOURCE_DIR}/../src"  "${PROJECT_SOURCE_DIR}/../tests")


add_executable (${PROJECT_TEST_NAME} ${${PROJECT_NAME}_CXX_TESTS} ${${PROJECT_NAME}_CXX_SRC})
# add_executable (${PROJECT_TEST_NAME} ${${PROJECT_NAME}_CXX_SRC})
add_dependencies(${PROJECT_TEST_NAME} googletest)
target_link_libraries (${PROJECT_TEST_NAME} 
    ${GTEST_LIBS_DIR}/libgtest.a
    ${GTEST_LIBS_DIR}/libgtest_main.a
    pthread
)

ไฟล์ tests/external/gtest/CMakeLists.txt
cmake_minimum_required(VERSION 2.8.8)
project(gtest_builder C CXX)
include(ExternalProject)

ExternalProject_Add(googletest
    SOURCE_DIR /usr/src/gtest
    CMAKE_ARGS -DCMAKE_ARCHIVE_OUTPUT_DIRECTORY_DEBUG:PATH=DebugLibs
               -DCMAKE_ARCHIVE_OUTPUT_DIRECTORY_RELEASE:PATH=ReleaseLibs
               -DCMAKE_CXX_FLAGS=${MSVC_COMPILER_DEFS}
               -Dgtest_force_shared_crt=ON
     PREFIX "${CMAKE_CURRENT_BINARY_DIR}"
# Disable install step
    INSTALL_COMMAND ""
)

# Specify include dir
ExternalProject_Get_Property(googletest source_dir)
set(GTEST_INCLUDE_DIRS ${source_dir}/include PARENT_SCOPE)

# Specify MainTest's link libraries
ExternalProject_Get_Property(googletest binary_dir)
set(GTEST_LIBS_DIR ${binary_dir} PARENT_SCOPE)

เวลาใช้งานก็แค่ รัน cpp-testrunner ทิ้งไว้ ทุกครั้งที่มีการบันทึกไฟล์ สคลิปต์ จะรัน cmake และ make ให้อัตโนมัติ พร้อมกับรัน Test Case ของไฟล์นั้นให้เลย 

ความคิดเห็น

โพสต์ยอดนิยมจากบล็อกนี้

ตัวเอ๋ยตัวผม

กลอนนี้ใช้เวลาประมาณ 20 นาทีเขียนขึ้นมาในห้องเรียนวิชาสัมนา 1 เพราะอาจารย์อยากให้แนะนำตัวเองเป็นกลอน ไม่รู้จะแต่งว่าไงเลยแต่งออกมาเป็นดอกสร้อย เห็นว่าพอใช้ได้เลยเอามาลงไว้เป็นอนุสร ๏ ตัวเอ๋ยตัวผม นิยมในพระพุทธศาสนา ตั้งจิตตั้งใจตั้งหน้า ใฝ่หาความรู้สู่ตน ตั้งใจศึกษาให้เชี่ยวชาญ ชำนาญในศาสตร์ที่ฝึกฝน ฝึกจิตฝึกสันดานให้เป็นคน เป็นชนในชาติที่ดีเอย ๚ะ๛

บันทึกการจัดงานศพ: พิธีฌาปนกิจศพ

ตรงส่วนนี้คงจะเขียนเกี่ยวกับพิธียกศพออกจากบ้าน และเกร็ดต่างๆ เล็กๆ น้อยๆ เนื่องจากที่จัดงานจะไม่นิยมไว้ศพที่วัด จะไว้ศพที่บ้าน และถ้าเป็นไปได้จะไว้ศพในบ้านเสียด้วยซ่ำ เมื่อถึงวันฌาปนกิจศพ หรือเผาศพ ก็จะมีการเซ่นไหว้ครั้งใหญ่ก่อนเคลื่อนย้ายศพไปวัดเพื่อฌาปนกิจ เครื่องเซ่นไว้จะประกอบไปด้วย ข้าว 5 ถ้วย กับข้าว 5 อย่าง หัวหมู ไก่ต้ม ไข่ต้ม หมูสามชั้นต้ม หมี่เหลืองผัด กุ้ง หอย ปู ปลา ผลไม้ 5 อย่าง ขนมขึ้น เมื่อมีการเซ่นไหว้ทุกครั้งจะต้องมี สัปรด น้ำชา 3 จอก เหล้าขาว 5 จอก(หลานๆ บอกว่าเจ็คไม่กินเหล้าขาว แต่มีคนบอกว่าเป็นการไหว้ตามประเพณี ^^ ) ซึ่งแต่ละอย่างมีความหมาย แต่ผมจำไม่ได้ต้องหาอีกครั้งนึง ตัวอย่างเครื่องเซ่นไหว้ เมื่อถึงพิธีเซ่นไหว้ จะมีการเซ่นไหว้โดยแบ่งออกเป็นคณะ แต่เพื่อความสะดวกและรวบรัดจึงมีการไหว้เพียงไม่กี่คณะ ซึ่งก็เหมือนเดิมคือผู้ที่มีศักดิ์สูงกว่าจะไม่รวมการเซ่นไหว้ครั้งนี้ คณะแรกจะเป็นผู้ไกล้ชิดผู้ตายมากที่สุดเริ่มตั้งแต่ลูกและภรรยา หลังจากนั้นก็จะเป็นน้องๆ แล้วก็หลานๆ และก็มิตรรักและผู้คนที่นับถือผู้ตาย หากเป็นเมื่อสมัยก่อนนั้น ต้องแยกออกเป็นเขย เป็นสะไภ้ ไหว้กันหลายยกหล

ด้วยระลึกถึงคุณย่า บันทึกจากความทรงจำ

บันทึกนี้เขียนขึ้นเพื่อบันทึกความทรงจำของผมที่มีต่อคุณย่าที่ล่วงลับไปแล้วอย่างไม่มีวันหวนคืน คุณย่าเปรียบเหมือนฟางเส้นสุดท้ายที่ร้อยครอบครัวใหญ่ของเราเอาไว้ไม่ให้แตกแยก หลังจากที่เสียคุณปู่ไปเมื่อ 23 ปีก่อน เนื่องจากบริเวณจังหวัดกระบี่ ตรัง พังงา ภูเก็ต มีชาวจีนอาศัยอยู่มาก แต่มักจะเป็นชาวจีนที่อพยพมาไทยนานแล้ว จากการการสังเกตของผม ชาวจีนแถบนี้โดยมากน่าจะเป็นชาว เปอรานากัน หรือชาวจีนที่อพยพมาจากจีนแล้วตั้งถิ่นฐานอยู่ในแหลมลายูหรืออินโดนีเซีย แล้วหลังจากนั้นจึงอพยพมาอาศัยต่อที่ประเทศไทย จากการบอกเล่าของคุณแม่ ก๋งเคยเล่าให้ฟังว่าตอนยังเด็กเคยแจวเรือจ้างอยู่ที่ปีนัง คุณย่าเคยเล่าว่าเป็นชาวฮกเกี้ยน อีกทั้วจากรูปวาดคุณย่าทวดที่มีการเกล้ามวยผม สวมเสื้อคอลึก ส่วนทางบ้านมีการใช้คำเรียกจีนผสมไทยถิ่นใต้อยู่มาก ผู้หญิงทุกคนนิยมสวมผ้าปาเต๊ะ เสื้อลูกไม้ (เสื้อฉลุลายดอกไม้) อาหารการกินเป็นแบบชาวไทยถิ่นใต้ทุกประการ (กินน้ำพริก แกงส้มเก่งกันทุกคน ยกเว้นก๋ง :D) อีกทั้งก๋งเกิดที่ดินแดนแถบนี้ไม่ได้เดินทางมาจากเมืองจีน (บางทีเรียก เตี่ยต่อเตี่ย คือ ทวดมาจากจีน ส่วนสถานที่เกิดไม่แน่ใจว่าเป็นปีนังหรือไทย)