diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 207dc3ef..c845f41e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -195,6 +195,30 @@ jobs: run: | docker run --rm --volume $(pwd):/note-c/ --workdir /note-c/ --entrypoint ./scripts/run_cppcheck.sh ghcr.io/blues/note_c_ci:latest + run_macos_unit_tests: + runs-on: macos-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Configure + run: cmake -B build -DNOTE_C_BUILD_TESTS=ON + + - name: Build + run: cmake --build build --parallel $(sysctl -n hw.ncpu) + + # NOTE: ctest is not run here. A subset of tests that rely on FFF + # symbol interposition fail on macOS due to two-level namespace + # linking. The build step above verifies that all sources and tests + # compile and link successfully. + # + # Even with -Wl,-flat_namespace, intra-library calls within the + # note_c dylib are resolved at link time as direct calls, so FFF + # fakes in the test executable cannot interpose them. Fully fixing + # this would require linking note-c as a static library for tests + # or using a different mocking approach on macOS. + publish_ci_image: runs-on: ubuntu-latest # Make sure unit tests unit tests passed before publishing. diff --git a/CMakeLists.txt b/CMakeLists.txt index 01bd3fa6..2eaa1a70 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.20) +cmake_minimum_required(VERSION 3.21) cmake_policy(SET CMP0095 NEW) if ("${CMAKE_BINARY_DIR}" STREQUAL "${CMAKE_SOURCE_DIR}") @@ -16,7 +16,7 @@ if(NOT EXISTS ${PROJECT_BINARY_DIR}/.gitignore) endif() option(NOTE_C_BUILD_DOCS "Build docs." OFF) -option(NOTE_C_BUILD_TESTS "Build tests." ON) +option(NOTE_C_BUILD_TESTS "Build tests." ${PROJECT_IS_TOP_LEVEL}) option(NOTE_C_COVERAGE "Compile for test NOTE_C_COVERAGE reporting." OFF) option(NOTE_C_LOW_MEM "Build the library tailored for low memory usage." OFF) option(NOTE_C_MEM_CHECK "Run tests with Valgrind." OFF) @@ -27,61 +27,62 @@ option(NOTE_C_SINGLE_PRECISION "Use single precision for JSON floating point num option(NOTE_C_HEARTBEAT_CALLBACK "Enable heartbeat callback support." OFF) set(NOTE_C_SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR}) -add_library(note_c SHARED) -target_sources( - note_c - PRIVATE - ${NOTE_C_SRC_DIR}/n_atof.c - ${NOTE_C_SRC_DIR}/n_b64.c - ${NOTE_C_SRC_DIR}/n_cjson.c - ${NOTE_C_SRC_DIR}/n_cjson_helpers.c - ${NOTE_C_SRC_DIR}/n_cobs.c - ${NOTE_C_SRC_DIR}/n_const.c - ${NOTE_C_SRC_DIR}/n_ftoa.c - ${NOTE_C_SRC_DIR}/n_helpers.c - ${NOTE_C_SRC_DIR}/n_hooks.c - ${NOTE_C_SRC_DIR}/n_i2c.c - ${NOTE_C_SRC_DIR}/n_md5.c - ${NOTE_C_SRC_DIR}/n_printf.c - ${NOTE_C_SRC_DIR}/n_request.c - ${NOTE_C_SRC_DIR}/n_serial.c - ${NOTE_C_SRC_DIR}/n_str.c -) -target_compile_options( - note_c - PRIVATE - -Wall - -Wextra - -Wpedantic - -Werror - -Og - -ggdb - PUBLIC - -m32 - -mfpmath=sse - -msse2 -) -target_include_directories( - note_c - PUBLIC ${NOTE_C_SRC_DIR} -) -target_link_directories( - note_c - PUBLIC - /lib32 - /usr/lib32 - /usr/lib/gcc/x86_64-linux-gnu/12/32 -) -target_link_options( - note_c - PUBLIC - -m32 - -Wl,-melf_i386 + +# Detect whether the system already provides strlcpy/strlcat (macOS, *BSD). +# When present, we skip the bundled implementations in n_str.c and define +# guards so note.h doesn't redeclare them (which collides with the platform's +# fortified macros). +include(CheckSymbolExists) +check_symbol_exists(strlcpy "string.h" HAVE_STRLCPY) +check_symbol_exists(strlcat "string.h" HAVE_STRLCAT) + +set(NOTE_C_SOURCES + ${NOTE_C_SRC_DIR}/n_atof.c + ${NOTE_C_SRC_DIR}/n_b64.c + ${NOTE_C_SRC_DIR}/n_cjson.c + ${NOTE_C_SRC_DIR}/n_cjson_helpers.c + ${NOTE_C_SRC_DIR}/n_cobs.c + ${NOTE_C_SRC_DIR}/n_const.c + ${NOTE_C_SRC_DIR}/n_ftoa.c + ${NOTE_C_SRC_DIR}/n_helpers.c + ${NOTE_C_SRC_DIR}/n_hooks.c + ${NOTE_C_SRC_DIR}/n_i2c.c + ${NOTE_C_SRC_DIR}/n_md5.c + ${NOTE_C_SRC_DIR}/n_printf.c + ${NOTE_C_SRC_DIR}/n_request.c + ${NOTE_C_SRC_DIR}/n_serial.c ) +# n_str.c provides weak strlcpy/strlcat. On systems that already have them +# the file won't compile because the platform headers define these as +# fortified macros that conflict with the function definitions. +if(NOT HAVE_STRLCPY OR NOT HAVE_STRLCAT) + list(APPEND NOTE_C_SOURCES ${NOTE_C_SRC_DIR}/n_str.c) +endif() +# --------------------------------------------------------------------------- +# note_c_lib — static library for downstream consumers. +# +# Projects that include note-c as a submodule can simply do: +# +# add_subdirectory(note-c) +# target_link_libraries(my_app PRIVATE note_c_lib) +# +# This target carries the include path and platform compile definitions +# (HAVE_STRLCPY, etc.) but no test flags, 32-bit cross-compile options, +# or other CI-specific settings. +# --------------------------------------------------------------------------- +add_library(note_c_lib STATIC ${NOTE_C_SOURCES}) +target_include_directories(note_c_lib PUBLIC ${NOTE_C_SRC_DIR}) + +if(HAVE_STRLCPY) + target_compile_definitions(note_c_lib PUBLIC HAVE_STRLCPY) +endif() +if(HAVE_STRLCAT) + target_compile_definitions(note_c_lib PUBLIC HAVE_STRLCAT) +endif() if(NOTE_C_LOW_MEM) target_compile_definitions( - note_c + note_c_lib PUBLIC NOTE_C_LOW_MEM ) @@ -90,7 +91,7 @@ else() # about an empty translation unit, so we only add it to the build if # NOTE_C_LOW_MEM is false. target_sources( - note_c + note_c_lib PRIVATE ${NOTE_C_SRC_DIR}/n_ua.c ) @@ -98,7 +99,7 @@ endif() if(NOTE_C_NO_LIBC) target_link_options( - note_c + note_c_lib PRIVATE -nostdlib -nodefaultlibs @@ -108,7 +109,7 @@ endif() if(NOTE_NODEBUG) target_compile_definitions( - note_c + note_c_lib PUBLIC NOTE_NODEBUG ) @@ -116,7 +117,7 @@ endif() if(NOTE_C_SHOW_MALLOC) target_compile_definitions( - note_c + note_c_lib PUBLIC NOTE_C_SHOW_MALLOC ) @@ -124,7 +125,7 @@ endif() if(NOTE_C_SINGLE_PRECISION) target_compile_definitions( - note_c + note_c_lib PUBLIC NOTE_C_SINGLE_PRECISION ) @@ -132,13 +133,124 @@ endif() if(NOTE_C_HEARTBEAT_CALLBACK) target_compile_definitions( - note_c + note_c_lib PUBLIC NOTE_C_HEARTBEAT_CALLBACK ) endif() +# --------------------------------------------------------------------------- +# note_c — shared library used by the unit test harness. +# +# This target adds -Werror, 32-bit cross-compile flags (Linux CI), FFF +# interposition support (macOS), and the NOTE_C_TEST define. It is NOT +# intended for downstream consumption. +# --------------------------------------------------------------------------- if(NOTE_C_BUILD_TESTS) + add_library(note_c SHARED ${NOTE_C_SOURCES}) + target_include_directories(note_c PUBLIC ${NOTE_C_SRC_DIR}) + + if(HAVE_STRLCPY) + target_compile_definitions(note_c PUBLIC HAVE_STRLCPY) + endif() + if(HAVE_STRLCAT) + target_compile_definitions(note_c PUBLIC HAVE_STRLCAT) + endif() + + target_compile_options( + note_c + PRIVATE + -Wall + -Wextra + -Wpedantic + -Werror + -Og + -ggdb + ) + # The Linux CI runs tests in 32-bit mode. These flags are x86-specific and + # unavailable on Apple toolchains or non-x86 architectures. + if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|i[3-6]86") + target_compile_options(note_c PUBLIC -m32 -mfpmath=sse -msse2) + endif() + if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|i[3-6]86") + target_link_directories( + note_c + PUBLIC + /lib32 + /usr/lib32 + /usr/lib/gcc/x86_64-linux-gnu/12/32 + ) + target_link_options( + note_c + PUBLIC + -m32 + -Wl,-melf_i386 + ) + endif() + if(APPLE) + # Use flat namespace so test fakes (FFF) can interpose library symbols + target_link_options(note_c PRIVATE -Wl,-flat_namespace) + endif() + + if(NOTE_C_LOW_MEM) + target_compile_definitions( + note_c + PUBLIC + NOTE_C_LOW_MEM + ) + else() + # This file is empty if NOTE_C_LOW_MEM is defined, which leads to a warning + # about an empty translation unit, so we only add it to the build if + # NOTE_C_LOW_MEM is false. + target_sources( + note_c + PRIVATE + ${NOTE_C_SRC_DIR}/n_ua.c + ) + endif() + + if(NOTE_C_NO_LIBC) + target_link_options( + note_c + PRIVATE + -nostdlib + -nodefaultlibs + LINKER:--no-undefined + ) + endif() + + if(NOTE_NODEBUG) + target_compile_definitions( + note_c + PUBLIC + NOTE_NODEBUG + ) + endif() + + if(NOTE_C_SHOW_MALLOC) + target_compile_definitions( + note_c + PUBLIC + NOTE_C_SHOW_MALLOC + ) + endif() + + if(NOTE_C_SINGLE_PRECISION) + target_compile_definitions( + note_c + PUBLIC + NOTE_C_SINGLE_PRECISION + ) + endif() + + if(NOTE_C_HEARTBEAT_CALLBACK) + target_compile_definitions( + note_c + PUBLIC + NOTE_C_HEARTBEAT_CALLBACK + ) + endif() + # Including CTest here rather than in test/CMakeLists.txt allows us to run # ctest from the root build directory (e.g. build/ instead of build/test/). # We also need to set MEMORYCHECK_COMMAND_OPTIONS before including this. diff --git a/n_helpers.c b/n_helpers.c index 3c02cd61..3a46c2e3 100644 --- a/n_helpers.c +++ b/n_helpers.c @@ -2403,7 +2403,7 @@ uint32_t NoteMemAvailable(void) objHeader *lastObj = NULL; static long int maxsize = 35000; for (long int i=maxsize; i>=(long int)sizeof(objHeader); i=i-sizeof(objHeader)) { - for (long int j=0;; j++) { + for (;;) { objHeader *thisObj; thisObj = (objHeader *) _Malloc(i); if (thisObj == NULL) { @@ -2416,16 +2416,8 @@ uint32_t NoteMemAvailable(void) } // Free the objects backwards - long int lastLength = 0; - long int lastLengthCount = 0; uint32_t total = 0; while (lastObj != NULL) { - if (lastObj->length != lastLength) { - lastLength = lastObj->length; - lastLengthCount = 1; - } else { - lastLengthCount++; - } objHeader *thisObj = lastObj; lastObj = lastObj->prev; total += thisObj->length; diff --git a/n_ua.c b/n_ua.c index 91815291..c270373b 100644 --- a/n_ua.c +++ b/n_ua.c @@ -92,9 +92,9 @@ __attribute__((weak)) void NoteUserAgentUpdate(J *ua) */ /**************************************************************************/ #if defined(_MSC_VER) -J *NoteUserAgent() +J *NoteUserAgent(void) #else -__attribute__((weak)) J *NoteUserAgent() +__attribute__((weak)) J *NoteUserAgent(void) #endif { diff --git a/scripts/check_libc_dependencies.sh b/scripts/check_libc_dependencies.sh index f3c7fadc..498f3686 100755 --- a/scripts/check_libc_dependencies.sh +++ b/scripts/check_libc_dependencies.sh @@ -30,6 +30,8 @@ LIBC_WHITELIST=( "strchr" "strcmp" "strlen" + "strlcpy" # bundled in n_str.c; also available in glibc 2.38+ + "strlcat" # bundled in n_str.c; also available in glibc 2.38+ "strncmp" "strstr" "strtol" # required by atoi in NoteGenEnvInt diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index f4a0c38a..a74e6d23 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -16,8 +16,8 @@ FetchContent_Declare( ) FetchContent_MakeAvailable(Catch2) -# Add specific build flags for Catch2 -if(TARGET Catch2) +# Add specific build flags for Catch2 (32-bit mode for Linux x86 CI only) +if(TARGET Catch2 AND CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|i[3-6]86") target_compile_options(Catch2 PUBLIC -m32 @@ -47,6 +47,10 @@ macro(add_test TEST_NAME) PRIVATE -Og PRIVATE -ggdb ) + if(APPLE) + # Allow test fakes (FFF) to interpose symbols from the note_c dylib + target_link_options(${TEST_NAME} PRIVATE -Wl,-flat_namespace) + endif() list(APPEND TEST_TARGETS ${TEST_NAME}) @@ -239,8 +243,12 @@ if(NOTE_C_COVERAGE) # ourselves, so we don't care about coverage for them. set( EXCLUDE_FROM_COVERAGE - "n_atof.c;n_b64.c;n_cjson.c;n_ftoa.c;n_md5.c;n_str.c" + "n_atof.c;n_b64.c;n_cjson.c;n_ftoa.c;n_md5.c" ) + # n_str.c is only compiled when the system lacks strlcpy/strlcat. + if(NOT HAVE_STRLCPY OR NOT HAVE_STRLCAT) + list(APPEND EXCLUDE_FROM_COVERAGE "n_str.c") + endif() foreach(EXCLUDE_FILE ${EXCLUDE_FROM_COVERAGE}) string(APPEND LCOV_EXCLUDE "--exclude '*/${EXCLUDE_FILE}' ") endforeach() diff --git a/test/integration/CMakeLists.txt b/test/integration/CMakeLists.txt new file mode 100644 index 00000000..e7973225 --- /dev/null +++ b/test/integration/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.21) +project(note_c_integration_test LANGUAGES C) + +# Pull in note-c as a subdirectory, the way a downstream project would. +# NOTE_C_BUILD_TESTS defaults to OFF because we are not the top-level project. +add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../.. note-c) + +add_executable(link_test link_test.c) +target_link_libraries(link_test PRIVATE note_c_lib) + +enable_testing() +add_test(NAME link_test COMMAND link_test) diff --git a/test/integration/link_test.c b/test/integration/link_test.c new file mode 100644 index 00000000..56870f50 --- /dev/null +++ b/test/integration/link_test.c @@ -0,0 +1,39 @@ +// Integration test: verify that note_c_lib links correctly and key +// symbols are callable from a downstream project. + +#include +#include +#include +#include "note.h" + +int main(void) +{ + // Exercise a representative set of note-c API functions to prove + // the static library links and the public headers are usable. + + // Register standard malloc/free so note-c can allocate memory. + NoteSetFnDefault(malloc, free, NULL, NULL); + + // NoteNewRequest creates a JSON request object. + J *req = NoteNewRequest("hub.status"); + if (req == NULL) { + fprintf(stderr, "NoteNewRequest returned NULL\n"); + return 1; + } + + // JAddStringToObject exercises cJSON helpers. + JAddStringToObject(req, "test", "value"); + + // Read it back. + const char *val = JGetString(req, "test"); + if (val == NULL || strcmp(val, "value") != 0) { + fprintf(stderr, "JGetString mismatch\n"); + JDelete(req); + return 1; + } + + JDelete(req); + + printf("note_c_lib link test passed\n"); + return 0; +}