diff --git a/build.sbt b/build.sbt index bc3070129..568950bac 100644 --- a/build.sbt +++ b/build.sbt @@ -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"), diff --git a/main/src/test/scala/Delegates.scala b/main/src/test/scala/Delegates.scala index cc27d7a96..2d317e8c2 100644 --- a/main/src/test/scala/Delegates.scala +++ b/main/src/test/scala/Delegates.scala @@ -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) } } diff --git a/main/src/test/scala/ParseKey.scala b/main/src/test/scala/ParseKey.scala index 953d4bf5d..343e0bfe2 100644 --- a/main/src/test/scala/ParseKey.scala +++ b/main/src/test/scala/ParseKey.scala @@ -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//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) } } diff --git a/main/src/test/scala/ParserSpec.scala b/main/src/test/scala/ParserSpec.scala index 3cc2ab105..5022fa9d3 100644 --- a/main/src/test/scala/ParserSpec.scala +++ b/main/src/test/scala/ParserSpec.scala @@ -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(_ => "", _.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(_ => "", _.toString) + (result ==== Right(scopedKey)) + .log(s"$string parsed back to $resultStr rather than $scopedKey") } } diff --git a/main/src/test/scala/sbt/internal/TestBuild.scala b/main/src/test/scala/sbt/internal/TestBuild.scala index b79141709..a5e02ab7e 100644 --- a/main/src/test/scala/sbt/internal/TestBuild.scala +++ b/main/src/test/scala/sbt/internal/TestBuild.scala @@ -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())) + } + } } diff --git a/project/Dependencies.scala b/project/Dependencies.scala index e8ed09edb..fd4a91489 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -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" } diff --git a/project/NightlyPlugin.scala b/project/NightlyPlugin.scala index 7e5b9ca28..2a1c1a678 100644 --- a/project/NightlyPlugin.scala +++ b/project/NightlyPlugin.scala @@ -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() ) }