use semver::Version;
use tokio::time::{Duration, sleep};
use url::Url;
use crate::{
config::Config,
forge::{
config::{LEGACY_PENDING_LABEL, PENDING_LABEL, RepoUrl, Scheme},
manager::ForgeManager,
request::{
CreateCommitRequest, CreatePrRequest, CreateReleaseBranchRequest,
FileChange, FileUpdateType, GetFileContentRequest, GetPrRequest,
PrLabelsRequest,
},
tests::common::traits::ForgeTestHelper,
},
result::ReleasaurusError,
};
pub fn parse_repo_url(raw: &str) -> RepoUrl {
let parsed = Url::parse(raw).expect("invalid test URL");
let scheme = if parsed.scheme() == "http" {
Scheme::Http
} else {
Scheme::Https
};
let host = parsed.host_str().unwrap_or("").to_string();
let port = parsed.port();
let segments: Vec<&str> = parsed
.path_segments()
.map(|s| s.collect())
.unwrap_or_default();
let owner = segments.first().copied().unwrap_or("").to_string();
let name = segments.get(1).copied().unwrap_or("").to_string();
let path = format!("/{}/{}", owner, name);
RepoUrl {
scheme,
host,
owner,
name,
path,
port,
token: None,
}
}
const SHORT_WAIT: Duration = Duration::from_secs(2);
const LONG_WAIT: Duration = Duration::from_secs(20);
pub async fn run_forge_test(
forge: &ForgeManager,
helper: &dyn ForgeTestHelper,
) {
log::info!("resetting repository");
helper.reset().await.unwrap();
let default_branch = forge.default_branch();
let release_branch = "release-branch";
let test_file_path = "test.txt";
let test_file_content = "test content";
log::info!("looking for non-existent file content");
let get_file_req = GetFileContentRequest {
branch: Some(default_branch.to_string()),
path: test_file_path.to_string(),
};
let file_content = forge.get_file_content(get_file_req).await.unwrap();
assert!(file_content.is_none());
log::info!("creating commit with new file content");
let create_commit_req = CreateCommitRequest {
target_branch: default_branch.to_string(),
message: "fix: test fix commit".into(),
file_changes: vec![FileChange {
content: test_file_content.to_string(),
path: test_file_path.to_string(),
update_type: FileUpdateType::Replace,
}],
};
let created_commit = forge.create_commit(create_commit_req).await.unwrap();
assert!(!created_commit.sha.is_empty());
sleep(SHORT_WAIT).await;
let pre_tag_branch = "pre-tag-branch";
log::info!("creating pre-tag-branch before release merge");
let pre_tag_branch_req = CreateReleaseBranchRequest {
base_branch: default_branch.to_string(),
message: "chore: pre-tag branch".into(),
release_branch: pre_tag_branch.to_string(),
file_changes: vec![FileChange {
content: "pre-tag".to_string(),
path: "pre-tag.txt".into(),
update_type: FileUpdateType::Replace,
}],
};
forge
.create_release_branch(pre_tag_branch_req)
.await
.unwrap();
sleep(SHORT_WAIT).await;
let commits = forge.get_commits(None, None).await.unwrap();
assert_eq!(commits.len(), 2);
log::info!("looking for existing file content");
let get_file_req = GetFileContentRequest {
branch: Some(default_branch.to_string()),
path: test_file_path.to_string(),
};
let file_content = forge.get_file_content(get_file_req).await.unwrap();
assert!(file_content.is_some());
let file_content = file_content.unwrap();
assert_eq!(file_content, test_file_content);
log::info!("creating release-branch");
let create_release_branch_req = CreateReleaseBranchRequest {
base_branch: default_branch.to_string(),
message: "chore(main): release".into(),
release_branch: release_branch.to_string(),
file_changes: vec![FileChange {
content: format!("# Changelog - {}", created_commit.sha),
path: "CHANGELOG.md".into(),
update_type: FileUpdateType::Prepend,
}],
};
let release_commit = forge
.create_release_branch(create_release_branch_req)
.await
.unwrap();
assert!(!release_commit.sha.is_empty());
sleep(SHORT_WAIT).await;
log::info!("re-creating release-branch (force-reset scenario)");
let re_create_req = CreateReleaseBranchRequest {
base_branch: default_branch.to_string(),
message: "chore(main): release (re-run)".into(),
release_branch: release_branch.to_string(),
file_changes: vec![FileChange {
content: format!("# Changelog - {} (updated)", created_commit.sha),
path: "CHANGELOG.md".into(),
update_type: FileUpdateType::Prepend,
}],
};
let re_create_commit =
forge.create_release_branch(re_create_req).await.unwrap();
assert!(!re_create_commit.sha.is_empty());
sleep(SHORT_WAIT).await;
log::info!("getting non-existent open PR");
let get_pr_req = GetPrRequest {
base_branch: default_branch.to_string(),
head_branch: release_branch.to_string(),
};
let open_pr = forge.get_open_release_pr(get_pr_req).await.unwrap();
assert!(open_pr.is_none());
log::info!("creating PR");
let create_pr_req = CreatePrRequest {
base_branch: default_branch.to_string(),
body: "Test PR".into(),
head_branch: release_branch.to_string(),
title: "Test PR".into(),
};
let release_pr = forge.create_pr(create_pr_req).await.unwrap();
assert_ne!(release_pr.number, 0);
assert!(!release_pr.body.is_empty());
assert!(!release_pr.sha.is_empty());
sleep(SHORT_WAIT).await;
log::info!(
"replacing PR labels with legacy pending label: {}",
LEGACY_PENDING_LABEL
);
let replace_labels_req = PrLabelsRequest {
labels: vec![LEGACY_PENDING_LABEL.into()],
pr_number: release_pr.number,
};
forge.replace_pr_labels(replace_labels_req).await.unwrap();
sleep(LONG_WAIT).await;
log::info!("looking for newly created open PR with legacy label");
let get_pr_req = GetPrRequest {
base_branch: default_branch.to_string(),
head_branch: release_branch.to_string(),
};
let open_pr = forge.get_open_release_pr(get_pr_req).await.unwrap();
assert!(open_pr.is_some());
let open_pr = open_pr.unwrap();
assert_ne!(open_pr.number, 0);
assert!(!open_pr.body.is_empty());
assert!(!open_pr.sha.is_empty());
log::info!(
"replacing PR labels with new scoped pending label: {}",
PENDING_LABEL
);
let replace_labels_req = PrLabelsRequest {
labels: vec![PENDING_LABEL.into()],
pr_number: release_pr.number,
};
forge.replace_pr_labels(replace_labels_req).await.unwrap();
sleep(LONG_WAIT).await;
log::info!("looking for newly created open PR with new scoped label");
let get_pr_req = GetPrRequest {
base_branch: default_branch.to_string(),
head_branch: release_branch.to_string(),
};
let open_pr = forge.get_open_release_pr(get_pr_req).await.unwrap();
assert!(open_pr.is_some());
let open_pr = open_pr.unwrap();
assert_ne!(open_pr.number, 0);
assert!(!open_pr.body.is_empty());
assert!(!open_pr.sha.is_empty());
log::info!("looking for non-existent merged PR");
let get_pr_req = GetPrRequest {
base_branch: default_branch.to_string(),
head_branch: release_branch.to_string(),
};
let merged_pr = forge.get_merged_release_pr(get_pr_req).await.unwrap();
assert!(merged_pr.is_none());
log::info!("merging release PR via helper");
helper.merge_pr(release_pr.number).await.unwrap();
sleep(LONG_WAIT).await;
log::info!("looking for newly merged release PR");
let get_pr_req = GetPrRequest {
base_branch: default_branch.to_string(),
head_branch: release_branch.to_string(),
};
let merged_pr = forge.get_merged_release_pr(get_pr_req).await.unwrap();
assert!(merged_pr.is_some());
let merged_pr = merged_pr.unwrap();
assert_ne!(merged_pr.number, 0);
assert!(!merged_pr.sha.is_empty());
assert!(!merged_pr.body.is_empty());
log::info!("looking for non-existent tag by prefix");
let semver = "1.1.0";
let tag = format!("v{}", semver);
let current_tag = forge
.get_latest_tag_for_prefix("v", default_branch)
.await
.unwrap();
assert!(current_tag.is_none());
log::info!("tagging commit");
forge.tag_commit(&tag, &merged_pr.sha).await.unwrap();
sleep(SHORT_WAIT).await;
log::info!("looking for newly tagged commit by prefix");
let current_tag = forge
.get_latest_tag_for_prefix("v", default_branch)
.await
.unwrap();
assert!(current_tag.is_some());
let current_tag = current_tag.unwrap();
assert_eq!(current_tag.name, tag);
assert_eq!(current_tag.semver, Version::parse(semver).unwrap());
assert_eq!(current_tag.sha, merged_pr.sha);
assert!(
current_tag.timestamp.is_some(),
"tag must carry a timestamp so multi-package repos filter commits correctly"
);
log::info!("verifying tag is not visible on branch forked before release");
let pre_tag_result = forge
.get_latest_tag_for_prefix("v", pre_tag_branch)
.await
.unwrap();
assert!(
pre_tag_result.is_none(),
"tag must not appear on a branch that diverged before it \
was created"
);
if helper.supports_native_releases() {
log::info!("getting non-existent release by tag name");
let err = forge
.get_release_by_tag(¤t_tag.name)
.await
.unwrap_err();
assert!(matches!(err, ReleasaurusError::ForgeError(_)));
}
log::info!("creating release for tag");
forge
.create_release(¤t_tag.name, ¤t_tag.sha, "release notes")
.await
.unwrap();
sleep(SHORT_WAIT).await;
if helper.supports_native_releases() {
log::info!("getting newly created release by tag name");
let release =
forge.get_release_by_tag(¤t_tag.name).await.unwrap();
assert_eq!(release.tag, current_tag.name);
}
log::info!("loading non-existent config file");
let config = forge.load_config(None).await.unwrap();
assert_eq!(
config.packages[0].workspace_root,
Config::default().packages[0].workspace_root
);
assert_eq!(config.packages[0].path, Config::default().packages[0].path);
log::info!("creating commit to add releasaurus config file");
let releasaurus_toml_content = r#"
[[package]]
name = "test-package"
workspace_root = "packages"
path = "test-package"
"#;
let create_commit_req = CreateCommitRequest {
target_branch: default_branch.to_string(),
message: "chore: adds releasaurus.toml".into(),
file_changes: vec![FileChange {
content: releasaurus_toml_content.to_string(),
path: "releasaurus.toml".to_string(),
update_type: FileUpdateType::Replace,
}],
};
log::info!("loading newly created releasaurus config file");
let created_commit = forge.create_commit(create_commit_req).await.unwrap();
assert!(!created_commit.sha.is_empty());
sleep(SHORT_WAIT).await;
let config = forge
.load_config(Some(default_branch.to_string()))
.await
.unwrap();
assert_eq!(config.packages[0].name, "test-package");
assert_eq!(config.packages[0].workspace_root, "packages");
assert_eq!(config.packages[0].path, "test-package");
log::info!("getting commits since release tag");
let commits_since_tag = forge
.get_commits(
Some(default_branch.to_string()),
Some(current_tag.sha.clone()),
)
.await
.unwrap();
assert_eq!(
commits_since_tag.len(),
1,
"expected exactly one commit after the release tag"
);
}