Merge pull request #1467 from sbt/wip/eviction-warning

Eviction warnings (Fixes #1200)
This commit is contained in:
Josh Suereth 2014-07-29 11:08:42 -04:00
commit 984bcf589c
8 changed files with 994 additions and 32 deletions

View File

@ -2,6 +2,11 @@ package sbt
import DependencyFilter._
/**
* Provide warnings for cross version conflicts.
* A library foo_2.10 and foo_2.11 can potentially be both included on the
* library dependency graph by mistake, but it won't be caught by eviction.
*/
final case class ConflictWarning(label: String, level: Level.Value, failOnConflict: Boolean) {
@deprecated("`filter` is no longer used", "0.13.0")
val filter: ModuleFilter = (_: ModuleID) => false

View File

@ -0,0 +1,200 @@
package sbt
import collection.mutable
import Configurations.Compile
final class EvictionWarningOptions private[sbt] (
val configurations: Seq[Configuration],
val warnScalaVersionEviction: Boolean,
val warnDirectEvictions: Boolean,
val warnTransitiveEvictions: Boolean,
val showCallers: Boolean,
val guessCompatible: Function1[(ModuleID, Option[ModuleID], Option[IvyScala]), Boolean]) {
private[sbt] def configStrings = configurations map { _.name }
def withConfigurations(configurations: Seq[Configuration]): EvictionWarningOptions =
copy(configurations = configurations)
def withWarnScalaVersionEviction(warnScalaVersionEviction: Boolean): EvictionWarningOptions =
copy(warnScalaVersionEviction = warnScalaVersionEviction)
def withWarnDirectEvictions(warnDirectEvictions: Boolean): EvictionWarningOptions =
copy(warnDirectEvictions = warnDirectEvictions)
def withWarnTransitiveEvictions(warnTransitiveEvictions: Boolean): EvictionWarningOptions =
copy(warnTransitiveEvictions = warnTransitiveEvictions)
def withShowCallers(showCallers: Boolean): EvictionWarningOptions =
copy(showCallers = showCallers)
def withGuessCompatible(guessCompatible: Function1[(ModuleID, Option[ModuleID], Option[IvyScala]), Boolean]): EvictionWarningOptions =
copy(guessCompatible = guessCompatible)
private[sbt] def copy(configurations: Seq[Configuration] = configurations,
warnScalaVersionEviction: Boolean = warnScalaVersionEviction,
warnDirectEvictions: Boolean = warnDirectEvictions,
warnTransitiveEvictions: Boolean = warnTransitiveEvictions,
showCallers: Boolean = showCallers,
guessCompatible: Function1[(ModuleID, Option[ModuleID], Option[IvyScala]), Boolean] = guessCompatible): EvictionWarningOptions =
new EvictionWarningOptions(configurations = configurations,
warnScalaVersionEviction = warnScalaVersionEviction,
warnDirectEvictions = warnDirectEvictions,
warnTransitiveEvictions = warnTransitiveEvictions,
showCallers = showCallers,
guessCompatible = guessCompatible)
}
object EvictionWarningOptions {
def default: EvictionWarningOptions =
new EvictionWarningOptions(Vector(Compile), true, true, false, false, defaultGuess)
def full: EvictionWarningOptions =
new EvictionWarningOptions(Vector(Compile), true, true, true, true, defaultGuess)
lazy val defaultGuess: Function1[(ModuleID, Option[ModuleID], Option[IvyScala]), Boolean] =
guessSecondSegment orElse guessSemVer orElse guessFalse
lazy val guessSecondSegment: PartialFunction[(ModuleID, Option[ModuleID], Option[IvyScala]), Boolean] = {
case (m1, Some(m2), Some(ivyScala)) if m2.name.endsWith("_" + ivyScala.scalaFullVersion) || m2.name.endsWith("_" + ivyScala.scalaBinaryVersion) =>
(m1.revision, m2.revision) match {
case (VersionNumber(ns1, ts1, es1), VersionNumber(ns2, ts2, es2)) =>
VersionNumber.SecondSegment.isCompatible(VersionNumber(ns1, ts1, es1), VersionNumber(ns2, ts2, es2))
case _ => false
}
}
lazy val guessSemVer: PartialFunction[(ModuleID, Option[ModuleID], Option[IvyScala]), Boolean] = {
case (m1, Some(m2), _) =>
(m1.revision, m2.revision) match {
case (VersionNumber(ns1, ts1, es1), VersionNumber(ns2, ts2, es2)) =>
VersionNumber.SemVer.isCompatible(VersionNumber(ns1, ts1, es1), VersionNumber(ns2, ts2, es2))
case _ => false
}
}
lazy val guessFalse: PartialFunction[(ModuleID, Option[ModuleID], Option[IvyScala]), Boolean] = {
case (_, _, _) => false
}
}
final class EvictionPair private[sbt] (
val organization: String,
val name: String,
val winner: Option[ModuleReport],
val evicteds: Seq[ModuleReport],
val includesDirect: Boolean,
val showCallers: Boolean) {
override def toString: String =
EvictionPair.evictionPairLines.showLines(this).mkString
}
object EvictionPair {
implicit val evictionPairLines: ShowLines[EvictionPair] = ShowLines { a: EvictionPair =>
val revs = a.evicteds map { _.module.revision }
val revsStr = if (revs.size <= 1) revs.mkString else "(" + revs.mkString(", ") + ")"
val winnerRev = (a.winner map { r =>
val callers: String =
if (a.showCallers)
r.callers match {
case Seq() => ""
case cs => (cs map { _.caller.toString }).mkString(" (caller: ", ", ", ")")
}
else ""
r.module.revision + callers
}).headOption map { " -> " + _ } getOrElse ""
Seq(s"\t* ${a.organization}:${a.name}:${revsStr}$winnerRev")
}
}
final class EvictionWarning private[sbt] (
val options: EvictionWarningOptions,
val scalaEvictions: Seq[EvictionPair],
val directEvictions: Seq[EvictionPair],
val transitiveEvictions: Seq[EvictionPair],
val allEvictions: Seq[EvictionPair]) {
def reportedEvictions: Seq[EvictionPair] = scalaEvictions ++ directEvictions ++ transitiveEvictions
}
object EvictionWarning {
def apply(module: IvySbt#Module, options: EvictionWarningOptions, report: UpdateReport, log: Logger): EvictionWarning = {
val evictions = buildEvictions(options, report)
processEvictions(module, options, evictions)
}
private[sbt] def buildEvictions(options: EvictionWarningOptions, report: UpdateReport): Seq[ModuleDetailReport] = {
val buffer: mutable.ListBuffer[ModuleDetailReport] = mutable.ListBuffer()
val confs = report.configurations filter { x => options.configStrings contains x.configuration }
confs flatMap { confReport =>
confReport.details map { detail =>
if ((detail.modules exists { _.evicted }) &&
!(buffer exists { x => (x.organization == detail.organization) && (x.name == detail.name) })) {
buffer += detail
}
}
}
buffer.toList.toVector
}
private[sbt] def isScalaArtifact(module: IvySbt#Module, organization: String, name: String): Boolean =
module.moduleSettings.ivyScala match {
case Some(s) =>
organization == s.scalaOrganization &&
(name == "scala-library") || (name == "scala-compiler")
case _ => false
}
private[sbt] def processEvictions(module: IvySbt#Module, options: EvictionWarningOptions, reports: Seq[ModuleDetailReport]): EvictionWarning = {
val directDependencies = module.moduleSettings match {
case x: InlineConfiguration => x.dependencies
case _ => Seq()
}
val pairs = reports map { detail =>
val evicteds = detail.modules filter { _.evicted }
val winner = (detail.modules filterNot { _.evicted }).headOption
val includesDirect: Boolean =
options.warnDirectEvictions &&
(directDependencies exists { dep =>
(detail.organization == dep.organization) && (detail.name == dep.name)
})
new EvictionPair(detail.organization, detail.name, winner, evicteds, includesDirect, options.showCallers)
}
val scalaEvictions: mutable.ListBuffer[EvictionPair] = mutable.ListBuffer()
val directEvictions: mutable.ListBuffer[EvictionPair] = mutable.ListBuffer()
val transitiveEvictions: mutable.ListBuffer[EvictionPair] = mutable.ListBuffer()
def guessCompatible(p: EvictionPair): Boolean =
p.evicteds forall { r =>
options.guessCompatible(r.module, p.winner map { _.module }, module.moduleSettings.ivyScala)
}
pairs foreach {
case p if isScalaArtifact(module, p.organization, p.name) =>
(module.moduleSettings.ivyScala, p.winner) match {
case (Some(s), Some(winner)) if ((s.scalaFullVersion != winner.module.revision) && options.warnScalaVersionEviction) =>
scalaEvictions += p
case _ =>
}
case p if p.includesDirect =>
if (!guessCompatible(p) && options.warnDirectEvictions) {
directEvictions += p
}
case p =>
if (!guessCompatible(p) && options.warnTransitiveEvictions) {
transitiveEvictions += p
}
}
new EvictionWarning(options, scalaEvictions.toList,
directEvictions.toList, transitiveEvictions.toList, pairs)
}
implicit val evictionWarningLines: ShowLines[EvictionWarning] = ShowLines { a: EvictionWarning =>
import ShowLines._
val out: mutable.ListBuffer[String] = mutable.ListBuffer()
if (!a.scalaEvictions.isEmpty) {
out += "Scala version was updated by one of library dependencies:"
out ++= (a.scalaEvictions flatMap { _.lines })
}
if (!a.directEvictions.isEmpty || !a.transitiveEvictions.isEmpty) {
out += "There may be incompatibilities among your library dependencies."
out += "Here are some of the libraries that were evicted:"
out ++= (a.directEvictions flatMap { _.lines })
out ++= (a.transitiveEvictions flatMap { _.lines })
}
if (!a.allEvictions.isEmpty && !a.reportedEvictions.isEmpty && !a.options.showCallers) {
out += "Run 'evicted' to see detailed eviction warnings"
}
out.toList
}
}

View File

@ -4,35 +4,124 @@
package sbt
import java.io.File
import java.{ util => ju }
import collection.mutable
import java.net.URL
import org.apache.ivy.core.{ module, report, resolve }
import module.descriptor.{ Artifact => IvyArtifact }
import module.id.ModuleRevisionId
import resolve.IvyNode
import module.descriptor.{ Artifact => IvyArtifact, License => IvyLicense }
import module.id.{ ModuleRevisionId, ModuleId => IvyModuleId }
import report.{ ArtifactDownloadReport, ConfigurationResolveReport, ResolveReport }
import resolve.{ IvyNode, IvyNodeCallers }
import IvyNodeCallers.{ Caller => IvyCaller }
object IvyRetrieve {
def reports(report: ResolveReport): Seq[ConfigurationResolveReport] =
report.getConfigurations map report.getConfigurationReport
def moduleReports(confReport: ConfigurationResolveReport): Seq[ModuleReport] =
for (revId <- confReport.getModuleRevisionIds.toArray collect { case revId: ModuleRevisionId => revId }) yield artifactReports(toModuleID(revId), confReport getDownloadReports revId)
for {
revId <- confReport.getModuleRevisionIds.toArray.toVector collect { case revId: ModuleRevisionId => revId }
} yield moduleRevisionDetail(confReport, confReport.getDependency(revId))
@deprecated("Internal only. No longer in use.", "0.13.6")
def artifactReports(mid: ModuleID, artReport: Seq[ArtifactDownloadReport]): ModuleReport =
{
val (resolved, missing) = artifacts(mid, artReport)
ModuleReport(mid, resolved, missing)
}
private[sbt] def artifacts(mid: ModuleID, artReport: Seq[ArtifactDownloadReport]): (Seq[(Artifact, File)], Seq[Artifact]) =
{
val missing = new mutable.ListBuffer[Artifact]
val resolved = new mutable.ListBuffer[(Artifact, File)]
for (r <- artReport) {
val file = r.getLocalFile
val fileOpt = Option(r.getLocalFile)
val art = toArtifact(r.getArtifact)
if (file eq null)
missing += art
else
resolved += ((art, file))
fileOpt match {
case Some(file) => resolved += ((art, file))
case None => missing += art
}
}
new ModuleReport(mid, resolved.toSeq, missing.toSeq)
(resolved.toSeq, missing.toSeq)
}
// We need this because current module report used as part of UpdateReport/ConfigurationReport contains
// only the revolved modules.
// Sometimes the entire module can be excluded via rules etc.
private[sbt] def details(confReport: ConfigurationResolveReport): Seq[ModuleDetailReport] = {
val dependencies = confReport.getModuleRevisionIds.toArray.toVector collect { case revId: ModuleRevisionId => revId }
val moduleIds = confReport.getModuleIds.toArray.toVector collect { case mId: IvyModuleId => mId }
def moduleDetail(mid: IvyModuleId): ModuleDetailReport = {
val deps = confReport.getNodes(mid).toArray.toVector collect { case node: IvyNode => node }
new ModuleDetailReport(mid.getOrganisation, mid.getName, deps map { moduleRevisionDetail(confReport, _) })
}
moduleIds map { moduleDetail }
}
private[sbt] def moduleRevisionDetail(confReport: ConfigurationResolveReport, dep: IvyNode): ModuleReport = {
def toExtraAttributes(ea: ju.Map[_, _]): Map[String, String] =
Map(ea.entrySet.toArray collect {
case entry: ju.Map.Entry[_, _] => (entry.getKey.toString, entry.getValue.toString)
}: _*)
def toCaller(caller: IvyCaller): Caller = {
val m = toModuleID(caller.getModuleRevisionId)
val callerConfigurations = caller.getCallerConfigurations.toArray.toVector
val extraAttributes = toExtraAttributes(caller.getDependencyDescriptor.getExtraAttributes)
new Caller(m, callerConfigurations, extraAttributes)
}
val revId = dep.getResolvedId
val moduleId = toModuleID(revId)
val branch = Option(revId.getBranch)
val (status, publicationDate, resolver, artifactResolver) = dep.isLoaded match {
case true =>
(Option(dep.getDescriptor.getStatus),
Some(new ju.Date(dep.getPublication)),
Option(dep.getModuleRevision.getResolver.getName),
Option(dep.getModuleRevision.getArtifactResolver.getName))
case _ => (None, None, None, None)
}
val (evicted, evictedData, evictedReason) = dep.isEvicted(confReport.getConfiguration) match {
case true =>
val ed = dep.getEvictedData(confReport.getConfiguration)
(true,
Some(Option(ed.getConflictManager) map { _.toString } getOrElse { "transitive" }),
Option(ed.getDetail))
case _ => (false, None, None)
}
val problem = dep.hasProblem match {
case true => Option(dep.getProblem.getMessage)
case _ => None
}
val mdOpt = for {
mr <- Option(dep.getModuleRevision)
md <- Option(mr.getDescriptor)
} yield md
val homepage = mdOpt match {
case Some(md) =>
Option(md.getHomePage)
case _ => None
}
val extraAttributes: Map[String, String] = toExtraAttributes(mdOpt match {
case Some(md) => md.getExtraAttributes
case _ => dep.getResolvedId.getExtraAttributes
})
val isDefault = Option(dep.getDescriptor) map { _.isDefault }
val configurations = dep.getConfigurations(confReport.getConfiguration).toArray.toList
val licenses: Seq[(String, Option[String])] = mdOpt match {
case Some(md) => md.getLicenses.toArray.toVector collect {
case lic: IvyLicense =>
(lic.getName, Option(lic.getUrl))
}
case _ => Nil
}
val callers = dep.getCallers(confReport.getConfiguration).toArray.toVector map { toCaller }
val (resolved, missing) = artifacts(moduleId, confReport getDownloadReports revId)
new ModuleReport(moduleId, resolved, missing, status, publicationDate, resolver, artifactResolver,
evicted, evictedData, evictedReason, problem, homepage, extraAttributes, isDefault, branch,
configurations, licenses, callers)
}
def evicted(confReport: ConfigurationResolveReport): Seq[ModuleID] =
confReport.getEvictedNodes.map(node => toModuleID(node.getId))
@ -50,7 +139,7 @@ object IvyRetrieve {
def updateStats(report: ResolveReport): UpdateStats =
new UpdateStats(report.getResolveTime, report.getDownloadTime, report.getDownloadSize, false)
def configurationReport(confReport: ConfigurationResolveReport): ConfigurationReport =
new ConfigurationReport(confReport.getConfiguration, moduleReports(confReport), evicted(confReport))
new ConfigurationReport(confReport.getConfiguration, moduleReports(confReport), details(confReport), evicted(confReport))
/**
* Tries to find Ivy graph path the from node to target.

View File

@ -4,6 +4,8 @@
package sbt
import java.io.File
import java.net.URL
import java.{ util => ju }
/**
* Provides information about dependency resolution.
@ -37,10 +39,22 @@ final class UpdateReport(val cachedDescriptor: File, val configurations: Seq[Con
/**
* Provides information about resolution of a single configuration.
* @param configuration the configuration this report is for.
* @param modules a seqeuence containing one report for each module resolved for this configuration.
* @param modules a sequence containing one report for each module resolved for this configuration.
* @param details a sequence containing one report for each org/name, which may or may not be part of the final resolution.
* @param evicted a sequence of evicted modules
*/
final class ConfigurationReport(val configuration: String, val modules: Seq[ModuleReport], val evicted: Seq[ModuleID]) {
override def toString = "\t" + configuration + ":\n" + modules.mkString + evicted.map("\t\t(EVICTED) " + _ + "\n").mkString
final class ConfigurationReport(
val configuration: String,
val modules: Seq[ModuleReport],
val details: Seq[ModuleDetailReport],
@deprecated("Use details instead to get better eviction info.", "0.13.6")
val evicted: Seq[ModuleID]) {
def this(configuration: String, modules: Seq[ModuleReport], evicted: Seq[ModuleID]) =
this(configuration, modules, Nil, evicted)
override def toString = s"\t$configuration:\n" +
(if (details.isEmpty) modules.mkString + evicted.map("\t\t(EVICTED) " + _ + "\n").mkString
else details.mkString)
/**
* All resolved modules for this configuration.
@ -50,25 +64,131 @@ final class ConfigurationReport(val configuration: String, val modules: Seq[Modu
private[this] def addConfiguration(mod: ModuleID): ModuleID = if (mod.configurations.isEmpty) mod.copy(configurations = Some(configuration)) else mod
def retrieve(f: (String, ModuleID, Artifact, File) => File): ConfigurationReport =
new ConfigurationReport(configuration, modules map { _.retrieve((mid, art, file) => f(configuration, mid, art, file)) }, evicted)
new ConfigurationReport(configuration, modules map { _.retrieve((mid, art, file) => f(configuration, mid, art, file)) }, details, evicted)
}
/**
* ModuleDetailReport represents an organization+name entry in Ivy resolution report.
* In sbt's terminology, "module" consists of organization, name, and version.
* In Ivy's, "module" means just organization and name, and the one including version numbers
* are called revisions.
*
* A sequence of ModuleDetailReport called details is newly added to ConfigurationReport, replacing evicted.
* (Note old evicted was just a seq of ModuleIDs).
* ModuleDetailReport groups the ModuleReport of both winners and evicted reports by their organization and name,
* which can be used to calculate detailed evction warning etc.
*/
final class ModuleDetailReport(
val organization: String,
val name: String,
val modules: Seq[ModuleReport]) {
override def toString: String = {
val details = modules map { _.detailReport }
s"\t$organization:$name\n${details.mkString}\n"
}
}
/**
* Provides information about the resolution of a module.
* This information is in the context of a specific configuration.
* @param module the `ModuleID` this report is for.
* @param artifacts the resolved artifacts for this module, paired with the File the artifact was retrieved to. This File may be in the
* @param artifacts the resolved artifacts for this module, paired with the File the artifact was retrieved to.
* @param missingArtifacts the missing artifacts for this module.
*/
final class ModuleReport(val module: ModuleID, val artifacts: Seq[(Artifact, File)], val missingArtifacts: Seq[Artifact]) {
override def toString =
{
val arts = artifacts.map(_.toString) ++ missingArtifacts.map(art => "(MISSING) " + art)
"\t\t" + module + ": " +
(if (arts.size <= 1) "" else "\n\t\t\t") + arts.mkString("\n\t\t\t") + "\n"
}
final class ModuleReport(
val module: ModuleID,
val artifacts: Seq[(Artifact, File)],
val missingArtifacts: Seq[Artifact],
val status: Option[String],
val publicationDate: Option[ju.Date],
val resolver: Option[String],
val artifactResolver: Option[String],
val evicted: Boolean,
val evictedData: Option[String],
val evictedReason: Option[String],
val problem: Option[String],
val homepage: Option[String],
val extraAttributes: Map[String, String],
val isDefault: Option[Boolean],
val branch: Option[String],
val configurations: Seq[String],
val licenses: Seq[(String, Option[String])],
val callers: Seq[Caller]) {
private[this] lazy val arts: Seq[String] = artifacts.map(_.toString) ++ missingArtifacts.map(art => "(MISSING) " + art)
override def toString: String = {
s"\t\t$module: " +
(if (arts.size <= 1) "" else "\n\t\t\t") + arts.mkString("\n\t\t\t") + "\n"
}
private[sbt] def detailReport: String =
s"\t\t- ${module.revision}\n" +
(if (arts.size <= 1) "" else arts.mkString("\t\t\t", "\n\t\t\t", "\n")) +
reportStr("status", status) +
reportStr("publicationDate", publicationDate map { _.toString }) +
reportStr("resolver", resolver) +
reportStr("artifactResolver", artifactResolver) +
reportStr("evicted", Some(evicted.toString)) +
reportStr("evictedData", evictedData) +
reportStr("evictedReason", evictedReason) +
reportStr("problem", problem) +
reportStr("homepage", homepage) +
reportStr("textraAttributes",
if (extraAttributes.isEmpty) None
else { Some(extraAttributes.toString) }) +
reportStr("isDefault", isDefault map { _.toString }) +
reportStr("branch", branch) +
reportStr("configurations",
if (configurations.isEmpty) None
else { Some(configurations.mkString(", ")) }) +
reportStr("licenses",
if (licenses.isEmpty) None
else { Some(licenses.mkString(", ")) }) +
reportStr("callers",
if (callers.isEmpty) None
else { Some(callers.mkString(", ")) })
private[sbt] def reportStr(key: String, value: Option[String]): String =
value map { x => s"\t\t\t$key: $x\n" } getOrElse ""
def retrieve(f: (ModuleID, Artifact, File) => File): ModuleReport =
new ModuleReport(module, artifacts.map { case (art, file) => (art, f(module, art, file)) }, missingArtifacts)
copy(artifacts = artifacts.map { case (art, file) => (art, f(module, art, file)) })
private[sbt] def copy(
module: ModuleID = module,
artifacts: Seq[(Artifact, File)] = artifacts,
missingArtifacts: Seq[Artifact] = missingArtifacts,
status: Option[String] = status,
publicationDate: Option[ju.Date] = publicationDate,
resolver: Option[String] = resolver,
artifactResolver: Option[String] = artifactResolver,
evicted: Boolean = evicted,
evictedData: Option[String] = evictedData,
evictedReason: Option[String] = evictedReason,
problem: Option[String] = problem,
homepage: Option[String] = homepage,
extraAttributes: Map[String, String] = extraAttributes,
isDefault: Option[Boolean] = isDefault,
branch: Option[String] = branch,
configurations: Seq[String] = configurations,
licenses: Seq[(String, Option[String])] = licenses,
callers: Seq[Caller] = callers): ModuleReport =
new ModuleReport(module, artifacts, missingArtifacts, status, publicationDate, resolver, artifactResolver,
evicted, evictedData, evictedReason, problem, homepage, extraAttributes, isDefault, branch, configurations, licenses, callers)
}
object ModuleReport {
def apply(module: ModuleID, artifacts: Seq[(Artifact, File)], missingArtifacts: Seq[Artifact]): ModuleReport =
new ModuleReport(module, artifacts, missingArtifacts, None, None, None, None,
false, None, None, None, None, Map(), None, None, Nil, Nil, Nil)
}
final class Caller(
val caller: ModuleID,
val callerConfigurations: Seq[String],
val callerExtraAttributes: Map[String, String]) {
override def toString: String =
s"$caller"
}
object UpdateReport {
implicit def richUpdateReport(report: UpdateReport): RichUpdateReport = new RichUpdateReport(report)
@ -101,15 +221,18 @@ object UpdateReport {
/** Constructs a new report that only contains files matching the specified filter.*/
def filter(f: DependencyFilter): UpdateReport =
moduleReportMap { (configuration, modReport) =>
import modReport._
val newArtifacts = artifacts filter { case (art, file) => f(configuration, module, art) }
val newMissing = missingArtifacts filter { art => f(configuration, module, art) }
new ModuleReport(module, newArtifacts, newMissing)
modReport.copy(
artifacts = modReport.artifacts filter { case (art, file) => f(configuration, modReport.module, art) },
missingArtifacts = modReport.missingArtifacts filter { art => f(configuration, modReport.module, art) }
)
}
def substitute(f: (String, ModuleID, Seq[(Artifact, File)]) => Seq[(Artifact, File)]): UpdateReport =
moduleReportMap { (configuration, modReport) =>
val newArtifacts = f(configuration, modReport.module, modReport.artifacts)
new ModuleReport(modReport.module, newArtifacts, Nil)
modReport.copy(
artifacts = f(configuration, modReport.module, modReport.artifacts),
missingArtifacts = Nil
)
}
def toSeq: Seq[(String, ModuleID, Artifact, File)] =
@ -120,8 +243,9 @@ object UpdateReport {
def addMissing(f: ModuleID => Seq[Artifact]): UpdateReport =
moduleReportMap { (configuration, modReport) =>
import modReport._
new ModuleReport(module, artifacts, (missingArtifacts ++ f(module)).distinct)
modReport.copy(
missingArtifacts = (modReport.missingArtifacts ++ f(modReport.module)).distinct
)
}
def moduleReportMap(f: (String, ModuleReport) => ModuleReport): UpdateReport =
@ -129,7 +253,7 @@ object UpdateReport {
val newConfigurations = report.configurations.map { confReport =>
import confReport._
val newModules = modules map { modReport => f(configuration, modReport) }
new ConfigurationReport(configuration, newModules, evicted)
new ConfigurationReport(configuration, newModules, details, evicted)
}
new UpdateReport(report.cachedDescriptor, newConfigurations, report.stats, report.stamps)
}

View File

@ -0,0 +1,147 @@
package sbt
final class VersionNumber private[sbt] (
val numbers: Seq[Long],
val tags: Seq[String],
val extras: Seq[String]) {
def _1: Option[Long] = get(0)
def _2: Option[Long] = get(1)
def _3: Option[Long] = get(2)
def _4: Option[Long] = get(3)
def get(idx: Int): Option[Long] =
if (size <= idx) None
else Some(numbers(idx))
def size: Int = numbers.size
private[this] val versionStr: String =
numbers.mkString(".") +
(tags match {
case Seq() => ""
case ts => "-" + ts.mkString("-")
}) +
extras.mkString("")
override def toString: String = versionStr
override def hashCode: Int =
numbers.hashCode * 41 * 41 +
tags.hashCode * 41 +
extras.hashCode
override def equals(o: Any): Boolean =
o match {
case v: VersionNumber => (this.numbers == v.numbers) && (this.tags == v.tags) && (this.extras == v.extras)
case _ => false
}
}
object VersionNumber {
/**
* @param numbers numbers delimited by a dot.
* @param tags string prefixed by a dash.
* @param any other strings at the end.
*/
def apply(numbers: Seq[Long], tags: Seq[String], extras: Seq[String]): VersionNumber =
new VersionNumber(numbers, tags, extras)
def apply(v: String): VersionNumber =
unapply(v) match {
case Some((ns, ts, es)) => VersionNumber(ns, ts, es)
case _ => sys.error(s"Invalid version number: $v")
}
def unapply(v: VersionNumber): Option[(Seq[Long], Seq[String], Seq[String])] =
Some((v.numbers, v.tags, v.extras))
def unapply(v: String): Option[(Seq[Long], Seq[String], Seq[String])] = {
def splitDot(s: String): Vector[Long] =
Option(s) match {
case Some(x) => x.split('.').toVector.filterNot(_ == "").map(_.toLong)
case _ => Vector()
}
def splitDash(s: String): Vector[String] =
Option(s) match {
case Some(x) => x.split('-').toVector.filterNot(_ == "")
case _ => Vector()
}
def splitPlus(s: String): Vector[String] =
Option(s) match {
case Some(x) => x.split('+').toVector.filterNot(_ == "").map("+" + _)
case _ => Vector()
}
val TaggedVersion = """(\d{1,14})([\.\d{1,14}]*)((?:-\w+)*)((?:\+.+)*)""".r
val NonSpaceString = """(\S+)""".r
v match {
case TaggedVersion(m, ns, ts, es) => Some((Vector(m.toLong) ++ splitDot(ns), splitDash(ts), splitPlus(es)))
case "" => None
case NonSpaceString(s) => Some((Vector(), Vector(), Vector(s)))
case _ => None
}
}
/**
* Strict. Checks everythig.
*/
object Strict extends VersionNumberCompatibility {
def name: String = "Strict"
def isCompatible(v1: VersionNumber, v2: VersionNumber): Boolean = v1 == v2
}
/**
* Semantic versioning. See http://semver.org/spec/v2.0.0.html
*/
object SemVer extends VersionNumberCompatibility {
def name: String = "Semantic Versioning"
def isCompatible(v1: VersionNumber, v2: VersionNumber): Boolean =
doIsCompat(v1, v2) || doIsCompat(v2, v1)
private[this] def doIsCompat(v1: VersionNumber, v2: VersionNumber): Boolean =
(v1, v2) match {
case (v1, v2) if (v1.size >= 2) && (v2.size >= 2) => // A normal version number MUST take the form X.Y.Z
(v1._1.get, v1._2.get, v1._3.getOrElse(0), v1.tags, v2._1.get, v2._2.get, v2._3.getOrElse(0), v2.tags) match {
case (0L, _, _, _, 0L, _, _, _) =>
// Major version zero (0.y.z) is for initial development. Anything may change at any time. The public API should not be considered stable.
equalsIgnoreExtra(v1, v2)
case (_, 0, 0, ts1, _, 0, 0, ts2) if (!ts1.isEmpty) || (!ts2.isEmpty) =>
// A pre-release version MAY be denoted by appending a hyphen and a series of dot separated identifiers
equalsIgnoreExtra(v1, v2)
case (x1, _, _, _, x2, _, _, _) =>
// Patch version Z (x.y.Z | x > 0) MUST be incremented if only backwards compatible bug fixes are introduced.
// Minor version Y (x.Y.z | x > 0) MUST be incremented if new, backwards compatible functionality is introduced
x1 == x2
case _ => equalsIgnoreExtra(v1, v2)
}
case _ => false
}
// Build metadata SHOULD be ignored when determining version precedence.
private[this] def equalsIgnoreExtra(v1: VersionNumber, v2: VersionNumber): Boolean =
(v1.numbers == v2.numbers) && (v1.tags == v2.tags)
}
/* A variant of SemVar that seems to be common among the Scala libraries.
* The second segment (y in x.y.z) increments breaks the binary compatibility even when x > 0.
* Also API comatibility is expected even when the first segment is zero.
*/
object SecondSegment extends VersionNumberCompatibility {
def name: String = "Second Segment Variant"
def isCompatible(v1: VersionNumber, v2: VersionNumber): Boolean =
doIsCompat(v1, v2) || doIsCompat(v2, v1)
private[this] def doIsCompat(v1: VersionNumber, v2: VersionNumber): Boolean =
(v1, v2) match {
case (v1, v2) if (v1.size >= 3) && (v2.size >= 3) => // A normal version number MUST take the form X.Y.Z
(v1._1.get, v1._2.get, v1._3.get, v1.tags, v2._1.get, v2._2.get, v2._3.get, v2.tags) match {
case (x1, y1, 0, ts1, x2, y2, 0, ts2) if (!ts1.isEmpty) || (!ts2.isEmpty) =>
// A pre-release version MAY be denoted by appending a hyphen and a series of dot separated identifiers
equalsIgnoreExtra(v1, v2)
case (x1, y1, _, _, x2, y2, _, _) =>
// Patch version Z (x.y.Z | x > 0) MUST be incremented if only backwards compatible changes are introduced.
(x1 == x2) && (y1 == y2)
case _ => equalsIgnoreExtra(v1, v2)
}
case _ => false
}
// Build metadata SHOULD be ignored when determining version precedence.
private[this] def equalsIgnoreExtra(v1: VersionNumber, v2: VersionNumber): Boolean =
(v1.numbers == v2.numbers) && (v1.tags == v2.tags)
}
}
trait VersionNumberCompatibility {
def name: String
def isCompatible(v1: VersionNumber, v2: VersionNumber): Boolean
}

View File

@ -0,0 +1,51 @@
package sbt
import Path._, Configurations._
import java.io.File
import org.specs2._
import cross.CrossVersionUtil
trait BaseIvySpecification extends Specification {
def currentBase: File = new File(".")
def currentTarget: File = currentBase / "target" / "ivyhome"
def defaultModuleId: ModuleID = ModuleID("com.example", "foo", "0.1.0", Some("compile"))
lazy val ivySbt = new IvySbt(mkIvyConfiguration)
lazy val log = Logger.Null
def module(moduleId: ModuleID, deps: Seq[ModuleID], scalaFullVersion: Option[String]): IvySbt#Module = {
val ivyScala = scalaFullVersion map { fv =>
new IvyScala(
scalaFullVersion = fv,
scalaBinaryVersion = CrossVersionUtil.binaryScalaVersion(fv),
configurations = Nil,
checkExplicit = true,
filterImplicit = false,
overrideScalaVersion = false)
}
val moduleSetting: ModuleSettings = InlineConfiguration(
module = moduleId,
moduleInfo = ModuleInfo("foo"),
dependencies = deps,
configurations = Seq(Compile, Test, Runtime),
ivyScala = ivyScala)
new ivySbt.Module(moduleSetting)
}
def mkIvyConfiguration: IvyConfiguration = {
val paths = new IvyPaths(currentBase, Some(currentTarget))
val rs = Seq(DefaultMavenRepository)
val other = Nil
val moduleConfs = Seq(ModuleConfiguration("*", DefaultMavenRepository))
val off = false
val check = Nil
val resCacheDir = currentTarget / "resolution-cache"
val uo = UpdateOptions()
new InlineIvyConfiguration(paths, rs, other, moduleConfs, off, None, check, Some(resCacheDir), uo, log)
}
def ivyUpdate(module: IvySbt#Module) = {
// IO.delete(currentTarget)
val config = new UpdateConfiguration(None, false, UpdateLogging.Full)
IvyActions.update(module, config, log)
}
}

View File

@ -0,0 +1,224 @@
package sbt
import org.specs2._
class EvictionWarningSpec extends BaseIvySpecification {
def is = s2"""
This is a specification to check the eviction warnings
Eviction of scala-library whose scalaVersion should
be detected $scalaVersionWarn1
not be detected if it's diabled $scalaVersionWarn2
print out message about the eviction $scalaVersionWarn3
print out message about the eviction with callers $scalaVersionWarn4
Including two (suspect) binary incompatible Java libraries to
direct dependencies should
be detected as eviction $javaLibWarn1
not be detected if it's disabled $javaLibWarn2
print out message about the eviction $javaLibWarn3
print out message about the eviction with callers $javaLibWarn4
Including two (suspect) binary compatible Java libraries to
direct dependencies should
not be detected as eviction $javaLibNoWarn1
print out message about the eviction $javaLibNoWarn2
Including two (suspect) transitively binary incompatible Java libraries to
direct dependencies should
be not detected as eviction $javaLibTransitiveWarn1
be detected if it's enabled $javaLibTransitiveWarn2
print out message about the eviction if it's enabled $javaLibTransitiveWarn3
Including two (suspect) binary incompatible Scala libraries to
direct dependencies should
be detected as eviction $scalaLibWarn1
print out message about the eviction $scalaLibWarn2
Including two (suspect) binary compatible Scala libraries to
direct dependencies should
not be detected as eviction $scalaLibNoWarn1
print out message about the eviction $scalaLibNoWarn2
Including two (suspect) transitively binary incompatible Scala libraries to
direct dependencies should
be not detected as eviction $scalaLibTransitiveWarn1
be detected if it's enabled $scalaLibTransitiveWarn2
print out message about the eviction if it's enabled $scalaLibTransitiveWarn3
"""
def akkaActor214 = ModuleID("com.typesafe.akka", "akka-actor", "2.1.4", Some("compile")) cross CrossVersion.binary
def akkaActor230 = ModuleID("com.typesafe.akka", "akka-actor", "2.3.0", Some("compile")) cross CrossVersion.binary
def akkaActor234 = ModuleID("com.typesafe.akka", "akka-actor", "2.3.4", Some("compile")) cross CrossVersion.binary
def scala2102 = ModuleID("org.scala-lang", "scala-library", "2.10.2", Some("compile"))
def scala2103 = ModuleID("org.scala-lang", "scala-library", "2.10.3", Some("compile"))
def scala2104 = ModuleID("org.scala-lang", "scala-library", "2.10.4", Some("compile"))
def commonsIo13 = ModuleID("commons-io", "commons-io", "1.3", Some("compile"))
def commonsIo14 = ModuleID("commons-io", "commons-io", "1.4", Some("compile"))
def commonsIo24 = ModuleID("commons-io", "commons-io", "2.4", Some("compile"))
def bnfparser10 = ModuleID("ca.gobits.bnf", "bnfparser", "1.0", Some("compile")) // uses commons-io 2.4
def unfilteredUploads080 = ModuleID("net.databinder", "unfiltered-uploads", "0.8.0", Some("compile")) cross CrossVersion.binary // uses commons-io 1.4
def bananaSesame04 = ModuleID("org.w3", "banana-sesame", "0.4", Some("compile")) cross CrossVersion.binary // uses akka-actor 2.1.4
def akkaRemote234 = ModuleID("com.typesafe.akka", "akka-remote", "2.3.4", Some("compile")) cross CrossVersion.binary // uses akka-actor 2.3.4
def defaultOptions = EvictionWarningOptions.default
import ShowLines._
def scalaVersionDeps = Seq(scala2102, akkaActor230)
def scalaVersionWarn1 = {
val m = module(defaultModuleId, scalaVersionDeps, Some("2.10.2"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions, report, log).scalaEvictions must have size (1)
}
def scalaVersionWarn2 = {
val m = module(defaultModuleId, scalaVersionDeps, Some("2.10.2"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions.withWarnScalaVersionEviction(false), report, log).scalaEvictions must have size (0)
}
def scalaVersionWarn3 = {
val m = module(defaultModuleId, scalaVersionDeps, Some("2.10.2"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions, report, log).lines must_==
List("Scala version was updated by one of library dependencies:",
"\t* org.scala-lang:scala-library:2.10.2 -> 2.10.3",
"Run 'evicted' to see detailed eviction warnings")
}
def scalaVersionWarn4 = {
val m = module(defaultModuleId, scalaVersionDeps, Some("2.10.2"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions.withShowCallers(true), report, log).lines must_==
List("Scala version was updated by one of library dependencies:",
"\t* org.scala-lang:scala-library:2.10.2 -> 2.10.3 (caller: com.typesafe.akka:akka-actor_2.10:2.3.0, com.example:foo:0.1.0)")
}
def javaLibDirectDeps = Seq(commonsIo14, commonsIo24)
def javaLibWarn1 = {
val m = module(defaultModuleId, javaLibDirectDeps, Some("2.10.3"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions, report, log).reportedEvictions must have size (1)
}
def javaLibWarn2 = {
val m = module(defaultModuleId, javaLibDirectDeps, Some("2.10.3"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions.withWarnDirectEvictions(false), report, log).reportedEvictions must have size (0)
}
def javaLibWarn3 = {
val m = module(defaultModuleId, javaLibDirectDeps, Some("2.10.3"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions, report, log).lines must_==
List("There may be incompatibilities among your library dependencies.",
"Here are some of the libraries that were evicted:",
"\t* commons-io:commons-io:1.4 -> 2.4",
"Run 'evicted' to see detailed eviction warnings")
}
def javaLibWarn4 = {
val m = module(defaultModuleId, javaLibDirectDeps, Some("2.10.3"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions.withShowCallers(true), report, log).lines must_==
List("There may be incompatibilities among your library dependencies.",
"Here are some of the libraries that were evicted:",
"\t* commons-io:commons-io:1.4 -> 2.4 (caller: com.example:foo:0.1.0)")
}
def javaLibNoWarn1 = {
val deps = Seq(commonsIo14, commonsIo13)
val m = module(defaultModuleId, deps, Some("2.10.3"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions, report, log).reportedEvictions must have size (0)
}
def javaLibNoWarn2 = {
val deps = Seq(commonsIo14, commonsIo13)
val m = module(defaultModuleId, deps, Some("2.10.3"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions, report, log).lines must_== Nil
}
def javaLibTransitiveDeps = Seq(unfilteredUploads080, bnfparser10)
def javaLibTransitiveWarn1 = {
val m = module(defaultModuleId, javaLibTransitiveDeps, Some("2.10.3"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions, report, log).reportedEvictions must have size (0)
}
def javaLibTransitiveWarn2 = {
val m = module(defaultModuleId, javaLibTransitiveDeps, Some("2.10.3"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions.withWarnTransitiveEvictions(true), report, log).reportedEvictions must have size (1)
}
def javaLibTransitiveWarn3 = {
val m = module(defaultModuleId, javaLibTransitiveDeps, Some("2.10.3"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions.withWarnTransitiveEvictions(true).withShowCallers(true), report, log).lines must_==
List("There may be incompatibilities among your library dependencies.",
"Here are some of the libraries that were evicted:",
"\t* commons-io:commons-io:1.4 -> 2.4 (caller: ca.gobits.bnf:bnfparser:1.0, net.databinder:unfiltered-uploads_2.10:0.8.0)")
}
def scalaLibWarn1 = {
val deps = Seq(scala2104, akkaActor214, akkaActor234)
val m = module(defaultModuleId, deps, Some("2.10.4"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions, report, log).reportedEvictions must have size (1)
}
def scalaLibWarn2 = {
val deps = Seq(scala2104, akkaActor214, akkaActor234)
val m = module(defaultModuleId, deps, Some("2.10.4"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions, report, log).lines must_==
List("There may be incompatibilities among your library dependencies.",
"Here are some of the libraries that were evicted:",
"\t* com.typesafe.akka:akka-actor_2.10:2.1.4 -> 2.3.4",
"Run 'evicted' to see detailed eviction warnings")
}
def scalaLibNoWarn1 = {
val deps = Seq(scala2104, akkaActor230, akkaActor234)
val m = module(defaultModuleId, deps, Some("2.10.4"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions, report, log).reportedEvictions must have size (0)
}
def scalaLibNoWarn2 = {
val deps = Seq(scala2104, akkaActor230, akkaActor234)
val m = module(defaultModuleId, deps, Some("2.10.4"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions, report, log).lines must_== Nil
}
def scalaLibTransitiveDeps = Seq(scala2104, bananaSesame04, akkaRemote234)
def scalaLibTransitiveWarn1 = {
val m = module(defaultModuleId, scalaLibTransitiveDeps, Some("2.10.4"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions, report, log).reportedEvictions must have size (0)
}
def scalaLibTransitiveWarn2 = {
val m = module(defaultModuleId, scalaLibTransitiveDeps, Some("2.10.4"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions.withWarnTransitiveEvictions(true), report, log).reportedEvictions must have size (1)
}
def scalaLibTransitiveWarn3 = {
val m = module(defaultModuleId, scalaLibTransitiveDeps, Some("2.10.4"))
val report = ivyUpdate(m)
EvictionWarning(m, defaultOptions.withWarnTransitiveEvictions(true).withShowCallers(true), report, log).lines must_==
List("There may be incompatibilities among your library dependencies.",
"Here are some of the libraries that were evicted:",
"\t* com.typesafe.akka:akka-actor_2.10:2.1.4 -> 2.3.4 (caller: com.typesafe.akka:akka-remote_2.10:2.3.4, org.w3:banana-sesame_2.10:0.4, org.w3:banana-rdf_2.10:0.4)")
}
}

View File

@ -0,0 +1,122 @@
package sbt
import org.specs2._
class VersionNumberSpec extends Specification {
def is = s2"""
This is a specification to check the version number parsing.
1 should
${beParsedAs("1", Seq(1), Seq(), Seq())}
${breakDownTo("1", Some(1))}
1.0 should
${beParsedAs("1.0", Seq(1, 0), Seq(), Seq())}
${breakDownTo("1.0", Some(1), Some(0))}
1.0.0 should
${beParsedAs("1.0.0", Seq(1, 0, 0), Seq(), Seq())}
${breakDownTo("1.0.0", Some(1), Some(0), Some(0))}
${beSemVerCompatWith("1.0.0", "1.0.1")}
${beSemVerCompatWith("1.0.0", "1.1.1")}
${notBeSemVerCompatWith("1.0.0", "2.0.0")}
${notBeSemVerCompatWith("1.0.0", "1.0.0-M1")}
${beSecSegCompatWith("1.0.0", "1.0.1")}
${notBeSecSegCompatWith("1.0.0", "1.1.1")}
${notBeSecSegCompatWith("1.0.0", "2.0.0")}
${notBeSecSegCompatWith("1.0.0", "1.0.0-M1")}
1.0.0.0 should
${beParsedAs("1.0.0.0", Seq(1, 0, 0, 0), Seq(), Seq())}
${breakDownTo("1.0.0.0", Some(1), Some(0), Some(0), Some(0))}
0.12.0 should
${beParsedAs("0.12.0", Seq(0, 12, 0), Seq(), Seq())}
${breakDownTo("0.12.0", Some(0), Some(12), Some(0))}
${notBeSemVerCompatWith("0.12.0", "0.12.0-RC1")}
${notBeSemVerCompatWith("0.12.0", "0.12.1")}
${notBeSemVerCompatWith("0.12.0", "0.12.1-M1")}
${notBeSecSegCompatWith("0.12.0", "0.12.0-RC1")}
${beSecSegCompatWith("0.12.0", "0.12.1")}
${beSecSegCompatWith("0.12.0", "0.12.1-M1")}
0.1.0-SNAPSHOT should
${beParsedAs("0.1.0-SNAPSHOT", Seq(0, 1, 0), Seq("SNAPSHOT"), Seq())}
${beSemVerCompatWith("0.1.0-SNAPSHOT", "0.1.0-SNAPSHOT")}
${notBeSemVerCompatWith("0.1.0-SNAPSHOT", "0.1.0")}
${beSemVerCompatWith("0.1.0-SNAPSHOT", "0.1.0-SNAPSHOT+001")}
${beSecSegCompatWith("0.1.0-SNAPSHOT", "0.1.0-SNAPSHOT")}
${notBeSecSegCompatWith("0.1.0-SNAPSHOT", "0.1.0")}
${beSecSegCompatWith("0.1.0-SNAPSHOT", "0.1.0-SNAPSHOT+001")}
0.1.0-M1 should
${beParsedAs("0.1.0-M1", Seq(0, 1, 0), Seq("M1"), Seq())}
0.1.0-RC1 should
${beParsedAs("0.1.0-RC1", Seq(0, 1, 0), Seq("RC1"), Seq())}
0.1.0-MSERVER-1 should
${beParsedAs("0.1.0-MSERVER-1", Seq(0, 1, 0), Seq("MSERVER", "1"), Seq())}
2.10.4-20140115-000117-b3a-sources should
${beParsedAs("2.10.4-20140115-000117-b3a-sources", Seq(2, 10, 4), Seq("20140115", "000117", "b3a", "sources"), Seq())}
${beSemVerCompatWith("2.10.4-20140115-000117-b3a-sources", "2.0.0")}
${notBeSecSegCompatWith("2.10.4-20140115-000117-b3a-sources", "2.0.0")}
20140115000117-b3a-sources should
${beParsedAs("20140115000117-b3a-sources", Seq(20140115000117L), Seq("b3a", "sources"), Seq())}
1.0.0-alpha+001+002 should
${beParsedAs("1.0.0-alpha+001+002", Seq(1, 0, 0), Seq("alpha"), Seq("+001", "+002"))}
non.space.!?string should
${beParsedAs("non.space.!?string", Seq(), Seq(), Seq("non.space.!?string"))}
space !?string should
${beParsedAsError("space !?string")}
blank string should
${beParsedAsError("")}
"""
def beParsedAs(s: String, ns: Seq[Long], ts: Seq[String], es: Seq[String]) =
s match {
case VersionNumber(ns1, ts1, es1) if (ns1 == ns && ts1 == ts && es1 == es) =>
(VersionNumber(ns, ts, es).toString must_== s) and
(VersionNumber(ns, ts, es) == VersionNumber(ns, ts, es))
case VersionNumber(ns1, ts1, es1) =>
sys.error(s"$ns1, $ts1, $es1")
}
def breakDownTo(s: String, major: Option[Long], minor: Option[Long] = None,
patch: Option[Long] = None, buildNumber: Option[Long] = None) =
s match {
case VersionNumber(ns, ts, es) =>
val v = VersionNumber(ns, ts, es)
(v._1 must_== major) and
(v._2 must_== minor) and
(v._3 must_== patch) and
(v._4 must_== buildNumber)
}
def beParsedAsError(s: String) =
s match {
case VersionNumber(ns1, ts1, es1) => failure
case _ => success
}
def beSemVerCompatWith(v1: String, v2: String) =
VersionNumber.SemVer.isCompatible(VersionNumber(v1), VersionNumber(v2)) must_== true
def notBeSemVerCompatWith(v1: String, v2: String) =
VersionNumber.SemVer.isCompatible(VersionNumber(v1), VersionNumber(v2)) must_== false
def beSecSegCompatWith(v1: String, v2: String) =
VersionNumber.SecondSegment.isCompatible(VersionNumber(v1), VersionNumber(v2)) must_== true
def notBeSecSegCompatWith(v1: String, v2: String) =
VersionNumber.SecondSegment.isCompatible(VersionNumber(v1), VersionNumber(v2)) must_== false
}