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.sonatypeRepo("snapshots"),
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),
testFrameworks += TestFramework("hedgehog.sbt.Framework"),
concurrentRestrictions in Global += Util.testExclusiveRestriction,
testOptions in Test += Tests.Argument(TestFrameworks.ScalaCheck, "-w", "1"),
testOptions in Test += Tests.Argument(TestFrameworks.ScalaCheck, "-verbosity", "2"),

View File

@ -7,125 +7,141 @@
package sbt
import Project._
import sbt.internal.util.Types.idFun
import sbt.internal.TestBuild._
import sbt.librarymanagement.Configuration
import org.scalacheck._
import Prop._
import Gen._
import hedgehog._
import hedgehog.Result.{ all, assert, failure, success }
import hedgehog.runner._
object Delegates extends Properties("delegates") {
property("generate non-empty configs") = forAll { (c: Vector[Configuration]) =>
c.nonEmpty
}
property("generate non-empty tasks") = forAll { (t: Vector[Taskk]) =>
t.nonEmpty
}
object Delegates extends Properties {
property("no duplicate scopes") = forAll { (keys: TestKeys) =>
allDelegates(keys) { (_, ds) =>
ds.distinct.size == ds.size
}
}
property("delegates non-empty") = forAll { (keys: TestKeys) =>
allDelegates(keys) { (_, ds) =>
ds.nonEmpty
}
}
property("An initially Zero axis is Zero in all delegates") = allAxes(alwaysZero)
property("Projects precede builds precede Zero") = forAll { (keys: TestKeys) =>
allDelegates(keys) { (scope, ds) =>
val projectAxes = ds.map(_.project)
val nonProject = projectAxes.dropWhile {
case Select(_: ProjectRef) => true; case _ => false
}
val global = nonProject.dropWhile { case Select(_: BuildRef) => true; case _ => false }
global forall { _ == Zero }
}
}
property("Initial scope present with all combinations of Global axes") = allAxes(
(s, ds, _) => globalCombinations(s, ds)
)
property("initial scope first") = forAll { (keys: TestKeys) =>
allDelegates(keys) { (scope, ds) =>
ds.head == scope
}
}
property("global scope last") = forAll { (keys: TestKeys) =>
allDelegates(keys) { (_, ds) =>
ds.last == Scope.GlobalScope
}
}
property("Project axis delegates to BuildRef then Zero") = forAll { (keys: TestKeys) =>
allDelegates(keys) { (key, ds) =>
key.project match {
case Zero => true // filtering out of testing
case Select(rr: ResolvedReference) =>
rr match {
case BuildRef(_) => ds.indexOf(key) < ds.indexOf(key.copy(project = Zero))
case ProjectRef(uri, _) =>
val buildScoped = key.copy(project = Select(BuildRef(uri)))
val idxKey = ds.indexOf(key)
val idxB = ds.indexOf(buildScoped)
val z = key.copy(project = Zero)
val idxZ = ds.indexOf(z)
if (z == Scope.GlobalScope) true
else
(s"idxKey = $idxKey; idxB = $idxB; idxZ = $idxZ") |: (idxKey < idxB) && (idxB < idxZ)
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) =>
ds.distinct.size ==== ds.size
}
}),
property("delegates non-empty", keysGen.forAll.map { keys =>
allDelegates(keys) { (_, ds) =>
assert(ds.nonEmpty)
}
}),
property("An initially Zero axis is Zero in all delegates", allAxes(alwaysZero)),
property(
"Projects precede builds precede Zero",
keysGen.forAll.map { keys =>
allDelegates(keys) { (scope, ds) =>
val projectAxes = ds.map(_.project)
val nonProject = projectAxes.dropWhile {
case Select(_: ProjectRef) => true; case _ => false
}
val global = nonProject.dropWhile { case Select(_: BuildRef) => true; case _ => false }
all(global.map { _ ==== Zero }.toList)
}
case Select(_) | This =>
throw new AssertionError(s"Scope's reference should be resolved, but was ${key.project}")
}
}
}
property("Config axis delegates to parent configuration") = forAll { (keys: TestKeys) =>
allDelegates(keys) { (key, ds) =>
key.config match {
case Zero => true
case Select(config) if key.project.isSelect =>
val p = key.project.toOption.get
val r = keys.env.resolve(p)
keys.env.inheritConfig(r, config).headOption.fold(Prop(true)) { parent =>
val idxKey = ds.indexOf(key)
val a = key.copy(config = Select(parent))
val idxA = ds.indexOf(a)
(s"idxKey = $idxKey; a = $a; idxA = $idxA") |: idxKey < idxA
}
),
property(
"Initial scope present with all combinations of Global axes",
allAxes(
(s, ds, _) => globalCombinations(s, ds)
)
),
property("initial scope first", keysGen.forAll.map { keys =>
allDelegates(keys) { (scope, ds) =>
ds.head ==== scope
}
}),
property("global scope last", keysGen.forAll.map { keys =>
allDelegates(keys) { (_, ds) =>
ds.last ==== Scope.GlobalScope
}
}),
property(
"Project axis delegates to BuildRef then Zero",
keysGen.forAll.map { keys =>
allDelegates(keys) {
(key, ds) =>
key.project match {
case Zero => success // filtering out of testing
case Select(rr: ResolvedReference) =>
rr match {
case BuildRef(_) =>
assert(ds.indexOf(key) < ds.indexOf(key.copy(project = Zero)))
case ProjectRef(uri, _) =>
val buildScoped = key.copy(project = Select(BuildRef(uri)))
val idxKey = ds.indexOf(key)
val idxB = ds.indexOf(buildScoped)
val z = key.copy(project = Zero)
val idxZ = ds.indexOf(z)
(z ==== Scope.GlobalScope)
.or(
assert((idxKey < idxB) && (idxB < idxZ))
.log(s"idxKey = $idxKey; idxB = $idxB; idxZ = $idxZ")
)
}
case Select(_) | This =>
failure.log(s"Scope's reference should be resolved, but was ${key.project}")
}
}
case _ => true
}
}
}
}
),
property(
"Config axis delegates to parent configuration",
keysGen.forAll.map { keys =>
allDelegates(keys) {
(key, ds) =>
key.config match {
case Zero => success
case Select(config) =>
key.project match {
case Select(p @ ProjectRef(_, _)) =>
val r = keys.env.resolve(p)
keys.env.inheritConfig(r, config).headOption.fold(success) { parent =>
val idxKey = ds.indexOf(key)
val a = key.copy(config = Select(parent))
val idxA = ds.indexOf(a)
assert(idxKey < idxA)
.log(s"idxKey = $idxKey; a = $a; idxA = $idxA")
}
case _ => success
}
case _ => success
}
}
}
)
)
def allAxes(f: (Scope, Seq[Scope], Scope => ScopeAxis[_]) => Prop): Prop = forAll {
(keys: TestKeys) =>
def allAxes(f: (Scope, Seq[Scope], Scope => ScopeAxis[_]) => hedgehog.Result): Property =
keysGen.forAll.map { keys =>
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 =
all(keys.scopes map { scope =>
def allDelegates(keys: TestKeys)(f: (Scope, Seq[Scope]) => hedgehog.Result): hedgehog.Result =
all(keys.scopes.map { 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 =
(axis(s) != Zero) ||
all(ds map { d =>
(axis(d) == Zero): Prop
}: _*)
def alwaysZero(s: Scope, ds: Seq[Scope], axis: Scope => ScopeAxis[_]): hedgehog.Result =
assert(axis(s) != Zero).or(
all(ds.map { d =>
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](
_.copy(project = Zero),
_.copy(config = Zero),
@ -143,6 +159,6 @@ object Delegates extends Properties("delegates") {
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
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.internal.TestBuild._
import sbt.internal.util.AttributeKey
import sbt.internal.util.complete.{ DefaultParsers, Parser }
import sbt.internal.util.complete.Parser
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.
* This includes properly resolving omitted components.
*/
object ParseKey extends Properties("Key parser test") {
propertyWithSeed("An explicitly specified axis is always parsed to that explicit value", None) =
forAll(roundtrip(_))
object ParseKey extends Properties {
val exampleCount = 1000
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) = {
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
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 expected = resolve(structure, key, mask)
parseCheck(structure, key, mask, showZeroConfig)(
sk =>
Project.equal(sk, expected, mask)
:| s"$sk.key == $expected.key: ${sk.key == expected.key}"
:| s"${sk.scope} == ${expected.scope}: ${Scope.equal(sk.scope, expected.scope, mask)}"
) :| s"Expected: ${displayFull(expected)}"
hedgehog.Result
.assert(Project.equal(sk, expected, mask))
.log(s"$sk.key == $expected.key: ${sk.key == expected.key}")
.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) = {
import skm.{ structure, key }
val mask = skm.mask.copy(project = false)
// skip when config axis is set to Zero
val hasZeroConfig = key.scope.config == Zero
val showZeroConfig = hasAmbiguousLowercaseAxes(key)
val hasZeroConfig = key.scope.config ==== Zero
val showZeroConfig = hasAmbiguousLowercaseAxes(key, structure)
parseCheck(structure, key, mask, showZeroConfig)(
sk =>
(hasZeroConfig || sk.scope.project == Select(structure.current))
:| s"Current: ${structure.current}"
(hasZeroConfig or sk.scope.project ==== Select(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) = {
import skm.{ structure, key }
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) = {
import skm.{ structure, key }
val mask = ScopeMask(config = false)
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)(
sk => (sk.scope.config == resolvedConfig) || (sk.scope == Scope.GlobalScope)
) :| s"Expected configuration: ${resolvedConfig map (_.name)}"
sk => (sk.scope.config ==== resolvedConfig) or (sk.scope ==== Scope.GlobalScope)
).log(s"Expected configuration: ${resolvedConfig map (_.name)}")
}
implicit val arbStructure: Arbitrary[Structure] = Arbitrary {
val arbStructure: Gen[Structure] =
for {
env <- mkEnv
loadFactor <- choose(0.0, 1.0)
loadFactor <- Gen.double(Range.linearFrac(0.0, 1.0))
scopes <- pickN(loadFactor, env.allFullScopes)
current <- oneOf(env.allProjects.unzip._1)
structure <- {
val settings = structureSettings(scopes, env)
TestBuild.structure(env, settings, current)
}
} yield structure
}
} yield {
val settings = structureSettings(scopes, env)
TestBuild.structure(env, settings, current)
}
def structureSettings(scopes: Seq[Scope], env: Env): Seq[Def.Setting[String]] = {
for {
@ -111,18 +121,18 @@ object ParseKey extends Properties("Key parser test") {
final case class StructureKeyMask(structure: Structure, key: ScopedKey[_], mask: ScopeMask)
implicit val arbStructureKeyMask: Arbitrary[StructureKeyMask] = Arbitrary {
for {
val arbStructureKeyMask: Gen[StructureKeyMask] =
(for {
structure <- arbStructure
// NOTE: Generating this after the structure improves shrinking
mask <- maskGen
structure <- arbitrary[Structure]
key <- for {
scope <- TestBuild.scope(structure.env)
key <- oneOf(structure.allAttributeKeys.toSeq)
} yield ScopedKey(scope, key)
skm = StructureKeyMask(structure, key, mask)
if configExistsInIndex(skm)
} yield skm
}
} yield skm)
.filter(configExistsInIndex)
private def configExistsInIndex(skm: StructureKeyMask): Boolean = {
import skm._
@ -153,18 +163,19 @@ object ParseKey extends Properties("Key parser test") {
key: ScopedKey[_],
mask: ScopeMask,
showZeroConfig: Boolean = false,
)(f: ScopedKey[_] => Prop): Prop = {
)(f: ScopedKey[_] => hedgehog.Result): hedgehog.Result = {
val s = displayMasked(key, mask, showZeroConfig)
val parser = makeParser(structure)
val parsed = Parser.result(parser, s).left.map(_().toString)
(
parsed.fold(_ => falsified, f)
:| s"Key: ${Scope.displayPedantic(key.scope, key.key.label)}"
:| s"Mask: $mask"
:| s"Key string: '$s'"
:| s"Parsed: ${parsed.right.map(displayFull)}"
:| s"Structure: $structure"
)
parsed
.fold(_ => hedgehog.Result.failure, f)
.log(s"Key: ${Scope.displayPedantic(key.scope, key.key.label)}")
.log(s"Mask: $mask")
.log(s"Key string: '$s'")
.log(s"Parsed: ${parsed.right.map(displayFull)}")
.log(s"Structure: $structure")
)
}
// pickN is a function that randomly picks load % items from the "from" sequence.
@ -172,207 +183,16 @@ object ParseKey extends Properties("Key parser test") {
def pickN[T](load: Double, from: Seq[T]): Gen[Seq[T]] =
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")
// then a scoped key like `foo/<conf>/foo/name` would render as `foo/name`
// which would be interpreted as `foo/Zero/Zero/name`
// so we mitigate this by explicitly displaying the configuration axis set to Zero
def hasAmbiguousLowercaseAxes(key: ScopedKey[_]) = PartialFunction.cond(key.scope) {
case Scope(Select(ProjectRef(_, proj)), _, Select(key), _) => proj == key.label
def hasAmbiguousLowercaseAxes(key: ScopedKey[_], structure: Structure): Boolean = {
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)
*/
package sbt
import java.net.URI
import org.scalatest.matchers.MatchResult
import org.scalatest.prop.PropertyChecks
import org.scalatest.{ Matchers, PropSpec }
import sbt.Def._
import sbt._
import sbt.internal.TestBuild
import sbt.internal.TestBuild._
import sbt.internal.util.AttributeKey
import sbt.internal.util.complete.DefaultParsers
import sbt.librarymanagement.Configuration
import hedgehog._
import hedgehog.runner._
class ParserSpec extends PropSpec with PropertyChecks with Matchers {
property("can parse any build") {
forAll(TestBuild.uriGen) { uri =>
parse(buildURI = uri)
}
}
property("can parse any project") {
forAll(TestBuild.nonEmptyId) { id =>
parse(projectID = id)
}
}
property("can parse any configuration") {
forAll(TestBuild.nonEmptyId.map(_.capitalize)) { name =>
parse(configName = name)
}
}
property("can parse any attribute") {
forAll(TestBuild.kebabIdGen) { name =>
parse(attributeName = name)
}
}
object ParserSpec extends Properties {
override def tests: List[Test] =
List(
property("can parse any build", TestBuild.uriGen.forAll.map { uri =>
parse(buildURI = uri)
}),
property("can parse any project", TestBuild.nonEmptyId.forAll.map { id =>
parse(projectID = id)
}),
property("can parse any configuration", TestBuild.nonEmptyId.map(_.capitalize).forAll.map {
name =>
parse(configName = name)
}),
property("can parse any attribute", TestBuild.kebabIdGen.forAll.map { name =>
parse(attributeName = name)
})
)
private def parse(
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 string = displayMasked(scopedKey, ScopeMask())
val parser = makeParser(structure)
string should { left =>
val result = DefaultParsers.result(parser, left)
val resultStr = result.fold(_ => "<parse error>", _.toString)
MatchResult(
result == Right(scopedKey),
s"$left parsed back to $resultStr rather than $scopedKey",
s"$left parsed back to $scopedKey",
)
}
val result = DefaultParsers.result(parser, string)
val resultStr = result.fold(_ => "<parse error>", _.toString)
(result ==== Right(scopedKey))
.log(s"$string parsed back to $resultStr rather than $scopedKey")
}
}

View File

@ -10,16 +10,15 @@ package internal
import Def.{ ScopedKey, Setting }
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.librarymanagement.Configuration
import java.net.URI
import org.scalacheck._
import Gen._
// Notes:
// Generator doesn't produce cross-build project dependencies or do anything with the 'extra' axis
import hedgehog._
import hedgehog.predef.sequence
object TestBuild extends TestBuild
abstract class TestBuild {
val MaxTasks = 6
@ -30,25 +29,26 @@ abstract class TestBuild {
val MaxDeps = 8
val KeysPerEnv = 10
val MaxTasksGen = chooseShrinkable(1, MaxTasks)
val MaxProjectsGen = chooseShrinkable(1, MaxProjects)
val MaxConfigsGen = chooseShrinkable(1, MaxConfigs)
val MaxBuildsGen = chooseShrinkable(1, MaxBuilds)
val MaxDepsGen = chooseShrinkable(0, MaxDeps)
val MaxTasksGen = Range.linear(1, MaxTasks)
val MaxProjectsGen = Range.linear(1, MaxProjects)
val MaxConfigsGen = Range.linear(1, MaxConfigs)
val MaxBuildsGen = Range.linear(1, MaxBuilds)
val MaxDepsGen = Range.linear(0, MaxDeps)
val MaxIDSizeGen = Range.linear(0, MaxIDSize)
def chooseShrinkable(min: Int, max: Int): Gen[Int] =
sized(sz => choose(min, (max min sz) max 1))
def alphaLowerChar: Gen[Char] = Gen.char('a', 'z')
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 {
c <- alphaLowerChar
cs <- listOfN(MaxIDSize, alphaNumChar)
cs <- Gen.list(alphaNumChar, MaxIDSizeGen)
} yield (c :: cs).mkString
implicit val cGen = Arbitrary {
genConfigs(nonEmptyId.map(_.capitalize), MaxDepsGen, MaxConfigsGen)
}
implicit val tGen = Arbitrary { genTasks(kebabIdGen, MaxDepsGen, MaxTasksGen) }
val seed = rng.Seed.random
def cGen = genConfigs(nonEmptyId map { _.capitalize }, MaxDepsGen, MaxConfigsGen)
def tGen = genTasks(kebabIdGen, MaxDepsGen, MaxTasksGen)
class TestKeys(val env: Env, val scopes: Seq[Scope]) {
override def toString = env + "\n" + scopes.mkString("Scopes:\n\t", "\n\t", "")
@ -194,12 +194,11 @@ abstract class TestBuild {
(f(t), t)
} toMap;
implicit lazy val arbKeys: Arbitrary[TestKeys] = Arbitrary(keysGen)
lazy val keysGen: Gen[TestKeys] = for {
env <- mkEnv
keyCount <- chooseShrinkable(1, KeysPerEnv)
keys <- listOfN(keyCount, scope(env))
} yield new TestKeys(env, keys)
lazy val keysGen: Gen[TestKeys] =
for {
env <- mkEnv
keys <- scope(env).list(Range.linear(1, KeysPerEnv))
} yield new TestKeys(env, keys)
def scope(env: Env): Gen[Scope] =
for {
@ -207,11 +206,16 @@ abstract class TestBuild {
project <- oneOf(build.projects)
cAxis <- oneOrGlobal(project.configurations map toConfigKey)
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)
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 makeParser(structure: Structure): Parser[ScopedKey[_]] = {
@ -227,7 +231,7 @@ abstract class TestBuild {
}
def structure(env: Env, settings: Seq[Setting[_]], current: ProjectRef): Structure = {
implicit val display = Def.showRelativeKey2(current)
val display = Def.showRelativeKey2(current)
if (settings.isEmpty) {
try {
sys.error("settings is empty")
@ -249,54 +253,56 @@ abstract class TestBuild {
Structure(env, current, data, KeyIndex(keys, projectsMap, confMap), keyMap)
}
implicit lazy val mkEnv: Gen[Env] = {
implicit val pGen = (uri: URI) =>
genProjects(uri)(nonEmptyId, MaxDepsGen, MaxProjectsGen, cGen.arbitrary)
envGen(buildGen(uriGen, pGen), tGen.arbitrary)
lazy val mkEnv: Gen[Env] = {
val pGen = (uri: URI) => genProjects(uri)(nonEmptyId, MaxDepsGen, MaxProjectsGen, cGen)
envGen(buildGen(uriGen, pGen), tGen)
}
implicit def maskGen(implicit arbBoolean: Arbitrary[Boolean]): Gen[ScopeMask] = {
val b = arbBoolean.arbitrary
def maskGen: Gen[ScopeMask] = {
val b = Gen.boolean
for (p <- b; c <- b; t <- b; x <- b)
yield ScopeMask(project = p, config = c, task = t, extra = x)
}
val kebabIdGen: Gen[String] = for {
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
} yield (List(c) ++ cs ++ List(end)).mkString
val uriChar: Gen[Char] = {
frequency(9 -> alphaNumChar, 1 -> oneOf("/?-".toSeq))
}
val optIDGen: Gen[Option[String]] = Gen.choice1(nonEmptyId.map(some.fn), Gen.constant(None))
val optIDGen: Gen[Option[String]] =
Gen.oneOf(nonEmptyId.map(x => Some(x)), Gen.const(None))
val pathGen = for {
c <- alphaLowerChar
cs <- Gen.list(alphaNumChar, Range.linear(6, MaxIDSize))
} yield (c :: cs).mkString
val uriGen: Gen[URI] = {
for {
ssp <- nonEmptyId
ssp <- pathGen
frag <- optIDGen
} yield new URI("file", "///" + ssp + "/", frag.orNull)
}
implicit def envGen(implicit bGen: Gen[Build], tasks: Gen[Vector[Taskk]]): Gen[Env] =
for (i <- MaxBuildsGen; bs <- containerOfN[Vector, Build](i, bGen); ts <- tasks)
def envGen(bGen: Gen[Build], tasks: Gen[Vector[Taskk]]): Gen[Env] =
for (bs <- bGen.list(MaxBuildsGen).map(_.toVector); ts <- tasks)
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)
def nGen[T](igen: Gen[Int])(implicit g: Gen[T]): Gen[Vector[T]] = igen flatMap { ig =>
containerOfN[Vector, T](ig, g)
def nGen[T](igen: Gen[Int])(g: Gen[T]): Gen[Vector[T]] = igen flatMap { ig =>
g.list(Range.linear(ig, ig)).map(_.toVector)
}
implicit def genProjects(build: URI)(
implicit genID: Gen[String],
maxDeps: Gen[Int],
count: Gen[Int],
confs: Gen[Seq[Configuration]]
): Gen[Seq[Proj]] =
def genProjects(build: URI)(
genID: Gen[String],
maxDeps: Range[Int],
count: Range[Int],
confs: Gen[Vector[Configuration]]
): Gen[Vector[Proj]] =
genAcyclic(maxDeps, genID, count) { (id: String) =>
for (cs <- confs) yield { (deps: Seq[Proj]) =>
new Proj(id, deps.map { dep =>
@ -307,8 +313,8 @@ abstract class TestBuild {
def genConfigs(
implicit genName: Gen[String],
maxDeps: Gen[Int],
count: Gen[Int]
maxDeps: Range[Int],
count: Range[Int]
): Gen[Vector[Configuration]] =
genAcyclicDirect[Configuration, String](maxDeps, genName, count)(
(key, deps) =>
@ -319,35 +325,34 @@ abstract class TestBuild {
def genTasks(
implicit genName: Gen[String],
maxDeps: Gen[Int],
count: Gen[Int]
maxDeps: Range[Int],
count: Range[Int]
): Gen[Vector[Taskk]] =
genAcyclicDirect[Taskk, String](maxDeps, genName, count)(
(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
): Gen[Vector[A]] =
genAcyclic[A, T](maxDeps, keyGen, max) { t =>
Gen.const { deps =>
Gen.constant { deps =>
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]
): Gen[Vector[A]] =
max flatMap { count =>
containerOfN[Vector, T](count, keyGen) flatMap { keys =>
genAcyclic(maxDeps, keys.distinct)(make)
}
): Gen[Vector[A]] = {
keyGen.list(max) flatMap { keys =>
genAcyclic(maxDeps, keys.distinct.toVector)(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]
): Gen[Vector[A]] =
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]
for ((key, deps, mk) <- inputs)
made(key) = mk(deps map made)
@ -361,22 +366,36 @@ abstract class TestBuild {
}
def genAcyclic[T](
maxDeps: Gen[Int],
maxDeps: Range[Int],
names: Vector[T],
acc: Vector[Gen[(T, Vector[T])]]
): Gen[Vector[(T, Vector[T])]] =
names match {
case Vector() => sequence(acc)
case Vector() => sequence(acc.toList).map(_.toVector)
case Vector(x, xs @ _*) =>
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)
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)
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 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 ++= (
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()
)
}