From 89d2aa291cba2bab1c68e098b79150436bd11da4 Mon Sep 17 00:00:00 2001 From: Dan Sanduleac Date: Wed, 11 Dec 2013 15:24:31 +0000 Subject: [PATCH 1/8] DerivedSetting not a DefaultSetting anymore --- util/collection/src/main/scala/sbt/Settings.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/util/collection/src/main/scala/sbt/Settings.scala b/util/collection/src/main/scala/sbt/Settings.scala index 32d4b9f85..9c551d829 100644 --- a/util/collection/src/main/scala/sbt/Settings.scala +++ b/util/collection/src/main/scala/sbt/Settings.scala @@ -90,7 +90,7 @@ trait Init[Scope] * Only the static dependencies are tracked, however. Dependencies on previous values do not introduce a derived setting either. */ final def derive[T](s: Setting[T], allowDynamic: Boolean = false, filter: Scope => Boolean = const(true), trigger: AttributeKey[_] => Boolean = const(true)): Setting[T] = { deriveAllowed(s, allowDynamic) foreach error - new DerivedSetting[T](s.key, s.init, s.pos, filter, trigger, nextDefaultID()) + new DerivedSetting[T](s.key, s.init, s.pos, filter, trigger) } def deriveAllowed[T](s: Setting[T], allowDynamic: Boolean): Option[String] = s.init match { case _: Bind[_,_] if !allowDynamic => Some("Cannot derive from dynamic dependencies.") @@ -456,8 +456,8 @@ trait Init[Scope] protected[sbt] def isDerived: Boolean = false private[sbt] def setScope(s: Scope): Setting[T] = make(key.copy(scope = s), init.mapReferenced(mapScope(const(s))), pos) } - private[Init] final class DerivedSetting[T](sk: ScopedKey[T], i: Initialize[T], p: SourcePosition, val filter: Scope => Boolean, val trigger: AttributeKey[_] => Boolean, id: Long) extends DefaultSetting[T](sk, i, p, id) { - override def make[T](key: ScopedKey[T], init: Initialize[T], pos: SourcePosition): Setting[T] = new DerivedSetting[T](key, init, pos, filter, trigger, id) + private[Init] final class DerivedSetting[T](sk: ScopedKey[T], i: Initialize[T], p: SourcePosition, val filter: Scope => Boolean, val trigger: AttributeKey[_] => Boolean) extends Setting[T](sk, i, p) { + override def make[T](key: ScopedKey[T], init: Initialize[T], pos: SourcePosition): Setting[T] = new DerivedSetting[T](key, init, pos, filter, trigger) protected[sbt] override def isDerived: Boolean = true } // Only keep the first occurence of this setting and move it to the front so that it has lower precedence than non-defaults. From 431c61775c2effbbcdef1c27a9c23c656f4a6435 Mon Sep 17 00:00:00 2001 From: Dan Sanduleac Date: Wed, 11 Dec 2013 15:24:46 +0000 Subject: [PATCH 2/8] Derived settings to replace their DerivedSetting, not go at the beginning --- .../src/main/scala/sbt/Settings.scala | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/util/collection/src/main/scala/sbt/Settings.scala b/util/collection/src/main/scala/sbt/Settings.scala index 9c551d829..557113d01 100644 --- a/util/collection/src/main/scala/sbt/Settings.scala +++ b/util/collection/src/main/scala/sbt/Settings.scala @@ -301,6 +301,7 @@ trait Init[Scope] val dependencies = setting.dependencies.map(_.key) def triggeredBy = dependencies.filter(setting.trigger) val inScopes = new mutable.HashSet[Scope] + val outputs = new mutable.ListBuffer[Setting[_]] } final class Deriveds(val key: AttributeKey[_], val settings: mutable.ListBuffer[Derived]) { def dependencies = settings.flatMap(_.dependencies) @@ -312,6 +313,7 @@ trait Init[Scope] val (derived, rawDefs) = Util.separate[Setting[_],Derived,Setting[_]](init) { case d: DerivedSetting[_] => Left(new Derived(d)); case s => Right(s) } val defs = addLocal(rawDefs)(scopeLocal) + // group derived settings by the key they define val derivsByDef = new mutable.HashMap[AttributeKey[_], Deriveds] for(s <- derived) { @@ -329,6 +331,10 @@ trait Init[Scope] for(s <- derived; d <- s.triggeredBy) derivedBy.getOrElseUpdate(d, new mutable.ListBuffer) += s + // Map a DerivedSetting[_] to the `Derived` struct wrapping it. Used to ultimately replace a DerivedSetting with + // the `Setting`s that were actually derived from it: `Derived.outputs` + val derivedToStruct: Map[DerivedSetting[_], Derived] = (derived map { s => s.setting -> s }).toMap + // set of defined scoped keys, used to ensure a derived setting is only added if all dependencies are present val defined = new mutable.HashSet[ScopedKey[_]] def addDefs(ss: Seq[Setting[_]]) { for(s <- ss) defined += s.key } @@ -357,7 +363,9 @@ trait Init[Scope] val local = d.dependencies.flatMap(dep => scopeLocal(ScopedKey(scope, dep))) if(allDepsDefined(d, scope, local.map(_.key.key).toSet)) { d.inScopes.add(scope) - local :+ d.setting.setScope(scope) + val out = local :+ d.setting.setScope(scope) + d.outputs ++= out + out } else Nil } @@ -366,21 +374,22 @@ trait Init[Scope] } val processed = new mutable.HashSet[ScopedKey[_]] - // valid derived settings to be added before normal settings - val out = new mutable.ListBuffer[Setting[_]] // derives settings, transitively so that a derived setting can trigger another def process(rem: List[Setting[_]]): Unit = rem match { case s :: ss => val sk = s.key val ds = if(processed.add(sk)) deriveFor(sk) else Nil - out ++= ds addDefs(ds) process(ds ::: ss) case Nil => } process(defs.toList) - out.toList ++ defs + + // Take all the original defs and DerivedSettings along with locals, replace each DerivedSetting with the actual + // settings that were derived. + val allDefs = addLocal(init)(scopeLocal) + allDefs flatMap { case d: DerivedSetting[_] => (derivedToStruct get d map (_.outputs)).toStream.flatten; case s => Stream(s) } } sealed trait Initialize[T] From 6f5242d812acc1d99fdfae512fffbbaa9f715578 Mon Sep 17 00:00:00 2001 From: Dan Sanduleac Date: Tue, 17 Dec 2013 12:30:49 +0000 Subject: [PATCH 3/8] Derive settings only under the scope of the DerivedSetting --- .../src/main/scala/sbt/Settings.scala | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/util/collection/src/main/scala/sbt/Settings.scala b/util/collection/src/main/scala/sbt/Settings.scala index 557113d01..89e8a9d7e 100644 --- a/util/collection/src/main/scala/sbt/Settings.scala +++ b/util/collection/src/main/scala/sbt/Settings.scala @@ -348,28 +348,37 @@ trait Init[Scope] def allDepsDefined(d: Derived, scope: Scope, local: Set[AttributeKey[_]]): Boolean = d.dependencies.forall(dep => local(dep) || isDefined(dep, scope)) - // List of injectable derived settings and their local settings for `sk`. + // Returns the list of injectable derived settings and their local settings for `sk`. + // The settings are to be injected under `outputScope` = whichever scope is more specific of: + // * the dependency's (`sk`) scope + // * the DerivedSetting's scope in which it has been declared, `definingScope` + // provided that these two scopes intersect. // A derived setting is injectable if: - // 1. it has not been previously injected into this scope - // 2. it applies to this scope (as determined by its `filter`) - // 3. all of its dependencies that match `trigger` are defined for that scope (allowing for delegation) + // 1. it has not been previously injected into outputScope + // 2. it applies to outputScope (as determined by its `filter`) + // 3. all of its dependencies are defined for outputScope (allowing for delegation) // This needs to handle local settings because a derived setting wouldn't be injected if it's local setting didn't exist yet. val deriveFor = (sk: ScopedKey[_]) => { val derivedForKey: List[Derived] = derivedBy.get(sk.key).toList.flatten val scope = sk.scope - def localAndDerived(d: Derived): Seq[Setting[_]] = - if(!d.inScopes.contains(scope) && d.setting.filter(scope)) - { - val local = d.dependencies.flatMap(dep => scopeLocal(ScopedKey(scope, dep))) - if(allDepsDefined(d, scope, local.map(_.key.key).toSet)) { - d.inScopes.add(scope) - val out = local :+ d.setting.setScope(scope) + def localAndDerived(d: Derived): Seq[Setting[_]] = { + def definingScope = d.setting.key.scope + def intersect(s1: Scope, s2: Scope): Option[Scope] = + if (delegates(s1).contains(s2)) Some(s1) // s1 is more specific + else if (delegates(s2).contains(s1)) Some(s2) // s2 is more specific + else None + val outputScope = intersect(scope, definingScope) + outputScope collect { case s if !d.inScopes.contains(s) && d.setting.filter(s) => + val local = d.dependencies.flatMap(dep => scopeLocal(ScopedKey(s, dep))) + if(allDepsDefined(d, s, local.map(_.key.key).toSet)) { + d.inScopes.add(s) + val out = local :+ d.setting.setScope(s) d.outputs ++= out out } else Nil - } - else Nil + } getOrElse Nil + } derivedForKey.flatMap(localAndDerived) } From decd323b645f442c29b6f8129879de1aa2040b3b Mon Sep 17 00:00:00 2001 From: Dan Sanduleac Date: Tue, 17 Dec 2013 18:19:43 +0000 Subject: [PATCH 4/8] Decouple DefaultSetting from Setting/DerivedSetting; BuildCommon.derive() produces default settings by default --- .../src/main/scala/sbt/Settings.scala | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/util/collection/src/main/scala/sbt/Settings.scala b/util/collection/src/main/scala/sbt/Settings.scala index 89e8a9d7e..af56cb4f8 100644 --- a/util/collection/src/main/scala/sbt/Settings.scala +++ b/util/collection/src/main/scala/sbt/Settings.scala @@ -88,19 +88,17 @@ trait Init[Scope] * is explicitly defined and the where the scope matches `filter`. * A setting initialized with dynamic dependencies is only allowed if `allowDynamic` is true. * Only the static dependencies are tracked, however. Dependencies on previous values do not introduce a derived setting either. */ - final def derive[T](s: Setting[T], allowDynamic: Boolean = false, filter: Scope => Boolean = const(true), trigger: AttributeKey[_] => Boolean = const(true)): Setting[T] = { + final def derive[T](s: Setting[T], allowDynamic: Boolean = false, filter: Scope => Boolean = const(true), trigger: AttributeKey[_] => Boolean = const(true), default: Boolean = false): Setting[T] = { deriveAllowed(s, allowDynamic) foreach error - new DerivedSetting[T](s.key, s.init, s.pos, filter, trigger) + def d = new DerivedSetting[T](s.key, s.init, s.pos, filter, trigger) + if (default) d.default() else d } def deriveAllowed[T](s: Setting[T], allowDynamic: Boolean): Option[String] = s.init match { case _: Bind[_,_] if !allowDynamic => Some("Cannot derive from dynamic dependencies.") case _ => None } // id is used for equality - private[sbt] final def defaultSetting[T](s: Setting[T]): Setting[T] = s match { - case _: DefaultSetting[_] | _: DerivedSetting[_] => s - case _ => new DefaultSetting[T](s.key, s.init, s.pos, nextDefaultID()) - } + private[sbt] final def defaultSetting[T](s: Setting[T]): Setting[T] = s.default() private[sbt] def defaultSettings(ss: Seq[Setting[_]]): Seq[Setting[_]] = ss.map(s => defaultSetting(s)) private[this] final val nextID = new java.util.concurrent.atomic.AtomicLong private[this] final def nextDefaultID(): Long = nextID.incrementAndGet() @@ -473,17 +471,28 @@ trait Init[Scope] protected[this] def make[T](key: ScopedKey[T], init: Initialize[T], pos: SourcePosition): Setting[T] = new Setting[T](key, init, pos) protected[sbt] def isDerived: Boolean = false private[sbt] def setScope(s: Scope): Setting[T] = make(key.copy(scope = s), init.mapReferenced(mapScope(const(s))), pos) + /** Turn this setting into a `DefaultSetting` if it's not already, otherwise returns `this` */ + private[sbt] def default(id: => Long = nextDefaultID()): DefaultSetting[T] = DefaultSetting(key, init, pos, id) } - private[Init] final class DerivedSetting[T](sk: ScopedKey[T], i: Initialize[T], p: SourcePosition, val filter: Scope => Boolean, val trigger: AttributeKey[_] => Boolean) extends Setting[T](sk, i, p) { + private[Init] sealed class DerivedSetting[T](sk: ScopedKey[T], i: Initialize[T], p: SourcePosition, val filter: Scope => Boolean, val trigger: AttributeKey[_] => Boolean) extends Setting[T](sk, i, p) { override def make[T](key: ScopedKey[T], init: Initialize[T], pos: SourcePosition): Setting[T] = new DerivedSetting[T](key, init, pos, filter, trigger) protected[sbt] override def isDerived: Boolean = true + override def default(_id: => Long): DefaultSetting[T] = new DerivedSetting[T](sk, i, p, filter, trigger) with DefaultSetting[T] { val id = _id } + override def toString = "derived " + super.toString } // Only keep the first occurence of this setting and move it to the front so that it has lower precedence than non-defaults. // This is intended for internal sbt use only, where alternatives like Plugin.globalSettings are not available. - private[Init] sealed class DefaultSetting[T](sk: ScopedKey[T], i: Initialize[T], p: SourcePosition, val id: Long) extends Setting[T](sk, i, p) { - override def make[T](key: ScopedKey[T], init: Initialize[T], pos: SourcePosition): Setting[T] = new DefaultSetting[T](key, init, pos, id) + private[Init] sealed trait DefaultSetting[T] extends Setting[T] { + val id: Long + override def make[T](key: ScopedKey[T], init: Initialize[T], pos: SourcePosition): Setting[T] = super.make(key, init, pos) default id override final def hashCode = id.hashCode override final def equals(o: Any): Boolean = o match { case d: DefaultSetting[_] => d.id == id; case _ => false } + override def toString = s"default($id) " + super.toString + override def default(id: => Long) = this + } + + object DefaultSetting { + def apply[T](sk: ScopedKey[T], i: Initialize[T], p: SourcePosition, _id: Long) = new Setting[T](sk, i, p) with DefaultSetting[T] { val id = _id } } From 962f0bad763ea57aa0bf39a51d329e3f355bafdc Mon Sep 17 00:00:00 2001 From: Dan Sanduleac Date: Wed, 18 Dec 2013 10:52:45 +0000 Subject: [PATCH 5/8] Optimise scope intersection for GlobalScope --- util/collection/src/main/scala/sbt/Settings.scala | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/util/collection/src/main/scala/sbt/Settings.scala b/util/collection/src/main/scala/sbt/Settings.scala index af56cb4f8..bc88520f1 100644 --- a/util/collection/src/main/scala/sbt/Settings.scala +++ b/util/collection/src/main/scala/sbt/Settings.scala @@ -3,7 +3,7 @@ */ package sbt - import Types._ +import Types._ sealed trait Settings[Scope] { @@ -291,6 +291,11 @@ trait Init[Scope] } else "" } + /** + * Intersects two scopes, returning the more specific one if they intersect, or None otherwise. + * Not implemented here because we want to optimise for Scope.GlobalScope which is inaccessible here. */ + private[sbt] def intersect(s1: Scope, s2: Scope)(implicit delegates: Scope => Seq[Scope]): Option[Scope] = ??? + private[this] def deriveAndLocal(init: Seq[Setting[_]])(implicit delegates: Scope => Seq[Scope], scopeLocal: ScopeLocal): Seq[Setting[_]] = { import collection.mutable @@ -361,10 +366,6 @@ trait Init[Scope] val scope = sk.scope def localAndDerived(d: Derived): Seq[Setting[_]] = { def definingScope = d.setting.key.scope - def intersect(s1: Scope, s2: Scope): Option[Scope] = - if (delegates(s1).contains(s2)) Some(s1) // s1 is more specific - else if (delegates(s2).contains(s1)) Some(s2) // s2 is more specific - else None val outputScope = intersect(scope, definingScope) outputScope collect { case s if !d.inScopes.contains(s) && d.setting.filter(s) => val local = d.dependencies.flatMap(dep => scopeLocal(ScopedKey(s, dep))) From 767e2487d030b96a1021480558d39a8fffdd40d5 Mon Sep 17 00:00:00 2001 From: Dan Sanduleac Date: Mon, 23 Dec 2013 15:27:29 +0000 Subject: [PATCH 6/8] Couple of fixes --- util/collection/src/main/scala/sbt/Settings.scala | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/util/collection/src/main/scala/sbt/Settings.scala b/util/collection/src/main/scala/sbt/Settings.scala index bc88520f1..7a6a7b7ee 100644 --- a/util/collection/src/main/scala/sbt/Settings.scala +++ b/util/collection/src/main/scala/sbt/Settings.scala @@ -90,7 +90,7 @@ trait Init[Scope] * Only the static dependencies are tracked, however. Dependencies on previous values do not introduce a derived setting either. */ final def derive[T](s: Setting[T], allowDynamic: Boolean = false, filter: Scope => Boolean = const(true), trigger: AttributeKey[_] => Boolean = const(true), default: Boolean = false): Setting[T] = { deriveAllowed(s, allowDynamic) foreach error - def d = new DerivedSetting[T](s.key, s.init, s.pos, filter, trigger) + val d = new DerivedSetting[T](s.key, s.init, s.pos, filter, trigger) if (default) d.default() else d } def deriveAllowed[T](s: Setting[T], allowDynamic: Boolean): Option[String] = s.init match { @@ -293,8 +293,11 @@ trait Init[Scope] /** * Intersects two scopes, returning the more specific one if they intersect, or None otherwise. - * Not implemented here because we want to optimise for Scope.GlobalScope which is inaccessible here. */ - private[sbt] def intersect(s1: Scope, s2: Scope)(implicit delegates: Scope => Seq[Scope]): Option[Scope] = ??? + */ + private[sbt] def intersect(s1: Scope, s2: Scope)(implicit delegates: Scope => Seq[Scope]): Option[Scope] = + if (delegates(s1).contains(s2)) Some(s1) // s1 is more specific + else if (delegates(s2).contains(s1)) Some(s2) // s2 is more specific + else None private[this] def deriveAndLocal(init: Seq[Setting[_]])(implicit delegates: Scope => Seq[Scope], scopeLocal: ScopeLocal): Seq[Setting[_]] = { From eb93fdd7a6d6d5b9f3d00742a3f9022e1563df85 Mon Sep 17 00:00:00 2001 From: Dan Sanduleac Date: Thu, 1 May 2014 03:09:50 +0100 Subject: [PATCH 7/8] Improve SettingsExample to allow orthogonal scopes (like projects/tasks) at a certain nestIndex --- util/collection/src/test/scala/SettingsExample.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/util/collection/src/test/scala/SettingsExample.scala b/util/collection/src/test/scala/SettingsExample.scala index 637f0ad51..9d863be31 100644 --- a/util/collection/src/test/scala/SettingsExample.scala +++ b/util/collection/src/test/scala/SettingsExample.scala @@ -3,7 +3,7 @@ package sbt /** Define our settings system */ // A basic scope indexed by an integer. -final case class Scope(index: Int) +final case class Scope(nestIndex: Int, idAtIndex: Int = 0) // Extend the Init trait. // (It is done this way because the Scope type parameter is used everywhere in Init. @@ -14,12 +14,12 @@ object SettingsExample extends Init[Scope] { // Provides a way of showing a Scope+AttributeKey[_] val showFullKey: Show[ScopedKey[_]] = new Show[ScopedKey[_]] { - def apply(key: ScopedKey[_]) = key.scope.index + "/" + key.key.label + def apply(key: ScopedKey[_]) = s"${key.scope.nestIndex}(${key.scope.idAtIndex})/${key.key.label}" } // A sample delegation function that delegates to a Scope with a lower index. - val delegates: Scope => Seq[Scope] = { case s @ Scope(index) => - s +: (if(index <= 0) Nil else delegates(Scope(index-1)) ) + val delegates: Scope => Seq[Scope] = { case s @ Scope(index, proj) => + s +: (if(index <= 0) Nil else { (if (proj > 0) List(Scope(index)) else Nil) ++: delegates(Scope(index-1)) }) } // Not using this feature in this example. From 1e9dee900a4acd2a9e7f1b2a75e93db6568ab17e Mon Sep 17 00:00:00 2001 From: Dan Sanduleac Date: Thu, 1 May 2014 03:11:20 +0100 Subject: [PATCH 8/8] Add 2 derived settings tests: 1) non-default derived settings, if they produce anything, the settings they produce must supersede previous assignents (in the settings seq) to the same key. 2) even if a derived setting is scoped at a higher scope (e.g. ThisBuild) the settings it produces are scoped at the intersection of that (the defining) scope and the scope of the triggering dependency. 2 is particularly nice as it enables this behaviour: derive(b in ThisBuild := a.value + 1) a in project1 := 0 // a could be defined in all projects ==> Now (b in project1).value == (a in project1).value + 1 == 1 and similarly in all other projects all with a single derived setting --- .../src/test/scala/SettingsTest.scala | 104 ++++++++++++++---- 1 file changed, 80 insertions(+), 24 deletions(-) diff --git a/util/collection/src/test/scala/SettingsTest.scala b/util/collection/src/test/scala/SettingsTest.scala index 0cfb5ea83..1bdea8f38 100644 --- a/util/collection/src/test/scala/SettingsTest.scala +++ b/util/collection/src/test/scala/SettingsTest.scala @@ -7,6 +7,9 @@ import SettingsExample._ object SettingsTest extends Properties("settings") { + + import scala.reflect.Manifest + final val ChainMax = 5000 lazy val chainLengthGen = Gen.choose(1, ChainMax) @@ -35,31 +38,84 @@ object SettingsTest extends Properties("settings") evaluate( setting(chk, iterate(top)) :: Nil); true } - property("Derived setting chain depending on (prev derived, normal setting)") = forAllNoShrink(Gen.choose(1, 100)) { derivedSettings } - final def derivedSettings(nr: Int): Prop = - { - val alphaStr = Gen.alphaStr - val genScopedKeys = { - val attrKeys = for { - list <- Gen.listOfN(nr, alphaStr) suchThat (l => l.size == l.distinct.size) - item <- list - } yield AttributeKey[Int](item) - attrKeys map (_ map (ak => ScopedKey(Scope(0), ak))) - } - forAll(genScopedKeys) { scopedKeys => - val last = scopedKeys.last - val derivedSettings: Seq[Setting[Int]] = ( - for { - List(scoped0, scoped1) <- chk :: scopedKeys sliding 2 - nextInit = if (scoped0 == chk) chk - else (scoped0 zipWith chk) { (p, _) => p + 1 } - } yield derive(setting(scoped1, nextInit)) - ).toSeq + property("Derived setting chain depending on (prev derived, normal setting)") = forAllNoShrink(Gen.choose(1, 100)) { derivedSettings } + final def derivedSettings(nr: Int): Prop = + { + val genScopedKeys = { + val attrKeys = mkAttrKeys[Int](nr) + attrKeys map (_ map (ak => ScopedKey(Scope(0), ak))) + } + forAll(genScopedKeys) { scopedKeys => + val last = scopedKeys.last + val derivedSettings: Seq[Setting[Int]] = ( + for { + List(scoped0, scoped1) <- chk :: scopedKeys sliding 2 + nextInit = if (scoped0 == chk) chk + else (scoped0 zipWith chk) { (p, _) => p + 1 } + } yield derive(setting(scoped1, nextInit)) + ).toSeq - { checkKey(last, Some(nr-1), evaluate(setting(chk, value(0)) +: derivedSettings)) :| "Not derived?" } && - { checkKey( last, None, evaluate(derivedSettings)) :| "Should not be derived" } - } - } + { checkKey(last, Some(nr-1), evaluate(setting(chk, value(0)) +: derivedSettings)) :| "Not derived?" } && + { checkKey( last, None, evaluate(derivedSettings)) :| "Should not be derived" } + } + } + + private def mkAttrKeys[T](nr: Int)(implicit mf: Manifest[T]): Gen[List[AttributeKey[T]]] = + { + val alphaStr = Gen.alphaStr + for { + list <- Gen.listOfN(nr, alphaStr) suchThat (l => l.size == l.distinct.size) + item <- list + } yield AttributeKey[T](item) + } + + property("Derived setting(s) replace DerivedSetting in the Seq[Setting[_]]") = derivedKeepsPosition + final def derivedKeepsPosition: Prop = + { + val a: ScopedKey[Int] = ScopedKey(Scope(0), AttributeKey[Int]("a")) + val b: ScopedKey[Int] = ScopedKey(Scope(0), AttributeKey[Int]("b")) + val prop1 = { + val settings: Seq[Setting[_]] = Seq( + setting(a, value(3)), + setting(b, value(6)), + derive(setting(b, a)), + setting(a, value(5)), + setting(b, value(8)) + ) + val ev = evaluate(settings) + checkKey(a, Some(5), ev) && checkKey(b, Some(8), ev) + } + val prop2 = { + val settings: Seq[Setting[Int]] = Seq( + setting(a, value(3)), + setting(b, value(6)), + derive(setting(b, a)), + setting(a, value(5)) + ) + val ev = evaluate(settings) + checkKey(a, Some(5), ev) && checkKey(b, Some(5), ev) + } + prop1 && prop2 + } + + property("DerivedSetting in ThisBuild scopes derived settings under projects thus allowing safe +=") = forAllNoShrink(Gen.choose(1, 100)) { derivedSettingsScope } + final def derivedSettingsScope(nrProjects: Int): Prop = + { + forAll(mkAttrKeys[Int](2)) { case List(key, derivedKey) => + val projectKeys = for { proj <- 1 to nrProjects } yield ScopedKey(Scope(1, proj), key) + val projectDerivedKeys = for { proj <- 1 to nrProjects } yield ScopedKey(Scope(1, proj), derivedKey) + val globalKey = ScopedKey(Scope(0), key) + val globalDerivedKey = ScopedKey(Scope(0), derivedKey) + // Each project defines an initial value, but the update is defined in globalKey. + // However, the derived Settings that come from this should be scoped in each project. + val settings: Seq[Setting[_]] = + derive(setting(globalDerivedKey, SettingsExample.map(globalKey)(_ + 1))) +: projectKeys.map(pk => setting(pk, value(0))) + val ev = evaluate(settings) + // Also check that the key has no value at the "global" scope + val props = for { pk <- projectDerivedKeys } yield checkKey(pk, Some(1), ev) + checkKey(globalDerivedKey, None, ev) && Prop.all(props: _*) + } + } // Circular (dynamic) references currently loop infinitely. // This is the expected behavior (detecting dynamic cycles is expensive),