Repository: 7mind/izumi
Pull request #788 (2019-12-17 20:24:58)

Update sbt-pgp to 2.0.1
Updates com.jsuereth:sbt-pgp from 2.0.1-M3 to 2.0.1.


I'll automatically update this PR to resolve conflicts as long as you don't change it yourself.

If you'd like to skip this version, you can just close this PR. If you have any feedback, just mention me in the comments below.

Have a fantastic day writing Scala!

<details>
<summary>Ignore future updates</summary>

Add this to your `.scala-steward.conf` file to ignore future updates of this dependency:
```
updates.ignore = [ { groupId = "com.jsuereth", artifactId = "sbt-pgp" } ]
```
</details>

labels: semver-pre-release


Patch:
diff --git a/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/PluginConfig.scala b/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/PluginConfig.scala
index 6c7af4d51..cbc49f380 100644
--- a/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/PluginConfig.scala
+++ b/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/PluginConfig.scala
@@ -1,10 +1,19 @@
 package izumi.distage.plugins
 
 final case class PluginConfig(
-                               debug: Boolean = false,
                                packagesEnabled: Seq[String],
-                               packagesDisabled: Seq[String] = Nil,
+                               packagesDisabled: Seq[String],
+                               cachePackages: Boolean,
+                               debug: Boolean,
                              )
 object PluginConfig {
-  def empty: PluginConfig = PluginConfig(debug = false, Nil, Nil)
+  def empty: PluginConfig = PluginConfig(Nil, Nil, cachePackages = false, debug = false)
+  def packages(packagesEnabled: Seq[String]): PluginConfig = PluginConfig(packagesEnabled, Nil, cachePackages = true, debug = false)
+  def cached(packagesEnabled: Seq[String]): PluginConfig = {
+    import izumi.fundamentals.platform.strings.IzString._
+    val cacheEnabled = System
+      .getProperty(DebugProperties.`izumi.distage.testkit.plugins.memoize`)
+      .asBoolean(true)
+    PluginConfig(packagesEnabled, Nil, cachePackages = cacheEnabled, debug = false)
+  }
 }
diff --git a/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/load/PluginLoaderDefaultImpl.scala b/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/load/PluginLoaderDefaultImpl.scala
index 3375e1b3a..f667ec076 100644
--- a/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/load/PluginLoaderDefaultImpl.scala
+++ b/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/load/PluginLoaderDefaultImpl.scala
@@ -3,6 +3,7 @@ package izumi.distage.plugins.load
 import io.github.classgraph.ClassGraph
 import izumi.distage.plugins.{PluginBase, PluginConfig, PluginDef}
 import izumi.functional.Value
+import izumi.fundamentals.platform.cache.SyncCache
 
 import scala.jdk.CollectionConverters._
 
@@ -11,22 +12,40 @@ class PluginLoaderDefaultImpl
   pluginConfig: PluginConfig,
 ) extends PluginLoader {
   def load(): Seq[PluginBase] = {
-    val pluginBase = classOf[PluginBase]
-    val pluginDef = classOf[PluginDef]
     val config = pluginConfig.copy(packagesEnabled = pluginConfig.packagesEnabled)
 
     val enabledPackages: Seq[String] = config.packagesEnabled.filterNot(config.packagesDisabled.contains)
     val disabledPackages: Seq[String] = config.packagesDisabled
 
-    PluginLoaderDefaultImpl.load[PluginBase](pluginBase.getName, Seq(pluginDef.getName), enabledPackages, disabledPackages, config.debug)
+    val pluginBase = classOf[PluginBase]
+    val pluginDef = classOf[PluginDef]
+    val whitelistedClasses = Seq(pluginDef.getName)
+
+    def loadPkgs(pkgs: Seq[String]): Seq[PluginBase] = {
+      PluginLoaderDefaultImpl.doLoad[PluginBase](pluginBase.getName, whitelistedClasses, pkgs, disabledPackages, config.debug)
+    }
+
+    if (!pluginConfig.cachePackages) {
+      loadPkgs(enabledPackages)
+    } else {
+      val h1 = scala.util.hashing.MurmurHash3.seqHash(whitelistedClasses)
+      val h2 = scala.util.hashing.MurmurHash3.seqHash(disabledPackages)
+      enabledPackages.flatMap{
+        p =>
+          val key = s"$p;$h1;$h2"
+          PluginLoaderDefaultImpl.cache.getOrCompute(key, loadPkgs(Seq(p)))
+      }
+    }
   }
 }
 
 object PluginLoaderDefaultImpl {
-  def load[T](base: String, whitelist: Seq[String], enabledPackages: Seq[String], disabledPackages: Seq[String], debug: Boolean): Seq[T] = {
+  private lazy val cache = new SyncCache[String, Seq[PluginBase]]()
+
+  def doLoad[T](base: String, whitelistClasses: Seq[String], enabledPackages: Seq[String], disabledPackages: Seq[String], debug: Boolean): Seq[T] = {
     val scanResult = Value(new ClassGraph())
       .map(_.whitelistPackages(enabledPackages: _*))
-      .map(_.whitelistClasses(whitelist :+ base: _*))
+      .map(_.whitelistClasses(whitelistClasses :+ base: _*))
       .map(_.blacklistPackages(disabledPackages: _*))
       .map(_.enableMethodInfo())
       .map(if (debug) _.verbose() else identity)
diff --git a/distage/distage-framework/src/main/scala/izumi/distage/framework/model/BootstrapConfig.scala b/distage/distage-framework/src/main/scala/izumi/distage/framework/model/BootstrapConfig.scala
index dd5622ad1..723e72647 100644
--- a/distage/distage-framework/src/main/scala/izumi/distage/framework/model/BootstrapConfig.scala
+++ b/distage/distage-framework/src/main/scala/izumi/distage/framework/model/BootstrapConfig.scala
@@ -7,5 +7,5 @@ final case class BootstrapConfig(
                                   bootstrapPluginConfig: Option[PluginConfig] = None,
                                 )
 object BootstrapConfig {
-  def empty = BootstrapConfig(PluginConfig.empty, None)
+  def empty: BootstrapConfig = BootstrapConfig(PluginConfig.empty, None)
 }
diff --git a/distage/distage-framework/src/main/scala/izumi/distage/staticinjector/plugins/macros/StaticPluginCheckerMacro.scala b/distage/distage-framework/src/main/scala/izumi/distage/staticinjector/plugins/macros/StaticPluginCheckerMacro.scala
index dc46145ac..b110424e9 100644
--- a/distage/distage-framework/src/main/scala/izumi/distage/staticinjector/plugins/macros/StaticPluginCheckerMacro.scala
+++ b/distage/distage-framework/src/main/scala/izumi/distage/staticinjector/plugins/macros/StaticPluginCheckerMacro.scala
@@ -67,12 +67,7 @@ object StaticPluginCheckerMacro {
     val loadedPlugins = if (pluginPath == "") {
       Seq.empty
     } else {
-      val pluginLoader = new PluginLoaderDefaultImpl(PluginConfig(
-        debug = false,
-        packagesEnabled = Seq(pluginPath),
-        packagesDisabled = Seq.empty,
-      ))
-
+      val pluginLoader = new PluginLoaderDefaultImpl(PluginConfig.packages(Seq(pluginPath)))
       pluginLoader.load()
     }
 
diff --git a/project/project/PluginVersions.scala b/project/project/PluginVersions.scala
index 2b0c5c45e..642b282a8 100644
--- a/project/project/PluginVersions.scala
+++ b/project/project/PluginVersions.scala
@@ -5,7 +5,7 @@ object PV {
   val sbt_site = "1.3.3"
   val sbt_unidoc = "0.4.2"
   val sbt_scoverage = "1.6.1"
-  val sbt_pgp = "2.0.1-M3"
+  val sbt_pgp = "2.0.1"
   val sbt_assembly = "0.14.9"
 
   val scala_js_version = "0.6.29"


Test patch:
diff --git a/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/DebugProperties.scala b/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/DebugProperties.scala
similarity index 89%
rename from distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/DebugProperties.scala
rename to distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/DebugProperties.scala
index 2a4cb1914..09490cab1 100644
--- a/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/DebugProperties.scala
+++ b/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/DebugProperties.scala
@@ -1,4 +1,4 @@
-package izumi.distage.testkit
+package izumi.distage.plugins
 
 import izumi.fundamentals.platform.logging.DebugProperties
 
diff --git a/distage/distage-framework-docker/src/test/scala/izumi/distage/testkit/docker/DistageTestDockerBIO.scala b/distage/distage-framework-docker/src/test/scala/izumi/distage/testkit/docker/DistageTestDockerBIO.scala
index 5acdc3e6a..434150969 100644
--- a/distage/distage-framework-docker/src/test/scala/izumi/distage/testkit/docker/DistageTestDockerBIO.scala
+++ b/distage/distage-framework-docker/src/test/scala/izumi/distage/testkit/docker/DistageTestDockerBIO.scala
@@ -31,7 +31,7 @@ final class DistageTestDockerBIO extends DistageBIOSpecScalatest[IO] {
   }
 
   override protected def config: TestConfig = {
-    TestConfig(
+    super.config.copy(
       memoizationRoots = Set(
         DIKey.get[DynamoDocker.Container],
         DIKey.get[PostgresDocker.Container],
diff --git a/distage/distage-framework/src/test/scala/izumi/distage/roles/test/RoleAppTest.scala b/distage/distage-framework/src/test/scala/izumi/distage/roles/test/RoleAppTest.scala
index 427c29d5f..db1368f4c 100644
--- a/distage/distage-framework/src/test/scala/izumi/distage/roles/test/RoleAppTest.scala
+++ b/distage/distage-framework/src/test/scala/izumi/distage/roles/test/RoleAppTest.scala
@@ -213,10 +213,10 @@ class RoleAppTest extends WordSpec
       val roleCfg = cfg("testrole00", version)
       val roleCfgMin = cfg("testrole00-minimized", version)
 
-      assert(cwCfg.exists())
-      assert(cwCfgMin.exists())
-      assert(roleCfg.exists())
-      assert(roleCfgMin.exists())
+      assert(cwCfg.exists(), s"$cwCfg exists")
+      assert(cwCfgMin.exists(), s"$cwCfgMin exists")
+      assert(roleCfg.exists(), s"$roleCfg exists")
+      assert(roleCfgMin.exists(), s"$roleCfgMin exists")
 
       assert(cwCfg.length() > cwCfgMin.length())
       assert(roleCfg.length() > roleCfgMin.length())
diff --git a/distage/distage-framework/src/test/scala/izumi/distage/roles/test/TestLauncher.scala b/distage/distage-framework/src/test/scala/izumi/distage/roles/test/TestLauncher.scala
index 853949f4e..3e8c1265b 100644
--- a/distage/distage-framework/src/test/scala/izumi/distage/roles/test/TestLauncher.scala
+++ b/distage/distage-framework/src/test/scala/izumi/distage/roles/test/TestLauncher.scala
@@ -9,11 +9,7 @@ import izumi.fundamentals.platform.language.SourcePackageMaterializer.thisPkg
 
 class TestLauncher extends RoleAppLauncher.LauncherF[IO] {
   override protected def pluginSource: PluginSource = PluginSource(
-    PluginConfig(
-      debug = false,
-      packagesEnabled = Seq(s"$thisPkg.fixtures"),
-      packagesDisabled = Seq.empty,
-    )
+    PluginConfig.cached(Seq(s"$thisPkg.fixtures"))
   )
   override protected val shutdownStrategy: AppShutdownStrategy[IO] = new ImmediateExitShutdownStrategy()
 }
diff --git a/distage/distage-framework/src/test/scala/izumi/distage/roles/test/WithProperties.scala b/distage/distage-framework/src/test/scala/izumi/distage/roles/test/WithProperties.scala
index 4f9e294ae..d0532c56e 100644
--- a/distage/distage-framework/src/test/scala/izumi/distage/roles/test/WithProperties.scala
+++ b/distage/distage-framework/src/test/scala/izumi/distage/roles/test/WithProperties.scala
@@ -7,7 +7,7 @@ trait WithProperties {
     withProperties(properties.toSeq :_*)(f)
   }
 
-  final def withProperties(properties: (String, String)*)(f: => Unit): Unit = {
+  final def withProperties(properties: (String, String)*)(f: => Unit): Unit = synchronized {
     try {
       properties.foreach {
         case (k, v) =>
diff --git a/distage/distage-framework/src/test/scala/izumi/distage/roles/test/fixtures/Fixture.scala b/distage/distage-framework/src/test/scala/izumi/distage/roles/test/fixtures/Fixture.scala
index 61bff0365..b635b84f7 100644
--- a/distage/distage-framework/src/test/scala/izumi/distage/roles/test/fixtures/Fixture.scala
+++ b/distage/distage-framework/src/test/scala/izumi/distage/roles/test/fixtures/Fixture.scala
@@ -2,8 +2,9 @@ package izumi.distage.roles.test.fixtures
 
 import izumi.distage.framework.model.IntegrationCheck
 import izumi.fundamentals.platform.integration.ResourceCheck
+import izumi.fundamentals.platform.language.Quirks._
 
-import scala.collection.{immutable, mutable}
+import scala.collection.mutable
 
 object Fixture {
   trait Dummy
@@ -25,9 +26,9 @@ object Fixture {
     private val closedCloseables: mutable.ArrayBuffer[AutoCloseable] = mutable.ArrayBuffer()
     private val checkedResources: mutable.ArrayBuffer[IntegrationCheck] = mutable.ArrayBuffer()
 
-    def onStart(c: AutoCloseable): Unit = this.synchronized(startedCloseables += c)
-    def onClose(c: AutoCloseable): Unit = this.synchronized(closedCloseables += c)
-    def onCheck(c: IntegrationCheck): Unit = this.synchronized(checkedResources += c)
+    def onStart(c: AutoCloseable): Unit = this.synchronized(startedCloseables += c).discard()
+    def onClose(c: AutoCloseable): Unit = this.synchronized(closedCloseables += c).discard()
+    def onCheck(c: IntegrationCheck): Unit = this.synchronized(checkedResources += c).discard()
 
     def getStartedCloseables(): Seq[AutoCloseable] = this.synchronized(startedCloseables.toList)
     def getClosedCloseables(): Seq[AutoCloseable] = this.synchronized(closedCloseables.toList)
diff --git a/distage/distage-framework/src/test/scala/izumi/distage/roles/test/fixtures/TestPlugin.scala b/distage/distage-framework/src/test/scala/izumi/distage/roles/test/fixtures/TestPlugin.scala
index d105471bf..9f7c1bcfd 100644
--- a/distage/distage-framework/src/test/scala/izumi/distage/roles/test/fixtures/TestPlugin.scala
+++ b/distage/distage-framework/src/test/scala/izumi/distage/roles/test/fixtures/TestPlugin.scala
@@ -17,7 +17,7 @@ import izumi.fundamentals.reflection.Tags.TagK
 class TestPluginBase[F[_]: TagK] extends CatsDIEffectModule with PluginDef with ConfigModuleDef {
   tag(Env.Prod)
 
-  private val version = Option(System.getProperty(TestPlugin.versionProperty)) match {
+  private def version = Option(System.getProperty(TestPlugin.versionProperty)) match {
     case Some(value) =>
       value
     case None =>
diff --git a/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/TestConfig.scala b/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/TestConfig.scala
index 28c8bbd2d..4f37ac389 100644
--- a/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/TestConfig.scala
+++ b/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/TestConfig.scala
@@ -1,11 +1,11 @@
 package izumi.distage.testkit
 
-import distage.{BootstrapModule, DIKey, Module, StandardAxis, Activation}
+import distage.{Activation, BootstrapModule, DIKey, Module, StandardAxis}
 import izumi.distage.framework.model.PluginSource
+import izumi.distage.plugins.PluginConfig
 
 /**
-  * @param pluginSource       If `None`, recursively scans packages of the test class itself
-  *                           each pluginSource creates a distinct memoization group, i.e.
+  * @param pluginSource       Each pluginSource creates a distinct memoization group, i.e.
   *                           objects will be memoized only between tests with the same plugins
   * @param activation         Chosen configurations. Different Activations have distinct memoization groups
   * @param memoizationRoots   Every distinct set of `memoizationRoots` will have a distinct memoization group
@@ -16,9 +16,13 @@ import izumi.distage.framework.model.PluginSource
   * @param bootstrapOverrides Same as [[moduleOverrides]], but for [[BootstrapModule]]
   */
 final case class TestConfig(
-                             pluginSource: Option[PluginSource] = None,
+                             pluginSource: PluginSource,
                              activation: Activation = StandardAxis.testProdActivation,
                              memoizationRoots: Set[DIKey] = Set.empty,
                              moduleOverrides: Module = Module.empty,
                              bootstrapOverrides: BootstrapModule = BootstrapModule.empty,
                            )
+
+object TestConfig {
+  def forSuite(clazz: Class[_]): TestConfig = TestConfig(PluginSource(PluginConfig.cached(Seq(this.getClass.getPackage.getName)), None))
+}
\ No newline at end of file
diff --git a/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/PluginsCache.scala b/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/PluginsCache.scala
deleted file mode 100644
index 58c0721d7..000000000
--- a/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/PluginsCache.scala
+++ /dev/null
@@ -1,19 +0,0 @@
-package izumi.distage.testkit.services
-
-import izumi.distage.framework.model.{AllLoadedPlugins, BootstrapConfig, PluginSource}
-
-object PluginsCache {
-  final case class CacheKey(config: BootstrapConfig)
-  final case class CacheValue(plugins: AllLoadedPlugins)
-
-  // sbt in nofork mode runs each module in it's own classloader thus we have separate cache per module per run
-  object Instance extends SyncCache[CacheKey, CacheValue]
-
-  def cachePluginSource(pluginSource: PluginSource): PluginSource = {
-    pluginSource.replaceLoaders {
-      case p@PluginSource.Load(_, _, Some(bootstrapConfig)) =>
-        PluginSource(Instance.getOrCompute(CacheKey(bootstrapConfig), CacheValue(p.load())).plugins)
-      case p => p
-    }
-  }
-}
diff --git a/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/dstest/DebugProperties.scala b/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/dstest/DebugProperties.scala
new file mode 100644
index 000000000..5b116330a
--- /dev/null
+++ b/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/dstest/DebugProperties.scala
@@ -0,0 +1,8 @@
+package izumi.distage.testkit.services.dstest
+
+import izumi.fundamentals.platform.logging.DebugProperties
+
+object DebugProperties extends DebugProperties {
+  /** Scan classpath only once per test run */
+  final val `izumi.distage.testkit.environment.memoize` = "izumi.distage.testkit.environment.memoize"
+}
diff --git a/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/dstest/DistageTestEnv.scala b/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/dstest/DistageTestEnv.scala
new file mode 100644
index 000000000..cd7a62f1d
--- /dev/null
+++ b/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/dstest/DistageTestEnv.scala
@@ -0,0 +1,74 @@
+package izumi.distage.testkit.services.dstest
+
+import izumi.distage.framework.activation.PruningPlanMergingPolicyLoggedImpl
+import izumi.distage.framework.model.ActivationInfo
+import izumi.distage.framework.services.ActivationInfoExtractor
+import izumi.distage.model.definition.{Activation, BootstrapModuleDef}
+import izumi.distage.model.planning.PlanMergingPolicy
+import izumi.distage.plugins.merge.{PluginMergeStrategy, SimplePluginMergeStrategy}
+import izumi.distage.roles.model.meta.RolesInfo
+import izumi.distage.testkit.TestConfig
+import izumi.fundamentals.platform.cache.SyncCache
+import izumi.fundamentals.platform.language.unused
+import izumi.logstage.api.IzLogger
+
+
+trait DistageTestEnv {
+  protected[distage] def loadEnvironment(logger: IzLogger, testConfig: TestConfig): TestEnvironment = {
+    val roles = loadRoles(logger)
+    val mergeStrategy = makeMergeStrategy(logger)
+
+    import izumi.fundamentals.platform.strings.IzString._
+    def doMake(): TestEnvironment = {
+      makeEnv(logger, testConfig, roles, mergeStrategy)
+    }
+
+    if (DebugProperties.`izumi.distage.testkit.environment.memoize`.asBoolean(true)) {
+      DistageTestEnv.cache.getOrCompute(DistageTestEnv.EnvCacheKey(testConfig, roles, mergeStrategy), doMake())
+    } else {
+      doMake()
+    }
+  }
+
+  private def makeEnv(logger: IzLogger, testConfig: TestConfig, roles: RolesInfo, mergeStrategy: PluginMergeStrategy): TestEnvironment = {
+    val plugins = testConfig.pluginSource.load()
+    val appModule = mergeStrategy.merge(plugins.app)
+    val bootstrapModule = mergeStrategy.merge(plugins.bootstrap)
+    val availableActivations = ActivationInfoExtractor.findAvailableChoices(logger, appModule)
+
+    val baseEnv = TestEnvironment(
+      bsModule = bootstrapModule,
+      appModule = appModule,
+      roles = roles,
+      activationInfo = availableActivations,
+      activation = testConfig.activation,
+      memoizedKeys = Set.empty,
+    )
+
+    val bsModule = baseEnv.bsModule overridenBy new BootstrapModuleDef {
+      make[PlanMergingPolicy].from[PruningPlanMergingPolicyLoggedImpl]
+      make[ActivationInfo].fromValue(baseEnv.activationInfo)
+      make[Activation].fromValue(baseEnv.activation)
+    }
+
+    baseEnv.copy(bsModule = bsModule)
+  }
+
+  protected def loadRoles(@unused logger: IzLogger): RolesInfo = {
+    // For all normal scenarios we don't need roles to setup a test
+    RolesInfo(Set.empty, Seq.empty, Seq.empty, Seq.empty, Set.empty)
+  }
+
+  protected def makeMergeStrategy(@unused lateLogger: IzLogger): PluginMergeStrategy = {
+    SimplePluginMergeStrategy
+  }
+
+}
+
+object DistageTestEnv {
+
+  private lazy val cache = new SyncCache[EnvCacheKey, TestEnvironment]
+
+  case class EnvCacheKey(config: TestConfig, rolesInfo: RolesInfo, mergeStrategy: PluginMergeStrategy)
+
+}
\ No newline at end of file
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 362f10ce3..79e5c0d30 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
@@ -256,7 +256,7 @@ object DistageTestRunner {
 
   sealed trait TestStatus
   object TestStatus {
-    case object Scheduled extends TestStatus
+//    case object Scheduled extends TestStatus
     case object Running extends TestStatus
 
     sealed trait Done extends TestStatus
diff --git a/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/dstest/TestEnvironment.scala b/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/dstest/TestEnvironment.scala
index ef86a1ea1..29180dfb3 100644
--- a/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/dstest/TestEnvironment.scala
+++ b/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/dstest/TestEnvironment.scala
@@ -1,10 +1,8 @@
 package izumi.distage.testkit.services.dstest
 
 import distage._
-import izumi.distage.framework.activation.PruningPlanMergingPolicyLoggedImpl
 import izumi.distage.framework.model.ActivationInfo
 import izumi.distage.model.definition.Activation
-import izumi.distage.model.planning.PlanMergingPolicy
 import izumi.distage.roles.model.meta.RolesInfo
 
 final case class TestEnvironment(
diff --git a/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/dstest/TestEnvironmentProvider.scala b/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/dstest/TestEnvironmentProvider.scala
deleted file mode 100644
index 7be9371d2..000000000
--- a/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/dstest/TestEnvironmentProvider.scala
+++ /dev/null
@@ -1,75 +0,0 @@
-package izumi.distage.testkit.services.dstest
-
-import distage.DIKey
-import izumi.distage.framework.model.{ActivationInfo, PluginSource}
-import izumi.distage.framework.services.ActivationInfoExtractor
-import izumi.distage.model.definition.{Activation, BootstrapModule, ModuleBase}
-import izumi.distage.plugins.merge.{PluginMergeStrategy, SimplePluginMergeStrategy}
-import izumi.distage.roles.model.meta.RolesInfo
-import izumi.distage.testkit.DebugProperties
-import izumi.distage.testkit.services.PluginsCache
-import izumi.fundamentals.platform.language.unused
-import izumi.logstage.api.IzLogger
-
-trait TestEnvironmentProvider {
-  /** Merge strategy will be applied only once for all the tests with the same BootstrapConfig, if memoization is on */
-  def loadEnvironment(logger: IzLogger): TestEnvironment
-}
-
-object TestEnvironmentProvider {
-
-  class Impl
-  (
-    protected val pluginSource: PluginSource,
-    protected val activation: Activation,
-    protected val memoizedKeys: Set[DIKey],
-    protected val bootstrapOverrides: BootstrapModule,
-    protected val moduleOverrides: ModuleBase,
-  ) extends TestEnvironmentProvider {
-
-    override final def loadEnvironment(logger: IzLogger): TestEnvironment = {
-      val cachedSource = if (memoizePlugins) {
-        PluginsCache.cachePluginSource(pluginSource)
-      } else pluginSource
-
-      val plugins = cachedSource.load()
-      val mergeStrategy = makeMergeStrategy(logger)
-      val appModule = mergeStrategy.merge(plugins.app)
-      val bootstrapModule = mergeStrategy.merge(plugins.bootstrap)
-      val availableActivations = ActivationInfoExtractor.findAvailableChoices(logger, appModule)
-
-      doLoad(logger, appModule, bootstrapModule, availableActivations)
-    }
-
-    protected final def doLoad(logger: IzLogger, appModule: ModuleBase, bootstrapModule: ModuleBase, availableActivations: ActivationInfo): TestEnvironment = {
-      val roles = loadRoles(logger)
-      TestEnvironment(
-        bsModule = bootstrapModule overridenBy bootstrapOverrides,
-        appModule = appModule overridenBy moduleOverrides,
-        roles = roles,
-        activationInfo = availableActivations,
-        activation = activation,
-        memoizedKeys = memoizedKeys,
-      )
-    }
-
-    protected def memoizePlugins: Boolean = {
-      import izumi.fundamentals.platform.strings.IzString._
-
-      System
-        .getProperty(DebugProperties.`izumi.distage.testkit.plugins.memoize`)
-        .asBoolean(true)
-    }
-
-    protected def loadRoles(@unused logger: IzLogger): RolesInfo = {
-      // For all normal scenarios we don't need roles to setup a test
-      RolesInfo(Set.empty, Seq.empty, Seq.empty, Seq.empty, Set.empty)
-    }
-
-    protected def makeMergeStrategy(@unused lateLogger: IzLogger): PluginMergeStrategy = {
-      SimplePluginMergeStrategy
-    }
-
-  }
-
-}
diff --git a/distage/distage-testkit-legacy/src/main/scala/izumi/distage/testkit/services/scalatest/adapter/DistagePluginTestSupport.scala b/distage/distage-testkit-legacy/src/main/scala/izumi/distage/testkit/services/scalatest/adapter/DistagePluginTestSupport.scala
index ac4ee2ae7..a979cadb6 100644
--- a/distage/distage-testkit-legacy/src/main/scala/izumi/distage/testkit/services/scalatest/adapter/DistagePluginTestSupport.scala
+++ b/distage/distage-testkit-legacy/src/main/scala/izumi/distage/testkit/services/scalatest/adapter/DistagePluginTestSupport.scala
@@ -1,24 +1,19 @@
 package izumi.distage.testkit.services.scalatest.adapter
 
 import distage.SafeType
-import izumi.distage.framework.activation.PruningPlanMergingPolicyLoggedImpl
-import izumi.distage.framework.model.{ActivationInfo, BootstrapConfig, PluginSource}
-import izumi.distage.framework.services.ActivationInfoExtractor
+import izumi.distage.framework.model.{BootstrapConfig, PluginSource}
+import izumi.distage.model.definition.Activation
 import izumi.distage.model.definition.StandardAxis._
-import izumi.distage.model.definition.{Activation, BootstrapModuleDef, ModuleBase}
-import izumi.distage.model.planning.PlanMergingPolicy
 import izumi.distage.plugins.PluginConfig
-import izumi.distage.plugins.merge.{PluginMergeStrategy, SimplePluginMergeStrategy}
-import izumi.distage.roles.model.meta.RolesInfo
-import izumi.distage.testkit.DebugProperties
-import izumi.distage.testkit.services.PluginsCache
-import izumi.distage.testkit.services.dstest.TestEnvironment
-import izumi.fundamentals.platform.language.unused
+import izumi.distage.testkit.TestConfig
+import izumi.distage.testkit.services.dstest.{DistageTestEnv, TestEnvironment}
 import izumi.fundamentals.reflection.Tags.TagK
 import izumi.logstage.api.IzLogger
 
+
+
 @deprecated("Use dstest", "2019/Jul/18")
-abstract class DistagePluginTestSupport[F[_] : TagK] extends DistageTestSupport[F] {
+abstract class DistagePluginTestSupport[F[_] : TagK] extends DistageTestSupport[F] with DistageTestEnv {
 
   /**
     * This may be used as an implementation of [[pluginPackages]] in simple cases.
@@ -36,47 +31,8 @@ abstract class DistagePluginTestSupport[F[_] : TagK] extends DistageTestSupport[
 
   override final def loadEnvironment(logger: IzLogger): TestEnvironment = {
     val pluginSource = PluginSource(bootstrapConfig)
-    val cachedSource = if (memoizePlugins) {
-      PluginsCache.cachePluginSource(pluginSource)
-    } else pluginSource
-
-    val plugins = cachedSource.load()
-    val mergeStrategy = makeMergeStrategy(logger)
-    val appModule = mergeStrategy.merge(plugins.app)
-    val bootstrapModule = mergeStrategy.merge(plugins.bootstrap)
-    val availableActivations = ActivationInfoExtractor.findAvailableChoices(logger, appModule)
-
-    doLoad(logger, appModule, bootstrapModule, availableActivations)
-  }
-
-  protected final def doLoad(logger: IzLogger, appModule: ModuleBase, bootstrapModule: ModuleBase, availableActivations: ActivationInfo): TestEnvironment = {
-    val roles = loadRoles(logger)
-    val bsModule = bootstrapModule overridenBy new BootstrapModuleDef {
-      make[PlanMergingPolicy].from[PruningPlanMergingPolicyLoggedImpl]
-      make[ActivationInfo].fromValue(availableActivations)
-      make[Activation].fromValue(activation)
-    }
-    TestEnvironment(
-      bsModule = bsModule,
-      appModule = appModule,
-      roles = roles,
-      activationInfo = availableActivations,
-      activation = activation,
-      memoizedKeys = Set.empty,
-    )
-  }
-
-  protected def memoizePlugins: Boolean = {
-    import izumi.fundamentals.platform.strings.IzString._
-
-    System
-      .getProperty(DebugProperties.`izumi.distage.testkit.plugins.memoize`)
-      .asBoolean(true)
-  }
-
-  protected def loadRoles(@unused logger: IzLogger): RolesInfo = {
-    // For all normal scenarios we don't need roles to setup a test
-    RolesInfo(Set.empty, Seq.empty, Seq.empty, Seq.empty, Set.empty)
+    val config = TestConfig(pluginSource, activation)
+    loadEnvironment(logger, config)
   }
 
   protected def activation: Activation = {
@@ -87,14 +43,10 @@ abstract class DistagePluginTestSupport[F[_] : TagK] extends DistageTestSupport[
     MemoizationContextId.PerRuntimeAndActivationAndBsconfig[F](bootstrapConfig, activation, SafeType.getK[F])
   }
 
-  protected def makeMergeStrategy(@unused lateLogger: IzLogger): PluginMergeStrategy = {
-    SimplePluginMergeStrategy
-  }
-
   protected def bootstrapConfig: BootstrapConfig = {
     BootstrapConfig(
-      PluginConfig(debug = false, pluginPackages, Seq.empty),
-      pluginBootstrapPackages.map(p => PluginConfig(debug = false, p, Seq.empty)),
+      PluginConfig.cached(pluginPackages),
+      pluginBootstrapPackages.map(p => PluginConfig.cached(p)),
     )
   }
 
diff --git a/distage/distage-testkit-legacy/src/main/scala/izumi/distage/testkit/services/scalatest/adapter/ExternalResourceProvider.scala b/distage/distage-testkit-legacy/src/main/scala/izumi/distage/testkit/services/scalatest/adapter/ExternalResourceProvider.scala
index 9faea89e1..998ebc931 100644
--- a/distage/distage-testkit-legacy/src/main/scala/izumi/distage/testkit/services/scalatest/adapter/ExternalResourceProvider.scala
+++ b/distage/distage-testkit-legacy/src/main/scala/izumi/distage/testkit/services/scalatest/adapter/ExternalResourceProvider.scala
@@ -7,8 +7,8 @@ import izumi.distage.model.definition.{DIResource, LocatorDef}
 import izumi.distage.model.effect.{DIEffect, DIEffectAsync, DIEffectRunner}
 import izumi.distage.model.provisioning.PlanInterpreter.Finalizer
 import izumi.distage.model.references.IdentifiedRef
-import izumi.distage.testkit.services.SyncCache
 import izumi.distage.testkit.services.scalatest.adapter.ExternalResourceProvider.{MemoizedInstance, PreparedShutdownRuntime}
+import izumi.fundamentals.platform.cache.SyncCache
 import izumi.fundamentals.platform.console.TrivialLogger
 import izumi.fundamentals.platform.functional.Identity
 import izumi.fundamentals.platform.language.Quirks
diff --git a/distage/distage-testkit-scalatest/src/main/scala/izumi/distage/testkit/scalatest/DistageBIOSpecScalatest.scala b/distage/distage-testkit-scalatest/src/main/scala/izumi/distage/testkit/scalatest/DistageBIOSpecScalatest.scala
index 442200532..4ae5011b2 100644
--- a/distage/distage-testkit-scalatest/src/main/scala/izumi/distage/testkit/scalatest/DistageBIOSpecScalatest.scala
+++ b/distage/distage-testkit-scalatest/src/main/scala/izumi/distage/testkit/scalatest/DistageBIOSpecScalatest.scala
@@ -1,7 +1,7 @@
 package izumi.distage.testkit.scalatest
 
 import distage.{TagK, TagKK}
-import izumi.distage.testkit.services.scalatest.dstest.DistageAbstractScalatestSpec
+import izumi.distage.testkit.services.scalatest.dstest.{DistageAbstractScalatestSpec, DistageTestsRegistrySingleton}
 import izumi.distage.testkit.services.scalatest.dstest.DistageAbstractScalatestSpec.DSWordSpecStringWrapper2
 import org.scalatest.DistageScalatestTestSuiteRunner
 
diff --git a/distage/distage-testkit-scalatest/src/main/scala/izumi/distage/testkit/scalatest/DistageSpecScalatest.scala b/distage/distage-testkit-scalatest/src/main/scala/izumi/distage/testkit/scalatest/DistageSpecScalatest.scala
index 088afb708..41b9b6cc9 100644
--- a/distage/distage-testkit-scalatest/src/main/scala/izumi/distage/testkit/scalatest/DistageSpecScalatest.scala
+++ b/distage/distage-testkit-scalatest/src/main/scala/izumi/distage/testkit/scalatest/DistageSpecScalatest.scala
@@ -1,7 +1,7 @@
 package izumi.distage.testkit.scalatest
 
 import distage.TagK
-import izumi.distage.testkit.services.scalatest.dstest.DistageAbstractScalatestSpec
+import izumi.distage.testkit.services.scalatest.dstest.{DistageAbstractScalatestSpec, DistageTestsRegistrySingleton}
 import org.scalatest.DistageScalatestTestSuiteRunner
 
 abstract class DistageSpecScalatest[F[_]]()(implicit val tagMonoIO: TagK[F])
diff --git a/distage/distage-testkit-scalatest/src/main/scala/izumi/distage/testkit/services/scalatest/dstest/DistageAbstractScalatestSpec.scala b/distage/distage-testkit-scalatest/src/main/scala/izumi/distage/testkit/services/scalatest/dstest/DistageAbstractScalatestSpec.scala
index 98290d050..454a79e79 100644
--- a/distage/distage-testkit-scalatest/src/main/scala/izumi/distage/testkit/services/scalatest/dstest/DistageAbstractScalatestSpec.scala
+++ b/distage/distage-testkit-scalatest/src/main/scala/izumi/distage/testkit/services/scalatest/dstest/DistageAbstractScalatestSpec.scala
@@ -1,10 +1,8 @@
 package izumi.distage.testkit.services.scalatest.dstest
 
 import distage.{Tag, TagK, TagKK}
-import izumi.distage.framework.model.PluginSource
 import izumi.distage.model.effect.DIEffect
 import izumi.distage.model.providers.ProviderMagnet
-import izumi.distage.plugins.PluginConfig
 import izumi.distage.testkit.TestConfig
 import izumi.distage.testkit.services.dstest.DistageTestRunner.{DistageTest, TestId, TestMeta}
 import izumi.distage.testkit.services.dstest._
@@ -23,31 +21,19 @@ trait WithSingletonTestRegistration[F[_]] extends AbstractDistageSpec[F] {
   }
 }
 
-trait DistageAbstractScalatestSpec[F[_]] extends ScalatestWords with WithSingletonTestRegistration[F] {
+trait DistageAbstractScalatestSpec[F[_]] extends ScalatestWords with WithSingletonTestRegistration[F] with DistageTestEnv {
   this: AbstractDistageSpec[F] =>
 
-  private[this] lazy val testEnvProvider: TestEnvironmentProvider = makeTestEnvProvider()
   final protected lazy val testEnv: TestEnvironment = makeTestEnv()
 
-  protected def makeTestEnvProvider(): TestEnvironmentProvider = {
-    val conf = config
-    val pluginSource = conf.pluginSource
-      .getOrElse(PluginSource(PluginConfig(packagesEnabled = Seq(this.getClass.getPackage.getName)), None))
-
-    new TestEnvironmentProvider.Impl(
-      pluginSource,
-      conf.activation,
-      conf.memoizationRoots,
-      conf.bootstrapOverrides,
-      conf.moduleOverrides,
-    )
+  protected def makeTestEnv(): TestEnvironment = {
+    loadEnvironment(logger, config)
   }
-  protected def makeTestEnv(): TestEnvironment = testEnvProvider.loadEnvironment(logger)
 
   protected def distageSuiteName: String = getSimpleNameOfAnObjectsClass(this)
   protected def distageSuiteId: String = this.getClass.getName
 
-  protected def config: TestConfig = TestConfig()
+  protected def config: TestConfig = TestConfig.forSuite(this.getClass)
 
   protected def logger: IzLogger = IzLogger(Log.Level.Debug)("phase" -> "test")
 
diff --git a/distage/distage-testkit-scalatest/src/main/scala/izumi/distage/testkit/services/scalatest/dstest/DistageTestsRegistrySingleton.scala b/distage/distage-testkit-scalatest/src/main/scala/izumi/distage/testkit/services/scalatest/dstest/DistageTestsRegistrySingleton.scala
index 611f8270d..4fc9ce147 100644
--- a/distage/distage-testkit-scalatest/src/main/scala/izumi/distage/testkit/services/scalatest/dstest/DistageTestsRegistrySingleton.scala
+++ b/distage/distage-testkit-scalatest/src/main/scala/izumi/distage/testkit/services/scalatest/dstest/DistageTestsRegistrySingleton.scala
@@ -5,12 +5,15 @@ import java.util.concurrent.ConcurrentHashMap
 import distage.{SafeType, TagK}
 import izumi.distage.testkit.services.dstest.DistageTestRunner.DistageTest
 import izumi.fundamentals.platform.language.Quirks._
+import org.scalatest.{StatefulStatus, Tracker}
 
 import scala.collection.mutable
 
 object DistageTestsRegistrySingleton {
   private[DistageTestsRegistrySingleton] type Fake[T]
   private[this] val registry = new mutable.HashMap[SafeType, mutable.ArrayBuffer[DistageTest[Fake]]]()
+  private[this] val statuses = new mutable.HashMap[String, StatefulStatus]()
+  private[this] val trackers = new mutable.HashMap[String, Either[mutable.ArrayBuffer[Tracker => Unit], Tracker]]()
   private[this] val runTracker = new ConcurrentHashMap[SafeType, Boolean]()
 
   def list[F[_]: TagK]: Seq[DistageTest[F]] = synchronized {
@@ -25,4 +28,31 @@ object DistageTestsRegistrySingleton {
     val tpe = SafeType.getK[F]
     !runTracker.putIfAbsent(tpe, true)
   }
+
+  def registerStatus(suiteId: String, status: StatefulStatus): Unit = synchronized {
+    statuses(suiteId) = status
+  }
+
+  def completeStatuses(suiteIds: Set[String]): Unit = synchronized {
+    suiteIds.foreach(statuses.get(_).foreach(_.setCompleted()))
+  }
+
+  def runReport(suiteId: String)(f: Tracker => Unit): Unit = synchronized {
+    trackers.getOrElseUpdate(suiteId, Left(mutable.ArrayBuffer.empty)) match {
+      case Left(reports) =>
+        reports += f
+      case Right(tracker) =>
+        f(tracker)
+    }
+    ()
+  }
+
+  def registerTracker(suiteId: String)(tracker: Tracker): Unit = synchronized {
+    trackers.getOrElseUpdate(suiteId, Right(tracker)) match {
+      case Left(reports) =>
+        trackers(suiteId) = Right(tracker)
+        reports.foreach(_.apply(tracker))
+      case Right(_) =>
+    }
+  }
 }
diff --git a/distage/distage-testkit-scalatest/src/main/scala/org/scalatest/DistageScalatestTestSuiteRunner.scala b/distage/distage-testkit-scalatest/src/main/scala/org/scalatest/DistageScalatestTestSuiteRunner.scala
index 98bece712..3d197c0d2 100644
--- a/distage/distage-testkit-scalatest/src/main/scala/org/scalatest/DistageScalatestTestSuiteRunner.scala
+++ b/distage/distage-testkit-scalatest/src/main/scala/org/scalatest/DistageScalatestTestSuiteRunner.scala
@@ -7,7 +7,6 @@ import izumi.distage.testkit.services.dstest.DistageTestRunner._
 import izumi.distage.testkit.services.dstest.{AbstractDistageSpec, DistageTestRunner, SpecEnvironment}
 import izumi.distage.testkit.services.scalatest.dstest.DistageTestsRegistrySingleton
 import izumi.logstage.api.{IzLogger, Log}
-import org.scalatest.events._
 import org.scalatest.exceptions.TestCanceledException
 
 import scala.collection.immutable.TreeSet
@@ -57,25 +56,20 @@ trait DistageScalatestTestSuiteRunner[F[_]] extends Suite with AbstractDistageSp
     DistageTestsRegistrySingleton.list[F]
   }
 
-  override def run(testName: Option[String], args: Args): Status = {
-    val status = new StatefulStatus
+  private[this] val status = new StatefulStatus
+  DistageTestsRegistrySingleton.registerStatus(suiteId, status)
 
+  override def run(testName: Option[String], args: Args): Status = {
+    DistageTestsRegistrySingleton.registerTracker(suiteId)(args.tracker)
     try {
       if (DistageTestsRegistrySingleton.ticketToProceed[F]()) {
         doRun(testName, args)
-      } else {
-        addStub(args, None)
       }
     } catch {
       case t: Throwable =>
-        // IDEA reporter is insane
-        //status.setFailedWith(t)
-        addStub(args, Some(t))
-
-    } finally {
-      status.setCompleted()
+        status.setFailedWith(t)
+        status.setCompleted()
     }
-
     status
   }
 
@@ -91,17 +85,17 @@ trait DistageScalatestTestSuiteRunner[F[_]] extends Suite with AbstractDistageSp
     val testTags: Set[String] = Set.empty
 
     new TestData {
-      val configMap: ConfigMap = theConfigMap
-      val name: String = testName
-      val scopes: Vector[Nothing] = Vector.empty
-      val text: String = testName
-      val tags: Set[String] = Set.empty ++ suiteTags ++ testTags
-      val pos: None.type = None
+      override val configMap: ConfigMap = theConfigMap
+      override val name: String = testName
+      override val scopes: Vector[Nothing] = Vector.empty
+      override val text: String = testName
+      override val tags: Set[String] = Set.empty ++ suiteTags ++ testTags
+      override val pos: None.type = None
     }
   }
 
   private def doRun(testName: Option[String], args: Args): Unit = {
-    val dreporter = mkTestReporter(args)
+    val testReporter = mkTestReporter(args)
 
     val toRun = testName match {
       case None =>
@@ -134,54 +128,24 @@ trait DistageScalatestTestSuiteRunner[F[_]] extends Suite with AbstractDistageSp
     val runner = {
       val logger = IzLogger(Log.Level.Debug)("phase" -> "test")
       val checker = new IntegrationChecker.Impl(logger)
-      new DistageTestRunner[F](dreporter, checker, specEnv, toRun, _.isInstanceOf[TestCanceledException], parallelTestsAlways)
+      new DistageTestRunner[F](testReporter, checker, specEnv, toRun, _.isInstanceOf[TestCanceledException], parallelTestsAlways)
     }
 
-    runner.run()
+    try {
+      runner.run()
+    } finally {
+      DistageTestsRegistrySingleton.completeStatuses(testsInThisMonad.map(_.meta.id.suiteId).toSet)
+    }
   }
 
   // FIXME: read scalatest runner configuration???
   protected def parallelTestsAlways: Boolean = true
 
   private def mkTestReporter(args: Args): TestReporter = {
-    val scalatestReporter = new ScalatestReporter(args, suiteName, suiteId)
+    val scalatestReporter = new ScalatestReporter(args.reporter)
     new SafeTestReporter(scalatestReporter)
   }
 
-  private def addStub(args: Args, failure: Option[Throwable]): Unit = {
-    val tracker = args.tracker
-    val FUCK_SCALATEST = "Scalatest and IDEA aren't so nice"
-    val SUITE_FAILED = "Whole suite failed :/"
-
-    failure match {
-      case Some(_) =>
-        args.reporter(TestStarting(
-          tracker.nextOrdinal(),
-          suiteName, suiteId, Some(suiteId),
-          SUITE_FAILED,
-          SUITE_FAILED,
-        ))
-        args.reporter(TestFailed(
-          tracker.nextOrdinal(),
-          s"suite failed",
-          suiteName, suiteId, Some(suiteId),
-          SUITE_FAILED,
-          SUITE_FAILED,
-          scala.collection.immutable.IndexedSeq.empty[RecordableEvent],
-          throwable = failure
-        ))
-      case None =>
-        args.reporter(TestCanceled(
-          tracker.nextOrdinal(), FUCK_SCALATEST,
-          suiteName, suiteId, Some(suiteId),
-          FUCK_SCALATEST,
-          FUCK_SCALATEST,
-          Vector.empty,
-        ))
-    }
-
-  }
-
   override final val styleName: String = "DistageSuite"
 }
 
diff --git a/distage/distage-testkit-scalatest/src/main/scala/org/scalatest/SafeTestReporter.scala b/distage/distage-testkit-scalatest/src/main/scala/org/scalatest/SafeTestReporter.scala
index 60a54f3b5..707e3407f 100644
--- a/distage/distage-testkit-scalatest/src/main/scala/org/scalatest/SafeTestReporter.scala
+++ b/distage/distage-testkit-scalatest/src/main/scala/org/scalatest/SafeTestReporter.scala
@@ -1,15 +1,15 @@
 package org.scalatest
 
-import izumi.distage.testkit.services.dstest.DistageTestRunner.TestStatus.{Done, Finished, Ignored}
+import izumi.distage.testkit.services.dstest.DistageTestRunner.TestStatus.{Done, Finished, Ignored, Running}
 import izumi.distage.testkit.services.dstest.DistageTestRunner.{SuiteData, TestMeta, TestReporter, TestStatus}
-import izumi.fundamentals.platform.language.Quirks
+import izumi.fundamentals.platform.language.Quirks._
+import izumi.fundamentals.platform.language.unused
 
 import scala.collection.mutable
 
 class SafeTestReporter(underlying: TestReporter) extends TestReporter {
-  private val delayedReports = mutable.LinkedHashMap.empty[TestMeta, TestStatus]
-  private var signalled: Option[TestMeta] = None
-  private var openSuite: Option[SuiteData] = None
+  private val delayedReports = new mutable.LinkedHashMap[TestMeta, TestStatus]()
+  private var runningTest: Option[TestMeta] = None
 
   override def onFailure(f: Throwable): Unit = synchronized {
     endAll()
@@ -18,45 +18,41 @@ class SafeTestReporter(underlying: TestReporter) extends TestReporter {
 
   override def endAll(): Unit = synchronized {
     finish()
-    finishOpen()
     underlying.endAll()
   }
 
-  override def beginSuite(id: SuiteData): Unit = synchronized {
-    Quirks.discard(id)
-  }
+  override def beginSuite(@unused id: SuiteData): Unit = {}
 
-  override def endSuite(id: SuiteData): Unit = synchronized {
-    Quirks.discard(id)
-  }
+  override def endSuite(@unused id: SuiteData): Unit = {}
 
   override def testStatus(test: TestMeta, testStatus: TestStatus): Unit = synchronized {
     testStatus match {
-      case TestStatus.Scheduled =>
-        reportStatus(test, testStatus)
+//      case TestStatus.Scheduled => reportStatus(test, testStatus)
 
       case TestStatus.Running =>
-        signalled match {
+        runningTest match {
           case Some(_) =>
+            delayedReports.put(test, testStatus)
           case None =>
             reportStatus(test, testStatus)
-            signalled = Some(test)
+            runningTest = Some(test)
         }
 
       case _: Ignored =>
-        signalled match {
+        runningTest match {
           case None =>
             reportStatus(test, TestStatus.Running)
             reportStatus(test, testStatus)
           case Some(_) =>
+            // Ignores happen before running, so an ignored test cannot be the same as `signalled`
             delayedReports.put(test, testStatus)
         }
 
       case _: Finished =>
-        signalled match {
+        runningTest match {
           case Some(value) if value == test =>
             finishTest(test, testStatus)
-            signalled = None
+            runningTest = None
             finish()
           case _ =>
             delayedReports.put(test, testStatus)
@@ -66,38 +62,25 @@ class SafeTestReporter(underlying: TestReporter) extends TestReporter {
   }
 
   private def reportStatus(test: TestMeta, testStatus: TestStatus): Unit = synchronized {
-    val fakeSuite = SuiteData(test.id.suiteName, test.id.suiteId, test.id.suiteClassName)
-
-    nextSuite(fakeSuite)
     underlying.testStatus(test, testStatus)
   }
 
-  private val fakeSuites = false
-  private def nextSuite(fakeSuite: SuiteData): Unit = {
-    if (fakeSuites) {
-      if (!openSuite.contains(fakeSuite)) {
-        finishOpen()
-        underlying.beginSuite(fakeSuite)
-        openSuite = Some(fakeSuite)
-      }
-    }
-  }
-
-  private def finishOpen(): Unit = {
-    if (fakeSuites) {
-      openSuite.foreach(underlying.endSuite)
-    }
-  }
-
   private def finish(): Unit = {
     val finishedTests = delayedReports.collect { case (t, s: Done) => (t, s) }
 
+    // report all finished tests
     finishedTests
       .foreach {
         case (t, s) =>
           reportStatus(t, TestStatus.Running)
           finishTest(t, s)
       }
+    // switch to another test if it's already running
+    delayedReports.collectFirst {
+      case (t, Running) =>
+        reportStatus(t, TestStatus.Running)
+        runningTest = Some(t)
+    }.discard()
   }
 
   private def finishTest(test: TestMeta, testStatus: TestStatus): Option[TestStatus] = {
diff --git a/distage/distage-testkit-scalatest/src/main/scala/org/scalatest/ScalatestReporter.scala b/distage/distage-testkit-scalatest/src/main/scala/org/scalatest/ScalatestReporter.scala
index 22188877c..c35dea0bc 100644
--- a/distage/distage-testkit-scalatest/src/main/scala/org/scalatest/ScalatestReporter.scala
+++ b/distage/distage-testkit-scalatest/src/main/scala/org/scalatest/ScalatestReporter.scala
@@ -1,12 +1,11 @@
 package org.scalatest
 
 import izumi.distage.testkit.services.dstest.DistageTestRunner.{SuiteData, TestMeta, TestReporter, TestStatus}
-import izumi.fundamentals.platform.language.Quirks
-import org.scalatest.events._
+import izumi.distage.testkit.services.scalatest.dstest.DistageTestsRegistrySingleton
 import izumi.fundamentals.platform.strings.IzString._
+import org.scalatest.events._
 
-class ScalatestReporter(args: Args, suiteName: String, suiteId: String) extends TestReporter {
-  private val tracker = args.tracker
+class ScalatestReporter(reporter: Reporter) extends TestReporter {
 
   override def onFailure(f: Throwable): Unit = {
     System.err.println("Test runner failed")
@@ -16,107 +15,98 @@ class ScalatestReporter(args: Args, suiteName: String, suiteId: String) extends
   override def endAll(): Unit = {}
 
   override def beginSuite(id: SuiteData): Unit = {
-    args.reporter.apply(TestStarting(
-      tracker.nextOrdinal(),
-      suiteName, suiteId, Some(suiteId),
-      id.suiteName,
-      id.suiteName,
-    ))
+//    doReport(id.suiteId)(TestStarting(
+//      _,
+//      id.suiteName, id.suiteId, Some(id.suiteClassName),
+//      id.suiteName,
+//      id.suiteName,
+//    ))
   }
 
   override def endSuite(id: SuiteData): Unit = {
-    args.reporter.apply(TestSucceeded(
-      tracker.nextOrdinal(),
-      suiteName, suiteId, Some(suiteId),
-      id.suiteName,
-      id.suiteName,
-      scala.collection.immutable.IndexedSeq.empty[RecordableEvent],
-    ))
+//    doReport(id.suiteId)(TestSucceeded(
+//      _,
+//      id.suiteName, id.suiteId, Some(id.suiteClassName),
+//      id.suiteName,
+//      id.suiteName,
+//      Vector.empty,
+//    ))
   }
 
   override def testStatus(test: TestMeta, testStatus: TestStatus): Unit = {
-    val suiteName1 = suiteName
-    val suiteId1 = suiteId
+    val suiteName1 = test.id.suiteName
+    val suiteId1 = test.id.suiteId
+    val suiteClassName1 = test.id.suiteId
     val testName = mkName(test)
 
     testStatus match {
-      case TestStatus.Scheduled =>
+//      case TestStatus.Scheduled =>
 
       case TestStatus.Running =>
-        args.reporter.apply(TestStarting(
-          ord(test),
-          suiteName1, suiteId1, Some(suiteId1),
+        doReport(suiteId1)(TestStarting(
+          _,
+          suiteName1, suiteId1, Some(suiteClassName1),
           testName,
           testName,
           location = Some(LineInFile(test.pos.position.line, test.pos.position.file, None)),
+          rerunner = Some(test.id.suiteClassName),
         ))
       case TestStatus.Succeed(duration) =>
-        args.reporter.apply(TestSucceeded(
-          ord(test),
-          suiteName1, suiteId1, Some(suiteId1),
+        doReport(suiteId1)(TestSucceeded(
+          _,
+          suiteName1, suiteId1, Some(suiteClassName1),
           testName,
           testName,
-          scala.collection.immutable.IndexedSeq.empty[RecordableEvent],
-          location = Some(LineInFile(test.pos.position.line, test.pos.position.file, None)),
+          recordedEvents = Vector.empty,
           duration = Some(duration.toMillis),
+          location = Some(LineInFile(test.pos.position.line, test.pos.position.file, None)),
           rerunner = Some(test.id.suiteClassName),
         ))
       case TestStatus.Failed(t, duration) =>
-        args.reporter.apply(TestFailed(
-          ord(test),
+        doReport(suiteId1)(TestFailed(
+          _,
           "Test failed",
-          suiteName1, suiteId1, Some(suiteId1),
+          suiteName1, suiteId1, Some(suiteClassName1),
           testName,
           testName,
-          scala.collection.immutable.IndexedSeq.empty[RecordableEvent],
-          location = Some(LineInFile(test.pos.position.line, test.pos.position.file, None)),
+          recordedEvents = Vector.empty,
           throwable = Some(t),
           duration = Some(duration.toMillis),
+          location = Some(LineInFile(test.pos.position.line, test.pos.position.file, None)),
           rerunner = Some(test.id.suiteClassName),
         ))
       case TestStatus.Cancelled(clue, duration) =>
-        args.reporter.apply(TestCanceled(
-          ord(test),
+        doReport(suiteId1)(TestCanceled(
+          _,
           s"cancelled: $clue",
-          suiteName1, suiteId1, Some(suiteId1),
+          suiteName1, suiteId1, Some(suiteClassName1),
           testName,
           testName,
-          scala.collection.immutable.IndexedSeq.empty[RecordableEvent],
-          location = Some(LineInFile(test.pos.position.line, test.pos.position.file, None)),
+          recordedEvents = Vector.empty,
           duration = Some(duration.toMillis),
+          location = Some(LineInFile(test.pos.position.line, test.pos.position.file, None)),
           rerunner = Some(test.id.suiteClassName),
         ))
       case TestStatus.Ignored(checks) =>
-//              args.reporter.apply(TestIgnored(
-//                ord(test),
-//                suiteName1, suiteId1, Some(suiteId1),
-//                testName,
-//                testName,
-//                None,
-//                location = Some(LineInFile(test.pos.position.line, test.pos.position.file, None)),
-//                payload = None,
-//                threadName = "",
-//                timeStamp = 0,
-//              ))
-        args.reporter.apply(TestCanceled(
-          ord(test),
+        doReport(suiteId1)(TestCanceled(
+          _,
           s"ignored: ${checks.niceList()}",
-          suiteName1, suiteId1, Some(suiteId1),
+          suiteName1, suiteId1, Some(suiteClassName1),
           testName,
           testName,
-          scala.collection.immutable.IndexedSeq.empty[RecordableEvent],
+          recordedEvents = Vector.empty,
           location = Some(LineInFile(test.pos.position.line, test.pos.position.file, None)),
           rerunner = Some(test.id.suiteClassName),
         ))
     }
   }
 
-  private def mkName(test: TestMeta) = {
+  private def mkName(test: TestMeta): String = {
     s"${test.id.suiteName}: ${test.id.name}"
   }
 
-  private def ord(testId: TestMeta): Ordinal = {
-    Quirks.discard(testId)
-    tracker.nextOrdinal()
+  @inline private[this] def doReport(suiteId: String)(f: Ordinal => Event): Unit = {
+    DistageTestsRegistrySingleton.runReport(suiteId)(tracker => reporter(f(tracker.nextOrdinal())))
   }
+
 }
diff --git a/distage/distage-testkit-scalatest/src/test/scala/izumi/distage/testkit/distagesuite/DistageTestExample.scala b/distage/distage-testkit-scalatest/src/test/scala/izumi/distage/testkit/distagesuite/DistageTestExample.scala
index 08dc134fd..2d3abe974 100644
--- a/distage/distage-testkit-scalatest/src/test/scala/izumi/distage/testkit/distagesuite/DistageTestExample.scala
+++ b/distage/distage-testkit-scalatest/src/test/scala/izumi/distage/testkit/distagesuite/DistageTestExample.scala
@@ -2,8 +2,10 @@ package izumi.distage.testkit.distagesuite
 
 import cats.effect.{IO => CIO}
 import distage._
+import izumi.distage.framework.model.PluginSource
 import izumi.distage.model.effect.DIEffect
 import izumi.distage.model.effect.DIEffect.syntax._
+import izumi.distage.plugins.PluginConfig
 import izumi.distage.testkit.TestConfig
 import izumi.distage.testkit.distagesuite.fixtures.{ApplePaymentProvider, MockCache, MockCachedUserService, MockUserRepository}
 import izumi.distage.testkit.scalatest.{DistageBIOSpecScalatest, DistageSpecScalatest}
@@ -11,9 +13,10 @@ import izumi.distage.testkit.services.scalatest.dstest.DistageAbstractScalatestS
 import izumi.fundamentals.platform.functional.Identity
 import zio.Task
 
-trait DistageMemoizeExample[F[_]] { this: DistageAbstractScalatestSpec[F] =>
+trait DistageMemoizeExample[F[_]] {
+  this: DistageAbstractScalatestSpec[F] =>
   override protected def config: TestConfig = {
-    TestConfig(
+    TestConfig.forSuite(this.getClass).copy(
       memoizationRoots = Set(
         DIKey.get[MockCache[CIO]],
         DIKey.get[MockCache[Task]],
@@ -34,7 +37,12 @@ class DistageTestExampleBIO extends DistageBIOSpecScalatest[zio.IO] with Distage
 
 }
 
-abstract class DistageTestExampleBase[F[_]: TagK](implicit F: DIEffect[F]) extends DistageSpecScalatest[F] with DistageMemoizeExample[F] {
+abstract class DistageTestExampleBase[F[_] : TagK](implicit F: DIEffect[F]) extends DistageSpecScalatest[F] with DistageMemoizeExample[F] {
+
+  override protected def config: TestConfig = super.config.copy(
+    pluginSource = super.config.pluginSource ++ PluginSource(PluginConfig.cached(Seq("xxx")))
+  )
+
 
   "distage test custom runner" should {
     "test 1" in {
@@ -65,7 +73,7 @@ abstract class DistageTestExampleBase[F[_]: TagK](implicit F: DIEffect[F]) exten
 
     "test 5 (should be ignored)" skip {
       _: MockCachedUserService[F] =>
-       ???
+        ???
     }
 
     "test 6 (should be ignored)" in {
@@ -77,5 +85,8 @@ abstract class DistageTestExampleBase[F[_]: TagK](implicit F: DIEffect[F]) exten
 }
 
 final class DistageTestExampleId extends DistageTestExampleBase[Identity]
+
 final class DistageTestExampleCIO extends DistageTestExampleBase[CIO]
+
 final class DistageTestExampleZIO extends DistageTestExampleBase[Task]
+final class DistageTestExampleZIO2 extends DistageTestExampleBase[Task]
diff --git a/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/SyncCache.scala b/fundamentals/fundamentals-platform/src/main/scala/izumi/fundamentals/platform/cache/SyncCache.scala
similarity index 95%
rename from distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/SyncCache.scala
rename to fundamentals/fundamentals-platform/src/main/scala/izumi/fundamentals/platform/cache/SyncCache.scala
index 4f9a0d919..2f253e8bb 100644
--- a/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/SyncCache.scala
+++ b/fundamentals/fundamentals-platform/src/main/scala/izumi/fundamentals/platform/cache/SyncCache.scala
@@ -1,4 +1,4 @@
-package izumi.distage.testkit.services
+package izumi.fundamentals.platform.cache
 
 import izumi.fundamentals.platform.language.Quirks._