From 849d128b90429f893b26ac7f59beb2b94db0bf83 Mon Sep 17 00:00:00 2001 From: Yi Cheng Date: Mon, 29 Jan 2018 12:02:49 -0800 Subject: [PATCH] Specify classifier on module level on CLI (#735) --- cli/src/main/scala-2.11/BUILD | 17 +- .../main/scala-2.11/coursier/cli/Helper.scala | 87 ++++----- .../coursier/cli/CliIntegrationTest.scala | 184 +++++++++++++++++- .../main/scala/coursier/core/Resolution.scala | 17 +- .../src/main/scala/coursier/util/Parse.scala | 144 ++++++++++++-- pants.ini | 3 + .../scala/coursier/test/CentralTests.scala | 54 ++++- .../test/scala/coursier/test/ParseTests.scala | 84 +++++++- 8 files changed, 513 insertions(+), 77 deletions(-) diff --git a/cli/src/main/scala-2.11/BUILD b/cli/src/main/scala-2.11/BUILD index bee2ad756..b7bb4b575 100644 --- a/cli/src/main/scala-2.11/BUILD +++ b/cli/src/main/scala-2.11/BUILD @@ -7,8 +7,23 @@ scala_library( "core:core", "extra/src/main/scala/coursier/extra:extra", "extra/src/main/scala-2.11/coursier/extra:native", + ":util", ], - sources = rglobs("*.scala"), + sources = globs( + "coursier/cli/scaladex/*.scala", + "coursier/cli/spark/*.scala", + "coursier/cli/*.scala", + ), +) + +scala_library( + name = "util", + dependencies = [ + "3rdparty/jvm:argonaut-shapeless", + "cache/src/main/scala:cache", + "core:core", + ], + sources = globs("coursier/cli/util/*.scala"), ) jvm_binary( diff --git a/cli/src/main/scala-2.11/coursier/cli/Helper.scala b/cli/src/main/scala-2.11/coursier/cli/Helper.scala index 2ae88bf5a..1fb9afcd1 100644 --- a/cli/src/main/scala-2.11/coursier/cli/Helper.scala +++ b/cli/src/main/scala-2.11/coursier/cli/Helper.scala @@ -10,6 +10,7 @@ import coursier.cli.scaladex.Scaladex import coursier.cli.util.{JsonElem, JsonPrintRequirement, JsonReport} import coursier.extra.Typelevel import coursier.ivy.IvyRepository +import coursier.util.Parse.ModuleRequirements import coursier.util.{Parse, Print} import scala.annotation.tailrec @@ -83,9 +84,9 @@ class Helper( isolated: IsolatedLoaderOptions = IsolatedLoaderOptions(), warnBaseLoaderNotFound: Boolean = true ) { - import common._ import Helper.errPrintln import Util._ + import common._ val ttl0 = if (ttl.isEmpty) @@ -152,7 +153,7 @@ class Helper( val (scaladexRawDependencies, otherRawDependencies) = rawDependencies.partition(s => s.contains("/") || !s.contains(":")) - val scaladexModuleVersionConfigs = + val scaladexDeps: List[Dependency] = if (scaladexRawDependencies.isEmpty) Nil else { @@ -212,29 +213,9 @@ class Helper( res .collect { case \/-(l) => l } .flatten - .map { case (mod, ver) => (mod, ver, None) } + .map { case (mod, ver) => Dependency(mod, ver) } } - - val (modVerCfgErrors, moduleVersionConfigs) = - Parse.moduleVersionConfigs(otherRawDependencies, scalaVersion) - val (intransitiveModVerCfgErrors, intransitiveModuleVersionConfigs) = - Parse.moduleVersionConfigs(intransitive, scalaVersion) - - def allModuleVersionConfigs = - // FIXME Order of the dependencies is not respected here (scaladex ones go first) - scaladexModuleVersionConfigs ++ moduleVersionConfigs - - prematureExitIf(modVerCfgErrors.nonEmpty) { - s"Cannot parse dependencies:\n" + modVerCfgErrors.map(" "+_).mkString("\n") - } - - prematureExitIf(intransitiveModVerCfgErrors.nonEmpty) { - s"Cannot parse intransitive dependencies:\n" + - intransitiveModVerCfgErrors.map(" "+_).mkString("\n") - } - - val (forceVersionErrors, forceVersions0) = Parse.moduleVersions(forceVersion, scalaVersion) prematureExitIf(forceVersionErrors.nonEmpty) { @@ -265,12 +246,12 @@ class Helper( prematureExitIf(excludesWithAttr.nonEmpty) { s"Excluded modules with attributes not supported:\n" + - excludesWithAttr - .map(" " + _) - .mkString("\n") + excludesWithAttr + .map(" " + _) + .mkString("\n") } - val excludes: Set[(String, String)] = excludesNoAttr.map { mod => + val globalExcludes: Set[(String, String)] = excludesNoAttr.map { mod => (mod.organization, mod.name) }.toSet @@ -296,30 +277,28 @@ class Helper( }).groupBy(_._1).mapValues(_.map(_._2).toSet).toMap } - val baseDependencies = allModuleVersionConfigs.map { - case (module, version, configOpt) => - Dependency( - module, - version, - attributes = Attributes("", ""), - configuration = configOpt.getOrElse(defaultConfiguration), - exclusions = localExcludeMap.getOrElse(module.orgName, Set()) | excludes - ) + val moduleReq = ModuleRequirements(globalExcludes, localExcludeMap, defaultConfiguration) + + val (modVerCfgErrors: Seq[String], normalDeps: Seq[Dependency]) = + Parse.moduleVersionConfigs(otherRawDependencies, moduleReq, transitive=true, scalaVersion) + + val (intransitiveModVerCfgErrors: Seq[String], intransitiveDeps: Seq[Dependency]) = + Parse.moduleVersionConfigs(intransitive, moduleReq, transitive=false, scalaVersion) + + prematureExitIf(modVerCfgErrors.nonEmpty) { + s"Cannot parse dependencies:\n" + modVerCfgErrors.map(" "+_).mkString("\n") } - val intransitiveDependencies = intransitiveModuleVersionConfigs.map { - case (module, version, configOpt) => - Dependency( - module, - version, - attributes = Attributes("", ""), - configuration = configOpt.getOrElse(defaultConfiguration), - exclusions = excludes, - transitive = false - ) + prematureExitIf(intransitiveModVerCfgErrors.nonEmpty) { + s"Cannot parse intransitive dependencies:\n" + + intransitiveModVerCfgErrors.map(" "+_).mkString("\n") } - val dependencies = baseDependencies ++ intransitiveDependencies + val transitiveDeps: Seq[Dependency] = + // FIXME Order of the dependencies is not respected here (scaladex ones go first) + scaladexDeps ++ normalDeps + + val allDependencies: Seq[Dependency] = transitiveDeps ++ intransitiveDeps val checksums = { val splitChecksumArgs = checksum.flatMap(_.split(',')).filter(_.nonEmpty) @@ -335,7 +314,7 @@ class Helper( val userEnabledProfiles = profile.toSet val startRes = Resolution( - dependencies.toSet, + allDependencies.toSet, forceVersions = forceVersions, filter = Some(dep => keepOptional || !dep.optional), userActivations = @@ -376,7 +355,7 @@ class Helper( errPrintln( s" Dependencies:\n" + Print.dependenciesUnknownConfigs( - dependencies, + allDependencies, Map.empty, printExclusions = verbosityLevel >= 2 ) @@ -507,7 +486,7 @@ class Helper( val depsStr = if (reverseTree || tree) Print.dependencyTree( - dependencies, + allDependencies, res, printExclusions = verbosityLevel >= 1, reverse = reverseTree @@ -711,7 +690,7 @@ class Helper( val deps: Seq[Dependency] = Set(getDepArtifactsForClassifier(sources, javadoc, res).map(_._1): _*).toSeq // A map from requested org:name:version to reconciled org:name:version - val conflictResolutionForRoots: Map[String, String] = dependencies.map({ dep => + val conflictResolutionForRoots: Map[String, String] = allDependencies.map({ dep => val reconciledVersion: String = res.reconciledVersions .getOrElse(dep.module, dep.version) if (reconciledVersion != dep.version) { @@ -842,7 +821,8 @@ class Helper( // Trying to get the main class of the first artifact val mainClassOpt = for { - (module, _, _) <- allModuleVersionConfigs.headOption + dep: Dependency <- transitiveDeps.headOption + module = dep.module mainClass <- mainClasses.collectFirst { case ((org, name), mainClass) if org == module.organization && ( @@ -854,7 +834,8 @@ class Helper( } yield mainClass def sameOrgOnlyMainClassOpt = for { - (module, _, _) <- allModuleVersionConfigs.headOption + dep: Dependency <- transitiveDeps.headOption + module = dep.module orgMainClasses = mainClasses.collect { case ((org, name), mainClass) if org == module.organization => diff --git a/cli/src/test/scala-2.11/coursier/cli/CliIntegrationTest.scala b/cli/src/test/scala-2.11/coursier/cli/CliIntegrationTest.scala index f7ab87938..7c017fb22 100644 --- a/cli/src/test/scala-2.11/coursier/cli/CliIntegrationTest.scala +++ b/cli/src/test/scala-2.11/coursier/cli/CliIntegrationTest.scala @@ -2,8 +2,8 @@ package coursier.cli import java.io.{File, FileWriter} -import coursier.cli.util.ReportNode -import argonaut._, Argonaut._ +import argonaut.Argonaut._ +import coursier.cli.util.{DepNode, ReportNode} import org.junit.runner.RunWith import org.scalatest.FlatSpec import org.scalatest.junit.JUnitRunner @@ -215,7 +215,185 @@ class CliIntegrationTest extends FlatSpec { assert(node.conflict_resolution == Map("org.tukaani:xz:1.1" -> "org.tukaani:xz:1.2")) } } - } + /** + * Result: + * |└─ org.apache.commons:commons-compress:1.5 + * | └─ org.tukaani:xz:1.2 + */ + "classifier tests" should "have tests.jar" in withFile() { + (excludeFile, _) => + withFile() { + (jsonFile, _) => { + val commonOpt = CommonOptions(jsonOutputFile = jsonFile.getPath) + val fetchOpt = FetchOptions(common = commonOpt) + + val fetch = new Fetch(fetchOpt) with TestOnlyExtraArgsApp + fetch.setRemainingArgs(Seq("org.apache.commons:commons-compress:1.5,classifier=tests"), Seq()) + fetch.apply() + + val node: ReportNode = getReportFromJson(jsonFile) + + val compressNode = node.dependencies.find(_.coord == "org.apache.commons:commons-compress:1.5") + assert(compressNode.isDefined) + assert(compressNode.get.files.head._1 == "tests") + assert(compressNode.get.files.head._2.contains("commons-compress-1.5-tests.jar")) + assert(compressNode.get.dependencies.contains("org.tukaani:xz:1.2")) + } + } + } + + /** + * Result: + * |├─ org.apache.commons:commons-compress:1.5 + * |│ └─ org.tukaani:xz:1.2 + * |└─ org.apache.commons:commons-compress:1.5 + * | └─ org.tukaani:xz:1.2 + */ + "mixed vanilla and classifier " should "have tests.jar and .jar" in withFile() { + (excludeFile, _) => + withFile() { + (jsonFile, _) => { + val commonOpt = CommonOptions(jsonOutputFile = jsonFile.getPath) + val fetchOpt = FetchOptions(common = commonOpt) + + val fetch = new Fetch(fetchOpt) with TestOnlyExtraArgsApp + fetch.setRemainingArgs( + Seq("org.apache.commons:commons-compress:1.5,classifier=tests", + "org.apache.commons:commons-compress:1.5"), + Seq()) + fetch.apply() + + val node: ReportNode = getReportFromJson(jsonFile) + + val compressNodes: Seq[DepNode] = node.dependencies + .filter(_.coord == "org.apache.commons:commons-compress:1.5") + .sortBy(_.files.head._1.length) // sort by first classifier length + assert(compressNodes.length == 2) + assert(compressNodes.head.files.head._1 == "") + assert(compressNodes.head.files.head._2.contains("commons-compress-1.5.jar")) + + assert(compressNodes.last.files.head._1 == "tests") + assert(compressNodes.last.files.head._2.contains("commons-compress-1.5-tests.jar")) + } + } + } + + /** + * Result: + * |└─ org.apache.commons:commons-compress:1.5 + * | └─ org.tukaani:xz:1.2 // should not be fetched + */ + "intransitive" should "only fetch a single jar" in withFile() { + (_, _) => + withFile() { + (jsonFile, _) => { + val commonOpt = CommonOptions(jsonOutputFile = jsonFile.getPath, intransitive = List("org.apache.commons:commons-compress:1.5")) + val fetchOpt = FetchOptions(common = commonOpt) + + val fetch = new Fetch(fetchOpt) with TestOnlyExtraArgsApp + fetch.apply() + + val node: ReportNode = getReportFromJson(jsonFile) + val compressNode = node.dependencies.find(_.coord == "org.apache.commons:commons-compress:1.5") + assert(compressNode.isDefined) + assert(compressNode.get.files.head._1 == "") + assert(compressNode.get.files.head._2.contains("commons-compress-1.5.jar")) + + assert(compressNode.get.dependencies.isEmpty) + } + } + } + + /** + * Result: + * |└─ org.apache.commons:commons-compress:1.5 + * | └─ org.tukaani:xz:1.2 + */ + "intransitive classifier" should "only fetch a single tests jar" in withFile() { + (excludeFile, _) => + withFile() { + (jsonFile, _) => { + val commonOpt = CommonOptions(jsonOutputFile = jsonFile.getPath, intransitive = List("org.apache.commons:commons-compress:1.5,classifier=tests")) + val fetchOpt = FetchOptions(common = commonOpt) + + val fetch = new Fetch(fetchOpt) with TestOnlyExtraArgsApp + fetch.setRemainingArgs(Seq(), Seq()) + fetch.apply() + + val node: ReportNode = getReportFromJson(jsonFile) + + val compressNode = node.dependencies.find(_.coord == "org.apache.commons:commons-compress:1.5") + assert(compressNode.isDefined) + assert(compressNode.get.files.head._1 == "tests") + assert(compressNode.get.files.head._2.contains("commons-compress-1.5-tests.jar")) + + assert(compressNode.get.dependencies.isEmpty) + } + } + } + + /** + * Result: + * |└─ org.apache.commons:commons-compress:1.5 -> 1.4.1 + * | └─ org.tukaani:xz:1.0 + */ + "classifier with forced version" should "fetch tests jar" in withFile() { + (excludeFile, _) => + withFile() { + (jsonFile, _) => { + val commonOpt = CommonOptions(jsonOutputFile = jsonFile.getPath, forceVersion = List("org.apache.commons:commons-compress:1.4.1")) + val fetchOpt = FetchOptions(common = commonOpt) + + val fetch = new Fetch(fetchOpt) with TestOnlyExtraArgsApp + fetch.setRemainingArgs(Seq("org.apache.commons:commons-compress:1.5,classifier=tests"), Seq()) + fetch.apply() + + val node: ReportNode = getReportFromJson(jsonFile) + + assert(!node.dependencies.exists(_.coord == "org.apache.commons:commons-compress:1.5")) + + val compressNode = node.dependencies.find(_.coord == "org.apache.commons:commons-compress:1.4.1") + assert(compressNode.isDefined) + assert(compressNode.get.files.head._1 == "tests") + assert(compressNode.get.files.head._2.contains("commons-compress-1.4.1-tests.jar")) + + assert(compressNode.get.dependencies.size == 1) + assert(compressNode.get.dependencies.head == "org.tukaani:xz:1.0") + } + } + } + + /** + * Result: + * |└─ org.apache.commons:commons-compress:1.5 -> 1.4.1 + * | └─ org.tukaani:xz:1.0 // should not be there + */ + "intransitive, classifier, forced version" should "fetch a single tests jar" in withFile() { + (excludeFile, _) => + withFile() { + (jsonFile, _) => { + val commonOpt = CommonOptions(jsonOutputFile = jsonFile.getPath, + intransitive = List("org.apache.commons:commons-compress:1.5,classifier=tests"), + forceVersion = List("org.apache.commons:commons-compress:1.4.1")) + val fetchOpt = FetchOptions(common = commonOpt) + + val fetch = new Fetch(fetchOpt) with TestOnlyExtraArgsApp + fetch.setRemainingArgs(Seq(), Seq()) + fetch.apply() + + val node: ReportNode = getReportFromJson(jsonFile) + + assert(!node.dependencies.exists(_.coord == "org.apache.commons:commons-compress:1.5")) + + val compressNode = node.dependencies.find(_.coord == "org.apache.commons:commons-compress:1.4.1") + assert(compressNode.isDefined) + assert(compressNode.get.files.head._1 == "tests") + assert(compressNode.get.files.head._2.contains("commons-compress-1.4.1-tests.jar")) + + assert(compressNode.get.dependencies.isEmpty) + } + } + } } diff --git a/core/shared/src/main/scala/coursier/core/Resolution.scala b/core/shared/src/main/scala/coursier/core/Resolution.scala index 99609dc9c..7431ca700 100644 --- a/core/shared/src/main/scala/coursier/core/Resolution.scala +++ b/core/shared/src/main/scala/coursier/core/Resolution.scala @@ -1057,8 +1057,23 @@ final case class Resolution( (source, proj) <- projectCache .get(dep.moduleVersion) .toSeq + + classifiers = { + if (!dep.attributes.classifier.isEmpty) { + val stringSeq: Seq[String] = overrideClassifiers.getOrElse(Seq()) ++ Seq(dep.attributes.classifier) + if (stringSeq.isEmpty) { + Option.empty + } + else { + Some(stringSeq) + } + } else { + overrideClassifiers + } + } + artifact <- source - .artifacts(dep, proj, overrideClassifiers) + .artifacts(dep, proj, classifiers) if optional || !artifact.isOptional } yield dep -> artifact diff --git a/core/shared/src/main/scala/coursier/util/Parse.scala b/core/shared/src/main/scala/coursier/util/Parse.scala index 270a427e3..eb2da60a4 100644 --- a/core/shared/src/main/scala/coursier/util/Parse.scala +++ b/core/shared/src/main/scala/coursier/util/Parse.scala @@ -1,11 +1,11 @@ package coursier.util -import coursier.core.{Repository, Module} +import coursier.{Attributes, Dependency} +import coursier.core.{Module, Repository} import coursier.ivy.IvyRepository import coursier.maven.MavenRepository import scala.collection.mutable.ArrayBuffer - import scalaz.\/ import scalaz.Scalaz.ToEitherOps @@ -113,44 +113,131 @@ object Parse { } } + class ModuleParseError(private val message: String = "", + private val cause: Throwable = None.orNull) + extends Exception(message, cause) + @deprecated("use the variant accepting a default scala version", "1.0.0-M13") - def moduleVersionConfig(s: String): Either[String, (Module, String, Option[String])] = - moduleVersionConfig(s, defaultScalaVersion) + def moduleVersionConfig(s: String, defaultScalaVersion: String): Either[String, (Module, String, Option[String])] = { + val mvc: Either[String, Dependency] = moduleVersionConfig(s, ModuleRequirements(), transitive = true, defaultScalaVersion) + mvc match { + case Left(x) => Left(x) + case Right(d) => Right(d.module, d.version, Option(d.configuration).filter(_.trim.nonEmpty)) + } + } + + + @deprecated("use the variant accepting a default scala version", "1.0.0-M13") + def moduleVersionConfig(s: String): Either[String, (Module, String, Option[String])] = { + val mvc: Either[String, Dependency] = moduleVersionConfig(s, ModuleRequirements(), transitive = true, defaultScalaVersion) + mvc match { + case Left(x) => Left(x) + case Right(d) => Right(d.module, d.version, Option(d.configuration).filter(_.trim.nonEmpty)) + } + } /** * Parses coordinates like * org:name:version - * possibly with attributes, like - * org:name;attr1=val1;attr2=val2:version + * with attributes, like + * org:name:version,attr1=val1,attr2=val2 * and a configuration, like * org:name:version:config * or - * org:name;attr1=val1;attr2=val2:version:config + * org:name:version:config,attr1=val1,attr2=val2 + * + * Currently only "classifier" attribute is used, and others are ignored. */ - def moduleVersionConfig(s: String, defaultScalaVersion: String): Either[String, (Module, String, Option[String])] = { + def moduleVersionConfig(s: String, + req: ModuleRequirements, + transitive: Boolean, + defaultScalaVersion: String): Either[String, Dependency] = { - val parts = s.split(":", 5) + // Assume org:name:version,attr1=val1,attr2=val2 + // That is ',' has to go after ':'. + // E.g. "org:name,attr1=val1,attr2=val2:version:config" is illegal. + val attrSeparator = "," + val argSeparator = ":" + + val strings = s.split(attrSeparator) + val coords = strings.head + + val attrs = strings.drop(1).map({ x => { + if (x.mkString.contains(argSeparator)) { + throw new ModuleParseError(s"'$argSeparator' is not allowed in attribute '$x' in '$s'. Please follow the format " + + s"'org${argSeparator}name[${argSeparator}version][${argSeparator}config]${attrSeparator}attr1=val1${attrSeparator}attr2=val2'") + } + val y = x.split("=") + if (y.length != 2) { + throw new ModuleParseError(s"Failed to parse attribute '$x' in '$s'. Keyword argument expected such as 'classifier=tests'") + } + (y(0), y(1)) + } + }).toMap + + val parts = coords.split(":", 5) + + val attributes = attrs.get("classifier") match { + case Some(c) => Attributes("", c) + case None => Attributes("", "") + } + + val localExcludes = req.localExcludes + val globalExcludes = req.globalExcludes + val defaultConfig = req.defaultConfiguration parts match { case Array(org, "", rawName, version, config) => module(s"$org::$rawName", defaultScalaVersion) .right - .map((_, version, Some(config))) + .map(mod => { + Dependency( + mod, + version, + config, + attributes, + transitive = transitive, + exclusions = localExcludes.getOrElse(mod.orgName, Set()) | globalExcludes) + }) case Array(org, "", rawName, version) => module(s"$org::$rawName", defaultScalaVersion) .right - .map((_, version, None)) + .map(mod => { + Dependency( + mod, + version, + configuration = defaultConfig, + attributes = attributes, + transitive = transitive, + exclusions = localExcludes.getOrElse(mod.orgName, Set()) | globalExcludes) + }) case Array(org, rawName, version, config) => module(s"$org:$rawName", defaultScalaVersion) .right - .map((_, version, Some(config))) + .map(mod => { + Dependency( + mod, + version, + config, + attributes, + transitive = transitive, + exclusions = localExcludes.getOrElse(mod.orgName, Set()) | globalExcludes) + }) case Array(org, rawName, version) => module(s"$org:$rawName", defaultScalaVersion) .right - .map((_, version, None)) + .map(mod => { + Dependency( + mod, + version, + configuration = defaultConfig, + attributes = attributes, + transitive = transitive, + exclusions = localExcludes.getOrElse(mod.orgName, Set()) | globalExcludes) + }) case _ => Left(s"Malformed dependency: $s") @@ -170,16 +257,39 @@ object Parse { valuesAndErrors(moduleVersion(_, defaultScalaVersion), l) @deprecated("use the variant accepting a default scala version", "1.0.0-M13") - def moduleVersionConfigs(l: Seq[String]): (Seq[String], Seq[(Module, String, Option[String])]) = - moduleVersionConfigs(l, defaultScalaVersion) + def moduleVersionConfigs(l: Seq[String]): (Seq[String], Seq[(Module, String, Option[String])]) = { + val mvc: (Seq[String], Seq[Dependency]) = moduleVersionConfigs(l, ModuleRequirements(), transitive = true, defaultScalaVersion) + // convert empty config to None + (mvc._1, mvc._2.map(d => (d.module, d.version, Option(d.configuration).filter(_.trim.nonEmpty)))) + } + + @deprecated("use the variant accepting a default scala version", "1.0.0-M13") + def moduleVersionConfigs(l: Seq[String], defaultScalaVersion: String): (Seq[String], Seq[(Module, String, Option[String])]) = { + val mvc: (Seq[String], Seq[Dependency]) = moduleVersionConfigs(l, ModuleRequirements(), transitive = true, defaultScalaVersion) + (mvc._1, mvc._2.map(d => (d.module, d.version, Option(d.configuration).filter(_.trim.nonEmpty)))) + } + + /** + * Data holder for additional info that needs to be considered when parsing the module. + * + * @param globalExcludes global excludes that need to be applied to all modules + * @param localExcludes excludes to be applied to specific modules + * @param defaultConfiguration default configuration + */ + case class ModuleRequirements(globalExcludes: Set[(String, String)] = Set(), + localExcludes: Map[String, Set[(String, String)]] = Map(), + defaultConfiguration: String = "default(compile)") /** * Parses a sequence of coordinates having an optional configuration. * * @return Sequence of errors, and sequence of modules / versions / optional configurations */ - def moduleVersionConfigs(l: Seq[String], defaultScalaVersion: String): (Seq[String], Seq[(Module, String, Option[String])]) = - valuesAndErrors(moduleVersionConfig(_, defaultScalaVersion), l) + def moduleVersionConfigs(l: Seq[String], + req: ModuleRequirements, + transitive: Boolean, + defaultScalaVersion: String): (Seq[String], Seq[Dependency]) = + valuesAndErrors(moduleVersionConfig(_, req, transitive, defaultScalaVersion), l) def repository(s: String): String \/ Repository = if (s == "central") diff --git a/pants.ini b/pants.ini index 40fa969a1..a2b85482f 100644 --- a/pants.ini +++ b/pants.ini @@ -33,3 +33,6 @@ jvm_options: [ # TODO: Remove after https://github.com/pantsbuild/pants/issues/4477 is pulled in. '-Dzinc.analysis.cache.limit=5000', ] + +[test.junit] +jvm_options = ['-Xmx2048m'] \ No newline at end of file diff --git a/tests/shared/src/test/scala/coursier/test/CentralTests.scala b/tests/shared/src/test/scala/coursier/test/CentralTests.scala index a50ab4525..266bbf0e8 100644 --- a/tests/shared/src/test/scala/coursier/test/CentralTests.scala +++ b/tests/shared/src/test/scala/coursier/test/CentralTests.scala @@ -2,8 +2,9 @@ package coursier package test import utest._ -import scala.async.Async.{ async, await } +import scala.async.Async.{async, await} +import coursier.MavenRepository import coursier.Platform.fetch import coursier.test.compatibility._ @@ -503,6 +504,57 @@ abstract class CentralTests extends TestSuite { } } + 'classifier - { + + // Adding extra repo so it's agnostic from nexus which only has the poms + val extraRepo = MavenRepository("https://repo1.maven.org/maven2") + + 'vanilla - { + async { + val deps = Set( + Dependency( + Module("org.apache.avro", "avro"), "1.8.1" + ) + ) + val res = await(resolve(deps, extraRepos = Seq(extraRepo))) + val filenames: Set[String] = res.artifacts.map(_.url.split("/").last).toSet + assert(filenames.contains("avro-1.8.1.jar")) + assert(!filenames.contains("avro-1.8.1-tests.jar")) + } + } + + 'tests - { + async { + val deps = Set( + Dependency( + Module("org.apache.avro", "avro"), "1.8.1", attributes = Attributes("", "tests") + ) + ) + val res = await(resolve(deps, extraRepos = Seq(extraRepo))) + val filenames: Set[String] = res.artifacts.map(_.url.split("/").last).toSet + assert(!filenames.contains("avro-1.8.1.jar")) + assert(filenames.contains("avro-1.8.1-tests.jar")) + } + } + + 'mixed - { + async { + val deps = Set( + Dependency( + Module("org.apache.avro", "avro"), "1.8.1" + ), + Dependency( + Module("org.apache.avro", "avro"), "1.8.1", attributes = Attributes("", "tests") + ) + ) + val res = await(resolve(deps, extraRepos = Seq(extraRepo))) + val filenames: Set[String] = res.artifacts.map(_.url.split("/").last).toSet + assert(filenames.contains("avro-1.8.1.jar")) + assert(filenames.contains("avro-1.8.1-tests.jar")) + } + } + } + 'artifacts - { 'uniqueness - { async { diff --git a/tests/shared/src/test/scala/coursier/test/ParseTests.scala b/tests/shared/src/test/scala/coursier/test/ParseTests.scala index 510304771..aabdb5991 100644 --- a/tests/shared/src/test/scala/coursier/test/ParseTests.scala +++ b/tests/shared/src/test/scala/coursier/test/ParseTests.scala @@ -1,8 +1,9 @@ package coursier.test -import coursier.{MavenRepository, Repository} +import coursier.{Attributes, MavenRepository, Repository} import coursier.ivy.IvyRepository import coursier.util.Parse +import coursier.util.Parse.{ModuleParseError, ModuleRequirements} import utest._ object ParseTests extends TestSuite { @@ -47,5 +48,86 @@ object ParseTests extends TestSuite { val res = Parse.repository("jitpack") assert(res.exists(isMavenRepo)) } + + // Module parsing tests + "org:name:version" - { + Parse.moduleVersionConfig("org.apache.avro:avro:1.7.4", ModuleRequirements(), transitive = true, "2.11.11") match { + case Left(err) => assert(false) + case Right(dep) => + assert(dep.module.organization == "org.apache.avro") + assert(dep.module.name == "avro") + assert(dep.version == "1.7.4") + assert(dep.configuration == "default(compile)") + assert(dep.attributes == Attributes()) + } + } + + "org:name:version:conifg" - { + Parse.moduleVersionConfig("org.apache.avro:avro:1.7.4:runtime", ModuleRequirements(), transitive = true, "2.11.11") match { + case Left(err) => assert(false) + case Right(dep) => + assert(dep.module.organization == "org.apache.avro") + assert(dep.module.name == "avro") + assert(dep.version == "1.7.4") + assert(dep.configuration == "runtime") + assert(dep.attributes == Attributes()) + } + } + + "single attr" - { + Parse.moduleVersionConfig("org.apache.avro:avro:1.7.4:runtime,classifier=tests", ModuleRequirements(), transitive = true, "2.11.11") match { + case Left(err) => assert(false) + case Right(dep) => + assert(dep.module.organization == "org.apache.avro") + assert(dep.module.name == "avro") + assert(dep.version == "1.7.4") + assert(dep.configuration == "runtime") + assert(dep.attributes == Attributes("", "tests")) + } + } + + "multiple attrs" - { + Parse.moduleVersionConfig("org.apache.avro:avro:1.7.4:runtime,classifier=tests,nickname=superman", ModuleRequirements(), transitive = true, "2.11.11") match { + case Left(err) => assert(false) + case Right(dep) => + assert(dep.module.organization == "org.apache.avro") + assert(dep.module.name == "avro") + assert(dep.version == "1.7.4") + assert(dep.configuration == "runtime") + assert(dep.attributes == Attributes("", "tests")) + } + } + + "single attr with org::name:version" - { + Parse.moduleVersionConfig("io.get-coursier.scala-native::sandbox_native0.3:0.3.0-coursier-1,attr1=val1", ModuleRequirements(), transitive = true, "2.11.11") match { + case Left(err) => assert(false) + case Right(dep) => + assert(dep.module.organization == "io.get-coursier.scala-native") + assert(dep.module.name.contains("sandbox_native0.3")) // use `contains` to be scala version agnostic + assert(dep.version == "0.3.0-coursier-1") + } + } + + "illegal 1" - { + try { + Parse.moduleVersionConfig("org.apache.avro:avro,1.7.4:runtime,classifier=tests", ModuleRequirements(), transitive = true, "2.11.11") + assert(false) // Parsing should fail but succeeded. + } + catch { + case foo: ModuleParseError => assert(foo.getMessage().contains("':' is not allowed in attribute")) // do nothing + case _: Throwable => assert(false) // Unexpected exception + } + } + + "illegal 2" - { + try { + Parse.moduleVersionConfig("junit:junit:4.12,attr", ModuleRequirements(), transitive = true, "2.11.11") + assert(false) // Parsing should fail but succeeded. + } + catch { + case foo: ModuleParseError => assert(foo.getMessage().contains("Failed to parse attribute")) // do nothing + case _: Throwable => assert(false) // Unexpected exception + } + } } }