mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-20 01:07:18 +03:00
LF: check dependencies are not compiled to newer version. (#7050)
This PR prevents a module to depend on an older version of LF than the one is compile to. CHANGELOG_BEGIN CHANGELOG_END
This commit is contained in:
parent
8bb4cccdd8
commit
a39b7cc003
@ -124,8 +124,11 @@ final class ConcurrentCompiledPackages extends MutableCompiledPackages {
|
|||||||
|
|
||||||
// update the packageMaxLanguageVersions
|
// update the packageMaxLanguageVersions
|
||||||
// If the package is empty, no update
|
// If the package is empty, no update
|
||||||
computePackageLanguageVersion(_packagesLanguageVersions, pkg)
|
pkg.modules.values.headOption
|
||||||
.foreach(_packagesLanguageVersions.update(pkgId, _))
|
.foreach(
|
||||||
|
mod =>
|
||||||
|
// all modules of a package are compiled to the same LF version
|
||||||
|
_packagesLanguageVersions.update(pkgId, mod.languageVersion))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,183 +0,0 @@
|
|||||||
// Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package com.daml.lf
|
|
||||||
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
|
||||||
|
|
||||||
import com.daml.lf.data.Ref
|
|
||||||
import com.daml.lf.data.Ref.PackageId
|
|
||||||
import com.daml.lf.engine.ConcurrentCompiledPackages
|
|
||||||
import com.daml.lf.language.Ast.Package
|
|
||||||
import com.daml.lf.language.{Ast, LanguageVersion, Util => AstUtil}
|
|
||||||
import org.scalatest.prop.TableDrivenPropertyChecks
|
|
||||||
import org.scalatest.{Matchers, WordSpec}
|
|
||||||
|
|
||||||
final class CompiledPackageSpec extends WordSpec with Matchers with TableDrivenPropertyChecks {
|
|
||||||
|
|
||||||
val Seq(v1_6, v1_7, v1_8) =
|
|
||||||
Seq("6", "7", "8").map(minor =>
|
|
||||||
LanguageVersion(LanguageVersion.Major.V1, LanguageVersion.Minor.Stable(minor)))
|
|
||||||
|
|
||||||
private[this] final class ModBuilder(languageVersion: LanguageVersion) {
|
|
||||||
|
|
||||||
import ModBuilder._
|
|
||||||
|
|
||||||
val name = Ref.ModuleName.assertFromString(s"module${counter.incrementAndGet()}")
|
|
||||||
|
|
||||||
private[this] var _deps = Set.empty[PackageId]
|
|
||||||
private[this] var _body: Ast.Expr = AstUtil.EUnit
|
|
||||||
private[this] var _built = false
|
|
||||||
|
|
||||||
def addDeps(deps: (Ref.PackageId, Ref.ModuleName)): this.type = {
|
|
||||||
assert(!_built)
|
|
||||||
val (pkgId, modName) = deps
|
|
||||||
val id = Ref.Identifier(pkgId, Ref.QualifiedName(modName, noOpName))
|
|
||||||
_deps = _deps + id.packageId
|
|
||||||
_body = Ast.ELet(Ast.Binding(None, AstUtil.TUnit, Ast.EVal(id)), _body)
|
|
||||||
this
|
|
||||||
}
|
|
||||||
|
|
||||||
def build(): (Ast.Module, Set[PackageId]) = {
|
|
||||||
_built = true
|
|
||||||
Ast.Module(
|
|
||||||
name,
|
|
||||||
Map(noOpName -> Ast.DValue(AstUtil.TUnit, false, _body, false)),
|
|
||||||
languageVersion,
|
|
||||||
Ast.FeatureFlags.default
|
|
||||||
) -> _deps
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private[this] object ModBuilder {
|
|
||||||
private val counter = new AtomicInteger()
|
|
||||||
|
|
||||||
private def noOpName = Ref.DottedName.assertFromString("noOp")
|
|
||||||
|
|
||||||
def apply(
|
|
||||||
languageVersion: LanguageVersion,
|
|
||||||
deps: (Ref.PackageId, Ref.ModuleName)*,
|
|
||||||
): ModBuilder =
|
|
||||||
deps.foldLeft(new ModBuilder(languageVersion))(_.addDeps(_))
|
|
||||||
}
|
|
||||||
|
|
||||||
private[this] final class PkgBuilder {
|
|
||||||
|
|
||||||
import PkgBuilder._
|
|
||||||
|
|
||||||
val id = Ref.PackageId.assertFromString(s"package${counter.incrementAndGet()}")
|
|
||||||
|
|
||||||
private[this] var _modules = List.empty[Ast.Module]
|
|
||||||
private[this] var _deps = Set.empty[PackageId]
|
|
||||||
private[this] var _built = false
|
|
||||||
|
|
||||||
def addMod(modBuilder: ModBuilder): this.type = {
|
|
||||||
assert(!_built)
|
|
||||||
val (mod, deps) = modBuilder.build()
|
|
||||||
_modules = mod :: _modules
|
|
||||||
_deps = _deps | deps
|
|
||||||
this
|
|
||||||
}
|
|
||||||
|
|
||||||
def build: Package = {
|
|
||||||
_built = true
|
|
||||||
Package(_modules, _deps, None)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private[this] object PkgBuilder {
|
|
||||||
private val counter = new AtomicInteger()
|
|
||||||
|
|
||||||
def apply(modBuilders: ModBuilder*): PkgBuilder =
|
|
||||||
modBuilders.foldLeft(new PkgBuilder())(_.addMod(_))
|
|
||||||
}
|
|
||||||
|
|
||||||
tests("PureCompiledPackages")(PureCompiledPackages(_).toOption.get)
|
|
||||||
|
|
||||||
tests("ConcurrentCompiledPackages") { packages =>
|
|
||||||
val compiledPackages = ConcurrentCompiledPackages()
|
|
||||||
packages.foreach {
|
|
||||||
case (pkgId, pkg) =>
|
|
||||||
compiledPackages
|
|
||||||
.addPackage(pkgId, pkg)
|
|
||||||
.consume(
|
|
||||||
_ => sys.error("unexpected error"),
|
|
||||||
packages.get,
|
|
||||||
_ => sys.error("unexpected error"),
|
|
||||||
)
|
|
||||||
.toOption
|
|
||||||
.get
|
|
||||||
}
|
|
||||||
compiledPackages
|
|
||||||
}
|
|
||||||
|
|
||||||
def tests(name: String)(_compile: Map[Ref.PackageId, Ast.Package] => CompiledPackages) =
|
|
||||||
s"$name#getMaxLanguageVersione" should {
|
|
||||||
|
|
||||||
def compile(packages: PkgBuilder*) =
|
|
||||||
_compile(packages.map(b => b.id -> b.build).toMap)
|
|
||||||
|
|
||||||
"return None for empty Package" in {
|
|
||||||
val pkg1 = PkgBuilder()
|
|
||||||
val compiledPackages = compile(pkg1)
|
|
||||||
|
|
||||||
compiledPackages.packageLanguageVersion.lift(pkg1.id) shouldBe None
|
|
||||||
}
|
|
||||||
|
|
||||||
"return None for non defined Package" in {
|
|
||||||
val compiledPackages = compile()
|
|
||||||
compiledPackages.packageLanguageVersion.lift(PkgBuilder().id) shouldBe None
|
|
||||||
}
|
|
||||||
|
|
||||||
"return the newest language version of package with unique modules" in {
|
|
||||||
val pkg = PkgBuilder(ModBuilder(v1_6))
|
|
||||||
val compiledPackages = compile(pkg)
|
|
||||||
compiledPackages.packageLanguageVersion.lift(pkg.id) shouldBe Some(v1_6)
|
|
||||||
}
|
|
||||||
|
|
||||||
"return the newest language version of package with several modules" in {
|
|
||||||
val Seq(mod6, mod7, mod8) = Seq(v1_6, v1_7, v1_8).map(ModBuilder(_))
|
|
||||||
|
|
||||||
Seq(mod6, mod7).permutations.foreach { mods =>
|
|
||||||
val pkg = PkgBuilder(mods: _*)
|
|
||||||
compile(pkg).packageLanguageVersion.lift(pkg.id) shouldBe Some(v1_7)
|
|
||||||
}
|
|
||||||
Seq(mod6, mod7, mod8).permutations.foreach { mods =>
|
|
||||||
val pkg = PkgBuilder(mods: _*)
|
|
||||||
compile(pkg).packageLanguageVersion.lift(pkg.id) shouldBe Some(v1_8)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
"return the newest language version of package with several package" in {
|
|
||||||
val Seq(mod6, mod7, mod8) = Seq(v1_6, v1_7, v1_8).map(ModBuilder(_))
|
|
||||||
val Seq(pkg6, pkg7, pkg8) = Seq(mod6, mod7, mod8).map(PkgBuilder(_))
|
|
||||||
val Seq(dep6, dep7, dep8) =
|
|
||||||
Seq(pkg6.id -> mod6.name, pkg7.id -> mod7.name, pkg8.id -> mod8.name)
|
|
||||||
|
|
||||||
val testCases = Table(
|
|
||||||
("pkgVersion", "dependencies", "output"),
|
|
||||||
(v1_6, Seq(dep6), v1_6),
|
|
||||||
(v1_6, Seq(dep7), v1_7),
|
|
||||||
(v1_6, Seq(dep8), v1_8),
|
|
||||||
(v1_6, Seq(dep6, dep7), v1_7),
|
|
||||||
(v1_6, Seq(dep7, dep8), v1_8),
|
|
||||||
(v1_6, Seq(dep6, dep7, dep8), v1_8),
|
|
||||||
(v1_7, Seq(dep6), v1_7),
|
|
||||||
(v1_7, Seq(dep8), v1_8),
|
|
||||||
(v1_7, Seq(dep7, dep8), v1_8),
|
|
||||||
(v1_7, Seq(dep6, dep7, dep8), v1_8),
|
|
||||||
(v1_8, Seq(dep6), v1_8),
|
|
||||||
(v1_8, Seq(dep7), v1_8),
|
|
||||||
(v1_8, Seq(dep6, dep7), v1_8),
|
|
||||||
)
|
|
||||||
|
|
||||||
testCases.forEvery {
|
|
||||||
case (pkgVersion, dependencies, output) =>
|
|
||||||
val pkg = PkgBuilder(ModBuilder(pkgVersion, dependencies: _*))
|
|
||||||
compile(pkg, pkg6, pkg7, pkg8).packageLanguageVersion.lift(pkg.id) shouldBe Some(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -27,19 +27,6 @@ abstract class CompiledPackages {
|
|||||||
def profilingMode: Compiler.ProfilingMode
|
def profilingMode: Compiler.ProfilingMode
|
||||||
|
|
||||||
def compiler: Compiler = Compiler(packages, stackTraceMode, profilingMode)
|
def compiler: Compiler = Compiler(packages, stackTraceMode, profilingMode)
|
||||||
|
|
||||||
// computes the newest language version used in `pkg` and all its dependencies.
|
|
||||||
// assumes that `maxVersionOfDependencies` is defined for all dependencies of `pkg`
|
|
||||||
// returns None iff the package is empty.
|
|
||||||
protected def computePackageLanguageVersion(
|
|
||||||
dependenciesPackageVersion: PartialFunction[PackageId, LanguageVersion],
|
|
||||||
pkg: Package,
|
|
||||||
): Option[LanguageVersion] = {
|
|
||||||
import transaction.VersionTimeline.maxVersion
|
|
||||||
val moduleVersions = pkg.modules.values.iterator.map(_.languageVersion)
|
|
||||||
val dependencyVersions = pkg.directDeps.iterator.map(dependenciesPackageVersion)
|
|
||||||
(moduleVersions ++ dependencyVersions).reduceOption(maxVersion[LanguageVersion])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final class PureCompiledPackages private (
|
final class PureCompiledPackages private (
|
||||||
@ -54,20 +41,12 @@ final class PureCompiledPackages private (
|
|||||||
override def stackTraceMode = stacktracing
|
override def stackTraceMode = stacktracing
|
||||||
override def profilingMode = profiling
|
override def profilingMode = profiling
|
||||||
|
|
||||||
private[this] def sortedPkgIds: List[PackageId] = {
|
|
||||||
val dependencyGraph = packageIds.view
|
|
||||||
.flatMap(pkgId => getPackage(pkgId).map(pkg => pkgId -> pkg.directDeps).toList)
|
|
||||||
.toMap
|
|
||||||
language.Graphs
|
|
||||||
.topoSort(dependencyGraph)
|
|
||||||
.getOrElse(throw new IllegalArgumentException("cyclic package definitions"))
|
|
||||||
}
|
|
||||||
|
|
||||||
override val packageLanguageVersion: Map[PackageId, LanguageVersion] =
|
override val packageLanguageVersion: Map[PackageId, LanguageVersion] =
|
||||||
sortedPkgIds.foldLeft(Map.empty[PackageId, LanguageVersion])(
|
packages.foldLeft(Map.empty[PackageId, LanguageVersion]) {
|
||||||
(acc, pkgId) =>
|
case (acc, (pkgId, pkg)) =>
|
||||||
computePackageLanguageVersion(acc, packages(pkgId)).fold(acc)(acc.updated(pkgId, _))
|
// all modules of a package are compiled to the same LF version
|
||||||
)
|
pkg.modules.values.headOption.fold(acc)(mod => acc.updated(pkgId, mod.languageVersion))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object PureCompiledPackages {
|
object PureCompiledPackages {
|
||||||
|
@ -1635,6 +1635,8 @@ Then, a collection of packages ``Ξ`` is well-formed if:
|
|||||||
package of ``Ξ``.
|
package of ``Ξ``.
|
||||||
* There are no cycles between type synonym definitions, modules, and
|
* There are no cycles between type synonym definitions, modules, and
|
||||||
packages references.
|
packages references.
|
||||||
|
* Each package ``p`` only depends on packages whose LF version is older
|
||||||
|
than or the same as the LF version of ``p`` itself.
|
||||||
|
|
||||||
|
|
||||||
Operational semantics
|
Operational semantics
|
||||||
|
@ -20,6 +20,7 @@ da_scala_library(
|
|||||||
deps = [
|
deps = [
|
||||||
"//daml-lf/data",
|
"//daml-lf/data",
|
||||||
"//daml-lf/language",
|
"//daml-lf/language",
|
||||||
|
"//daml-lf/transaction",
|
||||||
"@maven//:org_scalaz_scalaz_core_2_12",
|
"@maven//:org_scalaz_scalaz_core_2_12",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@ -34,5 +35,6 @@ da_scala_test(
|
|||||||
"//daml-lf/data",
|
"//daml-lf/data",
|
||||||
"//daml-lf/language",
|
"//daml-lf/language",
|
||||||
"//daml-lf/parser",
|
"//daml-lf/parser",
|
||||||
|
"@maven//:org_scalaz_scalaz_core_2_12",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -10,8 +10,8 @@ import com.daml.lf.validation.Util._
|
|||||||
|
|
||||||
private[validation] object Collision {
|
private[validation] object Collision {
|
||||||
|
|
||||||
def checkPackage(pkgId: PackageId, modules: Traversable[(ModuleName, Ast.Module)]): Unit = {
|
def checkPackage(pkgId: PackageId, pkg: Ast.Package): Unit = {
|
||||||
val entitiesMap = namedEntitiesFromPkg(modules).groupBy(_.fullyResolvedName)
|
val entitiesMap = namedEntitiesFromPkg(pkg.modules).groupBy(_.fullyResolvedName)
|
||||||
entitiesMap.values.foreach(cs => checkCollisions(pkgId, cs.toList))
|
entitiesMap.values.foreach(cs => checkCollisions(pkgId, cs.toList))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
// Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package com.daml.lf.validation
|
||||||
|
|
||||||
|
import com.daml.lf.data.Ref._
|
||||||
|
import com.daml.lf.language.Ast._
|
||||||
|
import com.daml.lf.transaction.VersionTimeline
|
||||||
|
|
||||||
|
private[validation] object DependencyVersion {
|
||||||
|
|
||||||
|
@throws[ValidationError]
|
||||||
|
def checkPackage(world: World, pkgId: PackageId, pkg: Package): Unit = {
|
||||||
|
import VersionTimeline.Implicits._
|
||||||
|
|
||||||
|
for {
|
||||||
|
pkgFirstModule <- pkg.modules.values.take(1)
|
||||||
|
// all modules of a package are compiled to the same LF version
|
||||||
|
pkgLangVersion = pkgFirstModule.languageVersion
|
||||||
|
depPkgId <- pkg.directDeps
|
||||||
|
depPkg = world.lookupPackage(NoContext, depPkgId)
|
||||||
|
depFirstModule <- depPkg.modules.values.take(1)
|
||||||
|
depLangVersion = depFirstModule.languageVersion
|
||||||
|
} if (pkgLangVersion precedes depLangVersion)
|
||||||
|
throw EModuleVersionDependencies(
|
||||||
|
pkgId,
|
||||||
|
pkgLangVersion,
|
||||||
|
depPkgId,
|
||||||
|
depLangVersion
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -13,14 +13,14 @@ private[validation] object Recursion {
|
|||||||
/* Check there are no cycles in the module references */
|
/* Check there are no cycles in the module references */
|
||||||
|
|
||||||
@throws[ValidationError]
|
@throws[ValidationError]
|
||||||
def checkPackage(pkgId: PackageId, modules: Map[ModuleName, Module]): Unit = {
|
def checkPackage(pkgId: PackageId, pkg: Package): Unit = {
|
||||||
val g = modules.map {
|
val g = pkg.modules.map {
|
||||||
case (name, mod) => name -> (mod.definitions.values.flatMap(modRefs(pkgId, _)).toSet - name)
|
case (name, mod) => name -> (mod.definitions.values.flatMap(modRefs(pkgId, _)).toSet - name)
|
||||||
}
|
}
|
||||||
|
|
||||||
Graphs.topoSort(g).left.foreach(cycle => throw EImportCycle(NoContext, cycle.vertices))
|
Graphs.topoSort(g).left.foreach(cycle => throw EImportCycle(NoContext, cycle.vertices))
|
||||||
|
|
||||||
modules.foreach { case (modName, mod) => checkModule(pkgId, modName, mod) }
|
pkg.modules.foreach { case (modName, mod) => checkModule(pkgId, modName, mod) }
|
||||||
}
|
}
|
||||||
|
|
||||||
def modRefs(pkgId: PackageId, definition: Definition): Set[ModuleName] = {
|
def modRefs(pkgId: PackageId, definition: Definition): Set[ModuleName] = {
|
||||||
|
@ -21,17 +21,18 @@ object Validation {
|
|||||||
): Either[ValidationError, Unit] =
|
): Either[ValidationError, Unit] =
|
||||||
runSafely {
|
runSafely {
|
||||||
val world = new World(pkgs)
|
val world = new World(pkgs)
|
||||||
unsafeCheckPackage(world, pkgId, world.lookupPackage(NoContext, pkgId).modules)
|
unsafeCheckPackage(world, pkgId, world.lookupPackage(NoContext, pkgId))
|
||||||
}
|
}
|
||||||
|
|
||||||
private def unsafeCheckPackage(
|
private def unsafeCheckPackage(
|
||||||
world: World,
|
world: World,
|
||||||
pkgId: PackageId,
|
pkgId: PackageId,
|
||||||
modules: Map[ModuleName, Module]
|
pkg: Package
|
||||||
): Unit = {
|
): Unit = {
|
||||||
Collision.checkPackage(pkgId, modules)
|
Collision.checkPackage(pkgId, pkg)
|
||||||
Recursion.checkPackage(pkgId, modules)
|
Recursion.checkPackage(pkgId, pkg)
|
||||||
modules.values.foreach(unsafeCheckModule(world, pkgId, _))
|
DependencyVersion.checkPackage(world, pkgId, pkg)
|
||||||
|
pkg.modules.values.foreach(unsafeCheckModule(world, pkgId, _))
|
||||||
}
|
}
|
||||||
|
|
||||||
def checkModule(
|
def checkModule(
|
||||||
|
@ -6,6 +6,7 @@ package com.daml.lf.validation
|
|||||||
import com.daml.lf.data.ImmArray
|
import com.daml.lf.data.ImmArray
|
||||||
import com.daml.lf.data.Ref._
|
import com.daml.lf.data.Ref._
|
||||||
import com.daml.lf.language.Ast._
|
import com.daml.lf.language.Ast._
|
||||||
|
import com.daml.lf.language.LanguageVersion
|
||||||
|
|
||||||
sealed abstract class LookupError extends Product with Serializable {
|
sealed abstract class LookupError extends Product with Serializable {
|
||||||
def pretty: String
|
def pretty: String
|
||||||
@ -367,3 +368,20 @@ final case class ECollision(
|
|||||||
override protected def prettyInternal: String =
|
override protected def prettyInternal: String =
|
||||||
s"collision between ${entity1.pretty} and ${entity2.pretty}"
|
s"collision between ${entity1.pretty} and ${entity2.pretty}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final case class EModuleVersionDependencies(
|
||||||
|
pkgId: PackageId,
|
||||||
|
pkgLangVersion: LanguageVersion,
|
||||||
|
depPkgId: PackageId,
|
||||||
|
dependencyLangVersion: LanguageVersion,
|
||||||
|
) extends ValidationError {
|
||||||
|
import com.daml.lf.transaction.VersionTimeline.Implicits._
|
||||||
|
|
||||||
|
assert(pkgId != depPkgId)
|
||||||
|
assert(pkgLangVersion precedes dependencyLangVersion)
|
||||||
|
|
||||||
|
override protected def prettyInternal: String =
|
||||||
|
s"package $pkgId using version $pkgLangVersion depends on package $depPkgId using newer version $dependencyLangVersion"
|
||||||
|
|
||||||
|
override def context: Context = NoContext
|
||||||
|
}
|
||||||
|
@ -11,7 +11,7 @@ import org.scalatest.{Matchers, WordSpec}
|
|||||||
class CollisionSpec extends WordSpec with Matchers {
|
class CollisionSpec extends WordSpec with Matchers {
|
||||||
|
|
||||||
def check(pkg: Package): Unit =
|
def check(pkg: Package): Unit =
|
||||||
Collision.checkPackage(defaultPackageId, pkg.modules)
|
Collision.checkPackage(defaultPackageId, pkg)
|
||||||
|
|
||||||
"Collision validation" should {
|
"Collision validation" should {
|
||||||
|
|
||||||
|
@ -0,0 +1,85 @@
|
|||||||
|
// Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package com.daml.lf.validation
|
||||||
|
|
||||||
|
import com.daml.lf.data.Ref.{DottedName, Identifier, PackageId, QualifiedName}
|
||||||
|
import com.daml.lf.language.Ast._
|
||||||
|
import com.daml.lf.language.Util._
|
||||||
|
import com.daml.lf.language.{LanguageVersion => LV}
|
||||||
|
import org.scalatest.prop.TableDrivenPropertyChecks
|
||||||
|
import org.scalatest.{Matchers, WordSpec}
|
||||||
|
|
||||||
|
class DependencyVersionSpec extends WordSpec with TableDrivenPropertyChecks with Matchers {
|
||||||
|
|
||||||
|
private[this] val v1_6 = LV(LV.Major.V1, LV.Minor.Stable("6"))
|
||||||
|
private[this] val v1_7 = LV(LV.Major.V1, LV.Minor.Stable("7"))
|
||||||
|
private[this] val v1_8 = LV(LV.Major.V1, LV.Minor.Stable("8"))
|
||||||
|
private[this] val A = (PackageId.assertFromString("-pkg1-"), DottedName.assertFromString("A"))
|
||||||
|
private[this] val B = (PackageId.assertFromString("-pkg2-"), DottedName.assertFromString("B"))
|
||||||
|
private[this] val E = (PackageId.assertFromString("-pkg3-"), DottedName.assertFromString("E"))
|
||||||
|
private[this] val u = DottedName.assertFromString("u")
|
||||||
|
|
||||||
|
"Dependency validation should detect cycles between modules" in {
|
||||||
|
|
||||||
|
def pkg(
|
||||||
|
ref: (PackageId, DottedName),
|
||||||
|
langVersion: LV,
|
||||||
|
depRefs: (PackageId, DottedName)*,
|
||||||
|
) = {
|
||||||
|
val (pkgId, modName) = ref
|
||||||
|
|
||||||
|
val mod = Module(
|
||||||
|
modName,
|
||||||
|
(
|
||||||
|
(u -> DValue(TUnit, true, EUnit, false)) +:
|
||||||
|
depRefs.map {
|
||||||
|
case (depPkgId, depModName) =>
|
||||||
|
depModName -> DValue(
|
||||||
|
TUnit,
|
||||||
|
true,
|
||||||
|
EVal(Identifier(depPkgId, QualifiedName(depModName, u))),
|
||||||
|
false)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
langVersion,
|
||||||
|
FeatureFlags.default
|
||||||
|
)
|
||||||
|
|
||||||
|
pkgId -> Package(Map(modName -> mod), depRefs.iterator.map(_._1).toSet - pkgId, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
val negativeTestCases = Table(
|
||||||
|
"valid packages",
|
||||||
|
Map(pkg(A, v1_8, A, B, E), pkg(B, v1_7, B, E), pkg(E, v1_6, E)),
|
||||||
|
Map(pkg(A, v1_8, A, B, E), pkg(B, v1_8, B, E), pkg(E, v1_6, E)),
|
||||||
|
)
|
||||||
|
|
||||||
|
val postiveTestCase = Table(
|
||||||
|
("invalid module", "packages"),
|
||||||
|
A -> Map(pkg(A, v1_6, A, B, E), pkg(B, v1_7, B, E), pkg(E, v1_6, E)),
|
||||||
|
A -> Map(pkg(A, v1_7, A, B, E), pkg(B, v1_7, B, E), pkg(E, v1_8, E)),
|
||||||
|
B -> Map(pkg(A, v1_8, A, B, E), pkg(B, v1_6, B, E), pkg(E, v1_7, E)),
|
||||||
|
)
|
||||||
|
|
||||||
|
forEvery(negativeTestCases) { pkgs =>
|
||||||
|
pkgs.foreach {
|
||||||
|
case (pkgId, pkg) =>
|
||||||
|
DependencyVersion.checkPackage(new World(pkgs), pkgId, pkg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
forEvery(postiveTestCase) {
|
||||||
|
case ((pkgdId, _), pkgs) =>
|
||||||
|
val world = new World(pkgs)
|
||||||
|
an[EModuleVersionDependencies] should be thrownBy
|
||||||
|
DependencyVersion.checkPackage(
|
||||||
|
world,
|
||||||
|
pkgdId,
|
||||||
|
world.lookupPackage(NoContext, pkgdId),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -22,7 +22,7 @@ class RecursionSpec extends WordSpec with TableDrivenPropertyChecks with Matcher
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
Recursion.checkPackage(defaultPackageId, p.modules)
|
Recursion.checkPackage(defaultPackageId, p)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,11 +61,9 @@ class RecursionSpec extends WordSpec with TableDrivenPropertyChecks with Matcher
|
|||||||
${module("E", "E")}
|
${module("E", "E")}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
Recursion.checkPackage(defaultPackageId, negativeCase.modules)
|
Recursion.checkPackage(defaultPackageId, negativeCase)
|
||||||
an[EImportCycle] should be thrownBy
|
an[EImportCycle] should be thrownBy Recursion.checkPackage(defaultPackageId, positiveCase1)
|
||||||
Recursion.checkPackage(defaultPackageId, positiveCase1.modules)
|
an[EImportCycle] should be thrownBy Recursion.checkPackage(defaultPackageId, positiveCase2)
|
||||||
an[EImportCycle] should be thrownBy
|
|
||||||
Recursion.checkPackage(defaultPackageId, positiveCase2.modules)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,11 +96,9 @@ class RecursionSpec extends WordSpec with TableDrivenPropertyChecks with Matcher
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
Recursion.checkPackage(defaultPackageId, negativeCase.modules)
|
Recursion.checkPackage(defaultPackageId, negativeCase)
|
||||||
an[ETypeSynCycle] should be thrownBy
|
an[ETypeSynCycle] should be thrownBy Recursion.checkPackage(defaultPackageId, positiveCase1)
|
||||||
Recursion.checkPackage(defaultPackageId, positiveCase1.modules)
|
an[ETypeSynCycle] should be thrownBy Recursion.checkPackage(defaultPackageId, positiveCase2)
|
||||||
an[ETypeSynCycle] should be thrownBy
|
|
||||||
Recursion.checkPackage(defaultPackageId, positiveCase2.modules)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user