MyTAP 코드분석
MyTAP은 MySQL 프로젝트에서 구현하고 사용하는 TAP 구현체이다
TAP (Test Anything Protocol)
- TAP producer와 TAP consumer 사이의 프로토콜
- TAP producer - 테스트 로직. 특정 기능을 위해 작성된 테스트를 의미한다
- TAP consumer - 테스트 결과를 파싱하여 통계 자료 등에 활용. TAP harness라고도 표현한다
- YAPC에서 2008년부터 TAP을 위한 IETF 표준을 만드는 프로젝트를 진행중이다
TAP format
- TAP Producer는 아래와 같은 형태로 테스트 결과를 출력한다
1..48
ok 1 Description # Directive
# Diagnostic
....
ok 47 Description
ok 48 Description
1..4
ok 1 - Input file opened
not ok 2 - First line of the input valid.
More output from test 2. There can be
arbitrary number of lines for any output
so long as there is at least some kind
of whitespace at beginning of line.
ok 3 - Read the rest of the file
#TAP meta information
not ok 4 - Summarized correctly
MyTAP
- Testing C and C++ using MyTAP 번역/의역과 보충설명
- italic style text는 아직 이해하지 못한 내용
Introduction
- Unit test - 시스템의 개별적인 구성 요소(component)에 대한 테스트
- 일반적으로 함수, 클래스 또는 작은 단위를 테스트하는 코드
- Functional test - 전체 시스템에 대한 테스트
- 단위 테스트의 필요성
- functional test만 작성하는 것보다 빈틈없는 테스트가 가능하다. 개발자의 의도를 벗어난 API 호출애 대해서도 테스트할 수 있기 때문이다. 이는 전체 시스템의 robustness를 향상시켜 유지하기 쉽게 만든다
- 시스템에 문제가 발생했을때 원인이 되는 component를 쉽게 찾을수 있다. 이는 컴파일-실행-코드 수정의 주기를 단축한다
- component들은 두 가지 경우에 대해 지원해야 한다. 시스템에 API로서 사용되는 경우와 단위 테스트이다. 이는 component가 일반적(generic)이고 견고한 인터페이스를 가지도록 유도한다. 또한 재사용 가능한 component를 개발하도록 유도한다
- 새로운 기능 추가, api 구현 변경 등에 대한 작업 난이도와 비용을 줄인다. 모듈 A에 대해 코드를 수정할때마다 다른 모듈에 대한 side effect를 분석하는 것은 시간이 많이 들고 어렵다. 하지만 단위 테스트가 있다면 단순히 테스트를 실행하고 결과를 확인하는 것만으로 문제가 없음을 보장할 수 있다
- functional test 예시
- 트랜잭션이 spec을 준수하는가?
- 클라이언트가 서버에 연결하여 statement를 수행할수 있는가?
- unit test 예시
String
클래스가 spec에 명시된 character set들을 처리할수 있는가?my_bitmap
에 대한 모든 연산의 결과가 정확한가?
Writing unit test
- 단위 테스트를 작성하는 이유는 테스트를 통과하는 솔루션으로 컴포넌트 개발을 진행하기 위함이다.
단위 테스트는 완벽(모든 케이스를 고려)해야 하며 최소한 다음의 내용을 포함해야 한다
- Normal input
- Borderline cases
- Faulty input - 의도하지 않은 입력값에 대한 테스트
- 예를 들어
int foo(int *n)
라는 함수를 구현했고 이 함수에는NULL
이 전달되면 안될때, - 이 함수에
NULL
이 전달되었더라도 프로그램(시스템)이 다운되는 것보다 적절한 에러를 반환하는게 바람직하다 - 그렇다고 이 함수가 실행될때마다 입력값이
NULL
인지 확인하는 것은 추가적인 cycle을 소모하고 이 비용이 얻는 이득보다 클 수 있다 - debug build시에만 입력값이
NULL
인 경우assert
가 호출되도록 구현하자
- 예를 들어
- Error handling - 명시적인 에러 상황에 의도대로 동작하는지 확인
- Bad environment - API가 실행되는 환경(e.g. OS, system call)을 고려한 테스트
- system call이 실패하더라도 api는 정상적으로 동작해야 하는 경우가 있다.
예를 들면 동적 메모리 공간이 부족하거나 disk의 공간이 부족한 경우이다.
이러한 경우는
malloc
과 같은 함수를 반드시 실패하는 버전으로 대체하여 진행할 수 있다. 이 때 다음의 두 가지를 명심하자 - 대체 함수는 반복적으로 bad environment를 재현할 수 있도록(deterministically) 구현하자
- Make sure that it doesn't just fail immediately. The unit might have checks for the first case, but might actually fail some time in the near future.
- system call이 실패하더라도 api는 정상적으로 동작해야 하는 경우가 있다.
예를 들면 동적 메모리 공간이 부족하거나 disk의 공간이 부족한 경우이다.
이러한 경우는
The basic structure of a test
- 유닛 테스트는 plan, test, report 구조이다
- Plan - 테스트가 몇 개 실행될지 미리 알려준다.
plan
함수를 호출하지 않으면 모든 테스트가 종료된 후에 몇 개의 테스트가 실행되었는지 확인할 수 있다. 이는 unit을 개발하는 동안 테스트가 계속 더해질 경우에 대해서 의도된 것이며 해당 unit이 배포되는 시점에는plan
함수를 사용하는 것을 전제로 구현되었다 - Test -
ok
함수를 사용하여 테스트의 결과를 전달한다. 테스트 결과가 표준 출력으로 출력되고 출력되는 문자열은 TAP format을 따르기 때문에 TAP handling framework가 해석(parse)할 수 있다 - Report -
exit_status
함수는main
함수에 포함된 모든 테스트의 결과를 (TAP producer)로 반환한다. 모든 테스트가 성공한 경우EXIT_SUCCESS
, 하나라도 실패한 경우 다른 값을 반환한다
example code
- MariaDB 프로젝트의
unittest
디렉토리에 각 모듈별 단위 테스트 코드들이 있다
unittest/mysys/my_malloc-t.c
- mysys/my_malloc 모듈에 대한 단위 테스트
/* Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; version 2 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1335 USA */
#include <my_global.h>
#include <my_sys.h>
#include "tap.h"
int main(int argc __attribute__((unused)),char *argv[])
{
void *p;
MY_INIT(argv[0]);
plan(4);
/*
* Borderline case에 대한 테스트
* 요청한 메모리 크기가 0인 경우에도
*/
p= my_malloc(PSI_NOT_INSTRUMENTED, 0, MYF(0));
ok(p != NULL, "Zero-sized block allocation.");
/* Normal inpu test */
p= my_realloc(PSI_NOT_INSTRUMENTED, p, 32, MYF(0));
ok(p != NULL, "Reallocated zero-sized block.");
/* Normal inpu test */
p= my_realloc(PSI_NOT_INSTRUMENTED, p, 16, MYF(0));
ok(p != NULL, "Trimmed block.");
my_free(p);
p= NULL;
/*
* my_free에 NULL이 전달되면 아무것도 하지 않는다
* stdlib의 free함수도 NULL이 전달될 경우 아무것도 하지 않는다고 명시하고 있다
*/
ok((my_free(p), 1), "Free NULL pointer.");
my_end(0);
return exit_status();
}
my_malloc
함수는 이렇게 생겼다- 요청한 메모리의 크기가 0인 경우 1로 설정된다
/**
Allocate a sized block of memory.
@param key Key to register instrumented memory
@param size The size of the memory block in bytes.
@param flags Failure action modifiers (bitmasks).
@return A pointer to the allocated memory block, or NULL on failure.
*/
ATTRIBUTE_MALLOC
void *my_malloc(PSI_memory_key key, size_t size, myf my_flags)
{
my_memory_header *mh;
void *point;
DBUG_ENTER("my_malloc");
DBUG_PRINT("my",("size: %zu flags: %lu", size, my_flags));
compile_time_assert(sizeof(my_memory_header) <= HEADER_SIZE);
if (!(my_flags & (MY_WME | MY_FAE)))
my_flags|= my_global_flags;
/* Safety */
if (!size)
size=1;
if (size > SIZE_T_MAX - 1024L*1024L*16L) /* Wrong call */
DBUG_RETURN(0);
/* We have to align size as we store MY_THREAD_SPECIFIC flag in the LSB */
size= ALIGN_SIZE(size);
if (DBUG_IF("simulate_out_of_memory"))
mh= NULL;
else
mh= (my_memory_header*) sf_malloc(size + HEADER_SIZE, my_flags);
if (mh == NULL)
{
my_errno=errno;
if (my_flags & MY_FAE)
error_handler_hook=fatal_error_handler_hook;
if (my_flags & (MY_FAE+MY_WME))
my_error(EE_OUTOFMEMORY, MYF(ME_BELL+ME_ERROR_LOG+ME_FATAL),size);
if (my_flags & MY_FAE)
abort();
point= NULL;
}
else
{
int flag= MY_TEST(my_flags & MY_THREAD_SPECIFIC);
mh->m_size= size | flag;
mh->m_key= PSI_CALL_memory_alloc(key, size, & mh->m_owner);
if (update_malloc_size)
{
mh->m_size|=2;
update_malloc_size(size + HEADER_SIZE, flag);
}
point= HEADER_TO_USER(mh);
if (my_flags & MY_ZEROFILL)
bzero(point, size);
else
TRASH_ALLOC(point, size);
}
DBUG_PRINT("exit",("ptr: %p", point));
DBUG_RETURN(point);
}