diff --git a/ivy/src/main/scala/sbt/ConflictWarning.scala b/ivy/src/main/scala/sbt/ConflictWarning.scala index 7c5c9a918..6e6b8a37d 100644 --- a/ivy/src/main/scala/sbt/ConflictWarning.scala +++ b/ivy/src/main/scala/sbt/ConflictWarning.scala @@ -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 diff --git a/ivy/src/main/scala/sbt/EvictionWarning.scala b/ivy/src/main/scala/sbt/EvictionWarning.scala new file mode 100644 index 000000000..bacebdee3 --- /dev/null +++ b/ivy/src/main/scala/sbt/EvictionWarning.scala @@ -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 + } +} diff --git a/ivy/src/main/scala/sbt/IvyRetrieve.scala b/ivy/src/main/scala/sbt/IvyRetrieve.scala index e863d6723..b3bf04563 100644 --- a/ivy/src/main/scala/sbt/IvyRetrieve.scala +++ b/ivy/src/main/scala/sbt/IvyRetrieve.scala @@ -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. diff --git a/ivy/src/main/scala/sbt/UpdateReport.scala b/ivy/src/main/scala/sbt/UpdateReport.scala index 71d58390b..f26f3fc10 100644 --- a/ivy/src/main/scala/sbt/UpdateReport.scala +++ b/ivy/src/main/scala/sbt/UpdateReport.scala @@ -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) } diff --git a/ivy/src/main/scala/sbt/VersionNumber.scala b/ivy/src/main/scala/sbt/VersionNumber.scala new file mode 100644 index 000000000..7db8c8fef --- /dev/null +++ b/ivy/src/main/scala/sbt/VersionNumber.scala @@ -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 +} diff --git a/ivy/src/test/scala/BaseIvySpecification.scala b/ivy/src/test/scala/BaseIvySpecification.scala new file mode 100644 index 000000000..e754903fe --- /dev/null +++ b/ivy/src/test/scala/BaseIvySpecification.scala @@ -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) + } +} diff --git a/ivy/src/test/scala/EvictionWarningSpec.scala b/ivy/src/test/scala/EvictionWarningSpec.scala new file mode 100644 index 000000000..171d60bd5 --- /dev/null +++ b/ivy/src/test/scala/EvictionWarningSpec.scala @@ -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)") + } +} diff --git a/ivy/src/test/scala/VersionNumberSpec.scala b/ivy/src/test/scala/VersionNumberSpec.scala new file mode 100644 index 000000000..7b836050f --- /dev/null +++ b/ivy/src/test/scala/VersionNumberSpec.scala @@ -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 +}