diff --git a/.github/scripts/run-drmemory.ps1 b/.github/scripts/run-drmemory.ps1 index 67f7a87dd..376b53664 100644 --- a/.github/scripts/run-drmemory.ps1 +++ b/.github/scripts/run-drmemory.ps1 @@ -27,7 +27,7 @@ $ErrorActionPreference = "Stop" function Get-LeakCount { param( [Parameter(Mandatory = $true)] - [string]$Results, + [string[]]$Results, [Parameter(Mandatory = $true)] [string]$Category @@ -35,16 +35,17 @@ function Get-LeakCount { $escapedCategory = [regex]::Escape($Category) $pattern = "(?m)^\s*(?:~~Dr\.M~~\s+)?([\d,]+) unique,\s+([\d,]+) total,\s+([\d,]+) byte\(s\) of $escapedCategory\r?$" - $match = [regex]::Match($Results, $pattern) - if (-not $match.Success) { - throw "Dr. Memory results do not contain the '$Category' summary." - } - - return @{ - Unique = [int64]($match.Groups[1].Value -replace ",", "") - Total = [int64]($match.Groups[2].Value -replace ",", "") - Bytes = [int64]($match.Groups[3].Value -replace ",", "") + $counts = @{ Unique = [int64]0; Total = [int64]0; Bytes = [int64]0 } + foreach ($result in $Results) { + $match = [regex]::Match($result, $pattern) + if (-not $match.Success) { + throw "Dr. Memory results do not contain the '$Category' summary." + } + $counts.Unique += [int64]($match.Groups[1].Value -replace ",", "") + $counts.Total += [int64]($match.Groups[2].Value -replace ",", "") + $counts.Bytes += [int64]($match.Groups[3].Value -replace ",", "") } + return $counts } $resolvedDrMemoryPath = (Resolve-Path -LiteralPath $DrMemoryPath).Path @@ -70,11 +71,12 @@ $resultFiles = @(Get-ChildItem -LiteralPath $scenarioDirectory -Filter results.t $resultFiles = @($resultFiles | Where-Object { Select-String -LiteralPath $_.FullName -Pattern '^(?:NO )?ERRORS FOUND:\r?$' -Quiet }) -if ($resultFiles.Count -ne 1) { - throw "Expected one completed Dr. Memory results.txt for $Scenario, found $($resultFiles.Count)." +if ($resultFiles.Count -eq 0) { + throw "Expected at least one completed Dr. Memory results.txt for $Scenario, found none." } -$results = Get-Content -LiteralPath $resultFiles[0].FullName -Raw +# Forked tests produce separate reports; include every completed process. +$results = @($resultFiles | ForEach-Object { Get-Content -LiteralPath $_.FullName -Raw }) $leaks = Get-LeakCount -Results $results -Category "leak(s)" $possibleLeaks = Get-LeakCount -Results $results -Category "possible leak(s)" $reachable = Get-LeakCount -Results $results -Category "still-reachable allocation(s)" diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index 1e167f762..3bc84c4b1 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -9,6 +9,7 @@ on: - dev/* - release/* - buildme/* + - fix/* - maharrim/* pull_request: @@ -30,6 +31,35 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: + no-exceptions: + name: Android without C++ exceptions (${{ matrix.abi }}, Room=${{ matrix.room }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + abi: [armeabi-v7a, arm64-v8a, x86, x86_64] + room: [OFF, ON] + steps: + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - name: Configure full JNI SDK + run: >- + cmake -S . -B out-no-exceptions -G Ninja + -DCMAKE_TOOLCHAIN_FILE="$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake" + -DANDROID_ABI=${{ matrix.abi }} + -DANDROID_PLATFORM=android-24 + -DCMAKE_BUILD_TYPE=Debug + -DMATSDK_DISABLE_EXCEPTIONS=ON + -DMATSDK_BUILD_JNI_WRAPPER=ON + -DMATSDK_ANDROID_USE_ROOM=${{ matrix.room }} + -DMATSDK_DISABLE_LOGGING=${{ matrix.room }} + -DMATSDK_BUILD_UNIT_TESTS=OFF + -DMATSDK_BUILD_FUNC_TESTS=OFF + -DMATSDK_BUILD_TEST_TOOL=OFF + -DMATSDK_BUILD_PACKAGE=OFF + - name: Compile full SDK + run: cmake --build out-no-exceptions --target mat --parallel 2 + build: runs-on: windows-latest name: Build for Android diff --git a/.github/workflows/build-ios-mac.yml b/.github/workflows/build-ios-mac.yml index d7687200d..773c04748 100644 --- a/.github/workflows/build-ios-mac.yml +++ b/.github/workflows/build-ios-mac.yml @@ -9,6 +9,7 @@ on: - dev/* - release/* - buildme/* + - fix/* pull_request: branches: @@ -31,6 +32,34 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: + no-exceptions: + name: iOS without C++ exceptions (${{ matrix.sdk }}) + runs-on: macos-latest + strategy: + matrix: + sdk: [iphoneos, iphonesimulator] + steps: + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - name: Configure SDK and Objective-C wrappers + run: >- + cmake -S . -B out-no-exceptions -G Xcode + -DCMAKE_SYSTEM_NAME=iOS + -DCMAKE_OSX_SYSROOT=${{ matrix.sdk }} + -DCMAKE_OSX_ARCHITECTURES=arm64 + -DCMAKE_OSX_DEPLOYMENT_TARGET=13.0 + -DMATSDK_DISABLE_EXCEPTIONS=ON + -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED=NO + -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED=NO + -DMATSDK_BUILD_OBJC_WRAPPER=ON + -DMATSDK_BUILD_SWIFT_WRAPPER=OFF + -DMATSDK_BUILD_UNIT_TESTS=OFF + -DMATSDK_BUILD_FUNC_TESTS=OFF + -DMATSDK_BUILD_TEST_TOOL=OFF + -DMATSDK_BUILD_PACKAGE=OFF + - name: Compile full SDK + run: cmake --build out-no-exceptions --config Debug --target mat --parallel 2 + build: strategy: matrix: diff --git a/.github/workflows/build-posix-latest.yml b/.github/workflows/build-posix-latest.yml index fa75b6733..2ed2e41a8 100644 --- a/.github/workflows/build-posix-latest.yml +++ b/.github/workflows/build-posix-latest.yml @@ -9,6 +9,7 @@ on: - dev/* - release/* - buildme/* + - fix/* pull_request: branches: @@ -31,6 +32,17 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: + build-runner: + name: Build runner contracts (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - name: Verify test-runner failure propagation + run: python3 tests/build-runner-tests.py + build: runs-on: ${{ matrix.os }} @@ -92,23 +104,42 @@ jobs: run: bash tests/headers/check_public_headers.sh no-exceptions: - name: Linux Curl without C++ exceptions - runs-on: ubuntu-latest + name: Full SDK without C++ exceptions (${{ matrix.os }}, ${{ matrix.compiler }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + compiler: gcc + cc: gcc + cxx: g++ + - os: ubuntu-latest + compiler: clang + cc: clang + cxx: clang++ + - os: macos-latest + compiler: appleclang + cc: clang + cxx: clang++ + env: + CC: ${{ matrix.cc }} + CXX: ${{ matrix.cxx }} steps: - name: Checkout uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - - name: Install Curl development package - run: sudo apt-get update && sudo apt-get install -y libcurl4-openssl-dev + - name: Install build dependencies + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y clang libcurl4-openssl-dev libsqlite3-dev zlib1g-dev - name: Configure run: >- - cmake -S . -B out-no-exceptions + cmake -S tests/no-exceptions -B out-no-exceptions -DCMAKE_BUILD_TYPE=Debug - -DCMAKE_CXX_FLAGS=-fno-exceptions - -DMATSDK_BUILD_UNIT_TESTS=OFF - -DMATSDK_BUILD_FUNC_TESTS=OFF - -DMATSDK_BUILD_TEST_TOOL=OFF - - name: Compile Curl transport + - name: Compile full SDK run: >- - make -C out-no-exceptions - -f lib/CMakeFiles/mat.dir/build.make - lib/CMakeFiles/mat.dir/http/HttpClient_Curl.cpp.o + cmake --build out-no-exceptions --target no-exceptions-smoke --parallel 2 + - name: Exercise no-exceptions runtime failure paths + run: ctest --test-dir out-no-exceptions --output-on-failure + - name: Public headers and exception macro regression + if: runner.os == 'macOS' + run: bash tests/headers/check_public_headers.sh diff --git a/.github/workflows/build-windows-vs2022.yaml b/.github/workflows/build-windows-vs2022.yaml index c6efdb426..168393173 100644 --- a/.github/workflows/build-windows-vs2022.yaml +++ b/.github/workflows/build-windows-vs2022.yaml @@ -6,6 +6,7 @@ on: - master - main - dev + - fix/* pull_request: branches: @@ -20,6 +21,39 @@ permissions: contents: read jobs: + no-exceptions: + name: Windows without C++ exceptions (${{ matrix.compiler }}, WinInet=${{ matrix.wininet }}) + runs-on: windows-2022 + strategy: + fail-fast: false + matrix: + compiler: [msvc, clang-cl] + wininet: [OFF, ON] + steps: + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - name: Configure full SDK + shell: pwsh + run: | + $options = @( + '-S', 'tests/no-exceptions', '-B', 'out-no-exceptions', + '-G', 'Visual Studio 17 2022', '-A', 'x64', + '-DMATSDK_USE_WININET=${{ matrix.wininet }}' + ) + if ('${{ matrix.compiler }}' -eq 'clang-cl') { + $options += @('-T', 'ClangCL') + } + cmake @options + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + - name: Compile full SDK + run: cmake --build out-no-exceptions --config Debug --target no-exceptions-smoke --parallel 2 + - name: Exercise no-exceptions runtime failure paths + run: ctest --test-dir out-no-exceptions -C Debug --output-on-failure + - name: Public headers and exception macro regression + if: matrix.compiler == 'msvc' && matrix.wininet == 'OFF' + shell: cmd + run: tests\headers\check_public_headers.cmd + build: runs-on: windows-2022 diff --git a/.github/workflows/memory-leak-analysis.yml b/.github/workflows/memory-leak-analysis.yml index 982ca149e..be5cb8778 100644 --- a/.github/workflows/memory-leak-analysis.yml +++ b/.github/workflows/memory-leak-analysis.yml @@ -9,6 +9,7 @@ on: - main paths: - .github/scripts/run-drmemory.ps1 + - tests/memory-leak-analysis/** - .github/memory-leak-baseline.csv - .github/workflows/memory-leak-analysis.yml - CMakeLists.txt @@ -28,6 +29,7 @@ on: - main paths: - .github/scripts/run-drmemory.ps1 + - tests/memory-leak-analysis/** - .github/memory-leak-baseline.csv - .github/workflows/memory-leak-analysis.yml @@ -51,6 +53,10 @@ jobs: - name: Checkout uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - name: Test Dr. Memory report parsing + shell: pwsh + run: ./tests/memory-leak-analysis/run-drmemory-tests.ps1 + - name: Initialize googletest run: git submodule update --init --depth=1 third_party/googletest @@ -170,6 +176,10 @@ jobs: - name: Checkout uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - name: Test Dr. Memory report parsing + shell: pwsh + run: ./tests/memory-leak-analysis/run-drmemory-tests.ps1 + - name: Initialize googletest run: git submodule update --init --depth=1 third_party/googletest diff --git a/.github/workflows/test-embedding.yml b/.github/workflows/test-embedding.yml index 5c13e1e25..54980ed04 100644 --- a/.github/workflows/test-embedding.yml +++ b/.github/workflows/test-embedding.yml @@ -82,7 +82,7 @@ jobs: - name: Build and run run: | cmake --build build-embedding --target embedding_test --parallel 4 - ./build-embedding/embedding_test + ctest --test-dir build-embedding --output-on-failure windows: runs-on: windows-2022 @@ -98,7 +98,7 @@ jobs: shell: pwsh run: | cmake --build build-embedding --config Release --target embedding_test -- /m - & .\build-embedding\Release\embedding_test.exe + ctest --test-dir build-embedding -C Release --output-on-failure installed-package-linux: runs-on: ubuntu-latest @@ -146,7 +146,7 @@ jobs: -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_PREFIX_PATH="${RUNNER_TEMP}/matsdk" cmake --build build-consumer --parallel 4 - ./build-consumer/vcpkg_test + ctest --test-dir build-consumer --output-on-failure installed-package-macos: runs-on: macos-latest @@ -171,7 +171,7 @@ jobs: -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_PREFIX_PATH="${RUNNER_TEMP}/matsdk" cmake --build build-consumer --parallel 4 - ./build-consumer/vcpkg_test + ctest --test-dir build-consumer --output-on-failure macos: runs-on: macos-latest diff --git a/.github/workflows/test-vcpkg.yml b/.github/workflows/test-vcpkg.yml index 7cae03115..48ff3d718 100644 --- a/.github/workflows/test-vcpkg.yml +++ b/.github/workflows/test-vcpkg.yml @@ -33,14 +33,17 @@ jobs: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Test release port promotion run: python tests/vcpkg/test-release-port.py + - name: Verify native feature mappings + run: cmake -P tests/vcpkg/native-feature-tests.cmake windows: runs-on: windows-latest - name: Windows (x64-windows-static, ${{ matrix.transport }}) + name: Windows (x64-windows-static, ${{ matrix.transport }}, ${{ matrix.features }}) strategy: fail-fast: false matrix: transport: [WinHTTP, WinInet] + features: [default, "minimal-sqlite;no-exceptions;no-logging"] steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 @@ -56,6 +59,9 @@ jobs: if ("${{ matrix.transport }}" -eq "WinInet") { $arguments.WinInet = $true } + if ("${{ matrix.features }}" -ne "default") { + $arguments.Features = "${{ matrix.features }}".Split(';') + } .\tests\vcpkg\test-vcpkg-windows.ps1 @arguments shell: pwsh @@ -65,7 +71,7 @@ jobs: strategy: fail-fast: false matrix: - features: [default, minimal-sqlite] + features: [default, minimal-sqlite, "minimal-sqlite;no-exceptions;no-logging"] steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 @@ -123,7 +129,7 @@ jobs: strategy: fail-fast: false matrix: - features: [default, minimal-sqlite, "minimal-sqlite;android-curl-openssl", "minimal-sqlite;android-curl-mbedtls"] + features: [default, minimal-sqlite, "minimal-sqlite;android-curl-openssl", "minimal-sqlite;android-curl-mbedtls", "minimal-sqlite;no-exceptions;no-logging;android-capi-http-client"] steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 diff --git a/CMakeLists.txt b/CMakeLists.txt index 7835483cf..d47c0167a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,42 +58,7 @@ message(STATUS "CMAKE_CXX_COMPILER_ID: ${CMAKE_CXX_COMPILER_ID}") include(tools/ParseOsRelease.cmake) -# SDK-owned compiler policy. This interface target is linked PRIVATE by SDK -# targets, so add_subdirectory()/FetchContent consumers and vendored dependency -# targets never inherit the SDK's warning-as-error or optimization policy. -add_library(matsdk_build_options INTERFACE) -if(MSVC) - target_compile_options(matsdk_build_options INTERFACE - /W4 - $<$:/WX> - /Gy - $<$:/Gw>) -else() - target_compile_options(matsdk_build_options INTERFACE - -Wall - -Wextra - -Wno-unused-parameter - -Wno-unused-but-set-variable - $<$:-Werror> - $<$:-Wno-unknown-warning-option> - $<$:-Wno-unknown-warning-option> - $<$:-ggdb> - $<$:-gdwarf-2> - $<$:-O0> - $<$:-fno-builtin-malloc> - $<$:-fno-builtin-calloc> - $<$:-fno-builtin-realloc> - $<$:-fno-builtin-free> - $<$>:-Os> - $<$>:-fmerge-all-constants> - -ffunction-sections - $<$:-fdata-sections>) - if(NOT WIN32) - target_compile_options(matsdk_build_options INTERFACE - -fvisibility=hidden - $<$:-fvisibility-inlines-hidden>) - endif() -endif() +include(cmake/MatsdkCompilerOptions.cmake) include(tools/Utils.cmake) include(GNUInstallDirs) diff --git a/build-tests.sh b/build-tests.sh index eecd7fd87..c3beffd79 100755 --- a/build-tests.sh +++ b/build-tests.sh @@ -1,13 +1,19 @@ #!/bin/sh +set -e + cd "${0%/*}" SKU=${1:-release} -echo Building and running $SKU tests... -CMAKE_OPTS="${CMAKE_OPTS} -DMATSDK_BUILD_UNIT_TESTS=ON -DMATSDK_BUILD_FUNC_TESTS=ON" \ - ./build.sh ${SKU} -# Fail on test errors -set -e +echo "Building and running $SKU tests..." +CMAKE_OPTS="${CMAKE_OPTS:-} -DMATSDK_BUILD_UNIT_TESTS=ON -DMATSDK_BUILD_FUNC_TESTS=ON" \ + ./build.sh "$SKU" cd out -./tests/functests/FuncTests --gtest_filter=-APITest.C_API_Test # TBD - CAPI tests are failing in CI. Need to be fixed -./tests/unittests/UnitTests -./tests/functests/FuncTests --gtest_filter=MultipleLogManagersTests.MultiProcessesLogManager & \ +ctest --output-on-failure + +./tests/functests/FuncTests --gtest_filter=MultipleLogManagersTests.MultiProcessesLogManager & +first_pid=$! ./tests/functests/FuncTests --gtest_filter=MultipleLogManagersTests.MultiProcessesLogManager & +second_pid=$! +status=0 +wait "$first_pid" || status=$? +wait "$second_pid" || status=$? +exit "$status" diff --git a/cmake/MatsdkCompilerOptions.cmake b/cmake/MatsdkCompilerOptions.cmake new file mode 100644 index 000000000..263d081e1 --- /dev/null +++ b/cmake/MatsdkCompilerOptions.cmake @@ -0,0 +1,50 @@ +# SDK-owned policy stays private to SDK targets and out of dependency builds. +add_library(matsdk_build_options INTERFACE) +if(MSVC) + target_compile_options(matsdk_build_options INTERFACE + /W4 + $<$:/WX> + /Gy + $<$:/Gw>) +else() + target_compile_options(matsdk_build_options INTERFACE + -Wall + -Wextra + -Wno-unused-parameter + -Wno-unused-but-set-variable + $<$:-Werror> + $<$:-Wno-unknown-warning-option> + $<$:-Wno-unknown-warning-option> + $<$:-ggdb> + $<$:-gdwarf-2> + $<$:-O0> + $<$:-fno-builtin-malloc> + $<$:-fno-builtin-calloc> + $<$:-fno-builtin-realloc> + $<$:-fno-builtin-free> + $<$>:-Os> + $<$>:-fmerge-all-constants> + -ffunction-sections + $<$:-fdata-sections>) + if(NOT WIN32) + target_compile_options(matsdk_build_options INTERFACE + -fvisibility=hidden + $<$:-fvisibility-inlines-hidden>) + endif() +endif() + +if(MATSDK_DISABLE_EXCEPTIONS) + if(MSVC) + target_compile_options(matsdk_build_options INTERFACE + $<$:/EHs-c-> + $<$:/clang:-fno-exceptions>) + target_compile_definitions(matsdk_build_options INTERFACE + $<$:_HAS_EXCEPTIONS=0>) + elseif(CMAKE_CXX_COMPILER_ID MATCHES "^(GNU|Clang|AppleClang)$") + target_compile_options(matsdk_build_options INTERFACE + $<$:-fno-exceptions>) + else() + message(FATAL_ERROR + "MATSDK_DISABLE_EXCEPTIONS is unsupported with ${CMAKE_CXX_COMPILER_ID}.") + endif() +endif() diff --git a/cmake/MatsdkOptions.cmake b/cmake/MatsdkOptions.cmake index 71a400ab1..433f035b3 100644 --- a/cmake/MatsdkOptions.cmake +++ b/cmake/MatsdkOptions.cmake @@ -46,6 +46,8 @@ option(MATSDK_USE_WININET "Use WinInet instead of WinHTTP as the Win32 desktop HTTP client" OFF) option(MATSDK_DISABLE_LOGGING "Compile internal SDK logging out" OFF) +option(MATSDK_DISABLE_EXCEPTIONS + "Compile SDK-owned C++ targets without exception support" OFF) option(MATSDK_ENABLE_DEVICE_ID "Collect a device ID using the SDK's platform-specific implementation" ON) diff --git a/docs/Offline-storage-settings.md b/docs/Offline-storage-settings.md index a3f2fd60f..c4f49d72a 100644 --- a/docs/Offline-storage-settings.md +++ b/docs/Offline-storage-settings.md @@ -9,10 +9,15 @@ There are several configurations that can alter the offline storage handler beha | CFG_INT_CACHE_FILE_SIZE | int | 3145728 | Sets size limit for the cache file. | CFG_INT_STORAGE_FULL_PCT | int | 75 | Sets the notification threshold (percentage) for storage full notifications. If the cache file size excceds CFG_INT_STORAGE_FULL_PCT percent, an EVT_STORAGE_FULL debug event will be fired. | CFG_INT_STORAGE_FULL_CHECK_TIME | int | 5000 | Sets the minimum time (ms) between storage full notifications. -| CFG_BOOL_ENABLE_DB_DROP_IF_FULL | bool | false | When set to true, trim events if cache size reaches CFG_INT_CACHE_FILE_SIZE +| CFG_BOOL_ENABLE_DB_DROP_IF_FULL | bool | false | When set to true, trim events if the SQLite database size, including page and index overhead, exceeds CFG_INT_CACHE_FILE_SIZE. | CFG_STR_CACHE_FILE_PATH | string | %TEMP% | Sets the path for the cache file | skipSqliteInitAndShutdown | string | unset | When set to `"true"`, the SDK skips its process-wide `sqlite3_initialize()` and `sqlite3_shutdown()` calls for offline storage. +When `CFG_BOOL_ENABLE_DB_DROP_IF_FULL` is enabled, SQLite's allocated page size +is checked after each stored record or batch. This check is independent of +`CFG_INT_STORAGE_FULL_CHECK_TIME`, which only throttles storage-full notifications. +The size does not include separate WAL or shared-memory files. + ## Shared SQLite lifecycle ownership By default, the SDK initializes SQLite when the first offline-storage-backed instance starts and shuts SQLite down after the last such instance is released. diff --git a/docs/building-with-vcpkg.md b/docs/building-with-vcpkg.md index 5e37272cb..53531cc03 100644 --- a/docs/building-with-vcpkg.md +++ b/docs/building-with-vcpkg.md @@ -311,6 +311,31 @@ will automatically use the optimized zlib-ng build. > zlib. When using `ZLIB_COMPAT=ON`, ensure all dependencies resolve to > zlib-ng rather than mixing stock zlib and zlib-ng. +## Native build options + +| vcpkg feature | SDK CMake option | Behavior | +| ------------- | ---------------- | -------- | +| `no-exceptions` | `MATSDK_DISABLE_EXCEPTIONS=ON` | Disable C++ exceptions in SDK-owned targets only. Consumer and dependency exception policies are unchanged. | +| `no-logging` | `MATSDK_DISABLE_LOGGING=ON` | Remove internal diagnostics, not telemetry event collection. | +| `device-id` (default) | `MATSDK_ENABLE_DEVICE_ID=ON` | Enable native device-ID collection; omit with `default-features=false` to disable it. | +| `android-capi-http-client` | `MATSDK_ENABLE_CAPI_HTTP_CLIENT=ON` | Allow custom HTTP send/cancel callbacks through the Android C API. | + +These features compose with the SQLite and HTTP transport features. For example, +select `["minimal-sqlite", "curl-mbedtls", "no-exceptions", "no-logging"]` +with `default-features=false` for a Linux build without native device-ID +collection. Exception-free builds can still terminate on unrecoverable allocation +failure; they do not turn every standard-library allocation into a recoverable +operation. + +Static/shared linkage, architecture, CRT selection, optimization, and additional +compiler/linker flags belong to the vcpkg triplet/toolchain, not SDK features. +The port builds the native core and disables tests, sample tools, language +wrappers, and SDK packaging. Room storage and JNI/Objective-C/Swift wrappers +remain source-build integrations; proprietary modules require a source checkout. +Dependency download URLs and vendored zlib are source-build settings, since +vcpkg manages those dependencies. +RTTI remains required by the SDK's HTTP cancellation implementation. + ## Reducing binary footprint This section applies when the SDK is linked **statically** into your binary diff --git a/docs/linux-setup-build.md b/docs/linux-setup-build.md index 2e045fa59..4e89f0eb3 100644 --- a/docs/linux-setup-build.md +++ b/docs/linux-setup-build.md @@ -72,3 +72,59 @@ make ``` Package for your platform is going to be created and placed in ./out directory. + +When the optional Azure Monitor module is present and `MATSDK_BUILD_AZMON` and +`MATSDK_BUILD_FUNC_TESTS` are enabled, CMake includes `AISendTests` in `FuncTests`. +These loopback tests wait for server notifications and do not require host +network discovery. + +### Building without C++ exceptions + +Set `MATSDK_DISABLE_EXCEPTIONS=ON` to compile SDK-owned C++ targets without +exceptions using GCC, Clang, Apple Clang, MSVC, or clang-cl: + +```console +cmake -S . -B out-no-exceptions \ + -DCMAKE_BUILD_TYPE=Debug \ + -DMATSDK_DISABLE_EXCEPTIONS=ON \ + -DMATSDK_BUILD_UNIT_TESTS=OFF \ + -DMATSDK_BUILD_FUNC_TESTS=OFF \ + -DMATSDK_BUILD_TEST_TOOL=OFF +cmake --build out-no-exceptions --target mat --parallel 2 +``` + +Select Clang with `CC=clang CXX=clang++` when configuring a fresh build +directory. CI builds the full public SDK `mat` target with both compilers, +rather than compiling only the Curl transport object. It does not clone the +private optional-modules repository. Exception handlers are retained in +exception-enabled builds and omitted when exceptions are disabled. Numeric +validation does not rely on throwing conversions, and payload expansion +reports buffer-allocation failure through its existing `false` result. +Disabling exceptions does not make every standard-library allocation +recoverable; operations without an explicit non-throwing failure path can +still terminate on allocation failure. + +The same option applies to Android NDK and Apple Clang builds, including the +JNI and Objective-C wrappers. For Android, also pass +`-DMATSDK_BUILD_JNI_WRAPPER=ON`; both SQLite and +`-DMATSDK_ANDROID_USE_ROOM=ON` storage builds are covered. For Apple builds, +disable the independent Swift build with `-DMATSDK_BUILD_SWIFT_WRAPPER=OFF`. +With the Xcode generator, also set +`-DCMAKE_XCODE_ATTRIBUTE_GCC_ENABLE_CPP_EXCEPTIONS=NO`. +Objective-C exception handling can remain enabled independently; the SDK detects +C++ exception support separately, including in Objective-C++ translation units. + +The option selects `/EHs-c-` and `_HAS_EXCEPTIONS=0` for MSVC, and additionally +`/clang:-fno-exceptions` for clang-cl. It does not change compiler flags on +consumer or dependency targets. Explicit compiler flags remain supported. +CI covers both +WinHTTP and WinInet transports, macOS, iOS device/simulator, and all four +Android ABIs with both storage backends. Managed C++/CLI and C++/CX wrappers +require their platform exception machinery and are not exception-free native +build targets. + +The existing `MATSDK_THROW` policy is unchanged: when C++ exceptions are +disabled, paths that would explicitly throw terminate via `std::abort()`, +including unrecoverable JNI/Room errors. Room record-ID validation instead +uses a non-throwing conversion and reports invalid or overflowing IDs through +the storage observer. diff --git a/docs/maintainer-onboarding.md b/docs/maintainer-onboarding.md index 02209f911..47e9111e0 100644 --- a/docs/maintainer-onboarding.md +++ b/docs/maintainer-onboarding.md @@ -161,9 +161,9 @@ prerequisites installed: ./tests/vcpkg/test-vcpkg-linux.sh ``` -`build-tests.sh` currently excludes `APITest.C_API_Test`. Do not describe that -run as complete C API coverage, and re-evaluate the exclusion when changing the -C API or its implementation. +`build-tests.sh` runs the registered unit and functional suites through CTest, +including the C API test, then waits for both concurrent multi-process checks. +Build failures and failures from either concurrent process fail the runner. ### vcpkg consumer tests @@ -261,6 +261,13 @@ Leak-count increases produce warnings, not failures: a green job does not mean the change introduced no leaks. Instrumentation or target failures fail the job, and Windows also fails if `netprofm.dll` is loaded again. +Forked tests can produce multiple completed process reports. The runner sums +their counts and bytes into one scenario summary (unique counts are summed +per process, not deduplicated across processes). Header-only reports are +ignored, but missing completed reports or malformed summaries fail the job. +Run `pwsh -File tests/memory-leak-analysis/run-drmemory-tests.ps1` to check +the parser without installing Dr. Memory. + Two tests remain in normal CI but are excluded under instrumentation: `BasicFuncTests.killSwitchWorks` because Dr. Memory changes its asynchronous drop count, and diff --git a/lib/api/CorrelationVector.cpp b/lib/api/CorrelationVector.cpp index 3dfaf2159..6aba5a194 100644 --- a/lib/api/CorrelationVector.cpp +++ b/lib/api/CorrelationVector.cpp @@ -7,7 +7,7 @@ #include #include -#include +#include #include using std::string; @@ -275,41 +275,27 @@ namespace MAT_NS_BEGIN else { size_t lastDot = cv.find_last_of("."); - bool parsingFailed = false; string vectorString = cv.substr(lastDot + 1, string::npos); - // note: unsigned long is 32-bit on 32-bit arm devices - unsigned long currentVector = 0; - - try - { - // do a manual string comparison before trying to parse the value to avoid throwing an exception - if (vectorString.length() == 0 || - vectorString.length() > s_maxVectorElementValue.length() || - (vectorString.length() == s_maxVectorElementValue.length() && vectorString > s_maxVectorElementValue)) - { - parsingFailed = true; - } - else - { - currentVector = std::stoul(vectorString); - } - } - catch (std::invalid_argument&) + if (vectorString.empty() || + vectorString.length() > s_maxVectorElementValue.length() || + (vectorString.length() == s_maxVectorElementValue.length() && vectorString > s_maxVectorElementValue)) { - parsingFailed = true; + return false; } - catch (std::out_of_range&) + + // Digits and the 32-bit upper bound have already been validated. + uint64_t currentVector = 0; + for (char digit : vectorString) { - parsingFailed = true; + currentVector = currentVector * 10 + static_cast(digit - '0'); } - - if (parsingFailed || currentVector > std::numeric_limits::max()) + if (currentVector > std::numeric_limits::max()) { return false; } m_baseVector = cv.substr(0, lastDot); - m_currentVector = currentVector; + m_currentVector = static_cast(currentVector); } m_maxLength = maxLength; @@ -319,4 +305,3 @@ namespace MAT_NS_BEGIN } } MAT_NS_END - diff --git a/lib/api/LogConfiguration.cpp b/lib/api/LogConfiguration.cpp index 0eb6581b2..52fabf647 100644 --- a/lib/api/LogConfiguration.cpp +++ b/lib/api/LogConfiguration.cpp @@ -4,6 +4,7 @@ // #include "mat/config.h" #include "LogConfiguration.hpp" +#include "pal/PAL.hpp" #ifdef HAVE_MAT_JSONHPP #include @@ -74,7 +75,17 @@ namespace MAT_NS_BEGIN { { ILogConfiguration result; #ifdef HAVE_MAT_JSONHPP - auto src = json::parse(configuration); + if (configuration == nullptr) + { + LOG_ERROR("JSON configuration is null"); + return result; + } + auto src = json::parse(configuration, nullptr, HAVE_EXCEPTIONS != 0); + if (!src.is_object()) + { + LOG_ERROR("JSON configuration must be a valid object"); + return result; + } std::function parse; parse = [&parse](json &src, VariantMap &dst)->void { for (json::iterator it = src.begin(); it != src.end(); ++it) { diff --git a/lib/api/capi.cpp b/lib/api/capi.cpp index 531d9419c..92bef825e 100644 --- a/lib/api/capi.cpp +++ b/lib/api/capi.cpp @@ -144,12 +144,15 @@ static evt_status_t mat_open_core( // Create custom HttpClient if (httpSendFn != nullptr && httpCancelFn != nullptr) { +#if HAVE_EXCEPTIONS try +#endif { auto http = std::make_shared(httpSendFn, httpCancelFn); clients[code].http = http; clients[code].config.AddModule(CFG_MODULE_HTTP_CLIENT, http); } +#if HAVE_EXCEPTIONS catch (...) { // Roll back the partially-populated client so a later open with the @@ -159,17 +162,21 @@ static evt_status_t mat_open_core( ctx->handle = 0; return EFAULT; } +#endif } #endif // Create custom worker thread if (taskDispatcherQueueFn != nullptr && taskDispatcherCancelFn != nullptr && taskDispatcherJoinFn != nullptr) { +#if HAVE_EXCEPTIONS try +#endif { auto taskDispatcher = std::make_shared(taskDispatcherQueueFn, taskDispatcherCancelFn, taskDispatcherJoinFn); clients[code].taskDispatcher = taskDispatcher; clients[code].config.AddModule(CFG_MODULE_TASK_DISPATCHER, taskDispatcher); } +#if HAVE_EXCEPTIONS catch (...) { // Roll back the partially-populated client so a later open with the @@ -179,6 +186,7 @@ static evt_status_t mat_open_core( ctx->handle = 0; return EFAULT; } +#endif } status_t status = static_cast(EFAULT); diff --git a/lib/decoder/PayloadDecoder.cpp b/lib/decoder/PayloadDecoder.cpp index 5c8ad7adf..25a0231ce 100644 --- a/lib/decoder/PayloadDecoder.cpp +++ b/lib/decoder/PayloadDecoder.cpp @@ -34,6 +34,8 @@ MAT_NS_END #include #include #include +#include +#include #ifdef _WIN32 #include @@ -442,7 +444,9 @@ namespace clienttelemetry { /// /// /// - bool Expand(const char* source, size_t sourceLen, char** dest, size_t& destLen, bool sizeAtZeroIndex) + // Successful custom allocations must be compatible with delete[]. + bool Expand(const char* source, size_t sourceLen, char** dest, size_t& destLen, bool sizeAtZeroIndex, + char* (*allocate)(size_t)) { if (!(source) || !(sourceLen)) { @@ -467,34 +471,36 @@ namespace clienttelemetry { destLen = s32; } + if (destLen > static_cast(std::numeric_limits::max())) + { + TEST_LOG_ERROR("Decompression size is not representable: destLen=%zu", destLen); + destLen = 0; + return false; + } + // Allocate memory for the new uncompressed buffer if (destLen > 0) { - try - { - char* decompBody = new char[destLen]; - if (source != NULL) - { - // Inflate - uLongf len = (uLongf)destLen; - int res = uncompress((Bytef *)decompBody, &len, (const Bytef *)(source + reserved), (uLong)(sourceLen - reserved)); - if ((res != Z_OK) || (len != destLen)) - { - TEST_LOG_ERROR("Decompression failed, error=%d, len=%u, destLen=%u", res, static_cast(len), static_cast(destLen)); - delete[] decompBody; - return false; - } - *dest = decompBody; - destLen = len; - return true; - } - } - catch (std::bad_alloc&) + char* decompBody = allocate(destLen); + if (decompBody == nullptr) { TEST_LOG_ERROR("Decompression failed (out of memory): destLen=%zu", destLen); - dest = NULL; destLen = 0; + return false; } + + // Inflate + uLongf len = (uLongf)destLen; + int res = uncompress((Bytef *)decompBody, &len, (const Bytef *)(source + reserved), (uLong)(sourceLen - reserved)); + if ((res != Z_OK) || (len != destLen)) + { + TEST_LOG_ERROR("Decompression failed, error=%d, len=%u, destLen=%u", res, static_cast(len), static_cast(destLen)); + delete[] decompBody; + return false; + } + *dest = decompBody; + destLen = len; + return true; } // OOM @@ -502,6 +508,11 @@ namespace clienttelemetry { return false; } + bool Expand(const char* source, size_t sourceLen, char** dest, size_t& destLen, bool sizeAtZeroIndex) + { + return Expand(source, sourceLen, dest, destLen, sizeAtZeroIndex, + [](size_t size) { return new (std::nothrow) char[size]; }); + } bool ExpandVector(std::vector& in, std::vector& out) { diff --git a/lib/http/HttpClient_WinHttp.cpp b/lib/http/HttpClient_WinHttp.cpp index 10259e2c9..70707a65f 100644 --- a/lib/http/HttpClient_WinHttp.cpp +++ b/lib/http/HttpClient_WinHttp.cpp @@ -389,7 +389,8 @@ class WinHttpRequestWrapper : public std::enable_shared_from_thishCertStore, &chainPara, 0, NULL, &pChainCtx)) { CERT_CHAIN_POLICY_STATUS pps = { 0, 0, 0, 0, nullptr }; diff --git a/lib/http/HttpClient_WinInet.cpp b/lib/http/HttpClient_WinInet.cpp index a57433aed..667c7d921 100644 --- a/lib/http/HttpClient_WinInet.cpp +++ b/lib/http/HttpClient_WinInet.cpp @@ -1047,6 +1047,7 @@ class WinInetRequestWrapper : public std::enable_shared_from_this(dwContext); if (context == nullptr) diff --git a/lib/http/HttpResponseDecoder.cpp b/lib/http/HttpResponseDecoder.cpp index b46eeff98..803b52bd4 100644 --- a/lib/http/HttpResponseDecoder.cpp +++ b/lib/http/HttpResponseDecoder.cpp @@ -9,6 +9,8 @@ #include "utils/Utils.hpp" #include #include +#include +#include #ifdef HAVE_MAT_JSONHPP #include @@ -190,33 +192,45 @@ namespace MAT_NS_BEGIN { LOG_ERROR("HTTP response: body is not valid JSON, skipping processing"); return; } - - int accepted = 0; - auto acc = responseBody.find("acc"); - if (responseBody.end() != acc) + if (!responseBody.is_object()) { - if (acc.value().is_number()) - { - accepted = acc.value().get(); - } + LOG_ERROR("HTTP response: body is not a JSON object, skipping processing"); + return; } - int rejected = 0; - auto rej = responseBody.find("rej"); - if (responseBody.end() != rej) + const auto readCount = [&responseBody](const char* name, int& count) { - if (rej.value().is_number()) + const auto value = responseBody.find(name); + if (value == responseBody.end() || !value->is_number()) + { + return true; + } + const auto number = value->get(); + if (!std::isfinite(number) || number < 0 || number > std::numeric_limits::max()) { - rejected = rej.value().get(); + return false; } + count = static_cast(number); + return true; + }; + int accepted = 0; + int rejected = 0; + if (!readCount("acc", accepted) || !readCount("rej", rejected)) + { + LOG_ERROR("HTTP response: event count is out of range, skipping processing"); + return; } auto efi = responseBody.find("efi"); if (responseBody.end() != efi) { - for (auto it = responseBody["efi"].begin(); it != responseBody["efi"].end(); ++it) + if (!efi->is_object()) + { + LOG_ERROR("HTTP response: efi is not a JSON object, skipping processing"); + return; + } + for (auto it = efi->begin(); it != efi->end(); ++it) { - std::string efiKey(it.key()); nlohmann::json val = it.value(); if (val.is_array()) { diff --git a/lib/include/public/LogConfiguration.hpp b/lib/include/public/LogConfiguration.hpp index 4cf31d7aa..7b995b8bd 100644 --- a/lib/include/public/LogConfiguration.hpp +++ b/lib/include/public/LogConfiguration.hpp @@ -104,9 +104,11 @@ namespace MAT_NS_BEGIN { MATSDK_LIBABI ILogConfiguration FromLogConfiguration(MAT_v1::LogConfiguration &src); + /// Parses a JSON configuration object. Null input or a non-object root + /// returns an empty configuration and logs an error. Invalid JSON syntax + /// throws when exceptions are enabled; otherwise it logs and returns empty. MATSDK_LIBABI ILogConfiguration FromJSON(const char* json); } MAT_NS_END #endif - diff --git a/lib/include/public/TransmitProfiles.hpp b/lib/include/public/TransmitProfiles.hpp index 37e8e4355..303a859f7 100644 --- a/lib/include/public/TransmitProfiles.hpp +++ b/lib/include/public/TransmitProfiles.hpp @@ -192,13 +192,19 @@ namespace MAT_NS_BEGIN /// /// Parses transmit profiles from JSON. + /// Schema errors stop parsing at the first invalid profile. The valid + /// prefix replaces existing custom profiles; defaults remain available. + /// Entries after the first invalid profile are not loaded. /// /// A string that contains the the transmit profiles in JSON. - /// The size (in bytes) of the resulting TransmitProfiles object. + /// The number of profiles loaded, including a valid partial prefix. static size_t parse(const std::string& profiles_json); /// /// Loads customer-supplied transmit profiles. + /// Returns true when at least one profile is loaded, even if a later + /// profile has a schema error. This is not an atomic validation API. + /// Use the vector overload to validate all profiles before replacement. /// /// A string that contains the the transmit profiles in JSON. /// A boolean value that indicates success (true) or failure (false) if at least one transmit profile parses correctly. @@ -276,4 +282,3 @@ namespace MAT_NS_BEGIN /// @endcond #endif - diff --git a/lib/include/public/ctmacros.hpp b/lib/include/public/ctmacros.hpp index 026176a04..518e7007a 100644 --- a/lib/include/public/ctmacros.hpp +++ b/lib/include/public/ctmacros.hpp @@ -5,6 +5,10 @@ #ifndef CTMACROS_HPP #define CTMACROS_HPP +#ifdef __cplusplus +#include +#endif + #ifdef HAVE_MAT_SHORT_NS #define MAT_NS_BEGIN MAT #define MAT_NS_END @@ -121,7 +125,14 @@ #endif /* Exceptions support is optional */ -#if (__cpp_exceptions) || defined(__EXCEPTIONS) +#if defined(__clang__) +// Objective-C exceptions can enable __EXCEPTIONS without C++ exceptions. +#if __has_feature(cxx_exceptions) +#define HAVE_EXCEPTIONS 1 +#else +#define HAVE_EXCEPTIONS 0 +#endif +#elif defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND) #define HAVE_EXCEPTIONS 1 #else #define HAVE_EXCEPTIONS 0 diff --git a/lib/jni/LogManager_jni.cpp b/lib/jni/LogManager_jni.cpp index 5d465c72c..e400b6f83 100644 --- a/lib/jni/LogManager_jni.cpp +++ b/lib/jni/LogManager_jni.cpp @@ -402,7 +402,7 @@ namespace if (env->ExceptionCheck()) { env->Throw(env->ExceptionOccurred()); - throw std::runtime_error("JNI exception"); + MATSDK_THROW(std::runtime_error("JNI exception")); } } @@ -604,7 +604,7 @@ namespace if (!TryJStringToStdString(env, key, stringKey)) { rethrow(env); - throw std::runtime_error("Unable to convert configuration key"); + MATSDK_THROW(std::runtime_error("Unable to convert configuration key")); } auto value = env->CallObjectMethod(configuration, getMethod, key); rethrow(env); @@ -664,7 +664,7 @@ namespace if (!TryJStringToStdString(env, s, cppString)) { rethrow(env); - throw std::runtime_error("Unable to convert string value"); + MATSDK_THROW(std::runtime_error("Unable to convert string value")); } return Variant(std::move(cppString)); } @@ -682,7 +682,7 @@ namespace return Variant(std::move(subArray)); } default: - throw std::logic_error("Unknown enum value"); + MATSDK_THROW(std::logic_error("Unknown enum value")); } } // if class matches } // for (... classCache){ @@ -703,7 +703,7 @@ namespace if (!TryJStringToStdString(env, jName, className)) { rethrow(env); - throw std::runtime_error("Unable to convert class name"); + MATSDK_THROW(std::runtime_error("Unable to convert class name")); } __android_log_print(ANDROID_LOG_ERROR, "MAE", @@ -942,10 +942,11 @@ namespace } for (const auto& dataViewer : dataViewers) { - try + MATSDK_TRY { manager->GetDataViewerCollection().UnregisterViewer(dataViewer.first.c_str()); } +#if HAVE_EXCEPTIONS catch (const std::exception& exception) { #ifdef HAVE_MAT_LOGGING @@ -959,6 +960,7 @@ namespace (void)exception; #endif } +#endif } } } @@ -1658,14 +1660,19 @@ Java_com_microsoft_applications_events_LogManagerProvider_00024LogManagerImpl_na return false; } +#if HAVE_EXCEPTIONS bool collectionRegistered = false; - try +#endif + MATSDK_TRY { manager->GetDataViewerCollection().RegisterViewer(proxy); +#if HAVE_EXCEPTIONS collectionRegistered = true; +#endif manager_and_config->javaDataViewers.emplace(proxy->GetName(), proxy); return true; } +#if HAVE_EXCEPTIONS catch (const std::exception& exception) { if (collectionRegistered) @@ -1701,6 +1708,7 @@ Java_com_microsoft_applications_events_LogManagerProvider_00024LogManagerImpl_na #endif return false; } +#endif } extern "C" JNIEXPORT jboolean JNICALL @@ -1737,11 +1745,12 @@ Java_com_microsoft_applications_events_LogManagerProvider_00024LogManagerImpl_na manager_and_config->javaDataViewers.erase(viewer); } - try + MATSDK_TRY { manager->GetDataViewerCollection().UnregisterViewer(name.c_str()); return true; } +#if HAVE_EXCEPTIONS catch (const std::exception& exception) { #ifdef HAVE_MAT_LOGGING @@ -1756,6 +1765,7 @@ Java_com_microsoft_applications_events_LogManagerProvider_00024LogManagerImpl_na #endif return false; } +#endif } extern "C" JNIEXPORT void JNICALL @@ -1877,7 +1887,7 @@ namespace env->DeleteGlobalRef(eventClass); eventClass = nullptr; rethrow(env); - throw std::runtime_error("Unable to retain debug event listener"); + MATSDK_THROW(std::runtime_error("Unable to retain debug event listener")); } env->DeleteLocalRef(localListenerClass); @@ -2066,7 +2076,7 @@ Java_com_microsoft_applications_events_LogManagerProvider_00024LogManagerImpl_na return -1; } - try + MATSDK_TRY { EnsurePendingReleaseCallbackRegistered(); @@ -2130,10 +2140,11 @@ Java_com_microsoft_applications_events_LogManagerProvider_00024LogManagerImpl_na } } - try + MATSDK_TRY { logManager->AddEventListener(eventType, *callback); } +#if HAVE_EXCEPTIONS catch (...) { std::lock_guard lock(listeners_mutex); @@ -2158,6 +2169,7 @@ Java_com_microsoft_applications_events_LogManagerProvider_00024LogManagerImpl_na } throw; } +#endif bool removeCancelledRegistration = false; { @@ -2201,6 +2213,7 @@ Java_com_microsoft_applications_events_LogManagerProvider_00024LogManagerImpl_na std::lock_guard lock(listeners_mutex); return callback->HasLiveRegistrations() ? identity : -1; } +#if HAVE_EXCEPTIONS catch (const std::exception& e) { if (!env->ExceptionCheck()) @@ -2214,6 +2227,7 @@ Java_com_microsoft_applications_events_LogManagerProvider_00024LogManagerImpl_na } return -1; } +#endif } extern "C" diff --git a/lib/modules b/lib/modules index 04409b653..95d333220 160000 --- a/lib/modules +++ b/lib/modules @@ -1 +1 @@ -Subproject commit 04409b6536da5ca9c79f82ea5f37959c43531c57 +Subproject commit 95d33322081ebf0023293ddb0566349482e4e2d7 diff --git a/lib/offline/KillSwitchManager.hpp b/lib/offline/KillSwitchManager.hpp index 244edc537..9bd1e935b 100644 --- a/lib/offline/KillSwitchManager.hpp +++ b/lib/offline/KillSwitchManager.hpp @@ -7,12 +7,12 @@ #include "pal/PAL.hpp" +#include #include #include #include #include #include -#include #include #include #include @@ -211,9 +211,7 @@ namespace MAT_NS_BEGIN { // Parse a count of seconds from a response-header value (Retry-After / // kill-duration). Returns false when the value is malformed or out of - // range instead of letting std::stoll throw: the worker thread that drives - // handleResponse has no exception guard, so a throw here would crash the - // process. + // range using checked arithmetic, without exceptions or allocations. // // RFC 7231 allows Retry-After to be either delay-seconds or an HTTP-date. // We deliberately accept only delay-seconds and ignore the HTTP-date form: @@ -251,32 +249,24 @@ namespace MAT_NS_BEGIN { { return false; } + int64_t parsed = 0; for (size_t i = begin; i < end; ++i) { if (value[i] < '0' || value[i] > '9') { return false; } + const int64_t digit = value[i] - '0'; + if (parsed > (std::numeric_limits::max() - digit) / 10) + { + return false; + } + parsed = parsed * 10 + digit; } - try - { - // The substring is all digits, so std::stoll itself can only throw - // std::out_of_range; substr() may also throw (e.g. std::bad_alloc). - // Either way the std::exception catch below ignores the value rather - // than crashing. - const long long parsed = std::stoll(value.substr(begin, end - begin)); - // Clamp to a value that cannot overflow when later converted to - // milliseconds to compute an expiry time. No legitimate - // Retry-After / kill-duration approaches this; an absurd value is - // capped instead of wrapping the expiry into the past. - const int64_t kMaxSeconds = 100LL * 365 * 24 * 60 * 60; // ~100 years - outSeconds = (parsed > kMaxSeconds) ? kMaxSeconds : static_cast(parsed); - return true; - } - catch (const std::exception&) - { - return false; - } + // Cap valid durations before converting them to milliseconds. + const int64_t kMaxSeconds = 100LL * 365 * 24 * 60 * 60; // ~100 years + outSeconds = std::min(parsed, kMaxSeconds); + return true; } // Tenant tokens are opaque (they may legitimately contain spaces, quotes, diff --git a/lib/offline/OfflineStorage_Room.cpp b/lib/offline/OfflineStorage_Room.cpp index 4f988515e..db074894e 100644 --- a/lib/offline/OfflineStorage_Room.cpp +++ b/lib/offline/OfflineStorage_Room.cpp @@ -4,7 +4,9 @@ // #include "OfflineStorage_Room.hpp" #include "pal/PAL.hpp" +#include #include +#include #include #include @@ -12,6 +14,26 @@ namespace { static constexpr bool s_throwExceptions = true; + void AppendRoomId(const std::string& id, std::vector& ids, + MAT::IOfflineStorageObserver& observer) + { + char* end = nullptr; + errno = 0; + const long long value = std::strtoll(id.c_str(), &end, 10); + if (errno == ERANGE) + { + observer.OnStorageFailed("ID out of range"); + } + else if (end == id.c_str()) + { + observer.OnStorageFailed("Empty ID"); + } + else if (value > 0) + { + ids.push_back(value); + } + } + // RAII guard that deletes a JNI global class reference on all exit paths, // including std::logic_error (ThrowLogic) and std::runtime_error (ThrowRuntime). struct GlobalRefGuard { @@ -111,7 +133,7 @@ namespace MAT_NS_BEGIN void OfflineStorage_Room::ConnectedEnv::popLocalFrame() { - try + MATSDK_TRY { if (push_count > 0) { @@ -119,10 +141,12 @@ namespace MAT_NS_BEGIN --push_count; } } - catch (std::exception e) +#if HAVE_EXCEPTIONS + catch (const std::exception&) { LOG_ERROR("Exception in popLocalFrame"); } +#endif } /** @@ -156,7 +180,7 @@ namespace MAT_NS_BEGIN { if (s_vm && m_room) { - try + MATSDK_TRY { ConnectedEnv env(s_vm); @@ -181,10 +205,12 @@ namespace MAT_NS_BEGIN env->DeleteGlobalRef(m_room); env->ExceptionClear(); } - catch (std::logic_error& e) +#if HAVE_EXCEPTIONS + catch (std::logic_error&) { // just swallow the error } +#endif m_room = nullptr; } } @@ -205,7 +231,7 @@ namespace MAT_NS_BEGIN s_vm = nullptr; env->ExceptionDescribe(); env->ExceptionClear(); - throw std::runtime_error("Unable to acquire JavaVM pointer"); + MATSDK_THROW(std::runtime_error("Unable to acquire JavaVM pointer")); return; } s_context = env->NewGlobalRef(appContext); @@ -270,7 +296,7 @@ namespace MAT_NS_BEGIN HttpHeaders, bool& fromMemory) { - try + MATSDK_TRY { fromMemory = false; if (ids.empty()) @@ -297,23 +323,7 @@ namespace MAT_NS_BEGIN roomIds.reserve(ids.size()); for (auto& id : ids) { - long long n = 0; - try - { - n = std::stoll(id); - if (n > 0) - { - roomIds.push_back(n); - } - } - catch (std::out_of_range e) - { - m_observer->OnStorageFailed("ID out of range"); - } - catch (std::invalid_argument e) - { - m_observer->OnStorageFailed("Empty ID"); - } + AppendRoomId(id, roomIds, *m_observer); } if (roomIds.empty()) { @@ -327,6 +337,7 @@ namespace MAT_NS_BEGIN env->CallLongMethod(m_room, method, ids_java); ThrowRuntime(env, "deleteById"); } +#if HAVE_EXCEPTIONS catch (const std::runtime_error& error) { auto what = error.what(); @@ -337,6 +348,7 @@ namespace MAT_NS_BEGIN LOG_ERROR("Exception in DeleteRecords: %s", what); // do nothing more; no recovery } +#endif } /** @@ -381,7 +393,7 @@ namespace MAT_NS_BEGIN { constexpr int64_t chunkSize = 1024; int64_t requested = maxCount ? maxCount : INT64_MAX; - try + MATSDK_TRY { ConnectedEnv env(s_vm); if (!env) @@ -567,6 +579,7 @@ namespace MAT_NS_BEGIN m_lastReadCount.store(std::min(collected, static_cast(INT32_MAX))); return collected > 0; } +#if HAVE_EXCEPTIONS catch (const std::runtime_error& e) { auto what = e.what(); @@ -577,6 +590,7 @@ namespace MAT_NS_BEGIN LOG_ERROR("Exception in GetAndReserveRecords: %s", what); return false; } +#endif } /** @@ -594,7 +608,7 @@ namespace MAT_NS_BEGIN static constexpr char k_init_string[] = "Room/Init"; m_observer = &observer; - try + MATSDK_TRY { ConnectedEnv env(s_vm); if (!env) @@ -623,6 +637,7 @@ namespace MAT_NS_BEGIN ThrowRuntime(env, "Exception creating global ref to OfflineRoom"); m_observer->OnStorageOpened(k_init_string); } +#if HAVE_EXCEPTIONS catch (const std::runtime_error& error) { auto what = error.what(); @@ -632,6 +647,7 @@ namespace MAT_NS_BEGIN } LOG_ERROR("Exception in Initialize: %s", what); } +#endif } /** @@ -668,7 +684,7 @@ namespace MAT_NS_BEGIN { return; } - try + MATSDK_TRY { ConnectedEnv env(s_vm); if (!env) @@ -694,22 +710,7 @@ namespace MAT_NS_BEGIN roomIds.reserve(ids.size()); for (auto const& id : ids) { - try - { - long long roomId = std::stoll(id); - if (roomId > 0) - { - roomIds.push_back(roomId); - } - } - catch (std::out_of_range e) - { - m_observer->OnStorageFailed("id out of range"); - } - catch (std::invalid_argument e) - { - m_observer->OnStorageFailed("id empty"); - } + AppendRoomId(id, roomIds, *m_observer); } if (roomIds.empty()) { @@ -789,6 +790,7 @@ namespace MAT_NS_BEGIN m_observer->OnStorageRecordsDropped(dropped); } } +#if HAVE_EXCEPTIONS catch (const std::runtime_error& error) { auto what = error.what(); @@ -796,8 +798,9 @@ namespace MAT_NS_BEGIN { what = "*nothing*"; } - LOG_ERROR("Exception in ReleaseRecords", what); + LOG_ERROR("Exception in ReleaseRecords: %s", what); } +#endif } /** @@ -843,7 +846,7 @@ namespace MAT_NS_BEGIN return 0; } - try + MATSDK_TRY { ConnectedEnv env(s_vm); if (!env) @@ -962,6 +965,7 @@ namespace MAT_NS_BEGIN } return count; } +#if HAVE_EXCEPTIONS catch (const std::runtime_error& error) { auto what = error.what(); @@ -972,6 +976,7 @@ namespace MAT_NS_BEGIN LOG_ERROR("Exception in StoreRecords: %s", what); return 0; } +#endif } /** @@ -982,7 +987,7 @@ namespace MAT_NS_BEGIN bool OfflineStorage_Room::DeleteSetting(std::string const& name) { - try + MATSDK_TRY { ConnectedEnv env(s_vm); if (!env) @@ -1003,6 +1008,7 @@ namespace MAT_NS_BEGIN ThrowLogic(env, "exception in delete setting"); return true; } +#if HAVE_EXCEPTIONS catch (const std::runtime_error& error) { auto what = error.what(); @@ -1013,6 +1019,7 @@ namespace MAT_NS_BEGIN LOG_ERROR("Exception in DeleteSetting: %s", what); return false; } +#endif } /** @@ -1030,7 +1037,7 @@ namespace MAT_NS_BEGIN { return DeleteSetting(name); } - try + MATSDK_TRY { ConnectedEnv env(s_vm); if (!env) @@ -1057,6 +1064,7 @@ namespace MAT_NS_BEGIN ThrowRuntime(env, "Exception StoreSetting"); return (count == 1); } +#if HAVE_EXCEPTIONS catch (const std::runtime_error& error) { auto what = error.what(); @@ -1067,6 +1075,7 @@ namespace MAT_NS_BEGIN LOG_ERROR("Exception in StoreSetting: %s", what); return false; } +#endif } /** @@ -1085,7 +1094,7 @@ namespace MAT_NS_BEGIN { return ""; } - try + MATSDK_TRY { ConnectedEnv env(s_vm); if (!env) @@ -1115,6 +1124,7 @@ namespace MAT_NS_BEGIN } return result; } +#if HAVE_EXCEPTIONS catch (const std::runtime_error& error) { auto what = error.what(); @@ -1125,6 +1135,7 @@ namespace MAT_NS_BEGIN LOG_ERROR("Exception in GetSetting: %s", what); return ""; } +#endif } /** @@ -1133,7 +1144,7 @@ namespace MAT_NS_BEGIN size_t OfflineStorage_Room::GetSize() { - try + MATSDK_TRY { ConnectedEnv env(s_vm); if (!env) @@ -1142,6 +1153,7 @@ namespace MAT_NS_BEGIN } return GetSizeInternal(env); } +#if HAVE_EXCEPTIONS catch (const std::runtime_error& error) { auto what = error.what(); @@ -1152,6 +1164,7 @@ namespace MAT_NS_BEGIN LOG_ERROR("Exception in GetSetting: %s", what); return 0; } +#endif } /** @@ -1184,7 +1197,7 @@ namespace MAT_NS_BEGIN size_t OfflineStorage_Room::GetRecordCount(EventLatency latency) const { - try + MATSDK_TRY { ConnectedEnv env(s_vm); if (!env) @@ -1201,6 +1214,7 @@ namespace MAT_NS_BEGIN auto count = env->CallLongMethod(m_room, count_id, static_cast(latency)); return count; } +#if HAVE_EXCEPTIONS catch (const std::runtime_error& error) { auto what = error.what(); @@ -1211,11 +1225,12 @@ namespace MAT_NS_BEGIN LOG_ERROR("Exception in GetRecordCount: %s", what); return 0; } +#endif } bool OfflineStorage_Room::ResizeDb() { - try + MATSDK_TRY { ConnectedEnv env(s_vm); if (!env) @@ -1225,6 +1240,7 @@ namespace MAT_NS_BEGIN bool result = ResizeDbInternal(env); return result; } +#if HAVE_EXCEPTIONS catch (const std::runtime_error& error) { auto what = error.what(); @@ -1235,6 +1251,7 @@ namespace MAT_NS_BEGIN LOG_ERROR("Exception in ResizeDb: %s", what); return false; } +#endif } bool OfflineStorage_Room::ResizeDbInternal(ConnectedEnv& env) @@ -1280,7 +1297,7 @@ namespace MAT_NS_BEGIN } StorageRecordVector records; - try + MATSDK_TRY { ConnectedEnv env(s_vm); @@ -1393,6 +1410,7 @@ namespace MAT_NS_BEGIN } return records; } +#if HAVE_EXCEPTIONS catch (const std::runtime_error& error) { auto what = error.what(); @@ -1403,6 +1421,7 @@ namespace MAT_NS_BEGIN LOG_ERROR("Exception in GetRecords: %s", what); return records; } +#endif } void diff --git a/lib/offline/OfflineStorage_SQLite.cpp b/lib/offline/OfflineStorage_SQLite.cpp index 516629163..631fd6dd5 100644 --- a/lib/offline/OfflineStorage_SQLite.cpp +++ b/lib/offline/OfflineStorage_SQLite.cpp @@ -11,9 +11,10 @@ #include "StorageRecordValidation.hpp" #include "utils/StringUtils.hpp" #include +#include +#include #include #include -#include namespace MAT_NS_BEGIN { @@ -226,6 +227,12 @@ namespace MAT_NS_BEGIN { void OfflineStorage_SQLite::checkStorageSizeLimits() { + if ((m_DbSizeLimit != 0) && m_config[CFG_BOOL_ENABLE_DB_DROP_IF_FULL]) + { + // Payload estimates omit SQLite page and index overhead. + m_DbSizeEstimate = std::max(m_DbSizeEstimate.load(), GetSize()); + } + if ((m_DbSizeNotificationLimit != 0) && (m_DbSizeEstimate>m_DbSizeNotificationLimit)) { auto now = PAL::getMonotonicTimeMs(); @@ -671,20 +678,13 @@ namespace MAT_NS_BEGIN { } else { - int64_t numeric = 0; - size_t consumed = 0; - try - { - numeric = static_cast(std::stoll(value, &consumed)); - } - catch (const std::exception&) - { - consumed = 0; - } + char* end = nullptr; + errno = 0; + const int64_t numeric = static_cast(std::strtoll(value.c_str(), &end, 10)); // Treat a non-numeric value for an integer column as an invalid // filter and abort, rather than coercing to 0 and deleting rows // that happen to match 0. - if (value.empty() || consumed != value.size()) + if (value.empty() || errno == ERANGE || end != value.c_str() + value.size()) { LOG_WARN("DeleteRecords: invalid numeric filter value for column '%s'; nothing deleted", it->first.c_str()); diff --git a/lib/offline/SQLiteWrapper.hpp b/lib/offline/SQLiteWrapper.hpp index 00363af3a..c8305602d 100644 --- a/lib/offline/SQLiteWrapper.hpp +++ b/lib/offline/SQLiteWrapper.hpp @@ -492,12 +492,16 @@ namespace MAT_NS_BEGIN { { UNREFERENCED_PARAMETER(p_col_names); SQLRecords* records = static_cast(p_data); +#if HAVE_EXCEPTIONS try { +#endif records->emplace_back(p_fields, p_fields + num_fields); +#if HAVE_EXCEPTIONS } catch (...) { return 1; } +#endif return 0; } @@ -553,7 +557,7 @@ namespace MAT_NS_BEGIN { } bool lock() { -#ifndef NDEBUG +#if !defined(NDEBUG) && !defined(MATSDK_DISABLE_LOGGING) unsigned count = 0; #endif unsigned waitTime = 0; @@ -567,7 +571,7 @@ namespace MAT_NS_BEGIN { return false; } waitTime += MAX_DB_LOCKWAIT_DELAY; // 500ms, 1000ms -#ifndef NDEBUG +#if !defined(NDEBUG) && !defined(MATSDK_DISABLE_LOGGING) count++; LOG_DEBUG("Lock: waiting to acquire the lock: count=%u, waitTime=%u", count, waitTime); #endif diff --git a/lib/packager/Packager.cpp b/lib/packager/Packager.cpp index 92d565b67..86e59d4c6 100644 --- a/lib/packager/Packager.cpp +++ b/lib/packager/Packager.cpp @@ -22,7 +22,9 @@ namespace MAT_NS_BEGIN { void Packager::handleAddEventToPackage(EventsUploadContextPtr const& ctx, StorageRecord const& record, bool& wantMore) { +#if HAVE_EXCEPTIONS try { +#endif if (ctx->maxUploadSize == 0) { ctx->maxUploadSize = m_config.GetMaximumUploadSizeBytes(); } @@ -64,11 +66,13 @@ namespace MAT_NS_BEGIN { ctx->recordIdsAndTenantIds[record.id] = record.tenantToken; ctx->recordTimestamps.push_back(record.timestamp); ctx->maxRetryCountSeen = std::max(ctx->maxRetryCountSeen, record.retryCount); +#if HAVE_EXCEPTIONS } catch (const std::bad_alloc&) { wantMore = false; LOG_ERROR("Failed to add new record to package: record.blob.size=%zu", record.blob.size()); } +#endif } void Packager::handleFinalizePackage(EventsUploadContextPtr const& ctx) @@ -86,4 +90,3 @@ namespace MAT_NS_BEGIN { } MAT_NS_END - diff --git a/lib/pal/InformationProviderImpl.cpp b/lib/pal/InformationProviderImpl.cpp index 915b44a66..057697d74 100644 --- a/lib/pal/InformationProviderImpl.cpp +++ b/lib/pal/InformationProviderImpl.cpp @@ -61,7 +61,9 @@ namespace PAL_NS_BEGIN { void InformatonProviderImpl::OnChanged(std::string const& propertyName, std::string const& propertyValue) { +#if HAVE_EXCEPTIONS try { +#endif // OnChange shouldn't block new callbacks to be registered. // However, those newly registered callbacks are not called by // this current notification. @@ -91,12 +93,13 @@ namespace PAL_NS_BEGIN { cur_callback->OnChanged(propertyName, propertyValue); } } +#if HAVE_EXCEPTIONS } catch (...) { } +#endif } } PAL_NS_END - diff --git a/lib/pal/PAL.cpp b/lib/pal/PAL.cpp index 59873c282..94a71b4a0 100644 --- a/lib/pal/PAL.cpp +++ b/lib/pal/PAL.cpp @@ -50,6 +50,7 @@ #pragma comment(lib, "Ole32.Lib") /* CoCreateGuid */ #include #include +#include "utils/WindowsUtils.hpp" #endif #if defined(ANDROID) && defined(HAVE_MAT_LOGGING) @@ -73,8 +74,8 @@ namespace PAL_NS_BEGIN { HMODULE kernel32 = ::GetModuleHandleW(L"kernel32.dll"); if (kernel32 != nullptr) { - proc = reinterpret_cast( - ::GetProcAddress(kernel32, "GetSystemTimePreciseAsFileTime")); + proc = MAT::GetWindowsProcAddress( + kernel32, "GetSystemTimePreciseAsFileTime"); } }); return proc; diff --git a/lib/pal/WorkerThread.cpp b/lib/pal/WorkerThread.cpp index e18693b29..03878f298 100644 --- a/lib/pal/WorkerThread.cpp +++ b/lib/pal/WorkerThread.cpp @@ -314,7 +314,9 @@ namespace PAL_NS_BEGIN { protected: static void threadFunc(void* lpThreadParameter) { +#ifndef MATSDK_DISABLE_LOGGING uint64_t wakeupCount = 0; +#endif WorkerThread* self = reinterpret_cast(lpThreadParameter); { @@ -325,7 +327,9 @@ namespace PAL_NS_BEGIN { for (;;) { std::unique_ptr item = nullptr; +#ifndef MATSDK_DISABLE_LOGGING wakeupCount++; +#endif unsigned nextTimerInMs = MAX_FUTURE_DELTA_MS; { LOCKGUARD(self->m_lock); diff --git a/lib/pal/desktop/NetworkDetector.cpp b/lib/pal/desktop/NetworkDetector.cpp index 7f46596ae..408366682 100644 --- a/lib/pal/desktop/NetworkDetector.cpp +++ b/lib/pal/desktop/NetworkDetector.cpp @@ -19,6 +19,7 @@ #include "DebugEvents.hpp" #include "ILogManager.hpp" #include "pal/PAL.hpp" +#include "utils/WindowsUtils.hpp" #define NETDETECTOR_REFRESH WM_USER + 1 @@ -216,10 +217,10 @@ namespace MAT_NS_BEGIN NetworkDetector::NetworkDetector() { const auto module = GetModuleHandleW(L"iphlpapi.dll"); - getConnectivityHint = reinterpret_cast( - GetProcAddress(module, "GetNetworkConnectivityHint")); - notifyConnectivityHint = reinterpret_cast( - GetProcAddress(module, "NotifyNetworkConnectivityHintChange")); + getConnectivityHint = GetWindowsProcAddress( + module, "GetNetworkConnectivityHint"); + notifyConnectivityHint = GetWindowsProcAddress( + module, "NotifyNetworkConnectivityHintChange"); if (getConnectivityHint == nullptr || notifyConnectivityHint == nullptr) { getConnectivityHint = nullptr; diff --git a/lib/pal/desktop/WindowsDesktopSystemInformationImpl.cpp b/lib/pal/desktop/WindowsDesktopSystemInformationImpl.cpp index 17eea75df..deaf38d4e 100644 --- a/lib/pal/desktop/WindowsDesktopSystemInformationImpl.cpp +++ b/lib/pal/desktop/WindowsDesktopSystemInformationImpl.cpp @@ -21,6 +21,7 @@ #include "pal/PAL.hpp" #include "utils/Utils.hpp" +#include "utils/WindowsUtils.hpp" #include @@ -198,7 +199,9 @@ namespace PAL_NS_BEGIN { if (result != ERROR_SUCCESS) { if (result == ERROR_MORE_DATA) + { LOG_WARN("Commercial identifier exceeds system information buffer; rejecting"); + } return {}; } return buff; @@ -212,7 +215,7 @@ namespace PAL_NS_BEGIN { HMODULE hNtDll = ::GetModuleHandle(TEXT("ntdll.dll")); typedef HRESULT NTSTATUS; typedef NTSTATUS(__stdcall * RtlGetVersion_t)(PRTL_OSVERSIONINFOW); - RtlGetVersion_t pRtlGetVersion = hNtDll ? reinterpret_cast(::GetProcAddress(hNtDll, "RtlGetVersion")) : nullptr; + RtlGetVersion_t pRtlGetVersion = hNtDll ? GetWindowsProcAddress(hNtDll, "RtlGetVersion") : nullptr; RTL_OSVERSIONINFOW rtlOsvi = { sizeof(rtlOsvi), 0, 0, 0, 0, {0} }; if (pRtlGetVersion && SUCCEEDED(pRtlGetVersion(&rtlOsvi))) @@ -288,7 +291,7 @@ namespace PAL_NS_BEGIN { HMODULE hNtDll = ::GetModuleHandle(TEXT("ntdll.dll")); typedef HRESULT NTSTATUS; typedef NTSTATUS(__stdcall * RtlConvertDeviceFamilyInfoToString_t)(unsigned long*, unsigned long*, PWSTR, PWSTR); - RtlConvertDeviceFamilyInfoToString_t pRtlConvertDeviceFamilyInfoToString = hNtDll ? reinterpret_cast(::GetProcAddress(hNtDll, "RtlConvertDeviceFamilyInfoToString")) : nullptr; + RtlConvertDeviceFamilyInfoToString_t pRtlConvertDeviceFamilyInfoToString = hNtDll ? GetWindowsProcAddress(hNtDll, "RtlConvertDeviceFamilyInfoToString") : nullptr; if (pRtlConvertDeviceFamilyInfoToString) { diff --git a/lib/stats/MetaStats.hpp b/lib/stats/MetaStats.hpp index 1404e6561..bd7c3c2c3 100644 --- a/lib/stats/MetaStats.hpp +++ b/lib/stats/MetaStats.hpp @@ -264,9 +264,11 @@ namespace MAT_NS_BEGIN { ~RecordStats() { +#ifndef MATSDK_DISABLE_LOGGING static size_t count = 0; count++; LOG_TRACE("RecordStats destroyed: %u", count); +#endif } protected: @@ -614,4 +616,3 @@ namespace MAT_NS_BEGIN { } MAT_NS_END #endif - diff --git a/lib/tpm/TransmitProfiles.cpp b/lib/tpm/TransmitProfiles.cpp index b26766f6f..cfec76362 100644 --- a/lib/tpm/TransmitProfiles.cpp +++ b/lib/tpm/TransmitProfiles.cpp @@ -14,6 +14,10 @@ #include #include +#include +#include +#include +#include using namespace MAT; using namespace std; @@ -191,12 +195,118 @@ namespace MAT_NS_BEGIN { std::vector newProfiles; using nlohmann::json; + const auto parseProfile = [](const json& input, TransmitProfileRules& profile) + { + if (!input.is_object()) + { + return false; + } + const auto name = input.find(attributeName); + if (name == input.end() || !name->is_string()) + { + return false; + } + profile.name = name->get(); + const auto rules = input.find(attributeRules); + if (rules == input.end()) + { + return true; + } + if (!rules->is_array()) + { + return false; + } + for (const auto& inputRule : *rules) + { + if (!inputRule.is_object()) + { + return false; + } + TransmitProfileRule rule; + const auto netCost = inputRule.find("netCost"); + if (netCost != inputRule.end()) + { + if (!netCost->is_string()) + { + return false; + } + const auto value = transmitProfileNetCost.find(netCost->get()); + if (value != transmitProfileNetCost.end()) + { + rule.netCost = static_cast(value->second); + } + } + const auto powerState = inputRule.find("powerState"); + if (powerState != inputRule.end()) + { + if (!powerState->is_string()) + { + return false; + } + const auto value = transmitProfilePowerState.find(powerState->get()); + if (value != transmitProfilePowerState.end()) + { + rule.powerState = static_cast(value->second); + } + } + const auto timers = inputRule.find("timers"); + if (timers != inputRule.end()) + { + if (!timers->is_array()) + { + return false; + } + for (const auto& timer : *timers) + { + int value = 0; + if (timer.is_number_unsigned()) + { + const auto number = timer.get(); + if (number > static_cast(std::numeric_limits::max())) + { + return false; + } + value = static_cast(number); + } + else if (timer.is_number_integer()) + { + const auto number = timer.get(); + if (number < std::numeric_limits::min() || number > std::numeric_limits::max()) + { + return false; + } + value = static_cast(number); + } + else if (timer.is_number_float()) + { + const auto number = timer.get(); + if (!std::isfinite(number) || + number < std::numeric_limits::min() || number > std::numeric_limits::max()) + { + return false; + } + value = static_cast(number); + } + else + { + LOG_WARN("Ignoring non-numeric transmit timer"); + continue; + } + rule.timers.push_back(value); + } + } + profile.rules.push_back(std::move(rule)); + } + return true; + }; +#if HAVE_EXCEPTIONS try +#endif { - json temp = json::parse(profiles_json.c_str()); + json temp = json::parse(profiles_json, nullptr, false); // Try to parse the JSON string into result variant - if (temp.is_array()) + if (!temp.is_discarded() && temp.is_array()) { size_t numProfiles = temp.size(); if (numProfiles > MAX_TRANSMIT_PROFILES) { @@ -204,78 +314,33 @@ namespace MAT_NS_BEGIN { } LOG_TRACE("got %u profiles", numProfiles); - for (auto it = temp.begin(); it != temp.end(); ++it) + for (const auto& input : temp) { TransmitProfileRules profile; - json rulesObj = it.value(); - if (rulesObj.is_object()) + if (input.is_object()) { - std::string name = rulesObj[attributeName]; - - profile.name = name; - json rules = rulesObj[attributeRules]; - - if (rules.is_array()) + const auto rules = input.find(attributeRules); + if (rules != input.end() && rules->is_array() && rules->size() > MAX_TRANSMIT_RULES) { - size_t numRules = rules.size(); - if (numRules > MAX_TRANSMIT_RULES) - { - LOG_ERROR("Exceeded max transmit rules %d>%d for profile", - numRules, MAX_TRANSMIT_RULES); - goto parsing_failed; - } - - profile.rules.clear(); - for (auto itRule = rules.begin(); itRule != rules.end(); ++itRule) - { - if (itRule.value().is_object()) - { - TransmitProfileRule rule; - auto itnetCost = itRule.value().find("netCost"); - if (itRule.value().end() != itnetCost) - { - std::string netCost = itRule.value()["netCost"]; - std::map::const_iterator iter = transmitProfileNetCost.find(netCost); - if (iter != transmitProfileNetCost.end()) - { - rule.netCost = static_cast(iter->second); - } - } - - auto itpowerState = itRule.value().find("powerState"); - if (itRule.value().end() != itpowerState) - { - std::string powerState = itRule.value()["powerState"]; - std::map::const_iterator iter = transmitProfilePowerState.find(powerState); - if (iter != transmitProfilePowerState.end()) - { - rule.powerState = static_cast(iter->second); - } - } - - auto timers = itRule.value()["timers"]; - - for (const auto& timer : timers) - { - if (timer.is_number()) - { - rule.timers.push_back(timer); - } - } - profile.rules.push_back(rule); - } - - } + LOG_ERROR("Exceeded max transmit rules %d>%d for profile", rules->size(), MAX_TRANSMIT_RULES); + goto parsing_failed; } } - newProfiles.push_back(profile); + if (!parseProfile(input, profile)) + { + LOG_ERROR("Invalid transmit profile schema"); + break; + } + newProfiles.push_back(std::move(profile)); } } } +#if HAVE_EXCEPTIONS catch (...) { LOG_ERROR("JSON parsing failed miserably! Please check your config to fix above errors."); } +#endif numProfilesParsed = newProfiles.size(); UpdateProfiles(newProfiles); diff --git a/lib/utils/Utils.cpp b/lib/utils/Utils.cpp index a1cf48ee7..33b0fb221 100644 --- a/lib/utils/Utils.cpp +++ b/lib/utils/Utils.cpp @@ -14,6 +14,7 @@ #ifdef _WIN32 #include +#include "WindowsUtils.hpp" #ifdef _WINRT_DLL // Win 10-specific APIs @@ -77,10 +78,10 @@ namespace MAT_NS_BEGIN { bool IsRunningInApp() noexcept { #ifdef _WINRT_DLL // Win 10 UWP - typedef LONG (*LPFN_GPFN)(UINT32*, PWSTR); + typedef LONG (WINAPI *LPFN_GPFN)(UINT32*, PWSTR); bool isRunningInApp = true; - LPFN_GPFN lpGetPackageFamilyName = (LPFN_GPFN)GetProcAddress(GetModuleHandle(TEXT("kernel32")), "GetCurrentPackageFamilyName"); + LPFN_GPFN lpGetPackageFamilyName = GetWindowsProcAddress(GetModuleHandle(TEXT("kernel32")), "GetCurrentPackageFamilyName"); if (lpGetPackageFamilyName) { UINT32 size = 0; @@ -140,7 +141,7 @@ namespace MAT_NS_BEGIN { std::string GetTempDirectory() { #ifdef _WIN32 - auto lpGetTempPathW = reinterpret_cast(GetProcAddress(GetModuleHandle(TEXT("kernel32")), "GetTempPath2W")); + auto lpGetTempPathW = GetWindowsProcAddress(GetModuleHandle(TEXT("kernel32")), "GetTempPath2W"); if (lpGetTempPathW == NULL) { lpGetTempPathW = ::GetTempPathW; diff --git a/lib/utils/WindowsUtils.hpp b/lib/utils/WindowsUtils.hpp new file mode 100644 index 000000000..fce206b49 --- /dev/null +++ b/lib/utils/WindowsUtils.hpp @@ -0,0 +1,32 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +// +#ifndef LIB_WINDOWS_UTILS_HPP +#define LIB_WINDOWS_UTILS_HPP + +#include "ctmacros.hpp" + +#ifdef _WIN32 +#include +#include +#include + +namespace MAT_NS_BEGIN { + + template + FunctionPointer GetWindowsProcAddress(HMODULE module, LPCSTR name) noexcept + { + static_assert(std::is_function::type>::value, + "Windows procedure lookup requires a function pointer"); + const auto address = ::GetProcAddress(module, name); + FunctionPointer function = nullptr; + static_assert(sizeof(function) == sizeof(address), "Windows function pointers must have the same size"); + std::memcpy(&function, &address, sizeof(function)); + return function; + } + +} MAT_NS_END +#endif + +#endif diff --git a/tests/build-runner-tests.py b/tests/build-runner-tests.py new file mode 100644 index 000000000..8360c1142 --- /dev/null +++ b/tests/build-runner-tests.py @@ -0,0 +1,128 @@ +"""Exercise the POSIX test runner without compiling or contacting telemetry services.""" + +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + + +REPO_ROOT = Path(__file__).resolve().parents[1] + + +class BuildRunnerTests(unittest.TestCase): + def setUp(self): + self.temporary = tempfile.TemporaryDirectory() + self.addCleanup(self.temporary.cleanup) + self.root = Path(self.temporary.name) + self.calls = self.root / "calls.txt" + shutil.copyfile(REPO_ROOT / "build-tests.sh", self.root / "build-tests.sh") + self.write_script( + "build.sh", + 'printf "build %s\\n" "$*" >> "$CALLS"\n' + 'printf "options %s\\n" "$CMAKE_OPTS" >> "$CALLS"\n' + 'exit "${BUILD_RESULT:-0}"\n', + ) + self.write_script( + "bin/ctest", + 'printf "ctest %s\\n" "$*" >> "$CALLS"\n' + 'printf "ctest-cwd %s\\n" "$PWD" >> "$CALLS"\n' + 'exit "${CTEST_RESULT:-0}"\n', + ) + self.write_script( + "out/tests/functests/FuncTests", + 'printf "concurrent %s\\n" "$*" >> "$CALLS"\n' + 'if mkdir "$CASE_ROOT/first-child" 2>/dev/null; then\n' + ' sleep 0.05\n' + ' echo first-finished >> "$CALLS"\n' + ' exit "${FIRST_RESULT:-0}"\n' + 'else\n' + ' sleep 0.1\n' + ' echo second-finished >> "$CALLS"\n' + ' exit "${SECOND_RESULT:-0}"\n' + 'fi\n', + ) + + def write_script(self, name, body): + path = self.root / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("#!/bin/sh\n" + body, encoding="utf-8") + path.chmod(0o755) + + def run_runner(self, *arguments, **variables): + environment = dict( + os.environ, + PATH=str(self.root / "bin") + os.pathsep + os.environ["PATH"], + CALLS=str(self.calls), + CASE_ROOT=str(self.root), + BUILD_RESULT="0", + CTEST_RESULT="0", + FIRST_RESULT="0", + SECOND_RESULT="0", + ) + environment.pop("CMAKE_OPTS", None) + environment.update(variables) + result = subprocess.run( + ["sh", str(self.root / "build-tests.sh"), *arguments], + cwd=self.temporary.name, + env=environment, + capture_output=True, + text=True, + timeout=10, + ) + calls = self.calls.read_text(encoding="utf-8").splitlines() + return result, calls + + def test_runs_complete_ctest_suite_and_waits_for_both_children(self): + result, calls = self.run_runner() + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("build release", calls) + self.assertIn("ctest --output-on-failure", calls) + self.assertIn("ctest-cwd " + str(self.root / "out"), calls) + self.assertIn("first-finished", calls) + self.assertIn("second-finished", calls) + self.assertEqual(sum(line.startswith("concurrent ") for line in calls), 2) + self.assertTrue( + all( + line == "concurrent --gtest_filter=MultipleLogManagersTests.MultiProcessesLogManager" + for line in calls + if line.startswith("concurrent ") + ) + ) + + def test_preserves_configuration_and_caller_cmake_options(self): + result, calls = self.run_runner("debug", CMAKE_OPTS="-DMATSDK_ENABLE_DEVICE_ID=OFF") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("build debug", calls) + self.assertIn( + "options -DMATSDK_ENABLE_DEVICE_ID=OFF " + "-DMATSDK_BUILD_UNIT_TESTS=ON -DMATSDK_BUILD_FUNC_TESTS=ON", + calls, + ) + + def test_build_failure_stops_before_running_stale_tests(self): + result, calls = self.run_runner(BUILD_RESULT="23") + self.assertEqual(result.returncode, 23) + self.assertFalse(any(line.startswith(("ctest ", "concurrent ")) for line in calls)) + + def test_ctest_failure_stops_before_concurrent_checks(self): + result, calls = self.run_runner(CTEST_RESULT="24") + self.assertEqual(result.returncode, 24) + self.assertFalse(any(line.startswith("concurrent ") for line in calls)) + + def test_first_child_failure_is_not_lost(self): + result, calls = self.run_runner(FIRST_RESULT="25") + self.assertEqual(result.returncode, 25) + self.assertIn("first-finished", calls) + self.assertIn("second-finished", calls) + + def test_second_child_failure_is_not_lost(self): + result, calls = self.run_runner(SECOND_RESULT="26") + self.assertEqual(result.returncode, 26) + self.assertIn("first-finished", calls) + self.assertIn("second-finished", calls) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/embedding/CMakeLists.txt b/tests/embedding/CMakeLists.txt index 7b6a77370..49b11b481 100644 --- a/tests/embedding/CMakeLists.txt +++ b/tests/embedding/CMakeLists.txt @@ -78,3 +78,6 @@ if(MSVC) else() target_compile_options(embedding_test PRIVATE -Wall -Wextra -Werror) endif() + +enable_testing() +add_test(NAME embedding_test COMMAND embedding_test) diff --git a/tests/functests/AISendTests.cpp b/tests/functests/AISendTests.cpp index dfd0bf185..e9f80b853 100644 --- a/tests/functests/AISendTests.cpp +++ b/tests/functests/AISendTests.cpp @@ -103,7 +103,6 @@ class AISendTests : public ::testing::Test, std::atomic isRunning; std::condition_variable cv_gotEvents; - std::mutex cv_m; public: AISendTests() : @@ -154,6 +153,8 @@ class AISendTests : public ::testing::Test, configuration[CFG_INT_SDK_MODE] = SdkModeTypes_AI; configuration[CFG_STR_COLLECTOR_URL] = (serverAddress + path).c_str(); configuration[CFG_MAP_HTTP][CFG_BOOL_HTTP_COMPRESSION] = compression; + // Loopback HTTP tests do not require host network discovery. + configuration[CFG_BOOL_ENABLE_NET_DETECT] = false; configuration[CFG_INT_TRACE_LEVEL_MASK] = 0xFFFFFFFF; #ifdef NDEBUG @@ -205,6 +206,7 @@ class AISendTests : public ::testing::Test, LOCKGUARD(mtx_requests); receivedRequests.push_back(request); } + cv_gotEvents.notify_all(); if (request.uri.compare("/v2/track/400/") == 0) { @@ -255,48 +257,45 @@ class AISendTests : public ::testing::Test, bool waitForRequests(unsigned timeOutSec, unsigned expected_count = 1) { - std::unique_lock lk(cv_m); - if (cv_gotEvents.wait_for(lk, std::chrono::milliseconds(1000 * timeOutSec), [&] { return receivedRequests.size() >= expected_count; })) - { - return true; - } - return false; + std::unique_lock lk(mtx_requests); + return cv_gotEvents.wait_for(lk, std::chrono::seconds(timeOutSec), + [&] { return receivedRequests.size() >= expected_count; }); } void waitForEvents(unsigned timeOutSec, unsigned expectedRequests, bool compression) { size_t receivedEvents = 0; - unsigned timeoutMs = 1000 * timeOutSec; - auto start = PAL::getUtcSystemTimeMs(); - while (((PAL::getUtcSystemTimeMs() - start) < timeoutMs) - && (receivedEvents != expectedRequests)) + size_t nextRequest = 0; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(timeOutSec); + while (receivedEvents < expectedRequests) { - /* Give time for our friendly HTTP server thread to process incoming request */ - std::this_thread::yield(); + HttpServer::Request request; { - LOCKGUARD(mtx_requests); - if (receivedRequests.size()) + std::unique_lock lock(mtx_requests); + if (!cv_gotEvents.wait_until(lock, deadline, + [&] { return nextRequest < receivedRequests.size(); })) { - auto request = receivedRequests.at(0); - nlohmann::json body; - auto it = request.headers.find("Content-Encoding"); - if (it != request.headers.end()) - { - EXPECT_TRUE(compression); - std::vector content(request.content.begin(), request.content.end()); - std::vector inflated; - ZlibUtils::InflateVector(content, inflated, true); - body = nlohmann::json::parse(inflated.begin(), inflated.end()); - } - else - { - EXPECT_FALSE(compression); - body = nlohmann::json::parse(request.content.begin(), request.content.end()); - } - EXPECT_TRUE(body.is_array()); - receivedEvents += body.size(); + break; } + request = receivedRequests.at(nextRequest++); + } + nlohmann::json body; + const auto encoding = request.headers.find("Content-Encoding"); + if (encoding != request.headers.end()) + { + EXPECT_TRUE(compression); + std::vector content(request.content.begin(), request.content.end()); + std::vector inflated; + ASSERT_TRUE(ZlibUtils::InflateVector(content, inflated, true)); + body = nlohmann::json::parse(inflated.begin(), inflated.end()); } + else + { + EXPECT_FALSE(compression); + body = nlohmann::json::parse(request.content.begin(), request.content.end()); + } + ASSERT_TRUE(body.is_array()); + receivedEvents += body.size(); } ASSERT_EQ(receivedEvents, expectedRequests); } @@ -377,6 +376,28 @@ class AISendTests : public ::testing::Test, } }; +TEST_F(AISendTests, waitForEventsCountsEachRequestOnce) +{ + HttpServer::Request request; + request.uri = "/v2/track/"; + request.content = "[{}]"; + HttpServer::Response response; + onHttpRequest(request, response); + std::thread producer([this, request]() mutable + { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + request.content = "[{},{}]"; + HttpServer::Response nextResponse; + onHttpRequest(request, nextResponse); + }); + waitForEvents(5, 3, false); + { + LOCKGUARD(mtx_requests); + EXPECT_EQ(receivedRequests.size(), 2u); + } + producer.join(); +} + TEST_F(AISendTests, sendOneEvent) { AITestDebugEventListener debugListener; diff --git a/tests/functests/CMakeLists.txt b/tests/functests/CMakeLists.txt index e97bb0b30..058a5a1e4 100644 --- a/tests/functests/CMakeLists.txt +++ b/tests/functests/CMakeLists.txt @@ -9,6 +9,11 @@ set(SRCS ) set(MATSDK_FUNC_TEST_DEFINITIONS) +if(EXISTS "${PROJECT_SOURCE_DIR}/lib/modules/azmon/" AND MATSDK_BUILD_AZMON) + list(APPEND MATSDK_FUNC_TEST_DEFINITIONS HAVE_MAT_AI) + list(APPEND SRCS AISendTests.cpp) +endif() + if(EXISTS "${PROJECT_SOURCE_DIR}/lib/modules/privacyguard/" AND MATSDK_BUILD_PRIVACYGUARD) list(APPEND MATSDK_FUNC_TEST_DEFINITIONS HAVE_MAT_PRIVACYGUARD) list(APPEND SRCS @@ -71,4 +76,6 @@ if(TARGET nlohmann_json::nlohmann_json) target_link_libraries(FuncTests PRIVATE nlohmann_json::nlohmann_json) endif() -add_test(FuncTests FuncTests "--gtest_output=xml:${PROJECT_BINARY_DIR}/test-reports/FuncTests.xml") +add_test(NAME FuncTests COMMAND FuncTests + "--gtest_output=xml:${PROJECT_BINARY_DIR}/test-reports/FuncTests.xml") +set_tests_properties(FuncTests PROPERTIES WORKING_DIRECTORY "${PROJECT_BINARY_DIR}") diff --git a/tests/headers/check_public_headers.cmd b/tests/headers/check_public_headers.cmd index 4bb82cd69..56ac29fc0 100644 --- a/tests/headers/check_public_headers.cmd +++ b/tests/headers/check_public_headers.cmd @@ -60,6 +60,10 @@ set "TOTAL=0" REM MSVC does not expose a /std:c++11 switch; /std:c++14 is its lowest selectable mode. call :RunCxxHeaders c++14 /std:c++14 "cl (c++14, /W4 /WX)" call :RunCxxHeaders c++17 /std:c++17 "cl (c++17, /W4 /WX)" +call :RunExceptionMacros 1 "/EHsc" +set "CXX_COMMON=%CXX_COMMON:/EHsc=/EHs-c- /D_HAS_EXCEPTIONS=0%" +call :RunCxxHeaders c++14 /std:c++14 "cl (c++14, exceptions disabled, /W4 /WX)" +call :RunExceptionMacros 0 "/EHs-c- /D_HAS_EXCEPTIONS=0" call :RunCHeader rmdir /s /q "%WORK%" 2>nul @@ -71,6 +75,23 @@ if "%FAIL%"=="1" ( echo Public header gate passed. ^(!TOTAL! checks^) exit /b 0 +:RunExceptionMacros +cl /nologo /std:c++14 /W4 /WX %~2 /DMATSDK_TEST_EXCEPTIONS=%1 /I "%PUB%" "%SCRIPT_DIR%exception-macros.cpp" /Fo"%WORK%\exception-macros.obj" /Fe"%WORK%\exception-macros.exe" > "%WORK%\err.txt" 2>&1 +if errorlevel 1 ( + type "%WORK%\err.txt" + set "FAIL=1" + exit /b 0 +) +"%WORK%\exception-macros.exe" +if errorlevel 1 ( + echo FAIL: exception macros ^(exceptions=%1^) + set "FAIL=1" +) else ( + set /a TOTAL+=1 + echo Exception macros ^(exceptions=%1^): passed +) +exit /b 0 + :RunCxxHeaders set "STD_NAME=%~1" set "STD_FLAG=%~2" diff --git a/tests/headers/check_public_headers.sh b/tests/headers/check_public_headers.sh index 79724e588..35ccdb008 100644 --- a/tests/headers/check_public_headers.sh +++ b/tests/headers/check_public_headers.sh @@ -116,14 +116,53 @@ run_c_compiler() { } cxx_ran=0 +run_exception_macros() { + local cc="$1" mode="$2" + shift 2 + local exe="$tmp/exception-macros" out + if out="$("$cc" -std=c++11 -Wall -Wextra -Werror "$@" \ + "-DMATSDK_TEST_EXCEPTIONS=$mode" -I"$PUB" \ + "$REPO_ROOT/tests/headers/exception-macros.cpp" -o "$exe" 2>&1)" && + "$exe"; then + echo " $cc exception macros (exceptions=$mode): passed" + else + echo " FAIL: $cc exception macros (exceptions=$mode)" + echo "$out" + fail=1 + fi +} + +check_objcxx_exception_macros() { + local mode="$1" out + shift + if out="$(clang++ -x objective-c++ -std=c++11 -Wall -Wextra -Werror \ + -fobjc-exceptions "$@" "-DMATSDK_TEST_EXCEPTIONS=$mode" -I"$PUB" \ + -fsyntax-only "$REPO_ROOT/tests/headers/exception-macros.cpp" 2>&1)"; then + echo " clang++ Objective-C++ exception macros (C++ exceptions=$mode): passed" + else + echo " FAIL: clang++ Objective-C++ exception macros (C++ exceptions=$mode)" + echo "$out" + fail=1 + fi +} + if command -v g++ >/dev/null 2>&1; then run_cxx_compiler g++ c++11 "g++ (c++11, -Wall -Wextra -Werror)" run_cxx_compiler g++ c++17 "g++ (c++17, -Wall -Wextra -Werror)" + run_cxx_compiler g++ c++11 "g++ (c++11, -fno-exceptions)" -fno-exceptions + run_exception_macros g++ 1 -fexceptions + run_exception_macros g++ 0 -fno-exceptions cxx_ran=1 fi if command -v clang++ >/dev/null 2>&1; then run_cxx_compiler clang++ c++11 "clang++ (c++11, + -Wshorten-64-to-32)" -Wshorten-64-to-32 run_cxx_compiler clang++ c++17 "clang++ (c++17, + -Wshorten-64-to-32)" -Wshorten-64-to-32 + run_cxx_compiler clang++ c++11 "clang++ (c++11, -fno-exceptions)" -fno-exceptions -Wshorten-64-to-32 + run_exception_macros clang++ 1 -fexceptions + run_exception_macros clang++ 0 -fno-exceptions + check_objcxx_exception_macros 1 -fexceptions + check_objcxx_exception_macros 0 -fno-exceptions + check_objcxx_exception_macros 0 -fno-exceptions -D__EXCEPTIONS=1 cxx_ran=1 fi diff --git a/tests/headers/exception-macros.cpp b/tests/headers/exception-macros.cpp new file mode 100644 index 000000000..f896e7f1e --- /dev/null +++ b/tests/headers/exception-macros.cpp @@ -0,0 +1,40 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +#include "ctmacros.hpp" +#include + +static_assert(HAVE_EXCEPTIONS == MATSDK_TEST_EXCEPTIONS, + "Exception detection must match the compiler flags"); + +int main() +{ + int calls = 0; + MATSDK_TRY + { + ++calls; + } + MATSDK_CATCH(...) + { + return 1; + } + +#if HAVE_EXCEPTIONS + MATSDK_TRY + { + MATSDK_THROW(std::runtime_error("test")); + } + MATSDK_CATCH(const std::runtime_error& error) + { + if (error.what()[0] != 't') + { + return 2; + } + ++calls; + } + return calls == 2 ? 0 : 3; +#else + return calls == 1 ? 0 : 3; +#endif +} diff --git a/tests/memory-leak-analysis/run-drmemory-tests.ps1 b/tests/memory-leak-analysis/run-drmemory-tests.ps1 new file mode 100644 index 000000000..ca137c5c4 --- /dev/null +++ b/tests/memory-leak-analysis/run-drmemory-tests.ps1 @@ -0,0 +1,106 @@ +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +$runner = Join-Path $PSScriptRoot "../../.github/scripts/run-drmemory.ps1" +$root = Join-Path ([System.IO.Path]::GetTempPath()) ("drmemory-tests-" + [guid]::NewGuid()) +New-Item -ItemType Directory -Path $root | Out-Null +$fakeDrMemory = Join-Path $root "drmemory.ps1" +$target = Join-Path $root "target" +Set-Content -LiteralPath $fakeDrMemory -Value '$global:LASTEXITCODE = 0' +Set-Content -LiteralPath $target -Value "" +$completedReport = @" +ERRORS FOUND: + 1 unique, 2 total, 3,000 byte(s) of leak(s) + 4 unique, 5 total, 6 byte(s) of possible leak(s) +ERRORS IGNORED: + 7 unique, 8 total, 9 byte(s) of still-reachable allocation(s) +"@ + +function Invoke-Scenario { + param([string]$Name, [string[]]$Reports, [string]$ExpectedError, [switch]$CheckBaseline) + + $logs = Join-Path $root $Name + $scenario = Join-Path $logs $Name + New-Item -ItemType Directory -Path $scenario -Force | Out-Null + for ($i = 0; $i -lt $Reports.Count; ++$i) { + $process = Join-Path $scenario "process-$i" + New-Item -ItemType Directory -Path $process | Out-Null + Set-Content -LiteralPath (Join-Path $process "results.txt") -Value $Reports[$i] + } + $arguments = @{ + DrMemoryPath = $fakeDrMemory + LogDirectory = $logs + Scenario = $Name + TargetPath = $target + } + if ($CheckBaseline) { + $baseline = Join-Path $root "baseline.csv" + [pscustomobject]@{ + Platform = if ($env:RUNNER_OS) { $env:RUNNER_OS } else { [System.Environment]::OSVersion.Platform } + Scenario = $Name + UniqueLeaks = 0; TotalLeaks = 0; LeakBytes = 0 + UniquePossibleLeaks = 0; TotalPossibleLeaks = 0; PossibleLeakBytes = 0 + UniqueReachable = 0; TotalReachable = 0; ReachableBytes = 0 + } | Export-Csv -LiteralPath $baseline -NoTypeInformation + $arguments.BaselinePath = $baseline + } + $failure = $null + $output = try { + & $runner @arguments 6>&1 + } + catch { + $failure = $_ + } + if ($ExpectedError) { + if (-not $failure -or $failure.Exception.Message -notlike "*$ExpectedError*") { + throw "$Name did not fail with '$ExpectedError': $failure" + } + if (Test-Path (Join-Path $logs "summary.csv")) { + throw "$Name emitted a success summary for invalid results." + } + return + } + if ($failure) { + throw $failure + } + if ($CheckBaseline -and ($output -join "`n") -notmatch "::warning title=Dr. Memory regression") { + throw "$Name did not report baseline regressions." + } + $summaries = @(Import-Csv -LiteralPath (Join-Path $logs "summary.csv")) + if ($summaries.Count -ne 1) { + throw "$Name did not produce exactly one scenario summary." + } + return $summaries[0] +} + +function Assert-Counts { + param($Summary, [int]$Multiplier) + + $expected = @{ + UniqueLeaks = 1; TotalLeaks = 2; LeakBytes = 3000 + UniquePossibleLeaks = 4; TotalPossibleLeaks = 5; PossibleLeakBytes = 6 + UniqueReachable = 7; TotalReachable = 8; ReachableBytes = 9 + } + foreach ($metric in $expected.Keys) { + if ([int64]$Summary.$metric -ne $expected[$metric] * $Multiplier) { + throw "$metric was $($Summary.$metric), expected $($expected[$metric] * $Multiplier)." + } + } +} + +try { + Assert-Counts (Invoke-Scenario "single" @($completedReport)) 1 + Assert-Counts (Invoke-Scenario "forked" @($completedReport, $completedReport, "Dr. Memory version header")) 2 + $cleanReport = $completedReport -replace "ERRORS FOUND:", "NO ERRORS FOUND:" -replace "\d[\d,]* (?=unique|total|byte)", "0 " + Assert-Counts (Invoke-Scenario "clean-child" @($completedReport, $cleanReport)) 1 + Assert-Counts (Invoke-Scenario "baseline" @($completedReport, $completedReport) -CheckBaseline) 2 + Invoke-Scenario "missing" @("Dr. Memory version header") -ExpectedError "found none" + Invoke-Scenario "malformed" @($completedReport, "ERRORS FOUND:") -ExpectedError "do not contain" + Set-Content -LiteralPath $fakeDrMemory -Value '$global:LASTEXITCODE = 7' + Invoke-Scenario "target-failure" @($completedReport) -ExpectedError "exited with code 7" + $global:LASTEXITCODE = 0 + Write-Host "Passed 7 Dr. Memory runner regression scenarios." +} +finally { + Remove-Item -LiteralPath $root -Recurse -Force +} diff --git a/tests/no-exceptions/CMakeLists.txt b/tests/no-exceptions/CMakeLists.txt new file mode 100644 index 000000000..a261eb762 --- /dev/null +++ b/tests/no-exceptions/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.15...3.31) +project(MatsdkNoExceptionsSmoke LANGUAGES C CXX) + +set(MATSDK_DISABLE_EXCEPTIONS ON CACHE BOOL + "Compile SDK-owned C++ targets without exception support") +set(MATSDK_BUILD_UNIT_TESTS OFF CACHE BOOL "" FORCE) +set(MATSDK_BUILD_FUNC_TESTS OFF CACHE BOOL "" FORCE) +set(MATSDK_BUILD_TEST_TOOL OFF CACHE BOOL "" FORCE) +set(MATSDK_BUILD_PACKAGE OFF CACHE BOOL "" FORCE) +set(MATSDK_BUILD_SWIFT_WRAPPER OFF CACHE BOOL "" FORCE) +add_subdirectory(../.. sdk) + +add_executable(no-exceptions-smoke no-exceptions-smoke.cpp) +target_compile_features(no-exceptions-smoke PRIVATE cxx_std_14) +target_include_directories(no-exceptions-smoke PRIVATE + ../../lib + ../../lib/include + ../../lib/include/mat + ../../lib/pal + ../../lib/utils) +target_link_libraries(no-exceptions-smoke PRIVATE mat matsdk_internal_config matsdk_build_options) +enable_testing() +add_test(NAME no-exceptions-smoke COMMAND no-exceptions-smoke) diff --git a/tests/no-exceptions/README.md b/tests/no-exceptions/README.md new file mode 100644 index 000000000..4ae6ad875 --- /dev/null +++ b/tests/no-exceptions/README.md @@ -0,0 +1,37 @@ +# No-exceptions runtime regression + +This dependency-free smoke executable links the full SDK and compiles both the +SDK and driver with C++ exceptions disabled. It checks malformed configuration +and response JSON, configuration and timer numeric limits, partial profile +loading, correlation-vector overflow, unrepresentable expansion sizes, and +deterministic allocation failure for a representable expansion. +It requires neither Google Test nor the private optional modules. + +For GCC, Clang, Apple Clang, MSVC, or clang-cl: + +```sh +cmake -S tests/no-exceptions -B out-no-exceptions \ + -DCMAKE_BUILD_TYPE=Debug -DMATSDK_DISABLE_EXCEPTIONS=ON +cmake --build out-no-exceptions --target no-exceptions-smoke --parallel 2 +ctest --test-dir out-no-exceptions --output-on-failure +``` + +For clang-cl, select the `ClangCL` toolset. +Use a fresh build directory when changing compilers or exception modes. +Expansion requests larger than `PTRDIFF_MAX` fail before calling an allocator; +representable allocations still use `std::nothrow` and report failure normally. +The internal expansion overload accepts a per-call allocator. Regression tests +return null for a 32-byte request and verify the allocator was called, a +previously non-null output was cleared, and the output length became zero. +Production calls retain the normal `std::nothrow` allocator; there is no global +failure-injection state. + +`FromJSON` preserves throwing syntax errors in exception-enabled builds. When +exceptions are disabled it logs malformed syntax and returns an empty +configuration. Null input and a non-object root log an error and return empty. + +`TransmitProfiles::load(string)` retains its historical partial-load contract: +a schema error stops parsing, replaces existing custom profiles with the valid +prefix, and returns true if that prefix is nonempty. Default profiles remain +available. Entries after the first invalid profile are not loaded. The vector +overload validates every candidate before replacing existing custom profiles. diff --git a/tests/no-exceptions/no-exceptions-smoke.cpp b/tests/no-exceptions/no-exceptions-smoke.cpp new file mode 100644 index 000000000..d85307af3 --- /dev/null +++ b/tests/no-exceptions/no-exceptions-smoke.cpp @@ -0,0 +1,166 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +// +#include "LogConfiguration.hpp" +#include "CorrelationVector.hpp" +#include "TransmitProfiles.hpp" +#include "http/HttpResponseDecoder.hpp" +#include "utils/ZlibUtils.hpp" + +#include +#include +#include + +#if HAVE_EXCEPTIONS +#error This regression executable must be compiled without C++ exceptions. +#endif +#if !defined(HAVE_MAT_JSONHPP) || !defined(HAVE_MAT_ZLIB) +#error This regression executable requires JSON and zlib support. +#endif + +namespace clienttelemetry { + namespace data { + namespace v3 { + bool Expand(const char*, size_t, char**, size_t&, bool); + bool Expand(const char*, size_t, char**, size_t&, bool, char* (*)(size_t)); + } + } +} + +using namespace MAT; + +namespace +{ + void check(bool condition, const char* message) + { + if (!condition) + { + std::fprintf(stderr, "FAILED: %s\n", message); + std::exit(EXIT_FAILURE); + } + } + + [[noreturn]] void unexpectedSystemCall() + { + std::fputs("FAILED: response parsing unexpectedly accessed a telemetry service\n", stderr); + std::exit(EXIT_FAILURE); + } + + class UnusedSystem : public ITelemetrySystem + { + public: + void start() override { unexpectedSystemCall(); } + void stop() override { unexpectedSystemCall(); } + void pause() override { unexpectedSystemCall(); } + void resume() override { unexpectedSystemCall(); } + bool upload() override { unexpectedSystemCall(); } + void cleanup() override { unexpectedSystemCall(); } + ILogManager& getLogManager() override { unexpectedSystemCall(); } + IRuntimeConfig& getConfig() override { unexpectedSystemCall(); } + ISemanticContext& getContext() override { unexpectedSystemCall(); } + EventsUploadContextPtr createEventsUploadContext() override { unexpectedSystemCall(); } + bool DispatchEvent(DebugEvent) override { unexpectedSystemCall(); } + void sendEvent(IncomingEventContextPtr const&) override { unexpectedSystemCall(); } + void handleFlushTaskDispatcher() override { unexpectedSystemCall(); } + void signalDone() override { unexpectedSystemCall(); } + void handleIncomingEventPrepared(IncomingEventContextPtr const&) override { unexpectedSystemCall(); } + void preparedIncomingEventAsync(IncomingEventContextPtr const&) override { unexpectedSystemCall(); } + }; + + class ResponseDecoder : public HttpResponseDecoder + { + public: + explicit ResponseDecoder(ITelemetrySystem& system) : HttpResponseDecoder(system) {} + using HttpResponseDecoder::processBody; + unsigned expiredTickets = 0; + bool DispatchEvent(DebugEvent event) override + { + check(event.type == DebugEventType::EVT_TICKET_EXPIRED, "unexpected response event"); + ++expiredTickets; + return true; + } + }; + + class ProfileAccess : public TransmitProfiles + { + public: + static const std::map& loaded() { return profiles; } + }; +} + +int main() +{ + const char* invalidConfigurations[] = {nullptr, "", "[", "not-json", "null", "1", "\"text\"", "[]", "[{}]"}; + for (const char* input : invalidConfigurations) + { + auto config = FromJSON(input); + check((*config).empty(), "invalid configuration must return empty without aborting"); + } + auto config = FromJSON(R"({"enabled":true,"signed":-9223372036854775808,"unsigned":18446744073709551615,"nested":{"key":"value"}})"); + check(config.HasConfig("enabled") && config.HasConfig("nested"), "valid configuration was lost"); + check(static_cast(config["signed"]) == std::numeric_limits::min(), "signed configuration limit"); + check(static_cast(config["unsigned"]) == std::numeric_limits::max(), "unsigned configuration limit"); + + for (const char* input : { + "not-json", "[", "[null]", R"([{"name":1}])", + R"([{"name":"Bad","rules":[{"timers":[2147483648]}]}])", + R"([{"name":"Bad","rules":[{"timers":[-2147483649]}]}])", + R"([{"name":"Bad","rules":[{"timers":[1e999]}]}])"}) + { + check(!TransmitProfiles::load(input), "malformed profiles must return false without aborting"); + } + check(TransmitProfiles::load(R"([{"name":"Limits","rules":[{"timers":[-2147483648,2147483647,1.5]}]}])"), "valid timer limits"); + check(ProfileAccess::loaded().at("Limits").rules[0].timers == + std::vector({std::numeric_limits::min(), std::numeric_limits::max(), 1}), "timer limit conversion"); + check(TransmitProfiles::load(R"([{"name":"Prefix","rules":[]},{"name":null},{"name":"Skipped"}])"), "partial profile loading"); + check(ProfileAccess::loaded().count("Prefix") == 1 && + ProfileAccess::loaded().count("Skipped") == 0 && + ProfileAccess::loaded().count("Limits") == 0, "partial loading must replace only the valid prefix"); + + UnusedSystem system; + ResponseDecoder decoder(system); + SimpleHttpResponse response("no-exceptions"); + for (const char* input : { + "not-json", "[", "null", "1", "[]", + R"({"efi":null})", R"({"efi":1})", R"({"efi":"all"})", R"({"efi":["all"]})", + R"({"acc":1e100,"efi":{"tenant":"all"}})", + R"({"rej":18446744073709551615,"efi":{"tenant":"all"}})"}) + { + const std::string body(input); + response.m_body.assign(body.begin(), body.end()); + auto result = Accepted; + decoder.processBody(response, result); + check(result == Accepted && decoder.expiredTickets == 0, "malformed response changed upload outcome"); + } + const std::string validResponse = R"({"acc":2147483647,"rej":0,"efi":{"tenant":"all"},"TokenCrackingFailure":true})"; + response.m_body.assign(validResponse.begin(), validResponse.end()); + auto result = Accepted; + decoder.processBody(response, result); + check(result == Rejected && decoder.expiredTickets == 1, "valid response lost rejection or ticket event"); + + CorrelationVector vector; + check(vector.SetValue("jj9XLhDw7EuXoC2L.4294967295"), "maximum correlation vector element"); + check(!vector.SetValue("jj9XLhDw7EuXoC2L.4294967296"), "overflowing correlation vector element"); + check(!vector.SetValue("jj9XLhDw7EuXoC2L.9999999999999999999999999"), "oversized correlation vector element"); + + const char invalidCompressed[] = "invalid"; + char* output = nullptr; + size_t outputSize = std::numeric_limits::max(); + check(!clienttelemetry::data::v3::Expand(invalidCompressed, sizeof(invalidCompressed), &output, outputSize, false), + "oversized expansion must fail"); + check(output == nullptr && outputSize == 0, "failed expansion must clear outputs"); + char previousOutput = 0; + output = &previousOutput; + outputSize = 32; + static size_t requestedSize = 0; + check(!clienttelemetry::data::v3::Expand(invalidCompressed, sizeof(invalidCompressed), &output, outputSize, false, + [](size_t size) -> char* { + requestedSize = size; + return nullptr; + }), "representable allocation failure must return false"); + check(requestedSize == 32, "failure injection must reach the allocator"); + check(output == nullptr && outputSize == 0, "allocation failure must clear an existing output and its length"); + std::puts("Passed no-exceptions configuration, response, profile, numeric-limit, and expansion smoke checks."); + return EXIT_SUCCESS; +} diff --git a/tests/unittests/CMakeLists.txt b/tests/unittests/CMakeLists.txt index 14502a66f..84a73b4ce 100644 --- a/tests/unittests/CMakeLists.txt +++ b/tests/unittests/CMakeLists.txt @@ -31,6 +31,7 @@ set(SRCS InformationProviderImplTests.cpp KillSwitchManagerTests.cpp LoggerTests.cpp + LogConfigurationTests.cpp LogManagerImplTests.cpp LogSessionDataTests.cpp LogSessionDataDBTests.cpp @@ -68,6 +69,11 @@ endif() # The AIJsonSerializer test sources are guarded by HAVE_MAT_AI. if (EXISTS "${PROJECT_SOURCE_DIR}/lib/modules/azmon/AIJsonSerializer.hpp") list(APPEND MATSDK_UNIT_TEST_DEFINITIONS HAVE_MAT_AI) + if(MATSDK_BUILD_AZMON AND EXISTS + "${PROJECT_SOURCE_DIR}/lib/modules/azmon/tests/AIHttpPartialResponseHandlerTests.cpp") + list(APPEND SRCS + "${PROJECT_SOURCE_DIR}/lib/modules/azmon/tests/AIHttpPartialResponseHandlerTests.cpp") + endif() endif() if (APPLE) @@ -145,4 +151,6 @@ if(TARGET nlohmann_json::nlohmann_json) target_link_libraries(UnitTests PRIVATE nlohmann_json::nlohmann_json) endif() -add_test(UnitTests UnitTests "--gtest_output=xml:${PROJECT_BINARY_DIR}/test-reports/UnitTests.xml") +add_test(NAME UnitTests COMMAND UnitTests + "--gtest_output=xml:${PROJECT_BINARY_DIR}/test-reports/UnitTests.xml") +set_tests_properties(UnitTests PROPERTIES WORKING_DIRECTORY "${PROJECT_BINARY_DIR}") diff --git a/tests/unittests/CorrelationVectorTests.cpp b/tests/unittests/CorrelationVectorTests.cpp index 66ee9d47e..c604052d9 100644 --- a/tests/unittests/CorrelationVectorTests.cpp +++ b/tests/unittests/CorrelationVectorTests.cpp @@ -111,3 +111,21 @@ TEST(CorrelationVectorTests, TestCorrelationVector_Version2) { TestCorrelationVectorVersion(2, 22, 127, "01234567890123456789ab.4294967295.4294967294"); } + +TEST(CorrelationVectorTests, SetValue_DecimalBoundariesAndLeadingZeros) +{ + CorrelationVector cv; + const string base = "0123456789abcdef"; + ASSERT_TRUE(cv.SetValue(base + ".4294967295")); + EXPECT_EQ(cv.GetValue(), base + ".4294967295"); + EXPECT_FALSE(cv.CanIncrement()); + + ASSERT_TRUE(cv.SetValue(base + ".0000000001")); + EXPECT_EQ(cv.GetValue(), base + ".1"); + const string savedValue = cv.GetValue(); + for (const char* invalid : {"4294967296", "9999999999", "00000000001", "+1", "1x", " 1"}) + { + EXPECT_FALSE(cv.SetValue(base + "." + invalid)) << invalid; + EXPECT_EQ(cv.GetValue(), savedValue); + } +} diff --git a/tests/unittests/HttpResponseDecoderTests.cpp b/tests/unittests/HttpResponseDecoderTests.cpp index 7d11ae4b8..cf595704c 100644 --- a/tests/unittests/HttpResponseDecoderTests.cpp +++ b/tests/unittests/HttpResponseDecoderTests.cpp @@ -114,3 +114,26 @@ TEST_F(HttpResponseDecoderTests, SkipsAbortedRequests) })); decoder.decode(ctx); } + +#ifdef HAVE_MAT_JSONHPP +TEST_F(HttpResponseDecoderTests, MalformedJsonSchemaDoesNotChangeSuccessfulOutcome) +{ + for (const char* body : { + "not-json", "[", "null", "1", "[]", + R"({"efi":null})", R"({"efi":1})", R"({"efi":"all"})", R"({"efi":["all"]})", + R"({"acc":1e100,"efi":{"tenant":"all"}})", + R"({"rej":18446744073709551615,"efi":{"tenant":"all"}})"}) + { + auto ctx = createContextWith(HttpResult_OK, 200, body); + EXPECT_CALL(*this, resultEventsAccepted(ctx)).WillOnce(Return()); + decoder.decode(ctx); + } +} + +TEST_F(HttpResponseDecoderTests, ObjectFailureInformationStillRejectsEvents) +{ + auto ctx = createContextWith(HttpResult_OK, 200, R"({"efi":{"tenant":"all"}})"); + EXPECT_CALL(*this, resultEventsRejected(ctx)).WillOnce(Return()); + decoder.decode(ctx); +} +#endif diff --git a/tests/unittests/KillSwitchManagerTests.cpp b/tests/unittests/KillSwitchManagerTests.cpp index ceec1f450..79983d4b3 100644 --- a/tests/unittests/KillSwitchManagerTests.cpp +++ b/tests/unittests/KillSwitchManagerTests.cpp @@ -257,6 +257,26 @@ TEST(KillSwitchManagerTests, handleResponse_OutOfRangeKillDuration_DoesNotThrowA ASSERT_FALSE(manager.isTokenBlocked("tenant-token-1")); } +TEST(KillSwitchManagerTests, handleResponse_RetryAfterSigned64BitBoundary) +{ + for (const char* valid : {"9223372036854775807", "00000000000000000000000000001"}) + { + KillSwitchManager manager; + HttpHeaders headers; + headers.add("Retry-After", valid); + manager.handleResponse(headers); + EXPECT_TRUE(manager.isRetryAfterActive()) << valid; + } + for (const char* invalid : {"9223372036854775808", "18446744073709551615"}) + { + KillSwitchManager manager; + HttpHeaders headers; + headers.add("Retry-After", invalid); + manager.handleResponse(headers); + EXPECT_FALSE(manager.isRetryAfterActive()) << invalid; + } +} + TEST(KillSwitchManagerTests, handleResponse_KillTokenWithAllSuffix_BlocksBaseToken) { // The collector may send ":all" to mean all events of that tenant are diff --git a/tests/unittests/LogConfigurationTests.cpp b/tests/unittests/LogConfigurationTests.cpp new file mode 100644 index 000000000..af14df27d --- /dev/null +++ b/tests/unittests/LogConfigurationTests.cpp @@ -0,0 +1,44 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +// +#include "common/Common.hpp" +#include "LogConfiguration.hpp" + +#include + +using namespace MAT; + +#ifdef HAVE_MAT_JSONHPP +TEST(LogConfigurationTests, NullAndNonObjectRootsReturnEmpty) +{ + const char* inputs[] = {nullptr, "null", "1", "true", "\"text\"", "[]", "[{}]"}; + for (const auto input : inputs) + { + auto config = FromJSON(input); + EXPECT_TRUE((*config).empty()); + } +} + +TEST(LogConfigurationTests, MalformedSyntaxRespectsExceptionMode) +{ + for (const char* input : {"", "[", "not-json"}) + { +#if HAVE_EXCEPTIONS + EXPECT_ANY_THROW(FromJSON(input)); +#else + auto config = FromJSON(input); + EXPECT_TRUE((*config).empty()); +#endif + } +} + +TEST(LogConfigurationTests, PreservesNestedValuesAndNumericLimits) +{ + auto config = FromJSON(R"({"nested":{"key":"value"},"signed":-9223372036854775808,"unsigned":18446744073709551615,"enabled":true})"); + EXPECT_TRUE(config.HasConfig("nested")); + EXPECT_EQ(static_cast(config["signed"]), std::numeric_limits::min()); + EXPECT_EQ(static_cast(config["unsigned"]), std::numeric_limits::max()); + EXPECT_TRUE(static_cast(config["enabled"])); +} +#endif diff --git a/tests/unittests/OfflineStorageTests_SQLite.cpp b/tests/unittests/OfflineStorageTests_SQLite.cpp index 1d9ca9a7a..79f631c83 100644 --- a/tests/unittests/OfflineStorageTests_SQLite.cpp +++ b/tests/unittests/OfflineStorageTests_SQLite.cpp @@ -533,7 +533,13 @@ TEST_F(OfflineStorageTests_SQLite, DeleteRecordsInvalidNumericFilterDeletesNothi // be rejected as an invalid filter, not coerced to 0 and used to match rows. // Both stored records have retry_count = 0, so a coerced "0" would wrongly // delete them; fail-closed behavior leaves both intact. - offlineStorage->DeleteRecords({{"retry_count", "0 OR 1=1"}}); + for (const std::string& invalid : { + std::string("0 OR 1=1"), std::string("9223372036854775808"), + std::string("-9223372036854775809"), std::string(" "), + std::string(""), std::string("0\0trailing", 10)}) + { + offlineStorage->DeleteRecords({{"retry_count", invalid}}); + } TestRecordConsumer consumer; EXPECT_THAT(offlineStorage->GetAndReserveRecords(consumer, 100000), true); @@ -1129,6 +1135,40 @@ TEST_F(OfflineStorageTests_SQLite, ResizeDbCompactsThePhysicalDatabase) EXPECT_LE(offlineStorage->GetSize(), maximumSize); } +TEST_F(OfflineStorageTests_SQLite, StorageLimitIncludesLargePageOverhead) +{ + sqlite3* db = nullptr; + ASSERT_EQ(sqlite3_open(storageFilename.c_str(), &db), SQLITE_OK); + const int result = sqlite3_exec(db, "PRAGMA page_size=65536; PRAGMA auto_vacuum=FULL; VACUUM;", nullptr, nullptr, nullptr); + const int closeResult = sqlite3_close(db); + ASSERT_EQ(result, SQLITE_OK); + ASSERT_EQ(closeResult, SQLITE_OK); + + constexpr size_t maximumSize = 1024 * 1024 - 1; + EXPECT_CALL(configMock, GetOfflineStorageMaximumSizeBytes()) + .WillRepeatedly(Return(maximumSize)); + configMock[CFG_BOOL_ENABLE_DB_DROP_IF_FULL] = true; + configMock[CFG_INT_STORAGE_FULL_PCT] = 75; + configMock[CFG_INT_STORAGE_FULL_CHECK_TIME] = 5000; + initializeStorage(false); + + const size_t initialSize = offlineStorage->GetSize(); + ASSERT_LT(initialSize, maximumSize); + for (int i = 0; i < 5; ++i) + { + const std::string id = "record-" + std::to_string(i); + const std::string token = "token"; + ASSERT_TRUE(offlineStorage->StoreRecord({ + id, + token, + EventLatency_Normal, + EventPersistence_Normal, + i + 1, + StorageBlob(maximumSize - initialSize - id.size() - token.size()) })); + ASSERT_LE(offlineStorage->GetSize(), maximumSize) << "record " << i; + } +} + TEST_F(OfflineStorageTests_SQLite, TrimmingAlwaysDropsAtLeastOneEvent) { EXPECT_CALL(configMock, GetOfflineStorageMaximumSizeBytes()) diff --git a/tests/unittests/PayloadDecoderTests.cpp b/tests/unittests/PayloadDecoderTests.cpp index 5ec715f54..6a3d8f6a2 100644 --- a/tests/unittests/PayloadDecoderTests.cpp +++ b/tests/unittests/PayloadDecoderTests.cpp @@ -5,11 +5,22 @@ #include "common/Common.hpp" #include "PayloadDecoder.hpp" #include "utils/ZlibUtils.hpp" +#include #if defined(HAVE_MAT_ZLIB) && defined(HAVE_MAT_JSONHPP) #include "bond/All.hpp" #include "bond/generated/CsProtocol_writers.hpp" #include + +namespace clienttelemetry { + namespace data { + namespace v3 { + bool Expand(const char* source, size_t sourceLen, char** dest, size_t& destLen, bool sizeAtZeroIndex); + bool Expand(const char* source, size_t sourceLen, char** dest, size_t& destLen, bool sizeAtZeroIndex, + char* (*allocate)(size_t)); + } + } +} #endif using namespace testing; @@ -134,3 +145,33 @@ TEST(PayloadDecoderTests, DecodeRecord_ValidUtf8_IsPreserved) << "Valid UTF-8 must not be altered"; } } + +#if defined(HAVE_MAT_ZLIB) && defined(HAVE_MAT_JSONHPP) +TEST(PayloadDecoderTests, Expand_UnrepresentableSizeClearsOutput) +{ + const char source[] = "invalid"; + char* output = nullptr; + size_t outputSize = std::numeric_limits::max(); + EXPECT_FALSE(clienttelemetry::data::v3::Expand(source, sizeof(source), &output, outputSize, false)); + EXPECT_EQ(output, nullptr); + EXPECT_EQ(outputSize, 0u); +} + +TEST(PayloadDecoderTests, Expand_AllocationFailureClearsExistingOutput) +{ + const char source[] = "invalid"; + char previousOutput = 0; + char* output = &previousOutput; + size_t outputSize = 32; + static size_t requestedSize = 0; + requestedSize = 0; + EXPECT_FALSE(clienttelemetry::data::v3::Expand(source, sizeof(source), &output, outputSize, false, + [](size_t size) -> char* { + requestedSize = size; + return nullptr; + })); + EXPECT_EQ(requestedSize, 32u); + EXPECT_EQ(output, nullptr); + EXPECT_EQ(outputSize, 0u); +} +#endif diff --git a/tests/unittests/TransmitProfilesTests.cpp b/tests/unittests/TransmitProfilesTests.cpp index ce8839de5..7140416ee 100644 --- a/tests/unittests/TransmitProfilesTests.cpp +++ b/tests/unittests/TransmitProfilesTests.cpp @@ -4,6 +4,7 @@ // #include "common/Common.hpp" #include +#include using namespace testing; using namespace MAT; @@ -459,6 +460,46 @@ R"([{ ASSERT_TRUE(TransmitProfiles::load(badRule)); } + +TEST_F(TransmitProfilesTests, load_Json_MalformedSyntaxOrSchema_ReturnsFalse) +{ + for (const char* profile : { + "not-json", "[", "null", "{}", "[null]", "[1]", + R"([{"name":null}])", R"([{"name":1}])", R"([{"name":[]}])", + R"([{"name":"Invalid","rules":null}])", + R"([{"name":"Invalid","rules":{}}])", + R"([{"name":"Invalid","rules":[null]}])", + R"([{"name":"Invalid","rules":[{"netCost":1}]}])", + R"([{"name":"Invalid","rules":[{"powerState":null}]}])", + R"([{"name":"Invalid","rules":[{"timers":1}]}])", + R"([{"name":"Invalid","rules":[{"timers":{}}]}])", + R"([{"name":"Invalid","rules":[{"timers":[2147483648]}]}])", + R"([{"name":"Invalid","rules":[{"timers":[-2147483649]}]}])", + R"([{"name":"Invalid","rules":[{"timers":[18446744073709551615]}]}])", + R"([{"name":"Invalid","rules":[{"timers":[1e100]}]}])", + R"([{"name":"Invalid","rules":[{"timers":[-1e100]}]}])", + R"([{"name":"Invalid","rules":[{"timers":[1e999]}]}])"}) + { + EXPECT_FALSE(TransmitProfiles::load(profile)) << profile; + EXPECT_EQ(TransmitProfiles::profiles.count("Invalid"), 0u) << profile; + } +} + +TEST_F(TransmitProfilesTests, load_Json_TimerLimitsAndFractionalValuesPreserveConversions) +{ + ASSERT_TRUE(TransmitProfiles::load( + R"([{"name":"Boundary","rules":[{"timers":[-2147483648,2147483647,1.5]}]}])")); + const auto& timers = TransmitProfiles::profiles["Boundary"].rules[0].timers; + EXPECT_EQ(timers, (std::vector{std::numeric_limits::min(), std::numeric_limits::max(), 1})); +} + +TEST_F(TransmitProfilesTests, load_Json_InvalidProfilePreservesValidPrefix) +{ + ASSERT_TRUE(TransmitProfiles::load( + R"([{"name":"Valid","rules":[{"timers":[1,2,3]}]},{"name":"Invalid","rules":false}])")); + EXPECT_EQ(TransmitProfiles::profiles.count("Valid"), 1u); + EXPECT_EQ(TransmitProfiles::profiles.count("Invalid"), 0u); +} #else TEST_F(TransmitProfilesTests, load_Json_JsonNotEnabled_ReturnsFalse) { diff --git a/tests/unittests/UnitTests.vcxproj b/tests/unittests/UnitTests.vcxproj index ec66e75b6..ce61469ff 100644 --- a/tests/unittests/UnitTests.vcxproj +++ b/tests/unittests/UnitTests.vcxproj @@ -457,6 +457,7 @@ + diff --git a/tests/unittests/UtilsTests.cpp b/tests/unittests/UtilsTests.cpp index b1cb0bfd9..88bacda90 100644 --- a/tests/unittests/UtilsTests.cpp +++ b/tests/unittests/UtilsTests.cpp @@ -6,6 +6,9 @@ #include "common/Common.hpp" #include #include "CorrelationVector.hpp" +#ifdef _WIN32 +#include "utils/WindowsUtils.hpp" +#endif using namespace testing; using namespace MAT; @@ -69,3 +72,22 @@ TEST(UtilsTests, TestValidatePropertyName) EXPECT_TRUE(validatePropertyName(CorrelationVector::PropertyName)); } +#ifdef _WIN32 +TEST(UtilsTests, WindowsProcedureLookupPreservesSignatures) +{ + const auto kernel32 = ::GetModuleHandleW(L"kernel32.dll"); + ASSERT_NE(kernel32, nullptr); + const auto getTempPath = GetWindowsProcAddress(kernel32, "GetTempPathW"); + ASSERT_NE(getTempPath, nullptr); + wchar_t path[MAX_PATH + 1] = {}; + EXPECT_GT(getTempPath(MAX_PATH + 1, path), DWORD { 0 }); + + const auto getTime = GetWindowsProcAddress( + kernel32, "GetSystemTimePreciseAsFileTime"); + ASSERT_NE(getTime, nullptr); + FILETIME time = {}; + getTime(&time); + EXPECT_NE(time.dwHighDateTime, DWORD { 0 }); + EXPECT_EQ(GetWindowsProcAddress(kernel32, "MatSdkMissingProcedure"), nullptr); +} +#endif diff --git a/tests/vcpkg/CMakeLists.txt b/tests/vcpkg/CMakeLists.txt index 709b3038c..b197a0920 100644 --- a/tests/vcpkg/CMakeLists.txt +++ b/tests/vcpkg/CMakeLists.txt @@ -12,6 +12,13 @@ endif() find_package(MSTelemetry CONFIG REQUIRED) +get_target_property(SDK_COMPILE_OPTIONS MSTelemetry::mat INTERFACE_COMPILE_OPTIONS) +get_target_property(SDK_COMPILE_DEFINITIONS MSTelemetry::mat INTERFACE_COMPILE_DEFINITIONS) +if(SDK_COMPILE_OPTIONS MATCHES "fno-exceptions|/EHs-c-" + OR SDK_COMPILE_DEFINITIONS MATCHES "_HAS_EXCEPTIONS|MATSDK_DISABLE_LOGGING|MATSDK_DISABLE_DEVICE_ID") + message(FATAL_ERROR "SDK-private compiler policy leaked to consumers.") +endif() + add_executable(vcpkg_test main.cpp) target_link_libraries(vcpkg_test PRIVATE MSTelemetry::mat) diff --git a/tests/vcpkg/native-feature-tests.cmake b/tests/vcpkg/native-feature-tests.cmake new file mode 100644 index 000000000..85892ac2c --- /dev/null +++ b/tests/vcpkg/native-feature-tests.cmake @@ -0,0 +1,172 @@ +cmake_minimum_required(VERSION 3.19) + +get_filename_component(REPO_ROOT "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE) +set(ENV{MATSDK_VCPKG_SOURCE_DIR} "${REPO_ROOT}") + +function(vcpkg_cmake_configure) + cmake_parse_arguments(CONFIG "" "SOURCE_PATH" "OPTIONS" ${ARGN}) + foreach(option IN LISTS EXPECTED_OPTIONS) + if(NOT "${option}" IN_LIST CONFIG_OPTIONS) + message(FATAL_ERROR "Missing ${option}: ${CONFIG_OPTIONS}") + endif() + endforeach() + set(LEGACY_OPTIONS) + foreach(option IN LISTS CONFIG_OPTIONS) + if(option MATCHES "^-D(MATSDK_USE_VCPKG_DEPS|MATSDK_MINIMAL_SQLITE)=") + message(FATAL_ERROR "Obsolete dependency option was forwarded: ${option}") + endif() + if(option MATCHES "^-DBUILD_(HEADERS|LIBRARY|TEST_TOOL|UNIT_TESTS|FUNC_TESTS|JNI_WRAPPER|OBJC_WRAPPER|SWIFT_WRAPPER|PACKAGE|APPLE_HTTP)=") + list(APPEND LEGACY_OPTIONS "${option}") + endif() + endforeach() + if(NOT "${LEGACY_OPTIONS}" STREQUAL "${EXPECTED_LEGACY_OPTIONS}") + message(FATAL_ERROR "Incorrect legacy build options: ${LEGACY_OPTIONS}") + endif() + set(CONFIGURED ON PARENT_SCOPE) +endfunction() + +function(vcpkg_cmake_install) +endfunction() +function(vcpkg_cmake_config_fixup) +endfunction() +function(vcpkg_install_copyright) +endfunction() + +function(check_feature_mapping platform linkage) + set(FEATURES ${ARGN}) + set(CURRENT_PORT_DIR "${REPO_ROOT}/tools/ports/cpp-client-telemetry") + set(CURRENT_PACKAGES_DIR "${REPO_ROOT}/out/native-feature-tests") + set(VCPKG_LIBRARY_LINKAGE "${linkage}") + foreach(target LINUX WINDOWS OSX IOS ANDROID) + set(VCPKG_TARGET_IS_${target} OFF) + endforeach() + set(VCPKG_TARGET_IS_${platform} ON) + set(EXPECTED_OPTIONS + -DMATSDK_BUILD_HEADERS=ON -DMATSDK_BUILD_LIBRARY=ON + -DMATSDK_BUILD_TEST_TOOL=OFF -DMATSDK_BUILD_UNIT_TESTS=OFF + -DMATSDK_BUILD_FUNC_TESTS=OFF -DMATSDK_BUILD_PACKAGE=OFF + -DMATSDK_BUILD_JNI_WRAPPER=OFF -DMATSDK_BUILD_OBJC_WRAPPER=OFF + -DMATSDK_BUILD_SWIFT_WRAPPER=OFF) + foreach(pair IN ITEMS + "no-exceptions|MATSDK_DISABLE_EXCEPTIONS" + "no-logging|MATSDK_DISABLE_LOGGING" + "android-capi-http-client|MATSDK_ENABLE_CAPI_HTTP_CLIENT" + "device-id|MATSDK_ENABLE_DEVICE_ID" + "wininet|MATSDK_USE_WININET") + string(REPLACE "|" ";" pair "${pair}") + list(GET pair 0 feature) + list(GET pair 1 option) + set(enabled OFF) + if(feature IN_LIST FEATURES) + set(enabled ON) + endif() + list(APPEND EXPECTED_OPTIONS "-D${option}=${enabled}") + endforeach() + set(shared OFF) + if(linkage STREQUAL "dynamic") + set(shared ON) + endif() + list(APPEND EXPECTED_OPTIONS "-DBUILD_SHARED_LIBS=${shared}") + set(sqlite SYSTEM) + if("minimal-sqlite" IN_LIST FEATURES) + set(sqlite MINIMAL) + endif() + list(APPEND EXPECTED_OPTIONS "-DMATSDK_SQLITE_PROVIDER=${sqlite}") + set(apple_http OFF) + if(platform STREQUAL "OSX" OR platform STREQUAL "IOS") + set(apple_http ON) + endif() + list(APPEND EXPECTED_OPTIONS "-DMATSDK_BUILD_APPLE_HTTP=${apple_http}") + set(android_http AUTO) + if(platform STREQUAL "ANDROID" + AND ("android-curl-openssl" IN_LIST FEATURES OR "android-curl-mbedtls" IN_LIST FEATURES)) + set(android_http CURL) + endif() + list(APPEND EXPECTED_OPTIONS "-DMATSDK_ANDROID_HTTP_CLIENT=${android_http}") + set(CONFIGURED OFF) + include("${CURRENT_PORT_DIR}/portfile.cmake") + if(NOT CONFIGURED) + message(FATAL_ERROR "The feature mapping did not configure the SDK.") + endif() +endfunction() + +if(DEFINED UNSUPPORTED_FEATURE) + set(ENV{MATSDK_VCPKG_SOURCE_DIR} "${OLD_SOURCE}") + if(UNSUPPORTED_FEATURE STREQUAL "minimal-sqlite") + check_feature_mapping(ANDROID static device-id minimal-sqlite) + else() + check_feature_mapping(ANDROID static device-id system-sqlite "${UNSUPPORTED_FEATURE}") + endif() + return() +endif() + +file(READ "${REPO_ROOT}/tools/ports/cpp-client-telemetry/vcpkg.json" MANIFEST) +foreach(feature no-exceptions no-logging android-capi-http-client) + string(JSON DESCRIPTION GET "${MANIFEST}" features "${feature}" description) +endforeach() +string(JSON CAPI_PLATFORMS GET "${MANIFEST}" features android-capi-http-client supports) +if(NOT CAPI_PLATFORMS STREQUAL "android") + message(FATAL_ERROR "Custom Android C API HTTP callbacks must be Android-only.") +endif() +string(JSON DEFAULT_COUNT LENGTH "${MANIFEST}" default-features) +math(EXPR DEFAULT_LAST "${DEFAULT_COUNT} - 1") +set(DEFAULT_FEATURES) +foreach(index RANGE ${DEFAULT_LAST}) + string(JSON FEATURE GET "${MANIFEST}" default-features ${index}) + if(FEATURE MATCHES "^(no-exceptions|no-logging|android-capi-http-client)$") + message(FATAL_ERROR "${FEATURE} must remain opt-in.") + endif() + list(APPEND DEFAULT_FEATURES "${FEATURE}") +endforeach() +check_feature_mapping(LINUX static ${DEFAULT_FEATURES}) + +foreach(platform LINUX WINDOWS OSX IOS ANDROID) + foreach(linkage static dynamic) + foreach(options IN ITEMS "" "no-exceptions" "no-logging" "no-exceptions;no-logging") + check_feature_mapping("${platform}" "${linkage}" + device-id curl-openssl system-sqlite ${options}) + check_feature_mapping("${platform}" "${linkage}" + curl-mbedtls minimal-sqlite ${options}) + endforeach() + endforeach() +endforeach() +check_feature_mapping(WINDOWS static wininet minimal-sqlite no-exceptions no-logging) +foreach(transport "" android-curl-openssl android-curl-mbedtls) + check_feature_mapping(ANDROID static + minimal-sqlite android-capi-http-client no-exceptions no-logging ${transport}) +endforeach() + +set(OLD_SOURCE "${REPO_ROOT}/out/native-feature-tests/older-source") +file(MAKE_DIRECTORY "${OLD_SOURCE}") +file(WRITE "${OLD_SOURCE}/CMakeLists.txt" + "# MATSDK_ANDROID_HTTP_CLIENT MATSDK_USE_WININET MATSDK_ENABLE_DEVICE_ID\n") +foreach(feature no-exceptions no-logging android-capi-http-client minimal-sqlite) + execute_process( + COMMAND "${CMAKE_COMMAND}" "-DUNSUPPORTED_FEATURE=${feature}" + "-DOLD_SOURCE=${OLD_SOURCE}" -P "${CMAKE_CURRENT_LIST_FILE}" + RESULT_VARIABLE RESULT OUTPUT_QUIET ERROR_VARIABLE ERROR) + if(RESULT EQUAL 0 OR NOT ERROR MATCHES "The ${feature} feature requires") + message(FATAL_ERROR "Unsupported ${feature} must fail explicitly: ${ERROR}") + endif() +endforeach() + +file(APPEND "${OLD_SOURCE}/CMakeLists.txt" + "# MATSDK_USE_VCPKG_DEPS MATSDK_MINIMAL_SQLITE MATSDK_SQLITE_PROVIDER\n") +set(ENV{MATSDK_VCPKG_SOURCE_DIR} "${OLD_SOURCE}") +set(EXPECTED_LEGACY_OPTIONS + -DBUILD_HEADERS=ON -DBUILD_LIBRARY=ON -DBUILD_TEST_TOOL=OFF + -DBUILD_UNIT_TESTS=OFF -DBUILD_FUNC_TESTS=OFF -DBUILD_JNI_WRAPPER=OFF + -DBUILD_OBJC_WRAPPER=OFF -DBUILD_SWIFT_WRAPPER=OFF -DBUILD_PACKAGE=OFF + -DBUILD_APPLE_HTTP=OFF) +check_feature_mapping(LINUX static device-id curl-openssl minimal-sqlite) +list(REMOVE_ITEM EXPECTED_LEGACY_OPTIONS -DBUILD_APPLE_HTTP=OFF) +list(APPEND EXPECTED_LEGACY_OPTIONS -DBUILD_APPLE_HTTP=ON) +check_feature_mapping(IOS static device-id minimal-sqlite) + +file(APPEND "${OLD_SOURCE}/CMakeLists.txt" + "option(MATSDK_BUILD_HEADERS \"Build API headers\" ON)\n") +list(REMOVE_ITEM EXPECTED_LEGACY_OPTIONS -DBUILD_HEADERS=ON -DBUILD_APPLE_HTTP=ON) +list(APPEND EXPECTED_LEGACY_OPTIONS -DBUILD_APPLE_HTTP=OFF) +check_feature_mapping(ANDROID static device-id system-sqlite) +file(REMOVE "${OLD_SOURCE}/CMakeLists.txt") +message(STATUS "Native vcpkg feature mappings passed") diff --git a/tests/vcpkg/test-release-port.py b/tests/vcpkg/test-release-port.py index b637627ae..c75cceb34 100644 --- a/tests/vcpkg/test-release-port.py +++ b/tests/vcpkg/test-release-port.py @@ -64,6 +64,14 @@ def test_promotes_complete_release_port(self): self.assertIn(f"SHA512 {'a' * 128}\n", portfile) self.assertIn("-DMATSDK_SQLITE_PROVIDER=${MATSDK_VCPKG_SQLITE_PROVIDER}", portfile) self.assertIn("-DMATSDK_ANDROID_HTTP_CLIENT=${MATSDK_ANDROID_HTTP_CLIENT}", portfile) + self.assertIn("${MATSDK_NATIVE_FEATURE_OPTIONS}", portfile) + for feature, option in ( + ("no-exceptions", "MATSDK_DISABLE_EXCEPTIONS"), + ("no-logging", "MATSDK_DISABLE_LOGGING"), + ("android-capi-http-client", "MATSDK_ENABLE_CAPI_HTTP_CLIENT"), + ): + self.assertIn(feature, actual["features"]) + self.assertIn(f"{feature}|{option}", portfile) self.assertEqual((self.destination / "release.patch").read_text(), "new patch") self.assertFalse((self.destination / "obsolete.patch").exists()) self.assertEqual( diff --git a/tests/vcpkg/test-vcpkg-android.sh b/tests/vcpkg/test-vcpkg-android.sh index af9bc6a88..3f0268db2 100755 --- a/tests/vcpkg/test-vcpkg-android.sh +++ b/tests/vcpkg/test-vcpkg-android.sh @@ -3,7 +3,7 @@ # Usage: ./tests/vcpkg/test-vcpkg-android.sh [ABI] [API_LEVEL] [FEATURES] # ABI: arm64-v8a (default), armeabi-v7a, x86_64, x86 # API_LEVEL: 23 (default), 28, or another level with a matching overlay triplet -# FEATURES: default, minimal-sqlite, or minimal-sqlite;android-curl-{openssl,mbedtls} +# FEATURES: default or semicolon-separated manifest features # Prerequisites: VCPKG_ROOT set, ANDROID_NDK_HOME set, cmake, ninja set -e @@ -19,17 +19,20 @@ export MATSDK_VCPKG_SOURCE_DIR="${REPO_ROOT}" ANDROID_ABI="${1:-arm64-v8a}" ANDROID_API="${2:-23}" MANIFEST_ARGS=() -case "${3:-default}" in - default) ;; - minimal-sqlite|"minimal-sqlite;android-curl-openssl"|"minimal-sqlite;android-curl-mbedtls") - MANIFEST_ARGS=(-DVCPKG_MANIFEST_NO_DEFAULT_FEATURES=ON "-DVCPKG_MANIFEST_FEATURES=${3}") - ;; - *) - echo "ERROR: Unsupported feature set '${3}'. Use default, minimal-sqlite," - echo " minimal-sqlite;android-curl-openssl, or minimal-sqlite;android-curl-mbedtls." - exit 1 - ;; -esac +FEATURES="${3:-default}" +if [ "${FEATURES}" != "default" ]; then + IFS=';' read -ra REQUESTED_FEATURES <<< "${FEATURES}" + for FEATURE in "${REQUESTED_FEATURES[@]}"; do + case "${FEATURE}" in + minimal-sqlite|android-curl-openssl|android-curl-mbedtls|android-capi-http-client|no-exceptions|no-logging) ;; + *) echo "ERROR: Unsupported feature '${FEATURE}'."; exit 1 ;; + esac + done + MANIFEST_ARGS=("-DVCPKG_MANIFEST_FEATURES=${FEATURES}") + if [[ ";${FEATURES};" == *";minimal-sqlite;"* ]]; then + MANIFEST_ARGS+=(-DVCPKG_MANIFEST_NO_DEFAULT_FEATURES=ON) + fi +fi # Map ABI to vcpkg triplet case "${ANDROID_ABI}" in diff --git a/tests/vcpkg/test-vcpkg-linux.sh b/tests/vcpkg/test-vcpkg-linux.sh index 06b03293d..5e754e96e 100755 --- a/tests/vcpkg/test-vcpkg-linux.sh +++ b/tests/vcpkg/test-vcpkg-linux.sh @@ -1,6 +1,7 @@ #!/bin/bash # Test script: Verify cpp-client-telemetry vcpkg port on Linux -# Usage: ./tests/vcpkg/test-vcpkg-linux.sh [default|minimal-sqlite] +# Usage: ./tests/vcpkg/test-vcpkg-linux.sh [default|FEATURES] +# FEATURES: semicolon-separated minimal-sqlite, no-exceptions, no-logging # Prerequisites: VCPKG_ROOT set, gcc/g++, cmake set -e @@ -9,16 +10,20 @@ REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" BUILD_DIR="${SCRIPT_DIR}/build-linux" OVERLAY_PORTS="${REPO_ROOT}/tools/ports" MANIFEST_ARGS=() -case "${1:-default}" in - default) ;; - minimal-sqlite) - MANIFEST_ARGS=(-DVCPKG_MANIFEST_NO_DEFAULT_FEATURES=ON -DVCPKG_MANIFEST_FEATURES=minimal-sqlite) - ;; - *) - echo "ERROR: Unsupported feature set '${1}'. Use default or minimal-sqlite." - exit 1 - ;; -esac +FEATURES="${1:-default}" +if [ "${FEATURES}" != "default" ]; then + IFS=';' read -ra REQUESTED_FEATURES <<< "${FEATURES}" + for FEATURE in "${REQUESTED_FEATURES[@]}"; do + case "${FEATURE}" in + minimal-sqlite|no-exceptions|no-logging) ;; + *) echo "ERROR: Unsupported feature '${FEATURE}'."; exit 1 ;; + esac + done + MANIFEST_ARGS=("-DVCPKG_MANIFEST_FEATURES=${FEATURES}") + if [[ ";${FEATURES};" == *";minimal-sqlite;"* ]]; then + MANIFEST_ARGS+=(-DVCPKG_MANIFEST_NO_DEFAULT_FEATURES=ON) + fi +fi # Build the working tree under review (not a pinned release) so this test # validates the actual SDK source together with the port manifest/portfile. @@ -68,7 +73,7 @@ cmake --build "${BUILD_DIR}/consumer" --config Release echo "" echo "--- Step 3: Run test ---" -"${BUILD_DIR}/consumer/vcpkg_test" +ctest --test-dir "${BUILD_DIR}/consumer" -C Release --output-on-failure echo "" echo "=== Linux vcpkg port test PASSED ===" diff --git a/tests/vcpkg/test-vcpkg-macos.sh b/tests/vcpkg/test-vcpkg-macos.sh index 9a7d1bfd3..b81728dff 100755 --- a/tests/vcpkg/test-vcpkg-macos.sh +++ b/tests/vcpkg/test-vcpkg-macos.sh @@ -61,7 +61,7 @@ cmake --build "${BUILD_DIR}/consumer" --config Release echo "" echo "--- Step 3: Run test ---" -"${BUILD_DIR}/consumer/vcpkg_test" +ctest --test-dir "${BUILD_DIR}/consumer" -C Release --output-on-failure echo "" echo "=== macOS vcpkg port test PASSED ===" diff --git a/tests/vcpkg/test-vcpkg-windows.ps1 b/tests/vcpkg/test-vcpkg-windows.ps1 index 759834413..06275d93e 100644 --- a/tests/vcpkg/test-vcpkg-windows.ps1 +++ b/tests/vcpkg/test-vcpkg-windows.ps1 @@ -5,7 +5,9 @@ param( [string]$VcpkgRoot = "", [string]$Triplet = "", - [switch]$WinInet + [switch]$WinInet, + [ValidateSet("minimal-sqlite", "no-exceptions", "no-logging")] + [string[]]$Features = @() ) $ErrorActionPreference = "Stop" @@ -87,8 +89,15 @@ $CmakeArgs = @( "-DVCPKG_OVERLAY_PORTS=$OverlayPorts", "-DCMAKE_BUILD_TYPE=Release" ) +$ManifestFeatures = @($Features) if ($WinInet) { - $CmakeArgs += "-DVCPKG_MANIFEST_FEATURES=wininet" + $ManifestFeatures += "wininet" +} +if ($ManifestFeatures.Count -gt 0) { + $CmakeArgs += "-DVCPKG_MANIFEST_FEATURES=$($ManifestFeatures -join ';')" +} +if ("minimal-sqlite" -in $ManifestFeatures) { + $CmakeArgs += "-DVCPKG_MANIFEST_NO_DEFAULT_FEATURES=ON" } # Detect whether cl.exe is on PATH (i.e., running from VS Developer Command Prompt) @@ -145,13 +154,7 @@ if (-not $clExe) { Write-Host "" Write-Host "--- Step 3: Run test ---" -ForegroundColor Yellow -$TestExe = Get-ChildItem -Path $ConsumerBuild -Recurse -Filter "vcpkg_test.exe" | Select-Object -First 1 -if ($null -eq $TestExe) { - Write-Error "Test executable not found" - exit 1 -} - -& $TestExe.FullName +ctest --test-dir $ConsumerBuild -C Release --output-on-failure if ($LASTEXITCODE -ne 0) { Write-Error "Test execution failed" exit 1 diff --git a/tests/vcpkg/vcpkg.json b/tests/vcpkg/vcpkg.json index df03c0829..3cfc37f5a 100644 --- a/tests/vcpkg/vcpkg.json +++ b/tests/vcpkg/vcpkg.json @@ -12,6 +12,19 @@ "sdk-defaults" ], "features": { + "android-capi-http-client": { + "description": "Exercise custom Android C API HTTP callbacks.", + "supports": "android", + "dependencies": [ + { + "name": "cpp-client-telemetry", + "default-features": false, + "features": [ + "android-capi-http-client" + ] + } + ] + }, "android-curl-mbedtls": { "description": "Exercise the native Android curl transport with mbedTLS.", "supports": "android", @@ -51,6 +64,30 @@ } ] }, + "no-exceptions": { + "description": "Exercise an exception-free SDK with an exception-enabled consumer.", + "dependencies": [ + { + "name": "cpp-client-telemetry", + "default-features": false, + "features": [ + "no-exceptions" + ] + } + ] + }, + "no-logging": { + "description": "Exercise the SDK with internal diagnostics compiled out.", + "dependencies": [ + { + "name": "cpp-client-telemetry", + "default-features": false, + "features": [ + "no-logging" + ] + } + ] + }, "sdk-defaults": { "description": "Exercise the default SDK dependency graph.", "dependencies": [ @@ -63,6 +100,7 @@ "dependencies": [ { "name": "cpp-client-telemetry", + "default-features": false, "features": [ "wininet" ] diff --git a/tools/ports/cpp-client-telemetry/portfile.cmake b/tools/ports/cpp-client-telemetry/portfile.cmake index b6ff32379..7c36c8f5d 100644 --- a/tools/ports/cpp-client-telemetry/portfile.cmake +++ b/tools/ports/cpp-client-telemetry/portfile.cmake @@ -32,6 +32,12 @@ if(NOT DEFINED SOURCE_PATH) ) endif() +file(READ "${SOURCE_PATH}/CMakeLists.txt" MATSDK_OPTION_SOURCE) +if(EXISTS "${SOURCE_PATH}/cmake/MatsdkOptions.cmake") + file(READ "${SOURCE_PATH}/cmake/MatsdkOptions.cmake" MATSDK_OPTIONS_CMAKE) + string(APPEND MATSDK_OPTION_SOURCE "\n${MATSDK_OPTIONS_CMAKE}") +endif() + # Determine if Apple HTTP should be used (no curl needed). # Note: MATSDK_BUILD_APPLE_HTTP must remain ON for macOS/iOS because the vcpkg.json # curl dependency is excluded on these platforms. @@ -48,15 +54,7 @@ endif() set(MATSDK_ANDROID_HTTP_CLIENT AUTO) if(VCPKG_TARGET_IS_ANDROID) - file(READ "${SOURCE_PATH}/CMakeLists.txt" _matsdk_root_cmake) - set(_matsdk_android_option_source "${_matsdk_root_cmake}") - if(EXISTS "${SOURCE_PATH}/cmake/MatsdkOptions.cmake") - file(READ "${SOURCE_PATH}/cmake/MatsdkOptions.cmake" - _matsdk_options_cmake) - string(APPEND _matsdk_android_option_source - "\n${_matsdk_options_cmake}") - endif() - if(NOT _matsdk_android_option_source MATCHES "MATSDK_ANDROID_HTTP_CLIENT") + if(NOT MATSDK_OPTION_SOURCE MATCHES "MATSDK_ANDROID_HTTP_CLIENT") message(FATAL_ERROR "Android vcpkg builds require a cpp-client-telemetry source revision that " "supports MATSDK_ANDROID_HTTP_CLIENT. Update this port's REF/SHA512 to a " @@ -115,6 +113,10 @@ endif() set(MATSDK_VCPKG_SQLITE_PROVIDER SYSTEM) if("minimal-sqlite" IN_LIST FEATURES) + if(NOT MATSDK_OPTION_SOURCE MATCHES "MATSDK_SQLITE_PROVIDER") + message(FATAL_ERROR + "The minimal-sqlite feature requires an SDK revision supporting MATSDK_SQLITE_PROVIDER.") + endif() set(MATSDK_VCPKG_SQLITE_PROVIDER MINIMAL) endif() @@ -124,12 +126,6 @@ else() set(MATSDK_VCPKG_BUILD_SHARED_LIBS OFF) endif() -file(READ "${SOURCE_PATH}/CMakeLists.txt" MATSDK_ROOT_CMAKE) -set(MATSDK_OPTION_SOURCE "${MATSDK_ROOT_CMAKE}") -if(EXISTS "${SOURCE_PATH}/cmake/MatsdkOptions.cmake") - file(READ "${SOURCE_PATH}/cmake/MatsdkOptions.cmake" MATSDK_OPTIONS_CMAKE) - string(APPEND MATSDK_OPTION_SOURCE "\n${MATSDK_OPTIONS_CMAKE}") -endif() if(VCPKG_TARGET_IS_WINDOWS AND NOT MATSDK_OPTION_SOURCE MATCHES "MATSDK_USE_WININET") message(FATAL_ERROR @@ -138,13 +134,13 @@ if(VCPKG_TARGET_IS_WINDOWS "Update this port's REF/SHA512 to a newer SDK release, or set " "MATSDK_VCPKG_SOURCE_DIR to a local checkout containing that option.") endif() -set(MATSDK_PINNED_SOURCE_OPTIONS) +set(MATSDK_DEVICE_ID_OPTIONS) if(MATSDK_OPTION_SOURCE MATCHES "MATSDK_ENABLE_DEVICE_ID") set(MATSDK_ENABLE_DEVICE_ID OFF) if("device-id" IN_LIST FEATURES) set(MATSDK_ENABLE_DEVICE_ID ON) endif() - list(APPEND MATSDK_PINNED_SOURCE_OPTIONS + list(APPEND MATSDK_DEVICE_ID_OPTIONS -DMATSDK_ENABLE_DEVICE_ID=${MATSDK_ENABLE_DEVICE_ID}) elseif(NOT "device-id" IN_LIST FEATURES) message(FATAL_ERROR @@ -153,50 +149,63 @@ elseif(NOT "device-id" IN_LIST FEATURES) "REF/SHA512 to a newer SDK release, or set MATSDK_VCPKG_SOURCE_DIR " "to a local checkout containing that option.") endif() -if(MATSDK_ROOT_CMAKE MATCHES "MATSDK_USE_VCPKG_DEPS") - list(APPEND MATSDK_PINNED_SOURCE_OPTIONS -DMATSDK_USE_VCPKG_DEPS=ON) -endif() -if(MATSDK_ROOT_CMAKE MATCHES "MATSDK_MINIMAL_SQLITE" - AND "minimal-sqlite" IN_LIST FEATURES) - list(APPEND MATSDK_PINNED_SOURCE_OPTIONS -DMATSDK_MINIMAL_SQLITE=ON) -endif() set(MATSDK_USE_WININET OFF) if("wininet" IN_LIST FEATURES) set(MATSDK_USE_WININET ON) endif() +set(MATSDK_NATIVE_FEATURE_OPTIONS) +foreach(_matsdk_feature_option IN ITEMS + "no-exceptions|MATSDK_DISABLE_EXCEPTIONS" + "no-logging|MATSDK_DISABLE_LOGGING" + "android-capi-http-client|MATSDK_ENABLE_CAPI_HTTP_CLIENT") + string(REPLACE "|" ";" _matsdk_feature_option "${_matsdk_feature_option}") + list(GET _matsdk_feature_option 0 _matsdk_feature) + list(GET _matsdk_feature_option 1 _matsdk_option) + set(_matsdk_enabled OFF) + if(_matsdk_feature IN_LIST FEATURES) + if(NOT MATSDK_OPTION_SOURCE MATCHES "${_matsdk_option}") + message(FATAL_ERROR + "The ${_matsdk_feature} feature requires an SDK revision supporting ${_matsdk_option}.") + endif() + set(_matsdk_enabled ON) + endif() + list(APPEND MATSDK_NATIVE_FEATURE_OPTIONS "-D${_matsdk_option}=${_matsdk_enabled}") +endforeach() + +set(MATSDK_BUILD_OPTIONS + -DMATSDK_BUILD_HEADERS=ON + -DMATSDK_BUILD_LIBRARY=ON + -DMATSDK_BUILD_TEST_TOOL=OFF + -DMATSDK_BUILD_UNIT_TESTS=OFF + -DMATSDK_BUILD_FUNC_TESTS=OFF + -DMATSDK_BUILD_JNI_WRAPPER=OFF + -DMATSDK_BUILD_OBJC_WRAPPER=OFF + -DMATSDK_BUILD_SWIFT_WRAPPER=OFF + -DMATSDK_BUILD_PACKAGE=OFF + -DMATSDK_BUILD_APPLE_HTTP=${MATSDK_BUILD_APPLE_HTTP}) +set(MATSDK_LEGACY_BUILD_OPTIONS) +foreach(_matsdk_build_option IN LISTS MATSDK_BUILD_OPTIONS) + string(REGEX REPLACE "^-D([^=]+)=.*$" "\\1" _matsdk_option_name "${_matsdk_build_option}") + if(NOT MATSDK_OPTION_SOURCE MATCHES "${_matsdk_option_name}") + string(REPLACE "-DMATSDK_BUILD_" "-DBUILD_" _matsdk_legacy_option "${_matsdk_build_option}") + list(APPEND MATSDK_LEGACY_BUILD_OPTIONS "${_matsdk_legacy_option}") + endif() +endforeach() + vcpkg_cmake_configure( SOURCE_PATH "${SOURCE_PATH}" OPTIONS - ${MATSDK_PINNED_SOURCE_OPTIONS} + ${MATSDK_DEVICE_ID_OPTIONS} + ${MATSDK_NATIVE_FEATURE_OPTIONS} + ${MATSDK_BUILD_OPTIONS} + ${MATSDK_LEGACY_BUILD_OPTIONS} -DMATSDK_SQLITE_PROVIDER=${MATSDK_VCPKG_SQLITE_PROVIDER} -DBUILD_SHARED_LIBS=${MATSDK_VCPKG_BUILD_SHARED_LIBS} -DMATSDK_ANDROID_HTTP_CLIENT=${MATSDK_ANDROID_HTTP_CLIENT} -DMATSDK_USE_WININET=${MATSDK_USE_WININET} - -DMATSDK_BUILD_HEADERS=ON - -DMATSDK_BUILD_LIBRARY=ON - -DMATSDK_BUILD_TEST_TOOL=OFF - -DMATSDK_BUILD_UNIT_TESTS=OFF - -DMATSDK_BUILD_FUNC_TESTS=OFF - -DMATSDK_BUILD_JNI_WRAPPER=OFF - -DMATSDK_BUILD_OBJC_WRAPPER=OFF - -DMATSDK_BUILD_SWIFT_WRAPPER=OFF - -DMATSDK_BUILD_PACKAGE=OFF -DBUILD_VERSION=${VERSION} - -DMATSDK_BUILD_APPLE_HTTP=${MATSDK_BUILD_APPLE_HTTP} - # Legacy aliases keep the pinned release fallback buildable until the - # next release contains the canonical MATSDK_* options. - -DBUILD_HEADERS=ON - -DBUILD_LIBRARY=ON - -DBUILD_TEST_TOOL=OFF - -DBUILD_UNIT_TESTS=OFF - -DBUILD_FUNC_TESTS=OFF - -DBUILD_JNI_WRAPPER=OFF - -DBUILD_OBJC_WRAPPER=OFF - -DBUILD_SWIFT_WRAPPER=OFF - -DBUILD_PACKAGE=OFF - -DBUILD_APPLE_HTTP=${MATSDK_BUILD_APPLE_HTTP} ${MATSDK_APPLE_DEPLOYMENT_OPTIONS} ) diff --git a/tools/ports/cpp-client-telemetry/vcpkg.json b/tools/ports/cpp-client-telemetry/vcpkg.json index c467f869f..bea3d1295 100644 --- a/tools/ports/cpp-client-telemetry/vcpkg.json +++ b/tools/ports/cpp-client-telemetry/vcpkg.json @@ -26,6 +26,10 @@ "system-sqlite" ], "features": { + "android-capi-http-client": { + "description": "Allow Android C API consumers to provide custom HTTP send/cancel callbacks.", + "supports": "android" + }, "android-curl-mbedtls": { "description": "On Android, explicitly build the native libcurl HTTP client with the mbedTLS backend instead of the default Java/JNI HttpClient_Android bridge.", "supports": "android", @@ -86,6 +90,12 @@ "minimal-sqlite": { "description": "Build a private, feature-stripped SQLite compiled from the SDK's vendored amalgamation instead of linking the external sqlite3 package. Smaller footprint; combine with [core,minimal-sqlite] to also drop the sqlite3 dependency." }, + "no-exceptions": { + "description": "Compile the native SDK without C++ exception support. Does not change consumer or dependency compiler flags." + }, + "no-logging": { + "description": "Compile out internal SDK diagnostics without disabling telemetry event collection." + }, "system-sqlite": { "description": "Link the external vcpkg sqlite3 package for the offline storage cache (default). On macOS/iOS the SDK links the system libsqlite3 instead, so this dependency is not pulled there.", "dependencies": [ diff --git a/wrappers/obj-c/ODWDiagnosticDataViewer.mm b/wrappers/obj-c/ODWDiagnosticDataViewer.mm index 84301c2f2..515679022 100644 --- a/wrappers/obj-c/ODWDiagnosticDataViewer.mm +++ b/wrappers/obj-c/ODWDiagnosticDataViewer.mm @@ -19,11 +19,12 @@ @implementation ODWDiagnosticDataViewer +(void)initializeViewerWithMachineIdentifier:(NSString *)machineIdentifier { const std::string identifier = { [machineIdentifier UTF8String] }; - try + MATSDK_TRY { _viewer = std::make_shared (nullptr, identifier); LogManager::GetDataViewerCollection().RegisterViewer(_viewer); } +#if HAVE_EXCEPTIONS catch (const std::exception &e) { if ([ODWLogConfiguration surfaceCppExceptions]) @@ -32,6 +33,7 @@ +(void)initializeViewerWithMachineIdentifier:(NSString *)machineIdentifier } [ODWLogger traceException: e.what()]; } +#endif } +(void)enableRemoteViewer:(NSString *)endpoint completionWithResult:(void(^)(bool result))completion @@ -48,7 +50,7 @@ +(void)enableRemoteViewer:(NSString *)endpoint completionWithResult:(void(^)(boo +(bool)enableRemoteViewer:(NSString *)endpoint { bool result = false; - try + MATSDK_TRY { result = _viewer->EnableRemoteViewer(std::string([endpoint UTF8String])); if ([ODWLogConfiguration enableConsoleLogging]) @@ -56,6 +58,7 @@ +(bool)enableRemoteViewer:(NSString *)endpoint NSLog(@"RemoteDataViewer enabled on endpoint: %@ and result: %@", endpoint, result ? @"success" : @"failure"); } } +#if HAVE_EXCEPTIONS catch (const std::exception &e) { if ([ODWLogConfiguration surfaceCppExceptions]) @@ -64,6 +67,7 @@ +(bool)enableRemoteViewer:(NSString *)endpoint } [ODWLogger traceException: e.what()]; } +#endif return result; } @@ -93,10 +97,11 @@ +(bool)disableViewer +(bool)viewerEnabled { bool result = false; - try + MATSDK_TRY { result = LogManager::GetDataViewerCollection().IsViewerEnabled(_viewer->GetName()); } +#if HAVE_EXCEPTIONS catch (const std::exception &e) { if ([ODWLogConfiguration surfaceCppExceptions]) @@ -105,6 +110,7 @@ +(bool)viewerEnabled } [ODWLogger traceException: e.what()]; } +#endif return result; } diff --git a/wrappers/obj-c/ODWLogManager.mm b/wrappers/obj-c/ODWLogManager.mm index 127de8dc3..ce359f6f6 100644 --- a/wrappers/obj-c/ODWLogManager.mm +++ b/wrappers/obj-c/ODWLogManager.mm @@ -37,10 +37,11 @@ +(nullable ODWLogger *)loggerWithTenant:(nonnull NSString *)tenantToken std::string strToken = std::string([tenantToken UTF8String]); std::string strSource = std::string([source UTF8String]); ILogger* logger = nullptr; - try + MATSDK_TRY { logger = LogManager::GetLogger(strToken, strSource); } +#if HAVE_EXCEPTIONS catch (const std::exception &e) { if ([ODWLogConfiguration surfaceCppExceptions]) @@ -49,6 +50,7 @@ +(nullable ODWLogger *)loggerWithTenant:(nonnull NSString *)tenantToken } [ODWLogger traceException: e.what()]; } +#endif if(!logger) return nil; @@ -82,10 +84,11 @@ +(nullable ODWLogger *)loggerWithTenant:(nonnull NSString *)tenantToken std::string strToken = std::string([tenantToken UTF8String]); std::string strSource = std::string([source UTF8String]); ILogger* logger = nullptr; - try + MATSDK_TRY { logger = manager->GetLogger(strToken, strSource); } +#if HAVE_EXCEPTIONS catch (const std::exception &e) { if ([ODWLogConfiguration surfaceCppExceptions]) @@ -94,6 +97,7 @@ +(nullable ODWLogger *)loggerWithTenant:(nonnull NSString *)tenantToken } [ODWLogger traceException: e.what()]; } +#endif if(!logger) return nil; @@ -120,7 +124,7 @@ +(nullable ODWLogger *)initForTenant:(nonnull NSString *)tenantToken +(nullable ILogger *)initializeLogManager:(nonnull NSString *)tenantToken withConfig:(nullable NSDictionary *)config { ILogger* logger = nullptr; - try + MATSDK_TRY { static ILogConfiguration logManagerConfig; @@ -202,6 +206,7 @@ +(nullable ILogger *)initializeLogManager:(nonnull NSString *)tenantToken withCo semanticContext->SetAppLanguage(strBundleLocale); semanticContext->SetUserLanguage(strUserLocale); } +#if HAVE_EXCEPTIONS catch (const std::exception &e) { if ([ODWLogConfiguration surfaceCppExceptions]) @@ -210,6 +215,7 @@ +(nullable ILogger *)initializeLogManager:(nonnull NSString *)tenantToken withCo } [ODWLogger traceException: e.what()]; } +#endif _initialized = logger != NULL; return logger; @@ -219,10 +225,11 @@ +(nullable ODWLogger *)loggerForSource:(nonnull NSString *)source { std::string strSource = std::string([source UTF8String]); ILogger* logger = nullptr; - try + MATSDK_TRY { logger = LogManager::GetLogger(strSource); } +#if HAVE_EXCEPTIONS catch (const std::exception &e) { if ([ODWLogConfiguration surfaceCppExceptions]) @@ -231,6 +238,7 @@ +(nullable ODWLogger *)loggerForSource:(nonnull NSString *)source } [ODWLogger traceException: e.what()]; } +#endif if(!logger) return nil; return [[ODWLogger alloc] initWithILogger: logger]; @@ -245,10 +253,11 @@ +(void)uploadNow +(ODWStatus)flush { - try + MATSDK_TRY { return ((ODWStatus)LogManager::Flush()); } +#if HAVE_EXCEPTIONS catch (const std::exception &e) { if ([ODWLogConfiguration surfaceCppExceptions]) @@ -258,14 +267,16 @@ +(ODWStatus)flush [ODWLogger traceException: e.what()]; return ODWEfail; } +#endif } +(ODWStatus)flushAndTeardown { - try + MATSDK_TRY { return ((ODWStatus)LogManager::FlushAndTeardown()); } +#if HAVE_EXCEPTIONS catch (const std::exception &e) { if ([ODWLogConfiguration surfaceCppExceptions]) @@ -275,6 +286,7 @@ +(ODWStatus)flushAndTeardown [ODWLogger traceException: e.what()]; return ODWEfail; } +#endif } +(void)setTransmissionProfile:(ODWTransmissionProfile)profile @@ -286,10 +298,11 @@ +(void)setTransmissionProfile:(ODWTransmissionProfile)profile +(void)pauseTransmission { - try + MATSDK_TRY { LogManager::PauseTransmission(); } +#if HAVE_EXCEPTIONS catch (const std::exception &e) { if ([ODWLogConfiguration surfaceCppExceptions]) @@ -298,6 +311,7 @@ +(void)pauseTransmission } [ODWLogger traceException: e.what()]; } +#endif } +(void)resumeTransmission diff --git a/wrappers/obj-c/ODWLogger.mm b/wrappers/obj-c/ODWLogger.mm index 9a341d0c7..faab41258 100644 --- a/wrappers/obj-c/ODWLogger.mm +++ b/wrappers/obj-c/ODWLogger.mm @@ -410,10 +410,11 @@ void PerformActionWithCppExceptionsCatch(void (^block)()) if (!canUseSDK) { return; } - try + MATSDK_TRY { block(); } +#if HAVE_EXCEPTIONS catch (const std::exception &e) { if ([ODWLogConfiguration surfaceCppExceptions]) @@ -422,6 +423,7 @@ void PerformActionWithCppExceptionsCatch(void (^block)()) } [ODWLogger traceException: e.what()]; } +#endif } -(void)initializePrivacyGuardWithODWPrivacyGuardInitConfig:(ODWPrivacyGuardInitConfig *)initConfigObject