diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0065843..54674a0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,8 +28,14 @@ jobs: - name: Check out uses: actions/checkout@v2 - name: Build + shell: bash run: | mkdir build cd build cmake .. -G "${{matrix.platform.generator}}" - cmake --build . + cmake --build . --verbose + - name: Test + shell: bash + run: | + cd build + CTEST_OUTPUT_ON_FAILURE=1 ctest --build-config Debug diff --git a/CMakeLists.txt b/CMakeLists.txt index 12d4af1..a680d0e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.16..3.29) project(clar LANGUAGES C) -option(BUILD_TESTS "Build test executable" ON) +option(BUILD_EXAMPLE "Build the example." ON) add_library(clar INTERFACE) target_sources(clar INTERFACE @@ -25,4 +25,8 @@ if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) if(BUILD_TESTING) add_subdirectory(test) endif() + + if(BUILD_EXAMPLE) + add_subdirectory(example) + endif() endif() diff --git a/README.md b/README.md index a8961c5..7e70993 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,7 @@ Can you count to funk? ~~~~ sh $ mkdir tests $ cp -r $CLAR_ROOT/clar* tests - $ cp $CLAR_ROOT/test/clar_test.h tests - $ cp $CLAR_ROOT/test/main.c.sample tests/main.c + $ cp $CLAR_ROOT/example/*.c tests ~~~~ - **One: Write some tests** @@ -147,7 +146,7 @@ To use Clar: 1. copy the Clar boilerplate to your test directory 2. copy (and probably modify) the sample `main.c` (from - `$CLAR_PATH/test/main.c.sample`) + `$CLAR_PATH/example/main.c`) 3. run the Clar mixer (a.k.a. `generate.py`) to scan your test directory and write out the test suite metadata. 4. compile your test files and the Clar boilerplate into a single test @@ -159,7 +158,7 @@ The Clar boilerplate gives you a set of useful test assertions and features the `clar.c` and `clar.h` files, plus the code in the `clar/` subdirectory. You should not need to edit these files. -The sample `main.c` (i.e. `$CLAR_PATH/test/main.c.sample`) file invokes +The sample `main.c` (i.e. `$CLAR_PATH/example/main.c`) file invokes `clar_test(argc, argv)` to run the tests. Usually, you will edit this file to perform any framework specific initialization and teardown that you need. diff --git a/clar.c b/clar.c index d54e455..6022400 100644 --- a/clar.c +++ b/clar.c @@ -433,7 +433,7 @@ clar_usage(const char *arg) printf(" -t Display results in tap format\n"); printf(" -l Print suite names\n"); printf(" -r[filename] Write summary file (to the optional filename)\n"); - exit(-1); + exit(1); } static void @@ -660,7 +660,7 @@ static void abort_test(void) clar_print_onabort( "Fatal error: a cleanup method raised an exception.\n"); clar_report_errors(_clar.last_report); - exit(-1); + exit(1); } CL_TRACE(CL_TRACE__TEST__LONGJMP); @@ -826,7 +826,8 @@ void clar__assert_equal( void *p1 = va_arg(args, void *), *p2 = va_arg(args, void *); is_equal = (p1 == p2); if (!is_equal) - p_snprintf(buf, sizeof(buf), "%p != %p", p1, p2); + p_snprintf(buf, sizeof(buf), "0x%"PRIxPTR" != 0x%"PRIxPTR, + (uintptr_t)p1, (uintptr_t)p2); } else { int i1 = va_arg(args, int), i2 = va_arg(args, int); diff --git a/clar.h b/clar.h index 8c22382..99b1904 100644 --- a/clar.h +++ b/clar.h @@ -9,6 +9,16 @@ #include +#ifndef CLAR_SELFTEST +# define CLAR_CURRENT_FILE __FILE__ +# define CLAR_CURRENT_LINE __LINE__ +# define CLAR_CURRENT_FUNC __func__ +#else +# define CLAR_CURRENT_FILE "file" +# define CLAR_CURRENT_LINE 42 +# define CLAR_CURRENT_FUNC "func" +#endif + enum cl_test_status { CL_TEST_OK, CL_TEST_FAILURE, @@ -86,16 +96,16 @@ const char *cl_fixture_basename(const char *fixture_name); /** * Assertion macros with explicit error message */ -#define cl_must_pass_(expr, desc) clar__assert((expr) >= 0, __FILE__, __func__, __LINE__, "Function call failed: " #expr, desc, 1) -#define cl_must_fail_(expr, desc) clar__assert((expr) < 0, __FILE__, __func__, __LINE__, "Expected function call to fail: " #expr, desc, 1) -#define cl_assert_(expr, desc) clar__assert((expr) != 0, __FILE__, __func__, __LINE__, "Expression is not true: " #expr, desc, 1) +#define cl_must_pass_(expr, desc) clar__assert((expr) >= 0, CLAR_CURRENT_FILE, CLAR_CURRENT_FUNC, CLAR_CURRENT_LINE, "Function call failed: " #expr, desc, 1) +#define cl_must_fail_(expr, desc) clar__assert((expr) < 0, CLAR_CURRENT_FILE, CLAR_CURRENT_FUNC, CLAR_CURRENT_LINE, "Expected function call to fail: " #expr, desc, 1) +#define cl_assert_(expr, desc) clar__assert((expr) != 0, CLAR_CURRENT_FILE, CLAR_CURRENT_FUNC, CLAR_CURRENT_LINE, "Expression is not true: " #expr, desc, 1) /** * Check macros with explicit error message */ -#define cl_check_pass_(expr, desc) clar__assert((expr) >= 0, __FILE__, __func__, __LINE__, "Function call failed: " #expr, desc, 0) -#define cl_check_fail_(expr, desc) clar__assert((expr) < 0, __FILE__, __func__, __LINE__, "Expected function call to fail: " #expr, desc, 0) -#define cl_check_(expr, desc) clar__assert((expr) != 0, __FILE__, __func__, __LINE__, "Expression is not true: " #expr, desc, 0) +#define cl_check_pass_(expr, desc) clar__assert((expr) >= 0, CLAR_CURRENT_FILE, CLAR_CURRENT_FUNC, CLAR_CURRENT_LINE, "Function call failed: " #expr, desc, 0) +#define cl_check_fail_(expr, desc) clar__assert((expr) < 0, CLAR_CURRENT_FILE, CLAR_CURRENT_FUNC, CLAR_CURRENT_LINE, "Expected function call to fail: " #expr, desc, 0) +#define cl_check_(expr, desc) clar__assert((expr) != 0, CLAR_CURRENT_FILE, CLAR_CURRENT_FUNC, CLAR_CURRENT_LINE, "Expression is not true: " #expr, desc, 0) /** * Assertion macros with no error message @@ -114,33 +124,33 @@ const char *cl_fixture_basename(const char *fixture_name); /** * Forced failure/warning */ -#define cl_fail(desc) clar__fail(__FILE__, __func__, __LINE__, "Test failed.", desc, 1) -#define cl_warning(desc) clar__fail(__FILE__, __func__, __LINE__, "Warning during test execution:", desc, 0) +#define cl_fail(desc) clar__fail(CLAR_CURRENT_FILE, CLAR_CURRENT_FUNC, CLAR_CURRENT_LINE, "Test failed.", desc, 1) +#define cl_warning(desc) clar__fail(CLAR_CURRENT_FILE, CLAR_CURRENT_FUNC, CLAR_CURRENT_LINE, "Warning during test execution:", desc, 0) #define cl_skip() clar__skip() /** * Typed assertion macros */ -#define cl_assert_equal_s(s1,s2) clar__assert_equal(__FILE__,__func__,__LINE__,"String mismatch: " #s1 " != " #s2, 1, "%s", (s1), (s2)) -#define cl_assert_equal_s_(s1,s2,note) clar__assert_equal(__FILE__,__func__,__LINE__,"String mismatch: " #s1 " != " #s2 " (" #note ")", 1, "%s", (s1), (s2)) +#define cl_assert_equal_s(s1,s2) clar__assert_equal(CLAR_CURRENT_FILE,CLAR_CURRENT_FUNC,CLAR_CURRENT_LINE,"String mismatch: " #s1 " != " #s2, 1, "%s", (s1), (s2)) +#define cl_assert_equal_s_(s1,s2,note) clar__assert_equal(CLAR_CURRENT_FILE,CLAR_CURRENT_FUNC,CLAR_CURRENT_LINE,"String mismatch: " #s1 " != " #s2 " (" #note ")", 1, "%s", (s1), (s2)) -#define cl_assert_equal_wcs(wcs1,wcs2) clar__assert_equal(__FILE__,__func__,__LINE__,"String mismatch: " #wcs1 " != " #wcs2, 1, "%ls", (wcs1), (wcs2)) -#define cl_assert_equal_wcs_(wcs1,wcs2,note) clar__assert_equal(__FILE__,__func__,__LINE__,"String mismatch: " #wcs1 " != " #wcs2 " (" #note ")", 1, "%ls", (wcs1), (wcs2)) +#define cl_assert_equal_wcs(wcs1,wcs2) clar__assert_equal(CLAR_CURRENT_FILE,CLAR_CURRENT_FUNC,CLAR_CURRENT_LINE,"String mismatch: " #wcs1 " != " #wcs2, 1, "%ls", (wcs1), (wcs2)) +#define cl_assert_equal_wcs_(wcs1,wcs2,note) clar__assert_equal(CLAR_CURRENT_FILE,CLAR_CURRENT_FUNC,CLAR_CURRENT_LINE,"String mismatch: " #wcs1 " != " #wcs2 " (" #note ")", 1, "%ls", (wcs1), (wcs2)) -#define cl_assert_equal_strn(s1,s2,len) clar__assert_equal(__FILE__,__func__,__LINE__,"String mismatch: " #s1 " != " #s2, 1, "%.*s", (s1), (s2), (int)(len)) -#define cl_assert_equal_strn_(s1,s2,len,note) clar__assert_equal(__FILE__,__func__,__LINE__,"String mismatch: " #s1 " != " #s2 " (" #note ")", 1, "%.*s", (s1), (s2), (int)(len)) +#define cl_assert_equal_strn(s1,s2,len) clar__assert_equal(CLAR_CURRENT_FILE,CLAR_CURRENT_FUNC,CLAR_CURRENT_LINE,"String mismatch: " #s1 " != " #s2, 1, "%.*s", (s1), (s2), (int)(len)) +#define cl_assert_equal_strn_(s1,s2,len,note) clar__assert_equal(CLAR_CURRENT_FILE,CLAR_CURRENT_FUNC,CLAR_CURRENT_LINE,"String mismatch: " #s1 " != " #s2 " (" #note ")", 1, "%.*s", (s1), (s2), (int)(len)) -#define cl_assert_equal_wcsn(wcs1,wcs2,len) clar__assert_equal(__FILE__,__func__,__LINE__,"String mismatch: " #wcs1 " != " #wcs2, 1, "%.*ls", (wcs1), (wcs2), (int)(len)) -#define cl_assert_equal_wcsn_(wcs1,wcs2,len,note) clar__assert_equal(__FILE__,__func__,__LINE__,"String mismatch: " #wcs1 " != " #wcs2 " (" #note ")", 1, "%.*ls", (wcs1), (wcs2), (int)(len)) +#define cl_assert_equal_wcsn(wcs1,wcs2,len) clar__assert_equal(CLAR_CURRENT_FILE,CLAR_CURRENT_FUNC,CLAR_CURRENT_LINE,"String mismatch: " #wcs1 " != " #wcs2, 1, "%.*ls", (wcs1), (wcs2), (int)(len)) +#define cl_assert_equal_wcsn_(wcs1,wcs2,len,note) clar__assert_equal(CLAR_CURRENT_FILE,CLAR_CURRENT_FUNC,CLAR_CURRENT_LINE,"String mismatch: " #wcs1 " != " #wcs2 " (" #note ")", 1, "%.*ls", (wcs1), (wcs2), (int)(len)) -#define cl_assert_equal_i(i1,i2) clar__assert_equal(__FILE__,__func__,__LINE__,#i1 " != " #i2, 1, "%d", (int)(i1), (int)(i2)) -#define cl_assert_equal_i_(i1,i2,note) clar__assert_equal(__FILE__,__func__,__LINE__,#i1 " != " #i2 " (" #note ")", 1, "%d", (i1), (i2)) -#define cl_assert_equal_i_fmt(i1,i2,fmt) clar__assert_equal(__FILE__,__func__,__LINE__,#i1 " != " #i2, 1, (fmt), (int)(i1), (int)(i2)) +#define cl_assert_equal_i(i1,i2) clar__assert_equal(CLAR_CURRENT_FILE,CLAR_CURRENT_FUNC,CLAR_CURRENT_LINE,#i1 " != " #i2, 1, "%d", (int)(i1), (int)(i2)) +#define cl_assert_equal_i_(i1,i2,note) clar__assert_equal(CLAR_CURRENT_FILE,CLAR_CURRENT_FUNC,CLAR_CURRENT_LINE,#i1 " != " #i2 " (" #note ")", 1, "%d", (i1), (i2)) +#define cl_assert_equal_i_fmt(i1,i2,fmt) clar__assert_equal(CLAR_CURRENT_FILE,CLAR_CURRENT_FUNC,CLAR_CURRENT_LINE,#i1 " != " #i2, 1, (fmt), (int)(i1), (int)(i2)) -#define cl_assert_equal_b(b1,b2) clar__assert_equal(__FILE__,__func__,__LINE__,#b1 " != " #b2, 1, "%d", (int)((b1) != 0),(int)((b2) != 0)) +#define cl_assert_equal_b(b1,b2) clar__assert_equal(CLAR_CURRENT_FILE,CLAR_CURRENT_FUNC,CLAR_CURRENT_LINE,#b1 " != " #b2, 1, "%d", (int)((b1) != 0),(int)((b2) != 0)) -#define cl_assert_equal_p(p1,p2) clar__assert_equal(__FILE__,__func__,__LINE__,"Pointer mismatch: " #p1 " != " #p2, 1, "%p", (p1), (p2)) +#define cl_assert_equal_p(p1,p2) clar__assert_equal(CLAR_CURRENT_FILE,CLAR_CURRENT_FUNC,CLAR_CURRENT_LINE,"Pointer mismatch: " #p1 " != " #p2, 1, "%p", (p1), (p2)) void clar__skip(void); diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt new file mode 100644 index 0000000..b72f187 --- /dev/null +++ b/example/CMakeLists.txt @@ -0,0 +1,28 @@ +find_package(Python COMPONENTS Interpreter REQUIRED) + +add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/clar.suite" + COMMAND "${Python_EXECUTABLE}" "${CMAKE_SOURCE_DIR}/generate.py" --output "${CMAKE_CURRENT_BINARY_DIR}" + DEPENDS main.c example.c + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" +) + +add_executable(example) +set_target_properties(example PROPERTIES + C_STANDARD 90 + C_STANDARD_REQUIRED ON + C_EXTENSIONS OFF +) +target_sources(example PRIVATE + main.c + example.c + "${CMAKE_CURRENT_BINARY_DIR}/clar.suite" +) +target_compile_definitions(example PRIVATE) +target_compile_options(example PRIVATE + $,/W4,-Wall> +) +target_include_directories(example PRIVATE + "${CMAKE_SOURCE_DIR}" + "${CMAKE_CURRENT_BINARY_DIR}" +) +target_link_libraries(example clar) diff --git a/example/example.c b/example/example.c new file mode 100644 index 0000000..c07d6bf --- /dev/null +++ b/example/example.c @@ -0,0 +1,6 @@ +#include "clar.h" + +void test_example__simple_assert(void) +{ + cl_assert_equal_i(1, 1); +} diff --git a/test/main.c.sample b/example/main.c similarity index 96% rename from test/main.c.sample rename to example/main.c index a4d91b7..f8def7f 100644 --- a/test/main.c.sample +++ b/example/main.c @@ -5,7 +5,7 @@ * For full terms see the included COPYING file. */ -#include "clar_test.h" +#include "clar.h" /* * Minimal main() for clar tests. diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 7f2c1dc..96abd6e 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,13 +1,15 @@ +add_subdirectory(selftest_suite) + find_package(Python COMPONENTS Interpreter REQUIRED) add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/clar.suite" COMMAND "${Python_EXECUTABLE}" "${CMAKE_SOURCE_DIR}/generate.py" --output "${CMAKE_CURRENT_BINARY_DIR}" - DEPENDS main.c sample.c clar_test.h + DEPENDS main.c selftest.c WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" ) -add_executable(clar_test) -set_target_properties(clar_test PROPERTIES +add_executable(selftest) +set_target_properties(selftest PROPERTIES C_STANDARD 90 C_STANDARD_REQUIRED ON C_EXTENSIONS OFF @@ -15,25 +17,38 @@ set_target_properties(clar_test PROPERTIES # MSVC generates all kinds of warnings. We may want to fix these in the future # and then unconditionally treat warnings as errors. -if(NOT MSVC) - set_target_properties(clar_test PROPERTIES +if (NOT MSVC) + set_target_properties(selftest PROPERTIES COMPILE_WARNING_AS_ERROR ON ) endif() -target_sources(clar_test PRIVATE +target_sources(selftest PRIVATE main.c - sample.c + selftest.c "${CMAKE_CURRENT_BINARY_DIR}/clar.suite" ) -target_compile_definitions(clar_test PRIVATE - CLAR_FIXTURE_PATH="${CMAKE_CURRENT_SOURCE_DIR}/resources/" +target_compile_definitions(selftest PRIVATE + CLAR_FIXTURE_PATH="${CMAKE_CURRENT_SOURCE_DIR}/expected/" ) -target_compile_options(clar_test PRIVATE +target_compile_options(selftest PRIVATE $,/W4,-Wall> ) -target_include_directories(clar_test PRIVATE +target_include_directories(selftest PRIVATE "${CMAKE_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}" ) -target_link_libraries(clar_test clar) +target_link_libraries(selftest clar) + +add_test(NAME build_selftest_suite + COMMAND "${CMAKE_COMMAND}" --build "${CMAKE_BINARY_DIR}" --config "$" --target selftest_suite +) +set_tests_properties(build_selftest_suite PROPERTIES FIXTURES_SETUP clar_test_fixture) + +add_test(NAME build_selftest + COMMAND "${CMAKE_COMMAND}" --build "${CMAKE_BINARY_DIR}" --config "$" --target selftest +) +set_tests_properties(build_selftest PROPERTIES FIXTURES_SETUP clar_test_fixture) + +add_test(NAME selftest COMMAND "${CMAKE_CURRENT_BINARY_DIR}/selftest" "$") +set_tests_properties(selftest PROPERTIES FIXTURES_REQUIRED clar_test_fixture) diff --git a/test/clar_test.h b/test/clar_test.h deleted file mode 100644 index 0fcaa63..0000000 --- a/test/clar_test.h +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright (c) Vicent Marti. All rights reserved. - * - * This file is part of clar, distributed under the ISC license. - * For full terms see the included COPYING file. - */ -#ifndef __CLAR_TEST__ -#define __CLAR_TEST__ - -/* Import the standard clar helper functions */ -#include "clar.h" - -/* Your custom shared includes / defines here */ -extern int global_test_counter; - -#endif diff --git a/test/expected/help b/test/expected/help new file mode 100644 index 0000000..37eaae5 --- /dev/null +++ b/test/expected/help @@ -0,0 +1,12 @@ +Usage: selftest [options] + +Options: + -sname Run only the suite with `name` (can go to individual test name) + -iname Include the suite with `name` + -xname Exclude the suite with `name` + -v Increase verbosity (show suite names) + -q Only report tests that had an error + -Q Quit as soon as a test fails + -t Display results in tap format + -l Print suite names + -r[filename] Write summary file (to the optional filename) diff --git a/test/expected/quiet b/test/expected/quiet new file mode 100644 index 0000000..c1eb689 --- /dev/null +++ b/test/expected/quiet @@ -0,0 +1,80 @@ +Loaded 1 suites: +Started (test status codes: OK='.' FAILURE='F' SKIPPED='S') + 1) Failure: +selftest::suite::1 [file:42] + Function call failed: -1 + + 1) Failure: +selftest::suite::2 [file:42] + Expression is not true: 100 == 101 + + 1) Failure: +selftest::suite::strings [file:42] + String mismatch: "mismatched" != actual ("this one fails") + 'mismatched' != 'expected' (at byte 0) + + 1) Failure: +selftest::suite::strings_with_length [file:42] + String mismatch: "exactly" != actual ("this one fails") + 'exa' != 'exp' (at byte 2) + + 1) Failure: +selftest::suite::int [file:42] + 101 != value ("extra note on failing test") + 101 != 100 + + 1) Failure: +selftest::suite::int_fmt [file:42] + 022 != value + 0022 != 0144 + + 1) Failure: +selftest::suite::bool [file:42] + 0 != value + 0 != 1 + + 1) Failure: +selftest::suite::ptr [file:42] + Pointer mismatch: p1 != p2 + 0x1 != 0x2 + + + + 1) Failure: +selftest::suite::1 [file:42] + Function call failed: -1 + + 2) Failure: +selftest::suite::2 [file:42] + Expression is not true: 100 == 101 + + 3) Failure: +selftest::suite::strings [file:42] + String mismatch: "mismatched" != actual ("this one fails") + 'mismatched' != 'expected' (at byte 0) + + 4) Failure: +selftest::suite::strings_with_length [file:42] + String mismatch: "exactly" != actual ("this one fails") + 'exa' != 'exp' (at byte 2) + + 5) Failure: +selftest::suite::int [file:42] + 101 != value ("extra note on failing test") + 101 != 100 + + 6) Failure: +selftest::suite::int_fmt [file:42] + 022 != value + 0022 != 0144 + + 7) Failure: +selftest::suite::bool [file:42] + 0 != value + 0 != 1 + + 8) Failure: +selftest::suite::ptr [file:42] + Pointer mismatch: p1 != p2 + 0x1 != 0x2 + diff --git a/test/expected/specific_test b/test/expected/specific_test new file mode 100644 index 0000000..afa2150 --- /dev/null +++ b/test/expected/specific_test @@ -0,0 +1,9 @@ +Loaded 1 suites: +Started (test status codes: OK='.' FAILURE='F' SKIPPED='S') +F + + 1) Failure: +selftest::suite::bool [file:42] + 0 != value + 0 != 1 + diff --git a/test/expected/stop_on_failure b/test/expected/stop_on_failure new file mode 100644 index 0000000..1156ade --- /dev/null +++ b/test/expected/stop_on_failure @@ -0,0 +1,8 @@ +Loaded 1 suites: +Started (test status codes: OK='.' FAILURE='F' SKIPPED='S') +F + + 1) Failure: +selftest::suite::1 [file:42] + Function call failed: -1 + diff --git a/test/expected/suite_names b/test/expected/suite_names new file mode 100644 index 0000000..1b0f639 --- /dev/null +++ b/test/expected/suite_names @@ -0,0 +1,2 @@ +Test suites (use -s to run just one): + 0: selftest::suite diff --git a/test/expected/summary.xml b/test/expected/summary.xml new file mode 100644 index 0000000..223ddd3 --- /dev/null +++ b/test/expected/summary.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/expected/summary_with_filename b/test/expected/summary_with_filename new file mode 100644 index 0000000..337838e --- /dev/null +++ b/test/expected/summary_with_filename @@ -0,0 +1,43 @@ +Loaded 1 suites: +Started (test status codes: OK='.' FAILURE='F' SKIPPED='S') +FFFFFFFF + + 1) Failure: +selftest::suite::1 [file:42] + Function call failed: -1 + + 2) Failure: +selftest::suite::2 [file:42] + Expression is not true: 100 == 101 + + 3) Failure: +selftest::suite::strings [file:42] + String mismatch: "mismatched" != actual ("this one fails") + 'mismatched' != 'expected' (at byte 0) + + 4) Failure: +selftest::suite::strings_with_length [file:42] + String mismatch: "exactly" != actual ("this one fails") + 'exa' != 'exp' (at byte 2) + + 5) Failure: +selftest::suite::int [file:42] + 101 != value ("extra note on failing test") + 101 != 100 + + 6) Failure: +selftest::suite::int_fmt [file:42] + 022 != value + 0022 != 0144 + + 7) Failure: +selftest::suite::bool [file:42] + 0 != value + 0 != 1 + + 8) Failure: +selftest::suite::ptr [file:42] + Pointer mismatch: p1 != p2 + 0x1 != 0x2 + +written summary file to different.xml diff --git a/test/expected/summary_without_filename b/test/expected/summary_without_filename new file mode 100644 index 0000000..96df14d --- /dev/null +++ b/test/expected/summary_without_filename @@ -0,0 +1,43 @@ +Loaded 1 suites: +Started (test status codes: OK='.' FAILURE='F' SKIPPED='S') +FFFFFFFF + + 1) Failure: +selftest::suite::1 [file:42] + Function call failed: -1 + + 2) Failure: +selftest::suite::2 [file:42] + Expression is not true: 100 == 101 + + 3) Failure: +selftest::suite::strings [file:42] + String mismatch: "mismatched" != actual ("this one fails") + 'mismatched' != 'expected' (at byte 0) + + 4) Failure: +selftest::suite::strings_with_length [file:42] + String mismatch: "exactly" != actual ("this one fails") + 'exa' != 'exp' (at byte 2) + + 5) Failure: +selftest::suite::int [file:42] + 101 != value ("extra note on failing test") + 101 != 100 + + 6) Failure: +selftest::suite::int_fmt [file:42] + 022 != value + 0022 != 0144 + + 7) Failure: +selftest::suite::bool [file:42] + 0 != value + 0 != 1 + + 8) Failure: +selftest::suite::ptr [file:42] + Pointer mismatch: p1 != p2 + 0x1 != 0x2 + +written summary file to summary.xml diff --git a/test/expected/tap b/test/expected/tap new file mode 100644 index 0000000..6a3b04d --- /dev/null +++ b/test/expected/tap @@ -0,0 +1,81 @@ +TAP version 13 +# start of suite 1: selftest::suite +not ok 1 - selftest::suite::1 + --- + reason: | + Function call failed: -1 + at: + file: 'file' + line: 42 + function: 'func' + --- +not ok 2 - selftest::suite::2 + --- + reason: | + Expression is not true: 100 == 101 + at: + file: 'file' + line: 42 + function: 'func' + --- +not ok 3 - selftest::suite::strings + --- + reason: | + String mismatch: "mismatched" != actual ("this one fails") + 'mismatched' != 'expected' (at byte 0) + at: + file: 'file' + line: 42 + function: 'func' + --- +not ok 4 - selftest::suite::strings_with_length + --- + reason: | + String mismatch: "exactly" != actual ("this one fails") + 'exa' != 'exp' (at byte 2) + at: + file: 'file' + line: 42 + function: 'func' + --- +not ok 5 - selftest::suite::int + --- + reason: | + 101 != value ("extra note on failing test") + 101 != 100 + at: + file: 'file' + line: 42 + function: 'func' + --- +not ok 6 - selftest::suite::int_fmt + --- + reason: | + 022 != value + 0022 != 0144 + at: + file: 'file' + line: 42 + function: 'func' + --- +not ok 7 - selftest::suite::bool + --- + reason: | + 0 != value + 0 != 1 + at: + file: 'file' + line: 42 + function: 'func' + --- +not ok 8 - selftest::suite::ptr + --- + reason: | + Pointer mismatch: p1 != p2 + 0x1 != 0x2 + at: + file: 'file' + line: 42 + function: 'func' + --- +1..8 diff --git a/test/expected/without_arguments b/test/expected/without_arguments new file mode 100644 index 0000000..14fc6c2 --- /dev/null +++ b/test/expected/without_arguments @@ -0,0 +1,42 @@ +Loaded 1 suites: +Started (test status codes: OK='.' FAILURE='F' SKIPPED='S') +FFFFFFFF + + 1) Failure: +selftest::suite::1 [file:42] + Function call failed: -1 + + 2) Failure: +selftest::suite::2 [file:42] + Expression is not true: 100 == 101 + + 3) Failure: +selftest::suite::strings [file:42] + String mismatch: "mismatched" != actual ("this one fails") + 'mismatched' != 'expected' (at byte 0) + + 4) Failure: +selftest::suite::strings_with_length [file:42] + String mismatch: "exactly" != actual ("this one fails") + 'exa' != 'exp' (at byte 2) + + 5) Failure: +selftest::suite::int [file:42] + 101 != value ("extra note on failing test") + 101 != 100 + + 6) Failure: +selftest::suite::int_fmt [file:42] + 022 != value + 0022 != 0144 + + 7) Failure: +selftest::suite::bool [file:42] + 0 != value + 0 != 1 + + 8) Failure: +selftest::suite::ptr [file:42] + Pointer mismatch: p1 != p2 + 0x1 != 0x2 + diff --git a/test/main.c b/test/main.c index 59e56ad..b1ba299 100644 --- a/test/main.c +++ b/test/main.c @@ -1,23 +1,9 @@ -/* - * Copyright (c) Vicent Marti. All rights reserved. - * - * This file is part of clar, distributed under the ISC license. - * For full terms see the included COPYING file. - */ +#include +#include -#include "clar_test.h" +#include "selftest.h" -/* - * Sample main() for clar tests. - * - * You should write your own main routine for clar tests that does specific - * setup and teardown as necessary for your application. The only required - * line is the call to `clar_test(argc, argv)`, which will execute the test - * suite. If you want to check the return value of the test application, - * your main() should return the same value returned by clar_test(). - */ - -int global_test_counter = 0; +const char *selftest_binary_path; #ifdef _WIN32 int __cdecl main(int argc, char *argv[]) @@ -25,16 +11,15 @@ int __cdecl main(int argc, char *argv[]) int main(int argc, char *argv[]) #endif { - int ret; - - /* Your custom initialization here */ - global_test_counter = 0; - - /* Run the test suite */ - ret = clar_test(argc, argv); + if (argc < 2) { + fprintf(stderr, "usage: %s \n", + argv[0]); + exit(1); + } - /* Your custom cleanup here */ - cl_assert_equal_i(8, global_test_counter); + selftest_binary_path = argv[1]; + memmove(argv + 1, argv + 2, argc - 1); + argc -= 1; - return ret; + return clar_test(argc, argv); } diff --git a/test/selftest.c b/test/selftest.c new file mode 100644 index 0000000..c74214c --- /dev/null +++ b/test/selftest.c @@ -0,0 +1,289 @@ +#include +#include +#include +#include + +#include "selftest.h" + +#ifdef _WIN32 +# define WIN32_LEAN_AND_MEAN +# include + +static char *read_full(HANDLE h, int is_pipe) +{ + char *data = NULL; + size_t data_size = 0; + + while (1) { + CHAR buf[4096]; + DWORD bytes_read; + + if (!ReadFile(h, buf, sizeof(buf), &bytes_read, NULL)) { + if (!is_pipe) + cl_fail("Failed reading file handle."); + cl_assert_equal_i(GetLastError(), ERROR_BROKEN_PIPE); + break; + } + if (!bytes_read) + break; + + data = realloc(data, data_size + bytes_read); + cl_assert(data); + memcpy(data + data_size, buf, bytes_read); + data_size += bytes_read; + } + + data = realloc(data, data_size + 1); + cl_assert(data); + data[data_size] = '\0'; + + while (strstr(data, "\r\n")) { + char *ptr = strstr(data, "\r\n"); + memmove(ptr, ptr + 1, strlen(ptr)); + } + + return data; +} + +static char *read_file(const char *path) +{ + char *content; + HANDLE file; + + file = CreateFile(path, GENERIC_READ, FILE_SHARE_READ, NULL, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + cl_assert(file != INVALID_HANDLE_VALUE); + content = read_full(file, 0); + cl_assert_equal_b(1, CloseHandle(file)); + + return content; +} + +static void run(const char *expected_output_file, int expected_error_code, ...) +{ + SECURITY_ATTRIBUTES security_attributes = { 0 }; + PROCESS_INFORMATION process_info = { 0 }; + STARTUPINFO startup_info = { 0 }; + char cmdline[4096] = { 0 }; + char *expected_output = NULL; + char *output = NULL; + HANDLE stdout_write; + HANDLE stdout_read; + DWORD exit_code; + va_list ap; + + /* + * Assemble command line arguments. In theory we'd have to properly + * quote them. In practice none of our tests actually care. + */ + va_start(ap, expected_error_code); + snprintf(cmdline, sizeof(cmdline), "selftest"); + while (1) { + size_t cmdline_len = strlen(cmdline); + const char *arg; + + arg = va_arg(ap, const char *); + if (!arg) + break; + + cl_assert(cmdline_len + strlen(arg) < sizeof(cmdline)); + snprintf(cmdline + cmdline_len, sizeof(cmdline) - cmdline_len, + " %s", arg); + } + va_end(ap); + + /* + * Create a pipe that we will use to read data from the child process. + * The writing side needs to be inheritable such that the child can use + * it as stdout and stderr. The reading side should only be used by the + * parent. + */ + security_attributes.nLength = sizeof(security_attributes); + security_attributes.bInheritHandle = TRUE; + cl_assert_equal_b(1, CreatePipe(&stdout_read, &stdout_write, &security_attributes, 0)); + cl_assert_equal_b(1, SetHandleInformation(stdout_read, HANDLE_FLAG_INHERIT, 0)); + + /* + * Create the child process with our pipe. + */ + startup_info.cb = sizeof(startup_info); + startup_info.hStdError = stdout_write; + startup_info.hStdOutput = stdout_write; + startup_info.dwFlags |= STARTF_USESTDHANDLES; + cl_assert_equal_b(1, CreateProcess(selftest_binary_path, cmdline, NULL, NULL, TRUE, + 0, NULL, NULL, &startup_info, &process_info)); + cl_assert_equal_b(1, CloseHandle(stdout_write)); + + output = read_full(stdout_read, 1); + cl_assert_equal_b(1, CloseHandle(stdout_read)); + cl_assert_equal_b(1, GetExitCodeProcess(process_info.hProcess, &exit_code)); + + expected_output = read_file(cl_fixture(expected_output_file)); + cl_assert_equal_s(output, expected_output); + cl_assert_equal_i(exit_code, expected_error_code); + + free(expected_output); + free(output); +} + +#else +# include +# include +# include +# include +# include + +static char *read_full(int fd) +{ + size_t data_bytes = 0; + char *data = NULL; + + while (1) { + char buf[4096]; + ssize_t n; + + n = read(fd, buf, sizeof(buf)); + if (n < 0) { + if (errno == EAGAIN || errno == EINTR) + continue; + cl_fail("Failed reading from child process."); + } + if (!n) + break; + + data = realloc(data, data_bytes + n); + cl_assert(data); + + memcpy(data + data_bytes, buf, n); + data_bytes += n; + } + + data = realloc(data, data_bytes + 1); + cl_assert(data); + data[data_bytes] = '\0'; + + return data; +} + +static char *read_file(const char *path) +{ + char *data; + int fd; + + fd = open(path, O_RDONLY); + if (fd < 0) + cl_fail("Failed reading expected file."); + + data = read_full(fd); + cl_must_pass(close(fd)); + + return data; +} + +static void run(const char *expected_output_file, int expected_error_code, ...) +{ + const char *argv[16]; + int pipe_fds[2]; + va_list ap; + pid_t pid; + int i; + + va_start(ap, expected_error_code); + argv[0] = "selftest"; + for (i = 1; ; i++) { + cl_assert(i < sizeof(argv) / sizeof(*argv)); + + argv[i] = va_arg(ap, const char *); + if (!argv[i]) + break; + } + va_end(ap); + + cl_must_pass(pipe(pipe_fds)); + + pid = fork(); + if (!pid) { + if (dup2(pipe_fds[1], STDOUT_FILENO) < 0 || + dup2(pipe_fds[1], STDERR_FILENO) < 0 || + close(0) < 0 || + close(pipe_fds[0]) < 0 || + close(pipe_fds[1]) < 0) + exit(1); + + execv(selftest_binary_path, (char **) argv); + exit(1); + } else if (pid > 0) { + pid_t waited_pid; + char *expected_output, *output; + int stat; + + cl_must_pass(close(pipe_fds[1])); + + output = read_full(pipe_fds[0]); + + waited_pid = waitpid(pid, &stat, 0); + cl_assert_equal_i(pid, waited_pid); + cl_assert(WIFEXITED(stat)); + cl_assert_equal_i(WEXITSTATUS(stat), expected_error_code); + + expected_output = read_file(cl_fixture(expected_output_file)); + cl_assert_equal_s(output, expected_output); + + free(expected_output); + free(output); + } else { + cl_fail("Fork failed."); + } +} +#endif + +void test_selftest__help(void) +{ + run("help", 1, "-h", NULL); +} + +void test_selftest__without_arguments(void) +{ + run("without_arguments", 8, NULL); +} + +void test_selftest__specific_test(void) +{ + run("specific_test", 1, "-sselftest::suite::bool", NULL); +} + +void test_selftest__stop_on_failure(void) +{ + run("stop_on_failure", 1, "-Q", NULL); +} + +void test_selftest__quiet(void) +{ + run("quiet", 8, "-q", NULL); +} + +void test_selftest__tap(void) +{ + run("tap", 8, "-t", NULL); +} + +void test_selftest__suite_names(void) +{ + run("suite_names", 0, "-l", NULL); +} + +void test_selftest__summary_without_filename(void) +{ + struct stat st; + run("summary_without_filename", 8, "-r", NULL); + /* The summary contains timestamps, so we cannot verify its contents. */ + cl_must_pass(stat("summary.xml", &st)); +} + +void test_selftest__summary_with_filename(void) +{ + struct stat st; + run("summary_with_filename", 8, "-rdifferent.xml", NULL); + /* The summary contains timestamps, so we cannot verify its contents. */ + cl_must_pass(stat("different.xml", &st)); +} diff --git a/test/selftest.h b/test/selftest.h new file mode 100644 index 0000000..220a350 --- /dev/null +++ b/test/selftest.h @@ -0,0 +1,3 @@ +#include "clar.h" + +extern const char *selftest_binary_path; diff --git a/test/selftest_suite/CMakeLists.txt b/test/selftest_suite/CMakeLists.txt new file mode 100644 index 0000000..9597d67 --- /dev/null +++ b/test/selftest_suite/CMakeLists.txt @@ -0,0 +1,40 @@ +find_package(Python COMPONENTS Interpreter REQUIRED) + +add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/clar.suite" + COMMAND "${Python_EXECUTABLE}" "${CMAKE_SOURCE_DIR}/generate.py" --output "${CMAKE_CURRENT_BINARY_DIR}" + DEPENDS main.c selftest_suite.c + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" +) + +add_executable(selftest_suite) +set_target_properties(selftest_suite PROPERTIES + C_STANDARD 90 + C_STANDARD_REQUIRED ON + C_EXTENSIONS OFF +) + +# MSVC generates all kinds of warnings. We may want to fix these in the future +# and then unconditionally treat warnings as errors. +if(NOT MSVC) + set_target_properties(selftest_suite PROPERTIES + COMPILE_WARNING_AS_ERROR ON + ) +endif() + +target_sources(selftest_suite PRIVATE + main.c + selftest_suite.c + "${CMAKE_CURRENT_BINARY_DIR}/clar.suite" +) +target_compile_definitions(selftest_suite PRIVATE + CLAR_FIXTURE_PATH="${CMAKE_CURRENT_SOURCE_DIR}/resources/" + CLAR_SELFTEST +) +target_compile_options(selftest_suite PRIVATE + $,/W4,-Wall> +) +target_include_directories(selftest_suite PRIVATE + "${CMAKE_SOURCE_DIR}" + "${CMAKE_CURRENT_BINARY_DIR}" +) +target_link_libraries(selftest_suite clar) diff --git a/test/selftest_suite/main.c b/test/selftest_suite/main.c new file mode 100644 index 0000000..3ab581d --- /dev/null +++ b/test/selftest_suite/main.c @@ -0,0 +1,27 @@ +/* + * Copyright (c) Vicent Marti. All rights reserved. + * + * This file is part of clar, distributed under the ISC license. + * For full terms see the included COPYING file. + */ + +#include "clar.h" + +/* + * Selftest main() for clar tests. + * + * You should write your own main routine for clar tests that does specific + * setup and teardown as necessary for your application. The only required + * line is the call to `clar_test(argc, argv)`, which will execute the test + * suite. If you want to check the return value of the test application, + * your main() should return the same value returned by clar_test(). + */ + +#ifdef _WIN32 +int __cdecl main(int argc, char *argv[]) +#else +int main(int argc, char *argv[]) +#endif +{ + return clar_test(argc, argv); +} diff --git a/test/resources/test/file b/test/selftest_suite/resources/test/file similarity index 100% rename from test/resources/test/file rename to test/selftest_suite/resources/test/file diff --git a/test/sample.c b/test/selftest_suite/selftest_suite.c similarity index 72% rename from test/sample.c rename to test/selftest_suite/selftest_suite.c index faa1209..37b2fca 100644 --- a/test/sample.c +++ b/test/selftest_suite/selftest_suite.c @@ -1,6 +1,7 @@ -#include "clar_test.h" #include +#include "clar.h" + static int file_size(const char *filename) { struct stat st; @@ -10,19 +11,14 @@ static int file_size(const char *filename) return -1; } -void test_sample__initialize(void) -{ - global_test_counter++; -} - -void test_sample__cleanup(void) +void test_selftest_suite__cleanup(void) { cl_fixture_cleanup("test"); cl_assert(file_size("test/file") == -1); } -void test_sample__1(void) +void test_selftest_suite__1(void) { cl_assert(1); cl_must_pass(0); /* 0 == success */ @@ -30,7 +26,7 @@ void test_sample__1(void) cl_must_pass(-1); /* demonstrate a failing call */ } -void test_sample__2(void) +void test_selftest_suite__2(void) { cl_fixture_sandbox("test"); @@ -39,7 +35,7 @@ void test_sample__2(void) cl_assert(100 == 101); } -void test_sample__strings(void) +void test_selftest_suite__strings(void) { const char *actual = "expected"; cl_assert_equal_s("expected", actual); @@ -47,7 +43,7 @@ void test_sample__strings(void) cl_assert_equal_s_("mismatched", actual, "this one fails"); } -void test_sample__strings_with_length(void) +void test_selftest_suite__strings_with_length(void) { const char *actual = "expected"; cl_assert_equal_strn("expected_", actual, 8); @@ -56,29 +52,29 @@ void test_sample__strings_with_length(void) cl_assert_equal_strn_("exactly", actual, 3, "this one fails"); } -void test_sample__int(void) +void test_selftest_suite__int(void) { int value = 100; cl_assert_equal_i(100, value); cl_assert_equal_i_(101, value, "extra note on failing test"); } -void test_sample__int_fmt(void) +void test_selftest_suite__int_fmt(void) { int value = 100; cl_assert_equal_i_fmt(022, value, "%04o"); } -void test_sample__bool(void) +void test_selftest_suite__bool(void) { int value = 100; cl_assert_equal_b(1, value); /* test equality as booleans */ cl_assert_equal_b(0, value); } -void test_sample__ptr(void) +void test_selftest_suite__ptr(void) { - const char *actual = "expected"; - cl_assert_equal_p(actual, actual); /* pointers to same object */ - cl_assert_equal_p(&actual, actual); + void *p1 = (void *)0x1, *p2 = (void *)0x2; + cl_assert_equal_p(p1, p1); /* pointers to same object */ + cl_assert_equal_p(p1, p2); }