Skip to content

Commit 6cfad3a

Browse files
authored
test: add tests for AsyncValueSubject type (#694)
* test: add tests for AsyncValueSubject type * fix
1 parent d11bbb4 commit 6cfad3a

File tree

2 files changed

+123
-8
lines changed

2 files changed

+123
-8
lines changed

Sources/Helpers/AsyncValueSubject.swift

+20-8
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ package final class AsyncValueSubject<Value: Sendable>: Sendable {
2020
var value: Value
2121
var continuations: [UInt: AsyncStream<Value>.Continuation] = [:]
2222
var count: UInt = 0
23+
var finished = false
2324
}
2425

25-
let bufferingPolicy: BufferingPolicy
26+
let bufferingPolicy: UncheckedSendable<BufferingPolicy>
2627
let mutableState: LockIsolated<MutableState>
2728

2829
/// Creates a new AsyncValueSubject with an initial value.
@@ -31,7 +32,7 @@ package final class AsyncValueSubject<Value: Sendable>: Sendable {
3132
/// - bufferingPolicy: Determines how values are buffered in the AsyncStream (defaults to .unbounded)
3233
package init(_ initialValue: Value, bufferingPolicy: BufferingPolicy = .unbounded) {
3334
self.mutableState = LockIsolated(MutableState(value: initialValue))
34-
self.bufferingPolicy = bufferingPolicy
35+
self.bufferingPolicy = UncheckedSendable(bufferingPolicy)
3536
}
3637

3738
deinit {
@@ -43,12 +44,17 @@ package final class AsyncValueSubject<Value: Sendable>: Sendable {
4344
mutableState.value
4445
}
4546

46-
/// Sends a new value to the subject and notifies all observers.
47-
/// - Parameter value: The new value to send
47+
/// Resume the task awaiting the next iteration point by having it return normally from its suspension point with a given element.
48+
/// - Parameter value: The value to yield from the continuation.
49+
///
50+
/// If nothing is awaiting the next value, this method attempts to buffer the result’s element.
51+
///
52+
/// This can be called more than once and returns to the caller immediately without blocking for any awaiting consumption from the iteration.
4853
package func yield(_ value: Value) {
4954
mutableState.withValue {
50-
$0.value = value
55+
guard !$0.finished else { return }
5156

57+
$0.value = value
5258
for (_, continuation) in $0.continuations {
5359
continuation.yield(value)
5460
}
@@ -62,14 +68,20 @@ package final class AsyncValueSubject<Value: Sendable>: Sendable {
6268
/// finish, the stream enters a terminal state and doesn't produce any
6369
/// additional elements.
6470
package func finish() {
65-
for (_, continuation) in mutableState.continuations {
66-
continuation.finish()
71+
mutableState.withValue {
72+
guard $0.finished == false else { return }
73+
74+
$0.finished = true
75+
76+
for (_, continuation) in $0.continuations {
77+
continuation.finish()
78+
}
6779
}
6880
}
6981

7082
/// An AsyncStream that emits the current value and all subsequent updates.
7183
package var values: AsyncStream<Value> {
72-
AsyncStream(bufferingPolicy: bufferingPolicy) { continuation in
84+
AsyncStream(bufferingPolicy: bufferingPolicy.value) { continuation in
7385
insert(continuation)
7486
}
7587
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
//
2+
// AsyncValueSubjectTests.swift
3+
// Supabase
4+
//
5+
// Created by Guilherme Souza on 24/03/25.
6+
//
7+
8+
import ConcurrencyExtras
9+
import XCTest
10+
11+
@testable import Helpers
12+
13+
final class AsyncValueSubjectTests: XCTestCase {
14+
15+
override func invokeTest() {
16+
withMainSerialExecutor {
17+
super.invokeTest()
18+
}
19+
}
20+
21+
func testInitialValue() async {
22+
let subject = AsyncValueSubject<Int>(42)
23+
XCTAssertEqual(subject.value, 42)
24+
}
25+
26+
func testYieldUpdatesValue() async {
27+
let subject = AsyncValueSubject<Int>(0)
28+
subject.yield(10)
29+
XCTAssertEqual(subject.value, 10)
30+
}
31+
32+
func testValuesStream() async {
33+
let subject = AsyncValueSubject<Int>(0)
34+
let values = LockIsolated<[Int]>([])
35+
36+
let task = Task {
37+
for await value in subject.values {
38+
let values = values.withValue {
39+
$0.append(value)
40+
return $0
41+
}
42+
if values.count == 4 {
43+
break
44+
}
45+
}
46+
}
47+
48+
await Task.yield()
49+
50+
subject.yield(1)
51+
subject.yield(2)
52+
subject.yield(3)
53+
subject.finish()
54+
55+
await task.value
56+
57+
XCTAssertEqual(values.value, [0, 1, 2, 3])
58+
}
59+
60+
func testOnChangeHandler() async {
61+
let subject = AsyncValueSubject<Int>(0)
62+
let values = LockIsolated<[Int]>([])
63+
64+
let task = subject.onChange { value in
65+
values.withValue {
66+
$0.append(value)
67+
}
68+
}
69+
70+
await Task.yield()
71+
72+
subject.yield(1)
73+
subject.yield(2)
74+
subject.yield(3)
75+
subject.finish()
76+
77+
await task.value
78+
79+
XCTAssertEqual(values.value, [0, 1, 2, 3])
80+
}
81+
82+
func testFinish() async {
83+
let subject = AsyncValueSubject<Int>(0)
84+
let values = LockIsolated<[Int]>([])
85+
86+
let task = Task {
87+
for await value in subject.values {
88+
values.withValue { $0.append(value) }
89+
}
90+
}
91+
92+
await Task.yield()
93+
94+
subject.yield(1)
95+
subject.finish()
96+
subject.yield(2)
97+
98+
await task.value
99+
100+
XCTAssertEqual(values.value, [0, 1])
101+
XCTAssertEqual(subject.value, 1)
102+
}
103+
}

0 commit comments

Comments
 (0)