Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
^\.covrignore$
^\.github$
^.*\.Rproj$
^paper$
^\_pkgdown.yml$
.clang-format
README.md
Expand Down
4 changes: 2 additions & 2 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ Imports:
methods,
tools
URL: https://CRAN.R-project.org/package=MCSimMod, https://github.com/USEPA/MCSimMod
License: GPL-3
License: GPL-3 + file LICENSE
Encoding: UTF-8
LazyData: true
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.3.2
RoxygenNote: 7.3.3
Suggests:
knitr,
rmarkdown,
Expand Down
32 changes: 26 additions & 6 deletions R/MCSim_model.R
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ Model <- setRefClass("Model",
#' @field paths List of character strings that are names of files associated with the model.
#' @field writeTemp Boolean specifying whether to write model files to a temporary directory. If value is TRUE, model files will be written to a temporary directory; if value is FALSE, model files will be written to the same directory that contains the model specification file.
#' @field verboseOutput Boolean specifying whether to write translator messages to standard output. If value is TRUE, messages will be written to standard output; if value is FALSE, messages will be written to files in a temporary directory.
#' @field recompiled Boolean specifying is model has been recompiled due to change in source file
Comment thread
tjzurlin marked this conversation as resolved.
Outdated
mName = "character", mString = "character", initParms = "function",
initStates = "function", Outputs = "ANY", parms = "numeric", Y0 = "numeric",
paths = "list", writeTemp = "logical", verboseOutput = "logical"
paths = "list", writeTemp = "logical", verboseOutput = "logical", recompiled = "logical"
),
methods = list(
initialize = function(...) {
Expand All @@ -53,20 +54,25 @@ Model <- setRefClass("Model",
}
file <- tempfile(pattern = "mcsimmod_", fileext = ".model")
writeLines(mString, file)
source_file <- normalizePath(file, winslash = "/")
file <- source_file
} else {
if (writeTemp == TRUE) {
source_file <- normalizePath(paste0(mName, ".model"))
source_file <- normalizePath(paste0(mName, ".model"), winslash = "/")
temp_directory <- tempdir()
file <- file.path(temp_directory, basename(source_file))
file_copied <- file.copy(from = source_file, to = file)
Comment thread
tjzurlin marked this conversation as resolved.
Outdated
file <- normalizePath(file, winslash = "/")
} else {
file <- normalizePath(paste0(mName, ".model"))
source_file <- normalizePath(paste0(mName, ".model"), winslash = "/")
file <- source_file
}
}
mList <- .fixPath(file)
sList <- .fixPath(source_file)
mName <<- mList$mName
mPath <- mList$mPath

sPath <- sList$mPath

paths <<- list(
dll_name = paste0(mName, "_model"),
Expand All @@ -75,17 +81,26 @@ Model <- setRefClass("Model",
dll_file = file.path(mPath, paste0(mName, "_model", .Platform$dynlib.ext)),
inits_file = file.path(mPath, paste0(mName, "_model_inits.R")),
model_file = file.path(mPath, paste0(mName, ".model")),
source_file = file.path(sPath, paste0(mName, ".model")),
hash_file = file.path(mPath, paste0(mName, "_model.md5"))
)

# Calculate and save initial hash during initialization
# This allows loadModel to immediately check if source has changed
if (!file.exists(paths$hash_file)) {
initial_hash <- as.character(tools::md5sum(paths$source_file))
write(initial_hash, file = paths$hash_file)
}
},
loadModel = function(force = FALSE) {
"Translate (if necessary) the model specification text to C, compile (if necessary) the resulting C file to create a dynamic link library (DLL) file (on Windows) or a shared object (SO) file (on Unix), and then load all essential information about the Model object into memory (for use in the current R session)."
hash_exists <- file.exists(paths$hash_file)
if (hash_exists) {
hash_has_changed <- .fileHasChanged(paths$model_file, paths$hash_file)
hash_has_changed <- .fileHasChanged(paths$source_file, paths$hash_file)
} else {
hash_has_changed <- TRUE
}
recompiled <<- FALSE

# Conditions for compiling a model:
# 1. The DLL (on Windows) or SO (on Unix) associated with the model
Expand All @@ -98,7 +113,12 @@ Model <- setRefClass("Model",
# specification file has been changed since the last translation and
# compiling.
if (!file.exists(paths$dll_file) | (force) | (!hash_exists) | (hash_exists & hash_has_changed)) {
compileModel(paths$model_file, paths$c_file, paths$dll_name, paths$dll_file, hash_file = paths$hash_file, verbose_output = verboseOutput)
# When using writeTemp=TRUE, ensure the model file is updated from source before compilation
if (writeTemp & (paths$source_file != paths$model_file)) {
file.copy(from = paths$source_file, to = paths$model_file, overwrite = TRUE)
}
compileModel(paths$model_file, paths$c_file, paths$dll_name, paths$dll_file, source_file = paths$source_file, hash_file = paths$hash_file, verbose_output = verboseOutput)
recompiled <<- TRUE
}

# Load the compiled model (DLL).
Expand Down
13 changes: 7 additions & 6 deletions R/compileModel.R
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@
#' @param c_file Name of a C source code file to be created by compiling the MCSim model specification file.
#' @param dll_name Name of a DLL or SO file without the extension (".dll" or ".so").
#' @param dll_file Name of the same DLL or SO file with the appropriate extension (".dll" or ".so").
#' @param hash_file Name of a file containing a hash key for determining if `model_file` has changed since the previous translation and compilation.
#' @param source_file Name of the original source file to use for hash calculation. Defaults to \code{model_file} for backward compatibility. When \code{writeTemp=TRUE} in \code{createModel()}, this should be set to the original source file path to ensure hash tracking works correctly when the source file is separate from the compiled model file.
#' @param hash_file Name of a file containing a hash key for determining if `source_file` has changed since the previous translation and compilation.
#' @param verbose_output Boolean specifying whether to write translator messages to standard output. If value is TRUE, messages will be written to standard output; if value is FALSE, messages will be written to files in a temporary directory.
#' @returns No return value. Creates files and saves them in locations specified by function arguments.
#' @import tools
#' @useDynLib MCSimMod, .registration=TRUE
#' @export
compileModel <- function(model_file, c_file, dll_name, dll_file, hash_file = NULL, verbose_output = FALSE) {
compileModel <- function(model_file, c_file, dll_name, dll_file, source_file = model_file, hash_file = NULL, verbose_output = FALSE) {
# Unload DLL if it has been loaded.
mList <- .fixPath(model_file)
model_name <- mList$mName
Expand Down Expand Up @@ -120,7 +121,7 @@ compileModel <- function(model_file, c_file, dll_name, dll_file, hash_file = NUL
"event",
"root"
)
for (idx in seq(length(item_to_replace))) {
for (idx in seq_along(item_to_replace)) {
lines <- gsub(
paste0("\\b", item_to_replace[idx], "\\b"),
paste0(item_to_replace[idx], "_", model_name),
Expand Down Expand Up @@ -151,7 +152,7 @@ compileModel <- function(model_file, c_file, dll_name, dll_file, hash_file = NUL
"initStates",
"initState"
)
for (idx in seq(length(item_to_replace))) {
for (idx in seq_along(item_to_replace)) {
lines <- gsub(
paste0("\\b", item_to_replace[idx], "\\b"),
paste0(item_to_replace[idx], "_", model_name),
Expand Down Expand Up @@ -181,10 +182,10 @@ compileModel <- function(model_file, c_file, dll_name, dll_file, hash_file = NUL
normalizePath(out_file), ".\n"
)

# If hash file name was provided, create a hash (md5 sum) for the model file
# If hash file name was provided, create a hash (md5 sum) for the source file
# and print a message about its location.
if (!is.null(hash_file)) {
file_hash <- as.character(md5sum(model_file))
file_hash <- as.character(tools::md5sum(source_file))
write(file_hash, file = hash_file)
message(
"Hash created and saved in the file ", normalizePath(hash_file),
Expand Down
6 changes: 3 additions & 3 deletions R/fileHasChanged.R
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
#-----------------
# compareHash
#----------------
# Private function to determine if the .model file has changed
# Private function to determine if the source .model file has changed

.fileHasChanged <- function(model_file, hash_file) {
.fileHasChanged <- function(source_file, hash_file) {
# Calculate hash for current model file
current_hash <- as.character(md5sum(model_file))
current_hash <- tools::md5sum(source_file)

# Read saved hash
saved_hash <- readLines(hash_file, n = 1)
Expand Down
3 changes: 0 additions & 3 deletions R/fixPath.R
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@
.fixPath <- function(file) {
new.mName <- strsplit(basename(file), "[.]")[[1]][1]
new.mPath <- dirname(file)
if (.Platform$OS.type == "windows") {
new.mPath <- gsub("\\\\", "/", utils::shortPathName(new.mPath))
}

has_space <- grepl(" ", new.mPath)
if (has_space == T) {
Expand Down
2 changes: 2 additions & 0 deletions man/Model-class.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion man/compileModel.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 47 additions & 0 deletions tests/testthat/test-compareHash.R
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,50 @@ testthat::test_that("test_compareHash", {

model$cleanup()
})

testthat::test_that("test_tmp_compileModel", {
# Test writeTemp=TRUE functionality with proper source/compilation file separation
# Create a proper source file in a permanent location (not temp)
source_dir <- file.path(tempdir(), "sourceDir")
dir.create(source_dir, showWarnings = FALSE)
mName <- file.path(source_dir, "test_model")
mString <- readLines(file.path(testthat::test_path(), "data", "exponential.model"))
writeLines(mString, paste0(mName, ".model"))

# Verify source file exists
testthat::expect_true(file.exists(paste0(mName, ".model")))

# Create model with writeTemp=TRUE - this should use the source file
# and create compilation files in temp directory
model <- createModel(mName, writeTemp = TRUE)

# Verify paths are properly separated
testthat::expect_true(model$paths$source_file != model$paths$model_file)
testthat::expect_true(file.exists(model$paths$source_file))
testthat::expect_true(file.exists(model$paths$model_file))

# First load - should compile
model$loadModel()
testthat::expect_true(file.exists(model$paths$hash_file)) # Check if hash was created
testthat::expect_true(model$recompiled) # Should be TRUE on first compile

# Second load - should NOT recompile (no changes)
model$loadModel()
testthat::expect_false(.fileHasChanged(model$paths$source_file, model$paths$hash_file))
testthat::expect_false(model$recompiled) # Should be FALSE, no recompile needed

# Edit source file (what users actually edit)
write("# File is edited", file = model$paths$source_file, append = TRUE, sep = "\n")
testthat::expect_true(.fileHasChanged(model$paths$source_file, model$paths$hash_file))

# Third load after edit - should recompile
model$loadModel()
testthat::expect_true(model$recompiled) # Should be TRUE, source changed

# Fourth load - should NOT recompile (our bug fix test!)
model$loadModel()
testthat::expect_false(.fileHasChanged(model$paths$source_file, model$paths$hash_file))
testthat::expect_false(model$recompiled) # Should be FALSE, hash should now match

model$cleanup()
})
2 changes: 1 addition & 1 deletion tests/testthat/test-sim.R
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ testthat::test_that("Model$absoluteModel", {
# Use absolute path of temp directory,
# Test to make sure changing the file returns a changed path

dir.create(file.path(tempdir(), "testDir"))
dir.create(file.path(tempdir(), "testDir"), showWarnings = FALSE)
mName <- tempfile(pattern = "mcsimmod_", tmpdir = file.path(tempdir(), "testDir"))
mString <- readLines(file.path(testthat::test_path(), "data", "exponential.model"))
writeLines(mString, paste0(mName, ".model"))
Expand Down