Add TestProject wrapper for mutable tests.
Change-Id: I1e03bc8fbb78411375777388620f522cde12b21f
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/qg/+/126771
Commit-Queue: Erik Gilling <konkers@google.com>
Reviewed-by: Alexei Frolov <frolv@google.com>
diff --git a/qg/Cargo.toml b/qg/Cargo.toml
index f242e21..7e12ce2 100644
--- a/qg/Cargo.toml
+++ b/qg/Cargo.toml
@@ -12,16 +12,17 @@
fixedbitset = "0.4.2"
futures = "0.3.25"
num-traits = "0.2.15"
+nom = "7.1.2"
once_cell = "1.16.0"
petgraph = "0.6.2"
regex = "1.7.0"
+reqwest = "0.11.13"
serde = { version = "1.0.147", features = ["derive"] }
+sha2 = "0.10.6"
thiserror = "1.0.37"
tokio = { version = "1.21.2", features = ["full"] }
toml = "0.5.9"
-nom = "7.1.2"
-reqwest = "0.11.13"
-sha2 = "0.10.6"
+walkdir = "2"
[dependencies.rustpython]
git = "https://github.com/RustPython/RustPython"
diff --git a/qg/src/lib.rs b/qg/src/lib.rs
index 4ac1154..c1a412e 100644
--- a/qg/src/lib.rs
+++ b/qg/src/lib.rs
@@ -31,6 +31,9 @@
#[cfg(test)]
pub(crate) mod fake;
+#[cfg(test)]
+pub(crate) mod test_util;
+
#[doc(inline)]
pub use target::Target;
diff --git a/qg/src/test_util.rs b/qg/src/test_util.rs
new file mode 100644
index 0000000..e4db038
--- /dev/null
+++ b/qg/src/test_util.rs
@@ -0,0 +1,65 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+use std::path::{Path, PathBuf};
+
+use tempfile::{Builder, TempDir};
+
+use crate::{util::copy_directory, Project, Result};
+
+/// `TestProject` copies a project to a temporary directory for tests
+/// which need to write to the project directory.
+///
+/// The temporary directory is deleted when the `TestProject` is dropped.
+pub(crate) struct TestProject {
+ pub project: Project,
+
+ // `TempDir` will be deleted when the `TestProject` is dropped.
+ project_root: TempDir,
+}
+
+impl TestProject {
+ /// Creates a new `TestProject` from the project at `path`
+ ///
+ /// # Errors
+ /// Returns an error if they project can not be copied.
+ pub fn new(path: impl AsRef<Path>) -> Result<Self> {
+ let test_project_root = Builder::new().prefix("qg-test-project").tempdir()?;
+
+ copy_directory(path, &test_project_root)?;
+
+ Ok(Self {
+ project: Project::load(&test_project_root)?,
+ project_root: test_project_root,
+ })
+ }
+
+ /// Returns a `PathBuf` to the file at the relative path `path` within
+ /// the `TestProject` temporary copy.
+ #[allow(unused)]
+ pub fn relative_path(&self, path: impl AsRef<Path>) -> PathBuf {
+ self.project_root.path().join(path)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_project() {
+ let test_project = TestProject::new("./src/test_projects/simple").unwrap();
+ assert_eq!(test_project.project.name(), "test-qg-project");
+ }
+}
diff --git a/qg/src/util.rs b/qg/src/util.rs
index af907fb..6bd1320 100644
--- a/qg/src/util.rs
+++ b/qg/src/util.rs
@@ -26,8 +26,54 @@
use once_cell::sync::Lazy;
use regex::Regex;
+#[cfg(test)]
+use std::path::Path;
+#[cfg(test)]
+use walkdir::WalkDir;
+
use crate::{Error, Result};
+/// Copy `src` directory to `dest`. `dest` will be created if it or its
+/// parents do not exist.
+///
+/// # Errors
+/// Returns errors when:
+/// - The `src` directory does not exist.
+/// - A directory or file can not be created in the `dest` directory.
+/// - The `src` directory contains symlinks.
+#[cfg(test)] // Only used in tests. Remove if used elsewhere
+pub(crate) fn copy_directory(src: impl AsRef<Path>, dest: impl AsRef<Path>) -> Result<()> {
+ copy_directory_impl(src.as_ref(), dest.as_ref())
+}
+
+#[cfg(test)] // Only used in tests. Remove if used elsewhere
+pub(crate) fn copy_directory_impl(src: &Path, dest: &Path) -> Result<()> {
+ let dest = dest.to_path_buf();
+ for entry in WalkDir::new(src) {
+ let entry = entry.map_err(|e| Error::StringErrorPlaceholder(e.to_string()))?;
+
+ let src_relative_path = entry
+ .path()
+ .strip_prefix(&src)
+ .expect("child entry should be in directory path");
+ let dest_location = dest.join(&src_relative_path);
+
+ if entry.file_type().is_dir() {
+ std::fs::create_dir_all(dest_location)?;
+ } else if entry.file_type().is_file() {
+ // Because we're walking the directory, a file's parent directory
+ // will already be created.
+ std::fs::copy(entry.path(), dest_location)?;
+ } else if entry.file_type().is_symlink() {
+ return Err(Error::StringErrorPlaceholder(format!(
+ "copying symlinks not supported: {entry:?}"
+ )));
+ }
+ }
+
+ Ok(())
+}
+
#[derive(Debug)]
enum StringFragment {
Literal(String),