# Unit test for doRNG # # Author: Renaud Gaujoux # Creation: 28 Mar 2012 ############################################################################### context('dorng') #test.CMRGseed <- function(){ # # msg <- function(...) paste(.msg, ':', ...) # # # Unit tests # .msg <- "Call CMRGseed without argument" # rs <- .Random.seed # expect_identical( length(CMRGseed()), 7L, msg("Seed is of length 7") ) # expect_identical(rs, .Random.seed, msg("does not change .Random.seed")) # # .msg <- "Call CMRGseed with a single argument" # rs <- .Random.seed # expect_identical( length(CMRGseed(1)), 7L, msg("Seed is of length 7") ) # expect_identical(rs, .Random.seed, msg("does not change .Random.seed")) # expect_true( all(!is.na(CMRGseed(1))), msg("No NA in the returned seed") ) # #} checkRNG <- function(x, y, msg = NULL, ...){ expect_true(rng.equal(x, y), info = msg, ...) } # 1-length loop test_that("dorng1", { set.seed(1234) # needed to avoid weird behaviors in checks rng_seed <- RNGseq(n = 1, seed = 123, simplify = FALSE)[[1L]] set.seed(123) x <- foreach(i=1) %dorng% { runif(1) } y <- foreach(i=1, .options.RNG = 123) %dorng% { runif(1) } expect_identical(x, y) # check attributes on results result_attributes <- attributes(x) expect_true(setequal(names(result_attributes), c("rng", "doRNG_version")), info = "Result has all the expected attributes") expect_identical(result_attributes[["rng"]][[1L]], rng_seed, info = "Attribute 'rng' does not have the expected value") expect_identical(result_attributes[["doRNG_version"]], doRNGversion(), info = "Attribute 'doRNG_version' does not have the expected value") }) test_that("dorng", { test_dopar <- function(.msg, s.seq){ orng <- RNGseed() on.exit({ doRNGversion(NULL); RNGseed(orng); registerDoSEQ()} ) msg <- function(...) paste(.msg, ':', ...) noattr <- function(x){ attributes(x) <- NULL; x} # RNG restoration after %dorng% rng0 <- getRNG() foreach(i=1:4, .options.RNG = 123) %dorng% { runif(1) } checkRNG(rng0, msg = "RNG is restored after seeded %dorng%") # foreach(i=1:4) %dorng% { runif(1) } expect_identical(RNGtype(), RNGtype(rng0), info = "RNG kind is restored after unseeded %dorng%") # standard %dopar% loops are _not_ reproducible set.seed(1234) s1 <- foreach(i=1:4) %dopar% { runif(1) } set.seed(1234) s2 <- foreach(i=1:4) %dopar% { runif(1) } if( !missing(s.seq) ) expect_true( !identical(s1, s2), msg("Standard %dopar% loop is not reproducible")) # %dorng% loops ensure reproducibility local({ set.seed(1234) s1 <- foreach(i=1:4) %dorng% { runif(1) } runif(10) set.seed(1234) s2 <- foreach(i=1:4) %dorng% { runif(1) } expect_identical(s1, s2, msg("%dorng% loop is reproducible with set.seed")) # check RNG settings in result set.seed(1234) ref <- RNGseq(4) rngs <- attr(s1, 'rng') expect_true(!is.null(rngs), msg("Results contains RNG data")) expect_identical(rngs, ref, msg("Results contains whole sequence of RNG seeds")) }) # or local({ s1 <- foreach(i=1:4, .options.RNG=1234) %dorng% { runif(1) } s2 <- foreach(i=1:4, .options.RNG=1234) %dorng% { runif(1) } expect_identical(s1, s2, msg("%dorng% loop is reproducible with .options.RNG")) ref <- RNGseq(4, 1234) rngs <- attr(s1, 'rng') expect_true(!is.null(rngs), msg("Results of seed loop contains RNG data")) expect_identical(rngs, ref, msg("Results of seeded loop contains whole sequence of RNG seeds")) }) # check with unamed foreach arguments (issue #8) local({ on.exit( registerDoSEQ() ) registerDoRNG() set.seed(567) res <- foreach(a = 1:4, .combine = 'c') %dopar% {rnorm(1, mean = 0, sd = 1)} set.seed(567) res2 <- foreach(1:4, .combine = 'c') %dopar% {rnorm(1, mean = 0, sd = 1)} expect_identical(res, res2, info = "First argument named or unamed is equivalent") # set.seed(567) res <- foreach(a = 1:4, 1:2, .combine = 'c') %dopar% {rnorm(1, mean = 0, sd = 1)} set.seed(567) res2 <- foreach(1:4, 1:2, .combine = 'c') %dopar% {rnorm(1, mean = 0, sd = 1)} expect_identical(res, res2, info = "First argument named or unamed, with second unamed argument is equivalent") }) ## ## check extra arguments to .options.RNG # Normal RNG parameter is taken into account s.unif.noNk <- foreach(i=1:4, .options.RNG=1234) %dorng% { runif(5) } s.unif.wNk <- foreach(i=1:4, .options.RNG=list(1234, normal.kind="Ahrens-Dieter")) %dorng% { runif(5) } expect_identical(noattr(s.unif.noNk), noattr(s.unif.wNk) , msg("%dorng% loop (runif) with normal.kind in .options.RNG is identical as without")) s.norm.noNk <- foreach(i=1:4, .options.RNG=1234) %dorng% { rnorm(5) } s.norm.wNk <- foreach(i=1:4, .options.RNG=list(1234, normal.kind="Ahrens-Dieter")) %dorng% { rnorm(5) } expect_true(!isTRUE(all.equal(noattr(s.norm.noNk), noattr(s.norm.wNk))) , msg("%dorng% loop (rnorm) with normal.kind in .options.RNG is different as without")) # reproduce previous loop runif(10) s1 <- foreach(i=1:4) %dorng% { runif(5) } s2 <- foreach(i=1:4, .options.RNG=s1) %dorng% { runif(5) } expect_identical(s1, s2, "Seeding using .options.RNG={result from other loop} give identical results") # directly set sequence of seeds sL <- list(c(407,1:6), c(407,11:16), c(407,21:26), c(407, 31:36)) s1.L <- foreach(i=1:4, .options.RNG=sL) %dorng% { runif(5) } runif(10) s2.L <- foreach(i=1:4, .options.RNG=sL) %dorng% { runif(5) } expect_identical(s1.L, s2.L, "Seeding using .options.RNG=list twice give identical results") # directly set sequence of seeds as a matrix sM <- sapply(sL, identity) s1.M <- foreach(i=1:4, .options.RNG=sM) %dorng% { runif(5) } runif(10) s2.M <- foreach(i=1:4, .options.RNG=sM) %dorng% { runif(5) } expect_identical(s1.M, s2.M, "Seeding using .options.RNG=matrix twice give identical results") expect_identical(s1.M, s1.L, "Seeding using .options.RNG=matrix gives identical results as the same seed in list") # separate %dorng% loops are different set.seed(1234) rs1 <- .Random.seed s1 <- foreach(i=1:4) %dorng% { runif(1) } rs1_2 <- .Random.seed s2 <- foreach(i=1:4) %dorng% { runif(1) } expect_true( !identical(rs1, rs1_2), msg("unseed %dorng% loop changes .Random.seed")) expect_true( !identical(s1, s2), msg("two consecutive (unseeded) %dorng% loops are not identical")) expect_true( !identical(unlist(s1), unlist(s2)), msg("two consecutive (unseeded) %dorng% loops are not identical (values)")) # But the whole sequence of loops is reproducible set.seed(1234) s1.2 <- foreach(i=1:4) %dorng% { runif(1) } s2.2 <- foreach(i=1:4) %dorng% { runif(1) } expect_true( identical(s1, s1.2) && identical(s2, s2.2), msg("set.seed + two consecutive %dorng% loops are reproducible")) s <- list(s1, s2) if( !missing(s.seq) ) expect_identical(s, s.seq, msg("result is identical to sequential computation")) # check behaviour with set.seed set.seed(789) s1 <- foreach(i=1:6) %dorng%{ runif(1) } s2 <- foreach(i=1:6, .options.RNG=789) %dorng%{ runif(1) } expect_identical(s1, s2, "set.seed before %dorng% is identical to using .options.RNG") # current RNG is CRMG set.seed(789, "L'Ec") s1 <- foreach(i=1:6) %dorng%{ runif(1) } s2 <- foreach(i=1:6, .options.RNG=789) %dorng%{ runif(1) } expect_identical(s1, s2, "set.seed before %dorng% is identical to using .options.RNG, if current RNG is CRMG") s } orng <- RNGseed() on.exit({ doRNGversion(NULL); RNGseed(orng); registerDoSEQ()} ) testthat::skip_if_not_installed("doParallel") # Sequential computation registerDoSEQ() s.seq <- test_dopar("Sequential") # Multicore cluster if( .Platform$OS.type != 'windows'){ # Note: for some reason, running this test in RStudio fails when checking that the standard # %dopar% loop is not reproducible doParallel::registerDoParallel(cores=2) s <- test_dopar("Multicore", s.seq) } # SNOW-like cluster local({ cl <- NULL on.exit({ if (!is.null(cl)) parallel::stopCluster(cl) }, add = TRUE) cl <- parallel::makeCluster(2) doParallel::registerDoParallel(cl) test_dopar("SNOW-like cluster", s.seq) }) skip("doMPI test because doMPI::startMPIcluster hangs inexplicably") # Works with doMPI if (requireNamespace("doMPI", quietly = TRUE)) { cl_mpi <- NULL on.exit({ if (!is.null(cl_mpi)) closeCluster(cl_mpi) }, add = TRUE) cl_mpi <- doMPI::startMPIcluster(2) doMPI::registerDoMPI(cl_mpi) test_dopar("MPI cluster", s.seq) closeCluster(cl_mpi); cl_mpi <- NULL } }) test_that("registerDoRNG", { orng <- RNGseed() on.exit({ doRNGversion(NULL); RNGseed(orng); registerDoSEQ()} ) # RNG restoration after %dorng% over doSEQ registerDoSEQ() registerDoRNG() set.seed(123) rng0 <- getRNG() res1 <- foreach(i=1:4) %dorng% { runif(1) } expect_identical(RNGtype(), RNGtype(rng0), "RNG kind is restored after unseeded %dorng%") set.seed(123) res2 <- foreach(i=1:4) %dorng% { runif(1) } expect_identical(res1, res2, "%dorng% loop over doSEQ are reproducible") testthat::skip_if_not_installed("doParallel") cl <- NULL on.exit({ if (!is.null(cl)) parallel::stopCluster(cl) }, add = TRUE) cl <- parallel::makeCluster(2) doParallel::registerDoParallel(cl) # One can make existing %dopar% loops reproducible using %dorng% loops or registerDoRNG set.seed(1234) r1 <- foreach(i=1:4) %dorng% { runif(1) } registerDoRNG() set.seed(1234) r2 <- foreach(i=1:4) %dopar% { runif(1) } expect_identical(r1, r2, "registerDoRNG + set.seed: makes a %dopar% loop behave like a set.seed + %dorng% loop") # Registering another foreach backend disables doRNG local({ cl2 <- NULL on.exit({ foreach::registerDoSEQ() if (!is.null(cl2)) parallel::stopCluster(cl2) }, add = TRUE) cl2 <- parallel::makeCluster(2) doParallel::registerDoParallel(cl2) set.seed(1234) s1 <- foreach(i=1:4) %dopar% { runif(1) } set.seed(1234) s2 <- foreach(i=1:4) %dorng% { runif(1) } expect_true(!identical(s1, s2), "Registering another foreach backend disables doRNG") }) # doRNG is re-nabled by re-registering it registerDoRNG() set.seed(1234) r3 <- foreach(i=1:4) %dopar% { runif(1) } expect_identical(r2, r3, "doRNG is re-nabled by re-registering it") r4 <- foreach(i=1:4) %dopar% { runif(1) } # NB: the results are identical independently of the task scheduling # (r2 used 2 nodes, while r3 used 3 nodes) # Reproducibility of sequences of loops # pass seed to registerDoRNG runif(10) registerDoRNG(1234) r1 <- foreach(i=1:4) %dopar% { runif(1) } r2 <- foreach(i=1:4) %dopar% { runif(1) } registerDoRNG(1234) r3 <- foreach(i=1:4) %dopar% { runif(1) } r4 <- foreach(i=1:4) %dopar% { runif(1) } expect_identical(r3, r1, "registerDoRNG(1234) allow reproducing sequences of %dopar% loops (1)") expect_identical(r4, r2, "registerDoRNG(1234) allow reproducing sequences of %dopar% loops (2)") # use set.seed runif(10) registerDoRNG() set.seed(1234) s1 <- foreach(i=1:4) %dopar% { runif(1) } s2 <- foreach(i=1:4) %dopar% { runif(1) } set.seed(1234) s3 <- foreach(i=1:4) %dopar% { runif(1) } s4 <- foreach(i=1:4) %dopar% { runif(1) } expect_identical(s3, s1, "registerDoRNG + set.seed(1234) allow reproducing sequences of %dopar% loops (1)") expect_identical(s4, s2, "registerDoRNG + set.seed(1234) allow reproducing sequences of %dopar% loops (2)") runif(5) registerDoRNG() set.seed(1234) s5 <- foreach(i=1:4) %dopar% { runif(1) } s6 <- foreach(i=1:4) %dopar% { runif(1) } expect_identical(s5, r3, "registerDoRNG() + set.seed give same results as registerDoRNG(1234) (1)") expect_identical(s6, r4, "registerDoRNG() + set.seed give same results as registerDoRNG(1234) (2)") # argument `once=FALSE` reseed doRNG's seed at the beginning of each loop registerDoRNG(1234, once=FALSE) r1 <- foreach(i=1:4) %dopar% { runif(1) } r2 <- foreach(i=1:4) %dopar% { runif(1) } r3 <- foreach(i=1:4, .options.RNG=1234) %dopar% { runif(1) } expect_identical(r1, r2, "argument `once=FALSE` reseed doRNG's seed at the beginning of each loop") expect_identical(r1, r3, "argument `once=FALSE` reseed %dorng% loop as .options.RNG") # Once doRNG is registered the seed can also be passed as an option to %dopar% r1.2 <- foreach(i=1:4, .options.RNG=456) %dopar% { runif(1) } r2.2 <- foreach(i=1:4, .options.RNG=456) %dopar% { runif(1) } expect_identical(r1.2, r2.2, "Once doRNG is registered the seed can also be passed as an option to %dopar%") expect_true(!identical(r1.2, r1), "The seed passed as an option is really taken into account") }) # Test the use-case discussed in https://github.com/renozao/doRNG/issues/12 # Note: when run under RStudio, this test_that("Initial RNG state is properly handled", { # write script that loads the package being tested .run_test_script <- function(version){ pkg_path <- path.package("doRNG") lib_path <- dirname(pkg_path) # determine if the package is a development or installed package if( dir.exists(file.path(pkg_path, "Meta")) ) load_cmd <- sprintf("library(doRNG, lib = '%s')", lib_path) else load_cmd <- sprintf("devtools::load_all('%s')", pkg_path) # results are saved in a temporary .rds file (substitute backslashes with forward slash for Windows) tmp_res <- gsub("\\", "/", tempfile("rscript_res_", fileext = ".rds"), fixed = TRUE) on.exit( unlink(tmp_res) ) r_code <- paste0(collapse = "; ", c(load_cmd, if( !is.null(version) ) sprintf("doRNGversion('%s')", version), "r1 <- foreach(i=1:4, .options.RNG=1234) %dorng% { runif(1) }", "r2 <- foreach(i=1:4, .options.RNG=1234) %dorng% { runif(1) }", "r3 <- foreach(i=1:4, .options.RNG=1234) %dorng% { runif(1) }", sprintf("saveRDS(list(r1 = r1, r2 = r2, r3 = r3), '%s')", tmp_res) ) ) # run code in an independent fresh session rscript <- file.path(R.home("bin"), "Rscript") system(sprintf('"%s" -e "%s"', rscript, r_code), ignore.stdout = TRUE, ignore.stderr = TRUE) # load result readRDS(tmp_res) } # pre-1.7.4: results are not-reproducible res <- .run_test_script("1.7.3") expect_true(is.list(res) && identical(names(res), paste0("r", 1:3))) expect_true(all(sapply(res, attr, 'doRNG_version') == "1.7.3"), info = "doRNG version is correctly stored") expect_true(!identical(res[["r1"]], res[["r2"]]), info = "Version pre-1.7.3: results 1 & 2 are not identical") expect_identical(res[["r3"]], res[["r2"]], info = "Version pre-1.7.3: results 2 & 3 are identical") # post-1.7.4: results are reproducible res <- .run_test_script("1.7.4") expect_true(is.list(res) && identical(names(res), paste0("r", 1:3))) expect_true(all(sapply(res, attr, 'doRNG_version') == "1.7.4"), info = "doRNG version is correctly stored") expect_identical(res[["r1"]], res[["r2"]], info = "Version 1.7.4: results 1 & 2 are identical") expect_identical(res[["r3"]], res[["r2"]], info = "Version 1.7.4: results 3 & 3 are identical") # current version: results are reproducible res <- .run_test_script(NULL) expect_true(is.list(res) && identical(names(res), paste0("r", 1:3))) expect_true(all(sapply(res, attr, 'doRNG_version') == doRNGversion()), info = "doRNG version is correctly stored") expect_identical(res[["r1"]], res[["r2"]], info = "Current version: results 1 & 2 are identical") expect_identical(res[["r3"]], res[["r2"]], info = "Current version: results 2 & 3 are identical") }) test_that("RNG warnings", { .local <- function(){ orng <- RNGseed() oo <- options() on.exit({ options(oo); doRNGversion(NULL); RNGseed(orng); registerDoSEQ()} ) registerDoSEQ() registerDoRNG() expect_warning(y <- foreach(x = 1:2) %dorng% { rnorm(1); x }, NA) options(doRNG.rng_change_warning_force = TRUE) expect_warning(y <- foreach(x = 1:2) %dorng% { rnorm(1); x }, "Foreach loop \\(doSEQ\\) .* changed .* RNG type") options(doRNG.rng_change_warning_force = NULL) testthat::skip_if_not_installed("doParallel") local({ cl <- NULL on.exit({ if (!is.null(cl)) parallel::stopCluster(cl) }, add = TRUE) cl <- parallel::makeCluster(2) doParallel::registerDoParallel(cl) options(doRNG.rng_change_warning_force = TRUE) options(doRNG.rng_change_warning_skip = "doParallelSNOW") expect_warning(y <- foreach(x = 1:2) %dorng% { rnorm(1); x }, "Foreach loop \\(doParallelSNOW\\) .* changed .* RNG type") options(doRNG.rng_change_warning_force = NULL) expect_warning(y <- foreach(x = 1:2) %dorng% { rnorm(1); x }, NA) }) options(doRNG.rng_change_warning_skip = NULL) } .local() })