mirror of https://github.com/sbt/sbt.git
[2.x] fix: make ScopeFilter .all ordering deterministic and preserve project filter order (#8861)
Fixes nondeterministic ordering in `ScopeFilter`-backed `.all(...)` aggregation. `SettingKeyAll.all` and `TaskKeyAll.all` previously iterated a `Set[Scope]` using `toSeq`, which made returned values order-dependent on hash iteration. This PR keeps `.all(...)` deterministic and preserves explicit project ordering when a filter provides it (for example, `inProjects(c, a, b)`).
This commit is contained in:
parent
2d583bfe65
commit
dfa5e31571
|
|
@ -9,11 +9,11 @@
|
|||
package sbt
|
||||
|
||||
import sbt.internal.{ Load, LoadedBuildUnit }
|
||||
import sbt.internal.util.{ AttributeKey, Dag }
|
||||
import sbt.internal.util.{ AttributeKey, AttributeMap, Dag }
|
||||
import sbt.librarymanagement.{ ConfigRef, Configuration }
|
||||
import sbt.internal.util.Types.const
|
||||
import Def.Initialize
|
||||
import sbt.ScopeAxis.{ Select, Zero }
|
||||
import sbt.ScopeAxis.{ Select, This, Zero }
|
||||
import java.net.URI
|
||||
|
||||
sealed abstract class ScopeFilter { self =>
|
||||
|
|
@ -21,6 +21,12 @@ sealed abstract class ScopeFilter { self =>
|
|||
/** Implements this filter. */
|
||||
private[ScopeFilter] def apply(data: ScopeFilter.Data): Set[Scope]
|
||||
|
||||
/**
|
||||
* Optional ordering hint used by `.all(...)` when the filter can provide a stable
|
||||
* project ordering.
|
||||
*/
|
||||
private[ScopeFilter] def scopeOrdering(data: ScopeFilter.Data): Scope => Option[Int] = _ => None
|
||||
|
||||
/** Constructs a filter that selects values that match this filter but not `other`. */
|
||||
def --(other: ScopeFilter): ScopeFilter = this && -other
|
||||
|
||||
|
|
@ -84,6 +90,10 @@ object ScopeFilter {
|
|||
} yield scope
|
||||
res.toSet
|
||||
|
||||
override private[ScopeFilter] def scopeOrdering(data: Data): Scope => Option[Int] =
|
||||
val projectOrdering = projects.ordering(data)
|
||||
scope => projectOrdering(scope.project)
|
||||
|
||||
def debug(delegate: ScopeFilter): ScopeFilter =
|
||||
new ScopeFilter:
|
||||
def apply(data: Data): Set[Scope] =
|
||||
|
|
@ -97,7 +107,8 @@ object ScopeFilter {
|
|||
* static inspections will not show them.
|
||||
*/
|
||||
def all(sfilter: => ScopeFilter): Initialize[Seq[A]] = Def.flatMap(getData) { data =>
|
||||
sfilter(data).toSeq.map(s => Project.inScope(s, i)).join
|
||||
val filter = sfilter
|
||||
orderedScopes(filter(data), filter.scopeOrdering(data)).map(s => Project.inScope(s, i)).join
|
||||
}
|
||||
|
||||
final class TaskKeyAll[A] private[sbt] (i: Initialize[Task[A]]):
|
||||
|
|
@ -107,9 +118,103 @@ object ScopeFilter {
|
|||
*/
|
||||
def all(sfilter: => ScopeFilter): Initialize[Task[Seq[A]]] = Def.flatMap(getData) { data =>
|
||||
import std.TaskExtra.*
|
||||
sfilter(data).toSeq.map(s => Project.inScope(s, i)).join(_.join)
|
||||
val filter = sfilter
|
||||
orderedScopes(filter(data), filter.scopeOrdering(data))
|
||||
.map(s => Project.inScope(s, i))
|
||||
.join(
|
||||
_.join
|
||||
)
|
||||
}
|
||||
|
||||
private type ScopeAxisOrderingKey = (Int, String)
|
||||
private type ScopeOrderingKey = (
|
||||
ScopeAxisOrderingKey,
|
||||
ScopeAxisOrderingKey,
|
||||
ScopeAxisOrderingKey,
|
||||
ScopeAxisOrderingKey
|
||||
)
|
||||
|
||||
private def orderedScopes(
|
||||
scopes: Iterable[Scope],
|
||||
scopeOrdering: Scope => Option[Int]
|
||||
): Seq[Scope] =
|
||||
scopes.toSeq.sortBy { scope =>
|
||||
(
|
||||
scopeOrdering(scope) match
|
||||
case Some(order) => (0, order)
|
||||
case None => (1, Int.MaxValue),
|
||||
scopeOrderingKey(scope)
|
||||
)
|
||||
}
|
||||
|
||||
private[sbt] def orderedScopesForTests(
|
||||
scopes: Iterable[Scope],
|
||||
scopeOrdering: Scope => Option[Int]
|
||||
): Seq[Scope] = orderedScopes(scopes, scopeOrdering)
|
||||
|
||||
private def scopeOrderingKey(scope: Scope): ScopeOrderingKey =
|
||||
(
|
||||
scopeAxisOrderingKey(scope.project)(referenceOrderingKey),
|
||||
scopeAxisOrderingKey(scope.config)(_.name),
|
||||
scopeAxisOrderingKey(scope.task)(attributeKeyOrderingKey),
|
||||
scopeAxisOrderingKey(scope.extra)(attributeMapOrderingKey)
|
||||
)
|
||||
|
||||
private def scopeAxisOrderingKey[A](
|
||||
axis: ScopeAxis[A]
|
||||
)(selectedKey: A => String): ScopeAxisOrderingKey =
|
||||
axis match
|
||||
case Select(selected) => (0, selectedKey(selected))
|
||||
case This => (1, "")
|
||||
case Zero => (2, "")
|
||||
|
||||
private def referenceOrderingKey(reference: Reference): String =
|
||||
reference match
|
||||
case ThisBuild => "0"
|
||||
case ThisProject => "1"
|
||||
case LocalAggregate => "2"
|
||||
case LocalRootProject => "3"
|
||||
case LocalProject(id) => s"4:$id"
|
||||
case BuildRef(build) => s"5:${uriOrderingKey(build)}"
|
||||
case RootProject(build) => s"6:${uriOrderingKey(build)}"
|
||||
case ProjectRef(build, project) =>
|
||||
s"7:${uriOrderingKey(build)}:$project"
|
||||
|
||||
private def uriOrderingKey(uri: URI): String = uri.normalize.toASCIIString
|
||||
|
||||
private def attributeKeyOrderingKey(key: AttributeKey[?]): String =
|
||||
s"${key.label}:${key.tag}:rank=${key.rank}"
|
||||
|
||||
private def attributeMapOrderingKey(map: AttributeMap): String =
|
||||
map.entries.toSeq
|
||||
.map(entry =>
|
||||
val key = attributeKeyOrderingKey(entry.key)
|
||||
val value = attributeValueOrderingKey(entry.value)
|
||||
s"$key=$value"
|
||||
)
|
||||
.sorted
|
||||
.mkString("[", ",", "]")
|
||||
|
||||
private def attributeValueOrderingKey(value: Any): String =
|
||||
value match
|
||||
case null => "null"
|
||||
case m: collection.Map[?, ?] =>
|
||||
m.iterator
|
||||
.map((k, v) => s"${attributeValueOrderingKey(k)}->${attributeValueOrderingKey(v)}")
|
||||
.toSeq
|
||||
.sorted
|
||||
.mkString("Map(", ",", ")")
|
||||
case i: Iterable[?] =>
|
||||
i.iterator
|
||||
.map(attributeValueOrderingKey)
|
||||
.mkString("Iterable(", ",", ")")
|
||||
case p: Product =>
|
||||
p.productIterator
|
||||
.map(attributeValueOrderingKey)
|
||||
.mkString(s"Product(${p.productPrefix}|", ",", ")")
|
||||
case other =>
|
||||
s"${other.getClass.getName}(${other.toString})"
|
||||
|
||||
private[sbt] val Make = new Make {}
|
||||
trait Make {
|
||||
|
||||
|
|
@ -283,7 +388,20 @@ object ScopeFilter {
|
|||
inResolvedProjects(data => projects.map(data.resolve))
|
||||
|
||||
private def inResolvedProjects(projects: Data => Seq[ProjectRef]): ProjectFilter =
|
||||
selectAxis(data => projects(data).toSet)
|
||||
new AxisFilter[Reference]:
|
||||
private[sbt] def apply(data: Data): ScopeAxis[Reference] => Boolean =
|
||||
val selected = projects(data).toSet
|
||||
_ match
|
||||
case Select(ref: ProjectRef) => selected(ref)
|
||||
case _ => false
|
||||
|
||||
override private[ScopeFilter] def ordering(
|
||||
data: Data
|
||||
): ScopeAxis[Reference] => Option[Int] =
|
||||
val index = projects(data).zipWithIndex.toMap
|
||||
_ match
|
||||
case Select(ref: ProjectRef) => index.get(ref)
|
||||
case _ => None
|
||||
|
||||
private def zeroAxis[T]: AxisFilter[T] = new AxisFilter[T] {
|
||||
private[sbt] def apply(data: Data): ScopeAxis[T] => Boolean = _ == Zero
|
||||
|
|
@ -305,6 +423,11 @@ object ScopeFilter {
|
|||
/** Implements this filter. */
|
||||
private[ScopeFilter] def apply(data: Data): ScopeAxis[In] => Boolean
|
||||
|
||||
/**
|
||||
* Optional ordering hint for axes selected by this filter.
|
||||
*/
|
||||
private[ScopeFilter] def ordering(data: Data): ScopeAxis[In] => Option[Int] = _ => None
|
||||
|
||||
/** Constructs a filter that selects values that match this filter but not `other`. */
|
||||
def --(other: AxisFilter[In]): AxisFilter[In] = this && -other
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* sbt
|
||||
* Copyright 2023, Scala center
|
||||
* Copyright 2011 - 2022, Lightbend, Inc.
|
||||
* Copyright 2008 - 2010, Mark Harrah
|
||||
* Licensed under Apache License 2.0 (see LICENSE)
|
||||
*/
|
||||
|
||||
package sbt
|
||||
|
||||
import sbt.ScopeAxis.{ Select, Zero }
|
||||
import hedgehog.*
|
||||
import hedgehog.runner.*
|
||||
|
||||
import java.net.URI
|
||||
|
||||
object ScopeFilterOrderingSpec extends Properties {
|
||||
private val buildUri = new URI("file:///scope-filter-ordering/")
|
||||
|
||||
override def tests: List[Test] =
|
||||
List(
|
||||
property(
|
||||
"all ordering is deterministic without ordering hints",
|
||||
projectIdsGen.forAll.map { projectIds =>
|
||||
val scopes = projectIds.map(toProjectRef).flatMap(projectScopes)
|
||||
val noHint: Scope => Option[Int] = _ => None
|
||||
val fromSet = ScopeFilter.orderedScopesForTests(scopes.toSet, noHint)
|
||||
val fromReverse = ScopeFilter.orderedScopesForTests(scopes.reverse, noHint)
|
||||
fromSet ==== fromReverse
|
||||
}
|
||||
),
|
||||
property(
|
||||
"all ordering preserves explicit project ordering hints",
|
||||
projectIdsGen.forAll.map { projectIds =>
|
||||
val orderedProjects = projectIds.map(toProjectRef)
|
||||
val expected = orderedProjects.flatMap(projectScopes)
|
||||
val ordering = orderedProjects.zipWithIndex.toMap
|
||||
val byProjectOrder: Scope => Option[Int] = _.project match
|
||||
case Select(ref: ProjectRef) => ordering.get(ref)
|
||||
case _ => None
|
||||
val actual = ScopeFilter.orderedScopesForTests(expected.reverse, byProjectOrder)
|
||||
actual ==== expected
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
private val projectIdGen: Gen[String] =
|
||||
for
|
||||
head <- Gen.char('a', 'z')
|
||||
tail <- Gen.string(Gen.alphaNum, Range.linear(0, 6))
|
||||
yield s"$head$tail"
|
||||
|
||||
private val projectIdsGen: Gen[List[String]] =
|
||||
projectIdGen.list(Range.linear(1, 6)).filter { ids =>
|
||||
ids.distinct.size == ids.size
|
||||
}
|
||||
|
||||
private def toProjectRef(id: String): ProjectRef =
|
||||
ProjectRef(buildUri, id)
|
||||
|
||||
private def projectScopes(project: ProjectRef): List[Scope] =
|
||||
List(
|
||||
Scope(Select(project), Select(ConfigKey("compile")), Zero, Zero),
|
||||
Scope(Select(project), Select(ConfigKey("test")), Zero, Zero),
|
||||
Scope(Select(project), Zero, Zero, Zero)
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
lazy val marker = taskKey[String]("Identifies the selected scope.")
|
||||
|
||||
lazy val checkProjectOrdering = taskKey[Unit]("Checks deterministic project ordering.")
|
||||
lazy val checkConfigOrdering = taskKey[Unit]("Checks deterministic configuration ordering.")
|
||||
lazy val checkAllOrdering = taskKey[Unit]("Runs all ScopeFilter ordering checks.")
|
||||
|
||||
def assertEquals[A](actual: Seq[A], expected: Seq[A], context: String): Unit =
|
||||
assert(actual == expected, s"$context expected=$expected actual=$actual")
|
||||
|
||||
lazy val a = project.settings(
|
||||
marker := "a-global",
|
||||
Compile / marker := "a-compile",
|
||||
Test / marker := "a-test",
|
||||
)
|
||||
|
||||
lazy val b = project.settings(
|
||||
marker := "b-global",
|
||||
Compile / marker := "b-compile",
|
||||
Test / marker := "b-test",
|
||||
)
|
||||
|
||||
lazy val c = project.settings(
|
||||
marker := "c-global",
|
||||
Compile / marker := "c-compile",
|
||||
Test / marker := "c-test",
|
||||
)
|
||||
|
||||
lazy val root = project
|
||||
.aggregate(a, b, c)
|
||||
.settings(
|
||||
checkProjectOrdering := {
|
||||
val values = marker.all(ScopeFilter(inProjects(c, a, b))).value
|
||||
assertEquals(values, Seq("c-global", "a-global", "b-global"), "project ordering")
|
||||
},
|
||||
checkConfigOrdering := {
|
||||
val values = marker.all(
|
||||
ScopeFilter(
|
||||
projects = inProjects(b),
|
||||
configurations = inConfigurations(Test, Compile) || inZeroConfiguration
|
||||
)
|
||||
).value
|
||||
assertEquals(values, Seq("b-compile", "b-test", "b-global"), "configuration ordering")
|
||||
},
|
||||
checkAllOrdering := {
|
||||
checkProjectOrdering.value
|
||||
checkConfigOrdering.value
|
||||
},
|
||||
)
|
||||
|
|
@ -0,0 +1 @@
|
|||
> checkAllOrdering
|
||||
Loading…
Reference in New Issue