diff --git a/ivy/src/main/scala/sbt/ConvertResolver.scala b/ivy/src/main/scala/sbt/ConvertResolver.scala index 58ba6d4ac..74c5c119c 100644 --- a/ivy/src/main/scala/sbt/ConvertResolver.scala +++ b/ivy/src/main/scala/sbt/ConvertResolver.scala @@ -10,13 +10,91 @@ import core.module.id.ModuleRevisionId import core.module.descriptor.DependencyDescriptor import core.resolve.ResolveData import core.settings.IvySettings -import plugins.resolver.{BasicResolver, DependencyResolver, IBiblioResolver} +import plugins.resolver.{BasicResolver, DependencyResolver, IBiblioResolver, RepositoryResolver} import plugins.resolver.{AbstractPatternsBasedResolver, AbstractSshBasedResolver, FileSystemResolver, SFTPResolver, SshResolver, URLResolver} import plugins.repository.url.{URLRepository => URLRepo} import plugins.repository.file.{FileRepository => FileRepo, FileResource} +import java.io.File +import org.apache.ivy.util.ChecksumHelper +import org.apache.ivy.core.module.descriptor.{Artifact=>IArtifact} + private object ConvertResolver { + /** This class contains all the reflective lookups used in the + * checksum-friendly URL publishing shim. + */ + private object ChecksumFriendlyURLResolver { + // TODO - When we dump JDK6 support we can remove this hackery + // import java.lang.reflect.AccessibleObject + type AccessibleObject = { + def setAccessible(value: Boolean): Unit + } + private def reflectiveLookup[A <: AccessibleObject](f: Class[_] => A): Option[A] = + try { + val cls = classOf[RepositoryResolver] + val thing = f(cls) + import scala.language.reflectiveCalls + thing.setAccessible(true) + Some(thing) + } catch { + case (_: java.lang.NoSuchFieldException) | + (_: java.lang.SecurityException) | + (_: java.lang.NoSuchMethodException) => None + } + private val signerNameField: Option[java.lang.reflect.Field] = + reflectiveLookup(_.getDeclaredField("signerName")) + private val putChecksumMethod: Option[java.lang.reflect.Method] = + reflectiveLookup(_.getDeclaredMethod("putChecksum", + classOf[IArtifact], classOf[File], classOf[String], + classOf[Boolean], classOf[String])) + private val putSignatureMethod: Option[java.lang.reflect.Method] = + reflectiveLookup(_.getDeclaredMethod("putSignature", + classOf[IArtifact], classOf[File], classOf[String], + classOf[Boolean])) + } + /** + * The default behavior of ivy's overwrite flags ignores the fact that a lot of repositories + * will autogenerate checksums *for* an artifact if it doesn't already exist. Therefore + * if we succeed in publishing an artifact, we need to just blast the checksums in place. + * This acts as a "shim" on RepositoryResolvers so that we can hook our methods into + * both the IBiblioResolver + URLResolver without having to duplicate the code in two + * places. However, this does mean our use of reflection is awesome. + * + * TODO - See about contributing back to ivy. + */ + private trait ChecksumFriendlyURLResolver extends RepositoryResolver { + import ChecksumFriendlyURLResolver._ + private def signerName: String = signerNameField match { + case Some(field) => field.get(this).asInstanceOf[String] + case None => null + } + override protected def put(artifact: IArtifact, src: File, dest: String, overwrite: Boolean): Unit = { + // verify the checksum algorithms before uploading artifacts! + val checksums = getChecksumAlgorithms() + val repository = getRepository() + for { + checksum <- checksums + if !ChecksumHelper.isKnownAlgorithm(checksum) + } throw new IllegalArgumentException("Unknown checksum algorithm: " + checksum) + repository.put(artifact, src, dest, overwrite); + // Fix for sbt#1156 - Artifactory will auto-generate MD5/sha1 files, so + // we need to overwrite what it has. + for (checksum <- checksums) { + putChecksumMethod match { + case Some(method) => method.invoke(this, artifact, src, dest, true: java.lang.Boolean, checksum) + case None => // TODO - issue warning? + } + } + if (signerName != null) { + putSignatureMethod match { + case None => () + case Some(method) => method.invoke(artifact, src, dest, true: java.lang.Boolean) + } + } + } + } + /** Converts the given sbt resolver into an Ivy resolver..*/ def apply(r: Resolver, settings: IvySettings, log: Logger) = { @@ -25,7 +103,7 @@ private object ConvertResolver case repo: MavenRepository => { val pattern = Collections.singletonList(Resolver.resolvePattern(repo.root, Resolver.mavenStyleBasePattern)) - final class PluginCapableResolver extends IBiblioResolver with DescriptorRequired { + final class PluginCapableResolver extends IBiblioResolver with ChecksumFriendlyURLResolver with DescriptorRequired { def setPatterns() { // done this way for access to protected methods. setArtifactPatterns(pattern) setIvyPatterns(pattern) @@ -61,7 +139,13 @@ private object ConvertResolver } case repo: FileRepository => { - val resolver = new FileSystemResolver with DescriptorRequired + val resolver = new FileSystemResolver with DescriptorRequired { + // Workaround for #1156 + // Temporarily in sbt 0.13.x we deprecate overwriting + // in local files for non-changing revisions. + // This will be fully enforced in sbt 1.0. + setRepository(new WarnOnOverwriteFileRepo()) + } resolver.setName(repo.name) initializePatterns(resolver, repo.patterns, settings) import repo.configuration.{isLocal, isTransactional} @@ -71,7 +155,7 @@ private object ConvertResolver } case repo: URLRepository => { - val resolver = new URLResolver with DescriptorRequired + val resolver = new URLResolver with ChecksumFriendlyURLResolver with DescriptorRequired resolver.setName(repo.name) initializePatterns(resolver, repo.patterns, settings) resolver @@ -135,7 +219,7 @@ private object ConvertResolver /** A custom Ivy URLRepository that returns FileResources for file URLs. * This allows using the artifacts from the Maven local repository instead of copying them to the Ivy cache. */ private[this] final class LocalIfFileRepo extends URLRepo { - private[this] val repo = new FileRepo + private[this] val repo = new WarnOnOverwriteFileRepo() override def getResource(source: String) = { val url = new URL(source) if(url.getProtocol == IO.FileScheme) @@ -144,4 +228,16 @@ private object ConvertResolver super.getResource(source) } } + + private[this] final class WarnOnOverwriteFileRepo extends FileRepo() { + override def put(source: java.io.File, destination: String, overwrite: Boolean): Unit = { + try super.put(source, destination, overwrite) + catch { + case e: java.io.IOException if e.getMessage.contains("destination already exists") => + import org.apache.ivy.util.Message + Message.warn(s"Attempting to overwrite $destination\n\tThis usage is deprecated and will be removed in sbt 1.0.") + super.put(source, destination, true) + } + } + } } diff --git a/ivy/src/main/scala/sbt/CustomPomParser.scala b/ivy/src/main/scala/sbt/CustomPomParser.scala index 7023ab8d9..871c1f07c 100644 --- a/ivy/src/main/scala/sbt/CustomPomParser.scala +++ b/ivy/src/main/scala/sbt/CustomPomParser.scala @@ -43,7 +43,7 @@ object CustomPomParser val JarPackagings = Set("eclipse-plugin", "hk2-jar", "orbit") val default = new CustomPomParser(PomModuleDescriptorParser.getInstance, defaultTransform) - private[this] val TransformedHashKey = "sbtTransformHash" + private[this] val TransformedHashKey = "e:sbtTransformHash" // A hash of the parameters transformation is based on. // If a descriptor has a different hash, we need to retransform it. private[this] val TransformHash: String = hash((unqualifiedKeys ++ JarPackagings).toSeq.sorted) @@ -57,8 +57,14 @@ object CustomPomParser private[this] def transformedByThisVersion(md: ModuleDescriptor): Boolean = { + val oldTransformedHashKey = "sbtTransformHash" val extraInfo = md.getExtraInfo - extraInfo != null && extraInfo.get(TransformedHashKey) == TransformHash + // sbt 0.13.1 used "sbtTransformHash" instead of "e:sbtTransformHash" until #1192 so read both + Option(extraInfo).isDefined && + ((Option(extraInfo get TransformedHashKey) orElse Option(extraInfo get oldTransformedHashKey)) match { + case Some(TransformHash) => true + case _ => false + }) } private[this] def defaultTransformImpl(parser: ModuleDescriptorParser, md: ModuleDescriptor): ModuleDescriptor = diff --git a/ivy/src/main/scala/sbt/Ivy.scala b/ivy/src/main/scala/sbt/Ivy.scala index e1dca53ae..a6519c6bc 100644 --- a/ivy/src/main/scala/sbt/Ivy.scala +++ b/ivy/src/main/scala/sbt/Ivy.scala @@ -7,7 +7,6 @@ import Resolver.PluginPattern import java.io.File import java.net.URI -import java.text.ParseException import java.util.concurrent.Callable import java.util.{Collection, Collections => CS} import CS.singleton @@ -24,9 +23,7 @@ import core.settings.IvySettings import plugins.latest.LatestRevisionStrategy import plugins.matcher.PatternMatcher import plugins.parser.m2.PomModuleDescriptorParser -import plugins.repository.ResourceDownloader import plugins.resolver.{ChainResolver, DependencyResolver} -import plugins.resolver.util.ResolvedResource import util.{Message, MessageLogger} import util.extendable.ExtendableItem @@ -99,6 +96,8 @@ final class IvySbt(val configuration: IvyConfiguration) def withIvy[T](log: MessageLogger)(f: Ivy => T): T = withDefaultLogger(log) { + // See #429 - We always insert a helper authenticator here which lets us get more useful authentication errors. + ivyint.ErrorMessageAuthenticator.install() ivy.pushContext() ivy.getLoggerEngine.pushLogger(log) try { f(ivy) } @@ -356,41 +355,8 @@ private object IvySbt case pr: ProjectResolver => true case _ => false } - /** This is overridden to delete outofdate artifacts of changing modules that are not listed in the metadata. - * This occurs for artifacts with classifiers, for example. */ - @throws(classOf[ParseException]) - override def cacheModuleDescriptor(resolver: DependencyResolver, mdRef: ResolvedResource, dd: DependencyDescriptor, moduleArtifact: IArtifact, downloader: ResourceDownloader, options: CacheMetadataOptions): ResolvedModuleRevision = - { - val rmrRaw = super.cacheModuleDescriptor(null, mdRef, dd, moduleArtifact, downloader, options) - val rmr = resetArtifactResolver(rmrRaw) - val mrid = moduleArtifact.getModuleRevisionId - def shouldClear(): Boolean = rmr != null && - ( (rmr.getReport != null && rmr.getReport.isSearched && isChanging(dd, mrid)) || - isProjectResolver(rmr.getResolver) ) - // only handle changing modules whose metadata actually changed. - // Typically, the publication date in the metadata has to change to get here. - if(shouldClear()) { - // this is the locally cached metadata as originally retrieved (e.g. the pom) - val original = rmr.getReport.getOriginalLocalFile - if(original != null) { - // delete all files in subdirectories that are older than the original metadata file's publication date - // The publication date is used because the metadata will be redownloaded for changing files, - // so the last modified time changes, but the publication date doesn't - val pubDate = rmrRaw.getPublicationDate - val lm = if(pubDate eq null) original.lastModified else pubDate.getTime - val indirectFiles = PathFinder(original.getParentFile).*(DirectoryFilter).**(-DirectoryFilter).get.toList - val older = indirectFiles.filter(f => f.lastModified < lm).toList - Message.verbose("Deleting additional old artifacts from cache for changed module " + mrid + older.mkString(":\n\t", "\n\t", "")) - IO.delete(older) - } - } - rmr - } // ignore the original resolver wherever possible to avoid issues like #704 override def saveResolvers(descriptor: ModuleDescriptor, metadataResolverName: String, artifactResolverName: String) {} - - def isChanging(dd: DependencyDescriptor, requestedRevisionId: ModuleRevisionId): Boolean = - !localOnly && (dd.isChanging || requestedRevisionId.getRevision.contains("-SNAPSHOT")) } manager.setArtifactPattern(PluginPattern + manager.getArtifactPattern) manager.setDataFilePattern(PluginPattern + manager.getDataFilePattern) diff --git a/ivy/src/main/scala/sbt/IvyActions.scala b/ivy/src/main/scala/sbt/IvyActions.scala index f3ac22c82..0ad3376d3 100644 --- a/ivy/src/main/scala/sbt/IvyActions.scala +++ b/ivy/src/main/scala/sbt/IvyActions.scala @@ -16,7 +16,11 @@ import core.resolve.ResolveOptions import plugins.resolver.{BasicResolver, DependencyResolver} final class DeliverConfiguration(val deliverIvyPattern: String, val status: String, val configurations: Option[Seq[Configuration]], val logging: UpdateLogging.Value) -final class PublishConfiguration(val ivyFile: Option[File], val resolverName: String, val artifacts: Map[Artifact, File], val checksums: Seq[String], val logging: UpdateLogging.Value) +final class PublishConfiguration(val ivyFile: Option[File], val resolverName: String, val artifacts: Map[Artifact, File], val checksums: Seq[String], val logging: UpdateLogging.Value, + val overwrite: Boolean) { + def this(ivyFile: Option[File], resolverName: String, artifacts: Map[Artifact, File], checksums: Seq[String], logging: UpdateLogging.Value) = + this(ivyFile, resolverName, artifacts, checksums, logging, false) +} final class UpdateConfiguration(val retrieve: Option[RetrieveConfiguration], val missingOk: Boolean, val logging: UpdateLogging.Value) final class RetrieveConfiguration(val retrieveDirectory: File, val outputPattern: String) @@ -86,11 +90,11 @@ object IvyActions import configuration._ module.withModule(log) { case (ivy, md, default) => val resolver = ivy.getSettings.getResolver(resolverName) - if(resolver eq null) error("Undefined resolver '" + resolverName + "'") + if(resolver eq null) sys.error("Undefined resolver '" + resolverName + "'") val ivyArtifact = ivyFile map { file => (MDArtifact.newIvyArtifact(md), file) } val cross = crossVersionMap(module.moduleSettings) - val as = mapArtifacts(md, cross, artifacts) ++ ivyArtifact.toList - withChecksums(resolver, checksums) { publish(md, as, resolver, overwrite = true) } + val as = mapArtifacts(md, cross, artifacts) ++ ivyArtifact.toSeq + withChecksums(resolver, checksums) { publish(md, as, resolver, overwrite = overwrite) } } } private[this] def withChecksums[T](resolver: DependencyResolver, checksums: Seq[String])(act: => T): T = diff --git a/ivy/src/main/scala/sbt/MakePom.scala b/ivy/src/main/scala/sbt/MakePom.scala index 5e49243d3..16ec28333 100644 --- a/ivy/src/main/scala/sbt/MakePom.scala +++ b/ivy/src/main/scala/sbt/MakePom.scala @@ -188,7 +188,7 @@ class MakePom(val log: Logger) {mrid.getOrganisation} {mrid.getName} - {mrid.getRevision} + {makeDependencyVersion(mrid.getRevision)} { scopeElem(scope) } { optionalElem(optional) } { classifierElem(classifier) } @@ -197,6 +197,44 @@ class MakePom(val log: Logger) } + + + def makeDependencyVersion(revision: String): String = { + def plusRange(s:String, shift:Int = 0) = { + def pow(i:Int):Int = if (i>0) 10 * pow(i-1) else 1 + val (prefixVersion, lastVersion) = (s+"0"*shift).reverse.split("\\.",2) match { + case Array(revLast,revRest) => + ( revRest.reverse + ".", revLast.reverse ) + case Array(revLast) => ("", revLast.reverse) + } + val lastVersionInt = lastVersion.toInt + s"[${prefixVersion}${lastVersion},${prefixVersion}${lastVersionInt+pow(shift)})" + } + val startSym=Set(']','[','(') + val stopSym=Set(']','[',')') + try { + if (revision endsWith ".+") { + plusRange(revision.substring(0,revision.length-2)) + } else if (revision endsWith "+") { + val base = revision.take(revision.length-1) + // This is a heuristic. Maven just doesn't support Ivy's notions of 1+, so + // we assume version ranges never go beyond 5 siginificant digits. + (0 to 5).map(plusRange(base,_)).mkString(",") + } else if (startSym(revision(0)) && stopSym(revision(revision.length-1))) { + val start = revision(0) + val stop = revision(revision.length-1) + val mid = revision.substring(1,revision.length-1) + (if (start == ']') "(" else start) + mid + (if (stop == '[') ")" else stop) + } else revision + } catch { + case e: NumberFormatException => + // TODO - if the version doesn't meet our expectations, maybe we just issue a hard + // error instead of softly ignoring the attempt to rewrite. + //sys.error(s"Could not fix version [$revision] into maven style version") + revision + } + } + @deprecated("No longer used and will be removed.", "0.12.1") def classifier(dependency: DependencyDescriptor, includeTypes: Set[String]): NodeSeq = { diff --git a/ivy/src/main/scala/sbt/ivyint/ErrorMessageAuthenticator.scala b/ivy/src/main/scala/sbt/ivyint/ErrorMessageAuthenticator.scala new file mode 100644 index 000000000..3d0d174d8 --- /dev/null +++ b/ivy/src/main/scala/sbt/ivyint/ErrorMessageAuthenticator.scala @@ -0,0 +1,128 @@ +package sbt +package ivyint + +import java.lang.reflect.Field +import java.lang.reflect.Method +import java.net.Authenticator +import java.net.PasswordAuthentication +import org.apache.ivy.util.Credentials +import org.apache.ivy.util.Message +import org.apache.ivy.util.url.IvyAuthenticator +import org.apache.ivy.util.url.CredentialsStore + +/** + * Helper to install an Authenticator that works with the IvyAuthenticator to provide better error messages when + * credentials don't line up. + */ +object ErrorMessageAuthenticator { + private var securityWarningLogged = false + + private def originalAuthenticator: Option[Authenticator] = { + try { + val f = classOf[Authenticator].getDeclaredField("theAuthenticator"); + f.setAccessible(true); + Option(f.get(null).asInstanceOf[Authenticator]) + } catch { + // TODO - Catch more specific errors. + case t: Throwable => + Message.debug("Error occurred while getting the original authenticator: " + t.getMessage) + None + } + } + + private lazy val ivyOriginalField = { + val field = classOf[IvyAuthenticator].getDeclaredField("original") + field.setAccessible(true) + field + } + // Attempts to get the original authenticator form the ivy class or returns null. + private def installIntoIvy(ivy: IvyAuthenticator): Option[Authenticator] = { + // Here we install ourselves as the IvyAuthenticator's default so we get called AFTER Ivy has a chance to run. + def installIntoIvyImpl(original: Option[Authenticator]): Unit = { + val newOriginal = new ErrorMessageAuthenticator(original) + ivyOriginalField.set(ivy, newOriginal) + } + + try Option(ivyOriginalField.get(ivy).asInstanceOf[Authenticator]) match { + case Some(alreadyThere: ErrorMessageAuthenticator) => // We're already installed, no need to do the work again. + case originalOpt => installIntoIvyImpl(originalOpt) + } catch { + case t: Throwable => + Message.debug("Error occurred will trying to install debug messages into Ivy Authentication" + t.getMessage) + } + Some(ivy) + } + + /** Installs the error message authenticator so we have nicer error messages when using java's URL for downloading. */ + def install() { + // Actually installs the error message authenticator. + def doInstall(original: Option[Authenticator]): Unit = + try Authenticator.setDefault(new ErrorMessageAuthenticator(original)) + catch { + case e: SecurityException if !securityWarningLogged => + securityWarningLogged = true; + Message.warn("Not enough permissions to set the ErorrMessageAuthenticator. " + + "Helpful debug messages disabled!"); + } + // We will try to use the original authenticator as backup authenticator. + // Since there is no getter available, so try to use some reflection to + // obtain it. If that doesn't work, assume there is no original authenticator + def doInstallIfIvy(original: Option[Authenticator]): Unit = + original match { + case Some(installed: ErrorMessageAuthenticator) => // Ignore, we're already installed + case Some(ivy: IvyAuthenticator) => installIntoIvy(ivy) + case original => doInstall(original) + } + doInstallIfIvy(originalAuthenticator) + } +} +/** + * An authenticator which just delegates to a previous authenticator and issues *nice* + * error messages on failure to find credentials. + * + * Since ivy installs its own credentials handler EVERY TIME it resolves or publishes, we want to + * install this one at some point and eventually ivy will capture it and use it. + */ +private[sbt] final class ErrorMessageAuthenticator(original: Option[Authenticator]) extends Authenticator { + + protected override def getPasswordAuthentication(): PasswordAuthentication = { + // We're guaranteed to only get here if Ivy's authentication fails + if (!isProxyAuthentication) { + val host = getRequestingHost + // TODO - levenshtein distance "did you mean" message. + Message.error(s"Unable to find credentials for [${getRequestingPrompt} @ ${host}].") + val configuredRealms = IvyCredentialsLookup.realmsForHost.getOrElse(host, Set.empty) + if(!configuredRealms.isEmpty) { + Message.error(s" Is one of these realms mispelled for host [${host}]:") + configuredRealms foreach { realm => + Message.error(s" * ${realm}") + } + } + } + // TODO - Maybe we should work on a helpful proxy message... + + // TODO - To be more maven friendly, we may want to also try to grab the "first" authentication that shows up for a server and try it. + // or maybe allow that behavior to be configured, since maven users aren't used to realms (which they should be). + + // Grabs the authentication that would have been provided had we not been installed... + def originalAuthentication: Option[PasswordAuthentication] = { + Authenticator.setDefault(original.getOrElse(null)) + try Option(Authenticator.requestPasswordAuthentication( + getRequestingHost, + getRequestingSite, + getRequestingPort, + getRequestingProtocol, + getRequestingPrompt, + getRequestingScheme)) + finally Authenticator.setDefault(this) + } + originalAuthentication.getOrElse(null) + } + + /** Returns true if this authentication if for a proxy and not for an HTTP server. + * We want to display different error messages, depending. + */ + private def isProxyAuthentication: Boolean = + getRequestorType == Authenticator.RequestorType.PROXY + +} \ No newline at end of file diff --git a/ivy/src/main/scala/sbt/ivyint/IvyCredentialsLookup.scala b/ivy/src/main/scala/sbt/ivyint/IvyCredentialsLookup.scala new file mode 100644 index 000000000..365ffe698 --- /dev/null +++ b/ivy/src/main/scala/sbt/ivyint/IvyCredentialsLookup.scala @@ -0,0 +1,63 @@ +package sbt +package ivyint + +import org.apache.ivy.util.url.CredentialsStore +import collection.JavaConverters._ + +/** A key used to store credentials in the ivy credentials store. */ +private[sbt] sealed trait CredentialKey +/** Represents a key in the ivy credentials store that is only specific to a host. */ +private[sbt] case class Host(name: String) extends CredentialKey +/** Represents a key in the ivy credentials store that is keyed to both a host and a "realm". */ +private[sbt] case class Realm(host: String, realm: String) extends CredentialKey + +/** + * Helper mechanism to improve credential related error messages. + * + * This evil class exposes to us the necessary information to warn on credential failure and offer + * spelling/typo suggestions. + */ +private[sbt] object IvyCredentialsLookup { + + /** Helper extractor for Ivy's key-value store of credentials. */ + private object KeySplit { + def unapply(key: String): Option[(String,String)] = { + key.indexOf('@') match { + case -1 => None + case n => Some(key.take(n) -> key.drop(n+1)) + } + } + } + + /** Here we cheat runtime private so we can look in the credentials store. + * + * TODO - Don't bomb at class load time... + */ + private val credKeyringField = { + val tmp = classOf[CredentialsStore].getDeclaredField("KEYRING") + tmp.setAccessible(true) + tmp + } + + /** All the keys for credentials in the ivy configuration store. */ + def keyringKeys: Set[CredentialKey] = { + val map = credKeyringField.get(null).asInstanceOf[java.util.HashMap[String, Any]] + // make a clone of the set... + (map.keySet.asScala.map { + case KeySplit(realm, host) => Realm(host, realm) + case host => Host(host) + })(collection.breakOut) + } + + /** + * A mapping of host -> realms in the ivy credentials store. + */ + def realmsForHost: Map[String, Set[String]] = + keyringKeys collect { + case x: Realm => x + } groupBy { realm => + realm.host + } mapValues { realms => + realms map (_.realm) + } +} \ No newline at end of file diff --git a/ivy/src/test/scala/MakePomTest.scala b/ivy/src/test/scala/MakePomTest.scala new file mode 100644 index 000000000..1341b207d --- /dev/null +++ b/ivy/src/test/scala/MakePomTest.scala @@ -0,0 +1,29 @@ +package sbt + +import java.io.File +import org.specs2._ +import mutable.Specification + +object MakePomTest extends Specification +{ + val mp = new MakePom(ConsoleLogger()) + import mp.{makeDependencyVersion=>v} + "MakePom makeDependencyVersion" should { + "Handle .+ in versions" in { + v("1.+") must_== "[1,2)" + v("1.2.3.4.+") must_== "[1.2.3.4,1.2.3.5)" + v("12.31.42.+") must_== "[12.31.42,12.31.43)" + } + /* TODO - do we care about this case? + * 1+ --> [1,2),[10,20),[100,200),[1000,2000),[10000,20000),[100000,200000) + */ + "Handle ]* bracket in version ranges" in { + v("]1,3]") must_== "(1,3]" + v("]1.1,1.3]") must_== "(1.1,1.3]" + } + "Handle *[ bracket in version ranges" in { + v("[1,3[") must_== "[1,3)" + v("[1.1,1.3[") must_== "[1.1,1.3)" + } + } +}