CHECKMK(1) CHECKMK(1)

checkmk - اسکریپت Awk برای تولید تست‌های واحد C جهت استفاده با چارچوب آزمون واحد Check

checkmk [ clean_mode=1 ] [ input-file ]

تولید فایل‌های منبع زبان C شامل تست‌های واحد برای استفاده با چارچوب آزمون واحد Check. هدف این اسکریپت، خودکارسازی بخشی از کدهای تکراری مرسوم (boilerplate) است که هنگام نوشتن یک مجموعه آزمون با استفاده از Check باید نوشته شوند؛ به ویژه نمونه‌سازی از SRunner، Suite(ها) و TCase(ها)، و برقراری ارتباط بین این اشیاء و توابع تست.

این ابزار برای کسانی طراحی شده است که با چارچوب آزمون واحد Check آشنایی دارند. در سرتاسر این راهنما، فرض بر این است که با این چارچوب آشنا هستید.

چارچوب Check همراه با اطلاعات مربوط به آن در https://libcheck.github.io/check در دسترس است.

آرگومان input-file در checkmk از یک ساختار نحوی ساده و شبیه به پیش‌پردازنده C برای اعلان توابع تست و توصیف روابط آن‌ها با Suites و TCases در Check استفاده می‌کند. سپس checkmk از این اطلاعات برای نوشتن خودکار یک تابع main() شامل تمامی تعاریف لازم و کدهای مورد نیاز جهت اجرای مجموعه‌های آزمون بهره می‌برد. خروجی نهایی زبان C در خروجی استاندارد checkmk چاپ می‌شود.

امکاناتی برای درج کدهای کاربر در تابع main() تولیدشده فراهم شده است تا استفاده از ثبت گزارش (logging)، فیکسچرهای آزمون (test fixtures) یا مقادیر خروج سفارشی امکان‌پذیر باشد.

اگرچه می‌توان آرگومان input-file را در checkmk حذف کرد و فایل ورودی را به‌جای آن از طریق ورودی استاندارد checkmk ارائه داد، اما عموماً توصیه می‌شود که آن را به عنوان یک آرگومان مشخص کنید. این کار به checkmk امکان می‌دهد تا از نام فایل مطلع شود، ارجاعاتی به آن در توضیحات ابتدایی خروجی زبان C قرار دهد و دستورالعمل‌های C #line را در سرتاسر کد درج کند تا با هدایت کاربر به فایل ورودی اصلی، اشکال‌زدایی خطاها تسهیل شود.

تنها گزینه رسماً پشتیبانی‌شده، تعیین یک مقدار درست (بر اساس تعریف Awk برای «درست») برای متغیر clean_mode است. این کار باعث می‌شود checkmk دستورالعمل‌های مناسب #line را در کد منبع قرار ندهد، که ممکن است از نظر برخی افراد شلوغی غیرضروری به نظر برسد.

نویسنده استفاده از این گزینه را توصیه نمی‌کند، چرا که باعث می‌شود کامپایلرهای C و ابزارهای اشکال‌زدایی به‌جای ارجاع به فایل‌های ورودی اصلی checkmk، به خطوط خروجی تولیدشده به‌صورت خودکار ارجاع دهند. این موضوع کاربران را تشویق می‌کند تا به‌جای فایل‌های ورودی اصلی، فایل‌های خروجی را ویرایش کنند؛ این امر نمایش فایل مناسب جهت ویرایش را برای ویرایشگرهای هوشمند یا محیط‌های توسعه یکپارچه (IDE) دشوار می‌سازد و ممکن است هنگام تولید مجدد فایل‌های خروجی، اصلاحات انجام‌شده بازنویسی شوند.

دستورالعمل‌های #line هنگامی که فایل ورودی به جای آرگومان خط فرمان، از طریق ورودی استاندارد ارائه شود، به‌طور خودکار غیرفعال می‌شوند.

در ساده‌ترین حالت، یک فایل ورودی می‌تواند صرفاً شامل یک مقدمه (prologue) و یک تابع آزمون باشد. هر چیزی که پیش از اولین تابع آزمون قرار گیرد در بخش مقدمه خواهد بود و عیناً در خروجی کپی می‌شود. تابع آزمون با خطی به این شکل آغاز می‌شود:

#test test_name

که در آن test_name نام تابع آزمون شما است. این نام برای نام‌گذاری یک تابع C استفاده خواهد شد، بنابراین باید یک شناسه معتبر در C باشد.

در اینجا یک مثال کوچک و کامل آورده شده است:

--------------------------------------------------
/* A complete test example */
#include <stdio.h>
#test the_test
    int nc;
    const char msg[] = "\n\n    Hello, world!\n";
    nc = printf("%s", msg);
    ck_assert(nc == (sizeof(msg) - 1)); /* for terminating NUL. */
--------------------------------------------------

اگر موارد بالا را در فایلی به نام basic_complete.ts قرار دهید و آن را با دستور زیر پردازش کنید:

$ checkmk basic_complete.ts > basic_complete.c

فایل basic_complete.c حاوی خروجی مشابه زیر خواهد بود:

--------------------------------------------------
/*
 * DO NOT EDIT THIS FILE. Generated by checkmk.
 * Edit the original source file "in" instead.
 */
#include <check.h>
/* A complete test example */
#include <stdio.h>
START_TEST(the_test)
{
    int nc;
    const char msg[] = "\n\n    Hello, world!\n";
    nc = printf("%s", msg);
    ck_assert(nc == (sizeof(msg) - 1)); /* for terminating NUL. */
}
END_TEST
int main(void)
{
    Suite *s1 = suite_create("Core");
    TCase *tc1_1 = tcase_create("Core");
    SRunner *sr = srunner_create(s1);
    int nf;
    suite_add_tcase(s1, tc1_1);
    tcase_add_test(tc1_1, the_test);
    srunner_run_all(sr, CK_ENV);
    nf = srunner_ntests_failed(sr);
    srunner_free(sr);
    return nf == 0 ? 0 : 1;
}
--------------------------------------------------

در کاربرد واقعی، basic_complete.c شامل دستورالعمل‌های #line نیز خواهد بود..SH "خلاصه دستورالعمل‌ها (DIRECTIVE SUMMARY)"

در اینجا خلاصه کاملی از تمام دستورالعمل‌های سبک پیش‌پردازنده C آورده شده است که توسط checkmk فهمیده می‌شوند. برای جزئیات بیشتر به ادامه مطلب مراجعه کنید.

# test test_name
# test-signal(signal) test_name
# test-exit(exit_code) test_name
# test-loop(start, end) test_name
# test-loop-signal(signal, start, end) test_name
# test-loop-exit(exit_code, start, end) test_name
# suite TestSuiteName
# tcase TestCaseName
# main-pre
# main-post

تمام دستورالعمل‌ها به بزرگی و کوچکی حروف حساس نیستند (case-insensitive). فاصله‌های خالی می‌توانند در ابتدای خط پیش از #، بین # و دستورالعمل، بین دستورالعمل و هر آرگومان، و در انتهای خط قرار گیرند.

در اینجا توضیحات دقیق‌تری در مورد دستورالعمل‌هایی ارائه شده است که می‌توانند برای تعریف توابع آزمون و دربرگیرنده‌های آن‌ها استفاده شوند.

# test test_name
# test-signal(signal) test_name
# test-exit(exit_code) test_name
# test-loop(start, end) test_name
# test-loop-signal(signal, start, end) test_name
# test-loop-exit(exit_code, start, end) test_name

این‌ها پایه‌ای‌ترین دستورالعمل‌ها برای ایجاد یک قالب ورودی به checkmk هستند. آن‌ها تنها دستورالعمل‌های اجباری می‌باشند: باید حداقل یک دستورالعمل #test* در قالب وجود داشته باشد، در غیر این صورت checkmk با یک پیام خطا با شکست مواجه خواهد شد. دستورالعمل‌های #test* می‌توانند چندین بار مشخص شوند که هر کدام آغازگر تعریف یک تابع آزمون جدید است.

آرگومان test_name به عنوان نام تابع آزمون در خروجی به زبان C استفاده خواهد شد، بنابراین باید یک شناسه معتبر C باشد. به این معنی که باید با یک نویسه الفبایی یا زیرخط (_) آغاز شود، و به دنبال آن نویسه‌های اختیاری الفبایی-عددی و/یا زیرخط قرار گیرند.

نام‌های نویسه عمومی (معرفی‌شده در C99) نیز به فرم \uXXXX یا \UXXXXXXXX مجاز هستند، که در آن‌ها Xها نشان‌دهنده ارقام هگزادسیمال می‌باشند.

مشخص کردن یک test_name یکسان در بیش از یک دستورالعمل #test* یک خطا محسوب می‌شود، بدون در نظر گرفتن این‌که آیا به موردها یا مجموعه‌های آزمون متفاوتی مرتبط هستند یا خیر.

برای دیدن فهرست شناسه‌هایی که نباید به عنوان نام تابع آزمون استفاده شوند، به بخش شناسه‌های CHECKMK (CHECKMK IDENTIFIERS) مراجعه کنید.

# suite TestSuiteName

این دستورالعمل نام مجموعه آزمون (شیء Suite در چارچوب آزمون Check) را مشخص می‌کند که تمام موردهای آزمون بعدی (و توابع آزمون آن‌ها) به آن افزوده خواهند شد.

مقدار TestSuiteName یک رشته متنی است و می‌تواند شامل هر نوع نویسه‌ای باشد (به جز نویسه ASCII NUL و خط جدید (newline) که دستورالعمل را پایان می‌دهد). هرگونه فاصله خالی در ابتدا یا انتهای نام مجموعه آزمون نادیده گرفته خواهد شد.

آغاز یک مجموعه آزمون جدید همچنین یک مورد آزمون جدید را شروع می‌کند که نام آن با مجموعه آزمون جدید یکسان است. این نام مورد آزمون می‌تواند توسط یک دستورالعمل بعدی #tcase بازنویسی شود.

توجه داشته باشید که یک شیء Suite در خروجی C توسط checkmk در عمل تعریف نخواهد شد، مگر اینکه در نقطه‌ای پس از آن یک دستورالعمل #test (بدون قرار گرفتن یک #suite در این بین) آمده باشد. نداشتن هیچ #test مرتبطی برای یک #suite خطا محسوب نمی‌شود؛ در این حالت #suite (و هر #tcase مرتبط با آن) به‌سادگی هیچ عملیاتی را از جانب checkmk در پی نخواهند داشت (و در نتیجه بی‌فایده خواهند بود).

مشخص کردن چندباره یک مجموعه آزمون یکسان (حساس به بزرگی و کوچکی حروف) توسط دستورالعمل #suite خطا است، مگر اینکه استفاده‌های قبلی به دلیل نبود حداقل یک دستورالعمل مرتبط #test نمونه‌سازی (instantiated) نشده باشند.

اگر قبل از اولین دستورالعمل #test یک دستورالعمل #suite را مشخص نکنید، checkmk معادل یک دستورالعمل ضمنی #suite با رشته "Core" به عنوان مقدار TestSuiteName عمل می‌کند (این موضوع همچنین نشان‌دهنده یک شیء مورد آزمون "Core" است). این مورد در بالا در بخش مثال پایه (BASIC EXAMPLE) نشان داده شده است.

# tcase TestCaseName

این دستورالعمل نام مورد آزمون (شیء TCase در چارچوب آزمون Check) را مشخص می‌کند که تمام توابع آزمون بعدی به آن افزوده خواهند شد.

دستورالعمل #tcase به شیوه‌ای بسیار شبیه به #suite عمل می‌کند. مقدار TestCaseName یک رشته متنی است و می‌تواند شامل نویسه‌های دلخواه باشد؛ و یک شیء TCase در عمل تعریف نخواهد شد مگر اینکه به دنبال آن یک دستورالعمل مرتبط #test بیاید.

مشخص کردن چندباره یک مورد آزمون یکسان (حساس به بزرگی و کوچکی حروف) توسط دستورالعمل #tcase خطا است، مگر اینکه استفاده‌های قبلی به دلیل نبود حداقل یک دستورالعمل مرتبط #test نمونه‌سازی نشده باشند.

همچنین به دستورالعمل #suite که در بالا شرح داده شد مراجعه کنید..SH "کد کاربر در MAIN() (USER CODE IN MAIN())"

تابع main() در زبان C به‌طور خودکار توسط checkmk تولید می‌شود و SRunnerها، Suiteها و TCaseهای لازم را که توسط دستورالعمل‌های تعریف آزمون مشخص‌شده از سوی کاربر نیاز است، تعریف می‌کند.

در بیشتر شرایط، این main() کاملاً خودکار به همین شکل بسیار مناسب است. با این حال، موقعیت‌هایی وجود دارد که ممکن است فرد بخواهد کد سفارشی به main() اضافه کند. برای مثال، اگر کاربر بخواهد:

•
مقدار مهلت زمانی (timeout) آزمون را از طریق tcase_set_timeout() تغییر دهد،
•
حالت «عدم فورک» (no-fork-mode) ابزار Check را از طریق srunner_set_fork_status() مشخص کند،
•
فیکسچرهای آزمون را برای برخی از موارد آزمون از طریق tcase_add_checked_fixture() یا tcase_add_unchecked_fixture() برپا کند،
•
ثبت وقایع آزمون (logging) را برای اجراکنندهٔ مجموعه (suite runner) از طریق srunner_set_log() یا srunner_set_xml() برپا کند، یا
•
پس از اجرای مجموعه‌های آزمون، عملیات پاک‌سازی و جمع‌بندی سفارشی را انجام دهد.

برای این اهداف، دستورالعمل‌های #main-pre و #main-post ارائه شده‌اند.

# main-pre

متن پس از این دستورالعمل، عیناً در بدنهٔ تابع تولیدشدهٔ main()، درست پس از اعلان متغیرهای محلی خود checkmk و پیش از انجام هرگونه اجرای آزمون قرار خواهد گرفت (در واقع، حتی قبل از اینکه روابط بین آزمون‌ها، موارد آزمون و مجموعه‌های آزمون برقرار شود، هرچند این موضوع نباید تفاوت چندانی ایجاد کند). از آنجا که checkmk تازه اعلان‌های خود را به پایان رسانده است، حتی طبق رهنمودهای سخت‌گیرانهٔ ISO C 1990 نیز مجاز است که اعلان‌های متغیر سفارشی در اینجا قرار گیرند.

برخلاف دستورالعمل‌های توصیف‌شدهٔ قبلی، #main-pre حداکثر یک‌بار می‌تواند مشخص شود. نباید قبل از آن دستورالعمل #main-post قرار گیرد و هیچ دستورالعمل #suite، #tcase یا #test نمی‌تواند پس از آن ظاهر شود.

دستورالعمل #main-pre مکان مناسبی برای تغییر تنظیمات یا برپاسازی فیکسچرهای آزمون است. البته برای انجام این کار، باید بدانید checkmk از چه نام‌هایی برای نمونه‌سازی SRunnerها، Suiteها و TCaseها استفاده کرده است.

اشاره‌گرها به Suiteها با استفاده از الگوی sX اعلان می‌شوند، که در آن X عددی است که از ۱ شروع می‌شود و برای هر دستورالعمل #suite بعدی افزایش می‌یابد. s1 همیشه وجود دارد و شامل تابع آزمونی است که توسط اولین دستورالعمل #test اعلان شده است. اگر قبل از آن دستورالعمل یک #suite نیامده باشد، نام "Core" به آن داده خواهد شد.

اشاره‌گرها به TCaseها با استفاده از الگوی tcX_Y اعلان می‌شوند، که در آن X با شمارهٔ استفاده‌شده برای نام Suite که این TCase را در بر دارد مطابقت می‌کند؛ و Y عددی است که برای هر Suite جدید از ۱ آغاز می‌شود و برای هر TCase در آن Suite افزایش می‌یابد.

اشاره‌گری به SRunner با شناسهٔ sr اعلان می‌شود؛ همچنین یک عدد صحیح به نام nf وجود دارد که تعداد شکست‌های آزمون را (پس از اجرای آزمون‌ها) در خود نگه می‌دارد.

به دلایل واضح، کاربر نباید سعی کند شناسه‌های محلی در main() اعلان کند، یا ماکروها یا توابع آزمونی را تعریف کند که نام آن‌ها ممکن است با نام متغیرهای محلی مورد استفاده توسط checkmk تداخل داشته باشد. به‌طور خلاصه، این نام‌ها عبارتند از:

sX

tcX_Y

sr

nf.

# main-post

اگرچه به آن اندازه مفید نیست، اما checkmk همچنین یک دستورالعمل #main-post برای درج کد سفارشی در انتهای main()، پس از اجرای آزمون‌ها، ارائه می‌دهد. از این دستورالعمل می‌توان برای آزادسازی منابعی که در بخش مقدمه تخصیص یافته‌اند، چاپ اطلاعات دربارهٔ آزمون‌های ناموفق، یا ارائهٔ یک کد وضعیت خروج سفارشی استفاده کرد.

توجه داشته باشید که اگر از این دستورالعمل استفاده کنید، checkmk دستور return را ارائه نخواهد داد: خودتان باید یکی ارائه دهید.

دستورالعمل #main-post نباید با هیچ دستورالعمل دیگری که توسط checkmk شناخته شده است دنبال شود..SH "مثال جامع (COMPREHENSIVE EXAMPLE)"

اکنون که با توضیحات تفصیلی رهنمودهای گوناگون آشنا شده‌اید، بیایید عملکرد همهٔ آن‌ها را در قالب این الگوی نسبتاً جامع ببینیم.

--------------------------------------------------
#include "mempool.h"  /* defines MEMPOOLSZ, prototypes for
                         mempool_init() and mempool_free() */
void *mempool;
void mp_setup(void)
{
    mempool = mempool_init(MEMPOOLSZ);
    ck_assert_msg(mempool != NULL, "Couldn't allocate mempool.");
}
void mp_teardown(void)
{
    mempool_free(mempool);
}
/* end of prologue */
#suite Mempool
#tcase MP Init
#test mempool_init_zero_test
    mempool = mempool_init(0);
    ck_assert_msg(mempool == NULL, "Allocated a zero-sized mempool!");
    ck_assert_msg(mempool_error(), "Didn't get an error for zero alloc.");
/* "MP Util" TCase uses checked fixture. */
#tcase MP Util
#test mempool_copy_test
    void *cp = mempool_copy(mempool);
    ck_assert_msg(cp != NULL, "Couldn't perform mempool copy.");
    ck_assert_msg(cp != mempool, "Copy returned original pointer!");
#test mempool_size_test
    ck_assert(mempool_getsize(mempool) == MEMPOOLSZ);
#main-pre
    tcase_add_checked_fixture(tc1_2, mp_setup, mp_teardown);
    srunner_set_log(sr, "mplog.txt");
#main-post
    if (nf != 0) {
      printf("Hey, something's wrong! %d whole tests failed!\n", nf);
    }
    return 0; /* Harness checks for output, always return success
                 regardless. */
--------------------------------------------------

با دادن این ورودی به checkmk، خروجی تقریباً به صورت زیر حاصل خواهد شد:

--------------------------------------------------
/*
 * DO NOT EDIT THIS FILE. Generated by checkmk.
 * Edit the original source file "comprehensive.ts" instead.
 */
#include <check.h>
#include "mempool.h"
void *mempool;
void mp_setup(void)
{
...
}
void mp_teardown(void)
{
...
}
/* end of prologue */
START_TEST(mempool_init_zero_test)
{
...
}
END_TEST
START_TEST(mempool_copy_test)
{
...
}
END_TEST
START_TEST(mempool_size_test)
{
...
}
END_TEST
int main(void)
{
    Suite *s1 = suite_create("Mempool");
    TCase *tc1_1 = tcase_create("MP Init");
    TCase *tc1_2 = tcase_create("MP Util");
    SRunner *sr = srunner_create(s1);
    int nf;
    /* User-specified pre-run code */
    tcase_add_checked_fixture(tc1_2, mp_setup, mp_teardown);
    srunner_set_log(sr, "mplog.txt");
    suite_add_tcase(s1, tc1_1);
    tcase_add_test(tc1_1, mempool_init_zero_test);
    suite_add_tcase(s1, tc1_2);
    tcase_add_test(tc1_2, mempool_copy_test);
    tcase_add_test(tc1_2, mempool_size_test);
    srunner_run_all(sr, CK_ENV);
    nf = srunner_ntests_failed(sr);
    srunner_free(sr);
    /* User-specified post-run code */
    if (nf != 0) {
      printf("Hey, something's wrong! %d whole tests failed!\n", nf);
    }
    return 0; /* Harness checks for output, always return success
                 regardless. */
}
--------------------------------------------------
checkmk و این راهنما توسط Micah J Cowan نوشته شده‌اند.
حق نشر (C) 2006, 2010 Micah J Cowan.
09 February 2010