diff --git a/.github/workflows/R-CMD-check-wsl.yaml b/.github/workflows/R-CMD-check-wsl.yaml index 2d6475c3f..699958a91 100644 --- a/.github/workflows/R-CMD-check-wsl.yaml +++ b/.github/workflows/R-CMD-check-wsl.yaml @@ -30,11 +30,6 @@ jobs: CMDSTANR_OPENCL_TESTS: true steps: - - name: cmdstan env vars - run: | - echo "CMDSTAN_PATH=${HOME}/.cmdstan" >> $GITHUB_ENV - shell: bash - - uses: actions/checkout@v6 - uses: r-lib/actions/setup-r@v2 @@ -44,7 +39,7 @@ jobs: with: extra-packages: any::rcmdcheck, local::. - - uses: Vampire/setup-wsl@v6 + - uses: Vampire/setup-wsl@v7 with: distribution: Ubuntu-22.04 wsl-version: 2 @@ -55,12 +50,18 @@ jobs: run: | sudo apt-get update sudo apt-get install -y build-essential libopenmpi-dev ocl-icd-opencl-dev pocl-opencl-icd + sudo chmod 755 /root shell: wsl-bash {0} - name: Install cmdstan run: | cmdstanr::check_cmdstan_toolchain() cmdstanr::install_cmdstan(cores = 2, wsl = TRUE, overwrite = TRUE) + cat( + paste0("CMDSTAN=", cmdstanr::cmdstan_path(), "\n"), + file = Sys.getenv("GITHUB_ENV"), + append = TRUE + ) shell: Rscript {0} - name: Session info diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index be322ea4a..85293dab4 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -53,11 +53,6 @@ jobs: PKG_SYSREQS_DB_UPDATE_TIMEOUT: 30s steps: - - name: cmdstan env vars - run: | - echo "CMDSTAN_PATH=${HOME}/.cmdstan" >> $GITHUB_ENV - shell: bash - - uses: actions/checkout@v6 - uses: r-lib/actions/setup-r@v2 diff --git a/.github/workflows/Test-coverage.yaml b/.github/workflows/Test-coverage.yaml index 5598d53b6..166ad66c3 100644 --- a/.github/workflows/Test-coverage.yaml +++ b/.github/workflows/Test-coverage.yaml @@ -32,11 +32,6 @@ jobs: NOT_CRAN: true steps: - - name: cmdstan env vars - run: | - echo "CMDSTAN_PATH=${HOME}/.cmdstan" >> $GITHUB_ENV - shell: bash - - uses: n1hility/cancel-previous-runs@v3 with: token: ${{ secrets.GITHUB_TOKEN }} diff --git a/R/install.R b/R/install.R index 2aa5453c6..ee91fd1cb 100644 --- a/R/install.R +++ b/R/install.R @@ -327,11 +327,17 @@ cmdstan_make_local <- function(dir = cmdstan_path(), } write(built_flags, file = make_local_path, append = append) } - if (file.exists(make_local_path)) { - return(trimws(strsplit(trimws(readChar(make_local_path, file.info(make_local_path)$size)), "\n")[[1]])) - } else { + make_local_contents <- tryCatch( + suppressWarnings(readLines(make_local_path, warn = FALSE)), + error = function(e) NULL + ) + if (is.null(make_local_contents)) { return(NULL) } + if (length(make_local_contents) == 0) { + return("") + } + trimws(strsplit(trimws(paste(make_local_contents, collapse = "\n")), "\n", fixed = TRUE)[[1]]) } #' @rdname install_cmdstan diff --git a/R/path.R b/R/path.R index 3e4fecc15..e2c564db2 100644 --- a/R/path.R +++ b/R/path.R @@ -9,7 +9,8 @@ #' @export #' #' @param path (string) The full file path to the CmdStan installation. If -#' `NULL` (the default) then the path is set to the default path used by +#' `NULL` (the default) then the path is set using the `"CMDSTAN"` +#' environment variable when available, otherwise the default path used by #' [install_cmdstan()] if it exists. #' @return A string. Either the file path to the CmdStan installation or the #' CmdStan version number. @@ -39,7 +40,20 @@ #' set_cmdstan_path <- function(path = NULL) { if (is.null(path)) { - path <- cmdstan_default_path() + env_path <- resolve_cmdstan_path_from_env() + if (isTRUE(is.na(env_path))) { + unset_cmdstan_path() + return(invisible(NULL)) + } + if (!is.null(env_path)) { + path <- env_path + } else { + path <- cmdstan_default_path() + if (is.null(path)) { + unset_cmdstan_path() + return(invisible(NULL)) + } + } } if (dir.exists(path)) { path <- absolute_path(path) @@ -50,9 +64,7 @@ set_cmdstan_path <- function(path = NULL) { "cmdstanr now requires CmdStan v", cmdstan_min_version(), " or newer.", call. = FALSE ) - .cmdstanr$PATH <- NULL - .cmdstanr$VERSION <- NULL - .cmdstanr$WSL <- FALSE + unset_cmdstan_path() return(invisible(path)) } .cmdstanr$PATH <- path @@ -60,7 +72,7 @@ set_cmdstan_path <- function(path = NULL) { .cmdstanr$WSL <- grepl("//wsl$", path, fixed = TRUE) message("CmdStan path set to: ", path) } else { - warning("Path not set. Can't find directory: ", path, call. = FALSE) + warning("CmdStan path not set. Can't find directory: ", path, call. = FALSE) } invisible(path) } @@ -102,6 +114,12 @@ cmdstan_version <- function(error_on_NA = TRUE) { .cmdstanr$TEMP_DIR <- NULL .cmdstanr$WSL <- FALSE +unset_cmdstan_path <- function() { + .cmdstanr$PATH <- NULL + .cmdstanr$VERSION <- NULL + .cmdstanr$WSL <- FALSE +} + # path to temp directory cmdstan_tempdir <- function() { .cmdstanr$TEMP_DIR @@ -128,8 +146,36 @@ is_supported_cmdstan_version <- function(version) { isTRUE(cmp >= 0) } -#' cmdstan_default_install_path -#' +resolve_cmdstan_path_from_env <- function() { + path <- Sys.getenv("CMDSTAN") + if (!nzchar(path)) { + return(NULL) + } + if (!dir.exists(path)) { + warning( + "CmdStan path not set. Can't find directory specified by environment ", + "variable 'CMDSTAN'.", + call. = FALSE + ) + return(NA_character_) + } + path <- absolute_path(path) + version <- suppressWarnings(read_cmdstan_version(path)) + if (!is.null(version)) { + return(path) + } + path <- cmdstan_default_path(dir = path) + if (is.null(path)) { + warning( + "CmdStan path not set. No CmdStan installation found in the path ", + "specified by the environment variable 'CMDSTAN'.", + call. = FALSE + ) + return(NA_character_) + } + path +} + #' Path to where [install_cmdstan()] with default settings installs CmdStan. #' #' @keywords internal @@ -140,14 +186,24 @@ cmdstan_default_install_path <- function(wsl = FALSE) { if (wsl) { file.path(paste0(wsl_dir_prefix(wsl = TRUE), wsl_home_dir()), ".cmdstan") } else { - file.path(.home_path(), ".cmdstan") + file.path(home_path(), ".cmdstan") } } -#' cmdstan_default_path -#' -#' Returns the path to the installation of CmdStan with the most recent release -#' version. +home_path <- function() { + home <- Sys.getenv("HOME") + if (os_is_windows()) { + userprofile <- Sys.getenv("USERPROFILE") + h_drivepath <- file.path(Sys.getenv("HOMEDRIVE"), Sys.getenv("HOMEPATH")) + win_home <- ifelse(userprofile == "", h_drivepath, userprofile) + if (win_home != "") { + home <- win_home + } + } + home +} + +#' Path to the installation of CmdStan with the most recent release version #' #' For Windows systems with WSL CmdStan installs, if there are side-by-side WSL #' and native installs with the same version then the WSL is preferred. @@ -180,9 +236,9 @@ cmdstan_default_path <- function(dir = NULL) { } if (dir.exists(installs_path) || wsl_path_exists) { latest_cmdstan <- ifelse(dir.exists(installs_path), - .latest_cmdstan_installed(installs_path), "") + latest_cmdstan_installed(installs_path), "") latest_wsl_cmdstan <- ifelse(wsl_path_exists, - .latest_cmdstan_installed(wsl_installs_path), "") + latest_cmdstan_installed(wsl_installs_path), "") if (!nzchar(latest_cmdstan) && !nzchar(latest_wsl_cmdstan)) { return(NULL) } @@ -195,16 +251,8 @@ cmdstan_default_path <- function(dir = NULL) { NULL } -.latest_cmdstan_installed <- function(installs_path) { +latest_cmdstan_installed <- function(installs_path) { cmdstan_installs <- list.dirs(path = installs_path, recursive = FALSE, full.names = FALSE) - # if installed in cmdstan folder with no version move to cmdstan-version folder - if ("cmdstan" %in% cmdstan_installs) { - ver <- read_cmdstan_version(file.path(installs_path, "cmdstan")) - old_path <- file.path(installs_path, "cmdstan") - new_path <- file.path(installs_path, paste0("cmdstan-", ver)) - file.rename(old_path, new_path) - cmdstan_installs <- list.dirs(path = installs_path, recursive = FALSE, full.names = FALSE) - } latest_cmdstan <- "" if (length(cmdstan_installs) > 0) { cmdstan_installs <- grep("^cmdstan-", cmdstan_installs, value = TRUE) @@ -219,6 +267,79 @@ cmdstan_default_path <- function(dir = NULL) { latest_cmdstan } +is_wsl_unc_path <- function(path) { + is.character(path) && + length(path) == 1 && + !is.na(path) && + startsWith(repair_path(path), "//wsl$/") +} + +# Extract the distro name from a WSL UNC path like //wsl$/Ubuntu-22.04/... +wsl_unc_distro_name <- function(path) { + sub("^//wsl\\$/([^/]+).*$", "\\1", repair_path(path)) +} + +# Convert a WSL UNC path to the corresponding Linux path within the distro. +wsl_unc_path_to_linux <- function(path) { + sub("^//wsl\\$/[^/]+", "", repair_path(path)) +} + +cmdstan_version_from_path <- function(path) { + path <- repair_path(path) + match <- regmatches( + path, + regexpr("cmdstan-[0-9]+\\.[0-9]+\\.[0-9]+(?:-rc[0-9]+)?$", path) + ) + if (!length(match) || is.na(match) || !nzchar(match)) { + return(NULL) + } + sub("^cmdstan-", "", match) +} + +read_lines_direct <- function(path) { + tryCatch( + suppressWarnings(readLines(path, warn = FALSE)), + error = function(e) NULL + ) +} + +# Fall back to reading through `wsl` when Windows R can't read a WSL UNC path. +read_lines_via_wsl <- function(path) { + wsl_args <- list( + c("cat", wsl_unc_path_to_linux(path)), + c("-d", wsl_unc_distro_name(path), "cat", wsl_unc_path_to_linux(path)) + ) + file_contents <- NULL + for (args in wsl_args) { + file_contents <- processx::run( + command = "wsl", + args = args, + error_on_status = FALSE + ) + if (file_contents$status == 0) { + break + } + } + if (is.null(file_contents) || file_contents$status != 0) { + return(NULL) + } + if (!nzchar(file_contents$stdout)) { + return(character(0)) + } + con <- textConnection(file_contents$stdout) + on.exit(close(con), add = TRUE) + readLines(con, warn = FALSE) +} + +# Preserve existing direct reads and only use the WSL fallback when needed. +read_lines_with_wsl_fallback <- function(path) { + file_contents <- read_lines_direct(path) + if (!is.null(file_contents) || !is_wsl_unc_path(path)) { + return(file_contents) + } + read_lines_via_wsl(path) +} + #' Find the version of CmdStan from makefile #' @noRd @@ -226,7 +347,14 @@ cmdstan_default_path <- function(dir = NULL) { #' @return Version number as a string. read_cmdstan_version <- function(path) { makefile_path <- file.path(path, "makefile") - if (!file.exists(makefile_path)) { + makefile <- read_lines_with_wsl_fallback(makefile_path) + if (is.null(makefile)) { + if (is_wsl_unc_path(path)) { + version_from_path <- cmdstan_version_from_path(path) + if (!is.null(version_from_path)) { + return(version_from_path) + } + } warning( "Can't find CmdStan makefile to detect version number. ", "Path may not point to valid installation.", @@ -234,7 +362,6 @@ read_cmdstan_version <- function(path) { ) return(NULL) } - makefile <- readLines(makefile_path) version_line <- grep("^CMDSTAN_VERSION :=", makefile, value = TRUE) if (length(version_line) == 0) { stop("CmdStan makefile is missing a version number.", call. = FALSE) @@ -242,6 +369,7 @@ read_cmdstan_version <- function(path) { sub("CMDSTAN_VERSION := ", "", version_line) } + #' Returns whether the supplied installation is a release candidate #' @noRd #' @param path Path to installation. @@ -253,12 +381,6 @@ is_release_candidate <- function(path) { grepl(pattern = "-rc[0-9]*$", x = path) } -# unset the path (only used in tests) -unset_cmdstan_path <- function() { - .cmdstanr$PATH <- NULL - .cmdstanr$VERSION <- NULL - .cmdstanr$WSL <- FALSE -} # fake a cmdstan version (only used in tests) fake_cmdstan_version <- function(version, mod = NULL) { @@ -275,16 +397,3 @@ fake_cmdstan_version <- function(version, mod = NULL) { reset_cmdstan_version <- function(mod = NULL) { fake_cmdstan_version(read_cmdstan_version(cmdstan_path()), mod = mod) } - -.home_path <- function() { - home <- Sys.getenv("HOME") - if (os_is_windows()) { - userprofile <- Sys.getenv("USERPROFILE") - h_drivepath <- file.path(Sys.getenv("HOMEDRIVE"), Sys.getenv("HOMEPATH")) - win_home <- ifelse(userprofile == "", h_drivepath, userprofile) - if (win_home != "") { - home <- win_home - } - } - home -} diff --git a/R/zzz.R b/R/zzz.R index 68d59245f..bea367db0 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -35,46 +35,7 @@ startup_messages <- function() { } cmdstanr_initialize <- function() { - # First check for environment variable CMDSTAN, but if not found - # then see if default - path <- Sys.getenv("CMDSTAN") - if (isTRUE(nzchar(path))) { # CMDSTAN environment variable found - if (dir.exists(path)) { - path <- absolute_path(path) - suppressWarnings(suppressMessages(set_cmdstan_path(path))) - if (is.null(cmdstan_version(error_on_NA = FALSE))) { - path <- cmdstan_default_path(dir = path) - if (is.null(path)) { - warning( - "No CmdStan installation found in the path specified ", - "by the environment variable 'CMDSTAN'.", - call. = FALSE - ) - .cmdstanr$PATH <- NULL - .cmdstanr$VERSION <- NULL - .cmdstanr$WSL <- FALSE - } else { - set_cmdstan_path(path) - } - } - } else { - warning( - "Can't find directory specified by environment variable 'CMDSTAN'. ", - "Path not set.", - call. = FALSE - ) - .cmdstanr$PATH <- NULL - .cmdstanr$VERSION <- NULL - .cmdstanr$WSL <- FALSE - } - - } else { # environment variable not found - path <- cmdstan_default_path() - if (!is.null(path)) { - suppressMessages(set_cmdstan_path(path)) - } - } - + suppressMessages(set_cmdstan_path()) .cmdstanr$TEMP_DIR <- tempdir(check = TRUE) invisible(TRUE) } diff --git a/man/cmdstan_default_install_path.Rd b/man/cmdstan_default_install_path.Rd index b5ebd9b73..5faaa03ea 100644 --- a/man/cmdstan_default_install_path.Rd +++ b/man/cmdstan_default_install_path.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/path.R \name{cmdstan_default_install_path} \alias{cmdstan_default_install_path} -\title{cmdstan_default_install_path} +\title{Path to where \code{\link[=install_cmdstan]{install_cmdstan()}} with default settings installs CmdStan.} \usage{ cmdstan_default_install_path(wsl = FALSE) } diff --git a/man/cmdstan_default_path.Rd b/man/cmdstan_default_path.Rd index 2d4ffe9b0..c4b8f3fbf 100644 --- a/man/cmdstan_default_path.Rd +++ b/man/cmdstan_default_path.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/path.R \name{cmdstan_default_path} \alias{cmdstan_default_path} -\title{cmdstan_default_path} +\title{Path to the installation of CmdStan with the most recent release version} \usage{ cmdstan_default_path(dir = NULL) } @@ -14,10 +14,6 @@ Path to the CmdStan installation with the most recent release version, or \code{NULL} if no installation found. } \description{ -Returns the path to the installation of CmdStan with the most recent release -version. -} -\details{ For Windows systems with WSL CmdStan installs, if there are side-by-side WSL and native installs with the same version then the WSL is preferred. Otherwise, the most recent release is chosen, regardless of whether it is diff --git a/man/cmdstanr-package.Rd b/man/cmdstanr-package.Rd index e6479f576..be303bac3 100644 --- a/man/cmdstanr-package.Rd +++ b/man/cmdstanr-package.Rd @@ -34,22 +34,22 @@ algorithms, and writing results to output files. \subsection{Advantages of RStan}{ \itemize{ \item Allows other developers to distribute R packages with \emph{pre-compiled} -Stan programs (like \strong{rstanarm}) on CRAN. (Note: As of 2023, this -can mostly be achieved with CmdStanR as well. See \href{https://mc-stan.org/cmdstanr/articles/cmdstanr-internals.html#developing-using-cmdstanr}{Developing using CmdStanR}.) -\item Avoids use of R6 classes, which may result in more familiar syntax -for many R users. +Stan programs (like \strong{rstanarm}) on CRAN. (Note: As of 2023, this can +mostly be achieved with CmdStanR as well. See \href{https://mc-stan.org/cmdstanr/articles/cmdstanr-internals.html#developing-using-cmdstanr}{Developing using CmdStanR}.) +\item Avoids use of R6 classes, which may result in more familiar syntax for +many R users. \item CRAN binaries available for Mac and Windows. } } \subsection{Advantages of CmdStanR}{ \itemize{ -\item Compatible with latest versions of Stan. Keeping up with Stan -releases is complicated for RStan, often requiring non-trivial -changes to the \strong{rstan} package and new CRAN releases of both -\strong{rstan} and \strong{StanHeaders}. With CmdStanR the latest improvements -in Stan will be available from R immediately after updating CmdStan -using \code{cmdstanr::install_cmdstan()}. +\item Compatible with latest versions of Stan. Keeping up with Stan releases +is complicated for RStan, often requiring non-trivial changes to the +\strong{rstan} package and new CRAN releases of both \strong{rstan} and +\strong{StanHeaders}. With CmdStanR the latest improvements in Stan will be +available from R immediately after updating CmdStan using +\code{cmdstanr::install_cmdstan()}. \item Running Stan via external processes results in fewer unexpected crashes, especially in RStudio. \item Less memory overhead. @@ -229,6 +229,7 @@ Other contributors: \item Martin ModrĂ¡k [contributor] \item Ven Popov [contributor] \item Visruth Srimath Kandali [contributor] + \item Aki Vehtari [contributor] } } diff --git a/man/set_cmdstan_path.Rd b/man/set_cmdstan_path.Rd index b803cdd8e..fe77e09c3 100644 --- a/man/set_cmdstan_path.Rd +++ b/man/set_cmdstan_path.Rd @@ -14,7 +14,8 @@ cmdstan_version(error_on_NA = TRUE) } \arguments{ \item{path}{(string) The full file path to the CmdStan installation. If -\code{NULL} (the default) then the path is set to the default path used by +\code{NULL} (the default) then the path is set using the \code{"CMDSTAN"} +environment variable when available, otherwise the default path used by \code{\link[=install_cmdstan]{install_cmdstan()}} if it exists.} \item{error_on_NA}{(logical) Should an error be thrown if CmdStan is not diff --git a/tests/testthat/test-install.R b/tests/testthat/test-install.R index ccb781cc1..d892c273f 100644 --- a/tests/testthat/test-install.R +++ b/tests/testthat/test-install.R @@ -143,6 +143,7 @@ test_that("toolchain checks on Unix work", { }) test_that("clean and rebuild works", { + set_cmdstan_path(cmdstan_default_path()) expect_output( rebuild_cmdstan(cores = CORES), paste0("CmdStan v", cmdstan_version(), " built"), diff --git a/tests/testthat/test-path.R b/tests/testthat/test-path.R index 5bc258191..6dd1e8dbe 100644 --- a/tests/testthat/test-path.R +++ b/tests/testthat/test-path.R @@ -45,6 +45,31 @@ test_that("Setting path from env var is detected", { expect_false(is.null(.cmdstanr$VERSION)) }) +test_that("set_cmdstan_path() uses CMDSTAN env var when path is omitted", { + unset_cmdstan_path() + withr::local_envvar(c(CMDSTAN = PATH)) + expect_message( + set_cmdstan_path(), + paste("CmdStan path set to:", PATH), + fixed = TRUE + ) + expect_equal(cmdstan_path(), PATH) +}) + +test_that("set_cmdstan_path() clears cached state when no path is detected", { + .cmdstanr$PATH <- PATH + .cmdstanr$VERSION <- VERSION + .cmdstanr$WSL <- TRUE + local_mocked_bindings( + resolve_cmdstan_path_from_env = function() NULL, + cmdstan_default_path = function(dir = NULL) NULL + ) + expect_silent(set_cmdstan_path()) + expect_null(.cmdstanr$PATH) + expect_null(.cmdstanr$VERSION) + expect_false(isTRUE(.cmdstanr$WSL)) +}) + test_that("Unsupported CmdStan path from env var is rejected", { unset_cmdstan_path() .cmdstanr$WSL <- TRUE @@ -73,7 +98,7 @@ test_that("Existing CMDSTAN env path with no install resets cached state", { withr::local_envvar(c(CMDSTAN = empty_parent)) expect_warning( cmdstanr_initialize(), - "No CmdStan installation found in the path specified by the environment variable 'CMDSTAN'.", + "CmdStan path not set. No CmdStan installation found in the path specified by the environment variable 'CMDSTAN'.", fixed = TRUE ) expect_null(.cmdstanr$PATH) @@ -178,12 +203,117 @@ test_that("cmdstan_default_path() returns NULL for empty custom install director expect_null(cmdstan_default_path(dir = installs)) }) +test_that("cmdstan_default_path() ignores unversioned cmdstan directory", { + installs <- withr::local_tempdir(pattern = "cmdstan-legacy-installs") + dir.create(file.path(installs, "cmdstan"), recursive = TRUE, showWarnings = FALSE) + dir.create(file.path(installs, "cmdstan-2.36.0"), recursive = TRUE, showWarnings = FALSE) + + expect_equal( + cmdstan_default_path(dir = installs), + file.path(installs, "cmdstan-2.36.0") + ) + expect_true(dir.exists(file.path(installs, "cmdstan"))) +}) + test_that("CmdStan version helpers handle invalid inputs", { expect_identical(cmdstan_min_version(), "2.35.0") expect_false(is_supported_cmdstan_version(NULL)) expect_false(is_supported_cmdstan_version("not-a-version")) }) +test_that("WSL UNC path helpers work", { + wsl_path <- "//wsl$/Ubuntu-22.04/root/.cmdstan/cmdstan-2.38.0" + expect_true(is_wsl_unc_path(wsl_path)) + expect_false(is_wsl_unc_path("C:/Users/runneradmin/.cmdstan/cmdstan-2.38.0")) + expect_equal(wsl_unc_distro_name(wsl_path), "Ubuntu-22.04") + expect_equal(cmdstan_version_from_path(wsl_path), "2.38.0") + expect_equal(cmdstan_version_from_path(paste0(wsl_path, "/")), "2.38.0") + expect_null(cmdstan_version_from_path("//wsl$/Ubuntu-22.04/root/not-cmdstan")) + expect_equal( + wsl_unc_path_to_linux(file.path(wsl_path, "makefile")), + "/root/.cmdstan/cmdstan-2.38.0/makefile" + ) +}) + +test_that("read_cmdstan_version() prefers direct reads for UNC paths", { + wsl_path <- "//wsl$/Ubuntu-22.04/root/.cmdstan/cmdstan-2.38.0" + local_mocked_bindings( + read_lines_direct = function(path) { + expect_equal( + path, + file.path(wsl_path, "makefile") + ) + "CMDSTAN_VERSION := 2.38.0" + }, + read_lines_via_wsl = function(path) { + stop("WSL fallback should not be used when direct reads succeed.") + } + ) + expect_equal(read_cmdstan_version(wsl_path), "2.38.0") +}) + +test_that("read_cmdstan_version() falls back to distro-aware WSL reads", { + wsl_path <- "//wsl$/Ubuntu-22.04/root/.cmdstan/cmdstan-2.38.0" + local_mocked_bindings( + read_lines_direct = function(path) { + expect_equal( + path, + file.path(wsl_path, "makefile") + ) + NULL + }, + read_lines_via_wsl = function(path) { + expect_equal(path, file.path(wsl_path, "makefile")) + "CMDSTAN_VERSION := 2.38.0" + } + ) + expect_equal(read_cmdstan_version(wsl_path), "2.38.0") +}) + +test_that("read_cmdstan_version() can recover version from WSL install path", { + wsl_path <- "//wsl$/Ubuntu-22.04/root/.cmdstan/cmdstan-2.38.0" + local_mocked_bindings( + read_lines_direct = function(path) { + expect_equal(path, file.path(wsl_path, "makefile")) + NULL + }, + read_lines_via_wsl = function(path) { + expect_equal(path, file.path(wsl_path, "makefile")) + NULL + } + ) + expect_equal(read_cmdstan_version(wsl_path), "2.38.0") +}) + +test_that("read_lines_via_wsl() tries plain wsl before distro-specific fallback", { + wsl_path <- "//wsl$/Ubuntu-22.04/root/.cmdstan/cmdstan-2.38.0/makefile" + calls <- list() + out <- with_mocked_bindings( + read_lines_via_wsl(wsl_path), + run = function(command, args, error_on_status = FALSE) { + calls[[length(calls) + 1]] <<- list( + command = command, + args = args, + error_on_status = error_on_status + ) + if (length(calls) == 1) { + return(list(status = 1, stdout = "")) + } + list(status = 0, stdout = "CMDSTAN_VERSION := 2.38.0") + }, + .package = "processx" + ) + + expect_equal(out, "CMDSTAN_VERSION := 2.38.0") + expect_length(calls, 2) + expect_equal(calls[[1]]$command, "wsl") + expect_equal(calls[[1]]$args, c("cat", "/root/.cmdstan/cmdstan-2.38.0/makefile")) + expect_equal( + calls[[2]]$args, + c("-d", "Ubuntu-22.04", "cat", "/root/.cmdstan/cmdstan-2.38.0/makefile") + ) +}) + test_that("cmdstan_ext() works", { if (os_is_windows() && !os_is_wsl()) { expect_identical(cmdstan_ext(), ".exe") diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R index b27628c5b..2aaf0abad 100644 --- a/tests/testthat/test-utils.R +++ b/tests/testthat/test-utils.R @@ -196,6 +196,14 @@ test_that("cmdstan_make_local() works", { cmdstan_make_local(cpp_options = as.list(exisiting_make_local), append = FALSE) }) +test_that("cmdstan_make_local() preserves empty make/local behavior", { + dir <- withr::local_tempdir() + dir.create(file.path(dir, "make"), recursive = TRUE, showWarnings = FALSE) + file.create(file.path(dir, "make", "local")) + + expect_identical(cmdstan_make_local(dir = dir), "") +}) + test_that("matching_variables() works", { ret <- matching_variables(c("beta"), c("alpha", "beta[1]", "beta[2]", "beta[3]")) expect_equal(