Skip to content

Commit f18ecb2

Browse files
johnynekOscar Boykin
andauthored
Use SortedMap in GradleResolver, fix property tests (#377)
Co-authored-by: Oscar Boykin <oboykin@netflix.com>
1 parent f5608bf commit f18ecb2

File tree

7 files changed

+155
-53
lines changed

7 files changed

+155
-53
lines changed

src/scala/com/github/johnynek/bazel_deps/GradleLockFile.scala

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.github.johnynek.bazel_deps
33
import io.circe.Decoder.Result
44
import io.circe.{Decoder, Error, HCursor, Json, KeyDecoder, Parser}
55
import io.circe.generic.auto
6+
import scala.collection.immutable.SortedMap
67

78
import scala.util.{Failure, Success, Try}
89

@@ -69,10 +70,10 @@ object GradleLockFile {
6970
}
7071

7172
case class GradleLockFile(
72-
annotationProcessor: Option[Map[String, GradleLockDependency]],
73-
compileClasspath: Option[Map[String, GradleLockDependency]],
74-
resolutionRules: Option[Map[String, GradleLockDependency]],
75-
runtimeClasspath: Option[Map[String, GradleLockDependency]],
76-
testCompileClasspath: Option[Map[String, GradleLockDependency]],
77-
testRuntimeClasspath: Option[Map[String, GradleLockDependency]]
73+
annotationProcessor: Option[SortedMap[String, GradleLockDependency]],
74+
compileClasspath: Option[SortedMap[String, GradleLockDependency]],
75+
resolutionRules: Option[SortedMap[String, GradleLockDependency]],
76+
runtimeClasspath: Option[SortedMap[String, GradleLockDependency]],
77+
testCompileClasspath: Option[SortedMap[String, GradleLockDependency]],
78+
testRuntimeClasspath: Option[SortedMap[String, GradleLockDependency]]
7879
)

src/scala/com/github/johnynek/bazel_deps/GradleResolver.scala

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class GradleResolver(
4242
// we just want one, so we merge them all
4343
private def collapseDepTypes(
4444
lockFile: GradleLockFile
45-
): Try[Map[String, GradleLockDependency]] =
45+
): Try[SortedMap[String, GradleLockDependency]] =
4646
TryMerge.tryMergeAll(
4747
None,
4848
NonEmptyList.of(
@@ -54,7 +54,7 @@ class GradleResolver(
5454
lockFile.testRuntimeClasspath
5555
)
5656
)
57-
.map(_.getOrElse(Map.empty))
57+
.map(_.getOrElse(SortedMap.empty[String, GradleLockDependency]))
5858

5959
private val unit = Validated.valid(())
6060

@@ -238,7 +238,7 @@ private def cleanUpMap(
238238
}
239239

240240
def buildGraphFromDepMap(
241-
depMap: Map[String, GradleLockDependency]
241+
depMap: SortedMap[String, GradleLockDependency]
242242
): Try[Graph[MavenCoordinate, Unit]] =
243243
assertConnectedDependencyMap(depMap)
244244
.map(_ => cleanUpMap(depMap))
@@ -274,7 +274,6 @@ private def cleanUpMap(
274274

275275
depMap
276276
.toList
277-
.sortBy(_._1)
278277
.traverse[V, (MavenCoordinate, List[MavenCoordinate])] { case (n, deps) =>
279278
toCoord(n)
280279
.product(

src/scala/com/github/johnynek/bazel_deps/TryMerge.scala

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.github.johnynek.bazel_deps
33
import cats.{Foldable, Reducible}
44
import cats.data.NonEmptyList
55
import scala.util.{Failure, Success, Try}
6+
import scala.collection.immutable.SortedMap
67

78
import cats.syntax.all._
89

@@ -40,33 +41,29 @@ object TryMerge {
4041
}
4142
}
4243

43-
implicit def tryStringMapMerge[T: TryMerge]: TryMerge[Map[String, T]] =
44-
new TryMerge[Map[String, T]] {
45-
private val rev = reverse
44+
implicit def tryStringMapMerge[T: TryMerge]: TryMerge[SortedMap[String, T]] =
45+
new TryMerge[SortedMap[String, T]] {
4646
def tryMerge(
4747
debugName: Option[String],
48-
left: Map[String, T],
49-
right: Map[String, T]
50-
): Try[Map[String, T]] = {
51-
if (right.size > left.size) rev.tryMerge(debugName, right, left)
52-
else {
53-
// right <= left in size
54-
val overlaps = right.exists { case (k, _) => left.contains(k) }
55-
if (!overlaps) Success(left ++ right)
56-
else {
57-
right.toList.sortBy(_._1)
58-
.foldM(left) { case (acc, (k, rv)) =>
59-
acc.get(k) match {
60-
case Some(lv) =>
61-
TryMerge.tryMerge(Some(debugName.fold(k) {p => s"$p:$k"}), lv, rv)
62-
.map(acc.updated(k, _))
63-
case None =>
64-
Success(acc.updated(k, rv))
65-
}
66-
}
48+
left: SortedMap[String, T],
49+
right: SortedMap[String, T]
50+
): Try[SortedMap[String, T]] = {
51+
val tmt = implicitly[TryMerge[T]]
52+
val (big, small, tm) =
53+
if (right.size > left.size) (right, left, tmt.reverse)
54+
else (left, right, tmt)
55+
56+
small.toStream
57+
.foldM(big) { case (acc, (k, rv)) =>
58+
acc.get(k) match {
59+
case Some(lv) =>
60+
val nextDebug = Some(debugName.fold(k) {p => s"$p:$k"})
61+
tm.tryMerge(nextDebug, lv, rv).map(acc.updated(k, _))
62+
case None =>
63+
Success(acc.updated(k, rv))
64+
}
6765
}
6866
}
6967
}
70-
}
7168
}
7269

test/scala/com/github/johnynek/bazel_deps/BUILD.bazel

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,4 +203,16 @@ scala_test(
203203
]
204204
)
205205

206+
scala_test(
207+
name = "trymergetest",
208+
srcs = ["TryMergeTest.scala"],
209+
deps = [
210+
"//src/scala/com/github/johnynek/bazel_deps:trymerge",
211+
"//3rdparty/jvm/org/scalacheck",
212+
"//3rdparty/jvm/org/scalatest:scalatest_propspec",
213+
"//3rdparty/jvm/org/scalatestplus:scalacheck_1_17",
214+
"//3rdparty/jvm/org/typelevel:cats_core",
215+
]
216+
)
217+
206218
test_suite(name = "all_tests")

test/scala/com/github/johnynek/bazel_deps/GradleResolverTest.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import io.circe.jawn.JawnParser
44
import java.nio.file.Paths
55
import org.scalatest.funsuite.AnyFunSuite
66
import scala.util.{Success, Failure}
7+
import scala.collection.immutable.SortedMap
78

89
import Decoders._
910

@@ -18,8 +19,8 @@ class GradleResolverTest extends AnyFunSuite {
1819
case Left(error) => sys.error(s"Failed to decode $str: ${error}")
1920
}
2021

21-
def deps(str: String): Map[String, GradleLockDependency] =
22-
(new JawnParser).decode[Map[String, GradleLockDependency]](str) match {
22+
def deps(str: String): SortedMap[String, GradleLockDependency] =
23+
(new JawnParser).decode[SortedMap[String, GradleLockDependency]](str) match {
2324
case Right(t) => t
2425
case Left(error) => sys.error(s"Failed to decode $str: ${error}")
2526
}

test/scala/com/github/johnynek/bazel_deps/GraphPropTest.scala

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package com.github.johnynek.bazel_deps
22

33
import org.scalatest.propspec.AnyPropSpec
4-
import org.scalacheck.Prop.forAll
4+
import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks
55
import org.scalacheck.{Gen, Arbitrary}
66

7-
class GraphPropTest extends AnyPropSpec {
7+
class GraphPropTest extends AnyPropSpec with ScalaCheckPropertyChecks {
88

99
def graphGen[N, E](g: Gen[(N, Option[(N, E)])]): Gen[Graph[N, E]] =
1010
Gen.sized { s =>
@@ -89,21 +89,22 @@ class GraphPropTest extends AnyPropSpec {
8989
property("Sanity checks on generated graphs (non-dag) 4") {
9090
forAll(graphGen(genIntNode), nodeGen) { (g, n) =>
9191
val newG = g.removeNode(n)
92-
(!newG.nodes(n)) && (!newG.edgeIterator.exists { case Edge(s, d, _) =>
92+
assert((!newG.nodes(n)) && (!newG.edgeIterator.exists { case Edge(s, d, _) =>
9393
(s == n) || (d == n)
94-
})
94+
}))
9595
}
9696
}
9797

9898
property("Sanity checks on generated graphs (non-dag) 5") {
9999
forAll(graphGen(genIntNode), nodeGen, nodeGen) { (g, s, d) =>
100100
val newEdge = Edge(s, d, ())
101101
val newG = g.addEdge(newEdge)
102-
newG.nodes(s) &&
103-
newG.nodes(d) &&
104-
newG.hasSource(s)(newEdge) &&
105-
newG.hasDestination(d)(newEdge) &&
106-
newG.edgeIterator.toSet(newEdge)
102+
103+
assert(newG.nodes(s) &&
104+
newG.nodes(d) &&
105+
newG.hasSource(s)(newEdge) &&
106+
newG.hasDestination(d)(newEdge) &&
107+
newG.edgeIterator.toSet(newEdge))
107108
}
108109
}
109110

@@ -114,8 +115,8 @@ class GraphPropTest extends AnyPropSpec {
114115
}) {
115116
case (g, Some(e)) =>
116117
val newG = g.removeEdge(e)
117-
!(newG.edgeIterator.exists(_ == e))
118-
case (g, None) => true
118+
assert(!(newG.edgeIterator.exists(_ == e)))
119+
case (g, None) => ()
119120
}
120121
}
121122

@@ -126,9 +127,12 @@ class GraphPropTest extends AnyPropSpec {
126127
}) {
127128
case (g, Some(n)) =>
128129
val newG = g.removeNode(n)
129-
newG.hasDestination(n).isEmpty &&
130-
newG.hasSource(n).isEmpty &&
131-
(!newG.nodes(n))
130+
131+
assert(
132+
newG.hasDestination(n).isEmpty &&
133+
newG.hasSource(n).isEmpty &&
134+
(!newG.nodes(n))
135+
)
132136
case (g, None) => true
133137
}
134138
}
@@ -142,7 +146,7 @@ class GraphPropTest extends AnyPropSpec {
142146
} yield (g, optPair)
143147

144148
forAll(genEnds) {
145-
case (g, Some((s, e))) => g.reflexiveTransitiveClosure(List(s))(e)
149+
case (g, Some((s, e))) => assert(g.reflexiveTransitiveClosure(List(s))(e))
146150
case (g, None) => true
147151
}
148152
}
@@ -152,6 +156,7 @@ class GraphPropTest extends AnyPropSpec {
152156
g <- graphGen(genIntNode)
153157
e <- edgeFrom(g)
154158
} yield (g, e)
159+
155160
forAll(gen, nodeGen, nodeGen) { case ((g, e), n1, n2) =>
156161
val maybePresent = Edge(n1, n2, ())
157162
val maybeCheck =
@@ -164,9 +169,9 @@ class GraphPropTest extends AnyPropSpec {
164169
g.hasDestination(n2).contains(maybePresent))
165170
}
166171

167-
g.edgeIterator.forall(g.hasEdge) &&
172+
assert(g.edgeIterator.forall(g.hasEdge) &&
168173
e.forall(g.hasEdge(_)) &&
169-
maybeCheck
174+
maybeCheck)
170175
}
171176
}
172-
}
177+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package com.github.johnynek.bazel_deps
2+
3+
import org.scalatest.propspec.AnyPropSpec
4+
import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks
5+
import org.scalacheck.{Prop, Gen, Arbitrary}
6+
import cats.{Semigroup, Eq}
7+
import scala.util.{Success, Failure}
8+
import scala.collection.immutable.SortedMap
9+
10+
import TryMerge.tryMerge
11+
import java.util.concurrent.Semaphore
12+
13+
class TryMergeTest extends AnyPropSpec with ScalaCheckPropertyChecks {
14+
def associative[A: TryMerge: Eq](a: A, b: A, c: A) = {
15+
val right = tryMerge(None, b, c).flatMap(tryMerge(None, a, _))
16+
val left = tryMerge(None, a, b).flatMap(tryMerge(None, _, c))
17+
18+
(left, right) match {
19+
case (Success(l), Success(r)) => assert(Eq.eqv(l, r))
20+
case (Failure(_), Failure(_)) => ()
21+
case mismatch => fail(s"associative mismatch: $a, $b, $c => $mismatch")
22+
}
23+
}
24+
25+
implicit val tryMergeI: TryMerge[Int] =
26+
new TryMerge[Int] {
27+
def tryMerge(debug: Option[String], a: Int, b: Int) =
28+
// take the high 16 bits of a and low 16 bits of b
29+
// this is associative but not commutative
30+
Success((a & 0xFFFF0000) | (b & 0x0000FFFF))
31+
}
32+
33+
def revTry[A: TryMerge]: TryMerge[A] = implicitly[TryMerge[A]].reverse
34+
35+
property("try merge is associative Int") {
36+
forAll { (a: Int, b: Int, c: Int) =>
37+
associative(a, b, c)
38+
associative(a, b, c)(revTry, implicitly)
39+
}
40+
}
41+
42+
43+
property("try merge is associative Option[Int]") {
44+
forAll { (a: Option[Int], b: Option[Int], c: Option[Int]) =>
45+
associative(a, b, c)
46+
associative(a, b, c)(revTry, implicitly)
47+
}
48+
}
49+
50+
property("try merge is associative SortedMap[String, Int]") {
51+
forAll { (a: SortedMap[String, Int], b: SortedMap[String, Int], c: SortedMap[String, Int]) =>
52+
associative(a, b, c)
53+
associative(a, b, c)(revTry, implicitly)
54+
}
55+
}
56+
57+
property("try merge with taking this rhs is the same as ++ on SortedMap") {
58+
forAll { (a: SortedMap[String, Int], b: SortedMap[String, Int]) =>
59+
implicit val rhs = new TryMerge[Int] {
60+
def tryMerge(d: Option[String], a: Int, b: Int) = Success(b)
61+
}
62+
assert(tryMerge(None, a, b) == Success(a ++ b))
63+
}
64+
}
65+
66+
property("if we always return it's the same as a Semigroup") {
67+
forAll { (a: SortedMap[String, Int], b: SortedMap[String, Int]) =>
68+
implicit val sgBits = new Semigroup[Int] {
69+
def combine(a: Int, b: Int) = (a & 0xFFFF0000) | (b & 0x0000FFFF)
70+
}
71+
val sm = new Semigroup[SortedMap[String, Int]] {
72+
def combine(a: SortedMap[String, Int], b: SortedMap[String, Int]) = {
73+
val allKeys = a.keySet | b.keySet
74+
SortedMap(allKeys.iterator.map { k =>
75+
(a.get(k), b.get(k)) match {
76+
case (Some(l), Some(r)) => (k, sgBits.combine(l, r))
77+
case (Some(l), None) => (k, l)
78+
case (None, Some(r)) => (k, r)
79+
case (None, None) => sys.error(s"unreachable: non-sense key: $k")
80+
}
81+
}.toList: _*)
82+
}
83+
}
84+
assert(tryMerge(None, a, b) == Success(sm.combine(a, b)))
85+
}
86+
}
87+
}

0 commit comments

Comments
 (0)