From d0b46864c832ec1315ff886e55f6b94dfa3dfb0d Mon Sep 17 00:00:00 2001 From: Nick Howard Date: Fri, 9 Mar 2018 15:47:27 -0700 Subject: [PATCH] json-report: one file per dependency instead of multiple; use m2 coords (#782) When classifiers are used as part of dependency specifications, it's important to be able to select just the classified artifact. Unfortunately, in the current json, dependencies don't specify classifiers, so it isn't possible to just get one of the artifacts for a dependency when a classifier is required. This patch introduces maven style artifact prefixes in order to include classifier and packaging information in the coordinates. By doing that, we can use them as keys in dependency lists more easily and it allows consumers of the json to treat those dependency keys as mostly opaque ids rather than having to parse them. Addresses #743 --- .../main/scala-2.12/coursier/cli/Helper.scala | 44 +++- .../coursier/cli/util/JsonReport.scala | 61 ++++-- cli/src/test/scala-2.12/coursier/cli/BUILD | 2 +- .../cli/CliFetchIntegrationTest.scala | 200 +++++++++++++----- .../coursier/cli/util/JsonReportTest.scala | 60 ++++++ .../scala/coursier/core/Definitions.scala | 23 +- 6 files changed, 308 insertions(+), 82 deletions(-) create mode 100644 cli/src/test/scala-2.12/coursier/cli/util/JsonReportTest.scala diff --git a/cli/src/main/scala-2.12/coursier/cli/Helper.scala b/cli/src/main/scala-2.12/coursier/cli/Helper.scala index add604fc0..3f842c66a 100644 --- a/cli/src/main/scala-2.12/coursier/cli/Helper.scala +++ b/cli/src/main/scala-2.12/coursier/cli/Helper.scala @@ -584,17 +584,33 @@ class Helper( } private def getDepArtifactsForClassifier(sources: Boolean, javadoc: Boolean, res0: Resolution): Seq[(Dependency, Artifact)] = { - if (classifier0.nonEmpty || sources || javadoc) { - var classifiers = classifier0 - if (sources) - classifiers = classifiers + "sources" - if (javadoc) - classifiers = classifiers + "javadoc" + val raw: Seq[(Dependency, Artifact)] = if (hasOverrideClassifiers(sources, javadoc)) { //TODO: this function somehow gives duplicated things - res0.dependencyClassifiersArtifacts(classifiers.toVector.sorted) + res0.dependencyClassifiersArtifacts(overrideClassifiers(sources, javadoc).toVector.sorted) } else { res0.dependencyArtifacts(withOptional = true) } + + raw.map({ case (dep, artifact) => + ( + dep.copy( + attributes = dep.attributes.copy(classifier = artifact.classifier)), + artifact + ) + }) + } + + private def overrideClassifiers(sources: Boolean, javadoc:Boolean): Set[String] = { + var classifiers = classifier0 + if (sources) + classifiers = classifiers + "sources" + if (javadoc) + classifiers = classifiers + "javadoc" + classifiers + } + + private def hasOverrideClassifiers(sources: Boolean, javadoc: Boolean): Boolean = { + classifier0.nonEmpty || sources || javadoc } def fetchMap( @@ -708,9 +724,17 @@ class Helper( val artifacts: Seq[(Dependency, Artifact)] = res.dependencyArtifacts val jsonReq = JsonPrintRequirement(artifactToFile, depToArtifacts) - val roots = deps.toVector.map(JsonElem(_, artifacts, Option(jsonReq), res, printExclusions = verbosityLevel >= 1, excluded = false, colors = false)) - val jsonStr = JsonReport(roots, conflictResolutionForRoots)(_.children, _.reconciledVersionStr, _.requestedVersionStr, _.downloadedFiles) - + val roots = deps.toVector.map(JsonElem(_, artifacts, Option(jsonReq), res, printExclusions = verbosityLevel >= 1, excluded = false, colors = false, overrideClassifiers = overrideClassifiers(sources, javadoc))) + val jsonStr = JsonReport( + roots, + conflictResolutionForRoots, + overrideClassifiers(sources, javadoc) + )( + _.children, + _.reconciledVersionStr, + _.requestedVersionStr, + _.downloadedFile + ) val pw = new PrintWriter(new File(jsonOutputFile)) pw.write(jsonStr) pw.close() diff --git a/cli/src/main/scala-2.12/coursier/cli/util/JsonReport.scala b/cli/src/main/scala-2.12/coursier/cli/util/JsonReport.scala index ecde1a281..33dec84ca 100644 --- a/cli/src/main/scala-2.12/coursier/cli/util/JsonReport.scala +++ b/cli/src/main/scala-2.12/coursier/cli/util/JsonReport.scala @@ -12,9 +12,18 @@ import scala.collection.parallel.ParSeq import argonaut._ import Argonaut._ +/** + * Lookup table for files and artifacts to print in the JsonReport. + */ final case class JsonPrintRequirement(fileByArtifact: Map[String, File], depToArtifacts: Map[Dependency, Vector[Artifact]]) -final case class DepNode(coord: String, files: Vector[(String, String)], dependencies: Set[String]) +/** + * Represents a resolved dependency's artifact in the JsonReport. + * @param coord String representation of the artifact's maven coordinate. + * @param file The path to the file for the artifact. + * @param dependencies The dependencies of the artifact. + */ +final case class DepNode(coord: String, file: Option[String], dependencies: Set[String]) final case class ReportNode(conflict_resolution: Map[String, String], dependencies: Vector[DepNode], version: String) @@ -34,7 +43,7 @@ object ReportNode { import argonaut.ArgonautShapeless._ implicit val encodeJson = EncodeJson.of[ReportNode] implicit val decodeJson = DecodeJson.of[ReportNode] - val version = "0.0.1" + val version = "0.1.0" } @@ -42,8 +51,8 @@ object JsonReport { private val printer = PrettyParams.nospace.copy(preserveOrder = true) - def apply[T](roots: IndexedSeq[T], conflictResolutionForRoots: Map[String, String]) - (children: T => Seq[T], reconciledVersionStr: T => String, requestedVersionStr: T => String, getFiles: T => Seq[(String, String)]): String = { + def apply[T](roots: IndexedSeq[T], conflictResolutionForRoots: Map[String, String], overrideClassifiers: Set[String]) + (children: T => Seq[T], reconciledVersionStr: T => String, requestedVersionStr: T => String, getFile: T => Option[String]): String = { val rootDeps: ParSeq[DepNode] = roots.par.map(r => { @@ -64,10 +73,10 @@ object JsonReport { val acc = scala.collection.mutable.Set[String]() flattenDeps(Seq(r), Set(), acc) - DepNode(reconciledVersionStr(r), getFiles(r).toVector, acc.toSet) + DepNode(reconciledVersionStr(r), getFile(r), acc.toSet) }) - val report = ReportNode(conflictResolutionForRoots, rootDeps.toVector, ReportNode.version) + val report = ReportNode(conflictResolutionForRoots, rootDeps.toVector.sortBy(_.coord), ReportNode.version) printer.pretty(report.asJson) } @@ -80,7 +89,9 @@ final case class JsonElem(dep: Dependency, resolution: Resolution, colors: Boolean, printExclusions: Boolean, - excluded: Boolean) { + excluded: Boolean, + overrideClassifiers: Set[String] + ) { val (red, yellow, reset) = if (colors) @@ -89,23 +100,24 @@ final case class JsonElem(dep: Dependency, ("", "", "") // This is used to printing json output - // Seq of (classifier, file path) tuple - lazy val downloadedFiles: Seq[(String, String)] = { - jsonPrintRequirement match { - case Some(req) => + // Option of the file path + lazy val downloadedFile: Option[String] = { + jsonPrintRequirement.flatMap(req => req.depToArtifacts.getOrElse(dep, Seq()) - .map(x => (x.classifier, req.fileByArtifact.get(x.url))) - .filter(_._2.isDefined) - .map(x => (x._1, x._2.get.getPath)) - case None => Seq() - } + .filter(_.classifier == dep.attributes.classifier) + .map(x => req.fileByArtifact.get(x.url)) + .filter(_.isDefined) + .filter(_.nonEmpty) + .map(_.get.getPath) + .headOption + ) } lazy val reconciledVersion: String = resolution.reconciledVersions .getOrElse(dep.module, dep.version) // These are used to printing json output - val reconciledVersionStr = s"${dep.module}:$reconciledVersion" + val reconciledVersionStr = s"${dep.mavenPrefix}:$reconciledVersion" val requestedVersionStr = s"${dep.module}:${dep.version}" lazy val repr = @@ -150,6 +162,12 @@ final case class JsonElem(dep: Dependency, withReconciledVersions = false ).sortBy { trDep => (trDep.module.organization, trDep.module.name, trDep.version) + }.map { d => + if (overrideClassifiers.contains(dep0.attributes.classifier)) { + d.copy(attributes = d.attributes.copy(classifier = dep0.attributes.classifier)) + } else { + d + } } def excluded = resolution @@ -164,17 +182,18 @@ final case class JsonElem(dep: Dependency, .filterNot(dependencies.map(_.moduleVersion).toSet).map { case (mod, ver) => JsonElem( - Dependency(mod, ver, "", Set.empty, Attributes("", ""), false, false), + Dependency(mod, ver, "", Set.empty, Attributes("", ""), optional = false, transitive = false), artifacts, jsonPrintRequirement, resolution, colors, printExclusions, - excluded = true + excluded = true, + overrideClassifiers = overrideClassifiers ) } - dependencies.map(JsonElem(_, artifacts, jsonPrintRequirement, resolution, colors, printExclusions, excluded = false)) ++ + dependencies.map(JsonElem(_, artifacts, jsonPrintRequirement, resolution, colors, printExclusions, excluded = false, overrideClassifiers = overrideClassifiers)) ++ (if (printExclusions) excluded else Nil) } @@ -183,5 +202,5 @@ final case class JsonElem(dep: Dependency, * children's children, causing performance issue. Hash collision should be rare, but when that happens, the * default equality check should take of the recursive aspect of `children`. */ - override def hashCode(): Int = Objects.hash(dep, requestedVersionStr, reconciledVersion, downloadedFiles) + override def hashCode(): Int = Objects.hash(dep, requestedVersionStr, reconciledVersion, downloadedFile) } diff --git a/cli/src/test/scala-2.12/coursier/cli/BUILD b/cli/src/test/scala-2.12/coursier/cli/BUILD index 7eb254259..8f6241bd8 100644 --- a/cli/src/test/scala-2.12/coursier/cli/BUILD +++ b/cli/src/test/scala-2.12/coursier/cli/BUILD @@ -5,7 +5,7 @@ junit_tests( "3rdparty/jvm:scalatest", "cli/src/main/scala-2.12:cli", ], - sources = ["CliFetchIntegrationTest.scala", "CliUnitTest.scala"], + sources = rglobs("*.scala", exclude = ["CliTestLib.scala", "CliBootstrapIntegrationTest.scala"]), ) scala_library( diff --git a/cli/src/test/scala-2.12/coursier/cli/CliFetchIntegrationTest.scala b/cli/src/test/scala-2.12/coursier/cli/CliFetchIntegrationTest.scala index 1eac9d23d..aa09480a6 100644 --- a/cli/src/test/scala-2.12/coursier/cli/CliFetchIntegrationTest.scala +++ b/cli/src/test/scala-2.12/coursier/cli/CliFetchIntegrationTest.scala @@ -1,7 +1,7 @@ package coursier.cli import java.io._ -import java.util.zip.ZipInputStream + import java.net.URLEncoder.encode import argonaut.Argonaut._ import coursier.cli.util.{DepNode, ReportNode} @@ -27,6 +27,8 @@ class CliFetchIntegrationTest extends FlatSpec with CliTestLib { } } + private val fileNameLength: DepNode => Int = _.file.getOrElse("").length + "Normal fetch" should "get all files" in { val fetchOpt = FetchOptions(common = CommonOptions()) @@ -92,7 +94,7 @@ class CliFetchIntegrationTest extends FlatSpec with CliTestLib { // org.apache.commons:commons-compress:1.4.1 should not contain deps underneath it. val compressNode = node.dependencies.find(_.coord == "org.apache.commons:commons-compress:1.4.1") - assert(compressNode.isDefined) + assert(node.dependencies.exists(_.coord == "org.apache.commons:commons-compress:1.4.1")) assert(compressNode.get.dependencies.isEmpty) } } @@ -200,10 +202,10 @@ class CliFetchIntegrationTest extends FlatSpec with CliTestLib { val node: ReportNode = getReportFromJson(jsonFile) - val compressNode = node.dependencies.find(_.coord == "org.apache.commons:commons-compress:1.5") + val compressNode = node.dependencies.find(_.coord == "org.apache.commons:commons-compress:jar:tests: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")) + compressNode.get.file.map(f => assert(f.contains("commons-compress-1.5-tests.jar"))).orElse(fail("Not Defined")) assert(compressNode.get.dependencies.contains("org.tukaani:xz:1.2")) } } @@ -237,14 +239,15 @@ class CliFetchIntegrationTest extends FlatSpec with CliTestLib { 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")) + .filter(_.coord.startsWith("org.apache.commons:commons-compress")) + .sortBy(_.coord.length) // sort by coord length - assert(compressNodes.last.files.head._1 == "tests") - assert(compressNodes.last.files.head._2.contains("commons-compress-1.5-tests.jar")) + assert(compressNodes.length == 2) + assert(compressNodes.head.coord == "org.apache.commons:commons-compress:1.5") + compressNodes.head.file.map( f => assert(f.contains("commons-compress-1.5.jar"))).orElse(fail("Not Defined")) + + assert(compressNodes.last.coord == "org.apache.commons:commons-compress:jar:tests:1.5") + compressNodes.last.file.map( f => assert(f.contains("commons-compress-1.5-tests.jar"))).orElse(fail("Not Defined")) } } } @@ -266,8 +269,7 @@ class CliFetchIntegrationTest extends FlatSpec with CliTestLib { 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")) + compressNode.get.file.map( f => assert(f.contains("commons-compress-1.5.jar"))).orElse(fail("Not Defined")) assert(compressNode.get.dependencies.isEmpty) } @@ -290,10 +292,9 @@ class CliFetchIntegrationTest extends FlatSpec with CliTestLib { val node: ReportNode = getReportFromJson(jsonFile) - val compressNode = node.dependencies.find(_.coord == "org.apache.commons:commons-compress:1.5") + val compressNode = node.dependencies.find(_.coord == "org.apache.commons:commons-compress:jar:tests: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")) + compressNode.get.file.map( f => assert(f.contains("commons-compress-1.5-tests.jar"))).orElse(fail("Not Defined")) assert(compressNode.get.dependencies.isEmpty) } @@ -321,10 +322,10 @@ class CliFetchIntegrationTest extends FlatSpec with CliTestLib { 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") + val compressNode = node.dependencies.find(_.coord == "org.apache.commons:commons-compress:jar:tests: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")) + compressNode.get.file.map( f => assert(f.contains("commons-compress-1.4.1-tests.jar"))).orElse(fail("Not Defined")) assert(compressNode.get.dependencies.size == 1) assert(compressNode.get.dependencies.head == "org.tukaani:xz:1.0") @@ -352,10 +353,10 @@ class CliFetchIntegrationTest extends FlatSpec with CliTestLib { 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") + val compressNode = node.dependencies.find(_.coord == "org.apache.commons:commons-compress:jar:tests: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")) + compressNode.get.file.map( f => assert(f.contains("commons-compress-1.4.1-tests.jar"))).orElse(fail("Not Defined")) assert(compressNode.get.dependencies.isEmpty) } @@ -381,6 +382,29 @@ class CliFetchIntegrationTest extends FlatSpec with CliTestLib { assert(!node.dependencies.exists(_.coord.startsWith("org.scala-lang:scala-library:2.11."))) } + "com.spotify:helios-testing:0.9.193" should "have dependencies with classifiers" in withFile() { + (excludeFile, _) => + withFile() { + (jsonFile, _) => { + val commonOpt = CommonOptions(jsonOutputFile = jsonFile.getPath) + val fetchOpt = FetchOptions(common = commonOpt) + + val heliosCoord = "com.spotify:helios-testing:0.9.193" + + Fetch( + fetchOpt, + RemainingArgs(Seq(heliosCoord), Seq()) + ) + val node: ReportNode = getReportFromJson(jsonFile) + val testEntry: DepNode = node.dependencies.find(_.coord == heliosCoord).get + assert( + testEntry.dependencies.exists(_.startsWith("com.spotify:docker-client:jar:shaded:"))) + assert( + node.dependencies.exists(_.coord.startsWith("com.spotify:docker-client:jar:shaded:"))) + } + } + } + /** * Result: * |└─ org.apache.commons:commons-compress:1.5 @@ -411,11 +435,10 @@ class CliFetchIntegrationTest extends FlatSpec with CliTestLib { val depNodes: Seq[DepNode] = node.dependencies .filter(_.coord == "org.apache.commons:commons-compress:1.5") - .sortBy(_.files.head._1.length) + .sortBy(fileNameLength) assert(depNodes.length == 1) - val urlInJsonFile = depNodes.head.files.head._2 - assert(depNodes.head.files.head._1 == "") + val urlInJsonFile = depNodes.head.file.get assert(urlInJsonFile.contains(path)) // open jar and inspect contents @@ -453,10 +476,9 @@ class CliFetchIntegrationTest extends FlatSpec with CliTestLib { val depNodes: Seq[DepNode] = node.dependencies .filter(_.coord == "org.apache.commons:commons-compress:1.5") - .sortBy(_.files.head._1.length) + .sortBy(fileNameLength) assert(depNodes.length == 1) - assert(depNodes.head.files.head._1 == "") - assert(depNodes.head.files.head._2.contains("junit/junit/4.12/junit-4.12.jar")) + depNodes.head.file.map( f => assert(f.contains("junit/junit/4.12/junit-4.12.jar"))).orElse(fail("Not Defined")) } } @@ -487,10 +509,9 @@ class CliFetchIntegrationTest extends FlatSpec with CliTestLib { val depNodes: Seq[DepNode] = node.dependencies .filter(_.coord == "a:b:c") - .sortBy(_.files.head._1.length) + .sortBy(fileNameLength) assert(depNodes.length == 1) - assert(depNodes.head.files.head._1 == "") - assert(depNodes.head.files.head._2.contains("junit/junit/4.12/junit-4.12.jar")) + depNodes.head.file.map( f => assert(f.contains("junit/junit/4.12/junit-4.12.jar"))).orElse(fail("Not Defined")) } } @@ -519,12 +540,96 @@ class CliFetchIntegrationTest extends FlatSpec with CliTestLib { val node: ReportNode = getReportFromJson(jsonFile) val depNodes: Seq[DepNode] = node.dependencies - .filter(_.coord == "org.apache.commons:commons-compress:1.5") - .sortBy(_.files.head._1.length) + .filter(_.coord.startsWith("org.apache.commons:commons-compress:")) + .sortBy(fileNameLength) + + + val coords: Seq[String] = node.dependencies + .map(_.coord) + .sorted + assert(depNodes.length == 1) // classifier doesn't matter when we have a url so it is not listed - assert(depNodes.head.files.head._1 == "") - assert(depNodes.head.files.head._2.contains("junit/junit/4.12/junit-4.12.jar")) + depNodes.head.file.map( f => assert(f.contains("junit/junit/4.12/junit-4.12.jar"))).orElse(fail("Not Defined")) + } + } + + /** + * Result: + * |└─ org.apache.commons:commons-compress:1.5 + * | └─ org.tukaani:xz:1.2 + * |└─ org.tukaani:xz:1.2 // with the file from the URL + */ + "external dep url with classifier that is a transitive dep" should "fetch junit-4.12.jar and classifier gets thrown away" in withFile() { + (jsonFile, _) => { + val commonOpt = CommonOptions(jsonOutputFile = jsonFile.getPath) + val fetchOpt = FetchOptions(common = commonOpt) + + // encode path to different jar than requested + val externalUrl = encode("http://central.maven.org/maven2/junit/junit/4.12/junit-4.12.jar", "UTF-8") + + Fetch.run( + fetchOpt, + RemainingArgs( + Seq( + "org.apache.commons:commons-compress:1.5", + "org.tukaani:xz:1.2,classifier=tests,url="+externalUrl + ), + Seq() + ) + ) + + val node: ReportNode = getReportFromJson(jsonFile) + val depNodes: Seq[DepNode] = node.dependencies + .filter(_.coord.startsWith("org.tukaani:xz:")) + .sortBy(fileNameLength) + val coords: Seq[String] = node.dependencies.map(_.coord).sorted + + assert(coords == Seq("org.apache.commons:commons-compress:1.5", "org.tukaani:xz:1.2")) + assert(depNodes.length == 1) + assert(depNodes.last.file.isDefined) + depNodes.last.file.map( f => assert(f.contains("junit/junit/4.12/junit-4.12.jar"))).orElse(fail("Not Defined")) + } + } + + /** + * Result: + * |└─ org.apache.commons:commons-compress:1.5,classifier=sources + * └─ org.tukaani:xz:1.2,classifier=sources + */ + "classifier sources" should "fetch sources jar" in withFile() { + (jsonFile, _) => { + val commonOpt = CommonOptions(jsonOutputFile = jsonFile.getPath) + val fetchOpt = FetchOptions(common = commonOpt, sources=true) + + // encode path to different jar than requested + + Fetch.run( + fetchOpt, + RemainingArgs( + Seq( + "org.apache.commons:commons-compress:1.5,classifier=sources" + ), + Seq() + ) + ) + val node: ReportNode = getReportFromJson(jsonFile) + val coords: Seq[String] = node.dependencies.map(_.coord).sorted + val depNodes: Seq[DepNode] = node.dependencies + .filter(_.coord.startsWith("org.apache.commons")) + .sortBy(fileNameLength) + + assert(depNodes.length == 1) + assert(depNodes.head.file.isDefined) + depNodes.head.file.map(f => assert(f.contains("1.5-sources.jar"))).orElse(fail("Not Defined")) + depNodes.head.dependencies.foreach(d => { + assert(d.contains(":sources:")) + }) + + assert(coords == Seq( + "org.apache.commons:commons-compress:jar:sources:1.5", + "org.tukaani:xz:jar:sources:1.2") + ) } } @@ -559,24 +664,23 @@ class CliFetchIntegrationTest extends FlatSpec with CliTestLib { val compressNodes = depNodes .filter(_.coord == "org.apache.commons:commons-compress:1.5") - .sortBy(_.files.head._1.length) + .sortBy(fileNameLength) assert(compressNodes.length == 1) - assert(compressNodes.head.files.head._1 == "") - assert(compressNodes.head.files.head._2.contains("junit/junit/4.12/junit-4.12.jar")) + compressNodes.head.file.map( f => assert(f.contains("junit/junit/4.12/junit-4.12.jar"))).orElse(fail("Not Defined")) val jacksonMapperNodes = depNodes .filter(_.coord == "org.codehaus.jackson:jackson-mapper-asl:1.8.8") - .sortBy(_.files.head._1.length) + .sortBy(fileNameLength) assert(jacksonMapperNodes.length == 1) - assert(jacksonMapperNodes.head.files.head._2.contains("org/codehaus/jackson/jackson-mapper-asl/1.8.8/jackson-mapper-asl-1.8.8.jar")) + jacksonMapperNodes.head.file.map( f => assert(f.contains("org/codehaus/jackson/jackson-mapper-asl/1.8.8/jackson-mapper-asl-1.8.8.jar"))).orElse(fail("Not Defined")) assert(jacksonMapperNodes.head.dependencies.size == 1) assert(jacksonMapperNodes.head.dependencies.head == "org.codehaus.jackson:jackson-core-asl:1.8.8") val jacksonCoreNodes = depNodes .filter(_.coord == "org.codehaus.jackson:jackson-core-asl:1.8.8") - .sortBy(_.files.head._1.length) + .sortBy(fileNameLength) assert(jacksonCoreNodes.length == 1) - assert(jacksonCoreNodes.head.files.head._2.contains("org/codehaus/jackson/jackson-core-asl/1.8.8/jackson-core-asl-1.8.8.jar")) + jacksonCoreNodes.head.file.map( f => assert(f.contains("org/codehaus/jackson/jackson-core-asl/1.8.8/jackson-core-asl-1.8.8.jar"))).orElse(fail("Not Defined")) } } @@ -634,8 +738,7 @@ class CliFetchIntegrationTest extends FlatSpec with CliTestLib { val depNodes: Seq[DepNode] = node.dependencies assert(depNodes.length == 1) - assert(depNodes.head.files.head._1 == "") - assert(depNodes.head.files.head._2.contains("junit/junit/4.12/junit-4.12.jar")) + depNodes.head.file.map( f => assert(f.contains("junit/junit/4.12/junit-4.12.jar"))).orElse(fail("Not Defined")) } } @@ -666,10 +769,9 @@ class CliFetchIntegrationTest extends FlatSpec with CliTestLib { val depNodes: Seq[DepNode] = node.dependencies .filter(_.coord == "org.apache.commons:commons-compress:1.5") - .sortBy(_.files.head._1.length) + .sortBy(fileNameLength) assert(depNodes.length == 1) - assert(depNodes.head.files.head._1 == "") - assert(depNodes.head.files.head._2.contains("junit/junit/4.12/junit-4.12.jar")) + depNodes.head.file.map( f => assert(f.contains("junit/junit/4.12/junit-4.12.jar"))).orElse(fail("Not Defined")) } } @@ -701,7 +803,7 @@ class CliFetchIntegrationTest extends FlatSpec with CliTestLib { val depNode = node.dependencies.find(_.coord == "org.apache.commons:commons-compress:1.5") assert(depNode.isDefined) - assert(depNode.get.files.head._2.contains("commons-compress-1.5.jar")) + depNode.get.file.map( f => assert(f.contains("commons-compress-1.5.jar"))).orElse(fail("Not Defined")) assert(depNode.get.dependencies.size == 1) assert(depNode.get.dependencies.head.contains("org.tukaani:xz:1.2")) diff --git a/cli/src/test/scala-2.12/coursier/cli/util/JsonReportTest.scala b/cli/src/test/scala-2.12/coursier/cli/util/JsonReportTest.scala new file mode 100644 index 000000000..39ec06e2c --- /dev/null +++ b/cli/src/test/scala-2.12/coursier/cli/util/JsonReportTest.scala @@ -0,0 +1,60 @@ +package coursier.cli.util + +import coursier.cli.CliTestLib +import org.junit.runner.RunWith +import org.scalatest.FlatSpec +import org.scalatest.junit.JUnitRunner + +@RunWith(classOf[JUnitRunner]) +class JsonReportTest extends FlatSpec with CliTestLib { + "empty JsonReport" should "be empty" in { + val report: String = JsonReport[String](IndexedSeq(), Map(), Set())( + children = _ => Seq(), + reconciledVersionStr = _ => "", + requestedVersionStr = _ => "", + getFile = _ => Option("") + ) + + assert( + report == "{\"conflict_resolution\":{},\"dependencies\":[],\"version\":\"0.1.0\"}") + } + + "JsonReport containing two deps" should "not be empty" in { + val children = Map("a" -> Seq("b"), "b" -> Seq()) + val report: String = JsonReport[String]( + roots = IndexedSeq("a", "b"), + conflictResolutionForRoots = Map(), + overrideClassifiers = Set() + )( + children = children(_), + reconciledVersionStr = s => s"$s:reconciled", + requestedVersionStr = s => s"$s:requested", + getFile = _ => Option("") + ) + + assert( + report == "{\"conflict_resolution\":{},\"dependencies\":[" + + "{\"coord\":\"a:reconciled\",\"file\":\"\",\"dependencies\":[\"b:reconciled\"]}," + + "{\"coord\":\"b:reconciled\",\"file\":\"\",\"dependencies\":[]}]," + + "\"version\":\"0.1.0\"}") + } + "JsonReport containing two deps" should "be sorted alphabetically regardless of input order" in { + val children = Map("a" -> Seq("b"), "b" -> Seq()) + val report: String = JsonReport[String]( + roots = IndexedSeq( "b", "a"), + conflictResolutionForRoots = Map(), + overrideClassifiers = Set() + )( + children = children(_), + reconciledVersionStr = s => s"$s:reconciled", + requestedVersionStr = s => s"$s:requested", + getFile = _ => Option("") + ) + + assert( + report == "{\"conflict_resolution\":{},\"dependencies\":[" + + "{\"coord\":\"a:reconciled\",\"file\":\"\",\"dependencies\":[\"b:reconciled\"]}," + + "{\"coord\":\"b:reconciled\",\"file\":\"\",\"dependencies\":[]}]," + + "\"version\":\"0.1.0\"}") + } +} diff --git a/core/shared/src/main/scala/coursier/core/Definitions.scala b/core/shared/src/main/scala/coursier/core/Definitions.scala index 82b23d584..859c9e565 100644 --- a/core/shared/src/main/scala/coursier/core/Definitions.scala +++ b/core/shared/src/main/scala/coursier/core/Definitions.scala @@ -58,6 +58,14 @@ final case class Dependency( lazy val moduleVersion = (module, version) override lazy val hashCode = Dependency.unapply(this).get.hashCode() + + def mavenPrefix: String = { + if (attributes.isEmpty) + module.orgName + else { + s"${module.orgName}:${attributes.packagingAndClassifier}" + } + } } // Maven-specific @@ -65,6 +73,19 @@ final case class Attributes( `type`: String, classifier: String ) { + def packaging: String = if (`type`.isEmpty) + "jar" + else + `type` + + def packagingAndClassifier: String = if (isEmpty) { + "" + } else if (classifier.isEmpty) { + packaging + } else { + s"$packaging:$classifier" + } + def publication(name: String, ext: String): Publication = Publication(name, `type`, ext, classifier) @@ -238,4 +259,4 @@ final case class Authentication( ) { override def toString: String = s"Authentication($user, *******)" -} \ No newline at end of file +}