File: watcher.R

package info (click to toggle)
r-cran-testthat 3.3.2-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 4,048 kB
  • sloc: cpp: 9,269; sh: 14; ansic: 14; makefile: 5
file content (103 lines) | stat: -rw-r--r-- 3,151 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
#' Watch a directory for changes (additions, deletions & modifications).
#'
#' This is used to power the [auto_test()] and
#' [auto_test_package()] functions which are used to rerun tests
#' whenever source code changes.
#'
#' Use Ctrl + break (windows), Esc (mac gui) or Ctrl + C (command line) to
#' stop the watcher.
#'
#' @param path character vector of paths to watch.  Omit trailing backslash.
#' @param pattern file pattern passed to [dir()]
#' @param callback function called every time a change occurs.  It should
#'   have three parameters: added, deleted, modified, and should return
#'   `TRUE` to keep watching, or `FALSE` to stop.
#' @param hash hashes are more accurate at detecting changes, but are slower
#'   for large files.  When `FALSE`, uses modification time stamps
#' @export
#' @keywords internal
watch <- function(path, callback, pattern = NULL, hash = TRUE) {
  prev <- dir_state(path, pattern, hash = hash)

  repeat {
    Sys.sleep(1)

    curr <- dir_state(path, pattern, hash = hash)
    changes <- compare_state(prev, curr)

    if (changes$n > 0) {
      # cat("C")
      keep_going <- TRUE
      try(
        keep_going <- callback(changes$added, changes$deleted, changes$modified)
      )

      if (!isTRUE(keep_going)) return(invisible())
    } else {
      # cat(".")
    }

    prev <- curr
  }
}

safe_digest <- function(path) {
  if (!file.exists(path)) {
    return(NA_character_)
  }
  if (is_directory(path)) {
    return(NA_character_)
  }
  if (!is_readable(path)) {
    return(NA_character_)
  }

  rlang::hash_file(path)
}

#' Capture the state of a directory.
#'
#' @param path path to directory
#' @param pattern regular expression with which to filter files
#' @param hash use hash (slow but accurate) or time stamp (fast but less
#'   accurate)
#' @keywords internal
dir_state <- function(path, pattern = NULL, hash = TRUE) {
  files <- dir(path, pattern, full.names = TRUE)

  # It's possible for any of the files to be deleted between the dir()
  # call above and the calls below; `file.info` handles this
  # gracefully, but digest::digest doesn't -- so we wrap it. Both
  # cases will return NA for files that have gone missing.
  if (hash) {
    file_states <- map_chr(files, safe_digest)
  } else {
    file_states <- file.info(files)$mtime
  }
  file_states <- stats::setNames(file_states, files)
  file_states[!is.na(file_states)]
}

#' Compare two directory states.
#'
#' @param old previous state
#' @param new current state
#' @return list containing number of changes and files which have been
#'   `added`, `deleted` and `modified`
#' @keywords internal
compare_state <- function(old, new) {
  added <- setdiff(names(new), names(old))
  deleted <- setdiff(names(old), names(new))

  same <- intersect(names(old), names(new))
  modified <- names(new[same])[new[same] != old[same]]

  n <- length(added) + length(deleted) + length(modified)

  list(n = n, added = added, deleted = deleted, modified = modified)
}

# Helpers -----------------------------------------------------------------

is_directory <- function(x) file.info(x)$isdir
is_readable <- function(x) file.access(x, 4) == 0