Skip to content

Commit 8269b5f

Browse files
authored
test(subscriber): add tests for excessive poll counts (#476)
A defect in the way polls are counted was fixed in #440. The defect caused the polls of a "child" task to also be counted on the "parent" task. Where in this case, the "parent" is a task that spawns the "child" task. This change adds a regression test for that fix. To do so, functionality to record and validate poll counts is added to the `console-subscriber` integration test framework. The new functionality is also used to add some basic tests for poll counts.
1 parent d2dc1cf commit 8269b5f

File tree

6 files changed

+155
-2
lines changed

6 files changed

+155
-2
lines changed

console-subscriber/tests/framework.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,27 @@ fn fail_1_of_2_expected_tasks() {
177177
assert_tasks(expected_tasks, future);
178178
}
179179

180+
#[test]
181+
fn polls() {
182+
let expected_task = ExpectedTask::default().match_default_name().expect_polls(2);
183+
184+
let future = async { yield_to_runtime().await };
185+
186+
assert_task(expected_task, future);
187+
}
188+
189+
#[test]
190+
#[should_panic(expected = "Test failed: Task validation failed:
191+
- Task { name=console-test::main }: expected `polls` to be 2, but actual was 1
192+
")]
193+
fn fail_polls() {
194+
let expected_task = ExpectedTask::default().match_default_name().expect_polls(2);
195+
196+
let future = async {};
197+
198+
assert_task(expected_task, future);
199+
}
200+
180201
async fn yield_to_runtime() {
181202
// There is a race condition that can occur when tests are run in parallel,
182203
// caused by tokio-rs/tracing#2743. It tends to cause test failures only

console-subscriber/tests/poll.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
use std::time::Duration;
2+
3+
use tokio::time::sleep;
4+
5+
mod support;
6+
use support::{assert_task, ExpectedTask};
7+
8+
#[test]
9+
fn single_poll() {
10+
let expected_task = ExpectedTask::default().match_default_name().expect_polls(1);
11+
12+
let future = futures::future::ready(());
13+
14+
assert_task(expected_task, future);
15+
}
16+
17+
#[test]
18+
fn two_polls() {
19+
let expected_task = ExpectedTask::default().match_default_name().expect_polls(2);
20+
21+
let future = async {
22+
sleep(Duration::ZERO).await;
23+
};
24+
25+
assert_task(expected_task, future);
26+
}
27+
28+
#[test]
29+
fn many_polls() {
30+
let expected_task = ExpectedTask::default()
31+
.match_default_name()
32+
.expect_polls(11);
33+
34+
let future = async {
35+
for _ in 0..10 {
36+
sleep(Duration::ZERO).await;
37+
}
38+
};
39+
40+
assert_task(expected_task, future);
41+
}

console-subscriber/tests/spawn.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
use std::time::Duration;
2+
3+
use tokio::time::sleep;
4+
5+
mod support;
6+
use support::{assert_tasks, spawn_named, ExpectedTask};
7+
8+
/// This test asserts the behavior that was fixed in #440. Before that fix,
9+
/// the polls of a child were also counted towards the parent (the task which
10+
/// spawned the child task). In this scenario, that would result in the parent
11+
/// having 3 polls counted, when it should really be 1.
12+
#[test]
13+
fn child_polls_dont_count_towards_parent_polls() {
14+
let expected_tasks = vec![
15+
ExpectedTask::default()
16+
.match_name("parent".into())
17+
.expect_polls(1),
18+
ExpectedTask::default()
19+
.match_name("child".into())
20+
.expect_polls(2),
21+
];
22+
23+
let future = async {
24+
let child_join_handle = spawn_named("parent", async {
25+
spawn_named("child", async {
26+
sleep(Duration::ZERO).await;
27+
})
28+
})
29+
.await
30+
.expect("joining parent failed");
31+
32+
child_join_handle.await.expect("joining child failed");
33+
};
34+
35+
assert_tasks(expected_tasks, future);
36+
}

console-subscriber/tests/support/mod.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use subscriber::run_test;
88

99
pub(crate) use subscriber::MAIN_TASK_NAME;
1010
pub(crate) use task::ExpectedTask;
11+
use tokio::task::JoinHandle;
1112

1213
/// Assert that an `expected_task` is recorded by a console-subscriber
1314
/// when driving the provided `future` to completion.
@@ -45,3 +46,19 @@ where
4546
{
4647
run_test(expected_tasks, future)
4748
}
49+
50+
/// Spawn a named task and unwrap.
51+
///
52+
/// This is a convenience function to create a task with a name and then spawn
53+
/// it directly (unwrapping the `Result` which the task builder API returns).
54+
#[allow(dead_code)]
55+
pub(crate) fn spawn_named<Fut>(name: &str, f: Fut) -> JoinHandle<<Fut as Future>::Output>
56+
where
57+
Fut: Future + Send + 'static,
58+
Fut::Output: Send + 'static,
59+
{
60+
tokio::task::Builder::new()
61+
.name(name)
62+
.spawn(f)
63+
.expect(&format!("spawning task '{name}' failed"))
64+
}

console-subscriber/tests/support/subscriber.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -283,8 +283,7 @@ async fn record_actual_tasks(
283283

284284
for (id, stats) in &task_update.stats_update {
285285
if let Some(task) = tasks.get_mut(id) {
286-
task.wakes = stats.wakes;
287-
task.self_wakes = stats.self_wakes;
286+
task.update_from_stats(stats);
288287
}
289288
}
290289
}

console-subscriber/tests/support/task.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use std::{error, fmt};
22

3+
use console_api::tasks;
4+
35
use super::MAIN_TASK_NAME;
46

57
/// An actual task
@@ -13,6 +15,7 @@ pub(super) struct ActualTask {
1315
pub(super) name: Option<String>,
1416
pub(super) wakes: u64,
1517
pub(super) self_wakes: u64,
18+
pub(super) polls: u64,
1619
}
1720

1821
impl ActualTask {
@@ -22,6 +25,15 @@ impl ActualTask {
2225
name: None,
2326
wakes: 0,
2427
self_wakes: 0,
28+
polls: 0,
29+
}
30+
}
31+
32+
pub(super) fn update_from_stats(&mut self, stats: &tasks::Stats) {
33+
self.wakes = stats.wakes;
34+
self.self_wakes = stats.self_wakes;
35+
if let Some(poll_stats) = &stats.poll_stats {
36+
self.polls = poll_stats.polls;
2537
}
2638
}
2739
}
@@ -78,6 +90,7 @@ pub(crate) struct ExpectedTask {
7890
expect_present: Option<bool>,
7991
expect_wakes: Option<u64>,
8092
expect_self_wakes: Option<u64>,
93+
expect_polls: Option<u64>,
8194
}
8295

8396
impl Default for ExpectedTask {
@@ -87,6 +100,7 @@ impl Default for ExpectedTask {
87100
expect_present: None,
88101
expect_wakes: None,
89102
expect_self_wakes: None,
103+
expect_polls: None,
90104
}
91105
}
92106
}
@@ -164,6 +178,21 @@ impl ExpectedTask {
164178
}
165179
}
166180

181+
if let Some(expected_polls) = self.expect_polls {
182+
no_expectations = false;
183+
if expected_polls != actual_task.polls {
184+
return Err(TaskValidationFailure {
185+
expected: self.clone(),
186+
actual: Some(actual_task.clone()),
187+
failure: format!(
188+
"{self}: expected `polls` to be {expected_polls}, but \
189+
actual was {actual_polls}",
190+
actual_polls = actual_task.polls,
191+
),
192+
});
193+
}
194+
}
195+
167196
if no_expectations {
168197
return Err(TaskValidationFailure {
169198
expected: self.clone(),
@@ -229,6 +258,16 @@ impl ExpectedTask {
229258
self.expect_self_wakes = Some(self_wakes);
230259
self
231260
}
261+
262+
/// Expects that a task has a specific value for `polls`.
263+
///
264+
/// To validate, the actual task must have a count of polls (on
265+
/// `PollStats`) equal to `polls`.
266+
#[allow(dead_code)]
267+
pub(crate) fn expect_polls(mut self, polls: u64) -> Self {
268+
self.expect_polls = Some(polls);
269+
self
270+
}
232271
}
233272

234273
impl fmt::Display for ExpectedTask {

0 commit comments

Comments
 (0)