diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d062124..bdc837d0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: os: [ubuntu-latest] scala: [2.12, 2.13, 3] java: [temurin@11, temurin@17] - project: [rootJVM] + project: [rootJS, rootJVM] runs-on: ${{ matrix.os }} timeout-minutes: 60 steps: @@ -72,6 +72,10 @@ jobs: if: matrix.java == 'temurin@11' && matrix.os == 'ubuntu-latest' run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' headerCheckAll scalafmtCheckAll 'project /' scalafmtSbtCheck + - name: scalaJSLink + if: matrix.project == 'rootJS' + run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' Test/scalaJSLinkerResult + - name: Test run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' test @@ -85,11 +89,11 @@ jobs: - name: Make target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/master') - run: mkdir -p fs2/target zio/target core/target interop/target project/target + run: mkdir -p zio/.js/target interop/.jvm/target interop/.js/target core/.js/target zio/.jvm/target core/.jvm/target fs2/.js/target fs2/.jvm/target project/target - name: Compress target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/master') - run: tar cf targets.tar fs2/target zio/target core/target interop/target project/target + run: tar cf targets.tar zio/.js/target interop/.jvm/target interop/.js/target core/.js/target zio/.jvm/target core/.jvm/target fs2/.js/target fs2/.jvm/target project/target - name: Upload target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/master') @@ -139,6 +143,16 @@ jobs: if: matrix.java == 'temurin@17' && steps.setup-java-temurin-17.outputs.cache-hit == 'false' run: sbt +update + - name: Download target directories (2.12, rootJS) + uses: actions/download-artifact@v3 + with: + name: target-${{ matrix.os }}-${{ matrix.java }}-2.12-rootJS + + - name: Inflate target directories (2.12, rootJS) + run: | + tar xf targets.tar + rm targets.tar + - name: Download target directories (2.12, rootJVM) uses: actions/download-artifact@v3 with: @@ -149,6 +163,16 @@ jobs: tar xf targets.tar rm targets.tar + - name: Download target directories (2.13, rootJS) + uses: actions/download-artifact@v3 + with: + name: target-${{ matrix.os }}-${{ matrix.java }}-2.13-rootJS + + - name: Inflate target directories (2.13, rootJS) + run: | + tar xf targets.tar + rm targets.tar + - name: Download target directories (2.13, rootJVM) uses: actions/download-artifact@v3 with: @@ -159,6 +183,16 @@ jobs: tar xf targets.tar rm targets.tar + - name: Download target directories (3, rootJS) + uses: actions/download-artifact@v3 + with: + name: target-${{ matrix.os }}-${{ matrix.java }}-3-rootJS + + - name: Inflate target directories (3, rootJS) + run: | + tar xf targets.tar + rm targets.tar + - name: Download target directories (3, rootJVM) uses: actions/download-artifact@v3 with: diff --git a/build.sbt b/build.sbt index 7d971496..ff662503 100644 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ val scala_212 = "2.12.18" val scala_213 = "2.13.12" val scala_3 = "3.3.1" -val versionOf = new { +val V = new { val cats = "2.10.0" val catsEffect = "3.5.2" val fs2 = "3.9.3" @@ -10,42 +10,25 @@ val versionOf = new { val log4s = "1.10.0" val scalaCheck = "1.17.0" val scalaTest = "3.2.17" - val zio = "2.0.19" val scribe = "3.12.2" + val zio = "2.0.19" } -lazy val coreDependencies = Seq( - "org.log4s" %% "log4s" % versionOf.log4s, - "com.outr" %% "scribe" % versionOf.scribe -).map(_.withSources) - -lazy val fs2Dependencies = Seq( - "org.log4s" %% "log4s" % versionOf.log4s, - "com.outr" %% "scribe" % versionOf.scribe, - "org.typelevel" %% "cats-core" % versionOf.cats, - "org.typelevel" %% "cats-effect" % versionOf.catsEffect, - "co.fs2" %% "fs2-core" % versionOf.fs2 -).map(_.withSources) - -lazy val zioDependencies = Seq( - "org.log4s" %% "log4s" % versionOf.log4s, - "com.outr" %% "scribe" % versionOf.scribe, - "dev.zio" %% "zio" % versionOf.zio -).map(_.withSources) - -lazy val interopDependencies = Seq( - "org.typelevel" %% "log4cats-core" % versionOf.log4cats, - "org.typelevel" %% "log4cats-slf4j" % versionOf.log4cats % Test, - "org.typelevel" %% "cats-effect" % versionOf.catsEffect % Test -).map(_.withSources) - -lazy val testDependencies = Seq( - "org.scalacheck" %% "scalacheck" % versionOf.scalaCheck % Test, - "org.scalatest" %% "scalatest" % versionOf.scalaTest % Test, - "org.log4s" %% "log4s-testing" % versionOf.log4s % Test -).map(_.withSources) +val D = new { + lazy val `cats-core` = Def.setting("org.typelevel" %%% "cats-core" % V.cats) + lazy val `cats-effect` = Def.setting("org.typelevel" %%% "cats-effect" % V.catsEffect) + lazy val `fs2-core` = Def.setting("co.fs2" %%% "fs2-core" % V.fs2) + lazy val `log4cats-core` = Def.setting("org.typelevel" %%% "log4cats-core" % V.log4cats) + lazy val `log4cats-testing` = Def.setting("org.typelevel" %%% "log4cats-testing" % V.log4cats) + lazy val log4s = Def.setting("org.log4s" %%% "log4s" % V.log4s) + lazy val `log4s-testing` = Def.setting("org.log4s" %%% "log4s-testing" % V.log4s) + lazy val scalacheck = Def.setting("org.scalacheck" %%% "scalacheck" % V.scalaCheck) + lazy val scalatest = Def.setting("org.scalatest" %%% "scalatest" % V.scalaTest) + lazy val scribe = Def.setting("com.outr" %%% "scribe" % V.scribe) + lazy val zio = Def.setting("dev.zio" %%% "zio" % V.zio) +} -ThisBuild / tlBaseVersion := "0.17" +ThisBuild / tlBaseVersion := "0.18" ThisBuild / tlCiReleaseBranches := Seq("master") ThisBuild / tlVersionIntroduced := Map("3" -> "0.16.3") ThisBuild / tlSonatypeUseLegacyHost := true @@ -62,43 +45,65 @@ ThisBuild / githubWorkflowJavaVersions := Seq( ThisBuild / githubWorkflowBuildMatrixExclusions := Seq() ThisBuild / Test / parallelExecution := false -ThisBuild / libraryDependencies ++= testDependencies +ThisBuild / libraryDependencies ++= Seq( + D.scalacheck.value % Test, + D.scalatest.value % Test +) lazy val root = tlCrossRootProject .aggregate(core, fs2, zio, interop) .settings( - addCommandAlias("fmt", "scalafmt;Test/scalafmt;scalafmtSbt"), - addCommandAlias("checkFormat", "scalafmtCheck;Test/scalafmtCheck;scalafmtSbtCheck"), - addCommandAlias("check", "checkFormat;clean;test") + addCommandAlias("fmt", "scalafmt; Test/scalafmt; scalafmtSbt"), + addCommandAlias("checkFormat", "scalafmtCheck; Test/scalafmtCheck; scalafmtSbtCheck"), + addCommandAlias("check", "checkFormat; clean; test") ) -lazy val core = project +lazy val core = crossProject(JVMPlatform, JSPlatform) + .crossType(CrossType.Pure) .in(file("core")) .settings( name := "log-effect-core", - libraryDependencies ++= coreDependencies + libraryDependencies ++= Seq(D.log4s.value, D.scribe.value) ) -lazy val fs2 = project +lazy val fs2 = crossProject(JVMPlatform, JSPlatform) + .crossType(CrossType.Pure) .in(file("fs2")) .dependsOn(core) .settings( name := "log-effect-fs2", - libraryDependencies ++= fs2Dependencies + libraryDependencies ++= Seq( + D.`cats-core`.value, + D.`cats-effect`.value, + D.`fs2-core`.value, + D.log4s.value, + D.scribe.value + ) ) -lazy val zio = project +lazy val zio = crossProject(JVMPlatform, JSPlatform) + .crossType(CrossType.Pure) .in(file("zio")) .dependsOn(core) .settings( name := "log-effect-zio", - libraryDependencies ++= zioDependencies + libraryDependencies ++= Seq( + D.log4s.value, + D.`log4s-testing`.value % Test, + D.scribe.value, + D.zio.value + ) ) -lazy val interop = project +lazy val interop = crossProject(JVMPlatform, JSPlatform) + .crossType(CrossType.Pure) .in(file("interop")) .dependsOn(core, fs2) .settings( name := "log-effect-interop", - libraryDependencies ++= interopDependencies + libraryDependencies ++= Seq( + D.`cats-effect`.value % Test, + D.`log4cats-core`.value, + D.`log4cats-testing`.value % Test + ) ) diff --git a/fs2/src/test/scala/log/effect/fs2/TestLogCapture.scala b/fs2/src/test/scala/log/effect/fs2/TestLogCapture.scala index 7ed4bae6..fd1ce535 100644 --- a/fs2/src/test/scala/log/effect/fs2/TestLogCapture.scala +++ b/fs2/src/test/scala/log/effect/fs2/TestLogCapture.scala @@ -25,7 +25,6 @@ package fs2 import java.io.{ByteArrayOutputStream, PrintStream} import cats.effect.IO -import cats.effect.unsafe.implicits.global trait TestLogCapture { @@ -33,7 +32,7 @@ trait TestLogCapture { val lowerStream = new ByteArrayOutputStream() val outStream = new PrintStream(lowerStream) - Console.withOut(outStream)(aWrite.unsafeRunSync()) + Console.withOut(outStream)(aWrite.syncStep(Int.MaxValue).unsafeRunSync()) lowerStream.toString } diff --git a/interop/src/test/resources/logback-test.xml b/interop/src/test/resources/logback-test.xml deleted file mode 100644 index 1222e423..00000000 --- a/interop/src/test/resources/logback-test.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/interop/src/test/scala/InteropLogSelectorTest.scala b/interop/src/test/scala/InteropLogSelectorTest.scala index ad5ccd30..61fd2214 100644 --- a/interop/src/test/scala/InteropLogSelectorTest.scala +++ b/interop/src/test/scala/InteropLogSelectorTest.scala @@ -21,15 +21,12 @@ import cats.effect.{IO, Resource, Sync} import cats.syntax.flatMap._ -import org.typelevel.log4cats.SelfAwareStructuredLogger -import org.typelevel.log4cats.slf4j.Slf4jLogger import log.effect.fs2.LogSelector -import log.effect.interop.TestLogCapture +import org.typelevel.log4cats.testing.StructuredTestingLogger import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpecLike -import org.slf4j.Logger -final class InteropLogSelectorTest extends AnyWordSpecLike with Matchers with TestLogCapture { +final class InteropLogSelectorTest extends AnyWordSpecLike with Matchers { private[this] sealed trait ALoggingClient[F[_]] { def address: String @@ -49,34 +46,33 @@ final class InteropLogSelectorTest extends AnyWordSpecLike with Matchers with Te )(_ => F.unit) } + private[this] def testLogger[F[_]: Sync]: F[StructuredTestingLogger[F]] = + Sync[F].pure(StructuredTestingLogger.impl()) + "Log selector can infer the correct log if a log4cats Logger is in scope" in { import log.effect.interop.log4cats._ - def buildLog4catsLogger[F[_]: Sync](logger: Logger): F[SelfAwareStructuredLogger[F]] = - Slf4jLogger.fromSlf4j[F](logger) - - def useLoggingClient[F[_]: Sync](address: String)(logger: Logger): F[Unit] = - buildLog4catsLogger[F](logger) >>= { implicit l => + def useLoggingClient[F[_]: Sync](address: String): F[StructuredTestingLogger[F]] = + testLogger[F].flatTap { implicit l => ALoggingClient[F](address).use(_.useIt) } - val logged = capturedLog4sOutOf(useLoggingClient[IO]("an address")).map(_.message) + val logged = + useLoggingClient[IO]("an address").flatMap(_.logged).syncStep(Int.MaxValue).unsafeRunSync() - logged shouldBe Seq("this is a test") + logged shouldBe Right(Seq(StructuredTestingLogger.INFO("this is a test", None, Map.empty))) } "Log selector will default to no logs if no log4cats Logger is in scope" in { - def buildLog4catsLogger[F[_]: Sync](logger: Logger): F[SelfAwareStructuredLogger[F]] = - Slf4jLogger.fromSlf4j[F](logger) - - def useLoggingClient[F[_]: Sync](address: String)(logger: Logger): F[Unit] = - buildLog4catsLogger[F](logger) >>= { _ => + def useLoggingClient[F[_]: Sync](address: String): F[StructuredTestingLogger[F]] = + testLogger[F].flatTap { _ => ALoggingClient[F](address).use(_.useIt) } - val logged = capturedLog4sOutOf(useLoggingClient[IO]("an address")) + val logged = + useLoggingClient[IO]("an address").flatMap(_.logged).syncStep(Int.MaxValue).unsafeRunSync() - logged shouldBe Seq() + logged shouldBe Right(Seq()) } } diff --git a/interop/src/test/scala/InteropTest.scala b/interop/src/test/scala/InteropTest.scala index 463ed59f..ba61494b 100644 --- a/interop/src/test/scala/InteropTest.scala +++ b/interop/src/test/scala/InteropTest.scala @@ -21,10 +21,8 @@ import cats.effect.{Resource, Sync} import log.effect.LogWriter -import log.effect.interop.TestLogCapture import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpecLike -import org.typelevel.log4cats.SelfAwareStructuredLogger import scala.annotation.nowarn @@ -44,12 +42,11 @@ object RedisClient { )(_ => F.unit) } -final class InteropTest extends AnyWordSpecLike with Matchers with TestLogCapture { +final class InteropTest extends AnyWordSpecLike with Matchers { "A LogWriter instance can be derived from a log4cats Logger" in { import cats.effect.IO - import cats.effect.unsafe.implicits.global - import org.typelevel.log4cats.slf4j.Slf4jLogger + import org.typelevel.log4cats.testing.StructuredTestingLogger import log.effect.internal.Show final class A() @@ -58,37 +55,36 @@ final class InteropTest extends AnyWordSpecLike with Matchers with TestLogCaptur (_: A) => "an A" } - val logged = capturedLog4sOutOf { logger => - import log.effect.interop.log4cats._ - - implicit val buildMessageLogger: SelfAwareStructuredLogger[IO] = - Slf4jLogger.fromSlf4j[IO](logger).unsafeRunSync() - - val lw = implicitly[LogWriter[IO]] - - lw.trace("a message") >> - lw.trace(new Exception("an exception")) >> - lw.trace("a message", new Exception("an exception")) >> - lw.trace(new A()) >> - lw.debug("a message") >> - lw.debug(new Exception("an exception")) >> - lw.debug("a message", new Exception("an exception")) >> - lw.debug(new A()) >> - lw.info("a message") >> - lw.info(new Exception("an exception")) >> - lw.info("a message", new Exception("an exception")) >> - lw.info(new A()) >> - lw.warn("a message") >> - lw.warn(new Exception("an exception")) >> - lw.warn("a message", new Exception("an exception")) >> - lw.warn(new A()) >> - lw.error("a message") >> - lw.error(new Exception("an exception")) >> - lw.error("a message", new Exception("an exception")) >> - lw.error(new A()) - } + implicit val testLogger: StructuredTestingLogger[IO] = StructuredTestingLogger.impl[IO]() + + import log.effect.interop.log4cats._ + + val lw = implicitly[LogWriter[IO]] + + val logs = lw.trace("a message") >> + lw.trace(new Exception("an exception")) >> + lw.trace("a message", new Exception("an exception")) >> + lw.trace(new A()) >> + lw.debug("a message") >> + lw.debug(new Exception("an exception")) >> + lw.debug("a message", new Exception("an exception")) >> + lw.debug(new A()) >> + lw.info("a message") >> + lw.info(new Exception("an exception")) >> + lw.info("a message", new Exception("an exception")) >> + lw.info(new A()) >> + lw.warn("a message") >> + lw.warn(new Exception("an exception")) >> + lw.warn("a message", new Exception("an exception")) >> + lw.warn(new A()) >> + lw.error("a message") >> + lw.error(new Exception("an exception")) >> + lw.error("a message", new Exception("an exception")) >> + lw.error(new A()) + + val loggedQty = (logs >> testLogger.logged.map(_.size)).syncStep(Int.MaxValue).unsafeRunSync() - logged.size shouldBe 20 + loggedQty shouldBe Right(20) } "The readme interop example compiles" in { diff --git a/interop/src/test/scala/log/effect/interop/TestLogCapture.scala b/interop/src/test/scala/log/effect/interop/TestLogCapture.scala deleted file mode 100644 index e23d6ee7..00000000 --- a/interop/src/test/scala/log/effect/interop/TestLogCapture.scala +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2023 LaserDisc - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package log.effect -package interop - -import cats.effect.IO -import cats.effect.unsafe.implicits.global -import cats.syntax.flatMap._ -import org.log4s.{LoggedEvent, TestAppender, getLogger} -import org.slf4j.Logger - -trait TestLogCapture { - - protected final def capturedLog4sOutOf( - logWrite: Logger => IO[Unit] - ): Seq[LoggedEvent] = { - val loggingAction = IO.delay(getLogger("Test Logger").logger) >>= { logger => - TestAppender.withAppender() { - logWrite(logger) - } - } - loggingAction.unsafeRunSync() - - TestAppender.dequeueAll() - } -} diff --git a/project/plugins.sbt b/project/plugins.sbt index 59a7d748..a41e56ca 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1 +1,3 @@ -addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.6.2") +addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.2") +addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.6.2")