Skip to content

Commit 98d4ad1

Browse files
authored
[Local Catalog] Fix flaky and slow POS catalog sync tests (#16227)
2 parents 67041f2 + ef47178 commit 98d4ad1

File tree

7 files changed

+74
-48
lines changed

7 files changed

+74
-48
lines changed

Modules/Sources/Yosemite/Tools/POS/POSCatalogFullSyncService.swift

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ public struct POSCatalog {
2828
// periphery:ignore
2929
public final class POSCatalogFullSyncService: POSCatalogFullSyncServiceProtocol {
3030
private let syncRemote: POSCatalogSyncRemoteProtocol
31-
private let batchSize: Int
3231
private let persistenceService: POSCatalogPersistenceServiceProtocol
3332
private let batchedLoader: BatchedRequestLoader
3433

@@ -49,11 +48,15 @@ public final class POSCatalogFullSyncService: POSCatalogFullSyncServiceProtocol
4948
self.init(syncRemote: syncRemote, batchSize: batchSize, persistenceService: persistenceService)
5049
}
5150

52-
init(syncRemote: POSCatalogSyncRemoteProtocol, batchSize: Int, persistenceService: POSCatalogPersistenceServiceProtocol) {
51+
init(
52+
syncRemote: POSCatalogSyncRemoteProtocol,
53+
batchSize: Int,
54+
retryDelay: TimeInterval = 2.0,
55+
persistenceService: POSCatalogPersistenceServiceProtocol
56+
) {
5357
self.syncRemote = syncRemote
54-
self.batchSize = batchSize
5558
self.persistenceService = persistenceService
56-
self.batchedLoader = BatchedRequestLoader(batchSize: batchSize)
59+
self.batchedLoader = BatchedRequestLoader(batchSize: batchSize, retryDelay: retryDelay)
5760
}
5861

5962
// MARK: - Protocol Conformance

Modules/Sources/Yosemite/Tools/POS/POSCatalogIncrementalSyncService.swift

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ public protocol POSCatalogIncrementalSyncServiceProtocol {
2020
// periphery:ignore
2121
public final class POSCatalogIncrementalSyncService: POSCatalogIncrementalSyncServiceProtocol {
2222
private let syncRemote: POSCatalogSyncRemoteProtocol
23-
private let batchSize: Int
2423
private let persistenceService: POSCatalogPersistenceServiceProtocol
2524
private let batchedLoader: BatchedRequestLoader
2625

@@ -41,11 +40,15 @@ public final class POSCatalogIncrementalSyncService: POSCatalogIncrementalSyncSe
4140
self.init(syncRemote: syncRemote, batchSize: batchSize, persistenceService: persistenceService)
4241
}
4342

44-
init(syncRemote: POSCatalogSyncRemoteProtocol, batchSize: Int, persistenceService: POSCatalogPersistenceServiceProtocol) {
43+
init(
44+
syncRemote: POSCatalogSyncRemoteProtocol,
45+
batchSize: Int,
46+
retryDelay: TimeInterval = 2.0,
47+
persistenceService: POSCatalogPersistenceServiceProtocol
48+
) {
4549
self.syncRemote = syncRemote
46-
self.batchSize = batchSize
4750
self.persistenceService = persistenceService
48-
self.batchedLoader = BatchedRequestLoader(batchSize: batchSize)
51+
self.batchedLoader = BatchedRequestLoader(batchSize: batchSize, retryDelay: retryDelay)
4952
}
5053

5154
// MARK: - Protocol Conformance

Modules/Tests/YosemiteTests/Mocks/MockPOSCatalogSyncRemote.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ final class MockPOSCatalogSyncRemote: POSCatalogSyncRemoteProtocol {
88
private(set) var incrementalProductResults: [Int: Result<PagedItems<POSProduct>, Error>] = [:]
99
private(set) var incrementalVariationResults: [Int: Result<PagedItems<POSProductVariation>, Error>] = [:]
1010

11-
private(set) var loadProductsCallCount = 0
12-
private(set) var loadProductVariationsCallCount = 0
13-
private(set) var loadIncrementalProductsCallCount = 0
14-
private(set) var loadIncrementalProductVariationsCallCount = 0
11+
let loadProductsCallCount = Counter()
12+
let loadProductVariationsCallCount = Counter()
13+
let loadIncrementalProductsCallCount = Counter()
14+
let loadIncrementalProductVariationsCallCount = Counter()
1515

1616
private(set) var lastIncrementalProductsModifiedAfter: Date?
1717
private(set) var lastIncrementalVariationsModifiedAfter: Date?
@@ -67,7 +67,7 @@ final class MockPOSCatalogSyncRemote: POSCatalogSyncRemoteProtocol {
6767
// MARK: - Protocol Methods - Incremental Sync
6868

6969
func loadProducts(modifiedAfter: Date, siteID: Int64, pageNumber: Int) async throws -> PagedItems<POSProduct> {
70-
loadIncrementalProductsCallCount += 1
70+
await loadIncrementalProductsCallCount.increment()
7171
lastIncrementalProductsModifiedAfter = modifiedAfter
7272

7373
if let result = incrementalProductResults[pageNumber] {
@@ -82,7 +82,7 @@ final class MockPOSCatalogSyncRemote: POSCatalogSyncRemoteProtocol {
8282
}
8383

8484
func loadProductVariations(modifiedAfter: Date, siteID: Int64, pageNumber: Int) async throws -> PagedItems<POSProductVariation> {
85-
loadIncrementalProductVariationsCallCount += 1
85+
await loadIncrementalProductVariationsCallCount.increment()
8686
lastIncrementalVariationsModifiedAfter = modifiedAfter
8787

8888
if let result = incrementalVariationResults[pageNumber] {
@@ -99,7 +99,7 @@ final class MockPOSCatalogSyncRemote: POSCatalogSyncRemoteProtocol {
9999
// MARK: - Protocol Methods - Full Sync
100100

101101
func loadProducts(siteID: Int64, pageNumber: Int) async throws -> PagedItems<POSProduct> {
102-
loadProductsCallCount += 1
102+
await loadProductsCallCount.increment()
103103

104104
if let result = productResults[pageNumber] {
105105
switch result {
@@ -113,7 +113,7 @@ final class MockPOSCatalogSyncRemote: POSCatalogSyncRemoteProtocol {
113113
}
114114

115115
func loadProductVariations(siteID: Int64, pageNumber: Int) async throws -> PagedItems<POSProductVariation> {
116-
loadProductVariationsCallCount += 1
116+
await loadProductVariationsCallCount.increment()
117117

118118
if let result = variationResults[pageNumber] {
119119
switch result {

Modules/Tests/YosemiteTests/Tools/POS/BatchedRequestLoaderTests.swift

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ struct BatchedRequestLoaderTests {
1111
// Given
1212
let sut = BatchedRequestLoader(batchSize: 2)
1313
let expectedItems = ["item1", "item2", "item3"]
14-
var callCount = 0
14+
let callCount = Counter()
1515

1616
let makeRequest: (Int) async throws -> PagedItems<String> = { pageNumber in
17-
callCount += 1
17+
await callCount.increment()
1818
#expect(pageNumber == 1 || pageNumber == 2)
1919
if pageNumber == 1 {
2020
return PagedItems(items: expectedItems, hasMorePages: false, totalItems: 3)
@@ -28,7 +28,7 @@ struct BatchedRequestLoaderTests {
2828

2929
// Then
3030
#expect(result == expectedItems)
31-
#expect(callCount == 2) // batchSize = 2, so it fetches pages 1 and 2
31+
#expect(await callCount.value == 2) // batchSize = 2, so it fetches pages 1 and 2
3232
}
3333

3434
// MARK: - Retry Logic Tests
@@ -43,13 +43,23 @@ struct BatchedRequestLoaderTests {
4343
errorEvaluator: mockErrorEvaluator
4444
)
4545

46-
var attemptsByPage: [Int: Int] = [:]
46+
let attemptCountForPage1 = Counter()
47+
let attemptCountForPage2 = Counter()
48+
let attemptCountForPage3 = Counter()
4749
let makeRequest: (Int) async throws -> PagedItems<String> = { pageNumber in
48-
attemptsByPage[pageNumber, default: 0] += 1
49-
50-
// Page 2 fails once, then succeeds
51-
if pageNumber == 2 && attemptsByPage[pageNumber] == 1 {
52-
throw URLError(.timedOut)
50+
switch pageNumber {
51+
case 1:
52+
await attemptCountForPage1.increment()
53+
case 2:
54+
await attemptCountForPage2.increment()
55+
// Page 2 fails once, then succeeds
56+
if await attemptCountForPage2.value == 1 {
57+
throw URLError(.timedOut)
58+
}
59+
case 3:
60+
await attemptCountForPage3.increment()
61+
default:
62+
throw NSError(domain: "Invalid page number", code: 0)
5363
}
5464

5565
return PagedItems(items: ["page \(pageNumber) success"], hasMorePages: false, totalItems: 1)
@@ -60,9 +70,9 @@ struct BatchedRequestLoaderTests {
6070

6171
// Then
6272
#expect(result == ["page 1 success", "page 2 success", "page 3 success"])
63-
#expect(attemptsByPage[1] == 1) // Page 1 succeeded immediately
64-
#expect(attemptsByPage[2] == 2) // Page 2 failed once, then succeeded
65-
#expect(attemptsByPage[3] == 1) // Page 3 succeeded immediately
73+
#expect(await attemptCountForPage1.value == 1) // Page 1 succeeded immediately
74+
#expect(await attemptCountForPage2.value == 2) // Page 2 failed once, then succeeded
75+
#expect(await attemptCountForPage3.value == 1) // Page 3 succeeded immediately
6676
}
6777

6878
@Test func loadAll_does_not_retry_on_non_retryable_error() async throws {
@@ -75,17 +85,17 @@ struct BatchedRequestLoaderTests {
7585
errorEvaluator: mockErrorEvaluator
7686
)
7787

78-
var attemptCount = 0
88+
let attemptCount = Counter()
7989
let makeRequest: (Int) async throws -> PagedItems<String> = { pageNumber in
80-
attemptCount += 1
90+
await attemptCount.increment()
8191
throw NetworkError.unacceptableStatusCode(statusCode: 401, response: nil)
8292
}
8393

8494
// When/Then
8595
await #expect(throws: NetworkError.self) {
8696
try await sut.loadAll(makeRequest: makeRequest)
8797
}
88-
#expect(attemptCount == 2) // No retries for non-retryable error for both pages
98+
#expect(await attemptCount.value == 2) // No retries for non-retryable error for both pages
8999
}
90100

91101
@Test func loadAll_fails_after_max_retries() async throws {
@@ -98,18 +108,18 @@ struct BatchedRequestLoaderTests {
98108
errorEvaluator: mockErrorEvaluator
99109
)
100110

101-
var attemptCount = 0
111+
let attemptCount = Counter()
102112
let expectedError = URLError(.networkConnectionLost)
103113
let makeRequest: (Int) async throws -> PagedItems<String> = { pageNumber in
104-
attemptCount += 1
114+
await attemptCount.increment()
105115
throw expectedError
106116
}
107117

108118
// When/Then
109119
await #expect(throws: URLError.self) {
110120
try await sut.loadAll(makeRequest: makeRequest)
111121
}
112-
#expect(attemptCount == 3) // maxRetries = 3
122+
#expect(await attemptCount.value == 3) // maxRetries = 3
113123
}
114124
}
115125

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/// A simple counter actor to track increments in a thread-safe manner.
2+
actor Counter {
3+
var value = 0
4+
5+
func increment() {
6+
value += 1
7+
}
8+
}

Modules/Tests/YosemiteTests/Tools/POS/POSCatalogFullSyncServiceTests.swift

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ struct POSCatalogFullSyncServiceTests {
3333
// Then
3434
#expect(result.products.count == expectedProducts.count)
3535
#expect(result.variations.count == expectedVariations.count)
36-
#expect(mockSyncRemote.loadProductsCallCount == 2) // 1 batch of 2 requests
37-
#expect(mockSyncRemote.loadProductVariationsCallCount == 2) // 1 batch of 2 requests
36+
#expect(await mockSyncRemote.loadProductsCallCount.value == 2) // 1 batch of 2 requests
37+
#expect(await mockSyncRemote.loadProductVariationsCallCount.value == 2) // 1 batch of 2 requests
3838
}
3939

4040
@Test func startFullSync_handles_paginated_products_correctly() async throws {
@@ -54,8 +54,8 @@ struct POSCatalogFullSyncServiceTests {
5454

5555
// Then
5656
#expect(result.products.count == 3)
57-
#expect(mockSyncRemote.loadProductsCallCount == 4) // 2 batches of 2 requests
58-
#expect(mockSyncRemote.loadProductVariationsCallCount == 2) // 1 batch of 2 requests
57+
#expect(await mockSyncRemote.loadProductsCallCount.value == 4) // 2 batches of 2 requests
58+
#expect(await mockSyncRemote.loadProductVariationsCallCount.value == 2) // 1 batch of 2 requests
5959
}
6060

6161
@Test func startFullSync_handles_paginated_variations_correctly() async throws {
@@ -75,8 +75,8 @@ struct POSCatalogFullSyncServiceTests {
7575

7676
// Then
7777
#expect(result.variations.count == 4)
78-
#expect(mockSyncRemote.loadProductsCallCount == 2) // 1 batch of 2 requests
79-
#expect(mockSyncRemote.loadProductVariationsCallCount == 4) // 2 batches of 2 requests
78+
#expect(await mockSyncRemote.loadProductsCallCount.value == 2) // 1 batch of 2 requests
79+
#expect(await mockSyncRemote.loadProductVariationsCallCount.value == 4) // 2 batches of 2 requests
8080
}
8181

8282
@Test func startFullSync_stops_pagination_when_no_new_items_returned_and_hasMorePages_is_inaccurate() async throws {
@@ -95,7 +95,7 @@ struct POSCatalogFullSyncServiceTests {
9595

9696
// Then - Should stop after empty page
9797
#expect(result.products.count == 1)
98-
#expect(mockSyncRemote.loadProductsCallCount == 4) // The results from the second batch are empty
98+
#expect(await mockSyncRemote.loadProductsCallCount.value == 4) // The results from the second batch are empty
9999
}
100100

101101
@Test func startFullSync_handles_batch_processing_correctly() async throws {
@@ -116,13 +116,14 @@ struct POSCatalogFullSyncServiceTests {
116116

117117
// Then
118118
#expect(result.products.count == 5)
119-
#expect(mockSyncRemote.loadProductsCallCount == 6)
119+
#expect(await mockSyncRemote.loadProductsCallCount.value == 6)
120120
}
121121

122122
@Test func startFullSync_propagates_network_errors() async throws {
123123
// Given
124124
let expectedError = NSError(domain: "network", code: 500, userInfo: [NSLocalizedDescriptionKey: "Network error"])
125125
mockSyncRemote.setProductResult(pageNumber: 1, result: .failure(expectedError))
126+
let sut = POSCatalogFullSyncService(syncRemote: mockSyncRemote, batchSize: 2, retryDelay: 0, persistenceService: mockPersistenceService)
126127

127128
// When/Then
128129
await #expect(throws: expectedError) {
@@ -176,7 +177,7 @@ struct POSCatalogFullSyncServiceTests {
176177
_ = try await service.startFullSync(for: sampleSiteID)
177178

178179
// Then
179-
#expect(mockSyncRemote.loadProductsCallCount == 5)
180-
#expect(mockSyncRemote.loadProductVariationsCallCount == 5)
180+
#expect(await mockSyncRemote.loadProductsCallCount.value == 5)
181+
#expect(await mockSyncRemote.loadProductVariationsCallCount.value == 5)
181182
}
182183
}

Modules/Tests/YosemiteTests/Tools/POS/POSCatalogIncrementalSyncServiceTests.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ struct POSCatalogIncrementalSyncServiceTests {
3131
try await sut.startIncrementalSync(for: sampleSiteID, lastFullSyncDate: lastFullSyncDate, lastIncrementalSyncDate: nil)
3232

3333
// Then
34-
#expect(mockSyncRemote.loadIncrementalProductsCallCount == 2)
35-
#expect(mockSyncRemote.loadIncrementalProductVariationsCallCount == 2)
34+
#expect(await mockSyncRemote.loadIncrementalProductsCallCount.value == 2)
35+
#expect(await mockSyncRemote.loadIncrementalProductVariationsCallCount.value == 2)
3636
#expect(mockSyncRemote.lastIncrementalProductsModifiedAfter == lastFullSyncDate)
3737
#expect(mockSyncRemote.lastIncrementalVariationsModifiedAfter == lastFullSyncDate)
3838
#expect(mockPersistenceService.persistIncrementalCatalogDataCallCount == 1)
@@ -76,7 +76,7 @@ struct POSCatalogIncrementalSyncServiceTests {
7676
try await sut.startIncrementalSync(for: sampleSiteID, lastFullSyncDate: lastFullSyncDate, lastIncrementalSyncDate: nil)
7777

7878
// Then
79-
#expect(mockSyncRemote.loadIncrementalProductsCallCount == 4)
79+
#expect(await mockSyncRemote.loadIncrementalProductsCallCount.value == 4)
8080
let persistedCatalog = try #require(mockPersistenceService.persistIncrementalCatalogDataLastPersistedCatalog)
8181
#expect(persistedCatalog.products.count == 3)
8282
}
@@ -97,7 +97,7 @@ struct POSCatalogIncrementalSyncServiceTests {
9797
try await sut.startIncrementalSync(for: sampleSiteID, lastFullSyncDate: lastFullSyncDate, lastIncrementalSyncDate: nil)
9898

9999
// Then
100-
#expect(mockSyncRemote.loadIncrementalProductVariationsCallCount == 2)
100+
#expect(await mockSyncRemote.loadIncrementalProductVariationsCallCount.value == 2)
101101
let persistedCatalog = try #require(mockPersistenceService.persistIncrementalCatalogDataLastPersistedCatalog)
102102
#expect(persistedCatalog.variations.count == 2)
103103
}
@@ -108,6 +108,7 @@ struct POSCatalogIncrementalSyncServiceTests {
108108
// Given
109109
let lastFullSyncDate = Date(timeIntervalSince1970: 1000)
110110
let expectedError = NSError(domain: "test", code: 500, userInfo: nil)
111+
let sut = POSCatalogIncrementalSyncService(syncRemote: mockSyncRemote, batchSize: 2, retryDelay: 0, persistenceService: mockPersistenceService)
111112

112113
mockSyncRemote.setIncrementalProductResult(pageNumber: 1, result: .failure(expectedError))
113114
mockSyncRemote.setIncrementalVariationResult(pageNumber: 1, result: .success(PagedItems(items: [], hasMorePages: false, totalItems: 0)))

0 commit comments

Comments
 (0)