1use crate::{FakeFs, FakeFsEntry, Fs};
2use anyhow::{Context as _, Result, bail};
3use collections::{HashMap, HashSet};
4use futures::future::{self, BoxFuture, join_all};
5use git::{
6 Oid, RunHook,
7 blame::Blame,
8 repository::{
9 AskPassDelegate, Branch, CommitDetails, CommitOptions, FetchOptions, GitRepository,
10 GitRepositoryCheckpoint, PushOptions, Remote, RepoPath, ResetMode, Worktree,
11 },
12 status::{
13 DiffTreeType, FileStatus, GitStatus, StatusCode, TrackedStatus, TreeDiff, TreeDiffStatus,
14 UnmergedStatus,
15 },
16};
17use gpui::{AsyncApp, BackgroundExecutor, SharedString, Task, TaskLabel};
18use ignore::gitignore::GitignoreBuilder;
19use parking_lot::Mutex;
20use rope::Rope;
21use smol::future::FutureExt as _;
22use std::{
23 path::PathBuf,
24 sync::{Arc, LazyLock},
25};
26use util::{paths::PathStyle, rel_path::RelPath};
27
28pub static LOAD_INDEX_TEXT_TASK: LazyLock<TaskLabel> = LazyLock::new(TaskLabel::new);
29pub static LOAD_HEAD_TEXT_TASK: LazyLock<TaskLabel> = LazyLock::new(TaskLabel::new);
30
31#[derive(Clone)]
32pub struct FakeGitRepository {
33 pub(crate) fs: Arc<FakeFs>,
34 pub(crate) checkpoints: Arc<Mutex<HashMap<Oid, FakeFsEntry>>>,
35 pub(crate) executor: BackgroundExecutor,
36 pub(crate) dot_git_path: PathBuf,
37 pub(crate) repository_dir_path: PathBuf,
38 pub(crate) common_dir_path: PathBuf,
39}
40
41#[derive(Debug, Clone)]
42pub struct FakeGitRepositoryState {
43 pub event_emitter: smol::channel::Sender<PathBuf>,
44 pub unmerged_paths: HashMap<RepoPath, UnmergedStatus>,
45 pub head_contents: HashMap<RepoPath, String>,
46 pub index_contents: HashMap<RepoPath, String>,
47 // everything in commit contents is in oids
48 pub merge_base_contents: HashMap<RepoPath, Oid>,
49 pub oids: HashMap<Oid, String>,
50 pub blames: HashMap<RepoPath, Blame>,
51 pub current_branch_name: Option<String>,
52 pub branches: HashSet<String>,
53 pub simulated_index_write_error_message: Option<String>,
54 pub refs: HashMap<String, String>,
55}
56
57impl FakeGitRepositoryState {
58 pub fn new(event_emitter: smol::channel::Sender<PathBuf>) -> Self {
59 FakeGitRepositoryState {
60 event_emitter,
61 head_contents: Default::default(),
62 index_contents: Default::default(),
63 unmerged_paths: Default::default(),
64 blames: Default::default(),
65 current_branch_name: Default::default(),
66 branches: Default::default(),
67 simulated_index_write_error_message: Default::default(),
68 refs: HashMap::from_iter([("HEAD".into(), "abc".into())]),
69 merge_base_contents: Default::default(),
70 oids: Default::default(),
71 }
72 }
73}
74
75impl FakeGitRepository {
76 fn with_state_async<F, T>(&self, write: bool, f: F) -> BoxFuture<'static, Result<T>>
77 where
78 F: 'static + Send + FnOnce(&mut FakeGitRepositoryState) -> Result<T>,
79 T: Send,
80 {
81 let fs = self.fs.clone();
82 let executor = self.executor.clone();
83 let dot_git_path = self.dot_git_path.clone();
84 async move {
85 executor.simulate_random_delay().await;
86 fs.with_git_state(&dot_git_path, write, f)?
87 }
88 .boxed()
89 }
90}
91
92impl GitRepository for FakeGitRepository {
93 fn reload_index(&self) {}
94
95 fn load_index_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>> {
96 let fut = self.with_state_async(false, move |state| {
97 state
98 .index_contents
99 .get(&path)
100 .context("not present in index")
101 .cloned()
102 });
103 self.executor
104 .spawn_labeled(*LOAD_INDEX_TEXT_TASK, async move { fut.await.ok() })
105 .boxed()
106 }
107
108 fn load_committed_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>> {
109 let fut = self.with_state_async(false, move |state| {
110 state
111 .head_contents
112 .get(&path)
113 .context("not present in HEAD")
114 .cloned()
115 });
116 self.executor
117 .spawn_labeled(*LOAD_HEAD_TEXT_TASK, async move { fut.await.ok() })
118 .boxed()
119 }
120
121 fn load_blob_content(&self, oid: git::Oid) -> BoxFuture<'_, Result<String>> {
122 self.with_state_async(false, move |state| {
123 state.oids.get(&oid).cloned().context("oid does not exist")
124 })
125 .boxed()
126 }
127
128 fn load_commit(
129 &self,
130 _commit: String,
131 _cx: AsyncApp,
132 ) -> BoxFuture<'_, Result<git::repository::CommitDiff>> {
133 unimplemented!()
134 }
135
136 fn set_index_text(
137 &self,
138 path: RepoPath,
139 content: Option<String>,
140 _env: Arc<HashMap<String, String>>,
141 _is_executable: bool,
142 ) -> BoxFuture<'_, anyhow::Result<()>> {
143 self.with_state_async(true, move |state| {
144 if let Some(message) = &state.simulated_index_write_error_message {
145 anyhow::bail!("{message}");
146 } else if let Some(content) = content {
147 state.index_contents.insert(path, content);
148 } else {
149 state.index_contents.remove(&path);
150 }
151 Ok(())
152 })
153 }
154
155 fn remote_url(&self, _name: &str) -> Option<String> {
156 None
157 }
158
159 fn diff_tree(&self, _request: DiffTreeType) -> BoxFuture<'_, Result<TreeDiff>> {
160 let mut entries = HashMap::default();
161 self.with_state_async(false, |state| {
162 for (path, content) in &state.head_contents {
163 let status = if let Some((oid, original)) = state
164 .merge_base_contents
165 .get(path)
166 .map(|oid| (oid, &state.oids[oid]))
167 {
168 if original == content {
169 continue;
170 }
171 TreeDiffStatus::Modified { old: *oid }
172 } else {
173 TreeDiffStatus::Added
174 };
175 entries.insert(path.clone(), status);
176 }
177 for (path, oid) in &state.merge_base_contents {
178 if !entries.contains_key(path) {
179 entries.insert(path.clone(), TreeDiffStatus::Deleted { old: *oid });
180 }
181 }
182 Ok(TreeDiff { entries })
183 })
184 .boxed()
185 }
186
187 fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<'_, Result<Vec<Option<String>>>> {
188 self.with_state_async(false, |state| {
189 Ok(revs
190 .into_iter()
191 .map(|rev| state.refs.get(&rev).cloned())
192 .collect())
193 })
194 }
195
196 fn show(&self, commit: String) -> BoxFuture<'_, Result<CommitDetails>> {
197 async {
198 Ok(CommitDetails {
199 sha: commit.into(),
200 ..Default::default()
201 })
202 }
203 .boxed()
204 }
205
206 fn reset(
207 &self,
208 _commit: String,
209 _mode: ResetMode,
210 _env: Arc<HashMap<String, String>>,
211 ) -> BoxFuture<'_, Result<()>> {
212 unimplemented!()
213 }
214
215 fn checkout_files(
216 &self,
217 _commit: String,
218 _paths: Vec<RepoPath>,
219 _env: Arc<HashMap<String, String>>,
220 ) -> BoxFuture<'_, Result<()>> {
221 unimplemented!()
222 }
223
224 fn path(&self) -> PathBuf {
225 self.repository_dir_path.clone()
226 }
227
228 fn main_repository_path(&self) -> PathBuf {
229 self.common_dir_path.clone()
230 }
231
232 fn merge_message(&self) -> BoxFuture<'_, Option<String>> {
233 async move { None }.boxed()
234 }
235
236 fn status(&self, path_prefixes: &[RepoPath]) -> Task<Result<GitStatus>> {
237 let workdir_path = self.dot_git_path.parent().unwrap();
238
239 // Load gitignores
240 let ignores = workdir_path
241 .ancestors()
242 .filter_map(|dir| {
243 let ignore_path = dir.join(".gitignore");
244 let content = self.fs.read_file_sync(ignore_path).ok()?;
245 let content = String::from_utf8(content).ok()?;
246 let mut builder = GitignoreBuilder::new(dir);
247 for line in content.lines() {
248 builder.add_line(Some(dir.into()), line).ok()?;
249 }
250 builder.build().ok()
251 })
252 .collect::<Vec<_>>();
253
254 // Load working copy files.
255 let git_files: HashMap<RepoPath, (String, bool)> = self
256 .fs
257 .files()
258 .iter()
259 .filter_map(|path| {
260 // TODO better simulate git status output in the case of submodules and worktrees
261 let repo_path = path.strip_prefix(workdir_path).ok()?;
262 let mut is_ignored = repo_path.starts_with(".git");
263 for ignore in &ignores {
264 match ignore.matched_path_or_any_parents(path, false) {
265 ignore::Match::None => {}
266 ignore::Match::Ignore(_) => is_ignored = true,
267 ignore::Match::Whitelist(_) => break,
268 }
269 }
270 let content = self
271 .fs
272 .read_file_sync(path)
273 .ok()
274 .map(|content| String::from_utf8(content).unwrap())?;
275 let repo_path = RelPath::new(repo_path, PathStyle::local()).ok()?;
276 Some((RepoPath::from_rel_path(&repo_path), (content, is_ignored)))
277 })
278 .collect();
279
280 let result = self.fs.with_git_state(&self.dot_git_path, false, |state| {
281 let mut entries = Vec::new();
282 let paths = state
283 .head_contents
284 .keys()
285 .chain(state.index_contents.keys())
286 .chain(git_files.keys())
287 .collect::<HashSet<_>>();
288 for path in paths {
289 if !path_prefixes.iter().any(|prefix| path.starts_with(prefix)) {
290 continue;
291 }
292
293 let head = state.head_contents.get(path);
294 let index = state.index_contents.get(path);
295 let unmerged = state.unmerged_paths.get(path);
296 let fs = git_files.get(path);
297 let status = match (unmerged, head, index, fs) {
298 (Some(unmerged), _, _, _) => FileStatus::Unmerged(*unmerged),
299 (_, Some(head), Some(index), Some((fs, _))) => {
300 FileStatus::Tracked(TrackedStatus {
301 index_status: if head == index {
302 StatusCode::Unmodified
303 } else {
304 StatusCode::Modified
305 },
306 worktree_status: if fs == index {
307 StatusCode::Unmodified
308 } else {
309 StatusCode::Modified
310 },
311 })
312 }
313 (_, Some(head), Some(index), None) => FileStatus::Tracked(TrackedStatus {
314 index_status: if head == index {
315 StatusCode::Unmodified
316 } else {
317 StatusCode::Modified
318 },
319 worktree_status: StatusCode::Deleted,
320 }),
321 (_, Some(_), None, Some(_)) => FileStatus::Tracked(TrackedStatus {
322 index_status: StatusCode::Deleted,
323 worktree_status: StatusCode::Added,
324 }),
325 (_, Some(_), None, None) => FileStatus::Tracked(TrackedStatus {
326 index_status: StatusCode::Deleted,
327 worktree_status: StatusCode::Deleted,
328 }),
329 (_, None, Some(index), Some((fs, _))) => FileStatus::Tracked(TrackedStatus {
330 index_status: StatusCode::Added,
331 worktree_status: if fs == index {
332 StatusCode::Unmodified
333 } else {
334 StatusCode::Modified
335 },
336 }),
337 (_, None, Some(_), None) => FileStatus::Tracked(TrackedStatus {
338 index_status: StatusCode::Added,
339 worktree_status: StatusCode::Deleted,
340 }),
341 (_, None, None, Some((_, is_ignored))) => {
342 if *is_ignored {
343 continue;
344 }
345 FileStatus::Untracked
346 }
347 (_, None, None, None) => {
348 unreachable!();
349 }
350 };
351 if status
352 != FileStatus::Tracked(TrackedStatus {
353 index_status: StatusCode::Unmodified,
354 worktree_status: StatusCode::Unmodified,
355 })
356 {
357 entries.push((path.clone(), status));
358 }
359 }
360 entries.sort_by(|a, b| a.0.cmp(&b.0));
361 anyhow::Ok(GitStatus {
362 entries: entries.into(),
363 })
364 });
365 Task::ready(match result {
366 Ok(result) => result,
367 Err(e) => Err(e),
368 })
369 }
370
371 fn stash_entries(&self) -> BoxFuture<'_, Result<git::stash::GitStash>> {
372 async { Ok(git::stash::GitStash::default()) }.boxed()
373 }
374
375 fn branches(&self) -> BoxFuture<'_, Result<Vec<Branch>>> {
376 self.with_state_async(false, move |state| {
377 let current_branch = &state.current_branch_name;
378 Ok(state
379 .branches
380 .iter()
381 .map(|branch_name| Branch {
382 is_head: Some(branch_name) == current_branch.as_ref(),
383 ref_name: branch_name.into(),
384 most_recent_commit: None,
385 upstream: None,
386 })
387 .collect())
388 })
389 }
390
391 fn worktrees(&self) -> BoxFuture<'_, Result<Vec<Worktree>>> {
392 unimplemented!()
393 }
394
395 fn create_worktree(
396 &self,
397 _: String,
398 _: PathBuf,
399 _: Option<String>,
400 ) -> BoxFuture<'_, Result<()>> {
401 unimplemented!()
402 }
403
404 fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
405 self.with_state_async(true, |state| {
406 state.current_branch_name = Some(name);
407 Ok(())
408 })
409 }
410
411 fn create_branch(
412 &self,
413 name: String,
414 _base_branch: Option<String>,
415 ) -> BoxFuture<'_, Result<()>> {
416 self.with_state_async(true, move |state| {
417 state.branches.insert(name);
418 Ok(())
419 })
420 }
421
422 fn rename_branch(&self, branch: String, new_name: String) -> BoxFuture<'_, Result<()>> {
423 self.with_state_async(true, move |state| {
424 if !state.branches.remove(&branch) {
425 bail!("no such branch: {branch}");
426 }
427 state.branches.insert(new_name.clone());
428 if state.current_branch_name == Some(branch) {
429 state.current_branch_name = Some(new_name);
430 }
431 Ok(())
432 })
433 }
434
435 fn delete_branch(&self, _name: String) -> BoxFuture<'_, Result<()>> {
436 unimplemented!()
437 }
438
439 fn blame(&self, path: RepoPath, _content: Rope) -> BoxFuture<'_, Result<git::blame::Blame>> {
440 self.with_state_async(false, move |state| {
441 state
442 .blames
443 .get(&path)
444 .with_context(|| format!("failed to get blame for {:?}", path))
445 .cloned()
446 })
447 }
448
449 fn file_history(&self, path: RepoPath) -> BoxFuture<'_, Result<git::repository::FileHistory>> {
450 self.file_history_paginated(path, 0, None)
451 }
452
453 fn file_history_paginated(
454 &self,
455 path: RepoPath,
456 _skip: usize,
457 _limit: Option<usize>,
458 ) -> BoxFuture<'_, Result<git::repository::FileHistory>> {
459 async move {
460 Ok(git::repository::FileHistory {
461 entries: Vec::new(),
462 path,
463 })
464 }
465 .boxed()
466 }
467
468 fn stage_paths(
469 &self,
470 paths: Vec<RepoPath>,
471 _env: Arc<HashMap<String, String>>,
472 ) -> BoxFuture<'_, Result<()>> {
473 Box::pin(async move {
474 let contents = paths
475 .into_iter()
476 .map(|path| {
477 let abs_path = self
478 .dot_git_path
479 .parent()
480 .unwrap()
481 .join(&path.as_std_path());
482 Box::pin(async move { (path.clone(), self.fs.load(&abs_path).await.ok()) })
483 })
484 .collect::<Vec<_>>();
485 let contents = join_all(contents).await;
486 self.with_state_async(true, move |state| {
487 for (path, content) in contents {
488 if let Some(content) = content {
489 state.index_contents.insert(path, content);
490 } else {
491 state.index_contents.remove(&path);
492 }
493 }
494 Ok(())
495 })
496 .await
497 })
498 }
499
500 fn unstage_paths(
501 &self,
502 paths: Vec<RepoPath>,
503 _env: Arc<HashMap<String, String>>,
504 ) -> BoxFuture<'_, Result<()>> {
505 self.with_state_async(true, move |state| {
506 for path in paths {
507 match state.head_contents.get(&path) {
508 Some(content) => state.index_contents.insert(path, content.clone()),
509 None => state.index_contents.remove(&path),
510 };
511 }
512 Ok(())
513 })
514 }
515
516 fn stash_paths(
517 &self,
518 _paths: Vec<RepoPath>,
519 _env: Arc<HashMap<String, String>>,
520 ) -> BoxFuture<'_, Result<()>> {
521 unimplemented!()
522 }
523
524 fn stash_pop(
525 &self,
526 _index: Option<usize>,
527 _env: Arc<HashMap<String, String>>,
528 ) -> BoxFuture<'_, Result<()>> {
529 unimplemented!()
530 }
531
532 fn stash_apply(
533 &self,
534 _index: Option<usize>,
535 _env: Arc<HashMap<String, String>>,
536 ) -> BoxFuture<'_, Result<()>> {
537 unimplemented!()
538 }
539
540 fn stash_drop(
541 &self,
542 _index: Option<usize>,
543 _env: Arc<HashMap<String, String>>,
544 ) -> BoxFuture<'_, Result<()>> {
545 unimplemented!()
546 }
547
548 fn commit(
549 &self,
550 _message: gpui::SharedString,
551 _name_and_email: Option<(gpui::SharedString, gpui::SharedString)>,
552 _options: CommitOptions,
553 _askpass: AskPassDelegate,
554 _env: Arc<HashMap<String, String>>,
555 ) -> BoxFuture<'_, Result<()>> {
556 unimplemented!()
557 }
558
559 fn run_hook(
560 &self,
561 _hook: RunHook,
562 _env: Arc<HashMap<String, String>>,
563 ) -> BoxFuture<'_, Result<()>> {
564 unimplemented!()
565 }
566
567 fn push(
568 &self,
569 _branch: String,
570 _remote: String,
571 _options: Option<PushOptions>,
572 _askpass: AskPassDelegate,
573 _env: Arc<HashMap<String, String>>,
574 _cx: AsyncApp,
575 ) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
576 unimplemented!()
577 }
578
579 fn pull(
580 &self,
581 _branch: Option<String>,
582 _remote: String,
583 _rebase: bool,
584 _askpass: AskPassDelegate,
585 _env: Arc<HashMap<String, String>>,
586 _cx: AsyncApp,
587 ) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
588 unimplemented!()
589 }
590
591 fn fetch(
592 &self,
593 _fetch_options: FetchOptions,
594 _askpass: AskPassDelegate,
595 _env: Arc<HashMap<String, String>>,
596 _cx: AsyncApp,
597 ) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
598 unimplemented!()
599 }
600
601 fn get_push_remote(&self, _branch: String) -> BoxFuture<'_, Result<Option<Remote>>> {
602 unimplemented!()
603 }
604
605 fn get_branch_remote(&self, _branch: String) -> BoxFuture<'_, Result<Option<Remote>>> {
606 unimplemented!()
607 }
608
609 fn get_all_remotes(&self) -> BoxFuture<'_, Result<Vec<Remote>>> {
610 unimplemented!()
611 }
612
613 fn check_for_pushed_commit(&self) -> BoxFuture<'_, Result<Vec<gpui::SharedString>>> {
614 future::ready(Ok(Vec::new())).boxed()
615 }
616
617 fn diff(&self, _diff: git::repository::DiffType) -> BoxFuture<'_, Result<String>> {
618 unimplemented!()
619 }
620
621 fn checkpoint(&self) -> BoxFuture<'static, Result<GitRepositoryCheckpoint>> {
622 let executor = self.executor.clone();
623 let fs = self.fs.clone();
624 let checkpoints = self.checkpoints.clone();
625 let repository_dir_path = self.repository_dir_path.parent().unwrap().to_path_buf();
626 async move {
627 executor.simulate_random_delay().await;
628 let oid = git::Oid::random(&mut executor.rng());
629 let entry = fs.entry(&repository_dir_path)?;
630 checkpoints.lock().insert(oid, entry);
631 Ok(GitRepositoryCheckpoint { commit_sha: oid })
632 }
633 .boxed()
634 }
635
636 fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<'_, Result<()>> {
637 let executor = self.executor.clone();
638 let fs = self.fs.clone();
639 let checkpoints = self.checkpoints.clone();
640 let repository_dir_path = self.repository_dir_path.parent().unwrap().to_path_buf();
641 async move {
642 executor.simulate_random_delay().await;
643 let checkpoints = checkpoints.lock();
644 let entry = checkpoints
645 .get(&checkpoint.commit_sha)
646 .context(format!("invalid checkpoint: {}", checkpoint.commit_sha))?;
647 fs.insert_entry(&repository_dir_path, entry.clone())?;
648 Ok(())
649 }
650 .boxed()
651 }
652
653 fn compare_checkpoints(
654 &self,
655 left: GitRepositoryCheckpoint,
656 right: GitRepositoryCheckpoint,
657 ) -> BoxFuture<'_, Result<bool>> {
658 let executor = self.executor.clone();
659 let checkpoints = self.checkpoints.clone();
660 async move {
661 executor.simulate_random_delay().await;
662 let checkpoints = checkpoints.lock();
663 let left = checkpoints
664 .get(&left.commit_sha)
665 .context(format!("invalid left checkpoint: {}", left.commit_sha))?;
666 let right = checkpoints
667 .get(&right.commit_sha)
668 .context(format!("invalid right checkpoint: {}", right.commit_sha))?;
669
670 Ok(left == right)
671 }
672 .boxed()
673 }
674
675 fn diff_checkpoints(
676 &self,
677 _base_checkpoint: GitRepositoryCheckpoint,
678 _target_checkpoint: GitRepositoryCheckpoint,
679 ) -> BoxFuture<'_, Result<String>> {
680 unimplemented!()
681 }
682
683 fn default_branch(&self) -> BoxFuture<'_, Result<Option<SharedString>>> {
684 async { Ok(Some("main".into())) }.boxed()
685 }
686}
687
688#[cfg(test)]
689mod tests {
690 use crate::{FakeFs, Fs};
691 use gpui::BackgroundExecutor;
692 use serde_json::json;
693 use std::path::Path;
694 use util::path;
695
696 #[gpui::test]
697 async fn test_checkpoints(executor: BackgroundExecutor) {
698 let fs = FakeFs::new(executor);
699 fs.insert_tree(
700 path!("/"),
701 json!({
702 "bar": {
703 "baz": "qux"
704 },
705 "foo": {
706 ".git": {},
707 "a": "lorem",
708 "b": "ipsum",
709 },
710 }),
711 )
712 .await;
713 fs.with_git_state(Path::new("/foo/.git"), true, |_git| {})
714 .unwrap();
715 let repository = fs
716 .open_repo(Path::new("/foo/.git"), Some("git".as_ref()))
717 .unwrap();
718
719 let checkpoint_1 = repository.checkpoint().await.unwrap();
720 fs.write(Path::new("/foo/b"), b"IPSUM").await.unwrap();
721 fs.write(Path::new("/foo/c"), b"dolor").await.unwrap();
722 let checkpoint_2 = repository.checkpoint().await.unwrap();
723 let checkpoint_3 = repository.checkpoint().await.unwrap();
724
725 assert!(
726 repository
727 .compare_checkpoints(checkpoint_2.clone(), checkpoint_3.clone())
728 .await
729 .unwrap()
730 );
731 assert!(
732 !repository
733 .compare_checkpoints(checkpoint_1.clone(), checkpoint_2.clone())
734 .await
735 .unwrap()
736 );
737
738 repository.restore_checkpoint(checkpoint_1).await.unwrap();
739 assert_eq!(
740 fs.files_with_contents(Path::new("")),
741 [
742 (Path::new(path!("/bar/baz")).into(), b"qux".into()),
743 (Path::new(path!("/foo/a")).into(), b"lorem".into()),
744 (Path::new(path!("/foo/b")).into(), b"ipsum".into())
745 ]
746 );
747 }
748}