@@ -105,6 +105,163 @@ test_that("packrat warns when lockfile hash does not match installed hash", {
105105 )
106106})
107107
108+ test_that(" moveInstalledPackageToCache caches a fresh package" , {
109+ skip_on_os(" windows" )
110+
111+ packagePath <- makeTestPackage(" oatmeal" )
112+ cacheDir <- tempfile(" packrat-cache-" )
113+ on.exit(
114+ unlink(c(dirname(packagePath ), cacheDir ), recursive = TRUE ),
115+ add = TRUE
116+ )
117+
118+ hash <- strrep(" a" , 32 )
119+ cachedPackagePath <- file.path(cacheDir , " oatmeal" , hash , " oatmeal" )
120+
121+ result <- moveInstalledPackageToCache(packagePath , hash , cacheDir = cacheDir )
122+
123+ expect_identical(result , cachedPackagePath )
124+ expect_true(is.symlink(packagePath ))
125+ expect_true(file.exists(file.path(cachedPackagePath , " DESCRIPTION" )))
126+ })
127+
128+ test_that(" moveInstalledPackageToCache adopts a competing process's copy" , {
129+ skip_on_os(" windows" )
130+
131+ packagePath <- makeTestPackage(" oatmeal" )
132+ cacheDir <- tempfile(" packrat-cache-" )
133+ on.exit(
134+ unlink(c(dirname(packagePath ), cacheDir ), recursive = TRUE ),
135+ add = TRUE
136+ )
137+
138+ hash <- strrep(" a" , 32 )
139+ cachedPackagePath <- file.path(cacheDir , " oatmeal" , hash , " oatmeal" )
140+
141+ # Simulate losing the race to populate the cache: renames into the final
142+ # cache location fail (as when a competing process created the destination
143+ # first), and the competitor's byte-identical copy appears between the
144+ # file.exists() check and the second rename.
145+ realRename <- base :: file.rename
146+ renamesToCache <- 0
147+ local_mocked_bindings(
148+ file.rename = function (from , to ) {
149+ if (! identical(to , cachedPackagePath )) {
150+ return (realRename(from , to ))
151+ }
152+ renamesToCache <<- renamesToCache + 1
153+ if (renamesToCache == 2 ) {
154+ dir.create(cachedPackagePath , recursive = TRUE )
155+ writeLines(
156+ c(" Package: oatmeal" , " Version: 1.0" ),
157+ file.path(cachedPackagePath , " DESCRIPTION" )
158+ )
159+ }
160+ FALSE
161+ },
162+ .package = " base"
163+ )
164+
165+ result <- moveInstalledPackageToCache(packagePath , hash , cacheDir = cacheDir )
166+
167+ expect_identical(result , cachedPackagePath )
168+ expect_true(is.symlink(packagePath ))
169+ expect_true(file.exists(file.path(packagePath , " DESCRIPTION" )))
170+ })
171+
172+ test_that(" moveInstalledPackageToCache reports a fresh-package cache failure" , {
173+ skip_on_os(" windows" )
174+
175+ packagePath <- makeTestPackage(" oatmeal" )
176+ cacheDir <- tempfile(" packrat-cache-" )
177+ on.exit(
178+ unlink(c(dirname(packagePath ), cacheDir ), recursive = TRUE ),
179+ add = TRUE
180+ )
181+
182+ hash <- strrep(" a" , 32 )
183+ cachedPackagePath <- file.path(cacheDir , " oatmeal" , hash , " oatmeal" )
184+
185+ # All renames into the final cache location fail and no competing process
186+ # populates the cache. With no pre-existing cache entry there is nothing to
187+ # roll back, so the error should report the copy failure rather than a
188+ # bogus "package may be lost from cache".
189+ realRename <- base :: file.rename
190+ local_mocked_bindings(
191+ file.rename = function (from , to ) {
192+ if (! identical(to , cachedPackagePath )) {
193+ return (realRename(from , to ))
194+ }
195+ FALSE
196+ },
197+ .package = " base"
198+ )
199+
200+ expect_error(
201+ moveInstalledPackageToCache(packagePath , hash , cacheDir = cacheDir ),
202+ " failed to copy package 'oatmeal' to cache"
203+ )
204+
205+ # the original installation is untouched
206+ expect_false(is.symlink(packagePath ))
207+ expect_true(file.exists(file.path(packagePath , " DESCRIPTION" )))
208+ })
209+
210+ test_that(" moveInstalledPackageToCache restores a backed-up cache entry" , {
211+ skip_on_os(" windows" )
212+
213+ packagePath <- makeTestPackage(" oatmeal" , version = " 2.0" )
214+ cacheDir <- tempfile(" packrat-cache-" )
215+ on.exit(
216+ unlink(c(dirname(packagePath ), cacheDir ), recursive = TRUE ),
217+ add = TRUE
218+ )
219+
220+ hash <- strrep(" a" , 32 )
221+ cachedPackagePath <- file.path(cacheDir , " oatmeal" , hash , " oatmeal" )
222+
223+ # a pre-existing cache entry that overwrite = TRUE will back up
224+ dir.create(cachedPackagePath , recursive = TRUE )
225+ writeLines(
226+ c(" Package: oatmeal" , " Version: 1.0" ),
227+ file.path(cachedPackagePath , " DESCRIPTION" )
228+ )
229+
230+ # the first two renames into the final cache location (direct rename, then
231+ # rename of the temporary copy) fail; the third is the rollback of the
232+ # backup, which must be allowed through
233+ realRename <- base :: file.rename
234+ renamesToCache <- 0
235+ local_mocked_bindings(
236+ file.rename = function (from , to ) {
237+ if (! identical(to , cachedPackagePath )) {
238+ return (realRename(from , to ))
239+ }
240+ renamesToCache <<- renamesToCache + 1
241+ if (renamesToCache < = 2 ) {
242+ return (FALSE )
243+ }
244+ realRename(from , to )
245+ },
246+ .package = " base"
247+ )
248+
249+ expect_error(
250+ moveInstalledPackageToCache(
251+ packagePath ,
252+ hash ,
253+ overwrite = TRUE ,
254+ fatal = TRUE ,
255+ cacheDir = cacheDir
256+ ),
257+ " failed to copy package 'oatmeal' to cache"
258+ )
259+
260+ # the pre-existing cache entry was restored from backup
261+ desc <- readLines(file.path(cachedPackagePath , " DESCRIPTION" ))
262+ expect_true(" Version: 1.0" %in% desc )
263+ })
264+
108265test_that(" packrat uses the untrusted cache when instructed" , {
109266 skip_on_cran()
110267 skip_on_os(" windows" )
0 commit comments