From 4f14503400242d805e34e639add181429c465337 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Thu, 18 Dec 2025 19:33:56 -0300 Subject: [PATCH 1/3] chore: Update version to 3.5.2-rc2 --- Split.podspec | 2 +- Split/Common/Utils/Version.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Split.podspec b/Split.podspec index 698b5d22..4b7d37b5 100644 --- a/Split.podspec +++ b/Split.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = 'Split' s.module_name = 'Split' - s.version = '3.5.2-rc1' + s.version = '3.5.2-rc2' s.summary = 'iOS SDK for Split' s.description = <<-DESC This SDK is designed to work with Split, the platform for controlled rollouts, serving features to your users via the Split feature flag to manage your complete customer experience. diff --git a/Split/Common/Utils/Version.swift b/Split/Common/Utils/Version.swift index a7dba831..eb82f2f8 100644 --- a/Split/Common/Utils/Version.swift +++ b/Split/Common/Utils/Version.swift @@ -10,7 +10,7 @@ import Foundation class Version { private static let kSdkPlatform: String = "ios" - private static let kVersion = "3.5.2-rc1" + private static let kVersion = "3.5.2-rc2" static var semantic: String { return kVersion From 05e033ecd1904f7d8ff66deb1e0e3da33865a918 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Thu, 18 Dec 2025 19:34:39 -0300 Subject: [PATCH 2/3] Use async write in flags transaction --- Split/Storage/CoreDataHelper.swift | 8 +++++++ .../Splits/PersistentSplitsStorage.swift | 7 +++++-- .../Fake/Storage/CoreDataHelperStub.swift | 5 +++++ SplitTests/Storage/CoreDataHelperTests.swift | 21 +++++++++++++++++++ ...stentSplitsStorageTransactionalTests.swift | 15 +++++++++++++ 5 files changed, 54 insertions(+), 2 deletions(-) diff --git a/Split/Storage/CoreDataHelper.swift b/Split/Storage/CoreDataHelper.swift index 713b8e9f..f8f7aa54 100644 --- a/Split/Storage/CoreDataHelper.swift +++ b/Split/Storage/CoreDataHelper.swift @@ -122,6 +122,14 @@ class CoreDataHelper { } } + /// Roll back any unsaved changes in the managed object context. + /// Useful after a failed save(), to prevent the context from keeping invalid pending changes. + func rollback() { + managedObjectContext.performAndWait { + self.managedObjectContext.rollback() + } + } + private func delete(entity: CoreDataEntity, predicate: NSPredicate? = nil) { managedObjectContext.performAndWait { diff --git a/Split/Storage/Splits/PersistentSplitsStorage.swift b/Split/Storage/Splits/PersistentSplitsStorage.swift index 88a122f1..c0ce94d9 100644 --- a/Split/Storage/Splits/PersistentSplitsStorage.swift +++ b/Split/Storage/Splits/PersistentSplitsStorage.swift @@ -38,8 +38,9 @@ class DefaultPersistentSplitsStorage: PersistentSplitsStorage { } func update(splitChange: ProcessedSplitChange, onFailure: ((Error) -> Void)? = nil) { - // Execute transactionally: all operations must succeed or all must fail - coreDataHelper.performAndWait { [weak self] in + // This is intentionally async to avoid blocking the caller thread. + // All operations must succeed or all must fail. + coreDataHelper.perform { [weak self] in guard let self = self else { return } do { @@ -55,6 +56,8 @@ class DefaultPersistentSplitsStorage: PersistentSplitsStorage { try self.coreDataHelper.saveWithErrorHandling() } catch { Logger.e("Transactional flags update failed: \(error.localizedDescription)") + // Rollback to avoid leaving invalid pending changes in the shared context, + self.coreDataHelper.rollback() onFailure?(error) } } diff --git a/SplitTests/Fake/Storage/CoreDataHelperStub.swift b/SplitTests/Fake/Storage/CoreDataHelperStub.swift index 0dd736f5..aead6c42 100644 --- a/SplitTests/Fake/Storage/CoreDataHelperStub.swift +++ b/SplitTests/Fake/Storage/CoreDataHelperStub.swift @@ -11,6 +11,7 @@ class CoreDataHelperStub: CoreDataHelper { var shouldFailOnSave = false var saveError: Error = NSError(domain: "TestCoreData", code: 500, userInfo: [NSLocalizedDescriptionKey: "Simulated save failure"]) + var rollbackCalled = false init() { let model = NSManagedObjectModel() @@ -39,5 +40,9 @@ class CoreDataHelperStub: CoreDataHelper { } // Success } + + override func rollback() { + rollbackCalled = true + } } diff --git a/SplitTests/Storage/CoreDataHelperTests.swift b/SplitTests/Storage/CoreDataHelperTests.swift index c2c6bae8..c41c111e 100644 --- a/SplitTests/Storage/CoreDataHelperTests.swift +++ b/SplitTests/Storage/CoreDataHelperTests.swift @@ -80,5 +80,26 @@ class CoreDataHelperTests: XCTestCase { // Context without persistent store should not throw when there are no changes XCTAssertNoThrow(try invalidHelper.saveWithErrorHandling()) } + + func testRollbackClearsInvalidPendingChangesAndAllowsNextSave() { + // Create an invalid entity that will cause a validation failure on save. + coreDataHelper.performAndWait { + _ = self.coreDataHelper.create(entity: .generalInfo) + } + + XCTAssertThrowsError(try coreDataHelper.saveWithErrorHandling()) + + // Rollback should clear the invalid pending changes so future saves can succeed. + coreDataHelper.rollback() + + coreDataHelper.performAndWait { + if let entity = self.coreDataHelper.create(entity: .generalInfo) as? GeneralInfoEntity { + entity.name = "post_rollback_ok" + entity.stringValue = "value" + } + } + + XCTAssertNoThrow(try coreDataHelper.saveWithErrorHandling()) + } } diff --git a/SplitTests/Storage/PersistentSplitsStorageTransactionalTests.swift b/SplitTests/Storage/PersistentSplitsStorageTransactionalTests.swift index 76856bb7..732f625c 100644 --- a/SplitTests/Storage/PersistentSplitsStorageTransactionalTests.swift +++ b/SplitTests/Storage/PersistentSplitsStorageTransactionalTests.swift @@ -65,6 +65,21 @@ class PersistentSplitsStorageTransactionalTests: XCTestCase { XCTAssertNotNil(reportedError, "Error should be reported") } + func testRollbackIsInvokedOnSaveError() { + coreDataHelperStub.shouldFailOnSave = true + + let change = ProcessedSplitChange( + activeSplits: [createSplit(name: "split1")], + archivedSplits: [], + changeNumber: 100, + updateTimestamp: 1000 + ) + + splitsStorage.update(splitChange: change, onFailure: { _ in }) + + XCTAssertTrue(coreDataHelperStub.rollbackCalled, "Rollback should be invoked when transactional save fails") + } + func testNilFailureCallbackIsHandled() { let change = ProcessedSplitChange( activeSplits: [createSplit(name: "split1")], From 066ca1592f00eff582d25760210c4eb6a17ba8ab Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Thu, 18 Dec 2025 19:55:34 -0300 Subject: [PATCH 3/3] Fix test --- .../EncryptionKeyValidationIntegrationTest.swift | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/SplitTests/Integration/EncryptionKeyValidationIntegrationTest.swift b/SplitTests/Integration/EncryptionKeyValidationIntegrationTest.swift index e601cbe7..5360b692 100644 --- a/SplitTests/Integration/EncryptionKeyValidationIntegrationTest.swift +++ b/SplitTests/Integration/EncryptionKeyValidationIntegrationTest.swift @@ -243,14 +243,19 @@ class EncryptionKeyValidationIntegrationTest: XCTestCase { } wait(for: [checkExp], timeout: 3) - // 2. Disable encryption + // 2. Create a fresh dbHelper + dbHelper = IntegrationCoreDataHelper.get(databaseName: testDbName, + dispatchQueue: DispatchQueue.global()) + let freshGeneralInfoDao = CoreDataGeneralInfoDao(coreDataHelper: dbHelper) + + // 3. Disable encryption let factory = createFactory(encryptionEnabled: false) waitForReady(factory: factory) - // 3. Verify verifier is removed + // 4. Verify verifier is removed let verifyExp = expectation(description: "Verifier removed") DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) { - XCTAssertNil(generalInfoDao.stringValue(info: .encryptionVerifier)) + XCTAssertNil(freshGeneralInfoDao.stringValue(info: .encryptionVerifier)) verifyExp.fulfill() } wait(for: [verifyExp], timeout: 3)