From da6af7c5f7b3d785e105e0154f06ee0fb34436b9 Mon Sep 17 00:00:00 2001 From: Benjy Date: Sun, 13 Oct 2013 22:27:10 -0700 Subject: [PATCH] Test for Analysis split/merge. Requires scalacheck generators for Analysis and its subobjects. These may be useful for other tests in the future. Also fixes a bug in RelationTest. --- .../src/test/scala/sbt/inc/AnalysisTest.scala | 88 +++++++++++++ .../scala/sbt/inc/TestCaseGenerators.scala | 124 ++++++++++++++++++ .../src/test/scala/RelationTest.scala | 2 +- 3 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 compile/inc/src/test/scala/sbt/inc/AnalysisTest.scala create mode 100644 compile/inc/src/test/scala/sbt/inc/TestCaseGenerators.scala diff --git a/compile/inc/src/test/scala/sbt/inc/AnalysisTest.scala b/compile/inc/src/test/scala/sbt/inc/AnalysisTest.scala new file mode 100644 index 000000000..def2caeaf --- /dev/null +++ b/compile/inc/src/test/scala/sbt/inc/AnalysisTest.scala @@ -0,0 +1,88 @@ +package sbt +package inc + +import java.io.File +import scala.math.abs +import sbt.inc.TestCaseGenerators._ +import org.scalacheck._ +import Gen._ +import Prop._ + + +object AnalysisTest extends Properties("Analysis") { + // Merge and split a hard-coded trivial example. + property("Simple Merge and Split") = { + def f(s: String) = new File(s) + val aScala = f("A.scala") + val bScala = f("B.scala") + val aSource = genSource("A" :: "A$" :: Nil).sample.get + val bSource = genSource("B" :: "B$" :: Nil).sample.get + val cSource = genSource("C" :: Nil).sample.get + val exists = new Exists(true) + val sourceInfos = SourceInfos.makeInfo(Nil, Nil) + + // a + var a = Analysis.Empty + a = a.addProduct(aScala, f("A.class"), exists, "A") + a = a.addProduct(aScala, f("A$.class"), exists, "A$") + a = a.addSource(aScala, aSource, exists, Nil, Nil, sourceInfos) + a = a.addBinaryDep(aScala, f("x.jar"), "x", exists) + a = a.addExternalDep(aScala, "C", cSource, inherited=false) + + // b + var b = Analysis.Empty + b = b.addProduct(bScala, f("B.class"), exists, "B") + b = b.addProduct(bScala, f("B$.class"), exists, "B$") + b = b.addSource(bScala, bSource, exists, Nil, Nil, sourceInfos) + b = b.addBinaryDep(bScala, f("x.jar"), "x", exists) + b = b.addBinaryDep(bScala, f("y.jar"), "y", exists) + b = b.addExternalDep(bScala, "A", aSource, inherited=true) + + // ab + var ab = Analysis.Empty + ab = ab.addProduct(aScala, f("A.class"), exists, "A") + ab = ab.addProduct(aScala, f("A$.class"), exists, "A$") + ab = ab.addProduct(bScala, f("B.class"), exists, "B") + ab = ab.addProduct(bScala, f("B$.class"), exists, "B$") + ab = ab.addSource(aScala, aSource, exists, Nil, Nil, sourceInfos) + ab = ab.addSource(bScala, bSource, exists, aScala :: Nil, aScala :: Nil, sourceInfos) + ab = ab.addBinaryDep(aScala, f("x.jar"), "x", exists) + ab = ab.addBinaryDep(bScala, f("x.jar"), "x", exists) + ab = ab.addBinaryDep(bScala, f("y.jar"), "y", exists) + ab = ab.addExternalDep(aScala, "C", cSource, inherited=false) + + val split: Map[String, Analysis] = ab.groupBy({ f: File => f.getName.substring(0, 1) }) + + val aSplit = split.getOrElse("A", Analysis.Empty) + val bSplit = split.getOrElse("B", Analysis.Empty) + + val merged = Analysis.merge(a :: b :: Nil) + + ("split(AB)(A) == A" |: compare(a, aSplit)) && + ("split(AB)(B) == B" |: compare(b, bSplit)) && + ("merge(A, B) == AB" |: compare(merged, ab)) + } + + // Merge and split large, generated examples. + // Mustn't shrink, as the default Shrink[Int] doesn't respect the lower bound of choose(), which will cause + // a divide-by-zero error masking the original error. + property("Complex Merge and Split") = forAllNoShrink(genAnalysis, choose(1, 10)) { (analysis: Analysis, numSplits: Int) => + val grouped: Map[Int, Analysis] = analysis.groupBy({ f: File => abs(f.hashCode()) % numSplits}) + def getGroup(i: Int): Analysis = grouped.getOrElse(i, Analysis.Empty) + val splits = (Range(0, numSplits) map getGroup).toList + + val merged: Analysis = Analysis.merge(splits) + "Merge all" |: compare(analysis, merged) + } + + // Compare two analyses with useful labelling when they aren't equal. + private[this] def compare(left: Analysis, right: Analysis) = { + val res = left == right + ("UNEQUAL" |: res) || + ((" LEFT: " + left) |: false) || + (("RIGHT: " + right) |: false) || + (("STAMPS EQUAL: " + (left.stamps == right.stamps)) |: false) || + (("APIS EQUAL: " + (left.apis == right.apis)) |: false) || + (("RELATIONS EQUAL: " + (left.relations == right.relations)) |: false) + } +} diff --git a/compile/inc/src/test/scala/sbt/inc/TestCaseGenerators.scala b/compile/inc/src/test/scala/sbt/inc/TestCaseGenerators.scala new file mode 100644 index 000000000..04447eaa8 --- /dev/null +++ b/compile/inc/src/test/scala/sbt/inc/TestCaseGenerators.scala @@ -0,0 +1,124 @@ +package sbt +package inc + +import java.io.File + +import org.scalacheck._ +import Arbitrary._ +import Gen._ + +import sbt.Relation +import xsbti.api._ +import xsbti.SafeLazy + + +/** + * Scalacheck generators for Analysis objects and their substructures. + * Fairly complex, as Analysis has interconnected state that can't be + * independently generated. + */ +object TestCaseGenerators { + // We restrict sizes, otherwise the generated Analysis objects get huge and the tests take a long time. + val maxSources = 10 // Max number of source files. + val maxRelatives = 10 // Max number of things that a source x can relate to in a single Relation. + val maxPathSegmentLen = 10 // Max number of characters in a path segment. + val maxPathLen = 6 // Max number of path segments in a path. + + // Ensure that we generate unique class names and file paths every time. + // Using repeated strings may lead to all sorts of undesirable interactions. + val used = scala.collection.mutable.Set.empty[String] + def unique[T](g: Gen[T]) = g suchThat { o: T => used.add(o.toString) } + + def genFilePathSegment: Gen[String] = for { + n <- choose(3, maxPathSegmentLen) // Segments have at least 3 characters. + c <- alphaChar + cs <- listOfN(n - 1, alphaNumChar) + } yield (c::cs).mkString + + def genFile: Gen[File] = for { + n <- choose(2, maxPathLen) // Paths have at least 2 segments. + path <- listOfN(n, genFilePathSegment) + } yield new File(path.mkString("/")) + + def genStamp: Gen[Stamp] = for { + b <- oneOf(true, false) + } yield new Exists(b) + + def zipMap[A, B](a: Seq[A], b: Seq[B]): Map[A, B] = (a zip b).toMap + + def genStamps(rel: Relations): Gen[Stamps] = { + val prod = rel.allProducts.toList + val src = rel.allSources.toList + val bin = rel.allBinaryDeps.toList + for { + prodStamps <- listOfN(prod.length, genStamp) + srcStamps <- listOfN(src.length, genStamp) + binStamps <- listOfN(bin.length, genStamp) + binClassNames <- listOfN(bin.length, unique(identifier)) + } yield Stamps(zipMap(prod, prodStamps), zipMap(src, srcStamps), zipMap(bin, binStamps), zipMap(bin, binClassNames)) + } + + // We need "proper" definitions with specific class names, as groupBy use these to pick a representative top-level class when splitting. + private[this] def makeDefinition(name: String): Definition = + new ClassLike(DefinitionType.ClassDef, lzy(new EmptyType()), + lzy(new Structure(lzy(Array()), lzy(Array()), lzy(Array()))), Array(), Array(), + name, new Public(), new Modifiers(false, false, false, false, false, false, false), Array()) + + private [this] def lzy[T <: AnyRef](x: T) = SafeLazy.strict(x) + + def genSource(defns: Seq[String]): Gen[Source] = for { + startTime <- arbitrary[Long] + hashLen <- choose(10, 20) // Requred by SameAPI to be > 0. + hash <- Gen.containerOfN[Array,Byte](hashLen, arbitrary[Byte]) + apiHash <- arbitrary[Int] + hasMacro <- arbitrary[Boolean] + } yield new Source(new Compilation(startTime, Array()), hash, new SourceAPI(Array(), Array(defns map makeDefinition:_*)), apiHash, hasMacro) + + def genSources(all_defns: Seq[Seq[String]]): Gen[Seq[Source]] = Gen.sequence[List, Source](all_defns.map(genSource)) + + def genAPIs(rel: Relations): Gen[APIs] = { + val internal = rel.allInternalSrcDeps.toList.sorted + val external = rel.allExternalDeps.toList.sorted + for { + internalSources <- genSources(internal map { f: File => rel.classNames(f).toList.sorted }) + externalSources <- genSources(external map { s: String => s :: Nil }) + } yield APIs(zipMap(internal, internalSources), zipMap(external, externalSources)) + } + + def genRelation[T](g: Gen[T])(srcs: List[File]): Gen[Relation[File, T]] = for { + n <- choose(1, maxRelatives) + entries <- listOfN(srcs.length, containerOfN[Set, T](n, g)) + } yield Relation.reconstruct(zipMap(srcs, entries)) + + val genFileRelation = genRelation[File](unique(genFile)) _ + val genStringRelation = genRelation[String](unique(identifier)) _ + + def genRSource(srcs: List[File]): Gen[Relations.Source] = for { + internal <- listOfN(srcs.length, someOf(srcs)) // Internal dep targets must come from list of sources. + external <- genStringRelation(srcs) + } yield Relations.makeSource( // Ensure that we don't generate a dep of some file on itself. + Relation.reconstruct((srcs zip (internal map { _.toSet } ) map {case (a, b) => (a, b - a) }).toMap), + external) + + def genSubRSource(src: Relations.Source): Gen[Relations.Source] = for { + internal <- someOf(src.internal.all.toList) + external <- someOf(src.external.all.toList) + } yield Relations.makeSource(Relation.empty ++ internal, Relation.empty ++ external) + + def genRelations: Gen[Relations] = for { + numSrcs <- choose(0, maxSources) + srcs <- listOfN(numSrcs, genFile) + srcProd <- genFileRelation(srcs) + binaryDep <- genFileRelation(srcs) + direct <- genRSource(srcs) + publicInherited <- genSubRSource(direct) + classes <- genStringRelation(srcs) + + } yield Relations.make(srcProd, binaryDep, direct, publicInherited , classes) + + def genAnalysis: Gen[Analysis] = for { + rels <- genRelations + stamps <- genStamps(rels) + apis <- genAPIs(rels) + } yield new MAnalysis(stamps, apis, rels, SourceInfos.empty, Compilations.empty) +} diff --git a/util/relation/src/test/scala/RelationTest.scala b/util/relation/src/test/scala/RelationTest.scala index de63fe893..03b728915 100644 --- a/util/relation/src/test/scala/RelationTest.scala +++ b/util/relation/src/test/scala/RelationTest.scala @@ -52,7 +52,7 @@ object RelationTest extends Properties("Relation") } property("Groups correctly") = forAll { (entries: List[(Int, Double)], randomInt: Int) => - val splitInto = randomInt % 10 + 1 // Split into 1-10 groups. + val splitInto = math.abs(randomInt) % 10 + 1 // Split into 1-10 groups. val rel = Relation.empty[Int, Double] ++ entries val grouped = rel groupBy (_._1 % splitInto) all(grouped.toSeq) {