Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ci: basic add_subdirectory cmake integration test #447

Merged
merged 8 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/actions/cmake-test/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# This is a composite to allow sharing these steps into other workflows.
# It isn't a shared workflow, because then it isn't convenient to add
# additional package-specific steps.
name: CMake Integration Test
description: 'CMake integration test suitable for running on multiple platforms.'
inputs:
platform_version:
description: 'Boost platform version'
required: false
default: "22.04"
toolset:
description: 'Boost toolset'
required: false


runs:
using: composite
steps:
- name: Install Ninja
uses: ./.github/actions/install-ninja
- name: Install boost
uses: ./.github/actions/install-boost
id: install-boost
with:
platform_version: ${{ inputs.platform_version }}
toolset: ${{ inputs.toolset }}
- name: Install OpenSSL
uses: ./.github/actions/install-openssl
id: install-openssl
- name: Configure CMake Integration Tests
shell: bash
run: ./scripts/configure-cmake-integration-tests.sh
env:
# These will be injected into the SDK CMake project on the command line, via "-D BOOST_ROOT=..."
# and "-D OPENSSL_ROOT_DIR=...". When the integration tests are configured, they will then be passed
# along in the same manner to those test projects via the command line.
BOOST_ROOT: ${{ steps.install-boost.outputs.BOOST_ROOT }}
OPENSSL_ROOT_DIR: ${{ steps.install-openssl.outputs.OPENSSL_ROOT_DIR }}
- name: Run CMake Integration Tests
shell: bash
run: |
export CTEST_OUTPUT_ON_FAILURE=1
cd build/cmake-tests && ctest
43 changes: 43 additions & 0 deletions .github/workflows/cmake.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: cmake-integration

on:
push:
branches: [ main ]
paths-ignore:
- '**.md' # Do not need to run CI for markdown changes.
pull_request:
branches: [ "main", "feat/**" ]
paths-ignore:
- '**.md'
schedule:
# Run daily at midnight PST
- cron: '0 8 * * *'

jobs:
test-ubuntu:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/cmake-test
with:
platform_version: '22.04'

test-macos:
runs-on: macos-12
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/cmake-test
with:
platform_version: '12'

test-windows:
runs-on: windows-2022
steps:
- uses: actions/checkout@v4
- uses: ilammy/msvc-dev-cmd@v1
- uses: ./.github/actions/cmake-test
env:
BOOST_ROOT: 'C:\local\boost_1_81_0\lib64-msvc-14.3'
with:
platform_version: 2022
toolset: msvc
27 changes: 21 additions & 6 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ cmake_dependent_option(LD_BUILD_CONTRACT_TESTS
OFF # otherwise, off
)

# Add an option for enabling the "CMake Integration Tests" (see cmake-tests README).
# These tests require testing to be enabled (BUILD_TESTING), but aren't unit tests, so are disabled by default.
cmake_dependent_option(LD_CMAKE_INTEGRATION_TESTS
"Test integration of SDK into other CMake projects" OFF # Default to disabling the cmake integration tests.
"BUILD_TESTING" # Only expose if testing is enabled.
OFF # otherwise, off.
)

# The general strategy is to produce a fat artifact containing all of our dependencies so users
# only have a single thing to link. We should support this either being a static or shared library.
# Because OpenSSL is a large, and security relevant dependency, we should have a separate option
Expand Down Expand Up @@ -108,6 +116,13 @@ if (LD_BUILD_UNIT_TESTS)
enable_testing()
endif ()

if (LD_CMAKE_INTEGRATION_TESTS)
message(STATUS "LaunchDarkly: building CMake integration tests")
add_subdirectory(cmake-tests)
enable_testing()
endif ()


if (LD_DYNAMIC_LINK_OPENSSL)
message(STATUS "LaunchDarkly: searching for shared OpenSSL library")
set(OPENSSL_USE_STATIC_LIBS OFF)
Expand All @@ -130,13 +145,13 @@ endif ()

if (LD_BUILD_SHARED_LIBS)
if (LD_BUILD_EXPORT_ALL_SYMBOLS)
message(STATUS "LaunchDarkly: exposing all symbols in shared libraries")
message(STATUS "LaunchDarkly: exposing all symbols in shared libraries")
else ()
message(STATUS "LaunchDarkly: hiding all symbols in shared libraries except for C API")
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
set(CMAKE_VISIBILITY_INLINES_HIDDEN 1)
endif()
endif()
message(STATUS "LaunchDarkly: hiding all symbols in shared libraries except for C API")
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
set(CMAKE_VISIBILITY_INLINES_HIDDEN 1)
endif ()
endif ()

set(Boost_USE_MULTITHREADED ON)
set(Boost_USE_STATIC_RUNTIME OFF)
Expand Down
2 changes: 2 additions & 0 deletions cmake-tests/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
include(declareProjectTest.cmake)
add_subdirectory(test_add_subdirectory)
64 changes: 64 additions & 0 deletions cmake-tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
## CMake project tests overview

This directory contains tests for various integration techniques that users of the
LaunchDarkly C++ SDKs may employ.

Each test takes the form of a minimal CMake project with a `CMakeLists.txt` and `main.cpp`.
An additional `CMakeLists.txt` sets up the test properties.

Structure:

```
some_test_directory
project # Contains the CMake project under test.
- main.cpp # Minimal code that invokes LaunchDarkly SDK.
- CMakeLists.txt # CMake configuration that builds the project executable.
- CMakeLists.txt # CMake configuration that sets up the CTest machinery for this test.
```

*Important note about `main.cpp`*:

The optimizer employed by whatever toolchain is building the project might omit function definitions in the SDK
during static linking, if those functions are proven to be unused.

The code in the main file should not have any branches that allow this to happen
(such as a check for an SDK key, like in the hello demo projects.)

This could obscure linker errors that would have otherwise been caught.

## CMake test setup

The toplevel `CMakeLists.txt` in each subdirectory is responsible for setting up
the actual CTest tests that configure and build the projects.

Note, the logic described below is encapsulated in two macros defined in `declareProjectTest.cmake`, so that
that new tests don't need to copy boilerplate.

Test creation is generally done in two phases:

1) Make a test that configures the project (simulating `cmake .. [options]`)
2) Make a test that builds the project (simulating `cmake --build .`)

The tests are ordered via `set_tests_properties` to ensure the configure test
runs before the build test, as would be expected.

The test creation logic harbors additional complexity because these tests are executed
in CI on multiple types of executors (Windows/Mac/Linux) in various configurations.

In particular, some environment variables must be forwarded to each test project CMake configuration.
These include `C` and `CXX` variables, which are explicitly set/overridden in the `clang11` CI build.
Without setting these, the test would fail to build with the same compilers as the SDK.

Additionally, certain variables must be forwarded to each test project CMake configuration.

| Variable | Explanation |
|--------------------|------------------|
| `BOOST_ROOT` | Path to Boost. |
| `OPENSSL_ROOT_DIR` | Path to OpenSSL. |

## Tests

### cmake_projects/test_add_subdirectory

Checks that a project can include the SDK as a sub-project, via `add_subdirectory`.
This would be a likely use-case when the repo is a submodule of another project.
90 changes: 90 additions & 0 deletions cmake-tests/declareProjectTest.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# This file defines macros which can be used to setup
# new cmake project tests without introducing excessive boilerplate.

# declare_add_subdirectory_test(<name of test>):
# Use when the test depends on launchdarkly via add_subdirectory.

# declare_find_package_test(<test name>):
# Use when the test depends on launchdarkly via find_package.

# add_build_step(<test name>):
# By default, the declare_* macros result in a test where "cmake -DSOMEVARIABLE=WHATEVER .."
# (the cmake configure step) is invoked. This may be sufficient for a particular test,
# for example testing that the configure step fails.
# If the test should also invoke "cmake --build .", use this macro.

# require_configure_failure(<test name>):
# Asserts that the cmake configure step should fail. For example, this would
# happen if a required version of a dependency couldn't be satisfied with find_package.

# require_build_failure(<test name>):
# Asserts that the cmake build step should fail.

macro(declare_add_subdirectory_test name)
set(test_prefix ${name})

add_test(
NAME ${test_prefix}_configure
COMMAND
${CMAKE_COMMAND}
# Since project/CMakeLists.txt is going to call add_subdirectory(), it needs to know where
# the SDK's project is (which is actually a couple directories above this particular file; not normally the case.)
# The variable name is arbitrary.
-DLAUNCHDARKLY_SOURCE_DIR=${PROJECT_SOURCE_DIR}
# Do not setup all of the SDK's testing machinery, which would normally happen when calling add_subdirectory.
-DBUILD_TESTING=OFF
-DBOOST_ROOT=${BOOST_ROOT}
-DOPENSSL_ROOT_DIR=${OPENSSL_ROOT_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/project
)

set_tests_properties(${test_prefix}_configure
PROPERTIES
FIXTURES_SETUP ${test_prefix}
# Forward along the CC and CXX environment variables, because clang11 CI build uses them.
ENVIRONMENT "CC=${CMAKE_C_COMPILER};CXX=${CMAKE_CXX_COMPILER}"
)
endmacro()

macro(require_configure_failure name)
set_tests_properties(${name}_configure PROPERTIES WILL_FAIL TRUE)
endmacro()

macro(require_build_failure name)
set_tests_properties(${name}_build PROPERTIES WILL_FAIL TRUE)
endmacro()

macro(add_build_step name)
# Setup a 'test' to perform the cmake build step.
add_test(
NAME ${name}_build
COMMAND ${CMAKE_COMMAND} --build .
)

set_tests_properties(${name}_build
PROPERTIES
FIXTURES_REQUIRED ${name}
)
endmacro()

macro(declare_find_package_test name)
# This test assumes that the SDK has been installed at CMAKE_INSTALL_PREFIX.
set(test_prefix ${name})

add_test(
NAME ${test_prefix}_configure
COMMAND
${CMAKE_COMMAND}
# Since project/CMakeLists.txt uses find_package(), it needs to know where to find
# ldserverapiConfig.cmake. That can be found where the SDK is installed, which is CMAKE_INSTALL_PREFIX.
-DCMAKE_PREFIX_PATH=${CMAKE_INSTALL_PREFIX}
${CMAKE_CURRENT_SOURCE_DIR}/project
)

set_tests_properties(${test_prefix}_configure
PROPERTIES
FIXTURES_SETUP ${test_prefix}
# Forward along the CC and CXX environment variables, because clang11 CI build uses them.
ENVIRONMENT "CC=${CMAKE_C_COMPILER};CXX=${CMAKE_CXX_COMPILER}"
)
endmacro()
2 changes: 2 additions & 0 deletions cmake-tests/test_add_subdirectory/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
declare_add_subdirectory_test(test_add_subdirectory)
add_build_step(test_add_subdirectory)
26 changes: 26 additions & 0 deletions cmake-tests/test_add_subdirectory/project/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
cmake_minimum_required(VERSION 3.19)

project(AddSubdirectoryTest)

add_subdirectory(
# Source directory where the SDK's CMakeLists.txt is located.
${LAUNCHDARKLY_SOURCE_DIR}
# Binary directory must be specified when using an out-of-tree source.
${CMAKE_CURRENT_BINARY_DIR}/launchdarkly
)

set(TARGET_PREFIX add_subdirectory)

# Server-side
add_executable(${TARGET_PREFIX}_server_cpp main_server.cpp)
target_link_libraries(${TARGET_PREFIX}_server_cpp launchdarkly-cpp-server)

add_executable(${TARGET_PREFIX}_server_cpp_alias main_server.cpp)
target_link_libraries(${TARGET_PREFIX}_server_cpp_alias launchdarkly::server)

# Client-side
add_executable(${TARGET_PREFIX}_client_cpp main_client.cpp)
target_link_libraries(${TARGET_PREFIX}_client_cpp launchdarkly-cpp-client)

add_executable(${TARGET_PREFIX}_client_cpp_alias main_client.cpp)
target_link_libraries(${TARGET_PREFIX}_client_cpp_alias launchdarkly::client)
25 changes: 25 additions & 0 deletions cmake-tests/test_add_subdirectory/project/main_client.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#include <launchdarkly/client_side/client.hpp>

Check notice on line 1 in cmake-tests/test_add_subdirectory/project/main_client.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

Run clang-format on cmake-tests/test_add_subdirectory/project/main_client.cpp

File cmake-tests/test_add_subdirectory/project/main_client.cpp does not conform to Custom style guidelines. (lines 17)
#include <launchdarkly/context_builder.hpp>

#include <cstring>
#include <iostream>

using namespace launchdarkly;
using namespace launchdarkly::client_side;

int main() {
auto config = ConfigBuilder("sdk-key").Build();
if (!config) {
std::cout << "error: config is invalid: " << config.error() << '\n';
return 1;
}

auto context =
ContextBuilder().Kind("user", "example-user-key").Name("Sandy").Build();

auto client = Client(std::move(*config), std::move(context));

client.StartAsync();

std::cout << client.Initialized() << '\n';
}
23 changes: 23 additions & 0 deletions cmake-tests/test_add_subdirectory/project/main_server.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#include <launchdarkly/server_side/client.hpp>

Check notice on line 1 in cmake-tests/test_add_subdirectory/project/main_server.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

Run clang-format on cmake-tests/test_add_subdirectory/project/main_server.cpp

File cmake-tests/test_add_subdirectory/project/main_server.cpp does not conform to Custom style guidelines. (lines 21)
#include <launchdarkly/server_side/config/config_builder.hpp>

#include <cstring>
#include <iostream>

using namespace launchdarkly;
using namespace launchdarkly::server_side;

int main() {
auto config = ConfigBuilder("sdk-key").Build();
if (!config) {
std::cout << "error: config is invalid: " << config.error() << '\n';
return 1;
}

auto client = Client(std::move(*config));

client.StartAsync();

std::cout << client.Initialized() << '\n';

}
20 changes: 20 additions & 0 deletions scripts/configure-cmake-integration-tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/bin/bash -e

function cleanup {
cd ..
}

mkdir -p build
cd build
# After we enter the directory we want to make sure we always exit it when the
# script ends.
trap cleanup EXIT



cmake -G Ninja -D CMAKE_COMPILE_WARNING_AS_ERROR=TRUE \
-D BUILD_TESTING=ON \
-D LD_CMAKE_INTEGRATION_TESTS=ON \
-D BOOST_ROOT="$BOOST_ROOT" \
-D OPENSSL_ROOT_DIR="$OPENSSL_ROOT_DIR" \
-D LD_BUILD_EXAMPLES=OFF ..
Loading