Repository: 7mind/izumi
Pull request #1143 (2020-07-11 23:30:09)

WIP: Adapt distage-testkit to 0.11.0 Activation API changes


Patch:
diff --git a/distage/distage-core-api/src/main/scala/izumi/distage/model/Planner.scala b/distage/distage-core-api/src/main/scala/izumi/distage/model/Planner.scala
index f950f12f5..1a8e85f9c 100644
--- a/distage/distage-core-api/src/main/scala/izumi/distage/model/Planner.scala
+++ b/distage/distage-core-api/src/main/scala/izumi/distage/model/Planner.scala
@@ -19,7 +19,7 @@ trait Planner extends PlanSplittingOps {
   def rewrite(bindings: ModuleBase): ModuleBase
 
   @deprecated("used in tests only!", "will be removed in 0.11.1")
-  def finish(semiPlan: SemiPlan): OrderedPlan
+  private[distage] def finish(semiPlan: SemiPlan): OrderedPlan
 
   private[distage] def truncate(plan: OrderedPlan, roots: Set[DIKey]): OrderedPlan
 }
diff --git a/distage/distage-core-api/src/main/scala/izumi/distage/model/PlannerInput.scala b/distage/distage-core-api/src/main/scala/izumi/distage/model/PlannerInput.scala
index 02f56781a..c29b4c10e 100644
--- a/distage/distage-core-api/src/main/scala/izumi/distage/model/PlannerInput.scala
+++ b/distage/distage-core-api/src/main/scala/izumi/distage/model/PlannerInput.scala
@@ -47,14 +47,10 @@ object PlannerInput {
     */
   def target[T: Tag](name: String)(bindings: ModuleBase, activation: Activation): PlannerInput = PlannerInput(bindings, activation, DIKey.get[T].named(name))
 
-  /** Disable all pruning.
-    * Every binding in `bindings` will be instantiated eagerly, without selection
-    */
+  /** Disable all pruning. Every binding in `bindings` will be instantiated eagerly, without selection of the root bindings */
   def noGC(bindings: ModuleBase, activation: Activation = Activation.empty): PlannerInput = PlannerInput(bindings, activation, Roots.Everything)
 
-  /** Disable all pruning.
-    * Every binding in `bindings` will be instantiated eagerly, without selection
-    */
+  /** Disable all pruning. Every binding in `bindings` will be instantiated eagerly, without selection of the root bindings */
   @deprecated("use noGC", "will be removed in 0.11.1")
   def noGc(bindings: ModuleBase, activation: Activation = Activation.empty): PlannerInput = PlannerInput(bindings, activation, Roots.Everything)
 }
diff --git a/distage/distage-core-api/src/main/scala/izumi/distage/model/plan/operations/OperationOrigin.scala b/distage/distage-core-api/src/main/scala/izumi/distage/model/plan/operations/OperationOrigin.scala
index eb1cfa7c9..6143665c8 100644
--- a/distage/distage-core-api/src/main/scala/izumi/distage/model/plan/operations/OperationOrigin.scala
+++ b/distage/distage-core-api/src/main/scala/izumi/distage/model/plan/operations/OperationOrigin.scala
@@ -4,6 +4,11 @@ import izumi.distage.model.definition.Binding
 
 sealed trait OperationOrigin {
   def toSynthetic: OperationOrigin.Synthetic
+
+  @inline final def fold[A](onUnknown: => A, onDefined: Binding => A): A = this match {
+    case defined: OperationOrigin.Defined => onDefined(defined.binding)
+    case OperationOrigin.Unknown => onUnknown
+  }
 }
 
 object OperationOrigin {
diff --git a/distage/distage-core/src/main/scala/distage/Injector.scala b/distage/distage-core/src/main/scala/distage/Injector.scala
index dc297f299..b64801c3f 100644
--- a/distage/distage-core/src/main/scala/distage/Injector.scala
+++ b/distage/distage-core/src/main/scala/distage/Injector.scala
@@ -94,12 +94,6 @@ object Injector extends InjectorFactory {
       new InjectorDefaultImpl(parent, this)
     }
 
-    override final def bootloader(
-      input: PlannerInput,
-      activation: Activation = Activation.empty,
-      bootstrapModule: BootstrapModule = BootstrapModule.empty,
-    ): Bootloader = super.bootloader(input, activation, bootstrapModule)
-
     private[this] def cycleActivation: Activation = Activation(Cycles -> cycleChoice)
   }
 
diff --git a/distage/distage-core/src/main/scala/izumi/distage/InjectorDefaultImpl.scala b/distage/distage-core/src/main/scala/izumi/distage/InjectorDefaultImpl.scala
index ebeab865c..631868953 100644
--- a/distage/distage-core/src/main/scala/izumi/distage/InjectorDefaultImpl.scala
+++ b/distage/distage-core/src/main/scala/izumi/distage/InjectorDefaultImpl.scala
@@ -33,7 +33,7 @@ class InjectorDefaultImpl(
   }
 
   @deprecated("used in tests only!", "")
-  override def finish(semiPlan: SemiPlan): OrderedPlan = {
+  private[distage] override def finish(semiPlan: SemiPlan): OrderedPlan = {
     planner.finish(semiPlan)
   }
 
diff --git a/distage/distage-core/src/main/scala/izumi/distage/bootstrap/BootstrapLocator.scala b/distage/distage-core/src/main/scala/izumi/distage/bootstrap/BootstrapLocator.scala
index 01ba48a0b..2728fb445 100644
--- a/distage/distage-core/src/main/scala/izumi/distage/bootstrap/BootstrapLocator.scala
+++ b/distage/distage-core/src/main/scala/izumi/distage/bootstrap/BootstrapLocator.scala
@@ -17,7 +17,7 @@ import izumi.distage.model.provisioning.{PlanInterpreter, ProvisioningFailureInt
 import izumi.distage.model.references.IdentifiedRef
 import izumi.distage.model.reflection.{DIKey, MirrorProvider}
 import izumi.distage.planning._
-import izumi.distage.planning.gc.{NoopDIGC, TracingDIGC}
+import izumi.distage.planning.gc.TracingDIGC
 import izumi.distage.provisioning._
 import izumi.distage.provisioning.strategies._
 import izumi.fundamentals.platform.console.TrivialLogger
@@ -28,8 +28,8 @@ final class BootstrapLocator(bindings0: BootstrapContextModule, bootstrapActivat
   override val parent: Option[AbstractLocator] = None
   override val plan: OrderedPlan = {
     // BootstrapModule & bootstrap plugins cannot modify `Activation` after 0.11.0,
-    // it's solely under control of `PlannerInput` now,
-    // please open an issue if you need the ability to override Activation using BootstrapModule
+    // it's solely under control of `PlannerInput` now.
+    // Please open an issue if you need the ability to override Activation using BootstrapModule
     val bindings = bindings0 ++ new BootstrapModuleDef {
         make[Activation].fromValue(bootstrapActivation)
         make[BootstrapModule].fromValue(bindings0)
@@ -88,13 +88,11 @@ object BootstrapLocator {
     val translator = new BindingTranslator.Impl()
     val forwardingRefResolver = new ForwardingRefResolverDefaultImpl(analyzer, true)
     val sanityChecker = new SanityCheckerDefaultImpl(analyzer)
-    val gc = NoopDIGC
     val mp = mirrorProvider
 
     new PlannerDefaultImpl(
       forwardingRefResolver = forwardingRefResolver,
       sanityChecker = sanityChecker,
-      gc = gc,
       planningObserver = bootstrapObserver,
       hook = hook,
       bindingTranslator = translator,
@@ -149,12 +147,11 @@ object BootstrapLocator {
     make[BindingTranslator].from[BindingTranslator.Impl]
 
     make[ProxyProvider].tagged(Cycles.Proxy).from[CglibProxyProvider]
-    make[ProxyStrategy].tagged(Cycles.Proxy).from[ProxyStrategyDefaultImpl]
-
     make[ProxyProvider].tagged(Cycles.Byname).from[ProxyProviderFailingImpl]
-    make[ProxyStrategy].tagged(Cycles.Byname).from[ProxyStrategyDefaultImpl]
-
     make[ProxyProvider].tagged(Cycles.Disable).from[ProxyProviderFailingImpl]
+
+    make[ProxyStrategy].tagged(Cycles.Proxy).from[ProxyStrategyDefaultImpl]
+    make[ProxyStrategy].tagged(Cycles.Byname).from[ProxyStrategyDefaultImpl]
     make[ProxyStrategy].tagged(Cycles.Disable).from[ProxyStrategyFailingImpl]
   }
 
diff --git a/distage/distage-core/src/main/scala/izumi/distage/planning/PlannerDefaultImpl.scala b/distage/distage-core/src/main/scala/izumi/distage/planning/PlannerDefaultImpl.scala
index 8fecee7af..87093fca5 100644
--- a/distage/distage-core/src/main/scala/izumi/distage/planning/PlannerDefaultImpl.scala
+++ b/distage/distage-core/src/main/scala/izumi/distage/planning/PlannerDefaultImpl.scala
@@ -1,8 +1,10 @@
 package izumi.distage.planning
 
+import izumi.distage.model.definition.Axis.AxisValue
+
 import scala.annotation.nowarn
 import izumi.distage.model.definition.BindingTag.AxisTag
-import izumi.distage.model.definition.{Binding, ModuleBase}
+import izumi.distage.model.definition.{Activation, Binding, ModuleBase}
 import izumi.distage.model.exceptions.{ConflictResolutionException, DIBugException, SanityCheckFailedException}
 import izumi.distage.model.plan.ExecutableOp.{CreateSet, ImportDependency, InstantiationOp, MonadicOp, WiringOp}
 import izumi.distage.model.plan._
@@ -22,7 +24,6 @@ import izumi.fundamentals.platform.strings.IzString._
 class PlannerDefaultImpl(
   forwardingRefResolver: ForwardingRefResolver,
   sanityChecker: SanityChecker,
-  gc: DIGarbageCollector,
   planningObserver: PlanningObserver,
   hook: PlanningHook,
   bindingTranslator: BindingTranslator,
@@ -37,7 +38,7 @@ class PlannerDefaultImpl(
   override def planNoRewrite(input: PlannerInput): OrderedPlan = {
     resolveConflicts(input) match {
       case Left(errors) =>
-        throwOnConflict(errors)
+        throwOnConflict(input.activation, errors)
 
       case Right(resolved) =>
         val mappedGraph = resolved.predcessors.links.toSeq.map {
@@ -77,12 +78,12 @@ class PlannerDefaultImpl(
     hook.hookDefinition(module)
   }
 
-  @deprecated("used in tests only!", "")
-  override def finish(semiPlan: SemiPlan): OrderedPlan = {
+  @deprecated("used in tests only!", "will be removed in 0.11.1")
+  private[distage] override def finish(semiPlan: SemiPlan): OrderedPlan = {
     Value(semiPlan)
       .map(addImports)
       .eff(planningObserver.onPhase05PreGC)
-      .map(gc.gc)
+      .map(TracingDIGC.gc)
       .map(order)
       .get
   }
@@ -334,27 +335,27 @@ class PlannerDefaultImpl(
       .get
   }
 
-  protected[this] def throwOnConflict(issues: List[ConflictResolutionError[DIKey, InstantiationOp]]): Nothing = {
-    val issueRepr = issues.map(formatConflict).niceList()
+  protected[this] def throwOnConflict(activation: Activation, issues: List[ConflictResolutionError[DIKey, InstantiationOp]]): Nothing = {
+    val issueRepr = issues.map(formatConflict(activation)).mkString("\n", "\n", "")
 
     throw new ConflictResolutionException(
       s"""There must be exactly one valid binding for each DIKey.
          |
          |You can use named instances: `make[X].named("id")` method and `distage.Id` annotation to disambiguate between multiple instances with the same type.
          |
-         |List of problematic bindings: $issueRepr
+         |List of problematic bindings:$issueRepr
        """.stripMargin,
       issues,
     )
   }
 
-  protected[this] def formatConflict(conflictResolutionError: ConflictResolutionError[DIKey, InstantiationOp]): String = {
+  protected[this] def formatConflict(activation: Activation)(conflictResolutionError: ConflictResolutionError[DIKey, InstantiationOp]): String = {
     conflictResolutionError match {
       case AmbigiousActivationsSet(issues) =>
         val printedActivationSelections = issues.map {
           case (axis, choices) => s"axis: `$axis`, selected: {${choices.map(_.value).mkString(", ")}}"
         }
-        s"""Mulitple axis choices selected for axes, only one choice must be made selected for an axis:
+        s"""Multiple axis choices selected for axes, only one choice must be made selected for an axis:
            |
            |${printedActivationSelections.niceList().shift(4)}""".stripMargin
 
@@ -363,40 +364,43 @@ class PlannerDefaultImpl(
           .map {
             case (k, nodes) =>
               val candidates = conflictingAxisTagsHint(
-                nodes.flatMap(_._1),
-                nodes.map(_._2.meta.origin).collect {
-                  case defined: OperationOrigin.Defined => defined.binding
-                },
+                activeChoices = activation.activeChoices.values.toSet,
+                ops = nodes.map(_._2.meta.origin),
               )
-              s"""Conflict resolution failed key for $k with reason:
+              s"""Conflict resolution failed for key `${k.asString}` with reason:
                  |
-                 |${"reason dunno lmao".shift(4)}
+                 |   Conflicting definitions available without a disambiguating axis choice
                  |
-                 |    Candidates left: ${candidates.niceList().shift(4)}""".stripMargin
+                 |   Candidates left: ${candidates.niceList().shift(4)}""".stripMargin
           }.niceList()
 
       case UnsolvedConflicts(defs) =>
         defs
           .map {
             case (k, axisBinds) =>
-              s"""multiple axee Conflict resolution failed key for $k with reason:
+              s"""Conflict resolution failed for key `${k.asString}` with reason:
                  |
-                 |${"reason axises".shift(4)}
+                 |   Unsolved conflicts.
                  |
-                 |    Candidates left: ${axisBinds.niceList().shift(4)}""".stripMargin
+                 |   Candidates left: ${axisBinds.niceList().shift(4)}""".stripMargin
           }.niceList()
     }
   }
 
-  protected[this] def conflictingAxisTagsHint(activeChoices: Set[AxisPoint], ops: Set[Binding]): Seq[String] = {
+  protected[this] def conflictingAxisTagsHint(activeChoices: Set[AxisValue], ops: Set[OperationOrigin]): Seq[String] = {
+    val axisValuesInBindings = ops
+      .iterator.collect {
+        case d: OperationOrigin.Defined => d.binding.tags
+      }.flatten.collect {
+        case AxisTag(t) => t
+      }.toSet
+    val alreadyActiveTags = activeChoices.intersect(axisValuesInBindings)
     ops.toSeq.map {
       op =>
-        val axisValues = op.tags.collect { case AxisTag(t) => t.toAxisPoint }
-
-        val bindingTags = axisValues.diff(activeChoices)
-        val alreadyActiveTags = axisValues.intersect(activeChoices)
+        val bindingTags = op.fold(Set.empty, _.tags.collect { case AxisTag(t) => t })
 
-        s"${op.origin}, possible: {${bindingTags.mkString(", ")}}, active: {${alreadyActiveTags.mkString(", ")}}"
+        val originStr = op.fold("Unknown", _.origin.toString)
+        s"$originStr, possible: {${bindingTags.mkString(", ")}}, active: {${alreadyActiveTags.mkString(", ")}}"
     }
   }
 
diff --git a/distage/distage-core/src/main/scala/izumi/distage/planning/gc/NoopDIGC.scala b/distage/distage-core/src/main/scala/izumi/distage/planning/gc/NoopDIGC.scala
deleted file mode 100644
index a1b9c819d..000000000
--- a/distage/distage-core/src/main/scala/izumi/distage/planning/gc/NoopDIGC.scala
+++ /dev/null
@@ -1,8 +0,0 @@
-package izumi.distage.planning.gc
-
-import izumi.distage.model.plan.SemiPlan
-import izumi.distage.model.planning.DIGarbageCollector
-
-object NoopDIGC extends DIGarbageCollector {
-  override def gc(plan: SemiPlan): SemiPlan = plan
-}
diff --git a/fundamentals/fundamentals-collections/src/main/scala/izumi/fundamentals/graphs/tools/MutationResolver.scala b/fundamentals/fundamentals-collections/src/main/scala/izumi/fundamentals/graphs/tools/MutationResolver.scala
index 5939fc841..8b7268be5 100644
--- a/fundamentals/fundamentals-collections/src/main/scala/izumi/fundamentals/graphs/tools/MutationResolver.scala
+++ b/fundamentals/fundamentals-collections/src/main/scala/izumi/fundamentals/graphs/tools/MutationResolver.scala
@@ -41,6 +41,7 @@ object MutationResolver {
   final case class Selected[N](key: N, axis: Set[AxisPoint])
   final case class MutSel[N](key: N, mut: Option[Int]) {
     def isMutator: Boolean = mut.isDefined
+    def asString: String = s"$key${mut.fold("")(i => s":$i")}"
   }
 
   final case class Resolution[N, V](graph: DG[MutSel[N], RemappedValue[V, N]]) //, unresolved: Map[Annotated[N], Seq[Node[N, V]]])


Test patch:
diff --git a/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/dstest/DistageTestRunner.scala b/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/dstest/DistageTestRunner.scala
index 39772fd7a..c5f65bd7f 100644
--- a/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/dstest/DistageTestRunner.scala
+++ b/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/dstest/DistageTestRunner.scala
@@ -3,7 +3,7 @@ package izumi.distage.testkit.services.dstest
 import java.util.concurrent.{ConcurrentHashMap, TimeUnit}
 
 import distage._
-import izumi.distage.bootstrap.BootstrapLocator
+import izumi.distage.bootstrap.{BootstrapLocator, Cycles}
 import izumi.distage.config.model.AppConfig
 import izumi.distage.framework.model.exceptions.IntegrationCheckException
 import izumi.distage.framework.model.{ActivationInfo, IntegrationCheck}
@@ -75,6 +75,7 @@ class DistageTestRunner[F[_]: TagK](
       strengthenedKeys: Set[DIKey],
     )
 
+    // FIXME: _bootstrap_ keys that may vary between envs but shouldn't cause them to differ (because they should only impact bootstrap)
     val unstableKeys = {
       val activationKeys = Set(DIKey[Activation], DIKey[ActivationInfo])
       val recursiveKeys = Set(DIKey[BootstrapModule])
@@ -97,19 +98,29 @@ class DistageTestRunner[F[_]: TagK](
               // here we scan our classpath to enumerate of our components (we have "bootstrap" components - injector plugins, and app components)
               val options = envExec.planningOptions
               val provider = env.bootstrapFactory.makeModuleProvider[F](options, config, lateLogger.router, env.roles, env.activationInfo, env.activation)
+
               val bsModule = provider.bootstrapModules().merge overridenBy env.bsModule
+              val finalBsModule = BootstrapLocator.defaultBootstrap overridenBy bsModule
+
               val appModule = provider.appModules().merge overridenBy env.appModule
-              val (bsPlanMinusActivations, bsModuleMinusActivations, injector, planner) = {
-                // FIXME: Checking both bootstrap Plan & bootstrap module to prevent `Bootloader` becoming becoming inconsistent
-                //  if used in tests (if BootstrapModule isn't checked it could be different from expected)
-                //  we're also removing & re-injecting Planner, Activations & BootstrapModule (in 0.11.0 activation won't be set via bsModules & won't be stored in Planner)
+
+              val (bsPlanMinusUnstable, bsModuleMinusUnstable, injector, planner) = {
+                // FIXME: Including both bootstrap Plan & bootstrap Module into merge criteria to prevent `Bootloader`
+                //  becoming becoming inconsistent across envs (if BootstrapModule isn't considered it could come from different env than expected).
+
+                // FIXME: We're also removing & re-injecting Planner, Activations & BootstrapModule (in 0.11.0 activation won't be set via bsModules & won't be stored in Planner)
                 //  (planner holds activations & the rest is for Bootloader self-introspection)
-                val bsLocator = new BootstrapLocator(BootstrapLocator.defaultBootstrap overridenBy bsModule, env.activation)
+
+                // create bsLocator separately because we can't retrieve Plan & Module from Injector
+                // fixme: adding Cycles.Proxy manually here
+                val bsLocator = new BootstrapLocator(finalBsModule, Activation(Cycles -> Cycles.Proxy) ++ env.activation)
+
+                val bsPlanMinusUnstable = bsLocator.plan.steps.filterNot(unstableKeys contains _.target)
+                val bsModuleMinusUnstable = bsLocator.get[BootstrapModule].drop(unstableKeys)
+
                 val injector = Injector.inherit(bsLocator)
-                val bsPlanMinusActivations = bsLocator.plan.steps.filterNot(unstableKeys contains _.target)
-                val bsModuleMinusActivations = bsLocator.get[BootstrapModule].drop(unstableKeys)
                 val planner = bsLocator.get[Planner]
-                (bsPlanMinusActivations, bsModuleMinusActivations, injector, planner)
+                (bsPlanMinusUnstable, bsModuleMinusUnstable, injector, planner)
               }
 
               // runtime plan with `runtimeGcRoots`
@@ -142,7 +153,7 @@ class DistageTestRunner[F[_]: TagK](
                 _.collectChildren[IntegrationCheck[F]].map(_.target).toSet
               }
 
-              val envMergeCriteria = EnvMergeCriteria(bsPlanMinusActivations, bsModuleMinusActivations, shared, runtimePlan, envExec)
+              val envMergeCriteria = EnvMergeCriteria(bsPlanMinusUnstable, bsModuleMinusUnstable, shared, runtimePlan, envExec)
 
               val memoEnvHashCode = envMergeCriteria.hashCode()
               val integrationLogger = lateLogger("memoEnv" -> memoEnvHashCode)
@@ -578,8 +589,8 @@ object DistageTestRunner {
     }
   }
 
-  private val enableDebugOutput = DebugProperties.`izumi.distage.testkit.debug`.boolValue(false)
+  private val enableDebugOutput: Boolean = DebugProperties.`izumi.distage.testkit.debug`.boolValue(false)
 
-  private val memoizedConfig = new ConcurrentHashMap[(String, BootstrapFactory, Option[AppConfig]), AppConfig]()
+  private final val memoizedConfig = new ConcurrentHashMap[(String, BootstrapFactory, Option[AppConfig]), AppConfig]
 
 }
diff --git a/fundamentals/fundamentals-collections/src/test/scala/izumi/fundamentals/graphs/GraphFixtures.scala b/fundamentals/fundamentals-collections/src/test/scala/izumi/fundamentals/graphs/GraphFixtures.scala
index acc24ddc1..417c502c2 100644
--- a/fundamentals/fundamentals-collections/src/test/scala/izumi/fundamentals/graphs/GraphFixtures.scala
+++ b/fundamentals/fundamentals-collections/src/test/scala/izumi/fundamentals/graphs/GraphFixtures.scala
@@ -102,11 +102,6 @@ object GraphFixtures {
 
   implicit object AShow extends NodeShow[MutSel[String]] {
     override def show(t: MutSel[String]): String = {
-//      val base = if (t.con.isEmpty) {
-//        t.key
-//      } else {
-//        s"${t.key}${t.con.mkString("{", ",", "}")}"
-//      }
       val base = t.key
       t.mut match {
         case Some(value) =>