From 663b8915961ab079b57620d4c345845bed2d87c9 Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Mon, 4 Jan 2021 17:25:27 +0000 Subject: [PATCH] Scala 3.0.0-M3 (#158) Additionally: * BUGFIX: incorrect rendering of durations * Rename forall to forEach * Build improvements --- .scalafmt.conf | 4 + build.sbt | 73 +++++--- docs/expectations.md | 2 +- docs/installation.md | 33 ++-- .../cats/src-ce2/weaver/BaseIOSuite.scala | 7 +- modules/core/src-ce2/weaver/CECompat.scala | 1 - modules/core/src-scala-2/Expect.scala | 13 ++ modules/core/src-scala-2/ScalaCompat.scala | 5 + .../src-scala-2/SourceLocationMacro.scala | 49 ++++++ modules/core/src-scala-3/ExpectMacro.scala | 18 ++ modules/core/src-scala-3/ScalaCompat.scala | 5 + .../src-scala-3/SourceLocationMacro.scala | 36 ++++ modules/core/src/weaver/Expect.scala | 50 ------ modules/core/src/weaver/Expectations.scala | 4 +- modules/core/src/weaver/ExpectyListener.scala | 41 +++++ modules/core/src/weaver/Formatter.scala | 7 +- modules/core/src/weaver/Platform.scala | 5 +- modules/core/src/weaver/Result.scala | 2 +- modules/core/src/weaver/SourceLocation.scala | 51 +----- .../core/src/weaver/TestErrorFormatter.scala | 5 +- .../main/scala/weaver/MatrixRendering.scala | 39 ++-- .../cats/test/src/DogFoodTests.scala | 22 ++- .../cats/test/src/ExpectationsTests.scala | 7 + .../cats/test/src/FormatterTests.scala | 21 +++ modules/framework/cats/test/src/Meta.scala | 6 +- .../cats/test/src/TracingTests.scala | 2 +- modules/framework/src-js/DogFoodCompat.scala | 2 +- modules/framework/src-js/RunnerCompat.scala | 2 +- modules/framework/src-jvm/DogFoodCompat.scala | 16 +- .../src/weaver/framework/package.scala | 4 +- .../src/weaver/scalacheck/Checkers.scala | 87 ++++++--- .../src/weaver/scalacheck/CheckersTest.scala | 10 +- .../scalacheck/PropertyDogFoodTest.scala | 24 ++- project/WeaverPlugin.scala | 166 ++++++++++++------ project/plugins.sbt | 1 + 35 files changed, 547 insertions(+), 273 deletions(-) create mode 100644 modules/core/src-scala-2/Expect.scala create mode 100644 modules/core/src-scala-2/ScalaCompat.scala create mode 100644 modules/core/src-scala-2/SourceLocationMacro.scala create mode 100644 modules/core/src-scala-3/ExpectMacro.scala create mode 100644 modules/core/src-scala-3/ScalaCompat.scala create mode 100644 modules/core/src-scala-3/SourceLocationMacro.scala delete mode 100644 modules/core/src/weaver/Expect.scala create mode 100644 modules/core/src/weaver/ExpectyListener.scala create mode 100644 modules/framework/cats/test/src/FormatterTests.scala diff --git a/.scalafmt.conf b/.scalafmt.conf index 7c8689eb..95ca5850 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -25,3 +25,7 @@ spaces { } project.git = true + +project.excludeFilters = [ + ".*-scala-3.*" +] diff --git a/build.sbt b/build.sbt index 1d631b08..02306cdd 100644 --- a/build.sbt +++ b/build.sbt @@ -7,16 +7,17 @@ ThisBuild / commands += Command.command("ci") { state => "test:scalafix --check" :: "clean" :: "test:compile" :: + "test:fastLinkJS" :: // do this separately as it's memory intensive "test" :: "docs/docusaurusCreateSite" :: "core/publishLocal" :: state } ThisBuild / commands += Command.command("fix") { state => - "scalafmtAll" :: - "scalafmtSbt" :: - "scalafix" :: - "test:scalafix" :: state + "scalafix" :: + "test:scalafix" :: + "scalafmtAll" :: + "scalafmtSbt" :: state } ThisBuild / commands += Command.command("release") { state => @@ -33,7 +34,6 @@ Global / (Test / testOptions) += Tests.Argument("--quickstart") lazy val root = project .in(file(".")) - .enablePlugins(ScalafixPlugin) .aggregate(allModules: _*) .configure(WeaverPlugin.profile) .settings(WeaverPlugin.doNotPublishArtifact) @@ -71,21 +71,24 @@ lazy val core = projectMatrix .in(file("modules/core")) .configure(WeaverPlugin.profile) .settings(WeaverPlugin.simpleLayout) - .crossCatsEffect + .full .configure(catsEffectDependencies) .settings( libraryDependencies ++= Seq( - "com.eed3si9n.expecty" %%% "expecty" % "0.14.1", - "org.portable-scala" %%% "portable-scala-reflect" % "1.0.0" + "com.eed3si9n.expecty" %%% "expecty" % "0.15.0", + // https://github.com/portable-scala/portable-scala-reflect/issues/23 + ("org.portable-scala" %%% "portable-scala-reflect" % "1.0.0").withDottyCompat( + scalaVersion.value) ), libraryDependencies ++= { if (virtualAxes.value.contains(VirtualAxis.jvm)) Seq( - "org.scala-js" %%% "scalajs-stubs" % "1.0.0" % "provided" + ("org.scala-js" %%% "scalajs-stubs" % "1.0.0" % "provided").withDottyCompat( + scalaVersion.value) ) else { Seq( - "io.github.cquiroz" %%% "scala-java-time" % "2.0.0" + "io.github.cquiroz" %%% "scala-java-time" % "2.1.0" ) } } @@ -109,7 +112,7 @@ val allIntegrationsCoresFilter: ScopeFilter = lazy val docs = projectMatrix .in(file("modules/docs")) - .jvmPlatform(WeaverPlugin.supportedScalaVersions) + .jvmPlatform(WeaverPlugin.supportedScala2Versions) .enablePlugins(DocusaurusPlugin, MdocPlugin) .dependsOn(core, scalacheck, cats, zio, monix, monixBio, specs2) .settings( @@ -157,6 +160,14 @@ lazy val docs = projectMatrix val integrations = process(projectsWithAxes.all(allIntegrationsCoresFilter).value) + val artifactsCE2Version = (cats.finder( + VirtualAxis.jvm, + CatsEffect2Axis).apply(scala213) / version).value + + val artifactsCE3Version = (cats.finder( + VirtualAxis.jvm, + CatsEffect3Axis).apply(scala213) / version).value + IO.write( filePath, s""" @@ -164,6 +175,8 @@ lazy val docs = projectMatrix | | object BuildMatrix { | val catsEffect3Version = ${q(catsEffect3Version)} + | val artifactsCE2Version = ${q(artifactsCE2Version)} + | val artifactsCE3Version = ${q(artifactsCE3Version)} | val effects = $effects | val integrations = $integrations | } @@ -177,18 +190,20 @@ lazy val docs = projectMatrix lazy val framework = projectMatrix .in(file("modules/framework")) .dependsOn(core) - .crossCatsEffect + .full .settings( libraryDependencies ++= { if (virtualAxes.value.contains(VirtualAxis.jvm)) Seq( - "org.scala-sbt" % "test-interface" % "1.0", - "org.scala-js" %%% "scalajs-stubs" % "1.0.0" % "provided" + "org.scala-sbt" % "test-interface" % "1.0", + ("org.scala-js" %%% "scalajs-stubs" % "1.0.0" % "provided").withDottyCompat( + scalaVersion.value) ) else Seq( - "org.scala-js" %% "scalajs-test-interface" % scalaJSVersion, - "io.github.cquiroz" %%% "scala-java-time-tzdb" % "2.0.0" % Test + ("org.scala-js" %% "scalajs-test-interface" % scalaJSVersion).withDottyCompat( + scalaVersion.value), + "io.github.cquiroz" %%% "scala-java-time-tzdb" % "2.1.0" % Test ) } ) @@ -197,7 +212,7 @@ lazy val framework = projectMatrix lazy val scalacheck = projectMatrix .in(file("modules/scalacheck")) - .crossCatsEffect + .full .dependsOn(core, cats % "test->compile") .configure(WeaverPlugin.profile) .settings(WeaverPlugin.simpleLayout) @@ -210,7 +225,7 @@ lazy val scalacheck = projectMatrix lazy val specs2 = projectMatrix .in(file("modules/specs2")) - .crossCatsEffect + .sparse(withCE3 = true, withJS = true, withScala3 = false) .dependsOn(core, cats % "test->compile") .configure(WeaverPlugin.profile) .settings( @@ -231,7 +246,7 @@ lazy val effectCores: Seq[ProjectReference] = lazy val coreCats = projectMatrix .in(file("modules/core/cats")) - .crossCatsEffect + .full .dependsOn(core) .configure(WeaverPlugin.profile) .settings(WeaverPlugin.simpleLayout) @@ -239,7 +254,7 @@ lazy val coreCats = projectMatrix lazy val coreMonix = projectMatrix .in(file("modules/core/monix")) - .onlyCatsEffect2() + .sparse(withCE3 = false, withJS = true, withScala3 = false) .dependsOn(core) .configure(WeaverPlugin.profile) .settings(WeaverPlugin.simpleLayout) @@ -252,7 +267,7 @@ lazy val coreMonix = projectMatrix lazy val coreMonixBio = projectMatrix .in(file("modules/core/monixBio")) - .onlyCatsEffect2() + .sparse(withCE3 = false, withJS = true, withScala3 = false) .dependsOn(core) .configure(WeaverPlugin.profile) .settings(WeaverPlugin.simpleLayout) @@ -265,7 +280,7 @@ lazy val coreMonixBio = projectMatrix lazy val coreZio = projectMatrix .in(file("modules/core/zio")) - .onlyCatsEffect2() + .sparse(withCE3 = false, withJS = true, withScala3 = false) .dependsOn(core) .configure(WeaverPlugin.profile) .settings(WeaverPlugin.simpleLayout) @@ -290,18 +305,20 @@ lazy val effectFrameworks: Seq[ProjectReference] = Seq( lazy val cats = projectMatrix .in(file("modules/framework/cats")) .dependsOn(framework, coreCats) - .crossCatsEffect + .full .configure(WeaverPlugin.profile) .settings(WeaverPlugin.simpleLayout) .settings( name := "cats", testFrameworks := Seq(new TestFramework("weaver.framework.CatsEffect")), - libraryDependencies += "io.github.cquiroz" %%% "scala-java-time-tzdb" % "2.0.0" % Test + libraryDependencies += { + "io.github.cquiroz" %%% "scala-java-time-tzdb" % "2.1.0" % Test + } ) lazy val monix = projectMatrix .in(file("modules/framework/monix")) - .onlyCatsEffect2() + .sparse(withCE3 = false, withJS = true, withScala3 = false) .dependsOn(framework, coreMonix) .configure(WeaverPlugin.profile) .settings(WeaverPlugin.simpleLayout) @@ -312,7 +329,7 @@ lazy val monix = projectMatrix lazy val monixBio = projectMatrix .in(file("modules/framework/monix-bio")) - .onlyCatsEffect2() + .sparse(withCE3 = false, withJS = true, withScala3 = false) .dependsOn(framework, coreMonixBio) .configure(WeaverPlugin.profile) .settings(WeaverPlugin.simpleLayout) @@ -323,7 +340,7 @@ lazy val monixBio = projectMatrix lazy val zio = projectMatrix .in(file("modules/framework/zio")) - .onlyCatsEffect2() + .sparse(withCE3 = false, withJS = true, withScala3 = false) .dependsOn(framework, coreZio) .configure(WeaverPlugin.profile) .settings(WeaverPlugin.simpleLayout) @@ -337,7 +354,7 @@ lazy val zio = projectMatrix // ################################################################################################# lazy val intellijRunner = projectMatrix - .onlyCatsEffect2(withJs = false) + .sparse(withCE3 = false, withJS = false, withScala3 = false) .in(file("modules/intellij-runner")) .dependsOn(core, framework, framework % "test->compile") .configure(WeaverPlugin.profile) diff --git a/docs/expectations.md b/docs/expectations.md index 214c63c7..de199fdd 100644 --- a/docs/expectations.md +++ b/docs/expectations.md @@ -25,7 +25,7 @@ object MySuite2 extends SimpleIOSuite { pureTest("Foldable operations") { val list = List(1,2,3) import cats.instances.list._ - forall(list)(i => expect(i > 0)) and + forEach(list)(i => expect(i > 0)) and exists(list)(i => expect(i == 3)) } diff --git a/docs/installation.md b/docs/installation.md index 255842b6..19a5919a 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -3,24 +3,25 @@ id: installation title: Installation --- -All of the artifacts from the table below are: +All of the artifacts below are available for both **JVM and Scala.js**. -1. Available for **Scala 2.12 and 2.13** -2. Available for **JVM and Scala.js** +Note, that artifacts that use Cats Effect 3 are published under a different version to those published for Cats Effect 2 (minor version bump), because they're binary incompatible. ```scala mdoc:passthrough import weaver.docs._ -val effects = Table - .create("Effect types", BuildMatrix.effects) - .render(BuildMatrix.catsEffect3Version) - -val integrations = Table - .create("Integrations", BuildMatrix.integrations) - .render(BuildMatrix.catsEffect3Version) +import BuildMatrix._ -println(effects) -println(integrations) +val effectsTable = Table + .create("Effect types", effects) + .render(catsEffect3Version, artifactsCE2Version, artifactsCE3Version) + +val integrationsTable = Table + .create("Integrations", integrations) + .render(catsEffect3Version, artifactsCE2Version, artifactsCE3Version) + +println(effectsTable) +println(integrationsTable) ``` Weaver offers effect-type specific test frameworks. The Build setup depends on @@ -28,7 +29,7 @@ the effect-type library you've elected to use (or test against). Refer yourself to the library specific pages to get the correct configuration. -* [cats](cats_effect_usage.md) -* [monix](monix_usage.md) -* [monix-bio](monix_bio_usage.md) -* [zio](zio_usage.md) +- [cats](cats_effect_usage.md) +- [monix](monix_usage.md) +- [monix-bio](monix_bio_usage.md) +- [zio](zio_usage.md) diff --git a/modules/core/cats/src-ce2/weaver/BaseIOSuite.scala b/modules/core/cats/src-ce2/weaver/BaseIOSuite.scala index c2f5ef38..ce6bd6df 100644 --- a/modules/core/cats/src-ce2/weaver/BaseIOSuite.scala +++ b/modules/core/cats/src-ce2/weaver/BaseIOSuite.scala @@ -1,9 +1,10 @@ package weaver -import cats.effect.IO +import cats.effect.{ ContextShift, IO, Timer } trait BaseIOSuite extends RunnableSuite[IO] { implicit protected def effectCompat: UnsafeRun[IO] = CatsUnsafeRun - final implicit protected def contextShift = effectCompat.contextShift - final implicit protected def timer = effectCompat.timer + final implicit protected def contextShift: ContextShift[IO] = + effectCompat.contextShift + final implicit protected def timer: Timer[IO] = effectCompat.timer } diff --git a/modules/core/src-ce2/weaver/CECompat.scala b/modules/core/src-ce2/weaver/CECompat.scala index 33e7c90f..eaa07c82 100644 --- a/modules/core/src-ce2/weaver/CECompat.scala +++ b/modules/core/src-ce2/weaver/CECompat.scala @@ -55,5 +55,4 @@ private[weaver] trait CECompat { } } } - } diff --git a/modules/core/src-scala-2/Expect.scala b/modules/core/src-scala-2/Expect.scala new file mode 100644 index 00000000..93ff078a --- /dev/null +++ b/modules/core/src-scala-2/Expect.scala @@ -0,0 +1,13 @@ +package weaver + +import com.eed3si9n.expecty._ + +class Expect + extends Recorder[Boolean, Expectations] + with UnaryRecorder[Boolean, Expectations] { + + def all(recordings: Boolean*): Expectations = + macro VarargsRecorderMacro.apply[Boolean, Expectations] + + override lazy val listener = new ExpectyListener +} diff --git a/modules/core/src-scala-2/ScalaCompat.scala b/modules/core/src-scala-2/ScalaCompat.scala new file mode 100644 index 00000000..c020fcfa --- /dev/null +++ b/modules/core/src-scala-2/ScalaCompat.scala @@ -0,0 +1,5 @@ +package weaver + +object ScalaCompat { + def isScala3: Boolean = false +} diff --git a/modules/core/src-scala-2/SourceLocationMacro.scala b/modules/core/src-scala-2/SourceLocationMacro.scala new file mode 100644 index 00000000..ef0bbc8f --- /dev/null +++ b/modules/core/src-scala-2/SourceLocationMacro.scala @@ -0,0 +1,49 @@ +package weaver + +// kudos to https://github.com/monix/minitest +// format: off +import scala.reflect.macros.whitebox + +trait SourceLocationMacro { + + import macros._ + + trait Here { + /** + * Pulls source location without being affected by implicit scope. + */ + def here: SourceLocation = macro Macros.fromContext + } + + implicit def fromContext: SourceLocation = + macro Macros.fromContext + + +} + +object macros { + class Macros(val c: whitebox.Context) { + import c.universe._ + + def fromContext: Tree = { + val (pathExpr, relPathExpr, lineExpr) = getSourceLocation + val SourceLocationSym = symbolOf[SourceLocation].companion + q"""$SourceLocationSym($pathExpr, $relPathExpr, $lineExpr)""" + } + + private def getSourceLocation = { + val pwd = java.nio.file.Paths.get("").toAbsolutePath + val p = c.enclosingPosition.source.path + val abstractFile = c.enclosingPosition.source.file + + val rp = if (!abstractFile.isVirtual){ + pwd.relativize(abstractFile.file.toPath()).toString() + } else p + + val line = c.Expr[Int](Literal(Constant(c.enclosingPosition.line))) + (p, rp, line) + } + + } +} +// format: on diff --git a/modules/core/src-scala-3/ExpectMacro.scala b/modules/core/src-scala-3/ExpectMacro.scala new file mode 100644 index 00000000..72f394f5 --- /dev/null +++ b/modules/core/src-scala-3/ExpectMacro.scala @@ -0,0 +1,18 @@ +package weaver + +import cats.data.{ NonEmptyList, ValidatedNel } +import cats.syntax.all._ + +import com.eed3si9n.expecty._ + +import scala.quoted._ + +class Expect + extends Recorder[Boolean, Expectations] + with UnaryRecorder[Boolean, Expectations] { + + inline def all(inline recordings: Boolean*): Expectations = + ${ RecorderMacro.varargs('recordings, 'listener) } + + override lazy val listener = new ExpectyListener +} diff --git a/modules/core/src-scala-3/ScalaCompat.scala b/modules/core/src-scala-3/ScalaCompat.scala new file mode 100644 index 00000000..f632d49e --- /dev/null +++ b/modules/core/src-scala-3/ScalaCompat.scala @@ -0,0 +1,5 @@ +package weaver + +object ScalaCompat { + def isScala3: Boolean = true +} diff --git a/modules/core/src-scala-3/SourceLocationMacro.scala b/modules/core/src-scala-3/SourceLocationMacro.scala new file mode 100644 index 00000000..d6c6c941 --- /dev/null +++ b/modules/core/src-scala-3/SourceLocationMacro.scala @@ -0,0 +1,36 @@ +package weaver + +// kudos to https://github.com/monix/minitest +// format: off + +import scala.quoted._ + +trait SourceLocationMacro { + trait Here { + /** + * Pulls source location without being affected by implicit scope. + */ + inline def here: SourceLocation = ${macros.fromContextImpl} + } + + implicit inline def fromContext: SourceLocation = ${macros.fromContextImpl} +} + +object macros { + def fromContextImpl(using ctx: Quotes): Expr[SourceLocation] = { + import ctx.reflect.Position._ + import ctx.reflect._ + + val pwd = java.nio.file.Paths.get("").toAbsolutePath + + val position = Position.ofMacroExpansion + + val rp = Expr(pwd.relativize(position.sourceFile.jpath).toString) + val absPath = Expr(position.sourceFile.jpath.toAbsolutePath.toString) + val l = Expr(position.startLine + 1) + + '{new SourceLocation($absPath, $rp, $l) } + } +} + +// format: on diff --git a/modules/core/src/weaver/Expect.scala b/modules/core/src/weaver/Expect.scala deleted file mode 100644 index 9d546867..00000000 --- a/modules/core/src/weaver/Expect.scala +++ /dev/null @@ -1,50 +0,0 @@ -package weaver - -import cats.data.{ NonEmptyList, ValidatedNel } -import cats.syntax.all._ - -import com.eed3si9n.expecty._ - -class Expect - extends Recorder[Boolean, Expectations] - with UnaryRecorder[Boolean, Expectations] { - - def all(recordings: Boolean*): Expectations = - macro VarargsRecorderMacro.apply[Boolean, Expectations] - - class ExpectyListener extends RecorderListener[Boolean, Expectations] { - def sourceLocation(loc: Location): SourceLocation = { - SourceLocation(loc.path, loc.relativePath, loc.line) - } - - override def expressionRecorded( - recordedExpr: RecordedExpression[Boolean], - recordedMessage: Function0[String]): Unit = {} - - override def recordingCompleted( - recording: Recording[Boolean], - recordedMessage: Function0[String]): Expectations = { - type Exp = ValidatedNel[AssertionException, Unit] - val res = recording.recordedExprs.foldMap[Exp] { - expr => - lazy val rendering: String = - new ExpressionRenderer(showTypes = false).render(expr) - - if (!expr.value) { - val msg = recordedMessage() - val header = - "assertion failed" + - (if (msg == "") "" - else ": " + msg) - val fullMessage = header + "\n\n" + rendering - val sourceLoc = sourceLocation(expr.location) - val sourceLocs = NonEmptyList.of(sourceLoc) - new AssertionException(fullMessage, sourceLocs).invalidNel - } else ().validNel - } - Expectations(res) - } - } - - override lazy val listener = new ExpectyListener -} diff --git a/modules/core/src/weaver/Expectations.scala b/modules/core/src/weaver/Expectations.scala index b28e9b1e..85385a60 100644 --- a/modules/core/src/weaver/Expectations.scala +++ b/modules/core/src/weaver/Expectations.scala @@ -116,7 +116,7 @@ object Expectations { * Checks that an assertion is true for all elements in a foldable. * Succeeds if the foldable is empty. */ - def forall[L[_], A](la: L[A])(f: A => Expectations)( + def forEach[L[_], A](la: L[A])(f: A => Expectations)( implicit L: Foldable[L]): Expectations = la.foldMap(f) /** @@ -132,7 +132,7 @@ object Expectations { * Alias to forall */ def inEach[L[_], A](la: L[A])(f: A => Expectations)( - implicit L: Foldable[L]): Expectations = forall(la)(f) + implicit L: Foldable[L]): Expectations = forEach(la)(f) def verify(condition: Boolean, hint: String)( implicit pos: SourceLocation): Expectations = diff --git a/modules/core/src/weaver/ExpectyListener.scala b/modules/core/src/weaver/ExpectyListener.scala new file mode 100644 index 00000000..ed88301f --- /dev/null +++ b/modules/core/src/weaver/ExpectyListener.scala @@ -0,0 +1,41 @@ +package weaver + +import cats.data.{ NonEmptyList, ValidatedNel } +import cats.syntax.all._ + +import com.eed3si9n.expecty._ + +class ExpectyListener extends RecorderListener[Boolean, Expectations] { + def sourceLocation(loc: Location): SourceLocation = { + SourceLocation(loc.path, loc.relativePath, loc.line) + } + + override def expressionRecorded( + recordedExpr: RecordedExpression[Boolean], + recordedMessage: Function0[String]): Unit = {} + + override def recordingCompleted( + recording: Recording[Boolean], + recordedMessage: Function0[String]): Expectations = { + type Exp = ValidatedNel[AssertionException, Unit] + val res = recording.recordedExprs.foldMap[Exp] { + expr => + lazy val rendering: String = + new ExpressionRenderer(showTypes = false, shortString = true).render( + expr) + + if (!expr.value) { + val msg = recordedMessage() + val header = + "assertion failed" + + (if (msg == "") "" + else ": " + msg) + val fullMessage = header + "\n\n" + rendering + val sourceLoc = sourceLocation(expr.location) + val sourceLocs = NonEmptyList.of(sourceLoc) + new AssertionException(fullMessage, sourceLocs).invalidNel + } else ().validNel + } + Expectations(res) + } +} diff --git a/modules/core/src/weaver/Formatter.scala b/modules/core/src/weaver/Formatter.scala index 01ec16f3..6da10ab4 100644 --- a/modules/core/src/weaver/Formatter.scala +++ b/modules/core/src/weaver/Formatter.scala @@ -122,11 +122,12 @@ object Formatter { val fullMinutes = seconds / 60 val remSeconds = seconds % 60 - if (remSeconds == 0) { + if (remSeconds == 0) s"${fullMinutes}min" - } else { + else if (remSeconds >= 10L) s"${fullMinutes}:${remSeconds}min" - } + else + s"${fullMinutes}:0${remSeconds}min" } } } diff --git a/modules/core/src/weaver/Platform.scala b/modules/core/src/weaver/Platform.scala index 0d65fe1a..9c9d6b67 100644 --- a/modules/core/src/weaver/Platform.scala +++ b/modules/core/src/weaver/Platform.scala @@ -3,8 +3,9 @@ package weaver sealed abstract class Platform(val name: String) object Platform { - def isJVM: Boolean = PlatformCompat.platform == JVM - def isJS: Boolean = PlatformCompat.platform == JS + def isJVM: Boolean = PlatformCompat.platform == JVM + def isJS: Boolean = PlatformCompat.platform == JS + def isScala3: Boolean = ScalaCompat.isScala3 case object JS extends Platform("js") case object JVM extends Platform("jvm") diff --git a/modules/core/src/weaver/Result.scala b/modules/core/src/weaver/Result.scala index 9850175c..7665eec6 100644 --- a/modules/core/src/weaver/Result.scala +++ b/modules/core/src/weaver/Result.scala @@ -17,7 +17,7 @@ object Result { Result.Failure(ex.message, Some(ex), ex.locations.toList))) } - final case object Success extends Result { + case object Success extends Result { def formatted: Option[String] = None } diff --git a/modules/core/src/weaver/SourceLocation.scala b/modules/core/src/weaver/SourceLocation.scala index af7d6b59..3fe6499b 100644 --- a/modules/core/src/weaver/SourceLocation.scala +++ b/modules/core/src/weaver/SourceLocation.scala @@ -1,51 +1,12 @@ package weaver // kudos to https://github.com/monix/minitest -// format: off -import scala.reflect.macros.whitebox - final case class SourceLocation( - filePath: String, - fileRelativePath: String, - line: Int -){ - def fileName : Option[String] = filePath.split("/").lastOption + filePath: String, + fileRelativePath: String, + line: Int +) { + def fileName: Option[String] = filePath.split("/").lastOption } -object SourceLocation { - - trait Here { - /** - * Pulls source location without being affected by implicit scope. - */ - def here: SourceLocation = macro Macros.fromContext - } - - implicit def fromContext: SourceLocation = - macro Macros.fromContext - - class Macros(val c: whitebox.Context) { - import c.universe._ - - def fromContext: Tree = { - val (pathExpr, relPathExpr, lineExpr) = getSourceLocation - val SourceLocationSym = symbolOf[SourceLocation].companion - q"""$SourceLocationSym($pathExpr, $relPathExpr, $lineExpr)""" - } - - private def getSourceLocation = { - val pwd = java.nio.file.Paths.get("").toAbsolutePath - val p = c.enclosingPosition.source.path - val abstractFile = c.enclosingPosition.source.file - - val rp = if (!abstractFile.isVirtual){ - pwd.relativize(abstractFile.file.toPath()).toString() - } else p - - val line = c.Expr[Int](Literal(Constant(c.enclosingPosition.line))) - (p, rp, line) - } - - } -} -// format: on +object SourceLocation extends SourceLocationMacro diff --git a/modules/core/src/weaver/TestErrorFormatter.scala b/modules/core/src/weaver/TestErrorFormatter.scala index cea3ebdf..ae3bb556 100644 --- a/modules/core/src/weaver/TestErrorFormatter.scala +++ b/modules/core/src/weaver/TestErrorFormatter.scala @@ -55,9 +55,8 @@ object TestErrorFormatter { elements.map(el => el -> exclusion(el)).foreach { case (el, exclusion) => (latest, exclusion) match { - case (Some(Snip(pack)), Some(excl)) if pack == excl => () - case (Some(Snip(pack)), Some(excl)) if pack != excl => - append(Snip(excl)) + case (Some(Snip(pack)), Some(excl)) => + if (pack != excl) append(Snip(excl)) case (_, None) => append(Element(el)) case (None, Some(excl)) => diff --git a/modules/docs/src/main/scala/weaver/MatrixRendering.scala b/modules/docs/src/main/scala/weaver/MatrixRendering.scala index c0125f97..fee7444c 100644 --- a/modules/docs/src/main/scala/weaver/MatrixRendering.scala +++ b/modules/docs/src/main/scala/weaver/MatrixRendering.scala @@ -14,8 +14,8 @@ case class Artifact( ) case class Cell( - jvm: Boolean, - js: Boolean, + jvm: Seq[String], + js: Seq[String], version: String ) @@ -39,16 +39,33 @@ case class Table( def _cell(c: Option[Cell]): String = { c match { case Some(c) => - if (c.jvm && c.js) { - s"✅ `${c.version}`" - } else "❌" // TODO: do we always assume platform-complete artifacts? + val allVersions = (c.jvm.toSet ++ c.js.toSet).map { + case ver if ver.startsWith("2.") => + ver.split("\\.").take(2).mkString(".") + case other => other + }.toList.sorted + + if (c.jvm.nonEmpty && c.js.nonEmpty) + s"✅ Scala ${allVersions.mkString(", ")}" + else + "❌" case None => "❌" } } - def render(catsEffect3Version: String) = { + def render( + catsEffect3Version: String, + ce2ArtifactsVersion: String, + ce3ArtifactsVersion: String) = { val sb = new StringBuilder - sb.append(_row(Seq(name, "Cats Effect 2", s"Cats Effect $catsEffect3Version"), header = true)) + sb.append(_row( + Seq( + name, + s"Cats Effect 2

Weaver version: `$ce2ArtifactsVersion`", + s"Cats Effect $catsEffect3Version

Weaver version: `$ce3ArtifactsVersion`" + ), + header = true + )) rows.map { case Row(name, ce2, ce3) => sb.append(_row(Seq(name, _cell(ce2), _cell(ce3)))) @@ -70,12 +87,12 @@ object Table { def artifactsToCell(artifacts: List[Artifact]): Option[Cell] = { artifacts.map(_.version).distinct.headOption.map { version => - val hasJVM = artifacts.exists(_.jvm) - val hasJS = artifacts.exists(_.js) + val jvmVersions = artifacts.filter(_.jvm).map(_.scalaVersion) + val jsVersions = artifacts.filter(_.js).map(_.scalaVersion) Cell( - jvm = hasJVM, - js = hasJS, + jvm = jvmVersions, + js = jsVersions, version = version ) } diff --git a/modules/framework/cats/test/src/DogFoodTests.scala b/modules/framework/cats/test/src/DogFoodTests.scala index 70f1d69c..1d3a8e84 100644 --- a/modules/framework/cats/test/src/DogFoodTests.scala +++ b/modules/framework/cats/test/src/DogFoodTests.scala @@ -6,7 +6,7 @@ import cats.data.Chain import cats.effect.{ IO, Resource } import cats.syntax.all._ -import sbt.testing.Status +import sbt.testing.Status.Error object DogFoodTests extends IOSuite { @@ -17,7 +17,7 @@ object DogFoodTests extends IOSuite { test("test suite reports successes events") { dogfood => import dogfood._ runSuite(Meta.MutableSuiteTest).map { - case (_, events) => forall(events)(isSuccess) + case (_, events) => forEach(events)(isSuccess) } } @@ -33,7 +33,7 @@ object DogFoodTests extends IOSuite { val name = event.fullyQualifiedName() expect.all( name == "weaver.framework.test.Meta$CrashingSuite", - event.status() == Status.Error + event.status() == Error ) } and exists(errorLogs) { log => expect.all( @@ -128,14 +128,18 @@ object DogFoodTests extends IOSuite { case LoggedEvent.Error(msg) => msg }.get - val expected = """ + // HONESTLY. + val (location, capturedExpression) = + if (Platform.isScala3) (27, "1 == 2") else (28, "expect(1 == 2)") + + val expected = s""" |- lots 0ms | of | multiline | (failure) - | assertion failed (modules/framework/cats/test/src/Meta.scala:28) + | assertion failed (modules/framework/cats/test/src/Meta.scala:$location) | - | expect(1 == 2) + | $capturedExpression | """.stripMargin.trim @@ -232,13 +236,13 @@ object DogFoodTests extends IOSuite { } private def expectEqual( - expected: String, - actual: String)(implicit loc: SourceLocation): Expectations = { + actual: String, + expected: String)(implicit loc: SourceLocation): Expectations = { if (expected.trim != actual.trim) { val report = multiLineComparisonReport(expected.trim, actual.trim) failure( - s"Output is not as expected (line-by-line-comparison below): \n\n$report") + s"Output is not as expected (line-by-line-comparison below, expected content is on the LEFT): \n\n$report") } else Expectations.Helpers.success } diff --git a/modules/framework/cats/test/src/ExpectationsTests.scala b/modules/framework/cats/test/src/ExpectationsTests.scala index ce08edb0..ce25268e 100644 --- a/modules/framework/cats/test/src/ExpectationsTests.scala +++ b/modules/framework/cats/test/src/ExpectationsTests.scala @@ -29,4 +29,11 @@ object ExpectationsTests extends SimpleIOSuite { ) } + pureTest("forall (success)") { + forEach(List(true, true))(value => expect(value == true)) + } + + pureTest("forall (failure)") { + not(forEach(List(true, false))(value => expect(value == true))) + } } diff --git a/modules/framework/cats/test/src/FormatterTests.scala b/modules/framework/cats/test/src/FormatterTests.scala new file mode 100644 index 00000000..4483cceb --- /dev/null +++ b/modules/framework/cats/test/src/FormatterTests.scala @@ -0,0 +1,21 @@ +package weaver +package framework +package test + +import scala.concurrent.duration._ + +object FormatterTests extends SimpleIOSuite { + + pureTest("rendering of durations") { + val render = Formatter.renderDuration _ + + expect.all( + render(134.millis) == "134ms", + render(25.second) == "25s", + render(61.second) == "1:01min", + render(25.minute) == "25min", + render(1.minute) == "1min", + render(150.second) == "2:30min" + ) + } +} diff --git a/modules/framework/cats/test/src/Meta.scala b/modules/framework/cats/test/src/Meta.scala index 804d6e10..0724a4b1 100644 --- a/modules/framework/cats/test/src/Meta.scala +++ b/modules/framework/cats/test/src/Meta.scala @@ -18,7 +18,7 @@ object Meta { object Rendering extends SimpleIOSuite { override implicit protected def effectCompat: UnsafeRun[IO] = SetTimeUnsafeRun - implicit val sourceLocation = TimeCop.sourceLocation + implicit val sourceLocation: SourceLocation = TimeCop.sourceLocation simpleTest("lots\nof\nmultiline\n(success)") { expect(1 == 1) @@ -40,7 +40,7 @@ object Meta { object FailingTestStatusReporting extends SimpleIOSuite { override implicit protected def effectCompat: UnsafeRun[IO] = SetTimeUnsafeRun - implicit val sourceLocation = TimeCop.sourceLocation + implicit val sourceLocation: SourceLocation = TimeCop.sourceLocation simpleTest("I succeeded") { success @@ -58,7 +58,7 @@ object Meta { object FailingSuiteWithlogs extends SimpleIOSuite { override implicit protected def effectCompat: UnsafeRun[IO] = SetTimeUnsafeRun - implicit val sourceLocation = TimeCop.sourceLocation + implicit val sourceLocation: SourceLocation = TimeCop.sourceLocation loggedTest("failure") { log => val context = Map( diff --git a/modules/framework/cats/test/src/TracingTests.scala b/modules/framework/cats/test/src/TracingTests.scala index 67885e10..ec545810 100644 --- a/modules/framework/cats/test/src/TracingTests.scala +++ b/modules/framework/cats/test/src/TracingTests.scala @@ -28,7 +28,7 @@ object TracingTests extends SimpleIOSuite { case Invalid(e) => val locations = e.head.locations.toList val paths = locations.map(_.fileRelativePath).map(standardise) - forall(paths)(p => expect(p == thisFile)) && + forEach(paths)(p => expect(p == thisFile)) && expect(locations.map(_.line).distinct.size == 4) case Valid(_) => failure("Should have been invalid") } diff --git a/modules/framework/src-js/DogFoodCompat.scala b/modules/framework/src-js/DogFoodCompat.scala index 808fb1fe..510c1cac 100644 --- a/modules/framework/src-js/DogFoodCompat.scala +++ b/modules/framework/src-js/DogFoodCompat.scala @@ -15,7 +15,7 @@ private[weaver] trait DogFoodCompat[F[_]] { self: DogFood[F] => logger: sbt.testing.Logger)(tasks: List[sbt.testing.Task]): F[Unit] = { tasks.traverse { task => self.framework.unsafeRun.async { - cb: (Either[Throwable, Unit] => Unit) => + (cb: (Either[Throwable, Unit] => Unit)) => task.execute(eventHandler, Array(logger), _ => cb(Right(()))) } }.map { _ => diff --git a/modules/framework/src-js/RunnerCompat.scala b/modules/framework/src-js/RunnerCompat.scala index 7f348ad5..96b7161c 100644 --- a/modules/framework/src-js/RunnerCompat.scala +++ b/modules/framework/src-js/RunnerCompat.scala @@ -55,7 +55,7 @@ trait RunnerCompat[F[_]] { self: sbt.testing.Runner => override def done(): String = { val sb = new StringBuilder - val s = { str: String => + val s = { (str: String) => val _ = sb.append(str + TaskCompat.lineSeparator) } diff --git a/modules/framework/src-jvm/DogFoodCompat.scala b/modules/framework/src-jvm/DogFoodCompat.scala index 4dc0ab3d..1008b26e 100644 --- a/modules/framework/src-jvm/DogFoodCompat.scala +++ b/modules/framework/src-jvm/DogFoodCompat.scala @@ -15,12 +15,16 @@ private[weaver] trait DogFoodCompat[F[_]] { self: DogFood[F] => def runTasksCompat( runner: WeaverRunner[F], eventHandler: EventHandler, - logger: Logger)(tasks: List[sbt.testing.Task]): F[Unit] = - tasks.toVector.parTraverse { task => - blocker.block(task.execute( - eventHandler, - Array(logger))) - }.void + logger: Logger)(tasks: List[sbt.testing.Task]): F[Unit] = { + + effect.void { + tasks.toVector.parTraverse { task => + blocker.block(task.execute( + eventHandler, + Array(logger))) + } + } + } def done(runner: Runner): F[String] = blocker.block(runner.done()) diff --git a/modules/framework/src/weaver/framework/package.scala b/modules/framework/src/weaver/framework/package.scala index 87a235db..90e44d3f 100644 --- a/modules/framework/src/weaver/framework/package.scala +++ b/modules/framework/src/weaver/framework/package.scala @@ -12,7 +12,7 @@ package object framework { loader: ClassLoader)( implicit A: ClassTag[A], C: ClassTag[C]): A => C = { - Reflect.lookupInstantiatableClass(qualifiedName) match { + Reflect.lookupInstantiatableClass(qualifiedName, loader) match { case None => throw new Exception(s"Could not find class $qualifiedName") with NoStackTrace @@ -40,7 +40,7 @@ package object framework { qualifiedName: String, loader: ClassLoader): Any = { val moduleName = qualifiedName + "$" - Reflect.lookupLoadableModuleClass(moduleName) match { + Reflect.lookupLoadableModuleClass(moduleName, loader) match { case None => throw new Exception(s"Could not load object $moduleName") with NoStackTrace diff --git a/modules/scalacheck/src/weaver/scalacheck/Checkers.scala b/modules/scalacheck/src/weaver/scalacheck/Checkers.scala index 7197fb58..05b2dbf3 100644 --- a/modules/scalacheck/src/weaver/scalacheck/Checkers.scala +++ b/modules/scalacheck/src/weaver/scalacheck/Checkers.scala @@ -3,7 +3,7 @@ package scalacheck import cats.effect.IO import cats.syntax.all._ -import cats.{ Defer, Show } +import cats.{ Applicative, Defer, Show } import org.scalacheck.rng.Seed import org.scalacheck.{ Arbitrary, Gen } @@ -16,40 +16,55 @@ trait IOCheckers extends Checkers[IO] { trait Checkers[F[_]] { self: EffectSuite[F] => + import Checkers._ - type Prop = F[Expectations] + type PropF[A] = Prop[F, A] + + private def liftProp[A, B: PropF](f: A => B): A => F[Expectations] = { + f andThen (b => Prop[F, B].lift(b)) + } // Configuration for property-based tests def checkConfig: CheckConfig = CheckConfig.default - def forall[A1: Arbitrary: Show](f: A1 => Prop)( + def forall[A1: Arbitrary: Show, B: PropF](f: A1 => B)( implicit loc: SourceLocation): F[Expectations] = - forall(implicitly[Arbitrary[A1]].arbitrary)(f) + forall(implicitly[Arbitrary[A1]].arbitrary)(liftProp(f)) - def forall[A1: Arbitrary: Show, A2: Arbitrary: Show](f: (A1, A2) => Prop)( + def forall[A1: Arbitrary: Show, A2: Arbitrary: Show, B: PropF](f: ( + A1, + A2) => B)( implicit loc: SourceLocation): F[Expectations] = - forall(implicitly[Arbitrary[(A1, A2)]].arbitrary)(f.tupled) + forall(implicitly[Arbitrary[(A1, A2)]].arbitrary)(liftProp( + f.tupled)) - def forall[A1: Arbitrary: Show, A2: Arbitrary: Show, A3: Arbitrary: Show]( - f: (A1, A2, A3) => Prop)( + def forall[ + A1: Arbitrary: Show, + A2: Arbitrary: Show, + A3: Arbitrary: Show, + B: PropF]( + f: (A1, A2, A3) => B)( implicit loc: SourceLocation): F[Expectations] = { implicit val tuple3Show: Show[(A1, A2, A3)] = { case (a1, a2, a3) => s"(${a1.show},${a2.show},${a3.show})" } - forall(implicitly[Arbitrary[(A1, A2, A3)]].arbitrary)(f.tupled) + forall(implicitly[Arbitrary[(A1, A2, A3)]].arbitrary)(liftProp( + f.tupled)) } def forall[ A1: Arbitrary: Show, A2: Arbitrary: Show, A3: Arbitrary: Show, - A4: Arbitrary: Show - ](f: (A1, A2, A3, A4) => Prop)( + A4: Arbitrary: Show, + B: PropF + ](f: (A1, A2, A3, A4) => B)( implicit loc: SourceLocation): F[Expectations] = { implicit val tuple3Show: Show[(A1, A2, A3, A4)] = { case (a1, a2, a3, a4) => s"(${a1.show},${a2.show},${a3.show},${a4.show})" } - forall(implicitly[Arbitrary[(A1, A2, A3, A4)]].arbitrary)(f.tupled) + forall(implicitly[Arbitrary[(A1, A2, A3, A4)]].arbitrary)( + liftProp(f.tupled)) } def forall[ @@ -57,14 +72,16 @@ trait Checkers[F[_]] { A2: Arbitrary: Show, A3: Arbitrary: Show, A4: Arbitrary: Show, - A5: Arbitrary: Show - ](f: (A1, A2, A3, A4, A5) => Prop)( + A5: Arbitrary: Show, + B: PropF + ](f: (A1, A2, A3, A4, A5) => B)( implicit loc: SourceLocation): F[Expectations] = { implicit val tuple3Show: Show[(A1, A2, A3, A4, A5)] = { case (a1, a2, a3, a4, a5) => s"(${a1.show},${a2.show},${a3.show},${a4.show},${a5.show})" } - forall(implicitly[Arbitrary[(A1, A2, A3, A4, A5)]].arbitrary)(f.tupled) + forall(implicitly[Arbitrary[(A1, A2, A3, A4, A5)]].arbitrary)( + liftProp(f.tupled)) } def forall[ @@ -73,24 +90,26 @@ trait Checkers[F[_]] { A3: Arbitrary: Show, A4: Arbitrary: Show, A5: Arbitrary: Show, - A6: Arbitrary: Show - ](f: (A1, A2, A3, A4, A5, A6) => Prop)( + A6: Arbitrary: Show, + B: PropF + ](f: (A1, A2, A3, A4, A5, A6) => B)( implicit loc: SourceLocation): F[Expectations] = { implicit val tuple3Show: Show[(A1, A2, A3, A4, A5, A6)] = { case (a1, a2, a3, a4, a5, a6) => s"(${a1.show},${a2.show},${a3.show},${a4.show},${a5.show},${a6.show})" } - forall(implicitly[Arbitrary[(A1, A2, A3, A4, A5, A6)]].arbitrary)(f.tupled) + forall(implicitly[Arbitrary[(A1, A2, A3, A4, A5, A6)]].arbitrary)( + liftProp(f.tupled)) } /** ScalaCheck test parameters instance. */ val numbers = fs2.Stream.iterate(1)(_ + 1) - def forall[A: Show](gen: Gen[A])(f: A => Prop)( + def forall[A: Show](gen: Gen[A])(f: A => F[Expectations])( implicit loc: SourceLocation): F[Expectations] = Ref[F].of(Status.start[A]).flatMap(forall_(gen, f)) - private def forall_[A: Show](gen: Gen[A], f: A => Prop)( + private def forall_[A: Show](gen: Gen[A], f: A => F[Expectations])( state: Ref[F, Status[A]])( implicit loc: SourceLocation): F[Expectations] = { paramStream @@ -121,13 +140,15 @@ trait Checkers[F[_]] { private def testOneTupled[T: Show]( gen: Gen[T], state: Ref[F, Status[T]], - f: T => Prop)(ps: (Gen.Parameters, Seed)) = + f: T => F[Expectations])(ps: (Gen.Parameters, Seed)) = testOne(gen, state, f)(ps._1, ps._2) private def testOne[T: Show]( gen: Gen[T], state: Ref[F, Status[T]], - f: T => Prop)(params: Gen.Parameters, seed: Seed): F[Status[T]] = { + f: T => F[Expectations])( + params: Gen.Parameters, + seed: Seed): F[Status[T]] = { Defer[F](self.effect).defer { gen(params, seed) .traverse(x => f(x).map(x -> _)) @@ -183,3 +204,25 @@ trait Checkers[F[_]] { } } + +object Checkers { + trait Prop[F[_], A] { + def lift(a: A): F[Expectations] + } + + object Prop { + def apply[F[_], B](implicit ev: Prop[F, B]): Prop[F, B] = ev + + implicit def wrap[F[_]: Applicative]: Prop[F, Expectations] = + new Prop[F, Expectations] { + def lift(a: Expectations): F[Expectations] = Applicative[F].pure(a) + } + + implicit def unwrapped[F[_]]: Prop[F, F[Expectations]] = + new Prop[F, F[Expectations]] { + def lift(a: F[Expectations]): F[Expectations] = a + } + + } + +} diff --git a/modules/scalacheck/test/src/weaver/scalacheck/CheckersTest.scala b/modules/scalacheck/test/src/weaver/scalacheck/CheckersTest.scala index 12194faa..58d64afd 100644 --- a/modules/scalacheck/test/src/weaver/scalacheck/CheckersTest.scala +++ b/modules/scalacheck/test/src/weaver/scalacheck/CheckersTest.scala @@ -20,7 +20,7 @@ object CheckersTest extends SimpleIOSuite with IOCheckers { } simpleTest("form 1") { - forall { a: Int => + forall { (a: Int) => expect(a * 2 == 2 * a) } } @@ -55,7 +55,13 @@ object CheckersTest extends SimpleIOSuite with IOCheckers { } } - simpleTest("io form") { + simpleTest("IO form (1)") { + forall { (a1: Int) => + IO.sleep(100.millis).map(_ => expect(a1 * 2 == a1 + a1)) + } + } + + simpleTest("IO form (2)") { forall { (a1: Int, a2: Int) => IO.sleep(1.second).map(_ => expect(a1 + a2 == a2 + a1)) } diff --git a/modules/scalacheck/test/src/weaver/scalacheck/PropertyDogFoodTest.scala b/modules/scalacheck/test/src/weaver/scalacheck/PropertyDogFoodTest.scala index 8d9fd9df..079ea980 100644 --- a/modules/scalacheck/test/src/weaver/scalacheck/PropertyDogFoodTest.scala +++ b/modules/scalacheck/test/src/weaver/scalacheck/PropertyDogFoodTest.scala @@ -16,13 +16,25 @@ object PropertyDogFoodTest extends IOSuite { test("Failed property tests get reported properly") { dogfood => for { - (logs, events) <- dogfood.runSuite(Meta.FailedChecks) + results <- dogfood.runSuite(Meta.FailedChecks) + logs = results._1 + events = results._2 } yield { val errorLogs = logs.collect { case LoggedEvent.Error(msg) => msg } exists(errorLogs) { log => - val expectedMessage = "Property test failed on try 5 with input 0" + // Go into software engineering they say + // Learn how to make amazing algorithms + // Build robust and deterministic software + val (attempt, value) = + if (ScalaCompat.isScala3) + ("4", "-2147483648") + else + ("2", "0") + + val expectedMessage = + s"Property test failed on try $attempt with input $value" expect(log.contains(expectedMessage)) } } @@ -31,8 +43,8 @@ object PropertyDogFoodTest extends IOSuite { // 100 checks sleeping 1 second each should not take 100 seconds test("Checks are parallelised") { dogfood => for { - (_, events) <- dogfood.runSuite(Meta.ParallelChecks) - _ <- expect(events.size == 1).failFast + events <- dogfood.runSuite(Meta.ParallelChecks).map(_._2) + _ <- expect(events.size == 1).failFast } yield { expect(events.headOption.get.duration() < 10000) } @@ -57,10 +69,10 @@ object Meta { object FailedChecks extends SimpleIOSuite with IOCheckers { override def checkConfig: CheckConfig = - super.checkConfig.copy(perPropertyParallelism = 1, initialSeed = Some(1L)) + super.checkConfig.copy(perPropertyParallelism = 1, initialSeed = Some(5L)) simpleTest("foobar") { - forall { x: Int => + forall { (x: Int) => expect(x > 0) } } diff --git a/project/WeaverPlugin.scala b/project/WeaverPlugin.scala index 25c8dbf4..d3871841 100644 --- a/project/WeaverPlugin.scala +++ b/project/WeaverPlugin.scala @@ -16,6 +16,9 @@ import org.scalajs.sbtplugin.ScalaJSPlugin import scala.collection.immutable.Nil import java.util.regex.MatchResult import lmcoursier.definitions.Reconciliation.SemVer +import sbt.VirtualAxis.ScalaVersionAxis +import _root_.scalafix.sbt.ScalafixPlugin +import org.scalafmt.sbt.ScalafmtPlugin case class CatsEffectAxis(idSuffix: String, directorySuffix: String) extends VirtualAxis.WeakAxis @@ -28,48 +31,80 @@ object WeaverPlugin extends AutoPlugin { val CatsEffect2Axis = CatsEffectAxis("_CE2", "ce2") val CatsEffect3Axis = CatsEffectAxis("_CE3", "ce3") - val defaults = Seq[VirtualAxis]( - CatsEffect2Axis, - VirtualAxis.jvm, - VirtualAxis.scalaVersionAxis(WeaverPlugin.scala213, "2.13")) - implicit final class ProjectMatrixOps(pmx: ProjectMatrix) { - def onlyCatsEffect2(withJs: Boolean = true) = { - val tmp = pmx.defaultAxes(defaults: _*) - .customRow( - scalaVersions = WeaverPlugin.supportedScalaVersions, - axisValues = Seq(CatsEffect2Axis, VirtualAxis.jvm), - Seq() - ) - if (withJs) - tmp.customRow( - scalaVersions = WeaverPlugin.supportedScalaVersions, - axisValues = Seq(CatsEffect2Axis, VirtualAxis.js), - configureScalaJSProject(_) + type ConfigureX = ProjectMatrix => ProjectMatrix + type Configure = Project => Project + + val defaults = Seq[VirtualAxis]( + CatsEffect2Axis, + VirtualAxis.jvm, + VirtualAxis.scalaVersionAxis(WeaverPlugin.scala213, "2.13")) + + def addOne( + scalaVersion: String, + platform: VirtualAxis.PlatformAxis, + catsEffectAxis: CatsEffectAxis): ConfigureX = { + projectMatrix => + val addScalafix: Configure = + if (scalaVersion == scala213) + (_: Project).enablePlugins(ScalafixPlugin) + else (_: Project).disablePlugins(ScalafixPlugin) + + val addScalafmt: Configure = + if (scalaVersion == scala213) + (_: Project).enablePlugins(ScalafmtPlugin) + else (_: Project).disablePlugins(ScalafmtPlugin) + + val scalaJSSettings: Configure = + if (platform == VirtualAxis.js) configureScalaJSProject else identity + + val ce3VersionOverride: Configure = + if (catsEffectAxis == CatsEffect3Axis) + _.settings(versionOverrideForCE3) + else identity + + val configureProject = + addScalafix andThen addScalafmt andThen scalaJSSettings andThen ce3VersionOverride + + projectMatrix.defaultAxes(defaults: _*).customRow( + scalaVersions = List(scalaVersion), + axisValues = Seq(catsEffectAxis, platform), + configureProject ) - else tmp } - def crossCatsEffect = { - pmx.defaultAxes(defaults: _*) - .customRow( - scalaVersions = WeaverPlugin.supportedScalaVersions, - axisValues = Seq(CatsEffect2Axis, VirtualAxis.jvm), - Seq() - ).customRow( - scalaVersions = WeaverPlugin.supportedScalaVersions, - axisValues = Seq(CatsEffect3Axis, VirtualAxis.jvm), - versionOverrideForCE3 - ).customRow( - scalaVersions = WeaverPlugin.supportedScalaVersions, - axisValues = Seq(CatsEffect2Axis, VirtualAxis.js), - configureScalaJSProject(_) - ).customRow( - scalaVersions = WeaverPlugin.supportedScalaVersions, - axisValues = Seq(CatsEffect3Axis, VirtualAxis.js), - configureScalaJSProject(_).settings(versionOverrideForCE3) - ) + def add( + scalaVersions: Iterable[String], + platform: VirtualAxis.PlatformAxis, + catsEffectAxis: CatsEffectAxis): ConfigureX = { + scalaVersions.map(addOne(_, platform, catsEffectAxis)).reduce(_ andThen _) + } + def full = sparse(true, true, true) + + def sparse( + withCE3: Boolean, + withJS: Boolean, + withScala3: Boolean + ): ProjectMatrix = { + val defaultScalaVersions = supportedScala2Versions + val defaultPlatform = List(VirtualAxis.jvm) + val defaultCE = List(CatsEffect2Axis) + + val addJs = if (withJS) List(VirtualAxis.js) else Nil + val addScala3 = if (withScala3) List(scala3) else Nil + val addCE3 = if (withCE3) List(CatsEffect3Axis) else Nil + + val configurators = for { + scalaVersion <- defaultScalaVersions ++ addScala3 + platform <- defaultPlatform ++ addJs + catsEffect <- defaultCE ++ addCE3 + } yield addOne(scalaVersion, platform, catsEffect) + + val configure: ConfigureX = configurators.reduce(_ andThen _) + + configure(pmx) } + } lazy val versionOverrideForCE3: Seq[Def.Setting[_]] = Seq( @@ -118,40 +153,59 @@ object WeaverPlugin extends AutoPlugin { lazy val scala212 = "2.12.12" lazy val scala213 = "2.13.3" - lazy val supportedScalaVersions = List(scala212, scala213) + lazy val scala3 = "3.0.0-M3" + lazy val supportedScalaVersions = List(scala212, scala213, scala3) + + lazy val supportedScala2Versions = List(scala212, scala213) /** @see [[sbt.AutoPlugin]] */ override val projectSettings = Seq( moduleName := s"weaver-${name.value}", - // crossScalaVersions := supportedScalaVersions, scalacOptions ++= compilerOptions(scalaVersion.value), Test / scalacOptions ~= (_ filterNot (_ == "-Xfatal-warnings")), // Turning off fatal warnings for ScalaDoc, otherwise we can't release. Compile / doc / scalacOptions ~= (_ filterNot (_ == "-Xfatal-warnings")), // ScalaDoc settings autoAPIMappings := true, - ThisBuild / scalacOptions ++= Seq( - // Note, this is used by the doc-source-url feature to determine the - // relative path of a given source file. If it's not a prefix of a the - // absolute path of the source file, the absolute path of that file - // will be put into the FILE_SOURCE variable, which is - // definitely not what we want. - "-sourcepath", - file(".").getAbsolutePath.replaceAll("[.]$", "") - ), + ThisBuild / scalacOptions ++= { + if (!(ThisBuild / scalacOptions).value.contains("-sourcepath")) + Seq( + // Note, this is used by the doc-source-url feature to determine the + // relative path of a given source file. If it's not a prefix of a the + // absolute path of the source file, the absolute path of that file + // will be put into the FILE_SOURCE variable, which is + // definitely not what we want. + "-sourcepath", + file(".").getAbsolutePath.replaceAll("[.]$", "") + ) + else Seq.empty + }, // https://github.com/sbt/sbt/issues/2654 incOptions := incOptions.value.withLogRecompileOnMacro(false), // https://scalacenter.github.io/scalafix/docs/users/installation.html - semanticdbEnabled := true, + semanticdbEnabled := !scalaVersion.value.startsWith("3.0"), semanticdbVersion := scalafixSemanticdb.revision, - addCompilerPlugin("com.olegpy" %% "better-monadic-for" % "0.3.1"), - addCompilerPlugin( - "org.typelevel" %% "kind-projector" % "0.11.2" cross CrossVersion.full - ) + libraryDependencies ++= { + if (scalaVersion.value.startsWith("3.")) Seq.empty + else Seq( + compilerPlugin("com.olegpy" %% "better-monadic-for" % "0.3.1"), + compilerPlugin( + "org.typelevel" %% "kind-projector" % "0.11.2" cross CrossVersion.full + ) + ) + } ) ++ coverageSettings ++ publishSettings def compilerOptions(scalaVersion: String) = { - commonCompilerOptions ++ { + val allowed = + if (scalaVersion.startsWith("3.")) + commonCompilerOptions.filterNot(flg => + flg.contains("explaintypes") || flg.contains( + "-Xlint") || flg.contains( + "-Ywarn-") || flg.contains("-Xcheckinit")) + else commonCompilerOptions + + allowed ++ { if (priorTo2_13(scalaVersion)) compilerOptions2_12_Only else Seq.empty } @@ -244,6 +298,7 @@ object WeaverPlugin extends AutoPlugin { case "coverage" => pr case _ => pr.disablePlugins(scoverage.ScoverageSbtPlugin) } + withCoverage } @@ -268,6 +323,9 @@ object WeaverPlugin extends AutoPlugin { case VirtualAxis.jvm => List("", "-jvm") case CatsEffect3Axis => List("", "-ce3") case CatsEffect2Axis => List("", "-ce2") + case ScalaVersionAxis(ver, _) => + if (ver.startsWith("3.")) List("", "-scala-3") + else List("", "-scala-2") }.toList def sequence[A](ll: List[List[A]]): List[List[A]] = diff --git a/project/plugins.sbt b/project/plugins.sbt index da42b58c..e464f8a4 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -12,3 +12,4 @@ addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.2") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.6.1") addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.2.14") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.10.0") +addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.5.1")