Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions api/src/urls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,26 @@ pub fn url_for_test_case(
Ok(url.to_string())
}

/// Short link to a single upload. Unlike [`url_for_test_case`] this needs no `repo` query
/// param: the collection short id fully scopes the webapp's lookup of the upload's
/// timestamp, which is the half of the canonical URL's key that cannot fit in a short link.
pub fn url_for_upload(
public_api_address: &str,
org_url_slug: &str,
test_collection_short_id: &str,
bundle_meta_id: &str,
) -> Result<String, ParseError> {
let mut url = Url::parse(convert_to_app_url(public_api_address).as_str())?;
url.set_path(
format!(
"{}/flaky-tests/collections/{}/u/{}",
org_url_slug, test_collection_short_id, bundle_meta_id
)
.as_str(),
);
Ok(url.to_string())
}

fn convert_to_app_url(public_api_address: &str) -> String {
public_api_address.replace("https://api.", "https://app.")
}
Expand Down Expand Up @@ -87,6 +107,23 @@ mod tests {
);
}

#[test]
fn test_upload_url_generated() {
let actual = url_for_upload(
"https://api.trunk-staging.io",
"bad-app-org",
"tc_123",
"82c6a6e5-f8ea-4d93-9a26-b8ab6ff8f6bc",
);

assert_eq!(
actual,
Ok(String::from(
"https://app.trunk-staging.io/bad-app-org/flaky-tests/collections/tc_123/u/82c6a6e5-f8ea-4d93-9a26-b8ab6ff8f6bc"
)),
);
}

#[test]
fn test_collection_url_generated() {
let actual = url_for_test_case(
Expand Down
51 changes: 43 additions & 8 deletions cli/src/upload_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ use std::path::PathBuf;
use std::sync::mpsc::Sender;

use api::client::{ApiClient, ApiErrorEndpoint};
use api::{client::get_api_host, urls::url_for_test_case};
use api::{
client::get_api_host,
urls::{url_for_test_case, url_for_upload},
};
use bundle::{BundleMeta, BundlerUtil, QuarantineResolutionMode, Test, unzip_tarball};
use clap::{ArgAction, Args};
use codeowners::OwnersSource;
Expand Down Expand Up @@ -384,6 +387,7 @@ pub struct UploadRunResult {
pub show_failure_messages: bool,
pub test_collection_short_id: Option<String>,
pub hide_test_collection_links: bool,
pub api_address: String,
}

pub struct RunUploadOptions {
Expand Down Expand Up @@ -658,6 +662,7 @@ pub async fn run_upload(
.test_collection_short_id
.filter(|id| !id.is_empty()),
hide_test_collection_links: upload_args.hide_test_collection_links,
api_address: api_client.api_host.clone(),
})
}

Expand Down Expand Up @@ -716,6 +721,31 @@ pub fn get_bundle_upload_id_message(bundle_upload_id: &str) -> String {
format!("🏷️ Bundle Upload ID: {}", bundle_upload_id)
}

pub fn get_bundle_upload_url_message(url: &str) -> String {
format!("🏷️ Upload: {}", url)
}

impl UploadRunResult {
/// Short link to this upload's page, when the run is in test-collection link mode.
/// Gated on `test_collection_short_id` rather than `base_props.test_collection.short_id`
/// so an exported-but-blank collection id can't yield a malformed `/collections//u/`
/// link; the payload supplies the bundle_meta id the link is keyed on.
fn collection_upload_url(&self) -> Option<String> {
if self.hide_test_collection_links {
return None;
}
let short_id = self.test_collection_short_id.as_deref()?;
let test_collection = self.meta.base_props.test_collection.as_ref()?;
url_for_upload(
&self.api_address,
&self.meta.base_props.org,
short_id,
&test_collection.bundle_meta_id,
)
.ok()
}
}

impl EndOutput for UploadRunResult {
fn output(&self) -> anyhow::Result<Vec<Line>> {
let mut output: Vec<Line> = Vec::new();
Expand All @@ -733,15 +763,20 @@ impl EndOutput for UploadRunResult {
}
}

// Add the bundle upload ID message
// Add the bundle upload message: the short link when the run is in
// test-collection link mode, otherwise the bare id as before.
{
let bundle_upload_id = self.meta.base_props.bundle_upload_id.clone();
if !bundle_upload_id.is_empty() {
let bundle_upload_id = &self.meta.base_props.bundle_upload_id;
let message = match self.collection_upload_url() {
Some(url) => Some(get_bundle_upload_url_message(&url)),
None if !bundle_upload_id.is_empty() => {
Some(get_bundle_upload_id_message(bundle_upload_id))
}
None => None,
};
if let Some(message) = message {
output.push(Line::from_iter([Span::new_styled(
style(get_bundle_upload_id_message(
&self.meta.base_props.bundle_upload_id,
))
.attribute(Attribute::Bold),
style(message).attribute(Attribute::Bold),
)?]));
output.push(Line::default());
}
Expand Down
55 changes: 52 additions & 3 deletions cli/tests/upload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,10 +292,10 @@ async fn upload_bundle() {

// HINT: View CLI output with `cargo test -- --nocapture`
println!("{assert}");
// Verify that the bundle upload ID message is printed
let bundle_upload_id = base_props.bundle_upload_id.clone();
// This run passes a collection id, so the upload surfaces as a short link rather
// than the bare id (the bare-id form is covered by the links-hidden test below).
assert.stderr(predicate::str::contains(
get_bundle_upload_id_message(&bundle_upload_id).as_str(),
"/test-org/flaky-tests/collections/tc_123/u/82c6a6e5-f8ea-4d93-9a26-b8ab6ff8f6bc",
));
}

Expand All @@ -322,6 +322,55 @@ async fn upload_bundle_prints_test_collection_links() {
.stderr(predicate::str::contains("?repo=trunk-io%2Fanalytics-cli"));
}

// NOTE: must be multi threaded to start a mock server
#[tokio::test(flavor = "multi_thread")]
async fn upload_bundle_prints_test_collection_upload_link() {
let temp_dir = tempdir().unwrap();
generate_mock_git_repo(&temp_dir);
generate_mock_valid_junit_xmls(&temp_dir);

let state = MockServerBuilder::new().spawn_mock_server().await;

let assert = CommandBuilder::upload(temp_dir.path(), state.host.clone())
.command()
.arg("--test-collection-id")
.arg("tc_123")
.assert()
.failure();

// the upload link is keyed on the bundle_meta id the server returned, not the
// bundle upload id, and needs no ?repo= — the short id scopes the lookup
assert
.stderr(predicate::str::contains(
"/test-org/flaky-tests/collections/tc_123/u/82c6a6e5-f8ea-4d93-9a26-b8ab6ff8f6bc",
))
.stderr(predicate::str::contains("Bundle Upload ID:").not());
}

// NOTE: must be multi threaded to start a mock server
#[tokio::test(flavor = "multi_thread")]
async fn upload_bundle_prints_bare_upload_id_when_links_hidden() {
let temp_dir = tempdir().unwrap();
generate_mock_git_repo(&temp_dir);
generate_mock_valid_junit_xmls(&temp_dir);

let state = MockServerBuilder::new().spawn_mock_server().await;

let assert = CommandBuilder::upload(temp_dir.path(), state.host.clone())
.command()
.env("TRUNK_HIDE_TEST_COLLECTION_LINKS", "true")
.arg("--test-collection-id")
.arg("tc_123")
.assert()
.failure();

assert
.stderr(predicate::str::contains(
get_bundle_upload_id_message("test-bundle-upload-id").as_str(),
))
.stderr(predicate::str::contains("/flaky-tests/collections/tc_123/u/").not());
}

// NOTE: must be multi threaded to start a mock server
#[tokio::test(flavor = "multi_thread")]
async fn upload_bundle_hides_test_collection_links_when_env_set() {
Expand Down
Loading