Refactor project loading for testability.

Change-Id: Ib0be12b67b2f32c809610248013b8610f234d2b7
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/qg/+/125530
Reviewed-by: Alexei Frolov <frolv@google.com>
Commit-Queue: Erik Gilling <konkers@google.com>
diff --git a/qg/src/project/mod.rs b/qg/src/project/mod.rs
index 8d76556..8fe7bb9 100644
--- a/qg/src/project/mod.rs
+++ b/qg/src/project/mod.rs
@@ -62,18 +62,40 @@
     /// - [`Error::ProjectNotFound`]: No project could be located.
     pub fn locate() -> Result<Self> {
         let cwd = std::env::current_dir()?;
-        let mut cwd = cwd.as_path();
+        let cwd = cwd.as_path();
+        Self::locate_from_path(cwd)
+    }
 
-        let project_root = loop {
-            if cwd.join(Self::MANIFEST_FILE).exists() {
-                break cwd;
+    // Implementation details of `locate()` without looking up the
+    // current working directory.  This allows testing of the core
+    // logic without dependence on the environment and makes it
+    // friendlier to parallel test execution.
+    fn locate_from_path<P: AsRef<Path>>(cwd: P) -> Result<Self> {
+        let mut cwd = cwd.as_ref();
+
+        loop {
+            if let Ok(project) = Self::load(cwd) {
+                return Ok(project);
             }
 
             cwd = match cwd.parent() {
                 Some(p) => p,
                 None => return Err(Error::ProjectNotFound),
             }
-        };
+        }
+    }
+
+    /// Load a qg project from an specific path.
+    ///
+    /// # Errors
+    /// May return one of the following errors:
+    /// - [`Error::File`]: Failed to access the filesystem.
+    /// - [`Error::ProjectNotFound`]: No project manifest found in `project_root`.
+    pub fn load<P: AsRef<Path>>(project_root: P) -> Result<Self> {
+        let project_root = project_root.as_ref();
+        if !project_root.join(Self::MANIFEST_FILE).exists() {
+            return Err(Error::ProjectNotFound);
+        }
 
         let manifest = self::File::new(project_root.join(Self::MANIFEST_FILE))
             .deserialize_toml::<Manifest>()?;
@@ -314,4 +336,22 @@
             Error::InvalidPath,
         ));
     }
+
+    #[test]
+    fn locate_test_project() {
+        // Test that `locate()` and find a manifest from a parent directory.
+        let project =
+            Project::locate_from_path("./src/test_projects/simple/subdir/subsubdir").unwrap();
+        assert_eq!(project.name, "test-qg-project");
+    }
+
+    #[test]
+    fn load_test_project() {
+        // Loads from the project root succeed.
+        let project = Project::load("./src/test_projects/simple").unwrap();
+        assert_eq!(project.name, "test-qg-project");
+
+        // Loads from a project subdirectory fail.
+        assert!(Project::load("./src/test_projects/simple/subdir/subsubdir").is_err());
+    }
 }
diff --git a/qg/src/test_projects/simple/qg.toml b/qg/src/test_projects/simple/qg.toml
new file mode 100644
index 0000000..23405e2
--- /dev/null
+++ b/qg/src/test_projects/simple/qg.toml
@@ -0,0 +1,2 @@
+[project]
+name = "test-qg-project"
diff --git a/qg/src/test_projects/simple/subdir/subsubdir/keepdir b/qg/src/test_projects/simple/subdir/subsubdir/keepdir
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/qg/src/test_projects/simple/subdir/subsubdir/keepdir