Skip to content

Commit ded34cd

Browse files
committed
x-bow: better tests and fire_immediately option for signal_stream
1 parent 5390fd2 commit ded34cd

File tree

7 files changed

+288
-16
lines changed

7 files changed

+288
-16
lines changed

x-bow/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ x-bow-macros = { path = "../x-bow-macros/" }
1414
pin-project-lite = "0.2"
1515

1616
[dev-dependencies]
17-
futures-lite = "1.13.0"
17+
futures-lite = "1.13.0"
18+
pollster = { version = "0.3.0", features = ["macro"] }

x-bow/src/path_ext.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -198,9 +198,12 @@ pub trait PathExt: Path {
198198
UntilChange::new(self.store_wakers(), self)
199199
}
200200

201-
/// Get a [Stream][futures_core::Stream] that fires **once now** and once
201+
/// Get a [Stream][futures_core::Stream] that fires once
202202
/// every time the data changes, yielding [Ref]s to the data.
203203
///
204+
/// If the `fire_immediately` argument is `true`, then the stream will fire
205+
/// on the first poll too.
206+
///
204207
/// The stream ends when the data cannot be borrowed (when [borrow_opt][Self::borrow_opt]
205208
/// returns None).
206209
///
@@ -217,7 +220,7 @@ pub trait PathExt: Path {
217220
/// # }
218221
/// // set the text with the current data
219222
/// // and update the text whenever the data change
220-
/// path.signal_stream().for_each(|data: Ref<'_, String>| {
223+
/// path.signal_stream(true).for_each(|data: Ref<'_, String>| {
221224
/// let s: &str = &**data;
222225
/// ui_element.set_text(s);
223226
/// }).await;
@@ -243,16 +246,16 @@ pub trait PathExt: Path {
243246
/// # async fn example(path: &impl Path) {
244247
/// let stream = path.signal_stream();
245248
/// // is equivalent to
246-
/// let stream = futures_lite::stream::once(()) // fire once in the beginning...
249+
/// let stream = futures_lite::stream::once(()) // fire immediately in the beginning...
247250
/// .chain(path.until_change()) // and after every change
248251
/// .map(|_| path.borrow_opt()) // borrow the data into a Ref
249252
/// .take_while(Option::is_some) // end the stream if the data is unavailable
250253
/// .map(Option::unwrap); // we've already checked that the data isn't none
251254
/// # }
252255
/// ```
253256
#[must_use = "the returned Stream is lazy; poll it or use StreamExt on it"]
254-
fn signal_stream(&self) -> signal_stream::SignalStream<'_, Self> {
255-
signal_stream::SignalStream::new(self, self.until_change())
257+
fn signal_stream(&self, fire_immediately: bool) -> signal_stream::SignalStream<'_, Self> {
258+
signal_stream::SignalStream::new(self, self.until_change(), fire_immediately)
256259
}
257260

258261
/// Execute the given function with the data as argument. Repeat every time

x-bow/src/path_ext/signal_stream.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,16 @@ use crate::{until_change::UntilChange, Path, PathExt};
88
pub struct SignalStream<'a, P: Path + ?Sized> {
99
path: &'a P,
1010
until_change: UntilChange<'a>,
11+
fire_immediately: bool,
1112
}
1213

1314
impl<'a, P: Path + ?Sized> SignalStream<'a, P> {
14-
pub(super) fn new(path: &'a P, until_change: UntilChange<'a>) -> Self {
15-
Self { path, until_change }
15+
pub(super) fn new(path: &'a P, until_change: UntilChange<'a>, fire_immediately: bool) -> Self {
16+
Self {
17+
path,
18+
until_change,
19+
fire_immediately,
20+
}
1621
}
1722
}
1823

@@ -25,7 +30,9 @@ impl<'a, P: Path + ?Sized> Stream for SignalStream<'a, P> {
2530
) -> Poll<Option<Self::Item>> {
2631
let this = self.get_mut();
2732
let first = !this.until_change.has_been_polled();
28-
if first | Pin::new(&mut this.until_change).poll_next(cx).is_ready() {
33+
if (first && this.fire_immediately)
34+
| Pin::new(&mut this.until_change).poll_next(cx).is_ready()
35+
{
2936
Poll::Ready(this.path.borrow_opt())
3037
} else {
3138
Poll::Pending

x-bow/tests/basic_typecheck.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use x_bow::Trackable;
55

66
#[test]
77
fn just_leaf() {
8-
let mut a: i32 = 5;
8+
let a: i32 = 5;
99
let store = Store::new(a);
1010
let _ = async {
1111
let _ = store.build_path().until_change();
@@ -49,12 +49,12 @@ fn struct_project() {
4949
let _: String = store.build_path().field_3().t1().borrow().clone();
5050
}
5151
let _ = async {
52-
store.build_path().field_1().until_change();
53-
store.build_path().field_1().field_1().until_change();
54-
store.build_path().field_2().until_change();
55-
store.build_path().field_3().until_change();
56-
store.build_path().field_3().t0().until_change();
57-
store.build_path().field_3().t1().until_change();
52+
let _ = store.build_path().field_1().until_change();
53+
let _ = store.build_path().field_1().field_1().until_change();
54+
let _ = store.build_path().field_2().until_change();
55+
let _ = store.build_path().field_3().until_change();
56+
let _ = store.build_path().field_3().t0().until_change();
57+
let _ = store.build_path().field_3().t1().until_change();
5858
};
5959
}
6060

x-bow/tests/changes.rs

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
mod types;
2+
mod utils;
3+
4+
use std::pin::pin;
5+
6+
use types::*;
7+
use utils::{is_all_pending, is_all_ready};
8+
use x_bow::{PathExt, PathExtGuaranteed, Store};
9+
10+
#[pollster::test]
11+
async fn regular_changes() {
12+
let state = Root::default();
13+
let state = Store::new(state);
14+
state
15+
.build_path()
16+
.field_1()
17+
.field_12()
18+
.borrow_mut()
19+
.push(());
20+
state
21+
.build_path()
22+
.field_2()
23+
.borrow_mut()
24+
.push(Enum2::VariantA(Default::default()));
25+
state
26+
.build_path()
27+
.field_2()
28+
.borrow_mut()
29+
.push(Enum2::VariantB {
30+
field: Default::default(),
31+
});
32+
33+
let stream = state.build_path();
34+
let mut stream = pin!(stream.until_change());
35+
let stream_1 = state.build_path().field_1();
36+
let mut stream_1 = pin!(stream_1.until_change());
37+
let stream_1_11 = state.build_path().field_1().field_11();
38+
let mut stream_1_11 = pin!(stream_1_11.until_change());
39+
let stream_1_12 = state.build_path().field_1().field_12();
40+
let mut stream_1_12 = pin!(stream_1_12.until_change());
41+
let stream_2 = state.build_path().field_2();
42+
let mut stream_2 = pin!(stream_2.until_change());
43+
let stream_2_1 = state.build_path().field_2().index(1);
44+
let mut stream_2_1 = pin!(stream_2_1.until_change());
45+
let stream_2_1_a = state.build_path().field_2().index(1).VariantA_0();
46+
let mut stream_2_1_a = pin!(stream_2_1_a.until_change());
47+
let stream_2_1_b = state.build_path().field_2().index(1).VariantB_field();
48+
let mut stream_2_1_b = pin!(stream_2_1_b.until_change());
49+
let stream_2_1_b_d = state
50+
.build_path()
51+
.field_2()
52+
.index(1)
53+
.VariantB_field()
54+
.data();
55+
let mut stream_2_1_b_d = pin!(stream_2_1_b_d.until_change());
56+
57+
assert!(
58+
is_all_pending([
59+
stream.as_mut(),
60+
stream_1.as_mut(),
61+
stream_1_11.as_mut(),
62+
stream_1_12.as_mut(),
63+
stream_2.as_mut(),
64+
stream_2_1.as_mut(),
65+
stream_2_1_a.as_mut(),
66+
stream_2_1_b.as_mut(),
67+
stream_2_1_b_d.as_mut(),
68+
]),
69+
"all pending in the beginning"
70+
);
71+
72+
state.build_path().field_2().borrow_mut();
73+
74+
assert!(
75+
is_all_pending([
76+
stream.as_mut(),
77+
stream_1.as_mut(),
78+
stream_1_11.as_mut(),
79+
stream_1_12.as_mut(),
80+
]),
81+
"field_1 not woken by field_2 change"
82+
);
83+
assert!(
84+
is_all_ready([
85+
stream_2.as_mut(),
86+
stream_2_1.as_mut(),
87+
stream_2_1_a.as_mut(),
88+
stream_2_1_b.as_mut(),
89+
stream_2_1_b_d.as_mut(),
90+
]),
91+
"field_2 and descendants all woken"
92+
);
93+
assert!(
94+
is_all_pending([
95+
stream_2.as_mut(),
96+
stream_2_1.as_mut(),
97+
stream_2_1_a.as_mut(),
98+
stream_2_1_b.as_mut(),
99+
stream_2_1_b_d.as_mut(),
100+
]),
101+
"woken once, pending later"
102+
);
103+
104+
state.build_path().field_1().borrow_mut();
105+
assert!(
106+
is_all_ready([
107+
stream_1.as_mut(),
108+
stream_1_11.as_mut(),
109+
stream_1_12.as_mut(),
110+
]),
111+
"field_1 and descendants all woken"
112+
);
113+
assert!(
114+
is_all_pending([
115+
stream.as_mut(),
116+
stream_2.as_mut(),
117+
stream_2_1.as_mut(),
118+
stream_2_1_a.as_mut(),
119+
stream_2_1_b.as_mut(),
120+
stream_2_1_b_d.as_mut(),
121+
]),
122+
"field_2 not woken by field_1 change"
123+
);
124+
125+
state.build_path().borrow_mut();
126+
assert!(
127+
is_all_ready([
128+
stream.as_mut(),
129+
stream_1.as_mut(),
130+
stream_1_11.as_mut(),
131+
stream_1_12.as_mut(),
132+
stream_2.as_mut(),
133+
stream_2_1.as_mut(),
134+
stream_2_1_a.as_mut(),
135+
stream_2_1_b.as_mut(),
136+
stream_2_1_b_d.as_mut(),
137+
]),
138+
"all ready after root change"
139+
);
140+
141+
{
142+
let deep_stream = state.build_path().field_1().field_12().index(0);
143+
let mut deep_stream = pin!(deep_stream.until_change());
144+
assert!(
145+
is_all_pending([deep_stream.as_mut()]),
146+
"deep stream starts off pending"
147+
);
148+
state
149+
.build_path()
150+
.field_1()
151+
.field_12()
152+
.index(0)
153+
.borrow_opt_mut();
154+
assert!(
155+
is_all_pending([
156+
stream.as_mut(),
157+
stream_1.as_mut(),
158+
stream_1_11.as_mut(),
159+
stream_1_12.as_mut(),
160+
stream_2.as_mut(),
161+
stream_2_1.as_mut(),
162+
stream_2_1_a.as_mut(),
163+
stream_2_1_b.as_mut(),
164+
stream_2_1_b_d.as_mut(),
165+
]),
166+
"deep change wakes no one above"
167+
);
168+
assert!(
169+
is_all_ready([deep_stream.as_mut()]),
170+
"the deep change is woken"
171+
);
172+
}
173+
174+
{
175+
let deep_stream = state.build_path().field_1().field_12().index(1);
176+
let mut deep_stream = pin!(deep_stream.until_change());
177+
assert!(
178+
is_all_pending([deep_stream.as_mut()]),
179+
"deep stream starts off pending"
180+
);
181+
state
182+
.build_path()
183+
.field_1()
184+
.field_12()
185+
.index(1)
186+
.borrow_opt_mut();
187+
assert!(
188+
is_all_pending([
189+
stream.as_mut(),
190+
stream_1.as_mut(),
191+
stream_1_11.as_mut(),
192+
stream_1_12.as_mut(),
193+
stream_2.as_mut(),
194+
stream_2_1.as_mut(),
195+
stream_2_1_a.as_mut(),
196+
stream_2_1_b.as_mut(),
197+
stream_2_1_b_d.as_mut(),
198+
]),
199+
"deep change wakes no one above"
200+
);
201+
assert!(
202+
is_all_pending([deep_stream.as_mut()]),
203+
"the deep change is not woken because borrow_opt_mut failed"
204+
);
205+
}
206+
}

x-bow/tests/types.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
use std::collections::HashMap;
2+
3+
use x_bow::Trackable;
4+
5+
#[derive(Trackable, Default)]
6+
#[track(deep)]
7+
pub struct Root {
8+
pub field_1: Struct1,
9+
pub field_2: Vec<Enum2>,
10+
}
11+
12+
#[derive(Trackable, Default)]
13+
#[track(deep)]
14+
pub struct Struct1 {
15+
pub field_11: String,
16+
pub field_12: Vec<()>,
17+
}
18+
19+
#[derive(Trackable, Default)]
20+
#[track(deep)]
21+
pub struct Struct3<T> {
22+
pub data: HashMap<i32, T>,
23+
}
24+
25+
#[derive(Trackable)]
26+
#[track(deep)]
27+
pub enum Enum2 {
28+
VariantA(Struct1),
29+
VariantB { field: Struct3<String> },
30+
VariantC,
31+
}
32+
33+
impl Default for Enum2 {
34+
fn default() -> Self {
35+
Self::VariantC
36+
}
37+
}

x-bow/tests/utils.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
use core::{pin::Pin, task::Context};
2+
3+
use async_ui_internal_utils::dummy_waker::dummy_waker;
4+
use futures_core::Stream;
5+
6+
pub fn is_ready(s: Pin<&mut dyn Stream<Item = ()>>) -> bool {
7+
let waker = dummy_waker();
8+
let mut ctx = Context::from_waker(&waker);
9+
s.poll_next(&mut ctx).is_ready()
10+
}
11+
12+
pub fn is_all_pending<const N: usize>(s: [Pin<&mut dyn Stream<Item = ()>>; N]) -> bool {
13+
s.into_iter().all(|one| !is_ready(one))
14+
}
15+
16+
pub fn is_all_ready<const N: usize>(s: [Pin<&mut dyn Stream<Item = ()>>; N]) -> bool {
17+
s.into_iter().all(|one| is_ready(one))
18+
}

0 commit comments

Comments
 (0)