Skip to content

Commit d15f53f

Browse files
authored
Merge pull request #1214 from stan-dev/bugfix/wsl-output-paths-1110
Fix handling of wsl output paths
2 parents e81b379 + 5e747ec commit d15f53f

5 files changed

Lines changed: 181 additions & 17 deletions

File tree

NEWS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# cmdstanr (development version)
22

3+
* When using CmdStan through WSL, paths for output, diagnostic, profile, config,
4+
and metric files now remain accessible to Windows R when an explicit output
5+
directory is supplied. (#1110; related: #1113)
36
* Lists of matrices/vectors and data frames can now be supplied for variables
47
declared as `int` in the Stan program. Previously these worked only for `real`
58
variables and errored for `int` ones. (#817)

R/args.R

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,10 @@ CmdStanArgs <- R6::R6Class(
6161
self$using_tempdir <- is.null(output_dir)
6262
self$model_variables <- model_variables
6363
self$save_cmdstan_config <- save_cmdstan_config
64-
if (os_is_wsl()) {
64+
if (os_is_wsl() && self$using_tempdir) {
6565
# Want to ensure that any files under WSL are written to a tempdir within
6666
# WSL to avoid IO performance issues
67-
self$output_dir <- ifelse(is.null(output_dir),
68-
file.path(wsl_dir_prefix(), wsl_tempdir()),
69-
wsl_safe_path(output_dir))
67+
self$output_dir <- file.path(wsl_dir_prefix(), wsl_tempdir())
7068
} else {
7169
self$output_dir <- output_dir %||% tempdir(check = TRUE)
7270
}

R/fit.R

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,6 @@ CmdStanFit <- R6::R6Class(
3535
if (!is.null(private$model_methods_env_$model_ptr)) {
3636
initialize_model_pointer(private$model_methods_env_, self$data_file(), 0)
3737
}
38-
# Need to update the output directory path to one that can be accessed
39-
# from Windows, for the post-processing of results
40-
self$runset$args$output_dir <- wsl_safe_path(self$runset$args$output_dir, revert = TRUE)
4138
invisible(self)
4239
},
4340
num_procs = function() {

R/run.R

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ CmdStanRun <- R6::R6Class(
7676
},
7777
config_files = function(include_failed = FALSE) {
7878
files <- private$config_files_
79-
files_win_path <- sapply(private$config_files_, wsl_safe_path, revert = TRUE)
8079
if (include_failed) {
8180
files
8281
} else {
@@ -86,7 +85,6 @@ CmdStanRun <- R6::R6Class(
8685
},
8786
metric_files = function(include_failed = FALSE) {
8887
files <- private$metric_files_
89-
files_win_path <- sapply(private$metric_files_, wsl_safe_path, revert = TRUE)
9088
if (include_failed) {
9189
files
9290
} else {

tests/testthat/test-model-output_dir.R

Lines changed: 176 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,98 @@ local_output_sandbox <- function(pattern = "sandbox", .local_envir = parent.fram
44
withr::local_tempdir(pattern = pattern, .local_envir = .local_envir)
55
}
66

7+
test_that("WSL output paths stay host-native until command composition", {
8+
# Use minimal method arguments so this test exercises path handling without
9+
# launching CmdStan.
10+
method_args <- list(
11+
method = "sample",
12+
save_metric = NULL,
13+
validate = function(num_procs) invisible(),
14+
compose = function(idx, args) args
15+
)
16+
# Cover system and non-system Windows drives as well as a WSL UNC path.
17+
host_dirs <- c(
18+
"C:/output",
19+
"D:/output",
20+
"//wsl$/Ubuntu/home/user/output"
21+
)
22+
wsl_dirs <- c(
23+
"/mnt/c/output",
24+
"/mnt/d/output",
25+
"/home/user/output"
26+
)
27+
as_wsl_path <- function(path = NULL, revert = FALSE) {
28+
if (is.null(path) || revert) {
29+
return(path)
30+
}
31+
path <- sub("//wsl$/Ubuntu", "", path, fixed = TRUE)
32+
for (i in seq_along(host_dirs)) {
33+
path <- sub(host_dirs[i], wsl_dirs[i], path, fixed = TRUE)
34+
}
35+
path
36+
}
37+
# Simulate Windows R using WSL so this boundary test runs on every platform.
38+
with_mocked_bindings(
39+
{
40+
args <- lapply(host_dirs, function(output_dir) {
41+
CmdStanArgs$new(
42+
model_name = "model",
43+
exe_file = "model",
44+
proc_ids = 1,
45+
method_args = method_args,
46+
output_dir = output_dir,
47+
output_basename = "model"
48+
)
49+
})
50+
output_files <- file.path(host_dirs, "model-1.csv")
51+
expect_equal(
52+
vapply(args, function(x) x$output_dir, character(1)),
53+
host_dirs
54+
)
55+
expect_equal(
56+
vapply(args, function(x) x$new_files("output"), character(1)),
57+
output_files
58+
)
59+
cmdstan_output_files <- vapply(seq_along(args), function(i) {
60+
command_args <- args[[i]]$compose_all_args(
61+
output_file = output_files[i]
62+
)
63+
sub("file=", "", command_args[grepl("^file=", command_args)], fixed = TRUE)
64+
}, character(1))
65+
expect_equal(cmdstan_output_files, file.path(wsl_dirs, "model-1.csv"))
66+
67+
command_args <- args[[1]]$compose_all_args(
68+
output_file = output_files[1],
69+
profile_file = file.path(host_dirs[1], "model-profile-1.csv"),
70+
latent_dynamics_file = file.path(host_dirs[1], "model-diagnostic-1.csv")
71+
)
72+
expect_in("diagnostic_file=/mnt/c/output/model-diagnostic-1.csv", command_args)
73+
expect_in("profile_file=/mnt/c/output/model-profile-1.csv", command_args)
74+
75+
# Omitting output_dir must still use the faster WSL-native temp directory.
76+
default_args <- CmdStanArgs$new(
77+
model_name = "model",
78+
exe_file = "model",
79+
proc_ids = 1,
80+
method_args = method_args,
81+
output_basename = "model"
82+
)
83+
expect_equal(default_args$output_dir, "//wsl$/Ubuntu/tmp/cmdstanr")
84+
expect_in(
85+
"file=/tmp/cmdstanr/model-1.csv",
86+
default_args$compose_all_args(
87+
output_file = default_args$new_files("output")
88+
)
89+
)
90+
},
91+
os_is_wsl = function() TRUE,
92+
wsl_safe_path = as_wsl_path,
93+
wsl_dir_prefix = function(...) "//wsl$/Ubuntu",
94+
wsl_tempdir = function() "/tmp/cmdstanr",
95+
validate_cmdstan_args = function(self) invisible()
96+
)
97+
})
98+
799
test_that("all fitting methods work with output_dir", {
8100
sandbox <- local_output_sandbox()
9101
for (method in c("sample", "optimize", "variational")) {
@@ -29,24 +121,22 @@ test_that("all fitting methods work with output_dir", {
29121
call_args$save_metric <- TRUE
30122
}
31123
fit <- do.call(testing_fit, call_args)
32-
# WSL path manipulations result in a short path which slightly differs
33-
# from the original tempdir(), so need to normalise both for comparison
124+
# Normalize to account for platform-specific path representations.
34125
expect_equal(normalizePath(fit$runset$args$output_dir),
35126
normalizePath(method_dir))
36127
files <- normalizePath(list.files(method_dir, full.names = TRUE))
128+
expect_equal(files[grepl("\\.csv$", files)],
129+
normalizePath(fit$output_files()))
37130
if (method == "sample") {
38131
mult <- 3
39132
expect_equal(files[grepl("metric", files)],
40-
normalizePath(sapply(fit$metric_files(), wsl_safe_path, revert = TRUE,
41-
USE.NAMES = FALSE)))
133+
normalizePath(fit$metric_files()))
42134
expect_equal(files[grepl("config", files)],
43-
normalizePath(sapply(fit$config_files(), wsl_safe_path, revert = TRUE,
44-
USE.NAMES = FALSE)))
135+
normalizePath(fit$config_files()))
45136
} else {
46137
mult <- 2
47138
expect_equal(files[grepl("config", files)],
48-
normalizePath(sapply(fit$config_files(), wsl_safe_path, revert = TRUE,
49-
USE.NAMES = FALSE)))
139+
normalizePath(fit$config_files()))
50140
}
51141
expect_equal(length(list.files(method_dir)), mult * fit$num_procs())
52142

@@ -69,6 +159,84 @@ test_that("all fitting methods work with output_dir", {
69159
sum(grepl("diagnostic", files)),
70160
fit$num_procs()
71161
)
162+
expect_equal(
163+
normalizePath(fit$latent_dynamics_files()),
164+
normalizePath(list.files(
165+
file.path(sandbox, "sample"),
166+
pattern = "diagnostic",
167+
full.names = TRUE
168+
))
169+
)
170+
})
171+
172+
test_that("explicit WSL output paths are usable by Windows R", {
173+
skip_if_not(os_is_wsl())
174+
# Unlike the mocked test above, this exercises the full Windows/WSL workflow.
175+
output_dir <- local_output_sandbox("wsl-output-dir")
176+
mod <- testing_model("logistic_profiling")
177+
utils::capture.output(
178+
fit <- mod$sample(
179+
data = testing_data("logistic"),
180+
chains = 1,
181+
parallel_chains = 1,
182+
seed = 123,
183+
refresh = 0,
184+
output_dir = output_dir,
185+
save_latent_dynamics = TRUE,
186+
save_cmdstan_config = TRUE,
187+
save_metric = TRUE
188+
)
189+
)
190+
paths <- c(
191+
fit$output_files(),
192+
fit$latent_dynamics_files(),
193+
fit$profile_files(),
194+
fit$config_files(),
195+
fit$metric_files()
196+
)
197+
expect_equal(file.exists(paths), rep(TRUE, length(paths)))
198+
expect_equal(
199+
normalizePath(dirname(paths)),
200+
rep(normalizePath(output_dir), length(paths))
201+
)
202+
expect_output(fit$cmdstan_summary(), "Inference for Stan model")
203+
expect_output(fit$cmdstan_diagnose(), "Processing complete")
204+
205+
# All generated file types should remain usable when moved by Windows R.
206+
save_root <- local_output_sandbox("wsl-save-files")
207+
save_dirs <- file.path(
208+
save_root,
209+
c("output", "diagnostic", "profile", "config", "metric")
210+
)
211+
for (dir in save_dirs) {
212+
dir.create(dir)
213+
}
214+
saved_paths <- suppressMessages(c(
215+
fit$save_output_files(save_dirs[1]),
216+
fit$save_latent_dynamics_files(save_dirs[2]),
217+
fit$save_profile_files(save_dirs[3]),
218+
fit$save_config_files(save_dirs[4]),
219+
fit$save_metric_files(save_dirs[5])
220+
))
221+
expect_equal(file.exists(saved_paths), rep(TRUE, length(saved_paths)))
222+
})
223+
224+
test_that("explicit WSL UNC output_dir remains supported", {
225+
skip_if_not(os_is_wsl())
226+
# This covers explicit output only; #1113's temporary input paths are separate.
227+
output_dir <- repair_path(file.path(wsl_dir_prefix(), wsl_tempdir()))
228+
withr::defer(unlink(output_dir, recursive = TRUE))
229+
fit <- testing_fit(
230+
"bernoulli",
231+
method = "optimize",
232+
output_dir = output_dir
233+
)
234+
235+
expect_equal(file.exists(fit$output_files()), TRUE)
236+
expect_equal(
237+
normalizePath(dirname(fit$output_files())),
238+
normalizePath(output_dir)
239+
)
72240
})
73241

74242
test_that("error if output_dir is invalid", {

0 commit comments

Comments
 (0)