diff --git a/Cargo.lock b/Cargo.lock index 80f1e5b9..58085892 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -105,6 +105,15 @@ version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" +[[package]] +name = "api" +version = "0.1.0" +dependencies = [ + "context", + "serde", + "serde_json", +] + [[package]] name = "arc-swap" version = "1.7.1" @@ -447,11 +456,18 @@ version = "0.1.0" dependencies = [ "anyhow", "chrono", + "gix", "junit-mock", + "openssl", "pretty_assertions", "quick-junit", "quick-xml", + "regex", + "serde", + "serde_json", "speedate", + "tempfile", + "test_utils", "thiserror", ] @@ -639,9 +655,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.33" +version = "1.0.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" dependencies = [ "crc32fast", "miniz_oxide", @@ -2655,6 +2671,21 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" +[[package]] +name = "test_utils" +version = "0.1.0" +dependencies = [ + "anyhow", + "api", + "axum", + "git2", + "junit-mock", + "tar", + "tempfile", + "tokio", + "zstd", +] + [[package]] name = "thiserror" version = "1.0.63" @@ -2858,16 +2889,16 @@ name = "trunk-analytics-cli" version = "0.0.0" dependencies = [ "anyhow", + "api", "assert_cmd", "assert_matches", "axum", "chrono", "clap", "codeowners", + "context", "env_logger", "exitcode", - "git2", - "gix", "glob", "junit-mock", "junit-parser", @@ -2880,6 +2911,7 @@ dependencies = [ "serde_json", "tar", "tempfile", + "test_utils", "tokio", "tokio-retry", "uuid", @@ -3318,9 +3350,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.18" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index d2beb673..1d48f18d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["cli", "codeowners", "junit-mock", "context"] +members = ["api", "cli", "codeowners", "context", "junit-mock", "test_utils"] resolver = "2" [profile.release] diff --git a/api/Cargo.toml b/api/Cargo.toml new file mode 100644 index 00000000..0490f4fb --- /dev/null +++ b/api/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "api" +version = "0.1.0" +edition = "2021" + +[dependencies] +context = { path = "../context" } +serde = { version = "1.0.130", default-features = false, features = ["derive"] } +serde_json = "1.0.68" diff --git a/api/src/lib.rs b/api/src/lib.rs new file mode 100644 index 00000000..296aa832 --- /dev/null +++ b/api/src/lib.rs @@ -0,0 +1,42 @@ +use std::collections::HashSet; + +use context::repo::RepoUrlParts; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Clone, Deserialize, PartialEq, Eq)] +pub struct CreateBundleUploadRequest { + pub repo: RepoUrlParts, + #[serde(rename = "orgUrlSlug")] + pub org_url_slug: String, +} + +#[derive(Debug, Serialize, Clone, Deserialize)] +pub struct CreateBundleUploadResponse { + pub id: String, + pub url: String, + pub key: String, +} + +#[derive(Debug, Serialize, Clone, Deserialize, PartialEq, Eq)] +pub struct CreateRepoRequest { + pub repo: RepoUrlParts, + #[serde(rename = "orgUrlSlug")] + pub org_url_slug: String, + #[serde(rename = "remoteUrls")] + pub remote_urls: Vec, +} + +#[derive(Debug, Serialize, Clone, Deserialize, PartialEq, Eq)] +pub struct GetQuarantineBulkTestStatusRequest { + pub repo: RepoUrlParts, + #[serde(rename = "orgUrlSlug")] + pub org_url_slug: String, +} + +#[derive(Debug, Serialize, Clone, Deserialize, Default)] +pub struct QuarantineConfig { + #[serde(rename = "isPreview")] + pub is_preview_mode: bool, + #[serde(rename = "testIds")] + pub quarantined_tests: HashSet, +} diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 181b7e37..e2a024f2 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -13,8 +13,10 @@ path = "src/lib.rs" [dependencies] anyhow = "1.0.44" +api = { path = "../api" } chrono = { version = "0.4.33", default-features = false, features = ["clock"] } clap = { version = "4.4.18", features = ["derive", "env"] } +context = { path = "../context" } env_logger = { version = "0.11.0", default-features = false } log = "0.4.14" exitcode = "1.1.1" @@ -24,7 +26,6 @@ tokio = { version = "*", default-features = false, features = [ ] } tempfile = "3.2.0" tokio-retry = { version = "0.3", default-features = false } -gix = { version = "0.63.0", default-features = false, features = [] } glob = "0.3.0" regex = { version = "1.10.3", default-features = false, features = ["std"] } reqwest = { version = "0.12.5", default-features = false, features = [ @@ -46,8 +47,8 @@ uuid = { version = "1.10.0", features = ["v5"] } assert_cmd = "2.0.16" assert_matches = "1.5.0" axum = { version = "0.7.5", features = ["macros"] } -git2 = "0.19.0" # Used for creating test repos with libgit2 junit-mock = { path = "../junit-mock" } +test_utils = { path = "../test_utils" } [build-dependencies] vergen = { version = "8.3.1", features = [ diff --git a/cli/src/clients.rs b/cli/src/clients.rs index b37d317e..2a179281 100644 --- a/cli/src/clients.rs +++ b/cli/src/clients.rs @@ -1,12 +1,12 @@ -use std::format; -use std::path::PathBuf; +use std::{format, path::PathBuf}; use anyhow::Context; - -use crate::types::{ +use api::{ CreateBundleUploadRequest, CreateBundleUploadResponse, CreateRepoRequest, - GetQuarantineBulkTestStatusRequest, QuarantineConfig, Repo, + GetQuarantineBulkTestStatusRequest, QuarantineConfig, }; +use context::repo::RepoUrlParts; + use crate::utils::status_code_help; pub const TRUNK_API_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30); @@ -16,7 +16,7 @@ pub async fn create_trunk_repo( origin: &str, api_token: &str, org_slug: &str, - repo: &Repo, + repo: &RepoUrlParts, remote_urls: &[String], ) -> anyhow::Result<()> { let client = reqwest::Client::new(); @@ -52,7 +52,7 @@ pub async fn create_bundle_upload_intent( origin: &str, api_token: &str, org_slug: &str, - repo: &Repo, + repo: &RepoUrlParts, ) -> anyhow::Result { let client = reqwest::Client::new(); let resp = match client @@ -87,7 +87,7 @@ pub async fn get_quarantining_config( origin: &str, api_token: &str, org_slug: &str, - repo: &Repo, + repo: &RepoUrlParts, ) -> anyhow::Result { let client = reqwest::Client::new(); let resp = match client diff --git a/cli/src/main.rs b/cli/src/main.rs index 190fd465..deb8a3ab 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -3,6 +3,7 @@ use std::io::Write; use std::time::{SystemTime, UNIX_EPOCH}; use clap::{Args, Parser, Subcommand}; +use context::repo::BundleRepo; use tokio_retry::strategy::ExponentialBackoff; use tokio_retry::Retry; use trunk_analytics_cli::bundler::BundlerUtil; @@ -16,11 +17,11 @@ use trunk_analytics_cli::constants::{ use trunk_analytics_cli::runner::{ build_filesets, extract_failed_tests, run_quarantine, run_test_command, }; -use trunk_analytics_cli::scanner::{BundleRepo, EnvScanner}; +use trunk_analytics_cli::scanner::EnvScanner; use trunk_analytics_cli::types::{ BundleMeta, QuarantineBulkTestStatus, QuarantineRunResult, RunResult, META_VERSION, }; -use trunk_analytics_cli::utils::{from_non_empty_or_default, parse_custom_tags}; +use trunk_analytics_cli::utils::parse_custom_tags; #[derive(Debug, Parser)] #[command( @@ -159,7 +160,7 @@ async fn run_upload( codeowners_path, } = upload_args; - let repo = BundleRepo::try_read_from_root( + let repo = BundleRepo::new( repo_root, repo_url, repo_head_sha, @@ -171,11 +172,7 @@ async fn run_upload( return Err(anyhow::anyhow!("No junit paths provided.")); } - let api_address = from_non_empty_or_default( - std::env::var(TRUNK_PUBLIC_API_ADDRESS_ENV).ok(), - DEFAULT_ORIGIN.to_string(), - |s| s, - ); + let api_address = get_api_address(); let codeowners = codeowners.or_else(|| CodeOwners::find_file(&repo.repo_root, &codeowners_path)); @@ -340,7 +337,7 @@ async fn run_test(test_args: TestArgs) -> anyhow::Result { .. } = &upload_args; - let repo = BundleRepo::try_read_from_root( + let repo = BundleRepo::new( repo_root.clone(), repo_url.clone(), repo_head_sha.clone(), @@ -352,11 +349,7 @@ async fn run_test(test_args: TestArgs) -> anyhow::Result { return Err(anyhow::anyhow!("No junit paths provided.")); } - let api_address = from_non_empty_or_default( - std::env::var(TRUNK_PUBLIC_API_ADDRESS_ENV).ok(), - DEFAULT_ORIGIN.to_string(), - |s| s, - ); + let api_address = get_api_address(); let codeowners = CodeOwners::find_file(&repo.repo_root, codeowners_path); @@ -456,3 +449,10 @@ fn setup_logger() -> anyhow::Result<()> { builder.init(); Ok(()) } + +fn get_api_address() -> String { + std::env::var(TRUNK_PUBLIC_API_ADDRESS_ENV) + .ok() + .and_then(|s| if s.is_empty() { None } else { Some(s) }) + .unwrap_or_else(|| DEFAULT_ORIGIN.to_string()) +} diff --git a/cli/src/runner.rs b/cli/src/runner.rs index a79d83fd..4fbffca4 100644 --- a/cli/src/runner.rs +++ b/cli/src/runner.rs @@ -1,6 +1,8 @@ use std::process::{Command, Stdio}; use std::time::SystemTime; +use api::QuarantineConfig; +use context::repo::BundleRepo; use junit_parser; use tokio_retry::strategy::ExponentialBackoff; use tokio_retry::Retry; @@ -8,10 +10,8 @@ use tokio_retry::Retry; use crate::clients::get_quarantining_config; use crate::codeowners::CodeOwners; use crate::constants::{EXIT_FAILURE, EXIT_SUCCESS}; -use crate::scanner::{BundleRepo, FileSet, FileSetCounter}; -use crate::types::{ - QuarantineBulkTestStatus, QuarantineConfig, QuarantineRunResult, RunResult, Test, -}; +use crate::scanner::{FileSet, FileSetCounter}; +use crate::types::{QuarantineBulkTestStatus, QuarantineRunResult, RunResult, Test}; pub async fn run_test_command( repo: &BundleRepo, diff --git a/cli/src/scanner.rs b/cli/src/scanner.rs index 799ebd2f..e889e06d 100644 --- a/cli/src/scanner.rs +++ b/cli/src/scanner.rs @@ -7,16 +7,7 @@ use serde::{Deserialize, Serialize}; use crate::codeowners::{CodeOwners, Owners}; use crate::constants::{ALLOW_LIST, ENVS_TO_GET}; -use crate::types::{BundledFile, FileSetType, Repo}; -use crate::utils::from_non_empty_or_default; - -pub const GIT_REMOTE_ORIGIN_URL_CONFIG: &str = "remote.origin.url"; - -#[derive(Debug)] -struct HeadAuthor { - pub name: String, - pub email: String, -} +use crate::types::{BundledFile, FileSetType}; #[derive(Default, Debug)] pub struct FileSetCounter { @@ -145,146 +136,6 @@ impl FileSet { } } -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct BundleRepo { - pub repo: Repo, - pub repo_root: String, - pub repo_url: String, - pub repo_head_sha: String, - pub repo_head_branch: String, - pub repo_head_commit_epoch: i64, - pub repo_head_commit_message: String, - pub repo_head_author_name: String, - pub repo_head_author_email: String, -} - -impl BundleRepo { - /// Read important fields from git repo root. - /// - pub fn try_read_from_root( - in_repo_root: Option, - in_repo_url: Option, - in_repo_head_sha: Option, - in_repo_head_branch: Option, - in_repo_head_commit_epoch: Option, - ) -> anyhow::Result { - let mut out_repo_url = in_repo_url.clone(); - let mut out_repo_head_sha = in_repo_head_sha.clone(); - let mut out_repo_head_branch = in_repo_head_branch.clone(); - let mut out_repo_head_commit_epoch = - from_non_empty_or_default(in_repo_head_commit_epoch, None, |s| { - Some( - s.parse::() - .expect("failed to parse commit epoch override"), - ) - }); - let out_repo_root = - from_non_empty_or_default(in_repo_root, Self::default_to_working_directory(), Some); - - let mut git_head_author = None; - let mut git_head_commit_message = None; - // If repo root found, try to get repo details from git. - if let Some(repo_root) = &out_repo_root { - // Read git repo. - log::info!("Reading git repo at {:?}", &repo_root); - - let git_repo = gix::open(repo_root)?; - let git_url = git_repo - .config_snapshot() - .string_by_key(GIT_REMOTE_ORIGIN_URL_CONFIG) - .map(|s| s.to_string()); - let mut git_head = git_repo.head()?; - - let mut git_head_branch = git_head.referent_name().map(|s| s.as_bstr().to_string()); - if git_head_branch.is_none() { - for r in git_repo.references()?.remote_branches()? { - match r { - Ok(r) => { - let target = r.target(); - let id = target.try_id(); - if id.is_some() - && git_head.id().is_some() - && id.unwrap().to_string() == git_head.id().unwrap().to_string() - { - git_head_branch = - r.name().to_path().to_str().map(|s| s.to_string()); - break; - }; - } - Err(e) => { - log::debug!("Unexpected error when trying to find reference {:?}", e); - } - } - } - } - let git_head_sha = git_head.id().map(|id| id.to_string()); - let git_head_commit_time = git_head.peel_to_commit_in_place()?.time()?; - git_head_commit_message = git_head.peel_to_commit_in_place().map_or(None, |commit| { - commit - .message() - .map_or(None, |msg| Some(msg.title.to_string())) - }); - git_head_author = git_head - .peel_to_commit_in_place() - .map(|commit| { - if let Ok(author) = commit.author() { - Some(HeadAuthor { - name: author.name.to_string(), - email: author.email.to_string(), - }) - } else { - None - } - }) - .ok() - .flatten(); - log::info!("Found git_url: {:?}", git_url); - log::info!("Found git_sha: {:?}", git_head_sha); - log::info!("Found git_branch: {:?}", git_head_branch); - log::info!("Found git_commit_time: {:?}", git_head_commit_time); - log::info!("Found git_commit_message: {:?}", git_head_commit_message); - log::info!("Found git_author: {:?}", git_head_author); - - out_repo_url = from_non_empty_or_default(in_repo_url, git_url, Some); - out_repo_head_sha = from_non_empty_or_default(in_repo_head_sha, git_head_sha, Some); - out_repo_head_branch = - from_non_empty_or_default(in_repo_head_branch, git_head_branch, Some); - if out_repo_head_commit_epoch.is_none() { - out_repo_head_commit_epoch = Some(git_head_commit_time.seconds); - } - } - - // Require URL which should be known at this point. - let repo_url = out_repo_url.expect("failed to get repo url"); - let repo = Repo::from_url(&repo_url)?; - let (git_head_author_name, git_head_author_email) = if let Some(author) = git_head_author { - (author.name, author.email) - } else { - (String::default(), String::default()) - }; - - Ok(BundleRepo { - repo, - repo_root: out_repo_root.unwrap_or("".to_string()), - repo_url, - repo_head_branch: out_repo_head_branch.unwrap_or_default(), - repo_head_sha: out_repo_head_sha.expect("failed to get repo head sha"), - repo_head_commit_epoch: out_repo_head_commit_epoch - .expect("failed to get repo head commit time"), - repo_head_commit_message: git_head_commit_message.unwrap_or("".to_string()), - repo_head_author_name: git_head_author_name, - repo_head_author_email: git_head_author_email, - }) - } - - fn default_to_working_directory() -> Option { - std::env::current_dir() - .expect("failed to resolve working directory") - .to_str() - .map(|s| s.to_string()) - } -} - pub struct EnvScanner; impl EnvScanner { diff --git a/cli/src/types.rs b/cli/src/types.rs index f64dc1da..ca79e5f3 100644 --- a/cli/src/types.rs +++ b/cli/src/types.rs @@ -1,10 +1,8 @@ -use std::collections::HashSet; - -use regex::Regex; +use context::repo::BundleRepo; use serde::{Deserialize, Serialize}; use crate::codeowners::CodeOwners; -use crate::scanner::{BundleRepo, FileSet}; +use crate::scanner::FileSet; pub struct RunResult { pub exit_code: i32, @@ -17,29 +15,6 @@ pub struct QuarantineRunResult { pub quarantine_status: QuarantineBulkTestStatus, } -#[derive(Debug, Serialize, Clone, Deserialize, PartialEq, Eq)] -pub struct CreateRepoRequest { - pub repo: Repo, - #[serde(rename = "orgUrlSlug")] - pub org_url_slug: String, - #[serde(rename = "remoteUrls")] - pub remote_urls: Vec, -} - -#[derive(Debug, Serialize, Clone, Deserialize, PartialEq, Eq)] -pub struct CreateBundleUploadRequest { - pub repo: Repo, - #[serde(rename = "orgUrlSlug")] - pub org_url_slug: String, -} - -#[derive(Debug, Serialize, Clone, Deserialize, PartialEq, Eq)] -pub struct GetQuarantineBulkTestStatusRequest { - pub repo: Repo, - #[serde(rename = "orgUrlSlug")] - pub org_url_slug: String, -} - #[derive(Debug, Serialize, Clone, Deserialize)] pub struct Test { pub name: String, @@ -91,84 +66,6 @@ pub struct QuarantineBulkTestStatus { pub quarantine_results: Vec, } -#[derive(Debug, Serialize, Clone, Deserialize, Default)] -pub struct QuarantineConfig { - #[serde(rename = "isPreview")] - pub is_preview_mode: bool, - #[serde(rename = "testIds")] - pub quarantined_tests: HashSet, -} - -#[derive(Debug, Serialize, Clone, Deserialize)] -pub struct CreateBundleUploadResponse { - pub id: String, - pub url: String, - pub key: String, -} - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -pub struct Repo { - pub host: String, - pub owner: String, - pub name: String, -} - -impl Repo { - pub fn from_url(url: &str) -> anyhow::Result { - let re1 = Regex::new(r"^(ssh|git|http|https|ftp|ftps)://([^/]*?@)?([^/]*)/(.+)/([^/]+)")?; - let re2 = Regex::new(r"^([^/]*?@)([^/]*):(.+)/([^/]+)")?; - - let parts = if re1.is_match(url) { - let caps = re1.captures(url).expect("failed to parse url"); - if caps.len() != 6 { - return Err(anyhow::anyhow!( - "Invalid repo url format. Expected 6 parts: {:?} (url: {})", - caps, - url - )); - } - let domain = caps.get(3).map(|m| m.as_str()).unwrap_or(""); - let owner = caps.get(4).map(|m| m.as_str()).unwrap_or(""); - let name = caps.get(5).map(|m| m.as_str()).unwrap_or(""); - (domain, owner, name) - } else if re2.is_match(url) { - let caps = re2.captures(url).expect("failed to parse url"); - if caps.len() != 5 { - return Err(anyhow::anyhow!( - "Invalid repo url format. Expected 6 parts: {:?} (url: {})", - caps, - url - )); - } - let domain = caps.get(2).map(|m| m.as_str()).unwrap_or(""); - let owner = caps.get(3).map(|m| m.as_str()).unwrap_or(""); - let name = caps.get(4).map(|m| m.as_str()).unwrap_or(""); - (domain, owner, name) - } else { - return Err(anyhow::anyhow!("Invalid repo url format: {}", url)); - }; - - let host = parts.0.trim().to_string(); - let owner = parts.1.trim().to_string(); - let name = parts - .2 - .trim() - .trim_end_matches('/') - .trim_end_matches(".git") - .to_string(); - - if host.is_empty() || owner.is_empty() || name.is_empty() { - return Err(anyhow::anyhow!( - "Invalid repo url format. Expected non-empty parts: {:?} (url: {})", - parts, - url - )); - } - - Ok(Repo { host, owner, name }) - } -} - #[derive(Debug, Serialize, Clone)] pub struct BundleUploader { pub org_slug: String, @@ -218,195 +115,10 @@ pub struct BundleMeta { #[cfg(test)] mod tests { + use context::repo::RepoUrlParts; use super::*; - #[test] - fn test_parse_ssh_urls() { - let good_urls = &[ - ( - "git@github.com:user/repository.git", - Repo { - host: "github.com".to_string(), - owner: "user".to_string(), - name: "repository".to_string(), - }, - ), - ( - "git@gitlab.com:group/project.git", - Repo { - host: "gitlab.com".to_string(), - owner: "group".to_string(), - name: "project".to_string(), - }, - ), - ( - "git@bitbucket.org:team/repo.git", - Repo { - host: "bitbucket.org".to_string(), - owner: "team".to_string(), - name: "repo".to_string(), - }, - ), - ( - "git@ssh.dev.azure.com:company/project", - Repo { - host: "ssh.dev.azure.com".to_string(), - owner: "company".to_string(), - name: "project".to_string(), - }, - ), - ( - "git@sourceforge.net:owner/repo.git", - Repo { - host: "sourceforge.net".to_string(), - owner: "owner".to_string(), - name: "repo".to_string(), - }, - ), - ]; - - for (url, expected) in good_urls { - let actual = Repo::from_url(url).unwrap(); - assert_eq!(actual, *expected); - } - } - - #[test] - fn test_parse_https_urls() { - let good_urls = &[ - ( - "https://github.com/username/repository.git", - Repo { - host: "github.com".to_string(), - owner: "username".to_string(), - name: "repository".to_string(), - }, - ), - ( - "https://gitlab.com/group/project.git", - Repo { - host: "gitlab.com".to_string(), - owner: "group".to_string(), - name: "project".to_string(), - }, - ), - ( - "https://bitbucket.org/teamname/reponame.git", - Repo { - host: "bitbucket.org".to_string(), - owner: "teamname".to_string(), - name: "reponame".to_string(), - }, - ), - ( - "https://dev.azure.com/organization/project", - Repo { - host: "dev.azure.com".to_string(), - owner: "organization".to_string(), - name: "project".to_string(), - }, - ), - ( - "https://gitlab.example.edu/groupname/project.git", - Repo { - host: "gitlab.example.edu".to_string(), - owner: "groupname".to_string(), - name: "project".to_string(), - }, - ), - ]; - - for (url, expected) in good_urls { - let actual = Repo::from_url(url).unwrap(); - assert_eq!(actual, *expected); - } - } - - #[test] - fn test_parse_git_urls() { - let good_urls = &[ - ( - "ssh://github.com/github/testrepo", - Repo { - host: "github.com".to_string(), - owner: "github".to_string(), - name: "testrepo".to_string(), - }, - ), - ( - "git://github.com/github/testrepo", - Repo { - host: "github.com".to_string(), - owner: "github".to_string(), - name: "testrepo".to_string(), - }, - ), - ( - "http://github.com/github/testrepo", - Repo { - host: "github.com".to_string(), - owner: "github".to_string(), - name: "testrepo".to_string(), - }, - ), - ( - "https://github.com/github/testrepo", - Repo { - host: "github.com".to_string(), - owner: "github".to_string(), - name: "testrepo".to_string(), - }, - ), - ( - "ftp://github.com/github/testrepo", - Repo { - host: "github.com".to_string(), - owner: "github".to_string(), - name: "testrepo".to_string(), - }, - ), - ( - "ftps://github.com/github/testrepo", - Repo { - host: "github.com".to_string(), - owner: "github".to_string(), - name: "testrepo".to_string(), - }, - ), - ( - "user@github.com:github/testrepo", - Repo { - host: "github.com".to_string(), - owner: "github".to_string(), - name: "testrepo".to_string(), - }, - ), - ]; - - let bad_urls = &[ - "sshy://github.com/github/testrepo", - "ssh://github.com//testrepo", - "ssh:/github.com//testrepo", - "ssh:///testrepo", - "ssh://github.com/github/", - ]; - - for (url, expected) in good_urls { - let actual1 = Repo::from_url(url).unwrap(); - assert_eq!(actual1, *expected); - let actual2 = Repo::from_url(&(url.to_string() + ".git")).unwrap(); - assert_eq!(actual2, *expected); - let actual3 = Repo::from_url(&(url.to_string() + ".git/")).unwrap(); - assert_eq!(actual3, *expected); - } - - for url in bad_urls { - let actual = Repo::from_url(url); - assert!(actual.is_err()); - } - } - #[test] pub fn test_parse_good_custom_tags() { let good_tags = &[ @@ -500,7 +212,7 @@ mod tests { let file = Some("file".to_string()); let org_slug = "org_slug"; let repo = BundleRepo { - repo: crate::types::Repo { + repo: RepoUrlParts { host: "host".to_string(), owner: "owner".to_string(), name: "name".to_string(), diff --git a/cli/src/utils.rs b/cli/src/utils.rs index 0c9c0959..30467d70 100644 --- a/cli/src/utils.rs +++ b/cli/src/utils.rs @@ -23,19 +23,6 @@ pub fn status_code_help(status: reqwest::StatusCode) -> String { .to_string() } -pub fn from_non_empty_or_default R>( - s: Option, - default: R, - from_non_empty: F, -) -> R { - if let Some(s) = s { - if !s.trim().is_empty() { - return from_non_empty(s); - } - } - default -} - pub fn parse_custom_tags(tags: &[String]) -> anyhow::Result> { let parsed = tags.iter() .filter(|tag_str| !tag_str.trim().is_empty()) diff --git a/cli/tests/scanner.rs b/cli/tests/scanner.rs deleted file mode 100644 index 31384970..00000000 --- a/cli/tests/scanner.rs +++ /dev/null @@ -1,181 +0,0 @@ -use test_utils::mock_git_repo::{setup_repo_with_commit, TEST_BRANCH, TEST_ORIGIN}; -use trunk_analytics_cli::scanner::*; -use trunk_analytics_cli::types::Repo; - -mod test_utils; - -#[test] -fn test_try_read_from_root() { - let root = tempfile::tempdir() - .expect("failed to create temp directory") - .into_path(); - setup_repo_with_commit(&root).expect("failed to setup repo"); - let bundle_repo = BundleRepo::try_read_from_root( - Some(root.to_str().unwrap().to_string()), - None, - None, - None, - None, - ); - - assert!(bundle_repo.is_ok()); - let bundle_repo = bundle_repo.unwrap(); - assert_eq!(bundle_repo.repo_root, root.to_str().unwrap()); - assert_eq!( - bundle_repo.repo, - Repo { - host: "github.com".to_string(), - owner: "trunk-io".to_string(), - name: "analytics-cli".to_string(), - } - ); - assert_eq!(bundle_repo.repo_url, TEST_ORIGIN); - assert_eq!( - bundle_repo.repo_head_branch, - format!("refs/heads/{}", TEST_BRANCH) - ); - assert_eq!(bundle_repo.repo_head_sha.len(), 40); - assert!(bundle_repo.repo_head_commit_epoch > 0); - assert_eq!(bundle_repo.repo_head_commit_message, "Initial commit"); -} - -#[test] -fn test_try_read_from_root_with_url_override() { - let root = tempfile::tempdir() - .expect("failed to create temp directory") - .into_path(); - setup_repo_with_commit(&root).expect("failed to setup repo"); - let origin_url = "https://host.com/owner/repo.git"; - let bundle_repo = BundleRepo::try_read_from_root( - Some(root.to_str().unwrap().to_string()), - Some(origin_url.to_string()), - None, - None, - None, - ); - - assert!(bundle_repo.is_ok()); - let bundle_repo = bundle_repo.unwrap(); - assert_eq!(bundle_repo.repo_root, root.to_str().unwrap()); - assert_eq!( - bundle_repo.repo, - Repo { - host: "host.com".to_string(), - owner: "owner".to_string(), - name: "repo".to_string(), - } - ); - assert_eq!(bundle_repo.repo_url, origin_url); - assert_eq!( - bundle_repo.repo_head_branch, - format!("refs/heads/{}", TEST_BRANCH) - ); - assert_eq!(bundle_repo.repo_head_sha.len(), 40); - assert!(bundle_repo.repo_head_commit_epoch > 0); - assert_eq!(bundle_repo.repo_head_commit_message, "Initial commit"); -} - -#[test] -fn test_try_read_from_root_with_sha_override() { - let root = tempfile::tempdir() - .expect("failed to create temp directory") - .into_path(); - setup_repo_with_commit(&root).expect("failed to setup repo"); - let sha = "1234567890123456789012345678901234567890"; - let bundle_repo = BundleRepo::try_read_from_root( - Some(root.to_str().unwrap().to_string()), - None, - Some(sha.to_string()), - None, - None, - ); - - assert!(bundle_repo.is_ok()); - let bundle_repo = bundle_repo.unwrap(); - assert_eq!(bundle_repo.repo_root, root.to_str().unwrap()); - assert_eq!( - bundle_repo.repo, - Repo { - host: "github.com".to_string(), - owner: "trunk-io".to_string(), - name: "analytics-cli".to_string(), - } - ); - assert_eq!(bundle_repo.repo_url, TEST_ORIGIN); - assert_eq!( - bundle_repo.repo_head_branch, - format!("refs/heads/{}", TEST_BRANCH) - ); - assert_eq!(bundle_repo.repo_head_sha, sha); - assert!(bundle_repo.repo_head_commit_epoch > 0); - assert_eq!(bundle_repo.repo_head_commit_message, "Initial commit"); -} - -#[test] -fn test_try_read_from_root_with_branch_override() { - let root = tempfile::tempdir() - .expect("failed to create temp directory") - .into_path(); - setup_repo_with_commit(&root).expect("failed to setup repo"); - let branch = "other-branch"; - let bundle_repo = BundleRepo::try_read_from_root( - Some(root.to_str().unwrap().to_string()), - None, - None, - Some(branch.to_string()), - None, - ); - - assert!(bundle_repo.is_ok()); - let bundle_repo = bundle_repo.unwrap(); - assert_eq!(bundle_repo.repo_root, root.to_str().unwrap()); - assert_eq!( - bundle_repo.repo, - Repo { - host: "github.com".to_string(), - owner: "trunk-io".to_string(), - name: "analytics-cli".to_string(), - } - ); - assert_eq!(bundle_repo.repo_url, TEST_ORIGIN); - assert_eq!(bundle_repo.repo_head_branch, branch); - assert_eq!(bundle_repo.repo_head_sha.len(), 40); - assert!(bundle_repo.repo_head_commit_epoch > 0); - assert_eq!(bundle_repo.repo_head_commit_message, "Initial commit"); -} - -#[test] -fn test_try_read_from_root_with_time_override() { - let root = tempfile::tempdir() - .expect("failed to create temp directory") - .into_path(); - setup_repo_with_commit(&root).expect("failed to setup repo"); - let epoch = "123"; - let bundle_repo = BundleRepo::try_read_from_root( - Some(root.to_str().unwrap().to_string()), - None, - None, - None, - Some(epoch.to_string()), - ); - - assert!(bundle_repo.is_ok()); - let bundle_repo = bundle_repo.unwrap(); - assert_eq!(bundle_repo.repo_root, root.to_str().unwrap()); - assert_eq!( - bundle_repo.repo, - Repo { - host: "github.com".to_string(), - owner: "trunk-io".to_string(), - name: "analytics-cli".to_string(), - } - ); - assert_eq!(bundle_repo.repo_url, TEST_ORIGIN); - assert_eq!( - bundle_repo.repo_head_branch, - format!("refs/heads/{}", TEST_BRANCH) - ); - assert_eq!(bundle_repo.repo_head_sha.len(), 40); - assert_eq!(bundle_repo.repo_head_commit_epoch, 123); - assert_eq!(bundle_repo.repo_head_commit_message, "Initial commit"); -} diff --git a/cli/tests/upload.rs b/cli/tests/upload.rs index a406b0c2..a9b67469 100644 --- a/cli/tests/upload.rs +++ b/cli/tests/upload.rs @@ -2,19 +2,18 @@ use std::fs; use std::io::BufReader; use std::path::Path; +use api::{CreateBundleUploadRequest, CreateRepoRequest, GetQuarantineBulkTestStatusRequest}; use assert_cmd::Command; use assert_matches::assert_matches; +use context::repo::RepoUrlParts; use junit_mock::JunitMock; use tempfile::tempdir; -use test_utils::mock_git_repo::setup_repo_with_commit; -use test_utils::mock_server::{spawn_mock_server, RequestPayload}; -use trunk_analytics_cli::codeowners::CodeOwners; -use trunk_analytics_cli::types::{ - BundleMeta, CreateBundleUploadRequest, CreateRepoRequest, FileSetType, - GetQuarantineBulkTestStatusRequest, Repo, +use test_utils::{ + mock_git_repo::setup_repo_with_commit, + mock_server::{spawn_mock_server, RequestPayload}, }; - -mod test_utils; +use trunk_analytics_cli::codeowners::CodeOwners; +use trunk_analytics_cli::types::{BundleMeta, FileSetType}; fn generate_mock_git_repo>(directory: T) { setup_repo_with_commit(directory).unwrap(); @@ -71,7 +70,7 @@ async fn upload_bundle() { assert_eq!( requests_iter.next().unwrap(), RequestPayload::GetQuarantineBulkTestStatus(GetQuarantineBulkTestStatusRequest { - repo: Repo { + repo: RepoUrlParts { host: String::from("github.com"), owner: String::from("trunk-io"), name: String::from("analytics-cli"), @@ -83,7 +82,7 @@ async fn upload_bundle() { assert_eq!( requests_iter.next().unwrap(), RequestPayload::CreateBundleUpload(CreateBundleUploadRequest { - repo: Repo { + repo: RepoUrlParts { host: String::from("github.com"), owner: String::from("trunk-io"), name: String::from("analytics-cli"), @@ -102,7 +101,7 @@ async fn upload_bundle() { assert_eq!(bundle_meta.org, "test-org"); assert_eq!( bundle_meta.repo.repo, - Repo { + RepoUrlParts { host: String::from("github.com"), owner: String::from("trunk-io"), name: String::from("analytics-cli"), @@ -159,7 +158,7 @@ async fn upload_bundle() { assert_eq!( requests_iter.next().unwrap(), RequestPayload::CreateRepo(CreateRepoRequest { - repo: Repo { + repo: RepoUrlParts { host: String::from("github.com"), owner: String::from("trunk-io"), name: String::from("analytics-cli"), diff --git a/context/Cargo.toml b/context/Cargo.toml index 88cbbaf3..ff18992c 100644 --- a/context/Cargo.toml +++ b/context/Cargo.toml @@ -6,11 +6,18 @@ edition = "2021" [dev-dependencies] pretty_assertions = "0.6" junit-mock = { path = "../junit-mock" } +test_utils = { path = "../test_utils" } +tempfile = "3.2.0" [dependencies] anyhow = "1.0.44" chrono = "0.4.33" +gix = { version = "0.63.0", default-features = false, features = [] } +openssl = { version = "0.10.66", features = ["vendored"] } quick-junit = "0.5.0" quick-xml = "0.36.2" +regex = { version = "1.10.3", default-features = false, features = ["std"] } +serde = { version = "1.0.130", default-features = false, features = ["derive"] } +serde_json = "1.0.68" speedate = "0.14.4" thiserror = "1.0.63" diff --git a/context/src/lib.rs b/context/src/lib.rs index 78de46a7..fad4210c 100644 --- a/context/src/lib.rs +++ b/context/src/lib.rs @@ -4,6 +4,7 @@ pub mod env; pub mod junit; +pub mod repo; fn safe_truncate_string<'a, const MAX_LEN: usize, T: AsRef>(value: &'a T) -> &'a str { safe_truncate_str::(value.as_ref()) diff --git a/context/src/repo/mod.rs b/context/src/repo/mod.rs new file mode 100644 index 00000000..6e15e796 --- /dev/null +++ b/context/src/repo/mod.rs @@ -0,0 +1,198 @@ +use std::path::PathBuf; + +use anyhow::Context; +use gix::Repository; +use regex::Regex; +use serde::{Deserialize, Serialize}; + +pub const GIT_REMOTE_ORIGIN_URL_CONFIG: &str = "remote.origin.url"; + +#[derive(Debug, Clone, Default)] +struct BundleRepoOptions { + repo_root: Option, + repo_url: Option, + repo_head_sha: Option, + repo_head_branch: Option, + repo_head_commit_epoch: Option, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct BundleRepo { + pub repo: RepoUrlParts, + pub repo_root: String, + pub repo_url: String, + pub repo_head_sha: String, + pub repo_head_branch: String, + pub repo_head_commit_epoch: i64, + pub repo_head_commit_message: String, + pub repo_head_author_name: String, + pub repo_head_author_email: String, +} + +impl BundleRepo { + pub fn new( + repo_root: Option, + repo_url: Option, + repo_head_sha: Option, + repo_head_branch: Option, + repo_head_commit_epoch: Option, + ) -> anyhow::Result { + let mut bundle_repo_options = BundleRepoOptions { + repo_root: repo_root + .as_ref() + .map(|repo_root| PathBuf::from(repo_root)) + .or_else(|| std::env::current_dir().ok()), + repo_url, + repo_head_sha, + repo_head_branch, + repo_head_commit_epoch: repo_head_commit_epoch.and_then(|s| s.parse().ok()), + }; + + let mut head_commit_message = None; + let mut head_commit_author = None; + + // If repo root found, try to get repo details from git. + if let Some(git_repo) = bundle_repo_options + .repo_root + .as_ref() + .and_then(|dir| gix::open(dir).ok()) + { + bundle_repo_options.repo_url = bundle_repo_options.repo_url.or_else(|| { + git_repo + .config_snapshot() + .string_by_key(GIT_REMOTE_ORIGIN_URL_CONFIG) + .map(|s| s.to_string()) + }); + + if let Ok(mut git_head) = git_repo.head() { + bundle_repo_options.repo_head_branch = bundle_repo_options + .repo_head_branch + .or_else(|| git_head.referent_name().map(|s| s.as_bstr().to_string())) + .or_else(|| { + Self::git_head_branch_from_remote_branches(&git_repo) + .ok() + .flatten() + }); + + bundle_repo_options.repo_head_sha = bundle_repo_options + .repo_head_sha + .or_else(|| git_head.id().map(|id| id.to_string())); + + if let Ok(commit) = git_head.peel_to_commit_in_place() { + bundle_repo_options.repo_head_commit_epoch = bundle_repo_options + .repo_head_commit_epoch + .or_else(|| commit.time().ok().map(|time| time.seconds)); + head_commit_message = commit.message().map(|msg| msg.title.to_string()).ok(); + head_commit_author = commit.author().ok().map(|signature| signature.to_owned()); + } + } + } + + // Require URL which should be known at this point. + let repo_url = bundle_repo_options + .repo_url + .context("failed to get repo URL")?; + let repo_url_parts = + RepoUrlParts::from_url(&repo_url).context("failed to parse repo URL")?; + let (repo_head_author_name, repo_head_author_email) = head_commit_author + .as_ref() + .map(|a| (a.name.to_string(), a.email.to_string())) + .unwrap_or_default(); + Ok(BundleRepo { + repo: repo_url_parts, + repo_root: bundle_repo_options + .repo_root + .and_then(|p| p.to_str().map(String::from)) + .unwrap_or_default(), + repo_url, + repo_head_branch: bundle_repo_options.repo_head_branch.unwrap_or_default(), + repo_head_sha: bundle_repo_options.repo_head_sha.unwrap_or_default(), + repo_head_commit_epoch: bundle_repo_options + .repo_head_commit_epoch + .unwrap_or_default(), + repo_head_commit_message: head_commit_message.unwrap_or_default(), + repo_head_author_name, + repo_head_author_email, + }) + } + + fn git_head_branch_from_remote_branches( + git_repo: &Repository, + ) -> anyhow::Result> { + for remote_branch in git_repo + .references()? + .remote_branches()? + .filter_map(Result::ok) + { + if let Some(target_id) = remote_branch.target().try_id() { + if target_id.as_bytes() == remote_branch.id().as_bytes() { + return Ok(remote_branch.name().to_path().to_str().map(String::from)); + } + } + } + Ok(None) + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] +pub struct RepoUrlParts { + pub host: String, + pub owner: String, + pub name: String, +} + +impl RepoUrlParts { + pub fn from_url(url: &str) -> anyhow::Result { + let re1 = Regex::new(r"^(ssh|git|http|https|ftp|ftps)://([^/]*?@)?([^/]*)/(.+)/([^/]+)")?; + let re2 = Regex::new(r"^([^/]*?@)([^/]*):(.+)/([^/]+)")?; + + let parts = if re1.is_match(url) { + let caps = re1.captures(url).expect("failed to parse url"); + if caps.len() != 6 { + return Err(anyhow::anyhow!( + "Invalid repo url format. Expected 6 parts: {:?} (url: {})", + caps, + url + )); + } + let domain = caps.get(3).map(|m| m.as_str()).unwrap_or(""); + let owner = caps.get(4).map(|m| m.as_str()).unwrap_or(""); + let name = caps.get(5).map(|m| m.as_str()).unwrap_or(""); + (domain, owner, name) + } else if re2.is_match(url) { + let caps = re2.captures(url).expect("failed to parse url"); + if caps.len() != 5 { + return Err(anyhow::anyhow!( + "Invalid repo url format. Expected 6 parts: {:?} (url: {})", + caps, + url + )); + } + let domain = caps.get(2).map(|m| m.as_str()).unwrap_or(""); + let owner = caps.get(3).map(|m| m.as_str()).unwrap_or(""); + let name = caps.get(4).map(|m| m.as_str()).unwrap_or(""); + (domain, owner, name) + } else { + return Err(anyhow::anyhow!("Invalid repo url format: {}", url)); + }; + + let host = parts.0.trim().to_string(); + let owner = parts.1.trim().to_string(); + let name = parts + .2 + .trim() + .trim_end_matches('/') + .trim_end_matches(".git") + .to_string(); + + if host.is_empty() || owner.is_empty() || name.is_empty() { + return Err(anyhow::anyhow!( + "Invalid repo url format. Expected non-empty parts: {:?} (url: {})", + parts, + url + )); + } + + Ok(Self { host, owner, name }) + } +} diff --git a/context/tests/repo.rs b/context/tests/repo.rs new file mode 100644 index 00000000..5ca3ef29 --- /dev/null +++ b/context/tests/repo.rs @@ -0,0 +1,364 @@ +use context::repo::{BundleRepo, RepoUrlParts}; +use test_utils::mock_git_repo::{setup_repo_with_commit, TEST_BRANCH, TEST_ORIGIN}; + +#[test] +fn test_try_read_from_root() { + let root = tempfile::tempdir() + .expect("failed to create temp directory") + .into_path(); + setup_repo_with_commit(&root).expect("failed to setup repo"); + let bundle_repo = BundleRepo::new( + Some(root.to_str().unwrap().to_string()), + None, + None, + None, + None, + ); + + assert!(bundle_repo.is_ok()); + let bundle_repo = bundle_repo.unwrap(); + assert_eq!(bundle_repo.repo_root, root.to_str().unwrap()); + assert_eq!( + bundle_repo.repo, + RepoUrlParts { + host: "github.com".to_string(), + owner: "trunk-io".to_string(), + name: "analytics-cli".to_string(), + } + ); + assert_eq!(bundle_repo.repo_url, TEST_ORIGIN); + assert_eq!( + bundle_repo.repo_head_branch, + format!("refs/heads/{}", TEST_BRANCH) + ); + assert_eq!(bundle_repo.repo_head_sha.len(), 40); + assert!(bundle_repo.repo_head_commit_epoch > 0); + assert_eq!(bundle_repo.repo_head_commit_message, "Initial commit"); +} + +#[test] +fn test_try_read_from_root_with_url_override() { + let root = tempfile::tempdir() + .expect("failed to create temp directory") + .into_path(); + setup_repo_with_commit(&root).expect("failed to setup repo"); + let origin_url = "https://host.com/owner/repo.git"; + let bundle_repo = BundleRepo::new( + Some(root.to_str().unwrap().to_string()), + Some(origin_url.to_string()), + None, + None, + None, + ); + + assert!(bundle_repo.is_ok()); + let bundle_repo = bundle_repo.unwrap(); + assert_eq!(bundle_repo.repo_root, root.to_str().unwrap()); + assert_eq!( + bundle_repo.repo, + RepoUrlParts { + host: "host.com".to_string(), + owner: "owner".to_string(), + name: "repo".to_string(), + } + ); + assert_eq!(bundle_repo.repo_url, origin_url); + assert_eq!( + bundle_repo.repo_head_branch, + format!("refs/heads/{}", TEST_BRANCH) + ); + assert_eq!(bundle_repo.repo_head_sha.len(), 40); + assert!(bundle_repo.repo_head_commit_epoch > 0); + assert_eq!(bundle_repo.repo_head_commit_message, "Initial commit"); +} + +#[test] +fn test_try_read_from_root_with_sha_override() { + let root = tempfile::tempdir() + .expect("failed to create temp directory") + .into_path(); + setup_repo_with_commit(&root).expect("failed to setup repo"); + let sha = "1234567890123456789012345678901234567890"; + let bundle_repo = BundleRepo::new( + Some(root.to_str().unwrap().to_string()), + None, + Some(sha.to_string()), + None, + None, + ); + + assert!(bundle_repo.is_ok()); + let bundle_repo = bundle_repo.unwrap(); + assert_eq!(bundle_repo.repo_root, root.to_str().unwrap()); + assert_eq!( + bundle_repo.repo, + RepoUrlParts { + host: "github.com".to_string(), + owner: "trunk-io".to_string(), + name: "analytics-cli".to_string(), + } + ); + assert_eq!(bundle_repo.repo_url, TEST_ORIGIN); + assert_eq!( + bundle_repo.repo_head_branch, + format!("refs/heads/{}", TEST_BRANCH) + ); + assert_eq!(bundle_repo.repo_head_sha, sha); + assert!(bundle_repo.repo_head_commit_epoch > 0); + assert_eq!(bundle_repo.repo_head_commit_message, "Initial commit"); +} + +#[test] +fn test_try_read_from_root_with_branch_override() { + let root = tempfile::tempdir() + .expect("failed to create temp directory") + .into_path(); + setup_repo_with_commit(&root).expect("failed to setup repo"); + let branch = "other-branch"; + let bundle_repo = BundleRepo::new( + Some(root.to_str().unwrap().to_string()), + None, + None, + Some(branch.to_string()), + None, + ); + + assert!(bundle_repo.is_ok()); + let bundle_repo = bundle_repo.unwrap(); + assert_eq!(bundle_repo.repo_root, root.to_str().unwrap()); + assert_eq!( + bundle_repo.repo, + RepoUrlParts { + host: "github.com".to_string(), + owner: "trunk-io".to_string(), + name: "analytics-cli".to_string(), + } + ); + assert_eq!(bundle_repo.repo_url, TEST_ORIGIN); + assert_eq!(bundle_repo.repo_head_branch, branch); + assert_eq!(bundle_repo.repo_head_sha.len(), 40); + assert!(bundle_repo.repo_head_commit_epoch > 0); + assert_eq!(bundle_repo.repo_head_commit_message, "Initial commit"); +} + +#[test] +fn test_try_read_from_root_with_time_override() { + let root = tempfile::tempdir() + .expect("failed to create temp directory") + .into_path(); + setup_repo_with_commit(&root).expect("failed to setup repo"); + let epoch = "123"; + let bundle_repo = BundleRepo::new( + Some(root.to_str().unwrap().to_string()), + None, + None, + None, + Some(epoch.to_string()), + ); + + assert!(bundle_repo.is_ok()); + let bundle_repo = bundle_repo.unwrap(); + assert_eq!(bundle_repo.repo_root, root.to_str().unwrap()); + assert_eq!( + bundle_repo.repo, + RepoUrlParts { + host: "github.com".to_string(), + owner: "trunk-io".to_string(), + name: "analytics-cli".to_string(), + } + ); + assert_eq!(bundle_repo.repo_url, TEST_ORIGIN); + assert_eq!( + bundle_repo.repo_head_branch, + format!("refs/heads/{}", TEST_BRANCH) + ); + assert_eq!(bundle_repo.repo_head_sha.len(), 40); + assert_eq!(bundle_repo.repo_head_commit_epoch, 123); + assert_eq!(bundle_repo.repo_head_commit_message, "Initial commit"); +} + +#[test] +fn test_parse_ssh_urls() { + let good_urls = &[ + ( + "git@github.com:user/repository.git", + RepoUrlParts { + host: "github.com".to_string(), + owner: "user".to_string(), + name: "repository".to_string(), + }, + ), + ( + "git@gitlab.com:group/project.git", + RepoUrlParts { + host: "gitlab.com".to_string(), + owner: "group".to_string(), + name: "project".to_string(), + }, + ), + ( + "git@bitbucket.org:team/repo.git", + RepoUrlParts { + host: "bitbucket.org".to_string(), + owner: "team".to_string(), + name: "repo".to_string(), + }, + ), + ( + "git@ssh.dev.azure.com:company/project", + RepoUrlParts { + host: "ssh.dev.azure.com".to_string(), + owner: "company".to_string(), + name: "project".to_string(), + }, + ), + ( + "git@sourceforge.net:owner/repo.git", + RepoUrlParts { + host: "sourceforge.net".to_string(), + owner: "owner".to_string(), + name: "repo".to_string(), + }, + ), + ]; + + for (url, expected) in good_urls { + let actual = RepoUrlParts::from_url(url).unwrap(); + assert_eq!(actual, *expected); + } +} + +#[test] +fn test_parse_https_urls() { + let good_urls = &[ + ( + "https://github.com/username/repository.git", + RepoUrlParts { + host: "github.com".to_string(), + owner: "username".to_string(), + name: "repository".to_string(), + }, + ), + ( + "https://gitlab.com/group/project.git", + RepoUrlParts { + host: "gitlab.com".to_string(), + owner: "group".to_string(), + name: "project".to_string(), + }, + ), + ( + "https://bitbucket.org/teamname/reponame.git", + RepoUrlParts { + host: "bitbucket.org".to_string(), + owner: "teamname".to_string(), + name: "reponame".to_string(), + }, + ), + ( + "https://dev.azure.com/organization/project", + RepoUrlParts { + host: "dev.azure.com".to_string(), + owner: "organization".to_string(), + name: "project".to_string(), + }, + ), + ( + "https://gitlab.example.edu/groupname/project.git", + RepoUrlParts { + host: "gitlab.example.edu".to_string(), + owner: "groupname".to_string(), + name: "project".to_string(), + }, + ), + ]; + + for (url, expected) in good_urls { + let actual = RepoUrlParts::from_url(url).unwrap(); + assert_eq!(actual, *expected); + } +} + +#[test] +fn test_parse_git_urls() { + let good_urls = &[ + ( + "ssh://github.com/github/testrepo", + RepoUrlParts { + host: "github.com".to_string(), + owner: "github".to_string(), + name: "testrepo".to_string(), + }, + ), + ( + "git://github.com/github/testrepo", + RepoUrlParts { + host: "github.com".to_string(), + owner: "github".to_string(), + name: "testrepo".to_string(), + }, + ), + ( + "http://github.com/github/testrepo", + RepoUrlParts { + host: "github.com".to_string(), + owner: "github".to_string(), + name: "testrepo".to_string(), + }, + ), + ( + "https://github.com/github/testrepo", + RepoUrlParts { + host: "github.com".to_string(), + owner: "github".to_string(), + name: "testrepo".to_string(), + }, + ), + ( + "ftp://github.com/github/testrepo", + RepoUrlParts { + host: "github.com".to_string(), + owner: "github".to_string(), + name: "testrepo".to_string(), + }, + ), + ( + "ftps://github.com/github/testrepo", + RepoUrlParts { + host: "github.com".to_string(), + owner: "github".to_string(), + name: "testrepo".to_string(), + }, + ), + ( + "user@github.com:github/testrepo", + RepoUrlParts { + host: "github.com".to_string(), + owner: "github".to_string(), + name: "testrepo".to_string(), + }, + ), + ]; + + let bad_urls = &[ + "sshy://github.com/github/testrepo", + "ssh://github.com//testrepo", + "ssh:/github.com//testrepo", + "ssh:///testrepo", + "ssh://github.com/github/", + ]; + + for (url, expected) in good_urls { + let actual1 = RepoUrlParts::from_url(url).unwrap(); + assert_eq!(actual1, *expected); + let actual2 = RepoUrlParts::from_url(&(url.to_string() + ".git")).unwrap(); + assert_eq!(actual2, *expected); + let actual3 = RepoUrlParts::from_url(&(url.to_string() + ".git/")).unwrap(); + assert_eq!(actual3, *expected); + } + + for url in bad_urls { + let actual = RepoUrlParts::from_url(url); + assert!(actual.is_err()); + } +} diff --git a/test_utils/Cargo.toml b/test_utils/Cargo.toml new file mode 100644 index 00000000..1d3787fa --- /dev/null +++ b/test_utils/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "test_utils" +version = "0.1.0" +edition = "2021" + +[dependencies] +api = { path = "../api" } +anyhow = "1.0.44" +axum = { version = "0.7.5", features = ["macros"] } +git2 = "0.19.0" +junit-mock = { path = "../junit-mock" } +tar = { version = "0.4.30", default-features = false } +tempfile = "3.2.0" +tokio = { version = "*", default-features = false, features = [ + "rt-multi-thread", + "macros", +] } +zstd = { version = "0.13.0", default-features = false } diff --git a/cli/tests/test_utils/mod.rs b/test_utils/src/lib.rs similarity index 100% rename from cli/tests/test_utils/mod.rs rename to test_utils/src/lib.rs diff --git a/cli/tests/test_utils/mock_git_repo.rs b/test_utils/src/mock_git_repo.rs similarity index 100% rename from cli/tests/test_utils/mock_git_repo.rs rename to test_utils/src/mock_git_repo.rs diff --git a/cli/tests/test_utils/mock_server.rs b/test_utils/src/mock_server.rs similarity index 84% rename from cli/tests/test_utils/mock_server.rs rename to test_utils/src/mock_server.rs index 015de7e5..56c1e5e9 100644 --- a/cli/tests/test_utils/mock_server.rs +++ b/test_utils/src/mock_server.rs @@ -1,22 +1,25 @@ -use std::collections::HashSet; -use std::fs; -use std::io::Read; -use std::path::PathBuf; -use std::sync::{Arc, Mutex}; +use std::{ + collections::HashSet, + fs, + io::Read, + path::PathBuf, + sync::{Arc, Mutex}, +}; -use axum::body::Bytes; -use axum::extract::State; -use axum::http::StatusCode; -use axum::response::Response; -use axum::routing::{any, post, put}; -use axum::{Json, Router}; -use tempfile::tempdir; -use tokio::net::TcpListener; -use tokio::spawn; -use trunk_analytics_cli::types::{ +use api::{ CreateBundleUploadRequest, CreateBundleUploadResponse, CreateRepoRequest, GetQuarantineBulkTestStatusRequest, QuarantineConfig, }; +use axum::{ + body::Bytes, + extract::State, + http::StatusCode, + response::Response, + routing::{any, post, put}, + {Json, Router}, +}; +use tempfile::tempdir; +use tokio::{net::TcpListener, spawn}; #[derive(Debug, Clone, PartialEq, Eq)] pub enum RequestPayload { @@ -37,7 +40,6 @@ pub type SharedMockServerState = Arc; /// Mock server spawned in a new thread. /// /// NOTE: must use a multithreaded executor to have the server run while running tests -#[allow(dead_code)] // TODO: move this to its own crate to get rid of the need for this pub async fn spawn_mock_server() -> SharedMockServerState { let listener = TcpListener::bind("localhost:0").await.unwrap(); let random_port = listener.local_addr().unwrap().port(); @@ -81,7 +83,6 @@ pub async fn spawn_mock_server() -> SharedMockServerState { state } -#[allow(dead_code)] // TODO: move this to its own crate to get rid of the need for this #[axum::debug_handler] async fn repo_create_handler( State(state): State, @@ -95,7 +96,6 @@ async fn repo_create_handler( Response::new(String::from("OK")) } -#[allow(dead_code)] // TODO: move this to its own crate to get rid of the need for this #[axum::debug_handler] async fn create_bundle_handler( State(state): State, @@ -116,7 +116,6 @@ async fn create_bundle_handler( }) } -#[allow(dead_code)] // TODO: move this to its own crate to get rid of the need for this #[axum::debug_handler] async fn get_quarantining_config_handler( State(state): State, @@ -135,7 +134,6 @@ async fn get_quarantining_config_handler( }) } -#[allow(dead_code)] // TODO: move this to its own crate to get rid of the need for this #[axum::debug_handler] async fn s3_upload_handler( State(state): State,