Skip to content

Commit 81441ae

Browse files
authored
Merge pull request #107 from rust-lang/comments-comm
Implement comment checking in tests
2 parents f0537f1 + ea0cd98 commit 81441ae

File tree

7 files changed

+247
-142
lines changed

7 files changed

+247
-142
lines changed

src/bors/handlers/ping.rs

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,32 +17,15 @@ pub(super) async fn command_ping<Client: RepositoryClient>(
1717

1818
#[cfg(test)]
1919
mod tests {
20-
use tracing_test::traced_test;
21-
22-
use crate::tests::event::default_pr_number;
23-
use crate::tests::mocks::BorsBuilder;
24-
use crate::tests::state::ClientBuilder;
20+
use crate::tests::mocks::run_test;
2521

2622
#[sqlx::test]
2723
async fn test_ping(pool: sqlx::PgPool) {
28-
let state = ClientBuilder::default()
29-
.pool(pool.clone())
30-
.create_state()
31-
.await;
32-
state.comment("@bors ping").await;
33-
state
34-
.client()
35-
.check_comments(default_pr_number(), &["Pong 🏓!"]);
36-
}
37-
38-
#[traced_test]
39-
#[sqlx::test]
40-
async fn test_ping2(pool: sqlx::PgPool) {
41-
BorsBuilder::new(pool)
42-
.run_test(|mut tester| async {
43-
tester.comment("@bors ping").await;
44-
Ok(tester)
45-
})
46-
.await;
24+
run_test(pool, |mut tester| async {
25+
tester.post_comment("@bors ping").await;
26+
assert_eq!(tester.get_comment().await, "Pong 🏓!");
27+
Ok(tester)
28+
})
29+
.await;
4730
}
4831
}

src/tests/mocks/bors.rs

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ use tower::Service;
1010

1111
use crate::github::api::load_repositories;
1212
use crate::tests::database::MockedDBClient;
13+
use crate::tests::event::default_pr_number;
1314
use crate::tests::mocks::comment::{Comment, GitHubIssueCommentEventPayload};
1415
use crate::tests::mocks::webhook::{create_webhook_request, TEST_WEBHOOK_SECRET};
15-
use crate::tests::mocks::{ExternalHttpMock, World};
16+
use crate::tests::mocks::{ExternalHttpMock, Repo, World};
1617
use crate::{
1718
create_app, create_bors_process, BorsContext, CommandParser, ServerState, WebhookSecret,
1819
};
@@ -50,6 +51,17 @@ impl BorsBuilder {
5051
}
5152
}
5253

54+
/// Simple end-to-end test entrypoint for tests that don't need to prepare any custom state.
55+
pub async fn run_test<
56+
F: FnOnce(BorsTester) -> Fut,
57+
Fut: Future<Output = anyhow::Result<BorsTester>>,
58+
>(
59+
pool: PgPool,
60+
f: F,
61+
) {
62+
BorsBuilder::new(pool).run_test(f).await
63+
}
64+
5365
/// Represents a running bors web application and also the background
5466
/// bors process. This structure should be used in tests to test interaction
5567
/// with the bot.
@@ -58,6 +70,7 @@ impl BorsBuilder {
5870
/// send channels for the bors process, which should stop its async task.
5971
pub struct BorsTester {
6072
app: Router,
73+
http_mock: ExternalHttpMock,
6174
bors: JoinHandle<()>,
6275
}
6376

@@ -87,14 +100,32 @@ impl BorsTester {
87100
);
88101
let app = create_app(state);
89102
let bors = tokio::spawn(bors_process);
90-
Self { app, bors }
103+
Self {
104+
app,
105+
http_mock: mock,
106+
bors,
107+
}
91108
}
92109

93-
pub async fn comment(&mut self, content: &str) {
94-
self.webhook_comment(Comment::new(content)).await;
110+
/// Wait until the next bot comment is received on the default repo and the default PR.
111+
pub async fn get_comment(&mut self) -> String {
112+
self.http_mock
113+
.gh_server
114+
.get_comment(Repo::default().name, default_pr_number())
115+
.await
116+
.content
117+
}
118+
119+
pub async fn post_comment(&mut self, content: &str) {
120+
self.webhook_comment(Comment::new(
121+
Repo::default().name,
122+
default_pr_number(),
123+
content,
124+
))
125+
.await;
95126
}
96127

97-
pub async fn webhook_comment(&mut self, comment: Comment) {
128+
async fn webhook_comment(&mut self, comment: Comment) {
98129
self.send_webhook(
99130
"issue_comment",
100131
GitHubIssueCommentEventPayload::from(comment),
@@ -124,7 +155,11 @@ impl BorsTester {
124155
}
125156

126157
pub async fn finish(self) {
158+
// Make sure that the event channel senders are closed
127159
drop(self.app);
160+
// Wait until all events are handled in the bors service
128161
self.bors.await.unwrap();
162+
// Flush any local queues
163+
self.http_mock.gh_server.assert_empty_queues().await;
129164
}
130165
}

src/tests/mocks/comment.rs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,30 @@ use serde::Serialize;
66
use url::Url;
77

88
use crate::github::GithubRepoName;
9-
use crate::tests::event::default_pr_number;
10-
use crate::tests::mocks::repository::{GitHubRepository, Repo};
9+
use crate::tests::mocks::repository::GitHubRepository;
1110
use crate::tests::mocks::user::{GitHubUser, User};
1211

1312
#[derive(Clone, Debug)]
1413
pub struct Comment {
15-
repo: GithubRepoName,
16-
pr: u64,
17-
author: User,
18-
content: String,
14+
pub repo: GithubRepoName,
15+
pub pr: u64,
16+
pub author: User,
17+
pub content: String,
1918
}
2019

2120
impl Comment {
22-
pub fn new(content: &str) -> Self {
21+
pub fn new(repo: GithubRepoName, pr: u64, content: &str) -> Self {
2322
Self {
24-
repo: Repo::default().name,
25-
pr: default_pr_number(),
23+
repo,
24+
pr,
2625
author: User::default(),
2726
content: content.to_string(),
2827
}
2928
}
29+
30+
pub fn with_author(self, author: User) -> Self {
31+
Self { author, ..self }
32+
}
3033
}
3134

3235
// Copied from octocrab, since its version if #[non_exhaustive]

src/tests/mocks/github.rs

Lines changed: 97 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,83 @@
11
use octocrab::Octocrab;
2+
use std::collections::HashMap;
3+
use std::time::Duration;
24
use wiremock::MockServer;
35

46
use crate::create_github_client;
7+
use crate::github::GithubRepoName;
58
use crate::tests::mocks::app::{default_app_id, AppHandler};
6-
use crate::tests::mocks::repository::RepositoriesHandler;
9+
use crate::tests::mocks::comment::Comment;
10+
use crate::tests::mocks::repository::{mock_repo, mock_repo_list};
711
use crate::tests::mocks::World;
812

13+
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(3);
14+
15+
/// Represents the state of a simulated GH repo.
16+
pub struct GitHubRepoState {
17+
// We store comments from all PRs inside a single queue, because
18+
// we don't necessarily know beforehand which PRs will receive comments.
19+
comments_queue: tokio::sync::mpsc::Receiver<Comment>,
20+
// We need a local queue to avoid skipping comments received out of order.
21+
pending_comments: Vec<Comment>,
22+
}
23+
24+
impl GitHubRepoState {
25+
/// Wait until a comment from the given pull request was received.
26+
/// If a comment from a different PR is received, it is inserted into
27+
/// `pending_comments`, to be picked up later.
28+
async fn get_comment(&mut self, pr: u64) -> Comment {
29+
// First, try to resolve the comment from the pending comment list
30+
if let Some(index) = self.pending_comments.iter().position(|c| c.pr == pr) {
31+
return self.pending_comments.remove(index);
32+
}
33+
// If it is not there, wait until some comment is received
34+
loop {
35+
let comment = self
36+
.comments_queue
37+
.recv()
38+
.await
39+
.expect("Channel was closed while waiting for a comment");
40+
41+
if comment.pr == pr {
42+
return comment;
43+
}
44+
tracing::warn!(
45+
"Received comment for PR {}, while expected for PR {pr}",
46+
comment.pr
47+
);
48+
self.pending_comments.push(comment);
49+
}
50+
}
51+
}
52+
953
pub struct GitHubMockServer {
1054
mock_server: MockServer,
55+
repos: HashMap<GithubRepoName, GitHubRepoState>,
1156
}
1257

1358
impl GitHubMockServer {
1459
pub async fn start(world: &World) -> Self {
1560
let mock_server = MockServer::start().await;
16-
RepositoriesHandler.mount(world, &mock_server).await;
61+
mock_repo_list(world, &mock_server).await;
62+
63+
// Repositories are mocked separately to make it easier to
64+
// pass comm. channels to them.
65+
let mut repos = HashMap::default();
66+
for (name, repo) in &world.repos {
67+
let (comments_tx, comments_rx) = tokio::sync::mpsc::channel(1024);
68+
mock_repo(repo, comments_tx, &mock_server).await;
69+
repos.insert(
70+
name.clone(),
71+
GitHubRepoState {
72+
comments_queue: comments_rx,
73+
pending_comments: Default::default(),
74+
},
75+
);
76+
}
77+
1778
AppHandler::default().mount(&mock_server).await;
18-
Self { mock_server }
79+
80+
Self { mock_server, repos }
1981
}
2082

2183
pub fn client(&self) -> Octocrab {
@@ -26,6 +88,38 @@ impl GitHubMockServer {
2688
)
2789
.unwrap()
2890
}
91+
92+
pub async fn get_comment(&mut self, repo: GithubRepoName, pr: u64) -> Comment {
93+
let repo = self.repos.get_mut(&repo).unwrap();
94+
let fut = repo.get_comment(pr);
95+
tokio::time::timeout(DEFAULT_TIMEOUT, fut)
96+
.await
97+
.expect("Timed out while waiting for a comment to be received")
98+
}
99+
100+
/// Make sure that there are no leftover events left in the queues.
101+
pub async fn assert_empty_queues(mut self) {
102+
// This will remove all mocks and thus also any leftover
103+
// channel senders, so that we can be sure below that the `recv`
104+
// call will not block indefinitely.
105+
self.mock_server.reset().await;
106+
drop(self.mock_server);
107+
108+
for (name, repo) in self.repos.iter_mut() {
109+
if !repo.pending_comments.is_empty() {
110+
panic!(
111+
"Expected that {name} won't have any received comments, but it has {:?}",
112+
repo.pending_comments
113+
);
114+
}
115+
// Make sure that the queue has been closed and nothing is in it.
116+
if let Some(comment) = repo.comments_queue.recv().await {
117+
panic!(
118+
"Expected that {name} won't have any received comments, but it has {comment:?}"
119+
);
120+
}
121+
}
122+
}
29123
}
30124

31125
const GITHUB_MOCK_PRIVATE_KEY: &str = r###"-----BEGIN PRIVATE KEY-----

src/tests/mocks/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use crate::tests::mocks::github::GitHubMockServer;
88
use crate::tests::mocks::permissions::TeamApiMockServer;
99
use crate::TeamApiClient;
1010

11-
pub use bors::BorsBuilder;
11+
pub use bors::run_test;
1212
pub use repository::Repo;
1313
pub use user::User;
1414

0 commit comments

Comments
 (0)