Skip to content

Commit 96d4bc0

Browse files
committed
fix(storage): Ensure progress from Amplify.Storage.uploadFile completes
1 parent d271d77 commit 96d4bc0

File tree

4 files changed

+92
-31
lines changed

4 files changed

+92
-31
lines changed

Amplify/Core/Support/ChildTask.swift

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ import Foundation
1111
/// Child Task is cancelled it will also cancel the parent.
1212
actor ChildTask<InProcess, Success, Failure: Error>: BufferingSequence {
1313
typealias Element = InProcess
14-
let parent: Cancellable
15-
var inProcessChannel: AmplifyAsyncSequence<InProcess>? = nil
16-
var valueContinuations: [CheckedContinuation<Success, Error>] = []
17-
var storedResult: Result<Success, Failure>? = nil
18-
var isCancelled = false
14+
private let parent: Cancellable
15+
private var inProcessChannel: AmplifyAsyncSequence<InProcess>? = nil
16+
private var valueContinuations: [CheckedContinuation<Success, Error>] = []
17+
private var storedResult: Result<Success, Failure>? = nil
18+
private var isCancelled = false
1919

2020
var inProcess: AmplifyAsyncSequence<InProcess> {
2121
let channel: AmplifyAsyncSequence<InProcess>
@@ -75,9 +75,11 @@ actor ChildTask<InProcess, Success, Failure: Error>: BufferingSequence {
7575
func finish(_ result: Result<Success, Failure>) {
7676
if !valueContinuations.isEmpty {
7777
send(result)
78-
} else {
79-
// store result for when the value property is used
80-
self.storedResult = result
78+
}
79+
// store result for when the value property is used
80+
self.storedResult = result
81+
if let channel = inProcessChannel {
82+
channel.finish()
8183
}
8284
}
8385

AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginProgressTests.swift

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ import Amplify
2424
/// of ~0-1MB. After ~1 MB, I start getting notified more frequently.
2525
class AWSS3StoragePluginProgressTests: AWSS3StoragePluginTestBase {
2626

27+
/// - Given: An upload task
28+
/// - When: A subscription to its progress `resultPublisher` is established **during** upload
29+
/// - Then: Its publisher emits progress values and completes alongside the upload
2730
func testUploadProgressViaPublisher() async throws {
2831
var cancellables = Set<AnyCancellable>()
2932

@@ -56,6 +59,9 @@ class AWSS3StoragePluginProgressTests: AWSS3StoragePluginTestBase {
5659
await remove(key: key)
5760
}
5861

62+
/// - Given: An upload task
63+
/// - When: A subscription to its progress `resultPublisher` is established **after** completion
64+
/// - Then: Its publisher completes immediately
5965
func testPublisherDeliveryAfterUploadCompletes() async throws {
6066
var cancellables = Set<AnyCancellable>()
6167

@@ -94,6 +100,44 @@ class AWSS3StoragePluginProgressTests: AWSS3StoragePluginTestBase {
94100
// Remove the key
95101
await remove(key: key)
96102
}
103+
104+
/// - Given: An upload task
105+
/// - When: Its progress AsyncSequence is iterated while the upload is **in progress**
106+
/// - Then: Its iteration completes when the upload is done
107+
func testUploadAndMonitorProgressUntilCompletion() async throws {
108+
let key = UUID().uuidString
109+
let task = Amplify.Storage.uploadData(
110+
key: key,
111+
data: .testDataOfSize(.bytes(64*1024))
112+
)
113+
let progress = await task.progress
114+
var progressReports: [Progress] = []
115+
for await current in progress {
116+
XCTAssertGreaterThan(current.fractionCompleted, 0)
117+
XCTAssertLessThanOrEqual(current.fractionCompleted, 1)
118+
progressReports.append(current)
119+
}
120+
XCTAssertGreaterThan(progressReports.count, 0)
121+
_ = try await task.value
122+
await remove(key: key)
123+
}
124+
125+
/// - Given: An upload task
126+
/// - When: Its progress AsyncSequence is iterated **after** completion
127+
/// - Then: Its iteration completes without providing a value
128+
func testUploadAndMonitorProgressAfterCompletion() async throws {
129+
let key = UUID().uuidString
130+
let task = Amplify.Storage.uploadData(
131+
key: key,
132+
data: .testDataOfSize(.bytes(256))
133+
)
134+
_ = try await task.value
135+
let progress = await task.progress
136+
for await current in progress {
137+
XCTFail("Not expecting a current progress value but got: \(current)")
138+
}
139+
await remove(key: key)
140+
}
97141
}
98142

99143
private extension Data {

AmplifyTests/CoreTests/AmplifyTaskTests.swift

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -66,39 +66,33 @@ class AmplifyTaskTests: XCTestCase {
6666
}
6767
#endif
6868

69+
/// Given: A operation with discrete steps (10)
70+
/// When: Its progress sequence is iterated
71+
/// Then: It reports each step up to a `fractionCompleted` value that represents 100% completion
6972
func testLongOperation() async throws {
70-
var success = false
71-
var output: String? = nil
72-
var thrown: Error? = nil
73-
7473
let request = LongOperationRequest(steps: 10, delay: 0.01)
7574
let longTask = await runLongOperation(request: request)
7675

77-
Task {
78-
var progressCount = 0
79-
var lastProgress: Double = 0
76+
var progressCount = 0
77+
var lastProgress: Double = 0
78+
await longTask.progress.forEach { progress in
79+
lastProgress = progress.fractionCompleted
80+
progressCount += 1
81+
}
8082

81-
await longTask.progress.forEach { progress in
82-
lastProgress = progress.fractionCompleted
83-
progressCount += 1
84-
}
83+
// The first progress report happens on `fractionCompleted` 0.0
84+
XCTAssertEqual(progressCount, 11)
8585

86-
XCTAssertEqual(progressCount, 11)
87-
XCTAssertEqual(lastProgress, 100)
88-
}
86+
// Note that `fractionComleted` is calculated by dividing
87+
// `completedUnitCount` by `totalUnitCount`. See:
88+
//https://developer.apple.com/documentation/foundation/progress/1408579-fractioncompleted
89+
XCTAssertEqual(lastProgress, 1.0)
8990

90-
do {
91-
let value = try await longTask.value
92-
output = value.id
93-
success = true
94-
} catch {
95-
thrown = error
96-
}
91+
let value = try await longTask.value
92+
let output = value.id
9793

98-
XCTAssertTrue(success)
9994
XCTAssertNotNil(output)
10095
XCTAssertFalse(output.isEmpty)
101-
XCTAssertNil(thrown)
10296
}
10397

10498
#if canImport(Combine)

AmplifyTests/CoreTests/ChildTaskTests.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,16 @@ class ChildTaskTests: XCTestCase {
1818
}
1919
}
2020

21+
/// Given: A ChildTask instance associated to a fast operation
22+
/// When: Multiple `Task` instances are created to wait for its results
23+
/// Then: All `Task` instances receive the exact-same `value`
2124
func testFastOperationWithMultipleAwaits() async throws {
2225
let input = [1, 2, 3]
2326
let request = FastOperationRequest(numbers: input)
2427
let queue = OperationQueue()
2528
let operation = FastOperation(request: request)
2629
let childTask: ChildTask<Void, FastOperation.Success, FastOperation.Failure> = ChildTask(parent: operation)
30+
let progressSequence = await childTask.inProcess
2731
let token = operation.subscribe { result in
2832
Task {
2933
await childTask.finish(result)
@@ -54,11 +58,17 @@ class ChildTaskTests: XCTestCase {
5458
XCTAssertEqual(output1, expectedOutput)
5559
XCTAssertEqual(output2, expectedOutput)
5660
XCTAssertEqual(output3, expectedOutput)
61+
62+
// Ensure the channel's AsyncSequence does not block after completion
63+
for await _ in progressSequence {
64+
XCTFail("Unexpected channel iteration since task has completed.")
65+
}
5766
}
5867

5968
func testChildTaskResultCancelled() async throws {
6069
let worker = Worker()
6170
let childTask: ChildTask<Void, String, Never> = ChildTask(parent: worker)
71+
let progressSequence = await childTask.inProcess
6272
let cancelExp = expectation(description: "cancel")
6373
cancelExp.isInverted = true
6474

@@ -77,11 +87,17 @@ class ChildTaskTests: XCTestCase {
7787

7888
await waitForExpectations(timeout: 0.01)
7989
task.cancel()
90+
91+
// Ensure the channel's AsyncSequence does not block after completion
92+
for await _ in progressSequence {
93+
XCTFail("Unexpected channel iteration since task was cancelled.")
94+
}
8095
}
8196

8297
func testChildTaskResultAlreadyCancelled() async throws {
8398
let worker = Worker()
8499
let childTask: ChildTask<Void, String, Never> = ChildTask(parent: worker)
100+
let progressSequence = await childTask.inProcess
85101
let cancelExp = expectation(description: "cancel")
86102
cancelExp.isInverted = true
87103

@@ -100,6 +116,11 @@ class ChildTaskTests: XCTestCase {
100116
}
101117

102118
await waitForExpectations(timeout: 0.01)
119+
120+
// Ensure the channel's AsyncSequence does not block after completion
121+
for await _ in progressSequence {
122+
XCTFail("Unexpected channel iteration since task was cancelled.")
123+
}
103124
}
104125

105126
}

0 commit comments

Comments
 (0)