@@ -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+
799test_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
74242test_that(" error if output_dir is invalid" , {
0 commit comments