Repository: 7mind/izumi (scala)

Issue:
Shorthand assertIO variants
Unpacking assertIO, to replace
```scala
abc <- x.getY
_ <- assertIO(abc == 3)
```
with
```scala
assertIO(x.getY)(_ == 3)
```

Pull request:
Fix #2155

Patch:
diff --git a/fundamentals/fundamentals-platform/src/main/scala-2/izumi/fundamentals/reflection/ReflectionUtil.scala b/fundamentals/fundamentals-platform/src/main/scala-2/izumi/fundamentals/reflection/ReflectionUtil.scala
index 07675a8b3b..3e18931f4b 100644
--- a/fundamentals/fundamentals-platform/src/main/scala-2/izumi/fundamentals/reflection/ReflectionUtil.scala
+++ b/fundamentals/fundamentals-platform/src/main/scala-2/izumi/fundamentals/reflection/ReflectionUtil.scala
@@ -131,4 +131,21 @@ object ReflectionUtil {
     }.headOption
   }
 
+  def betaReduceLambda(c: blackbox.Context)(lambda: c.Expr[Any], names: List[String]): c.universe.Tree = {
+    import c.universe._
+    lambda.tree match {
+      case Function(params, body) =>
+        assert(params.size == names.size)
+        val substitutions = params.map(_.symbol).zip(names).toMap
+        val transformer = new Transformer {
+          override def transform(tree: Tree): Tree = tree match {
+            case Ident(_) if substitutions.exists(_._1 == tree.symbol) => Ident(TermName(substitutions(tree.symbol)))
+            case _ => super.transform(tree)
+          }
+        }
+        transformer.transform(body)
+      case _ => c.abort(c.enclosingPosition, "Expected a lambda function.")
+    }
+  }
+
 }


Test patch:
diff --git a/distage/distage-testkit-scalatest/src/main/scala-2/izumi/distage/testkit/scalatest/AssertCIO.scala b/distage/distage-testkit-scalatest/src/main/scala-2/izumi/distage/testkit/scalatest/AssertCIO.scala
index e96cc11aae..8fba4c4eca 100644
--- a/distage/distage-testkit-scalatest/src/main/scala-2/izumi/distage/testkit/scalatest/AssertCIO.scala
+++ b/distage/distage-testkit-scalatest/src/main/scala-2/izumi/distage/testkit/scalatest/AssertCIO.scala
@@ -2,6 +2,7 @@ package izumi.distage.testkit.scalatest
 
 import cats.effect.IO
 import izumi.distage.testkit.scalatest.AssertCIO.AssertCIOMacro
+import izumi.fundamentals.reflection.ReflectionUtil
 import org.scalactic.Prettifier
 import org.scalactic.source.Position
 import org.scalatest.Assertion
@@ -13,6 +14,21 @@ import scala.reflect.macros.blackbox
 /** scalatest assertion macro for [[cats.effect.IO]] */
 trait AssertCIO {
   final def assertIO(arg: Boolean)(implicit prettifier: Prettifier, pos: Position): IO[Assertion] = macro AssertCIOMacro.impl
+
+  final def assertIO[T](
+    effect: IO[T]
+  )(predicate: T => Boolean
+  )(implicit prettifier: Prettifier,
+    pos: Position,
+  ): IO[Assertion] = macro AssertCIOMacro.shortImpl1[T]
+
+  final def assertIO[A, B](
+    effectA: IO[A],
+    effectB: IO[B],
+  )(predicate: (A, B) => Boolean
+  )(implicit prettifier: Prettifier,
+    pos: Position,
+  ): IO[Assertion] = macro AssertCIOMacro.shortImpl2[A, B]
 }
 
 object AssertCIO extends AssertCIO {
@@ -22,6 +38,43 @@ object AssertCIO extends AssertCIO {
       import c.universe._
       c.Expr[IO[Assertion]](q"_root_.cats.effect.IO.delay(${DistageAssertionsMacro.assert(c)(arg)(prettifier, pos)})")
     }
+
+    def shortImpl1[T: c.WeakTypeTag](
+      c: blackbox.Context
+    )(effect: c.Expr[IO[T]]
+    )(predicate: c.Expr[T => Boolean]
+    )(prettifier: c.Expr[Prettifier],
+      pos: c.Expr[Position],
+    ): c.Expr[IO[Assertion]] = {
+      import c.universe._
+
+      val resultName = TermName(c.freshName("result"))
+      val predicateBody = ReflectionUtil.betaReduceLambda(c)(predicate, List(resultName.toString))
+
+      c.Expr[IO[Assertion]](
+        q"$effect.flatMap { ($resultName: ${weakTypeOf[T]}) => _root_.izumi.distage.testkit.scalatest.AssertCIO.assertIO($predicateBody)($prettifier, $pos) }"
+      )
+    }
+
+    def shortImpl2[A: c.WeakTypeTag, B: c.WeakTypeTag](
+      c: blackbox.Context
+    )(effectA: c.Expr[IO[A]],
+      effectB: c.Expr[IO[B]],
+    )(predicate: c.Expr[(A, B) => Boolean]
+    )(prettifier: c.Expr[Prettifier],
+      pos: c.Expr[Position],
+    ): c.Expr[IO[Assertion]] = {
+      import c.universe._
+
+      val resultAName = TermName(c.freshName("resultA"))
+      val resultBName = TermName(c.freshName("resultB"))
+      val predicateBody = ReflectionUtil.betaReduceLambda(c)(predicate, List(resultAName.toString, resultBName.toString))
+
+      c.Expr[IO[Assertion]](q"""$effectA.flatMap {
+           ($resultAName: ${weakTypeOf[A]}) =>
+              $effectB.flatMap { ($resultBName: ${weakTypeOf[B]}) => _root_.izumi.distage.testkit.scalatest.AssertCIO.assertIO($predicateBody)($prettifier, $pos) }
+        }""")
+    }
   }
 
 }
diff --git a/distage/distage-testkit-scalatest/src/main/scala-2/izumi/distage/testkit/scalatest/AssertIO2.scala b/distage/distage-testkit-scalatest/src/main/scala-2/izumi/distage/testkit/scalatest/AssertIO2.scala
index b0d77ba2b7..8e11146a82 100644
--- a/distage/distage-testkit-scalatest/src/main/scala-2/izumi/distage/testkit/scalatest/AssertIO2.scala
+++ b/distage/distage-testkit-scalatest/src/main/scala-2/izumi/distage/testkit/scalatest/AssertIO2.scala
@@ -2,7 +2,9 @@ package izumi.distage.testkit.scalatest
 
 import izumi.distage.testkit.scalatest.AssertIO2.AssertIO2Macro
 import izumi.functional.bio.IO2
-import org.scalactic.{Prettifier, source}
+import izumi.fundamentals.reflection.ReflectionUtil
+import org.scalactic.Prettifier
+import org.scalactic.source.Position
 import org.scalatest.Assertion
 import org.scalatest.distage.DistageAssertionsMacro
 
@@ -11,11 +13,27 @@ import scala.reflect.macros.blackbox
 
 /** scalatest assertion macro for any [[izumi.functional.bio.IO2]] */
 trait AssertIO2[F[+_, +_]] {
-  final def assertIO(arg: Boolean)(implicit IO2: IO2[F], prettifier: Prettifier, pos: source.Position): F[Nothing, Assertion] = macro AssertIO2Macro.impl[F]
+  final def assertIO(arg: Boolean)(implicit IO2: IO2[F], prettifier: Prettifier, pos: Position): F[Nothing, Assertion] = macro AssertIO2Macro.impl[F]
+  final def assertIO[E, T](
+    effect: F[E, T]
+  )(predicate: T => Boolean
+  )(implicit IO2: IO2[F],
+    prettifier: Prettifier,
+    pos: Position,
+  ): F[E, Assertion] = macro AssertIO2Macro.shortImpl1[F, E, T]
+
+  final def assertIO[E, A, B](
+    effectA: F[E, A],
+    effectB: F[E, B],
+  )(predicate: (A, B) => Boolean
+  )(implicit IO2: IO2[F],
+    prettifier: Prettifier,
+    pos: Position,
+  ): F[E, Assertion] = macro AssertIO2Macro.shortImpl2[F, E, A, B]
 }
 
 object AssertIO2 {
-  final def assertIO[F[+_, +_]](arg: Boolean)(implicit IO2: IO2[F], prettifier: Prettifier, pos: source.Position): F[Nothing, Assertion] = macro AssertIO2Macro.impl[F]
+  final def assertIO[F[+_, +_]](arg: Boolean)(implicit IO2: IO2[F], prettifier: Prettifier, pos: Position): F[Nothing, Assertion] = macro AssertIO2Macro.impl[F]
 
   object AssertIO2Macro {
     def impl[F[+_, +_]](
@@ -23,10 +41,58 @@ object AssertIO2 {
     )(arg: c.Expr[Boolean]
     )(IO2: c.Expr[IO2[F]],
       prettifier: c.Expr[Prettifier],
-      pos: c.Expr[org.scalactic.source.Position],
+      pos: c.Expr[Position],
     ): c.Expr[F[Nothing, Assertion]] = {
       import c.universe._
       c.Expr[F[Nothing, Assertion]](q"$IO2.sync(${DistageAssertionsMacro.assert(c)(arg)(prettifier, pos)})")
     }
+
+    def shortImpl1[F[+_, +_], E: c.WeakTypeTag, T: c.WeakTypeTag](
+      c: blackbox.Context
+    )(effect: c.Expr[F[E, T]]
+    )(predicate: c.Expr[T => Boolean]
+    )(IO2: c.Expr[IO2[F]],
+      prettifier: c.Expr[Prettifier],
+      pos: c.Expr[Position],
+    ): c.Expr[F[E, Assertion]] = {
+      import c.universe._
+
+      val io2FreshName = TermName(c.freshName("IO2"))
+      val resultName = TermName(c.freshName("result"))
+      val predicateBody = ReflectionUtil.betaReduceLambda(c)(predicate, List(resultName.toString))
+
+      c.Expr[F[Nothing, Assertion]](
+        q"""{
+           val $io2FreshName = $IO2
+           $io2FreshName.flatMap($effect) {
+             ($resultName: ${weakTypeOf[T]}) => _root_.izumi.distage.testkit.scalatest.AssertIO2.assertIO($predicateBody)($io2FreshName, $prettifier, $pos)
+           }}"""
+      )
+    }
+
+    def shortImpl2[F[+_, +_], E: c.WeakTypeTag, A: c.WeakTypeTag, B: c.WeakTypeTag](
+      c: blackbox.Context
+    )(effectA: c.Expr[F[E, A]],
+      effectB: c.Expr[F[E, B]],
+    )(predicate: c.Expr[(A, B) => Boolean]
+    )(IO2: c.Expr[IO2[F]],
+      prettifier: c.Expr[Prettifier],
+      pos: c.Expr[Position],
+    ): c.Expr[F[E, Assertion]] = {
+      import c.universe._
+
+      val io2FreshName = TermName(c.freshName("IO2"))
+      val resultAName = TermName(c.freshName("resultA"))
+      val resultBName = TermName(c.freshName("resultB"))
+      val predicateBody = ReflectionUtil.betaReduceLambda(c)(predicate, List(resultAName.toString, resultBName.toString))
+
+      c.Expr[F[Nothing, Assertion]](q"""{
+        val $io2FreshName = $IO2
+        $io2FreshName.flatMap($effectA) {
+           ($resultAName: ${weakTypeOf[A]}) =>
+              $io2FreshName.flatMap($effectB) { ($resultBName: ${weakTypeOf[B]}) =>
+                _root_.izumi.distage.testkit.scalatest.AssertIO2.assertIO($predicateBody)($io2FreshName, $prettifier, $pos) }
+        }}""")
+    }
   }
 }
diff --git a/distage/distage-testkit-scalatest/src/main/scala-2/izumi/distage/testkit/scalatest/AssertSync.scala b/distage/distage-testkit-scalatest/src/main/scala-2/izumi/distage/testkit/scalatest/AssertSync.scala
index 95a77763ea..9b6fbc8b5d 100644
--- a/distage/distage-testkit-scalatest/src/main/scala-2/izumi/distage/testkit/scalatest/AssertSync.scala
+++ b/distage/distage-testkit-scalatest/src/main/scala-2/izumi/distage/testkit/scalatest/AssertSync.scala
@@ -2,7 +2,9 @@ package izumi.distage.testkit.scalatest
 
 import cats.effect.kernel.Sync
 import izumi.distage.testkit.scalatest.AssertSync.AssertSyncMacro
-import org.scalactic.{Prettifier, source}
+import izumi.fundamentals.reflection.ReflectionUtil
+import org.scalactic.source.Position
+import org.scalactic.Prettifier
 import org.scalatest.Assertion
 import org.scalatest.distage.DistageAssertionsMacro
 
@@ -11,11 +13,28 @@ import scala.reflect.macros.blackbox
 
 /** scalatest assertion macro for any [[cats.effect.kernel.Sync]] */
 trait AssertSync[F[_]] {
-  final def assertIO(arg: Boolean)(implicit Sync: Sync[F], prettifier: Prettifier, pos: source.Position): F[Assertion] = macro AssertSyncMacro.impl[F]
+  final def assertIO(arg: Boolean)(implicit Sync: Sync[F], prettifier: Prettifier, pos: Position): F[Assertion] = macro AssertSyncMacro.impl[F]
+
+  final def assertIO[T](
+    effect: F[T]
+  )(predicate: T => Boolean
+  )(implicit Sync: Sync[F],
+    prettifier: Prettifier,
+    pos: Position,
+  ): F[Assertion] = macro AssertSyncMacro.shortImpl1[F, T]
+
+  final def assertIO[A, B](
+    effectA: F[A],
+    effectB: F[B],
+  )(predicate: (A, B) => Boolean
+  )(implicit Sync: Sync[F],
+    prettifier: Prettifier,
+    pos: Position,
+  ): F[Assertion] = macro AssertSyncMacro.shortImpl2[F, A, B]
 }
 
 object AssertSync {
-  final def assertIO[F[_]](arg: Boolean)(implicit Sync: Sync[F], prettifier: Prettifier, pos: source.Position): F[Assertion] = macro AssertSyncMacro.impl[F]
+  final def assertIO[F[_]](arg: Boolean)(implicit Sync: Sync[F], prettifier: Prettifier, pos: Position): F[Assertion] = macro AssertSyncMacro.impl[F]
 
   object AssertSyncMacro {
     def impl[F[_]](
@@ -23,10 +42,59 @@ object AssertSync {
     )(arg: c.Expr[Boolean]
     )(Sync: c.Expr[Sync[F]],
       prettifier: c.Expr[Prettifier],
-      pos: c.Expr[org.scalactic.source.Position],
+      pos: c.Expr[Position],
     ): c.Expr[F[Assertion]] = {
       import c.universe._
       c.Expr[F[Assertion]](q"$Sync.delay(${DistageAssertionsMacro.assert(c)(arg)(prettifier, pos)})")
     }
+
+    def shortImpl1[F[_], T: c.WeakTypeTag](
+      c: blackbox.Context
+    )(effect: c.Expr[F[T]]
+    )(predicate: c.Expr[T => Boolean]
+    )(Sync: c.Expr[Sync[F]],
+      prettifier: c.Expr[Prettifier],
+      pos: c.Expr[Position],
+    ): c.Expr[F[Assertion]] = {
+      import c.universe._
+
+      val syncFreshName = TermName(c.freshName("Sync"))
+      val resultName = TermName(c.freshName("result"))
+      val predicateBody = ReflectionUtil.betaReduceLambda(c)(predicate, List(resultName.toString))
+
+      c.Expr[F[Assertion]](
+        q"""{
+            val $syncFreshName = $Sync
+            $syncFreshName.flatMap($effect){
+              ($resultName: ${weakTypeOf[T]}) => _root_.izumi.distage.testkit.scalatest.AssertSync.assertIO($predicateBody)($syncFreshName, $prettifier, $pos)
+            }}"""
+      )
+    }
+
+    def shortImpl2[F[_], A: c.WeakTypeTag, B: c.WeakTypeTag](
+      c: blackbox.Context
+    )(effectA: c.Expr[F[A]],
+      effectB: c.Expr[F[B]],
+    )(predicate: c.Expr[(A, B) => Boolean]
+    )(Sync: c.Expr[Sync[F]],
+      prettifier: c.Expr[Prettifier],
+      pos: c.Expr[Position],
+    ): c.Expr[F[Assertion]] = {
+      import c.universe._
+
+      val syncFreshName = TermName(c.freshName("Sync"))
+      val resultAName = TermName(c.freshName("resultA"))
+      val resultBName = TermName(c.freshName("resultB"))
+      val predicateBody = ReflectionUtil.betaReduceLambda(c)(predicate, List(resultAName.toString, resultBName.toString))
+
+      c.Expr[F[Assertion]](q"""{
+          val $syncFreshName = $Sync
+          $syncFreshName.flatMap($effectA){
+           ($resultAName: ${weakTypeOf[A]}) =>
+              $syncFreshName.flatMap($effectB){ ($resultBName: ${weakTypeOf[B]}) =>
+                _root_.izumi.distage.testkit.scalatest.AssertSync.assertIO($predicateBody)($syncFreshName, $prettifier, $pos) }
+        }
+     }""")
+    }
   }
 }
diff --git a/distage/distage-testkit-scalatest/src/main/scala-2/izumi/distage/testkit/scalatest/AssertZIO.scala b/distage/distage-testkit-scalatest/src/main/scala-2/izumi/distage/testkit/scalatest/AssertZIO.scala
index 72a23317e6..b906d854b0 100644
--- a/distage/distage-testkit-scalatest/src/main/scala-2/izumi/distage/testkit/scalatest/AssertZIO.scala
+++ b/distage/distage-testkit-scalatest/src/main/scala-2/izumi/distage/testkit/scalatest/AssertZIO.scala
@@ -1,11 +1,12 @@
 package izumi.distage.testkit.scalatest
 
 import izumi.distage.testkit.scalatest.AssertZIO.AssertZIOMacro
+import izumi.fundamentals.reflection.ReflectionUtil
 import org.scalactic.Prettifier
 import org.scalactic.source.Position
 import org.scalatest.Assertion
 import org.scalatest.distage.DistageAssertionsMacro
-import zio.IO
+import zio.{IO, ZIO}
 
 import scala.language.experimental.macros
 import scala.reflect.macros.blackbox
@@ -13,6 +14,23 @@ import scala.reflect.macros.blackbox
 /** scalatest assertion macro for [[zio.ZIO]] */
 trait AssertZIO {
   final def assertIO(arg: Boolean)(implicit prettifier: Prettifier, pos: Position, zioTrace: zio.Trace): IO[Nothing, Assertion] = macro AssertZIOMacro.impl
+
+  final def assertIO[R, E, T](
+    effect: ZIO[R, E, T]
+  )(predicate: T => Boolean
+  )(implicit prettifier: Prettifier,
+    pos: Position,
+    zioTrace: zio.Trace,
+  ): ZIO[R, E, Assertion] = macro AssertZIOMacro.shortImpl1[R, E, T]
+
+  final def assertIO[R, E, A, B](
+    effectA: ZIO[R, E, A],
+    effectB: ZIO[R, E, B],
+  )(predicate: (A, B) => Boolean
+  )(implicit prettifier: Prettifier,
+    pos: Position,
+    zioTrace: zio.Trace,
+  ): ZIO[R, E, Assertion] = macro AssertZIOMacro.shortImpl2[R, E, A, B]
 }
 
 object AssertZIO extends AssertZIO {
@@ -28,6 +46,46 @@ object AssertZIO extends AssertZIO {
       import c.universe._
       c.Expr[IO[Nothing, Assertion]](q"_root_.zio.ZIO.succeed(${DistageAssertionsMacro.assert(c)(arg)(prettifier, pos)})($zioTrace)")
     }
+
+    def shortImpl1[R: c.WeakTypeTag, E: c.WeakTypeTag, T: c.WeakTypeTag](
+      c: blackbox.Context
+    )(effect: c.Expr[ZIO[R, E, T]]
+    )(predicate: c.Expr[T => Boolean]
+    )(prettifier: c.Expr[Prettifier],
+      pos: c.Expr[Position],
+      zioTrace: c.Expr[zio.Trace],
+    ): c.Expr[ZIO[R, E, Assertion]] = {
+      import c.universe._
+
+      val resultName = TermName(c.freshName("result"))
+      val predicateBody = ReflectionUtil.betaReduceLambda(c)(predicate, List(resultName.toString))
+
+      c.Expr[IO[Nothing, Assertion]](
+        q"$effect.flatMap { ($resultName: ${weakTypeOf[T]}) => _root_.izumi.distage.testkit.scalatest.AssertZIO.assertIO($predicateBody)($prettifier, $pos, $zioTrace) }"
+      )
+    }
+
+    def shortImpl2[R: c.WeakTypeTag, E: c.WeakTypeTag, A: c.WeakTypeTag, B: c.WeakTypeTag](
+      c: blackbox.Context
+    )(effectA: c.Expr[ZIO[R, E, A]],
+      effectB: c.Expr[ZIO[R, E, B]],
+    )(predicate: c.Expr[(A, B) => Boolean]
+    )(prettifier: c.Expr[Prettifier],
+      pos: c.Expr[Position],
+      zioTrace: c.Expr[zio.Trace],
+    ): c.Expr[ZIO[R, E, Assertion]] = {
+      import c.universe._
+
+      val resultAName = TermName(c.freshName("resultA"))
+      val resultBName = TermName(c.freshName("resultB"))
+      val predicateBody = ReflectionUtil.betaReduceLambda(c)(predicate, List(resultAName.toString, resultBName.toString))
+
+      c.Expr[IO[Nothing, Assertion]](q"""$effectA.flatMap {
+           ($resultAName: ${weakTypeOf[A]}) =>
+              $effectB.flatMap { ($resultBName: ${weakTypeOf[B]}) =>
+                _root_.izumi.distage.testkit.scalatest.AssertZIO.assertIO($predicateBody)($prettifier, $pos, $zioTrace) }
+        }""")
+    }
   }
 
 }
diff --git a/distage/distage-testkit-scalatest/src/main/scala-3/izumi/distage/testkit/scalatest/AssertCIO.scala b/distage/distage-testkit-scalatest/src/main/scala-3/izumi/distage/testkit/scalatest/AssertCIO.scala
index c5580689a6..fd1a387633 100644
--- a/distage/distage-testkit-scalatest/src/main/scala-3/izumi/distage/testkit/scalatest/AssertCIO.scala
+++ b/distage/distage-testkit-scalatest/src/main/scala-3/izumi/distage/testkit/scalatest/AssertCIO.scala
@@ -10,6 +10,25 @@ trait AssertCIO {
   inline final def assertIO(inline arg: Boolean)(implicit prettifier: Prettifier, pos: Position): IO[Assertion] = {
     IO.delay(Assertions.assert(arg))
   }
+
+  inline final def assertIO[T](
+    inline effect: IO[T]
+  )(inline predicate: T => Boolean
+  )(implicit prettifier: Prettifier,
+    pos: Position,
+  ): IO[Assertion] = {
+    effect.flatMap(result => assertIO(predicate(result)))
+  }
+
+  inline final def assertIO[A, B](
+    inline effectA: IO[A],
+    inline effectB: IO[B],
+  )(inline predicate: (A, B) => Boolean
+  )(implicit prettifier: Prettifier,
+    pos: Position,
+  ): IO[Assertion] = {
+    effectA.flatMap(resultA => effectB.flatMap(resultB => assertIO(predicate(resultA, resultB))))
+  }
 }
 
 object AssertCIO extends AssertCIO
diff --git a/distage/distage-testkit-scalatest/src/main/scala-3/izumi/distage/testkit/scalatest/AssertIO2.scala b/distage/distage-testkit-scalatest/src/main/scala-3/izumi/distage/testkit/scalatest/AssertIO2.scala
index 2ec3712dfb..1f65d71536 100644
--- a/distage/distage-testkit-scalatest/src/main/scala-3/izumi/distage/testkit/scalatest/AssertIO2.scala
+++ b/distage/distage-testkit-scalatest/src/main/scala-3/izumi/distage/testkit/scalatest/AssertIO2.scala
@@ -9,6 +9,27 @@ trait AssertIO2[F[+_, +_]] {
   inline final def assertIO(inline arg: Boolean)(implicit IO2: IO2[F], prettifier: Prettifier, pos: source.Position): F[Nothing, Assertion] = {
     IO2.sync(Assertions.assert(arg))
   }
+
+  inline final def assertIO[E, T](
+    inline effect: F[E, T]
+  )(inline predicate: T => Boolean
+  )(implicit IO2: IO2[F],
+    prettifier: Prettifier,
+    pos: source.Position,
+  ): F[E, Assertion] = {
+    IO2.flatMap(effect)(result => assertIO(predicate(result)))
+  }
+
+  inline final def assertIO[E, A, B](
+    inline effectA: F[E, A],
+    inline effectB: F[E, B],
+  )(inline predicate: (A, B) => Boolean
+  )(implicit IO2: IO2[F],
+    prettifier: Prettifier,
+    pos: source.Position,
+  ): F[E, Assertion] = {
+    IO2.flatMap(effectA)(resultA => IO2.flatMap(effectB)(resultB => assertIO(predicate(resultA, resultB))))
+  }
 }
 
 object AssertIO2 {
diff --git a/distage/distage-testkit-scalatest/src/main/scala-3/izumi/distage/testkit/scalatest/AssertSync.scala b/distage/distage-testkit-scalatest/src/main/scala-3/izumi/distage/testkit/scalatest/AssertSync.scala
index 40fb12eae6..54efab844f 100644
--- a/distage/distage-testkit-scalatest/src/main/scala-3/izumi/distage/testkit/scalatest/AssertSync.scala
+++ b/distage/distage-testkit-scalatest/src/main/scala-3/izumi/distage/testkit/scalatest/AssertSync.scala
@@ -9,6 +9,27 @@ trait AssertSync[F[_]] {
   inline final def assertIO(inline arg: Boolean)(implicit Sync: Sync[F], prettifier: Prettifier, pos: source.Position): F[Assertion] = {
     Sync.delay(Assertions.assert(arg))
   }
+
+  inline final def assertIO[T](
+    inline effect: F[T]
+  )(inline predicate: T => Boolean
+  )(implicit Sync: Sync[F],
+    prettifier: Prettifier,
+    pos: source.Position,
+  ): F[Assertion] = {
+    Sync.flatMap(effect)(result => assertIO(predicate(result)))
+  }
+
+  inline final def assertIO[A, B](
+    inline effectA: F[A],
+    inline effectB: F[B],
+  )(inline predicate: (A, B) => Boolean
+  )(implicit Sync: Sync[F],
+    prettifier: Prettifier,
+    pos: source.Position,
+  ): F[Assertion] = {
+    Sync.flatMap(effectA)(resultA => Sync.flatMap(effectB)(resultB => assertIO(predicate(resultA, resultB))))
+  }
 }
 
 object AssertSync {
diff --git a/distage/distage-testkit-scalatest/src/main/scala-3/izumi/distage/testkit/scalatest/AssertZIO.scala b/distage/distage-testkit-scalatest/src/main/scala-3/izumi/distage/testkit/scalatest/AssertZIO.scala
index cc078282ce..6394df2bf4 100644
--- a/distage/distage-testkit-scalatest/src/main/scala-3/izumi/distage/testkit/scalatest/AssertZIO.scala
+++ b/distage/distage-testkit-scalatest/src/main/scala-3/izumi/distage/testkit/scalatest/AssertZIO.scala
@@ -10,6 +10,27 @@ trait AssertZIO {
   inline final def assertIO(inline arg: Boolean)(implicit prettifier: Prettifier, pos: Position, zioTrace: zio.Trace): ZIO[Any, Nothing, Assertion] = {
     ZIO.succeed(Assertions.assert(arg))(zioTrace)
   }
+
+  inline final def assertIO[R, E, T](
+    inline effect: ZIO[R, E, T]
+  )(inline predicate: T => Boolean
+  )(implicit prettifier: Prettifier,
+    pos: Position,
+    zioTrace: zio.Trace,
+  ): ZIO[R, E, Assertion] = {
+    effect.flatMap(result => assertIO(predicate(result)))
+  }
+
+  inline final def assertIO[R, E, A, B](
+    inline effectA: ZIO[R, E, A],
+    inline effectB: ZIO[R, E, B],
+  )(inline predicate: (A, B) => Boolean
+  )(implicit prettifier: Prettifier,
+    pos: Position,
+    zioTrace: zio.Trace,
+  ): ZIO[R, E, Assertion] = {
+    effectA.flatMap(resultA => effectB.flatMap(resultB => assertIO(predicate(resultA, resultB))))
+  }
 }
 
 object AssertZIO extends AssertZIO
diff --git a/distage/distage-testkit-scalatest/src/test/scala/izumi/distage/testkit/distagesuite/generic/tests.scala b/distage/distage-testkit-scalatest/src/test/scala/izumi/distage/testkit/distagesuite/generic/tests.scala
index 08de8fa55a..ed4b864df6 100644
--- a/distage/distage-testkit-scalatest/src/test/scala/izumi/distage/testkit/distagesuite/generic/tests.scala
+++ b/distage/distage-testkit-scalatest/src/test/scala/izumi/distage/testkit/distagesuite/generic/tests.scala
@@ -6,13 +6,16 @@ import izumi.distage.modules.DefaultModule
 import izumi.distage.testkit.distagesuite.fixtures.*
 import izumi.distage.testkit.distagesuite.generic.DistageTestExampleBase.*
 import izumi.distage.testkit.model.TestConfig
-import izumi.distage.testkit.scalatest.{AssertZIO, Spec1, Spec2, SpecZIO}
+import izumi.distage.testkit.scalatest.*
 import izumi.distage.testkit.services.scalatest.dstest.DistageAbstractScalatestSpec
+import izumi.functional.bio.{Exit, F, IO2}
 import izumi.functional.quasi.QuasiIO
 import izumi.functional.quasi.QuasiIO.syntax.*
 import izumi.fundamentals.platform.language.Quirks
 import izumi.fundamentals.platform.language.Quirks.*
 import org.scalatest.exceptions.TestFailedException
+import cats.effect.kernel.Sync
+import cats.effect.IO as CIO
 import zio.{Task, ZEnvironment, ZIO}
 
 import java.util.concurrent.atomic.{AtomicInteger, AtomicReference}
@@ -360,3 +363,84 @@ abstract class ForcedRootTest[F[_]: QuasiIO: TagK: DefaultModule] extends Spec1[
       assert(locatorRef.get.get[ForcedRootProbe].started)
   }
 }
+
+class ShorthandAssertionsTestZIO extends SpecZIO with AssertZIO {
+  "shorthand assertions ZIO" should {
+    "support short assert versions" in {
+      for {
+        _ <- assertIO(ZIO.attempt(42))(_ == 42)
+        _ <- assertIO(ZIO.attempt(42))(_ != 21)
+        _ <- assertIO(ZIO.attempt(List("one", "two")))(_.nonEmpty)
+        _ <- assertIO(ZIO.attempt(42))(_ == 21).sandboxExit.map {
+          case Exit.Termination(err, _, _) =>
+            assert(err.getMessage.contains("42 did not equal 21"))
+          case other =>
+            fail(s"Unexpected error: $other")
+        }
+
+        _ <- assertIO(ZIO.attempt(42), ZIO.attempt(21))(_ > _)
+        _ <- assertIO(ZIO.attempt("test"), ZIO.attempt(4))(_.length == _)
+      } yield ()
+    }
+  }
+}
+
+class ShorthandAssertionsTestCIO extends Spec1[CIO] with AssertCIO {
+  "shorthand assertions CIO" should {
+    "support short assert versions" in {
+      for {
+        _ <- assertIO(CIO.pure(42))(_ == 42)
+        _ <- assertIO(CIO.pure(42))(_ != 21)
+        _ <- assertIO(CIO.pure(List("one", "two")))(_.nonEmpty)
+        err <- assertIO(CIO.pure(42))(_ == 21).attempt
+        _ <- assertIO(err.left.exists(_.getMessage.contains("42 did not equal 21")))
+
+        _ <- assertIO(CIO.pure(42), CIO.pure(21))(_ > _)
+        _ <- assertIO(CIO.pure("test"), CIO.pure(4))(_.length == _)
+      } yield ()
+    }
+  }
+}
+
+abstract class ShorthandAssertionsIO2TestBase[F[+_, +_]: IO2: TagKK: DefaultModule2] extends Spec2[F] with AssertIO2[F] {
+  "shorthand assertions IO2" should {
+    "support short assert versions" in {
+      for {
+        _ <- assertIO(F.syncThrowable(42))(_ == 42)
+        _ <- assertIO(F.syncThrowable(42))(_ != 21)
+        _ <- assertIO(F.syncThrowable(List("one", "two")))(_.nonEmpty)
+        _ <- assertIO(F.syncThrowable(42))(_ == 21).sandboxExit.map {
+          case Exit.Termination(err, _, _) =>
+            assert(err.getMessage.contains("42 did not equal 21"))
+          case other =>
+            fail(s"Unexpected error: $other")
+        }
+        _ <- assertIO(F.syncThrowable(42), F.syncThrowable(21))(_ > _)
+        _ <- assertIO(F.syncThrowable("test"), F.syncThrowable(4))(_.length == _)
+      } yield ()
+    }
+  }
+}
+
+class ShorthandAssertionsTestIO2 extends ShorthandAssertionsIO2TestBase[zio.IO]
+
+abstract class ShorthandAssertionsTestSyncBase[F[_]: TagK: DefaultModule](implicit F: Sync[F]) extends Spec1[F] with AssertSync[F] {
+  import cats.syntax.applicativeError.catsSyntaxApplicativeError
+
+  "shorthand assertions IO2" should {
+    "support short assert versions" in {
+      for {
+        _ <- assertIO(F.pure(42))(_ == 42)
+        _ <- assertIO(F.pure(42))(_ != 21)
+        _ <- assertIO(F.pure(List("one", "two")))(_.nonEmpty)
+        err <- assertIO(F.pure(42))(_ == 21).attempt
+        _ <- assertIO(err.left.exists(_.getMessage.contains("42 did not equal 21")))
+
+        _ <- assertIO(F.pure(42), F.pure(21))(_ > _)
+        _ <- assertIO(F.pure("test"), F.pure(4))(_.length == _)
+      } yield ()
+    }
+  }
+}
+
+class ShorthandAssertionsTestSync extends ShorthandAssertionsTestSyncBase[CIO]