From d3a5fca5b7df49ab0a3c44d13ab6fc06e044c470 Mon Sep 17 00:00:00 2001 From: Stephen Griffin Date: Wed, 21 Oct 2026 10:52:04 -0400 Subject: [PATCH 1/3] Fix table export row indices across query batches Keep exported row indices monotonic across QueryRows batches, including short batches. Fixes #882. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- core/mapi/mapiOutput.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/mapi/mapiOutput.cpp b/core/mapi/mapiOutput.cpp index 256bb9698..2b79e4053 100644 --- a/core/mapi/mapiOutput.cpp +++ b/core/mapi/mapiOutput.cpp @@ -252,6 +252,7 @@ namespace output Output(ulDbgLvl, fFile, false, g_szXMLHeader); Output(ulDbgLvl, fFile, false, L"\n"); + ULONG iOutputRow = 0; for (;;) { FreeProws(lpRows); @@ -261,7 +262,7 @@ namespace output for (ULONG iCurRow = 0; iCurRow < lpRows->cRows; iCurRow++) { - Outputf(ulDbgLvl, fFile, false, L"\n", iCurRow); + Outputf(ulDbgLvl, fFile, false, L"\n", iOutputRow++); outputSRow(ulDbgLvl, fFile, &lpRows->aRow[iCurRow], nullptr); Output(ulDbgLvl, fFile, false, L"\n"); } From 708a5ee3b004ec032c2d255690e4d738653f42bf Mon Sep 17 00:00:00 2001 From: Stephen Griffin Date: Wed, 21 Oct 2026 11:33:43 -0400 Subject: [PATCH 2/3] Add provider-independent table export regression coverage Exercise actual XML export across batch boundaries, row payloads, seek and query failures, input preservation, diagnostics, and allocation cleanup. Route only the test exporter's memory calls through the existing MAPI stub seam so both architectures run without initialized native MAPI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- UnitTest/UnitTest.vcxproj | 1 + UnitTest/UnitTest.vcxproj.filters | 3 + UnitTest/tests/mapioutputtest.cpp | 517 ++++++++++++++++++++++++++++++ 3 files changed, 521 insertions(+) create mode 100644 UnitTest/tests/mapioutputtest.cpp diff --git a/UnitTest/UnitTest.vcxproj b/UnitTest/UnitTest.vcxproj index e0db92731..db3b2fc5b 100644 --- a/UnitTest/UnitTest.vcxproj +++ b/UnitTest/UnitTest.vcxproj @@ -795,6 +795,7 @@ + diff --git a/UnitTest/UnitTest.vcxproj.filters b/UnitTest/UnitTest.vcxproj.filters index 5a2b4b128..a81f9d748 100644 --- a/UnitTest/UnitTest.vcxproj.filters +++ b/UnitTest/UnitTest.vcxproj.filters @@ -32,6 +32,9 @@ Source Files + + Source Files + Source Files diff --git a/UnitTest/tests/mapioutputtest.cpp b/UnitTest/tests/mapioutputtest.cpp new file mode 100644 index 000000000..a6dd7f0db --- /dev/null +++ b/UnitTest/tests/mapioutputtest.cpp @@ -0,0 +1,517 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Resolve only the exporter's memory APIs through this DLL, not an installed MAPI provider. +#ifdef _M_IX86 +#pragma comment(linker, "/export:MAPIAllocateBuffer@8=_TestMAPIAllocateBuffer@8") +#pragma comment(linker, "/export:MAPIFreeBuffer@4=_TestMAPIFreeBuffer@4") +#pragma comment(linker, "/export:FreeProws@4=_TestFreeProws@4") +#else +#pragma comment(linker, "/export:MAPIAllocateBuffer=TestMAPIAllocateBuffer") +#pragma comment(linker, "/export:MAPIFreeBuffer=TestMAPIFreeBuffer") +#pragma comment(linker, "/export:FreeProws=TestFreeProws") +#endif + +namespace +{ + union Buffer { + Buffer* next; + std::max_align_t alignment; + }; + + Buffer* buffers{}; + ULONG invalidFrees{}; + ULONG freedRowSets{}; + std::vector freedPropertyTags; +} // namespace + +extern "C" SCODE STDAPICALLTYPE TestMAPIAllocateBuffer(ULONG size, LPVOID* result) +{ + if (!result) return MAPI_E_INVALID_PARAMETER; + *result = nullptr; + if (static_cast(size) > (std::numeric_limits::max)() - sizeof(Buffer)) + return MAPI_E_NOT_ENOUGH_MEMORY; + const auto buffer = static_cast(HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(Buffer) + size)); + if (!buffer) return MAPI_E_NOT_ENOUGH_MEMORY; + buffer->next = buffers; + buffers = buffer; + *result = buffer + 1; + return S_OK; +} + +extern "C" ULONG STDAPICALLTYPE TestMAPIFreeBuffer(LPVOID buffer) +{ + if (!buffer) return 0; + auto link = &buffers; + while (*link && *link + 1 != buffer) + link = &(*link)->next; + if (!*link) + { + ++invalidFrees; + return static_cast(MAPI_E_INVALID_PARAMETER); + } + + const auto allocation = *link; + *link = allocation->next; + HeapFree(GetProcessHeap(), 0, allocation); + return 0; +} + +extern "C" void STDAPICALLTYPE TestFreeProws(LPSRowSet rows) +{ + if (!rows) return; + ++freedRowSets; + for (ULONG row = 0; row < rows->cRows; ++row) + { + for (ULONG prop = 0; prop < rows->aRow[row].cValues; ++prop) + freedPropertyTags.push_back(rows->aRow[row].lpProps[prop].ulPropTag); + TestMAPIFreeBuffer(rows->aRow[row].lpProps); + } + + TestMAPIFreeBuffer(rows); +} + +namespace mapioutputtest +{ + constexpr ULONG numberTag = 0x60010003; + constexpr ULONG stringTag = 0x6002001F; + + class TestTable final : public IMAPITable + { + public: + TestTable(ULONG rowCount, ULONG batchSize, bool withProperties = false) + : m_rowCount(rowCount), m_batchSize(batchSize), m_withProperties(withProperties) + { + } + + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID, void** object) override + { + *object = nullptr; + return E_NOINTERFACE; + } + ULONG STDMETHODCALLTYPE AddRef() override { return ++m_refs; } + ULONG STDMETHODCALLTYPE Release() override { return --m_refs; } + HRESULT STDMETHODCALLTYPE GetLastError(HRESULT, ULONG, LPMAPIERROR*) override { return MAPI_E_NO_SUPPORT; } + HRESULT STDMETHODCALLTYPE Advise(ULONG, LPMAPIADVISESINK, ULONG_PTR*) override { return MAPI_E_NO_SUPPORT; } + HRESULT STDMETHODCALLTYPE Unadvise(ULONG_PTR) override { return MAPI_E_NO_SUPPORT; } + HRESULT STDMETHODCALLTYPE GetStatus(ULONG*, ULONG*) override { return MAPI_E_NO_SUPPORT; } + HRESULT STDMETHODCALLTYPE SetColumns(LPSPropTagArray, ULONG) override { return MAPI_E_NO_SUPPORT; } + HRESULT STDMETHODCALLTYPE QueryColumns(ULONG, LPSPropTagArray*) override { return MAPI_E_NO_SUPPORT; } + HRESULT STDMETHODCALLTYPE GetRowCount(ULONG, ULONG*) override { return MAPI_E_NO_SUPPORT; } + HRESULT STDMETHODCALLTYPE SeekRow(BOOKMARK origin, LONG count, LONG* sought) override + { + Assert::AreEqual(static_cast(BOOKMARK_BEGINNING), origin); + Assert::AreEqual(0L, count); + ++seekCount; + if (FAILED(seekResult)) return seekResult; + position = 0; + if (sought) *sought = 0; + return S_OK; + } + HRESULT STDMETHODCALLTYPE SeekRowApprox(ULONG, ULONG) override { return MAPI_E_NO_SUPPORT; } + HRESULT STDMETHODCALLTYPE QueryPosition(ULONG*, ULONG*, ULONG*) override { return MAPI_E_NO_SUPPORT; } + HRESULT STDMETHODCALLTYPE FindRow(LPSRestriction, BOOKMARK, ULONG) override { return MAPI_E_NO_SUPPORT; } + HRESULT STDMETHODCALLTYPE Restrict(LPSRestriction, ULONG) override { return MAPI_E_NO_SUPPORT; } + HRESULT STDMETHODCALLTYPE CreateBookmark(BOOKMARK*) override { return MAPI_E_NO_SUPPORT; } + HRESULT STDMETHODCALLTYPE FreeBookmark(BOOKMARK) override { return MAPI_E_NO_SUPPORT; } + HRESULT STDMETHODCALLTYPE SortTable(LPSSortOrderSet, ULONG) override { return MAPI_E_NO_SUPPORT; } + HRESULT STDMETHODCALLTYPE QuerySortOrder(LPSSortOrderSet*) override { return MAPI_E_NO_SUPPORT; } + HRESULT STDMETHODCALLTYPE QueryRows(LONG requested, ULONG flags, LPSRowSet* rows) override + { + Assert::AreEqual(20L, requested); + Assert::AreEqual(0UL, flags); + ++queryCount; + *rows = nullptr; + if (queryCount == failOnQuery && !returnRowsOnFailure) return MAPI_E_CALL_FAILED; + if (position == m_rowCount && nullAtEnd) return S_OK; + const auto count = (std::min) ({static_cast(requested), m_batchSize, m_rowCount - position}); + *rows = mapi::allocate(CbNewSRowSet(count)); + if (!*rows) return MAPI_E_NOT_ENOUGH_MEMORY; + (*rows)->cRows = count; + if (m_withProperties) + { + for (ULONG row = 0; row < count; ++row) + { + auto& outputRow = (*rows)->aRow[row]; + outputRow.lpProps = mapi::allocate(2 * sizeof(SPropValue)); + if (!outputRow.lpProps) return MAPI_E_NOT_ENOUGH_MEMORY; + outputRow.cValues = 2; + outputRow.lpProps[0].ulPropTag = stringTag; + outputRow.lpProps[0].Value.lpszW = m_text.data(); + outputRow.lpProps[1].ulPropTag = numberTag; + outputRow.lpProps[1].Value.l = static_cast(position + row); + } + } + + position += count; + return queryCount == failOnQuery ? MAPI_E_CALL_FAILED : S_OK; + } + HRESULT STDMETHODCALLTYPE Abort() override { return MAPI_E_NO_SUPPORT; } + HRESULT STDMETHODCALLTYPE ExpandRow(ULONG, LPBYTE, ULONG, ULONG, LPSRowSet*, ULONG*) override + { + return MAPI_E_NO_SUPPORT; + } + HRESULT STDMETHODCALLTYPE CollapseRow(ULONG, LPBYTE, ULONG, ULONG*) override { return MAPI_E_NO_SUPPORT; } + HRESULT STDMETHODCALLTYPE WaitForCompletion(ULONG, ULONG, ULONG*) override { return MAPI_E_NO_SUPPORT; } + HRESULT STDMETHODCALLTYPE GetCollapseState(ULONG, ULONG, LPBYTE, ULONG*, LPBYTE*) override + { + return MAPI_E_NO_SUPPORT; + } + HRESULT STDMETHODCALLTYPE SetCollapseState(ULONG, ULONG, LPBYTE, BOOKMARK*) override + { + return MAPI_E_NO_SUPPORT; + } + + ULONG queryCount{}; + ULONG seekCount{}; + ULONG position{}; + ULONG failOnQuery{}; + HRESULT seekResult{S_OK}; + bool returnRowsOnFailure{}; + bool nullAtEnd{}; + + private: + ULONG m_refs{1}; + ULONG m_rowCount{}; + ULONG m_batchSize{}; + bool m_withProperties{}; + std::wstring m_text{L"\u03A9<&"}; + }; + + class OutputFile + { + public: + OutputFile() + { + FILE* file{}; + Assert::AreEqual(0, tmpfile_s(&file)); + m_file.reset(file); + Assert::IsTrue(_setmode(_fileno(file), _O_U16TEXT) != -1); + } + + FILE* get() const noexcept { return m_file.get(); } + + std::wstring read() const + { + Assert::AreEqual(0, fflush(get())); + Assert::AreEqual(0, fseek(get(), 0, SEEK_SET)); + auto actual = std::wstring{}; + for (auto ch = fgetwc(get()); ch != WEOF; ch = fgetwc(get())) + actual.push_back(static_cast(ch)); + Assert::AreEqual(0, ferror(get())); + return actual; + } + + private: + std::unique_ptr m_file{nullptr, fclose}; + }; + + std::wstring ExpectedNumber(ULONG tag, ULONG value) + { + const auto names = + tag == numberTag ? L"\t\tPR_DOTSTUFF_STATE\n" + L"\t\tPR_NICK_NAME, PR_NICK_NAME_A, PR_NICK_NAME_W\n" + : tag == 0x60030003 + ? L"\t\tPR_DROPDOWN_DISPLAY_NAME\n" + L"\t\tPR_DROPDOWN_DISPLAY_NAME_A, PR_DROPDOWN_DISPLAY_NAME_W\n" + : L""; + return strings::format( + L"\t\n" + L"%ws" + L"\t\t%u\n" + L"\t\t0x%X\n" + L"\t\n", + tag, + names, + value, + value); + } + + std::wstring ExpectedProperties(ULONG value) + { + return ExpectedNumber(numberTag, value) + L"\t\n" + L"\t\t\n" + L"\t\tA9033C002600\n" + L"\t\n"; + } + + std::wstring ExpectedTable(ULONG rowCount, bool withProperties = false, ULONG firstValue = 0) + { + auto expected = std::wstring(L"\n
\n"); + for (ULONG row = 0; row < rowCount; ++row) + { + expected += strings::format(L"\n", row); + if (withProperties) expected += ExpectedProperties(firstValue + row); + + expected += L"\n"; + } + + return expected + L"
\n"; + } + + TEST_CLASS(mapioutputtest) + { + public: + TEST_CLASS_INITIALIZE(initialize) { unittest::init(); } + + TEST_METHOD_INITIALIZE(initializeMemory) + { + Assert::IsTrue(buffers == nullptr); + invalidFrees = 0; + freedRowSets = 0; + freedPropertyTags.clear(); + m_previousFreedRowSets = 0; + m_errors.clear(); + HMODULE testModule{}; + Assert::IsTrue(GetModuleHandleExW(0, L"UnitTest.dll", &testModule) != FALSE); + const auto original = mapistub::GetMAPIHandle(); + if (original) + { + if (!GetModuleHandleExW( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, reinterpret_cast(original), &m_originalMAPI)) + { + FreeLibrary(testModule); + Assert::Fail(L"Could not retain the original MAPI module."); + } + } + + m_debugTag = registry::debugTag; + registry::debugTag = 0; + mapistub::SetMAPIHandle(nullptr); + mapistub::SetMAPIHandle(testModule); + m_installed = true; + m_originalDisplayError = std::move(error::displayError); + error::displayError = [this](const std::wstring& text) { m_errors.push_back(text); }; + } + + TEST_METHOD_CLEANUP(uninitializeMemory) + { + ULONG leakedBuffers{}; + while (buffers) + { + ++leakedBuffers; + TestMAPIFreeBuffer(buffers + 1); + } + + if (m_installed) + { + error::displayError = std::move(m_originalDisplayError); + mapistub::SetMAPIHandle(nullptr); + if (m_originalMAPI) mapistub::SetMAPIHandle(m_originalMAPI); + registry::debugTag = m_debugTag; + m_originalMAPI = nullptr; + m_installed = false; + } + + Assert::AreEqual(0UL, leakedBuffers, L"The exporter leaked MAPI allocations."); + Assert::AreEqual(0UL, invalidFrees, L"The exporter freed an unowned MAPI allocation."); + } + + TEST_METHOD(Test_TableRowIndicesAcrossBatches) { CheckTable(41, 20); } + TEST_METHOD(Test_TableRowIndicesShortBatches) { CheckTable(41, 7); } + TEST_METHOD(Test_TableRowIndicesSingleRowBatches) { CheckTable(21, 1); } + + TEST_METHOD(Test_TableRowIndicesBoundaries) + { + for (const auto count : {0UL, 1UL, 19UL, 20UL, 21UL, 40UL}) + CheckTable(count, 20); + } + + TEST_METHOD(Test_TablePropertiesStayWithRowsAcrossBatches) + { + CheckTable(41, 7, true); + Assert::AreEqual(size_t{82}, freedPropertyTags.size()); + for (size_t prop = 0; prop < freedPropertyTags.size(); prop += 2) + { + Assert::AreEqual(stringTag, freedPropertyTags[prop]); + Assert::AreEqual(numberTag, freedPropertyTags[prop + 1]); + } + } + + TEST_METHOD(Test_TableSeeksToBeginning) + { + auto table = TestTable(3, 2, true); + table.position = 2; + auto file = OutputFile{}; + output::outputTable(output::dbgLevel::NoDebug, file.get(), &table); + unittest::AreEqualEx(ExpectedTable(3, true), file.read()); + Assert::AreEqual(1UL, table.seekCount); + Assert::IsTrue(m_errors.empty()); + } + + TEST_METHOD(Test_TableAcceptsNullTerminalRowSet) + { + auto table = TestTable(21, 20, true); + table.nullAtEnd = true; + auto file = OutputFile{}; + output::outputTable(output::dbgLevel::NoDebug, file.get(), &table); + unittest::AreEqualEx(ExpectedTable(21, true), file.read()); + Assert::AreEqual(3UL, table.queryCount); + Assert::AreEqual(2UL, freedRowSets); + Assert::IsTrue(m_errors.empty()); + } + + TEST_METHOD(Test_TableQueryFailureBeforeFirstRow) { CheckQueryFailure(1, false); } + TEST_METHOD(Test_TableQueryFailureAfterBatch) { CheckQueryFailure(2, false); } + TEST_METHOD(Test_TableQueryFailureFreesReturnedRows) { CheckQueryFailure(2, true); } + + TEST_METHOD(Test_TableSeekFailureIsReportedAndExportContinues) + { + auto table = TestTable(3, 20, true); + table.position = 1; + table.seekResult = MAPI_E_CALL_FAILED; + auto file = OutputFile{}; + output::outputTable(output::dbgLevel::NoDebug, file.get(), &table); + unittest::AreEqualEx(ExpectedTable(2, true, 1), file.read()); + CheckError(L"SeekRow"); + } + + TEST_METHOD(Test_NullTableProducesNoOutput) + { + auto file = OutputFile{}; + output::outputTable(output::dbgLevel::NoDebug, file.get(), nullptr); + Assert::IsTrue(file.read().empty()); + Assert::IsTrue(m_errors.empty()); + } + + TEST_METHOD(Test_DisabledDebugDoesNotReadTable) + { + auto table = TestTable(3, 20); + output::outputTable(output::dbgLevel::Generic, nullptr, &table); + Assert::AreEqual(0UL, table.queryCount); + Assert::AreEqual(0UL, table.seekCount); + Assert::IsTrue(m_errors.empty()); + } + + TEST_METHOD(Test_PropertiesAreSortedWithoutChangingInput) + { + SPropValue props[3]{}; + props[0].ulPropTag = 0x60030003; + props[0].Value.l = 3; + props[1].ulPropTag = numberTag; + props[1].Value.l = 1; + props[2].ulPropTag = 0x60020003; + props[2].Value.l = 2; + const auto original = std::vector(std::begin(props), std::end(props)); + auto file = OutputFile{}; + output::outputProperties(output::dbgLevel::NoDebug, file.get(), 3, props, nullptr, false); + unittest::AreEqualEx( + ExpectedNumber(numberTag, 1) + ExpectedNumber(0x60020003, 2) + ExpectedNumber(0x60030003, 3), + file.read()); + Assert::IsTrue(memcmp(original.data(), props, sizeof(props)) == 0); + Assert::IsTrue(m_errors.empty()); + } + + TEST_METHOD(Test_RowSetOutputsEveryRowsProperties) + { + auto table = TestTable(2, 20, true); + LPSRowSet rows{}; + Assert::AreEqual(S_OK, table.QueryRows(20, 0, &rows)); + auto file = OutputFile{}; + output::outputSRowSet(output::dbgLevel::NoDebug, file.get(), rows, nullptr); + FreeProws(rows); + unittest::AreEqualEx(ExpectedProperties(0) + ExpectedProperties(1), file.read()); + Assert::IsTrue(m_errors.empty()); + } + + TEST_METHOD(Test_EmptyRowSetProducesNoOutput) + { + SRowSet rows{}; + auto file = OutputFile{}; + output::outputSRowSet(output::dbgLevel::NoDebug, file.get(), &rows, nullptr); + Assert::IsTrue(file.read().empty()); + Assert::IsTrue(m_errors.empty()); + } + + TEST_METHOD(Test_InvalidRowInputsProduceDiagnostics) + { + auto file = OutputFile{}; + output::outputSRowSet(output::dbgLevel::NoDebug, file.get(), nullptr, nullptr); + output::outputSRow(output::dbgLevel::NoDebug, file.get(), nullptr, nullptr); + SRow row{}; + row.cValues = 1; + output::outputSRow(output::dbgLevel::NoDebug, file.get(), &row, nullptr); + output::outputProperties(output::dbgLevel::NoDebug, file.get(), 1, nullptr, nullptr, false); + const auto actual = file.read(); + for (const auto message : + {L"OutputSRowSet called with NULL lpRowSet!", + L"OutputSRow called with NULL lpSRow!", + L"OutputSRow called with NULL lpSRow->lpProps!", + L"OutputProperties called with NULL lpProps!"}) + Assert::IsTrue(actual.find(message) != std::wstring::npos); + Assert::IsTrue(m_errors.empty()); + } + + TEST_METHOD(Test_DisabledDebugSkipsRowAndPropertyOutput) + { + output::outputSRowSet(output::dbgLevel::Generic, nullptr, nullptr, nullptr); + output::outputSRow(output::dbgLevel::Generic, nullptr, nullptr, nullptr); + output::outputProperties(output::dbgLevel::Generic, nullptr, 1, nullptr, nullptr, false); + output::outputProperty(output::dbgLevel::Generic, nullptr, nullptr, nullptr, false); + Assert::IsTrue(m_errors.empty()); + } + + TEST_METHOD(Test_NullPropertyProducesNoOutput) + { + auto file = OutputFile{}; + output::outputProperty(output::dbgLevel::NoDebug, file.get(), nullptr, nullptr, false); + Assert::IsTrue(file.read().empty()); + Assert::IsTrue(m_errors.empty()); + } + + private: + void CheckTable(ULONG rowCount, ULONG batchSize, bool withProperties = false) + { + auto table = TestTable(rowCount, batchSize, withProperties); + auto file = OutputFile{}; + output::outputTable(output::dbgLevel::NoDebug, file.get(), &table); + unittest::AreEqualEx( + ExpectedTable(rowCount, withProperties), + file.read(), + strings::format(L"Rows: %u, batch size: %u", rowCount, batchSize).c_str()); + Assert::AreEqual((rowCount + batchSize - 1) / batchSize + 1, table.queryCount); + Assert::AreEqual(table.queryCount, freedRowSets - m_previousFreedRowSets); + m_previousFreedRowSets = freedRowSets; + Assert::AreEqual(1UL, table.seekCount); + Assert::IsTrue(m_errors.empty()); + } + + void CheckQueryFailure(ULONG failOnQuery, bool returnRows) + { + auto table = TestTable(25, 20, true); + table.failOnQuery = failOnQuery; + table.returnRowsOnFailure = returnRows; + auto file = OutputFile{}; + output::outputTable(output::dbgLevel::NoDebug, file.get(), &table); + unittest::AreEqualEx(ExpectedTable((failOnQuery - 1) * 20, true), file.read()); + Assert::AreEqual(failOnQuery, table.queryCount); + Assert::AreEqual(failOnQuery - (returnRows ? 0UL : 1UL), freedRowSets); + CheckError(L"QueryRows"); + } + + void CheckError(const std::wstring& function) + { + Assert::AreEqual(size_t{1}, m_errors.size()); + Assert::IsTrue(m_errors[0].find(function) != std::wstring::npos); + Assert::IsTrue(m_errors[0].find(L"0x80004005") != std::wstring::npos); + } + + HMODULE m_originalMAPI{}; + bool m_installed{}; + ULONG m_debugTag{}; + ULONG m_previousFreedRowSets{}; + std::function m_originalDisplayError; + std::vector m_errors; + }; +} // namespace mapioutputtest From f914df0e6e7e460f1fa63b733eaa30292005881a Mon Sep 17 00:00:00 2001 From: Stephen Griffin Date: Wed, 21 Oct 2026 12:25:16 -0400 Subject: [PATCH 3/3] Match clang-format 17 spacing in table export tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- UnitTest/tests/mapioutputtest.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UnitTest/tests/mapioutputtest.cpp b/UnitTest/tests/mapioutputtest.cpp index a6dd7f0db..31dbc13b3 100644 --- a/UnitTest/tests/mapioutputtest.cpp +++ b/UnitTest/tests/mapioutputtest.cpp @@ -135,7 +135,7 @@ namespace mapioutputtest *rows = nullptr; if (queryCount == failOnQuery && !returnRowsOnFailure) return MAPI_E_CALL_FAILED; if (position == m_rowCount && nullAtEnd) return S_OK; - const auto count = (std::min) ({static_cast(requested), m_batchSize, m_rowCount - position}); + const auto count = (std::min)({static_cast(requested), m_batchSize, m_rowCount - position}); *rows = mapi::allocate(CbNewSRowSet(count)); if (!*rows) return MAPI_E_NOT_ENOUGH_MEMORY; (*rows)->cRows = count;