# SPDX-License-Identifier: GPL-2.0-only
# Copyright (c) 2023 Meta Platforms, Inc. and affiliates.

# - Set property to files that should not be build.
#
# As bpfilter unit tests directly include the source file (.c) into the test
# file, it can leave to ODR violation. This issue could be fixed by excluding
# the tested files from the target sources list, which would break the
# dependencies chain (changing the source file wouldn't rebuild the tests, as
# the test target doesn't depend on it). Instead, tested source files can be
# marked as HEADER_ONLY, so they would still be part of the dependecies chain
# without being build.
#
# set_source_files_properties() will apply the HEADER_ONLY property to the
# source file but only for the targets defined in the CMakeLists.txt file it's
# called.
#
# Because the test files have the same name as the tested files, we only need
# to compare the relative file path to know whether a given source file is
# tested, hence should have the HEADER_ONLY property assigned.
#
# Usage:
#   bf_test_configure_non_build_srcs(${TARGET}
#       TESTS
#           # List of test files
#       SOURCES
#           # List of source files
#   )
#
function(bf_test_configure_non_build_srcs TARGET)
    cmake_parse_arguments(PARSE_ARGV 1 _LOCAL "" "" "TESTS;SOURCES")

    set(_test_srcs "")
    foreach(_test_src IN LISTS _LOCAL_TESTS)
        # Get absolute path to source file, and path relative to the project's
        # test directory.
        get_filename_component(_abs_test_src ${_test_src} ABSOLUTE)
        file(RELATIVE_PATH _rel_test_src ${CMAKE_CURRENT_SOURCE_DIR} ${_abs_test_src})
        list(APPEND _test_srcs "${_rel_test_src}")
    endforeach()

    foreach(_bf_src IN LISTS _LOCAL_SOURCES)
        get_source_file_property(
            IS_HEADER_ONLY ${_bf_src}
            TARGET_DIRECTORY bpfilter
            HEADER_FILE_ONLY
        )

        # Get absolute path to source file, and path relative to the project's
        # root directory.
        get_filename_component(_abs_bf_src ${_bf_src} ABSOLUTE)
        file(RELATIVE_PATH _rel_bf_src ${CMAKE_SOURCE_DIR}/src ${_abs_bf_src})

        if (${_rel_bf_src} IN_LIST _test_srcs OR IS_HEADER_ONLY)
            set_source_files_properties(${_abs_bf_src}
                PROPERTIES
                    HEADER_FILE_ONLY ON
            )
        endif()
    endforeach()
endfunction()

# - Define a new mock
#
# bpfilter uses ld's --wrap option to mock functions. --wrap will rename the
# given symbol ${SYM} as __real_${SYM}, and every call to ${SYM} will actually
# call __wrap_${SYM}. This function will add the necessary option to the
# target in order for ld to wrap the requested symbol.
#
# See https://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_node/ld_3.html.
#
function(bf_test_mock TARGET)
    cmake_parse_arguments(PARSE_ARGV 1 _LOCAL "" "" "FUNCTIONS")

    list(LENGTH _LOCAL_FUNCTIONS N_MOCKS)

    message(STATUS "Mocking ${N_MOCKS} functions for target '${TARGET}'")
    foreach(_function IN LISTS _LOCAL_FUNCTIONS)
        target_link_options(${TARGET}
            PUBLIC
                -Wl,--wrap=${_function}
        )
        message(VERBOSE "  - ${_function}()")
    endforeach()
endfunction()

include(ElfStubs)

enable_testing()

set(bf_test_srcs
    core/btf.c
    core/chain.c
    core/flavor.c
    core/front.c
    core/helper.c
    core/hook.c
    core/list.c
    core/marsh.c
    core/matcher.c
    core/rule.c
    core/verdict.c
    bpfilter/opts.c
    bpfilter/cgen/cgen.c
    bpfilter/cgen/jmp.c
    bpfilter/cgen/printer.c
    bpfilter/cgen/program.c
    bpfilter/cgen/prog/map.c
    bpfilter/cgen/swich.c
    bpfilter/ctx.c
    bpfilter/xlate/nft/nft.c
    bpfilter/xlate/nft/nfmsg.c
    bpfilter/xlate/nft/nfgroup.c
)

get_target_property(core_srcs core SOURCES)
get_target_property(bpfilter_srcs bpfilter SOURCES)
get_target_property(lib_srcs libbpfilter SOURCES)

list(REMOVE_ITEM bpfilter_srcs ${CMAKE_SOURCE_DIR}/src/bpfilter/main.c)

add_executable(unit_bin EXCLUDE_FROM_ALL
    ${CMAKE_CURRENT_SOURCE_DIR}/assert_override.h
    ${CMAKE_CURRENT_SOURCE_DIR}/main.c
    ${CMAKE_CURRENT_SOURCE_DIR}/fake.h  ${CMAKE_CURRENT_SOURCE_DIR}/fake.c
    ${CMAKE_CURRENT_SOURCE_DIR}/mock.h  ${CMAKE_CURRENT_SOURCE_DIR}/mock.c

    ${bf_test_srcs}

    ${core_srcs}
    ${bpfilter_srcs}
    ${lib_srcs}
)

file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/include/bpfilter/cgen)
bf_target_add_elfstubs(unit_bin
    DIR ${CMAKE_SOURCE_DIR}/src/bpfilter/bpf
    SYM_PREFIX "_bf_rawstubs_"
    DECL_HDR_PATH ${CMAKE_CURRENT_BINARY_DIR}/include/bpfilter/cgen/rawstubs.h
    STUBS
        "parse_ipv6_eh"
        "parse_ipv6_nh"
        "update_counters"
        "log"
)

bf_test_configure_non_build_srcs(unit_bin
    TESTS
        ${bf_test_srcs}
    SOURCES
        ${core_srcs}
        ${bpfilter_srcs}
        ${lib_srcs}
)

set_source_files_properties(${bf_test_srcs} ${core_srcs} ${bpfilter_srcs} ${lib_srcs}
    PROPERTIES
        COMPILE_OPTIONS "-ftest-coverage;-fprofile-arcs;-fprofile-abs-path"
)

target_compile_options(unit_bin
    PRIVATE
        -include ${CMAKE_CURRENT_SOURCE_DIR}/assert_override.h
        -fno-inline-small-functions     # Prevent inlining that would be -Wl,wrap (e.g. bf_ctx_token())
)

target_include_directories(unit_bin
    PRIVATE
        ${CMAKE_SOURCE_DIR}/src         # First look for headers in src/
        ${CMAKE_CURRENT_SOURCE_DIR}     # Then use overrides in tests/units
        ${CMAKE_BINARY_DIR}/include
        ${CMAKE_CURRENT_BINARY_DIR}/include
)

target_link_libraries(unit_bin
    PRIVATE
        bf_global_flags
        harness
        gcov
)

bf_test_mock(unit_bin
    FUNCTIONS
        bf_bpf
        bf_bpf_obj_get
        bf_btf_get_id
        bf_ctx_token
        btf__load_vmlinux_btf
        calloc
        malloc
        nlmsg_alloc
        nlmsg_append
        nlmsg_convert
        nlmsg_put
        open
        read
        snprintf
        vsnprintf
        write
)

add_custom_target(test
    COMMAND
        $<TARGET_FILE:unit_bin>
    DEPENDS
        unit_bin
    COMMENT "Running tests"
)

if (NOT ${NO_DOCS})
    include(ProcessorCount)
    find_program(LCOV_BIN lcov REQUIRED)

    ProcessorCount(N)
    if(N EQUAL 0)
        set(N 1)
    endif()

    add_custom_command(TARGET test
        POST_BUILD
        COMMAND
            ${CMAKE_COMMAND}
                -E make_directory
                ${CMAKE_BINARY_DIR}/output/tests
        COMMAND
            ${LCOV_BIN}
                --capture
                --directory ${CMAKE_BINARY_DIR}
                --output-file ${CMAKE_CURRENT_BINARY_DIR}/lcov.out
                --parallel ${N}
                --quiet
        # Only keep the coverage for bpfilter's source files, not the tests.
        COMMAND
            ${LCOV_BIN}
                --output-file ${CMAKE_BINARY_DIR}/output/tests/lcov.out
                --extract ${CMAKE_CURRENT_BINARY_DIR}/lcov.out
                --ignore-errors unused
                --parallel ${N}
                --quiet
                "${CMAKE_SOURCE_DIR}/src/\\*"
        COMMAND
            ${LCOV_BIN}
                --summary ${CMAKE_BINARY_DIR}/output/tests/lcov.out
        COMMENT "Generate the lcov.out summary file"
    )
endif ()
