Prune ignored subtrees
- SHA
1a5c74c897d3c0655981e2b3ebfc2890f6b46ed8- Parents
-
a377eef - Tree
a8af1eb
1a5c74c
1a5c74c897d3c0655981e2b3ebfc2890f6b46ed8a377eef
a8af1eb| Status | File | + | - |
|---|---|---|---|
| M |
src/fgof_watch.f90
|
65 | 2 |
| M |
src/fgof_watch_poll.c
|
142 | 4 |
| M |
test/test_watch_errors.f90
|
79 | 29 |
src/fgof_watch.f90modified@@ -22,10 +22,14 @@ module fgof_watch | ||
| 22 | 22 | public :: set_ignore_prefixes |
| 23 | 23 | |
| 24 | 24 | interface |
| 25 | - integer(c_int) function fgof_watch_collect_snapshot_c(root, recursive, buffer, buffer_len) bind(C, name="fgof_watch_collect_snapshot") | |
| 25 | + integer(c_int) function fgof_watch_collect_snapshot_c(root, recursive, ignore_hidden, prefix_count, prefix_stride, prefixes, buffer, buffer_len) bind(C, name="fgof_watch_collect_snapshot") | |
| 26 | 26 | import :: c_char, c_int, c_ptr, c_size_t |
| 27 | 27 | character(kind=c_char), intent(in) :: root(*) |
| 28 | 28 | integer(c_int), value :: recursive |
| 29 | + integer(c_int), value :: ignore_hidden | |
| 30 | + integer(c_int), value :: prefix_count | |
| 31 | + integer(c_int), value :: prefix_stride | |
| 32 | + character(kind=c_char), intent(in) :: prefixes(*) | |
| 29 | 33 | type(c_ptr), intent(out) :: buffer |
| 30 | 34 | integer(c_size_t), intent(out) :: buffer_len |
| 31 | 35 | end function fgof_watch_collect_snapshot_c |
@@ -179,16 +183,29 @@ contains | ||
| 179 | 183 | integer(c_int) :: status |
| 180 | 184 | integer(c_size_t) :: raw_len |
| 181 | 185 | character(kind=c_char), allocatable :: c_root(:) |
| 186 | + character(kind=c_char), allocatable :: c_prefixes(:) | |
| 187 | + integer(c_int) :: prefix_count | |
| 188 | + integer(c_int) :: prefix_stride | |
| 182 | 189 | character(kind=c_char), pointer :: raw_chars(:) |
| 183 | 190 | character(len=:), allocatable :: text |
| 184 | 191 | |
| 185 | 192 | c_root = to_c_string(root) |
| 193 | + call pack_ignore_prefixes(options, prefix_count, prefix_stride, c_prefixes) | |
| 186 | 194 | raw_ptr = c_null_ptr |
| 187 | 195 | raw_len = 0_c_size_t |
| 188 | 196 | status_code = 0 |
| 189 | 197 | status_message = "" |
| 190 | 198 | |
| 191 | - status = fgof_watch_collect_snapshot_c(c_root, merge(1_c_int, 0_c_int, options%recursive), raw_ptr, raw_len) | |
| 199 | + status = fgof_watch_collect_snapshot_c( & | |
| 200 | + c_root, & | |
| 201 | + merge(1_c_int, 0_c_int, options%recursive), & | |
| 202 | + merge(1_c_int, 0_c_int, options%ignore_hidden), & | |
| 203 | + prefix_count, & | |
| 204 | + prefix_stride, & | |
| 205 | + c_prefixes, & | |
| 206 | + raw_ptr, & | |
| 207 | + raw_len & | |
| 208 | + ) | |
| 192 | 209 | if (status /= 0_c_int) then |
| 193 | 210 | allocate(entries(0)) |
| 194 | 211 | if (c_associated(raw_ptr)) call fgof_watch_free_buffer_c(raw_ptr) |
@@ -212,6 +229,45 @@ contains | ||
| 212 | 229 | call sort_entries(entries) |
| 213 | 230 | end subroutine collect_snapshot |
| 214 | 231 | |
| 232 | + subroutine pack_ignore_prefixes(options, count, stride, buffer) | |
| 233 | + type(watch_options), intent(in) :: options | |
| 234 | + integer(c_int), intent(out) :: count | |
| 235 | + integer(c_int), intent(out) :: stride | |
| 236 | + character(kind=c_char), allocatable, intent(out) :: buffer(:) | |
| 237 | + integer :: i | |
| 238 | + integer :: j | |
| 239 | + integer :: width | |
| 240 | + integer :: offset | |
| 241 | + | |
| 242 | + if (.not. allocated(options%ignore_prefixes)) then | |
| 243 | + count = 0_c_int | |
| 244 | + stride = 0_c_int | |
| 245 | + buffer = empty_c_string() | |
| 246 | + return | |
| 247 | + end if | |
| 248 | + | |
| 249 | + if (size(options%ignore_prefixes) == 0) then | |
| 250 | + count = 0_c_int | |
| 251 | + stride = 0_c_int | |
| 252 | + buffer = empty_c_string() | |
| 253 | + return | |
| 254 | + end if | |
| 255 | + | |
| 256 | + width = max_string_length(options%ignore_prefixes) + 1 | |
| 257 | + count = int(size(options%ignore_prefixes), c_int) | |
| 258 | + stride = int(width, c_int) | |
| 259 | + allocate(buffer(size(options%ignore_prefixes) * width)) | |
| 260 | + buffer = c_null_char | |
| 261 | + | |
| 262 | + do i = 1, size(options%ignore_prefixes) | |
| 263 | + offset = (i - 1) * width | |
| 264 | + do j = 1, len_trim(options%ignore_prefixes(i)) | |
| 265 | + buffer(offset + j) = options%ignore_prefixes(i)(j:j) | |
| 266 | + end do | |
| 267 | + buffer(offset + len_trim(options%ignore_prefixes(i)) + 1) = c_null_char | |
| 268 | + end do | |
| 269 | + end subroutine pack_ignore_prefixes | |
| 270 | + | |
| 215 | 271 | subroutine filter_entries(root, options, entries) |
| 216 | 272 | character(len=*), intent(in) :: root |
| 217 | 273 | type(watch_options), intent(in) :: options |
@@ -820,6 +876,13 @@ contains | ||
| 820 | 876 | buf(n + 1) = c_null_char |
| 821 | 877 | end function to_c_string |
| 822 | 878 | |
| 879 | + function empty_c_string() result(buf) | |
| 880 | + character(kind=c_char), allocatable :: buf(:) | |
| 881 | + | |
| 882 | + allocate(buf(1)) | |
| 883 | + buf(1) = c_null_char | |
| 884 | + end function empty_c_string | |
| 885 | + | |
| 823 | 886 | subroutine append_entry(entries, entry) |
| 824 | 887 | type(watch_entry), allocatable, intent(inout) :: entries(:) |
| 825 | 888 | type(watch_entry), intent(in) :: entry |
src/fgof_watch_poll.cmodified@@ -14,6 +14,101 @@ typedef struct { | ||
| 14 | 14 | size_t cap; |
| 15 | 15 | } fgof_watch_buffer; |
| 16 | 16 | |
| 17 | +static const char *fgof_watch_basename(const char *path) { | |
| 18 | + const char *slash; | |
| 19 | + | |
| 20 | + slash = strrchr(path, '/'); | |
| 21 | + if (slash == NULL) { | |
| 22 | + return path; | |
| 23 | + } | |
| 24 | + return slash + 1; | |
| 25 | +} | |
| 26 | + | |
| 27 | +static const char *fgof_watch_relative_path(const char *root, const char *path) { | |
| 28 | + size_t root_len; | |
| 29 | + | |
| 30 | + if (strcmp(path, root) == 0) { | |
| 31 | + return fgof_watch_basename(root); | |
| 32 | + } | |
| 33 | + | |
| 34 | + root_len = strlen(root); | |
| 35 | + if (strncmp(path, root, root_len) == 0 && path[root_len] == '/') { | |
| 36 | + return path + root_len + 1; | |
| 37 | + } | |
| 38 | + | |
| 39 | + return path; | |
| 40 | +} | |
| 41 | + | |
| 42 | +static int fgof_watch_contains_hidden_segment(const char *path) { | |
| 43 | + const char *segment_start; | |
| 44 | + const char *cursor; | |
| 45 | + | |
| 46 | + if (path == NULL || path[0] == '\0') { | |
| 47 | + return 0; | |
| 48 | + } | |
| 49 | + | |
| 50 | + segment_start = path; | |
| 51 | + for (cursor = path; ; ++cursor) { | |
| 52 | + if (*cursor != '/' && *cursor != '\0') { | |
| 53 | + continue; | |
| 54 | + } | |
| 55 | + if (cursor > segment_start && segment_start[0] == '.') { | |
| 56 | + return 1; | |
| 57 | + } | |
| 58 | + if (*cursor == '\0') { | |
| 59 | + break; | |
| 60 | + } | |
| 61 | + segment_start = cursor + 1; | |
| 62 | + } | |
| 63 | + | |
| 64 | + return 0; | |
| 65 | +} | |
| 66 | + | |
| 67 | +static int fgof_watch_matches_prefix(const char *path, int prefix_count, int prefix_stride, const char *prefixes) { | |
| 68 | + int i; | |
| 69 | + const char *prefix; | |
| 70 | + size_t prefix_len; | |
| 71 | + | |
| 72 | + if (prefix_count <= 0 || prefix_stride <= 0 || prefixes == NULL) { | |
| 73 | + return 0; | |
| 74 | + } | |
| 75 | + | |
| 76 | + for (i = 0; i < prefix_count; ++i) { | |
| 77 | + prefix = prefixes + (i * prefix_stride); | |
| 78 | + prefix_len = strlen(prefix); | |
| 79 | + if (prefix_len == 0) { | |
| 80 | + continue; | |
| 81 | + } | |
| 82 | + if (strcmp(path, prefix) == 0) { | |
| 83 | + return 1; | |
| 84 | + } | |
| 85 | + if (strncmp(path, prefix, prefix_len) == 0 && path[prefix_len] == '/') { | |
| 86 | + return 1; | |
| 87 | + } | |
| 88 | + } | |
| 89 | + | |
| 90 | + return 0; | |
| 91 | +} | |
| 92 | + | |
| 93 | +static int fgof_watch_should_ignore( | |
| 94 | + const char *root, | |
| 95 | + const char *path, | |
| 96 | + int ignore_hidden, | |
| 97 | + int prefix_count, | |
| 98 | + int prefix_stride, | |
| 99 | + const char *prefixes | |
| 100 | +) { | |
| 101 | + if (ignore_hidden && fgof_watch_contains_hidden_segment(fgof_watch_relative_path(root, path))) { | |
| 102 | + return 1; | |
| 103 | + } | |
| 104 | + | |
| 105 | + if (fgof_watch_matches_prefix(path, prefix_count, prefix_stride, prefixes)) { | |
| 106 | + return 1; | |
| 107 | + } | |
| 108 | + | |
| 109 | + return 0; | |
| 110 | +} | |
| 111 | + | |
| 17 | 112 | static int fgof_watch_append_text(fgof_watch_buffer *buffer, const char *text, size_t text_len) { |
| 18 | 113 | char *grown; |
| 19 | 114 | size_t needed; |
@@ -86,11 +181,25 @@ static int fgof_watch_append_entry(fgof_watch_buffer *buffer, const char *path, | ||
| 86 | 181 | return fgof_watch_append_text(buffer, line, (size_t)line_len); |
| 87 | 182 | } |
| 88 | 183 | |
| 89 | -static int fgof_watch_visit(const char *path, int recursive, int depth, fgof_watch_buffer *buffer) { | |
| 184 | +static int fgof_watch_visit( | |
| 185 | + const char *root, | |
| 186 | + const char *path, | |
| 187 | + int recursive, | |
| 188 | + int depth, | |
| 189 | + int ignore_hidden, | |
| 190 | + int prefix_count, | |
| 191 | + int prefix_stride, | |
| 192 | + const char *prefixes, | |
| 193 | + fgof_watch_buffer *buffer | |
| 194 | +) { | |
| 90 | 195 | DIR *dirp; |
| 91 | 196 | struct dirent *entry; |
| 92 | 197 | struct stat st; |
| 93 | 198 | |
| 199 | + if (fgof_watch_should_ignore(root, path, ignore_hidden, prefix_count, prefix_stride, prefixes)) { | |
| 200 | + return 0; | |
| 201 | + } | |
| 202 | + | |
| 94 | 203 | if (lstat(path, &st) != 0) { |
| 95 | 204 | if (errno == ENOENT || errno == ENOTDIR) { |
| 96 | 205 | return 0; |
@@ -135,7 +244,17 @@ static int fgof_watch_visit(const char *path, int recursive, int depth, fgof_wat | ||
| 135 | 244 | } |
| 136 | 245 | |
| 137 | 246 | snprintf(child, child_len, "%s/%s", path, entry->d_name); |
| 138 | - status = fgof_watch_visit(child, recursive, depth + 1, buffer); | |
| 247 | + status = fgof_watch_visit( | |
| 248 | + root, | |
| 249 | + child, | |
| 250 | + recursive, | |
| 251 | + depth + 1, | |
| 252 | + ignore_hidden, | |
| 253 | + prefix_count, | |
| 254 | + prefix_stride, | |
| 255 | + prefixes, | |
| 256 | + buffer | |
| 257 | + ); | |
| 139 | 258 | free(child); |
| 140 | 259 | |
| 141 | 260 | if (status != 0) { |
@@ -148,7 +267,16 @@ static int fgof_watch_visit(const char *path, int recursive, int depth, fgof_wat | ||
| 148 | 267 | return 0; |
| 149 | 268 | } |
| 150 | 269 | |
| 151 | -int fgof_watch_collect_snapshot(const char *root, int recursive, char **buffer_out, size_t *buffer_len_out) { | |
| 270 | +int fgof_watch_collect_snapshot( | |
| 271 | + const char *root, | |
| 272 | + int recursive, | |
| 273 | + int ignore_hidden, | |
| 274 | + int prefix_count, | |
| 275 | + int prefix_stride, | |
| 276 | + const char *prefixes, | |
| 277 | + char **buffer_out, | |
| 278 | + size_t *buffer_len_out | |
| 279 | +) { | |
| 152 | 280 | fgof_watch_buffer buffer; |
| 153 | 281 | int status; |
| 154 | 282 | |
@@ -163,7 +291,17 @@ int fgof_watch_collect_snapshot(const char *root, int recursive, char **buffer_o | ||
| 163 | 291 | return 0; |
| 164 | 292 | } |
| 165 | 293 | |
| 166 | - status = fgof_watch_visit(root, recursive != 0, 0, &buffer); | |
| 294 | + status = fgof_watch_visit( | |
| 295 | + root, | |
| 296 | + root, | |
| 297 | + recursive != 0, | |
| 298 | + 0, | |
| 299 | + ignore_hidden != 0, | |
| 300 | + prefix_count, | |
| 301 | + prefix_stride, | |
| 302 | + prefixes, | |
| 303 | + &buffer | |
| 304 | + ); | |
| 167 | 305 | if (status != 0) { |
| 168 | 306 | free(buffer.data); |
| 169 | 307 | return status; |
test/test_watch_errors.f90modified@@ -1,34 +1,84 @@ | ||
| 1 | 1 | program test_watch_errors |
| 2 | - use fgof_watch, only : init_watch, poll_watch, reset_watch | |
| 3 | - use fgof_watch_types, only : FGOF_WATCH_ERR_NONE, FGOF_WATCH_ERR_SNAPSHOT_FAILED, watch_event, watch_session | |
| 2 | + use fgof_watch, only : init_watch, poll_watch, reset_watch, set_ignore_prefixes | |
| 3 | + use fgof_watch_types, only : FGOF_WATCH_ERR_NONE, FGOF_WATCH_ERR_SNAPSHOT_FAILED, watch_event, watch_options, watch_session | |
| 4 | 4 | use watch_test_support, only : chmod_mode, ensure_clean_dir, expect_no_events, make_dir, remove_tree, write_text |
| 5 | 5 | implicit none |
| 6 | 6 | |
| 7 | - character(len=*), parameter :: root = "build/watch-tests-errors" | |
| 8 | - character(len=*), parameter :: locked_dir = "build/watch-tests-errors/locked" | |
| 9 | - character(len=*), parameter :: locked_file = "build/watch-tests-errors/locked/file.txt" | |
| 10 | - type(watch_event), allocatable :: events(:) | |
| 11 | - type(watch_session) :: session | |
| 12 | - | |
| 13 | - call ensure_clean_dir(root) | |
| 14 | - call make_dir(locked_dir) | |
| 15 | - call write_text(locked_file, "alpha") | |
| 16 | - | |
| 17 | - call init_watch(session, root) | |
| 18 | - if (session%last_error_code /= FGOF_WATCH_ERR_NONE) error stop "initial snapshot should succeed" | |
| 19 | - | |
| 20 | - call chmod_mode(locked_dir, "000") | |
| 21 | - events = poll_watch(session) | |
| 22 | - call expect_no_events(events, "snapshot failure should not emit false remove events") | |
| 23 | - if (session%last_error_code /= FGOF_WATCH_ERR_SNAPSHOT_FAILED) error stop "snapshot failure should set the session error code" | |
| 24 | - if (.not. allocated(session%last_error_message)) error stop "snapshot failure should preserve an error message" | |
| 25 | - if (len(session%last_error_message) == 0) error stop "snapshot failure message should not be empty" | |
| 26 | - | |
| 27 | - call chmod_mode(locked_dir, "755") | |
| 28 | - events = poll_watch(session) | |
| 29 | - call expect_no_events(events, "state should recover cleanly after access is restored") | |
| 30 | - if (session%last_error_code /= FGOF_WATCH_ERR_NONE) error stop "successful poll should clear the session error" | |
| 31 | - | |
| 32 | - call reset_watch(session) | |
| 33 | - call remove_tree(root) | |
| 7 | + call test_runtime_snapshot_failure() | |
| 8 | + call test_hidden_pruning_avoids_failure() | |
| 9 | + call test_prefix_pruning_avoids_failure() | |
| 10 | + | |
| 11 | +contains | |
| 12 | + | |
| 13 | + subroutine test_runtime_snapshot_failure() | |
| 14 | + character(len=*), parameter :: root = "build/watch-tests-errors" | |
| 15 | + character(len=*), parameter :: locked_dir = "build/watch-tests-errors/locked" | |
| 16 | + character(len=*), parameter :: locked_file = "build/watch-tests-errors/locked/file.txt" | |
| 17 | + type(watch_event), allocatable :: events(:) | |
| 18 | + type(watch_session) :: session | |
| 19 | + | |
| 20 | + call ensure_clean_dir(root) | |
| 21 | + call make_dir(locked_dir) | |
| 22 | + call write_text(locked_file, "alpha") | |
| 23 | + | |
| 24 | + call init_watch(session, root) | |
| 25 | + if (session%last_error_code /= FGOF_WATCH_ERR_NONE) error stop "initial snapshot should succeed" | |
| 26 | + | |
| 27 | + call chmod_mode(locked_dir, "000") | |
| 28 | + events = poll_watch(session) | |
| 29 | + call expect_no_events(events, "snapshot failure should not emit false remove events") | |
| 30 | + if (session%last_error_code /= FGOF_WATCH_ERR_SNAPSHOT_FAILED) error stop "snapshot failure should set the session error code" | |
| 31 | + if (.not. allocated(session%last_error_message)) error stop "snapshot failure should preserve an error message" | |
| 32 | + if (len(session%last_error_message) == 0) error stop "snapshot failure message should not be empty" | |
| 33 | + | |
| 34 | + call chmod_mode(locked_dir, "755") | |
| 35 | + events = poll_watch(session) | |
| 36 | + call expect_no_events(events, "state should recover cleanly after access is restored") | |
| 37 | + if (session%last_error_code /= FGOF_WATCH_ERR_NONE) error stop "successful poll should clear the session error" | |
| 38 | + | |
| 39 | + call reset_watch(session) | |
| 40 | + call remove_tree(root) | |
| 41 | + end subroutine test_runtime_snapshot_failure | |
| 42 | + | |
| 43 | + subroutine test_hidden_pruning_avoids_failure() | |
| 44 | + character(len=*), parameter :: root = "build/watch-tests-hidden-prune" | |
| 45 | + character(len=*), parameter :: hidden_dir = "build/watch-tests-hidden-prune/.cache" | |
| 46 | + character(len=*), parameter :: hidden_file = "build/watch-tests-hidden-prune/.cache/file.txt" | |
| 47 | + type(watch_options) :: options | |
| 48 | + type(watch_session) :: session | |
| 49 | + | |
| 50 | + call ensure_clean_dir(root) | |
| 51 | + call make_dir(hidden_dir) | |
| 52 | + call write_text(hidden_file, "alpha") | |
| 53 | + call chmod_mode(hidden_dir, "000") | |
| 54 | + | |
| 55 | + options%ignore_hidden = .true. | |
| 56 | + call init_watch(session, root, options) | |
| 57 | + if (session%last_error_code /= FGOF_WATCH_ERR_NONE) error stop "ignored hidden subtree should be pruned before snapshot failure" | |
| 58 | + | |
| 59 | + call chmod_mode(hidden_dir, "755") | |
| 60 | + call reset_watch(session) | |
| 61 | + call remove_tree(root) | |
| 62 | + end subroutine test_hidden_pruning_avoids_failure | |
| 63 | + | |
| 64 | + subroutine test_prefix_pruning_avoids_failure() | |
| 65 | + character(len=*), parameter :: root = "build/watch-tests-prefix-prune" | |
| 66 | + character(len=*), parameter :: vendor_dir = "build/watch-tests-prefix-prune/vendor" | |
| 67 | + character(len=*), parameter :: vendor_file = "build/watch-tests-prefix-prune/vendor/file.txt" | |
| 68 | + type(watch_options) :: options | |
| 69 | + type(watch_session) :: session | |
| 70 | + | |
| 71 | + call ensure_clean_dir(root) | |
| 72 | + call make_dir(vendor_dir) | |
| 73 | + call write_text(vendor_file, "alpha") | |
| 74 | + call chmod_mode(vendor_dir, "000") | |
| 75 | + | |
| 76 | + call set_ignore_prefixes(options, [character(len=len(vendor_dir)) :: vendor_dir]) | |
| 77 | + call init_watch(session, root, options) | |
| 78 | + if (session%last_error_code /= FGOF_WATCH_ERR_NONE) error stop "ignored prefix subtree should be pruned before snapshot failure" | |
| 79 | + | |
| 80 | + call chmod_mode(vendor_dir, "755") | |
| 81 | + call reset_watch(session) | |
| 82 | + call remove_tree(root) | |
| 83 | + end subroutine test_prefix_pruning_avoids_failure | |
| 34 | 84 | end program test_watch_errors |