diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..5205d69 --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,6 @@ +### unreleased + +- First release of `virtfs`, exposing the modules `Path`, for + abstracting file paths, `Tree` for describing an abstract file tree, + and `Tree.Simple`, which is a very simple version of a file system + (where the contents of files are strings). diff --git a/README.md b/README.md index 3778732..23bd1c4 100644 --- a/README.md +++ b/README.md @@ -23,3 +23,9 @@ single compact dependency, without dependencies (and without using the it easy to use in a [Js_of_ocaml](https://ocsigen.org/js_of_ocaml/latest/manual/overview) programme). + +The library offers a `Path` module for constructing globally portable +and resolvable file paths. A `Tree` module for describing a file tree, +and `Tree.Simple`, which mimics (approximately) the behaviour of a +very limited Unix file system (with only the modification date, +`mtime`, as metadata). diff --git a/lib/dune b/lib/dune index 4bd457a..1f611c9 100644 --- a/lib/dune +++ b/lib/dune @@ -1,6 +1,7 @@ (library (name virtfs) - (public_name virtfs)) + (public_name virtfs) + (modules_without_implementation virtfs)) (mdx (files *.mli) diff --git a/lib/tree.ml b/lib/tree.ml index 9beb709..c7a5666 100644 --- a/lib/tree.ml +++ b/lib/tree.ml @@ -281,7 +281,7 @@ let cat ~to_string fs path = | Some (File { content; _ }) -> to_string content ;; -module Dummy = struct +module Simple = struct (* NOTE: A very minimal implementation of a file system that shares some naive characteristics with Unix. As the purpose is to be used primarily for testing, its support is fairly basic. *) @@ -293,28 +293,210 @@ module Dummy = struct type nonrec item = (content, metadata) item type nonrec t = (content, metadata) t - let dummy_clock x _ = x + type error = + | Mkdir of Path.t * string + | Stat of Path.t * string + | Write_file of Path.t * string + | Read_file of Path.t * string + | Read_dir of Path.t * string + | Remove of Path.t * string + + exception Simple_error of error + + let err_file_exists = "File exists" + let err_no_such_target = "No such file or directory" + let err_is_file = "Is a file" + let err_is_directory = "Is a directory" + let err_overriden = "Cannot be overridden" + let err_not_empty = "Directory not empty" + + let error_s path prim err reason = + prim ^ ": " ^ err ^ " '" ^ Path.to_string path ^ "': " ^ reason + ;; + + let raise_error error = raise (Simple_error error) + let const_clock x _ = x - let mount ?(clock = dummy_clock 1.0) ~scope children = + let mount ?(clock = const_clock 1.0) ~scope children = make ~scope_metadata:(fun path -> Some { mtime = clock path }) ~scope children ;; - let file ?(clock = dummy_clock 1.0) ~name content = + let file ?(clock = const_clock 1.0) ~name content = file ~metadata:{ mtime = clock (name, content) } ~name content ;; - let dir ?(clock = dummy_clock 1.0) ~name children = + let dir ?(clock = const_clock 1.0) ~name children = dir ~metadata:{ mtime = clock name } ~name children ;; - let mtime item = - item - |> metadata - |> Option.fold - ~none:0.0 (* OKAY: having [0.0] as a default result seems ok. *) - ~some:(fun { mtime } -> mtime) + let error_to_string = function + | Mkdir (p, reason) -> error_s p "mkdir" "cannot create directory" reason + | Stat (p, reason) -> error_s p "stat" "cannot statx" reason + | Write_file (p, reason) -> + error_s p "create_file" "cannot create file" reason + | Read_file (p, reason) -> error_s p "read_file" "cannot read file" reason + | Read_dir (p, reason) -> + error_s p "read_dir" "cannot read directory" reason + | Remove (p, reason) -> error_s p "rm" "cannot remove" reason + ;; + + let run ?(finalizer = fun _ -> ()) callback = + try finalizer (callback ()) with + | Simple_error err -> err |> error_to_string |> prerr_endline + ;; + + let create_dir ?(clock = const_clock 1.0) ~path fs = + let dname = Path.dirname path in + match fetch ~path:dname fs, fetch ~path fs with + | Some _, None -> + update + ~path + (fun ~previous:_ ~path -> + let bname = Path.basename path in + let item = dir ~clock ~name:bname [] in + Some item) + fs + | _, Some _ -> raise_error (Mkdir (path, err_file_exists)) + | None, _ -> raise_error (Mkdir (path, err_no_such_target)) + ;; + + let create_dir_rec ?(clock = const_clock 1.0) ~path fs = + let rec aux path fs = + let file = fetch ~path fs in + match file with + | Some (File _) -> raise_error (Mkdir (path, err_file_exists)) + | Some (Directory _) -> fs + | None -> + let p = Path.dirname path in + let fs = aux p fs in + create_dir ~clock ~path fs + in + aux path fs + ;; + + let mkdir ?(recursive = false) ?(clock = const_clock 1.0) ~path fs = + if recursive + then create_dir_rec ~clock ~path fs + else create_dir ~clock ~path fs + ;; + + let mtime ~path fs = + match fetch ~path fs with + | Some item -> + item + |> metadata + |> Option.fold + ~none:0.0 (* OKAY: having [0.0] as a default result seems ok. *) + ~some:(fun { mtime } -> mtime) + | _ -> raise_error (Stat (path, err_no_such_target)) + ;; + + let write_file + ?(overwrite = false) + ?(clock = const_clock 1.0) + ~path + content + fs + = + let parent = Path.dirname path in + match fetch ~path:parent fs, fetch ~path fs with + | None, _ -> raise_error (Write_file (path, err_no_such_target)) + | Some _, Some (Directory _) -> + raise_error (Write_file (path, err_is_directory)) + | Some _, Some (File _) when not overwrite -> + raise_error (Write_file (path, err_overriden)) + | Some _, (Some _ | None) -> + update + ~path + (fun ~previous:_ ~path -> + let name = Path.basename path in + Some (file ~clock ~name content)) + fs + ;; + + let read_file ~path fs = + match fetch ~path fs with + | None -> raise_error (Read_file (path, err_no_such_target)) + | Some (Directory _) -> raise_error (Read_file (path, err_is_directory)) + | Some (File { content; _ }) -> content + ;; + + let file_exists ~path fs = + match fetch ~path fs with + | None -> false + | Some _ -> true + ;; + + let is_directory ~path fs = + match fetch ~path fs with + | None -> false + | Some item -> is_directory item + ;; + + let is_file ~path fs = + match fetch ~path fs with + | None -> false + | Some item -> is_file item + ;; + + let read_dir ~path fs = + match fetch ~path fs with + | None -> raise_error (Read_dir (path, err_no_such_target)) + | Some (File _) -> raise_error (Read_dir (path, err_is_file)) + | Some (Directory { content; _ }) -> + List.fold_left + (fun map elt -> + let key = Path.(path / name elt) in + Path.Map.add key elt map) + Path.Map.empty + content + ;; + + let is_empty_dir ~path fs = + match fetch ~path fs with + | None -> raise_error (Read_dir (path, err_no_such_target)) + | Some (File _) -> raise_error (Read_dir (path, err_is_file)) + | Some (Directory { content = []; _ }) -> true + | Some (Directory _) -> false + ;; + + let rm_file ~path fs = + match fetch ~path fs with + | None -> raise_error (Remove (path, err_no_such_target)) + | Some (Directory _) -> raise_error (Remove (path, err_is_directory)) + | Some (File _) -> rm_file ~path fs + ;; + + let generic_rm_dir = rm_dir + + let rec rm_dir ?(recursive = false) ~path fs = + if recursive + then ( + let rec aux path fs = + if is_file ~path fs + then rm_file ~path fs + else if is_empty_dir ~path fs + then rm_dir ~path fs + else ( + let fs = + Path.Map.fold (fun path _ fs -> aux path fs) (read_dir ~path fs) fs + in + rm_dir ~path fs) + in + aux path fs) + else if is_empty_dir ~path fs + then generic_rm_dir ~path fs + else raise_error (Remove (path, err_not_empty)) + ;; + + let rm ?(recursive = false) ~path fs = + if is_file ~path fs + then rm_file ~path fs + else if is_directory ~path fs + then rm_dir ~recursive ~path fs + else raise_error (Remove (path, err_no_such_target)) ;; end diff --git a/lib/tree.mli b/lib/tree.mli index eacfbbe..c5e5bc8 100644 --- a/lib/tree.mli +++ b/lib/tree.mli @@ -124,14 +124,15 @@ val touch (** [rm ~path fs] remove the item by a given [path]. *) val rm : path:Path.t -> ('a, 'metadata) t -> ('a, 'metadata) t -(** [rm_file fs path] remove the file by a given [path]. *) +(** [rm_file ~path fs] remove the file by a given [path]. *) val rm_file : path:Path.t -> ('a, 'metadata) t -> ('a, 'metadata) t -(** [rm_dir fs path] remove the directory by a given [path]. *) +(** [rm_dir ~path fs] remove the directory by a given [path]. *) val rm_dir : path:Path.t -> ('a, 'metadata) t -> ('a, 'metadata) t -(** [mv fs ~target ~source:p] move [p] as [target]. If the [target] exists, or - the given [p] does not exists, [fs] remains unchanged. *) +(** [mv fs ~target ~source] move [source] as [target]. If the [target] + exists, or the given [source] does not exists, [fs] remains + unchanged. *) val mv : target:Path.t -> source:Path.t @@ -158,12 +159,15 @@ val cat : to_string:('a -> string) -> ('a, 'metadata) t -> Path.t -> string (** {1 A Dummy File System Implementation} The implementation is not abstract, which allows generic functions - to be used on a [Dummy] tree. *) + to be used on a [Simple] tree (mostly used for tests). *) -module Dummy : sig +module Simple : sig (** A truly {b very naive} implementation of a file system where the contents of files are strings and their metadata only associates - modification dates. *) + modification dates. + + The API throws {!exception:Simple_error} exceptions to mimic + Unix behaviour. *) (** {1 Types} *) @@ -188,11 +192,24 @@ module Dummy : sig (** Items of the file system. *) type nonrec t = (content, metadata) t + (** {2 Error handling} + + The API relies on exceptions to describe failures. Each function + that may fail throws the [Dummy_tree] exception. *) + + (** Set of all possible errors. *) + type error + + exception Simple_error of error + + (** Render an error as an Unix-like error message. *) + val error_to_string : error -> string + (** {1 Tree construction} *) - (** [dummy_clock f] creates a constant clock, always returning + (** [const_clock f] creates a constant clock, always returning [f]. *) - val dummy_clock : float -> 'a clock + val const_clock : float -> 'a clock (** [mount ?clock ~scope children] creates a tree using {!val:make}. *) val mount : ?clock:Path.t clock -> scope:Path.t -> item list -> t @@ -205,6 +222,77 @@ module Dummy : sig parametrized by the [name] of the directory. *) val dir : ?clock:string clock -> name:string -> item list -> item - (** [mtime item] returns the {i modification time} of the given [item]. *) - val mtime : item -> float + (** {1 Tree operation} *) + + (** [mtime ~path fs] returns the {i modification time} of the given + {!type:item} located at the given [path]. *) + val mtime : path:Path.t -> t -> float + + (* [file_exists ~path fs] returns [true] if the file/directory + exists at the given [path] for the given [fs], [false] + otherwise. *) + val file_exists : path:Path.t -> t -> bool + + (* [is_directory ~path fs] returns [true] if the directory + exists at the given [path] for the given [fs], [false] + otherwise (even if the target does not exists). *) + val is_directory : path:Path.t -> t -> bool + + (* [is_file ~path fs] returns [true] if the file + exists at the given [path] for the given [fs], [false] + otherwise (even if the target does not exists). *) + val is_file : path:Path.t -> t -> bool + + (** [is_empty_dir ~path fs] returns [true] if the directory located + at [path] for the given [fs] is an empty directory. *) + val is_empty_dir : path:Path.t -> t -> bool + + (** [mkdir ?recursive ?clock ~path] creates the directory referenced + by the given [path] with behaviour similar to the Unix command + [mkdir] (the [recursive] flag is for [mkdir -p], default is + [false]). *) + val mkdir + : ?recursive:bool + -> ?clock:(content -> time) + -> path:Path.t + -> t + -> t + + (** [rm ~path fs] remove the item by a given [path] (like + {!val:Tree.rm} but raising exception). *) + val rm : ?recursive:bool -> path:Path.t -> t -> t + + (** [rm_file fs path] remove the file by a given [path] (like + {!val:Tree.rm_file} but raising exception). *) + val rm_file : path:Path.t -> t -> t + + (** [rm_dir fs path] remove the directory by a given [path] (like + {!val:Tree.rm_dir} but raising exception). *) + val rm_dir : ?recursive:bool -> path:Path.t -> t -> t + + (** [write_file ?overwrite ?clock ~path content fs] creates (or + overwrites, depending on the [overwrite] flag, default [false]) + the file [path] with content [content] on the given [fs].*) + val write_file + : ?overwrite:bool + -> ?clock:(string * content -> time) + -> path:Path.t + -> string + -> t + -> t + + (** [read_file ~path fs] Reads the contents of the file referenced + by its [path] for a given [fs]. *) + val read_file : path:Path.t -> t -> string + + (** [read_dir ~path fs] returns the direct children of the directory + passed as an argument (in the form of a map of {{!type:item} + items} indexed by {{!type:Path.t} Paths}).*) + val read_dir : path:Path.t -> t -> item Path.Map.t + + (** {1 Misc} *) + + (** [run ?finalizer callback] runs [callback] and print errors on + [stderr]. *) + val run : ?finalizer:('a -> unit) -> (unit -> 'a) -> unit end diff --git a/lib/virtfs.mli b/lib/virtfs.mli new file mode 100644 index 0000000..d5c0b03 --- /dev/null +++ b/lib/virtfs.mli @@ -0,0 +1,27 @@ +(* Copyright (c) 2026, Cargocut and the Virtfs developers. + All rights reserved. + + SPDX-License-Identifier: BSD-3-Clause *) + +(** [Virtfs] provides primitives for describing paths in a file system + in an {i abstract} (platform-independent) and {i relatively + portable} manner, while allowing virtual file systems to be easily + mounted to facilitate the writing of unit tests in programmes that + abstract effects and rely on the file system.*) + +(** {1 Abstraction over Path} + + Describes resolvable, portable, and platform-independent file + paths. *) + +module Path = Path + +(** {1 Virtual File System} + + Implementation of an {i in-memory} file system based on + {!module:Path}, which abstracts file contents and metadata. The + module also exposes {!module:Tree.Simple}, which is an opinionated + implementation of a very limited version of a system that mimics + (somewhat) the behaviour of Unix file systems. *) + +module Tree = Tree diff --git a/test/create_read_file_test.ml b/test/create_read_file_test.ml new file mode 100644 index 0000000..2c5e0c1 --- /dev/null +++ b/test/create_read_file_test.ml @@ -0,0 +1,149 @@ +(* Copyright (c) 2026, Cargocut and the Virtfs developers. + All rights reserved. + + SPDX-License-Identifier: BSD-3-Clause *) + +module T = Tree +module U = T.Simple + +let fs = + let open U in + mount + ~scope:Path.root + [ dir + ~name:"1-foo" + [ dir ~name:"bar" [ file ~name:"index.md" "Hello World" ] ] + ; dir + ~name:"2-foo" + [ dir + ~name:"bar" + [ dir ~name:"baz" [ file ~name:"index.md" "Hello World 2" ] ] + ] + ; dir ~name:"3-foo" [ dir ~name:"bar" [] ] + ] +;; + +let finalizer (fs, tm, ct) = + print_endline ("mtime: " ^ Float.to_string tm); + print_endline ct; + fs |> T.tree |> print_endline +;; + +let%expect_test "writing on a directory" = + let clock _ = 5.0 + and path = Path.abs [ "1-foo"; "bar" ] in + U.run + (fun () -> + let fs = fs |> U.write_file ~clock ~path "My new content" in + let tm = U.mtime ~path fs + and ct = U.read_file ~path fs in + fs, tm, ct) + ~finalizer; + [%expect {| create_file: cannot create file '/1-foo/bar': Is a directory |}] +;; + +let%expect_test "writing a file in an invalid path" = + let clock _ = 5.0 + and path = Path.abs [ "4-foo"; "index.md" ] in + U.run + (fun () -> + let fs = fs |> U.write_file ~clock ~path "My new content" in + let tm = U.mtime ~path fs + and ct = U.read_file ~path fs in + fs, tm, ct) + ~finalizer; + [%expect + {| create_file: cannot create file '/4-foo/index.md': No such file or directory |}] +;; + +let%expect_test "writing on an existing file (without overwrite flag)" = + let clock _ = 5.0 + and path = Path.abs [ "1-foo"; "bar"; "index.md" ] in + U.run + (fun () -> + let fs = fs |> U.write_file ~clock ~path "My new content" in + let tm = U.mtime ~path fs + and ct = U.read_file ~path fs in + fs, tm, ct) + ~finalizer; + [%expect + {| create_file: cannot create file '/1-foo/bar/index.md': Cannot be overridden |}] +;; + +let%expect_test "writing on an existing file (with overwrite flag)" = + let clock _ = 5.0 + and path = Path.abs [ "1-foo"; "bar"; "index.md" ] in + U.run + (fun () -> + let fs = + fs |> U.write_file ~overwrite:true ~clock ~path "My new content" + in + let tm = U.mtime ~path fs + and ct = U.read_file ~path fs in + fs, tm, ct) + ~finalizer; + [%expect + {| + mtime: 5. + My new content + + └─/ + └─1-foo/ + └─bar/ + └─index.md + └─2-foo/ + └─bar/ + └─baz/ + └─index.md + └─3-foo/ + └─bar/ + |}] +;; + +let%expect_test "writing on a new file" = + let clock _ = 5.0 + and path = Path.abs [ "1-foo"; "bar"; "index-2.md" ] in + U.run + (fun () -> + let fs = fs |> U.write_file ~clock ~path "My new content" in + let tm = U.mtime ~path fs + and ct = U.read_file ~path fs in + fs, tm, ct) + ~finalizer; + [%expect + {| + mtime: 5. + My new content + + └─/ + └─1-foo/ + └─bar/ + └─index-2.md + └─index.md + └─2-foo/ + └─bar/ + └─baz/ + └─index.md + └─3-foo/ + └─bar/ + |}] +;; + +let%expect_test "read_file on a directory" = + let path = Path.abs [ "1-foo"; "bar" ] in + U.run (fun () -> U.read_file ~path fs) ~finalizer:print_endline; + [%expect {| read_file: cannot read file '/1-foo/bar': Is a directory |}] +;; + +let%expect_test "read_file when not exists" = + let path = Path.abs [ "1-foo"; "bar.md" ] in + U.run (fun () -> U.read_file ~path fs) ~finalizer:print_endline; + [%expect + {| read_file: cannot read file '/1-foo/bar.md': No such file or directory |}] +;; + +let%expect_test "read_file" = + let path = Path.abs [ "1-foo"; "bar"; "index.md" ] in + U.run (fun () -> U.read_file ~path fs) ~finalizer:print_endline; + [%expect {| Hello World |}] +;; diff --git a/test/dune b/test/dune index 36b4afe..2d016b2 100644 --- a/test/dune +++ b/test/dune @@ -9,6 +9,10 @@ path_move_test yocaml_www_resolver tree_core_test - tree_update_test) + tree_update_test + mkdir_test + create_read_file_test + read_dir_test + rm_test) (preprocess (pps ppx_expect))) diff --git a/test/mkdir_test.ml b/test/mkdir_test.ml new file mode 100644 index 0000000..cc2f28a --- /dev/null +++ b/test/mkdir_test.ml @@ -0,0 +1,145 @@ +(* Copyright (c) 2026, Cargocut and the Virtfs developers. + All rights reserved. + + SPDX-License-Identifier: BSD-3-Clause *) + +module T = Tree +module U = T.Simple + +let fs = + let open U in + mount + ~scope:Path.root + [ dir + ~name:"1-foo" + [ dir ~name:"bar" [ file ~name:"index.md" "Hello World" ] ] + ; dir + ~name:"2-foo" + [ dir + ~name:"bar" + [ dir ~name:"baz" [ file ~name:"index.md" "Hello World 2" ] ] + ] + ; dir ~name:"3-foo" [ dir ~name:"bar" [] ] + ] +;; + +let finalizer fs = fs |> T.tree |> print_endline + +let%expect_test "mkdir when nested path does not exists" = + let clock _ = 2.0 in + let path = Path.abs [ "1-foo"; "bar"; "baz"; "a-directory" ] in + U.run ~finalizer (fun () -> fs |> U.mkdir ~clock ~path); + [%expect + {| mkdir: cannot create directory '/1-foo/bar/baz/a-directory': No such file or directory |}] +;; + +let%expect_test "mkdir when target exists" = + let clock _ = 2.0 in + let path = Path.abs [ "1-foo"; "bar" ] in + U.run (fun () -> fs |> U.mkdir ~clock ~path); + [%expect {| mkdir: cannot create directory '/1-foo/bar': File exists |}] +;; + +let%expect_test "mkdir" = + let clock _ = 2.0 in + let path = Path.abs [ "1-foo"; "bar"; "storage" ] in + U.run + (fun () -> + let fs = fs |> U.mkdir ~clock ~path in + let tm = U.mtime ~path fs in + fs, tm) + ~finalizer:(fun (fs, tm) -> + tm |> Float.to_string |> print_endline; + fs |> T.tree |> print_endline); + [%expect + {| + 2. + + └─/ + └─1-foo/ + └─bar/ + └─storage/ + └─index.md + └─2-foo/ + └─bar/ + └─baz/ + └─index.md + └─3-foo/ + └─bar/ + |}] +;; + +let%expect_test "mkdir recursive" = + let clock _ = 5.0 in + let path = Path.abs [ "1-foo"; "bar"; "index.md" ] in + U.run + (fun () -> + let fs = fs |> U.mkdir ~recursive:true ~clock ~path in + let tm = U.mtime ~path fs in + fs, tm) + ~finalizer:(fun (fs, tm) -> + tm |> Float.to_string |> print_endline; + fs |> T.tree |> print_endline); + [%expect + {| mkdir: cannot create directory '/1-foo/bar/index.md': File exists |}] +;; + +let%expect_test "mkdir recursive" = + let clock _ = 5.0 in + let path = Path.abs [ "1-foo"; "bar" ] in + U.run + (fun () -> + let fs = fs |> U.mkdir ~recursive:true ~clock ~path in + let tm = U.mtime ~path fs in + fs, tm) + ~finalizer:(fun (fs, tm) -> + tm |> Float.to_string |> print_endline; + fs |> T.tree |> print_endline); + [%expect + {| + 1. + + └─/ + └─1-foo/ + └─bar/ + └─index.md + └─2-foo/ + └─bar/ + └─baz/ + └─index.md + └─3-foo/ + └─bar/ + |}] +;; + +let%expect_test "mkdir recursive" = + let clock _ = 5.0 in + let path = Path.abs [ "4-foo"; "bar"; "baz"; "storage" ] in + U.run + (fun () -> + let fs = fs |> U.mkdir ~recursive:true ~clock ~path in + let tm = U.mtime ~path fs in + fs, tm) + ~finalizer:(fun (fs, tm) -> + tm |> Float.to_string |> print_endline; + fs |> T.tree |> print_endline); + [%expect + {| + 5. + + └─/ + └─1-foo/ + └─bar/ + └─index.md + └─2-foo/ + └─bar/ + └─baz/ + └─index.md + └─3-foo/ + └─bar/ + └─4-foo/ + └─bar/ + └─baz/ + └─storage/ + |}] +;; diff --git a/test/read_dir_test.ml b/test/read_dir_test.ml new file mode 100644 index 0000000..a9423af --- /dev/null +++ b/test/read_dir_test.ml @@ -0,0 +1,117 @@ +(* Copyright (c) 2026, Cargocut and the Virtfs developers. + All rights reserved. + + SPDX-License-Identifier: BSD-3-Clause *) + +module T = Tree +module U = T.Simple + +let fs = + let open U in + mount + ~scope:Path.root + [ dir + ~name:"1-foo" + [ dir + ~name:"bar" + [ file ~name:"index.md" "Hello World" + ; file ~name:"index-2.md" "Hello World" + ; file ~name:"index-3.md" "Hello World" + ; file ~name:"index-4.md" "Hello World" + ; file ~name:"index-5-final-version.md" "Hello World" + ] + ] + ; dir + ~name:"2-foo" + [ dir + ~name:"bar" + [ dir ~name:"baz" [ file ~name:"index.md" "Hello World 2" ] ] + ] + ; dir ~name:"3-foo" [ dir ~name:"bar" [] ] + ] +;; + +let finalizer = + Path.Map.iter (fun path elt -> + let ps = Path.to_string path in + let es = T.name elt in + ps ^ " -> " ^ es |> print_endline) +;; + +let%expect_test "read_directory without target" = + let path = Path.abs [ "4-foo" ] in + U.run (fun () -> U.read_dir ~path fs) ~finalizer; + [%expect + {| read_dir: cannot read directory '/4-foo': No such file or directory |}] +;; + +let%expect_test "read_directory on a file" = + let path = Path.abs [ "2-foo"; "bar"; "baz"; "index.md" ] in + U.run (fun () -> U.read_dir ~path fs) ~finalizer; + [%expect + {| read_dir: cannot read directory '/2-foo/bar/baz/index.md': Is a file |}] +;; + +let%expect_test "read_directory" = + let path = Path.root in + U.run (fun () -> U.read_dir ~path fs) ~finalizer; + [%expect + {| + /1-foo -> 1-foo/ + /2-foo -> 2-foo/ + /3-foo -> 3-foo/ + |}] +;; + +let%expect_test "read_directory" = + let path = Path.abs [ "1-foo" ] in + U.run (fun () -> U.read_dir ~path fs) ~finalizer; + [%expect {| /1-foo/bar -> bar/ |}] +;; + +let%expect_test "read_directory" = + let path = Path.abs [ "1-foo"; "bar" ] in + U.run (fun () -> U.read_dir ~path fs) ~finalizer; + [%expect + {| + /1-foo/bar/index-2.md -> index-2.md + /1-foo/bar/index-3.md -> index-3.md + /1-foo/bar/index-4.md -> index-4.md + /1-foo/bar/index-5-final-version.md -> index-5-final-version.md + /1-foo/bar/index.md -> index.md + |}] +;; + +let%expect_test "is_empty_dir on absent target" = + let path = Path.abs [ "0-foo" ] in + U.run + (fun () -> U.is_empty_dir ~path fs) + ~finalizer:(fun x -> x |> string_of_bool |> print_endline); + [%expect + {| read_dir: cannot read directory '/0-foo': No such file or directory |}] +;; + +let%expect_test "is_empty_dir on file" = + let path = Path.abs [ "1-foo"; "bar"; "index.md" ] in + U.run + (fun () -> U.is_empty_dir ~path fs) + ~finalizer:(fun x -> x |> string_of_bool |> print_endline); + [%expect + {| read_dir: cannot read directory '/1-foo/bar/index.md': Is a file |}] +;; + +let%expect_test "is_empty_dir on empty dir" = + let path = Path.abs [ "3-foo"; "bar" ] in + U.run + (fun () -> U.is_empty_dir ~path fs) + ~finalizer:(fun x -> x |> string_of_bool |> print_endline); + [%expect {| true |}] +;; + +let%expect_test "is_empty_dir on dir" = + let path = Path.abs [ "1-foo"; "bar" ] in + U.run + (fun () -> U.is_empty_dir ~path fs) + ~finalizer:(fun x -> x |> string_of_bool |> print_endline); + [%expect {| false |}] +;; diff --git a/test/rm_test.ml b/test/rm_test.ml new file mode 100644 index 0000000..09d2376 --- /dev/null +++ b/test/rm_test.ml @@ -0,0 +1,159 @@ +(* Copyright (c) 2026, Cargocut and the Virtfs developers. + All rights reserved. + + SPDX-License-Identifier: BSD-3-Clause *) + +module T = Tree +module U = T.Simple + +let fs = + let open U in + mount + ~scope:Path.root + [ dir + ~name:"1-foo" + [ dir ~name:"bar" [ file ~name:"index.md" "Hello World" ] ] + ; dir + ~name:"2-foo" + [ dir + ~name:"bar" + [ dir ~name:"baz" [ file ~name:"index.md" "Hello World 2" ] ] + ] + ; dir ~name:"3-foo" [ dir ~name:"bar" [] ] + ] +;; + +let finalizer fs = fs |> T.tree |> print_endline + +let%expect_test "rm_file on a directory" = + let path = Path.abs [ "1-foo" ] in + U.run ~finalizer (fun () -> U.rm_file ~path fs); + [%expect {| rm: cannot remove '/1-foo': Is a directory |}] +;; + +let%expect_test "rm_file on absent" = + let path = Path.abs [ "1-foo"; "foo.md" ] in + U.run ~finalizer (fun () -> U.rm_file ~path fs); + [%expect {| rm: cannot remove '/1-foo/foo.md': No such file or directory |}] +;; + +let%expect_test "rm_file on a file" = + let path = Path.abs [ "1-foo"; "bar"; "index.md" ] in + U.run ~finalizer (fun () -> U.rm_file ~path fs); + [%expect + {| + └─/ + └─1-foo/ + └─bar/ + └─2-foo/ + └─bar/ + └─baz/ + └─index.md + └─3-foo/ + └─bar/ + |}] +;; + +let%expect_test "rm_dir on a file" = + let path = Path.abs [ "1-foo"; "bar"; "index.md" ] in + U.run ~finalizer (fun () -> U.rm_dir ~path fs); + [%expect + {| read_dir: cannot read directory '/1-foo/bar/index.md': Is a file |}] +;; + +let%expect_test "rm_dir on absent" = + let path = Path.abs [ "1-foo"; "baz" ] in + U.run ~finalizer (fun () -> U.rm_dir ~path fs); + [%expect + {| read_dir: cannot read directory '/1-foo/baz': No such file or directory |}] +;; + +let%expect_test "rm_dir on an non-empty directory" = + let path = Path.abs [ "1-foo" ] in + U.run ~finalizer (fun () -> U.rm_dir ~path fs); + [%expect {| rm: cannot remove '/1-foo': Directory not empty |}] +;; + +let%expect_test "rm_dir on an empty directory" = + let path = Path.abs [ "3-foo"; "bar" ] in + U.run ~finalizer (fun () -> U.rm_dir ~path fs); + [%expect + {| + └─/ + └─1-foo/ + └─bar/ + └─index.md + └─2-foo/ + └─bar/ + └─baz/ + └─index.md + └─3-foo/ + |}] +;; + +let%expect_test "rm on absent" = + let path = Path.abs [ "3-foo"; "baz" ] in + U.run ~finalizer (fun () -> U.rm ~path fs); + [%expect {| rm: cannot remove '/3-foo/baz': No such file or directory |}] +;; + +let%expect_test "rm on non-empty directory" = + let path = Path.abs [ "3-foo" ] in + U.run ~finalizer (fun () -> U.rm ~path fs); + [%expect {| rm: cannot remove '/3-foo': Directory not empty |}] +;; + +let%expect_test "rm on an empty directory" = + let path = Path.abs [ "3-foo"; "bar" ] in + U.run ~finalizer (fun () -> U.rm ~path fs); + [%expect + {| + └─/ + └─1-foo/ + └─bar/ + └─index.md + └─2-foo/ + └─bar/ + └─baz/ + └─index.md + └─3-foo/ + |}] +;; + +let%expect_test "rm on a file" = + let path = Path.abs [ "1-foo"; "bar"; "index.md" ] in + U.run ~finalizer (fun () -> U.rm ~path fs); + [%expect + {| + └─/ + └─1-foo/ + └─bar/ + └─2-foo/ + └─bar/ + └─baz/ + └─index.md + └─3-foo/ + └─bar/ + |}] +;; + +let%expect_test "rm-dir recursive" = + let path = Path.abs [ "1-foo" ] in + U.run ~finalizer (fun () -> U.rm_dir ~recursive:true ~path fs); + [%expect + {| + └─/ + └─2-foo/ + └─bar/ + └─baz/ + └─index.md + └─3-foo/ + └─bar/ + |}] +;; + +let%expect_test "rm-dir recursive" = + let path = Path.root in + U.run ~finalizer (fun () -> U.rm_dir ~recursive:true ~path fs); + [%expect {| |}] +;;