diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 82bfc878f6..ebb90459f5 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -709,6 +709,9 @@ 9286059729A5098900F96038 /* SentryGeo.m in Sources */ = {isa = PBXBuildFile; fileRef = 9286059629A5098900F96038 /* SentryGeo.m */; }; 9286059929A50BAB00F96038 /* SentryGeoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9286059829A50BAA00F96038 /* SentryGeoTests.swift */; }; 928BED2B2E16977A00B4D398 /* SentryLogBatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 928BED2A2E16977A00B4D398 /* SentryLogBatcherTests.swift */; }; + 92B472832EF1AD7A00DD2891 /* SentryBatchBufferC.c in Sources */ = {isa = PBXBuildFile; fileRef = 92B472822EF1AD7A00DD2891 /* SentryBatchBufferC.c */; }; + 92B472842EF1AD7A00DD2891 /* SentryBatchBufferC.h in Headers */ = {isa = PBXBuildFile; fileRef = 92B472812EF1AD7A00DD2891 /* SentryBatchBufferC.h */; }; + 92B4728B2EF1ADA000DD2891 /* CrashSafeBatchBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92B4728A2EF1ADA000DD2891 /* CrashSafeBatchBuffer.swift */; }; 92B6BDA92E05B8F600D538B3 /* SentryLogLevelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92B6BDA82E05B8F000D538B3 /* SentryLogLevelTests.swift */; }; 92B6BDAD2E05B9FB00D538B3 /* SentryLogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92B6BDAC2E05B9F700D538B3 /* SentryLogTests.swift */; }; 92D957732E05A44600E20E66 /* SentryAsyncLog.m in Sources */ = {isa = PBXBuildFile; fileRef = 92D957722E05A44600E20E66 /* SentryAsyncLog.m */; }; @@ -821,6 +824,7 @@ D4D7AA762EEADF4B00E28DFB /* InMemoryBatchBufferTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4D7AA752EEADF4800E28DFB /* InMemoryBatchBufferTests.swift */; }; D4D7AA782EEAE30F00E28DFB /* BatcherScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4D7AA772EEAE30B00E28DFB /* BatcherScopeTests.swift */; }; D4DDC0F42EE8572F00F321F6 /* BatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DDC0F32EE8572F00F321F6 /* BatcherTests.swift */; }; + 92B4728D2EF1ADB000DD2891 /* CrashSafeBatchBufferTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92B4728C2EF1ADB000DD2891 /* CrashSafeBatchBufferTests.swift */; }; D4DEE6592E439B2E00FCA5A9 /* SentryProfileTimeseriesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D4DEE6582E439B2E00FCA5A9 /* SentryProfileTimeseriesTests.m */; }; D4E3F35D2D4A864600F79E2B /* SentryNSDictionarySanitizeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D42E48582D48FC8F00D251BC /* SentryNSDictionarySanitizeTests.swift */; }; D4E3F35E2D4A877300F79E2B /* SentryNSDictionarySanitize+Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = D41909942D490006002B83D0 /* SentryNSDictionarySanitize+Tests.m */; }; @@ -2077,6 +2081,9 @@ 9286059629A5098900F96038 /* SentryGeo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryGeo.m; sourceTree = ""; }; 9286059829A50BAA00F96038 /* SentryGeoTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SentryGeoTests.swift; sourceTree = ""; }; 928BED2A2E16977A00B4D398 /* SentryLogBatcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLogBatcherTests.swift; sourceTree = ""; }; + 92B472812EF1AD7A00DD2891 /* SentryBatchBufferC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SentryBatchBufferC.h; sourceTree = ""; }; + 92B472822EF1AD7A00DD2891 /* SentryBatchBufferC.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SentryBatchBufferC.c; sourceTree = ""; }; + 92B4728A2EF1ADA000DD2891 /* CrashSafeBatchBuffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashSafeBatchBuffer.swift; sourceTree = ""; }; 92B6BDA82E05B8F000D538B3 /* SentryLogLevelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLogLevelTests.swift; sourceTree = ""; }; 92B6BDAC2E05B9F700D538B3 /* SentryLogTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLogTests.swift; sourceTree = ""; }; 92D957722E05A44600E20E66 /* SentryAsyncLog.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryAsyncLog.m; sourceTree = ""; }; @@ -2194,6 +2201,7 @@ D4D7AA752EEADF4800E28DFB /* InMemoryBatchBufferTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InMemoryBatchBufferTests.swift; sourceTree = ""; }; D4D7AA772EEAE30B00E28DFB /* BatcherScopeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatcherScopeTests.swift; sourceTree = ""; }; D4DDC0F32EE8572F00F321F6 /* BatcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatcherTests.swift; sourceTree = ""; }; + 92B4728C2EF1ADB000DD2891 /* CrashSafeBatchBufferTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashSafeBatchBufferTests.swift; sourceTree = ""; }; D4DEE6582E439B2E00FCA5A9 /* SentryProfileTimeseriesTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryProfileTimeseriesTests.m; sourceTree = ""; }; D4E942042E9D1CF300DB7521 /* TestSessionReplayEnvironmentChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSessionReplayEnvironmentChecker.swift; sourceTree = ""; }; D4E9420B2E9D1D7600DB7521 /* TestSessionReplayEnvironmentCheckerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSessionReplayEnvironmentCheckerTests.swift; sourceTree = ""; }; @@ -3250,6 +3258,8 @@ 63FE700520DA4C1000CDBAE8 /* Tools */ = { isa = PBXGroup; children = ( + 92B472812EF1AD7A00DD2891 /* SentryBatchBufferC.h */, + 92B472822EF1AD7A00DD2891 /* SentryBatchBufferC.c */, 63FE702E20DA4C1000CDBAE8 /* SentryCrashNSErrorUtil.h */, 63FE700E20DA4C1000CDBAE8 /* SentryCrashNSErrorUtil.m */, 63FE703E20DA4C1000CDBAE8 /* SentryCrashCPU_Apple.h */, @@ -4465,6 +4475,7 @@ D4D7AA692EEAD89300E28DFB /* BatcherItem.swift */, D4D7AA6A2EEAD89300E28DFB /* BatcherScope.swift */, D4D7AA6B2EEAD89300E28DFB /* BatchBuffer.swift */, + 92B4728A2EF1ADA000DD2891 /* CrashSafeBatchBuffer.swift */, D4D7AA6C2EEAD89300E28DFB /* InMemoryBatchBuffer.swift */, ); path = Batcher; @@ -4476,6 +4487,7 @@ D4D7AA772EEAE30B00E28DFB /* BatcherScopeTests.swift */, D4DDC0F32EE8572F00F321F6 /* BatcherTests.swift */, D4D7AA752EEADF4800E28DFB /* InMemoryBatchBufferTests.swift */, + 92B4728C2EF1ADB000DD2891 /* CrashSafeBatchBufferTests.swift */, ); path = Batcher; sourceTree = ""; @@ -5231,6 +5243,7 @@ 84AF45A629A7FFA500FBB177 /* SentryProfiledTracerConcurrency.h in Headers */, 8EAE980B261E9F530073B6B3 /* SentryPerformanceTracker.h in Headers */, F48F75732E5FA649009D4E7D /* SentryBinaryImageCacheCallbacks.h in Headers */, + 92B472842EF1AD7A00DD2891 /* SentryBatchBufferC.h in Headers */, 63FE718520DA4C1100CDBAE8 /* SentryCrashC.h in Headers */, 8EA1ED0D2669028C00E62B98 /* SentryUIViewControllerSwizzling.h in Headers */, D820CDB82BB1895F00BA339D /* SentrySessionReplayIntegration.h in Headers */, @@ -5852,6 +5865,7 @@ 92D957732E05A44600E20E66 /* SentryAsyncLog.m in Sources */, 7B08A3472924CF9C0059603A /* SentryMetricKitIntegration.m in Sources */, 623FD9022D3FA5E000803EDA /* SentryFrameCodable.swift in Sources */, + 92B472832EF1AD7A00DD2891 /* SentryBatchBufferC.c in Sources */, 62BDDD122D51FD540024CCD1 /* SentryThreadCodable.swift in Sources */, 7B63459B280EB9E200CFA05A /* SentryUIEventTrackingIntegration.m in Sources */, 15E0A8ED240F2CB000F044E3 /* SentrySerialization.m in Sources */, @@ -5940,6 +5954,7 @@ 843FB3232D0CD04D00558F18 /* SentryUserAccess.m in Sources */, 63FE716720DA4C1100CDBAE8 /* SentryCrashCPU.c in Sources */, 63FE717320DA4C1100CDBAE8 /* SentryCrashC.c in Sources */, + 92B4728B2EF1ADA000DD2891 /* CrashSafeBatchBuffer.swift in Sources */, D4D7AA6E2EEAD89300E28DFB /* Batcher.swift in Sources */, D4D7AA6F2EEAD89300E28DFB /* BatchBuffer.swift in Sources */, D4D7AA702EEAD89300E28DFB /* BatcherItem.swift in Sources */, @@ -6345,6 +6360,7 @@ 7B6C5ED6264E62CA0010D138 /* SentryTransactionTests.swift in Sources */, D81FDF12280EA1060045E0E4 /* SentryScreenshotSourceTests.swift in Sources */, D4DDC0F42EE8572F00F321F6 /* BatcherTests.swift in Sources */, + 92B4728D2EF1ADB000DD2891 /* CrashSafeBatchBufferTests.swift in Sources */, D8019910286B089000C277F0 /* SentryCrashReportSinkTests.swift in Sources */, D885266427739D01001269FC /* SentryFileIOTrackingIntegrationTests.swift in Sources */, 7BBD18992449DE9D00427C76 /* TestRateLimits.swift in Sources */, diff --git a/Sources/Sentry/include/SentryPrivate.h b/Sources/Sentry/include/SentryPrivate.h index fef09791e5..3423e82a91 100644 --- a/Sources/Sentry/include/SentryPrivate.h +++ b/Sources/Sentry/include/SentryPrivate.h @@ -9,6 +9,7 @@ #import "NSLocale+Sentry.h" #import "SentryANRStoppedResultInternal.h" #import "SentryANRTrackerInternalDelegate.h" +#import "SentryBatchBufferC.h" #import "SentryBinaryImageCacheCallbacks.h" #import "SentryClient+Private.h" #import "SentryConcurrentRateLimitsDictionary.h" diff --git a/Sources/SentryCrash/Recording/Tools/SentryBatchBufferC.c b/Sources/SentryCrash/Recording/Tools/SentryBatchBufferC.c new file mode 100644 index 0000000000..287e7f6fb3 --- /dev/null +++ b/Sources/SentryCrash/Recording/Tools/SentryBatchBufferC.c @@ -0,0 +1,168 @@ +// +// sentry_batch_buffer.c +// +// Buffer wrapper for managing memory buffers. +// + +#include "SentryBatchBufferC.h" +#include +#include + +bool +sentry_batch_buffer_init(SentryBatchBufferC *buffer, size_t data_capacity, size_t items_capacity) +{ + if (buffer == NULL) { + return false; + } + + buffer->data = NULL; + buffer->data_capacity = 0; + buffer->data_size = 0; + buffer->item_offsets = NULL; + buffer->item_sizes = NULL; + buffer->items_capacity = 0; + buffer->item_count = 0; + + if (data_capacity == 0 || items_capacity == 0) { + return true; + } + + buffer->data = (char *)malloc(data_capacity); + if (buffer->data == NULL) { + return false; + } + buffer->data_capacity = data_capacity; + + buffer->item_offsets = (size_t *)malloc(items_capacity * sizeof(size_t)); + if (buffer->item_offsets == NULL) { + free(buffer->data); + buffer->data = NULL; + return false; + } + + buffer->item_sizes = (size_t *)malloc(items_capacity * sizeof(size_t)); + if (buffer->item_sizes == NULL) { + free(buffer->item_offsets); + free(buffer->data); + buffer->item_offsets = NULL; + buffer->data = NULL; + return false; + } + + buffer->items_capacity = items_capacity; + + return true; +} + +void +sentry_batch_buffer_destroy(SentryBatchBufferC *buffer) +{ + if (buffer == NULL) { + return; + } + + if (buffer->data != NULL) { + free(buffer->data); + buffer->data = NULL; + } + + if (buffer->item_offsets != NULL) { + free(buffer->item_offsets); + buffer->item_offsets = NULL; + } + + if (buffer->item_sizes != NULL) { + free(buffer->item_sizes); + buffer->item_sizes = NULL; + } + + buffer->data_capacity = 0; + buffer->data_size = 0; + buffer->items_capacity = 0; + buffer->item_count = 0; +} + +bool +sentry_batch_buffer_add_item(SentryBatchBufferC *buffer, const char *data, size_t length) +{ + if (buffer == NULL || data == NULL) { + return false; + } + + if (length == 0) { + return true; + } + + if (buffer->item_count >= buffer->items_capacity) { + return false; + } + + if (buffer->data_size + length > buffer->data_capacity) { + return false; + } + + buffer->item_offsets[buffer->item_count] = buffer->data_size; + buffer->item_sizes[buffer->item_count] = length; + + memcpy(buffer->data + buffer->data_size, data, length); + buffer->data_size += length; + + buffer->item_count++; + return true; +} + +const char * +sentry_batch_buffer_get_item(const SentryBatchBufferC *buffer, size_t index, size_t *size_out) +{ + if (buffer == NULL) { + return NULL; + } + + if (index >= buffer->item_count) { + return NULL; + } + + if (size_out != NULL) { + *size_out = buffer->item_sizes[index]; + } + + return buffer->data + buffer->item_offsets[index]; +} + +const char * +sentry_batch_buffer_get_data(const SentryBatchBufferC *buffer) +{ + if (buffer == NULL || buffer->data == NULL || buffer->data_size == 0) { + return NULL; + } + return buffer->data; +} + +size_t +sentry_batch_buffer_get_data_size(const SentryBatchBufferC *buffer) +{ + if (buffer == NULL) { + return 0; + } + return buffer->data_size; +} + +void +sentry_batch_buffer_clear(SentryBatchBufferC *buffer) +{ + if (buffer == NULL) { + return; + } + + buffer->data_size = 0; + buffer->item_count = 0; +} + +size_t +sentry_batch_buffer_get_item_count(const SentryBatchBufferC *buffer) +{ + if (buffer == NULL) { + return 0; + } + return buffer->item_count; +} diff --git a/Sources/SentryCrash/Recording/Tools/SentryBatchBufferC.h b/Sources/SentryCrash/Recording/Tools/SentryBatchBufferC.h new file mode 100644 index 0000000000..07a2290892 --- /dev/null +++ b/Sources/SentryCrash/Recording/Tools/SentryBatchBufferC.h @@ -0,0 +1,50 @@ +// +// sentry_batch_buffer.h +// +// Buffer wrapper for managing memory buffers. +// + +#ifndef HDR_SENTRY_BATCH_BUFFER_H +#define HDR_SENTRY_BATCH_BUFFER_H + +#include +#include + +typedef struct { + char *data; + size_t data_capacity; + size_t data_size; + + size_t *item_offsets; + size_t *item_sizes; + + size_t items_capacity; + size_t item_count; +} SentryBatchBufferC; + +/** @return true if initialization was successful, false otherwise. + * + * @note This function is NOT async-signal-safe because it calls malloc(). + */ +bool sentry_batch_buffer_init( + SentryBatchBufferC *buffer, size_t data_capacity, size_t items_capacity); + +void sentry_batch_buffer_destroy(SentryBatchBufferC *buffer); + +/** @return true if the item was successfully added, false if buffer is full. */ +bool sentry_batch_buffer_add_item(SentryBatchBufferC *buffer, const char *data, size_t length); + +/** @return A pointer to the item's data, or NULL if index is invalid. */ +const char *sentry_batch_buffer_get_item( + const SentryBatchBufferC *buffer, size_t index, size_t *size_out); + +/** @return A pointer to the data buffer, or NULL if the buffer is empty. */ +const char *sentry_batch_buffer_get_data(const SentryBatchBufferC *buffer); + +size_t sentry_batch_buffer_get_data_size(const SentryBatchBufferC *buffer); + +void sentry_batch_buffer_clear(SentryBatchBufferC *buffer); + +size_t sentry_batch_buffer_get_item_count(const SentryBatchBufferC *buffer); + +#endif // HDR_SENTRY_BATCH_BUFFER_H diff --git a/Sources/Swift/Tools/Batcher/BatchBuffer.swift b/Sources/Swift/Tools/Batcher/BatchBuffer.swift index 84abe6b1a8..f93eda8a86 100644 --- a/Sources/Swift/Tools/Batcher/BatchBuffer.swift +++ b/Sources/Swift/Tools/Batcher/BatchBuffer.swift @@ -1,3 +1,7 @@ +enum BatchBufferError: Error { + case bufferFull +} + protocol BatchBuffer { associatedtype Item diff --git a/Sources/Swift/Tools/Batcher/CrashSafeBatchBuffer.swift b/Sources/Swift/Tools/Batcher/CrashSafeBatchBuffer.swift new file mode 100644 index 0000000000..389f5a416e --- /dev/null +++ b/Sources/Swift/Tools/Batcher/CrashSafeBatchBuffer.swift @@ -0,0 +1,64 @@ +@_implementationOnly import _SentryPrivate +import Foundation + +/// Errors that can occur when using `CrashSafeBatchBuffer`. +enum CrashSafeBatchBufferError: Error { + case initializationFailed +} + +/// A wrapper around the `SentryBatchBufferC` C API. +final class CrashSafeBatchBuffer { + private var buffer: SentryBatchBufferC + + init(dataCapacity: Int, itemsCapacity: Int) throws { + var cBuffer = SentryBatchBufferC() + guard sentry_batch_buffer_init(&cBuffer, dataCapacity, itemsCapacity) else { + throw CrashSafeBatchBufferError.initializationFailed + } + self.buffer = cBuffer + } + + deinit { + sentry_batch_buffer_destroy(&buffer) + } + + /// Adds raw data to the buffer. + /// - Parameter data: The data to add + /// - Returns: `true` if the item was successfully added, `false` if the buffer is full + func addItem(_ data: Data) -> Bool { + guard !data.isEmpty else { + return true + } + return data.withUnsafeBytes { bytes in + guard let baseAddress = bytes.baseAddress else { + return false + } + return sentry_batch_buffer_add_item(&buffer, baseAddress.assumingMemoryBound(to: CChar.self), data.count) + } + } + + func clear() { + sentry_batch_buffer_clear(&buffer) + } + + var itemCount: Int { + return Int(sentry_batch_buffer_get_item_count(&buffer)) + } + + var dataSize: Int { + return Int(sentry_batch_buffer_get_data_size(&buffer)) + } + + func getAllItems() -> [Data] { + var items: [Data] = [] + let count = itemCount + for i in 0..: BatchBuffer { + private var crashSaveBatchBuffer: CrashSafeBatchBuffer? + private var elements: [Data] = [] - var itemsDataSize: Int = 0 + private var elementsDataSize: Int = 0 private let encoder: JSONEncoder = { let encoder = JSONEncoder() @@ -8,24 +10,64 @@ struct InMemoryBatchBuffer: BatchBuffer { return encoder }() - init() {} + init(dataCapacity: Int, itemsCapacity: Int) { + do { + crashSaveBatchBuffer = try CrashSafeBatchBuffer( + dataCapacity: dataCapacity * 2, + itemsCapacity: itemsCapacity + ) + } catch { + let warningText = "InMemoryBatchBuffer: Could not init crash safe storage." + SentrySDKLog.warning(warningText) + assertionFailure(warningText) + } + } mutating func append(_ item: Item) throws { let encoded = try encoder.encode(item) - elements.append(encoded) - itemsDataSize += encoded.count + + if let buffer = crashSaveBatchBuffer { + if !buffer.addItem(encoded) { + throw BatchBufferError.bufferFull + } + } else { + elements.append(encoded) + elementsDataSize += encoded.count + } } mutating func clear() { - elements.removeAll() - itemsDataSize = 0 + if let crashSaveBatchBuffer { + crashSaveBatchBuffer.clear() + } else { + elements.removeAll() + elementsDataSize = 0 + } + } + + var itemsDataSize: Int { + if let crashSaveBatchBuffer { + return crashSaveBatchBuffer.dataSize + } else { + return elementsDataSize + } } var itemsCount: Int { - elements.count + if let crashSaveBatchBuffer { + return crashSaveBatchBuffer.itemCount + } else { + return elements.count + } } var batchedData: Data { - Data("{\"items\":[".utf8) + elements.joined(separator: Data(",".utf8)) + Data("]}".utf8) + let elements: [Data] + if let crashSaveBatchBuffer { + elements = crashSaveBatchBuffer.getAllItems() + } else { + elements = self.elements + } + return Data("{\"items\":[".utf8) + elements.joined(separator: Data(",".utf8)) + Data("]}".utf8) } } diff --git a/Sources/Swift/Tools/SentryLogBatcher.swift b/Sources/Swift/Tools/SentryLogBatcher.swift index 6132de7b8b..e82dd25427 100644 --- a/Sources/Swift/Tools/SentryLogBatcher.swift +++ b/Sources/Swift/Tools/SentryLogBatcher.swift @@ -81,7 +81,10 @@ import Foundation releaseName: options.releaseName, installationId: SentryInstallation.cachedId(withCacheDirectoryPath: options.cacheDirectoryPath) ), - buffer: InMemoryBatchBuffer(), + buffer: InMemoryBatchBuffer( + dataCapacity: maxBufferSizeBytes, + itemsCapacity: maxLogCount + ), dateProvider: dateProvider, dispatchQueue: dispatchQueue ) diff --git a/Tests/SentryTests/Batcher/CrashSafeBatchBufferTests.swift b/Tests/SentryTests/Batcher/CrashSafeBatchBufferTests.swift new file mode 100644 index 0000000000..5a9531d5c1 --- /dev/null +++ b/Tests/SentryTests/Batcher/CrashSafeBatchBufferTests.swift @@ -0,0 +1,436 @@ +@_spi(Private) @testable import Sentry +@_spi(Private) import SentryTestUtils +import XCTest + +final class CrashSafeBatchBufferTests: XCTestCase { + // MARK: - Initialization Tests + + func testInit_whenValidCapacity_shouldSucceed() throws { + // -- Act -- + let sut = try CrashSafeBatchBuffer(dataCapacity: 1_024, itemsCapacity: 10) + + // -- Assert -- + XCTAssertEqual(sut.itemCount, 0) + XCTAssertEqual(sut.dataSize, 0) + } + + func testInit_whenZeroCapacity_shouldSucceed() throws { + // -- Act -- + let sut = try CrashSafeBatchBuffer(dataCapacity: 0, itemsCapacity: 0) + + // -- Assert -- + XCTAssertEqual(sut.itemCount, 0) + XCTAssertEqual(sut.dataSize, 0) + } + + // MARK: - Item Count Tests + + func testItemCount_withNoElements_shouldReturnZero() throws { + // -- Act -- + let sut = try CrashSafeBatchBuffer(dataCapacity: 1_024, itemsCapacity: 10) + + // -- Assert -- + XCTAssertEqual(sut.itemCount, 0) + } + + func testItemCount_withSingleElement_shouldReturnOne() throws { + // -- Arrange -- + let sut = try CrashSafeBatchBuffer(dataCapacity: 1_024, itemsCapacity: 10) + let data = Data("test".utf8) + + // -- Act -- + let success = sut.addItem(data) + + // -- Assert -- + XCTAssertTrue(success) + XCTAssertEqual(sut.itemCount, 1) + } + + func testItemCount_withMultipleElements_shouldReturnCorrectCount() throws { + // -- Arrange -- + let sut = try CrashSafeBatchBuffer(dataCapacity: 1_024, itemsCapacity: 10) + + // -- Act -- + XCTAssertTrue(sut.addItem(Data("item1".utf8))) + XCTAssertTrue(sut.addItem(Data("item2".utf8))) + XCTAssertTrue(sut.addItem(Data("item3".utf8))) + + // -- Assert -- + XCTAssertEqual(sut.itemCount, 3) + } + + // MARK: - Add Item Tests + + func testAddItem_withSingleElement_shouldAddElement() throws { + // -- Arrange -- + let sut = try CrashSafeBatchBuffer(dataCapacity: 1_024, itemsCapacity: 10) + let data = Data("test".utf8) + + // -- Act -- + let success = sut.addItem(data) + + // -- Assert -- + XCTAssertTrue(success) + XCTAssertEqual(sut.itemCount, 1) + let items = sut.getAllItems() + XCTAssertEqual(items.count, 1) + XCTAssertEqual(items[0], data) + } + + func testAddItem_withMultipleElements_shouldAddAllElements() throws { + // -- Arrange -- + let sut = try CrashSafeBatchBuffer(dataCapacity: 1_024, itemsCapacity: 10) + let data1 = Data("item1".utf8) + let data2 = Data("item2".utf8) + let data3 = Data("item3".utf8) + + // -- Act -- + XCTAssertTrue(sut.addItem(data1)) + XCTAssertTrue(sut.addItem(data2)) + XCTAssertTrue(sut.addItem(data3)) + + // -- Assert -- + XCTAssertEqual(sut.itemCount, 3) + let items = sut.getAllItems() + XCTAssertEqual(items.count, 3) + XCTAssertEqual(items[0], data1) + XCTAssertEqual(items[1], data2) + XCTAssertEqual(items[2], data3) + } + + func testAddItem_withMultipleElements_shouldMaintainOrder() throws { + // -- Arrange -- + let sut = try CrashSafeBatchBuffer(dataCapacity: 1_024, itemsCapacity: 10) + + // -- Act -- + XCTAssertTrue(sut.addItem(Data("first".utf8))) + XCTAssertTrue(sut.addItem(Data("second".utf8))) + XCTAssertTrue(sut.addItem(Data("third".utf8))) + + // -- Assert -- + let items = sut.getAllItems() + XCTAssertEqual(String(data: items[0], encoding: .utf8), "first") + XCTAssertEqual(String(data: items[1], encoding: .utf8), "second") + XCTAssertEqual(String(data: items[2], encoding: .utf8), "third") + } + + func testAddItem_withEmptyData_shouldReturnTrue() throws { + // -- Arrange -- + let sut = try CrashSafeBatchBuffer(dataCapacity: 1_024, itemsCapacity: 10) + + // -- Act -- + let success = sut.addItem(Data()) + + // -- Assert -- + XCTAssertTrue(success) + XCTAssertEqual(sut.itemCount, 0) // Empty data doesn't add an item + } + + func testAddItem_whenBufferFull_shouldReturnFalse() throws { + // -- Arrange -- + let sut = try CrashSafeBatchBuffer(dataCapacity: 10, itemsCapacity: 2) + let largeData = Data("this is too large".utf8) // 17 bytes, exceeds capacity + + // -- Act -- + let success = sut.addItem(largeData) + + // -- Assert -- + XCTAssertFalse(success) + } + + func testAddItem_whenItemsCapacityReached_shouldReturnFalse() throws { + // -- Arrange -- + let sut = try CrashSafeBatchBuffer(dataCapacity: 1_024, itemsCapacity: 2) + + // -- Act -- + XCTAssertTrue(sut.addItem(Data("item1".utf8))) + XCTAssertTrue(sut.addItem(Data("item2".utf8))) + let success = sut.addItem(Data("item3".utf8)) // Should fail + + // -- Assert -- + XCTAssertFalse(success) + XCTAssertEqual(sut.itemCount, 2) + } + + // MARK: - Data Size Tests + + func testDataSize_withNoElements_shouldReturnZero() throws { + // -- Arrange -- + let sut = try CrashSafeBatchBuffer(dataCapacity: 1_024, itemsCapacity: 10) + + // -- Act -- + let size = sut.dataSize + + // -- Assert -- + XCTAssertEqual(size, 0) + } + + func testDataSize_withSingleElement_shouldReturnElementSize() throws { + // -- Arrange -- + let sut = try CrashSafeBatchBuffer(dataCapacity: 1_024, itemsCapacity: 10) + let data = Data("test".utf8) + + // -- Act -- + XCTAssertTrue(sut.addItem(data)) + + // -- Assert -- + XCTAssertEqual(sut.dataSize, data.count) + } + + func testDataSize_withMultipleElements_shouldReturnSumOfSizes() throws { + // -- Arrange -- + let sut = try CrashSafeBatchBuffer(dataCapacity: 1_024, itemsCapacity: 10) + let data1 = Data("item1".utf8) + let data2 = Data("item2".utf8) + let data3 = Data("item3".utf8) + + // -- Act -- + XCTAssertTrue(sut.addItem(data1)) + XCTAssertTrue(sut.addItem(data2)) + XCTAssertTrue(sut.addItem(data3)) + + // -- Assert -- + XCTAssertEqual(sut.dataSize, data1.count + data2.count + data3.count) + } + + func testDataSize_afterClear_shouldReturnZero() throws { + // -- Arrange -- + let sut = try CrashSafeBatchBuffer(dataCapacity: 1_024, itemsCapacity: 10) + XCTAssertTrue(sut.addItem(Data("test1".utf8))) + XCTAssertTrue(sut.addItem(Data("test2".utf8))) + + // Assert pre-condition + XCTAssertGreaterThan(sut.dataSize, 0) + + // -- Act -- + sut.clear() + + // -- Assert -- + XCTAssertEqual(sut.dataSize, 0) + } + + func testDataSize_shouldUpdateAfterEachAdd() throws { + // -- Arrange -- + let sut = try CrashSafeBatchBuffer(dataCapacity: 1_024, itemsCapacity: 10) + let data1 = Data("first".utf8) + let data2 = Data("second".utf8) + + // -- Act & Assert -- + XCTAssertEqual(sut.dataSize, 0) + + XCTAssertTrue(sut.addItem(data1)) + XCTAssertEqual(sut.dataSize, data1.count) + + XCTAssertTrue(sut.addItem(data2)) + XCTAssertEqual(sut.dataSize, data1.count + data2.count) + } + + // MARK: - Clear Tests + + func testClear_withNoElements_shouldDoNothing() throws { + // -- Arrange -- + let sut = try CrashSafeBatchBuffer(dataCapacity: 1_024, itemsCapacity: 10) + + // Assert pre-condition + XCTAssertEqual(sut.itemCount, 0) + XCTAssertEqual(sut.dataSize, 0) + + // -- Act -- + sut.clear() + + // -- Assert -- + XCTAssertEqual(sut.itemCount, 0) + XCTAssertEqual(sut.dataSize, 0) + } + + func testClear_withSingleElement_shouldClearStorage() throws { + // -- Arrange -- + let sut = try CrashSafeBatchBuffer(dataCapacity: 1_024, itemsCapacity: 10) + XCTAssertTrue(sut.addItem(Data("test".utf8))) + + // Assert pre-condition + XCTAssertEqual(sut.itemCount, 1) + XCTAssertGreaterThan(sut.dataSize, 0) + + // -- Act -- + sut.clear() + + // -- Assert -- + XCTAssertEqual(sut.itemCount, 0) + XCTAssertEqual(sut.dataSize, 0) + XCTAssertEqual(sut.getAllItems().count, 0) + } + + func testClear_withMultipleElements_shouldClearStorage() throws { + // -- Arrange -- + let sut = try CrashSafeBatchBuffer(dataCapacity: 1_024, itemsCapacity: 10) + XCTAssertTrue(sut.addItem(Data("item1".utf8))) + XCTAssertTrue(sut.addItem(Data("item2".utf8))) + XCTAssertTrue(sut.addItem(Data("item3".utf8))) + + // Assert pre-condition + XCTAssertEqual(sut.itemCount, 3) + XCTAssertGreaterThan(sut.dataSize, 0) + + // -- Act -- + sut.clear() + + // -- Assert -- + XCTAssertEqual(sut.itemCount, 0) + XCTAssertEqual(sut.dataSize, 0) + XCTAssertEqual(sut.getAllItems().count, 0) + } + + func testClear_afterClear_shouldAllowNewAdds() throws { + // -- Arrange -- + let sut = try CrashSafeBatchBuffer(dataCapacity: 1_024, itemsCapacity: 10) + XCTAssertTrue(sut.addItem(Data("item1".utf8))) + XCTAssertTrue(sut.addItem(Data("item2".utf8))) + + // -- Act -- + sut.clear() + XCTAssertTrue(sut.addItem(Data("item3".utf8))) + + // -- Assert -- + XCTAssertEqual(sut.itemCount, 1) + let items = sut.getAllItems() + XCTAssertEqual(items.count, 1) + XCTAssertEqual(String(data: items[0], encoding: .utf8), "item3") + } + + func testMultipleClearCalls_shouldNotCauseIssues() throws { + // -- Arrange -- + let sut = try CrashSafeBatchBuffer(dataCapacity: 1_024, itemsCapacity: 10) + XCTAssertTrue(sut.addItem(Data("test".utf8))) + + // -- Act -- + sut.clear() + sut.clear() + sut.clear() + + // -- Assert -- + XCTAssertEqual(sut.itemCount, 0) + XCTAssertEqual(sut.dataSize, 0) + XCTAssertEqual(sut.getAllItems().count, 0) + } + + // MARK: - GetAllItems Tests + + func testGetAllItems_withNoElements_shouldReturnEmptyArray() throws { + // -- Arrange -- + let sut = try CrashSafeBatchBuffer(dataCapacity: 1_024, itemsCapacity: 10) + + // -- Act -- + let items = sut.getAllItems() + + // -- Assert -- + XCTAssertEqual(items.count, 0) + } + + func testGetAllItems_withSingleElement_shouldReturnSingleElement() throws { + // -- Arrange -- + let sut = try CrashSafeBatchBuffer(dataCapacity: 1_024, itemsCapacity: 10) + let data = Data("test".utf8) + XCTAssertTrue(sut.addItem(data)) + + // -- Act -- + let items = sut.getAllItems() + + // -- Assert -- + XCTAssertEqual(items.count, 1) + XCTAssertEqual(items[0], data) + } + + func testGetAllItems_withMultipleElements_shouldReturnAllElements() throws { + // -- Arrange -- + let sut = try CrashSafeBatchBuffer(dataCapacity: 1_024, itemsCapacity: 10) + let data1 = Data("item1".utf8) + let data2 = Data("item2".utf8) + let data3 = Data("item3".utf8) + XCTAssertTrue(sut.addItem(data1)) + XCTAssertTrue(sut.addItem(data2)) + XCTAssertTrue(sut.addItem(data3)) + + // -- Act -- + let items = sut.getAllItems() + + // -- Assert -- + XCTAssertEqual(items.count, 3) + XCTAssertEqual(items[0], data1) + XCTAssertEqual(items[1], data2) + XCTAssertEqual(items[2], data3) + } + + func testGetAllItems_shouldMaintainElementOrder() throws { + // -- Arrange -- + let sut = try CrashSafeBatchBuffer(dataCapacity: 1_024, itemsCapacity: 10) + XCTAssertTrue(sut.addItem(Data("first".utf8))) + XCTAssertTrue(sut.addItem(Data("second".utf8))) + XCTAssertTrue(sut.addItem(Data("third".utf8))) + + // -- Act -- + let items = sut.getAllItems() + + // -- Assert -- + XCTAssertEqual(String(data: items[0], encoding: .utf8), "first") + XCTAssertEqual(String(data: items[1], encoding: .utf8), "second") + XCTAssertEqual(String(data: items[2], encoding: .utf8), "third") + } + + // MARK: - Integration Tests + + func testAddClearAdd_shouldWorkCorrectly() throws { + // -- Arrange -- + let sut = try CrashSafeBatchBuffer(dataCapacity: 1_024, itemsCapacity: 10) + + // -- Act & Assert -- + XCTAssertTrue(sut.addItem(Data("item1".utf8))) + XCTAssertEqual(sut.itemCount, 1) + XCTAssertGreaterThan(sut.dataSize, 0) + + sut.clear() + XCTAssertEqual(sut.itemCount, 0) + XCTAssertEqual(sut.dataSize, 0) + + XCTAssertTrue(sut.addItem(Data("item2".utf8))) + XCTAssertTrue(sut.addItem(Data("item3".utf8))) + XCTAssertEqual(sut.itemCount, 2) + + let items = sut.getAllItems() + XCTAssertEqual(items.count, 2) + XCTAssertEqual(String(data: items[0], encoding: .utf8), "item2") + XCTAssertEqual(String(data: items[1], encoding: .utf8), "item3") + } + + // MARK: - Binary Data Tests + + func testAddItem_withBinaryData_shouldPreserveData() throws { + // -- Arrange -- + let sut = try CrashSafeBatchBuffer(dataCapacity: 1_024, itemsCapacity: 10) + let binaryData = Data([0x00, 0x01, 0x02, 0xFF, 0xFE, 0xFD]) + + // -- Act -- + XCTAssertTrue(sut.addItem(binaryData)) + + // -- Assert -- + let items = sut.getAllItems() + XCTAssertEqual(items.count, 1) + XCTAssertEqual(items[0], binaryData) + } + + func testAddItem_withLargeData_shouldWork() throws { + // -- Arrange -- + let sut = try CrashSafeBatchBuffer(dataCapacity: 10_000, itemsCapacity: 100) + let largeData = Data(repeating: 0x42, count: 5_000) + + // -- Act -- + let success = sut.addItem(largeData) + + // -- Assert -- + XCTAssertTrue(success) + XCTAssertEqual(sut.itemCount, 1) + XCTAssertEqual(sut.dataSize, 5_000) + let items = sut.getAllItems() + XCTAssertEqual(items[0].count, 5_000) + } +} diff --git a/Tests/SentryTests/Batcher/InMemoryBatchBufferTests.swift b/Tests/SentryTests/Batcher/InMemoryBatchBufferTests.swift index 053620cf99..efc3e7efeb 100644 --- a/Tests/SentryTests/Batcher/InMemoryBatchBufferTests.swift +++ b/Tests/SentryTests/Batcher/InMemoryBatchBufferTests.swift @@ -15,7 +15,7 @@ final class InMemoryBatchBufferTests: XCTestCase { func testCount_withNoElements_shouldReturnZero() { // -- Act -- - let sut = InMemoryBatchBuffer() + let sut = InMemoryBatchBuffer(dataCapacity: 1_024 * 1_024, itemsCapacity: 100) // -- Assert -- XCTAssertEqual(sut.itemsCount, 0) @@ -23,7 +23,7 @@ final class InMemoryBatchBufferTests: XCTestCase { func testCount_withSingleElement_shouldReturnOne() throws { // -- Arrange -- - var sut = InMemoryBatchBuffer() + var sut = InMemoryBatchBuffer(dataCapacity: 1_024 * 1_024, itemsCapacity: 100) // -- Act -- try sut.append(TestElement(id: 1)) @@ -34,7 +34,7 @@ final class InMemoryBatchBufferTests: XCTestCase { func testCount_withMultipleElements_shouldReturnCorrectCount() throws { // -- Arrange -- - var sut = InMemoryBatchBuffer() + var sut = InMemoryBatchBuffer(dataCapacity: 1_024 * 1_024, itemsCapacity: 100) // -- Act -- try sut.append(TestElement(id: 1)) @@ -49,7 +49,7 @@ final class InMemoryBatchBufferTests: XCTestCase { func testAppend_withSingleElement_shouldAddElement() throws { // -- Arrange -- - var sut = InMemoryBatchBuffer() + var sut = InMemoryBatchBuffer(dataCapacity: 1_024 * 1_024, itemsCapacity: 100) // -- Act -- try sut.append(TestElement(id: 1)) @@ -62,7 +62,7 @@ final class InMemoryBatchBufferTests: XCTestCase { func testAppend_withMultipleElements_shouldAddAllElements() throws { // -- Arrange -- - var sut = InMemoryBatchBuffer() + var sut = InMemoryBatchBuffer(dataCapacity: 1_024 * 1_024, itemsCapacity: 100) // -- Act -- try sut.append(TestElement(id: 1)) @@ -81,7 +81,7 @@ final class InMemoryBatchBufferTests: XCTestCase { func testAppend_withMultipleElements_shouldMaintainOrder() throws { // -- Arrange -- - var sut = InMemoryBatchBuffer() + var sut = InMemoryBatchBuffer(dataCapacity: 1_024 * 1_024, itemsCapacity: 100) // -- Act -- try sut.append(TestElement(id: 1)) @@ -97,7 +97,7 @@ final class InMemoryBatchBufferTests: XCTestCase { func testAppend_shouldIncreaseSize() throws { // -- Arrange -- - var sut = InMemoryBatchBuffer() + var sut = InMemoryBatchBuffer(dataCapacity: 1_024 * 1_024, itemsCapacity: 100) let initialSize = sut.itemsDataSize XCTAssertEqual(initialSize, 0) @@ -122,7 +122,7 @@ final class InMemoryBatchBufferTests: XCTestCase { func testFlush_withNoElements_shouldDoNothing() { // -- Arrange -- - var sut = InMemoryBatchBuffer() + var sut = InMemoryBatchBuffer(dataCapacity: 1_024 * 1_024, itemsCapacity: 100) // Assert pre-condition XCTAssertEqual(sut.itemsCount, 0) @@ -138,7 +138,7 @@ final class InMemoryBatchBufferTests: XCTestCase { func testFlush_withSingleElement_shouldClearStorage() throws { // -- Arrange -- - var sut = InMemoryBatchBuffer() + var sut = InMemoryBatchBuffer(dataCapacity: 1_024 * 1_024, itemsCapacity: 100) try sut.append(TestElement(id: 1)) // Assert pre-condition @@ -157,7 +157,7 @@ final class InMemoryBatchBufferTests: XCTestCase { func testFlush_withMultipleElements_shouldClearStorage() throws { // -- Arrange -- - var sut = InMemoryBatchBuffer() + var sut = InMemoryBatchBuffer(dataCapacity: 1_024 * 1_024, itemsCapacity: 100) try sut.append(TestElement(id: 1)) try sut.append(TestElement(id: 2)) try sut.append(TestElement(id: 3)) @@ -178,7 +178,7 @@ final class InMemoryBatchBufferTests: XCTestCase { func testFlush_afterFlush_shouldAllowNewAppends() throws { // -- Arrange -- - var sut = InMemoryBatchBuffer() + var sut = InMemoryBatchBuffer(dataCapacity: 1_024 * 1_024, itemsCapacity: 100) try sut.append(TestElement(id: 1)) try sut.append(TestElement(id: 2)) @@ -196,7 +196,7 @@ final class InMemoryBatchBufferTests: XCTestCase { func testData_withNoElements_shouldReturnEmptyArray() throws { // -- Arrange -- - let sut = InMemoryBatchBuffer() + let sut = InMemoryBatchBuffer(dataCapacity: 1_024 * 1_024, itemsCapacity: 100) // -- Act -- let data = sut.batchedData @@ -208,7 +208,7 @@ final class InMemoryBatchBufferTests: XCTestCase { func testData_withSingleElement_shouldReturnSingleElement() throws { // -- Arrange -- - var sut = InMemoryBatchBuffer() + var sut = InMemoryBatchBuffer(dataCapacity: 1_024 * 1_024, itemsCapacity: 100) try sut.append(TestElement(id: 1)) // -- Act -- @@ -221,7 +221,7 @@ final class InMemoryBatchBufferTests: XCTestCase { func testData_withMultipleElements_shouldReturnAllElements() throws { // -- Arrange -- - var sut = InMemoryBatchBuffer() + var sut = InMemoryBatchBuffer(dataCapacity: 1_024 * 1_024, itemsCapacity: 100) try sut.append(TestElement(id: 1)) try sut.append(TestElement(id: 2)) try sut.append(TestElement(id: 3)) @@ -240,7 +240,7 @@ final class InMemoryBatchBufferTests: XCTestCase { func testData_shouldReturnValidJSONFormat() throws { // -- Arrange -- - var sut = InMemoryBatchBuffer() + var sut = InMemoryBatchBuffer(dataCapacity: 1_024 * 1_024, itemsCapacity: 100) try sut.append(TestElement(id: 1)) try sut.append(TestElement(id: 2)) @@ -260,7 +260,7 @@ final class InMemoryBatchBufferTests: XCTestCase { func testData_shouldMaintainElementOrder() throws { // -- Arrange -- - var sut = InMemoryBatchBuffer() + var sut = InMemoryBatchBuffer(dataCapacity: 1_024 * 1_024, itemsCapacity: 100) try sut.append(TestElement(id: 10)) try sut.append(TestElement(id: 20)) try sut.append(TestElement(id: 30)) @@ -279,7 +279,7 @@ final class InMemoryBatchBufferTests: XCTestCase { func testSize_withNoElements_shouldReturnZero() { // -- Arrange -- - let sut = InMemoryBatchBuffer() + let sut = InMemoryBatchBuffer(dataCapacity: 1_024 * 1_024, itemsCapacity: 100) // -- Act -- let size = sut.itemsDataSize @@ -290,7 +290,7 @@ final class InMemoryBatchBufferTests: XCTestCase { func testSize_withSingleElement_shouldReturnEncodedElementSize() throws { // -- Arrange -- - var sut = InMemoryBatchBuffer() + var sut = InMemoryBatchBuffer(dataCapacity: 1_024 * 1_024, itemsCapacity: 100) let element = TestElement(id: 1) let expectedSize = try JSONEncoder().encode(element).count @@ -303,7 +303,7 @@ final class InMemoryBatchBufferTests: XCTestCase { func testSize_withMultipleElements_shouldReturnSumOfEncodedSizes() throws { // -- Arrange -- - var sut = InMemoryBatchBuffer() + var sut = InMemoryBatchBuffer(dataCapacity: 1_024 * 1_024, itemsCapacity: 100) let element1 = TestElement(id: 1) let element2 = TestElement(id: 2) let element3 = TestElement(id: 3) @@ -324,7 +324,7 @@ final class InMemoryBatchBufferTests: XCTestCase { func testSize_afterFlush_shouldReturnZero() throws { // -- Arrange -- - var sut = InMemoryBatchBuffer() + var sut = InMemoryBatchBuffer(dataCapacity: 1_024 * 1_024, itemsCapacity: 100) try sut.append(TestElement(id: 1)) try sut.append(TestElement(id: 2)) @@ -340,7 +340,7 @@ final class InMemoryBatchBufferTests: XCTestCase { func testSize_shouldUpdateAfterEachAppend() throws { // -- Arrange -- - var sut = InMemoryBatchBuffer() + var sut = InMemoryBatchBuffer(dataCapacity: 1_024 * 1_024, itemsCapacity: 100) let element1 = TestElement(id: 1) let element2 = TestElement(id: 2) let encoder = JSONEncoder() @@ -361,7 +361,7 @@ final class InMemoryBatchBufferTests: XCTestCase { func testAppendFlushAppend_shouldWorkCorrectly() throws { // -- Arrange -- - var sut = InMemoryBatchBuffer() + var sut = InMemoryBatchBuffer(dataCapacity: 1_024 * 1_024, itemsCapacity: 100) // -- Act & Assert -- try sut.append(TestElement(id: 1)) @@ -385,7 +385,7 @@ final class InMemoryBatchBufferTests: XCTestCase { func testMultipleFlushCalls_shouldNotCauseIssues() throws { // -- Arrange -- - var sut = InMemoryBatchBuffer() + var sut = InMemoryBatchBuffer(dataCapacity: 1_024 * 1_024, itemsCapacity: 100) try sut.append(TestElement(id: 1)) // -- Act -- @@ -413,7 +413,7 @@ final class InMemoryBatchBufferTests: XCTestCase { func testAppend_withDateProperty_shouldEncodeAsSecondsSince1970() throws { // -- Arrange -- - var sut = InMemoryBatchBuffer() + var sut = InMemoryBatchBuffer(dataCapacity: 1_024 * 1_024, itemsCapacity: 100) let expectedTimestamp = Date(timeIntervalSince1970: 1_234_567_890.987654) let element = TestElementWithDate(id: 1, timestamp: expectedTimestamp) @@ -442,6 +442,78 @@ final class InMemoryBatchBufferTests: XCTestCase { ) } + // MARK: - Capacity Limit Tests + + private struct TestElementWithSize: Codable { + let id: Int + let data: String + + init(id: Int, size: Int) { + self.id = id + // Create a string of approximately the desired size + // JSON encoding adds overhead, so we adjust + let baseSize = "{\"id\":\(id),\"data\":\"".utf8.count + "\"}".utf8.count + let dataSize = max(0, size - baseSize) + self.data = String(repeating: "x", count: dataSize) + } + } + + func testAppend_whenDoubleDataCapacity_shouldSucceed() throws { + // -- Arrange -- + let dataCapacity = 100 + var sut = InMemoryBatchBuffer(dataCapacity: dataCapacity, itemsCapacity: 10) + + let elementSize = dataCapacity * 2 + let element = TestElementWithSize(id: 1, size: elementSize) + let encoded = try JSONEncoder().encode(element) + + // Verify the encoded size is what we expect (slightly over dataCapacity) + XCTAssertGreaterThan(encoded.count, dataCapacity) + XCTAssertLessThan(encoded.count, dataCapacity * 2) + + // -- Act -- + try sut.append(element) + + // -- Assert -- + XCTAssertEqual(sut.itemsCount, 1) + XCTAssertGreaterThan(sut.itemsDataSize, dataCapacity) + XCTAssertLessThan(sut.itemsDataSize, dataCapacity * 2) + } + + func testAppend_whenGreaterDoubleDataCapacity_shouldThrowBufferFull() throws { + // -- Arrange -- + let dataCapacity = 100 + var sut = InMemoryBatchBuffer(dataCapacity: dataCapacity, itemsCapacity: 10) + + let elementSize = dataCapacity * 2 + 1 + let element = TestElementWithSize(id: 1, size: elementSize) + + // -- Act & Assert -- + XCTAssertThrowsError(try sut.append(element)) { error in + XCTAssertEqual(error as? BatchBufferError, BatchBufferError.bufferFull) + } + XCTAssertEqual(sut.itemsCount, 0) + } + + func testAppend_whenItemsCapacityReached_shouldThrowBufferFull() throws { + // -- Arrange -- + let itemsCapacity = 3 + var sut = InMemoryBatchBuffer(dataCapacity: 1_024 * 1_024, itemsCapacity: itemsCapacity) + + // -- Act -- + try sut.append(TestElement(id: 1)) + try sut.append(TestElement(id: 2)) + try sut.append(TestElement(id: 3)) + + XCTAssertEqual(sut.itemsCount, itemsCapacity) + + // -- Act & Assert -- + XCTAssertThrowsError(try sut.append(TestElement(id: 4))) { error in + XCTAssertEqual(error as? BatchBufferError, BatchBufferError.bufferFull) + } + XCTAssertEqual(sut.itemsCount, itemsCapacity) + } + // MARK: - Helpers private func decodePayload(data: Data) throws -> TestPayload {