"Fossies" - the Fresh Open Source Software Archive

Member "scala-js-1.0.0-RC1/linker/shared/src/test/scala/org/scalajs/linker/AnalyzerTest.scala" (21 Nov 2019, 20808 Bytes) of package /linux/www/scala-js-1.0.0-RC1.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Scala source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file.

    1 /*
    2  * Scala.js (https://www.scala-js.org/)
    3  *
    4  * Copyright EPFL.
    5  *
    6  * Licensed under Apache License 2.0
    7  * (https://www.apache.org/licenses/LICENSE-2.0).
    8  *
    9  * See the NOTICE file distributed with this work for
   10  * additional information regarding copyright ownership.
   11  */
   12 
   13 package org.scalajs.linker
   14 
   15 import scala.concurrent._
   16 
   17 import org.junit.Test
   18 import org.junit.Assert._
   19 
   20 import org.scalajs.ir.ClassKind
   21 import org.scalajs.ir.EntryPointsInfo
   22 import org.scalajs.ir.Names._
   23 import org.scalajs.ir.Trees._
   24 import org.scalajs.ir.Types._
   25 
   26 import org.scalajs.junit.async._
   27 
   28 import org.scalajs.logging.NullLogger
   29 import org.scalajs.linker._
   30 import org.scalajs.linker.analyzer._
   31 import org.scalajs.linker.standard._
   32 
   33 import Analysis._
   34 
   35 import org.scalajs.linker.testutils._
   36 import org.scalajs.linker.testutils.TestIRBuilder._
   37 
   38 class AnalyzerTest {
   39   import scala.concurrent.ExecutionContext.Implicits.global
   40   import AnalyzerTest._
   41 
   42   @Test
   43   def trivialOK(): AsyncResult = await {
   44     val analysis = computeAnalysis(Nil)
   45     assertNoError(analysis)
   46   }
   47 
   48   @Test
   49   def missingJavaLangObject(): AsyncResult = await {
   50     val analysis = computeAnalysis(Nil, stdlib = None)
   51     assertExactErrors(analysis, MissingJavaLangObjectClass(fromAnalyzer))
   52   }
   53 
   54   @Test
   55   def invalidJavaLangObject(): AsyncResult = await {
   56     val invalidJLObjectDefs = Seq(
   57         // j.l.Object cannot have a super class
   58         classDef(ObjectClass, superClass = Some("Parent")),
   59         // j.l.Object must have kind == ClassKind.Class
   60         classDef(ObjectClass, kind = ClassKind.ModuleClass),
   61         // j.l.Object cannot extend any interface
   62         classDef(ObjectClass, interfaces = List("Parent"))
   63     )
   64 
   65     Future.traverse(invalidJLObjectDefs) { jlObjectDef =>
   66       val analysis = computeAnalysis(Seq(jlObjectDef), stdlib = None)
   67       assertExactErrors(analysis,
   68           InvalidJavaLangObjectClass(fromAnalyzer))
   69     }
   70   }
   71 
   72   @Test
   73   def cycleInInheritanceChainThroughParentClasses(): AsyncResult = await {
   74     val classDefs = Seq(
   75         classDef("A", superClass = Some("B")),
   76         classDef("B", superClass = Some("A"))
   77     )
   78 
   79     val analysis = computeAnalysis(classDefs, reqsFactory.classData("A"))
   80 
   81     assertContainsError("CycleInInheritanceChain(A, B)", analysis) {
   82       case CycleInInheritanceChain(List(AClass, BClass), `fromAnalyzer`) => true
   83     }
   84   }
   85 
   86   @Test
   87   def cycleInInheritanceChainThroughInterfaces(): AsyncResult = await {
   88     val classDefs = Seq(
   89         classDef("A", superClass = Some("B")),
   90         classDef("B", superClass = Some(ObjectClass), interfaces = List("A"))
   91     )
   92 
   93     val analysis = computeAnalysis(classDefs, reqsFactory.classData("A"))
   94 
   95     assertContainsError("CycleInInheritanceChain(A, B)", analysis) {
   96       case CycleInInheritanceChain(List(AClass, BClass), `fromAnalyzer`) => true
   97     }
   98   }
   99 
  100   @Test
  101   def bigCycleInInheritanceChain(): AsyncResult = await {
  102     val classDefs = Seq(
  103         classDef("A", superClass = Some("B")),
  104         classDef("B", superClass = Some("C")),
  105 
  106         // Start of cycle.
  107         classDef("C", superClass = Some("D")),
  108         classDef("D", superClass = Some("E")),
  109         classDef("E", superClass = Some("C"))
  110     )
  111 
  112     val analysis = computeAnalysis(classDefs, reqsFactory.classData("A"))
  113 
  114     assertContainsError("CycleInInheritanceChain(B, C, D)", analysis) {
  115       case CycleInInheritanceChain(List(CClass, DClass, EClass), `fromAnalyzer`) => true
  116     }
  117   }
  118 
  119   @Test
  120   def missingClassDirect(): AsyncResult = await {
  121     val analysis = computeAnalysis(Nil, reqsFactory.classData("A"))
  122 
  123     assertContainsError("MissingClass(A)", analysis) {
  124       case MissingClass(ClsInfo("A"), `fromUnitTest`) => true
  125     }
  126   }
  127 
  128   @Test
  129   def missingClassParent(): AsyncResult = await {
  130     val classDefs = Seq(
  131         classDef("A", superClass = Some("B"))
  132     )
  133 
  134     val analysis = computeAnalysis(classDefs, reqsFactory.classData("A"))
  135 
  136     assertContainsError("MissingClass(B)", analysis) {
  137       case MissingClass(ClsInfo("B"), FromClass(ClsInfo("A"))) => true
  138     }
  139   }
  140 
  141   @Test
  142   def missingSuperClass(): AsyncResult = await {
  143     val kinds = Seq(
  144         ClassKind.Class,
  145         ClassKind.ModuleClass,
  146         ClassKind.HijackedClass,
  147         ClassKind.JSClass,
  148         ClassKind.JSModuleClass,
  149         ClassKind.NativeJSClass,
  150         ClassKind.NativeJSModuleClass
  151     )
  152 
  153     Future.traverse(kinds) { kind =>
  154       val classDefs = Seq(
  155           classDef("A", kind = kind, memberDefs = List(trivialCtor("A")))
  156       )
  157 
  158       val analysis = computeAnalysis(classDefs,
  159           reqsFactory.instantiateClass("A", NoArgConstructorName))
  160 
  161       assertContainsError("MissingSuperClass(A)", analysis) {
  162         case MissingSuperClass(ClsInfo("A"), FromClass(ClsInfo("A"))) => true
  163       }
  164     }
  165   }
  166 
  167   @Test
  168   def invalidSuperClass(): AsyncResult = await {
  169     val kindsSub = Seq(
  170         ClassKind.Class,
  171         ClassKind.ModuleClass,
  172         ClassKind.HijackedClass,
  173         ClassKind.Interface,
  174         ClassKind.JSClass,
  175         ClassKind.JSModuleClass,
  176         ClassKind.NativeJSClass,
  177         ClassKind.NativeJSModuleClass,
  178         ClassKind.AbstractJSType
  179     )
  180 
  181     def kindsBaseFor(kindSub: ClassKind): Seq[ClassKind] = {
  182       import ClassKind._
  183       kindSub match {
  184         case Class | ModuleClass | HijackedClass =>
  185           Seq(Interface, ModuleClass, JSClass, NativeJSClass)
  186         case Interface =>
  187           Seq(Class, Interface)
  188         case JSClass | JSModuleClass | NativeJSClass | NativeJSModuleClass |
  189             AbstractJSType =>
  190           Seq(Class, Interface, AbstractJSType, JSModuleClass)
  191       }
  192     }
  193 
  194     Future.traverse(kindsSub) { kindSub =>
  195       Future.traverse(kindsBaseFor(kindSub)) { kindBase =>
  196 
  197         val classDefs = Seq(
  198             classDef("A", kind = kindSub, superClass = Some("B")),
  199             classDef("B", kind = kindBase,
  200                 superClass = validParentForKind(kindBase))
  201         )
  202 
  203         val analysis = computeAnalysis(classDefs,
  204             reqsFactory.instantiateClass("A", NoArgConstructorName))
  205 
  206         assertContainsError("InvalidSuperClass(B, A)", analysis) {
  207           case InvalidSuperClass(ClsInfo("B"), ClsInfo("A"),
  208               FromClass(ClsInfo("A"))) =>
  209             true
  210         }
  211       }
  212     }
  213   }
  214 
  215   @Test
  216   def invalidImplementedInterface(): AsyncResult = await {
  217     val kindsCls = Seq(
  218         ClassKind.Class,
  219         ClassKind.ModuleClass,
  220         ClassKind.HijackedClass,
  221         ClassKind.Interface,
  222         ClassKind.JSClass,
  223         ClassKind.JSModuleClass,
  224         ClassKind.NativeJSClass,
  225         ClassKind.NativeJSModuleClass,
  226         ClassKind.AbstractJSType
  227     )
  228 
  229     def kindsIntfFor(kindCls: ClassKind): Seq[ClassKind] = {
  230       import ClassKind._
  231       kindCls match {
  232         case Class | ModuleClass | HijackedClass | Interface =>
  233           Seq(Class, ModuleClass, JSClass, NativeJSClass, AbstractJSType)
  234         case JSClass | JSModuleClass | NativeJSClass | NativeJSModuleClass |
  235             AbstractJSType =>
  236           Seq(Class, ModuleClass, HijackedClass, Interface, JSClass,
  237               JSModuleClass, NativeJSClass, NativeJSModuleClass)
  238       }
  239     }
  240 
  241     Future.traverse(kindsCls) { kindCls =>
  242       Future.traverse(kindsIntfFor(kindCls)) { kindIntf =>
  243         val classDefs = Seq(
  244             classDef("A", kind = kindCls,
  245                 superClass = validParentForKind(kindCls),
  246                 interfaces = List("B")),
  247             classDef("B", kind = kindIntf,
  248                 superClass = validParentForKind(kindIntf))
  249         )
  250 
  251         val analysis = computeAnalysis(classDefs,
  252             reqsFactory.instantiateClass("A", NoArgConstructorName))
  253 
  254         assertContainsError("InvalidImplementedInterface(B, A)", analysis) {
  255           case InvalidImplementedInterface(ClsInfo("B"), ClsInfo("A"),
  256               FromClass(ClsInfo("A"))) =>
  257             true
  258         }
  259       }
  260     }
  261   }
  262 
  263   @Test
  264   def notAModule(): AsyncResult = await {
  265     val classDefs = Seq(
  266         classDef("A", superClass = Some(ObjectClass),
  267             memberDefs = List(trivialCtor("A")))
  268     )
  269 
  270     val analysis = computeAnalysis(classDefs, reqsFactory.accessModule("A"))
  271 
  272     assertContainsError("NotAModule(A)", analysis) {
  273       case NotAModule(ClsInfo("A"), `fromUnitTest`) => true
  274     }
  275   }
  276 
  277   @Test
  278   def missingMethod(): AsyncResult = await {
  279     val classDefs = Seq(
  280         classDef("A", superClass = Some(ObjectClass),
  281             memberDefs = List(trivialCtor("A")))
  282     )
  283 
  284     val analysis = computeAnalysis(classDefs,
  285         reqsFactory.instantiateClass("A", NoArgConstructorName) ++
  286         reqsFactory.callMethod("A", m("foo", Nil, V)))
  287 
  288     assertContainsError("MissingMethod(A.foo;V)", analysis) {
  289       case MissingMethod(MethInfo("A", "foo;V"), `fromUnitTest`) => true
  290     }
  291   }
  292 
  293   @Test
  294   def missingAbstractMethod(): AsyncResult = await {
  295     val fooMethodName = m("foo", Nil, IntRef)
  296 
  297     val classDefs = Seq(
  298         classDef("A", superClass = Some(ObjectClass),
  299             memberDefs = List(trivialCtor("A"))),
  300         classDef("B", superClass = Some("A"),
  301             memberDefs = List(
  302                 trivialCtor("B"),
  303                 MethodDef(EMF, fooMethodName, NON, Nil, IntType, Some(int(5)))(EOH, None)
  304             ))
  305     )
  306 
  307     val analysis = computeAnalysis(classDefs,
  308         reqsFactory.instantiateClass("B", NoArgConstructorName) ++
  309         reqsFactory.callMethod("A", fooMethodName))
  310 
  311     assertContainsError("MissingMethod(A.foo;I)", analysis) {
  312       case MissingMethod(MethInfo("A", "foo;I"), `fromUnitTest`) => true
  313     }
  314   }
  315 
  316   @Test
  317   def conflictingDefaultMethods(): AsyncResult = await {
  318     val defaultMethodDef = MethodDef(EMF, m("foo", Nil, V), NON, Nil,
  319         NoType, Some(Skip()))(EOH, None)
  320     val classDefs = Seq(
  321         classDef("I1", kind = ClassKind.Interface,
  322             memberDefs = List(defaultMethodDef)),
  323         classDef("I2", kind = ClassKind.Interface,
  324             memberDefs = List(defaultMethodDef)),
  325         classDef("A", superClass = Some(ObjectClass),
  326             interfaces = List("I1", "I2"),
  327             memberDefs = List(trivialCtor("A")))
  328     )
  329 
  330     val analysis = computeAnalysis(classDefs,
  331         reqsFactory.instantiateClass("A", NoArgConstructorName) ++
  332         reqsFactory.callMethod("A", m("foo", Nil, V)))
  333 
  334     assertContainsError("ConflictingDefaultMethods(I1.foo;V, I2.foo;V)", analysis) {
  335       case ConflictingDefaultMethods(
  336           List(MethInfo("I1", "foo;V"), MethInfo("I2", "foo;V")),
  337           `fromAnalyzer`) =>
  338         true
  339       case ConflictingDefaultMethods(
  340           List(MethInfo("I2", "foo;V"), MethInfo("I1", "foo;V")),
  341           `fromAnalyzer`) =>
  342         true
  343     }
  344   }
  345 
  346   @Test
  347   def conflictingTopLevelExports(): AsyncResult = await {
  348     def singleDef(name: String) = {
  349       classDef(name,
  350           kind = ClassKind.ModuleClass, superClass = Some(ObjectClass),
  351           memberDefs = List(trivialCtor(name)),
  352           topLevelExportDefs = List(TopLevelModuleExportDef("foo")))
  353     }
  354 
  355     val classDefs = Seq(singleDef("A"), singleDef("B"))
  356     val analysis = computeAnalysis(classDefs)
  357 
  358     assertContainsError("ConflictingTopLevelExport(foo, A, B)", analysis) {
  359       case ConflictingTopLevelExport("foo", List(ClsInfo("A"), ClsInfo("B"))) =>
  360         true
  361       case ConflictingTopLevelExport("foo", List(ClsInfo("B"), ClsInfo("A"))) =>
  362         true
  363     }
  364   }
  365 
  366   @Test
  367   def degenerateConflictingTopLevelExports(): AsyncResult = await {
  368     val classDefs = Seq(classDef("A",
  369         kind = ClassKind.ModuleClass, superClass = Some(ObjectClass),
  370         memberDefs = List(trivialCtor("A")),
  371         topLevelExportDefs = List(
  372             TopLevelModuleExportDef("foo"),
  373             TopLevelModuleExportDef("foo"))))
  374 
  375     val analysis = computeAnalysis(classDefs)
  376     assertContainsError("ConflictingTopLevelExport(foo, <degenerate>)", analysis) {
  377       case ConflictingTopLevelExport("foo", _) => true
  378     }
  379   }
  380 
  381   @Test
  382   def juPropertiesNotReachableWhenUsingGetSetClearProperty(): AsyncResult = await {
  383     val systemMod = LoadModule("java.lang.System$")
  384     val emptyStr = StringLiteral("")
  385     val StringType = ClassType(BoxedStringClass)
  386 
  387     val classDefs = Seq(
  388         classDef("A", superClass = Some(ObjectClass), memberDefs = List(
  389             trivialCtor("A"),
  390             MethodDef(EMF, m("test", Nil, V), NON, Nil, NoType, Some(Block(
  391                 Apply(EAF, systemMod, m("getProperty", List(T), T), List(emptyStr))(StringType),
  392                 Apply(EAF, systemMod, m("getProperty", List(T, T), T), List(emptyStr))(StringType),
  393                 Apply(EAF, systemMod, m("setProperty", List(T, T), T), List(emptyStr))(StringType),
  394                 Apply(EAF, systemMod, m("clearProperty", List(T), T), List(emptyStr))(StringType)
  395             )))(EOH, None)
  396         ))
  397     )
  398 
  399     for {
  400       analysis <- computeAnalysis(classDefs,
  401           reqsFactory.instantiateClass("A", NoArgConstructorName) ++
  402           reqsFactory.callMethod("A", m("test", Nil, V)),
  403           stdlib = Some(TestIRRepo.fulllib))
  404     } yield {
  405       assertNoError(analysis)
  406 
  407       val juPropertiesClass = analysis.classInfos("java.util.Properties")
  408       assertFalse(juPropertiesClass.isAnySubclassInstantiated)
  409       assertFalse(juPropertiesClass.areInstanceTestsUsed)
  410       assertFalse(juPropertiesClass.isDataAccessed)
  411     }
  412   }
  413 
  414   @Test  // #3571
  415   def specificReflectiveProxy(): AsyncResult = await {
  416     val fooAMethodName = m("foo", Nil, ClassRef("A"))
  417     val fooBMethodName = m("foo", Nil, ClassRef("B"))
  418 
  419     val fooReflProxyName =
  420       MethodName.reflectiveProxy(SimpleMethodName("foo"), Nil)
  421 
  422     val classDefs = Seq(
  423         classDef("A", superClass = Some(ObjectClass)),
  424         classDef("B", superClass = Some("A")),
  425         classDef("X", superClass = Some(ObjectClass),
  426             memberDefs = List(
  427                 trivialCtor("X"),
  428                 MethodDef(EMF, fooAMethodName, NON, Nil, ClassType("A"),
  429                     Some(Null()))(EOH, None),
  430                 MethodDef(EMF, fooBMethodName, NON, Nil, ClassType("B"),
  431                     Some(Null()))(EOH, None)
  432             )
  433         )
  434     )
  435 
  436     for {
  437       analysis <- computeAnalysis(classDefs,
  438           reqsFactory.instantiateClass("X", NoArgConstructorName) ++
  439           reqsFactory.callMethod("X", fooReflProxyName))
  440     } yield {
  441       assertNoError(analysis)
  442 
  443       val MethodSyntheticKind.ReflectiveProxy(target) = {
  444         analysis.classInfos("X")
  445           .methodInfos(MemberNamespace.Public)(fooReflProxyName)
  446           .syntheticKind
  447       }
  448 
  449       assertEquals(fooBMethodName, target)
  450     }
  451   }
  452 
  453   @Test
  454   def isAbstractReachable(): AsyncResult = await {
  455     val fooMethodName = m("foo", Nil, IntRef)
  456     val barMethodName = m("bar", Nil, IntRef)
  457 
  458     val classDefs = Seq(
  459         classDef("I1", kind = ClassKind.Interface,
  460             memberDefs = List(
  461                 MethodDef(EMF, barMethodName, NON, Nil, IntType, None)(EOH, None)
  462             )),
  463         classDef("I2", kind = ClassKind.Interface,
  464             memberDefs = List(
  465                 MethodDef(EMF, barMethodName, NON, Nil, IntType, None)(EOH, None)
  466             )),
  467         classDef("A", superClass = Some(ObjectClass), interfaces = List("I1"),
  468             memberDefs = List(
  469                 trivialCtor("A"),
  470                 MethodDef(EMF, fooMethodName, NON, Nil, IntType, None)(EOH, None)
  471             )),
  472         classDef("B", superClass = Some("A"), interfaces = List("I2"),
  473             memberDefs = List(
  474                 trivialCtor("B"),
  475                 MethodDef(EMF, fooMethodName, NON, Nil, IntType, Some(int(5)))(EOH, None)
  476             )),
  477         classDef("C", superClass = Some("B"),
  478             memberDefs = List(
  479                 trivialCtor("C"),
  480                 MethodDef(EMF, barMethodName, NON, Nil, IntType, Some(int(5)))(EOH, None)
  481             ))
  482     )
  483 
  484     val analysisFuture = computeAnalysis(classDefs,
  485         reqsFactory.instantiateClass("C", NoArgConstructorName) ++
  486         reqsFactory.callMethod("A", fooMethodName) ++
  487         reqsFactory.callMethod("B", barMethodName))
  488 
  489     for (analysis <- analysisFuture) yield {
  490       assertNoError(analysis)
  491 
  492       val BClassInfo = analysis.classInfos("C")
  493       assertEquals(List[ClassName]("C", "B", "A", ObjectClass, "I1", "I2"),
  494           BClassInfo.ancestors.map(_.className))
  495 
  496       val AfooMethodInfo = analysis.classInfos("A")
  497         .methodInfos(MemberNamespace.Public)(fooMethodName)
  498       assertTrue(AfooMethodInfo.isAbstractReachable)
  499 
  500       val I1barMethodInfo = analysis.classInfos("I1")
  501         .methodInfos(MemberNamespace.Public)(barMethodName)
  502       assertTrue(I1barMethodInfo.isAbstractReachable)
  503 
  504       val I2barMethodInfo = analysis.classInfos("I2")
  505         .methodInfos(MemberNamespace.Public)(barMethodName)
  506       assertFalse(I2barMethodInfo.isAbstractReachable)
  507     }
  508   }
  509 }
  510 
  511 object AnalyzerTest {
  512   private val reqsFactory = SymbolRequirement.factory("unit test")
  513 
  514   private val fromAnalyzer = FromCore("analyzer")
  515   private val fromUnitTest = FromCore("unit test")
  516 
  517   private val AClass = ClassName("A")
  518   private val BClass = ClassName("B")
  519   private val CClass = ClassName("C")
  520   private val DClass = ClassName("D")
  521   private val EClass = ClassName("E")
  522 
  523   private def validParentForKind(kind: ClassKind): Option[ClassName] = {
  524     import ClassKind._
  525     kind match {
  526       case Class | ModuleClass | HijackedClass | NativeJSClass |
  527           NativeJSModuleClass =>
  528         Some(ObjectClass)
  529       case JSClass | JSModuleClass =>
  530         Some(ClassName("scala.scalajs.js.Object"))
  531       case Interface | AbstractJSType =>
  532         None
  533     }
  534   }
  535 
  536   private def computeAnalysis(classDefs: Seq[ClassDef],
  537       symbolRequirements: SymbolRequirement = reqsFactory.none(),
  538       stdlib: Option[TestIRRepo] = Some(TestIRRepo.minilib))(
  539       implicit ec: ExecutionContext): Future[Analysis] = {
  540 
  541     val classesWithEntryPoints0 = classDefs
  542       .map(EntryPointsInfo.forClassDef)
  543       .withFilter(_.hasEntryPoint)
  544       .map(_.className)
  545 
  546     val classNameToInfo =
  547       classDefs.map(c => c.name.name -> Infos.generateClassInfo(c)).toMap
  548 
  549     def inputProvider(loader: Option[TestIRRepo.InfoLoader]) = new Analyzer.InputProvider {
  550       def classesWithEntryPoints(): Iterable[ClassName] = classesWithEntryPoints0
  551 
  552       def loadInfo(className: ClassName)(
  553           implicit ec: ExecutionContext): Option[Future[Infos.ClassInfo]] = {
  554         /* Note: We could use Future.successful here to complete the future
  555          * immediately. However, in order to exercise as much asynchronizity as
  556          * possible, we don't.
  557          */
  558         val own = classNameToInfo.get(className).map(Future(_))
  559         own.orElse(loader.flatMap(_.loadInfo(className)))
  560       }
  561     }
  562 
  563     for {
  564       loader <- Future.traverse(stdlib.toList)(_.loader).map(_.headOption)
  565       analysis <- Analyzer.computeReachability(CommonPhaseConfig(),
  566           symbolRequirements, allowAddingSyntheticMethods = true,
  567           checkAbstractReachability = true, inputProvider(loader))
  568     } yield {
  569       analysis
  570     }
  571   }
  572 
  573   private def assertNoError(analysis: Future[Analysis])(
  574       implicit ec: ExecutionContext): Future[Unit] = {
  575     assertExactErrors(analysis)
  576   }
  577 
  578   private def assertNoError(analysis: Analysis): Unit =
  579     assertExactErrors(analysis)
  580 
  581   private def assertExactErrors(analysis: Future[Analysis],
  582       expectedErrors: Error*)(implicit ec: ExecutionContext): Future[Unit] = {
  583     analysis.map(assertExactErrors(_, expectedErrors: _*))
  584   }
  585 
  586   private def assertExactErrors(analysis: Analysis,
  587       expectedErrors: Error*): Unit = {
  588     val actualErrors = analysis.errors
  589 
  590     for (expectedError <- expectedErrors) {
  591       assertTrue(s"Missing expected error: $expectedError",
  592           actualErrors.contains(expectedError))
  593     }
  594 
  595     if (actualErrors.size != expectedErrors.size) {
  596       for (actualError <- actualErrors) {
  597         assertTrue(s"Unexpected error: $actualError",
  598             expectedErrors.contains(actualError))
  599       }
  600     }
  601   }
  602 
  603   private def assertContainsError(msg: String, analysis: Future[Analysis])(
  604       pf: PartialFunction[Error, Boolean])(
  605       implicit ec: ExecutionContext): Future[Unit] = {
  606     analysis.map(assertContainsError(msg, _)(pf))
  607   }
  608 
  609   private def assertContainsError(msg: String, analysis: Analysis)(
  610       pf: PartialFunction[Error, Boolean]): Unit = {
  611     val fullMessage = s"Expected $msg, got ${analysis.errors}"
  612     assertTrue(fullMessage, analysis.errors.exists {
  613       e => pf.applyOrElse(e, (_: Error) => false)
  614     })
  615   }
  616 
  617   object ClsInfo {
  618     def unapply(classInfo: Analysis.ClassInfo): Some[String] =
  619       Some(classInfo.className.nameString)
  620   }
  621 
  622   object MethInfo {
  623     def unapply(methodInfo: Analysis.MethodInfo): Some[(String, String)] =
  624       Some((methodInfo.owner.className.nameString, methodInfo.methodName.nameString))
  625   }
  626 }