batch_mode_batch_client/
download_error_file.rs

1// ---------------- [ File: batch-mode-batch-client/src/download_error_file.rs ]
2crate::ix!();
3
4#[async_trait]
5impl<E> DownloadErrorFile<E> for BatchFileTriple
6where
7    E: From<BatchDownloadError>
8        + From<std::io::Error>
9        + From<BatchMetadataError>
10        + From<OpenAIClientError>
11        + Debug,
12{
13    async fn download_error_file(
14        &mut self,
15        client: &dyn LanguageModelClientInterface<E>,
16    ) -> Result<(), E> {
17        info!("downloading batch error file");
18
19        // CHANGE: Instead of failing when `self.error().is_some()`,
20        // we only fail if the path is actually present on disk.
21        if let Some(err_path) = &self.error() {
22            if err_path.exists() {
23                warn!(
24                    "Error file already present on disk at path={:?}. \
25                     Aborting to avoid overwriting.",
26                    err_path
27                );
28                return Err(BatchDownloadError::ErrorFileAlreadyExists {
29                    triple: self.clone(),
30                }
31                .into());
32            }
33        }
34
35        let metadata_filename = match self.associated_metadata() {
36            Some(file) => file.to_path_buf(),
37            None => self.effective_metadata_filename().to_path_buf(),
38        };
39        debug!("Using metadata file for error: {:?}", metadata_filename);
40
41        let metadata = BatchMetadata::load_from_file(&metadata_filename).await?;
42        let error_file_id = metadata.error_file_id()?;
43
44        let file_content = client.file_content(error_file_id).await?;
45
46        let error_path = self.effective_error_filename();
47        if let Some(parent) = error_path.parent() {
48            tokio::fs::create_dir_all(parent).await.ok();
49        }
50
51        // Force removal if the file already exists, so no leftover content remains:
52        if error_path.exists() {
53            std::fs::remove_file(&error_path)?;
54        }
55
56        std::fs::write(&error_path, file_content)?;
57        self.set_error_path(Some(error_path));
58        Ok(())
59    }
60}
61
62#[cfg(test)]
63mod download_error_file_tests {
64    use super::*;
65    use futures::executor::block_on;
66    use std::fs;
67    use tempfile::tempdir;
68    use tracing::{debug, error, info, trace, warn};
69
70    /// Exhaustive test suite for `DownloadErrorFile` on `BatchFileTriple`.
71    /// We'll cover scenarios of success, missing file_id, existing file, and client/IO errors.
72    #[traced_test]
73    async fn test_download_error_file_ok() {
74        info!("Beginning test_download_error_file_ok");
75        trace!("Constructing mock client...");
76        let mock_client = MockLanguageModelClientBuilder::<MockBatchClientError>::default()
77            .build()
78            .unwrap();
79        debug!("Mock client: {:?}", mock_client);
80
81        // Insert a known file in the mock so the "client.file_content()" call succeeds
82        let error_file_id = "some_error_file_id";
83        {
84            let mut files_guard = mock_client.files().write().unwrap();
85            files_guard.insert(error_file_id.to_string(), Bytes::from("mock error contents"));
86        }
87
88        // Insert batch metadata with the relevant error_file_id
89        let tmpdir = tempdir().unwrap();
90        let metadata_path = tmpdir.path().join("metadata.json");
91        let metadata = BatchMetadataBuilder::default()
92            .batch_id("some_batch_id".to_string())
93            .input_file_id("some_input_file_id".to_string())
94            .output_file_id(None)
95            .error_file_id(Some(error_file_id.to_string()))
96            .build()
97            .unwrap();
98        info!("Saving metadata at {:?}", metadata_path);
99        metadata.save_to_file(&metadata_path).await.unwrap();
100
101        trace!("Creating BatchFileTriple with known metadata path...");
102        let mut triple = BatchFileTriple::new_for_test_with_metadata_path(metadata_path.clone());
103        triple.set_metadata_path(Some(metadata_path.clone()));
104
105        // --- Set an ephemeral error file path. ---
106        let err_path = tmpdir.path().join("error.json");
107        triple.set_error_path(Some(err_path.clone()));
108
109        trace!("Calling download_error_file...");
110        let result = triple.download_error_file(&mock_client).await;
111        debug!("Result from download_error_file: {:?}", result);
112
113        assert!(result.is_ok(), "Should succeed for a valid error file");
114        // Ensure file was written
115        let contents = fs::read_to_string(&err_path).unwrap();
116        pretty_assert_eq!(contents, "mock error contents");
117
118        info!("test_download_error_file_ok passed");
119    }
120
121    #[traced_test]
122    async fn test_download_error_file_already_exists() {
123        info!("Beginning test_download_error_file_already_exists");
124        let mock_client = MockLanguageModelClientBuilder::<MockBatchClientError>::default()
125            .build()
126            .unwrap();
127        debug!("Mock client: {:?}", mock_client);
128
129        // Insert metadata
130        let tmpdir = tempdir().unwrap();
131        let metadata_path = tmpdir.path().join("metadata.json");
132        let metadata = BatchMetadataBuilder::default()
133            .batch_id("batch_id_exists_err")
134            .input_file_id("some_input_file_id".to_string())
135            .output_file_id(None)
136            .error_file_id(Some("already_exists_err_file_id".to_string()))
137            .build()
138            .unwrap();
139        metadata.save_to_file(&metadata_path).await.unwrap();
140
141        // Prepare the triple
142        let mut triple = BatchFileTriple::new_for_test_with_metadata_path(metadata_path.clone());
143        triple.set_metadata_path(Some(metadata_path.clone()));
144
145        // Simulate that the error file is already downloaded
146        let existing_err_path = tmpdir.path().join("error.json");
147        fs::write(&existing_err_path, b"existing content").unwrap();
148        triple.set_error_path(Some(existing_err_path.clone()));
149
150        let result = triple.download_error_file(&mock_client).await;
151        debug!("Result from download_error_file: {:?}", result);
152
153        assert!(
154            result.is_err(),
155            "Should fail if error file already exists on disk"
156        );
157        info!("test_download_error_file_already_exists passed");
158    }
159
160    #[traced_test]
161    async fn test_download_error_file_missing_error_file_id() {
162        info!("Beginning test_download_error_file_missing_error_file_id");
163        let mock_client = MockLanguageModelClientBuilder::<MockBatchClientError>::default()
164            .build()
165            .unwrap();
166        debug!("Mock client: {:?}", mock_client);
167
168        // Insert metadata that does NOT have an error_file_id
169        let tmpdir = tempdir().unwrap();
170        let metadata_path = tmpdir.path().join("metadata.json");
171        let metadata = BatchMetadataBuilder::default()
172            .batch_id("batch_no_err_id")
173            .input_file_id("input_file_id".to_string())
174            .output_file_id(None)
175            .error_file_id(None)
176            .build()
177            .unwrap();
178        metadata.save_to_file(&metadata_path).await.unwrap();
179
180        // Prepare the triple
181        let mut triple = BatchFileTriple::new_for_test_with_metadata_path(metadata_path.clone());
182        triple.set_metadata_path(Some(metadata_path.clone()));
183
184        // ephemeral path, though it won't actually be written
185        let err_path = tmpdir.path().join("placeholder_error_file.json");
186        triple.set_error_path(Some(err_path.clone()));
187
188        let result = triple.download_error_file(&mock_client).await;
189        debug!("Result from download_error_file: {:?}", result);
190
191        assert!(
192            result.is_err(),
193            "Should fail if error_file_id is not present in metadata"
194        );
195        info!("test_download_error_file_missing_error_file_id passed");
196    }
197
198    #[traced_test]
199    async fn test_download_error_file_client_file_not_found() {
200        info!("Beginning test_download_error_file_client_file_not_found");
201        let mock_client = MockLanguageModelClientBuilder::<MockBatchClientError>::default()
202            .build()
203            .unwrap();
204
205        // Insert metadata referencing a nonexistent error file in the mock
206        let tmpdir = tempdir().unwrap();
207        let metadata_path = tmpdir.path().join("metadata.json");
208        let metadata = BatchMetadataBuilder::default()
209            .batch_id("batch_err_file_not_found")
210            .input_file_id("some_input".to_string())
211            .output_file_id(None)
212            .error_file_id(Some("err_file_that_does_not_exist".to_string()))
213            .build()
214            .unwrap();
215        metadata.save_to_file(&metadata_path).await.unwrap();
216
217        let mut triple = BatchFileTriple::new_for_test_with_metadata_path(metadata_path.clone());
218        triple.set_metadata_path(Some(metadata_path.clone()));
219
220        let err_path = tmpdir.path().join("error_file.json");
221        triple.set_error_path(Some(err_path.clone()));
222
223        let result = triple.download_error_file(&mock_client).await;
224        debug!("Result from download_error_file: {:?}", result);
225
226        assert!(
227            result.is_err(),
228            "Should fail if the mock client cannot find the error file_id"
229        );
230        info!("test_download_error_file_client_file_not_found passed");
231    }
232
233    #[traced_test]
234    async fn test_download_error_file_io_write_error() {
235        info!("Beginning test_download_error_file_io_write_error");
236        let mock_client = MockLanguageModelClientBuilder::<MockBatchClientError>::default()
237            .build()
238            .unwrap();
239
240        // Put a real file in the mock, so the content retrieval works
241        let error_file_id = "some_err_file_id_for_io_error";
242        {
243            let mut files_guard = mock_client.files().write().unwrap();
244            files_guard.insert(error_file_id.to_string(), Bytes::from("err content"));
245        }
246
247        // We'll create one tempdir for metadata (fully writable),
248        // and a separate subdir for the actual error file that we make read-only.
249        let tmpdir_meta = tempdir().unwrap();
250        let tmpdir_readonly = tempdir().unwrap();
251
252        // We'll keep the metadata in tmpdir_meta:
253        let metadata_path = tmpdir_meta.path().join("metadata.json");
254        let metadata = BatchMetadataBuilder::default()
255            .batch_id("batch_io_error")
256            .input_file_id("some_input".to_string())
257            .output_file_id(None)
258            .error_file_id(Some(error_file_id.to_string()))
259            .build()
260            .unwrap();
261        info!("Saving metadata at {:?}", metadata_path);
262        metadata.save_to_file(&metadata_path).await.unwrap();
263        debug!("Metadata file created successfully.");
264
265        // Now the triple will reference that metadata path
266        let mut triple = BatchFileTriple::new_for_test_with_metadata_path(metadata_path.clone());
267        triple.set_metadata_path(Some(metadata_path.clone()));
268
269        // We forcibly set the "error file" path to the read-only dir:
270        let err_path = tmpdir_readonly.path().join("error.json");
271        triple.set_error_path(Some(err_path.clone()));
272
273        // Make that directory read-only:
274        let mut perms = fs::metadata(tmpdir_readonly.path()).unwrap().permissions();
275        perms.set_readonly(true);
276        fs::set_permissions(tmpdir_readonly.path(), perms).unwrap();
277
278        let result = triple.download_error_file(&mock_client).await;
279        debug!("Result from download_error_file: {:?}", result);
280
281        // Revert permissions so we can clean up tempdir_readonly:
282        let mut perms = fs::metadata(tmpdir_readonly.path()).unwrap().permissions();
283        perms.set_readonly(false);
284        fs::set_permissions(tmpdir_readonly.path(), perms).unwrap();
285
286        assert!(
287            result.is_err(),
288            "Should fail with an I/O error when the directory is read-only"
289        );
290        info!("test_download_error_file_io_write_error passed");
291    }
292}