Skip to content

Commit 28be36d

Browse files
committed
Simplify compilation state tests
1 parent 5a497d6 commit 28be36d

9 files changed

Lines changed: 99 additions & 453 deletions

R/model.R

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -908,7 +908,6 @@ compile <- function(quiet = TRUE,
908908

909909
# Clear functions in place to preserve existing references. Because the public
910910
# field can be replaced, verify it is a mutable environment before installing.
911-
# Locked bindings are okay because rm() can remove them.
912911
if (!is.environment(self$functions) ||
913912
environmentIsLocked(self$functions)) {
914913
stop(

tests/testthat/helper-mock-cli.R

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
real_wcr <- wsl_compatible_run
22

3-
# Distinct contents for every mocked build in the session. A mock that wrote the
4-
# same empty file each time could not tell "the new artifact was installed" from
5-
# "the old one was left in place", which is the invariant most of these tests
6-
# exist to check.
3+
# Use distinct contents so tests can tell successive builds apart.
74
mock_exe_contents <- local({
85
n <- 0L
96
function() {
@@ -18,20 +15,14 @@ with_mocked_cli <- function(code, compile_ret, info_ret) {
1815
local_mocked_bindings(
1916
wsl_compatible_run = function(command, args, ...) {
2017
if (
21-
# make_cmd() rather than "make": production honours $MAKE, so a literal
22-
# comparison lets the mock be bypassed and the real command run.
18+
# Match the configured make command.
2319
!is.null(command)
2420
&& command == make_cmd()
2521
&& !is.null(args)
2622
&& startsWith(basename(args[1]), "model-")
2723
) {
2824
message("mock-compile-was-called")
29-
# Real `make` writes the executable named by args[1] when it succeeds and
30-
# writes nothing when it fails. Without this, code that installs the
31-
# compiled artifact silently has nothing to install. `isTRUE()` because
32-
# callers may pass a `compile_ret` with no status at all.
33-
# Executable mode as well, so that installation losing it is something
34-
# the tests can notice rather than something the mock never modelled.
25+
# Successful builds create an executable artifact, just like make.
3526
if (isTRUE(compile_ret$status == 0)) {
3627
mock_exe <- wsl_safe_path(args[1], revert = TRUE)
3728
writeLines(mock_exe_contents(), mock_exe)

tests/testthat/test-model-code-print.R

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,7 @@ test_that("code() doesn't change when file changes (unless recompiled or recreat
5959
expect_identical(mod$code(), code_2_answer)
6060
expect_identical(utils::capture.output(mod$print()), code_2_answer)
6161

62-
# overwrite with the original code, mod$code() shouldn't change until the
63-
# model is successfully recompiled (#1228)
62+
# Recompilation refreshes the cached code (#1228).
6463
writeLines(code_1_answer, stan_file_1)
6564
expect_identical(mod$code(), code_2_answer)
6665
mod$compile()

tests/testthat/test-model-compile-user_header.R

Lines changed: 32 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@ local_mocked_stanc <- function(.local_envir = parent.frame()) {
66
)
77
}
88

9-
# A mocked compile installs an executable, so anything that compiles for real
10-
# (even with a mocked compiler) works on a temporary copy rather than writing
11-
# into the package's test resources.
9+
# Mocked compiles use temporary model copies to protect test resources.
1210
local_external_model <- function(.local_envir = parent.frame()) {
1311
stan_file <- file.path(
1412
withr::local_tempdir(.local_envir = .local_envir),
@@ -26,9 +24,7 @@ user_header_routes <- function(header) {
2624
)
2725
}
2826

29-
# This test is deliberately placed above the file-level skip_if(os_is_macos())
30-
# below: it mocks the stanc call and never compiles, so it needs no toolchain
31-
# and should run on every platform.
27+
# Keep mocked compilation tests above the toolchain skip below.
3228
test_that("cpp_options user headers allow undefined functions", {
3329
stan_file <- testing_stan_file("bernoulli_external")
3430
user_header <- withr::local_tempfile(lines = "", fileext = ".hpp")
@@ -61,8 +57,6 @@ test_that("cpp_options user headers allow undefined functions", {
6157
)
6258
})
6359

64-
# Also above the file-level skip_if() below: the compiler is mocked, so these
65-
# need no toolchain either.
6660
test_that("compile() reuses the user header from the previous compilation", {
6761
stan_file <- file.path(withr::local_tempdir(), "bernoulli_external.stan")
6862
file.copy(testing_stan_file("bernoulli_external"), stan_file)
@@ -128,8 +122,6 @@ test_that("a no-op compile preserves a header supplied via cpp_options", {
128122
wsl_safe_path(absolute_path(user_header))
129123
)
130124

131-
# The executable is up to date, so this call compiles nothing and must leave
132-
# the options describing it alone.
133125
with_mocked_cli(
134126
compile_ret = list(status = 0),
135127
info_ret = list(status = 1),
@@ -157,8 +149,7 @@ test_that("compile() uses a user header supplied to cmdstan_model()", {
157149
user_header = user_header,
158150
compile = FALSE
159151
)
160-
# A mocked compile rather than a dry run: a dry run builds nothing, so it
161-
# records nothing about a compiled artifact.
152+
# Use a successful compile so its options are recorded.
162153
with_mocked_cli(
163154
compile_ret = list(status = 0),
164155
info_ret = list(status = 1),
@@ -192,12 +183,8 @@ test_that("a header configured over a current executable does not rebuild", {
192183
Sys.setFileTime(header, Sys.time() - 60)
193184
Sys.setFileTime(exe, Sys.time())
194185

195-
# Nothing records which header an executable was built with -- the binary
196-
# cannot report it and nothing is written alongside it -- so a fresh object
197-
# cannot tell a header it was configured with from the one already compiled
198-
# in. Rebuilding on the possibility would recompile in every new R session,
199-
# so the up-to-date executable is kept and $cpp_options() does not claim a
200-
# header it cannot vouch for. Documented under `force_recompile`.
186+
# A fresh object cannot know which header built an existing executable, so it
187+
# keeps the executable without recording the requested header.
201188
for (route in user_header_routes(header)) {
202189
model <- do.call(
203190
cmdstan_model,
@@ -210,8 +197,7 @@ test_that("a header configured over a current executable does not rebuild", {
210197
)
211198
expect_null(model$cpp_options()[["USER_HEADER"]])
212199
expect_null(model$cpp_options()[["user_header"]])
213-
# Source configuration is a separate axis and still reflects the request:
214-
# it is what makes stanc accept the undefined functions the header defines.
200+
# Stanc still uses the configured header.
215201
expect_true(model$.__enclos_env__$private$using_user_header_)
216202
}
217203
})
@@ -303,8 +289,6 @@ test_that("a bare retry after a failed compile keeps the newly supplied header",
303289
expect_equal(private$user_header_, resolve_path(h1))
304290
expect_false(private$user_header_dirty_)
305291

306-
# The usual route to this is a bug in h2 itself, so the header the user just
307-
# supplied has to survive the failure.
308292
with_mocked_cli(
309293
compile_ret = list(status = 1),
310294
info_ret = list(status = 1),
@@ -313,8 +297,7 @@ test_that("a bare retry after a failed compile keeps the newly supplied header",
313297
expect_equal(private$user_header_, resolve_path(h2))
314298
expect_true(private$user_header_dirty_)
315299

316-
# A bare retry must build h2 rather than reverting to h1 or no-op'ing: the
317-
# reuse branch resolves back to h2, so nothing here looks like a change.
300+
# A bare retry must build h2 rather than reverting to h1.
318301
with_mocked_cli(
319302
compile_ret = list(status = 0),
320303
info_ret = list(status = 1),
@@ -369,8 +352,7 @@ test_that("user_header = NULL clears a header from every supply route", {
369352
)
370353
expect_true(private$using_user_header_)
371354

372-
# The executable is up to date but was built against a header the model no
373-
# longer uses, so clearing has to force a rebuild rather than no-op.
355+
# Clearing a compiled header must force a rebuild.
374356
with_mocked_cli(
375357
compile_ret = list(status = 0),
376358
info_ret = list(status = 1),
@@ -387,11 +369,7 @@ test_that("duplicate headers of one spelling take the last, as make does", {
387369
first <- withr::local_tempfile(lines = "", fileext = ".hpp")
388370
second <- withr::local_tempfile(lines = "", fileext = ".hpp")
389371

390-
# Every duplicate reaches make and a makefile takes the last, which is what
391-
# the cpp_options parser implements. Reading with [["USER_HEADER"]] took the
392-
# first instead, so the model compiled against a header make would not have
393-
# used -- and removing by name dropped only one occurrence, leaving the other
394-
# to reach make alongside the header selected here.
372+
# Use the last duplicate and remove every header entry before calling make.
395373
for (spelling in c("USER_HEADER", "user_header")) {
396374
duplicated <- structure(
397375
list(first, second),
@@ -402,8 +380,7 @@ test_that("duplicate headers of one spelling take the last, as make does", {
402380
expect_length(resolved$cpp_options, 0)
403381
}
404382

405-
# Across spellings, the last of each is what make would have seen, and
406-
# precedence still picks USER_HEADER. Neither survives the strip.
383+
# USER_HEADER still takes precedence across spellings.
407384
mixed <- structure(
408385
list(first, second, first),
409386
names = c("user_header", "USER_HEADER", "user_header")
@@ -417,11 +394,7 @@ test_that("a NULL header entry clears a persisted one rather than being ignored"
417394
persisted <- withr::local_tempfile(lines = "", fileext = ".hpp")
418395
first <- withr::local_tempfile(lines = "", fileext = ".hpp")
419396

420-
# A NULL entry stands for an explicit `USER_HEADER=`, which make takes as
421-
# clearing whatever came before it. Reading presence off the value made NULL
422-
# indistinguishable from absence, so the persisted header was carried forward
423-
# and the object described a header the build did not use. Every shape below
424-
# went wrong the same way; only the trailing-duplicate one was reported.
397+
# A NULL entry emits USER_HEADER= and clears any previous header.
425398
for (spelling in c("USER_HEADER", "user_header")) {
426399
single <- structure(list(NULL), names = spelling)
427400
resolved <- resolve_user_header(NULL, FALSE, single, previous = persisted)
@@ -599,64 +572,26 @@ test_that("cmdstan_model works with user_header with mock", {
599572
test_that("wsl path conversion is done as expected", {
600573
tmp_file <- withr::local_tempfile(lines = hpp, fileext = ".hpp")
601574
local_mocked_stanc()
602-
# Mocked successful compiles rather than dry runs: only a compilation that
603-
# produced an executable records the options describing it.
604575

605-
# Case 1: arg
606-
with_mocked_cli(
607-
compile_ret = list(status = 0),
608-
info_ret = list(status = 1),
609-
code = {
610-
mod <- cmdstan_model(
611-
stan_file = local_external_model(),
612-
user_header = tmp_file
613-
)
614-
}
615-
)
616-
617-
# USER_HEADER is converted
618-
# user_header is NULL
619-
expect_equal(mod$cpp_options()[['USER_HEADER']], w_path(tmp_file))
620-
expect_true(is.null(mod$cpp_options()[['user_header']]))
621-
622-
# Case 2: cpp opt USER_HEADER
623-
with_mocked_cli(
624-
compile_ret = list(status = 0),
625-
info_ret = list(status = 1),
626-
code = {
627-
mod <- cmdstan_model(
628-
stan_file = local_external_model(),
629-
cpp_options = list(
630-
USER_HEADER = tmp_file
631-
)
632-
)
633-
}
634-
)
635-
636-
# USER_HEADER is converted
637-
# user_header is unconverted
638-
expect_equal(mod$cpp_options()[['USER_HEADER']], w_path(tmp_file))
639-
expect_true(is.null(mod$cpp_options()[['user_header']]))
640-
641-
# Case # 3: only user_header opt
642-
with_mocked_cli(
643-
compile_ret = list(status = 0),
644-
info_ret = list(status = 1),
645-
code = {
646-
mod <- cmdstan_model(
647-
stan_file = local_external_model(),
648-
cpp_options = list(
649-
user_header = tmp_file
576+
routes <- user_header_routes(tmp_file)
577+
expected_names <- c("USER_HEADER", "USER_HEADER", "user_header")
578+
for (i in seq_along(routes)) {
579+
with_mocked_cli(
580+
compile_ret = list(status = 0),
581+
info_ret = list(status = 1),
582+
code = {
583+
mod <- do.call(
584+
cmdstan_model,
585+
c(list(stan_file = local_external_model()), routes[[i]])
650586
)
651-
)
652-
}
653-
)
654-
587+
}
588+
)
655589

656-
# In other cases, in the *output* USER_HEADER is windows style user_header is not.
657-
# In this case, USER_HEADER is null.
658-
expect_true(is.null(mod$cpp_options()[['USER_HEADER']]))
659-
expect_equal(mod$cpp_options()[['user_header']], w_path(tmp_file))
590+
expected_name <- expected_names[[i]]
591+
other_name <- setdiff(c("USER_HEADER", "user_header"), expected_name)
592+
expect_equal(mod$cpp_options()[[expected_name]], w_path(tmp_file))
593+
expect_null(mod$cpp_options()[[other_name]])
594+
}
660595
})
661596

662597
test_that("user_header precedence order is correct", {
@@ -667,12 +602,9 @@ test_that("user_header precedence order is correct", {
667602
))
668603

669604
local_mocked_stanc()
670-
# Asserted after a mocked successful compile rather than a dry run: only a
671-
# compilation that produced an executable records the options describing it.
672-
# The ignored spelling is dropped in every case, so the next compile has a
673-
# single source for the header.
605+
# Successful compiles record the selected header and drop ignored spellings.
674606

675-
# Case # 1: all 3 specified
607+
# The explicit argument wins.
676608
mod <- cmdstan_model(local_external_model(), compile = FALSE)
677609
with_mocked_cli(
678610
compile_ret = list(status = 0),
@@ -688,16 +620,13 @@ test_that("user_header precedence order is correct", {
688620
)
689621
}, "User header specified both")
690622
)
691-
# In this case:
692-
# cpp_options[['USER_HEADER']] == tmp_files[1] <- actually used
693-
# tmp_files[2] and tmp_files[3] are not stored
694623
expect_equal(
695624
match(!!(mod$cpp_options()[['USER_HEADER']]), w_path(tmp_files)),
696625
1
697626
)
698627
expect_null(mod$cpp_options()[['user_header']])
699628

700-
# Case # 2: Both opts, but no arg
629+
# USER_HEADER wins over user_header.
701630
mod <- cmdstan_model(local_external_model(), compile = FALSE)
702631
with_mocked_cli(
703632
compile_ret = list(status = 0),
@@ -712,16 +641,13 @@ test_that("user_header precedence order is correct", {
712641
)
713642
}, "User header specified both")
714643
)
715-
# In this case:
716-
# cpp_options[['USER_HEADER']] == tmp_files[2] <- actually used
717-
# tmp_files[3] is not stored
718644
expect_equal(
719645
match(!!(mod$cpp_options()[['USER_HEADER']]), w_path(tmp_files)),
720646
2
721647
)
722648
expect_null(mod$cpp_options()[['user_header']])
723649

724-
# Case # 3: Both opts, other order
650+
# Option order does not change precedence.
725651
mod <- cmdstan_model(local_external_model(), compile = FALSE)
726652
with_mocked_cli(
727653
compile_ret = list(status = 0),
@@ -736,7 +662,6 @@ test_that("user_header precedence order is correct", {
736662
)
737663
}, "User header specified both")
738664
)
739-
# Same as Case #2: USER_HEADER wins whichever order the two appear in
740665
expect_equal(
741666
match(!!(mod$cpp_options()[['USER_HEADER']]), w_path(tmp_files)),
742667
2

0 commit comments

Comments
 (0)