project(tests C)

include_directories(
  ${CMAKE_BINARY_DIR}
  ${CMAKE_CURRENT_SOURCE_DIR}
  ${CMAKE_SOURCE_DIR}/src
  ${CMOCKA_INCLUDE_DIR}
)

# Required for cmocka >= 1.1.6
if (TARGET cmocka::cmocka)
    set(CMOCKA_LIBRARY cmocka::cmocka)
endif()

# Check for newer cmocka functions to avoid deprecated warnings
include(CheckLibraryExists)
set(CMAKE_REQUIRED_LIBRARIES ${CMOCKA_LIBRARY})
check_library_exists("${CMOCKA_LIBRARY}" _assert_uint_in_range "" HAVE_ASSERT_UINT_IN_RANGE)

set(TORTURE_LIBRARY torture)

# RFC862 echo server
add_executable(echo_srv echo_srv.c)
target_compile_options(echo_srv
                       PRIVATE
                           ${DEFAULT_C_COMPILE_FLAGS}
                           -D_GNU_SOURCE)
target_link_libraries(echo_srv ${SWRAP_REQUIRED_LIBRARIES})
if (DEFINED DEFAULT_LINK_FLAGS)
    set_target_properties(echo_srv
                          PROPERTIES
                              LINK_FLAGS ${DEFAULT_LINK_FLAGS})
endif()

# Helper tool to calculate optimal unix_scm_rights_payload configuration
add_executable(calc_unix_scm_rights_payload calc_unix_scm_rights_payload.c)
target_compile_options(calc_unix_scm_rights_payload
                       PRIVATE
                           ${DEFAULT_C_COMPILE_FLAGS}
                           -D_GNU_SOURCE)
target_include_directories(calc_unix_scm_rights_payload
                           PRIVATE
                               ${CMAKE_BINARY_DIR})
target_link_libraries(calc_unix_scm_rights_payload
    ${SWRAP_REQUIRED_LIBRARIES}
    ${CMAKE_THREAD_LIBS_INIT})
if (DEFINED DEFAULT_LINK_FLAGS)
    set_target_properties(calc_unix_scm_rights_payload
                          PROPERTIES
                              LINK_FLAGS ${DEFAULT_LINK_FLAGS})
endif()

add_library(${TORTURE_LIBRARY} STATIC torture.c)
target_compile_options(${TORTURE_LIBRARY}
                       PRIVATE
                           ${DEFAULT_C_COMPILE_FLAGS}
                           -D_GNU_SOURCE)
target_link_libraries(${TORTURE_LIBRARY}
    ${CMOCKA_LIBRARY}
    ${SWRAP_REQUIRED_LIBRARIES}
    ${CMAKE_THREAD_LIBS_INIT})

add_library(swrap_fake_uid_wrapper SHARED swrap_fake_uid_wrapper.c)
target_compile_options(swrap_fake_uid_wrapper
                       PRIVATE
                           ${DEFAULT_C_COMPILE_FLAGS}
                           -D_GNU_SOURCE)
#target_include_directories(swrap_fake_uid_wrapper
#        PRIVATE ${CMAKE_BINARY_DIR} ${CMOCKA_INCLUDE_DIR})
set(SWRAP_FAKE_UID_WRAPPER_LOCATION "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}swrap_fake_uid_wrapper${CMAKE_SHARED_LIBRARY_SUFFIX}")

set(SWRAP_THREADED_TESTS
    test_thread_sockets
    test_thread_echo_tcp_connect
    test_thread_echo_tcp_write_read
    test_thread_echo_tcp_sendmsg_recvmsg
    test_thread_echo_udp_send_recv)

set(SWRAP_TESTS
    test_ioctl
    test_tcp_listen
    test_tcp_dup2
    test_fcntl
    test_fcntl_lock
    test_echo_tcp_connect
    test_echo_tcp_bind
    test_echo_tcp_socket_options
    test_echo_tcp_sendmsg_recvmsg
    test_echo_tcp_sendmmsg_recvmmsg
    test_echo_tcp_write_read
    test_echo_tcp_poll
    test_echo_tcp_writev_readv
    test_echo_tcp_get_peer_sock_name
    test_echo_udp_sendto_recvfrom
    test_echo_udp_send_recv
    test_echo_udp_sendmsg_recvmsg
    test_max_sockets
    test_public_functions
    test_close_failure
    test_tcp_socket_overwrite
    test_syscall_uwrap
    ${SWRAP_THREADED_TESTS})

if (HAVE_STRUCT_MSGHDR_MSG_CONTROL)
    set(SWRAP_TESTS ${SWRAP_TESTS} test_sendmsg_recvmsg_fd test_echo_tcp_sendmsg_recvmsg_fd)
endif (HAVE_STRUCT_MSGHDR_MSG_CONTROL)

function(ADD_CMOCKA_TEST_ENVIRONMENT _TEST_NAME)
    if (CMAKE_BUILD_TYPE)
        string(TOLOWER "${CMAKE_BUILD_TYPE}" CMAKE_BUILD_TYPE_LOWER)
        if (CMAKE_BUILD_TYPE_LOWER STREQUAL "addresssanitizer")
            find_library(ASAN_LIBRARY
                         NAMES asan)
            if (NOT ASAN_LIBRARY)
                foreach(version RANGE 10 1)
                    if (NOT ASAN_LIBRARY)
                        find_library(ASAN_LIBRARY libasan.so.${version})
                    endif()
                endforeach()
            endif()
        endif()
    endif()

    if (ASAN_LIBRARY)
        list(APPEND PRELOAD_LIBRARIES ${ASAN_LIBRARY})
    endif()
    list(APPEND PRELOAD_LIBRARIES ${SWRAP_FAKE_UID_WRAPPER_LOCATION})
    list(APPEND PRELOAD_LIBRARIES ${SOCKET_WRAPPER_LOCATION})

    if (OSX)
        set(TORTURE_ENVIRONMENT "DYLD_FORCE_FLAT_NAMESPACE=1;DYLD_INSERT_LIBRARIES=${SOCKET_WRAPPER_LOCATION}")
    else ()
        string(REPLACE ";" ":" _TMP_ENV "${PRELOAD_LIBRARIES}")
        set(TORTURE_ENVIRONMENT "LD_PRELOAD=${_TMP_ENV}")
    endif()

    if (CMAKE_BUILD_TYPE)
        string(TOLOWER "${CMAKE_BUILD_TYPE}" CMAKE_BUILD_TYPE_LOWER)
        if (CMAKE_BUILD_TYPE_LOWER STREQUAL "addresssanitizer" OR
            CMAKE_BUILD_TYPE_LOWER STREQUAL "threadsanitizer" OR
            CMAKE_BUILD_TYPE_LOWER STREQUAL "undefinedsanitizer")
            list(APPEND TORTURE_ENVIRONMENT "SOCKET_WRAPPER_DISABLE_DEEPBIND=1")
        endif()
    endif()

    set_property(TEST
                    ${_TEST_NAME}
                PROPERTY
                    ENVIRONMENT "${TORTURE_ENVIRONMENT}")
endfunction()

if (CMAKE_SIZEOF_VOID_P EQUAL 4)
    execute_process(
        COMMAND getconf LFS_CFLAGS
        OUTPUT_VARIABLE GETCONF_LFS_CFLAGS
        OUTPUT_STRIP_TRAILING_WHITESPACE)
    # Create a list from the string
    set(LFS_CFLAGS)
    if (GETCONF_LFS_CFLAGS)
        string(REPLACE " " ";" LFS_CFLAGS ${GETCONF_LFS_CFLAGS})
    endif()
    message(STATUS "Enabling large file support for tests: ${LFS_CFLAGS}")
endif()

foreach(_SWRAP_TEST ${SWRAP_TESTS})
    add_cmocka_test(${_SWRAP_TEST}
                    SOURCES ${_SWRAP_TEST}.c
                    COMPILE_OPTIONS ${DEFAULT_C_COMPILE_FLAGS} -D_GNU_SOURCE ${LFS_CFLAGS}
                    LINK_LIBRARIES ${TORTURE_LIBRARY} ${CMOCKA_LIBRARY} socket_wrapper_noop
                    LINK_OPTIONS ${DEFAULT_LINK_FLAGS})
    add_cmocka_test_environment(${_SWRAP_TEST})
endforeach()

# Add compile definition for test that needs to check for newer cmocka function
if (HAVE_ASSERT_UINT_IN_RANGE)
    target_compile_definitions(test_echo_tcp_get_peer_sock_name PRIVATE HAVE_ASSERT_UINT_IN_RANGE)
endif()

if (HELGRIND_TESTING)
    find_program(VALGRIND_EXECUTABLE valgrind)
    if (VALGRIND_EXECUTABLE)
        set(VALGRIND_HELGRIND_OPTIONS -v --trace-children=yes --tool=helgrind --error-exitcode=1 --read-var-info=yes --suppressions=${CMAKE_CURRENT_SOURCE_DIR}/helgrind.supp)

        foreach(_TEST ${SWRAP_THREADED_TESTS})
            set(_HELGRIND_TEST "helgrind_${_TEST}")

            add_test(NAME ${_HELGRIND_TEST} COMMAND ${VALGRIND_EXECUTABLE} ${VALGRIND_HELGRIND_OPTIONS} ${CMAKE_CURRENT_BINARY_DIR}/${_TEST})
            if (OSX)
                set_property(
                    TEST
                        ${_HELGRIND_TEST}
                    PROPERTY
                        ENVIRONMENT DYLD_FORCE_FLAT_NAMESPACE=1;DYLD_INSERT_LIBRARIES=${SOCKET_WRAPPER_LOCATION})
            else ()
                set_property(
                    TEST
                        ${_HELGRIND_TEST}
                    PROPERTY
                        ENVIRONMENT LD_PRELOAD=${SOCKET_WRAPPER_LOCATION} SOCKET_WRAPPER_DISABLE_DEEPBIND=1)
            endif()
        endforeach()
    endif()
endif()

# test_swrap_unit (don't use LFS)
add_cmocka_test(test_swrap_unit
                SOURCES test_swrap_unit.c
                COMPILE_OPTIONS ${DEFAULT_C_COMPILE_FLAGS} -D_GNU_SOURCE
                LINK_LIBRARIES ${TORTURE_LIBRARY} ${CMOCKA_LIBRARY} socket_wrapper_noop
                LINK_OPTIONS ${DEFAULT_LINK_FLAGS})
add_cmocka_test_environment(test_swrap_unit)

# test_fork_pthread
add_library(thread_deadlock SHARED thread_deadlock.c)
target_link_libraries(thread_deadlock ${CMAKE_THREAD_LIBS_INIT})
target_compile_options(thread_deadlock PRIVATE ${DEFAULT_C_COMPILE_FLAGS})

add_cmocka_test(test_fork_thread_deadlock
                SOURCES test_fork_thread_deadlock.c
                COMPILE_OPTIONS ${DEFAULT_C_COMPILE_FLAGS} -D_GNU_SOURCE
                LINK_LIBRARIES ${TORTURE_LIBRARY} ${CMOCKA_LIBRARY} thread_deadlock
                LINK_OPTIONS ${DEFAULT_LINK_FLAGS})
add_cmocka_test_environment(test_fork_thread_deadlock)

# Python concurrency reproducer test
# Skip this test when building with ThreadSanitizer as it causes issues
if (CMAKE_BUILD_TYPE)
    string(TOLOWER "${CMAKE_BUILD_TYPE}" CMAKE_BUILD_TYPE_LOWER_FOR_PYTHON)
endif()

find_package(Python3 COMPONENTS Interpreter)
if (Python3_FOUND AND NOT CMAKE_BUILD_TYPE_LOWER_FOR_PYTHON STREQUAL "threadsanitizer")
    # Create wrapper script that sets up temporary directory
    set(PYTHON_TEST_WRAPPER "${CMAKE_CURRENT_BINARY_DIR}/run_python_concurrency_test.sh")
    file(WRITE ${PYTHON_TEST_WRAPPER}
"#!/bin/bash

TMPDIR=\$(mktemp -d /tmp/swrap_python_XXXXXX)
trap 'rm -rf \$TMPDIR' EXIT

export SOCKET_WRAPPER_DIR=\$TMPDIR
export SOCKET_WRAPPER_DEFAULT_IFACE=10

${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/python_concurency_reproducer.py
")

    add_test(NAME test_python_concurrency_reproducer
             COMMAND bash ${PYTHON_TEST_WRAPPER})

    if (ASAN_LIBRARY)
        list(APPEND PYTHON_PRELOAD_LIBRARIES ${ASAN_LIBRARY})
    endif()
    list(APPEND PYTHON_PRELOAD_LIBRARIES ${SWRAP_FAKE_UID_WRAPPER_LOCATION})
    list(APPEND PYTHON_PRELOAD_LIBRARIES ${SOCKET_WRAPPER_LOCATION})

    if (OSX)
        set(PYTHON_TORTURE_ENVIRONMENT "DYLD_FORCE_FLAT_NAMESPACE=1;DYLD_INSERT_LIBRARIES=${SOCKET_WRAPPER_LOCATION}")
    else ()
        string(REPLACE ";" ":" _PYTHON_TMP_ENV "${PYTHON_PRELOAD_LIBRARIES}")
        set(PYTHON_TORTURE_ENVIRONMENT "LD_PRELOAD=${_PYTHON_TMP_ENV}")
    endif()

    if (CMAKE_BUILD_TYPE)
        string(TOLOWER "${CMAKE_BUILD_TYPE}" CMAKE_BUILD_TYPE_LOWER)
        if (CMAKE_BUILD_TYPE_LOWER STREQUAL "addresssanitizer" OR
            CMAKE_BUILD_TYPE_LOWER STREQUAL "threadsanitizer" OR
            CMAKE_BUILD_TYPE_LOWER STREQUAL "undefinedsanitizer")
            list(APPEND PYTHON_TORTURE_ENVIRONMENT "SOCKET_WRAPPER_DISABLE_DEEPBIND=1")
        endif()
    endif()

    set_property(TEST test_python_concurrency_reproducer
                 PROPERTY ENVIRONMENT "${PYTHON_TORTURE_ENVIRONMENT}")
endif()
