diff --git a/librarymanagement/src/main/contraband/librarymanagement.json b/librarymanagement/src/main/contraband/librarymanagement.json index c93091568..3758bd902 100644 --- a/librarymanagement/src/main/contraband/librarymanagement.json +++ b/librarymanagement/src/main/contraband/librarymanagement.json @@ -634,7 +634,8 @@ { "name": "retrieve", "type": "sbt.internal.librarymanagement.RetrieveConfiguration?" }, { "name": "missingOk", "type": "boolean" }, { "name": "logging", "type": "sbt.librarymanagement.UpdateLogging" }, - { "name": "artifactFilter", "type": "sbt.librarymanagement.ArtifactTypeFilter" } + { "name": "artifactFilter", "type": "sbt.librarymanagement.ArtifactTypeFilter" }, + { "name": "offline", "type": "boolean" } ] }, { diff --git a/librarymanagement/src/main/scala/sbt/internal/librarymanagement/IvyActions.scala b/librarymanagement/src/main/scala/sbt/internal/librarymanagement/IvyActions.scala index 0c7cef4a6..4d8465a10 100644 --- a/librarymanagement/src/main/scala/sbt/internal/librarymanagement/IvyActions.scala +++ b/librarymanagement/src/main/scala/sbt/internal/librarymanagement/IvyActions.scala @@ -467,6 +467,7 @@ object IvyActions { val resolveId = ResolveOptions.getDefaultResolveId(moduleDescriptor) resolveOptions.setResolveId(resolveId) resolveOptions.setArtifactFilter(updateConfiguration.artifactFilter) + resolveOptions.setUseCacheOnly(updateConfiguration.offline) resolveOptions.setLog(ivyLogLevel(logging)) ResolutionCache.cleanModule( moduleDescriptor.getModuleRevisionId, @@ -519,6 +520,7 @@ object IvyActions { val resolveId = ResolveOptions.getDefaultResolveId(descriptor) resolveOptions.setResolveId(resolveId) resolveOptions.setArtifactFilter(updateConfiguration.artifactFilter) + resolveOptions.setUseCacheOnly(updateConfiguration.offline) resolveOptions.setLog(ivyLogLevel(updateConfiguration.logging)) val acceptError = updateConfiguration.missingOk resolver.customResolve(descriptor, acceptError, logicalClock, resolveOptions, cache, log) diff --git a/librarymanagement/src/main/scala/sbt/librarymanagement/DefaultLibraryManagement.scala b/librarymanagement/src/main/scala/sbt/librarymanagement/DefaultLibraryManagement.scala index 43bf8b231..3c5d38167 100644 --- a/librarymanagement/src/main/scala/sbt/librarymanagement/DefaultLibraryManagement.scala +++ b/librarymanagement/src/main/scala/sbt/librarymanagement/DefaultLibraryManagement.scala @@ -68,7 +68,8 @@ class DefaultLibraryManagement(ivyConfiguration: IvyConfiguration, log: Logger) Some(retrieveConfiguration), true, UpdateLogging.DownloadOnly, - artifactFilter + artifactFilter, + false ) log.debug(s"Attempting to fetch ${dependenciesNames(module)}. This operation may fail.") diff --git a/librarymanagement/src/test/scala/BaseIvySpecification.scala b/librarymanagement/src/test/scala/BaseIvySpecification.scala index 50a7f1f95..b01f1407d 100644 --- a/librarymanagement/src/test/scala/BaseIvySpecification.scala +++ b/librarymanagement/src/test/scala/BaseIvySpecification.scala @@ -69,18 +69,19 @@ trait BaseIvySpecification extends UnitSpec { log) } - def makeUpdateConfiguration: UpdateConfiguration = { + def makeUpdateConfiguration(offline: Boolean): UpdateConfiguration = { val retrieveConfig = RetrieveConfiguration(currentManaged, Resolver.defaultRetrievePattern).withSync(false) UpdateConfiguration(Some(retrieveConfig), false, UpdateLogging.Full, - ArtifactTypeFilter.forbid(Set("src", "doc"))) + ArtifactTypeFilter.forbid(Set("src", "doc")), + offline) } def ivyUpdateEither(module: IvySbt#Module): Either[UnresolvedWarning, UpdateReport] = { // IO.delete(currentTarget) - val config = makeUpdateConfiguration + val config = makeUpdateConfiguration(false) IvyActions.updateEither(module, config, UnresolvedWarningConfiguration(), diff --git a/librarymanagement/src/test/scala/OfflineModeSpec.scala b/librarymanagement/src/test/scala/OfflineModeSpec.scala new file mode 100644 index 000000000..903b7125d --- /dev/null +++ b/librarymanagement/src/test/scala/OfflineModeSpec.scala @@ -0,0 +1,69 @@ +package sbt.librarymanagement + +import org.scalatest.Assertion +import sbt.internal.librarymanagement._ +import sbt.internal.librarymanagement.impl.DependencyBuilders + +class OfflineModeSpec extends BaseIvySpecification with DependencyBuilders { + private final val targetDir = Some(currentDependency) + private final val onlineConf = makeUpdateConfiguration(false) + private final val offlineConf = makeUpdateConfiguration(true) + private final val noClock = LogicalClock.unknown + private final val warningConf = UnresolvedWarningConfiguration() + private final val normalOptions = UpdateOptions() + private final val cachedOptions = UpdateOptions().withCachedResolution(true) + + final val scalaCompiler = Vector("org.scala-lang" % "scala-compiler" % "2.12.2" % "compile") + + def checkOnlineAndOfflineResolution(updateOptions: UpdateOptions): Assertion = { + cleanIvyCache() + + val toResolve = module(defaultModuleId, scalaCompiler, None, updateOptions) + val isCachedResolution = updateOptions.cachedResolution + if (isCachedResolution) cleanCachedResolutionCache(toResolve) + + val onlineResolution = + IvyActions.updateEither(toResolve, onlineConf, warningConf, noClock, targetDir, log) + assert(onlineResolution.isRight) + assert(onlineResolution.right.exists(report => report.stats.resolveTime > 0)) + + // Compute an estimate to ensure that the second resolution does indeed use the cache + val resolutionTime: Long = onlineResolution.right.map(_.stats.resolveTime).getOrElse(0L) + val estimatedCachedTime = resolutionTime * 0.15 + + val offlineResolution = + IvyActions.updateEither(toResolve, offlineConf, warningConf, noClock, targetDir, log) + assert(offlineResolution.isRight) + + if (!isCachedResolution) { + // Only check the estimate for the non cached resolution, otherwise resolution is cached + assert(offlineResolution.right.exists(_.stats.resolveTime <= estimatedCachedTime), + "Offline resolution took more than 15% of normal resolution's running time.") + } else assert(true) // We cannot check offline resolution if it's cached. + } + + "Offline update configuration" should "reuse the caches when it's enabled" in { + checkOnlineAndOfflineResolution(normalOptions) + } + + it should "work with cached resolution" in { + checkOnlineAndOfflineResolution(cachedOptions) + } + + def checkFailingResolution(updateOptions: UpdateOptions): Assertion = { + cleanIvyCache() + val toResolve = module(defaultModuleId, scalaCompiler, None, updateOptions) + if (updateOptions.cachedResolution) cleanCachedResolutionCache(toResolve) + val failedOfflineResolution = + IvyActions.updateEither(toResolve, offlineConf, warningConf, noClock, targetDir, log) + assert(failedOfflineResolution.isLeft) + } + + it should "fail when artifacts are missing in the cache" in { + checkFailingResolution(normalOptions) + } + + it should "fail when artifacts are missing in the cache for cached resolution" in { + checkFailingResolution(cachedOptions) + } +} diff --git a/librarymanagement/src/test/scala/sbt/internal/librarymanagement/IvyRepoSpec.scala b/librarymanagement/src/test/scala/sbt/internal/librarymanagement/IvyRepoSpec.scala index 61c9aa27c..51be80d06 100644 --- a/librarymanagement/src/test/scala/sbt/internal/librarymanagement/IvyRepoSpec.scala +++ b/librarymanagement/src/test/scala/sbt/internal/librarymanagement/IvyRepoSpec.scala @@ -44,7 +44,7 @@ class IvyRepoSpec extends BaseIvySpecification with DependencyBuilders { val m = makeModuleForDepWithSources // the "default" configuration used in updateEither. - val c = makeUpdateConfiguration + val c = makeUpdateConfiguration(false) val ivyScala = m.moduleSettings.ivyScala val srcTypes = Set("src")