From 245bf8502e7705ef827a6b96e6a3008ed00e356d Mon Sep 17 00:00:00 2001 From: Julien Sirocchi Date: Mon, 4 Sep 2023 15:34:34 -0700 Subject: [PATCH] add scalajs support --- build.sbt | 102 +++++++++--------- .../scala/log/effect/fs2/TestLogCapture.scala | 3 +- interop/src/test/resources/logback-test.xml | 8 -- .../test/scala/InteropLogSelectorTest.scala | 34 +++--- interop/src/test/scala/InteropTest.scala | 66 ++++++------ .../log/effect/interop/TestLogCapture.scala | 45 -------- project/plugins.sbt | 4 +- 7 files changed, 102 insertions(+), 160 deletions(-) delete mode 100644 interop/src/test/resources/logback-test.xml delete mode 100644 interop/src/test/scala/log/effect/interop/TestLogCapture.scala diff --git a/build.sbt b/build.sbt index dd8c75ce..a3bdbcbb 100644 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ val scala_212 = "2.12.18" val scala_213 = "2.13.11" val scala_3 = "3.3.0" -val versionOf = new { +val V = new { val cats = "2.9.0" val catsEffect = "3.5.1" val fs2 = "3.8.0" @@ -11,46 +11,26 @@ val versionOf = new { val log4s = "1.10.0" val scalaCheck = "1.17.0" val scalaTest = "3.2.16" - val zio = "2.0.15" val scribe = "3.11.9" + val zio = "2.0.15" } -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 -) - -lazy val compilerPluginsDependencies = Seq( - compilerPlugin( - "org.typelevel" %% "kind-projector" % versionOf.kindProjector cross CrossVersion.full +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 `kind-projector` = Def.setting( + compilerPlugin("org.typelevel" %%% "kind-projector" % V.kindProjector cross CrossVersion.full) ) -) + 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 / tlCiReleaseBranches := Seq("master") @@ -68,46 +48,68 @@ ThisBuild / githubWorkflowJavaVersions := Seq( ThisBuild / githubWorkflowBuildMatrixExclusions := Seq() ThisBuild / Test / parallelExecution := false -ThisBuild / libraryDependencies ++= testDependencies +ThisBuild / libraryDependencies ++= Seq( + D.scalacheck.value % Test, + D.scalatest.value % Test +) ThisBuild / libraryDependencies ++= { - if (tlIsScala3.value) Seq.empty else compilerPluginsDependencies + if (tlIsScala3.value) Seq.empty else Seq(D.`kind-projector`.value) } 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 dfdc8358..2111b175 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 @@ -45,12 +43,11 @@ object RedisClient { } @nowarn("msg=local method [a-zA-Z0-9]+ in value is never used") -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() @@ -59,37 +56,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.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 4a74d56a..804b429b 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1 +1,3 @@ -addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.4.22") +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.4.22")