Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,9 @@ jobs:
matrix:
os:
- ubuntu-latest
- macos-latest
ocaml-compiler:
- ocaml-base-compiler.5.3.0
- 5.2.x
- 5.1.x
- 5
runs-on: ${{ matrix.os }}

steps:
Expand Down
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- Add `Action.remove_residuals` for erasing residuals files (by [xvw](https://xvw.lol))
- Add `Yocaml.Data.Validation.sub_record` for validating a complete structure as a record field (by [xvw](https://xvw.lol))
- Add `Batch.iter_tree` and `Batch.fold_tree` (by [gr-im](https://github.com/gr-im))

#### Yocaml_git

Expand Down
24 changes: 24 additions & 0 deletions lib/core/batch.ml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,27 @@ let fold_files ?where ~state = fold_children ~only:`Files ?where ~state

let fold_directories ?where ~state =
fold_children ~only:`Directories ?where ~state

let fold_tree ?(where = fun _ _ -> true) ~state path action =
let rec aux state path =
fold_children ~only:`Both ~state path (fun path state cache ->
let open Eff in
let* is_file = is_file ~on:`Source path in
if is_file && where `File path then action path state cache
else
let* is_dir = is_directory ~on:`Source path in
if is_dir && where `Directory path then aux state path cache
else return (cache, state))
in
aux state path

let iter_tree ?where path action cache =
let open Eff in
let+ cache, () =
fold_tree ?where ~state:() path
(fun path () cache ->
let+ cache = action path cache in
(cache, ()))
cache
in
cache
20 changes: 20 additions & 0 deletions lib/core/batch.mli
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,26 @@ val iter_files :
?where:(Path.t -> bool) -> Path.t -> (Path.t -> Action.t) -> Action.t
(** [iter_files] is [iter_children ~only:`Files]. *)

val iter_tree :
?where:([ `Directory | `File ] -> Path.t -> bool)
-> Path.t
-> (Path.t -> Action.t)
-> Action.t
(** [iter_tree ?where path action] will apply the [action] passed as an argument
to all files, recursively in the directory tree located at [path]. You can
use the second parameter of the [where] predicate to distinguish whether you
are observing a file or a directory. *)

val fold_tree :
?where:([ `Directory | `File ] -> Path.t -> bool)
-> state:'a
-> Path.t
-> (Path.t -> 'a -> Cache.t -> (Cache.t * 'a) Eff.t)
-> Cache.t
-> (Cache.t * 'a) Eff.t
(** [fold_tree ?where ~state path action] is like {!val:iter_children} but you
can maintain your own additional state. *)

val fold_files :
?where:(Path.t -> bool)
-> state:'a
Expand Down
278 changes: 278 additions & 0 deletions test/yocaml/batch_test.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
(* YOCaml a static blog generator.
Copyright (C) 2025 The Funkyworkers and The YOCaml's developers

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. *)

open Test_lib
module Batch = Yocaml.Batch
module Path = Yocaml.Path

let test_iter_tree_1 =
let open Alcotest in
test_case "iter_tree - without nesting" `Quick (fun () ->
let base_fs =
let open Fs in
from_list
[ dir "." [ dir "content" [ file "a.md" "a"; file "b.md" "b" ] ] ]
in
let trace = Fs.create_trace ~time:10 base_fs in
let program () =
let open Yocaml.Eff in
return Yocaml.Cache.empty
>>= Fs.increase_time_with 1
>>= Batch.iter_tree (Path.rel [ "content" ]) (fun p ->
let into =
p
|> Path.dirname
|> Path.trim ~prefix:(Path.rel [ "content" ])
|> Path.relocate ~into:(Path.rel [ "_www" ])
in
Yocaml.Action.copy_file ~into p)
in
let trace, _cache = Fs.run ~trace program () in
let computed_file_system = Fs.trace_system trace in
let expected_file_system =
let open Fs in
from_list
[
dir "."
[
dir "content" [ file "a.md" "a"; file "b.md" "b" ]
; dir "_www"
[ file ~mtime:11 "a.md" "a"; file ~mtime:11 "b.md" "b" ]
]
]
in
check Testable.fs "should be equal" expected_file_system
computed_file_system)

let test_iter_tree_2 =
let open Alcotest in
test_case "iter_tree - with nesting" `Quick (fun () ->
let base_fs =
let open Fs in
from_list
[
dir "."
[
dir "content"
[
file "a.md" "a"
; file "b.md" "b"
; dir "foo"
[
file "c.md" "c"
; file "d.md" "d"
; dir "bar" [ file "e.md" "e"; file "f.md" "f" ]
; dir "foobar" [ file "g.txt" "g" ]
]
]
]
]
in
let trace = Fs.create_trace ~time:10 base_fs in
let program () =
let open Yocaml.Eff in
return Yocaml.Cache.empty
>>= Fs.increase_time_with 1
>>= Batch.iter_tree (Path.rel [ "content" ]) (fun p ->
let into =
p
|> Path.dirname
|> Path.trim ~prefix:(Path.rel [ "content" ])
|> Path.relocate ~into:(Path.rel [ "_www" ])
in
Yocaml.Action.copy_file ~into p)
in
let trace, _cache = Fs.run ~trace program () in
let computed_file_system = Fs.trace_system trace in
let expected_file_system =
let open Fs in
from_list
[
dir "."
[
dir "content"
[
file "a.md" "a"
; file "b.md" "b"
; dir "foo"
[
file "c.md" "c"
; file "d.md" "d"
; dir "bar" [ file "e.md" "e"; file "f.md" "f" ]
; dir "foobar" [ file "g.txt" "g" ]
]
]
; dir "_www"
[
file ~mtime:11 "a.md" "a"
; file ~mtime:11 "b.md" "b"
; dir ~mtime:11 "foo"
[
file ~mtime:11 "c.md" "c"
; file ~mtime:11 "d.md" "d"
; dir "bar"
[
file ~mtime:11 "e.md" "e"; file ~mtime:11 "f.md" "f"
]
; dir "foobar" [ file ~mtime:11 "g.txt" "g" ]
]
]
]
]
in
check Testable.fs "should be equal" expected_file_system
computed_file_system)

let test_fold_tree_1 =
let open Alcotest in
test_case "fold_tree - with nesting" `Quick (fun () ->
let base_fs =
let open Fs in
from_list
[
dir "."
[
dir "content"
[
file "a.md" "a"
; file "b.md" "b"
; dir "foo"
[
file "c.md" "c"
; file "d.md" "d"
; dir "bar" [ file "e.md" "e"; file "f.md" "f" ]
; dir "foobar" [ file "g.txt" "g" ]
]
]
]
]
in
let trace = Fs.create_trace ~time:10 base_fs in
let program () =
let open Yocaml.Eff in
return Yocaml.Cache.empty
>>= Fs.increase_time_with 1
>>= Batch.fold_tree ~state:"" (Path.rel [ "content" ])
(fun p state cache ->
let+ ctn = read_file ~on:`Source p in
(cache, state ^ ctn))
>>= fun (c, s) ->
Yocaml.Action.write_static_file (Path.rel [ "OUT" ])
(Yocaml.Task.const s) c
in
let trace, _cache = Fs.run ~trace program () in
let computed_file_system = Fs.trace_system trace in
let expected_file_system =
let open Fs in
from_list
[
dir "."
[
dir "content"
[
file "a.md" "a"
; file "b.md" "b"
; dir "foo"
[
file "c.md" "c"
; file "d.md" "d"
; dir "bar" [ file "e.md" "e"; file "f.md" "f" ]
; dir "foobar" [ file "g.txt" "g" ]
]
]
; file ~mtime:11 "OUT" "abcdefg"
]
]
in
check Testable.fs "should be equal" expected_file_system
computed_file_system)

let test_fold_tree_2 =
let open Alcotest in
test_case "fold_tree - with filtering" `Quick (fun () ->
let base_fs =
let open Fs in
from_list
[
dir "."
[
dir "content"
[
file "a.md" "a"
; file "b.md" "b"
; dir "foo"
[
file "c.md" "c"
; file "d.md" "d"
; dir "bar" [ file "e.md" "e"; file "f.md" "f" ]
; dir "foobar" [ file "g.txt" "g" ]
]
]
]
]
in
let reject name p =
match Path.basename p with
| None -> true
| Some x -> not (String.equal x name)
in
let trace = Fs.create_trace ~time:10 base_fs in
let program () =
let open Yocaml.Eff in
return Yocaml.Cache.empty
>>= Fs.increase_time_with 1
>>= Batch.fold_tree
~where:(function
| `Directory -> reject "foobar" | `File -> reject "d.md")
~state:"" (Path.rel [ "content" ])
(fun p state cache ->
let+ ctn = read_file ~on:`Source p in
(cache, state ^ ctn))
>>= fun (c, s) ->
Yocaml.Action.write_static_file (Path.rel [ "OUT" ])
(Yocaml.Task.const s) c
in
let trace, _cache = Fs.run ~trace program () in
let computed_file_system = Fs.trace_system trace in
let expected_file_system =
let open Fs in
from_list
[
dir "."
[
dir "content"
[
file "a.md" "a"
; file "b.md" "b"
; dir "foo"
[
file "c.md" "c"
; file "d.md" "d"
; dir "bar" [ file "e.md" "e"; file "f.md" "f" ]
; dir "foobar" [ file "g.txt" "g" ]
]
]
; file ~mtime:11 "OUT" "abcef"
]
]
in
check Testable.fs "should be equal" expected_file_system
computed_file_system)

let cases =
( "Yocaml.Batch"
, [ test_iter_tree_1; test_iter_tree_2; test_fold_tree_1; test_fold_tree_2 ]
)
18 changes: 18 additions & 0 deletions test/yocaml/batch_test.mli
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
(* YOCaml a static blog generator.
Copyright (C) 2025 The Funkyworkers and The YOCaml's developers

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. *)

val cases : string * unit Alcotest.test_case list
(** Returns the list of test cases. *)
1 change: 1 addition & 0 deletions test/yocaml/yocaml_test.ml
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ let () =
; Exec_command.cases
; Conditional_test.cases
; Trace_test.cases
; Batch_test.cases
]
Loading