Merge pull request #1572 from Duhemm/textanalysisformat-dependency-kinds

Don't hardcode existing relations in TextAnalysisFormat
This commit is contained in:
Grzegorz Kossakowski 2014-09-11 16:55:08 +02:00
commit 7885fd2558
6 changed files with 257 additions and 83 deletions

View File

@ -175,9 +175,67 @@ trait Relations {
* Relation between source files and _unqualified_ term and type names used in given source file.
*/
private[inc] def names: Relation[File, String]
/**
* Lists of all the pairs (header, relation) that sbt knows of.
* Used by TextAnalysisFormat to persist relations.
* This cannot be stored as a Map because the order is important.
*/
private[inc] def allRelations: List[(String, Relation[File, _])]
}
object Relations {
/**
* Represents all the relations that sbt knows of along with a way to recreate each
* of their elements from their string representation.
*/
private[inc] val existingRelations = {
val string2File: String => File = new File(_)
List(
("products", string2File),
("binary dependencies", string2File),
("direct source dependencies", string2File),
("direct external dependencies", identity[String] _),
("public inherited source dependencies", string2File),
("public inherited external dependencies", identity[String] _),
("member reference internal dependencies", string2File),
("member reference external dependencies", identity[String] _),
("inheritance internal dependencies", string2File),
("inheritance external dependencies", identity[String] _),
("class names", identity[String] _),
("used names", identity[String] _))
}
/**
* Reconstructs a Relations from a list of Relation
* The order in which the relations are read matters and is defined by `existingRelations`.
*/
def construct(nameHashing: Boolean, relations: List[Relation[_, _]]) =
relations match {
case p :: bin :: di :: de :: pii :: pie :: mri :: mre :: ii :: ie :: cn :: un :: Nil =>
val srcProd = p.asInstanceOf[Relation[File, File]]
val binaryDep = bin.asInstanceOf[Relation[File, File]]
val directSrcDeps = makeSource(di.asInstanceOf[Relation[File, File]], de.asInstanceOf[Relation[File, String]])
val publicInheritedSrcDeps = makeSource(pii.asInstanceOf[Relation[File, File]], pie.asInstanceOf[Relation[File, String]])
val memberRefSrcDeps = makeSourceDependencies(mri.asInstanceOf[Relation[File, File]], mre.asInstanceOf[Relation[File, String]])
val inheritanceSrcDeps = makeSourceDependencies(ii.asInstanceOf[Relation[File, File]], ie.asInstanceOf[Relation[File, String]])
val classes = cn.asInstanceOf[Relation[File, String]]
val names = un.asInstanceOf[Relation[File, String]]
// we don't check for emptiness of publicInherited/inheritance relations because
// we assume that invariant that says they are subsets of direct/memberRef holds
assert(nameHashing || (memberRefSrcDeps == emptySourceDependencies), "When name hashing is disabled the `memberRef` relation should be empty.")
assert(!nameHashing || (directSrcDeps == emptySource), "When name hashing is enabled the `direct` relation should be empty.")
if (nameHashing)
Relations.make(srcProd, binaryDep, memberRefSrcDeps, inheritanceSrcDeps, classes, names)
else {
assert(names.all.isEmpty, s"When `nameHashing` is disabled `names` relation should be empty: $names")
Relations.make(srcProd, binaryDep, directSrcDeps, publicInheritedSrcDeps, classes)
}
case _ => throw new java.io.IOException(s"Expected to read ${existingRelations.length} relations but read ${relations.length}.")
}
/** Tracks internal and external source dependencies for a specific dependency type, such as direct or inherited.*/
final class Source private[sbt] (val internal: Relation[File, File], val external: Relation[File, String]) {
def addInternal(source: File, dependsOn: Iterable[File]): Source = new Source(internal + (source, dependsOn), external)
@ -403,6 +461,23 @@ private class MRelationsDefaultImpl(srcProd: Relation[File, File], binaryDep: Re
case _ => false
}
def allRelations = {
val rels = List(
srcProd,
binaryDep,
direct.internal,
direct.external,
publicInherited.internal,
publicInherited.external,
Relations.emptySourceDependencies.internal, // Default implementation doesn't provide memberRef source deps
Relations.emptySourceDependencies.external, // Default implementation doesn't provide memberRef source deps
Relations.emptySourceDependencies.internal, // Default implementation doesn't provide inheritance source deps
Relations.emptySourceDependencies.external, // Default implementation doesn't provide inheritance source deps
classes,
Relation.empty[File, String]) // Default implementation doesn't provide used names relation
Relations.existingRelations map (_._1) zip rels
}
override def hashCode = (srcProd :: binaryDep :: direct :: publicInherited :: classes :: Nil).hashCode
override def toString = (
@ -490,6 +565,23 @@ private class MRelationsNameHashing(srcProd: Relation[File, File], binaryDep: Re
case _ => false
}
def allRelations = {
val rels = List(
srcProd,
binaryDep,
Relations.emptySource.internal, // NameHashing doesn't provide direct dependencies
Relations.emptySource.external, // NameHashing doesn't provide direct dependencies
Relations.emptySource.internal, // NameHashing doesn't provide public inherited dependencies
Relations.emptySource.external, // NameHashing doesn't provide public inherited dependencies
memberRef.internal,
memberRef.external,
inheritance.internal,
inheritance.external,
classes,
names)
Relations.existingRelations map (_._1) zip rels
}
override def hashCode = (srcProd :: binaryDep :: memberRef :: inheritance :: classes :: Nil).hashCode
override def toString = (

View File

@ -65,7 +65,9 @@ object AnalysisTest extends Properties("Analysis") {
// 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) =>
// Note that the generated Analyses have nameHashing = false (Grouping of Analyses with name hashing enabled
// is not supported right now)
property("Complex Merge and Split") = forAllNoShrink(genAnalysis(nameHashing = false), 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(false))
val splits = (Range(0, numSplits) map getGroup).toList

View File

@ -25,9 +25,18 @@ object TestCaseGenerators {
// 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]
val used1 = scala.collection.mutable.Set.empty[String]
val used2 = scala.collection.mutable.Set.empty[String]
def unique[T](g: Gen[T]) = g retryUntil { o: T => used.add(o.toString) }
// When using `retryUntil`, the condition is actually tested twice (see implementation in ScalaCheck),
// which is why we need to insert twice the element.
// If the element is present in both sets, then it has already been used.
def unique[T](g: Gen[T]) = g retryUntil { o: T =>
if (used1.add(o.toString))
true
else
used2.add(o.toString)
}
def identifier: Gen[String] = sized { size =>
resize(Math.max(size, 3), Gen.identifier)
@ -134,6 +143,18 @@ object TestCaseGenerators {
external <- someOf(src.external.all.toList)
} yield Relations.makeSource(Relation.empty ++ internal, Relation.empty ++ external)
def genRSourceDependencies(srcs: List[File]): Gen[Relations.SourceDependencies] = for {
internal <- listOfN(srcs.length, someOf(srcs))
external <- genStringRelation(srcs)
} yield Relations.makeSourceDependencies(
Relation.reconstruct((srcs zip (internal map { _.toSet }) map { case (a, b) => (a, b - a) }).toMap),
external)
def genSubRSourceDependencies(src: Relations.SourceDependencies): Gen[Relations.SourceDependencies] = for {
internal <- someOf(src.internal.all.toList)
external <- someOf(src.external.all.toList)
} yield Relations.makeSourceDependencies(Relation.empty ++ internal, Relation.empty ++ external)
def genRelations: Gen[Relations] = for {
numSrcs <- choose(0, maxSources)
srcs <- listOfN(numSrcs, genFile)
@ -145,8 +166,19 @@ object TestCaseGenerators {
} yield Relations.make(srcProd, binaryDep, direct, publicInherited, classes)
def genAnalysis: Gen[Analysis] = for {
rels <- genRelations
def genRelationsNameHashing: Gen[Relations] = for {
numSrcs <- choose(0, maxSources)
srcs <- listOfN(numSrcs, genFile)
srcProd <- genFileRelation(srcs)
binaryDep <- genFileRelation(srcs)
memberRef <- genRSourceDependencies(srcs)
inheritance <- genSubRSourceDependencies(memberRef)
classes <- genStringRelation(srcs)
names <- genStringRelation(srcs)
} yield Relations.make(srcProd, binaryDep, memberRef, inheritance, classes, names)
def genAnalysis(nameHashing: Boolean): Gen[Analysis] = for {
rels <- if (nameHashing) genRelationsNameHashing else genRelations
stamps <- genStamps(rels)
apis <- genAPIs(rels)
} yield new MAnalysis(stamps, apis, rels, SourceInfos.empty, Compilations.empty)

View File

@ -122,7 +122,13 @@ object TextAnalysisFormat {
}
def write(out: Writer, relations: Relations) {
def writeRelation[T](header: String, rel: Relation[File, T])(implicit ord: Ordering[T]) {
// This ordering is used to persist all values in order. Since all values will be
// persisted using their string representation, it makes sense to sort them using
// their string representation.
val toStringOrd = new Ordering[Any] {
def compare(a: Any, b: Any) = a.toString compare b.toString
}
def writeRelation[T](header: String, rel: Relation[File, T]) {
writeHeader(out, header)
writeSize(out, rel.size)
// We sort for ease of debugging and for more efficient reconstruction when reading.
@ -131,38 +137,15 @@ object TextAnalysisFormat {
rel.forwardMap.toSeq.sortBy(_._1).foreach {
case (k, vs) =>
val kStr = k.toString
vs.toSeq.sorted foreach { v =>
vs.toSeq.sorted(toStringOrd) foreach { v =>
out.write(kStr); out.write(" -> "); out.write(v.toString); out.write("\n")
}
}
}
val nameHashing = relations.nameHashing
writeRelation(Headers.srcProd, relations.srcProd)
writeRelation(Headers.binaryDep, relations.binaryDep)
val direct = if (nameHashing) Relations.emptySource else relations.direct
val publicInherited = if (nameHashing)
Relations.emptySource else relations.publicInherited
val memberRef = if (nameHashing)
relations.memberRef else Relations.emptySourceDependencies
val inheritance = if (nameHashing)
relations.inheritance else Relations.emptySourceDependencies
val names = if (nameHashing) relations.names else Relation.empty[File, String]
writeRelation(Headers.directSrcDep, direct.internal)
writeRelation(Headers.directExternalDep, direct.external)
writeRelation(Headers.internalSrcDepPI, publicInherited.internal)
writeRelation(Headers.externalDepPI, publicInherited.external)
writeRelation(Headers.memberRefInternalDep, memberRef.internal)
writeRelation(Headers.memberRefExternalDep, memberRef.external)
writeRelation(Headers.inheritanceInternalDep, inheritance.internal)
writeRelation(Headers.inheritanceExternalDep, inheritance.external)
writeRelation(Headers.classes, relations.classes)
writeRelation(Headers.usedNames, names)
relations.allRelations.foreach {
case (header, rel) => writeRelation(header, rel)
}
}
def read(in: BufferedReader, nameHashing: Boolean): Relations = {
@ -186,56 +169,9 @@ object TextAnalysisFormat {
Relation.reconstruct(forward.toMap)
}
def readFileRelation(expectedHeader: String) = readRelation(expectedHeader, { new File(_) })
def readStringRelation(expectedHeader: String) = readRelation(expectedHeader, identity[String])
val relations = Relations.existingRelations map { case (header, fun) => readRelation(header, fun) }
val srcProd = readFileRelation(Headers.srcProd)
val binaryDep = readFileRelation(Headers.binaryDep)
import sbt.inc.Relations.{
Source,
SourceDependencies,
makeSourceDependencies,
emptySource,
makeSource,
emptySourceDependencies
}
val directSrcDeps: Source = {
val internalSrcDep = readFileRelation(Headers.directSrcDep)
val externalDep = readStringRelation(Headers.directExternalDep)
makeSource(internalSrcDep, externalDep)
}
val publicInheritedSrcDeps: Source = {
val internalSrcDepPI = readFileRelation(Headers.internalSrcDepPI)
val externalDepPI = readStringRelation(Headers.externalDepPI)
makeSource(internalSrcDepPI, externalDepPI)
}
val memberRefSrcDeps: SourceDependencies = {
val internalMemberRefDep = readFileRelation(Headers.memberRefInternalDep)
val externalMemberRefDep = readStringRelation(Headers.memberRefExternalDep)
makeSourceDependencies(internalMemberRefDep, externalMemberRefDep)
}
val inheritanceSrcDeps: SourceDependencies = {
val internalInheritanceDep = readFileRelation(Headers.inheritanceInternalDep)
val externalInheritanceDep = readStringRelation(Headers.inheritanceExternalDep)
makeSourceDependencies(internalInheritanceDep, externalInheritanceDep)
}
// we don't check for emptiness of publicInherited/inheritance relations because
// we assume that invariant that says they are subsets of direct/memberRef holds
assert(nameHashing || (memberRefSrcDeps == emptySourceDependencies),
"When name hashing is disabled the `memberRef` relation should be empty.")
assert(!nameHashing || (directSrcDeps == emptySource),
"When name hashing is enabled the `direct` relation should be empty.")
val classes = readStringRelation(Headers.classes)
val names = readStringRelation(Headers.usedNames)
if (nameHashing)
Relations.make(srcProd, binaryDep, memberRefSrcDeps, inheritanceSrcDeps, classes, names)
else {
assert(names.all.isEmpty, "When `nameHashing` is disabled `names` relation " +
s"should be empty: $names")
Relations.make(srcProd, binaryDep, directSrcDeps, publicInheritedSrcDeps, classes)
}
Relations.construct(nameHashing, relations)
}
}

View File

@ -0,0 +1,112 @@
package sbt
package inc
import java.io.{ BufferedReader, File, StringReader, StringWriter }
import scala.math.abs
import org.scalacheck._
import Gen._
import Prop._
object TextAnalysisFormatTest extends Properties("TextAnalysisFormat") {
val nameHashing = true
val dummyOutput = new xsbti.compile.SingleOutput { def outputDirectory: java.io.File = new java.io.File("dummy") }
val commonSetup = new CompileSetup(dummyOutput, new CompileOptions(Nil, Nil), "2.10.4", xsbti.compile.CompileOrder.Mixed, nameHashing)
val commonHeader = """format version: 5
|output mode:
|1 items
|0 -> single
|output directories:
|1 items
|output dir -> dummy
|compile options:
|0 items
|javac options:
|0 items
|compiler version:
|1 items
|0 -> 2.10.4
|compile order:
|1 items
|0 -> Mixed
|name hashing:
|1 items
|0 -> true""".stripMargin
property("Write and read empty Analysis") = {
val writer = new StringWriter
val analysis = Analysis.empty(nameHashing)
TextAnalysisFormat.write(writer, analysis, commonSetup)
val result = writer.toString
result.startsWith(commonHeader)
val reader = new BufferedReader(new StringReader(result))
val (readAnalysis, readSetup) = TextAnalysisFormat.read(reader)
analysis == readAnalysis
}
property("Write and read simple Analysis") = {
import TestCaseGenerators._
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)
var analysis = Analysis.empty(nameHashing)
analysis = analysis.addProduct(aScala, f("A.class"), exists, "A")
analysis = analysis.addProduct(aScala, f("A$.class"), exists, "A$")
analysis = analysis.addSource(aScala, aSource, exists, Nil, Nil, sourceInfos)
analysis = analysis.addBinaryDep(aScala, f("x.jar"), "x", exists)
analysis = analysis.addExternalDep(aScala, "C", cSource, inherited = false)
val writer = new StringWriter
TextAnalysisFormat.write(writer, analysis, commonSetup)
val result = writer.toString
result.startsWith(commonHeader)
val reader = new BufferedReader(new StringReader(result))
val (readAnalysis, readSetup) = TextAnalysisFormat.read(reader)
compare(analysis, readAnalysis)
}
property("Write and read complex Analysis") = forAllNoShrink(TestCaseGenerators.genAnalysis(nameHashing)) { analysis: Analysis =>
val writer = new StringWriter
TextAnalysisFormat.write(writer, analysis, commonSetup)
val result = writer.toString
result.startsWith(commonHeader)
val reader = new BufferedReader(new StringReader(result))
val (readAnalysis, readSetup) = TextAnalysisFormat.read(reader)
compare(analysis, readAnalysis)
}
// Compare two analyses with useful labelling when they aren't equal.
private[this] def compare(left: Analysis, right: Analysis): Prop =
s" LEFT: $left" |:
s"RIGHT: $right" |:
s"STAMPS EQUAL: ${left.stamps == right.stamps}" |:
s"APIS EQUAL: ${left.apis == right.apis}" |:
s"RELATIONS EQUAL: ${left.relations == right.relations}" |:
"UNEQUAL" |:
(left == right)
}

View File

@ -154,7 +154,7 @@ object Sbt extends Build {
// Defines the data structures for representing file fingerprints and relationships and the overall source analysis
lazy val compileIncrementalSub = testedBaseProject(compilePath / "inc", "Incremental Compiler") dependsOn (apiSub, ioSub, logSub, classpathSub, relationSub)
// Persists the incremental data structures using SBinary
lazy val compilePersistSub = baseProject(compilePath / "persist", "Persist") dependsOn (compileIncrementalSub, apiSub) settings (sbinary)
lazy val compilePersistSub = testedBaseProject(compilePath / "persist", "Persist") dependsOn (compileIncrementalSub, apiSub, compileIncrementalSub % "test->test") settings (sbinary)
// sbt-side interface to compiler. Calls compiler-side interface reflectively
lazy val compilerSub = testedBaseProject(compilePath, "Compile") dependsOn (launchInterfaceSub, interfaceSub % "compile;test->test", logSub, ioSub, classpathSub,
logSub % "test->test", launchSub % "test->test", apiSub % "test") settings (compilerSettings: _*)