Feature/add fuzzing (#799)
Add fuzz testing
* Also improves benchmark setup
* Add fuzzing workflow and enable benchmarking options in CI configurations
* Add documentation for building benchmarks, fuzz testing, and running tests
* Remove C++ flag for macos, resulted in undefined symbols
* Increase celix error buffer size for fuzz testing
* Eliminate clang warnings -Werror,-Wvla-cxx-extension
* Refactor `benchmark` dependency to `test_requires` when `enable_benchmarking` is set.
* Fix formatting issue in README under "C Patterns" section.
* Update to actions/cache 4.3.0
* Add asan and ubsan for fuzz testing builds
* Enable ubsan with asan for linux ci
---------
Co-authored-by: PengZheng <howtofly@gmail.com>
diff --git a/.github/workflows/fuzzing.yml b/.github/workflows/fuzzing.yml
new file mode 100644
index 0000000..242b475
--- /dev/null
+++ b/.github/workflows/fuzzing.yml
@@ -0,0 +1,59 @@
+name: Celix Fuzzing
+
+on:
+ push:
+ pull_request:
+ schedule:
+ - cron: '0 3 * * *'
+
+jobs:
+ fuzz-utils:
+ runs-on: ubuntu-22.04
+ timeout-minutes: 30
+ steps:
+ - name: Checkout source code
+ uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c #v3.3.0
+ - name: Set up Python
+ uses: actions/setup-python@7f4fc3e22c37d6ff65e88745f38bd3157c663f7c #v4.9.1
+ with:
+ python-version: '3.x'
+ - name: Set Compiler Environment Variables
+ run: |
+ echo "CC=clang" >> $GITHUB_ENV
+ echo "CXX=clang++" >> $GITHUB_ENV
+ - name: Install Conan
+ run: pip install conan
+ - name: Cache Conan
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 #v4.3.0
+ with:
+ path: ~/.conan2/p
+ key: ${{ runner.os }}-conan-${{ hashFiles('conanfile.py', 'libs/utils/**') }}
+ restore-keys: |
+ ${{ runner.os }}-conan-
+ - name: Setup Conan Profile
+ run: |
+ conan profile detect
+ - name: Conan install
+ run: conan install . --output-folder=build --build=missing -o "celix/*:build_utils=True" -o "celix/*:enable_fuzzing=True" -o "celix/*:enable_address_sanitizer=True" -o "celix/*:enable_undefined_sanitizer=True"
+ - name: Conan build
+ run: conan build . --output-folder=build -o "celix/*:build_utils=True" -o "celix/*:enable_fuzzing=True" -o "celix/*:enable_address_sanitizer=True" -o "celix/*:enable_undefined_sanitizer=True" -o "celix/*:celix_err_buffer_size=5120"
+ - name: Set fuzzer run time
+ id: set-runtime
+ run: |
+ if [[ "${{ github.event_name }}" == "schedule" ]]; then
+ echo "FUZZ_TIME=600" >> ${GITHUB_ENV}
+ else
+ echo "FUZZ_TIME=30" >> ${GITHUB_ENV}
+ fi
+ - name: Run properties fuzzer
+ run: |
+ source build/conanrun.sh
+ ./build/libs/utils/fuzzing/celix_properties_fuzzer -max_total_time=$FUZZ_TIME ./build/libs/utils/fuzzing/properties_corpus
+ - name: Run version fuzzer
+ run: |
+ source build/conanrun.sh
+ ./build/libs/utils/fuzzing/celix_version_fuzzer -max_total_time=$FUZZ_TIME ./build/libs/utils/fuzzing/version_corpus
+ - name: Run filter fuzzer
+ run: |
+ source build/conanrun.sh
+ ./build/libs/utils/fuzzing/celix_filter_fuzzer -max_total_time=$FUZZ_TIME ./build/libs/utils/fuzzing/filter_corpus
diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml
index 61bfd9e..eceb860 100644
--- a/.github/workflows/macos.yml
+++ b/.github/workflows/macos.yml
@@ -56,6 +56,7 @@
env:
CONAN_BUILD_OPTIONS: |
-o celix/*:enable_testing=True
+ -o celix/*:enable_benchmarking=True
-o celix/*:enable_address_sanitizer=True
-o celix/*:build_all=True
-o celix/*:enable_cmake_warning_tests=True
@@ -79,7 +80,7 @@
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c #v3.3.0
- name: Install dependencies
run: |
- brew install lcov jansson rapidjson libzip ccache ninja openssl@1.1
+ brew install lcov jansson rapidjson libzip ccache ninja openssl@1.1 google-benchmark
- name: Prepare ccache timestamp
id: ccache_cache_timestamp
run: |
@@ -95,6 +96,7 @@
env:
BUILD_OPTIONS: |
-DENABLE_TESTING=ON
+ -DENABLE_BENCHMARKING=ON
-DENABLE_ADDRESS_SANITIZER=ON
-DENABLE_TESTING_ON_CI=ON
-DCMAKE_BUILD_TYPE=Release
diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml
index a472b72..dc0964f 100644
--- a/.github/workflows/ubuntu.yml
+++ b/.github/workflows/ubuntu.yml
@@ -77,7 +77,9 @@
CXX: ${{ matrix.compiler[1] }}
CONAN_BUILD_OPTIONS: |
-o celix:enable_testing=True
+ -o celix:enable_benchmarking=True
-o celix:enable_address_sanitizer=True
+ -o celix:enable_undefined_sanitizer=True
-o celix:build_all=True
-o celix:enable_cmake_warning_tests=True
-o celix:enable_testing_on_ci=True
@@ -120,6 +122,7 @@
libzip-dev \
libjansson-dev \
libcurl4-openssl-dev \
+ libbenchmark-dev \
default-jdk \
cmake \
libffi-dev \
@@ -145,6 +148,7 @@
BUILD_OPTIONS: |
-DBUILD_EXPERIMENTAL=ON
-DENABLE_TESTING=ON
+ -DENABLE_BENCHMARKING=ON
-DRSA_JSON_RPC=ON
-DRSA_REMOTE_SERVICE_ADMIN_SHM_V2=ON
-DENABLE_TESTING_ON_CI=ON
diff --git a/CMakeLists.txt b/CMakeLists.txt
index d72ce4d..09e065a 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -168,6 +168,8 @@
option(ENABLE_CMAKE_WARNING_TESTS "Enable cmake warning tests to test warning prints" OFF)
option(ENABLE_TESTING_ON_CI "Whether to enable testing on CI. This influence allowed timing errors during unit tests" OFF)
option(ENABLE_DEPRECATED_WARNINGS "Enable compiler warnings for usage of deprecated functionality" OFF)
+option(ENABLE_FUZZING "Enable fuzz testing, using LibFuzzer" OFF) #Note support for LibFuzzer is built-in for Clang
+option(ENABLE_BENCHMARKING "Enable benchmarking, using Google Benchmark" OFF)
if (NOT ENABLE_DEPRECATED_WARNINGS)
set(CMAKE_C_FLAGS "-Wno-deprecated-declarations ${CMAKE_C_FLAGS}")
diff --git a/cmake/celix_project/CelixProject.cmake b/cmake/celix_project/CelixProject.cmake
index 0f31327..8f231a2 100644
--- a/cmake/celix_project/CelixProject.cmake
+++ b/cmake/celix_project/CelixProject.cmake
@@ -25,13 +25,17 @@
mark_as_advanced(CLEAR ENABLE_THREAD_SANITIZER)
if (ENABLE_ADDRESS_SANITIZER)
+ set(UBSAN_SAN "")
+ if (ENABLE_UNDEFINED_SANITIZER)
+ set(UBSAN_SAN "undefined,")
+ endif ()
if("${CMAKE_C_COMPILER_ID}" MATCHES "Clang")
set(CMAKE_C_FLAGS "-DCELIX_ASAN_ENABLED ${CMAKE_C_FLAGS}")
- set(CMAKE_C_FLAGS "-shared-libasan -fsanitize=address -fno-omit-frame-pointer ${CMAKE_C_FLAGS}")
- set(CMAKE_CXX_FLAGS "-shared-libasan -fsanitize=address -fno-omit-frame-pointer ${CMAKE_CXX_FLAGS}")
+ set(CMAKE_C_FLAGS "-shared-libasan -fsanitize=${UBSAN_SAN}address -fno-omit-frame-pointer ${CMAKE_C_FLAGS}")
+ set(CMAKE_CXX_FLAGS "-shared-libasan -fsanitize=${UBSAN_SAN}address -fno-omit-frame-pointer ${CMAKE_CXX_FLAGS}")
if (APPLE)
- set(CMAKE_EXE_LINKER_FLAGS "-fsanitize=address ${CMAKE_EXE_LINKER_FLAGS}")
- set(CMAKE_SHARED_LINKER_FLAGS "-fsanitize=address ${CMAKE_SHARED_LINKER_FLAGS}")
+ set(CMAKE_EXE_LINKER_FLAGS "-fsanitize=${UBSAN_SAN}address ${CMAKE_EXE_LINKER_FLAGS}")
+ set(CMAKE_SHARED_LINKER_FLAGS "-fsanitize=${UBSAN_SAN}address ${CMAKE_SHARED_LINKER_FLAGS}")
else ()
# Fix a linux clang deficiency where the ASan runtime library is not found automatically
# Find the ASan runtime library path and set RPATH
@@ -56,19 +60,15 @@
endif ()
elseif ("${CMAKE_C_COMPILER_ID}" STREQUAL "GNU")
set(CMAKE_C_FLAGS "-DCELIX_ASAN_ENABLED ${CMAKE_C_FLAGS}")
- set(CMAKE_C_FLAGS "-lasan -fsanitize=address -fno-omit-frame-pointer ${CMAKE_C_FLAGS}")
- set(CMAKE_CXX_FLAGS "-lasan -fsanitize=address -fno-omit-frame-pointer ${CMAKE_CXX_FLAGS}")
+ set(CMAKE_C_FLAGS "-lasan -fsanitize=${UBSAN_SAN}address -fno-omit-frame-pointer ${CMAKE_C_FLAGS}")
+ set(CMAKE_CXX_FLAGS "-lasan -fsanitize=${UBSAN_SAN}address -fno-omit-frame-pointer ${CMAKE_CXX_FLAGS}")
else ()
message(WARNING "Address sanitizer is not supported for ${CMAKE_C_COMPILER_ID}")
endif ()
-endif()
-
-if (ENABLE_UNDEFINED_SANITIZER)
+elseif (ENABLE_UNDEFINED_SANITIZER)
set(CMAKE_C_FLAGS "-fsanitize=undefined ${CMAKE_C_FLAGS}")
set(CMAKE_CXX_FLAGS "-fsanitize=undefined ${CMAKE_CXX_FLAGS}")
-endif()
-
-if (ENABLE_THREAD_SANITIZER)
+elseif (ENABLE_THREAD_SANITIZER)
set(CMAKE_C_FLAGS "-fsanitize=thread ${CMAKE_C_FLAGS}")
set(CMAKE_CXX_FLAGS "-fsanitize=thread ${CMAKE_CXX_FLAGS}")
endif()
diff --git a/cmake/cmake_celix/BundlePackaging.cmake b/cmake/cmake_celix/BundlePackaging.cmake
index 4200f39..bb48690 100644
--- a/cmake/cmake_celix/BundlePackaging.cmake
+++ b/cmake/cmake_celix/BundlePackaging.cmake
@@ -287,16 +287,7 @@
#########################################################
###### Packaging the bundle using using jar or zip and a content dir. Configuring dependencies ######
- if (JAR_COMMAND)
- add_custom_command(OUTPUT ${BUNDLE_FILE}
- COMMAND ${CMAKE_COMMAND} -E make_directory ${BUNDLE_CONTENT_DIR}
- COMMAND ${CMAKE_COMMAND} -E copy_if_different ${BUNDLE_GEN_DIR}/MANIFEST.json ${BUNDLE_CONTENT_DIR}/META-INF/MANIFEST.json
- COMMAND ${JAR_COMMAND} ${CELIX_JAR_COMMAND_ARGUMENTS} ${BUNDLE_FILE} -C ${BUNDLE_CONTENT_DIR} .
- COMMENT "Packaging ${BUNDLE_TARGET_NAME}"
- DEPENDS ${BUNDLE_TARGET_NAME} "$<TARGET_PROPERTY:${BUNDLE_TARGET_NAME},BUNDLE_DEPEND_TARGETS>" ${BUNDLE_GEN_DIR}/MANIFEST.json
- WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
- )
- elseif (ZIP_COMMAND)
+ if (ZIP_COMMAND)
file(MAKE_DIRECTORY ${BUNDLE_CONTENT_DIR}) #Note needed because working_directory is bundle content dir
add_custom_command(OUTPUT ${BUNDLE_FILE}
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${BUNDLE_GEN_DIR}/MANIFEST.json ${BUNDLE_CONTENT_DIR}/META-INF/MANIFEST.json
@@ -305,6 +296,15 @@
DEPENDS ${BUNDLE_TARGET_NAME} "$<TARGET_PROPERTY:${BUNDLE_TARGET_NAME},BUNDLE_DEPEND_TARGETS>" ${BUNDLE_GEN_DIR}/MANIFEST.json
WORKING_DIRECTORY ${BUNDLE_CONTENT_DIR}
)
+ elseif (JAR_COMMAND)
+ add_custom_command(OUTPUT ${BUNDLE_FILE}
+ COMMAND ${CMAKE_COMMAND} -E make_directory ${BUNDLE_CONTENT_DIR}
+ COMMAND ${CMAKE_COMMAND} -E copy_if_different ${BUNDLE_GEN_DIR}/MANIFEST.json ${BUNDLE_CONTENT_DIR}/META-INF/MANIFEST.json
+ COMMAND ${JAR_COMMAND} ${CELIX_JAR_COMMAND_ARGUMENTS} ${BUNDLE_FILE} -C ${BUNDLE_CONTENT_DIR} .
+ COMMENT "Packaging ${BUNDLE_TARGET_NAME}"
+ DEPENDS ${BUNDLE_TARGET_NAME} "$<TARGET_PROPERTY:${BUNDLE_TARGET_NAME},BUNDLE_DEPEND_TARGETS>" ${BUNDLE_GEN_DIR}/MANIFEST.json
+ WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+ )
else ()
message(FATAL_ERROR "A jar or zip command is needed to jar/zip bundles")
endif ()
@@ -934,16 +934,7 @@
set(BUNDLE_FILE_INSTALL "${BUNDLE_FILE}.install")
get_target_property(BUNDLE_FILE_NAME ${BUNDLE} "BUNDLE_FILE_NAME")
get_target_property(BUNDLE_GEN_DIR ${BUNDLE} "BUNDLE_GEN_DIR")
- if (JAR_COMMAND)
- install(CODE
- "execute_process(
- COMMAND ${CMAKE_COMMAND} -E copy_if_different ${BUNDLE_GEN_DIR}/MANIFEST.json META-INF/MANIFEST.json
- COMMAND ${JAR_COMMAND} ${CELIX_JAR_COMMAND_ARGUMENTS} ${BUNDLE_FILE_INSTALL} -C ${BUNDLE_CONTENT_DIR} .
- WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
- )"
- COMPONENT ${BUNDLE}
- )
- elseif (ZIP_COMMAND)
+ if (ZIP_COMMAND)
install(CODE
"execute_process(
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${BUNDLE_GEN_DIR}/MANIFEST.json META-INF/MANIFEST.json
@@ -952,6 +943,15 @@
)"
COMPONENT ${BUNDLE}
)
+ elseif (JAR_COMMAND)
+ install(CODE
+ "execute_process(
+ COMMAND ${CMAKE_COMMAND} -E copy_if_different ${BUNDLE_GEN_DIR}/MANIFEST.json META-INF/MANIFEST.json
+ COMMAND ${JAR_COMMAND} ${CELIX_JAR_COMMAND_ARGUMENTS} ${BUNDLE_FILE_INSTALL} -C ${BUNDLE_CONTENT_DIR} .
+ WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+ )"
+ COMPONENT ${BUNDLE}
+ )
else ()
message(FATAL_ERROR "A jar or zip command is needed to jar/zip bundles")
endif ()
diff --git a/conanfile.py b/conanfile.py
index 09ca5a3..bb1c639 100644
--- a/conanfile.py
+++ b/conanfile.py
@@ -47,6 +47,8 @@
"enable_address_sanitizer": False,
"enable_undefined_sanitizer": False,
"enable_thread_sanitizer": False,
+ "enable_fuzzing": False,
+ "enable_benchmarking": False,
"install_find_modules": False,
"build_all": False,
"build_http_admin": False,
@@ -130,6 +132,9 @@
if self.options.build_rsa_discovery_zeroconf and self.settings.os != "Linux":
raise ConanInvalidConfiguration("Celix build_rsa_discovery_zeroconf is only supported for Linux")
+ if self.options.enable_fuzzing and self.settings.compiler != "clang" and self.settings.compiler != "apple-clang":
+ raise ConanInvalidConfiguration("Celix enable_fuzzing=True requires the 'clang' compiler")
+
self.validate_config_option_is_positive_number("celix_err_buffer_size")
self.validate_config_option_is_positive_number("celix_utils_max_strlen")
self.validate_config_option_is_positive_number("celix_properties_optimization_string_buffer_size")
@@ -145,12 +150,18 @@
del self.info.options.enable_testing_on_ci
del self.info.options.enable_ccache
del self.info.options.enable_deprecated_warnings
+ del self.info.options.enable_testing
+ del self.info.options.enable_benchmarking
+ del self.info.options.enable_fuzzing
+ del self.info.options.enable_code_coverage
def build_requirements(self):
if self.options.enable_testing:
self.test_requires("gtest/1.10.0")
if self.options.enable_ccache:
self.build_requires("ccache/4.7.4")
+ if self.options.enable_benchmarking:
+ self.test_requires("benchmark/[>=1.6.2]")
def configure(self):
# copy options to options, fill in defaults if not set
@@ -308,6 +319,8 @@
self.options['openssl'].shared = True
if self.options.enable_testing:
self.options['gtest'].shared = True
+ if self.options.enable_benchmarking:
+ self.options['benchmark'].shared = True
if (self.options.build_rsa_discovery_common
or (self.options.build_rsa_remote_service_admin_dfi and self.options.enable_testing)):
self.options['libxml2'].shared = True
diff --git a/documents/README.md b/documents/README.md
index fefec79..c633e7d 100644
--- a/documents/README.md
+++ b/documents/README.md
@@ -81,7 +81,6 @@
* Building
* [Building and Installing Apache Celix](building/README.md)
- * [Building and Developing Apache Celix with CLion](building/dev_celix_with_clion.md)
* C Patterns
* [Apache Celix C Patterns](c_patterns.md)
* Utils
diff --git a/documents/building/README.md b/documents/building/README.md
index c643510..7d01b8b 100644
--- a/documents/building/README.md
+++ b/documents/building/README.md
@@ -237,3 +237,10 @@
make -j
sudo make install
```
+
+# Further Reading
+
+- [Building with CLion](dev_celix_with_clion.md)
+- [Building and Running Tests](testing.md)
+- [Fuzz Testing](fuzz_testing.md)
+- [Building and Running Benchmarks](benchmarks.md)
diff --git a/documents/building/benchmarks.md b/documents/building/benchmarks.md
new file mode 100644
index 0000000..d1061b3
--- /dev/null
+++ b/documents/building/benchmarks.md
@@ -0,0 +1,74 @@
+---
+title: Benchmarks in Apache Celix
+---
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+
+# Benchmarks in Apache Celix
+
+This document describes how to build and run benchmarks for Apache Celix.
+
+## Building Benchmarks
+
+Benchmarks can be built using the CMake option `ENABLE_BENCHMARKING`
+The Apache Celix benchmarks uses Google benchmark library.
+
+To build benchmarks run:
+
+```sh
+cmake -B build -DENABLE_BENCHMARKING=ON
+cmake --build build
+```
+
+## Benchmarks
+
+The following benchmark executables are available after building the utils and framework benchmarks:
+
+**Utils Benchmarks:**
+- `build/libs/utils/benchmark/celix_filter_benchmark`
+- `build/libs/utils/benchmark/celix_long_hashmap_benchmark`
+- `build/libs/utils/benchmark/celix_string_hashmap_benchmark`
+- `build/libs/utils/benchmark/celix_utils_benchmark`
+
+**Framework Benchmarks:**
+- `build/libs/framework/benchmark/celix_framework_benchmark`
+
+Paths may vary depending on your configuration and enabled options.
+
+## Running Benchmarks
+
+Benchmark executables are located in the `build` directory, typically under the relevant bundle or library subdirectory. To run a benchmark:
+
+```sh
+./build/libs/utils/benchmarks/celix_utils_benchmark
+## Command-Line Options
+The benchmark executables accept standard Google Benchmark command-line options.
+For example, to run only benchmarks matching a specific pattern and output results in JSON format:
+
+```bash
+./build/libs/utils/benchmark/celix_filter_benchmark --benchmark_filter=complexFilter --benchmark_format=json
+```
+
+Replace `celix_utils_benchmark` and the filter pattern as needed. To see a list of supported command-line flags, run the benchmark executable with the `--help` option:
+
+```bash
+./build/libs/utils/benchmarks/./celix_filter_benchmark --help
+```
+
+This will display all available Google Benchmark options.
diff --git a/documents/building/fuzz_testing.md b/documents/building/fuzz_testing.md
new file mode 100644
index 0000000..50f0abb
--- /dev/null
+++ b/documents/building/fuzz_testing.md
@@ -0,0 +1,74 @@
+---
+title: Fuzz testing with libFuzzer
+---
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+# Fuzz Testing with libFuzzer
+
+The utilities library contains fuzz targets that can be built with
+[LLVM libFuzzer](https://llvm.org/docs/LibFuzzer.html). Fuzzing is
+enabled when using the Clang compiler and the `UTILS_LIBFUZZER` CMake
+option.
+
+## Building
+
+Configure CMake with Clang and enable the libFuzzer option:
+
+```bash
+cmake \
+ -G Ninja \
+ -S . -B build \
+ -DCMAKE_C_COMPILER=clang \
+ -DCMAKE_CXX_COMPILER=clang++ \
+ -DENABLE_FUZZING=ON
+```
+
+Build the fuzzer executables:
+
+```bash
+cmake --build build --parallel --target celix_properties_fuzzer celix_version_fuzzer celix_filter_fuzzer
+```
+
+## Corpus
+
+The `corpus` directories for the fuzzers contain a few seed inputs, which help guide the initial fuzzing process.
+More files can be added to these directories to improve coverage. The fuzzer will automatically use all files in the
+specified corpus directory as starting points for mutation and exploration.
+
+## Running
+The resulting fuzzers accept standard libFuzzer command line options. For example, to run each fuzzer for 30 seconds
+using the provided seed corpus and print coverage information:
+
+```bash
+./build/libs/utils/celix_filter_fuzzer -max_total_time=30 -print_coverage=1 ./build/libs/utils/filter_corpus
+```
+
+Replace `celix_filter_fuzzer` and `filter_corpus` with the appropriate fuzzer executable and corpus directory as needed.
+To see a list of supported command-line flags, run the fuzzer executable with the `-help=1` option. For example:
+
+```bash
+./build/libs/utils/celix_filter_fuzzer -help=1
+```
+
+This will display all available LibFuzzer options.
+
+## Continuous Fuzzing
+
+A GitHub Actions workflow runs the fuzzer periodically. The workflow
+configuration can be found at `.github/workflows/fuzzing.yml`.
diff --git a/documents/building/testing.md b/documents/building/testing.md
new file mode 100644
index 0000000..557258c
--- /dev/null
+++ b/documents/building/testing.md
@@ -0,0 +1,65 @@
+---
+title: Testing Apache Celix
+---
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+# Testing Apache Celix
+
+This document describes how to build and run tests for Apache Celix.
+
+## Building Tests
+
+Celix uses CMake and Google Test for its unit and integration tests. To build the tests, ensure you have all dependencies installed, then run:
+
+```sh
+cmake -B build -DENABLE_TESTING=ON -DCMAKE_BUILD_TYPE=Debug
+cmake --build build
+```
+
+```sh
+#conan
+
+To enable AddressSanitizer (ASAN) when building tests, configure CMake with the `ENABLE_ASAN` option:
+
+```sh
+cmake -B build -DENABLE_TESTING=ON -DENABLE_ADDRESS_SANITIZER=ON -DCMAKE_BUILD_TYPE=Debug
+cmake --build build
+```
+
+This will build Apache Celix and its tests with ASAN enabled, helping to detect memory errors during test execution.
+
+## Running Tests
+
+After building, you can run all tests using CTest:
+
+```sh
+ctest --output-on-failure --test-dir build
+```
+
+Or run a test for a specific subdir, e.g.:
+
+```sh
+ctest --output-on-failure --test-dir build/bundles/shell
+```
+
+Or run a specific test binary directly from the `build` directory, e.g.:
+
+```sh
+./build/bundles/components_ready_check/tests/components_ready_check_test
+```
diff --git a/libs/framework/benchmark/CMakeLists.txt b/libs/framework/benchmark/CMakeLists.txt
index ba39fcb..875d2f7 100644
--- a/libs/framework/benchmark/CMakeLists.txt
+++ b/libs/framework/benchmark/CMakeLists.txt
@@ -15,14 +15,7 @@
# specific language governing permissions and limitations
# under the License.
-set(FRAMEWORK_BENCHMARK_DEFAULT "OFF")
-find_package(benchmark QUIET)
-if (benchmark_FOUND)
- set(FRAMEWORK_BENCHMARK_DEFAULT "ON")
-endif ()
-
-celix_subproject(FRAMEWORK_BENCHMARK "Option to enable Celix framework benchmark" ${FRAMEWORK_BENCHMARK_DEFAULT})
-if (FRAMEWORK_BENCHMARK AND CELIX_CXX17)
+if (ENABLE_BENCHMARKING AND CELIX_CXX17)
set(CMAKE_CXX_STANDARD 17)
find_package(benchmark REQUIRED)
diff --git a/libs/utils/CMakeLists.txt b/libs/utils/CMakeLists.txt
index 4b8c08a..e28317b 100644
--- a/libs/utils/CMakeLists.txt
+++ b/libs/utils/CMakeLists.txt
@@ -126,5 +126,23 @@
add_subdirectory(gtest)
endif ()
+
+ if (ENABLE_FUZZING)
+ add_library(utils_cuf STATIC ${UTILS_SRC})
+ target_compile_definitions(utils_cuf PRIVATE CELIX_UTILS_STATIC_DEFINE)
+ target_include_directories(utils_cuf PUBLIC
+ ${CMAKE_CURRENT_LIST_DIR}/include
+ ${CMAKE_CURRENT_LIST_DIR}/include_internal
+ ${CMAKE_BINARY_DIR}/celix/gen/includes/utils
+ ${CMAKE_BINARY_DIR}/celix/gen/src/utils
+ include_deprecated
+ )
+ target_link_libraries(utils_cuf PUBLIC ${UTILS_PUBLIC_DEPS} ${UTILS_PRIVATE_DEPS})
+ target_compile_options(utils_cuf PRIVATE -fsanitize=fuzzer)
+ target_link_options(utils_cuf PRIVATE -Wl,--gc-sections)
+
+ add_subdirectory(fuzzing)
+ endif ()
+
add_subdirectory(benchmark)
endif ()
diff --git a/libs/utils/benchmark/CMakeLists.txt b/libs/utils/benchmark/CMakeLists.txt
index 2f891e6..4c22fc2 100644
--- a/libs/utils/benchmark/CMakeLists.txt
+++ b/libs/utils/benchmark/CMakeLists.txt
@@ -15,14 +15,7 @@
# specific language governing permissions and limitations
# under the License.
-set(UTILS_BENCHMARK_DEFAULT "OFF")
-find_package(benchmark QUIET)
-if (benchmark_FOUND)
- set(UTILS_BENCHMARK_DEFAULT "ON")
-endif ()
-
-celix_subproject(UTILS_BENCHMARK "Option to enable Celix framework benchmark" ${UTILS_BENCHMARK_DEFAULT})
-if (UTILS_BENCHMARK)
+if (ENABLE_BENCHMARKING)
find_package(benchmark REQUIRED)
add_executable(celix_string_hashmap_benchmark
diff --git a/libs/utils/fuzzing/CMakeLists.txt b/libs/utils/fuzzing/CMakeLists.txt
new file mode 100644
index 0000000..a7e248e
--- /dev/null
+++ b/libs/utils/fuzzing/CMakeLists.txt
@@ -0,0 +1,52 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+if (ENABLE_ADDRESS_SANITIZER OR ENABLE_UNDEFINED_SANITIZER)
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -static-libsan")
+endif ()
+
+add_executable(celix_properties_fuzzer src/PropertiesFuzz.cc)
+target_link_libraries(celix_properties_fuzzer PRIVATE utils_cuf)
+target_compile_options(celix_properties_fuzzer PRIVATE -fsanitize=fuzzer)
+target_link_options(celix_properties_fuzzer PRIVATE -fsanitize=fuzzer)
+
+add_executable(celix_version_fuzzer src/VersionFuzz.cc)
+target_link_libraries(celix_version_fuzzer PRIVATE utils_cuf)
+target_compile_options(celix_version_fuzzer PRIVATE -fsanitize=fuzzer)
+target_link_options(celix_version_fuzzer PRIVATE -fsanitize=fuzzer)
+
+add_executable(celix_filter_fuzzer src/FilterFuzz.cc)
+target_link_libraries(celix_filter_fuzzer PRIVATE utils_cuf)
+target_compile_options(celix_filter_fuzzer PRIVATE -fsanitize=fuzzer)
+target_link_options(celix_filter_fuzzer PRIVATE -fsanitize=fuzzer)
+
+add_custom_command(TARGET celix_properties_fuzzer POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy_directory
+ ${CMAKE_CURRENT_SOURCE_DIR}/properties_corpus
+ $<TARGET_FILE_DIR:celix_properties_fuzzer>/properties_corpus
+)
+
+add_custom_command(TARGET celix_version_fuzzer POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy_directory
+ ${CMAKE_CURRENT_SOURCE_DIR}/version_corpus
+ $<TARGET_FILE_DIR:celix_version_fuzzer>/version_corpus
+)
+
+add_custom_command(TARGET celix_filter_fuzzer POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy_directory
+ ${CMAKE_CURRENT_SOURCE_DIR}/filter_corpus
+ $<TARGET_FILE_DIR:celix_filter_fuzzer>/filter_corpus
+)
+
diff --git a/libs/utils/fuzzing/filter_corpus/complex.txt b/libs/utils/fuzzing/filter_corpus/complex.txt
new file mode 100644
index 0000000..1ffe405
--- /dev/null
+++ b/libs/utils/fuzzing/filter_corpus/complex.txt
@@ -0,0 +1 @@
+(&(test_attr1=attr1)(|(test_attr2=attr2)(test_attr3=attr3)))
diff --git a/libs/utils/fuzzing/filter_corpus/simple.txt b/libs/utils/fuzzing/filter_corpus/simple.txt
new file mode 100644
index 0000000..bfa80ac
--- /dev/null
+++ b/libs/utils/fuzzing/filter_corpus/simple.txt
@@ -0,0 +1 @@
+(key=value)
diff --git a/libs/utils/fuzzing/properties_corpus/complex.json b/libs/utils/fuzzing/properties_corpus/complex.json
new file mode 100644
index 0000000..0708565
--- /dev/null
+++ b/libs/utils/fuzzing/properties_corpus/complex.json
@@ -0,0 +1,2 @@
+{"key1":"value1","key2":"value2","object1":{"key3":"value3","key4":"value4"},"object2":{"key5":"value5"},"object3":{"object4":{"key6":"value6"}},"strArr":["value1","value2"],"intArr":[1,2],"realArr":[1.0,2.0],"boolArr":[true,false],"versionArr":["version<1.2.3.qualifier>","version<4.5.6.qualifier>"]}
+
diff --git a/libs/utils/fuzzing/properties_corpus/simple.json b/libs/utils/fuzzing/properties_corpus/simple.json
new file mode 100644
index 0000000..29f05f6
--- /dev/null
+++ b/libs/utils/fuzzing/properties_corpus/simple.json
@@ -0,0 +1 @@
+{"key":"value"}
diff --git a/libs/utils/fuzzing/src/FilterFuzz.cc b/libs/utils/fuzzing/src/FilterFuzz.cc
new file mode 100644
index 0000000..ddc5021
--- /dev/null
+++ b/libs/utils/fuzzing/src/FilterFuzz.cc
@@ -0,0 +1,23 @@
+#include <celix_filter.h>
+#include <celix_err.h>
+#include <cstdint>
+#include <cstdlib>
+#include <cstring>
+
+int filterParseFuzzOneInput(const uint8_t* data, size_t size) {
+ char* buffer = static_cast<char*>(malloc(size + 1));
+ if (buffer == nullptr) {
+ return 0;
+ }
+ memcpy(buffer, data, size);
+ buffer[size] = '\0';
+
+ celix_filter_t* filter = celix_filter_create(buffer);
+ celix_filter_destroy(filter);
+ celix_err_resetErrors();
+
+ free(buffer);
+ return 0;
+}
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { return filterParseFuzzOneInput(data, size); }
diff --git a/libs/utils/fuzzing/src/PropertiesFuzz.cc b/libs/utils/fuzzing/src/PropertiesFuzz.cc
new file mode 100644
index 0000000..627c2dd
--- /dev/null
+++ b/libs/utils/fuzzing/src/PropertiesFuzz.cc
@@ -0,0 +1,32 @@
+#include <celix_properties.h>
+#include <celix_err.h>
+#include <cstdint>
+#include <cstdlib>
+#include <cstring>
+
+int propertiesParserFuzzOneInput(const uint8_t* data, size_t size) {
+ int flags = 0;
+ if (size > 0) {
+ flags = data[0] & 0x3F; // use 6 bits for decode flags
+ data += 1;
+ size -= 1;
+ }
+ char* buffer = static_cast<char*>(malloc(size + 1));
+ if (buffer == nullptr) {
+ return 0;
+ }
+ memcpy(buffer, data, size);
+ buffer[size] = '\0';
+
+ celix_properties_t* props = nullptr;
+ celix_properties_loadFromString(buffer, flags, &props);
+ celix_properties_destroy(props);
+ celix_err_resetErrors();
+
+ free(buffer);
+ return 0;
+}
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+ return propertiesParserFuzzOneInput(data, size);
+}
diff --git a/libs/utils/fuzzing/src/VersionFuzz.cc b/libs/utils/fuzzing/src/VersionFuzz.cc
new file mode 100644
index 0000000..b1fc7e6
--- /dev/null
+++ b/libs/utils/fuzzing/src/VersionFuzz.cc
@@ -0,0 +1,23 @@
+#include <celix_version.h>
+#include <celix_err.h>
+#include <cstdint>
+#include <cstdlib>
+#include <cstring>
+
+int versionParseFuzzOneInput(const uint8_t* data, size_t size) {
+ char* buffer = static_cast<char*>(malloc(size + 1));
+ if (buffer == nullptr) {
+ return 0;
+ }
+ memcpy(buffer, data, size);
+ buffer[size] = '\0';
+
+ celix_version_t* version = celix_version_createVersionFromString(buffer);
+ celix_version_destroy(version);
+ celix_err_resetErrors();
+
+ free(buffer);
+ return 0;
+}
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { return versionParseFuzzOneInput(data, size); }
diff --git a/libs/utils/fuzzing/version_corpus/qualifier.txt b/libs/utils/fuzzing/version_corpus/qualifier.txt
new file mode 100644
index 0000000..da3ad8b
--- /dev/null
+++ b/libs/utils/fuzzing/version_corpus/qualifier.txt
@@ -0,0 +1 @@
+2.0.0.qualifier
diff --git a/libs/utils/fuzzing/version_corpus/simple.txt b/libs/utils/fuzzing/version_corpus/simple.txt
new file mode 100644
index 0000000..0495c4a
--- /dev/null
+++ b/libs/utils/fuzzing/version_corpus/simple.txt
@@ -0,0 +1 @@
+1.2.3