use std::borrow::Cow;
use std::env;
use std::fs;
use std::io::IsTerminal;
use std::ops::Sub;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::sync::Arc;
use std::time::Duration;
use async_trait::async_trait;
use deno_core::anyhow::Context;
use deno_core::anyhow::bail;
use deno_core::error::AnyError;
use deno_core::unsync::spawn;
use deno_core::url::Url;
use deno_lib::shared::ReleaseChannel;
use deno_lib::version;
use deno_semver::SmallStackString;
use deno_semver::Version;
use once_cell::sync::Lazy;
use sha2::Digest;
use sys_traits::FsDirEntry;
use sys_traits::FsMetadataValue;
use crate::args::Flags;
use crate::args::UPGRADE_USAGE;
use crate::args::UpgradeFlags;
use crate::colors;
use crate::factory::CliFactory;
use crate::http_util::HttpClient;
use crate::http_util::HttpClientProvider;
use crate::util::archive;
use crate::util::progress_bar::ProgressBar;
use crate::util::progress_bar::ProgressBarStyle;
static RELEASE_URL: &str = "https://github.com/denoland/deno/releases";
static CANARY_URL: &str = "https://dl.deno.land/canary";
static DL_RELEASE_URL: &str = "https://dl.deno.land/release";
pub static ARCHIVE_NAME: Lazy<String> =
Lazy::new(|| format!("deno-{}.zip", env!("TARGET")));
const UPGRADE_CHECK_INTERVAL: i64 = 24;
const UPGRADE_CHECK_FETCH_DELAY: Duration = Duration::from_millis(500);
trait UpdateCheckerEnvironment: Clone {
fn read_check_file(&self) -> String;
fn write_check_file(&self, text: &str);
fn current_time(&self) -> chrono::DateTime<chrono::Utc>;
}
#[derive(Clone)]
struct RealUpdateCheckerEnvironment {
cache_file_path: PathBuf,
current_time: chrono::DateTime<chrono::Utc>,
}
impl RealUpdateCheckerEnvironment {
pub fn new(cache_file_path: PathBuf) -> Self {
Self {
cache_file_path,
current_time: chrono::Utc::now(),
}
}
}
impl UpdateCheckerEnvironment for RealUpdateCheckerEnvironment {
fn read_check_file(&self) -> String {
std::fs::read_to_string(&self.cache_file_path).unwrap_or_default()
}
fn write_check_file(&self, text: &str) {
let _ = std::fs::write(&self.cache_file_path, text);
}
fn current_time(&self) -> chrono::DateTime<chrono::Utc> {
self.current_time
}
}
#[derive(Debug, Copy, Clone)]
enum UpgradeCheckKind {
Execution,
Lsp,
}
#[async_trait(?Send)]
trait VersionProvider: Clone {
async fn latest_version(
&self,
release_channel: ReleaseChannel,
) -> Result<AvailableVersion, AnyError>;
fn current_version(&self) -> Cow<'_, str>;
fn get_current_exe_release_channel(&self) -> ReleaseChannel;
}
#[derive(Clone)]
struct RealVersionProvider {
http_client_provider: Arc<HttpClientProvider>,
check_kind: UpgradeCheckKind,
}
impl RealVersionProvider {
pub fn new(
http_client_provider: Arc<HttpClientProvider>,
check_kind: UpgradeCheckKind,
) -> Self {
Self {
http_client_provider,
check_kind,
}
}
}
#[async_trait(?Send)]
impl VersionProvider for RealVersionProvider {
async fn latest_version(
&self,
release_channel: ReleaseChannel,
) -> Result<AvailableVersion, AnyError> {
fetch_latest_version(
&self.http_client_provider.get_or_create()?,
release_channel,
self.check_kind,
)
.await
}
fn current_version(&self) -> Cow<'_, str> {
Cow::Borrowed(version::DENO_VERSION_INFO.version_or_git_hash())
}
fn get_current_exe_release_channel(&self) -> ReleaseChannel {
version::DENO_VERSION_INFO.release_channel
}
}
struct UpdateChecker<
TEnvironment: UpdateCheckerEnvironment,
TVersionProvider: VersionProvider,
> {
env: TEnvironment,
version_provider: TVersionProvider,
maybe_file: Option<CheckVersionFile>,
}
impl<TEnvironment: UpdateCheckerEnvironment, TVersionProvider: VersionProvider>
UpdateChecker<TEnvironment, TVersionProvider>
{
pub fn new(env: TEnvironment, version_provider: TVersionProvider) -> Self {
let maybe_file = CheckVersionFile::parse(env.read_check_file());
Self {
env,
version_provider,
maybe_file,
}
}
pub fn should_check_for_new_version(&self) -> bool {
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)
}
pub fn should_prompt(&self) -> Option<(ReleaseChannel, String)> {
let file = self.maybe_file.as_ref()?;
let current_version = self.version_provider.current_version();
if file.current_version != current_version {
return None;
}
if file.latest_version == current_version {
return None;
}
if let Ok(current) = Version::parse_standard(¤t_version)
&& let Ok(latest) = Version::parse_standard(&file.latest_version)
&& current >= latest
{
return None;
}
let last_prompt_age = self
.env
.current_time()
.signed_duration_since(file.last_prompt);
if last_prompt_age > chrono::Duration::hours(UPGRADE_CHECK_INTERVAL) {
Some((file.current_release_channel, file.latest_version.clone()))
} else {
None
}
}
pub fn store_prompted(self) {
if let Some(file) = self.maybe_file {
self.env.write_check_file(
&file.with_last_prompt(self.env.current_time()).serialize(),
);
}
}
}
fn get_minor_version_blog_post_url(semver: &Version) -> String {
format!("https://deno.com/blog/v{}.{}", semver.major, semver.minor)
}
fn get_rc_version_blog_post_url(semver: &Version) -> String {
format!(
"https://deno.com/blog/v{}.{}-rc-{}",
semver.major, semver.minor, semver.pre[1]
)
}
async fn print_release_notes(
current_version: &str,
new_version: &str,
client: &HttpClient,
) {
let Ok(current_semver) = Version::parse_standard(current_version) else {
return;
};
let Ok(new_semver) = Version::parse_standard(new_version) else {
return;
};
let is_switching_from_deno1_to_deno2 =
new_semver.major == 2 && current_semver.major == 1;
let is_deno_2_rc = new_semver.major == 2
&& new_semver.minor == 0
&& new_semver.patch == 0
&& new_semver.pre.first().map(|s| s.as_str()) == Some("rc");
if is_deno_2_rc || is_switching_from_deno1_to_deno2 {
log::info!(
"{}\n\n {}\n",
colors::gray("Migration guide:"),
colors::bold(
"https://docs.deno.com/runtime/manual/advanced/migrate_deprecations"
)
);
}
if is_deno_2_rc {
log::info!(
"{}\n\n {}\n",
colors::gray("If you find a bug, please report to:"),
colors::bold("https://github.com/denoland/deno/issues/new")
);
let blog_url_str = get_rc_version_blog_post_url(&new_semver);
let blog_url = Url::parse(&blog_url_str).unwrap();
if client.download(blog_url).await.is_ok() {
log::info!(
"{}\n\n {}\n",
colors::gray("Blog post:"),
colors::bold(blog_url_str)
);
}
return;
}
let should_print = current_semver.major != new_semver.major
|| current_semver.minor != new_semver.minor;
if !should_print {
return;
}
log::info!(
"{}\n\n {}\n",
colors::gray("Release notes:"),
colors::bold(format!(
"https://github.com/denoland/deno/releases/tag/v{}",
&new_version,
))
);
log::info!(
"{}\n\n {}\n",
colors::gray("Blog post:"),
colors::bold(get_minor_version_blog_post_url(&new_semver))
);
}
pub fn upgrade_check_enabled() -> bool {
matches!(
env::var("DENO_NO_UPDATE_CHECK"),
Err(env::VarError::NotPresent)
)
}
pub fn check_for_upgrades(
http_client_provider: Arc<HttpClientProvider>,
cache_file_path: PathBuf,
) {
if !upgrade_check_enabled() {
return;
}
let env = RealUpdateCheckerEnvironment::new(cache_file_path);
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() {
let env = update_checker.env.clone();
let version_provider = update_checker.version_provider.clone();
spawn(async move {
tokio::time::sleep(UPGRADE_CHECK_FETCH_DELAY).await;
fetch_and_store_latest_version(&env, &version_provider).await;
log::debug!("Finished upgrade checker.")
});
}
let should_prompt =
log::log_enabled!(log::Level::Info) && std::io::stderr().is_terminal();
if !should_prompt {
return;
}
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:"),
colors::cyan(version::DENO_VERSION_INFO.deno),
colors::cyan(&upgrade_version),
colors::italic_gray("Run `deno upgrade` to install it.")
);
}
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.")
);
}
ReleaseChannel::Lts => {
log::info!(
"{} {} → {} {}",
colors::green("A new LTS release of Deno is available:"),
colors::cyan(version::DENO_VERSION_INFO.deno),
colors::cyan(&upgrade_version),
colors::italic_gray("Run `deno upgrade lts` to install it.")
);
}
ReleaseChannel::Alpha => {
log::info!(
"{} {} → {} {}",
colors::green("A new alpha release of Deno is available:"),
colors::cyan(version::DENO_VERSION_INFO.deno),
colors::cyan(&upgrade_version),
colors::italic_gray("Run `deno upgrade alpha` to install it.")
);
}
ReleaseChannel::Beta => {
log::info!(
"{} {} → {} {}",
colors::green("A new beta release of Deno is available:"),
colors::cyan(version::DENO_VERSION_INFO.deno),
colors::cyan(&upgrade_version),
colors::italic_gray("Run `deno upgrade beta` to install it.")
);
}
}
update_checker.store_prompted();
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LspVersionUpgradeInfo {
pub latest_version: String,
pub is_canary: bool,
}
pub async fn check_for_upgrades_for_lsp(
http_client_provider: Arc<HttpClientProvider>,
) -> Result<Option<LspVersionUpgradeInfo>, AnyError> {
if !upgrade_check_enabled() {
return Ok(None);
}
let version_provider =
RealVersionProvider::new(http_client_provider, UpgradeCheckKind::Lsp);
check_for_upgrades_for_lsp_with_provider(&version_provider).await
}
async fn check_for_upgrades_for_lsp_with_provider(
version_provider: &impl VersionProvider,
) -> Result<Option<LspVersionUpgradeInfo>, AnyError> {
let release_channel = version_provider.get_current_exe_release_channel();
let latest_version = version_provider.latest_version(release_channel).await?;
let current_version = version_provider.current_version();
if current_version == latest_version.version_or_hash {
return Ok(None);
}
match release_channel {
ReleaseChannel::Stable
| ReleaseChannel::Rc
| ReleaseChannel::Lts
| ReleaseChannel::Alpha
| ReleaseChannel::Beta => {
if let Ok(current) = Version::parse_standard(¤t_version)
&& let Ok(latest) =
Version::parse_standard(&latest_version.version_or_hash)
&& current >= latest
{
return Ok(None); }
Ok(Some(LspVersionUpgradeInfo {
latest_version: latest_version.version_or_hash,
is_canary: false,
}))
}
ReleaseChannel::Canary => Ok(Some(LspVersionUpgradeInfo {
latest_version: latest_version.version_or_hash,
is_canary: true,
})),
}
}
async fn fetch_and_store_latest_version<
TEnvironment: UpdateCheckerEnvironment,
TVersionProvider: VersionProvider,
>(
env: &TEnvironment,
version_provider: &TVersionProvider,
) {
let release_channel = version_provider.get_current_exe_release_channel();
let Ok(latest_version) =
version_provider.latest_version(release_channel).await
else {
return;
};
let version_file = CheckVersionFile {
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());
}
fn get_binary_cache_path(
dl_dir: &Path,
version: &str,
release_channel: ReleaseChannel,
) -> PathBuf {
let binary_path_suffix = match release_channel {
ReleaseChannel::Canary => {
format!("canary/{}/{}", version, *ARCHIVE_NAME)
}
_ => {
format!("release/v{}/{}", version, *ARCHIVE_NAME)
}
};
dl_dir.join(binary_path_suffix)
}
fn prune_canary_cache<
Sys: sys_traits::FsReadDir + sys_traits::FsRemoveDirAll,
>(
sys: &Sys,
dl_dir: &Path,
max_entries: usize,
) {
let canary_dir = dl_dir.join("canary");
let Ok(entries) = sys.fs_read_dir(&canary_dir) else {
return;
};
let mut dirs: Vec<(PathBuf, std::time::SystemTime)> = entries
.filter_map(|e| e.ok())
.filter(|e| e.file_type().map(|t| t.is_dir()).unwrap_or(false))
.filter_map(|e| {
let modified = e.metadata().ok()?.modified().ok()?;
Some((e.path().into_owned(), modified))
})
.collect();
if dirs.len() <= max_entries {
return;
}
dirs.sort_by(|a, b| b.1.cmp(&a.1));
for (path, _) in dirs.into_iter().skip(max_entries) {
let _ = sys.fs_remove_dir_all(&path);
}
}
fn try_read_cached_binary(
sys: &impl sys_traits::FsRead,
cache_path: &Path,
) -> Option<Vec<u8>> {
match sys.fs_read(cache_path) {
Ok(data) => Some(data.into_owned()),
Err(_) => None,
}
}
fn store_cached_binary(
sys: &impl deno_path_util::fs::AtomicWriteFileSys,
cache_path: &Path,
data: &[u8],
) {
if let Err(err) =
deno_path_util::fs::atomic_write_file(sys, cache_path, data, 0o644)
{
log::debug!("Failed to cache binary: {}", err);
}
}
fn get_pr_artifact_name() -> Result<String, AnyError> {
let target = env!("TARGET");
let (os, arch) = if target.contains("linux") && target.contains("x86_64") {
("linux", "x86_64")
} else if target.contains("linux") && target.contains("aarch64") {
("linux", "aarch64")
} else if target.contains("apple") && target.contains("x86_64") {
("macos", "x86_64")
} else if target.contains("apple") && target.contains("aarch64") {
("macos", "aarch64")
} else if target.contains("windows") && target.contains("x86_64") {
("windows", "x86_64")
} else if target.contains("windows") && target.contains("aarch64") {
("windows", "aarch64")
} else {
bail!("Unsupported platform for PR builds: {}", target)
};
Ok(format!("release-{os}-{arch}-deno"))
}
fn get_pr_debug_artifact_name() -> Result<String, AnyError> {
let release_name = get_pr_artifact_name()?;
Ok(release_name.replacen("release-", "debug-", 1))
}
fn upgrade_from_pr(
pr_number: u64,
upgrade_flags: &UpgradeFlags,
) -> Result<(), AnyError> {
let gh_version = Command::new("gh").arg("--version").output();
if gh_version.is_err() {
bail!(
"The `gh` CLI is required for installing from a PR.\n\
Install it from https://cli.github.com/ and run `gh auth login`."
);
}
log::info!("{}", colors::gray(format!("Looking up PR #{pr_number}...")));
let pr_info = Command::new("gh")
.args([
"pr",
"view",
&pr_number.to_string(),
"--repo",
"denoland/deno",
"--json",
"title,state,headRefName,headRefOid",
"-q",
r#"[.title, .state, .headRefName, .headRefOid] | @tsv"#,
])
.output()
.context("failed to run `gh pr view`")?;
if !pr_info.status.success() {
let stderr = String::from_utf8_lossy(&pr_info.stderr);
bail!("Failed to find PR #{pr_number}: {stderr}");
}
let pr_info_str = String::from_utf8_lossy(&pr_info.stdout);
let pr_fields: Vec<&str> = pr_info_str.trim().splitn(4, '\t').collect();
let pr_title = pr_fields.first().unwrap_or(&"unknown");
let pr_state = pr_fields.get(1).unwrap_or(&"unknown");
let pr_branch = pr_fields.get(2).unwrap_or(&"");
let pr_head_sha = pr_fields.get(3).unwrap_or(&"");
log::info!(
"PR #{}: {} ({})",
pr_number,
colors::bold(pr_title),
pr_state
);
let artifact_name = get_pr_artifact_name()?;
let debug_artifact_name = get_pr_debug_artifact_name()?;
log::info!("{}", colors::gray("Finding CI artifacts..."));
let mut all_run_ids = Vec::new();
if !pr_branch.is_empty() {
let jq_filter = if pr_head_sha.is_empty() {
".[].databaseId".to_string()
} else {
format!(
r#"[.[] | select(.headSha == "{}")] | .[].databaseId"#,
pr_head_sha
)
};
let branch_runs = Command::new("gh")
.args([
"run",
"list",
"--repo",
"denoland/deno",
"--branch",
pr_branch,
"--workflow",
"ci",
"--limit",
"5",
"--json",
"databaseId,headSha",
"-q",
&jq_filter,
])
.output()
.context("failed to query CI runs by branch")?;
if branch_runs.status.success() {
let ids = String::from_utf8_lossy(&branch_runs.stdout);
for id in ids.trim().lines() {
if !id.is_empty() {
all_run_ids.push(id.to_string());
}
}
}
}
if all_run_ids.is_empty() {
bail!(
"No CI runs found for PR #{pr_number}. \
The PR may not have been pushed yet, CI hasn't started, \
or CI hasn't run on the latest commit yet."
);
}
let temp_dir =
tempfile::TempDir::new().context("failed to create temporary directory")?;
let download_dir = temp_dir.path();
let mut downloaded = false;
for run_id in &all_run_ids {
for name in [&artifact_name, &debug_artifact_name] {
log::info!(
"{}",
colors::gray(format!("Trying run {run_id}, artifact \"{name}\"..."))
);
let dl_result = Command::new("gh")
.args([
"run",
"download",
run_id,
"--repo",
"denoland/deno",
"--name",
name,
"--dir",
&download_dir.to_string_lossy(),
])
.output()
.context("failed to run `gh run download`")?;
if dl_result.status.success() {
log::info!(
"Downloaded artifact \"{}\" from run {}",
colors::green(name),
run_id
);
downloaded = true;
break;
}
}
if downloaded {
break;
}
}
if !downloaded {
bail!(
"Could not find a \"{}\" artifact for PR #{pr_number}.\n\
Available artifacts may have expired or CI may not have completed.\n\
Only release builds on linux-x86_64 and debug builds are typically available for PRs.",
artifact_name
);
}
let exe_name = if cfg!(windows) { "deno.exe" } else { "deno" };
let new_exe_path = download_dir.join(exe_name);
if !new_exe_path.exists() {
bail!(
"Downloaded artifact does not contain '{}'. Contents: {:?}",
exe_name,
fs::read_dir(download_dir)?
.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().to_string())
.collect::<Vec<_>>()
);
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&new_exe_path, std::fs::Permissions::from_mode(0o755))?;
}
check_exe(&new_exe_path)?;
if upgrade_flags.dry_run {
log::info!("Upgraded successfully (dry run)");
drop(temp_dir);
return Ok(());
}
let current_exe_path = std::env::current_exe()
.context("failed to get the path of the current executable")?;
let output_exe_path = if let Some(output) = &upgrade_flags.output {
Cow::Owned(PathBuf::from(output))
} else {
Cow::Borrowed(¤t_exe_path)
};
#[cfg(windows)]
kill_running_deno_lsp_processes();
let output_result = if *output_exe_path == current_exe_path {
replace_exe(&new_exe_path, &output_exe_path)
} else {
fs::rename(&new_exe_path, &*output_exe_path)
.or_else(|_| fs::copy(&new_exe_path, &*output_exe_path).map(|_| ()))
};
check_windows_access_denied_error(output_result, &output_exe_path)?;
log::info!(
"\nUpgraded successfully from PR #{} {}\n",
colors::green(&pr_number.to_string()),
colors::gray(&format!("({})", pr_title))
);
drop(temp_dir);
Ok(())
}
fn upgrade_from_branch(
branch: &str,
upgrade_flags: &UpgradeFlags,
) -> Result<(), AnyError> {
let gh_version = Command::new("gh").arg("--version").output();
if gh_version.is_err() {
bail!(
"The `gh` CLI is required for installing from a branch.\n\
Install it from https://cli.github.com/ and run `gh auth login`."
);
}
log::info!(
"{}",
colors::gray(format!("Finding CI artifacts for branch '{branch}'..."))
);
let artifact_name = get_pr_artifact_name()?;
let debug_artifact_name = get_pr_debug_artifact_name()?;
let runs_output = Command::new("gh")
.args([
"run",
"list",
"--repo",
"denoland/deno",
"--branch",
branch,
"--workflow",
"ci",
"--limit",
"5",
"--json",
"databaseId",
"-q",
".[].databaseId",
])
.output()
.context("failed to query CI runs")?;
if !runs_output.status.success() {
let stderr = String::from_utf8_lossy(&runs_output.stderr);
bail!("Failed to find CI runs for branch '{branch}': {stderr}");
}
let run_ids: Vec<String> = String::from_utf8_lossy(&runs_output.stdout)
.trim()
.lines()
.filter(|l| !l.is_empty())
.map(|s| s.to_string())
.collect();
if run_ids.is_empty() {
bail!(
"No CI runs found for branch '{branch}'. \
CI may not have run yet."
);
}
let temp_dir =
tempfile::TempDir::new().context("failed to create temporary directory")?;
let download_dir = temp_dir.path();
let mut downloaded = false;
for run_id in &run_ids {
for name in [&artifact_name, &debug_artifact_name] {
log::info!(
"{}",
colors::gray(format!("Trying run {run_id}, artifact \"{name}\"..."))
);
let dl_result = Command::new("gh")
.args([
"run",
"download",
run_id,
"--repo",
"denoland/deno",
"--name",
name,
"--dir",
&download_dir.to_string_lossy(),
])
.output()
.context("failed to run `gh run download`")?;
if dl_result.status.success() {
log::info!(
"Downloaded artifact \"{}\" from run {}",
colors::green(name),
run_id
);
downloaded = true;
break;
}
}
if downloaded {
break;
}
}
if !downloaded {
bail!(
"Could not find a \"{artifact_name}\" artifact for branch '{branch}'.\n\
Artifacts may have expired or CI may not have completed."
);
}
let exe_name = if cfg!(windows) { "deno.exe" } else { "deno" };
let new_exe_path = download_dir.join(exe_name);
if !new_exe_path.exists() {
bail!("Downloaded artifact does not contain '{exe_name}'.");
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&new_exe_path, std::fs::Permissions::from_mode(0o755))?;
}
check_exe(&new_exe_path)?;
if upgrade_flags.dry_run {
log::info!("Upgraded successfully (dry run)");
drop(temp_dir);
return Ok(());
}
let current_exe_path = std::env::current_exe()
.context("failed to get the path of the current executable")?;
let output_exe_path = if let Some(output) = &upgrade_flags.output {
Cow::Owned(PathBuf::from(output))
} else {
Cow::Borrowed(¤t_exe_path)
};
#[cfg(windows)]
kill_running_deno_lsp_processes();
let output_result = if *output_exe_path == current_exe_path {
replace_exe(&new_exe_path, &output_exe_path)
} else {
fs::rename(&new_exe_path, &*output_exe_path)
.or_else(|_| fs::copy(&new_exe_path, &*output_exe_path).map(|_| ()))
};
check_windows_access_denied_error(output_result, &output_exe_path)?;
log::info!(
"\nUpgraded successfully from branch '{}'\n",
colors::green(branch),
);
drop(temp_dir);
Ok(())
}
pub async fn upgrade(
flags: Arc<Flags>,
upgrade_flags: UpgradeFlags,
) -> Result<(), AnyError> {
if let Some(pr_number) = upgrade_flags.pr {
return upgrade_from_pr(pr_number, &upgrade_flags);
}
if let Some(ref branch) = upgrade_flags.branch {
return upgrade_from_branch(branch, &upgrade_flags);
}
let factory = CliFactory::from_flags(flags);
let cli_options = factory.cli_options()?;
let http_client_provider = factory.http_client_provider();
let client = http_client_provider.get_or_create()?;
let current_exe_path = std::env::current_exe()
.context("failed to get the path of the current executable")?;
let full_path_output_flag = upgrade_flags
.output
.as_ref()
.map(|output| cli_options.initial_cwd().join(output));
let output_exe_path =
full_path_output_flag.as_ref().unwrap_or(¤t_exe_path);
let permissions = set_exe_permissions(¤t_exe_path, output_exe_path)?;
let force_selection_of_new_version =
upgrade_flags.force || full_path_output_flag.is_some();
let requested_version =
RequestedVersion::from_upgrade_flags(upgrade_flags.clone())?;
log::info!("Current Deno version: v{}", version::DENO_VERSION_INFO.deno);
let maybe_selected_version_to_upgrade = match &requested_version {
RequestedVersion::Latest(channel) => {
find_latest_version_to_upgrade(
http_client_provider.clone(),
*channel,
force_selection_of_new_version,
)
.await?
}
RequestedVersion::SpecificVersion(channel, version) => {
select_specific_version_for_upgrade(
*channel,
version.clone(),
force_selection_of_new_version,
)?
}
};
let Some(selected_version_to_upgrade) = maybe_selected_version_to_upgrade
else {
return Ok(());
};
let banner_handle = spawn_banner_task(
&selected_version_to_upgrade.version_or_hash,
selected_version_to_upgrade.release_channel,
http_client_provider.get_or_create()?,
);
let dl_dir = factory.deno_dir()?.dl_folder_path();
let cache_path = get_binary_cache_path(
&dl_dir,
&selected_version_to_upgrade.version_or_hash,
requested_version.release_channel(),
);
let sys = &sys_traits::impls::RealSys;
let archive_data =
if let Some(data) = try_read_cached_binary(sys, &cache_path) {
log::info!(
"{}",
colors::gray(format!(
"Using cached binary from {}",
cache_path.display()
))
);
data
} else {
let download_url = get_download_url(
&selected_version_to_upgrade.version_or_hash,
requested_version.release_channel(),
)?;
log::info!("{}", colors::gray(format!("Downloading {}", &download_url)));
let Some(data) = download_package(&client, download_url).await? else {
log::error!("Download could not be found, aborting");
if requested_version.release_channel() == ReleaseChannel::Canary {
log::error!("Note: canary releases are only kept for 30 days.");
}
deno_runtime::exit(1)
};
store_cached_binary(sys, &cache_path, &data);
data
};
if let Some(expected_checksum) = &upgrade_flags.checksum {
verify_checksum(&archive_data, expected_checksum)?;
}
log::info!(
"{}",
colors::gray(format!(
"Deno is upgrading to version {}",
&selected_version_to_upgrade.version_or_hash
))
);
let temp_dir =
tempfile::TempDir::new().context("failed to create temporary directory")?;
let new_exe_path = archive::unpack_into_dir(archive::UnpackArgs {
exe_name: "deno",
archive_name: &ARCHIVE_NAME,
archive_data: &archive_data,
is_windows: cfg!(windows),
dest_path: temp_dir.path(),
})
.context("failed to extract archive")?;
fs::set_permissions(&new_exe_path, permissions).with_context(|| {
format!("failed to set permissions on '{}'", new_exe_path.display())
})?;
check_exe(&new_exe_path)?;
if upgrade_flags.dry_run {
fs::remove_file(&new_exe_path).with_context(|| {
format!("failed to remove '{}'", new_exe_path.display())
})?;
log::info!("Upgraded successfully (dry run)");
if requested_version.release_channel() == ReleaseChannel::Stable {
print_release_notes(
version::DENO_VERSION_INFO.deno,
&selected_version_to_upgrade.version_or_hash,
&client,
)
.await;
}
drop(temp_dir);
return Ok(());
}
let output_exe_path =
full_path_output_flag.as_ref().unwrap_or(¤t_exe_path);
#[cfg(windows)]
kill_running_deno_lsp_processes();
let output_result = if *output_exe_path == current_exe_path {
replace_exe(&new_exe_path, output_exe_path)
} else {
fs::rename(&new_exe_path, output_exe_path)
.or_else(|_| fs::copy(&new_exe_path, output_exe_path).map(|_| ()))
};
check_windows_access_denied_error(output_result, output_exe_path)?;
log::info!(
"\nUpgraded successfully to Deno {} {}\n",
colors::green(selected_version_to_upgrade.display()),
colors::gray(&format!(
"({})",
selected_version_to_upgrade.release_channel.name()
))
);
if requested_version.release_channel() == ReleaseChannel::Stable {
print_release_notes(
version::DENO_VERSION_INFO.deno,
&selected_version_to_upgrade.version_or_hash,
&client,
)
.await;
}
if let Ok(Some(text)) = banner_handle.await {
log::info!("\n{}\n", text);
}
drop(temp_dir);
if requested_version.release_channel() == ReleaseChannel::Canary {
prune_canary_cache(sys, &dl_dir, 10);
}
Ok(())
}
#[derive(Debug, PartialEq)]
enum RequestedVersion {
Latest(ReleaseChannel),
SpecificVersion(ReleaseChannel, String),
}
impl RequestedVersion {
fn from_upgrade_flags(upgrade_flags: UpgradeFlags) -> Result<Self, AnyError> {
let is_canary = upgrade_flags.canary;
let re_hash = lazy_regex::regex!("^[0-9a-f]{40}$");
let channel = if is_canary {
ReleaseChannel::Canary
} else if upgrade_flags.release_candidate {
ReleaseChannel::Rc
} else {
ReleaseChannel::Stable
};
let mut maybe_passed_version = upgrade_flags.version.clone();
if let Some(val) = &upgrade_flags.version_or_hash_or_channel {
if let Ok(channel) = ReleaseChannel::deserialize(&val.to_lowercase()) {
return Ok(Self::Latest(channel));
} else if re_hash.is_match(val) {
return Ok(Self::SpecificVersion(
ReleaseChannel::Canary,
val.to_string(),
));
} else {
maybe_passed_version = Some(val.to_string());
}
}
let Some(passed_version) = maybe_passed_version else {
return Ok(Self::Latest(channel));
};
let passed_version = passed_version
.strip_prefix('v')
.unwrap_or(&passed_version)
.to_string();
let (channel, passed_version) = if is_canary {
if !re_hash.is_match(&passed_version) {
bail!(
"Invalid commit hash passed ({})\n\nPass a semver, or a full 40 character git commit hash, or a release channel name.\n\nUsage:\n{}",
colors::gray(passed_version),
UPGRADE_USAGE
);
}
(ReleaseChannel::Canary, passed_version)
} else {
let Ok(semver) = Version::parse_standard(&passed_version) else {
bail!(
"Invalid version passed ({})\n\nPass a semver, or a full 40 character git commit hash, or a release channel name.\n\nUsage:\n{}",
colors::gray(passed_version),
UPGRADE_USAGE
);
};
if semver.pre.contains(&SmallStackString::from_static("alpha")) {
(ReleaseChannel::Alpha, passed_version)
} else if semver.pre.contains(&SmallStackString::from_static("beta")) {
(ReleaseChannel::Beta, passed_version)
} else if semver.pre.contains(&SmallStackString::from_static("rc")) {
(ReleaseChannel::Rc, passed_version)
} else {
(ReleaseChannel::Stable, passed_version)
}
};
Ok(RequestedVersion::SpecificVersion(channel, passed_version))
}
pub fn release_channel(&self) -> ReleaseChannel {
match self {
Self::Latest(channel) => *channel,
Self::SpecificVersion(channel, _) => *channel,
}
}
}
fn select_specific_version_for_upgrade(
release_channel: ReleaseChannel,
version: String,
force: bool,
) -> Result<Option<AvailableVersion>, AnyError> {
let current_is_passed = match release_channel {
ReleaseChannel::Stable
| ReleaseChannel::Rc
| ReleaseChannel::Lts
| ReleaseChannel::Alpha
| ReleaseChannel::Beta => {
version::DENO_VERSION_INFO.release_channel == release_channel
&& version::DENO_VERSION_INFO.deno == version
}
ReleaseChannel::Canary => version::DENO_VERSION_INFO.git_hash == version,
};
if !force && current_is_passed {
log::info!(
"Version {} is already installed",
version::DENO_VERSION_INFO.deno
);
return Ok(None);
}
Ok(Some(AvailableVersion {
version_or_hash: version,
release_channel,
}))
}
async fn find_latest_version_to_upgrade(
http_client_provider: Arc<HttpClientProvider>,
release_channel: ReleaseChannel,
force: bool,
) -> Result<Option<AvailableVersion>, AnyError> {
log::info!(
"{}",
colors::gray(&format!("Looking up {} version", release_channel.name()))
);
let client = http_client_provider.get_or_create()?;
let latest_version_found = match fetch_latest_version(
&client,
release_channel,
UpgradeCheckKind::Execution,
)
.await
{
Ok(v) => v,
Err(err) => {
if err.to_string().contains("Not found") {
bail!(
"No {} release available at the moment.",
release_channel.name()
);
} else {
return Err(err);
}
}
};
let current_version = match release_channel {
ReleaseChannel::Canary => version::DENO_VERSION_INFO.git_hash,
ReleaseChannel::Stable
| ReleaseChannel::Lts
| ReleaseChannel::Rc
| ReleaseChannel::Alpha
| ReleaseChannel::Beta => version::DENO_VERSION_INFO.deno,
};
let should_upgrade = force
|| current_version != latest_version_found.version_or_hash
|| version::DENO_VERSION_INFO.release_channel != release_channel;
log::info!("");
if should_upgrade {
log::info!(
"Found latest {} version {}",
latest_version_found.release_channel.name(),
color_print::cformat!("<g>{}</>", latest_version_found.display())
);
} else {
log::info!(
"Local deno version {} is the most recent release",
color_print::cformat!("<g>{}</>", current_version)
);
}
log::info!("");
Ok(should_upgrade.then_some(latest_version_found))
}
#[derive(Debug, Clone, PartialEq)]
struct AvailableVersion {
version_or_hash: String,
release_channel: ReleaseChannel,
}
impl AvailableVersion {
fn display(&self) -> Cow<'_, str> {
match self.release_channel {
ReleaseChannel::Canary => Cow::Borrowed(&self.version_or_hash),
_ => Cow::Owned(format!("v{}", self.version_or_hash)),
}
}
}
async fn fetch_latest_version(
client: &HttpClient,
release_channel: ReleaseChannel,
check_kind: UpgradeCheckKind,
) -> Result<AvailableVersion, AnyError> {
let url = get_latest_version_url(release_channel, env!("TARGET"), check_kind);
let text = client.download_text(url.parse()?).await?;
let version = normalize_version_from_server(release_channel, &text)?;
Ok(version)
}
fn normalize_version_from_server(
release_channel: ReleaseChannel,
text: &str,
) -> Result<AvailableVersion, AnyError> {
let text = text.trim();
match release_channel {
ReleaseChannel::Stable
| ReleaseChannel::Rc
| ReleaseChannel::Lts
| ReleaseChannel::Alpha
| ReleaseChannel::Beta => {
let v = text.trim_start_matches('v').to_string();
Ok(AvailableVersion {
version_or_hash: v.to_string(),
release_channel,
})
}
ReleaseChannel::Canary => Ok(AvailableVersion {
version_or_hash: text.to_string(),
release_channel,
}),
}
}
fn get_latest_version_url(
release_channel: ReleaseChannel,
target_tuple: &str,
check_kind: UpgradeCheckKind,
) -> String {
let file_name = match release_channel {
ReleaseChannel::Stable => Cow::Borrowed("release-latest.txt"),
ReleaseChannel::Canary => {
Cow::Owned(format!("canary-{target_tuple}-latest.txt"))
}
ReleaseChannel::Rc => Cow::Borrowed("release-rc-latest.txt"),
ReleaseChannel::Lts => Cow::Borrowed("release-lts-latest.txt"),
ReleaseChannel::Alpha => Cow::Borrowed("release-alpha-latest.txt"),
ReleaseChannel::Beta => Cow::Borrowed("release-beta-latest.txt"),
};
let query_param = match check_kind {
UpgradeCheckKind::Execution => "",
UpgradeCheckKind::Lsp => "?lsp",
};
format!("{}/{}{}", base_upgrade_url(), file_name, query_param)
}
fn base_upgrade_url() -> Cow<'static, str> {
if let Ok(url) = env::var("DENO_DONT_USE_INTERNAL_BASE_UPGRADE_URL") {
Cow::Owned(url)
} else {
Cow::Borrowed("https://dl.deno.land")
}
}
fn get_download_url(
version: &str,
release_channel: ReleaseChannel,
) -> Result<Url, AnyError> {
let download_url = match release_channel {
ReleaseChannel::Stable | ReleaseChannel::Alpha | ReleaseChannel::Beta => {
let release_url = if std::env::var_os("DENO_TESTING_UPGRADE").is_some() {
"http://localhost:4545/deno-upgrade"
} else {
RELEASE_URL
};
format!("{}/download/v{}/{}", release_url, version, *ARCHIVE_NAME)
}
ReleaseChannel::Rc => {
format!("{}/v{}/{}", DL_RELEASE_URL, version, *ARCHIVE_NAME)
}
ReleaseChannel::Canary => {
format!("{}/{}/{}", CANARY_URL, version, *ARCHIVE_NAME)
}
ReleaseChannel::Lts => {
format!("{}/v{}/{}", DL_RELEASE_URL, version, *ARCHIVE_NAME)
}
};
Url::parse(&download_url).with_context(|| {
format!(
"Failed to parse URL to download new release: {}",
download_url
)
})
}
fn spawn_banner_task(
version: &str,
release_channel: ReleaseChannel,
client: HttpClient,
) -> deno_core::unsync::JoinHandle<Option<String>> {
let banner_url = get_banner_url(version, release_channel);
deno_core::unsync::spawn(async move {
let banner_url = banner_url?;
tokio::select! {
result = client.download_text(banner_url) => {
result.ok()
}
_ = tokio::time::sleep(Duration::from_secs(5)) => {
None
}
}
})
}
fn get_banner_url(
version: &str,
release_channel: ReleaseChannel,
) -> Option<Url> {
let download_url = match release_channel {
ReleaseChannel::Stable => {
format!("{}/v{}/banner.txt", DL_RELEASE_URL, version)
}
ReleaseChannel::Rc
| ReleaseChannel::Lts
| ReleaseChannel::Canary
| ReleaseChannel::Alpha
| ReleaseChannel::Beta => {
return None;
}
};
Url::parse(&download_url).ok()
}
async fn download_package(
client: &HttpClient,
download_url: Url,
) -> Result<Option<Vec<u8>>, AnyError> {
let progress_bar = ProgressBar::new(ProgressBarStyle::DownloadBars);
let progress = progress_bar.update("");
let response = client
.download_with_progress_and_retries(download_url.clone(), &Default::default(), &progress)
.await
.with_context(|| format!("Failed downloading {download_url}. The version you requested may not have been built for the current architecture."))?;
Ok(response.into_maybe_bytes()?)
}
fn verify_checksum(
data: &[u8],
expected_checksum: &str,
) -> Result<(), AnyError> {
let computed = sha2::Sha256::digest(data);
let computed_hex = faster_hex::hex_string(&computed);
let expected_checksum = expected_checksum.trim().to_lowercase();
if computed_hex != expected_checksum {
bail!(
"Checksum verification failed.\n Actual: {}\n Expected: {}",
expected_checksum,
computed_hex
);
}
log::info!("{}", colors::gray("Checksum verified"));
Ok(())
}
fn replace_exe(from: &Path, to: &Path) -> Result<(), std::io::Error> {
if cfg!(windows) {
fs::rename(to, to.with_extension("old.exe"))?;
} else {
fs::remove_file(to)?;
}
fs::rename(from, to).or_else(|_| fs::copy(from, to).map(|_| ()))?;
Ok(())
}
fn check_windows_access_denied_error(
output_result: Result<(), std::io::Error>,
output_exe_path: &Path,
) -> Result<(), AnyError> {
let Err(err) = output_result else {
return Ok(());
};
if !cfg!(windows) {
return Err(err).with_context(|| {
format!(
"failed to replace the executable at '{}'",
output_exe_path.display()
)
});
}
const WIN_ERROR_ACCESS_DENIED: i32 = 5;
if err.raw_os_error() != Some(WIN_ERROR_ACCESS_DENIED) {
return Err(err.into());
};
Err(err).with_context(|| {
format!(
concat!(
"Could not replace the deno executable. This may be because an ",
"existing deno process is running. Please ensure there are no ",
"running deno processes (ex. Stop-Process -Name deno ; deno {}), ",
"close any editors before upgrading, and ensure you have ",
"sufficient permission to '{}'."
),
std::env::args().skip(1).collect::<Vec<_>>().join(" "),
output_exe_path.display(),
)
})
}
#[cfg(windows)]
fn kill_running_deno_lsp_processes() {
let is_debug = log::log_enabled!(log::Level::Debug);
let get_pipe = || {
if is_debug {
std::process::Stdio::inherit()
} else {
std::process::Stdio::null()
}
};
let _ = Command::new("powershell.exe")
.args([
"-Command",
r#"Get-WmiObject Win32_Process | Where-Object {
$_.Name -eq 'deno.exe' -and
$_.CommandLine -match '^(?:\"[^\"]+\"|\S+)\s+lsp\b'
} | ForEach-Object {
if ($_.Terminate()) {
Write-Host 'Terminated:' $_.ProcessId
}
}"#,
])
.stdout(get_pipe())
.stderr(get_pipe())
.output();
}
fn set_exe_permissions(
current_exe_path: &Path,
output_exe_path: &Path,
) -> Result<std::fs::Permissions, AnyError> {
let Ok(metadata) = fs::metadata(output_exe_path) else {
let metadata = fs::metadata(current_exe_path).with_context(|| {
format!(
"failed to get metadata of the current executable at '{}'",
current_exe_path.display()
)
})?;
return Ok(metadata.permissions());
};
let permissions = metadata.permissions();
if permissions.readonly() {
bail!(
"You do not have write permission to {}",
output_exe_path.display()
);
}
#[cfg(unix)]
if std::os::unix::fs::MetadataExt::uid(&metadata) == 0
&& !nix::unistd::Uid::effective().is_root()
{
bail!(
concat!(
"You don't have write permission to {} because it's owned by root.\n",
"Consider updating deno through your package manager if its installed from it.\n",
"Otherwise run `deno upgrade` as root.",
),
output_exe_path.display()
);
}
Ok(permissions)
}
fn check_exe(exe_path: &Path) -> Result<(), AnyError> {
let output = Command::new(exe_path)
.arg("-V")
.stderr(std::process::Stdio::inherit())
.output()
.with_context(|| format!("failed to run '{}'", exe_path.display()))?;
if !output.status.success() {
bail!(
"Failed to validate Deno executable. This may be because your OS is unsupported or the executable is corrupted"
)
} else {
Ok(())
}
}
#[derive(Debug)]
struct CheckVersionFile {
pub last_prompt: chrono::DateTime<chrono::Utc>,
pub last_checked: chrono::DateTime<chrono::Utc>,
pub current_version: String,
pub latest_version: String,
pub current_release_channel: ReleaseChannel,
}
impl CheckVersionFile {
pub fn parse(content: String) -> Option<Self> {
let split_content = content.split('!').collect::<Vec<_>>();
if split_content.len() != 5 {
return None;
}
let latest_version = split_content[2].trim().to_owned();
if latest_version.is_empty() {
return None;
}
let current_version = split_content[3].trim().to_owned();
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))
.ok()?;
let last_checked = chrono::DateTime::parse_from_rfc3339(split_content[1])
.map(|dt| dt.with_timezone(&chrono::Utc))
.ok()?;
Some(CheckVersionFile {
last_prompt,
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()
)
}
fn with_last_prompt(self, dt: chrono::DateTime<chrono::Utc>) -> Self {
Self {
last_prompt: dt,
..self
}
}
}
#[cfg(test)]
mod test {
use std::cell::RefCell;
use std::rc::Rc;
use test_util::assert_contains;
use super::*;
#[test]
fn test_get_pr_artifact_name() {
let name = get_pr_artifact_name().unwrap();
assert!(
name.starts_with("release-"),
"artifact name should start with 'release-': {name}"
);
assert!(
name.ends_with("-deno"),
"artifact name should end with '-deno': {name}"
);
assert!(
name.contains("linux")
|| name.contains("macos")
|| name.contains("windows"),
"artifact name should contain os: {name}"
);
assert!(
name.contains("x86_64") || name.contains("aarch64"),
"artifact name should contain arch: {name}"
);
}
#[test]
fn test_get_pr_debug_artifact_name() {
let release_name = get_pr_artifact_name().unwrap();
let debug_name = get_pr_debug_artifact_name().unwrap();
assert!(
debug_name.starts_with("debug-"),
"debug artifact name should start with 'debug-': {debug_name}"
);
assert_eq!(
release_name.strip_prefix("release-"),
debug_name.strip_prefix("debug-"),
);
}
#[test]
fn test_requested_version() {
let mut upgrade_flags = UpgradeFlags {
dry_run: false,
force: false,
release_candidate: false,
canary: false,
version: None,
output: None,
version_or_hash_or_channel: None,
checksum: None,
pr: None,
branch: None,
};
let req_ver =
RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap();
assert_eq!(req_ver, RequestedVersion::Latest(ReleaseChannel::Stable));
upgrade_flags.version = Some("1.46.0".to_string());
let req_ver =
RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap();
assert_eq!(
req_ver,
RequestedVersion::SpecificVersion(
ReleaseChannel::Stable,
"1.46.0".to_string()
)
);
upgrade_flags.version = None;
upgrade_flags.canary = true;
let req_ver =
RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap();
assert_eq!(req_ver, RequestedVersion::Latest(ReleaseChannel::Canary));
upgrade_flags.version =
Some("5c69b4861b52ab406e73b9cd85c254f0505cb20f".to_string());
let req_ver =
RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap();
assert_eq!(
req_ver,
RequestedVersion::SpecificVersion(
ReleaseChannel::Canary,
"5c69b4861b52ab406e73b9cd85c254f0505cb20f".to_string()
)
);
upgrade_flags.version = None;
upgrade_flags.canary = false;
upgrade_flags.release_candidate = true;
let req_ver =
RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap();
assert_eq!(req_ver, RequestedVersion::Latest(ReleaseChannel::Rc));
upgrade_flags.release_candidate = false;
upgrade_flags.version_or_hash_or_channel = Some("v1.46.5".to_string());
let req_ver =
RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap();
assert_eq!(
req_ver,
RequestedVersion::SpecificVersion(
ReleaseChannel::Stable,
"1.46.5".to_string()
)
);
upgrade_flags.version_or_hash_or_channel = Some("2.0.0-rc.0".to_string());
let req_ver =
RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap();
assert_eq!(
req_ver,
RequestedVersion::SpecificVersion(
ReleaseChannel::Rc,
"2.0.0-rc.0".to_string()
)
);
upgrade_flags.version_or_hash_or_channel = Some("canary".to_string());
let req_ver =
RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap();
assert_eq!(req_ver, RequestedVersion::Latest(ReleaseChannel::Canary,));
upgrade_flags.version_or_hash_or_channel = Some("rc".to_string());
let req_ver =
RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap();
assert_eq!(req_ver, RequestedVersion::Latest(ReleaseChannel::Rc,));
upgrade_flags.version_or_hash_or_channel = Some("alpha".to_string());
let req_ver =
RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap();
assert_eq!(req_ver, RequestedVersion::Latest(ReleaseChannel::Alpha));
upgrade_flags.version_or_hash_or_channel = Some("beta".to_string());
let req_ver =
RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap();
assert_eq!(req_ver, RequestedVersion::Latest(ReleaseChannel::Beta));
upgrade_flags.version_or_hash_or_channel =
Some("2.8.0-alpha.0".to_string());
let req_ver =
RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap();
assert_eq!(
req_ver,
RequestedVersion::SpecificVersion(
ReleaseChannel::Alpha,
"2.8.0-alpha.0".to_string()
)
);
upgrade_flags.version_or_hash_or_channel = Some("2.8.0-beta.1".to_string());
let req_ver =
RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap();
assert_eq!(
req_ver,
RequestedVersion::SpecificVersion(
ReleaseChannel::Beta,
"2.8.0-beta.1".to_string()
)
);
upgrade_flags.version_or_hash_or_channel =
Some("5c69b4861b52ab406e73b9cd85c254f0505cb20f".to_string());
let req_ver =
RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap();
assert_eq!(
req_ver,
RequestedVersion::SpecificVersion(
ReleaseChannel::Canary,
"5c69b4861b52ab406e73b9cd85c254f0505cb20f".to_string()
)
);
upgrade_flags.version_or_hash_or_channel =
Some("5c69b4861b52a".to_string());
let err = RequestedVersion::from_upgrade_flags(upgrade_flags.clone())
.unwrap_err()
.to_string();
assert_contains!(err, "Invalid version passed");
assert_contains!(
err,
"Pass a semver, or a full 40 character git commit hash, or a release channel name."
);
upgrade_flags.version_or_hash_or_channel = Some("11.asd.1324".to_string());
let err = RequestedVersion::from_upgrade_flags(upgrade_flags.clone())
.unwrap_err()
.to_string();
assert_contains!(err, "Invalid version passed");
assert_contains!(
err,
"Pass a semver, or a full 40 character git commit hash, or a release channel name."
);
}
#[test]
fn test_parse_upgrade_check_file() {
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());
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!(
file.last_prompt.to_rfc3339(),
"2020-01-01T00:00:00+00:00".to_string()
);
assert_eq!(
file.last_checked.to_rfc3339(),
"2020-01-01T00:00:00+00:00".to_string()
);
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());
assert!(result.is_none());
let result = CheckVersionFile::parse("garbage!test".to_string());
assert!(result.is_none());
let result = CheckVersionFile::parse("test".to_string());
assert!(result.is_none());
}
#[test]
fn test_serialize_upgrade_check_file() {
let mut file = CheckVersionFile {
last_prompt: chrono::DateTime::parse_from_rfc3339("2020-01-01T00:00:00Z")
.unwrap()
.with_timezone(&chrono::Utc),
last_checked: chrono::DateTime::parse_from_rfc3339(
"2020-01-01T00:00:00Z",
)
.unwrap()
.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!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"
);
file.current_release_channel = ReleaseChannel::Alpha;
assert_eq!(
file.serialize(),
"2020-01-01T00:00:00+00:00!2020-01-01T00:00:00+00:00!1.2.3!1.2.2!alpha"
);
file.current_release_channel = ReleaseChannel::Beta;
assert_eq!(
file.serialize(),
"2020-01-01T00:00:00+00:00!2020-01-01T00:00:00+00:00!1.2.3!1.2.2!beta"
);
}
#[derive(Clone)]
struct TestUpdateCheckerEnvironment {
file_text: Rc<RefCell<String>>,
release_channel: Rc<RefCell<ReleaseChannel>>,
current_version: Rc<RefCell<String>>,
latest_version: Rc<RefCell<Result<AvailableVersion, String>>>,
time: Rc<RefCell<chrono::DateTime<chrono::Utc>>>,
}
impl TestUpdateCheckerEnvironment {
pub fn new() -> Self {
Self {
file_text: Default::default(),
current_version: Default::default(),
release_channel: Rc::new(RefCell::new(ReleaseChannel::Stable)),
latest_version: Rc::new(RefCell::new(Ok(AvailableVersion {
version_or_hash: "".to_string(),
release_channel: ReleaseChannel::Stable,
}))),
time: Rc::new(RefCell::new(chrono::Utc::now())),
}
}
pub fn add_hours(&self, hours: i64) {
let mut time = self.time.borrow_mut();
*time = time
.checked_add_signed(chrono::Duration::hours(hours))
.unwrap();
}
pub fn set_file_text(&self, text: &str) {
*self.file_text.borrow_mut() = text.to_string();
}
pub fn set_current_version(&self, version: &str) {
*self.current_version.borrow_mut() = version.to_string();
}
pub fn set_latest_version(
&self,
version: &str,
release_channel: ReleaseChannel,
) {
*self.latest_version.borrow_mut() = Ok(AvailableVersion {
version_or_hash: version.to_string(),
release_channel,
});
}
pub fn set_latest_version_err(&self, err: &str) {
*self.latest_version.borrow_mut() = Err(err.to_string());
}
pub fn set_release_channel(&self, channel: ReleaseChannel) {
*self.release_channel.borrow_mut() = channel;
}
}
#[async_trait(?Send)]
impl VersionProvider for TestUpdateCheckerEnvironment {
async fn latest_version(
&self,
_release_channel: ReleaseChannel,
) -> Result<AvailableVersion, AnyError> {
match self.latest_version.borrow().clone() {
Ok(result) => Ok(result),
Err(err) => bail!("{}", err),
}
}
fn current_version(&self) -> Cow<'_, str> {
Cow::Owned(self.current_version.borrow().clone())
}
fn get_current_exe_release_channel(&self) -> ReleaseChannel {
*self.release_channel.borrow()
}
}
impl UpdateCheckerEnvironment for TestUpdateCheckerEnvironment {
fn read_check_file(&self) -> String {
self.file_text.borrow().clone()
}
fn write_check_file(&self, text: &str) {
self.set_file_text(text);
}
fn current_time(&self) -> chrono::DateTime<chrono::Utc> {
*self.time.borrow()
}
}
#[tokio::test]
async fn test_update_checker() {
let env = TestUpdateCheckerEnvironment::new();
env.set_current_version("1.0.0");
env.set_latest_version("1.1.0", ReleaseChannel::Stable);
let checker = UpdateChecker::new(env.clone(), env.clone());
assert!(checker.should_check_for_new_version());
assert_eq!(checker.should_prompt(), None);
fetch_and_store_latest_version(&env, &env).await;
let checker = UpdateChecker::new(env.clone(), env.clone());
assert!(!checker.should_check_for_new_version());
assert_eq!(
checker.should_prompt(),
Some((ReleaseChannel::Stable, "1.1.0".to_string()))
);
env.add_hours(1);
env.set_latest_version("1.2.0", ReleaseChannel::Stable);
assert!(!checker.should_check_for_new_version());
assert_eq!(
checker.should_prompt(),
Some((ReleaseChannel::Stable, "1.1.0".to_string()))
);
env.add_hours(UPGRADE_CHECK_INTERVAL);
assert!(checker.should_check_for_new_version());
assert_eq!(
checker.should_prompt(),
Some((ReleaseChannel::Stable, "1.1.0".to_string()))
);
fetch_and_store_latest_version(&env, &env).await;
let checker = UpdateChecker::new(env.clone(), env.clone());
assert!(!checker.should_check_for_new_version());
assert_eq!(
checker.should_prompt(),
Some((ReleaseChannel::Stable, "1.2.0".to_string()))
);
checker.store_prompted();
let checker = UpdateChecker::new(env.clone(), env.clone());
assert!(!checker.should_check_for_new_version());
assert_eq!(checker.should_prompt(), None);
env.add_hours(UPGRADE_CHECK_INTERVAL + 1);
assert!(checker.should_check_for_new_version());
assert_eq!(
checker.should_prompt(),
Some((ReleaseChannel::Stable, "1.2.0".to_string()))
);
env.set_current_version("1.2.0");
assert!(checker.should_check_for_new_version());
assert_eq!(checker.should_prompt(), None);
env.add_hours(UPGRADE_CHECK_INTERVAL + 1);
env.set_latest_version_err("Failed");
env.set_latest_version("1.3.0", ReleaseChannel::Stable);
fetch_and_store_latest_version(&env, &env).await;
assert!(checker.should_check_for_new_version());
assert_eq!(checker.should_prompt(), None);
env.set_release_channel(ReleaseChannel::Rc);
env.set_current_version("1.46.0-rc.0");
env.set_latest_version("1.46.0-rc.1", ReleaseChannel::Rc);
fetch_and_store_latest_version(&env, &env).await;
env.add_hours(UPGRADE_CHECK_INTERVAL + 1);
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()))
);
env.set_release_channel(ReleaseChannel::Alpha);
env.set_current_version("2.8.0-alpha.0");
env.set_latest_version("2.8.0-alpha.1", ReleaseChannel::Alpha);
fetch_and_store_latest_version(&env, &env).await;
env.add_hours(UPGRADE_CHECK_INTERVAL + 1);
let checker = UpdateChecker::new(env.clone(), env.clone());
assert!(checker.should_check_for_new_version());
assert_eq!(
checker.should_prompt(),
Some((ReleaseChannel::Alpha, "2.8.0-alpha.1".to_string()))
);
env.set_release_channel(ReleaseChannel::Beta);
env.set_current_version("2.8.0-beta.0");
env.set_latest_version("2.8.0-beta.1", ReleaseChannel::Beta);
fetch_and_store_latest_version(&env, &env).await;
env.add_hours(UPGRADE_CHECK_INTERVAL + 1);
let checker = UpdateChecker::new(env.clone(), env.clone());
assert!(checker.should_check_for_new_version());
assert_eq!(
checker.should_prompt(),
Some((ReleaseChannel::Beta, "2.8.0-beta.1".to_string()))
);
}
#[tokio::test]
async fn test_update_checker_current_newer_than_latest() {
let env = TestUpdateCheckerEnvironment::new();
let file_content = CheckVersionFile {
last_prompt: env
.current_time()
.sub(chrono::Duration::hours(UPGRADE_CHECK_INTERVAL + 1)),
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);
env.set_current_version("1.27.0");
env.set_latest_version("1.26.2", ReleaseChannel::Stable);
let checker = UpdateChecker::new(env.clone(), env);
assert_eq!(checker.should_prompt(), None);
}
#[tokio::test]
async fn test_should_not_prompt_if_current_cli_version_has_changed() {
let env = TestUpdateCheckerEnvironment::new();
let file_content = CheckVersionFile {
last_prompt: env
.current_time()
.sub(chrono::Duration::hours(UPGRADE_CHECK_INTERVAL + 1)),
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);
env.set_current_version("61fbfabe440f1cfffa7b8d17426ffdece4d430d0");
let checker = UpdateChecker::new(env.clone(), env);
assert_eq!(checker.should_prompt(), None);
}
#[test]
fn test_get_latest_version_url() {
assert_eq!(
get_latest_version_url(
ReleaseChannel::Canary,
"aarch64-apple-darwin",
UpgradeCheckKind::Execution
),
"https://dl.deno.land/canary-aarch64-apple-darwin-latest.txt"
);
assert_eq!(
get_latest_version_url(
ReleaseChannel::Canary,
"aarch64-apple-darwin",
UpgradeCheckKind::Lsp
),
"https://dl.deno.land/canary-aarch64-apple-darwin-latest.txt?lsp"
);
assert_eq!(
get_latest_version_url(
ReleaseChannel::Canary,
"x86_64-pc-windows-msvc",
UpgradeCheckKind::Execution
),
"https://dl.deno.land/canary-x86_64-pc-windows-msvc-latest.txt"
);
assert_eq!(
get_latest_version_url(
ReleaseChannel::Canary,
"x86_64-pc-windows-msvc",
UpgradeCheckKind::Lsp
),
"https://dl.deno.land/canary-x86_64-pc-windows-msvc-latest.txt?lsp"
);
assert_eq!(
get_latest_version_url(
ReleaseChannel::Stable,
"aarch64-apple-darwin",
UpgradeCheckKind::Execution
),
"https://dl.deno.land/release-latest.txt"
);
assert_eq!(
get_latest_version_url(
ReleaseChannel::Stable,
"aarch64-apple-darwin",
UpgradeCheckKind::Lsp
),
"https://dl.deno.land/release-latest.txt?lsp"
);
assert_eq!(
get_latest_version_url(
ReleaseChannel::Stable,
"x86_64-pc-windows-msvc",
UpgradeCheckKind::Execution
),
"https://dl.deno.land/release-latest.txt"
);
assert_eq!(
get_latest_version_url(
ReleaseChannel::Rc,
"x86_64-pc-windows-msvc",
UpgradeCheckKind::Lsp
),
"https://dl.deno.land/release-rc-latest.txt?lsp"
);
assert_eq!(
get_latest_version_url(
ReleaseChannel::Rc,
"aarch64-apple-darwin",
UpgradeCheckKind::Execution
),
"https://dl.deno.land/release-rc-latest.txt"
);
assert_eq!(
get_latest_version_url(
ReleaseChannel::Rc,
"aarch64-apple-darwin",
UpgradeCheckKind::Lsp
),
"https://dl.deno.land/release-rc-latest.txt?lsp"
);
assert_eq!(
get_latest_version_url(
ReleaseChannel::Rc,
"x86_64-pc-windows-msvc",
UpgradeCheckKind::Execution
),
"https://dl.deno.land/release-rc-latest.txt"
);
assert_eq!(
get_latest_version_url(
ReleaseChannel::Rc,
"x86_64-pc-windows-msvc",
UpgradeCheckKind::Lsp
),
"https://dl.deno.land/release-rc-latest.txt?lsp"
);
assert_eq!(
get_latest_version_url(
ReleaseChannel::Lts,
"x86_64-pc-windows-msvc",
UpgradeCheckKind::Lsp
),
"https://dl.deno.land/release-lts-latest.txt?lsp"
);
assert_eq!(
get_latest_version_url(
ReleaseChannel::Lts,
"aarch64-apple-darwin",
UpgradeCheckKind::Execution
),
"https://dl.deno.land/release-lts-latest.txt"
);
assert_eq!(
get_latest_version_url(
ReleaseChannel::Lts,
"aarch64-apple-darwin",
UpgradeCheckKind::Lsp
),
"https://dl.deno.land/release-lts-latest.txt?lsp"
);
assert_eq!(
get_latest_version_url(
ReleaseChannel::Lts,
"x86_64-pc-windows-msvc",
UpgradeCheckKind::Execution
),
"https://dl.deno.land/release-lts-latest.txt"
);
assert_eq!(
get_latest_version_url(
ReleaseChannel::Lts,
"x86_64-pc-windows-msvc",
UpgradeCheckKind::Lsp
),
"https://dl.deno.land/release-lts-latest.txt?lsp"
);
assert_eq!(
get_latest_version_url(
ReleaseChannel::Alpha,
"aarch64-apple-darwin",
UpgradeCheckKind::Execution
),
"https://dl.deno.land/release-alpha-latest.txt"
);
assert_eq!(
get_latest_version_url(
ReleaseChannel::Alpha,
"x86_64-pc-windows-msvc",
UpgradeCheckKind::Lsp
),
"https://dl.deno.land/release-alpha-latest.txt?lsp"
);
assert_eq!(
get_latest_version_url(
ReleaseChannel::Beta,
"aarch64-apple-darwin",
UpgradeCheckKind::Execution
),
"https://dl.deno.land/release-beta-latest.txt"
);
assert_eq!(
get_latest_version_url(
ReleaseChannel::Beta,
"x86_64-pc-windows-msvc",
UpgradeCheckKind::Lsp
),
"https://dl.deno.land/release-beta-latest.txt?lsp"
);
}
#[test]
fn test_normalize_version_server() {
assert_eq!(
normalize_version_from_server(ReleaseChannel::Stable, "v1.0.0").unwrap(),
AvailableVersion {
version_or_hash: "1.0.0".to_string(),
release_channel: ReleaseChannel::Stable,
},
);
assert_eq!(
normalize_version_from_server(
ReleaseChannel::Stable,
" v1.0.0-test-v\n\n "
)
.unwrap(),
AvailableVersion {
version_or_hash: "1.0.0-test-v".to_string(),
release_channel: ReleaseChannel::Stable,
}
);
assert_eq!(
normalize_version_from_server(
ReleaseChannel::Canary,
" v1452345asdf \n\n "
)
.unwrap(),
AvailableVersion {
version_or_hash: "v1452345asdf".to_string(),
release_channel: ReleaseChannel::Canary,
}
);
assert_eq!(
normalize_version_from_server(ReleaseChannel::Rc, "v1.46.0-rc.0\n\n")
.unwrap(),
AvailableVersion {
version_or_hash: "1.46.0-rc.0".to_string(),
release_channel: ReleaseChannel::Rc,
},
);
assert_eq!(
normalize_version_from_server(
ReleaseChannel::Alpha,
"v2.8.0-alpha.0\n\n"
)
.unwrap(),
AvailableVersion {
version_or_hash: "2.8.0-alpha.0".to_string(),
release_channel: ReleaseChannel::Alpha,
},
);
assert_eq!(
normalize_version_from_server(ReleaseChannel::Beta, "v2.8.0-beta.1\n\n")
.unwrap(),
AvailableVersion {
version_or_hash: "2.8.0-beta.1".to_string(),
release_channel: ReleaseChannel::Beta,
},
);
}
#[tokio::test]
async fn test_upgrades_lsp() {
let env = TestUpdateCheckerEnvironment::new();
env.set_current_version("1.0.0");
env.set_latest_version("2.0.0", ReleaseChannel::Stable);
{
let maybe_info = check_for_upgrades_for_lsp_with_provider(&env)
.await
.unwrap();
assert_eq!(
maybe_info,
Some(LspVersionUpgradeInfo {
latest_version: "2.0.0".to_string(),
is_canary: false,
})
);
}
{
env.set_latest_version("1.0.0", ReleaseChannel::Stable);
let maybe_info = check_for_upgrades_for_lsp_with_provider(&env)
.await
.unwrap();
assert_eq!(maybe_info, None);
}
{
env.set_latest_version("0.9.0", ReleaseChannel::Stable);
let maybe_info = check_for_upgrades_for_lsp_with_provider(&env)
.await
.unwrap();
assert_eq!(maybe_info, None);
}
{
env.set_current_version("123");
env.set_latest_version("123", ReleaseChannel::Stable);
env.set_release_channel(ReleaseChannel::Canary);
let maybe_info = check_for_upgrades_for_lsp_with_provider(&env)
.await
.unwrap();
assert_eq!(maybe_info, None);
}
{
env.set_latest_version("1234", ReleaseChannel::Stable);
let maybe_info = check_for_upgrades_for_lsp_with_provider(&env)
.await
.unwrap();
assert_eq!(
maybe_info,
Some(LspVersionUpgradeInfo {
latest_version: "1234".to_string(),
is_canary: true,
})
);
}
{
env.set_release_channel(ReleaseChannel::Rc);
env.set_current_version("1.2.3-rc.0");
env.set_latest_version("1.2.3-rc.0", ReleaseChannel::Rc);
let maybe_info = check_for_upgrades_for_lsp_with_provider(&env)
.await
.unwrap();
assert_eq!(maybe_info, None);
}
{
env.set_latest_version("1.2.3-rc.0", ReleaseChannel::Rc);
env.set_latest_version("1.2.3-rc.1", ReleaseChannel::Rc);
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: false,
})
);
}
{
env.set_release_channel(ReleaseChannel::Alpha);
env.set_current_version("2.8.0-alpha.0");
env.set_latest_version("2.8.0-alpha.0", ReleaseChannel::Alpha);
let maybe_info = check_for_upgrades_for_lsp_with_provider(&env)
.await
.unwrap();
assert_eq!(maybe_info, None);
}
{
env.set_latest_version("2.8.0-alpha.1", ReleaseChannel::Alpha);
let maybe_info = check_for_upgrades_for_lsp_with_provider(&env)
.await
.unwrap();
assert_eq!(
maybe_info,
Some(LspVersionUpgradeInfo {
latest_version: "2.8.0-alpha.1".to_string(),
is_canary: false,
})
);
}
{
env.set_release_channel(ReleaseChannel::Beta);
env.set_current_version("2.8.0-beta.0");
env.set_latest_version("2.8.0-beta.0", ReleaseChannel::Beta);
let maybe_info = check_for_upgrades_for_lsp_with_provider(&env)
.await
.unwrap();
assert_eq!(maybe_info, None);
}
{
env.set_latest_version("2.8.0-beta.1", ReleaseChannel::Beta);
let maybe_info = check_for_upgrades_for_lsp_with_provider(&env)
.await
.unwrap();
assert_eq!(
maybe_info,
Some(LspVersionUpgradeInfo {
latest_version: "2.8.0-beta.1".to_string(),
is_canary: false,
})
);
}
}
#[test]
fn blog_post_links() {
let version = Version::parse_standard("1.46.0").unwrap();
assert_eq!(
get_minor_version_blog_post_url(&version),
"https://deno.com/blog/v1.46"
);
let version = Version::parse_standard("2.1.1").unwrap();
assert_eq!(
get_minor_version_blog_post_url(&version),
"https://deno.com/blog/v2.1"
);
let version = Version::parse_standard("2.0.0-rc.0").unwrap();
assert_eq!(
get_rc_version_blog_post_url(&version),
"https://deno.com/blog/v2.0-rc-0"
);
let version = Version::parse_standard("2.0.0-rc.2").unwrap();
assert_eq!(
get_rc_version_blog_post_url(&version),
"https://deno.com/blog/v2.0-rc-2"
);
}
#[test]
fn test_get_binary_cache_path() {
let dl_dir = Path::new("/dl");
let path = get_binary_cache_path(dl_dir, "1.46.0", ReleaseChannel::Stable);
assert_eq!(
path,
dl_dir.join(format!("release/v1.46.0/{}", *ARCHIVE_NAME))
);
let path = get_binary_cache_path(dl_dir, "1.46.0-rc.0", ReleaseChannel::Rc);
assert_eq!(
path,
dl_dir.join(format!("release/v1.46.0-rc.0/{}", *ARCHIVE_NAME))
);
let path = get_binary_cache_path(dl_dir, "1.0.0", ReleaseChannel::Lts);
assert_eq!(
path,
dl_dir.join(format!("release/v1.0.0/{}", *ARCHIVE_NAME))
);
let path =
get_binary_cache_path(dl_dir, "abc123def456", ReleaseChannel::Canary);
assert_eq!(
path,
dl_dir.join(format!("canary/abc123def456/{}", *ARCHIVE_NAME))
);
let path =
get_binary_cache_path(dl_dir, "2.8.0-alpha.0", ReleaseChannel::Alpha);
assert_eq!(
path,
dl_dir.join(format!("release/v2.8.0-alpha.0/{}", *ARCHIVE_NAME))
);
let path =
get_binary_cache_path(dl_dir, "2.8.0-beta.1", ReleaseChannel::Beta);
assert_eq!(
path,
dl_dir.join(format!("release/v2.8.0-beta.1/{}", *ARCHIVE_NAME))
);
}
#[test]
fn test_try_read_cached_binary() {
use sys_traits::FsCreateDirAll;
use sys_traits::FsWrite;
use sys_traits::impls::InMemorySys;
let sys = InMemorySys::default();
let result =
try_read_cached_binary(&sys, Path::new("/dl/release/v1.0.0/deno.zip"));
assert!(result.is_none());
let cache_path = Path::new("/dl/release/v1.0.0/deno.zip");
sys.fs_create_dir_all(cache_path.parent().unwrap()).unwrap();
sys.fs_write(cache_path, b"archive-data").unwrap();
let result = try_read_cached_binary(&sys, cache_path);
assert_eq!(result.unwrap(), b"archive-data");
}
#[test]
fn test_store_cached_binary() {
use sys_traits::FsRead;
use sys_traits::impls::InMemorySys;
let sys = InMemorySys::default();
sys.set_seed(Some(42));
let cache_path = Path::new("/dl/release/v1.0.0/deno.zip");
store_cached_binary(&sys, cache_path, b"archive-data");
let data = sys.fs_read(cache_path).unwrap();
assert_eq!(data.as_ref(), b"archive-data");
}
#[test]
fn test_store_and_read_cached_binary_roundtrip() {
use sys_traits::impls::InMemorySys;
let sys = InMemorySys::default();
sys.set_seed(Some(42));
let cache_path = Path::new("/dl/canary/abc123/deno.zip");
assert!(try_read_cached_binary(&sys, cache_path).is_none());
let original_data = b"test-archive-contents";
store_cached_binary(&sys, cache_path, original_data);
let cached = try_read_cached_binary(&sys, cache_path).unwrap();
assert_eq!(cached, original_data);
}
#[test]
fn test_prune_canary_cache_no_dir() {
use sys_traits::impls::InMemorySys;
let sys = InMemorySys::default();
prune_canary_cache(&sys, Path::new("/dl"), 10);
}
#[test]
fn test_prune_canary_cache_under_limit() {
use sys_traits::FsCreateDirAll;
use sys_traits::FsMetadata;
use sys_traits::impls::InMemorySys;
let sys = InMemorySys::default();
let dl_dir = Path::new("/dl");
sys.fs_create_dir_all(dl_dir.join("canary/hash1")).unwrap();
sys.fs_create_dir_all(dl_dir.join("canary/hash2")).unwrap();
prune_canary_cache(&sys, dl_dir, 5);
assert!(sys.fs_exists(dl_dir.join("canary/hash1")).unwrap());
assert!(sys.fs_exists(dl_dir.join("canary/hash2")).unwrap());
}
#[test]
fn test_prune_canary_cache_over_limit() {
use std::time::Duration;
use std::time::SystemTime;
use sys_traits::FsCreateDirAll;
use sys_traits::FsMetadata;
use sys_traits::FsSetFileTimes;
use sys_traits::impls::InMemorySys;
let sys = InMemorySys::default();
let dl_dir = Path::new("/dl");
let base_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000_000);
for (i, name) in ["oldest", "old", "new", "newest"].iter().enumerate() {
let dir = dl_dir.join(format!("canary/{}", name));
sys.fs_create_dir_all(&dir).unwrap();
let time = base_time + Duration::from_secs(i as u64 * 100);
sys.fs_set_file_times(&dir, time, time).unwrap();
}
prune_canary_cache(&sys, dl_dir, 2);
assert!(sys.fs_exists(dl_dir.join("canary/newest")).unwrap());
assert!(sys.fs_exists(dl_dir.join("canary/new")).unwrap());
assert!(!sys.fs_exists(dl_dir.join("canary/oldest")).unwrap());
assert!(!sys.fs_exists(dl_dir.join("canary/old")).unwrap());
}
}