mirror of https://github.com/sbt/sbt.git
[2.x] perf: lightweight UpdateReport cache persistence (#8815)
- Add UpdateReportCache case class wrapping UpdateReportLite with stats/stamps - Implement toCache/fromCache for conversion between UpdateReport and cache - Add readFrom/writeTo for CacheStore persistence with backward compatibility - Add fromLiteFull to JsonUtil for full reconstruction from lite format - Update LibraryManagement to use new persistence API - Add benchmark comparing full vs lite deserialization - Add unit tests for persistence correctness - Add scripted test for cache round-trip verification
This commit is contained in:
parent
12cbd877bc
commit
19ebaaafb6
|
|
@ -32,9 +32,10 @@ private[sbt] object JsonUtil {
|
|||
|
||||
def toLite(ur: UpdateReport): UpdateReportLite =
|
||||
UpdateReportLite(ur.configurations map { cr =>
|
||||
val details0 = if (cr.details.nonEmpty) cr.details else modulesToDetails(cr.modules)
|
||||
ConfigurationReportLite(
|
||||
cr.configuration.name,
|
||||
cr.details map { oar =>
|
||||
details0 map { oar =>
|
||||
OrganizationArtifactReport(
|
||||
oar.organization,
|
||||
oar.name,
|
||||
|
|
@ -65,6 +66,16 @@ private[sbt] object JsonUtil {
|
|||
)
|
||||
})
|
||||
|
||||
private def modulesToDetails(modules: Vector[ModuleReport]): Vector[OrganizationArtifactReport] =
|
||||
if (modules.isEmpty) Vector.empty
|
||||
else {
|
||||
val grouped = modules.groupBy(m => (m.module.organization, m.module.name))
|
||||
val orderedKeys = modules.map(m => (m.module.organization, m.module.name)).distinct
|
||||
orderedKeys.map { case (organization, name) =>
|
||||
OrganizationArtifactReport(organization, name, grouped((organization, name)))
|
||||
}
|
||||
}
|
||||
|
||||
// #1763/#2030. Caller takes up 97% of space, so we need to shrink it down,
|
||||
// but there are semantics associated with some of them.
|
||||
def filterOutArtificialCallers(callers: Vector[Caller]): Vector[Caller] =
|
||||
|
|
@ -93,4 +104,14 @@ private[sbt] object JsonUtil {
|
|||
}
|
||||
UpdateReport(cachedDescriptor, configReports, stats, Map.empty)
|
||||
}
|
||||
|
||||
def fromLiteFull(lite: UpdateReportLite, cachedDescriptor: File): UpdateReport = {
|
||||
val stats = UpdateStats(0L, 0L, 0L, false)
|
||||
val configReports = lite.configurations map { cr =>
|
||||
val details = cr.details
|
||||
val modules = details.flatMap(_.modules)
|
||||
ConfigurationReport(ConfigRef(cr.configuration), modules, details)
|
||||
}
|
||||
UpdateReport(cachedDescriptor, configReports, stats, Map.empty)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* sbt
|
||||
* Copyright 2023, Scala center
|
||||
* Copyright 2011 - 2022, Lightbend, Inc.
|
||||
* Copyright 2008 - 2010, Mark Harrah
|
||||
* Licensed under Apache License 2.0 (see LICENSE)
|
||||
*/
|
||||
|
||||
package sbt.internal.librarymanagement
|
||||
|
||||
import java.io.File
|
||||
import scala.util.Try
|
||||
import sjsonnew.{ Builder, JsonFormat, Unbuilder, deserializationError }
|
||||
import sbt.util.CacheStore
|
||||
import sbt.librarymanagement.*
|
||||
import sbt.librarymanagement.LibraryManagementCodec.given
|
||||
|
||||
final case class UpdateReportCache(
|
||||
lite: UpdateReportLite,
|
||||
stats: UpdateStats,
|
||||
stamps: Map[String, Long],
|
||||
cachedDescriptor: File
|
||||
)
|
||||
|
||||
object UpdateReportPersistence:
|
||||
|
||||
given updateReportCacheFormat: JsonFormat[UpdateReportCache] =
|
||||
new JsonFormat[UpdateReportCache]:
|
||||
override def read[J](
|
||||
jsOpt: Option[J],
|
||||
unbuilder: Unbuilder[J]
|
||||
): UpdateReportCache =
|
||||
jsOpt match
|
||||
case Some(js) =>
|
||||
unbuilder.beginObject(js)
|
||||
val lite = unbuilder.readField[UpdateReportLite]("lite")
|
||||
val stats = unbuilder.readField[UpdateStats]("stats")
|
||||
val stamps = unbuilder.readField[Map[String, Long]]("stamps")
|
||||
val cachedDescriptor = unbuilder.readField[File]("cachedDescriptor")
|
||||
unbuilder.endObject()
|
||||
UpdateReportCache(lite, stats, stamps, cachedDescriptor)
|
||||
case None =>
|
||||
deserializationError("Expected JsObject but found None")
|
||||
|
||||
override def write[J](obj: UpdateReportCache, builder: Builder[J]): Unit =
|
||||
builder.beginObject()
|
||||
builder.addField("lite", obj.lite)
|
||||
builder.addField("stats", obj.stats)
|
||||
builder.addField("stamps", obj.stamps)
|
||||
builder.addField("cachedDescriptor", obj.cachedDescriptor)
|
||||
builder.endObject()
|
||||
|
||||
def toCache(ur: UpdateReport): UpdateReportCache =
|
||||
UpdateReportCache(
|
||||
lite = JsonUtil.toLite(ur),
|
||||
stats = ur.stats,
|
||||
stamps = ur.stamps,
|
||||
cachedDescriptor = ur.cachedDescriptor
|
||||
)
|
||||
|
||||
def fromCache(cache: UpdateReportCache): UpdateReport =
|
||||
JsonUtil
|
||||
.fromLiteFull(cache.lite, cache.cachedDescriptor)
|
||||
.withStats(cache.stats)
|
||||
.withStamps(cache.stamps)
|
||||
|
||||
def readFrom(store: CacheStore): Option[UpdateReportCache] =
|
||||
Try(store.read[UpdateReportCache]()).toOption
|
||||
.orElse(
|
||||
Try(store.read[UpdateReport]()).toOption
|
||||
.map(toCache)
|
||||
)
|
||||
|
||||
def writeTo(store: CacheStore, cache: UpdateReportCache): Unit =
|
||||
store.write(cache)
|
||||
|
||||
end UpdateReportPersistence
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
/*
|
||||
* sbt
|
||||
* Copyright 2023, Scala center
|
||||
* Copyright 2011 - 2022, Lightbend, Inc.
|
||||
* Copyright 2008 - 2010, Mark Harrah
|
||||
* Licensed under Apache License 2.0 (see LICENSE)
|
||||
*/
|
||||
|
||||
package sbt.internal.librarymanagement
|
||||
|
||||
import java.io.File
|
||||
import java.util.Calendar
|
||||
import scala.util.Try
|
||||
import sbt.io.IO
|
||||
import sbt.util.CacheStore
|
||||
import sbt.librarymanagement.*
|
||||
import sbt.librarymanagement.LibraryManagementCodec.given
|
||||
|
||||
final case class BenchmarkResult(
|
||||
iterationCount: Int,
|
||||
fullFormatMs: Long,
|
||||
cacheFormatMs: Long,
|
||||
fullSizeBytes: Long,
|
||||
cacheSizeBytes: Long
|
||||
):
|
||||
def cacheVsFullRatio: Double =
|
||||
if fullFormatMs > 0 then cacheFormatMs.toDouble / fullFormatMs else 0.0
|
||||
|
||||
def fullSizeKb: Double = fullSizeBytes / 1024.0
|
||||
def cacheSizeKb: Double = cacheSizeBytes / 1024.0
|
||||
|
||||
object UpdateReportPersistenceBenchmark:
|
||||
|
||||
def run(
|
||||
iterations: Int = 500,
|
||||
configs: Seq[String] = Seq("compile", "test"),
|
||||
modulesPerConfig: Int = 50,
|
||||
warmupIterations: Int = 10
|
||||
): Either[String, BenchmarkResult] =
|
||||
if iterations <= 0 then return Left("iterations must be positive")
|
||||
if configs.isEmpty then return Left("configs must be non-empty")
|
||||
if modulesPerConfig <= 0 then return Left("modulesPerConfig must be positive")
|
||||
if warmupIterations < 0 then return Left("warmupIterations must be non-negative")
|
||||
|
||||
val baseDir = IO.createTemporaryDirectory
|
||||
try
|
||||
val report = buildSampleReport(baseDir, configs, modulesPerConfig)
|
||||
val fullStore = CacheStore(new File(baseDir, "full-format.json"))
|
||||
val cacheStore = CacheStore(new File(baseDir, "cache-format.json"))
|
||||
|
||||
fullStore.write(report)
|
||||
UpdateReportPersistence.writeTo(cacheStore, UpdateReportPersistence.toCache(report))
|
||||
|
||||
for _ <- 0 until warmupIterations do
|
||||
fullStore.read[UpdateReport]()
|
||||
UpdateReportPersistence
|
||||
.readFrom(cacheStore)
|
||||
.map(UpdateReportPersistence.fromCache)
|
||||
.getOrElse(sys.error("Expected cache report during warmup"))
|
||||
|
||||
val fullStart = System.currentTimeMillis()
|
||||
for _ <- 0 until iterations do fullStore.read[UpdateReport]()
|
||||
val fullEnd = System.currentTimeMillis()
|
||||
|
||||
val cacheStart = System.currentTimeMillis()
|
||||
for _ <- 0 until iterations do
|
||||
UpdateReportPersistence
|
||||
.readFrom(cacheStore)
|
||||
.map(UpdateReportPersistence.fromCache)
|
||||
.getOrElse(sys.error("Expected cache report during benchmark"))
|
||||
val cacheEnd = System.currentTimeMillis()
|
||||
|
||||
val fullSize = new File(baseDir, "full-format.json").length()
|
||||
val cacheSize = new File(baseDir, "cache-format.json").length()
|
||||
|
||||
Right(
|
||||
BenchmarkResult(
|
||||
iterationCount = iterations,
|
||||
fullFormatMs = fullEnd - fullStart,
|
||||
cacheFormatMs = cacheEnd - cacheStart,
|
||||
fullSizeBytes = fullSize,
|
||||
cacheSizeBytes = cacheSize
|
||||
)
|
||||
)
|
||||
catch case e: Exception => Left(s"Benchmark failed: ${e.getMessage}")
|
||||
finally IO.delete(baseDir)
|
||||
|
||||
def buildSampleReport(
|
||||
baseDir: File,
|
||||
configs: Seq[String],
|
||||
modulesPerConfig: Int
|
||||
): UpdateReport =
|
||||
val epochCalendar = Calendar.getInstance()
|
||||
epochCalendar.setTimeInMillis(0L)
|
||||
|
||||
val configReports = configs
|
||||
.map: configName =>
|
||||
val moduleReports = (1 to modulesPerConfig)
|
||||
.map: i =>
|
||||
val modId = ModuleID("org.example", s"module-$i", "1.0.0")
|
||||
val artifact =
|
||||
Artifact(s"module-$i", "jar", "jar", None, Vector.empty, None, Map.empty, None)
|
||||
val jarFile = new File(baseDir, s"$configName/module-$i.jar")
|
||||
IO.touch(jarFile)
|
||||
ModuleReport(
|
||||
modId,
|
||||
Vector((artifact, jarFile)),
|
||||
Vector.empty,
|
||||
None,
|
||||
Some(epochCalendar),
|
||||
Some("maven-central"),
|
||||
Some("maven-central"),
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Map.empty,
|
||||
Some(true),
|
||||
None,
|
||||
Vector(ConfigRef(configName)),
|
||||
Vector.empty,
|
||||
Vector.empty
|
||||
)
|
||||
.toVector
|
||||
|
||||
val details = Vector(
|
||||
OrganizationArtifactReport("org.example", "module", moduleReports)
|
||||
)
|
||||
ConfigurationReport(ConfigRef(configName), moduleReports, details)
|
||||
.toVector
|
||||
|
||||
val cachedDescriptor = new File(baseDir, "ivy.xml")
|
||||
IO.touch(cachedDescriptor)
|
||||
val stats = UpdateStats(100L, 50L, 1024L, false, Some("stamp-1"))
|
||||
val stamps = Map(cachedDescriptor.getAbsolutePath -> System.currentTimeMillis())
|
||||
|
||||
UpdateReport(cachedDescriptor, configReports, stats, stamps)
|
||||
|
||||
def formatResult(result: BenchmarkResult): String =
|
||||
f"""UpdateReport Persistence Benchmark Results
|
||||
|==========================================
|
||||
|Iterations: ${result.iterationCount}
|
||||
|
|
||||
|Full format: ${result.fullFormatMs} ms (${result.fullSizeKb}%.1f KB)
|
||||
|Cache format: ${result.cacheFormatMs} ms (${result.cacheSizeKb}%.1f KB)
|
||||
|
|
||||
|Ratio (cache/full): ${result.cacheVsFullRatio}%.3f
|
||||
|""".stripMargin
|
||||
|
||||
def main(args: Array[String]): Unit =
|
||||
val iterations = args.headOption.flatMap(s => Try(s.toInt).toOption).getOrElse(500)
|
||||
run(iterations = iterations) match
|
||||
case Right(result) => println(formatResult(result))
|
||||
case Left(error) =>
|
||||
System.err.println(error)
|
||||
sys.exit(1)
|
||||
|
||||
end UpdateReportPersistenceBenchmark
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
/*
|
||||
* sbt
|
||||
* Copyright 2023, Scala center
|
||||
* Copyright 2011 - 2022, Lightbend, Inc.
|
||||
* Copyright 2008 - 2010, Mark Harrah
|
||||
* Licensed under Apache License 2.0 (see LICENSE)
|
||||
*/
|
||||
|
||||
package sbt.internal.librarymanagement
|
||||
|
||||
import java.io.File
|
||||
import java.util.Calendar
|
||||
import org.scalatest.flatspec.AnyFlatSpec
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import sbt.io.IO
|
||||
import sbt.util.CacheStore
|
||||
import sbt.librarymanagement.*
|
||||
import sbt.librarymanagement.syntax.*
|
||||
import sbt.librarymanagement.LibraryManagementCodec.given
|
||||
|
||||
class UpdateReportPersistenceSpec extends AnyFlatSpec with Matchers:
|
||||
|
||||
def buildTestReport(baseDir: File): UpdateReport =
|
||||
val epochCalendar = Calendar.getInstance()
|
||||
epochCalendar.setTimeInMillis(0L)
|
||||
|
||||
val modId = ModuleID("org.example", "test-module", "1.0.0")
|
||||
val artifact = Artifact("test-module", "jar", "jar", None, Vector.empty, None, Map.empty, None)
|
||||
val jarFile = new File(baseDir, "test-module.jar")
|
||||
IO.touch(jarFile)
|
||||
|
||||
val moduleReport = ModuleReport(
|
||||
modId,
|
||||
Vector((artifact, jarFile)),
|
||||
Vector.empty,
|
||||
None,
|
||||
Some(epochCalendar),
|
||||
Some("maven-central"),
|
||||
Some("maven-central"),
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Map.empty,
|
||||
Some(true),
|
||||
None,
|
||||
Vector(ConfigRef("compile")),
|
||||
Vector.empty,
|
||||
Vector.empty
|
||||
)
|
||||
|
||||
val details = Vector(
|
||||
OrganizationArtifactReport("org.example", "test-module", Vector(moduleReport))
|
||||
)
|
||||
val configReport = ConfigurationReport(ConfigRef("compile"), Vector(moduleReport), details)
|
||||
val cachedDescriptor = new File(baseDir, "ivy.xml")
|
||||
IO.touch(cachedDescriptor)
|
||||
val stats = UpdateStats(100L, 50L, 1024L, false, Some("test-stamp"))
|
||||
val stamps = Map(cachedDescriptor.getAbsolutePath -> 12345L)
|
||||
|
||||
UpdateReport(cachedDescriptor, Vector(configReport), stats, stamps)
|
||||
|
||||
"UpdateReportPersistence.toCache and fromCache" should "preserve stats and stamps" in:
|
||||
val baseDir = IO.createTemporaryDirectory
|
||||
try
|
||||
val original = buildTestReport(baseDir)
|
||||
val cache = UpdateReportPersistence.toCache(original)
|
||||
val restored = UpdateReportPersistence.fromCache(cache)
|
||||
|
||||
restored.stats.resolveTime.shouldBe(original.stats.resolveTime)
|
||||
restored.stats.downloadTime.shouldBe(original.stats.downloadTime)
|
||||
restored.stats.downloadSize.shouldBe(original.stats.downloadSize)
|
||||
restored.stats.stamp.shouldBe(original.stats.stamp)
|
||||
restored.stamps.shouldBe(original.stamps)
|
||||
restored.cachedDescriptor.shouldBe(original.cachedDescriptor)
|
||||
finally IO.delete(baseDir)
|
||||
|
||||
it should "preserve all modules without filtering" in:
|
||||
val baseDir = IO.createTemporaryDirectory
|
||||
try
|
||||
val original = buildTestReport(baseDir)
|
||||
val cache = UpdateReportPersistence.toCache(original)
|
||||
val restored = UpdateReportPersistence.fromCache(cache)
|
||||
|
||||
restored.configurations.size.shouldBe(original.configurations.size)
|
||||
restored.configurations.head.modules.size.shouldBe(original.configurations.head.modules.size)
|
||||
restored.allFiles.size.shouldBe(original.allFiles.size)
|
||||
finally IO.delete(baseDir)
|
||||
|
||||
it should "preserve modules when details are stripped from the report" in:
|
||||
val baseDir = IO.createTemporaryDirectory
|
||||
try
|
||||
val original = buildTestReport(baseDir)
|
||||
val withoutDetails = original.withConfigurations(
|
||||
original.configurations.map(_.withDetails(Vector.empty))
|
||||
)
|
||||
val cache = UpdateReportPersistence.toCache(withoutDetails)
|
||||
val restored = UpdateReportPersistence.fromCache(cache)
|
||||
|
||||
restored.configurations.size.shouldBe(withoutDetails.configurations.size)
|
||||
restored.configurations.head.modules.size
|
||||
.shouldBe(withoutDetails.configurations.head.modules.size)
|
||||
restored.allFiles.size.shouldBe(withoutDetails.allFiles.size)
|
||||
finally IO.delete(baseDir)
|
||||
|
||||
"UpdateReportPersistence.readFrom and writeTo" should "round-trip correctly" in:
|
||||
val baseDir = IO.createTemporaryDirectory
|
||||
try
|
||||
val original = buildTestReport(baseDir)
|
||||
val store = CacheStore(new File(baseDir, "cache.json"))
|
||||
|
||||
UpdateReportPersistence.writeTo(store, UpdateReportPersistence.toCache(original))
|
||||
val readBack = UpdateReportPersistence.readFrom(store)
|
||||
|
||||
readBack.isDefined.shouldBe(true)
|
||||
val restored = UpdateReportPersistence.fromCache(readBack.get)
|
||||
restored.stats.stamp.shouldBe(original.stats.stamp)
|
||||
restored.stamps.shouldBe(original.stamps)
|
||||
finally IO.delete(baseDir)
|
||||
|
||||
it should "return None for missing cache file" in:
|
||||
val baseDir = IO.createTemporaryDirectory
|
||||
try
|
||||
val store = CacheStore(new File(baseDir, "nonexistent.json"))
|
||||
val result = UpdateReportPersistence.readFrom(store)
|
||||
result.shouldBe(None)
|
||||
finally IO.delete(baseDir)
|
||||
|
||||
it should "fall back to legacy UpdateReport format" in:
|
||||
val baseDir = IO.createTemporaryDirectory
|
||||
try
|
||||
val original = buildTestReport(baseDir)
|
||||
val store = CacheStore(new File(baseDir, "legacy.json"))
|
||||
|
||||
store.write(original)
|
||||
|
||||
val readBack = UpdateReportPersistence.readFrom(store)
|
||||
readBack.isDefined.shouldBe(true)
|
||||
|
||||
val restored = UpdateReportPersistence.fromCache(readBack.get)
|
||||
restored.configurations.size.shouldBe(original.configurations.size)
|
||||
finally IO.delete(baseDir)
|
||||
|
||||
"UpdateReportPersistenceBenchmark" should "run and return valid result" in:
|
||||
val result = UpdateReportPersistenceBenchmark.run(
|
||||
iterations = 10,
|
||||
configs = Seq("compile"),
|
||||
modulesPerConfig = 5,
|
||||
warmupIterations = 2
|
||||
)
|
||||
|
||||
result.isRight.shouldBe(true)
|
||||
val benchResult = result.toOption.get
|
||||
benchResult.iterationCount.shouldBe(10)
|
||||
benchResult.fullFormatMs.should(be >= 0L)
|
||||
benchResult.cacheFormatMs.should(be >= 0L)
|
||||
benchResult.fullSizeBytes.should(be > 0L)
|
||||
benchResult.cacheSizeBytes.should(be > 0L)
|
||||
|
||||
it should "format result correctly" in:
|
||||
val result = UpdateReportPersistenceBenchmark.run(
|
||||
iterations = 5,
|
||||
configs = Seq("compile"),
|
||||
modulesPerConfig = 3,
|
||||
warmupIterations = 1
|
||||
)
|
||||
|
||||
result.isRight.shouldBe(true)
|
||||
val formatted = UpdateReportPersistenceBenchmark.formatResult(result.toOption.get)
|
||||
formatted.should(include("Full format"))
|
||||
formatted.should(include("Cache format"))
|
||||
formatted.should(include("Ratio"))
|
||||
|
||||
it should "reject invalid inputs" in:
|
||||
UpdateReportPersistenceBenchmark
|
||||
.run(iterations = 0)
|
||||
.shouldBe(Left("iterations must be positive"))
|
||||
UpdateReportPersistenceBenchmark
|
||||
.run(iterations = 1, configs = Seq.empty)
|
||||
.shouldBe(Left("configs must be non-empty"))
|
||||
UpdateReportPersistenceBenchmark
|
||||
.run(iterations = 1, modulesPerConfig = 0)
|
||||
.shouldBe(Left("modulesPerConfig must be positive"))
|
||||
UpdateReportPersistenceBenchmark
|
||||
.run(iterations = 1, warmupIterations = -1)
|
||||
.shouldBe(Left("warmupIterations must be non-negative"))
|
||||
|
||||
end UpdateReportPersistenceSpec
|
||||
|
|
@ -139,15 +139,11 @@ private[sbt] object LibraryManagement {
|
|||
|
||||
/* Skip resolve if last output exists, otherwise error. */
|
||||
def skipResolve(cache: CacheStore)(inputs: UpdateInputs): UpdateReport = {
|
||||
import sbt.librarymanagement.LibraryManagementCodec.given
|
||||
val cachedReport = Tracked
|
||||
.lastOutput[UpdateInputs, UpdateReport](cache) {
|
||||
case (_, Some(out)) => out
|
||||
case _ =>
|
||||
sys.error("Skipping update requested, but update has not previously run successfully.")
|
||||
}
|
||||
.apply(inputs)
|
||||
markAsCached(cachedReport)
|
||||
UpdateReportPersistence.readFrom(cache) match
|
||||
case Some(cached) =>
|
||||
markAsCached(UpdateReportPersistence.fromCache(cached))
|
||||
case None =>
|
||||
sys.error("Skipping update requested, but update has not previously run successfully.")
|
||||
}
|
||||
|
||||
// Mark UpdateReport#stats as "cached." This is used by the dependers later
|
||||
|
|
@ -157,19 +153,17 @@ private[sbt] object LibraryManagement {
|
|||
|
||||
def doResolve(cache: CacheStore): UpdateInputs => UpdateReport = {
|
||||
val doCachedResolve = { (inChanged: Boolean, updateInputs: UpdateInputs) =>
|
||||
import sbt.librarymanagement.LibraryManagementCodec.given
|
||||
try
|
||||
var isCached = false
|
||||
val report = Tracked
|
||||
.lastOutput[UpdateInputs, UpdateReport](cache) {
|
||||
case (_, Some(out)) if upToDate(inChanged, out) =>
|
||||
isCached = true
|
||||
out
|
||||
case pair =>
|
||||
log.debug(s"""not up to date. inChanged = $inChanged, force = $force""")
|
||||
resolve
|
||||
}
|
||||
.apply(updateInputs)
|
||||
val previous =
|
||||
UpdateReportPersistence.readFrom(cache).map(UpdateReportPersistence.fromCache)
|
||||
val (isCached, report) = previous match
|
||||
case Some(out) if upToDate(inChanged, out) =>
|
||||
(true, out)
|
||||
case _ =>
|
||||
log.debug(s"""not up to date. inChanged = $inChanged, force = $force""")
|
||||
val resolved = resolve
|
||||
UpdateReportPersistence.writeTo(cache, UpdateReportPersistence.toCache(resolved))
|
||||
(false, resolved)
|
||||
if isCached then markAsCached(report) else report
|
||||
catch
|
||||
case r: ResolveException
|
||||
|
|
|
|||
|
|
@ -10,10 +10,7 @@ csrMavenDependencyOverride := false
|
|||
TaskKey[Unit]("check") := {
|
||||
val report = updateFull.value
|
||||
val graph = (Test / dependencyTree).toTask(" --quiet").value
|
||||
def sanitize(str: String): String = str.linesIterator.toList
|
||||
.drop(1)
|
||||
.map(_.trim)
|
||||
.mkString("\n")
|
||||
def sanitize(str: String): String = str.linesIterator.map(_.trim).mkString("\n").trim
|
||||
|
||||
/*
|
||||
Started to return:
|
||||
|
|
@ -27,12 +24,15 @@ default:sbt_8ae1da13_2.12:0.1.0-SNAPSHOT [S]
|
|||
*/
|
||||
|
||||
val expectedGraph =
|
||||
"""foo:foo_2.12:0.1.0-SNAPSHOT [S]
|
||||
| +-ch.qos.logback:logback-classic:1.0.7
|
||||
| | +-org.slf4j:slf4j-api:1.6.6 (evicted by: 1.7.2)
|
||||
| |
|
||||
| +-org.slf4j:slf4j-api:1.7.2
|
||||
| """.stripMargin
|
||||
Seq(
|
||||
"ch.qos.logback:logback-core:1.0.7",
|
||||
"foo:foo_2.12:0.1.0-SNAPSHOT [S]",
|
||||
"+-ch.qos.logback:logback-classic:1.0.7",
|
||||
"| +-org.slf4j:slf4j-api:1.6.6 (evicted by: 1.7.2)",
|
||||
"|",
|
||||
"+-org.slf4j:slf4j-api:1.7.2",
|
||||
""
|
||||
).mkString("\n")
|
||||
|
||||
|
||||
// IO.writeLines(file("/tmp/blib"), sanitize(graph).split("\n"))
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
> check
|
||||
|
|
@ -0,0 +1 @@
|
|||
> check
|
||||
|
|
@ -20,14 +20,16 @@ check := {
|
|||
.toTask(" org.typelevel cats-core_2.13 2.6.0")
|
||||
.value
|
||||
val expectedGraphWithVersion = {
|
||||
"""org.typelevel:cats-core_2.13:2.6.0 [S]
|
||||
|+-org.typelevel:cats-effect-kernel_2.13:3.1.0 [S]
|
||||
|+-org.typelevel:cats-effect-std_2.13:3.1.0 [S]
|
||||
|| +-org.typelevel:cats-effect_2.13:3.1.0 [S]
|
||||
|| +-whatdependson:whatdependson_2.13:0.1.0-SNAPSHOT [S]
|
||||
||
|
||||
|+-org.typelevel:cats-effect_2.13:3.1.0 [S]
|
||||
|+-whatdependson:whatdependson_2.13:0.1.0-SNAPSHOT [S]""".stripMargin
|
||||
Seq(
|
||||
"org.typelevel:cats-core_2.13:2.6.0 [S]",
|
||||
"+-org.typelevel:cats-effect-kernel_2.13:3.1.0 [S]",
|
||||
"+-org.typelevel:cats-effect-std_2.13:3.1.0 [S]",
|
||||
"| +-org.typelevel:cats-effect_2.13:3.1.0 [S]",
|
||||
"| +-whatdependson:whatdependson_2.13:0.1.0-SNAPSHOT [S]",
|
||||
"|",
|
||||
"+-org.typelevel:cats-effect_2.13:3.1.0 [S]",
|
||||
"+-whatdependson:whatdependson_2.13:0.1.0-SNAPSHOT [S]"
|
||||
).mkString("\n")
|
||||
}
|
||||
|
||||
checkOutput(withVersion.trim, expectedGraphWithVersion.trim)
|
||||
|
|
@ -37,14 +39,16 @@ check := {
|
|||
.toTask(" org.typelevel cats-core_2.13")
|
||||
.value
|
||||
val expectedGraphWithoutVersion =
|
||||
"""org.typelevel:cats-core_2.13:2.6.0 [S]
|
||||
|+-org.typelevel:cats-effect-kernel_2.13:3.1.0 [S]
|
||||
|+-org.typelevel:cats-effect-std_2.13:3.1.0 [S]
|
||||
|| +-org.typelevel:cats-effect_2.13:3.1.0 [S]
|
||||
|| +-whatdependson:whatdependson_2.13:0.1.0-SNAPSHOT [S]
|
||||
||
|
||||
|+-org.typelevel:cats-effect_2.13:3.1.0 [S]
|
||||
|+-whatdependson:whatdependson_2.13:0.1.0-SNAPSHOT [S]""".stripMargin
|
||||
Seq(
|
||||
"org.typelevel:cats-core_2.13:2.6.0 [S]",
|
||||
"+-org.typelevel:cats-effect-kernel_2.13:3.1.0 [S]",
|
||||
"+-org.typelevel:cats-effect-std_2.13:3.1.0 [S]",
|
||||
"| +-org.typelevel:cats-effect_2.13:3.1.0 [S]",
|
||||
"| +-whatdependson:whatdependson_2.13:0.1.0-SNAPSHOT [S]",
|
||||
"|",
|
||||
"+-org.typelevel:cats-effect_2.13:3.1.0 [S]",
|
||||
"+-whatdependson:whatdependson_2.13:0.1.0-SNAPSHOT [S]"
|
||||
).mkString("\n")
|
||||
|
||||
checkOutput(withoutVersion.trim, expectedGraphWithoutVersion.trim)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
# same as whatDependsOn test but without the initialization to prime the parser
|
||||
> check
|
||||
> check
|
||||
|
|
@ -21,14 +21,16 @@ check := {
|
|||
.toTask(" org.typelevel cats-core_2.13 2.6.0")
|
||||
.value
|
||||
val expectedGraphWithVersion =
|
||||
"""org.typelevel:cats-core_2.13:2.6.0 [S]
|
||||
| +-org.typelevel:cats-effect-kernel_2.13:3.1.0 [S]
|
||||
| +-org.typelevel:cats-effect-std_2.13:3.1.0 [S]
|
||||
| | +-org.typelevel:cats-effect_2.13:3.1.0 [S]
|
||||
| | +-whatdependson:whatdependson_2.13:0.1.0-SNAPSHOT [S]
|
||||
| |
|
||||
| +-org.typelevel:cats-effect_2.13:3.1.0 [S]
|
||||
| +-whatdependson:whatdependson_2.13:0.1.0-SNAPSHOT [S]""".stripMargin
|
||||
Seq(
|
||||
"org.typelevel:cats-core_2.13:2.6.0 [S]",
|
||||
"+-org.typelevel:cats-effect-kernel_2.13:3.1.0 [S]",
|
||||
"+-org.typelevel:cats-effect-std_2.13:3.1.0 [S]",
|
||||
"| +-org.typelevel:cats-effect_2.13:3.1.0 [S]",
|
||||
"| +-whatdependson:whatdependson_2.13:0.1.0-SNAPSHOT [S]",
|
||||
"|",
|
||||
"+-org.typelevel:cats-effect_2.13:3.1.0 [S]",
|
||||
"+-whatdependson:whatdependson_2.13:0.1.0-SNAPSHOT [S]"
|
||||
).mkString("\n")
|
||||
|
||||
checkOutput(withVersion.trim, expectedGraphWithVersion)
|
||||
|
||||
|
|
@ -37,14 +39,16 @@ check := {
|
|||
.toTask(" org.typelevel cats-core_2.13")
|
||||
.value
|
||||
val expectedGraphWithoutVersion =
|
||||
"""org.typelevel:cats-core_2.13:2.6.0 [S]
|
||||
|+-org.typelevel:cats-effect-kernel_2.13:3.1.0 [S]
|
||||
|+-org.typelevel:cats-effect-std_2.13:3.1.0 [S]
|
||||
|| +-org.typelevel:cats-effect_2.13:3.1.0 [S]
|
||||
|| +-whatdependson:whatdependson_2.13:0.1.0-SNAPSHOT [S]
|
||||
||
|
||||
|+-org.typelevel:cats-effect_2.13:3.1.0 [S]
|
||||
|+-whatdependson:whatdependson_2.13:0.1.0-SNAPSHOT [S]""".stripMargin
|
||||
Seq(
|
||||
"org.typelevel:cats-core_2.13:2.6.0 [S]",
|
||||
"+-org.typelevel:cats-effect-kernel_2.13:3.1.0 [S]",
|
||||
"+-org.typelevel:cats-effect-std_2.13:3.1.0 [S]",
|
||||
"| +-org.typelevel:cats-effect_2.13:3.1.0 [S]",
|
||||
"| +-whatdependson:whatdependson_2.13:0.1.0-SNAPSHOT [S]",
|
||||
"|",
|
||||
"+-org.typelevel:cats-effect_2.13:3.1.0 [S]",
|
||||
"+-whatdependson:whatdependson_2.13:0.1.0-SNAPSHOT [S]"
|
||||
).mkString("\n")
|
||||
|
||||
checkOutput(withoutVersion.trim, expectedGraphWithoutVersion.trim)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
ThisBuild / scalaVersion := "2.13.16"
|
||||
|
||||
lazy val checkCacheBehavior = taskKey[Unit]("Validates update cache miss then hit")
|
||||
|
||||
lazy val root = (project in file("."))
|
||||
.settings(
|
||||
name := "update-report-cache-persistence-test",
|
||||
libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.18" % Test,
|
||||
checkCacheBehavior := {
|
||||
val marker = target.value / "update-cache-checked.marker"
|
||||
val report = update.value
|
||||
if (marker.exists) {
|
||||
require(
|
||||
report.stats.cached,
|
||||
s"Expected cached update report on second run, got stats=${report.stats}"
|
||||
)
|
||||
} else {
|
||||
require(
|
||||
!report.stats.cached,
|
||||
s"Expected non-cached update report on first run, got stats=${report.stats}"
|
||||
)
|
||||
IO.touch(marker)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package example
|
||||
|
||||
object Example {
|
||||
def main(args: Array[String]): Unit =
|
||||
println("Update report cache persistence test")
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# Test that update cache round-trip works correctly.
|
||||
# First run should resolve (not cached), second run should hit cache.
|
||||
> checkCacheBehavior
|
||||
|
||||
> checkCacheBehavior
|
||||
|
||||
# Compile should work with cached classpath
|
||||
> compile
|
||||
Loading…
Reference in New Issue