1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
// Copyright © The Daybrite Project
// SPDX-License-Identifier: MPL-2.0
//! The built-in pieces' conformance cases, gathered (docs/testing.md). Each piece's cases live
//! in a `mod conformance` at the end of the file that defines it, next to the constructors they
//! prove; the attribute discovers them automatically. This module holds cases that belong
//! to no piece and builds the conformance app's browsing page.
//! Compiled only under the `conformance` feature, so a shipping app links none of it.
use day_core::conformance::{Case, Drive};
use day_core::*;
use day_reactive::Signal;
use crate::*;
/// The cases that belong to no single piece.
mod general {
use super::*;
/// A headless test: no page, app state checked on the app's own thread, in its own process.
#[day_macros::test(day_core)]
fn headless_sanity() -> Case {
Case::headless().run(|t: Drive| async move {
let count = Signal::new(1i64);
count.update(|n| *n += 1);
t.check_eq(count.get(), 2)?;
day_core::sleep(1).await;
t.check_eq(count.get(), 2)
})
}
/// The mock runs posts, timers and blocking jobs inline on the test thread (and the web
/// has no threads), so the cases below assert less about time and threads there.
fn synchronous_backend() -> bool {
day_core::backend_name() == Some("mock-mock")
}
/// Poll `done` between short sleeps until it holds or a few seconds pass: an emulator in
/// CI is slow, so no case judges a timer against a tight ceiling.
async fn settle(
t: &Drive,
what: &str,
done: impl Fn() -> bool,
) -> day_core::conformance::TestResult {
for _ in 0..400 {
if done() {
return Ok(());
}
day_core::sleep(20).await;
}
t.check(done(), what)
}
/// `day::sleep` is the toolkit's own timer (docs/async.md): it resolves after at least its
/// delay, back on the UI thread where the task reads and writes signals as before.
#[day_macros::test(day_core)]
fn sleep_resolves_after_its_delay() -> Case {
Case::headless().run(|t: Drive| async move {
let thread = std::thread::current().id();
let count = Signal::new(0i64);
// Exercise both sides of Windows' timer tick boundaries and repeat the longer
// delay: a single sample can miss a WM_TIMER delivered before its deadline.
for ms in [1, 10, 15, 16, 31, 120, 120, 120] {
#[cfg(not(target_arch = "wasm32"))]
let started = std::time::Instant::now();
day_core::sleep(ms).await;
#[cfg(not(target_arch = "wasm32"))]
if !synchronous_backend() {
// The one millisecond of slack covers the timer's clock and `Instant`
// ticking apart; the backend must not round down to a system tick.
let slept = started.elapsed();
t.check(
slept >= std::time::Duration::from_millis(u64::from(ms - 1)),
&format!("a {ms} ms sleep resolved after {slept:?}"),
)?;
}
}
t.check(
std::thread::current().id() == thread,
"the task resumed on another thread",
)?;
count.set(1);
t.check_eq(count.get(), 1)
})
}
/// A zero delay still yields: the rest of the task runs on a later turn, after the code
/// that followed `task(..)`.
#[day_macros::test(day_core)]
fn sleep_zero_yields_the_turn() -> Case {
Case::headless().run(|t: Drive| async move {
let flag = Signal::new(false);
let seen = Signal::new(None::<bool>);
day_core::task(async move {
day_core::sleep(0).await;
seen.set(Some(flag.get()));
});
// Runs after the task's eager first poll parked it on the timer.
flag.set(true);
settle(&t, "the zero sleep never resolved", move || {
seen.get().is_some()
})
.await?;
if synchronous_backend() {
return Ok(());
}
t.check_eq(seen.get(), Some(true))
})
}
/// Timers resolve in delay order whatever order they were armed in, and many at once.
#[day_macros::test(day_core)]
fn sleeps_resolve_in_delay_order() -> Case {
Case::headless().run(|t: Drive| async move {
let order = Signal::new(Vec::<u32>::new());
for ms in [200u32, 10, 100, 50] {
day_core::task(async move {
day_core::sleep(ms).await;
order.update(|o| o.push(ms));
});
}
let done = Signal::new(0usize);
for i in 0..64u32 {
day_core::task(async move {
day_core::sleep(1 + (i * 7) % 80).await;
done.update(|d| *d += 1);
});
}
settle(&t, "not every sleep resolved", move || {
order.with(|o| o.len()) == 4 && done.get() == 64
})
.await?;
if synchronous_backend() {
return Ok(());
}
t.check_eq(order.get(), vec![10, 50, 100, 200])
})
}
/// Aborting a task parked on a sleep drops the sleep: the timer may still fire natively,
/// but nothing resumes.
#[day_macros::test(day_core)]
fn an_aborted_task_never_wakes_from_its_sleep() -> Case {
Case::headless().run(|t: Drive| async move {
let fired = Signal::new(false);
let handle = day_core::task(async move {
day_core::sleep(60).await;
fired.set(true);
});
handle.abort();
day_core::sleep(250).await;
if synchronous_backend() {
// Inline timers resolved the sleep before the abort could; nothing to judge.
return Ok(());
}
t.check(!fired.get(), "an aborted task resumed after its sleep")
})
}
/// `spawn_blocking` runs its job on a pool thread and resolves back on the UI thread, where
/// the value lands in a signal like any other.
#[day_macros::test(day_core)]
fn a_blocking_job_runs_off_the_ui_thread_and_resolves_on_it() -> Case {
Case::headless().run(|t: Drive| async move {
let ui = std::thread::current().id();
let answer = Signal::new(0i64);
let got = day_core::spawn_blocking(move || {
let elsewhere = std::thread::current().id() != ui;
(elsewhere, (1..=9i64).product::<i64>())
})
.await;
let (elsewhere, value) = got.map_err(|e| day_core::conformance::Fail(e.to_string()))?;
t.check(
std::thread::current().id() == ui,
"the await resumed off the UI thread",
)?;
t.check_eq(elsewhere, !day_core::blocking_inline())?;
answer.set(value);
t.check_eq(answer.get(), 362_880)
})
}
/// A job that panics is an error on the awaiting side, and the pool keeps serving. Not on
/// the web, where a panic aborts the whole instance before anything could catch it.
#[day_macros::test(day_core)]
fn a_panicking_blocking_job_is_an_error() -> Case {
Case::headless().run(|t: Drive| async move {
#[cfg(not(target_arch = "wasm32"))]
{
let failed: Result<(), _> =
day_core::spawn_blocking(|| panic!("the job failed on purpose")).await;
t.check_eq(failed, Err(day_core::BlockingError::Panicked))?;
}
let next = day_core::spawn_blocking(|| 7).await;
t.check_eq(next, Ok(7))
})
}
/// Dropping the future before the job started cancels the job: it never runs.
#[day_macros::test(day_core)]
fn a_blocking_job_dropped_before_it_starts_never_runs() -> Case {
Case::headless().run(|t: Drive| async move {
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, mpsc};
let ran = Arc::new(AtomicBool::new(false));
if day_core::blocking_inline() {
// Inline jobs run at the first poll; a future never polled never runs.
let marker = ran.clone();
drop(day_core::spawn_blocking(move || {
marker.store(true, Ordering::SeqCst)
}));
let _ = day_core::spawn_blocking(|| ()).await;
return t.check(!ran.load(Ordering::SeqCst), "a cancelled inline job ran");
}
// Hold every pool thread behind a latch so the marker has to queue.
let limit = day_core::blocking_limit();
let (release, held) = mpsc::channel::<()>();
let held = Arc::new(Mutex::new(held));
let started = Arc::new(AtomicUsize::new(0));
let holders: Vec<_> = (0..limit)
.map(|_| {
let (held, started) = (held.clone(), started.clone());
day_core::spawn_blocking(move || {
started.fetch_add(1, Ordering::SeqCst);
let _ = held.lock().map(|h| h.recv());
})
})
.collect();
settle(&t, "the pool never filled", move || {
started.load(Ordering::SeqCst) >= limit
})
.await?;
let marker = ran.clone();
drop(day_core::spawn_blocking(move || {
marker.store(true, Ordering::SeqCst)
}));
for _ in 0..limit {
let _ = release.send(());
}
for holder in holders {
t.check_eq(holder.await, Ok(()))?;
}
// The holders are through, so whatever queued behind them has drained too.
t.check_eq(day_core::spawn_blocking(|| ()).await, Ok(()))?;
t.check(!ran.load(Ordering::SeqCst), "a cancelled job ran")
})
}
/// Jobs beyond the thread limit queue and every one completes.
#[day_macros::test(day_core)]
fn blocking_jobs_beyond_the_limit_all_complete() -> Case {
Case::headless().run(|t: Drive| async move {
let n = day_core::blocking_limit().max(1) * 6;
let jobs: Vec<_> = (0..n)
.map(|i| {
day_core::spawn_blocking(move || {
if !day_core::blocking_inline() {
std::thread::sleep(std::time::Duration::from_millis(2));
}
i
})
})
.collect();
let mut sum = 0;
for job in jobs {
sum += job
.await
.map_err(|e| day_core::conformance::Fail(e.to_string()))?;
}
t.check_eq(sum, (0..n).sum::<usize>())
})
}
}
/// The conformance app's content: a nav over every registered GUI case, one route per case,
/// so a person can open any case on any toolkit and look at it. A run does not go through it:
/// the app roots it in [`test_host`](crate::test_host), which shows each driven case's page
/// alone while it runs. An app with its own tests can browse them the same way.
///
/// Split, never tabs: a split nav builds a case's page when it is first opened, where tabs
/// (what a phone picks for a flat nav) keep every page resident and build them all at launch.
/// Since cases hold navigation, covers and pieces of their own, that kept a CI phone's launch
/// transitioning past the run's first `wait_idle`.
pub fn test_pages() -> impl Piece {
let selection = Signal::new(String::new());
nav(selection)
.presentation(day_spec::props::NavPresentation::Split)
.items(nav_items(
|| {
day_core::conformance::cases()
.into_iter()
.filter(|c| c.kind() == day_core::conformance::TestKind::Gui)
.map(|c| c.name().to_owned())
.collect::<Vec<String>>()
},
|name: &String| item(name.clone(), name.clone()),
))
.destination(
|key: &String| match day_core::conformance::case_named(key) {
Some(case) => guarded_page(case),
None => label(format!("no test {key:?}")).any(),
},
)
}