Merge pull request #5137 from eed3si9n/wip/hedgehog

Use Scala Hedgehog in ParseKey test
This commit is contained in:
eugene yokota 2019-09-30 10:43:40 -04:00 committed by GitHub
commit 0bdde4aee8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 321 additions and 474 deletions

View File

@ -67,7 +67,9 @@ def commonSettings: Seq[Setting[_]] = Def.settings(
resolvers += Resolver.typesafeIvyRepo("releases").withName("typesafe-sbt-build-ivy-releases"), resolvers += Resolver.typesafeIvyRepo("releases").withName("typesafe-sbt-build-ivy-releases"),
resolvers += Resolver.sonatypeRepo("snapshots"), resolvers += Resolver.sonatypeRepo("snapshots"),
resolvers += "bintray-sbt-maven-releases" at "https://dl.bintray.com/sbt/maven-releases/", resolvers += "bintray-sbt-maven-releases" at "https://dl.bintray.com/sbt/maven-releases/",
resolvers += Resolver.url("bintray-scala-hedgehog", url("https://dl.bintray.com/hedgehogqa/scala-hedgehog"))(Resolver.ivyStylePatterns),
addCompilerPlugin("org.spire-math" % "kind-projector" % "0.9.4" cross CrossVersion.binary), addCompilerPlugin("org.spire-math" % "kind-projector" % "0.9.4" cross CrossVersion.binary),
testFrameworks += TestFramework("hedgehog.sbt.Framework"),
concurrentRestrictions in Global += Util.testExclusiveRestriction, concurrentRestrictions in Global += Util.testExclusiveRestriction,
testOptions in Test += Tests.Argument(TestFrameworks.ScalaCheck, "-w", "1"), testOptions in Test += Tests.Argument(TestFrameworks.ScalaCheck, "-w", "1"),
testOptions in Test += Tests.Argument(TestFrameworks.ScalaCheck, "-verbosity", "2"), testOptions in Test += Tests.Argument(TestFrameworks.ScalaCheck, "-verbosity", "2"),

View File

@ -7,125 +7,141 @@
package sbt package sbt
import Project._
import sbt.internal.util.Types.idFun import sbt.internal.util.Types.idFun
import sbt.internal.TestBuild._ import sbt.internal.TestBuild._
import sbt.librarymanagement.Configuration import hedgehog._
import org.scalacheck._ import hedgehog.Result.{ all, assert, failure, success }
import Prop._ import hedgehog.runner._
import Gen._
object Delegates extends Properties("delegates") { object Delegates extends Properties {
property("generate non-empty configs") = forAll { (c: Vector[Configuration]) =>
c.nonEmpty
}
property("generate non-empty tasks") = forAll { (t: Vector[Taskk]) =>
t.nonEmpty
}
property("no duplicate scopes") = forAll { (keys: TestKeys) => override def tests: List[Test] =
List(
property("generate non-empty configs", cGen.forAll.map { c =>
assert(c.nonEmpty)
}),
property("generate non-empty tasks", tGen.forAll.map { t =>
assert(t.nonEmpty)
}),
property("no duplicate scopes", keysGen.forAll.map { keys =>
allDelegates(keys) { (_, ds) => allDelegates(keys) { (_, ds) =>
ds.distinct.size == ds.size ds.distinct.size ==== ds.size
} }
} }),
property("delegates non-empty") = forAll { (keys: TestKeys) => property("delegates non-empty", keysGen.forAll.map { keys =>
allDelegates(keys) { (_, ds) => allDelegates(keys) { (_, ds) =>
ds.nonEmpty assert(ds.nonEmpty)
} }
} }),
property("An initially Zero axis is Zero in all delegates", allAxes(alwaysZero)),
property("An initially Zero axis is Zero in all delegates") = allAxes(alwaysZero) property(
"Projects precede builds precede Zero",
property("Projects precede builds precede Zero") = forAll { (keys: TestKeys) => keysGen.forAll.map { keys =>
allDelegates(keys) { (scope, ds) => allDelegates(keys) { (scope, ds) =>
val projectAxes = ds.map(_.project) val projectAxes = ds.map(_.project)
val nonProject = projectAxes.dropWhile { val nonProject = projectAxes.dropWhile {
case Select(_: ProjectRef) => true; case _ => false case Select(_: ProjectRef) => true; case _ => false
} }
val global = nonProject.dropWhile { case Select(_: BuildRef) => true; case _ => false } val global = nonProject.dropWhile { case Select(_: BuildRef) => true; case _ => false }
global forall { _ == Zero } all(global.map { _ ==== Zero }.toList)
} }
} }
),
property("Initial scope present with all combinations of Global axes") = allAxes( property(
"Initial scope present with all combinations of Global axes",
allAxes(
(s, ds, _) => globalCombinations(s, ds) (s, ds, _) => globalCombinations(s, ds)
) )
),
property("initial scope first") = forAll { (keys: TestKeys) => property("initial scope first", keysGen.forAll.map { keys =>
allDelegates(keys) { (scope, ds) => allDelegates(keys) { (scope, ds) =>
ds.head == scope ds.head ==== scope
} }
} }),
property("global scope last", keysGen.forAll.map { keys =>
property("global scope last") = forAll { (keys: TestKeys) =>
allDelegates(keys) { (_, ds) => allDelegates(keys) { (_, ds) =>
ds.last == Scope.GlobalScope ds.last ==== Scope.GlobalScope
} }
} }),
property(
property("Project axis delegates to BuildRef then Zero") = forAll { (keys: TestKeys) => "Project axis delegates to BuildRef then Zero",
allDelegates(keys) { (key, ds) => keysGen.forAll.map { keys =>
allDelegates(keys) {
(key, ds) =>
key.project match { key.project match {
case Zero => true // filtering out of testing case Zero => success // filtering out of testing
case Select(rr: ResolvedReference) => case Select(rr: ResolvedReference) =>
rr match { rr match {
case BuildRef(_) => ds.indexOf(key) < ds.indexOf(key.copy(project = Zero)) case BuildRef(_) =>
assert(ds.indexOf(key) < ds.indexOf(key.copy(project = Zero)))
case ProjectRef(uri, _) => case ProjectRef(uri, _) =>
val buildScoped = key.copy(project = Select(BuildRef(uri))) val buildScoped = key.copy(project = Select(BuildRef(uri)))
val idxKey = ds.indexOf(key) val idxKey = ds.indexOf(key)
val idxB = ds.indexOf(buildScoped) val idxB = ds.indexOf(buildScoped)
val z = key.copy(project = Zero) val z = key.copy(project = Zero)
val idxZ = ds.indexOf(z) val idxZ = ds.indexOf(z)
if (z == Scope.GlobalScope) true (z ==== Scope.GlobalScope)
else .or(
(s"idxKey = $idxKey; idxB = $idxB; idxZ = $idxZ") |: (idxKey < idxB) && (idxB < idxZ) assert((idxKey < idxB) && (idxB < idxZ))
.log(s"idxKey = $idxKey; idxB = $idxB; idxZ = $idxZ")
)
} }
case Select(_) | This => case Select(_) | This =>
throw new AssertionError(s"Scope's reference should be resolved, but was ${key.project}") failure.log(s"Scope's reference should be resolved, but was ${key.project}")
} }
} }
} }
),
property("Config axis delegates to parent configuration") = forAll { (keys: TestKeys) => property(
allDelegates(keys) { (key, ds) => "Config axis delegates to parent configuration",
keysGen.forAll.map { keys =>
allDelegates(keys) {
(key, ds) =>
key.config match { key.config match {
case Zero => true case Zero => success
case Select(config) if key.project.isSelect => case Select(config) =>
val p = key.project.toOption.get key.project match {
case Select(p @ ProjectRef(_, _)) =>
val r = keys.env.resolve(p) val r = keys.env.resolve(p)
keys.env.inheritConfig(r, config).headOption.fold(Prop(true)) { parent => keys.env.inheritConfig(r, config).headOption.fold(success) { parent =>
val idxKey = ds.indexOf(key) val idxKey = ds.indexOf(key)
val a = key.copy(config = Select(parent)) val a = key.copy(config = Select(parent))
val idxA = ds.indexOf(a) val idxA = ds.indexOf(a)
(s"idxKey = $idxKey; a = $a; idxA = $idxA") |: idxKey < idxA assert(idxKey < idxA)
.log(s"idxKey = $idxKey; a = $a; idxA = $idxA")
} }
case _ => true case _ => success
}
case _ => success
} }
} }
} }
)
)
def allAxes(f: (Scope, Seq[Scope], Scope => ScopeAxis[_]) => Prop): Prop = forAll { def allAxes(f: (Scope, Seq[Scope], Scope => ScopeAxis[_]) => hedgehog.Result): Property =
(keys: TestKeys) => keysGen.forAll.map { keys =>
allDelegates(keys) { (s, ds) => allDelegates(keys) { (s, ds) =>
all(f(s, ds, _.project), f(s, ds, _.config), f(s, ds, _.task), f(s, ds, _.extra)) all(List(f(s, ds, _.project), f(s, ds, _.config), f(s, ds, _.task), f(s, ds, _.extra)))
} }
} }
def allDelegates(keys: TestKeys)(f: (Scope, Seq[Scope]) => Prop): Prop = def allDelegates(keys: TestKeys)(f: (Scope, Seq[Scope]) => hedgehog.Result): hedgehog.Result =
all(keys.scopes map { scope => all(keys.scopes.map { scope =>
val delegates = keys.env.delegates(scope) val delegates = keys.env.delegates(scope)
("Scope: " + Scope.display(scope, "_")) |:
("Delegates:\n\t" + delegates.map(scope => Scope.display(scope, "_")).mkString("\n\t")) |:
f(scope, delegates) f(scope, delegates)
}: _*) .log("Scope: " + Scope.display(scope, "_"))
.log("Delegates:\n\t" + delegates.map(scope => Scope.display(scope, "_")).mkString("\n\t"))
}.toList)
def alwaysZero(s: Scope, ds: Seq[Scope], axis: Scope => ScopeAxis[_]): Prop = def alwaysZero(s: Scope, ds: Seq[Scope], axis: Scope => ScopeAxis[_]): hedgehog.Result =
(axis(s) != Zero) || assert(axis(s) != Zero).or(
all(ds map { d => all(ds.map { d =>
(axis(d) == Zero): Prop axis(d) ==== Zero
}: _*) }.toList)
)
def globalCombinations(s: Scope, ds: Seq[Scope]): Prop = { def globalCombinations(s: Scope, ds: Seq[Scope]): hedgehog.Result = {
val mods = List[Scope => Scope]( val mods = List[Scope => Scope](
_.copy(project = Zero), _.copy(project = Zero),
_.copy(config = Zero), _.copy(config = Zero),
@ -143,6 +159,6 @@ object Delegates extends Properties("delegates") {
loop(s, s :: acc, xs) loop(s, s :: acc, xs)
} }
} }
all(loop(s, Nil, modAndIdent).map(ds contains _: Prop): _*) all(loop(s, Nil, modAndIdent).map(x => assert(ds contains x)).toList)
} }
} }

View File

@ -7,26 +7,47 @@
package sbt package sbt
import java.net.URI
import org.scalacheck.Arbitrary.{ arbBool, arbitrary }
import org.scalacheck.Gen._
import org.scalacheck.Prop._
import org.scalacheck._
import sbt.Def.{ ScopedKey, displayFull, displayMasked } import sbt.Def.{ ScopedKey, displayFull, displayMasked }
import sbt.internal.TestBuild._ import sbt.internal.TestBuild._
import sbt.internal.util.AttributeKey import sbt.internal.util.complete.Parser
import sbt.internal.util.complete.{ DefaultParsers, Parser }
import sbt.internal.{ Resolve, TestBuild } import sbt.internal.{ Resolve, TestBuild }
import sbt.librarymanagement.Configuration import hedgehog._
import hedgehog.core.{ ShrinkLimit, SuccessCount }
import hedgehog.runner._
/** /**
* Tests that the scoped key parser in Act can correctly parse a ScopedKey converted by Def.show*Key. * Tests that the scoped key parser in Act can correctly parse a ScopedKey converted by Def.show*Key.
* This includes properly resolving omitted components. * This includes properly resolving omitted components.
*/ */
object ParseKey extends Properties("Key parser test") { object ParseKey extends Properties {
propertyWithSeed("An explicitly specified axis is always parsed to that explicit value", None) = val exampleCount = 1000
forAll(roundtrip(_))
override def tests: List[Test] = List(
propertyN(
"An explicitly specified axis is always parsed to that explicit value",
arbStructureKeyMask.forAll.map(roundtrip),
5000
),
propertyN(
"An unspecified project axis resolves to the current project",
arbStructureKeyMask.forAll.map(noProject),
5000
),
propertyN(
"An unspecified task axis resolves to Zero",
arbStructureKeyMask.forAll.map(noTask),
exampleCount
),
propertyN(
"An unspecified configuration axis resolves to the first configuration directly defining the key or else Zero",
arbStructureKeyMask.forAll.map(noConfig),
exampleCount
)
)
def propertyN(name: String, result: => Property, n: Int): Test =
Test(name, result)
.config(_.copy(testLimit = SuccessCount(n), shrinkLimit = ShrinkLimit(n * 10)))
def roundtrip(skm: StructureKeyMask) = { def roundtrip(skm: StructureKeyMask) = {
import skm.{ structure, key } import skm.{ structure, key }
@ -37,70 +58,59 @@ object ParseKey extends Properties("Key parser test") {
// so we mitigate this by explicitly displaying the configuration axis set to Zero // so we mitigate this by explicitly displaying the configuration axis set to Zero
val hasZeroConfig = key.scope.config == Zero val hasZeroConfig = key.scope.config == Zero
val showZeroConfig = hasZeroConfig || hasAmbiguousLowercaseAxes(key) val showZeroConfig = hasZeroConfig || hasAmbiguousLowercaseAxes(key, structure)
val mask = if (showZeroConfig) skm.mask.copy(project = true) else skm.mask val mask = if (showZeroConfig) skm.mask.copy(project = true) else skm.mask
val expected = resolve(structure, key, mask) val expected = resolve(structure, key, mask)
parseCheck(structure, key, mask, showZeroConfig)( parseCheck(structure, key, mask, showZeroConfig)(
sk => sk =>
Project.equal(sk, expected, mask) hedgehog.Result
:| s"$sk.key == $expected.key: ${sk.key == expected.key}" .assert(Project.equal(sk, expected, mask))
:| s"${sk.scope} == ${expected.scope}: ${Scope.equal(sk.scope, expected.scope, mask)}" .log(s"$sk.key == $expected.key: ${sk.key == expected.key}")
) :| s"Expected: ${displayFull(expected)}" .log(s"${sk.scope} == ${expected.scope}: ${Scope.equal(sk.scope, expected.scope, mask)}")
).log(s"Expected: ${displayFull(expected)}")
} }
propertyWithSeed("An unspecified project axis resolves to the current project", None) = forAll(
noProject(_)
)
def noProject(skm: StructureKeyMask) = { def noProject(skm: StructureKeyMask) = {
import skm.{ structure, key } import skm.{ structure, key }
val mask = skm.mask.copy(project = false) val mask = skm.mask.copy(project = false)
// skip when config axis is set to Zero // skip when config axis is set to Zero
val hasZeroConfig = key.scope.config == Zero val hasZeroConfig = key.scope.config ==== Zero
val showZeroConfig = hasAmbiguousLowercaseAxes(key) val showZeroConfig = hasAmbiguousLowercaseAxes(key, structure)
parseCheck(structure, key, mask, showZeroConfig)( parseCheck(structure, key, mask, showZeroConfig)(
sk => sk =>
(hasZeroConfig || sk.scope.project == Select(structure.current)) (hasZeroConfig or sk.scope.project ==== Select(structure.current))
:| s"Current: ${structure.current}" .log(s"parsed subproject: ${sk.scope.project}")
.log(s"current subproject: ${structure.current}")
) )
} }
propertyWithSeed("An unspecified task axis resolves to Zero", None) = forAll(noTask(_))
def noTask(skm: StructureKeyMask) = { def noTask(skm: StructureKeyMask) = {
import skm.{ structure, key } import skm.{ structure, key }
val mask = skm.mask.copy(task = false) val mask = skm.mask.copy(task = false)
parseCheck(structure, key, mask)(_.scope.task == Zero) parseCheck(structure, key, mask)(_.scope.task ==== Zero)
} }
propertyWithSeed(
"An unspecified configuration axis resolves to the first configuration directly defining the key or else Zero",
None
) = forAll(noConfig(_))
def noConfig(skm: StructureKeyMask) = { def noConfig(skm: StructureKeyMask) = {
import skm.{ structure, key } import skm.{ structure, key }
val mask = ScopeMask(config = false) val mask = ScopeMask(config = false)
val resolvedConfig = Resolve.resolveConfig(structure.extra, key.key, mask)(key.scope).config val resolvedConfig = Resolve.resolveConfig(structure.extra, key.key, mask)(key.scope).config
val showZeroConfig = hasAmbiguousLowercaseAxes(key) val showZeroConfig = hasAmbiguousLowercaseAxes(key, structure)
parseCheck(structure, key, mask, showZeroConfig)( parseCheck(structure, key, mask, showZeroConfig)(
sk => (sk.scope.config == resolvedConfig) || (sk.scope == Scope.GlobalScope) sk => (sk.scope.config ==== resolvedConfig) or (sk.scope ==== Scope.GlobalScope)
) :| s"Expected configuration: ${resolvedConfig map (_.name)}" ).log(s"Expected configuration: ${resolvedConfig map (_.name)}")
} }
implicit val arbStructure: Arbitrary[Structure] = Arbitrary { val arbStructure: Gen[Structure] =
for { for {
env <- mkEnv env <- mkEnv
loadFactor <- choose(0.0, 1.0) loadFactor <- Gen.double(Range.linearFrac(0.0, 1.0))
scopes <- pickN(loadFactor, env.allFullScopes) scopes <- pickN(loadFactor, env.allFullScopes)
current <- oneOf(env.allProjects.unzip._1) current <- oneOf(env.allProjects.unzip._1)
structure <- { } yield {
val settings = structureSettings(scopes, env) val settings = structureSettings(scopes, env)
TestBuild.structure(env, settings, current) TestBuild.structure(env, settings, current)
} }
} yield structure
}
def structureSettings(scopes: Seq[Scope], env: Env): Seq[Def.Setting[String]] = { def structureSettings(scopes: Seq[Scope], env: Env): Seq[Def.Setting[String]] = {
for { for {
@ -111,18 +121,18 @@ object ParseKey extends Properties("Key parser test") {
final case class StructureKeyMask(structure: Structure, key: ScopedKey[_], mask: ScopeMask) final case class StructureKeyMask(structure: Structure, key: ScopedKey[_], mask: ScopeMask)
implicit val arbStructureKeyMask: Arbitrary[StructureKeyMask] = Arbitrary { val arbStructureKeyMask: Gen[StructureKeyMask] =
for { (for {
structure <- arbStructure
// NOTE: Generating this after the structure improves shrinking
mask <- maskGen mask <- maskGen
structure <- arbitrary[Structure]
key <- for { key <- for {
scope <- TestBuild.scope(structure.env) scope <- TestBuild.scope(structure.env)
key <- oneOf(structure.allAttributeKeys.toSeq) key <- oneOf(structure.allAttributeKeys.toSeq)
} yield ScopedKey(scope, key) } yield ScopedKey(scope, key)
skm = StructureKeyMask(structure, key, mask) skm = StructureKeyMask(structure, key, mask)
if configExistsInIndex(skm) } yield skm)
} yield skm .filter(configExistsInIndex)
}
private def configExistsInIndex(skm: StructureKeyMask): Boolean = { private def configExistsInIndex(skm: StructureKeyMask): Boolean = {
import skm._ import skm._
@ -153,17 +163,18 @@ object ParseKey extends Properties("Key parser test") {
key: ScopedKey[_], key: ScopedKey[_],
mask: ScopeMask, mask: ScopeMask,
showZeroConfig: Boolean = false, showZeroConfig: Boolean = false,
)(f: ScopedKey[_] => Prop): Prop = { )(f: ScopedKey[_] => hedgehog.Result): hedgehog.Result = {
val s = displayMasked(key, mask, showZeroConfig) val s = displayMasked(key, mask, showZeroConfig)
val parser = makeParser(structure) val parser = makeParser(structure)
val parsed = Parser.result(parser, s).left.map(_().toString) val parsed = Parser.result(parser, s).left.map(_().toString)
( (
parsed.fold(_ => falsified, f) parsed
:| s"Key: ${Scope.displayPedantic(key.scope, key.key.label)}" .fold(_ => hedgehog.Result.failure, f)
:| s"Mask: $mask" .log(s"Key: ${Scope.displayPedantic(key.scope, key.key.label)}")
:| s"Key string: '$s'" .log(s"Mask: $mask")
:| s"Parsed: ${parsed.right.map(displayFull)}" .log(s"Key string: '$s'")
:| s"Structure: $structure" .log(s"Parsed: ${parsed.right.map(displayFull)}")
.log(s"Structure: $structure")
) )
} }
@ -172,207 +183,16 @@ object ParseKey extends Properties("Key parser test") {
def pickN[T](load: Double, from: Seq[T]): Gen[Seq[T]] = def pickN[T](load: Double, from: Seq[T]): Gen[Seq[T]] =
pick((load * from.size).toInt max 1, from) pick((load * from.size).toInt max 1, from)
implicit val shrinkStructureKeyMask: Shrink[StructureKeyMask] = Shrink { skm =>
Shrink
.shrink(skm.structure)
.map(s => skm.copy(structure = s))
.flatMap(fixKey)
}
def fixKey(skm: StructureKeyMask): Stream[StructureKeyMask] = {
for {
scope <- fixScope(skm)
attributeKey <- fixAttributeKey(skm)
} yield skm.copy(key = ScopedKey(scope, attributeKey))
}
def fixScope(skm: StructureKeyMask): Stream[Scope] = {
def validScope(scope: Scope) = scope match {
case Scope(Select(BuildRef(build)), _, _, _) if !validBuild(build) => false
case Scope(Select(ProjectRef(build, project)), _, _, _) if !validProject(build, project) =>
false
case Scope(Select(ProjectRef(build, project)), Select(ConfigKey(config)), _, _)
if !validConfig(build, project, config) =>
false
case Scope(_, Select(ConfigKey(config)), _, _) if !configExists(config) =>
false
case Scope(_, _, Select(task), _) => validTask(task)
case _ => true
}
def validBuild(build: URI) = skm.structure.env.buildMap.contains(build)
def validProject(build: URI, project: String) = {
skm.structure.env.buildMap
.get(build)
.exists(_.projectMap.contains(project))
}
def validConfig(build: URI, project: String, config: String) = {
skm.structure.env.buildMap
.get(build)
.toSeq
.flatMap(_.projectMap.get(project))
.flatMap(_.configurations.map(_.name))
.contains(config)
}
def configExists(config: String) = {
val configs = for {
build <- skm.structure.env.builds
project <- build.projects
config <- project.configurations
} yield config.name
configs.contains(config)
}
def validTask(task: AttributeKey[_]) = skm.structure.env.taskMap.contains(task)
if (validScope(skm.key.scope)) {
Stream(skm.key.scope)
} else {
// We could return all scopes here but we want to explore the other paths first since there
// is a greater chance of a successful shrink. If necessary these could be appended to the end.
Stream.empty
}
}
def fixAttributeKey(skm: StructureKeyMask): Stream[AttributeKey[_]] = {
if (skm.structure.allAttributeKeys.contains(skm.key.key)) {
Stream(skm.key.key)
} else {
// Likewise here, we should try other paths before trying different attribute keys.
Stream.empty
}
}
implicit val shrinkStructure: Shrink[Structure] = Shrink { s =>
Shrink.shrink(s.env).flatMap { env =>
val scopes = s.data.scopes intersect env.allFullScopes.toSet
val settings = structureSettings(scopes.toSeq, env)
if (settings.nonEmpty) {
val currents = env.allProjects.find {
case (ref, _) => ref == s.current
} match {
case Some((current, _)) => Stream(current)
case None => env.allProjects.map(_._1).toStream
}
currents.map(structure(env, settings, _))
} else {
Stream.empty
}
}
}
implicit val shrinkEnv: Shrink[Env] = Shrink { env =>
val shrunkBuilds = Shrink
.shrink(env.builds)
.filter(_.nonEmpty)
.map(b => env.copy(builds = b))
.map(fixProjectRefs)
.map(fixConfigurations)
val shrunkTasks = shrinkTasks(env.tasks)
.map(t => env.copy(tasks = t))
shrunkBuilds ++ shrunkTasks
}
private def fixProjectRefs(env: Env): Env = {
def fixBuild(build: Build): Build = {
build.copy(projects = build.projects.map(fixProject))
}
def fixProject(project: Proj): Proj = {
project.copy(delegates = project.delegates.filter(delegateExists))
}
def delegateExists(delegate: ProjectRef): Boolean = {
env.buildMap
.get(delegate.build)
.flatMap(_.projectMap.get(delegate.project))
.nonEmpty
}
env.copy(builds = env.builds.map(fixBuild))
}
private def fixConfigurations(env: Env): Env = {
val configs = env.allProjects.map {
case (_, proj) => proj -> proj.configurations.toSet
}.toMap
def fixBuild(build: Build): Build = {
build.copy(projects = build.projects.map(fixProject(build.uri)))
}
def fixProject(buildURI: URI)(project: Proj): Proj = {
val projConfigs = configs(project)
project.copy(configurations = project.configurations.map(fixConfig(projConfigs)))
}
def fixConfig(projConfigs: Set[Configuration])(config: Configuration): Configuration = {
import config.{ name => configName, _ }
val extendsConfigs = config.extendsConfigs.filter(projConfigs.contains)
Configuration.of(id, configName, description, isPublic, extendsConfigs, transitive)
}
env.copy(builds = env.builds.map(fixBuild))
}
implicit val shrinkBuild: Shrink[Build] = Shrink { build =>
Shrink
.shrink(build.projects)
.filter(_.nonEmpty)
.map(p => build.copy(projects = p))
// Could also shrink the URI here but that requires updating all the references.
}
implicit val shrinkProject: Shrink[Proj] = Shrink { project =>
val shrunkDelegates = Shrink
.shrink(project.delegates)
.map(d => project.copy(delegates = d))
val shrunkConfigs = Shrink
.shrink(project.configurations)
.map(c => project.copy(configurations = c))
val shrunkID = shrinkID(project.id)
.map(id => project.copy(id = id))
shrunkDelegates ++ shrunkConfigs ++ shrunkID
}
implicit val shrinkDelegate: Shrink[ProjectRef] = Shrink { delegate =>
val shrunkBuild = Shrink
.shrink(delegate.build)
.map(b => delegate.copy(build = b))
val shrunkProject = Shrink
.shrink(delegate.project)
.map(p => delegate.copy(project = p))
shrunkBuild ++ shrunkProject
}
implicit val shrinkConfiguration: Shrink[Configuration] = Shrink { configuration =>
import configuration.{ name => configName, _ }
val shrunkExtends = Shrink
.shrink(configuration.extendsConfigs)
.map(configuration.withExtendsConfigs)
val shrunkID = Shrink.shrink(id.tail).map { tail =>
Configuration
.of(id.head + tail, configName, description, isPublic, extendsConfigs, transitive)
}
shrunkExtends ++ shrunkID
}
val shrinkStringLength: Shrink[String] = Shrink { s =>
// Only change the string length don't change the characters.
implicit val shrinkChar: Shrink[Char] = Shrink.shrinkAny
Shrink.shrinkContainer[List, Char].shrink(s.toList).map(_.mkString)
}
def shrinkID(id: String): Stream[String] = {
Shrink.shrink(id).filter(DefaultParsers.validID)
}
def shrinkTasks(tasks: Vector[Taskk]): Stream[Vector[Taskk]] = {
Shrink.shrink(tasks)
}
implicit val shrinkTask: Shrink[Taskk] = Shrink { task =>
Shrink.shrink((task.delegates, task.key)).map {
case (delegates, key) => Taskk(key, delegates)
}
}
// if both a project and a key share the same name (e.g. "foo") // if both a project and a key share the same name (e.g. "foo")
// then a scoped key like `foo/<conf>/foo/name` would render as `foo/name` // then a scoped key like `foo/<conf>/foo/name` would render as `foo/name`
// which would be interpreted as `foo/Zero/Zero/name` // which would be interpreted as `foo/Zero/Zero/name`
// so we mitigate this by explicitly displaying the configuration axis set to Zero // so we mitigate this by explicitly displaying the configuration axis set to Zero
def hasAmbiguousLowercaseAxes(key: ScopedKey[_]) = PartialFunction.cond(key.scope) { def hasAmbiguousLowercaseAxes(key: ScopedKey[_], structure: Structure): Boolean = {
case Scope(Select(ProjectRef(_, proj)), _, Select(key), _) => proj == key.label val label = key.key.label
val allProjects = for {
uri <- structure.keyIndex.buildURIs
project <- structure.keyIndex.projects(uri)
} yield project
allProjects(label)
} }
} }

View File

@ -5,43 +5,36 @@
* Licensed under Apache License 2.0 (see LICENSE) * Licensed under Apache License 2.0 (see LICENSE)
*/ */
package sbt
import java.net.URI import java.net.URI
import org.scalatest.matchers.MatchResult
import org.scalatest.prop.PropertyChecks
import org.scalatest.{ Matchers, PropSpec }
import sbt.Def._ import sbt.Def._
import sbt._
import sbt.internal.TestBuild import sbt.internal.TestBuild
import sbt.internal.TestBuild._ import sbt.internal.TestBuild._
import sbt.internal.util.AttributeKey import sbt.internal.util.AttributeKey
import sbt.internal.util.complete.DefaultParsers import sbt.internal.util.complete.DefaultParsers
import sbt.librarymanagement.Configuration import sbt.librarymanagement.Configuration
import hedgehog._
import hedgehog.runner._
class ParserSpec extends PropSpec with PropertyChecks with Matchers { object ParserSpec extends Properties {
property("can parse any build") { override def tests: List[Test] =
forAll(TestBuild.uriGen) { uri => List(
property("can parse any build", TestBuild.uriGen.forAll.map { uri =>
parse(buildURI = uri) parse(buildURI = uri)
} }),
} property("can parse any project", TestBuild.nonEmptyId.forAll.map { id =>
property("can parse any project") {
forAll(TestBuild.nonEmptyId) { id =>
parse(projectID = id) parse(projectID = id)
} }),
} property("can parse any configuration", TestBuild.nonEmptyId.map(_.capitalize).forAll.map {
name =>
property("can parse any configuration") {
forAll(TestBuild.nonEmptyId.map(_.capitalize)) { name =>
parse(configName = name) parse(configName = name)
} }),
} property("can parse any attribute", TestBuild.kebabIdGen.forAll.map { name =>
property("can parse any attribute") {
forAll(TestBuild.kebabIdGen) { name =>
parse(attributeName = name) parse(attributeName = name)
} })
} )
private def parse( private def parse(
buildURI: URI = new java.net.URI("file", "///path/", null), buildURI: URI = new java.net.URI("file", "///path/", null),
@ -71,14 +64,9 @@ class ParserSpec extends PropSpec with PropertyChecks with Matchers {
val structure = TestBuild.structure(env, settings, build.allProjects.head._1) val structure = TestBuild.structure(env, settings, build.allProjects.head._1)
val string = displayMasked(scopedKey, ScopeMask()) val string = displayMasked(scopedKey, ScopeMask())
val parser = makeParser(structure) val parser = makeParser(structure)
string should { left => val result = DefaultParsers.result(parser, string)
val result = DefaultParsers.result(parser, left)
val resultStr = result.fold(_ => "<parse error>", _.toString) val resultStr = result.fold(_ => "<parse error>", _.toString)
MatchResult( (result ==== Right(scopedKey))
result == Right(scopedKey), .log(s"$string parsed back to $resultStr rather than $scopedKey")
s"$left parsed back to $resultStr rather than $scopedKey",
s"$left parsed back to $scopedKey",
)
}
} }
} }

View File

@ -10,16 +10,15 @@ package internal
import Def.{ ScopedKey, Setting } import Def.{ ScopedKey, Setting }
import sbt.internal.util.{ AttributeKey, AttributeMap, Relation, Settings } import sbt.internal.util.{ AttributeKey, AttributeMap, Relation, Settings }
import sbt.internal.util.Types.const import sbt.internal.util.Types.{ const, some }
import sbt.internal.util.complete.Parser import sbt.internal.util.complete.Parser
import sbt.librarymanagement.Configuration import sbt.librarymanagement.Configuration
import java.net.URI import java.net.URI
import org.scalacheck._
import Gen._
// Notes: import hedgehog._
// Generator doesn't produce cross-build project dependencies or do anything with the 'extra' axis import hedgehog.predef.sequence
object TestBuild extends TestBuild object TestBuild extends TestBuild
abstract class TestBuild { abstract class TestBuild {
val MaxTasks = 6 val MaxTasks = 6
@ -30,25 +29,26 @@ abstract class TestBuild {
val MaxDeps = 8 val MaxDeps = 8
val KeysPerEnv = 10 val KeysPerEnv = 10
val MaxTasksGen = chooseShrinkable(1, MaxTasks) val MaxTasksGen = Range.linear(1, MaxTasks)
val MaxProjectsGen = chooseShrinkable(1, MaxProjects) val MaxProjectsGen = Range.linear(1, MaxProjects)
val MaxConfigsGen = chooseShrinkable(1, MaxConfigs) val MaxConfigsGen = Range.linear(1, MaxConfigs)
val MaxBuildsGen = chooseShrinkable(1, MaxBuilds) val MaxBuildsGen = Range.linear(1, MaxBuilds)
val MaxDepsGen = chooseShrinkable(0, MaxDeps) val MaxDepsGen = Range.linear(0, MaxDeps)
val MaxIDSizeGen = Range.linear(0, MaxIDSize)
def chooseShrinkable(min: Int, max: Int): Gen[Int] = def alphaLowerChar: Gen[Char] = Gen.char('a', 'z')
sized(sz => choose(min, (max min sz) max 1)) def alphaUpperChar: Gen[Char] = Gen.char('A', 'Z')
def numChar: Gen[Char] = Gen.char('0', '9')
def alphaNumChar: Gen[Char] =
Gen.frequency1(8 -> alphaLowerChar, 1 -> alphaUpperChar, 1 -> numChar)
val nonEmptyId = for { val nonEmptyId = for {
c <- alphaLowerChar c <- alphaLowerChar
cs <- listOfN(MaxIDSize, alphaNumChar) cs <- Gen.list(alphaNumChar, MaxIDSizeGen)
} yield (c :: cs).mkString } yield (c :: cs).mkString
implicit val cGen = Arbitrary { def cGen = genConfigs(nonEmptyId map { _.capitalize }, MaxDepsGen, MaxConfigsGen)
genConfigs(nonEmptyId.map(_.capitalize), MaxDepsGen, MaxConfigsGen) def tGen = genTasks(kebabIdGen, MaxDepsGen, MaxTasksGen)
}
implicit val tGen = Arbitrary { genTasks(kebabIdGen, MaxDepsGen, MaxTasksGen) }
val seed = rng.Seed.random
class TestKeys(val env: Env, val scopes: Seq[Scope]) { class TestKeys(val env: Env, val scopes: Seq[Scope]) {
override def toString = env + "\n" + scopes.mkString("Scopes:\n\t", "\n\t", "") override def toString = env + "\n" + scopes.mkString("Scopes:\n\t", "\n\t", "")
@ -194,11 +194,10 @@ abstract class TestBuild {
(f(t), t) (f(t), t)
} toMap; } toMap;
implicit lazy val arbKeys: Arbitrary[TestKeys] = Arbitrary(keysGen) lazy val keysGen: Gen[TestKeys] =
lazy val keysGen: Gen[TestKeys] = for { for {
env <- mkEnv env <- mkEnv
keyCount <- chooseShrinkable(1, KeysPerEnv) keys <- scope(env).list(Range.linear(1, KeysPerEnv))
keys <- listOfN(keyCount, scope(env))
} yield new TestKeys(env, keys) } yield new TestKeys(env, keys)
def scope(env: Env): Gen[Scope] = def scope(env: Env): Gen[Scope] =
@ -207,11 +206,16 @@ abstract class TestBuild {
project <- oneOf(build.projects) project <- oneOf(build.projects)
cAxis <- oneOrGlobal(project.configurations map toConfigKey) cAxis <- oneOrGlobal(project.configurations map toConfigKey)
tAxis <- oneOrGlobal(env.tasks map getKey) tAxis <- oneOrGlobal(env.tasks map getKey)
pAxis <- orGlobal(frequency((1, BuildRef(build.uri)), (3, ProjectRef(build.uri, project.id)))) pAxis <- orGlobal(
Gen.frequency1(
(1, Gen.constant[Reference](BuildRef(build.uri))),
(3, Gen.constant[Reference](ProjectRef(build.uri, project.id)))
)
)
} yield Scope(pAxis, cAxis, tAxis, Zero) } yield Scope(pAxis, cAxis, tAxis, Zero)
def orGlobal[T](gen: Gen[T]): Gen[ScopeAxis[T]] = def orGlobal[T](gen: Gen[T]): Gen[ScopeAxis[T]] =
frequency((1, gen map Select.apply), (1, Zero)) Gen.frequency1((1, gen map Select.apply), (1, Gen.constant(Zero)))
def oneOrGlobal[T](gen: Seq[T]): Gen[ScopeAxis[T]] = orGlobal(oneOf(gen)) def oneOrGlobal[T](gen: Seq[T]): Gen[ScopeAxis[T]] = orGlobal(oneOf(gen))
def makeParser(structure: Structure): Parser[ScopedKey[_]] = { def makeParser(structure: Structure): Parser[ScopedKey[_]] = {
@ -227,7 +231,7 @@ abstract class TestBuild {
} }
def structure(env: Env, settings: Seq[Setting[_]], current: ProjectRef): Structure = { def structure(env: Env, settings: Seq[Setting[_]], current: ProjectRef): Structure = {
implicit val display = Def.showRelativeKey2(current) val display = Def.showRelativeKey2(current)
if (settings.isEmpty) { if (settings.isEmpty) {
try { try {
sys.error("settings is empty") sys.error("settings is empty")
@ -249,54 +253,56 @@ abstract class TestBuild {
Structure(env, current, data, KeyIndex(keys, projectsMap, confMap), keyMap) Structure(env, current, data, KeyIndex(keys, projectsMap, confMap), keyMap)
} }
implicit lazy val mkEnv: Gen[Env] = { lazy val mkEnv: Gen[Env] = {
implicit val pGen = (uri: URI) => val pGen = (uri: URI) => genProjects(uri)(nonEmptyId, MaxDepsGen, MaxProjectsGen, cGen)
genProjects(uri)(nonEmptyId, MaxDepsGen, MaxProjectsGen, cGen.arbitrary) envGen(buildGen(uriGen, pGen), tGen)
envGen(buildGen(uriGen, pGen), tGen.arbitrary)
} }
implicit def maskGen(implicit arbBoolean: Arbitrary[Boolean]): Gen[ScopeMask] = { def maskGen: Gen[ScopeMask] = {
val b = arbBoolean.arbitrary val b = Gen.boolean
for (p <- b; c <- b; t <- b; x <- b) for (p <- b; c <- b; t <- b; x <- b)
yield ScopeMask(project = p, config = c, task = t, extra = x) yield ScopeMask(project = p, config = c, task = t, extra = x)
} }
val kebabIdGen: Gen[String] = for { val kebabIdGen: Gen[String] = for {
c <- alphaLowerChar c <- alphaLowerChar
cs <- listOfN(MaxIDSize - 2, frequency(MaxIDSize -> alphaNumChar, 1 -> Gen.const('-'))) cs <- Gen.list(
Gen.frequency(MaxIDSize -> alphaNumChar, List(1 -> Gen.constant('-'))),
Range.linear(0, MaxIDSize - 2)
)
end <- alphaNumChar end <- alphaNumChar
} yield (List(c) ++ cs ++ List(end)).mkString } yield (List(c) ++ cs ++ List(end)).mkString
val uriChar: Gen[Char] = { val optIDGen: Gen[Option[String]] = Gen.choice1(nonEmptyId.map(some.fn), Gen.constant(None))
frequency(9 -> alphaNumChar, 1 -> oneOf("/?-".toSeq))
}
val optIDGen: Gen[Option[String]] = val pathGen = for {
Gen.oneOf(nonEmptyId.map(x => Some(x)), Gen.const(None)) c <- alphaLowerChar
cs <- Gen.list(alphaNumChar, Range.linear(6, MaxIDSize))
} yield (c :: cs).mkString
val uriGen: Gen[URI] = { val uriGen: Gen[URI] = {
for { for {
ssp <- nonEmptyId ssp <- pathGen
frag <- optIDGen frag <- optIDGen
} yield new URI("file", "///" + ssp + "/", frag.orNull) } yield new URI("file", "///" + ssp + "/", frag.orNull)
} }
implicit def envGen(implicit bGen: Gen[Build], tasks: Gen[Vector[Taskk]]): Gen[Env] = def envGen(bGen: Gen[Build], tasks: Gen[Vector[Taskk]]): Gen[Env] =
for (i <- MaxBuildsGen; bs <- containerOfN[Vector, Build](i, bGen); ts <- tasks) for (bs <- bGen.list(MaxBuildsGen).map(_.toVector); ts <- tasks)
yield new Env(bs, ts) yield new Env(bs, ts)
implicit def buildGen(implicit uGen: Gen[URI], pGen: URI => Gen[Seq[Proj]]): Gen[Build] = def buildGen(uGen: Gen[URI], pGen: URI => Gen[Vector[Proj]]): Gen[Build] =
for (u <- uGen; ps <- pGen(u)) yield new Build(u, ps) for (u <- uGen; ps <- pGen(u)) yield new Build(u, ps)
def nGen[T](igen: Gen[Int])(implicit g: Gen[T]): Gen[Vector[T]] = igen flatMap { ig => def nGen[T](igen: Gen[Int])(g: Gen[T]): Gen[Vector[T]] = igen flatMap { ig =>
containerOfN[Vector, T](ig, g) g.list(Range.linear(ig, ig)).map(_.toVector)
} }
implicit def genProjects(build: URI)( def genProjects(build: URI)(
implicit genID: Gen[String], genID: Gen[String],
maxDeps: Gen[Int], maxDeps: Range[Int],
count: Gen[Int], count: Range[Int],
confs: Gen[Seq[Configuration]] confs: Gen[Vector[Configuration]]
): Gen[Seq[Proj]] = ): Gen[Vector[Proj]] =
genAcyclic(maxDeps, genID, count) { (id: String) => genAcyclic(maxDeps, genID, count) { (id: String) =>
for (cs <- confs) yield { (deps: Seq[Proj]) => for (cs <- confs) yield { (deps: Seq[Proj]) =>
new Proj(id, deps.map { dep => new Proj(id, deps.map { dep =>
@ -307,8 +313,8 @@ abstract class TestBuild {
def genConfigs( def genConfigs(
implicit genName: Gen[String], implicit genName: Gen[String],
maxDeps: Gen[Int], maxDeps: Range[Int],
count: Gen[Int] count: Range[Int]
): Gen[Vector[Configuration]] = ): Gen[Vector[Configuration]] =
genAcyclicDirect[Configuration, String](maxDeps, genName, count)( genAcyclicDirect[Configuration, String](maxDeps, genName, count)(
(key, deps) => (key, deps) =>
@ -319,35 +325,34 @@ abstract class TestBuild {
def genTasks( def genTasks(
implicit genName: Gen[String], implicit genName: Gen[String],
maxDeps: Gen[Int], maxDeps: Range[Int],
count: Gen[Int] count: Range[Int]
): Gen[Vector[Taskk]] = ): Gen[Vector[Taskk]] =
genAcyclicDirect[Taskk, String](maxDeps, genName, count)( genAcyclicDirect[Taskk, String](maxDeps, genName, count)(
(key, deps) => new Taskk(AttributeKey[String](key), deps) (key, deps) => new Taskk(AttributeKey[String](key), deps)
) )
def genAcyclicDirect[A, T](maxDeps: Gen[Int], keyGen: Gen[T], max: Gen[Int])( def genAcyclicDirect[A, T](maxDeps: Range[Int], keyGen: Gen[T], max: Range[Int])(
make: (T, Vector[A]) => A make: (T, Vector[A]) => A
): Gen[Vector[A]] = ): Gen[Vector[A]] =
genAcyclic[A, T](maxDeps, keyGen, max) { t => genAcyclic[A, T](maxDeps, keyGen, max) { t =>
Gen.const { deps => Gen.constant { deps =>
make(t, deps.toVector) make(t, deps.toVector)
} }
} }
def genAcyclic[A, T](maxDeps: Gen[Int], keyGen: Gen[T], max: Gen[Int])( def genAcyclic[A, T](maxDeps: Range[Int], keyGen: Gen[T], max: Range[Int])(
make: T => Gen[Vector[A] => A] make: T => Gen[Vector[A] => A]
): Gen[Vector[A]] = ): Gen[Vector[A]] = {
max flatMap { count => keyGen.list(max) flatMap { keys =>
containerOfN[Vector, T](count, keyGen) flatMap { keys => genAcyclic(maxDeps, keys.distinct.toVector)(make)
genAcyclic(maxDeps, keys.distinct)(make)
} }
} }
def genAcyclic[A, T](maxDeps: Gen[Int], keys: Vector[T])( def genAcyclic[A, T](maxDeps: Range[Int], keys: Vector[T])(
make: T => Gen[Vector[A] => A] make: T => Gen[Vector[A] => A]
): Gen[Vector[A]] = ): Gen[Vector[A]] =
genAcyclic(maxDeps, keys, Vector()) flatMap { pairs => genAcyclic(maxDeps, keys, Vector()) flatMap { pairs =>
sequence(pairs.map { case (key, deps) => mapMake(key, deps, make) }) flatMap { inputs => sequence(pairs.map { case (key, deps) => mapMake(key, deps, make) }.toList) map { inputs =>
val made = new collection.mutable.HashMap[T, A] val made = new collection.mutable.HashMap[T, A]
for ((key, deps, mk) <- inputs) for ((key, deps, mk) <- inputs)
made(key) = mk(deps map made) made(key) = mk(deps map made)
@ -361,22 +366,36 @@ abstract class TestBuild {
} }
def genAcyclic[T]( def genAcyclic[T](
maxDeps: Gen[Int], maxDeps: Range[Int],
names: Vector[T], names: Vector[T],
acc: Vector[Gen[(T, Vector[T])]] acc: Vector[Gen[(T, Vector[T])]]
): Gen[Vector[(T, Vector[T])]] = ): Gen[Vector[(T, Vector[T])]] =
names match { names match {
case Vector() => sequence(acc) case Vector() => sequence(acc.toList).map(_.toVector)
case Vector(x, xs @ _*) => case Vector(x, xs @ _*) =>
val next = val next =
for (depCount <- maxDeps; d <- pick(depCount min xs.size, xs)) for (depCount <- Gen.int(maxDeps); d <- pick(depCount, xs))
yield (x, d.toVector) yield (x, d.toVector)
genAcyclic(maxDeps, xs.toVector, next +: acc) genAcyclic(maxDeps, xs.toVector, next +: acc)
} }
def sequence[T](gs: Vector[Gen[T]]): Gen[Vector[T]] = Gen.parameterized { prms =>
delay(gs map { g =>
g(prms, seed) getOrElse sys.error("failed generator")
})
}
type Inputs[A, T] = (T, Vector[T], Vector[A] => A) type Inputs[A, T] = (T, Vector[T], Vector[A] => A)
def oneOf[A](a: Seq[A]): Gen[A] =
Gen.element(a.head, a.tail.toList)
// TODO Should move to hedgehog possible?
def pick[A](n: Int, as: Seq[A]): Gen[Seq[A]] = {
if (n >= as.length) {
Gen.constant(as)
} else {
def go(m: Int, bs: Set[Int], cs: Set[Int]): Gen[Set[Int]] =
if (m == 0)
Gen.constant(cs)
else
Gen.element(bs.head, bs.tail.toList).flatMap(a => go(m - 1, bs - a, cs + a))
go(n, as.indices.toSet, Set())
.map(is => as.zipWithIndex.flatMap(a => if (is(a._2)) Seq(a._1) else Seq()))
}
}
} }

View File

@ -141,4 +141,6 @@ object Dependencies {
val log4jDependencies = Vector(log4jApi, log4jCore, log4jSlf4jImpl) val log4jDependencies = Vector(log4jApi, log4jCore, log4jSlf4jImpl)
val scalaCacheCaffeine = "com.github.cb372" %% "scalacache-caffeine" % "0.20.0" val scalaCacheCaffeine = "com.github.cb372" %% "scalacache-caffeine" % "0.20.0"
val hedgehog = "hedgehog" %% "hedgehog-sbt" % "0.1.0"
} }

View File

@ -11,7 +11,7 @@ object NightlyPlugin extends AutoPlugin {
def testDependencies = libraryDependencies ++= ( def testDependencies = libraryDependencies ++= (
if (includeTestDependencies.value) if (includeTestDependencies.value)
Seq(scalacheck % Test, specs2 % Test, junit % Test, scalatest % Test) Seq(scalacheck % Test, specs2 % Test, junit % Test, scalatest % Test, hedgehog % Test)
else Seq() else Seq()
) )
} }