pop_common/
helpers.rs

1// SPDX-License-Identifier: GPL-3.0
2
3use crate::Error;
4use std::{
5	collections::HashMap,
6	fs,
7	io::{Read, Write},
8	path::{Path, PathBuf},
9};
10
11/// Replaces occurrences of specified strings in a file with new values.
12///
13/// # Arguments
14///
15/// * `file_path` - A `PathBuf` specifying the path to the file to be modified.
16/// * `replacements` - A `HashMap` where each key-value pair represents a target string and its
17///   corresponding replacement string.
18pub fn replace_in_file(file_path: PathBuf, replacements: HashMap<&str, &str>) -> Result<(), Error> {
19	// Read the file content
20	let mut file_content = String::new();
21	fs::File::open(&file_path)?.read_to_string(&mut file_content)?;
22	// Perform the replacements
23	let mut modified_content = file_content;
24	for (target, replacement) in &replacements {
25		modified_content = modified_content.replace(target, replacement);
26	}
27	// Write the modified content back to the file
28	let mut file = fs::File::create(&file_path)?;
29	file.write_all(modified_content.as_bytes())?;
30	Ok(())
31}
32
33/// Gets the last component (name of a project) of a path or returns a default value if the path has
34/// no valid last component.
35///
36/// # Arguments
37/// * `path` - Location path of the project.
38/// * `default` - The default string to return if the path has no valid last component.
39pub fn get_project_name_from_path<'a>(path: &'a Path, default: &'a str) -> &'a str {
40	path.file_name().and_then(|name| name.to_str()).unwrap_or(default)
41}
42
43/// Returns the relative path from `base` to `full` if `full` is inside `base`.
44/// If `full` is outside `base`, returns the absolute path instead.
45///
46/// # Arguments
47/// * `base` - The base directory to compare against.
48/// * `full` - The full path to be shortened.
49pub fn get_relative_or_absolute_path(base: &Path, full: &Path) -> PathBuf {
50	match full.strip_prefix(base) {
51		Ok(relative) => relative.to_path_buf(),
52		// If prefix is different, return the full path
53		Err(_) => full.to_path_buf(),
54	}
55}
56
57/// Temporarily changes the current working directory while executing a closure.
58pub fn with_current_dir<F, R>(dir: &Path, f: F) -> anyhow::Result<R>
59where
60	F: FnOnce() -> anyhow::Result<R>,
61{
62	let original_dir = std::env::current_dir()?;
63	std::env::set_current_dir(dir)?;
64	let result = f();
65	std::env::set_current_dir(original_dir)?;
66	result
67}
68
69/// Temporarily changes the current working directory while executing an asynchronous closure.
70pub async fn with_current_dir_async<F, R>(dir: &Path, f: F) -> anyhow::Result<R>
71where
72	F: AsyncFnOnce() -> anyhow::Result<R>,
73{
74	let original_dir = std::env::current_dir()?;
75	std::env::set_current_dir(dir)?;
76	let result = f().await;
77	std::env::set_current_dir(original_dir)?;
78	result
79}
80
81#[cfg(test)]
82mod tests {
83	use super::*;
84	use anyhow::Result;
85	use std::{
86		fs,
87		sync::{Mutex, OnceLock},
88	};
89
90	// Changing the current working directory is a global, process-wide side effect.
91	// Serialize such tests to avoid flakiness when tests run in parallel.
92	static CWD_TEST_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
93
94	fn cwd_lock() -> std::sync::MutexGuard<'static, ()> {
95		CWD_TEST_MUTEX.get_or_init(|| Mutex::new(())).lock().unwrap()
96	}
97
98	#[test]
99	fn test_replace_in_file() -> Result<(), Error> {
100		let temp_dir = tempfile::tempdir()?;
101		let file_path = temp_dir.path().join("file.toml");
102		let mut file = fs::File::create(temp_dir.path().join("file.toml"))?;
103		writeln!(file, "name = test, version = 5.0.0")?;
104		let mut replacements_in_cargo = HashMap::new();
105		replacements_in_cargo.insert("test", "changed_name");
106		replacements_in_cargo.insert("5.0.0", "5.0.1");
107		replace_in_file(file_path.clone(), replacements_in_cargo)?;
108		let content = fs::read_to_string(file_path).expect("Could not read file");
109		assert_eq!(content.trim(), "name = changed_name, version = 5.0.1");
110		Ok(())
111	}
112
113	#[test]
114	fn get_project_name_from_path_works() -> Result<(), Error> {
115		let path = Path::new("./path/to/project/my-parachain");
116		assert_eq!(get_project_name_from_path(path, "default_name"), "my-parachain");
117		Ok(())
118	}
119
120	#[test]
121	fn get_project_name_from_path_default_value() -> Result<(), Error> {
122		let path = Path::new("./");
123		assert_eq!(get_project_name_from_path(path, "my-contract"), "my-contract");
124		Ok(())
125	}
126
127	#[test]
128	fn get_relative_or_absolute_path_works() {
129		[
130			("/path/to/project", "/path/to/project", ""),
131			("/path/to/project", "/path/to/src", "/path/to/src"),
132			("/path/to/project", "/path/to/project/main.rs", "main.rs"),
133			("/path/to/project", "/path/to/project/../main.rs", "../main.rs"),
134			("/path/to/project", "/path/to/project/src/main.rs", "src/main.rs"),
135		]
136		.into_iter()
137		.for_each(|(base, full, expected)| {
138			assert_eq!(
139				get_relative_or_absolute_path(Path::new(base), Path::new(full)),
140				Path::new(expected)
141			);
142		});
143	}
144
145	#[test]
146	fn with_current_dir_changes_and_restores_cwd() -> anyhow::Result<()> {
147		let _guard = cwd_lock();
148		let original = std::env::current_dir()?;
149		let temp_dir = tempfile::tempdir()?;
150		let tmp_path = temp_dir.path().to_path_buf();
151
152		let res: &str = with_current_dir(&tmp_path, || {
153			// Inside the closure, the cwd should be the temp dir (canonicalized for macOS /private
154			// symlink).
155			let cwd = std::env::current_dir().unwrap();
156			assert_eq!(cwd.canonicalize().unwrap(), tmp_path.canonicalize().unwrap());
157			// Create a file relative to the new cwd to verify it's applied.
158			fs::write("hello.txt", b"world").unwrap();
159			Ok("done")
160		})?;
161		assert_eq!(res, "done");
162
163		// After the closure, cwd should be restored.
164		assert_eq!(std::env::current_dir()?, original);
165		// The file should exist inside the temp dir.
166		assert!(tmp_path.join("hello.txt").exists());
167		Ok(())
168	}
169
170	#[tokio::test]
171	async fn with_current_dir_async_changes_and_restores_cwd() -> anyhow::Result<()> {
172		// Acquire and drop the mutex guard before async operations
173		{
174			let _guard = cwd_lock();
175		}
176
177		let original = std::env::current_dir()?;
178		let temp_dir = tempfile::tempdir()?;
179		let tmp_path = temp_dir.path().to_path_buf();
180
181		let res: &str = with_current_dir_async(&tmp_path, || async {
182			// Inside the async closure, the cwd should be the temp dir (canonicalized for macOS
183			// /private symlink).
184			let cwd = std::env::current_dir().unwrap();
185			assert_eq!(cwd.canonicalize().unwrap(), tmp_path.canonicalize().unwrap());
186			// Create a file relative to the new cwd to verify it's applied.
187			fs::write("async.txt", b"ok").unwrap();
188			Ok("async-done")
189		})
190		.await?;
191		assert_eq!(res, "async-done");
192
193		// After the closure, cwd should be restored.
194		assert_eq!(std::env::current_dir()?, original);
195		// The file should exist inside the temp dir.
196		assert!(tmp_path.join("async.txt").exists());
197		Ok(())
198	}
199}