From 0de5330351b09bff6628e43ed23d1e6150105f1b Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Thu, 25 Jun 2015 00:18:49 +0100 Subject: [PATCH] Small refactoring --- .../core/{Resolver.scala => Resolution.scala} | 573 +++++++++--------- core/src/main/scala/coursier/package.scala | 8 +- ...olverTests.scala => ResolutionTests.scala} | 5 +- 3 files changed, 293 insertions(+), 293 deletions(-) rename core/src/main/scala/coursier/core/{Resolver.scala => Resolution.scala} (53%) rename core/src/test/scala/coursier/test/{ResolverTests.scala => ResolutionTests.scala} (99%) diff --git a/core/src/main/scala/coursier/core/Resolver.scala b/core/src/main/scala/coursier/core/Resolution.scala similarity index 53% rename from core/src/main/scala/coursier/core/Resolver.scala rename to core/src/main/scala/coursier/core/Resolution.scala index 035c047aa..4ccabce8f 100644 --- a/core/src/main/scala/coursier/core/Resolver.scala +++ b/core/src/main/scala/coursier/core/Resolution.scala @@ -7,7 +7,7 @@ import scala.collection.mutable import scalaz.concurrent.Task import scalaz.{EitherT, \/-, \/, -\/} -object Resolver { +object Resolution { type ModuleVersion = (Module, String) @@ -283,291 +283,6 @@ object Resolver { } } - /** - * State of a dependency resolution. - * - * Done if method `isDone` returns `true`. - * - * @param dependencies: current set of dependencies - * @param conflicts: conflicting dependencies - * @param projectsCache: cache of known projects - * @param errors: keeps track of the modules whose project definition could not be found - */ - case class Resolution(rootDependencies: Set[Dependency], - dependencies: Set[Dependency], - conflicts: Set[Dependency], - projectsCache: Map[ModuleVersion, (Repository, Project)], - errors: Map[ModuleVersion, Seq[String]], - filter: Option[Dependency => Boolean], - profileActivation: Option[(String, Activation, Map[String, String]) => Boolean]) { - - private val finalDependenciesCache = new mutable.HashMap[Dependency, Seq[Dependency]]() - private def finalDependencies0(dep: Dependency) = finalDependenciesCache.synchronized { - finalDependenciesCache.getOrElseUpdate(dep, - projectsCache.get(dep.moduleVersion) match { - case Some((_, proj)) => finalDependencies(dep, proj).filter(filter getOrElse defaultFilter) - case None => Nil - } - ) - } - - /** - * Transitive dependencies of the current dependencies, according to what there currently is in cache. - * No attempt is made to solve version conflicts here. - */ - def transitiveDependencies: Seq[Dependency] = - for { - dep <- (dependencies -- conflicts).toList - trDep <- finalDependencies0(dep) - } yield trDep - - /** - * The "next" dependency set, made of the current dependencies and their transitive dependencies, - * trying to solve version conflicts. Transitive dependencies are calculated with the current cache. - * - * May contain dependencies added in previous iterations, but no more required. These are filtered below, see - * `newDependencies`. - * - * Returns a tuple made of the conflicting dependencies, and all the dependencies. - */ - def nextDependenciesAndConflicts: (Seq[Dependency], Seq[Dependency]) = { - merge(dependencies ++ transitiveDependencies) - } - - /** - * The modules we miss some info about. - */ - def missingFromCache: Set[ModuleVersion] = { - val modules = dependencies.map(_.moduleVersion) - val nextModules = nextDependenciesAndConflicts._2.map(_.moduleVersion) - - (modules ++ nextModules) - .filterNot(mod => projectsCache.contains(mod) || errors.contains(mod)) - } - - - /** - * Whether the resolution is done. - */ - def isDone: Boolean = { - def isFixPoint = { - val (nextConflicts, _) = nextDependenciesAndConflicts - dependencies == (newDependencies ++ nextConflicts) && conflicts == nextConflicts.toSet - } - - missingFromCache.isEmpty && isFixPoint - } - - private def eraseVersion(dep: Dependency) = dep.copy(version = "") - - /** - * Returns a map giving the dependencies that brought each of the dependency of the "next" dependency set. - * - * The versions of all the dependencies returned are erased (emptied). - */ - def reverseDependencies: Map[Dependency, Vector[Dependency]] = { - val (updatedConflicts, updatedDeps) = nextDependenciesAndConflicts - - val trDepsSeq = - for { - dep <- updatedDeps - trDep <- finalDependencies0(dep) - } yield eraseVersion(trDep) -> eraseVersion(dep) - - val knownDeps = (updatedDeps ++ updatedConflicts).map(eraseVersion).toSet - - trDepsSeq - .groupBy(_._1) - .mapValues(_.map(_._2).toVector) - .filterKeys(knownDeps) - .toList.toMap // Eagerly evaluate filterKeys/mapValues - } - - /** - * Returns dependencies from the "next" dependency set, filtering out - * those that are no more required. - * - * The versions of all the dependencies returned are erased (emptied). - */ - def remainingDependencies: Set[Dependency] = { - val rootDependencies0 = rootDependencies.map(eraseVersion) - - @tailrec - def helper(reverseDeps: Map[Dependency, Vector[Dependency]]): Map[Dependency, Vector[Dependency]] = { - val (toRemove, remaining) = reverseDeps.partition(kv => kv._2.isEmpty && !rootDependencies0(kv._1)) - - if (toRemove.isEmpty) reverseDeps - else helper(remaining.mapValues(_.filter(x => remaining.contains(x) || rootDependencies0(x))).toList.toMap) - } - - val filteredReverseDependencies = helper(reverseDependencies) - - rootDependencies0 ++ filteredReverseDependencies.keys - } - - /** - * The final next dependency set, stripped of no more required ones. - */ - def newDependencies: Set[Dependency] = { - val remainingDependencies0 = remainingDependencies - nextDependenciesAndConflicts._2 - .filter(dep => remainingDependencies0(eraseVersion(dep))) - .toSet - } - - private def nextNoMissingUnsafe: Resolution = { - val (newConflicts, _) = nextDependenciesAndConflicts - copy(dependencies = newDependencies ++ newConflicts, conflicts = newConflicts.toSet) - } - - /** - * If no module info is missing, the next state of the resolution, which can be immediately calculated. - * Else, the current resolution itself. - */ - def nextIfNoMissing: Resolution = { - val missing = missingFromCache - if (missing.isEmpty) nextNoMissingUnsafe - else this - } - - /** - * Do a new iteration, fetching the missing modules along the way. - */ - def next(fetchModule: ModuleVersion => EitherT[Task, List[String], (Repository, Project)]): Task[Resolution] = { - val missing = missingFromCache - if (missing.isEmpty) Task.now(nextNoMissingUnsafe) - else fetch(missing.toList, fetchModule).map(_.nextIfNoMissing) - } - - /** - * Required modules for the dependency management of `project`. - */ - def dependencyManagementRequirements(project: Project): Set[ModuleVersion] = { - val approxProperties = - project.parent - .flatMap(projectsCache.get) - .map(_._2.properties) - .fold(project.properties)(mergeProperties(project.properties, _)) - - val profileDependencies = - profiles(project, approxProperties, profileActivation getOrElse defaultProfileActivation) - .flatMap(_.dependencies) - - val modules = - (project.dependencies ++ profileDependencies) - .collect{ case dep if dep.scope == Scope.Import => dep.moduleVersion } ++ - project.parent - - modules.toSet - } - - /** - * Missing modules in cache, to get the full list of dependencies of `project`, taking - * dependency management / inheritance into account. - * - * Note that adding the missing modules to the cache may unveil other missing modules, so - * these modules should be added to the cache, and `dependencyManagementMissing` checked again - * for new missing modules. - */ - def dependencyManagementMissing(project: Project): Set[ModuleVersion] = { - - @tailrec - def helper(toCheck: Set[ModuleVersion], - done: Set[ModuleVersion], - missing: Set[ModuleVersion]): Set[ModuleVersion] = { - - if (toCheck.isEmpty) missing - else if (toCheck.exists(done)) helper(toCheck -- done, done, missing) - else if (toCheck.exists(missing)) helper(toCheck -- missing, done, missing) - else if (toCheck.exists(projectsCache.contains)) { - val (checking, remaining) = toCheck.partition(projectsCache.contains) - val directRequirements = checking.flatMap(mod => dependencyManagementRequirements(projectsCache(mod)._2)) - - helper(remaining ++ directRequirements, done ++ checking, missing) - } else if (toCheck.exists(errors.contains)) { - val (errored, remaining) = toCheck.partition(errors.contains) - helper(remaining, done ++ errored, missing) - } else - helper(Set.empty, done, missing ++ toCheck) - } - - helper(dependencyManagementRequirements(project), Set(project.moduleVersion), Set.empty) - } - - /** - * Add dependency management / inheritance related items to `project`, from what's available in cache. - * It is recommended to have fetched what `dependencyManagementMissing` returned prior to calling - * `withDependencyManagement`. - */ - def withDependencyManagement(project: Project): Project = { - - val approxProperties = - project.parent - .filter(projectsCache.contains) - .map(projectsCache(_)._2.properties) - .fold(project.properties)(mergeProperties(project.properties, _)) - - val profiles0 = profiles(project, approxProperties, profileActivation getOrElse defaultProfileActivation) - - val dependencies0 = addDependencies(project.dependencies +: profiles0.map(_.dependencies)) - val properties0 = (project.properties /: profiles0)((acc, p) => mergeProperties(acc, p.properties)) - - val deps = - dependencies0 - .collect{ case dep if dep.scope == Scope.Import && projectsCache.contains(dep.moduleVersion) => dep.moduleVersion } ++ - project.parent.filter(projectsCache.contains) - val projs = deps.map(projectsCache(_)._2) - - val depMgmt = - (project.dependencyManagement +: (profiles0.map(_.dependencyManagement) ++ projs.map(_.dependencyManagement))) - .foldLeft(Map.empty[DepMgmtKey, Dependency])(dependencyManagementAddSeq) - - val depsSet = deps.toSet - - project.copy( - dependencies = dependencies0 - .filterNot(dep => dep.scope == Scope.Import && depsSet(dep.moduleVersion)) ++ - project.parent - .filter(projectsCache.contains) - .toSeq - .flatMap(projectsCache(_)._2.dependencies), - dependencyManagement = depMgmt.values.toSeq, - properties = project.parent - .filter(projectsCache.contains) - .map(projectsCache(_)._2.properties) - .fold(properties0)(mergeProperties(properties0, _)) - ) - } - - /** - * Fetch `modules` with `fetchModules`, and add the resulting errors and projects to the cache. - */ - def fetch(modules: Seq[ModuleVersion], - fetchModule: ModuleVersion => EitherT[Task, List[String], (Repository, Project)]): Task[Resolution] = { - - val lookups = modules.map(dep => fetchModule(dep).run.map(dep -> _)) - val gatheredLookups = Task.gatherUnordered(lookups, exceptionCancels = true) - gatheredLookups.flatMap{ lookupResults => - val errors0 = errors ++ lookupResults.collect{case (modVer, -\/(repoErrors)) => modVer -> repoErrors} - val newProjects = lookupResults.collect{case (modVer, \/-(proj)) => modVer -> proj} - - /* - * newProjects are project definitions, fresh from the repositories. We need to add - * dependency management / inheritance-related bits to them. - */ - - newProjects.foldLeft(Task.now(copy(errors = errors0))) { case (accTask, (modVer, (repo, proj))) => - for { - current <- accTask - updated <- current.fetch(current.dependencyManagementMissing(proj).toList, fetchModule) - proj0 = updated.withDependencyManagement(proj) - } yield updated.copy(projectsCache = updated.projectsCache + (modVer -> (repo, proj0))) - } - } - } - - } - /** * Default function checking whether a profile is active, given its id, activation conditions, * and the properties of its project. @@ -630,5 +345,291 @@ object Resolver { helper(startResolution, maxIterations).map(_._1) } +} + + + /** + * State of a dependency resolution. + * + * Done if method `isDone` returns `true`. + * + * @param dependencies: current set of dependencies + * @param conflicts: conflicting dependencies + * @param projectsCache: cache of known projects + * @param errors: keeps track of the modules whose project definition could not be found + */ +case class Resolution(rootDependencies: Set[Dependency], + dependencies: Set[Dependency], + conflicts: Set[Dependency], + projectsCache: Map[Resolution.ModuleVersion, (Repository, Project)], + errors: Map[Resolution.ModuleVersion, Seq[String]], + filter: Option[Dependency => Boolean], + profileActivation: Option[(String, Activation, Map[String, String]) => Boolean]) { + import Resolution._ + + private val finalDependenciesCache = new mutable.HashMap[Dependency, Seq[Dependency]]() + private def finalDependencies0(dep: Dependency) = finalDependenciesCache.synchronized { + finalDependenciesCache.getOrElseUpdate(dep, + projectsCache.get(dep.moduleVersion) match { + case Some((_, proj)) => finalDependencies(dep, proj).filter(filter getOrElse defaultFilter) + case None => Nil + } + ) + } + + /** + * Transitive dependencies of the current dependencies, according to what there currently is in cache. + * No attempt is made to solve version conflicts here. + */ + def transitiveDependencies: Seq[Dependency] = + for { + dep <- (dependencies -- conflicts).toList + trDep <- finalDependencies0(dep) + } yield trDep + + /** + * The "next" dependency set, made of the current dependencies and their transitive dependencies, + * trying to solve version conflicts. Transitive dependencies are calculated with the current cache. + * + * May contain dependencies added in previous iterations, but no more required. These are filtered below, see + * `newDependencies`. + * + * Returns a tuple made of the conflicting dependencies, and all the dependencies. + */ + def nextDependenciesAndConflicts: (Seq[Dependency], Seq[Dependency]) = { + merge(dependencies ++ transitiveDependencies) + } + + /** + * The modules we miss some info about. + */ + def missingFromCache: Set[ModuleVersion] = { + val modules = dependencies.map(_.moduleVersion) + val nextModules = nextDependenciesAndConflicts._2.map(_.moduleVersion) + + (modules ++ nextModules) + .filterNot(mod => projectsCache.contains(mod) || errors.contains(mod)) + } + + + /** + * Whether the resolution is done. + */ + def isDone: Boolean = { + def isFixPoint = { + val (nextConflicts, _) = nextDependenciesAndConflicts + dependencies == (newDependencies ++ nextConflicts) && conflicts == nextConflicts.toSet + } + + missingFromCache.isEmpty && isFixPoint + } + + private def eraseVersion(dep: Dependency) = dep.copy(version = "") + + /** + * Returns a map giving the dependencies that brought each of the dependency of the "next" dependency set. + * + * The versions of all the dependencies returned are erased (emptied). + */ + def reverseDependencies: Map[Dependency, Vector[Dependency]] = { + val (updatedConflicts, updatedDeps) = nextDependenciesAndConflicts + + val trDepsSeq = + for { + dep <- updatedDeps + trDep <- finalDependencies0(dep) + } yield eraseVersion(trDep) -> eraseVersion(dep) + + val knownDeps = (updatedDeps ++ updatedConflicts).map(eraseVersion).toSet + + trDepsSeq + .groupBy(_._1) + .mapValues(_.map(_._2).toVector) + .filterKeys(knownDeps) + .toList.toMap // Eagerly evaluate filterKeys/mapValues + } + + /** + * Returns dependencies from the "next" dependency set, filtering out + * those that are no more required. + * + * The versions of all the dependencies returned are erased (emptied). + */ + def remainingDependencies: Set[Dependency] = { + val rootDependencies0 = rootDependencies.map(eraseVersion) + + @tailrec + def helper(reverseDeps: Map[Dependency, Vector[Dependency]]): Map[Dependency, Vector[Dependency]] = { + val (toRemove, remaining) = reverseDeps.partition(kv => kv._2.isEmpty && !rootDependencies0(kv._1)) + + if (toRemove.isEmpty) reverseDeps + else helper(remaining.mapValues(_.filter(x => remaining.contains(x) || rootDependencies0(x))).toList.toMap) + } + + val filteredReverseDependencies = helper(reverseDependencies) + + rootDependencies0 ++ filteredReverseDependencies.keys + } + + /** + * The final next dependency set, stripped of no more required ones. + */ + def newDependencies: Set[Dependency] = { + val remainingDependencies0 = remainingDependencies + nextDependenciesAndConflicts._2 + .filter(dep => remainingDependencies0(eraseVersion(dep))) + .toSet + } + + private def nextNoMissingUnsafe: Resolution = { + val (newConflicts, _) = nextDependenciesAndConflicts + copy(dependencies = newDependencies ++ newConflicts, conflicts = newConflicts.toSet) + } + + /** + * If no module info is missing, the next state of the resolution, which can be immediately calculated. + * Else, the current resolution itself. + */ + def nextIfNoMissing: Resolution = { + val missing = missingFromCache + if (missing.isEmpty) nextNoMissingUnsafe + else this + } + + /** + * Do a new iteration, fetching the missing modules along the way. + */ + def next(fetchModule: ModuleVersion => EitherT[Task, List[String], (Repository, Project)]): Task[Resolution] = { + val missing = missingFromCache + if (missing.isEmpty) Task.now(nextNoMissingUnsafe) + else fetch(missing.toList, fetchModule).map(_.nextIfNoMissing) + } + + /** + * Required modules for the dependency management of `project`. + */ + def dependencyManagementRequirements(project: Project): Set[ModuleVersion] = { + val approxProperties = + project.parent + .flatMap(projectsCache.get) + .map(_._2.properties) + .fold(project.properties)(mergeProperties(project.properties, _)) + + val profileDependencies = + profiles(project, approxProperties, profileActivation getOrElse defaultProfileActivation) + .flatMap(_.dependencies) + + val modules = + (project.dependencies ++ profileDependencies) + .collect{ case dep if dep.scope == Scope.Import => dep.moduleVersion } ++ + project.parent + + modules.toSet + } + + /** + * Missing modules in cache, to get the full list of dependencies of `project`, taking + * dependency management / inheritance into account. + * + * Note that adding the missing modules to the cache may unveil other missing modules, so + * these modules should be added to the cache, and `dependencyManagementMissing` checked again + * for new missing modules. + */ + def dependencyManagementMissing(project: Project): Set[ModuleVersion] = { + + @tailrec + def helper(toCheck: Set[ModuleVersion], + done: Set[ModuleVersion], + missing: Set[ModuleVersion]): Set[ModuleVersion] = { + + if (toCheck.isEmpty) missing + else if (toCheck.exists(done)) helper(toCheck -- done, done, missing) + else if (toCheck.exists(missing)) helper(toCheck -- missing, done, missing) + else if (toCheck.exists(projectsCache.contains)) { + val (checking, remaining) = toCheck.partition(projectsCache.contains) + val directRequirements = checking.flatMap(mod => dependencyManagementRequirements(projectsCache(mod)._2)) + + helper(remaining ++ directRequirements, done ++ checking, missing) + } else if (toCheck.exists(errors.contains)) { + val (errored, remaining) = toCheck.partition(errors.contains) + helper(remaining, done ++ errored, missing) + } else + helper(Set.empty, done, missing ++ toCheck) + } + + helper(dependencyManagementRequirements(project), Set(project.moduleVersion), Set.empty) + } + + /** + * Add dependency management / inheritance related items to `project`, from what's available in cache. + * It is recommended to have fetched what `dependencyManagementMissing` returned prior to calling + * `withDependencyManagement`. + */ + def withDependencyManagement(project: Project): Project = { + + val approxProperties = + project.parent + .filter(projectsCache.contains) + .map(projectsCache(_)._2.properties) + .fold(project.properties)(mergeProperties(project.properties, _)) + + val profiles0 = profiles(project, approxProperties, profileActivation getOrElse defaultProfileActivation) + + val dependencies0 = addDependencies(project.dependencies +: profiles0.map(_.dependencies)) + val properties0 = (project.properties /: profiles0)((acc, p) => mergeProperties(acc, p.properties)) + + val deps = + dependencies0 + .collect{ case dep if dep.scope == Scope.Import && projectsCache.contains(dep.moduleVersion) => dep.moduleVersion } ++ + project.parent.filter(projectsCache.contains) + val projs = deps.map(projectsCache(_)._2) + + val depMgmt = + (project.dependencyManagement +: (profiles0.map(_.dependencyManagement) ++ projs.map(_.dependencyManagement))) + .foldLeft(Map.empty[DepMgmtKey, Dependency])(dependencyManagementAddSeq) + + val depsSet = deps.toSet + + project.copy( + dependencies = dependencies0 + .filterNot(dep => dep.scope == Scope.Import && depsSet(dep.moduleVersion)) ++ + project.parent + .filter(projectsCache.contains) + .toSeq + .flatMap(projectsCache(_)._2.dependencies), + dependencyManagement = depMgmt.values.toSeq, + properties = project.parent + .filter(projectsCache.contains) + .map(projectsCache(_)._2.properties) + .fold(properties0)(mergeProperties(properties0, _)) + ) + } + + /** + * Fetch `modules` with `fetchModules`, and add the resulting errors and projects to the cache. + */ + def fetch(modules: Seq[ModuleVersion], + fetchModule: ModuleVersion => EitherT[Task, List[String], (Repository, Project)]): Task[Resolution] = { + + val lookups = modules.map(dep => fetchModule(dep).run.map(dep -> _)) + val gatheredLookups = Task.gatherUnordered(lookups, exceptionCancels = true) + gatheredLookups.flatMap{ lookupResults => + val errors0 = errors ++ lookupResults.collect{case (modVer, -\/(repoErrors)) => modVer -> repoErrors} + val newProjects = lookupResults.collect{case (modVer, \/-(proj)) => modVer -> proj} + + /* + * newProjects are project definitions, fresh from the repositories. We need to add + * dependency management / inheritance-related bits to them. + */ + + newProjects.foldLeft(Task.now(copy(errors = errors0))) { case (accTask, (modVer, (repo, proj))) => + for { + current <- accTask + updated <- current.fetch(current.dependencyManagementMissing(proj).toList, fetchModule) + proj0 = updated.withDependencyManagement(proj) + } yield updated.copy(projectsCache = updated.projectsCache + (modVer -> (repo, proj0))) + } + } + } } diff --git a/core/src/main/scala/coursier/package.scala b/core/src/main/scala/coursier/package.scala index f4bbcde21..5a9a0d079 100644 --- a/core/src/main/scala/coursier/package.scala +++ b/core/src/main/scala/coursier/package.scala @@ -68,9 +68,9 @@ package object coursier { type Repository = core.Repository def fetchFrom(repositories: Seq[Repository]): ModuleVersion => EitherT[Task, List[String], (Repository, Project)] = - modVersion => core.Resolver.find(repositories, modVersion._1, modVersion._2) + modVersion => core.Resolution.find(repositories, modVersion._1, modVersion._2) - type Resolution = core.Resolver.Resolution + type Resolution = core.Resolution object Resolution { val empty = apply() def apply(rootDependencies: Set[Dependency] = Set.empty, @@ -80,7 +80,7 @@ package object coursier { errors: Map[ModuleVersion, Seq[String]] = Map.empty, filter: Option[Dependency => Boolean] = None, profileActivation: Option[(String, Profile.Activation, Map[String, String]) => Boolean] = None): Resolution = - core.Resolver.Resolution(rootDependencies, dependencies, conflicts, projectsCache, errors, filter, profileActivation) + core.Resolution(rootDependencies, dependencies, conflicts, projectsCache, errors, filter, profileActivation) } def resolve(dependencies: Set[Dependency], @@ -88,6 +88,6 @@ package object coursier { maxIterations: Option[Int] = Some(200), filter: Option[Dependency => Boolean] = None, profileActivation: Option[(String, Profile.Activation, Map[String, String]) => Boolean] = None): Task[Resolution] = { - core.Resolver.resolve(dependencies, fetch, maxIterations, filter, profileActivation) + core.Resolution.resolve(dependencies, fetch, maxIterations, filter, profileActivation) } } diff --git a/core/src/test/scala/coursier/test/ResolverTests.scala b/core/src/test/scala/coursier/test/ResolutionTests.scala similarity index 99% rename from core/src/test/scala/coursier/test/ResolverTests.scala rename to core/src/test/scala/coursier/test/ResolutionTests.scala index 58f1457c6..9e9b577b3 100644 --- a/core/src/test/scala/coursier/test/ResolverTests.scala +++ b/core/src/test/scala/coursier/test/ResolutionTests.scala @@ -1,13 +1,12 @@ package coursier package test -import coursier.core.Resolver import utest._ import scala.async.Async.{async, await} import coursier.test.compatibility._ -object ResolverTests extends TestSuite { +object ResolutionTests extends TestSuite { implicit class ProjectOps(val p: Project) extends AnyVal { def kv: (ModuleVersion, (Repository, Project)) = p.moduleVersion -> (testRepository, p) @@ -496,7 +495,7 @@ object ResolverTests extends TestSuite { 'parts{ 'propertySubstitution{ val res = - Resolver.withProperties( + core.Resolution.withProperties( Seq(Dependency(Module("a-company", "a-name"), "${a.property}")), Map("a.property" -> "a-version")) val expected = Seq(Dependency(Module("a-company", "a-name"), "a-version"))