Speedup binstalk-git-repo-api unit testing (#1731)

* Share `remote::Client` between tests when in cargo-test

Signed-off-by: Jiahao XU <Jiahao_XU@outlook.com>

* Run rate-limited binstalk-git-repo-api test in serial

Signed-off-by: Jiahao XU <Jiahao_XU@outlook.com>

* Update client-side rate limit to 1 request per 300ms

Signed-off-by: Jiahao XU <Jiahao_XU@outlook.com>

* Reduce test cases in `rate_limited_test_has_release_artifact_and_download_artifacts`

Signed-off-by: Jiahao XU <Jiahao_XU@outlook.com>

* Add `cargo-nextest` that I forgot

Signed-off-by: Jiahao XU <Jiahao_XU@outlook.com>

* Fix unit testing: Pass auth-token to restful API

to avoid rate limit

Signed-off-by: Jiahao XU <Jiahao_XU@outlook.com>

---------

Signed-off-by: Jiahao XU <Jiahao_XU@outlook.com>
This commit is contained in:
Jiahao XU 2024-06-12 20:34:50 +10:00 committed by GitHub
parent 4d7a91aa4c
commit 6622bf1ae3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 75 additions and 35 deletions

6
.config/nextest.toml Normal file
View file

@ -0,0 +1,6 @@
[test-groups]
rate-limited = { max-threads = 1 }
[[profile.default.overrides]]
filter = 'test(rate_limited::)'
test-group = 'rate-limited'

1
Cargo.lock generated
View file

@ -354,6 +354,7 @@ version = "0.0.0"
dependencies = [ dependencies = [
"binstalk-downloader", "binstalk-downloader",
"compact_str", "compact_str",
"once_cell",
"percent-encoding", "percent-encoding",
"serde", "serde",
"serde-tuple-vec-map", "serde-tuple-vec-map",

View file

@ -26,3 +26,4 @@ url = "2.3.1"
[dev-dependencies] [dev-dependencies]
binstalk-downloader = { version = "0.11.1", path = "../binstalk-downloader" } binstalk-downloader = { version = "0.11.1", path = "../binstalk-downloader" }
tracing-subscriber = "0.3" tracing-subscriber = "0.3"
once_cell = "1"

View file

@ -131,6 +131,8 @@ struct Inner {
auth_token: Option<CompactString>, auth_token: Option<CompactString>,
is_auth_token_valid: AtomicBool, is_auth_token_valid: AtomicBool,
only_use_restful_api: AtomicBool,
} }
/// Github API client for querying whether a release artifact exitsts. /// Github API client for querying whether a release artifact exitsts.
@ -147,9 +149,16 @@ impl GhApiClient {
auth_token, auth_token,
is_auth_token_valid: AtomicBool::new(true), is_auth_token_valid: AtomicBool::new(true),
only_use_restful_api: AtomicBool::new(false),
})) }))
} }
/// If you don't want to use GitHub GraphQL API for whatever reason, call this.
pub fn set_only_use_restful_api(&self) {
self.0.only_use_restful_api.store(true, Relaxed);
}
pub fn remote_client(&self) -> &remote::Client { pub fn remote_client(&self) -> &remote::Client {
&self.0.client &self.0.client
} }
@ -193,12 +202,13 @@ impl GhApiClient {
) -> Result<U, GhApiError> ) -> Result<U, GhApiError>
where where
GraphQLFn: Fn(&remote::Client, &T, &str) -> GraphQLFut, GraphQLFn: Fn(&remote::Client, &T, &str) -> GraphQLFut,
RestfulFn: Fn(&remote::Client, &T) -> RestfulFut, RestfulFn: Fn(&remote::Client, &T, Option<&str>) -> RestfulFut,
GraphQLFut: Future<Output = Result<U, GhApiError>> + Send + Sync + 'static, GraphQLFut: Future<Output = Result<U, GhApiError>> + Send + Sync + 'static,
RestfulFut: Future<Output = Result<U, GhApiError>> + Send + Sync + 'static, RestfulFut: Future<Output = Result<U, GhApiError>> + Send + Sync + 'static,
{ {
self.check_retry_after()?; self.check_retry_after()?;
if !self.0.only_use_restful_api.load(Relaxed) {
if let Some(auth_token) = self.get_auth_token() { if let Some(auth_token) = self.get_auth_token() {
match graphql_func(&self.0.client, data, auth_token).await { match graphql_func(&self.0.client, data, auth_token).await {
Err(GhApiError::Unauthorized) => { Err(GhApiError::Unauthorized) => {
@ -207,8 +217,9 @@ impl GhApiClient {
res => return res.map_err(|err| err.context("GraphQL API")), res => return res.map_err(|err| err.context("GraphQL API")),
} }
} }
}
restful_func(&self.0.client, data) restful_func(&self.0.client, data, self.get_auth_token())
.await .await
.map_err(|err| err.context("Restful API")) .map_err(|err| err.context("Restful API"))
} }
@ -313,6 +324,7 @@ impl GhApiClient {
mod test { mod test {
use super::*; use super::*;
use compact_str::{CompactString, ToCompactString}; use compact_str::{CompactString, ToCompactString};
use once_cell::sync::OnceCell;
use std::{env, num::NonZeroU16, time::Duration}; use std::{env, num::NonZeroU16, time::Duration};
use tokio::time::sleep; use tokio::time::sleep;
use tracing::subscriber::set_global_default; use tracing::subscriber::set_global_default;
@ -368,6 +380,7 @@ mod test {
tag: CompactString::new_inline("cargo-audit/v0.17.6"), tag: CompactString::new_inline("cargo-audit/v0.17.6"),
}; };
#[allow(unused)]
pub(super) const ARTIFACTS: &[&str] = &[ pub(super) const ARTIFACTS: &[&str] = &[
"cargo-audit-aarch64-unknown-linux-gnu-v0.17.6.tgz", "cargo-audit-aarch64-unknown-linux-gnu-v0.17.6.tgz",
"cargo-audit-armv7-unknown-linux-gnueabihf-v0.17.6.tgz", "cargo-audit-armv7-unknown-linux-gnueabihf-v0.17.6.tgz",
@ -490,14 +503,20 @@ mod test {
} }
fn create_remote_client() -> remote::Client { fn create_remote_client() -> remote::Client {
static CLIENT: OnceCell<remote::Client> = OnceCell::new();
CLIENT
.get_or_init(|| {
remote::Client::new( remote::Client::new(
concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")), concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")),
None, None,
NonZeroU16::new(200).unwrap(), NonZeroU16::new(300).unwrap(),
1.try_into().unwrap(), 1.try_into().unwrap(),
[], [],
) )
.unwrap() .unwrap()
})
.clone()
} }
/// Mark this as an async fn so that you won't accidentally use it in /// Mark this as an async fn so that you won't accidentally use it in
@ -505,17 +524,24 @@ mod test {
fn create_client() -> Vec<GhApiClient> { fn create_client() -> Vec<GhApiClient> {
let client = create_remote_client(); let client = create_remote_client();
let mut gh_clients = vec![GhApiClient::new(client.clone(), None)]; let auth_token = env::var("CI_UNIT_TEST_GITHUB_TOKEN")
.ok()
.map(CompactString::from);
if let Ok(token) = env::var("CI_UNIT_TEST_GITHUB_TOKEN") { let gh_client = GhApiClient::new(client.clone(), auth_token.clone());
gh_clients.push(GhApiClient::new(client, Some(token.into()))); gh_client.set_only_use_restful_api();
let mut gh_clients = vec![gh_client];
if auth_token.is_some() {
gh_clients.push(GhApiClient::new(client, auth_token));
} }
gh_clients gh_clients
} }
#[tokio::test] #[tokio::test]
async fn test_get_repo_info() { async fn rate_limited_test_get_repo_info() {
const PUBLIC_REPOS: [GhRepo; 1] = [GhRepo { const PUBLIC_REPOS: [GhRepo; 1] = [GhRepo {
owner: CompactString::new_inline("cargo-bins"), owner: CompactString::new_inline("cargo-bins"),
repo: CompactString::new_inline("cargo-binstall"), repo: CompactString::new_inline("cargo-binstall"),
@ -575,17 +601,11 @@ mod test {
} }
#[tokio::test] #[tokio::test]
async fn test_has_release_artifact_and_download_artifacts() { async fn rate_limited_test_has_release_artifact_and_download_artifacts() {
const RELEASES: [(GhRelease, &[&str]); 2] = [ const RELEASES: [(GhRelease, &[&str]); 1] = [(
(
cargo_binstall_v0_20_1::RELEASE, cargo_binstall_v0_20_1::RELEASE,
cargo_binstall_v0_20_1::ARTIFACTS, cargo_binstall_v0_20_1::ARTIFACTS,
), )];
(
cargo_audit_v_0_17_6::RELEASE,
cargo_audit_v_0_17_6::ARTIFACTS,
),
];
const NON_EXISTENT_RELEASES: [GhRelease; 1] = [GhRelease { const NON_EXISTENT_RELEASES: [GhRelease; 1] = [GhRelease {
repo: GhRepo { repo: GhRepo {
owner: CompactString::new_inline("cargo-bins"), owner: CompactString::new_inline("cargo-bins"),

View file

@ -38,6 +38,7 @@ fn get_api_endpoint() -> &'static Url {
pub(super) fn issue_restful_api<T>( pub(super) fn issue_restful_api<T>(
client: &remote::Client, client: &remote::Client,
path: &[&str], path: &[&str],
auth_token: Option<&str>,
) -> impl Future<Output = Result<T, GhApiError>> + Send + Sync + 'static ) -> impl Future<Output = Result<T, GhApiError>> + Send + Sync + 'static
where where
T: DeserializeOwned, T: DeserializeOwned,
@ -50,11 +51,16 @@ where
debug!("Getting restful API: {url}"); debug!("Getting restful API: {url}");
let future = client let mut request_builder = client
.get(url) .get(url)
.header("Accept", "application/vnd.github+json") .header("Accept", "application/vnd.github+json")
.header("X-GitHub-Api-Version", "2022-11-28") .header("X-GitHub-Api-Version", "2022-11-28");
.send(false);
if let Some(auth_token) = auth_token {
request_builder = request_builder.bearer_auth(&auth_token);
}
let future = request_builder.send(false);
async move { async move {
let response = check_http_status_and_header(future.await?)?; let response = check_http_status_and_header(future.await?)?;

View file

@ -73,8 +73,13 @@ pub(super) fn fetch_release_artifacts_restful_api(
repo: GhRepo { owner, repo }, repo: GhRepo { owner, repo },
tag, tag,
}: &GhRelease, }: &GhRelease,
auth_token: Option<&str>,
) -> impl Future<Output = Result<Artifacts, GhApiError>> + Send + Sync + 'static { ) -> impl Future<Output = Result<Artifacts, GhApiError>> + Send + Sync + 'static {
issue_restful_api(client, &["repos", owner, repo, "releases", "tags", tag]) issue_restful_api(
client,
&["repos", owner, repo, "releases", "tags", tag],
auth_token,
)
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]

View file

@ -44,8 +44,9 @@ impl RepoInfo {
pub(super) fn fetch_repo_info_restful_api( pub(super) fn fetch_repo_info_restful_api(
client: &remote::Client, client: &remote::Client,
GhRepo { owner, repo }: &GhRepo, GhRepo { owner, repo }: &GhRepo,
auth_token: Option<&str>,
) -> impl Future<Output = Result<Option<RepoInfo>, GhApiError>> + Send + Sync + 'static { ) -> impl Future<Output = Result<Option<RepoInfo>, GhApiError>> + Send + Sync + 'static {
issue_restful_api(client, &["repos", owner, repo]) issue_restful_api(client, &["repos", owner, repo], auth_token)
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]