Skip to content

Commit 26b8a8a

Browse files
committed
Fix pCloud swapped move tuple, harden integration test setUp for eventual consistency
1 parent 79b2a3a commit 26b8a8a

7 files changed

Lines changed: 114 additions & 5 deletions

File tree

Sources/CryptomatorCloudAccess/PCloud/PCloudCloudProvider.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -404,8 +404,8 @@ public class PCloudCloudProvider: CloudProvider {
404404
private func resolveForMove(from sourceCloudPath: CloudPath, to targetCloudPath: CloudPath) -> Promise<(PCloudItem, PCloudItem)> {
405405
return resolveParentPath(forItemAt: targetCloudPath).then { targetParentItem -> Promise<(PCloudItem, PCloudItem)> in
406406
return all(
407-
self.checkForNameCollision(targetCloudPath.lastPathComponent, inFolder: targetParentItem).then { targetParentItem },
408-
self.resolvePath(forItemAt: sourceCloudPath)
407+
self.resolvePath(forItemAt: sourceCloudPath),
408+
self.checkForNameCollision(targetCloudPath.lastPathComponent, inFolder: targetParentItem).then { targetParentItem }
409409
)
410410
}
411411
}

Tests/CryptomatorCloudAccessIntegrationTests/CloudAccessIntegrationTest.swift

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ class CloudAccessIntegrationTest: XCTestCase {
6060
}
6161

6262
override class func tearDown() {
63-
_ = setUpProvider.deleteFolder(at: integrationTestRootCloudPath).then {
63+
guard let provider = setUpProvider else { return }
64+
_ = provider.deleteFolder(at: integrationTestRootCloudPath).then {
6465
setUpProvider = nil
6566
}
6667
_ = waitForPromises(timeout: 60.0)
@@ -224,9 +225,9 @@ class CloudAccessIntegrationTest: XCTestCase {
224225
FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDirectory)
225226
let cloudPath = integrationTestRootCloudPath.appendingPathComponent(nextObject)
226227
if isDirectory.boolValue {
227-
try awaitPromise(provider.createFolder(at: cloudPath))
228+
try retryOnEventualConsistencyError { try awaitPromise(provider.createFolder(at: cloudPath)) }
228229
} else {
229-
_ = try awaitPromise(provider.uploadFile(from: fileURL, to: cloudPath, replaceExisting: false))
230+
try retryOnEventualConsistencyError { _ = try awaitPromise(provider.uploadFile(from: fileURL, to: cloudPath, replaceExisting: false)) }
230231
}
231232
}
232233
fulfill(())
@@ -236,6 +237,41 @@ class CloudAccessIntegrationTest: XCTestCase {
236237
}
237238
}
238239

240+
/// Retries an operation that fails due to eventual consistency (e.g. S3).
241+
/// During setUp, `parentFolderDoesNotExist` and `itemNotFound` indicate that a just-created parent
242+
/// folder or its directory metadata hasn't propagated yet.
243+
private static func retryOnEventualConsistencyError(maxAttempts: Int = 10, operation: () throws -> Void) throws {
244+
var lastError: Error = CloudProviderError.parentFolderDoesNotExist
245+
for attempt in 0 ..< maxAttempts {
246+
do {
247+
try operation()
248+
return
249+
} catch CloudProviderError.parentFolderDoesNotExist, CloudProviderError.itemNotFound {
250+
lastError = CloudProviderError.parentFolderDoesNotExist
251+
if attempt == maxAttempts - 1 {
252+
throw lastError
253+
}
254+
Thread.sleep(forTimeInterval: 1.0)
255+
}
256+
}
257+
}
258+
259+
/// Polls `fetchItemList` until the expected number of items is visible, retrying up to 10 times with a 1-second delay.
260+
/// Used after setUp uploads test fixtures to wait for eventual consistency (e.g. S3, pCloud).
261+
static func waitForConsistency(provider: CloudProvider, folderPath: CloudPath, expectedItemCount: Int, attempt: Int = 0) -> Promise<Void> {
262+
return provider.fetchItemList(forFolderAt: folderPath, withPageToken: nil).then { itemList -> Promise<Void> in
263+
if itemList.items.count >= expectedItemCount {
264+
return Promise(())
265+
}
266+
if attempt >= 10 {
267+
return Promise(IntegrationTestError.consistencyTimeout)
268+
}
269+
return Promise(()).delay(1.0).then {
270+
return waitForConsistency(provider: provider, folderPath: folderPath, expectedItemCount: expectedItemCount, attempt: attempt + 1)
271+
}
272+
}
273+
}
274+
239275
// MARK: - fetchItemMetadata Tests
240276

241277
func testFetchItemMetadataForFile() {

Tests/CryptomatorCloudAccessIntegrationTests/CryptoDecorator/VaultFormat6/VaultFormat6PCloudIntegrationTests.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ class VaultFormat6PCloudIntegrationTests: CloudAccessIntegrationTest {
4242
return
4343
}
4444
super.setUp()
45+
guard classSetUpError == nil else { return }
46+
// Wait for pCloud's eventual consistency to catch up after setUp uploaded all test fixtures.
47+
let expectedItemCount = 6 // 5 files (test 0-4.txt) + 1 folder (testFolder)
48+
_ = waitForConsistency(provider: setUpProvider, folderPath: integrationTestRootCloudPath, expectedItemCount: expectedItemCount)
49+
guard waitForPromises(timeout: 60.0) else {
50+
classSetUpError = IntegrationTestError.oneTimeSetUpTimeout
51+
return
52+
}
4553
}
4654

4755
override func setUpWithError() throws {

Tests/CryptomatorCloudAccessIntegrationTests/CryptoDecorator/VaultFormat6/VaultFormat6S3IntegrationTests.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,35 @@ class VaultFormat6S3IntegrationTests: CloudAccessIntegrationTest {
4141
classSetUpError = error
4242
return
4343
}
44+
// Wait for Scaleway S3's eventual consistency to catch up after vault creation.
45+
_ = waitForVaultReadiness(provider: setUpProvider)
46+
guard waitForPromises(timeout: 60.0) else {
47+
classSetUpError = IntegrationTestError.oneTimeSetUpTimeout
48+
return
49+
}
4450
super.setUp()
51+
guard classSetUpError == nil else { return }
52+
// Wait for Scaleway S3's eventual consistency to catch up after setUp uploaded all test fixtures.
53+
let expectedItemCount = 6 // 5 files (test 0-4.txt) + 1 folder (testFolder)
54+
_ = waitForConsistency(provider: setUpProvider, folderPath: integrationTestRootCloudPath, expectedItemCount: expectedItemCount)
55+
guard waitForPromises(timeout: 60.0) else {
56+
classSetUpError = IntegrationTestError.oneTimeSetUpTimeout
57+
return
58+
}
59+
}
60+
61+
private static func waitForVaultReadiness(provider: CloudProvider, attempt: Int = 0) -> Promise<Void> {
62+
let probePath = CloudPath("/.s3-consistency-probe")
63+
return provider.createFolderIfMissing(at: probePath).then {
64+
return provider.deleteFolderIfExisting(at: probePath)
65+
}.recover { error -> Promise<Void> in
66+
if attempt >= 10 {
67+
return Promise(error)
68+
}
69+
return Promise(()).delay(2.0).then {
70+
return waitForVaultReadiness(provider: provider, attempt: attempt + 1)
71+
}
72+
}
4573
}
4674

4775
override func setUpWithError() throws {

Tests/CryptomatorCloudAccessIntegrationTests/CryptoDecorator/VaultFormat7/VaultFormat7PCloudIntegrationTests.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ class VaultFormat7PCloudIntegrationTests: CloudAccessIntegrationTest {
4343
return
4444
}
4545
super.setUp()
46+
guard classSetUpError == nil else { return }
47+
// Wait for pCloud's eventual consistency to catch up after setUp uploaded all test fixtures.
48+
let expectedItemCount = 6 // 5 files (test 0-4.txt) + 1 folder (testFolder)
49+
_ = waitForConsistency(provider: setUpProvider, folderPath: integrationTestRootCloudPath, expectedItemCount: expectedItemCount)
50+
guard waitForPromises(timeout: 60.0) else {
51+
classSetUpError = IntegrationTestError.oneTimeSetUpTimeout
52+
return
53+
}
4654
}
4755

4856
override func setUpWithError() throws {

Tests/CryptomatorCloudAccessIntegrationTests/CryptoDecorator/VaultFormat7/VaultFormat7S3IntegrationTests.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,35 @@ class VaultFormat7S3IntegrationTests: CloudAccessIntegrationTest {
4141
classSetUpError = error
4242
return
4343
}
44+
// Wait for Scaleway S3's eventual consistency to catch up after vault creation.
45+
_ = waitForVaultReadiness(provider: setUpProvider)
46+
guard waitForPromises(timeout: 60.0) else {
47+
classSetUpError = IntegrationTestError.oneTimeSetUpTimeout
48+
return
49+
}
4450
super.setUp()
51+
guard classSetUpError == nil else { return }
52+
// Wait for Scaleway S3's eventual consistency to catch up after setUp uploaded all test fixtures.
53+
let expectedItemCount = 6 // 5 files (test 0-4.txt) + 1 folder (testFolder)
54+
_ = waitForConsistency(provider: setUpProvider, folderPath: integrationTestRootCloudPath, expectedItemCount: expectedItemCount)
55+
guard waitForPromises(timeout: 60.0) else {
56+
classSetUpError = IntegrationTestError.oneTimeSetUpTimeout
57+
return
58+
}
59+
}
60+
61+
private static func waitForVaultReadiness(provider: CloudProvider, attempt: Int = 0) -> Promise<Void> {
62+
let probePath = CloudPath("/.s3-consistency-probe")
63+
return provider.createFolderIfMissing(at: probePath).then {
64+
return provider.deleteFolderIfExisting(at: probePath)
65+
}.recover { error -> Promise<Void> in
66+
if attempt >= 10 {
67+
return Promise(error)
68+
}
69+
return Promise(()).delay(2.0).then {
70+
return waitForVaultReadiness(provider: provider, attempt: attempt + 1)
71+
}
72+
}
4573
}
4674

4775
override func setUpWithError() throws {

Tests/CryptomatorCloudAccessIntegrationTests/IntegrationTestError.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import Foundation
1111
enum IntegrationTestError: Error {
1212
case cloudProviderInitError
1313
case missingDirectoryEnumerator
14+
case consistencyTimeout
1415
case oneTimeSetUpTimeout
1516
case setUpTimeout
1617
}

0 commit comments

Comments
 (0)