// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use pretty_assertions::assert_eq; use std::borrow::Cow; use std::ffi::OsStr; use std::fs; use std::fs::OpenOptions; use std::io::Write; use std::path::Path; use std::path::PathBuf; use std::process::Command; use std::sync::Arc; use anyhow::Context; use lsp_types::Url; use serde::de::DeserializeOwned; use serde::Serialize; use crate::assertions::assert_wildcard_match; use crate::testdata_path; /// Represents a path on the file system, which can be used /// to perform specific actions. #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct PathRef(PathBuf); impl AsRef for PathRef { fn as_ref(&self) -> &Path { self.as_path() } } impl AsRef for PathRef { fn as_ref(&self) -> &OsStr { self.as_path().as_ref() } } impl std::fmt::Display for PathRef { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.as_path().display()) } } impl PathRef { pub fn new(path: impl AsRef) -> Self { Self(path.as_ref().to_path_buf()) } pub fn parent(&self) -> PathRef { PathRef(self.as_path().parent().unwrap().to_path_buf()) } pub fn uri_dir(&self) -> Url { Url::from_directory_path(self.as_path()).unwrap() } pub fn uri_file(&self) -> Url { Url::from_file_path(self.as_path()).unwrap() } pub fn as_path(&self) -> &Path { self.0.as_path() } pub fn to_path_buf(&self) -> PathBuf { self.0.to_path_buf() } pub fn to_string_lossy(&self) -> Cow { self.0.to_string_lossy() } pub fn exists(&self) -> bool { self.0.exists() } pub fn try_exists(&self) -> std::io::Result { self.0.try_exists() } pub fn is_dir(&self) -> bool { self.0.is_dir() } pub fn is_file(&self) -> bool { self.0.is_file() } pub fn join(&self, path: impl AsRef) -> PathRef { PathRef(self.as_path().join(path)) } pub fn with_extension(&self, ext: impl AsRef) -> PathRef { PathRef(self.as_path().with_extension(ext)) } pub fn canonicalize(&self) -> PathRef { PathRef(strip_unc_prefix(self.as_path().canonicalize().unwrap())) } pub fn create_dir_all(&self) { fs::create_dir_all(self).unwrap(); } pub fn remove_file(&self) { fs::remove_file(self).unwrap(); } pub fn remove_dir_all(&self) { fs::remove_dir_all(self).unwrap(); } pub fn read_to_string(&self) -> String { self.read_to_string_if_exists().unwrap() } pub fn read_to_string_if_exists(&self) -> Result { fs::read_to_string(self) .with_context(|| format!("Could not read file: {}", self)) } pub fn read_to_bytes_if_exists(&self) -> Result, anyhow::Error> { fs::read(self).with_context(|| format!("Could not read file: {}", self)) } pub fn read_json(&self) -> TValue { serde_json::from_str(&self.read_to_string()).unwrap() } pub fn read_json_value(&self) -> serde_json::Value { serde_json::from_str(&self.read_to_string()).unwrap() } pub fn rename(&self, to: impl AsRef) { fs::rename(self, self.join(to)).unwrap(); } pub fn append(&self, text: impl AsRef) { let mut file = OpenOptions::new().append(true).open(self).unwrap(); file.write_all(text.as_ref().as_bytes()).unwrap(); } pub fn write(&self, text: impl AsRef) { fs::write(self, text.as_ref()).unwrap(); } pub fn write_json(&self, value: &TValue) { let text = serde_json::to_string_pretty(value).unwrap(); self.write(text); } pub fn symlink_dir( &self, oldpath: impl AsRef, newpath: impl AsRef, ) { #[cfg(unix)] { use std::os::unix::fs::symlink; symlink(self.as_path().join(oldpath), self.as_path().join(newpath)) .unwrap(); } #[cfg(not(unix))] { use std::os::windows::fs::symlink_dir; symlink_dir(self.as_path().join(oldpath), self.as_path().join(newpath)) .unwrap(); } } pub fn symlink_file( &self, oldpath: impl AsRef, newpath: impl AsRef, ) { #[cfg(unix)] { use std::os::unix::fs::symlink; symlink(self.as_path().join(oldpath), self.as_path().join(newpath)) .unwrap(); } #[cfg(not(unix))] { use std::os::windows::fs::symlink_file; symlink_file(self.as_path().join(oldpath), self.as_path().join(newpath)) .unwrap(); } } pub fn read_dir(&self) -> fs::ReadDir { fs::read_dir(self.as_path()) .with_context(|| format!("Reading {}", self.as_path().display())) .unwrap() } pub fn copy(&self, to: &impl AsRef) { std::fs::copy(self.as_path(), to) .with_context(|| format!("Copying {} to {}", self, to.as_ref().display())) .unwrap(); } /// Copies this directory to another directory. /// /// Note: Does not handle symlinks. pub fn copy_to_recursive(&self, to: &PathRef) { to.create_dir_all(); let read_dir = self.read_dir(); for entry in read_dir { let entry = entry.unwrap(); let file_type = entry.file_type().unwrap(); let new_from = self.join(entry.file_name()); let new_to = to.join(entry.file_name()); if file_type.is_dir() { new_from.copy_to_recursive(&new_to); } else if file_type.is_file() { new_from.copy(&new_to); } } } pub fn make_dir_readonly(&self) { self.create_dir_all(); if cfg!(windows) { Command::new("attrib").arg("+r").arg(self).output().unwrap(); } else if cfg!(unix) { Command::new("chmod").arg("555").arg(self).output().unwrap(); } } #[track_caller] pub fn assert_matches_file(&self, wildcard_file: impl AsRef) -> &Self { let wildcard_file = testdata_path().join(wildcard_file); println!("output path {}", wildcard_file); let expected_text = wildcard_file.read_to_string(); self.assert_matches_text(&expected_text) } #[track_caller] pub fn assert_matches_text(&self, wildcard_text: impl AsRef) -> &Self { let actual = self.read_to_string(); assert_wildcard_match(&actual, wildcard_text.as_ref()); self } #[track_caller] pub fn assert_matches_json(&self, expected: serde_json::Value) { let actual_json = self.read_json_value(); if actual_json != expected { let actual_text = serde_json::to_string_pretty(&actual_json).unwrap(); let expected_text = serde_json::to_string_pretty(&expected).unwrap(); assert_eq!(actual_text, expected_text); } } } #[cfg(not(windows))] #[inline] fn strip_unc_prefix(path: PathBuf) -> PathBuf { path } /// Strips the unc prefix (ex. \\?\) from Windows paths. /// /// Lifted from deno_core for use in the tests. #[cfg(windows)] fn strip_unc_prefix(path: PathBuf) -> PathBuf { use std::path::Component; use std::path::Prefix; let mut components = path.components(); match components.next() { Some(Component::Prefix(prefix)) => { match prefix.kind() { // \\?\device Prefix::Verbatim(device) => { let mut path = PathBuf::new(); path.push(format!(r"\\{}\", device.to_string_lossy())); path.extend(components.filter(|c| !matches!(c, Component::RootDir))); path } // \\?\c:\path Prefix::VerbatimDisk(_) => { let mut path = PathBuf::new(); path.push(prefix.as_os_str().to_string_lossy().replace(r"\\?\", "")); path.extend(components); path } // \\?\UNC\hostname\share_name\path Prefix::VerbatimUNC(hostname, share_name) => { let mut path = PathBuf::new(); path.push(format!( r"\\{}\{}\", hostname.to_string_lossy(), share_name.to_string_lossy() )); path.extend(components.filter(|c| !matches!(c, Component::RootDir))); path } _ => path, } } _ => path, } } enum TempDirInner { TempDir { path_ref: PathRef, // kept alive for the duration of the temp dir _dir: tempfile::TempDir, }, Path(PathRef), Symlinked { symlink: Arc, target: Arc, }, } impl TempDirInner { pub fn path(&self) -> &PathRef { match self { Self::Path(path_ref) => path_ref, Self::TempDir { path_ref, .. } => path_ref, Self::Symlinked { symlink, .. } => symlink.path(), } } pub fn target_path(&self) -> &PathRef { match self { TempDirInner::Symlinked { target, .. } => target.target_path(), _ => self.path(), } } } impl Drop for TempDirInner { fn drop(&mut self) { if let Self::Path(path) = self { _ = fs::remove_dir_all(path); } } } /// For creating temporary directories in tests. /// /// This was done because `tempfiles::TempDir` was very slow on Windows. /// /// Note: Do not use this in actual code as this does not protect against /// "insecure temporary file" security vulnerabilities. #[derive(Clone)] pub struct TempDir(Arc); impl Default for TempDir { fn default() -> Self { Self::new() } } impl TempDir { pub fn new() -> Self { Self::new_inner(&std::env::temp_dir(), None) } pub fn new_with_prefix(prefix: &str) -> Self { Self::new_inner(&std::env::temp_dir(), Some(prefix)) } pub fn new_in(parent_dir: &Path) -> Self { Self::new_inner(parent_dir, None) } pub fn new_with_path(path: &Path) -> Self { Self(Arc::new(TempDirInner::Path(PathRef(path.to_path_buf())))) } pub fn new_symlinked(target: TempDir) -> Self { let target_path = target.path(); let path = target_path.parent().join(format!( "{}_symlinked", target_path.as_path().file_name().unwrap().to_str().unwrap() )); target.symlink_dir(target.path(), &path); TempDir(Arc::new(TempDirInner::Symlinked { target: target.0, symlink: Self::new_with_path(path.as_path()).0, })) } /// Create a new temporary directory with the given prefix as part of its name, if specified. fn new_inner(parent_dir: &Path, prefix: Option<&str>) -> Self { let mut builder = tempfile::Builder::new(); builder.prefix(prefix.unwrap_or("deno-cli-test")); let dir = builder .tempdir_in(parent_dir) .expect("Failed to create a temporary directory"); Self(Arc::new(TempDirInner::TempDir { path_ref: PathRef(dir.path().to_path_buf()), _dir: dir, })) } pub fn uri(&self) -> Url { Url::from_directory_path(self.path()).unwrap() } pub fn path(&self) -> &PathRef { self.0.path() } /// The resolved final target path if this is a symlink. pub fn target_path(&self) -> &PathRef { self.0.target_path() } pub fn create_dir_all(&self, path: impl AsRef) { self.target_path().join(path).create_dir_all() } pub fn remove_file(&self, path: impl AsRef) { self.target_path().join(path).remove_file() } pub fn remove_dir_all(&self, path: impl AsRef) { self.target_path().join(path).remove_dir_all() } pub fn read_to_string(&self, path: impl AsRef) -> String { self.target_path().join(path).read_to_string() } pub fn rename(&self, from: impl AsRef, to: impl AsRef) { self.target_path().join(from).rename(to) } pub fn write(&self, path: impl AsRef, text: impl AsRef) { self.target_path().join(path).write(text) } pub fn symlink_dir( &self, oldpath: impl AsRef, newpath: impl AsRef, ) { self.target_path().symlink_dir(oldpath, newpath) } pub fn symlink_file( &self, oldpath: impl AsRef, newpath: impl AsRef, ) { self.target_path().symlink_file(oldpath, newpath) } }