From 90acf9ca45204e2ca7226e6c84fa0ed0aee3d4ed Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Thu, 1 Oct 2026 01:35:52 -0500 Subject: [PATCH 01/14] Enable full SDK builds without C++ exceptions Replace throwing numeric conversions and payload allocation with explicit failure paths, guard exception-only handlers, and build the full SDK with GCC and Clang in CI so blockers outside Curl cannot remain hidden. Initialize submodules and pin the companion module fixes without dropping the existing SDK module baseline. Files changed: .github/workflows/build-posix-latest.yml; docs/linux-setup-build.md; lib/api/CorrelationVector.cpp; lib/api/capi.cpp; lib/decoder/PayloadDecoder.cpp; lib/include/public/ctmacros.hpp; lib/modules; lib/offline/KillSwitchManager.hpp; lib/offline/OfflineStorage_SQLite.cpp; lib/offline/SQLiteWrapper.hpp; lib/packager/Packager.cpp; lib/pal/InformationProviderImpl.cpp; lib/tpm/TransmitProfiles.cpp; tests/unittests/CorrelationVectorTests.cpp; tests/unittests/KillSwitchManagerTests.cpp; tests/unittests/OfflineStorageTests_SQLite.cpp; tests/unittests/PayloadDecoderTests.cpp Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 133d63e0-e402-4d40-bcc8-e801d9f5c438 --- .github/workflows/build-posix-latest.yml | 27 +++++++++---- docs/linux-setup-build.md | 25 ++++++++++++ lib/api/CorrelationVector.cpp | 39 ++++++------------- lib/api/capi.cpp | 8 ++++ lib/decoder/PayloadDecoder.cpp | 38 ++++++++---------- lib/include/public/ctmacros.hpp | 4 ++ lib/modules | 2 +- lib/offline/KillSwitchManager.hpp | 36 +++++++---------- lib/offline/OfflineStorage_SQLite.cpp | 18 +++------ lib/offline/SQLiteWrapper.hpp | 4 ++ lib/packager/Packager.cpp | 5 ++- lib/pal/InformationProviderImpl.cpp | 5 ++- lib/tpm/TransmitProfiles.cpp | 4 ++ tests/unittests/CorrelationVectorTests.cpp | 18 +++++++++ tests/unittests/KillSwitchManagerTests.cpp | 20 ++++++++++ .../unittests/OfflineStorageTests_SQLite.cpp | 8 +++- tests/unittests/PayloadDecoderTests.cpp | 23 +++++++++++ 17 files changed, 190 insertions(+), 94 deletions(-) diff --git a/.github/workflows/build-posix-latest.yml b/.github/workflows/build-posix-latest.yml index 95c387fd0..be08ed50b 100644 --- a/.github/workflows/build-posix-latest.yml +++ b/.github/workflows/build-posix-latest.yml @@ -61,13 +61,28 @@ jobs: run: bash tests/headers/check_public_headers.sh no-exceptions: - name: Linux Curl without C++ exceptions + name: Full Linux SDK without C++ exceptions (${{ matrix.compiler }}) runs-on: ubuntu-latest + strategy: + matrix: + compiler: [gcc, clang] + include: + - compiler: gcc + cc: gcc + cxx: g++ + - compiler: clang + 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 + with: + submodules: true + - name: Install build dependencies + 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 @@ -76,8 +91,6 @@ jobs: -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 mat --parallel 2 diff --git a/docs/linux-setup-build.md b/docs/linux-setup-build.md index 2e045fa59..95594c067 100644 --- a/docs/linux-setup-build.md +++ b/docs/linux-setup-build.md @@ -72,3 +72,28 @@ make ``` Package for your platform is going to be created and placed in ./out directory. + +### Building without C++ exceptions + +GCC and Clang can compile the complete SDK with `-fno-exceptions`: + +```console +cmake -S . -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 +cmake --build out-no-exceptions --target mat --parallel 2 +``` + +Select Clang with `CC=clang CXX=clang++` when configuring a fresh build +directory. CI initializes the submodules and builds the full `mat` target +with both compilers, rather than +compiling only the Curl transport object. 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. 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/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 6d86a7057..8ac306918 100644 --- a/lib/decoder/PayloadDecoder.cpp +++ b/lib/decoder/PayloadDecoder.cpp @@ -34,6 +34,7 @@ MAT_NS_END #include #include #include +#include #ifdef _WIN32 #include @@ -472,31 +473,26 @@ namespace clienttelemetry { // 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 = new (std::nothrow) char[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 diff --git a/lib/include/public/ctmacros.hpp b/lib/include/public/ctmacros.hpp index 026176a04..5cee64129 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 diff --git a/lib/modules b/lib/modules index 04409b653..ecf962d95 160000 --- a/lib/modules +++ b/lib/modules @@ -1 +1 @@ -Subproject commit 04409b6536da5ca9c79f82ea5f37959c43531c57 +Subproject commit ecf962d95fae9d23a61542017abdec8baa07e4cf 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_SQLite.cpp b/lib/offline/OfflineStorage_SQLite.cpp index 516629163..a1a8ef4b0 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 { @@ -671,20 +672,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..fc2b96aa2 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; } 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/tpm/TransmitProfiles.cpp b/lib/tpm/TransmitProfiles.cpp index b26766f6f..8c245b143 100644 --- a/lib/tpm/TransmitProfiles.cpp +++ b/lib/tpm/TransmitProfiles.cpp @@ -191,7 +191,9 @@ namespace MAT_NS_BEGIN { std::vector newProfiles; using nlohmann::json; +#if HAVE_EXCEPTIONS try +#endif { json temp = json::parse(profiles_json.c_str()); @@ -272,10 +274,12 @@ namespace MAT_NS_BEGIN { } } } +#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/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/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/OfflineStorageTests_SQLite.cpp b/tests/unittests/OfflineStorageTests_SQLite.cpp index 1d9ca9a7a..a344df75e 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); diff --git a/tests/unittests/PayloadDecoderTests.cpp b/tests/unittests/PayloadDecoderTests.cpp index 8e8051cee..fc40201fb 100644 --- a/tests/unittests/PayloadDecoderTests.cpp +++ b/tests/unittests/PayloadDecoderTests.cpp @@ -4,6 +4,17 @@ // #include "common/Common.hpp" #include "PayloadDecoder.hpp" +#include + +#if defined(HAVE_MAT_ZLIB) && defined(HAVE_MAT_JSONHPP) +namespace clienttelemetry { + namespace data { + namespace v3 { + bool Expand(const char* source, size_t sourceLen, char** dest, size_t& destLen, bool sizeAtZeroIndex); + } + } +} +#endif using namespace testing; using namespace MAT; @@ -84,3 +95,15 @@ TEST(PayloadDecoderTests, DecodeRecord_ValidUtf8_IsPreserved) << "Valid UTF-8 must not be altered"; } } + +#if defined(HAVE_MAT_ZLIB) && defined(HAVE_MAT_JSONHPP) +TEST(PayloadDecoderTests, Expand_AllocationFailureClearsOutput) +{ + 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); +} +#endif From d71a437136e17822a8332edd62be2226519e4ecd Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Thu, 1 Oct 2026 01:53:06 -0500 Subject: [PATCH 02/14] Keep public no-exceptions CI independent of private modules Remove submodule cloning from the public GCC/Clang build gate because the optional modules repository is private. Keep building the complete public mat target, document that boundary, pin the reviewed module schema fixes, and include their regression tests only when those modules are already available locally. Files changed: .github/workflows/build-posix-latest.yml; docs/linux-setup-build.md; tests/unittests/CMakeLists.txt; lib/modules Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 133d63e0-e402-4d40-bcc8-e801d9f5c438 --- .github/workflows/build-posix-latest.yml | 2 -- docs/linux-setup-build.md | 6 +++--- lib/modules | 2 +- tests/unittests/CMakeLists.txt | 5 +++++ 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-posix-latest.yml b/.github/workflows/build-posix-latest.yml index be08ed50b..41277968a 100644 --- a/.github/workflows/build-posix-latest.yml +++ b/.github/workflows/build-posix-latest.yml @@ -79,8 +79,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - with: - submodules: true - name: Install build dependencies run: sudo apt-get update && sudo apt-get install -y clang libcurl4-openssl-dev libsqlite3-dev zlib1g-dev - name: Configure diff --git a/docs/linux-setup-build.md b/docs/linux-setup-build.md index 95594c067..69f75718f 100644 --- a/docs/linux-setup-build.md +++ b/docs/linux-setup-build.md @@ -88,9 +88,9 @@ cmake --build out-no-exceptions --target mat --parallel 2 ``` Select Clang with `CC=clang CXX=clang++` when configuring a fresh build -directory. CI initializes the submodules and builds the full `mat` target -with both compilers, rather than -compiling only the Curl transport object. Exception handlers are retained in +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. diff --git a/lib/modules b/lib/modules index ecf962d95..b2718c982 160000 --- a/lib/modules +++ b/lib/modules @@ -1 +1 @@ -Subproject commit ecf962d95fae9d23a61542017abdec8baa07e4cf +Subproject commit b2718c9824d902f356b14a361ee5fe8a30f2a0c7 diff --git a/tests/unittests/CMakeLists.txt b/tests/unittests/CMakeLists.txt index bcdb0acb8..4543b433a 100644 --- a/tests/unittests/CMakeLists.txt +++ b/tests/unittests/CMakeLists.txt @@ -66,6 +66,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) From da4b09069e1a08b0e3c3beb706c66f8af75df7a3 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Thu, 1 Oct 2026 02:04:30 -0500 Subject: [PATCH 03/14] Reject unsafe profile JSON conversions without exceptions Address Copilot comment 4152647239: parse customer JSON with allow_exceptions=false, check member/container types before conversion, and bounds-check signed, unsigned, and floating timer values so malformed configuration cannot abort a no-exceptions host. Verified at lib/tpm/TransmitProfiles.cpp:198-309. Preserve existing permissive optional-field handling, ignored nonnumeric timers, valid fractional timer conversion, and valid-prefix loading; keep all legacy pinning tests. All 62 profile tests passed on VS 2026 and against full module-free GCC and Clang SDK builds with -fno-exceptions. Files changed: lib/tpm/TransmitProfiles.cpp; tests/unittests/TransmitProfilesTests.cpp Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 133d63e0-e402-4d40-bcc8-e801d9f5c438 --- lib/tpm/TransmitProfiles.cpp | 183 ++++++++++++++-------- tests/unittests/TransmitProfilesTests.cpp | 41 +++++ 2 files changed, 163 insertions(+), 61 deletions(-) diff --git a/lib/tpm/TransmitProfiles.cpp b/lib/tpm/TransmitProfiles.cpp index 8c245b143..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,14 +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) { @@ -206,71 +314,24 @@ 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)); } } } 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) { From 90dcde5446ae24ccc4f6e499bf5c032b62916b89 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Fri, 2 Oct 2026 17:28:49 -0500 Subject: [PATCH 04/14] Support exception-free native builds across platforms Keep Android JNI/Room and Apple wrappers buildable when native consumers disable C++ exceptions. Preserve enabled handlers and Room ID error reporting, detect MSVC unwind support, and gate native platforms in CI, including fix branches. Files changed: - .github/workflows/build-android.yml - .github/workflows/build-ios-mac.yml - .github/workflows/build-posix-latest.yml - .github/workflows/build-windows-vs2022.yaml - docs/linux-setup-build.md - lib/include/public/ctmacros.hpp - lib/jni/LogManager_jni.cpp - lib/offline/OfflineStorage_Room.cpp - tests/headers/check_public_headers.cmd - tests/headers/check_public_headers.sh - tests/headers/exception-macros.cpp - wrappers/obj-c/ODWDiagnosticDataViewer.mm - wrappers/obj-c/ODWLogManager.mm - wrappers/obj-c/ODWLogger.mm Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build-android.yml | 29 +++++ .github/workflows/build-ios-mac.yml | 30 +++++ .github/workflows/build-posix-latest.yml | 18 ++- .github/workflows/build-windows-vs2022.yaml | 36 ++++++ docs/linux-setup-build.md | 22 ++++ lib/include/public/ctmacros.hpp | 2 +- lib/jni/LogManager_jni.cpp | 36 ++++-- lib/offline/OfflineStorage_Room.cpp | 121 +++++++++++--------- tests/headers/check_public_headers.cmd | 21 ++++ tests/headers/check_public_headers.sh | 22 ++++ tests/headers/exception-macros.cpp | 40 +++++++ wrappers/obj-c/ODWDiagnosticDataViewer.mm | 12 +- wrappers/obj-c/ODWLogManager.mm | 28 +++-- wrappers/obj-c/ODWLogger.mm | 4 +- 14 files changed, 342 insertions(+), 79 deletions(-) create mode 100644 tests/headers/exception-macros.cpp diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index 96cb76c80..e29a6b246 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,34 @@ 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: + 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 + -DCMAKE_CXX_FLAGS=-fno-exceptions + -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..bdc60d790 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,35 @@ 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 + -DCMAKE_CXX_FLAGS=-fno-exceptions + -DCMAKE_XCODE_ATTRIBUTE_GCC_ENABLE_CPP_EXCEPTIONS=NO + -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 41277968a..28c31f577 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: @@ -61,16 +62,21 @@ jobs: run: bash tests/headers/check_public_headers.sh no-exceptions: - name: Full Linux SDK without C++ exceptions (${{ matrix.compiler }}) - runs-on: ubuntu-latest + name: Full SDK without C++ exceptions (${{ matrix.os }}, ${{ matrix.compiler }}) + runs-on: ${{ matrix.os }} strategy: matrix: - compiler: [gcc, clang] include: - - compiler: gcc + - os: ubuntu-latest + compiler: gcc cc: gcc cxx: g++ - - compiler: clang + - os: ubuntu-latest + compiler: clang + cc: clang + cxx: clang++ + - os: macos-latest + compiler: appleclang cc: clang cxx: clang++ env: @@ -80,12 +86,14 @@ jobs: - name: Checkout uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - 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 -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS=-fno-exceptions + -DMATSDK_BUILD_SWIFT_WRAPPER=OFF -DMATSDK_BUILD_UNIT_TESTS=OFF -DMATSDK_BUILD_FUNC_TESTS=OFF -DMATSDK_BUILD_TEST_TOOL=OFF diff --git a/.github/workflows/build-windows-vs2022.yaml b/.github/workflows/build-windows-vs2022.yaml index c6efdb426..394a1ec2c 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,41 @@ permissions: contents: read jobs: + no-exceptions: + name: Windows without C++ exceptions (${{ matrix.compiler }}, WinInet=${{ matrix.wininet }}) + runs-on: windows-2022 + strategy: + 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', '.', '-B', 'out-no-exceptions', '-A', 'x64', + '-DMATSDK_BUILD_UNIT_TESTS=OFF', + '-DMATSDK_BUILD_FUNC_TESTS=OFF', + '-DMATSDK_BUILD_TEST_TOOL=OFF', + '-DMATSDK_BUILD_PACKAGE=OFF', + '-DMATSDK_USE_WININET=${{ matrix.wininet }}' + ) + $flags = '/DWIN32 /D_WINDOWS /EHs-c- /D_HAS_EXCEPTIONS=0' + if ('${{ matrix.compiler }}' -eq 'clang-cl') { + $options += @('-T', 'ClangCL') + $flags += ' /clang:-fno-exceptions' + } + cmake @options "-DCMAKE_CXX_FLAGS=$flags" + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + - name: Compile full SDK + run: cmake --build out-no-exceptions --config Debug --target mat --parallel 2 + - 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/docs/linux-setup-build.md b/docs/linux-setup-build.md index 69f75718f..ea914e770 100644 --- a/docs/linux-setup-build.md +++ b/docs/linux-setup-build.md @@ -97,3 +97,25 @@ 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 flag 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`. + +For native Windows MSVC builds, configure a fresh directory with +`"-DCMAKE_CXX_FLAGS=/DWIN32 /D_WINDOWS /EHs-c- /D_HAS_EXCEPTIONS=0"`. +For clang-cl, append `/clang:-fno-exceptions` to those flags. 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/lib/include/public/ctmacros.hpp b/lib/include/public/ctmacros.hpp index 5cee64129..356e8f963 100644 --- a/lib/include/public/ctmacros.hpp +++ b/lib/include/public/ctmacros.hpp @@ -125,7 +125,7 @@ #endif /* Exceptions support is optional */ -#if (__cpp_exceptions) || defined(__EXCEPTIONS) +#if 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/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/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..3c7e8b251 100644 --- a/tests/headers/check_public_headers.sh +++ b/tests/headers/check_public_headers.sh @@ -116,14 +116,36 @@ 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 +} + 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 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/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 From 2feba2ea381911e9db4467c913f2dc2badd31a37 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Sun, 4 Oct 2026 19:14:29 -0500 Subject: [PATCH 05/14] Make loopback AI tests wait without starving the HTTP server Replace the request polling loop with notifications under the request mutex, a steady-clock deadline, and one-time counting of each batch. Preserve the five-second timeout and existing HTTP/error assertions; add a split-batch regression that rejects duplicate counting. Disable host network discovery for loopback tests after a VS 2026 stress run blocked in WinRT activation before HTTP initialization. Include AISendTests in CMake when Azure Monitor is enabled so both Windows and Linux exercise the same functional coverage. Files changed: tests/functests/AISendTests.cpp; tests/functests/CMakeLists.txt; docs/linux-setup-build.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/linux-setup-build.md | 5 ++ tests/functests/AISendTests.cpp | 87 ++++++++++++++++++++------------- tests/functests/CMakeLists.txt | 5 ++ 3 files changed, 64 insertions(+), 33 deletions(-) diff --git a/docs/linux-setup-build.md b/docs/linux-setup-build.md index ea914e770..1eb143334 100644 --- a/docs/linux-setup-build.md +++ b/docs/linux-setup-build.md @@ -73,6 +73,11 @@ 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 GCC and Clang can compile the complete SDK with `-fno-exceptions`: 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..2bb8c3b4e 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 From 86cdf51d3c2d9a35c59cf2f771a8e77bf0f900bc Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Mon, 5 Oct 2026 05:55:07 -0500 Subject: [PATCH 06/14] Respect C++ exception mode and physical cache limits across CI platforms Use Clang C++ feature detection so Objective-C exceptions cannot enable C++ try/catch in exception-free wrappers. Preserve dynamically resolved Windows API signatures without incompatible casts, and fully initialize the certificate-chain parameters for strict clang-cl builds. Check SQLite page allocation before opt-in trimming so page and index overhead cannot exceed the configured limit between throttled notifications. Add Objective-C++ macro coverage on Apple CI and a deterministic storage boundary regression without relaxing the existing functional assertion. Files changed: .github/workflows/build-posix-latest.yml; docs/Offline-storage-settings.md; docs/linux-setup-build.md; lib/http/HttpClient_WinHttp.cpp; lib/include/public/ctmacros.hpp; lib/offline/OfflineStorage_SQLite.cpp; lib/utils/Utils.cpp; tests/headers/check_public_headers.sh; tests/unittests/OfflineStorageTests_SQLite.cpp Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build-posix-latest.yml | 3 ++ docs/Offline-storage-settings.md | 7 +++- docs/linux-setup-build.md | 2 ++ lib/http/HttpClient_WinHttp.cpp | 3 +- lib/include/public/ctmacros.hpp | 9 ++++- lib/offline/OfflineStorage_SQLite.cpp | 6 ++++ lib/utils/Utils.cpp | 6 +++- tests/headers/check_public_headers.sh | 17 ++++++++++ .../unittests/OfflineStorageTests_SQLite.cpp | 34 +++++++++++++++++++ 9 files changed, 83 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-posix-latest.yml b/.github/workflows/build-posix-latest.yml index 28c31f577..59f0db297 100644 --- a/.github/workflows/build-posix-latest.yml +++ b/.github/workflows/build-posix-latest.yml @@ -100,3 +100,6 @@ jobs: - name: Compile full SDK run: >- cmake --build out-no-exceptions --target mat --parallel 2 + - name: Public headers and exception macro regression + if: runner.os == 'macOS' + run: bash tests/headers/check_public_headers.sh 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/linux-setup-build.md b/docs/linux-setup-build.md index 1eb143334..55e25c226 100644 --- a/docs/linux-setup-build.md +++ b/docs/linux-setup-build.md @@ -110,6 +110,8 @@ JNI and Objective-C wrappers. For Android, also pass 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. For native Windows MSVC builds, configure a fresh directory with `"-DCMAKE_CXX_FLAGS=/DWIN32 /D_WINDOWS /EHs-c- /D_HAS_EXCEPTIONS=0"`. diff --git a/lib/http/HttpClient_WinHttp.cpp b/lib/http/HttpClient_WinHttp.cpp index 9230f9277..5d346674b 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/include/public/ctmacros.hpp b/lib/include/public/ctmacros.hpp index 356e8f963..518e7007a 100644 --- a/lib/include/public/ctmacros.hpp +++ b/lib/include/public/ctmacros.hpp @@ -125,7 +125,14 @@ #endif /* Exceptions support is optional */ -#if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND) +#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/offline/OfflineStorage_SQLite.cpp b/lib/offline/OfflineStorage_SQLite.cpp index a1a8ef4b0..631fd6dd5 100644 --- a/lib/offline/OfflineStorage_SQLite.cpp +++ b/lib/offline/OfflineStorage_SQLite.cpp @@ -227,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(); diff --git a/lib/utils/Utils.cpp b/lib/utils/Utils.cpp index a1cf48ee7..1fd1191fa 100644 --- a/lib/utils/Utils.cpp +++ b/lib/utils/Utils.cpp @@ -10,6 +10,7 @@ #endif #include +#include #include #ifdef _WIN32 @@ -140,7 +141,10 @@ namespace MAT_NS_BEGIN { std::string GetTempDirectory() { #ifdef _WIN32 - auto lpGetTempPathW = reinterpret_cast(GetProcAddress(GetModuleHandle(TEXT("kernel32")), "GetTempPath2W")); + const auto proc = GetProcAddress(GetModuleHandle(TEXT("kernel32")), "GetTempPath2W"); + decltype(&::GetTempPathW) lpGetTempPathW = nullptr; + static_assert(sizeof(lpGetTempPathW) == sizeof(proc), "Windows function pointers must have the same size"); + std::memcpy(&lpGetTempPathW, &proc, sizeof(lpGetTempPathW)); if (lpGetTempPathW == NULL) { lpGetTempPathW = ::GetTempPathW; diff --git a/tests/headers/check_public_headers.sh b/tests/headers/check_public_headers.sh index 3c7e8b251..35ccdb008 100644 --- a/tests/headers/check_public_headers.sh +++ b/tests/headers/check_public_headers.sh @@ -132,6 +132,20 @@ run_exception_macros() { 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)" @@ -146,6 +160,9 @@ if command -v clang++ >/dev/null 2>&1; then 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/unittests/OfflineStorageTests_SQLite.cpp b/tests/unittests/OfflineStorageTests_SQLite.cpp index a344df75e..79f631c83 100644 --- a/tests/unittests/OfflineStorageTests_SQLite.cpp +++ b/tests/unittests/OfflineStorageTests_SQLite.cpp @@ -1135,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()) From 376ee1ceb620a777ecbb2b34dc20c74b56f52f50 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Mon, 5 Oct 2026 06:09:05 -0500 Subject: [PATCH 07/14] Preserve Windows API signatures without incompatible procedure casts Clang 19 in CI diagnoses the precise-clock loader after the temporary-directory cast is fixed. Use a shared typed procedure loader for every SDK Windows API lookup so size-checked pointer copies retain each export signature without suppressing the diagnostic. Preserve missing-export fallbacks and correct the packaged-app API calling convention on 32-bit Windows. Add regression coverage that calls both DWORD-returning and void-returning exports and verifies a missing export remains null. Validate both HTTP transports with the cast diagnostic explicitly enabled, since newer local Clang does not enable it by default. Files changed: lib/utils/WindowsUtils.hpp; lib/utils/Utils.cpp; lib/pal/PAL.cpp; lib/pal/desktop/WindowsDesktopSystemInformationImpl.cpp; tests/unittests/UtilsTests.cpp Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/pal/PAL.cpp | 5 +-- .../WindowsDesktopSystemInformationImpl.cpp | 6 ++-- lib/utils/Utils.cpp | 11 +++---- lib/utils/WindowsUtils.hpp | 32 +++++++++++++++++++ tests/unittests/UtilsTests.cpp | 22 +++++++++++++ 5 files changed, 64 insertions(+), 12 deletions(-) create mode 100644 lib/utils/WindowsUtils.hpp diff --git a/lib/pal/PAL.cpp b/lib/pal/PAL.cpp index c9d750232..a7f9561d5 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/desktop/WindowsDesktopSystemInformationImpl.cpp b/lib/pal/desktop/WindowsDesktopSystemInformationImpl.cpp index 8bc6b1147..f8bca9d61 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 @@ -206,7 +207,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))) @@ -277,7 +278,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) { @@ -315,4 +316,3 @@ namespace PAL_NS_BEGIN { } } PAL_NS_END - diff --git a/lib/utils/Utils.cpp b/lib/utils/Utils.cpp index 1fd1191fa..33b0fb221 100644 --- a/lib/utils/Utils.cpp +++ b/lib/utils/Utils.cpp @@ -10,11 +10,11 @@ #endif #include -#include #include #ifdef _WIN32 #include +#include "WindowsUtils.hpp" #ifdef _WINRT_DLL // Win 10-specific APIs @@ -78,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; @@ -141,10 +141,7 @@ namespace MAT_NS_BEGIN { std::string GetTempDirectory() { #ifdef _WIN32 - const auto proc = GetProcAddress(GetModuleHandle(TEXT("kernel32")), "GetTempPath2W"); - decltype(&::GetTempPathW) lpGetTempPathW = nullptr; - static_assert(sizeof(lpGetTempPathW) == sizeof(proc), "Windows function pointers must have the same size"); - std::memcpy(&lpGetTempPathW, &proc, sizeof(lpGetTempPathW)); + 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/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 From a546a9b8551d66c49e44c95ecf0fa10c73cc83df Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Mon, 5 Oct 2026 18:21:40 -0500 Subject: [PATCH 08/14] Track merged module fixes from master Update the module pin to the latest master HEAD so the no-exceptions SDK PR uses the merged companion module changes rather than the development branch snapshot. Files changed: lib/modules Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/modules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules b/lib/modules index b2718c982..95d333220 160000 --- a/lib/modules +++ b/lib/modules @@ -1 +1 @@ -Subproject commit b2718c9824d902f356b14a361ee5fe8a30f2a0c7 +Subproject commit 95d33322081ebf0023293ddb0566349482e4e2d7 From bff54fb08f89210e4069b996c35d2a6a37b07f90 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Tue, 6 Oct 2026 03:57:54 -0500 Subject: [PATCH 09/14] Address review blockers so no-exceptions builds exercise runtime failures 4191920933: resolve both IP Helper functions through GetWindowsProcAddress instead of incompatible FARPROC casts (lib/pal/desktop/NetworkDetector.cpp). 4191920942: disable Android fail-fast so each ABI and Room combination runs. 4191920945: link a standalone runtime smoke test against the full exception-disabled SDK and run it on POSIX and all four Windows compiler/transport entries. 4191920951: document the existing valid-prefix profile-loading contract without breaking its pinning tests. Also address the review summary: guard null/non-object FromJSON input and disable throwing JSON parse errors only in no-exceptions builds; validate collector response objects, efi types, and numeric counts before conversion. Reject unrepresentable expansion lengths before allocation so the SIZE_MAX regression does not enter an instrumented allocator. Files changed: .github/workflows/build-android.yml; .github/workflows/build-posix-latest.yml; .github/workflows/build-windows-vs2022.yaml; lib/api/LogConfiguration.cpp; lib/decoder/PayloadDecoder.cpp; lib/http/HttpResponseDecoder.cpp; lib/include/public/LogConfiguration.hpp; lib/include/public/TransmitProfiles.hpp; lib/pal/desktop/NetworkDetector.cpp; tests/no-exceptions/CMakeLists.txt; tests/no-exceptions/README.md; tests/no-exceptions/no-exceptions-smoke.cpp; tests/unittests/CMakeLists.txt; tests/unittests/HttpResponseDecoderTests.cpp; tests/unittests/LogConfigurationTests.cpp; tests/unittests/PayloadDecoderTests.cpp; tests/unittests/UnitTests.vcxproj. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f81706eb-e23d-4739-a41b-8851e2c3653a --- .github/workflows/build-android.yml | 1 + .github/workflows/build-posix-latest.yml | 7 +- .github/workflows/build-windows-vs2022.yaml | 7 +- lib/api/LogConfiguration.cpp | 13 +- lib/decoder/PayloadDecoder.cpp | 8 + lib/http/HttpResponseDecoder.cpp | 44 ++++-- lib/include/public/LogConfiguration.hpp | 4 +- lib/include/public/TransmitProfiles.hpp | 9 +- lib/pal/desktop/NetworkDetector.cpp | 9 +- tests/no-exceptions/CMakeLists.txt | 21 +++ tests/no-exceptions/README.md | 32 ++++ tests/no-exceptions/no-exceptions-smoke.cpp | 154 +++++++++++++++++++ tests/unittests/CMakeLists.txt | 1 + tests/unittests/HttpResponseDecoderTests.cpp | 23 +++ tests/unittests/LogConfigurationTests.cpp | 44 ++++++ tests/unittests/PayloadDecoderTests.cpp | 2 +- tests/unittests/UnitTests.vcxproj | 1 + 17 files changed, 352 insertions(+), 28 deletions(-) create mode 100644 tests/no-exceptions/CMakeLists.txt create mode 100644 tests/no-exceptions/README.md create mode 100644 tests/no-exceptions/no-exceptions-smoke.cpp create mode 100644 tests/unittests/LogConfigurationTests.cpp diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index e584923bd..b7e75c317 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -35,6 +35,7 @@ jobs: 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] diff --git a/.github/workflows/build-posix-latest.yml b/.github/workflows/build-posix-latest.yml index 0a4e3b594..867fd6462 100644 --- a/.github/workflows/build-posix-latest.yml +++ b/.github/workflows/build-posix-latest.yml @@ -96,6 +96,7 @@ jobs: name: Full SDK without C++ exceptions (${{ matrix.os }}, ${{ matrix.compiler }}) runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: include: - os: ubuntu-latest @@ -121,7 +122,7 @@ jobs: 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_SWIFT_WRAPPER=OFF @@ -130,7 +131,9 @@ jobs: -DMATSDK_BUILD_TEST_TOOL=OFF - name: Compile full SDK run: >- - cmake --build out-no-exceptions --target mat --parallel 2 + 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 394a1ec2c..752110369 100644 --- a/.github/workflows/build-windows-vs2022.yaml +++ b/.github/workflows/build-windows-vs2022.yaml @@ -25,6 +25,7 @@ jobs: 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] @@ -35,7 +36,7 @@ jobs: shell: pwsh run: | $options = @( - '-S', '.', '-B', 'out-no-exceptions', '-A', 'x64', + '-S', 'tests/no-exceptions', '-B', 'out-no-exceptions', '-A', 'x64', '-DMATSDK_BUILD_UNIT_TESTS=OFF', '-DMATSDK_BUILD_FUNC_TESTS=OFF', '-DMATSDK_BUILD_TEST_TOOL=OFF', @@ -50,7 +51,9 @@ jobs: cmake @options "-DCMAKE_CXX_FLAGS=$flags" if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - name: Compile full SDK - run: cmake --build out-no-exceptions --config Debug --target mat --parallel 2 + 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 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/decoder/PayloadDecoder.cpp b/lib/decoder/PayloadDecoder.cpp index 1a8a06d65..0268f9dfb 100644 --- a/lib/decoder/PayloadDecoder.cpp +++ b/lib/decoder/PayloadDecoder.cpp @@ -34,6 +34,7 @@ MAT_NS_END #include #include #include +#include #include #ifdef _WIN32 @@ -468,6 +469,13 @@ 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) { 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/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/tests/no-exceptions/CMakeLists.txt b/tests/no-exceptions/CMakeLists.txt new file mode 100644 index 000000000..9aa575c8c --- /dev/null +++ b/tests/no-exceptions/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.15...3.31) +project(MatsdkNoExceptionsSmoke LANGUAGES C CXX) + +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..e27bf4b51 --- /dev/null +++ b/tests/no-exceptions/README.md @@ -0,0 +1,32 @@ +# 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, and unrepresentable expansion sizes. +It requires neither Google Test nor the private optional modules. + +For GCC or Clang: + +```sh +cmake -S tests/no-exceptions -B out-no-exceptions \ + -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS=-fno-exceptions +cmake --build out-no-exceptions --target no-exceptions-smoke --parallel 2 +ctest --test-dir out-no-exceptions --output-on-failure +``` + +For MSVC, configure with `/EHs-c- /D_HAS_EXCEPTIONS=0` in `CMAKE_CXX_FLAGS`. +For clang-cl, add `/clang:-fno-exceptions` and 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. + +`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..1115d47ed --- /dev/null +++ b/tests/no-exceptions/no-exceptions-smoke.cpp @@ -0,0 +1,154 @@ +// +// 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); + } + } +} + +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"); + 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 18f78c4be..fa3e5d190 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 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/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/PayloadDecoderTests.cpp b/tests/unittests/PayloadDecoderTests.cpp index 91055c567..f9e54ce80 100644 --- a/tests/unittests/PayloadDecoderTests.cpp +++ b/tests/unittests/PayloadDecoderTests.cpp @@ -145,7 +145,7 @@ TEST(PayloadDecoderTests, DecodeRecord_ValidUtf8_IsPreserved) } #if defined(HAVE_MAT_ZLIB) && defined(HAVE_MAT_JSONHPP) -TEST(PayloadDecoderTests, Expand_AllocationFailureClearsOutput) +TEST(PayloadDecoderTests, Expand_UnrepresentableSizeClearsOutput) { const char source[] = "invalid"; char* output = nullptr; 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 @@ + From 1503a1004a27da3c514d18e58c93a983e8efecc2 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Tue, 6 Oct 2026 04:59:55 -0500 Subject: [PATCH 10/14] Test real expansion allocation failures instead of size-guard rejection Address Copilot comment 4193506718. Keep the normal Expand entry point and std::nothrow allocation unchanged, but route it through an internal overload with a per-call allocator. Inject failure for a representable 32-byte request and assert the allocator ran, false was returned, a previously non-null output was cleared, and the length became zero. Cover exception-enabled unit tests and exception-disabled runtime smoke tests without process-global injection controls. Files changed: lib/decoder/PayloadDecoder.cpp; tests/unittests/PayloadDecoderTests.cpp; tests/no-exceptions/no-exceptions-smoke.cpp; tests/no-exceptions/README.md. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f81706eb-e23d-4739-a41b-8851e2c3653a --- lib/decoder/PayloadDecoder.cpp | 11 +++++++++-- tests/no-exceptions/README.md | 8 +++++++- tests/no-exceptions/no-exceptions-smoke.cpp | 12 ++++++++++++ tests/unittests/PayloadDecoderTests.cpp | 20 ++++++++++++++++++++ 4 files changed, 48 insertions(+), 3 deletions(-) diff --git a/lib/decoder/PayloadDecoder.cpp b/lib/decoder/PayloadDecoder.cpp index 0268f9dfb..25a0231ce 100644 --- a/lib/decoder/PayloadDecoder.cpp +++ b/lib/decoder/PayloadDecoder.cpp @@ -444,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)) { @@ -479,7 +481,7 @@ namespace clienttelemetry { // Allocate memory for the new uncompressed buffer if (destLen > 0) { - char* decompBody = new (std::nothrow) char[destLen]; + char* decompBody = allocate(destLen); if (decompBody == nullptr) { TEST_LOG_ERROR("Decompression failed (out of memory): destLen=%zu", destLen); @@ -506,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/tests/no-exceptions/README.md b/tests/no-exceptions/README.md index e27bf4b51..9d287fb71 100644 --- a/tests/no-exceptions/README.md +++ b/tests/no-exceptions/README.md @@ -3,7 +3,8 @@ 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, and unrepresentable expansion sizes. +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 or Clang: @@ -20,6 +21,11 @@ For clang-cl, add `/clang:-fno-exceptions` and 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 diff --git a/tests/no-exceptions/no-exceptions-smoke.cpp b/tests/no-exceptions/no-exceptions-smoke.cpp index 1115d47ed..d85307af3 100644 --- a/tests/no-exceptions/no-exceptions-smoke.cpp +++ b/tests/no-exceptions/no-exceptions-smoke.cpp @@ -23,6 +23,7 @@ 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)); } } } @@ -149,6 +150,17 @@ int main() 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/PayloadDecoderTests.cpp b/tests/unittests/PayloadDecoderTests.cpp index f9e54ce80..6a3d8f6a2 100644 --- a/tests/unittests/PayloadDecoderTests.cpp +++ b/tests/unittests/PayloadDecoderTests.cpp @@ -16,6 +16,8 @@ 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)); } } } @@ -154,4 +156,22 @@ TEST(PayloadDecoderTests, Expand_UnrepresentableSizeClearsOutput) 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 From 4c2f0065e9d1ab361d6a9fcd9c54202506686a31 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Tue, 6 Oct 2026 13:06:48 -0500 Subject: [PATCH 11/14] Expose native build policies so vcpkg consumers can select SDK behavior Add opt-in exception-free, diagnostic-free, and Android C API HTTP callback features while preserving package defaults and private compiler policy. Exercise real package builds and remove diagnostic-only code that breaks strict logging-disabled builds. Files changed: - .github/workflows/build-posix-latest.yml - .github/workflows/test-vcpkg.yml - CMakeLists.txt - cmake/MatsdkOptions.cmake - docs/building-with-vcpkg.md - docs/linux-setup-build.md - lib/http/HttpClient_WinInet.cpp - lib/offline/SQLiteWrapper.hpp - lib/pal/WorkerThread.cpp - lib/stats/MetaStats.hpp - tests/no-exceptions/README.md - tests/vcpkg/CMakeLists.txt - tests/vcpkg/test-release-port.py - tests/vcpkg/test-vcpkg-android.sh - tests/vcpkg/test-vcpkg-linux.sh - tests/vcpkg/test-vcpkg-windows.ps1 - tests/vcpkg/vcpkg.json - tools/ports/cpp-client-telemetry/portfile.cmake - tools/ports/cpp-client-telemetry/vcpkg.json - tests/vcpkg/native-feature-tests.cmake Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ae9709f0-338e-457b-9554-6be83625f8f3 --- .github/workflows/build-posix-latest.yml | 2 +- .github/workflows/test-vcpkg.yml | 12 +- CMakeLists.txt | 16 ++ cmake/MatsdkOptions.cmake | 2 + docs/building-with-vcpkg.md | 25 ++++ docs/linux-setup-build.md | 14 +- lib/http/HttpClient_WinInet.cpp | 1 + lib/offline/SQLiteWrapper.hpp | 4 +- lib/pal/WorkerThread.cpp | 4 + lib/stats/MetaStats.hpp | 3 +- tests/no-exceptions/README.md | 7 +- tests/vcpkg/CMakeLists.txt | 7 + tests/vcpkg/native-feature-tests.cmake | 137 ++++++++++++++++++ tests/vcpkg/test-release-port.py | 8 + tests/vcpkg/test-vcpkg-android.sh | 27 ++-- tests/vcpkg/test-vcpkg-linux.sh | 27 ++-- tests/vcpkg/test-vcpkg-windows.ps1 | 13 +- tests/vcpkg/vcpkg.json | 38 +++++ .../ports/cpp-client-telemetry/portfile.cmake | 20 +++ tools/ports/cpp-client-telemetry/vcpkg.json | 10 ++ 20 files changed, 335 insertions(+), 42 deletions(-) create mode 100644 tests/vcpkg/native-feature-tests.cmake diff --git a/.github/workflows/build-posix-latest.yml b/.github/workflows/build-posix-latest.yml index 867fd6462..63b22f169 100644 --- a/.github/workflows/build-posix-latest.yml +++ b/.github/workflows/build-posix-latest.yml @@ -124,7 +124,7 @@ jobs: run: >- cmake -S tests/no-exceptions -B out-no-exceptions -DCMAKE_BUILD_TYPE=Debug - -DCMAKE_CXX_FLAGS=-fno-exceptions + -DMATSDK_DISABLE_EXCEPTIONS=ON -DMATSDK_BUILD_SWIFT_WRAPPER=OFF -DMATSDK_BUILD_UNIT_TESTS=OFF -DMATSDK_BUILD_FUNC_TESTS=OFF 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..9886f29e4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -95,6 +95,22 @@ else() 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() + include(tools/Utils.cmake) include(GNUInstallDirs) include(CMakePackageConfigHelpers) 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/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 55e25c226..4e89f0eb3 100644 --- a/docs/linux-setup-build.md +++ b/docs/linux-setup-build.md @@ -80,12 +80,13 @@ network discovery. ### Building without C++ exceptions -GCC and Clang can compile the complete SDK with `-fno-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 \ - -DCMAKE_CXX_FLAGS=-fno-exceptions \ + -DMATSDK_DISABLE_EXCEPTIONS=ON \ -DMATSDK_BUILD_UNIT_TESTS=OFF \ -DMATSDK_BUILD_FUNC_TESTS=OFF \ -DMATSDK_BUILD_TEST_TOOL=OFF @@ -103,7 +104,7 @@ 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 flag applies to Android NDK and Apple Clang builds, including the +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, @@ -113,9 +114,10 @@ With the Xcode generator, also set Objective-C exception handling can remain enabled independently; the SDK detects C++ exception support separately, including in Objective-C++ translation units. -For native Windows MSVC builds, configure a fresh directory with -`"-DCMAKE_CXX_FLAGS=/DWIN32 /D_WINDOWS /EHs-c- /D_HAS_EXCEPTIONS=0"`. -For clang-cl, append `/clang:-fno-exceptions` to those flags. CI covers both +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 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/offline/SQLiteWrapper.hpp b/lib/offline/SQLiteWrapper.hpp index fc2b96aa2..c8305602d 100644 --- a/lib/offline/SQLiteWrapper.hpp +++ b/lib/offline/SQLiteWrapper.hpp @@ -557,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; @@ -571,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/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/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/tests/no-exceptions/README.md b/tests/no-exceptions/README.md index 9d287fb71..4ae6ad875 100644 --- a/tests/no-exceptions/README.md +++ b/tests/no-exceptions/README.md @@ -7,17 +7,16 @@ 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 or Clang: +For GCC, Clang, Apple Clang, MSVC, or clang-cl: ```sh cmake -S tests/no-exceptions -B out-no-exceptions \ - -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS=-fno-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 MSVC, configure with `/EHs-c- /D_HAS_EXCEPTIONS=0` in `CMAKE_CXX_FLAGS`. -For clang-cl, add `/clang:-fno-exceptions` and select the `ClangCL` toolset. +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. 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..4b6187169 --- /dev/null +++ b/tests/vcpkg/native-feature-tests.cmake @@ -0,0 +1,137 @@ +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(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}") + check_feature_mapping(ANDROID static device-id system-sqlite "${UNSUPPORTED_FEATURE}") + 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) + 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(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..6e6b7bcbd 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. diff --git a/tests/vcpkg/test-vcpkg-windows.ps1 b/tests/vcpkg/test-vcpkg-windows.ps1 index 759834413..ef243a5a0 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) 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..63142a1f1 100644 --- a/tools/ports/cpp-client-telemetry/portfile.cmake +++ b/tools/ports/cpp-client-telemetry/portfile.cmake @@ -166,10 +166,30 @@ 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() + vcpkg_cmake_configure( SOURCE_PATH "${SOURCE_PATH}" OPTIONS ${MATSDK_PINNED_SOURCE_OPTIONS} + ${MATSDK_NATIVE_FEATURE_OPTIONS} -DMATSDK_SQLITE_PROVIDER=${MATSDK_VCPKG_SQLITE_PROVIDER} -DBUILD_SHARED_LIBS=${MATSDK_VCPKG_BUILD_SHARED_LIBS} -DMATSDK_ANDROID_HTTP_CLIENT=${MATSDK_ANDROID_HTTP_CLIENT} 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": [ From 9c93080ef3cc39aedf2f538d07f239e7b7258809 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Tue, 6 Oct 2026 13:18:42 -0500 Subject: [PATCH 12/14] Consolidate port option handling to avoid redundant compatibility flags Read SDK option sources once and derive legacy build aliases from the canonical recipe only when the source needs them. Preserve native feature behavior and cover modern, legacy, and partially migrated sources. Files changed: - tools/ports/cpp-client-telemetry/portfile.cmake - tests/vcpkg/native-feature-tests.cmake Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ae9709f0-338e-457b-9554-6be83625f8f3 --- tests/vcpkg/native-feature-tests.cmake | 36 ++++++++++ .../ports/cpp-client-telemetry/portfile.cmake | 67 +++++++++---------- 2 files changed, 66 insertions(+), 37 deletions(-) diff --git a/tests/vcpkg/native-feature-tests.cmake b/tests/vcpkg/native-feature-tests.cmake index 4b6187169..14d0fb751 100644 --- a/tests/vcpkg/native-feature-tests.cmake +++ b/tests/vcpkg/native-feature-tests.cmake @@ -10,6 +10,20 @@ function(vcpkg_cmake_configure) message(FATAL_ERROR "Missing ${option}: ${CONFIG_OPTIONS}") endif() endforeach() + set(LEGACY_OPTIONS) + foreach(option IN LISTS CONFIG_OPTIONS) + 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() + foreach(option IN LISTS EXPECTED_COMPATIBILITY_OPTIONS) + if(NOT "${option}" IN_LIST CONFIG_OPTIONS) + message(FATAL_ERROR "Missing compatibility option ${option}") + endif() + endforeach() set(CONFIGURED ON PARENT_SCOPE) endfunction() @@ -133,5 +147,27 @@ foreach(feature no-exceptions no-logging android-capi-http-client) message(FATAL_ERROR "Unsupported ${feature} must fail explicitly: ${ERROR}") endif() endforeach() + +file(APPEND "${OLD_SOURCE}/CMakeLists.txt" + "# MATSDK_USE_VCPKG_DEPS MATSDK_MINIMAL_SQLITE\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) +set(EXPECTED_COMPATIBILITY_OPTIONS + -DMATSDK_USE_VCPKG_DEPS=ON -DMATSDK_MINIMAL_SQLITE=ON) +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) +set(EXPECTED_COMPATIBILITY_OPTIONS -DMATSDK_USE_VCPKG_DEPS=ON) +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/tools/ports/cpp-client-telemetry/portfile.cmake b/tools/ports/cpp-client-telemetry/portfile.cmake index 63142a1f1..5296d6154 100644 --- a/tools/ports/cpp-client-telemetry/portfile.cmake +++ b/tools/ports/cpp-client-telemetry/portfile.cmake @@ -32,6 +32,13 @@ if(NOT DEFINED SOURCE_PATH) ) 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() + # 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 +55,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 " @@ -124,12 +123,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 @@ -185,38 +178,38 @@ foreach(_matsdk_feature_option IN ITEMS 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_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} ) From e563304ee6fec4cdda220a382bda6271ebbf7936 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Tue, 6 Oct 2026 13:52:07 -0500 Subject: [PATCH 13/14] Remove recent dependency shims in favor of provider selection Use automatic vcpkg detection and MATSDK_SQLITE_PROVIDER instead of the obsolete dependency toggles. Reject minimal SQLite requests against SDK sources without the provider option rather than silently losing the selected behavior. Retain the older BUILD_* compatibility names. Files changed: - tools/ports/cpp-client-telemetry/portfile.cmake - tests/vcpkg/native-feature-tests.cmake Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ae9709f0-338e-457b-9554-6be83625f8f3 --- tests/vcpkg/native-feature-tests.cmake | 21 +++++++++---------- .../ports/cpp-client-telemetry/portfile.cmake | 20 +++++++----------- 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/tests/vcpkg/native-feature-tests.cmake b/tests/vcpkg/native-feature-tests.cmake index 14d0fb751..85892ac2c 100644 --- a/tests/vcpkg/native-feature-tests.cmake +++ b/tests/vcpkg/native-feature-tests.cmake @@ -12,6 +12,9 @@ function(vcpkg_cmake_configure) 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() @@ -19,11 +22,6 @@ function(vcpkg_cmake_configure) if(NOT "${LEGACY_OPTIONS}" STREQUAL "${EXPECTED_LEGACY_OPTIONS}") message(FATAL_ERROR "Incorrect legacy build options: ${LEGACY_OPTIONS}") endif() - foreach(option IN LISTS EXPECTED_COMPATIBILITY_OPTIONS) - if(NOT "${option}" IN_LIST CONFIG_OPTIONS) - message(FATAL_ERROR "Missing compatibility option ${option}") - endif() - endforeach() set(CONFIGURED ON PARENT_SCOPE) endfunction() @@ -94,7 +92,11 @@ endfunction() if(DEFINED UNSUPPORTED_FEATURE) set(ENV{MATSDK_VCPKG_SOURCE_DIR} "${OLD_SOURCE}") - check_feature_mapping(ANDROID static device-id system-sqlite "${UNSUPPORTED_FEATURE}") + 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() @@ -138,7 +140,7 @@ 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) +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}" @@ -149,15 +151,13 @@ foreach(feature no-exceptions no-logging android-capi-http-client) endforeach() file(APPEND "${OLD_SOURCE}/CMakeLists.txt" - "# MATSDK_USE_VCPKG_DEPS MATSDK_MINIMAL_SQLITE\n") + "# 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) -set(EXPECTED_COMPATIBILITY_OPTIONS - -DMATSDK_USE_VCPKG_DEPS=ON -DMATSDK_MINIMAL_SQLITE=ON) 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) @@ -167,7 +167,6 @@ 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) -set(EXPECTED_COMPATIBILITY_OPTIONS -DMATSDK_USE_VCPKG_DEPS=ON) 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/tools/ports/cpp-client-telemetry/portfile.cmake b/tools/ports/cpp-client-telemetry/portfile.cmake index 5296d6154..7c36c8f5d 100644 --- a/tools/ports/cpp-client-telemetry/portfile.cmake +++ b/tools/ports/cpp-client-telemetry/portfile.cmake @@ -32,8 +32,7 @@ if(NOT DEFINED SOURCE_PATH) ) endif() -file(READ "${SOURCE_PATH}/CMakeLists.txt" MATSDK_ROOT_CMAKE) -set(MATSDK_OPTION_SOURCE "${MATSDK_ROOT_CMAKE}") +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}") @@ -114,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() @@ -131,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 @@ -146,13 +149,6 @@ 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) @@ -201,7 +197,7 @@ 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} From f2783f3aeef399a0cddd11db67cd753e6755d920 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Tue, 6 Oct 2026 14:52:42 -0500 Subject: [PATCH 14/14] Centralize build policy and make test failures visible Exercise the supported exception option consistently in CI without changing dependency compiler policy. Use CTest registration for native consumers, restore C API coverage, and propagate build and concurrent-process failures instead of accepting stale or incomplete runs. Add deterministic runner contracts to Ubuntu and macOS CI. Files changed: - .github/workflows/build-android.yml - .github/workflows/build-ios-mac.yml - .github/workflows/build-posix-latest.yml - .github/workflows/build-windows-vs2022.yaml - .github/workflows/test-embedding.yml - CMakeLists.txt - build-tests.sh - cmake/MatsdkCompilerOptions.cmake - docs/maintainer-onboarding.md - tests/build-runner-tests.py - tests/embedding/CMakeLists.txt - tests/functests/CMakeLists.txt - tests/no-exceptions/CMakeLists.txt - tests/unittests/CMakeLists.txt - tests/vcpkg/test-vcpkg-linux.sh - tests/vcpkg/test-vcpkg-macos.sh - tests/vcpkg/test-vcpkg-windows.ps1 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ae9709f0-338e-457b-9554-6be83625f8f3 --- .github/workflows/build-android.yml | 2 +- .github/workflows/build-ios-mac.yml | 3 +- .github/workflows/build-posix-latest.yml | 16 ++- .github/workflows/build-windows-vs2022.yaml | 11 +- .github/workflows/test-embedding.yml | 8 +- CMakeLists.txt | 53 +------- build-tests.sh | 22 ++-- cmake/MatsdkCompilerOptions.cmake | 50 ++++++++ docs/maintainer-onboarding.md | 6 +- tests/build-runner-tests.py | 128 ++++++++++++++++++++ tests/embedding/CMakeLists.txt | 3 + tests/functests/CMakeLists.txt | 4 +- tests/no-exceptions/CMakeLists.txt | 2 + tests/unittests/CMakeLists.txt | 4 +- tests/vcpkg/test-vcpkg-linux.sh | 2 +- tests/vcpkg/test-vcpkg-macos.sh | 2 +- tests/vcpkg/test-vcpkg-windows.ps1 | 8 +- 17 files changed, 230 insertions(+), 94 deletions(-) create mode 100644 cmake/MatsdkCompilerOptions.cmake create mode 100644 tests/build-runner-tests.py diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index b7e75c317..3bc84c4b1 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -49,7 +49,7 @@ jobs: -DANDROID_ABI=${{ matrix.abi }} -DANDROID_PLATFORM=android-24 -DCMAKE_BUILD_TYPE=Debug - -DCMAKE_CXX_FLAGS=-fno-exceptions + -DMATSDK_DISABLE_EXCEPTIONS=ON -DMATSDK_BUILD_JNI_WRAPPER=ON -DMATSDK_ANDROID_USE_ROOM=${{ matrix.room }} -DMATSDK_DISABLE_LOGGING=${{ matrix.room }} diff --git a/.github/workflows/build-ios-mac.yml b/.github/workflows/build-ios-mac.yml index bdc60d790..773c04748 100644 --- a/.github/workflows/build-ios-mac.yml +++ b/.github/workflows/build-ios-mac.yml @@ -48,8 +48,7 @@ jobs: -DCMAKE_OSX_SYSROOT=${{ matrix.sdk }} -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=13.0 - -DCMAKE_CXX_FLAGS=-fno-exceptions - -DCMAKE_XCODE_ATTRIBUTE_GCC_ENABLE_CPP_EXCEPTIONS=NO + -DMATSDK_DISABLE_EXCEPTIONS=ON -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED=NO -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED=NO -DMATSDK_BUILD_OBJC_WRAPPER=ON diff --git a/.github/workflows/build-posix-latest.yml b/.github/workflows/build-posix-latest.yml index 63b22f169..2ed2e41a8 100644 --- a/.github/workflows/build-posix-latest.yml +++ b/.github/workflows/build-posix-latest.yml @@ -32,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 }} @@ -124,11 +135,6 @@ jobs: run: >- cmake -S tests/no-exceptions -B out-no-exceptions -DCMAKE_BUILD_TYPE=Debug - -DMATSDK_DISABLE_EXCEPTIONS=ON - -DMATSDK_BUILD_SWIFT_WRAPPER=OFF - -DMATSDK_BUILD_UNIT_TESTS=OFF - -DMATSDK_BUILD_FUNC_TESTS=OFF - -DMATSDK_BUILD_TEST_TOOL=OFF - name: Compile full SDK run: >- cmake --build out-no-exceptions --target no-exceptions-smoke --parallel 2 diff --git a/.github/workflows/build-windows-vs2022.yaml b/.github/workflows/build-windows-vs2022.yaml index 752110369..168393173 100644 --- a/.github/workflows/build-windows-vs2022.yaml +++ b/.github/workflows/build-windows-vs2022.yaml @@ -36,19 +36,14 @@ jobs: shell: pwsh run: | $options = @( - '-S', 'tests/no-exceptions', '-B', 'out-no-exceptions', '-A', 'x64', - '-DMATSDK_BUILD_UNIT_TESTS=OFF', - '-DMATSDK_BUILD_FUNC_TESTS=OFF', - '-DMATSDK_BUILD_TEST_TOOL=OFF', - '-DMATSDK_BUILD_PACKAGE=OFF', + '-S', 'tests/no-exceptions', '-B', 'out-no-exceptions', + '-G', 'Visual Studio 17 2022', '-A', 'x64', '-DMATSDK_USE_WININET=${{ matrix.wininet }}' ) - $flags = '/DWIN32 /D_WINDOWS /EHs-c- /D_HAS_EXCEPTIONS=0' if ('${{ matrix.compiler }}' -eq 'clang-cl') { $options += @('-T', 'ClangCL') - $flags += ' /clang:-fno-exceptions' } - cmake @options "-DCMAKE_CXX_FLAGS=$flags" + 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 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/CMakeLists.txt b/CMakeLists.txt index 9886f29e4..d47c0167a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,58 +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() - -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() +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/docs/maintainer-onboarding.md b/docs/maintainer-onboarding.md index d805b4166..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 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/CMakeLists.txt b/tests/functests/CMakeLists.txt index 2bb8c3b4e..058a5a1e4 100644 --- a/tests/functests/CMakeLists.txt +++ b/tests/functests/CMakeLists.txt @@ -76,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/no-exceptions/CMakeLists.txt b/tests/no-exceptions/CMakeLists.txt index 9aa575c8c..a261eb762 100644 --- a/tests/no-exceptions/CMakeLists.txt +++ b/tests/no-exceptions/CMakeLists.txt @@ -1,6 +1,8 @@ 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) diff --git a/tests/unittests/CMakeLists.txt b/tests/unittests/CMakeLists.txt index fa3e5d190..84a73b4ce 100644 --- a/tests/unittests/CMakeLists.txt +++ b/tests/unittests/CMakeLists.txt @@ -151,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/vcpkg/test-vcpkg-linux.sh b/tests/vcpkg/test-vcpkg-linux.sh index 6e6b7bcbd..5e754e96e 100755 --- a/tests/vcpkg/test-vcpkg-linux.sh +++ b/tests/vcpkg/test-vcpkg-linux.sh @@ -73,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 ef243a5a0..06275d93e 100644 --- a/tests/vcpkg/test-vcpkg-windows.ps1 +++ b/tests/vcpkg/test-vcpkg-windows.ps1 @@ -154,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