From 3f9bbe88832cb19061a13dec666c9d58bf4954a4 Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Tue, 23 Jun 2026 10:00:23 -0500 Subject: [PATCH 01/18] Remove dead code: unused helpers and unreachable returns - Drop unused internal helpers verify_status(), %||%, version_above(), and block_show() (zero callers; rlang already provides %||%). - Remove return()/FALSE statements that are unreachable because the preceding cli_abort() always throws (binary_download, dmg_package_install, pkg_install). - Regenerate docs to drop the orphaned man pages. --- R/blocks.R | 7 ------- R/installers.R | 10 +--------- R/system.R | 10 ---------- R/utils.R | 40 ---------------------------------------- man/null_coalesce.Rd | 21 --------------------- man/verify_status.Rd | 24 ------------------------ man/version_above.Rd | 20 -------------------- 7 files changed, 1 insertion(+), 131 deletions(-) delete mode 100644 man/null_coalesce.Rd delete mode 100644 man/verify_status.Rd delete mode 100644 man/version_above.Rd diff --git a/R/blocks.R b/R/blocks.R index 54765f7..66c3a42 100644 --- a/R/blocks.R +++ b/R/blocks.R @@ -130,13 +130,6 @@ block_replace = function(desc, value, path, write_utf8(path, lines) } - -block_show = function(path, block_start = "# <<<", block_end = "# >>>") { - lines = read_utf8(path) - block = block_find(lines, block_start, block_end) - lines[seq2(block[[1]], block[[2]])] -} - block_find = function(lines, block_start = "# <<<", block_end = "# >>>") { # No file if (base::is.null(lines)) { diff --git a/R/installers.R b/R/installers.R index 43c2809..a3c046e 100644 --- a/R/installers.R +++ b/R/installers.R @@ -170,7 +170,7 @@ binary_download <- function(url, binary_file_name = base::basename(url), verbose # Download with progress and error handling download_start_time <- base::Sys.time() - download_result <- base::tryCatch({ + base::tryCatch({ utils::download.file( url, save_location, @@ -180,7 +180,6 @@ binary_download <- function(url, binary_file_name = base::basename(url), verbose method = "auto", timeout = timeout ) - TRUE }, error = function(e) { if (verbose) cli::cli_progress_done(pb_id) @@ -195,15 +194,10 @@ binary_download <- function(url, binary_file_name = base::basename(url), verbose ">" = "Status code: {.val {status_info}}", "i" = "Check your internet connection or try again later. The server may be temporarily unavailable." )) - FALSE }) download_duration <- base::difftime(base::Sys.time(), download_start_time, units = "secs") - if (!download_result) { - return(NULL) - } - if (verbose) { file_size <- base::file.info(save_location)$size file_size_mb <- base::round(file_size / (1024 * 1024), 2) @@ -313,7 +307,6 @@ dmg_package_install <- function(path_to_dmg, "Disk image: {.file {volume_with_extension}}", "Status code: {.val {mount_status}}" )) - return(FALSE) } if (verbose) { @@ -409,7 +402,6 @@ pkg_install <- function(path_to_pkg, "Package: {.file {package_name}}", "Status code: {.val {status}}" )) - return(FALSE) } if (verbose) { diff --git a/R/system.R b/R/system.R index a42919d..847f7dd 100644 --- a/R/system.R +++ b/R/system.R @@ -153,16 +153,6 @@ is_macos_high_sierra <- function() { version_between(mac_version, "10.13.0", "10.14.0") } -#' Check if Version is Above Threshold -#' -#' @param software_version Version string to check -#' @param than Threshold version to compare against -#' @return TRUE if software_version is above than, FALSE otherwise -#' @keywords internal -version_above <- function(software_version, than) { - utils::compareVersion(software_version, than) == 1L -} - #' Check if Version is Between Bounds #' #' @param software_version Version string to check diff --git a/R/utils.R b/R/utils.R index e753663..8a066fe 100644 --- a/R/utils.R +++ b/R/utils.R @@ -20,35 +20,6 @@ print.cli <- function(x, ...) { return(base::invisible(x)) } -#' Verify Status of Operation -#' -#' @param status Status code from operation -#' @param program Name of the program being installed or uninstalled -#' @param url Optional URL for manual instructions -#' @param type Type of operation ("uninstall" or "install") -#' @return TRUE if status is successful, FALSE otherwise (invisibly) -#' @keywords internal -verify_status <- function(status, program, url, type = c("uninstall", "install")) { - type <- base::match.arg(type) - - if(base::isFALSE(status)) { - time_of_failure <- base::format(base::Sys.time(), '%Y-%m-%d %H:%M:%S') - url_info <- if(!base::missing(url)) c("Manual instructions available at:", "{.url {url}}") else NULL - - cli::cli_abort(c( - "{.pkg macrtools}: Operation failed: Could not {type} {.pkg {program}}.", - "{.pkg macrtools}: Status: {.val {status}}", - "{.pkg macrtools}: Operation type: {.val {type}}", - "{.pkg macrtools}: Time of failure: {.val {time_of_failure}}", - url_info - ), - advice = base::paste0("You may need to run this operation with administrative privileges or check for system compatibility issues.")) - return(base::invisible(FALSE)) - } - - base::invisible(TRUE) -} - #' Force Password Entry if Not Provided #' #' @param supplied_password Password provided by user (may be NULL) @@ -79,14 +50,3 @@ force_password <- function(supplied_password) { caller_env <- function(n = 1) { base::parent.frame(n + 1) } - -#' Null Coalesce Operator -#' -#' @param x First value (may be NULL) -#' @param y Default value if x is NULL -#' @return x if not NULL, otherwise y -#' @name null_coalesce -#' @keywords internal -`%||%` <- function(x, y) { - if (base::is.null(x)) y else x -} diff --git a/man/null_coalesce.Rd b/man/null_coalesce.Rd deleted file mode 100644 index 5506d47..0000000 --- a/man/null_coalesce.Rd +++ /dev/null @@ -1,21 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/utils.R -\name{null_coalesce} -\alias{null_coalesce} -\alias{\%||\%} -\title{Null Coalesce Operator} -\usage{ -x \%||\% y -} -\arguments{ -\item{x}{First value (may be NULL)} - -\item{y}{Default value if x is NULL} -} -\value{ -x if not NULL, otherwise y -} -\description{ -Null Coalesce Operator -} -\keyword{internal} diff --git a/man/verify_status.Rd b/man/verify_status.Rd deleted file mode 100644 index e0482d7..0000000 --- a/man/verify_status.Rd +++ /dev/null @@ -1,24 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/utils.R -\name{verify_status} -\alias{verify_status} -\title{Verify Status of Operation} -\usage{ -verify_status(status, program, url, type = c("uninstall", "install")) -} -\arguments{ -\item{status}{Status code from operation} - -\item{program}{Name of the program being installed or uninstalled} - -\item{url}{Optional URL for manual instructions} - -\item{type}{Type of operation ("uninstall" or "install")} -} -\value{ -TRUE if status is successful, FALSE otherwise (invisibly) -} -\description{ -Verify Status of Operation -} -\keyword{internal} diff --git a/man/version_above.Rd b/man/version_above.Rd deleted file mode 100644 index 4121ba2..0000000 --- a/man/version_above.Rd +++ /dev/null @@ -1,20 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/system.R -\name{version_above} -\alias{version_above} -\title{Check if Version is Above Threshold} -\usage{ -version_above(software_version, than) -} -\arguments{ -\item{software_version}{Version string to check} - -\item{than}{Threshold version to compare against} -} -\value{ -TRUE if software_version is above than, FALSE otherwise -} -\description{ -Check if Version is Above Threshold -} -\keyword{internal} From 1386b15c941ec90a9f6774575c2975b58e59b70d Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Tue, 23 Jun 2026 10:02:00 -0500 Subject: [PATCH 02/18] Fix documentation typos in roxygen blocks - gfortran: heredoc target '~./Renviron' -> '~/.Renviron' (4 examples). - toolchain: 'macros_rtools_*'/'mac_rtools_*' -> real 'macos_rtools_*' names. - xcode: removal step showed 'touch' instead of 'rm'. --- R/gfortran.R | 8 ++++---- R/toolchain.R | 6 +++--- R/xcode.R | 2 +- man/gfortran.Rd | 8 ++++---- man/macos-rtools.Rd | 6 +++--- man/xcode-cli.Rd | 2 +- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/R/gfortran.R b/R/gfortran.R index 2564a44..c5f5093 100644 --- a/R/gfortran.R +++ b/R/gfortran.R @@ -71,7 +71,7 @@ gfortran_version <- function() { #' #' ```sh #' touch ~/.Renviron -#' cat << "EOF" > ~./Renviron +#' cat << "EOF" > ~/.Renviron #' ## macrtools - gfortran: start #' PATH=${PATH}:/opt/gfortran/bin #' ## macrtools - gfortran: end @@ -96,7 +96,7 @@ gfortran_version <- function() { #' #' ```sh #' touch ~/.Renviron -#' cat << "EOF" > ~./Renviron +#' cat << "EOF" > ~/.Renviron #' ## macrtools - gfortran: start #' PATH=${PATH}:/opt/gfortran/bin #' ## macrtools - gfortran: end @@ -126,7 +126,7 @@ gfortran_version <- function() { #' #' ```sh #' touch ~/.Renviron -#' cat << "EOF" > ~./Renviron +#' cat << "EOF" > ~/.Renviron #' ## macrtools - gfortran: start #' PATH=${PATH}:/usr/local/gfortran/bin #' ## macrtools - gfortran: end @@ -166,7 +166,7 @@ gfortran_version <- function() { #' #' ```sh #' touch ~/.Renviron -#' cat << "EOF" > ~./Renviron +#' cat << "EOF" > ~/.Renviron #' ## macrtools - gfortran: start #' PATH=${PATH}:/opt/R/arm64/gfortran/bin #' ## macrtools - gfortran: end diff --git a/R/toolchain.R b/R/toolchain.R index 2cfb9c3..b1db45b 100644 --- a/R/toolchain.R +++ b/R/toolchain.R @@ -1,7 +1,7 @@ #' Install and Uninstall the macOS R Toolchain #' #' The `macos_rtools_install()` function aims to install all required dependencies -#' for the macOS R compilation toolchain. Meanwhile, the `macros_rtools_uninstall()` +#' for the macOS R compilation toolchain. Meanwhile, the `macos_rtools_uninstall()` #' function aims to remove any installed files from your computer. #' #' @param password Password for user account to request `sudo` access. @@ -14,11 +14,11 @@ #' 2. gfortran #' 3. A series of binary packages from the [`recipes`](https://github.com/R-macos/recipes) system to compile R. #' -#' The `mac_rtools_install()` function attempts to install each of the required +#' The `macos_rtools_install()` function attempts to install each of the required #' components. If we detect that the Xcode.app IDE is installed, we'll skip #' attempting to install the Xcode CLI software. #' -#' Meanwhile, the `mac_rtools_uninstall()` function aims to +#' Meanwhile, the `macos_rtools_uninstall()` function aims to #' delete or uninstall the Xcode CLI and gfortran binaries. At the present moment, #' there is no support for uninstalling the binary packages from `recipes`. #' diff --git a/R/xcode.R b/R/xcode.R index c07d025..5d59ea3 100644 --- a/R/xcode.R +++ b/R/xcode.R @@ -189,7 +189,7 @@ xcode_cli_path <- function() { #' Finally, we remove the temporary installation file. #' #' ```sh -#' touch /tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress +#' rm /tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress #' ``` #' #' These steps were obtained from Timothy Sutton's diff --git a/man/gfortran.Rd b/man/gfortran.Rd index 205c3ef..10b24be 100644 --- a/man/gfortran.Rd +++ b/man/gfortran.Rd @@ -87,7 +87,7 @@ Once installed, we modify the \code{PATH} environment variable to recognize the installed software by adding into the \verb{~/.Renviron} file the following: \if{html}{\out{
}}\preformatted{touch ~/.Renviron -cat << "EOF" > ~./Renviron +cat << "EOF" > ~/.Renviron ## macrtools - gfortran: start PATH=$\{PATH\}:/opt/gfortran/bin ## macrtools - gfortran: end @@ -111,7 +111,7 @@ Once installed, we modify the \code{PATH} environment variable to recognize the installed software by adding into the \verb{~/.Renviron} file the following: \if{html}{\out{
}}\preformatted{touch ~/.Renviron -cat << "EOF" > ~./Renviron +cat << "EOF" > ~/.Renviron ## macrtools - gfortran: start PATH=$\{PATH\}:/opt/gfortran/bin ## macrtools - gfortran: end @@ -140,7 +140,7 @@ Lastly, we modify the \code{PATH} environment variable to recognize the newly installed software by adding into the \verb{~/.Renviron} file the following: \if{html}{\out{
}}\preformatted{touch ~/.Renviron -cat << "EOF" > ~./Renviron +cat << "EOF" > ~/.Renviron ## macrtools - gfortran: start PATH=$\{PATH\}:/usr/local/gfortran/bin ## macrtools - gfortran: end @@ -178,7 +178,7 @@ Lastly, we modify the \code{PATH} environment variable to recognize the newly installed software by adding into the \verb{~/.Renviron} file the following: \if{html}{\out{
}}\preformatted{touch ~/.Renviron -cat << "EOF" > ~./Renviron +cat << "EOF" > ~/.Renviron ## macrtools - gfortran: start PATH=$\{PATH\}:/opt/R/arm64/gfortran/bin ## macrtools - gfortran: end diff --git a/man/macos-rtools.Rd b/man/macos-rtools.Rd index 5aae017..a212077 100644 --- a/man/macos-rtools.Rd +++ b/man/macos-rtools.Rd @@ -22,7 +22,7 @@ macos_rtools_uninstall( } \description{ The \code{macos_rtools_install()} function aims to install all required dependencies -for the macOS R compilation toolchain. Meanwhile, the \code{macros_rtools_uninstall()} +for the macOS R compilation toolchain. Meanwhile, the \code{macos_rtools_uninstall()} function aims to remove any installed files from your computer. } \details{ @@ -33,11 +33,11 @@ The macOS R compilation toolchain consists of: \item A series of binary packages from the \href{https://github.com/R-macos/recipes}{\code{recipes}} system to compile R. } -The \code{mac_rtools_install()} function attempts to install each of the required +The \code{macos_rtools_install()} function attempts to install each of the required components. If we detect that the Xcode.app IDE is installed, we'll skip attempting to install the Xcode CLI software. -Meanwhile, the \code{mac_rtools_uninstall()} function aims to +Meanwhile, the \code{macos_rtools_uninstall()} function aims to delete or uninstall the Xcode CLI and gfortran binaries. At the present moment, there is no support for uninstalling the binary packages from \code{recipes}. } diff --git a/man/xcode-cli.Rd b/man/xcode-cli.Rd index 3d99e11..7b6b123 100644 --- a/man/xcode-cli.Rd +++ b/man/xcode-cli.Rd @@ -100,7 +100,7 @@ where \verb{$product_information} is obtained from the previous command. Finally, we remove the temporary installation file. -\if{html}{\out{
}}\preformatted{touch /tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress +\if{html}{\out{
}}\preformatted{rm /tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress }\if{html}{\out{
}} These steps were obtained from Timothy Sutton's From 9d05713dae646621e8545e1ead1f63b9789cdfbd Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Tue, 23 Jun 2026 10:03:01 -0500 Subject: [PATCH 03/18] Remove unused timeout parameter and circular rlang check - shell_execute() declared a timeout= parameter that the body never used and no caller passes; drop it and its @param. - .onLoad() called rlang::check_installed("rlang"), which is circular: the rlang:: prefix already loads rlang, a hard Imports dependency. --- R/shell.R | 3 +-- R/zzz.R | 3 --- man/shell_execute.Rd | 10 +--------- 3 files changed, 2 insertions(+), 14 deletions(-) diff --git a/R/shell.R b/R/shell.R index 5a79dcf..534972e 100644 --- a/R/shell.R +++ b/R/shell.R @@ -64,10 +64,9 @@ shell_sudo_command <- function(cmd, password, verbose = TRUE, prefix = "sudo -kS #' @param sudo Whether to use sudo (default: FALSE) #' @param password User password for sudo (only required when sudo=TRUE) #' @param verbose Display the command being executed -#' @param timeout Timeout in seconds (default: 300) #' @return The exit status of the command (0 for success) #' @keywords internal -shell_execute <- function(cmd, sudo = FALSE, password = NULL, verbose = TRUE, timeout = 300) { +shell_execute <- function(cmd, sudo = FALSE, password = NULL, verbose = TRUE) { command_start_time <- base::Sys.time() result <- if (sudo) { diff --git a/R/zzz.R b/R/zzz.R index b7c4889..1af4821 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -40,9 +40,6 @@ } .onLoad <- function(libname, pkgname) { - # Import rlang functions - rlang::check_installed("rlang") - # Set cli options op <- base::options() op.macrtools <- base::list( diff --git a/man/shell_execute.Rd b/man/shell_execute.Rd index 771dc25..d1fc85c 100644 --- a/man/shell_execute.Rd +++ b/man/shell_execute.Rd @@ -4,13 +4,7 @@ \alias{shell_execute} \title{Execute a shell command} \usage{ -shell_execute( - cmd, - sudo = FALSE, - password = NULL, - verbose = TRUE, - timeout = 300 -) +shell_execute(cmd, sudo = FALSE, password = NULL, verbose = TRUE) } \arguments{ \item{cmd}{The command to execute} @@ -20,8 +14,6 @@ shell_execute( \item{password}{User password for sudo (only required when sudo=TRUE)} \item{verbose}{Display the command being executed} - -\item{timeout}{Timeout in seconds (default: 300)} } \value{ The exit status of the command (0 for success) From 151d47570df30962ee6788c3b80ca15f0d3c5369 Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Tue, 23 Jun 2026 10:03:28 -0500 Subject: [PATCH 04/18] Align xcode_cli_reset zero-status check with file convention Use base::identical(status, 0L) like the sibling xcode_cli_* helpers instead of the lone '== 0L', which is more robust to length-0/NA status. --- R/xcode.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/xcode.R b/R/xcode.R index 5d59ea3..3a18259 100644 --- a/R/xcode.R +++ b/R/xcode.R @@ -460,7 +460,7 @@ xcode_cli_reset <- function(password = base::getOption("macrtools.password"), ve password = password, verbose = verbose) - xcli_reset_clean <- xcli_reset_status == 0L + xcli_reset_clean <- base::identical(xcli_reset_status, 0L) if(base::isFALSE(xcli_reset_clean)) { cli::cli_abort("{.pkg macrtools}: Failed to reset Xcode CLI settings.") From 3e0f1aa642aeb3e93bd58ddf7e9859be41f2f282 Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Tue, 23 Jun 2026 10:05:13 -0500 Subject: [PATCH 05/18] Collapse near-identical gfortran installers into URL-parameterized helpers install_gfortran_12_arm/11_arm were byte-identical except for the URL, as were install_gfortran_14_2_universal/12_2_universal. Extract install_gfortran_arm_tar() and install_gfortran_universal_pkg() and have the named entry points delegate with their URL. Behavior and the version-dispatch entry points are unchanged. --- R/gfortran.R | 82 +++++++++++++++++++++++++--------------------------- 1 file changed, 40 insertions(+), 42 deletions(-) diff --git a/R/gfortran.R b/R/gfortran.R index c5f5093..2b6e148 100644 --- a/R/gfortran.R +++ b/R/gfortran.R @@ -536,17 +536,18 @@ install_gfortran_82_mojave <- function(password = base::getOption("macrtools.pas } -install_gfortran_12_arm <- function(password = base::getOption("macrtools.password"), - verbose = TRUE) { - - gfortran_12_url <- "https://mac.r-project.org/tools/gfortran-12.0.1-20220312-is-darwin20-arm64.tar.xz" +# Shared download + tar install for the arm64 legacy gfortran builds; the two +# named entry points differ only in their download URL. +install_gfortran_arm_tar <- function(url, + password = base::getOption("macrtools.password"), + verbose = TRUE) { # Figure out installation directories install_directory <- install_location() strip_levels <- install_strip_level() # Download tar - path_to_tar <- binary_download(gfortran_12_url, verbose = verbose) + path_to_tar <- binary_download(url, verbose = verbose) # Install tar into the appropriate location tar_package_install(path_to_tar, @@ -558,25 +559,38 @@ install_gfortran_12_arm <- function(password = base::getOption("macrtools.passwo } -install_gfortran_11_arm <- function(password = base::getOption("macrtools.password"), +install_gfortran_12_arm <- function(password = base::getOption("macrtools.password"), verbose = TRUE) { + install_gfortran_arm_tar( + "https://mac.r-project.org/tools/gfortran-12.0.1-20220312-is-darwin20-arm64.tar.xz", + password = password, + verbose = verbose) +} - gfortran_11_url <- "https://mac.r-project.org/libs-arm64/gfortran-f51f1da0-darwin20.0-arm64.tar.gz" - # Figure out installation directories - install_directory <- install_location() - strip_levels <- install_strip_level() +install_gfortran_11_arm <- function(password = base::getOption("macrtools.password"), + verbose = TRUE) { + install_gfortran_arm_tar( + "https://mac.r-project.org/libs-arm64/gfortran-f51f1da0-darwin20.0-arm64.tar.gz", + password = password, + verbose = verbose) +} - # Download tar - path_to_tar <- binary_download(gfortran_11_url, verbose = verbose) - # Install tar into the appropriate location - tar_package_install(path_to_tar, - install_directory, - strip_levels, - password = password, - verbose = verbose) +# Shared download + pkg install for the universal gfortran builds; the two +# named entry points differ only in their download URL. +install_gfortran_universal_pkg <- function(url, + password = base::getOption("macrtools.password"), + verbose = TRUE) { + # Download pkg + path_to_pkg <- binary_download(url, verbose = verbose) + + # Install pkg into the appropriate location + pkg_install(path_to_pkg, + "/", + password = password, + verbose = verbose) } @@ -590,18 +604,10 @@ install_gfortran_11_arm <- function(password = base::getOption("macrtools.passwo install_gfortran_14_2_universal <- function( password = base::getOption("macrtools.password"), verbose = TRUE) { - - # URL for gfortran 14.2 universal installer for R 4.5-4.6 - gfortran_14_universal <- "https://github.com/R-macos/gcc-14-branch/releases/download/gcc-14.2-darwin-r2.1/gfortran-14.2-universal.pkg" - - # Download pkg - path_to_pkg <- binary_download(gfortran_14_universal, verbose = verbose) - - # Install pkg into the appropriate location - pkg_install(path_to_pkg, - "/", - password = password, - verbose = verbose) + install_gfortran_universal_pkg( + "https://github.com/R-macos/gcc-14-branch/releases/download/gcc-14.2-darwin-r2.1/gfortran-14.2-universal.pkg", + password = password, + verbose = verbose) } @@ -615,16 +621,8 @@ install_gfortran_14_2_universal <- function( install_gfortran_12_2_universal <- function( password = base::getOption("macrtools.password"), verbose = TRUE) { - - # URL - gfortran_12_universal <- "https://mac.r-project.org/tools/gfortran-12.2-universal.pkg" - - # Download tar - path_to_pkg <- binary_download(gfortran_12_universal, verbose = verbose) - - # Install pkg into the appropriate location - pkg_install(path_to_pkg, - "/", - password = password, - verbose = verbose) + install_gfortran_universal_pkg( + "https://mac.r-project.org/tools/gfortran-12.2-universal.pkg", + password = password, + verbose = verbose) } From 0dc3e62ffe69b94fc79a01c2141ef56cd1887f48 Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Tue, 23 Jun 2026 10:07:58 -0500 Subject: [PATCH 06/18] Extract exec_text() helper for repeated version-lookup idiom The pattern tryCatch(sys::as_text(sys::exec_internal(cmd, args)$stdout), error = function(e) fallback) was copy-pasted across toolchain.R (xcode-select, xcodebuild, gfortran), gfortran.R (gfortran) and openmp.R (otool, clang). Replace with a single exec_text(command, args, fallback = "Unknown") helper. The codesign (stderr) and structured gfortran()/xcode wrappers are left as-is since they read a different stream / return structured output. --- R/gfortran.R | 5 +---- R/openmp.R | 10 ++-------- R/system.R | 18 ++++++++++++++++++ R/toolchain.R | 15 +++------------ man/exec_text.Rd | 24 ++++++++++++++++++++++++ 5 files changed, 48 insertions(+), 24 deletions(-) create mode 100644 man/exec_text.Rd diff --git a/R/gfortran.R b/R/gfortran.R index 2b6e148..0478c30 100644 --- a/R/gfortran.R +++ b/R/gfortran.R @@ -187,10 +187,7 @@ gfortran_install <- function(password = base::getOption("macrtools.password"), v if(verbose) { # Get gfortran path and version info install_path <- base::file.path(gfortran_install_location(), 'gfortran') - version_info <- base::tryCatch( - sys::as_text(sys::exec_internal('gfortran', '--version')$stdout), - error = function(e) 'Unknown' - ) + version_info <- exec_text('gfortran', '--version') cli::cli_alert_info("{.pkg macrtools}: gfortran is already installed.") cli::cli_bullets(c( diff --git a/R/openmp.R b/R/openmp.R index 00019be..b633da9 100644 --- a/R/openmp.R +++ b/R/openmp.R @@ -61,10 +61,7 @@ openmp_version <- function() { # Try to get version information from the library library_path <- "/usr/local/lib/libomp.dylib" - version_info <- base::tryCatch( - sys::as_text(sys::exec_internal('otool', c('-L', library_path))$stdout), - error = function(e) 'Unknown' - ) + version_info <- exec_text('otool', c('-L', library_path)) version_info } @@ -314,10 +311,7 @@ openmp_uninstall <- function(password = base::getOption("macrtools.password"), #' Get Apple Clang Version Information #' @noRd get_apple_clang_version <- function() { - version_info <- base::tryCatch( - sys::as_text(sys::exec_internal('clang', '--version')$stdout), - error = function(e) NULL - ) + version_info <- exec_text('clang', '--version', fallback = NULL) if (base::is.null(version_info)) { return(list(version_string = "Unknown", build_number = 0)) diff --git a/R/system.R b/R/system.R index 847f7dd..cfc8a6e 100644 --- a/R/system.R +++ b/R/system.R @@ -46,6 +46,24 @@ shell_mac_version <- function() { sys::as_text(sys::exec_internal("sw_vers", "-productVersion")$stdout) } +#' Capture the Text Output of an External Command +#' +#' Runs `command` with `args` via [sys::exec_internal()] and returns its +#' standard output as trimmed text. If the command cannot be run, `fallback` +#' is returned instead. +#' +#' @param command Name of the program to execute. +#' @param args Character vector of arguments passed to the program. +#' @param fallback Value returned when execution fails. Default `"Unknown"`. +#' @return The command's standard output as text, or `fallback` on error. +#' @keywords internal +exec_text <- function(command, args, fallback = "Unknown") { + base::tryCatch( + sys::as_text(sys::exec_internal(command, args)$stdout), + error = function(e) fallback + ) +} + #' Check if macOS Version is Supported for R #' #' @return TRUE if macOS version is supported, FALSE otherwise diff --git a/R/toolchain.R b/R/toolchain.R index b1db45b..0a4895b 100644 --- a/R/toolchain.R +++ b/R/toolchain.R @@ -168,10 +168,7 @@ rtools_install_xcode_cli <- function(entered_password, verbose, describe_steps, } else { if(describe_steps) { # Get Xcode CLI version information - xcode_version <- base::tryCatch( - sys::as_text(sys::exec_internal('xcode-select', '--version')$stdout), - error = function(e) 'Unknown' - ) + xcode_version <- exec_text('xcode-select', '--version') cli::cli_alert_info("{.pkg macrtools}: Xcode Command Line Tools already installed.") cli::cli_bullets(c( @@ -186,10 +183,7 @@ rtools_install_xcode_cli <- function(entered_password, verbose, describe_steps, } else { if(describe_steps) { # Get full Xcode app version information - xcode_app_info <- base::tryCatch( - sys::as_text(sys::exec_internal('xcodebuild', '-version')$stdout), - error = function(e) "Unknown" - ) + xcode_app_info <- exec_text('xcodebuild', '-version') cli::cli_alert_info("{.pkg macrtools}: Full Xcode.app IDE is installed.") cli::cli_bullets(c( @@ -246,10 +240,7 @@ rtools_install_gfortran <- function(entered_password, verbose, describe_steps, p } else { if(describe_steps) { # Get gfortran version information - gfortran_version_info <- base::tryCatch( - sys::as_text(sys::exec_internal('gfortran', '--version')$stdout), - error = function(e) 'Unknown' - ) + gfortran_version_info <- exec_text('gfortran', '--version') install_path <- base::file.path(gfortran_install_location(), 'gfortran') diff --git a/man/exec_text.Rd b/man/exec_text.Rd new file mode 100644 index 0000000..9432269 --- /dev/null +++ b/man/exec_text.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/system.R +\name{exec_text} +\alias{exec_text} +\title{Capture the Text Output of an External Command} +\usage{ +exec_text(command, args, fallback = "Unknown") +} +\arguments{ +\item{command}{Name of the program to execute.} + +\item{args}{Character vector of arguments passed to the program.} + +\item{fallback}{Value returned when execution fails. Default \code{"Unknown"}.} +} +\value{ +The command's standard output as text, or \code{fallback} on error. +} +\description{ +Runs \code{command} with \code{args} via \code{\link[sys:exec_internal]{sys::exec_internal()}} and returns its +standard output as trimmed text. If the command cannot be run, \code{fallback} +is returned instead. +} +\keyword{internal} From effc0134da0bb5382c6dbf174f2c5b0c76c340a9 Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Tue, 23 Jun 2026 10:11:44 -0500 Subject: [PATCH 07/18] Extract small duplicated literals and parsing blocks - timestamp_now(): centralize the '%Y-%m-%d %H:%M:%S' format used twice in toolchain.R. - remove_file_if_exists(): replace the duplicated temp-file cleanup blocks in xcode_cli_install(). - ls_ld_fields(path, fields): collapse the two 'ls -ld' field-parsing tryCatch blocks in create_install_location(). - Hoist the 'hdiutil detach' command, built twice in dmg_package_install(), into a single detach_cmd. The block-delimiter defaults in blocks.R are left as-is to preserve parity with the upstream usethis/rlang code it is vendored from. --- R/installers.R | 26 ++++++++++++++------------ R/toolchain.R | 4 ++-- R/utils.R | 18 ++++++++++++++++++ R/xcode.R | 8 ++------ man/remove_file_if_exists.Rd | 15 +++++++++++++++ man/timestamp_now.Rd | 15 +++++++++++++++ 6 files changed, 66 insertions(+), 20 deletions(-) create mode 100644 man/remove_file_if_exists.Rd create mode 100644 man/timestamp_now.Rd diff --git a/R/installers.R b/R/installers.R index a3c046e..6d05ecc 100644 --- a/R/installers.R +++ b/R/installers.R @@ -61,6 +61,15 @@ gfortran_install_location <- function(arch = system_arch()) { } } +# Parse selected space-delimited fields from `ls -ld `, returning +# "Unknown" if the command fails. +ls_ld_fields <- function(path, fields) { + base::tryCatch( + base::paste(base::strsplit(base::system(base::paste('ls -ld', path), intern = TRUE), ' ')[[1]][fields], collapse = ' '), + error = function(e) "Unknown" + ) +} + create_install_location <- function(arch = system_arch(), password = base::getOption("macrtools.password")) { install_dir <- install_location(arch) @@ -70,10 +79,7 @@ create_install_location <- function(arch = system_arch(), password = base::getOp } # Get current directory permissions - dir_perms <- base::tryCatch( - base::paste(base::strsplit(base::system(base::paste('ls -ld', base::dirname(install_dir)), intern = TRUE), ' ')[[1]][1:3], collapse = ' '), - error = function(e) "Unknown" - ) + dir_perms <- ls_ld_fields(base::dirname(install_dir), 1:3) current_user <- base::Sys.info()['user'] @@ -104,10 +110,7 @@ create_install_location <- function(arch = system_arch(), password = base::getOp )) } else { # Get owner of new directory - new_owner <- base::tryCatch( - base::paste(base::strsplit(base::system(base::paste('ls -ld', install_dir), intern = TRUE), ' ')[[1]][3], collapse = ' '), - error = function(e) "Unknown" - ) + new_owner <- ls_ld_fields(install_dir, 3) cli::cli_alert_success("{.pkg macrtools}: Installation directory created successfully.") cli::cli_bullets(c( @@ -288,6 +291,7 @@ dmg_package_install <- function(path_to_dmg, volume_with_extension <- base::basename(path_to_dmg) bare_volume <- tools::file_path_sans_ext(volume_with_extension) + detach_cmd <- base::paste("hdiutil", "detach", base::shQuote(base::file.path("/Volumes", bare_volume))) if (verbose) { cli::cli_alert_info("{.pkg macrtools}: Mounting disk image.") @@ -331,8 +335,7 @@ dmg_package_install <- function(path_to_dmg, if (install_status != 0) { # Unmount the volume before surfacing the failure so we do not leak it. - cmd <- base::paste("hdiutil", "detach", base::shQuote(base::file.path("/Volumes", bare_volume))) - shell_execute(cmd, sudo = FALSE) + shell_execute(detach_cmd, sudo = FALSE) cli::cli_abort(c( "{.pkg macrtools}: Failed to install package from disk image.", "Disk image: {.file {volume_with_extension}}", @@ -348,8 +351,7 @@ dmg_package_install <- function(path_to_dmg, cli::cli_text("") # Add spacing } - cmd <- base::paste("hdiutil", "detach", base::shQuote(base::file.path("/Volumes", bare_volume))) - status <- shell_execute(cmd, sudo = FALSE) + status <- shell_execute(detach_cmd, sudo = FALSE) if (status != 0) { cli::cli_alert_warning(c( diff --git a/R/toolchain.R b/R/toolchain.R index 0a4895b..8e7f2dd 100644 --- a/R/toolchain.R +++ b/R/toolchain.R @@ -78,7 +78,7 @@ macos_rtools_install <- function( total = 100 ) - current_time <- base::format(base::Sys.time(), '%Y-%m-%d %H:%M:%S') + current_time <- timestamp_now() cli::cli_alert_info("Installation process started at: {.val {current_time}}") cli::cli_text("") # Add spacing } @@ -308,7 +308,7 @@ rtools_install_summary <- function(result_xcode, result_gfortran, result_base_de cli::cli_text("You can install packages from source with: {.code install.packages('package_name', type = 'source')}") cli::cli_text("") # Add spacing - current_time <- base::format(base::Sys.time(), '%Y-%m-%d %H:%M:%S') + current_time <- timestamp_now() cli::cli_alert_info("Installation completed at: {.val {current_time}}") } else { cli::cli_abort(c( diff --git a/R/utils.R b/R/utils.R index 8a066fe..c25c901 100644 --- a/R/utils.R +++ b/R/utils.R @@ -50,3 +50,21 @@ force_password <- function(supplied_password) { caller_env <- function(n = 1) { base::parent.frame(n + 1) } + +#' Current Timestamp as a Formatted String +#' +#' @return The current time formatted as `"%Y-%m-%d %H:%M:%S"`. +#' @keywords internal +timestamp_now <- function() { + base::format(base::Sys.time(), '%Y-%m-%d %H:%M:%S') +} + +#' Remove a File if it Exists +#' +#' @param path Path to the file to remove. +#' @keywords internal +remove_file_if_exists <- function(path) { + if (base::file.exists(path)) { + base::file.remove(path) + } +} diff --git a/R/xcode.R b/R/xcode.R index 3a18259..fbb4f60 100644 --- a/R/xcode.R +++ b/R/xcode.R @@ -234,9 +234,7 @@ xcode_cli_install <- function(password = base::getOption("macrtools.password"), if (base::length(product_information) == 0) { # Remove temporary in-progress file if left in place before aborting. - if(base::file.exists(temporary_xcli_file)) { - base::file.remove(temporary_xcli_file) - } + remove_file_if_exists(temporary_xcli_file) cli::cli_abort(c( "{.pkg macrtools}: Could not find Xcode CLI in software updates.", "i" = "Try installing manually with 'xcode-select --install' in Terminal." @@ -258,9 +256,7 @@ xcode_cli_install <- function(password = base::getOption("macrtools.password"), sudo = TRUE, password = password, verbose = verbose) # Remove temporary in-progress file if left in place - if(base::file.exists(temporary_xcli_file)) { - base::file.remove(temporary_xcli_file) - } + remove_file_if_exists(temporary_xcli_file) xcli_clean <- base::identical(xcli_status, 0L) diff --git a/man/remove_file_if_exists.Rd b/man/remove_file_if_exists.Rd new file mode 100644 index 0000000..def4fb8 --- /dev/null +++ b/man/remove_file_if_exists.Rd @@ -0,0 +1,15 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils.R +\name{remove_file_if_exists} +\alias{remove_file_if_exists} +\title{Remove a File if it Exists} +\usage{ +remove_file_if_exists(path) +} +\arguments{ +\item{path}{Path to the file to remove.} +} +\description{ +Remove a File if it Exists +} +\keyword{internal} diff --git a/man/timestamp_now.Rd b/man/timestamp_now.Rd new file mode 100644 index 0000000..6273d35 --- /dev/null +++ b/man/timestamp_now.Rd @@ -0,0 +1,15 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils.R +\name{timestamp_now} +\alias{timestamp_now} +\title{Current Timestamp as a Formatted String} +\usage{ +timestamp_now() +} +\value{ +The current time formatted as \code{"\%Y-\%m-\%d \%H:\%M:\%S"}. +} +\description{ +Current Timestamp as a Formatted String +} +\keyword{internal} From d93cc3c6f27622ca1b93b51f356c29ccb58beb7d Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Tue, 23 Jun 2026 10:16:02 -0500 Subject: [PATCH 08/18] Centralize OpenMP path and Makevars-flag literals Introduce openmp_library_path(), openmp_header_path() and openmp_makevars_lines() as the single source of truth for the OpenMP install paths and compiler flags, replacing the scattered string literals across is_openmp_installed/openmp_version/openmp_install/openmp_uninstall/openmp_test and configure_openmp_makevars. Display sites wrap the flags with the existing two-space indent via paste0(); the written-config and bare message sites use them directly, so rendered output is unchanged. Also dedupe the default OpenMP filename by reusing openmp_mapping$filename[1] for the fallback. --- R/openmp.R | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/R/openmp.R b/R/openmp.R index b633da9..9d7f5a5 100644 --- a/R/openmp.R +++ b/R/openmp.R @@ -45,8 +45,8 @@ is_openmp_installed <- function() { assert_mac() # Check if the main library file exists - library_path <- "/usr/local/lib/libomp.dylib" - header_path <- "/usr/local/include/omp.h" + library_path <- openmp_library_path() + header_path <- openmp_header_path() # Both library and header should exist base::file.exists(library_path) && base::file.exists(header_path) @@ -60,7 +60,7 @@ openmp_version <- function() { } # Try to get version information from the library - library_path <- "/usr/local/lib/libomp.dylib" + library_path <- openmp_library_path() version_info <- exec_text('otool', c('-L', library_path)) version_info @@ -120,7 +120,7 @@ openmp_install <- function(password = base::getOption("macrtools.password"), ver if(base::isTRUE(is_openmp_installed())) { if(verbose) { # Get OpenMP version info - library_path <- "/usr/local/lib/libomp.dylib" + library_path <- openmp_library_path() version_info <- base::tryCatch( base::system(base::paste('otool -L', library_path, '| grep libomp'), intern = TRUE), error = function(e) 'Unknown' @@ -195,16 +195,14 @@ openmp_install <- function(password = base::getOption("macrtools.password"), ver cli::cli_alert_warning("{.pkg macrtools}: OpenMP installed but Makevars configuration failed.") cli::cli_bullets(c( "You may need to manually add the following to ~/.R/Makevars:", - " CPPFLAGS += -Xclang -fopenmp", - " LDFLAGS += -lomp" + base::paste0(" ", openmp_makevars_lines()) )) cli::cli_text("") # Add spacing } } else if (verbose) { cli::cli_alert_info("{.pkg macrtools}: To use OpenMP, add the following to your ~/.R/Makevars:") cli::cli_bullets(c( - " CPPFLAGS += -Xclang -fopenmp", - " LDFLAGS += -lomp" + base::paste0(" ", openmp_makevars_lines()) )) cli::cli_text("") # Add spacing } @@ -247,8 +245,8 @@ openmp_uninstall <- function(password = base::getOption("macrtools.password"), # Files to remove files_to_remove <- c( - "/usr/local/lib/libomp.dylib", - "/usr/local/include/omp.h", + openmp_library_path(), + openmp_header_path(), "/usr/local/include/ompt.h", "/usr/local/include/omp-tools.h", "/usr/local/include/ompx.h" # May not exist in older versions @@ -308,6 +306,14 @@ openmp_uninstall <- function(password = base::getOption("macrtools.password"), # Helper functions ---- +# Default OpenMP install locations and Makevars flags, kept here as the single +# source of truth for the library/header paths and compiler flags. +openmp_library_path <- function() "/usr/local/lib/libomp.dylib" +openmp_header_path <- function() "/usr/local/include/omp.h" +openmp_makevars_lines <- function() { + c("CPPFLAGS += -Xclang -fopenmp", "LDFLAGS += -lomp") +} + #' Get Apple Clang Version Information #' @noRd get_apple_clang_version <- function() { @@ -385,7 +391,7 @@ get_openmp_url_for_xcode <- function() { if (base::is.null(selected_file)) { # Default to latest if version is too new or couldn't be detected - selected_file <- "openmp-19.1.5-darwin20-Release.tar.gz" + selected_file <- openmp_mapping$filename[1] } base::paste0("https://mac.r-project.org/openmp/", selected_file) @@ -459,21 +465,19 @@ openmp_test <- function() { cli::cli_alert_warning("{.pkg macrtools}: OpenMP flags not found in ~/.R/Makevars") cli::cli_bullets(c( "Add the following lines to ~/.R/Makevars:", - " CPPFLAGS += -Xclang -fopenmp", - " LDFLAGS += -lomp" + base::paste0(" ", openmp_makevars_lines()) )) } } else { cli::cli_alert_info("{.pkg macrtools}: ~/.R/Makevars not found.") cli::cli_bullets(c( "Create ~/.R/Makevars with the following content:", - " CPPFLAGS += -Xclang -fopenmp", - " LDFLAGS += -lomp" + base::paste0(" ", openmp_makevars_lines()) )) } # Test library signature - library_path <- "/usr/local/lib/libomp.dylib" + library_path <- openmp_library_path() signature_info <- base::tryCatch( sys::as_text(sys::exec_internal('codesign', c('-d', '-vv', library_path))$stderr), error = function(e) "Could not verify signature" @@ -524,10 +528,7 @@ configure_openmp_makevars <- function(verbose = TRUE) { } # Define the OpenMP configuration - openmp_config <- c( - "CPPFLAGS += -Xclang -fopenmp", - "LDFLAGS += -lomp" - ) + openmp_config <- openmp_makevars_lines() # Use the block system to add configuration config_result <- base::tryCatch({ @@ -549,8 +550,7 @@ configure_openmp_makevars <- function(verbose = TRUE) { if (config_result && verbose) { cli::cli_alert_success("{.pkg macrtools}: OpenMP flags added to ~/.R/Makevars") cli::cli_bullets(c( - "CPPFLAGS += -Xclang -fopenmp", - "LDFLAGS += -lomp", + openmp_makevars_lines(), "You may need to restart R for the changes to take effect." )) cli::cli_text("") # Add spacing From 54cc743e8791be058631dd43169152afd6a2974c Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Tue, 23 Jun 2026 10:20:09 -0500 Subject: [PATCH 09/18] Collapse macOS version predicates into macos_version_in_range helper The 9 is_macos_() detectors and is_macos_r_supported() repeated the body 'mac_version <- shell_mac_version(); version_between(mac_version, lo, hi)'; introduce macos_version_in_range(lower, upper) and make each a one-liner. Update the is_macos_r_supported test to mock shell_mac_version at the namespace level (so the stub reaches through the new helper) and exercise the real version_between logic, and add a focused test for macos_version_in_range. --- R/system.R | 40 +++++++++++++++++------------------ man/macos_version_in_range.Rd | 20 ++++++++++++++++++ tests/testthat/test-system.R | 16 ++++++++++---- 3 files changed, 52 insertions(+), 24 deletions(-) create mode 100644 man/macos_version_in_range.Rd diff --git a/R/system.R b/R/system.R index cfc8a6e..87c653b 100644 --- a/R/system.R +++ b/R/system.R @@ -64,13 +64,22 @@ exec_text <- function(command, args, fallback = "Unknown") { ) } +#' Check if the macOS Version Falls in a Range +#' +#' @param lower Lower bound for the macOS version (inclusive). +#' @param upper Upper bound for the macOS version (exclusive). +#' @return TRUE if the running macOS version is in `[lower, upper)`. +#' @keywords internal +macos_version_in_range <- function(lower, upper) { + version_between(shell_mac_version(), lower, upper) +} + #' Check if macOS Version is Supported for R #' #' @return TRUE if macOS version is supported, FALSE otherwise #' @keywords internal is_macos_r_supported <- function() { - mac_version <- shell_mac_version() - version_between(mac_version, "10.13.0", "27.0") + macos_version_in_range("10.13.0", "27.0") } #' Check if macOS Tahoe @@ -83,8 +92,7 @@ is_macos_r_supported <- function() { #' @return TRUE if system is macOS Tahoe, FALSE otherwise #' @keywords internal is_macos_tahoe <- function() { - mac_version <- shell_mac_version() - version_between(mac_version, "26.0", "27.0") + macos_version_in_range("26.0", "27.0") } #' Check if macOS Sequoia @@ -98,8 +106,7 @@ is_macos_tahoe <- function() { #' @return TRUE if system is macOS Sequoia, FALSE otherwise #' @keywords internal is_macos_sequoia <- function() { - mac_version <- shell_mac_version() - version_between(mac_version, "15.0.0", "16.0.0") + macos_version_in_range("15.0.0", "16.0.0") } #' Check if macOS Sonoma @@ -113,8 +120,7 @@ is_macos_sequoia <- function() { #' @return TRUE if system is macOS Sonoma, FALSE otherwise #' @keywords internal is_macos_sonoma <- function() { - mac_version <- shell_mac_version() - version_between(mac_version, "14.0.0", "15.0.0") + macos_version_in_range("14.0.0", "15.0.0") } #' Check if macOS Ventura @@ -122,8 +128,7 @@ is_macos_sonoma <- function() { #' @return TRUE if system is macOS Ventura, FALSE otherwise #' @keywords internal is_macos_ventura <- function() { - mac_version <- shell_mac_version() - version_between(mac_version, "13.0.0", "14.0.0") + macos_version_in_range("13.0.0", "14.0.0") } #' Check if macOS Monterey @@ -131,8 +136,7 @@ is_macos_ventura <- function() { #' @return TRUE if system is macOS Monterey, FALSE otherwise #' @keywords internal is_macos_monterey <- function() { - mac_version <- shell_mac_version() - version_between(mac_version, "12.0.0", "13.0.0") + macos_version_in_range("12.0.0", "13.0.0") } #' Check if macOS Big Sur @@ -140,8 +144,7 @@ is_macos_monterey <- function() { #' @return TRUE if system is macOS Big Sur, FALSE otherwise #' @keywords internal is_macos_big_sur <- function() { - mac_version <- shell_mac_version() - version_between(mac_version, "11.0.0", "12.0.0") + macos_version_in_range("11.0.0", "12.0.0") } #' Check if macOS Catalina @@ -149,8 +152,7 @@ is_macos_big_sur <- function() { #' @return TRUE if system is macOS Catalina, FALSE otherwise #' @keywords internal is_macos_catalina <- function() { - mac_version <- shell_mac_version() - version_between(mac_version, "10.15.0", "10.16.0") + macos_version_in_range("10.15.0", "10.16.0") } #' Check if macOS Mojave @@ -158,8 +160,7 @@ is_macos_catalina <- function() { #' @return TRUE if system is macOS Mojave, FALSE otherwise #' @keywords internal is_macos_mojave <- function() { - mac_version <- shell_mac_version() - version_between(mac_version, "10.14.0", "10.15.0") + macos_version_in_range("10.14.0", "10.15.0") } #' Check if macOS High Sierra @@ -167,8 +168,7 @@ is_macos_mojave <- function() { #' @return TRUE if system is macOS High Sierra, FALSE otherwise #' @keywords internal is_macos_high_sierra <- function() { - mac_version <- shell_mac_version() - version_between(mac_version, "10.13.0", "10.14.0") + macos_version_in_range("10.13.0", "10.14.0") } #' Check if Version is Between Bounds diff --git a/man/macos_version_in_range.Rd b/man/macos_version_in_range.Rd new file mode 100644 index 0000000..c6bbcaf --- /dev/null +++ b/man/macos_version_in_range.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/system.R +\name{macos_version_in_range} +\alias{macos_version_in_range} +\title{Check if the macOS Version Falls in a Range} +\usage{ +macos_version_in_range(lower, upper) +} +\arguments{ +\item{lower}{Lower bound for the macOS version (inclusive).} + +\item{upper}{Upper bound for the macOS version (exclusive).} +} +\value{ +TRUE if the running macOS version is in \verb{[lower, upper)}. +} +\description{ +Check if the macOS Version Falls in a Range +} +\keyword{internal} diff --git a/tests/testthat/test-system.R b/tests/testthat/test-system.R index 5776548..32043b8 100644 --- a/tests/testthat/test-system.R +++ b/tests/testthat/test-system.R @@ -37,15 +37,23 @@ test_that("shell_mac_version returns the correct macOS version", { }) test_that("is_macos_r_supported correctly identifies supported macOS versions", { - mockery::stub(is_macos_r_supported, "shell_mac_version", function() "10.13.0") - mockery::stub(is_macos_r_supported, "version_between", function(...) TRUE) + # Mock at the namespace level so the stub reaches shell_mac_version through + # the shared macos_version_in_range() helper, and exercise the real + # version_between() logic. + local_mocked_bindings(shell_mac_version = function() "10.13.0") expect_true(is_macos_r_supported()) - mockery::stub(is_macos_r_supported, "shell_mac_version", function() "10.12.0") - mockery::stub(is_macos_r_supported, "version_between", function(...) FALSE) + local_mocked_bindings(shell_mac_version = function() "10.12.0") expect_false(is_macos_r_supported()) }) +test_that("macos_version_in_range checks the running macOS version against bounds", { + local_mocked_bindings(shell_mac_version = function() "14.2.0") + expect_true(macos_version_in_range("14.0.0", "15.0.0")) + expect_false(macos_version_in_range("15.0.0", "16.0.0")) + expect_false(macos_version_in_range("13.0.0", "14.0.0")) +}) + test_that("version_between correctly determines if version is within bounds", { expect_true(version_between("10.14.0", "10.13.0", "10.15.0")) expect_true(version_between("10.13.0", "10.13.0", "10.15.0")) From c387a2025f0178aac437a6dec4aa910b54e3d366 Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Tue, 23 Jun 2026 10:22:41 -0500 Subject: [PATCH 10/18] Route toolchain password prompts through force_password() macos_rtools_install() and macos_rtools_uninstall() each re-implemented the 'prompt for a password when NULL' logic with their own wording. Delegate both to the existing force_password() helper so the prompt text and NULL-handling live in one place. This unifies (and slightly changes) the two prompt messages. Update the uninstall tests to stub force_password() instead of the now-deeper askpass::askpass() call. The sudo-guarded prompt in recipes.R is left as-is since that file is vendored near-verbatim from R Core's installer. --- R/toolchain.R | 11 ++--------- tests/testthat/test-toolchain.R | 3 ++- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/R/toolchain.R b/R/toolchain.R index 8e7f2dd..c99d466 100644 --- a/R/toolchain.R +++ b/R/toolchain.R @@ -61,11 +61,7 @@ macos_rtools_install <- function( rtools_install_announce(os_version, os_release, arch, r_version) - entered_password <- password - if(base::is.null(entered_password)) { - cli::cli_alert_info("Administrative privileges are required for installation.") - entered_password <- askpass::askpass("Please enter your administrator password:") - } + entered_password <- force_password(password) describe_steps <- base::isTRUE(verbose) @@ -350,10 +346,7 @@ macos_rtools_uninstall <- function( )) cli::cli_text("") # Add spacing - if(base::is.null(password)) { - cli::cli_alert_info("{.pkg macrtools}: Administrative privileges required.") - password <- askpass::askpass("Please enter your password to continue:") - } + password <- force_password(password) # Create a progress bar if (verbose) { diff --git a/tests/testthat/test-toolchain.R b/tests/testthat/test-toolchain.R index e53c6f8..9abef9e 100644 --- a/tests/testthat/test-toolchain.R +++ b/tests/testthat/test-toolchain.R @@ -127,7 +127,7 @@ test_that("macos_rtools_uninstall handles component uninstallations", { mockery::stub(macos_rtools_uninstall, "cli::cli_alert_info", function(...) NULL) mockery::stub(macos_rtools_uninstall, "cli::cli_bullets", function(...) NULL) mockery::stub(macos_rtools_uninstall, "cli::cli_text", function(...) NULL) - mockery::stub(macos_rtools_uninstall, "askpass::askpass", function(...) "password") + mockery::stub(macos_rtools_uninstall, "force_password", function(...) "password") mockery::stub(macos_rtools_uninstall, "cli::cli_progress_bar", function(...) 1) mockery::stub(macos_rtools_uninstall, "cli::cli_progress_update", function(...) NULL) mockery::stub(macos_rtools_uninstall, "cli::cli_progress_done", function(...) NULL) @@ -158,6 +158,7 @@ test_that("macos_rtools_uninstall handles component failures", { mockery::stub(macos_rtools_uninstall, "cli::cli_progress_bar", function(...) 1) mockery::stub(macos_rtools_uninstall, "cli::cli_progress_update", function(...) NULL) mockery::stub(macos_rtools_uninstall, "cli::cli_abort", function(...) stop("Uninstallation failed")) + mockery::stub(macos_rtools_uninstall, "force_password", function(...) "password") # Mock component detection and uninstallation failure mockery::stub(macos_rtools_uninstall, "is_xcode_cli_installed", function() TRUE) From bceab8ec8b83c0d152de52148d06cc88706e24db Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Tue, 23 Jun 2026 10:24:38 -0500 Subject: [PATCH 11/18] Surface the dropped advice text in assertion errors Every assert_* helper passed advice='...' to cli::cli_abort(), but cli_abort has no advice parameter, so the guidance was captured into ... as a condition field and never shown. Fold each advice string into the message vector as an 'i' bullet so it renders. Add a test confirming the guidance now appears in the error message. --- R/assertions.R | 30 +++++++++++++++--------------- tests/testthat/test-assertions.R | 8 ++++++++ 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/R/assertions.R b/R/assertions.R index 83da8a0..643eca9 100644 --- a/R/assertions.R +++ b/R/assertions.R @@ -25,10 +25,10 @@ assert_mac <- function(call = caller_env()) { current_os <- system_os() cli::cli_abort(c( "{.pkg macrtools}: This function requires macOS.", - "{.pkg macrtools}: The current operating system is {.val {current_os}}." + "{.pkg macrtools}: The current operating system is {.val {current_os}}.", + "i" = "macrtools only works on macOS systems with Intel or Apple Silicon processors." ), - call = call, - advice = "macrtools only works on macOS systems with Intel or Apple Silicon processors.") + call = call) } } @@ -41,10 +41,10 @@ assert_macos_supported <- function(call = caller_env()) { mac_version <- shell_mac_version() cli::cli_abort(c( "{.pkg macrtools}: Your macOS version {.val {mac_version}} is not supported.", - "{.pkg macrtools}: Supported versions: macOS High Sierra (10.13) through macOS Tahoe (26.x)." + "{.pkg macrtools}: Supported versions: macOS High Sierra (10.13) through macOS Tahoe (26.x).", + "i" = "Please upgrade your macOS to a supported version or use an alternative method to install development tools." ), - call = call, - advice = "Please upgrade your macOS to a supported version or use an alternative method to install development tools.") + call = call) } } @@ -55,10 +55,10 @@ assert_aarch64 <- function(call = caller_env()) { arch <- system_arch() cli::cli_abort(c( "{.pkg macrtools}: This function requires an Apple Silicon (M-series) Mac.", - "{.pkg macrtools}: Current architecture: {.val {arch}}." + "{.pkg macrtools}: Current architecture: {.val {arch}}.", + "i" = "This feature is specifically designed for Apple Silicon processors (M1, M2, M3, etc.). Intel Macs require different components." ), - call = call, - advice = "This feature is specifically designed for Apple Silicon processors (M1, M2, M3, etc.). Intel Macs require different components.") + call = call) } } @@ -69,10 +69,10 @@ assert_x86_64 <- function(call = caller_env()) { arch <- system_arch() cli::cli_abort(c( "{.pkg macrtools}: This function requires an Intel-based Mac.", - "{.pkg macrtools}: Current architecture: {.val {arch}}." + "{.pkg macrtools}: Current architecture: {.val {arch}}.", + "i" = "This feature is specifically designed for Intel processors. Apple Silicon Macs require different components." ), - call = call, - advice = "This feature is specifically designed for Intel processors. Apple Silicon Macs require different components.") + call = call) } } @@ -85,9 +85,9 @@ assert_r_version_supported <- function(call = caller_env()) { supported_max <- maximum_supported_r_version() cli::cli_abort(c( "{.pkg macrtools}: The installed R version {.val {version_number}} is not supported.", - "{.pkg macrtools}: Supported versions: R {supported_min}.x through R {supported_max}.x." + "{.pkg macrtools}: Supported versions: R {supported_min}.x through R {supported_max}.x.", + "i" = "Please upgrade or downgrade your R installation to a supported version." ), - call = call, - advice = "Please upgrade or downgrade your R installation to a supported version.") + call = call) } } diff --git a/tests/testthat/test-assertions.R b/tests/testthat/test-assertions.R index 7f58146..60e0448 100644 --- a/tests/testthat/test-assertions.R +++ b/tests/testthat/test-assertions.R @@ -17,6 +17,14 @@ test_that("assert_mac throws error on non-macOS", { expect_error(assert_mac(), regexp = "This function requires macOS") }) +test_that("assert_mac surfaces the guidance advice in the error message", { + mockery::stub(assert_mac, "is_macos", function() FALSE) + mockery::stub(assert_mac, "system_os", function() "linux") + # The advice line is folded into the message as an info bullet and must + # render (it was previously passed as a swallowed `advice=` argument). + expect_error(assert_mac(), regexp = "Intel or Apple Silicon processors") +}) + test_that("assert_macos_supported succeeds on supported macOS version", { # Create a stub that correctly handles the call parameter mockery::stub(assert_macos_supported, "assert_mac", function(...) NULL) From 8c82a142e2028a309685472fb156210a354436b7 Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Tue, 23 Jun 2026 10:26:33 -0500 Subject: [PATCH 12/18] Render multi-line cli alerts correctly cli_alert_info/cli_alert_warning only render their first element, so passing a multi-element vector silently dropped the rest. Split each into a single-line headline alert followed by cli_bullets() for the detail lines (the pattern already used elsewhere in the package): - force_password() (utils.R): 3 dropped explanation lines now shown. - shell_sudo_command() failure branch (shell.R): status/cause lines now shown. - dmg_package_install() unmount-failure warning (installers.R): the 'unmount manually' hint now shown (this third site was not in the original survey). --- R/installers.R | 4 ++-- R/shell.R | 4 ++-- R/utils.R | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/R/installers.R b/R/installers.R index 6d05ecc..b8014fc 100644 --- a/R/installers.R +++ b/R/installers.R @@ -354,8 +354,8 @@ dmg_package_install <- function(path_to_dmg, status <- shell_execute(detach_cmd, sudo = FALSE) if (status != 0) { - cli::cli_alert_warning(c( - "{.pkg macrtools}: Failed to unmount disk image.", + cli::cli_alert_warning("{.pkg macrtools}: Failed to unmount disk image.") + cli::cli_bullets(c( "i" = "You may need to unmount it manually." )) cli::cli_text("") # Add spacing diff --git a/R/shell.R b/R/shell.R index 534972e..8072938 100644 --- a/R/shell.R +++ b/R/shell.R @@ -45,8 +45,8 @@ shell_sudo_command <- function(cmd, password, verbose = TRUE, prefix = "sudo -kS } if (verbose && result != 0) { - cli::cli_alert_warning(c( - "{.pkg macrtools}: Command execution failed.", + cli::cli_alert_warning("{.pkg macrtools}: Command execution failed.") + cli::cli_bullets(c( "Status code: {.val {result}}", "This might indicate permission issues or syntax errors." )) diff --git a/R/utils.R b/R/utils.R index c25c901..031972c 100644 --- a/R/utils.R +++ b/R/utils.R @@ -30,8 +30,8 @@ force_password <- function(supplied_password) { if(base::is.null(entered_password)) { current_user <- base::Sys.info()['user'] - cli::cli_alert_info(c( - "{.pkg macrtools}: Administrative privileges required.", + cli::cli_alert_info("{.pkg macrtools}: Administrative privileges required.") + cli::cli_bullets(c( "Your user account password is needed to execute privileged operations.", "This password will not be stored and is only used for the current session.", "Current user: {.val {current_user}}" From b9345b926756bd7a763e1442d6f9004b60d6441c Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Tue, 23 Jun 2026 10:27:54 -0500 Subject: [PATCH 13/18] Interpolate path in use_r_environ creation message use_r_environ() is parameterized on path but hardcoded '~/.Renviron' in its 'Creating a new Renviron file' message, so a non-default path made the two messages disagree. Interpolate path instead. The base::message calls are kept (rather than migrated to cli) to stay consistent with the vendored block_append helper this function calls. --- R/renviron.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/renviron.R b/R/renviron.R index 86cb508..e54e111 100644 --- a/R/renviron.R +++ b/R/renviron.R @@ -4,7 +4,7 @@ use_r_environ = function(option, value, path = "~/.Renviron", if (!base::file.exists(path)) { base::message("`", path ,"` file not found at location.") - base::message("Creating a new Renviron file at: ~/.Renviron") + base::message("Creating a new Renviron file at: ", path) base::file.create(path) } From 65096fac5af6062a9e6f597ffe19892ec4b6cd7e Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Tue, 23 Jun 2026 10:38:47 -0500 Subject: [PATCH 14/18] Retarget test mocks orphaned by helper extraction Two stubs targeted calls that moved into extracted helpers, making the stubs dead no-ops so the tests silently shelled out to the real system: - test-gfortran.R: stub exec_text (not base::tryCatch) after the gfortran --version lookup moved into exec_text(). - test-toolchain.R: stub timestamp_now (not base::format) after the timestamp moved into timestamp_now(). Surfaced by an adversarial review of the refactoring diff. --- tests/testthat/test-gfortran.R | 2 +- tests/testthat/test-toolchain.R | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/testthat/test-gfortran.R b/tests/testthat/test-gfortran.R index c4195d1..9ce1997 100644 --- a/tests/testthat/test-gfortran.R +++ b/tests/testthat/test-gfortran.R @@ -38,7 +38,7 @@ test_that("gfortran_install skips when already installed", { mockery::stub(gfortran_install, "cli::cli_alert_info", function(...) NULL) mockery::stub(gfortran_install, "cli::cli_bullets", function(...) NULL) mockery::stub(gfortran_install, "cli::cli_text", function(...) NULL) - mockery::stub(gfortran_install, "base::tryCatch", function(...) "Mock version") + mockery::stub(gfortran_install, "exec_text", function(...) "Mock version") mockery::stub(gfortran_install, "base::file.path", function(...) "/opt/gfortran") result <- gfortran_install(verbose = TRUE) diff --git a/tests/testthat/test-toolchain.R b/tests/testthat/test-toolchain.R index 9abef9e..03ec8bf 100644 --- a/tests/testthat/test-toolchain.R +++ b/tests/testthat/test-toolchain.R @@ -73,7 +73,7 @@ test_that("rtools_install_summary returns TRUE on success and aborts on failure" mockery::stub(rtools_install_summary, "cli::cli_text", function(...) NULL) mockery::stub(rtools_install_summary, "cli::cli_alert_info", function(...) NULL) mockery::stub(rtools_install_summary, "cli::cli_alert_success", function(...) NULL) - mockery::stub(rtools_install_summary, "base::format", function(...) "now") + mockery::stub(rtools_install_summary, "timestamp_now", function(...) "now") expect_true(rtools_install_summary(TRUE, TRUE, TRUE)) expect_error(rtools_install_summary(TRUE, FALSE, TRUE), "Installation failed") From f4cc9ae740f9470b5171c154e8b78eed10ecf3a9 Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Tue, 23 Jun 2026 10:38:47 -0500 Subject: [PATCH 15/18] Drop unused rlang dependency Removing the circular rlang::check_installed("rlang") from .onLoad left rlang declared in Imports but never used directly (R CMD check NOTE: 'All declared Imports should be used'). rlang remains available transitively via cli. --- DESCRIPTION | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 076cec5..fefca52 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -25,8 +25,7 @@ Imports: sys, tools, utils, - cli, - rlang + cli URL: https://mac.thecoatlessprofessor.com/macrtools/ Suggests: rstudioapi, From e8638b6cb841ab597d318039f67711a1852664d3 Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Tue, 23 Jun 2026 13:46:06 -0500 Subject: [PATCH 16/18] Test real is_r_version and modernize system mocks - Replace the simplified_is_r_version reimplementation (which tested a copy of the logic, not the function) with a real test of is_r_version() that mocks the version helpers via local_mocked_bindings(). - Migrate the is_macos/is_aarch64/is_x86_64 internal-function stubs from mockery::stub() to local_mocked_bindings() so they survive helper extraction, and add the complementary FALSE assertions. base::Sys.info / sys:: stubs are left as mockery (local_mocked_bindings cannot mock base/other-package calls). --- tests/testthat/test-system.R | 56 ++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 32 deletions(-) diff --git a/tests/testthat/test-system.R b/tests/testthat/test-system.R index 32043b8..299f53b 100644 --- a/tests/testthat/test-system.R +++ b/tests/testthat/test-system.R @@ -7,23 +7,25 @@ test_that("system_os returns the correct OS name", { }) test_that("system_arch returns the correct architecture", { - # Instead of trying to mock R.version directly, we test the function itself - expected_arch <- base::R.version$arch - expect_equal(system_arch(), expected_arch) + # We can't mock R.version, so test the function against the real value. + expect_equal(system_arch(), base::R.version$arch) +}) - # For additional coverage, we test that the functions that use system_arch work - mockery::stub(is_aarch64, "system_arch", function() "aarch64") +test_that("is_aarch64 and is_x86_64 reflect the system architecture", { + local_mocked_bindings(system_arch = function() "aarch64") expect_true(is_aarch64()) + expect_false(is_x86_64()) - mockery::stub(is_x86_64, "system_arch", function() "x86_64") + local_mocked_bindings(system_arch = function() "x86_64") + expect_false(is_aarch64()) expect_true(is_x86_64()) }) test_that("is_macos correctly identifies macOS", { - mockery::stub(is_macos, "system_os", function() "darwin") + local_mocked_bindings(system_os = function() "darwin") expect_true(is_macos()) - mockery::stub(is_macos, "system_os", function() "linux") + local_mocked_bindings(system_os = function() "linux") expect_false(is_macos()) }) @@ -62,30 +64,20 @@ test_that("version_between correctly determines if version is within bounds", { }) test_that("is_r_version correctly identifies R versions", { - # We can't easily mock R.version, so instead we test a simplified version - # of the function that uses mock data - simplified_is_r_version <- function(target_version, r_major = "4", r_minor = "2.1") { - minor_value <- strsplit(r_minor, ".", fixed = TRUE)[[1]][1] - version_string <- paste(r_major, minor_value, sep = ".") - return(version_string == target_version) - } - - expect_true(simplified_is_r_version("4.2")) - expect_false(simplified_is_r_version("4.1")) - - # Test with compare_major_minor = FALSE (directly using full version) - simplified_is_r_version_full <- function(target_version, r_major = "4", r_minor = "2.1", compare_major_minor = FALSE) { - if (compare_major_minor) { - minor_value <- strsplit(r_minor, ".", fixed = TRUE)[[1]][1] - } else { - minor_value <- r_minor - } - version_string <- paste(r_major, minor_value, sep = ".") - return(version_string == target_version) - } - - expect_true(simplified_is_r_version_full("4.2.1", compare_major_minor = FALSE)) - expect_false(simplified_is_r_version_full("4.2.0", compare_major_minor = FALSE)) + # Mock the version helpers so we exercise the real is_r_version() against a + # controlled R version, rather than testing a reimplemented copy. + local_mocked_bindings( + r_version_major_minor = function() "4.2", + r_version_full = function() "4.2.1" + ) + + # Default compares major.minor + expect_true(is_r_version("4.2")) + expect_false(is_r_version("4.1")) + + # compare_major_minor = FALSE compares the full major.minor.patch + expect_true(is_r_version("4.2.1", compare_major_minor = FALSE)) + expect_false(is_r_version("4.2.0", compare_major_minor = FALSE)) }) test_that("is_r_version_at_least compares major.minor versions", { From 60be9d44096796e8564f50871f48689e5deba301 Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Tue, 23 Jun 2026 13:47:13 -0500 Subject: [PATCH 17/18] Modernize assertion tests with local_mocked_bindings All stubs here target internal predicates, so migrate them from mockery::stub() to local_mocked_bindings() (robust to helper extraction). Drop the two cli::cli_abort interception stubs and the base::paste hack: the real cli_abort already throws a matchable message, and mocking r_version_full() directly is clearer than rebinding base::paste to fake the version. --- tests/testthat/test-assertions.R | 53 +++++++++++++------------------- 1 file changed, 21 insertions(+), 32 deletions(-) diff --git a/tests/testthat/test-assertions.R b/tests/testthat/test-assertions.R index 60e0448..d824286 100644 --- a/tests/testthat/test-assertions.R +++ b/tests/testthat/test-assertions.R @@ -7,79 +7,68 @@ test_that("assert throws error when condition is FALSE", { }) test_that("assert_mac succeeds on macOS", { - mockery::stub(assert_mac, "is_macos", function() TRUE) + local_mocked_bindings(is_macos = function() TRUE) expect_no_error(assert_mac()) }) test_that("assert_mac throws error on non-macOS", { - mockery::stub(assert_mac, "is_macos", function() FALSE) - mockery::stub(assert_mac, "system_os", function() "linux") + local_mocked_bindings(is_macos = function() FALSE, system_os = function() "linux") expect_error(assert_mac(), regexp = "This function requires macOS") }) test_that("assert_mac surfaces the guidance advice in the error message", { - mockery::stub(assert_mac, "is_macos", function() FALSE) - mockery::stub(assert_mac, "system_os", function() "linux") + local_mocked_bindings(is_macos = function() FALSE, system_os = function() "linux") # The advice line is folded into the message as an info bullet and must # render (it was previously passed as a swallowed `advice=` argument). expect_error(assert_mac(), regexp = "Intel or Apple Silicon processors") }) test_that("assert_macos_supported succeeds on supported macOS version", { - # Create a stub that correctly handles the call parameter - mockery::stub(assert_macos_supported, "assert_mac", function(...) NULL) - mockery::stub(assert_macos_supported, "is_macos_r_supported", function() TRUE) + local_mocked_bindings( + assert_mac = function(...) NULL, + is_macos_r_supported = function() TRUE + ) expect_no_error(assert_macos_supported()) }) test_that("assert_macos_supported throws error on unsupported macOS version", { - # Create a stub that correctly handles the call parameter - mockery::stub(assert_macos_supported, "assert_mac", function(...) NULL) - mockery::stub(assert_macos_supported, "is_macos_r_supported", function() FALSE) - mockery::stub(assert_macos_supported, "shell_mac_version", function() "10.12") - # Mock cli::cli_abort to track error message but not actually throw - mockery::stub(assert_macos_supported, "cli::cli_abort", - function(message, ...) stop(paste(message[1], collapse=" "))) - + local_mocked_bindings( + assert_mac = function(...) NULL, + is_macos_r_supported = function() FALSE, + shell_mac_version = function() "10.12" + ) expect_error(assert_macos_supported(), regexp = "not supported") }) test_that("assert_aarch64 succeeds on Apple Silicon", { - mockery::stub(assert_aarch64, "is_aarch64", function() TRUE) + local_mocked_bindings(is_aarch64 = function() TRUE) expect_no_error(assert_aarch64()) }) test_that("assert_aarch64 throws error on non-Apple Silicon", { - mockery::stub(assert_aarch64, "is_aarch64", function() FALSE) - mockery::stub(assert_aarch64, "system_arch", function() "x86_64") + local_mocked_bindings(is_aarch64 = function() FALSE, system_arch = function() "x86_64") expect_error(assert_aarch64(), regexp = "requires an Apple Silicon") }) test_that("assert_x86_64 succeeds on Intel", { - mockery::stub(assert_x86_64, "is_x86_64", function() TRUE) + local_mocked_bindings(is_x86_64 = function() TRUE) expect_no_error(assert_x86_64()) }) test_that("assert_x86_64 throws error on non-Intel", { - mockery::stub(assert_x86_64, "is_x86_64", function() FALSE) - mockery::stub(assert_x86_64, "system_arch", function() "aarch64") + local_mocked_bindings(is_x86_64 = function() FALSE, system_arch = function() "aarch64") expect_error(assert_x86_64(), regexp = "requires an Intel-based Mac") }) test_that("assert_r_version_supported succeeds on supported R version", { - # Mock the supported-window check to report a supported version - mockery::stub(assert_r_version_supported, "is_r_version_supported", function(...) TRUE) + local_mocked_bindings(is_r_version_supported = function(...) TRUE) expect_no_error(assert_r_version_supported()) }) test_that("assert_r_version_supported throws error on unsupported R version", { - # Report an unsupported version - mockery::stub(assert_r_version_supported, "is_r_version_supported", function(...) FALSE) - # Instead of trying to mock R.version, mock paste to return a known version - mockery::stub(assert_r_version_supported, "base::paste", function(...) "3.6.0") - # Mock cli::cli_abort to track error message but not actually throw - mockery::stub(assert_r_version_supported, "cli::cli_abort", - function(message, ...) stop(paste(message[1], collapse=" "))) - + local_mocked_bindings( + is_r_version_supported = function(...) FALSE, + r_version_full = function() "3.6.0" + ) expect_error(assert_r_version_supported(), regexp = "not supported") }) From 65c21888721269bf8929eacd0c4a12df471c2c9c Mon Sep 17 00:00:00 2001 From: "james.balamuta@gmail.com" Date: Tue, 23 Jun 2026 13:55:16 -0500 Subject: [PATCH 18/18] Migrate internal-function test mocks to local_mocked_bindings Convert the ~143 mockery::stub() calls that target macrtools' own internal functions (predicate gates like is_gfortran_installed/is_xcode_cli_installed, and extracted helpers like force_password/exec_text/shell_execute) to testthat 3's local_mocked_bindings() across the six remaining test files. Namespace-level mocking survives helper extraction, which is the exact failure mode that orphaned several stubs during the refactor. base::/cli::/sys::/askpass:: stubs are intentionally left as mockery::stub() (local_mocked_bindings cannot mock base or other-package functions), so some tests now use both mechanisms. No assertions, inputs, or test descriptions were changed; full suite and R CMD check (with tests) stay green. --- tests/testthat/test-gfortran.R | 158 ++++++++++++++----------- tests/testthat/test-installers.R | 52 +++++---- tests/testthat/test-openmp.R | 15 ++- tests/testthat/test-shell.R | 24 ++-- tests/testthat/test-toolchain.R | 113 ++++++++++-------- tests/testthat/test-xcode.R | 190 +++++++++++++++++-------------- 6 files changed, 310 insertions(+), 242 deletions(-) diff --git a/tests/testthat/test-gfortran.R b/tests/testthat/test-gfortran.R index 9ce1997..50a02df 100644 --- a/tests/testthat/test-gfortran.R +++ b/tests/testthat/test-gfortran.R @@ -1,7 +1,9 @@ test_that("is_gfortran_installed correctly identifies installed gfortran", { # Mock dependencies for success scenario - mockery::stub(is_gfortran_installed, "assert_mac", function() NULL) - mockery::stub(is_gfortran_installed, "gfortran_install_location", function() "/opt") + local_mocked_bindings( + assert_mac = function() NULL, + gfortran_install_location = function() "/opt" + ) mockery::stub(is_gfortran_installed, "base::file.path", function(...) "/opt/gfortran") mockery::stub(is_gfortran_installed, "base::dir.exists", function(path) TRUE) @@ -14,15 +16,17 @@ test_that("is_gfortran_installed correctly identifies installed gfortran", { test_that("gfortran_version returns correct output", { mock_output <- "GNU Fortran (GCC) 9.3.0" - mockery::stub(gfortran_version, "gfortran", function(...) { - base::structure( - base::list( - output = mock_output, - status = 0L - ), - class = c("gfortran", "cli") - ) - }) + local_mocked_bindings( + gfortran = function(...) { + base::structure( + base::list( + output = mock_output, + status = 0L + ), + class = c("gfortran", "cli") + ) + } + ) result <- gfortran_version() expect_equal(result$output, mock_output) @@ -31,14 +35,16 @@ test_that("gfortran_version returns correct output", { test_that("gfortran_install skips when already installed", { # Mock dependencies - mockery::stub(gfortran_install, "assert_mac", function() NULL) - mockery::stub(gfortran_install, "assert_macos_supported", function() NULL) - mockery::stub(gfortran_install, "assert_r_version_supported", function() NULL) - mockery::stub(gfortran_install, "is_gfortran_installed", function() TRUE) + local_mocked_bindings( + assert_mac = function() NULL, + assert_macos_supported = function() NULL, + assert_r_version_supported = function() NULL, + is_gfortran_installed = function() TRUE, + exec_text = function(...) "Mock version" + ) mockery::stub(gfortran_install, "cli::cli_alert_info", function(...) NULL) mockery::stub(gfortran_install, "cli::cli_bullets", function(...) NULL) mockery::stub(gfortran_install, "cli::cli_text", function(...) NULL) - mockery::stub(gfortran_install, "exec_text", function(...) "Mock version") mockery::stub(gfortran_install, "base::file.path", function(...) "/opt/gfortran") result <- gfortran_install(verbose = TRUE) @@ -47,19 +53,21 @@ test_that("gfortran_install skips when already installed", { test_that("gfortran_install installs correct version for R 4.3+", { # Mock dependencies - mockery::stub(gfortran_install, "assert_mac", function() NULL) - mockery::stub(gfortran_install, "assert_macos_supported", function() NULL) - mockery::stub(gfortran_install, "assert_r_version_supported", function() NULL) - mockery::stub(gfortran_install, "is_gfortran_installed", function() FALSE) - mockery::stub(gfortran_install, "is_r_version_at_least", - function(target, ...) utils::compareVersion("4.3", target) >= 0) - mockery::stub(gfortran_install, "gfortran_install_location", function() "/opt") + local_mocked_bindings( + assert_mac = function() NULL, + assert_macos_supported = function() NULL, + assert_r_version_supported = function() NULL, + is_gfortran_installed = function() FALSE, + is_r_version_at_least = + function(target, ...) utils::compareVersion("4.3", target) >= 0, + gfortran_install_location = function() "/opt", + force_password = function(pw) "mockpw", + create_install_location = function(...) TRUE, + install_gfortran_12_2_universal = function(...) TRUE, + renviron_gfortran_path = function(...) NULL + ) mockery::stub(gfortran_install, "base::file.path", function(...) "/opt/gfortran/bin") mockery::stub(gfortran_install, "base::paste0", function(...) "$PATH:/opt/gfortran/bin") - mockery::stub(gfortran_install, "force_password", function(pw) "mockpw") - mockery::stub(gfortran_install, "create_install_location", function(...) TRUE) - mockery::stub(gfortran_install, "install_gfortran_12_2_universal", function(...) TRUE) - mockery::stub(gfortran_install, "renviron_gfortran_path", function(...) NULL) mockery::stub(gfortran_install, "cli::cli_alert_info", function(...) NULL) mockery::stub(gfortran_install, "cli::cli_bullets", function(...) NULL) mockery::stub(gfortran_install, "cli::cli_text", function(...) NULL) @@ -71,22 +79,24 @@ test_that("gfortran_install installs correct version for R 4.3+", { test_that("gfortran_install uses the 14.2 universal installer for R 4.6", { # Mock dependencies - mockery::stub(gfortran_install, "assert_mac", function() NULL) - mockery::stub(gfortran_install, "assert_macos_supported", function() NULL) - mockery::stub(gfortran_install, "assert_r_version_supported", function() NULL) - mockery::stub(gfortran_install, "is_gfortran_installed", function() FALSE) - mockery::stub(gfortran_install, "is_r_version_at_least", - function(target, ...) utils::compareVersion("4.6", target) >= 0) - mockery::stub(gfortran_install, "gfortran_install_location", function() "/opt") + # The 14.2 universal installer must be chosen for R 4.6; fail loudly otherwise. + local_mocked_bindings( + assert_mac = function() NULL, + assert_macos_supported = function() NULL, + assert_r_version_supported = function() NULL, + is_gfortran_installed = function() FALSE, + is_r_version_at_least = + function(target, ...) utils::compareVersion("4.6", target) >= 0, + gfortran_install_location = function() "/opt", + force_password = function(pw) "mockpw", + create_install_location = function(...) TRUE, + install_gfortran_14_2_universal = function(...) TRUE, + install_gfortran_12_2_universal = + function(...) stop("wrong installer: 12.2 used for R 4.6"), + renviron_gfortran_path = function(...) NULL + ) mockery::stub(gfortran_install, "base::file.path", function(...) "/opt/gfortran/bin") mockery::stub(gfortran_install, "base::paste0", function(...) "$PATH:/opt/gfortran/bin") - mockery::stub(gfortran_install, "force_password", function(pw) "mockpw") - mockery::stub(gfortran_install, "create_install_location", function(...) TRUE) - # The 14.2 universal installer must be chosen for R 4.6; fail loudly otherwise. - mockery::stub(gfortran_install, "install_gfortran_14_2_universal", function(...) TRUE) - mockery::stub(gfortran_install, "install_gfortran_12_2_universal", - function(...) stop("wrong installer: 12.2 used for R 4.6")) - mockery::stub(gfortran_install, "renviron_gfortran_path", function(...) NULL) mockery::stub(gfortran_install, "cli::cli_alert_info", function(...) NULL) mockery::stub(gfortran_install, "cli::cli_bullets", function(...) NULL) mockery::stub(gfortran_install, "cli::cli_text", function(...) NULL) @@ -98,19 +108,21 @@ test_that("gfortran_install uses the 14.2 universal installer for R 4.6", { test_that("gfortran_install delegates to the legacy installer for R 4.2", { # Mock dependencies - mockery::stub(gfortran_install, "assert_mac", function() NULL) - mockery::stub(gfortran_install, "assert_macos_supported", function() NULL) - mockery::stub(gfortran_install, "assert_r_version_supported", function() NULL) - mockery::stub(gfortran_install, "is_gfortran_installed", function() FALSE) - mockery::stub(gfortran_install, "is_r_version_at_least", - function(target, ...) utils::compareVersion("4.2", target) >= 0) - mockery::stub(gfortran_install, "gfortran_install_location", function() "/usr/local") + local_mocked_bindings( + assert_mac = function() NULL, + assert_macos_supported = function() NULL, + assert_r_version_supported = function() NULL, + is_gfortran_installed = function() FALSE, + is_r_version_at_least = + function(target, ...) utils::compareVersion("4.2", target) >= 0, + gfortran_install_location = function() "/usr/local", + force_password = function(pw) "mockpw", + create_install_location = function(...) TRUE, + install_gfortran_legacy = function(...) TRUE, + renviron_gfortran_path = function(...) NULL + ) mockery::stub(gfortran_install, "base::file.path", function(...) "/usr/local/gfortran/bin") mockery::stub(gfortran_install, "base::paste0", function(...) "$PATH:/usr/local/gfortran/bin") - mockery::stub(gfortran_install, "force_password", function(pw) "mockpw") - mockery::stub(gfortran_install, "create_install_location", function(...) TRUE) - mockery::stub(gfortran_install, "install_gfortran_legacy", function(...) TRUE) - mockery::stub(gfortran_install, "renviron_gfortran_path", function(...) NULL) mockery::stub(gfortran_install, "cli::cli_alert_info", function(...) NULL) mockery::stub(gfortran_install, "cli::cli_bullets", function(...) NULL) mockery::stub(gfortran_install, "cli::cli_text", function(...) NULL) @@ -122,32 +134,40 @@ test_that("gfortran_install delegates to the legacy installer for R 4.2", { test_that("install_gfortran_legacy dispatches by architecture and R version", { # Intel -> gfortran 8.2 Mojave DMG installer - mockery::stub(install_gfortran_legacy, "is_x86_64", function() TRUE) - mockery::stub(install_gfortran_legacy, "install_gfortran_82_mojave", function(...) TRUE) + local_mocked_bindings( + is_x86_64 = function() TRUE, + install_gfortran_82_mojave = function(...) TRUE + ) expect_true(install_gfortran_legacy("pw", verbose = FALSE)) # Apple Silicon + R 4.2 -> gfortran 12 arm tarball - mockery::stub(install_gfortran_legacy, "is_x86_64", function() FALSE) - mockery::stub(install_gfortran_legacy, "is_aarch64", function() TRUE) - mockery::stub(install_gfortran_legacy, "is_r_version", function(v) v == "4.2") - mockery::stub(install_gfortran_legacy, "install_gfortran_12_arm", function(...) TRUE) + local_mocked_bindings( + is_x86_64 = function() FALSE, + is_aarch64 = function() TRUE, + is_r_version = function(v) v == "4.2", + install_gfortran_12_arm = function(...) TRUE + ) expect_true(install_gfortran_legacy("pw", verbose = FALSE)) # Apple Silicon + R 4.0 -> abort (Apple Silicon began in R 4.1) - mockery::stub(install_gfortran_legacy, "is_r_version", function(v) FALSE) + local_mocked_bindings(is_r_version = function(v) FALSE) expect_error(install_gfortran_legacy("pw", verbose = FALSE), "Apple Silicon") # Neither Intel nor Apple Silicon -> unsupported architecture abort - mockery::stub(install_gfortran_legacy, "is_x86_64", function() FALSE) - mockery::stub(install_gfortran_legacy, "is_aarch64", function() FALSE) - mockery::stub(install_gfortran_legacy, "system_arch", function() "ppc") + local_mocked_bindings( + is_x86_64 = function() FALSE, + is_aarch64 = function() FALSE, + system_arch = function() "ppc" + ) expect_error(install_gfortran_legacy("pw", verbose = FALSE), "Unsupported macOS architecture") }) test_that("gfortran_uninstall succeeds when not installed", { # Mock dependencies - mockery::stub(gfortran_uninstall, "assert_mac", function() NULL) - mockery::stub(gfortran_uninstall, "is_gfortran_installed", function() FALSE) + local_mocked_bindings( + assert_mac = function() NULL, + is_gfortran_installed = function() FALSE + ) mockery::stub(gfortran_uninstall, "cli::cli_alert_info", function(...) NULL) mockery::stub(gfortran_uninstall, "cli::cli_text", function(...) NULL) @@ -157,9 +177,12 @@ test_that("gfortran_uninstall succeeds when not installed", { test_that("gfortran_uninstall removes installed gfortran", { # Mock dependencies with a proper file.path mock - mockery::stub(gfortran_uninstall, "assert_mac", function() NULL) - mockery::stub(gfortran_uninstall, "is_gfortran_installed", function() TRUE) - mockery::stub(gfortran_uninstall, "gfortran_install_location", function() "/opt") + local_mocked_bindings( + assert_mac = function() NULL, + is_gfortran_installed = function() TRUE, + gfortran_install_location = function() "/opt", + shell_execute = function(...) 0L + ) # Create a more flexible file.path mock file_path_mock <- function(...) { @@ -174,7 +197,6 @@ test_that("gfortran_uninstall removes installed gfortran", { mockery::stub(gfortran_uninstall, "base::file.path", file_path_mock) mockery::stub(gfortran_uninstall, "base::paste0", function(...) "rm -rf /opt/gfortran /opt/bin/gfortran") - mockery::stub(gfortran_uninstall, "shell_execute", function(...) 0L) mockery::stub(gfortran_uninstall, "cli::cli_alert_info", function(...) NULL) mockery::stub(gfortran_uninstall, "cli::cli_bullets", function(...) NULL) mockery::stub(gfortran_uninstall, "cli::cli_text", function(...) NULL) diff --git a/tests/testthat/test-installers.R b/tests/testthat/test-installers.R index ef023c7..d4dd93b 100644 --- a/tests/testthat/test-installers.R +++ b/tests/testthat/test-installers.R @@ -50,7 +50,7 @@ test_that("tar_package_install handles successful installation", { mockery::stub(tar_package_install, "cli::cli_alert_info", function(...) NULL) mockery::stub(tar_package_install, "cli::cli_bullets", function(...) NULL) mockery::stub(tar_package_install, "cli::cli_text", function(...) NULL) - mockery::stub(tar_package_install, "shell_execute", function(...) 0) + local_mocked_bindings(shell_execute = function(...) 0) mockery::stub(tar_package_install, "base::unlink", function(...) NULL) mockery::stub(tar_package_install, "cli::cli_alert_success", function(...) NULL) @@ -66,7 +66,7 @@ test_that("tar_package_install handles installation errors", { mockery::stub(tar_package_install, "cli::cli_alert_info", function(...) NULL) mockery::stub(tar_package_install, "cli::cli_bullets", function(...) NULL) mockery::stub(tar_package_install, "cli::cli_text", function(...) NULL) - mockery::stub(tar_package_install, "shell_execute", function(...) -1) + local_mocked_bindings(shell_execute = function(...) -1) mockery::stub(tar_package_install, "cli::cli_abort", function(...) stop("Installation failed")) expect_error(tar_package_install("/tmp/test.tar.gz", "/opt", 2, verbose = TRUE), @@ -75,7 +75,7 @@ test_that("tar_package_install handles installation errors", { test_that("create_install_location succeeds when directory exists", { # Mock dependencies - mockery::stub(create_install_location, "install_location", function(...) "/opt/test") + local_mocked_bindings(install_location = function(...) "/opt/test") mockery::stub(create_install_location, "base::dir.exists", function(...) TRUE) result <- create_install_location() @@ -90,7 +90,7 @@ test_that("pkg_install handles successful installation", { mockery::stub(pkg_install, "cli::cli_bullets", function(...) NULL) mockery::stub(pkg_install, "cli::cli_text", function(...) NULL) mockery::stub(pkg_install, "base::paste", function(...) "sudo -kS installer -pkg test.pkg -target /") - mockery::stub(pkg_install, "shell_execute", function(...) 0) + local_mocked_bindings(shell_execute = function(...) 0) mockery::stub(pkg_install, "cli::cli_alert_success", function(...) NULL) mockery::stub(pkg_install, "cli::cli_abort", function(...) NULL) @@ -100,53 +100,65 @@ test_that("pkg_install handles successful installation", { test_that("recipe_binary_install_strip_level follows the toolchain tiers", { # Modern tier (R >= 4.3): strip 3 for every architecture - mockery::stub(recipe_binary_install_strip_level, "assert_supported_r_version_for", function(...) invisible(NULL)) - mockery::stub(recipe_binary_install_strip_level, "is_r_version_at_least", function(...) TRUE) + local_mocked_bindings( + assert_supported_r_version_for = function(...) invisible(NULL), + is_r_version_at_least = function(...) TRUE + ) expect_equal(recipe_binary_install_strip_level("arm64"), 3) expect_equal(recipe_binary_install_strip_level("aarch64"), 3) expect_equal(recipe_binary_install_strip_level("x86_64"), 3) # Legacy tier (4.0 <= R < 4.3): delegates to install_strip_level() - mockery::stub(recipe_binary_install_strip_level, "is_r_version_at_least", function(...) FALSE) - mockery::stub(recipe_binary_install_strip_level, "install_strip_level", function(...) 99) + local_mocked_bindings( + is_r_version_at_least = function(...) FALSE, + install_strip_level = function(...) 99 + ) expect_equal(recipe_binary_install_strip_level("arm64"), 99) # Unsupported R version: abort - mockery::stub(recipe_binary_install_strip_level, "assert_supported_r_version_for", function(...) stop("Unsupported R version")) + local_mocked_bindings(assert_supported_r_version_for = function(...) stop("Unsupported R version")) expect_error(recipe_binary_install_strip_level("arm64"), regexp = "Unsupported R version") }) test_that("recipe_binary_install_location follows the toolchain tiers", { # Modern tier (R >= 4.3): /opt/R/ - mockery::stub(recipe_binary_install_location, "assert_supported_r_version_for", function(...) invisible(NULL)) - mockery::stub(recipe_binary_install_location, "is_r_version_at_least", function(...) TRUE) + local_mocked_bindings( + assert_supported_r_version_for = function(...) invisible(NULL), + is_r_version_at_least = function(...) TRUE + ) expect_equal(recipe_binary_install_location("arm64"), "/opt/R/arm64") expect_equal(recipe_binary_install_location("aarch64"), "/opt/R/arm64") expect_equal(recipe_binary_install_location("x86_64"), "/opt/R/x86_64") # Legacy tier (4.0 <= R < 4.3): delegates to install_location() - mockery::stub(recipe_binary_install_location, "is_r_version_at_least", function(...) FALSE) - mockery::stub(recipe_binary_install_location, "install_location", function(...) "/legacy/loc") + local_mocked_bindings( + is_r_version_at_least = function(...) FALSE, + install_location = function(...) "/legacy/loc" + ) expect_equal(recipe_binary_install_location("x86_64"), "/legacy/loc") # Unsupported R version: abort - mockery::stub(recipe_binary_install_location, "assert_supported_r_version_for", function(...) stop("Unsupported R version")) + local_mocked_bindings(assert_supported_r_version_for = function(...) stop("Unsupported R version")) expect_error(recipe_binary_install_location("arm64"), regexp = "Unsupported R version") }) test_that("gfortran_install_location follows the toolchain tiers", { # Modern tier (R >= 4.3): /opt - mockery::stub(gfortran_install_location, "assert_supported_r_version_for", function(...) invisible(NULL)) - mockery::stub(gfortran_install_location, "is_r_version_at_least", function(...) TRUE) + local_mocked_bindings( + assert_supported_r_version_for = function(...) invisible(NULL), + is_r_version_at_least = function(...) TRUE + ) expect_equal(gfortran_install_location("arm64"), "/opt") # Legacy tier (4.0 <= R < 4.3): delegates to install_location() - mockery::stub(gfortran_install_location, "is_r_version_at_least", function(...) FALSE) - mockery::stub(gfortran_install_location, "install_location", function(...) "/legacy/loc") + local_mocked_bindings( + is_r_version_at_least = function(...) FALSE, + install_location = function(...) "/legacy/loc" + ) expect_equal(gfortran_install_location("x86_64"), "/legacy/loc") # Unsupported R version: abort - mockery::stub(gfortran_install_location, "assert_supported_r_version_for", function(...) stop("Unsupported R version")) + local_mocked_bindings(assert_supported_r_version_for = function(...) stop("Unsupported R version")) expect_error(gfortran_install_location("arm64"), regexp = "Unsupported R version") }) @@ -159,7 +171,7 @@ test_that("tar_package_install aborts on a positive (non-zero) exit status", { mockery::stub(tar_package_install, "cli::cli_alert_info", function(...) NULL) mockery::stub(tar_package_install, "cli::cli_bullets", function(...) NULL) mockery::stub(tar_package_install, "cli::cli_text", function(...) NULL) - mockery::stub(tar_package_install, "shell_execute", function(...) 1) + local_mocked_bindings(shell_execute = function(...) 1) mockery::stub(tar_package_install, "cli::cli_abort", function(...) stop("Installation failed")) expect_error(tar_package_install("/tmp/test.tar.gz", "/opt", 2, verbose = TRUE), diff --git a/tests/testthat/test-openmp.R b/tests/testthat/test-openmp.R index 7abce36..5d5ce96 100644 --- a/tests/testthat/test-openmp.R +++ b/tests/testthat/test-openmp.R @@ -1,5 +1,5 @@ test_that("is_openmp_installed checks for the library and header", { - mockery::stub(is_openmp_installed, "assert_mac", function() NULL) + local_mocked_bindings(assert_mac = function(...) NULL) mockery::stub(is_openmp_installed, "base::file.exists", function(p) TRUE) expect_true(is_openmp_installed()) @@ -10,8 +10,9 @@ test_that("is_openmp_installed checks for the library and header", { test_that("get_openmp_url_for_xcode maps Apple clang build numbers to runtimes", { pick <- function(build) { - mockery::stub(get_openmp_url_for_xcode, "get_apple_clang_version", - function() list(build_number = build)) + local_mocked_bindings( + get_apple_clang_version = function() list(build_number = build) + ) get_openmp_url_for_xcode() } @@ -30,10 +31,12 @@ test_that("get_openmp_url_for_xcode maps Apple clang build numbers to runtimes", test_that("openmp_uninstall honors configure_makevars without erroring", { # Regression: openmp_uninstall() previously referenced an undefined # `remove_makevars_config`, erroring after removing the libraries. - mockery::stub(openmp_uninstall, "assert_mac", function() NULL) - mockery::stub(openmp_uninstall, "is_openmp_installed", function() TRUE) + local_mocked_bindings( + assert_mac = function(...) NULL, + is_openmp_installed = function() TRUE, + remove_openmp_makevars_config = function(...) TRUE + ) mockery::stub(openmp_uninstall, "base::file.exists", function(...) FALSE) - mockery::stub(openmp_uninstall, "remove_openmp_makevars_config", function(...) TRUE) mockery::stub(openmp_uninstall, "cli::cli_alert_info", function(...) NULL) mockery::stub(openmp_uninstall, "cli::cli_bullets", function(...) NULL) mockery::stub(openmp_uninstall, "cli::cli_text", function(...) NULL) diff --git a/tests/testthat/test-shell.R b/tests/testthat/test-shell.R index 34312f0..4da11d5 100644 --- a/tests/testthat/test-shell.R +++ b/tests/testthat/test-shell.R @@ -53,11 +53,13 @@ test_that("shell_execute routes commands correctly", { mockery::stub(shell_execute, "cli::cli_bullets", function(...) NULL) # Test regular command - mockery::stub(shell_execute, "shell_command", function(cmd, verbose) { - if (cmd == "echo test") return(0) - return(1) - }) - mockery::stub(shell_execute, "shell_sudo_command", function(cmd, password, verbose) 1) + local_mocked_bindings( + shell_command = function(cmd, verbose) { + if (cmd == "echo test") return(0) + return(1) + }, + shell_sudo_command = function(cmd, password, verbose) 1 + ) mockery::stub(shell_execute, "base::Sys.time", function() Sys.time()) mockery::stub(shell_execute, "base::difftime", function(...) 1) mockery::stub(shell_execute, "base::round", function(...) 1) @@ -67,11 +69,13 @@ test_that("shell_execute routes commands correctly", { expect_equal(result, 0) # Test sudo command - mockery::stub(shell_execute, "shell_command", function(cmd, verbose) 1) - mockery::stub(shell_execute, "shell_sudo_command", function(cmd, password, verbose) { - if (cmd == "echo test" && password == "password") return(0) - return(1) - }) + local_mocked_bindings( + shell_command = function(cmd, verbose) 1, + shell_sudo_command = function(cmd, password, verbose) { + if (cmd == "echo test" && password == "password") return(0) + return(1) + } + ) result <- shell_execute("echo test", sudo = TRUE, password = "password", verbose = TRUE) expect_equal(result, 0) diff --git a/tests/testthat/test-toolchain.R b/tests/testthat/test-toolchain.R index 03ec8bf..be00985 100644 --- a/tests/testthat/test-toolchain.R +++ b/tests/testthat/test-toolchain.R @@ -1,6 +1,6 @@ test_that("macos_rtools_install performs system checks first", { # Mock system checks - mockery::stub(macos_rtools_install, "assert_mac", function() stop("Not macOS")) + local_mocked_bindings(assert_mac = function() stop("Not macOS")) # CLI mocks to avoid output mockery::stub(macos_rtools_install, "cli::cli_h3", function(...) NULL) @@ -10,27 +10,29 @@ test_that("macos_rtools_install performs system checks first", { expect_error(macos_rtools_install(), "Not macOS") # Reset mocks for assert_mac to pass but fail on next check - mockery::stub(macos_rtools_install, "assert_mac", function() NULL) - mockery::stub(macos_rtools_install, "assert_macos_supported", function() stop("Unsupported macOS")) + local_mocked_bindings( + assert_mac = function() NULL, + assert_macos_supported = function() stop("Unsupported macOS") + ) expect_error(macos_rtools_install(), "Unsupported macOS") }) test_that("macos_rtools_install orchestrates the component steps", { - mockery::stub(macos_rtools_install, "assert_mac", function() NULL) - mockery::stub(macos_rtools_install, "assert_macos_supported", function() NULL) - mockery::stub(macos_rtools_install, "assert_r_version_supported", function() NULL) - mockery::stub(macos_rtools_install, "shell_mac_version", function() "14.0") - mockery::stub(macos_rtools_install, "system_arch", function() "aarch64") + local_mocked_bindings( + assert_mac = function() NULL, + assert_macos_supported = function() NULL, + assert_r_version_supported = function() NULL, + shell_mac_version = function() "14.0", + system_arch = function() "aarch64", + rtools_install_announce = function(...) NULL, + # The three component steps are the seams; orchestration just wires them up. + rtools_install_xcode_cli = function(...) TRUE, + rtools_install_gfortran = function(...) TRUE, + rtools_install_recipes = function(...) TRUE, + rtools_install_summary = function(x, g, b) x && g && base::isTRUE(b) + ) mockery::stub(macos_rtools_install, "base::Sys.info", function() c(release = "23.0")) - mockery::stub(macos_rtools_install, "rtools_install_announce", function(...) NULL) - - # The three component steps are the seams; orchestration just wires them up. - mockery::stub(macos_rtools_install, "rtools_install_xcode_cli", function(...) TRUE) - mockery::stub(macos_rtools_install, "rtools_install_gfortran", function(...) TRUE) - mockery::stub(macos_rtools_install, "rtools_install_recipes", function(...) TRUE) - mockery::stub(macos_rtools_install, "rtools_install_summary", - function(x, g, b) x && g && base::isTRUE(b)) # Progress-bar scaffolding lives in macos_rtools_install itself mockery::stub(macos_rtools_install, "cli::cli_progress_bar", function(...) 1) @@ -44,17 +46,18 @@ test_that("macos_rtools_install orchestrates the component steps", { }) test_that("macos_rtools_install aborts when a component step fails", { - mockery::stub(macos_rtools_install, "assert_mac", function() NULL) - mockery::stub(macos_rtools_install, "assert_macos_supported", function() NULL) - mockery::stub(macos_rtools_install, "assert_r_version_supported", function() NULL) - mockery::stub(macos_rtools_install, "shell_mac_version", function() "14.0") - mockery::stub(macos_rtools_install, "system_arch", function() "aarch64") + local_mocked_bindings( + assert_mac = function() NULL, + assert_macos_supported = function() NULL, + assert_r_version_supported = function() NULL, + shell_mac_version = function() "14.0", + system_arch = function() "aarch64", + rtools_install_announce = function(...) NULL, + rtools_install_xcode_cli = function(...) TRUE, + rtools_install_gfortran = function(...) TRUE, + rtools_install_recipes = function(...) FALSE + ) mockery::stub(macos_rtools_install, "base::Sys.info", function() c(release = "23.0")) - mockery::stub(macos_rtools_install, "rtools_install_announce", function(...) NULL) - - mockery::stub(macos_rtools_install, "rtools_install_xcode_cli", function(...) TRUE) - mockery::stub(macos_rtools_install, "rtools_install_gfortran", function(...) TRUE) - mockery::stub(macos_rtools_install, "rtools_install_recipes", function(...) FALSE) mockery::stub(macos_rtools_install, "cli::cli_progress_bar", function(...) 1) mockery::stub(macos_rtools_install, "cli::cli_progress_update", function(...) NULL) @@ -73,7 +76,7 @@ test_that("rtools_install_summary returns TRUE on success and aborts on failure" mockery::stub(rtools_install_summary, "cli::cli_text", function(...) NULL) mockery::stub(rtools_install_summary, "cli::cli_alert_info", function(...) NULL) mockery::stub(rtools_install_summary, "cli::cli_alert_success", function(...) NULL) - mockery::stub(rtools_install_summary, "timestamp_now", function(...) "now") + local_mocked_bindings(timestamp_now = function(...) "now") expect_true(rtools_install_summary(TRUE, TRUE, TRUE)) expect_error(rtools_install_summary(TRUE, FALSE, TRUE), "Installation failed") @@ -86,13 +89,14 @@ test_that("rtools_install_xcode_cli installs when missing and aborts on failure" mockery::stub(rtools_install_xcode_cli, "cli::cli_alert_info", function(...) NULL) mockery::stub(rtools_install_xcode_cli, "cli::cli_bullets", function(...) NULL) mockery::stub(rtools_install_xcode_cli, "cli::cli_progress_update", function(...) NULL) - mockery::stub(rtools_install_xcode_cli, "is_xcode_app_installed", function() FALSE) - mockery::stub(rtools_install_xcode_cli, "is_xcode_cli_installed", function() FALSE) - - mockery::stub(rtools_install_xcode_cli, "xcode_cli_install", function(...) TRUE) + local_mocked_bindings( + is_xcode_app_installed = function() FALSE, + is_xcode_cli_installed = function() FALSE, + xcode_cli_install = function(...) TRUE + ) expect_true(rtools_install_xcode_cli("pw", FALSE, FALSE, NULL)) - mockery::stub(rtools_install_xcode_cli, "xcode_cli_install", function(...) FALSE) + local_mocked_bindings(xcode_cli_install = function(...) FALSE) expect_error(rtools_install_xcode_cli("pw", FALSE, FALSE, NULL), "Failed to install Xcode") }) @@ -103,12 +107,13 @@ test_that("rtools_install_gfortran installs when missing and aborts on failure", mockery::stub(rtools_install_gfortran, "cli::cli_alert_info", function(...) NULL) mockery::stub(rtools_install_gfortran, "cli::cli_bullets", function(...) NULL) mockery::stub(rtools_install_gfortran, "cli::cli_progress_update", function(...) NULL) - mockery::stub(rtools_install_gfortran, "is_gfortran_installed", function() FALSE) - - mockery::stub(rtools_install_gfortran, "gfortran_install", function(...) TRUE) + local_mocked_bindings( + is_gfortran_installed = function() FALSE, + gfortran_install = function(...) TRUE + ) expect_true(rtools_install_gfortran("pw", FALSE, FALSE, NULL, "aarch64", "4.6.0")) - mockery::stub(rtools_install_gfortran, "gfortran_install", function(...) FALSE) + local_mocked_bindings(gfortran_install = function(...) FALSE) expect_error(rtools_install_gfortran("pw", FALSE, FALSE, NULL, "aarch64", "4.6.0"), "Failed to install GNU Fortran") }) @@ -117,7 +122,7 @@ test_that("rtools_install_recipes returns the recipes install result", { mockery::stub(rtools_install_recipes, "cli::cli_h3", function(...) NULL) mockery::stub(rtools_install_recipes, "cli::cli_text", function(...) NULL) mockery::stub(rtools_install_recipes, "cli::cli_ul", function(...) NULL) - mockery::stub(rtools_install_recipes, "recipes_binary_install", function(...) TRUE) + local_mocked_bindings(recipes_binary_install = function(...) TRUE) expect_true(rtools_install_recipes("pw", FALSE, NULL, "aarch64")) }) @@ -127,24 +132,28 @@ test_that("macos_rtools_uninstall handles component uninstallations", { mockery::stub(macos_rtools_uninstall, "cli::cli_alert_info", function(...) NULL) mockery::stub(macos_rtools_uninstall, "cli::cli_bullets", function(...) NULL) mockery::stub(macos_rtools_uninstall, "cli::cli_text", function(...) NULL) - mockery::stub(macos_rtools_uninstall, "force_password", function(...) "password") mockery::stub(macos_rtools_uninstall, "cli::cli_progress_bar", function(...) 1) mockery::stub(macos_rtools_uninstall, "cli::cli_progress_update", function(...) NULL) mockery::stub(macos_rtools_uninstall, "cli::cli_progress_done", function(...) NULL) mockery::stub(macos_rtools_uninstall, "cli::cli_alert_success", function(...) NULL) # Mock component detection and uninstallation - mockery::stub(macos_rtools_uninstall, "is_xcode_cli_installed", function() TRUE) - mockery::stub(macos_rtools_uninstall, "xcode_cli_uninstall", function(...) TRUE) - mockery::stub(macos_rtools_uninstall, "is_gfortran_installed", function() TRUE) - mockery::stub(macos_rtools_uninstall, "gfortran_uninstall", function(...) TRUE) + local_mocked_bindings( + force_password = function(...) "password", + is_xcode_cli_installed = function() TRUE, + xcode_cli_uninstall = function(...) TRUE, + is_gfortran_installed = function() TRUE, + gfortran_uninstall = function(...) TRUE + ) result <- macos_rtools_uninstall(verbose = TRUE) expect_true(result) # Test when components are not installed - mockery::stub(macos_rtools_uninstall, "is_xcode_cli_installed", function() FALSE) - mockery::stub(macos_rtools_uninstall, "is_gfortran_installed", function() FALSE) + local_mocked_bindings( + is_xcode_cli_installed = function() FALSE, + is_gfortran_installed = function() FALSE + ) result <- macos_rtools_uninstall(verbose = TRUE) expect_true(result) @@ -158,18 +167,22 @@ test_that("macos_rtools_uninstall handles component failures", { mockery::stub(macos_rtools_uninstall, "cli::cli_progress_bar", function(...) 1) mockery::stub(macos_rtools_uninstall, "cli::cli_progress_update", function(...) NULL) mockery::stub(macos_rtools_uninstall, "cli::cli_abort", function(...) stop("Uninstallation failed")) - mockery::stub(macos_rtools_uninstall, "force_password", function(...) "password") # Mock component detection and uninstallation failure - mockery::stub(macos_rtools_uninstall, "is_xcode_cli_installed", function() TRUE) - mockery::stub(macos_rtools_uninstall, "xcode_cli_uninstall", function(...) FALSE) + local_mocked_bindings( + force_password = function(...) "password", + is_xcode_cli_installed = function() TRUE, + xcode_cli_uninstall = function(...) FALSE + ) expect_error(macos_rtools_uninstall(verbose = TRUE), "Uninstallation failed") # Test when gfortran fails - mockery::stub(macos_rtools_uninstall, "is_xcode_cli_installed", function() FALSE) - mockery::stub(macos_rtools_uninstall, "is_gfortran_installed", function() TRUE) - mockery::stub(macos_rtools_uninstall, "gfortran_uninstall", function(...) FALSE) + local_mocked_bindings( + is_xcode_cli_installed = function() FALSE, + is_gfortran_installed = function() TRUE, + gfortran_uninstall = function(...) FALSE + ) expect_error(macos_rtools_uninstall(verbose = TRUE), "Uninstallation failed") }) diff --git a/tests/testthat/test-xcode.R b/tests/testthat/test-xcode.R index ad757e1..eed235e 100644 --- a/tests/testthat/test-xcode.R +++ b/tests/testthat/test-xcode.R @@ -6,16 +6,18 @@ test_that("xcode_select_path returns correct output", { status = 0L ) - mockery::stub(xcode_select_path, "xcode_select", function(...) { - base::structure( - base::list( - output = "/Library/Developer/CommandLineTools", - error = "", - status = 0L - ), - class = c("xcodeselect", "cli") - ) - }) + local_mocked_bindings( + xcode_select = function(...) { + base::structure( + base::list( + output = "/Library/Developer/CommandLineTools", + error = "", + status = 0L + ), + class = c("xcodeselect", "cli") + ) + } + ) result <- xcode_select_path() expect_equal(result$status, 0L) @@ -24,113 +26,123 @@ test_that("xcode_select_path returns correct output", { test_that("is_xcode_cli_installed correctly identifies installed CLI tools", { # First, mock dependencies for success scenario - mockery::stub(is_xcode_cli_installed, "assert_mac", function() TRUE) - - # Mock successful path detection - mockery::stub(is_xcode_cli_installed, "xcode_select_path", function() { - base::structure( - base::list( - output = "/Library/Developer/CommandLineTools", - error = "", - status = 0L - ), - class = c("xcodeselect", "cli") - ) - }) - - # Mock installation directory exists - mockery::stub(is_xcode_cli_installed, "install_directory_xcode_cli", function() "/Library/Developer/CommandLineTools") + local_mocked_bindings( + assert_mac = function() TRUE, + # Mock successful path detection + xcode_select_path = function() { + base::structure( + base::list( + output = "/Library/Developer/CommandLineTools", + error = "", + status = 0L + ), + class = c("xcodeselect", "cli") + ) + }, + # Mock installation directory exists + install_directory_xcode_cli = function() "/Library/Developer/CommandLineTools" + ) mockery::stub(is_xcode_cli_installed, "base::dir.exists", function(path) TRUE) expect_true(is_xcode_cli_installed()) # Now mock failure scenarios - mockery::stub(is_xcode_cli_installed, "assert_mac", function() TRUE) - mockery::stub(is_xcode_cli_installed, "xcode_select_path", function() { - base::structure( - base::list( - output = "/Library/Developer/CommandLineTools", - error = "", - status = 1L # Non-zero status - ), - class = c("xcodeselect", "cli") - ) - }) + local_mocked_bindings( + assert_mac = function() TRUE, + xcode_select_path = function() { + base::structure( + base::list( + output = "/Library/Developer/CommandLineTools", + error = "", + status = 1L # Non-zero status + ), + class = c("xcodeselect", "cli") + ) + } + ) expect_false(is_xcode_cli_installed()) }) test_that("is_xcode_app_installed correctly identifies installed Xcode app", { # Mock dependencies for success scenario - mockery::stub(is_xcode_app_installed, "assert_mac", function() TRUE) - - # Mock successful path detection - mockery::stub(is_xcode_app_installed, "xcode_select_path", function() { - base::structure( - base::list( - output = "/Applications/Xcode.app/Contents/Developer", - error = "", - status = 0L - ), - class = c("xcodeselect", "cli") - ) - }) - - # Mock installation directory exists - mockery::stub(is_xcode_app_installed, "install_directory_xcode_app", function() "/Applications/Xcode.app/Contents/Developer") + local_mocked_bindings( + assert_mac = function() TRUE, + # Mock successful path detection + xcode_select_path = function() { + base::structure( + base::list( + output = "/Applications/Xcode.app/Contents/Developer", + error = "", + status = 0L + ), + class = c("xcodeselect", "cli") + ) + }, + # Mock installation directory exists + install_directory_xcode_app = function() "/Applications/Xcode.app/Contents/Developer" + ) mockery::stub(is_xcode_app_installed, "base::dir.exists", function(path) TRUE) expect_true(is_xcode_app_installed()) # Now mock failure scenarios - mockery::stub(is_xcode_app_installed, "xcode_select_path", function() { - base::structure( - base::list( - output = "/Library/Developer/CommandLineTools", # Wrong path - error = "", - status = 0L - ), - class = c("xcodeselect", "cli") - ) - }) + local_mocked_bindings( + xcode_select_path = function() { + base::structure( + base::list( + output = "/Library/Developer/CommandLineTools", # Wrong path + error = "", + status = 0L + ), + class = c("xcodeselect", "cli") + ) + } + ) expect_false(is_xcode_app_installed()) }) test_that("xcode_cli_path returns correct path", { # Mock successful xcode-select call - mockery::stub(xcode_cli_path, "xcode_select_path", function() { - base::structure( - base::list( - output = "/Library/Developer/CommandLineTools", - error = "", - status = 0L - ), - class = c("xcodeselect", "cli") - ) - }) + local_mocked_bindings( + xcode_select_path = function() { + base::structure( + base::list( + output = "/Library/Developer/CommandLineTools", + error = "", + status = 0L + ), + class = c("xcodeselect", "cli") + ) + } + ) expect_equal(xcode_cli_path(), "/Library/Developer/CommandLineTools") # Mock failed xcode-select call - mockery::stub(xcode_cli_path, "xcode_select_path", function() { - base::structure( - base::list( - output = "", - error = "Error: command not found", - status = 1L - ), - class = c("xcodeselect", "cli") - ) - }) + local_mocked_bindings( + xcode_select_path = function() { + base::structure( + base::list( + output = "", + error = "Error: command not found", + status = 1L + ), + class = c("xcodeselect", "cli") + ) + } + ) expect_equal(xcode_cli_path(), "") }) test_that("xcode_cli_install skips when already installed", { # Mock successful check - mockery::stub(xcode_cli_install, "assert_mac", function() TRUE) - mockery::stub(xcode_cli_install, "is_xcode_cli_installed", function() TRUE) + local_mocked_bindings( + assert_mac = function() TRUE, + is_xcode_cli_installed = function() TRUE + ) mockery::stub(xcode_cli_install, "cli::cli_alert_info", function(...) NULL) mockery::stub(xcode_cli_install, "cli::cli_text", function(...) NULL) @@ -140,9 +152,11 @@ test_that("xcode_cli_install skips when already installed", { test_that("xcode_cli_install skips when Xcode app is installed", { # Mock CLI not installed but Xcode app is - mockery::stub(xcode_cli_install, "assert_mac", function() TRUE) - mockery::stub(xcode_cli_install, "is_xcode_cli_installed", function() FALSE) - mockery::stub(xcode_cli_install, "is_xcode_app_installed", function() TRUE) + local_mocked_bindings( + assert_mac = function() TRUE, + is_xcode_cli_installed = function() FALSE, + is_xcode_app_installed = function() TRUE + ) mockery::stub(xcode_cli_install, "cli::cli_alert_info", function(...) NULL) mockery::stub(xcode_cli_install, "cli::cli_bullets", function(...) NULL) mockery::stub(xcode_cli_install, "cli::cli_text", function(...) NULL)