Skip to content

Commit dd97c88

Browse files
committed
Rewrite testing framework to use recordings only
1 parent 41be270 commit dd97c88

File tree

98 files changed

+26454
-6193
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

98 files changed

+26454
-6193
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ rand = "0.8.5"
4444
ignore = "0.4.18"
4545
postgres-types = { version = "0.2.4", features = ["derive"] }
4646
cron = { version = "0.12.0" }
47+
bytes = "1.1.0"
4748

4849
[dependencies.serde]
4950
version = "1"

src/github.rs

+46-54
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
use crate::test_record;
12
use anyhow::Context;
23
use async_trait::async_trait;
4+
use bytes::Bytes;
35
use chrono::{DateTime, FixedOffset, Utc};
46
use futures::{future::BoxFuture, FutureExt};
57
use hyper::header::HeaderValue;
@@ -21,22 +23,32 @@ pub struct User {
2123
}
2224

2325
impl GithubClient {
24-
async fn _send_req(&self, req: RequestBuilder) -> anyhow::Result<(Response, String)> {
26+
async fn send_req(&self, req: RequestBuilder) -> anyhow::Result<(Bytes, String)> {
2527
const MAX_ATTEMPTS: usize = 2;
26-
log::debug!("_send_req with {:?}", req);
28+
log::debug!("send_req with {:?}", req);
2729
let req_dbg = format!("{:?}", req);
2830
let req = req
2931
.build()
3032
.with_context(|| format!("building reqwest {}", req_dbg))?;
3133

34+
let test_capture_info = test_record::capture_request(&req);
35+
3236
let mut resp = self.client.execute(req.try_clone().unwrap()).await?;
3337
if let Some(sleep) = Self::needs_retry(&resp).await {
3438
resp = self.retry(req, sleep, MAX_ATTEMPTS).await?;
3539
}
40+
let status = resp.status();
41+
let maybe_err = resp.error_for_status_ref().err();
42+
let body = resp
43+
.bytes()
44+
.await
45+
.with_context(|| format!("failed to read response body {req_dbg}"))?;
46+
test_record::record_request(test_capture_info, status, &body);
47+
if let Some(e) = maybe_err {
48+
return Err(e.into());
49+
}
3650

37-
resp.error_for_status_ref()?;
38-
39-
Ok((resp, req_dbg))
51+
Ok((body, req_dbg))
4052
}
4153

4254
async fn needs_retry(resp: &Response) -> Option<Duration> {
@@ -116,6 +128,7 @@ impl GithubClient {
116128
.unwrap(),
117129
)
118130
.await?;
131+
rate_resp.error_for_status_ref()?;
119132
let rate_limit_response = rate_resp.json::<RateLimitResponse>().await?;
120133

121134
// Check url for search path because github has different rate limits for the search api
@@ -151,27 +164,12 @@ impl GithubClient {
151164
.boxed()
152165
}
153166

154-
async fn send_req(&self, req: RequestBuilder) -> anyhow::Result<Vec<u8>> {
155-
let (mut resp, req_dbg) = self._send_req(req).await?;
156-
157-
let mut body = Vec::new();
158-
while let Some(chunk) = resp.chunk().await.transpose() {
159-
let chunk = chunk
160-
.context("reading stream failed")
161-
.map_err(anyhow::Error::from)
162-
.context(req_dbg.clone())?;
163-
body.extend_from_slice(&chunk);
164-
}
165-
166-
Ok(body)
167-
}
168-
169167
pub async fn json<T>(&self, req: RequestBuilder) -> anyhow::Result<T>
170168
where
171169
T: serde::de::DeserializeOwned,
172170
{
173-
let (resp, req_dbg) = self._send_req(req).await?;
174-
Ok(resp.json().await.context(req_dbg)?)
171+
let (body, _req_dbg) = self.send_req(req).await?;
172+
Ok(serde_json::from_slice(&body)?)
175173
}
176174
}
177175

@@ -414,8 +412,8 @@ impl IssueRepository {
414412
async fn has_label(&self, client: &GithubClient, label: &str) -> anyhow::Result<bool> {
415413
#[allow(clippy::redundant_pattern_matching)]
416414
let url = format!("{}/labels/{}", self.url(client), label);
417-
match client._send_req(client.get(&url)).await {
418-
Ok((_, _)) => Ok(true),
415+
match client.send_req(client.get(&url)).await {
416+
Ok(_) => Ok(true),
419417
Err(e) => {
420418
if e.downcast_ref::<reqwest::Error>()
421419
.map_or(false, |e| e.status() == Some(StatusCode::NOT_FOUND))
@@ -495,7 +493,7 @@ impl Issue {
495493
body: &'a str,
496494
}
497495
client
498-
._send_req(client.patch(&edit_url).json(&ChangedIssue { body }))
496+
.send_req(client.patch(&edit_url).json(&ChangedIssue { body }))
499497
.await
500498
.context("failed to edit issue body")?;
501499
Ok(())
@@ -513,7 +511,7 @@ impl Issue {
513511
body: &'a str,
514512
}
515513
client
516-
._send_req(
514+
.send_req(
517515
client
518516
.patch(&comment_url)
519517
.json(&NewComment { body: new_body }),
@@ -534,7 +532,7 @@ impl Issue {
534532
.expect("expected api host");
535533
let comments_url = format!("{}{comments_path}", client.api_url);
536534
client
537-
._send_req(client.post(&comments_url).json(&PostComment { body }))
535+
.send_req(client.post(&comments_url).json(&PostComment { body }))
538536
.await
539537
.context("failed to post comment")?;
540538
Ok(())
@@ -560,7 +558,7 @@ impl Issue {
560558
}
561559

562560
client
563-
._send_req(client.delete(&url))
561+
.send_req(client.delete(&url))
564562
.await
565563
.context("failed to delete label")?;
566564

@@ -617,7 +615,7 @@ impl Issue {
617615
}
618616

619617
client
620-
._send_req(client.post(&url).json(&LabelsReq {
618+
.send_req(client.post(&url).json(&LabelsReq {
621619
labels: known_labels,
622620
}))
623621
.await
@@ -668,7 +666,7 @@ impl Issue {
668666
assignees: &'a [&'a str],
669667
}
670668
client
671-
._send_req(client.delete(&url).json(&AssigneeReq {
669+
.send_req(client.delete(&url).json(&AssigneeReq {
672670
assignees: &assignees[..],
673671
}))
674672
.await
@@ -761,7 +759,7 @@ impl Issue {
761759
}
762760
let url = format!("{}/issues/{}", self.repository().url(client), self.number);
763761
client
764-
._send_req(client.patch(&url).json(&SetMilestone {
762+
.send_req(client.patch(&url).json(&SetMilestone {
765763
milestone: milestone_no,
766764
}))
767765
.await
@@ -776,7 +774,7 @@ impl Issue {
776774
state: &'a str,
777775
}
778776
client
779-
._send_req(
777+
.send_req(
780778
client
781779
.patch(&edit_url)
782780
.json(&CloseIssue { state: "closed" }),
@@ -801,8 +799,9 @@ impl Issue {
801799
after
802800
));
803801
req = req.header("Accept", "application/vnd.github.v3.diff");
804-
let diff = client.send_req(req).await?;
805-
Ok(Some(String::from(String::from_utf8_lossy(&diff))))
802+
let (diff, _) = client.send_req(req).await?;
803+
let body = String::from_utf8_lossy(&diff).to_string();
804+
Ok(Some(body))
806805
}
807806

808807
/// Returns the commits from this pull request (no commits are returned if this `Issue` is not
@@ -1246,7 +1245,7 @@ impl Repository {
12461245
) -> anyhow::Result<()> {
12471246
let url = format!("{}/git/refs/{}", self.url(client), refname);
12481247
client
1249-
._send_req(client.patch(&url).json(&serde_json::json!({
1248+
.send_req(client.patch(&url).json(&serde_json::json!({
12501249
"sha": sha,
12511250
"force": true,
12521251
})))
@@ -1505,7 +1504,7 @@ impl Repository {
15051504
pub async fn merge_upstream(&self, client: &GithubClient, branch: &str) -> anyhow::Result<()> {
15061505
let url = format!("{}/merge-upstream", self.url(client));
15071506
client
1508-
._send_req(client.post(&url).json(&serde_json::json!({
1507+
.send_req(client.post(&url).json(&serde_json::json!({
15091508
"branch": branch,
15101509
})))
15111510
.await
@@ -1811,27 +1810,23 @@ impl GithubClient {
18111810
repo: &str,
18121811
branch: &str,
18131812
path: &str,
1814-
) -> anyhow::Result<Option<Vec<u8>>> {
1813+
) -> anyhow::Result<Option<Bytes>> {
18151814
let url = format!("{}/{repo}/{branch}/{path}", self.raw_url);
18161815
let req = self.get(&url);
18171816
let req_dbg = format!("{:?}", req);
18181817
let req = req
18191818
.build()
18201819
.with_context(|| format!("failed to build request {:?}", req_dbg))?;
1821-
let mut resp = self.client.execute(req).await.context(req_dbg.clone())?;
1820+
let test_capture_info = test_record::capture_request(&req);
1821+
let resp = self.client.execute(req).await.context(req_dbg.clone())?;
18221822
let status = resp.status();
1823+
let body = resp
1824+
.bytes()
1825+
.await
1826+
.with_context(|| format!("failed to read response body {req_dbg}"))?;
1827+
test_record::record_request(test_capture_info, status, &body);
18231828
match status {
1824-
StatusCode::OK => {
1825-
let mut buf = Vec::with_capacity(resp.content_length().unwrap_or(4) as usize);
1826-
while let Some(chunk) = resp.chunk().await.transpose() {
1827-
let chunk = chunk
1828-
.context("reading stream failed")
1829-
.map_err(anyhow::Error::from)
1830-
.context(req_dbg.clone())?;
1831-
buf.extend_from_slice(&chunk);
1832-
}
1833-
Ok(Some(buf))
1834-
}
1829+
StatusCode::OK => Ok(Some(body)),
18351830
StatusCode::NOT_FOUND => Ok(None),
18361831
status => anyhow::bail!("failed to GET {}: {}", url, status),
18371832
}
@@ -2036,11 +2031,8 @@ impl IssuesQuery for LeastRecentlyReviewedPullRequests {
20362031
let req = client.post(&format!("{}/graphql", client.api_url));
20372032
let req = req.json(&query);
20382033

2039-
let (resp, req_dbg) = client._send_req(req).await?;
2040-
let data = resp
2041-
.json::<cynic::GraphQlResponse<queries::LeastRecentlyReviewedPullRequests>>()
2042-
.await
2043-
.context(req_dbg)?;
2034+
let data: cynic::GraphQlResponse<queries::LeastRecentlyReviewedPullRequests> =
2035+
client.json(req).await?;
20442036
if let Some(errors) = data.errors {
20452037
anyhow::bail!("There were graphql errors. {:?}", errors);
20462038
}

src/handlers/github_releases.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ async fn load_changelog(
132132
.await?
133133
.ok_or_else(|| anyhow::Error::msg("missing file"))?;
134134

135-
Ok(String::from_utf8(resp)?)
135+
Ok(String::from_utf8(resp.to_vec())?)
136136
}
137137

138138
async fn load_paginated<T, R, F>(ctx: &Context, url: &str, key: F) -> anyhow::Result<HashMap<R, T>>

src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ pub mod payload;
2626
pub mod rfcbot;
2727
pub mod team;
2828
mod team_data;
29+
pub mod test_record;
2930
pub mod triage;
3031
pub mod zulip;
3132

src/main.rs

+2-66
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ use futures::future::FutureExt;
55
use futures::StreamExt;
66
use hyper::{header, Body, Request, Response, Server, StatusCode};
77
use route_recognizer::Router;
8-
use std::path::PathBuf;
98
use std::{env, net::SocketAddr, sync::Arc};
109
use tokio::{task, time};
1110
use tower::{Service, ServiceExt};
@@ -219,7 +218,7 @@ async fn serve_req(
219218
.unwrap());
220219
}
221220
};
222-
maybe_record_test(&event, &payload);
221+
triagebot::test_record::record_event(&event, &payload);
223222

224223
match triagebot::webhook(event, payload, &ctx).await {
225224
Ok(true) => Ok(Response::new(Body::from("processed request"))),
@@ -234,70 +233,6 @@ async fn serve_req(
234233
}
235234
}
236235

237-
/// Webhook recording to help with writing server_test integration tests.
238-
fn maybe_record_test(event: &EventName, payload: &str) {
239-
if std::env::var_os("TRIAGEBOT_TEST_RECORD").is_none() {
240-
return;
241-
}
242-
let sequence_path = |name| {
243-
let path = PathBuf::from(format!("{name}.json"));
244-
if !path.exists() {
245-
return path;
246-
}
247-
let mut index = 1;
248-
loop {
249-
let path = PathBuf::from(format!("{name}.{index}.json"));
250-
if !path.exists() {
251-
return path;
252-
}
253-
index += 1;
254-
}
255-
};
256-
let payload_json: serde_json::Value = serde_json::from_str(payload).expect("valid json");
257-
let name = match event {
258-
EventName::PullRequest => {
259-
let action = payload_json["action"].as_str().unwrap();
260-
let number = payload_json["number"].as_u64().unwrap();
261-
format!("pr{number}_{action}")
262-
}
263-
EventName::PullRequestReview => {
264-
let action = payload_json["action"].as_str().unwrap();
265-
let number = payload_json["pull_request"]["number"].as_u64().unwrap();
266-
format!("pr{number}_review_{action}")
267-
}
268-
EventName::PullRequestReviewComment => {
269-
let action = payload_json["action"].as_str().unwrap();
270-
let number = payload_json["pull_request"]["number"].as_u64().unwrap();
271-
format!("pr{number}_review_comment_{action}")
272-
}
273-
EventName::IssueComment => {
274-
let action = payload_json["action"].as_str().unwrap();
275-
let number = payload_json["issue"]["number"].as_u64().unwrap();
276-
format!("issue{number}_comment_{action}")
277-
}
278-
EventName::Issue => {
279-
let action = payload_json["action"].as_str().unwrap();
280-
let number = payload_json["issue"]["number"].as_u64().unwrap();
281-
format!("issue{number}_{action}")
282-
}
283-
EventName::Push => {
284-
let after = payload_json["after"].as_str().unwrap();
285-
format!("push_{after}")
286-
}
287-
EventName::Create => {
288-
let ref_type = payload_json["ref_type"].as_str().unwrap();
289-
let git_ref = payload_json["ref"].as_str().unwrap();
290-
format!("create_{ref_type}_{git_ref}")
291-
}
292-
EventName::Other => {
293-
return;
294-
}
295-
};
296-
let path = sequence_path(name);
297-
std::fs::write(&path, payload).unwrap();
298-
log::info!("recorded JSON payload to {:?}", path);
299-
}
300-
301236
async fn run_server(addr: SocketAddr) -> anyhow::Result<()> {
302237
let pool = db::ClientPool::new();
303238
db::run_migrations(&*pool.get().await)
@@ -426,6 +361,7 @@ async fn main() {
426361
.with_ansi(std::env::var_os("DISABLE_COLOR").is_none())
427362
.try_init()
428363
.unwrap();
364+
triagebot::test_record::init().unwrap();
429365

430366
let port = env::var("PORT")
431367
.ok()

0 commit comments

Comments
 (0)