aether_evals/evals/
workspace.rs1use crate::WorkspaceError;
2use schemars::JsonSchema;
3use serde::Serialize;
4use std::{
5 fs::{create_dir_all, write},
6 path::{Path, PathBuf},
7};
8use tempfile::TempDir;
9
10pub struct Workspace {
11 path: PathBuf,
12 root_path: PathBuf,
13 relative_cwd: Option<PathBuf>,
14 temp_dir: TempDir,
15}
16
17#[derive(Debug, Clone, Serialize, JsonSchema)]
18#[serde(rename_all = "camelCase", deny_unknown_fields)]
19pub struct RetainedWorkspaceInfo {
20 pub root_path: PathBuf,
21 pub path: PathBuf,
22}
23
24impl Workspace {
25 pub fn empty() -> Result<Self, WorkspaceError> {
26 Self::from_temp_dir(new_temp_dir()?, None)
27 }
28
29 pub fn from_dir(src_path: impl Into<PathBuf>) -> Result<Self, WorkspaceError> {
30 let src_path = src_path.into();
31 let temp_dir = new_temp_dir()?;
32
33 copy_dir_contents(&src_path, temp_dir.path()).map_err(|source| WorkspaceError::CopyFixture {
34 from: src_path.clone(),
35 to: temp_dir.path().to_path_buf(),
36 source,
37 })?;
38
39 Self::from_temp_dir(temp_dir, None)
40 }
41
42 pub fn from_files<T: AsRef<Path>, U: AsRef<str>>(
43 files: impl IntoIterator<Item = (T, U)>,
44 ) -> Result<Self, WorkspaceError> {
45 let workspace = Self::empty()?;
46 for (relative_path, contents) in files {
47 let path = workspace.path().join(relative_path.as_ref());
48 if let Some(parent) = path.parent() {
49 create_dir_all(parent)
50 .map_err(|source| WorkspaceError::WriteFile { path: parent.to_path_buf(), source })?;
51 }
52 write(&path, contents.as_ref()).map_err(|source| WorkspaceError::WriteFile { path, source })?;
53 }
54 Ok(workspace)
55 }
56
57 pub fn path(&self) -> &Path {
58 &self.path
59 }
60
61 pub fn join(&self, relative_path: impl AsRef<Path>) -> PathBuf {
62 self.path.join(relative_path)
63 }
64
65 pub fn root_path(&self) -> &Path {
66 &self.root_path
67 }
68
69 pub fn relative_cwd(&self) -> Option<&Path> {
70 self.relative_cwd.as_deref()
71 }
72
73 pub fn persist(self) -> RetainedWorkspaceInfo {
75 let root_path = self.temp_dir.keep();
76 let path = self.relative_cwd.map_or_else(|| root_path.clone(), |relative_cwd| root_path.join(relative_cwd));
77 RetainedWorkspaceInfo { root_path, path }
78 }
79
80 pub(super) fn from_temp_dir(temp_dir: TempDir, subdir: Option<PathBuf>) -> Result<Self, WorkspaceError> {
81 let root_path = temp_dir.path().to_path_buf();
82 let (path, relative_cwd) = resolve_subdir(&root_path, subdir)?;
83 Ok(Self { path, root_path, relative_cwd, temp_dir })
84 }
85}
86
87pub(super) fn new_temp_dir() -> Result<TempDir, WorkspaceError> {
88 tempfile::tempdir().map_err(WorkspaceError::CreateTempDir)
89}
90
91fn resolve_subdir(root_path: &Path, subdir: Option<PathBuf>) -> Result<(PathBuf, Option<PathBuf>), WorkspaceError> {
92 match subdir {
93 None => Ok((root_path.to_path_buf(), None)),
94 Some(relative_cwd) => {
95 let working_path = root_path.join(&relative_cwd);
96 if !working_path.exists() {
97 return Err(WorkspaceError::MissingSubdir { path: working_path });
98 }
99 Ok((working_path, Some(relative_cwd)))
100 }
101 }
102}
103
104fn copy_dir_contents(src: &Path, dst: &Path) -> std::io::Result<()> {
105 for entry in std::fs::read_dir(src)? {
106 let entry = entry?;
107 let source_path = entry.path();
108 let dest_path = dst.join(entry.file_name());
109 let file_type = entry.file_type()?;
110
111 if file_type.is_dir() {
112 std::fs::create_dir_all(&dest_path)?;
113 copy_dir_contents(&source_path, &dest_path)?;
114 } else if file_type.is_file() {
115 std::fs::copy(&source_path, &dest_path)?;
116 }
117 }
118 Ok(())
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124 use std::fs::{read_to_string, remove_dir_all};
125
126 #[test]
127 fn persist_reports_root_and_path_for_local_workspace() {
128 let workspace = Workspace::from_files([("notes.txt", "hi\n")]).unwrap();
129
130 let retained = workspace.persist();
131
132 assert_eq!(retained.root_path, retained.path);
133 assert_eq!(read_to_string(retained.path.join("notes.txt")).unwrap(), "hi\n");
134 remove_dir_all(retained.root_path).unwrap();
135 }
136}