From 3c70b9435a649a53614bd3ae6f434c2a455d575a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Mon, 12 Aug 2024 19:32:53 +0100 Subject: [PATCH] feat: `deno upgrade --rc` (#24905) This commit adds the "--rc" flag to "deno upgrade" subcommand. This flag allows to upgrade to the latest "release candidate" release. The update checker was also updated to take this into account. --- Cargo.lock | 2 +- cli/Cargo.toml | 2 +- cli/args/flags.rs | 38 +++ cli/tools/upgrade.rs | 584 ++++++++++++++++++++++++++++++++++--------- 4 files changed, 508 insertions(+), 118 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 184af900dc..79e9ddc2c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1127,7 +1127,7 @@ dependencies = [ [[package]] name = "deno" -version = "1.45.5" +version = "1.46.0-rc.0" dependencies = [ "async-trait", "base32", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 8bf53dba4f..fcf4976928 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno" -version = "1.45.5" +version = "1.46.0-rc.0" authors.workspace = true default-run = "deno" edition.workspace = true diff --git a/cli/args/flags.rs b/cli/args/flags.rs index 0c4b75f890..c6bb904309 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -398,6 +398,7 @@ pub struct TestFlags { pub struct UpgradeFlags { pub dry_run: bool, pub force: bool, + pub release_candidate: bool, pub canary: bool, pub version: Option, pub output: Option, @@ -2908,6 +2909,13 @@ update to a different location, use the --output flag: .help("Upgrade to canary builds") .action(ArgAction::SetTrue), ) + .arg( + Arg::new("release-candidate") + .long("rc") + .help("Upgrade to a release candidate") + .conflicts_with_all(["canary", "version"]) + .action(ArgAction::SetTrue), + ) .arg(ca_file_arg()) }) } @@ -4568,11 +4576,13 @@ fn upgrade_parse(flags: &mut Flags, matches: &mut ArgMatches) { let dry_run = matches.get_flag("dry-run"); let force = matches.get_flag("force"); let canary = matches.get_flag("canary"); + let release_candidate = matches.get_flag("release-candidate"); let version = matches.remove_one::("version"); let output = matches.remove_one::("output"); flags.subcommand = DenoSubcommand::Upgrade(UpgradeFlags { dry_run, force, + release_candidate, canary, version, output, @@ -5057,6 +5067,7 @@ mod tests { force: true, dry_run: true, canary: false, + release_candidate: false, version: None, output: None, }), @@ -5075,6 +5086,7 @@ mod tests { force: false, dry_run: false, canary: false, + release_candidate: false, version: None, output: Some(String::from("example.txt")), }), @@ -9039,6 +9051,7 @@ mod tests { force: false, dry_run: false, canary: false, + release_candidate: false, version: None, output: None, }), @@ -9048,6 +9061,31 @@ mod tests { ); } + #[test] + fn upgrade_release_candidate() { + let r = flags_from_vec(svec!["deno", "upgrade", "--rc"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Upgrade(UpgradeFlags { + force: false, + dry_run: false, + canary: false, + release_candidate: true, + version: None, + output: None, + }), + ..Flags::default() + } + ); + + let r = flags_from_vec(svec!["deno", "upgrade", "--rc", "--canary"]); + assert!(r.is_err()); + + let r = flags_from_vec(svec!["deno", "upgrade", "--rc", "--version"]); + assert!(r.is_err()); + } + #[test] fn cache_with_cafile() { let r = flags_from_vec(svec![ diff --git a/cli/tools/upgrade.rs b/cli/tools/upgrade.rs index fe5c565f03..8821e92a77 100644 --- a/cli/tools/upgrade.rs +++ b/cli/tools/upgrade.rs @@ -93,12 +93,14 @@ trait VersionProvider: Clone { async fn latest_version( &self, release_channel: ReleaseChannel, - ) -> Result; + ) -> Result; - // TODO(bartlomieju): what this one actually returns? + /// Returns either a semver or git hash. It's up to implementor to + /// decide which one is appropriate, but in general only "stable" + /// and "lts" versions use semver. fn current_version(&self) -> Cow; - // TODO(bartlomieju): update to handle `Lts` and `Rc` channels + // TODO(bartlomieju): update to handle `Lts` channel async fn get_current_exe_release_channel( &self, ) -> Result; @@ -127,7 +129,7 @@ impl VersionProvider for RealVersionProvider { async fn latest_version( &self, release_channel: ReleaseChannel, - ) -> Result { + ) -> Result { fetch_latest_version( &self.http_client_provider.get_or_create()?, release_channel, @@ -140,12 +142,26 @@ impl VersionProvider for RealVersionProvider { Cow::Borrowed(version::release_version_or_canary_commit_hash()) } - // TODO(bartlomieju): update to handle `Lts` and `Rc` channels + // TODO(bartlomieju): update to handle `Lts` channel async fn get_current_exe_release_channel( &self, ) -> Result { if version::is_canary() { - Ok(ReleaseChannel::Canary) + let rc_versions = get_rc_versions( + &self.http_client_provider.get_or_create()?, + self.check_kind, + ) + .await?; + + let is_current_exe_an_rc = rc_versions + .iter() + .any(|(hash, _)| hash == version::GIT_COMMIT_HASH); + + if is_current_exe_an_rc { + Ok(ReleaseChannel::Rc) + } else { + Ok(ReleaseChannel::Canary) + } } else { Ok(ReleaseChannel::Stable) } @@ -176,20 +192,19 @@ impl< } pub fn should_check_for_new_version(&self) -> bool { - match &self.maybe_file { - Some(file) => { - let last_check_age = self - .env - .current_time() - .signed_duration_since(file.last_checked); - last_check_age > chrono::Duration::hours(UPGRADE_CHECK_INTERVAL) - } - None => true, - } + let Some(file) = &self.maybe_file else { + return true; + }; + + let last_check_age = self + .env + .current_time() + .signed_duration_since(file.last_checked); + last_check_age > chrono::Duration::hours(UPGRADE_CHECK_INTERVAL) } - /// Returns the version if a new one is available and it should be prompted about. - pub fn should_prompt(&self) -> Option { + /// Returns the current exe release channel and a version if a new one is available and it should be prompted about. + pub fn should_prompt(&self) -> Option<(ReleaseChannel, String)> { let file = self.maybe_file.as_ref()?; // If the current version saved is not the actually current version of the binary // It means @@ -217,7 +232,7 @@ impl< .current_time() .signed_duration_since(file.last_prompt); if last_prompt_age > chrono::Duration::hours(UPGRADE_CHECK_INTERVAL) { - Some(file.latest_version.clone()) + Some((file.current_release_channel, file.latest_version.clone())) } else { None } @@ -238,6 +253,9 @@ fn get_minor_version(version: &str) -> &str { } fn print_release_notes(current_version: &str, new_version: &str) { + // TODO(bartlomieju): we might want to reconsider this one for RC releases. + // TODO(bartlomieju): also maybe just parse using `Version::standard` instead + // of using `get_minor_version`? if get_minor_version(current_version) == get_minor_version(new_version) { return; } @@ -274,8 +292,10 @@ pub fn check_for_upgrades( } let env = RealUpdateCheckerEnvironment::new(cache_file_path); - let version_provider = - RealVersionProvider::new(http_client_provider, UpgradeCheckKind::Execution); + let version_provider = RealVersionProvider::new( + http_client_provider.clone(), + UpgradeCheckKind::Execution, + ); let update_checker = UpdateChecker::new(env, version_provider); if update_checker.should_check_for_new_version() { @@ -294,16 +314,20 @@ pub fn check_for_upgrades( }); } + // Don't bother doing any more computation if we're not in TTY environment. + let should_prompt = + log::log_enabled!(log::Level::Info) && std::io::stderr().is_terminal(); + + if !should_prompt { + return; + } + // Print a message if an update is available - if let Some(upgrade_version) = update_checker.should_prompt() { - if log::log_enabled!(log::Level::Info) && std::io::stderr().is_terminal() { - if version::is_canary() { - log::info!( - "{} {}", - colors::green("A new canary release of Deno is available."), - colors::italic_gray("Run `deno upgrade --canary` to install it.") - ); - } else { + if let Some((release_channel, upgrade_version)) = + update_checker.should_prompt() + { + match release_channel { + ReleaseChannel::Stable => { log::info!( "{} {} → {} {}", colors::green("A new release of Deno is available:"), @@ -312,16 +336,31 @@ pub fn check_for_upgrades( colors::italic_gray("Run `deno upgrade` to install it.") ); } - - update_checker.store_prompted(); + ReleaseChannel::Canary => { + log::info!( + "{} {}", + colors::green("A new canary release of Deno is available."), + colors::italic_gray("Run `deno upgrade --canary` to install it.") + ); + } + ReleaseChannel::Rc => { + log::info!( + "{} {}", + colors::green("A new release candidate of Deno is available."), + colors::italic_gray("Run `deno upgrade --rc` to install it.") + ); + } + // TODO(bartlomieju) + ReleaseChannel::Lts => unreachable!(), } + + update_checker.store_prompted(); } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct LspVersionUpgradeInfo { pub latest_version: String, - // TODO(bartlomieju): use `ReleaseChannel` instead pub is_canary: bool, } @@ -346,34 +385,39 @@ async fn check_for_upgrades_for_lsp_with_provider( let current_version = version_provider.current_version(); // Nothing to upgrade - if current_version == latest_version { + if current_version == latest_version.version_or_hash { return Ok(None); } match release_channel { ReleaseChannel::Stable => { if let Ok(current) = Version::parse_standard(¤t_version) { - if let Ok(latest) = Version::parse_standard(&latest_version) { + if let Ok(latest) = + Version::parse_standard(&latest_version.version_or_hash) + { if current >= latest { return Ok(None); // nothing to upgrade } } } Ok(Some(LspVersionUpgradeInfo { - latest_version, + latest_version: latest_version.display, is_canary: false, })) } ReleaseChannel::Canary => Ok(Some(LspVersionUpgradeInfo { - latest_version, + latest_version: latest_version.display, + is_canary: true, + })), + + ReleaseChannel::Rc => Ok(Some(LspVersionUpgradeInfo { + latest_version: latest_version.display, is_canary: true, })), // TODO(bartlomieju) ReleaseChannel::Lts => unreachable!(), - // TODO(bartlomieju) - ReleaseChannel::Rc => unreachable!(), } } @@ -396,18 +440,18 @@ async fn fetch_and_store_latest_version< return; }; - env.write_check_file( - &CheckVersionFile { - // put a date in the past here so that prompt can be shown on next run - last_prompt: env - .current_time() - .sub(chrono::Duration::hours(UPGRADE_CHECK_INTERVAL + 1)), - last_checked: env.current_time(), - current_version: version_provider.current_version().to_string(), - latest_version, - } - .serialize(), - ); + let version_file = CheckVersionFile { + // put a date in the past here so that prompt can be shown on next run + last_prompt: env + .current_time() + .sub(chrono::Duration::hours(UPGRADE_CHECK_INTERVAL + 1)), + last_checked: env.current_time(), + current_version: version_provider.current_version().to_string(), + latest_version: latest_version.version_or_hash, + current_release_channel: release_channel, + }; + + env.write_check_file(&version_file.serialize()); } pub async fn upgrade( @@ -437,29 +481,33 @@ pub async fn upgrade( let requested_version = RequestedVersion::from_upgrade_flags(upgrade_flags.clone())?; - let maybe_install_version = match requested_version { + let maybe_selected_version_to_upgrade = match &requested_version { RequestedVersion::Latest(channel) => { find_latest_version_to_upgrade( http_client_provider.clone(), - channel, + *channel, force_selection_of_new_version, ) .await? } RequestedVersion::SpecificVersion(channel, version) => { select_specific_version_for_upgrade( - channel, - version, + *channel, + version.clone(), force_selection_of_new_version, )? } }; - let Some(install_version) = maybe_install_version else { + let Some(selected_version_to_upgrade) = maybe_selected_version_to_upgrade + else { return Ok(()); }; - let download_url = get_download_url(&install_version, upgrade_flags.canary)?; + let download_url = get_download_url( + &selected_version_to_upgrade.version_or_hash, + requested_version.is_canary(), + )?; log::info!("{}", colors::gray(format!("Downloading {}", &download_url))); let Some(archive_data) = download_package(&client, download_url).await? else { @@ -469,7 +517,10 @@ pub async fn upgrade( log::info!( "{}", - colors::gray(format!("Deno is upgrading to version {}", &install_version)) + colors::gray(format!( + "Deno is upgrading to version {}", + &selected_version_to_upgrade.display + )) ); let temp_dir = tempfile::TempDir::new()?; @@ -486,8 +537,11 @@ pub async fn upgrade( if upgrade_flags.dry_run { fs::remove_file(&new_exe_path)?; log::info!("Upgraded successfully (dry run)"); - if !upgrade_flags.canary { - print_release_notes(version::deno(), &install_version); + if !requested_version.is_canary() { + print_release_notes( + version::deno(), + &selected_version_to_upgrade.version_or_hash, + ); } drop(temp_dir); return Ok(()); @@ -507,11 +561,11 @@ pub async fn upgrade( "{}", colors::green(format!( "\nUpgraded successfully to Deno v{}\n", - install_version + selected_version_to_upgrade.display )) ); - if !upgrade_flags.canary { - print_release_notes(version::deno(), &install_version); + if !requested_version.is_canary() { + print_release_notes(version::deno(), &selected_version_to_upgrade.display); } drop(temp_dir); // delete the temp dir @@ -526,10 +580,13 @@ enum RequestedVersion { impl RequestedVersion { fn from_upgrade_flags(upgrade_flags: UpgradeFlags) -> Result { let is_canary = upgrade_flags.canary; + let is_release_candidate = upgrade_flags.release_candidate; let Some(passed_version) = upgrade_flags.version else { let channel = if is_canary { ReleaseChannel::Canary + } else if is_release_candidate { + ReleaseChannel::Rc } else { ReleaseChannel::Stable }; @@ -556,13 +613,25 @@ impl RequestedVersion { Ok(RequestedVersion::SpecificVersion(channel, passed_version)) } + + /// Channels that use Git hashes as versions are considered canary. + pub fn is_canary(&self) -> bool { + match self { + Self::Latest(channel) => { + matches!(channel, ReleaseChannel::Canary | ReleaseChannel::Rc) + } + Self::SpecificVersion(channel, _) => { + matches!(channel, ReleaseChannel::Canary | ReleaseChannel::Rc) + } + } + } } fn select_specific_version_for_upgrade( release_channel: ReleaseChannel, version: String, force: bool, -) -> Result, AnyError> { +) -> Result, AnyError> { match release_channel { ReleaseChannel::Stable => { let current_is_passed = if !version::is_canary() { @@ -576,7 +645,10 @@ fn select_specific_version_for_upgrade( return Ok(None); } - Ok(Some(version)) + Ok(Some(AvailableVersion { + version_or_hash: version.to_string(), + display: version, + })) } ReleaseChannel::Canary => { let current_is_passed = version::GIT_COMMIT_HASH == version; @@ -585,7 +657,10 @@ fn select_specific_version_for_upgrade( return Ok(None); } - Ok(Some(version)) + Ok(Some(AvailableVersion { + version_or_hash: version.to_string(), + display: version, + })) } // TODO(bartlomieju) ReleaseChannel::Rc => unreachable!(), @@ -598,14 +673,14 @@ async fn find_latest_version_to_upgrade( http_client_provider: Arc, release_channel: ReleaseChannel, force: bool, -) -> Result, AnyError> { +) -> Result, AnyError> { log::info!( "{}", colors::gray(&format!("Looking up {} version", release_channel.name())) ); let client = http_client_provider.get_or_create()?; - let latest_version = + let latest_version_found = fetch_latest_version(&client, release_channel, UpgradeCheckKind::Execution) .await?; @@ -614,7 +689,9 @@ async fn find_latest_version_to_upgrade( let current_version = version::deno(); let current_is_most_recent = if !version::is_canary() { let current = Version::parse_standard(current_version).unwrap(); - let latest = Version::parse_standard(&latest_version).unwrap(); + let latest = + Version::parse_standard(&latest_version_found.version_or_hash) + .unwrap(); current >= latest } else { false @@ -623,21 +700,31 @@ async fn find_latest_version_to_upgrade( if !force && current_is_most_recent { (None, current_version) } else { - (Some(latest_version), current_version) + (Some(latest_version_found), current_version) } } ReleaseChannel::Canary => { let current_version = version::GIT_COMMIT_HASH; - let current_is_most_recent = current_version == latest_version; + let current_is_most_recent = + current_version == latest_version_found.version_or_hash; if !force && current_is_most_recent { (None, current_version) } else { - (Some(latest_version), current_version) + (Some(latest_version_found), current_version) + } + } + ReleaseChannel::Rc => { + let current_version = version::GIT_COMMIT_HASH; + let current_is_most_recent = + current_version == latest_version_found.version_or_hash; + + if !force && current_is_most_recent { + (None, current_version) + } else { + (Some(latest_version_found), current_version) } } - // TODO(bartlomieju) - ReleaseChannel::Rc => unreachable!(), // TODO(bartlomieju) ReleaseChannel::Lts => unreachable!(), }; @@ -648,7 +735,7 @@ async fn find_latest_version_to_upgrade( "{}", color_print::cformat!( "Found latest version {}", - newer_latest_version + newer_latest_version.display ) ); } else { @@ -665,7 +752,7 @@ async fn find_latest_version_to_upgrade( Ok(maybe_newer_latest_version) } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] enum ReleaseChannel { /// Stable version, eg. 1.45.4, 2.0.0, 2.1.0 Stable, @@ -677,8 +764,7 @@ enum ReleaseChannel { #[allow(unused)] Lts, - /// Release candidate - #[allow(unused)] + /// Release candidate, poiting to a git hash Rc, } @@ -691,26 +777,106 @@ impl ReleaseChannel { Self::Lts => "LTS (long term support)", } } + + fn serialize(&self) -> String { + match self { + Self::Stable => "stable", + Self::Canary => "canary", + Self::Rc => "rc", + Self::Lts => "lts", + } + .to_string() + } + + fn deserialize(str_: &str) -> Result { + Ok(match str_ { + "stable" => Self::Stable, + "canary" => Self::Canary, + "rc" => Self::Rc, + "lts" => Self::Lts, + unknown => bail!("Unrecognized release channel: {}", unknown), + }) + } +} + +// Return a list of available RC release in format of (, ) +fn parse_rc_versions_text( + text: &str, +) -> Result, AnyError> { + let lines: Vec<_> = text + .split("\n") + .map(|s| s.to_string()) + .filter(|s| !s.is_empty()) + .collect(); + if lines.is_empty() { + bail!("No release candidates available"); + } + + let mut parsed = Vec::with_capacity(lines.len()); + + for line in lines { + let v = line.split(' ').collect::>(); + if v.len() != 2 { + bail!("Malformed list of RC releases"); + } + parsed.push((v[0].to_string(), v[1].to_string())); + } + + Ok(parsed) +} + +async fn get_rc_versions( + client: &HttpClient, + check_kind: UpgradeCheckKind, +) -> Result, AnyError> { + let url = + get_latest_version_url(ReleaseChannel::Rc, env!("TARGET"), check_kind); + let text = client.download_text(url.parse()?).await?; + parse_rc_versions_text(&text) +} + +#[derive(Debug, Clone, PartialEq)] +struct AvailableVersion { + version_or_hash: String, + display: String, } async fn fetch_latest_version( client: &HttpClient, release_channel: ReleaseChannel, check_kind: UpgradeCheckKind, -) -> Result { +) -> Result { let url = get_latest_version_url(release_channel, env!("TARGET"), check_kind); let text = client.download_text(url.parse()?).await?; - Ok(normalize_version_from_server(release_channel, &text)) + let version = normalize_version_from_server(release_channel, &text)?; + Ok(version) } fn normalize_version_from_server( release_channel: ReleaseChannel, text: &str, -) -> String { +) -> Result { let text = text.trim(); match release_channel { - ReleaseChannel::Stable => text.trim_start_matches('v').to_string(), - ReleaseChannel::Canary => text.to_string(), + ReleaseChannel::Stable => { + let v = text.trim_start_matches('v').to_string(); + Ok(AvailableVersion { + version_or_hash: v.to_string(), + display: v.to_string(), + }) + } + ReleaseChannel::Canary => Ok(AvailableVersion { + version_or_hash: text.to_string(), + display: text.to_string(), + }), + ReleaseChannel::Rc => { + let lines = parse_rc_versions_text(text)?; + let latest = lines.last().unwrap(); + Ok(AvailableVersion { + version_or_hash: latest.0.to_string(), + display: latest.1.to_string(), + }) + } _ => unreachable!(), } } @@ -725,6 +891,7 @@ fn get_latest_version_url( ReleaseChannel::Canary => { Cow::Owned(format!("canary-{target_tuple}-latest.txt")) } + ReleaseChannel::Rc => Cow::Borrowed("release-rc.txt"), _ => unreachable!(), }; let query_param = match check_kind { @@ -864,13 +1031,14 @@ struct CheckVersionFile { pub last_checked: chrono::DateTime, pub current_version: String, pub latest_version: String, + pub current_release_channel: ReleaseChannel, } impl CheckVersionFile { pub fn parse(content: String) -> Option { let split_content = content.split('!').collect::>(); - if split_content.len() != 4 { + if split_content.len() != 5 { return None; } @@ -882,6 +1050,15 @@ impl CheckVersionFile { if current_version.is_empty() { return None; } + let current_release_channel = split_content[4].trim().to_owned(); + if current_release_channel.is_empty() { + return None; + } + let Ok(current_release_channel) = + ReleaseChannel::deserialize(¤t_release_channel) + else { + return None; + }; let last_prompt = chrono::DateTime::parse_from_rfc3339(split_content[0]) .map(|dt| dt.with_timezone(&chrono::Utc)) @@ -895,16 +1072,18 @@ impl CheckVersionFile { last_checked, current_version, latest_version, + current_release_channel, }) } fn serialize(&self) -> String { format!( - "{}!{}!{}!{}", + "{}!{}!{}!{}!{}", self.last_prompt.to_rfc3339(), self.last_checked.to_rfc3339(), self.latest_version, self.current_version, + self.current_release_channel.serialize(), ) } @@ -925,9 +1104,16 @@ mod test { #[test] fn test_parse_upgrade_check_file() { - let file = CheckVersionFile::parse( + // NOTE(bartlomieju): pre-1.46 format + let maybe_file = CheckVersionFile::parse( "2020-01-01T00:00:00+00:00!2020-01-01T00:00:00+00:00!1.2.3!1.2.2" .to_string(), + ); + assert!(maybe_file.is_none()); + // NOTE(bartlomieju): post-1.46 format + let file = CheckVersionFile::parse( + "2020-01-01T00:00:00+00:00!2020-01-01T00:00:00+00:00!1.2.3!1.2.2!stable" + .to_string(), ) .unwrap(); assert_eq!( @@ -940,6 +1126,7 @@ mod test { ); assert_eq!(file.latest_version, "1.2.3".to_string()); assert_eq!(file.current_version, "1.2.2".to_string()); + assert_eq!(file.current_release_channel, ReleaseChannel::Stable); let result = CheckVersionFile::parse("2020-01-01T00:00:00+00:00!".to_string()); @@ -952,9 +1139,46 @@ mod test { assert!(result.is_none()); } + #[test] + fn test_parse_rc_versions_text() { + let rc_versions = parse_rc_versions_text( + r#"qwerw3452gbxcvbarwett234 v1.46.0-rc.0 +cvbnfhuertt23523452345 v1.46.0-rc.1 +sdfq3452345egasdfgsdgf v2.0.0-rc.0 +asdf456yegfncbvjwe4523 v2.0.0-rc.1 +hjr6562w34rgzcvh56734a v2.0.0-rc.2 +bdfgtd6wergsdg3243234v v2.0.0-rc.3 +"#, + ) + .unwrap(); + + assert_eq!(rc_versions.len(), 6); + assert_eq!(rc_versions[3].0, "asdf456yegfncbvjwe4523"); + assert_eq!(rc_versions[3].1, "v2.0.0-rc.1"); + + let rc_versions = parse_rc_versions_text( + r#"qwerw3452gbxcvbarwett234 v1.46.0-rc.0 + +cvbnfhuertt23523452345 v1.46.0-rc.1 +"#, + ) + .unwrap(); + + assert_eq!(rc_versions.len(), 2); + assert_eq!(rc_versions[1].0, "cvbnfhuertt23523452345"); + assert_eq!(rc_versions[1].1, "v1.46.0-rc.1"); + + let err = + parse_rc_versions_text(r#"qwerw3452gbxcvbarwett234 v1.46.0-rc.0"#) + .unwrap_err(); + assert_eq!(err.to_string(), "Malformed list of RC releases"); + let err = parse_rc_versions_text("").unwrap_err(); + assert_eq!(err.to_string(), "No release candidates available"); + } + #[test] fn test_serialize_upgrade_check_file() { - let file = CheckVersionFile { + let mut file = CheckVersionFile { last_prompt: chrono::DateTime::parse_from_rfc3339("2020-01-01T00:00:00Z") .unwrap() .with_timezone(&chrono::Utc), @@ -965,19 +1189,35 @@ mod test { .with_timezone(&chrono::Utc), latest_version: "1.2.3".to_string(), current_version: "1.2.2".to_string(), + current_release_channel: ReleaseChannel::Stable, }; assert_eq!( file.serialize(), - "2020-01-01T00:00:00+00:00!2020-01-01T00:00:00+00:00!1.2.3!1.2.2" + "2020-01-01T00:00:00+00:00!2020-01-01T00:00:00+00:00!1.2.3!1.2.2!stable" + ); + file.current_release_channel = ReleaseChannel::Canary; + assert_eq!( + file.serialize(), + "2020-01-01T00:00:00+00:00!2020-01-01T00:00:00+00:00!1.2.3!1.2.2!canary" + ); + file.current_release_channel = ReleaseChannel::Rc; + assert_eq!( + file.serialize(), + "2020-01-01T00:00:00+00:00!2020-01-01T00:00:00+00:00!1.2.3!1.2.2!rc" + ); + file.current_release_channel = ReleaseChannel::Lts; + assert_eq!( + file.serialize(), + "2020-01-01T00:00:00+00:00!2020-01-01T00:00:00+00:00!1.2.3!1.2.2!lts" ); } #[derive(Clone)] struct TestUpdateCheckerEnvironment { file_text: Rc>, - is_canary: Rc>, + release_channel: Rc>, current_version: Rc>, - latest_version: Rc>>, + latest_version: Rc>>, time: Rc>>, } @@ -986,8 +1226,11 @@ mod test { Self { file_text: Default::default(), current_version: Default::default(), - is_canary: Default::default(), - latest_version: Rc::new(RefCell::new(Ok("".to_string()))), + release_channel: Rc::new(RefCell::new(ReleaseChannel::Stable)), + latest_version: Rc::new(RefCell::new(Ok(AvailableVersion { + version_or_hash: "".to_string(), + display: "".to_string(), + }))), time: Rc::new(RefCell::new(chrono::Utc::now())), } } @@ -1008,15 +1251,18 @@ mod test { } pub fn set_latest_version(&self, version: &str) { - *self.latest_version.borrow_mut() = Ok(version.to_string()); + *self.latest_version.borrow_mut() = Ok(AvailableVersion { + version_or_hash: version.to_string(), + display: version.to_string(), + }); } pub fn set_latest_version_err(&self, err: &str) { *self.latest_version.borrow_mut() = Err(err.to_string()); } - pub fn set_is_canary(&self, is_canary: bool) { - *self.is_canary.borrow_mut() = is_canary; + pub fn set_release_channel(&self, channel: ReleaseChannel) { + *self.release_channel.borrow_mut() = channel; } } @@ -1026,7 +1272,7 @@ mod test { async fn latest_version( &self, _release_channel: ReleaseChannel, - ) -> Result { + ) -> Result { match self.latest_version.borrow().clone() { Ok(result) => Ok(result), Err(err) => bail!("{}", err), @@ -1037,15 +1283,10 @@ mod test { Cow::Owned(self.current_version.borrow().clone()) } - // TODO(bartlomieju): update to handle `Lts` and `Rc` channels async fn get_current_exe_release_channel( &self, ) -> Result { - if *self.is_canary.borrow() { - Ok(ReleaseChannel::Canary) - } else { - Ok(ReleaseChannel::Stable) - } + Ok(*self.release_channel.borrow()) } } @@ -1083,25 +1324,37 @@ mod test { // should not check for latest version because we just did assert!(!checker.should_check_for_new_version()); // but should prompt - assert_eq!(checker.should_prompt(), Some("1.1.0".to_string())); + assert_eq!( + checker.should_prompt(), + Some((ReleaseChannel::Stable, "1.1.0".to_string())) + ); // fast forward an hour and bump the latest version env.add_hours(1); env.set_latest_version("1.2.0"); assert!(!checker.should_check_for_new_version()); - assert_eq!(checker.should_prompt(), Some("1.1.0".to_string())); + assert_eq!( + checker.should_prompt(), + Some((ReleaseChannel::Stable, "1.1.0".to_string())) + ); // fast forward again and it should check for a newer version env.add_hours(UPGRADE_CHECK_INTERVAL); assert!(checker.should_check_for_new_version()); - assert_eq!(checker.should_prompt(), Some("1.1.0".to_string())); + assert_eq!( + checker.should_prompt(), + Some((ReleaseChannel::Stable, "1.1.0".to_string())) + ); fetch_and_store_latest_version(&env, &env).await; // reload and store that we prompted let checker = UpdateChecker::new(env.clone(), env.clone()); assert!(!checker.should_check_for_new_version()); - assert_eq!(checker.should_prompt(), Some("1.2.0".to_string())); + assert_eq!( + checker.should_prompt(), + Some((ReleaseChannel::Stable, "1.2.0".to_string())) + ); checker.store_prompted(); // reload and it should now say not to prompt @@ -1112,7 +1365,10 @@ mod test { // but if we fast forward past the upgrade interval it should prompt again env.add_hours(UPGRADE_CHECK_INTERVAL + 1); assert!(checker.should_check_for_new_version()); - assert_eq!(checker.should_prompt(), Some("1.2.0".to_string())); + assert_eq!( + checker.should_prompt(), + Some((ReleaseChannel::Stable, "1.2.0".to_string())) + ); // upgrade the version and it should stop prompting env.set_current_version("1.2.0"); @@ -1128,6 +1384,21 @@ mod test { fetch_and_store_latest_version(&env, &env).await; assert!(checker.should_check_for_new_version()); assert_eq!(checker.should_prompt(), None); + + // now switch to RC release + env.set_release_channel(ReleaseChannel::Rc); + env.set_current_version("1.46.0-rc.0"); + env.set_latest_version("1.46.0-rc.1"); + fetch_and_store_latest_version(&env, &env).await; + env.add_hours(UPGRADE_CHECK_INTERVAL + 1); + + // We should check for new version and prompt + let checker = UpdateChecker::new(env.clone(), env.clone()); + assert!(checker.should_check_for_new_version()); + assert_eq!( + checker.should_prompt(), + Some((ReleaseChannel::Rc, "1.46.0-rc.1".to_string())) + ); } #[tokio::test] @@ -1140,6 +1411,7 @@ mod test { last_checked: env.current_time(), latest_version: "1.26.2".to_string(), current_version: "1.27.0".to_string(), + current_release_channel: ReleaseChannel::Stable, } .serialize(); env.write_check_file(&file_content); @@ -1162,6 +1434,7 @@ mod test { last_checked: env.current_time(), latest_version: "1.26.2".to_string(), current_version: "1.25.0".to_string(), + current_release_channel: ReleaseChannel::Stable, } .serialize(); env.write_check_file(&file_content); @@ -1231,11 +1504,43 @@ mod test { ); assert_eq!( get_latest_version_url( - ReleaseChannel::Stable, + ReleaseChannel::Rc, "x86_64-pc-windows-msvc", UpgradeCheckKind::Lsp ), - "https://dl.deno.land/release-latest.txt?lsp" + "https://dl.deno.land/release-rc.txt?lsp" + ); + assert_eq!( + get_latest_version_url( + ReleaseChannel::Rc, + "aarch64-apple-darwin", + UpgradeCheckKind::Execution + ), + "https://dl.deno.land/release-rc.txt" + ); + assert_eq!( + get_latest_version_url( + ReleaseChannel::Rc, + "aarch64-apple-darwin", + UpgradeCheckKind::Lsp + ), + "https://dl.deno.land/release-rc.txt?lsp" + ); + assert_eq!( + get_latest_version_url( + ReleaseChannel::Rc, + "x86_64-pc-windows-msvc", + UpgradeCheckKind::Execution + ), + "https://dl.deno.land/release-rc.txt" + ); + assert_eq!( + get_latest_version_url( + ReleaseChannel::Rc, + "x86_64-pc-windows-msvc", + UpgradeCheckKind::Lsp + ), + "https://dl.deno.land/release-rc.txt?lsp" ); } @@ -1243,24 +1548,46 @@ mod test { fn test_normalize_version_server() { // should strip v for stable assert_eq!( - normalize_version_from_server(ReleaseChannel::Stable, "v1.0.0"), - "1.0.0" + normalize_version_from_server(ReleaseChannel::Stable, "v1.0.0").unwrap(), + AvailableVersion { + version_or_hash: "1.0.0".to_string(), + display: "1.0.0".to_string() + }, ); // should not replace v after start assert_eq!( normalize_version_from_server( ReleaseChannel::Stable, " v1.0.0-test-v\n\n " - ), - "1.0.0-test-v" + ) + .unwrap(), + AvailableVersion { + version_or_hash: "1.0.0-test-v".to_string(), + display: "1.0.0-test-v".to_string() + } ); // should not strip v for canary assert_eq!( normalize_version_from_server( ReleaseChannel::Canary, " v1452345asdf \n\n " - ), - "v1452345asdf" + ) + .unwrap(), + AvailableVersion { + version_or_hash: "v1452345asdf".to_string(), + display: "v1452345asdf".to_string() + } + ); + assert_eq!( + normalize_version_from_server( + ReleaseChannel::Rc, + "asdfq345wdfasdfasdf v1.46.0-rc.0\nasdfq345wdfasdfasdf v1.46.0-rc.1\n" + ) + .unwrap(), + AvailableVersion { + version_or_hash: "asdfq345wdfasdfasdf".to_string(), + display: "v1.46.0-rc.1".to_string(), + }, ); } @@ -1303,7 +1630,7 @@ mod test { { env.set_current_version("123"); env.set_latest_version("123"); - env.set_is_canary(true); + env.set_release_channel(ReleaseChannel::Canary); let maybe_info = check_for_upgrades_for_lsp_with_provider(&env) .await .unwrap(); @@ -1323,5 +1650,30 @@ mod test { }) ); } + // rc equal + { + env.set_release_channel(ReleaseChannel::Rc); + env.set_current_version("1.2.3-rc.0"); + env.set_latest_version("1.2.3-rc.0"); + let maybe_info = check_for_upgrades_for_lsp_with_provider(&env) + .await + .unwrap(); + assert_eq!(maybe_info, None); + } + // canary different + { + env.set_latest_version("1.2.3-rc.0"); + env.set_latest_version("1.2.3-rc.1"); + let maybe_info = check_for_upgrades_for_lsp_with_provider(&env) + .await + .unwrap(); + assert_eq!( + maybe_info, + Some(LspVersionUpgradeInfo { + latest_version: "1.2.3-rc.1".to_string(), + is_canary: true, + }) + ); + } } }